Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ serde_json = "1.0"
serde_yaml = "0.9"
tokio = { version = "1.0", features = ["full"] }
tauri = { version = "2.0.0", features = ["tray-icon"] }
windows = { version = "0.52", features = ["Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", "Win32_Foundation"] }
tauri-plugin-shell = "2.0.0"
tauri-plugin-dialog = "2.0.0"
tauri-plugin-fs = "2.0.0"
Expand Down
19 changes: 18 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ <h3>使用帮助</h3>
<div id="prompts-panel" class="panel active">
<div class="panel-header">
<h2>提示词管理</h2>
<div class="prompt-toolbar">
<div class="search-box">
<input type="text" id="prompt-search" placeholder="搜索提示词..." aria-label="搜索提示词">
<button id="prompt-search-btn" class="btn-icon" type="button" title="搜索" aria-label="搜索">
<i class="icon-search"></i>
</button>
</div>
<button id="export-btn" class="secondary-btn" type="button" title="导出提示词">
导出
</button>
<button id="import-btn" class="secondary-btn" type="button" title="导入提示词">
导入
</button>
</div>
<div class="segmented-control" id="view-toggle">
<div class="segment-slider"></div>
<button class="segment active" data-view="card" title="卡片视图">
Expand Down Expand Up @@ -209,8 +223,11 @@ <h3>调试日志</h3>
</main>
</div>

<!-- 隐藏的文件选择器用于导入 -->
<input type="file" id="import-file-input" accept=".json" style="display:none">

<!-- 模态框将由JavaScript动态创建 -->

<script type="module" src="main_simple.js"></script>
</body>
</html>
</html>
165 changes: 160 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,31 @@ impl ServiceState {

println!("🚀 正在启动内嵌提示词引擎 (Embedded Thread)...");

// 启动后台线程运行 Service 逻辑
// 启动后台线程运行 Service 逻辑(带 panic 保护)
std::thread::spawn(|| {
// 注意:service::run_service 内部会处理循环
service::run_service();
loop {
let result = std::panic::catch_unwind(|| {
service::run_service();
});
Comment on lines +96 to +99
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This panic-supervision loop uses catch_unwind, but the workspace Cargo.toml sets [profile.release] panic = "abort", which prevents unwinding in release builds—panics will abort the process and never be caught/restarted. If restart-on-panic is required in production, switch release to panic = "unwind" (and audit the cost), or supervise the service via an external process instead of relying on catch_unwind.

Copilot uses AI. Check for mistakes.
match result {
Ok(_) => {
eprintln!("⚠️ [ENGINE] 服务线程正常退出,尝试重启...");
}
Err(e) => {
let msg = if let Some(s) = e.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = e.downcast_ref::<String>() {
s.clone()
} else {
"未知错误".to_string()
};
eprintln!("❌ [ENGINE] 服务线程崩溃: {},3秒后自动重启...", msg);
}
}
// 短暂等待后自动重启
std::thread::sleep(std::time::Duration::from_secs(3));
eprintln!("🔄 [ENGINE] 正在重启服务线程...");
}
Comment on lines +100 to +118
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful restarting service::run_service() in-process: service::run_service() currently calls env_logger::init_from_env(...), which panics if the logger is already initialized. After the first panic, the restart loop is likely to immediately panic again during logger init. Consider making logger initialization idempotent (e.g., try_init with ignored error) or moving logger init outside the restart loop.

Copilot uses AI. Check for mistakes.
});

// 设置为已激活
Expand Down Expand Up @@ -232,7 +253,10 @@ fn main() {
exit_application,
clear_usage_logs,
toggle_prompt_pin, // Wheel: Toggle pin status
get_all_prompts_with_pin // Wheel: Get prompts with pin status
get_all_prompts_with_pin, // Wheel: Get prompts with pin status
export_prompts, // Import/Export: Export all prompts
import_prompts, // Import/Export: Import prompts from JSON
search_prompts // Search: Search prompts by keyword
])
.setup(|app| {
// 创建系统托盘菜单
Expand Down Expand Up @@ -1221,4 +1245,135 @@ fn restart_service(app: AppHandle) -> Result<String, String> {
Ok(()) => Ok("服务已重启".to_string()),
Err(e) => Err(e)
}
}
}

// === Import/Export ===

#[derive(Serialize, Deserialize, Debug)]
struct ExportData {
version: String,
exported_at: String,
prompts: Vec<Prompt>,
}

