evolcore 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +813 -0
- package/LICENSE +21 -0
- package/MIGRATION-0.5.0.md +378 -0
- package/README.md +318 -14
- package/ROLE_ACCESS_CONTROL.md +174 -0
- package/assets/.env.template +4 -0
- package/assets/brand/evolcore/README.md +19 -0
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +13 -0
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +126 -0
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +14 -0
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +14 -0
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +10 -0
- package/bin/ec-safe-output.js +161 -0
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent.js +163 -0
- package/dist/agents/claude-runner.js +2385 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2639 -0
- package/dist/agents/gemini-runner.js +666 -0
- package/dist/agents/runner-types.js +75 -0
- package/dist/aun/aid/agentmd.js +216 -0
- package/dist/aun/aid/client.js +132 -0
- package/dist/aun/aid/control-aid.js +91 -0
- package/dist/aun/aid/identity.js +518 -0
- package/dist/aun/aid/index.js +4 -0
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/group-identity.js +10 -0
- package/dist/aun/msg/group-index.js +6 -0
- package/dist/aun/msg/group.js +1231 -0
- package/dist/aun/msg/history.js +123 -0
- package/dist/aun/msg/index.js +5 -0
- package/dist/aun/msg/p2p.js +393 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +137 -0
- package/dist/aun/outbox.js +160 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +25 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/service-proxy.js +225 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +68 -0
- package/dist/channels/aun.js +4147 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +649 -0
- package/dist/channels/feishu.js +1789 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom.js +565 -0
- package/dist/cli/agent-command.js +641 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +1948 -0
- package/dist/cli/bench.js +1228 -0
- package/dist/cli/cli-argv.js +66 -0
- package/dist/cli/code-stats.js +329 -0
- package/dist/cli/command-log.js +82 -0
- package/dist/cli/config-selector.js +69 -0
- package/dist/cli/config.js +261 -0
- package/dist/cli/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2887 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +31 -0
- package/dist/cli/index.js +358 -0
- package/dist/cli/init-channel.js +1377 -0
- package/dist/cli/init.js +553 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +590 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +126 -0
- package/dist/cli/response.js +345 -0
- package/dist/cli/restart-monitor.js +456 -0
- package/dist/cli/stats.js +607 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +505 -0
- package/dist/cli/version.js +88 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +673 -0
- package/dist/config/boot-log.js +266 -0
- package/dist/config/builtin-role-templates.js +30 -0
- package/dist/config/builtin-roles.js +85 -0
- package/dist/config/config-batch-get.js +11 -0
- package/dist/config/config-field-policy.js +261 -0
- package/dist/config/config-manager.js +916 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-book.js +371 -0
- package/dist/config/gateway-config.js +858 -0
- package/dist/config/lifecycle.js +17 -0
- package/dist/config/mention-mode.js +27 -0
- package/dist/config/merge.js +161 -0
- package/dist/config/owner-policy.js +4 -0
- package/dist/config/peer-role-resolver.js +139 -0
- package/dist/config/resolved-config-op.js +483 -0
- package/dist/config/role-config-v4-startup.js +32 -0
- package/dist/config/role-config-v5-startup.js +27 -0
- package/dist/config/role-schema.js +105 -0
- package/dist/config/role-service.js +159 -0
- package/dist/config/role-store.js +204 -0
- package/dist/config/roles.js +55 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +503 -0
- package/dist/core/auth/agent-delegation.js +111 -0
- package/dist/core/auth/auth-gateway.js +166 -0
- package/dist/core/auth/authenticated-actor.js +6 -0
- package/dist/core/auth/authorization-audit.js +110 -0
- package/dist/core/auth/operation-authorizer.js +718 -0
- package/dist/core/auth/operation-catalog.js +675 -0
- package/dist/core/baseagent-loader.js +54 -0
- package/dist/core/bootstrap-service.js +175 -0
- package/dist/core/capability/capability-manager.js +316 -0
- package/dist/core/capability/providers/claude-capability-provider.js +176 -0
- package/dist/core/capability/providers/codex-capability-provider.js +148 -0
- package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
- package/dist/core/capability/types.js +27 -0
- package/dist/core/causation/audit.js +103 -0
- package/dist/core/causation/aun-association.js +111 -0
- package/dist/core/causation/context.js +93 -0
- package/dist/core/causation/index.js +4 -0
- package/dist/core/causation/types.js +2 -0
- package/dist/core/channel-loader.js +277 -0
- package/dist/core/command/agent-control.js +616 -0
- package/dist/core/command/cli-intent-parser.js +225 -0
- package/dist/core/command/command-handler.js +1638 -0
- package/dist/core/command/menu-handler.js +3354 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1524 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3056 -0
- package/dist/core/daemon-file-cache.js +216 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +740 -0
- package/dist/core/evolagent-registry.js +546 -0
- package/dist/core/evolagent.js +331 -0
- package/dist/core/handoff/dispatcher.js +229 -0
- package/dist/core/handoff/mutex.js +46 -0
- package/dist/core/handoff/runtime.js +324 -0
- package/dist/core/handoff/store.js +537 -0
- package/dist/core/handoff/types.js +12 -0
- package/dist/core/inference/text-inference.js +173 -0
- package/dist/core/interaction-registration.js +10 -0
- package/dist/core/interaction-router.js +278 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +657 -0
- package/dist/core/message/items-formatter.js +76 -0
- package/dist/core/message/logical-queue-bridge.js +123 -0
- package/dist/core/message/message-bridge.js +803 -0
- package/dist/core/message/message-cache.js +56 -0
- package/dist/core/message/message-log.js +339 -0
- package/dist/core/message/message-processor.js +4 -0
- package/dist/core/message/message-queue.js +1436 -0
- package/dist/core/message/message-utils.js +70 -0
- package/dist/core/message/peer-mode.js +105 -0
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +33 -0
- package/dist/core/message/response-engine.js +3776 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/stream-debouncer.js +130 -0
- package/dist/core/message/stream-idle-monitor.js +124 -0
- package/dist/core/model/config-scope.js +162 -0
- package/dist/core/model/field-scope.js +78 -0
- package/dist/core/model/model-catalog.js +227 -0
- package/dist/core/model/model-diagnostics.js +182 -0
- package/dist/core/model/model-permission.js +90 -0
- package/dist/core/permission/approval-gateway.js +1017 -0
- package/dist/core/permission/ec-command-parser.js +347 -0
- package/dist/core/permission/execution-sandbox.js +16 -0
- package/dist/core/permission/index.js +6 -0
- package/dist/core/permission/mode.js +24 -0
- package/dist/core/permission/sandbox-runtime.js +265 -0
- package/dist/core/permission/tool-policy.js +987 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +330 -0
- package/dist/core/relation/peer-identity.js +222 -0
- package/dist/core/relation/peer-key.js +1 -0
- package/dist/core/role/runtime-policy.js +141 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-fs-store.js +232 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1587 -0
- package/dist/core/session/session-mapper.js +100 -0
- package/dist/core/session/session-renew.js +314 -0
- package/dist/core/session/session-title.js +128 -0
- package/dist/core/session/session-turn-coordinator.js +205 -0
- package/dist/core/session/session-turns.js +67 -0
- package/dist/core/system-channels.js +29 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/group-rules-sync.js +345 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/kit-renderer.js +359 -0
- package/dist/eck/manifest-engine.js +446 -0
- package/dist/eck/message-renderer.js +199 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +2870 -4
- package/dist/ipc.js +748 -0
- package/dist/paths.js +262 -0
- package/dist/product.js +18 -0
- package/dist/response-system/context-builder.js +71 -0
- package/dist/response-system/coordinator.js +117 -0
- package/dist/response-system/decision-executor.js +86 -0
- package/dist/response-system/engines/v1/index.js +21 -0
- package/dist/response-system/engines/v1/interactive-flow.js +27 -0
- package/dist/response-system/engines/v1/proactive-flow.js +137 -0
- package/dist/response-system/engines/v1/types.js +1 -0
- package/dist/response-system/extensions.js +41 -0
- package/dist/response-system/index.js +6 -0
- package/dist/response-system/modes/index.js +7 -0
- package/dist/response-system/modes/single-session/index.js +72 -0
- package/dist/response-system/queues/fifo-queue.js +44 -0
- package/dist/response-system/queues/index.js +6 -0
- package/dist/response-system/queues/lifo-queue.js +42 -0
- package/dist/response-system/queues/priority-queue.js +63 -0
- package/dist/response-system/registry.js +97 -0
- package/dist/response-system/resolver.js +37 -0
- package/dist/response-system/selector.js +23 -0
- package/dist/response-system/types.js +7 -0
- package/dist/stats/billing.js +151 -0
- package/dist/stats/budget.js +93 -0
- package/dist/stats/db.js +403 -0
- package/dist/stats/eck-vars.js +89 -0
- package/dist/stats/index.js +11 -0
- package/dist/stats/normalizer.js +80 -0
- package/dist/stats/price-resolver.js +138 -0
- package/dist/stats/query.js +763 -0
- package/dist/stats/role-budget.js +168 -0
- package/dist/stats/writer.js +151 -0
- package/dist/trigger/anomaly-store.js +258 -0
- package/dist/trigger/audit.js +152 -0
- package/dist/trigger/event-source.js +119 -0
- package/dist/trigger/feedback.js +685 -0
- package/dist/trigger/history.js +290 -0
- package/dist/trigger/manager.js +291 -0
- package/dist/trigger/parser.js +520 -0
- package/dist/trigger/patch.js +148 -0
- package/dist/trigger/scheduler.js +1600 -0
- package/dist/trigger/script-executor.js +155 -0
- package/dist/trigger/state.js +145 -0
- package/dist/trigger/types.js +1 -0
- package/dist/trigger/validation.js +621 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +299 -0
- package/dist/utils/atomic-write.js +95 -0
- package/dist/utils/avatar-upload.js +123 -0
- package/dist/utils/cross-platform.js +297 -0
- package/dist/utils/ecweb-utils.js +73 -0
- package/dist/utils/error-dict.json +153 -0
- package/dist/utils/error-utils.js +349 -0
- package/dist/utils/instance-registry.js +437 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +224 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +271 -0
- package/dist/utils/model-prices.jsonl +17 -0
- package/dist/utils/npm-ops.js +210 -0
- package/dist/utils/process-introspect.js +133 -0
- package/dist/utils/process-tree-stats.js +271 -0
- package/dist/utils/project-path.js +74 -0
- package/dist/utils/stats.js +410 -0
- package/dist/utils/tool-summary.js +284 -0
- package/dist/utils/welcome.js +268 -0
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +65 -0
- package/kits/docs/aun/CHEATSHEET.md +19 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +65 -0
- package/kits/docs/channels/feishu.md +56 -0
- package/kits/docs/context-assembly.md +366 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolcore/INDEX.md +66 -0
- package/kits/docs/evolcore/agent.md +69 -0
- package/kits/docs/evolcore/aid.md +49 -0
- package/kits/docs/evolcore/config.md +149 -0
- package/kits/docs/evolcore/ctl.md +46 -0
- package/kits/docs/evolcore/event.md +216 -0
- package/kits/docs/evolcore/fs-architecture.md +1215 -0
- package/kits/docs/evolcore/fs.md +101 -0
- package/kits/docs/evolcore/group-fs.md +17 -0
- package/kits/docs/evolcore/group-rules.md +226 -0
- package/kits/docs/evolcore/group.md +141 -0
- package/kits/docs/evolcore/model.md +47 -0
- package/kits/docs/evolcore/msg.md +130 -0
- package/kits/docs/evolcore/response.md +80 -0
- package/kits/docs/evolcore/rpc.md +35 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +70 -0
- package/kits/docs/evolcore/storage.md +49 -0
- package/kits/docs/evolcore/trigger.md +524 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +23 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/docs/prompt-loading-architecture.md +266 -0
- package/kits/docs/venues/aun-group.md +45 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +25 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.auxiliary.json +43 -0
- package/kits/eck_manifest.json +191 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -0
- package/kits/migrations/migrate-role-config-v4.mjs +623 -0
- package/kits/migrations/migrate-role-config-v5.mjs +346 -0
- package/kits/migrations/rename-config-file.mjs +99 -0
- package/kits/rules/01-overview.md +142 -0
- package/kits/rules/02-navigation.md +76 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +59 -0
- package/kits/rules/05-venue.md +44 -0
- package/kits/rules/06-channel.md +59 -0
- package/kits/schemas/_meta.json +29 -0
- package/kits/schemas/agent-config.schema.1.json +177 -0
- package/kits/schemas/agent-config.schema.2.json +239 -0
- package/kits/schemas/agent-config.schema.3.json +119 -0
- package/kits/schemas/agent-config.schema.4.json +208 -0
- package/kits/schemas/agent-config.schema.5.json +326 -0
- package/kits/schemas/contact-book.schema.1.json +36 -0
- package/kits/schemas/daemon.schema.1.json +90 -0
- package/kits/schemas/defaults.schema.1.json +81 -0
- package/kits/schemas/menu-exec-schema-commands.md +208 -0
- package/kits/schemas/migrations/README.md +28 -0
- package/kits/schemas/relation-config.schema.1.json +158 -0
- package/kits/schemas/relation-config.schema.2.json +73 -0
- package/kits/schemas/relation-config.schema.3.json +50 -0
- package/kits/schemas/relation-config.schema.4.json +47 -0
- package/kits/schemas/role-config.schema.1.json +200 -0
- package/kits/schemas/role-registry.schema.1.json +35 -0
- package/kits/schemas/single-session.schema.1.json +31 -0
- package/kits/templates/bootstrap-welcome.md +15 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
- package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/roles/admin.json +8 -0
- package/kits/templates/roles/member.json +40 -0
- package/kits/templates/roles/owner.json +8 -0
- package/kits/templates/roles/visitor.json +39 -0
- package/kits/templates/system-fragments/baseagent.md +14 -0
- package/kits/templates/system-fragments/bootstrap.md +16 -0
- package/kits/templates/system-fragments/channel.md +48 -0
- package/kits/templates/system-fragments/commands.md +26 -0
- package/kits/templates/system-fragments/identity.md +11 -0
- package/kits/templates/system-fragments/relation.md +19 -0
- package/kits/templates/system-fragments/session.md +53 -0
- package/kits/templates/system-fragments/venue.md +31 -0
- package/package.json +50 -15
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,1587 @@
|
|
|
1
|
+
import { ensureDir } from '../../utils/atomic-write.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { encodePath } from '../../utils/cross-platform.js';
|
|
4
|
+
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
|
|
5
|
+
import { sessionToFile, fileToSession } from './session-mapper.js';
|
|
6
|
+
import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
|
|
7
|
+
import { tryParseChannelKey } from '../channel-loader.js';
|
|
8
|
+
import { isSystemControlChannel } from '../system-channels.js';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
/** 判定用户是否为指定渠道的 admin */
|
|
13
|
+
/** 判定用户是否为指定渠道的 member */
|
|
14
|
+
export class SessionManager {
|
|
15
|
+
sessionsDir;
|
|
16
|
+
eventBus;
|
|
17
|
+
identityResolver;
|
|
18
|
+
fileAdapters = new Map();
|
|
19
|
+
sessionEncryptState = new Map();
|
|
20
|
+
constructor(sessionsDir, eventBus, identityResolver) {
|
|
21
|
+
ensureDir(sessionsDir);
|
|
22
|
+
this.sessionsDir = sessionsDir;
|
|
23
|
+
this.eventBus = eventBus;
|
|
24
|
+
this.identityResolver = identityResolver;
|
|
25
|
+
this.migrateChannelKeyFormat();
|
|
26
|
+
}
|
|
27
|
+
setIdentityResolver(resolver) {
|
|
28
|
+
this.identityResolver = resolver;
|
|
29
|
+
}
|
|
30
|
+
registerFileAdapter(adapter) {
|
|
31
|
+
this.fileAdapters.set(adapter.baseagent, adapter);
|
|
32
|
+
logger.debug(`[SessionManager] Registered file adapter: ${adapter.baseagent}`);
|
|
33
|
+
}
|
|
34
|
+
getFileAdapter(baseagent) {
|
|
35
|
+
return this.fileAdapters.get(baseagent);
|
|
36
|
+
}
|
|
37
|
+
getProjectDirName(projectPath) {
|
|
38
|
+
return encodePath(projectPath);
|
|
39
|
+
}
|
|
40
|
+
getSessionFilePath(projectPath, sessionId) {
|
|
41
|
+
const homeDir = os.homedir();
|
|
42
|
+
const encodedPath = this.getProjectDirName(projectPath);
|
|
43
|
+
return path.join(homeDir, '.claude', 'projects', encodedPath, `${sessionId}.jsonl`);
|
|
44
|
+
}
|
|
45
|
+
resolveIdentity(channel, userId, chatType, conversationId) {
|
|
46
|
+
return this.identityResolver?.(channel, userId, chatType, conversationId)
|
|
47
|
+
?? { role: 'none', mode: 'interactive' };
|
|
48
|
+
}
|
|
49
|
+
async updateIdentity(sessionId, identity) {
|
|
50
|
+
logger.debug(`[SessionManager] updateIdentity: sessionId=${sessionId}, role=${identity.role}`);
|
|
51
|
+
}
|
|
52
|
+
extractUserMessageText(messageContent) {
|
|
53
|
+
if (typeof messageContent === 'string') {
|
|
54
|
+
const text = messageContent.trim().replace(/\s+/g, ' ');
|
|
55
|
+
return text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
|
56
|
+
}
|
|
57
|
+
else if (Array.isArray(messageContent)) {
|
|
58
|
+
const textContent = messageContent.find((c) => c.type === 'text');
|
|
59
|
+
if (textContent?.text) {
|
|
60
|
+
const text = textContent.text.trim().replace(/\s+/g, ' ');
|
|
61
|
+
return text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// ─── File I/O helpers ───
|
|
67
|
+
/**
|
|
68
|
+
* 解析 chat 目录路径。
|
|
69
|
+
* 需要明确的 channelType + selfAID 才能确定路径。
|
|
70
|
+
*/
|
|
71
|
+
resolveChatDir(channel, channelId, channelType, selfAID) {
|
|
72
|
+
return chatDirPath(this.sessionsDir, channelType, channelId, selfAID);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 给定明确的 channelType + selfAID 时直接计算路径(不扫描)。
|
|
76
|
+
* 用于 caller 已经知道完整路由信息的场景(如 message-bridge 透传)。
|
|
77
|
+
*/
|
|
78
|
+
resolveChatDirExact(channel, channelId, channelType, selfAID) {
|
|
79
|
+
return chatDirPath(this.sessionsDir, channelType, channelId, selfAID);
|
|
80
|
+
}
|
|
81
|
+
resolveChatDirFromSession(session) {
|
|
82
|
+
if (!session.channelType) {
|
|
83
|
+
throw new Error(`[SessionManager] missing channelType for session ${session.id}`);
|
|
84
|
+
}
|
|
85
|
+
return chatDirPath(this.sessionsDir, session.channelType, session.channelId, session.selfAID);
|
|
86
|
+
}
|
|
87
|
+
deriveChannelIdentity(channel, channelId) {
|
|
88
|
+
const parsed = tryParseChannelKey(channel);
|
|
89
|
+
if (parsed) {
|
|
90
|
+
return { channelType: parsed.type, selfAID: parsed.type === 'aun' ? parsed.selfAID : '' };
|
|
91
|
+
}
|
|
92
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
93
|
+
if (existingDir) {
|
|
94
|
+
const active = readJsonFile(path.join(existingDir, 'active.json'));
|
|
95
|
+
if (active) {
|
|
96
|
+
return { channelType: active.channelType || channel, selfAID: active.selfAID || '' };
|
|
97
|
+
}
|
|
98
|
+
for (const metaFile of scanMetaFiles(existingDir)) {
|
|
99
|
+
const meta = readLastJsonlLine(path.join(existingDir, metaFile));
|
|
100
|
+
if (meta) {
|
|
101
|
+
return { channelType: meta.channelType || channel, selfAID: meta.selfAID || '' };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { channelType: channel, selfAID: '' };
|
|
106
|
+
}
|
|
107
|
+
/** Public accessor: get the chat directory path for a session (for message log etc.) */
|
|
108
|
+
getChatDir(session) {
|
|
109
|
+
return this.resolveChatDirFromSession(session);
|
|
110
|
+
}
|
|
111
|
+
/** Like resolveChatDir but also ensures the dir + _threads + _trash exist. */
|
|
112
|
+
ensureResolvedChatDir(channel, channelId, channelType, selfAID) {
|
|
113
|
+
const dir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
115
|
+
fs.mkdirSync(path.join(dir, '_threads'), { recursive: true });
|
|
116
|
+
fs.mkdirSync(path.join(dir, '_trash'), { recursive: true });
|
|
117
|
+
return dir;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 扫描已有 chat 目录,找到匹配 channel+channelId 的目录并返回其 chatDir 路径。
|
|
121
|
+
* 用于不知道 channelType/selfAID 的 caller 在调用 resolveChatDir 前定位已有目录。
|
|
122
|
+
*/
|
|
123
|
+
findExistingChatDir(channel, channelId) {
|
|
124
|
+
const parsed = tryParseChannelKey(channel);
|
|
125
|
+
if (parsed) {
|
|
126
|
+
const exactDir = this.resolveChatDir(channel, channelId, parsed.type, parsed.type === 'aun' ? parsed.selfAID : undefined);
|
|
127
|
+
const active = readJsonFile(path.join(exactDir, 'active.json'));
|
|
128
|
+
if (active && active.channel === channel)
|
|
129
|
+
return exactDir;
|
|
130
|
+
for (const mf of scanMetaFiles(exactDir)) {
|
|
131
|
+
const meta = readLastJsonlLine(path.join(exactDir, mf));
|
|
132
|
+
if (meta && meta.channel === channel)
|
|
133
|
+
return exactDir;
|
|
134
|
+
}
|
|
135
|
+
const threadsDir = path.join(exactDir, '_threads');
|
|
136
|
+
if (fs.existsSync(threadsDir)) {
|
|
137
|
+
const threadMetas = scanMetaFiles(threadsDir);
|
|
138
|
+
for (const mf of threadMetas) {
|
|
139
|
+
const meta = readLastJsonlLine(path.join(threadsDir, mf));
|
|
140
|
+
if (meta && meta.channel === channel)
|
|
141
|
+
return exactDir;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (fs.existsSync(exactDir))
|
|
145
|
+
return exactDir;
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const dirs = scanChatDirs(this.sessionsDir);
|
|
149
|
+
for (const d of dirs) {
|
|
150
|
+
if (d.channelId !== channelId)
|
|
151
|
+
continue;
|
|
152
|
+
const active = readJsonFile(path.join(d.dirPath, 'active.json'));
|
|
153
|
+
if (active && active.channel === channel)
|
|
154
|
+
return d.dirPath;
|
|
155
|
+
// 即使没有 active.json,也检查 meta 文件
|
|
156
|
+
const metaFiles = scanMetaFiles(d.dirPath);
|
|
157
|
+
for (const mf of metaFiles) {
|
|
158
|
+
const meta = readLastJsonlLine(path.join(d.dirPath, mf));
|
|
159
|
+
if (meta && meta.channel === channel)
|
|
160
|
+
return d.dirPath;
|
|
161
|
+
}
|
|
162
|
+
// 仅 thread session 场景:主目录无 active.json 也无 main meta,但 _threads/ 有内容
|
|
163
|
+
const threadsDir = path.join(d.dirPath, '_threads');
|
|
164
|
+
if (fs.existsSync(threadsDir)) {
|
|
165
|
+
const threadMetas = scanMetaFiles(threadsDir);
|
|
166
|
+
for (const mf of threadMetas) {
|
|
167
|
+
const meta = readLastJsonlLine(path.join(threadsDir, mf));
|
|
168
|
+
if (meta && meta.channel === channel)
|
|
169
|
+
return d.dirPath;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 安全版 resolveChatDir:先尝试用提供的 channelType/selfAID,
|
|
177
|
+
* 如果没有则扫描已有目录推断。用于操作已有 session 的公共方法。
|
|
178
|
+
*/
|
|
179
|
+
resolveChatDirSafe(channel, channelId, channelType, selfAID) {
|
|
180
|
+
if (channelType && (selfAID || channelType !== 'aun')) {
|
|
181
|
+
return this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
182
|
+
}
|
|
183
|
+
// 尝试从已有目录推断
|
|
184
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
185
|
+
if (existingDir)
|
|
186
|
+
return existingDir;
|
|
187
|
+
throw new Error(`[SessionManager] Cannot resolve chat dir for channel="${channel}" channelId="${channelId}". No channelType/selfAID provided and no existing session found.`);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 安全版 ensureResolvedChatDir:先尝试用提供的 channelType/selfAID,
|
|
191
|
+
* 如果没有则扫描已有目录推断。确保目录存在。
|
|
192
|
+
*/
|
|
193
|
+
ensureResolvedChatDirSafe(channel, channelId, channelType, selfAID) {
|
|
194
|
+
if (channelType && (selfAID || channelType !== 'aun')) {
|
|
195
|
+
return this.ensureResolvedChatDir(channel, channelId, channelType, selfAID);
|
|
196
|
+
}
|
|
197
|
+
// 尝试从已有目录推断
|
|
198
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
199
|
+
if (existingDir) {
|
|
200
|
+
// 确保子目录存在
|
|
201
|
+
fs.mkdirSync(existingDir, { recursive: true });
|
|
202
|
+
fs.mkdirSync(path.join(existingDir, '_threads'), { recursive: true });
|
|
203
|
+
fs.mkdirSync(path.join(existingDir, '_trash'), { recursive: true });
|
|
204
|
+
return existingDir;
|
|
205
|
+
}
|
|
206
|
+
throw new Error(`[SessionManager] Cannot resolve chat dir for channel="${channel}" channelId="${channelId}". No channelType/selfAID provided and no existing session found.`);
|
|
207
|
+
}
|
|
208
|
+
readActive(channel, channelId, channelType, selfAID) {
|
|
209
|
+
let dir;
|
|
210
|
+
if (channelType && selfAID) {
|
|
211
|
+
dir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
215
|
+
if (!existingDir)
|
|
216
|
+
return undefined;
|
|
217
|
+
dir = existingDir;
|
|
218
|
+
}
|
|
219
|
+
const activePath = path.join(dir, 'active.json');
|
|
220
|
+
const file = readJsonFile(activePath);
|
|
221
|
+
if (!file)
|
|
222
|
+
return undefined;
|
|
223
|
+
if (Object.prototype.hasOwnProperty.call(file, 'chatMode')) {
|
|
224
|
+
delete file.chatMode;
|
|
225
|
+
atomicWriteJson(activePath, file);
|
|
226
|
+
}
|
|
227
|
+
return fileToSession(file);
|
|
228
|
+
}
|
|
229
|
+
writeActive(channel, channelId, session) {
|
|
230
|
+
const dir = this.ensureChatDirForSession(session);
|
|
231
|
+
const file = sessionToFile(session);
|
|
232
|
+
atomicWriteJson(path.join(dir, 'active.json'), file);
|
|
233
|
+
}
|
|
234
|
+
clearActive(channel, channelId, channelType, selfAID) {
|
|
235
|
+
let dir;
|
|
236
|
+
if (channelType && selfAID) {
|
|
237
|
+
dir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
241
|
+
if (!existingDir)
|
|
242
|
+
return;
|
|
243
|
+
dir = existingDir;
|
|
244
|
+
}
|
|
245
|
+
const activePath = path.join(dir, 'active.json');
|
|
246
|
+
try {
|
|
247
|
+
fs.unlinkSync(activePath);
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
if (e.code !== 'ENOENT')
|
|
251
|
+
throw e;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
ensureChatDirForSession(session) {
|
|
255
|
+
const channelType = session.channelType || session.channel;
|
|
256
|
+
return ensureChatDir(this.sessionsDir, channelType, session.channelId, session.selfAID);
|
|
257
|
+
}
|
|
258
|
+
metaFilePath(chatDir, sessionId) {
|
|
259
|
+
return path.join(chatDir, `${sessionId}.jsonl`);
|
|
260
|
+
}
|
|
261
|
+
/** 由 session 自身的 channelType/channelId/selfId/threadId 直接定位其 .jsonl 路径(不扫描目录)。 */
|
|
262
|
+
metaPathForSession(session) {
|
|
263
|
+
const dir = this.ensureChatDirForSession(session);
|
|
264
|
+
const targetDir = session.threadId ? path.join(dir, '_threads') : dir;
|
|
265
|
+
return this.metaFilePath(targetDir, session.id);
|
|
266
|
+
}
|
|
267
|
+
appendMeta(channel, channelId, session) {
|
|
268
|
+
const file = sessionToFile(session);
|
|
269
|
+
appendJsonl(this.metaPathForSession(session), file);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Session 持久化的唯一事务原语。所有 session 写操作都应经此,业务层不直接调
|
|
273
|
+
* appendMeta / writeActive。
|
|
274
|
+
*
|
|
275
|
+
* 契约:.jsonl 是权威源,active.json 是热缓存。
|
|
276
|
+
* 1. 内建 diff 去重:与该 session 的 .jsonl 末行比较(忽略 updatedAt),无变化则跳过
|
|
277
|
+
* 2. appendMeta(.jsonl) —— 始终先落历史档案(权威源)
|
|
278
|
+
* 3. 按 intent 决定 active.json 行为:
|
|
279
|
+
* - 'set' : 无条件让该 session 成为 active(创建/切换主会话)
|
|
280
|
+
* - 'sync' : 仅当该 session 已是当前 active 时同步缓存(更新自身字段)
|
|
281
|
+
* - 'none' : 不碰 active.json(thread 会话 / 后台会话)
|
|
282
|
+
*
|
|
283
|
+
* @param session 要持久化的 session(其 updatedAt 会被刷新)
|
|
284
|
+
* @param intent active.json 行为意图
|
|
285
|
+
* @param opts.forceWrite 强制写入一条新记录,跳过去重。用于 markProcessing/clearProcessing
|
|
286
|
+
* 这类"状态转换事件必须留痕"的场景。
|
|
287
|
+
* @returns 是否真的写入了 .jsonl(去重命中时返回 false)
|
|
288
|
+
*/
|
|
289
|
+
persistSession(session, intent, opts) {
|
|
290
|
+
if (!opts?.forceWrite) {
|
|
291
|
+
const lastMeta = readLastJsonlLine(this.metaPathForSession(session));
|
|
292
|
+
if (lastMeta && this.sessionFilesEqual(lastMeta, sessionToFile(session))) {
|
|
293
|
+
// .jsonl 末行已与目标一致:仍可能需要把缓存对齐('set' 语义)
|
|
294
|
+
if (intent === 'set') {
|
|
295
|
+
session.updatedAt = Date.now();
|
|
296
|
+
this.writeActive(session.channel, session.channelId, session);
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
session.updatedAt = Date.now();
|
|
302
|
+
this.appendMeta(session.channel, session.channelId, session);
|
|
303
|
+
if (intent === 'set') {
|
|
304
|
+
this.writeActive(session.channel, session.channelId, session);
|
|
305
|
+
}
|
|
306
|
+
else if (intent === 'sync') {
|
|
307
|
+
const active = this.readActive(session.channel, session.channelId);
|
|
308
|
+
if (active && active.id === session.id) {
|
|
309
|
+
this.writeActive(session.channel, session.channelId, session);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* 比较两个 SessionFile 是否在内容上相等(忽略 updatedAt / updatedAtStr)。
|
|
316
|
+
* 用于跳过"没真变化"的写入,避免 jsonl 写放大。
|
|
317
|
+
*/
|
|
318
|
+
sessionFilesEqual(a, b) {
|
|
319
|
+
const stripVolatile = ({ updatedAt, updatedAtStr, ...rest }) => rest;
|
|
320
|
+
return JSON.stringify(stripVolatile(a)) === JSON.stringify(stripVolatile(b));
|
|
321
|
+
}
|
|
322
|
+
readMetaLatest(metaFilePath) {
|
|
323
|
+
const file = readLastJsonlLine(metaFilePath);
|
|
324
|
+
if (!file)
|
|
325
|
+
return undefined;
|
|
326
|
+
return fileToSession(file);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 为 by-sessionId 改方法加载"当前 session 状态"。
|
|
330
|
+
*
|
|
331
|
+
* 设计契约(docs/refactor/01-db-to-fs.md):
|
|
332
|
+
* active.json 是热路径权威源。.jsonl 是历史档案。
|
|
333
|
+
*
|
|
334
|
+
* 读取策略:
|
|
335
|
+
* 1. 先按 sessionId 定位 .jsonl 文件(确认 session 存在 + 拿到 channel/channelId)
|
|
336
|
+
* 2. 优先读 active.json(如果 active.id === sessionId)—— 当前状态
|
|
337
|
+
* 3. 否则 fallback 到 .jsonl 末行 —— 非活跃 session 的更新(如多 session 并存时改非 active 那个)
|
|
338
|
+
*
|
|
339
|
+
* 返回 { current }:caller 修改后交给 persistSession 写回(去重由 persistSession 内建)。
|
|
340
|
+
*/
|
|
341
|
+
loadSessionForUpdate(sessionId) {
|
|
342
|
+
const found = this.findSessionFileById(sessionId);
|
|
343
|
+
if (!found)
|
|
344
|
+
return undefined;
|
|
345
|
+
// 先读 .jsonl 末行拿 channel/channelId(active.json 文件路径需要这两个)
|
|
346
|
+
const fromJsonl = this.readMetaLatest(found.metaPath);
|
|
347
|
+
if (!fromJsonl)
|
|
348
|
+
return undefined;
|
|
349
|
+
// 优先用 active.json 的当前状态(如果它就是这个 sessionId)
|
|
350
|
+
const active = this.readActive(fromJsonl.channel, fromJsonl.channelId);
|
|
351
|
+
const base = (active && active.id === sessionId) ? active : fromJsonl;
|
|
352
|
+
const current = JSON.parse(JSON.stringify(base));
|
|
353
|
+
return { current };
|
|
354
|
+
}
|
|
355
|
+
validateSessionFile(session) {
|
|
356
|
+
const agentSessionId = session.agentSessionId;
|
|
357
|
+
if (!agentSessionId)
|
|
358
|
+
return undefined;
|
|
359
|
+
const adapter = this.getFileAdapter(session.baseagent);
|
|
360
|
+
if (!adapter) {
|
|
361
|
+
logger.error(`[SessionManager] validateSessionFile: no adapter for baseagent=${session.baseagent}, session=${session.id}`);
|
|
362
|
+
return agentSessionId;
|
|
363
|
+
}
|
|
364
|
+
if (adapter.checkExists(session.projectPath, agentSessionId))
|
|
365
|
+
return agentSessionId;
|
|
366
|
+
logger.warn(`Session file not found for ${session.baseagent}: ${agentSessionId}, clearing session ID`);
|
|
367
|
+
session.agentSessionId = undefined;
|
|
368
|
+
this.persistSession(session, 'sync');
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
getActiveChatType(channel, channelId) {
|
|
372
|
+
const active = this.readActive(channel, channelId);
|
|
373
|
+
if (active && !active.threadId)
|
|
374
|
+
return active.chatType || 'private';
|
|
375
|
+
return 'private';
|
|
376
|
+
}
|
|
377
|
+
findAllSessionsInChat(chatDir, includeThreads = true) {
|
|
378
|
+
const metaFiles = scanMetaFiles(chatDir);
|
|
379
|
+
const results = [];
|
|
380
|
+
for (const metaFile of metaFiles) {
|
|
381
|
+
const session = this.readMetaLatest(path.join(chatDir, metaFile));
|
|
382
|
+
if (session)
|
|
383
|
+
results.push(session);
|
|
384
|
+
}
|
|
385
|
+
if (includeThreads) {
|
|
386
|
+
const threadsDir = path.join(chatDir, '_threads');
|
|
387
|
+
const threadMetas = scanMetaFiles(threadsDir);
|
|
388
|
+
for (const metaFile of threadMetas) {
|
|
389
|
+
const session = this.readMetaLatest(path.join(threadsDir, metaFile));
|
|
390
|
+
if (session)
|
|
391
|
+
results.push(session);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return results;
|
|
395
|
+
}
|
|
396
|
+
findSessionFileById(sessionId) {
|
|
397
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
398
|
+
for (const { dirPath } of chatDirs) {
|
|
399
|
+
const mainPath = path.join(dirPath, `${sessionId}.jsonl`);
|
|
400
|
+
try {
|
|
401
|
+
fs.statSync(mainPath);
|
|
402
|
+
return { chatDir: dirPath, metaPath: mainPath, isThread: false };
|
|
403
|
+
}
|
|
404
|
+
catch { }
|
|
405
|
+
const threadPath = path.join(dirPath, '_threads', `${sessionId}.jsonl`);
|
|
406
|
+
try {
|
|
407
|
+
fs.statSync(threadPath);
|
|
408
|
+
return { chatDir: dirPath, metaPath: threadPath, isThread: true };
|
|
409
|
+
}
|
|
410
|
+
catch { }
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
// ─── Public API ───
|
|
415
|
+
getKnownThreadIds(channel) {
|
|
416
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
417
|
+
const threadIds = [];
|
|
418
|
+
for (const { dirPath } of chatDirs) {
|
|
419
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
420
|
+
const matchInstance = active?.channel === channel;
|
|
421
|
+
// 也兼容没 active.json 时按目录顶层 channelType 匹配(fallback 路径布局)
|
|
422
|
+
if (!matchInstance)
|
|
423
|
+
continue;
|
|
424
|
+
const index = readThreadIndex(dirPath);
|
|
425
|
+
for (const tid of Object.keys(index))
|
|
426
|
+
threadIds.push(tid);
|
|
427
|
+
}
|
|
428
|
+
return threadIds;
|
|
429
|
+
}
|
|
430
|
+
markProcessing(sessionId, taskId) {
|
|
431
|
+
const now = Date.now();
|
|
432
|
+
const state = taskId ? `${now}:${taskId}` : String(now);
|
|
433
|
+
const found = this.findSessionFileById(sessionId);
|
|
434
|
+
if (!found)
|
|
435
|
+
return;
|
|
436
|
+
const session = this.readMetaLatest(found.metaPath);
|
|
437
|
+
if (!session)
|
|
438
|
+
return;
|
|
439
|
+
session.processingState = state;
|
|
440
|
+
this.persistSession(session, 'sync', { forceWrite: true });
|
|
441
|
+
}
|
|
442
|
+
getActiveTaskId(sessionId) {
|
|
443
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
444
|
+
for (const { dirPath } of chatDirs) {
|
|
445
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
446
|
+
if (active && active.id === sessionId) {
|
|
447
|
+
if (!active.activeTask)
|
|
448
|
+
return undefined;
|
|
449
|
+
const colonIdx = active.activeTask.indexOf(':');
|
|
450
|
+
return colonIdx > 0 ? active.activeTask.slice(colonIdx + 1) : undefined;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
clearProcessing(sessionId) {
|
|
456
|
+
const found = this.findSessionFileById(sessionId);
|
|
457
|
+
if (!found) {
|
|
458
|
+
this.sessionEncryptState.delete(sessionId);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const session = this.readMetaLatest(found.metaPath);
|
|
462
|
+
if (!session) {
|
|
463
|
+
this.sessionEncryptState.delete(sessionId);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// 仅当 .jsonl 末行确实有 activeTask 时才写(避免写放大)
|
|
467
|
+
if (session.processingState) {
|
|
468
|
+
session.processingState = undefined;
|
|
469
|
+
this.persistSession(session, 'sync', { forceWrite: true });
|
|
470
|
+
}
|
|
471
|
+
this.sessionEncryptState.delete(sessionId);
|
|
472
|
+
}
|
|
473
|
+
clearProcessingIfTask(sessionId, taskId) {
|
|
474
|
+
const found = this.findSessionFileById(sessionId);
|
|
475
|
+
if (!found) {
|
|
476
|
+
this.sessionEncryptState.delete(sessionId);
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const session = this.readMetaLatest(found.metaPath);
|
|
480
|
+
if (!session?.processingState) {
|
|
481
|
+
this.sessionEncryptState.delete(sessionId);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const colonIdx = session.processingState.indexOf(':');
|
|
485
|
+
const activeTaskId = colonIdx > 0 ? session.processingState.slice(colonIdx + 1) : undefined;
|
|
486
|
+
if (activeTaskId !== taskId)
|
|
487
|
+
return false;
|
|
488
|
+
session.processingState = undefined;
|
|
489
|
+
this.persistSession(session, 'sync', { forceWrite: true });
|
|
490
|
+
this.sessionEncryptState.delete(sessionId);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
setSessionEncrypt(sessionId, encrypted) {
|
|
494
|
+
this.sessionEncryptState.set(sessionId, encrypted);
|
|
495
|
+
}
|
|
496
|
+
getSessionEncrypt(sessionId) {
|
|
497
|
+
return this.sessionEncryptState.get(sessionId);
|
|
498
|
+
}
|
|
499
|
+
getPendingProcessingSessions(maxAgeMs = 60 * 60 * 1000) {
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
const result = [];
|
|
502
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
503
|
+
for (const { dirPath } of chatDirs) {
|
|
504
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
505
|
+
const session = this.readMetaLatest(path.join(dirPath, metaFile));
|
|
506
|
+
if (!session?.processingState)
|
|
507
|
+
continue;
|
|
508
|
+
const colonIdx = session.processingState.indexOf(':');
|
|
509
|
+
const ts = parseInt(colonIdx > 0 ? session.processingState.slice(0, colonIdx) : session.processingState, 10);
|
|
510
|
+
if (!isNaN(ts) && (now - ts) < maxAgeMs) {
|
|
511
|
+
result.push(session);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
this.clearProcessing(session.id);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
// ─── Session lifecycle ───
|
|
521
|
+
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, baseagent, selfAID, channelType, peerType, identity) {
|
|
522
|
+
if (isSystemControlChannel(channel) || isSystemControlChannel(channelType)) {
|
|
523
|
+
throw new Error(`[SessionManager] system control channel is sessionless. channel="${channel}"`);
|
|
524
|
+
}
|
|
525
|
+
if (!selfAID && channelType === 'aun') {
|
|
526
|
+
throw new Error(`[SessionManager] getOrCreateSession requires selfAID for aun channel. channelId="${channelId}"`);
|
|
527
|
+
}
|
|
528
|
+
if (!channelType) {
|
|
529
|
+
throw new Error(`[SessionManager] getOrCreateSession requires channelType. channel="${channel}" channelId="${channelId}"`);
|
|
530
|
+
}
|
|
531
|
+
// 群聊场景下 conversationId 应为群 ID(AUN 群聊 channelId 即 groupId),私聊为 userId。
|
|
532
|
+
// 用于 resolveIdentity fallback 正确命中群成员角色表(见 resolvePeerRoleDetail)。
|
|
533
|
+
const identityConversationId = chatType === 'group' ? (metadata?.groupId || channelId) : userId;
|
|
534
|
+
if (threadId) {
|
|
535
|
+
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType);
|
|
536
|
+
session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
|
|
537
|
+
return session;
|
|
538
|
+
}
|
|
539
|
+
// 使用精确路径解析(caller 提供了 channelType 时直接定位,避免扫描回落)
|
|
540
|
+
const exactDir = this.resolveChatDirExact(channel, channelId, channelType, selfAID);
|
|
541
|
+
const activeFile = readJsonFile(path.join(exactDir, 'active.json'));
|
|
542
|
+
const active = activeFile ? fileToSession(activeFile) : undefined;
|
|
543
|
+
if (active && !active.threadId) {
|
|
544
|
+
const validSessionId = this.validateSessionFile(active);
|
|
545
|
+
const session = { ...active, agentSessionId: validSessionId };
|
|
546
|
+
session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
|
|
547
|
+
let mutated = false;
|
|
548
|
+
if (chatType && session.chatType !== chatType) {
|
|
549
|
+
logger.info(`[SessionManager] Updating chatType for session ${session.id}: ${session.chatType} -> ${chatType}`);
|
|
550
|
+
session.chatType = chatType;
|
|
551
|
+
mutated = true;
|
|
552
|
+
}
|
|
553
|
+
if (session.selfAID !== selfAID) {
|
|
554
|
+
session.selfAID = selfAID;
|
|
555
|
+
mutated = true;
|
|
556
|
+
}
|
|
557
|
+
if (chatType === 'private' && userId) {
|
|
558
|
+
if (!session.metadata)
|
|
559
|
+
session.metadata = {};
|
|
560
|
+
if (!session.metadata.peerId) {
|
|
561
|
+
session.metadata.peerId = userId;
|
|
562
|
+
mutated = true;
|
|
563
|
+
}
|
|
564
|
+
if (!session.metadata.peerName && metadata?.peerName) {
|
|
565
|
+
session.metadata.peerName = metadata.peerName;
|
|
566
|
+
mutated = true;
|
|
567
|
+
}
|
|
568
|
+
if (metadata?.peerType && session.metadata.peerType !== metadata.peerType) {
|
|
569
|
+
session.metadata.peerType = metadata.peerType;
|
|
570
|
+
mutated = true;
|
|
571
|
+
}
|
|
572
|
+
if (metadata?.channelKey && session.metadata.channelKey !== metadata.channelKey) {
|
|
573
|
+
session.metadata.channelKey = metadata.channelKey;
|
|
574
|
+
mutated = true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (metadata?.channelKey && chatType !== 'private') {
|
|
578
|
+
if (!session.metadata)
|
|
579
|
+
session.metadata = {};
|
|
580
|
+
if (session.metadata.channelKey !== metadata.channelKey) {
|
|
581
|
+
session.metadata.channelKey = metadata.channelKey;
|
|
582
|
+
mutated = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (metadata?.peerType && session.metadata?.peerType !== metadata.peerType) {
|
|
586
|
+
if (!session.metadata)
|
|
587
|
+
session.metadata = {};
|
|
588
|
+
session.metadata.peerType = metadata.peerType;
|
|
589
|
+
mutated = true;
|
|
590
|
+
}
|
|
591
|
+
if (mutated) {
|
|
592
|
+
this.persistSession(session, 'sync');
|
|
593
|
+
}
|
|
594
|
+
return session;
|
|
595
|
+
}
|
|
596
|
+
// Find existing session for default project path
|
|
597
|
+
const chatDir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
598
|
+
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
599
|
+
const existing = allSessions
|
|
600
|
+
.filter(s => s.projectPath === defaultProjectPath && !s.threadId)
|
|
601
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
602
|
+
if (existing) {
|
|
603
|
+
const validSessionId = this.validateSessionFile(existing);
|
|
604
|
+
const session = { ...existing, agentSessionId: validSessionId };
|
|
605
|
+
session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
|
|
606
|
+
if (!session.metadata)
|
|
607
|
+
session.metadata = {};
|
|
608
|
+
if (session.selfAID !== selfAID) {
|
|
609
|
+
session.selfAID = selfAID;
|
|
610
|
+
}
|
|
611
|
+
if (chatType && session.chatType !== chatType) {
|
|
612
|
+
logger.info(`[SessionManager] Updating chatType for session ${session.id}: ${session.chatType} -> ${chatType}`);
|
|
613
|
+
session.chatType = chatType;
|
|
614
|
+
}
|
|
615
|
+
if (chatType === 'private' && userId && !session.metadata.peerId) {
|
|
616
|
+
session.metadata.peerId = userId;
|
|
617
|
+
}
|
|
618
|
+
if (chatType === 'private' && metadata?.peerName && !session.metadata.peerName) {
|
|
619
|
+
session.metadata.peerName = metadata.peerName;
|
|
620
|
+
}
|
|
621
|
+
if (metadata?.peerType) {
|
|
622
|
+
session.metadata.peerType = metadata.peerType;
|
|
623
|
+
}
|
|
624
|
+
this.persistSession(session, 'sync');
|
|
625
|
+
return session;
|
|
626
|
+
}
|
|
627
|
+
// Create new session
|
|
628
|
+
const sessionMetadata = { ...(metadata || {}) };
|
|
629
|
+
const newIdentity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
|
|
630
|
+
const resolvedBaseagent = baseagent || 'claude';
|
|
631
|
+
const session = {
|
|
632
|
+
id: generateSessionId(),
|
|
633
|
+
channel,
|
|
634
|
+
channelType,
|
|
635
|
+
channelId,
|
|
636
|
+
selfAID,
|
|
637
|
+
projectPath: defaultProjectPath,
|
|
638
|
+
threadId: '',
|
|
639
|
+
baseagent: resolvedBaseagent,
|
|
640
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
641
|
+
chatType: chatType || 'private',
|
|
642
|
+
metadata: sessionMetadata,
|
|
643
|
+
name: name || '默认会话',
|
|
644
|
+
createdAt: Date.now(),
|
|
645
|
+
updatedAt: Date.now(),
|
|
646
|
+
};
|
|
647
|
+
session.identity = newIdentity;
|
|
648
|
+
this.persistSession(session, 'set');
|
|
649
|
+
this.eventBus.publish({
|
|
650
|
+
type: 'session:created',
|
|
651
|
+
sessionId: session.id,
|
|
652
|
+
channel,
|
|
653
|
+
channelId,
|
|
654
|
+
projectPath: defaultProjectPath,
|
|
655
|
+
name: session.name,
|
|
656
|
+
chatType: session.chatType,
|
|
657
|
+
timestamp: Date.now(),
|
|
658
|
+
});
|
|
659
|
+
return session;
|
|
660
|
+
}
|
|
661
|
+
async updateSession(sessionId, updates) {
|
|
662
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
663
|
+
if (!loaded)
|
|
664
|
+
return;
|
|
665
|
+
const { current } = loaded;
|
|
666
|
+
if (updates.baseagent !== undefined)
|
|
667
|
+
current.baseagent = updates.baseagent;
|
|
668
|
+
if (updates.chatType !== undefined)
|
|
669
|
+
current.chatType = updates.chatType;
|
|
670
|
+
if (updates.name !== undefined)
|
|
671
|
+
current.name = updates.name;
|
|
672
|
+
if (updates.metadata !== undefined)
|
|
673
|
+
current.metadata = updates.metadata;
|
|
674
|
+
if ('agentSessionId' in updates)
|
|
675
|
+
current.agentSessionId = updates.agentSessionId ?? undefined;
|
|
676
|
+
this.persistSession(current, 'sync');
|
|
677
|
+
}
|
|
678
|
+
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType) {
|
|
679
|
+
// 使用精确路径(channelType + selfAID)
|
|
680
|
+
const chatDir = (channelType && selfAID)
|
|
681
|
+
? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
|
|
682
|
+
: this.ensureResolvedChatDirSafe(channel, channelId, channelType, selfAID);
|
|
683
|
+
const threadIndex = readThreadIndex(chatDir);
|
|
684
|
+
const existingEntry = threadIndex[threadId];
|
|
685
|
+
if (existingEntry) {
|
|
686
|
+
const metaPath = path.join(chatDir, '_threads', `${existingEntry.sessionId}.jsonl`);
|
|
687
|
+
const existing = this.readMetaLatest(metaPath);
|
|
688
|
+
if (existing) {
|
|
689
|
+
const validSessionId = this.validateSessionFile(existing);
|
|
690
|
+
if (metadata) {
|
|
691
|
+
const creatorPeerId = existing.metadata?.peerId;
|
|
692
|
+
existing.metadata = { ...(existing.metadata || {}), ...metadata };
|
|
693
|
+
if (creatorPeerId && existing.metadata)
|
|
694
|
+
existing.metadata.peerId = creatorPeerId;
|
|
695
|
+
this.persistSession(existing, 'none');
|
|
696
|
+
}
|
|
697
|
+
return { ...existing, agentSessionId: validSessionId };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const projectPath = defaultProjectPath;
|
|
701
|
+
const effectiveChatType = chatType || 'private';
|
|
702
|
+
const effectiveChannelType = channelType || channel;
|
|
703
|
+
const sessionKey = formatSessionKey(effectiveChannelType, channelId, threadId);
|
|
704
|
+
// 继承主会话的 baseagent(话题会话从属于主会话,应沿用相同 backend)
|
|
705
|
+
const mainActive = readJsonFile(path.join(chatDir, 'active.json'));
|
|
706
|
+
// 兼容旧字段名
|
|
707
|
+
const mainBaseagent = mainActive?.baseagent || mainActive?.agentType;
|
|
708
|
+
const inheritedBaseagent = baseagent || mainBaseagent;
|
|
709
|
+
if (!inheritedBaseagent) {
|
|
710
|
+
throw new Error('[SessionManager] getOrCreateThreadSession: baseagent is empty');
|
|
711
|
+
}
|
|
712
|
+
const session = {
|
|
713
|
+
id: generateSessionId(),
|
|
714
|
+
channel,
|
|
715
|
+
channelType: effectiveChannelType,
|
|
716
|
+
channelId,
|
|
717
|
+
selfAID: selfAID || '',
|
|
718
|
+
projectPath,
|
|
719
|
+
threadId,
|
|
720
|
+
baseagent: inheritedBaseagent,
|
|
721
|
+
sessionKey,
|
|
722
|
+
chatType: effectiveChatType,
|
|
723
|
+
metadata,
|
|
724
|
+
name: name || '话题会话',
|
|
725
|
+
createdAt: Date.now(),
|
|
726
|
+
updatedAt: Date.now(),
|
|
727
|
+
};
|
|
728
|
+
this.persistSession(session, 'none');
|
|
729
|
+
threadIndex[threadId] = {
|
|
730
|
+
sessionId: session.id,
|
|
731
|
+
sessionKey,
|
|
732
|
+
metaFile: `${session.id}.jsonl`,
|
|
733
|
+
};
|
|
734
|
+
writeThreadIndex(chatDir, threadIndex);
|
|
735
|
+
this.eventBus.publish({
|
|
736
|
+
type: 'session:created',
|
|
737
|
+
sessionId: session.id,
|
|
738
|
+
channel,
|
|
739
|
+
channelId,
|
|
740
|
+
projectPath,
|
|
741
|
+
name: session.name,
|
|
742
|
+
timestamp: Date.now(),
|
|
743
|
+
});
|
|
744
|
+
return session;
|
|
745
|
+
}
|
|
746
|
+
async switchProject(channel, channelId, newProjectPath, baseagent) {
|
|
747
|
+
if (!baseagent) {
|
|
748
|
+
throw new Error(`[SessionManager] switchProject: baseagent is empty for channel=${channel} channelId=${channelId}`);
|
|
749
|
+
}
|
|
750
|
+
logger.info(`[SessionManager] switchProject: channel=${channel} channelId=${channelId} newPath=${newProjectPath} baseagent=${baseagent}`);
|
|
751
|
+
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
752
|
+
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
753
|
+
const chatDir = this.ensureResolvedChatDir(channel, channelId, identity.channelType, identity.selfAID);
|
|
754
|
+
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
755
|
+
const target = allSessions
|
|
756
|
+
.filter(s => s.projectPath === newProjectPath && s.baseagent === baseagent && !s.threadId)
|
|
757
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
758
|
+
if (target) {
|
|
759
|
+
const validSessionId = this.validateSessionFile(target);
|
|
760
|
+
target.agentSessionId = validSessionId;
|
|
761
|
+
this.persistSession(target, 'set');
|
|
762
|
+
return target;
|
|
763
|
+
}
|
|
764
|
+
// Derive selfAID and channelType from existing sessions in this chatDir
|
|
765
|
+
const existingAny = allSessions[0];
|
|
766
|
+
const selfAID = existingAny?.selfAID || identity.selfAID;
|
|
767
|
+
const channelType = existingAny?.channelType || identity.channelType;
|
|
768
|
+
const session = {
|
|
769
|
+
id: generateSessionId(),
|
|
770
|
+
channel,
|
|
771
|
+
channelType,
|
|
772
|
+
channelId,
|
|
773
|
+
selfAID,
|
|
774
|
+
projectPath: newProjectPath,
|
|
775
|
+
threadId: '',
|
|
776
|
+
baseagent: baseagent,
|
|
777
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
778
|
+
chatType: inheritedChatType,
|
|
779
|
+
metadata: {},
|
|
780
|
+
name: '默认会话',
|
|
781
|
+
createdAt: Date.now(),
|
|
782
|
+
updatedAt: Date.now(),
|
|
783
|
+
};
|
|
784
|
+
this.persistSession(session, 'set');
|
|
785
|
+
this.eventBus.publish({
|
|
786
|
+
type: 'session:created',
|
|
787
|
+
sessionId: session.id,
|
|
788
|
+
channel,
|
|
789
|
+
channelId,
|
|
790
|
+
projectPath: newProjectPath,
|
|
791
|
+
name: session.name,
|
|
792
|
+
timestamp: Date.now(),
|
|
793
|
+
});
|
|
794
|
+
return session;
|
|
795
|
+
}
|
|
796
|
+
async updateAgentSessionId(channel, channelId, agentSessionId) {
|
|
797
|
+
const active = this.readActive(channel, channelId);
|
|
798
|
+
if (!active)
|
|
799
|
+
return;
|
|
800
|
+
active.agentSessionId = agentSessionId;
|
|
801
|
+
this.persistSession(active, 'sync');
|
|
802
|
+
}
|
|
803
|
+
async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
|
|
804
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
805
|
+
if (!loaded)
|
|
806
|
+
return;
|
|
807
|
+
const { current } = loaded;
|
|
808
|
+
current.agentSessionId = agentSessionId;
|
|
809
|
+
const wrote = this.persistSession(current, 'sync');
|
|
810
|
+
if (wrote) {
|
|
811
|
+
logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async switchAgent(channel, channelId, projectPath, newBaseagent) {
|
|
815
|
+
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
816
|
+
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
817
|
+
// Derive channelType/selfAID from existing sessions; fall back to parsed channel key.
|
|
818
|
+
const channelType = identity.channelType;
|
|
819
|
+
const selfAID = identity.selfAID;
|
|
820
|
+
const chatDir = this.ensureResolvedChatDir(channel, channelId, channelType, selfAID);
|
|
821
|
+
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
822
|
+
const target = allSessions
|
|
823
|
+
.filter(s => s.projectPath === projectPath && s.baseagent === newBaseagent && !s.threadId)
|
|
824
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
825
|
+
if (target) {
|
|
826
|
+
const validSessionId = this.validateSessionFile(target);
|
|
827
|
+
target.agentSessionId = validSessionId;
|
|
828
|
+
this.persistSession(target, 'set');
|
|
829
|
+
return target;
|
|
830
|
+
}
|
|
831
|
+
const session = {
|
|
832
|
+
id: generateSessionId(),
|
|
833
|
+
channel,
|
|
834
|
+
channelType,
|
|
835
|
+
channelId,
|
|
836
|
+
selfAID,
|
|
837
|
+
projectPath,
|
|
838
|
+
threadId: '',
|
|
839
|
+
baseagent: newBaseagent,
|
|
840
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
841
|
+
chatType: inheritedChatType,
|
|
842
|
+
metadata: {},
|
|
843
|
+
name: '默认会话',
|
|
844
|
+
createdAt: Date.now(),
|
|
845
|
+
updatedAt: Date.now(),
|
|
846
|
+
};
|
|
847
|
+
this.persistSession(session, 'set');
|
|
848
|
+
this.eventBus.publish({
|
|
849
|
+
type: 'session:created',
|
|
850
|
+
sessionId: session.id,
|
|
851
|
+
channel,
|
|
852
|
+
channelId,
|
|
853
|
+
projectPath,
|
|
854
|
+
name: session.name,
|
|
855
|
+
timestamp: Date.now(),
|
|
856
|
+
});
|
|
857
|
+
return session;
|
|
858
|
+
}
|
|
859
|
+
async clearActiveSession(channel, channelId) {
|
|
860
|
+
const active = this.readActive(channel, channelId);
|
|
861
|
+
if (!active)
|
|
862
|
+
return;
|
|
863
|
+
active.agentSessionId = undefined;
|
|
864
|
+
this.persistSession(active, 'sync');
|
|
865
|
+
}
|
|
866
|
+
getOwnerChatId(targetChannel, ownerPeerId) {
|
|
867
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
868
|
+
let bestMatch;
|
|
869
|
+
for (const { channelId, dirPath } of chatDirs) {
|
|
870
|
+
// Check active.json first
|
|
871
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
872
|
+
// 按 channel 实例名匹配(active.json 里有;缺失就跳过这个 chat)
|
|
873
|
+
if (!active || active.channel !== targetChannel)
|
|
874
|
+
continue;
|
|
875
|
+
const candidates = [active];
|
|
876
|
+
// Also scan meta files
|
|
877
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
878
|
+
const file = readLastJsonlLine(path.join(dirPath, metaFile));
|
|
879
|
+
if (file)
|
|
880
|
+
candidates.push(file);
|
|
881
|
+
}
|
|
882
|
+
for (const cand of candidates) {
|
|
883
|
+
if (cand.chatType !== 'private')
|
|
884
|
+
continue;
|
|
885
|
+
if (cand.metadata?.peerId !== ownerPeerId)
|
|
886
|
+
continue;
|
|
887
|
+
if (!bestMatch || cand.updatedAt > bestMatch.updatedAt) {
|
|
888
|
+
bestMatch = { channelId, updatedAt: cand.updatedAt };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return bestMatch?.channelId;
|
|
893
|
+
}
|
|
894
|
+
async getSessionById(sessionId) {
|
|
895
|
+
const found = this.findSessionFileById(sessionId);
|
|
896
|
+
if (!found)
|
|
897
|
+
return undefined;
|
|
898
|
+
return this.readMetaLatest(found.metaPath);
|
|
899
|
+
}
|
|
900
|
+
async getActiveSession(channel, channelId) {
|
|
901
|
+
return this.readActive(channel, channelId);
|
|
902
|
+
}
|
|
903
|
+
/** 同步版 getActiveSession,支持精确路径定位(避免扫描) */
|
|
904
|
+
getActiveSessionSync(channel, channelId, channelType, selfAID) {
|
|
905
|
+
return this.readActive(channel, channelId, channelType, selfAID);
|
|
906
|
+
}
|
|
907
|
+
/** 当前聊天是否已经存在任意主 Session,用于区分首次入站创建与存量无消息边界。 */
|
|
908
|
+
hasMainSession(channel, channelId, channelType, selfAID) {
|
|
909
|
+
const chatDir = this.resolveChatDirExact(channel, channelId, channelType, selfAID);
|
|
910
|
+
const active = readJsonFile(path.join(chatDir, 'active.json'));
|
|
911
|
+
if (active && !active.threadId)
|
|
912
|
+
return true;
|
|
913
|
+
return scanMetaFiles(chatDir).some(metaFile => {
|
|
914
|
+
const session = readLastJsonlLine(path.join(chatDir, metaFile));
|
|
915
|
+
return !!session && !session.threadId;
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
async getThreadSession(channel, channelId, threadId) {
|
|
919
|
+
let chatDir;
|
|
920
|
+
try {
|
|
921
|
+
chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
return undefined;
|
|
925
|
+
}
|
|
926
|
+
const threadIndex = readThreadIndex(chatDir);
|
|
927
|
+
const entry = threadIndex[threadId];
|
|
928
|
+
if (!entry)
|
|
929
|
+
return undefined;
|
|
930
|
+
const metaPath = path.join(chatDir, '_threads', `${entry.sessionId}.jsonl`);
|
|
931
|
+
const session = this.readMetaLatest(metaPath);
|
|
932
|
+
if (!session)
|
|
933
|
+
return undefined;
|
|
934
|
+
const validSessionId = this.validateSessionFile(session);
|
|
935
|
+
return { ...session, agentSessionId: validSessionId };
|
|
936
|
+
}
|
|
937
|
+
async listSessions(channel, channelId) {
|
|
938
|
+
let chatDir;
|
|
939
|
+
try {
|
|
940
|
+
chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
return [];
|
|
944
|
+
}
|
|
945
|
+
const sessions = this.findAllSessionsInChat(chatDir, true);
|
|
946
|
+
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
947
|
+
}
|
|
948
|
+
async getSessionByProjectPath(channel, channelId, projectPath) {
|
|
949
|
+
let chatDir;
|
|
950
|
+
try {
|
|
951
|
+
chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
return undefined;
|
|
955
|
+
}
|
|
956
|
+
const sessions = this.findAllSessionsInChat(chatDir, false);
|
|
957
|
+
const matched = sessions.filter(s => s.projectPath === projectPath);
|
|
958
|
+
if (matched.length === 0)
|
|
959
|
+
return undefined;
|
|
960
|
+
matched.sort((a, b) => {
|
|
961
|
+
const aProc = a.processingState ? 1 : 0;
|
|
962
|
+
const bProc = b.processingState ? 1 : 0;
|
|
963
|
+
if (aProc !== bProc)
|
|
964
|
+
return bProc - aProc;
|
|
965
|
+
return b.updatedAt - a.updatedAt;
|
|
966
|
+
});
|
|
967
|
+
return matched[0];
|
|
968
|
+
}
|
|
969
|
+
async getSessionByName(channel, channelId, name) {
|
|
970
|
+
let chatDir;
|
|
971
|
+
try {
|
|
972
|
+
chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
const sessions = this.findAllSessionsInChat(chatDir, true);
|
|
978
|
+
return sessions.find(s => s.name === name);
|
|
979
|
+
}
|
|
980
|
+
async switchToSession(channel, channelId, targetSessionId) {
|
|
981
|
+
let chatDir;
|
|
982
|
+
try {
|
|
983
|
+
chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const sessions = this.findAllSessionsInChat(chatDir, true);
|
|
989
|
+
const target = sessions.find(s => s.id === targetSessionId);
|
|
990
|
+
if (!target)
|
|
991
|
+
return null;
|
|
992
|
+
this.persistSession(target, 'set');
|
|
993
|
+
return target;
|
|
994
|
+
}
|
|
995
|
+
updateMetadata(sessionId, metadata) {
|
|
996
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
997
|
+
if (!loaded)
|
|
998
|
+
return;
|
|
999
|
+
const { current } = loaded;
|
|
1000
|
+
current.metadata = metadata;
|
|
1001
|
+
this.persistSession(current, 'sync');
|
|
1002
|
+
}
|
|
1003
|
+
async renameSession(sessionId, newName) {
|
|
1004
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
1005
|
+
if (!loaded)
|
|
1006
|
+
return false;
|
|
1007
|
+
const { current } = loaded;
|
|
1008
|
+
current.name = newName;
|
|
1009
|
+
this.persistSession(current, 'sync');
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
async unbindSession(sessionId) {
|
|
1013
|
+
const found = this.findSessionFileById(sessionId);
|
|
1014
|
+
if (!found)
|
|
1015
|
+
return false;
|
|
1016
|
+
const trashDir = path.join(found.chatDir, '_trash');
|
|
1017
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
1018
|
+
const trashPath = path.join(trashDir, path.basename(found.metaPath));
|
|
1019
|
+
try {
|
|
1020
|
+
fs.renameSync(found.metaPath, trashPath);
|
|
1021
|
+
}
|
|
1022
|
+
catch (e) {
|
|
1023
|
+
if (e.code !== 'ENOENT')
|
|
1024
|
+
throw e;
|
|
1025
|
+
}
|
|
1026
|
+
// If thread session, remove from thread-index
|
|
1027
|
+
if (found.isThread) {
|
|
1028
|
+
const threadIndex = readThreadIndex(found.chatDir);
|
|
1029
|
+
for (const [tid, entry] of Object.entries(threadIndex)) {
|
|
1030
|
+
if (entry.sessionId === sessionId) {
|
|
1031
|
+
delete threadIndex[tid];
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
writeThreadIndex(found.chatDir, threadIndex);
|
|
1036
|
+
}
|
|
1037
|
+
// Clear active.json if it pointed to this session
|
|
1038
|
+
const activePath = path.join(found.chatDir, 'active.json');
|
|
1039
|
+
const active = readJsonFile(activePath);
|
|
1040
|
+
if (active && active.id === sessionId) {
|
|
1041
|
+
try {
|
|
1042
|
+
fs.unlinkSync(activePath);
|
|
1043
|
+
}
|
|
1044
|
+
catch (e) {
|
|
1045
|
+
if (e.code !== 'ENOENT')
|
|
1046
|
+
throw e;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
async softDeleteSession(channelId) {
|
|
1052
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
1053
|
+
for (const { channelId: cid, dirPath } of chatDirs) {
|
|
1054
|
+
if (cid !== channelId)
|
|
1055
|
+
continue;
|
|
1056
|
+
const trashDir = path.join(dirPath, '_trash');
|
|
1057
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
1058
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
1059
|
+
const src = path.join(dirPath, metaFile);
|
|
1060
|
+
const dst = path.join(trashDir, metaFile);
|
|
1061
|
+
try {
|
|
1062
|
+
fs.renameSync(src, dst);
|
|
1063
|
+
}
|
|
1064
|
+
catch (e) {
|
|
1065
|
+
if (e.code !== 'ENOENT')
|
|
1066
|
+
throw e;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const threadsDir = path.join(dirPath, '_threads');
|
|
1070
|
+
for (const metaFile of scanMetaFiles(threadsDir)) {
|
|
1071
|
+
const src = path.join(threadsDir, metaFile);
|
|
1072
|
+
const dst = path.join(trashDir, metaFile);
|
|
1073
|
+
try {
|
|
1074
|
+
fs.renameSync(src, dst);
|
|
1075
|
+
}
|
|
1076
|
+
catch (e) {
|
|
1077
|
+
if (e.code !== 'ENOENT')
|
|
1078
|
+
throw e;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// Clear active.json
|
|
1082
|
+
const activePath = path.join(dirPath, 'active.json');
|
|
1083
|
+
try {
|
|
1084
|
+
fs.unlinkSync(activePath);
|
|
1085
|
+
}
|
|
1086
|
+
catch (e) {
|
|
1087
|
+
if (e.code !== 'ENOENT')
|
|
1088
|
+
throw e;
|
|
1089
|
+
}
|
|
1090
|
+
// Clear thread index
|
|
1091
|
+
try {
|
|
1092
|
+
fs.unlinkSync(path.join(threadsDir, 'thread-index.json'));
|
|
1093
|
+
}
|
|
1094
|
+
catch (e) {
|
|
1095
|
+
if (e.code !== 'ENOENT')
|
|
1096
|
+
throw e;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async createNewSession(channel, channelId, projectPath, name, baseagent) {
|
|
1101
|
+
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
1102
|
+
// Derive selfAID and channelType from existing sessions
|
|
1103
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
1104
|
+
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
1105
|
+
let channelType = identity.channelType;
|
|
1106
|
+
let selfAID = identity.selfAID;
|
|
1107
|
+
if (existingDir) {
|
|
1108
|
+
const active = readJsonFile(path.join(existingDir, 'active.json'));
|
|
1109
|
+
if (active) {
|
|
1110
|
+
channelType = active.channelType || channel;
|
|
1111
|
+
selfAID = active.selfAID || '';
|
|
1112
|
+
// 兼容旧字段名
|
|
1113
|
+
const activeBaseagent = active.baseagent || active.agentType;
|
|
1114
|
+
if (!baseagent && activeBaseagent)
|
|
1115
|
+
baseagent = activeBaseagent;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (!baseagent) {
|
|
1119
|
+
throw new Error(`[SessionManager] createNewSession: baseagent is empty for channel=${channel} channelId=${channelId}`);
|
|
1120
|
+
}
|
|
1121
|
+
const session = {
|
|
1122
|
+
id: generateSessionId(),
|
|
1123
|
+
channel,
|
|
1124
|
+
channelType,
|
|
1125
|
+
channelId,
|
|
1126
|
+
selfAID,
|
|
1127
|
+
projectPath,
|
|
1128
|
+
threadId: '',
|
|
1129
|
+
baseagent,
|
|
1130
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
1131
|
+
chatType: inheritedChatType,
|
|
1132
|
+
metadata: {},
|
|
1133
|
+
name: name || '默认会话',
|
|
1134
|
+
createdAt: Date.now(),
|
|
1135
|
+
updatedAt: Date.now(),
|
|
1136
|
+
};
|
|
1137
|
+
this.persistSession(session, 'set');
|
|
1138
|
+
this.eventBus.publish({
|
|
1139
|
+
type: 'session:created',
|
|
1140
|
+
sessionId: session.id,
|
|
1141
|
+
channel,
|
|
1142
|
+
channelId,
|
|
1143
|
+
projectPath,
|
|
1144
|
+
name: session.name,
|
|
1145
|
+
timestamp: Date.now(),
|
|
1146
|
+
});
|
|
1147
|
+
return session;
|
|
1148
|
+
}
|
|
1149
|
+
async createForkedSession(sourceSession, forkedAgentSessionId, name) {
|
|
1150
|
+
const channelType = sourceSession.channelType || sourceSession.channel;
|
|
1151
|
+
const threadId = sourceSession.threadId || '';
|
|
1152
|
+
const session = {
|
|
1153
|
+
id: generateSessionId(),
|
|
1154
|
+
channel: sourceSession.channel,
|
|
1155
|
+
channelType,
|
|
1156
|
+
channelId: sourceSession.channelId,
|
|
1157
|
+
selfAID: sourceSession.selfAID,
|
|
1158
|
+
projectPath: sourceSession.projectPath,
|
|
1159
|
+
threadId,
|
|
1160
|
+
baseagent: sourceSession.baseagent,
|
|
1161
|
+
sessionKey: formatSessionKey(channelType, sourceSession.channelId, threadId || DEFAULT_THREAD_ID),
|
|
1162
|
+
chatType: sourceSession.chatType || 'private',
|
|
1163
|
+
agentSessionId: forkedAgentSessionId,
|
|
1164
|
+
metadata: {},
|
|
1165
|
+
name: name || `${sourceSession.name || '会话'}-分支`,
|
|
1166
|
+
createdAt: Date.now(),
|
|
1167
|
+
updatedAt: Date.now(),
|
|
1168
|
+
};
|
|
1169
|
+
this.persistSession(session, 'set');
|
|
1170
|
+
this.eventBus.publish({
|
|
1171
|
+
type: 'session:created',
|
|
1172
|
+
sessionId: session.id,
|
|
1173
|
+
channel: sourceSession.channel,
|
|
1174
|
+
channelId: sourceSession.channelId,
|
|
1175
|
+
projectPath: sourceSession.projectPath,
|
|
1176
|
+
name: session.name,
|
|
1177
|
+
timestamp: Date.now(),
|
|
1178
|
+
});
|
|
1179
|
+
return session;
|
|
1180
|
+
}
|
|
1181
|
+
async createForkedThreadSession(sourceSession, forkedAgentSessionId, targetThreadId, options) {
|
|
1182
|
+
if (!targetThreadId.trim())
|
|
1183
|
+
throw new Error('targetThreadId is empty');
|
|
1184
|
+
if (sourceSession.chatType !== 'private')
|
|
1185
|
+
throw new Error('topic history fork is private-only');
|
|
1186
|
+
const channelType = sourceSession.channelType || sourceSession.channel;
|
|
1187
|
+
const chatDir = this.ensureResolvedChatDir(sourceSession.channel, sourceSession.channelId, channelType, sourceSession.selfAID);
|
|
1188
|
+
const threadIndex = readThreadIndex(chatDir);
|
|
1189
|
+
if (threadIndex[targetThreadId])
|
|
1190
|
+
throw new Error('target thread already exists');
|
|
1191
|
+
const now = Date.now();
|
|
1192
|
+
const sessionKey = formatSessionKey(channelType, sourceSession.channelId, targetThreadId);
|
|
1193
|
+
const sourceMetadata = sourceSession.metadata || {};
|
|
1194
|
+
const metadata = {
|
|
1195
|
+
...(sourceMetadata.channelKey ? { channelKey: sourceMetadata.channelKey } : {}),
|
|
1196
|
+
...(sourceMetadata.peerName ? { peerName: sourceMetadata.peerName } : {}),
|
|
1197
|
+
...(sourceMetadata.peerType ? { peerType: sourceMetadata.peerType } : {}),
|
|
1198
|
+
...(options.creatorPeerId ? { peerId: options.creatorPeerId } : sourceMetadata.peerId ? { peerId: sourceMetadata.peerId } : {}),
|
|
1199
|
+
replyContext: { threadId: targetThreadId },
|
|
1200
|
+
forkSource: {
|
|
1201
|
+
sessionId: sourceSession.id,
|
|
1202
|
+
threadId: sourceSession.threadId || null,
|
|
1203
|
+
assistantMessageId: options.sourceAssistantMessageId,
|
|
1204
|
+
turn: options.sourceTurn,
|
|
1205
|
+
name: sourceSession.name || null,
|
|
1206
|
+
createdAt: now,
|
|
1207
|
+
...(options.requestId ? { requestId: options.requestId } : {}),
|
|
1208
|
+
},
|
|
1209
|
+
};
|
|
1210
|
+
const session = {
|
|
1211
|
+
id: generateSessionId(),
|
|
1212
|
+
channel: sourceSession.channel,
|
|
1213
|
+
channelType,
|
|
1214
|
+
channelId: sourceSession.channelId,
|
|
1215
|
+
selfAID: sourceSession.selfAID,
|
|
1216
|
+
projectPath: sourceSession.projectPath,
|
|
1217
|
+
threadId: targetThreadId,
|
|
1218
|
+
baseagent: sourceSession.baseagent,
|
|
1219
|
+
sessionKey,
|
|
1220
|
+
chatType: 'private',
|
|
1221
|
+
agentSessionId: forkedAgentSessionId,
|
|
1222
|
+
metadata,
|
|
1223
|
+
name: options.name || `${sourceSession.name || '会话'} · 第 ${options.sourceTurn} 轮`,
|
|
1224
|
+
createdAt: now,
|
|
1225
|
+
updatedAt: now,
|
|
1226
|
+
};
|
|
1227
|
+
this.persistSession(session, 'none');
|
|
1228
|
+
try {
|
|
1229
|
+
threadIndex[targetThreadId] = {
|
|
1230
|
+
sessionId: session.id,
|
|
1231
|
+
sessionKey,
|
|
1232
|
+
metaFile: `${session.id}.jsonl`,
|
|
1233
|
+
};
|
|
1234
|
+
writeThreadIndex(chatDir, threadIndex);
|
|
1235
|
+
}
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
try {
|
|
1238
|
+
fs.unlinkSync(this.metaPathForSession(session));
|
|
1239
|
+
}
|
|
1240
|
+
catch { }
|
|
1241
|
+
throw error;
|
|
1242
|
+
}
|
|
1243
|
+
this.eventBus.publish({
|
|
1244
|
+
type: 'session:created',
|
|
1245
|
+
sessionId: session.id,
|
|
1246
|
+
channel: session.channel,
|
|
1247
|
+
channelId: session.channelId,
|
|
1248
|
+
projectPath: session.projectPath,
|
|
1249
|
+
name: session.name,
|
|
1250
|
+
chatType: session.chatType,
|
|
1251
|
+
threadId: session.threadId,
|
|
1252
|
+
timestamp: now,
|
|
1253
|
+
});
|
|
1254
|
+
return session;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* 为超时后的独立话题创建新会话。保留聊天路由和稳定 metadata,
|
|
1258
|
+
* 但不继承任何底层 agent session 或临时任务状态。
|
|
1259
|
+
*/
|
|
1260
|
+
async createRenewedSession(sourceSession) {
|
|
1261
|
+
const metadata = { ...(sourceSession.metadata || {}) };
|
|
1262
|
+
delete metadata.agentSessions;
|
|
1263
|
+
delete metadata.resumeAt;
|
|
1264
|
+
delete metadata.replyContext;
|
|
1265
|
+
const session = {
|
|
1266
|
+
id: generateSessionId(),
|
|
1267
|
+
channel: sourceSession.channel,
|
|
1268
|
+
channelType: sourceSession.channelType || sourceSession.channel,
|
|
1269
|
+
channelId: sourceSession.channelId,
|
|
1270
|
+
selfAID: sourceSession.selfAID,
|
|
1271
|
+
projectPath: sourceSession.projectPath,
|
|
1272
|
+
threadId: '',
|
|
1273
|
+
baseagent: sourceSession.baseagent,
|
|
1274
|
+
sessionKey: formatSessionKey(sourceSession.channelType || sourceSession.channel, sourceSession.channelId, DEFAULT_THREAD_ID),
|
|
1275
|
+
chatType: sourceSession.chatType || 'private',
|
|
1276
|
+
metadata,
|
|
1277
|
+
name: '默认会话',
|
|
1278
|
+
createdAt: Date.now(),
|
|
1279
|
+
updatedAt: Date.now(),
|
|
1280
|
+
};
|
|
1281
|
+
session.identity = sourceSession.identity;
|
|
1282
|
+
this.persistSession(session, 'set');
|
|
1283
|
+
this.eventBus.publish({
|
|
1284
|
+
type: 'session:created',
|
|
1285
|
+
sessionId: session.id,
|
|
1286
|
+
channel: session.channel,
|
|
1287
|
+
channelId: session.channelId,
|
|
1288
|
+
projectPath: session.projectPath,
|
|
1289
|
+
name: session.name,
|
|
1290
|
+
chatType: session.chatType,
|
|
1291
|
+
timestamp: Date.now(),
|
|
1292
|
+
});
|
|
1293
|
+
return session;
|
|
1294
|
+
}
|
|
1295
|
+
async scanCliSessions(projectPath, baseagent, limit = 10) {
|
|
1296
|
+
const adapter = this.getFileAdapter(baseagent);
|
|
1297
|
+
if (!adapter)
|
|
1298
|
+
return [];
|
|
1299
|
+
return adapter.scanCliSessions(projectPath, limit);
|
|
1300
|
+
}
|
|
1301
|
+
async listImportableCliSessions(projectPath, baseagent) {
|
|
1302
|
+
const boundSessionIds = this.listBoundAgentSessionIds(projectPath, baseagent);
|
|
1303
|
+
const displayLimit = 10;
|
|
1304
|
+
const cliSessions = await this.scanCliSessions(projectPath, baseagent, boundSessionIds.size + displayLimit);
|
|
1305
|
+
return cliSessions
|
|
1306
|
+
.filter(session => !boundSessionIds.has(session.uuid))
|
|
1307
|
+
.slice(0, displayLimit);
|
|
1308
|
+
}
|
|
1309
|
+
listBoundAgentSessionIds(projectPath, baseagent) {
|
|
1310
|
+
const targetProjectPath = path.resolve(projectPath);
|
|
1311
|
+
const sessionIds = new Set();
|
|
1312
|
+
for (const { dirPath } of scanChatDirs(this.sessionsDir)) {
|
|
1313
|
+
for (const session of this.findAllSessionsInChat(dirPath, true)) {
|
|
1314
|
+
if (session.baseagent !== baseagent || !session.agentSessionId)
|
|
1315
|
+
continue;
|
|
1316
|
+
if (path.resolve(session.projectPath) !== targetProjectPath)
|
|
1317
|
+
continue;
|
|
1318
|
+
sessionIds.add(session.agentSessionId);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return sessionIds;
|
|
1322
|
+
}
|
|
1323
|
+
checkSessionFileExists(projectPath, agentSessionId, baseagent) {
|
|
1324
|
+
const adapter = this.getFileAdapter(baseagent);
|
|
1325
|
+
if (!adapter)
|
|
1326
|
+
return false;
|
|
1327
|
+
return adapter.checkExists(projectPath, agentSessionId);
|
|
1328
|
+
}
|
|
1329
|
+
readSessionFirstMessage(projectPath, agentSessionId, baseagent) {
|
|
1330
|
+
const adapter = this.getFileAdapter(baseagent);
|
|
1331
|
+
if (!adapter)
|
|
1332
|
+
return null;
|
|
1333
|
+
return adapter.readFirstMessage(projectPath, agentSessionId);
|
|
1334
|
+
}
|
|
1335
|
+
readSessionLastUserMessage(projectPath, agentSessionId, baseagent) {
|
|
1336
|
+
const adapter = this.getFileAdapter(baseagent);
|
|
1337
|
+
if (!adapter)
|
|
1338
|
+
return null;
|
|
1339
|
+
return adapter.readLastUserMessage(projectPath, agentSessionId);
|
|
1340
|
+
}
|
|
1341
|
+
getSessionFileInfo(projectPath, agentSessionId, baseagent) {
|
|
1342
|
+
if (!baseagent) {
|
|
1343
|
+
logger.error(`[SessionManager] getSessionFileInfo: baseagent is empty, returning turns=0. agentSessionId=${agentSessionId}`);
|
|
1344
|
+
return { turns: 0 };
|
|
1345
|
+
}
|
|
1346
|
+
const adapter = this.getFileAdapter(baseagent);
|
|
1347
|
+
if (!adapter) {
|
|
1348
|
+
logger.error(`[SessionManager] getSessionFileInfo: no adapter for baseagent=${baseagent}, returning turns=0`);
|
|
1349
|
+
return { turns: 0 };
|
|
1350
|
+
}
|
|
1351
|
+
return adapter.getFileInfo(projectPath, agentSessionId);
|
|
1352
|
+
}
|
|
1353
|
+
async listSdkSessions(projectPath, baseagent) {
|
|
1354
|
+
const adapter = this.getFileAdapter(baseagent);
|
|
1355
|
+
if (!adapter?.listSdkSessions)
|
|
1356
|
+
return [];
|
|
1357
|
+
return adapter.listSdkSessions(projectPath);
|
|
1358
|
+
}
|
|
1359
|
+
async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
|
|
1360
|
+
const chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
1361
|
+
const sessions = this.findAllSessionsInChat(chatDir, true);
|
|
1362
|
+
const matched = sessions.filter(s => s.agentSessionId && s.agentSessionId.startsWith(uuidPrefix));
|
|
1363
|
+
if (matched.length === 0)
|
|
1364
|
+
return undefined;
|
|
1365
|
+
if (matched.length > 1) {
|
|
1366
|
+
logger.warn(`Multiple sessions found with UUID prefix: ${uuidPrefix}`);
|
|
1367
|
+
}
|
|
1368
|
+
return matched[0];
|
|
1369
|
+
}
|
|
1370
|
+
async importCliSession(channel, channelId, projectPath, agentSessionId, baseagent = 'claude') {
|
|
1371
|
+
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
1372
|
+
const fileInfo = this.getSessionFileInfo(projectPath, agentSessionId, baseagent);
|
|
1373
|
+
const name = fileInfo.title || `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
|
|
1374
|
+
// Derive selfAID and channelType from existing sessions
|
|
1375
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
1376
|
+
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
1377
|
+
let channelType = identity.channelType;
|
|
1378
|
+
let selfAID = identity.selfAID;
|
|
1379
|
+
if (existingDir) {
|
|
1380
|
+
const active = readJsonFile(path.join(existingDir, 'active.json'));
|
|
1381
|
+
if (active) {
|
|
1382
|
+
channelType = active.channelType || channel;
|
|
1383
|
+
selfAID = active.selfAID || '';
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
const session = {
|
|
1387
|
+
id: generateSessionId(),
|
|
1388
|
+
channel,
|
|
1389
|
+
channelType,
|
|
1390
|
+
channelId,
|
|
1391
|
+
selfAID,
|
|
1392
|
+
projectPath,
|
|
1393
|
+
threadId: '',
|
|
1394
|
+
baseagent,
|
|
1395
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
1396
|
+
chatType: inheritedChatType,
|
|
1397
|
+
agentSessionId,
|
|
1398
|
+
metadata: {},
|
|
1399
|
+
name,
|
|
1400
|
+
createdAt: Date.now(),
|
|
1401
|
+
updatedAt: Date.now(),
|
|
1402
|
+
};
|
|
1403
|
+
this.persistSession(session, 'set');
|
|
1404
|
+
this.eventBus.publish({
|
|
1405
|
+
type: 'session:created',
|
|
1406
|
+
sessionId: session.id,
|
|
1407
|
+
channel,
|
|
1408
|
+
channelId,
|
|
1409
|
+
projectPath,
|
|
1410
|
+
name,
|
|
1411
|
+
timestamp: Date.now(),
|
|
1412
|
+
});
|
|
1413
|
+
return session;
|
|
1414
|
+
}
|
|
1415
|
+
// ─── Health status ───
|
|
1416
|
+
healthFilePath(channel, channelId) {
|
|
1417
|
+
return path.join(this.ensureResolvedChatDirSafe(channel, channelId), 'health.jsonl');
|
|
1418
|
+
}
|
|
1419
|
+
/** Find the chat dir containing a given session id */
|
|
1420
|
+
chatDirForSession(sessionId) {
|
|
1421
|
+
const found = this.findSessionFileById(sessionId);
|
|
1422
|
+
if (!found)
|
|
1423
|
+
return undefined;
|
|
1424
|
+
// 从 meta jsonl 最后一行读 channel 实例名 + channelId
|
|
1425
|
+
const meta = readLastJsonlLine(found.metaPath);
|
|
1426
|
+
if (!meta)
|
|
1427
|
+
return undefined;
|
|
1428
|
+
return { channel: meta.channel, channelId: meta.channelId, dirPath: found.chatDir };
|
|
1429
|
+
}
|
|
1430
|
+
async getHealthStatus(sessionId) {
|
|
1431
|
+
const chatInfo = this.chatDirForSession(sessionId);
|
|
1432
|
+
if (!chatInfo) {
|
|
1433
|
+
return { consecutiveErrors: 0, safeMode: false, lastSuccessTime: Date.now() };
|
|
1434
|
+
}
|
|
1435
|
+
const healthPath = path.join(chatInfo.dirPath, 'health.jsonl');
|
|
1436
|
+
const records = readAllJsonlLines(healthPath).filter(r => r.sessionId === sessionId);
|
|
1437
|
+
let consecutiveErrors = 0;
|
|
1438
|
+
let lastError;
|
|
1439
|
+
let lastErrorType;
|
|
1440
|
+
let lastSuccessTime = 0;
|
|
1441
|
+
// Walk from tail to count consecutive errors
|
|
1442
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
1443
|
+
const rec = records[i];
|
|
1444
|
+
if (rec.type === 'error') {
|
|
1445
|
+
consecutiveErrors++;
|
|
1446
|
+
if (lastError === undefined) {
|
|
1447
|
+
lastError = rec.error;
|
|
1448
|
+
lastErrorType = rec.errorType;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
// success or reset breaks the streak
|
|
1453
|
+
if (rec.type === 'success' && rec.at > lastSuccessTime)
|
|
1454
|
+
lastSuccessTime = rec.at;
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
// Find most recent success time across all records
|
|
1459
|
+
if (lastSuccessTime === 0) {
|
|
1460
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
1461
|
+
if (records[i].type === 'success' && records[i].at > lastSuccessTime) {
|
|
1462
|
+
lastSuccessTime = records[i].at;
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (lastSuccessTime === 0)
|
|
1468
|
+
lastSuccessTime = Date.now();
|
|
1469
|
+
return {
|
|
1470
|
+
consecutiveErrors,
|
|
1471
|
+
lastError,
|
|
1472
|
+
lastErrorType,
|
|
1473
|
+
safeMode: false,
|
|
1474
|
+
lastSuccessTime,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
async recordSuccess(sessionId) {
|
|
1478
|
+
const chatInfo = this.chatDirForSession(sessionId);
|
|
1479
|
+
if (!chatInfo)
|
|
1480
|
+
return;
|
|
1481
|
+
const now = Date.now();
|
|
1482
|
+
const record = {
|
|
1483
|
+
type: 'success',
|
|
1484
|
+
sessionId,
|
|
1485
|
+
at: now,
|
|
1486
|
+
atStr: formatTimestamp(now),
|
|
1487
|
+
};
|
|
1488
|
+
appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
|
|
1489
|
+
}
|
|
1490
|
+
async recordError(sessionId, errorType, errorMessage) {
|
|
1491
|
+
const chatInfo = this.chatDirForSession(sessionId);
|
|
1492
|
+
if (!chatInfo)
|
|
1493
|
+
return 0;
|
|
1494
|
+
const now = Date.now();
|
|
1495
|
+
const record = {
|
|
1496
|
+
type: 'error',
|
|
1497
|
+
sessionId,
|
|
1498
|
+
errorType,
|
|
1499
|
+
error: errorMessage,
|
|
1500
|
+
at: now,
|
|
1501
|
+
atStr: formatTimestamp(now),
|
|
1502
|
+
};
|
|
1503
|
+
appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
|
|
1504
|
+
const status = await this.getHealthStatus(sessionId);
|
|
1505
|
+
return status.consecutiveErrors;
|
|
1506
|
+
}
|
|
1507
|
+
async resetHealthStatus(sessionId) {
|
|
1508
|
+
const chatInfo = this.chatDirForSession(sessionId);
|
|
1509
|
+
if (!chatInfo)
|
|
1510
|
+
return;
|
|
1511
|
+
const now = Date.now();
|
|
1512
|
+
const record = {
|
|
1513
|
+
type: 'reset',
|
|
1514
|
+
sessionId,
|
|
1515
|
+
at: now,
|
|
1516
|
+
atStr: formatTimestamp(now),
|
|
1517
|
+
};
|
|
1518
|
+
appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
|
|
1519
|
+
}
|
|
1520
|
+
close() {
|
|
1521
|
+
for (const adapter of this.fileAdapters.values())
|
|
1522
|
+
adapter.close?.();
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Migrate session channel key from old format "{selfPeerId}#{type}#{name}"
|
|
1526
|
+
* to new format "{type}#{selfPeerId}#{name}".
|
|
1527
|
+
* Runs once at startup, rewrites active.json and meta_*.jsonl in-place.
|
|
1528
|
+
*/
|
|
1529
|
+
migrateChannelKeyFormat() {
|
|
1530
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
1531
|
+
let migrated = 0;
|
|
1532
|
+
for (const { dirPath } of chatDirs) {
|
|
1533
|
+
const activePath = path.join(dirPath, 'active.json');
|
|
1534
|
+
const active = readJsonFile(activePath);
|
|
1535
|
+
if (!active?.channel)
|
|
1536
|
+
continue;
|
|
1537
|
+
const newKey = this.convertOldChannelKey(active.channel);
|
|
1538
|
+
if (!newKey)
|
|
1539
|
+
continue;
|
|
1540
|
+
// Rewrite active.json
|
|
1541
|
+
active.channel = newKey;
|
|
1542
|
+
atomicWriteJson(activePath, active);
|
|
1543
|
+
// Rewrite meta_*.jsonl files
|
|
1544
|
+
const metaFiles = scanMetaFiles(dirPath);
|
|
1545
|
+
for (const metaFile of metaFiles) {
|
|
1546
|
+
const metaPath = path.join(dirPath, metaFile);
|
|
1547
|
+
const lines = readAllJsonlLines(metaPath);
|
|
1548
|
+
if (!lines.length)
|
|
1549
|
+
continue;
|
|
1550
|
+
let changed = false;
|
|
1551
|
+
for (const line of lines) {
|
|
1552
|
+
if (line.channel && this.convertOldChannelKey(line.channel)) {
|
|
1553
|
+
line.channel = this.convertOldChannelKey(line.channel);
|
|
1554
|
+
changed = true;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (changed) {
|
|
1558
|
+
const content = lines.map(l => JSON.stringify(l)).join('\n') + '\n';
|
|
1559
|
+
fs.writeFileSync(metaPath, content, 'utf-8');
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
migrated++;
|
|
1563
|
+
}
|
|
1564
|
+
if (migrated > 0) {
|
|
1565
|
+
logger.info(`[SessionManager] Migrated ${migrated} session(s) to new channelKey format`);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Detect old format "selfPeerId#type#name" and convert to "type#selfPeerId#name".
|
|
1570
|
+
* Returns null if already in new format or not a valid key.
|
|
1571
|
+
*/
|
|
1572
|
+
convertOldChannelKey(key) {
|
|
1573
|
+
const parts = key.split('#');
|
|
1574
|
+
if (parts.length !== 3)
|
|
1575
|
+
return null;
|
|
1576
|
+
const [first, second, third] = parts;
|
|
1577
|
+
// New format: type#selfPeerId#name — type is short (aun, feishu, wechat, etc.)
|
|
1578
|
+
// Old format: selfPeerId#type#name — selfPeerId contains '.' (e.g., evolai.agentid.pub)
|
|
1579
|
+
const knownTypes = ['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom'];
|
|
1580
|
+
if (knownTypes.includes(first))
|
|
1581
|
+
return null; // already new format
|
|
1582
|
+
if (knownTypes.includes(second) && first.includes('.')) {
|
|
1583
|
+
return `${second}#${first}#${third}`;
|
|
1584
|
+
}
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
}
|