From a4ac9f966419990e764ee77cde1b88b5db660fd9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:33:46 +0000 Subject: [PATCH 1/5] fix: XSS security, panic protection, import/export, search P0 Security: - Fix XSS in loadPrompts() - escape prompt.name, content, tags with escapeHtml() - Fix XSS in showEditPromptModal() - use DOM APIs instead of string interpolation - Remove unused windows crate from root Cargo.toml (service has its own) P1 Features: - Add service thread panic protection with auto-restart (catch_unwind + 3s retry) - Add prompt export (export_prompts command + download JSON) - Add prompt import (import_prompts command + file picker UI) - Add main panel search (search_prompts command + search bar UI) Quick fixes: - Remove duplicate insertBefore call - Remove duplicate innerHTML assignment Co-Authored-By: Ha AI <1134180104@qq.com> --- Cargo.lock | 1 - Cargo.toml | 1 - src/index.html | 19 ++++- src/main.rs | 156 +++++++++++++++++++++++++++++++++++++-- src/main_simple.js | 178 +++++++++++++++++++++++++++++++++++++-------- 5 files changed, 315 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ac4ed1..0ac183b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3126,7 +3126,6 @@ dependencies = [ "tauri-plugin-shell", "tauri-plugin-single-instance", "tokio", - "windows 0.52.0", "winres", ] diff --git a/Cargo.toml b/Cargo.toml index 043fbaf..497e1bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/index.html b/src/index.html index 62f84b8..9554f72 100644 --- a/src/index.html +++ b/src/index.html @@ -38,6 +38,20 @@

使用帮助

提示词管理

+
+ + + +
+ + + - \ No newline at end of file + diff --git a/src/main.rs b/src/main.rs index bb482d8..2d03715 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); + }); + 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::() { + s.clone() + } else { + "未知错误".to_string() + }; + eprintln!("❌ [ENGINE] 服务线程崩溃: {},3秒后自动重启...", msg); + } + } + // 短暂等待后自动重启 + std::thread::sleep(std::time::Duration::from_secs(3)); + eprintln!("🔄 [ENGINE] 正在重启服务线程..."); + } }); // 设置为已激活 @@ -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| { // 创建系统托盘菜单 @@ -1221,4 +1245,126 @@ fn restart_service(app: AppHandle) -> Result { Ok(()) => Ok("服务已重启".to_string()), Err(e) => Err(e) } -} \ No newline at end of file +} + +// === Import/Export === + +#[derive(Serialize, Deserialize, Debug)] +struct ExportData { + version: String, + exported_at: String, + prompts: Vec, +} + +#[tauri::command] +fn export_prompts() -> Result { + 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 = stmt.query_map([], |row| { + let tags_str: Option = 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))? + .filter_map(|r| r.ok()) + .collect(); + + 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 { + 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, + 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()) +} + +// === Search === + +#[tauri::command] +fn search_prompts(query: String) -> Result, 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 = stmt.query_map([&search_pattern], |row| { + let tags_str: Option = 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))? + .filter_map(|r| r.ok()) + .collect(); + + Ok(prompts) +} diff --git a/src/main_simple.js b/src/main_simple.js index 4fdcb42..78d7a42 100644 --- a/src/main_simple.js +++ b/src/main_simple.js @@ -612,8 +612,6 @@ function bindFunctionButtons() { const logsToolbar = document.querySelector('.logs-toolbar'); if (logsToolbar && clearLogsBtn) { logsToolbar.insertBefore(refreshLogsBtn, clearLogsBtn); - logsToolbar.insertBefore(refreshLogsBtn, clearLogsBtn); - updateDebugInfo('已添加刷新日志按钮'); } @@ -669,6 +667,118 @@ function bindFunctionButtons() { updateDebugInfo('已绑定日志搜索按钮'); } + // === Prompt Search === + const promptSearchBtn = document.getElementById('prompt-search-btn'); + const promptSearchInput = document.getElementById('prompt-search'); + if (promptSearchBtn && promptSearchInput) { + const doSearch = async () => { + const query = promptSearchInput.value.trim(); + if (!query) { + await loadPrompts(); + return; + } + updateDebugInfo(`搜索提示词: ${query}`); + try { + const prompts = await safeInvoke('search_prompts', { query }); + updateDebugInfo(`搜索到 ${prompts.length} 个结果`); + const promptList = document.querySelector('.prompt-list'); + if (!promptList) return; + if (prompts.length === 0) { + promptList.innerHTML = `

