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,1637 @@
|
|
|
1
|
+
// ════════════════════════════════════════════════════════
|
|
2
|
+
// HKC UI — 数据层:演示数据 + API 客户端(与 hkc_sdk 对齐)
|
|
3
|
+
// ════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
const KU_COLORS = {
|
|
6
|
+
Entity: '#002FA7', // 克莱因蓝
|
|
7
|
+
Concept: '#2c9678', // 绿
|
|
8
|
+
Fact: '#5a78cc', // 蓝浅
|
|
9
|
+
Claim: '#6bbfa6', // 绿浅
|
|
10
|
+
Evidence: '#001f70', // 蓝深
|
|
11
|
+
Ability: '#1f6e58', // 绿深
|
|
12
|
+
};
|
|
13
|
+
const KU_LABEL = {
|
|
14
|
+
Entity:'实体', Concept:'概念', Fact:'事实',
|
|
15
|
+
Claim:'观点', Evidence:'证据', Ability:'能力'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ── 演示数据:一个投资领域的小知识图谱 ──
|
|
19
|
+
const DEMO = {
|
|
20
|
+
kus: {
|
|
21
|
+
ENT_01:{ku_id:'ENT_01',ku_type:'Entity',name:'Charlie Munger',summary:'伯克希尔·哈撒韦副董事长,价值投资与多元思维模型的代表人物。',domain:'Investment',confidence:1.0,status:'active',tags:['投资','心理学'],extra:{entity_type:'Person',aliases:['查理·芒格']}},
|
|
22
|
+
ENT_02:{ku_id:'ENT_02',ku_type:'Entity',name:'Warren Buffett',summary:'伯克希尔·哈撒韦董事长,被誉为奥马哈先知。',domain:'Investment',confidence:1.0,status:'active',tags:['投资'],extra:{entity_type:'Person',aliases:['巴菲特']}},
|
|
23
|
+
ENT_03:{ku_id:'ENT_03',ku_type:'Entity',name:'Benjamin Graham',summary:'价值投资之父,《证券分析》《聪明的投资者》作者。',domain:'Investment',confidence:1.0,status:'active',tags:['投资'],extra:{entity_type:'Person',aliases:['格雷厄姆']}},
|
|
24
|
+
CON_01:{ku_id:'CON_01',ku_type:'Concept',name:'价值投资',summary:'以低于内在价值的价格买入优质资产,长期持有。',domain:'Investment',confidence:1.0,status:'active',tags:['方法论'],extra:{definition:'寻找市场价格低于内在价值的证券'}},
|
|
25
|
+
CON_02:{ku_id:'CON_02',ku_type:'Concept',name:'多元思维模型',summary:'用多学科的核心模型交叉分析问题,避免单一视角偏差。',domain:'Investment',confidence:1.0,status:'active',tags:['思维'],extra:{definition:'Mental Models'}},
|
|
26
|
+
CON_03:{ku_id:'CON_03',ku_type:'Concept',name:'安全边际',summary:'买入价与内在价值之间的缓冲,价值投资的核心防御机制。',domain:'Investment',confidence:1.0,status:'active',tags:['估值'],extra:{definition:'Margin of Safety'}},
|
|
27
|
+
CON_04:{ku_id:'CON_04',ku_type:'Concept',name:'护城河',summary:'企业可持续的竞争优势,如品牌、网络效应、转换成本。',domain:'Investment',confidence:1.0,status:'active',tags:['竞争优势'],extra:{definition:'Economic Moat'}},
|
|
28
|
+
CLM_01:{ku_id:'CLM_01',ku_type:'Claim',name:'长期价值投资优于短线',summary:'',domain:'Investment',confidence:0.88,status:'active',tags:[],extra:{statement:'长期价值投资的回报显著优于短线交易',claim_status:'active',supports:['EVD_01','EVD_02']}},
|
|
29
|
+
CLM_02:{ku_id:'CLM_02',ku_type:'Claim',name:'集中投资优于分散',summary:'',domain:'Investment',confidence:0.71,status:'active',tags:[],extra:{statement:'对深度理解的少数标的集中投资优于广泛分散',claim_status:'disputed',supports:['EVD_02']}},
|
|
30
|
+
CLM_03:{ku_id:'CLM_03',ku_type:'Claim',name:'广泛分散降低风险',summary:'',domain:'Investment',confidence:0.69,status:'active',tags:[],extra:{statement:'广泛分散持仓是降低非系统性风险的最优策略',claim_status:'disputed',supports:['EVD_03']}},
|
|
31
|
+
EVD_01:{ku_id:'EVD_01',ku_type:'Evidence',name:'穷查理宝典',summary:'记录芒格投资哲学与思维方法的文集。',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{source:'Poor Charlie\'s Almanack',author:'Charlie Munger',year:2005,page:127}},
|
|
32
|
+
EVD_02:{ku_id:'EVD_02',ku_type:'Evidence',name:'巴菲特致股东信',summary:'1965年至今的伯克希尔年度股东信合集。',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{source:'Berkshire Shareholder Letters',author:'Warren Buffett',year:2023}},
|
|
33
|
+
EVD_03:{ku_id:'EVD_03',ku_type:'Evidence',name:'现代投资组合理论',summary:'马科维茨提出的均值-方差分散化框架。',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{source:'Portfolio Selection',author:'Harry Markowitz',year:1952}},
|
|
34
|
+
ABL_01:{ku_id:'ABL_01',ku_type:'Ability',name:'价值投资者',summary:'',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{ability_key:'value_investor'}},
|
|
35
|
+
},
|
|
36
|
+
relations:[
|
|
37
|
+
{from:'ENT_01',to:'CON_02',type:'CREATED_BY'},
|
|
38
|
+
{from:'ENT_01',to:'CON_01',type:'USES'},
|
|
39
|
+
{from:'ENT_02',to:'CON_01',type:'USES'},
|
|
40
|
+
{from:'ENT_03',to:'CON_01',type:'CREATED_BY'},
|
|
41
|
+
{from:'ENT_03',to:'CON_03',type:'CREATED_BY'},
|
|
42
|
+
{from:'CON_01',to:'CON_03',type:'DERIVED_FROM'},
|
|
43
|
+
{from:'CON_01',to:'CON_04',type:'DERIVED_FROM'},
|
|
44
|
+
{from:'EVD_01',to:'CLM_01',type:'SUPPORTS'},
|
|
45
|
+
{from:'EVD_02',to:'CLM_01',type:'SUPPORTS'},
|
|
46
|
+
{from:'EVD_02',to:'CLM_02',type:'SUPPORTS'},
|
|
47
|
+
{from:'EVD_03',to:'CLM_03',type:'SUPPORTS'},
|
|
48
|
+
{from:'CLM_02',to:'CLM_03',type:'CONTRADICTS'},
|
|
49
|
+
{from:'ABL_01',to:'CON_01',type:'USES'},
|
|
50
|
+
{from:'EVD_01',to:'ENT_01',type:'MENTIONS'},
|
|
51
|
+
],
|
|
52
|
+
abilities:[
|
|
53
|
+
{ability_key:'value_investor',display_name:'价值投资者',domain:'Investment',
|
|
54
|
+
coverage:{fundamental_analysis:0.82,valuation:0.78,moat_analysis:0.71,financial_statement:0.55},
|
|
55
|
+
can_compile:true,compiled:true},
|
|
56
|
+
{ability_key:'quant_analyst',display_name:'量化分析师',domain:'Investment',
|
|
57
|
+
coverage:{factor_investing:0.45,backtesting:0.30,risk_management:0.62,portfolio_opt:0.51,data_analysis:0.20},
|
|
58
|
+
can_compile:false,missing:['factor_investing','backtesting','data_analysis']},
|
|
59
|
+
{ability_key:'psychology_expert',display_name:'心理学专家',domain:'Psychology',
|
|
60
|
+
coverage:{cognitive_bias:0.68,behavioral_psychology:0.40,social_influence:0.33,decision_making:0.71},
|
|
61
|
+
can_compile:false,missing:['behavioral_psychology','social_influence']},
|
|
62
|
+
],
|
|
63
|
+
conflicts:[
|
|
64
|
+
{conflict_id:'CFT_01',claim_a_id:'CLM_02',claim_b_id:'CLM_03',domain:'Investment',
|
|
65
|
+
status:'open',resolution_strategy:'evidence_weight',resolution_note:''},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ── API 客户端:连真实 hkc-api,失败回退演示 ──
|
|
70
|
+
class HKCClient{
|
|
71
|
+
constructor(){ this.base=null; this.demo=true; this.apiKey=''; }
|
|
72
|
+
async connect(url){
|
|
73
|
+
try{
|
|
74
|
+
const r=await fetch(url.replace(/\/$/,'')+'/health',{signal:AbortSignal.timeout(4000)});
|
|
75
|
+
if(!r.ok) throw 0;
|
|
76
|
+
this.base=url.replace(/\/$/,''); this.demo=false; return true;
|
|
77
|
+
}catch(e){ this.demo=true; this.base=null; return false; }
|
|
78
|
+
}
|
|
79
|
+
useDemo(){ this.demo=true; this.base=null; }
|
|
80
|
+
// 后端若设了 HKC_API_KEY,前端在此带上 X-API-Key;未配置则为空、不影响开放部署。
|
|
81
|
+
_h(extra){ const h=Object.assign({}, extra||{}); if(this.apiKey) h['X-API-Key']=this.apiKey; return h; }
|
|
82
|
+
async _get(p){ const r=await fetch(this.base+p,{headers:this._h()}); if(!r.ok) throw new Error(r.status); return r.json(); }
|
|
83
|
+
async _post(p,b){ const r=await fetch(this.base+p,{method:'POST',headers:this._h({'Content-Type':'application/json'}),body:JSON.stringify(b)});
|
|
84
|
+
if(!r.ok){ const e=await r.json().catch(()=>({})); throw {status:r.status,detail:e.detail||e.error||e.message}; } return r.json(); }
|
|
85
|
+
async _postForm(p,fd){ const r=await fetch(this.base+p,{method:'POST',headers:this._h(),body:fd}); // 不设 Content-Type,浏览器自动带 multipart boundary
|
|
86
|
+
if(!r.ok){ const e=await r.json().catch(()=>({})); throw {status:r.status,detail:e.detail||e.error||e.message}; } return r.json(); }
|
|
87
|
+
}
|
|
88
|
+
const client=new HKCClient();
|
|
89
|
+
|
|
90
|
+
// ════════════════════════════════════════════════════════
|
|
91
|
+
// 状态 + 工具
|
|
92
|
+
// ════════════════════════════════════════════════════════
|
|
93
|
+
const state={ kus:{}, relations:[], abilities:[], conflicts:[], selected:null, mode:'hybrid', conflictPick:{}, synth:{}, pathSel:{} };
|
|
94
|
+
const $=s=>document.querySelector(s);
|
|
95
|
+
const $$=s=>document.querySelectorAll(s);
|
|
96
|
+
|
|
97
|
+
function toast(msg,kind=''){
|
|
98
|
+
const t=document.createElement('div');
|
|
99
|
+
t.className='toast '+kind;
|
|
100
|
+
t.innerHTML=(kind==='ok'?'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6 9 17l-5-5"/></svg>':'')+`<span>${msg}</span>`;
|
|
101
|
+
$('#toast-wrap').appendChild(t);
|
|
102
|
+
setTimeout(()=>{t.style.opacity='0';t.style.transition='opacity .3s';setTimeout(()=>t.remove(),300)},2600);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── 载入数据(演示或真实)──
|
|
106
|
+
async function loadData(){
|
|
107
|
+
if(client.demo){
|
|
108
|
+
state.kus=JSON.parse(JSON.stringify(DEMO.kus));
|
|
109
|
+
state.relations=DEMO.relations.map(r=>({...r}));
|
|
110
|
+
state.abilities=DEMO.abilities.map(a=>({...a}));
|
|
111
|
+
state.conflicts=DEMO.conflicts.map(c=>({...c}));
|
|
112
|
+
}else{
|
|
113
|
+
// 真实模式:一次拉取全量知识图谱(所有 KU + 关系),不再写死领域
|
|
114
|
+
try{
|
|
115
|
+
const g=await client._get('/knowledge/graph?limit=5000'); // 拉到后端硬上限(默认仅 2000)
|
|
116
|
+
// 真实库为空 → 本次用演示数据渲染(星球不空白);**不翻 client.demo**,连接保留、摄入仍走真实后端,
|
|
117
|
+
// 摄入知识后刷新即自动显示真实星球。修「嵌入 Foxclaw 空库时星球一片空白」。
|
|
118
|
+
if(!(g.kus && g.kus.length)){
|
|
119
|
+
toast('知识库为空,先展示演示星球;摄入知识后自动显示真实数据','');
|
|
120
|
+
state.kus=JSON.parse(JSON.stringify(DEMO.kus));
|
|
121
|
+
state.relations=DEMO.relations.map(r=>({...r}));
|
|
122
|
+
state.abilities=DEMO.abilities.map(a=>({...a}));
|
|
123
|
+
state.conflicts=DEMO.conflicts.map(c=>({...c}));
|
|
124
|
+
renderAll(); return;
|
|
125
|
+
}
|
|
126
|
+
state.kus={};
|
|
127
|
+
(g.kus||[]).forEach(k=>{ state.kus[k.ku_id]=k; });
|
|
128
|
+
state.relations=(g.relations||[]).map(r=>({
|
|
129
|
+
from:r.from_ku, to:r.to_ku, type:r.rel_type||'RELATED',
|
|
130
|
+
}));
|
|
131
|
+
const ab=await client._get('/abilities').catch(()=>({abilities:[]}));
|
|
132
|
+
state.abilities=[];
|
|
133
|
+
for(const key of (ab.abilities||[])){
|
|
134
|
+
const rep=await client._get(`/abilities/${key}/coverage`).catch(()=>null);
|
|
135
|
+
if(rep) state.abilities.push({ability_key:key,display_name:rep.display_name||key,
|
|
136
|
+
domain:(rep.domains||[]).join(' / '),
|
|
137
|
+
coverage:rep.coverage,can_compile:rep.can_compile,missing:rep.missing_skills});
|
|
138
|
+
}
|
|
139
|
+
// 冲突
|
|
140
|
+
const cf=await client._get('/conflicts?status=open').catch(()=>({conflicts:[]}));
|
|
141
|
+
state.conflicts=cf.conflicts||[];
|
|
142
|
+
// 数据可见性安全网:拉 /stats 拿真实 data_dir + 全库 KU 总数,
|
|
143
|
+
// 显示到连接胶囊(hover)+ toast,空库时明显告警——防止"连错目录以为数据丢了"。
|
|
144
|
+
updateDataInfo().catch(()=>{});
|
|
145
|
+
}catch(e){ toast('数据加载出错,回退演示模式','err'); client.useDemo(); return loadData(); }
|
|
146
|
+
}
|
|
147
|
+
renderAll();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 拉 /stats,把「后端实际数据目录 + 全库知识总数」暴露给用户。
|
|
151
|
+
// 关键:total_kus 是全库真实数,可能 > 星球显示数(星球有 limit 上限)。
|
|
152
|
+
async function updateDataInfo(){
|
|
153
|
+
const pill=$('#conn-pill');
|
|
154
|
+
const st=await client._get('/stats');
|
|
155
|
+
const dir=st.data_dir||'(未知)';
|
|
156
|
+
const total=st.total_kus||0; // 全库真实总数
|
|
157
|
+
const emb=st.embedding||''; // 生效的向量后端
|
|
158
|
+
const shown=Object.keys(state.kus).length; // 星球实际渲染数(受 limit 限制)
|
|
159
|
+
const stubEmb=/stub/i.test(emb);
|
|
160
|
+
if(pill){
|
|
161
|
+
let tip=`数据目录:${dir}\n全库知识:${total} 个`;
|
|
162
|
+
if(shown<total) tip+=`\n星球显示:${shown} 个(受上限限制,未全部显示)`;
|
|
163
|
+
if(emb) tip+=`\n向量后端:${emb}${stubEmb?'(无真实语义,语义搜索受限;设 HKC_EMBEDDING=local 启用)':''}`;
|
|
164
|
+
pill.title=tip;
|
|
165
|
+
}
|
|
166
|
+
if(total===0){
|
|
167
|
+
toast(`⚠️ 知识库为空!后端数据目录:${dir}\n若你之前上传过书,很可能是后端连错了目录`,'err');
|
|
168
|
+
}else{
|
|
169
|
+
let msg=`已连接 · 全库 ${total} 个知识`;
|
|
170
|
+
if(shown<total) msg+=` · 星球显示 ${shown} 个(达上限)`;
|
|
171
|
+
toast(msg,'ok');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderAll(){ renderGraph(); renderPanels(); }
|
|
176
|
+
function renderPanels(){
|
|
177
|
+
renderAbilities(); renderConflicts(); renderLegend();
|
|
178
|
+
$('#ability-count').textContent=state.abilities.length+' 项';
|
|
179
|
+
const oc=state.conflicts.filter(c=>c.status==='open').length;
|
|
180
|
+
$('#conflict-count').textContent=oc;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ════════════════════════════════════════════════════════
|
|
184
|
+
// G6 知识图谱(本地 vendor/g6.min.js,零网络)
|
|
185
|
+
// ════════════════════════════════════════════════════════
|
|
186
|
+
// ════════════════════════════════════════════════════════
|
|
187
|
+
// 知识星球 3D 引擎(Three.js,本地 vendor,零网络)
|
|
188
|
+
// 节点 = 3D 球体浮于空间,语义引力聚拢,缓慢有机漂浮
|
|
189
|
+
// ════════════════════════════════════════════════════════
|
|
190
|
+
let THREE = null; // 动态加载的 Three.js 命名空间
|
|
191
|
+
let planet = null; // 当前星球实例
|
|
192
|
+
|
|
193
|
+
function disputedOf(k){ return k.extra && k.extra.claim_status==='disputed'; }
|
|
194
|
+
|
|
195
|
+
// 节点半径(3D 世界单位)
|
|
196
|
+
function nodeRadius(k){
|
|
197
|
+
if(k.ku_type==='Ability') return 7;
|
|
198
|
+
if(k.ku_type==='Entity'||k.ku_type==='Concept') return 6;
|
|
199
|
+
if(k.ku_type==='Claim') return 3.5 + (k.confidence||0.5)*3;
|
|
200
|
+
return 5;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── 星球布局策略(伸缩的统一接入点)────────────────────────────
|
|
204
|
+
// 所有"节点/标签太多放不下怎么办"的逻辑都集中在此,后续扩展只改这个对象,
|
|
205
|
+
// 不动其它渲染代码:
|
|
206
|
+
// 已实现 · 单层 Fibonacci 球面 + 圆点随节点总数自适应缩小(dotScale)。
|
|
207
|
+
// 预留接入点 ↓
|
|
208
|
+
// · shells>1 → 分层多壳星球(外壳放不下→往里再套一层球);position() 已按
|
|
209
|
+
// shell 分配、内层半径递减,只需让 compute() 返回 shells>1 即启用。
|
|
210
|
+
// · labelScale → 书名标签随书数自适应缩小(_makeLabel 已乘该系数)。
|
|
211
|
+
// · SHELL_CAPACITY→ 单层舒适容量阈值,决定何时缩点 / 何时该分层。
|
|
212
|
+
const PLANET_LAYOUT = {
|
|
213
|
+
SHELL_CAPACITY: 800, // 单层舒适容量;超过开始缩点,(未来)超过可分层
|
|
214
|
+
BASE_R_MIN: 160, BASE_R_MAX: 320,
|
|
215
|
+
|
|
216
|
+
// 按节点总数计算布局参数(集中调参)
|
|
217
|
+
compute(n){
|
|
218
|
+
const radius = Math.max(this.BASE_R_MIN, Math.min(this.BASE_R_MAX, 90 + n*11));
|
|
219
|
+
const dotScale = Math.max(0.45, Math.min(1, Math.sqrt(this.SHELL_CAPACITY / Math.max(1, n))));
|
|
220
|
+
const shells = 1; // 预留:未来 Math.ceil(n/SHELL_CAPACITY) 启用分层
|
|
221
|
+
const labelScale = 1; // 预留:未来书多时缩小书名标签
|
|
222
|
+
return { radius, dotScale, shells, labelScale };
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
// 第 i 个节点(共 n 个)的球面坐标。已支持多壳:shells>1 时按层分配、内层半径递减;
|
|
226
|
+
// shells=1 时退化为单层 Fibonacci,布局与旧版完全一致。
|
|
227
|
+
position(i, n, params, T){
|
|
228
|
+
const golden = Math.PI * (3 - Math.sqrt(5));
|
|
229
|
+
const shells = params.shells || 1;
|
|
230
|
+
const per = Math.ceil(n / shells);
|
|
231
|
+
const shell = Math.floor(i / per);
|
|
232
|
+
const localI = i - shell * per;
|
|
233
|
+
const localN = Math.max(1, Math.min(per, n - shell * per));
|
|
234
|
+
const r = params.radius * (1 - shell * 0.32); // 内层半径递减
|
|
235
|
+
const y = 1 - (localI / Math.max(1, localN - 1)) * 2;
|
|
236
|
+
const rad = Math.sqrt(Math.max(0, 1 - y*y));
|
|
237
|
+
const theta = golden * localI;
|
|
238
|
+
return new T.Vector3(Math.cos(theta)*rad*r, y*r, Math.sin(theta)*rad*r);
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// 关系标签
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
// ── 知识星球类 ──
|
|
246
|
+
class KnowledgePlanet {
|
|
247
|
+
constructor(container, THREE){
|
|
248
|
+
this.T = THREE;
|
|
249
|
+
this.container = container;
|
|
250
|
+
this.nodes = []; // {id, ku, mesh, glow, vel, pos, radius, label}
|
|
251
|
+
this.edges = []; // {from, to, line, contradicts}
|
|
252
|
+
this.raf = null;
|
|
253
|
+
this.disposed = false;
|
|
254
|
+
this.hovered = null;
|
|
255
|
+
this.selected = null;
|
|
256
|
+
this._initScene();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_initScene(){
|
|
260
|
+
const T = this.T;
|
|
261
|
+
const w = this.container.clientWidth || 700;
|
|
262
|
+
const h = this.container.clientHeight || 600;
|
|
263
|
+
|
|
264
|
+
this.scene = new T.Scene();
|
|
265
|
+
|
|
266
|
+
// 相机
|
|
267
|
+
this.camera = new T.PerspectiveCamera(55, w/h, 1, 4000);
|
|
268
|
+
this.camera.position.set(0, 0, 720);
|
|
269
|
+
|
|
270
|
+
// 渲染器
|
|
271
|
+
this.renderer = new T.WebGLRenderer({ antialias:true, alpha:true });
|
|
272
|
+
this.renderer.setSize(w, h);
|
|
273
|
+
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio||1, 2));
|
|
274
|
+
this.renderer.setClearColor(0x000000, 0); // 透明,露出 CSS 星空背景
|
|
275
|
+
this.container.appendChild(this.renderer.domElement);
|
|
276
|
+
this.canvas = this.renderer.domElement;
|
|
277
|
+
this.canvas.style.display = 'block';
|
|
278
|
+
|
|
279
|
+
// 灯光:环境光 + 两个点光(冷暖对冲,呼应品牌绿/蓝)
|
|
280
|
+
this.scene.add(new T.AmbientLight(0xffffff, 0.75));
|
|
281
|
+
const l1 = new T.PointLight(0x9fe8d2, 0.9, 0); l1.position.set(200, 180, 300); this.scene.add(l1);
|
|
282
|
+
const l2 = new T.PointLight(0x7d97ff, 0.7, 0); l2.position.set(-220, -120, 160); this.scene.add(l2);
|
|
283
|
+
|
|
284
|
+
// 旋转容器:所有节点挂在这个 group 上,整体缓缓自转
|
|
285
|
+
this.world = new T.Group();
|
|
286
|
+
this.scene.add(this.world);
|
|
287
|
+
|
|
288
|
+
// 轨道控制(自写:拖拽转视角 + 滚轮缩放)
|
|
289
|
+
this._initControls();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 轻量轨道控制
|
|
293
|
+
_initControls(){
|
|
294
|
+
this.rot = { x: 0, y: 0 }; // 目标旋转
|
|
295
|
+
this.rotCur = { x: 0, y: 0 }; // 当前(缓动)
|
|
296
|
+
this.autoSpin = true;
|
|
297
|
+
this.camDist = 720;
|
|
298
|
+
this.camDistTarget = 720;
|
|
299
|
+
let dragging = false, lastX = 0, lastY = 0;
|
|
300
|
+
|
|
301
|
+
const el = this.canvas;
|
|
302
|
+
el.style.cursor = 'grab';
|
|
303
|
+
|
|
304
|
+
this._onDown = (e)=>{ dragging = true; this.dragging = true; lastX = e.clientX; lastY = e.clientY; el.style.cursor='grabbing';
|
|
305
|
+
this._downX = e.clientX; this._downY = e.clientY; // 记录起点,用于区分"拖动"与"点击"
|
|
306
|
+
// 接管当前视觉朝向再停自转,避免从摆动倾斜切到手动时跳变
|
|
307
|
+
this.rot.x = this.rotCur.x; this.rot.y = this.rotCur.y; this.autoSpin = false;
|
|
308
|
+
// 关键:清掉 flyTarget,否则聚焦态每帧把旋转拉回目标节点朝向 → 拖动被牵扯、松手即反弹
|
|
309
|
+
this._flyTarget = null; };
|
|
310
|
+
this._onMove = (e)=>{
|
|
311
|
+
if(!dragging) return;
|
|
312
|
+
const dx = e.clientX - lastX, dy = e.clientY - lastY;
|
|
313
|
+
lastX = e.clientX; lastY = e.clientY;
|
|
314
|
+
this.rot.y += dx * 0.006;
|
|
315
|
+
this.rot.x += dy * 0.006; // 不再钳制俯仰:可自由翻转,把任意节点(含两极)拉到正面
|
|
316
|
+
};
|
|
317
|
+
this._onUp = ()=>{ dragging = false; this.dragging = false; el.style.cursor='grab'; };
|
|
318
|
+
this._onWheel = (e)=>{
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
this.camDistTarget += e.deltaY * 0.5;
|
|
321
|
+
this.camDistTarget = Math.max(250, Math.min(1500, this.camDistTarget));
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
el.addEventListener('pointerdown', this._onDown);
|
|
325
|
+
window.addEventListener('pointermove', this._onMove);
|
|
326
|
+
window.addEventListener('pointerup', this._onUp);
|
|
327
|
+
el.addEventListener('wheel', this._onWheel, { passive:false });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 构建节点与边
|
|
331
|
+
build(kus, relations){
|
|
332
|
+
const T = this.T;
|
|
333
|
+
const ids = Object.keys(kus);
|
|
334
|
+
const n = ids.length;
|
|
335
|
+
if(!n) return;
|
|
336
|
+
|
|
337
|
+
// 稳定打散:后端按类型分组返回 KU,直接按序号布 Fibonacci 会把同类型节点
|
|
338
|
+
// (尤其只有几个的"书"/Evidence)全挤到同一极点、标签叠成一坨。按 id 哈希
|
|
339
|
+
// 稳定排序,让各类型均匀铺满球面(布局跨刷新稳定,不随机跳)。
|
|
340
|
+
// 必须用有雪崩效应的哈希(FNV-1a+混合):id 是 EVD_0000000X 这类共享前缀、
|
|
341
|
+
// 仅末位不同的序列,简单滚动哈希会给相近值 → 同前缀仍聚堆,起不到打散作用。
|
|
342
|
+
const _h = s => {
|
|
343
|
+
let h = 2166136261 >>> 0;
|
|
344
|
+
for(let k=0;k<s.length;k++){ h ^= s.charCodeAt(k); h = Math.imul(h, 16777619); }
|
|
345
|
+
h ^= h >>> 13; h = Math.imul(h, 0x5bd1e995); h ^= h >>> 15;
|
|
346
|
+
return h >>> 0;
|
|
347
|
+
};
|
|
348
|
+
ids.sort((a,b)=> _h(a) - _h(b));
|
|
349
|
+
|
|
350
|
+
// 布局参数(半径/圆点缩放/层数/标签缩放)统一由 PLANET_LAYOUT 计算,便于扩展
|
|
351
|
+
const params = PLANET_LAYOUT.compute(n);
|
|
352
|
+
this._layoutParams = params; // 供 _makeLabel 读取 labelScale 等
|
|
353
|
+
|
|
354
|
+
ids.forEach((id, i)=>{
|
|
355
|
+
const ku = kus[id];
|
|
356
|
+
const radius = nodeRadius(ku) * params.dotScale; // 圆点随节点总数自适应缩小
|
|
357
|
+
const pos = PLANET_LAYOUT.position(i, n, params, T);
|
|
358
|
+
|
|
359
|
+
// 球体
|
|
360
|
+
const color = new T.Color(KU_COLORS[ku.ku_type] || '#888');
|
|
361
|
+
const geo = new T.SphereGeometry(radius, 24, 24);
|
|
362
|
+
const mat = new T.MeshStandardMaterial({
|
|
363
|
+
color, emissive: color.clone().multiplyScalar(0.35),
|
|
364
|
+
roughness: 0.45, metalness: 0.15,
|
|
365
|
+
});
|
|
366
|
+
const mesh = new T.Mesh(geo, mat);
|
|
367
|
+
mesh.position.copy(pos);
|
|
368
|
+
mesh.userData.id = id;
|
|
369
|
+
this.world.add(mesh);
|
|
370
|
+
|
|
371
|
+
// 外发光(稍大的半透明球)
|
|
372
|
+
const glowMat = new T.MeshBasicMaterial({ color, transparent:true, opacity:0.12, side:T.BackSide });
|
|
373
|
+
const glow = new T.Mesh(new T.SphereGeometry(radius*1.6, 20, 20), glowMat);
|
|
374
|
+
mesh.add(glow);
|
|
375
|
+
|
|
376
|
+
// 冲突节点:暗红描边光环
|
|
377
|
+
if(disputedOf(ku)){
|
|
378
|
+
const ring = new T.Mesh(
|
|
379
|
+
new T.SphereGeometry(radius*1.35, 20, 20),
|
|
380
|
+
new T.MeshBasicMaterial({ color:0xb0413e, transparent:true, opacity:0.3, side:T.BackSide })
|
|
381
|
+
);
|
|
382
|
+
mesh.add(ring);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this.nodes.push({
|
|
386
|
+
id, ku, mesh, glow, radius,
|
|
387
|
+
pos: pos.clone(),
|
|
388
|
+
vel: new T.Vector3(),
|
|
389
|
+
basePos: pos.clone(),
|
|
390
|
+
baseColor: color.clone(), // 复用,避免每帧 new T.Color
|
|
391
|
+
phase: Math.random()*Math.PI*2, // 漂浮相位
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// 文字标签(Sprite,始终面向相机)
|
|
395
|
+
const label = this._makeLabel(ku.name);
|
|
396
|
+
label.position.set(0, -(radius+9), 0);
|
|
397
|
+
mesh.add(label);
|
|
398
|
+
const _nd = this.nodes[this.nodes.length-1];
|
|
399
|
+
_nd.label = label;
|
|
400
|
+
// 分级标签(按重要度钻取):
|
|
401
|
+
// 一级 = 书/来源(Evidence),默认在球面常显书名;
|
|
402
|
+
// 二级/三级 = 点击后按连接数(degree)展开该节点 top-N 重要邻居标签。
|
|
403
|
+
// 这样几十上百本默认只见几十个书名,清爽且可层层下钻。
|
|
404
|
+
_nd.isBook = (ku.ku_type === 'Evidence');
|
|
405
|
+
_nd.deg = 0; // 连接数,建边后回填(作重要度)
|
|
406
|
+
_nd._revealLabel = false;
|
|
407
|
+
label.visible = _nd.isBook;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// 边(连线)
|
|
411
|
+
const nodeById = {};
|
|
412
|
+
this.nodes.forEach(nd=> nodeById[nd.id] = nd);
|
|
413
|
+
(relations||[]).forEach(r=>{
|
|
414
|
+
const a = nodeById[r.from], b = nodeById[r.to];
|
|
415
|
+
if(!a || !b) return;
|
|
416
|
+
const contradicts = r.type === 'CONTRADICTS';
|
|
417
|
+
const mat = new T.LineBasicMaterial({
|
|
418
|
+
color: contradicts ? 0xb0413e : 0x9aa3b8,
|
|
419
|
+
transparent:true, opacity: contradicts ? 0.55 : 0.22,
|
|
420
|
+
});
|
|
421
|
+
const geo = new T.BufferGeometry().setFromPoints([a.pos.clone(), b.pos.clone()]);
|
|
422
|
+
const line = new T.Line(geo, mat);
|
|
423
|
+
line.visible = false; // 默认不画边(大图会糊成一团);点中节点才显示它的连线
|
|
424
|
+
this.world.add(line);
|
|
425
|
+
this.edges.push({ from:a, to:b, line, contradicts });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// 连接数(degree)=重要度,用于钻取时挑 top-N 邻居标签
|
|
429
|
+
this.edges.forEach(e=>{ e.from.deg++; e.to.deg++; });
|
|
430
|
+
|
|
431
|
+
this._buildPicking();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 射线拾取(hover / click)
|
|
435
|
+
_buildPicking(){
|
|
436
|
+
const T = this.T;
|
|
437
|
+
this.raycaster = new T.Raycaster();
|
|
438
|
+
this.pointer = new T.Vector2();
|
|
439
|
+
this._meshes = this.nodes.map(n=>n.mesh);
|
|
440
|
+
|
|
441
|
+
this._onPointerMove = (e)=>{
|
|
442
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
443
|
+
this.pointer.x = ((e.clientX - rect.left)/rect.width)*2 - 1;
|
|
444
|
+
this.pointer.y = -((e.clientY - rect.top)/rect.height)*2 + 1;
|
|
445
|
+
};
|
|
446
|
+
this._onClick = (e)=>{
|
|
447
|
+
// 拖动松手也会带出一次 click;若指针移动过阈值,判定为拖动而非点击 → 忽略,
|
|
448
|
+
// 否则空白处会 clearSelection 重开自转、有节点处会 selectKU 飞入,都表现为"反弹"。
|
|
449
|
+
const moved = Math.hypot(e.clientX - (this._downX||0), e.clientY - (this._downY||0));
|
|
450
|
+
if(moved > 6) return;
|
|
451
|
+
const hit = this._pick();
|
|
452
|
+
if(hit){ if(window.selectKU) window.selectKU(hit.id); }
|
|
453
|
+
else { if(window.clearSelection) window.clearSelection(); }
|
|
454
|
+
};
|
|
455
|
+
this.canvas.addEventListener('pointermove', this._onPointerMove);
|
|
456
|
+
this.canvas.addEventListener('click', this._onClick);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_pick(){
|
|
460
|
+
if(!this.raycaster) return null;
|
|
461
|
+
this.raycaster.setFromCamera(this.pointer, this.camera);
|
|
462
|
+
const hits = this.raycaster.intersectObjects(this._meshes, false);
|
|
463
|
+
if(hits.length){
|
|
464
|
+
const id = hits[0].object.userData.id;
|
|
465
|
+
return this.nodes.find(n=>n.id===id);
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 动画循环
|
|
471
|
+
start(){
|
|
472
|
+
const T = this.T;
|
|
473
|
+
const tick = ()=>{
|
|
474
|
+
if(this.disposed) return;
|
|
475
|
+
this.raf = requestAnimationFrame(tick);
|
|
476
|
+
this._update();
|
|
477
|
+
this.renderer.render(this.scene, this.camera);
|
|
478
|
+
};
|
|
479
|
+
tick();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
_update(){
|
|
483
|
+
const T = this.T;
|
|
484
|
+
const t = performance.now() * 0.001;
|
|
485
|
+
|
|
486
|
+
// 自动自转(绕 Y 轴)——静默时缓慢转动(约 100s/圈)
|
|
487
|
+
if(this.autoSpin) this.rot.y += 0.0011;
|
|
488
|
+
|
|
489
|
+
// 旋转缓动;自转时让倾斜角缓慢大幅度上下摆动(±~77°),使两极也周期性
|
|
490
|
+
// 转到正面,避免单轴自转把两极节点永久"沉"在顶/底而无法点击
|
|
491
|
+
// (即用户要的"无固定方向转动、无重力沉淀")。
|
|
492
|
+
const tiltX = this.autoSpin ? Math.sin(t*0.045)*1.35 : this.rot.x;
|
|
493
|
+
// 拖动时近乎 1:1 跟手,消除"橡皮筋/反馈力牵扯"感;静默时才平滑缓动。
|
|
494
|
+
const rotEase = this.dragging ? 0.85 : 0.08;
|
|
495
|
+
this.rotCur.x += (tiltX - this.rotCur.x) * rotEase;
|
|
496
|
+
this.rotCur.y += (this.rot.y - this.rotCur.y) * rotEase;
|
|
497
|
+
this.world.rotation.x = this.rotCur.x;
|
|
498
|
+
this.world.rotation.y = this.rotCur.y;
|
|
499
|
+
|
|
500
|
+
// 相机距离缓动
|
|
501
|
+
this.camDist += (this.camDistTarget - this.camDist) * 0.1;
|
|
502
|
+
this.camera.position.z = this.camDist;
|
|
503
|
+
|
|
504
|
+
// 飞入:把选中节点缓缓转到正面中心
|
|
505
|
+
if(this._flyTarget){
|
|
506
|
+
const nd = this._flyTarget;
|
|
507
|
+
// 期望旋转:让节点world坐标朝向 +Z(正对相机)
|
|
508
|
+
const p = nd.pos;
|
|
509
|
+
const targetRotY = -Math.atan2(p.x, p.z);
|
|
510
|
+
const targetRotX = Math.atan2(p.y, Math.sqrt(p.x*p.x+p.z*p.z));
|
|
511
|
+
this.rot.y = targetRotY;
|
|
512
|
+
this.rot.x = targetRotX; // 不钳制:点击两极节点也能完整转到正面中心
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── 球面分布 + 聚焦汇聚 ──
|
|
516
|
+
// 正常态:节点固定在球面 + 轻微呼吸,整体自转
|
|
517
|
+
// 聚焦态:点击某节点,相关节点汇聚到它周围并发光,无关节点淡出退远
|
|
518
|
+
const nodes = this.nodes;
|
|
519
|
+
nodes.forEach(nd=>{
|
|
520
|
+
// 目标位置:有 focusPos(聚焦汇聚)用它,否则回球面 basePos
|
|
521
|
+
let tx, ty, tz;
|
|
522
|
+
if(nd.focusPos){
|
|
523
|
+
tx = nd.focusPos.x; ty = nd.focusPos.y; tz = nd.focusPos.z;
|
|
524
|
+
}else{
|
|
525
|
+
const bx = Math.sin(t*0.5 + nd.phase)*1.0;
|
|
526
|
+
const by = Math.cos(t*0.4 + nd.phase)*1.0;
|
|
527
|
+
tx = nd.basePos.x + bx; ty = nd.basePos.y + by; tz = nd.basePos.z;
|
|
528
|
+
}
|
|
529
|
+
// 平滑移动到目标(汇聚/回归动画)
|
|
530
|
+
const cur = nd.mesh.position;
|
|
531
|
+
const ease = nd.focusPos ? 0.10 : 0.06;
|
|
532
|
+
cur.x += (tx - cur.x)*ease;
|
|
533
|
+
cur.y += (ty - cur.y)*ease;
|
|
534
|
+
cur.z += (tz - cur.z)*ease;
|
|
535
|
+
nd.pos.copy(cur);
|
|
536
|
+
|
|
537
|
+
// 发光过渡:聚焦的相关节点发光增强,淡出的减弱
|
|
538
|
+
const targetEmissive = nd.glowOn ? 1.0 : 0.35;
|
|
539
|
+
nd._emi = (nd._emi===undefined?0.35:nd._emi) + (targetEmissive - (nd._emi||0.35))*0.12;
|
|
540
|
+
if(nd.mesh.material && nd.mesh.material.emissive && nd.baseColor){
|
|
541
|
+
nd.mesh.material.emissive.copy(nd.baseColor).multiplyScalar(nd._emi);
|
|
542
|
+
}
|
|
543
|
+
// 淡出:无关节点透明度降低
|
|
544
|
+
const targetOpacity = (nd.dim ? 0.18 : 1.0);
|
|
545
|
+
nd._op = (nd._op===undefined?1:nd._op) + (targetOpacity - (nd._op||1))*0.12;
|
|
546
|
+
nd.mesh.material.opacity = nd._op;
|
|
547
|
+
nd.mesh.material.transparent = nd._op < 0.99;
|
|
548
|
+
// 标签分级可见性:
|
|
549
|
+
// - 聚焦态(选中了某节点):只显示发光簇(glowOn)+悬停的标签,淡出的隐藏
|
|
550
|
+
// - 默认态:只显示常显类型(Entity/Concept)+悬停的标签
|
|
551
|
+
if(nd.label){
|
|
552
|
+
const forced = (nd===this.hovered || nd.id===this.selected);
|
|
553
|
+
// 分级:钻取态只显示当前选中节点点亮的 top-N 邻居(二级/三级);
|
|
554
|
+
// 默认态只显示一级书名(Evidence);悬停/选中强制显示。
|
|
555
|
+
// 分级后可见标签本就稀疏(默认≈书数、钻取≈7),不做背面剔除,
|
|
556
|
+
// 否则仅有的几个书名会在球背面时被剔空,出现"无标签"空窗。
|
|
557
|
+
const show = forced || (this.selected ? nd._revealLabel : nd.isBook);
|
|
558
|
+
nd.label.visible = show;
|
|
559
|
+
if(show) nd.label.material.opacity = nd._op;
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// 更新边的端点(只更新可见的边,省性能)
|
|
564
|
+
this.edges.forEach(e=>{
|
|
565
|
+
if(!e.line.visible) return;
|
|
566
|
+
const arr = e.line.geometry.attributes.position.array;
|
|
567
|
+
arr[0]=e.from.pos.x; arr[1]=e.from.pos.y; arr[2]=e.from.pos.z;
|
|
568
|
+
arr[3]=e.to.pos.x; arr[4]=e.to.pos.y; arr[5]=e.to.pos.z;
|
|
569
|
+
e.line.geometry.attributes.position.needsUpdate = true;
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// hover 检测 + 放大(用 scale,不改几何,不抖)
|
|
573
|
+
const hit = this._pick();
|
|
574
|
+
if(hit !== this.hovered){
|
|
575
|
+
if(this.hovered) this._setHover(this.hovered, false);
|
|
576
|
+
if(hit) this._setHover(hit, true);
|
|
577
|
+
this.hovered = hit;
|
|
578
|
+
this.canvas.style.cursor = hit ? 'pointer' : 'grab';
|
|
579
|
+
}
|
|
580
|
+
// 平滑 scale
|
|
581
|
+
this.nodes.forEach(nd=>{
|
|
582
|
+
const target = (nd===this.hovered || nd.id===this.selected) ? 1.5 : 1.0;
|
|
583
|
+
const s = nd.mesh.scale.x + (target - nd.mesh.scale.x)*0.18;
|
|
584
|
+
nd.mesh.scale.set(s,s,s);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
_setHover(nd, on){ /* 状态切换占位,scale 在 _update 平滑处理 */ }
|
|
589
|
+
|
|
590
|
+
// 生成文字标签 Sprite
|
|
591
|
+
_makeLabel(text){
|
|
592
|
+
const T = this.T;
|
|
593
|
+
const label = (text||'').length>12 ? text.slice(0,11)+'…' : (text||'');
|
|
594
|
+
const cv = document.createElement('canvas');
|
|
595
|
+
const ctx = cv.getContext('2d');
|
|
596
|
+
const fs = 48;
|
|
597
|
+
ctx.font = `600 ${fs}px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif`;
|
|
598
|
+
const w = Math.ceil(ctx.measureText(label).width) + 24;
|
|
599
|
+
cv.width = w; cv.height = fs+24;
|
|
600
|
+
// 重设 font(canvas resize 后失效)
|
|
601
|
+
ctx.font = `600 ${fs}px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif`;
|
|
602
|
+
ctx.textAlign='center'; ctx.textBaseline='middle';
|
|
603
|
+
// 半透明深色描边底,深空背景上清晰
|
|
604
|
+
ctx.fillStyle='rgba(255,255,255,.92)';
|
|
605
|
+
ctx.shadowColor='rgba(0,0,0,.85)'; ctx.shadowBlur=8;
|
|
606
|
+
ctx.fillText(label, cv.width/2, cv.height/2);
|
|
607
|
+
const tex = new T.CanvasTexture(cv);
|
|
608
|
+
tex.minFilter = T.LinearFilter;
|
|
609
|
+
const mat = new T.SpriteMaterial({ map:tex, transparent:true, depthWrite:false });
|
|
610
|
+
const sp = new T.Sprite(mat);
|
|
611
|
+
const scale = 0.22 * (this._layoutParams ? this._layoutParams.labelScale : 1); // 标签自适应缩放接入点
|
|
612
|
+
sp.scale.set(cv.width*scale, cv.height*scale, 1);
|
|
613
|
+
return sp;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 相机平滑飞到某节点
|
|
617
|
+
flyTo(id){
|
|
618
|
+
const nd = this.nodes.find(n=>n.id===id);
|
|
619
|
+
if(!nd) return;
|
|
620
|
+
this.autoSpin = false;
|
|
621
|
+
// 把该节点转到正面:根据其当前世界位置反推目标旋转
|
|
622
|
+
this._flyTarget = nd;
|
|
623
|
+
this.camDistTarget = 380;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
setSelected(id){
|
|
627
|
+
this.selected = id;
|
|
628
|
+
if(id) this.focusOn(id);
|
|
629
|
+
else this.clearFocus();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// 聚焦汇聚:点击节点 → 相关节点发光 + 汇聚到它周围,无关节点淡出
|
|
633
|
+
focusOn(id){
|
|
634
|
+
const T = this.T;
|
|
635
|
+
const center = this.nodes.find(n=>n.id===id);
|
|
636
|
+
if(!center) return;
|
|
637
|
+
this.autoSpin = false;
|
|
638
|
+
this._flyTarget = center;
|
|
639
|
+
this.camDistTarget = 420;
|
|
640
|
+
|
|
641
|
+
// 找出所有与中心节点直接相连的节点;同时只显示该节点的连线(默认全隐藏)
|
|
642
|
+
const relatedIds = new Set([id]);
|
|
643
|
+
this.edges.forEach(e=>{
|
|
644
|
+
const on = (e.from.id===id || e.to.id===id);
|
|
645
|
+
e.line.visible = on;
|
|
646
|
+
if(on){ relatedIds.add(e.from.id); relatedIds.add(e.to.id); }
|
|
647
|
+
});
|
|
648
|
+
const related = this.nodes.filter(n=>relatedIds.has(n.id) && n.id!==id);
|
|
649
|
+
|
|
650
|
+
// 分级标签:本次钻取只点亮 top-N 最重要(连接数最高)的邻居标签。
|
|
651
|
+
// 点书 → 看全书最核心的几个;点其中一个 → 再看它的核心邻居(逐级下钻)。
|
|
652
|
+
this.nodes.forEach(n=> n._revealLabel=false);
|
|
653
|
+
related.slice().sort((a,b)=> (b.deg||0)-(a.deg||0)).slice(0, 6)
|
|
654
|
+
.forEach(n=> n._revealLabel=true);
|
|
655
|
+
|
|
656
|
+
// 中心节点:移到球心附近、发光、不淡出
|
|
657
|
+
center.focusPos = new T.Vector3(0, 0, 0);
|
|
658
|
+
center.glowOn = true; center.dim = false;
|
|
659
|
+
|
|
660
|
+
// 相关节点:围绕中心排成一圈(球面环),发光
|
|
661
|
+
const R = 95; // 汇聚半径
|
|
662
|
+
const n = related.length;
|
|
663
|
+
related.forEach((nd, i)=>{
|
|
664
|
+
const ang = (i / Math.max(1,n)) * Math.PI*2;
|
|
665
|
+
// 在一个倾斜的环上分布,有立体感
|
|
666
|
+
const tilt = 0.5;
|
|
667
|
+
nd.focusPos = new T.Vector3(
|
|
668
|
+
Math.cos(ang)*R,
|
|
669
|
+
Math.sin(ang)*R*tilt,
|
|
670
|
+
Math.sin(ang)*R*0.8
|
|
671
|
+
);
|
|
672
|
+
nd.glowOn = true; nd.dim = false;
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// 无关节点:淡出、退远、不发光
|
|
676
|
+
this.nodes.forEach(nd=>{
|
|
677
|
+
if(!relatedIds.has(nd.id)){
|
|
678
|
+
nd.focusPos = nd.basePos.clone().multiplyScalar(2.4); // 退远
|
|
679
|
+
nd.glowOn = false; nd.dim = true;
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// 清除聚焦:所有节点平滑回归球面
|
|
685
|
+
clearFocus(){
|
|
686
|
+
this._flyTarget = null;
|
|
687
|
+
this.camDistTarget = 720;
|
|
688
|
+
this.autoSpin = true;
|
|
689
|
+
this.nodes.forEach(nd=>{
|
|
690
|
+
nd.focusPos = null;
|
|
691
|
+
nd.glowOn = false;
|
|
692
|
+
nd.dim = false;
|
|
693
|
+
nd._revealLabel = false; // 回到默认态:只剩一级书名标签
|
|
694
|
+
});
|
|
695
|
+
this.edges.forEach(e=>{ e.line.visible = false; }); // 回到默认:不画边
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 搜索高亮:命中节点发光+显标签,其余淡出(原地,不汇聚);清空传空数组。
|
|
699
|
+
highlight(ids){
|
|
700
|
+
const set = new Set(ids || []);
|
|
701
|
+
if(!set.size){ this.setSelected(null); return; } // 无命中→恢复默认视图
|
|
702
|
+
this.autoSpin = false;
|
|
703
|
+
this._flyTarget = null;
|
|
704
|
+
this.selected = '__search__'; // 让标签逻辑走"只显示 _revealLabel(命中项)"分支
|
|
705
|
+
this.nodes.forEach(nd=>{
|
|
706
|
+
const hit = set.has(nd.id);
|
|
707
|
+
nd.focusPos = null; // 原地高亮,不像点击那样汇聚
|
|
708
|
+
nd.glowOn = hit;
|
|
709
|
+
nd.dim = !hit;
|
|
710
|
+
nd._revealLabel = hit; // 命中项显示标签,方便看是哪些
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
resetView(){
|
|
715
|
+
this.rot = { x:0, y:0 }; this.autoSpin = true;
|
|
716
|
+
this.camDistTarget = 720;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
resize(){
|
|
720
|
+
if(this.disposed) return;
|
|
721
|
+
const w = this.container.clientWidth, h = this.container.clientHeight;
|
|
722
|
+
this.camera.aspect = w/h; this.camera.updateProjectionMatrix();
|
|
723
|
+
this.renderer.setSize(w, h);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
dispose(){
|
|
727
|
+
this.disposed = true;
|
|
728
|
+
if(this.raf) cancelAnimationFrame(this.raf);
|
|
729
|
+
|
|
730
|
+
// 移除所有事件监听器
|
|
731
|
+
try{
|
|
732
|
+
window.removeEventListener('pointermove', this._onMove);
|
|
733
|
+
window.removeEventListener('pointerup', this._onUp);
|
|
734
|
+
if(this.canvas){
|
|
735
|
+
this.canvas.removeEventListener('pointerdown', this._onDown);
|
|
736
|
+
this.canvas.removeEventListener('wheel', this._onWheel);
|
|
737
|
+
this.canvas.removeEventListener('pointermove', this._onPointerMove);
|
|
738
|
+
this.canvas.removeEventListener('click', this._onClick);
|
|
739
|
+
}
|
|
740
|
+
}catch(e){}
|
|
741
|
+
|
|
742
|
+
// 递归释放场景里所有 geometry / material / texture
|
|
743
|
+
try{
|
|
744
|
+
this.scene.traverse(obj=>{
|
|
745
|
+
if(obj.geometry) obj.geometry.dispose();
|
|
746
|
+
if(obj.material){
|
|
747
|
+
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
748
|
+
mats.forEach(m=>{
|
|
749
|
+
if(m.map) m.map.dispose(); // 纹理(label CanvasTexture)
|
|
750
|
+
if(m.dispose) m.dispose();
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}catch(e){}
|
|
755
|
+
|
|
756
|
+
// 强制释放 WebGL context(renderer.dispose 不会释放 context,必须 forceContextLoss)
|
|
757
|
+
try{
|
|
758
|
+
this.renderer.dispose();
|
|
759
|
+
this.renderer.forceContextLoss();
|
|
760
|
+
if(this.canvas && this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas);
|
|
761
|
+
}catch(e){}
|
|
762
|
+
|
|
763
|
+
// 断开引用,助 GC
|
|
764
|
+
this.nodes = []; this.edges = []; this._meshes = null;
|
|
765
|
+
this.scene = null; this.renderer = null; this.camera = null; this.world = null;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ── 等待 Three.js(由 head 的 module 预加载到 window.THREE)──
|
|
770
|
+
async function ensureThree(){
|
|
771
|
+
if(THREE) return THREE;
|
|
772
|
+
if(window.THREE){ THREE = window.THREE; return THREE; }
|
|
773
|
+
if(window.__threeLoadError) return null;
|
|
774
|
+
// 等待 three-ready 事件(最多 5 秒)
|
|
775
|
+
return await new Promise(resolve=>{
|
|
776
|
+
let done = false;
|
|
777
|
+
const onReady = ()=>{ if(done) return; done=true; THREE = window.THREE; resolve(THREE); };
|
|
778
|
+
window.addEventListener('three-ready', onReady, { once:true });
|
|
779
|
+
const t0 = Date.now();
|
|
780
|
+
const poll = setInterval(()=>{
|
|
781
|
+
if(window.THREE){ clearInterval(poll); onReady(); }
|
|
782
|
+
else if(window.__threeLoadError || Date.now()-t0 > 5000){ clearInterval(poll); if(!done){ done=true; resolve(null); } }
|
|
783
|
+
}, 100);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function renderGraph(){
|
|
788
|
+
const container = document.getElementById('graph-canvas');
|
|
789
|
+
if(!container) return;
|
|
790
|
+
|
|
791
|
+
// 清旧实例
|
|
792
|
+
if(planet){ try{ planet.dispose(); }catch(e){} planet = null; }
|
|
793
|
+
// 移除旧 canvas(保留 hint/legend 等覆盖层)
|
|
794
|
+
Array.from(container.querySelectorAll('canvas')).forEach(c=>c.remove());
|
|
795
|
+
|
|
796
|
+
const T = await ensureThree();
|
|
797
|
+
if(!T){
|
|
798
|
+
container.insertAdjacentHTML('beforeend',
|
|
799
|
+
'<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#8b909b;font-size:13px;text-align:center;padding:20px">3D 引擎需通过 http 访问<br>请用本地服务器打开(python -m http.server)</div>');
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if(!Object.keys(state.kus).length) return;
|
|
803
|
+
|
|
804
|
+
planet = new KnowledgePlanet(container, T);
|
|
805
|
+
planet.build(state.kus, state.relations);
|
|
806
|
+
if(state.selected) planet.setSelected(state.selected);
|
|
807
|
+
planet.start();
|
|
808
|
+
window.planet = planet;
|
|
809
|
+
|
|
810
|
+
window._fitGraph = ()=>{ if(planet) planet.resetView(); };
|
|
811
|
+
if(!window._planetResizeBound){
|
|
812
|
+
window.addEventListener('resize', ()=>{ if(planet) planet.resize(); });
|
|
813
|
+
window._planetResizeBound = true;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// startRotation/stopRotation:上传/解析/摄入期间停掉星球渲染循环 ——
|
|
818
|
+
// 每帧 _update() + renderer.render() 会争 GPU 和主线程,拖慢大文件上传。
|
|
819
|
+
// 真正省 CPU 的是停 RAF(只关 autoSpin 不够,缓动还在跑)。
|
|
820
|
+
// planet.raf 在 start() 里被赋值,dispose() 里被 cancel —— 这里只 cancel。
|
|
821
|
+
function startRotation(){
|
|
822
|
+
if(!planet) return;
|
|
823
|
+
if(planet.raf) return; // 已在跑(避免 start() 双重 RAF 嵌套)
|
|
824
|
+
planet.start(); // 复用现有 start() 启动 tick
|
|
825
|
+
}
|
|
826
|
+
function stopRotation(){
|
|
827
|
+
if(!planet) return;
|
|
828
|
+
if(planet.raf){
|
|
829
|
+
cancelAnimationFrame(planet.raf);
|
|
830
|
+
planet.raf = null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
function relLabel(t){
|
|
836
|
+
const m={CREATED_BY:'提出',SUPPORTS:'支持',CONTRADICTS:'冲突',MENTIONS:'提及',
|
|
837
|
+
BELONGS_TO:'属于',DERIVED_FROM:'衍生',IMPLEMENTS:'实现',USES:'运用',SUPERSEDES:'取代'};
|
|
838
|
+
return m[t]||t;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function renderLegend(){
|
|
842
|
+
const types=['Entity','Concept','Claim','Evidence','Ability'];
|
|
843
|
+
$('#legend').innerHTML=types.map(t=>
|
|
844
|
+
`<div class="lg-row"><span class="lg-dot" style="background:${KU_COLORS[t]}"></span>${KU_LABEL[t]} ${t}</div>`
|
|
845
|
+
).join('')+`<div class="lg-row" style="border-top:1px solid var(--line-2);padding-top:6px;margin-top:1px">
|
|
846
|
+
<span class="lg-dot" style="background:none;border:2px dashed var(--alert);width:8px;height:8px"></span>观点冲突</div>`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ════════════════════════════════════════════════════════
|
|
850
|
+
// Wiki 详情面板
|
|
851
|
+
// ════════════════════════════════════════════════════════
|
|
852
|
+
function selectKU(id){
|
|
853
|
+
if(!state.kus[id]) return;
|
|
854
|
+
state.selected=id;
|
|
855
|
+
if(planet){ try{ planet.setSelected(id); }catch(e){} }
|
|
856
|
+
renderWiki(state.kus[id]);
|
|
857
|
+
hydrateKU(id); // 原文段落等重字段不在星球初始负载里(为提速),点开时按需补全
|
|
858
|
+
loadSynthesis(id); // 若该节点已有缓存综述,自动显示(GET,不调 LLM)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// 读缓存综述(只 GET,不触发 LLM/不花钱)。有缓存就填进 state.synth 并重渲染。
|
|
862
|
+
async function loadSynthesis(id){
|
|
863
|
+
if(client.demo) return;
|
|
864
|
+
const cur=state.synth[id];
|
|
865
|
+
if(cur && cur.content) return; // 本会话已加载过
|
|
866
|
+
try{
|
|
867
|
+
const r=await client._get('/knowledge/ku/'+encodeURIComponent(id)+'/synthesis');
|
|
868
|
+
if(r && r.has_cache){
|
|
869
|
+
state.synth[id]={content:r.content,model:r.model,generated_at:r.generated_at,stale:false};
|
|
870
|
+
if(state.selected===id) renderWiki(state.kus[id]);
|
|
871
|
+
}
|
|
872
|
+
}catch(e){ /* 无缓存/出错都静默,维持"生成"按钮 */ }
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// 点「生成综述」→ 调 LLM 生成(或返回缓存)。派生只读视图,后端不写知识库。
|
|
876
|
+
async function genSynthesis(id, force){
|
|
877
|
+
if(client.demo){ toast('演示模式无法生成综述,请先连接后端','err'); return; }
|
|
878
|
+
state.synth[id]='loading';
|
|
879
|
+
if(state.selected===id) renderWiki(state.kus[id]);
|
|
880
|
+
try{
|
|
881
|
+
const r=await client._post('/knowledge/ku/'+encodeURIComponent(id)+'/synthesis'+(force?'?force=true':''), {});
|
|
882
|
+
state.synth[id]={content:r.content,model:r.model,generated_at:r.generated_at,stale:!!r.stale};
|
|
883
|
+
}catch(e){
|
|
884
|
+
state.synth[id]=null;
|
|
885
|
+
toast('综述生成失败:'+(e&&e.message?e.message:'请检查已连接后端并配置了 LLM'),'err');
|
|
886
|
+
}
|
|
887
|
+
if(state.selected===id) renderWiki(state.kus[id]);
|
|
888
|
+
}
|
|
889
|
+
window.genSynthesis=genSynthesis;
|
|
890
|
+
|
|
891
|
+
// 路径查询(高级检索):把当前节点设为起/终点,查两点最短路径并在星球高亮。
|
|
892
|
+
function setPathNode(id, which){
|
|
893
|
+
state.pathSel=state.pathSel||{};
|
|
894
|
+
state.pathSel[which]=id; state.pathSel.result='';
|
|
895
|
+
if(state.selected) renderWiki(state.kus[state.selected]);
|
|
896
|
+
}
|
|
897
|
+
window.setPathNode=setPathNode;
|
|
898
|
+
async function findPath(){
|
|
899
|
+
const ps=state.pathSel||{};
|
|
900
|
+
if(!ps.from||!ps.to){ toast('请先设起点①和终点②','err'); return; }
|
|
901
|
+
try{
|
|
902
|
+
const r=await client._get('/search/path?from_id='+encodeURIComponent(ps.from)+'&to_id='+encodeURIComponent(ps.to));
|
|
903
|
+
if(r.reachable && (r.path||[]).length){
|
|
904
|
+
const names=r.path.map(id=>(state.kus[id]&&state.kus[id].name)||id);
|
|
905
|
+
ps.result='最短路径('+r.hops+' 跳):'+names.map(esc).join(' → ');
|
|
906
|
+
if(window.planet){ try{ planet.highlight(r.path); }catch(e){} }
|
|
907
|
+
}else{ ps.result='两节点间无连通路径'; }
|
|
908
|
+
}catch(e){ ps.result='路径查询失败(需已连接后端)'; }
|
|
909
|
+
if(state.selected) renderWiki(state.kus[state.selected]);
|
|
910
|
+
}
|
|
911
|
+
window.findPath=findPath;
|
|
912
|
+
|
|
913
|
+
// 按需补全:星球全量接口为瘦身省略了 source_text,点开节点时单独拉 /ku/{id} 拿完整原文,
|
|
914
|
+
// 合并回 state.kus 并重渲染(仍选中才刷新,避免快速点击的竞态覆盖)。
|
|
915
|
+
async function hydrateKU(id){
|
|
916
|
+
const ku=state.kus[id];
|
|
917
|
+
if(!ku || client.demo) return; // 演示数据自带原文,无需拉
|
|
918
|
+
const ex=ku.extra||{};
|
|
919
|
+
if(ex._hydrated || (ex.source_text&&ex.source_text.trim())) return;
|
|
920
|
+
try{
|
|
921
|
+
const full=await client._get('/knowledge/ku/'+encodeURIComponent(id));
|
|
922
|
+
ku.extra=Object.assign({}, ku.extra, full.extra||{}, {_hydrated:true});
|
|
923
|
+
if(state.selected===id) renderWiki(ku);
|
|
924
|
+
}catch(e){ /* 拉不到就维持精简视图,不报错 */ }
|
|
925
|
+
}
|
|
926
|
+
function clearSelection(){
|
|
927
|
+
state.selected=null;
|
|
928
|
+
if(planet){ try{ planet.setSelected(null); }catch(e){} }
|
|
929
|
+
$('#wiki-body').innerHTML=emptyWiki();
|
|
930
|
+
}
|
|
931
|
+
// 暴露给 3D 引擎(planet 点击节点时回调)
|
|
932
|
+
window.selectKU = selectKU;
|
|
933
|
+
window.clearSelection = clearSelection;
|
|
934
|
+
|
|
935
|
+
// 可折叠区块开关(供 side-label 的 onclick 调用)
|
|
936
|
+
function toggleCollapse(id){ const el=document.getElementById(id); if(el) el.classList.toggle('collapsed'); }
|
|
937
|
+
window.toggleCollapse = toggleCollapse;
|
|
938
|
+
function emptyWiki(){
|
|
939
|
+
return `<div class="wiki-empty"><svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg><p>点击图谱中的节点<br>查看知识单元详情</p></div>`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// 综合页(synthesis page):把节点在图里的邻域组织成可读的 wiki 词条,
|
|
943
|
+
// 而不只是几个原子字段。图仍是唯一真相源,这里只是它的一个只读投影(同 3D 星球)。
|
|
944
|
+
function renderWiki(ku){
|
|
945
|
+
if(!ku){ $('#wiki-body').innerHTML=emptyWiki(); return; }
|
|
946
|
+
const c=KU_COLORS[ku.ku_type];
|
|
947
|
+
const ex=ku.extra||{};
|
|
948
|
+
let html=`<div class="wiki-card">`;
|
|
949
|
+
html+=`<div class="wiki-type" style="color:${c};background:${c}1f">
|
|
950
|
+
<span class="lg-dot" style="background:${c};width:8px;height:8px"></span>${KU_LABEL[ku.ku_type]} · ${ku.ku_type}</div>`;
|
|
951
|
+
html+=`<div class="wiki-title">${esc(ku.name)}</div>`;
|
|
952
|
+
if(ku.domain) html+=`<div class="wiki-domain">${esc(ku.domain)}</div>`;
|
|
953
|
+
|
|
954
|
+
// ── 概述 ──
|
|
955
|
+
if(ku.ku_type==='Claim'){
|
|
956
|
+
if(ex.statement) html+=`<div class="wiki-summary">${esc(ex.statement)}</div>`;
|
|
957
|
+
const cs=ex.claim_status||'pending';
|
|
958
|
+
html+=`<div class="wiki-section"><div class="conf-row"><span style="font-size:12px;color:var(--ink-2);font-weight:600">置信度</span>
|
|
959
|
+
<span class="status-badge st-${cs}">${cs}</span></div>
|
|
960
|
+
<div class="conf-bar"><div class="conf-fill" style="width:${(ku.confidence*100).toFixed(0)}%"></div></div>
|
|
961
|
+
<div style="text-align:right;margin-top:4px"><span class="conf-val">${(ku.confidence*100).toFixed(0)}%</span></div></div>`;
|
|
962
|
+
} else if(ku.ku_type==='Fact'){
|
|
963
|
+
if(ex.statement) html+=`<div class="wiki-summary">${esc(ex.statement)}</div>`;
|
|
964
|
+
html+=`<div class="wiki-section"><span class="status-badge st-active">已验证事实 · 置信度 100%</span></div>`;
|
|
965
|
+
} else {
|
|
966
|
+
if(ku.summary) html+=`<div class="wiki-summary">${esc(ku.summary)}</div>`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ── AI 综述(第2档:LLM 把节点邻域组织成连贯词条;派生只读视图,不写回知识库)──
|
|
970
|
+
if(!client.demo){
|
|
971
|
+
const sy=state.synth[ku.ku_id];
|
|
972
|
+
html+=`<div class="wiki-section"><h4>AI 综述</h4>`;
|
|
973
|
+
if(sy==='loading'){
|
|
974
|
+
html+=`<div class="synth-loading">正在生成综述…(调用 LLM,请稍候)</div>`;
|
|
975
|
+
}else if(sy && sy.content){
|
|
976
|
+
html+=`<div class="synth-body">${esc(sy.content).replace(/\n/g,'<br>')}</div>`;
|
|
977
|
+
const meta=[sy.model?('模型 '+sy.model):'', sy.stale?'⚠ 知识已更新,建议重新生成':'派生只读视图'].filter(Boolean).join(' · ');
|
|
978
|
+
html+=`<div class="synth-meta">${esc(meta)}</div>
|
|
979
|
+
<button class="synth-btn ghost" onclick="genSynthesis('${ku.ku_id}',true)">重新生成</button>`;
|
|
980
|
+
}else{
|
|
981
|
+
html+=`<button class="synth-btn" onclick="genSynthesis('${ku.ku_id}',false)">✦ 生成 AI 综述</button>
|
|
982
|
+
<div class="synth-hint">把该节点的关联知识用 LLM 组织成连贯词条(只读,不写回知识库)</div>`;
|
|
983
|
+
}
|
|
984
|
+
html+=`</div>`;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// ── 路径查询(高级检索:两节点间最短路径,结果在星球高亮)──
|
|
988
|
+
if(!client.demo){
|
|
989
|
+
const ps=state.pathSel||{};
|
|
990
|
+
const nm=id=>id?esc(state.kus[id]&&state.kus[id].name||id):'(未设)';
|
|
991
|
+
html+=`<div class="wiki-section"><h4>路径查询</h4>
|
|
992
|
+
<div class="path-ctl">
|
|
993
|
+
<button class="synth-btn ghost" onclick="setPathNode('${ku.ku_id}','from')">设为起点①</button>
|
|
994
|
+
<button class="synth-btn ghost" onclick="setPathNode('${ku.ku_id}','to')">设为终点②</button>
|
|
995
|
+
</div>
|
|
996
|
+
<div class="path-meta">① ${nm(ps.from)} → ② ${nm(ps.to)}</div>`;
|
|
997
|
+
if(ps.from && ps.to) html+=`<button class="synth-btn" onclick="findPath()">查最短路径</button>`;
|
|
998
|
+
if(ps.result) html+=`<div class="path-result">${ps.result}</div>`;
|
|
999
|
+
html+=`</div>`;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ── 原文段落 ──
|
|
1003
|
+
if(ex.source_text && ex.source_text.trim()){
|
|
1004
|
+
html+=`<div class="wiki-section"><h4>原文段落</h4><div class="wiki-source">${esc(ex.source_text)}</div></div>`;
|
|
1005
|
+
}
|
|
1006
|
+
// Entity 别名
|
|
1007
|
+
if(ku.ku_type==='Entity'&&ex.aliases&&ex.aliases.length){
|
|
1008
|
+
html+=`<div class="wiki-section"><h4>别名</h4><div class="tag-row">${ex.aliases.map(a=>`<span class="tag">${esc(a)}</span>`).join('')}</div></div>`;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ── 邻域按类型分组(去重),组织成词条各段 ──
|
|
1012
|
+
const nb={Fact:[],Claim:[],Concept:[],Entity:[],Evidence:[]};
|
|
1013
|
+
const seen=new Set([ku.ku_id]);
|
|
1014
|
+
getRelations(ku.ku_id).forEach(r=>{
|
|
1015
|
+
const o=state.kus[r.other];
|
|
1016
|
+
if(!o || seen.has(o.ku_id)) return;
|
|
1017
|
+
seen.add(o.ku_id);
|
|
1018
|
+
if(nb[o.ku_type]) nb[o.ku_type].push(o);
|
|
1019
|
+
});
|
|
1020
|
+
// 图里事实/观点通常只连到 Evidence(书),不直接连实体/概念。为让词条真正丰满,
|
|
1021
|
+
// 再按"陈述里提到本节点名字/别名"补充相关事实与观点(纯前端聚合,不改图)。
|
|
1022
|
+
if(ku.ku_type!=='Evidence'){
|
|
1023
|
+
const names=[ku.name, ...((ex.aliases)||[])].filter(n=>n && n.length>=2);
|
|
1024
|
+
if(names.length){
|
|
1025
|
+
Object.values(state.kus).forEach(k=>{
|
|
1026
|
+
if((k.ku_type==='Fact'||k.ku_type==='Claim') && !seen.has(k.ku_id)){
|
|
1027
|
+
const st=(k.extra&&k.extra.statement)||k.summary||'';
|
|
1028
|
+
if(names.some(n=>st.indexOf(n)>=0)){ seen.add(k.ku_id); nb[k.ku_type].push(k); }
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const stmt=k=>(k.extra&&k.extra.statement)||k.summary||k.name;
|
|
1034
|
+
|
|
1035
|
+
if(ku.ku_type==='Evidence'){
|
|
1036
|
+
const isColl=(ku.tags||[]).includes('_collection');
|
|
1037
|
+
if(isColl){
|
|
1038
|
+
// 一级:合集(文件夹)→ 列出所含文件/章节(PART_OF 子节点),点击逐级下钻
|
|
1039
|
+
const files=(state.relations||[]).filter(r=>r.type==='PART_OF'&&r.to===ku.ku_id).map(r=>state.kus[r.from]).filter(Boolean);
|
|
1040
|
+
html+=`<div class="wiki-section"><h4>合集 · 来源</h4><div class="evid-item"><div class="evid-src">📁 ${esc(ku.name)}</div></div></div>`;
|
|
1041
|
+
if(files.length){
|
|
1042
|
+
html+=`<div class="wiki-section"><h4>包含文件/章节 ${files.length}(点击进入)</h4><div class="kw-row">`;
|
|
1043
|
+
files.forEach(f=>{ const nm=f.name.length>18?f.name.slice(0,17)+'…':f.name; html+=`<span class="kw-chip" onclick="selectKU('${f.ku_id}')">📄 ${esc(nm)}</span>`; });
|
|
1044
|
+
html+=`</div></div>`;
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
// 二级:文件(章节)或独立来源 → 来源 + 所属合集 + 本文件关键词
|
|
1048
|
+
html+=`<div class="wiki-section"><h4>来源</h4><div class="evid-item">
|
|
1049
|
+
<div class="evid-src">${esc(ex.source||ku.name)}</div>
|
|
1050
|
+
<div class="evid-meta">${[ex.author,ex.year,ex.page?'p.'+ex.page:''].filter(Boolean).join(' · ')}</div></div></div>`;
|
|
1051
|
+
const inCol=(ku.tags||[]).find(t=>t.startsWith('_incol:'));
|
|
1052
|
+
if(inCol){ const col=state.kus[inCol.slice(7)];
|
|
1053
|
+
if(col) html+=`<div class="wiki-section"><h4>所属合集</h4><div class="kw-row"><span class="kw-chip" onclick="selectKU('${col.ku_id}')">📁 ${esc(col.name)}</span></div></div>`;
|
|
1054
|
+
}
|
|
1055
|
+
const deg={}; (state.relations||[]).forEach(r=>{ deg[r.from]=(deg[r.from]||0)+1; deg[r.to]=(deg[r.to]||0)+1; });
|
|
1056
|
+
const kws=[...nb.Concept,...nb.Entity,...nb.Fact,...nb.Claim].sort((a,b)=>(deg[b.ku_id]||0)-(deg[a.ku_id]||0));
|
|
1057
|
+
if(kws.length){
|
|
1058
|
+
html+=`<div class="wiki-section"><h4>本文件关键词 ${kws.length}(点击看词条/原文)</h4><div class="kw-row">`;
|
|
1059
|
+
kws.slice(0,40).forEach(k=>{
|
|
1060
|
+
const nm = k.name.length>16 ? k.name.slice(0,15)+'…' : k.name;
|
|
1061
|
+
html+=`<span class="kw-chip" onclick="selectKU('${k.ku_id}')">${esc(nm)}</span>`;
|
|
1062
|
+
});
|
|
1063
|
+
html+=`</div></div>`;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
} else {
|
|
1067
|
+
// 实体/概念/事实/观点:综合成词条。相关事实/观点放进固定高度框,内部滚动,省纵向空间。
|
|
1068
|
+
if(nb.Fact.length){
|
|
1069
|
+
let inner='';
|
|
1070
|
+
nb.Fact.slice(0,50).forEach(k=>{ inner+=`<div class="wiki-item fact" onclick="selectKU('${k.ku_id}')">${esc(stmt(k))}</div>`; });
|
|
1071
|
+
html+=`<div class="wiki-section"><h4>相关事实 ${nb.Fact.length}</h4><div class="wiki-scroll">${inner}</div></div>`;
|
|
1072
|
+
}
|
|
1073
|
+
if(nb.Claim.length){
|
|
1074
|
+
let inner='';
|
|
1075
|
+
nb.Claim.slice(0,50).forEach(k=>{
|
|
1076
|
+
const conf=Math.round((k.confidence||0)*100);
|
|
1077
|
+
inner+=`<div class="wiki-item claim" onclick="selectKU('${k.ku_id}')"><span class="wiki-conf">${conf}%</span>${esc(stmt(k))}</div>`;
|
|
1078
|
+
});
|
|
1079
|
+
html+=`<div class="wiki-section"><h4>相关观点 ${nb.Claim.length}</h4><div class="wiki-scroll">${inner}</div></div>`;
|
|
1080
|
+
}
|
|
1081
|
+
// 关联概念/实体:小星球式散点(省空间,悬停看名、点击进入)
|
|
1082
|
+
const linked=[...nb.Concept,...nb.Entity];
|
|
1083
|
+
if(linked.length){
|
|
1084
|
+
html+=`<div class="wiki-section"><h4>关联 ${linked.length}</h4>${kwCluster(linked)}</div>`;
|
|
1085
|
+
}
|
|
1086
|
+
if(nb.Evidence.length){
|
|
1087
|
+
html+=`<div class="wiki-section"><h4>出自</h4><div class="kw-row">`;
|
|
1088
|
+
nb.Evidence.forEach(k=>{ html+=`<span class="kw-chip" onclick="selectKU('${k.ku_id}')">📖 ${esc(k.name)}</span>`; });
|
|
1089
|
+
html+=`</div></div>`;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// 标签
|
|
1094
|
+
if(ku.tags&&ku.tags.length){
|
|
1095
|
+
html+=`<div class="wiki-section"><h4>标签</h4><div class="tag-row">${ku.tags.map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</div></div>`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
html+=`</div>`;
|
|
1099
|
+
$('#wiki-body').innerHTML=html;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// 关联关键词的小星球式散点:fibonacci 圆盘分布、按类型着色、悬停显名(<title>)、点击进入。
|
|
1103
|
+
// 比一长串 chip 省纵向空间,且呼应星球主题。
|
|
1104
|
+
function kwCluster(list, capN){
|
|
1105
|
+
const N=Math.min(list.length, capN||80);
|
|
1106
|
+
const cx=130, cy=90, R=82, ga=Math.PI*(3-Math.sqrt(5));
|
|
1107
|
+
let dots='';
|
|
1108
|
+
for(let i=0;i<N;i++){
|
|
1109
|
+
const k=list[i];
|
|
1110
|
+
const rr=R*Math.sqrt((i+0.5)/N), th=i*ga;
|
|
1111
|
+
const x=(cx+rr*Math.cos(th)).toFixed(1), y=(cy+rr*Math.sin(th)).toFixed(1);
|
|
1112
|
+
const col=KU_COLORS[k.ku_type]||'#888';
|
|
1113
|
+
dots+=`<circle cx="${x}" cy="${y}" r="4" fill="${col}" onclick="selectKU('${k.ku_id}')"><title>${esc(k.name)}</title></circle>`;
|
|
1114
|
+
}
|
|
1115
|
+
const more=list.length>N?`(显示 ${N}/${list.length})`:'';
|
|
1116
|
+
return `<div class="kw-cluster"><svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet">${dots}</svg></div>`
|
|
1117
|
+
+ `<div class="kw-cluster-cap">悬停圆点看名称 · 点击进入${more}</div>`;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function getRelations(id){
|
|
1121
|
+
const out=[];
|
|
1122
|
+
state.relations.forEach(r=>{
|
|
1123
|
+
if(r.from===id) out.push({other:r.to,type:r.type,dir:'out'});
|
|
1124
|
+
else if(r.to===id) out.push({other:r.from,type:r.type,dir:'in'});
|
|
1125
|
+
});
|
|
1126
|
+
return out;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// ════════════════════════════════════════════════════════
|
|
1130
|
+
// 能力卡片
|
|
1131
|
+
// ════════════════════════════════════════════════════════
|
|
1132
|
+
function renderAbilities(){
|
|
1133
|
+
const body=$('#ability-body');
|
|
1134
|
+
body.innerHTML=state.abilities.map(a=>{
|
|
1135
|
+
const covRows=Object.entries(a.coverage).map(([k,v])=>{
|
|
1136
|
+
const pct=(v*100).toFixed(0);
|
|
1137
|
+
const low=v<0.5;
|
|
1138
|
+
return `<div class="ac-cov-row">
|
|
1139
|
+
<span class="ac-cov-label" title="${k}">${k}</span>
|
|
1140
|
+
<span class="ac-cov-track"><span class="ac-cov-bar" style="width:${pct}%;${low?'background:var(--klein-l)':''}"></span></span>
|
|
1141
|
+
<span class="ac-cov-pct" style="${low?'color:var(--ink-3)':''}">${pct}%</span></div>`;
|
|
1142
|
+
}).join('');
|
|
1143
|
+
const ready=a.can_compile;
|
|
1144
|
+
return `<div class="ability-card">
|
|
1145
|
+
<div class="ac-head">
|
|
1146
|
+
<div><div class="ac-name">${esc(a.display_name)}</div><div class="ac-domain">${esc(a.domain||'')}</div></div>
|
|
1147
|
+
<div class="ac-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></div>
|
|
1148
|
+
</div>
|
|
1149
|
+
<div class="ac-cov-list">${covRows}</div>
|
|
1150
|
+
<div class="ac-foot">
|
|
1151
|
+
<span class="ac-status ${ready?'ac-ready':'ac-locked'}">${ready?'✓ 可编译':'覆盖不足'}</span>
|
|
1152
|
+
<button class="btn btn-sm btn-compile" ${ready?'':'disabled'} data-ability="${a.ability_key}">
|
|
1153
|
+
${a.compiled?'重新编译':'编译'}</button>
|
|
1154
|
+
</div>
|
|
1155
|
+
</div>`;
|
|
1156
|
+
}).join('')||'<div style="color:var(--ink-3);font-size:13px;padding:20px">暂无可编译能力</div>';
|
|
1157
|
+
|
|
1158
|
+
body.querySelectorAll('.btn-compile').forEach(b=>{
|
|
1159
|
+
b.onclick=()=>compileAbility(b.dataset.ability,b);
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async function compileAbility(key,btn){
|
|
1164
|
+
const a=state.abilities.find(x=>x.ability_key===key);
|
|
1165
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span>';
|
|
1166
|
+
if(client.demo){
|
|
1167
|
+
await new Promise(r=>setTimeout(r,800));
|
|
1168
|
+
toast(`已编译能力包 ${a.display_name}.hkap`,'ok');
|
|
1169
|
+
btn.disabled=false; btn.textContent='重新编译'; a.compiled=true;
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
try{
|
|
1173
|
+
await client._post(`/abilities/${key}/compile`,{});
|
|
1174
|
+
toast(`已编译能力包 ${a.display_name}.hkap`,'ok');
|
|
1175
|
+
btn.disabled=false; btn.textContent='重新编译';
|
|
1176
|
+
}catch(e){
|
|
1177
|
+
if(e.status===422){
|
|
1178
|
+
const miss=(e.detail&&e.detail.missing_skills)||[];
|
|
1179
|
+
toast(`覆盖度不足,还缺:${miss.slice(0,3).join('、')}`,'err');
|
|
1180
|
+
}else toast('编译失败','err');
|
|
1181
|
+
btn.disabled=false; btn.textContent='编译';
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// ════════════════════════════════════════════════════════
|
|
1186
|
+
// 冲突裁决
|
|
1187
|
+
// ════════════════════════════════════════════════════════
|
|
1188
|
+
function renderConflicts(){
|
|
1189
|
+
const open=state.conflicts.filter(c=>c.status==='open');
|
|
1190
|
+
const body=$('#conflict-body');
|
|
1191
|
+
if(!open.length){
|
|
1192
|
+
body.innerHTML=`<div class="conflict-empty">
|
|
1193
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="9"/></svg>
|
|
1194
|
+
<p style="font-size:12.5px">暂无待裁决冲突<br><span style="color:var(--ink-3);font-size:11px">知识库观点一致</span></p></div>`;
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
body.innerHTML=open.map(c=>{
|
|
1198
|
+
const a=state.kus[c.claim_a_id], b=state.kus[c.claim_b_id];
|
|
1199
|
+
if(!a||!b)return'';
|
|
1200
|
+
const pick=state.conflictPick[c.conflict_id];
|
|
1201
|
+
return `<div class="conflict-card">
|
|
1202
|
+
<div class="cc-head"><span class="cc-domain">${esc(c.domain||'')}</span>
|
|
1203
|
+
<span style="font-size:10px;color:var(--ink-3);font-weight:600;margin-left:auto">${c.resolution_strategy}</span></div>
|
|
1204
|
+
<div class="cc-claim ${pick===c.claim_a_id?'picked':''}" onclick="pickClaim('${c.conflict_id}','${c.claim_a_id}')">
|
|
1205
|
+
<div class="cc-claim-txt">${esc((a.extra&&a.extra.statement)||a.name)}</div>
|
|
1206
|
+
<div class="cc-claim-conf">置信度 ${(a.confidence*100).toFixed(0)}%</div></div>
|
|
1207
|
+
<div class="cc-vs">对立</div>
|
|
1208
|
+
<div class="cc-claim ${pick===c.claim_b_id?'picked':''}" onclick="pickClaim('${c.conflict_id}','${c.claim_b_id}')">
|
|
1209
|
+
<div class="cc-claim-txt">${esc((b.extra&&b.extra.statement)||b.name)}</div>
|
|
1210
|
+
<div class="cc-claim-conf">置信度 ${(b.confidence*100).toFixed(0)}%</div></div>
|
|
1211
|
+
<div class="cc-resolve">
|
|
1212
|
+
<button class="btn btn-sm btn-compile" ${pick?'':'disabled'} onclick="resolveConflict('${c.conflict_id}')">采纳所选观点</button>
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>`;
|
|
1215
|
+
}).join('');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function pickClaim(cid,clmid){
|
|
1219
|
+
state.conflictPick[cid]=clmid;
|
|
1220
|
+
renderConflicts();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async function resolveConflict(cid){
|
|
1224
|
+
const winner=state.conflictPick[cid];
|
|
1225
|
+
if(!winner){ toast('请先选择采纳哪个观点','err'); return; }
|
|
1226
|
+
if(client.demo){
|
|
1227
|
+
await new Promise(r=>setTimeout(r,500));
|
|
1228
|
+
const c=state.conflicts.find(x=>x.conflict_id===cid);
|
|
1229
|
+
if(c) c.status='resolved';
|
|
1230
|
+
// 落败方标 superseded
|
|
1231
|
+
const loser=c.claim_a_id===winner?c.claim_b_id:c.claim_a_id;
|
|
1232
|
+
if(state.kus[loser]) state.kus[loser].extra.claim_status='superseded';
|
|
1233
|
+
if(state.kus[winner]) state.kus[winner].extra.claim_status='active';
|
|
1234
|
+
// 移除这对 Claim 间的 CONTRADICTS 关系
|
|
1235
|
+
state.relations=state.relations.filter(r=>
|
|
1236
|
+
!(r.type==='CONTRADICTS' &&
|
|
1237
|
+
((r.from===loser&&r.to===winner)||(r.from===winner&&r.to===loser))));
|
|
1238
|
+
delete state.conflictPick[cid];
|
|
1239
|
+
toast('冲突已裁决,落败观点标记为 superseded','ok');
|
|
1240
|
+
// 裁决是低频操作,destroy 重建最稳(增量更新与 G6 hover-activate 有内部时序冲突)
|
|
1241
|
+
renderGraph();
|
|
1242
|
+
renderPanels();
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
try{
|
|
1246
|
+
await client._post(`/conflicts/${cid}/resolve`,{winner_id:winner,note:'UI 裁决',resolved_by:'ui'});
|
|
1247
|
+
toast('冲突已裁决','ok');
|
|
1248
|
+
delete state.conflictPick[cid];
|
|
1249
|
+
await loadData();
|
|
1250
|
+
}catch(e){ toast('裁决失败','err'); }
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// ════════════════════════════════════════════════════════
|
|
1254
|
+
// 搜索
|
|
1255
|
+
// ════════════════════════════════════════════════════════
|
|
1256
|
+
let searchTimer;
|
|
1257
|
+
async function doSearch(q){
|
|
1258
|
+
if(!q.trim()){ if(window.planet) window.planet.setSelected(null); return; } // 清空搜索→恢复默认视图(不重建星球)
|
|
1259
|
+
if(client.demo){
|
|
1260
|
+
// 演示:名字/摘要匹配,高亮命中节点
|
|
1261
|
+
const hits=Object.values(state.kus).filter(k=>
|
|
1262
|
+
(k.name+k.summary+((k.extra&&k.extra.statement)||'')).toLowerCase().includes(q.toLowerCase()));
|
|
1263
|
+
highlightNodes(hits.map(h=>h.ku_id));
|
|
1264
|
+
if(hits.length) toast(`找到 ${hits.length} 个匹配(${state.mode})`);
|
|
1265
|
+
else toast('无匹配结果','err');
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
try{
|
|
1269
|
+
const res=await client._post('/search',{query:q,mode:state.mode,top_k:15});
|
|
1270
|
+
highlightNodes((res.hits||[]).map(h=>h.ku_id));
|
|
1271
|
+
toast(`找到 ${(res.hits||[]).length} 个结果`);
|
|
1272
|
+
}catch(e){ toast('搜索失败','err'); }
|
|
1273
|
+
}
|
|
1274
|
+
function highlightNodes(ids){
|
|
1275
|
+
// 在 Three.js 星球上高亮命中节点(旧 G6 实现已废弃)
|
|
1276
|
+
if(window.planet) window.planet.highlight(ids || []);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// ════════════════════════════════════════════════════════
|
|
1280
|
+
// 摄入(文本 + 文件/文件夹批量)
|
|
1281
|
+
// ════════════════════════════════════════════════════════
|
|
1282
|
+
let pickedFiles = []; // 待摄入文件列表
|
|
1283
|
+
|
|
1284
|
+
function fmtSize(b){
|
|
1285
|
+
if(b<1024) return b+' B';
|
|
1286
|
+
if(b<1024*1024) return (b/1024).toFixed(1)+' KB';
|
|
1287
|
+
return (b/1024/1024).toFixed(1)+' MB';
|
|
1288
|
+
}
|
|
1289
|
+
const ALLOWED_EXT=['.pdf','.md','.markdown','.txt','.epub','.mobi','.azw3','.azw'];
|
|
1290
|
+
function extOK(name){ return ALLOWED_EXT.some(e=>name.toLowerCase().endsWith(e)); }
|
|
1291
|
+
|
|
1292
|
+
function addFiles(fileList){
|
|
1293
|
+
for(const f of fileList){
|
|
1294
|
+
if(!extOK(f.name)) continue; // 跳过不支持格式
|
|
1295
|
+
if(pickedFiles.some(x=>x.name===f.name && x.size===f.size)) continue; // 去重
|
|
1296
|
+
pickedFiles.push(f);
|
|
1297
|
+
}
|
|
1298
|
+
renderFileList();
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function renderFileList(){
|
|
1302
|
+
const box=$('#file-list');
|
|
1303
|
+
box.innerHTML='';
|
|
1304
|
+
pickedFiles.forEach((f,i)=>{
|
|
1305
|
+
const row=document.createElement('div');
|
|
1306
|
+
row.className='file-item'; row.dataset.idx=i;
|
|
1307
|
+
row.innerHTML=`<span class="fi-name">${f.name}</span>`+
|
|
1308
|
+
`<span class="fi-size">${fmtSize(f.size)}</span>`+
|
|
1309
|
+
`<button class="fi-del" title="移除"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M18 6 6 18M6 6l12 12"/></svg></button>`;
|
|
1310
|
+
row.querySelector('.fi-del').onclick=()=>{ pickedFiles.splice(i,1); renderFileList(); };
|
|
1311
|
+
box.appendChild(row);
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// 异步摄入(方案A):文件上传后立即转后台,前端轮询 job 真进度,期间可继续操作。
|
|
1316
|
+
let ingJobs = []; // {jobId,name,status,processed,total,ku,error}
|
|
1317
|
+
let _ingPoll = null;
|
|
1318
|
+
|
|
1319
|
+
function renderIngestStatus(){
|
|
1320
|
+
const el=$('#ingest-status'); if(!el) return;
|
|
1321
|
+
const active = ingJobs.filter(j=>j.status!=='done'&&j.status!=='error');
|
|
1322
|
+
if(!active.length){ el.innerHTML=''; return; }
|
|
1323
|
+
const cur = active[0];
|
|
1324
|
+
const pct = cur.total ? Math.round(cur.processed/cur.total*100) : 0;
|
|
1325
|
+
const more = active.length>1 ? ` (+${active.length-1} 个排队)` : '';
|
|
1326
|
+
el.innerHTML =
|
|
1327
|
+
`<div class="ing-line"><span class="spinner"></span> 后台摄入『${cur.name}』`
|
|
1328
|
+
+ `${cur.total?`${cur.processed}/${cur.total} 段 ${pct}%`:'准备中…'}${more}</div>`
|
|
1329
|
+
+ `<div class="ing-hint">已转入后台,可关闭本窗或继续添加文件;完成后星球自动刷新</div>`;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function startIngestPoll(){
|
|
1333
|
+
if(_ingPoll) return;
|
|
1334
|
+
_ingPoll = setInterval(pollIngestJobs, 1200);
|
|
1335
|
+
}
|
|
1336
|
+
async function pollIngestJobs(){
|
|
1337
|
+
const pending = ingJobs.filter(j=>j.status!=='done'&&j.status!=='error');
|
|
1338
|
+
if(!pending.length){ clearInterval(_ingPoll); _ingPoll=null; renderIngestStatus(); return; }
|
|
1339
|
+
let anyDone=false;
|
|
1340
|
+
for(const j of pending){
|
|
1341
|
+
try{
|
|
1342
|
+
const s = await client._get(`/knowledge/ingest/jobs/${j.jobId}`);
|
|
1343
|
+
j.status=s.status; j.processed=s.processed; j.total=s.total; j.ku=s.ku_count; j.error=s.error;
|
|
1344
|
+
if(s.status==='done'){
|
|
1345
|
+
anyDone=true;
|
|
1346
|
+
if((s.ku_count||0) > 0){
|
|
1347
|
+
toast(`『${j.name}』摄入完成:${s.ku_count} 个知识单元`,'ok');
|
|
1348
|
+
}else{
|
|
1349
|
+
// 上传/解析成功但抽取 0 知识:最常见是 LLM API Key 无效/未配置(如 401),或内容太短
|
|
1350
|
+
toast(`『${j.name}』已上传但未抽取到知识(0 个)。多半是 LLM API Key 无效/未配置——请到「连接设置」重填正确 key 后重传`,'err');
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
else if(s.status==='error'){ toast(`『${j.name}』摄入失败:${(s.error||'').slice(0,60)}`,'err'); }
|
|
1354
|
+
}catch(e){ /* 网络抖动,下轮再试 */ }
|
|
1355
|
+
}
|
|
1356
|
+
renderIngestStatus();
|
|
1357
|
+
if(anyDone) loadData(); // 有完成的就刷新星球
|
|
1358
|
+
ingJobs = ingJobs.filter(j=> j.status!=='done' && j.status!=='error');
|
|
1359
|
+
if(!ingJobs.length){ clearInterval(_ingPoll); _ingPoll=null; renderIngestStatus(); }
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// 「清空知识库」已从前端移除(危险的破坏性操作不该是任何人可点的按钮)。
|
|
1363
|
+
// 需要清库改走后端 API:POST /knowledge/reset(设了 HKC_API_KEY 时需带 X-API-Key),
|
|
1364
|
+
// 例:curl -X POST http://127.0.0.1:8000/knowledge/reset -H "X-API-Key: <key>"
|
|
1365
|
+
|
|
1366
|
+
async function submitIngest(){
|
|
1367
|
+
const text=$('#ingest-text').value.trim();
|
|
1368
|
+
const url=$('#ingest-url').value.trim();
|
|
1369
|
+
const title=$('#ingest-title').value.trim();
|
|
1370
|
+
const domain=$('#ingest-domain').value;
|
|
1371
|
+
const hasFiles=pickedFiles.length>0;
|
|
1372
|
+
|
|
1373
|
+
if(!text && !url && !hasFiles){ toast('请输入文本、网址或选择文件','err'); return; }
|
|
1374
|
+
|
|
1375
|
+
const btn=$('#btn-ingest-submit');
|
|
1376
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 摄入中';
|
|
1377
|
+
|
|
1378
|
+
if(client.demo){
|
|
1379
|
+
btn.disabled=false; btn.textContent='开始摄入';
|
|
1380
|
+
toast('未连接后端,无法摄入。请点左下「连接设置」连接 hkc-api 后再试','err');
|
|
1381
|
+
// 自动打开连接设置,引导用户
|
|
1382
|
+
closeModal('#ingest-modal');
|
|
1383
|
+
openModal('#settings-modal');
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// 1) 文本(若有)——通常较短,走同步端点,拿到结果即刷新
|
|
1388
|
+
if(text){
|
|
1389
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 摄入文本';
|
|
1390
|
+
try{
|
|
1391
|
+
const res=await client._post('/knowledge/ingest/text',{text,source_title:title,domain});
|
|
1392
|
+
const ku=res.ku_count||0;
|
|
1393
|
+
toast(ku>0?`文本摄入完成:${ku} 个知识单元`:'文本已摄入,但未抽取到知识(内容太短或模型未配置好)', ku>0?'ok':'err');
|
|
1394
|
+
$('#ingest-text').value='';
|
|
1395
|
+
if(ku>0) await loadData();
|
|
1396
|
+
}catch(e){ toast('文本摄入失败','err'); }
|
|
1397
|
+
btn.disabled=false; btn.textContent='开始摄入';
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// 1.5) 网页 URL——同步抓取正文 + 抽取(单页通常较短)
|
|
1401
|
+
if(url){
|
|
1402
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 抓取网页';
|
|
1403
|
+
try{
|
|
1404
|
+
const res=await client._post('/knowledge/ingest/url',{url,source_title:title,domain});
|
|
1405
|
+
const ku=res.ku_count||0;
|
|
1406
|
+
toast(ku>0?`网页摄入完成:${ku} 个知识单元`:'网页已抓取,但未抽取到知识(正文太短/抓取失败/模型未配置)', ku>0?'ok':'err');
|
|
1407
|
+
$('#ingest-url').value='';
|
|
1408
|
+
if(ku>0) await loadData();
|
|
1409
|
+
}catch(e){
|
|
1410
|
+
const reason=(e&&e.detail)?String(e.detail).slice(0,60):'抓取或摄入失败';
|
|
1411
|
+
toast(`网页摄入失败:${reason}`,'err');
|
|
1412
|
+
}
|
|
1413
|
+
btn.disabled=false; btn.textContent='开始摄入';
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// 2) 文件——异步提交,秒返回 job_id,转后台;不阻塞,可继续添加/上传下一个
|
|
1417
|
+
if(hasFiles){
|
|
1418
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 提交中';
|
|
1419
|
+
let submitted=0;
|
|
1420
|
+
for(const f of pickedFiles.slice()){
|
|
1421
|
+
try{
|
|
1422
|
+
const fd=new FormData();
|
|
1423
|
+
fd.append('file', f, f.name);
|
|
1424
|
+
fd.append('source_title', title); // 留空时后端用文件名
|
|
1425
|
+
fd.append('domain', domain);
|
|
1426
|
+
const res=await client._postForm('/knowledge/ingest/file/async', fd);
|
|
1427
|
+
ingJobs.push({jobId:res.job_id, name:f.name, status:'queued', processed:0, total:0, ku:0, error:''});
|
|
1428
|
+
submitted++;
|
|
1429
|
+
}catch(e){
|
|
1430
|
+
const reason=(e&&e.detail)?String(e.detail).slice(0,50):'提交失败';
|
|
1431
|
+
toast(`『${f.name}』提交失败:${reason}`,'err');
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
btn.disabled=false; btn.textContent='开始摄入';
|
|
1435
|
+
if(submitted>0){
|
|
1436
|
+
pickedFiles=[]; renderFileList(); // 已转后台,清空选择区
|
|
1437
|
+
$('#ingest-title').value='';
|
|
1438
|
+
toast(`${submitted} 个文件已转入后台摄入,可继续添加或关闭窗口;完成后自动刷新`,'ok');
|
|
1439
|
+
startIngestPoll();
|
|
1440
|
+
renderIngestStatus();
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// ════════════════════════════════════════════════════════
|
|
1446
|
+
// 模态 + 连接
|
|
1447
|
+
// ════════════════════════════════════════════════════════
|
|
1448
|
+
// 打开摄入模态 → 停星球渲染(避免大文件上传被 GPU/主线程争用拖慢)
|
|
1449
|
+
function openModal(s){
|
|
1450
|
+
$(s).classList.add('show');
|
|
1451
|
+
if(s === '#ingest-modal') stopRotation();
|
|
1452
|
+
}
|
|
1453
|
+
// 关闭摄入模态 → 恢复星球渲染(idempotent:stopRotation 未调过也无害)
|
|
1454
|
+
function closeModal(s){
|
|
1455
|
+
$(s).classList.remove('show');
|
|
1456
|
+
if(s === '#ingest-modal') startRotation();
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// ── 书籍管理(按来源删除)──
|
|
1460
|
+
async function openBooks(){
|
|
1461
|
+
if(client.demo){ toast('未连接后端,无法管理书籍','err'); openModal('#settings-modal'); return; }
|
|
1462
|
+
openModal('#books-modal');
|
|
1463
|
+
await loadBooks();
|
|
1464
|
+
}
|
|
1465
|
+
async function loadBooks(){
|
|
1466
|
+
const box=$('#books-list');
|
|
1467
|
+
box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">加载中…</div>';
|
|
1468
|
+
try{
|
|
1469
|
+
const r=await client._get('/knowledge/sources');
|
|
1470
|
+
const list=(r&&r.sources)||[];
|
|
1471
|
+
if(!list.length){ box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">暂无已摄入的书籍</div>'; return; }
|
|
1472
|
+
box.innerHTML='';
|
|
1473
|
+
for(const b of list){
|
|
1474
|
+
const row=document.createElement('div');
|
|
1475
|
+
row.style.cssText='display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;margin-bottom:8px;';
|
|
1476
|
+
const name=document.createElement('span');
|
|
1477
|
+
name.style.cssText='font-size:13px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;';
|
|
1478
|
+
name.textContent=(b.name||b.source)+(b.year?` (${b.year})`:'');
|
|
1479
|
+
name.title=b.source;
|
|
1480
|
+
const del=document.createElement('button');
|
|
1481
|
+
del.className='btn btn-ghost btn-sm';
|
|
1482
|
+
del.textContent='删除';
|
|
1483
|
+
del.onclick=()=>deleteBook(b.source, name.textContent);
|
|
1484
|
+
row.appendChild(name); row.appendChild(del);
|
|
1485
|
+
box.appendChild(row);
|
|
1486
|
+
}
|
|
1487
|
+
}catch(e){ box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">加载失败:'+(e.status||e||'')+'</div>'; }
|
|
1488
|
+
}
|
|
1489
|
+
async function deleteBook(source, label){
|
|
1490
|
+
if(!confirm(`确定删除「${label}」吗?\n\n将移除该书及其独占知识;与其他书共享的知识只去掉该书引用。此操作不可撤销。`)) return;
|
|
1491
|
+
try{
|
|
1492
|
+
const r=await client._post('/knowledge/sources/delete',{source});
|
|
1493
|
+
toast(`已删除「${label}」:${r.deleted_kus} 个知识单元`,'ok');
|
|
1494
|
+
await loadBooks();
|
|
1495
|
+
await loadData();
|
|
1496
|
+
}catch(e){ toast('删除失败:'+(e.detail||e.status||e||''),'err'); }
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function setConn(){
|
|
1500
|
+
const p=$('#conn-pill'), t=$('#conn-text');
|
|
1501
|
+
if(client.demo){ p.className='conn-pill demo'; t.textContent='演示模式'; }
|
|
1502
|
+
else{ p.className='conn-pill live'; t.textContent='已连接'; }
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// 连接设置持久化(localStorage):刷新后回填 + 自动重连,避免每次重填 key。
|
|
1506
|
+
const CONN_KEY='hkc_conn_v1';
|
|
1507
|
+
function _connSnapshot(){
|
|
1508
|
+
return {
|
|
1509
|
+
url:$('#api-url').value.trim(),
|
|
1510
|
+
apiKey:$('#api-key').value.trim(),
|
|
1511
|
+
provider:$('#llm-provider').value,
|
|
1512
|
+
key:$('#llm-key').value.trim(),
|
|
1513
|
+
model:$('#llm-model').value.trim(),
|
|
1514
|
+
baseUrl:$('#llm-baseurl').value.trim(),
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function saveConnSettings(){ try{ localStorage.setItem(CONN_KEY, JSON.stringify(_connSnapshot())); }catch(e){} }
|
|
1518
|
+
function loadConnSettings(){ try{ return JSON.parse(localStorage.getItem(CONN_KEY)||'null'); }catch(e){ return null; } }
|
|
1519
|
+
function fillConnFields(s){
|
|
1520
|
+
if(!s) return;
|
|
1521
|
+
if(s.url!=null) $('#api-url').value=s.url;
|
|
1522
|
+
if(s.apiKey!=null){ $('#api-key').value=s.apiKey; client.apiKey=s.apiKey; }
|
|
1523
|
+
if(s.provider) $('#llm-provider').value=s.provider;
|
|
1524
|
+
if(s.key!=null) $('#llm-key').value=s.key;
|
|
1525
|
+
if(s.model!=null) $('#llm-model').value=s.model;
|
|
1526
|
+
if(s.baseUrl!=null) $('#llm-baseurl').value=s.baseUrl;
|
|
1527
|
+
_syncLLMFields();
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
async function tryConnect(){
|
|
1531
|
+
const url=$('#api-url').value.trim();
|
|
1532
|
+
if(!url){ toast('请输入 API 地址','err'); return; }
|
|
1533
|
+
client.apiKey=$('#api-key').value.trim(); // 鉴权 key 须在 /stats、/graph 等请求前就位
|
|
1534
|
+
const btn=$('#btn-connect'); btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 连接中';
|
|
1535
|
+
const ok=await client.connect(url);
|
|
1536
|
+
if(ok){
|
|
1537
|
+
// 连接成功后,把 LLM 配置发给后端(若填了 key)
|
|
1538
|
+
const provider=$('#llm-provider').value;
|
|
1539
|
+
const key=$('#llm-key').value.trim();
|
|
1540
|
+
const model=$('#llm-model').value.trim();
|
|
1541
|
+
const baseUrl=$('#llm-baseurl').value.trim();
|
|
1542
|
+
if(key){
|
|
1543
|
+
try{
|
|
1544
|
+
await client._post('/config/llm', {
|
|
1545
|
+
provider, api_key:key,
|
|
1546
|
+
model: model||null,
|
|
1547
|
+
base_url: baseUrl||null,
|
|
1548
|
+
});
|
|
1549
|
+
toast(`已连接,知识抽取模型:${provider}`,'ok');
|
|
1550
|
+
}catch(e){
|
|
1551
|
+
toast('已连接,但模型配置失败:'+(e.detail||e.status||e),'err');
|
|
1552
|
+
}
|
|
1553
|
+
}else{
|
|
1554
|
+
toast('已连接到 HKC API','ok');
|
|
1555
|
+
}
|
|
1556
|
+
saveConnSettings(); // 连接成功才记住设置(含 key),供刷新后自动重连
|
|
1557
|
+
btn.disabled=false; btn.textContent='连接';
|
|
1558
|
+
setConn(); closeModal('#settings-modal'); await loadData();
|
|
1559
|
+
}else{
|
|
1560
|
+
btn.disabled=false; btn.textContent='连接';
|
|
1561
|
+
toast('连接失败,请检查 hkc-api 是否运行','err');
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// provider 切换:联动 model 占位符 + base_url 显隐
|
|
1566
|
+
function _syncLLMFields(){
|
|
1567
|
+
const p=$('#llm-provider').value;
|
|
1568
|
+
const modelInput=$('#llm-model');
|
|
1569
|
+
const baseField=$('#llm-baseurl-field');
|
|
1570
|
+
const baseInput=$('#llm-baseurl');
|
|
1571
|
+
const defaults={ deepseek:'deepseek-chat', anthropic:'claude-sonnet-4-6', openai:'gpt-4o-mini', 'openai-compatible':'(自定义模型名)' };
|
|
1572
|
+
modelInput.placeholder=defaults[p]||'';
|
|
1573
|
+
// 只有自定义/本地需要填 base_url
|
|
1574
|
+
baseField.style.display=(p==='openai-compatible')?'':'none';
|
|
1575
|
+
// 本地模型默认填 Ollama 地址(后端要求 openai-compatible 必填 base_url,留空会 400);已有值不覆盖
|
|
1576
|
+
if(p==='openai-compatible' && !baseInput.value.trim()){
|
|
1577
|
+
baseInput.value='http://localhost:11434/v1';
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&','<':'<','>':'>','"':'"'}[m])); }
|
|
1582
|
+
|
|
1583
|
+
// ════════════════════════════════════════════════════════
|
|
1584
|
+
// 事件绑定 + 启动
|
|
1585
|
+
// ════════════════════════════════════════════════════════
|
|
1586
|
+
$('#modes').addEventListener('click',e=>{
|
|
1587
|
+
const b=e.target.closest('.mode-btn'); if(!b)return;
|
|
1588
|
+
$$('.mode-btn').forEach(x=>x.classList.remove('on'));
|
|
1589
|
+
b.classList.add('on'); state.mode=b.dataset.mode;
|
|
1590
|
+
});
|
|
1591
|
+
$('#search-input').addEventListener('input',e=>{
|
|
1592
|
+
clearTimeout(searchTimer);
|
|
1593
|
+
searchTimer=setTimeout(()=>doSearch(e.target.value),450);
|
|
1594
|
+
});
|
|
1595
|
+
$('#search-input').addEventListener('keydown',e=>{ if(e.key==='Enter') doSearch(e.target.value); });
|
|
1596
|
+
|
|
1597
|
+
$('#btn-ingest').onclick=()=>openModal('#ingest-modal');
|
|
1598
|
+
$('#btn-books').onclick=openBooks;
|
|
1599
|
+
$('#btn-settings').onclick=()=>openModal('#settings-modal');
|
|
1600
|
+
// 文件/文件夹选择
|
|
1601
|
+
$('#btn-pick-files').onclick=()=>$('#ingest-files').click();
|
|
1602
|
+
$('#btn-pick-folder').onclick=()=>$('#ingest-folder').click();
|
|
1603
|
+
$('#ingest-files').addEventListener('change',e=>{ addFiles(e.target.files); e.target.value=''; });
|
|
1604
|
+
$('#ingest-folder').addEventListener('change',e=>{ addFiles(e.target.files); e.target.value=''; });
|
|
1605
|
+
$('#btn-ingest-submit').onclick=submitIngest;
|
|
1606
|
+
$('#btn-connect').onclick=tryConnect;
|
|
1607
|
+
$('#llm-provider').addEventListener('change', _syncLLMFields);
|
|
1608
|
+
_syncLLMFields(); // 初始化字段状态
|
|
1609
|
+
$('#btn-use-demo').onclick=()=>{ client.useDemo(); setConn(); closeModal('#settings-modal'); loadData(); toast('已切换到演示数据'); };
|
|
1610
|
+
$('#btn-fit').onclick=()=>window._fitGraph&&window._fitGraph();
|
|
1611
|
+
$$('[data-close]').forEach(b=>b.onclick=()=>{ closeModal('#ingest-modal'); closeModal('#settings-modal'); closeModal('#books-modal'); });
|
|
1612
|
+
// 点背景关模态:走 closeModal(里面会触发 ingest 的 startRotation,见 openModal 注释)
|
|
1613
|
+
$$('.modal-bg').forEach(m=>m.addEventListener('click',e=>{ if(e.target===m) closeModal('#'+m.id); }));
|
|
1614
|
+
// G6 的 autoResize:true 已处理窗口缩放,无需手动重建
|
|
1615
|
+
|
|
1616
|
+
// 启动:有保存的连接设置则回填并静默自动重连,否则按演示模式启动
|
|
1617
|
+
async function startup(){
|
|
1618
|
+
const saved=loadConnSettings();
|
|
1619
|
+
if(saved && saved.url){
|
|
1620
|
+
fillConnFields(saved);
|
|
1621
|
+
client.apiKey=saved.apiKey||''; // 鉴权 key 只能取保存的 apiKey,绝不混用模型的 key
|
|
1622
|
+
const ok=await client.connect(saved.url);
|
|
1623
|
+
if(ok){
|
|
1624
|
+
// 不再启动时自动 POST 保存的 LLM 配置:那会把 localStorage 里可能过期的配置
|
|
1625
|
+
// 覆盖到后端、压掉 .env 的权威配置。模型配置只在用户点「连接」时显式应用。
|
|
1626
|
+
setConn(); await loadData(); return;
|
|
1627
|
+
}
|
|
1628
|
+
// 自动重连失败(后端没起/地址变了)→ 回退演示,字段已回填,用户可手动重连
|
|
1629
|
+
toast('已保存的后端未连上,回退演示模式(设置已回填,可在「连接设置」重连)','err');
|
|
1630
|
+
}
|
|
1631
|
+
// 嵌入 Foxclaw(同源部署):无保存设置或保存地址连不上时,优先自动连**当前站点**(相对 /knowledge/*),
|
|
1632
|
+
// 连通即用真实知识;连不上(如独立打开本文件)再回退演示。
|
|
1633
|
+
if(await client.connect('')){ setConn(); await loadData(); return; }
|
|
1634
|
+
setConn();
|
|
1635
|
+
loadData();
|
|
1636
|
+
}
|
|
1637
|
+
startup();
|