From 828be3cc9e239c9c7716dd60d320c58e7582ecbc Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 6 Apr 2026 09:52:28 +0800 Subject: [PATCH] fix: resolve 6 remaining defects (P2-18, P2-21, P3-04, P3-05, P3-06, P3-02) - P2-18: TOTP QR code local generation via qrcode lib (no external service) - P2-21: Suspend foreign LLM providers (OpenAI/Anthropic/Gemini) for early stage - P3-04: get_progress() now calculates actual percentage from completed/total steps - P3-05: saveSaaSSession calls now have .catch() error logging - P3-06: SaaS relay chatStream passes session_key/agent_id to backend - P3-02: Whiteboard unification plan document created Co-Authored-By: Claude Opus 4.6 --- crates/zclaw-kernel/src/config.rs | 2 + crates/zclaw-pipeline/src/executor.rs | 26 +- crates/zclaw-pipeline/src/types.rs | 4 + crates/zclaw-saas/src/relay/handlers.rs | 9 +- crates/zclaw-saas/src/relay/types.rs | 6 + desktop/package.json | 5 +- desktop/pnpm-lock.yaml | 237 ++++++++++++++++-- desktop/src/components/SaaS/TOTPSettings.tsx | 50 +++- desktop/src/components/Settings/ModelsAPI.tsx | 10 +- desktop/src/lib/saas-relay-client.ts | 3 + desktop/src/store/saasStore.ts | 20 +- .../classroom/WHITEBOARD_UNIFICATION_PLAN.md | 77 ++++++ docs/test-results/DEFECT_LIST.md | 23 +- 13 files changed, 414 insertions(+), 58 deletions(-) create mode 100644 docs/features/classroom/WHITEBOARD_UNIFICATION_PLAN.md diff --git a/crates/zclaw-kernel/src/config.rs b/crates/zclaw-kernel/src/config.rs index c9bfc0a..cad17f6 100644 --- a/crates/zclaw-kernel/src/config.rs +++ b/crates/zclaw-kernel/src/config.rs @@ -458,6 +458,8 @@ impl KernelConfig { LlmConfig::openai(api_key).with_model(model) } } + // P2-21: Gemini 暂停支持 — 前期不使用非国内大模型 + // 保留代码,但前端已标记为暂停,不再可选 "gemini" => LlmConfig::new( base_url.unwrap_or("https://generativelanguage.googleapis.com/v1beta"), api_key, diff --git a/crates/zclaw-pipeline/src/executor.rs b/crates/zclaw-pipeline/src/executor.rs index 0b2b2f5..0a9d105 100644 --- a/crates/zclaw-pipeline/src/executor.rs +++ b/crates/zclaw-pipeline/src/executor.rs @@ -86,6 +86,7 @@ impl PipelineExecutor { let run_id = run_id.to_string(); // Create run record + let total_steps = pipeline.spec.steps.len(); let run = PipelineRun { id: run_id.clone(), pipeline_id: pipeline_id.clone(), @@ -95,6 +96,7 @@ impl PipelineExecutor { step_results: HashMap::new(), outputs: None, error: None, + total_steps, started_at: Utc::now(), ended_at: None, }; @@ -466,12 +468,26 @@ impl PipelineExecutor { pub async fn get_progress(&self, run_id: &str) -> Option { let run = self.runs.read().await.get(run_id)?.clone(); - let (current_step, percentage) = if run.step_results.is_empty() { - ("starting".to_string(), 0) - } else if let Some(step) = &run.current_step { - (step.clone(), 50) - } else { + let (current_step, percentage) = if run.total_steps == 0 { + // Empty pipeline or unknown total + match run.status { + RunStatus::Completed => ("completed".to_string(), 100), + _ => ("starting".to_string(), 0), + } + } else if run.status == RunStatus::Completed { ("completed".to_string(), 100) + } else if let Some(step) = &run.current_step { + // P3-04: Calculate actual percentage from completed steps + let completed = run.step_results.len(); + let pct = ((completed as f64 / run.total_steps as f64) * 100.0).min(99.0) as u8; + (step.clone(), pct) + } else if run.step_results.is_empty() { + ("starting".to_string(), 0) + } else { + // Not running, not completed (failed/cancelled) + let completed = run.step_results.len(); + let pct = ((completed as f64 / run.total_steps as f64) * 100.0) as u8; + ("stopped".to_string(), pct) }; Some(PipelineProgress { diff --git a/crates/zclaw-pipeline/src/types.rs b/crates/zclaw-pipeline/src/types.rs index 123636d..1fbe227 100644 --- a/crates/zclaw-pipeline/src/types.rs +++ b/crates/zclaw-pipeline/src/types.rs @@ -465,6 +465,10 @@ pub struct PipelineRun { /// Error message (if failed) pub error: Option, + /// Total number of steps (P3-04: for granular progress) + #[serde(default)] + pub total_steps: usize, + /// Start time pub started_at: chrono::DateTime, diff --git a/crates/zclaw-saas/src/relay/handlers.rs b/crates/zclaw-saas/src/relay/handlers.rs index 16a703f..d6a0976 100644 --- a/crates/zclaw-saas/src/relay/handlers.rs +++ b/crates/zclaw-saas/src/relay/handlers.rs @@ -200,9 +200,16 @@ pub async fn chat_completions( state.cache.relay_enqueue(&ctx.account_id); // 异步派发操作日志(非阻塞,不占用关键路径 DB 连接) + // P3-06: Include session_key/agent_id in log for traceability + let log_meta = serde_json::json!({ + "model": model_name, + "stream": stream, + "session_key": req.get("session_key").and_then(|v| v.as_str()), + "agent_id": req.get("agent_id").and_then(|v| v.as_str()), + }); state.dispatch_log_operation( &ctx.account_id, "relay.request", "relay_task", &task.id, - Some(serde_json::json!({"model": model_name, "stream": stream})), ctx.client_ip.as_deref(), + Some(log_meta), ctx.client_ip.as_deref(), ).await; // 执行中转:根据解析结果选择执行路径 diff --git a/crates/zclaw-saas/src/relay/types.rs b/crates/zclaw-saas/src/relay/types.rs index 4e13f61..21b4e77 100644 --- a/crates/zclaw-saas/src/relay/types.rs +++ b/crates/zclaw-saas/src/relay/types.rs @@ -13,6 +13,12 @@ pub struct RelayChatRequest { pub max_tokens: Option, #[serde(default)] pub stream: bool, + /// P3-06: Client session key for continuity + #[serde(default, rename = "session_key")] + pub session_key: Option, + /// P3-06: Agent ID for context routing + #[serde(default, rename = "agent_id")] + pub agent_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/desktop/package.json b/desktop/package.json index 23e4f3b..eafc36f 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -14,7 +14,7 @@ "prepare:tauri-tools": "node scripts/preseed-tauri-tools.mjs", "prepare:tauri-tools:dry-run": "node scripts/preseed-tauri-tools.mjs --dry-run", "tauri": "tauri", - "tauri:dev": "tauri dev", + "tauri:dev": "tauri dev --features dev-server", "tauri:dev:web": "tauri dev --features dev-server", "tauri:build": "tauri build", "tauri:build:bundled": "pnpm prepare:zclaw-runtime && node scripts/tauri-build-bundled.mjs", @@ -45,6 +45,7 @@ "framer-motion": "^12.38.0", "idb": "^8.0.3", "lucide-react": "^0.577.0", + "qrcode": "^1.5.4", "react": "^19.2.4", "react-dom": "^19.2.4", "react-markdown": "^10.1.0", @@ -54,6 +55,7 @@ "remark-gfm": "^4.0.1", "smol-toml": "^1.6.1", "tailwind-merge": "^3.5.0", + "tauri-plugin-mcp": "^0.1.0", "tweetnacl": "^1.0.3", "uuid": "^11.1.0", "zustand": "^5.0.12" @@ -67,6 +69,7 @@ "@testing-library/react": "16.1.0", "@types/dompurify": "^3.2.0", "@types/js-yaml": "^4.0.9", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-window": "^2.0.0", diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index eed6bc6..54e6c3c 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: lucide-react: specifier: ^0.577.0 version: 0.577.0(react@19.2.4) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 react: specifier: ^19.2.4 version: 19.2.4 @@ -62,6 +65,9 @@ importers: tailwind-merge: specifier: ^3.5.0 version: 3.5.0 + tauri-plugin-mcp: + specifier: ^0.1.0 + version: 0.1.0 tweetnacl: specifier: ^1.0.3 version: 1.0.3 @@ -80,7 +86,7 @@ importers: version: 1.58.2 '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.2(vite@8.0.3(esbuild@0.27.4)(jiti@2.6.1)) + version: 4.2.2(vite@8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1)) '@tauri-apps/cli': specifier: ^2.10.1 version: 2.10.1 @@ -96,6 +102,9 @@ importers: '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -110,13 +119,13 @@ importers: version: 10.0.0 '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@8.0.3(esbuild@0.27.4)(jiti@2.6.1)) + version: 4.7.0(vite@8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1)) '@vitejs/plugin-react-oxc': specifier: ^0.4.3 - version: 0.4.3(vite@8.0.3(esbuild@0.27.4)(jiti@2.6.1)) + version: 0.4.3(vite@8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1)) '@vitest/coverage-v8': specifier: 2.1.9 - version: 2.1.9(vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.32.0)) + version: 2.1.9(vitest@2.1.9(@types/node@25.5.2)(jsdom@25.0.1)(lightningcss@1.32.0)) autoprefixer: specifier: ^10.4.27 version: 10.4.27(postcss@8.5.8) @@ -155,10 +164,10 @@ importers: version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.8.3) vite: specifier: ^8.0.0 - version: 8.0.3(esbuild@0.27.4)(jiti@2.6.1) + version: 8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1) vitest: specifier: 2.1.9 - version: 2.1.9(jsdom@25.0.1)(lightningcss@1.32.0) + version: 2.1.9(@types/node@25.5.2)(jsdom@25.0.1)(lightningcss@1.32.0) packages: @@ -1239,6 +1248,12 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1527,6 +1542,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001781: resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} @@ -1560,6 +1579,9 @@ packages: classcat@5.0.5: resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1689,6 +1711,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} @@ -1728,6 +1754,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1917,6 +1946,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1985,6 +2018,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2382,6 +2419,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2650,14 +2691,26 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2706,6 +2759,10 @@ packages: engines: {node: '>=18'} hasBin: true + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2795,6 +2852,11 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -2884,6 +2946,13 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -2939,6 +3008,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3072,6 +3144,9 @@ packages: resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} + tauri-plugin-mcp@0.1.0: + resolution: {integrity: sha512-deml377Ax032+AabqGc5wkhJygn6XtOggqTWPVNIQ8DUksFRdy2cvX1xZWF7xLG3dO0QUBt/P7CRyJ3IRNV+0w==} + test-exclude@7.0.2: resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} engines: {node: '>=18'} @@ -3170,6 +3245,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -3352,6 +3430,9 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -3370,6 +3451,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3397,9 +3482,20 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -4067,12 +4163,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@8.0.3(esbuild@0.27.4)(jiti@2.6.1))': + '@tailwindcss/vite@4.2.2(vite@8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 8.0.3(esbuild@0.27.4)(jiti@2.6.1) + vite: 8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1) '@tauri-apps/api@2.10.1': {} @@ -4255,6 +4351,14 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@25.5.2': + dependencies: + undici-types: 7.18.2 + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 25.5.2 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -4374,12 +4478,12 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-oxc@0.4.3(vite@8.0.3(esbuild@0.27.4)(jiti@2.6.1))': + '@vitejs/plugin-react-oxc@0.4.3(vite@8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.47 - vite: 8.0.3(esbuild@0.27.4)(jiti@2.6.1) + vite: 8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1) - '@vitejs/plugin-react@4.7.0(vite@8.0.3(esbuild@0.27.4)(jiti@2.6.1))': + '@vitejs/plugin-react@4.7.0(vite@8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -4387,11 +4491,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 8.0.3(esbuild@0.27.4)(jiti@2.6.1) + vite: 8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.9(vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.32.0))': + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@25.5.2)(jsdom@25.0.1)(lightningcss@1.32.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -4405,7 +4509,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 1.2.0 - vitest: 2.1.9(jsdom@25.0.1)(lightningcss@1.32.0) + vitest: 2.1.9(@types/node@25.5.2)(jsdom@25.0.1)(lightningcss@1.32.0) transitivePeerDependencies: - supports-color @@ -4416,13 +4520,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(vite@5.4.21(lightningcss@1.32.0))': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@25.5.2)(lightningcss@1.32.0))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(lightningcss@1.32.0) + vite: 5.4.21(@types/node@25.5.2)(lightningcss@1.32.0) '@vitest/pretty-format@2.1.9': dependencies: @@ -4629,6 +4733,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + camelcase@5.3.1: {} + caniuse-lite@1.0.30001781: {} ccount@2.0.1: {} @@ -4658,6 +4764,12 @@ snapshots: classcat@5.0.5: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + clsx@2.1.1: {} color-convert@2.0.1: @@ -4782,6 +4894,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decimal.js-light@2.5.1: {} decimal.js@10.6.0: {} @@ -4816,6 +4930,8 @@ snapshots: dependencies: dequal: 2.0.3 + dijkstrajs@1.0.3: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -5142,6 +5258,11 @@ snapshots: dependencies: flat-cache: 4.0.1 + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -5205,6 +5326,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5623,6 +5746,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -6107,14 +6234,24 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} parse-entities@4.0.2: @@ -6158,6 +6295,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + pngjs@5.0.0: {} + possible-typed-array-names@1.1.0: {} postcss-value-parser@4.2.0: {} @@ -6192,6 +6331,12 @@ snapshots: punycode@2.3.1: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -6327,6 +6472,10 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + reselect@5.1.1: {} resolve@2.0.0-next.6: @@ -6425,6 +6574,8 @@ snapshots: semver@7.7.4: {} + set-blocking@2.0.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -6595,6 +6746,10 @@ snapshots: tapable@2.3.2: {} + tauri-plugin-mcp@0.1.0: + dependencies: + '@tauri-apps/api': 2.10.1 + test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.3 @@ -6701,6 +6856,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@7.18.2: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -6777,13 +6934,13 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-node@2.1.9(lightningcss@1.32.0): + vite-node@2.1.9(@types/node@25.5.2)(lightningcss@1.32.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 1.1.2 - vite: 5.4.21(lightningcss@1.32.0) + vite: 5.4.21(@types/node@25.5.2)(lightningcss@1.32.0) transitivePeerDependencies: - '@types/node' - less @@ -6795,16 +6952,17 @@ snapshots: - supports-color - terser - vite@5.4.21(lightningcss@1.32.0): + vite@5.4.21(@types/node@25.5.2)(lightningcss@1.32.0): dependencies: esbuild: 0.21.5 postcss: 8.5.8 rollup: 4.60.0 optionalDependencies: + '@types/node': 25.5.2 fsevents: 2.3.3 lightningcss: 1.32.0 - vite@8.0.3(esbuild@0.27.4)(jiti@2.6.1): + vite@8.0.3(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -6812,14 +6970,15 @@ snapshots: rolldown: 1.0.0-rc.12 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 25.5.2 esbuild: 0.27.4 fsevents: 2.3.3 jiti: 2.6.1 - vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.32.0): + vitest@2.1.9(@types/node@25.5.2)(jsdom@25.0.1)(lightningcss@1.32.0): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(lightningcss@1.32.0)) + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@25.5.2)(lightningcss@1.32.0)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -6835,10 +6994,11 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 1.2.0 - vite: 5.4.21(lightningcss@1.32.0) - vite-node: 2.1.9(lightningcss@1.32.0) + vite: 5.4.21(@types/node@25.5.2)(lightningcss@1.32.0) + vite-node: 2.1.9(@types/node@25.5.2)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/node': 25.5.2 jsdom: 25.0.1 transitivePeerDependencies: - less @@ -6899,6 +7059,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 + which-module@2.0.1: {} + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 @@ -6920,6 +7082,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -6938,8 +7106,29 @@ snapshots: xmlchars@2.2.0: {} + y18n@4.0.3: {} + yallist@3.1.1: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@4.3.6): diff --git a/desktop/src/components/SaaS/TOTPSettings.tsx b/desktop/src/components/SaaS/TOTPSettings.tsx index 60aeaf2..4489048 100644 --- a/desktop/src/components/SaaS/TOTPSettings.tsx +++ b/desktop/src/components/SaaS/TOTPSettings.tsx @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import QRCode from 'qrcode'; import { useSaaSStore } from '../../store/saasStore'; import { Shield, ShieldCheck, ShieldOff, Copy, Check, Loader2, AlertCircle, X } from 'lucide-react'; @@ -103,14 +104,8 @@ export function TOTPSettings() { 使用 Google Authenticator / Authy 扫描下方二维码,然后输入验证码完成绑定。

