foxclaw 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -9,6 +9,7 @@ import {
9
9
  runSearch, say, send, setMode, stop,
10
10
  } from '../stores/chat.js'
11
11
  import { setModule } from '../stores/app.js'
12
+ import { t } from '../lib/i18n.js'
12
13
 
13
14
  const text = ref('')
14
15
  const ta = ref(null)
@@ -17,29 +18,31 @@ const mq = ref('')
17
18
  const hover = ref(0)
18
19
  const skills = ref([])
19
20
 
20
- const CMD = [
21
- { key: '/新建', desc: '开一个新会话', run: () => newSession() },
22
- { key: '/流式', desc: '切到流式对话', run: () => { setMode('stream'); say('已切到流式对话') } },
23
- { key: '/任务', desc: '切到编排任务', run: () => { setMode('orchestrate'); say('已切到编排任务') } },
24
- { key: '/搜索', desc: '检索记忆/知识/联网:/搜索 关键词', arg: true },
25
- { key: '/摄入', desc: '选文件摄入知识库', run: () => requestIngest() },
26
- { key: '/设置', desc: '打开设置面板', run: () => setModule('settings') },
27
- { key: '/停止', desc: '停止当前生成', run: () => stop() },
28
- { key: '/清屏', desc: '只清空当前显示,不动后端', run: () => { chat.messages = [] } },
21
+ // 命令字面量保持中文(输入语法不变),只有说明文字跟随界面语言
22
+ const CMD = computed(() => [
23
+ { key: '/新建', desc: t('chat.cmd.new'), run: () => newSession() },
24
+ { key: '/流式', desc: t('chat.cmd.stream'), run: () => { setMode('stream'); say(t('chat.saidStream')) } },
25
+ { key: '/任务', desc: t('chat.cmd.orchestrate'), run: () => { setMode('orchestrate'); say(t('chat.saidOrchestrate')) } },
26
+ { key: '/搜索', desc: t('chat.cmd.search'), arg: true },
27
+ { key: '/摄入', desc: t('chat.cmd.ingest'), run: () => requestIngest() },
28
+ { key: '/设置', desc: t('chat.cmd.settings'), run: () => setModule('settings') },
29
+ { key: '/停止', desc: t('chat.cmd.stop'), run: () => stop() },
30
+ { key: '/清屏', desc: t('chat.cmd.clear'), run: () => { chat.messages = [] } },
29
31
  {
30
32
  key: '/帮助',
31
- desc: '列出全部命令',
32
- run: () => pushLocal(CMD.map((c) => `${c.key.padEnd(6)} ${c.desc}`).join('\n')),
33
+ desc: t('chat.cmd.help'),
34
+ run: () => pushLocal(CMD.value.map((c) => `${c.key.padEnd(6)} ${c.desc}`).join('\n')),
33
35
  },
34
- ]
36
+ ])
35
37
 
36
38
  const busy = () => chat.streamState === 'streaming'
37
39
 
38
40
  const items = computed(() => {
39
41
  const q = mq.value.toLowerCase()
40
42
  if (menu.value === 'slash') {
41
- return CMD.filter((c) => c.key.slice(1).toLowerCase().includes(q) || c.desc.includes(mq.value))
42
- .map((c) => ({ label: c.key, desc: c.desc, tag: c.arg ? '带参数' : '' }))
43
+ return CMD.value
44
+ .filter((c) => c.key.slice(1).toLowerCase().includes(q) || c.desc.includes(mq.value))
45
+ .map((c) => ({ label: c.key, desc: c.desc, tag: c.arg ? t('chat.tagArg') : '' }))
43
46
  }
44
47
  if (menu.value === 'at') {
45
48
  return skills.value
@@ -49,7 +52,7 @@ const items = computed(() => {
49
52
  return []
50
53
  })
51
54
 
52
- const hint = computed(() => (menu.value === 'slash' ? '命令' : '提及技能'))
55
+ const hint = computed(() => (menu.value === 'slash' ? t('chat.hintSlash') : t('chat.hintAt')))
53
56
 
54
57
  onMounted(async () => {
55
58
  grow()
@@ -68,13 +71,11 @@ onMounted(async () => {
68
71
  })),
69
72
  ]
70
73
  } catch {
71
- /* 拿不到就只留命令面板 */
72
74
  }
73
75
  })
