evolclaw 3.1.3 → 3.1.5

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 (100) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/assets/.env.template +4 -0
  3. package/assets/config.json.template +6 -0
  4. package/assets/wechat-group-qr.jpeg +0 -0
  5. package/dist/agents/claude-runner.js +348 -156
  6. package/dist/agents/kit-renderer.js +211 -42
  7. package/dist/aun/aid/agentmd.js +75 -139
  8. package/dist/aun/aid/client.js +1 -14
  9. package/dist/aun/aid/identity.js +381 -54
  10. package/dist/aun/aid/index.js +3 -2
  11. package/dist/aun/aid/store.js +74 -0
  12. package/dist/aun/msg/p2p.js +26 -2
  13. package/dist/aun/rpc/connection.js +23 -35
  14. package/dist/channels/aun.js +92 -144
  15. package/dist/channels/dingtalk.js +1 -0
  16. package/dist/channels/feishu.js +270 -190
  17. package/dist/channels/qqbot.js +1 -0
  18. package/dist/channels/wechat.js +1 -0
  19. package/dist/channels/wecom.js +1 -0
  20. package/dist/cli/agent.js +26 -27
  21. package/dist/cli/bench.js +45 -34
  22. package/dist/cli/help.js +23 -0
  23. package/dist/cli/index.js +538 -77
  24. package/dist/cli/init-channel.js +7 -4
  25. package/dist/cli/link-rules.js +2 -1
  26. package/dist/cli/model.js +324 -0
  27. package/dist/cli/net-check.js +138 -56
  28. package/dist/cli/watch-msg.js +7 -7
  29. package/dist/cli/watch-web/debug-log.js +18 -0
  30. package/dist/cli/watch-web/server.js +306 -0
  31. package/dist/cli/watch-web/sources/aid.js +63 -0
  32. package/dist/cli/watch-web/sources/msg.js +70 -0
  33. package/dist/cli/watch-web/sources/session.js +638 -0
  34. package/dist/cli/watch-web/sources/types.js +10 -0
  35. package/dist/cli/watch-web/static/app.js +546 -0
  36. package/dist/cli/watch-web/static/index.html +54 -0
  37. package/dist/cli/watch-web/static/style.css +247 -0
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +87 -93
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -4
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/message-bridge.js +6 -6
  44. package/dist/core/message/message-log.js +2 -2
  45. package/dist/core/message/message-processor.js +104 -118
  46. package/dist/core/message/stream-idle-monitor.js +21 -0
  47. package/dist/core/model/model-catalog.js +215 -0
  48. package/dist/core/model/model-scope.js +250 -0
  49. package/dist/core/relation/peer-identity.js +78 -44
  50. package/dist/core/relation/peer-key.js +16 -0
  51. package/dist/core/session/session-fs-store.js +34 -55
  52. package/dist/core/session/session-key.js +24 -0
  53. package/dist/core/session/session-manager.js +312 -251
  54. package/dist/core/session/session-mapper.js +9 -4
  55. package/dist/core/trigger/manager.js +37 -0
  56. package/dist/core/trigger/scheduler.js +2 -1
  57. package/dist/index.js +10 -3
  58. package/dist/ipc.js +22 -0
  59. package/dist/paths.js +87 -16
  60. package/dist/utils/npm-ops.js +18 -11
  61. package/kits/docs/GUIDE.md +2 -2
  62. package/kits/docs/INDEX.md +11 -7
  63. package/kits/docs/channels/aun.md +56 -17
  64. package/kits/docs/channels/feishu.md +41 -12
  65. package/kits/docs/context-assembly.md +181 -0
  66. package/kits/docs/evolclaw/agent.md +49 -0
  67. package/kits/docs/evolclaw/aid.md +49 -0
  68. package/kits/docs/evolclaw/ctl.md +46 -0
  69. package/kits/docs/evolclaw/group.md +82 -0
  70. package/kits/docs/evolclaw/msg.md +86 -0
  71. package/kits/docs/evolclaw/rpc.md +35 -0
  72. package/kits/docs/evolclaw/storage.md +49 -0
  73. package/kits/docs/venues/aun-group.md +10 -0
  74. package/kits/docs/venues/aun-private.md +10 -0
  75. package/kits/docs/venues/client-desktop.md +10 -0
  76. package/kits/docs/venues/client-mobile.md +10 -0
  77. package/kits/docs/venues/feishu-group.md +13 -0
  78. package/kits/docs/venues/feishu-private.md +9 -0
  79. package/kits/docs/venues/group.md +11 -0
  80. package/kits/docs/venues/private.md +10 -0
  81. package/kits/eck_manifest.json +75 -39
  82. package/kits/rules/01-overview.md +20 -10
  83. package/kits/rules/05-venue.md +2 -2
  84. package/kits/rules/06-channel.md +30 -27
  85. package/kits/templates/system-fragments/baseagent.md +7 -1
  86. package/kits/templates/system-fragments/channel.md +4 -1
  87. package/kits/templates/system-fragments/identity.md +4 -4
  88. package/kits/templates/system-fragments/relation.md +8 -5
  89. package/kits/templates/system-fragments/session.md +27 -0
  90. package/kits/templates/system-fragments/venue.md +13 -1
  91. package/package.json +13 -6
  92. package/dist/aun/aid/lifecycle-log.js +0 -33
  93. package/dist/net-check.js +0 -640
  94. package/dist/utils/aid-lifecycle-log.js +0 -33
  95. package/dist/watch-msg.js +0 -544
  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
  100. package/kits/templates/system-fragments/eckruntime.md +0 -14