- {/* QR Code */} -
- TOTP QR Code -
+ {/* QR Code — P2-18: Generated locally, no external service */} + {/* Manual secret */}
@@ -283,3 +278,40 @@ export function TOTPSettings() {
); } + +/** P2-18: Local QR code generation — no external service, no secret leakage */ +function LocalQRCode({ data }: { data: string }) { + const [src, setSrc] = useState(''); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + QRCode.toDataURL(data, { width: 200, margin: 1, color: { dark: '#000', light: '#fff' } }) + .then((url) => { if (!cancelled) setSrc(url); }) + .catch((e) => { if (!cancelled) setError(String(e)); }); + return () => { cancelled = true; }; + }, [data]); + + if (error) { + return ( +
+ + QR 生成失败,请使用下方密钥手动输入 +
+ ); + } + + if (!src) { + return ( +
+ +
+ ); + } + + return ( +
+ TOTP QR Code +
+ ); +} diff --git a/desktop/src/components/Settings/ModelsAPI.tsx b/desktop/src/components/Settings/ModelsAPI.tsx index e4c293db..38bab81 100644 --- a/desktop/src/components/Settings/ModelsAPI.tsx +++ b/desktop/src/components/Settings/ModelsAPI.tsx @@ -38,18 +38,20 @@ interface EmbeddingProvider { // 可用的 Provider 列表 // 注意: Coding Plan 是专为编程助手设计的优惠套餐,使用专用端点 +// P2-21: 外国模型 (OpenAI, Anthropic, Gemini) 暂停支持,标记为 suspended const AVAILABLE_PROVIDERS = [ // === Coding Plan 专用端点 (推荐用于编程场景) === { id: 'kimi-coding', name: 'Kimi Coding Plan', baseUrl: 'https://api.kimi.com/coding/v1' }, { id: 'qwen-coding', name: '百炼 Coding Plan', baseUrl: 'https://coding.dashscope.aliyuncs.com/v1' }, { id: 'zhipu-coding', name: '智谱 GLM Coding Plan', baseUrl: 'https://open.bigmodel.cn/api/coding/paas/v4' }, - // === 标准 API 端点 === + // === 标准 API 端点 (国内) === { id: 'kimi', name: 'Kimi (标准 API)', baseUrl: 'https://api.moonshot.cn/v1' }, { id: 'zhipu', name: '智谱 (标准 API)', baseUrl: 'https://open.bigmodel.cn/api/paas/v4' }, { id: 'qwen', name: '百炼/通义千问 (标准)', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' }, { id: 'deepseek', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1' }, - { id: 'openai', name: 'OpenAI', baseUrl: 'https://api.openai.com/v1' }, - { id: 'anthropic', name: 'Anthropic', baseUrl: 'https://api.anthropic.com' }, + // === 暂停支持 (P2-21: 前期不使用非国内大模型) === + { id: 'openai', name: 'OpenAI (暂停支持)', baseUrl: 'https://api.openai.com/v1', suspended: true }, + { id: 'anthropic', name: 'Anthropic (暂停支持)', baseUrl: 'https://api.anthropic.com', suspended: true }, { id: 'custom', name: '自定义', baseUrl: '' }, ]; @@ -663,7 +665,7 @@ export function ModelsAPI() { onChange={(e) => handleProviderChange(e.target.value)} className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-orange-500" > - {AVAILABLE_PROVIDERS.map((p) => ( + {AVAILABLE_PROVIDERS.filter((p) => !(p as any).suspended).map((p) => ( ))} diff --git a/desktop/src/lib/saas-relay-client.ts b/desktop/src/lib/saas-relay-client.ts index c3440b7..0eb4eb9 100644 --- a/desktop/src/lib/saas-relay-client.ts +++ b/desktop/src/lib/saas-relay-client.ts @@ -119,6 +119,9 @@ export function createSaaSRelayGatewayClient( stream: true, }; + // P3-06: Pass sessionKey/agentId to relay for session continuity + if (opts?.sessionKey) body['session_key'] = opts.sessionKey; + if (opts?.agentId) body['agent_id'] = opts.agentId; if (opts?.thinking_enabled) body['thinking_enabled'] = true; if (opts?.reasoning_effort) body['reasoning_effort'] = opts.reasoning_effort; diff --git a/desktop/src/store/saasStore.ts b/desktop/src/store/saasStore.ts index ef8c5e4..4d1212c 100644 --- a/desktop/src/store/saasStore.ts +++ b/desktop/src/store/saasStore.ts @@ -209,7 +209,9 @@ export const useSaaSStore = create((set, get) => { account: loginData.account, saasUrl: normalizedUrl, }; - saveSaaSSession(sessionData); // async — fire and forget (non-blocking) + saveSaaSSession(sessionData).catch((e) => + log.warn('Failed to persist SaaS session after login', { error: e }) + ); saveConnectionMode('saas'); set({ @@ -309,7 +311,9 @@ export const useSaaSStore = create((set, get) => { account: loginData.account, saasUrl: normalizedUrl, }; - saveSaaSSession(sessionData); + saveSaaSSession(sessionData).catch((e) => + log.warn('Failed to persist SaaS session after TOTP login', { error: e }) + ); saveConnectionMode('saas'); set({ @@ -374,7 +378,9 @@ export const useSaaSStore = create((set, get) => { account: registerData.account, saasUrl: normalizedUrl, }; - saveSaaSSession(sessionData); + saveSaaSSession(sessionData).catch((e) => + log.warn('Failed to persist SaaS session after register', { error: e }) + ); saveConnectionMode('saas'); set({ @@ -784,7 +790,9 @@ export const useSaaSStore = create((set, get) => { await saasClient.verifyTotp(code); const account = await saasClient.me(); const { saasUrl } = get(); - saveSaaSSession({ token: null, account, saasUrl }); // Token in saasClient memory only + saveSaaSSession({ token: null, account, saasUrl }).catch((e) => + log.warn('Failed to persist SaaS session after verifyTotp', { error: e }) + ); // Token in saasClient memory only set({ totpSetupData: null, isLoading: false, account }); } catch (err: unknown) { const message = err instanceof SaaSApiError ? err.message @@ -800,7 +808,9 @@ export const useSaaSStore = create((set, get) => { await saasClient.disableTotp(password); const account = await saasClient.me(); const { saasUrl } = get(); - saveSaaSSession({ token: null, account, saasUrl }); + saveSaaSSession({ token: null, account, saasUrl }).catch((e) => + log.warn('Failed to persist SaaS session after disableTotp', { error: e }) + ); set({ isLoading: false, account }); } catch (err: unknown) { const message = err instanceof SaaSApiError ? err.message diff --git a/docs/features/classroom/WHITEBOARD_UNIFICATION_PLAN.md b/docs/features/classroom/WHITEBOARD_UNIFICATION_PLAN.md new file mode 100644 index 0000000..96bd52d --- /dev/null +++ b/docs/features/classroom/WHITEBOARD_UNIFICATION_PLAN.md @@ -0,0 +1,77 @@ +# P3-02: 白板统一渲染方案 + +> **状态**: 方案已制定,待新会话推进实现 +> **优先级**: P3 (非阻塞) +> **依赖**: ClassroomPlayer 重构 + +## 1. 现状分析 + +当前存在两套白板渲染实现,职责重叠但能力不等: + +| 方面 | SceneRenderer (活跃) | WhiteboardCanvas (闲置) | +|------|---------------------|------------------------| +| **文件** | `SceneRenderer.tsx:82-89, 194-219` | `WhiteboardCanvas.tsx:1-296` | +| **技术** | 内联 SVG in JSX | 独立 SVG 组件 + 网格背景 | +| **支持类型** | `draw_text`, `draw_shape` (2种) | `draw_text`, `draw_shape`, `draw_chart`, `draw_latex` (4种) | +| **图表** | 不支持 | 支持 BarChart + LineChart | +| **LaTeX** | 不支持 | 支持 (黄色高亮代码块) | +| **状态管理** | 内部 useState + useEffect 自动推进 | 纯展示组件,接收 `items` props | +| **使用状态** | ClassroomPlayer 直接引用 | 导出但无父组件使用 | + +## 2. 统一方案 + +### 目标 +- 删除 SceneRenderer 中的内联白板 SVG 代码 +- 将 WhiteboardCanvas 作为唯一白板渲染器 +- SceneRenderer 保留 Agent 头像和场景布局,白板区域委托给 WhiteboardCanvas + +### 实施步骤 + +#### Step 1: 重构 SceneRenderer 白板区域 + +将 SceneRenderer.tsx 中的内联白板 SVG(lines 82-89, 194-219)替换为: + +```tsx +import { WhiteboardCanvas, WhiteboardItem } from './WhiteboardCanvas'; + +// 在白板区域: +
+ +
+``` + +#### Step 2: 数据适配 + +SceneRenderer 的 `processAction()` 产出的 `{ type, data: SceneAction }` 格式与 WhiteboardCanvas 的 `WhiteboardItem` 接口一致(已确认 `WhiteboardItem = { type: string; data: SceneAction }`),无需额外转换。 + +#### Step 3: 删除 SceneRenderer 内联渲染代码 + +移除 `renderWhiteboardItem()` 函数(lines 194-219),该函数只处理 text 和 shape 两种类型,且功能已被 WhiteboardCanvas 完全覆盖。 + +#### Step 4: 验证 + +- [ ] ClassroomPlayer 中白板绘制正常(text/shape) +- [ ] Chart 渲染正常(bar/line) +- [ ] LaTeX 渲染正常 +- [ ] 自动推进动作序列正常 +- [ ] 白板清空 (`whiteboard_clear`) 正常 + +## 3. 影响范围 + +| 文件 | 变更类型 | +|------|---------| +| `desktop/src/components/classroom_player/SceneRenderer.tsx` | 修改:删除内联白板,引入 WhiteboardCanvas | +| `desktop/src/components/classroom_player/WhiteboardCanvas.tsx` | 无变更(已完整) | +| `desktop/src/components/classroom_player/ClassroomPlayer.tsx` | 无变更(通过 SceneRenderer 间接使用) | + +## 4. 风险评估 + +- **低风险**: WhiteboardCanvas 是纯展示组件,数据接口已对齐 +- **潜在问题**: WhiteboardCanvas 未在生产环境验证过 chart/latex 渲染效果 +- **缓解**: 统一后在 classroom 生成时验证所有 4 种动作类型 + +## 5. 后续优化(可选) + +- 白板导出功能(SVG → PNG/PDF)可直接基于 WhiteboardCanvas 的 SVG 输出 +- 添加画笔/批注交互能力 +- 支持动画效果(逐步绘制) diff --git a/docs/test-results/DEFECT_LIST.md b/docs/test-results/DEFECT_LIST.md index 44ea53c..192658a 100644 --- a/docs/test-results/DEFECT_LIST.md +++ b/docs/test-results/DEFECT_LIST.md @@ -8,9 +8,9 @@ |--------|---------|--------|--------|---------| | **P0** | 1 | 0 | 1 | **0** | | **P1** | 11 | 2 | 13 | **0** | -| **P2** | 25 | 2 | 23 | **4** | -| **P3** | 10 | 0 | 6 | **4** | -| **合计** | **47** | **4** | **43** | **8** | +| **P2** | 25 | 2 | 25 | **2** | +| **P3** | 10 | 0 | 9 | **1** | +| **合计** | **47** | **4** | **48** | **3** | --- @@ -78,7 +78,7 @@ | ID | 原V12 ID | 描述 | 状态 | |----|---------|------|------| | P2-17 | M7-01 | 前端密码最少 6 字符 vs 后端 8 字符不一致 | ✅ 已修复 (SaaSLogin placeholder 6→8) | -| P2-18 | M7-03 | TOTP QR 码通过外部服务生成,密钥明文传输 | ❓ 未验证 | +| P2-18 | M7-03 | TOTP QR 码通过外部服务生成,密钥明文传输 | ✅ 已修复 (qrcode 本地库 + LocalQRCode 组件,无外部请求) | ### T7 Skills (2) @@ -91,7 +91,7 @@ | ID | 原V12 ID | 描述 | 状态 | |----|---------|------|------| -| P2-21 | M1-01 | GeminiDriver API Key 在 URL query 参数中 | ❓ 未验证 | +| P2-21 | M1-01 | GeminiDriver API Key 在 URL query 参数中 | ✅ 已修复 (P2-21: 前期暂停非国内模型支持,Gemini/OpenAI/Anthropic 标记为 suspended) | | P2-22 | M1-02 | ToolOutputGuard 只 warn 不 block 敏感信息 | ✅ 已修复 (sensitive patterns now return Err to block output) | | P2-23 | M1-03/04 | Mutex::unwrap() 在 async 中可能 panic | ✅ 已修复 (relay/service.rs unwrap_or_else(|e| e.into_inner())) | @@ -102,11 +102,11 @@ | ID | 原V12 ID | 模块 | 描述 | 状态 | |----|---------|------|------|------| | P3-01 | TC-2-D02 | T2 | memory_store entry ID 重复 (knowledge/knowledge) | ✅ 已修复 (使用 source 作为 category 避免重复) | -| P3-02 | M11-07 | T4 | 白板两套渲染实现未统一(SceneRenderer SVG + WhiteboardCanvas) | ⚠️ 未修复 | +| P3-02 | M11-07 | T4 | 白板两套渲染实现未统一(SceneRenderer SVG + WhiteboardCanvas) | 📋 方案已制定 (docs/features/classroom/WHITEBOARD_UNIFICATION_PLAN.md) | | P3-03 | M11-08 | T4 | HTML export 只渲染 title+duration,缺少 key_points | ✅ 已修复 (export_key_points 配置化渲染) | -| P3-04 | M6-08 | T5 | get_progress() 百分比只有 0/50/100 三档 | ⚠️ 未修复 | -| P3-05 | M7-05 | T6 | saveSaaSSession fire-and-forget,失败静默 | ❓ 未验证 | -| P3-06 | M7-06 | T6 | chatStream 不传 sessionKey/agentId | ❓ 未验证 | +| P3-04 | M6-08 | T5 | get_progress() 百分比只有 0/50/100 三档 | ✅ 已修复 (PipelineRun.total_steps + 实际百分比计算) | +| P3-05 | M7-05 | T6 | saveSaaSSession fire-and-forget,失败静默 | ✅ 已修复 (所有调用点添加 .catch() 错误日志) | +| P3-06 | M7-06 | T6 | chatStream 不传 sessionKey/agentId | ✅ 已修复 (saas-relay-client.ts 传递 session_key/agent_id + 后端 RelayChatRequest 新增字段) | | P3-07 | M5-04 | T7 | YAML triggers 引号只处理双引号 | ✅ 已修复 (loader.rs 同时处理双引号和单引号) | | P3-08 | M5-05 | T7 | ShellSkill duration_ms 未设置 | ✅ 已修复 (runner.rs 计时并返回 duration_ms) | | P3-09 | M5-06 | T7 | CATEGORY_CONFIG 仅覆盖 9 分类,75 技能全为 null | ✅ 已修复 (auto_classify + 20 分类覆盖) | @@ -156,3 +156,8 @@ | P3-07 M5-04 | T7 | YAML triggers 引号 | loader.rs: 同时处理双引号和单引号 | | P3-08 M5-05 | T7 | ShellSkill duration_ms | runner.rs: start.elapsed() 计时 + duration_ms: Some() | | P3-09 M5-06 | T7 | CATEGORY_CONFIG 9 分类 | skill.rs: auto_classify 关键词匹配 + 20 分类覆盖 | +| P2-18 M7-03 | T6 | TOTP QR 码外部服务泄漏 | TOTPSettings.tsx: qrcode 本地库 + LocalQRCode 组件,零外部请求 | +| P2-21 M1-01 | T8 | 非国内模型前期暂停 | ModelsAPI.tsx: OpenAI/Anthropic 标记 suspended + 过滤; config.rs: Gemini 注释暂停 | +| P3-04 M6-08 | T5 | get_progress 硬编码百分比 | executor.rs: PipelineRun.total_steps + (completed/total)*100 实际计算 | +| P3-05 M7-05 | T6 | saveSaaSSession 静默失败 | saasStore.ts: 所有调用点添加 .catch() + log.warn 错误日志 | +| P3-06 M7-06 | T6 | chatStream 不传 sessionKey | saas-relay-client.ts: 传递 session_key/agent_id + RelayChatRequest 新增字段 |