74
76
 
75
77
  onBeforeUnmount(() => window.removeEventListener('resize', onResize))
76
78
 
77
- /* 只看光标左侧的 token:用户在中间补字时也能正确触发 */
78
79
  function syncMenu() {
79
80
  const el = ta.value
80
81
  if (!el) return
@@ -113,7 +114,6 @@ function choose(i) {
113
114
  if (menu.value === 'slash') {
114
115
  const cmd = CMD.find((c) => c.key === it.label)
115
116
  if (cmd && cmd.arg) {
116
- /* 带参数的命令:把命令留在输入框,让用户接着写关键词 */
117
117
  text.value = `${cmd.key} `
118
118
  closeMenu()
119
119
  nextTick(() => { el && el.focus(); grow() })
@@ -126,7 +126,6 @@ function choose(i) {
126
126
  return
127
127
  }
128
128
 
129
- /* @ 技能:把当前 @token 替换成完整技能名 */
130
129
  const idx = before.lastIndexOf('@')
131
130
  const after = text.value.slice(pos)
132
131
  text.value = `${before.slice(0, idx)}@${it.label} ${after}`
@@ -134,13 +133,12 @@ function choose(i) {
134
133
  nextTick(() => { el && el.focus(); grow() })
135
134
  }
136
135
 
137
- /* 纯命令(不带面板)也要能执行 */
138
- function tryCommand(t) {
139
- if (!t.startsWith('/')) return false
140
- const [head, ...rest] = t.split(/\s+/)
141
- const cmd = CMD.find((c) => c.key === head)
136
+ function tryCommand(raw) {
137
+ if (!raw.startsWith('/')) return false
138
+ const [head, ...rest] = raw.split(/\s+/)
139
+ const cmd = CMD.value.find((c) => c.key === head)
142
140
  if (!cmd) {
143
- say(`未知命令:${head}`)
141
+ say(t('chat.unknownCmd', { head }))
144
142
  return true
145
143
  }
146
144
  const arg = rest.join(' ').trim()
@@ -160,8 +158,6 @@ function submit() {
160
158
  send(t)
161
159
  }
162
160
 
163
- /* textarea 高度:聚焦即撑开(约 3 行),内容变多继续长高,到上限才转内部滚动。
164
- 上限跟视口走,窄屏/高分屏都不会把消息流挤没。 */
165
161
  const FOCUS_MIN = 84
166
162
  const focused = ref(false)
167
163
  function capH() {
@@ -175,7 +171,6 @@ function grow() {
175
171
  const min = focused.value ? FOCUS_MIN : 0
176
172
  el.style.height = `${Math.min(Math.max(need, min), capH())}px`
177
173
  }
178
- /* blur 时 document.activeElement 已变,不能用它判断,单独跟一个标志 */
179
174
  function onFocus() {
180
175
  focused.value = true
181
176
  grow()
@@ -240,8 +235,8 @@ function kb(n) {
240
235
  <div v-if="chat.ingest" class="chip ing" :class="chat.ingest.status">
241
236
  <span class="cn">{{ chat.ingest.name }}</span>
242
237
  <span class="cs">
243
- {{ chat.ingest.status === 'done' ? `${chat.ingest.ku} 条`
244
- : chat.ingest.status === 'error' ? '失败'
238
+ {{ chat.ingest.status === 'done' ? t('common.count', { n: chat.ingest.ku })
239
+ : chat.ingest.status === 'error' ? t('chat.ingestFailed')
245
240
  : `${chat.ingest.pct}%` }}
246
241
  </span>
247
242
  <span class="bar"><i :style="{ width: `${chat.ingest.status === 'done' ? 100 : chat.ingest.pct}%` }" /></span>
@@ -265,7 +260,7 @@ function kb(n) {
265
260
  ref="ta"
266
261
  v-model="text"
267
262
  rows="1"
268
- placeholder="告诉我你想做什么… / 命令 · @ 技能 · Enter 发送"
263
+ :placeholder="t('chat.placeholder')"
269
264
  @keydown="onKey"
270
265
  @input="grow"
271
266
  @focus="onFocus"
@@ -273,7 +268,7 @@ function kb(n) {
273
268
  @click="syncMenu"
274
269
  @keyup="syncMenu"
275
270
  />
276
- <button class="send" :class="{ stop: busy() }" :title="busy() ? '停止生成' : '发送'" @click="submit">
271
+ <button class="send" :class="{ stop: busy() }" :title="busy() ? t('chat.stopGen') : t('chat.send')" @click="submit">
277
272
  {{ busy() ? '■' : '➤' }}
278
273
  </button>
279
274
  </div>
@@ -288,10 +283,6 @@ function kb(n) {
288
283
  <style scoped>
289
284
  .dock { padding: 0 8px 8px; position: relative; }
290
285
 
291
- /* 输入框宽度 = 正文列宽。正文列是 .ms-in(max-width 780,左右 padding 22)居中于
292
- .stage-body(margin 0 8px),这里用同一个算式 100% - 44:
293
- 宽屏时同为 736,窄屏时同为 列宽-60,任何宽度下左右边都压在正文的文字边上。
294
- 菜单 / 提示条挂在这层里,才会跟着一起变窄。 */
295
286
  .dock-in {
296
287
  position: relative;
297
288
  width: min(736px, calc(100% - 44px));
@@ -342,13 +333,10 @@ textarea {
342
333
  font-size: 13.5px; line-height: 1.6;
343
334
  padding: 5px 2px; border: 0; background: none;
344
335
  resize: none; overflow-y: auto;
345
- /* 高度由 grow() 写死具体 px;max-height 只作兜底,避免 JS 失效时无限长高 */
346
336
  max-height: 340px;
347
337
  transition: height 0.14s ease;
348
338
  }
349
339
  textarea:focus { outline: none; }
350
- /* placeholder 是「第一次进来唯一能读的提示」,用 dim 而不是 faint:
351
- 输入框是视觉焦点,提示跟着提亮一档才不会被当成禁用态。 */
352
340
  textarea::placeholder { color: var(--dim); opacity: 1; }
353
341
 
354
342
  .send {
@@ -10,13 +10,10 @@ const lines = computed(() =>
10
10
  (props.doc.content || '').replace(/\n$/, '').split('\n')
11
11
  )
12
12
 
13
- /* 文字版正文走 lib/md.js 的共用渲染器:技能 SKILL.md 与记忆摘要都带
14
- # / ** / - / > / ```,这里原来自带一套只认前四样,代码块原样倒出来。 */
15
13
  const readHTML = computed(() => mdToHTML(props.doc.content))
16
14
  </script>
17
15
 
18
16
  <template>
19
- <!-- 代码 → CLI 皮肤:暗色、等宽、行号、横向滚动。整栏不变暗,只有正文块变暗。 -->
20
17
  <div v-if="doc.kind === 'code'" class="cli">
21
18
  <div class="term-bar">
22
19
  <span class="tl" /><span class="tl" /><span class="tl" />
@@ -31,7 +28,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
31
28
  </div>
32
29
  </div>
33
30
 
34
- <!-- 文字 → 阅读版:衬线正文、68ch 行宽、留白优先 -->
35
31
  <article v-else class="read scroll">
36
32
  <header class="read-head">
37
33
  <h2>{{ doc.title }}</h2>
@@ -45,7 +41,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
45
41
  </template>
46
42
 
47
43
  <style scoped>
48
- /* ═══ CLI 皮肤 ═══ */
49
44
  .cli {
50
45
  flex: 1; min-width: 0; display: flex; flex-direction: column;
51
46
  margin: 8px; border-radius: var(--r-mid); overflow: hidden;
@@ -87,7 +82,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
87
82
  }
88
83
  .code { color: var(--cli-ink); padding-right: 24px; }
89
84
 
90
- /* ═══ 阅读皮肤 ═══ */
91
85
  .read { flex: 1; overflow-y: auto; padding: 34px 8px 60px; }
92
86
  .read-head, .read-body { max-width: 68ch; margin: 0 auto; }
93
87
  .read-head { padding: 0 20px 18px; border-bottom: 1px solid var(--line-soft); }
@@ -101,7 +95,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
101
95
  }
102
96
  .read-meta code { font-family: var(--mono); font-size: 11.5px; color: var(--ink); }
103
97
  .read-body { padding: 20px 20px 0; }
104
- /* 正文是 v-html 生成的,没有 scoped 属性,必须 :deep */
105
98
  .read-body :deep(p) {
106
99
  font-family: var(--serif);
107
100
  font-size: 15px; line-height: 1.92; color: #24302a;
@@ -149,7 +142,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
149
142
  margin: 26px 0 10px;
150
143
  }
151
144
 
152
- /* 代码块:md.js 现在真会把 ``` 渲染成 <pre><code>,以前是整段原样倒出来 */
153
145
  .read-body :deep(pre) {
154
146
  margin: 0 0 15px; padding: 11px 13px;
155
147
  background: var(--cli-bg); border-radius: var(--r-sm);
@@ -160,7 +152,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
160
152
  font-size: 12px; line-height: 1.72;
161
153
  }
162
154
 
163
- /* 表格 */
164
155
  .read-body :deep(table) { width: 100%; border-collapse: collapse; margin: 0 0 15px; }
165
156
  .read-body :deep(th),
166
157
  .read-body :deep(td) {
@@ -5,7 +5,6 @@ import { state, loadProgress, closeDoc, setModule } from '../stores/app.js'
5
5
  import { panels, loadPanel, openKu, startReview } from '../stores/panels.js'
6
6
  import { fmtTime } from '../lib/fmt.js'
7
7
 
8
- /* 右栏三块数据与对应面板共用同一份,进来就保证已加载 */
9
8
  onMounted(() => {
10
9
  loadProgress()
11
10
  loadPanel('knowledge')
@@ -16,7 +15,6 @@ const doc = computed(() => state.doc)
16
15
  const kb = panels.knowledge
17
16
  const rv = panels.review
18
17
 
19
- /* 今日待办 = 到期 + 新卡(SM-2 队列就是这个口径) */
20
18
  const reviewTodo = computed(() => (rv.stats.due || 0) + (rv.stats.fresh || 0))
21
19
  const masteryPct = computed(() => Math.round((rv.stats.avg_mastery || 0) * 100))
22
20
 
@@ -26,7 +24,6 @@ function goReview() {
26
24
  startReview()
27
25
  }
28
26
 
29
- /* 右栏只留 10 条,多的走「更多」到知识库面板看全量 */
30
27
  const LIMIT = 10
31
28
  const recentKb = computed(() => kb.recent.slice(0, LIMIT))
32
29
  const kbMore = computed(() => kb.recentTotal > recentKb.value.length)
@@ -36,7 +33,6 @@ function moreKb() {
36
33
  setModule('knowledge')
37
34
  }
38
35
 
39
- /* 大纲:代码取顶层定义,文字取段落首句。P2 换成真解析 */
40
36
  const outline = computed(() => {
41
37
  const d = doc.value
42
38
  if (!d) return []
@@ -164,7 +160,6 @@ const outline = computed(() => {
164
160
  }
165
161
  .kpi div {
166
162
  padding: 8px 9px; border-radius: var(--r-sm);
167
- /* 纸面上要的是「白卡」而不是「更浅的纸」:0.72 的白压在暖底上几乎看不出边界 */
168
163
  background: rgba(255, 255, 255, 0.9);
169
164
  border: 1px solid var(--line);
170
165
  }
@@ -196,7 +191,6 @@ const outline = computed(() => {
196
191
  .row b { font-size: 13px; font-weight: 600; color: var(--w-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
197
192
  .row span { font-size: 11.5px; color: var(--w-dim); }
198
193
 
199
- /* 「更多」:三条杠 + 文字,点进知识库看全量 */
200
194
  .more {
201
195
  width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px;
202
196
  margin-top: 4px; padding: 7px 9px;
@@ -2,9 +2,11 @@
2
2
  import { computed, nextTick, onMounted, ref } from 'vue'
3
3
  import MenuPopover from './MenuPopover.vue'
4
4
  import {
5
- state, loadSessions, openSession, moduleLabel, setModule, railItems, RAIL,
5
+ state, loadSessions, openSession, moduleLabel, moduleIcon, setModule,
6
+ railItems, moreItems, PINNED, RAIL,
6
7
  } from '../stores/app.js'
7
8
  import { deleteSession, loadSession, newSession, renameSession } from '../stores/chat.js'
9
+ import { numberLocale, t } from '../lib/i18n.js'
8
10
 
9
11
  const btnRef = ref(null)
10
12
  const menuOpen = ref(false)
@@ -13,13 +15,10 @@ const anchor = ref(null)
13
15
  const count = computed(() => state.sessions.length)
14
16
  const rail = railItems()
15
17
 
16
- /* 当前模块不属于常驻项 → 落在「更多」里。概览是默认落地页,不给它加后缀,否则
17
- 一进站就显示「更多 · 概览」,像是从一个隐藏入口进来的。 */
18
- const isMore = computed(() => !RAIL.includes(state.module))
18
+ const pinnedItems = PINNED.map((id) => ({ id, icon: moduleIcon(id) }))
19
+ const isMore = computed(() => moreItems().some((m) => m.id === state.module))
19
20
  const moreCur = computed(() => (isMore.value && state.module !== 'overview' ? moduleLabel(state.module) : ''))
20
21
 
21
- /* 线性图标:统一 24 视框 + 描边,和右栏 / 顶栏的图形语言一致。
22
- 只放 path 的 d,颜色与线宽交给 CSS,换肤时不用动 JS。 */
23
22
  const ICONS = {
24
23
  book: [
25
24
  'M5 5.6A2.1 2.1 0 0 1 7.1 3.5H19v17H7.1A2.1 2.1 0 0 1 5 18.4z',
@@ -43,6 +42,17 @@ const ICONS = {
43
42
  'M6.1 21.4v-3.6h3.6',
44
43
  ],
45
44
  memory: ['M7.2 3.4h9.6v17.2L12 17.2l-4.8 3.4z'],
45
+ gear: [
46
+ 'M12 15.2a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4z',
47
+ 'M12 3.1v1.9',
48
+ 'M12 19v1.9',
49
+ 'M3.1 12h1.9',
50
+ 'M19 12h1.9',
51
+ 'M5.7 5.7l1.4 1.4',
52
+ 'M16.9 16.9l1.4 1.4',
53
+ 'M18.3 5.7l-1.4 1.4',
54
+ 'M7.1 16.9l-1.4 1.4',
55
+ ],
46
56
  dot: ['M12 10.6a1.4 1.4 0 1 0 0 2.8 1.4 1.4 0 0 0 0-2.8z'],
47
57
  }
48
58
 
@@ -60,7 +70,6 @@ function toggleMenu() {
60
70
  return
61
71
  }
62
72
  const r = btnRef.value?.getBoundingClientRect()
63
- /* 入口现在在栏顶,浮层必须往下长(原来的实现是贴着栏底往上长) */
64
73
  if (r) anchor.value = { left: r.left, bottom: r.bottom }
65
74
  menuOpen.value = true
66
75
  }
@@ -70,13 +79,11 @@ function pick(id) {
70
79
  menuOpen.value = false
71
80
  }
72
81
 
73
- /* 切会话 = 切左侧高亮 + 拉后端历史消息 */
74
82
  function pickSession(s) {
75
83
  openSession(s)
76
84
  loadSession(s.id)
77
85
  }
78
86
 
79
- /* ── 会话重命名 / 删除 ── */
80
87
  const editingId = ref('')
81
88
  const draft = ref('')
82
89
  let editEl = null
@@ -94,19 +101,17 @@ function cancelRename() {
94
101
  editingId.value = ''
95
102
  }
96
103
 
97
- /* enter / blur 都会走到这里:先清 editingId 再做守卫,重复提交自然变成空操作 */
98
104
  async function commitRename(s) {
99
105
  if (editingId.value !== s.id) return
100
106
  editingId.value = ''
101
- const t = draft.value.trim()
102
- if (!t || t === (s.title || '')) return
103
- await renameSession(s.id, t)
107
+ const next = draft.value.trim()
108
+ if (!next || next === (s.title || '')) return
109
+ await renameSession(s.id, next)
104
110
  }
105
111
 
106
112
  async function removeSession(s) {
107
- const name = s.title || '新会话'
108
- /* 删除会连对话历史一起删掉,不能手滑 —— 用系统确认框,不引新组件 */
109
- if (!confirm(`删除会话「${name}」?\n对话历史会一起删掉,不可恢复。`)) return
113
+ const name = s.title || t('rail.untitled')
114
+ if (!confirm(t('rail.deleteConfirm', { name }))) return
110
115
  await deleteSession(s.id)
111
116
  }
112
117
 
@@ -117,19 +122,19 @@ async function startNew() {
117
122
 
118
123
  function fmt(ts) {
119
124
  if (!ts) return ''
120
- return new Date(ts * 1000).toLocaleString('zh-CN', {
125
+ return new Date(ts * 1000).toLocaleString(numberLocale(), {
121
126
  month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false,
122
127
  })
123
128
  }
124
129
 
125
130
  function meta(s) {
126
- return [fmt(s.updated_at), s.message_count ? `${s.message_count} 条` : ''].filter(Boolean).join(' · ')
131
+ return [fmt(s.updated_at), s.message_count ? t('common.count', { n: s.message_count }) : ''].filter(Boolean).join(' · ')
127
132
  }
128
133
  </script>
129
134
 
130
135
  <template>
131
136
  <aside class="col col--left rail-left">
132
- <button class="new-sess" title="开一个新会话" @click="startNew">+ 新会话</button>
137
+ <button class="new-sess" :title="t('rail.newSession')" @click="startNew">{{ t('rail.newSessionBtn') }}</button>
133
138
 
134
139
  <nav class="rail-nav">
135
140
  <div
@@ -142,7 +147,7 @@ function meta(s) {
142
147
  <span class="ic">
143
148
  <svg viewBox="0 0 24 24"><path v-for="(d, i) in icon(m.icon)" :key="i" :d="d" /></svg>
144
149
  </span>
145
- <b>{{ m.label }}</b>
150
+ <b>{{ moduleLabel(m.id) }}</b>
146
151
  <span v-if="m.beta" class="beta">BETA</span>
147
152
  </button>
148
153
 
@@ -151,7 +156,7 @@ function meta(s) {
151
156
  v-for="q in quickOf(m)"
152
157
  :key="q.id"
153
158
  class="act"
154
- :title="`打开${q.label}`"
159
+ :title="t('rail.open', { label: q.label })"
155
160
  :aria-label="q.label"
156
161
  @click.stop="pick(q.id)"
157
162
  >
@@ -165,13 +170,13 @@ function meta(s) {
165
170
  ref="btnRef"
166
171
  class="rail-item rail-anchor"
167
172
  :aria-expanded="menuOpen"
168
- title="其它功能"
173
+ :title="t('rail.otherFeatures')"
169
174
  @click="toggleMenu"
170
175
  >
171
176
  <span class="ic">
172
177
  <svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('more')" :key="i" :d="d" /></svg>
173
178
  </span>
174
- <b>更多</b>
179
+ <b>{{ t('rail.more') }}</b>
175
180
  <span v-if="moreCur" class="cur">{{ moreCur }}</span>
176
181
  <span class="caret" :class="{ open: menuOpen }">▾</span>
177
182
  </button>
@@ -179,13 +184,13 @@ function meta(s) {
179
184
  </nav>
180
185
 
181
186
  <div class="sec-head">
182
- 会话历史 <em>{{ count }}</em>
187
+ {{ t('rail.history') }} <em>{{ count }}</em>
183
188
  </div>
184
189
 
185
190
  <div class="ns-list scroll">
186
- <div v-if="state.sessionsState === 'loading'" class="empty">加载中…</div>
187
- <div v-else-if="state.sessionsState === 'error'" class="empty">后端未连接</div>
188
- <div v-else-if="!count" class="empty">还没有会话 · 发一条消息即开始</div>
191
+ <div v-if="state.sessionsState === 'loading'" class="empty">{{ t('common.loading') }}</div>
192
+ <div v-else-if="state.sessionsState === 'error'" class="empty">{{ t('common.offline') }}</div>
193
+ <div v-else-if="!count" class="empty">{{ t('rail.empty') }}</div>
189
194
  <template v-else>
190
195
  <div
191
196
  v-for="s in state.sessions"
@@ -199,24 +204,24 @@ function meta(s) {
199
204
  v-model="draft"
200
205
  class="ns-edit"
201
206
  maxlength="60"
202
- aria-label="会话标题"
207
+ :aria-label="t('rail.sessionTitle')"
203
208
  @keydown.enter.prevent="commitRename(s)"
204
209
  @keydown.esc.prevent="cancelRename"
205
210
  @blur="commitRename(s)"
206
211
  />
207
212
  <template v-else>
208
- <button class="ns-item" :title="s.title || '新会话'" @click="pickSession(s)">
209
- <b>{{ s.title || '新会话' }}</b>
213
+ <button class="ns-item" :title="s.title || t('rail.untitled')" @click="pickSession(s)">
214
+ <b>{{ s.title || t('rail.untitled') }}</b>
210
215
  <span>{{ meta(s) }}</span>
211
216
  </button>
212
217
  <span class="ns-acts">
213
- <button class="act" title="重命名会话" @click.stop="beginRename(s)">
218
+ <button class="act" :title="t('rail.rename')" @click.stop="beginRename(s)">
214
219
  <svg viewBox="0 0 24 24">
215
220
  <path d="M4.4 19.6h4l10.8-10.8-4-4L4.4 15.6z" />
216
221
  <path d="M14.4 5.6l3.8 3.8" />
217
222
  </svg>
218
223
  </button>
219
- <button class="act del" title="删除会话" @click.stop="removeSession(s)">
224
+ <button class="act del" :title="t('rail.delete')" @click.stop="removeSession(s)">
220
225
  <svg viewBox="0 0 24 24">
221
226
  <path d="M5 7h14" />
222
227
  <path d="M9.6 7V4.6h4.8V7" />
@@ -229,6 +234,22 @@ function meta(s) {
229
234
  </template>
230
235
  </div>
231
236
 
237
+ <div class="rail-foot">
238
+ <div
239
+ v-for="m in pinnedItems"
240
+ :key="m.id"
241
+ class="rail-row"
242
+ :class="{ on: state.module === m.id }"
243
+ >
244
+ <button class="rail-item" :title="moduleLabel(m.id)" @click="pick(m.id)">
245
+ <span class="ic">
246
+ <svg viewBox="0 0 24 24"><path v-for="(d, i) in icon(m.icon)" :key="i" :d="d" /></svg>
247
+ </span>
248
+ <b>{{ moduleLabel(m.id) }}</b>
249
+ </button>
250
+ </div>
251
+ </div>
252
+
232
253
  <MenuPopover
233
254
  v-if="menuOpen"
234
255
  :anchor="anchor"
@@ -253,7 +274,6 @@ function meta(s) {
253
274
  .new-sess:hover { background: rgba(26, 157, 95, 0.17); }
254
275
  .new-sess:active { transform: scale(0.985); }
255
276
 
256
- /* ── 常驻功能入口 ── */
257
277
  .rail-nav { display: flex; flex-direction: column; gap: 2px; flex-shrink: 0; }
258
278
 
259
279
  .rail-row { position: relative; display: flex; align-items: center; }
@@ -285,12 +305,9 @@ function meta(s) {
285
305
  .rail-row.on .rail-item .ic { color: var(--green); }
286
306
  .rail-row.on .rail-item b { font-weight: 600; }
287
307
 
288
- /* 停在子视图(复习 / 记忆中心)时,父项给一层更淡的高亮:既说明「你在这条线上」,
289
- 又不和真正选中的白卡混淆。 */
290
308
  .rail-row.sub .rail-item { background: rgba(255, 255, 255, 0.5); }
291
309
  .rail-row.sub .rail-item .ic { color: var(--green); }
292
310
 
293
- /* 成熟度标签:比正式入口新,或者语义还在变 */
294
311
  .beta {
295
312
  flex-shrink: 0;
296
313
  font-size: 10px; font-weight: 700; letter-spacing: 0.08em;
@@ -299,7 +316,6 @@ function meta(s) {
299
316
  background: rgba(199, 116, 0, 0.12);
300
317
  }
301
318
 
302
- /* 同源数据的另一视图:只在 hover / 聚焦时出现,不占常驻视觉 */
303
319
  .rail-acts {
304
320
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
305
321
  display: flex; gap: 2px;
@@ -342,8 +358,13 @@ function meta(s) {
342
358
 
343
359
  .ns-list { display: flex; flex-direction: column; gap: 3px; flex: 1; margin: 0 -4px; padding: 0 4px; }
344
360
 
345
- /* 行容器负责底色/边框,hover 时右侧才浮出改名与删除 —— 常驻两个图标会把
346
- 12 个字的标题挤成 6 个字。 */
361
+ .rail-foot {
362
+ flex-shrink: 0;
363
+ margin-top: 2px;
364
+ padding-top: 8px;
365
+ border-top: 1px solid var(--line);
366
+ }
367
+
347
368
  .ns-row {
348
369
  position: relative; display: flex; align-items: center;
349
370
  border-radius: var(--r-sm);
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, onBeforeUnmount, onMounted } from 'vue'
3
- import { moreItems } from '../stores/app.js'
3
+ import { groupLabel, moduleLabel, moreItems } from '../stores/app.js'
4
+ import { t } from '../lib/i18n.js'
4
5
 
5
6
  const props = defineProps({
6
7
  anchor: { type: Object, default: null },
@@ -8,18 +9,16 @@ const props = defineProps({
8
9
  })
9
10
  const emit = defineEmits(['pick', 'close'])
10
11
 
11
- /* 只剩常驻入口之外的功能(概览 / 活动 / 使用情况 / 复习 / 记忆中心)。
12
- 常驻项不在这里重复出现 —— 「更多」的语义是「别处放不下的」,不是「全部重列一遍」。 */
13
12
  const groups = computed(() => {
14
13
  const out = []
15
14
  let cur = null
16
15
  for (const m of moreItems()) {
17
- if (!m.group) {
18
- out.push({ title: '', items: [m] })
16
+ if (!m.groupKey) {
17
+ out.push({ titleKey: '', items: [m] })
19
18
  continue
20
19
  }
21
- if (!cur || cur.title !== m.group) {
22
- cur = { title: m.group, items: [] }
20
+ if (!cur || cur.titleKey !== m.groupKey) {
21
+ cur = { titleKey: m.groupKey, items: [] }
23
22
  out.push(cur)
24
23
  }
25
24
  cur.items.push(m)
@@ -27,7 +26,6 @@ const groups = computed(() => {
27
26
  return out
28
27
  })
29
28
 
30
- /* 入口在栏顶 → 浮层向下展开。left 直接对齐按钮左沿;宽度固定,左栏只有 296px,不会溢出视口右侧。 */
31
29
  const style = computed(() => {
32
30
  const a = props.anchor
33
31
  if (!a) return { left: '16px', top: '120px' }
@@ -61,10 +59,10 @@ onBeforeUnmount(() => {
61
59
  <template>
62
60
  <Teleport to="body">
63
61
  <div class="nav-pop" :style="style">
64
- <div class="pop-head">更多功能 <span class="x" title="关闭 (Esc)" @click="$emit('close')">✕</span></div>
62
+ <div class="pop-head">{{ t('menu.title') }} <span class="x" :title="t('menu.close')" @click="$emit('close')">✕</span></div>
65
63
  <div class="pop-body">
66
64
  <template v-for="(g, gi) in groups" :key="gi">
67
- <div v-if="g.title" class="pop-sec">{{ g.title }}</div>
65
+ <div v-if="g.titleKey" class="pop-sec">{{ groupLabel(g.titleKey) }}</div>
68
66
  <button
69
67
  v-for="m in g.items"
70
68
  :key="m.id"
@@ -72,7 +70,7 @@ onBeforeUnmount(() => {
72
70
  :class="{ on: m.id === active }"
73
71
  @click="$emit('pick', m.id)"
74
72
  >
75
- <span class="lbl">{{ m.label }}</span>
73
+ <span class="lbl">{{ moduleLabel(m.id) }}</span>
76
74
  <span class="tag">{{ m.tag }}</span>
77
75
  </button>
78
76
  </template>