foxclaw 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -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 +115 -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 +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -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 +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -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 +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -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 +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -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 +144 -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 +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -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 +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -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 +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -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 +1436 -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-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -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 +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -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 +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -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 +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -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 +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -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,1436 @@
|
|
|
1
|
+
|
|
2
|
+
const KU_COLORS = {
|
|
3
|
+
Entity: '#002FA7', // 克莱因蓝
|
|
4
|
+
Concept: '#2c9678', // 绿
|
|
5
|
+
Fact: '#5a78cc', // 蓝浅
|
|
6
|
+
Claim: '#6bbfa6', // 绿浅
|
|
7
|
+
Evidence: '#001f70', // 蓝深
|
|
8
|
+
Ability: '#1f6e58', // 绿深
|
|
9
|
+
};
|
|
10
|
+
const KU_LABEL = {
|
|
11
|
+
Entity:'实体', Concept:'概念', Fact:'事实',
|
|
12
|
+
Claim:'观点', Evidence:'证据', Ability:'能力'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEMO = {
|
|
16
|
+
kus: {
|
|
17
|
+
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:['查理·芒格']}},
|
|
18
|
+
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:['巴菲特']}},
|
|
19
|
+
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:['格雷厄姆']}},
|
|
20
|
+
CON_01:{ku_id:'CON_01',ku_type:'Concept',name:'价值投资',summary:'以低于内在价值的价格买入优质资产,长期持有。',domain:'Investment',confidence:1.0,status:'active',tags:['方法论'],extra:{definition:'寻找市场价格低于内在价值的证券'}},
|
|
21
|
+
CON_02:{ku_id:'CON_02',ku_type:'Concept',name:'多元思维模型',summary:'用多学科的核心模型交叉分析问题,避免单一视角偏差。',domain:'Investment',confidence:1.0,status:'active',tags:['思维'],extra:{definition:'Mental Models'}},
|
|
22
|
+
CON_03:{ku_id:'CON_03',ku_type:'Concept',name:'安全边际',summary:'买入价与内在价值之间的缓冲,价值投资的核心防御机制。',domain:'Investment',confidence:1.0,status:'active',tags:['估值'],extra:{definition:'Margin of Safety'}},
|
|
23
|
+
CON_04:{ku_id:'CON_04',ku_type:'Concept',name:'护城河',summary:'企业可持续的竞争优势,如品牌、网络效应、转换成本。',domain:'Investment',confidence:1.0,status:'active',tags:['竞争优势'],extra:{definition:'Economic Moat'}},
|
|
24
|
+
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']}},
|
|
25
|
+
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']}},
|
|
26
|
+
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']}},
|
|
27
|
+
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}},
|
|
28
|
+
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}},
|
|
29
|
+
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}},
|
|
30
|
+
ABL_01:{ku_id:'ABL_01',ku_type:'Ability',name:'价值投资者',summary:'',domain:'Investment',confidence:1.0,status:'active',tags:[],extra:{ability_key:'value_investor'}},
|
|
31
|
+
},
|
|
32
|
+
relations:[
|
|
33
|
+
{from:'ENT_01',to:'CON_02',type:'CREATED_BY'},
|
|
34
|
+
{from:'ENT_01',to:'CON_01',type:'USES'},
|
|
35
|
+
{from:'ENT_02',to:'CON_01',type:'USES'},
|
|
36
|
+
{from:'ENT_03',to:'CON_01',type:'CREATED_BY'},
|
|
37
|
+
{from:'ENT_03',to:'CON_03',type:'CREATED_BY'},
|
|
38
|
+
{from:'CON_01',to:'CON_03',type:'DERIVED_FROM'},
|
|
39
|
+
{from:'CON_01',to:'CON_04',type:'DERIVED_FROM'},
|
|
40
|
+
{from:'EVD_01',to:'CLM_01',type:'SUPPORTS'},
|
|
41
|
+
{from:'EVD_02',to:'CLM_01',type:'SUPPORTS'},
|
|
42
|
+
{from:'EVD_02',to:'CLM_02',type:'SUPPORTS'},
|
|
43
|
+
{from:'EVD_03',to:'CLM_03',type:'SUPPORTS'},
|
|
44
|
+
{from:'CLM_02',to:'CLM_03',type:'CONTRADICTS'},
|
|
45
|
+
{from:'ABL_01',to:'CON_01',type:'USES'},
|
|
46
|
+
{from:'EVD_01',to:'ENT_01',type:'MENTIONS'},
|
|
47
|
+
],
|
|
48
|
+
abilities:[
|
|
49
|
+
{ability_key:'value_investor',display_name:'价值投资者',domain:'Investment',
|
|
50
|
+
coverage:{fundamental_analysis:0.82,valuation:0.78,moat_analysis:0.71,financial_statement:0.55},
|
|
51
|
+
can_compile:true,compiled:true},
|
|
52
|
+
{ability_key:'quant_analyst',display_name:'量化分析师',domain:'Investment',
|
|
53
|
+
coverage:{factor_investing:0.45,backtesting:0.30,risk_management:0.62,portfolio_opt:0.51,data_analysis:0.20},
|
|
54
|
+
can_compile:false,missing:['factor_investing','backtesting','data_analysis']},
|
|
55
|
+
{ability_key:'psychology_expert',display_name:'心理学专家',domain:'Psychology',
|
|
56
|
+
coverage:{cognitive_bias:0.68,behavioral_psychology:0.40,social_influence:0.33,decision_making:0.71},
|
|
57
|
+
can_compile:false,missing:['behavioral_psychology','social_influence']},
|
|
58
|
+
],
|
|
59
|
+
conflicts:[
|
|
60
|
+
{conflict_id:'CFT_01',claim_a_id:'CLM_02',claim_b_id:'CLM_03',domain:'Investment',
|
|
61
|
+
status:'open',resolution_strategy:'evidence_weight',resolution_note:''},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
class HKCClient{
|
|
66
|
+
constructor(){ this.base=null; this.demo=true; this.apiKey=''; }
|
|
67
|
+
async connect(url){
|
|
68
|
+
try{
|
|
69
|
+
const r=await fetch(url.replace(/\/$/,'')+'/health',{signal:AbortSignal.timeout(4000)});
|
|
70
|
+
if(!r.ok) throw 0;
|
|
71
|
+
this.base=url.replace(/\/$/,''); this.demo=false; return true;
|
|
72
|
+
}catch(e){ this.demo=true; this.base=null; return false; }
|
|
73
|
+
}
|
|
74
|
+
useDemo(){ this.demo=true; this.base=null; }
|
|
75
|
+
_h(extra){ const h=Object.assign({}, extra||{}); if(this.apiKey) h['X-API-Key']=this.apiKey; return h; }
|
|
76
|
+
async _get(p){ const r=await fetch(this.base+p,{headers:this._h()}); if(!r.ok) throw new Error(r.status); return r.json(); }
|
|
77
|
+
async _post(p,b){ const r=await fetch(this.base+p,{method:'POST',headers:this._h({'Content-Type':'application/json'}),body:JSON.stringify(b)});
|
|
78
|
+
if(!r.ok){ const e=await r.json().catch(()=>({})); throw {status:r.status,detail:e.detail||e.error||e.message}; } return r.json(); }
|
|
79
|
+
async _postForm(p,fd){ const r=await fetch(this.base+p,{method:'POST',headers:this._h(),body:fd}); // 不设 Content-Type,浏览器自动带 multipart boundary
|
|
80
|
+
if(!r.ok){ const e=await r.json().catch(()=>({})); throw {status:r.status,detail:e.detail||e.error||e.message}; } return r.json(); }
|
|
81
|
+
}
|
|
82
|
+
const client=new HKCClient();
|
|
83
|
+
|
|
84
|
+
const state={ kus:{}, relations:[], abilities:[], conflicts:[], selected:null, mode:'hybrid', conflictPick:{}, synth:{}, pathSel:{} };
|
|
85
|
+
const $=s=>document.querySelector(s);
|
|
86
|
+
const $$=s=>document.querySelectorAll(s);
|
|
87
|
+
|
|
88
|
+
function toast(msg,kind=''){
|
|
89
|
+
const t=document.createElement('div');
|
|
90
|
+
t.className='toast '+kind;
|
|
91
|
+
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>`;
|
|
92
|
+
$('#toast-wrap').appendChild(t);
|
|
93
|
+
setTimeout(()=>{t.style.opacity='0';t.style.transition='opacity .3s';setTimeout(()=>t.remove(),300)},2600);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function loadData(){
|
|
97
|
+
if(client.demo){
|
|
98
|
+
state.kus=JSON.parse(JSON.stringify(DEMO.kus));
|
|
99
|
+
state.relations=DEMO.relations.map(r=>({...r}));
|
|
100
|
+
state.abilities=DEMO.abilities.map(a=>({...a}));
|
|
101
|
+
state.conflicts=DEMO.conflicts.map(c=>({...c}));
|
|
102
|
+
}else{
|
|
103
|
+
try{
|
|
104
|
+
const g=await client._get('/knowledge/graph?limit=5000'); // 拉到后端硬上限(默认仅 2000)
|
|
105
|
+
if(!(g.kus && g.kus.length)){
|
|
106
|
+
toast('知识库为空,先展示演示星球;摄入知识后自动显示真实数据','');
|
|
107
|
+
state.kus=JSON.parse(JSON.stringify(DEMO.kus));
|
|
108
|
+
state.relations=DEMO.relations.map(r=>({...r}));
|
|
109
|
+
state.abilities=DEMO.abilities.map(a=>({...a}));
|
|
110
|
+
state.conflicts=DEMO.conflicts.map(c=>({...c}));
|
|
111
|
+
renderAll(); return;
|
|
112
|
+
}
|
|
113
|
+
state.kus={};
|
|
114
|
+
(g.kus||[]).forEach(k=>{ state.kus[k.ku_id]=k; });
|
|
115
|
+
state.relations=(g.relations||[]).map(r=>({
|
|
116
|
+
from:r.from_ku, to:r.to_ku, type:r.rel_type||'RELATED',
|
|
117
|
+
}));
|
|
118
|
+
const ab=await client._get('/abilities').catch(()=>({abilities:[]}));
|
|
119
|
+
state.abilities=[];
|
|
120
|
+
for(const key of (ab.abilities||[])){
|
|
121
|
+
const rep=await client._get(`/abilities/${key}/coverage`).catch(()=>null);
|
|
122
|
+
if(rep) state.abilities.push({ability_key:key,display_name:rep.display_name||key,
|
|
123
|
+
domain:(rep.domains||[]).join(' / '),
|
|
124
|
+
coverage:rep.coverage,can_compile:rep.can_compile,missing:rep.missing_skills});
|
|
125
|
+
}
|
|
126
|
+
const cf=await client._get('/conflicts?status=open').catch(()=>({conflicts:[]}));
|
|
127
|
+
state.conflicts=cf.conflicts||[];
|
|
128
|
+
updateDataInfo().catch(()=>{});
|
|
129
|
+
}catch(e){ toast('数据加载出错,回退演示模式','err'); client.useDemo(); return loadData(); }
|
|
130
|
+
}
|
|
131
|
+
renderAll();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function updateDataInfo(){
|
|
135
|
+
const pill=$('#conn-pill');
|
|
136
|
+
const st=await client._get('/stats');
|
|
137
|
+
const dir=st.data_dir||'(未知)';
|
|
138
|
+
const total=st.total_kus||0; // 全库真实总数
|
|
139
|
+
const emb=st.embedding||''; // 生效的向量后端
|
|
140
|
+
const shown=Object.keys(state.kus).length; // 星球实际渲染数(受 limit 限制)
|
|
141
|
+
const stubEmb=/stub/i.test(emb);
|
|
142
|
+
if(pill){
|
|
143
|
+
let tip=`数据目录:${dir}\n全库知识:${total} 个`;
|
|
144
|
+
if(shown<total) tip+=`\n星球显示:${shown} 个(受上限限制,未全部显示)`;
|
|
145
|
+
if(emb) tip+=`\n向量后端:${emb}${stubEmb?'(无真实语义,语义搜索受限;设 HKC_EMBEDDING=local 启用)':''}`;
|
|
146
|
+
pill.title=tip;
|
|
147
|
+
}
|
|
148
|
+
if(total===0){
|
|
149
|
+
toast(`⚠️ 知识库为空!后端数据目录:${dir}\n若你之前上传过书,很可能是后端连错了目录`,'err');
|
|
150
|
+
}else{
|
|
151
|
+
let msg=`已连接 · 全库 ${total} 个知识`;
|
|
152
|
+
if(shown<total) msg+=` · 星球显示 ${shown} 个(达上限)`;
|
|
153
|
+
toast(msg,'ok');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderAll(){ renderGraph(); renderPanels(); }
|
|
158
|
+
function renderPanels(){
|
|
159
|
+
renderAbilities(); renderConflicts(); renderLegend();
|
|
160
|
+
$('#ability-count').textContent=state.abilities.length+' 项';
|
|
161
|
+
const oc=state.conflicts.filter(c=>c.status==='open').length;
|
|
162
|
+
$('#conflict-count').textContent=oc;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let THREE = null; // 动态加载的 Three.js 命名空间
|
|
166
|
+
let planet = null; // 当前星球实例
|
|
167
|
+
|
|
168
|
+
function disputedOf(k){ return k.extra && k.extra.claim_status==='disputed'; }
|
|
169
|
+
|
|
170
|
+
function nodeRadius(k){
|
|
171
|
+
if(k.ku_type==='Ability') return 7;
|
|
172
|
+
if(k.ku_type==='Entity'||k.ku_type==='Concept') return 6;
|
|
173
|
+
if(k.ku_type==='Claim') return 3.5 + (k.confidence||0.5)*3;
|
|
174
|
+
return 5;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const PLANET_LAYOUT = {
|
|
178
|
+
SHELL_CAPACITY: 800, // 单层舒适容量;超过开始缩点,(未来)超过可分层
|
|
179
|
+
BASE_R_MIN: 160, BASE_R_MAX: 320,
|
|
180
|
+
|
|
181
|
+
compute(n){
|
|
182
|
+
const radius = Math.max(this.BASE_R_MIN, Math.min(this.BASE_R_MAX, 90 + n*11));
|
|
183
|
+
const dotScale = Math.max(0.45, Math.min(1, Math.sqrt(this.SHELL_CAPACITY / Math.max(1, n))));
|
|
184
|
+
const shells = 1; // 预留:未来 Math.ceil(n/SHELL_CAPACITY) 启用分层
|
|
185
|
+
const labelScale = 1; // 预留:未来书多时缩小书名标签
|
|
186
|
+
return { radius, dotScale, shells, labelScale };
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
position(i, n, params, T){
|
|
190
|
+
const golden = Math.PI * (3 - Math.sqrt(5));
|
|
191
|
+
const shells = params.shells || 1;
|
|
192
|
+
const per = Math.ceil(n / shells);
|
|
193
|
+
const shell = Math.floor(i / per);
|
|
194
|
+
const localI = i - shell * per;
|
|
195
|
+
const localN = Math.max(1, Math.min(per, n - shell * per));
|
|
196
|
+
const r = params.radius * (1 - shell * 0.32); // 内层半径递减
|
|
197
|
+
const y = 1 - (localI / Math.max(1, localN - 1)) * 2;
|
|
198
|
+
const rad = Math.sqrt(Math.max(0, 1 - y*y));
|
|
199
|
+
const theta = golden * localI;
|
|
200
|
+
return new T.Vector3(Math.cos(theta)*rad*r, y*r, Math.sin(theta)*rad*r);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
class KnowledgePlanet {
|
|
205
|
+
constructor(container, THREE){
|
|
206
|
+
this.T = THREE;
|
|
207
|
+
this.container = container;
|
|
208
|
+
this.nodes = []; // {id, ku, mesh, glow, vel, pos, radius, label}
|
|
209
|
+
this.edges = []; // {from, to, line, contradicts}
|
|
210
|
+
this.raf = null;
|
|
211
|
+
this.disposed = false;
|
|
212
|
+
this.hovered = null;
|
|
213
|
+
this.selected = null;
|
|
214
|
+
this._initScene();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_initScene(){
|
|
218
|
+
const T = this.T;
|
|
219
|
+
const w = this.container.clientWidth || 700;
|
|
220
|
+
const h = this.container.clientHeight || 600;
|
|
221
|
+
|
|
222
|
+
this.scene = new T.Scene();
|
|
223
|
+
|
|
224
|
+
this.camera = new T.PerspectiveCamera(55, w/h, 1, 4000);
|
|
225
|
+
this.camera.position.set(0, 0, 720);
|
|
226
|
+
|
|
227
|
+
this.renderer = new T.WebGLRenderer({ antialias:true, alpha:true });
|
|
228
|
+
this.renderer.setSize(w, h);
|
|
229
|
+
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio||1, 2));
|
|
230
|
+
this.renderer.setClearColor(0x000000, 0); // 透明,露出 CSS 星空背景
|
|
231
|
+
this.container.appendChild(this.renderer.domElement);
|
|
232
|
+
this.canvas = this.renderer.domElement;
|
|
233
|
+
this.canvas.style.display = 'block';
|
|
234
|
+
|
|
235
|
+
this.scene.add(new T.AmbientLight(0xffffff, 0.75));
|
|
236
|
+
const l1 = new T.PointLight(0x9fe8d2, 0.9, 0); l1.position.set(200, 180, 300); this.scene.add(l1);
|
|
237
|
+
const l2 = new T.PointLight(0x7d97ff, 0.7, 0); l2.position.set(-220, -120, 160); this.scene.add(l2);
|
|
238
|
+
|
|
239
|
+
this.world = new T.Group();
|
|
240
|
+
this.scene.add(this.world);
|
|
241
|
+
|
|
242
|
+
this._initControls();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_initControls(){
|
|
246
|
+
this.rot = { x: 0, y: 0 }; // 目标旋转
|
|
247
|
+
this.rotCur = { x: 0, y: 0 }; // 当前(缓动)
|
|
248
|
+
this.autoSpin = true;
|
|
249
|
+
this.camDist = 720;
|
|
250
|
+
this.camDistTarget = 720;
|
|
251
|
+
let dragging = false, lastX = 0, lastY = 0;
|
|
252
|
+
|
|
253
|
+
const el = this.canvas;
|
|
254
|
+
el.style.cursor = 'grab';
|
|
255
|
+
|
|
256
|
+
this._onDown = (e)=>{ dragging = true; this.dragging = true; lastX = e.clientX; lastY = e.clientY; el.style.cursor='grabbing';
|
|
257
|
+
this._downX = e.clientX; this._downY = e.clientY; // 记录起点,用于区分"拖动"与"点击"
|
|
258
|
+
this.rot.x = this.rotCur.x; this.rot.y = this.rotCur.y; this.autoSpin = false;
|
|
259
|
+
this._flyTarget = null; };
|
|
260
|
+
this._onMove = (e)=>{
|
|
261
|
+
if(!dragging) return;
|
|
262
|
+
const dx = e.clientX - lastX, dy = e.clientY - lastY;
|
|
263
|
+
lastX = e.clientX; lastY = e.clientY;
|
|
264
|
+
this.rot.y += dx * 0.006;
|
|
265
|
+
this.rot.x += dy * 0.006; // 不再钳制俯仰:可自由翻转,把任意节点(含两极)拉到正面
|
|
266
|
+
};
|
|
267
|
+
this._onUp = ()=>{ dragging = false; this.dragging = false; el.style.cursor='grab'; };
|
|
268
|
+
this._onWheel = (e)=>{
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
this.camDistTarget += e.deltaY * 0.5;
|
|
271
|
+
this.camDistTarget = Math.max(250, Math.min(1500, this.camDistTarget));
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
el.addEventListener('pointerdown', this._onDown);
|
|
275
|
+
window.addEventListener('pointermove', this._onMove);
|
|
276
|
+
window.addEventListener('pointerup', this._onUp);
|
|
277
|
+
el.addEventListener('wheel', this._onWheel, { passive:false });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
build(kus, relations){
|
|
281
|
+
const T = this.T;
|
|
282
|
+
const ids = Object.keys(kus);
|
|
283
|
+
const n = ids.length;
|
|
284
|
+
if(!n) return;
|
|
285
|
+
|
|
286
|
+
const _h = s => {
|
|
287
|
+
let h = 2166136261 >>> 0;
|
|
288
|
+
for(let k=0;k<s.length;k++){ h ^= s.charCodeAt(k); h = Math.imul(h, 16777619); }
|
|
289
|
+
h ^= h >>> 13; h = Math.imul(h, 0x5bd1e995); h ^= h >>> 15;
|
|
290
|
+
return h >>> 0;
|
|
291
|
+
};
|
|
292
|
+
ids.sort((a,b)=> _h(a) - _h(b));
|
|
293
|
+
|
|
294
|
+
const params = PLANET_LAYOUT.compute(n);
|
|
295
|
+
this._layoutParams = params; // 供 _makeLabel 读取 labelScale 等
|
|
296
|
+
|
|
297
|
+
ids.forEach((id, i)=>{
|
|
298
|
+
const ku = kus[id];
|
|
299
|
+
const radius = nodeRadius(ku) * params.dotScale; // 圆点随节点总数自适应缩小
|
|
300
|
+
const pos = PLANET_LAYOUT.position(i, n, params, T);
|
|
301
|
+
|
|
302
|
+
const color = new T.Color(KU_COLORS[ku.ku_type] || '#888');
|
|
303
|
+
const geo = new T.SphereGeometry(radius, 24, 24);
|
|
304
|
+
const mat = new T.MeshStandardMaterial({
|
|
305
|
+
color, emissive: color.clone().multiplyScalar(0.35),
|
|
306
|
+
roughness: 0.45, metalness: 0.15,
|
|
307
|
+
});
|
|
308
|
+
const mesh = new T.Mesh(geo, mat);
|
|
309
|
+
mesh.position.copy(pos);
|
|
310
|
+
mesh.userData.id = id;
|
|
311
|
+
this.world.add(mesh);
|
|
312
|
+
|
|
313
|
+
const glowMat = new T.MeshBasicMaterial({ color, transparent:true, opacity:0.12, side:T.BackSide });
|
|
314
|
+
const glow = new T.Mesh(new T.SphereGeometry(radius*1.6, 20, 20), glowMat);
|
|
315
|
+
mesh.add(glow);
|
|
316
|
+
|
|
317
|
+
if(disputedOf(ku)){
|
|
318
|
+
const ring = new T.Mesh(
|
|
319
|
+
new T.SphereGeometry(radius*1.35, 20, 20),
|
|
320
|
+
new T.MeshBasicMaterial({ color:0xb0413e, transparent:true, opacity:0.3, side:T.BackSide })
|
|
321
|
+
);
|
|
322
|
+
mesh.add(ring);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.nodes.push({
|
|
326
|
+
id, ku, mesh, glow, radius,
|
|
327
|
+
pos: pos.clone(),
|
|
328
|
+
vel: new T.Vector3(),
|
|
329
|
+
basePos: pos.clone(),
|
|
330
|
+
baseColor: color.clone(), // 复用,避免每帧 new T.Color
|
|
331
|
+
phase: Math.random()*Math.PI*2, // 漂浮相位
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const label = this._makeLabel(ku.name);
|
|
335
|
+
label.position.set(0, -(radius+9), 0);
|
|
336
|
+
mesh.add(label);
|
|
337
|
+
const _nd = this.nodes[this.nodes.length-1];
|
|
338
|
+
_nd.label = label;
|
|
339
|
+
_nd.isBook = (ku.ku_type === 'Evidence');
|
|
340
|
+
_nd.deg = 0; // 连接数,建边后回填(作重要度)
|
|
341
|
+
_nd._revealLabel = false;
|
|
342
|
+
label.visible = _nd.isBook;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const nodeById = {};
|
|
346
|
+
this.nodes.forEach(nd=> nodeById[nd.id] = nd);
|
|
347
|
+
(relations||[]).forEach(r=>{
|
|
348
|
+
const a = nodeById[r.from], b = nodeById[r.to];
|
|
349
|
+
if(!a || !b) return;
|
|
350
|
+
const contradicts = r.type === 'CONTRADICTS';
|
|
351
|
+
const mat = new T.LineBasicMaterial({
|
|
352
|
+
color: contradicts ? 0xb0413e : 0x9aa3b8,
|
|
353
|
+
transparent:true, opacity: contradicts ? 0.55 : 0.22,
|
|
354
|
+
});
|
|
355
|
+
const geo = new T.BufferGeometry().setFromPoints([a.pos.clone(), b.pos.clone()]);
|
|
356
|
+
const line = new T.Line(geo, mat);
|
|
357
|
+
line.visible = false; // 默认不画边(大图会糊成一团);点中节点才显示它的连线
|
|
358
|
+
this.world.add(line);
|
|
359
|
+
this.edges.push({ from:a, to:b, line, contradicts });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
this.edges.forEach(e=>{ e.from.deg++; e.to.deg++; });
|
|
363
|
+
|
|
364
|
+
this._buildPicking();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_buildPicking(){
|
|
368
|
+
const T = this.T;
|
|
369
|
+
this.raycaster = new T.Raycaster();
|
|
370
|
+
this.pointer = new T.Vector2();
|
|
371
|
+
this._meshes = this.nodes.map(n=>n.mesh);
|
|
372
|
+
|
|
373
|
+
this._onPointerMove = (e)=>{
|
|
374
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
375
|
+
this.pointer.x = ((e.clientX - rect.left)/rect.width)*2 - 1;
|
|
376
|
+
this.pointer.y = -((e.clientY - rect.top)/rect.height)*2 + 1;
|
|
377
|
+
};
|
|
378
|
+
this._onClick = (e)=>{
|
|
379
|
+
const moved = Math.hypot(e.clientX - (this._downX||0), e.clientY - (this._downY||0));
|
|
380
|
+
if(moved > 6) return;
|
|
381
|
+
const hit = this._pick();
|
|
382
|
+
if(hit){ if(window.selectKU) window.selectKU(hit.id); }
|
|
383
|
+
else { if(window.clearSelection) window.clearSelection(); }
|
|
384
|
+
};
|
|
385
|
+
this.canvas.addEventListener('pointermove', this._onPointerMove);
|
|
386
|
+
this.canvas.addEventListener('click', this._onClick);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
_pick(){
|
|
390
|
+
if(!this.raycaster) return null;
|
|
391
|
+
this.raycaster.setFromCamera(this.pointer, this.camera);
|
|
392
|
+
const hits = this.raycaster.intersectObjects(this._meshes, false);
|
|
393
|
+
if(hits.length){
|
|
394
|
+
const id = hits[0].object.userData.id;
|
|
395
|
+
return this.nodes.find(n=>n.id===id);
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
start(){
|
|
401
|
+
const T = this.T;
|
|
402
|
+
const tick = ()=>{
|
|
403
|
+
if(this.disposed) return;
|
|
404
|
+
this.raf = requestAnimationFrame(tick);
|
|
405
|
+
this._update();
|
|
406
|
+
this.renderer.render(this.scene, this.camera);
|
|
407
|
+
};
|
|
408
|
+
tick();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
_update(){
|
|
412
|
+
const T = this.T;
|
|
413
|
+
const t = performance.now() * 0.001;
|
|
414
|
+
|
|
415
|
+
if(this.autoSpin) this.rot.y += 0.0011;
|
|
416
|
+
|
|
417
|
+
const tiltX = this.autoSpin ? Math.sin(t*0.045)*1.35 : this.rot.x;
|
|
418
|
+
const rotEase = this.dragging ? 0.85 : 0.08;
|
|
419
|
+
this.rotCur.x += (tiltX - this.rotCur.x) * rotEase;
|
|
420
|
+
this.rotCur.y += (this.rot.y - this.rotCur.y) * rotEase;
|
|
421
|
+
this.world.rotation.x = this.rotCur.x;
|
|
422
|
+
this.world.rotation.y = this.rotCur.y;
|
|
423
|
+
|
|
424
|
+
this.camDist += (this.camDistTarget - this.camDist) * 0.1;
|
|
425
|
+
this.camera.position.z = this.camDist;
|
|
426
|
+
|
|
427
|
+
if(this._flyTarget){
|
|
428
|
+
const nd = this._flyTarget;
|
|
429
|
+
const p = nd.pos;
|
|
430
|
+
const targetRotY = -Math.atan2(p.x, p.z);
|
|
431
|
+
const targetRotX = Math.atan2(p.y, Math.sqrt(p.x*p.x+p.z*p.z));
|
|
432
|
+
this.rot.y = targetRotY;
|
|
433
|
+
this.rot.x = targetRotX; // 不钳制:点击两极节点也能完整转到正面中心
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const nodes = this.nodes;
|
|
437
|
+
nodes.forEach(nd=>{
|
|
438
|
+
let tx, ty, tz;
|
|
439
|
+
if(nd.focusPos){
|
|
440
|
+
tx = nd.focusPos.x; ty = nd.focusPos.y; tz = nd.focusPos.z;
|
|
441
|
+
}else{
|
|
442
|
+
const bx = Math.sin(t*0.5 + nd.phase)*1.0;
|
|
443
|
+
const by = Math.cos(t*0.4 + nd.phase)*1.0;
|
|
444
|
+
tx = nd.basePos.x + bx; ty = nd.basePos.y + by; tz = nd.basePos.z;
|
|
445
|
+
}
|
|
446
|
+
const cur = nd.mesh.position;
|
|
447
|
+
const ease = nd.focusPos ? 0.10 : 0.06;
|
|
448
|
+
cur.x += (tx - cur.x)*ease;
|
|
449
|
+
cur.y += (ty - cur.y)*ease;
|
|
450
|
+
cur.z += (tz - cur.z)*ease;
|
|
451
|
+
nd.pos.copy(cur);
|
|
452
|
+
|
|
453
|
+
const targetEmissive = nd.glowOn ? 1.0 : 0.35;
|
|
454
|
+
nd._emi = (nd._emi===undefined?0.35:nd._emi) + (targetEmissive - (nd._emi||0.35))*0.12;
|
|
455
|
+
if(nd.mesh.material && nd.mesh.material.emissive && nd.baseColor){
|
|
456
|
+
nd.mesh.material.emissive.copy(nd.baseColor).multiplyScalar(nd._emi);
|
|
457
|
+
}
|
|
458
|
+
const targetOpacity = (nd.dim ? 0.18 : 1.0);
|
|
459
|
+
nd._op = (nd._op===undefined?1:nd._op) + (targetOpacity - (nd._op||1))*0.12;
|
|
460
|
+
nd.mesh.material.opacity = nd._op;
|
|
461
|
+
nd.mesh.material.transparent = nd._op < 0.99;
|
|
462
|
+
if(nd.label){
|
|
463
|
+
const forced = (nd===this.hovered || nd.id===this.selected);
|
|
464
|
+
const show = forced || (this.selected ? nd._revealLabel : nd.isBook);
|
|
465
|
+
nd.label.visible = show;
|
|
466
|
+
if(show) nd.label.material.opacity = nd._op;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
this.edges.forEach(e=>{
|
|
471
|
+
if(!e.line.visible) return;
|
|
472
|
+
const arr = e.line.geometry.attributes.position.array;
|
|
473
|
+
arr[0]=e.from.pos.x; arr[1]=e.from.pos.y; arr[2]=e.from.pos.z;
|
|
474
|
+
arr[3]=e.to.pos.x; arr[4]=e.to.pos.y; arr[5]=e.to.pos.z;
|
|
475
|
+
e.line.geometry.attributes.position.needsUpdate = true;
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const hit = this._pick();
|
|
479
|
+
if(hit !== this.hovered){
|
|
480
|
+
if(this.hovered) this._setHover(this.hovered, false);
|
|
481
|
+
if(hit) this._setHover(hit, true);
|
|
482
|
+
this.hovered = hit;
|
|
483
|
+
this.canvas.style.cursor = hit ? 'pointer' : 'grab';
|
|
484
|
+
}
|
|
485
|
+
this.nodes.forEach(nd=>{
|
|
486
|
+
const target = (nd===this.hovered || nd.id===this.selected) ? 1.5 : 1.0;
|
|
487
|
+
const s = nd.mesh.scale.x + (target - nd.mesh.scale.x)*0.18;
|
|
488
|
+
nd.mesh.scale.set(s,s,s);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
_setHover(nd, on){ /* 状态切换占位,scale 在 _update 平滑处理 */ }
|
|
493
|
+
|
|
494
|
+
_makeLabel(text){
|
|
495
|
+
const T = this.T;
|
|
496
|
+
const label = (text||'').length>12 ? text.slice(0,11)+'…' : (text||'');
|
|
497
|
+
const cv = document.createElement('canvas');
|
|
498
|
+
const ctx = cv.getContext('2d');
|
|
499
|
+
const fs = 48;
|
|
500
|
+
ctx.font = `600 ${fs}px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif`;
|
|
501
|
+
const w = Math.ceil(ctx.measureText(label).width) + 24;
|
|
502
|
+
cv.width = w; cv.height = fs+24;
|
|
503
|
+
ctx.font = `600 ${fs}px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif`;
|
|
504
|
+
ctx.textAlign='center'; ctx.textBaseline='middle';
|
|
505
|
+
ctx.fillStyle='rgba(255,255,255,.92)';
|
|
506
|
+
ctx.shadowColor='rgba(0,0,0,.85)'; ctx.shadowBlur=8;
|
|
507
|
+
ctx.fillText(label, cv.width/2, cv.height/2);
|
|
508
|
+
const tex = new T.CanvasTexture(cv);
|
|
509
|
+
tex.minFilter = T.LinearFilter;
|
|
510
|
+
const mat = new T.SpriteMaterial({ map:tex, transparent:true, depthWrite:false });
|
|
511
|
+
const sp = new T.Sprite(mat);
|
|
512
|
+
const scale = 0.22 * (this._layoutParams ? this._layoutParams.labelScale : 1); // 标签自适应缩放接入点
|
|
513
|
+
sp.scale.set(cv.width*scale, cv.height*scale, 1);
|
|
514
|
+
return sp;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
flyTo(id){
|
|
518
|
+
const nd = this.nodes.find(n=>n.id===id);
|
|
519
|
+
if(!nd) return;
|
|
520
|
+
this.autoSpin = false;
|
|
521
|
+
this._flyTarget = nd;
|
|
522
|
+
this.camDistTarget = 380;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
setSelected(id){
|
|
526
|
+
this.selected = id;
|
|
527
|
+
if(id) this.focusOn(id);
|
|
528
|
+
else this.clearFocus();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
focusOn(id){
|
|
532
|
+
const T = this.T;
|
|
533
|
+
const center = this.nodes.find(n=>n.id===id);
|
|
534
|
+
if(!center) return;
|
|
535
|
+
this.autoSpin = false;
|
|
536
|
+
this._flyTarget = center;
|
|
537
|
+
this.camDistTarget = 420;
|
|
538
|
+
|
|
539
|
+
const relatedIds = new Set([id]);
|
|
540
|
+
this.edges.forEach(e=>{
|
|
541
|
+
const on = (e.from.id===id || e.to.id===id);
|
|
542
|
+
e.line.visible = on;
|
|
543
|
+
if(on){ relatedIds.add(e.from.id); relatedIds.add(e.to.id); }
|
|
544
|
+
});
|
|
545
|
+
const related = this.nodes.filter(n=>relatedIds.has(n.id) && n.id!==id);
|
|
546
|
+
|
|
547
|
+
this.nodes.forEach(n=> n._revealLabel=false);
|
|
548
|
+
related.slice().sort((a,b)=> (b.deg||0)-(a.deg||0)).slice(0, 6)
|
|
549
|
+
.forEach(n=> n._revealLabel=true);
|
|
550
|
+
|
|
551
|
+
center.focusPos = new T.Vector3(0, 0, 0);
|
|
552
|
+
center.glowOn = true; center.dim = false;
|
|
553
|
+
|
|
554
|
+
const R = 95; // 汇聚半径
|
|
555
|
+
const n = related.length;
|
|
556
|
+
related.forEach((nd, i)=>{
|
|
557
|
+
const ang = (i / Math.max(1,n)) * Math.PI*2;
|
|
558
|
+
const tilt = 0.5;
|
|
559
|
+
nd.focusPos = new T.Vector3(
|
|
560
|
+
Math.cos(ang)*R,
|
|
561
|
+
Math.sin(ang)*R*tilt,
|
|
562
|
+
Math.sin(ang)*R*0.8
|
|
563
|
+
);
|
|
564
|
+
nd.glowOn = true; nd.dim = false;
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
this.nodes.forEach(nd=>{
|
|
568
|
+
if(!relatedIds.has(nd.id)){
|
|
569
|
+
nd.focusPos = nd.basePos.clone().multiplyScalar(2.4); // 退远
|
|
570
|
+
nd.glowOn = false; nd.dim = true;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
clearFocus(){
|
|
576
|
+
this._flyTarget = null;
|
|
577
|
+
this.camDistTarget = 720;
|
|
578
|
+
this.autoSpin = true;
|
|
579
|
+
this.nodes.forEach(nd=>{
|
|
580
|
+
nd.focusPos = null;
|
|
581
|
+
nd.glowOn = false;
|
|
582
|
+
nd.dim = false;
|
|
583
|
+
nd._revealLabel = false; // 回到默认态:只剩一级书名标签
|
|
584
|
+
});
|
|
585
|
+
this.edges.forEach(e=>{ e.line.visible = false; }); // 回到默认:不画边
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
highlight(ids){
|
|
589
|
+
const set = new Set(ids || []);
|
|
590
|
+
if(!set.size){ this.setSelected(null); return; } // 无命中→恢复默认视图
|
|
591
|
+
this.autoSpin = false;
|
|
592
|
+
this._flyTarget = null;
|
|
593
|
+
this.selected = '__search__'; // 让标签逻辑走"只显示 _revealLabel(命中项)"分支
|
|
594
|
+
this.nodes.forEach(nd=>{
|
|
595
|
+
const hit = set.has(nd.id);
|
|
596
|
+
nd.focusPos = null; // 原地高亮,不像点击那样汇聚
|
|
597
|
+
nd.glowOn = hit;
|
|
598
|
+
nd.dim = !hit;
|
|
599
|
+
nd._revealLabel = hit; // 命中项显示标签,方便看是哪些
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
resetView(){
|
|
604
|
+
this.rot = { x:0, y:0 }; this.autoSpin = true;
|
|
605
|
+
this.camDistTarget = 720;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
resize(){
|
|
609
|
+
if(this.disposed) return;
|
|
610
|
+
const w = this.container.clientWidth, h = this.container.clientHeight;
|
|
611
|
+
this.camera.aspect = w/h; this.camera.updateProjectionMatrix();
|
|
612
|
+
this.renderer.setSize(w, h);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
dispose(){
|
|
616
|
+
this.disposed = true;
|
|
617
|
+
if(this.raf) cancelAnimationFrame(this.raf);
|
|
618
|
+
|
|
619
|
+
try{
|
|
620
|
+
window.removeEventListener('pointermove', this._onMove);
|
|
621
|
+
window.removeEventListener('pointerup', this._onUp);
|
|
622
|
+
if(this.canvas){
|
|
623
|
+
this.canvas.removeEventListener('pointerdown', this._onDown);
|
|
624
|
+
this.canvas.removeEventListener('wheel', this._onWheel);
|
|
625
|
+
this.canvas.removeEventListener('pointermove', this._onPointerMove);
|
|
626
|
+
this.canvas.removeEventListener('click', this._onClick);
|
|
627
|
+
}
|
|
628
|
+
}catch(e){}
|
|
629
|
+
|
|
630
|
+
try{
|
|
631
|
+
this.scene.traverse(obj=>{
|
|
632
|
+
if(obj.geometry) obj.geometry.dispose();
|
|
633
|
+
if(obj.material){
|
|
634
|
+
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
635
|
+
mats.forEach(m=>{
|
|
636
|
+
if(m.map) m.map.dispose(); // 纹理(label CanvasTexture)
|
|
637
|
+
if(m.dispose) m.dispose();
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}catch(e){}
|
|
642
|
+
|
|
643
|
+
try{
|
|
644
|
+
this.renderer.dispose();
|
|
645
|
+
this.renderer.forceContextLoss();
|
|
646
|
+
if(this.canvas && this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas);
|
|
647
|
+
}catch(e){}
|
|
648
|
+
|
|
649
|
+
this.nodes = []; this.edges = []; this._meshes = null;
|
|
650
|
+
this.scene = null; this.renderer = null; this.camera = null; this.world = null;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function ensureThree(){
|
|
655
|
+
if(THREE) return THREE;
|
|
656
|
+
if(window.THREE){ THREE = window.THREE; return THREE; }
|
|
657
|
+
if(window.__threeLoadError) return null;
|
|
658
|
+
return await new Promise(resolve=>{
|
|
659
|
+
let done = false;
|
|
660
|
+
const onReady = ()=>{ if(done) return; done=true; THREE = window.THREE; resolve(THREE); };
|
|
661
|
+
window.addEventListener('three-ready', onReady, { once:true });
|
|
662
|
+
const t0 = Date.now();
|
|
663
|
+
const poll = setInterval(()=>{
|
|
664
|
+
if(window.THREE){ clearInterval(poll); onReady(); }
|
|
665
|
+
else if(window.__threeLoadError || Date.now()-t0 > 5000){ clearInterval(poll); if(!done){ done=true; resolve(null); } }
|
|
666
|
+
}, 100);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function renderGraph(){
|
|
671
|
+
const container = document.getElementById('graph-canvas');
|
|
672
|
+
if(!container) return;
|
|
673
|
+
|
|
674
|
+
if(planet){ try{ planet.dispose(); }catch(e){} planet = null; }
|
|
675
|
+
Array.from(container.querySelectorAll('canvas')).forEach(c=>c.remove());
|
|
676
|
+
|
|
677
|
+
const T = await ensureThree();
|
|
678
|
+
if(!T){
|
|
679
|
+
container.insertAdjacentHTML('beforeend',
|
|
680
|
+
'<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>');
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if(!Object.keys(state.kus).length) return;
|
|
684
|
+
|
|
685
|
+
planet = new KnowledgePlanet(container, T);
|
|
686
|
+
planet.build(state.kus, state.relations);
|
|
687
|
+
if(state.selected) planet.setSelected(state.selected);
|
|
688
|
+
planet.start();
|
|
689
|
+
window.planet = planet;
|
|
690
|
+
|
|
691
|
+
window._fitGraph = ()=>{ if(planet) planet.resetView(); };
|
|
692
|
+
if(!window._planetResizeBound){
|
|
693
|
+
window.addEventListener('resize', ()=>{ if(planet) planet.resize(); });
|
|
694
|
+
window._planetResizeBound = true;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function startRotation(){
|
|
699
|
+
if(!planet) return;
|
|
700
|
+
if(planet.raf) return; // 已在跑(避免 start() 双重 RAF 嵌套)
|
|
701
|
+
planet.start(); // 复用现有 start() 启动 tick
|
|
702
|
+
}
|
|
703
|
+
function stopRotation(){
|
|
704
|
+
if(!planet) return;
|
|
705
|
+
if(planet.raf){
|
|
706
|
+
cancelAnimationFrame(planet.raf);
|
|
707
|
+
planet.raf = null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function relLabel(t){
|
|
712
|
+
const m={CREATED_BY:'提出',SUPPORTS:'支持',CONTRADICTS:'冲突',MENTIONS:'提及',
|
|
713
|
+
BELONGS_TO:'属于',DERIVED_FROM:'衍生',IMPLEMENTS:'实现',USES:'运用',SUPERSEDES:'取代'};
|
|
714
|
+
return m[t]||t;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function renderLegend(){
|
|
718
|
+
const types=['Entity','Concept','Claim','Evidence','Ability'];
|
|
719
|
+
$('#legend').innerHTML=types.map(t=>
|
|
720
|
+
`<div class="lg-row"><span class="lg-dot" style="background:${KU_COLORS[t]}"></span>${KU_LABEL[t]} ${t}</div>`
|
|
721
|
+
).join('')+`<div class="lg-row" style="border-top:1px solid var(--line-2);padding-top:6px;margin-top:1px">
|
|
722
|
+
<span class="lg-dot" style="background:none;border:2px dashed var(--alert);width:8px;height:8px"></span>观点冲突</div>`;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function selectKU(id){
|
|
726
|
+
if(!state.kus[id]) return;
|
|
727
|
+
state.selected=id;
|
|
728
|
+
if(planet){ try{ planet.setSelected(id); }catch(e){} }
|
|
729
|
+
renderWiki(state.kus[id]);
|
|
730
|
+
hydrateKU(id); // 原文段落等重字段不在星球初始负载里(为提速),点开时按需补全
|
|
731
|
+
loadSynthesis(id); // 若该节点已有缓存综述,自动显示(GET,不调 LLM)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function loadSynthesis(id){
|
|
735
|
+
if(client.demo) return;
|
|
736
|
+
const cur=state.synth[id];
|
|
737
|
+
if(cur && cur.content) return; // 本会话已加载过
|
|
738
|
+
try{
|
|
739
|
+
const r=await client._get('/knowledge/ku/'+encodeURIComponent(id)+'/synthesis');
|
|
740
|
+
if(r && r.has_cache){
|
|
741
|
+
state.synth[id]={content:r.content,model:r.model,generated_at:r.generated_at,stale:false};
|
|
742
|
+
if(state.selected===id) renderWiki(state.kus[id]);
|
|
743
|
+
}
|
|
744
|
+
}catch(e){ /* 无缓存/出错都静默,维持"生成"按钮 */ }
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function genSynthesis(id, force){
|
|
748
|
+
if(client.demo){ toast('演示模式无法生成综述,请先连接后端','err'); return; }
|
|
749
|
+
state.synth[id]='loading';
|
|
750
|
+
if(state.selected===id) renderWiki(state.kus[id]);
|
|
751
|
+
try{
|
|
752
|
+
const r=await client._post('/knowledge/ku/'+encodeURIComponent(id)+'/synthesis'+(force?'?force=true':''), {});
|
|
753
|
+
state.synth[id]={content:r.content,model:r.model,generated_at:r.generated_at,stale:!!r.stale};
|
|
754
|
+
}catch(e){
|
|
755
|
+
state.synth[id]=null;
|
|
756
|
+
toast('综述生成失败:'+(e&&e.message?e.message:'请检查已连接后端并配置了 LLM'),'err');
|
|
757
|
+
}
|
|
758
|
+
if(state.selected===id) renderWiki(state.kus[id]);
|
|
759
|
+
}
|
|
760
|
+
window.genSynthesis=genSynthesis;
|
|
761
|
+
|
|
762
|
+
function setPathNode(id, which){
|
|
763
|
+
state.pathSel=state.pathSel||{};
|
|
764
|
+
state.pathSel[which]=id; state.pathSel.result='';
|
|
765
|
+
if(state.selected) renderWiki(state.kus[state.selected]);
|
|
766
|
+
}
|
|
767
|
+
window.setPathNode=setPathNode;
|
|
768
|
+
async function findPath(){
|
|
769
|
+
const ps=state.pathSel||{};
|
|
770
|
+
if(!ps.from||!ps.to){ toast('请先设起点①和终点②','err'); return; }
|
|
771
|
+
try{
|
|
772
|
+
const r=await client._get('/search/path?from_id='+encodeURIComponent(ps.from)+'&to_id='+encodeURIComponent(ps.to));
|
|
773
|
+
if(r.reachable && (r.path||[]).length){
|
|
774
|
+
const names=r.path.map(id=>(state.kus[id]&&state.kus[id].name)||id);
|
|
775
|
+
ps.result='最短路径('+r.hops+' 跳):'+names.map(esc).join(' → ');
|
|
776
|
+
if(window.planet){ try{ planet.highlight(r.path); }catch(e){} }
|
|
777
|
+
}else{ ps.result='两节点间无连通路径'; }
|
|
778
|
+
}catch(e){ ps.result='路径查询失败(需已连接后端)'; }
|
|
779
|
+
if(state.selected) renderWiki(state.kus[state.selected]);
|
|
780
|
+
}
|
|
781
|
+
window.findPath=findPath;
|
|
782
|
+
|
|
783
|
+
async function hydrateKU(id){
|
|
784
|
+
const ku=state.kus[id];
|
|
785
|
+
if(!ku || client.demo) return; // 演示数据自带原文,无需拉
|
|
786
|
+
const ex=ku.extra||{};
|
|
787
|
+
if(ex._hydrated || (ex.source_text&&ex.source_text.trim())) return;
|
|
788
|
+
try{
|
|
789
|
+
const full=await client._get('/knowledge/ku/'+encodeURIComponent(id));
|
|
790
|
+
ku.extra=Object.assign({}, ku.extra, full.extra||{}, {_hydrated:true});
|
|
791
|
+
if(state.selected===id) renderWiki(ku);
|
|
792
|
+
}catch(e){ /* 拉不到就维持精简视图,不报错 */ }
|
|
793
|
+
}
|
|
794
|
+
function clearSelection(){
|
|
795
|
+
state.selected=null;
|
|
796
|
+
if(planet){ try{ planet.setSelected(null); }catch(e){} }
|
|
797
|
+
$('#wiki-body').innerHTML=emptyWiki();
|
|
798
|
+
}
|
|
799
|
+
window.selectKU = selectKU;
|
|
800
|
+
window.clearSelection = clearSelection;
|
|
801
|
+
|
|
802
|
+
function toggleCollapse(id){ const el=document.getElementById(id); if(el) el.classList.toggle('collapsed'); }
|
|
803
|
+
window.toggleCollapse = toggleCollapse;
|
|
804
|
+
function emptyWiki(){
|
|
805
|
+
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>`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function renderWiki(ku){
|
|
809
|
+
if(!ku){ $('#wiki-body').innerHTML=emptyWiki(); return; }
|
|
810
|
+
const c=KU_COLORS[ku.ku_type];
|
|
811
|
+
const ex=ku.extra||{};
|
|
812
|
+
let html=`<div class="wiki-card">`;
|
|
813
|
+
html+=`<div class="wiki-type" style="color:${c};background:${c}1f">
|
|
814
|
+
<span class="lg-dot" style="background:${c};width:8px;height:8px"></span>${KU_LABEL[ku.ku_type]} · ${ku.ku_type}</div>`;
|
|
815
|
+
html+=`<div class="wiki-title">${esc(ku.name)}</div>`;
|
|
816
|
+
if(ku.domain) html+=`<div class="wiki-domain">${esc(ku.domain)}</div>`;
|
|
817
|
+
|
|
818
|
+
if(ku.ku_type==='Claim'){
|
|
819
|
+
if(ex.statement) html+=`<div class="wiki-summary">${esc(ex.statement)}</div>`;
|
|
820
|
+
const cs=ex.claim_status||'pending';
|
|
821
|
+
html+=`<div class="wiki-section"><div class="conf-row"><span style="font-size:12px;color:var(--ink-2);font-weight:600">置信度</span>
|
|
822
|
+
<span class="status-badge st-${cs}">${cs}</span></div>
|
|
823
|
+
<div class="conf-bar"><div class="conf-fill" style="width:${(ku.confidence*100).toFixed(0)}%"></div></div>
|
|
824
|
+
<div style="text-align:right;margin-top:4px"><span class="conf-val">${(ku.confidence*100).toFixed(0)}%</span></div></div>`;
|
|
825
|
+
} else if(ku.ku_type==='Fact'){
|
|
826
|
+
if(ex.statement) html+=`<div class="wiki-summary">${esc(ex.statement)}</div>`;
|
|
827
|
+
html+=`<div class="wiki-section"><span class="status-badge st-active">已验证事实 · 置信度 100%</span></div>`;
|
|
828
|
+
} else {
|
|
829
|
+
if(ku.summary) html+=`<div class="wiki-summary">${esc(ku.summary)}</div>`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if(!client.demo){
|
|
833
|
+
const sy=state.synth[ku.ku_id];
|
|
834
|
+
html+=`<div class="wiki-section"><h4>AI 综述</h4>`;
|
|
835
|
+
if(sy==='loading'){
|
|
836
|
+
html+=`<div class="synth-loading">正在生成综述…(调用 LLM,请稍候)</div>`;
|
|
837
|
+
}else if(sy && sy.content){
|
|
838
|
+
html+=`<div class="synth-body">${esc(sy.content).replace(/\n/g,'<br>')}</div>`;
|
|
839
|
+
const meta=[sy.model?('模型 '+sy.model):'', sy.stale?'⚠ 知识已更新,建议重新生成':'派生只读视图'].filter(Boolean).join(' · ');
|
|
840
|
+
html+=`<div class="synth-meta">${esc(meta)}</div>
|
|
841
|
+
<button class="synth-btn ghost" onclick="genSynthesis('${ku.ku_id}',true)">重新生成</button>`;
|
|
842
|
+
}else{
|
|
843
|
+
html+=`<button class="synth-btn" onclick="genSynthesis('${ku.ku_id}',false)">✦ 生成 AI 综述</button>
|
|
844
|
+
<div class="synth-hint">把该节点的关联知识用 LLM 组织成连贯词条(只读,不写回知识库)</div>`;
|
|
845
|
+
}
|
|
846
|
+
html+=`</div>`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if(!client.demo){
|
|
850
|
+
const ps=state.pathSel||{};
|
|
851
|
+
const nm=id=>id?esc(state.kus[id]&&state.kus[id].name||id):'(未设)';
|
|
852
|
+
html+=`<div class="wiki-section"><h4>路径查询</h4>
|
|
853
|
+
<div class="path-ctl">
|
|
854
|
+
<button class="synth-btn ghost" onclick="setPathNode('${ku.ku_id}','from')">设为起点①</button>
|
|
855
|
+
<button class="synth-btn ghost" onclick="setPathNode('${ku.ku_id}','to')">设为终点②</button>
|
|
856
|
+
</div>
|
|
857
|
+
<div class="path-meta">① ${nm(ps.from)} → ② ${nm(ps.to)}</div>`;
|
|
858
|
+
if(ps.from && ps.to) html+=`<button class="synth-btn" onclick="findPath()">查最短路径</button>`;
|
|
859
|
+
if(ps.result) html+=`<div class="path-result">${ps.result}</div>`;
|
|
860
|
+
html+=`</div>`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if(ex.source_text && ex.source_text.trim()){
|
|
864
|
+
html+=`<div class="wiki-section"><h4>原文段落</h4><div class="wiki-source">${esc(ex.source_text)}</div></div>`;
|
|
865
|
+
}
|
|
866
|
+
if(ku.ku_type==='Entity'&&ex.aliases&&ex.aliases.length){
|
|
867
|
+
html+=`<div class="wiki-section"><h4>别名</h4><div class="tag-row">${ex.aliases.map(a=>`<span class="tag">${esc(a)}</span>`).join('')}</div></div>`;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const nb={Fact:[],Claim:[],Concept:[],Entity:[],Evidence:[]};
|
|
871
|
+
const seen=new Set([ku.ku_id]);
|
|
872
|
+
getRelations(ku.ku_id).forEach(r=>{
|
|
873
|
+
const o=state.kus[r.other];
|
|
874
|
+
if(!o || seen.has(o.ku_id)) return;
|
|
875
|
+
seen.add(o.ku_id);
|
|
876
|
+
if(nb[o.ku_type]) nb[o.ku_type].push(o);
|
|
877
|
+
});
|
|
878
|
+
if(ku.ku_type!=='Evidence'){
|
|
879
|
+
const names=[ku.name, ...((ex.aliases)||[])].filter(n=>n && n.length>=2);
|
|
880
|
+
if(names.length){
|
|
881
|
+
Object.values(state.kus).forEach(k=>{
|
|
882
|
+
if((k.ku_type==='Fact'||k.ku_type==='Claim') && !seen.has(k.ku_id)){
|
|
883
|
+
const st=(k.extra&&k.extra.statement)||k.summary||'';
|
|
884
|
+
if(names.some(n=>st.indexOf(n)>=0)){ seen.add(k.ku_id); nb[k.ku_type].push(k); }
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const stmt=k=>(k.extra&&k.extra.statement)||k.summary||k.name;
|
|
890
|
+
|
|
891
|
+
if(ku.ku_type==='Evidence'){
|
|
892
|
+
const isColl=(ku.tags||[]).includes('_collection');
|
|
893
|
+
if(isColl){
|
|
894
|
+
const files=(state.relations||[]).filter(r=>r.type==='PART_OF'&&r.to===ku.ku_id).map(r=>state.kus[r.from]).filter(Boolean);
|
|
895
|
+
html+=`<div class="wiki-section"><h4>合集 · 来源</h4><div class="evid-item"><div class="evid-src">📁 ${esc(ku.name)}</div></div></div>`;
|
|
896
|
+
if(files.length){
|
|
897
|
+
html+=`<div class="wiki-section"><h4>包含文件/章节 ${files.length}(点击进入)</h4><div class="kw-row">`;
|
|
898
|
+
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>`; });
|
|
899
|
+
html+=`</div></div>`;
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
html+=`<div class="wiki-section"><h4>来源</h4><div class="evid-item">
|
|
903
|
+
<div class="evid-src">${esc(ex.source||ku.name)}</div>
|
|
904
|
+
<div class="evid-meta">${[ex.author,ex.year,ex.page?'p.'+ex.page:''].filter(Boolean).join(' · ')}</div></div></div>`;
|
|
905
|
+
const inCol=(ku.tags||[]).find(t=>t.startsWith('_incol:'));
|
|
906
|
+
if(inCol){ const col=state.kus[inCol.slice(7)];
|
|
907
|
+
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>`;
|
|
908
|
+
}
|
|
909
|
+
const deg={}; (state.relations||[]).forEach(r=>{ deg[r.from]=(deg[r.from]||0)+1; deg[r.to]=(deg[r.to]||0)+1; });
|
|
910
|
+
const kws=[...nb.Concept,...nb.Entity,...nb.Fact,...nb.Claim].sort((a,b)=>(deg[b.ku_id]||0)-(deg[a.ku_id]||0));
|
|
911
|
+
if(kws.length){
|
|
912
|
+
html+=`<div class="wiki-section"><h4>本文件关键词 ${kws.length}(点击看词条/原文)</h4><div class="kw-row">`;
|
|
913
|
+
kws.slice(0,40).forEach(k=>{
|
|
914
|
+
const nm = k.name.length>16 ? k.name.slice(0,15)+'…' : k.name;
|
|
915
|
+
html+=`<span class="kw-chip" onclick="selectKU('${k.ku_id}')">${esc(nm)}</span>`;
|
|
916
|
+
});
|
|
917
|
+
html+=`</div></div>`;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
} else {
|
|
921
|
+
if(nb.Fact.length){
|
|
922
|
+
let inner='';
|
|
923
|
+
nb.Fact.slice(0,50).forEach(k=>{ inner+=`<div class="wiki-item fact" onclick="selectKU('${k.ku_id}')">${esc(stmt(k))}</div>`; });
|
|
924
|
+
html+=`<div class="wiki-section"><h4>相关事实 ${nb.Fact.length}</h4><div class="wiki-scroll">${inner}</div></div>`;
|
|
925
|
+
}
|
|
926
|
+
if(nb.Claim.length){
|
|
927
|
+
let inner='';
|
|
928
|
+
nb.Claim.slice(0,50).forEach(k=>{
|
|
929
|
+
const conf=Math.round((k.confidence||0)*100);
|
|
930
|
+
inner+=`<div class="wiki-item claim" onclick="selectKU('${k.ku_id}')"><span class="wiki-conf">${conf}%</span>${esc(stmt(k))}</div>`;
|
|
931
|
+
});
|
|
932
|
+
html+=`<div class="wiki-section"><h4>相关观点 ${nb.Claim.length}</h4><div class="wiki-scroll">${inner}</div></div>`;
|
|
933
|
+
}
|
|
934
|
+
const linked=[...nb.Concept,...nb.Entity];
|
|
935
|
+
if(linked.length){
|
|
936
|
+
html+=`<div class="wiki-section"><h4>关联 ${linked.length}</h4>${kwCluster(linked)}</div>`;
|
|
937
|
+
}
|
|
938
|
+
if(nb.Evidence.length){
|
|
939
|
+
html+=`<div class="wiki-section"><h4>出自</h4><div class="kw-row">`;
|
|
940
|
+
nb.Evidence.forEach(k=>{ html+=`<span class="kw-chip" onclick="selectKU('${k.ku_id}')">📖 ${esc(k.name)}</span>`; });
|
|
941
|
+
html+=`</div></div>`;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if(ku.tags&&ku.tags.length){
|
|
946
|
+
html+=`<div class="wiki-section"><h4>标签</h4><div class="tag-row">${ku.tags.map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</div></div>`;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
html+=`</div>`;
|
|
950
|
+
$('#wiki-body').innerHTML=html;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function kwCluster(list, capN){
|
|
954
|
+
const N=Math.min(list.length, capN||80);
|
|
955
|
+
const cx=130, cy=90, R=82, ga=Math.PI*(3-Math.sqrt(5));
|
|
956
|
+
let dots='';
|
|
957
|
+
for(let i=0;i<N;i++){
|
|
958
|
+
const k=list[i];
|
|
959
|
+
const rr=R*Math.sqrt((i+0.5)/N), th=i*ga;
|
|
960
|
+
const x=(cx+rr*Math.cos(th)).toFixed(1), y=(cy+rr*Math.sin(th)).toFixed(1);
|
|
961
|
+
const col=KU_COLORS[k.ku_type]||'#888';
|
|
962
|
+
dots+=`<circle cx="${x}" cy="${y}" r="4" fill="${col}" onclick="selectKU('${k.ku_id}')"><title>${esc(k.name)}</title></circle>`;
|
|
963
|
+
}
|
|
964
|
+
const more=list.length>N?`(显示 ${N}/${list.length})`:'';
|
|
965
|
+
return `<div class="kw-cluster"><svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet">${dots}</svg></div>`
|
|
966
|
+
+ `<div class="kw-cluster-cap">悬停圆点看名称 · 点击进入${more}</div>`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function getRelations(id){
|
|
970
|
+
const out=[];
|
|
971
|
+
state.relations.forEach(r=>{
|
|
972
|
+
if(r.from===id) out.push({other:r.to,type:r.type,dir:'out'});
|
|
973
|
+
else if(r.to===id) out.push({other:r.from,type:r.type,dir:'in'});
|
|
974
|
+
});
|
|
975
|
+
return out;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function renderAbilities(){
|
|
979
|
+
const body=$('#ability-body');
|
|
980
|
+
body.innerHTML=state.abilities.map(a=>{
|
|
981
|
+
const covRows=Object.entries(a.coverage).map(([k,v])=>{
|
|
982
|
+
const pct=(v*100).toFixed(0);
|
|
983
|
+
const low=v<0.5;
|
|
984
|
+
return `<div class="ac-cov-row">
|
|
985
|
+
<span class="ac-cov-label" title="${k}">${k}</span>
|
|
986
|
+
<span class="ac-cov-track"><span class="ac-cov-bar" style="width:${pct}%;${low?'background:var(--klein-l)':''}"></span></span>
|
|
987
|
+
<span class="ac-cov-pct" style="${low?'color:var(--ink-3)':''}">${pct}%</span></div>`;
|
|
988
|
+
}).join('');
|
|
989
|
+
const ready=a.can_compile;
|
|
990
|
+
return `<div class="ability-card">
|
|
991
|
+
<div class="ac-head">
|
|
992
|
+
<div><div class="ac-name">${esc(a.display_name)}</div><div class="ac-domain">${esc(a.domain||'')}</div></div>
|
|
993
|
+
<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>
|
|
994
|
+
</div>
|
|
995
|
+
<div class="ac-cov-list">${covRows}</div>
|
|
996
|
+
<div class="ac-foot">
|
|
997
|
+
<span class="ac-status ${ready?'ac-ready':'ac-locked'}">${ready?'✓ 可编译':'覆盖不足'}</span>
|
|
998
|
+
<button class="btn btn-sm btn-compile" ${ready?'':'disabled'} data-ability="${a.ability_key}">
|
|
999
|
+
${a.compiled?'重新编译':'编译'}</button>
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>`;
|
|
1002
|
+
}).join('')||'<div style="color:var(--ink-3);font-size:13px;padding:20px">暂无可编译能力</div>';
|
|
1003
|
+
|
|
1004
|
+
body.querySelectorAll('.btn-compile').forEach(b=>{
|
|
1005
|
+
b.onclick=()=>compileAbility(b.dataset.ability,b);
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function compileAbility(key,btn){
|
|
1010
|
+
const a=state.abilities.find(x=>x.ability_key===key);
|
|
1011
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span>';
|
|
1012
|
+
if(client.demo){
|
|
1013
|
+
await new Promise(r=>setTimeout(r,800));
|
|
1014
|
+
toast(`已编译能力包 ${a.display_name}.hkap`,'ok');
|
|
1015
|
+
btn.disabled=false; btn.textContent='重新编译'; a.compiled=true;
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
try{
|
|
1019
|
+
await client._post(`/abilities/${key}/compile`,{});
|
|
1020
|
+
toast(`已编译能力包 ${a.display_name}.hkap`,'ok');
|
|
1021
|
+
btn.disabled=false; btn.textContent='重新编译';
|
|
1022
|
+
}catch(e){
|
|
1023
|
+
if(e.status===422){
|
|
1024
|
+
const miss=(e.detail&&e.detail.missing_skills)||[];
|
|
1025
|
+
toast(`覆盖度不足,还缺:${miss.slice(0,3).join('、')}`,'err');
|
|
1026
|
+
}else toast('编译失败','err');
|
|
1027
|
+
btn.disabled=false; btn.textContent='编译';
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function renderConflicts(){
|
|
1032
|
+
const open=state.conflicts.filter(c=>c.status==='open');
|
|
1033
|
+
const body=$('#conflict-body');
|
|
1034
|
+
if(!open.length){
|
|
1035
|
+
body.innerHTML=`<div class="conflict-empty">
|
|
1036
|
+
<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>
|
|
1037
|
+
<p style="font-size:12.5px">暂无待裁决冲突<br><span style="color:var(--ink-3);font-size:11px">知识库观点一致</span></p></div>`;
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
body.innerHTML=open.map(c=>{
|
|
1041
|
+
const a=state.kus[c.claim_a_id], b=state.kus[c.claim_b_id];
|
|
1042
|
+
if(!a||!b)return'';
|
|
1043
|
+
const pick=state.conflictPick[c.conflict_id];
|
|
1044
|
+
return `<div class="conflict-card">
|
|
1045
|
+
<div class="cc-head"><span class="cc-domain">${esc(c.domain||'')}</span>
|
|
1046
|
+
<span style="font-size:10px;color:var(--ink-3);font-weight:600;margin-left:auto">${c.resolution_strategy}</span></div>
|
|
1047
|
+
<div class="cc-claim ${pick===c.claim_a_id?'picked':''}" onclick="pickClaim('${c.conflict_id}','${c.claim_a_id}')">
|
|
1048
|
+
<div class="cc-claim-txt">${esc((a.extra&&a.extra.statement)||a.name)}</div>
|
|
1049
|
+
<div class="cc-claim-conf">置信度 ${(a.confidence*100).toFixed(0)}%</div></div>
|
|
1050
|
+
<div class="cc-vs">对立</div>
|
|
1051
|
+
<div class="cc-claim ${pick===c.claim_b_id?'picked':''}" onclick="pickClaim('${c.conflict_id}','${c.claim_b_id}')">
|
|
1052
|
+
<div class="cc-claim-txt">${esc((b.extra&&b.extra.statement)||b.name)}</div>
|
|
1053
|
+
<div class="cc-claim-conf">置信度 ${(b.confidence*100).toFixed(0)}%</div></div>
|
|
1054
|
+
<div class="cc-resolve">
|
|
1055
|
+
<button class="btn btn-sm btn-compile" ${pick?'':'disabled'} onclick="resolveConflict('${c.conflict_id}')">采纳所选观点</button>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>`;
|
|
1058
|
+
}).join('');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function pickClaim(cid,clmid){
|
|
1062
|
+
state.conflictPick[cid]=clmid;
|
|
1063
|
+
renderConflicts();
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function resolveConflict(cid){
|
|
1067
|
+
const winner=state.conflictPick[cid];
|
|
1068
|
+
if(!winner){ toast('请先选择采纳哪个观点','err'); return; }
|
|
1069
|
+
if(client.demo){
|
|
1070
|
+
await new Promise(r=>setTimeout(r,500));
|
|
1071
|
+
const c=state.conflicts.find(x=>x.conflict_id===cid);
|
|
1072
|
+
if(c) c.status='resolved';
|
|
1073
|
+
const loser=c.claim_a_id===winner?c.claim_b_id:c.claim_a_id;
|
|
1074
|
+
if(state.kus[loser]) state.kus[loser].extra.claim_status='superseded';
|
|
1075
|
+
if(state.kus[winner]) state.kus[winner].extra.claim_status='active';
|
|
1076
|
+
state.relations=state.relations.filter(r=>
|
|
1077
|
+
!(r.type==='CONTRADICTS' &&
|
|
1078
|
+
((r.from===loser&&r.to===winner)||(r.from===winner&&r.to===loser))));
|
|
1079
|
+
delete state.conflictPick[cid];
|
|
1080
|
+
toast('冲突已裁决,落败观点标记为 superseded','ok');
|
|
1081
|
+
renderGraph();
|
|
1082
|
+
renderPanels();
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
try{
|
|
1086
|
+
await client._post(`/conflicts/${cid}/resolve`,{winner_id:winner,note:'UI 裁决',resolved_by:'ui'});
|
|
1087
|
+
toast('冲突已裁决','ok');
|
|
1088
|
+
delete state.conflictPick[cid];
|
|
1089
|
+
await loadData();
|
|
1090
|
+
}catch(e){ toast('裁决失败','err'); }
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
let searchTimer;
|
|
1094
|
+
async function doSearch(q){
|
|
1095
|
+
if(!q.trim()){ if(window.planet) window.planet.setSelected(null); return; } // 清空搜索→恢复默认视图(不重建星球)
|
|
1096
|
+
if(client.demo){
|
|
1097
|
+
const hits=Object.values(state.kus).filter(k=>
|
|
1098
|
+
(k.name+k.summary+((k.extra&&k.extra.statement)||'')).toLowerCase().includes(q.toLowerCase()));
|
|
1099
|
+
highlightNodes(hits.map(h=>h.ku_id));
|
|
1100
|
+
if(hits.length) toast(`找到 ${hits.length} 个匹配(${state.mode})`);
|
|
1101
|
+
else toast('无匹配结果','err');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
try{
|
|
1105
|
+
const res=await client._post('/search',{query:q,mode:state.mode,top_k:15});
|
|
1106
|
+
highlightNodes((res.hits||[]).map(h=>h.ku_id));
|
|
1107
|
+
toast(`找到 ${(res.hits||[]).length} 个结果`);
|
|
1108
|
+
}catch(e){ toast('搜索失败','err'); }
|
|
1109
|
+
}
|
|
1110
|
+
function highlightNodes(ids){
|
|
1111
|
+
if(window.planet) window.planet.highlight(ids || []);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
let pickedFiles = []; // 待摄入文件列表
|
|
1115
|
+
|
|
1116
|
+
function fmtSize(b){
|
|
1117
|
+
if(b<1024) return b+' B';
|
|
1118
|
+
if(b<1024*1024) return (b/1024).toFixed(1)+' KB';
|
|
1119
|
+
return (b/1024/1024).toFixed(1)+' MB';
|
|
1120
|
+
}
|
|
1121
|
+
const ALLOWED_EXT=['.pdf','.md','.markdown','.txt','.epub','.mobi','.azw3','.azw'];
|
|
1122
|
+
function extOK(name){ return ALLOWED_EXT.some(e=>name.toLowerCase().endsWith(e)); }
|
|
1123
|
+
|
|
1124
|
+
function addFiles(fileList){
|
|
1125
|
+
for(const f of fileList){
|
|
1126
|
+
if(!extOK(f.name)) continue; // 跳过不支持格式
|
|
1127
|
+
if(pickedFiles.some(x=>x.name===f.name && x.size===f.size)) continue; // 去重
|
|
1128
|
+
pickedFiles.push(f);
|
|
1129
|
+
}
|
|
1130
|
+
renderFileList();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function renderFileList(){
|
|
1134
|
+
const box=$('#file-list');
|
|
1135
|
+
box.innerHTML='';
|
|
1136
|
+
pickedFiles.forEach((f,i)=>{
|
|
1137
|
+
const row=document.createElement('div');
|
|
1138
|
+
row.className='file-item'; row.dataset.idx=i;
|
|
1139
|
+
row.innerHTML=`<span class="fi-name">${f.name}</span>`+
|
|
1140
|
+
`<span class="fi-size">${fmtSize(f.size)}</span>`+
|
|
1141
|
+
`<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>`;
|
|
1142
|
+
row.querySelector('.fi-del').onclick=()=>{ pickedFiles.splice(i,1); renderFileList(); };
|
|
1143
|
+
box.appendChild(row);
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
let ingJobs = []; // {jobId,name,status,processed,total,ku,error}
|
|
1148
|
+
let _ingPoll = null;
|
|
1149
|
+
|
|
1150
|
+
function renderIngestStatus(){
|
|
1151
|
+
const el=$('#ingest-status'); if(!el) return;
|
|
1152
|
+
const active = ingJobs.filter(j=>j.status!=='done'&&j.status!=='error');
|
|
1153
|
+
if(!active.length){ el.innerHTML=''; return; }
|
|
1154
|
+
const cur = active[0];
|
|
1155
|
+
const pct = cur.total ? Math.round(cur.processed/cur.total*100) : 0;
|
|
1156
|
+
const more = active.length>1 ? ` (+${active.length-1} 个排队)` : '';
|
|
1157
|
+
el.innerHTML =
|
|
1158
|
+
`<div class="ing-line"><span class="spinner"></span> 后台摄入『${cur.name}』`
|
|
1159
|
+
+ `${cur.total?`${cur.processed}/${cur.total} 段 ${pct}%`:'准备中…'}${more}</div>`
|
|
1160
|
+
+ `<div class="ing-hint">已转入后台,可关闭本窗或继续添加文件;完成后星球自动刷新</div>`;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function startIngestPoll(){
|
|
1164
|
+
if(_ingPoll) return;
|
|
1165
|
+
_ingPoll = setInterval(pollIngestJobs, 1200);
|
|
1166
|
+
}
|
|
1167
|
+
async function pollIngestJobs(){
|
|
1168
|
+
const pending = ingJobs.filter(j=>j.status!=='done'&&j.status!=='error');
|
|
1169
|
+
if(!pending.length){ clearInterval(_ingPoll); _ingPoll=null; renderIngestStatus(); return; }
|
|
1170
|
+
let anyDone=false;
|
|
1171
|
+
for(const j of pending){
|
|
1172
|
+
try{
|
|
1173
|
+
const s = await client._get(`/knowledge/ingest/jobs/${j.jobId}`);
|
|
1174
|
+
j.status=s.status; j.processed=s.processed; j.total=s.total; j.ku=s.ku_count; j.error=s.error;
|
|
1175
|
+
if(s.status==='done'){
|
|
1176
|
+
anyDone=true;
|
|
1177
|
+
if((s.ku_count||0) > 0){
|
|
1178
|
+
toast(`『${j.name}』摄入完成:${s.ku_count} 个知识单元`,'ok');
|
|
1179
|
+
}else{
|
|
1180
|
+
toast(`『${j.name}』已上传但未抽取到知识(0 个)。多半是 LLM API Key 无效/未配置——请到「连接设置」重填正确 key 后重传`,'err');
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
else if(s.status==='error'){ toast(`『${j.name}』摄入失败:${(s.error||'').slice(0,60)}`,'err'); }
|
|
1184
|
+
}catch(e){ /* 网络抖动,下轮再试 */ }
|
|
1185
|
+
}
|
|
1186
|
+
renderIngestStatus();
|
|
1187
|
+
if(anyDone) loadData(); // 有完成的就刷新星球
|
|
1188
|
+
ingJobs = ingJobs.filter(j=> j.status!=='done' && j.status!=='error');
|
|
1189
|
+
if(!ingJobs.length){ clearInterval(_ingPoll); _ingPoll=null; renderIngestStatus(); }
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async function submitIngest(){
|
|
1193
|
+
const text=$('#ingest-text').value.trim();
|
|
1194
|
+
const url=$('#ingest-url').value.trim();
|
|
1195
|
+
const title=$('#ingest-title').value.trim();
|
|
1196
|
+
const domain=$('#ingest-domain').value;
|
|
1197
|
+
const hasFiles=pickedFiles.length>0;
|
|
1198
|
+
|
|
1199
|
+
if(!text && !url && !hasFiles){ toast('请输入文本、网址或选择文件','err'); return; }
|
|
1200
|
+
|
|
1201
|
+
const btn=$('#btn-ingest-submit');
|
|
1202
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 摄入中';
|
|
1203
|
+
|
|
1204
|
+
if(client.demo){
|
|
1205
|
+
btn.disabled=false; btn.textContent='开始摄入';
|
|
1206
|
+
toast('未连接后端,无法摄入。请点左下「连接设置」连接 hkc-api 后再试','err');
|
|
1207
|
+
closeModal('#ingest-modal');
|
|
1208
|
+
openModal('#settings-modal');
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if(text){
|
|
1213
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 摄入文本';
|
|
1214
|
+
try{
|
|
1215
|
+
const res=await client._post('/knowledge/ingest/text',{text,source_title:title,domain});
|
|
1216
|
+
const ku=res.ku_count||0;
|
|
1217
|
+
toast(ku>0?`文本摄入完成:${ku} 个知识单元`:'文本已摄入,但未抽取到知识(内容太短或模型未配置好)', ku>0?'ok':'err');
|
|
1218
|
+
$('#ingest-text').value='';
|
|
1219
|
+
if(ku>0) await loadData();
|
|
1220
|
+
}catch(e){ toast('文本摄入失败','err'); }
|
|
1221
|
+
btn.disabled=false; btn.textContent='开始摄入';
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if(url){
|
|
1225
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 抓取网页';
|
|
1226
|
+
try{
|
|
1227
|
+
const res=await client._post('/knowledge/ingest/url',{url,source_title:title,domain});
|
|
1228
|
+
const ku=res.ku_count||0;
|
|
1229
|
+
toast(ku>0?`网页摄入完成:${ku} 个知识单元`:'网页已抓取,但未抽取到知识(正文太短/抓取失败/模型未配置)', ku>0?'ok':'err');
|
|
1230
|
+
$('#ingest-url').value='';
|
|
1231
|
+
if(ku>0) await loadData();
|
|
1232
|
+
}catch(e){
|
|
1233
|
+
const reason=(e&&e.detail)?String(e.detail).slice(0,60):'抓取或摄入失败';
|
|
1234
|
+
toast(`网页摄入失败:${reason}`,'err');
|
|
1235
|
+
}
|
|
1236
|
+
btn.disabled=false; btn.textContent='开始摄入';
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if(hasFiles){
|
|
1240
|
+
btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 提交中';
|
|
1241
|
+
let submitted=0;
|
|
1242
|
+
for(const f of pickedFiles.slice()){
|
|
1243
|
+
try{
|
|
1244
|
+
const fd=new FormData();
|
|
1245
|
+
fd.append('file', f, f.name);
|
|
1246
|
+
fd.append('source_title', title); // 留空时后端用文件名
|
|
1247
|
+
fd.append('domain', domain);
|
|
1248
|
+
const res=await client._postForm('/knowledge/ingest/file/async', fd);
|
|
1249
|
+
ingJobs.push({jobId:res.job_id, name:f.name, status:'queued', processed:0, total:0, ku:0, error:''});
|
|
1250
|
+
submitted++;
|
|
1251
|
+
}catch(e){
|
|
1252
|
+
const reason=(e&&e.detail)?String(e.detail).slice(0,50):'提交失败';
|
|
1253
|
+
toast(`『${f.name}』提交失败:${reason}`,'err');
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
btn.disabled=false; btn.textContent='开始摄入';
|
|
1257
|
+
if(submitted>0){
|
|
1258
|
+
pickedFiles=[]; renderFileList(); // 已转后台,清空选择区
|
|
1259
|
+
$('#ingest-title').value='';
|
|
1260
|
+
toast(`${submitted} 个文件已转入后台摄入,可继续添加或关闭窗口;完成后自动刷新`,'ok');
|
|
1261
|
+
startIngestPoll();
|
|
1262
|
+
renderIngestStatus();
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function openModal(s){
|
|
1268
|
+
$(s).classList.add('show');
|
|
1269
|
+
if(s === '#ingest-modal') stopRotation();
|
|
1270
|
+
}
|
|
1271
|
+
function closeModal(s){
|
|
1272
|
+
$(s).classList.remove('show');
|
|
1273
|
+
if(s === '#ingest-modal') startRotation();
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
async function openBooks(){
|
|
1277
|
+
if(client.demo){ toast('未连接后端,无法管理书籍','err'); openModal('#settings-modal'); return; }
|
|
1278
|
+
openModal('#books-modal');
|
|
1279
|
+
await loadBooks();
|
|
1280
|
+
}
|
|
1281
|
+
async function loadBooks(){
|
|
1282
|
+
const box=$('#books-list');
|
|
1283
|
+
box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">加载中…</div>';
|
|
1284
|
+
try{
|
|
1285
|
+
const r=await client._get('/knowledge/sources');
|
|
1286
|
+
const list=(r&&r.sources)||[];
|
|
1287
|
+
if(!list.length){ box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">暂无已摄入的书籍</div>'; return; }
|
|
1288
|
+
box.innerHTML='';
|
|
1289
|
+
for(const b of list){
|
|
1290
|
+
const row=document.createElement('div');
|
|
1291
|
+
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;';
|
|
1292
|
+
const name=document.createElement('span');
|
|
1293
|
+
name.style.cssText='font-size:13px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;';
|
|
1294
|
+
name.textContent=(b.name||b.source)+(b.year?` (${b.year})`:'');
|
|
1295
|
+
name.title=b.source;
|
|
1296
|
+
const del=document.createElement('button');
|
|
1297
|
+
del.className='btn btn-ghost btn-sm';
|
|
1298
|
+
del.textContent='删除';
|
|
1299
|
+
del.onclick=()=>deleteBook(b.source, name.textContent);
|
|
1300
|
+
row.appendChild(name); row.appendChild(del);
|
|
1301
|
+
box.appendChild(row);
|
|
1302
|
+
}
|
|
1303
|
+
}catch(e){ box.innerHTML='<div style="font-size:13px;color:var(--ink-2);text-align:center;padding:16px">加载失败:'+(e.status||e||'')+'</div>'; }
|
|
1304
|
+
}
|
|
1305
|
+
async function deleteBook(source, label){
|
|
1306
|
+
if(!confirm(`确定删除「${label}」吗?\n\n将移除该书及其独占知识;与其他书共享的知识只去掉该书引用。此操作不可撤销。`)) return;
|
|
1307
|
+
try{
|
|
1308
|
+
const r=await client._post('/knowledge/sources/delete',{source});
|
|
1309
|
+
toast(`已删除「${label}」:${r.deleted_kus} 个知识单元`,'ok');
|
|
1310
|
+
await loadBooks();
|
|
1311
|
+
await loadData();
|
|
1312
|
+
}catch(e){ toast('删除失败:'+(e.detail||e.status||e||''),'err'); }
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function setConn(){
|
|
1316
|
+
const p=$('#conn-pill'), t=$('#conn-text');
|
|
1317
|
+
if(client.demo){ p.className='conn-pill demo'; t.textContent='演示模式'; }
|
|
1318
|
+
else{ p.className='conn-pill live'; t.textContent='已连接'; }
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const CONN_KEY='hkc_conn_v1';
|
|
1322
|
+
function _connSnapshot(){
|
|
1323
|
+
return {
|
|
1324
|
+
url:$('#api-url').value.trim(),
|
|
1325
|
+
apiKey:$('#api-key').value.trim(),
|
|
1326
|
+
provider:$('#llm-provider').value,
|
|
1327
|
+
key:$('#llm-key').value.trim(),
|
|
1328
|
+
model:$('#llm-model').value.trim(),
|
|
1329
|
+
baseUrl:$('#llm-baseurl').value.trim(),
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function saveConnSettings(){ try{ localStorage.setItem(CONN_KEY, JSON.stringify(_connSnapshot())); }catch(e){} }
|
|
1333
|
+
function loadConnSettings(){ try{ return JSON.parse(localStorage.getItem(CONN_KEY)||'null'); }catch(e){ return null; } }
|
|
1334
|
+
function fillConnFields(s){
|
|
1335
|
+
if(!s) return;
|
|
1336
|
+
if(s.url!=null) $('#api-url').value=s.url;
|
|
1337
|
+
if(s.apiKey!=null){ $('#api-key').value=s.apiKey; client.apiKey=s.apiKey; }
|
|
1338
|
+
if(s.provider) $('#llm-provider').value=s.provider;
|
|
1339
|
+
if(s.key!=null) $('#llm-key').value=s.key;
|
|
1340
|
+
if(s.model!=null) $('#llm-model').value=s.model;
|
|
1341
|
+
if(s.baseUrl!=null) $('#llm-baseurl').value=s.baseUrl;
|
|
1342
|
+
_syncLLMFields();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
async function tryConnect(){
|
|
1346
|
+
const url=$('#api-url').value.trim();
|
|
1347
|
+
if(!url){ toast('请输入 API 地址','err'); return; }
|
|
1348
|
+
client.apiKey=$('#api-key').value.trim(); // 鉴权 key 须在 /stats、/graph 等请求前就位
|
|
1349
|
+
const btn=$('#btn-connect'); btn.disabled=true; btn.innerHTML='<span class="spinner"></span> 连接中';
|
|
1350
|
+
const ok=await client.connect(url);
|
|
1351
|
+
if(ok){
|
|
1352
|
+
const provider=$('#llm-provider').value;
|
|
1353
|
+
const key=$('#llm-key').value.trim();
|
|
1354
|
+
const model=$('#llm-model').value.trim();
|
|
1355
|
+
const baseUrl=$('#llm-baseurl').value.trim();
|
|
1356
|
+
if(key){
|
|
1357
|
+
try{
|
|
1358
|
+
await client._post('/config/llm', {
|
|
1359
|
+
provider, api_key:key,
|
|
1360
|
+
model: model||null,
|
|
1361
|
+
base_url: baseUrl||null,
|
|
1362
|
+
});
|
|
1363
|
+
toast(`已连接,知识抽取模型:${provider}`,'ok');
|
|
1364
|
+
}catch(e){
|
|
1365
|
+
toast('已连接,但模型配置失败:'+(e.detail||e.status||e),'err');
|
|
1366
|
+
}
|
|
1367
|
+
}else{
|
|
1368
|
+
toast('已连接到 HKC API','ok');
|
|
1369
|
+
}
|
|
1370
|
+
saveConnSettings(); // 连接成功才记住设置(含 key),供刷新后自动重连
|
|
1371
|
+
btn.disabled=false; btn.textContent='连接';
|
|
1372
|
+
setConn(); closeModal('#settings-modal'); await loadData();
|
|
1373
|
+
}else{
|
|
1374
|
+
btn.disabled=false; btn.textContent='连接';
|
|
1375
|
+
toast('连接失败,请检查 hkc-api 是否运行','err');
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function _syncLLMFields(){
|
|
1380
|
+
const p=$('#llm-provider').value;
|
|
1381
|
+
const modelInput=$('#llm-model');
|
|
1382
|
+
const baseField=$('#llm-baseurl-field');
|
|
1383
|
+
const baseInput=$('#llm-baseurl');
|
|
1384
|
+
const defaults={ deepseek:'deepseek-chat', anthropic:'claude-sonnet-4-6', openai:'gpt-4o-mini', 'openai-compatible':'(自定义模型名)' };
|
|
1385
|
+
modelInput.placeholder=defaults[p]||'';
|
|
1386
|
+
baseField.style.display=(p==='openai-compatible')?'':'none';
|
|
1387
|
+
if(p==='openai-compatible' && !baseInput.value.trim()){
|
|
1388
|
+
baseInput.value='http://localhost:11434/v1';
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&','<':'<','>':'>','"':'"'}[m])); }
|
|
1393
|
+
|
|
1394
|
+
$('#modes').addEventListener('click',e=>{
|
|
1395
|
+
const b=e.target.closest('.mode-btn'); if(!b)return;
|
|
1396
|
+
$$('.mode-btn').forEach(x=>x.classList.remove('on'));
|
|
1397
|
+
b.classList.add('on'); state.mode=b.dataset.mode;
|
|
1398
|
+
});
|
|
1399
|
+
$('#search-input').addEventListener('input',e=>{
|
|
1400
|
+
clearTimeout(searchTimer);
|
|
1401
|
+
searchTimer=setTimeout(()=>doSearch(e.target.value),450);
|
|
1402
|
+
});
|
|
1403
|
+
$('#search-input').addEventListener('keydown',e=>{ if(e.key==='Enter') doSearch(e.target.value); });
|
|
1404
|
+
|
|
1405
|
+
$('#btn-ingest').onclick=()=>openModal('#ingest-modal');
|
|
1406
|
+
$('#btn-books').onclick=openBooks;
|
|
1407
|
+
$('#btn-settings').onclick=()=>openModal('#settings-modal');
|
|
1408
|
+
$('#btn-pick-files').onclick=()=>$('#ingest-files').click();
|
|
1409
|
+
$('#btn-pick-folder').onclick=()=>$('#ingest-folder').click();
|
|
1410
|
+
$('#ingest-files').addEventListener('change',e=>{ addFiles(e.target.files); e.target.value=''; });
|
|
1411
|
+
$('#ingest-folder').addEventListener('change',e=>{ addFiles(e.target.files); e.target.value=''; });
|
|
1412
|
+
$('#btn-ingest-submit').onclick=submitIngest;
|
|
1413
|
+
$('#btn-connect').onclick=tryConnect;
|
|
1414
|
+
$('#llm-provider').addEventListener('change', _syncLLMFields);
|
|
1415
|
+
_syncLLMFields(); // 初始化字段状态
|
|
1416
|
+
$('#btn-use-demo').onclick=()=>{ client.useDemo(); setConn(); closeModal('#settings-modal'); loadData(); toast('已切换到演示数据'); };
|
|
1417
|
+
$('#btn-fit').onclick=()=>window._fitGraph&&window._fitGraph();
|
|
1418
|
+
$$('[data-close]').forEach(b=>b.onclick=()=>{ closeModal('#ingest-modal'); closeModal('#settings-modal'); closeModal('#books-modal'); });
|
|
1419
|
+
$$('.modal-bg').forEach(m=>m.addEventListener('click',e=>{ if(e.target===m) closeModal('#'+m.id); }));
|
|
1420
|
+
|
|
1421
|
+
async function startup(){
|
|
1422
|
+
const saved=loadConnSettings();
|
|
1423
|
+
if(saved && saved.url){
|
|
1424
|
+
fillConnFields(saved);
|
|
1425
|
+
client.apiKey=saved.apiKey||''; // 鉴权 key 只能取保存的 apiKey,绝不混用模型的 key
|
|
1426
|
+
const ok=await client.connect(saved.url);
|
|
1427
|
+
if(ok){
|
|
1428
|
+
setConn(); await loadData(); return;
|
|
1429
|
+
}
|
|
1430
|
+
toast('已保存的后端未连上,回退演示模式(设置已回填,可在「连接设置」重连)','err');
|
|
1431
|
+
}
|
|
1432
|
+
if(await client.connect('')){ setConn(); await loadData(); return; }
|
|
1433
|
+
setConn();
|
|
1434
|
+
loadData();
|
|
1435
|
+
}
|
|
1436
|
+
startup();
|