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
@@ -18,22 +18,20 @@ import re
18
18
  from dataclasses import dataclass, field
19
19
  from typing import Optional
20
20
 
21
- from ..planning import planner as P # 统一规划原语(P1-2:否定/拆分/打分单一真源)
21
+ from ..planning import planner as P
22
22
 
23
23
 
24
- # ============ skill-router:委托统一引擎,候选空间=skill ============
25
-
26
24
  @dataclass
27
25
  class RouteResult:
28
- primary: Optional[str] = None # 选中的主 skill id
26
+ primary: Optional[str] = None
29
27
  confidence: float = 0.0
30
- candidates: list = field(default_factory=list) # [(skill_id, score)]
28
+ candidates: list = field(default_factory=list)
31
29
  fallback: Optional[str] = None
32
30
  needs_composition: bool = False
33
31
  reason: str = ""
34
32
 
35
33
 
36
- def route(task: str, registry, fallback_id: str = None) -> RouteResult:
34
+ def route(task: str, registry, fallback_id: str | None = None) -> RouteResult:
37
35
  """skill-router:把 skill 适配成通用候选,交统一引擎 trigger_route 打分选择。
38
36
  算法(否定过滤触发命中 0.8 + 描述领域匹配 0.2)现由 foxclaw.planning 单一真源提供。"""
39
37
  skills = registry.all()
@@ -50,8 +48,6 @@ def route(task: str, registry, fallback_id: str = None) -> RouteResult:
50
48
  needs_composition=r.needs_composition, reason=r.reason)
51
49
 
52
50
 
53
- # ============ planner:任务拆解 ============
54
-
55
51
  @dataclass
56
52
  class PlanTask:
57
53
  id: str
@@ -63,9 +59,9 @@ class PlanTask:
63
59
  @dataclass
64
60
  class Plan:
65
61
  intent: str
66
- tasks: list = field(default_factory=list) # [PlanTask]
67
- flow_type: str = "sequential" # sequential/parallel/hybrid
68
- steps: list = field(default_factory=list) # 执行顺序 [task_id]
62
+ tasks: list = field(default_factory=list)
63
+ flow_type: str = "sequential"
64
+ steps: list = field(default_factory=list)
69
65
  needs_input: bool = False
70
66
  missing: list = field(default_factory=list)
71
67
 
@@ -95,7 +91,6 @@ def plan(task: str, registry) -> Plan:
95
91
  for i, p in enumerate(parts, 1):
96
92
  r = route(p, registry)
97
93
  tid = f"T{i}"
98
- # 顺序:依赖前一步;并行:无依赖(可同时 fork)
99
94
  deps = [f"T{i-1}"] if (i > 1 and flow == "sequential") else []
100
95
  tasks.append(PlanTask(id=tid, name=p, suggested_skill=r.primary,
101
96
  dependencies=deps))
@@ -104,8 +99,6 @@ def plan(task: str, registry) -> Plan:
104
99
  return Plan(intent=task, tasks=tasks, flow_type=flow, steps=steps)
105
100
 
106
101
 
107
- # ============ composition:多 skill 编排执行 ============
108
-
109
102
  @dataclass
110
103
  class StageResult:
111
104
  stage: int
@@ -116,16 +109,14 @@ class StageResult:
116
109
  @dataclass
117
110
  class CompositionResult:
118
111
  mode: str
119
- stages: list = field(default_factory=list) # [StageResult]
112
+ stages: list = field(default_factory=list)
120
113
  final_output: dict = field(default_factory=dict)
121
114
  failed: list = field(default_factory=list)
122
115
  rolled_back: bool = False
123
- rollback_clean: bool = True # 回滚是否干净完成(无补偿失败/无需人工)
124
- rollback_log: list = field(default_factory=list) # 回滚记录 [{skill,status,detail}]
125
- needs_manual: list = field(default_factory=list) # 需人工处理的 skill(无补偿/补偿失败)
126
- # 【P2-2】待确认状态:遇到 needs_confirmation 时填充(不回滚),供调用方展示+断点续跑
127
- pending_confirmation: dict = None # {task_id,skill_id,stage,message,args,completed_steps,resume_at}
128
- # 【P2-2】已完成步骤(断点续跑所需的现场): [(task_id, skill_id, output)]
116
+ rollback_clean: bool = True
117
+ rollback_log: list = field(default_factory=list)
118
+ needs_manual: list = field(default_factory=list)
119
+ pending_confirmation: Optional[dict] = None
129
120
  completed_steps: list = field(default_factory=list)
