evolclaw 3.1.4 → 3.1.6

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/agents/claude-runner.js +398 -161
  3. package/dist/agents/kit-renderer.js +191 -25
  4. package/dist/aun/aid/agentmd.js +75 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/group.js +2 -2
  10. package/dist/aun/msg/p2p.js +26 -2
  11. package/dist/aun/rpc/connection.js +23 -30
  12. package/dist/channels/aun.js +174 -99
  13. package/dist/channels/dingtalk.js +2 -1
  14. package/dist/channels/feishu.js +301 -199
  15. package/dist/channels/qqbot.js +2 -1
  16. package/dist/channels/wechat.js +2 -1
  17. package/dist/channels/wecom.js +2 -1
  18. package/dist/cli/agent.js +21 -16
  19. package/dist/cli/bench.js +41 -28
  20. package/dist/cli/help.js +8 -0
  21. package/dist/cli/index.js +176 -87
  22. package/dist/cli/init-channel.js +5 -1
  23. package/dist/cli/init.js +37 -21
  24. package/dist/cli/link-rules.js +1 -7
  25. package/dist/cli/model.js +549 -0
  26. package/dist/cli/net-check.js +133 -50
  27. package/dist/cli/watch-msg.js +7 -7
  28. package/dist/cli/watch-web/debug-log.js +18 -0
  29. package/dist/cli/watch-web/server.js +306 -0
  30. package/dist/cli/watch-web/sources/aid.js +63 -0
  31. package/dist/cli/watch-web/sources/msg.js +70 -0
  32. package/dist/cli/watch-web/sources/session.js +638 -0
  33. package/dist/cli/watch-web/sources/types.js +10 -0
  34. package/dist/cli/watch-web/static/app.js +546 -0
  35. package/dist/cli/watch-web/static/index.html +54 -0
  36. package/dist/cli/watch-web/static/style.css +247 -0
  37. package/dist/config-store.js +1 -22
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +261 -133
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -22
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/im-renderer.js +9 -20
  44. package/dist/core/message/message-bridge.js +13 -9
  45. package/dist/core/message/message-log.js +2 -2
  46. package/dist/core/message/message-processor.js +211 -123
  47. package/dist/core/message/stream-idle-monitor.js +21 -0
  48. package/dist/core/model/model-catalog.js +215 -0
  49. package/dist/core/model/model-scope.js +250 -0
  50. package/dist/core/relation/peer-identity.js +58 -55
  51. package/dist/core/relation/peer-key.js +16 -0
  52. package/dist/core/session/session-fs-store.js +34 -55
  53. package/dist/core/session/session-key.js +24 -0
  54. package/dist/core/session/session-manager.js +308 -251
  55. package/dist/core/session/session-mapper.js +9 -4
  56. package/dist/core/trigger/manager.js +3 -3
  57. package/dist/core/trigger/parser.js +4 -4
  58. package/dist/core/trigger/scheduler.js +22 -7
  59. package/dist/index.js +61 -7
  60. package/dist/ipc.js +23 -1
  61. package/dist/utils/error-utils.js +6 -0
  62. package/dist/utils/process-introspect.js +7 -5
  63. package/kits/docs/GUIDE.md +2 -2
  64. package/kits/docs/INDEX.md +8 -8
  65. package/kits/docs/channels/aun.md +56 -17
  66. package/kits/docs/channels/feishu.md +41 -12
  67. package/kits/docs/context-assembly.md +182 -0
  68. package/kits/docs/evolclaw/INDEX.md +43 -0
  69. package/kits/docs/evolclaw/agent.md +49 -0
  70. package/kits/docs/evolclaw/aid.md +49 -0
  71. package/kits/docs/evolclaw/ctl.md +46 -0
  72. package/kits/docs/evolclaw/group.md +89 -0
  73. package/kits/docs/evolclaw/model.md +51 -0
  74. package/kits/docs/evolclaw/msg.md +91 -0
  75. package/kits/docs/evolclaw/rpc.md +35 -0
  76. package/kits/docs/evolclaw/storage.md +49 -0
  77. package/kits/docs/venues/aun-group.md +10 -0
  78. package/kits/docs/venues/aun-private.md +10 -0
  79. package/kits/docs/venues/client-desktop.md +10 -0
  80. package/kits/docs/venues/client-mobile.md +10 -0
  81. package/kits/docs/venues/feishu-group.md +13 -0
  82. package/kits/docs/venues/feishu-private.md +9 -0
  83. package/kits/docs/venues/group.md +23 -0
  84. package/kits/docs/venues/private.md +10 -0
  85. package/kits/eck_manifest.json +81 -36
  86. package/kits/rules/01-overview.md +20 -10
  87. package/kits/rules/06-channel.md +34 -27
  88. package/kits/templates/system-fragments/baseagent.md +7 -1
  89. package/kits/templates/system-fragments/channel.md +7 -5
  90. package/kits/templates/system-fragments/commands.md +19 -0
  91. package/kits/templates/system-fragments/session.md +19 -3
  92. package/kits/templates/system-fragments/venue.md +24 -0
  93. package/package.json +10 -5
  94. package/dist/aun/aid/lifecycle-log.js +0 -33
  95. package/dist/utils/aid-lifecycle-log.js +0 -33
  96. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  97. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  98. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  99. package/kits/docs/evolclaw/tools.md +0 -25
