@bolloon/bolloon-agent 0.1.36 → 0.1.38
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/package.json +2 -2
- package/.auto-evolve-calls +0 -1
- package/.last-auto-evolve-baseline +0 -1
- package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +0 -130
- package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +0 -131
- package/Bolloon.md +0 -144
- package/Dive-into/CITATION.cff +0 -17
- package/Dive-into/LICENSE +0 -25
- package/Dive-into/README.md +0 -598
- package/Dive-into/README_zh.md +0 -599
- package/Dive-into/assets/context.png +0 -0
- package/Dive-into/assets/extensibility.png +0 -0
- package/Dive-into/assets/iteration.png +0 -0
- package/Dive-into/assets/layered_architecture.png +0 -0
- package/Dive-into/assets/main_structure.png +0 -0
- package/Dive-into/assets/permission.png +0 -0
- package/Dive-into/assets/session_compact.png +0 -0
- package/Dive-into/assets/subagent.png +0 -0
- package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
- package/dist/agents/constraint-layer.js.map +0 -1
- package/dist/agents/pi-sdk.js.map +0 -1
- package/dist/agents/workflow-engine.js.map +0 -1
- package/dist/bollharness/src/index.js +0 -5
- package/dist/bollharness/src/scripts/checks/check_adr_plan_numbering.js +0 -6
- package/dist/bollharness/src/scripts/checks/check_api_types.js +0 -45
- package/dist/bollharness/src/scripts/checks/check_artifact_link.js +0 -146
- package/dist/bollharness/src/scripts/checks/check_bridge_deps.js +0 -6
- package/dist/bollharness/src/scripts/checks/check_bugfix_binding.js +0 -6
- package/dist/bollharness/src/scripts/checks/check_bugfix_binding_ci.js +0 -6
- package/dist/bollharness/src/scripts/checks/check_doc_file_references.js +0 -6
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +0 -135
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +0 -31
- package/dist/bollharness/src/scripts/checks/check_file_existence_claims.js +0 -6
- package/dist/bollharness/src/scripts/checks/check_fragment_integrity.js +0 -34
- package/dist/bollharness/src/scripts/checks/check_hook_installed.js +0 -63
- package/dist/bollharness/src/scripts/checks/check_issue_closure.js +0 -41
- package/dist/bollharness/src/scripts/checks/check_mcp_parity.js +0 -6
- package/dist/bollharness/src/scripts/checks/check_security.js +0 -48
- package/dist/bollharness/src/scripts/checks/check_skill_parity.js +0 -6
- package/dist/bollharness/src/scripts/checks/check_versions.js +0 -6
- package/dist/bollharness/src/scripts/checks/finding.js +0 -13
- package/dist/bollharness/src/scripts/checks/next_decision_number.js +0 -20
- package/dist/bollharness/src/scripts/checks/regenerate_magic_docs.js +0 -6
- package/dist/bollharness/src/scripts/ci/detect_rebaseline_triggers.js +0 -8
- package/dist/bollharness/src/scripts/ci/scan_subprocess_cfg.js +0 -8
- package/dist/bollharness/src/scripts/ci/scan_verify_artifacts.js +0 -8
- package/dist/bollharness/src/scripts/ci/scan_yaml_schema.js +0 -8
- package/dist/bollharness/src/scripts/context_router.js +0 -67
- package/dist/bollharness/src/scripts/deploy-guard.js +0 -157
- package/dist/bollharness/src/scripts/guard-feedback.js +0 -192
- package/dist/bollharness/src/scripts/guard_router.js +0 -158
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +0 -6
- package/dist/bollharness/src/scripts/hooks/auto-python3.js +0 -6
- package/dist/bollharness/src/scripts/hooks/deploy-progress-on-session-end.js +0 -6
- package/dist/bollharness/src/scripts/hooks/failure-analyzer.js +0 -6
- package/dist/bollharness/src/scripts/hooks/gate-judgment-inject.js +0 -92
- package/dist/bollharness/src/scripts/hooks/gate-transition-judgment.js +0 -63
- package/dist/bollharness/src/scripts/hooks/inbox-ack.js +0 -6
- package/dist/bollharness/src/scripts/hooks/inbox-inject-on-start.js +0 -6
- package/dist/bollharness/src/scripts/hooks/inbox-validate.js +0 -6
- package/dist/bollharness/src/scripts/hooks/inbox-write-ledger.js +0 -6
- package/dist/bollharness/src/scripts/hooks/initializer-agent.js +0 -6
- package/dist/bollharness/src/scripts/hooks/loop-detection.js +0 -73
- package/dist/bollharness/src/scripts/hooks/owner-guard.js +0 -6
- package/dist/bollharness/src/scripts/hooks/precompact.js +0 -6
- package/dist/bollharness/src/scripts/hooks/review-agent-gatekeeper.js +0 -6
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +0 -108
- package/dist/bollharness/src/scripts/hooks/sanitize-on-read.js +0 -6
- package/dist/bollharness/src/scripts/hooks/session-reflection.js +0 -7
- package/dist/bollharness/src/scripts/hooks/session-start-magic-docs.js +0 -7
- package/dist/bollharness/src/scripts/hooks/session-start-reset-risk.js +0 -7
- package/dist/bollharness/src/scripts/hooks/session-start-toolkit-reminder.js +0 -7
- package/dist/bollharness/src/scripts/hooks/stop-evaluator.js +0 -157
- package/dist/bollharness/src/scripts/hooks/tool-call-counter.js +0 -6
- package/dist/bollharness/src/scripts/hooks/trace-analyzer.js +0 -10
- package/dist/bollharness/src/scripts/install/install-trust-token.js +0 -7
- package/dist/bollharness/src/scripts/install/multi_project_registry.js +0 -9
- package/dist/bollharness/src/scripts/install/phase2_auto.js +0 -21
- package/dist/bollharness/src/scripts/install/pre_commit_installer.js +0 -6
- package/dist/bollharness/src/scripts/install/tier_selector.js +0 -7
- package/dist/bollharness/src/scripts/install/transcript_miner.js +0 -7
- package/dist/bollharness/src/scripts/lib/claim_patterns.js +0 -10
- package/dist/bollharness/src/scripts/lib/sanitize_patterns.js +0 -12
- package/dist/bollharness/src/scripts/sanitize.js +0 -6
- package/dist/bollharness-integration/context-router-judgment.js.map +0 -1
- package/dist/bollharness-integration/context-router.js.map +0 -1
- package/dist/bollharness-integration/gate-state-machine.js.map +0 -1
- package/dist/bollharness-integration/gate-transition-hooks.js.map +0 -1
- package/dist/bollharness-integration/guard-checker.js.map +0 -1
- package/dist/bollharness-integration/index.js.map +0 -1
- package/dist/bollharness-integration/integration.js.map +0 -1
- package/dist/bollharness-integration/llm/pi-ai.d.ts +0 -60
- package/dist/bollharness-integration/pi-ecosystem-colony/index.d.ts +0 -153
- package/dist/bollharness-integration/pi-ecosystem-goals/index.d.ts +0 -136
- package/dist/bollharness-integration/pi-ecosystem-judgment/decision.d.ts +0 -117
- package/dist/bollharness-integration/pi-ecosystem-judgment/distillation.d.ts +0 -78
- package/dist/bollharness-integration/pi-ecosystem-judgment/index.d.ts +0 -139
- package/dist/bollharness-integration/pi-ecosystem-mcp/index.d.ts +0 -127
- package/dist/bollharness-integration/pi-ecosystem-subagents/index.d.ts +0 -95
- package/dist/bollharness-integration/skill-adapter.js.map +0 -1
- package/dist/constraint-runtime/src/_archive_helper.js +0 -9
- package/dist/constraint-runtime/src/agent/coordinator.js +0 -48
- package/dist/constraint-runtime/src/agent/index.js +0 -1
- package/dist/constraint-runtime/src/assistant/index.js +0 -12
- package/dist/constraint-runtime/src/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/src/bootstrap_graph.js +0 -13
- package/dist/constraint-runtime/src/bridge/index.js +0 -12
- package/dist/constraint-runtime/src/buddy/index.js +0 -12
- package/dist/constraint-runtime/src/cli/index.js +0 -12
- package/dist/constraint-runtime/src/command_graph.js +0 -10
- package/dist/constraint-runtime/src/commands.js +0 -60
- package/dist/constraint-runtime/src/components/index.js +0 -12
- package/dist/constraint-runtime/src/constants/index.js +0 -12
- package/dist/constraint-runtime/src/constraint/budget.js +0 -22
- package/dist/constraint-runtime/src/constraint/budget.js.map +0 -1
- package/dist/constraint-runtime/src/constraint/index.js +0 -2
- package/dist/constraint-runtime/src/constraint/permission.js +0 -20
- package/dist/constraint-runtime/src/constraint/permission.js.map +0 -1
- package/dist/constraint-runtime/src/context.js +0 -30
- package/dist/constraint-runtime/src/coordinator/index.js +0 -12
- package/dist/constraint-runtime/src/cost_hook.js +0 -4
- package/dist/constraint-runtime/src/cost_tracker.js +0 -8
- package/dist/constraint-runtime/src/deferred_init.js +0 -10
- package/dist/constraint-runtime/src/direct_modes.js +0 -6
- package/dist/constraint-runtime/src/dynamic-tool-loader.js +0 -85
- package/dist/constraint-runtime/src/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/src/execution_registry.js +0 -44
- package/dist/constraint-runtime/src/history.js +0 -9
- package/dist/constraint-runtime/src/hooks/index.js +0 -12
- package/dist/constraint-runtime/src/index.js +0 -26
- package/dist/constraint-runtime/src/ink.js +0 -4
- package/dist/constraint-runtime/src/keybindings/index.js +0 -12
- package/dist/constraint-runtime/src/memdir/index.js +0 -12
- package/dist/constraint-runtime/src/migrations/index.js +0 -12
- package/dist/constraint-runtime/src/models.js +0 -1
- package/dist/constraint-runtime/src/models.js.map +0 -1
- package/dist/constraint-runtime/src/moreright/index.js +0 -12
- package/dist/constraint-runtime/src/native_ts/index.js +0 -12
- package/dist/constraint-runtime/src/output_styles/index.js +0 -12
- package/dist/constraint-runtime/src/parity_audit.js +0 -12
- package/dist/constraint-runtime/src/plugins/index.js +0 -12
- package/dist/constraint-runtime/src/port_manifest.js +0 -11
- package/dist/constraint-runtime/src/prefetch.js +0 -9
- package/dist/constraint-runtime/src/query.js +0 -1
- package/dist/constraint-runtime/src/remote/index.js +0 -12
- package/dist/constraint-runtime/src/remote_runtime.js +0 -9
- package/dist/constraint-runtime/src/runtime/index.js +0 -1
- package/dist/constraint-runtime/src/runtime/session.js +0 -35
- package/dist/constraint-runtime/src/schemas/index.js +0 -12
- package/dist/constraint-runtime/src/screens/index.js +0 -12
- package/dist/constraint-runtime/src/server/index.js +0 -12
- package/dist/constraint-runtime/src/services/index.js +0 -12
- package/dist/constraint-runtime/src/session_store.js +0 -22
- package/dist/constraint-runtime/src/setup.js +0 -30
- package/dist/constraint-runtime/src/skills/index.js +0 -1
- package/dist/constraint-runtime/src/skills/skill-registry.js +0 -28
- package/dist/constraint-runtime/src/state/index.js +0 -12
- package/dist/constraint-runtime/src/system_init.js +0 -20
- package/dist/constraint-runtime/src/thinking/engine.js +0 -42
- package/dist/constraint-runtime/src/thinking/index.js +0 -1
- package/dist/constraint-runtime/src/tool_pool.js +0 -8
- package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +0 -7
- package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +0 -7
- package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +0 -7
- package/dist/constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js +0 -6
- package/dist/constraint-runtime/src/tools/PolymarketSDK/createOrder.js +0 -6
- package/dist/constraint-runtime/src/tools/PolymarketSDK/getMarket.js +0 -5
- package/dist/constraint-runtime/src/tools/PolymarketSDK/getOrders.js +0 -6
- package/dist/constraint-runtime/src/tools/PolymarketSDK/listMarkets.js +0 -4
- package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +0 -8
- package/dist/constraint-runtime/src/tools/SafeSDK/deploySafe.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools/WalletTools/autoPay.js +0 -29
- package/dist/constraint-runtime/src/tools/WalletTools/createWallet.js +0 -10
- package/dist/constraint-runtime/src/tools/WalletTools/getBalance.js +0 -13
- package/dist/constraint-runtime/src/tools/WalletTools/importWallet.js +0 -22
- package/dist/constraint-runtime/src/tools/WalletTools/sendTransaction.js +0 -25
- package/dist/constraint-runtime/src/tools/WalletTools/signMessage.js +0 -10
- package/dist/constraint-runtime/src/tools/WalletTools/transferToken.js +0 -25
- package/dist/constraint-runtime/src/tools.js +0 -80
- package/dist/constraint-runtime/src/transcript.js +0 -19
- package/dist/constraint-runtime/src/types/index.js +0 -12
- package/dist/constraint-runtime/src/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/src/utils/index.js +0 -12
- package/dist/constraint-runtime/src/vim/index.js +0 -12
- package/dist/constraint-runtime/src/voice/index.js +0 -12
- package/dist/constraints/index.js.map +0 -1
- package/dist/documents/reader.js.map +0 -1
- package/dist/llm/config-store.js.map +0 -1
- package/dist/llm/pi-ai.js.map +0 -1
- package/dist/network/agent-network.js.map +0 -1
- package/dist/network/iroh-integration.js.map +0 -1
- package/dist/network/iroh-transport.js.map +0 -1
- package/dist/network/p2p.js.map +0 -1
- package/dist/network/storage/adapters/json-adapter.js.map +0 -1
- package/dist/pi-ecosystem-colony/index.js.map +0 -1
- package/dist/pi-ecosystem-goals/index.js.map +0 -1
- package/dist/pi-ecosystem-judgment/decision.js.map +0 -1
- package/dist/pi-ecosystem-judgment/distillation.js.map +0 -1
- package/dist/pi-ecosystem-judgment/human-value-store.js.map +0 -1
- package/dist/pi-ecosystem-judgment/index.js.map +0 -1
- package/dist/pi-ecosystem-judgment/value-injection.js.map +0 -1
- package/dist/pi-ecosystem-mcp/index.js.map +0 -1
- package/dist/pi-ecosystem-subagents/index.js.map +0 -1
- package/dist/social/ant-colony/AdaptiveHeartbeat.js.map +0 -1
- package/dist/social/ant-colony/PheromoneEngine.js.map +0 -1
- package/dist/social/ant-colony/types.js.map +0 -1
- package/dist/social/channels/ChannelManager.js.map +0 -1
- package/dist/social/channels/DiapChannelBridge.js.map +0 -1
- package/dist/social/channels/InterestMatcher.js.map +0 -1
- package/dist/social/channels/types.js.map +0 -1
- package/dist/social/global-shared-context.js.map +0 -1
- package/dist/social/heartbeat.js.map +0 -1
- package/lefthook.yml +0 -33
- package/scripts/auto-evolve-loop.ts +0 -481
- package/scripts/build-web.ts +0 -116
- package/scripts/diff-reviewer.ts +0 -159
- package/scripts/validate-system-prompt.ts +0 -142
- package/scripts/weekly-report.ts +0 -364
- package/src/agents/agent-manifest-protocol.ts +0 -117
- package/src/agents/constraint-layer.ts +0 -309
- package/src/agents/iroh-secret.ts +0 -32
- package/src/agents/p2p-chat-tools.ts +0 -383
- package/src/agents/p2p-document-tools.ts +0 -347
- package/src/agents/permission-mode.ts +0 -127
- package/src/agents/pi-sdk.ts +0 -2895
- package/src/agents/pre-tool-validator.ts +0 -213
- package/src/agents/protocol.ts +0 -398
- package/src/agents/shell-guard.ts +0 -417
- package/src/agents/shell-tool.ts +0 -103
- package/src/agents/skill-loader.ts +0 -202
- package/src/agents/subagent-manager.ts +0 -553
- package/src/agents/workflow-engine.ts +0 -332
- package/src/agents/workflow-pivot-loop.ts +0 -766
- package/src/bollharness/.sanitize-report.json +0 -13
- package/src/bollharness/CLAUDE.md +0 -73
- package/src/bollharness/LICENSE +0 -21
- package/src/bollharness/README.md +0 -143
- package/src/bollharness/README.zh-CN.md +0 -131
- package/src/bollharness/package.json +0 -20
- package/src/bollharness/reference/SOURCE-COMMIT.txt +0 -3
- package/src/bollharness/reference/boll-reference/scripts/hooks/.sanitize-report.json +0 -12
- package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +0 -27
- package/src/bollharness/reference/boll-reference/scripts/hooks/precompact.sh +0 -57
- package/src/bollharness/reference/boll-reference/scripts/hooks/stop-evaluator.md +0 -57
- package/src/bollharness/schemas/metrics-jsonl-allowlist.json +0 -67
- package/src/bollharness/scripts/checks/next_decision_number.sh +0 -48
- package/src/bollharness/scripts/ci/count-components.sh +0 -65
- package/src/bollharness/scripts/context-fragments/artifact-linkage.md +0 -14
- package/src/bollharness/scripts/context-fragments/auth-consumers.md +0 -17
- package/src/bollharness/scripts/context-fragments/bridge-constitution.md +0 -13
- package/src/bollharness/scripts/context-fragments/catalyst-distributed.md +0 -18
- package/src/bollharness/scripts/context-fragments/closure-checklist.md +0 -13
- package/src/bollharness/scripts/context-fragments/contract-consumers.md +0 -15
- package/src/bollharness/scripts/context-fragments/db-shared-structures.md +0 -15
- package/src/bollharness/scripts/context-fragments/fixed-three-layers.md +0 -19
- package/src/bollharness/scripts/context-fragments/general-dev-principles.md +0 -11
- package/src/bollharness/scripts/context-fragments/issue-first.md +0 -8
- package/src/bollharness/scripts/context-fragments/mcp-parity.md +0 -16
- package/src/bollharness/scripts/context-fragments/pi-agent-operations.md +0 -108
- package/src/bollharness/scripts/context-fragments/protocol-consumers.md +0 -15
- package/src/bollharness/scripts/context-fragments/run-events-consumers.md +0 -15
- package/src/bollharness/scripts/context-fragments/scene-fidelity.md +0 -13
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +0 -15
- package/src/bollharness/scripts/context-fragments/two-language.md +0 -15
- package/src/bollharness/scripts/context-fragments/version-sources.md +0 -14
- package/src/bollharness/scripts/hooks/find-project-root.sh +0 -47
- package/src/bollharness/scripts/hooks/inbox-poll.sh +0 -78
- package/src/bollharness/scripts/hooks/precompact.sh +0 -56
- package/src/bollharness/scripts/hooks/stop-evaluator.md +0 -83
- package/src/bollharness/scripts/sync-from-upstream.sh +0 -281
- package/src/bollharness/src/index.ts +0 -5
- package/src/bollharness/src/scripts/checks/check_adr_plan_numbering.ts +0 -11
- package/src/bollharness/src/scripts/checks/check_api_types.ts +0 -52
- package/src/bollharness/src/scripts/checks/check_artifact_link.ts +0 -156
- package/src/bollharness/src/scripts/checks/check_bridge_deps.ts +0 -11
- package/src/bollharness/src/scripts/checks/check_bugfix_binding.ts +0 -11
- package/src/bollharness/src/scripts/checks/check_bugfix_binding_ci.ts +0 -11
- package/src/bollharness/src/scripts/checks/check_doc_file_references.ts +0 -11
- package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +0 -141
- package/src/bollharness/src/scripts/checks/check_doc_links.ts +0 -36
- package/src/bollharness/src/scripts/checks/check_file_existence_claims.ts +0 -11
- package/src/bollharness/src/scripts/checks/check_fragment_integrity.ts +0 -40
- package/src/bollharness/src/scripts/checks/check_hook_installed.ts +0 -70
- package/src/bollharness/src/scripts/checks/check_issue_closure.ts +0 -51
- package/src/bollharness/src/scripts/checks/check_mcp_parity.ts +0 -11
- package/src/bollharness/src/scripts/checks/check_security.ts +0 -54
- package/src/bollharness/src/scripts/checks/check_skill_parity.ts +0 -11
- package/src/bollharness/src/scripts/checks/check_versions.ts +0 -11
- package/src/bollharness/src/scripts/checks/finding.ts +0 -35
- package/src/bollharness/src/scripts/checks/next_decision_number.ts +0 -24
- package/src/bollharness/src/scripts/checks/regenerate_magic_docs.ts +0 -11
- package/src/bollharness/src/scripts/ci/detect_rebaseline_triggers.ts +0 -14
- package/src/bollharness/src/scripts/ci/scan_subprocess_cfg.ts +0 -14
- package/src/bollharness/src/scripts/ci/scan_verify_artifacts.ts +0 -14
- package/src/bollharness/src/scripts/ci/scan_yaml_schema.ts +0 -14
- package/src/bollharness/src/scripts/context_router.ts +0 -76
- package/src/bollharness/src/scripts/deploy-guard.ts +0 -182
- package/src/bollharness/src/scripts/guard-feedback.ts +0 -215
- package/src/bollharness/src/scripts/guard_router.ts +0 -194
- package/src/bollharness/src/scripts/hooks/_hook_output.js +0 -3
- package/src/bollharness/src/scripts/hooks/_hook_output.ts +0 -11
- package/src/bollharness/src/scripts/hooks/auto-python3.ts +0 -10
- package/src/bollharness/src/scripts/hooks/deploy-progress-on-session-end.ts +0 -10
- package/src/bollharness/src/scripts/hooks/failure-analyzer.ts +0 -10
- package/src/bollharness/src/scripts/hooks/gate-judgment-inject.ts +0 -111
- package/src/bollharness/src/scripts/hooks/gate-transition-judgment.ts +0 -74
- package/src/bollharness/src/scripts/hooks/inbox-ack.ts +0 -10
- package/src/bollharness/src/scripts/hooks/inbox-inject-on-start.ts +0 -10
- package/src/bollharness/src/scripts/hooks/inbox-validate.ts +0 -10
- package/src/bollharness/src/scripts/hooks/inbox-write-ledger.ts +0 -10
- package/src/bollharness/src/scripts/hooks/initializer-agent.ts +0 -10
- package/src/bollharness/src/scripts/hooks/loop-detection.ts +0 -83
- package/src/bollharness/src/scripts/hooks/owner-guard.ts +0 -10
- package/src/bollharness/src/scripts/hooks/precompact.ts +0 -10
- package/src/bollharness/src/scripts/hooks/review-agent-gatekeeper.ts +0 -10
- package/src/bollharness/src/scripts/hooks/risk-tracker.ts +0 -121
- package/src/bollharness/src/scripts/hooks/sanitize-on-read.ts +0 -10
- package/src/bollharness/src/scripts/hooks/session-reflection.ts +0 -12
- package/src/bollharness/src/scripts/hooks/session-start-magic-docs.ts +0 -12
- package/src/bollharness/src/scripts/hooks/session-start-reset-risk.ts +0 -12
- package/src/bollharness/src/scripts/hooks/session-start-toolkit-reminder.ts +0 -12
- package/src/bollharness/src/scripts/hooks/stop-evaluator.ts +0 -164
- package/src/bollharness/src/scripts/hooks/tool-call-counter.ts +0 -10
- package/src/bollharness/src/scripts/hooks/trace-analyzer.ts +0 -14
- package/src/bollharness/src/scripts/install/install-trust-token.ts +0 -13
- package/src/bollharness/src/scripts/install/multi_project_registry.ts +0 -13
- package/src/bollharness/src/scripts/install/phase2_auto.ts +0 -28
- package/src/bollharness/src/scripts/install/pre_commit_installer.ts +0 -10
- package/src/bollharness/src/scripts/install/tier_selector.ts +0 -10
- package/src/bollharness/src/scripts/install/transcript_miner.ts +0 -13
- package/src/bollharness/src/scripts/lib/claim_patterns.ts +0 -11
- package/src/bollharness/src/scripts/lib/sanitize_patterns.ts +0 -13
- package/src/bollharness/src/scripts/sanitize.ts +0 -9
- package/src/bollharness/templates/persona/default.json +0 -19
- package/src/bollharness/templates/scaffold/.gitignore.append +0 -16
- package/src/bollharness/templates/scaffold/CLAUDE.md +0 -89
- package/src/bollharness-integration/channel-judgment-engine.ts +0 -634
- package/src/bollharness-integration/context-chain-router.ts +0 -474
- package/src/bollharness-integration/context-router-judgment.ts +0 -339
- package/src/bollharness-integration/context-router.ts +0 -583
- package/src/bollharness-integration/gate-state-machine.ts +0 -444
- package/src/bollharness-integration/gate-transition-hooks.ts +0 -137
- package/src/bollharness-integration/guard-checker.ts +0 -451
- package/src/bollharness-integration/index.ts +0 -195
- package/src/bollharness-integration/integration.ts +0 -538
- package/src/bollharness-integration/judgment-prompts.yaml +0 -535
- package/src/bollharness-integration/llm-judgment-engine.ts +0 -712
- package/src/bollharness-integration/skill-adapter.ts +0 -646
- package/src/bootstrap/bootstrap.ts +0 -135
- package/src/bootstrap/context-collector.ts +0 -371
- package/src/bootstrap/context-hierarchy.ts +0 -283
- package/src/bootstrap/lifecycle-hooks.ts +0 -289
- package/src/bootstrap/project-context.ts +0 -171
- package/src/cli/interface.ts +0 -211
- package/src/cli-entry.ts +0 -304
- package/src/constraints/index.ts +0 -6
- package/src/context-compaction/auto-compact.ts +0 -153
- package/src/context-compaction/budget-gate.ts +0 -32
- package/src/context-compaction/budget-reduce.ts +0 -37
- package/src/context-compaction/context-collapse.ts +0 -72
- package/src/context-compaction/index.ts +0 -24
- package/src/context-compaction/microcompact.ts +0 -54
- package/src/context-compaction/pipeline.ts +0 -133
- package/src/context-compaction/snip.ts +0 -51
- package/src/context-compaction/token-estimator.ts +0 -36
- package/src/context-compaction/types.ts +0 -99
- package/src/documents/reader.ts +0 -85
- package/src/documents/store.ts +0 -252
- package/src/electron-preload.ts +0 -25
- package/src/electron.ts +0 -200
- package/src/heartbeat/DaemonManager.ts +0 -283
- package/src/heartbeat/HealthMonitor.ts +0 -317
- package/src/heartbeat/StartupVerifier.ts +0 -223
- package/src/heartbeat/Watchdog.ts +0 -208
- package/src/heartbeat/index.ts +0 -109
- package/src/heartbeat/self-improve-bus.ts +0 -110
- package/src/heartbeat/types.ts +0 -82
- package/src/index.ts +0 -1724
- package/src/llm/audio-config-store.ts +0 -246
- package/src/llm/config-store.ts +0 -400
- package/src/llm/llm-judgment-client.ts +0 -471
- package/src/llm/pi-ai.ts +0 -653
- package/src/llm/system-prompt/health.ts +0 -159
- package/src/llm/system-prompt/layers/channel/local.md +0 -14
- package/src/llm/system-prompt/layers/channel/p2p-agent.md +0 -18
- package/src/llm/system-prompt/layers/channel/p2p-visitor.md +0 -19
- package/src/llm/system-prompt/layers/core/artifacts_storage.md +0 -89
- package/src/llm/system-prompt/layers/core/evenhandedness.md +0 -21
- package/src/llm/system-prompt/layers/core/hibs_reminders.md +0 -15
- package/src/llm/system-prompt/layers/core/identity.md +0 -37
- package/src/llm/system-prompt/layers/core/knowledge.md +0 -17
- package/src/llm/system-prompt/layers/core/memory_system.md +0 -12
- package/src/llm/system-prompt/layers/core/network_filesystem.md +0 -28
- package/src/llm/system-prompt/layers/core/refusal.md +0 -37
- package/src/llm/system-prompt/layers/core/tone.md +0 -31
- package/src/llm/system-prompt/layers/core/tools.thin.md +0 -13
- package/src/llm/system-prompt/layers/core/wellbeing.md +0 -41
- package/src/llm/system-prompt/layers/role/architect.md +0 -20
- package/src/llm/system-prompt/layers/role/expert.md +0 -19
- package/src/llm/system-prompt/layers/role/implementer.md +0 -15
- package/src/llm/system-prompt/layers/role/security.md +0 -15
- package/src/llm/system-prompt/layers/tool/artifacts.md +0 -72
- package/src/llm/system-prompt/layers/tool/bash.md +0 -25
- package/src/llm/system-prompt/layers/tool/hibs_api.md +0 -171
- package/src/llm/system-prompt/layers/tool/image_search.md +0 -70
- package/src/llm/system-prompt/layers/tool/manifest.md +0 -89
- package/src/llm/system-prompt/layers/tool/mcp_apps.md +0 -53
- package/src/llm/system-prompt/layers/tool/web_search.md +0 -83
- package/src/llm/system-prompt/registry.ts +0 -325
- package/src/llm/system-prompt/strip-hibsml.ts +0 -52
- package/src/llm/tool-manifest/ask_user_input.ts +0 -37
- package/src/llm/tool-manifest/bash.ts +0 -25
- package/src/llm/tool-manifest/create_file.ts +0 -26
- package/src/llm/tool-manifest/fetch_sports_data.ts +0 -28
- package/src/llm/tool-manifest/image_search.ts +0 -26
- package/src/llm/tool-manifest/index.ts +0 -88
- package/src/llm/tool-manifest/mcp.ts +0 -46
- package/src/llm/tool-manifest/message_compose.ts +0 -33
- package/src/llm/tool-manifest/places.ts +0 -86
- package/src/llm/tool-manifest/present_files.ts +0 -23
- package/src/llm/tool-manifest/recipe.ts +0 -42
- package/src/llm/tool-manifest/recommend_apps.ts +0 -25
- package/src/llm/tool-manifest/str_replace.ts +0 -29
- package/src/llm/tool-manifest/types.ts +0 -52
- package/src/llm/tool-manifest/view.ts +0 -26
- package/src/llm/tool-manifest/weather.ts +0 -29
- package/src/llm/tool-manifest/web.ts +0 -54
- package/src/llm/video-config-store.ts +0 -257
- package/src/network/agent-network.ts +0 -800
- package/src/network/hybrid-messenger.ts +0 -199
- package/src/network/iroh-bootstrap.ts +0 -57
- package/src/network/iroh-discovery.ts +0 -208
- package/src/network/iroh-integration.ts +0 -158
- package/src/network/iroh-transport.ts +0 -637
- package/src/network/known-peers.ts +0 -102
- package/src/network/p2p-direct.ts +0 -262
- package/src/network/p2p-secret.ts +0 -153
- package/src/network/p2p.ts +0 -964
- package/src/network/source-intent-broadcaster.ts +0 -242
- package/src/network/source-intent.ts +0 -167
- package/src/network/storage/adapters/json-adapter.ts +0 -454
- package/src/network/storage/index.ts +0 -203
- package/src/network/storage/types.ts +0 -166
- package/src/security/builtin-guards.ts +0 -162
- package/src/security/context-router-tool.ts +0 -122
- package/src/security/input-scanner.ts +0 -287
- package/src/security/react-harness.ts +0 -177
- package/src/security/tool-gate.ts +0 -294
- package/src/social/ant-colony/index.js +0 -19
- package/src/social/channels/ChannelManager.ts +0 -485
- package/src/social/channels/DiapChannelBridge.ts +0 -501
- package/src/social/channels/InterestMatcher.ts +0 -189
- package/src/social/channels/agent-workflow-config.json +0 -214
- package/src/social/channels/agent-workflow-config.yaml +0 -334
- package/src/social/channels/channel-agent-session.ts +0 -407
- package/src/social/channels/channel-heartbeat-agent.ts +0 -622
- package/src/social/channels/diap-doc-parser.ts +0 -282
- package/src/social/channels/harness-workflow-integrator.ts +0 -594
- package/src/social/channels/index.ts +0 -22
- package/src/social/channels/types.ts +0 -115
- package/src/social/global-shared-context.ts +0 -506
- package/src/social/heartbeat.ts +0 -1057
- package/src/social/persona/enhanced-persona.ts +0 -359
- package/src/types.d.ts +0 -12
- package/src/utils/auto-evolve-policy.ts +0 -138
- package/src/utils/auto-update.ts +0 -482
- package/src/utils/clamp.ts +0 -5
- package/src/web/agent-delegate-server.ts +0 -148
- package/src/web/api-config.html +0 -520
- package/src/web/client.js +0 -2352
- package/src/web/client.ts +0 -4326
- package/src/web/components/p2p/P2PModal.tsx +0 -416
- package/src/web/components/p2p/index.ts +0 -566
- package/src/web/components/p2p/index.tsx +0 -297
- package/src/web/components/p2p/p2p-connection.ts +0 -338
- package/src/web/components/p2p/p2p-identity.ts +0 -92
- package/src/web/components/p2p/p2p-manager.ts +0 -147
- package/src/web/components/p2p/p2p-messages.ts +0 -219
- package/src/web/components/p2p/p2p-modal.ts +0 -685
- package/src/web/components/p2p/p2p-store-memory.ts +0 -162
- package/src/web/components/p2p/p2p-tools.ts +0 -315
- package/src/web/components/p2p/types.ts +0 -170
- package/src/web/components/wallet-viem.mjs +0 -118
- package/src/web/design.md +0 -99
- package/src/web/icons/apple-touch-icon.png +0 -0
- package/src/web/icons/favicon-16x16.png +0 -0
- package/src/web/icons/favicon-192x192.png +0 -0
- package/src/web/icons/favicon-32x32.png +0 -0
- package/src/web/icons/favicon-48x48.png +0 -0
- package/src/web/icons/favicon-512x512.png +0 -0
- package/src/web/icons/favicon.icns +0 -0
- package/src/web/icons/favicon.ico +0 -0
- package/src/web/icons/icon.png +0 -0
- package/src/web/icons/image.png +0 -0
- package/src/web/index.html +0 -373
- package/src/web/iroh-delegate-transport.ts +0 -139
- package/src/web/manifest.json +0 -21
- package/src/web/server.ts +0 -5731
- package/src/web/style.css +0 -4737
- package/src/web/ui/message-renderer.ts +0 -540
- package/src/web/ui/step-timeline.ts +0 -394
- package/staging/auto-evolve/clean-001/.review-verdict +0 -9
- package/staging/auto-evolve/clean-001/clean-001.patch +0 -14
- package/staging/auto-evolve/e2e-001/.patch-id +0 -1
- package/staging/auto-evolve/e2e-001/.review-verdict +0 -12
- package/staging/auto-evolve/e2e-001/e2e-001.patch +0 -11
- package/staging/auto-evolve/test-bad/.review-verdict +0 -12
- package/staging/auto-evolve/test-bad/test-bad.patch +0 -11
- package/test-results/.last-run.json +0 -6
- package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +0 -285
- package/tsconfig.cli.json +0 -16
- package/tsconfig.electron.json +0 -20
- package/tsconfig.json +0 -18
package/src/web/client.js
DELETED
|
@@ -1,2352 +0,0 @@
|
|
|
1
|
-
// marked 库可能从 CDN 加载失败, 这里做安全降级 (避免 ReferenceError 让 addMessage 整体崩溃)
|
|
2
|
-
if (typeof marked === 'undefined') {
|
|
3
|
-
window.marked = { parse: (text) => String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>') };
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const messagesEl = document.getElementById('messages');
|
|
7
|
-
const agentStatusEl = document.getElementById('agent-status');
|
|
8
|
-
const agentStatusTextEl = document.getElementById('agent-status-text');
|
|
9
|
-
const input = document.getElementById('input');
|
|
10
|
-
const sendBtn = document.getElementById('send');
|
|
11
|
-
const sidebar = document.getElementById('sidebar');
|
|
12
|
-
const sidebarToggle = document.getElementById('sidebar-toggle');
|
|
13
|
-
const themeToggle = document.getElementById('theme-toggle');
|
|
14
|
-
const channelList = document.getElementById('channel-list');
|
|
15
|
-
const newChannelBtn = document.getElementById('new-channel-btn');
|
|
16
|
-
const newChannelInput = document.getElementById('new-channel-input');
|
|
17
|
-
const channelNameEl = document.getElementById('channel-name');
|
|
18
|
-
const loadSessionBtn = document.getElementById('load-session-btn');
|
|
19
|
-
const sessionFileInput = document.getElementById('session-file-input');
|
|
20
|
-
const newSessionBtn = document.getElementById('new-session-btn'); // 兼容旧引用(右上角按钮已移除)
|
|
21
|
-
|
|
22
|
-
let eventSources = new Map(); // channelId -> EventSource
|
|
23
|
-
let currentChannelId = null;
|
|
24
|
-
let currentAgentId = '';
|
|
25
|
-
let channels = [];
|
|
26
|
-
let isSidebarCollapsed = false;
|
|
27
|
-
let reconnectAttempts = new Map(); // channelId -> attempts
|
|
28
|
-
let reconnectTimers = new Map(); // channelId -> timer
|
|
29
|
-
let heartbeatTimers = new Map(); // channelId -> setInterval handle (防止泄漏)
|
|
30
|
-
let lastUserCommand = ''; // 防止用户消息重复显示
|
|
31
|
-
let lastAiContent = ''; // 防止 AI 消息重复显示
|
|
32
|
-
let messagesContainers = new Map(); // channelId -> messages container div
|
|
33
|
-
let sessionMessages = new Map(); // channelId:sessionId -> messages array
|
|
34
|
-
let currentSessionId = null; // 当前显示的 session ID
|
|
35
|
-
let expandedAgents = new Set(); // 当前展开的 agent(channel) id 集合
|
|
36
|
-
|
|
37
|
-
function generateId() {
|
|
38
|
-
return crypto.randomUUID();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function applyTheme(theme) {
|
|
42
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function loadTheme() {
|
|
46
|
-
try {
|
|
47
|
-
const res = await fetch('/theme');
|
|
48
|
-
const data = await res.json();
|
|
49
|
-
applyTheme(data.theme);
|
|
50
|
-
if (data.agentId) {
|
|
51
|
-
currentAgentId = data.agentId;
|
|
52
|
-
}
|
|
53
|
-
return data;
|
|
54
|
-
} catch {
|
|
55
|
-
applyTheme('dark');
|
|
56
|
-
return { theme: 'dark', agentId: '' };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function saveTheme(theme, agentId) {
|
|
61
|
-
try {
|
|
62
|
-
await fetch('/theme', {
|
|
63
|
-
method: 'POST',
|
|
64
|
-
headers: { 'Content-Type': 'application/json' },
|
|
65
|
-
body: JSON.stringify({ theme, agentId })
|
|
66
|
-
});
|
|
67
|
-
} catch (err) {
|
|
68
|
-
console.error('Failed to save theme:', err);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function toggleTheme() {
|
|
73
|
-
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
74
|
-
const next = current === 'dark' ? 'light' : 'dark';
|
|
75
|
-
applyTheme(next);
|
|
76
|
-
saveTheme(next, currentAgentId);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function toggleSidebar() {
|
|
80
|
-
isSidebarCollapsed = !isSidebarCollapsed;
|
|
81
|
-
|
|
82
|
-
if (isSidebarCollapsed) {
|
|
83
|
-
sidebar.classList.add('collapsed');
|
|
84
|
-
} else {
|
|
85
|
-
sidebar.classList.remove('collapsed');
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function expandSidebar() {
|
|
90
|
-
isSidebarCollapsed = false;
|
|
91
|
-
sidebar.classList.remove('collapsed');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function loadChannels() {
|
|
95
|
-
try {
|
|
96
|
-
const res = await fetch('/channels');
|
|
97
|
-
channels = await res.json();
|
|
98
|
-
console.log('[加载频道] 从服务器获取到', channels.length, '个频道');
|
|
99
|
-
channels.forEach((ch, i) => {
|
|
100
|
-
console.log(` [${i}] ${ch.name} - did: "${ch.did}"`);
|
|
101
|
-
});
|
|
102
|
-
renderChannels();
|
|
103
|
-
} catch (err) {
|
|
104
|
-
console.error('[加载频道] 失败:', err);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function createChannel(name) {
|
|
109
|
-
if (!name.trim()) return;
|
|
110
|
-
try {
|
|
111
|
-
const res = await fetch('/channels', {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers: { 'Content-Type': 'application/json' },
|
|
114
|
-
body: JSON.stringify({ name: name.trim(), agentId: currentAgentId })
|
|
115
|
-
});
|
|
116
|
-
const channel = await res.json();
|
|
117
|
-
console.log('[创建频道] 服务器返回:', channel);
|
|
118
|
-
console.log('[创建频道] DID:', channel.did, 'CID:', channel.cid);
|
|
119
|
-
|
|
120
|
-
// 立即添加频道并切换(不等待 DID)
|
|
121
|
-
channels.push(channel);
|
|
122
|
-
renderChannels();
|
|
123
|
-
selectChannel(channel.id);
|
|
124
|
-
if (newChannelInput) newChannelInput.value = '';
|
|
125
|
-
|
|
126
|
-
// 后台更新 DID(如果还没有的话)
|
|
127
|
-
if (!channel.did || channel.did === 'undefined') {
|
|
128
|
-
console.log('[创建频道] 后台生成 DID...');
|
|
129
|
-
// 复用全局 channelRefreshTimer, 把多个刷新请求合并成一个 1.5s 后的请求
|
|
130
|
-
scheduleChannelsRefresh();
|
|
131
|
-
}
|
|
132
|
-
} catch (err) {
|
|
133
|
-
console.error('Failed to create channel:', err);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function deleteChannel(channelId, e) {
|
|
138
|
-
e.stopPropagation();
|
|
139
|
-
if (!confirm('确定要删除该智能体及其所有会话吗?此操作不可撤销。')) return;
|
|
140
|
-
try {
|
|
141
|
-
await fetch(`/channels/${channelId}`, { method: 'DELETE' });
|
|
142
|
-
channels = channels.filter(c => c.id !== channelId);
|
|
143
|
-
expandedAgents.delete(channelId);
|
|
144
|
-
|
|
145
|
-
// 释放浏览器侧引用 + DOM, 避免长时间使用后内存累积
|
|
146
|
-
cleanupChannelState(channelId);
|
|
147
|
-
|
|
148
|
-
if (currentChannelId === channelId) {
|
|
149
|
-
currentChannelId = channels[0]?.id || null;
|
|
150
|
-
currentSessionId = null;
|
|
151
|
-
if (currentChannelId) {
|
|
152
|
-
const ch = channels.find(c => c.id === currentChannelId);
|
|
153
|
-
if (channelNameEl) channelNameEl.textContent = ch?.name || 'Bolloon Agent';
|
|
154
|
-
await selectChannel(currentChannelId);
|
|
155
|
-
} else {
|
|
156
|
-
messagesEl.innerHTML = '';
|
|
157
|
-
if (channelNameEl) channelNameEl.textContent = 'Bolloon Agent';
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
renderChannels();
|
|
161
|
-
renderCollapsedChannels();
|
|
162
|
-
} catch (err) {
|
|
163
|
-
console.error('Failed to delete channel:', err);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** 释放一个 channel 在浏览器侧占用的所有资源 (DOM 容器, SSE, 缓存 session 消息) */
|
|
168
|
-
function cleanupChannelState(channelId) {
|
|
169
|
-
// 1. SSE 连接
|
|
170
|
-
if (eventSources.has(channelId)) {
|
|
171
|
-
try { eventSources.get(channelId).close(); } catch {}
|
|
172
|
-
eventSources.delete(channelId);
|
|
173
|
-
}
|
|
174
|
-
// 2. 心跳 + 重连 timer
|
|
175
|
-
if (heartbeatTimers.has(channelId)) {
|
|
176
|
-
clearInterval(heartbeatTimers.get(channelId));
|
|
177
|
-
heartbeatTimers.delete(channelId);
|
|
178
|
-
}
|
|
179
|
-
if (reconnectTimers.has(channelId)) {
|
|
180
|
-
clearTimeout(reconnectTimers.get(channelId));
|
|
181
|
-
reconnectTimers.delete(channelId);
|
|
182
|
-
}
|
|
183
|
-
reconnectAttempts.delete(channelId);
|
|
184
|
-
// 3. 消息容器 DOM — 真从 #messages 里移除, 不只是隐藏
|
|
185
|
-
const container = messagesContainers.get(channelId);
|
|
186
|
-
if (container && container.parentNode) {
|
|
187
|
-
container.parentNode.removeChild(container);
|
|
188
|
-
}
|
|
189
|
-
messagesContainers.delete(channelId);
|
|
190
|
-
// 4. 缓存的所有 session 消息 (按 channel:session 索引)
|
|
191
|
-
const prefix = `${channelId}:`;
|
|
192
|
-
for (const key of sessionMessages.keys()) {
|
|
193
|
-
if (key === channelId || key.startsWith(prefix)) {
|
|
194
|
-
sessionMessages.delete(key);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function createNewSession() {
|
|
200
|
-
if (!currentChannelId) {
|
|
201
|
-
console.log('[新会话] 没有选中的频道');
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
// 保存当前 session 的消息
|
|
206
|
-
saveCurrentSessionMessages();
|
|
207
|
-
|
|
208
|
-
const res = await fetch(`/channels/${currentChannelId}/sessions`, {
|
|
209
|
-
method: 'POST'
|
|
210
|
-
});
|
|
211
|
-
const data = await res.json();
|
|
212
|
-
console.log('[新会话] 创建成功:', data);
|
|
213
|
-
|
|
214
|
-
// 更新本地频道数据
|
|
215
|
-
const channel = channels.find(c => c.id === currentChannelId);
|
|
216
|
-
if (channel) {
|
|
217
|
-
if (!channel.sessions) channel.sessions = [];
|
|
218
|
-
channel.sessions.push(data.session);
|
|
219
|
-
channel.currentSessionId = data.currentSessionId;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// 切换到新 session
|
|
223
|
-
currentSessionId = data.currentSessionId;
|
|
224
|
-
|
|
225
|
-
// 清空容器并加载新 session
|
|
226
|
-
const container = messagesContainers.get(currentChannelId);
|
|
227
|
-
if (container) {
|
|
228
|
-
container.innerHTML = '';
|
|
229
|
-
showChannelView(currentChannelId);
|
|
230
|
-
addMessage('你好!新会话已开始,有什么我可以帮你的吗?', 'ai', false, container);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 展开当前智能体,刷新侧边栏让新会话显示出来
|
|
234
|
-
expandedAgents.add(currentChannelId);
|
|
235
|
-
renderChannels();
|
|
236
|
-
|
|
237
|
-
console.log('[新会话] 已切换到:', data.currentSessionId);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
console.error('Failed to create new session:', err);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function createNewSessionForChannel(channelId, e) {
|
|
244
|
-
if (e) e.stopPropagation();
|
|
245
|
-
if (!channelId) return;
|
|
246
|
-
|
|
247
|
-
// 给自己创建:复用统一的 createNewSession
|
|
248
|
-
if (channelId === currentChannelId) {
|
|
249
|
-
if (currentSessionId) saveCurrentSessionMessages();
|
|
250
|
-
await createNewSession();
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 给别的智能体创建:后端建好后直接 re-fetch 一次保持本地与后端一致
|
|
255
|
-
try {
|
|
256
|
-
const res = await fetch(`/channels/${channelId}/sessions`, { method: 'POST' });
|
|
257
|
-
if (!res.ok) throw new Error('create session failed');
|
|
258
|
-
const data = await res.json();
|
|
259
|
-
const channel = channels.find(c => c.id === channelId);
|
|
260
|
-
if (channel) {
|
|
261
|
-
if (!channel.sessions) channel.sessions = [];
|
|
262
|
-
channel.sessions.push(data.session);
|
|
263
|
-
channel.currentSessionId = data.currentSessionId;
|
|
264
|
-
}
|
|
265
|
-
expandedAgents.add(channelId);
|
|
266
|
-
renderChannels();
|
|
267
|
-
} catch (err) {
|
|
268
|
-
console.error('Failed to create new session:', err);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async function switchSession(channelId, sessionId, e) {
|
|
273
|
-
if (e) e.stopPropagation();
|
|
274
|
-
if (!channelId || !sessionId) return;
|
|
275
|
-
if (channelId === currentChannelId && sessionId === currentSessionId) return;
|
|
276
|
-
|
|
277
|
-
// 先保存当前 session 的本地消息
|
|
278
|
-
if (currentChannelId && currentSessionId) {
|
|
279
|
-
saveCurrentSessionMessages();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const res = await fetch(`/channels/${channelId}/sessions/${sessionId}/switch`, { method: 'POST' });
|
|
284
|
-
if (!res.ok) throw new Error('switch failed');
|
|
285
|
-
const channel = channels.find(c => c.id === channelId);
|
|
286
|
-
if (channel) {
|
|
287
|
-
channel.currentSessionId = sessionId;
|
|
288
|
-
await saveChannels();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// 切换到目标 agent + session
|
|
292
|
-
await selectChannel(channelId, sessionId);
|
|
293
|
-
renderChannels();
|
|
294
|
-
} catch (err) {
|
|
295
|
-
console.error('Failed to switch session:', err);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function deleteSession(channelId, sessionId, e) {
|
|
300
|
-
if (e) e.stopPropagation();
|
|
301
|
-
if (!confirm('确定要删除该会话吗?此操作不可撤销。')) return;
|
|
302
|
-
try {
|
|
303
|
-
const res = await fetch(`/channels/${channelId}/sessions/${sessionId}`, { method: 'DELETE' });
|
|
304
|
-
if (!res.ok) {
|
|
305
|
-
const err = await res.json().catch(() => ({}));
|
|
306
|
-
alert(err.error || '删除失败');
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
const data = await res.json();
|
|
310
|
-
const channel = channels.find(c => c.id === channelId);
|
|
311
|
-
if (channel) {
|
|
312
|
-
if (channel.sessions) {
|
|
313
|
-
channel.sessions = channel.sessions.filter(s => s.id !== sessionId);
|
|
314
|
-
}
|
|
315
|
-
if (data.currentSessionId) {
|
|
316
|
-
channel.currentSessionId = data.currentSessionId;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 如果删的是当前打开的会话,切换到新的当前会话
|
|
321
|
-
if (channelId === currentChannelId && sessionId === currentSessionId) {
|
|
322
|
-
if (data.currentSessionId) {
|
|
323
|
-
currentSessionId = data.currentSessionId;
|
|
324
|
-
const container = messagesContainers.get(channelId);
|
|
325
|
-
if (container) container.innerHTML = '';
|
|
326
|
-
await loadSession(channelId);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
renderChannels();
|
|
330
|
-
} catch (err) {
|
|
331
|
-
console.error('Failed to delete session:', err);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let _saveSessionMessagesDirty = false;
|
|
336
|
-
let _saveSessionMessagesTimer = null;
|
|
337
|
-
function saveCurrentSessionMessages() {
|
|
338
|
-
if (!currentChannelId || !currentSessionId) return;
|
|
339
|
-
// 内存保护: 多次快速调用合并成一个, 避免在切会话时反复 .textContent 读 DOM
|
|
340
|
-
// (每个 textContent 会序列化整棵子树, 200 条消息 = 几百 MB 临时字符串)
|
|
341
|
-
_saveSessionMessagesDirty = true;
|
|
342
|
-
if (_saveSessionMessagesTimer) return;
|
|
343
|
-
_saveSessionMessagesTimer = setTimeout(() => {
|
|
344
|
-
_saveSessionMessagesTimer = null;
|
|
345
|
-
if (!_saveSessionMessagesDirty) return;
|
|
346
|
-
_saveSessionMessagesDirty = false;
|
|
347
|
-
if (!currentChannelId || !currentSessionId) return;
|
|
348
|
-
const container = messagesContainers.get(currentChannelId);
|
|
349
|
-
if (!container) return;
|
|
350
|
-
const messages = Array.from(container.querySelectorAll('.message')).map(msg => ({
|
|
351
|
-
type: msg.classList.contains('message-user') ? 'user' : 'ai',
|
|
352
|
-
content: msg.querySelector('.message-content')?.textContent || ''
|
|
353
|
-
}));
|
|
354
|
-
if (messages.length > 0) {
|
|
355
|
-
sessionMessages.set(`${currentChannelId}:${currentSessionId}`, messages);
|
|
356
|
-
}
|
|
357
|
-
}, 50);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async function saveChannels() {
|
|
361
|
-
// 简单地 re-fetch,保持本地 channels 与服务端一致
|
|
362
|
-
// 改成走 scheduleChannelsRefresh, 多个调用合并成一个请求 — 减少内存峰值和后端压力
|
|
363
|
-
scheduleChannelsRefresh();
|
|
364
|
-
await new Promise(r => setTimeout(r, 600));
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
let channelRefreshTimer = null;
|
|
368
|
-
let channelRefreshInFlight = null;
|
|
369
|
-
function scheduleChannelsRefresh() {
|
|
370
|
-
if (channelRefreshTimer) return;
|
|
371
|
-
channelRefreshTimer = setTimeout(async () => {
|
|
372
|
-
channelRefreshTimer = null;
|
|
373
|
-
if (channelRefreshInFlight) return channelRefreshInFlight;
|
|
374
|
-
channelRefreshInFlight = (async () => {
|
|
375
|
-
try {
|
|
376
|
-
const res = await fetch('/channels');
|
|
377
|
-
if (res.ok) {
|
|
378
|
-
const fresh = await res.json();
|
|
379
|
-
channels = fresh;
|
|
380
|
-
renderChannels();
|
|
381
|
-
}
|
|
382
|
-
} catch (err) {
|
|
383
|
-
console.error('Failed to re-fetch channels:', err);
|
|
384
|
-
} finally {
|
|
385
|
-
channelRefreshInFlight = null;
|
|
386
|
-
}
|
|
387
|
-
})();
|
|
388
|
-
return channelRefreshInFlight;
|
|
389
|
-
}, 800);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function toggleAgentExpand(channelId, e) {
|
|
393
|
-
if (e) e.stopPropagation();
|
|
394
|
-
if (expandedAgents.has(channelId)) {
|
|
395
|
-
expandedAgents.delete(channelId);
|
|
396
|
-
} else {
|
|
397
|
-
expandedAgents.add(channelId);
|
|
398
|
-
}
|
|
399
|
-
renderChannels();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function renderChannels() {
|
|
403
|
-
if (!channelList) return;
|
|
404
|
-
channelList.innerHTML = '';
|
|
405
|
-
|
|
406
|
-
const fragment = document.createDocumentFragment();
|
|
407
|
-
|
|
408
|
-
// 滚动可见性监听只绑定一次 (channelList 是同一个 DOM 节点,
|
|
409
|
-
// renderChannels 每次清空 innerHTML 都会重渲, 不能重复 addEventListener)
|
|
410
|
-
if (!channelList._scrollListenersBound) {
|
|
411
|
-
const onUserScroll = () => {
|
|
412
|
-
channelList.classList.add('is-scrolling');
|
|
413
|
-
if (channelList._scrollIdleTimer) clearTimeout(channelList._scrollIdleTimer);
|
|
414
|
-
channelList._scrollIdleTimer = setTimeout(() => {
|
|
415
|
-
channelList.classList.remove('is-scrolling');
|
|
416
|
-
}, 1200);
|
|
417
|
-
};
|
|
418
|
-
channelList.addEventListener('wheel', onUserScroll, { passive: true });
|
|
419
|
-
channelList.addEventListener('touchmove', onUserScroll, { passive: true });
|
|
420
|
-
channelList.addEventListener('keydown', (ev) => {
|
|
421
|
-
if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End'].includes(ev.key)) {
|
|
422
|
-
onUserScroll();
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
channelList._scrollListenersBound = true;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
channels.forEach(ch => {
|
|
429
|
-
const li = document.createElement('li');
|
|
430
|
-
const isExpanded = expandedAgents.has(ch.id);
|
|
431
|
-
li.className = `agent-group ${isExpanded ? 'expanded' : ''}`;
|
|
432
|
-
li.dataset.channelId = ch.id;
|
|
433
|
-
|
|
434
|
-
// --- 智能体行 ---
|
|
435
|
-
const row = document.createElement('div');
|
|
436
|
-
row.className = `agent-row ${ch.id === currentChannelId ? 'active' : ''}`;
|
|
437
|
-
|
|
438
|
-
// 找到当前智能体(如果它是激活的)的当前 session
|
|
439
|
-
const currentSess = (ch.id === currentChannelId && Array.isArray(ch.sessions))
|
|
440
|
-
? ch.sessions.find(s => s.id === ch.currentSessionId)
|
|
441
|
-
: null;
|
|
442
|
-
const currentSessLabel = currentSess ? formatSessionName(currentSess) : '';
|
|
443
|
-
const sessionCount = Array.isArray(ch.sessions) ? ch.sessions.length : 0;
|
|
444
|
-
|
|
445
|
-
const walletBadge = ch.walletAddress
|
|
446
|
-
? `<span class="agent-wallet-badge" title="已绑定钱包: ${escapeHtml(ch.walletAddress)}">⛓</span>`
|
|
447
|
-
: '';
|
|
448
|
-
const toolsBadge = ch.autoInvokeTools
|
|
449
|
-
? `<span class="agent-tools-badge" title="自动工具调用已开启">⚡</span>`
|
|
450
|
-
: '';
|
|
451
|
-
|
|
452
|
-
row.innerHTML = `
|
|
453
|
-
<svg class="agent-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
454
|
-
<polyline points="9 18 15 12 9 6"></polyline>
|
|
455
|
-
</svg>
|
|
456
|
-
<div class="channel-icon">💬</div>
|
|
457
|
-
<span class="channel-name" title="${escapeHtml(ch.name)}">${escapeHtml(ch.name)}</span>
|
|
458
|
-
<span class="agent-row-meta">
|
|
459
|
-
${walletBadge}
|
|
460
|
-
${toolsBadge}
|
|
461
|
-
${sessionCount > 1 ? `<span class="agent-session-count" title="${sessionCount} 个会话">${sessionCount}</span>` : ''}
|
|
462
|
-
${currentSessLabel ? `<span class="agent-current-session" title="当前会话:${escapeHtml(currentSessLabel)}">· ${escapeHtml(currentSessLabel)}</span>` : ''}
|
|
463
|
-
<button class="agent-config-btn" title="配置智能体 (钱包 / 工具)">
|
|
464
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
465
|
-
<circle cx="12" cy="12" r="3"></circle>
|
|
466
|
-
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
|
467
|
-
</svg>
|
|
468
|
-
</button>
|
|
469
|
-
<button class="channel-delete" title="删除智能体">×</button>
|
|
470
|
-
</span>
|
|
471
|
-
`;
|
|
472
|
-
|
|
473
|
-
// 行点击:切换展开;点击名字/图标区域则切到该智能体
|
|
474
|
-
row.addEventListener('click', (ev) => {
|
|
475
|
-
// 如果点在删除/配置按钮上, 单独处理
|
|
476
|
-
if (ev.target.closest('.channel-delete')
|
|
477
|
-
|| ev.target.closest('.agent-config-btn')) return;
|
|
478
|
-
if (ev.target.closest('.agent-caret')) {
|
|
479
|
-
toggleAgentExpand(ch.id, ev);
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
toggleAgentExpand(ch.id, ev);
|
|
483
|
-
if (ch.id !== currentChannelId) {
|
|
484
|
-
expandSidebar();
|
|
485
|
-
selectChannel(ch.id);
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// 智能体删除
|
|
490
|
-
row.querySelector('.channel-delete').addEventListener('click', (ev) => deleteChannel(ch.id, ev));
|
|
491
|
-
// 配置按钮: 打开同一个 modal 编辑已有智能体
|
|
492
|
-
row.querySelector('.agent-config-btn').addEventListener('click', (ev) => {
|
|
493
|
-
ev.stopPropagation();
|
|
494
|
-
openAgentAddModal(ch);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
li.appendChild(row);
|
|
498
|
-
|
|
499
|
-
// --- Session 列表(仅展开时渲染 DOM)---
|
|
500
|
-
const sessionUl = document.createElement('ul');
|
|
501
|
-
sessionUl.className = 'session-list';
|
|
502
|
-
if (isExpanded) {
|
|
503
|
-
// "新建会话" 按钮 — 放在 session 列表最前面, 始终可见
|
|
504
|
-
const newSessLi = document.createElement('li');
|
|
505
|
-
newSessLi.className = 'session-new-item';
|
|
506
|
-
newSessLi.setAttribute('role', 'button');
|
|
507
|
-
newSessLi.setAttribute('tabindex', '0');
|
|
508
|
-
newSessLi.title = '新建会话';
|
|
509
|
-
newSessLi.innerHTML = `
|
|
510
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
511
|
-
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
512
|
-
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
513
|
-
</svg>
|
|
514
|
-
<span>新建会话</span>
|
|
515
|
-
`;
|
|
516
|
-
const onNewSession = (ev) => {
|
|
517
|
-
ev.stopPropagation();
|
|
518
|
-
createNewSessionForChannel(ch.id, ev);
|
|
519
|
-
};
|
|
520
|
-
newSessLi.addEventListener('click', onNewSession);
|
|
521
|
-
newSessLi.addEventListener('keydown', (ev) => {
|
|
522
|
-
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
523
|
-
ev.preventDefault();
|
|
524
|
-
onNewSession(ev);
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
sessionUl.appendChild(newSessLi);
|
|
528
|
-
|
|
529
|
-
const sessions = Array.isArray(ch.sessions) ? ch.sessions : [];
|
|
530
|
-
sessions.forEach(sess => {
|
|
531
|
-
const sessLi = document.createElement('li');
|
|
532
|
-
const isActive = ch.id === currentChannelId && sess.id === ch.currentSessionId;
|
|
533
|
-
sessLi.className = `session-item ${isActive ? 'active' : ''}`;
|
|
534
|
-
sessLi.innerHTML = `
|
|
535
|
-
<span class="session-name" title="${escapeHtml(formatSessionName(sess))}">${escapeHtml(formatSessionName(sess))}</span>
|
|
536
|
-
<button class="session-delete" title="删除会话">×</button>
|
|
537
|
-
`;
|
|
538
|
-
sessLi.addEventListener('click', (ev) => {
|
|
539
|
-
if (ev.target.closest('.session-delete')) return;
|
|
540
|
-
switchSession(ch.id, sess.id, ev);
|
|
541
|
-
});
|
|
542
|
-
sessLi.querySelector('.session-delete').addEventListener('click', (ev) => deleteSession(ch.id, sess.id, ev));
|
|
543
|
-
sessionUl.appendChild(sessLi);
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
li.appendChild(sessionUl);
|
|
547
|
-
|
|
548
|
-
fragment.appendChild(li);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
channelList.appendChild(fragment);
|
|
552
|
-
|
|
553
|
-
// header 钱包徽章计数: 只在 channels 变化时刷新, 避免每次 renderChannels 都重算
|
|
554
|
-
refreshWalletBadge();
|
|
555
|
-
|
|
556
|
-
// 把当前激活的 channel 平滑滚到视口内 — 用户切换后不会看不到
|
|
557
|
-
// 只在非用户主动滚动状态下执行, 避免与正在进行的滚动冲突
|
|
558
|
-
if (currentChannelId) {
|
|
559
|
-
requestAnimationFrame(() => scrollActiveChannelIntoView(false));
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/** 把当前激活的 channel 滚到侧边栏视口内 */
|
|
564
|
-
function scrollActiveChannelIntoView(smooth = true) {
|
|
565
|
-
if (!channelList || !currentChannelId) return;
|
|
566
|
-
const active = channelList.querySelector(`.agent-group[data-channel-id="${currentChannelId}"]`);
|
|
567
|
-
if (!active) return;
|
|
568
|
-
const listRect = channelList.getBoundingClientRect();
|
|
569
|
-
const itemRect = active.getBoundingClientRect();
|
|
570
|
-
const margin = 24; // 视口上下各留 24px
|
|
571
|
-
if (itemRect.top < listRect.top + margin) {
|
|
572
|
-
channelList.scrollBy({ top: itemRect.top - listRect.top - margin, behavior: smooth ? 'smooth' : 'auto' });
|
|
573
|
-
} else if (itemRect.bottom > listRect.bottom - margin) {
|
|
574
|
-
channelList.scrollBy({ top: itemRect.bottom - listRect.bottom + margin, behavior: smooth ? 'smooth' : 'auto' });
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function formatSessionName(sess) {
|
|
579
|
-
if (!sess) return '新会话';
|
|
580
|
-
if (sess.preview && sess.preview.trim()) return sess.preview.trim();
|
|
581
|
-
const id = sess.id || '';
|
|
582
|
-
return id ? `会话 ${id.slice(-6)}` : '新会话';
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function escapeHtml(s) {
|
|
586
|
-
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
587
|
-
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
588
|
-
}[c]));
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function renderCollapsedChannels() {
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function ensureMessageContainer(channelId) {
|
|
596
|
-
if (!messagesContainers.has(channelId)) {
|
|
597
|
-
const container = document.createElement('div');
|
|
598
|
-
container.className = 'channel-messages';
|
|
599
|
-
container.id = `channel-messages-${channelId}`;
|
|
600
|
-
container.style.display = 'none';
|
|
601
|
-
messagesEl.appendChild(container);
|
|
602
|
-
messagesContainers.set(channelId, container);
|
|
603
|
-
}
|
|
604
|
-
return messagesContainers.get(channelId);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function showChannelView(channelId) {
|
|
608
|
-
// Hide all channel message containers (不要 innerHTML='' 销毁, 保留以便快速切换)
|
|
609
|
-
messagesContainers.forEach((container, cid) => {
|
|
610
|
-
container.style.display = 'none';
|
|
611
|
-
});
|
|
612
|
-
// Show the selected channel's container
|
|
613
|
-
const container = messagesContainers.get(channelId);
|
|
614
|
-
if (container) {
|
|
615
|
-
container.style.display = 'block';
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
async function selectChannel(channelId, targetSessionId = null) {
|
|
620
|
-
console.log('[selectChannel] 开始切换到:', channelId, 'targetSession:', targetSessionId);
|
|
621
|
-
|
|
622
|
-
// 立即更新当前频道 ID
|
|
623
|
-
currentChannelId = channelId;
|
|
624
|
-
reconnectAttempts.set(channelId, 0);
|
|
625
|
-
|
|
626
|
-
// 找到当前频道和 session
|
|
627
|
-
const channel = channels.find(c => c.id === channelId);
|
|
628
|
-
if (channel) {
|
|
629
|
-
if (channelNameEl) channelNameEl.textContent = channel.name;
|
|
630
|
-
currentSessionId = targetSessionId || channel.currentSessionId || 'default';
|
|
631
|
-
if (targetSessionId) {
|
|
632
|
-
channel.currentSessionId = targetSessionId;
|
|
633
|
-
}
|
|
634
|
-
// 自动展开当前智能体的会话列表,让用户能切换会话
|
|
635
|
-
expandedAgents.add(channelId);
|
|
636
|
-
console.log('[selectChannel] 频道:', channel.name, 'session:', currentSessionId);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
renderChannels();
|
|
640
|
-
|
|
641
|
-
// 确保该频道有消息容器
|
|
642
|
-
const container = ensureMessageContainer(channelId);
|
|
643
|
-
|
|
644
|
-
// 切换到该频道的视图
|
|
645
|
-
showChannelView(channelId);
|
|
646
|
-
|
|
647
|
-
// 如果还没有 SSE 连接,建立连接
|
|
648
|
-
if (!eventSources.has(channelId)) {
|
|
649
|
-
console.log('[selectChannel] 建立 SSE 连接');
|
|
650
|
-
connect(channelId);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// 直接从 server 拉 session 消息 (container 跨 session 共享, 先清空再加载)
|
|
654
|
-
container.innerHTML = '';
|
|
655
|
-
try {
|
|
656
|
-
const res = await fetch(`/sessions/${channelId}?sessionId=${encodeURIComponent(currentSessionId)}`);
|
|
657
|
-
const session = await res.json();
|
|
658
|
-
if (session.messages && session.messages.length > 0) {
|
|
659
|
-
session.messages.forEach(msg => {
|
|
660
|
-
addMessage(msg.content, msg.type, false, container);
|
|
661
|
-
});
|
|
662
|
-
} else {
|
|
663
|
-
addMessage('你好!我是 Bolloon Agent。有什么我可以帮你的吗?', 'ai', false, container);
|
|
664
|
-
}
|
|
665
|
-
} catch (err) {
|
|
666
|
-
console.error('[selectChannel] 加载 session 失败:', err);
|
|
667
|
-
addMessage('你好!我是 Bolloon Agent。有什么我可以帮你的吗?', 'ai', false, container);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
async function loadSession(channelId, sessionId = null) {
|
|
672
|
-
const container = messagesContainers.get(channelId);
|
|
673
|
-
if (!container) return;
|
|
674
|
-
const targetSessionId = sessionId || currentSessionId || 'default';
|
|
675
|
-
try {
|
|
676
|
-
const res = await fetch(`/sessions/${channelId}?sessionId=${encodeURIComponent(targetSessionId)}`);
|
|
677
|
-
const session = await res.json();
|
|
678
|
-
container.innerHTML = '';
|
|
679
|
-
if (session.messages && session.messages.length > 0) {
|
|
680
|
-
session.messages.forEach(msg => {
|
|
681
|
-
addMessage(msg.content, msg.type, false, container);
|
|
682
|
-
});
|
|
683
|
-
} else {
|
|
684
|
-
addMessage('你好!我是 Bolloon Agent。有什么我可以帮你的吗?', 'ai', false, container);
|
|
685
|
-
}
|
|
686
|
-
} catch (err) {
|
|
687
|
-
console.error('Failed to load session:', err);
|
|
688
|
-
container.innerHTML = '';
|
|
689
|
-
addMessage('你好!我是 Bolloon Agent。有什么我可以帮你的吗?', 'ai', false, container);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function addMessage(content, type, save = true, container) {
|
|
694
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
695
|
-
|
|
696
|
-
// 浏览器侧内存保护: 单个 channel 的消息容器超过 MAX_MESSAGES_PER_CHANNEL
|
|
697
|
-
// 就从最旧的开始淘汰。SSE 流式场景下不淘汰 (save=true 时不裁剪),
|
|
698
|
-
// 因为流消息一般很短; 只对长会话加载 (save=false) 做上限。
|
|
699
|
-
// 上限是 200 条: 大约相当于 100 轮对话, 足够日常使用, 又把 DOM 控制在 5MB 以内.
|
|
700
|
-
if (!save && msgContainer && msgContainer.children.length > 200) {
|
|
701
|
-
const toRemove = msgContainer.children.length - 200;
|
|
702
|
-
for (let i = 0; i < toRemove; i++) {
|
|
703
|
-
const first = msgContainer.firstElementChild;
|
|
704
|
-
if (first) msgContainer.removeChild(first);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
// 去重:只有 save=true 时(来自 SSE)才去重,save=false 时(来自 session 加载)直接显示
|
|
708
|
-
if (save) {
|
|
709
|
-
const lastContent = type === 'user' ? lastUserCommand : lastAiContent;
|
|
710
|
-
if (lastContent && content === lastContent) {
|
|
711
|
-
console.log(`[addMessage] 跳过重复的 ${type} 消息`);
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
if (type === 'user') {
|
|
715
|
-
lastUserCommand = content;
|
|
716
|
-
} else {
|
|
717
|
-
lastAiContent = content;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const div = document.createElement('div');
|
|
722
|
-
div.className = `message message-${type}`;
|
|
723
|
-
|
|
724
|
-
// 清理工具结果容器(当新的 AI 消息到达时)
|
|
725
|
-
if (type === 'ai' && toolResultContainer) {
|
|
726
|
-
toolResultContainer.remove();
|
|
727
|
-
toolResultContainer = null;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// 清理内容:移除 tool call 标记和其他不应该显示的内容
|
|
731
|
-
let cleanContent = content
|
|
732
|
-
.replace(/\[TOOL_CALL\][\s\S]*?\[\/TOOL_CALL\]/g, '')
|
|
733
|
-
.replace(/TOOL_CALL[\s\S]*?\/TOOL_CALL/g, '')
|
|
734
|
-
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
|
735
|
-
.replace(/{\s*"tool":[\s\S]*?}/g, '')
|
|
736
|
-
// 兼容 pi-sdk LLM 输出: {tool => "name", args => {...}}
|
|
737
|
-
.replace(/\{\s*tool\s*=>\s*["'][^"']+["']\s*(?:,\s*args\s*=>\s*\{[\s\S]*?\})?\s*\}/g, '')
|
|
738
|
-
.replace(/\[Function[^\]]*\]\s*/g, '')
|
|
739
|
-
.trim();
|
|
740
|
-
|
|
741
|
-
// 处理思维链内容(</think> 标签)- 折叠显示
|
|
742
|
-
const thinkMatch = cleanContent.match(/<think>([\s\S]*?)<\/think>/);
|
|
743
|
-
let mainContent = cleanContent;
|
|
744
|
-
let thinkContainer = null;
|
|
745
|
-
|
|
746
|
-
if (thinkMatch) {
|
|
747
|
-
const thinkContent = thinkMatch[1].trim();
|
|
748
|
-
mainContent = cleanContent.replace(/<think>[\s\S]*?<\/think>/, '').trim();
|
|
749
|
-
|
|
750
|
-
// 创建思维链折叠区域
|
|
751
|
-
thinkContainer = document.createElement('div');
|
|
752
|
-
thinkContainer.className = 'think-container';
|
|
753
|
-
|
|
754
|
-
const thinkToggle = document.createElement('div');
|
|
755
|
-
thinkToggle.className = 'think-toggle';
|
|
756
|
-
thinkToggle.innerHTML = '💭 思考过程 <span class="think-arrow">▸</span>';
|
|
757
|
-
thinkToggle.onclick = function() {
|
|
758
|
-
const details = thinkContainer.querySelector('.think-content');
|
|
759
|
-
const arrow = thinkToggle.querySelector('.think-arrow');
|
|
760
|
-
if (details.style.display === 'none') {
|
|
761
|
-
details.style.display = 'block';
|
|
762
|
-
arrow.textContent = '▾';
|
|
763
|
-
} else {
|
|
764
|
-
details.style.display = 'none';
|
|
765
|
-
arrow.textContent = '▸';
|
|
766
|
-
}
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
const thinkDiv = document.createElement('div');
|
|
770
|
-
thinkDiv.className = 'think-content';
|
|
771
|
-
thinkDiv.style.display = 'none'; // 默认折叠
|
|
772
|
-
thinkDiv.innerHTML = `<pre>${thinkContent.replace(/</g, '<').replace(/>/g, '>')}</pre>`;
|
|
773
|
-
|
|
774
|
-
thinkContainer.appendChild(thinkToggle);
|
|
775
|
-
thinkContainer.appendChild(thinkDiv);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const envMatch = mainContent.match(/^(.+?)\n<environment_details>([\s\S]*?)<\/environment_details>\n([\s\S]*)$/);
|
|
779
|
-
|
|
780
|
-
if (envMatch) {
|
|
781
|
-
const identity = envMatch[1].trim();
|
|
782
|
-
const envDetails = envMatch[2].trim();
|
|
783
|
-
const messageBody = envMatch[3].trim();
|
|
784
|
-
|
|
785
|
-
const header = document.createElement('div');
|
|
786
|
-
header.className = 'message-header';
|
|
787
|
-
header.textContent = identity;
|
|
788
|
-
div.appendChild(header);
|
|
789
|
-
|
|
790
|
-
// 环境信息区域(可折叠,默认折叠)
|
|
791
|
-
const envContainer = document.createElement('div');
|
|
792
|
-
envContainer.className = 'env-container';
|
|
793
|
-
|
|
794
|
-
const envToggle = document.createElement('div');
|
|
795
|
-
envToggle.className = 'env-toggle';
|
|
796
|
-
envToggle.innerHTML = '⚙️ 环境信息 <span class="env-arrow">▸</span>';
|
|
797
|
-
envToggle.onclick = function() {
|
|
798
|
-
const details = envContainer.querySelector('.environment-details');
|
|
799
|
-
const arrow = envToggle.querySelector('.env-arrow');
|
|
800
|
-
if (details.style.display === 'none') {
|
|
801
|
-
details.style.display = 'block';
|
|
802
|
-
arrow.textContent = '▾';
|
|
803
|
-
} else {
|
|
804
|
-
details.style.display = 'none';
|
|
805
|
-
arrow.textContent = '▸';
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
const envDiv = document.createElement('div');
|
|
810
|
-
envDiv.className = 'environment-details';
|
|
811
|
-
envDiv.style.display = 'none'; // 默认折叠
|
|
812
|
-
envDiv.innerHTML = `<pre>${envDetails}</pre>`;
|
|
813
|
-
|
|
814
|
-
envContainer.appendChild(envToggle);
|
|
815
|
-
envContainer.appendChild(envDiv);
|
|
816
|
-
div.appendChild(envContainer);
|
|
817
|
-
|
|
818
|
-
// 思考在环境信息下面
|
|
819
|
-
if (thinkContainer) {
|
|
820
|
-
div.appendChild(thinkContainer);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (messageBody) {
|
|
824
|
-
const bubble = document.createElement('div');
|
|
825
|
-
bubble.className = `bubble bubble-${type}`;
|
|
826
|
-
bubble.innerHTML = marked.parse(messageBody);
|
|
827
|
-
div.appendChild(bubble);
|
|
828
|
-
}
|
|
829
|
-
} else if (cleanContent) {
|
|
830
|
-
// 没有环境信息时,思考放在消息之前
|
|
831
|
-
if (thinkContainer) {
|
|
832
|
-
div.appendChild(thinkContainer);
|
|
833
|
-
}
|
|
834
|
-
const bubble = document.createElement('div');
|
|
835
|
-
bubble.className = `bubble bubble-${type}`;
|
|
836
|
-
bubble.innerHTML = marked.parse(cleanContent);
|
|
837
|
-
div.appendChild(bubble);
|
|
838
|
-
} else {
|
|
839
|
-
return; // 没有有效内容,不显示空消息
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// 提取纯文本内容用于复制(去除 HTML 标签)
|
|
843
|
-
const rawContent = cleanContent
|
|
844
|
-
.replace(/<[^>]+>/g, '')
|
|
845
|
-
.replace(/</g, '<')
|
|
846
|
-
.replace(/>/g, '>')
|
|
847
|
-
.replace(/&/g, '&')
|
|
848
|
-
.replace(/ /g, ' ');
|
|
849
|
-
|
|
850
|
-
const time = document.createElement('div');
|
|
851
|
-
time.className = 'time';
|
|
852
|
-
time.textContent = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
853
|
-
|
|
854
|
-
// AI 消息添加操作按钮
|
|
855
|
-
if (type === 'ai') {
|
|
856
|
-
const actionsDiv = document.createElement('div');
|
|
857
|
-
actionsDiv.className = 'message-actions';
|
|
858
|
-
|
|
859
|
-
const copyBtn = document.createElement('button');
|
|
860
|
-
copyBtn.className = 'action-btn copy-btn';
|
|
861
|
-
copyBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> 复制`;
|
|
862
|
-
copyBtn.title = '复制消息';
|
|
863
|
-
copyBtn.onclick = function() {
|
|
864
|
-
navigator.clipboard.writeText(rawContent).then(() => {
|
|
865
|
-
copyBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg> 已复制`;
|
|
866
|
-
setTimeout(() => {
|
|
867
|
-
copyBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> 复制`;
|
|
868
|
-
}, 2000);
|
|
869
|
-
});
|
|
870
|
-
};
|
|
871
|
-
|
|
872
|
-
const regenerateBtn = document.createElement('button');
|
|
873
|
-
regenerateBtn.className = 'action-btn regenerate-btn';
|
|
874
|
-
regenerateBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"></path></svg> 重新回答`;
|
|
875
|
-
regenerateBtn.title = '重新生成回复';
|
|
876
|
-
regenerateBtn.onclick = function() {
|
|
877
|
-
regenerateBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin"><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"></path></svg> 生成中...`;
|
|
878
|
-
regenerateBtn.disabled = true;
|
|
879
|
-
// 获取当前频道对应的用户消息
|
|
880
|
-
const messages = div.parentElement.querySelectorAll('.message');
|
|
881
|
-
let lastUserMsg = '';
|
|
882
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
883
|
-
const msg = messages[i];
|
|
884
|
-
if (msg.classList.contains('message-user')) {
|
|
885
|
-
const bubble = msg.querySelector('.bubble');
|
|
886
|
-
if (bubble) {
|
|
887
|
-
lastUserMsg = bubble.textContent || bubble.innerText || '';
|
|
888
|
-
break;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
// 调用重新生成 API
|
|
893
|
-
fetch('/regenerate', {
|
|
894
|
-
method: 'POST',
|
|
895
|
-
headers: { 'Content-Type': 'application/json' },
|
|
896
|
-
body: JSON.stringify({ channelId: currentChannelId, userMessage: lastUserMsg })
|
|
897
|
-
}).then(res => {
|
|
898
|
-
if (!res.ok) {
|
|
899
|
-
throw new Error('regenerate failed');
|
|
900
|
-
}
|
|
901
|
-
}).catch(err => {
|
|
902
|
-
console.error('重新生成失败:', err);
|
|
903
|
-
regenerateBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"></path></svg> 失败`;
|
|
904
|
-
setTimeout(() => {
|
|
905
|
-
regenerateBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"></path></svg> 重新回答`;
|
|
906
|
-
regenerateBtn.disabled = false;
|
|
907
|
-
}, 2000);
|
|
908
|
-
});
|
|
909
|
-
};
|
|
910
|
-
|
|
911
|
-
actionsDiv.appendChild(copyBtn);
|
|
912
|
-
actionsDiv.appendChild(regenerateBtn);
|
|
913
|
-
div.appendChild(actionsDiv);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
div.appendChild(time);
|
|
917
|
-
msgContainer.appendChild(div);
|
|
918
|
-
|
|
919
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// Agent status bar — sits between the message list and the input box.
|
|
923
|
-
// Two visual states: "planning" (spinner) and "executing" (glowing icon).
|
|
924
|
-
// The text alternates to convey the action loop.
|
|
925
|
-
let agentStatusState = null; // 'planning' | 'executing' | null
|
|
926
|
-
let agentStatusTextIdx = 0;
|
|
927
|
-
|
|
928
|
-
const AGENT_STATUS_TEXTS = {
|
|
929
|
-
planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
|
|
930
|
-
executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
function setAgentStatus(state) {
|
|
934
|
-
if (!agentStatusEl || !agentStatusTextEl) return;
|
|
935
|
-
if (state === null) {
|
|
936
|
-
agentStatusEl.hidden = true;
|
|
937
|
-
agentStatusEl.removeAttribute('data-mode');
|
|
938
|
-
agentStatusState = null;
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
agentStatusEl.hidden = false;
|
|
942
|
-
agentStatusEl.setAttribute('data-mode', state);
|
|
943
|
-
agentStatusState = state;
|
|
944
|
-
// 重排一下文本, 避免长时间停留过于单调
|
|
945
|
-
agentStatusTextIdx = (agentStatusTextIdx + 1) % AGENT_STATUS_TEXTS[state].length;
|
|
946
|
-
agentStatusTextEl.textContent = AGENT_STATUS_TEXTS[state][agentStatusTextIdx];
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function showTyping(container) {
|
|
950
|
-
hideTyping();
|
|
951
|
-
// 兼容旧路径: container 参数保留但不再使用, status bar 是全局唯一的
|
|
952
|
-
void container;
|
|
953
|
-
setAgentStatus('planning');
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
function hideTyping() {
|
|
957
|
-
setAgentStatus(null);
|
|
958
|
-
// 兜底: 旧版本的 #typing 元素可能还残留在 DOM 里, 顺手清掉
|
|
959
|
-
const old = document.getElementById('typing');
|
|
960
|
-
if (old) old.remove();
|
|
961
|
-
hideStreaming();
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
let streamingMessageEl = null;
|
|
965
|
-
|
|
966
|
-
function showStreaming(container) {
|
|
967
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
968
|
-
hideStreaming();
|
|
969
|
-
streamingMessageEl = document.createElement('div');
|
|
970
|
-
streamingMessageEl.className = 'message message-ai';
|
|
971
|
-
streamingMessageEl.id = 'streaming';
|
|
972
|
-
const bubble = document.createElement('div');
|
|
973
|
-
bubble.className = 'bubble bubble-ai streaming-content';
|
|
974
|
-
streamingMessageEl.appendChild(bubble);
|
|
975
|
-
msgContainer.appendChild(streamingMessageEl);
|
|
976
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function hideStreaming() {
|
|
980
|
-
if (streamingMessageEl) {
|
|
981
|
-
streamingMessageEl.remove();
|
|
982
|
-
streamingMessageEl = null;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function updateStreamingContent(content, container) {
|
|
987
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
988
|
-
if (streamingMessageEl) {
|
|
989
|
-
const bubble = streamingMessageEl.querySelector('.streaming-content');
|
|
990
|
-
if (bubble) {
|
|
991
|
-
bubble.textContent = content;
|
|
992
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function handleStreamEvent(data, container) {
|
|
998
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
999
|
-
// 始终确保有工作流显示区域
|
|
1000
|
-
if (!workflowDisplayEl) {
|
|
1001
|
-
workflowDisplayEl = createWorkflowDisplay();
|
|
1002
|
-
msgContainer.appendChild(workflowDisplayEl);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
if (data.streamType === 'thinking') {
|
|
1006
|
-
showStreaming(msgContainer);
|
|
1007
|
-
updateStreamingContent(data.content || '思考中...', msgContainer);
|
|
1008
|
-
} else if (data.streamType === 'token') {
|
|
1009
|
-
showStreaming(msgContainer);
|
|
1010
|
-
const current = streamingMessageEl?.querySelector('.streaming-content')?.textContent || '';
|
|
1011
|
-
updateStreamingContent(current + data.content, msgContainer);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function handleStatusEvent(data, container) {
|
|
1016
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
1017
|
-
// 检查是否是工具调用结果
|
|
1018
|
-
const content = data.content || '';
|
|
1019
|
-
const isJsonResult = content.startsWith('{') && content.includes('"success"');
|
|
1020
|
-
|
|
1021
|
-
if (isJsonResult) {
|
|
1022
|
-
// 工具结果:折叠显示
|
|
1023
|
-
showToolResult(data.tool, content, msgContainer);
|
|
1024
|
-
} else {
|
|
1025
|
-
// 普通状态:流式显示
|
|
1026
|
-
showStreaming(msgContainer);
|
|
1027
|
-
const icon = data.tool ? `🔧 ${data.tool}: ` : '';
|
|
1028
|
-
updateStreamingContent(icon + data.content, msgContainer);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// 显示工具调用结果(折叠)
|
|
1033
|
-
let toolResultContainer = null;
|
|
1034
|
-
|
|
1035
|
-
function showToolResult(toolName, resultJson, container) {
|
|
1036
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
1037
|
-
// 清理之前的流式显示
|
|
1038
|
-
hideStreaming();
|
|
1039
|
-
|
|
1040
|
-
// 获取或创建工具结果容器
|
|
1041
|
-
if (!toolResultContainer) {
|
|
1042
|
-
toolResultContainer = document.createElement('div');
|
|
1043
|
-
toolResultContainer.className = 'tool-results-container';
|
|
1044
|
-
msgContainer.appendChild(toolResultContainer);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// 尝试解析并格式化 JSON
|
|
1048
|
-
let formattedResult = resultJson;
|
|
1049
|
-
try {
|
|
1050
|
-
const parsed = JSON.parse(resultJson);
|
|
1051
|
-
formattedResult = formatToolResult(parsed);
|
|
1052
|
-
} catch {}
|
|
1053
|
-
|
|
1054
|
-
// 创建折叠项
|
|
1055
|
-
const resultEl = document.createElement('div');
|
|
1056
|
-
resultEl.className = 'tool-result-item collapsed';
|
|
1057
|
-
|
|
1058
|
-
const toolDisplayName = toolName || '工具结果';
|
|
1059
|
-
const headerEl = document.createElement('div');
|
|
1060
|
-
headerEl.className = 'tool-result-header';
|
|
1061
|
-
headerEl.innerHTML = `
|
|
1062
|
-
<span class="tool-result-icon">🔧</span>
|
|
1063
|
-
<span class="tool-result-name">${toolDisplayName}</span>
|
|
1064
|
-
<span class="tool-result-toggle">▸</span>
|
|
1065
|
-
`;
|
|
1066
|
-
// 绑定事件处理器(避免内联 onclick)
|
|
1067
|
-
headerEl.addEventListener('click', () => {
|
|
1068
|
-
resultEl.classList.toggle('collapsed');
|
|
1069
|
-
resultEl.classList.toggle('expanded');
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
const contentEl = document.createElement('div');
|
|
1073
|
-
contentEl.className = 'tool-result-content';
|
|
1074
|
-
contentEl.innerHTML = `<pre>${formattedResult}</pre>`;
|
|
1075
|
-
|
|
1076
|
-
resultEl.appendChild(headerEl);
|
|
1077
|
-
resultEl.appendChild(contentEl);
|
|
1078
|
-
toolResultContainer.appendChild(resultEl);
|
|
1079
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// 格式化工具结果为易读格式
|
|
1083
|
-
function formatToolResult(obj, indent = 0) {
|
|
1084
|
-
const spaces = ' '.repeat(indent);
|
|
1085
|
-
|
|
1086
|
-
if (obj === null || obj === undefined) {
|
|
1087
|
-
return 'null';
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
if (typeof obj === 'object') {
|
|
1091
|
-
if (Array.isArray(obj)) {
|
|
1092
|
-
if (obj.length === 0) return '[]';
|
|
1093
|
-
return obj.map(item => spaces + '- ' + formatToolResult(item, indent + 1)).join('\n');
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const keys = Object.keys(obj);
|
|
1097
|
-
if (keys.length === 0) return '{}';
|
|
1098
|
-
|
|
1099
|
-
return keys.map(key => {
|
|
1100
|
-
const value = obj[key];
|
|
1101
|
-
if (typeof value === 'object') {
|
|
1102
|
-
return `${spaces}${key}:\n${formatToolResult(value, indent + 1)}`;
|
|
1103
|
-
}
|
|
1104
|
-
return `${spaces}${key}: ${value}`;
|
|
1105
|
-
}).join('\n');
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
return String(obj);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// 工作流状态显示
|
|
1112
|
-
let workflowDisplayEl = null;
|
|
1113
|
-
|
|
1114
|
-
function createWorkflowDisplay() {
|
|
1115
|
-
const container = document.createElement('div');
|
|
1116
|
-
container.id = 'workflow-display';
|
|
1117
|
-
container.className = 'workflow-display';
|
|
1118
|
-
container.innerHTML = `
|
|
1119
|
-
<div class="workflow-header">
|
|
1120
|
-
<span class="workflow-icon">🔄</span>
|
|
1121
|
-
<span class="workflow-title">工作流执行中</span>
|
|
1122
|
-
<span class="workflow-loop-count"></span>
|
|
1123
|
-
</div>
|
|
1124
|
-
<div class="workflow-steps-list"></div>
|
|
1125
|
-
<div class="workflow-streams"></div>
|
|
1126
|
-
`;
|
|
1127
|
-
return container;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
function handleTaskStatusEvent(data, container) {
|
|
1131
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
1132
|
-
console.log('[工作流] 任务状态:', data);
|
|
1133
|
-
|
|
1134
|
-
// 获取或创建工作流显示区域
|
|
1135
|
-
if (!workflowDisplayEl) {
|
|
1136
|
-
workflowDisplayEl = createWorkflowDisplay();
|
|
1137
|
-
msgContainer.appendChild(workflowDisplayEl);
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
const stepsList = workflowDisplayEl.querySelector('.workflow-steps-list');
|
|
1141
|
-
const header = workflowDisplayEl.querySelector('.workflow-header');
|
|
1142
|
-
|
|
1143
|
-
// 更新标题
|
|
1144
|
-
if (data.taskId) {
|
|
1145
|
-
header.querySelector('.workflow-title').textContent = `任务: ${data.taskId.substring(0, 12)}...`;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// 更新状态
|
|
1149
|
-
if (data.status) {
|
|
1150
|
-
const statusText = header.querySelector('.workflow-title');
|
|
1151
|
-
statusText.textContent = `任务 ${data.status === 'running' ? '执行中' : data.status}`;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// 更新进度
|
|
1155
|
-
if (data.progress !== undefined) {
|
|
1156
|
-
header.querySelector('.workflow-title').textContent = `进度: ${data.progress}%`;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// 更新步骤
|
|
1160
|
-
if (data.currentStep !== undefined && data.totalSteps) {
|
|
1161
|
-
const stepEl = document.createElement('div');
|
|
1162
|
-
stepEl.className = 'workflow-step-item';
|
|
1163
|
-
stepEl.innerHTML = `
|
|
1164
|
-
<span class="step-running">⟳</span>
|
|
1165
|
-
<span>步骤 ${data.currentStep + 1}/${data.totalSteps}</span>
|
|
1166
|
-
`;
|
|
1167
|
-
stepsList.appendChild(stepEl);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// 完成时移除显示
|
|
1171
|
-
if (data.status === 'completed' || data.status === 'failed') {
|
|
1172
|
-
setTimeout(() => {
|
|
1173
|
-
if (workflowDisplayEl) {
|
|
1174
|
-
workflowDisplayEl.remove();
|
|
1175
|
-
workflowDisplayEl = null;
|
|
1176
|
-
}
|
|
1177
|
-
}, 3000);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function handleWorkflowStepEvent(data, container) {
|
|
1182
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
1183
|
-
console.log('[工作流] 步骤:', data);
|
|
1184
|
-
console.log('[工作流] 步骤标签:', data.step, '内容:', data.content?.substring(0, 80));
|
|
1185
|
-
|
|
1186
|
-
// 获取或创建工作流显示区域
|
|
1187
|
-
if (!workflowDisplayEl) {
|
|
1188
|
-
workflowDisplayEl = createWorkflowDisplay();
|
|
1189
|
-
msgContainer.appendChild(workflowDisplayEl);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
const streamsDiv = workflowDisplayEl.querySelector('.workflow-streams');
|
|
1193
|
-
const header = workflowDisplayEl.querySelector('.workflow-header');
|
|
1194
|
-
|
|
1195
|
-
// 更新工作流标题
|
|
1196
|
-
if (data.step && data.step !== '系统' && data.step !== '状态') {
|
|
1197
|
-
header.querySelector('.workflow-title').textContent = `执行: ${data.step}`;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
// 如果有步骤标签,显示步骤信息
|
|
1201
|
-
if (data.step && data.content) {
|
|
1202
|
-
const stepEl = document.createElement('div');
|
|
1203
|
-
stepEl.className = 'workflow-step-stream';
|
|
1204
|
-
|
|
1205
|
-
// 根据内容类型选择不同样式
|
|
1206
|
-
const isError = data.content.includes('❌') || data.content.includes('错误');
|
|
1207
|
-
const isSuccess = data.content.includes('✅') || data.content.includes('成功');
|
|
1208
|
-
const isLoop = data.content.includes('🔄') || data.content.includes('循环');
|
|
1209
|
-
|
|
1210
|
-
let icon = '🔧';
|
|
1211
|
-
if (isError) icon = '❌';
|
|
1212
|
-
else if (isSuccess) icon = '✅';
|
|
1213
|
-
else if (isLoop) icon = '🔄';
|
|
1214
|
-
|
|
1215
|
-
stepEl.innerHTML = `
|
|
1216
|
-
<div class="step-label">${icon} ${data.step}</div>
|
|
1217
|
-
<div class="step-content">${data.content}</div>
|
|
1218
|
-
`;
|
|
1219
|
-
streamsDiv.appendChild(stepEl);
|
|
1220
|
-
|
|
1221
|
-
// 自动滚动
|
|
1222
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
function handleWorkflowLoopEvent(data, container) {
|
|
1227
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
1228
|
-
console.log('[工作流] 循环:', data);
|
|
1229
|
-
|
|
1230
|
-
if (!workflowDisplayEl) {
|
|
1231
|
-
workflowDisplayEl = createWorkflowDisplay();
|
|
1232
|
-
msgContainer.appendChild(workflowDisplayEl);
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
const loopCount = workflowDisplayEl.querySelector('.workflow-loop-count');
|
|
1236
|
-
const streamsDiv = workflowDisplayEl.querySelector('.workflow-streams');
|
|
1237
|
-
|
|
1238
|
-
// 更新循环次数
|
|
1239
|
-
if (data.loopCount !== undefined) {
|
|
1240
|
-
loopCount.textContent = `循环 #${data.loopCount}`;
|
|
1241
|
-
loopCount.style.display = 'inline';
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// 显示循环信息
|
|
1245
|
-
if (data.content) {
|
|
1246
|
-
const loopEl = document.createElement('div');
|
|
1247
|
-
loopEl.className = 'workflow-loop-item';
|
|
1248
|
-
loopEl.innerHTML = `
|
|
1249
|
-
<div class="loop-header">
|
|
1250
|
-
<span class="loop-icon">🔁</span>
|
|
1251
|
-
<span>循环 ${data.loopCount || '?'}</span>
|
|
1252
|
-
<span class="loop-status">${data.status || ''}</span>
|
|
1253
|
-
</div>
|
|
1254
|
-
<div class="loop-content">${data.content}</div>
|
|
1255
|
-
`;
|
|
1256
|
-
streamsDiv.appendChild(loopEl);
|
|
1257
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// 用户命令可视化 - 当用户发送命令时调用
|
|
1262
|
-
let userCommandDisplayEl = null;
|
|
1263
|
-
|
|
1264
|
-
function showUserCommand(command, container) {
|
|
1265
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
1266
|
-
// 先移除之前的消息中的 user bubble(如果有重复的话)
|
|
1267
|
-
const existingUserBubbles = msgContainer.querySelectorAll('.message-user');
|
|
1268
|
-
existingUserBubbles.forEach(el => el.remove());
|
|
1269
|
-
|
|
1270
|
-
// 移除之前的命令显示
|
|
1271
|
-
if (userCommandDisplayEl) {
|
|
1272
|
-
userCommandDisplayEl.remove();
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// 创建美化版本的命令显示
|
|
1276
|
-
userCommandDisplayEl = document.createElement('div');
|
|
1277
|
-
userCommandDisplayEl.className = 'message message-user';
|
|
1278
|
-
userCommandDisplayEl.innerHTML = `
|
|
1279
|
-
<div class="user-command-display">
|
|
1280
|
-
<div class="command-prompt">
|
|
1281
|
-
<span class="prompt-icon">›</span>
|
|
1282
|
-
<span class="prompt-text">${command}</span>
|
|
1283
|
-
</div>
|
|
1284
|
-
</div>
|
|
1285
|
-
`;
|
|
1286
|
-
|
|
1287
|
-
// 在工作流显示之前插入
|
|
1288
|
-
if (workflowDisplayEl) {
|
|
1289
|
-
msgContainer.insertBefore(userCommandDisplayEl, workflowDisplayEl);
|
|
1290
|
-
} else {
|
|
1291
|
-
msgContainer.appendChild(userCommandDisplayEl);
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
function connect(channelId) {
|
|
1298
|
-
const targetChannelId = channelId || currentChannelId;
|
|
1299
|
-
if (!targetChannelId) return;
|
|
1300
|
-
|
|
1301
|
-
// 清除该频道的重连定时器
|
|
1302
|
-
if (reconnectTimers.has(targetChannelId)) {
|
|
1303
|
-
clearTimeout(reconnectTimers.get(targetChannelId));
|
|
1304
|
-
reconnectTimers.delete(targetChannelId);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
// 清除该频道的心跳定时器 (防止多次调用 connect 导致 setInterval 累积)
|
|
1308
|
-
if (heartbeatTimers.has(targetChannelId)) {
|
|
1309
|
-
clearInterval(heartbeatTimers.get(targetChannelId));
|
|
1310
|
-
heartbeatTimers.delete(targetChannelId);
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// 关闭该频道的旧连接
|
|
1314
|
-
if (eventSources.has(targetChannelId)) {
|
|
1315
|
-
eventSources.get(targetChannelId).close();
|
|
1316
|
-
eventSources.delete(targetChannelId);
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
const sseUrl = `/events?channelId=${encodeURIComponent(targetChannelId)}`;
|
|
1320
|
-
console.log('[connect] 创建 SSE 连接:', sseUrl);
|
|
1321
|
-
|
|
1322
|
-
const eventSource = new EventSource(sseUrl);
|
|
1323
|
-
eventSources.set(targetChannelId, eventSource);
|
|
1324
|
-
|
|
1325
|
-
if (!reconnectAttempts.has(targetChannelId)) {
|
|
1326
|
-
reconnectAttempts.set(targetChannelId, 0);
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
eventSource.onopen = () => {
|
|
1330
|
-
console.log('[SSE] 已连接 channelId:', targetChannelId);
|
|
1331
|
-
reconnectAttempts.set(targetChannelId, 0);
|
|
1332
|
-
};
|
|
1333
|
-
|
|
1334
|
-
// 心跳超时: 如果 60s 没收到任何数据 (含 ping), 强制重建
|
|
1335
|
-
// 覆盖网络半开 / 浏览器没触发 onerror 的情况
|
|
1336
|
-
let lastEventTime = Date.now();
|
|
1337
|
-
const heartbeatTimer = setInterval(() => {
|
|
1338
|
-
if (!eventSources.has(targetChannelId)) {
|
|
1339
|
-
clearInterval(heartbeatTimer);
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
if (Date.now() - lastEventTime > 60000) {
|
|
1343
|
-
console.warn('[SSE] 60s 无数据, 强制重建连接:', targetChannelId);
|
|
1344
|
-
clearInterval(heartbeatTimer);
|
|
1345
|
-
try { eventSource.close(); } catch {}
|
|
1346
|
-
eventSources.delete(targetChannelId);
|
|
1347
|
-
// 退避重连 (有上限)
|
|
1348
|
-
const attempts = (reconnectAttempts.get(targetChannelId) || 0) + 1;
|
|
1349
|
-
reconnectAttempts.set(targetChannelId, attempts);
|
|
1350
|
-
const delay = Math.min(1000 * Math.pow(2, attempts - 1), 15000);
|
|
1351
|
-
const timer = setTimeout(() => connect(targetChannelId), delay);
|
|
1352
|
-
reconnectTimers.set(targetChannelId, timer);
|
|
1353
|
-
}
|
|
1354
|
-
}, 10000);
|
|
1355
|
-
|
|
1356
|
-
// onerror: 不要手动 close — 浏览器 EventSource 会自动重连
|
|
1357
|
-
// 我们只需在 readyState 永久 CLOSED 时 (罕见) 才介入
|
|
1358
|
-
eventSource.onerror = () => {
|
|
1359
|
-
console.warn('[SSE] 错误, 浏览器自动重连中:', targetChannelId, 'readyState=', eventSource.readyState);
|
|
1360
|
-
if (eventSource.readyState === EventSource.CLOSED) {
|
|
1361
|
-
// 浏览器放弃重连, 我们接手
|
|
1362
|
-
clearInterval(heartbeatTimer);
|
|
1363
|
-
eventSources.delete(targetChannelId);
|
|
1364
|
-
const attempts = (reconnectAttempts.get(targetChannelId) || 0) + 1;
|
|
1365
|
-
reconnectAttempts.set(targetChannelId, attempts);
|
|
1366
|
-
const delay = Math.min(1000 * Math.pow(2, attempts - 1), 15000);
|
|
1367
|
-
const timer = setTimeout(() => connect(targetChannelId), delay);
|
|
1368
|
-
reconnectTimers.set(targetChannelId, timer);
|
|
1369
|
-
}
|
|
1370
|
-
};
|
|
1371
|
-
|
|
1372
|
-
eventSource.onmessage = (e) => {
|
|
1373
|
-
lastEventTime = Date.now();
|
|
1374
|
-
try {
|
|
1375
|
-
const data = JSON.parse(e.data);
|
|
1376
|
-
const msgChannelId = data.channelId || targetChannelId;
|
|
1377
|
-
console.log('[SSE] 收到消息:', data.type, 'channelId:', msgChannelId);
|
|
1378
|
-
|
|
1379
|
-
// 路由消息到正确的频道
|
|
1380
|
-
// 只有 envelope.channelId 存在且与目标不同时才丢弃 (空/undefined 视为广播给自己)
|
|
1381
|
-
if (msgChannelId && msgChannelId !== targetChannelId) {
|
|
1382
|
-
console.log('[SSE] 忽略非目标频道消息');
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// 使用正确的消息容器
|
|
1387
|
-
const container = messagesContainers.get(msgChannelId) || messagesEl;
|
|
1388
|
-
|
|
1389
|
-
if (data.type === 'user') {
|
|
1390
|
-
showUserCommand(data.content, container);
|
|
1391
|
-
} else if (data.type === 'ai') {
|
|
1392
|
-
addMessage(data.content, 'ai', true, container);
|
|
1393
|
-
hideTyping();
|
|
1394
|
-
} else if (data.type === 'stream') {
|
|
1395
|
-
handleStreamEvent(data, container);
|
|
1396
|
-
setAgentStatus('executing');
|
|
1397
|
-
} else if (data.type === 'regenerating') {
|
|
1398
|
-
const messages = container.querySelectorAll('.message-ai');
|
|
1399
|
-
if (messages.length > 0) {
|
|
1400
|
-
const lastAiMsg = messages[messages.length - 1];
|
|
1401
|
-
lastAiMsg.remove();
|
|
1402
|
-
}
|
|
1403
|
-
showTyping(container);
|
|
1404
|
-
} else if (data.type === 'status') {
|
|
1405
|
-
handleStatusEvent(data, container);
|
|
1406
|
-
setAgentStatus('executing');
|
|
1407
|
-
} else if (data.type === 'done') {
|
|
1408
|
-
hideTyping();
|
|
1409
|
-
// AI 回复完, 把最后一条 ai 消息落盘 (兜底, 避免 server saveSession 漏写)
|
|
1410
|
-
const lastAi = container.querySelector('.message-ai:last-of-type .message-content');
|
|
1411
|
-
if (lastAi) {
|
|
1412
|
-
persistLastMessageToServer('ai', lastAi.textContent || '');
|
|
1413
|
-
}
|
|
1414
|
-
} else if (data.type === 'renamed') {
|
|
1415
|
-
const channel = channels.find(c => c.id === data.channelId);
|
|
1416
|
-
if (channel) {
|
|
1417
|
-
channel.name = data.newName;
|
|
1418
|
-
renderChannels();
|
|
1419
|
-
if (currentChannelId === data.channelId && channelNameEl) {
|
|
1420
|
-
channelNameEl.textContent = data.newName;
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
} else if (data.type === 'error') {
|
|
1424
|
-
hideTyping();
|
|
1425
|
-
addMessage('错误: ' + data.content, 'ai', true, container);
|
|
1426
|
-
} else if (data.type === 'task_status') {
|
|
1427
|
-
handleTaskStatusEvent(data, container);
|
|
1428
|
-
} else if (data.type === 'workflow_step') {
|
|
1429
|
-
handleWorkflowStepEvent(data, container);
|
|
1430
|
-
} else if (data.type === 'workflow_loop') {
|
|
1431
|
-
handleWorkflowLoopEvent(data, container);
|
|
1432
|
-
}
|
|
1433
|
-
} catch (parseErr) {
|
|
1434
|
-
console.error('[SSE] 解析错误', parseErr);
|
|
1435
|
-
}
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
async function sendMessage() {
|
|
1440
|
-
const text = input.value.trim();
|
|
1441
|
-
if (!text) return;
|
|
1442
|
-
|
|
1443
|
-
input.value = '';
|
|
1444
|
-
showTyping();
|
|
1445
|
-
|
|
1446
|
-
// 立即把用户消息落盘, 避免切走再切回时丢失
|
|
1447
|
-
persistLastMessageToServer('user', text);
|
|
1448
|
-
|
|
1449
|
-
// 获取当前频道的 DID
|
|
1450
|
-
const channel = channels.find(c => c.id === currentChannelId);
|
|
1451
|
-
const channelDid = channel?.did || '';
|
|
1452
|
-
console.log('[发送消息] 频道 DID:', channelDid);
|
|
1453
|
-
|
|
1454
|
-
try {
|
|
1455
|
-
const res = await fetch('/message', {
|
|
1456
|
-
method: 'POST',
|
|
1457
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1458
|
-
body: JSON.stringify({
|
|
1459
|
-
text,
|
|
1460
|
-
channelId: currentChannelId,
|
|
1461
|
-
channelDid
|
|
1462
|
-
})
|
|
1463
|
-
});
|
|
1464
|
-
|
|
1465
|
-
if (!res.ok) {
|
|
1466
|
-
hideTyping();
|
|
1467
|
-
addMessage('发送失败', 'ai');
|
|
1468
|
-
}
|
|
1469
|
-
} catch (err) {
|
|
1470
|
-
hideTyping();
|
|
1471
|
-
addMessage('连接错误', 'ai');
|
|
1472
|
-
console.error('Send error', err);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// 主动落盘: 把当前 channelId/sessionId 最后一条消息 PATCH 到 server
|
|
1477
|
-
// fire-and-forget, 失败只打日志, 不影响 UI
|
|
1478
|
-
function persistLastMessageToServer(type, content) {
|
|
1479
|
-
if (!currentChannelId || !currentSessionId) return;
|
|
1480
|
-
fetch(`/sessions/${currentChannelId}/${currentSessionId}`, {
|
|
1481
|
-
method: 'PATCH',
|
|
1482
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1483
|
-
body: JSON.stringify({
|
|
1484
|
-
message: { type, content, timestamp: new Date().toISOString() }
|
|
1485
|
-
})
|
|
1486
|
-
}).catch(err => {
|
|
1487
|
-
console.warn('[persist] 落盘失败:', err);
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
sendBtn.addEventListener('click', sendMessage);
|
|
1492
|
-
input.addEventListener('keydown', (e) => {
|
|
1493
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1494
|
-
e.preventDefault();
|
|
1495
|
-
sendMessage();
|
|
1496
|
-
}
|
|
1497
|
-
});
|
|
1498
|
-
|
|
1499
|
-
if (themeToggle) {
|
|
1500
|
-
themeToggle.addEventListener('click', toggleTheme);
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
const apiConfigBtn = document.getElementById('api-config-btn');
|
|
1504
|
-
if (apiConfigBtn) {
|
|
1505
|
-
apiConfigBtn.addEventListener('click', () => {
|
|
1506
|
-
window.location.href = '/api-config';
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// 钱包管理按钮
|
|
1511
|
-
const walletBtn = document.getElementById('wallet-btn');
|
|
1512
|
-
const walletBadge = document.getElementById('wallet-badge');
|
|
1513
|
-
if (walletBtn) {
|
|
1514
|
-
walletBtn.addEventListener('click', openWalletModal);
|
|
1515
|
-
}
|
|
1516
|
-
/** 刷新 header 钱包徽章: 统计已绑定钱包的智能体数 */
|
|
1517
|
-
function refreshWalletBadge() {
|
|
1518
|
-
if (!walletBadge) return;
|
|
1519
|
-
const count = channels.filter(c => c.walletAddress).length;
|
|
1520
|
-
if (count > 0) {
|
|
1521
|
-
walletBadge.textContent = String(count);
|
|
1522
|
-
walletBadge.style.display = '';
|
|
1523
|
-
} else {
|
|
1524
|
-
walletBadge.style.display = 'none';
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
if (sidebarToggle) {
|
|
1529
|
-
sidebarToggle.addEventListener('click', toggleSidebar);
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
if (loadSessionBtn && sessionFileInput) {
|
|
1533
|
-
loadSessionBtn.addEventListener('click', () => {
|
|
1534
|
-
sessionFileInput.click();
|
|
1535
|
-
});
|
|
1536
|
-
|
|
1537
|
-
sessionFileInput.addEventListener('change', async (e) => {
|
|
1538
|
-
const file = e.target.files?.[0];
|
|
1539
|
-
if (!file) return;
|
|
1540
|
-
|
|
1541
|
-
try {
|
|
1542
|
-
const text = await file.text();
|
|
1543
|
-
const session = JSON.parse(text);
|
|
1544
|
-
|
|
1545
|
-
if (session.messages && Array.isArray(session.messages)) {
|
|
1546
|
-
messagesEl.innerHTML = '';
|
|
1547
|
-
session.messages.forEach(msg => {
|
|
1548
|
-
addMessage(msg.content, msg.type, false);
|
|
1549
|
-
});
|
|
1550
|
-
|
|
1551
|
-
const channelName = session.channelId || file.name.replace('.json', '');
|
|
1552
|
-
if (channelNameEl) {
|
|
1553
|
-
channelNameEl.textContent = channelName;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
addMessage(`已加载 session: ${file.name}`, 'ai', false);
|
|
1557
|
-
} else {
|
|
1558
|
-
addMessage('无效的 session 文件格式', 'ai', false);
|
|
1559
|
-
}
|
|
1560
|
-
} catch (err) {
|
|
1561
|
-
console.error('Failed to load session:', err);
|
|
1562
|
-
addMessage('加载 session 失败: ' + err.message, 'ai', false);
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
sessionFileInput.value = '';
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
if (newChannelBtn) {
|
|
1570
|
-
newChannelBtn.addEventListener('click', () => {
|
|
1571
|
-
createChannel('智能体');
|
|
1572
|
-
});
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
if (newChannelInput) {
|
|
1576
|
-
newChannelInput.addEventListener('keydown', (e) => {
|
|
1577
|
-
if (e.key === 'Enter') {
|
|
1578
|
-
e.preventDefault();
|
|
1579
|
-
createChannel(newChannelInput.value);
|
|
1580
|
-
}
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
async function checkApiConfig() {
|
|
1585
|
-
try {
|
|
1586
|
-
const res = await fetch('/api/llm-config');
|
|
1587
|
-
const config = await res.json();
|
|
1588
|
-
|
|
1589
|
-
// 检查是否有供应商已配置
|
|
1590
|
-
const hasConfigured = Object.values(config.providers).some(p => p.enabled && p.apiKey);
|
|
1591
|
-
|
|
1592
|
-
if (!hasConfigured) {
|
|
1593
|
-
// 显示 API 配置提示
|
|
1594
|
-
const hint = document.createElement('div');
|
|
1595
|
-
hint.className = 'api-config-hint';
|
|
1596
|
-
hint.innerHTML = `
|
|
1597
|
-
<div class="hint-icon">⚠️</div>
|
|
1598
|
-
<div class="hint-text">
|
|
1599
|
-
<strong>API 未配置</strong><br>
|
|
1600
|
-
请先配置 AI 模型才能开始对话
|
|
1601
|
-
</div>
|
|
1602
|
-
<button class="hint-btn" id="api-config-hint-btn">前往配置</button>
|
|
1603
|
-
`;
|
|
1604
|
-
document.body.appendChild(hint);
|
|
1605
|
-
|
|
1606
|
-
// 绑定事件处理器(避免内联 onclick)
|
|
1607
|
-
const hintBtn = document.getElementById('api-config-hint-btn');
|
|
1608
|
-
if (hintBtn) {
|
|
1609
|
-
hintBtn.addEventListener('click', () => {
|
|
1610
|
-
window.location.href = '/api-config';
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
} catch (err) {
|
|
1615
|
-
console.error('Failed to check API config:', err);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
async function init() {
|
|
1620
|
-
const themeData = await loadTheme();
|
|
1621
|
-
currentAgentId = themeData.agentId || `agent_${generateId().substring(0, 8)}`;
|
|
1622
|
-
|
|
1623
|
-
if (!themeData.agentId) {
|
|
1624
|
-
await saveTheme(themeData.theme, currentAgentId);
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
await loadChannels();
|
|
1628
|
-
await checkApiConfig();
|
|
1629
|
-
|
|
1630
|
-
if (channels.length > 0) {
|
|
1631
|
-
await selectChannel(channels[0].id);
|
|
1632
|
-
} else {
|
|
1633
|
-
await createChannel('默认会话');
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
// P2P Network Modal - React Version
|
|
1638
|
-
const p2pNetworkBtn = document.getElementById('p2p-network-btn');
|
|
1639
|
-
|
|
1640
|
-
// 打开 P2P Modal (使用 React)
|
|
1641
|
-
if (p2pNetworkBtn) {
|
|
1642
|
-
p2pNetworkBtn.addEventListener('click', () => {
|
|
1643
|
-
if (typeof window.showP2PModal === 'function') {
|
|
1644
|
-
window.showP2PModal();
|
|
1645
|
-
}
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
// Task Queue
|
|
1650
|
-
const taskModal = document.getElementById('task-modal');
|
|
1651
|
-
const taskQueueBtn = document.getElementById('task-queue-btn');
|
|
1652
|
-
const taskModalClose = document.getElementById('task-modal-close');
|
|
1653
|
-
const createTaskModal = document.getElementById('create-task-modal');
|
|
1654
|
-
const createTaskModalClose = document.getElementById('create-task-modal-close');
|
|
1655
|
-
const taskList = document.getElementById('task-list');
|
|
1656
|
-
const taskAddBtn = document.getElementById('task-add-btn');
|
|
1657
|
-
const taskExecuteNextBtn = document.getElementById('task-execute-next-btn');
|
|
1658
|
-
const taskTypeSelect = document.getElementById('task-type');
|
|
1659
|
-
const taskTitleInput = document.getElementById('task-title');
|
|
1660
|
-
const taskDescInput = document.getElementById('task-desc');
|
|
1661
|
-
const taskStepsInput = document.getElementById('task-steps');
|
|
1662
|
-
const taskCreateBtn = document.getElementById('task-create-btn');
|
|
1663
|
-
const taskCancelBtn = document.getElementById('task-cancel-btn');
|
|
1664
|
-
const taskBadge = document.getElementById('task-badge');
|
|
1665
|
-
|
|
1666
|
-
let tasks = [];
|
|
1667
|
-
|
|
1668
|
-
async function loadTasks() {
|
|
1669
|
-
try {
|
|
1670
|
-
const res = await fetch('/api/tasks');
|
|
1671
|
-
if (res.ok) {
|
|
1672
|
-
tasks = await res.json();
|
|
1673
|
-
renderTasks();
|
|
1674
|
-
updateTaskBadge();
|
|
1675
|
-
}
|
|
1676
|
-
} catch {
|
|
1677
|
-
tasks = [];
|
|
1678
|
-
renderTasks();
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function renderTasks() {
|
|
1683
|
-
if (!taskList) return;
|
|
1684
|
-
|
|
1685
|
-
if (tasks.length === 0) {
|
|
1686
|
-
taskList.innerHTML = '<div class="task-empty">暂无任务,点击上方按钮创建</div>';
|
|
1687
|
-
return;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
// 使用 DocumentFragment 优化性能
|
|
1691
|
-
const fragment = document.createDocumentFragment();
|
|
1692
|
-
|
|
1693
|
-
tasks.forEach(task => {
|
|
1694
|
-
const div = document.createElement('div');
|
|
1695
|
-
div.className = `task-item ${task.status}`;
|
|
1696
|
-
div.dataset.id = task.id;
|
|
1697
|
-
|
|
1698
|
-
div.innerHTML = `
|
|
1699
|
-
<div class="task-item-header">
|
|
1700
|
-
<div class="task-item-title">
|
|
1701
|
-
<span>${getTaskIcon(task.type)}</span>
|
|
1702
|
-
<span>${task.title}</span>
|
|
1703
|
-
</div>
|
|
1704
|
-
<span class="task-item-status ${task.status}">${getTaskStatusText(task.status)}</span>
|
|
1705
|
-
</div>
|
|
1706
|
-
${task.description ? `<div class="task-item-desc">${task.description.substring(0, 100)}${task.description.length > 100 ? '...' : ''}</div>` : ''}
|
|
1707
|
-
<div class="task-item-progress">
|
|
1708
|
-
<div class="task-item-progress-bar" style="width: ${task.progress}%"></div>
|
|
1709
|
-
</div>
|
|
1710
|
-
${task.steps && task.steps.length > 0 ? `
|
|
1711
|
-
<div class="task-item-steps">
|
|
1712
|
-
${task.steps.map((step, i) => `
|
|
1713
|
-
<div class="task-item-step ${step.status}">
|
|
1714
|
-
${step.status === 'completed' ? '✓' : step.status === 'running' ? '⟳' : step.status === 'failed' ? '✗' : '○'} ${i + 1}. ${step.name}
|
|
1715
|
-
</div>
|
|
1716
|
-
`).join('')}
|
|
1717
|
-
</div>
|
|
1718
|
-
` : ''}
|
|
1719
|
-
<div class="task-item-actions">
|
|
1720
|
-
${task.status === 'pending' ? `<button class="btn-sm btn-primary" data-action="execute">▶ 执行</button>` : ''}
|
|
1721
|
-
${task.status === 'running' ? `<button class="btn-sm" disabled>执行中...</button>` : ''}
|
|
1722
|
-
${task.status === 'completed' ? `<button class="btn-sm" data-action="delete">删除</button>` : ''}
|
|
1723
|
-
${task.status === 'failed' ? `<button class="btn-sm btn-primary" data-action="retry">重试</button>` : ''}
|
|
1724
|
-
</div>
|
|
1725
|
-
`;
|
|
1726
|
-
|
|
1727
|
-
fragment.appendChild(div);
|
|
1728
|
-
});
|
|
1729
|
-
|
|
1730
|
-
taskList.innerHTML = '';
|
|
1731
|
-
taskList.appendChild(fragment);
|
|
1732
|
-
|
|
1733
|
-
// 绑定事件处理器(避免内联 onclick)
|
|
1734
|
-
taskList.querySelectorAll('[data-action]').forEach(btn => {
|
|
1735
|
-
btn.addEventListener('click', (e) => {
|
|
1736
|
-
e.stopPropagation();
|
|
1737
|
-
const item = e.currentTarget.closest('.task-item');
|
|
1738
|
-
const taskId = item?.dataset.id;
|
|
1739
|
-
if (!taskId) return;
|
|
1740
|
-
|
|
1741
|
-
const action = e.currentTarget.dataset.action;
|
|
1742
|
-
switch (action) {
|
|
1743
|
-
case 'execute': executeTask(taskId); break;
|
|
1744
|
-
case 'delete': deleteTask(taskId); break;
|
|
1745
|
-
case 'retry': retryTask(taskId); break;
|
|
1746
|
-
}
|
|
1747
|
-
});
|
|
1748
|
-
});
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
function getTaskIcon(type) {
|
|
1752
|
-
switch (type) {
|
|
1753
|
-
case 'chat': return '💬';
|
|
1754
|
-
case 'read': return '📄';
|
|
1755
|
-
case 'summarize': return '📝';
|
|
1756
|
-
case 'improve': return '✏️';
|
|
1757
|
-
case 'workflow': return '🔄';
|
|
1758
|
-
default: return '📋';
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
function getTaskStatusText(status) {
|
|
1763
|
-
switch (status) {
|
|
1764
|
-
case 'pending': return '待执行';
|
|
1765
|
-
case 'running': return '执行中';
|
|
1766
|
-
case 'completed': return '已完成';
|
|
1767
|
-
case 'failed': return '失败';
|
|
1768
|
-
case 'paused': return '已暂停';
|
|
1769
|
-
default: return status;
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
function updateTaskBadge() {
|
|
1774
|
-
if (!taskBadge) return;
|
|
1775
|
-
const pending = tasks.filter(t => t.status === 'pending').length;
|
|
1776
|
-
if (pending > 0) {
|
|
1777
|
-
taskBadge.textContent = pending.toString();
|
|
1778
|
-
taskBadge.style.display = 'block';
|
|
1779
|
-
} else {
|
|
1780
|
-
taskBadge.style.display = 'none';
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
function showTaskModal() {
|
|
1785
|
-
if (taskModal) {
|
|
1786
|
-
taskModal.classList.add('active');
|
|
1787
|
-
loadTasks();
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
function hideTaskModal() {
|
|
1792
|
-
if (taskModal) {
|
|
1793
|
-
taskModal.classList.remove('active');
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
function showCreateTaskModal() {
|
|
1798
|
-
if (createTaskModal) {
|
|
1799
|
-
createTaskModal.classList.add('active');
|
|
1800
|
-
if (taskTitleInput) taskTitleInput.value = '';
|
|
1801
|
-
if (taskDescInput) taskDescInput.value = '';
|
|
1802
|
-
if (taskStepsInput) taskStepsInput.value = '';
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
function hideCreateTaskModal() {
|
|
1807
|
-
if (createTaskModal) {
|
|
1808
|
-
createTaskModal.classList.remove('active');
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
async function createTask() {
|
|
1813
|
-
const type = taskTypeSelect?.value || 'chat';
|
|
1814
|
-
const title = taskTitleInput?.value?.trim();
|
|
1815
|
-
const description = taskDescInput?.value?.trim();
|
|
1816
|
-
|
|
1817
|
-
if (!title) {
|
|
1818
|
-
alert('请输入任务标题');
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
const taskData = {
|
|
1823
|
-
type,
|
|
1824
|
-
title,
|
|
1825
|
-
description
|
|
1826
|
-
};
|
|
1827
|
-
|
|
1828
|
-
if (type === 'workflow' && taskStepsInput?.value) {
|
|
1829
|
-
taskData.steps = taskStepsInput.value.split('\n').filter(s => s.trim());
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
try {
|
|
1833
|
-
const res = await fetch('/api/tasks', {
|
|
1834
|
-
method: 'POST',
|
|
1835
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1836
|
-
body: JSON.stringify(taskData)
|
|
1837
|
-
});
|
|
1838
|
-
|
|
1839
|
-
if (res.ok) {
|
|
1840
|
-
const task = await res.json();
|
|
1841
|
-
|
|
1842
|
-
// 自动执行任务
|
|
1843
|
-
if (currentChannelId) {
|
|
1844
|
-
await fetch(`/api/tasks/${task.id}/execute`, {
|
|
1845
|
-
method: 'POST',
|
|
1846
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1847
|
-
body: JSON.stringify({ channelId: currentChannelId })
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
hideCreateTaskModal();
|
|
1852
|
-
await loadTasks();
|
|
1853
|
-
}
|
|
1854
|
-
} catch (err) {
|
|
1855
|
-
console.error('Failed to create task:', err);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
async function executeTask(taskId) {
|
|
1860
|
-
if (!currentChannelId) {
|
|
1861
|
-
alert('请先选择一个频道');
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
try {
|
|
1866
|
-
await fetch(`/api/tasks/${taskId}/execute`, {
|
|
1867
|
-
method: 'POST',
|
|
1868
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1869
|
-
body: JSON.stringify({ channelId: currentChannelId })
|
|
1870
|
-
});
|
|
1871
|
-
await loadTasks();
|
|
1872
|
-
} catch (err) {
|
|
1873
|
-
console.error('Failed to execute task:', err);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
async function retryTask(taskId) {
|
|
1878
|
-
const tasks = await (await fetch('/api/tasks')).json();
|
|
1879
|
-
const task = tasks.find(t => t.id === taskId);
|
|
1880
|
-
if (task) {
|
|
1881
|
-
task.status = 'pending';
|
|
1882
|
-
task.error = undefined;
|
|
1883
|
-
await fetch(`/api/tasks/${taskId}`, {
|
|
1884
|
-
method: 'PATCH',
|
|
1885
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1886
|
-
body: JSON.stringify({ status: 'pending' })
|
|
1887
|
-
});
|
|
1888
|
-
await executeTask(taskId);
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
async function deleteTask(taskId) {
|
|
1893
|
-
try {
|
|
1894
|
-
await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' });
|
|
1895
|
-
await loadTasks();
|
|
1896
|
-
} catch (err) {
|
|
1897
|
-
console.error('Failed to delete task:', err);
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
async function executeNextTask() {
|
|
1902
|
-
if (!currentChannelId) {
|
|
1903
|
-
alert('请先选择一个频道');
|
|
1904
|
-
return;
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
try {
|
|
1908
|
-
const res = await fetch('/api/tasks/execute-next', {
|
|
1909
|
-
method: 'POST',
|
|
1910
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1911
|
-
body: JSON.stringify({ channelId: currentChannelId })
|
|
1912
|
-
});
|
|
1913
|
-
|
|
1914
|
-
if (res.ok) {
|
|
1915
|
-
const data = await res.json();
|
|
1916
|
-
if (!data.ok) {
|
|
1917
|
-
addMessage(data.message || '没有待执行的任务', 'ai');
|
|
1918
|
-
}
|
|
1919
|
-
await loadTasks();
|
|
1920
|
-
}
|
|
1921
|
-
} catch (err) {
|
|
1922
|
-
console.error('Failed to execute next task:', err);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
// Task modal events
|
|
1927
|
-
if (taskQueueBtn) {
|
|
1928
|
-
taskQueueBtn.addEventListener('click', showTaskModal);
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
if (taskModalClose) {
|
|
1932
|
-
taskModalClose.addEventListener('click', hideTaskModal);
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
if (taskModal) {
|
|
1936
|
-
taskModal.addEventListener('click', (e) => {
|
|
1937
|
-
if (e.target === taskModal) {
|
|
1938
|
-
hideTaskModal();
|
|
1939
|
-
}
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
if (taskAddBtn) {
|
|
1944
|
-
taskAddBtn.addEventListener('click', showCreateTaskModal);
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
if (taskExecuteNextBtn) {
|
|
1948
|
-
taskExecuteNextBtn.addEventListener('click', executeNextTask);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
if (taskCancelBtn) {
|
|
1952
|
-
taskCancelBtn.addEventListener('click', hideCreateTaskModal);
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
if (createTaskModalClose) {
|
|
1956
|
-
createTaskModalClose.addEventListener('click', hideCreateTaskModal);
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
if (createTaskModal) {
|
|
1960
|
-
createTaskModal.addEventListener('click', (e) => {
|
|
1961
|
-
if (e.target === createTaskModal) {
|
|
1962
|
-
hideCreateTaskModal();
|
|
1963
|
-
}
|
|
1964
|
-
});
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
if (taskCreateBtn) {
|
|
1968
|
-
taskCreateBtn.addEventListener('click', createTask);
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
if (taskTypeSelect) {
|
|
1972
|
-
taskTypeSelect.addEventListener('change', () => {
|
|
1973
|
-
const workflowSteps = document.querySelector('.workflow-steps');
|
|
1974
|
-
if (workflowSteps) {
|
|
1975
|
-
workflowSteps.style.display = taskTypeSelect.value === 'workflow' ? 'block' : 'none';
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
// Handle SSE task status updates
|
|
1981
|
-
const originalOnMessage = window.addEventListener ? null : null;
|
|
1982
|
-
|
|
1983
|
-
// Extend SSE handler for task updates
|
|
1984
|
-
const originalConnect = connect;
|
|
1985
|
-
connect = async function() {
|
|
1986
|
-
// Call original connect
|
|
1987
|
-
await originalConnect();
|
|
1988
|
-
|
|
1989
|
-
// Reconnect SSE for task updates
|
|
1990
|
-
const taskEventSource = new EventSource('/events');
|
|
1991
|
-
|
|
1992
|
-
taskEventSource.onmessage = (e) => {
|
|
1993
|
-
try {
|
|
1994
|
-
const data = JSON.parse(e.data);
|
|
1995
|
-
if (data.type === 'task_status') {
|
|
1996
|
-
loadTasks();
|
|
1997
|
-
}
|
|
1998
|
-
} catch {}
|
|
1999
|
-
};
|
|
2000
|
-
|
|
2001
|
-
taskEventSource.onerror = () => {
|
|
2002
|
-
taskEventSource.close();
|
|
2003
|
-
};
|
|
2004
|
-
};
|
|
2005
|
-
|
|
2006
|
-
// =====================================================
|
|
2007
|
-
// 钱包管理 (header 钱包按钮 → 全局管理面板)
|
|
2008
|
-
// =====================================================
|
|
2009
|
-
const walletModal = document.getElementById('wallet-modal');
|
|
2010
|
-
const walletModalClose = document.getElementById('wallet-modal-close');
|
|
2011
|
-
const walletBindAddress = document.getElementById('wallet-bind-address');
|
|
2012
|
-
const walletGenerateBtn = document.getElementById('wallet-generate-btn');
|
|
2013
|
-
const walletAutoTools = document.getElementById('wallet-auto-tools');
|
|
2014
|
-
const walletBindBtn = document.getElementById('wallet-bind-btn');
|
|
2015
|
-
const walletUnbindBtn = document.getElementById('wallet-unbind-btn');
|
|
2016
|
-
const walletNewInfo = document.getElementById('wallet-new-info');
|
|
2017
|
-
const walletListEl = document.getElementById('wallet-list');
|
|
2018
|
-
|
|
2019
|
-
/** 本次会话生成的私钥, 仅用于提示, 永不上传 */
|
|
2020
|
-
let walletModalPendingSecret = null;
|
|
2021
|
-
|
|
2022
|
-
function openWalletModal() {
|
|
2023
|
-
if (!walletModal) return;
|
|
2024
|
-
walletModalPendingSecret = null;
|
|
2025
|
-
walletNewInfo.style.display = 'none';
|
|
2026
|
-
walletNewInfo.innerHTML = '';
|
|
2027
|
-
walletBindAddress.value = '';
|
|
2028
|
-
// 用当前 channel 的状态预填
|
|
2029
|
-
const ch = channels.find(c => c.id === currentChannelId);
|
|
2030
|
-
if (ch) {
|
|
2031
|
-
walletBindAddress.value = ch.walletAddress || '';
|
|
2032
|
-
walletAutoTools.checked = !!ch.autoInvokeTools;
|
|
2033
|
-
}
|
|
2034
|
-
renderWalletList();
|
|
2035
|
-
walletModal.classList.add('active');
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
function closeWalletModal() {
|
|
2039
|
-
if (!walletModal) return;
|
|
2040
|
-
walletModal.classList.remove('active');
|
|
2041
|
-
walletModalPendingSecret = null;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
if (walletModalClose) walletModalClose.addEventListener('click', closeWalletModal);
|
|
2045
|
-
|
|
2046
|
-
if (walletGenerateBtn) {
|
|
2047
|
-
walletGenerateBtn.addEventListener('click', () => {
|
|
2048
|
-
const { address, privateKeyHex } = generateLocalWallet();
|
|
2049
|
-
walletBindAddress.value = address;
|
|
2050
|
-
walletModalPendingSecret = privateKeyHex;
|
|
2051
|
-
walletNewInfo.style.display = 'block';
|
|
2052
|
-
walletNewInfo.innerHTML = `
|
|
2053
|
-
✓ 已生成本地钱包<br>
|
|
2054
|
-
<strong>地址:</strong> <code>${escapeHtml(address)}</code><br>
|
|
2055
|
-
<strong>私钥 (本次会话, 刷新即丢):</strong> <code style="color:#f88;">${escapeHtml(privateKeyHex)}</code><br>
|
|
2056
|
-
<small style="color:#f88;">⚠ 关闭页面后无法找回。仅地址会发送到服务端。</small>
|
|
2057
|
-
`;
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
if (walletBindBtn) {
|
|
2062
|
-
walletBindBtn.addEventListener('click', async () => {
|
|
2063
|
-
if (!currentChannelId) {
|
|
2064
|
-
alert('请先在侧边栏选择一个智能体');
|
|
2065
|
-
return;
|
|
2066
|
-
}
|
|
2067
|
-
const address = (walletBindAddress.value || '').trim();
|
|
2068
|
-
if (!address) {
|
|
2069
|
-
alert('请输入钱包地址或点击「生成」');
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
try {
|
|
2073
|
-
const res = await fetch(`/channels/${currentChannelId}`, {
|
|
2074
|
-
method: 'PATCH',
|
|
2075
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2076
|
-
body: JSON.stringify({
|
|
2077
|
-
walletAddress: address,
|
|
2078
|
-
autoInvokeTools: !!walletAutoTools.checked
|
|
2079
|
-
})
|
|
2080
|
-
});
|
|
2081
|
-
if (!res.ok) {
|
|
2082
|
-
const err = await res.json().catch(() => ({}));
|
|
2083
|
-
throw new Error(err.error || 'bind failed');
|
|
2084
|
-
}
|
|
2085
|
-
const updated = await res.json();
|
|
2086
|
-
const idx = channels.findIndex(c => c.id === currentChannelId);
|
|
2087
|
-
if (idx >= 0) channels[idx] = updated;
|
|
2088
|
-
renderChannels();
|
|
2089
|
-
renderWalletList();
|
|
2090
|
-
walletModalPendingSecret = null;
|
|
2091
|
-
} catch (err) {
|
|
2092
|
-
alert('绑定失败: ' + err.message);
|
|
2093
|
-
}
|
|
2094
|
-
});
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
if (walletUnbindBtn) {
|
|
2098
|
-
walletUnbindBtn.addEventListener('click', async () => {
|
|
2099
|
-
if (!currentChannelId) {
|
|
2100
|
-
alert('请先选择一个智能体');
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
if (!confirm('解绑当前智能体的钱包?')) return;
|
|
2104
|
-
try {
|
|
2105
|
-
const res = await fetch(`/channels/${currentChannelId}`, {
|
|
2106
|
-
method: 'PATCH',
|
|
2107
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2108
|
-
body: JSON.stringify({ walletAddress: null })
|
|
2109
|
-
});
|
|
2110
|
-
if (!res.ok) throw new Error('unbind failed');
|
|
2111
|
-
const updated = await res.json();
|
|
2112
|
-
const idx = channels.findIndex(c => c.id === currentChannelId);
|
|
2113
|
-
if (idx >= 0) channels[idx] = updated;
|
|
2114
|
-
walletBindAddress.value = '';
|
|
2115
|
-
renderChannels();
|
|
2116
|
-
renderWalletList();
|
|
2117
|
-
} catch (err) {
|
|
2118
|
-
alert('解绑失败: ' + err.message);
|
|
2119
|
-
}
|
|
2120
|
-
});
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
/** 渲染"所有已绑定钱包"列表 */
|
|
2124
|
-
function renderWalletList() {
|
|
2125
|
-
if (!walletListEl) return;
|
|
2126
|
-
const bound = channels.filter(c => c.walletAddress);
|
|
2127
|
-
if (bound.length === 0) {
|
|
2128
|
-
walletListEl.innerHTML = '<div class="wallet-empty">暂未绑定钱包</div>';
|
|
2129
|
-
return;
|
|
2130
|
-
}
|
|
2131
|
-
// 用 DocumentFragment 避免多次 reflow
|
|
2132
|
-
const frag = document.createDocumentFragment();
|
|
2133
|
-
bound.forEach(ch => {
|
|
2134
|
-
const isActive = ch.id === currentChannelId;
|
|
2135
|
-
const chain = detectChain(ch.walletAddress);
|
|
2136
|
-
const row = document.createElement('div');
|
|
2137
|
-
row.className = 'wallet-row' + (isActive ? ' is-active' : '');
|
|
2138
|
-
row.innerHTML = `
|
|
2139
|
-
<span class="wallet-chain">${escapeHtml(chain)}</span>
|
|
2140
|
-
<div class="wallet-info">
|
|
2141
|
-
<span class="wallet-agent" title="${escapeHtml(ch.name)}">${escapeHtml(ch.name)}</span>
|
|
2142
|
-
<span class="wallet-address" title="${escapeHtml(ch.walletAddress)}">${escapeHtml(ch.walletAddress)}</span>
|
|
2143
|
-
</div>
|
|
2144
|
-
<div class="wallet-actions">
|
|
2145
|
-
<button class="wallet-mini-btn" data-action="copy" data-addr="${escapeHtml(ch.walletAddress)}" title="复制地址">
|
|
2146
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2147
|
-
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
|
|
2148
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
2149
|
-
</svg>
|
|
2150
|
-
</button>
|
|
2151
|
-
<button class="wallet-mini-btn" data-action="goto" data-id="${escapeHtml(ch.id)}" title="切换到该智能体">
|
|
2152
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2153
|
-
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
|
2154
|
-
</svg>
|
|
2155
|
-
</button>
|
|
2156
|
-
<button class="wallet-mini-btn" data-action="unbind" data-id="${escapeHtml(ch.id)}" title="解绑">
|
|
2157
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2158
|
-
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
2159
|
-
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
2160
|
-
</svg>
|
|
2161
|
-
</button>
|
|
2162
|
-
</div>
|
|
2163
|
-
`;
|
|
2164
|
-
frag.appendChild(row);
|
|
2165
|
-
});
|
|
2166
|
-
walletListEl.innerHTML = '';
|
|
2167
|
-
walletListEl.appendChild(frag);
|
|
2168
|
-
|
|
2169
|
-
// 事件委托: 一次绑定处理三个动作
|
|
2170
|
-
walletListEl.onclick = async (ev) => {
|
|
2171
|
-
const btn = ev.target.closest('button[data-action]');
|
|
2172
|
-
if (!btn) return;
|
|
2173
|
-
const action = btn.dataset.action;
|
|
2174
|
-
if (action === 'copy') {
|
|
2175
|
-
try {
|
|
2176
|
-
await navigator.clipboard.writeText(btn.dataset.addr);
|
|
2177
|
-
btn.style.background = 'var(--accent)';
|
|
2178
|
-
btn.style.color = 'var(--bg)';
|
|
2179
|
-
setTimeout(() => { btn.style.background = ''; btn.style.color = ''; }, 800);
|
|
2180
|
-
} catch {}
|
|
2181
|
-
} else if (action === 'goto') {
|
|
2182
|
-
closeWalletModal();
|
|
2183
|
-
selectChannel(btn.dataset.id);
|
|
2184
|
-
} else if (action === 'unbind') {
|
|
2185
|
-
if (!confirm('解绑该智能体的钱包?')) return;
|
|
2186
|
-
try {
|
|
2187
|
-
const res = await fetch(`/channels/${btn.dataset.id}`, {
|
|
2188
|
-
method: 'PATCH',
|
|
2189
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2190
|
-
body: JSON.stringify({ walletAddress: null })
|
|
2191
|
-
});
|
|
2192
|
-
if (!res.ok) throw new Error('unbind failed');
|
|
2193
|
-
const updated = await res.json();
|
|
2194
|
-
const idx = channels.findIndex(c => c.id === btn.dataset.id);
|
|
2195
|
-
if (idx >= 0) channels[idx] = updated;
|
|
2196
|
-
renderChannels();
|
|
2197
|
-
renderWalletList();
|
|
2198
|
-
if (btn.dataset.id === currentChannelId) walletBindAddress.value = '';
|
|
2199
|
-
} catch (err) {
|
|
2200
|
-
alert('解绑失败: ' + err.message);
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
};
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
function detectChain(addr) {
|
|
2207
|
-
if (!addr) return '?';
|
|
2208
|
-
if (/^0x[0-9a-fA-F]{40}$/.test(addr)) return 'EVM';
|
|
2209
|
-
if (/^0x[0-9a-fA-F]{64}$/.test(addr)) return 'SUI';
|
|
2210
|
-
if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(addr)) return 'SOL';
|
|
2211
|
-
return '?';
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
// 启动应用
|
|
2215
|
-
init();
|
|
2216
|
-
|
|
2217
|
-
// =====================================================
|
|
2218
|
-
// 智能体目录:catalog add 按钮 + 钱包注册 + 自动工具调用
|
|
2219
|
-
// =====================================================
|
|
2220
|
-
const catalogAddBtn = document.getElementById('catalog-add-btn');
|
|
2221
|
-
const agentAddModal = document.getElementById('agent-add-modal');
|
|
2222
|
-
const agentAddTitle = document.getElementById('agent-add-title');
|
|
2223
|
-
const agentAddModalClose = document.getElementById('agent-add-modal-close');
|
|
2224
|
-
const agentAddName = document.getElementById('agent-add-name');
|
|
2225
|
-
const agentAddWallet = document.getElementById('agent-add-wallet');
|
|
2226
|
-
const agentAddAutoTools = document.getElementById('agent-add-auto-tools');
|
|
2227
|
-
const agentAddConfirmBtn = document.getElementById('agent-add-confirm-btn');
|
|
2228
|
-
const agentAddCancelBtn = document.getElementById('agent-add-cancel-btn');
|
|
2229
|
-
const agentAddWalletInfo = document.getElementById('agent-add-wallet-info');
|
|
2230
|
-
const agentGenerateWalletBtn = document.getElementById('agent-generate-wallet-btn');
|
|
2231
|
-
|
|
2232
|
-
/** 客户端只为提示, 不向服务端发送私钥 */
|
|
2233
|
-
let pendingWalletSecret = null;
|
|
2234
|
-
|
|
2235
|
-
function openAgentAddModal(existingChannel) {
|
|
2236
|
-
if (!agentAddModal) return;
|
|
2237
|
-
if (existingChannel) {
|
|
2238
|
-
agentAddTitle.textContent = '配置智能体:' + existingChannel.name;
|
|
2239
|
-
agentAddName.value = existingChannel.name || '';
|
|
2240
|
-
agentAddName.readOnly = true; // 改名走 PATCH
|
|
2241
|
-
agentAddWallet.value = existingChannel.walletAddress || '';
|
|
2242
|
-
agentAddAutoTools.checked = !!existingChannel.autoInvokeTools;
|
|
2243
|
-
agentAddConfirmBtn.dataset.mode = 'update';
|
|
2244
|
-
agentAddConfirmBtn.dataset.channelId = existingChannel.id;
|
|
2245
|
-
} else {
|
|
2246
|
-
agentAddTitle.textContent = '添加智能体';
|
|
2247
|
-
agentAddName.value = '';
|
|
2248
|
-
agentAddName.readOnly = false;
|
|
2249
|
-
agentAddWallet.value = '';
|
|
2250
|
-
agentAddAutoTools.checked = true;
|
|
2251
|
-
agentAddConfirmBtn.dataset.mode = 'create';
|
|
2252
|
-
delete agentAddConfirmBtn.dataset.channelId;
|
|
2253
|
-
}
|
|
2254
|
-
agentAddWalletInfo.style.display = 'none';
|
|
2255
|
-
agentAddWalletInfo.innerHTML = '';
|
|
2256
|
-
pendingWalletSecret = null;
|
|
2257
|
-
agentAddModal.classList.add('active');
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
function closeAgentAddModal() {
|
|
2261
|
-
if (!agentAddModal) return;
|
|
2262
|
-
agentAddModal.classList.remove('active');
|
|
2263
|
-
pendingWalletSecret = null;
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
/** 本地生成一个 EVM 风格地址 — 仅用于演示; 生产应使用 ethers/wagmi 等真实库 */
|
|
2267
|
-
function generateLocalWallet() {
|
|
2268
|
-
const bytes = new Uint8Array(32);
|
|
2269
|
-
crypto.getRandomValues(bytes);
|
|
2270
|
-
const privateKeyHex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
2271
|
-
// 简化: 取末 20 字节当 address, 不做 keccak, 仅作为占位
|
|
2272
|
-
const addrBytes = bytes.slice(12);
|
|
2273
|
-
const address = '0x' + Array.from(addrBytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
2274
|
-
return { address, privateKeyHex };
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
if (agentGenerateWalletBtn) {
|
|
2278
|
-
agentGenerateWalletBtn.addEventListener('click', () => {
|
|
2279
|
-
try {
|
|
2280
|
-
const { address, privateKeyHex } = generateLocalWallet();
|
|
2281
|
-
agentAddWallet.value = address;
|
|
2282
|
-
pendingWalletSecret = privateKeyHex;
|
|
2283
|
-
agentAddWalletInfo.style.display = 'block';
|
|
2284
|
-
agentAddWalletInfo.innerHTML = `
|
|
2285
|
-
✓ 已生成本地钱包<br>
|
|
2286
|
-
<strong>地址:</strong> <code>${escapeHtml(address)}</code><br>
|
|
2287
|
-
<strong>私钥 (仅本次会话, 刷新即丢):</strong> <code style="color:#f88;">${escapeHtml(privateKeyHex)}</code><br>
|
|
2288
|
-
<small style="color:#f88;">⚠ 请抄写并妥善保存私钥, 关闭页面后无法找回。仅地址会发送到服务端。</small>
|
|
2289
|
-
`;
|
|
2290
|
-
} catch (err) {
|
|
2291
|
-
agentAddWalletInfo.style.display = 'block';
|
|
2292
|
-
agentAddWalletInfo.innerHTML = '✗ 生成钱包失败: ' + escapeHtml(err.message);
|
|
2293
|
-
}
|
|
2294
|
-
});
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
if (catalogAddBtn) {
|
|
2298
|
-
catalogAddBtn.addEventListener('click', () => openAgentAddModal(null));
|
|
2299
|
-
}
|
|
2300
|
-
if (agentAddModalClose) agentAddModalClose.addEventListener('click', closeAgentAddModal);
|
|
2301
|
-
if (agentAddCancelBtn) agentAddCancelBtn.addEventListener('click', closeAgentAddModal);
|
|
2302
|
-
|
|
2303
|
-
if (agentAddConfirmBtn) {
|
|
2304
|
-
agentAddConfirmBtn.addEventListener('click', async () => {
|
|
2305
|
-
const mode = agentAddConfirmBtn.dataset.mode || 'create';
|
|
2306
|
-
const name = (agentAddName.value || '').trim();
|
|
2307
|
-
if (!name && mode === 'create') {
|
|
2308
|
-
alert('请输入智能体名称');
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
const walletAddress = (agentAddWallet.value || '').trim();
|
|
2312
|
-
const autoInvokeTools = !!agentAddAutoTools.checked;
|
|
2313
|
-
|
|
2314
|
-
try {
|
|
2315
|
-
if (mode === 'create') {
|
|
2316
|
-
const res = await fetch('/channels', {
|
|
2317
|
-
method: 'POST',
|
|
2318
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2319
|
-
body: JSON.stringify({
|
|
2320
|
-
name,
|
|
2321
|
-
agentId: currentAgentId,
|
|
2322
|
-
walletAddress: walletAddress || undefined,
|
|
2323
|
-
autoInvokeTools
|
|
2324
|
-
})
|
|
2325
|
-
});
|
|
2326
|
-
if (!res.ok) throw new Error('create failed');
|
|
2327
|
-
const channel = await res.json();
|
|
2328
|
-
channels.push(channel);
|
|
2329
|
-
renderChannels();
|
|
2330
|
-
selectChannel(channel.id);
|
|
2331
|
-
} else {
|
|
2332
|
-
// update
|
|
2333
|
-
const channelId = agentAddConfirmBtn.dataset.channelId;
|
|
2334
|
-
const res = await fetch(`/channels/${channelId}`, {
|
|
2335
|
-
method: 'PATCH',
|
|
2336
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2337
|
-
body: JSON.stringify({ walletAddress: walletAddress || null, autoInvokeTools })
|
|
2338
|
-
});
|
|
2339
|
-
if (!res.ok) throw new Error('update failed');
|
|
2340
|
-
const updated = await res.json();
|
|
2341
|
-
const idx = channels.findIndex(c => c.id === channelId);
|
|
2342
|
-
if (idx >= 0) channels[idx] = updated;
|
|
2343
|
-
renderChannels();
|
|
2344
|
-
}
|
|
2345
|
-
closeAgentAddModal();
|
|
2346
|
-
} catch (err) {
|
|
2347
|
-
console.error('Failed to save agent:', err);
|
|
2348
|
-
alert('保存失败: ' + err.message);
|
|
2349
|
-
}
|
|
2350
|
-
});
|
|
2351
|
-
}
|
|
2352
|
-
|