130
121
 
131
122
  @property
@@ -148,16 +139,13 @@ def _build_args(skill, task_text: str, upstream: dict) -> dict:
148
139
  params = skill.params or {}
149
140
  if not params:
150
141
  return {}
151
- # 计算器类:从文本提取数学表达式
152
142
  if "expression" in params:
153
143
  m = re.search(r"[-+*/().\d\s]+", task_text)
154
144
  expr = (m.group(0).strip() if m else task_text)
155
145
  return {"expression": expr or task_text}
156
- # 单参数 skill:把整段任务文本作为该参数
157
146
  if len(params) == 1:
158
147
  only = next(iter(params))
159
148
  return {only: task_text}
160
- # 多参数:传 task,让下游(或 LLM)解析;附带上游
161
149
  return {"task": task_text, "upstream_outputs": dict(upstream)}
162
150
 
163
151
 
@@ -174,7 +162,7 @@ def _rollback(executed, registry, res: CompositionResult):
174
162
  continue
175
163
  side_effect = getattr(skill, "side_effect", "read")
176
164
  if side_effect == "read":
177
- continue # 只读操作无副作用,无需回滚
165
+ continue
178
166
  comp = getattr(skill, "compensate", None)
179
167
  if comp is not None:
180
168
  try:
@@ -184,12 +172,12 @@ def _rollback(executed, registry, res: CompositionResult):
184
172
  except Exception as e:
185
173
  res.rollback_log.append({"skill": sid, "status": "compensate_failed",
186
174
  "detail": f"补偿失败: {e}"})
187
- res.rollback_clean = False # 补偿失败 → 回滚不干净
175
+ res.rollback_clean = False
188
176
  res.needs_manual.append(sid)
189
177
  else:
190
178
  res.rollback_log.append({"skill": sid, "status": "manual_required",
191
179
  "detail": f"{side_effect} 操作无补偿动作,需人工处理"})
192
- res.rollback_clean = False # 有副作用却无法补偿 → 不干净
180
+ res.rollback_clean = False
193
181
  res.needs_manual.append(sid)
194
182
 
195
183
 
@@ -212,7 +200,7 @@ def _invoke(adapter, skill, args: dict, confirmed: bool, confirm_nonce: str):
212
200
 
213
201
 
214
202
  def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
215
- resume_state: dict = None) -> CompositionResult:
203
+ resume_state: dict | None = None) -> CompositionResult:
216
204
  """skill-composition:按计划顺序执行各 skill。
217
205
 
218
206
  结束态三种:
@@ -226,10 +214,9 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
226
214
  恢复已完成步骤的 upstream/executed 现场,跳过 resume_at 之前的步骤。
227
215
  """
228
216
  res = CompositionResult(mode=plan_obj.flow_type)
229
- upstream = {} # skill_id -> output(供下游读取)
230
- executed = [] # [(skill_id, output)] 按执行顺序,供回滚
217
+ upstream = {}
218
+ executed = []
231
219
 
232
- # 【P2-2】恢复已完成步骤(断点续跑)
233
220
  resume_at = None
234
221
  if resume_state:
235
222
  for tid, sid, output in resume_state.get("completed_steps", []):
@@ -239,9 +226,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
239
226
  resume_at = resume_state.get("resume_at")
240
227
 
241
228
  for i, t in enumerate(plan_obj.tasks, 1):
242
- # 【P2-2】跳过已完成的步骤(断点续跑):resume_at 之前的步骤不重跑;
243
- # 命中断点后清空 resume_at,让断点及其之后的步骤正常执行。
244
- # (若照抄"!= resume_at 就 continue",断点之后的步骤会被一并跳过)
245
229
  if resume_at:
246
230
  if t.id != resume_at:
247
231
  continue
@@ -251,7 +235,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
251
235
  res.failed.append(t.id); continue
252
236
  skill = registry.get(sid)
253
237
  call_args = _build_args(skill, t.name, upstream)
254
- # 关键操作二次确认码随 resume_state 回灌:同一 nonce 只能在原动作+原参数上用一次
255
238
  _nonce = (resume_state or {}).get("confirm_nonce", "") or ""
