evolcore 0.0.35 → 0.0.37
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 +42 -0
- package/README.md +6 -0
- package/bin/install-codex-managed-hooks.mjs +20 -3
- package/dist/agents/baseagent-availability.js +1 -1
- package/dist/agents/baseagent.js +88 -0
- package/dist/agents/claude-runner.js +59 -12
- package/dist/agents/codex/app-server-client.js +18 -1
- package/dist/agents/codex-runner.js +262 -21
- package/dist/agents/ecagent-runner.js +47 -11
- package/dist/agents/gemini-runner.js +36 -12
- package/dist/aun/aid/client.js +4 -3
- package/dist/aun/aid/control-aid.js +270 -8
- package/dist/aun/aid/identity.js +6 -3
- package/dist/aun/aid/ownership-operation.js +58 -0
- package/dist/aun/aid/store.js +1 -0
- package/dist/aun/group/fs-download.js +67 -2
- package/dist/aun/group/history.js +20 -5
- package/dist/aun/group/operations.js +8 -2
- package/dist/aun/menu/client.js +39 -2
- package/dist/aun/menu/transport.js +1 -1
- package/dist/aun/msg/history.js +43 -4
- package/dist/aun/msg/managed-operation.js +30 -53
- package/dist/aun/msg/p2p.js +51 -15
- package/dist/aun/outbox.js +4 -0
- package/dist/channels/aun.js +260 -73
- package/dist/channels/daemon.js +6 -4
- package/dist/channels/feishu.js +3 -0
- package/dist/cli/agent-command.js +161 -223
- package/dist/cli/agent-create.js +190 -223
- package/dist/cli/agent.js +3 -1
- package/dist/cli/aid-bind-command.js +53 -0
- package/dist/cli/aun-commands.js +384 -91
- package/dist/cli/bench.js +1 -1
- package/dist/cli/cli-argv.js +74 -0
- package/dist/cli/daemon-commands.js +105 -74
- package/dist/cli/diag.js +1 -1
- package/dist/cli/fs-command.js +18 -19
- package/dist/cli/getmeta-command.js +159 -6
- package/dist/cli/index.js +6 -12
- package/dist/cli/init-channel.js +8 -55
- package/dist/cli/init.js +125 -218
- package/dist/cli/queue-command.js +11 -8
- package/dist/cli/restart-monitor.js +177 -85
- package/dist/cli/session-command.js +1 -1
- package/dist/cli/stats.js +7 -5
- package/dist/cli/thread-command.js +103 -30
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-msg-diag.js +1 -0
- package/dist/config/access-policy.js +2 -2
- package/dist/config/boot-log.js +14 -11
- package/dist/config/config-field-policy.js +9 -1
- package/dist/config/config-manager.js +176 -90
- package/dist/config/config-operation-service.js +7 -14
- package/dist/config/contact-book-store.js +142 -26
- package/dist/config/contact-book.js +4 -2
- package/dist/config/lifecycle.js +1 -1
- package/dist/config/owner-config.js +96 -0
- package/dist/config/peer-role-resolver.js +12 -18
- package/dist/config/providers-config.js +382 -0
- package/dist/config/providers-import.js +74 -0
- package/dist/config/providers-management.js +328 -0
- package/dist/config/providers.js +22 -0
- package/dist/config/resolved-config-op.js +17 -14
- package/dist/config/snapshot.js +28 -16
- package/dist/config-store.js +18 -14
- package/dist/core/agent-ipc-registration.js +21 -63
- package/dist/core/agent-runtime.js +16 -5
- package/dist/core/auth/agent-delegation.js +199 -25
- package/dist/core/auth/auth-gateway.js +44 -2
- package/dist/core/auth/authorization-denial.js +1 -1
- package/dist/core/auth/debug-context.js +2 -1
- package/dist/core/auth/delegation-authorization.js +2 -2
- package/dist/core/auth/delegation-diagnostics.js +4 -1
- package/dist/core/auth/operation-authorizer.js +10 -1
- package/dist/core/auth/operation-catalog.js +19 -1
- package/dist/core/bootstrap-service.js +43 -20
- package/dist/core/channel-loader.js +0 -1
- package/dist/core/channel-runtime.js +30 -4
- package/dist/core/command/agent-control.js +196 -37
- package/dist/core/command/cli-intent-parser.js +60 -4
- package/dist/core/command/command-handler.js +212 -60
- package/dist/core/command/connect-menu.js +1 -0
- package/dist/core/command/diag-service.js +5 -4
- package/dist/core/command/file-search.js +368 -0
- package/dist/core/command/menu-catalog.js +2 -1
- package/dist/core/command/menu-handler.js +292 -107
- package/dist/core/command/menu-protocol.js +23 -0
- package/dist/core/command/role-menu.js +7 -19
- package/dist/core/command/skill-slash.js +130 -25
- package/dist/core/command/slash-handler.js +101 -53
- package/dist/core/command/topic-slash.js +16 -5
- package/dist/core/daemon-bootstrap.js +7 -1
- package/dist/core/daemon-control.js +19 -1
- package/dist/core/daemon-lifecycle.js +2 -1
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/evolagent-registry.js +13 -2
- package/dist/core/evolagent.js +3 -6
- package/dist/core/handoff/ipc-registration.js +2 -2
- package/dist/core/handoff/runtime.js +40 -6
- package/dist/core/handoff/store.js +1 -0
- package/dist/core/inference/text-inference.js +95 -6
- package/dist/core/ipc-registration.js +43 -24
- package/dist/core/memory/consolidation-context.js +4 -1
- package/dist/core/memory/consolidation.js +25 -14
- package/dist/core/message/aun-ipc-sender.js +296 -88
- package/dist/core/message/aun-request-registry.js +226 -0
- package/dist/core/message/create-status.js +33 -4
- package/dist/core/message/message-bridge.js +62 -13
- package/dist/core/message/message-log.js +0 -1
- package/dist/core/message/message-queue.js +4 -1
- package/dist/core/message/message-utils.js +8 -1
- package/dist/core/message/peer-mode.js +17 -9
- package/dist/core/message/response-engine.js +266 -124
- package/dist/core/message/retry-scheduler.js +11 -8
- package/dist/core/model/catalog-service.js +60 -18
- package/dist/core/model/model-catalog.js +112 -9
- package/dist/core/model/model-fallback.js +94 -26
- package/dist/core/ownership/owner-resolver.js +687 -0
- package/dist/core/permission/approval-gateway.js +196 -62
- package/dist/core/permission/ec-command-parser.js +138 -25
- package/dist/core/permission/filesystem-approval-scope.js +141 -0
- package/dist/core/permission/protected-paths.js +14 -1
- package/dist/core/permission/sandbox-runtime.js +171 -18
- package/dist/core/permission/tool-policy.js +13 -2
- package/dist/core/role/runtime-policy.js +3 -4
- package/dist/core/session/session-idle-monitor.js +10 -8
- package/dist/core/session/session-manager.js +72 -45
- package/dist/core/session/session-mapper.js +18 -7
- package/dist/core/session/session-renew.js +5 -4
- package/dist/core/session/session-title.js +2 -1
- package/dist/core/session/session-turn-coordinator.js +4 -2
- package/dist/core/skill/index-generator.js +2 -16
- package/dist/core/task/task-recovery-service.js +15 -2
- package/dist/core/thread/command-parser.js +28 -13
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +11 -8
- package/dist/core/thread/thread-operation.js +79 -44
- package/dist/core/thread/thread-route.js +54 -6
- package/dist/core/thread/thread-service.js +236 -45
- package/dist/core/thread/topic-id.js +248 -0
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/eck/message-renderer.js +12 -2
- package/dist/index.js +50 -27
- package/dist/ipc.js +59 -6
- package/dist/paths.js +1 -0
- package/dist/product.js +1 -0
- package/dist/response-system/engines/v1/proactive-flow.js +10 -3
- package/dist/stats/db.js +8 -0
- package/dist/stats/writer.js +26 -4
- package/dist/trigger/authorization.js +48 -3
- package/dist/trigger/feedback.js +28 -10
- package/dist/trigger/ipc-service.js +86 -10
- package/dist/trigger/local-files.js +55 -23
- package/dist/trigger/scheduler.js +57 -11
- package/dist/trigger/system-definitions.js +1 -0
- package/dist/trigger/validation.js +44 -15
- package/dist/utils/aid-bind.js +17 -297
- package/dist/utils/avatar-upload.js +16 -4
- package/dist/utils/cross-platform.js +115 -36
- package/dist/utils/ecweb-supervisor.js +1 -1
- package/dist/utils/ecweb-utils.js +8 -1
- package/dist/utils/error-utils.js +44 -6
- package/dist/utils/instance-registry.js +12 -2
- package/dist/utils/npm-ops.js +102 -27
- package/dist/utils/package-version-info.js +72 -27
- package/dist/utils/restart-safety.js +50 -0
- package/dist/utils/upgrade-jobs.js +304 -0
- package/dist/utils/windows-autostart.js +17 -10
- package/kits/docs/INDEX.md +5 -0
- package/kits/docs/aun/ABILITY_CALLING.md +239 -0
- package/kits/docs/evolcore/INDEX.md +14 -3
- package/kits/docs/evolcore/agent-create.md +55 -141
- package/kits/docs/evolcore/agent.md +38 -16
- package/kits/docs/evolcore/aid.md +34 -0
- package/kits/docs/evolcore/config.md +20 -1
- package/kits/docs/evolcore/event.md +2 -2
- package/kits/docs/evolcore/fs.md +18 -2
- package/kits/docs/evolcore/group-collaboration.md +13 -246
- package/kits/docs/evolcore/group.md +2 -1
- package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
- package/kits/docs/evolcore/menu-protocol.md +65 -1
- package/kits/docs/evolcore/msg.md +64 -2
- package/kits/docs/evolcore/skill.md +86 -0
- package/kits/docs/evolcore/stats.md +6 -0
- package/kits/docs/evolcore/task.md +2 -0
- package/kits/docs/evolcore/thread-chain.md +145 -51
- package/kits/docs/evolcore/trigger.md +48 -3
- package/kits/eck_manifest.json +21 -0
- package/kits/eck_message_manifest.json +4 -4
- package/kits/migrations/migrate-role-config-v5.mjs +20 -7
- package/kits/rules/01-overview.md +2 -1
- package/kits/rules/03-identity.md +3 -1
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/07-memory-and-thread.md +1 -1
- package/kits/schemas/_meta.json +16 -1
- package/kits/schemas/agent-config.schema.13.json +44 -2
- package/kits/schemas/contact-book.schema.3.json +5 -0
- package/kits/schemas/daemon.schema.6.json +3 -1
- package/kits/schemas/daemon.schema.8.json +329 -0
- package/kits/schemas/defaults.schema.5.json +39 -3
- package/kits/schemas/providers.schema.1.json +76 -0
- package/kits/schemas/relation-config.schema.9.json +5 -1
- package/kits/skills/.switches.json +28 -0
- package/kits/skills/aid-ability/SKILL.md +214 -0
- package/kits/skills/ec-agent-create/SKILL.md +162 -0
- package/kits/skills/ec-group/SKILL.md +2 -1
- package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
- package/kits/skills/ec-identity/SKILL.md +143 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
- package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
- package/kits/skills/ec-trigger/SKILL.md +167 -0
- package/kits/skills/ec-trigger/references/examples.md +153 -0
- package/kits/skills/thread-ops/SKILL.md +108 -420
- package/kits/templates/message-fragments/thread-context.md +1 -2
- package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
- package/kits/templates/roles/member.json +1 -1
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +1 -0
- package/kits/templates/system-fragments/channel.md +2 -1
- package/kits/templates/system-fragments/commands.md +5 -4
- package/kits/templates/system-fragments/identity.md +2 -0
- package/kits/templates/system-fragments/session.md +1 -1
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -3
|
@@ -5,6 +5,8 @@ import { agentSessionsDir, agentDir, agentThreadIndexPath, agentThreadSummariesD
|
|
|
5
5
|
import { atomicWriteJson, atomicReadJson } from '../../utils/atomic-write.js';
|
|
6
6
|
import { appendJsonl, appendJsonlDurable, readAllJsonlLines, readLastJsonlLine, readLastJsonlLineStrict, chatDirPath, scanChatDirs, tryParseChannelSessionKey } from '../session/session-fs-store.js';
|
|
7
7
|
import { sanitizeSessionTitle, SESSION_TITLE_MAX_LEN, SESSION_TITLE_STORED_MAX_LEN, SESSION_TITLE_ELLIPSIS } from '../session/session-title.js';
|
|
8
|
+
import { isMainSessionThreadId, isTopicThreadId, mainTopicPeer, safeFormatMainTopicId, sessionTopicThreadId } from './topic-id.js';
|
|
9
|
+
import { PeerIdentityCache } from '../relation/peer-identity.js';
|
|
8
10
|
import { ThreadContextStorage } from './thread-context-storage.js';
|
|
9
11
|
const THREAD_ID_RE = /^thread-[0-9]+-[0-9a-f]{4}-[0-9a-f]{4}$/;
|
|
10
12
|
const RESERVED_THREAD_IDS = new Set(['current', 'main', 'all']);
|
|
@@ -144,7 +146,79 @@ export class ThreadService {
|
|
|
144
146
|
findInIndex(selfAid, peer, threadId) {
|
|
145
147
|
const want = normalizeThreadPeer(peer);
|
|
146
148
|
const index = this.readThreadIndex(selfAid);
|
|
147
|
-
|
|
149
|
+
const indexed = index.threads.find(t => normalizeThreadPeer(t.peer) === want && t.threadId === threadId);
|
|
150
|
+
if (indexed)
|
|
151
|
+
return indexed;
|
|
152
|
+
// Older agents can still have a valid topic session under the per-chat
|
|
153
|
+
// `_threads/` directory while the consolidated agent index only contains
|
|
154
|
+
// newer topics. Summary reads depend on the consolidated index, so repair
|
|
155
|
+
// the missing entry lazily when a concrete topic is requested.
|
|
156
|
+
const recovered = this.recoverThreadFromSessionFiles(selfAid, want, threadId);
|
|
157
|
+
if (!recovered)
|
|
158
|
+
return undefined;
|
|
159
|
+
const next = {
|
|
160
|
+
version: 1,
|
|
161
|
+
threads: [...index.threads, recovered],
|
|
162
|
+
};
|
|
163
|
+
try {
|
|
164
|
+
this.writeThreadIndex(selfAid, next);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// An unwritable derived index must not hide a readable topic. Retry the
|
|
168
|
+
// repair on its next lookup, as with the legacy index-path migration.
|
|
169
|
+
}
|
|
170
|
+
return recovered;
|
|
171
|
+
}
|
|
172
|
+
recoverThreadFromSessionFiles(selfAid, peer, threadId) {
|
|
173
|
+
if (!peer || !threadId || RESERVED_THREAD_IDS.has(threadId))
|
|
174
|
+
return undefined;
|
|
175
|
+
const sessionsDir = agentSessionsDir(selfAid);
|
|
176
|
+
if (!fs.existsSync(sessionsDir))
|
|
177
|
+
return undefined;
|
|
178
|
+
for (const dirEntry of fs.readdirSync(sessionsDir, { withFileTypes: true })) {
|
|
179
|
+
if (!dirEntry.isDirectory())
|
|
180
|
+
continue;
|
|
181
|
+
const parsedDir = tryParseChannelSessionKey(dirEntry.name);
|
|
182
|
+
if (!parsedDir || normalizeThreadPeer(parsedDir.channelId) !== peer)
|
|
183
|
+
continue;
|
|
184
|
+
const chatDir = path.join(sessionsDir, dirEntry.name);
|
|
185
|
+
const threadsDir = path.join(chatDir, '_threads');
|
|
186
|
+
if (!fs.existsSync(threadsDir))
|
|
187
|
+
continue;
|
|
188
|
+
for (const file of fs.readdirSync(threadsDir, { withFileTypes: true })) {
|
|
189
|
+
if (!file.isFile() || !file.name.endsWith('.jsonl'))
|
|
190
|
+
continue;
|
|
191
|
+
const sessionId = file.name.slice(0, -'.jsonl'.length);
|
|
192
|
+
try {
|
|
193
|
+
const last = readLastJsonlLine(path.join(threadsDir, file.name));
|
|
194
|
+
if (!last || last.threadId !== threadId)
|
|
195
|
+
continue;
|
|
196
|
+
if (normalizeThreadPeer(last.channelId) !== peer)
|
|
197
|
+
continue;
|
|
198
|
+
if (last.selfAID && last.selfAID !== selfAid)
|
|
199
|
+
continue;
|
|
200
|
+
if (last.id && last.id !== sessionId)
|
|
201
|
+
continue;
|
|
202
|
+
return {
|
|
203
|
+
threadId,
|
|
204
|
+
sessionId,
|
|
205
|
+
peer,
|
|
206
|
+
channelType: parsedDir.channelType,
|
|
207
|
+
peerKind: last.chatType === 'group' ? 'group' : 'private',
|
|
208
|
+
name: last.name ?? threadId,
|
|
209
|
+
label: last.label ?? null,
|
|
210
|
+
topicStatus: last.topicStatus ?? 'pending',
|
|
211
|
+
createdAt: last.createdAt ?? 0,
|
|
212
|
+
lastActiveAt: last.updatedAt ?? 0,
|
|
213
|
+
...(last.nameUpdatedAt !== undefined ? { nameUpdatedAt: last.nameUpdatedAt } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Ignore an unrelated or corrupt historical session file.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
148
222
|
}
|
|
149
223
|
/**
|
|
150
224
|
* 该条目对应的会话目录。
|
|
@@ -221,19 +295,25 @@ export class ThreadService {
|
|
|
221
295
|
if (!sanitized)
|
|
222
296
|
throw new ThreadServiceError('话题名称不能为空', 'INVALID_NAME');
|
|
223
297
|
assertThreadNameLength(name, sanitized);
|
|
224
|
-
//
|
|
298
|
+
// 主话题(threadId 为空 / `main` / 规范 id)从不进话题索引,它由会话文件产生。
|
|
225
299
|
// 改名直接落到 active session 上,不走索引与快照。
|
|
226
300
|
// 群里主会话名只有 admin/owner 能改,且在授权层已经拦过;这里照常落本地。
|
|
227
|
-
if (
|
|
301
|
+
if (isMainSessionThreadId(threadId)) {
|
|
228
302
|
const main = await this.showMainSession(selfAid, peer);
|
|
229
303
|
if (!main)
|
|
230
304
|
throw new ThreadServiceError(`与 ${peer} 无活跃主会话`, 'TOPIC_NOT_FOUND');
|
|
231
305
|
if ((main.name ?? '') === sanitized)
|
|
232
306
|
return { changed: false, name: sanitized };
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
307
|
+
const nameUpdatedAt = Math.max(Date.now(), Number(main.nameUpdatedAt ?? 0) + 1);
|
|
308
|
+
// sessionId 为空说明目录存在但 active.json 还没生成(旧数据从未经过 daemon 持久化)。
|
|
309
|
+
// 此时本地存储里没有 session 记录可以改,跳过 renameSession 直接返回成功——
|
|
310
|
+
// 改名通知会发出去,对端能看到;本端下次 daemon 首次处理该会话时会写 active.json。
|
|
311
|
+
if (main.sessionId) {
|
|
312
|
+
const renamed = await this.sessionManager.renameSession(main.sessionId, sanitized, nameUpdatedAt);
|
|
313
|
+
if (!renamed)
|
|
314
|
+
throw new ThreadServiceError(`主会话改名失败: ${main.sessionId}`, 'RENAME_FAILED');
|
|
315
|
+
}
|
|
316
|
+
return { changed: true, name: sanitized, nameUpdatedAt, peerKind: main.peerKind };
|
|
237
317
|
}
|
|
238
318
|
const index = this.readThreadIndex(selfAid);
|
|
239
319
|
const entry = index.threads.find(t => t.peer === peer && t.threadId === threadId);
|
|
@@ -252,10 +332,10 @@ export class ThreadService {
|
|
|
252
332
|
if (session) {
|
|
253
333
|
await this.sessionManager.renameSession(session.id, sanitized);
|
|
254
334
|
}
|
|
255
|
-
return { changed: true, name: sanitized, nameUpdatedAt };
|
|
335
|
+
return { changed: true, name: sanitized, nameUpdatedAt, peerKind: entry.peerKind };
|
|
256
336
|
}
|
|
257
337
|
/**
|
|
258
|
-
* 应用对端发来的改名通知(`topic
|
|
338
|
+
* 应用对端发来的改名通知(`event/topic.renamed`)。
|
|
259
339
|
*
|
|
260
340
|
* 判据是最新更新时间先到先赢:通知里的 `nameUpdatedAt` 只有**严格大于**本地
|
|
261
341
|
* 记录时才生效。对端尚未建档时按通知内容建条目(与会话自动建档一致),
|
|
@@ -263,9 +343,25 @@ export class ThreadService {
|
|
|
263
343
|
*/
|
|
264
344
|
async applyRemoteRename(input) {
|
|
265
345
|
const sanitized = sanitizeSessionTitle(input.name);
|
|
266
|
-
if (!sanitized || !input.threadId)
|
|
346
|
+
if (!sanitized || !isMainSessionThreadId(input.threadId) && !isTopicThreadId(input.threadId)) {
|
|
267
347
|
return { applied: false, reason: 'invalid' };
|
|
348
|
+
}
|
|
268
349
|
const peer = normalizeThreadPeer(input.peer) ?? input.peer;
|
|
350
|
+
// 主话题:与话题同一套改名语义,只是落到主会话上——主话题从不进话题索引,
|
|
351
|
+
// 这里绝不能新建条目(会凭空多出一条不存在的话题)。id 自带双方,
|
|
352
|
+
// 先校验另一端就是本次发送方,避免拿别人的主话题 id 改错关系。
|
|
353
|
+
if (isMainSessionThreadId(input.threadId)) {
|
|
354
|
+
if (mainTopicPeer(input.threadId, input.selfAid) !== peer) {
|
|
355
|
+
return { applied: false, reason: 'invalid' };
|
|
356
|
+
}
|
|
357
|
+
const main = await this.showMainSession(input.selfAid, peer);
|
|
358
|
+
if (!main)
|
|
359
|
+
return { applied: false, reason: 'invalid' };
|
|
360
|
+
if (input.nameUpdatedAt <= Number(main.nameUpdatedAt ?? 0))
|
|
361
|
+
return { applied: false, reason: 'stale' };
|
|
362
|
+
const renamed = await this.sessionManager.renameSession(main.sessionId, sanitized, input.nameUpdatedAt);
|
|
363
|
+
return renamed ? { applied: true } : { applied: false, reason: 'invalid' };
|
|
364
|
+
}
|
|
269
365
|
const index = this.readThreadIndex(input.selfAid);
|
|
270
366
|
const entry = index.threads.find(t => t.peer === peer && t.threadId === input.threadId);
|
|
271
367
|
const current = entry?.nameUpdatedAt ?? 0;
|
|
@@ -367,7 +463,7 @@ export class ThreadService {
|
|
|
367
463
|
}
|
|
368
464
|
}
|
|
369
465
|
// 索引之外的兜底:磁盘上真实存在、且有消息或会话存档的 chat 目录。
|
|
370
|
-
//
|
|
466
|
+
// 只加索引里没有的对端——索引已经有了的,话题数已算过,不再重复计主会话。
|
|
371
467
|
// 从目录名解析 peer 与 channelType,避免信赖可能过期的索引字段。
|
|
372
468
|
try {
|
|
373
469
|
const agentsRoot = path.dirname(agentDir(selfAid));
|
|
@@ -420,7 +516,9 @@ export class ThreadService {
|
|
|
420
516
|
catch {
|
|
421
517
|
// 消息日志缺失/损坏时保持索引值
|
|
422
518
|
}
|
|
423
|
-
|
|
519
|
+
const cached = PeerIdentityCache.get('aun', peer, agentDir(selfAid));
|
|
520
|
+
const peerName = cached?.name ?? (typeof cached?.metadata?.['name'] === 'string' ? cached.metadata['name'] : undefined);
|
|
521
|
+
return { peer, peerName, peerKind: v.peerKind, threadCount: v.threadCount, lastActiveAt };
|
|
424
522
|
})
|
|
425
523
|
.sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
|
426
524
|
}
|
|
@@ -441,12 +539,11 @@ export class ThreadService {
|
|
|
441
539
|
return false;
|
|
442
540
|
return true;
|
|
443
541
|
});
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
// 合成记录,让 list 与 history 的口径一致。
|
|
542
|
+
// 主话题由会话文件产生、从不进话题索引。只列话题会让「和某对端聊过什么」
|
|
543
|
+
// 在列表里看不到主线,而 `history <peer>` 不传话题读的恰恰是主话题消息。
|
|
544
|
+
// 指定对端时补一条合成记录,让 list 与 history 的口径一致。
|
|
448
545
|
if (peer) {
|
|
449
|
-
const hasMain = threads.some(t =>
|
|
546
|
+
const hasMain = threads.some(t => isMainSessionThreadId(t.threadId));
|
|
450
547
|
if (!hasMain) {
|
|
451
548
|
const chatDir = this.chatDirFor(selfAid, peer);
|
|
452
549
|
const logPath = path.join(chatDir, 'messages.jsonl');
|
|
@@ -454,7 +551,7 @@ export class ThreadService {
|
|
|
454
551
|
if (fs.existsSync(logPath)) {
|
|
455
552
|
try {
|
|
456
553
|
for (const m of readAllJsonlLines(logPath)) {
|
|
457
|
-
if (
|
|
554
|
+
if (isMainSessionThreadId(m.threadId) && typeof m.ts === 'number' && m.ts > lastTs)
|
|
458
555
|
lastTs = m.ts;
|
|
459
556
|
}
|
|
460
557
|
}
|
|
@@ -465,14 +562,15 @@ export class ThreadService {
|
|
|
465
562
|
const active = this.sessionManager.getActiveSessionSync('aun', peer, 'aun', selfAid);
|
|
466
563
|
const peerKind = active?.chatType === 'group' ? 'group' : 'private';
|
|
467
564
|
const synthetic = {
|
|
468
|
-
|
|
565
|
+
// 主话题的 id 与话题并列,同样可直接作为 --thread 的值。
|
|
566
|
+
threadId: safeFormatMainTopicId(selfAid, peer) ?? '',
|
|
469
567
|
sessionId: active?.id ?? '',
|
|
470
568
|
peer,
|
|
471
569
|
channelType: 'aun',
|
|
472
570
|
peerKind,
|
|
473
571
|
// 主会话有自己的名字(创建时写 `默认会话`,可经 ec thread rename 修改)。
|
|
474
572
|
// 读真实值而不是写死 `主会话`:否则列表里的名字和会话存档对不上,
|
|
475
|
-
// 用户也没法从列表确认改名是否生效。`[
|
|
573
|
+
// 用户也没法从列表确认改名是否生效。`[主话题]` 前缀由 CLI 渲染。
|
|
476
574
|
name: active?.name || '主会话',
|
|
477
575
|
label: null,
|
|
478
576
|
topicStatus: 'pending',
|
|
@@ -493,6 +591,10 @@ export class ThreadService {
|
|
|
493
591
|
// 先读消息日志并派生真实活跃时间,再排序、再分页——
|
|
494
592
|
// 否则 recent 排序用的是索引里的管理动作时间,与实际展示的活跃时间不一致。
|
|
495
593
|
const ts24h = Date.now() - 86400000;
|
|
594
|
+
// 主话题在消息日志里可能是历史空值/`main`,也可能是现行规范 id;两者都算同一条主线。
|
|
595
|
+
const msgBelongsTo = (msgThreadId, topicThreadId) => isMainSessionThreadId(topicThreadId)
|
|
596
|
+
? isMainSessionThreadId(msgThreadId)
|
|
597
|
+
: (msgThreadId ?? '') === (topicThreadId ?? '');
|
|
496
598
|
// 按会话目录分组,每个目录只读一次 messages.jsonl。
|
|
497
599
|
// 键用 channelType + peer,避免不同命名空间下同名对端串数据。
|
|
498
600
|
const peerMessages = new Map();
|
|
@@ -526,12 +628,10 @@ export class ThreadService {
|
|
|
526
628
|
const lastMsgOf = new Map();
|
|
527
629
|
for (const t of threads) {
|
|
528
630
|
const msgs = (peerMessages.get(`${entryChannelType(t)}\u0000${t.peer}`) ?? [])
|
|
529
|
-
.filter(m => (m.threadId
|
|
631
|
+
.filter(m => msgBelongsTo(m.threadId, t.threadId));
|
|
530
632
|
const lastMsg = msgs.reduce((acc, m) => (!acc || m.ts > acc.ts) ? m : acc, null);
|
|
531
|
-
// 合成的主会话条目用 `main` 作 threadId,日志里主会话记的是 null。
|
|
532
|
-
const key = t.threadId ?? '';
|
|
533
633
|
if (lastMsg)
|
|
534
|
-
lastMsgOf.set(
|
|
634
|
+
lastMsgOf.set(t.threadId ?? '', lastMsg);
|
|
535
635
|
}
|
|
536
636
|
if (sort === 'created') {
|
|
537
637
|
threads.sort((a, b) => b.createdAt - a.createdAt);
|
|
@@ -545,7 +645,7 @@ export class ThreadService {
|
|
|
545
645
|
threads = threads.slice(offset, offset + limit);
|
|
546
646
|
threads = threads.map(t => {
|
|
547
647
|
const msgs = (peerMessages.get(`${entryChannelType(t)}\u0000${t.peer}`) ?? [])
|
|
548
|
-
.filter(m => (m.threadId
|
|
648
|
+
.filter(m => msgBelongsTo(m.threadId, t.threadId));
|
|
549
649
|
const msgTotal = msgs.length;
|
|
550
650
|
const msg24h = msgs.filter(m => m.ts >= ts24h).length;
|
|
551
651
|
const lastMsg = lastMsgOf.get(t.threadId ?? '');
|
|
@@ -598,18 +698,20 @@ export class ThreadService {
|
|
|
598
698
|
const peer = normalizeThreadPeer(peerInput) ?? peerInput;
|
|
599
699
|
// 不检查索引——允许读取已删除话题的历史(session 文件已移入 _trash,但 messages.jsonl 仍在)
|
|
600
700
|
// 索引里能查到条目时用条目的命名空间(内部话题在 daemon 下),查不到时退回 aun。
|
|
601
|
-
const indexed =
|
|
701
|
+
const indexed = isMainSessionThreadId(threadId)
|
|
702
|
+
? undefined
|
|
703
|
+
: this.readThreadIndex(selfAid).threads.find(t => t.threadId === threadId && normalizeThreadPeer(t.peer) === peer);
|
|
602
704
|
const chatDir = this.chatDirFor(selfAid, indexed?.peer ?? peer, indexed?.channelType);
|
|
603
705
|
const logPath = path.join(chatDir, 'messages.jsonl');
|
|
604
706
|
if (!fs.existsSync(logPath))
|
|
605
707
|
return { peer, messages: [] };
|
|
606
708
|
const messages = readAllJsonlLines(logPath)
|
|
607
709
|
.filter((m) => {
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
if (threadId)
|
|
611
|
-
return m.threadId
|
|
612
|
-
return
|
|
710
|
+
// 主话题的消息行在历史里是 '' 或 null,新写入的是规范 id;三种都算主话题。
|
|
711
|
+
// 话题按 id 精确匹配。
|
|
712
|
+
if (isMainSessionThreadId(threadId))
|
|
713
|
+
return isMainSessionThreadId(m.threadId);
|
|
714
|
+
return m.threadId === threadId;
|
|
613
715
|
})
|
|
614
716
|
.map((m) => {
|
|
615
717
|
// 消息体优先取 payloadSummary.text(加密消息的明文摘要),
|
|
@@ -651,7 +753,10 @@ export class ThreadService {
|
|
|
651
753
|
count: Math.min(filtered.length, limit),
|
|
652
754
|
total: filtered.length,
|
|
653
755
|
messages: shown,
|
|
654
|
-
|
|
756
|
+
// 主话题返回规范 id,让调用方拿到的东西可以直接再当 `--thread` 用。
|
|
757
|
+
threadId: isMainSessionThreadId(threadId)
|
|
758
|
+
? (safeFormatMainTopicId(selfAid, peer) ?? '')
|
|
759
|
+
: (threadId ?? ''),
|
|
655
760
|
peer,
|
|
656
761
|
};
|
|
657
762
|
}
|
|
@@ -748,20 +853,42 @@ export class ThreadService {
|
|
|
748
853
|
const normalized = normalizeThreadPeer(peer) ?? peer;
|
|
749
854
|
const channelKey = `aun#${selfAid}#main`;
|
|
750
855
|
const session = await this.sessionManager.getActiveSession(channelKey, normalized);
|
|
751
|
-
if (
|
|
856
|
+
if (session) {
|
|
857
|
+
return {
|
|
858
|
+
// 主话题返回规范 id(按调用方给的 selfAid/peer 拼),话题返回话题 id。
|
|
859
|
+
threadId: sessionTopicThreadId(session)
|
|
860
|
+
?? (safeFormatMainTopicId(selfAid, normalized) ?? ''),
|
|
861
|
+
sessionId: session.id,
|
|
862
|
+
peer: normalized,
|
|
863
|
+
channelType: 'aun',
|
|
864
|
+
peerKind: session.chatType === 'group' ? 'group' : 'private',
|
|
865
|
+
name: session.name ?? peer,
|
|
866
|
+
nameUpdatedAt: typeof session.metadata?.nameUpdatedAt === 'number' ? session.metadata.nameUpdatedAt : undefined,
|
|
867
|
+
label: session.label ?? null,
|
|
868
|
+
topicStatus: session.topicStatus ?? 'pending',
|
|
869
|
+
createdAt: session.createdAt ?? 0,
|
|
870
|
+
lastActiveAt: session.updatedAt ?? 0,
|
|
871
|
+
running: session.processingState?.startsWith('processing') ?? false,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
// active.json 不存在时(旧数据,未经 daemon 持久化),回退到目录推断。
|
|
875
|
+
// findMainSessionEntry 用目录存在性 + messages.jsonl 第一行推断 chatType。
|
|
876
|
+
const entry = ThreadService.findMainSessionEntry(selfAid, normalized);
|
|
877
|
+
if (!entry)
|
|
752
878
|
return undefined;
|
|
753
879
|
return {
|
|
754
|
-
threadId:
|
|
755
|
-
sessionId:
|
|
880
|
+
threadId: entry.threadId,
|
|
881
|
+
sessionId: entry.sessionId,
|
|
756
882
|
peer: normalized,
|
|
757
883
|
channelType: 'aun',
|
|
758
|
-
peerKind:
|
|
759
|
-
name:
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
884
|
+
peerKind: entry.peerKind,
|
|
885
|
+
name: entry.name,
|
|
886
|
+
nameUpdatedAt: undefined,
|
|
887
|
+
label: null,
|
|
888
|
+
topicStatus: 'pending',
|
|
889
|
+
createdAt: entry.createdAt,
|
|
890
|
+
lastActiveAt: entry.lastActiveAt,
|
|
891
|
+
running: false,
|
|
765
892
|
};
|
|
766
893
|
}
|
|
767
894
|
// ── touch (update lastActiveAt) ──
|
|
@@ -783,7 +910,7 @@ export class ThreadService {
|
|
|
783
910
|
* 禁止删除:busy(processingState 有值)的话题,以及 threadId 为空的主会话。
|
|
784
911
|
*/
|
|
785
912
|
async deleteThread(selfAid, peer, threadId) {
|
|
786
|
-
if (
|
|
913
|
+
if (isMainSessionThreadId(threadId))
|
|
787
914
|
throw new ThreadServiceError('不能删除主会话', 'NOT_A_TOPIC');
|
|
788
915
|
const want = normalizeThreadPeer(peer);
|
|
789
916
|
const indexPath = agentThreadIndexPath(selfAid);
|
|
@@ -865,7 +992,71 @@ export class ThreadService {
|
|
|
865
992
|
const index = readIndex(agentThreadIndexPath(selfAid));
|
|
866
993
|
return index.threads.find(t => t.threadId === threadId);
|
|
867
994
|
}
|
|
868
|
-
|
|
995
|
+
/**
|
|
996
|
+
* 同步读取主会话(threadId=null/空/"main")的等价 ThreadIndexEntry。
|
|
997
|
+
* 主会话不写入 threads 索引,需直读 active.json 构造。
|
|
998
|
+
* 供 resolveThreadRouteTarget 跨话题路由校验时使用。
|
|
999
|
+
*/
|
|
1000
|
+
static findMainSessionEntry(selfAid, peer) {
|
|
1001
|
+
const normalizedPeer = normalizeThreadPeer(peer) ?? peer;
|
|
1002
|
+
const channelKey = `aun#${selfAid}#main`;
|
|
1003
|
+
// chatDirPath 期望的是 `<root>/agents` 这一级,它会自己拼 `<aid>/sessions/...`;
|
|
1004
|
+
// 传 agentSessionsDir 会得到 `agents/<aid>/sessions/<aid>/sessions/...` 的双层路径。
|
|
1005
|
+
const agentsRoot = path.dirname(agentDir(selfAid));
|
|
1006
|
+
const dir = chatDirPath(agentsRoot, 'aun', normalizedPeer, selfAid, channelKey);
|
|
1007
|
+
// 先尝试 active.json(热缓存,有则最准确)。
|
|
1008
|
+
let raw;
|
|
1009
|
+
try {
|
|
1010
|
+
raw = JSON.parse(fs.readFileSync(`${dir}/active.json`, 'utf8'));
|
|
1011
|
+
// active.json 里 threadId 是话题 id 说明该目录放的不是主会话,丢弃。
|
|
1012
|
+
if (isTopicThreadId(raw['threadId']))
|
|
1013
|
+
return undefined;
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
raw = undefined;
|
|
1017
|
+
}
|
|
1018
|
+
// active.json 不存在(旧数据或未触发持久化)时,用目录本身推断:
|
|
1019
|
+
// 目录存在说明主会话存在,从 messages.jsonl 第一行读 chatType 补齐 peerKind。
|
|
1020
|
+
if (!raw) {
|
|
1021
|
+
if (!fs.existsSync(dir))
|
|
1022
|
+
return undefined;
|
|
1023
|
+
let chatType;
|
|
1024
|
+
try {
|
|
1025
|
+
const firstLine = fs.readFileSync(`${dir}/messages.jsonl`, 'utf8').split('\n')[0];
|
|
1026
|
+
if (firstLine?.trim()) {
|
|
1027
|
+
const msg = JSON.parse(firstLine);
|
|
1028
|
+
chatType = typeof msg['chatType'] === 'string' ? msg['chatType'] : undefined;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
catch { /* 读取失败时留 undefined,下面用默认值 */ }
|
|
1032
|
+
const peerKind = chatType === 'group' ? 'group' : 'private';
|
|
1033
|
+
return {
|
|
1034
|
+
threadId: safeFormatMainTopicId(selfAid, normalizedPeer) ?? '',
|
|
1035
|
+
sessionId: '',
|
|
1036
|
+
peer: normalizedPeer,
|
|
1037
|
+
channelType: 'aun',
|
|
1038
|
+
peerKind,
|
|
1039
|
+
name: normalizedPeer,
|
|
1040
|
+
topicStatus: 'pending',
|
|
1041
|
+
createdAt: 0,
|
|
1042
|
+
lastActiveAt: 0,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
const chatType = raw['chatType'];
|
|
1046
|
+
const peerKind = chatType === 'group' ? 'group' : 'private';
|
|
1047
|
+
return {
|
|
1048
|
+
threadId: safeFormatMainTopicId(selfAid, normalizedPeer) ?? '',
|
|
1049
|
+
sessionId: String(raw['id'] ?? raw['agentSessionId'] ?? ''),
|
|
1050
|
+
peer: normalizedPeer,
|
|
1051
|
+
channelType: 'aun',
|
|
1052
|
+
peerKind,
|
|
1053
|
+
name: String(raw['name'] ?? normalizedPeer),
|
|
1054
|
+
topicStatus: raw['topicStatus'] ?? 'pending',
|
|
1055
|
+
createdAt: Number(raw['createdAt'] ?? 0),
|
|
1056
|
+
lastActiveAt: Number(raw['updatedAt'] ?? 0),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
// ── Thread Context inbox(跨话题上下文的提醒缓存) ──
|
|
869
1060
|
/** 跨话题发送成功后写入本端 inbox。 */
|
|
870
1061
|
static appendInbox(selfAid, entry) {
|
|
871
1062
|
new ThreadContextStorage().append(selfAid, entry);
|
|
@@ -929,7 +1120,7 @@ export class ThreadService {
|
|
|
929
1120
|
if (!session)
|
|
930
1121
|
return undefined;
|
|
931
1122
|
peer = session.channelId;
|
|
932
|
-
threadId = session
|
|
1123
|
+
threadId = sessionTopicThreadId(session) ?? '';
|
|
933
1124
|
}
|
|
934
1125
|
if (!peer)
|
|
935
1126
|
return undefined;
|