@@ -6,7 +6,7 @@
6
6
  * 2. 仅在 agent.md 内容变化时重写 peer-identity.json
7
7
  * 3. 支持入站和出站消息的身份查询
8
8
  *
9
- * 信源:对端的 agent.md(通过 AUN SDK checkAgentMd + fetchAgentMd)
9
+ * 信源:对端的 agent.md(通过 AIDStore.checkAgentMd + downloadAgentMd,由 agentmdSync 封装)
10
10
  * 判定规则:type !== 'human' → agent
11
11
  * 缓存位置:$AGENT_DIR/relations/<channel>#<urlEncode(peerId)>/peer-identity.json
12
12
  */
@@ -15,6 +15,7 @@ import * as path from 'path';
15
15
  import * as crypto from 'crypto';
16
16
  import { logger } from '../../utils/logger.js';
17
17
  import { agentMdPath } from '../../paths.js';
18
+ import { formatPeerKey } from './peer-key.js';
18
19
  /**
19
20
  * 对端身份缓存管理器
20
21
  */
@@ -24,16 +25,16 @@ export class PeerIdentityCache {
24
25
  /**
25
26
  * 获取 peer-identity.json 文件路径
26
27
  */
27
- static getFilePath(channel, peerId, agentDir) {
28
- const peerKey = `${channel}#${encodeURIComponent(peerId)}`;
28
+ static getFilePath(channelType, peerId, agentDir) {
29
+ const peerKey = formatPeerKey(channelType, peerId);
29
30
  return path.join(agentDir, 'relations', peerKey, 'peer-identity.json');
30
31
  }
31
32
  /**
32
33
  * 从文件读取缓存
33
34
  * @returns PeerIdentity | null(缓存不存在)
34
35
  */
35
- static get(channel, peerId, agentDir) {
36
- const filePath = this.getFilePath(channel, peerId, agentDir);
36
+ static get(channelType, peerId, agentDir) {
37
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
37
38
  try {
38
39
  const content = fs.readFileSync(filePath, 'utf-8');
39
40
  return JSON.parse(content);
@@ -47,17 +48,19 @@ export class PeerIdentityCache {
47
48
  * @param maxAgeMs 最大缓存时间(默认 30 天)
48
49
  * @returns true=需要刷新
49
50
  */
50
- static needsRefresh(channel, peerId, agentDir, maxAgeMs = this.CACHE_MAX_AGE_MS) {
51
- const cached = this.get(channel, peerId, agentDir);
51
+ static needsRefresh(channelType, peerId, agentDir, maxAgeMs = this.CACHE_MAX_AGE_MS) {
52
+ const cached = this.get(channelType, peerId, agentDir);
52
53
  if (!cached)
53
54
  return true;
54
55
  return Date.now() - cached.lastCheckedAt > maxAgeMs;
55
56
  }
56
57
  /**
57
58
  * 从 agent.md 更新身份信息
59
+ * @param source 'agentmd'(验签通过)或 'agentmd-unverified'(内容可解析但验签未过)
60
+ * @param verifiedAt 验签通过时间戳;未验签传 0
58
61
  */
59
- static updateFromAgentMd(channel, peerId, agentDir, agentMd, verifiedAt) {
60
- const typeMatch = agentMd.match(/^type:\s*["']?(\w+)["']?/m);
62
+ static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt, source = 'agentmd') {
63
+ const typeMatch = agentMd.match(/^type:\s*["']?([^"'\n]+?)["']?\s*$/m);
61
64
  const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
62
65
  const type = typeMatch?.[1] || 'unknown';
63
66
  const isAgent = type !== 'human';
@@ -73,13 +76,13 @@ export class PeerIdentityCache {
73
76
  agentMdUpdatedAt: now,
74
77
  verifiedAt,
75
78
  lastCheckedAt: now,
76
- source: 'agentmd',
79
+ source,
77
80
  };
78
- const filePath = this.getFilePath(channel, peerId, agentDir);
81
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
79
82
  try {
80
83
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
81
84
  fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
82
- logger.debug(`[PeerIdentityCache] Updated: ${channel}#${peerId} type=${type} isAgent=${isAgent}`);
85
+ logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent} source=${source}`);
83
86
  }
84
87
  catch (err) {
85
88
  logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
@@ -89,9 +92,9 @@ export class PeerIdentityCache {
89
92
  /**
90
93
  * 仅更新 lastCheckedAt(内容未变时的轻量操作)
91
94
  */
92
- static touchLastChecked(channel, peerId, agentDir, cached) {
95
+ static touchLastChecked(channelType, peerId, agentDir, cached) {
93
96
  const updated = { ...cached, lastCheckedAt: Date.now() };
94
- const filePath = this.getFilePath(channel, peerId, agentDir);
97
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
95
98
  try {
96
99
  fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), 'utf-8');
97
100
  }
@@ -101,7 +104,7 @@ export class PeerIdentityCache {
101
104
  /**
102
105
  * 标记为 unknown(验签失败或无 agent.md)
103
106
  */
104
- static markUnknown(channel, peerId, agentDir) {
107
+ static markUnknown(channelType, peerId, agentDir) {
105
108
  const identity = {
106
109
  aid: peerId,
107
110
  type: 'unknown',
@@ -112,11 +115,11 @@ export class PeerIdentityCache {
112
115
  lastCheckedAt: Date.now(),
113
116
  source: 'unknown',
114
117
  };
115
- const filePath = this.getFilePath(channel, peerId, agentDir);
118
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
116
119
  try {
117
120
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
118
121
  fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
119
- logger.debug(`[PeerIdentityCache] Marked unknown: ${channel}#${peerId}`);
122
+ logger.debug(`[PeerIdentityCache] Marked unknown: ${channelType}#${peerId}`);
120
123
  }
121
124
  catch (err) {
122
125
  logger.warn(`[PeerIdentityCache] Failed to write unknown cache: ${filePath} err=${err}`);
@@ -126,76 +129,76 @@ export class PeerIdentityCache {
126
129
  /**
127
130
  * 完整流程:缓存检查 → agentmdSync(check+fetch)→ 按 changed 决定是否重写
128
131
  *
129
- * @param channel 渠道类型(如 'aun')
132
+ * @param channelType 渠道类型(如 'aun')
130
133
  * @param peerId 对端 ID(AUN 是 AID)
131
134
  * @param agentDir agent 数据根目录
132
- * @param aunClient AUN SDK client(需要有 checkAgentMd / fetchAgentMd 方法)
135
+ * @param store AIDStore 实例(由调用方提供,负责 checkAgentMd + downloadAgentMd)
133
136
  * @param forceRefresh 强制刷新(忽略缓存时效)
134
- * @returns PeerIdentity
135
137
  */
136
- static async resolve(channel, peerId, agentDir, aunClient, forceRefresh = false) {
138
+ static async resolve(channelType, peerId, agentDir, store, forceRefresh = false) {
137
139
  // 1. 缓存检查
138
- if (!forceRefresh && !this.needsRefresh(channel, peerId, agentDir)) {
139
- const cached = this.get(channel, peerId, agentDir);
140
+ if (!forceRefresh && !this.needsRefresh(channelType, peerId, agentDir)) {
141
+ const cached = this.get(channelType, peerId, agentDir);
140
142
  if (cached) {
141
- logger.debug(`[PeerIdentityCache] Cache hit: ${channel}#${peerId} type=${cached.type} age=${Math.floor((Date.now() - cached.lastCheckedAt) / 1000 / 60 / 60 / 24)}d`);
143
+ logger.debug(`[PeerIdentityCache] Cache hit: ${channelType}#${peerId} type=${cached.type} age=${Math.floor((Date.now() - cached.lastCheckedAt) / 1000 / 60 / 60 / 24)}d`);
142
144
  return cached;
143
145
  }
144
146
  }
145
- // 2. 标准流程:checkAgentMd → fetchAgentMd(如果有变化)
147
+ // 2. 通过 agentmdSync 拉取(内部走 store.checkAgentMd → store.downloadAgentMd)
146
148
  try {
147
- logger.debug(`[PeerIdentityCache] Syncing agent.md: ${channel}#${peerId}`);
148
- const state = await aunClient.checkAgentMd(peerId, 30);
149
- let content;
150
- if (state.in_sync && state.local_found) {
151
- // 本地已是最新,读本地文件
152
- const localPath = agentMdPath(peerId);
153
- try {
154
- content = fs.readFileSync(localPath, 'utf-8');
155
- }
156
- catch { /* ignore */ }
157
- }
149
+ logger.debug(`[PeerIdentityCache] Syncing agent.md: ${channelType}#${peerId}`);
150
+ const { agentmdSync } = await import('../../aun/aid/agentmd.js');
151
+ const result = await agentmdSync(peerId, { store });
152
+ const content = result.content;
158
153
  if (!content) {
159
- // 需要下载(不同步或本地不存在)
160
- const info = await aunClient.fetchAgentMd(peerId);
161
- content = info.content;
154
+ throw new Error('agent.md content unavailable');
155
+ }
156
+ // 验签通过 → 可信(source=agentmd);否则 type 仍解析但标记未验证
157
+ const verified = result.verification?.status === 'verified';
158
+ const source = verified ? 'agentmd' : 'agentmd-unverified';
159
+ if (!verified) {
160
+ logger.info(`[PeerIdentityCache] agent.md unverified for ${peerId}: status=${result.verification?.status ?? 'unknown'} reason=${result.verification?.reason ?? '-'} (type 仍按声明解析)`);
162
161
  }
163
- // 3. 比较 hash,仅在变化时重写 peer-identity.json
162
+ // 3. 比较 hash,内容与可信级别都未变时仅 touch
164
163
  const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
165
- const cached = this.get(channel, peerId, agentDir);
166
- if (cached && cached.agentMdHash === newHash && cached.source === 'agentmd') {
167
- return this.touchLastChecked(channel, peerId, agentDir, cached);
164
+ const cached = this.get(channelType, peerId, agentDir);
165
+ if (cached && cached.agentMdHash === newHash && cached.source === source) {
166
+ return this.touchLastChecked(channelType, peerId, agentDir, cached);
168
167
  }
169
- return this.updateFromAgentMd(channel, peerId, agentDir, content, Date.now());
168
+ return this.updateFromAgentMd(channelType, peerId, agentDir, content, verified ? Date.now() : 0, source);
170
169
  }
171
170
  catch (err) {
172
- // 4. 网络失败,fallback 本地文件
171
+ // 4. agentmdSync 抛错(非网络失败——网络失败时它内部已 fallback 返回本地内容;
172
+ // 这里通常是无内容或解析异常)。兜底读本地文件,但无法重新验签,
173
+ // 故沿用缓存里已有的可信级别,绝不凭空升级为已验签。
173
174
  const localPath = agentMdPath(peerId);
174
175
  try {
175
176
  if (fs.existsSync(localPath)) {
176
177
  const localContent = fs.readFileSync(localPath, 'utf-8');
177
- logger.info(`[PeerIdentityCache] Network failed, using local agent.md for ${peerId}`);
178
+ const cached = this.get(channelType, peerId, agentDir);
179
+ logger.info(`[PeerIdentityCache] Using local agent.md for ${peerId} (cached source=${cached?.source ?? 'none'})`);
178
180
  const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
179
- const cached = this.get(channel, peerId, agentDir);
180
- if (cached && cached.agentMdHash === localHash && cached.source === 'agentmd') {
181
- return this.touchLastChecked(channel, peerId, agentDir, cached);
181
+ if (cached && cached.agentMdHash === localHash && (cached.source === 'agentmd' || cached.source === 'agentmd-unverified')) {
182
+ return this.touchLastChecked(channelType, peerId, agentDir, cached);
182
183
  }
183
- return this.updateFromAgentMd(channel, peerId, agentDir, localContent, cached?.verifiedAt ?? 0);
184
+ // 无匹配缓存可信级别 本地内容未经本次验签,标记为未验证
185
+ const fallbackSource = cached?.source === 'agentmd' ? 'agentmd' : 'agentmd-unverified';
186
+ return this.updateFromAgentMd(channelType, peerId, agentDir, localContent, cached?.verifiedAt ?? 0, fallbackSource);
184
187
  }
185
188
  }
186
189
  catch { /* ignore fs errors */ }
187
- logger.warn(`[PeerIdentityCache] Failed to resolve: ${channel}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
188
- return this.markUnknown(channel, peerId, agentDir);
190
+ logger.warn(`[PeerIdentityCache] Failed to resolve: ${channelType}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
191
+ return this.markUnknown(channelType, peerId, agentDir);
189
192
  }
190
193
  }
191
194
  /**
192
195
  * 清除指定对端的缓存
193
196
  */
194
- static clear(channel, peerId, agentDir) {
195
- const filePath = this.getFilePath(channel, peerId, agentDir);
197
+ static clear(channelType, peerId, agentDir) {
198
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
196
199
  try {
197
200
  fs.unlinkSync(filePath);
198
- logger.debug(`[PeerIdentityCache] Cleared: ${channel}#${peerId}`);
201
+ logger.debug(`[PeerIdentityCache] Cleared: ${channelType}#${peerId}`);
199
202
  }
200
203
  catch {
201
204
  // 文件不存在,忽略
@@ -0,0 +1,16 @@
1
+ /**
2
+ * peerKey: 关系层路由键,格式 `<channelType>#<urlEncode(channelId)>`。
3
+ * 群聊场景下 channelId = groupId,所有发言者共用同一个 peerKey。
4
+ */
5
+ export function formatPeerKey(channelType, channelId) {
6
+ return `${channelType}#${encodeURIComponent(channelId)}`;
7
+ }
8
+ export function parsePeerKey(key) {
9
+ const idx = key.indexOf('#');
10
+ if (idx <= 0)
11
+ throw new Error(`Invalid peer key: ${key}`);
12
+ return {
13
+ channelType: key.slice(0, idx),
14
+ channelId: decodeURIComponent(key.slice(idx + 1)),
15
+ };
16
+ }
@@ -12,19 +12,16 @@ function decodeSegment(s) {
12
12
  }
13
13
  /**
14
14
  * 计算 chat 目录的完整路径。
15
- * - AUN:sessionsDir/aun/<urlEncode(selfId)>/<urlEncode(channelId)>/
16
- * - 其它:sessionsDir/<channelType>/<urlEncode(channelId)>/
17
- *
18
- * 注:channelType 自身不编码(限定枚举值,不含非法字符)。
15
+ * - aun: sessionsDir/aun/<urlEncode(selfAID|'_unknown')>/<urlEncode(channelId)>/
16
+ * - 其它: sessionsDir/<channelType>/<urlEncode(channelId)>/
19
17
  */
20
- export function chatDirPath(sessionsDir, channelType, channelId, selfId) {
18
+ export function chatDirPath(sessionsDir, channelType, channelId, selfAID) {
21
19
  if (channelType === 'aun') {
22
- const self = selfId || '_unknown';
23
- return path.join(sessionsDir, 'aun', encodeSegment(self), encodeSegment(channelId));
20
+ return path.join(sessionsDir, channelType, encodeSegment(selfAID || '_unknown'), encodeSegment(channelId));
24
21
  }
25
22
  return path.join(sessionsDir, channelType, encodeSegment(channelId));
26
23
  }
27
- /** 解码目录段(用于扫描时把目录名还原为原始 channelId/selfId) */
24
+ /** 解码目录段(用于扫描时把目录名还原为原始 channelId/selfAID) */
28
25
  export function decodeDirSegment(seg) {
29
26
  return decodeSegment(seg);
30
27
  }
@@ -128,8 +125,8 @@ export function readAllJsonlLines(filePath) {
128
125
  }
129
126
  /**
130
127
  * 扫描所有 chat 目录。
131
- * 顶层是 channelType 目录;aun 下面再有一层 selfId 目录。
132
- * 返回每个 chat 的:channelType、selfId(仅 aun 有)、channelId、dirPath。
128
+ * - aun: channelType / selfAID / channelId (3层)
129
+ * - 其它: channelType / channelId (2层)
133
130
  */
134
131
  export function scanChatDirs(sessionsDir) {
135
132
  const results = [];
@@ -146,40 +143,17 @@ export function scanChatDirs(sessionsDir) {
146
143
  if (!typeEntry.isDirectory())
147
144
  continue;
148
145
  const channelType = typeEntry.name;
149
- // 包含 '#' 的目录是旧 channelKey 格式(如 'aun#dddd.agentid.pub#main'),
150
- // 按通用 channel 布局扫描(sessionsDir/{channelKey}/{encodedChannelId}/),保持兼容
151
- if (channelType.includes('#')) {
152
- const typeDir = path.join(sessionsDir, channelType);
153
- let chatEntries;
154
- try {
155
- chatEntries = fs.readdirSync(typeDir, { withFileTypes: true });
156
- }
157
- catch {
158
- continue;
159
- }
160
- for (const chatEntry of chatEntries) {
161
- if (!chatEntry.isDirectory())
162
- continue;
163
- results.push({
164
- channelType,
165
- selfId: null,
166
- channelId: decodeSegment(chatEntry.name),
167
- dirPath: path.join(typeDir, chatEntry.name),
168
- });
169
- }
146
+ const typeDir = path.join(sessionsDir, channelType);
147
+ let level2Entries;
148
+ try {
149
+ level2Entries = fs.readdirSync(typeDir, { withFileTypes: true });
150
+ }
151
+ catch {
170
152
  continue;
171
153
  }
172
- const typeDir = path.join(sessionsDir, channelType);
173
154
  if (channelType === 'aun') {
174
- // aun 下还有一层 selfId
175
- let selfEntries;
176
- try {
177
- selfEntries = fs.readdirSync(typeDir, { withFileTypes: true });
178
- }
179
- catch {
180
- continue;
181
- }
182
- for (const selfEntry of selfEntries) {
155
+ // 3-layer: aun/selfAID/channelId
156
+ for (const selfEntry of level2Entries) {
183
157
  if (!selfEntry.isDirectory())
184
158
  continue;
185
159
  const selfDir = path.join(typeDir, selfEntry.name);
@@ -195,7 +169,7 @@ export function scanChatDirs(sessionsDir) {
195
169
  continue;
196
170
  results.push({
197
171
  channelType,
198
- selfId: decodeSegment(selfEntry.name),
172
+ selfAID: decodeSegment(selfEntry.name),
199
173
  channelId: decodeSegment(chatEntry.name),
200
174
  dirPath: path.join(selfDir, chatEntry.name),
201
175
  });
@@ -203,20 +177,13 @@ export function scanChatDirs(sessionsDir) {
203
177
  }
204
178
  }
205
179
  else {
206
- // 通用 channel:sessionsDir/{channelType}/{encodedChannelId}/
207
- let chatEntries;
208
- try {
209
- chatEntries = fs.readdirSync(typeDir, { withFileTypes: true });
210
- }
211
- catch {
212
- continue;
213
- }
214
- for (const chatEntry of chatEntries) {
180
+ // 2-layer: channelType/channelId
181
+ for (const chatEntry of level2Entries) {
215
182
  if (!chatEntry.isDirectory())
216
183
  continue;
217
184
  results.push({
218
185
  channelType,
219
- selfId: null,
186
+ selfAID: '',
220
187
  channelId: decodeSegment(chatEntry.name),
221
188
  dirPath: path.join(typeDir, chatEntry.name),
222
189
  });
@@ -238,15 +205,27 @@ export function scanMetaFiles(chatDir) {
238
205
  throw e;
239
206
  }
240
207
  }
241
- export function ensureChatDir(sessionsDir, channelType, channelId, selfId) {
242
- const dir = chatDirPath(sessionsDir, channelType, channelId, selfId);
208
+ export function ensureChatDir(sessionsDir, channelType, channelId, selfAID) {
209
+ const dir = chatDirPath(sessionsDir, channelType, channelId, selfAID);
243
210
  fs.mkdirSync(dir, { recursive: true });
244
211
  fs.mkdirSync(path.join(dir, '_threads'), { recursive: true });
245
212
  fs.mkdirSync(path.join(dir, '_trash'), { recursive: true });
246
213
  return dir;
247
214
  }
248
215
  export function readThreadIndex(chatDir) {
249
- return readJsonFile(path.join(chatDir, '_threads', 'thread-index.json')) || {};
216
+ const raw = readJsonFile(path.join(chatDir, '_threads', 'thread-index.json')) || {};
217
+ // Migrate legacy format: { threadId: sessionId } → { threadId: { sessionId, sessionKey, metaFile } }
218
+ const result = {};
219
+ for (const [tid, val] of Object.entries(raw)) {
220
+ if (typeof val === 'string') {
221
+ // Legacy: val is sessionId
222
+ result[tid] = { sessionId: val, sessionKey: '', metaFile: `${val}.jsonl` };
223
+ }
224
+ else {
225
+ result[tid] = val;
226
+ }
227
+ }
228
+ return result;
250
229
  }
251
230
  export function writeThreadIndex(chatDir, index) {
252
231
  atomicWriteJson(path.join(chatDir, '_threads', 'thread-index.json'), index);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * sessionKey: agent 内部会话路由键。
3
+ * 格式: channelType#urlEncode(channelId)#urlEncode(threadId)
4
+ * 无话题时 threadId 固定为 'main'。
5
+ */
6
+ export const DEFAULT_THREAD_ID = 'main';
7
+ export function formatSessionKey(channelType, channelId, threadId) {
8
+ const tid = threadId || DEFAULT_THREAD_ID;
9
+ return `${channelType}#${encodeURIComponent(channelId)}#${encodeURIComponent(tid)}`;
10
+ }
11
+ export function parseSessionKey(key) {
12
+ const first = key.indexOf('#');
13
+ if (first <= 0)
14
+ throw new Error(`Invalid session key: ${key}`);
15
+ const rest = key.slice(first + 1);
16
+ const second = rest.indexOf('#');
17
+ if (second <= 0)
18
+ throw new Error(`Invalid session key (missing threadId): ${key}`);
19
+ return {
20
+ channelType: key.slice(0, first),
21
+ channelId: decodeURIComponent(rest.slice(0, second)),
22
+ threadId: decodeURIComponent(rest.slice(second + 1)),
23
+ };
24
+ }