256
239
  out = _invoke(adapter, skill, call_args, confirmed, _nonce)
257
240
  res.stages.append(StageResult(stage=i, skill_id=sid, result=out.to_dict()))
@@ -260,7 +243,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
260
243
  executed.append((sid, out.output))
261
244
  res.completed_steps.append((t.id, sid, out.output))
262
245
  elif out.status == "needs_confirmation":
263
- # 【P2-2】待确认 → 暂停,不回滚;把断点现场打包给调用方
264
246
  res.pending_confirmation = {
265
247
  "task_id": t.id,
266
248
  "skill_id": sid,
@@ -269,8 +251,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
269
251
  "args": call_args,
270
252
  "completed_steps": list(res.completed_steps),
271
253
  "resume_at": t.id,
272
- # 关键操作:一次性确认码(绑定 skill + 本次 args,过期即废)。
273
- # 续跑时把它塞回 resume_state 一起传,才能通过 adapter 的二次确认校验。
274
254
  "confirm_nonce": getattr(out, "confirm_nonce", "") or "",
275
255
  "confirm_expires_at": getattr(out, "confirm_expires_at", 0.0) or 0.0,
276
256
  }
@@ -281,7 +261,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
281
261
  }
282
262
  return res
283
263
  else:
284
- # blocked/circuit_open/error/timeout → 关键路径失败 → 逆序回滚
285
264
  res.failed.append(t.id)
286
265
  res.final_output = {"error": out.message, "failed_at": t.id}
287
266
  _rollback(executed, registry, res)
@@ -290,8 +269,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
290
269
  return res
291
270
 
292
271
 
293
- # ============ 编排器:把 6 个元 skill 串起来 ============
294
-
295
272
  @dataclass
296
273
  class PipelineResult:
297
274
  task: str
@@ -302,7 +279,7 @@ class PipelineResult:
302
279
  summary: str
303
280
 
304
281
  @property
305
- def pending_confirmation(self) -> dict:
282
+ def pending_confirmation(self) -> dict | None:
306
283
  """【P2-2】待确认状态(若无则 None)。结构见 compose 的 pending_confirmation。"""
307
284
  return self.composition.get("pending_confirmation")
308
285
 
@@ -320,7 +297,7 @@ class AHSPipeline:
320
297
  self.adapter = adapter
321
298
 
322
299
  def run(self, task: str, confirmed: bool = False,
323
- resume_state: dict = None, confirm_nonce: str = "") -> PipelineResult:
300
+ resume_state: dict | None = None, confirm_nonce: str = "") -> PipelineResult:
324
301
  """同步执行。【P2-2】resume_state 用于从待确认状态断点续跑
325
302
  (传上次返回的 pending_confirmation;确认后调 run(task, confirmed=True, resume_state=...))。
326
303
 
@@ -329,25 +306,17 @@ class AHSPipeline:
329
306
  """
330
307
  if confirm_nonce:
331
308
  resume_state = {**(resume_state or {}), "confirm_nonce": confirm_nonce}
332
- # session_start 钩子
333
309
  self.adapter.hooks.fire("session_start", "", {"task": task})
334
310
 
335
- # 1. router 选主 skill
336
311
  r = route(task, self.registry)
337
312
 
338
- # 2. planner 拆解(总是出计划;简单任务=单步)
339
313
  p = plan(task, self.registry)
340
314
 
341
- # 3. composition 编排执行(每步内部过 guard+breaker+hooks)
342
315
  c = compose(p, self.registry, self.adapter,
343
316
  confirmed=confirmed, resume_state=resume_state)
344
317
 
345
- # session_end 钩子
346
318
  self.adapter.hooks.fire("session_end", "", {"task": task})
347
319
 
348
- # 【P2-2】success 判定区分 pending:待确认不是失败,但也不是成功
349
- # (旧语义:pending 会进 failed → success=False;新语义:pending 非空 → success=False。
350
- # 外向结果一致,语义不同——pending 的步骤没有被回滚,可以续跑)
351
320
  success = not c.failed and bool(c.stages) and not c.pending_confirmation
352
321
  if c.pending_confirmation:
