foxclaw 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -1,6 +1,3 @@
1
- // ════════════════════════════════════════════════════════
2
- // HKC UI — 数据层:演示数据 + API 客户端(与 hkc_sdk 对齐)
3
- // ════════════════════════════════════════════════════════
4
1
 
5
2
  const KU_COLORS = {
6
3
  Entity: '#002FA7', // 克莱因蓝
@@ -15,7 +12,6 @@ const KU_LABEL = {
15
12
  Claim:'观点', Evidence:'证据', Ability:'能力'
16
13
  };
17
14
 
18
- // ── 演示数据:一个投资领域的小知识图谱 ──
19
15
  const DEMO = {
20
16
  kus: {
21
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:['查理·芒格']}},
@@ -66,7 +62,6 @@ const DEMO = {
66
62
  ],
67
63
  };
68
64
 
69
- // ── API 客户端:连真实 hkc-api,失败回退演示 ──
70
65
  class HKCClient{
71
66
  constructor(){ this.base=null; this.demo=true; this.apiKey=''; }
72
67
  async connect(url){
@@ -77,7 +72,6 @@ class HKCClient{
77
72
  }catch(e){ this.demo=true; this.base=null; return false; }
78
73
  }
79
74
  useDemo(){ this.demo=true; this.base=null; }
80
- // 后端若设了 HKC_API_KEY,前端在此带上 X-API-Key;未配置则为空、不影响开放部署。
81
75
  _h(extra){ const h=Object.assign({}, extra||{}); if(this.apiKey) h['X-API-Key']=this.apiKey; return h; }
82
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(); }
83
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)});
@@ -87,9 +81,6 @@ class HKCClient{
87
81
  }
88
82
  const client=new HKCClient();
89
83
 
90
- // ════════════════════════════════════════════════════════
91
- // 状态 + 工具
92
- // ════════════════════════════════════════════════════════
93
84
  const state={ kus:{}, relations:[], abilities:[], conflicts:[], selected:null, mode:'hybrid', conflictPick:{}, synth:{}, pathSel:{} };
94
85
  const $=s=>document.querySelector(s);
95
86
  const $$=s=>document.querySelectorAll(s);
@@ -102,7 +93,6 @@ function toast(msg,kind=''){
102
93
  setTimeout(()=>{t.style.opacity='0';t.style.transition='opacity .3s';setTimeout(()=>t.remove(),300)},2600);
103
94
  }
104
95
 
105
- // ── 载入数据(演示或真实)──
106
96
  async function loadData(){
107
97
  if(client.demo){
108
98
  state.kus=JSON.parse(JSON.stringify(DEMO.kus));
@@ -110,11 +100,8 @@ async function loadData(){
110
100
  state.abilities=DEMO.abilities.map(a=>({...a}));
111
101
  state.conflicts=DEMO.conflicts.map(c=>({...c}));
112
102
  }else{
113
- // 真实模式:一次拉取全量知识图谱(所有 KU + 关系),不再写死领域
114
103
  try{
115
104
  const g=await client._get('/knowledge/graph?limit=5000'); // 拉到后端硬上限(默认仅 2000)
116
- // 真实库为空 → 本次用演示数据渲染(星球不空白);**不翻 client.demo**,连接保留、摄入仍走真实后端,
117
- // 摄入知识后刷新即自动显示真实星球。修「嵌入 Foxclaw 空库时星球一片空白」。
118
105
  if(!(g.kus && g.kus.length)){
119
106
  toast('知识库为空,先展示演示星球;摄入知识后自动显示真实数据','');
120
107
  state.kus=JSON.parse(JSON.stringify(DEMO.kus));
@@ -136,19 +123,14 @@ async function loadData(){
136
123
  domain:(rep.domains||[]).join(' / '),
137
124
  coverage:rep.coverage,can_compile:rep.can_compile,missing:rep.missing_skills});
138
125
  }
139
- // 冲突
140
126
  const cf=await client._get('/conflicts?status=open').catch(()=>({conflicts:[]}));
141
127
  state.conflicts=cf.conflicts||[];
142
- // 数据可见性安全网:拉 /stats 拿真实 data_dir + 全库 KU 总数,
143
- // 显示到连接胶囊(hover)+ toast,空库时明显告警——防止"连错目录以为数据丢了"。
144
128
  updateDataInfo().catch(()=>{});
145
129
  }catch(e){ toast('数据加载出错,回退演示模式','err'); client.useDemo(); return loadData(); }
146
130
  }
147
131
  renderAll();
148
132
  }
149
133
 
