foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -47,13 +47,12 @@ class OpenClawBridgeAdapter(ChannelAdapter):
|
|
|
47
47
|
关键:保留 OpenClaw 侧的 channel(真实渠道,如 wechat)和 conversation 引用,
|
|
48
48
|
以便回复时让 OpenClaw 发回正确的原渠道。
|
|
49
49
|
"""
|
|
50
|
-
# OpenClaw 消息常见结构:{channel, message:{id,text}, sender:{id,name}, conversation:{id,isGroup}}
|
|
51
50
|
msg = raw.get("message") or raw
|
|
52
51
|
if not isinstance(msg, dict):
|
|
53
52
|
return None
|
|
54
53
|
text = msg.get("text") or msg.get("content") or ""
|
|
55
54
|
if not text:
|
|
56
|
-
return None
|
|
55
|
+
return None
|
|
57
56
|
sender = raw.get("sender") or msg.get("sender") or {}
|
|
58
57
|
conv = raw.get("conversation") or msg.get("conversation") or {}
|
|
59
58
|
return InboundMessage(
|
|
@@ -64,7 +63,7 @@ class OpenClawBridgeAdapter(ChannelAdapter):
|
|
|
64
63
|
sender_name=str(sender.get("name") or sender.get("username") or ""),
|
|
65
64
|
conversation_id=str(conv.get("id") or conv.get("conversation_id") or ""),
|
|
66
65
|
is_group=bool(conv.get("isGroup") or conv.get("is_group") or False),
|
|
67
|
-
raw=raw)
|
|
66
|
+
raw=raw)
|
|
68
67
|
|
|
69
68
|
def _upstream_channel(self, inbound_raw: dict) -> str:
|
|
70
69
|
return (inbound_raw.get("channel")
|
|
@@ -86,21 +85,17 @@ class OpenClawBridgeAdapter(ChannelAdapter):
|
|
|
86
85
|
}
|
|
87
86
|
if msg.reply_to:
|
|
88
87
|
payload["reply_to"] = msg.reply_to
|
|
89
|
-
# 若上层指明了真实渠道(从入站 raw 透传),带上,确保发回正确渠道
|
|
90
88
|
if msg.extra.get("upstream_channel"):
|
|
91
89
|
payload["channel"] = msg.extra["upstream_channel"]
|
|
92
90
|
headers = {}
|
|
93
91
|
if self.api_token:
|
|
94
92
|
headers["Authorization"] = f"Bearer {self.api_token}"
|
|
95
|
-
# 先序列化成 bytes 再发:签名必须覆盖**实际发出去的字节**,否则网关侧
|
|
96
|
-
# 重新序列化(键序/转义差异)会导致验签失败。content= 而非 json=,
|
|
97
|
-
# 就是为了保证"签的就是发的"。
|
|
98
93
|
try:
|
|
99
94
|
body = json.dumps(payload, ensure_ascii=False,
|
|
100
95
|
sort_keys=True).encode("utf-8")
|
|
101
96
|
except Exception as e:
|
|
102
97
|
return SendResult(False, error=f"出站 payload 序列化失败: {e}")
|
|
103
|
-
headers["Content-Type"] = "application/json"
|
|
98
|
+
headers["Content-Type"] = "application/json"
|
|
104
99
|
if self.signer is not None and self.signer.enabled:
|
|
105
100
|
try:
|
|
106
101
|
headers.update(self.signer.headers(body))
|
|
@@ -130,7 +125,7 @@ class OpenClawBridgeAdapter(ChannelAdapter):
|
|
|
130
125
|
|
|
131
126
|
def verify_webhook(self, headers, body: bytes,
|
|
132
127
|
tolerance: float = _signing.DEFAULT_TOLERANCE,
|
|
133
|
-
replay: "_signing.ReplayGuard" = None):
|
|
128
|
+
replay: "_signing.ReplayGuard | None" = None):
|
|
134
129
|
"""验 OpenClaw 转发来的 webhook 签名,返回 (是否通过, 原因码)。
|
|
135
130
|
|
|
136
131
|
与出站签名同一套 canonical 结构、同一个共享密钥(`signing.verify`)。
|
|
@@ -41,7 +41,7 @@ KEY_ID_HEADER = "X-Foxclaw-Key-Id"
|
|
|
41
41
|
ENV_SECRET = "FOXCLAW_CHANNEL_SIGNING_SECRET"
|
|
42
42
|
ENV_KEY_ID = "FOXCLAW_CHANNEL_SIGNING_KEY_ID"
|
|
43
43
|
|
|
44
|
-
DEFAULT_TOLERANCE = 300.0
|
|
44
|
+
DEFAULT_TOLERANCE = 300.0
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def body_digest(body: bytes) -> str:
|
|
@@ -70,10 +70,10 @@ class ReplayGuard:
|
|
|
70
70
|
def __init__(self, ttl: float = DEFAULT_TOLERANCE, max_entries: int = 4096):
|
|
71
71
|
self.ttl = float(ttl)
|
|
72
72
|
self.max_entries = max(16, int(max_entries))
|
|
73
|
-
self._seen: dict[str, float] = {}
|
|
73
|
+
self._seen: dict[str, float] = {}
|
|
74
74
|
self._lock = threading.Lock()
|
|
75
75
|
|
|
76
|
-
def purge(self, now: float = None) -> int:
|
|
76
|
+
def purge(self, now: float | None = None) -> int:
|
|
77
77
|
"""清掉过期 nonce,返回清理条数。"""
|
|
78
78
|
cut = (now if now is not None else time.time()) - self.ttl
|
|
79
79
|
with self._lock:
|
|
@@ -82,10 +82,10 @@ class ReplayGuard:
|
|
|
82
82
|
self._seen.pop(k, None)
|
|
83
83
|
return len(dead)
|
|
84
84
|
|
|
85
|
-
def seen(self, nonce: str, now: float = None) -> bool:
|
|
85
|
+
def seen(self, nonce: str, now: float | None = None) -> bool:
|
|
86
86
|
"""见过 → True(**这是重放**);没见过则登记并返回 False。"""
|
|
87
87
|
if not nonce:
|
|
88
|
-
return True
|
|
88
|
+
return True
|
|
89
89
|
now = now if now is not None else time.time()
|
|
90
90
|
with self._lock:
|
|
91
91
|
cut = now - self.ttl
|
|
@@ -115,7 +115,7 @@ class OutboundSigner:
|
|
|
115
115
|
def enabled(self) -> bool:
|
|
116
116
|
return bool(self.secret)
|
|
117
117
|
|
|
118
|
-
def headers(self, body: bytes, nonce: str = "", now: float = None) -> dict:
|
|
118
|
+
def headers(self, body: bytes, nonce: str = "", now: float | None = None) -> dict:
|
|
119
119
|
"""生成签名头。未配置密钥直接抛错(fail-closed:调用方必须先判 `enabled`)。"""
|
|
120
120
|
if not self.enabled:
|
|
121
121
|
raise ValueError("未配置渠道签名密钥(FOXCLAW_CHANNEL_SIGNING_SECRET)")
|
|
@@ -145,8 +145,8 @@ def _header(headers: Mapping, name: str) -> str:
|
|
|
145
145
|
|
|
146
146
|
|
|
147
147
|
def verify(headers: Mapping, body: bytes, secret: str,
|
|
148
|
-
tolerance: float = DEFAULT_TOLERANCE, replay: ReplayGuard = None,
|
|
149
|
-
now: float = None) -> Tuple[bool, str]:
|
|
148
|
+
tolerance: float = DEFAULT_TOLERANCE, replay: ReplayGuard | None = None,
|
|
149
|
+
now: float | None = None) -> Tuple[bool, str]:
|
|
150
150
|
"""验签。返回 (是否通过, 原因码)。
|
|
151
151
|
|
|
152
152
|
原因码:``ok`` / ``missing_secret`` / ``missing_header`` / ``bad_version`` /
|
|
@@ -178,7 +178,7 @@ def verify(headers: Mapping, body: bytes, secret: str,
|
|
|
178
178
|
return True, "ok"
|
|
179
179
|
|
|
180
180
|
|
|
181
|
-
def from_env(env: Mapping = None) -> Optional[OutboundSigner]:
|
|
181
|
+
def from_env(env: Mapping | None = None) -> Optional[OutboundSigner]:
|
|
182
182
|
"""按环境变量构造签名器;未配置密钥 → None(不签,opt-in)。"""
|
|
183
183
|
env = env if env is not None else os.environ
|
|
184
184
|
secret = (env.get(ENV_SECRET) or "").strip()
|
|
@@ -22,7 +22,7 @@ class TelegramAdapter(ChannelAdapter):
|
|
|
22
22
|
super().__init__(on_message=on_message)
|
|
23
23
|
self.bot_token = bot_token
|
|
24
24
|
self.timeout = timeout
|
|
25
|
-
self._offset = 0
|
|
25
|
+
self._offset = 0
|
|
26
26
|
|
|
27
27
|
@property
|
|
28
28
|
def _base(self) -> str:
|
|
@@ -32,7 +32,7 @@ class TelegramAdapter(ChannelAdapter):
|
|
|
32
32
|
"""Telegram update → 标准 InboundMessage。非文本消息返回 None。"""
|
|
33
33
|
msg = raw.get("message") or raw.get("edited_message")
|
|
34
34
|
if not msg or "text" not in msg:
|
|
35
|
-
return None
|
|
35
|
+
return None
|
|
36
36
|
chat = msg.get("chat", {})
|
|
37
37
|
sender = msg.get("from", {})
|
|
38
38
|
chat_type = chat.get("type", "private")
|
|
@@ -97,11 +97,10 @@ class TelegramAdapter(ChannelAdapter):
|
|
|
97
97
|
return []
|
|
98
98
|
out = []
|
|
99
99
|
for update in data.get("result", []):
|
|
100
|
-
# 推进游标:下次从最大 update_id + 1 开始,避免重复
|
|
101
100
|
uid = update.get("update_id", 0)
|
|
102
101
|
if uid >= self._offset:
|
|
103
102
|
self._offset = uid + 1
|
|
104
|
-
inbound = self.handle_raw(update)
|
|
103
|
+
inbound = self.handle_raw(update)
|
|
105
104
|
if inbound:
|
|
106
105
|
out.append(inbound)
|
|
107
106
|
return out
|
|
@@ -22,20 +22,17 @@ import secrets
|
|
|
22
22
|
from typing import Optional
|
|
23
23
|
from . import ChannelAdapter, InboundMessage, OutboundMessage, SendResult
|
|
24
24
|
|
|
25
|
-
# 官方网关与端点(来自 Tencent/openclaw-weixin 源码 src/auth/accounts.ts)
|
|
26
25
|
WEIXIN_BASE_URL = "https://ilinkai.weixin.qq.com"
|
|
27
26
|
EP_GET_UPDATES = "ilink/bot/getupdates"
|
|
28
27
|
EP_SEND_MESSAGE = "ilink/bot/sendmessage"
|
|
29
28
|
EP_GET_CONFIG = "ilink/bot/getconfig"
|
|
30
29
|
EP_SEND_TYPING = "ilink/bot/sendtyping"
|
|
31
30
|
|
|
32
|
-
# 消息 item 类型(来自 README MessageItem 表)
|
|
33
31
|
ITEM_TEXT = 1
|
|
34
32
|
ITEM_IMAGE = 2
|
|
35
33
|
ITEM_VOICE = 3
|
|
36
34
|
ITEM_FILE = 4
|
|
37
35
|
ITEM_VIDEO = 5
|
|
38
|
-
# message_type
|
|
39
36
|
MSG_FROM_USER = 1
|
|
40
37
|
MSG_FROM_BOT = 2
|
|
41
38
|
|
|
@@ -55,8 +52,7 @@ class WeixinAdapter(ChannelAdapter):
|
|
|
55
52
|
self.ilink_app_id = ilink_app_id
|
|
56
53
|
self.long_poll_timeout_ms = long_poll_timeout_ms
|
|
57
54
|
self.timeout = timeout
|
|
58
|
-
self._cursor = ""
|
|
59
|
-
# 记录每个用户最近的 context_token(回复时必须带回)
|
|
55
|
+
self._cursor = ""
|
|
60
56
|
self._context_tokens: dict[str, str] = {}
|
|
61
57
|
|
|
62
58
|
@staticmethod
|
|
@@ -69,7 +65,6 @@ class WeixinAdapter(ChannelAdapter):
|
|
|
69
65
|
major = minor = patch = 0
|
|
70
66
|
return ((major & 0xFF) << 16) | ((minor & 0xFF) << 8) | (patch & 0xFF)
|
|
71
67
|
|
|
72
|
-
# ── 鉴权头(照官方 buildHeaders)──────────────────────
|
|
73
68
|
@staticmethod
|
|
74
69
|
def _random_wechat_uin() -> str:
|
|
75
70
|
"""X-WECHAT-UIN: 随机 uint32 → 十进制字符串 → base64。"""
|
|
@@ -91,7 +86,7 @@ class WeixinAdapter(ChannelAdapter):
|
|
|
91
86
|
def _base_info(self) -> dict:
|
|
92
87
|
return {"channel_version": self.channel_version, "bot_agent": self.bot_agent}
|
|
93
88
|
|
|
94
|
-
def _post(self, endpoint: str, body: dict, timeout: float = None):
|
|
89
|
+
def _post(self, endpoint: str, body: dict, timeout: float | None = None):
|
|
95
90
|
import httpx
|
|
96
91
|
url = f"{self.base_url}/{endpoint}"
|
|
97
92
|
payload = {**body, "base_info": self._base_info()}
|
|
@@ -100,7 +95,6 @@ class WeixinAdapter(ChannelAdapter):
|
|
|
100
95
|
timeout=timeout or self.timeout)
|
|
101
96
|
return resp
|
|
102
97
|
|
|
103
|
-
# ── 入站:getUpdates 长轮询 ────────────────────────────
|
|
104
98
|
def poll_once(self) -> list[InboundMessage]:
|
|
105
99
|
"""长轮询拉一批新消息。宿主在循环里反复调用即可持续接收。"""
|
|
106
100
|
try:
|
|
@@ -114,7 +108,7 @@ class WeixinAdapter(ChannelAdapter):
|
|
|
114
108
|
{"get_updates_buf": self._cursor},
|
|
115
109
|
timeout=self.long_poll_timeout_ms / 1000 + 5)
|
|
116
110
|
except Exception:
|
|
117
|
-
return []
|
|
111
|
+
return []
|
|
118
112
|
if resp.status_code != 200:
|
|
119
113
|
return []
|
|
120
114
|
try:
|
|
@@ -122,8 +116,7 @@ class WeixinAdapter(ChannelAdapter):
|
|
|
122
116
|
except Exception:
|
|
123
117
|
return []
|
|
124
118
|
if data.get("ret") != 0:
|
|
125
|
-
return []
|
|
126
|
-
# 推进游标
|
|
119
|
+
return []
|
|
127
120
|
new_buf = data.get("get_updates_buf")
|
|
128
121
|
if new_buf is not None:
|
|
129
122
|
self._cursor = new_buf
|
|
@@ -141,20 +134,18 @@ class WeixinAdapter(ChannelAdapter):
|
|
|
141
134
|
return self._normalize_msg(raw)
|
|
142
135
|
|
|
143
136
|
def _normalize_msg(self, msg: dict) -> Optional[InboundMessage]:
|
|
144
|
-
# 只处理用户发来的消息(message_type=1),忽略 bot 自己的回显
|
|
145
137
|
if msg.get("message_type") not in (None, MSG_FROM_USER):
|
|
146
138
|
return None
|
|
147
|
-
# 从 item_list 提取文本
|
|
148
139
|
text = ""
|
|
149
140
|
for item in msg.get("item_list", []) or []:
|
|
150
141
|
if item.get("type") == ITEM_TEXT and item.get("text_item"):
|
|
151
142
|
text += item["text_item"].get("text", "")
|
|
152
143
|
if not text:
|
|
153
|
-
return None
|
|
144
|
+
return None
|
|
154
145
|
from_user = str(msg.get("from_user_id", ""))
|
|
155
146
|
ctx = msg.get("context_token", "")
|
|
156
147
|
if from_user and ctx:
|
|
157
|
-
self._context_tokens[from_user] = ctx
|
|
148
|
+
self._context_tokens[from_user] = ctx
|
|
158
149
|
return InboundMessage(
|
|
159
150
|
channel=self.CHANNEL,
|
|
160
151
|
message_id=str(msg.get("message_id", "")),
|
|
@@ -162,11 +153,10 @@ class WeixinAdapter(ChannelAdapter):
|
|
|
162
153
|
sender_id=from_user,
|
|
163
154
|
sender_name="",
|
|
164
155
|
conversation_id=str(msg.get("session_id", "") or from_user),
|
|
165
|
-
is_group=False,
|
|
156
|
+
is_group=False,
|
|
166
157
|
timestamp=float(msg.get("create_time_ms", 0)) / 1000 or 0.0,
|
|
167
158
|
raw=msg)
|
|
168
159
|
|
|
169
|
-
# ── 出站:sendMessage ──────────────────────────────────
|
|
170
160
|
def send(self, msg: OutboundMessage) -> SendResult:
|
|
171
161
|
"""发文本消息。conversation_id 作为 to_user_id;context_token 从记录里取或由 extra 传入。"""
|
|
172
162
|
try:
|
package/python/foxclaw/cli.py
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
foxclaw serve # 启动 API 服务(默认 http://127.0.0.1:8000)
|
|
8
8
|
foxclaw config check # 检查 .env / 环境变量:类型写错、拼错的键、互相矛盾的组合
|
|
9
9
|
foxclaw config show # 打印当前生效配置(密钥默认脱敏)
|
|
10
|
+
foxclaw skills install @owner/slug # 装 SKILL.md 技能(ClawHub/GitHub/URL/本地包)
|
|
11
|
+
foxclaw skills list # 列出已装技能
|
|
10
12
|
foxclaw --version # 打印版本
|
|
11
13
|
|
|
12
14
|
`serve` 用字符串目标 `foxclaw.api:app` 交给 uvicorn 导入,避免 CLI 进程
|
|
@@ -34,8 +36,6 @@ def _repo_root() -> Path:
|
|
|
34
36
|
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
35
37
|
from foxclaw.core import settings
|
|
36
38
|
|
|
37
|
-
# 启动前先查配置:类型写错的值今天会崩在很远的地方(uvicorn 40 行 traceback 中间的
|
|
38
|
-
# `int()`),用户看不出是自己 .env 写错了。这里提前拦下并说清怎么改。
|
|
39
39
|
issues = settings.check()
|
|
40
40
|
fatal = settings.fatal_issues(issues)
|
|
41
41
|
if fatal:
|
|
@@ -43,22 +43,19 @@ def _cmd_serve(args: argparse.Namespace) -> int:
|
|
|
43
43
|
print("\n以上配置会让启动直接失败,已中止。改好后重试。", file=sys.stderr)
|
|
44
44
|
return 2
|
|
45
45
|
if issues:
|
|
46
|
-
# 不影响启动的问题(可疑但能跑)只提醒一次,不拦
|
|
47
46
|
print(settings.format_issues(issues, limit=8), file=sys.stderr)
|
|
48
47
|
print("(以上不阻断启动;`foxclaw config check` 看完整诊断)", file=sys.stderr)
|
|
49
48
|
|
|
50
49
|
import uvicorn
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
kwargs["reload_dirs"] = [str(_repo_root() / "foxclaw")]
|
|
61
|
-
uvicorn.run(APP_TARGET, **kwargs)
|
|
51
|
+
uvicorn.run(
|
|
52
|
+
APP_TARGET,
|
|
53
|
+
host=args.host,
|
|
54
|
+
port=args.port,
|
|
55
|
+
log_level=args.log_level,
|
|
56
|
+
reload=bool(args.reload),
|
|
57
|
+
reload_dirs=[str(_repo_root() / "foxclaw")] if args.reload else None,
|
|
58
|
+
)
|
|
62
59
|
return 0
|
|
63
60
|
|
|
64
61
|
|
|
@@ -97,6 +94,12 @@ def _cmd_config_show(args: argparse.Namespace) -> int:
|
|
|
97
94
|
return 0
|
|
98
95
|
|
|
99
96
|
|
|
97
|
+
def _cmd_skills(args: argparse.Namespace) -> int:
|
|
98
|
+
from foxclaw.skills.install_cmd import run
|
|
99
|
+
|
|
100
|
+
return run(args)
|
|
101
|
+
|
|
102
|
+
|
|
100
103
|
def build_parser() -> argparse.ArgumentParser:
|
|
101
104
|
parser = argparse.ArgumentParser(
|
|
102
105
|
prog="foxclaw",
|
|
@@ -131,6 +134,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
131
134
|
help="显示密钥原文(默认脱敏;别在截图/日志里用)")
|
|
132
135
|
c_show.set_defaults(handler=_cmd_config_show)
|
|
133
136
|
|
|
137
|
+
skills = subparsers.add_parser("skills", help="安装/查看/卸载指令技能(SKILL.md)")
|
|
138
|
+
skills_sub = skills.add_subparsers(dest="skills_action", metavar="<action>")
|
|
139
|
+
|
|
140
|
+
s_install = skills_sub.add_parser("install", help="从 ClawHub/GitHub/URL/本地包装一个技能")
|
|
141
|
+
s_install.add_argument(
|
|
142
|
+
"ref",
|
|
143
|
+
help="@owner/slug(ClawHub) | github:owner/repo[/子路径][@ref] | "
|
|
144
|
+
"http(s)://包地址 | 本地 .zip/.tar.gz/SKILL.md/目录")
|
|
145
|
+
s_install.add_argument("--yes", action="store_true",
|
|
146
|
+
help="跳过确认(脚本/CI 必填;非终端环境没有它一定失败)")
|
|
147
|
+
s_install.add_argument("--id", default="", help="覆盖技能 id(默认取 SKILL.md 里的 name)")
|
|
148
|
+
s_install.add_argument("--json", action="store_true", help="输出 JSON")
|
|
149
|
+
s_install.set_defaults(handler=_cmd_skills)
|
|
150
|
+
|
|
151
|
+
s_list = skills_sub.add_parser("list", help="列出已安装的指令技能")
|
|
152
|
+
s_list.add_argument("--json", action="store_true", help="输出 JSON")
|
|
153
|
+
s_list.set_defaults(handler=_cmd_skills)
|
|
154
|
+
|
|
155
|
+
s_remove = skills_sub.add_parser("remove", help="卸载一个指令技能")
|
|
156
|
+
s_remove.add_argument("id", help="技能 id(`foxclaw skills list` 第一列)")
|
|
157
|
+
s_remove.set_defaults(handler=_cmd_skills)
|
|
158
|
+
|
|
134
159
|
return parser
|
|
135
160
|
|
|
136
161
|
|
|
@@ -143,6 +168,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
143
168
|
if getattr(args, "command", None) == "config":
|
|
144
169
|
print("用法:`foxclaw config check` 或 `foxclaw config show`")
|
|
145
170
|
return 2
|
|
171
|
+
if getattr(args, "command", None) == "skills":
|
|
172
|
+
print("用法:`foxclaw skills install <ref>` / `foxclaw skills list` / "
|
|
173
|
+
"`foxclaw skills remove <id>`")
|
|
174
|
+
return 2
|
|
146
175
|
print(f"foxclaw {__version__}")
|
|
147
176
|
print("用 `foxclaw serve` 启动本地服务;`foxclaw config check` 检查配置;"
|
|
148
177
|
"`foxclaw --help` 查看全部子命令。")
|
|
@@ -15,10 +15,10 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
@dataclass
|
|
17
17
|
class Evidence:
|
|
18
|
-
factor: str
|
|
19
|
-
detail: str = ""
|
|
20
|
-
weight: float = 1.0
|
|
21
|
-
raw: Any = None
|
|
18
|
+
factor: str
|
|
19
|
+
detail: str = ""
|
|
20
|
+
weight: float = 1.0
|
|
21
|
+
raw: Any = None
|
|
22
22
|
|
|
23
23
|
def to_dict(self) -> dict:
|
|
24
24
|
return {"factor": self.factor, "detail": self.detail, "weight": self.weight}
|
|
@@ -26,11 +26,11 @@ class Evidence:
|
|
|
26
26
|
|
|
27
27
|
@dataclass
|
|
28
28
|
class ReasonedVerdict:
|
|
29
|
-
decision: str
|
|
30
|
-
rationale: str
|
|
31
|
-
evidence: list = field(default_factory=list)
|
|
32
|
-
hypotheses: list = field(default_factory=list)
|
|
33
|
-
source: str = "rules"
|
|
29
|
+
decision: str
|
|
30
|
+
rationale: str
|
|
31
|
+
evidence: list = field(default_factory=list)
|
|
32
|
+
hypotheses: list = field(default_factory=list)
|
|
33
|
+
source: str = "rules"
|
|
34
34
|
|
|
35
35
|
def to_dict(self) -> dict:
|
|
36
36
|
return {"decision": self.decision, "rationale": self.rationale,
|
|
@@ -61,7 +61,6 @@ class DomainReasoner:
|
|
|
61
61
|
ev = self.gather_evidence(subject, obs)
|
|
62
62
|
return self.arbitrate(subject, obs, hyps, ev)
|
|
63
63
|
|
|
64
|
-
# ── 双层增强的通用工具(v2 ③ Step4:itinerary/learning 复用)────────────
|
|
65
64
|
@staticmethod
|
|
66
65
|
def llm_available(llm, enabled: bool) -> bool:
|
|
67
66
|
"""LLM 是否真正可用:启用 + 非空 + 非 stub(无真实模型时视为不可用)。"""
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
可驱动前端"系统在想什么"实时面板,也用于调试与回放。
|
|
11
11
|
"""
|
|
12
12
|
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
13
15
|
import time
|
|
14
16
|
from dataclasses import dataclass, field
|
|
15
17
|
|
|
@@ -18,19 +20,18 @@ from dataclasses import dataclass, field
|
|
|
18
20
|
class CognitiveState:
|
|
19
21
|
"""系统当前认知状态快照。随事件总线上的活动自动更新。"""
|
|
20
22
|
|
|
21
|
-
active_goal: str = None
|
|
22
|
-
active_agent: str = None
|
|
23
|
-
semantic_focus: str = None
|
|
24
|
-
attention_scope: list = field(default_factory=list)
|
|
25
|
-
reasoning_depth: float = 0.0
|
|
26
|
-
confidence_map: dict = field(default_factory=dict)
|
|
27
|
-
activated_memories: list = field(default_factory=list)
|
|
28
|
-
last_route: str = None
|
|
29
|
-
active_processes: int = 0
|
|
30
|
-
temporal_window: str = "current_turn"
|
|
23
|
+
active_goal: Optional[str] = None
|
|
24
|
+
active_agent: Optional[str] = None
|
|
25
|
+
semantic_focus: Optional[str] = None
|
|
26
|
+
attention_scope: list = field(default_factory=list)
|
|
27
|
+
reasoning_depth: float = 0.0
|
|
28
|
+
confidence_map: dict = field(default_factory=dict)
|
|
29
|
+
activated_memories: list = field(default_factory=list)
|
|
30
|
+
last_route: Optional[str] = None
|
|
31
|
+
active_processes: int = 0
|
|
32
|
+
temporal_window: str = "current_turn"
|
|
31
33
|
updated_at: float = field(default_factory=time.time)
|
|
32
34
|
|
|
33
|
-
# ── 信号摄入(由事件驱动)──
|
|
34
35
|
|
|
35
36
|
def on_execution(self, payload: dict):
|
|
36
37
|
"""执行事件 → 更新目标/Agent/注意力。"""
|
|
@@ -54,13 +55,12 @@ class CognitiveState:
|
|
|
54
55
|
content = mu.get("content")
|
|
55
56
|
if content:
|
|
56
57
|
snippet = content[:80]
|
|
57
|
-
# 去重:已存在则先移除旧的,再插到最前(LRU 式)
|
|
58
58
|
existing = [m for m in self.activated_memories if m != snippet]
|
|
59
59
|
self.activated_memories = [snippet] + existing[:4]
|
|
60
60
|
self.temporal_window = "memory_augmented"
|
|
61
61
|
self.updated_at = time.time()
|
|
62
62
|
|
|
63
|
-
def on_process_change(self, payload: dict, active_count: int = None):
|
|
63
|
+
def on_process_change(self, payload: dict, active_count: int | None = None):
|
|
64
64
|
"""进程状态变更 → 更新活跃进程数。"""
|
|
65
65
|
if active_count is not None:
|
|
66
66
|
self.active_processes = active_count
|
|
@@ -69,14 +69,12 @@ class CognitiveState:
|
|
|
69
69
|
@staticmethod
|
|
70
70
|
def _keywords(text: str, n: int = 6) -> list:
|
|
71
71
|
import re
|
|
72
|
-
# 先按标点/空格切;若切出的片段仍很长(中文无空格),按 2-4 字滑窗取要点
|
|
73
72
|
rough = [w for w in re.split(r"[\s,,、。!?:;]", text or "") if len(w) >= 2]
|
|
74
73
|
out = []
|
|
75
74
|
for seg in rough:
|
|
76
75
|
if len(seg) <= 4:
|
|
77
76
|
out.append(seg)
|
|
78
77
|
else:
|
|
79
|
-
# 长片段取首尾 + 中部双字,作为注意力要点
|
|
80
78
|
out.append(seg[:3])
|
|
81
79
|
if len(seg) > 6:
|
|
82
80
|
out.append(seg[len(seg)//2:len(seg)//2+2])
|
|
@@ -106,25 +104,23 @@ class CognitiveStateTracker:
|
|
|
106
104
|
import threading
|
|
107
105
|
self.state = CognitiveState()
|
|
108
106
|
self.process_manager = process_manager
|
|
109
|
-
self._lock = threading.Lock()
|
|
107
|
+
self._lock = threading.Lock()
|
|
110
108
|
if bus is not None:
|
|
111
|
-
# 【P4-1】前两个是精确 channel 名 → exact=True;task.lifecycle 是刻意的前缀
|
|
112
|
-
# 订阅(收 created/queued/running/... 一族),保持默认前缀匹配。
|
|
113
109
|
bus.subscribe("execution.completed", self._on_exec, exact=True)
|
|
114
110
|
bus.subscribe("memory.updated", self._on_mem, exact=True)
|
|
115
|
-
bus.subscribe("task.lifecycle", self._on_proc)
|
|
111
|
+
bus.subscribe("task.lifecycle", self._on_proc)
|
|
116
112
|
|
|
117
113
|
def _on_exec(self, msg):
|
|
118
114
|
if msg is None:
|
|
119
115
|
return
|
|
120
116
|
with self._lock:
|
|
121
|
-
self.state.on_execution(getattr(msg, "payload", None))
|
|
117
|
+
self.state.on_execution(getattr(msg, "payload", None) or {})
|
|
122
118
|
|
|
123
119
|
def _on_mem(self, msg):
|
|
124
120
|
if msg is None:
|
|
125
121
|
return
|
|
126
122
|
with self._lock:
|
|
127
|
-
self.state.on_memory_updated(getattr(msg, "payload", None))
|
|
123
|
+
self.state.on_memory_updated(getattr(msg, "payload", None) or {})
|
|
128
124
|
|
|
129
125
|
def _on_proc(self, msg):
|
|
130
126
|
if msg is None:
|
|
@@ -132,15 +128,13 @@ class CognitiveStateTracker:
|
|
|
132
128
|
active = None
|
|
133
129
|
pm = self.process_manager
|
|
134
130
|
if pm is not None:
|
|
135
|
-
# 【深审优化】用 pm 的 O(1) active_count(),免去每次 lifecycle 事件 stats() 遍历
|
|
136
|
-
# 全部进程(O(n))。旧 pm 无此方法则回退到 stats()(scheduled+running)。
|
|
137
131
|
if hasattr(pm, "active_count"):
|
|
138
132
|
active = pm.active_count()
|
|
139
133
|
else:
|
|
140
134
|
st = pm.stats().get("by_state", {})
|
|
141
135
|
active = st.get("scheduled", 0) + st.get("running", 0)
|
|
142
136
|
with self._lock:
|
|
143
|
-
self.state.on_process_change(getattr(msg, "payload", None), active)
|
|
137
|
+
self.state.on_process_change(getattr(msg, "payload", None) or {}, active)
|
|
144
138
|
|
|
145
139
|
def snapshot(self) -> dict:
|
|
146
140
|
with self._lock:
|
|
@@ -28,10 +28,8 @@ import hashlib
|
|
|
28
28
|
import json
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
|
|
31
|
-
#: 首条记录的 prev_hash。用 64 个 0 而不是空串:一眼能看出「这是链头」。
|
|
32
31
|
GENESIS = "0" * 64
|
|
33
32
|
|
|
34
|
-
#: 从文件尾部回读的字节数:找 tail hash 只需最后一行,64KB 足够容下超长记录。
|
|
35
33
|
_TAIL_WINDOW = 65536
|
|
36
34
|
|
|
37
35
|
|
|
@@ -100,7 +98,7 @@ def verify(path) -> tuple:
|
|
|
100
98
|
return False, n, f"第 {i} 行不是合法 JSON"
|
|
101
99
|
h = rec.get("hash")
|
|
102
100
|
if not h:
|
|
103
|
-
continue
|
|
101
|
+
continue
|
|
104
102
|
if rec.get("prev_hash") != prev:
|
|
105
103
|
return False, n, (f"第 {i} 行链断:prev_hash 与上一条 hash 不符"
|
|
106
104
|
"(中间记录被删或被挪动)")
|
|
@@ -50,10 +50,10 @@ class SharedWriteAuditSink:
|
|
|
50
50
|
def __init__(self, path: Path):
|
|
51
51
|
self.path = Path(path)
|
|
52
52
|
self._lock = threading.Lock()
|
|
53
|
-
self._last_hash: str | None = None
|
|
53
|
+
self._last_hash: str | None = None
|
|
54
54
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
55
|
|
|
56
|
-
def __call__(self, evt):
|
|
56
|
+
def __call__(self, evt):
|
|
57
57
|
try:
|
|
58
58
|
rec = {
|
|
59
59
|
"ts": datetime.now().isoformat(timespec="seconds"),
|
|
@@ -69,7 +69,7 @@ class SharedWriteAuditSink:
|
|
|
69
69
|
f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
|
|
70
70
|
self._last_hash = rec["hash"]
|
|
71
71
|
except Exception:
|
|
72
|
-
pass
|
|
72
|
+
pass
|
|
73
73
|
|
|
74
74
|
def read(self, limit: int = 200) -> list:
|
|
75
75
|
"""读最近 limit 条(供诊断/工具用)。坏行跳过 —— 落盘绝不该反过来卡死读取。"""
|