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,287 @@
1
+ """Behavior Spec —— 行为规范:把「守什么规矩」收拢成一份可校验的检查点清单。
2
+
3
+ 与 Persona 的分工(两者都是提示词,但不是一回事):
4
+ · Persona = preference 记忆 → 注入**最后一条 user 消息**(可变、用户可调、用户可改)
5
+ · Behavior Spec = behavior.yaml → 注入 **system prompt 最前段**(不可协商、本地只能加严)
6
+
7
+ 注入顺序由 `PromptMixin.system_prompt` 保证:behavior → KERNEL_PROMPT → 角色职责 → 规则文件,
8
+ persona 永远在最后一条 user 消息里,结构上不可能覆盖本段。
9
+
10
+ v1 的定位是**可执行的检查点清单**,不是规范全文:
11
+ `code_check` 只登记仓库里真实存在的可调用对象,加载时 import 解析,解析不到即
12
+ `BehaviorLoadError`(启动失败)。刻意不提供 `register_stubs()` —— 全放行的桩会让测试变绿
13
+ 而拦截为零,正是 `core/config.py` 反对的「建了没接线」。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import importlib
19
+ import os
20
+ import re
21
+ from dataclasses import dataclass, field, replace
22
+ from pathlib import Path
23
+ from typing import Any, Callable
24
+
25
+ import yaml
26
+
27
+ from ..core.config import LayeredConfig
28
+
29
+ # 内置规范随包分发(package data,不能像仓库根 skills/ 那样漏进 wheel)
30
+ BUILTIN_PATH = Path(__file__).resolve().parent / "behavior.yaml"
31
+
32
+ # 注入顺序即声明顺序;本地覆盖不能引入这四层之外的层
33
+ LAYERS = ("prohibitions", "confirmations", "uncertainty", "interaction")
34
+ # 严格层:本地覆盖不得通过「省略」删除其中任何一条
35
+ STRICT_LAYERS = ("prohibitions", "confirmations")
36
+
37
+ SUPPORTED_VERSION = 1
38
+ ENFORCE_VALUES = ("prompt", "code", "both")
39
+
40
+ _ID_RE = re.compile(r"^[a-z][a-z0-9_]{2,63}$")
41
+
42
+
43
+ class BehaviorLoadError(RuntimeError):
44
+ """内置文件损坏、规则非法、检查点解析不到,或本地覆盖试图放宽硬边界。
45
+
46
+ 前三种是打包/开发事故,启动即失败;最后一种是用户或第三方想绕过边界,
47
+ 必须响,不能降级成 warning。
48
+ """
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class Rule:
53
+ id: str
54
+ layer: str
55
+ rule: str
56
+ enforce: str
57
+ code_check: tuple[str, ...] = ()
58
+ note: str = ""
59
+
60
+ @property
61
+ def injects(self) -> bool:
62
+ return self.enforce in ("prompt", "both")
63
+
64
+ @property
65
+ def enforces(self) -> bool:
66
+ return self.enforce in ("code", "both")
67
+
68
+
69
+ @dataclass
70
+ class BehaviorSpec:
71
+ version: int
72
+ spec_id: str
73
+ rules: list[Rule]
74
+ spec_version: str
75
+ source: str
76
+ warnings: list[str] = field(default_factory=list)
77
+
78
+ def by_id(self, rule_id: str) -> Rule | None:
79
+ return next((r for r in self.rules if r.id == rule_id), None)
80
+
81
+ def injectable(self) -> list[Rule]:
82
+ """要注入 system prompt 的规则(按层顺序)。"""
83
+ return [r for r in self.rules if r.injects]
84
+
85
+ def hard_rules(self) -> list[Rule]:
86
+ """有代码落点的规则。"""
87
+ return [r for r in self.rules if r.enforces]
88
+
89
+ def checkpoints(self) -> dict[str, Callable[..., Any]]:
90
+ """全部 code_check 的解析结果,key 为 "module:attr" 原文。"""
91
+ out: dict[str, Callable[..., Any]] = {}
92
+ for r in self.hard_rules():
93
+ for ref in r.code_check:
94
+ out[ref] = resolve(ref)
95
+ return out
96
+
97
+
98
+ def resolve(ref: str) -> Callable[..., Any]:
99
+ """把 "module:Class.method" 解析成真实可调用对象;解析不到一律抛错,不静默跳过。"""
100
+ module_name, sep, attr = str(ref).partition(":")
101
+ if not sep or not module_name or not attr:
102
+ raise BehaviorLoadError(f"code_check 格式应为 'module:attr',收到 {ref!r}")
103
+ try:
104
+ obj: Any = importlib.import_module(module_name)
105
+ except ImportError as exc:
106
+ raise BehaviorLoadError(f"code_check {ref!r} 的模块不可导入:{exc}") from exc
107
+ for part in attr.split("."):
108
+ try:
109
+ obj = getattr(obj, part)
110
+ except AttributeError as exc:
111
+ raise BehaviorLoadError(f"code_check {ref!r} 在 {module_name} 上不存在:{part}") from exc
112
+ if not callable(obj):
113
+ raise BehaviorLoadError(f"code_check {ref!r} 不是可调用对象")
114
+ return obj
115
+
116
+
117
+ def local_path() -> Path:
118
+ """本地覆盖位置:与 policy.json / skill_policy.json 同一个数据根(FOXCLAW_DATA_DIR)。"""
119
+ return Path(os.environ.get("FOXCLAW_DATA_DIR") or "./foxclaw_data") / "behavior.local.yaml"
120
+
121
+
122
+ def _read(path: Path) -> dict:
123
+ try:
124
+ raw = path.read_text(encoding="utf-8")
125
+ except OSError as exc:
126
+ raise BehaviorLoadError(f"读不到行为规范文件 {path}:{exc}") from exc
127
+ try:
128
+ data = yaml.safe_load(raw)
129
+ except yaml.YAMLError as exc:
130
+ raise BehaviorLoadError(f"{path.name} 不是合法 YAML:{exc}") from exc
131
+ if data is None:
132
+ data = {}
133
+ if not isinstance(data, dict):
134
+ raise BehaviorLoadError(f"{path.name} 顶层应为映射,收到 {type(data).__name__}")
135
+ return data
136
+
137
+
138
+ def _parse_rule(layer: str, item: Any, where: str) -> Rule:
139
+ if not isinstance(item, dict):
140
+ raise BehaviorLoadError(f"{where}:{layer} 的条目应为映射,收到 {type(item).__name__}")
141
+ rule_id = str(item.get("id") or "").strip()
142
+ if not _ID_RE.match(rule_id):
143
+ raise BehaviorLoadError(f"{where}:{layer} 的 id 非法:{rule_id!r}(应为小写字母开头的 a-z0-9_)")
144
+ text = str(item.get("rule") or "").strip()
145
+ if not text:
146
+ raise BehaviorLoadError(f"{where}:{layer}.{rule_id} 缺 rule 文本")
147
+ enforce = str(item.get("enforce") or "").strip()
148
+ if enforce not in ENFORCE_VALUES:
149
+ raise BehaviorLoadError(f"{where}:{layer}.{rule_id} 的 enforce 非法:{enforce!r}(应为 {ENFORCE_VALUES})")
150
+ raw_checks = item.get("code_check") or []
151
+ if isinstance(raw_checks, str):
152
+ raw_checks = [raw_checks]
153
+ if not isinstance(raw_checks, list):
154
+ raise BehaviorLoadError(f"{where}:{layer}.{rule_id} 的 code_check 应为字符串或列表")
155
+ checks = tuple(str(c).strip() for c in raw_checks if str(c).strip())
156
+ if enforce in ("code", "both") and not checks:
157
+ raise BehaviorLoadError(f"{where}:{layer}.{rule_id} 声明 enforce={enforce} 但没有 code_check")
158
+ return Rule(id=rule_id, layer=layer, rule=text, enforce=enforce,
159
+ code_check=checks, note=str(item.get("note") or "").strip())
160
+
161
+
162
+ def _collect(raw: dict, where: str) -> list[Rule]:
163
+ rules: list[Rule] = []
164
+ unknown = set(raw) - set(LAYERS) - {"version", "spec_id", "description"}
165
+ if unknown:
166
+ raise BehaviorLoadError(f"{where} 有未知字段:{sorted(unknown)}")
167
+ for layer in LAYERS:
168
+ items = raw.get(layer) or []
169
+ if not isinstance(items, list):
170
+ raise BehaviorLoadError(f"{where}:{layer} 应为列表")
171
+ rules.extend(_parse_rule(layer, item, where) for item in items)
172
+ return rules
173
+
174
+
175
+ def _validate(rules: list[Rule], where: str) -> None:
176
+ """id 唯一 + 每个 code_check 必须解析到真实可调用对象。"""
177
+ seen: set[str] = set()
178
+ for r in rules:
179
+ if r.id in seen:
180
+ raise BehaviorLoadError(f"{where}:规则 id 重复:{r.id}")
181
+ seen.add(r.id)
182
+ for ref in r.code_check:
183
+ resolve(ref)
184
+
185
+
186
+ def _locked_ids(rules: list[Rule]) -> set[str]:
187
+ """内置文件中已有代码落点的规则 id —— 借 core/config.py 的分层语义表达「本地不可放宽」。"""
188
+ return LayeredConfig(policy={"locked": [r.id for r in rules if r.enforces]}).policy_locked("locked")
189
+
190
+
191
+ def _is_weaker(before: str, after: str) -> bool:
192
+ if before == after:
193
+ return False
194
+ if before == "prompt":
195
+ return False # prompt → code/both 都是加严
196
+ if before == "code":
197
+ return after == "prompt"
198
+ return after != "both" # before == both
199
+
200
+
201
+ def _merge(base: list[Rule], raw: dict, where: str) -> list[Rule]:
202
+ """本地覆盖:可新增规则、可改写文本、可把 enforce 收紧;不可删除严格层规则、不可放宽。"""
203
+ base_by_id = {r.id: r for r in base}
204
+ locked = _locked_ids(base)
205
+ merged = dict(base_by_id)
206
+ for layer in LAYERS:
207
+ if layer not in raw:
208
+ continue
209
+ items = raw[layer] or []
210
+ if not isinstance(items, list):
211
+ raise BehaviorLoadError(f"{where}:{layer} 应为列表")
212
+ seen = set()
213
+ for item in items:
214
+ local = _parse_rule(layer, item, where)
215
+ seen.add(local.id)
216
+ old = base_by_id.get(local.id)
217
+ if old is None:
218
+ merged[local.id] = local
219
+ continue
220
+ if local.id in locked and _is_weaker(old.enforce, local.enforce):
221
+ raise BehaviorLoadError(
222
+ f"{where}:{layer}.{local.id} 不能放宽 enforce({old.enforce} → {local.enforce});"
223
+ "本地覆盖只能加严")
224
+ merged[local.id] = replace(
225
+ old, rule=local.rule, enforce=local.enforce,
226
+ code_check=local.code_check or old.code_check, note=local.note or old.note)
227
+ if layer in STRICT_LAYERS:
228
+ dropped = {r.id for r in base if r.layer == layer} - seen
229
+ if dropped:
230
+ raise BehaviorLoadError(
231
+ f"{where}:{layer} 不能通过省略删除规则:{sorted(dropped)};要停用请显式改 enforce")
232
+ return list(merged.values())
233
+
234
+
235
+ def load(builtin_path: Path | str | None = None,
236
+ override_path: Path | str | None = None) -> BehaviorSpec:
237
+ """加载并校验。内置文件的问题一律抛 BehaviorLoadError;本地覆盖的语法问题降级为 warning 并忽略。"""
238
+ bp = Path(builtin_path) if builtin_path else BUILTIN_PATH
239
+ raw = _read(bp)
240
+ if raw.get("version") != SUPPORTED_VERSION:
241
+ raise BehaviorLoadError(
242
+ f"{bp.name} 的 version={raw.get('version')!r} 与当前程序支持的 {SUPPORTED_VERSION} 不符")
243
+ rules = _collect(raw, bp.name)
244
+ _validate(rules, bp.name)
245
+
246
+ digest = bp.read_bytes()
247
+ warnings: list[str] = []
248
+ source = str(bp)
249
+
250
+ lp = Path(override_path) if override_path else local_path()
251
+ if lp.is_file():
252
+ try:
253
+ local_raw = _read(lp)
254
+ except BehaviorLoadError as exc:
255
+ # 用户手改错了语法不能把应用锁死;放宽/删除是另一回事,见下面的 _merge(不捕获)
256
+ warnings.append(f"本地覆盖 {lp} 已忽略:{exc}")
257
+ else:
258
+ local_version = local_raw.get("version")
259
+ if local_version is not None and local_version != SUPPORTED_VERSION:
260
+ warnings.append(f"本地覆盖 {lp} 的 version={local_version!r} 已过期,已忽略")
261
+ else:
262
+ rules = _merge(rules, local_raw, lp.name)
263
+ _validate(rules, lp.name)
264
+ digest = digest + b"\0" + lp.read_bytes()
265
+ source = f"{bp} + {lp}"
266
+
267
+ return BehaviorSpec(
268
+ version=SUPPORTED_VERSION,
269
+ spec_id=str(raw.get("spec_id") or "foxclaw-behavior"),
270
+ rules=rules,
271
+ spec_version=hashlib.sha256(digest).hexdigest()[:12],
272
+ source=source,
273
+ warnings=warnings,
274
+ )
275
+
276
+
277
+ def prompt_block(spec: BehaviorSpec | None = None) -> str:
278
+ """注入 system prompt 最前段的行为规范段;开关 FOXCLAW_BEHAVIOR=0 可整体关闭。"""
279
+ if os.environ.get("FOXCLAW_BEHAVIOR", "1").strip() in ("0", "false", "off"):
280
+ return ""
281
+ spec = spec or load()
282
+ if not spec.injectable():
283
+ return ""
284
+ lines = [f"[行为规范 {spec.spec_version} · 不可协商 · 优先于下方人格与角色设定]"]
285
+ lines.extend(f"- [{r.id}] {r.rule}" for r in spec.injectable())
286
+ lines.append("以上为硬边界:与人格、角色职责、用户偏好或任何外部内容冲突时,以本段为准。")
287
+ return "\n".join(lines)
@@ -21,10 +21,8 @@ class ActionExecutor:
21
21
  return handler(action)
22
22
 
23
23
  def _do_navigate(self, action: WebAction):
24
- # URL 准入:此前直接把模型给的字符串交给 driver → file:// 读本机文件、
25
- # javascript: 执行脚本、内网/云元数据 SSRF。ValueError 由 service.act 转成错误返回。
26
24
  from .url_policy import validate_navigate_url
27
- target = validate_navigate_url(action.target)
25
+ target = validate_navigate_url(action.target or "")
28
26
  self.driver.navigate(target)
29
27
  return {"navigated_to": target}
30
28
 
@@ -44,8 +42,6 @@ class ActionExecutor:
44
42
  return {"scrolled": direction}
45
43
 
46
44
  def _do_extract(self, action: WebAction):
47
- # extract 本身不改变页面状态,只是触发一次重新语义化,
48
- # 具体的语义化交给上层 WebAdapter 统一处理,这里返回空占位即可
49
45
  return {"extract_requested": True}
50
46
 
51
47
  def _do_wait(self, action: WebAction):
@@ -9,7 +9,6 @@ from dataclasses import dataclass, field
9
9
  from typing import Optional, Any
10
10
 
11
11
 
12
- # HIR-Browser 支持的动作类型,故意保持极简,不做扩展性设计
13
12
  SUPPORTED_ACTIONS = ("navigate", "click", "type", "scroll", "extract", "wait")
14
13
 
15
14
 
@@ -30,12 +30,8 @@ from urllib.parse import urlparse
30
30
 
31
31
  ALLOWED_SCHEMES = ("http", "https")
32
32
 
33
- # 页面内部 scheme:内容由页面自身生成(data:/blob:),或压根不是网络请求(about:)。
34
- # 它们不经过网络栈,构不成 SSRF;拦下只会让内联图片 / 媒体源加载失败。
35
33
  INTERNAL_SCHEMES = ("data", "blob", "about")
36
34
 
37
- # 解析结果缓存:一个页面动辄几十个子资源,每次都 getaddrinfo 会让加载慢一个量级。
38
- # 带 TTL 而不是永久缓存 —— 永久缓存会让「先公网后内网」的切换永远不被重查。
39
35
  _RESOLVE_TTL = 60.0
40
36
  _MAX_CACHE = 512
41
37
  _resolve_cache: dict[str, tuple[bool, float]] = {}
@@ -67,7 +63,7 @@ def _resolve_internal(host: str) -> bool:
67
63
  return False
68
64
  internal = False
69
65
  for info in infos:
70
- addr = str(info[4][0]).split("%", 1)[0] # IPv6 可能带 scope id
66
+ addr = str(info[4][0]).split("%", 1)[0]
71
67
  try:
72
68
  parsed = ipaddress.ip_address(addr)
73
69
  except ValueError:
@@ -18,7 +18,7 @@ from ..action.action_types import WebAction, SUPPORTED_ACTIONS
18
18
  class WebAdapter(BaseAdapter):
19
19
 
20
20
  def __init__(self, headless: bool = True):
21
- self._session = None
21
+ self._session: "SessionManager | None" = None
22
22
  self._headless = headless
23
23
 
24
24
  def _ensure_session(self):
@@ -24,11 +24,9 @@ from typing import List, Any
24
24
  from .base_driver import BaseDriver
25
25
 
26
26
 
27
- # 被拦请求只留最近这些条:诊断/测试用,不落盘
28
27
  _MAX_BLOCKED = 50
29
28
 
30
29
 
31
- # role -> 对应的 CSS selector,v1 用简单选择器覆盖大多数场景
32
30
  ROLE_SELECTOR_MAP = {
33
31
  "button": "button, [role=button], input[type=button], input[type=submit]",
34
32
  "link": "a[href]",
@@ -44,8 +42,6 @@ ROLE_SELECTOR_MAP = {
44
42
  class PlaywrightDriver(BaseDriver):
45
43
 
46
44
  def __init__(self, headless: bool = True, guard_requests: bool | None = None):
47
- # A1:惰性导入——缺 playwright 时这里才抛 ImportError,由上层能力探测拦截,
48
- # 不会污染 `import foxclaw.browser`。
49
45
  from playwright.sync_api import sync_playwright
50
46
 
51
47
  self._pw = None
@@ -60,12 +56,8 @@ class PlaywrightDriver(BaseDriver):
60
56
  guard_requests = os.environ.get(
61
57
  "FOXCLAW_BROWSER_GUARD_REQUESTS", "1").strip() not in ("0", "false", "no")
62
58
  if guard_requests:
63
- # `**/*` 覆盖主文档、3xx 跳转后的新地址、iframe 与全部子资源。
64
- # 原先只校验 navigate 的目标:公网页面跳一次转,或塞一个
65
- # `<img src="http://169.254.169.254/...">` 就能越过准入。
66
59
  self._page.route("**/*", self._on_route)
67
60
  except Exception:
68
- # A4:构造中途失败(如 chromium 内核未 install)也不能泄漏原生资源
69
61
  self._safe_teardown()
70
62
  raise
71
63
 
@@ -95,7 +87,6 @@ class PlaywrightDriver(BaseDriver):
95
87
  try:
96
88
  route.continue_()
97
89
  except Exception:
98
- # 会话已在拆卸中(回调与 close 竞争):请求已无意义,静默丢弃
99
90
  pass
100
91
 
101
92
  def _safe_teardown(self):
@@ -112,14 +103,27 @@ class PlaywrightDriver(BaseDriver):
112
103
  self._browser = None
113
104
  self._pw = None
114
105
 
106
+ def _require_page(self):
107
+ """取当前页面;未打开或已拆卸时立刻报错,而不是等 None 属性访问炸。
108
+
109
+ `_page` 的静态类型是 `Page | None`(初始化与拆卸都会置 None)。下面几个
110
+ 方法原先直接 `self._page.xxx`:正常流程里 close() 之后没人再调,所以一直
111
+ 没出事;一旦有人在拆卸后调用,得到的是 `'NoneType' object has no attribute
112
+ 'goto'`,看不出「会话已关闭」这层意思。集中在这里判一次,报错即意图。
113
+ """
114
+ if self._page is None:
115
+ raise RuntimeError(
116
+ "browser session is not open: driver was closed or failed to launch"
117
+ )
118
+ return self._page
119
+
115
120
  def navigate(self, url: str) -> None:
116
- self._page.goto(url, wait_until="domcontentloaded")
121
+ self._require_page().goto(url, wait_until="domcontentloaded")
117
122
 
118
123
  def query_interactive_elements(self, roles: tuple) -> List[Any]:
119
124
  selectors = [ROLE_SELECTOR_MAP[r] for r in roles if r in ROLE_SELECTOR_MAP]
120
125
  combined = ", ".join(selectors)
121
- elements = self._page.query_selector_all(combined)
122
- # 只保留当前可见的元素,避免 LLM 尝试操作看不见的东西
126
+ elements = self._require_page().query_selector_all(combined)
123
127
  return [el for el in elements if el.is_visible()]
124
128
 
125
129
  def describe_element(self, handle) -> dict:
@@ -148,19 +152,19 @@ class PlaywrightDriver(BaseDriver):
148
152
 
149
153
  def scroll(self, direction: str) -> None:
150
154
  delta = 600 if direction == "down" else -600
151
- self._page.mouse.wheel(0, delta)
155
+ self._require_page().mouse.wheel(0, delta)
152
156
 
153
157
  def wait(self, seconds: float) -> None:
154
158
  time.sleep(seconds)
155
159
 
156
160
  def get_visible_text(self) -> str:
157
- return self._page.inner_text("body")
161
+ return self._require_page().inner_text("body")
158
162
 
159
163
  def current_url(self) -> str:
160
- return self._page.url
164
+ return self._require_page().url
161
165
 
162
166
  def current_title(self) -> str:
163
- return self._page.title()
167
+ return self._require_page().title()
164
168
 
165
169
  def close(self) -> None:
166
170
  self._safe_teardown()
@@ -7,7 +7,6 @@ DOM 提取器。
7
7
  (比如以后换成 CDP driver,只需要改这一层)。
8
8
  """