150
- // 拉 /stats,把「后端实际数据目录 + 全库知识总数」暴露给用户。
151
- // 关键:total_kus 是全库真实数,可能 > 星球显示数(星球有 limit 上限)。
152
134
  async function updateDataInfo(){
153
135
  const pill=$('#conn-pill');
154
136
  const st=await client._get('/stats');
@@ -180,19 +162,11 @@ function renderPanels(){
180
162
  $('#conflict-count').textContent=oc;
181
163
  }
182
164
 
183
- // ════════════════════════════════════════════════════════
184
- // G6 知识图谱(本地 vendor/g6.min.js,零网络)
185
- // ════════════════════════════════════════════════════════
186
- // ════════════════════════════════════════════════════════
187
- // 知识星球 3D 引擎(Three.js,本地 vendor,零网络)
188
- // 节点 = 3D 球体浮于空间,语义引力聚拢,缓慢有机漂浮
189
- // ════════════════════════════════════════════════════════
190
165
  let THREE = null; // 动态加载的 Three.js 命名空间
191
166
  let planet = null; // 当前星球实例
192
167
 
193
168
  function disputedOf(k){ return k.extra && k.extra.claim_status==='disputed'; }
194
169
 
195
- // 节点半径(3D 世界单位)
196
170
  function nodeRadius(k){
197
171
  if(k.ku_type==='Ability') return 7;
198
172
  if(k.ku_type==='Entity'||k.ku_type==='Concept') return 6;
@@ -200,20 +174,10 @@ function nodeRadius(k){
200
174
  return 5;
201
175
  }
202
176
 
203
- // ── 星球布局策略(伸缩的统一接入点)────────────────────────────
204
- // 所有"节点/标签太多放不下怎么办"的逻辑都集中在此,后续扩展只改这个对象,
205
- // 不动其它渲染代码:
206
- // 已实现 · 单层 Fibonacci 球面 + 圆点随节点总数自适应缩小(dotScale)。
207
- // 预留接入点 ↓
208
- // · shells>1 → 分层多壳星球(外壳放不下→往里再套一层球);position() 已按
209
- // shell 分配、内层半径递减,只需让 compute() 返回 shells>1 即启用。
210
- // · labelScale → 书名标签随书数自适应缩小(_makeLabel 已乘该系数)。
211
- // · SHELL_CAPACITY→ 单层舒适容量阈值,决定何时缩点 / 何时该分层。
212
177
  const PLANET_LAYOUT = {
213
178
  SHELL_CAPACITY: 800, // 单层舒适容量;超过开始缩点,(未来)超过可分层
214
179
  BASE_R_MIN: 160, BASE_R_MAX: 320,
215
180
 
216
- // 按节点总数计算布局参数(集中调参)
217
181
  compute(n){
218
182
  const radius = Math.max(this.BASE_R_MIN, Math.min(this.BASE_R_MAX, 90 + n*11));
219
183
  const dotScale = Math.max(0.45, Math.min(1, Math.sqrt(this.SHELL_CAPACITY / Math.max(1, n))));
@@ -222,8 +186,6 @@ const PLANET_LAYOUT = {
222
186
  return { radius, dotScale, shells, labelScale };
223
187
  },
224
188
 
225
- // 第 i 个节点(共 n 个)的球面坐标。已支持多壳:shells>1 时按层分配、内层半径递减;
226
- // shells=1 时退化为单层 Fibonacci,布局与旧版完全一致。
227
189
  position(i, n, params, T){
228
190
  const golden = Math.PI * (3 - Math.sqrt(5));
229
191
  const shells = params.shells || 1;
@@ -239,10 +201,6 @@ const PLANET_LAYOUT = {
239
201
  },
240
202
  };
241
203
 
242
- // 关系标签
243
-
244
-
245
- // ── 知识星球类 ──
246
204
  class KnowledgePlanet {
247
205
  constructor(container, THREE){
248
206
  this.T = THREE;
@@ -263,11 +221,9 @@ class KnowledgePlanet {
263
221
 
264
222
  this.scene = new T.Scene();
265
223
 
266
- // 相机
267
224
  this.camera = new T.PerspectiveCamera(55, w/h, 1, 4000);
268
225
  this.camera.position.set(0, 0, 720);
269
226
 
270
- // 渲染器
271
227
  this.renderer = new T.WebGLRenderer({ antialias:true, alpha:true });
272
228
  this.renderer.setSize(w, h);
273
229
  this.renderer.setPixelRatio(Math.min(window.devicePixelRatio||1, 2));
@@ -276,20 +232,16 @@ class KnowledgePlanet {
276
232
  this.canvas = this.renderer.domElement;
277
233
  this.canvas.style.display = 'block';
278
234
 
279
- // 灯光:环境光 + 两个点光(冷暖对冲,呼应品牌绿/蓝)
280
235
  this.scene.add(new T.AmbientLight(0xffffff, 0.75));
281
236
  const l1 = new T.PointLight(0x9fe8d2, 0.9, 0); l1.position.set(200, 180, 300); this.scene.add(l1);
282
237
  const l2 = new T.PointLight(0x7d97ff, 0.7, 0); l2.position.set(-220, -120, 160); this.scene.add(l2);
283
238
 
284
- // 旋转容器:所有节点挂在这个 group 上,整体缓缓自转
285
239
  this.world = new T.Group();
286
240
  this.scene.add(this.world);
287
241
 
288
- // 轨道控制(自写:拖拽转视角 + 滚轮缩放)
289
242
  this._initControls();
290
243
  }
291
244
 
292
- // 轻量轨道控制
293
245
  _initControls(){
294
246
  this.rot = { x: 0, y: 0 }; // 目标旋转
295
247
  this.rotCur = { x: 0, y: 0 }; // 当前(缓动)
@@ -303,9 +255,7 @@ class KnowledgePlanet {
303
255
 
304
256
  this._onDown = (e)=>{ dragging = true; this.dragging = true; lastX = e.clientX; lastY = e.clientY; el.style.cursor='grabbing';
305
257
  this._downX = e.clientX; this._downY = e.clientY; // 记录起点,用于区分"拖动"与"点击"
306
- // 接管当前视觉朝向再停自转,避免从摆动倾斜切到手动时跳变
307
258
  this.rot.x = this.rotCur.x; this.rot.y = this.rotCur.y; this.autoSpin = false;
308
- // 关键:清掉 flyTarget,否则聚焦态每帧把旋转拉回目标节点朝向 → 拖动被牵扯、松手即反弹
309
259
  this._flyTarget = null; };
310
260
  this._onMove = (e)=>{
311
261
  if(!dragging) return;
@@ -327,18 +277,12 @@ class KnowledgePlanet {
327
277
  el.addEventListener('wheel', this._onWheel, { passive:false });
328
278
  }
329
279
 
330
- // 构建节点与边
331
280
  build(kus, relations){
332
281
  const T = this.T;
333
282
  const ids = Object.keys(kus);
334
283
  const n = ids.length;
335
284
  if(!n) return;
336
285
 
337
- // 稳定打散:后端按类型分组返回 KU,直接按序号布 Fibonacci 会把同类型节点
338
- // (尤其只有几个的"书"/Evidence)全挤到同一极点、标签叠成一坨。按 id 哈希
339
- // 稳定排序,让各类型均匀铺满球面(布局跨刷新稳定,不随机跳)。
340
- // 必须用有雪崩效应的哈希(FNV-1a+混合):id 是 EVD_0000000X 这类共享前缀、
341
- // 仅末位不同的序列,简单滚动哈希会给相近值 → 同前缀仍聚堆,起不到打散作用。
342
286
  const _h = s => {
343
287
  let h = 2166136261 >>> 0;
344
288
  for(let k=0;k<s.length;k++){ h ^= s.charCodeAt(k); h = Math.imul(h, 16777619); }
@@ -347,7 +291,6 @@ class KnowledgePlanet {
347
291
  };
348
292
  ids.sort((a,b)=> _h(a) - _h(b));
349
293
 
350
- // 布局参数(半径/圆点缩放/层数/标签缩放)统一由 PLANET_LAYOUT 计算,便于扩展
351
294
  const params = PLANET_LAYOUT.compute(n);
352
295
  this._layoutParams = params; // 供 _makeLabel 读取 labelScale 等
353
296
 
@@ -356,7 +299,6 @@ class KnowledgePlanet {
356
299
  const radius = nodeRadius(ku) * params.dotScale; // 圆点随节点总数自适应缩小
357
300
  const pos = PLANET_LAYOUT.position(i, n, params, T);
358
301
 
359
- // 球体
360
302
  const color = new T.Color(KU_COLORS[ku.ku_type] || '#888');
361
303
  const geo = new T.SphereGeometry(radius, 24, 24);
362
304
  const mat = new T.MeshStandardMaterial({
@@ -368,12 +310,10 @@ class KnowledgePlanet {
368
310
  mesh.userData.id = id;
369
311
  this.world.add(mesh);
370
312
 
371
- // 外发光(稍大的半透明球)
372
313
  const glowMat = new T.MeshBasicMaterial({ color, transparent:true, opacity:0.12, side:T.BackSide });
373
314
  const glow = new T.Mesh(new T.SphereGeometry(radius*1.6, 20, 20), glowMat);
374
315
  mesh.add(glow);
375
316
 
376
- // 冲突节点:暗红描边光环
377
317
  if(disputedOf(ku)){
378
318
  const ring = new T.Mesh(
379
319
  new T.SphereGeometry(radius*1.35, 20, 20),
@@ -391,23 +331,17 @@ class KnowledgePlanet {
391
331
  phase: Math.random()*Math.PI*2, // 漂浮相位
392
332
  });
393
333
 
394
- // 文字标签(Sprite,始终面向相机)
395
334
  const label = this._makeLabel(ku.name);
396
335
  label.position.set(0, -(radius+9), 0);
397
336
  mesh.add(label);
398
337
  const _nd = this.nodes[this.nodes.length-1];
399
338
  _nd.label = label;
400
- // 分级标签(按重要度钻取):
401
- // 一级 = 书/来源(Evidence),默认在球面常显书名;
402
- // 二级/三级 = 点击后按连接数(degree)展开该节点 top-N 重要邻居标签。
403
- // 这样几十上百本默认只见几十个书名,清爽且可层层下钻。
404
339
  _nd.isBook = (ku.ku_type === 'Evidence');
405
340
  _nd.deg = 0; // 连接数,建边后回填(作重要度)
406
341
  _nd._revealLabel = false;
407
342
  label.visible = _nd.isBook;
408
343
  });
409
344
 
410
- // 边(连线)
411
345
  const nodeById = {};
412
346
  this.nodes.forEach(nd=> nodeById[nd.id] = nd);
413
347
  (relations||[]).forEach(r=>{
@@ -425,13 +359,11 @@ class KnowledgePlanet {
425
359
  this.edges.push({ from:a, to:b, line, contradicts });
426
360
  });
427
361
 
428
- // 连接数(degree)=重要度,用于钻取时挑 top-N 邻居标签
429
362
  this.edges.forEach(e=>{ e.from.deg++; e.to.deg++; });
430
363
 
431
364
  this._buildPicking();
432
365
  }
433
366
 
434
- // 射线拾取(hover / click)
435
367
  _buildPicking(){
436
368
  const T = this.T;
437
369
  this.raycaster = new T.Raycaster();
@@ -444,8 +376,6 @@ class KnowledgePlanet {
444
376
  this.pointer.y = -((e.clientY - rect.top)/rect.height)*2 + 1;
445
377
  };
446
378
  this._onClick = (e)=>{
447
- // 拖动松手也会带出一次 click;若指针移动过阈值,判定为拖动而非点击 → 忽略,
448
- // 否则空白处会 clearSelection 重开自转、有节点处会 selectKU 飞入,都表现为"反弹"。
449
379
  const moved = Math.hypot(e.clientX - (this._downX||0), e.clientY - (this._downY||0));
450
380
  if(moved > 6) return;
451
381
  const hit = this._pick();
@@ -467,7 +397,6 @@ class KnowledgePlanet {
467
397
  return null;
468
398
  }
469
399
 
470
- // 动画循环
471
400
  start(){
472
401
  const T = this.T;
473
402
  const tick = ()=>{
@@ -483,28 +412,20 @@ class KnowledgePlanet {
483
412
  const T = this.T;
484
413
  const t = performance.now() * 0.001;
485
414
 
486
- // 自动自转(绕 Y 轴)——静默时缓慢转动(约 100s/圈)
487
415
  if(this.autoSpin) this.rot.y += 0.0011;
488
416
 
489
- // 旋转缓动;自转时让倾斜角缓慢大幅度上下摆动(±~77°),使两极也周期性
490
- // 转到正面,避免单轴自转把两极节点永久"沉"在顶/底而无法点击
491
- // (即用户要的"无固定方向转动、无重力沉淀")。
492
417
  const tiltX = this.autoSpin ? Math.sin(t*0.045)*1.35 : this.rot.x;
493
- // 拖动时近乎 1:1 跟手,消除"橡皮筋/反馈力牵扯"感;静默时才平滑缓动。
494
418
  const rotEase = this.dragging ? 0.85 : 0.08;
495
419
  this.rotCur.x += (tiltX - this.rotCur.x) * rotEase;
496
420
  this.rotCur.y += (this.rot.y - this.rotCur.y) * rotEase;
497
421
  this.world.rotation.x = this.rotCur.x;
498
422
  this.world.rotation.y = this.rotCur.y;
499
423
 
500
- // 相机距离缓动
501
424
  this.camDist += (this.camDistTarget - this.camDist) * 0.1;
502
425
  this.camera.position.z = this.camDist;
503
426
 
504
- // 飞入:把选中节点缓缓转到正面中心
505
427
  if(this._flyTarget){
506
428
  const nd = this._flyTarget;
507
- // 期望旋转:让节点world坐标朝向 +Z(正对相机)
508
429
  const p = nd.pos;
509
430
  const targetRotY = -Math.atan2(p.x, p.z);
510
431
  const targetRotX = Math.atan2(p.y, Math.sqrt(p.x*p.x+p.z*p.z));
@@ -512,12 +433,8 @@ class KnowledgePlanet {
512
433
  this.rot.x = targetRotX; // 不钳制:点击两极节点也能完整转到正面中心
513
434
  }
514
435
 
515
- // ── 球面分布 + 聚焦汇聚 ──
516
- // 正常态:节点固定在球面 + 轻微呼吸,整体自转
517
- // 聚焦态:点击某节点,相关节点汇聚到它周围并发光,无关节点淡出退远
518
436
  const nodes = this.nodes;
519
437
  nodes.forEach(nd=>{
520
- // 目标位置:有 focusPos(聚焦汇聚)用它,否则回球面 basePos
521
438
  let tx, ty, tz;
522
439
  if(nd.focusPos){
523
440
  tx = nd.focusPos.x; ty = nd.focusPos.y; tz = nd.focusPos.z;
@@ -526,7 +443,6 @@ class KnowledgePlanet {
526
443
  const by = Math.cos(t*0.4 + nd.phase)*1.0;
527
444
  tx = nd.basePos.x + bx; ty = nd.basePos.y + by; tz = nd.basePos.z;
528
445
  }
529
- // 平滑移动到目标(汇聚/回归动画)
530
446
  const cur = nd.mesh.position;
531
447
  const ease = nd.focusPos ? 0.10 : 0.06;
532
448
  cur.x += (tx - cur.x)*ease;
@@ -534,33 +450,23 @@ class KnowledgePlanet {
534
450
  cur.z += (tz - cur.z)*ease;
535
451
  nd.pos.copy(cur);
536
452
 
537
- // 发光过渡:聚焦的相关节点发光增强,淡出的减弱
538
453
  const targetEmissive = nd.glowOn ? 1.0 : 0.35;
539
454
  nd._emi = (nd._emi===undefined?0.35:nd._emi) + (targetEmissive - (nd._emi||0.35))*0.12;
540
455
  if(nd.mesh.material && nd.mesh.material.emissive && nd.baseColor){
541
456
  nd.mesh.material.emissive.copy(nd.baseColor).multiplyScalar(nd._emi);
542
457
  }
543
- // 淡出:无关节点透明度降低
544
458
  const targetOpacity = (nd.dim ? 0.18 : 1.0);
545
459
  nd._op = (nd._op===undefined?1:nd._op) + (targetOpacity - (nd._op||1))*0.12;
546
460
  nd.mesh.material.opacity = nd._op;
547
461
  nd.mesh.material.transparent = nd._op < 0.99;
548
- // 标签分级可见性:
549
- // - 聚焦态(选中了某节点):只显示发光簇(glowOn)+悬停的标签,淡出的隐藏
550
- // - 默认态:只显示常显类型(Entity/Concept)+悬停的标签
551
462
  if(nd.label){
552
463
  const forced = (nd===this.hovered || nd.id===this.selected);
553
- // 分级:钻取态只显示当前选中节点点亮的 top-N 邻居(二级/三级);
554
- // 默认态只显示一级书名(Evidence);悬停/选中强制显示。
555
- // 分级后可见标签本就稀疏(默认≈书数、钻取≈7),不做背面剔除,
556
- // 否则仅有的几个书名会在球背面时被剔空,出现"无标签"空窗。
557
464
  const show = forced || (this.selected ? nd._revealLabel : nd.isBook);
558
465
  nd.label.visible = show;
559
466
  if(show) nd.label.material.opacity = nd._op;
560
467
  }
561
468
  });
562
469
 
563
- // 更新边的端点(只更新可见的边,省性能)
564
470
  this.edges.forEach(e=>{
565
471
  if(!e.line.visible) return;
566
472
  const arr = e.line.geometry.attributes.position.array;
@@ -569,7 +475,6 @@ class KnowledgePlanet {
569
475
  e.line.geometry.attributes.position.needsUpdate = true;
570
476
  });
571
477
 
572
- // hover 检测 + 放大(用 scale,不改几何,不抖)
573
478
  const hit = this._pick();
574
479
  if(hit !== this.hovered){
575
480
  if(this.hovered) this._setHover(this.hovered, false);
@@ -577,7 +482,6 @@ class KnowledgePlanet {
577
482
  this.hovered = hit;
578
483
  this.canvas.style.cursor = hit ? 'pointer' : 'grab';
579
484
  }
580
- // 平滑 scale
581
485
  this.nodes.forEach(nd=>{
582
486
  const target = (nd===this.hovered || nd.id===this.selected) ? 1.5 : 1.0;
583
487
  const s = nd.mesh.scale.x + (target - nd.mesh.scale.x)*0.18;
@@ -587,7 +491,6 @@ class KnowledgePlanet {
587
491
 
588
492
  _setHover(nd, on){ /* 状态切换占位,scale 在 _update 平滑处理 */ }
589
493
 
590
- // 生成文字标签 Sprite
591
494
  _makeLabel(text){
592
495
  const T = this.T;
593
496
  const label = (text||'').length>12 ? text.slice(0,11)+'…' : (text||'');
@@ -597,10 +500,8 @@ class KnowledgePlanet {
597
500
  ctx.font = `600 ${fs}px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif`;
598
501
  const w = Math.ceil(ctx.measureText(label).width) + 24;
599
502
  cv.width = w; cv.height = fs+24;
600
- // 重设 font(canvas resize 后失效)
601
503
  ctx.font = `600 ${fs}px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif`;
602
504
  ctx.textAlign='center'; ctx.textBaseline='middle';
603
- // 半透明深色描边底,深空背景上清晰
604
505
  ctx.fillStyle='rgba(255,255,255,.92)';
605
506
  ctx.shadowColor='rgba(0,0,0,.85)'; ctx.shadowBlur=8;
606
507
  ctx.fillText(label, cv.width/2, cv.height/2);
@@ -613,12 +514,10 @@ class KnowledgePlanet {
613
514
  return sp;
614
515
  }
615
516
 
616
- // 相机平滑飞到某节点
617
517
  flyTo(id){
618
518
  const nd = this.nodes.find(n=>n.id===id);
619
519
  if(!nd) return;
620
520
  this.autoSpin = false;
621
- // 把该节点转到正面:根据其当前世界位置反推目标旋转
622
521
  this._flyTarget = nd;
623
522
  this.camDistTarget = 380;
624
523
  }
@@ -629,7 +528,6 @@ class KnowledgePlanet {
629
528
  else this.clearFocus();
630
529
  }
631
530
 
632
- // 聚焦汇聚:点击节点 → 相关节点发光 + 汇聚到它周围,无关节点淡出
633
531
  focusOn(id){
634
532
  const T = this.T;
635
533
  const center = this.nodes.find(n=>n.id===id);
@@ -638,7 +536,6 @@ class KnowledgePlanet {
638
536
  this._flyTarget = center;
639
537
  this.camDistTarget = 420;
640
538
 
641
- // 找出所有与中心节点直接相连的节点;同时只显示该节点的连线(默认全隐藏)
642
539
  const relatedIds = new Set([id]);
643
540
  this.edges.forEach(e=>{
644
541
  const on = (e.from.id===id || e.to.id===id);
@@ -647,22 +544,17 @@ class KnowledgePlanet {
647
544
  });
648
545
  const related = this.nodes.filter(n=>relatedIds.has(n.id) && n.id!==id);
649
546
 
650
- // 分级标签:本次钻取只点亮 top-N 最重要(连接数最高)的邻居标签。
651
- // 点书 → 看全书最核心的几个;点其中一个 → 再看它的核心邻居(逐级下钻)。
652
547
  this.nodes.forEach(n=> n._revealLabel=false);
653
548
  related.slice().sort((a,b)=> (b.deg||0)-(a.deg||0)).slice(0, 6)
654
549
  .forEach(n=> n._revealLabel=true);
655
550
 
656
- // 中心节点:移到球心附近、发光、不淡出
657
551
  center.focusPos = new T.Vector3(0, 0, 0);
658
552
  center.glowOn = true; center.dim = false;
659
553
 
660
- // 相关节点:围绕中心排成一圈(球面环),发光
661
554
  const R = 95; // 汇聚半径
662
555
  const n = related.length;
663
556
  related.forEach((nd, i)=>{
664
557
  const ang = (i / Math.max(1,n)) * Math.PI*2;
665
- // 在一个倾斜的环上分布,有立体感
666
558
  const tilt = 0.5;
667
559
  nd.focusPos = new T.Vector3(
668
560
  Math.cos(ang)*R,
@@ -672,7 +564,6 @@ class KnowledgePlanet {
672
564
  nd.glowOn = true; nd.dim = false;
673
565
  });
674
566
 
675
- // 无关节点:淡出、退远、不发光
676
567
  this.nodes.forEach(nd=>{
677
568
  if(!relatedIds.has(nd.id)){
678
569
  nd.focusPos = nd.basePos.clone().multiplyScalar(2.4); // 退远
@@ -681,7 +572,6 @@ class KnowledgePlanet {
681
572
  });
682
573
  }
683
574
 
684
- // 清除聚焦:所有节点平滑回归球面
685
575
  clearFocus(){
686
576
  this._flyTarget = null;
687
577
  this.camDistTarget = 720;
@@ -695,7 +585,6 @@ class KnowledgePlanet {
695
585
  this.edges.forEach(e=>{ e.line.visible = false; }); // 回到默认:不画边
696
586
  }
697
587
 
698
- // 搜索高亮:命中节点发光+显标签,其余淡出(原地,不汇聚);清空传空数组。
699
588
  highlight(ids){
700
589
  const set = new Set(ids || []);
701
590
  if(!set.size){ this.setSelected(null); return; } // 无命中→恢复默认视图
@@ -727,7 +616,6 @@ class KnowledgePlanet {
727
616
  this.disposed = true;
728
617
  if(this.raf) cancelAnimationFrame(this.raf);
729
618
 
730
- // 移除所有事件监听器
731
619
  try{
732
620
  window.removeEventListener('pointermove', this._onMove);
733
621
  window.removeEventListener('pointerup', this._onUp);
@@ -739,7 +627,6 @@ class KnowledgePlanet {
739
627
  }
740
628
  }catch(e){}
741
629
 
742
- // 递归释放场景里所有 geometry / material / texture
743
630
  try{
744
631
  this.scene.traverse(obj=>{
745
632
  if(obj.geometry) obj.geometry.dispose();
@@ -753,25 +640,21 @@ class KnowledgePlanet {
753
640
  });
754
641
  }catch(e){}
755
642
 
756
- // 强制释放 WebGL context(renderer.dispose 不会释放 context,必须 forceContextLoss)
757
643
  try{
758
644
  this.renderer.dispose();
759
645
  this.renderer.forceContextLoss();
760
646
  if(this.canvas && this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas);
761
647
  }catch(e){}
762
648
 
763
- // 断开引用,助 GC
764
649
  this.nodes = []; this.edges = []; this._meshes = null;
765
650
  this.scene = null; this.renderer = null; this.camera = null; this.world = null;
766
651
  }
767
652
  }
768
653
 
769
- // ── 等待 Three.js(由 head 的 module 预加载到 window.THREE)──
770
654
  async function ensureThree(){
771
655
  if(THREE) return THREE;
772
656
  if(window.THREE){ THREE = window.THREE; return THREE; }
773
657
  if(window.__threeLoadError) return null;
774
- // 等待 three-ready 事件(最多 5 秒)
775
658
  return await new Promise(resolve=>{
776
659
  let done = false;
777
660
  const onReady = ()=>{ if(done) return; done=true; THREE = window.THREE; resolve(THREE); };
@@ -788,9 +671,7 @@ async function renderGraph(){
788
671
  const container = document.getElementById('graph-canvas');
789
672
  if(!container) return;
790
673
 
791
- // 清旧实例
792
674
  if(planet){ try{ planet.dispose(); }catch(e){} planet = null; }
793
- // 移除旧 canvas(保留 hint/legend 等覆盖层)
794
675
  Array.from(container.querySelectorAll('canvas')).forEach(c=>c.remove());
795
676
 
796
677
  const T = await ensureThree();
@@ -814,10 +695,6 @@ async function renderGraph(){
814
695
  }
815
696
  }
816
697
 
817
- // startRotation/stopRotation:上传/解析/摄入期间停掉星球渲染循环 ——
818
- // 每帧 _update() + renderer.render() 会争 GPU 和主线程,拖慢大文件上传。
819
- // 真正省 CPU 的是停 RAF(只关 autoSpin 不够,缓动还在跑)。
820
- // planet.raf 在 start() 里被赋值,dispose() 里被 cancel —— 这里只 cancel。
821
698
  function startRotation(){
822
699
  if(!planet) return;
823
700
  if(planet.raf) return; // 已在跑(避免 start() 双重 RAF 嵌套)
@@ -831,7 +708,6 @@ function stopRotation(){
831
708
  }
832
709
  }
833
710
 
834
-
835
711
  function relLabel(t){
836
712
  const m={CREATED_BY:'提出',SUPPORTS:'支持',CONTRADICTS:'冲突',MENTIONS:'提及',
837
713
  BELONGS_TO:'属于',DERIVED_FROM:'衍生',IMPLEMENTS:'实现',USES:'运用',SUPERSEDES:'取代'};
@@ -846,9 +722,6 @@ function renderLegend(){
846
722
  <span class="lg-dot" style="background:none;border:2px dashed var(--alert);width:8px;height:8px"></span>观点冲突</div>`;
847
723
  }
848
724
 
849
- // ════════════════════════════════════════════════════════
850
- // Wiki 详情面板
851
- // ════════════════════════════════════════════════════════
852
725
  function selectKU(id){
853
726
  if(!state.kus[id]) return;
854
727
  state.selected=id;
@@ -858,7 +731,6 @@ function selectKU(id){
858
731
  loadSynthesis(id); // 若该节点已有缓存综述,自动显示(GET,不调 LLM)
859
732
  }
860
733
 
861
- // 读缓存综述(只 GET,不触发 LLM/不花钱)。有缓存就填进 state.synth 并重渲染。
862
734
  async function loadSynthesis(id){
863
735
  if(client.demo) return;
864
736
  const cur=state.synth[id];
@@ -872,7 +744,6 @@ async function loadSynthesis(id){
872
744
  }catch(e){ /* 无缓存/出错都静默,维持"生成"按钮 */ }
873
745
  }
874
746
 
875
- // 点「生成综述」→ 调 LLM 生成(或返回缓存)。派生只读视图,后端不写知识库。
876
747
  async function genSynthesis(id, force){
877
748
  if(client.demo){ toast('演示模式无法生成综述,请先连接后端','err'); return; }
878
749
  state.synth[id]='loading';
@@ -888,7 +759,6 @@ async function genSynthesis(id, force){
888
759
  }
889
760
  window.genSynthesis=genSynthesis;
890
761
 
891
- // 路径查询(高级检索):把当前节点设为起/终点,查两点最短路径并在星球高亮。
892
762
  function setPathNode(id, which){
893
763
  state.pathSel=state.pathSel||{};
894
764
  state.pathSel[which]=id; state.pathSel.result='';
@@ -910,8 +780,6 @@ async function findPath(){
910
780
  }
911
781
  window.findPath=findPath;
912
782
 
913
- // 按需补全:星球全量接口为瘦身省略了 source_text,点开节点时单独拉 /ku/{id} 拿完整原文,
914
- // 合并回 state.kus 并重渲染(仍选中才刷新,避免快速点击的竞态覆盖)。
915
783
  async function hydrateKU(id){
916
784
  const ku=state.kus[id];
917
785
  if(!ku || client.demo) return; // 演示数据自带原文,无需拉
@@ -928,19 +796,15 @@ function clearSelection(){
928
796
  if(planet){ try{ planet.setSelected(null); }catch(e){} }
929
797
  $('#wiki-body').innerHTML=emptyWiki();
930
798
  }
931
- // 暴露给 3D 引擎(planet 点击节点时回调)
932
799
  window.selectKU = selectKU;
933
800
  window.clearSelection = clearSelection;
934
801
 
935
- // 可折叠区块开关(供 side-label 的 onclick 调用)
936
802
  function toggleCollapse(id){ const el=document.getElementById(id); if(el) el.classList.toggle('collapsed'); }
937
803
  window.toggleCollapse = toggleCollapse;
938
804
  function emptyWiki(){
939
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>`;
940
806
  }
941
807
 
942
- // 综合页(synthesis page):把节点在图里的邻域组织成可读的 wiki 词条,
943
- // 而不只是几个原子字段。图仍是唯一真相源,这里只是它的一个只读投影(同 3D 星球)。
944
808
  function renderWiki(ku){
945
809
  if(!ku){ $('#wiki-body').innerHTML=emptyWiki(); return; }
946
810
  const c=KU_COLORS[ku.ku_type];
@@ -951,7 +815,6 @@ function renderWiki(ku){
951
815
  html+=`<div class="wiki-title">${esc(ku.name)}</div>`;
952
816
  if(ku.domain) html+=`<div class="wiki-domain">${esc(ku.domain)}</div>`;
953
817
 
954
- // ── 概述 ──
955
818
  if(ku.ku_type==='Claim'){
956
819
  if(ex.statement) html+=`<div class="wiki-summary">${esc(ex.statement)}</div>`;
957
820
  const cs=ex.claim_status||'pending';
@@ -966,7 +829,6 @@ function renderWiki(ku){
966
829
  if(ku.summary) html+=`<div class="wiki-summary">${esc(ku.summary)}</div>`;
967
830
  }
968
831
 
969
- // ── AI 综述(第2档:LLM 把节点邻域组织成连贯词条;派生只读视图,不写回知识库)──
970
832
  if(!client.demo){
971
833
  const sy=state.synth[ku.ku_id];
972
834
  html+=`<div class="wiki-section"><h4>AI 综述</h4>`;
@@ -984,7 +846,6 @@ function renderWiki(ku){
984
846
  html+=`</div>`;
985
847
  }
986
848
 
987
- // ── 路径查询(高级检索:两节点间最短路径,结果在星球高亮)──
988
849
  if(!client.demo){
989
850
  const ps=state.pathSel||{};
990
851
  const nm=id=>id?esc(state.kus[id]&&state.kus[id].name||id):'(未设)';
@@ -999,16 +860,13 @@ function renderWiki(ku){
999
860
  html+=`</div>`;
1000
861
  }
1001
862
 
1002
- // ── 原文段落 ──
1003
863
  if(ex.source_text && ex.source_text.trim()){
1004
864
  html+=`<div class="wiki-section"><h4>原文段落</h4><div class="wiki-source">${esc(ex.source_text)}</div></div>`;
1005
865
  }
1006
- // Entity 别名
1007
866
  if(ku.ku_type==='Entity'&&ex.aliases&&ex.aliases.length){
1008
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>`;
1009
868
  }
1010
869
 
1011
- // ── 邻域按类型分组(去重),组织成词条各段 ──
1012
870
  const nb={Fact:[],Claim:[],Concept:[],Entity:[],Evidence:[]};
1013
871
  const seen=new Set([ku.ku_id]);
1014
872
  getRelations(ku.ku_id).forEach(r=>{
@@ -1017,8 +875,6 @@ function renderWiki(ku){
1017
875
  seen.add(o.ku_id);
1018
876
  if(nb[o.ku_type]) nb[o.ku_type].push(o);
1019
877
  });
1020
- // 图里事实/观点通常只连到 Evidence(书),不直接连实体/概念。为让词条真正丰满,
1021
- // 再按"陈述里提到本节点名字/别名"补充相关事实与观点(纯前端聚合,不改图)。
1022
878
  if(ku.ku_type!=='Evidence'){
1023
879
  const names=[ku.name, ...((ex.aliases)||[])].filter(n=>n && n.length>=2);
1024
880
  if(names.length){
@@ -1035,7 +891,6 @@ function renderWiki(ku){
1035
891
  if(ku.ku_type==='Evidence'){
1036
892
  const isColl=(ku.tags||[]).includes('_collection');
1037
893
  if(isColl){
1038
- // 一级:合集(文件夹)→ 列出所含文件/章节(PART_OF 子节点),点击逐级下钻
1039
894
  const files=(state.relations||[]).filter(r=>r.type==='PART_OF'&&r.to===ku.ku_id).map(r=>state.kus[r.from]).filter(Boolean);
1040
895
  html+=`<div class="wiki-section"><h4>合集 · 来源</h4><div class="evid-item"><div class="evid-src">📁 ${esc(ku.name)}</div></div></div>`;
1041
896
  if(files.length){
@@ -1044,7 +899,6 @@ function renderWiki(ku){
1044
899
  html+=`</div></div>`;
1045
900
  }
1046
901
  } else {
1047
- // 二级:文件(章节)或独立来源 → 来源 + 所属合集 + 本文件关键词
1048
902
  html+=`<div class="wiki-section"><h4>来源</h4><div class="evid-item">
1049
903
  <div class="evid-src">${esc(ex.source||ku.name)}</div>
1050
904
  <div class="evid-meta">${[ex.author,ex.year,ex.page?'p.'+ex.page:''].filter(Boolean).join(' · ')}</div></div></div>`;
@@ -1064,7 +918,6 @@ function renderWiki(ku){
1064
918
  }
1065
919
  }
1066
920
  } else {
1067
- // 实体/概念/事实/观点:综合成词条。相关事实/观点放进固定高度框,内部滚动,省纵向空间。
1068
921
  if(nb.Fact.length){
1069
922
  let inner='';
1070
923
  nb.Fact.slice(0,50).forEach(k=>{ inner+=`<div class="wiki-item fact" onclick="selectKU('${k.ku_id}')">${esc(stmt(k))}</div>`; });
@@ -1078,7 +931,6 @@ function renderWiki(ku){
1078
931
  });
1079
932
  html+=`<div class="wiki-section"><h4>相关观点 ${nb.Claim.length}</h4><div class="wiki-scroll">${inner}</div></div>`;
1080
933
  }
1081
- // 关联概念/实体:小星球式散点(省空间,悬停看名、点击进入)
1082
934
  const linked=[...nb.Concept,...nb.Entity];
1083
935
  if(linked.length){
1084
936
  html+=`<div class="wiki-section"><h4>关联 ${linked.length}</h4>${kwCluster(linked)}</div>`;
@@ -1090,7 +942,6 @@ function renderWiki(ku){
1090
942
  }
1091
943
  }
1092
944
 
1093
- // 标签
1094
945
  if(ku.tags&&ku.tags.length){
1095
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>`;
1096
947
  }
@@ -1099,8 +950,6 @@ function renderWiki(ku){
1099
950
  $('#wiki-body').innerHTML=html;
1100
951
  }
1101
952
 
1102
- // 关联关键词的小星球式散点:fibonacci 圆盘分布、按类型着色、悬停显名(<title>)、点击进入。
1103
- // 比一长串 chip 省纵向空间,且呼应星球主题。
1104
953
  function kwCluster(list, capN){
1105
954
  const N=Math.min(list.length, capN||80);
1106
955
  const cx=130, cy=90, R=82, ga=Math.PI*(3-Math.sqrt(5));
@@ -1126,9 +975,6 @@ function getRelations(id){
1126
975
  return out;
1127
976
  }
1128
977
 
1129
- // ════════════════════════════════════════════════════════
1130
- // 能力卡片
1131
- // ════════════════════════════════════════════════════════
1132
978
  function renderAbilities(){
1133
979
  const body=$('#ability-body');
1134
980
  body.innerHTML=state.abilities.map(a=>{
@@ -1182,9 +1028,6 @@ async function compileAbility(key,btn){
1182
1028
  }
1183
1029
  }
1184
1030
 
1185
- // ════════════════════════════════════════════════════════
1186
- // 冲突裁决
1187
- // ════════════════════════════════════════════════════════
1188
1031
  function renderConflicts(){
1189
1032
  const open=state.conflicts.filter(c=>c.status==='open');
1190
1033
  const body=$('#conflict-body');
@@ -1210,6 +1053,7 @@ function renderConflicts(){
1210
1053
  <div class="cc-claim-conf">置信度 ${(b.confidence*100).toFixed(0)}%</div></div>
1211
1054
  <div class="cc-resolve">
1212
1055
  <button class="btn btn-sm btn-compile" ${pick?'':'disabled'} onclick="resolveConflict('${c.conflict_id}')">采纳所选观点</button>
1056
+ <button class="btn btn-sm btn-ghost" onclick="dismissConflict('${c.conflict_id}')" title="两者并不互斥:关闭卡片,双方观点都保留">误判</button>
1213
1057
  </div>
1214
1058
  </div>`;
1215
1059
  }).join('');
@@ -1227,17 +1071,14 @@ async function resolveConflict(cid){
1227
1071
  await new Promise(r=>setTimeout(r,500));
1228
1072
  const c=state.conflicts.find(x=>x.conflict_id===cid);
1229
1073
  if(c) c.status='resolved';
1230
- // 落败方标 superseded
1231
1074
  const loser=c.claim_a_id===winner?c.claim_b_id:c.claim_a_id;
1232
1075
  if(state.kus[loser]) state.kus[loser].extra.claim_status='superseded';
1233
1076
  if(state.kus[winner]) state.kus[winner].extra.claim_status='active';
1234
- // 移除这对 Claim 间的 CONTRADICTS 关系
1235
1077
  state.relations=state.relations.filter(r=>
1236
1078
  !(r.type==='CONTRADICTS' &&
1237
1079
  ((r.from===loser&&r.to===winner)||(r.from===winner&&r.to===loser))));
1238
1080
  delete state.conflictPick[cid];
1239
1081
  toast('冲突已裁决,落败观点标记为 superseded','ok');
1240
- // 裁决是低频操作,destroy 重建最稳(增量更新与 G6 hover-activate 有内部时序冲突)
1241
1082
  renderGraph();
1242
1083
  renderPanels();
1243
1084
  return;
@@ -1250,14 +1091,27 @@ async function resolveConflict(cid){
1250
1091
  }catch(e){ toast('裁决失败','err'); }
1251
1092
  }
1252
1093
 
1253
- // ════════════════════════════════════════════════════════
1254
- // 搜索
1255
- // ════════════════════════════════════════════════════════
1094
+ async function dismissConflict(cid){
1095
+ if(client.demo){
1096
+ const c=state.conflicts.find(x=>x.conflict_id===cid);
1097
+ if(c) c.status='resolved';
1098
+ delete state.conflictPick[cid];
1099
+ toast('已按误判关闭,双方观点都保留','ok');
1100
+ renderConflicts();
1101
+ return;
1102
+ }
1103
+ try{
1104
+ await client._post(`/conflicts/${cid}/dismiss`,{note:'UI 误判关闭',resolved_by:'ui'});
1105
+ toast('已按误判关闭,双方观点都保留','ok');
1106
+ delete state.conflictPick[cid];
1107
+ await loadData();
1108
+ }catch(e){ toast('关闭失败','err'); }
1109
+ }
1110
+
1256
1111
  let searchTimer;
1257
1112
  async function doSearch(q){
1258
1113
  if(!q.trim()){ if(window.planet) window.planet.setSelected(null); return; } // 清空搜索→恢复默认视图(不重建星球)
1259
1114
  if(client.demo){
1260
- // 演示:名字/摘要匹配,高亮命中节点
1261
1115
  const hits=Object.values(state.kus).filter(k=>
1262
1116
  (k.name+k.summary+((k.extra&&k.extra.statement)||'')).toLowerCase().includes(q.toLowerCase()));
1263
1117
  highlightNodes(hits.map(h=>h.ku_id));
@@ -1272,13 +1126,9 @@ async function doSearch(q){
1272
1126
  }catch(e){ toast('搜索失败','err'); }
1273
1127
  }
1274
1128
  function highlightNodes(ids){
1275
- // 在 Three.js 星球上高亮命中节点(旧 G6 实现已废弃)
1276
1129
  if(window.planet) window.planet.highlight(ids || []);
1277
1130
  }
1278
1131
 
1279
- // ════════════════════════════════════════════════════════
1280
- // 摄入(文本 + 文件/文件夹批量)
1281
- // ════════════════════════════════════════════════════════
1282
1132
  let pickedFiles = []; // 待摄入文件列表
1283
1133
 
1284
1134
  function fmtSize(b){
@@ -1312,7 +1162,6 @@ function renderFileList(){
1312
1162
  });
1313
1163
  }
1314
1164
 
1315
- // 异步摄入(方案A):文件上传后立即转后台,前端轮询 job 真进度,期间可继续操作。
1316
1165
  let ingJobs = []; // {jobId,name,status,processed,total,ku,error}
1317
1166
  let _ingPoll = null;
1318
1167
 
@@ -1346,7 +1195,6 @@ async function pollIngestJobs(){
1346
1195
  if((s.ku_count||0) > 0){
1347
1196
  toast(`『${j.name}』摄入完成:${s.ku_count} 个知识单元`,'ok');
1348
1197
  }else{
1349
- // 上传/解析成功但抽取 0 知识:最常见是 LLM API Key 无效/未配置(如 401),或内容太短
1350
1198
  toast(`『${j.name}』已上传但未抽取到知识(0 个)。多半是 LLM API Key 无效/未配置——请到「连接设置」重填正确 key 后重传`,'err');
1351
1199
  }
1352
1200
  }
@@ -1359,10 +1207,6 @@ async function pollIngestJobs(){
1359
1207
  if(!ingJobs.length){ clearInterval(_ingPoll); _ingPoll=null; renderIngestStatus(); }
1360
1208
  }
1361
1209
 
1362
- // 「清空知识库」已从前端移除(危险的破坏性操作不该是任何人可点的按钮)。
1363
- // 需要清库改走后端 API:POST /knowledge/reset(设了 HKC_API_KEY 时需带 X-API-Key),
1364
- // 例:curl -X POST http://127.0.0.1:8000/knowledge/reset -H "X-API-Key: <key>"
1365
-
1366
1210
  async function submitIngest(){
1367
1211
  const text=$('#ingest-text').value.trim();
1368
1212
  const url=$('#ingest-url').value.trim();
@@ -1378,13 +1222,11 @@ async function submitIngest(){
1378
1222
  if(client.demo){
1379
1223
  btn.disabled=false; btn.textContent='开始摄入';
1380
1224
  toast('未连接后端,无法摄入。请点左下「连接设置」连接 hkc-api 后再试','err');
1381
- // 自动打开连接设置,引导用户
1382
1225
  closeModal('#ingest-modal');
1383
1226
  openModal('#settings-modal');
1384
1227
  return;
1385
1228
  }
1386
1229
 
1387
- // 1) 文本(若有)——通常较短,走同步端点,拿到结果即刷新
1388
1230
  if(text){
1389
1231
  btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 摄入文本';
1390
1232
  try{
@@ -1397,7 +1239,6 @@ async function submitIngest(){
1397
1239
  btn.disabled=false; btn.textContent='开始摄入';
1398
1240
  }
1399
1241
 
1400
- // 1.5) 网页 URL——同步抓取正文 + 抽取(单页通常较短)
1401
1242
  if(url){
1402
1243
  btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 抓取网页';
1403
1244
  try{
@@ -1413,7 +1254,6 @@ async function submitIngest(){
1413
1254
  btn.disabled=false; btn.textContent='开始摄入';
1414
1255
  }
1415
1256
 
1416
- // 2) 文件——异步提交,秒返回 job_id,转后台;不阻塞,可继续添加/上传下一个
1417
1257
  if(hasFiles){
1418
1258
  btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 提交中';
1419
1259
  let submitted=0;
@@ -1442,21 +1282,15 @@ async function submitIngest(){
1442
1282
  }
1443
1283
  }
1444
1284
 
1445
- // ════════════════════════════════════════════════════════
1446
- // 模态 + 连接
1447
- // ════════════════════════════════════════════════════════
1448
- // 打开摄入模态 → 停星球渲染(避免大文件上传被 GPU/主线程争用拖慢)
1449
1285
  function openModal(s){
1450
1286
  $(s).classList.add('show');
1451
1287
  if(s === '#ingest-modal') stopRotation();
1452
1288
  }
1453
- // 关闭摄入模态 → 恢复星球渲染(idempotent:stopRotation 未调过也无害)
1454
1289
  function closeModal(s){
1455
1290
  $(s).classList.remove('show');
1456
1291
  if(s === '#ingest-modal') startRotation();
1457
1292
  }
1458
1293
 
1459
- // ── 书籍管理(按来源删除)──
1460
1294
  async function openBooks(){
1461
1295
  if(client.demo){ toast('未连接后端,无法管理书籍','err'); openModal('#settings-modal'); return; }
1462
1296
  openModal('#books-modal');
@@ -1502,7 +1336,6 @@ function setConn(){
1502
1336
  else{ p.className='conn-pill live'; t.textContent='已连接'; }
1503
1337
  }
1504
1338
 
1505
- // 连接设置持久化(localStorage):刷新后回填 + 自动重连,避免每次重填 key。
1506
1339
  const CONN_KEY='hkc_conn_v1';
1507
1340
  function _connSnapshot(){
1508
1341
  return {
@@ -1534,7 +1367,6 @@ async function tryConnect(){
1534
1367
  const btn=$('#btn-connect'); btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 连接中';
1535
1368
  const ok=await client.connect(url);
1536
1369
  if(ok){
1537
- // 连接成功后,把 LLM 配置发给后端(若填了 key)
1538
1370
  const provider=$('#llm-provider').value;
1539
1371
  const key=$('#llm-key').value.trim();
1540
1372
  const model=$('#llm-model').value.trim();
@@ -1562,7 +1394,6 @@ async function tryConnect(){
1562
1394
  }
1563
1395
  }
1564
1396
 
1565
- // provider 切换:联动 model 占位符 + base_url 显隐
1566
1397
  function _syncLLMFields(){
1567
1398
  const p=$('#llm-provider').value;
1568
1399
  const modelInput=$('#llm-model');
@@ -1570,9 +1401,7 @@ function _syncLLMFields(){
1570
1401
  const baseInput=$('#llm-baseurl');
1571
1402
  const defaults={ deepseek:'deepseek-chat', anthropic:'claude-sonnet-4-6', openai:'gpt-4o-mini', 'openai-compatible':'(自定义模型名)' };
1572
1403
  modelInput.placeholder=defaults[p]||'';
1573
- // 只有自定义/本地需要填 base_url
1574
1404
  baseField.style.display=(p==='openai-compatible')?'':'none';
1575
- // 本地模型默认填 Ollama 地址(后端要求 openai-compatible 必填 base_url,留空会 400);已有值不覆盖
1576
1405
  if(p==='openai-compatible' && !baseInput.value.trim()){
1577
1406
  baseInput.value='http://localhost:11434/v1';
1578
1407
  }
@@ -1580,9 +1409,6 @@ function _syncLLMFields(){
1580
1409
 
1581
1410
  function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m])); }
1582
1411
 
1583
- // ════════════════════════════════════════════════════════
1584
- // 事件绑定 + 启动
1585
- // ════════════════════════════════════════════════════════
1586
1412
  $('#modes').addEventListener('click',e=>{
1587
1413
  const b=e.target.closest('.mode-btn'); if(!b)return;
1588
1414
  $$('.mode-btn').forEach(x=>x.classList.remove('on'));
@@ -1597,7 +1423,6 @@ $('#search-input').addEventListener('keydown',e=>{ if(e.key==='Enter') doSearch(
1597
1423
  $('#btn-ingest').onclick=()=>openModal('#ingest-modal');
1598
1424
  $('#btn-books').onclick=openBooks;
1599
1425
  $('#btn-settings').onclick=()=>openModal('#settings-modal');
1600
- // 文件/文件夹选择
1601
1426
  $('#btn-pick-files').onclick=()=>$('#ingest-files').click();
1602
1427
  $('#btn-pick-folder').onclick=()=>$('#ingest-folder').click();
1603
1428
  $('#ingest-files').addEventListener('change',e=>{ addFiles(e.target.files); e.target.value=''; });
@@ -1609,11 +1434,8 @@ _syncLLMFields(); // 初始化字段状态
1609
1434
  $('#btn-use-demo').onclick=()=>{ client.useDemo(); setConn(); closeModal('#settings-modal'); loadData(); toast('已切换到演示数据'); };
1610
1435
  $('#btn-fit').onclick=()=>window._fitGraph&&window._fitGraph();
1611
1436
  $$('[data-close]').forEach(b=>b.onclick=()=>{ closeModal('#ingest-modal'); closeModal('#settings-modal'); closeModal('#books-modal'); });
1612
- // 点背景关模态:走 closeModal(里面会触发 ingest 的 startRotation,见 openModal 注释)
1613
1437
  $$('.modal-bg').forEach(m=>m.addEventListener('click',e=>{ if(e.target===m) closeModal('#'+m.id); }));
1614
- // G6 的 autoResize:true 已处理窗口缩放,无需手动重建
1615
1438
 
1616
- // 启动:有保存的连接设置则回填并静默自动重连,否则按演示模式启动
1617
1439
  async function startup(){
1618
1440
  const saved=loadConnSettings();
1619
1441
  if(saved && saved.url){
@@ -1621,15 +1443,10 @@ async function startup(){
1621
1443
  client.apiKey=saved.apiKey||''; // 鉴权 key 只能取保存的 apiKey,绝不混用模型的 key
1622
1444
  const ok=await client.connect(saved.url);
1623
1445
  if(ok){
1624
- // 不再启动时自动 POST 保存的 LLM 配置:那会把 localStorage 里可能过期的配置
1625
- // 覆盖到后端、压掉 .env 的权威配置。模型配置只在用户点「连接」时显式应用。
1626
1446
  setConn(); await loadData(); return;
1627
1447
  }
1628
- // 自动重连失败(后端没起/地址变了)→ 回退演示,字段已回填,用户可手动重连
1629
1448
  toast('已保存的后端未连上,回退演示模式(设置已回填,可在「连接设置」重连)','err');
1630
1449
  }
1631
- // 嵌入 Foxclaw(同源部署):无保存设置或保存地址连不上时,优先自动连**当前站点**(相对 /knowledge/*),
1632
- // 连通即用真实知识;连不上(如独立打开本文件)再回退演示。
1633
1450
  if(await client.connect('')){ setConn(); await loadData(); return; }
1634
1451
  setConn();
1635
1452
  loadData();