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,1436 @@
1
+
2
+ const KU_COLORS = {
3
+ Entity: '#002FA7', // 克莱因蓝
4
+ Concept: '#2c9678', // 绿
5
+ Fact: '#5a78cc', // 蓝浅
6
+ Claim: '#6bbfa6', // 绿浅
7
+ Evidence: '#001f70', // 蓝深
8
+ Ability: '#1f6e58', // 绿深
9
+ };
10
+ const KU_LABEL = {
11
+ Entity:'实体', Concept:'概念', Fact:'事实',
12
+ Claim:'观点', Evidence:'证据', Ability:'能力'
13
+ };
14
+
15
+ const DEMO = {
16
+ kus: {
17
+ ENT_01:{ku_id:'ENT_01',ku_type:'Entity',name:'Charlie Munger',summary:'伯克希尔·哈撒韦副董事长,价值投资与多元思维模型的代表人物。',domain:'Investment',confidence:1.0,status:'active',tags:['投资','心理学'],extra:{entity_type:'Person',aliases:['查理·芒格']}},
18
+ ENT_02:{ku_id:'ENT_02',ku_type:'Entity',name:'Warren Buffett',summary:'伯克希尔·哈撒韦董事长,被誉为奥马哈先知。',domain:'Investment',confidence:1.0,status:'active',tags:['投资'],extra:{entity_type:'Person',aliases:['巴菲特']}},
19
+ ENT_03:{ku_id:'ENT_03',ku_type:'Entity',name:'Benjamin Graham',summary:'价值投资之父,《证券分析》《聪明的投资者》作者。',domain:'Investment',confidence:1.0,status:'active',tags:['投资'],extra:{entity_type:'Person',aliases:['格雷厄姆']}},
20
+ CON_01:{ku_id:'CON_01',ku_type:'Concept',name:'价值投资',summary:'以低于内在价值的价格买入优质资产,长期持有。',domain:'Investment',confidence:1.0,status:'active',tags:['方法论'],extra:{definition:'寻找市场价格低于内在价值的证券'}},
21
+ CON_02:{ku_id:'CON_02',ku_type:'Concept',name:'多元思维模型',summary:'用多学科的核心模型交叉分析问题,避免单一视角偏差。',domain:'Investment',confidence:1.0,status:'active',tags:['思维'],extra:{definition:'Mental Models'}},
22
+ CON_03:{ku_id:'CON_03',ku_type:'Concept',name:'安全边际',summary:'买入价与内在价值之间的缓冲,价值投资的核心防御机制。',domain:'Investment',confidence:1.0,status:'active',tags:['估值'],extra:{definition:'Margin of Safety'}},
23
+ CON_04:{ku_id:'CON_04',ku_type:'Concept',name:'护城河',summary:'企业可持续的竞争优势,如品牌、网络效应、转换成本。',domain:'Investment',confidence:1.0,status:'active',tags:['竞争优势'],extra:{definition:'Economic Moat'}},
24
+ CLM_01:{ku_id:'CLM_01',ku_type:'Claim',name:'长期价值投资优于短线',summary:'',domain:'Investment',confidence:0.88,status:'active',tags:[],extra:{statement:'长期价值投资的回报显著优于短线交易',claim_status:'active',supports:['EVD_01','EVD_02']}},
25
+ CLM_02:{ku_id:'CLM_02',ku_type:'Claim',name:'集中投资优于分散',summary:'',domain:'Investment',confidence:0.71,status:'active',tags:[],extra:{statement:'对深度理解的少数标的集中投资优于广泛分散',claim_status:'disputed',supports:['EVD_02']}},
26
+ CLM_03:{ku_id:'CLM_03',ku_type:'Claim',name:'广泛分散降低风险',summary:'',domain:'Investment',confidence:0.69,status:'active',tags:[],extra:{statement:'广泛分散持仓是降低非系统性风险的最优策略',claim_status:'disputed',supports:['EVD_03']}},
27
+ EVD_01:{ku_id:'EVD_01',ku_type:'Evidence',name:'穷查理宝典',summary:'记录芒格投资哲学与思维方法的文集。',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{source:'Poor Charlie\'s Almanack',author:'Charlie Munger',year:2005,page:127}},
28
+ EVD_02:{ku_id:'EVD_02',ku_type:'Evidence',name:'巴菲特致股东信',summary:'1965年至今的伯克希尔年度股东信合集。',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{source:'Berkshire Shareholder Letters',author:'Warren Buffett',year:2023}},
29
+ EVD_03:{ku_id:'EVD_03',ku_type:'Evidence',name:'现代投资组合理论',summary:'马科维茨提出的均值-方差分散化框架。',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{source:'Portfolio Selection',author:'Harry Markowitz',year:1952}},
30
+ ABL_01:{ku_id:'ABL_01',ku_type:'Ability',name:'价值投资者',summary:'',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{ability_key:'value_investor'}},
31
+ },
32
+ relations:[
33
+ {from:'ENT_01',to:'CON_02',type:'CREATED_BY'},
34
+ {from:'ENT_01',to:'CON_01',type:'USES'},
35
+ {from:'ENT_02',to:'CON_01',type:'USES'},
36
+ {from:'ENT_03',to:'CON_01',type:'CREATED_BY'},
37
+ {from:'ENT_03',to:'CON_03',type:'CREATED_BY'},
38
+ {from:'CON_01',to:'CON_03',type:'DERIVED_FROM'},
39
+ {from:'CON_01',to:'CON_04',type:'DERIVED_FROM'},
40
+ {from:'EVD_01',to:'CLM_01',type:'SUPPORTS'},
41
+ {from:'EVD_02',to:'CLM_01',type:'SUPPORTS'},
42
+ {from:'EVD_02',to:'CLM_02',type:'SUPPORTS'},
43
+ {from:'EVD_03',to:'CLM_03',type:'SUPPORTS'},
44
+ {from:'CLM_02',to:'CLM_03',type:'CONTRADICTS'},
45
+ {from:'ABL_01',to:'CON_01',type:'USES'},
46
+ {from:'EVD_01',to:'ENT_01',type:'MENTIONS'},
47
+ ],
48
+ abilities:[
49
+ {ability_key:'value_investor',display_name:'价值投资者',domain:'Investment',
50
+ coverage:{fundamental_analysis:0.82,valuation:0.78,moat_analysis:0.71,financial_statement:0.55},
51
+ can_compile:true,compiled:true},
52
+ {ability_key:'quant_analyst',display_name:'量化分析师',domain:'Investment',
53
+ coverage:{factor_investing:0.45,backtesting:0.30,risk_management:0.62,portfolio_opt:0.51,data_analysis:0.20},
54
+ can_compile:false,missing:['factor_investing','backtesting','data_analysis']},
55
+ {ability_key:'psychology_expert',display_name:'心理学专家',domain:'Psychology',
56
+ coverage:{cognitive_bias:0.68,behavioral_psychology:0.40,social_influence:0.33,decision_making:0.71},
57
+ can_compile:false,missing:['behavioral_psychology','social_influence']},
58
+ ],
59
+ conflicts:[
60
+ {conflict_id:'CFT_01',claim_a_id:'CLM_02',claim_b_id:'CLM_03',domain:'Investment',
61
+ status:'open',resolution_strategy:'evidence_weight',resolution_note:''},
62
+ ],
63
+ };
64
+
65
+ class HKCClient{
66
+ constructor(){ this.base=null; this.demo=true; this.apiKey=''; }
67
+ async connect(url){
68
+ try{
69
+ const r=await fetch(url.replace(/\/$/,'')+'/health',{signal:AbortSignal.timeout(4000)});
70
+ if(!r.ok) throw 0;
71
+ this.base=url.replace(/\/$/,''); this.demo=false; return true;
72
+ }catch(e){ this.demo=true; this.base=null; return false; }
73
+ }
74
+ useDemo(){ this.demo=true; this.base=null; }
75
+ _h(extra){ const h=Object.assign({}, extra||{}); if(this.apiKey) h['X-API-Key']=this.apiKey; return h; }
76
+ async _get(p){ const r=await fetch(this.base+p,{headers:this._h()}); if(!r.ok) throw new Error(r.status); return r.json(); }
77
+ async _post(p,b){ const r=await fetch(this.base+p,{method:'POST',headers:this._h({'Content-Type':'application/json'}),body:JSON.stringify(b)});
78
+ if(!r.ok){ const e=await r.json().catch(()=>({})); throw {status:r.status,detail:e.detail||e.error||e.message}; } return r.json(); }
79
+ async _postForm(p,fd){ const r=await fetch(this.base+p,{method:'POST',headers:this._h(),body:fd}); // 不设 Content-Type,浏览器自动带 multipart boundary
80
+ if(!r.ok){ const e=await r.json().catch(()=>({})); throw {status:r.status,detail:e.detail||e.error||e.message}; } return r.json(); }
81
+ }
82
+ const client=new HKCClient();
83
+
84
+ const state={ kus:{}, relations:[], abilities:[], conflicts:[], selected:null, mode:'hybrid', conflictPick:{}, synth:{}, pathSel:{} };
85
+ const $=s=>document.querySelector(s);
86
+ const $$=s=>document.querySelectorAll(s);
87
+
88
+ function toast(msg,kind=''){
89
+ const t=document.createElement('div');
90
+ t.className='toast '+kind;
91
+ t.innerHTML=(kind==='ok'?'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6 9 17l-5-5"/></svg>':'')+`<span>${msg}</span>`;
92
+ $('#toast-wrap').appendChild(t);
93
+ setTimeout(()=>{t.style.opacity='0';t.style.transition='opacity .3s';setTimeout(()=>t.remove(),300)},2600);
94
+ }
95
+
96
+ async function loadData(){
97
+ if(client.demo){
98
+ state.kus=JSON.parse(JSON.stringify(DEMO.kus));
99
+ state.relations=DEMO.relations.map(r=>({...r}));
100
+ state.abilities=DEMO.abilities.map(a=>({...a}));
101
+ state.conflicts=DEMO.conflicts.map(c=>({...c}));
102
+ }else{
103
+ try{
104
+ const g=await client._get('/knowledge/graph?limit=5000'); // 拉到后端硬上限(默认仅 2000)
105
+ if(!(g.kus && g.kus.length)){
106
+ toast('知识库为空,先展示演示星球;摄入知识后自动显示真实数据','');
107
+ state.kus=JSON.parse(JSON.stringify(DEMO.kus));
108
+ state.relations=DEMO.relations.map(r=>({...r}));
109
+ state.abilities=DEMO.abilities.map(a=>({...a}));
110
+ state.conflicts=DEMO.conflicts.map(c=>({...c}));
111
+ renderAll(); return;
112
+ }
113
+ state.kus={};
114
+ (g.kus||[]).forEach(k=>{ state.kus[k.ku_id]=k; });
115
+ state.relations=(g.relations||[]).map(r=>({
116
+ from:r.from_ku, to:r.to_ku, type:r.rel_type||'RELATED',
117
+ }));
118
+ const ab=await client._get('/abilities').catch(()=>({abilities:[]}));
119
+ state.abilities=[];
120
+ for(const key of (ab.abilities||[])){
121
+ const rep=await client._get(`/abilities/${key}/coverage`).catch(()=>null);
122
+ if(rep) state.abilities.push({ability_key:key,display_name:rep.display_name||key,
123
+ domain:(rep.domains||[]).join(' / '),
124
+ coverage:rep.coverage,can_compile:rep.can_compile,missing:rep.missing_skills});
125
+ }
126
+ const cf=await client._get('/conflicts?status=open').catch(()=>({conflicts:[]}));
127
+ state.conflicts=cf.conflicts||[];
128
+ updateDataInfo().catch(()=>{});
129
+ }catch(e){ toast('数据加载出错,回退演示模式','err'); client.useDemo(); return loadData(); }
130
+ }
131
+ renderAll();
132
+ }
133
+
134
+ async function updateDataInfo(){
135
+ const pill=$('#conn-pill');
136
+ const st=await client._get('/stats');
137
+ const dir=st.data_dir||'(未知)';
138
+ const total=st.total_kus||0; // 全库真实总数
139
+ const emb=st.embedding||''; // 生效的向量后端
140
+ const shown=Object.keys(state.kus).length; // 星球实际渲染数(受 limit 限制)
141
+ const stubEmb=/stub/i.test(emb);
142
+ if(pill){
143
+ let tip=`数据目录:${dir}\n全库知识:${total} 个`;
144
+ if(shown<total) tip+=`\n星球显示:${shown} 个(受上限限制,未全部显示)`;
145
+ if(emb) tip+=`\n向量后端:${emb}${stubEmb?'(无真实语义,语义搜索受限;设 HKC_EMBEDDING=local 启用)':''}`;
146
+ pill.title=tip;
147
+ }
148
+ if(total===0){
149
+ toast(`⚠️ 知识库为空!后端数据目录:${dir}\n若你之前上传过书,很可能是后端连错了目录`,'err');
150
+ }else{
151
+ let msg=`已连接 · 全库 ${total} 个知识`;
152
+ if(shown<total) msg+=` · 星球显示 ${shown} 个(达上限)`;
153
+ toast(msg,'ok');
154
+ }
155
+ }
156
+
157
+ function renderAll(){ renderGraph(); renderPanels(); }
158
+ function renderPanels(){
159
+ renderAbilities(); renderConflicts(); renderLegend();
160
+ $('#ability-count').textContent=state.abilities.length+' 项';
161
+ const oc=state.conflicts.filter(c=>c.status==='open').length;
162
+ $('#conflict-count').textContent=oc;
163
+ }
164
+
165
+ let THREE = null; // 动态加载的 Three.js 命名空间
166
+ let planet = null; // 当前星球实例
167
+
168
+ function disputedOf(k){ return k.extra && k.extra.claim_status==='disputed'; }
169
+
170
+ function nodeRadius(k){
171
+ if(k.ku_type==='Ability') return 7;
172
+ if(k.ku_type==='Entity'||k.ku_type==='Concept') return 6;
173
+ if(k.ku_type==='Claim') return 3.5 + (k.confidence||0.5)*3;
174
+ return 5;
175
+ }
176
+
177
+ const PLANET_LAYOUT = {
178
+ SHELL_CAPACITY: 800, // 单层舒适容量;超过开始缩点,(未来)超过可分层
179
+ BASE_R_MIN: 160, BASE_R_MAX: 320,
180
+
181
+ compute(n){
182
+ const radius = Math.max(this.BASE_R_MIN, Math.min(this.BASE_R_MAX, 90 + n*11));
183
+ const dotScale = Math.max(0.45, Math.min(1, Math.sqrt(this.SHELL_CAPACITY / Math.max(1, n))));
184
+ const shells = 1; // 预留:未来 Math.ceil(n/SHELL_CAPACITY) 启用分层
185
+ const labelScale = 1; // 预留:未来书多时缩小书名标签
186
+ return { radius, dotScale, shells, labelScale };
187
+ },
188
+
189
+ position(i, n, params, T){
190
+ const golden = Math.PI * (3 - Math.sqrt(5));
191
+ const shells = params.shells || 1;
192
+ const per = Math.ceil(n / shells);
193
+ const shell = Math.floor(i / per);
194
+ const localI = i - shell * per;
195
+ const localN = Math.max(1, Math.min(per, n - shell * per));
196
+ const r = params.radius * (1 - shell * 0.32); // 内层半径递减
197
+ const y = 1 - (localI / Math.max(1, localN - 1)) * 2;
198
+ const rad = Math.sqrt(Math.max(0, 1 - y*y));
199
+ const theta = golden * localI;
200
+ return new T.Vector3(Math.cos(theta)*rad*r, y*r, Math.sin(theta)*rad*r);
201
+ },
202
+ };
203
+
204
+ class KnowledgePlanet {
205
+ constructor(container, THREE){
206
+ this.T = THREE;
207
+ this.container = container;
208
+ this.nodes = []; // {id, ku, mesh, glow, vel, pos, radius, label}
209
+ this.edges = []; // {from, to, line, contradicts}
210
+ this.raf = null;
211
+ this.disposed = false;
212
+ this.hovered = null;
213
+ this.selected = null;
214
+ this._initScene();
215
+ }
216
+
217
+ _initScene(){
218
+ const T = this.T;
219
+ const w = this.container.clientWidth || 700;
220
+ const h = this.container.clientHeight || 600;
221
+
222
+ this.scene = new T.Scene();
223
+
224
+ this.camera = new T.PerspectiveCamera(55, w/h, 1, 4000);
225
+ this.camera.position.set(0, 0, 720);
226
+
227
+ this.renderer = new T.WebGLRenderer({ antialias:true, alpha:true });
228
+ this.renderer.setSize(w, h);
229
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio||1, 2));
230
+ this.renderer.setClearColor(0x000000, 0); // 透明,露出 CSS 星空背景
231
+ this.container.appendChild(this.renderer.domElement);
232
+ this.canvas = this.renderer.domElement;
233
+ this.canvas.style.display = 'block';
234
+
235
+ this.scene.add(new T.AmbientLight(0xffffff, 0.75));
236
+ const l1 = new T.PointLight(0x9fe8d2, 0.9, 0); l1.position.set(200, 180, 300); this.scene.add(l1);
237
+ const l2 = new T.PointLight(0x7d97ff, 0.7, 0); l2.position.set(-220, -120, 160); this.scene.add(l2);
238
+
239
+ this.world = new T.Group();
240
+ this.scene.add(this.world);
241
+
242
+ this._initControls();
243
+ }
244
+
245
+ _initControls(){
246
+ this.rot = { x: 0, y: 0 }; // 目标旋转
247
+ this.rotCur = { x: 0, y: 0 }; // 当前(缓动)
248
+ this.autoSpin = true;
249
+ this.camDist = 720;
250
+ this.camDistTarget = 720;
251
+ let dragging = false, lastX = 0, lastY = 0;
252
+
253
+ const el = this.canvas;
254
+ el.style.cursor = 'grab';
255
+
256
+ this._onDown = (e)=>{ dragging = true; this.dragging = true; lastX = e.clientX; lastY = e.clientY; el.style.cursor='grabbing';
257
+ this._downX = e.clientX; this._downY = e.clientY; // 记录起点,用于区分"拖动"与"点击"
258
+ this.rot.x = this.rotCur.x; this.rot.y = this.rotCur.y; this.autoSpin = false;
259
+ this._flyTarget = null; };
260
+ this._onMove = (e)=>{
261
+ if(!dragging) return;
262
+ const dx = e.clientX - lastX, dy = e.clientY - lastY;
263
+ lastX = e.clientX; lastY = e.clientY;
264
+ this.rot.y += dx * 0.006;
265
+ this.rot.x += dy * 0.006; // 不再钳制俯仰:可自由翻转,把任意节点(含两极)拉到正面
266
+ };
267
+ this._onUp = ()=>{ dragging = false; this.dragging = false; el.style.cursor='grab'; };
268
+ this._onWheel = (e)=>{
269
+ e.preventDefault();
270
+ this.camDistTarget += e.deltaY * 0.5;
271
+ this.camDistTarget = Math.max(250, Math.min(1500, this.camDistTarget));
272
+ };
273
+
274
+ el.addEventListener('pointerdown', this._onDown);
275
+ window.addEventListener('pointermove', this._onMove);
276
+ window.addEventListener('pointerup', this._onUp);
277
+ el.addEventListener('wheel', this._onWheel, { passive:false });
278
+ }
279
+
280
+ build(kus, relations){
281
+ const T = this.T;
282
+ const ids = Object.keys(kus);
283
+ const n = ids.length;
284
+ if(!n) return;
285
+
286
+ const _h = s => {
287
+ let h = 2166136261 >>> 0;
288
+ for(let k=0;k<s.length;k++){ h ^= s.charCodeAt(k); h = Math.imul(h, 16777619); }
289
+ h ^= h >>> 13; h = Math.imul(h, 0x5bd1e995); h ^= h >>> 15;
290
+ return h >>> 0;
291
+ };
292
+ ids.sort((a,b)=> _h(a) - _h(b));
293
+
294
+ const params = PLANET_LAYOUT.compute(n);
295
+ this._layoutParams = params; // 供 _makeLabel 读取 labelScale 等
296
+
297
+ ids.forEach((id, i)=>{
298
+ const ku = kus[id];
299
+ const radius = nodeRadius(ku) * params.dotScale; // 圆点随节点总数自适应缩小
300
+ const pos = PLANET_LAYOUT.position(i, n, params, T);
301
+
302
+ const color = new T.Color(KU_COLORS[ku.ku_type] || '#888');
303
+ const geo = new T.SphereGeometry(radius, 24, 24);
304
+ const mat = new T.MeshStandardMaterial({
305
+ color, emissive: color.clone().multiplyScalar(0.35),
306
+ roughness: 0.45, metalness: 0.15,
307
+ });
308
+ const mesh = new T.Mesh(geo, mat);
309
+ mesh.position.copy(pos);
310
+ mesh.userData.id = id;
311
+ this.world.add(mesh);
312
+
313
+ const glowMat = new T.MeshBasicMaterial({ color, transparent:true, opacity:0.12, side:T.BackSide });
314
+ const glow = new T.Mesh(new T.SphereGeometry(radius*1.6, 20, 20), glowMat);
315
+ mesh.add(glow);
316
+
317
+ if(disputedOf(ku)){
318
+ const ring = new T.Mesh(
319
+ new T.SphereGeometry(radius*1.35, 20, 20),
320
+ new T.MeshBasicMaterial({ color:0xb0413e, transparent:true, opacity:0.3, side:T.BackSide })
321
+ );
322
+ mesh.add(ring);
323
+ }
324
+
325
+ this.nodes.push({
326
+ id, ku, mesh, glow, radius,
327
+ pos: pos.clone(),
328
+ vel: new T.Vector3(),
329
+ basePos: pos.clone(),
330
+ baseColor: color.clone(), // 复用,避免每帧 new T.Color
331
+ phase: Math.random()*Math.PI*2, // 漂浮相位
332
+ });
333
+
334
+ const label = this._makeLabel(ku.name);
335
+ label.position.set(0, -(radius+9), 0);
336
+ mesh.add(label);
337
+ const _nd = this.nodes[this.nodes.length-1];
338
+ _nd.label = label;
339
+ _nd.isBook = (ku.ku_type === 'Evidence');
340
+ _nd.deg = 0; // 连接数,建边后回填(作重要度)
341
+ _nd._revealLabel = false;
342
+ label.visible = _nd.isBook;
343
+ });
344
+
345
+ const nodeById = {};
346
+ this.nodes.forEach(nd=> nodeById[nd.id] = nd);
347
+ (relations||[]).forEach(r=>{
348
+ const a = nodeById[r.from], b = nodeById[r.to];
349
+ if(!a || !b) return;
350
+ const contradicts = r.type === 'CONTRADICTS';
351
+ const mat = new T.LineBasicMaterial({
352
+ color: contradicts ? 0xb0413e : 0x9aa3b8,
353
+ transparent:true, opacity: contradicts ? 0.55 : 0.22,
354
+ });
355
+ const geo = new T.BufferGeometry().setFromPoints([a.pos.clone(), b.pos.clone()]);
356
+ const line = new T.Line(geo, mat);
357
+ line.visible = false; // 默认不画边(大图会糊成一团);点中节点才显示它的连线
358
+ this.world.add(line);
359
+ this.edges.push({ from:a, to:b, line, contradicts });
360
+ });
361
+
362
+ this.edges.forEach(e=>{ e.from.deg++; e.to.deg++; });
363
+
364
+ this._buildPicking();
365
+ }
366
+
367
+ _buildPicking(){
368
+ const T = this.T;
369
+ this.raycaster = new T.Raycaster();
370
+ this.pointer = new T.Vector2();
371
+ this._meshes = this.nodes.map(n=>n.mesh);
372
+
373
+ this._onPointerMove = (e)=>{
374
+ const rect = this.canvas.getBoundingClientRect();
375
+ this.pointer.x = ((e.clientX - rect.left)/rect.width)*2 - 1;
376
+ this.pointer.y = -((e.clientY - rect.top)/rect.height)*2 + 1;
377
+ };
378
+ this._onClick = (e)=>{
379
+ const moved = Math.hypot(e.clientX - (this._downX||0), e.clientY - (this._downY||0));
380
+ if(moved > 6) return;
381
+ const hit = this._pick();
382
+ if(hit){ if(window.selectKU) window.selectKU(hit.id); }
383
+ else { if(window.clearSelection) window.clearSelection(); }
384
+ };
385
+ this.canvas.addEventListener('pointermove', this._onPointerMove);
386
+ this.canvas.addEventListener('click', this._onClick);
387
+ }
388
+
389
+ _pick(){
390
+ if(!this.raycaster) return null;
391
+ this.raycaster.setFromCamera(this.pointer, this.camera);
392
+ const hits = this.raycaster.intersectObjects(this._meshes, false);
393
+ if(hits.length){
394
+ const id = hits[0].object.userData.id;
395
+ return this.nodes.find(n=>n.id===id);
396
+ }
397
+ return null;
398
+ }
399
+
400
+ start(){
401
+ const T = this.T;
402
+ const tick = ()=>{
403
+ if(this.disposed) return;
404
+ this.raf = requestAnimationFrame(tick);
405
+ this._update();
406
+ this.renderer.render(this.scene, this.camera);
407
+ };
408
+ tick();
409
+ }
410
+
411
+ _update(){
412
+ const T = this.T;
413
+ const t = performance.now() * 0.001;
414
+
415
+ if(this.autoSpin) this.rot.y += 0.0011;
416
+
417
+ const tiltX = this.autoSpin ? Math.sin(t*0.045)*1.35 : this.rot.x;
418
+ const rotEase = this.dragging ? 0.85 : 0.08;
419
+ this.rotCur.x += (tiltX - this.rotCur.x) * rotEase;
420
+ this.rotCur.y += (this.rot.y - this.rotCur.y) * rotEase;
421
+ this.world.rotation.x = this.rotCur.x;
422
+ this.world.rotation.y = this.rotCur.y;
423
+
424
+ this.camDist += (this.camDistTarget - this.camDist) * 0.1;
425
+ this.camera.position.z = this.camDist;
426
+
427
+ if(this._flyTarget){
428
+ const nd = this._flyTarget;
429
+ const p = nd.pos;
430
+ const targetRotY = -Math.atan2(p.x, p.z);
431
+ const targetRotX = Math.atan2(p.y, Math.sqrt(p.x*p.x+p.z*p.z));
432
+ this.rot.y = targetRotY;
433
+ this.rot.x = targetRotX; // 不钳制:点击两极节点也能完整转到正面中心
434
+ }
435
+
436
+ const nodes = this.nodes;
437
+ nodes.forEach(nd=>{
438
+ let tx, ty, tz;
439
+ if(nd.focusPos){
440
+ tx = nd.focusPos.x; ty = nd.focusPos.y; tz = nd.focusPos.z;
441
+ }else{
442
+ const bx = Math.sin(t*0.5 + nd.phase)*1.0;
443
+ const by = Math.cos(t*0.4 + nd.phase)*1.0;
444
+ tx = nd.basePos.x + bx; ty = nd.basePos.y + by; tz = nd.basePos.z;
445
+ }
446
+ const cur = nd.mesh.position;
447
+ const ease = nd.focusPos ? 0.10 : 0.06;
448
+ cur.x += (tx - cur.x)*ease;
449
+ cur.y += (ty - cur.y)*ease;
450
+ cur.z += (tz - cur.z)*ease;
451
+ nd.pos.copy(cur);
452
+
453
+ const targetEmissive = nd.glowOn ? 1.0 : 0.35;
454
+ nd._emi = (nd._emi===undefined?0.35:nd._emi) + (targetEmissive - (nd._emi||0.35))*0.12;
455
+ if(nd.mesh.material && nd.mesh.material.emissive && nd.baseColor){
456
+ nd.mesh.material.emissive.copy(nd.baseColor).multiplyScalar(nd._emi);
457
+ }
458
+ const targetOpacity = (nd.dim ? 0.18 : 1.0);
459
+ nd._op = (nd._op===undefined?1:nd._op) + (targetOpacity - (nd._op||1))*0.12;
460
+ nd.mesh.material.opacity = nd._op;
461
+ nd.mesh.material.transparent = nd._op < 0.99;
462
+ if(nd.label){
463
+ const forced = (nd===this.hovered || nd.id===this.selected);
464
+ const show = forced || (this.selected ? nd._revealLabel : nd.isBook);
465
+ nd.label.visible = show;
466
+ if(show) nd.label.material.opacity = nd._op;
467
+ }
468
+ });
469
+
470
+ this.edges.forEach(e=>{
471
+ if(!e.line.visible) return;
472
+ const arr = e.line.geometry.attributes.position.array;
473
+ arr[0]=e.from.pos.x; arr[1]=e.from.pos.y; arr[2]=e.from.pos.z;
474
+ arr[3]=e.to.pos.x; arr[4]=e.to.pos.y; arr[5]=e.to.pos.z;
475
+ e.line.geometry.attributes.position.needsUpdate = true;
476
+ });
477
+
478
+ const hit = this._pick();
479
+ if(hit !== this.hovered){
480
+ if(this.hovered) this._setHover(this.hovered, false);
481
+ if(hit) this._setHover(hit, true);
482
+ this.hovered = hit;
483
+ this.canvas.style.cursor = hit ? 'pointer' : 'grab';
484
+ }
485
+ this.nodes.forEach(nd=>{
486
+ const target = (nd===this.hovered || nd.id===this.selected) ? 1.5 : 1.0;
487
+ const s = nd.mesh.scale.x + (target - nd.mesh.scale.x)*0.18;
488
+ nd.mesh.scale.set(s,s,s);
489
+ });
490
+ }
491
+
492
+ _setHover(nd, on){ /* 状态切换占位,scale 在 _update 平滑处理 */ }
493
+
494
+ _makeLabel(text){
495
+ const T = this.T;
496
+ const label = (text||'').length>12 ? text.slice(0,11)+'…' : (text||'');
497
+ const cv = document.createElement('canvas');
498
+ const ctx = cv.getContext('2d');
499
+ const fs = 48;
500
+ ctx.font = `600 ${fs}px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif`;
501
+ const w = Math.ceil(ctx.measureText(label).width) + 24;
502
+ cv.width = w; cv.height = fs+24;
503
+ ctx.font = `600 ${fs}px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif`;
504
+ ctx.textAlign='center'; ctx.textBaseline='middle';
505
+ ctx.fillStyle='rgba(255,255,255,.92)';
506
+ ctx.shadowColor='rgba(0,0,0,.85)'; ctx.shadowBlur=8;
507
+ ctx.fillText(label, cv.width/2, cv.height/2);
508
+ const tex = new T.CanvasTexture(cv);
509
+ tex.minFilter = T.LinearFilter;
510
+ const mat = new T.SpriteMaterial({ map:tex, transparent:true, depthWrite:false });
511
+ const sp = new T.Sprite(mat);
512
+ const scale = 0.22 * (this._layoutParams ? this._layoutParams.labelScale : 1); // 标签自适应缩放接入点
513
+ sp.scale.set(cv.width*scale, cv.height*scale, 1);
514
+ return sp;
515
+ }
516
+
517
+ flyTo(id){
518
+ const nd = this.nodes.find(n=>n.id===id);
519
+ if(!nd) return;
520
+ this.autoSpin = false;
521
+ this._flyTarget = nd;
522
+ this.camDistTarget = 380;
523
+ }
524
+
525
+ setSelected(id){
526
+ this.selected = id;
527
+ if(id) this.focusOn(id);
528
+ else this.clearFocus();
529
+ }
530
+
531
+ focusOn(id){
532
+ const T = this.T;
533
+ const center = this.nodes.find(n=>n.id===id);
534
+ if(!center) return;
535
+ this.autoSpin = false;
536
+ this._flyTarget = center;
537
+ this.camDistTarget = 420;
538
+
539
+ const relatedIds = new Set([id]);
540
+ this.edges.forEach(e=>{
541
+ const on = (e.from.id===id || e.to.id===id);
542
+ e.line.visible = on;
543
+ if(on){ relatedIds.add(e.from.id); relatedIds.add(e.to.id); }
544
+ });
545
+ const related = this.nodes.filter(n=>relatedIds.has(n.id) && n.id!==id);
546
+
547
+ this.nodes.forEach(n=> n._revealLabel=false);
548
+ related.slice().sort((a,b)=> (b.deg||0)-(a.deg||0)).slice(0, 6)
549
+ .forEach(n=> n._revealLabel=true);
550
+
551
+ center.focusPos = new T.Vector3(0, 0, 0);
552
+ center.glowOn = true; center.dim = false;
553
+
554
+ const R = 95; // 汇聚半径
555
+ const n = related.length;
556
+ related.forEach((nd, i)=>{
557
+ const ang = (i / Math.max(1,n)) * Math.PI*2;
558
+ const tilt = 0.5;
559
+ nd.focusPos = new T.Vector3(
560
+ Math.cos(ang)*R,
561
+ Math.sin(ang)*R*tilt,
562
+ Math.sin(ang)*R*0.8
563
+ );
564
+ nd.glowOn = true; nd.dim = false;
565
+ });
566
+
567
+ this.nodes.forEach(nd=>{
568
+ if(!relatedIds.has(nd.id)){
569
+ nd.focusPos = nd.basePos.clone().multiplyScalar(2.4); // 退远
570
+ nd.glowOn = false; nd.dim = true;
571
+ }
572
+ });
573
+ }
574
+
575
+ clearFocus(){
576
+ this._flyTarget = null;
577
+ this.camDistTarget = 720;
578
+ this.autoSpin = true;
579
+ this.nodes.forEach(nd=>{
580
+ nd.focusPos = null;
581
+ nd.glowOn = false;
582
+ nd.dim = false;
583
+ nd._revealLabel = false; // 回到默认态:只剩一级书名标签
584
+ });
585
+ this.edges.forEach(e=>{ e.line.visible = false; }); // 回到默认:不画边
586
+ }
587
+
588
+ highlight(ids){
589
+ const set = new Set(ids || []);
590
+ if(!set.size){ this.setSelected(null); return; } // 无命中→恢复默认视图
591
+ this.autoSpin = false;
592
+ this._flyTarget = null;
593
+ this.selected = '__search__'; // 让标签逻辑走"只显示 _revealLabel(命中项)"分支
594
+ this.nodes.forEach(nd=>{
595
+ const hit = set.has(nd.id);
596
+ nd.focusPos = null; // 原地高亮,不像点击那样汇聚
597
+ nd.glowOn = hit;
598
+ nd.dim = !hit;
599
+ nd._revealLabel = hit; // 命中项显示标签,方便看是哪些
600
+ });
601
+ }
602
+
603
+ resetView(){
604
+ this.rot = { x:0, y:0 }; this.autoSpin = true;
605
+ this.camDistTarget = 720;
606
+ }
607
+
608
+ resize(){
609
+ if(this.disposed) return;
610
+ const w = this.container.clientWidth, h = this.container.clientHeight;
611
+ this.camera.aspect = w/h; this.camera.updateProjectionMatrix();
612
+ this.renderer.setSize(w, h);
613
+ }
614
+
615
+ dispose(){
616
+ this.disposed = true;
617
+ if(this.raf) cancelAnimationFrame(this.raf);
618
+
619
+ try{
620
+ window.removeEventListener('pointermove', this._onMove);
621
+ window.removeEventListener('pointerup', this._onUp);
622
+ if(this.canvas){
623
+ this.canvas.removeEventListener('pointerdown', this._onDown);
624
+ this.canvas.removeEventListener('wheel', this._onWheel);
625
+ this.canvas.removeEventListener('pointermove', this._onPointerMove);
626
+ this.canvas.removeEventListener('click', this._onClick);
627
+ }
628
+ }catch(e){}
629
+
630
+ try{
631
+ this.scene.traverse(obj=>{
632
+ if(obj.geometry) obj.geometry.dispose();
633
+ if(obj.material){
634
+ const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
635
+ mats.forEach(m=>{
636
+ if(m.map) m.map.dispose(); // 纹理(label CanvasTexture)
637
+ if(m.dispose) m.dispose();
638
+ });
639
+ }
640
+ });
641
+ }catch(e){}
642
+
643
+ try{
644
+ this.renderer.dispose();
645
+ this.renderer.forceContextLoss();
646
+ if(this.canvas && this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas);
647
+ }catch(e){}
648
+
649
+ this.nodes = []; this.edges = []; this._meshes = null;
650
+ this.scene = null; this.renderer = null; this.camera = null; this.world = null;
651
+ }
652
+ }
653
+
654
+ async function ensureThree(){
655
+ if(THREE) return THREE;
656
+ if(window.THREE){ THREE = window.THREE; return THREE; }
657
+ if(window.__threeLoadError) return null;
658
+ return await new Promise(resolve=>{
659
+ let done = false;
660
+ const onReady = ()=>{ if(done) return; done=true; THREE = window.THREE; resolve(THREE); };
661
+ window.addEventListener('three-ready', onReady, { once:true });
662
+ const t0 = Date.now();
663
+ const poll = setInterval(()=>{
664
+ if(window.THREE){ clearInterval(poll); onReady(); }
665
+ else if(window.__threeLoadError || Date.now()-t0 > 5000){ clearInterval(poll); if(!done){ done=true; resolve(null); } }
666
+ }, 100);
667
+ });
668
+ }
669
+
670
+ async function renderGraph(){
671
+ const container = document.getElementById('graph-canvas');
672
+ if(!container) return;
673
+
674
+ if(planet){ try{ planet.dispose(); }catch(e){} planet = null; }
675
+ Array.from(container.querySelectorAll('canvas')).forEach(c=>c.remove());
676
+
677
+ const T = await ensureThree();
678
+ if(!T){
679
+ container.insertAdjacentHTML('beforeend',
680
+ '<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#8b909b;font-size:13px;text-align:center;padding:20px">3D 引擎需通过 http 访问<br>请用本地服务器打开(python -m http.server)</div>');
681
+ return;
682
+ }
683
+ if(!Object.keys(state.kus).length) return;
684
+
685
+ planet = new KnowledgePlanet(container, T);
686
+ planet.build(state.kus, state.relations);
687
+ if(state.selected) planet.setSelected(state.selected);
688
+ planet.start();
689
+ window.planet = planet;
690
+
691
+ window._fitGraph = ()=>{ if(planet) planet.resetView(); };
692
+ if(!window._planetResizeBound){
693
+ window.addEventListener('resize', ()=>{ if(planet) planet.resize(); });
694
+ window._planetResizeBound = true;
695
+ }
696
+ }
697
+
698
+ function startRotation(){
699
+ if(!planet) return;
700
+ if(planet.raf) return; // 已在跑(避免 start() 双重 RAF 嵌套)
701
+ planet.start(); // 复用现有 start() 启动 tick
702
+ }
703
+ function stopRotation(){
704
+ if(!planet) return;
705
+ if(planet.raf){
706
+ cancelAnimationFrame(planet.raf);
707
+ planet.raf = null;
708
+ }
709
+ }
710
+
711
+ function relLabel(t){
712
+ const m={CREATED_BY:'提出',SUPPORTS:'支持',CONTRADICTS:'冲突',MENTIONS:'提及',
713
+ BELONGS_TO:'属于',DERIVED_FROM:'衍生',IMPLEMENTS:'实现',USES:'运用',SUPERSEDES:'取代'};
714
+ return m[t]||t;
715
+ }
716
+
717
+ function renderLegend(){
718
+ const types=['Entity','Concept','Claim','Evidence','Ability'];
719
+ $('#legend').innerHTML=types.map(t=>
720
+ `<div class="lg-row"><span class="lg-dot" style="background:${KU_COLORS[t]}"></span>${KU_LABEL[t]} ${t}</div>`
721
+ ).join('')+`<div class="lg-row" style="border-top:1px solid var(--line-2);padding-top:6px;margin-top:1px">
722
+ <span class="lg-dot" style="background:none;border:2px dashed var(--alert);width:8px;height:8px"></span>观点冲突</div>`;
723
+ }
724
+
725
+ function selectKU(id){
726
+ if(!state.kus[id]) return;
727
+ state.selected=id;
728
+ if(planet){ try{ planet.setSelected(id); }catch(e){} }
729
+ renderWiki(state.kus[id]);
730
+ hydrateKU(id); // 原文段落等重字段不在星球初始负载里(为提速),点开时按需补全
731
+ loadSynthesis(id); // 若该节点已有缓存综述,自动显示(GET,不调 LLM)
732
+ }
733
+
734
+ async function loadSynthesis(id){
735
+ if(client.demo) return;
736
+ const cur=state.synth[id];
737
+ if(cur && cur.content) return; // 本会话已加载过
738
+ try{
739
+ const r=await client._get('/knowledge/ku/'+encodeURIComponent(id)+'/synthesis');
740
+ if(r && r.has_cache){
741
+ state.synth[id]={content:r.content,model:r.model,generated_at:r.generated_at,stale:false};
742
+ if(state.selected===id) renderWiki(state.kus[id]);
743
+ }
744
+ }catch(e){ /* 无缓存/出错都静默,维持"生成"按钮 */ }
745
+ }
746
+
747
+ async function genSynthesis(id, force){
748
+ if(client.demo){ toast('演示模式无法生成综述,请先连接后端','err'); return; }
749
+ state.synth[id]='loading';
750
+ if(state.selected===id) renderWiki(state.kus[id]);
751
+ try{
752
+ const r=await client._post('/knowledge/ku/'+encodeURIComponent(id)+'/synthesis'+(force?'?force=true':''), {});
753
+ state.synth[id]={content:r.content,model:r.model,generated_at:r.generated_at,stale:!!r.stale};
754
+ }catch(e){
755
+ state.synth[id]=null;
756
+ toast('综述生成失败:'+(e&&e.message?e.message:'请检查已连接后端并配置了 LLM'),'err');
757
+ }
758
+ if(state.selected===id) renderWiki(state.kus[id]);
759
+ }
760
+ window.genSynthesis=genSynthesis;
761
+
762
+ function setPathNode(id, which){
763
+ state.pathSel=state.pathSel||{};
764
+ state.pathSel[which]=id; state.pathSel.result='';
765
+ if(state.selected) renderWiki(state.kus[state.selected]);
766
+ }
767
+ window.setPathNode=setPathNode;
768
+ async function findPath(){
769
+ const ps=state.pathSel||{};
770
+ if(!ps.from||!ps.to){ toast('请先设起点①和终点②','err'); return; }
771
+ try{
772
+ const r=await client._get('/search/path?from_id='+encodeURIComponent(ps.from)+'&to_id='+encodeURIComponent(ps.to));
773
+ if(r.reachable && (r.path||[]).length){
774
+ const names=r.path.map(id=>(state.kus[id]&&state.kus[id].name)||id);
775
+ ps.result='最短路径('+r.hops+' 跳):'+names.map(esc).join(' → ');
776
+ if(window.planet){ try{ planet.highlight(r.path); }catch(e){} }
777
+ }else{ ps.result='两节点间无连通路径'; }
778
+ }catch(e){ ps.result='路径查询失败(需已连接后端)'; }
779
+ if(state.selected) renderWiki(state.kus[state.selected]);
780
+ }
781
+ window.findPath=findPath;
782
+
783
+ async function hydrateKU(id){
784
+ const ku=state.kus[id];
785
+ if(!ku || client.demo) return; // 演示数据自带原文,无需拉
786
+ const ex=ku.extra||{};
787
+ if(ex._hydrated || (ex.source_text&&ex.source_text.trim())) return;
788
+ try{
789
+ const full=await client._get('/knowledge/ku/'+encodeURIComponent(id));
790
+ ku.extra=Object.assign({}, ku.extra, full.extra||{}, {_hydrated:true});
791
+ if(state.selected===id) renderWiki(ku);
792
+ }catch(e){ /* 拉不到就维持精简视图,不报错 */ }
793
+ }
794
+ function clearSelection(){
795
+ state.selected=null;
796
+ if(planet){ try{ planet.setSelected(null); }catch(e){} }
797
+ $('#wiki-body').innerHTML=emptyWiki();
798
+ }
799
+ window.selectKU = selectKU;
800
+ window.clearSelection = clearSelection;
801
+
802
+ function toggleCollapse(id){ const el=document.getElementById(id); if(el) el.classList.toggle('collapsed'); }
803
+ window.toggleCollapse = toggleCollapse;
804
+ function emptyWiki(){
805
+ return `<div class="wiki-empty"><svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg><p>点击图谱中的节点<br>查看知识单元详情</p></div>`;
806
+ }
807
+
808
+ function renderWiki(ku){
809
+ if(!ku){ $('#wiki-body').innerHTML=emptyWiki(); return; }
810
+ const c=KU_COLORS[ku.ku_type];
811
+ const ex=ku.extra||{};
812
+ let html=`<div class="wiki-card">`;
813
+ html+=`<div class="wiki-type" style="color:${c};background:${c}1f">
814
+ <span class="lg-dot" style="background:${c};width:8px;height:8px"></span>${KU_LABEL[ku.ku_type]} · ${ku.ku_type}</div>`;
815
+ html+=`<div class="wiki-title">${esc(ku.name)}</div>`;
816
+ if(ku.domain) html+=`<div class="wiki-domain">${esc(ku.domain)}</div>`;
817
+
818
+ if(ku.ku_type==='Claim'){
819
+ if(ex.statement) html+=`<div class="wiki-summary">${esc(ex.statement)}</div>`;
820
+ const cs=ex.claim_status||'pending';
821
+ html+=`<div class="wiki-section"><div class="conf-row"><span style="font-size:12px;color:var(--ink-2);font-weight:600">置信度</span>
822
+ <span class="status-badge st-${cs}">${cs}</span></div>
823
+ <div class="conf-bar"><div class="conf-fill" style="width:${(ku.confidence*100).toFixed(0)}%"></div></div>
824
+ <div style="text-align:right;margin-top:4px"><span class="conf-val">${(ku.confidence*100).toFixed(0)}%</span></div></div>`;
825
+ } else if(ku.ku_type==='Fact'){
826
+ if(ex.statement) html+=`<div class="wiki-summary">${esc(ex.statement)}</div>`;
827
+ html+=`<div class="wiki-section"><span class="status-badge st-active">已验证事实 · 置信度 100%</span></div>`;
828
+ } else {
829
+ if(ku.summary) html+=`<div class="wiki-summary">${esc(ku.summary)}</div>`;
830
+ }
831
+
832
+ if(!client.demo){
833
+ const sy=state.synth[ku.ku_id];
834
+ html+=`<div class="wiki-section"><h4>AI 综述</h4>`;
835
+ if(sy==='loading'){
836
+ html+=`<div class="synth-loading">正在生成综述…(调用 LLM,请稍候)</div>`;
837
+ }else if(sy && sy.content){
838
+ html+=`<div class="synth-body">${esc(sy.content).replace(/\n/g,'<br>')}</div>`;
839
+ const meta=[sy.model?('模型 '+sy.model):'', sy.stale?'⚠ 知识已更新,建议重新生成':'派生只读视图'].filter(Boolean).join(' · ');
840
+ html+=`<div class="synth-meta">${esc(meta)}</div>
841
+ <button class="synth-btn ghost" onclick="genSynthesis('${ku.ku_id}',true)">重新生成</button>`;
842
+ }else{
843
+ html+=`<button class="synth-btn" onclick="genSynthesis('${ku.ku_id}',false)">✦ 生成 AI 综述</button>
844
+ <div class="synth-hint">把该节点的关联知识用 LLM 组织成连贯词条(只读,不写回知识库)</div>`;
845
+ }
846
+ html+=`</div>`;
847
+ }
848
+
849
+ if(!client.demo){
850
+ const ps=state.pathSel||{};
851
+ const nm=id=>id?esc(state.kus[id]&&state.kus[id].name||id):'(未设)';
852
+ html+=`<div class="wiki-section"><h4>路径查询</h4>
853
+ <div class="path-ctl">
854
+ <button class="synth-btn ghost" onclick="setPathNode('${ku.ku_id}','from')">设为起点①</button>
855
+ <button class="synth-btn ghost" onclick="setPathNode('${ku.ku_id}','to')">设为终点②</button>
856
+ </div>
857
+ <div class="path-meta">① ${nm(ps.from)} → ② ${nm(ps.to)}</div>`;
858
+ if(ps.from && ps.to) html+=`<button class="synth-btn" onclick="findPath()">查最短路径</button>`;
859
+ if(ps.result) html+=`<div class="path-result">${ps.result}</div>`;
860
+ html+=`</div>`;
861
+ }
862
+
863
+ if(ex.source_text && ex.source_text.trim()){
864
+ html+=`<div class="wiki-section"><h4>原文段落</h4><div class="wiki-source">${esc(ex.source_text)}</div></div>`;
865
+ }
866
+ if(ku.ku_type==='Entity'&&ex.aliases&&ex.aliases.length){
867
+ html+=`<div class="wiki-section"><h4>别名</h4><div class="tag-row">${ex.aliases.map(a=>`<span class="tag">${esc(a)}</span>`).join('')}</div></div>`;
868
+ }
869
+
870
+ const nb={Fact:[],Claim:[],Concept:[],Entity:[],Evidence:[]};
871
+ const seen=new Set([ku.ku_id]);
872
+ getRelations(ku.ku_id).forEach(r=>{
873
+ const o=state.kus[r.other];
874
+ if(!o || seen.has(o.ku_id)) return;
875
+ seen.add(o.ku_id);
876
+ if(nb[o.ku_type]) nb[o.ku_type].push(o);
877
+ });
878
+ if(ku.ku_type!=='Evidence'){
879
+ const names=[ku.name, ...((ex.aliases)||[])].filter(n=>n && n.length>=2);
880
+ if(names.length){
881
+ Object.values(state.kus).forEach(k=>{
882
+ if((k.ku_type==='Fact'||k.ku_type==='Claim') && !seen.has(k.ku_id)){
883
+ const st=(k.extra&&k.extra.statement)||k.summary||'';
884
+ if(names.some(n=>st.indexOf(n)>=0)){ seen.add(k.ku_id); nb[k.ku_type].push(k); }
885
+ }
886
+ });
887
+ }
888
+ }
889
+ const stmt=k=>(k.extra&&k.extra.statement)||k.summary||k.name;
890
+
891
+ if(ku.ku_type==='Evidence'){
892
+ const isColl=(ku.tags||[]).includes('_collection');
893
+ if(isColl){
894
+ const files=(state.relations||[]).filter(r=>r.type==='PART_OF'&&r.to===ku.ku_id).map(r=>state.kus[r.from]).filter(Boolean);
895
+ html+=`<div class="wiki-section"><h4>合集 · 来源</h4><div class="evid-item"><div class="evid-src">📁 ${esc(ku.name)}</div></div></div>`;
896
+ if(files.length){
897
+ html+=`<div class="wiki-section"><h4>包含文件/章节 ${files.length}(点击进入)</h4><div class="kw-row">`;
898
+ files.forEach(f=>{ const nm=f.name.length>18?f.name.slice(0,17)+'…':f.name; html+=`<span class="kw-chip" onclick="selectKU('${f.ku_id}')">📄 ${esc(nm)}</span>`; });
899
+ html+=`</div></div>`;
900
+ }
901
+ } else {
902
+ html+=`<div class="wiki-section"><h4>来源</h4><div class="evid-item">
903
+ <div class="evid-src">${esc(ex.source||ku.name)}</div>
904
+ <div class="evid-meta">${[ex.author,ex.year,ex.page?'p.'+ex.page:''].filter(Boolean).join(' · ')}</div></div></div>`;
905
+ const inCol=(ku.tags||[]).find(t=>t.startsWith('_incol:'));
906
+ if(inCol){ const col=state.kus[inCol.slice(7)];
907
+ if(col) html+=`<div class="wiki-section"><h4>所属合集</h4><div class="kw-row"><span class="kw-chip" onclick="selectKU('${col.ku_id}')">📁 ${esc(col.name)}</span></div></div>`;
908
+ }
909
+ const deg={}; (state.relations||[]).forEach(r=>{ deg[r.from]=(deg[r.from]||0)+1; deg[r.to]=(deg[r.to]||0)+1; });
910
+ const kws=[...nb.Concept,...nb.Entity,...nb.Fact,...nb.Claim].sort((a,b)=>(deg[b.ku_id]||0)-(deg[a.ku_id]||0));
911
+ if(kws.length){
912
+ html+=`<div class="wiki-section"><h4>本文件关键词 ${kws.length}(点击看词条/原文)</h4><div class="kw-row">`;
913
+ kws.slice(0,40).forEach(k=>{
914
+ const nm = k.name.length>16 ? k.name.slice(0,15)+'…' : k.name;
915
+ html+=`<span class="kw-chip" onclick="selectKU('${k.ku_id}')">${esc(nm)}</span>`;
916
+ });
917
+ html+=`</div></div>`;
918
+ }
919
+ }
920
+ } else {
921
+ if(nb.Fact.length){
922
+ let inner='';
923
+ nb.Fact.slice(0,50).forEach(k=>{ inner+=`<div class="wiki-item fact" onclick="selectKU('${k.ku_id}')">${esc(stmt(k))}</div>`; });
924
+ html+=`<div class="wiki-section"><h4>相关事实 ${nb.Fact.length}</h4><div class="wiki-scroll">${inner}</div></div>`;
925
+ }
926
+ if(nb.Claim.length){
927
+ let inner='';
928
+ nb.Claim.slice(0,50).forEach(k=>{
929
+ const conf=Math.round((k.confidence||0)*100);
930
+ inner+=`<div class="wiki-item claim" onclick="selectKU('${k.ku_id}')"><span class="wiki-conf">${conf}%</span>${esc(stmt(k))}</div>`;
931
+ });
932
+ html+=`<div class="wiki-section"><h4>相关观点 ${nb.Claim.length}</h4><div class="wiki-scroll">${inner}</div></div>`;
933
+ }
934
+ const linked=[...nb.Concept,...nb.Entity];
935
+ if(linked.length){
936
+ html+=`<div class="wiki-section"><h4>关联 ${linked.length}</h4>${kwCluster(linked)}</div>`;
937
+ }
938
+ if(nb.Evidence.length){
939
+ html+=`<div class="wiki-section"><h4>出自</h4><div class="kw-row">`;
940
+ nb.Evidence.forEach(k=>{ html+=`<span class="kw-chip" onclick="selectKU('${k.ku_id}')">📖 ${esc(k.name)}</span>`; });
941
+ html+=`</div></div>`;
942
+ }
943
+ }
944
+
945
+ if(ku.tags&&ku.tags.length){
946
+ html+=`<div class="wiki-section"><h4>标签</h4><div class="tag-row">${ku.tags.map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</div></div>`;
947
+ }
948
+
949
+ html+=`</div>`;
950
+ $('#wiki-body').innerHTML=html;
951
+ }
952
+
953
+ function kwCluster(list, capN){
954
+ const N=Math.min(list.length, capN||80);
955
+ const cx=130, cy=90, R=82, ga=Math.PI*(3-Math.sqrt(5));
956
+ let dots='';
957
+ for(let i=0;i<N;i++){
958
+ const k=list[i];
959
+ const rr=R*Math.sqrt((i+0.5)/N), th=i*ga;
960
+ const x=(cx+rr*Math.cos(th)).toFixed(1), y=(cy+rr*Math.sin(th)).toFixed(1);
961
+ const col=KU_COLORS[k.ku_type]||'#888';
962
+ dots+=`<circle cx="${x}" cy="${y}" r="4" fill="${col}" onclick="selectKU('${k.ku_id}')"><title>${esc(k.name)}</title></circle>`;
963
+ }
964
+ const more=list.length>N?`(显示 ${N}/${list.length})`:'';
965
+ return `<div class="kw-cluster"><svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet">${dots}</svg></div>`
966
+ + `<div class="kw-cluster-cap">悬停圆点看名称 · 点击进入${more}</div>`;
967
+ }
968
+
969
+ function getRelations(id){
970
+ const out=[];
971
+ state.relations.forEach(r=>{
972
+ if(r.from===id) out.push({other:r.to,type:r.type,dir:'out'});
973
+ else if(r.to===id) out.push({other:r.from,type:r.type,dir:'in'});
974
+ });
975
+ return out;
976
+ }
977
+
978
+ function renderAbilities(){
979
+ const body=$('#ability-body');
980
+ body.innerHTML=state.abilities.map(a=>{
981
+ const covRows=Object.entries(a.coverage).map(([k,v])=>{
982
+ const pct=(v*100).toFixed(0);
983
+ const low=v<0.5;
984
+ return `<div class="ac-cov-row">
985
+ <span class="ac-cov-label" title="${k}">${k}</span>
986
+ <span class="ac-cov-track"><span class="ac-cov-bar" style="width:${pct}%;${low?'background:var(--klein-l)':''}"></span></span>
987
+ <span class="ac-cov-pct" style="${low?'color:var(--ink-3)':''}">${pct}%</span></div>`;
988
+ }).join('');
989
+ const ready=a.can_compile;
990
+ return `<div class="ability-card">
991
+ <div class="ac-head">
992
+ <div><div class="ac-name">${esc(a.display_name)}</div><div class="ac-domain">${esc(a.domain||'')}</div></div>
993
+ <div class="ac-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></div>
994
+ </div>
995
+ <div class="ac-cov-list">${covRows}</div>
996
+ <div class="ac-foot">
997
+ <span class="ac-status ${ready?'ac-ready':'ac-locked'}">${ready?'✓ 可编译':'覆盖不足'}</span>
998
+ <button class="btn btn-sm btn-compile" ${ready?'':'disabled'} data-ability="${a.ability_key}">
999
+ ${a.compiled?'重新编译':'编译'}</button>
1000
+ </div>
1001
+ </div>`;
1002
+ }).join('')||'<div style="color:var(--ink-3);font-size:13px;padding:20px">暂无可编译能力</div>';
1003
+
1004
+ body.querySelectorAll('.btn-compile').forEach(b=>{
1005
+ b.onclick=()=>compileAbility(b.dataset.ability,b);
1006
+ });
1007
+ }
1008
+
1009
+ async function compileAbility(key,btn){
1010
+ const a=state.abilities.find(x=>x.ability_key===key);
1011
+ btn.disabled=true; btn.innerHTML='<span class="spinner"></span>';
1012
+ if(client.demo){
1013
+ await new Promise(r=>setTimeout(r,800));
1014
+ toast(`已编译能力包 ${a.display_name}.hkap`,'ok');
1015
+ btn.disabled=false; btn.textContent='重新编译'; a.compiled=true;
1016
+ return;
1017
+ }
1018
+ try{
1019
+ await client._post(`/abilities/${key}/compile`,{});
1020
+ toast(`已编译能力包 ${a.display_name}.hkap`,'ok');
1021
+ btn.disabled=false; btn.textContent='重新编译';
1022
+ }catch(e){
1023
+ if(e.status===422){
1024
+ const miss=(e.detail&&e.detail.missing_skills)||[];
1025
+ toast(`覆盖度不足,还缺:${miss.slice(0,3).join('、')}`,'err');
1026
+ }else toast('编译失败','err');
1027
+ btn.disabled=false; btn.textContent='编译';
1028
+ }
1029
+ }
1030
+
1031
+ function renderConflicts(){
1032
+ const open=state.conflicts.filter(c=>c.status==='open');
1033
+ const body=$('#conflict-body');
1034
+ if(!open.length){
1035
+ body.innerHTML=`<div class="conflict-empty">
1036
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="9"/></svg>
1037
+ <p style="font-size:12.5px">暂无待裁决冲突<br><span style="color:var(--ink-3);font-size:11px">知识库观点一致</span></p></div>`;
1038
+ return;
1039
+ }
1040
+ body.innerHTML=open.map(c=>{
1041
+ const a=state.kus[c.claim_a_id], b=state.kus[c.claim_b_id];
1042
+ if(!a||!b)return'';
1043
+ const pick=state.conflictPick[c.conflict_id];
1044
+ return `<div class="conflict-card">
1045
+ <div class="cc-head"><span class="cc-domain">${esc(c.domain||'')}</span>
1046
+ <span style="font-size:10px;color:var(--ink-3);font-weight:600;margin-left:auto">${c.resolution_strategy}</span></div>
1047
+ <div class="cc-claim ${pick===c.claim_a_id?'picked':''}" onclick="pickClaim('${c.conflict_id}','${c.claim_a_id}')">
1048
+ <div class="cc-claim-txt">${esc((a.extra&&a.extra.statement)||a.name)}</div>
1049
+ <div class="cc-claim-conf">置信度 ${(a.confidence*100).toFixed(0)}%</div></div>
1050
+ <div class="cc-vs">对立</div>
1051
+ <div class="cc-claim ${pick===c.claim_b_id?'picked':''}" onclick="pickClaim('${c.conflict_id}','${c.claim_b_id}')">
1052
+ <div class="cc-claim-txt">${esc((b.extra&&b.extra.statement)||b.name)}</div>
1053
+ <div class="cc-claim-conf">置信度 ${(b.confidence*100).toFixed(0)}%</div></div>
1054
+ <div class="cc-resolve">
1055
+ <button class="btn btn-sm btn-compile" ${pick?'':'disabled'} onclick="resolveConflict('${c.conflict_id}')">采纳所选观点</button>
1056
+ </div>
1057
+ </div>`;
1058
+ }).join('');
1059
+ }
1060
+
1061
+ function pickClaim(cid,clmid){
1062
+ state.conflictPick[cid]=clmid;
1063
+ renderConflicts();
1064
+ }
1065
+
1066
+ async function resolveConflict(cid){
1067
+ const winner=state.conflictPick[cid];
1068
+ if(!winner){ toast('请先选择采纳哪个观点','err'); return; }
1069
+ if(client.demo){
1070
+ await new Promise(r=>setTimeout(r,500));
1071
+ const c=state.conflicts.find(x=>x.conflict_id===cid);
1072
+ if(c) c.status='resolved';
1073
+ const loser=c.claim_a_id===winner?c.claim_b_id:c.claim_a_id;
1074
+ if(state.kus[loser]) state.kus[loser].extra.claim_status='superseded';
1075
+ if(state.kus[winner]) state.kus[winner].extra.claim_status='active';
1076
+ state.relations=state.relations.filter(r=>
1077
+ !(r.type==='CONTRADICTS' &&
1078
+ ((r.from===loser&&r.to===winner)||(r.from===winner&&r.to===loser))));
1079
+ delete state.conflictPick[cid];
1080
+ toast('冲突已裁决,落败观点标记为 superseded','ok');
1081
+ renderGraph();
1082
+ renderPanels();
1083
+ return;
1084
+ }
1085
+ try{
1086
+ await client._post(`/conflicts/${cid}/resolve`,{winner_id:winner,note:'UI 裁决',resolved_by:'ui'});
1087
+ toast('冲突已裁决','ok');
1088
+ delete state.conflictPick[cid];
1089
+ await loadData();
1090
+ }catch(e){ toast('裁决失败','err'); }
1091
+ }
1092
+
1093
+ let searchTimer;
1094
+ async function doSearch(q){
1095
+ if(!q.trim()){ if(window.planet) window.planet.setSelected(null); return; } // 清空搜索→恢复默认视图(不重建星球)
1096
+ if(client.demo){
1097
+ const hits=Object.values(state.kus).filter(k=>
1098
+ (k.name+k.summary+((k.extra&&k.extra.statement)||'')).toLowerCase().includes(q.toLowerCase()));
1099
+ highlightNodes(hits.map(h=>h.ku_id));
1100
+ if(hits.length) toast(`找到 ${hits.length} 个匹配(${state.mode})`);
1101
+ else toast('无匹配结果','err');
1102
+ return;
1103
+ }
1104
+ try{
1105
+ const res=await client._post('/search',{query:q,mode:state.mode,top_k:15});
1106
+ highlightNodes((res.hits||[]).map(h=>h.ku_id));
1107
+ toast(`找到 ${(res.hits||[]).length} 个结果`);
1108
+ }catch(e){ toast('搜索失败','err'); }
1109
+ }
1110
+ function highlightNodes(ids){
1111
+ if(window.planet) window.planet.highlight(ids || []);
1112
+ }
1113
+
1114
+ let pickedFiles = []; // 待摄入文件列表
1115
+
1116
+ function fmtSize(b){
1117
+ if(b<1024) return b+' B';
1118
+ if(b<1024*1024) return (b/1024).toFixed(1)+' KB';
1119
+ return (b/1024/1024).toFixed(1)+' MB';
1120
+ }
1121
+ const ALLOWED_EXT=['.pdf','.md','.markdown','.txt','.epub','.mobi','.azw3','.azw'];
1122
+ function extOK(name){ return ALLOWED_EXT.some(e=>name.toLowerCase().endsWith(e)); }
1123
+
1124
+ function addFiles(fileList){
1125
+ for(const f of fileList){
1126
+ if(!extOK(f.name)) continue; // 跳过不支持格式
1127
+ if(pickedFiles.some(x=>x.name===f.name && x.size===f.size)) continue; // 去重
1128
+ pickedFiles.push(f);
1129
+ }
1130
+ renderFileList();
1131
+ }
1132
+
1133
+ function renderFileList(){
1134
+ const box=$('#file-list');
1135
+ box.innerHTML='';
1136
+ pickedFiles.forEach((f,i)=>{
1137
+ const row=document.createElement('div');
1138
+ row.className='file-item'; row.dataset.idx=i;
1139
+ row.innerHTML=`<span class="fi-name">${f.name}</span>`+
1140
+ `<span class="fi-size">${fmtSize(f.size)}</span>`+
1141
+ `<button class="fi-del" title="移除"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M18 6 6 18M6 6l12 12"/></svg></button>`;
1142
+ row.querySelector('.fi-del').onclick=()=>{ pickedFiles.splice(i,1); renderFileList(); };
1143
+ box.appendChild(row);
1144
+ });
1145
+ }
1146
+
1147
+ let ingJobs = []; // {jobId,name,status,processed,total,ku,error}
1148
+ let _ingPoll = null;
1149
+
1150
+ function renderIngestStatus(){
1151
+ const el=$('#ingest-status'); if(!el) return;
1152
+ const active = ingJobs.filter(j=>j.status!=='done'&&j.status!=='error');
1153
+ if(!active.length){ el.innerHTML=''; return; }
1154
+ const cur = active[0];
1155
+ const pct = cur.total ? Math.round(cur.processed/cur.total*100) : 0;
1156
+ const more = active.length>1 ? ` (+${active.length-1} 个排队)` : '';
1157
+ el.innerHTML =
1158
+ `<div class="ing-line"><span class="spinner"></span> 后台摄入『${cur.name}』`
1159
+ + `${cur.total?`${cur.processed}/${cur.total} 段 ${pct}%`:'准备中…'}${more}</div>`
1160
+ + `<div class="ing-hint">已转入后台,可关闭本窗或继续添加文件;完成后星球自动刷新</div>`;
1161
+ }
1162
+
1163
+ function startIngestPoll(){
1164
+ if(_ingPoll) return;
1165
+ _ingPoll = setInterval(pollIngestJobs, 1200);
1166
+ }
1167
+ async function pollIngestJobs(){
1168
+ const pending = ingJobs.filter(j=>j.status!=='done'&&j.status!=='error');
1169
+ if(!pending.length){ clearInterval(_ingPoll); _ingPoll=null; renderIngestStatus(); return; }
1170
+ let anyDone=false;
1171
+ for(const j of pending){
1172
+ try{
1173
+ const s = await client._get(`/knowledge/ingest/jobs/${j.jobId}`);
1174
+ j.status=s.status; j.processed=s.processed; j.total=s.total; j.ku=s.ku_count; j.error=s.error;
1175
+ if(s.status==='done'){
1176
+ anyDone=true;
1177
+ if((s.ku_count||0) > 0){
1178
+ toast(`『${j.name}』摄入完成:${s.ku_count} 个知识单元`,'ok');
1179
+ }else{
1180
+ toast(`『${j.name}』已上传但未抽取到知识(0 个)。多半是 LLM API Key 无效/未配置——请到「连接设置」重填正确 key 后重传`,'err');
1181
+ }
1182
+ }
1183
+ else if(s.status==='error'){ toast(`『${j.name}』摄入失败:${(s.error||'').slice(0,60)}`,'err'); }
1184
+ }catch(e){ /* 网络抖动,下轮再试 */ }
1185
+ }
1186
+ renderIngestStatus();
1187
+ if(anyDone) loadData(); // 有完成的就刷新星球
1188
+ ingJobs = ingJobs.filter(j=> j.status!=='done' && j.status!=='error');
1189
+ if(!ingJobs.length){ clearInterval(_ingPoll); _ingPoll=null; renderIngestStatus(); }
1190
+ }
1191
+
1192
+ async function submitIngest(){
1193
+ const text=$('#ingest-text').value.trim();
1194
+ const url=$('#ingest-url').value.trim();
1195
+ const title=$('#ingest-title').value.trim();
1196
+ const domain=$('#ingest-domain').value;
1197
+ const hasFiles=pickedFiles.length>0;
1198
+
1199
+ if(!text && !url && !hasFiles){ toast('请输入文本、网址或选择文件','err'); return; }
1200
+
1201
+ const btn=$('#btn-ingest-submit');
1202
+ btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 摄入中';
1203
+
1204
+ if(client.demo){
1205
+ btn.disabled=false; btn.textContent='开始摄入';
1206
+ toast('未连接后端,无法摄入。请点左下「连接设置」连接 hkc-api 后再试','err');
1207
+ closeModal('#ingest-modal');
1208
+ openModal('#settings-modal');
1209
+ return;
1210
+ }
1211
+
1212
+ if(text){
1213
+ btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 摄入文本';
1214
+ try{
1215
+ const res=await client._post('/knowledge/ingest/text',{text,source_title:title,domain});
1216
+ const ku=res.ku_count||0;
1217
+ toast(ku>0?`文本摄入完成:${ku} 个知识单元`:'文本已摄入,但未抽取到知识(内容太短或模型未配置好)', ku>0?'ok':'err');
1218
+ $('#ingest-text').value='';
1219
+ if(ku>0) await loadData();
1220
+ }catch(e){ toast('文本摄入失败','err'); }
1221
+ btn.disabled=false; btn.textContent='开始摄入';
1222
+ }
1223
+
1224
+ if(url){
1225
+ btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 抓取网页';
1226
+ try{
1227
+ const res=await client._post('/knowledge/ingest/url',{url,source_title:title,domain});
1228
+ const ku=res.ku_count||0;
1229
+ toast(ku>0?`网页摄入完成:${ku} 个知识单元`:'网页已抓取,但未抽取到知识(正文太短/抓取失败/模型未配置)', ku>0?'ok':'err');
1230
+ $('#ingest-url').value='';
1231
+ if(ku>0) await loadData();
1232
+ }catch(e){
1233
+ const reason=(e&&e.detail)?String(e.detail).slice(0,60):'抓取或摄入失败';
1234
+ toast(`网页摄入失败:${reason}`,'err');
1235
+ }
1236
+ btn.disabled=false; btn.textContent='开始摄入';
1237
+ }
1238
+
1239
+ if(hasFiles){
1240
+ btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 提交中';
1241
+ let submitted=0;
1242
+ for(const f of pickedFiles.slice()){
1243
+ try{
1244
+ const fd=new FormData();
1245
+ fd.append('file', f, f.name);
1246
+ fd.append('source_title', title); // 留空时后端用文件名
1247
+ fd.append('domain', domain);
1248
+ const res=await client._postForm('/knowledge/ingest/file/async', fd);
1249
+ ingJobs.push({jobId:res.job_id, name:f.name, status:'queued', processed:0, total:0, ku:0, error:''});
1250
+ submitted++;
1251
+ }catch(e){
1252
+ const reason=(e&&e.detail)?String(e.detail).slice(0,50):'提交失败';
1253
+ toast(`『${f.name}』提交失败:${reason}`,'err');
1254
+ }
1255
+ }
1256
+ btn.disabled=false; btn.textContent='开始摄入';
1257
+ if(submitted>0){
1258
+ pickedFiles=[]; renderFileList(); // 已转后台,清空选择区
1259
+ $('#ingest-title').value='';
1260
+ toast(`${submitted} 个文件已转入后台摄入,可继续添加或关闭窗口;完成后自动刷新`,'ok');
1261
+ startIngestPoll();
1262
+ renderIngestStatus();
1263
+ }
1264
+ }
1265
+ }
1266
+
1267
+ function openModal(s){
1268
+ $(s).classList.add('show');
1269
+ if(s === '#ingest-modal') stopRotation();
1270
+ }
1271
+ function closeModal(s){
1272
+ $(s).classList.remove('show');
1273
+ if(s === '#ingest-modal') startRotation();
1274
+ }
1275
+
1276
+ async function openBooks(){
1277
+ if(client.demo){ toast('未连接后端,无法管理书籍','err'); openModal('#settings-modal'); return; }
1278
+ openModal('#books-modal');
1279
+ await loadBooks();
1280
+ }
1281
+ async function loadBooks(){
1282
+ const box=$('#books-list');
1283
+ box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">加载中…</div>';
1284
+ try{
1285
+ const r=await client._get('/knowledge/sources');
1286
+ const list=(r&&r.sources)||[];
1287
+ if(!list.length){ box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">暂无已摄入的书籍</div>'; return; }
1288
+ box.innerHTML='';
1289
+ for(const b of list){
1290
+ const row=document.createElement('div');
1291
+ row.style.cssText='display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;margin-bottom:8px;';
1292
+ const name=document.createElement('span');
1293
+ name.style.cssText='font-size:13px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;';
1294
+ name.textContent=(b.name||b.source)+(b.year?` (${b.year})`:'');
1295
+ name.title=b.source;
1296
+ const del=document.createElement('button');
1297
+ del.className='btn btn-ghost btn-sm';
1298
+ del.textContent='删除';
1299
+ del.onclick=()=>deleteBook(b.source, name.textContent);
1300
+ row.appendChild(name); row.appendChild(del);
1301
+ box.appendChild(row);
1302
+ }
1303
+ }catch(e){ box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">加载失败:'+(e.status||e||'')+'</div>'; }
1304
+ }
1305
+ async function deleteBook(source, label){
1306
+ if(!confirm(`确定删除「${label}」吗?\n\n将移除该书及其独占知识;与其他书共享的知识只去掉该书引用。此操作不可撤销。`)) return;
1307
+ try{
1308
+ const r=await client._post('/knowledge/sources/delete',{source});
1309
+ toast(`已删除「${label}」:${r.deleted_kus} 个知识单元`,'ok');
1310
+ await loadBooks();
1311
+ await loadData();
1312
+ }catch(e){ toast('删除失败:'+(e.detail||e.status||e||''),'err'); }
1313
+ }
1314
+
1315
+ function setConn(){
1316
+ const p=$('#conn-pill'), t=$('#conn-text');
1317
+ if(client.demo){ p.className='conn-pill demo'; t.textContent='演示模式'; }
1318
+ else{ p.className='conn-pill live'; t.textContent='已连接'; }
1319
+ }
1320
+
1321
+ const CONN_KEY='hkc_conn_v1';
1322
+ function _connSnapshot(){
1323
+ return {
1324
+ url:$('#api-url').value.trim(),
1325
+ apiKey:$('#api-key').value.trim(),
1326
+ provider:$('#llm-provider').value,
1327
+ key:$('#llm-key').value.trim(),
1328
+ model:$('#llm-model').value.trim(),
1329
+ baseUrl:$('#llm-baseurl').value.trim(),
1330
+ };
1331
+ }
1332
+ function saveConnSettings(){ try{ localStorage.setItem(CONN_KEY, JSON.stringify(_connSnapshot())); }catch(e){} }
1333
+ function loadConnSettings(){ try{ return JSON.parse(localStorage.getItem(CONN_KEY)||'null'); }catch(e){ return null; } }
1334
+ function fillConnFields(s){
1335
+ if(!s) return;
1336
+ if(s.url!=null) $('#api-url').value=s.url;
1337
+ if(s.apiKey!=null){ $('#api-key').value=s.apiKey; client.apiKey=s.apiKey; }
1338
+ if(s.provider) $('#llm-provider').value=s.provider;
1339
+ if(s.key!=null) $('#llm-key').value=s.key;
1340
+ if(s.model!=null) $('#llm-model').value=s.model;
1341
+ if(s.baseUrl!=null) $('#llm-baseurl').value=s.baseUrl;
1342
+ _syncLLMFields();
1343
+ }
1344
+
1345
+ async function tryConnect(){
1346
+ const url=$('#api-url').value.trim();
1347
+ if(!url){ toast('请输入 API 地址','err'); return; }
1348
+ client.apiKey=$('#api-key').value.trim(); // 鉴权 key 须在 /stats、/graph 等请求前就位
1349
+ const btn=$('#btn-connect'); btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 连接中';
1350
+ const ok=await client.connect(url);
1351
+ if(ok){
1352
+ const provider=$('#llm-provider').value;
1353
+ const key=$('#llm-key').value.trim();
1354
+ const model=$('#llm-model').value.trim();
1355
+ const baseUrl=$('#llm-baseurl').value.trim();
1356
+ if(key){
1357
+ try{
1358
+ await client._post('/config/llm', {
1359
+ provider, api_key:key,
1360
+ model: model||null,
1361
+ base_url: baseUrl||null,
1362
+ });
1363
+ toast(`已连接,知识抽取模型:${provider}`,'ok');
1364
+ }catch(e){
1365
+ toast('已连接,但模型配置失败:'+(e.detail||e.status||e),'err');
1366
+ }
1367
+ }else{
1368
+ toast('已连接到 HKC API','ok');
1369
+ }
1370
+ saveConnSettings(); // 连接成功才记住设置(含 key),供刷新后自动重连
1371
+ btn.disabled=false; btn.textContent='连接';
1372
+ setConn(); closeModal('#settings-modal'); await loadData();
1373
+ }else{
1374
+ btn.disabled=false; btn.textContent='连接';
1375
+ toast('连接失败,请检查 hkc-api 是否运行','err');
1376
+ }
1377
+ }
1378
+
1379
+ function _syncLLMFields(){
1380
+ const p=$('#llm-provider').value;
1381
+ const modelInput=$('#llm-model');
1382
+ const baseField=$('#llm-baseurl-field');
1383
+ const baseInput=$('#llm-baseurl');
1384
+ const defaults={ deepseek:'deepseek-chat', anthropic:'claude-sonnet-4-6', openai:'gpt-4o-mini', 'openai-compatible':'(自定义模型名)' };
1385
+ modelInput.placeholder=defaults[p]||'';
1386
+ baseField.style.display=(p==='openai-compatible')?'':'none';
1387
+ if(p==='openai-compatible' && !baseInput.value.trim()){
1388
+ baseInput.value='http://localhost:11434/v1';
1389
+ }
1390
+ }
1391
+
1392
+ function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m])); }
1393
+
1394
+ $('#modes').addEventListener('click',e=>{
1395
+ const b=e.target.closest('.mode-btn'); if(!b)return;
1396
+ $$('.mode-btn').forEach(x=>x.classList.remove('on'));
1397
+ b.classList.add('on'); state.mode=b.dataset.mode;
1398
+ });
1399
+ $('#search-input').addEventListener('input',e=>{
1400
+ clearTimeout(searchTimer);
1401
+ searchTimer=setTimeout(()=>doSearch(e.target.value),450);
1402
+ });
1403
+ $('#search-input').addEventListener('keydown',e=>{ if(e.key==='Enter') doSearch(e.target.value); });
1404
+
1405
+ $('#btn-ingest').onclick=()=>openModal('#ingest-modal');
1406
+ $('#btn-books').onclick=openBooks;
1407
+ $('#btn-settings').onclick=()=>openModal('#settings-modal');
1408
+ $('#btn-pick-files').onclick=()=>$('#ingest-files').click();
1409
+ $('#btn-pick-folder').onclick=()=>$('#ingest-folder').click();
1410
+ $('#ingest-files').addEventListener('change',e=>{ addFiles(e.target.files); e.target.value=''; });
1411
+ $('#ingest-folder').addEventListener('change',e=>{ addFiles(e.target.files); e.target.value=''; });
1412
+ $('#btn-ingest-submit').onclick=submitIngest;
1413
+ $('#btn-connect').onclick=tryConnect;
1414
+ $('#llm-provider').addEventListener('change', _syncLLMFields);
1415
+ _syncLLMFields(); // 初始化字段状态
1416
+ $('#btn-use-demo').onclick=()=>{ client.useDemo(); setConn(); closeModal('#settings-modal'); loadData(); toast('已切换到演示数据'); };
1417
+ $('#btn-fit').onclick=()=>window._fitGraph&&window._fitGraph();
1418
+ $$('[data-close]').forEach(b=>b.onclick=()=>{ closeModal('#ingest-modal'); closeModal('#settings-modal'); closeModal('#books-modal'); });
1419
+ $$('.modal-bg').forEach(m=>m.addEventListener('click',e=>{ if(e.target===m) closeModal('#'+m.id); }));
1420
+
1421
+ async function startup(){
1422
+ const saved=loadConnSettings();
1423
+ if(saved && saved.url){
1424
+ fillConnFields(saved);
1425
+ client.apiKey=saved.apiKey||''; // 鉴权 key 只能取保存的 apiKey,绝不混用模型的 key
1426
+ const ok=await client.connect(saved.url);
1427
+ if(ok){
1428
+ setConn(); await loadData(); return;
1429
+ }
1430
+ toast('已保存的后端未连上,回退演示模式(设置已回填,可在「连接设置」重连)','err');
1431
+ }
1432
+ if(await client.connect('')){ setConn(); await loadData(); return; }
1433
+ setConn();
1434
+ loadData();
1435
+ }
1436
+ startup();