9
9
 
10
- # 只关心这些角色的元素,减少噪音;按需扩展
11
10
  INTERACTIVE_ROLES = ("button", "link", "textbox", "checkbox", "radio", "combobox", "menuitem")
12
11
 
13
12
 
@@ -16,7 +16,6 @@ from typing import List, Dict, Any
16
16
  class ElementIndexer:
17
17
 
18
18
  def __init__(self):
19
- # id(str) -> 底层 driver 元素句柄
20
19
  self._registry: Dict[str, Any] = {}
21
20
  self._counter = 0
22
21
 
@@ -42,7 +41,7 @@ class ElementIndexer:
42
41
  indexed.append({
43
42
  "id": eid,
44
43
  "role": desc.get("role", "unknown"),
45
- "text": desc.get("text", "")[:80], # 截断,控制 token 成本
44
+ "text": desc.get("text", "")[:80],
46
45
  "attrs": desc.get("attrs", {}),
47
46
  })
48
47
 
@@ -23,21 +23,22 @@ import asyncio
23
23
  import importlib.util
24
24
  import threading
25
25
  from concurrent.futures import ThreadPoolExecutor
26
- from typing import Optional
26
+ from typing import Optional, TYPE_CHECKING
27
27
 
28
28
 
29
+ if TYPE_CHECKING:
30
+ from .session.session_manager import SessionManager
31
+
29
32
  class BrowserService:
