foxclaw 0.1.0 → 0.1.2
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/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +152 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +148 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1637 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +46 -0
- package/python/pyproject.toml +265 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,1950 @@
|
|
|
1
|
+
// 缓存刷新:改动子模块时,连同顶栏版本号一起把 ?v= 递增(零构建下的显式 cache-bust)
|
|
2
|
+
import './auth.js?v=5.37';
|
|
3
|
+
import { escapeHtml } from './util.js?v=2.9';
|
|
4
|
+
import { renderMarkdown, renderReply } from './markdown.js?v=2.9';
|
|
5
|
+
import './visuals.js?v=5.36'; // 球体特效默认关 + foxclawVisual.on/off/toggle API
|
|
6
|
+
import { _origin, API, WS, _lastKernelData } from './state.js?v=5.31';
|
|
7
|
+
import { fetchAMapConfig, kpSet, renderFinance, renderLearning, renderItinerary, itinSideHTML, renderSituation, drawAMapInto, initMapSearch } from './domain.js?v=5.31';
|
|
8
|
+
import { openSetMenu, switchProvider, uiPref, openAuthSetting } from './settings.js?v=5.36'; // uiPref.ballAnim 默认 false
|
|
9
|
+
import './chat_model_picker.js?v=1.4'; // chat 输入框旁的下拉式模型选择器;v1.4 保存即切换 + BASE URL 自动补 /v1 + 模型 ID 软提示
|
|
10
|
+
|
|
11
|
+
/* ════════════ 后端数据层:接真实 Foxclaw ════════════ */
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
let activeAgent='supervisor';
|
|
16
|
+
// 三个 Agent(supervisor/finance/office) + 一个**视图** admin(行程地图,已不是 Agent)
|
|
17
|
+
const AGENT_CN={supervisor:'主管',finance:'财务',office:'办公学习',admin:'行程'};
|
|
18
|
+
|
|
19
|
+
/* 在线状态 */
|
|
20
|
+
function setUplink(on){
|
|
21
|
+
const el=document.getElementById('upStat');
|
|
22
|
+
if(el){el.textContent=on?'100%':'离线'; el.style.color=on?'var(--green)':'var(--amber)';}
|
|
23
|
+
// 副标题固定为品牌语,不再被状态文字覆盖(状态已在右上角显示)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* 鉴权提示条:后端 401 → 顶部弹一条,引导去「访问密钥」配置(只弹一次) */
|
|
27
|
+
function showAuthHint(){
|
|
28
|
+
if(document.getElementById('authHint')) return;
|
|
29
|
+
const bar=document.createElement('div');
|
|
30
|
+
bar.id='authHint';
|
|
31
|
+
bar.innerHTML='<span class="ah-ico">⚠</span><span class="ah-txt">后端已开启鉴权,本机浏览器尚未配置访问密钥,数据无法加载</span>'
|
|
32
|
+
+'<button class="ah-btn" id="ah-go">配置密钥</button><button class="ah-x" id="ah-x">✕</button>';
|
|
33
|
+
document.body.appendChild(bar);
|
|
34
|
+
document.getElementById('ah-go').onclick=()=>{ bar.remove(); openAuthSetting(); };
|
|
35
|
+
document.getElementById('ah-x').onclick=()=>bar.remove();
|
|
36
|
+
}
|
|
37
|
+
// 鉴权注入模块(auth.js)检测到 401 时回调此函数弹提示条
|
|
38
|
+
window.__foxclawAuthHint=showAuthHint;
|
|
39
|
+
|
|
40
|
+
/* 1. 加载五 Agent 状态 */
|
|
41
|
+
async function loadStatus(){
|
|
42
|
+
try{
|
|
43
|
+
const r=await fetch(`${API}/status`); if(!r.ok)throw 0;
|
|
44
|
+
const data=await r.json(); setUplink(true);
|
|
45
|
+
document.querySelectorAll('.acard').forEach(card=>{
|
|
46
|
+
const id=card.dataset.agent; const st=data[id];
|
|
47
|
+
const dot=card.querySelector('.st');
|
|
48
|
+
if(st&&dot){dot.innerHTML='<i></i>在线'; dot.style.color='var(--green)';}
|
|
49
|
+
});
|
|
50
|
+
}catch(e){ setUplink(false); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* 2. 记忆中心真实统计 */
|
|
54
|
+
/* 右栏「记忆」面板:四联统计 + 最近记忆信息条。
|
|
55
|
+
沿用 loadMemStats 这个名字 —— 全文已有 8 处调用点(状态刷新/录入后/定时),
|
|
56
|
+
改这一个函数即可让新面板处处自动刷新,无需再散布新调用。 */
|
|
57
|
+
async function loadMemStats(){
|
|
58
|
+
const set=(id,v)=>{const el=document.getElementById(id); if(el)el.textContent=v;};
|
|
59
|
+
try{
|
|
60
|
+
const d=await fetch(`${API}/memory/stats`).then(r=>r.json());
|
|
61
|
+
set('msTotal',(d.total_memories||0).toLocaleString());
|
|
62
|
+
set('msPrivate',(d.private||0).toLocaleString());
|
|
63
|
+
set('msShared',(d.shared||0).toLocaleString());
|
|
64
|
+
set('msRel',(d.relations||0).toLocaleString());
|
|
65
|
+
set('memCount',(d.total_memories||0)+' 条');
|
|
66
|
+
set('memVec',(d.total_memories||0).toLocaleString()); // 兼容旧元素(若仍存在)
|
|
67
|
+
set('memRel',(d.relations||0).toLocaleString());
|
|
68
|
+
}catch(e){}
|
|
69
|
+
// 会话列表现由 loadSessions() 渲染进同一右栏(记忆面板下),这里不再管"最近记忆列表"。
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* 右栏「知识库」面板:知识信息条 */
|
|
73
|
+
async function loadKnowList(){
|
|
74
|
+
const list=document.getElementById('knowList'); if(!list)return;
|
|
75
|
+
try{
|
|
76
|
+
const r=await fetch(`${API}/learning/analysis`).then(x=>x.json());
|
|
77
|
+
const items=r.items||[];
|
|
78
|
+
const cnt=document.getElementById('knowCount'); if(cnt)cnt.textContent=(r.total_items??items.length)+' 条';
|
|
79
|
+
if(!items.length){ list.innerHTML='<div class="kp-empty">知识库为空 · 录入学习数据后显示</div>'; return; }
|
|
80
|
+
list.innerHTML=items.slice(0,20).map((k,i)=>`<div class="info-item" data-idx="${i}">
|
|
81
|
+
<div class="it-h"><b>${escapeHtml(k.title||k.name||'知识条目')}</b>
|
|
82
|
+
${(k.topic||k.tag)?`<span class="it-tag">${escapeHtml(k.topic||k.tag)}</span>`:''}</div>
|
|
83
|
+
${k.content?`<div class="it-sum">${escapeHtml(k.content)}</div>`:''}
|
|
84
|
+
${k.source?`<div class="it-meta">来源: ${escapeHtml(k.source)}</div>`:''}</div>`).join('');
|
|
85
|
+
list.querySelectorAll('.info-item').forEach(el=>el.onclick=()=>{
|
|
86
|
+
const it=items[+el.dataset.idx];
|
|
87
|
+
if(it) showKnowledgeArticle(it); else showKnowledgeFull();
|
|
88
|
+
});
|
|
89
|
+
}catch(e){ list.innerHTML='<div class="kp-empty">后端未连接</div>'; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* 3. Agent 卡片点击切换 */
|
|
93
|
+
document.querySelectorAll('.acard').forEach(card=>{
|
|
94
|
+
card.addEventListener('click',()=>{
|
|
95
|
+
document.querySelectorAll('.acard').forEach(c=>c.classList.remove('on'));
|
|
96
|
+
card.classList.add('on');
|
|
97
|
+
activeAgent=card.dataset.agent;
|
|
98
|
+
const cmd=document.getElementById('cmd');
|
|
99
|
+
if(cmd)cmd.placeholder=`告诉我你想做什么… ${AGENT_CN[activeAgent]} Agent 将处理`;
|
|
100
|
+
loadKernel(activeAgent);
|
|
101
|
+
showInCore(activeAgent); // 当前 Agent 内容投到中央焦点空间
|
|
102
|
+
refreshAgentStatus();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
/* 顶栏右侧「财务/地图」面板入口:只开中央仪表盘,不改 activeAgent —— 对话统一走
|
|
106
|
+
输入框(主管自动分诊)。再点一次收起面板。 */
|
|
107
|
+
document.querySelectorAll('.abtn').forEach(btn=>{
|
|
108
|
+
btn.addEventListener('click',()=>{
|
|
109
|
+
const ag=btn.dataset.agent, wasOn=btn.classList.contains('on');
|
|
110
|
+
document.querySelectorAll('.abtn').forEach(b=>b.classList.remove('on'));
|
|
111
|
+
if(wasOn){ clearCore(); return; }
|
|
112
|
+
btn.classList.add('on');
|
|
113
|
+
loadKernel(ag); showInCore(ag);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
/* 统一刷新各卡状态文字:工作中 / 在线(选中) / 离线(灰) */
|
|
117
|
+
function refreshAgentStatus(){
|
|
118
|
+
document.querySelectorAll('.acard').forEach(c=>{
|
|
119
|
+
const st=c.querySelector('.st-tx'); if(!st)return;
|
|
120
|
+
if(c.classList.contains('busy')) st.textContent='工作中';
|
|
121
|
+
else if(c.classList.contains('on')) st.textContent='在线';
|
|
122
|
+
else st.textContent='离线';
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
refreshAgentStatus();
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
/* ===== 中央焦点空间:激活 Agent 显示其当前内容,空闲回到全息球 ===== */
|
|
129
|
+
/* ═══ 中间栏输出中枢(Center)═══════════════════════════════════════════════
|
|
130
|
+
中间栏有两个叠放的输出层 —— 对话浮层 coreChat(z5)/ 面板层 coreStage(z3),外加背后的 3D 核心。
|
|
131
|
+
过去每个入口自己开关这两层,常忘关另一层 → 页面盖着不退、白白渲染(用户实测踩到)。
|
|
132
|
+
现在**统一由 Center 管**:凡要在中间栏显示东西,一律走 Center.panel()/chat()/home(),
|
|
133
|
+
同一时刻只有一个视图活着,天然互斥,不必再各处补"记得关另一层"。 */
|
|
134
|
+
const Center = {
|
|
135
|
+
mode:'home',
|
|
136
|
+
panel(){ // 打开面板层(知识/模块/领域视图);返回 #coreStage 供调用方写 innerHTML
|
|
137
|
+
const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
|
|
138
|
+
document.getElementById('coreChat')?.classList.remove('show'); // 关对话浮层
|
|
139
|
+
wrap?.classList.add('busy'); stage?.classList.add('show');
|
|
140
|
+
this.mode='panel'; return stage;
|
|
141
|
+
},
|
|
142
|
+
chat(){ // 打开对话浮层(对话/会话/单轮回复);返回 #coreChat
|
|
143
|
+
const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
|
|
144
|
+
wrap?.classList.remove('busy');
|
|
145
|
+
if(stage){ stage.classList.remove('show'); stage.innerHTML=''; } // 拆面板层
|
|
146
|
+
const chat=document.getElementById('coreChat'); chat?.classList.add('show');
|
|
147
|
+
this.mode='chat'; return chat;
|
|
148
|
+
},
|
|
149
|
+
home(){ // 回到 3D 核心:两层都收起
|
|
150
|
+
const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
|
|
151
|
+
document.getElementById('coreChat')?.classList.remove('show');
|
|
152
|
+
wrap?.classList.remove('busy');
|
|
153
|
+
if(stage){ stage.classList.remove('show'); stage.innerHTML=''; }
|
|
154
|
+
this.mode='home';
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
function clearCore(){ Center.home(); _syncNav('overview'); } // 兼容旧调用名(9 处)→ 统一走中枢
|
|
158
|
+
async function showInCore(agent){
|
|
159
|
+
const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
|
|
160
|
+
// 打开中间栏面板前先关掉对话浮层(core-chat z5 会盖住 core-stage z3,否则点侧栏"像没反应")
|
|
161
|
+
document.getElementById('coreChat')?.classList.remove('show');
|
|
162
|
+
const m=KP_META[agent]||KP_META.office;
|
|
163
|
+
// 主管空闲态:回到全息球(它没有"专属任务画面")
|
|
164
|
+
if(agent==='supervisor'){ clearCore(); return; }
|
|
165
|
+
Center.panel();
|
|
166
|
+
stage.innerHTML=`<div class="cs-head"><div class="t"><span class="dot"></span>${m.title} <em>${m.tag}</em></div>
|
|
167
|
+
<span class="x" id="csClose" title="返回核心">✕</span></div>
|
|
168
|
+
<div class="cs-body" id="csBody"><div class="kp-empty" style="padding:30px 0">加载中…</div></div>`;
|
|
169
|
+
document.getElementById('csClose').onclick=clearCore;
|
|
170
|
+
const body=document.getElementById('csBody');
|
|
171
|
+
try{
|
|
172
|
+
const d=_lastKernelData[agent]||{};
|
|
173
|
+
if(agent==='admin'){
|
|
174
|
+
body.innerHTML=`<div style="position:relative;height:100%;min-height:300px">
|
|
175
|
+
<div id="coreMap" style="width:100%;height:100%;border-radius:10px;overflow:hidden"></div>
|
|
176
|
+
<div class="map-search"><input id="mapSearchInput" placeholder="🔍 搜索地点…"><div id="mapSearchResults" class="map-search-results"></div></div>
|
|
177
|
+
</div>`;
|
|
178
|
+
setTimeout(()=>{drawAMapInto('coreMap',d.map_data||[]); initMapSearch();},60);
|
|
179
|
+
}else if(agent==='office'){
|
|
180
|
+
const items=(d.items||[]);
|
|
181
|
+
// 摄入入口必须常在(此前只挂在右栏内核面板,右栏改版后就消失了 —— 回归过一次,别再删)
|
|
182
|
+
const ingest='<div class="ingest-row" style="max-width:520px;margin:0 auto 14px">'
|
|
183
|
+
+'<button class="ing-btn" onclick="document.getElementById(\'cmdAttach\')?.click()">📄 导入文件</button>'
|
|
184
|
+
+'<button class="ing-btn" onclick="document.getElementById(\'cmdFolder\')?.click()">📁 导入文件夹</button>'
|
|
185
|
+
+'<button class="ing-btn" onclick="openEntry(\'office\')">+ 写入知识点</button></div>';
|
|
186
|
+
if(!items.length){
|
|
187
|
+
body.innerHTML=ingest+'<div class="kp-empty" style="padding:24px 0">还没有录入知识 · 用上面的按钮导入文件/文件夹到知识星球(HKC),或手写一条知识点。</div>';
|
|
188
|
+
}else{
|
|
189
|
+
body.innerHTML=ingest+'<div id="_kl"></div>';
|
|
190
|
+
_renderKnowList(document.getElementById('_kl'), items, ()=>showInCore('office'));
|
|
191
|
+
}
|
|
192
|
+
}else if(agent==='finance'){
|
|
193
|
+
body.innerHTML=renderFinance(d);
|
|
194
|
+
}
|
|
195
|
+
}catch(e){ body.innerHTML='<div class="kp-empty">后端未连接</div>'; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* 3b. 内核详情面板:随 Agent 切换展示对应内核数据 */
|
|
199
|
+
const KP_META={
|
|
200
|
+
supervisor:{title:'全局态势',tag:'Situation'},
|
|
201
|
+
finance:{title:'财务驾驶舱',tag:'Finance'},
|
|
202
|
+
admin:{title:'行程规划',tag:'Itinerary'}, // 视图,不是 Agent
|
|
203
|
+
office:{title:'知识与学习',tag:'Knowledge'},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
async function loadKernel(agent){
|
|
207
|
+
const m=KP_META[agent]||KP_META.office;
|
|
208
|
+
try{
|
|
209
|
+
// 每个分支独立容错:数据请求失败(如首次 HKC 装配慢/500)→ 用空数据 {} 渲染面板,
|
|
210
|
+
// **保留录入/导入入口**(它们不依赖数据),而不是抛到外层 catch 把整个面板变成"后端未连接"。
|
|
211
|
+
if(agent==='finance'){
|
|
212
|
+
const d=await fetch(`${API}/finance/analysis?period=2026-06¤t_balance=11000`).then(r=>r.json()).catch(()=>({}));
|
|
213
|
+
_lastKernelData.finance=d; kpSet(m.title,m.tag,renderFinance(d||{}));
|
|
214
|
+
}else if(agent==='office'){
|
|
215
|
+
const d=await fetch(`${API}/learning/analysis`).then(r=>r.json()).catch(()=>({}));
|
|
216
|
+
_lastKernelData.office=d; kpSet(m.title,m.tag,renderLearning(d||{}));
|
|
217
|
+
}else if(agent==='admin'){
|
|
218
|
+
const d=await fetch(`${API}/itinerary/plan`).then(r=>r.json()).catch(()=>({}));
|
|
219
|
+
_lastKernelData.admin=d; kpSet(m.title,m.tag,renderItinerary(d||{}));
|
|
220
|
+
if(autoMap)openOverlay('admin'); // 地图自动全屏
|
|
221
|
+
}else if(agent==='supervisor'){
|
|
222
|
+
const d=await fetch(`${API}/orchestration/situation`).then(r=>r.json()).catch(()=>({}));
|
|
223
|
+
_lastKernelData.supervisor=d; kpSet(m.title,m.tag,renderSituation(d||{}));
|
|
224
|
+
}else{
|
|
225
|
+
kpSet(m.title,m.tag,document.getElementById('taskList')?.outerHTML||'<div class="kp-empty">暂无任务</div>');
|
|
226
|
+
}
|
|
227
|
+
}catch(e){ kpSet(m.title,m.tag,'<div class="kp-empty">后端未连接 · 该面板需真实数据</div>'); }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* 领域文件导入:上传 csv/xlsx/json → /api/{domain}/import/file(超越 csv_path)*/
|
|
231
|
+
function importDomainFile(agent){
|
|
232
|
+
const map={finance:{d:'finance',cn:'交易'}, office:{d:'learning',cn:'知识点'}, admin:{d:'itinerary',cn:'行程点'}};
|
|
233
|
+
const m=map[agent]||map.finance;
|
|
234
|
+
let inp=document.getElementById('_domainImp');
|
|
235
|
+
if(!inp){ inp=document.createElement('input'); inp.type='file'; inp.id='_domainImp'; inp.style.display='none'; inp.accept='.csv,.xlsx,.json'; document.body.appendChild(inp); }
|
|
236
|
+
inp.onchange=async()=>{
|
|
237
|
+
const f=inp.files&&inp.files[0]; inp.value=''; if(!f) return;
|
|
238
|
+
showReplyPop(`📁 导入${m.cn}:${f.name}`, agent, true);
|
|
239
|
+
fillReplyPop('', '⬆ 上传解析中…', 'ok');
|
|
240
|
+
try{
|
|
241
|
+
const fd=new FormData(); fd.append('file', f);
|
|
242
|
+
const r=await fetch(`${API}/${m.d}/import/file`,{method:'POST',body:fd});
|
|
243
|
+
const j=await r.json().catch(()=>({}));
|
|
244
|
+
if(!r.ok){ fillReplyPop('', `导入失败(${r.status}):${String(j.detail||'').slice(0,120)}`, 'err'); return; }
|
|
245
|
+
fillReplyPop(`✓ 已导入 ${j.imported||0} 条${m.cn}`, j.note||'刷新面板查看', 'ok');
|
|
246
|
+
if(typeof loadKernel==='function'){ try{ loadKernel(agent); }catch(e){} }
|
|
247
|
+
}catch(e){ fillReplyPop('', '请求失败:后端未连接', 'err'); }
|
|
248
|
+
};
|
|
249
|
+
inp.click();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* 4. 发任务到后端 */
|
|
253
|
+
/* Agent 忙碌态:有任务时卡片亮主题色 + 显示任务标题 */
|
|
254
|
+
const agentTasks={};
|
|
255
|
+
function setAgentBusy(agent, task){
|
|
256
|
+
agentTasks[agent]=task;
|
|
257
|
+
const card=document.querySelector(`.acard[data-agent="${agent}"]`);
|
|
258
|
+
if(!card)return;
|
|
259
|
+
card.classList.add('busy');
|
|
260
|
+
const lbl=card.querySelector('.task-label');
|
|
261
|
+
if(lbl) lbl.textContent='▸ '+task;
|
|
262
|
+
if(typeof refreshAgentStatus==='function') refreshAgentStatus();
|
|
263
|
+
}
|
|
264
|
+
function clearAgentBusy(agent){
|
|
265
|
+
delete agentTasks[agent];
|
|
266
|
+
const card=document.querySelector(`.acard[data-agent="${agent}"]`);
|
|
267
|
+
if(card){
|
|
268
|
+
card.classList.remove('busy');
|
|
269
|
+
const l=card.querySelector('.task-label'); if(l)l.textContent='';
|
|
270
|
+
if(typeof refreshAgentStatus==='function') refreshAgentStatus();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function transmit(){
|
|
275
|
+
const inp=document.getElementById('cmd'); let task=(inp.value||'').trim();
|
|
276
|
+
const hasImgs=_pendingImages.length>0, hasFiles=_pendingFiles.length>0;
|
|
277
|
+
if(!task && !hasImgs && !hasFiles)return;
|
|
278
|
+
// 斜杠指令:以 / 开头交给命令处理器,不发给后端(如 /搜索 记忆架构、/工具、/清库 确认)
|
|
279
|
+
if(task[0]==='/'){ inp.value=''; hideSlashMenu(); runSlash(task); return; }
|
|
280
|
+
inp.value='';
|
|
281
|
+
if((hasImgs||hasFiles) && !task) task=hasImgs?'请描述并分析这张图片':'请阅读并总结这个文件';
|
|
282
|
+
prependTask(task,activeAgent,'pending');
|
|
283
|
+
// 标记当前 Agent 忙碌 + 显示任务标题
|
|
284
|
+
setAgentBusy(activeAgent, task);
|
|
285
|
+
// 数据流:核心连发数束光点射向当前 Agent(看得见它在工作)
|
|
286
|
+
if(window._emitStream){ for(let i=0;i<3;i++) setTimeout(()=>window._emitStream(activeAgent), i*180); }
|
|
287
|
+
// 弹出回复浮窗(加载态);带图则把用户图片缩略图渲染进提问区
|
|
288
|
+
const qThumbs=hasImgs?_pendingImages.map(x=>x.url):null;
|
|
289
|
+
const qFiles=hasFiles?_pendingFiles.map(x=>({name:x.name,size:x.size,url:x.url})):null;
|
|
290
|
+
showReplyPop(task, activeAgent, false, qThumbs, qFiles);
|
|
291
|
+
// 多模态/附件:带图或带文件必走流式(/chat/stream 消费 images/attachments);发送后清空附件条
|
|
292
|
+
if(hasImgs || hasFiles){
|
|
293
|
+
const imgs=hasImgs?_pendingImages.map(x=>({media_type:x.media_type, data:x.data})):null;
|
|
294
|
+
const atts=hasFiles?_pendingFiles.map(x=>({name:x.name,size:x.size,mime:x.mime,url:x.url,text:x.text})):null;
|
|
295
|
+
clearPendingImages(); clearPendingFiles();
|
|
296
|
+
_refTriggers={}; // 流式/多模态无技能路由,清掉 @引用残留(仅任务模式经 applyRefs 生效)
|
|
297
|
+
return transmitStream(task, activeAgent, imgs, atts);
|
|
298
|
+
}
|
|
299
|
+
if(chatModeStream){ _refTriggers={}; return transmitStream(task, activeAgent); }
|
|
300
|
+
// 任务模式:@引用把技能触发词带入后端 task(用户可见输入保留 @名称)
|
|
301
|
+
const outTask=applyRefs(task);
|
|
302
|
+
// 领域 Agent 任务 → 流式认知管线(逐 stage 进度 + act 逐字);主管编排任务不支持流式
|
|
303
|
+
// (/api/task/stream 只跑单 Agent 管线、不做多 Agent 分派),仍走阻塞 /api/task 保住编排能力。
|
|
304
|
+
if(activeAgent!=='supervisor'){ _refTriggers={}; return transmitTaskStream(outTask, activeAgent); }
|
|
305
|
+
try{
|
|
306
|
+
const r=await fetch(`${API}/task`,{method:'POST',headers:{'Content-Type':'application/json'},
|
|
307
|
+
body:JSON.stringify({task:outTask,agent_id:activeAgent})});
|
|
308
|
+
const data=await r.json();
|
|
309
|
+
if(data.error==='harness_blocked'){
|
|
310
|
+
markTask(task,'blocked');
|
|
311
|
+
fillReplyPop(`⚠ 任务被 Harness 拦截`, `偏离原因:${data.reason||'语义漂移超过阈值'}(Δ=${(data.delta||0).toFixed?.(3)||data.delta||'—'})`, 'blocked');
|
|
312
|
+
}else{
|
|
313
|
+
markTask(task,'ok');
|
|
314
|
+
const res=data.result||{};
|
|
315
|
+
// 多 Agent 顺序执行:展示每一步的子任务 + 处理 Agent + 回复
|
|
316
|
+
if(res.dispatched_to==='multi' && Array.isArray(res.multi_steps)){
|
|
317
|
+
let html=`<div style="margin-bottom:10px;color:var(--cyan);font-weight:600">${escapeHtml(res.summary||'多步任务')}</div>`;
|
|
318
|
+
res.multi_steps.forEach(s=>{
|
|
319
|
+
const agName=escapeHtml(AGENT_CN[s.agent]||s.agent);
|
|
320
|
+
const blockedTag=s.blocked?' <span style="color:var(--amber)">[被拦截]</span>':'';
|
|
321
|
+
html+=`<div style="margin:8px 0;padding:8px 10px;border-left:2px solid var(--cyan);background:rgba(0,217,255,.05)">
|
|
322
|
+
<div style="font-size:11px;color:var(--ink-dim);margin-bottom:3px">第${escapeHtml(s.step)}步 · ${agName} Agent${blockedTag}</div>
|
|
323
|
+
<div style="font-size:12px;color:var(--cyan)">「${escapeHtml(s.subtask)}」</div>
|
|
324
|
+
<div style="font-size:12px;margin-top:4px">${escapeHtml(s.output||'(无输出)')}</div>
|
|
325
|
+
</div>`;
|
|
326
|
+
});
|
|
327
|
+
fillReplyPop(html, '⤷ 主管已拆解为多步,跨 Agent 顺序执行', 'ok', true);
|
|
328
|
+
loadMemStats();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const reply=res.sub_result||res.summary||res.reply||(typeof res==='string'?res:JSON.stringify(res));
|
|
332
|
+
let via='';
|
|
333
|
+
if(res.dispatched_to&&res.dispatched_to!=='supervisor'&&res.dispatched_to!==activeAgent){
|
|
334
|
+
via=`⤷ 主管已分派给 ${AGENT_CN[res.dispatched_to]||res.dispatched_to} Agent 处理`;
|
|
335
|
+
// 实际处理的 Agent 也标忙碌
|
|
336
|
+
setAgentBusy(res.dispatched_to, task);
|
|
337
|
+
}else if(res.self_answered){
|
|
338
|
+
via='⤷ 主管直接答复';
|
|
339
|
+
}
|
|
340
|
+
fillReplyPop('<div class="cc-md">'+renderMarkdown(reply)+'</div>', via, 'ok', true);
|
|
341
|
+
}
|
|
342
|
+
loadMemStats();
|
|
343
|
+
}catch(e){
|
|
344
|
+
markTask(task,'blocked');
|
|
345
|
+
fillReplyPop('', '请求失败:后端未连接,或模型未配置 Key', 'err');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/* 回复显示在中央对话层(最上层)*/
|
|
349
|
+
function showReplyPop(task, agent, force, qImages, qFiles){
|
|
350
|
+
// 偏好关闭则不弹被动回复;但 force=true(如用户主动发起的文件摄入)必须可见,
|
|
351
|
+
// 否则点了 📎 却因偏好静默 → 屏幕零反应,以为坏了。
|
|
352
|
+
if(!force && typeof uiPref!=='undefined' && !uiPref.autoChat) return;
|
|
353
|
+
document.getElementById('ccAgent').textContent=`${AGENT_CN[agent]||agent} Agent`;
|
|
354
|
+
// 重置为单轮结构(对话线程 renderChatThread 可能已替换过 cc-body,这里恢复 ccQ/ccReply/ccStatus)
|
|
355
|
+
const _b=document.querySelector('#coreChat .cc-body');
|
|
356
|
+
if(_b && !document.getElementById('ccQ'))
|
|
357
|
+
_b.innerHTML='<div class="cc-q" id="ccQ"></div><div id="ccReply"></div><div class="cc-status" id="ccStatus"></div>';
|
|
358
|
+
let qh='你:'+escapeHtml(task);
|
|
359
|
+
if(qImages && qImages.length){ // 用户随问题发的图片 → 提问区渲染缩略图(本地 dataURL,可信)
|
|
360
|
+
qh+='<div class="cc-qimgs">'+qImages.map(u=>`<img class="cc-qimg" src="${u}" alt="">`).join('')+'</div>';
|
|
361
|
+
}
|
|
362
|
+
if(qFiles && qFiles.length){ // 用户随问题发的附件 → 提问区渲染文件卡片(可下载)
|
|
363
|
+
qh+=_attachmentsHtml(qFiles);
|
|
364
|
+
}
|
|
365
|
+
document.getElementById('ccQ').innerHTML=qh;
|
|
366
|
+
document.getElementById('ccReply').innerHTML='<div class="cc-loading">◌ 思考中…(真实模型回复需几秒)</div>';
|
|
367
|
+
document.getElementById('ccStatus').textContent='';
|
|
368
|
+
Center.chat();
|
|
369
|
+
}
|
|
370
|
+
function fillReplyPop(reply, status, kind, isHtml){
|
|
371
|
+
const rp=document.getElementById('ccReply'), st=document.getElementById('ccStatus');
|
|
372
|
+
if(isHtml){ rp.innerHTML=reply||''; }
|
|
373
|
+
else if(reply){ rp.textContent=reply; } else { rp.innerHTML=''; }
|
|
374
|
+
st.textContent=status||'';
|
|
375
|
+
if(kind==='blocked'){ st.style.color='var(--amber)'; rp.style.color='var(--amber)'; }
|
|
376
|
+
else if(kind==='err'){ st.style.color='var(--pink)'; }
|
|
377
|
+
else { st.style.color='var(--ink-dim)'; rp.style.color='var(--ink)'; }
|
|
378
|
+
}
|
|
379
|
+
document.getElementById('ccClose')?.addEventListener('click',()=>document.getElementById('coreChat')?.classList.remove('show'));
|
|
380
|
+
document.querySelector('.cmdbar .send')?.addEventListener('click',transmit);
|
|
381
|
+
document.getElementById('cmd')?.addEventListener('keydown',e=>{if(e.key==='Enter')transmit();});
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
/* ── 附件摄入:chat 栏 📎 → 文件进 HKC 知识库(异步 + 进度轮询)──────────────
|
|
385
|
+
交互层与知识层打通:对话框既能聊天,也能直接喂文件蒸馏成长期知识。
|
|
386
|
+
走 /knowledge/ingest/file/async(秒返 job_id,后台线程池抽取),前端轮询进度。*/
|
|
387
|
+
const _INGEST_MAX_BYTES=20*1024*1024;
|
|
388
|
+
let _ingestBusy=false; // 串行守卫:一次只摄入一个文件,避免并发轮询串写同一进度弹层
|
|
389
|
+
(function(){
|
|
390
|
+
const btn=document.getElementById('cmdAttach'), inp=document.getElementById('cmdFile');
|
|
391
|
+
if(!btn||!inp) return;
|
|
392
|
+
btn.addEventListener('click',()=>inp.click());
|
|
393
|
+
inp.addEventListener('change',async ()=>{
|
|
394
|
+
const f=inp.files&&inp.files[0]; inp.value=''; if(!f) return;
|
|
395
|
+
if(_ingestBusy){
|
|
396
|
+
showReplyPop(`📎 ${f.name}`, activeAgent, true);
|
|
397
|
+
fillReplyPop('', '请等当前文件摄入完成再上传下一个', 'err'); return;
|
|
398
|
+
}
|
|
399
|
+
_ingestBusy=true;
|
|
400
|
+
try{ await ingestChatFile(f); } finally{ _ingestBusy=false; }
|
|
401
|
+
});
|
|
402
|
+
})();
|
|
403
|
+
/* 若知识星球 iframe 已打开,刷新它拉最新图(同源 /planet/);未打开无需动作(下次懒加载自然是新数据)*/
|
|
404
|
+
function _refreshPlanet(){
|
|
405
|
+
try{ const fr=document.getElementById('planetFrame'); const w=fr&&fr.contentWindow;
|
|
406
|
+
if(w && fr.src && fr.src!=='about:blank' && typeof w.loadData==='function') w.loadData(); }catch(e){}
|
|
407
|
+
}
|
|
408
|
+
/* 摄入单个文件:上传 + 轮询,返回 {ok,ku_count,error,skipped};不弹窗(由调用方展示),不抛异常。
|
|
409
|
+
onStatus(msg) 可选进度回调。供单文件(📎)与文件夹(📁)逐个摄入共用。 */
|
|
410
|
+
async function _ingestFileOnce(file, onStatus, collection){
|
|
411
|
+
if(file.size>_INGEST_MAX_BYTES) return {ok:false, skipped:true, error:`过大(${(file.size/1048576).toFixed(1)}MB>20MB)`};
|
|
412
|
+
let job;
|
|
413
|
+
try{
|
|
414
|
+
const fd=new FormData(); fd.append('file', file);
|
|
415
|
+
// 文件名(去扩展名)当来源标题 → 文件 Evidence 以「文件名/章节名」命名(二级)
|
|
416
|
+
fd.append('source_title', (file.name||'').replace(/\.[^.]+$/,''));
|
|
417
|
+
// 文件夹名 → 合集(一级);后端据此建/挂合集节点、并让 KEE 按合集判同源
|
|
418
|
+
if(collection) fd.append('collection', collection);
|
|
419
|
+
const r=await fetch(`${_origin}/knowledge/ingest/file/async`,{method:'POST',body:fd});
|
|
420
|
+
if(!r.ok) return {ok:false, error: r.status===415?'不支持的格式': r.status===503?'知识核心未启用':`上传失败(${r.status})`};
|
|
421
|
+
job=await r.json();
|
|
422
|
+
}catch(e){ return {ok:false, error:'后端未连接'}; }
|
|
423
|
+
const jobId=job.job_id;
|
|
424
|
+
for(let i=0;i<600;i++){ // 每文件最多 ~10 分钟
|
|
425
|
+
await new Promise(res=>setTimeout(res,1200));
|
|
426
|
+
let j;
|
|
427
|
+
try{ j=await fetch(`${_origin}/knowledge/ingest/jobs/${encodeURIComponent(jobId)}`).then(x=>x.json()); }
|
|
428
|
+
catch(e){ continue; }
|
|
429
|
+
if(j.status==='running'||j.status==='queued'){
|
|
430
|
+
if(onStatus){ const pct=j.total>0?Math.round(j.processed/j.total*100):0; onStatus(`抽取中 ${j.processed}/${j.total||'?'} 段${j.total>0?' ('+pct+'%)':''}`); }
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if(j.status==='done') return {ok:true, ku_count:j.ku_count||0};
|
|
434
|
+
if(j.status==='error') return {ok:false, error:(j.error||'未知错误').slice(0,120)};
|
|
435
|
+
}
|
|
436
|
+
return {ok:false, error:'摄入超时'};
|
|
437
|
+
}
|
|
438
|
+
async function ingestChatFile(file){
|
|
439
|
+
showReplyPop(`📎 摄入文件:${file.name}`, activeAgent, true);
|
|
440
|
+
fillReplyPop('', '⬆ 上传中…', 'ok');
|
|
441
|
+
const res=await _ingestFileOnce(file, msg=>fillReplyPop('', '⚙ '+msg, 'ok'));
|
|
442
|
+
if(res.ok){
|
|
443
|
+
fillReplyPop(`✓ 已摄入 ${file.name}`,
|
|
444
|
+
`新增 ${res.ku_count} 条知识(KU)已进入知识星球。${res.ku_count===0?'(未配置 LLM 时抽取降级,可能为 0)':''}`, 'ok');
|
|
445
|
+
_refreshPlanet();
|
|
446
|
+
}else{
|
|
447
|
+
fillReplyPop('', (res.skipped?'跳过:':'摄入失败:')+res.error, 'err');
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* ── 文件夹摄入:📁 一次选一个文件夹 → 过滤支持格式 → 顺序逐个摄入(不做多文件夹/并发批量)── */
|
|
452
|
+
const _INGEST_EXTS=['.pdf','.md','.markdown','.txt','.epub','.mobi','.azw3','.azw','.docx','.xlsx'];
|
|
453
|
+
const _FOLDER_MAX_FILES=100; // 单文件夹一次最多摄入的文件数(防选到超大目录顺序摄入几小时+烧 LLM)
|
|
454
|
+
function _supportedFile(name){ const n=(name||'').toLowerCase(); return _INGEST_EXTS.some(e=>n.endsWith(e)); }
|
|
455
|
+
function _folderHtml(folder, cur, total, current, log, totalKu){
|
|
456
|
+
let h=`<div style="color:var(--cyan);font-weight:600;margin-bottom:6px">📁 ${escapeHtml(folder)} · ${cur}/${total}${totalKu?` · 累计 ${totalKu} KU`:''}</div>`;
|
|
457
|
+
if(current) h+=`<div style="font-size:12px;color:var(--ink-dim);margin-bottom:6px">当前:${escapeHtml(current)}</div>`;
|
|
458
|
+
log.slice(-8).forEach(e=>{ h+=`<div style="font-size:11.5px;margin:2px 0">${e.ok?'✓':'✗'} ${escapeHtml(e.name)} ${e.ok?`<span style="color:var(--ink-faint)">+${e.ku} KU</span>`:`<span style="color:var(--amber)">${escapeHtml(e.err||'')}</span>`}</div>`; });
|
|
459
|
+
return h;
|
|
460
|
+
}
|
|
461
|
+
async function ingestFolder(fileList){
|
|
462
|
+
const all=[...fileList];
|
|
463
|
+
let files=all.filter(f=>_supportedFile(f.name));
|
|
464
|
+
const folderName=((all[0]&&all[0].webkitRelativePath)||'').split('/')[0]||'文件夹';
|
|
465
|
+
showReplyPop(`📁 摄入文件夹:${folderName}`, activeAgent, true);
|
|
466
|
+
if(!files.length){
|
|
467
|
+
fillReplyPop('', `「${folderName}」里没有可摄入的文件(支持 pdf/txt/md/docx/xlsx/epub 等;共扫描 ${all.length} 个)`, 'err'); return;
|
|
468
|
+
}
|
|
469
|
+
let capNote='';
|
|
470
|
+
if(files.length>_FOLDER_MAX_FILES){ // 超上限 → 只摄前 N 个,提示分批(防失控)
|
|
471
|
+
capNote=` · 超上限,仅摄前 ${_FOLDER_MAX_FILES}(共 ${files.length},请分批)`;
|
|
472
|
+
files=files.slice(0, _FOLDER_MAX_FILES);
|
|
473
|
+
}
|
|
474
|
+
const N=files.length; let doneCount=0, failCount=0, totalKu=0, done=0; const log=[];
|
|
475
|
+
// 并发摄入:后端线程池 max_workers=2,前端开 3 个 worker 正好把池占满(匹配星球吞吐)。
|
|
476
|
+
// 之前严格串行(一次 1 个文件)比星球慢一倍。合集 find-or-create 已加锁,并发安全。
|
|
477
|
+
const CONC=Math.min(3, N); let idx=0;
|
|
478
|
+
async function _worker(){
|
|
479
|
+
while(idx<N){
|
|
480
|
+
const f=files[idx++];
|
|
481
|
+
const res=await _ingestFileOnce(f, null, folderName); // folderName=合集,文件按文件名入库
|
|
482
|
+
done++;
|
|
483
|
+
if(res.ok){ doneCount++; totalKu+=res.ku_count; log.push({name:f.name, ok:true, ku:res.ku_count}); }
|
|
484
|
+
else{ failCount++; log.push({name:f.name, ok:false, err:res.error}); }
|
|
485
|
+
fillReplyPop(_folderHtml(folderName, done, N, '', log, totalKu), '📁 并发摄入中…', 'ok', true);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
await Promise.all(Array.from({length:CONC}, _worker));
|
|
489
|
+
fillReplyPop(_folderHtml(folderName, N, N, '', log, totalKu),
|
|
490
|
+
`✓ 文件夹完成:${doneCount}/${N} 个文件,共 ${totalKu} 条知识${failCount?`,${failCount} 个失败/跳过`:''}${capNote}`, 'ok', true);
|
|
491
|
+
_refreshPlanet();
|
|
492
|
+
}
|
|
493
|
+
(function(){
|
|
494
|
+
const btn=document.getElementById('cmdFolder'), inp=document.getElementById('cmdFolderInput');
|
|
495
|
+
if(!btn||!inp) return;
|
|
496
|
+
btn.addEventListener('click',()=>inp.click());
|
|
497
|
+
inp.addEventListener('change',async ()=>{
|
|
498
|
+
// 先把 FileList 拷成数组再清空输入 —— inp.value='' 会清空 inp.files,
|
|
499
|
+
// 而 fs 若是它的活引用就会一起变空(fs.length→0)导致下面直接 return、什么都不做。
|
|
500
|
+
const fs=[...inp.files]; inp.value=''; if(!fs.length) return;
|
|
501
|
+
if(_ingestBusy){ showReplyPop('📁 文件夹', activeAgent, true); fillReplyPop('', '请等当前摄入完成再选文件夹', 'err'); return; }
|
|
502
|
+
_ingestBusy=true;
|
|
503
|
+
try{ await ingestFolder(fs); } finally{ _ingestBusy=false; }
|
|
504
|
+
});
|
|
505
|
+
})();
|
|
506
|
+
|
|
507
|
+
/* 流式/任务 模式开关:默认流式(逐字直答);任务模式走编排(/api/task,不流式) */
|
|
508
|
+
let chatModeStream=true;
|
|
509
|
+
(function(){
|
|
510
|
+
const el=document.getElementById('chatMode');
|
|
511
|
+
if(!el)return;
|
|
512
|
+
const sync=()=>{ el.textContent=chatModeStream?'⚡ 流式':'🔧 任务'; el.classList.toggle('task',!chatModeStream);
|
|
513
|
+
const cmd=document.getElementById('cmd'); if(cmd)cmd.placeholder=chatModeStream?'和我聊聊,逐字回复…':'交给我一个任务,主管自动分诊调度'; };
|
|
514
|
+
el.addEventListener('click',()=>{ chatModeStream=!chatModeStream; sync(); });
|
|
515
|
+
sync();
|
|
516
|
+
})();
|
|
517
|
+
|
|
518
|
+
/* ===== 工具菜单:输入框左侧「+」→ 上方竖排弹窗(模式/上传/文件夹/图片/连接器)=====
|
|
519
|
+
原先这些是 chat 栏一排横向图标,太挤。现收进弹窗;菜单项点击的是**隐藏保留的原按钮**,
|
|
520
|
+
因此上传/文件夹/图片/连接器/模式切换全部复用既有逻辑,零重复实现。 */
|
|
521
|
+
function toolMenuOpen(){ const m=document.getElementById('toolMenu'); return !!(m&&m.classList.contains('show')); }
|
|
522
|
+
function hideToolMenu(){ const m=document.getElementById('toolMenu'); if(m)m.classList.remove('show'); }
|
|
523
|
+
function _toolRows(){
|
|
524
|
+
const stream=chatModeStream;
|
|
525
|
+
const hit=id=>{ const b=document.getElementById(id); if(b)b.click(); };
|
|
526
|
+
return [
|
|
527
|
+
{ic: stream?'⚡':'🔧', b: stream?'流式对话':'编排任务', s:'点击切换',
|
|
528
|
+
act:()=>{ hit('chatMode'); renderToolMenu(); }}, // 切完不关,原地看到变化
|
|
529
|
+
{ic:'📎', b:'上传文件', s:'摄入知识库', act:()=>{ hideToolMenu(); hit('cmdAttach'); }},
|
|
530
|
+
{ic:'📁', b:'导入文件夹', s:'逐个摄入', act:()=>{ hideToolMenu(); hit('cmdFolder'); }},
|
|
531
|
+
{ic:'🖼', b:'附加图片', s:'发给视觉模型', act:()=>{ hideToolMenu(); hit('cmdImg'); }},
|
|
532
|
+
{ic:'📄', b:'附件文件', s:'随问题发给 AI', act:()=>{ hideToolMenu(); hit('cmdAttachFile'); }},
|
|
533
|
+
{ic:'🔌', b:'连接器/插件', s:'MCP · 技能', act:()=>{ hideToolMenu(); hit('cmdConn'); }},
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
function renderToolMenu(){
|
|
537
|
+
const m=document.getElementById('toolMenu'); if(!m)return;
|
|
538
|
+
const rows=_toolRows();
|
|
539
|
+
m.innerHTML=rows.map((r,i)=>`<div class="tool-item" data-i="${i}"><span class="ti-ic">${r.ic}</span>
|
|
540
|
+
<span class="ti-tx"><b>${escapeHtml(r.b)}</b><span>${escapeHtml(r.s)}</span></span></div>`).join('');
|
|
541
|
+
m.querySelectorAll('.tool-item').forEach(el=>el.addEventListener('click',e=>{ e.stopPropagation(); rows[+el.dataset.i].act(); }));
|
|
542
|
+
}
|
|
543
|
+
(function(){
|
|
544
|
+
const btn=document.getElementById('cmdMore'); if(!btn)return;
|
|
545
|
+
btn.addEventListener('click',e=>{
|
|
546
|
+
e.stopPropagation();
|
|
547
|
+
if(toolMenuOpen()){ hideToolMenu(); return; }
|
|
548
|
+
hideSlashMenu(); hideAtMenu(); // 同一时刻只开一个浮层
|
|
549
|
+
const p=document.getElementById('connPanel'); if(p)p.classList.remove('show');
|
|
550
|
+
renderToolMenu();
|
|
551
|
+
document.getElementById('toolMenu').classList.add('show');
|
|
552
|
+
});
|
|
553
|
+
document.addEventListener('click',e=>{ // 点外部关闭
|
|
554
|
+
const m=document.getElementById('toolMenu'), tools=document.getElementById('cmdTools');
|
|
555
|
+
if(!toolMenuOpen() || !m) return;
|
|
556
|
+
if(m.contains(e.target) || e.target.id==='cmdMore') return;
|
|
557
|
+
// 菜单项内部是用 .click() 触发隐藏原按钮的,那个合成事件会冒泡到 document——
|
|
558
|
+
// 不能把它当"点外部"而关掉菜单(否则切模式时菜单会意外收起)
|
|
559
|
+
if(tools && tools.contains(e.target)) return;
|
|
560
|
+
hideToolMenu();
|
|
561
|
+
});
|
|
562
|
+
})();
|
|
563
|
+
|
|
564
|
+
/* ===== 斜杠指令:输入框以 / 开头弹命令菜单,复用现有操作(纯前端,零新后端)=====
|
|
565
|
+
输入 / → 弹菜单;↑↓ 选择、Enter/Tab 执行或补全、Esc 关闭。
|
|
566
|
+
带 <参数> 的命令(如 /搜索)Enter 先补全命令名,再输参数回车执行。 */
|
|
567
|
+
const SLASH_COMMANDS=[
|
|
568
|
+
{cmd:'搜索', icon:'🔍', arg:'关键词', desc:'检索知识库', aliases:['search'], run:(q)=>slashSearch(q)},
|
|
569
|
+
{cmd:'摄入', icon:'📎', desc:'上传文件进知识库', aliases:['上传','upload'], run:()=>{ const f=document.getElementById('cmdFile'); if(f)f.click(); }},
|
|
570
|
+
{cmd:'财务', icon:'💰', desc:'打开财务驾驶舱', aliases:['finance'], run:()=>openAgentPanel('finance')},
|
|
571
|
+
{cmd:'地图', icon:'🗺', desc:'打开行程地图', aliases:['行程','admin','map'], run:()=>openAgentPanel('admin')},
|
|
572
|
+
{cmd:'工具', icon:'⚒', desc:'打开工具中心', aliases:['tools'], run:()=>{ try{showModuleInCore('tools');}catch(e){} }},
|
|
573
|
+
{cmd:'星球', icon:'🌐', desc:'打开知识星球', aliases:['planet'], run:()=>{ const b=document.getElementById('planetLaunch'); if(b)b.click(); }},
|
|
574
|
+
{cmd:'设置', icon:'⚙', desc:'打开设置菜单', aliases:['settings'], run:()=>{ try{openSetMenu();}catch(e){} }},
|
|
575
|
+
{cmd:'流式', icon:'⚡', desc:'切到流式对话(逐字直答)', aliases:['stream'], run:()=>{ const el=document.getElementById('chatMode'); if(el&&!chatModeStream)el.click(); }},
|
|
576
|
+
{cmd:'任务', icon:'🔧', desc:'切到编排任务模式', aliases:['task'], run:()=>{ const el=document.getElementById('chatMode'); if(el&&chatModeStream)el.click(); }},
|
|
577
|
+
{cmd:'帮助', icon:'❓', desc:'列出所有斜杠命令', aliases:['help','?'], run:()=>slashHelp()},
|
|
578
|
+
{cmd:'清库', icon:'🗑', danger:true, arg:'确认', desc:'清空知识库(需 /清库 确认)', aliases:['reset'], run:(a)=>slashReset(a)},
|
|
579
|
+
];
|
|
580
|
+
function openAgentPanel(ag){ try{ loadKernel(ag); showInCore(ag); }catch(e){} }
|
|
581
|
+
function slashFind(name){
|
|
582
|
+
name=(name||'').toLowerCase();
|
|
583
|
+
return SLASH_COMMANDS.find(c=>c.cmd.toLowerCase()===name || (c.aliases||[]).some(a=>a.toLowerCase()===name));
|
|
584
|
+
}
|
|
585
|
+
function runSlash(text){
|
|
586
|
+
const body=text.slice(1).trim();
|
|
587
|
+
const sp=body.indexOf(' ');
|
|
588
|
+
const name=sp<0?body:body.slice(0,sp);
|
|
589
|
+
const arg=sp<0?'':body.slice(sp+1).trim();
|
|
590
|
+
const c=slashFind(name);
|
|
591
|
+
if(!c){ showReplyPop('/'+body, activeAgent, true); fillReplyPop('', `未知命令 /${name}。输入 /帮助 查看全部`, 'err'); return; }
|
|
592
|
+
try{ c.run(arg); }catch(e){ showReplyPop('/'+body, activeAgent, true); fillReplyPop('', '命令执行出错:'+(e&&e.message||e), 'err'); }
|
|
593
|
+
}
|
|
594
|
+
async function slashSearch(q){
|
|
595
|
+
q=(q||'').trim();
|
|
596
|
+
if(!q){ showReplyPop('/搜索', activeAgent, true); fillReplyPop('', '用法:/搜索 <关键词>', 'err'); return; }
|
|
597
|
+
showReplyPop(`🔍 搜索:${q}`, activeAgent, true); fillReplyPop('', '检索中…', 'ok');
|
|
598
|
+
try{
|
|
599
|
+
const r=await fetch(`${API}/knowledge/search`,{method:'POST',headers:{'Content-Type':'application/json'},
|
|
600
|
+
body:JSON.stringify({query:q,mode:'hybrid',top_k:8})});
|
|
601
|
+
const d=await r.json(); const hits=d.hits||[];
|
|
602
|
+
if(d.available===false){ fillReplyPop('', '知识核心未启用', 'err'); return; }
|
|
603
|
+
if(!hits.length){ fillReplyPop('', `没有匹配「${q}」的知识`, 'ok'); return; }
|
|
604
|
+
let html=`<div style="color:var(--cyan);font-weight:600;margin-bottom:8px">🔍 ${escapeHtml(q)} · ${hits.length} 条</div>`;
|
|
605
|
+
hits.forEach(h=>{ const sc=(typeof h.score==='number')?h.score.toFixed(2):h.score;
|
|
606
|
+
html+=`<div style="margin:7px 0;padding:8px 10px;border-left:2px solid var(--cyan);background:rgba(0,217,255,.05)">
|
|
607
|
+
<div style="font-size:12px;color:var(--cyan);font-weight:600">${escapeHtml(h.name||'(未命名)')} <span style="color:var(--ink-faint);font-weight:400">${escapeHtml(h.ku_type||'')} · ${sc}</span></div>
|
|
608
|
+
<div style="font-size:11.5px;margin-top:3px;color:var(--ink-dim)">${escapeHtml((h.summary||'').slice(0,140))}</div></div>`; });
|
|
609
|
+
fillReplyPop(html, '混合检索 · 点「🌐 知识星球」看全图', 'ok', true);
|
|
610
|
+
}catch(e){ fillReplyPop('', '搜索失败:后端未连接', 'err'); }
|
|
611
|
+
}
|
|
612
|
+
function slashHelp(){
|
|
613
|
+
let html='<div style="color:var(--cyan);font-weight:600;margin-bottom:8px">斜杠命令</div>';
|
|
614
|
+
SLASH_COMMANDS.forEach(c=>{ html+=`<div style="display:flex;gap:8px;margin:4px 0;font-size:12px">
|
|
615
|
+
<span style="width:18px">${c.icon}</span><b style="color:${c.danger?'#ff8a87':'#eaf6ff'};min-width:70px">/${escapeHtml(c.cmd)}${c.arg?' <'+escapeHtml(c.arg)+'>':''}</b>
|
|
616
|
+
<span style="color:var(--ink-dim)">${escapeHtml(c.desc)}</span></div>`; });
|
|
617
|
+
showReplyPop('/帮助', activeAgent, true);
|
|
618
|
+
fillReplyPop(html, '直接输入命令即可,如 /搜索 记忆架构', 'ok', true);
|
|
619
|
+
}
|
|
620
|
+
async function slashReset(arg){
|
|
621
|
+
if((arg||'').trim()!=='确认'){
|
|
622
|
+
showReplyPop('/清库', activeAgent, true);
|
|
623
|
+
fillReplyPop('', '⚠ 清空整个知识库(所有 KU/关系),不可逆。确认请输入:/清库 确认', 'err'); return;
|
|
624
|
+
}
|
|
625
|
+
showReplyPop('/清库 确认', activeAgent, true); fillReplyPop('', '清库中…', 'ok');
|
|
626
|
+
try{
|
|
627
|
+
const r=await fetch(`${_origin}/knowledge/reset`,{method:'POST'});
|
|
628
|
+
const d=await r.json();
|
|
629
|
+
fillReplyPop('已清空知识库', `删除 ${d.cleared_kus??'?'} 条 KU`, 'ok');
|
|
630
|
+
try{ const fr=document.getElementById('planetFrame'); const w=fr&&fr.contentWindow;
|
|
631
|
+
if(w&&fr.src&&fr.src!=='about:blank'&&typeof w.loadData==='function')w.loadData(); }catch(e){}
|
|
632
|
+
}catch(e){ fillReplyPop('', '清库失败:后端未连接', 'err'); }
|
|
633
|
+
}
|
|
634
|
+
/* 菜单 UI + 键盘导航 */
|
|
635
|
+
let _slashSel=0, _slashList=[];
|
|
636
|
+
function slashFilter(text){
|
|
637
|
+
const q=text.slice(1).toLowerCase();
|
|
638
|
+
return SLASH_COMMANDS.filter(c=> !q || c.cmd.toLowerCase().includes(q)
|
|
639
|
+
|| (c.aliases||[]).some(a=>a.toLowerCase().includes(q)) || c.desc.toLowerCase().includes(q));
|
|
640
|
+
}
|
|
641
|
+
function slashMenuOpen(){ const m=document.getElementById('slashMenu'); return !!(m&&m.classList.contains('show')); }
|
|
642
|
+
function hideSlashMenu(){ const m=document.getElementById('slashMenu'); if(m){m.classList.remove('show'); m.innerHTML='';} _slashList=[]; _slashSel=0; }
|
|
643
|
+
function updateSlashSel(){ document.querySelectorAll('#slashMenu .slash-item').forEach(el=>el.classList.toggle('sel', +el.dataset.i===_slashSel)); }
|
|
644
|
+
function renderSlashMenu(){
|
|
645
|
+
const menu=document.getElementById('slashMenu'); if(!menu)return;
|
|
646
|
+
if(!_slashList.length){ hideSlashMenu(); return; }
|
|
647
|
+
let h='<div class="slash-hint">↑↓ 选择 · Enter 执行 · Esc 关闭</div>';
|
|
648
|
+
_slashList.forEach((c,i)=>{ h+=`<div class="slash-item ${c.danger?'danger':''} ${i===_slashSel?'sel':''}" data-i="${i}">
|
|
649
|
+
<span class="si-ic">${c.icon}</span><span class="si-cmd">/${escapeHtml(c.cmd)}${c.arg?' <'+escapeHtml(c.arg)+'>':''}</span>
|
|
650
|
+
<span class="si-desc">${escapeHtml(c.desc)}</span></div>`; });
|
|
651
|
+
menu.innerHTML=h; menu.classList.add('show');
|
|
652
|
+
menu.querySelectorAll('.slash-item').forEach(el=>{
|
|
653
|
+
el.addEventListener('mouseenter',()=>{ _slashSel=+el.dataset.i; updateSlashSel(); });
|
|
654
|
+
el.addEventListener('mousedown',e=>{ e.preventDefault(); pickSlash(+el.dataset.i); });
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function pickSlash(i){
|
|
658
|
+
const c=_slashList[i]; if(!c)return;
|
|
659
|
+
const inp=document.getElementById('cmd');
|
|
660
|
+
if(c.arg){ inp.value='/'+c.cmd+' '; hideSlashMenu(); inp.focus(); } // 需参数:补全命令,等用户输入
|
|
661
|
+
else{ inp.value=''; hideSlashMenu(); runSlash('/'+c.cmd); }
|
|
662
|
+
}
|
|
663
|
+
function onSlashInput(){
|
|
664
|
+
const inp=document.getElementById('cmd'); const v=inp.value;
|
|
665
|
+
if(v[0]!=='/' || v.indexOf(' ')>=0){ hideSlashMenu(); return; } // 含空格=进入参数阶段 → 关菜单
|
|
666
|
+
_slashList=slashFilter(v); _slashSel=0; renderSlashMenu();
|
|
667
|
+
}
|
|
668
|
+
(function(){
|
|
669
|
+
const inp=document.getElementById('cmd'); if(!inp)return;
|
|
670
|
+
inp.addEventListener('input', onSlashInput);
|
|
671
|
+
inp.addEventListener('blur', ()=>setTimeout(hideSlashMenu,150)); // 失焦关(留时间给 mousedown)
|
|
672
|
+
// 捕获阶段拦截:菜单开时,Enter/方向键先于 transmit 的 bubble 监听被处理
|
|
673
|
+
document.addEventListener('keydown', function(e){
|
|
674
|
+
if(e.target!==inp || !slashMenuOpen()) return;
|
|
675
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); e.stopPropagation(); _slashSel=(_slashSel+1)%_slashList.length; updateSlashSel(); }
|
|
676
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); e.stopPropagation(); _slashSel=(_slashSel-1+_slashList.length)%_slashList.length; updateSlashSel(); }
|
|
677
|
+
else if(e.key==='Enter'||e.key==='Tab'){ e.preventDefault(); e.stopPropagation(); pickSlash(_slashSel); }
|
|
678
|
+
else if(e.key==='Escape'){ e.preventDefault(); e.stopPropagation(); hideSlashMenu(); }
|
|
679
|
+
}, true);
|
|
680
|
+
})();
|
|
681
|
+
|
|
682
|
+
/* ===== 连接器/插件入口:🔌 面板 + @引用(纯前端,复用现有 AHS API)===== */
|
|
683
|
+
let _atList=[], _atSel=0, _atCache=null, _atCtx=null, _refTriggers={};
|
|
684
|
+
/* --- 🔌 面板:列出 MCP 连接器 / 技能 / SKILL.md,徽章 + 启停 --- */
|
|
685
|
+
async function openConnPanel(){
|
|
686
|
+
const p=document.getElementById('connPanel'); if(!p)return;
|
|
687
|
+
if(p.classList.contains('show')){ p.classList.remove('show'); return; }
|
|
688
|
+
hideSlashMenu(); hideAtMenu();
|
|
689
|
+
p.classList.add('show'); p.innerHTML='<div class="conn-empty">加载中…</div>';
|
|
690
|
+
try{
|
|
691
|
+
const [a,b,c]=await Promise.all([
|
|
692
|
+
fetch(`${API}/ahs/skills`).then(x=>x.json()).catch(()=>({})),
|
|
693
|
+
fetch(`${API}/ahs/mcp/servers`).then(x=>x.json()).catch(()=>({})),
|
|
694
|
+
fetch(`${API}/ahs/md-skills`).then(x=>x.json()).catch(()=>({})),
|
|
695
|
+
]);
|
|
696
|
+
renderConnPanel(p, a.skills||[], b.servers||[], c.skills||[]);
|
|
697
|
+
}catch(e){ p.innerHTML='<div class="conn-empty">后端未连接</div>'; }
|
|
698
|
+
}
|
|
699
|
+
function connBadge(trust){ const t=trust||'mid'; const lb={high:'高',mid:'中',low:'低'}[t]||t; return `<span class="conn-badge ${t}">${lb}信任</span>`; }
|
|
700
|
+
function renderConnPanel(p, skills, servers, md){
|
|
701
|
+
const kindIc={local:'🧩',mcp:'🔌',web_api:'🌐'};
|
|
702
|
+
let h=`<div class="conn-sec"><div class="conn-sec-h"><span>MCP 连接器</span><span>${servers.length}</span></div>`;
|
|
703
|
+
h+= servers.length?'':'<div class="conn-empty" style="padding:8px">未接入</div>';
|
|
704
|
+
servers.forEach(s=>{ h+=`<div class="conn-row"><span class="cr-ic">🔌</span>
|
|
705
|
+
<div><div class="cr-name">${escapeHtml(s.name||s.id)}</div><div class="cr-meta">${s.tool_count||0} 工具 · ${escapeHtml(s.transport||'')}</div></div></div>`; });
|
|
706
|
+
h+='</div>';
|
|
707
|
+
h+=`<div class="conn-sec"><div class="conn-sec-h"><span>技能</span><span>${skills.length}</span></div>`;
|
|
708
|
+
h+= skills.length?'':'<div class="conn-empty" style="padding:8px">无</div>';
|
|
709
|
+
skills.forEach(s=>{ const blk=s.blocked; h+=`<div class="conn-row"><span class="cr-ic">${kindIc[s.kind]||'🧩'}</span>
|
|
710
|
+
<div><div class="cr-name">${escapeHtml(s.name||s.id)}${blk?' <span class="conn-badge low">已封杀</span>':''}</div>
|
|
711
|
+
<div class="cr-meta">${escapeHtml((s.description||'').slice(0,42))}</div></div>
|
|
712
|
+
<div class="cr-right">${connBadge(s.trust)}<label class="conn-sw"><input type="checkbox" ${s.enabled&&!blk?'checked':''} ${blk?'disabled':''} data-skill="${escapeHtml(s.id)}"><i></i></label></div></div>`; });
|
|
713
|
+
h+='</div>';
|
|
714
|
+
h+=`<div class="conn-sec"><div class="conn-sec-h"><span>SKILL.md 指令技能</span><span>${md.length}</span></div>`;
|
|
715
|
+
h+= md.length?'':'<div class="conn-empty" style="padding:8px">无</div>';
|
|
716
|
+
md.forEach(s=>{ h+=`<div class="conn-row"><span class="cr-ic">📄</span>
|
|
717
|
+
<div><div class="cr-name">${escapeHtml(s.name||s.id)}</div><div class="cr-meta">${escapeHtml((s.description||'').slice(0,42))}</div></div>
|
|
718
|
+
<div class="cr-right"><label class="conn-sw"><input type="checkbox" ${s.enabled?'checked':''} data-md="${escapeHtml(s.id)}"><i></i></label></div></div>`; });
|
|
719
|
+
h+='</div><div class="conn-foot"><button id="connManage">⚒ 在工具中心管理全部</button></div>';
|
|
720
|
+
p.innerHTML=h;
|
|
721
|
+
p.querySelectorAll('input[data-skill]').forEach(el=>el.addEventListener('change',async()=>{
|
|
722
|
+
_atCache=null; // 启停变了 → 让 @引用缓存失效
|
|
723
|
+
try{ const r=await fetch(`${API}/ahs/skills/${encodeURIComponent(el.dataset.skill)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:el.checked})}); if(!r.ok)throw 0; }
|
|
724
|
+
catch(e){ el.checked=!el.checked; }
|
|
725
|
+
}));
|
|
726
|
+
p.querySelectorAll('input[data-md]').forEach(el=>el.addEventListener('change',async()=>{
|
|
727
|
+
_atCache=null;
|
|
728
|
+
try{ const r=await fetch(`${API}/ahs/md-skills/${encodeURIComponent(el.dataset.md)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:el.checked})}); if(!r.ok)throw 0; }
|
|
729
|
+
catch(e){ el.checked=!el.checked; }
|
|
730
|
+
}));
|
|
731
|
+
const mg=document.getElementById('connManage'); if(mg)mg.addEventListener('click',()=>{ p.classList.remove('show'); try{showModuleInCore('tools');}catch(e){} });
|
|
732
|
+
}
|
|
733
|
+
(function(){
|
|
734
|
+
const btn=document.getElementById('cmdConn'); if(btn)btn.addEventListener('click',e=>{ e.stopPropagation(); openConnPanel(); });
|
|
735
|
+
document.addEventListener('click',e=>{ const p=document.getElementById('connPanel');
|
|
736
|
+
if(p&&p.classList.contains('show')&&!p.contains(e.target)&&e.target.id!=='cmdConn') p.classList.remove('show'); });
|
|
737
|
+
})();
|
|
738
|
+
|
|
739
|
+
/* --- @引用:输入 @ 弹技能/连接器 → 插入 @名称;发送(任务模式)时把其触发词带入 task,让技能匹配可靠命中 --- */
|
|
740
|
+
async function ensureAtCache(){
|
|
741
|
+
if(_atCache) return _atCache;
|
|
742
|
+
try{
|
|
743
|
+
const [a,c]=await Promise.all([
|
|
744
|
+
fetch(`${API}/ahs/skills`).then(x=>x.json()).catch(()=>({})),
|
|
745
|
+
fetch(`${API}/ahs/md-skills`).then(x=>x.json()).catch(()=>({})),
|
|
746
|
+
]);
|
|
747
|
+
const sk=(a.skills||[]).filter(s=>s.enabled&&!s.blocked).map(s=>({id:s.id,name:s.name||s.id,trigger:(s.triggers&&s.triggers[0])||s.name||s.id,kind:s.kind}));
|
|
748
|
+
const md=(c.skills||[]).filter(s=>s.enabled).map(s=>({id:s.id,name:s.name||s.id,trigger:(s.triggers&&s.triggers[0])||s.name||s.id,kind:'md'}));
|
|
749
|
+
_atCache=sk.concat(md);
|
|
750
|
+
}catch(e){ _atCache=[]; }
|
|
751
|
+
return _atCache;
|
|
752
|
+
}
|
|
753
|
+
function atMenuOpen(){ const m=document.getElementById('atMenu'); return !!(m&&m.classList.contains('show')); }
|
|
754
|
+
function hideAtMenu(){ const m=document.getElementById('atMenu'); if(m){m.classList.remove('show'); m.innerHTML='';} _atList=[]; _atSel=0; _atCtx=null; }
|
|
755
|
+
function _atSelUpdate(){ document.querySelectorAll('#atMenu .slash-item').forEach(x=>x.classList.toggle('sel',+x.dataset.i===_atSel)); }
|
|
756
|
+
function atQueryAtCursor(){
|
|
757
|
+
const inp=document.getElementById('cmd'); const v=inp.value; const pos=inp.selectionStart==null?v.length:inp.selectionStart;
|
|
758
|
+
const upto=v.slice(0,pos); const at=upto.lastIndexOf('@');
|
|
759
|
+
if(at<0) return null;
|
|
760
|
+
const frag=upto.slice(at+1);
|
|
761
|
+
if(/\s/.test(frag)) return null; // @ 后有空格 = 已结束引用
|
|
762
|
+
return {at, pos, q:frag};
|
|
763
|
+
}
|
|
764
|
+
function renderAtMenu(){
|
|
765
|
+
const m=document.getElementById('atMenu'); if(!m)return;
|
|
766
|
+
if(!_atList.length){ hideAtMenu(); return; }
|
|
767
|
+
const kindIc={local:'🧩',mcp:'🔌',web_api:'🌐',md:'📄'};
|
|
768
|
+
let h='<div class="slash-hint">@ 引用技能/连接器 · Enter 插入 · Esc 关闭</div>';
|
|
769
|
+
_atList.forEach((c,i)=>{ h+=`<div class="slash-item ${i===_atSel?'sel':''}" data-i="${i}">
|
|
770
|
+
<span class="si-ic">${kindIc[c.kind]||'🧩'}</span><span class="si-cmd">@${escapeHtml(c.name)}</span>
|
|
771
|
+
<span class="si-desc">触发:${escapeHtml(c.trigger)}</span></div>`; });
|
|
772
|
+
m.innerHTML=h; m.classList.add('show');
|
|
773
|
+
m.querySelectorAll('.slash-item').forEach(el=>{
|
|
774
|
+
el.addEventListener('mouseenter',()=>{ _atSel=+el.dataset.i; _atSelUpdate(); });
|
|
775
|
+
el.addEventListener('mousedown',e=>{ e.preventDefault(); pickAt(+el.dataset.i); });
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
function pickAt(i){
|
|
779
|
+
const c=_atList[i]; if(!c||!_atCtx)return;
|
|
780
|
+
const inp=document.getElementById('cmd'); const v=inp.value;
|
|
781
|
+
const before=v.slice(0,_atCtx.at), after=v.slice(_atCtx.pos);
|
|
782
|
+
const ins='@'+c.name+' ';
|
|
783
|
+
inp.value=before+ins+after; _refTriggers[c.name]=c.trigger;
|
|
784
|
+
hideAtMenu(); inp.focus();
|
|
785
|
+
const np=(before+ins).length; try{ inp.setSelectionRange(np,np); }catch(e){}
|
|
786
|
+
}
|
|
787
|
+
async function onAtInput(){
|
|
788
|
+
const inp=document.getElementById('cmd');
|
|
789
|
+
if(inp.value[0]==='/'){ hideAtMenu(); return; } // 斜杠命令优先
|
|
790
|
+
const ctx=atQueryAtCursor();
|
|
791
|
+
if(!ctx){ hideAtMenu(); return; }
|
|
792
|
+
_atCtx=ctx;
|
|
793
|
+
const all=await ensureAtCache();
|
|
794
|
+
const q=ctx.q.toLowerCase();
|
|
795
|
+
_atList=all.filter(c=> !q || c.name.toLowerCase().includes(q) || (c.trigger||'').toLowerCase().includes(q)).slice(0,8);
|
|
796
|
+
_atSel=0; renderAtMenu();
|
|
797
|
+
}
|
|
798
|
+
/* 发送任务模式时,把已引用技能的触发词带入 task(不改用户可见输入),让后端 match 命中 */
|
|
799
|
+
function applyRefs(task){
|
|
800
|
+
let out=task;
|
|
801
|
+
for(const nm in _refTriggers){
|
|
802
|
+
if(task.includes('@'+nm)){ const tr=_refTriggers[nm]; if(tr && !out.includes(tr)) out+=' '+tr; }
|
|
803
|
+
}
|
|
804
|
+
_refTriggers={};
|
|
805
|
+
return out;
|
|
806
|
+
}
|
|
807
|
+
(function(){
|
|
808
|
+
const inp=document.getElementById('cmd'); if(!inp)return;
|
|
809
|
+
inp.addEventListener('input', onAtInput);
|
|
810
|
+
inp.addEventListener('blur', ()=>setTimeout(hideAtMenu,150));
|
|
811
|
+
document.addEventListener('keydown', function(e){
|
|
812
|
+
if(e.target!==inp || !atMenuOpen()) return;
|
|
813
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); e.stopPropagation(); _atSel=(_atSel+1)%_atList.length; _atSelUpdate(); }
|
|
814
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); e.stopPropagation(); _atSel=(_atSel-1+_atList.length)%_atList.length; _atSelUpdate(); }
|
|
815
|
+
else if(e.key==='Enter'||e.key==='Tab'){ e.preventDefault(); e.stopPropagation(); pickAt(_atSel); }
|
|
816
|
+
else if(e.key==='Escape'){ e.preventDefault(); e.stopPropagation(); hideAtMenu(); }
|
|
817
|
+
}, true);
|
|
818
|
+
})();
|
|
819
|
+
|
|
820
|
+
/* ===== 图片附件 → 多模态对话:附图 + 缩放压缩 + 随问题发给视觉模型 ===== */
|
|
821
|
+
let _pendingImages=[]; // [{media_type,data(base64),url(预览),name}]
|
|
822
|
+
const _IMG_MAX=6, _IMG_MAXDIM=1280, _IMG_Q=0.85;
|
|
823
|
+
function _downscaleImage(file){
|
|
824
|
+
return new Promise((resolve,reject)=>{
|
|
825
|
+
const img=new Image(); const url=URL.createObjectURL(file);
|
|
826
|
+
img.onload=()=>{
|
|
827
|
+
URL.revokeObjectURL(url);
|
|
828
|
+
let w=img.naturalWidth||img.width, h=img.naturalHeight||img.height;
|
|
829
|
+
const scale=Math.min(1, _IMG_MAXDIM/Math.max(w,h||1));
|
|
830
|
+
w=Math.max(1,Math.round(w*scale)); h=Math.max(1,Math.round(h*scale));
|
|
831
|
+
const cv=document.createElement('canvas'); cv.width=w; cv.height=h;
|
|
832
|
+
cv.getContext('2d').drawImage(img,0,0,w,h);
|
|
833
|
+
const isPng=/png/i.test(file.type); // png 可能带透明 → 保 png,其余转 jpeg 省体积
|
|
834
|
+
const mt=isPng?'image/png':'image/jpeg';
|
|
835
|
+
let durl; try{ durl=cv.toDataURL(mt,_IMG_Q); }catch(e){ reject(e); return; }
|
|
836
|
+
resolve({media_type:mt, data:durl.split(',')[1]||'', url:durl, name:file.name||'image'});
|
|
837
|
+
};
|
|
838
|
+
img.onerror=()=>{ URL.revokeObjectURL(url); reject(new Error('image load failed')); };
|
|
839
|
+
img.src=url;
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
async function attachImages(files){
|
|
843
|
+
for(const f of [...files]){
|
|
844
|
+
if(!/^image\//.test(f.type||'')) continue;
|
|
845
|
+
if(_pendingImages.length>=_IMG_MAX) break;
|
|
846
|
+
try{ _pendingImages.push(await _downscaleImage(f)); }catch(e){}
|
|
847
|
+
}
|
|
848
|
+
renderImgStrip(); visionAdvisory();
|
|
849
|
+
}
|
|
850
|
+
function renderImgStrip(){
|
|
851
|
+
const s=document.getElementById('imgStrip'); if(!s)return;
|
|
852
|
+
if(!_pendingImages.length){ s.classList.remove('show'); s.innerHTML=''; return; }
|
|
853
|
+
let h='';
|
|
854
|
+
_pendingImages.forEach((im,i)=>{ h+=`<div class="img-thumb" title="${escapeHtml(im.name)}"><img src="${im.url}" alt=""><span class="it-x" data-i="${i}">✕</span></div>`; });
|
|
855
|
+
h+='<div class="img-note" id="imgNote"></div>';
|
|
856
|
+
s.innerHTML=h; s.classList.add('show');
|
|
857
|
+
s.querySelectorAll('.it-x').forEach(el=>el.addEventListener('click',()=>{ _pendingImages.splice(+el.dataset.i,1); renderImgStrip(); visionAdvisory(); }));
|
|
858
|
+
}
|
|
859
|
+
function clearPendingImages(){ _pendingImages=[]; renderImgStrip(); }
|
|
860
|
+
async function visionAdvisory(){
|
|
861
|
+
const note=document.getElementById('imgNote'); if(!note)return;
|
|
862
|
+
try{
|
|
863
|
+
const m=await fetch(`${API}/model`).then(x=>x.json());
|
|
864
|
+
const model=(m.model||'').toLowerCase();
|
|
865
|
+
const vision=/(claude|sonnet|opus|haiku|gpt-4o|gpt-4\.1|gpt-5|o1|o3|[-_]vl|vision|gemini|pixtral|llava)/i.test(model);
|
|
866
|
+
if(vision){ note.textContent=`📷 ${_pendingImages.length} 张图将随问题发给视觉模型`; note.style.color='var(--ink-dim)'; }
|
|
867
|
+
else{ note.textContent=`⚠ 当前模型「${m.model||'?'}」可能不支持图片,请到「连接设置」切换视觉模型(claude / gpt-4o / qwen-vl 等)`; note.style.color='var(--amber)'; }
|
|
868
|
+
}catch(e){}
|
|
869
|
+
}
|
|
870
|
+
(function(){
|
|
871
|
+
const btn=document.getElementById('cmdImg'), inp=document.getElementById('cmdImgFile');
|
|
872
|
+
if(btn&&inp){
|
|
873
|
+
btn.addEventListener('click',()=>inp.click());
|
|
874
|
+
inp.addEventListener('change',()=>{ const fs=[...inp.files]; inp.value=''; if(fs.length) attachImages(fs); }); // 拷数组再清空(同 folder:inp.value='' 会清空活引用)
|
|
875
|
+
}
|
|
876
|
+
})();
|
|
877
|
+
|
|
878
|
+
/* ===== 文件附件 → 聊天附件:上传落盘 + 随问题发给 LLM(文本类提取正文,其它降级元数据) ===== */
|
|
879
|
+
let _pendingFiles=[]; // [{id,name,size,mime,url,text}]
|
|
880
|
+
const _FILE_MAX=6, _FILE_MAXB=20*1024*1024;
|
|
881
|
+
function _fileSize(sz){
|
|
882
|
+
if(!sz)return '';
|
|
883
|
+
return sz>=1048576?(sz/1048576).toFixed(1)+'MB':(sz/1024).toFixed(1)+'KB';
|
|
884
|
+
}
|
|
885
|
+
function _attachmentsHtml(atts){
|
|
886
|
+
if(!atts || !atts.length) return '';
|
|
887
|
+
return '<div class="cc-files">'+atts.map(a=>{
|
|
888
|
+
const nm=escapeHtml(a.name||'文件'), sz=_fileSize(a.size);
|
|
889
|
+
const url=a.url?escapeHtml(a.url):'';
|
|
890
|
+
if(url) return `<a class="cc-file" href="${url}" target="_blank" rel="noopener noreferrer">📄 <span>${nm}</span><span class="cc-file-size">${sz}</span></a>`;
|
|
891
|
+
return `<span class="cc-file">📄 <span>${nm}</span><span class="cc-file-size">${sz}</span></span>`;
|
|
892
|
+
}).join('')+'</div>';
|
|
893
|
+
}
|
|
894
|
+
async function attachFiles(files){
|
|
895
|
+
for(const f of [...files]){
|
|
896
|
+
if(_pendingFiles.length>=_FILE_MAX) break;
|
|
897
|
+
if(f.size>_FILE_MAXB){ console.warn(`[attach] 跳过超限文件: ${f.name} (${f.size}B > 20MB)`); continue; }
|
|
898
|
+
try{
|
|
899
|
+
const fd=new FormData(); fd.append('file', f);
|
|
900
|
+
const r=await fetch(`${API}/upload`,{method:'POST',body:fd});
|
|
901
|
+
if(!r.ok) throw new Error('http '+r.status);
|
|
902
|
+
const a=await r.json();
|
|
903
|
+
_pendingFiles.push({id:a.id,name:a.name||f.name,size:a.size,mime:a.mime,url:a.url,text:a.text||''});
|
|
904
|
+
}catch(e){ console.warn(`[attach] 上传失败: ${f.name}`, e); }
|
|
905
|
+
}
|
|
906
|
+
renderFileStrip();
|
|
907
|
+
}
|
|
908
|
+
function renderFileStrip(){
|
|
909
|
+
const s=document.getElementById('fileStrip'); if(!s)return;
|
|
910
|
+
if(!_pendingFiles.length){ s.classList.remove('show'); s.innerHTML=''; return; }
|
|
911
|
+
s.innerHTML=_pendingFiles.map((f,i)=>
|
|
912
|
+
`<div class="file-card" title="${escapeHtml(f.name)}"><span>📄</span><span class="fc-name">${escapeHtml(f.name)}</span>`
|
|
913
|
+
+`<span class="fc-size" style="color:var(--dim);font-size:11px">${_fileSize(f.size)}</span>`
|
|
914
|
+
+`<span class="fc-x" data-i="${i}">✕</span></div>`).join('');
|
|
915
|
+
s.classList.add('show');
|
|
916
|
+
s.querySelectorAll('.fc-x').forEach(el=>el.addEventListener('click',()=>{ _pendingFiles.splice(+el.dataset.i,1); renderFileStrip(); }));
|
|
917
|
+
}
|
|
918
|
+
function clearPendingFiles(){ _pendingFiles=[]; renderFileStrip(); }
|
|
919
|
+
(function(){
|
|
920
|
+
const btn=document.getElementById('cmdAttachFile'), inp=document.getElementById('cmdFileUpload');
|
|
921
|
+
if(btn&&inp){
|
|
922
|
+
btn.addEventListener('click',()=>inp.click());
|
|
923
|
+
inp.addEventListener('change',()=>{ const fs=[...inp.files]; inp.value=''; if(fs.length) attachFiles(fs); });
|
|
924
|
+
}
|
|
925
|
+
})();
|
|
926
|
+
|
|
927
|
+
/* 流式直答:SSE 逐字追加进回复浮窗 */
|
|
928
|
+
let _chatHistory=[]; // 多轮对话历史 [{role,content}];随请求带给后端 + 界面渲染成对话线程
|
|
929
|
+
function clearChatHistory(){ _chatHistory=[]; } // 新话题/清屏可调
|
|
930
|
+
|
|
931
|
+
/* ═══ 会话历史:对话不再刷新即丢,落盘到后端、左栏可回看 ═══ */
|
|
932
|
+
let _sessionId = localStorage.getItem('foxclawSessionId') || '';
|
|
933
|
+
async function ensureSession(){
|
|
934
|
+
if(_sessionId){
|
|
935
|
+
const ok = await fetch(`${API}/sessions/${_sessionId}`).then(r=>r.ok).catch(()=>false);
|
|
936
|
+
if(ok) return _sessionId;
|
|
937
|
+
}
|
|
938
|
+
return newSession(false);
|
|
939
|
+
}
|
|
940
|
+
async function newSession(focus=true){
|
|
941
|
+
try{
|
|
942
|
+
const s=await fetch(`${API}/sessions`,{method:'POST',
|
|
943
|
+
headers:{'Content-Type':'application/json'},body:JSON.stringify({})}).then(r=>r.json());
|
|
944
|
+
_sessionId=s.id; localStorage.setItem('foxclawSessionId',_sessionId);
|
|
945
|
+
_chatHistory=[];
|
|
946
|
+
if(focus){ document.getElementById('coreChat')?.classList.remove('show'); }
|
|
947
|
+
await loadSessions();
|
|
948
|
+
return _sessionId;
|
|
949
|
+
}catch(e){ return ''; }
|
|
950
|
+
}
|
|
951
|
+
async function openSession(id){
|
|
952
|
+
try{
|
|
953
|
+
const d=await fetch(`${API}/sessions/${id}`).then(r=>r.json());
|
|
954
|
+
_sessionId=d.id; localStorage.setItem('foxclawSessionId',_sessionId);
|
|
955
|
+
_chatHistory=(d.messages||[]).map(m=>({role:m.role,content:m.content,ts:(m.ts||0)*1000,attachments:m.attachments||undefined}));
|
|
956
|
+
renderSessionThread(d.agent_id||'supervisor');
|
|
957
|
+
await loadSessions();
|
|
958
|
+
}catch(e){}
|
|
959
|
+
}
|
|
960
|
+
/* 渲染已存在会话(只有历史,没有"当前问"和空答槽)*/
|
|
961
|
+
function renderSessionThread(agent){
|
|
962
|
+
const body=document.querySelector('#coreChat .cc-body'); if(!body)return;
|
|
963
|
+
const a=document.getElementById('ccAgent'); if(a)a.textContent=`${AGENT_CN[agent]||agent} Agent`;
|
|
964
|
+
let h='';
|
|
965
|
+
for(const m of _chatHistory){
|
|
966
|
+
const inner=m.role==='user'?escapeHtml(m.content)+_attachmentsHtml(m.attachments):renderMarkdown(m.content);
|
|
967
|
+
h+=_chatRow(m.role, inner, agent, m.ts, false);
|
|
968
|
+
}
|
|
969
|
+
body.innerHTML=h||'<div class="kp-empty" style="padding:30px">这个会话还没有消息</div>';
|
|
970
|
+
Center.chat();
|
|
971
|
+
body.scrollTop=body.scrollHeight;
|
|
972
|
+
}
|
|
973
|
+
/* 会话列表:渲染进左栏 #nsList(会话历史是这个界面的主体内容)。
|
|
974
|
+
每条 → openSession(id) 真实打开对应会话;✕ → 删除。 */
|
|
975
|
+
async function loadSessions(){
|
|
976
|
+
const list=document.getElementById('nsList'); if(!list)return;
|
|
977
|
+
const cnt=document.getElementById('nsCount');
|
|
978
|
+
try{
|
|
979
|
+
const d=await fetch(`${API}/sessions?limit=50`).then(r=>r.json());
|
|
980
|
+
const ss=d.sessions||[];
|
|
981
|
+
if(cnt) cnt.textContent=ss.length||'0';
|
|
982
|
+
if(!ss.length){ list.innerHTML='<div class="kp-empty">还没有会话 · 发一条消息即开始</div>'; return; }
|
|
983
|
+
list.innerHTML=ss.map(s=>{
|
|
984
|
+
const title=s.title||'新会话';
|
|
985
|
+
const when=s.updated_at?new Date(s.updated_at*1000).toLocaleString('zh-CN',{month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit',hour12:false}):'';
|
|
986
|
+
const meta=[when, s.message_count?`${s.message_count} 条`:''].filter(Boolean).join(' · ');
|
|
987
|
+
return `<div class="ns-item${s.id===_sessionId?' on':''}" data-sid="${escapeHtml(s.id)}" title="${escapeHtml(title)}">
|
|
988
|
+
<b>${escapeHtml(title)}</b>
|
|
989
|
+
<span>${escapeHtml(meta)}</span>
|
|
990
|
+
<span class="ns-del" data-del="${escapeHtml(s.id)}" title="删除会话">✕</span></div>`;
|
|
991
|
+
}).join('');
|
|
992
|
+
list.querySelectorAll('.ns-item').forEach(el=>{
|
|
993
|
+
el.onclick=(ev)=>{
|
|
994
|
+
if(ev.target.dataset.del){ ev.stopPropagation(); delSession(ev.target.dataset.del); return; }
|
|
995
|
+
openSession(el.dataset.sid);
|
|
996
|
+
};
|
|
997
|
+
});
|
|
998
|
+
}catch(e){ list.innerHTML='<div class="kp-empty">后端未连接</div>'; }
|
|
999
|
+
}
|
|
1000
|
+
async function delSession(id){
|
|
1001
|
+
try{ await fetch(`${API}/sessions/${id}`,{method:'DELETE'}); }catch(e){}
|
|
1002
|
+
if(id===_sessionId){ _sessionId=''; _chatHistory=[];
|
|
1003
|
+
localStorage.removeItem('foxclawSessionId');
|
|
1004
|
+
document.getElementById('coreChat')?.classList.remove('show');
|
|
1005
|
+
await ensureSession();
|
|
1006
|
+
}
|
|
1007
|
+
await loadSessions();
|
|
1008
|
+
}
|
|
1009
|
+
// 把 _chatHistory + 当前问 重建成双方对称的气泡对话线程(助手左/用户右,各自独立圆角框),
|
|
1010
|
+
// cc-body 可滚动 = 下拉浏览历史。保留上下文,不再只显示最新一条。
|
|
1011
|
+
function _fmtTime(ts){ const d=new Date(ts||Date.now()); return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2); }
|
|
1012
|
+
function _chatRow(role, inner, agent, ts, isSlot){
|
|
1013
|
+
const t=_fmtTime(ts);
|
|
1014
|
+
if(role==='user'){
|
|
1015
|
+
return `<div class="chat-row me"><div class="chat-col">`
|
|
1016
|
+
+`<div class="chat-meta"><span class="chat-time">${t}</span><span class="chat-name">你</span></div>`
|
|
1017
|
+
+`<div class="chat-bubble me">${inner}</div></div>`
|
|
1018
|
+
+`<div class="chat-av me">你</div></div>`;
|
|
1019
|
+
}
|
|
1020
|
+
const nm=`${AGENT_CN[agent]||agent} Agent`, av=(AGENT_CN[agent]||'H').slice(0,1);
|
|
1021
|
+
const bubble = isSlot
|
|
1022
|
+
? `<div class="chat-bubble ai cc-md" id="ccReply"><span class="cc-loading">◌ 思考中…</span></div>`
|
|
1023
|
+
: `<div class="chat-bubble ai cc-md">${inner}</div>`;
|
|
1024
|
+
return `<div class="chat-row ai"><div class="chat-av ai">${escapeHtml(av)}</div><div class="chat-col">`
|
|
1025
|
+
+`<div class="chat-meta"><span class="chat-name">${escapeHtml(nm)}</span>${isSlot?'':`<span class="chat-time">${t}</span>`}</div>`
|
|
1026
|
+
+bubble+`</div></div>`;
|
|
1027
|
+
}
|
|
1028
|
+
function renderChatThread(currentQ, agent, attachments){
|
|
1029
|
+
document.getElementById('ccAgent').textContent=`${AGENT_CN[agent]||agent} Agent`;
|
|
1030
|
+
const body=document.querySelector('#coreChat .cc-body'); if(!body) return;
|
|
1031
|
+
let h='';
|
|
1032
|
+
for(const m of _chatHistory){
|
|
1033
|
+
const inner=m.role==='user'?escapeHtml(m.content)+_attachmentsHtml(m.attachments):renderMarkdown(m.content);
|
|
1034
|
+
h += _chatRow(m.role, inner, agent, m.ts, false);
|
|
1035
|
+
}
|
|
1036
|
+
h += _chatRow('user', escapeHtml(currentQ||'')+_attachmentsHtml(attachments), agent, Date.now(), false);
|
|
1037
|
+
h += _chatRow('ai', '', agent, Date.now(), true); // 空答槽:#ccReply 供流式写入
|
|
1038
|
+
h += `<div class="cc-status" id="ccStatus"></div>`;
|
|
1039
|
+
body.innerHTML=h;
|
|
1040
|
+
Center.chat();
|
|
1041
|
+
body.scrollTop=body.scrollHeight;
|
|
1042
|
+
}
|
|
1043
|
+
/* 执行证据渲染:把后端每步的核验结论摊开给人看。
|
|
1044
|
+
三态有意不做成"全部打勾"——unverified 显示为「未核验」,failed 显示为「未完成」,
|
|
1045
|
+
因为把没凭据的步骤画成绿色,正是这套机制要消灭的东西。 */
|
|
1046
|
+
function renderEvidence(items){
|
|
1047
|
+
const wrap=document.getElementById('ccEvidence'); if(!wrap)return;
|
|
1048
|
+
if(!items||!items.length){ wrap.hidden=true; wrap.innerHTML=''; return; }
|
|
1049
|
+
const STAT={verified:['✓','已验证','ev-ok'],unverified:['?','未核验','ev-warn'],failed:['✕','未完成','ev-bad']};
|
|
1050
|
+
const rows=items.map(it=>{
|
|
1051
|
+
const s=STAT[it.status]||STAT.unverified;
|
|
1052
|
+
const chips=(it.evidence||[]).map(e=>{
|
|
1053
|
+
const label=escapeHtml(e.label||e.ref||'');
|
|
1054
|
+
const tip=escapeHtml(e.ref||'');
|
|
1055
|
+
return e.kind==='url'
|
|
1056
|
+
? `<a class="ev-item" href="${encodeURI(e.ref)}" target="_blank" rel="noopener" title="${tip}">${label}</a>`
|
|
1057
|
+
: `<span class="ev-item" title="${tip}">${label}</span>`;
|
|
1058
|
+
}).join('');
|
|
1059
|
+
const why=it.reason?`<span class="ev-why">${escapeHtml(it.reason)}</span>`:'';
|
|
1060
|
+
return `<div class="ev-row"><span class="ev-chip ${s[2]}">${s[0]} ${s[1]}</span>`
|
|
1061
|
+
+ `<span class="ev-tool">${escapeHtml(it.tool||'')}</span>${why}${chips}</div>`;
|
|
1062
|
+
}).join('');
|
|
1063
|
+
wrap.innerHTML=`<div class="ev-head">执行凭据 <span class="ev-count">${items.length}</span></div>${rows}`;
|
|
1064
|
+
wrap.hidden=false;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function transmitStream(task, agent, images, attachments){
|
|
1068
|
+
renderChatThread(task, agent, attachments); // 先把历史+当前问(含附件卡片)渲染成对话线程,再流式写入答案槽
|
|
1069
|
+
const _ccBody=document.querySelector('#coreChat .cc-body'); // 注意:下方请求体也叫 body,故这里另起名避免遮蔽
|
|
1070
|
+
const rp=document.getElementById('ccReply'), st=document.getElementById('ccStatus');
|
|
1071
|
+
rp.textContent=''; rp.style.color='var(--ink)'; st.style.color='var(--ink-dim)'; st.textContent='◌ 流式生成中…';
|
|
1072
|
+
let acc='';
|
|
1073
|
+
// 推理过程折叠块:每轮重置。后端以 SSE `event: thinking` 单独送出思考增量,
|
|
1074
|
+
// 正文(delta)不含思考,这里只做展示,不参与落库/复制。
|
|
1075
|
+
let _thinkWrap=document.getElementById('ccThink');
|
|
1076
|
+
if(!_thinkWrap && rp.parentNode){
|
|
1077
|
+
_thinkWrap=document.createElement('details');
|
|
1078
|
+
_thinkWrap.id='ccThink'; _thinkWrap.className='cc-think'; _thinkWrap.hidden=true;
|
|
1079
|
+
_thinkWrap.innerHTML='<summary>推理过程</summary><div class="cc-think-body" id="ccThinkBody"></div>';
|
|
1080
|
+
rp.parentNode.insertBefore(_thinkWrap, rp);
|
|
1081
|
+
}
|
|
1082
|
+
const _thinkBody=document.getElementById('ccThinkBody');
|
|
1083
|
+
if(_thinkBody) _thinkBody.textContent='';
|
|
1084
|
+
if(_thinkWrap){ _thinkWrap.hidden=true; _thinkWrap.open=false; }
|
|
1085
|
+
// 执行证据:工具轮每步「核验到没到位」+ 可点开的凭据(来源链接 / 真实文件)。
|
|
1086
|
+
// 挂在答案**下方**——正文是给用户读的结论,这里是给人查的凭据,两回事。
|
|
1087
|
+
let _evWrap=document.getElementById('ccEvidence');
|
|
1088
|
+
if(!_evWrap && rp.parentNode){
|
|
1089
|
+
_evWrap=document.createElement('div');
|
|
1090
|
+
_evWrap.id='ccEvidence'; _evWrap.className='cc-ev'; _evWrap.hidden=true;
|
|
1091
|
+
rp.parentNode.insertBefore(_evWrap, rp.nextSibling);
|
|
1092
|
+
}
|
|
1093
|
+
if(_evWrap){ _evWrap.hidden=true; _evWrap.innerHTML=''; }
|
|
1094
|
+
let _evs=[];
|
|
1095
|
+
try{
|
|
1096
|
+
if(!_sessionId) await ensureSession(); // 保证本轮问答有会话归属(否则落不了盘)
|
|
1097
|
+
const body={task, agent_id:agent, history:_chatHistory.slice(-12), session_id:_sessionId};
|
|
1098
|
+
if(images && images.length) body.images=images; // 多模态:图片随问题发给视觉模型
|
|
1099
|
+
if(attachments && attachments.length) body.attachments=attachments; // 聊天附件:随问题发给 LLM
|
|
1100
|
+
const r=await fetch(`${API}/chat/stream`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
1101
|
+
if(!r.ok||!r.body){ st.textContent='⚠ 流式请求失败: '+r.status; st.style.color='var(--pink)'; return; }
|
|
1102
|
+
const reader=r.body.getReader(), dec=new TextDecoder(); let buf='';
|
|
1103
|
+
while(true){
|
|
1104
|
+
const {value,done}=await reader.read(); if(done)break;
|
|
1105
|
+
buf+=dec.decode(value,{stream:true});
|
|
1106
|
+
let idx;
|
|
1107
|
+
while((idx=buf.indexOf('\n\n'))>=0){
|
|
1108
|
+
const block=buf.slice(0,idx); buf=buf.slice(idx+2);
|
|
1109
|
+
// 兼容命名事件(SSE 规范):`event: thinking` + `data: {...}`;无 event 行按默认事件处理。
|
|
1110
|
+
const evName=((block.match(/^event:\s*(\S+)/m)||[])[1])||'message';
|
|
1111
|
+
const dataLine=((block.match(/^data:\s*([\s\S]*)$/m)||[])[1]||'').trim();
|
|
1112
|
+
if(!dataLine)continue;
|
|
1113
|
+
let ev; try{ ev=JSON.parse(dataLine); }catch(e){ continue; }
|
|
1114
|
+
if(evName==='thinking'||ev.thinking){ // 推理增量 → 折叠区,不进正文
|
|
1115
|
+
const t=ev.thinking||'';
|
|
1116
|
+
if(t){ if(_thinkWrap) _thinkWrap.hidden=false; if(_thinkBody) _thinkBody.textContent+=t; }
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if(evName==='evidence'||ev.evidence){ // 执行证据 → 答案下方核对区
|
|
1120
|
+
if(Array.isArray(ev.evidence)){ _evs=ev.evidence; renderEvidence(_evs); }
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
if(ev.delta){ acc+=ev.delta; rp.textContent=acc; if(_ccBody) _ccBody.scrollTop=_ccBody.scrollHeight; }
|
|
1124
|
+
if(ev.error){ st.textContent='⚠ '+ev.error; st.style.color='var(--pink)'; }
|
|
1125
|
+
if(ev.done){ st.textContent='✓ 完成'; }
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if(acc){
|
|
1129
|
+
renderReply(acc); // 流式结束 → 富文本渲染(markdown + 内嵌图/音/视频)
|
|
1130
|
+
const _now=Date.now();
|
|
1131
|
+
_chatHistory.push({role:'user',content:task,ts:_now,attachments:attachments||undefined},{role:'assistant',content:acc,ts:_now});
|
|
1132
|
+
if(_chatHistory.length>24) _chatHistory=_chatHistory.slice(-24); // 只留最近 ~12 轮
|
|
1133
|
+
loadSessions(); // 会话标题/条数会变 → 刷新左栏列表
|
|
1134
|
+
}
|
|
1135
|
+
if(!acc && st.textContent.indexOf('⚠')<0) st.textContent='(无输出)';
|
|
1136
|
+
markTask(task,'ok');
|
|
1137
|
+
}catch(e){ st.textContent='⚠ '+e; st.style.color='var(--pink)'; markTask(task,'blocked'); }
|
|
1138
|
+
if(typeof loadMemStats==='function') loadMemStats();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/* 任务模式流式(/api/task/stream):SSE 逐 stage 进度 + act 逐字 + 最终结果。仅领域 Agent 用——
|
|
1142
|
+
该端点跑单 Agent 认知管线,不做主管的多 Agent 分派(主管编排任务仍走阻塞 /api/task)。 */
|
|
1143
|
+
async function transmitTaskStream(task, agent){
|
|
1144
|
+
const rp=document.getElementById('ccReply'), st=document.getElementById('ccStatus');
|
|
1145
|
+
rp.textContent=''; rp.style.color='var(--ink)'; st.style.color='var(--ink-dim)'; st.textContent='◌ 认知管线启动…';
|
|
1146
|
+
let acc='', stages=[];
|
|
1147
|
+
const STAGE_CN={perceive:'感知',recall:'召回',plan:'规划',route:'路由',act:'执行',verify:'校验',reflect:'反思'};
|
|
1148
|
+
// 显式计划块:后端在 plan stage 之后补发 {"plan":{steps,flow,multi_step}} → 列出计划步骤,
|
|
1149
|
+
// 让"它打算怎么做"在执行前就可见(此前只在跑完后才能从 thought chain 里翻)。
|
|
1150
|
+
let _planWrap=document.getElementById('ccPlan');
|
|
1151
|
+
if(!_planWrap && rp.parentNode){
|
|
1152
|
+
_planWrap=document.createElement('details');
|
|
1153
|
+
_planWrap.id='ccPlan'; _planWrap.className='cc-plan'; _planWrap.hidden=true;
|
|
1154
|
+
_planWrap.innerHTML='<summary>执行计划</summary><ol class="cc-plan-body" id="ccPlanBody"></ol>';
|
|
1155
|
+
rp.parentNode.insertBefore(_planWrap, rp);
|
|
1156
|
+
}
|
|
1157
|
+
const _planBody=document.getElementById('ccPlanBody');
|
|
1158
|
+
if(_planBody) _planBody.innerHTML='';
|
|
1159
|
+
if(_planWrap){ _planWrap.hidden=true; _planWrap.open=true; }
|
|
1160
|
+
try{
|
|
1161
|
+
const r=await fetch(`${API}/task/stream`,{method:'POST',headers:{'Content-Type':'application/json'},
|
|
1162
|
+
body:JSON.stringify({task, agent_id:agent})});
|
|
1163
|
+
if(!r.ok||!r.body){ st.textContent='⚠ 流式任务请求失败: '+r.status; st.style.color='var(--pink)'; markTask(task,'blocked'); return; }
|
|
1164
|
+
const reader=r.body.getReader(), dec=new TextDecoder(); let buf='';
|
|
1165
|
+
while(true){
|
|
1166
|
+
const {value,done}=await reader.read(); if(done)break;
|
|
1167
|
+
buf+=dec.decode(value,{stream:true});
|
|
1168
|
+
let idx;
|
|
1169
|
+
while((idx=buf.indexOf('\n\n'))>=0){
|
|
1170
|
+
const line=buf.slice(0,idx).trim(); buf=buf.slice(idx+2);
|
|
1171
|
+
if(!line.startsWith('data:'))continue;
|
|
1172
|
+
let ev; try{ ev=JSON.parse(line.slice(5).trim()); }catch(e){ continue; }
|
|
1173
|
+
if(ev.stage){ stages.push(STAGE_CN[ev.stage]||ev.stage); st.textContent='◌ '+stages.join(' → '); }
|
|
1174
|
+
if(ev.plan && ev.plan.steps && ev.plan.steps.length){ // 计划:整轮固定展示,供用户先看步骤
|
|
1175
|
+
if(_planBody) _planBody.innerHTML=ev.plan.steps.map(s=>`<li>${escapeHtml(String(s))}</li>`).join('');
|
|
1176
|
+
if(_planWrap) _planWrap.hidden=false;
|
|
1177
|
+
const _flow={parallel:'并行',sequential:'顺序',single:'单步'}[ev.plan.flow]||ev.plan.flow||'';
|
|
1178
|
+
st.textContent='◌ 计划('+_flow+' · '+ev.plan.steps.length+' 步)';
|
|
1179
|
+
}
|
|
1180
|
+
if(ev.delta){ acc+=ev.delta; rp.textContent=acc; rp.scrollTop=rp.scrollHeight; }
|
|
1181
|
+
if(ev.error){ st.textContent='⚠ '+ev.error; st.style.color='var(--pink)'; markTask(task,'blocked'); }
|
|
1182
|
+
if(ev.result){
|
|
1183
|
+
const res=ev.result||{};
|
|
1184
|
+
if(res.interrupted){ st.textContent='⚠ 任务被中断(部分完成)'; markTask(task,'blocked'); }
|
|
1185
|
+
else if(res.error==='harness_blocked'){ st.textContent='⚠ 被 Harness 拦截:'+(res.reason||''); st.style.color='var(--pink)'; markTask(task,'blocked'); }
|
|
1186
|
+
else if(res.status==='needs_confirmation'){ st.textContent='⏸ 待确认:'+((res.pending_action||{}).skill_id||''); markTask(task,'blocked'); }
|
|
1187
|
+
else{
|
|
1188
|
+
const reply=(typeof res.result==='string'?res.result:(res.result?JSON.stringify(res.result):''))||acc;
|
|
1189
|
+
if(!acc && reply){ acc=reply; rp.textContent=reply; } // skill dispatch 路径无 delta,用 result 兜底
|
|
1190
|
+
st.textContent='✓ 完成'; markTask(task,'ok');
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if(acc) renderReply(acc);
|
|
1196
|
+
else if(st.textContent.indexOf('⚠')<0 && st.textContent.indexOf('⏸')<0) st.textContent='(无输出)';
|
|
1197
|
+
}catch(e){ st.textContent='⚠ '+e; st.style.color='var(--pink)'; markTask(task,'blocked'); }
|
|
1198
|
+
if(typeof loadMemStats==='function') loadMemStats();
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/* 任务流操作 */
|
|
1202
|
+
function prependTask(name,agent,status){
|
|
1203
|
+
const list=document.getElementById('taskList'); if(!list)return;
|
|
1204
|
+
const now=new Date(); const hh=String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0');
|
|
1205
|
+
const div=document.createElement('div');
|
|
1206
|
+
div.className='task'+(status==='ok'?' ok':''); div.dataset.task=name;
|
|
1207
|
+
div.innerHTML=`<div class="ti">${status==='ok'?'✓':status==='blocked'?'✕':'◌'}</div>
|
|
1208
|
+
<div class="tb"><b>${escapeHtml(name)}</b><span>${escapeHtml(AGENT_CN[agent]||agent)} Agent</span></div><div class="tt">${hh}</div>`;
|
|
1209
|
+
list.prepend(div);
|
|
1210
|
+
while(list.children.length>6)list.removeChild(list.lastChild);
|
|
1211
|
+
}
|
|
1212
|
+
function markTask(name,status){
|
|
1213
|
+
const list=document.getElementById('taskList'); if(!list)return;
|
|
1214
|
+
const row=[...list.children].find(c=>c.dataset.task===name);
|
|
1215
|
+
if(row){row.className='task'+(status==='ok'?' ok':'');
|
|
1216
|
+
row.querySelector('.ti').textContent=status==='ok'?'✓':status==='blocked'?'✕':'◌';}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/* 5. WebSocket 实时事件流 */
|
|
1220
|
+
window.actPulse=0;
|
|
1221
|
+
function connectWS(){
|
|
1222
|
+
try{
|
|
1223
|
+
// 后端若开了鉴权,WS 用 ?key= 传(浏览器无法给 WS 设请求头)
|
|
1224
|
+
const _k=localStorage.getItem('foxclawApiKey');
|
|
1225
|
+
const ws=new WebSocket(WS + (_k?('?key='+encodeURIComponent(_k)):''));
|
|
1226
|
+
ws.onmessage=ev=>{
|
|
1227
|
+
let m; try{m=JSON.parse(ev.data);}catch(e){return;}
|
|
1228
|
+
const e=m.event, d=m.data||{};
|
|
1229
|
+
window.actPulse=Math.min(window.actPulse+0.4,1.5);
|
|
1230
|
+
if(e==='harness_block'){ flashCmd('var(--pink)'); }
|
|
1231
|
+
else if(e==='policy_updated'){ flashCmd('var(--violet)'); }
|
|
1232
|
+
};
|
|
1233
|
+
ws.onclose=()=>setTimeout(connectWS,3000);
|
|
1234
|
+
}catch(e){ setTimeout(connectWS,3000); }
|
|
1235
|
+
}
|
|
1236
|
+
function flashCmd(c){
|
|
1237
|
+
const bar=document.querySelector('.cmdbar');
|
|
1238
|
+
if(!bar)return; const old=bar.style.boxShadow;
|
|
1239
|
+
bar.style.boxShadow=`0 0 24px ${c}`; bar.style.borderColor=c;
|
|
1240
|
+
setTimeout(()=>{bar.style.boxShadow=''; bar.style.borderColor='';},700);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/* ===== 全屏 overlay 浮层 ===== */
|
|
1244
|
+
function openOverlay(agent){
|
|
1245
|
+
const m=KP_META[agent]||KP_META.office;
|
|
1246
|
+
document.getElementById('ovTitle').textContent=m.title;
|
|
1247
|
+
document.getElementById('ovTag').textContent=m.tag;
|
|
1248
|
+
const body=document.getElementById('ovBody');
|
|
1249
|
+
const d=_lastKernelData[agent]||{};
|
|
1250
|
+
if(agent==='admin'){
|
|
1251
|
+
// 行程:左留白 + 地图 + 右栏(录入行程),左右等宽对称
|
|
1252
|
+
body.innerHTML=`<div class="ov-grid">
|
|
1253
|
+
<div class="ov-spacer"></div>
|
|
1254
|
+
<div style="position:relative;height:100%">
|
|
1255
|
+
<div id="ovMap" style="width:100%;height:100%"></div>
|
|
1256
|
+
<div class="map-search">
|
|
1257
|
+
<input id="mapSearchInput" placeholder="🔍 搜索地点…">
|
|
1258
|
+
<div id="mapSearchResults" class="map-search-results"></div>
|
|
1259
|
+
</div>
|
|
1260
|
+
</div>
|
|
1261
|
+
<div class="ov-side" id="ovItinSide">${itinSideHTML(d)}</div>
|
|
1262
|
+
</div>`;
|
|
1263
|
+
setTimeout(()=>{drawAMapInto('ovMap',d.map_data||[]); initMapSearch();},80);
|
|
1264
|
+
}else if(agent==='office'){
|
|
1265
|
+
body.innerHTML=`<div style="height:100%"><canvas id="ovGraph" style="width:100%;height:100%"></canvas></div>`;
|
|
1266
|
+
setTimeout(()=>drawKnowledgeGraph('ovGraph',d),80);
|
|
1267
|
+
}else if(agent==='finance'){
|
|
1268
|
+
body.innerHTML=`<div style="max-width:900px;margin:0 auto">${renderFinance(d)}</div>`;
|
|
1269
|
+
}else if(agent==='supervisor'){
|
|
1270
|
+
body.innerHTML=`<div style="max-width:900px;margin:0 auto">${renderSituation(d)}</div>`;
|
|
1271
|
+
}else{
|
|
1272
|
+
body.innerHTML=`<div style="max-width:700px;margin:0 auto">${document.getElementById('kpBody')?.innerHTML||''}</div>`;
|
|
1273
|
+
}
|
|
1274
|
+
document.getElementById('ovMask').classList.add('show');
|
|
1275
|
+
}
|
|
1276
|
+
function closeOverlay(){document.getElementById('ovMask').classList.remove('show');}
|
|
1277
|
+
document.getElementById('kpFull')?.addEventListener('click',()=>openOverlay(activeAgent));
|
|
1278
|
+
document.getElementById('ovClose')?.addEventListener('click',closeOverlay);
|
|
1279
|
+
document.getElementById('ovMask')?.addEventListener('click',e=>{if(e.target.id==='ovMask')closeOverlay();});
|
|
1280
|
+
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeOverlay();});
|
|
1281
|
+
|
|
1282
|
+
/* 地图自动全屏开关(持久化到内存) */
|
|
1283
|
+
let autoMap=false;
|
|
1284
|
+
document.getElementById('ovAutoMap')?.addEventListener('change',e=>{autoMap=e.target.checked;});
|
|
1285
|
+
|
|
1286
|
+
/* 全屏知识图谱(Canvas 节点连线) */
|
|
1287
|
+
function drawKnowledgeGraph(canvasId,d){
|
|
1288
|
+
const c=document.getElementById(canvasId); if(!c)return;
|
|
1289
|
+
const x=c.getContext('2d');
|
|
1290
|
+
function size(){const r=c.parentElement.getBoundingClientRect();c.width=r.width;c.height=r.height;}
|
|
1291
|
+
size();
|
|
1292
|
+
const topics=Object.keys(d.topic_mastery||{});
|
|
1293
|
+
const N=Math.max(d.total_items||8,6);
|
|
1294
|
+
const nodes=[];
|
|
1295
|
+
for(let i=0;i<N;i++){nodes.push({a:i/N*Math.PI*2,r:0.3+Math.random()*0.4,
|
|
1296
|
+
topic:topics[i%(topics.length||1)]||'',s:Math.random()*4+4});}
|
|
1297
|
+
let rot=0;
|
|
1298
|
+
const COL=['#00d9ff','#ffc24b','#00ffb2','#ff9d00','#d99a3c'];
|
|
1299
|
+
function loop(){
|
|
1300
|
+
if(!document.getElementById('ovMask').classList.contains('show'))return;
|
|
1301
|
+
x.clearRect(0,0,c.width,c.height);rot+=0.002;
|
|
1302
|
+
const cx=c.width/2,cy=c.height/2,R=Math.min(cx,cy)*0.8;
|
|
1303
|
+
nodes.forEach(n=>{n._x=cx+Math.cos(n.a+rot)*n.r*R;n._y=cy+Math.sin(n.a+rot)*n.r*R;});
|
|
1304
|
+
// 连线(同主题相连)
|
|
1305
|
+
x.lineWidth=1;
|
|
1306
|
+
for(let i=0;i<nodes.length;i++)for(let j=i+1;j<nodes.length;j++){
|
|
1307
|
+
if(nodes[i].topic&&nodes[i].topic===nodes[j].topic){
|
|
1308
|
+
x.strokeStyle='rgba(255,194,75,.25)';x.beginPath();
|
|
1309
|
+
x.moveTo(nodes[i]._x,nodes[i]._y);x.lineTo(nodes[j]._x,nodes[j]._y);x.stroke();}
|
|
1310
|
+
}
|
|
1311
|
+
nodes.forEach((n,i)=>{const col=COL[topics.indexOf(n.topic)%COL.length]||'#00d9ff';
|
|
1312
|
+
x.beginPath();x.arc(n._x,n._y,n.s,0,Math.PI*2);
|
|
1313
|
+
x.fillStyle=col;x.shadowBlur=12;x.shadowColor=col;x.fill();x.shadowBlur=0;});
|
|
1314
|
+
requestAnimationFrame(loop);
|
|
1315
|
+
}
|
|
1316
|
+
loop();
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
/* ===== 左侧导航 hover 弹出精简条目 + 点击进中央大屏 ===== */
|
|
1321
|
+
const MODULE_META={
|
|
1322
|
+
tasks:{title:'任务中心',tag:'Tasks'},
|
|
1323
|
+
memory:{title:'记忆中心',tag:'Memory'},
|
|
1324
|
+
knowledge:{title:'知识库',tag:'Knowledge'},
|
|
1325
|
+
tools:{title:'技能与工具',tag:'Skills'},
|
|
1326
|
+
activity:{title:'活动',tag:'Activity'},
|
|
1327
|
+
agents:{title:'代理',tag:'Agents'},
|
|
1328
|
+
usage:{title:'使用情况',tag:'Usage'},
|
|
1329
|
+
};
|
|
1330
|
+
/* 取模块精简条目(供轮播框 + 中央大屏用)*/
|
|
1331
|
+
async function fetchModuleBrief(mod){
|
|
1332
|
+
try{
|
|
1333
|
+
if(mod==='tasks'){
|
|
1334
|
+
const out=[];
|
|
1335
|
+
for(const a of ['supervisor','finance','office']){
|
|
1336
|
+
const r=await fetch(`${API}/thoughtchain/${a}/summary`).then(x=>x.json()).catch(()=>null);
|
|
1337
|
+
if(r&&r.total_nodes){ out.push({title:`${AGENT_CN[a]||a} Agent`, sub:`${r.total_nodes} 节点 · ${r.blocked_nodes||0} 拦截`}); }
|
|
1338
|
+
}
|
|
1339
|
+
return out.length?out:[{title:'暂无任务记录',sub:'发任务后这里显示'}];
|
|
1340
|
+
}
|
|
1341
|
+
if(mod==='memory'){
|
|
1342
|
+
const s=await fetch(`${API}/memory/stats`).then(x=>x.json()).catch(()=>({}));
|
|
1343
|
+
const items=[{title:`记忆总数 ${s.total_memories??0} 条`,sub:`私有 ${s.private??0} · 共享 ${s.shared??0}`}];
|
|
1344
|
+
const ba=s.by_agent||{};
|
|
1345
|
+
Object.entries(ba).sort((a,b)=>b[1]-a[1]).forEach(([a,n])=>items.push({title:AGENT_CN[a]||a,sub:`${n} 条记忆`}));
|
|
1346
|
+
return items;
|
|
1347
|
+
}
|
|
1348
|
+
if(mod==='knowledge'){
|
|
1349
|
+
const r=await fetch(`${API}/learning/analysis`).then(x=>x.json()).catch(()=>null);
|
|
1350
|
+
if(r&&r.items&&r.items.length) return r.items.map(k=>({title:k.title||k.name||'知识条目',sub:k.tag||k.topic||'',content:k.content,topic:k.topic,source:k.source,tags:k.tags,id:k.id,notes:k.notes}));
|
|
1351
|
+
return [{title:'知识库为空',sub:'录入学习数据后显示'}];
|
|
1352
|
+
}
|
|
1353
|
+
if(mod==='tools'){
|
|
1354
|
+
return [{title:'Skill 接入',sub:'助手 Agent 工具扩展'},{title:'插件管理',sub:'开发中'}];
|
|
1355
|
+
}
|
|
1356
|
+
}catch(e){}
|
|
1357
|
+
return [{title:'加载失败',sub:'后端未连接'}];
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/* 填充三个轮播框 + 自动滚动 */
|
|
1361
|
+
function fillFeed(mod, trackId){
|
|
1362
|
+
const track=document.getElementById(trackId); if(!track)return;
|
|
1363
|
+
fetchModuleBrief(mod).then(items=>{
|
|
1364
|
+
if(!items||!items.length){ track.innerHTML='<div class="feed-empty">暂无数据</div>'; return; }
|
|
1365
|
+
const render=()=>items.map((it,idx)=>`<div class="feed-item" data-mod="${mod}" data-idx="${idx}"><b>${escapeHtml(it.title)}</b>${it.sub?`<span>${escapeHtml(it.sub)}</span>`:''}</div>`).join('');
|
|
1366
|
+
// 内容较多才滚动:复制一份首尾相接,无缝循环
|
|
1367
|
+
const enough=items.length>=4;
|
|
1368
|
+
track.innerHTML=render()+(enough?render():'');
|
|
1369
|
+
track.classList.toggle('rolling', enough);
|
|
1370
|
+
if(enough){ track.style.animationDuration=(items.length*2.2)+'s'; }
|
|
1371
|
+
else{ track.style.animationDuration=''; }
|
|
1372
|
+
// 点条目 → 中央大屏
|
|
1373
|
+
track.querySelectorAll('.feed-item').forEach(el=>el.onclick=()=>{
|
|
1374
|
+
if(mod==='knowledge'){ const it=items[+el.dataset.idx]; if(it) showKnowledgeArticle(it); }
|
|
1375
|
+
else showModuleInCore(mod);
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
function refreshFeeds(){
|
|
1380
|
+
fillFeed('tasks','feedTasks');
|
|
1381
|
+
fillFeed('memory','feedMemory');
|
|
1382
|
+
fillFeed('knowledge','feedKnowledge');
|
|
1383
|
+
}
|
|
1384
|
+
/* 单篇全文卡片(含删除 + 我的笔记)*/
|
|
1385
|
+
function _knowCardHtml(k){
|
|
1386
|
+
const notes=(k.notes||[]);
|
|
1387
|
+
return `<div class="know-card">
|
|
1388
|
+
<div class="know-head"><b>${escapeHtml(k.topic||'知识')}</b>
|
|
1389
|
+
<span class="know-tools">${k.source?`<span class="know-src">来源: ${escapeHtml(k.source)}</span>`:''}<span class="know-del" data-id="${escapeHtml(k.id||'')}" title="删除这条知识">🗑</span></span></div>
|
|
1390
|
+
<div class="know-body">${escapeHtml(k.content||k.title||'')}</div>
|
|
1391
|
+
${(k.tags&&k.tags.length)?`<div class="know-tags">${k.tags.map(t=>`<span>#${escapeHtml(t)}</span>`).join('')}</div>`:''}
|
|
1392
|
+
<div class="know-notes">
|
|
1393
|
+
<div class="know-notes-h">📝 我的笔记${notes.length?` (${notes.length})`:''}</div>
|
|
1394
|
+
${notes.length?notes.map(n=>`<div class="know-note"><span>${escapeHtml(n.text||'')}</span><em>${escapeHtml((n.ts||'').slice(0,16).replace('T',' '))}</em></div>`).join(''):'<div class="know-note-empty">还没有笔记,读到有心得就在下面记一条</div>'}
|
|
1395
|
+
<div class="know-note-add">
|
|
1396
|
+
<textarea class="know-note-input" placeholder="来吧,留下你的学习成果" rows="2"></textarea>
|
|
1397
|
+
<button class="know-note-save" data-id="${escapeHtml(k.id||'')}">保存笔记</button>
|
|
1398
|
+
</div>
|
|
1399
|
+
</div>
|
|
1400
|
+
</div>`;
|
|
1401
|
+
}
|
|
1402
|
+
/* 列表视图:每篇一行(标题+摘要+删除),点击进详情。reload=删除/刷新后重新加载 */
|
|
1403
|
+
function _renderKnowList(body, items, reload){
|
|
1404
|
+
body.innerHTML='<div class="know-index">'+items.map((k,i)=>`
|
|
1405
|
+
<div class="know-row" data-i="${i}">
|
|
1406
|
+
<div class="know-row-tx"><b>${escapeHtml(k.topic||'知识')}</b><span>${escapeHtml((k.content||k.title||'').replace(/\s+/g,' ').trim().slice(0,54))||'(空)'}</span></div>
|
|
1407
|
+
<span class="know-del" data-id="${escapeHtml(k.id||'')}" title="删除这条">🗑</span>
|
|
1408
|
+
</div>`).join('')+'</div>';
|
|
1409
|
+
body.querySelectorAll('.know-row').forEach(el=>el.onclick=(e)=>{ if(e.target.closest('.know-del'))return; _renderKnowDetail(body, items, +el.dataset.i, reload); });
|
|
1410
|
+
_wireKnowDelete(body, reload);
|
|
1411
|
+
}
|
|
1412
|
+
/* 详情视图:只显示这一篇的全文 + 返回列表 */
|
|
1413
|
+
/* 笔记保存(有内容才提交;保存后回调 onSaved 重渲染),多处复用 */
|
|
1414
|
+
function _wireKnowNote(body, item, onSaved){
|
|
1415
|
+
const saveBtn=body.querySelector('.know-note-save');
|
|
1416
|
+
if(!saveBtn)return;
|
|
1417
|
+
saveBtn.onclick=async()=>{
|
|
1418
|
+
const ta=body.querySelector('.know-note-input'); const text=(ta.value||'').trim();
|
|
1419
|
+
if(!text) return; // 没写就不存(不重复写盘)
|
|
1420
|
+
saveBtn.disabled=true;
|
|
1421
|
+
try{
|
|
1422
|
+
await fetch(`${API}/learning/note`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({item_id:item.id, note:text})});
|
|
1423
|
+
item.notes=(item.notes||[]).concat([{text, ts:new Date().toISOString()}]); // 本地追加
|
|
1424
|
+
onSaved && onSaved();
|
|
1425
|
+
}catch(e){ alert('保存笔记失败:'+e); saveBtn.disabled=false; }
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function _renderKnowDetail(body, items, i, reload){
|
|
1429
|
+
const k=items[i]; if(!k){ _renderKnowList(body, items, reload); return; }
|
|
1430
|
+
body.innerHTML=`<div class="know-back" id="knowBack">← 返回列表(共 ${items.length} 篇)</div>`+_knowCardHtml(k);
|
|
1431
|
+
document.getElementById('knowBack').onclick=()=>_renderKnowList(body, items, reload);
|
|
1432
|
+
_wireKnowDelete(body, reload);
|
|
1433
|
+
_wireKnowNote(body, k, ()=>_renderKnowDetail(body, items, i, reload));
|
|
1434
|
+
}
|
|
1435
|
+
/* 左栏点单条文章 → 中央「独立阅读器」只显示这一篇(不显示全部列表) */
|
|
1436
|
+
function showKnowledgeArticle(item){
|
|
1437
|
+
const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
|
|
1438
|
+
if(!wrap||!stage||!item)return;
|
|
1439
|
+
document.getElementById('coreChat')?.classList.remove('show'); // 开面板层前关对话浮层(两层互斥)
|
|
1440
|
+
Center.panel();
|
|
1441
|
+
stage.innerHTML=`<div class="cs-head"><div class="t"><span class="dot"></span>${escapeHtml(item.topic||'知识')} <em>Knowledge</em></div><span class="x" id="csClose" title="关闭">✕</span></div><div class="cs-body" id="csBody"></div>`;
|
|
1442
|
+
document.getElementById('csClose').onclick=clearCore;
|
|
1443
|
+
const body=document.getElementById('csBody');
|
|
1444
|
+
body.innerHTML=_knowCardHtml(item);
|
|
1445
|
+
_wireKnowDelete(body, clearCore); // 删除后清空中央
|
|
1446
|
+
_wireKnowNote(body, item, ()=>showKnowledgeArticle(item)); // 保存笔记后重渲染本篇
|
|
1447
|
+
}
|
|
1448
|
+
function _wireKnowDelete(container, onDone){
|
|
1449
|
+
container.querySelectorAll('.know-del').forEach(el=>el.onclick=async()=>{
|
|
1450
|
+
const id=el.dataset.id; if(!id||!confirm('删除这条知识?此操作不可撤销。'))return;
|
|
1451
|
+
try{
|
|
1452
|
+
await fetch(`${API}/learning/item/${encodeURIComponent(id)}`,{method:'DELETE'});
|
|
1453
|
+
onDone&&onDone(); refreshFeeds();
|
|
1454
|
+
try{ loadMemStats(); }catch(_){}
|
|
1455
|
+
}catch(e){ alert('删除失败:'+e); }
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
/* 知识库 → 中央展开全文卡片(读完整录入内容,保留换行) */
|
|
1459
|
+
async function showKnowledgeFull(){
|
|
1460
|
+
const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
|
|
1461
|
+
if(!wrap||!stage)return;
|
|
1462
|
+
document.getElementById('coreChat')?.classList.remove('show'); // 开面板前关对话浮层(否则被 z5 盖住)
|
|
1463
|
+
Center.panel();
|
|
1464
|
+
_syncNav('knowledge');
|
|
1465
|
+
stage.innerHTML=`<div class="cs-head"><div class="t"><span class="dot"></span>知识库 <em>Knowledge</em></div><span class="x" id="csClose" title="返回">✕</span></div><div class="cs-body" id="csBody"><div class="kp-empty" style="padding:30px 0">加载中…</div></div>`;
|
|
1466
|
+
document.getElementById('csClose').onclick=clearCore;
|
|
1467
|
+
const body=document.getElementById('csBody');
|
|
1468
|
+
try{
|
|
1469
|
+
const r=await fetch(`${API}/learning/analysis`).then(x=>x.json());
|
|
1470
|
+
const items=(r.items||[]);
|
|
1471
|
+
if(!items.length){ body.innerHTML='<div class="kp-empty" style="padding:30px 0">还没有录入知识。点顶部「录入知识点」添加。</div>'; return; }
|
|
1472
|
+
_renderKnowList(body, items, showKnowledgeFull);
|
|
1473
|
+
}catch(e){ body.innerHTML='<div class="kp-empty" style="padding:30px 0">后端未连接</div>'; }
|
|
1474
|
+
}
|
|
1475
|
+
/* 左栏「菜单」按钮:把原来平铺的八项功能收进一个分类菜单(浮层贴着按钮右侧展开)。
|
|
1476
|
+
靠按钮的实时位置定位,不写死坐标 —— 窗口缩放 / 侧栏宽度变化都不会错位。 */
|
|
1477
|
+
const _navMenu=document.getElementById('navMenu');
|
|
1478
|
+
const _navMenuBtn=document.getElementById('navMenuBtn');
|
|
1479
|
+
function setNavMenu(open){
|
|
1480
|
+
if(!_navMenu||!_navMenuBtn)return;
|
|
1481
|
+
const show=open===undefined?!_navMenu.classList.contains('show'):open;
|
|
1482
|
+
if(show){
|
|
1483
|
+
const r=_navMenuBtn.getBoundingClientRect();
|
|
1484
|
+
_navMenu.style.left=Math.round(r.right+10)+'px';
|
|
1485
|
+
_navMenu.style.bottom=Math.round(Math.max(10,window.innerHeight-r.bottom))+'px';
|
|
1486
|
+
_navMenu.classList.add('show'); _navMenu.setAttribute('aria-hidden','false');
|
|
1487
|
+
_navMenuBtn.classList.add('open'); _navMenuBtn.setAttribute('aria-expanded','true');
|
|
1488
|
+
}else{
|
|
1489
|
+
_navMenu.classList.remove('show'); _navMenu.setAttribute('aria-hidden','true');
|
|
1490
|
+
_navMenuBtn.classList.remove('open'); _navMenuBtn.setAttribute('aria-expanded','false');
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
_navMenuBtn?.addEventListener('click',e=>{ e.stopPropagation(); setNavMenu(); });
|
|
1494
|
+
document.getElementById('navMenuClose')?.addEventListener('click',()=>setNavMenu(false));
|
|
1495
|
+
document.addEventListener('click',e=>{
|
|
1496
|
+
if(!_navMenu||!_navMenu.classList.contains('show'))return;
|
|
1497
|
+
if(_navMenu.contains(e.target)||_navMenuBtn.contains(e.target))return;
|
|
1498
|
+
setNavMenu(false);
|
|
1499
|
+
});
|
|
1500
|
+
document.addEventListener('keydown',e=>{ if(e.key==='Escape') setNavMenu(false); });
|
|
1501
|
+
window.addEventListener('resize',()=>setNavMenu(false));
|
|
1502
|
+
|
|
1503
|
+
/* 当前视图 → 菜单按钮文案 + 菜单内高亮(视图可能从菜单、右栏 ›、slash 命令等多处打开,
|
|
1504
|
+
统一在这里同步,免得各处各写一份 active 状态) */
|
|
1505
|
+
function _syncNav(mod){
|
|
1506
|
+
let name='';
|
|
1507
|
+
document.querySelectorAll('#navList .nav-link').forEach(x=>{
|
|
1508
|
+
const hit=x.dataset.module===mod;
|
|
1509
|
+
x.classList.toggle('on',hit);
|
|
1510
|
+
if(hit) name=x.querySelector('b')?.textContent||'';
|
|
1511
|
+
});
|
|
1512
|
+
const lbl=document.getElementById('navMenuLabel');
|
|
1513
|
+
// 回显成「菜单 · 任务」:既保留"这是个菜单"的语义,又能一眼看出在哪一屏
|
|
1514
|
+
if(lbl) lbl.textContent=(name&&mod!=='overview')?`菜单 · ${name}`:'菜单';
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// 左栏导航:点击 → 中央大屏显示对应视图(overview 回核心;knowledge 走全屏知识视图)
|
|
1518
|
+
document.querySelectorAll('#navList .nav-link').forEach(el=>{
|
|
1519
|
+
el.addEventListener('click',()=>{
|
|
1520
|
+
const mod=el.dataset.module;
|
|
1521
|
+
_syncNav(mod);
|
|
1522
|
+
setNavMenu(false); // 选完就收起菜单,把屏幕还给内容
|
|
1523
|
+
if(mod==='overview') clearCore();
|
|
1524
|
+
else if(mod==='knowledge') showKnowledgeFull();
|
|
1525
|
+
else showModuleInCore(mod);
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
/* 中央大屏显示模块详情 */
|
|
1530
|
+
async function showModuleInCore(mod){
|
|
1531
|
+
const meta=MODULE_META[mod]||{title:mod,tag:''};
|
|
1532
|
+
const wrap=document.querySelector('.corewrap'), stage=document.getElementById('coreStage');
|
|
1533
|
+
document.getElementById('coreChat')?.classList.remove('show'); // 开面板前关对话浮层(否则被 z5 盖住)
|
|
1534
|
+
Center.panel();
|
|
1535
|
+
_syncNav(mod);
|
|
1536
|
+
stage.innerHTML=`<div class="cs-head"><div class="t"><span class="dot"></span>${meta.title} <em>${meta.tag}</em></div>
|
|
1537
|
+
<span class="x" id="csClose" title="返回核心">✕</span></div>
|
|
1538
|
+
<div class="cs-body" id="csBody"><div class="kp-empty" style="padding:30px 0">加载中…</div></div>`;
|
|
1539
|
+
document.getElementById('csClose').onclick=clearCore;
|
|
1540
|
+
const body=document.getElementById('csBody');
|
|
1541
|
+
try{
|
|
1542
|
+
body.innerHTML=await renderModuleBig(mod);
|
|
1543
|
+
if(mod==='tools') wireToolCenter(body);
|
|
1544
|
+
if(mod==='memory') wirePrefDelete(body);
|
|
1545
|
+
}catch(e){ body.innerHTML='<div class="kp-empty">后端未连接</div>'; }
|
|
1546
|
+
}
|
|
1547
|
+
/* 偏好删除按钮:删一条误记的偏好 → 刷新记忆中心大屏(F3 前端闭环) */
|
|
1548
|
+
function wirePrefDelete(body){
|
|
1549
|
+
body.querySelectorAll('.pref-del').forEach(btn=>{
|
|
1550
|
+
btn.onclick=async()=>{
|
|
1551
|
+
const id=btn.dataset.prefId; if(!id)return;
|
|
1552
|
+
btn.disabled=true; btn.textContent='…';
|
|
1553
|
+
try{ await fetch(`${API}/preferences/${encodeURIComponent(id)}`,{method:'DELETE'}); }catch(e){}
|
|
1554
|
+
showModuleInCore('memory'); // 刷新列表
|
|
1555
|
+
};
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
async function renderModuleBig(mod){
|
|
1559
|
+
if(mod==='tasks'){
|
|
1560
|
+
let h='<div class="big-grid">';
|
|
1561
|
+
for(const a of ['supervisor','finance','office']){
|
|
1562
|
+
const r=await fetch(`${API}/thoughtchain/${a}/summary`).then(x=>x.json()).catch(()=>null);
|
|
1563
|
+
h+=`<div class="big-card"><div class="bc-head">${AGENT_CN[a]||a} Agent</div>
|
|
1564
|
+
<div class="bc-row"><span>推理节点</span><b>${r?.total_nodes??0}</b></div>
|
|
1565
|
+
<div class="bc-row"><span>被拦截</span><b style="color:var(--amber)">${r?.blocked_nodes??0}</b></div>
|
|
1566
|
+
<div class="bc-row"><span>拦截率</span><b>${r?.block_rate!=null?(r.block_rate*100).toFixed(1)+'%':'—'}</b></div>
|
|
1567
|
+
<div class="bc-row"><span>平均置信</span><b>${r?.avg_confidence!=null?r.avg_confidence.toFixed(2):'—'}</b></div></div>`;
|
|
1568
|
+
}
|
|
1569
|
+
h+='</div>';
|
|
1570
|
+
return h;
|
|
1571
|
+
}
|
|
1572
|
+
if(mod==='activity'){
|
|
1573
|
+
const d=await fetch(`${API}/bus/history?limit=40`).then(x=>x.json()).catch(()=>({}));
|
|
1574
|
+
const evs=(d.events||[]);
|
|
1575
|
+
if(!evs.length) return '<div class="kp-empty" style="padding:40px">暂无活动记录 · 系统事件(执行→审计→记忆)会实时汇入这里</div>';
|
|
1576
|
+
// 事件真实字段:{channel, topic, payload, ts(epoch 秒·浮点), source, correlation_id}
|
|
1577
|
+
return '<div class="big-list">'+evs.slice().reverse().map(e=>{
|
|
1578
|
+
const topic=e.topic||e.channel||'事件';
|
|
1579
|
+
const ts=Number(e.ts);
|
|
1580
|
+
const when=(isFinite(ts)&&ts>0)?new Date(ts*1000).toLocaleString('zh-CN',{hour12:false}):'';
|
|
1581
|
+
const p=(e.payload&&typeof e.payload==='object')?e.payload:{};
|
|
1582
|
+
const bits=Object.entries(p).slice(0,4)
|
|
1583
|
+
.map(([k,v])=>`${k}=${(v&&typeof v==='object')?JSON.stringify(v):v}`).join(' · ');
|
|
1584
|
+
const meta=[when, e.source].filter(Boolean).join(' · ');
|
|
1585
|
+
return `<div class="big-li"><b>${escapeHtml(String(topic))}</b><span>${escapeHtml(meta)}${bits?' · '+escapeHtml(bits.slice(0,110)):''}</span></div>`;
|
|
1586
|
+
}).join('')+'</div>';
|
|
1587
|
+
}
|
|
1588
|
+
if(mod==='agents'){
|
|
1589
|
+
const AG=[['supervisor','主管','全局统筹·任务分派'],
|
|
1590
|
+
['finance','财务','收支·预算·理财'],['office','办公学习','学习·知识·写作·日程行程']];
|
|
1591
|
+
const stats=await fetch(`${API}/memory/stats`).then(x=>x.json()).catch(()=>({}));
|
|
1592
|
+
const ba=stats.by_agent||{};
|
|
1593
|
+
let h='<div class="big-grid">';
|
|
1594
|
+
for(const [id,cn,role] of AG){
|
|
1595
|
+
const tc=await fetch(`${API}/thoughtchain/${id}/summary`).then(x=>x.json()).catch(()=>null);
|
|
1596
|
+
h+=`<div class="big-card"><div class="bc-head">${cn} Agent</div>
|
|
1597
|
+
<div style="font-size:11px;color:var(--dim);margin:-4px 0 8px">${role}</div>
|
|
1598
|
+
<div class="bc-row"><span>记忆</span><b>${ba[id]??0} 条</b></div>
|
|
1599
|
+
<div class="bc-row"><span>推理节点</span><b>${tc?.total_nodes??0}</b></div>
|
|
1600
|
+
<div class="bc-row"><span>被拦截</span><b style="color:var(--amber)">${tc?.blocked_nodes??0}</b></div></div>`;
|
|
1601
|
+
}
|
|
1602
|
+
return h+'</div>';
|
|
1603
|
+
}
|
|
1604
|
+
if(mod==='usage'){
|
|
1605
|
+
const m=await fetch(`${API}/metrics`).then(x=>x.json()).catch(()=>({}));
|
|
1606
|
+
const tr=await fetch(`${API}/tokens/report`).then(x=>x.json()).catch(()=>({}));
|
|
1607
|
+
const llm=(m.llm||m.providers||{});
|
|
1608
|
+
const llmRows=Object.entries(llm).map(([k,v])=>{
|
|
1609
|
+
const calls=(v&&typeof v==='object')?(v.calls??v.count??v.total??v.ok??'—'):v;
|
|
1610
|
+
return `<div class="bc-row"><span>${escapeHtml(k)}</span><b>${escapeHtml(String(calls))}</b></div>`;
|
|
1611
|
+
}).join('');
|
|
1612
|
+
const up=m.uptime??m.uptime_seconds;
|
|
1613
|
+
const report=String(tr.report||tr.error||'token meter 未启用或暂无数据');
|
|
1614
|
+
return `<div class="big-grid">
|
|
1615
|
+
<div class="big-card"><div class="bc-head">运行</div>
|
|
1616
|
+
<div class="bc-row"><span>uptime</span><b>${up!=null?Math.round(up)+' s':'—'}</b></div>
|
|
1617
|
+
<div class="bc-row"><span>LLM provider 数</span><b>${Object.keys(llm).length}</b></div></div>
|
|
1618
|
+
<div class="big-card"><div class="bc-head">各 provider 调用</div>${llmRows||'<div class="kp-empty" style="padding:8px">暂无调用</div>'}</div>
|
|
1619
|
+
<div class="big-card" style="grid-column:1/-1"><div class="bc-head">Token 消耗报告</div>
|
|
1620
|
+
<pre style="white-space:pre-wrap;font-size:11px;color:var(--dim);line-height:1.55;margin:0;font-family:var(--font)">${escapeHtml(report)}</pre></div>
|
|
1621
|
+
</div>`;
|
|
1622
|
+
}
|
|
1623
|
+
if(mod==='memory'){
|
|
1624
|
+
const s=await fetch(`${API}/memory/stats`).then(x=>x.json()).catch(()=>({}));
|
|
1625
|
+
const pf=await fetch(`${API}/preferences`).then(x=>x.json()).catch(()=>({preferences:[]}));
|
|
1626
|
+
const ba=s.by_agent||{};
|
|
1627
|
+
let rows=Object.entries(ba).sort((a,b)=>b[1]-a[1]).map(([a,n])=>`<div class="bc-row"><span>${AGENT_CN[a]||a}</span><b>${n} 条</b></div>`).join('')||'<div class="kp-empty">暂无分层记忆</div>';
|
|
1628
|
+
const prefs=(pf.preferences||[]);
|
|
1629
|
+
const prefRows=prefs.length
|
|
1630
|
+
? prefs.map(p=>`<div class="bc-row" style="align-items:flex-start;gap:6px">
|
|
1631
|
+
<span style="flex:1;font-size:11px;line-height:1.4">${escapeHtml((p.content||'').replace(/^用户偏好[::]/,''))}</span>
|
|
1632
|
+
<button class="pref-del" data-pref-id="${escapeHtml(p.id||'')}" title="删除这条偏好" style="background:none;border:0;color:var(--amber);cursor:pointer;font-size:13px;padding:0 2px">✕</button></div>`).join('')
|
|
1633
|
+
: '<div class="kp-empty">暂无记住的偏好(说"我喜欢…/以后…"会被记住)</div>';
|
|
1634
|
+
return `<div class="big-grid">
|
|
1635
|
+
<div class="big-card"><div class="bc-head">记忆总览</div>
|
|
1636
|
+
<div class="bc-row"><span>总条数</span><b>${s.total_memories??0}</b></div>
|
|
1637
|
+
<div class="bc-row"><span>私有层</span><b>${s.private??0}</b></div>
|
|
1638
|
+
<div class="bc-row"><span>共享层</span><b>${s.shared??0}</b></div>
|
|
1639
|
+
<div class="bc-row"><span>知识关系</span><b>${s.relations??0}</b></div></div>
|
|
1640
|
+
<div class="big-card"><div class="bc-head">按 Agent 分布</div>${rows}</div>
|
|
1641
|
+
<div class="big-card"><div class="bc-head">后端</div>
|
|
1642
|
+
<div class="bc-row"><span>backend</span><b>${s.backend||'—'}</b></div>
|
|
1643
|
+
<div class="bc-row"><span>Embedding</span><b style="font-size:11px">${s.embedding||'—'}</b></div></div>
|
|
1644
|
+
<div class="big-card"><div class="bc-head">用户偏好 <em style="font-size:10px;color:var(--ink-dim);font-style:normal">点 ✕ 删除误记</em></div>${prefRows}</div>
|
|
1645
|
+
</div>`;
|
|
1646
|
+
}
|
|
1647
|
+
if(mod==='knowledge'){
|
|
1648
|
+
const r=await fetch(`${API}/learning/analysis`).then(x=>x.json()).catch(()=>null);
|
|
1649
|
+
if(r&&r.items&&r.items.length){
|
|
1650
|
+
return '<div class="big-list">'+r.items.map(k=>`<div class="big-li"><b>${k.title||k.name||'条目'}</b><span>${k.category||k.tag||''}</span></div>`).join('')+'</div>';
|
|
1651
|
+
}
|
|
1652
|
+
return '<div class="kp-empty" style="padding:40px">知识库为空 · 通过办公 Agent 录入学习数据后显示</div>';
|
|
1653
|
+
}
|
|
1654
|
+
if(mod==='tools'){
|
|
1655
|
+
let skills=[], breakers={}, audit=[];
|
|
1656
|
+
try{ skills=(await fetch(`${API}/ahs/skills`).then(x=>x.json())).skills||[]; }catch(e){}
|
|
1657
|
+
try{ breakers=(await fetch(`${API}/ahs/breaker`).then(x=>x.json())).breakers||{}; }catch(e){}
|
|
1658
|
+
try{ audit=(await fetch(`${API}/ahs/hooks/audit`).then(x=>x.json())).audit||[]; }catch(e){}
|
|
1659
|
+
let servers=[];
|
|
1660
|
+
try{ servers=(await fetch(`${API}/ahs/mcp/servers`).then(x=>x.json())).servers||[]; }catch(e){}
|
|
1661
|
+
let mdskills=[];
|
|
1662
|
+
try{ mdskills=(await fetch(`${API}/ahs/md-skills`).then(x=>x.json())).skills||[]; }catch(e){}
|
|
1663
|
+
const mdRows=mdskills.map(s=>`<div class="md-row${s.blocked?' tc-blocked':''}" data-id="${escapeHtml(s.id)}" data-triggers="${escapeHtml((s.triggers||[]).join(', '))}">
|
|
1664
|
+
<div class="md-h"><b>${escapeHtml(s.name)}</b>${s.version?` <span class="tc-kind">v${escapeHtml(s.version)}</span>`:''} <span class="tc-trust" style="background:#b0413e" title="信任分级">low</span> <span class="tc-src user">${s.enabled?'启用':'停用'}</span>${s.blocked?' <span class="tc-blk-tag">已封杀</span>':''}</div>
|
|
1665
|
+
<div class="md-desc">${escapeHtml(s.description||'')} · 触发: ${escapeHtml((s.triggers||[]).join(', ')||'无(靠描述相关注入)')}</div>
|
|
1666
|
+
<div class="md-acts"><button class="md-view">查看</button>
|
|
1667
|
+
<button class="md-trig">编辑触发词</button>
|
|
1668
|
+
<button class="md-toggle">${s.enabled?'停用':'启用'}</button>
|
|
1669
|
+
<button class="md-deny">${s.blocked?'解封':'封杀'}</button>
|
|
1670
|
+
<button class="md-del">删除</button></div></div>`).join('')||'<div class="kp-empty" style="padding:10px">未安装指令技能</div>';
|
|
1671
|
+
const srvRows=servers.map(s=>`<div class="mcp-row" data-id="${escapeHtml(s.id)}">
|
|
1672
|
+
<div><b>${escapeHtml(s.name||s.id)}</b> <span class="tc-kind">${escapeHtml(s.transport)}</span> <span class="mcp-tc">${s.tool_count} 工具</span></div>
|
|
1673
|
+
<div class="mcp-url">${escapeHtml(s.url)}</div>
|
|
1674
|
+
<button class="mcp-del">移除</button></div>`).join('')||'<div class="kp-empty" style="padding:10px">未接入 MCP 服务器</div>';
|
|
1675
|
+
const seColor={read:'#2c9678',write:'#d9822b',send:'#0066CC',pay:'#b0413e'};
|
|
1676
|
+
const trustColor={high:'#2c9678',mid:'#d9822b',low:'#b0413e'};
|
|
1677
|
+
const brDot=st=>st&&st.state==='open'?'#b0413e':st&&st.state==='half_open'?'#d9822b':'#2c9678';
|
|
1678
|
+
const rows=skills.map(s=>`<div class="tc-row${s.blocked?' tc-blocked':''}" data-id="${escapeHtml(s.id)}">
|
|
1679
|
+
<div class="tc-main"><b>${escapeHtml(s.name||s.id)}</b>
|
|
1680
|
+
<span class="tc-badge" style="background:${seColor[s.side_effect]||'#888'}">${escapeHtml(s.side_effect)}</span>
|
|
1681
|
+
<span class="tc-trust" style="background:${trustColor[s.trust]||'#888'}" title="信任分级">${escapeHtml(s.trust||'')}</span>
|
|
1682
|
+
<span class="tc-kind">${escapeHtml(s.kind)}</span>
|
|
1683
|
+
<span class="tc-src ${s.source}">${s.source==='user'?'已安装':'内置'}</span>
|
|
1684
|
+
<span class="tc-brk" title="断路器" style="background:${brDot(breakers[s.id])}"></span>
|
|
1685
|
+
${s.blocked?'<span class="tc-blk-tag">已封杀</span>':''}</div>
|
|
1686
|
+
<div class="tc-desc">${escapeHtml(s.description||'')} · 触发: ${escapeHtml((s.triggers||[]).join(', ')||'—')}</div>
|
|
1687
|
+
<div class="tc-acts">
|
|
1688
|
+
<label class="tc-sw"><input type="checkbox" class="tc-en" ${s.enabled?'checked':''}><span>启用</span></label>
|
|
1689
|
+
<button class="tc-test">测试</button>
|
|
1690
|
+
<button class="tc-deny">${s.blocked?'解封':'封杀'}</button>
|
|
1691
|
+
${s.source==='user'?'<button class="tc-del">删除</button>':''}
|
|
1692
|
+
</div></div>`).join('')||'<div class="kp-empty">还没有技能</div>';
|
|
1693
|
+
const auditRows=audit.slice(0,8).map(a=>`<div class="tc-audit"><span>${escapeHtml(a.skill_id||a.skill||'—')}</span><em>${escapeHtml(String(a.result||a.status||''))}</em></div>`).join('')||'<div class="kp-empty" style="padding:12px">暂无调用</div>';
|
|
1694
|
+
return `<div class="tc-wrap">
|
|
1695
|
+
<div class="tc-col">
|
|
1696
|
+
<div class="tc-h">连接器 · MCP <span>${servers.length}</span></div>
|
|
1697
|
+
<div class="mcp-list">${srvRows}</div>
|
|
1698
|
+
<div class="mcp-form">
|
|
1699
|
+
<input id="mcf-id" placeholder="服务器 id(如 my_mcp)">
|
|
1700
|
+
<input id="mcf-name" placeholder="名称">
|
|
1701
|
+
<input id="mcf-url" placeholder="URL https://.../mcp">
|
|
1702
|
+
<input id="mcf-token" placeholder="Authorization 令牌(可选,明文存本地)">
|
|
1703
|
+
<select id="mcf-transport"><option value="streamable_http">streamable_http</option><option value="sse">sse</option></select>
|
|
1704
|
+
<button id="mcf-submit">接入</button>
|
|
1705
|
+
</div>
|
|
1706
|
+
<div class="tc-h" style="margin-top:14px">技能 <span>${skills.length}</span></div>
|
|
1707
|
+
<div class="tc-list">${rows}</div>
|
|
1708
|
+
<div class="tc-h" style="margin-top:14px">指令技能 · SKILL.md <span>${mdskills.length}</span></div>
|
|
1709
|
+
<div class="md-list">${mdRows}</div>
|
|
1710
|
+
<div class="md-form">
|
|
1711
|
+
<input type="file" id="mdf-file" accept=".md,.markdown,.zip,text/markdown">
|
|
1712
|
+
<button id="mdf-submit">上传安装</button>
|
|
1713
|
+
</div>
|
|
1714
|
+
<div class="tc-h" style="margin-top:14px">安装技能</div>
|
|
1715
|
+
<div class="tc-form">
|
|
1716
|
+
<input id="tcf-id" placeholder="唯一 id(如 my_weather)">
|
|
1717
|
+
<input id="tcf-name" placeholder="名称">
|
|
1718
|
+
<select id="tcf-kind"><option value="web_api">web_api(HTTP 接口)</option><option value="mcp">mcp(MCP 工具)</option></select>
|
|
1719
|
+
<input id="tcf-triggers" placeholder="触发词(逗号分隔)">
|
|
1720
|
+
<select id="tcf-se"><option value="read">read(只读)</option><option value="write">write</option><option value="send">send</option><option value="pay">pay</option></select>
|
|
1721
|
+
<input id="tcf-endpoint" placeholder="web_api: https://... | mcp: server 名">
|
|
1722
|
+
<input id="tcf-tool" placeholder="mcp: tool 名(web_api 留空)">
|
|
1723
|
+
<button id="tcf-submit">安装</button>
|
|
1724
|
+
</div>
|
|
1725
|
+
</div>
|
|
1726
|
+
<div class="tc-col tc-gov">
|
|
1727
|
+
<div class="tc-h">治理 · 最近调用</div>
|
|
1728
|
+
<div class="tc-auditlist">${auditRows}</div>
|
|
1729
|
+
</div>
|
|
1730
|
+
</div>`;
|
|
1731
|
+
}
|
|
1732
|
+
return '<div class="kp-empty">未知模块</div>';
|
|
1733
|
+
}
|
|
1734
|
+
async function wireToolCenter(root){
|
|
1735
|
+
const reload=()=>showModuleInCore('tools');
|
|
1736
|
+
root.querySelectorAll('.tc-row').forEach(row=>{
|
|
1737
|
+
const id=row.dataset.id;
|
|
1738
|
+
const en=row.querySelector('.tc-en');
|
|
1739
|
+
if(en) en.onchange=async()=>{ await fetch(`${API}/ahs/skills/${encodeURIComponent(id)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:en.checked})}); reload(); };
|
|
1740
|
+
const del=row.querySelector('.tc-del');
|
|
1741
|
+
if(del) del.onclick=async()=>{ if(!confirm('删除这个技能?'))return; await fetch(`${API}/ahs/skills/${encodeURIComponent(id)}`,{method:'DELETE'}); reload(); };
|
|
1742
|
+
const deny=row.querySelector('.tc-deny');
|
|
1743
|
+
if(deny) deny.onclick=async()=>{ const on=deny.textContent.trim()==='封杀';
|
|
1744
|
+
if(on&&!confirm('封杀该技能?deny 会压倒启用、立即对所有执行/注入生效。'))return;
|
|
1745
|
+
if(on) await fetch(`${API}/ahs/policy/deny`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})});
|
|
1746
|
+
else await fetch(`${API}/ahs/policy/deny/${encodeURIComponent(id)}`,{method:'DELETE'});
|
|
1747
|
+
reload(); };
|
|
1748
|
+
const test=row.querySelector('.tc-test');
|
|
1749
|
+
if(test) test.onclick=async()=>{
|
|
1750
|
+
let args={}; const raw=prompt('测试参数(JSON,可留空)','{}');
|
|
1751
|
+
if(raw===null)return;
|
|
1752
|
+
try{ args=raw.trim()?JSON.parse(raw):{}; }catch(e){ alert('参数不是合法 JSON'); return; }
|
|
1753
|
+
let r=await fetch(`${API}/ahs/skills/${encodeURIComponent(id)}/test`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({args})}).then(x=>x.json());
|
|
1754
|
+
if(r.status==='needs_confirmation'){
|
|
1755
|
+
if(!confirm('此操作有副作用,确认执行?'))return;
|
|
1756
|
+
r=await fetch(`${API}/ahs/skills/${encodeURIComponent(id)}/test`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({args,confirmed:true})}).then(x=>x.json());
|
|
1757
|
+
}
|
|
1758
|
+
alert((r.success?'✓ 成功':'✗ 失败')+' · '+(r.message||JSON.stringify(r.output||{}).slice(0,200)));
|
|
1759
|
+
};
|
|
1760
|
+
});
|
|
1761
|
+
root.querySelectorAll('.mcp-row').forEach(row=>{
|
|
1762
|
+
const del=row.querySelector('.mcp-del');
|
|
1763
|
+
if(del) del.onclick=async()=>{ if(!confirm('移除该 MCP 服务器及其全部工具技能?'))return;
|
|
1764
|
+
await fetch(`${API}/ahs/mcp/servers/${encodeURIComponent(row.dataset.id)}`,{method:'DELETE'}); reload(); };
|
|
1765
|
+
});
|
|
1766
|
+
const msub=root.querySelector('#mcf-submit');
|
|
1767
|
+
if(msub) msub.onclick=async()=>{
|
|
1768
|
+
const g=s=>(root.querySelector('#mcf-'+s)||{}).value||'';
|
|
1769
|
+
const id=g('id').trim(), url=g('url').trim();
|
|
1770
|
+
if(!id||!url){ alert('id 和 URL 必填'); return; }
|
|
1771
|
+
const headers={}; const tok=g('token').trim(); if(tok) headers['Authorization']=tok;
|
|
1772
|
+
const body={id,name:g('name').trim()||id,url,headers,transport:g('transport')};
|
|
1773
|
+
const doPost=b=>fetch(`${API}/ahs/mcp/servers`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
|
|
1774
|
+
let r=await doPost(body);
|
|
1775
|
+
if(r.status===403){ const e=await r.json().catch(()=>({})); alert('❌ 接入被审核拦截:\n'+((e.detail&&e.detail.reasons)||['被拒绝']).join('\n')); return; }
|
|
1776
|
+
if(r.status===409){ const e=await r.json().catch(()=>({})); const w=(e.detail&&e.detail.warnings)||[];
|
|
1777
|
+
if(!confirm('⚠ 接入审核提示风险:\n'+w.join('\n')+'\n\n确认仍要接入?')) return;
|
|
1778
|
+
r=await doPost({...body, acknowledged:true});
|
|
1779
|
+
}
|
|
1780
|
+
if(!r.ok){ const e=await r.json().catch(()=>({})); alert('接入失败: '+(JSON.stringify(e&&e.detail)||r.status)); return; }
|
|
1781
|
+
const d=await r.json(); alert(`✓ 已接入,注册 ${d.tools_registered} 个工具技能`); reload();
|
|
1782
|
+
};
|
|
1783
|
+
root.querySelectorAll('.md-row').forEach(row=>{
|
|
1784
|
+
const id=row.dataset.id;
|
|
1785
|
+
const v=row.querySelector('.md-view');
|
|
1786
|
+
if(v) v.onclick=async()=>{ const d=await fetch(`${API}/ahs/md-skills/${encodeURIComponent(id)}`).then(x=>x.json());
|
|
1787
|
+
alert(`【${d.name}】\n${d.description}\n\n触发: ${(d.triggers||[]).join(', ')||'无'}\n\n${(d.body||'').slice(0,1500)}`); };
|
|
1788
|
+
const tg=row.querySelector('.md-toggle');
|
|
1789
|
+
if(tg) tg.onclick=async()=>{ const on=tg.textContent.trim()==='启用';
|
|
1790
|
+
await fetch(`${API}/ahs/md-skills/${encodeURIComponent(id)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:on})}); reload(); };
|
|
1791
|
+
const tr=row.querySelector('.md-trig');
|
|
1792
|
+
if(tr) tr.onclick=async()=>{
|
|
1793
|
+
const cur=row.dataset.triggers||'';
|
|
1794
|
+
const val=prompt('触发词(逗号分隔;留空=靠英文描述相关匹配)。\n中文任务想稳定触发就在这加中文词。', cur);
|
|
1795
|
+
if(val===null)return;
|
|
1796
|
+
const triggers=val.split(',').map(x=>x.trim()).filter(Boolean);
|
|
1797
|
+
await fetch(`${API}/ahs/md-skills/${encodeURIComponent(id)}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({triggers})}); reload(); };
|
|
1798
|
+
const dy=row.querySelector('.md-deny');
|
|
1799
|
+
if(dy) dy.onclick=async()=>{ const on=dy.textContent.trim()==='封杀';
|
|
1800
|
+
if(on&&!confirm('封杀该指令技能?封杀后不再注入任何 Agent 提示词。'))return;
|
|
1801
|
+
if(on) await fetch(`${API}/ahs/policy/deny`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})});
|
|
1802
|
+
else await fetch(`${API}/ahs/policy/deny/${encodeURIComponent(id)}`,{method:'DELETE'});
|
|
1803
|
+
reload(); };
|
|
1804
|
+
const dl=row.querySelector('.md-del');
|
|
1805
|
+
if(dl) dl.onclick=async()=>{ if(!confirm('删除该指令技能?'))return;
|
|
1806
|
+
await fetch(`${API}/ahs/md-skills/${encodeURIComponent(id)}`,{method:'DELETE'}); reload(); };
|
|
1807
|
+
});
|
|
1808
|
+
const mdsub=root.querySelector('#mdf-submit');
|
|
1809
|
+
if(mdsub) mdsub.onclick=async()=>{
|
|
1810
|
+
const f=(root.querySelector('#mdf-file')||{}).files?.[0];
|
|
1811
|
+
if(!f){ alert('请先选择 .md 或 OpenClaw .zip 包'); return; }
|
|
1812
|
+
let body;
|
|
1813
|
+
if(/\.zip$/i.test(f.name)){
|
|
1814
|
+
const b64=await new Promise((res,rej)=>{const fr=new FileReader();fr.onload=()=>res(String(fr.result).split(',')[1]);fr.onerror=rej;fr.readAsDataURL(f);});
|
|
1815
|
+
body={package_b64:b64};
|
|
1816
|
+
}else{
|
|
1817
|
+
const content=await f.text();
|
|
1818
|
+
const id=f.name.replace(/\.(md|markdown)$/i,'').replace(/[^a-z0-9]+/gi,'_').toLowerCase();
|
|
1819
|
+
body={content,id};
|
|
1820
|
+
}
|
|
1821
|
+
const doPost=b=>fetch(`${API}/ahs/md-skills`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
|
|
1822
|
+
let r=await doPost(body);
|
|
1823
|
+
if(r.status===400){ const e=await r.json().catch(()=>({})); alert('解析失败: '+(e.detail||'SKILL.md/包 格式错')); return; }
|
|
1824
|
+
if(r.status===409){ const e=await r.json().catch(()=>({})); const w=(e.detail&&e.detail.warnings)||[];
|
|
1825
|
+
if(!confirm('⚠ 安装审核提示:\n'+w.join('\n')+'\n\n确认安装?')) return;
|
|
1826
|
+
r=await doPost({...body,acknowledged:true});
|
|
1827
|
+
}
|
|
1828
|
+
if(!r.ok){ const e=await r.json().catch(()=>({})); alert('安装失败: '+(JSON.stringify(e&&e.detail)||r.status)); return; }
|
|
1829
|
+
alert('✓ 指令技能已安装'); reload();
|
|
1830
|
+
};
|
|
1831
|
+
const sub=root.querySelector('#tcf-submit');
|
|
1832
|
+
if(sub) sub.onclick=async()=>{
|
|
1833
|
+
const g=s=>(root.querySelector('#tcf-'+s)||{}).value||'';
|
|
1834
|
+
const kind=g('kind');
|
|
1835
|
+
const body={id:g('id').trim(),name:g('name').trim()||g('id').trim(),description:'',kind,
|
|
1836
|
+
triggers:g('triggers').split(',').map(s=>s.trim()).filter(Boolean),side_effect:g('se')};
|
|
1837
|
+
if(!body.id){ alert('id 必填'); return; }
|
|
1838
|
+
if(kind==='web_api'){ body.endpoint=g('endpoint').trim(); }
|
|
1839
|
+
else{ body.mcp_server=g('endpoint').trim(); body.mcp_tool=g('tool').trim(); }
|
|
1840
|
+
const doPost=b=>fetch(`${API}/ahs/skills`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
|
|
1841
|
+
let r=await doPost(body);
|
|
1842
|
+
if(r.status===403){ const e=await r.json().catch(()=>({})); alert('❌ 安装被审核拦截:\n'+((e.detail&&e.detail.reasons)||['被拒绝']).join('\n')); return; }
|
|
1843
|
+
if(r.status===409){ const e=await r.json().catch(()=>({})); const w=(e.detail&&e.detail.warnings)||[];
|
|
1844
|
+
if(!confirm('⚠ 安装审核提示风险:\n'+w.join('\n')+'\n\n确认仍要安装?')) return;
|
|
1845
|
+
r=await doPost({...body, acknowledged:true});
|
|
1846
|
+
}
|
|
1847
|
+
if(!r.ok){ const e=await r.json().catch(()=>({})); alert('安装失败: '+(JSON.stringify(e.detail)||r.status)); return; }
|
|
1848
|
+
reload();
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
/* ===== 数据录入表单 ===== */
|
|
1855
|
+
const ENTRY_FORMS={
|
|
1856
|
+
finance:{title:'录入交易',sub:'添加一笔收支记录',endpoint:'/finance/import',
|
|
1857
|
+
fields:[
|
|
1858
|
+
{k:'amount',label:'金额(支出填负数)',type:'number',ph:'-29.99'},
|
|
1859
|
+
{k:'category',label:'分类',type:'text',ph:'food / subscription / transport'},
|
|
1860
|
+
{k:'entity',label:'商户/对象',type:'text',ph:'星巴克'},
|
|
1861
|
+
{k:'account',label:'账户',type:'text',ph:'credit_card_1'},
|
|
1862
|
+
],
|
|
1863
|
+
build:v=>({rows:[{amount:parseFloat(v.amount),category:v.category||'uncategorized',
|
|
1864
|
+
entity:v.entity||'unknown',account:v.account||'default'}]})},
|
|
1865
|
+
office:{title:'录入知识点',sub:'添加一条学习知识',endpoint:'/learning/import',
|
|
1866
|
+
fields:[
|
|
1867
|
+
{k:'content',label:'知识内容(支持多行,可整段粘贴)',type:'textarea',ph:'粘贴完整知识/教程,支持多行\n如:\n第一步 …\n第二步 …'},
|
|
1868
|
+
{k:'topic',label:'主题',type:'text',ph:'学习方法'},
|
|
1869
|
+
{k:'source',label:'来源',type:'text',ph:'《学习之道》'},
|
|
1870
|
+
{k:'tags',label:'标签(多个用 | 分隔)',type:'text',ph:'例:安装 | 教程 | 工具'},
|
|
1871
|
+
],
|
|
1872
|
+
build:v=>({rows:[{content:v.content,topic:v.topic||'general',source:v.source||'',
|
|
1873
|
+
tags:(v.tags||'').split('|').map(s=>s.trim()).filter(Boolean)}]})},
|
|
1874
|
+
admin:{title:'录入行程点',sub:'添加一个行程地点',endpoint:'/itinerary/import',
|
|
1875
|
+
fields:[
|
|
1876
|
+
{k:'name',label:'地点名',type:'text',ph:'客户A会议'},
|
|
1877
|
+
{k:'address',label:'地址(配高德Key可自动定位)',type:'text',ph:'北京市朝阳区三里屯'},
|
|
1878
|
+
{k:'lng',label:'经度',type:'number',ph:'116.455'},
|
|
1879
|
+
{k:'lat',label:'纬度',type:'number',ph:'39.936'},
|
|
1880
|
+
{k:'start_time',label:'到达时间',type:'text',ph:'2026-06-10T10:00:00'},
|
|
1881
|
+
{k:'duration_min',label:'停留(分钟)',type:'number',ph:'60'},
|
|
1882
|
+
],
|
|
1883
|
+
build:v=>({rows:[{name:v.name,address:v.address||'',
|
|
1884
|
+
lng:v.lng?parseFloat(v.lng):null,lat:v.lat?parseFloat(v.lat):null,
|
|
1885
|
+
start_time:v.start_time||null,duration_min:parseInt(v.duration_min||0),city:''}]})},
|
|
1886
|
+
};
|
|
1887
|
+
let _entryKind=null;
|
|
1888
|
+
function openEntry(kind){
|
|
1889
|
+
const f=ENTRY_FORMS[kind]; if(!f)return; _entryKind=kind;
|
|
1890
|
+
document.getElementById('entryTitle').textContent=f.title;
|
|
1891
|
+
document.getElementById('entrySub').textContent=f.sub;
|
|
1892
|
+
document.getElementById('entryMsg').textContent='';
|
|
1893
|
+
let h='';
|
|
1894
|
+
// 行程点的经纬度并排
|
|
1895
|
+
f.fields.forEach(fd=>{
|
|
1896
|
+
const ctl = fd.type==='textarea'
|
|
1897
|
+
? `<textarea data-k="${fd.k}" placeholder="${fd.ph||''}" rows="8" style="width:100%;resize:vertical;font-family:inherit;line-height:1.6"></textarea>`
|
|
1898
|
+
: `<input data-k="${fd.k}" type="${fd.type}" placeholder="${fd.ph||''}">`;
|
|
1899
|
+
h+=`<div class="entry-field"><label>${fd.label}</label>${ctl}</div>`;
|
|
1900
|
+
});
|
|
1901
|
+
document.getElementById('entryFields').innerHTML=h;
|
|
1902
|
+
document.getElementById('entryMask').classList.add('show');
|
|
1903
|
+
}
|
|
1904
|
+
function closeEntry(){document.getElementById('entryMask').classList.remove('show');}
|
|
1905
|
+
document.getElementById('entryCancel')?.addEventListener('click',closeEntry);
|
|
1906
|
+
document.getElementById('entryMask')?.addEventListener('click',e=>{if(e.target.id==='entryMask')closeEntry();});
|
|
1907
|
+
document.getElementById('entrySubmit')?.addEventListener('click',async()=>{
|
|
1908
|
+
const f=ENTRY_FORMS[_entryKind]; if(!f)return;
|
|
1909
|
+
const v={}; document.querySelectorAll('#entryFields input, #entryFields textarea').forEach(i=>v[i.dataset.k]=i.value);
|
|
1910
|
+
const msg=document.getElementById('entryMsg');
|
|
1911
|
+
try{
|
|
1912
|
+
const body=f.build(v);
|
|
1913
|
+
const r=await fetch(`${API}${f.endpoint}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
1914
|
+
const d=await r.json();
|
|
1915
|
+
msg.style.color='var(--green)'; msg.textContent=`✓ 已录入 ${d.imported||0} 条`;
|
|
1916
|
+
setTimeout(()=>{closeEntry(); loadKernel(activeAgent); loadMemStats();},700);
|
|
1917
|
+
}catch(e){ msg.style.color='var(--amber)'; msg.textContent='录入失败:后端未连接'; }
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
|
|
1922
|
+
/* 启动 —— 全部包 try-catch,任一失败不影响其他;还失败的也在 upStat 显示具体错 */
|
|
1923
|
+
const _bootTry=(fn,name)=>{ try{ const r=fn(); if(r&&r.catch) r.catch(e=>{ try{document.getElementById('upStat').textContent='启动失败:'+name}catch(_){ } }); }
|
|
1924
|
+
catch(e){ try{document.getElementById('upStat').textContent='启动失败:'+name}catch(_){ } } };
|
|
1925
|
+
_bootTry(fetchAMapConfig, 'fetchAMapConfig');
|
|
1926
|
+
_bootTry(loadStatus, 'loadStatus');
|
|
1927
|
+
_bootTry(loadMemStats, 'loadMemStats');
|
|
1928
|
+
_bootTry(loadKnowList, 'loadKnowList');
|
|
1929
|
+
_bootTry(()=>connectWS(), 'connectWS');
|
|
1930
|
+
_bootTry(()=>loadKernel('supervisor'), 'loadKernel');
|
|
1931
|
+
try{ setInterval(loadStatus,15000); }catch(e){}
|
|
1932
|
+
try{ setInterval(loadMemStats,20000); }catch(e){}
|
|
1933
|
+
try{ setInterval(loadKnowList,30000); }catch(e){}
|
|
1934
|
+
// 右栏面板标题的 "›" → 打开对应中央大屏
|
|
1935
|
+
document.getElementById('memMore')?.addEventListener('click',()=>showModuleInCore('memory'));
|
|
1936
|
+
document.getElementById('knowMore')?.addEventListener('click',()=>showKnowledgeFull());
|
|
1937
|
+
// 知识摄入入口:复用底部指令栏里那三个隐藏的原有控件(逻辑不重复实现)
|
|
1938
|
+
document.getElementById('ingFile')?.addEventListener('click',()=>document.getElementById('cmdAttach')?.click());
|
|
1939
|
+
document.getElementById('ingFolder')?.addEventListener('click',()=>document.getElementById('cmdFolder')?.click());
|
|
1940
|
+
document.getElementById('ingNote')?.addEventListener('click',()=>{ try{ openEntry('office'); }catch(e){} });
|
|
1941
|
+
// 会话历史:只渲染列表,**不预建空会话**(否则每次刷新都堆一条 0 条的空壳);
|
|
1942
|
+
// 真正建会话推迟到用户发第一条消息时(transmitStream 里 ensureSession)。
|
|
1943
|
+
_bootTry(loadSessions, 'loadSessions');
|
|
1944
|
+
document.getElementById('sessNew')?.addEventListener('click',()=>newSession(true));
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
/* 内联 onclick 桥接:模块作用域下,HTML 里 onclick="fn()" 需从 window 解析 */
|
|
1949
|
+
Object.assign(window, { openEntry, importDomainFile, switchProvider });
|
|
1950
|
+
|