#[tauri::command]
fn export_prompts() -> Result<String, String> {
let conn = open_db()?;
let mut stmt = conn.prepare(
"SELECT id, name, tags, content, content_type, variables_json, app_scopes_json, inject_order, version, updated_at FROM prompts"
).map_err(|e| format!("查询失败: {}", e))?;

let prompts: Vec<Prompt> = stmt.query_map([], |row| {
let tags_str: Option<String> = row.get(2)?;
let tags = match tags_str {
Some(s) => Some(
serde_json::from_str::<Vec<String>>(&s).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
2,
rusqlite::types::Type::Text,
Box::new(e),
)
})?
),
None => None,
};
Ok(Prompt {
id: row.get(0)?,
name: row.get(1)?,
tags,
content: row.get(3)?,
content_type: row.get(4)?,
variables_json: row.get(5)?,
app_scopes_json: row.get(6)?,
inject_order: row.get(7)?,
version: row.get(8)?,
updated_at: row.get(9)?,
})
}).map_err(|e| format!("查询失败: {}", e))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("查询失败: {}", e))?;

let export_data = ExportData {
version: "1.0".to_string(),
exported_at: chrono_now_string(),
prompts,
};

serde_json::to_string_pretty(&export_data)
.map_err(|e| format!("序列化失败: {}", e))
}

#[tauri::command]
fn import_prompts(json_data: String) -> Result<String, String> {
let export_data: ExportData = serde_json::from_str(&json_data)
.map_err(|e| format!("JSON 解析失败: {}", e))?;

let conn = open_db()?;
let mut imported = 0;
let mut skipped = 0;

for prompt in &export_data.prompts {
let tags_str = prompt.tags.as_ref().map(|t| t.join(","));
let result = conn.execute(
"INSERT INTO prompts (name, tags, content, content_type, variables_json, app_scopes_json, inject_order, version, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
rusqlite::params![
prompt.name,
tags_str,
prompt.content,
Comment on lines +1315 to +1322
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import_prompts writes tags back to the DB as a comma-joined string, but the rest of the app expects the tags column to contain JSON (see create_prompt/update_prompt which serde_json::to_string the tag array). This will cause imported prompts to lose tags in get_all_prompts (JSON parse fails) and makes search/export inconsistent. Persist tags using the same JSON encoding used by the other CRUD paths.

Copilot uses AI. Check for mistakes.
prompt.content_type,
prompt.variables_json,
prompt.app_scopes_json,
prompt.inject_order,
prompt.version,
prompt.updated_at,
],
);
match result {
Ok(_) => imported += 1,
Err(_) => skipped += 1,
}
}

Ok(format!("导入完成:成功 {} 条,跳过 {} 条", imported, skipped))
}

fn chrono_now_string() -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
format!("{}", now.as_secs())
}
Comment on lines +1340 to +1345
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chrono_now_string() returns a Unix timestamp in seconds, but the name suggests a formatted datetime string. Consider renaming it to reflect what it returns (e.g., unix_timestamp_secs_string) or changing the implementation to emit an ISO-8601/RFC3339 timestamp if the export format expects a human-readable datetime.

Copilot uses AI. Check for mistakes.

// === Search ===

#[tauri::command]
fn search_prompts(query: String) -> Result<Vec<Prompt>, String> {
let conn = open_db()?;
let search_pattern = format!("%{}%", query);
let mut stmt = conn.prepare(
"SELECT id, name, tags, content, content_type, variables_json, app_scopes_json, inject_order, version, updated_at FROM prompts WHERE name LIKE ?1 OR content LIKE ?1 OR tags LIKE ?1"
).map_err(|e| format!("查询失败: {}", e))?;

let prompts: Vec<Prompt> = stmt.query_map([&search_pattern], |row| {
let tags_str: Option<String> = row.get(2)?;
let tags = tags_str.map(|s| {
s.split(',').map(|t| t.trim().to_string()).filter(|t| !t.is_empty()).collect()
});
Ok(Prompt {
id: row.get(0)?,
name: row.get(1)?,
tags,
content: row.get(3)?,
content_type: row.get(4)?,
variables_json: row.get(5)?,
app_scopes_json: row.get(6)?,
inject_order: row.get(7)?,
version: row.get(8)?,
updated_at: row.get(9)?,
})
}).map_err(|e| format!("查询失败: {}", e))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("查询失败: {}", e))?;

Ok(prompts)
}
Loading