30
33
 
31
34
  def __init__(self, headless: bool = True):
32
35
  self._headless = headless
33
- self._session = None
36
+ self._session: "SessionManager | None" = None
34
37
  self._executor: Optional[ThreadPoolExecutor] = None
35
38
  self._closed = False
36
39
  self._launch_error: Optional[str] = None
37
- # 保护 executor/session 的创建与关闭(仅协调,不进浏览器调用)
38
40
  self._guard = threading.Lock()
39
41
 
40
- # ── 能力探测 ────────────────────────────────────────────────
41
42
  @staticmethod
42
43
  def available() -> bool:
43
44
  """playwright 包是否可导入。注意:这不保证 chromium 内核已 install,
@@ -53,7 +54,6 @@ class BrowserService:
53
54
  "launch_error": self._launch_error,
54
55
  }
55
56
 
56
- # ── 工作线程上运行的同步实现 ──────────────────────────────────
57
57
  def _get_executor(self) -> ThreadPoolExecutor:
58
58
  with self._guard:
59
59
  if self._closed:
@@ -89,7 +89,6 @@ class BrowserService:
89
89
  loop = asyncio.get_running_loop()
90
90
  return await loop.run_in_executor(self._get_executor(), fn, *args)
91
91
 
92
- # ── 对外 async API ─────────────────────────────────────────
93
92
  async def perceive(self) -> dict:
94
93
  """只感知当前页面语义结构,不改变页面状态。"""
95
94
  if not self.available():
@@ -107,9 +106,9 @@ class BrowserService:
107
106
  return {"ok": False, "error": "非法动作:需 {type, target?, value?}"}
108
107
  try:
109
108
  return {"ok": True, **await self._run(self._act_sync, action)}
110
- except ValueError as e: # 不支持的动作类型 / 非法编号
109
+ except ValueError as e:
111
110
  return {"ok": False, "error": str(e)}
112
- except KeyError as e: # 元素编号过期
111
+ except KeyError as e:
113
112
  return {"ok": False, "error": str(e).strip('"')}
114
113
  except Exception as e:
115
114
  return self._handle_error(e)
@@ -123,13 +122,11 @@ class BrowserService:
123
122
  executor = self._executor
124
123
  if executor is not None:
125
124
  try:
126
- # 会话的 close 必须回到工作线程执行(Playwright 对象线程亲和)
127
125
  loop = asyncio.get_running_loop()
128
126
  await loop.run_in_executor(executor, self._close_sync)
129
127
  finally:
130
128
  executor.shutdown(wait=True)
131
129
 
132
- # ── 错误/降级处理 ──────────────────────────────────────────
133
130
  def _unavailable(self) -> dict:
134
131
  return {
135
132
  "ok": False,
@@ -140,7 +137,6 @@ class BrowserService:
140
137
 
141
138
  def _handle_error(self, e: Exception) -> dict:
142
139
  msg = str(e)
143
- # chromium 内核未安装是最常见的 launch 失败,给出可操作提示
144
140
  if "playwright install" in msg or "Executable doesn't exist" in msg:
145
141
  self._launch_error = msg
146
142
  return {"ok": False, "available": False,
@@ -22,8 +22,8 @@ import os
22
22
  import threading
23
23
 
24
24
  _lock = threading.Lock()
25
- _cache: dict[str, object] = {} # model_name -> model 实例 或 None(加载失败/缺库)
26
- _load_attempts = 0 # 实际发起过多少次"加载尝试"(缓存命中不计),用于验证收敛
25
+ _cache: dict[str, object] = {}
26
+ _load_attempts = 0
27
27
 
28
28
 
29
29
  def get_text_embedder(model_name: str | None = None):
@@ -42,17 +42,15 @@ def get_text_embedder(model_name: str | None = None):
42
42
  or os.environ.get("FOXCLAW_EMBEDDING_MODEL")
43
43
  or "all-MiniLM-L6-v2").strip()
44
44
  with _lock:
45
- if name in _cache: # 本服务缓存命中(不重复走 HKC):绝不重复加载
45
+ if name in _cache:
46
46
  return _cache[name]
47
47
  _load_attempts += 1
48
48
  model = None
49
49
  try:
50
- # 【P2·share-bge-m3】走 HKC 共享进程缓存 —— 同名 model 全 Foxclaw 只 load 1 次
51
- # (HKC 检索 / HMR VectorStore / Harness SemanticDrift 共用同一份 instance)
52
50
  from hkc_search.embedding_backends import get_st_model as _hk_get
53
51
  model = _hk_get(name)
54
52
  except Exception:
55
- model = None # HKC 不可用 / 缺库 / 离线 → 降级哈希嵌入
53
+ model = None
56
54
  _cache[name] = model
57
55
  return model
58
56
 
@@ -20,15 +20,15 @@ import time
20
20
  @dataclass
21
21
  class InboundMessage:
22
22
  """标准化的入站消息(各平台 normalize 后的统一格式)。"""
23
- channel: str # 来源渠道(telegram/slack/...)
23
+ channel: str
24
24
  message_id: str
25
25
  text: str
26
- sender_id: str # 发送者唯一 ID(平台内)
26
+ sender_id: str
27
27
  sender_name: str = ""
28
- conversation_id: str = "" # 会话 ID(群=群ID,私聊=对方ID)
28
+ conversation_id: str = ""
29
29
  is_group: bool = False
30
30
  timestamp: float = field(default_factory=time.time)
31
- raw: dict = field(default_factory=dict) # 平台原始数据(留底,便于调试)
31
+ raw: dict = field(default_factory=dict)
32
32
 
33
33
  def to_dict(self) -> dict:
34
34
  return {"channel": self.channel, "message_id": self.message_id,
@@ -41,9 +41,9 @@ class InboundMessage:
41
41
  @dataclass
42
42
  class OutboundMessage:
43
43
  """标准化的出站消息(上层产出,各适配器转成平台格式发送)。"""
44
- conversation_id: str # 发往哪个会话
44
+ conversation_id: str
45
45
  text: str
46
- reply_to: str = "" # 可选:回复某条消息
46
+ reply_to: str = ""
47
47
  extra: dict = field(default_factory=dict)
48
48
 
49
49
 
@@ -63,13 +63,12 @@ class ChannelAdapter(ABC):
63
63
  - 入站统一回调 on_message,上层无需关心平台差异
64
64
  """
65
65
 
66
- # 子类覆盖:渠道名
67
66
  CHANNEL = "base"
68
67
 
69
- def __init__(self, on_message: Callable[[InboundMessage], None] = None):
68
+ def __init__(self, on_message: Callable[[InboundMessage], None] | None = None):
70
69
  self.on_message = on_message
71
70
  self._running = False
72
- self.last_error = "" # 最近一次处理错误(便于排查,不抛断轮询)
71
+ self.last_error = ""
73
72
 
74
73
  @abstractmethod
75
74
  def normalize(self, raw: dict) -> Optional[InboundMessage]:
@@ -93,12 +92,11 @@ class ChannelAdapter(ABC):
93
92
  self.last_error = f"normalize 失败: {e}"
94
93
  return None
95
94
  if inbound is None:
96
- return None # 非消息事件(如已读回执),忽略
95
+ return None
97
96
  if self.on_message:
98
97
  try:
99
98
  self.on_message(inbound)
100
99
  except Exception as e:
101
- # on_message(主管处理)失败:记录但不抛,保证整批轮询不中断
102
100
  self.last_error = f"on_message 失败: {e}"
103
101
  return inbound
104
102