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