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
@@ -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
  }
@@ -44,9 +44,6 @@ async function flip(s) {
44
44
  busy.value = ''
45
45
  }
46
46
 
47
- /* ── 单技能测试:POST /ahs/skills/{id}/test ──
48
- 走真管线(权限/断路器/SSRF),所以界面要能填参数,也要把 needs_confirmation /
49
- blocked / circuit_open 这些非成功态原样显示,不能只画个绿勾。 */
50
47
  const tId = ref('')
51
48
  const tArgs = ref({})
52
49
  const tBusy = ref(false)
@@ -74,7 +71,6 @@ function beginTest(s) {
74
71
  err.value = ''
75
72
  }
76
73
 
77
- /* params 是 {名: 类型} 的朴素 schema,类型只是提示;这里按声明做最小转换,其余原样送字符串 */
78
74
  function coerce(params, args) {
79
75
  const out = {}
80
76
  for (const [k, ty] of Object.entries(params || {})) {
@@ -133,7 +129,6 @@ async function dropSkill(s) {
133
129
 
134
130
  const splitList = (t) => String(t || '').split(/[,,\s]+/).map((s) => s.trim()).filter(Boolean)
135
131
 
136
- /* "city:str, days:int" → {city:"str", days:"int"} */
137
132
  function parseParams(txt) {
138
133
  const out = {}
139
134
  for (const part of String(txt || '').split(/[,,;\n]/)) {
@@ -145,7 +140,6 @@ function parseParams(txt) {
145
140
  return out
146
141
  }
147
142
 
148
- /* "Authorization: Bearer x" 每行一条 → dict(空行 / 无冒号行忽略) */
149
143
  function parseHeaders(txt) {
150
144
  const out = {}
151
145
  for (const line of String(txt || '').split('\n')) {
@@ -155,8 +149,6 @@ function parseHeaders(txt) {
155
149
  return out
156
150
  }
157
151
 
158
- /* ── 安装技能:POST /ahs/skills ──
159
- 后端只收 web_api / mcp;审核门 deny → 403, warn → 409 needs_ack(要再点一次「已知风险」) */
160
152
  const SIDE = { read: 'read · 只读', write: 'write · 写入', send: 'send · 代发', pay: 'pay · 付款' }
161
153
  const showIns = ref(false)
162
154
  const insBusy = ref(false)
@@ -220,8 +212,93 @@ async function doInstall(ack = false) {
220
212
  }
221
213
  }
222
214
 
223
- /* ── 连接器:POST|DELETE /ahs/mcp/servers ──
224
- 接入会真连一次 server 拉工具清单(可能几十秒);连不上是 502,原样显示 */
215
+ const MI_MAX = 4 * 1024 * 1024
216
+ const miOpen = ref(false)
217
+ const miBusy = ref(false)
218
+ const miErr = ref('')
219
+ const miOk = ref('')
220
+ const miAck = ref(false)
221
+ const miText = ref('')
222
+ const miFile = ref('')
223
+ const miB64 = ref('') // zip 的 base64;非空时优先于 miText
224
+ const miRef = ref(null)
225
+
226
+ function toggleMi() {
227
+ miOpen.value = !miOpen.value
228
+ miErr.value = ''
229
+ miOk.value = ''
230
+ miAck.value = false
231
+ }
232
+
233
+ function resetMi() {
234
+ miText.value = ''
235
+ miFile.value = ''
236
+ miB64.value = ''
237
+ miAck.value = false
238
+ if (miRef.value) miRef.value.value = ''
239
+ }
240
+
241
+ function toB64(file) {
242
+ return new Promise((resolve, reject) => {
243
+ const fr = new FileReader()
244
+ fr.onload = () => {
245
+ const s = String(fr.result || '')
246
+ resolve(s.slice(s.indexOf(',') + 1)) // 去掉 data:...;base64, 前缀
247
+ }
248
+ fr.onerror = () => reject(new Error('读取文件失败'))
249
+ fr.readAsDataURL(file)
250
+ })
251
+ }
252
+
253
+ async function onMiFile(ev) {
254
+ const f = ev.target.files && ev.target.files[0]
255
+ miFile.value = ''
256
+ miB64.value = ''
257
+ miErr.value = ''
258
+ miOk.value = ''
259
+ miAck.value = false
260
+ if (!f) return
261
+ miFile.value = `${f.name} · ${(f.size / 1024).toFixed(0)} KB`
262
+ if (f.size > MI_MAX) { miErr.value = '文件超过 4MB,拒绝上传'; return }
263
+ try {
264
+ if (/\.zip$/i.test(f.name)) {
265
+ miB64.value = await toB64(f)
266
+ miText.value = ''
267
+ } else {
268
+ miText.value = await f.text()
269
+ }
270
+ } catch (e) {
271
+ miErr.value = String(e.message || e)
272
+ }
273
+ }
274
+
275
+ async function doMdInstall(ack = false) {
276
+ if (miBusy.value) return
277
+ const content = miText.value.trim()
278
+ if (!ack && !content && !miB64.value) {
279
+ miErr.value = '粘贴 SKILL.md 正文,或选一个 .md / .zip 文件'
280
+ return
281
+ }
282
+ miBusy.value = true
283
+ miErr.value = ''
284
+ miOk.value = ''
285
+ const body = { acknowledged: ack }
286
+ if (miB64.value) body.package_b64 = miB64.value
287
+ else body.content = content
288
+ try {
289
+ const d = await postJSON('/ahs/md-skills', body)
290
+ const name = (d.skill && (d.skill.name || d.skill.id)) || '指令技能'
291
+ miOk.value = `${d.created ? '已安装' : '已更新'}「${name}」`
292
+ resetMi()
293
+ await refreshPanel('tools')
294
+ } catch (e) {
295
+ miErr.value = String(e.message || e)
296
+ miAck.value = !!(e.detail && e.detail.needs_ack)
297
+ } finally {
298
+ miBusy.value = false
299
+ }
300
+ }
301
+
225
302
  const showMcpAdd = ref(false)
226
303
  const mcpBusy = ref(false)
227
304
  const mcpErr = ref('')
@@ -265,7 +342,6 @@ async function addMcp(ack = false) {
265
342
  }
266
343
  }
267
344
 
268
- /* 重连:server 那边的工具会变,POST .../refresh 增量对齐注册表 */
269
345
  async function refreshMcp(s) {
270
346
  if (mcpBusy.value) return
271
347
  mcpBusy.value = true
@@ -392,6 +468,43 @@ async function delMcp(s) {
392
468
  </div>
393
469
  </div>
394
470
 
471
+ <div class="pn-sec">
472
+ <div class="pn-h">
473
+ 安装指令技能
474
+ <button class="mbtn" @click="toggleMi">{{ miOpen ? '收起' : '+ 上传' }}</button>
475
+ </div>
476
+ <div v-if="miOpen" class="tk-form">
477
+ <label class="f"><span>SKILL.md 或 OpenClaw 包(.md / .zip)</span>
478
+ <input ref="miRef" type="file" accept=".md,.markdown,.zip" @change="onMiFile" />
479
+ </label>
480
+ <div v-if="miFile" class="pn-note">{{ miFile }}</div>
481
+ <label class="f"><span>或粘贴正文(frontmatter + 指示)</span>
482
+ <textarea
483
+ v-model="miText"
484
+ rows="8"
485
+ spellcheck="false"
486
+ :disabled="!!miB64"
487
+ placeholder="--- name: my-skill / description: 什么时候用它 --- 然后写正文"
488
+ />
489
+ </label>
490
+ <div class="fbar">
491
+ <button class="go" :disabled="miBusy" @click="doMdInstall(false)">
492
+ {{ miBusy ? '安装中…' : '安装' }}
493
+ </button>
494
+ <button v-if="miAck" class="go warn" :disabled="miBusy" @click="doMdInstall(true)">
495
+ 已知风险,继续安装
496
+ </button>
497
+ <span v-if="miErr" class="tip bad">{{ miErr }}</span>
498
+ <span v-else-if="miOk" class="tip ok">{{ miOk }}</span>
499
+ <span v-else class="tip dim">只取 SKILL.md 与 _meta.json;包内 scripts/ 不落地、不执行</span>
500
+ </div>
501
+ </div>
502
+ <div v-else class="pn-note">
503
+ 指令技能是注入提示词的 markdown,不是可执行工具。命令行也能装:
504
+ <code>foxclaw skills install @owner/slug</code>、<code>github:owner/repo</code>、<code>./pkg.zip</code>。
505
+ </div>
506
+ </div>
507
+
395
508
  <div class="pn-sec">
396
509
  <div class="pn-h">
397
510
  安装技能
@@ -532,11 +645,6 @@ async function delMcp(s) {
532
645
  <button class="mbtn danger" @click="delMcp(s)">移除</button>
533
646
  </div>
534
647
  </div>
535
-
536
- <div class="pn-note">
537
- 指令技能正文 <code>/api/ahs/md-skills/{id}</code>,执行技能运行 <code>POST /api/ahs/skills/{id}/test</code>;
538
- 连接器与技能面板是同一份数据(<code>/api/ahs/mcp/servers</code>)。
539
- </div>
540
648
  </PanelBox>
541
649
  </template>
542
650
 
@@ -550,7 +658,6 @@ async function delMcp(s) {
550
658
  .sw.on { background: rgba(26, 157, 95, 0.12); color: var(--green); }
551
659
  .sw:disabled { opacity: 0.5; }
552
660
 
553
- /* 行内小按钮:测试 / 删除 / 新增 —— 原来「执行技能」整行是死的,点了没反应 */
554
661
  .mbtn {
555
662
  flex-shrink: 0; padding: 4px 10px; border-radius: var(--r-pill);
556
663
  font-size: 11px; font-family: var(--mono); color: var(--dim);
@@ -562,7 +669,6 @@ async function delMcp(s) {
562
669
  .mbtn.danger:hover { color: var(--red); border-color: rgba(229, 72, 77, 0.35); }
563
670
  .mbtn:disabled { opacity: 0.5; }
564
671
 
565
- /* ── 表单(安装技能 / 接入连接器 / 技能测试)── */
566
672
  .tk-form, .tk-test {
567
673
  margin: 4px 0 6px;
568
674
  padding: 11px 12px;
@@ -595,6 +701,7 @@ async function delMcp(s) {
595
701
  .f textarea { resize: vertical; line-height: 1.6; }
596
702
  .f input:focus, .f select:focus, .f textarea:focus { border-color: var(--line); background: #fff; }
597
703
  .f input::placeholder, .f textarea::placeholder { color: var(--dim); }
704
+ .f input[type='file'] { padding: 4px 6px; font-size: 11.5px; color: var(--dim); }
598
705
 
599
706
  .chk {
600
707
  display: flex; align-items: center; gap: 6px;
@@ -622,7 +729,6 @@ async function delMcp(s) {
622
729
  .tip.ok { color: var(--green); }
623
730
  .tip.bad { color: var(--red); white-space: normal; }
624
731
 
625
- /* 测试结果:成功/失败都原文展示,失败(needs_confirmation/blocked)不涂成成功色 */
626
732
  .tk-res {
627
733
  margin-top: 4px; padding: 8px 10px; border-radius: var(--r-sm);
628
734
  background: rgba(23, 32, 26, 0.03); border: 1px solid var(--line-soft);
@@ -1,7 +1,4 @@
1
- /* 7 个面板共用的时间/数字格式化。集中一份,避免每个组件各写一遍还写得不一样。 */
2
1
 
3
- /* 后端时间戳三种形态都有:秒 float(事件总线)、毫秒 int、ISO 字符串(记忆 created_at)。
4
- 字符串交给 Date 自己解析,数字按量级判断。 */
5
2
  function toDate(ts) {
6
3
  if (!ts) return null
7
4
  if (typeof ts === 'string') {
@@ -41,7 +38,6 @@ export function fmtMs(ms) {
41
38
  return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`
42
39
  }
43
40
 
44
- /* 截断到 n 个字符(中文按 1 计),用于行内摘要 */
45
41
  export function clip(s, n = 46) {
46
42
  const t = String(s ?? '').replace(/\s+/g, ' ').trim()
47
43
  return t.length > n ? t.slice(0, n) + '…' : t