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
@@ -0,0 +1,228 @@
1
+ """剥离代码里的解释性注释。
2
+
3
+ 用法:
4
+ python tools/strip_comments.py --dry 只统计,不改文件
5
+ python tools/strip_comments.py 真正改写
6
+ python tools/strip_comments.py a.py b.js 只处理指定文件
7
+
8
+ 不能一刀切:下面这些注释被删会改变 lint / 类型检查 / 解释器行为,必须原样保留。
9
+ #!... shebang
10
+ # -*- coding -*- 编码声明
11
+ # noqa ... ruff 抑制
12
+ # type: ... mypy 注解(含 `x = [] # type: list[int]`)
13
+ # pragma / fmt: / isort: / pyright: / pylint: / nosec / coverage:
14
+ docstring 不动 —— 它是运行时对象,不是注释。
15
+
16
+ vendor/ 是内嵌第三方,不改;frontend/v3 是构建产物,由 vite 重新生成。
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import io
22
+ import re
23
+ import sys
24
+ import tokenize
25
+ from pathlib import Path
26
+
27
+ ROOT = Path(__file__).resolve().parent.parent
28
+
29
+ SKIP_PARTS = {".git", "node_modules", "__pycache__", ".venv", "venv", "vendor",
30
+ "foxclaw_data", "test_data", ".pytest_cache", ".ruff_cache", ".mypy_cache",
31
+ "build", "dist", "htmlcov"}
32
+ # 生成副本:npm/python 由 npm/build.cjs 从仓库根复制,frontend/v3 由 vite 构建
33
+ SKIP_PREFIX = {"frontend/v3/", "npm/python/"}
34
+ EXTS = {".py", ".js", ".vue", ".ps1"}
35
+
36
+ DIRECTIVE = re.compile(
37
+ r"^#\s*(?:!|-\*-|noqa|type:|pragma|fmt:|isort:|pyright:|pylint:|ruff:|nosec|"
38
+ r"coverage:|mypy:|noinspection|coding[:=])",
39
+ re.I,
40
+ )
41
+
42
+ MAX_BLANK = 2
43
+
44
+
45
+ def targets() -> list[Path]:
46
+ out = []
47
+ for p in ROOT.rglob("*"):
48
+ if p.suffix not in EXTS or not p.is_file():
49
+ continue
50
+ rel = p.relative_to(ROOT).as_posix()
51
+ if SKIP_PARTS & set(p.relative_to(ROOT).parts):
52
+ continue
53
+ if any(rel.startswith(x) for x in SKIP_PREFIX):
54
+ continue
55
+ if rel == "tools/strip_comments.py":
56
+ continue
57
+ out.append(p)
58
+ return sorted(out)
59
+
60
+
61
+ def collapse_blank(lines: list[str], limit: int = MAX_BLANK) -> list[str]:
62
+ out: list[str] = []
63
+ run = 0
64
+ for ln in lines:
65
+ if ln.strip() == "":
66
+ run += 1
67
+ if run > limit:
68
+ continue
69
+ else:
70
+ run = 0
71
+ out.append(ln)
72
+ return out
73
+
74
+
75
+ def strip_py(text: str) -> tuple[str, int]:
76
+ try:
77
+ toks = list(tokenize.generate_tokens(io.StringIO(text).readline))
78
+ except (tokenize.TokenError, IndentationError, SyntaxError):
79
+ return text, 0
80
+
81
+ lines = text.splitlines(keepends=True)
82
+ drops: dict[int, int | None] = {}
83
+ for t in toks:
84
+ if t.type != tokenize.COMMENT:
85
+ continue
86
+ row, col = t.start
87
+ if DIRECTIVE.match(t.string):
88
+ continue
89
+ line = lines[row - 1] if row - 1 < len(lines) else ""
90
+ head = line[:col]
91
+ drops[row] = None if head.strip() == "" else col
92
+
93
+ if not drops:
94
+ return text, 0
95
+
96
+ out: list[str] = []
97
+ for idx, line in enumerate(lines, 1):
98
+ if idx not in drops:
99
+ out.append(line)
100
+ continue
101
+ col = drops[idx]
102
+ if col is None:
103
+ continue
104
+ body = line[:col].rstrip()
105
+ eol = line[len(line.rstrip("\r\n")):]
106
+ out.append(body + eol)
107
+ out = collapse_blank(out)
108
+ return "".join(out), len(drops)
109
+
110
+
111
+ def _tpl_open(s: str) -> bool:
112
+ n = 0
113
+ i = 0
114
+ while i < len(s):
115
+ if s[i] == "\\":
116
+ i += 2
117
+ continue
118
+ if s[i] == "`":
119
+ n += 1
120
+ i += 1
121
+ return n % 2 == 1
122
+
123
+
124
+ def strip_js(text: str) -> tuple[str, int]:
125
+ """只删独占整行的注释,行尾注释一律不动。
126
+
127
+ 按位置切不行:正则字面量里的 \/\/ 和字符串里的 // 都会被当成注释起点 ——
128
+ markdown.js 的 /^https?:\/\//i 就被切掉半个函数。整行判定是安全的:
129
+ 行首出现 // 或 /* 时,它只能是注释。
130
+ """
131
+ lines = text.splitlines(keepends=True)
132
+ out: list[str] = []
133
+ removed = 0
134
+ in_block = ""
135
+ in_tpl = False
136
+ for line in lines:
137
+ body = line.rstrip("\r\n")
138
+ s = body.strip()
139
+ if in_block:
140
+ removed += 1
141
+ if (in_block == "<!--" and "-->" in s) or (in_block == "/*" and "*/" in s):
142
+ in_block = ""
143
+ continue
144
+ if in_tpl:
145
+ if _tpl_open(s):
146
+ in_tpl = False
147
+ out.append(line)
148
+ continue
149
+ if s.startswith("<!--"):
150
+ removed += 1
151
+ if "-->" not in s[4:]:
152
+ in_block = "<!--"
153
+ continue
154
+ if s.startswith("/*"):
155
+ removed += 1
156
+ if "*/" not in s[2:]:
157
+ in_block = "/*"
158
+ continue
159
+ if s.startswith("//"):
160
+ removed += 1
161
+ continue
162
+ if _tpl_open(body):
163
+ in_tpl = True
164
+ out.append(line)
165
+ return "".join(collapse_blank(out, 1)), removed
166
+
167
+
168
+ def strip_ps(text: str) -> tuple[str, int]:
169
+ """PowerShell 的注释符是 # 和 <# #>。只删独占行的,行尾的不碰 —— 拆字符串不值当。"""
170
+ out: list[str] = []
171
+ removed = 0
172
+ in_block = False
173
+ for line in text.splitlines(keepends=True):
174
+ stripped = line.strip()
175
+ if in_block:
176
+ removed += 1
177
+ if "#>" in stripped:
178
+ in_block = False
179
+ continue
180
+ if stripped.startswith("<#"):
181
+ removed += 1
182
+ if "#>" not in stripped[2:]:
183
+ in_block = True
184
+ continue
185
+ if stripped.startswith("#") and not re.match(r"#\s*requires\b", stripped, re.I):
186
+ removed += 1
187
+ continue
188
+ out.append(line)
189
+ return "".join(collapse_blank(out, 1)), removed
190
+
191
+
192
+ def main(argv: list[str]) -> int:
193
+ dry = "--dry" in argv
194
+ picked = [Path(a) for a in argv if not a.startswith("-")]
195
+ files = picked or targets()
196
+ total_files = 0
197
+ total_lines = 0
198
+ for p in files:
199
+ if not p.is_absolute():
200
+ p = ROOT / p
201
+ raw = p.read_bytes()
202
+ text = raw.decode("utf-8")
203
+ if p.suffix == ".py":
204
+ new, n = strip_py(text)
205
+ elif p.suffix == ".ps1":
206
+ new, n = strip_ps(text)
207
+ else:
208
+ new, n = strip_js(text)
209
+ if n == 0 or new == text:
210
+ continue
211
+ if p.suffix == ".py":
212
+ try:
213
+ compile(new, str(p), "exec")
214
+ except SyntaxError as e:
215
+ print(f"SKIP {p.relative_to(ROOT)}: 删后语法错误 {e}")
216
+ continue
217
+ total_files += 1
218
+ total_lines += n
219
+ if dry:
220
+ print(f"{n:5d} {p.relative_to(ROOT).as_posix()}")
221
+ else:
222
+ p.write_bytes(new.encode("utf-8"))
223
+ print(f"\n{'[dry] ' if dry else ''}files={total_files} comment_lines={total_lines}")
224
+ return 0
225
+
226
+
227
+ if __name__ == "__main__":
228
+ raise SystemExit(main(sys.argv[1:]))
@@ -8,6 +8,7 @@ from ..container import HKCContainer, get_container
8
8
  from ..schemas import (
9
9
  AbilityResponse, CoverageReportResponse,
10
10
  ConflictResponse, ConflictResolveRequest,
11
+ ConflictDismissRequest, ConflictBulkDismissRequest,
11
12
  )
