foxclaw 0.1.2 → 0.1.3

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 (344) 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/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -2,6 +2,7 @@
2
2
  import { nextTick, ref, watch } from 'vue'
3
3
  import { chat, retry } from '../stores/chat.js'
4
4
  import { mdToHTML } from '../lib/md.js'
5
+ import { t } from '../lib/i18n.js'
5
6
 
6
7
  const box = ref(null)
7
8
  let stick = true
@@ -12,7 +13,6 @@ function onScroll() {
12
13
  stick = el.scrollHeight - el.scrollTop - el.clientHeight < 80
13
14
  }
14
15
 
15
- /* 依赖「长度和」而不是整个数组:流式时只有最后一个对象在变,深度 watch 会打爆 */
16
16
  watch(
17
17
  () =>
18
18
  chat.messages
@@ -40,12 +40,16 @@ function stepText(s) {
40
40
  return s.desc || s.task || s.label || JSON.stringify(s)
41
41
  }
42
42
 
43
- const SRC_CN = { memory: '记忆', knowledge: '知识', web: '联网', agent: '态势' }
43
+ const SRC_KEY = {
44
+ memory: 'msg.srcMemory',
45
+ knowledge: 'msg.srcKnowledge',
46
+ web: 'msg.srcWeb',
47
+ agent: 'msg.srcAgent',
48
+ }
44
49
  function srcLabel(s) {
45
- return SRC_CN[s] || s || '证据'
50
+ return (s && SRC_KEY[s] ? t(SRC_KEY[s]) : s) || t('msg.srcEvidence')
46
51
  }
47
52
 
48
- /* ── 复制 / 重试 ── */
49
53
  const copiedKey = ref(-1)
50
54
  let copyTimer = 0
51
55
 
@@ -54,7 +58,6 @@ async function copy(m, i) {
54
58
  try {
55
59
  await navigator.clipboard.writeText(text)
56
60
  } catch {
57
- /* 局域网纯 http 不是安全上下文,clipboard 会拒绝 → 退回 textarea 选中法 */
58
61
  const ta = document.createElement('textarea')
59
62
  ta.value = text
60
63
  ta.style.position = 'fixed'
@@ -64,7 +67,6 @@ async function copy(m, i) {
64
67
  try {
65
68
  document.execCommand('copy')
66
69
  } catch {
67
- /* 再失败就只好让用户手动选 */
68
70
  }
69
71
  ta.remove()
70
72
  }
@@ -73,7 +75,6 @@ async function copy(m, i) {
73
75
  copyTimer = setTimeout(() => (copiedKey.value = -1), 1600)
74
76
  }
75
77
 
76
- /* 只有最后一条能重试:往前还有更新的对话时,重发会把后面的记录截掉 */
77
78
  function canRetry(i) {
78
79
  return i === chat.messages.length - 1
79
80
  }
@@ -83,7 +84,6 @@ function canRetry(i) {
83
84
  <div ref="box" class="ms scroll" @scroll="onScroll">
84
85
  <div class="ms-in">
85
86
  <article v-for="(m, i) in chat.messages" :key="i" class="msg" :class="m.role">
86
- <!-- 用户 -->
87
87
  <div v-if="m.role === 'user'" class="u-body">
88
88
  <div v-if="m.attachments && m.attachments.length" class="u-atts">
89
89
  <span v-for="(a, j) in m.attachments" :key="j" class="u-att">{{ a.name }}</span>
@@ -92,24 +92,23 @@ function canRetry(i) {
92
92
  <div class="u-time">{{ fmtTime(m.ts) }}</div>
93
93
  </div>
94
94
 
95
- <!-- 助手 -->
96
95
  <div v-else class="a-body">
97
96
  <div class="a-head">
98
97
  <span class="avatar" :class="{ live: m.streaming }" />
99
- <span class="who">主管 Agent</span>
98
+ <span class="who">{{ t('msg.supervisor') }}</span>
100
99
  <span v-if="m.stage" class="stage">◌ {{ m.stage }}</span>
101
- <span v-if="m.streaming" class="live-tag">生成中</span>
100
+ <span v-if="m.streaming" class="live-tag">{{ t('msg.generating') }}</span>
102
101
  <span class="a-time">{{ fmtTime(m.ts) }}</span>
103
102
  </div>
104
103
 
105
104
  <details v-if="m.thinking" class="fold think">
106
- <summary>推理过程 <em>{{ m.thinking.length }} 字</em></summary>
105
+ <summary>{{ t('msg.reasoning') }} <em>{{ t('msg.chars', { n: m.thinking.length }) }}</em></summary>
107
106
  <pre class="fold-body">{{ m.thinking }}</pre>
108
107
  </details>
109
108
 
110
109
  <details v-if="m.plan && (m.plan.steps || []).length" class="fold plan" open>
111
110
  <summary>
112
- 执行计划 <em>{{ m.plan.steps.length }} 步 · {{ m.plan.flow || 'single' }}</em>
111
+ {{ t('msg.plan') }} <em>{{ t('msg.steps', { n: m.plan.steps.length }) }} · {{ m.plan.flow || 'single' }}</em>
113
112
  </summary>
114
113
  <ol class="fold-body">
115
114
  <li v-for="(s, k) in m.plan.steps" :key="k">{{ stepText(s) }}</li>
@@ -119,8 +118,8 @@ function canRetry(i) {
119
118
  <div v-if="m.search" class="sres">
120
119
  <div class="sres-h">
121
120
  <span class="sroute">{{ m.search.route }}</span>
122
- <span>置信度 {{ Math.round((m.search.confidence || 0) * 100) }}%</span>
123
- <span>{{ m.search.iterations }} 轮迭代</span>
121
+ <span>{{ t('msg.confidence', { n: Math.round((m.search.confidence || 0) * 100) }) }}</span>
122
+ <span>{{ t('msg.iterations', { n: m.search.iterations }) }}</span>
124
123
  </div>
125
124
  <div v-for="(n, k) in m.search.nodes" :key="k" class="srow">
126
125
  <span class="ssrc" :class="n.source">{{ srcLabel(n.source) }}</span>
@@ -141,11 +140,11 @@ function canRetry(i) {
141
140
  <div v-if="m.error" class="err">✕ {{ m.error }}</div>
142
141
 
143
142
  <details v-if="m.evidence && m.evidence.length" class="fold ev">
144
- <summary>执行证据 <em>{{ m.evidence.length }} 项</em></summary>
143
+ <summary>{{ t('msg.evidence') }} <em>{{ t('msg.items', { n: m.evidence.length }) }}</em></summary>
145
144
  <div class="fold-body">
146
145
  <div v-for="(e, k) in m.evidence" :key="k" class="ev-row">
147
146
  <span class="ev-dot" :class="e.status || 'unverified'" />
148
- <b>{{ e.tool || e.kind || '工具' }}</b>
147
+ <b>{{ e.tool || e.kind || t('msg.tool') }}</b>
149
148
  <span class="ev-st">{{ e.status || e.reason || '' }}</span>
150
149
  <template v-for="(x, y) in e.evidence || []" :key="y">
151
150
  <a v-if="isUrl(x)" class="ev-link" :href="x.ref" target="_blank" rel="noreferrer">
@@ -159,9 +158,9 @@ function canRetry(i) {
159
158
 
160
159
  <div v-if="!m.streaming && (m.content || m.error)" class="a-acts">
161
160
  <button v-if="m.content" class="mini" @click="copy(m, i)">
162
- {{ copiedKey === i ? '已复制' : '复制' }}
161
+ {{ copiedKey === i ? t('msg.copied') : t('msg.copy') }}
163
162
  </button>
164
- <button v-if="canRetry(i)" class="mini" @click="retry(m)">重试</button>
163
+ <button v-if="canRetry(i)" class="mini" @click="retry(m)">{{ t('msg.retry') }}</button>
165
164
  </div>
166
165
  </div>
167
166
  </article>
@@ -177,7 +176,6 @@ function canRetry(i) {
177
176
  display: flex; flex-direction: column; gap: 20px;
178
177
  }
179
178
 
180
- /* ── 用户 ── */
181
179
  .msg.user { display: flex; justify-content: flex-end; }
182
180
  .u-body { max-width: 78%; display: flex; flex-direction: column; align-items: flex-end; gap: 5px; }
183
181
  .u-atts { display: flex; gap: 5px; flex-wrap: wrap; justify-content: flex-end; }
@@ -195,7 +193,6 @@ function canRetry(i) {
195
193
  }
196
194
  .u-time { font-size: 11px; color: var(--faint); font-family: var(--mono); }
197
195
 
198
- /* ── 助手 ── */
199
196
  .a-body { display: flex; flex-direction: column; gap: 9px; }
200
197
  .a-head { display: flex; align-items: center; gap: 7px; font-size: 11.5px; color: var(--dim); }
201
198
  .avatar {
@@ -213,13 +210,9 @@ function canRetry(i) {
213
210
  .a-head .live-tag { color: var(--green); font-size: 10.5px; font-weight: 600; }
214
211
  .a-time { margin-left: auto; font-family: var(--mono); }
215
212
 
216
- /* 正文由 lib/md.js 渲染成 HTML(见 v-html),所以子元素都是 :deep ——
217
- 块级元素自己带 margin,这里整体压紧,避免气泡被撑得很松。 */
218
213
  .a-text {
219
214
  font-size: 13.5px; line-height: 1.8; color: var(--ink);
220
215
  word-break: break-word;
221
- /* 渲染出来的 HTML 里没有裸换行(段落内用 <br>),pre-wrap 只负责保住连续空格 ——
222
- /帮助 之类的对齐文本原来靠它,去掉就会被折叠成单个空格。 */
223
216
  white-space: pre-wrap;
224
217
  }
225
218
  .a-text :deep(pre),
@@ -280,8 +273,6 @@ function canRetry(i) {
280
273
  .a-text :deep(th) { color: var(--dim); font-size: 11.5px; }
281
274
  .a-text :deep(a) { color: var(--green); border-bottom: 1px solid rgba(26, 157, 95, 0.3); }
282
275
 
283
- /* 流式光标挂在最后一个块级元素的末尾 —— 原来是一个 <span>,现在内容是 HTML 了,
284
- 放在外面会掉到下一行开头。 */
285
276
  .a-text.streaming > :last-child::after {
286
277
  content: '';
287
278
  display: inline-block; width: 6px; height: 13px; margin-left: 3px;
@@ -328,7 +319,6 @@ function canRetry(i) {
328
319
  .ev-file { color: var(--dim); }
329
320
  .ev-link:hover { text-decoration: underline; }
330
321
 
331
- /* ── 认知搜索结果卡 ── */
332
322
  .sres {
333
323
  border-radius: var(--r-sm);
334
324
  background: rgba(255, 255, 255, 0.75);
@@ -371,7 +361,6 @@ function canRetry(i) {
371
361
  background: rgba(200, 140, 40, 0.09);
372
362
  }
373
363
 
374
- /* 复制 / 重试:平时不占位,鼠标到这条消息才浮出 */
375
364
  .a-acts { display: flex; gap: 6px; opacity: 0; transition: opacity 0.13s; }
376
365
  .msg:hover .a-acts,
377
366
  .a-acts:focus-within { opacity: 1; }
@@ -387,7 +376,6 @@ function canRetry(i) {
387
376
  border-color: rgba(26, 157, 95, 0.35);
388
377
  }
389
378
 
390
- /* 触屏没有 hover:常显,否则永远点不到 */
391
379
  @media (hover: none) {
392
380
  .a-acts { opacity: 1; }
393
381
  }
@@ -46,7 +46,6 @@ function startAdd() {
46
46
  form.value = { edit: false, name: '', base_url: '', api_key: '', model: '', note: '' }
47
47
  }
48
48
 
49
- /* 编辑:先 GET 详情预填(key 只回 has_key,输入框留空 = 保留原值,后端语义一致) */
50
49
  async function startEdit(m) {
51
50
  busy.value = true
52
51
  try {
@@ -91,7 +90,6 @@ async function submit() {
91
90
  }
92
91
  }
93
92
 
94
- /* 连通性测试:POST /llm/test,后端失败也返回 200 + {ok:false,error},原样显示原因 */
95
93
  const tState = ref('idle') // idle | busy | ok | err
96
94
  const tKey = ref('')
97
95
  const tMsg = ref('')
@@ -295,12 +293,10 @@ function isCur(source, key) {
295
293
  .tg.warn { color: var(--amber); background: rgba(200, 140, 40, 0.13); }
296
294
  .it .x { flex-shrink: 0; font-size: 11px; color: var(--dim); padding: 2px 4px; }
297
295
  .it .x:hover { color: var(--red); }
298
- /* 测 / 编 是 button,去掉按钮底框,和 ✕ 同一个观感 */
299
296
  button.x { background: none; border: 0; font-family: inherit; cursor: pointer; }
300
297
  button.x:hover:not(:disabled) { color: var(--accent); }
301
298
  button.x:disabled { opacity: 0.4; cursor: default; }
302
299
 
303
- /* 连通性测试结果条:失败原因原样显示(常见是 key 错 / base_url 多一层 /v1) */
304
300
  .tres {
305
301
  margin-top: 5px; padding: 7px 9px; border-radius: var(--r-sm);
306
302
  font-size: 11.5px; line-height: 1.55; color: var(--dim);
@@ -317,7 +313,6 @@ button.x:disabled { opacity: 0.4; cursor: default; }
317
313
  }
318
314
  .empty { padding: 10px 7px; font-size: 12px; color: var(--dim); }
319
315
 
320
- /* 列表可能长到滚出视口,底部两个按钮必须常驻可点 */
321
316
  .foot {
322
317
  position: sticky; bottom: -6px;
323
318
  display: flex; gap: 5px;
@@ -1,8 +1,4 @@
1
1
  <script setup>
2
- /* 右栏上部的紧凑录入口。
3
- 和知识库面板里那个是同一个 store 写操作(ingestKnowledge),
4
- 区别只是这里只留「写一句 + 收录」,标题可填可不填 —— 右栏宽度放不下
5
- 标题/领域两行输入,而随手记的时候本来也不需要它们。 */
6
2
  import { computed, ref, onUnmounted } from 'vue'
7
3
  import { panels, ingestKnowledge } from '../stores/panels.js'
8
4
 
@@ -30,7 +26,6 @@ async function go() {
30
26
  await ingestKnowledge({ text, title: title.value })
31
27
  title.value = ''
32
28
  } catch {
33
- /* 失败原因已经在 kp.ingestErr 里,这里不用再抛 */
34
29
  }
35
30
  clearLater()
36
31
  }
@@ -61,7 +56,6 @@ onUnmounted(() => clearTimeout(timer))
61
56
  </template>
62
57
 
63
58
  <style scoped>
64
- /* 挂在右栏 .body 里(body 已有 12px 内边距),这里只管垂直节奏 */
65
59
  .qi {
66
60
  padding: 0 0 10px;
67
61
  margin-bottom: 14px;
@@ -11,13 +11,14 @@ import KnowledgePanel from './panels/KnowledgePanel.vue'
11
11
  import ReviewPanel from './panels/ReviewPanel.vue'
12
12
  import MemoryPanel from './panels/MemoryPanel.vue'
13
13
  import SettingsPanel from './panels/SettingsPanel.vue'
14
- import { chat, loadSession, send } from '../stores/chat.js'
14
+ import { chat, loadSession, say, send } from '../stores/chat.js'
15
15
  import { state, moduleLabel, closeDoc, loadSessions, openSession } from '../stores/app.js'
16
+ import { profile, loadProfile, saveNickname } from '../stores/profile.js'
16
17
  import { openKu } from '../stores/panels.js'
17
18
  import { getJSON } from '../api.js'
18
19
  import { clip } from '../lib/fmt.js'
20
+ import { t } from '../lib/i18n.js'
19
21
 
20
- /* 菜单 8 项 → 真实面板。overview 是核心对话视图,不在这里。 */
21
22
  const PANELS = {
22
23
  activity: ActivityPanel,
23
24
  tasks: TasksPanel,
@@ -31,29 +32,39 @@ const PANELS = {
31
32
  }
32
33
  const panel = computed(() => PANELS[state.module] || null)
33
34
 
34
- /* 标签条只在打开文档时需要(文档 + 回到面板)。
35
- 没文档时面板头部已经写着模块名,再挂一条同名 tab 就是重复。 */
36
35
  const hasTabs = computed(() => !!state.doc)
37
36
 
38
- /* ── 空态引导 ──
39
- 一块空白 + 一句「问我任何事」等于让用户自己想问题。给三条真实可用的起点:
40
- 示例提问(点了直接发)、继续上次会话、最近知识(点了开正文)。
41
- 数据各自 .catch:任何一条挂了都只是少一块引导,不该让空态变成报错页。 */
42
- const EXAMPLES = [
43
- '现在系统里有哪些技能可以用?',
44
- '帮我把今天要做的事排个优先级',
45
- '用一句话说清你能帮我做什么',
46
- ]
37
+ const EXAMPLES = computed(() => [t('hero.q1'), t('hero.q2'), t('hero.q3')])
47
38
 
48
39
  const recent = ref([])
40
+
41
+ const nick = ref('')
42
+ const nickSkipped = ref(localStorage.getItem('sfx.nick.skip') === '1')
43
+ const askNick = computed(() => profile.loaded && !profile.nickname && !nickSkipped.value)
44
+
49
45
  onMounted(async () => {
46
+ loadProfile().then(() => { nick.value = profile.nickname })
50
47
  if (!state.sessions.length) loadSessions().catch(() => {})
51
48
  const d = await getJSON('/knowledge/recent?limit=5').catch(() => null)
52
49
  recent.value = d?.kus || []
53
50
  })
54
51
 
55
- /* 「继续上次」:取列表里最新一条**真的有消息**的会话。
56
- 不能只看 sessions[0] —— 那个常常是刚建的空会话(count=0),跳过它才能拿到上次聊的。 */
52
+ async function saveNick() {
53
+ const n = nick.value.trim()
54
+ if (!n) return
55
+ try {
56
+ await saveNickname(n)
57
+ say(t('hero.saidNick', { name: n }))
58
+ } catch (e) {
59
+ say(t('hero.errSave', { msg: e.message }), 4000)
60
+ }
61
+ }
62
+
63
+ function skipNick() {
64
+ nickSkipped.value = true
65
+ localStorage.setItem('sfx.nick.skip', '1')
66
+ }
67
+
57
68
  const lastSession = computed(() => {
58
69
  const cur = chat.messages.length ? state.sessionId : ''
59
70
  return state.sessions.find((s) => s.id !== cur && (s.message_count || 0) > 0) || null
@@ -80,7 +91,7 @@ function resume() {
80
91
  </button>
81
92
  <button v-if="state.doc" class="tab on doc" @click="closeDoc()">
82
93
  <span class="t">{{ state.doc.title }}</span>
83
- <span class="x" title="关闭">✕</span>
94
+ <span class="x" :title="t('hero.close')">✕</span>
84
95
  </button>
85
96
  </div>
86
97
 
@@ -93,8 +104,21 @@ function resume() {
93
104
  <MessageStream v-if="chat.messages.length" />
94
105
  <div v-else class="core-empty">
95
106
  <div class="hero">
96
- <div class="hero-t">问我任何事</div>
97
- <div class="core-cap">主管 Agent 会按需调度子 Agent;想让它读你的知识库,直接问就行</div>
107
+ <div class="hero-t">{{ t('hero.title') }}</div>
108
+ <div class="core-cap">{{ t('hero.cap') }}</div>
109
+ </div>
110
+
111
+ <div v-if="askNick" class="ask-nick">
112
+ <span class="an-q">{{ t('hero.askName') }}</span>
113
+ <input
114
+ v-model="nick"
115
+ class="an-in"
116
+ maxlength="24"
117
+ :placeholder="t('hero.namePh')"
118
+ @keydown.enter.prevent="saveNick"
119
+ />
120
+ <button class="an-ok" :disabled="profile.saving || !nick.trim()" @click="saveNick">{{ t('hero.confirm') }}</button>
121
+ <button class="an-skip" :title="t('hero.skipTip')" @click="skipNick">{{ t('hero.skip') }}</button>
98
122
  </div>
99
123
 
100
124
  <div class="tries">
@@ -104,13 +128,13 @@ function resume() {
104
128
  </div>
105
129
 
106
130
  <button v-if="lastSession" class="resume" @click="resume">
107
- <span class="rl">继续上次</span>
108
- <b>{{ clip(lastSession.title || '未命名会话', 28) }}</b>
109
- <span class="rm">{{ lastSession.message_count }} 条消息</span>
131
+ <span class="rl">{{ t('hero.resume') }}</span>
132
+ <b>{{ clip(lastSession.title || t('hero.untitled'), 28) }}</b>
133
+ <span class="rm">{{ t('hero.msgs', { n: lastSession.message_count }) }}</span>
110
134
  </button>
111
135
 
112
136
  <div v-if="recent.length" class="recent">
113
- <div class="rh">最近知识</div>
137
+ <div class="rh">{{ t('hero.recentKnowledge') }}</div>
114
138
  <div class="rk-list">
115
139
  <button v-for="k in recent" :key="k.ku_id" class="rk" :title="k.summary || k.name" @click="openKu(k.ku_id)">
116
140
  {{ clip(k.name || k.ku_id, 30) }}
@@ -158,10 +182,8 @@ function resume() {
158
182
  overflow: hidden;
159
183
  display: flex;
160
184
  }
161
- /* 没有标签条时不留缺口,四角全圆 */
162
185
  .stage-body.bare { margin-top: 8px; border-radius: var(--r-mid); }
163
186
 
164
- /* ── 核心:空态居中,有消息时整块交给消息流 ── */
165
187
  .core { flex: 1; min-height: 0; display: flex; flex-direction: column; }
166
188
  .core-empty {
167
189
  flex: 1; display: flex; flex-direction: column;
@@ -178,7 +200,31 @@ function resume() {
178
200
  color: var(--ink); margin-bottom: 6px;
179
201
  }
180
202
 
181
- /* 示例提问:原来空态只有一句提示,用户不知道从哪开口 */
203
+ .ask-nick {
204
+ display: flex; align-items: center; gap: 8px;
205
+ width: 100%; max-width: 420px;
206
+ padding: 9px 11px; border-radius: var(--r-sm);
207
+ background: rgba(26, 157, 95, 0.06);
208
+ border: 1px solid rgba(26, 157, 95, 0.22);
209
+ }
210
+ .an-q { font-size: 12.5px; font-weight: 600; color: var(--ink); flex-shrink: 0; }
211
+ .an-in {
212
+ flex: 1; min-width: 56px;
213
+ padding: 5px 8px; border-radius: 7px;
214
+ font-size: 12.5px; color: var(--ink);
215
+ background: #fff; border: 1px solid var(--line);
216
+ }
217
+ .an-in:focus { outline: none; border-color: rgba(26, 157, 95, 0.5); }
218
+ .an-ok {
219
+ flex-shrink: 0;
220
+ padding: 5px 10px; border-radius: 7px;
221
+ font-size: 12px; font-weight: 600; color: #fff;
222
+ background: var(--green);
223
+ }
224
+ .an-ok:disabled { opacity: 0.45; cursor: default; }
225
+ .an-skip { flex-shrink: 0; padding: 4px 2px; font-size: 11.5px; color: var(--dim); }
226
+ .an-skip:hover { color: var(--ink); }
227
+
182
228
  .tries { display: flex; flex-direction: column; gap: 6px; width: 100%; max-width: 420px; }
183
229
  .try {
184
230
  display: flex; align-items: center; gap: 8px; width: 100%;
@@ -21,8 +21,6 @@ const rows = computed(() => [
21
21
  { ic: '📁', b: '导入文件夹', s: '逐个摄入', act: () => { close(); inFolder.value?.click() } },
22
22
  { ic: '🖼', b: '附加图片', s: '发给视觉模型', act: () => { close(); inImg.value?.click() } },
23
23
  { ic: '📄', b: '附件文件', s: '随问题发给 AI', act: () => { close(); inAttach.value?.click() } },
24
- /* 连接器只有一份实现,在技能面板里。这里不再内嵌一份只读副本(信任档位还是另一套词),
25
- 点进来直接去那个面板,要管 MCP 也只在一个地方管。 */
26
24
  { ic: '🔌', b: '技能 · 连接器', s: '到技能面板看全部', act: () => { close(); setModule('tools') } },
27
25
  ])
28
26
 
@@ -32,7 +30,6 @@ function toggle() {
32
30
  function close() {
33
31
  open.value = false
34
32
  }
35
- /* 斜杠命令 /摄入 通过 store 信号打到这里,避免把 input 引用抬到全局 */
36
33
  watch(() => ui.ingestTick, () => inFile.value?.click())
37
34
 
38
35
  function onDocDown(e) {
@@ -1,18 +1,31 @@
1
1
  <script setup>
2
- import { onMounted, ref } from 'vue'
2
+ import { computed, onMounted, ref } from 'vue'
3
3
  import { getJSON } from '../api.js'
4
4
  import { state, setModule } from '../stores/app.js'
5
+ import { system, loadSystemInfo } from '../stores/system.js'
6
+ import { t } from '../lib/i18n.js'
5
7
 
6
- const uplink = ref('···')
8
+ const uplink = ref('···') // '···' | 'online' | 'offline'
7
9
  const tone = ref('dim')
8
10
 
11
+ const ver = computed(() => system.version)
12
+
13
+ const upd = computed(() => (system.update?.update_available ? system.update : null))
14
+
15
+ const uplinkText = computed(() => {
16
+ if (uplink.value === 'online') return t('common.online')
17
+ if (uplink.value === 'offline') return t('common.offlineShort')
18
+ return '···'
19
+ })
20
+
9
21
  onMounted(async () => {
22
+ loadSystemInfo()
10
23
  try {
11
24
  await getJSON('/status')
12
- uplink.value = '在线'
25
+ uplink.value = 'online'
13
26
  tone.value = 'ok'
14
27
  } catch {
15
- uplink.value = '离线'
28
+ uplink.value = 'offline'
16
29
  tone.value = 'bad'
17
30
  }
18
31
  })
@@ -21,7 +34,6 @@ function openPlanet() {
21
34
  window.open('/planet/', '_blank', 'noopener')
22
35
  }
23
36
 
24
- /* 这个 ⚙ 一直是死按钮(title 写着「P3 接入」)。设置面板补齐后接上去。 */
25
37
  function openSettings() {
26
38
  setModule(state.module === 'settings' ? 'overview' : 'settings')
27
39
  }
@@ -32,18 +44,24 @@ function openSettings() {
32
44
  <div class="brand">
33
45
  <div class="logo">F</div>
34
46
  <h1>Foxclaw</h1>
35
- <span class="ver">v3.0</span>
47
+ <span v-if="ver" class="ver">v{{ ver }}</span>
48
+ <button
49
+ v-if="upd"
50
+ class="upd"
51
+ :title="t('topbar.newVersion', { latest: upd.latest, current: upd.current })"
52
+ @click="openSettings"
53
+ >↑</button>
36
54
  </div>
37
55
  <div class="spacer" />
38
56
  <div class="stat">
39
- <span class="lbl">系统状态</span>
40
- <span class="val" :class="tone">{{ uplink }}</span>
57
+ <span class="lbl">{{ t('topbar.systemStatus') }}</span>
58
+ <span class="val" :class="tone">{{ uplinkText }}</span>
41
59
  </div>
42
- <button class="tbtn" title="打开 HKC 知识星球" @click="openPlanet">🌐 知识星球</button>
60
+ <button class="tbtn" :title="t('topbar.planet')" @click="openPlanet">{{ t('topbar.planetBtn') }}</button>
43
61
  <button
44
62
  class="tbtn"
45
63
  :class="{ on: state.module === 'settings' }"
46
- title="设置 (Ctrl+,)"
64
+ :title="t('topbar.settings')"
47
65
  @click="openSettings"
48
66
  >⚙</button>
49
67
  </header>
@@ -73,6 +91,16 @@ function openSettings() {
73
91
  padding: 2px 7px; border-radius: var(--r-pill);
74
92
  background: rgba(255, 255, 255, 0.7); border: 1px solid var(--line);
75
93
  }
94
+ .upd {
95
+ display: grid; place-items: center;
96
+ width: 18px; height: 18px; margin-left: -3px;
97
+ border-radius: 50%;
98
+ font-size: 11px; font-weight: 700; line-height: 1;
99
+ color: var(--amber);
100
+ background: rgba(214, 158, 46, 0.12);
101
+ border: 1px solid rgba(214, 158, 46, 0.35);
102
+ }
103
+ .upd:hover { background: rgba(214, 158, 46, 0.2); }
76
104
  .spacer { flex: 1; }
77
105
  .stat { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--dim); }
78
106
  .stat .val { font-weight: 700; color: var(--ink); }
@@ -78,7 +78,5 @@ function evSub(e) {
78
78
  </template>
79
79
 
80
80
  <style scoped>
81
- /* 事件行是只读快照,没有可点的动作 —— hover 高亮会让人以为点了能看详情。
82
- 之前这里是 <button> 但没绑事件,视觉上"能点"、实际不动。 */
83
81
  .pn-row.static:hover { background: transparent; }
84
82
  </style>
@@ -66,11 +66,8 @@ function toggle(id) { open.value = open.value === id ? '' : id }
66
66
  </div>
67
67
  </div>
68
68
 
69
- <div class="pn-note">
70
- <template v-if="p.info">
71
- 版本 <code>{{ p.info.version }}</code> · 数据目录 <code>{{ p.info.data_dir }}</code>
72
- </template>
73
- 数据来自 <code>/api/status</code> 与 <code>/api/system/info</code>。
69
+ <div v-if="p.info" class="pn-note">
70
+ 版本 <code>{{ p.info.version }}</code>
74
71
  </div>
75
72
  </PanelBox>
76
73
  </template>
@@ -17,9 +17,6 @@ const idx = computed(() => p.index || {})
17
17
  const kus = computed(() => Object.entries(p.stats?.ku_counts || {}))
18
18
  const busy = computed(() => p.ingest === 'busy')
19
19
 
20
- /* 用户开始写新内容时,把上一次的「已收录 N 条」收起来,否则会让人以为这版也提交过了。
21
- 只在**写入了非空内容**时清 —— 提交成功后代码会把 draft 置空,那不是用户输入,
22
- 不能让它把刚拿到的结果提示擦掉。 */
23
20
  watch(draft, (v) => {
24
21
  if (v && !busy.value) { p.ingest = 'idle'; p.ingestMsg = ''; p.ingestErr = '' }
25
22
  })
@@ -31,12 +28,9 @@ async function submit() {
31
28
  draft.value = ''
32
29
  title.value = ''
33
30
  } catch {
34
- /* 错误已写进 store,模板里显示 */
35
31
  }
36
32
  }
37
33
 
38
- /* ── 检索:POST /api/knowledge/search ──
39
- 后端一直有这条混合检索,界面却只给「最近录入」,想按关键词翻库只能去知识星球。 */
40
34
  const q = ref('')
41
35
  const mode = ref('hybrid')
42
36
  const MODES = [
@@ -77,8 +71,6 @@ async function doSearch() {
77
71
  }
78
72
  }
79
73
 
80
- /* ── 能力详情:GET /api/knowledge/ability/{key} ──
81
- 能力 chip 以前是死的,点了没反应 —— 而这条接口正是"这个能力到底够不够"的答案。 */
82
74
  const abKey = ref('')
83
75
  const abState = ref('idle') // idle | busy | ok | err
84
76
  const abErr = ref('')
@@ -104,7 +96,6 @@ async function showAbility(k) {
104
96
  }
105
97
  }
106
98
 
107
- /* coverage 在不同能力下是数字或对象,取几个常见键;取不到就显示 '—',不猜格式 */
108
99
  function covText(c) {
109
100
  if (c == null) return ''
110
101
  if (typeof c === 'number') return `${Math.round(c * 100)}%`
@@ -124,7 +115,6 @@ function covText(c) {
124
115
  :ready="ready"
125
116
  @refresh="refreshPanel('knowledge')"
126
117
  >
127
- <!-- 录入:短知识在这里手录;整本书 / 大文档走知识星球 -->
128
118
  <div class="kb-in">
129
119
  <textarea
130
120
  v-model="draft"
@@ -273,7 +263,6 @@ function covText(c) {
273
263
  <div class="pn-note meta">HKC 未启用,无法编译能力。</div>
274
264
  </template>
275
265
 
276
- <!-- 覆盖不足不是错误,是"知识还不足以具备该能力"(证据驱动 gate) -->
277
266
  <template v-else-if="ab.sufficient === false">
278
267
  <div class="kb-ab-h">{{ abKey }}<span class="v">知识不足</span></div>
279
268
  <div class="pn-note meta">{{ ab.reason || '覆盖不足,暂不授予该能力。' }}</div>
@@ -312,7 +301,6 @@ function covText(c) {
312
301
  </template>
313
302
 
314
303
  <style scoped>
315
- /* ── 录入卡 ── */
316
304
  .kb-in {
317
305
  margin-bottom: 14px;
318
306
  border-radius: var(--r-mid);
@@ -369,8 +357,6 @@ function covText(c) {
369
357
  }
370
358
  .go:hover:not(:disabled) { opacity: 0.9; }
371
359
  .go:active:not(:disabled) { transform: scale(0.97); }
372
- /* 禁用态原来用 --faint 压在 12% 墨底上,对比度不到 2:1、几乎读不出字。
373
- 改成实心浅底 + 深墨字:一眼看得出「现在点不了」,但字仍然清楚。 */
374
360
  .go:disabled {
375
361
  background: rgba(23, 32, 26, 0.07); color: var(--dim);
376
362
  border: 1px solid var(--line-soft);
@@ -380,7 +366,6 @@ function covText(c) {
380
366
  .sec-gap { margin-top: 14px; }
381
367
  .pn-note.meta.bad { color: var(--red); }
382
368
 
383
- /* ── 检索条 ── */
384
369
  .kb-find {
385
370
  display: flex; align-items: center; gap: 6px;
386
371
  margin-bottom: 8px;
@@ -424,7 +409,6 @@ function covText(c) {
424
409
  font-variant-numeric: tabular-nums;
425
410
  }
426
411
 
427
- /* ── 能力详情 ── */
428
412
  .pn-chips.gap { margin-top: 8px; }
429
413
  .kb-ab {
430
414
  margin-top: 10px; padding: 10px 12px;
@@ -13,7 +13,6 @@ onMounted(() => loadPanel('memory'))
13
13
  const ready = computed(() => !!p.stats.backend)
14
14
  const byAgent = computed(() => Object.entries(p.stats.by_agent || {}))
15
15
 
16
- /* 记忆没有「单条详情」端点,但 recent 已经带了 summary 全文 —— 直接进正文皮肤,不再打一次后端 */
17
16
  function open(m) {
18
17
  openDoc({
19
18
  id: `mem:${m.id}`,