foxclaw 0.1.0 → 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 (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,1806 @@
1
+ import './auth.js?v=5.37';
2
+ import { escapeHtml } from './util.js?v=2.9';
3
+ import { renderMarkdown, renderReply } from './markdown.js?v=2.9';
4
+ import './visuals.js?v=5.36'; // 球体特效默认关 + foxclawVisual.on/off/toggle API
5
+ import { _origin, API, WS, _lastKernelData } from './state.js?v=5.31';
6
+ import { fetchAMapConfig, kpSet, renderFinance, renderLearning, renderItinerary, itinSideHTML, renderSituation, drawAMapInto, initMapSearch } from './domain.js?v=5.31';
7
+ import { openSetMenu, switchProvider, uiPref, openAuthSetting } from './settings.js?v=5.36'; // uiPref.ballAnim 默认 false
8
+ import './chat_model_picker.js?v=1.4'; // chat 输入框旁的下拉式模型选择器;v1.4 保存即切换 + BASE URL 自动补 /v1 + 模型 ID 软提示
9
+
10
+ let activeAgent='supervisor';
11
+ const AGENT_CN={supervisor:'主管',finance:'财务',office:'办公学习',admin:'行程'};
12
+
13
+ function setUplink(on){
14
+ const el=document.getElementById('upStat');
15
+ if(el){el.textContent=on?'100%':'离线'; el.style.color=on?'var(--green)':'var(--amber)';}
16
+ }
17
+
18
+ function showAuthHint(){
19
+ if(document.getElementById('authHint')) return;
20
+ const bar=document.createElement('div');
21
+ bar.id='authHint';
22
+ bar.innerHTML='<span class="ah-ico">⚠</span><span class="ah-txt">后端已开启鉴权,本机浏览器尚未配置访问密钥,数据无法加载</span>'
23
+ +'<button class="ah-btn" id="ah-go">配置密钥</button><button class="ah-x" id="ah-x">✕</button>';
24
+ document.body.appendChild(bar);
25
+ document.getElementById('ah-go').onclick=()=>{ bar.remove(); openAuthSetting(); };
26
+ document.getElementById('ah-x').onclick=()=>bar.remove();
27
+ }
28
+ window.__foxclawAuthHint=showAuthHint;
29
+
30
+ async function loadStatus(){
31
+ try{
32
+ const r=await fetch(`${API}/status`); if(!r.ok)throw 0;
33
+ const data=await r.json(); setUplink(true);
34
+ document.querySelectorAll('.acard').forEach(card=>{
35
+ const id=card.dataset.agent; const st=data[id];
36
+ const dot=card.querySelector('.st');
37
+ if(st&&dot){dot.innerHTML='<i></i>在线'; dot.style.color='var(--green)';}
38
+ });
39
+ }catch(e){ setUplink(false); }
40
+ }
41
+
42
+ async function loadMemStats(){
43
+ const set=(id,v)=>{const el=document.getElementById(id); if(el)el.textContent=v;};
44
+ try{
45
+ const d=await fetch(`${API}/memory/stats`).then(r=>r.json());
46
+ set('msTotal',(d.total_memories||0).toLocaleString());
47
+ set('msPrivate',(d.private||0).toLocaleString());
48
+ set('msShared',(d.shared||0).toLocaleString());
49
+ set('msRel',(d.relations||0).toLocaleString());
50
+ set('memCount',(d.total_memories||0)+' 条');
51
+ set('memVec',(d.total_memories||0).toLocaleString()); // 兼容旧元素(若仍存在)
52
+ set('memRel',(d.relations||0).toLocaleString());
53
+ }catch(e){}
54
+ }
55
+
56
+ async function loadKnowList(){
57
+ const list=document.getElementById('knowList'); if(!list)return;
58
+ try{
59
+ const r=await fetch(`${API}/learning/analysis`).then(x=>x.json());
60
+ const items=r.items||[];
61
+ const cnt=document.getElementById('knowCount'); if(cnt)cnt.textContent=(r.total_items??items.length)+' 条';
62
+ if(!items.length){ list.innerHTML='<div class="kp-empty">知识库为空 · 录入学习数据后显示</div>'; return; }
63
+ list.innerHTML=items.slice(0,20).map((k,i)=>`<div class="info-item" data-idx="${i}">
64
+ <div class="it-h"><b>${escapeHtml(k.title||k.name||'知识条目')}</b>
65
+ ${(k.topic||k.tag)?`<span class="it-tag">${escapeHtml(k.topic||k.tag)}</span>`:''}</div>
66
+ ${k.content?`<div class="it-sum">${escapeHtml(k.content)}</div>`:''}
67
+ ${k.source?`<div class="it-meta">来源: ${escapeHtml(k.source)}</div>`:''}</div>`).join('');
68
+ list.querySelectorAll('.info-item').forEach(el=>el.onclick=()=>{
69
+ const it=items[+el.dataset.idx];
70
+ if(it) showKnowledgeArticle(it); else showKnowledgeFull();
71
+ });
72
+ }catch(e){ list.innerHTML='<div class="kp-empty">后端未连接</div>'; }
73
+ }
74
+
75
+ document.querySelectorAll('.acard').forEach(card=>{
76
+ card.addEventListener('click',()=>{
77
+ document.querySelectorAll('.acard').forEach(c=>c.classList.remove('on'));
78
+ card.classList.add('on');
79
+ activeAgent=card.dataset.agent;
80
+ const cmd=document.getElementById('cmd');
81
+ if(cmd)cmd.placeholder=`告诉我你想做什么… ${AGENT_CN[activeAgent]} Agent 将处理`;
82
+ loadKernel(activeAgent);
83
+ showInCore(activeAgent); // 当前 Agent 内容投到中央焦点空间
84
+ refreshAgentStatus();
85
+ });
86
+ });
87
+ document.querySelectorAll('.abtn').forEach(btn=>{
88
+ btn.addEventListener('click',()=>{
89
+ const ag=btn.dataset.agent, wasOn=btn.classList.contains('on');
90
+ document.querySelectorAll('.abtn').forEach(b=>b.classList.remove('on'));
91
+ if(wasOn){ clearCore(); return; }
92
+ btn.classList.add('on');
93
+ loadKernel(ag); showInCore(ag);
94
+ });
95
+ });
96
+ function refreshAgentStatus(){
97
+ document.querySelectorAll('.acard').forEach(c=>{
98
+ const st=c.querySelector('.st-tx'); if(!st)return;
99
+ if(c.classList.contains('busy')) st.textContent='工作中';
100
+ else if(c.classList.contains('on')) st.textContent='在线';
101
+ else st.textContent='离线';
102
+ });
103
+ }
104
+ refreshAgentStatus();
105
+
106
+ const Center = {
107
+ mode:'home',
108
+ panel(){ // 打开面板层(知识/模块/领域视图);返回 #coreStage 供调用方写 innerHTML
109
+ const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
110
+ document.getElementById('coreChat')?.classList.remove('show'); // 关对话浮层
111
+ wrap?.classList.add('busy'); stage?.classList.add('show');
112
+ this.mode='panel'; return stage;
113
+ },
114
+ chat(){ // 打开对话浮层(对话/会话/单轮回复);返回 #coreChat
115
+ const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
116
+ wrap?.classList.remove('busy');
117
+ if(stage){ stage.classList.remove('show'); stage.innerHTML=''; } // 拆面板层
118
+ const chat=document.getElementById('coreChat'); chat?.classList.add('show');
119
+ this.mode='chat'; return chat;
120
+ },
121
+ home(){ // 回到 3D 核心:两层都收起
122
+ const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
123
+ document.getElementById('coreChat')?.classList.remove('show');
124
+ wrap?.classList.remove('busy');
125
+ if(stage){ stage.classList.remove('show'); stage.innerHTML=''; }
126
+ this.mode='home';
127
+ }
128
+ };
129
+ function clearCore(){ Center.home(); _syncNav('overview'); } // 兼容旧调用名(9 处)→ 统一走中枢
130
+ async function showInCore(agent){
131
+ const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
132
+ document.getElementById('coreChat')?.classList.remove('show');
133
+ const m=KP_META[agent]||KP_META.office;
134
+ if(agent==='supervisor'){ clearCore(); return; }
135
+ Center.panel();
136
+ stage.innerHTML=`<div class="cs-head"><div class="t"><span class="dot"></span>${m.title} <em>${m.tag}</em></div>
137
+ <span class="x" id="csClose" title="返回核心">✕</span></div>
138
+ <div class="cs-body" id="csBody"><div class="kp-empty" style="padding:30px 0">加载中…</div></div>`;
139
+ document.getElementById('csClose').onclick=clearCore;
140
+ const body=document.getElementById('csBody');
141
+ try{
142
+ const d=_lastKernelData[agent]||{};
143
+ if(agent==='admin'){
144
+ body.innerHTML=`<div style="position:relative;height:100%;min-height:300px">
145
+ <div id="coreMap" style="width:100%;height:100%;border-radius:10px;overflow:hidden"></div>
146
+ <div class="map-search"><input id="mapSearchInput" placeholder="🔍 搜索地点…"><div id="mapSearchResults" class="map-search-results"></div></div>
147
+ </div>`;
148
+ setTimeout(()=>{drawAMapInto('coreMap',d.map_data||[]); initMapSearch();},60);
149
+ }else if(agent==='office'){
150
+ const items=(d.items||[]);
151
+ const ingest='<div class="ingest-row" style="max-width:520px;margin:0 auto 14px">'
152
+ +'<button class="ing-btn" onclick="document.getElementById(\'cmdAttach\')?.click()">📄 导入文件</button>'
153
+ +'<button class="ing-btn" onclick="document.getElementById(\'cmdFolder\')?.click()">📁 导入文件夹</button>'
154
+ +'<button class="ing-btn" onclick="openEntry(\'office\')">+ 写入知识点</button></div>';
155
+ if(!items.length){
156
+ body.innerHTML=ingest+'<div class="kp-empty" style="padding:24px 0">还没有录入知识 · 用上面的按钮导入文件/文件夹到知识星球(HKC),或手写一条知识点。</div>';
157
+ }else{
158
+ body.innerHTML=ingest+'<div id="_kl"></div>';
159
+ _renderKnowList(document.getElementById('_kl'), items, ()=>showInCore('office'));
160
+ }
161
+ }else if(agent==='finance'){
162
+ body.innerHTML=renderFinance(d);
163
+ }
164
+ }catch(e){ body.innerHTML='<div class="kp-empty">后端未连接</div>'; }
165
+ }
166
+
167
+ const KP_META={
168
+ supervisor:{title:'全局态势',tag:'Situation'},
169
+ finance:{title:'财务驾驶舱',tag:'Finance'},
170
+ admin:{title:'行程规划',tag:'Itinerary'}, // 视图,不是 Agent
171
+ office:{title:'知识与学习',tag:'Knowledge'},
172
+ };
173
+
174
+ async function loadKernel(agent){
175
+ const m=KP_META[agent]||KP_META.office;
176
+ try{
177
+ if(agent==='finance'){
178
+ const d=await fetch(`${API}/finance/analysis?period=2026-06&current_balance=11000`).then(r=>r.json()).catch(()=>({}));
179
+ _lastKernelData.finance=d; kpSet(m.title,m.tag,renderFinance(d||{}));
180
+ }else if(agent==='office'){
181
+ const d=await fetch(`${API}/learning/analysis`).then(r=>r.json()).catch(()=>({}));
182
+ _lastKernelData.office=d; kpSet(m.title,m.tag,renderLearning(d||{}));
183
+ }else if(agent==='admin'){
184
+ const d=await fetch(`${API}/itinerary/plan`).then(r=>r.json()).catch(()=>({}));
185
+ _lastKernelData.admin=d; kpSet(m.title,m.tag,renderItinerary(d||{}));
186
+ if(autoMap)openOverlay('admin'); // 地图自动全屏
187
+ }else if(agent==='supervisor'){
188
+ const d=await fetch(`${API}/orchestration/situation`).then(r=>r.json()).catch(()=>({}));
189
+ _lastKernelData.supervisor=d; kpSet(m.title,m.tag,renderSituation(d||{}));
190
+ }else{
191
+ kpSet(m.title,m.tag,document.getElementById('taskList')?.outerHTML||'<div class="kp-empty">暂无任务</div>');
192
+ }
193
+ }catch(e){ kpSet(m.title,m.tag,'<div class="kp-empty">后端未连接 · 该面板需真实数据</div>'); }
194
+ }
195
+
196
+ function importDomainFile(agent){
197
+ const map={finance:{d:'finance',cn:'交易'}, office:{d:'learning',cn:'知识点'}, admin:{d:'itinerary',cn:'行程点'}};
198
+ const m=map[agent]||map.finance;
199
+ let inp=document.getElementById('_domainImp');
200
+ if(!inp){ inp=document.createElement('input'); inp.type='file'; inp.id='_domainImp'; inp.style.display='none'; inp.accept='.csv,.xlsx,.json'; document.body.appendChild(inp); }
201
+ inp.onchange=async()=>{
202
+ const f=inp.files&&inp.files[0]; inp.value=''; if(!f) return;
203
+ showReplyPop(`📁 导入${m.cn}:${f.name}`, agent, true);
204
+ fillReplyPop('', '⬆ 上传解析中…', 'ok');
205
+ try{
206
+ const fd=new FormData(); fd.append('file', f);
207
+ const r=await fetch(`${API}/${m.d}/import/file`,{method:'POST',body:fd});
208
+ const j=await r.json().catch(()=>({}));
209
+ if(!r.ok){ fillReplyPop('', `导入失败(${r.status}):${String(j.detail||'').slice(0,120)}`, 'err'); return; }
210
+ fillReplyPop(`✓ 已导入 ${j.imported||0} 条${m.cn}`, j.note||'刷新面板查看', 'ok');
211
+ if(typeof loadKernel==='function'){ try{ loadKernel(agent); }catch(e){} }
212
+ }catch(e){ fillReplyPop('', '请求失败:后端未连接', 'err'); }
213
+ };
214
+ inp.click();
215
+ }
216
+
217
+ const agentTasks={};
218
+ function setAgentBusy(agent, task){
219
+ agentTasks[agent]=task;
220
+ const card=document.querySelector(`.acard[data-agent="${agent}"]`);
221
+ if(!card)return;
222
+ card.classList.add('busy');
223
+ const lbl=card.querySelector('.task-label');
224
+ if(lbl) lbl.textContent='▸ '+task;
225
+ if(typeof refreshAgentStatus==='function') refreshAgentStatus();
226
+ }
227
+ function clearAgentBusy(agent){
228
+ delete agentTasks[agent];
229
+ const card=document.querySelector(`.acard[data-agent="${agent}"]`);
230
+ if(card){
231
+ card.classList.remove('busy');
232
+ const l=card.querySelector('.task-label'); if(l)l.textContent='';
233
+ if(typeof refreshAgentStatus==='function') refreshAgentStatus();
234
+ }
235
+ }
236
+
237
+ async function transmit(){
238
+ const inp=document.getElementById('cmd'); let task=(inp.value||'').trim();
239
+ const hasImgs=_pendingImages.length>0, hasFiles=_pendingFiles.length>0;
240
+ if(!task && !hasImgs && !hasFiles)return;
241
+ if(task[0]==='/'){ inp.value=''; hideSlashMenu(); runSlash(task); return; }
242
+ inp.value='';
243
+ if((hasImgs||hasFiles) && !task) task=hasImgs?'请描述并分析这张图片':'请阅读并总结这个文件';
244
+ prependTask(task,activeAgent,'pending');
245
+ setAgentBusy(activeAgent, task);
246
+ if(window._emitStream){ for(let i=0;i<3;i++) setTimeout(()=>window._emitStream(activeAgent), i*180); }
247
+ const qThumbs=hasImgs?_pendingImages.map(x=>x.url):null;
248
+ const qFiles=hasFiles?_pendingFiles.map(x=>({name:x.name,size:x.size,url:x.url})):null;
249
+ showReplyPop(task, activeAgent, false, qThumbs, qFiles);
250
+ if(hasImgs || hasFiles){
251
+ const imgs=hasImgs?_pendingImages.map(x=>({media_type:x.media_type, data:x.data})):null;
252
+ const atts=hasFiles?_pendingFiles.map(x=>({name:x.name,size:x.size,mime:x.mime,url:x.url,text:x.text})):null;
253
+ clearPendingImages(); clearPendingFiles();
254
+ _refTriggers={}; // 流式/多模态无技能路由,清掉 @引用残留(仅任务模式经 applyRefs 生效)
255
+ return transmitStream(task, activeAgent, imgs, atts);
256
+ }
257
+ if(chatModeStream){ _refTriggers={}; return transmitStream(task, activeAgent); }
258
+ const outTask=applyRefs(task);
259
+ if(activeAgent!=='supervisor'){ _refTriggers={}; return transmitTaskStream(outTask, activeAgent); }
260
+ try{
261
+ const r=await fetch(`${API}/task`,{method:'POST',headers:{'Content-Type':'application/json'},
262
+ body:JSON.stringify({task:outTask,agent_id:activeAgent})});
263
+ const data=await r.json();
264
+ if(data.error==='harness_blocked'){
265
+ markTask(task,'blocked');
266
+ fillReplyPop(`⚠ 任务被 Harness 拦截`, `偏离原因:${data.reason||'语义漂移超过阈值'}(Δ=${(data.delta||0).toFixed?.(3)||data.delta||'—'})`, 'blocked');
267
+ }else{
268
+ markTask(task,'ok');
269
+ const res=data.result||{};
270
+ if(res.dispatched_to==='multi' && Array.isArray(res.multi_steps)){
271
+ let html=`<div style="margin-bottom:10px;color:var(--cyan);font-weight:600">${escapeHtml(res.summary||'多步任务')}</div>`;
272
+ res.multi_steps.forEach(s=>{
273
+ const agName=escapeHtml(AGENT_CN[s.agent]||s.agent);
274
+ const blockedTag=s.blocked?' <span style="color:var(--amber)">[被拦截]</span>':'';
275
+ html+=`<div style="margin:8px 0;padding:8px 10px;border-left:2px solid var(--cyan);background:rgba(0,217,255,.05)">
276
+ <div style="font-size:11px;color:var(--ink-dim);margin-bottom:3px">第${escapeHtml(s.step)}步 · ${agName} Agent${blockedTag}</div>
277
+ <div style="font-size:12px;color:var(--cyan)">「${escapeHtml(s.subtask)}」</div>
278
+ <div style="font-size:12px;margin-top:4px">${escapeHtml(s.output||'(无输出)')}</div>
279
+ </div>`;
280
+ });
281
+ fillReplyPop(html, '⤷ 主管已拆解为多步,跨 Agent 顺序执行', 'ok', true);
282
+ loadMemStats();
283
+ return;
284
+ }
285
+ const reply=res.sub_result||res.summary||res.reply||(typeof res==='string'?res:JSON.stringify(res));
286
+ let via='';
287
+ if(res.dispatched_to&&res.dispatched_to!=='supervisor'&&res.dispatched_to!==activeAgent){
288
+ via=`⤷ 主管已分派给 ${AGENT_CN[res.dispatched_to]||res.dispatched_to} Agent 处理`;
289
+ setAgentBusy(res.dispatched_to, task);
290
+ }else if(res.self_answered){
291
+ via='⤷ 主管直接答复';
292
+ }
293
+ fillReplyPop('<div class="cc-md">'+renderMarkdown(reply)+'</div>', via, 'ok', true);
294
+ }
295
+ loadMemStats();
296
+ }catch(e){
297
+ markTask(task,'blocked');
298
+ fillReplyPop('', '请求失败:后端未连接,或模型未配置 Key', 'err');
299
+ }
300
+ }
301
+ function showReplyPop(task, agent, force, qImages, qFiles){
302
+ if(!force && typeof uiPref!=='undefined' && !uiPref.autoChat) return;
303
+ document.getElementById('ccAgent').textContent=`${AGENT_CN[agent]||agent} Agent`;
304
+ const _b=document.querySelector('#coreChat .cc-body');
305
+ if(_b && !document.getElementById('ccQ'))
306
+ _b.innerHTML='<div class="cc-q" id="ccQ"></div><div id="ccReply"></div><div class="cc-status" id="ccStatus"></div>';
307
+ let qh='你:'+escapeHtml(task);
308
+ if(qImages && qImages.length){ // 用户随问题发的图片 → 提问区渲染缩略图(本地 dataURL,可信)
309
+ qh+='<div class="cc-qimgs">'+qImages.map(u=>`<img class="cc-qimg" src="${u}" alt="">`).join('')+'</div>';
310
+ }
311
+ if(qFiles && qFiles.length){ // 用户随问题发的附件 → 提问区渲染文件卡片(可下载)
312
+ qh+=_attachmentsHtml(qFiles);
313
+ }
314
+ document.getElementById('ccQ').innerHTML=qh;
315
+ document.getElementById('ccReply').innerHTML='<div class="cc-loading">◌ 思考中…(真实模型回复需几秒)</div>';
316
+ document.getElementById('ccStatus').textContent='';
317
+ Center.chat();
318
+ }
319
+ function fillReplyPop(reply, status, kind, isHtml){
320
+ const rp=document.getElementById('ccReply'), st=document.getElementById('ccStatus');
321
+ if(isHtml){ rp.innerHTML=reply||''; }
322
+ else if(reply){ rp.textContent=reply; } else { rp.innerHTML=''; }
323
+ st.textContent=status||'';
324
+ if(kind==='blocked'){ st.style.color='var(--amber)'; rp.style.color='var(--amber)'; }
325
+ else if(kind==='err'){ st.style.color='var(--pink)'; }
326
+ else { st.style.color='var(--ink-dim)'; rp.style.color='var(--ink)'; }
327
+ }
328
+ document.getElementById('ccClose')?.addEventListener('click',()=>document.getElementById('coreChat')?.classList.remove('show'));
329
+ document.querySelector('.cmdbar .send')?.addEventListener('click',transmit);
330
+ document.getElementById('cmd')?.addEventListener('keydown',e=>{if(e.key==='Enter')transmit();});
331
+
332
+ const _INGEST_MAX_BYTES=20*1024*1024;
333
+ let _ingestBusy=false; // 串行守卫:一次只摄入一个文件,避免并发轮询串写同一进度弹层
334
+ (function(){
335
+ const btn=document.getElementById('cmdAttach'), inp=document.getElementById('cmdFile');
336
+ if(!btn||!inp) return;
337
+ btn.addEventListener('click',()=>inp.click());
338
+ inp.addEventListener('change',async ()=>{
339
+ const f=inp.files&&inp.files[0]; inp.value=''; if(!f) return;
340
+ if(_ingestBusy){
341
+ showReplyPop(`📎 ${f.name}`, activeAgent, true);
342
+ fillReplyPop('', '请等当前文件摄入完成再上传下一个', 'err'); return;
343
+ }
344
+ _ingestBusy=true;
345
+ try{ await ingestChatFile(f); } finally{ _ingestBusy=false; }
346
+ });
347
+ })();
348
+ function _refreshPlanet(){
349
+ try{ const fr=document.getElementById('planetFrame'); const w=fr&&fr.contentWindow;
350
+ if(w && fr.src && fr.src!=='about:blank' && typeof w.loadData==='function') w.loadData(); }catch(e){}
351
+ }
352
+ async function _ingestFileOnce(file, onStatus, collection){
353
+ if(file.size>_INGEST_MAX_BYTES) return {ok:false, skipped:true, error:`过大(${(file.size/1048576).toFixed(1)}MB>20MB)`};
354
+ let job;
355
+ try{
356
+ const fd=new FormData(); fd.append('file', file);
357
+ fd.append('source_title', (file.name||'').replace(/\.[^.]+$/,''));
358
+ if(collection) fd.append('collection', collection);
359
+ const r=await fetch(`${_origin}/knowledge/ingest/file/async`,{method:'POST',body:fd});
360
+ if(!r.ok) return {ok:false, error: r.status===415?'不支持的格式': r.status===503?'知识核心未启用':`上传失败(${r.status})`};
361
+ job=await r.json();
362
+ }catch(e){ return {ok:false, error:'后端未连接'}; }
363
+ const jobId=job.job_id;
364
+ for(let i=0;i<600;i++){ // 每文件最多 ~10 分钟
365
+ await new Promise(res=>setTimeout(res,1200));
366
+ let j;
367
+ try{ j=await fetch(`${_origin}/knowledge/ingest/jobs/${encodeURIComponent(jobId)}`).then(x=>x.json()); }
368
+ catch(e){ continue; }
369
+ if(j.status==='running'||j.status==='queued'){
370
+ if(onStatus){ const pct=j.total>0?Math.round(j.processed/j.total*100):0; onStatus(`抽取中 ${j.processed}/${j.total||'?'} 段${j.total>0?' ('+pct+'%)':''}`); }
371
+ continue;
372
+ }
373
+ if(j.status==='done') return {ok:true, ku_count:j.ku_count||0};
374
+ if(j.status==='error') return {ok:false, error:(j.error||'未知错误').slice(0,120)};
375
+ }
376
+ return {ok:false, error:'摄入超时'};
377
+ }
378
+ async function ingestChatFile(file){
379
+ showReplyPop(`📎 摄入文件:${file.name}`, activeAgent, true);
380
+ fillReplyPop('', '⬆ 上传中…', 'ok');
381
+ const res=await _ingestFileOnce(file, msg=>fillReplyPop('', '⚙ '+msg, 'ok'));
382
+ if(res.ok){
383
+ fillReplyPop(`✓ 已摄入 ${file.name}`,
384
+ `新增 ${res.ku_count} 条知识(KU)已进入知识星球。${res.ku_count===0?'(未配置 LLM 时抽取降级,可能为 0)':''}`, 'ok');
385
+ _refreshPlanet();
386
+ }else{
387
+ fillReplyPop('', (res.skipped?'跳过:':'摄入失败:')+res.error, 'err');
388
+ }
389
+ }
390
+
391
+ const _INGEST_EXTS=['.pdf','.md','.markdown','.txt','.epub','.mobi','.azw3','.azw','.docx','.xlsx'];
392
+ const _FOLDER_MAX_FILES=100; // 单文件夹一次最多摄入的文件数(防选到超大目录顺序摄入几小时+烧 LLM)
393
+ function _supportedFile(name){ const n=(name||'').toLowerCase(); return _INGEST_EXTS.some(e=>n.endsWith(e)); }
394
+ function _folderHtml(folder, cur, total, current, log, totalKu){
395
+ let h=`<div style="color:var(--cyan);font-weight:600;margin-bottom:6px">📁 ${escapeHtml(folder)} · ${cur}/${total}${totalKu?` · 累计 ${totalKu} KU`:''}</div>`;
396
+ if(current) h+=`<div style="font-size:12px;color:var(--ink-dim);margin-bottom:6px">当前:${escapeHtml(current)}</div>`;
397
+ log.slice(-8).forEach(e=>{ h+=`<div style="font-size:11.5px;margin:2px 0">${e.ok?'✓':'✗'} ${escapeHtml(e.name)} ${e.ok?`<span style="color:var(--ink-faint)">+${e.ku} KU</span>`:`<span style="color:var(--amber)">${escapeHtml(e.err||'')}</span>`}</div>`; });
398
+ return h;
399
+ }
400
+ async function ingestFolder(fileList){
401
+ const all=[...fileList];
402
+ let files=all.filter(f=>_supportedFile(f.name));
403
+ const folderName=((all[0]&&all[0].webkitRelativePath)||'').split('/')[0]||'文件夹';
404
+ showReplyPop(`📁 摄入文件夹:${folderName}`, activeAgent, true);
405
+ if(!files.length){
406
+ fillReplyPop('', `「${folderName}」里没有可摄入的文件(支持 pdf/txt/md/docx/xlsx/epub 等;共扫描 ${all.length} 个)`, 'err'); return;
407
+ }
408
+ let capNote='';
409
+ if(files.length>_FOLDER_MAX_FILES){ // 超上限 → 只摄前 N 个,提示分批(防失控)
410
+ capNote=` · 超上限,仅摄前 ${_FOLDER_MAX_FILES}(共 ${files.length},请分批)`;
411
+ files=files.slice(0, _FOLDER_MAX_FILES);
412
+ }
413
+ const N=files.length; let doneCount=0, failCount=0, totalKu=0, done=0; const log=[];
414
+ const CONC=Math.min(3, N); let idx=0;
415
+ async function _worker(){
416
+ while(idx<N){
417
+ const f=files[idx++];
418
+ const res=await _ingestFileOnce(f, null, folderName); // folderName=合集,文件按文件名入库
419
+ done++;
420
+ if(res.ok){ doneCount++; totalKu+=res.ku_count; log.push({name:f.name, ok:true, ku:res.ku_count}); }
421
+ else{ failCount++; log.push({name:f.name, ok:false, err:res.error}); }
422
+ fillReplyPop(_folderHtml(folderName, done, N, '', log, totalKu), '📁 并发摄入中…', 'ok', true);
423
+ }
424
+ }
425
+ await Promise.all(Array.from({length:CONC}, _worker));
426
+ fillReplyPop(_folderHtml(folderName, N, N, '', log, totalKu),
427
+ `✓ 文件夹完成:${doneCount}/${N} 个文件,共 ${totalKu} 条知识${failCount?`,${failCount} 个失败/跳过`:''}${capNote}`, 'ok', true);
428
+ _refreshPlanet();
429
+ }
430
+ (function(){
431
+ const btn=document.getElementById('cmdFolder'), inp=document.getElementById('cmdFolderInput');
432
+ if(!btn||!inp) return;
433
+ btn.addEventListener('click',()=>inp.click());
434
+ inp.addEventListener('change',async ()=>{
435
+ const fs=[...inp.files]; inp.value=''; if(!fs.length) return;
436
+ if(_ingestBusy){ showReplyPop('📁 文件夹', activeAgent, true); fillReplyPop('', '请等当前摄入完成再选文件夹', 'err'); return; }
437
+ _ingestBusy=true;
438
+ try{ await ingestFolder(fs); } finally{ _ingestBusy=false; }
439
+ });
440
+ })();
441
+
442
+ let chatModeStream=true;
443
+ (function(){
444
+ const el=document.getElementById('chatMode');
445
+ if(!el)return;
446
+ const sync=()=>{ el.textContent=chatModeStream?'⚡ 流式':'🔧 任务'; el.classList.toggle('task',!chatModeStream);
447
+ const cmd=document.getElementById('cmd'); if(cmd)cmd.placeholder=chatModeStream?'和我聊聊,逐字回复…':'交给我一个任务,主管自动分诊调度'; };
448
+ el.addEventListener('click',()=>{ chatModeStream=!chatModeStream; sync(); });
449
+ sync();
450
+ })();
451
+
452
+ function toolMenuOpen(){ const m=document.getElementById('toolMenu'); return !!(m&&m.classList.contains('show')); }
453
+ function hideToolMenu(){ const m=document.getElementById('toolMenu'); if(m)m.classList.remove('show'); }
454
+ function _toolRows(){
455
+ const stream=chatModeStream;
456
+ const hit=id=>{ const b=document.getElementById(id); if(b)b.click(); };
457
+ return [
458
+ {ic: stream?'⚡':'🔧', b: stream?'流式对话':'编排任务', s:'点击切换',
459
+ act:()=>{ hit('chatMode'); renderToolMenu(); }}, // 切完不关,原地看到变化
460
+ {ic:'📎', b:'上传文件', s:'摄入知识库', act:()=>{ hideToolMenu(); hit('cmdAttach'); }},
461
+ {ic:'📁', b:'导入文件夹', s:'逐个摄入', act:()=>{ hideToolMenu(); hit('cmdFolder'); }},
462
+ {ic:'🖼', b:'附加图片', s:'发给视觉模型', act:()=>{ hideToolMenu(); hit('cmdImg'); }},
463
+ {ic:'📄', b:'附件文件', s:'随问题发给 AI', act:()=>{ hideToolMenu(); hit('cmdAttachFile'); }},
464
+ {ic:'🔌', b:'连接器/插件', s:'MCP · 技能', act:()=>{ hideToolMenu(); hit('cmdConn'); }},
465
+ ];
466
+ }
467
+ function renderToolMenu(){
468
+ const m=document.getElementById('toolMenu'); if(!m)return;
469
+ const rows=_toolRows();
470
+ m.innerHTML=rows.map((r,i)=>`<div class="tool-item" data-i="${i}"><span class="ti-ic">${r.ic}</span>
471
+ <span class="ti-tx"><b>${escapeHtml(r.b)}</b><span>${escapeHtml(r.s)}</span></span></div>`).join('');
472
+ m.querySelectorAll('.tool-item').forEach(el=>el.addEventListener('click',e=>{ e.stopPropagation(); rows[+el.dataset.i].act(); }));
473
+ }
474
+ (function(){
475
+ const btn=document.getElementById('cmdMore'); if(!btn)return;
476
+ btn.addEventListener('click',e=>{
477
+ e.stopPropagation();
478
+ if(toolMenuOpen()){ hideToolMenu(); return; }
479
+ hideSlashMenu(); hideAtMenu(); // 同一时刻只开一个浮层
480
+ const p=document.getElementById('connPanel'); if(p)p.classList.remove('show');
481
+ renderToolMenu();
482
+ document.getElementById('toolMenu').classList.add('show');
483
+ });
484
+ document.addEventListener('click',e=>{ // 点外部关闭
485
+ const m=document.getElementById('toolMenu'), tools=document.getElementById('cmdTools');
486
+ if(!toolMenuOpen() || !m) return;
487
+ if(m.contains(e.target) || e.target.id==='cmdMore') return;
488
+ if(tools && tools.contains(e.target)) return;
489
+ hideToolMenu();
490
+ });
491
+ })();
492
+
493
+ const SLASH_COMMANDS=[
494
+ {cmd:'搜索', icon:'🔍', arg:'关键词', desc:'检索知识库', aliases:['search'], run:(q)=>slashSearch(q)},
495
+ {cmd:'摄入', icon:'📎', desc:'上传文件进知识库', aliases:['上传','upload'], run:()=>{ const f=document.getElementById('cmdFile'); if(f)f.click(); }},
496
+ {cmd:'财务', icon:'💰', desc:'打开财务驾驶舱', aliases:['finance'], run:()=>openAgentPanel('finance')},
497
+ {cmd:'地图', icon:'🗺', desc:'打开行程地图', aliases:['行程','admin','map'], run:()=>openAgentPanel('admin')},
498
+ {cmd:'工具', icon:'⚒', desc:'打开工具中心', aliases:['tools'], run:()=>{ try{showModuleInCore('tools');}catch(e){} }},
499
+ {cmd:'星球', icon:'🌐', desc:'打开知识星球', aliases:['planet'], run:()=>{ const b=document.getElementById('planetLaunch'); if(b)b.click(); }},
500
+ {cmd:'设置', icon:'⚙', desc:'打开设置菜单', aliases:['settings'], run:()=>{ try{openSetMenu();}catch(e){} }},
501
+ {cmd:'流式', icon:'⚡', desc:'切到流式对话(逐字直答)', aliases:['stream'], run:()=>{ const el=document.getElementById('chatMode'); if(el&&!chatModeStream)el.click(); }},
502
+ {cmd:'任务', icon:'🔧', desc:'切到编排任务模式', aliases:['task'], run:()=>{ const el=document.getElementById('chatMode'); if(el&&chatModeStream)el.click(); }},
503
+ {cmd:'帮助', icon:'❓', desc:'列出所有斜杠命令', aliases:['help','?'], run:()=>slashHelp()},
504
+ {cmd:'清库', icon:'🗑', danger:true, arg:'确认', desc:'清空知识库(需 /清库 确认)', aliases:['reset'], run:(a)=>slashReset(a)},
505
+ ];
506
+ function openAgentPanel(ag){ try{ loadKernel(ag); showInCore(ag); }catch(e){} }
507
+ function slashFind(name){
508
+ name=(name||'').toLowerCase();
509
+ return SLASH_COMMANDS.find(c=>c.cmd.toLowerCase()===name || (c.aliases||[]).some(a=>a.toLowerCase()===name));
510
+ }
511
+ function runSlash(text){
512
+ const body=text.slice(1).trim();
513
+ const sp=body.indexOf(' ');
514
+ const name=sp<0?body:body.slice(0,sp);
515
+ const arg=sp<0?'':body.slice(sp+1).trim();
516
+ const c=slashFind(name);
517
+ if(!c){ showReplyPop('/'+body, activeAgent, true); fillReplyPop('', `未知命令 /${name}。输入 /帮助 查看全部`, 'err'); return; }
518
+ try{ c.run(arg); }catch(e){ showReplyPop('/'+body, activeAgent, true); fillReplyPop('', '命令执行出错:'+(e&&e.message||e), 'err'); }
519
+ }
520
+ async function slashSearch(q){
521
+ q=(q||'').trim();
522
+ if(!q){ showReplyPop('/搜索', activeAgent, true); fillReplyPop('', '用法:/搜索 <关键词>', 'err'); return; }
523
+ showReplyPop(`🔍 搜索:${q}`, activeAgent, true); fillReplyPop('', '检索中…', 'ok');
524
+ try{
525
+ const r=await fetch(`${API}/knowledge/search`,{method:'POST',headers:{'Content-Type':'application/json'},
526
+ body:JSON.stringify({query:q,mode:'hybrid',top_k:8})});
527
+ const d=await r.json(); const hits=d.hits||[];
528
+ if(d.available===false){ fillReplyPop('', '知识核心未启用', 'err'); return; }
529
+ if(!hits.length){ fillReplyPop('', `没有匹配「${q}」的知识`, 'ok'); return; }
530
+ let html=`<div style="color:var(--cyan);font-weight:600;margin-bottom:8px">🔍 ${escapeHtml(q)} · ${hits.length} 条</div>`;
531
+ hits.forEach(h=>{ const sc=(typeof h.score==='number')?h.score.toFixed(2):h.score;
532
+ html+=`<div style="margin:7px 0;padding:8px 10px;border-left:2px solid var(--cyan);background:rgba(0,217,255,.05)">
533
+ <div style="font-size:12px;color:var(--cyan);font-weight:600">${escapeHtml(h.name||'(未命名)')} <span style="color:var(--ink-faint);font-weight:400">${escapeHtml(h.ku_type||'')} · ${sc}</span></div>
534
+ <div style="font-size:11.5px;margin-top:3px;color:var(--ink-dim)">${escapeHtml((h.summary||'').slice(0,140))}</div></div>`; });
535
+ fillReplyPop(html, '混合检索 · 点「🌐 知识星球」看全图', 'ok', true);
536
+ }catch(e){ fillReplyPop('', '搜索失败:后端未连接', 'err'); }
537
+ }
538
+ function slashHelp(){
539
+ let html='<div style="color:var(--cyan);font-weight:600;margin-bottom:8px">斜杠命令</div>';
540
+ SLASH_COMMANDS.forEach(c=>{ html+=`<div style="display:flex;gap:8px;margin:4px 0;font-size:12px">
541
+ <span style="width:18px">${c.icon}</span><b style="color:${c.danger?'#ff8a87':'#eaf6ff'};min-width:70px">/${escapeHtml(c.cmd)}${c.arg?' &lt;'+escapeHtml(c.arg)+'&gt;':''}</b>
542
+ <span style="color:var(--ink-dim)">${escapeHtml(c.desc)}</span></div>`; });
543
+ showReplyPop('/帮助', activeAgent, true);
544
+ fillReplyPop(html, '直接输入命令即可,如 /搜索 记忆架构', 'ok', true);
545
+ }
546
+ async function slashReset(arg){
547
+ if((arg||'').trim()!=='确认'){
548
+ showReplyPop('/清库', activeAgent, true);
549
+ fillReplyPop('', '⚠ 清空整个知识库(所有 KU/关系),不可逆。确认请输入:/清库 确认', 'err'); return;
550
+ }
551
+ showReplyPop('/清库 确认', activeAgent, true); fillReplyPop('', '清库中…', 'ok');
552
+ try{
553
+ const r=await fetch(`${_origin}/knowledge/reset`,{method:'POST'});
554
+ const d=await r.json();
555
+ fillReplyPop('已清空知识库', `删除 ${d.cleared_kus??'?'} 条 KU`, 'ok');
556
+ try{ const fr=document.getElementById('planetFrame'); const w=fr&&fr.contentWindow;
557
+ if(w&&fr.src&&fr.src!=='about:blank'&&typeof w.loadData==='function')w.loadData(); }catch(e){}
558
+ }catch(e){ fillReplyPop('', '清库失败:后端未连接', 'err'); }
559
+ }
560
+ let _slashSel=0, _slashList=[];
561
+ function slashFilter(text){
562
+ const q=text.slice(1).toLowerCase();
563
+ return SLASH_COMMANDS.filter(c=> !q || c.cmd.toLowerCase().includes(q)
564
+ || (c.aliases||[]).some(a=>a.toLowerCase().includes(q)) || c.desc.toLowerCase().includes(q));
565
+ }
566
+ function slashMenuOpen(){ const m=document.getElementById('slashMenu'); return !!(m&&m.classList.contains('show')); }
567
+ function hideSlashMenu(){ const m=document.getElementById('slashMenu'); if(m){m.classList.remove('show'); m.innerHTML='';} _slashList=[]; _slashSel=0; }
568
+ function updateSlashSel(){ document.querySelectorAll('#slashMenu .slash-item').forEach(el=>el.classList.toggle('sel', +el.dataset.i===_slashSel)); }
569
+ function renderSlashMenu(){
570
+ const menu=document.getElementById('slashMenu'); if(!menu)return;
571
+ if(!_slashList.length){ hideSlashMenu(); return; }
572
+ let h='<div class="slash-hint">↑↓ 选择 · Enter 执行 · Esc 关闭</div>';
573
+ _slashList.forEach((c,i)=>{ h+=`<div class="slash-item ${c.danger?'danger':''} ${i===_slashSel?'sel':''}" data-i="${i}">
574
+ <span class="si-ic">${c.icon}</span><span class="si-cmd">/${escapeHtml(c.cmd)}${c.arg?' &lt;'+escapeHtml(c.arg)+'&gt;':''}</span>
575
+ <span class="si-desc">${escapeHtml(c.desc)}</span></div>`; });
576
+ menu.innerHTML=h; menu.classList.add('show');
577
+ menu.querySelectorAll('.slash-item').forEach(el=>{
578
+ el.addEventListener('mouseenter',()=>{ _slashSel=+el.dataset.i; updateSlashSel(); });
579
+ el.addEventListener('mousedown',e=>{ e.preventDefault(); pickSlash(+el.dataset.i); });
580
+ });
581
+ }
582
+ function pickSlash(i){
583
+ const c=_slashList[i]; if(!c)return;
584
+ const inp=document.getElementById('cmd');
585
+ if(c.arg){ inp.value='/'+c.cmd+' '; hideSlashMenu(); inp.focus(); } // 需参数:补全命令,等用户输入
586
+ else{ inp.value=''; hideSlashMenu(); runSlash('/'+c.cmd); }
587
+ }
588
+ function onSlashInput(){
589
+ const inp=document.getElementById('cmd'); const v=inp.value;
590
+ if(v[0]!=='/' || v.indexOf(' ')>=0){ hideSlashMenu(); return; } // 含空格=进入参数阶段 → 关菜单
591
+ _slashList=slashFilter(v); _slashSel=0; renderSlashMenu();
592
+ }
593
+ (function(){
594
+ const inp=document.getElementById('cmd'); if(!inp)return;
595
+ inp.addEventListener('input', onSlashInput);
596
+ inp.addEventListener('blur', ()=>setTimeout(hideSlashMenu,150)); // 失焦关(留时间给 mousedown)
597
+ document.addEventListener('keydown', function(e){
598
+ if(e.target!==inp || !slashMenuOpen()) return;
599
+ if(e.key==='ArrowDown'){ e.preventDefault(); e.stopPropagation(); _slashSel=(_slashSel+1)%_slashList.length; updateSlashSel(); }
600
+ else if(e.key==='ArrowUp'){ e.preventDefault(); e.stopPropagation(); _slashSel=(_slashSel-1+_slashList.length)%_slashList.length; updateSlashSel(); }
601
+ else if(e.key==='Enter'||e.key==='Tab'){ e.preventDefault(); e.stopPropagation(); pickSlash(_slashSel); }
602
+ else if(e.key==='Escape'){ e.preventDefault(); e.stopPropagation(); hideSlashMenu(); }
603
+ }, true);
604
+ })();
605
+
606
+ let _atList=[], _atSel=0, _atCache=null, _atCtx=null, _refTriggers={};
607
+ async function openConnPanel(){
608
+ const p=document.getElementById('connPanel'); if(!p)return;
609
+ if(p.classList.contains('show')){ p.classList.remove('show'); return; }
610
+ hideSlashMenu(); hideAtMenu();
611
+ p.classList.add('show'); p.innerHTML='<div class="conn-empty">加载中…</div>';
612
+ try{
613
+ const [a,b,c]=await Promise.all([
614
+ fetch(`${API}/ahs/skills`).then(x=>x.json()).catch(()=>({})),
615
+ fetch(`${API}/ahs/mcp/servers`).then(x=>x.json()).catch(()=>({})),
616
+ fetch(`${API}/ahs/md-skills`).then(x=>x.json()).catch(()=>({})),
617
+ ]);
618
+ renderConnPanel(p, a.skills||[], b.servers||[], c.skills||[]);
619
+ }catch(e){ p.innerHTML='<div class="conn-empty">后端未连接</div>'; }
620
+ }
621
+ function connBadge(trust){ const t=trust||'mid'; const lb={high:'高',mid:'中',low:'低'}[t]||t; return `<span class="conn-badge ${t}">${lb}信任</span>`; }
622
+ function renderConnPanel(p, skills, servers, md){
623
+ const kindIc={local:'🧩',mcp:'🔌',web_api:'🌐'};
624
+ let h=`<div class="conn-sec"><div class="conn-sec-h"><span>MCP 连接器</span><span>${servers.length}</span></div>`;
625
+ h+= servers.length?'':'<div class="conn-empty" style="padding:8px">未接入</div>';
626
+ servers.forEach(s=>{ h+=`<div class="conn-row"><span class="cr-ic">🔌</span>
627
+ <div><div class="cr-name">${escapeHtml(s.name||s.id)}</div><div class="cr-meta">${s.tool_count||0} 工具 · ${escapeHtml(s.transport||'')}</div></div></div>`; });
628
+ h+='</div>';
629
+ h+=`<div class="conn-sec"><div class="conn-sec-h"><span>技能</span><span>${skills.length}</span></div>`;
630
+ h+= skills.length?'':'<div class="conn-empty" style="padding:8px">无</div>';
631
+ skills.forEach(s=>{ const blk=s.blocked; h+=`<div class="conn-row"><span class="cr-ic">${kindIc[s.kind]||'🧩'}</span>
632
+ <div><div class="cr-name">${escapeHtml(s.name||s.id)}${blk?' <span class="conn-badge low">已封杀</span>':''}</div>
633
+ <div class="cr-meta">${escapeHtml((s.description||'').slice(0,42))}</div></div>
634
+ <div class="cr-right">${connBadge(s.trust)}<label class="conn-sw"><input type="checkbox" ${s.enabled&&!blk?'checked':''} ${blk?'disabled':''} data-skill="${escapeHtml(s.id)}"><i></i></label></div></div>`; });
635
+ h+='</div>';
636
+ h+=`<div class="conn-sec"><div class="conn-sec-h"><span>SKILL.md 指令技能</span><span>${md.length}</span></div>`;
637
+ h+= md.length?'':'<div class="conn-empty" style="padding:8px">无</div>';
638
+ md.forEach(s=>{ h+=`<div class="conn-row"><span class="cr-ic">📄</span>
639
+ <div><div class="cr-name">${escapeHtml(s.name||s.id)}</div><div class="cr-meta">${escapeHtml((s.description||'').slice(0,42))}</div></div>
640
+ <div class="cr-right"><label class="conn-sw"><input type="checkbox" ${s.enabled?'checked':''} data-md="${escapeHtml(s.id)}"><i></i></label></div></div>`; });
641
+ h+='</div><div class="conn-foot"><button id="connManage">⚒ 在工具中心管理全部</button></div>';
642
+ p.innerHTML=h;
643
+ p.querySelectorAll('input[data-skill]').forEach(el=>el.addEventListener('change',async()=>{
644
+ _atCache=null; // 启停变了 → 让 @引用缓存失效
645
+ try{ const r=await fetch(`${API}/ahs/skills/${encodeURIComponent(el.dataset.skill)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:el.checked})}); if(!r.ok)throw 0; }
646
+ catch(e){ el.checked=!el.checked; }
647
+ }));
648
+ p.querySelectorAll('input[data-md]').forEach(el=>el.addEventListener('change',async()=>{
649
+ _atCache=null;
650
+ try{ const r=await fetch(`${API}/ahs/md-skills/${encodeURIComponent(el.dataset.md)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:el.checked})}); if(!r.ok)throw 0; }
651
+ catch(e){ el.checked=!el.checked; }
652
+ }));
653
+ const mg=document.getElementById('connManage'); if(mg)mg.addEventListener('click',()=>{ p.classList.remove('show'); try{showModuleInCore('tools');}catch(e){} });
654
+ }
655
+ (function(){
656
+ const btn=document.getElementById('cmdConn'); if(btn)btn.addEventListener('click',e=>{ e.stopPropagation(); openConnPanel(); });
657
+ document.addEventListener('click',e=>{ const p=document.getElementById('connPanel');
658
+ if(p&&p.classList.contains('show')&&!p.contains(e.target)&&e.target.id!=='cmdConn') p.classList.remove('show'); });
659
+ })();
660
+
661
+ async function ensureAtCache(){
662
+ if(_atCache) return _atCache;
663
+ try{
664
+ const [a,c]=await Promise.all([
665
+ fetch(`${API}/ahs/skills`).then(x=>x.json()).catch(()=>({})),
666
+ fetch(`${API}/ahs/md-skills`).then(x=>x.json()).catch(()=>({})),
667
+ ]);
668
+ const sk=(a.skills||[]).filter(s=>s.enabled&&!s.blocked).map(s=>({id:s.id,name:s.name||s.id,trigger:(s.triggers&&s.triggers[0])||s.name||s.id,kind:s.kind}));
669
+ const md=(c.skills||[]).filter(s=>s.enabled).map(s=>({id:s.id,name:s.name||s.id,trigger:(s.triggers&&s.triggers[0])||s.name||s.id,kind:'md'}));
670
+ _atCache=sk.concat(md);
671
+ }catch(e){ _atCache=[]; }
672
+ return _atCache;
673
+ }
674
+ function atMenuOpen(){ const m=document.getElementById('atMenu'); return !!(m&&m.classList.contains('show')); }
675
+ function hideAtMenu(){ const m=document.getElementById('atMenu'); if(m){m.classList.remove('show'); m.innerHTML='';} _atList=[]; _atSel=0; _atCtx=null; }
676
+ function _atSelUpdate(){ document.querySelectorAll('#atMenu .slash-item').forEach(x=>x.classList.toggle('sel',+x.dataset.i===_atSel)); }
677
+ function atQueryAtCursor(){
678
+ const inp=document.getElementById('cmd'); const v=inp.value; const pos=inp.selectionStart==null?v.length:inp.selectionStart;
679
+ const upto=v.slice(0,pos); const at=upto.lastIndexOf('@');
680
+ if(at<0) return null;
681
+ const frag=upto.slice(at+1);
682
+ if(/\s/.test(frag)) return null; // @ 后有空格 = 已结束引用
683
+ return {at, pos, q:frag};
684
+ }
685
+ function renderAtMenu(){
686
+ const m=document.getElementById('atMenu'); if(!m)return;
687
+ if(!_atList.length){ hideAtMenu(); return; }
688
+ const kindIc={local:'🧩',mcp:'🔌',web_api:'🌐',md:'📄'};
689
+ let h='<div class="slash-hint">@ 引用技能/连接器 · Enter 插入 · Esc 关闭</div>';
690
+ _atList.forEach((c,i)=>{ h+=`<div class="slash-item ${i===_atSel?'sel':''}" data-i="${i}">
691
+ <span class="si-ic">${kindIc[c.kind]||'🧩'}</span><span class="si-cmd">@${escapeHtml(c.name)}</span>
692
+ <span class="si-desc">触发:${escapeHtml(c.trigger)}</span></div>`; });
693
+ m.innerHTML=h; m.classList.add('show');
694
+ m.querySelectorAll('.slash-item').forEach(el=>{
695
+ el.addEventListener('mouseenter',()=>{ _atSel=+el.dataset.i; _atSelUpdate(); });
696
+ el.addEventListener('mousedown',e=>{ e.preventDefault(); pickAt(+el.dataset.i); });
697
+ });
698
+ }
699
+ function pickAt(i){
700
+ const c=_atList[i]; if(!c||!_atCtx)return;
701
+ const inp=document.getElementById('cmd'); const v=inp.value;
702
+ const before=v.slice(0,_atCtx.at), after=v.slice(_atCtx.pos);
703
+ const ins='@'+c.name+' ';
704
+ inp.value=before+ins+after; _refTriggers[c.name]=c.trigger;
705
+ hideAtMenu(); inp.focus();
706
+ const np=(before+ins).length; try{ inp.setSelectionRange(np,np); }catch(e){}
707
+ }
708
+ async function onAtInput(){
709
+ const inp=document.getElementById('cmd');
710
+ if(inp.value[0]==='/'){ hideAtMenu(); return; } // 斜杠命令优先
711
+ const ctx=atQueryAtCursor();
712
+ if(!ctx){ hideAtMenu(); return; }
713
+ _atCtx=ctx;
714
+ const all=await ensureAtCache();
715
+ const q=ctx.q.toLowerCase();
716
+ _atList=all.filter(c=> !q || c.name.toLowerCase().includes(q) || (c.trigger||'').toLowerCase().includes(q)).slice(0,8);
717
+ _atSel=0; renderAtMenu();
718
+ }
719
+ function applyRefs(task){
720
+ let out=task;
721
+ for(const nm in _refTriggers){
722
+ if(task.includes('@'+nm)){ const tr=_refTriggers[nm]; if(tr && !out.includes(tr)) out+=' '+tr; }
723
+ }
724
+ _refTriggers={};
725
+ return out;
726
+ }
727
+ (function(){
728
+ const inp=document.getElementById('cmd'); if(!inp)return;
729
+ inp.addEventListener('input', onAtInput);
730
+ inp.addEventListener('blur', ()=>setTimeout(hideAtMenu,150));
731
+ document.addEventListener('keydown', function(e){
732
+ if(e.target!==inp || !atMenuOpen()) return;
733
+ if(e.key==='ArrowDown'){ e.preventDefault(); e.stopPropagation(); _atSel=(_atSel+1)%_atList.length; _atSelUpdate(); }
734
+ else if(e.key==='ArrowUp'){ e.preventDefault(); e.stopPropagation(); _atSel=(_atSel-1+_atList.length)%_atList.length; _atSelUpdate(); }
735
+ else if(e.key==='Enter'||e.key==='Tab'){ e.preventDefault(); e.stopPropagation(); pickAt(_atSel); }
736
+ else if(e.key==='Escape'){ e.preventDefault(); e.stopPropagation(); hideAtMenu(); }
737
+ }, true);
738
+ })();
739
+
740
+ let _pendingImages=[]; // [{media_type,data(base64),url(预览),name}]
741
+ const _IMG_MAX=6, _IMG_MAXDIM=1280, _IMG_Q=0.85;
742
+ function _downscaleImage(file){
743
+ return new Promise((resolve,reject)=>{
744
+ const img=new Image(); const url=URL.createObjectURL(file);
745
+ img.onload=()=>{
746
+ URL.revokeObjectURL(url);
747
+ let w=img.naturalWidth||img.width, h=img.naturalHeight||img.height;
748
+ const scale=Math.min(1, _IMG_MAXDIM/Math.max(w,h||1));
749
+ w=Math.max(1,Math.round(w*scale)); h=Math.max(1,Math.round(h*scale));
750
+ const cv=document.createElement('canvas'); cv.width=w; cv.height=h;
751
+ cv.getContext('2d').drawImage(img,0,0,w,h);
752
+ const isPng=/png/i.test(file.type); // png 可能带透明 → 保 png,其余转 jpeg 省体积
753
+ const mt=isPng?'image/png':'image/jpeg';
754
+ let durl; try{ durl=cv.toDataURL(mt,_IMG_Q); }catch(e){ reject(e); return; }
755
+ resolve({media_type:mt, data:durl.split(',')[1]||'', url:durl, name:file.name||'image'});
756
+ };
757
+ img.onerror=()=>{ URL.revokeObjectURL(url); reject(new Error('image load failed')); };
758
+ img.src=url;
759
+ });
760
+ }
761
+ async function attachImages(files){
762
+ for(const f of [...files]){
763
+ if(!/^image\//.test(f.type||'')) continue;
764
+ if(_pendingImages.length>=_IMG_MAX) break;
765
+ try{ _pendingImages.push(await _downscaleImage(f)); }catch(e){}
766
+ }
767
+ renderImgStrip(); visionAdvisory();
768
+ }
769
+ function renderImgStrip(){
770
+ const s=document.getElementById('imgStrip'); if(!s)return;
771
+ if(!_pendingImages.length){ s.classList.remove('show'); s.innerHTML=''; return; }
772
+ let h='';
773
+ _pendingImages.forEach((im,i)=>{ h+=`<div class="img-thumb" title="${escapeHtml(im.name)}"><img src="${im.url}" alt=""><span class="it-x" data-i="${i}">✕</span></div>`; });
774
+ h+='<div class="img-note" id="imgNote"></div>';
775
+ s.innerHTML=h; s.classList.add('show');
776
+ s.querySelectorAll('.it-x').forEach(el=>el.addEventListener('click',()=>{ _pendingImages.splice(+el.dataset.i,1); renderImgStrip(); visionAdvisory(); }));
777
+ }
778
+ function clearPendingImages(){ _pendingImages=[]; renderImgStrip(); }
779
+ async function visionAdvisory(){
780
+ const note=document.getElementById('imgNote'); if(!note)return;
781
+ try{
782
+ const m=await fetch(`${API}/model`).then(x=>x.json());
783
+ const model=(m.model||'').toLowerCase();
784
+ const vision=/(claude|sonnet|opus|haiku|gpt-4o|gpt-4\.1|gpt-5|o1|o3|[-_]vl|vision|gemini|pixtral|llava)/i.test(model);
785
+ if(vision){ note.textContent=`📷 ${_pendingImages.length} 张图将随问题发给视觉模型`; note.style.color='var(--ink-dim)'; }
786
+ else{ note.textContent=`⚠ 当前模型「${m.model||'?'}」可能不支持图片,请到「连接设置」切换视觉模型(claude / gpt-4o / qwen-vl 等)`; note.style.color='var(--amber)'; }
787
+ }catch(e){}
788
+ }
789
+ (function(){
790
+ const btn=document.getElementById('cmdImg'), inp=document.getElementById('cmdImgFile');
791
+ if(btn&&inp){
792
+ btn.addEventListener('click',()=>inp.click());
793
+ inp.addEventListener('change',()=>{ const fs=[...inp.files]; inp.value=''; if(fs.length) attachImages(fs); }); // 拷数组再清空(同 folder:inp.value='' 会清空活引用)
794
+ }
795
+ })();
796
+
797
+ let _pendingFiles=[]; // [{id,name,size,mime,url,text}]
798
+ const _FILE_MAX=6, _FILE_MAXB=20*1024*1024;
799
+ function _fileSize(sz){
800
+ if(!sz)return '';
801
+ return sz>=1048576?(sz/1048576).toFixed(1)+'MB':(sz/1024).toFixed(1)+'KB';
802
+ }
803
+ function _attachmentsHtml(atts){
804
+ if(!atts || !atts.length) return '';
805
+ return '<div class="cc-files">'+atts.map(a=>{
806
+ const nm=escapeHtml(a.name||'文件'), sz=_fileSize(a.size);
807
+ const url=a.url?escapeHtml(a.url):'';
808
+ if(url) return `<a class="cc-file" href="${url}" target="_blank" rel="noopener noreferrer">📄 <span>${nm}</span><span class="cc-file-size">${sz}</span></a>`;
809
+ return `<span class="cc-file">📄 <span>${nm}</span><span class="cc-file-size">${sz}</span></span>`;
810
+ }).join('')+'</div>';
811
+ }
812
+ async function attachFiles(files){
813
+ for(const f of [...files]){
814
+ if(_pendingFiles.length>=_FILE_MAX) break;
815
+ if(f.size>_FILE_MAXB){ console.warn(`[attach] 跳过超限文件: ${f.name} (${f.size}B > 20MB)`); continue; }
816
+ try{
817
+ const fd=new FormData(); fd.append('file', f);
818
+ const r=await fetch(`${API}/upload`,{method:'POST',body:fd});
819
+ if(!r.ok) throw new Error('http '+r.status);
820
+ const a=await r.json();
821
+ _pendingFiles.push({id:a.id,name:a.name||f.name,size:a.size,mime:a.mime,url:a.url,text:a.text||''});
822
+ }catch(e){ console.warn(`[attach] 上传失败: ${f.name}`, e); }
823
+ }
824
+ renderFileStrip();
825
+ }
826
+ function renderFileStrip(){
827
+ const s=document.getElementById('fileStrip'); if(!s)return;
828
+ if(!_pendingFiles.length){ s.classList.remove('show'); s.innerHTML=''; return; }
829
+ s.innerHTML=_pendingFiles.map((f,i)=>
830
+ `<div class="file-card" title="${escapeHtml(f.name)}"><span>📄</span><span class="fc-name">${escapeHtml(f.name)}</span>`
831
+ +`<span class="fc-size" style="color:var(--dim);font-size:11px">${_fileSize(f.size)}</span>`
832
+ +`<span class="fc-x" data-i="${i}">✕</span></div>`).join('');
833
+ s.classList.add('show');
834
+ s.querySelectorAll('.fc-x').forEach(el=>el.addEventListener('click',()=>{ _pendingFiles.splice(+el.dataset.i,1); renderFileStrip(); }));
835
+ }
836
+ function clearPendingFiles(){ _pendingFiles=[]; renderFileStrip(); }
837
+ (function(){
838
+ const btn=document.getElementById('cmdAttachFile'), inp=document.getElementById('cmdFileUpload');
839
+ if(btn&&inp){
840
+ btn.addEventListener('click',()=>inp.click());
841
+ inp.addEventListener('change',()=>{ const fs=[...inp.files]; inp.value=''; if(fs.length) attachFiles(fs); });
842
+ }
843
+ })();
844
+
845
+ let _chatHistory=[]; // 多轮对话历史 [{role,content}];随请求带给后端 + 界面渲染成对话线程
846
+ function clearChatHistory(){ _chatHistory=[]; } // 新话题/清屏可调
847
+
848
+ let _sessionId = localStorage.getItem('foxclawSessionId') || '';
849
+ async function ensureSession(){
850
+ if(_sessionId){
851
+ const ok = await fetch(`${API}/sessions/${_sessionId}`).then(r=>r.ok).catch(()=>false);
852
+ if(ok) return _sessionId;
853
+ }
854
+ return newSession(false);
855
+ }
856
+ async function newSession(focus=true){
857
+ try{
858
+ const s=await fetch(`${API}/sessions`,{method:'POST',
859
+ headers:{'Content-Type':'application/json'},body:JSON.stringify({})}).then(r=>r.json());
860
+ _sessionId=s.id; localStorage.setItem('foxclawSessionId',_sessionId);
861
+ _chatHistory=[];
862
+ if(focus){ document.getElementById('coreChat')?.classList.remove('show'); }
863
+ await loadSessions();
864
+ return _sessionId;
865
+ }catch(e){ return ''; }
866
+ }
867
+ async function openSession(id){
868
+ try{
869
+ const d=await fetch(`${API}/sessions/${id}`).then(r=>r.json());
870
+ _sessionId=d.id; localStorage.setItem('foxclawSessionId',_sessionId);
871
+ _chatHistory=(d.messages||[]).map(m=>({role:m.role,content:m.content,ts:(m.ts||0)*1000,attachments:m.attachments||undefined}));
872
+ renderSessionThread(d.agent_id||'supervisor');
873
+ await loadSessions();
874
+ }catch(e){}
875
+ }
876
+ function renderSessionThread(agent){
877
+ const body=document.querySelector('#coreChat .cc-body'); if(!body)return;
878
+ const a=document.getElementById('ccAgent'); if(a)a.textContent=`${AGENT_CN[agent]||agent} Agent`;
879
+ let h='';
880
+ for(const m of _chatHistory){
881
+ const inner=m.role==='user'?escapeHtml(m.content)+_attachmentsHtml(m.attachments):renderMarkdown(m.content);
882
+ h+=_chatRow(m.role, inner, agent, m.ts, false);
883
+ }
884
+ body.innerHTML=h||'<div class="kp-empty" style="padding:30px">这个会话还没有消息</div>';
885
+ Center.chat();
886
+ body.scrollTop=body.scrollHeight;
887
+ }
888
+ async function loadSessions(){
889
+ const list=document.getElementById('nsList'); if(!list)return;
890
+ const cnt=document.getElementById('nsCount');
891
+ try{
892
+ const d=await fetch(`${API}/sessions?limit=50`).then(r=>r.json());
893
+ const ss=d.sessions||[];
894
+ if(cnt) cnt.textContent=ss.length||'0';
895
+ if(!ss.length){ list.innerHTML='<div class="kp-empty">还没有会话 · 发一条消息即开始</div>'; return; }
896
+ list.innerHTML=ss.map(s=>{
897
+ const title=s.title||'新会话';
898
+ const when=s.updated_at?new Date(s.updated_at*1000).toLocaleString('zh-CN',{month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit',hour12:false}):'';
899
+ const meta=[when, s.message_count?`${s.message_count} 条`:''].filter(Boolean).join(' · ');
900
+ return `<div class="ns-item${s.id===_sessionId?' on':''}" data-sid="${escapeHtml(s.id)}" title="${escapeHtml(title)}">
901
+ <b>${escapeHtml(title)}</b>
902
+ <span>${escapeHtml(meta)}</span>
903
+ <span class="ns-del" data-del="${escapeHtml(s.id)}" title="删除会话">✕</span></div>`;
904
+ }).join('');
905
+ list.querySelectorAll('.ns-item').forEach(el=>{
906
+ el.onclick=(ev)=>{
907
+ if(ev.target.dataset.del){ ev.stopPropagation(); delSession(ev.target.dataset.del); return; }
908
+ openSession(el.dataset.sid);
909
+ };
910
+ });
911
+ }catch(e){ list.innerHTML='<div class="kp-empty">后端未连接</div>'; }
912
+ }
913
+ async function delSession(id){
914
+ try{ await fetch(`${API}/sessions/${id}`,{method:'DELETE'}); }catch(e){}
915
+ if(id===_sessionId){ _sessionId=''; _chatHistory=[];
916
+ localStorage.removeItem('foxclawSessionId');
917
+ document.getElementById('coreChat')?.classList.remove('show');
918
+ await ensureSession();
919
+ }
920
+ await loadSessions();
921
+ }
922
+ function _fmtTime(ts){ const d=new Date(ts||Date.now()); return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2); }
923
+ function _chatRow(role, inner, agent, ts, isSlot){
924
+ const t=_fmtTime(ts);
925
+ if(role==='user'){
926
+ return `<div class="chat-row me"><div class="chat-col">`
927
+ +`<div class="chat-meta"><span class="chat-time">${t}</span><span class="chat-name">你</span></div>`
928
+ +`<div class="chat-bubble me">${inner}</div></div>`
929
+ +`<div class="chat-av me">你</div></div>`;
930
+ }
931
+ const nm=`${AGENT_CN[agent]||agent} Agent`, av=(AGENT_CN[agent]||'H').slice(0,1);
932
+ const bubble = isSlot
933
+ ? `<div class="chat-bubble ai cc-md" id="ccReply"><span class="cc-loading">◌ 思考中…</span></div>`
934
+ : `<div class="chat-bubble ai cc-md">${inner}</div>`;
935
+ return `<div class="chat-row ai"><div class="chat-av ai">${escapeHtml(av)}</div><div class="chat-col">`
936
+ +`<div class="chat-meta"><span class="chat-name">${escapeHtml(nm)}</span>${isSlot?'':`<span class="chat-time">${t}</span>`}</div>`
937
+ +bubble+`</div></div>`;
938
+ }
939
+ function renderChatThread(currentQ, agent, attachments){
940
+ document.getElementById('ccAgent').textContent=`${AGENT_CN[agent]||agent} Agent`;
941
+ const body=document.querySelector('#coreChat .cc-body'); if(!body) return;
942
+ let h='';
943
+ for(const m of _chatHistory){
944
+ const inner=m.role==='user'?escapeHtml(m.content)+_attachmentsHtml(m.attachments):renderMarkdown(m.content);
945
+ h += _chatRow(m.role, inner, agent, m.ts, false);
946
+ }
947
+ h += _chatRow('user', escapeHtml(currentQ||'')+_attachmentsHtml(attachments), agent, Date.now(), false);
948
+ h += _chatRow('ai', '', agent, Date.now(), true); // 空答槽:#ccReply 供流式写入
949
+ h += `<div class="cc-status" id="ccStatus"></div>`;
950
+ body.innerHTML=h;
951
+ Center.chat();
952
+ body.scrollTop=body.scrollHeight;
953
+ }
954
+ function renderEvidence(items){
955
+ const wrap=document.getElementById('ccEvidence'); if(!wrap)return;
956
+ if(!items||!items.length){ wrap.hidden=true; wrap.innerHTML=''; return; }
957
+ const STAT={verified:['✓','已验证','ev-ok'],unverified:['?','未核验','ev-warn'],failed:['✕','未完成','ev-bad']};
958
+ const rows=items.map(it=>{
959
+ const s=STAT[it.status]||STAT.unverified;
960
+ const chips=(it.evidence||[]).map(e=>{
961
+ const label=escapeHtml(e.label||e.ref||'');
962
+ const tip=escapeHtml(e.ref||'');
963
+ return e.kind==='url'
964
+ ? `<a class="ev-item" href="${encodeURI(e.ref)}" target="_blank" rel="noopener" title="${tip}">${label}</a>`
965
+ : `<span class="ev-item" title="${tip}">${label}</span>`;
966
+ }).join('');
967
+ const why=it.reason?`<span class="ev-why">${escapeHtml(it.reason)}</span>`:'';
968
+ return `<div class="ev-row"><span class="ev-chip ${s[2]}">${s[0]} ${s[1]}</span>`
969
+ + `<span class="ev-tool">${escapeHtml(it.tool||'')}</span>${why}${chips}</div>`;
970
+ }).join('');
971
+ wrap.innerHTML=`<div class="ev-head">执行凭据 <span class="ev-count">${items.length}</span></div>${rows}`;
972
+ wrap.hidden=false;
973
+ }
974
+
975
+ async function transmitStream(task, agent, images, attachments){
976
+ renderChatThread(task, agent, attachments); // 先把历史+当前问(含附件卡片)渲染成对话线程,再流式写入答案槽
977
+ const _ccBody=document.querySelector('#coreChat .cc-body'); // 注意:下方请求体也叫 body,故这里另起名避免遮蔽
978
+ const rp=document.getElementById('ccReply'), st=document.getElementById('ccStatus');
979
+ rp.textContent=''; rp.style.color='var(--ink)'; st.style.color='var(--ink-dim)'; st.textContent='◌ 流式生成中…';
980
+ let acc='';
981
+ let _thinkWrap=document.getElementById('ccThink');
982
+ if(!_thinkWrap && rp.parentNode){
983
+ _thinkWrap=document.createElement('details');
984
+ _thinkWrap.id='ccThink'; _thinkWrap.className='cc-think'; _thinkWrap.hidden=true;
985
+ _thinkWrap.innerHTML='<summary>推理过程</summary><div class="cc-think-body" id="ccThinkBody"></div>';
986
+ rp.parentNode.insertBefore(_thinkWrap, rp);
987
+ }
988
+ const _thinkBody=document.getElementById('ccThinkBody');
989
+ if(_thinkBody) _thinkBody.textContent='';
990
+ if(_thinkWrap){ _thinkWrap.hidden=true; _thinkWrap.open=false; }
991
+ let _evWrap=document.getElementById('ccEvidence');
992
+ if(!_evWrap && rp.parentNode){
993
+ _evWrap=document.createElement('div');
994
+ _evWrap.id='ccEvidence'; _evWrap.className='cc-ev'; _evWrap.hidden=true;
995
+ rp.parentNode.insertBefore(_evWrap, rp.nextSibling);
996
+ }
997
+ if(_evWrap){ _evWrap.hidden=true; _evWrap.innerHTML=''; }
998
+ let _evs=[];
999
+ try{
1000
+ if(!_sessionId) await ensureSession(); // 保证本轮问答有会话归属(否则落不了盘)
1001
+ const body={task, agent_id:agent, history:_chatHistory.slice(-12), session_id:_sessionId};
1002
+ if(images && images.length) body.images=images; // 多模态:图片随问题发给视觉模型
1003
+ if(attachments && attachments.length) body.attachments=attachments; // 聊天附件:随问题发给 LLM
1004
+ const r=await fetch(`${API}/chat/stream`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
1005
+ if(!r.ok||!r.body){ st.textContent='⚠ 流式请求失败: '+r.status; st.style.color='var(--pink)'; return; }
1006
+ const reader=r.body.getReader(), dec=new TextDecoder(); let buf='';
1007
+ while(true){
1008
+ const {value,done}=await reader.read(); if(done)break;
1009
+ buf+=dec.decode(value,{stream:true});
1010
+ let idx;
1011
+ while((idx=buf.indexOf('\n\n'))>=0){
1012
+ const block=buf.slice(0,idx); buf=buf.slice(idx+2);
1013
+ const evName=((block.match(/^event:\s*(\S+)/m)||[])[1])||'message';
1014
+ const dataLine=((block.match(/^data:\s*([\s\S]*)$/m)||[])[1]||'').trim();
1015
+ if(!dataLine)continue;
1016
+ let ev; try{ ev=JSON.parse(dataLine); }catch(e){ continue; }
1017
+ if(evName==='thinking'||ev.thinking){ // 推理增量 → 折叠区,不进正文
1018
+ const t=ev.thinking||'';
1019
+ if(t){ if(_thinkWrap) _thinkWrap.hidden=false; if(_thinkBody) _thinkBody.textContent+=t; }
1020
+ continue;
1021
+ }
1022
+ if(evName==='evidence'||ev.evidence){ // 执行证据 → 答案下方核对区
1023
+ if(Array.isArray(ev.evidence)){ _evs=ev.evidence; renderEvidence(_evs); }
1024
+ continue;
1025
+ }
1026
+ if(ev.delta){ acc+=ev.delta; rp.textContent=acc; if(_ccBody) _ccBody.scrollTop=_ccBody.scrollHeight; }
1027
+ if(ev.error){ st.textContent='⚠ '+ev.error; st.style.color='var(--pink)'; }
1028
+ if(ev.done){ st.textContent='✓ 完成'; }
1029
+ }
1030
+ }
1031
+ if(acc){
1032
+ renderReply(acc); // 流式结束 → 富文本渲染(markdown + 内嵌图/音/视频)
1033
+ const _now=Date.now();
1034
+ _chatHistory.push({role:'user',content:task,ts:_now,attachments:attachments||undefined},{role:'assistant',content:acc,ts:_now});
1035
+ if(_chatHistory.length>24) _chatHistory=_chatHistory.slice(-24); // 只留最近 ~12 轮
1036
+ loadSessions(); // 会话标题/条数会变 → 刷新左栏列表
1037
+ }
1038
+ if(!acc && st.textContent.indexOf('⚠')<0) st.textContent='(无输出)';
1039
+ markTask(task,'ok');
1040
+ }catch(e){ st.textContent='⚠ '+e; st.style.color='var(--pink)'; markTask(task,'blocked'); }
1041
+ if(typeof loadMemStats==='function') loadMemStats();
1042
+ }
1043
+
1044
+ async function transmitTaskStream(task, agent){
1045
+ const rp=document.getElementById('ccReply'), st=document.getElementById('ccStatus');
1046
+ rp.textContent=''; rp.style.color='var(--ink)'; st.style.color='var(--ink-dim)'; st.textContent='◌ 认知管线启动…';
1047
+ let acc='', stages=[];
1048
+ const STAGE_CN={perceive:'感知',recall:'召回',plan:'规划',route:'路由',act:'执行',verify:'校验',reflect:'反思'};
1049
+ let _planWrap=document.getElementById('ccPlan');
1050
+ if(!_planWrap && rp.parentNode){
1051
+ _planWrap=document.createElement('details');
1052
+ _planWrap.id='ccPlan'; _planWrap.className='cc-plan'; _planWrap.hidden=true;
1053
+ _planWrap.innerHTML='<summary>执行计划</summary><ol class="cc-plan-body" id="ccPlanBody"></ol>';
1054
+ rp.parentNode.insertBefore(_planWrap, rp);
1055
+ }
1056
+ const _planBody=document.getElementById('ccPlanBody');
1057
+ if(_planBody) _planBody.innerHTML='';
1058
+ if(_planWrap){ _planWrap.hidden=true; _planWrap.open=true; }
1059
+ try{
1060
+ const r=await fetch(`${API}/task/stream`,{method:'POST',headers:{'Content-Type':'application/json'},
1061
+ body:JSON.stringify({task, agent_id:agent})});
1062
+ if(!r.ok||!r.body){ st.textContent='⚠ 流式任务请求失败: '+r.status; st.style.color='var(--pink)'; markTask(task,'blocked'); return; }
1063
+ const reader=r.body.getReader(), dec=new TextDecoder(); let buf='';
1064
+ while(true){
1065
+ const {value,done}=await reader.read(); if(done)break;
1066
+ buf+=dec.decode(value,{stream:true});
1067
+ let idx;
1068
+ while((idx=buf.indexOf('\n\n'))>=0){
1069
+ const line=buf.slice(0,idx).trim(); buf=buf.slice(idx+2);
1070
+ if(!line.startsWith('data:'))continue;
1071
+ let ev; try{ ev=JSON.parse(line.slice(5).trim()); }catch(e){ continue; }
1072
+ if(ev.stage){ stages.push(STAGE_CN[ev.stage]||ev.stage); st.textContent='◌ '+stages.join(' → '); }
1073
+ if(ev.plan && ev.plan.steps && ev.plan.steps.length){ // 计划:整轮固定展示,供用户先看步骤
1074
+ if(_planBody) _planBody.innerHTML=ev.plan.steps.map(s=>`<li>${escapeHtml(String(s))}</li>`).join('');
1075
+ if(_planWrap) _planWrap.hidden=false;
1076
+ const _flow={parallel:'并行',sequential:'顺序',single:'单步'}[ev.plan.flow]||ev.plan.flow||'';
1077
+ st.textContent='◌ 计划('+_flow+' · '+ev.plan.steps.length+' 步)';
1078
+ }
1079
+ if(ev.delta){ acc+=ev.delta; rp.textContent=acc; rp.scrollTop=rp.scrollHeight; }
1080
+ if(ev.error){ st.textContent='⚠ '+ev.error; st.style.color='var(--pink)'; markTask(task,'blocked'); }
1081
+ if(ev.result){
1082
+ const res=ev.result||{};
1083
+ if(res.interrupted){ st.textContent='⚠ 任务被中断(部分完成)'; markTask(task,'blocked'); }
1084
+ else if(res.error==='harness_blocked'){ st.textContent='⚠ 被 Harness 拦截:'+(res.reason||''); st.style.color='var(--pink)'; markTask(task,'blocked'); }
1085
+ else if(res.status==='needs_confirmation'){ st.textContent='⏸ 待确认:'+((res.pending_action||{}).skill_id||''); markTask(task,'blocked'); }
1086
+ else{
1087
+ const reply=(typeof res.result==='string'?res.result:(res.result?JSON.stringify(res.result):''))||acc;
1088
+ if(!acc && reply){ acc=reply; rp.textContent=reply; } // skill dispatch 路径无 delta,用 result 兜底
1089
+ st.textContent='✓ 完成'; markTask(task,'ok');
1090
+ }
1091
+ }
1092
+ }
1093
+ }
1094
+ if(acc) renderReply(acc);
1095
+ else if(st.textContent.indexOf('⚠')<0 && st.textContent.indexOf('⏸')<0) st.textContent='(无输出)';
1096
+ }catch(e){ st.textContent='⚠ '+e; st.style.color='var(--pink)'; markTask(task,'blocked'); }
1097
+ if(typeof loadMemStats==='function') loadMemStats();
1098
+ }
1099
+
1100
+ function prependTask(name,agent,status){
1101
+ const list=document.getElementById('taskList'); if(!list)return;
1102
+ const now=new Date(); const hh=String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0');
1103
+ const div=document.createElement('div');
1104
+ div.className='task'+(status==='ok'?' ok':''); div.dataset.task=name;
1105
+ div.innerHTML=`<div class="ti">${status==='ok'?'✓':status==='blocked'?'✕':'◌'}</div>
1106
+ <div class="tb"><b>${escapeHtml(name)}</b><span>${escapeHtml(AGENT_CN[agent]||agent)} Agent</span></div><div class="tt">${hh}</div>`;
1107
+ list.prepend(div);
1108
+ while(list.children.length>6)list.removeChild(list.lastChild);
1109
+ }
1110
+ function markTask(name,status){
1111
+ const list=document.getElementById('taskList'); if(!list)return;
1112
+ const row=[...list.children].find(c=>c.dataset.task===name);
1113
+ if(row){row.className='task'+(status==='ok'?' ok':'');
1114
+ row.querySelector('.ti').textContent=status==='ok'?'✓':status==='blocked'?'✕':'◌';}
1115
+ }
1116
+
1117
+ window.actPulse=0;
1118
+ function connectWS(){
1119
+ try{
1120
+ const _k=localStorage.getItem('foxclawApiKey');
1121
+ const ws=new WebSocket(WS + (_k?('?key='+encodeURIComponent(_k)):''));
1122
+ ws.onmessage=ev=>{
1123
+ let m; try{m=JSON.parse(ev.data);}catch(e){return;}
1124
+ const e=m.event, d=m.data||{};
1125
+ window.actPulse=Math.min(window.actPulse+0.4,1.5);
1126
+ if(e==='harness_block'){ flashCmd('var(--pink)'); }
1127
+ else if(e==='policy_updated'){ flashCmd('var(--violet)'); }
1128
+ };
1129
+ ws.onclose=()=>setTimeout(connectWS,3000);
1130
+ }catch(e){ setTimeout(connectWS,3000); }
1131
+ }
1132
+ function flashCmd(c){
1133
+ const bar=document.querySelector('.cmdbar');
1134
+ if(!bar)return; const old=bar.style.boxShadow;
1135
+ bar.style.boxShadow=`0 0 24px ${c}`; bar.style.borderColor=c;
1136
+ setTimeout(()=>{bar.style.boxShadow=''; bar.style.borderColor='';},700);
1137
+ }
1138
+
1139
+ function openOverlay(agent){
1140
+ const m=KP_META[agent]||KP_META.office;
1141
+ document.getElementById('ovTitle').textContent=m.title;
1142
+ document.getElementById('ovTag').textContent=m.tag;
1143
+ const body=document.getElementById('ovBody');
1144
+ const d=_lastKernelData[agent]||{};
1145
+ if(agent==='admin'){
1146
+ body.innerHTML=`<div class="ov-grid">
1147
+ <div class="ov-spacer"></div>
1148
+ <div style="position:relative;height:100%">
1149
+ <div id="ovMap" style="width:100%;height:100%"></div>
1150
+ <div class="map-search">
1151
+ <input id="mapSearchInput" placeholder="🔍 搜索地点…">
1152
+ <div id="mapSearchResults" class="map-search-results"></div>
1153
+ </div>
1154
+ </div>
1155
+ <div class="ov-side" id="ovItinSide">${itinSideHTML(d)}</div>
1156
+ </div>`;
1157
+ setTimeout(()=>{drawAMapInto('ovMap',d.map_data||[]); initMapSearch();},80);
1158
+ }else if(agent==='office'){
1159
+ body.innerHTML=`<div style="height:100%"><canvas id="ovGraph" style="width:100%;height:100%"></canvas></div>`;
1160
+ setTimeout(()=>drawKnowledgeGraph('ovGraph',d),80);
1161
+ }else if(agent==='finance'){
1162
+ body.innerHTML=`<div style="max-width:900px;margin:0 auto">${renderFinance(d)}</div>`;
1163
+ }else if(agent==='supervisor'){
1164
+ body.innerHTML=`<div style="max-width:900px;margin:0 auto">${renderSituation(d)}</div>`;
1165
+ }else{
1166
+ body.innerHTML=`<div style="max-width:700px;margin:0 auto">${document.getElementById('kpBody')?.innerHTML||''}</div>`;
1167
+ }
1168
+ document.getElementById('ovMask').classList.add('show');
1169
+ }
1170
+ function closeOverlay(){document.getElementById('ovMask').classList.remove('show');}
1171
+ document.getElementById('kpFull')?.addEventListener('click',()=>openOverlay(activeAgent));
1172
+ document.getElementById('ovClose')?.addEventListener('click',closeOverlay);
1173
+ document.getElementById('ovMask')?.addEventListener('click',e=>{if(e.target.id==='ovMask')closeOverlay();});
1174
+ document.addEventListener('keydown',e=>{if(e.key==='Escape')closeOverlay();});
1175
+
1176
+ let autoMap=false;
1177
+ document.getElementById('ovAutoMap')?.addEventListener('change',e=>{autoMap=e.target.checked;});
1178
+
1179
+ function drawKnowledgeGraph(canvasId,d){
1180
+ const c=document.getElementById(canvasId); if(!c)return;
1181
+ const x=c.getContext('2d');
1182
+ function size(){const r=c.parentElement.getBoundingClientRect();c.width=r.width;c.height=r.height;}
1183
+ size();
1184
+ const topics=Object.keys(d.topic_mastery||{});
1185
+ const N=Math.max(d.total_items||8,6);
1186
+ const nodes=[];
1187
+ for(let i=0;i<N;i++){nodes.push({a:i/N*Math.PI*2,r:0.3+Math.random()*0.4,
1188
+ topic:topics[i%(topics.length||1)]||'',s:Math.random()*4+4});}
1189
+ let rot=0;
1190
+ const COL=['#00d9ff','#ffc24b','#00ffb2','#ff9d00','#d99a3c'];
1191
+ function loop(){
1192
+ if(!document.getElementById('ovMask').classList.contains('show'))return;
1193
+ x.clearRect(0,0,c.width,c.height);rot+=0.002;
1194
+ const cx=c.width/2,cy=c.height/2,R=Math.min(cx,cy)*0.8;
1195
+ nodes.forEach(n=>{n._x=cx+Math.cos(n.a+rot)*n.r*R;n._y=cy+Math.sin(n.a+rot)*n.r*R;});
1196
+ x.lineWidth=1;
1197
+ for(let i=0;i<nodes.length;i++)for(let j=i+1;j<nodes.length;j++){
1198
+ if(nodes[i].topic&&nodes[i].topic===nodes[j].topic){
1199
+ x.strokeStyle='rgba(255,194,75,.25)';x.beginPath();
1200
+ x.moveTo(nodes[i]._x,nodes[i]._y);x.lineTo(nodes[j]._x,nodes[j]._y);x.stroke();}
1201
+ }
1202
+ nodes.forEach((n,i)=>{const col=COL[topics.indexOf(n.topic)%COL.length]||'#00d9ff';
1203
+ x.beginPath();x.arc(n._x,n._y,n.s,0,Math.PI*2);
1204
+ x.fillStyle=col;x.shadowBlur=12;x.shadowColor=col;x.fill();x.shadowBlur=0;});
1205
+ requestAnimationFrame(loop);
1206
+ }
1207
+ loop();
1208
+ }
1209
+
1210
+ const MODULE_META={
1211
+ tasks:{title:'任务中心',tag:'Tasks'},
1212
+ memory:{title:'记忆中心',tag:'Memory'},
1213
+ knowledge:{title:'知识库',tag:'Knowledge'},
1214
+ tools:{title:'技能与工具',tag:'Skills'},
1215
+ activity:{title:'活动',tag:'Activity'},
1216
+ agents:{title:'代理',tag:'Agents'},
1217
+ usage:{title:'使用情况',tag:'Usage'},
1218
+ };
1219
+ async function fetchModuleBrief(mod){
1220
+ try{
1221
+ if(mod==='tasks'){
1222
+ const out=[];
1223
+ for(const a of ['supervisor','finance','office']){
1224
+ const r=await fetch(`${API}/thoughtchain/${a}/summary`).then(x=>x.json()).catch(()=>null);
1225
+ if(r&&r.total_nodes){ out.push({title:`${AGENT_CN[a]||a} Agent`, sub:`${r.total_nodes} 节点 · ${r.blocked_nodes||0} 拦截`}); }
1226
+ }
1227
+ return out.length?out:[{title:'暂无任务记录',sub:'发任务后这里显示'}];
1228
+ }
1229
+ if(mod==='memory'){
1230
+ const s=await fetch(`${API}/memory/stats`).then(x=>x.json()).catch(()=>({}));
1231
+ const items=[{title:`记忆总数 ${s.total_memories??0} 条`,sub:`私有 ${s.private??0} · 共享 ${s.shared??0}`}];
1232
+ const ba=s.by_agent||{};
1233
+ Object.entries(ba).sort((a,b)=>b[1]-a[1]).forEach(([a,n])=>items.push({title:AGENT_CN[a]||a,sub:`${n} 条记忆`}));
1234
+ return items;
1235
+ }
1236
+ if(mod==='knowledge'){
1237
+ const r=await fetch(`${API}/learning/analysis`).then(x=>x.json()).catch(()=>null);
1238
+ if(r&&r.items&&r.items.length) return r.items.map(k=>({title:k.title||k.name||'知识条目',sub:k.tag||k.topic||'',content:k.content,topic:k.topic,source:k.source,tags:k.tags,id:k.id,notes:k.notes}));
1239
+ return [{title:'知识库为空',sub:'录入学习数据后显示'}];
1240
+ }
1241
+ if(mod==='tools'){
1242
+ return [{title:'Skill 接入',sub:'助手 Agent 工具扩展'},{title:'插件管理',sub:'开发中'}];
1243
+ }
1244
+ }catch(e){}
1245
+ return [{title:'加载失败',sub:'后端未连接'}];
1246
+ }
1247
+
1248
+ function fillFeed(mod, trackId){
1249
+ const track=document.getElementById(trackId); if(!track)return;
1250
+ fetchModuleBrief(mod).then(items=>{
1251
+ if(!items||!items.length){ track.innerHTML='<div class="feed-empty">暂无数据</div>'; return; }
1252
+ const render=()=>items.map((it,idx)=>`<div class="feed-item" data-mod="${mod}" data-idx="${idx}"><b>${escapeHtml(it.title)}</b>${it.sub?`<span>${escapeHtml(it.sub)}</span>`:''}</div>`).join('');
1253
+ const enough=items.length>=4;
1254
+ track.innerHTML=render()+(enough?render():'');
1255
+ track.classList.toggle('rolling', enough);
1256
+ if(enough){ track.style.animationDuration=(items.length*2.2)+'s'; }
1257
+ else{ track.style.animationDuration=''; }
1258
+ track.querySelectorAll('.feed-item').forEach(el=>el.onclick=()=>{
1259
+ if(mod==='knowledge'){ const it=items[+el.dataset.idx]; if(it) showKnowledgeArticle(it); }
1260
+ else showModuleInCore(mod);
1261
+ });
1262
+ });
1263
+ }
1264
+ function refreshFeeds(){
1265
+ fillFeed('tasks','feedTasks');
1266
+ fillFeed('memory','feedMemory');
1267
+ fillFeed('knowledge','feedKnowledge');
1268
+ }
1269
+ function _knowCardHtml(k){
1270
+ const notes=(k.notes||[]);
1271
+ return `<div class="know-card">
1272
+ <div class="know-head"><b>${escapeHtml(k.topic||'知识')}</b>
1273
+ <span class="know-tools">${k.source?`<span class="know-src">来源: ${escapeHtml(k.source)}</span>`:''}<span class="know-del" data-id="${escapeHtml(k.id||'')}" title="删除这条知识">🗑</span></span></div>
1274
+ <div class="know-body">${escapeHtml(k.content||k.title||'')}</div>
1275
+ ${(k.tags&&k.tags.length)?`<div class="know-tags">${k.tags.map(t=>`<span>#${escapeHtml(t)}</span>`).join('')}</div>`:''}
1276
+ <div class="know-notes">
1277
+ <div class="know-notes-h">📝 我的笔记${notes.length?` (${notes.length})`:''}</div>
1278
+ ${notes.length?notes.map(n=>`<div class="know-note"><span>${escapeHtml(n.text||'')}</span><em>${escapeHtml((n.ts||'').slice(0,16).replace('T',' '))}</em></div>`).join(''):'<div class="know-note-empty">还没有笔记,读到有心得就在下面记一条</div>'}
1279
+ <div class="know-note-add">
1280
+ <textarea class="know-note-input" placeholder="来吧,留下你的学习成果" rows="2"></textarea>
1281
+ <button class="know-note-save" data-id="${escapeHtml(k.id||'')}">保存笔记</button>
1282
+ </div>
1283
+ </div>
1284
+ </div>`;
1285
+ }
1286
+ function _renderKnowList(body, items, reload){
1287
+ body.innerHTML='<div class="know-index">'+items.map((k,i)=>`
1288
+ <div class="know-row" data-i="${i}">
1289
+ <div class="know-row-tx"><b>${escapeHtml(k.topic||'知识')}</b><span>${escapeHtml((k.content||k.title||'').replace(/\s+/g,' ').trim().slice(0,54))||'(空)'}</span></div>
1290
+ <span class="know-del" data-id="${escapeHtml(k.id||'')}" title="删除这条">🗑</span>
1291
+ </div>`).join('')+'</div>';
1292
+ body.querySelectorAll('.know-row').forEach(el=>el.onclick=(e)=>{ if(e.target.closest('.know-del'))return; _renderKnowDetail(body, items, +el.dataset.i, reload); });
1293
+ _wireKnowDelete(body, reload);
1294
+ }
1295
+ function _wireKnowNote(body, item, onSaved){
1296
+ const saveBtn=body.querySelector('.know-note-save');
1297
+ if(!saveBtn)return;
1298
+ saveBtn.onclick=async()=>{
1299
+ const ta=body.querySelector('.know-note-input'); const text=(ta.value||'').trim();
1300
+ if(!text) return; // 没写就不存(不重复写盘)
1301
+ saveBtn.disabled=true;
1302
+ try{
1303
+ await fetch(`${API}/learning/note`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({item_id:item.id, note:text})});
1304
+ item.notes=(item.notes||[]).concat([{text, ts:new Date().toISOString()}]); // 本地追加
1305
+ onSaved && onSaved();
1306
+ }catch(e){ alert('保存笔记失败:'+e); saveBtn.disabled=false; }
1307
+ };
1308
+ }
1309
+ function _renderKnowDetail(body, items, i, reload){
1310
+ const k=items[i]; if(!k){ _renderKnowList(body, items, reload); return; }
1311
+ body.innerHTML=`<div class="know-back" id="knowBack">← 返回列表(共 ${items.length} 篇)</div>`+_knowCardHtml(k);
1312
+ document.getElementById('knowBack').onclick=()=>_renderKnowList(body, items, reload);
1313
+ _wireKnowDelete(body, reload);
1314
+ _wireKnowNote(body, k, ()=>_renderKnowDetail(body, items, i, reload));
1315
+ }
1316
+ function showKnowledgeArticle(item){
1317
+ const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
1318
+ if(!wrap||!stage||!item)return;
1319
+ document.getElementById('coreChat')?.classList.remove('show'); // 开面板层前关对话浮层(两层互斥)
1320
+ Center.panel();
1321
+ stage.innerHTML=`<div class="cs-head"><div class="t"><span class="dot"></span>${escapeHtml(item.topic||'知识')} <em>Knowledge</em></div><span class="x" id="csClose" title="关闭">✕</span></div><div class="cs-body" id="csBody"></div>`;
1322
+ document.getElementById('csClose').onclick=clearCore;
1323
+ const body=document.getElementById('csBody');
1324
+ body.innerHTML=_knowCardHtml(item);
1325
+ _wireKnowDelete(body, clearCore); // 删除后清空中央
1326
+ _wireKnowNote(body, item, ()=>showKnowledgeArticle(item)); // 保存笔记后重渲染本篇
1327
+ }
1328
+ function _wireKnowDelete(container, onDone){
1329
+ container.querySelectorAll('.know-del').forEach(el=>el.onclick=async()=>{
1330
+ const id=el.dataset.id; if(!id||!confirm('删除这条知识?此操作不可撤销。'))return;
1331
+ try{
1332
+ await fetch(`${API}/learning/item/${encodeURIComponent(id)}`,{method:'DELETE'});
1333
+ onDone&&onDone(); refreshFeeds();
1334
+ try{ loadMemStats(); }catch(_){}
1335
+ }catch(e){ alert('删除失败:'+e); }
1336
+ });
1337
+ }
1338
+ async function showKnowledgeFull(){
1339
+ const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
1340
+ if(!wrap||!stage)return;
1341
+ document.getElementById('coreChat')?.classList.remove('show'); // 开面板前关对话浮层(否则被 z5 盖住)
1342
+ Center.panel();
1343
+ _syncNav('knowledge');
1344
+ stage.innerHTML=`<div class="cs-head"><div class="t"><span class="dot"></span>知识库 <em>Knowledge</em></div><span class="x" id="csClose" title="返回">✕</span></div><div class="cs-body" id="csBody"><div class="kp-empty" style="padding:30px 0">加载中…</div></div>`;
1345
+ document.getElementById('csClose').onclick=clearCore;
1346
+ const body=document.getElementById('csBody');
1347
+ try{
1348
+ const r=await fetch(`${API}/learning/analysis`).then(x=>x.json());
1349
+ const items=(r.items||[]);
1350
+ if(!items.length){ body.innerHTML='<div class="kp-empty" style="padding:30px 0">还没有录入知识。点顶部「录入知识点」添加。</div>'; return; }
1351
+ _renderKnowList(body, items, showKnowledgeFull);
1352
+ }catch(e){ body.innerHTML='<div class="kp-empty" style="padding:30px 0">后端未连接</div>'; }
1353
+ }
1354
+ const _navMenu=document.getElementById('navMenu');
1355
+ const _navMenuBtn=document.getElementById('navMenuBtn');
1356
+ function setNavMenu(open){
1357
+ if(!_navMenu||!_navMenuBtn)return;
1358
+ const show=open===undefined?!_navMenu.classList.contains('show'):open;
1359
+ if(show){
1360
+ const r=_navMenuBtn.getBoundingClientRect();
1361
+ _navMenu.style.left=Math.round(r.right+10)+'px';
1362
+ _navMenu.style.bottom=Math.round(Math.max(10,window.innerHeight-r.bottom))+'px';
1363
+ _navMenu.classList.add('show'); _navMenu.setAttribute('aria-hidden','false');
1364
+ _navMenuBtn.classList.add('open'); _navMenuBtn.setAttribute('aria-expanded','true');
1365
+ }else{
1366
+ _navMenu.classList.remove('show'); _navMenu.setAttribute('aria-hidden','true');
1367
+ _navMenuBtn.classList.remove('open'); _navMenuBtn.setAttribute('aria-expanded','false');
1368
+ }
1369
+ }
1370
+ _navMenuBtn?.addEventListener('click',e=>{ e.stopPropagation(); setNavMenu(); });
1371
+ document.getElementById('navMenuClose')?.addEventListener('click',()=>setNavMenu(false));
1372
+ document.addEventListener('click',e=>{
1373
+ if(!_navMenu||!_navMenu.classList.contains('show'))return;
1374
+ if(_navMenu.contains(e.target)||_navMenuBtn.contains(e.target))return;
1375
+ setNavMenu(false);
1376
+ });
1377
+ document.addEventListener('keydown',e=>{ if(e.key==='Escape') setNavMenu(false); });
1378
+ window.addEventListener('resize',()=>setNavMenu(false));
1379
+
1380
+ function _syncNav(mod){
1381
+ let name='';
1382
+ document.querySelectorAll('#navList .nav-link').forEach(x=>{
1383
+ const hit=x.dataset.module===mod;
1384
+ x.classList.toggle('on',hit);
1385
+ if(hit) name=x.querySelector('b')?.textContent||'';
1386
+ });
1387
+ const lbl=document.getElementById('navMenuLabel');
1388
+ if(lbl) lbl.textContent=(name&&mod!=='overview')?`菜单 · ${name}`:'菜单';
1389
+ }
1390
+
1391
+ document.querySelectorAll('#navList .nav-link').forEach(el=>{
1392
+ el.addEventListener('click',()=>{
1393
+ const mod=el.dataset.module;
1394
+ _syncNav(mod);
1395
+ setNavMenu(false); // 选完就收起菜单,把屏幕还给内容
1396
+ if(mod==='overview') clearCore();
1397
+ else if(mod==='knowledge') showKnowledgeFull();
1398
+ else showModuleInCore(mod);
1399
+ });
1400
+ });
1401
+
1402
+ async function showModuleInCore(mod){
1403
+ const meta=MODULE_META[mod]||{title:mod,tag:''};
1404
+ const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
1405
+ document.getElementById('coreChat')?.classList.remove('show'); // 开面板前关对话浮层(否则被 z5 盖住)
1406
+ Center.panel();
1407
+ _syncNav(mod);
1408
+ stage.innerHTML=`<div class="cs-head"><div class="t"><span class="dot"></span>${meta.title} <em>${meta.tag}</em></div>
1409
+ <span class="x" id="csClose" title="返回核心">✕</span></div>
1410
+ <div class="cs-body" id="csBody"><div class="kp-empty" style="padding:30px 0">加载中…</div></div>`;
1411
+ document.getElementById('csClose').onclick=clearCore;
1412
+ const body=document.getElementById('csBody');
1413
+ try{
1414
+ body.innerHTML=await renderModuleBig(mod);
1415
+ if(mod==='tools') wireToolCenter(body);
1416
+ if(mod==='memory') wirePrefDelete(body);
1417
+ }catch(e){ body.innerHTML='<div class="kp-empty">后端未连接</div>'; }
1418
+ }
1419
+ function wirePrefDelete(body){
1420
+ body.querySelectorAll('.pref-del').forEach(btn=>{
1421
+ btn.onclick=async()=>{
1422
+ const id=btn.dataset.prefId; if(!id)return;
1423
+ btn.disabled=true; btn.textContent='…';
1424
+ try{ await fetch(`${API}/preferences/${encodeURIComponent(id)}`,{method:'DELETE'}); }catch(e){}
1425
+ showModuleInCore('memory'); // 刷新列表
1426
+ };
1427
+ });
1428
+ }
1429
+ async function renderModuleBig(mod){
1430
+ if(mod==='tasks'){
1431
+ let h='<div class="big-grid">';
1432
+ for(const a of ['supervisor','finance','office']){
1433
+ const r=await fetch(`${API}/thoughtchain/${a}/summary`).then(x=>x.json()).catch(()=>null);
1434
+ h+=`<div class="big-card"><div class="bc-head">${AGENT_CN[a]||a} Agent</div>
1435
+ <div class="bc-row"><span>推理节点</span><b>${r?.total_nodes??0}</b></div>
1436
+ <div class="bc-row"><span>被拦截</span><b style="color:var(--amber)">${r?.blocked_nodes??0}</b></div>
1437
+ <div class="bc-row"><span>拦截率</span><b>${r?.block_rate!=null?(r.block_rate*100).toFixed(1)+'%':'—'}</b></div>
1438
+ <div class="bc-row"><span>平均置信</span><b>${r?.avg_confidence!=null?r.avg_confidence.toFixed(2):'—'}</b></div></div>`;
1439
+ }
1440
+ h+='</div>';
1441
+ return h;
1442
+ }
1443
+ if(mod==='activity'){
1444
+ const d=await fetch(`${API}/bus/history?limit=40`).then(x=>x.json()).catch(()=>({}));
1445
+ const evs=(d.events||[]);
1446
+ if(!evs.length) return '<div class="kp-empty" style="padding:40px">暂无活动记录 · 系统事件(执行→审计→记忆)会实时汇入这里</div>';
1447
+ return '<div class="big-list">'+evs.slice().reverse().map(e=>{
1448
+ const topic=e.topic||e.channel||'事件';
1449
+ const ts=Number(e.ts);
1450
+ const when=(isFinite(ts)&&ts>0)?new Date(ts*1000).toLocaleString('zh-CN',{hour12:false}):'';
1451
+ const p=(e.payload&&typeof e.payload==='object')?e.payload:{};
1452
+ const bits=Object.entries(p).slice(0,4)
1453
+ .map(([k,v])=>`${k}=${(v&&typeof v==='object')?JSON.stringify(v):v}`).join(' · ');
1454
+ const meta=[when, e.source].filter(Boolean).join(' · ');
1455
+ return `<div class="big-li"><b>${escapeHtml(String(topic))}</b><span>${escapeHtml(meta)}${bits?' · '+escapeHtml(bits.slice(0,110)):''}</span></div>`;
1456
+ }).join('')+'</div>';
1457
+ }
1458
+ if(mod==='agents'){
1459
+ const AG=[['supervisor','主管','全局统筹·任务分派'],
1460
+ ['finance','财务','收支·预算·理财'],['office','办公学习','学习·知识·写作·日程行程']];
1461
+ const stats=await fetch(`${API}/memory/stats`).then(x=>x.json()).catch(()=>({}));
1462
+ const ba=stats.by_agent||{};
1463
+ let h='<div class="big-grid">';
1464
+ for(const [id,cn,role] of AG){
1465
+ const tc=await fetch(`${API}/thoughtchain/${id}/summary`).then(x=>x.json()).catch(()=>null);
1466
+ h+=`<div class="big-card"><div class="bc-head">${cn} Agent</div>
1467
+ <div style="font-size:11px;color:var(--dim);margin:-4px 0 8px">${role}</div>
1468
+ <div class="bc-row"><span>记忆</span><b>${ba[id]??0} 条</b></div>
1469
+ <div class="bc-row"><span>推理节点</span><b>${tc?.total_nodes??0}</b></div>
1470
+ <div class="bc-row"><span>被拦截</span><b style="color:var(--amber)">${tc?.blocked_nodes??0}</b></div></div>`;
1471
+ }
1472
+ return h+'</div>';
1473
+ }
1474
+ if(mod==='usage'){
1475
+ const m=await fetch(`${API}/metrics`).then(x=>x.json()).catch(()=>({}));
1476
+ const tr=await fetch(`${API}/tokens/report`).then(x=>x.json()).catch(()=>({}));
1477
+ const llm=(m.llm||m.providers||{});
1478
+ const llmRows=Object.entries(llm).map(([k,v])=>{
1479
+ const calls=(v&&typeof v==='object')?(v.calls??v.count??v.total??v.ok??'—'):v;
1480
+ return `<div class="bc-row"><span>${escapeHtml(k)}</span><b>${escapeHtml(String(calls))}</b></div>`;
1481
+ }).join('');
1482
+ const up=m.uptime??m.uptime_seconds;
1483
+ const report=String(tr.report||tr.error||'token meter 未启用或暂无数据');
1484
+ return `<div class="big-grid">
1485
+ <div class="big-card"><div class="bc-head">运行</div>
1486
+ <div class="bc-row"><span>uptime</span><b>${up!=null?Math.round(up)+' s':'—'}</b></div>
1487
+ <div class="bc-row"><span>LLM provider 数</span><b>${Object.keys(llm).length}</b></div></div>
1488
+ <div class="big-card"><div class="bc-head">各 provider 调用</div>${llmRows||'<div class="kp-empty" style="padding:8px">暂无调用</div>'}</div>
1489
+ <div class="big-card" style="grid-column:1/-1"><div class="bc-head">Token 消耗报告</div>
1490
+ <pre style="white-space:pre-wrap;font-size:11px;color:var(--dim);line-height:1.55;margin:0;font-family:var(--font)">${escapeHtml(report)}</pre></div>
1491
+ </div>`;
1492
+ }
1493
+ if(mod==='memory'){
1494
+ const s=await fetch(`${API}/memory/stats`).then(x=>x.json()).catch(()=>({}));
1495
+ const pf=await fetch(`${API}/preferences`).then(x=>x.json()).catch(()=>({preferences:[]}));
1496
+ const ba=s.by_agent||{};
1497
+ let rows=Object.entries(ba).sort((a,b)=>b[1]-a[1]).map(([a,n])=>`<div class="bc-row"><span>${AGENT_CN[a]||a}</span><b>${n} 条</b></div>`).join('')||'<div class="kp-empty">暂无分层记忆</div>';
1498
+ const prefs=(pf.preferences||[]);
1499
+ const prefRows=prefs.length
1500
+ ? prefs.map(p=>`<div class="bc-row" style="align-items:flex-start;gap:6px">
1501
+ <span style="flex:1;font-size:11px;line-height:1.4">${escapeHtml((p.content||'').replace(/^用户偏好[::]/,''))}</span>
1502
+ <button class="pref-del" data-pref-id="${escapeHtml(p.id||'')}" title="删除这条偏好" style="background:none;border:0;color:var(--amber);cursor:pointer;font-size:13px;padding:0 2px">✕</button></div>`).join('')
1503
+ : '<div class="kp-empty">暂无记住的偏好(说"我喜欢…/以后…"会被记住)</div>';
1504
+ return `<div class="big-grid">
1505
+ <div class="big-card"><div class="bc-head">记忆总览</div>
1506
+ <div class="bc-row"><span>总条数</span><b>${s.total_memories??0}</b></div>
1507
+ <div class="bc-row"><span>私有层</span><b>${s.private??0}</b></div>
1508
+ <div class="bc-row"><span>共享层</span><b>${s.shared??0}</b></div>
1509
+ <div class="bc-row"><span>知识关系</span><b>${s.relations??0}</b></div></div>
1510
+ <div class="big-card"><div class="bc-head">按 Agent 分布</div>${rows}</div>
1511
+ <div class="big-card"><div class="bc-head">后端</div>
1512
+ <div class="bc-row"><span>backend</span><b>${s.backend||'—'}</b></div>
1513
+ <div class="bc-row"><span>Embedding</span><b style="font-size:11px">${s.embedding||'—'}</b></div></div>
1514
+ <div class="big-card"><div class="bc-head">用户偏好 <em style="font-size:10px;color:var(--ink-dim);font-style:normal">点 ✕ 删除误记</em></div>${prefRows}</div>
1515
+ </div>`;
1516
+ }
1517
+ if(mod==='knowledge'){
1518
+ const r=await fetch(`${API}/learning/analysis`).then(x=>x.json()).catch(()=>null);
1519
+ if(r&&r.items&&r.items.length){
1520
+ return '<div class="big-list">'+r.items.map(k=>`<div class="big-li"><b>${k.title||k.name||'条目'}</b><span>${k.category||k.tag||''}</span></div>`).join('')+'</div>';
1521
+ }
1522
+ return '<div class="kp-empty" style="padding:40px">知识库为空 · 通过办公 Agent 录入学习数据后显示</div>';
1523
+ }
1524
+ if(mod==='tools'){
1525
+ let skills=[], breakers={}, audit=[];
1526
+ try{ skills=(await fetch(`${API}/ahs/skills`).then(x=>x.json())).skills||[]; }catch(e){}
1527
+ try{ breakers=(await fetch(`${API}/ahs/breaker`).then(x=>x.json())).breakers||{}; }catch(e){}
1528
+ try{ audit=(await fetch(`${API}/ahs/hooks/audit`).then(x=>x.json())).audit||[]; }catch(e){}
1529
+ let servers=[];
1530
+ try{ servers=(await fetch(`${API}/ahs/mcp/servers`).then(x=>x.json())).servers||[]; }catch(e){}
1531
+ let mdskills=[];
1532
+ try{ mdskills=(await fetch(`${API}/ahs/md-skills`).then(x=>x.json())).skills||[]; }catch(e){}
1533
+ const mdRows=mdskills.map(s=>`<div class="md-row${s.blocked?' tc-blocked':''}" data-id="${escapeHtml(s.id)}" data-triggers="${escapeHtml((s.triggers||[]).join(', '))}">
1534
+ <div class="md-h"><b>${escapeHtml(s.name)}</b>${s.version?` <span class="tc-kind">v${escapeHtml(s.version)}</span>`:''} <span class="tc-trust" style="background:#b0413e" title="信任分级">low</span> <span class="tc-src user">${s.enabled?'启用':'停用'}</span>${s.blocked?' <span class="tc-blk-tag">已封杀</span>':''}</div>
1535
+ <div class="md-desc">${escapeHtml(s.description||'')} · 触发: ${escapeHtml((s.triggers||[]).join(', ')||'无(靠描述相关注入)')}</div>
1536
+ <div class="md-acts"><button class="md-view">查看</button>
1537
+ <button class="md-trig">编辑触发词</button>
1538
+ <button class="md-toggle">${s.enabled?'停用':'启用'}</button>
1539
+ <button class="md-deny">${s.blocked?'解封':'封杀'}</button>
1540
+ <button class="md-del">删除</button></div></div>`).join('')||'<div class="kp-empty" style="padding:10px">未安装指令技能</div>';
1541
+ const srvRows=servers.map(s=>`<div class="mcp-row" data-id="${escapeHtml(s.id)}">
1542
+ <div><b>${escapeHtml(s.name||s.id)}</b> <span class="tc-kind">${escapeHtml(s.transport)}</span> <span class="mcp-tc">${s.tool_count} 工具</span></div>
1543
+ <div class="mcp-url">${escapeHtml(s.url)}</div>
1544
+ <button class="mcp-del">移除</button></div>`).join('')||'<div class="kp-empty" style="padding:10px">未接入 MCP 服务器</div>';
1545
+ const seColor={read:'#2c9678',write:'#d9822b',send:'#0066CC',pay:'#b0413e'};
1546
+ const trustColor={high:'#2c9678',mid:'#d9822b',low:'#b0413e'};
1547
+ const brDot=st=>st&&st.state==='open'?'#b0413e':st&&st.state==='half_open'?'#d9822b':'#2c9678';
1548
+ const rows=skills.map(s=>`<div class="tc-row${s.blocked?' tc-blocked':''}" data-id="${escapeHtml(s.id)}">
1549
+ <div class="tc-main"><b>${escapeHtml(s.name||s.id)}</b>
1550
+ <span class="tc-badge" style="background:${seColor[s.side_effect]||'#888'}">${escapeHtml(s.side_effect)}</span>
1551
+ <span class="tc-trust" style="background:${trustColor[s.trust]||'#888'}" title="信任分级">${escapeHtml(s.trust||'')}</span>
1552
+ <span class="tc-kind">${escapeHtml(s.kind)}</span>
1553
+ <span class="tc-src ${s.source}">${s.source==='user'?'已安装':'内置'}</span>
1554
+ <span class="tc-brk" title="断路器" style="background:${brDot(breakers[s.id])}"></span>
1555
+ ${s.blocked?'<span class="tc-blk-tag">已封杀</span>':''}</div>
1556
+ <div class="tc-desc">${escapeHtml(s.description||'')} · 触发: ${escapeHtml((s.triggers||[]).join(', ')||'—')}</div>
1557
+ <div class="tc-acts">
1558
+ <label class="tc-sw"><input type="checkbox" class="tc-en" ${s.enabled?'checked':''}><span>启用</span></label>
1559
+ <button class="tc-test">测试</button>
1560
+ <button class="tc-deny">${s.blocked?'解封':'封杀'}</button>
1561
+ ${s.source==='user'?'<button class="tc-del">删除</button>':''}
1562
+ </div></div>`).join('')||'<div class="kp-empty">还没有技能</div>';
1563
+ const auditRows=audit.slice(0,8).map(a=>`<div class="tc-audit"><span>${escapeHtml(a.skill_id||a.skill||'—')}</span><em>${escapeHtml(String(a.result||a.status||''))}</em></div>`).join('')||'<div class="kp-empty" style="padding:12px">暂无调用</div>';
1564
+ return `<div class="tc-wrap">
1565
+ <div class="tc-col">
1566
+ <div class="tc-h">连接器 · MCP <span>${servers.length}</span></div>
1567
+ <div class="mcp-list">${srvRows}</div>
1568
+ <div class="mcp-form">
1569
+ <input id="mcf-id" placeholder="服务器 id(如 my_mcp)">
1570
+ <input id="mcf-name" placeholder="名称">
1571
+ <input id="mcf-url" placeholder="URL https://.../mcp">
1572
+ <input id="mcf-token" placeholder="Authorization 令牌(可选,明文存本地)">
1573
+ <select id="mcf-transport"><option value="streamable_http">streamable_http</option><option value="sse">sse</option></select>
1574
+ <button id="mcf-submit">接入</button>
1575
+ </div>
1576
+ <div class="tc-h" style="margin-top:14px">技能 <span>${skills.length}</span></div>
1577
+ <div class="tc-list">${rows}</div>
1578
+ <div class="tc-h" style="margin-top:14px">指令技能 · SKILL.md <span>${mdskills.length}</span></div>
1579
+ <div class="md-list">${mdRows}</div>
1580
+ <div class="md-form">
1581
+ <input type="file" id="mdf-file" accept=".md,.markdown,.zip,text/markdown">
1582
+ <button id="mdf-submit">上传安装</button>
1583
+ </div>
1584
+ <div class="tc-h" style="margin-top:14px">安装技能</div>
1585
+ <div class="tc-form">
1586
+ <input id="tcf-id" placeholder="唯一 id(如 my_weather)">
1587
+ <input id="tcf-name" placeholder="名称">
1588
+ <select id="tcf-kind"><option value="web_api">web_api(HTTP 接口)</option><option value="mcp">mcp(MCP 工具)</option></select>
1589
+ <input id="tcf-triggers" placeholder="触发词(逗号分隔)">
1590
+ <select id="tcf-se"><option value="read">read(只读)</option><option value="write">write</option><option value="send">send</option><option value="pay">pay</option></select>
1591
+ <input id="tcf-endpoint" placeholder="web_api: https://... | mcp: server 名">
1592
+ <input id="tcf-tool" placeholder="mcp: tool 名(web_api 留空)">
1593
+ <button id="tcf-submit">安装</button>
1594
+ </div>
1595
+ </div>
1596
+ <div class="tc-col tc-gov">
1597
+ <div class="tc-h">治理 · 最近调用</div>
1598
+ <div class="tc-auditlist">${auditRows}</div>
1599
+ </div>
1600
+ </div>`;
1601
+ }
1602
+ return '<div class="kp-empty">未知模块</div>';
1603
+ }
1604
+ async function wireToolCenter(root){
1605
+ const reload=()=>showModuleInCore('tools');
1606
+ root.querySelectorAll('.tc-row').forEach(row=>{
1607
+ const id=row.dataset.id;
1608
+ const en=row.querySelector('.tc-en');
1609
+ if(en) en.onchange=async()=>{ await fetch(`${API}/ahs/skills/${encodeURIComponent(id)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:en.checked})}); reload(); };
1610
+ const del=row.querySelector('.tc-del');
1611
+ if(del) del.onclick=async()=>{ if(!confirm('删除这个技能?'))return; await fetch(`${API}/ahs/skills/${encodeURIComponent(id)}`,{method:'DELETE'}); reload(); };
1612
+ const deny=row.querySelector('.tc-deny');
1613
+ if(deny) deny.onclick=async()=>{ const on=deny.textContent.trim()==='封杀';
1614
+ if(on&&!confirm('封杀该技能?deny 会压倒启用、立即对所有执行/注入生效。'))return;
1615
+ if(on) await fetch(`${API}/ahs/policy/deny`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})});
1616
+ else await fetch(`${API}/ahs/policy/deny/${encodeURIComponent(id)}`,{method:'DELETE'});
1617
+ reload(); };
1618
+ const test=row.querySelector('.tc-test');
1619
+ if(test) test.onclick=async()=>{
1620
+ let args={}; const raw=prompt('测试参数(JSON,可留空)','{}');
1621
+ if(raw===null)return;
1622
+ try{ args=raw.trim()?JSON.parse(raw):{}; }catch(e){ alert('参数不是合法 JSON'); return; }
1623
+ let r=await fetch(`${API}/ahs/skills/${encodeURIComponent(id)}/test`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({args})}).then(x=>x.json());
1624
+ if(r.status==='needs_confirmation'){
1625
+ if(!confirm('此操作有副作用,确认执行?'))return;
1626
+ r=await fetch(`${API}/ahs/skills/${encodeURIComponent(id)}/test`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({args,confirmed:true})}).then(x=>x.json());
1627
+ }
1628
+ alert((r.success?'✓ 成功':'✗ 失败')+' · '+(r.message||JSON.stringify(r.output||{}).slice(0,200)));
1629
+ };
1630
+ });
1631
+ root.querySelectorAll('.mcp-row').forEach(row=>{
1632
+ const del=row.querySelector('.mcp-del');
1633
+ if(del) del.onclick=async()=>{ if(!confirm('移除该 MCP 服务器及其全部工具技能?'))return;
1634
+ await fetch(`${API}/ahs/mcp/servers/${encodeURIComponent(row.dataset.id)}`,{method:'DELETE'}); reload(); };
1635
+ });
1636
+ const msub=root.querySelector('#mcf-submit');
1637
+ if(msub) msub.onclick=async()=>{
1638
+ const g=s=>(root.querySelector('#mcf-'+s)||{}).value||'';
1639
+ const id=g('id').trim(), url=g('url').trim();
1640
+ if(!id||!url){ alert('id 和 URL 必填'); return; }
1641
+ const headers={}; const tok=g('token').trim(); if(tok) headers['Authorization']=tok;
1642
+ const body={id,name:g('name').trim()||id,url,headers,transport:g('transport')};
1643
+ const doPost=b=>fetch(`${API}/ahs/mcp/servers`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
1644
+ let r=await doPost(body);
1645
+ if(r.status===403){ const e=await r.json().catch(()=>({})); alert('❌ 接入被审核拦截:\n'+((e.detail&&e.detail.reasons)||['被拒绝']).join('\n')); return; }
1646
+ if(r.status===409){ const e=await r.json().catch(()=>({})); const w=(e.detail&&e.detail.warnings)||[];
1647
+ if(!confirm('⚠ 接入审核提示风险:\n'+w.join('\n')+'\n\n确认仍要接入?')) return;
1648
+ r=await doPost({...body, acknowledged:true});
1649
+ }
1650
+ if(!r.ok){ const e=await r.json().catch(()=>({})); alert('接入失败: '+(JSON.stringify(e&&e.detail)||r.status)); return; }
1651
+ const d=await r.json(); alert(`✓ 已接入,注册 ${d.tools_registered} 个工具技能`); reload();
1652
+ };
1653
+ root.querySelectorAll('.md-row').forEach(row=>{
1654
+ const id=row.dataset.id;
1655
+ const v=row.querySelector('.md-view');
1656
+ if(v) v.onclick=async()=>{ const d=await fetch(`${API}/ahs/md-skills/${encodeURIComponent(id)}`).then(x=>x.json());
1657
+ alert(`【${d.name}】\n${d.description}\n\n触发: ${(d.triggers||[]).join(', ')||'无'}\n\n${(d.body||'').slice(0,1500)}`); };
1658
+ const tg=row.querySelector('.md-toggle');
1659
+ if(tg) tg.onclick=async()=>{ const on=tg.textContent.trim()==='启用';
1660
+ await fetch(`${API}/ahs/md-skills/${encodeURIComponent(id)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:on})}); reload(); };
1661
+ const tr=row.querySelector('.md-trig');
1662
+ if(tr) tr.onclick=async()=>{
1663
+ const cur=row.dataset.triggers||'';
1664
+ const val=prompt('触发词(逗号分隔;留空=靠英文描述相关匹配)。\n中文任务想稳定触发就在这加中文词。', cur);
1665
+ if(val===null)return;
1666
+ const triggers=val.split(',').map(x=>x.trim()).filter(Boolean);
1667
+ await fetch(`${API}/ahs/md-skills/${encodeURIComponent(id)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({triggers})}); reload(); };
1668
+ const dy=row.querySelector('.md-deny');
1669
+ if(dy) dy.onclick=async()=>{ const on=dy.textContent.trim()==='封杀';
1670
+ if(on&&!confirm('封杀该指令技能?封杀后不再注入任何 Agent 提示词。'))return;
1671
+ if(on) await fetch(`${API}/ahs/policy/deny`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})});
1672
+ else await fetch(`${API}/ahs/policy/deny/${encodeURIComponent(id)}`,{method:'DELETE'});
1673
+ reload(); };
1674
+ const dl=row.querySelector('.md-del');
1675
+ if(dl) dl.onclick=async()=>{ if(!confirm('删除该指令技能?'))return;
1676
+ await fetch(`${API}/ahs/md-skills/${encodeURIComponent(id)}`,{method:'DELETE'}); reload(); };
1677
+ });
1678
+ const mdsub=root.querySelector('#mdf-submit');
1679
+ if(mdsub) mdsub.onclick=async()=>{
1680
+ const f=(root.querySelector('#mdf-file')||{}).files?.[0];
1681
+ if(!f){ alert('请先选择 .md 或 OpenClaw .zip 包'); return; }
1682
+ let body;
1683
+ if(/\.zip$/i.test(f.name)){
1684
+ const b64=await new Promise((res,rej)=>{const fr=new FileReader();fr.onload=()=>res(String(fr.result).split(',')[1]);fr.onerror=rej;fr.readAsDataURL(f);});
1685
+ body={package_b64:b64};
1686
+ }else{
1687
+ const content=await f.text();
1688
+ const id=f.name.replace(/\.(md|markdown)$/i,'').replace(/[^a-z0-9]+/gi,'_').toLowerCase();
1689
+ body={content,id};
1690
+ }
1691
+ const doPost=b=>fetch(`${API}/ahs/md-skills`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
1692
+ let r=await doPost(body);
1693
+ if(r.status===400){ const e=await r.json().catch(()=>({})); alert('解析失败: '+(e.detail||'SKILL.md/包 格式错')); return; }
1694
+ if(r.status===409){ const e=await r.json().catch(()=>({})); const w=(e.detail&&e.detail.warnings)||[];
1695
+ if(!confirm('⚠ 安装审核提示:\n'+w.join('\n')+'\n\n确认安装?')) return;
1696
+ r=await doPost({...body,acknowledged:true});
1697
+ }
1698
+ if(!r.ok){ const e=await r.json().catch(()=>({})); alert('安装失败: '+(JSON.stringify(e&&e.detail)||r.status)); return; }
1699
+ alert('✓ 指令技能已安装'); reload();
1700
+ };
1701
+ const sub=root.querySelector('#tcf-submit');
1702
+ if(sub) sub.onclick=async()=>{
1703
+ const g=s=>(root.querySelector('#tcf-'+s)||{}).value||'';
1704
+ const kind=g('kind');
1705
+ const body={id:g('id').trim(),name:g('name').trim()||g('id').trim(),description:'',kind,
1706
+ triggers:g('triggers').split(',').map(s=>s.trim()).filter(Boolean),side_effect:g('se')};
1707
+ if(!body.id){ alert('id 必填'); return; }
1708
+ if(kind==='web_api'){ body.endpoint=g('endpoint').trim(); }
1709
+ else{ body.mcp_server=g('endpoint').trim(); body.mcp_tool=g('tool').trim(); }
1710
+ const doPost=b=>fetch(`${API}/ahs/skills`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
1711
+ let r=await doPost(body);
1712
+ if(r.status===403){ const e=await r.json().catch(()=>({})); alert('❌ 安装被审核拦截:\n'+((e.detail&&e.detail.reasons)||['被拒绝']).join('\n')); return; }
1713
+ if(r.status===409){ const e=await r.json().catch(()=>({})); const w=(e.detail&&e.detail.warnings)||[];
1714
+ if(!confirm('⚠ 安装审核提示风险:\n'+w.join('\n')+'\n\n确认仍要安装?')) return;
1715
+ r=await doPost({...body, acknowledged:true});
1716
+ }
1717
+ if(!r.ok){ const e=await r.json().catch(()=>({})); alert('安装失败: '+(JSON.stringify(e.detail)||r.status)); return; }
1718
+ reload();
1719
+ };
1720
+ }
1721
+
1722
+ const ENTRY_FORMS={
1723
+ finance:{title:'录入交易',sub:'添加一笔收支记录',endpoint:'/finance/import',
1724
+ fields:[
1725
+ {k:'amount',label:'金额(支出填负数)',type:'number',ph:'-29.99'},
1726
+ {k:'category',label:'分类',type:'text',ph:'food / subscription / transport'},
1727
+ {k:'entity',label:'商户/对象',type:'text',ph:'星巴克'},
1728
+ {k:'account',label:'账户',type:'text',ph:'credit_card_1'},
1729
+ ],
1730
+ build:v=>({rows:[{amount:parseFloat(v.amount),category:v.category||'uncategorized',
1731
+ entity:v.entity||'unknown',account:v.account||'default'}]})},
1732
+ office:{title:'录入知识点',sub:'添加一条学习知识',endpoint:'/learning/import',
1733
+ fields:[
1734
+ {k:'content',label:'知识内容(支持多行,可整段粘贴)',type:'textarea',ph:'粘贴完整知识/教程,支持多行\n如:\n第一步 …\n第二步 …'},
1735
+ {k:'topic',label:'主题',type:'text',ph:'学习方法'},
1736
+ {k:'source',label:'来源',type:'text',ph:'《学习之道》'},
1737
+ {k:'tags',label:'标签(多个用 | 分隔)',type:'text',ph:'例:安装 | 教程 | 工具'},
1738
+ ],
1739
+ build:v=>({rows:[{content:v.content,topic:v.topic||'general',source:v.source||'',
1740
+ tags:(v.tags||'').split('|').map(s=>s.trim()).filter(Boolean)}]})},
1741
+ admin:{title:'录入行程点',sub:'添加一个行程地点',endpoint:'/itinerary/import',
1742
+ fields:[
1743
+ {k:'name',label:'地点名',type:'text',ph:'客户A会议'},
1744
+ {k:'address',label:'地址(配高德Key可自动定位)',type:'text',ph:'北京市朝阳区三里屯'},
1745
+ {k:'lng',label:'经度',type:'number',ph:'116.455'},
1746
+ {k:'lat',label:'纬度',type:'number',ph:'39.936'},
1747
+ {k:'start_time',label:'到达时间',type:'text',ph:'2026-06-10T10:00:00'},
1748
+ {k:'duration_min',label:'停留(分钟)',type:'number',ph:'60'},
1749
+ ],
1750
+ build:v=>({rows:[{name:v.name,address:v.address||'',
1751
+ lng:v.lng?parseFloat(v.lng):null,lat:v.lat?parseFloat(v.lat):null,
1752
+ start_time:v.start_time||null,duration_min:parseInt(v.duration_min||0),city:''}]})},
1753
+ };
1754
+ let _entryKind=null;
1755
+ function openEntry(kind){
1756
+ const f=ENTRY_FORMS[kind]; if(!f)return; _entryKind=kind;
1757
+ document.getElementById('entryTitle').textContent=f.title;
1758
+ document.getElementById('entrySub').textContent=f.sub;
1759
+ document.getElementById('entryMsg').textContent='';
1760
+ let h='';
1761
+ f.fields.forEach(fd=>{
1762
+ const ctl = fd.type==='textarea'
1763
+ ? `<textarea data-k="${fd.k}" placeholder="${fd.ph||''}" rows="8" style="width:100%;resize:vertical;font-family:inherit;line-height:1.6"></textarea>`
1764
+ : `<input data-k="${fd.k}" type="${fd.type}" placeholder="${fd.ph||''}">`;
1765
+ h+=`<div class="entry-field"><label>${fd.label}</label>${ctl}</div>`;
1766
+ });
1767
+ document.getElementById('entryFields').innerHTML=h;
1768
+ document.getElementById('entryMask').classList.add('show');
1769
+ }
1770
+ function closeEntry(){document.getElementById('entryMask').classList.remove('show');}
1771
+ document.getElementById('entryCancel')?.addEventListener('click',closeEntry);
1772
+ document.getElementById('entryMask')?.addEventListener('click',e=>{if(e.target.id==='entryMask')closeEntry();});
1773
+ document.getElementById('entrySubmit')?.addEventListener('click',async()=>{
1774
+ const f=ENTRY_FORMS[_entryKind]; if(!f)return;
1775
+ const v={}; document.querySelectorAll('#entryFields input, #entryFields textarea').forEach(i=>v[i.dataset.k]=i.value);
1776
+ const msg=document.getElementById('entryMsg');
1777
+ try{
1778
+ const body=f.build(v);
1779
+ const r=await fetch(`${API}${f.endpoint}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
1780
+ const d=await r.json();
1781
+ msg.style.color='var(--green)'; msg.textContent=`✓ 已录入 ${d.imported||0} 条`;
1782
+ setTimeout(()=>{closeEntry(); loadKernel(activeAgent); loadMemStats();},700);
1783
+ }catch(e){ msg.style.color='var(--amber)'; msg.textContent='录入失败:后端未连接'; }
1784
+ });
1785
+
1786
+ const _bootTry=(fn,name)=>{ try{ const r=fn(); if(r&&r.catch) r.catch(e=>{ try{document.getElementById('upStat').textContent='启动失败:'+name}catch(_){ } }); }
1787
+ catch(e){ try{document.getElementById('upStat').textContent='启动失败:'+name}catch(_){ } } };
1788
+ _bootTry(fetchAMapConfig, 'fetchAMapConfig');
1789
+ _bootTry(loadStatus, 'loadStatus');
1790
+ _bootTry(loadMemStats, 'loadMemStats');
1791
+ _bootTry(loadKnowList, 'loadKnowList');
1792
+ _bootTry(()=>connectWS(), 'connectWS');
1793
+ _bootTry(()=>loadKernel('supervisor'), 'loadKernel');
1794
+ try{ setInterval(loadStatus,15000); }catch(e){}
1795
+ try{ setInterval(loadMemStats,20000); }catch(e){}
1796
+ try{ setInterval(loadKnowList,30000); }catch(e){}
1797
+ document.getElementById('memMore')?.addEventListener('click',()=>showModuleInCore('memory'));
1798
+ document.getElementById('knowMore')?.addEventListener('click',()=>showKnowledgeFull());
1799
+ document.getElementById('ingFile')?.addEventListener('click',()=>document.getElementById('cmdAttach')?.click());
1800
+ document.getElementById('ingFolder')?.addEventListener('click',()=>document.getElementById('cmdFolder')?.click());
1801
+ document.getElementById('ingNote')?.addEventListener('click',()=>{ try{ openEntry('office'); }catch(e){} });
1802
+ _bootTry(loadSessions, 'loadSessions');
1803
+ document.getElementById('sessNew')?.addEventListener('click',()=>newSession(true));
1804
+
1805
+ Object.assign(window, { openEntry, importDomainFile, switchProvider });
1806
+