@@ -2,11 +2,11 @@
2
2
  * PeerIdentityCache - 对端身份缓存管理
3
3
  *
4
4
  * 职责:
5
- * 1. 从对端的 agent.md 确定身份(human / agent
6
- * 2. 缓存到关系层文件(30天时效)
5
+ * 1. 通过 agentmdSync 标准流程获取对端 agent.md(check fetch if changed
6
+ * 2. 仅在 agent.md 内容变化时重写 peer-identity.json
7
7
  * 3. 支持入站和出站消息的身份查询
8
8
  *
9
- * 信源:对端的 agent.md(通过 AUN SDK 下载并验签)
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
  */
@@ -14,6 +14,8 @@ import * as fs from 'fs';
14
14
  import * as path from 'path';
15
15
  import * as crypto from 'crypto';
16
16
  import { logger } from '../../utils/logger.js';
17
+ import { agentMdPath } from '../../paths.js';
18
+ import { formatPeerKey } from './peer-key.js';
17
19
  /**
18
20
  * 对端身份缓存管理器
19
21
  */
@@ -23,16 +25,16 @@ export class PeerIdentityCache {
23
25
  /**
24
26
  * 获取 peer-identity.json 文件路径
25
27
  */
26
- static getFilePath(channel, peerId, agentDir) {
27
- const peerKey = `${channel}#${encodeURIComponent(peerId)}`;
28
+ static getFilePath(channelType, peerId, agentDir) {
29
+ const peerKey = formatPeerKey(channelType, peerId);
28
30
  return path.join(agentDir, 'relations', peerKey, 'peer-identity.json');
29
31
  }
30
32
  /**
31
33
  * 从文件读取缓存
32
34
  * @returns PeerIdentity | null(缓存不存在)
33
35
  */
34
- static get(channel, peerId, agentDir) {
35
- const filePath = this.getFilePath(channel, peerId, agentDir);
36
+ static get(channelType, peerId, agentDir) {
37
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
36
38
  try {
37
39
  const content = fs.readFileSync(filePath, 'utf-8');
38
40
  return JSON.parse(content);
@@ -46,66 +48,76 @@ export class PeerIdentityCache {
46
48
  * @param maxAgeMs 最大缓存时间(默认 30 天)
47
49
  * @returns true=需要刷新
48
50
  */
49
- static needsRefresh(channel, peerId, agentDir, maxAgeMs = this.CACHE_MAX_AGE_MS) {
50
- 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);
51
53
  if (!cached)
52
54
  return true;
53
55
  return Date.now() - cached.lastCheckedAt > maxAgeMs;
54
56
  }
55
57
  /**
56
58
  * 从 agent.md 更新身份信息
57
- * @param agentMd 已验签的 agent.md 内容
58
59
  */
59
- static updateFromAgentMd(channel, peerId, agentDir, agentMd, verifiedAt) {
60
- // 解析 type 和 name
61
- const typeMatch = agentMd.match(/^type:\s*["']?(\w+)["']?/m);
60
+ static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt) {
61
+ const typeMatch = agentMd.match(/^type:\s*["']?([^"'\n]+?)["']?\s*$/m);
62
62
  const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
63
63
  const type = typeMatch?.[1] || 'unknown';
64
64
  const isAgent = type !== 'human';
65
65
  const name = nameMatch?.[1]?.trim();
66
- // 计算 hash
67
66
  const agentMdHash = 'sha256:' + crypto.createHash('sha256').update(agentMd, 'utf-8').digest('hex');
68
- // 构建身份信息
67
+ const now = Date.now();
69
68
  const identity = {
70
69
  aid: peerId,
71
70
  type,
72
71
  isAgent,
73
72
  name,
74
73
  agentMdHash,
74
+ agentMdUpdatedAt: now,
75
75
  verifiedAt,
76
- lastCheckedAt: Date.now(),
76
+ lastCheckedAt: now,
77
77
  source: 'agentmd',
78
78
  };
79
- // 写入文件
80
- const filePath = this.getFilePath(channel, peerId, agentDir);
79
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
81
80
  try {
82
81
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
83
82
  fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
84
- logger.debug(`[PeerIdentityCache] Updated: ${channel}#${peerId} type=${type} isAgent=${isAgent}`);
83
+ logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent}`);
85
84
  }
86
85
  catch (err) {
87
86
  logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
88
87
  }
89
88
  return identity;
90
89
  }
90
+ /**
91
+ * 仅更新 lastCheckedAt(内容未变时的轻量操作)
92
+ */
93
+ static touchLastChecked(channelType, peerId, agentDir, cached) {
94
+ const updated = { ...cached, lastCheckedAt: Date.now() };
95
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
96
+ try {
97
+ fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), 'utf-8');
98
+ }
99
+ catch { /* ignore */ }
100
+ return updated;
101
+ }
91
102
  /**
92
103
  * 标记为 unknown(验签失败或无 agent.md)
93
104
  */
94
- static markUnknown(channel, peerId, agentDir) {
105
+ static markUnknown(channelType, peerId, agentDir) {
95
106
  const identity = {
96
107
  aid: peerId,
97
108
  type: 'unknown',
98
- isAgent: true, // 验签失败 → 当做 agent(安全策略)
109
+ isAgent: true,
99
110
  agentMdHash: '',
111
+ agentMdUpdatedAt: 0,
100
112
  verifiedAt: 0,
101
113
  lastCheckedAt: Date.now(),
102
114
  source: 'unknown',
103
115
  };
104
- const filePath = this.getFilePath(channel, peerId, agentDir);
116
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
105
117
  try {
106
118
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
107
119
  fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
108
- logger.debug(`[PeerIdentityCache] Marked unknown: ${channel}#${peerId}`);
120
+ logger.debug(`[PeerIdentityCache] Marked unknown: ${channelType}#${peerId}`);
109
121
  }
110
122
  catch (err) {
111
123
  logger.warn(`[PeerIdentityCache] Failed to write unknown cache: ${filePath} err=${err}`);
@@ -113,46 +125,68 @@ export class PeerIdentityCache {
113
125
  return identity;
114
126
  }
115
127
  /**
116
- * 完整流程:检查缓存需要刷新则下载 agent.md 更新缓存
128
+ * 完整流程:缓存检查agentmdSync(check+fetch)→ changed 决定是否重写
117
129
  *
118
- * @param channel 渠道类型(如 'aun')
130
+ * @param channelType 渠道类型(如 'aun')
119
131
  * @param peerId 对端 ID(AUN 是 AID)
120
132
  * @param agentDir agent 数据根目录
121
- * @param aunClient AUN SDK client(需要有 fetchAgentMd 方法)
133
+ * @param store AIDStore 实例(由调用方提供,负责 checkAgentMd + downloadAgentMd)
122
134
  * @param forceRefresh 强制刷新(忽略缓存时效)
123
- * @returns PeerIdentity
124
135
  */
125
- static async resolve(channel, peerId, agentDir, aunClient, forceRefresh = false) {
126
- // 1. 检查缓存
127
- if (!forceRefresh && !this.needsRefresh(channel, peerId, agentDir)) {
128
- const cached = this.get(channel, peerId, agentDir);
136
+ static async resolve(channelType, peerId, agentDir, store, forceRefresh = false) {
137
+ // 1. 缓存检查
138
+ if (!forceRefresh && !this.needsRefresh(channelType, peerId, agentDir)) {
139
+ const cached = this.get(channelType, peerId, agentDir);
129
140
  if (cached) {
130
- logger.debug(`[PeerIdentityCache] Cache hit: ${channel}#${peerId} type=${cached.type} age=${Math.floor((Date.now() - cached.lastCheckedAt) / 1000 / 60 / 60 / 24)}d`);
141
+ logger.debug(`[PeerIdentityCache] Cache hit: ${channelType}#${peerId} type=${cached.type} age=${Math.floor((Date.now() - cached.lastCheckedAt) / 1000 / 60 / 60 / 24)}d`);
131
142
  return cached;
132
143
  }
133
144
  }
134
- // 2. 下载并验签 agent.md(SDK 自动验签)
145
+ // 2. 通过 agentmdSync 拉取(内部走 store.checkAgentMd → store.downloadAgentMd)
135
146
  try {
136
- logger.debug(`[PeerIdentityCache] Fetching agent.md: ${channel}#${peerId}`);
137
- const result = await aunClient.fetchAgentMd(peerId);
138
- const agentMd = result.content;
139
- // 3. 更新缓存
140
- return this.updateFromAgentMd(channel, peerId, agentDir, agentMd, Date.now());
147
+ logger.debug(`[PeerIdentityCache] Syncing agent.md: ${channelType}#${peerId}`);
148
+ const { agentmdSync } = await import('../../aun/aid/agentmd.js');
149
+ const result = await agentmdSync(peerId, { store });
150
+ const content = result.content;
151
+ if (!content) {
152
+ throw new Error('agent.md content unavailable');
153
+ }
154
+ // 3. 比较 hash,仅在变化时重写 peer-identity.json
155
+ const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
156
+ const cached = this.get(channelType, peerId, agentDir);
157
+ if (cached && cached.agentMdHash === newHash && cached.source === 'agentmd') {
158
+ return this.touchLastChecked(channelType, peerId, agentDir, cached);
159
+ }
160
+ return this.updateFromAgentMd(channelType, peerId, agentDir, content, Date.now());
141
161
  }
142
162
  catch (err) {
143
- // 验签失败或下载失败 标记为 unknown,当做 agent
144
- logger.warn(`[PeerIdentityCache] Failed to fetch agent.md: ${channel}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
145
- return this.markUnknown(channel, peerId, agentDir);
163
+ // 4. 网络失败,fallback 本地文件
164
+ const localPath = agentMdPath(peerId);
165
+ try {
166
+ if (fs.existsSync(localPath)) {
167
+ const localContent = fs.readFileSync(localPath, 'utf-8');
168
+ logger.info(`[PeerIdentityCache] Network failed, using local agent.md for ${peerId}`);
169
+ const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
170
+ const cached = this.get(channelType, peerId, agentDir);
171
+ if (cached && cached.agentMdHash === localHash && cached.source === 'agentmd') {
172
+ return this.touchLastChecked(channelType, peerId, agentDir, cached);
173
+ }
174
+ return this.updateFromAgentMd(channelType, peerId, agentDir, localContent, cached?.verifiedAt ?? 0);
175
+ }
176
+ }
177
+ catch { /* ignore fs errors */ }
178
+ logger.warn(`[PeerIdentityCache] Failed to resolve: ${channelType}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
179
+ return this.markUnknown(channelType, peerId, agentDir);
146
180
  }
147
181
  }
148
182
  /**
149
183
  * 清除指定对端的缓存
150
184
  */
151
- static clear(channel, peerId, agentDir) {
152
- const filePath = this.getFilePath(channel, peerId, agentDir);
185
+ static clear(channelType, peerId, agentDir) {
186
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
153
187
  try {
154
188
  fs.unlinkSync(filePath);
155
- logger.debug(`[PeerIdentityCache] Cleared: ${channel}#${peerId}`);
189
+ logger.debug(`[PeerIdentityCache] Cleared: ${channelType}#${peerId}`);
156
190
  }
157
191
  catch {
158
192
  // 文件不存在,忽略
@@ -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
+ }