12
13
 
13
14
  router = APIRouter(tags=["ability"])
@@ -149,3 +150,33 @@ def resolve_conflict(
149
150
  detail=f"裁决失败:冲突 {conflict_id} 不存在、已解决,或 winner_id 无效"
150
151
  )
151
152
  return {"conflict_id": conflict_id, "resolved": True, "winner": req.winner_id}
153
+
154
+
155
+ @router.post("/conflicts/bulk-dismiss")
156
+ def bulk_dismiss_conflicts(
157
+ req: ConflictBulkDismissRequest,
158
+ c: HKCContainer = Depends(get_container),
159
+ ):
160
+ """批量把误判冲突关闭(不判胜负、不废弃任何一方观点)。"""
161
+ n = c.kee.dismiss_conflicts(
162
+ req.conflict_ids, note=req.note, resolved_by=req.resolved_by
163
+ )
164
+ return {"requested": len(req.conflict_ids), "dismissed": n}
165
+
166
+
167
+ @router.post("/conflicts/{conflict_id}/dismiss")
168
+ def dismiss_conflict(
169
+ conflict_id: str,
170
+ req: ConflictDismissRequest,
171
+ c: HKCContainer = Depends(get_container),
172
+ ):
173
+ """把一张误判冲突标记为已解决(双方观点都保留)。"""
174
+ ok = c.kee.dismiss_conflict(
175
+ conflict_id, note=req.note, resolved_by=req.resolved_by
176
+ )
177
+ if not ok:
178
+ raise HTTPException(
179
+ status_code=400,
180
+ detail=f"关闭失败:冲突 {conflict_id} 不存在或已解决"
181
+ )
182
+ return {"conflict_id": conflict_id, "dismissed": True}
@@ -138,6 +138,17 @@ class ConflictResolveRequest(BaseModel):
138
138
  resolved_by: str = "human"
