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
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, onMounted, ref } from 'vue'
3
3
  import PanelBox from './PanelBox.vue'
4
- import { panels, loadPanel, refreshPanel, dropPreference } from '../../stores/panels.js'
4
+ import { panels, loadPanel, refreshPanel, dropPreference, approveSensitiveWrite, rejectSensitiveWrite } from '../../stores/panels.js'
5
5
  import { openDoc } from '../../stores/app.js'
6
6
  import { fmtTime, fmtNum, clip } from '../../lib/fmt.js'
7
7
 
@@ -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}`,
@@ -32,6 +31,16 @@ async function drop(pref) {
32
31
  try { await dropPreference(pref.id) } catch (e) { err.value = String(e.message || e) }
33
32
  busy.value = ''
34
33
  }
34
+
35
+ async function decide(item, keep) {
36
+ busy.value = item.id
37
+ err.value = ''
38
+ try {
39
+ if (keep) await approveSensitiveWrite(item.id)
40
+ else await rejectSensitiveWrite(item.id)
41
+ } catch (e) { err.value = String(e.message || e) }
42
+ busy.value = ''
43
+ }
35
44
  </script>
36
45
 
37
46
  <template>
@@ -48,10 +57,32 @@ async function drop(pref) {
48
57
  <div><b>{{ fmtNum(p.stats.shared ?? 0) }}</b><span>共享</span></div>
49
58
  <div><b>{{ fmtNum(p.stats.private ?? 0) }}</b><span>私有</span></div>
50
59
  <div><b>{{ fmtNum(p.prefs.length) }}</b><span>用户偏好</span></div>
60
+ <div v-if="p.pending.length"><b>{{ fmtNum(p.pending.length) }}</b><span>待确认</span></div>
51
61
  </div>
52
62
 
53
63
  <div v-if="err" class="pn-st err">{{ err }}</div>
54
64
 
65
+ <div v-if="p.pending.length" class="pn-sec pending">
66
+ <div class="pn-h">待确认写入 <em>{{ p.pending.length }}</em></div>
67
+ <div class="pn-empty">
68
+ 这几条含凭据、证件、健康或账户信息,按规矩没有直接写进长期记忆。确认无误再点「记住」。
69
+ </div>
70
+ <div v-for="it in p.pending" :key="it.id" class="pn-row">
71
+ <span class="m">
72
+ <b>{{ clip(it.content, 60) }}</b>
73
+ <span class="s">
74
+ {{ (it.categories || []).join(' · ') }} · {{ it.agent_id || '—' }} · {{ it.id }}
75
+ </span>
76
+ </span>
77
+ <button class="act ok" :disabled="busy === it.id" title="确认写入长期记忆" @click.stop="decide(it, true)">
78
+ 记住
79
+ </button>
80
+ <button class="act" :disabled="busy === it.id" title="丢弃,不写入" @click.stop="decide(it, false)">
81
+ 丢弃
82
+ </button>
83
+ </div>
84
+ </div>
85
+
55
86
  <div class="pn-cols">
56
87
  <div class="pn-sec">
57
88
  <div class="pn-h">最近记忆 <em>{{ p.recent.length }}</em></div>
@@ -101,13 +132,21 @@ async function drop(pref) {
101
132
  <div class="pn-note">
102
133
  后端 <code>{{ p.stats.backend || '—' }}</code> v{{ p.stats.version || '—' }} ·
103
134
  向量 <code>{{ p.stats.embedding || '—' }}</code>。
104
- 数据来自 <code>/api/memory/stats</code>、<code>/api/memory/recent</code>、<code>/api/preferences</code>。
135
+ 数据来自 <code>/api/memory/stats</code>、<code>/api/memory/recent</code>、<code>/api/preferences</code>、<code>/api/memory/pending</code>。
105
136
  </div>
106
137
  </PanelBox>
107
138
  </template>
108
139
 
109
140
  <style scoped>
110
141
  .sec-gap { margin-top: 14px; }
142
+ .pending .pn-h em { color: var(--amber, #d99a2b); }
143
+ .act {
144
+ flex-shrink: 0; padding: 4px 10px; border-radius: var(--r-pill);
145
+ font-size: 11px; font-weight: 600; color: var(--dim);
146
+ background: var(--surface-2); border: 1px solid var(--line-soft);
147
+ }
148
+ .act.ok { color: var(--accent, #3b82f6); border-color: currentColor; }
149
+ .act:disabled { opacity: 0.5; }
111
150
  .del {
112
151
  flex-shrink: 0; padding: 4px 10px; border-radius: var(--r-pill);
113
152
  font-size: 11px; font-weight: 600; color: var(--dim);
@@ -1,9 +1,10 @@
1
1
  <script setup>
2
+ import { t } from '../../lib/i18n.js'
3
+
2
4
  const props = defineProps({
3
5
  title: { type: String, default: '' },
4
6
  tag: { type: String, default: '' },
5
7
  state: { type: String, default: 'idle' },
6
- /* 已有旧数据时,刷新不要整块闪成「加载中」 */
7
8
  ready: { type: Boolean, default: false },
8
9
  })
9
10
  defineEmits(['refresh'])
@@ -16,21 +17,19 @@ defineEmits(['refresh'])
16
17
  <h2>{{ props.title }}</h2>
17
18
  <span v-if="props.tag" class="pn-tag">{{ props.tag }}</span>
18
19
  </div>
19
- <button class="rf" :class="{ spin: props.state === 'loading' }" title="重新拉取" @click="$emit('refresh')">
20
- 刷新
20
+ <button class="rf" :class="{ spin: props.state === 'loading' }" :title="t('common.refresh')" @click="$emit('refresh')">
21
+ {{ t('common.refresh') }}
21
22
  </button>
22
23
  </header>
23
24
  <div class="pb scroll">
24
- <div v-if="props.state === 'loading' && !props.ready" class="pn-st">加载中…</div>
25
- <div v-else-if="props.state === 'error'" class="pn-st err">后端未连接,这个面板暂时拉不到数据</div>
25
+ <div v-if="props.state === 'loading' && !props.ready" class="pn-st">{{ t('common.loading') }}</div>
26
+ <div v-else-if="props.state === 'error'" class="pn-st err">{{ t('common.offlinePanel') }}</div>
26
27
  <slot v-else />
27
28
  </div>
28
29
  </section>
29
30
  </template>
30
31
 
31
32
  <style scoped>
32
- /* min-width: 0 让面板能被父级 flex 压窄 —— 否则 flex item 的 min-width:auto 会把它按
33
- min-content 撑开,窗口拉到 420px 时面板比中栏还宽,右侧内容直接落到视口外。 */
34
33
  .panel { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
35
34
  .ph {
36
35
  display: flex; align-items: center; gap: 10px; flex-shrink: 0;
@@ -1,6 +1,4 @@
1
1
  <script setup>
2
- /* 复习面板 = SM-2 账本 + 今日队列 + 遗忘曲线。
3
- 卡片就是知识库里的 KU(复习状态在 KU.extra.review 里),不另存一份。 */
4
2
  import { computed, onMounted, ref } from 'vue'
5
3
  import PanelBox from './PanelBox.vue'
6
4
  import {
@@ -20,10 +18,6 @@ const pos = computed(() => Math.min(rv.index + 1, total.value))
20
18
  const masteryPct = computed(() => Math.round((st.value.avg_mastery || 0) * 100))
21
19
  const todo = computed(() => (st.value.due || 0) + (st.value.fresh || 0))
22
20
 
23
- /* 遗忘曲线:Ebbinghaus R = e^(-t/S)。
24
- SM-2 的语义是「复习时保留率约 90%」,由 R=0.9 反推稳定度
25
- S ≈ interval / ln(1/0.9) ≈ 9.49 × interval;EF 越高衰减越慢。
26
- 新卡没有 interval,给一个很小的 S,曲线会陡降 —— 这正是「不复习就忘」的直观样子。 */
27
21
  const W = 620
28
22
  const H = 130
29
23
  const DAYS = 30
@@ -32,8 +26,6 @@ const curve = computed(() => {
32
26
  const c = card.value
33
27
  const interval = c?.interval || 0
34
28
  const ef = c?.ease_factor || 2.5
35
- /* 新卡没有 interval,S 给 1.2 让曲线在 30 天窗口里真能看出衰减;
36
- 取 0 会让整条曲线贴死底边,看上去像渲染坏了。 */
37
29
  const S = interval > 0 ? 9.49 * interval * (ef / 2.5) : 1.2
38
30
 
39
31
  const pts = []
@@ -50,13 +42,10 @@ const curve = computed(() => {
50
42
  due: showDot
51
43
  ? { x: (dueDay / DAYS) * W, y: H - Math.exp(-dueDay / S) * H }
52
44
  : null,
53
- /* 今天的位置(曲线起点)保留率恒为 1 */
54
45
  now: { x: 0, y: 0 },
55
46
  }
56
47
  })
57
48
 
58
- /* by_day 是稀疏字典,补齐 7 天才能画成等距柱 */
59
- /* 不到一天时说「几小时后」比说「0 天后」有用得多 */
60
49
  const dueText = computed(() => {
61
50
  const c = card.value
62
51
  if (!c || !c.interval) return '新卡:曲线是「不复习就忘」的示意'
@@ -210,13 +199,10 @@ async function grade(g) {
210
199
  gap: 22px;
211
200
  align-items: start;
212
201
  }
213
- /* 断点看面板容器宽度,不是视口 —— 中栏只占窗口一半,
214
- 1600 的窗口里也只有 ~580px,按视口判断会永远两列挤成两条窄条。 */
215
202
  @container (max-width: 820px) {
216
203
  .rv-grid { grid-template-columns: minmax(0, 1fr); gap: 20px; }
217
204
  }
218
205
 
219
- /* ── 卡片 ── */
220
206
  .card {
221
207
  padding: 16px 18px 18px;
222
208
  border-radius: var(--r-md);
@@ -269,7 +255,6 @@ async function grade(g) {
269
255
  .done p, .idle p { font-size: 12px; line-height: 1.8; color: var(--dim); margin: 6px 0 14px; }
270
256
  .idle b { color: var(--ink); }
271
257
 
272
- /* ── 曲线 ── */
273
258
  .curve {
274
259
  display: block; width: 100%; height: 130px;
275
260
  overflow: visible;
@@ -292,7 +277,6 @@ async function grade(g) {
292
277
  color: #8a6a24;
293
278
  }
294
279
 
295
- /* ── 7 天负荷 ── */
296
280
  .load { display: flex; align-items: flex-end; gap: 6px; padding: 8px 2px 0; }
297
281
  .load .bar {
298
282
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
@@ -5,17 +5,18 @@ import { getJSON } from '../../api.js'
5
5
  import { currentLabel, loadModels, modelOptions, selectModel } from '../../stores/model.js'
6
6
  import { setModule } from '../../stores/app.js'
7
7
  import { setMode, chat, say } from '../../stores/chat.js'
8
+ import { profile, loadProfile, saveNickname } from '../../stores/profile.js'
9
+ import { loadSystemInfo, system } from '../../stores/system.js'
10
+ import { i18n, LOCALES, setLocale, t } from '../../lib/i18n.js'
8
11
 
9
- /* __APP_INFO__ 由 vite define 在构建时注入 { git:{sha,ts}, built };没有 git 时是 dev。
10
- 用 typeof 兜一层:万一在没走过 vite 的环境里被单独加载,不要抛 ReferenceError。 */
11
12
  const info = typeof __APP_INFO__ === 'undefined' ? null : __APP_INFO__
12
13
  const sha = info?.git?.sha || 'dev'
13
- const age = (() => {
14
+ const age = computed(() => {
14
15
  const ts = info?.git?.ts || 0
15
- if (!ts) return '本地构建'
16
+ if (!ts) return t('settings.buildLocal')
16
17
  const d = Math.floor((Date.now() - ts) / 86400000)
17
- return d <= 0 ? '今天' : `${d} 天前`
18
- })()
18
+ return d <= 0 ? t('settings.buildToday') : t('settings.buildDays', { n: d })
19
+ })
19
20
 
20
21
  const open = ref('') // '' | 'model' | 'keys' —— 一次只展开一组,行数不会失控
21
22
  const up = ref(null) // null=探测中 | true | false
@@ -24,23 +25,59 @@ const err = ref('')
24
25
  const cur = computed(() => currentLabel())
25
26
  const options = computed(() => modelOptions())
26
27
 
27
- /* 只列真正存在的键位。ChatDock.onKey 里没有 Ctrl+Enter 收录、没有 / 之外的快捷键,
28
- 这里就不写 —— 设置里写一条按不出来的键,比不写更糟。 */
29
- const KEYS = [
30
- ['Enter', '发送'],
31
- ['Shift + Enter', '换行'],
32
- ['/', '命令补全(如 /搜索)'],
33
- ['@', '技能补全'],
34
- ['↑ ↓', '在补全列表里选'],
35
- ['Esc', '关闭补全 / 浮层'],
36
- ['Ctrl + ,', '打开设置'],
37
- ]
28
+ const KEYS = computed(() => [
29
+ ['Enter', t('key.enter')],
30
+ ['Shift + Enter', t('key.shiftEnter')],
31
+ ['/', t('key.slash')],
32
+ ['@', t('key.at')],
33
+ ['↑ ↓', t('key.arrows')],
34
+ ['Esc', t('key.esc')],
35
+ ['Ctrl + ,', t('key.ctrlComma')],
36
+ ])
37
+
38
+ const nickDraft = ref('')
38
39
 
39
40
  onMounted(() => {
40
41
  loadModels()
42
+ loadSystemInfo()
43
+ loadProfile().then(() => { nickDraft.value = profile.nickname })
41
44
  probe()
42
45
  })
43
46
 
47
+ async function saveNick() {
48
+ try {
49
+ await saveNickname(nickDraft.value)
50
+ say(profile.nickname ? t('settings.saidNick', { name: profile.nickname }) : t('settings.saidNickCleared'))
51
+ open.value = ''
52
+ } catch (e) {
53
+ say(t('settings.errSaveNick', { msg: e.message }), 4000)
54
+ }
55
+ }
56
+
57
+ function pickLang(id) {
58
+ setLocale(id)
59
+ const name = LOCALES.find((l) => l.id === id)?.label || id
60
+ say(t('settings.saidLang', { name }))
61
+ }
62
+
63
+ const upd = computed(() => system.update || null)
64
+ const updLabel = computed(() => {
65
+ const u = upd.value
66
+ if (!u) return '…'
67
+ if (!u.enabled) return t('settings.updNotEnabled')
68
+ if (u.update_available) return t('settings.updAvailable', { v: u.latest })
69
+ if (u.latest) return t('settings.updLatest')
70
+ return u.error ? t('settings.updFailed') : t('settings.updPending')
71
+ })
72
+ const updHint = computed(() => {
73
+ const u = upd.value
74
+ if (!u) return t('common.loading')
75
+ if (!u.enabled) return t('settings.updHintOff')
76
+ if (u.error) return t('settings.updHintError', { msg: u.error })
77
+ if (u.update_available) return t('settings.updHintNew', { cur: u.current, latest: u.latest })
78
+ return t('settings.updHintOk', { cur: u.current })
79
+ })
80
+
44
81
  async function probe() {
45
82
  up.value = null
46
83
  try {
@@ -53,6 +90,7 @@ async function probe() {
53
90
 
54
91
  async function refresh() {
55
92
  await loadModels(true).catch(() => {})
93
+ loadSystemInfo(true) // 更新状态也刷一次(后端 24h 缓存内不会真的外呼)
56
94
  probe()
57
95
  }
58
96
 
@@ -64,16 +102,16 @@ async function pick(o) {
64
102
  name: o.source === 'custom' ? o.key : null,
65
103
  })
66
104
  open.value = ''
67
- say(`模型已切到 ${o.label}`)
105
+ say(t('settings.saidSwitched', { label: o.label }))
68
106
  } catch (e) {
69
107
  err.value = e.message
70
- say(`切换失败:${e.message}`, 4000)
108
+ say(t('settings.errSwitch', { msg: e.message }), 4000)
71
109
  }
72
110
  }
73
111
 
74
112
  function mode(m) {
75
113
  setMode(m)
76
- say(m === 'stream' ? '流式:逐字输出' : '编排:先出计划再逐步执行')
114
+ say(m === 'stream' ? t('settings.saidStream') : t('settings.saidOrchestrate'))
77
115
  }
78
116
 
79
117
  const ICONS = {
@@ -82,19 +120,20 @@ const ICONS = {
82
120
  chat: ['M19.6 14.8a1.8 1.8 0 0 1-1.8 1.8H8.6L5 20v-3.4H6.2A1.8 1.8 0 0 1 4.4 14.8V6.4A1.8 1.8 0 0 1 6.2 4.6h11.6a1.8 1.8 0 0 1 1.8 1.8z'],
83
121
  keys: ['M3.8 8.2h16.4v7.6H3.8z', 'M7 11h.01', 'M11 11h.01', 'M15 11h.01', 'M9 13.8h6'],
84
122
  server: ['M4.4 5.4h15.2v4.8H4.4z', 'M4.4 13.8h15.2v4.8H4.4z', 'M7.4 7.8h.01', 'M7.4 16.2h.01'],
123
+ globe: ['M12 3.4a8.6 8.6 0 1 0 0 17.2 8.6 8.6 0 0 0 0-17.2z', 'M3.6 12h16.8', 'M12 3.4c2.2 2.4 3.4 5.4 3.4 8.6s-1.2 6.2-3.4 8.6c-2.2-2.4-3.4-5.4-3.4-8.6s1.2-6.2 3.4-8.6z'],
85
124
  }
86
125
 
87
126
  const icon = (n) => ICONS[n] || ICONS.gear
88
127
  </script>
89
128
 
90
129
  <template>
91
- <PanelBox title="设置" tag="Settings" state="idle" :ready="true" @refresh="refresh">
130
+ <PanelBox :title="t('settings.title')" tag="Settings" state="idle" :ready="true" @refresh="refresh">
92
131
  <div class="card">
93
- <div class="grp">模型</div>
132
+ <div class="grp">{{ t('settings.groupModel') }}</div>
94
133
 
95
134
  <button class="row" :aria-expanded="open === 'model'" @click="open = open === 'model' ? '' : 'model'">
96
135
  <span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('gear')" :key="i" :d="d" /></svg></span>
97
- <span class="m"><b>模型与供应商</b><span>切换后服务端立即生效,不用重启</span></span>
136
+ <span class="m"><b>{{ t('settings.modelProvider') }}</b><span>{{ t('settings.modelProviderSub') }}</span></span>
98
137
  <span class="hint"><i class="dot" :style="{ background: cur.color }" />{{ cur.name }}</span>
99
138
  <span class="chev" :class="{ open: open === 'model' }">›</span>
100
139
  </button>
@@ -109,38 +148,59 @@ const icon = (n) => ICONS[n] || ICONS.gear
109
148
  >
110
149
  <span class="dot" :style="{ background: o.color }" />
111
150
  <span class="m"><b>{{ o.label }}</b><span>{{ o.sub }}</span></span>
112
- <span v-if="o.active" class="pn-tag green">当前</span>
113
- <span v-else-if="o.configured === false" class="pn-tag amber">未配置</span>
151
+ <span v-if="o.active" class="pn-tag green">{{ t('settings.current') }}</span>
152
+ <span v-else-if="o.configured === false" class="pn-tag amber">{{ t('settings.unconfigured') }}</span>
114
153
  </button>
115
154
  <p class="tip">
116
- 「未配置」= 后端缺该厂商的 Key。自定义模型的 Key / Base URL 在输入框左侧的模型胶囊里加。
155
+ {{ t('settings.providerNote') }}
117
156
  </p>
118
157
  <p v-if="err" class="tip err">{{ err }}</p>
119
158
  </div>
120
159
 
121
160
  <button class="row" @click="setModule('usage')">
122
161
  <span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('gauge')" :key="i" :d="d" /></svg></span>
123
- <span class="m"><b>使用情况</b><span>token、调用次数、余额</span></span>
162
+ <span class="m"><b>{{ t('settings.usage') }}</b><span>{{ t('settings.usageSub') }}</span></span>
124
163
  <span class="chev">›</span>
125
164
  </button>
126
165
  </div>
127
166
 
128
167
  <div class="card">
129
- <div class="grp">对话</div>
168
+ <div class="grp">{{ t('settings.groupChat') }}</div>
130
169
 
131
170
  <div class="row static">
132
171
  <span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('chat')" :key="i" :d="d" /></svg></span>
133
- <span class="m"><b>模式</b><span>逐字流式 / 先出计划再逐步执行</span></span>
172
+ <span class="m"><b>{{ t('settings.mode') }}</b><span>{{ t('settings.modeSub') }}</span></span>
134
173
  <span class="seg">
135
- <button :class="{ on: chat.mode === 'stream' }" @click="mode('stream')">流式</button>
136
- <button :class="{ on: chat.mode === 'orchestrate' }" @click="mode('orchestrate')">编排</button>
174
+ <button :class="{ on: chat.mode === 'stream' }" @click="mode('stream')">{{ t('settings.stream') }}</button>
175
+ <button :class="{ on: chat.mode === 'orchestrate' }" @click="mode('orchestrate')">{{ t('settings.orchestrate') }}</button>
137
176
  </span>
138
177
  </div>
139
178
 
179
+ <button class="row" :aria-expanded="open === 'nick'" @click="open = open === 'nick' ? '' : 'nick'">
180
+ <span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('chat')" :key="i" :d="d" /></svg></span>
181
+ <span class="m"><b>{{ t('settings.nickname') }}</b><span>{{ t('settings.nicknameSub') }}</span></span>
182
+ <span class="hint">{{ profile.nickname || t('common.notSet') }}</span>
183
+ <span class="chev" :class="{ open: open === 'nick' }">›</span>
184
+ </button>
185
+
186
+ <div v-if="open === 'nick'" class="sub">
187
+ <div class="nick-edit">
188
+ <input
189
+ v-model="nickDraft"
190
+ class="nick-in"
191
+ maxlength="24"
192
+ :placeholder="t('settings.nicknamePh')"
193
+ @keydown.enter.prevent="saveNick"
194
+ />
195
+ <button class="nick-save" :disabled="profile.saving" @click="saveNick">{{ t('common.save') }}</button>
196
+ </div>
197
+ <p class="tip">{{ t('settings.nicknameNote') }}</p>
198
+ </div>
199
+
140
200
  <button class="row" :aria-expanded="open === 'keys'" @click="open = open === 'keys' ? '' : 'keys'">
141
201
  <span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('keys')" :key="i" :d="d" /></svg></span>
142
- <span class="m"><b>快捷键</b><span>输入框里的键位</span></span>
143
- <span class="hint">{{ KEYS.length }} 条</span>
202
+ <span class="m"><b>{{ t('settings.shortcuts') }}</b><span>{{ t('settings.shortcutsSub') }}</span></span>
203
+ <span class="hint">{{ t('common.count', { n: KEYS.length }) }}</span>
144
204
  <span class="chev" :class="{ open: open === 'keys' }">›</span>
145
205
  </button>
146
206
 
@@ -153,20 +213,48 @@ const icon = (n) => ICONS[n] || ICONS.gear
153
213
  </div>
154
214
 
155
215
  <div class="card">
156
- <div class="grp">系统</div>
216
+ <div class="grp">{{ t('settings.groupUi') }}</div>
217
+
218
+ <div class="row static">
219
+ <span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('globe')" :key="i" :d="d" /></svg></span>
220
+ <span class="m"><b>{{ t('settings.language') }}</b><span>{{ t('settings.languageSub') }}</span></span>
221
+ <span class="seg">
222
+ <button
223
+ v-for="l in LOCALES"
224
+ :key="l.id"
225
+ :class="{ on: i18n.locale === l.id }"
226
+ @click="pickLang(l.id)"
227
+ >{{ l.label }}</button>
228
+ </span>
229
+ </div>
230
+
231
+ <p class="tip">{{ t('settings.languageNote') }}</p>
232
+ </div>
233
+
234
+ <div class="card">
235
+ <div class="grp">{{ t('settings.groupSystem') }}</div>
157
236
 
158
- <button class="row" :title="up ? '再探一次' : '重试'" @click="probe">
237
+ <button class="row" :title="up ? t('settings.reprobe') : t('settings.retry')" @click="probe">
159
238
  <span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('server')" :key="i" :d="d" /></svg></span>
160
- <span class="m"><b>后端连接</b><span>/api · {{ up === null ? '探测中…' : up ? '在线' : '离线' }}</span></span>
239
+ <span class="m">
240
+ <b>{{ t('settings.backend') }}</b>
241
+ <span>/api · {{ up === null ? t('settings.probing') : up ? t('common.online') : t('common.offlineShort') }}</span>
242
+ </span>
161
243
  <span class="pn-tag" :class="up ? 'green' : up === false ? 'amber' : ''">
162
- {{ up === null ? '…' : up ? '正常' : '重试' }}
244
+ {{ up === null ? '…' : up ? t('settings.ok') : t('settings.retry') }}
163
245
  </span>
164
246
  </button>
247
+
248
+ <div class="row static">
249
+ <span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('gauge')" :key="i" :d="d" /></svg></span>
250
+ <span class="m"><b>{{ t('settings.updates') }}</b><span>{{ updHint }}</span></span>
251
+ <span class="pn-tag" :class="upd?.update_available ? 'amber' : ''">{{ updLabel }}</span>
252
+ </div>
165
253
  </div>
166
254
 
167
255
  <div class="foot">
168
256
  <span class="mono">git@{{ sha }} · {{ age }}</span>
169
- <span class="dev" title="本地构建">▭</span>
257
+ <span class="dev" :title="t('settings.buildLocal')">▭</span>
170
258
  </div>
171
259
  </PanelBox>
172
260
  </template>
@@ -235,6 +323,22 @@ const icon = (n) => ICONS[n] || ICONS.gear
235
323
  .tip { padding: 4px 8px 2px; font-size: 11.5px; line-height: 1.6; color: var(--dim); }
236
324
  .tip.err { color: var(--red); }
237
325
 
326
+ .nick-edit { display: flex; align-items: center; gap: 6px; padding: 4px 8px 2px; }
327
+ .nick-in {
328
+ flex: 1; min-width: 0;
329
+ padding: 5px 8px; border-radius: 7px;
330
+ font-size: 12.5px; color: var(--ink);
331
+ background: #fff; border: 1px solid var(--line);
332
+ }
333
+ .nick-in:focus { outline: none; border-color: rgba(26, 157, 95, 0.5); }
334
+ .nick-save {
335
+ flex-shrink: 0;
336
+ padding: 5px 11px; border-radius: 7px;
337
+ font-size: 12px; font-weight: 600; color: #fff;
338
+ background: var(--green);
339
+ }
340
+ .nick-save:disabled { opacity: 0.45; }
341
+
238
342
  .kv { display: flex; align-items: center; gap: 10px; padding: 4px 8px; }
239
343
  .kv kbd {
240
344
  min-width: 92px; flex-shrink: 0;
@@ -10,7 +10,6 @@ onMounted(() => loadPanel('tasks'))
10
10
  const ready = computed(() => p.state === 'ok' && Object.keys(p.sched).length > 0)
11
11
  const byState = computed(() => p.procStats?.by_state || {})
12
12
 
13
- /* 后端把每个任务建模成「进程」,状态机 created→queued→scheduled→running→completed/failed */
14
13
  function stateTag(s) {
15
14
  return s === 'completed' ? 'green' : s === 'failed' ? 'red' : s === 'running' ? 'amber' : ''
16
15
  }