353
322
  summary = (f"执行 {len(c.stages)} 步,待确认:"
@@ -367,13 +336,12 @@ class AHSPipeline:
367
336
  "failed": c.failed, "final": c.final_output,
368
337
  "rolled_back": c.rolled_back, "rollback_clean": c.rollback_clean,
369
338
  "rollback_log": c.rollback_log, "needs_manual": c.needs_manual,
370
- # 【P2-2】待确认 + 已完成步骤(供前端识别/续跑)
371
339
  "pending_confirmation": c.pending_confirmation,
372
340
  "completed_steps": [s[0] for s in c.completed_steps]},
373
341
  success=success, summary=summary)
374
342
 
375
343
  async def run_async(self, task: str, confirmed: bool = False,
376
- resume_state: dict = None,
344
+ resume_state: dict | None = None,
377
345
  confirm_nonce: str = "") -> PipelineResult:
378
346
  """【P2-1】异步入口:把同步 run 放到线程池,避免阻塞事件循环。
379
347
 
@@ -12,19 +12,15 @@ class SkillPolicy:
12
12
  def __init__(self, data_dir):
13
13
  self._path = Path(data_dir) / "skill_policy.json"
14
14
  self._lock = threading.RLock()
15
- self._deny: set = set() # user 层:运行时可 deny/undeny,持久化到 skill_policy.json
16
- self._allow: set = set() # 空 = 全放行
17
- # E1 分层:policy 层 deny(管理员锁定,env FOXCLAW_POLICY_DENY + <data_dir>/policy.json)。
18
- # 与 user 层物理分开、不写回 skill_policy.json;**不可被 undeny 撤销**。默认空 = 现状。
15
+ self._deny: set = set()
16
+ self._allow: set = set()
19
17
  from ..core.config import LayeredConfig, load_policy_layer
20
18
  policy_layer = load_policy_layer(data_dir)
21
- # user 层传 self._deny(set 引用)→ 运行时增删被 merged_set 实时反映,无需重建 LayeredConfig
22
19
  self._layers = LayeredConfig(policy=policy_layer, user={"deny": self._deny})
23
20
  self._load()
24
21
 
25
22
  def is_blocked(self, skill_id: str) -> bool:
26
23
  with self._lock:
27
- # deny = policy 层 ∪ user 层(只增不减);deny 压倒 allow
28
24
  if skill_id in self._layers.merged_set("deny"):
29
25
  return True
30
26
  if self._allow and skill_id not in self._allow:
@@ -39,7 +35,7 @@ class SkillPolicy:
39
35
  def undeny(self, skill_id: str) -> bool:
40
36
  with self._lock:
41
37
  if skill_id in self._layers.policy_locked("deny"):
42
- return False # E1:policy 层为管理员锁定,用户 undeny 无效(deny 铁律)
38
+ return False
43
39
  existed = skill_id in self._deny
44
40
  self._deny.discard(skill_id)
45
41
  self._save()
@@ -53,15 +49,13 @@ class SkillPolicy:
53
49
  def snapshot(self) -> dict:
54
50
  with self._lock:
55
51
  return {"deny": sorted(self._deny), "allow": sorted(self._allow),
56
- "policy_deny": sorted(self._layers.policy_locked("deny"))} # E1:管理员锁定项(只读)
52
+ "policy_deny": sorted(self._layers.policy_locked("deny"))}
57
53
 
58
54
  def _load(self):
59
55
  if not self._path.exists():
60
56
  return
61
57
  try:
62
58
  d = json.loads(self._path.read_text(encoding="utf-8"))
63
- # 就地更新 self._deny(不重新赋值!)——LayeredConfig.user 持有本 set 的引用,
64
- # 重新赋值会断开引用,导致从盘加载的 user deny 不被 merged_set 反映(E1 集成的坑)。
65
59
  self._deny.clear()
66
60
  self._deny.update(d.get("deny") or [])
67
61
  self._allow = set(d.get("allow") or [])
@@ -20,7 +20,7 @@ def _is_internal_host(host: str) -> bool:
20
20
  try:
21
21
  infos = socket.getaddrinfo(host, None)
22
22
  except Exception:
23
- return False # 解析不了 → 不在安装期硬挡,交运行时 SSRF 兜底
23
+ return False
24
24
  for info in infos:
25
25
  try:
26
26
  addr = ipaddress.ip_address(info[4][0])
@@ -28,7 +28,7 @@ def _is_internal_host(host: str) -> bool:
28
28
  continue
29
29
  if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved:
30
30
  return True
31
- if str(addr) == "169.254.169.254": # 云元数据
31
+ if str(addr) == "169.254.169.254":
32
32
  return True
33
33
  return False
34
34
 
@@ -69,4 +69,4 @@ def review_mcp_server(url: str) -> dict:
69
69
  warnings.append("接入外部 MCP server,其返回内容为外部不可信数据,不应被当作指令执行")
70
70
  if reasons:
71
71
  return {"verdict": "deny", "reasons": reasons, "warnings": warnings}
72
- return {"verdict": "warn", "reasons": [], "warnings": warnings} # 外部低信任恒 warn
72
+ return {"verdict": "warn", "reasons": [], "warnings": warnings}
@@ -1,7 +1,3 @@
1
- /* 鉴权注入:后端若设了 FOXCLAW_API_KEY,同源 /api、/knowledge 请求需带 X-API-Key。
2
- 从 localStorage.foxclawApiKey 读取并包一层 fetch 自动注入。
3
- 必须最先加载(在 settings.js 等业务模块之前),否则模块顶层提前发出的请求
4
- (如 settings.js 的 loadModel → /api/model)会漏带 key 被 401。 */
5
1
  (function(){
6
2
  if(window.__foxclawFetchPatched) return;
7
3
  const _fetch=window.fetch.bind(window);
@@ -10,14 +6,11 @@
10
6
  try{
11
7
  const key=localStorage.getItem('foxclawApiKey');
12
8
  const u=String(url||'');
13
- // /knowledge/* 与 /api/* 一样受后端可选鉴权守护,两者都要注入 key
14
- // /stats 是 HKC 统计数据面,后端已纳入守护,同样需要注入
15
9
  if(key && (u.indexOf('/api')>=0 || u.indexOf('/knowledge')>=0 || u.indexOf('/stats')>=0)){
16
10
  opts=opts||{}; opts.headers=Object.assign({}, opts.headers, {'X-API-Key':key});
17
11
  }
18
12
  }catch(e){}
19
13
  const p=_fetch(url,opts);
20
- // 401 检测:后端开了鉴权但本机浏览器没配 key(或 key 错)→ 弹提示条引导(只弹一次)
21
14
  try{
22
15
  p.then(r=>{
23
16
  if(r && r.status===401 && !_authHinted){
@@ -1,11 +1,3 @@
1
- // chat_model_picker.js — chat 输入框旁的「下拉式模型选择器」
2
- //
3
- // 视觉:参考图1 (CodeBuddy 模型列表) + 图2 (「添加自定义」对话框)。
4
- // 数据:全部走 foxclaw/api/routes/llm_admin.py 的 REST 端点(GET/POST/DELETE)。
5
- // 状态:切换写入 FOXCLAW_DATA_DIR/.current_model,model_router 用 mtime 缓存 lazy reload,
6
- // 用户改完**无需重启 foxclaw**,下次 chat 自动走新模型。
7
- //
8
- // 不会外泄:GET /api/llm/models 只返 has_key 布尔;POST /api/llm/custom/add 只在服务端保存。
9
1
 
10
2
  const API_BASE = (typeof window !== 'undefined' && window.API_BASE) || '';
11
3
  const _api = (p) => `${API_BASE}/api/llm${p}`;
@@ -13,18 +5,12 @@ const _api = (p) => `${API_BASE}/api/llm${p}`;
13
5
  let _models = { providers: [], custom: [], current: null };
14
6
  let _menuOpen = false;
15
7
 
16
- // ── 工具 ─────────────────────────────────────────────
17
-
18
8
  function _esc(s) {
19
9
  return String(s == null ? '' : s)
20
10
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
21
11
  .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
22
12
  }
23
13
 
24
- // BASE URL 规范化(与后端 core/model_router.normalize_base_url 同规则):
25
- // 无 scheme → 补 https://;path 为空 → 补 /v1;已有 path(/v1、/compatible-mode/v1…)原样保留。
26
- // 病根:用户照厂商文档填 https://api.deepseek.com,SDK 拼出 /chat/completions → 多数网关 404/401,
27
- // 用户以为是"模型填不上"。前端先收拾干净,后端再兜一层(旧记录 / 直接调 API 的场景)。
28
14
  function _normalizeBaseUrl(u) {
29
15
  let s = String(u || '').trim();
30
16
  if (!s) return s;
@@ -37,7 +23,6 @@ function _normalizeBaseUrl(u) {
37
23
  return `${parsed.protocol}//${parsed.host}${path}${parsed.search || ''}`;
38
24
  }
39
25
 
40
- // 常见厂商的模型 ID 提示(只做 placeholder 引导,不阻断保存 —— 自建网关可能有自定义名)
41
26
  const _MODEL_HINTS = [
42
27
  { re: /api\.deepseek\.com/i, hint: 'deepseek-chat', note: 'DeepSeek 官方 ID:deepseek-chat / deepseek-reasoner' },
43
28
  { re: /api\.openai\.com/i, hint: 'gpt-4o-mini', note: 'OpenAI 官方 ID:gpt-4o-mini / gpt-4o' },
@@ -61,7 +46,6 @@ function _autoCurrent() {
61
46
  return !_models.current;
62
47
  }
63
48
 
64
- // 当前显示在 pill 上的名称 + 颜色
65
49
  function _currentLabel() {
66
50
  if (_isCustomCurrent()) {
67
51
  const m = _models.custom.find(m => m.name === _models.current.name);
@@ -76,15 +60,12 @@ function _currentLabel() {
76
60
  return { name: 'Auto', color: '#7e93b8', isCustom: false };
77
61
  }
78
62
 
79
- // ── 渲染:菜单内容 ─────────────────────────────────────
80
-
81
63
  function renderMenu() {
82
64
  const menu = document.getElementById('chatModelMenu');
83
65
  if (!menu) return;
84
66
  const cur = _currentLabel();
85
67
  let h = '';
86
68
 
87
- // Auto(让用户能清掉选择,退回 .env 默认)
88
69
  h += `<div class="mp-item ${_autoCurrent() ? 'active' : ''}" data-act="auto">
89
70
  <span class="mp-dot" style="background:#7e93b8"></span>
90
71
  <span class="mp-name"><b>Auto</b><span>跟随 .env 默认</span></span>
@@ -107,9 +88,6 @@ function renderMenu() {
107
88
  h += '<div class="mp-sep">自定义</div>';
108
89
  _models.custom.forEach(m => {
109
90
  const isCur = _isCustomCurrent() && _models.current.name === m.name;
110
- // 三个 tag 都常驻:当前(按需显隐)+ ✎ 编辑 + ✕ 删除
111
- // 即使是当前选中的,用户也应该能直接编辑它(改 key / 加 group_id),不能强制切走
112
- // 删除当前选中的也安全 —— 后端 DELETE 已处理清 .current_model
113
91
  h += `<div class="mp-item ${isCur ? 'active' : ''}" data-act="custom" data-name="${_esc(m.name)}">
114
92
  <span class="mp-dot" style="background:#a78bfa;box-shadow:0 0 6px #a78bfa"></span>
115
93
  <span class="mp-name"><b>${_esc(m.name)}</b><span>${_esc(m.model)}</span></span>
@@ -127,7 +105,6 @@ function renderMenu() {
127
105
 
128
106
  menu.innerHTML = h;
129
107
 
130
- // 事件
131
108
  menu.querySelectorAll('.mp-item').forEach(el => {
132
109
  el.addEventListener('click', (e) => {
133
110
  if (e.target.classList.contains('mp-del')) {
@@ -164,8 +141,6 @@ function renderPill() {
164
141
  if (name) name.textContent = cur.name;
165
142
  }
166
143
 
167
- // ── API ─────────────────────────────────────────────
168
-
169
144
  async function loadModels() {
170
145
  try {
171
146
  const r = await fetch(_api('/models'));
@@ -230,14 +205,12 @@ async function addCustom(payload) {
230
205
  }
231
206
 
232
207
  async function getCustom(name) {
233
- // 取单个自定义模型详情(后端不返回 api_key 明文,只返 has_key)
234
208
  const r = await fetch(_api(`/custom/${encodeURIComponent(name)}`));
235
209
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
236
210
  return r.json();
237
211
  }
238
212
 
239
213
  async function updateCustom(name, payload) {
240
- // payload 字段语义:填了非空 = 用新值;没传(或不传该字段)= 不变;api_key 留空=保留原值
241
214
  const r = await fetch(_api(`/custom/${encodeURIComponent(name)}`), {
242
215
  method: 'PUT',
243
216
  headers: { 'Content-Type': 'application/json' },
@@ -252,7 +225,6 @@ async function updateCustom(name, payload) {
252
225
  }
253
226
 
254
227
  async function editCustom(name) {
255
- // 加载详情 → 复用 openAddDialog 切到 edit 模式预填表单
256
228
  try {
257
229
  const m = await getCustom(name);
258
230
  openAddDialog('edit', { ...m, _editingName: name });
@@ -274,9 +246,6 @@ async function testModel(source, provider, name) {
274
246
  return r.json();
275
247
  }
276
248
 
277
- // ── 添加对话框(图2 风格)────────────────────────────
278
-
279
- // 对话框全局状态:add / edit 模式共用同一表单
280
249
  let _cmMode = 'add'; // 'add' | 'edit'
281
250
  let _cmEditingName = null; // edit 模式下的原名(PUT URL 用);改名后仍指向磁盘上那条记录
282
251
 
@@ -289,7 +258,6 @@ function openAddDialog(mode = 'add', preset = null) {
289
258
  const mask = document.getElementById('cmAddMask');
290
259
  if (!mask) return;
291
260
 
292
- // 标题 + 副标题
293
261
  const title = document.getElementById('cmAddTitle');
294
262
  if (title) {
295
263
  title.textContent = mode === 'edit' && preset?.name
@@ -297,7 +265,6 @@ function openAddDialog(mode = 'add', preset = null) {
297
265
  : '添加自定义模型';
298
266
  }
299
267
 
300
- // 填表单 — api_key 永远不清空回填(安全考虑)
301
268
  const set = (id, v) => { const el = document.getElementById(id); if (el) el.value = v || ''; };
302
269
  set('cm_name', preset?.name);
303
270
  set('cm_base_url', preset?.base_url);
@@ -309,7 +276,6 @@ function openAddDialog(mode = 'add', preset = null) {
309
276
  set('cm_note', preset?.note);
310
277
  _applyModelHint(preset?.base_url);
311
278
 
312
- // API KEY 字段 placeholder 区分 add / edit
313
279
  const keyInp = document.getElementById('cm_api_key');
314
280
  if (keyInp) {
315
281
  keyInp.placeholder = (mode === 'edit' && preset?.has_key)
@@ -323,7 +289,6 @@ function openAddDialog(mode = 'add', preset = null) {
323
289
  setTimeout(() => document.getElementById('cm_name')?.focus(), 50);
324
290
  }
325
291
 
326
- // 已知厂商的"模型 ID 写错"软提示(只警告不阻断 —— 自建网关可以有自定义别名)
327
292
  function _modelWarning(baseUrl, modelId) {
328
293
  const s = String(baseUrl || ''), m = String(modelId || '').trim();
329
294
  if (!m) return '';
@@ -332,7 +297,6 @@ function _modelWarning(baseUrl, modelId) {
332
297
  return '';
333
298
  }
334
299
 
335
- // 按 BASE URL 域名更新「模型 ID」的 placeholder + 副标题提示(不阻断保存)
336
300
  function _applyModelHint(baseUrl) {
337
301
  const hint = _hintForBaseUrl(baseUrl);
338
302
  const inp = document.getElementById('cm_model');
@@ -360,7 +324,6 @@ function _readForm() {
360
324
  const v = (id) => (document.getElementById(id)?.value || '').trim();
361
325
  const out = {
362
326
  name: v('cm_name'),
363
- // 保存前再规范化一次(用户可能没触发 blur 直接点保存);后端也会兜一层
364
327
  base_url: _normalizeBaseUrl(v('cm_base_url')),
365
328
  api_key: v('cm_api_key'),
366
329
  model: v('cm_model'),
@@ -384,7 +347,6 @@ function _readForm() {
384
347
  async function _onSave() {
385
348
  try {
386
349
  const payload = _readForm();
387
- // name / base_url / model 必填;api_key 在 edit 模式下留空 = 保留(后端校验)
388
350
  if (!payload.name || !payload.base_url || !payload.model) {
389
351
  _setMsg('请填全 * 号必填项', 'err');
390
352
  return;
@@ -401,13 +363,9 @@ async function _onSave() {
401
363
  } else {
402
364
  const r = await updateCustom(_cmEditingName, payload);
403
365
  savedName = r.name;
404
- // 改名后,_cmEditingName 失效 → 下次 _onTest 会失败 → 清掉,避免误测
405
366
  _cmEditingName = r.name;
406
367
  }
407
368
  await loadModels();
408
- // 保存即生效 —— 用户点「保存」的诉求就是「用它」。
409
- // 病根:此前只写盘不切换,聊天仍走 .env 的旧 provider,用户看到 AuthenticationError/stub,
410
- // 以为是"模型填不上";实际上是要再回菜单点一次这个条目。现在省掉这一步。
411
369
  _setMsg('已保存,正在切换…', 'info');
412
370
  const ok = await selectModel('custom', null, savedName);
413
371
  _setMsg(ok ? `已保存并切换到 ${savedName}` : `已保存 ${savedName},但切换失败(见弹窗)`,
@@ -426,8 +384,6 @@ async function _onTest() {
426
384
  return;
427
385
  }
428
386
  if (_cmMode === 'edit') {
429
- // edit 模式:测磁盘上**已保存**的配置(避免被 form 里未保存的改动误导)
430
- // 若想测表单里的新值,先点「保存」再点「测试连通性」
431
387
  _setMsg(`测试已保存配置(${_cmEditingName})…`, 'info');
432
388
  const r = await testModel('custom', null, _cmEditingName);
433
389
  if (r.ok) {
@@ -437,7 +393,6 @@ async function _onTest() {
437
393
  }
438
394
  return;
439
395
  }
440
- // add 模式:必须 name + api_key,临时 add 一个 tmp 名测,失败保留
441
396
  if (!payload.name || !payload.api_key) {
442
397
  _setMsg('add 模式请先填「模型名称」+「API KEY」再测试', 'err');
443
398
  return;
@@ -446,7 +401,6 @@ async function _onTest() {
446
401
  await addCustom(payload);
447
402
  const r = await testModel('custom', null, payload.name);
448
403
  if (r.ok) {
449
- // 测试通过 = 这份配置真的能用 → 直接切过去,不用用户再回菜单点一次
450
404
  await loadModels();
451
405
  await selectModel('custom', null, payload.name);
452
406
  _setMsg(`✓ 连通 (${r.latency_ms} ms, model=${r.model}) — 已切换`, 'ok');
@@ -459,8 +413,6 @@ async function _onTest() {
459
413
  }
460
414
  }
461
415
 
462
- // ── 事件绑定 ─────────────────────────────────────
463
-
464
416
  function bindEvents() {
465
417
  const pill = document.getElementById('chatModelPill');
466
418
  const menu = document.getElementById('chatModelMenu');
@@ -470,7 +422,6 @@ function bindEvents() {
470
422
  _menuOpen = !_menuOpen;
471
423
  menu.classList.toggle('show', _menuOpen);
472
424
  });
473
- // 点外面收起
474
425
  document.addEventListener('click', (e) => {
475
426
  if (!_menuOpen) return;
476
427
  if (e.target.closest('#chatModelPick')) return;
@@ -500,7 +451,6 @@ function bindEvents() {
500
451
  if (e.target === mask) closeAddDialog();
501
452
  });
502
453
  }
503
- // BASE URL 失焦即规范化(补 https:// / 补 /v1),并联动模型 ID 提示
504
454
  const baseInp = document.getElementById('cm_base_url');
505
455
  if (baseInp) {
506
456
  baseInp.addEventListener('blur', () => {
@@ -521,16 +471,12 @@ function bindEvents() {
521
471
  });
522
472
  }
523
473
 
524
- // ── 启动 ─────────────────────────────────────────
525
-
526
474
  (function init() {
527
475
  bindEvents();
528
476
  loadModels();
529
- // 多 tab 同步:监听 storage 事件
530
477
  window.addEventListener('storage', (e) => {
531
478
  if (e.key === '__foxclaw_model_changed__') loadModels();
532
479
  });
533
- // 切完模型后,广播给其它 tab(简版:周期性 reload)
534
480
  setInterval(() => {
535
481
  if (_menuOpen) loadModels();
536
482
  }, 5000);