139
139
 
140
140
 
141
+ class ConflictDismissRequest(BaseModel):
142
+ note: str = ""
143
+ resolved_by: str = "human"
144
+
145
+
146
+ class ConflictBulkDismissRequest(BaseModel):
147
+ conflict_ids: list[str]
148
+ note: str = ""
149
+ resolved_by: str = "human"
150
+
151
+
141
152
  # ── Stats ────────────────────────────────────────────────────
142
153
 
143
154
  class StatsResponse(BaseModel):
@@ -37,6 +37,10 @@ _NEG_CJK_FALSE = (
37
37
  "非常", "非凡",
38
38
  "不过", "不仅", "不但", "不断", "不只", "不妨", "不禁", "不由", "不时", "不外乎", "不失为",
39
39
  "差不多", "差点",
40
+ # 描述性「不/无」:修饰名词或状态而非否定命题(不变/不可观测/不同/不管…),
41
+ # 是最主要的假阳性来源——技术文档几乎每句都带,却与立场无关。
42
+ "不变", "不同", "不论", "不管", "不再", "不需", "无需", "无须",
43
+ "不可观测", "不可见", "不可控", "不可用", "不可变", "不可能",
40
44
  )
41
45
  _NEG_ASCII_RE = _re.compile(r"\b(" + "|".join(sorted(_NEG_ASCII, key=len, reverse=True)) + r")\b")