未找到匹配的提示词

`; + } else { + const promptsHtml = prompts.map(prompt => ` +
+
+

${escapeHtml(prompt.name)}

+
+ + + +
+
+
+

${escapeHtml(prompt.content.substring(0, 100))}${prompt.content.length > 100 ? '...' : ''}

+
+ ${prompt.tags && prompt.tags.length > 0 ? ` +
+ ${prompt.tags.map(tag => `${escapeHtml(tag)}`).join('')} +
+ ` : ''} +
+ `).join(''); + promptList.innerHTML = promptsHtml; + } + } catch (err) { + updateDebugInfo(`搜索失败: ${err}`); + showNotification('搜索失败: ' + err, 'error'); + } + }; + promptSearchBtn.addEventListener('click', doSearch); + promptSearchInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') doSearch(); + }); + // Clear search restores full list + promptSearchInput.addEventListener('input', (e) => { + if (!e.target.value.trim()) loadPrompts(); + }); + updateDebugInfo('已绑定提示词搜索功能'); + } + + // === Export === + const exportBtn = document.getElementById('export-btn'); + if (exportBtn) { + exportBtn.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + updateDebugInfo('导出提示词按钮被点击'); + try { + const jsonStr = await safeInvoke('export_prompts'); + const blob = new Blob([jsonStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `promptkey-export-${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + showNotification('导出成功', 'success'); + } catch (err) { + updateDebugInfo('导出失败: ' + err); + showNotification('导出失败: ' + err, 'error'); + } + }); + updateDebugInfo('已绑定导出按钮'); + } + + // === Import === + const importBtn = document.getElementById('import-btn'); + const importFileInput = document.getElementById('import-file-input'); + if (importBtn && importFileInput) { + importBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + importFileInput.click(); + }); + importFileInput.addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) return; + updateDebugInfo(`导入文件: ${file.name}`); + try { + const text = await file.text(); + const result = await safeInvoke('import_prompts', { jsonData: text }); + showNotification(result, 'success'); + updateDebugInfo(`导入结果: ${result}`); + await loadPrompts(); + } catch (err) { + updateDebugInfo('导入失败: ' + err); + showNotification('导入失败: ' + err, 'error'); + } + importFileInput.value = ''; + }); + updateDebugInfo('已绑定导入按钮'); + } + // T1-009: View Mode Toggle (Segmented Control) const viewToggle = document.getElementById('view-toggle'); const segments = viewToggle?.querySelectorAll('.segment'); @@ -1004,7 +1114,7 @@ async function loadPrompts() { const promptsHtml = prompts.map(prompt => `
-

${prompt.name}

+

${escapeHtml(prompt.name)}

@@ -1013,11 +1123,11 @@ async function loadPrompts() {
-

${prompt.content.substring(0, 100)}${prompt.content.length > 100 ? '...' : ''}

+

${escapeHtml(prompt.content.substring(0, 100))}${prompt.content.length > 100 ? '...' : ''}

${prompt.tags && prompt.tags.length > 0 ? `
- ${prompt.tags.map(tag => `${tag}`).join('')} + ${prompt.tags.map(tag => `${escapeHtml(tag)}`).join('')}
` : ''}
@@ -1025,8 +1135,6 @@ async function loadPrompts() { promptList.innerHTML = promptsHtml; - promptList.innerHTML = promptsHtml; - // Selection logic removed (T1-008) } @@ -1123,38 +1231,44 @@ function showEditPromptModal(prompt) { // 创建模态框HTML const tagsString = prompt.tags ? prompt.tags.join(', ') : ''; - const modalHtml = ` -