42
46
  _NEGATION = _NEG_ASCII | _NEG_CJK # 供 _score_keyword 的词集匹配(word-set,ASCII 本就安全)
@@ -57,6 +61,31 @@ class _SimpleEmbedder:
57
61
  无 ST/加载失败 → 回退 v1 关键词重叠 + 否定词。接口不变。
58
62
  """
59
63
  _MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2" # 与 HKC LocalSTBackend 默认同名 → 共享一份
64
+ # 否定翻转的两个门槛,缺一不可:
65
+ # NEG_FLIP_MIN_COS —— 主题相关性(同领域才算得上对立);
66
+ # NEG_FLIP_MIN_OVERLAP —— 字面同命题(二元组 Jaccard)。
67
+ # 只用余弦时,"同领域不同命题"也会被判反向(实测 1063 张伪冲突的主因);而只用字面
68
+ # 重合又认不出换了说法的对立。两条一起过 = 讲同一件事且一方否定,才是真矛盾。
69
+ NEG_FLIP_MIN_COS = 0.35
70
+ NEG_FLIP_MIN_OVERLAP = 0.40
71
+
72
+ def __init__(self, neg_flip_min_cos: float | None = None,
73
+ neg_flip_min_overlap: float | None = None):
74
+ if neg_flip_min_cos is not None:
75
+ self.NEG_FLIP_MIN_COS = float(neg_flip_min_cos)
76
+ if neg_flip_min_overlap is not None:
77
+ self.NEG_FLIP_MIN_OVERLAP = float(neg_flip_min_overlap)
78
+
79
+ @staticmethod
80
+ def _bigrams(text: str) -> set:
81
+ t = "".join(ch for ch in (text or "").lower() if not ch.isspace())
82
+ return {t[i:i + 2] for i in range(len(t) - 1)}
83
+
84
+ def _same_proposition(self, text_a: str, text_b: str) -> bool:
85
+ ga, gb = self._bigrams(text_a), self._bigrams(text_b)
86
+ if not ga or not gb:
87
+ return False
88
+ return len(ga & gb) / len(ga | gb) >= self.NEG_FLIP_MIN_OVERLAP
60
89
 
61
90
  @staticmethod
62
91
  def _has_neg(text: str) -> bool:
@@ -81,8 +110,10 @@ class _SimpleEmbedder:
81
110
  va = _encode_one(model, self._MODEL_NAME, text_a) # 缓存,不重复编码
82
111
  vb = _encode_one(model, self._MODEL_NAME, text_b)
83
112
  cos = float(np.dot(va, vb)) # 归一化后点积=余弦 ∈ [-1,1]
84
- # 否定方向:主题高度相似但一方否定另一方不否定 → 立场相反 → 反号(矛盾)
85
- if self._has_neg(text_a) != self._has_neg(text_b) and cos > 0.35:
113
+ # 否定方向:同一命题一方否定 → 立场相反 → 反号(矛盾)
114
+ if (self._has_neg(text_a) != self._has_neg(text_b)
115
+ and cos >= self.NEG_FLIP_MIN_COS
116
+ and self._same_proposition(text_a, text_b)):
86
117
  return -abs(cos)
87
118
  return max(-1.0, min(1.0, cos))
88
119
  except Exception:
@@ -161,16 +192,23 @@ class KnowledgeEvolutionEngine:
161
192
  id_gen: IDGenerator,
162
193
  similarity_threshold: float = 0.30,
163
194
  auto_resolve_gap: float = 0.20,
195
+ neg_flip_min_cos: float | None = None,
196
+ neg_flip_min_overlap: float | None = None,
164
197
  ):
165
198
  self.store = graph_store
166
199
  self.bus = event_bus
167
200
  self.id_gen = id_gen
168
- self.embedder = _SimpleEmbedder()
201
+ self.embedder = _SimpleEmbedder(
202
+ neg_flip_min_cos=neg_flip_min_cos,
203
+ neg_flip_min_overlap=neg_flip_min_overlap,
204
+ )
169
205
  self.state_machine = ClaimStateMachine()
170
206
  # 语义方向阈值(score∈[-1,1]):|score|>threshold 判同向(合并)/反向(冲突)。
171
207
  # 默认 0.30 与既有硬编码一致(behavior-preserving);可经构造参数调严/调宽。
172
208
  self.similarity_threshold = similarity_threshold
173
209
  self.auto_resolve_gap = auto_resolve_gap
210
+ self.neg_flip_min_cos = self.embedder.NEG_FLIP_MIN_COS
211
+ self.neg_flip_min_overlap = self.embedder.NEG_FLIP_MIN_OVERLAP
174
212
 
175
213
  # open 冲突卡的 (claim_a, claim_b) 对索引:替代 _handle_conflict 里每次
176
214
  # list_conflicts 全扫去重(冲突多时 O(冲突²))。并发摄入会跨线程碰它 → 加锁。
@@ -539,3 +577,86 @@ class KnowledgeEvolutionEngine:
539
577
  }
540
578
  })
541
579
  return True
580
+
581
+ def dismiss_conflict(
582
+ self,
583
+ conflict_id: str,
584
+ note: str = "",
585
+ resolved_by: str = "human",
586
+ ) -> bool:
587
+ """按「误判」关闭一张冲突卡:不判胜负,双方 claim_status 保持原样。"""
588
+ return self.dismiss_conflicts(
589
+ [conflict_id], note=note, resolved_by=resolved_by
590
+ ) == 1
591
+
592
+ def dismiss_conflicts(
593
+ self,
594
+ conflict_ids,
595
+ note: str = "",
596
+ resolved_by: str = "human",
597
+ ) -> int:
598
+ """批量关闭误判冲突卡,返回真正关闭的张数。
599
+
600
+ 与 manual_resolve 的差别:没有 winner,也不把任何一方标成 superseded。
601
+ 唯一例外是因冲突被置为 disputed 的 claim——disputed 会被检索层剔除
602
+ (_sync_ku 分支 3),若它已不牵涉任何 open 冲突,这里恢复成 active;
603
+ 否则关掉卡片等于让这条知识永久隐身。
604
+ """
605
+ cards = []
606
+ for cid in dict.fromkeys(conflict_ids or []):
607
+ card = self.store.get_conflict(cid)
608
+ if card and card.status == ConflictStatus.OPEN:
609
+ cards.append(card)
610
+ if not cards:
611
+ return 0
612
+
613
+ note = note or "误判:双方观点并存,不构成对立"
614
+ ts = datetime.now(timezone.utc).isoformat()
615
+ touched = set()
616
+ for card in cards:
617
+ card.status = ConflictStatus.RESOLVED
618
+ card.resolved_by = resolved_by
619
+ card.resolved_at = ts
620
+ card.resolution_note = note
621
+ self.store.save_conflict(card)
622
+ self._forget_open_pair(card.claim_a_id, card.claim_b_id)
623
+ touched.update((card.claim_a_id, card.claim_b_id))
624
+ self.bus.publish({
625
+ "event": KEPEvents.CONFLICT_RESOLVED,
626
+ "source": "KEE",
627
+ "payload": {
628
+ "conflict_id": card.conflict_id,
629
+ "reason": "dismissed",
630
+ "by": resolved_by,
631
+ }
632
+ })
633
+
634
+ with self._conflict_lock:
635
+ self._ensure_open_pairs()
636
+ still_open = set(self._open_pairs or ())
637
+
638
+ restored = []
639
+ for ku_id in touched:
640
+ if any(ku_id in pair for pair in still_open):
641
+ continue
642
+ ku = self.store.get(ku_id)
643
+ if ku is None or getattr(ku, "claim_status", None) != ClaimStatus.DISPUTED:
644
+ continue
645
+ ku.claim_status = ClaimStatus.ACTIVE
646
+ if self.store.patch_ku(
647
+ ku_id, extra={"claim_status": ClaimStatus.ACTIVE.value}
648
+ ) == 0:
649
+ self.store.put(ku)
650
+ restored.append(ku_id)
651
+
652
+ if restored:
653
+ self.bus.publish({
654
+ "event": KEPEvents.KNOWLEDGE_UPDATED,
655
+ "source": "KEE",
656
+ "payload": {
657
+ "ku_ids": restored,
658
+ "reason": "conflict_dismissed_restored",
659
+ }
660
+ })
661
+ return len(cards)
662
+
@@ -125,6 +125,28 @@ class BaseClient(ABC):
125
125
  """人工裁决冲突。"""
126
126
  ...
127
127
 
128
+ @abstractmethod
129
+ def dismiss_conflict(
130
+ self,
131
+ conflict_id: str,
132
+ note: str = "",
133
+ resolved_by: str = "sdk",
134
+ ) -> bool:
135
+ """把误判冲突关闭:不判胜负,双方观点都保留。"""
136
+ ...
137
+
138
+ def dismiss_conflicts(
139
+ self,
140
+ conflict_ids: list[str],
141
+ note: str = "",
142
+ resolved_by: str = "sdk",
143
+ ) -> int:
144
+ """批量关闭误判冲突,返回真正关闭的张数。默认逐条调用。"""
145
+ return sum(
146
+ 1 for cid in conflict_ids
147
+ if self.dismiss_conflict(cid, note=note, resolved_by=resolved_by)
148
+ )
149
+
128
150
  # ── 系统 ─────────────────────────────────────────────────
129
151
 
130
152
  @abstractmethod
@@ -143,6 +143,16 @@ class DirectClient(BaseClient):
143
143
  note=note, resolved_by=resolved_by,
144
144
  )
145
145
 
146
+ def dismiss_conflict(self, conflict_id, note="", resolved_by="sdk") -> bool:
147
+ return self.c.kee.dismiss_conflict(
148
+ conflict_id=conflict_id, note=note, resolved_by=resolved_by,
149
+ )
150
+
151
+ def dismiss_conflicts(self, conflict_ids, note="", resolved_by="sdk") -> int:
152
+ return self.c.kee.dismiss_conflicts(
153
+ list(conflict_ids or []), note=note, resolved_by=resolved_by,
154
+ )
155
+
146
156
  # ── 系统 ─────────────────────────────────────────────────
147
157
 
148
158
  def stats(self) -> dict:
@@ -152,6 +152,25 @@ class HTTPClient(BaseClient):
152
152
  except HKCBadRequest:
153
153
  return False
154
154
 
155
+ def dismiss_conflict(self, conflict_id, note="", resolved_by="sdk") -> bool:
156
+ from .exceptions import HKCBadRequest
157
+ try:
158
+ data = self._request("POST", f"/conflicts/{conflict_id}/dismiss", json={
159
+ "note": note, "resolved_by": resolved_by,
160
+ })
161
+ return data.get("dismissed", False)
162
+ except HKCBadRequest:
163
+ return False
164
+
165
+ def dismiss_conflicts(self, conflict_ids, note="", resolved_by="sdk") -> int:
166
+ conflict_ids = list(conflict_ids or [])
167
+ if not conflict_ids:
168
+ return 0
169
+ data = self._request("POST", "/conflicts/bulk-dismiss", json={
170
+ "conflict_ids": conflict_ids, "note": note, "resolved_by": resolved_by,
171
+ })
172
+ return int(data.get("dismissed", 0))
173
+
155
174
  # ── 系统 ─────────────────────────────────────────────────
156
175
 
157
176
  def stats(self) -> dict:
@@ -1,123 +0,0 @@
1
- """app 配置与挂载:CORS、HKC 星球数据面、前端静态文件。
2
-
3
- 从 `foxclaw/api/__init__.py` 搬移出来的「挂载类」逻辑,均为接收 `app` 参数的纯函数,
4
- 在 `__init__.py` 创建 `app` 之后按原顺序调用,保持行为不变。
5
- """
6
- import os
7
- from pathlib import Path
8
-
9
- from fastapi import HTTPException
10
- from fastapi.middleware.cors import CORSMiddleware
11
- from fastapi.staticfiles import StaticFiles
12
- from fastapi.responses import FileResponse
13
-
14
- import foxclaw.api as _api
15
-
16
-
17
- def _configure_cors(app):
18
- # ── 可选鉴权 + CORS 收紧(部署安全)──────────────────────────
19
- # FOXCLAW_API_KEY 未设 → 开放(本地默认,保持现状,既有测试/前端不受影响)。
20
- # 设了 → 所有 /api/* 请求必须带正确的 X-API-Key,否则 401。
21
- # 注意:http 中间件**看不到 websocket scope**,故 /ws 的鉴权在 WS 端点内单独做
22
- # (见 harness_websocket,用 ?key= 查询参数)。前端(静态)与 /docs 始终开放。
23
- # 默认只放行回环来源(本地开发足够:前端与 API 是同源挂载,同源请求本就不经 CORS)。
24
- # 需要跨源时设 FOXCLAW_CORS_ORIGINS=来源1,来源2(逗号分隔);显式设 "*" 才会全开(不推荐,
25
- # 与未鉴权叠加等于把 API 交给任意网页)。收紧默认值属于「默认安全」:不再默认 allow_origins=["*"]。
26
- _cors_env = os.environ.get("FOXCLAW_CORS_ORIGINS", "").strip()
27
- if _cors_env == "*":
28
- _CORS_ORIGINS = ["*"]
29
- _CORS_ORIGIN_REGEX = None
30
- elif _cors_env:
31
- _CORS_ORIGINS = [o.strip() for o in _cors_env.split(",") if o.strip()]
32
- _CORS_ORIGIN_REGEX = None
33
- else:
34
- _CORS_ORIGINS = []
35
- _CORS_ORIGIN_REGEX = r"^https?://(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$"
36
-
37
- app.add_middleware(
38
- CORSMiddleware,
39
- allow_origins=_CORS_ORIGINS,
40
- allow_origin_regex=_CORS_ORIGIN_REGEX,
41
- allow_methods=["*"],
42
- allow_headers=["*"],
43
- )
44
-
45
-
46
- def _mount_hkc(app):
47
- # ── HKC 星球数据面:挂载 HKC 自带的 /knowledge REST 路由 ──────────────────
48
- # 星球 UI(frontend/planet)需要 /knowledge/graph、/ku/{id}、/ku/{id}/neighbors、
49
- # /ingest/*、/reset 等。HKC 在本进程装配,这里把它自带的 knowledge 路由挂上,并用
50
- # dependency_overrides 把 get_container 指向 **Foxclaw 已装配的同一个容器**(依赖倒置,
51
- # 不另开一套;保持惰性:首个 /knowledge 请求才触发 HKC 装配)。鉴权由上面中间件覆盖 /knowledge。
52
- # 缺 vendor/hkc 或导入失败 → 静默跳过(星球无数据但不影响主应用)。
53
- try:
54
- from foxclaw.knowledge import _ensure_hkc_importable as _ensure_hkc
55
- _ensure_hkc()
56
- from hkc_api.routes.knowledge import router as _hkc_knowledge_router
57
- from hkc_api.container import get_container as _hkc_get_container
58
-
59
- def _resolve_hkc_container():
60
- svc = _api._os_instance.hkc if _api._os_instance is not None else None
61
- if svc is None or not getattr(svc, "available", False):
62
- raise HTTPException(503, "HKC 未启用(未 vendor 或缺依赖)")
63
- return svc.container
64
-
65
- app.dependency_overrides[_hkc_get_container] = _resolve_hkc_container
66
- app.include_router(_hkc_knowledge_router)
67
- # /stats 系统统计:独立 hkc-api 在 main.py 顶层 @app.get("/stats") 定义,不在任何
68
- # router 里,嵌入式挂载时会漏掉 → 星球 UI 的 updateDataInfo 调 /stats 404,
69
- # 「数据目录 / 全库知识总数」提示失效。这里补挂,复用同一容器 override(惰性装配)。
70
- @app.get("/stats", tags=["system"])
71
- def _hkc_stats():
72
- return _resolve_hkc_container().stats()
73
- # 连接设置(星球 UI 左下的「连接设置」面板)POST /config/llm 做运行时 LLM 配置,
74
- # 覆盖 .env 的 HKC_LLM_*。独立 hkc-api 有这个路由,嵌入式 foxclaw 之前没挂 → 前端
75
- # 报「模型配置失败:404」且改配置无效。这里补挂 config 路由(同一容器 override 已覆盖),
76
- # 让「连接设置」的「知识抽取模型」段在 foxclaw 下真正生效。
77
- from hkc_api.routes.config import router as _hkc_config_router
78
- app.include_router(_hkc_config_router) # /config/llm(GET 读配置 / POST 改配置)
79
- # 星球副面板(冲突裁决/能力编译/最短路径)用的 HKC 路由。不挂的话 /conflicts、
80
- # /abilities、/search/path 在 Foxclaw 下 404 → 星球「冲突裁决」面板拉不到数据、
81
- # 永远显示 0(误导),能力/路径面板同样空。这里补挂(同一容器 override 已覆盖)。
82
- try:
83
- from hkc_api.routes.ability import router as _hkc_ability_router
84
- from hkc_api.routes.search import router as _hkc_search_router
85
- app.include_router(_hkc_ability_router) # /abilities, /conflicts
86
- app.include_router(_hkc_search_router) # /search/path 等
87
- _sub = "/abilities, /conflicts, /search/*"
88
- except Exception as _e2:
89
- _sub = f"(能力/搜索路由未挂: {_e2})"
90
- print(f"[Foxclaw API] ✓ HKC 星球数据面已挂载 (/knowledge/*, /config/*, {_sub})")
91
- except Exception as _e:
92
- print(f"[Foxclaw API] · HKC 星球数据面未挂载(可选): {_e}")
93
-
94
-
95
- def _mount_frontend(app):
96
- # ── 前端静态文件 ───────────────────────────────────────────
97
- # frontend/ 下现在并存三套:frontend/v3(Vue,当前主界面)、deck_v2.html(旧主界面)、
98
- # deck.html(最早的一版)。根路径必须给 v3 —— 之前一直指向 deck_v2,新版只能手输 /v3/,
99
- # 等于新界面根本没上线。
100
- frontend_dir = Path(__file__).parent.parent.parent / "frontend"
101
- if not frontend_dir.exists():
102
- return
103
-
104
- v3_index = frontend_dir / "v3" / "index.html"
105
-
106
- @app.get("/")
107
- async def serve_root():
108
- """主界面。v3 未构建(没跑 npm run build)时退回旧 deck_v2,不让根路径 404。"""
109
- if v3_index.exists():
110
- return FileResponse(str(v3_index))
111
- legacy = frontend_dir / "deck_v2.html"
112
- return FileResponse(str(legacy if legacy.exists() else frontend_dir / "deck.html"))
113
-
114
- @app.get("/classic")
115
- async def serve_classic():
116
- """旧界面(deck_v2,v5.34):新版出问题时的手动回退入口,也留着它的二级面板历史。"""
117
- return FileResponse(str(frontend_dir / "deck_v2.html"))
118
-
119
- @app.get("/old")
120
- async def serve_deck_old():
121
- return FileResponse(str(frontend_dir / "deck.html"))
122
-
123
- app.mount("/", StaticFiles(directory=str(frontend_dir), html=True), name="frontend")