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.
- package/CHANGELOG.md +60 -0
- package/dist/agents/claude-runner.js +398 -161
- package/dist/agents/kit-renderer.js +191 -25
- package/dist/aun/aid/agentmd.js +75 -103
- package/dist/aun/aid/client.js +1 -29
- package/dist/aun/aid/identity.js +105 -64
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/group.js +2 -2
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +174 -99
- package/dist/channels/dingtalk.js +2 -1
- package/dist/channels/feishu.js +301 -199
- package/dist/channels/qqbot.js +2 -1
- package/dist/channels/wechat.js +2 -1
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent.js +21 -16
- package/dist/cli/bench.js +41 -28
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +176 -87
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +549 -0
- package/dist/cli/net-check.js +133 -50
- package/dist/cli/watch-msg.js +7 -7
- package/dist/cli/watch-web/debug-log.js +18 -0
- package/dist/cli/watch-web/server.js +306 -0
- package/dist/cli/watch-web/sources/aid.js +63 -0
- package/dist/cli/watch-web/sources/msg.js +70 -0
- package/dist/cli/watch-web/sources/session.js +638 -0
- package/dist/cli/watch-web/sources/types.js +10 -0
- package/dist/cli/watch-web/static/app.js +546 -0
- package/dist/cli/watch-web/static/index.html +54 -0
- package/dist/cli/watch-web/static/style.css +247 -0
- package/dist/config-store.js +1 -22
- package/dist/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +261 -133
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -22
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +13 -9
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +211 -123
- package/dist/core/message/stream-idle-monitor.js +21 -0
- package/dist/core/model/model-catalog.js +215 -0
- package/dist/core/model/model-scope.js +250 -0
- package/dist/core/relation/peer-identity.js +58 -55
- package/dist/core/relation/peer-key.js +16 -0
- package/dist/core/session/session-fs-store.js +34 -55
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +308 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +3 -3
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +22 -7
- package/dist/index.js +61 -7
- package/dist/ipc.js +23 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +8 -8
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +182 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/agent.md +49 -0
- package/kits/docs/evolclaw/aid.md +49 -0
- package/kits/docs/evolclaw/ctl.md +46 -0
- package/kits/docs/evolclaw/group.md +89 -0
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +91 -0
- package/kits/docs/evolclaw/rpc.md +35 -0
- package/kits/docs/evolclaw/storage.md +49 -0
- package/kits/docs/venues/aun-group.md +10 -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 +23 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +81 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +34 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +7 -5
- package/kits/templates/system-fragments/commands.md +19 -0
- package/kits/templates/system-fragments/session.md +19 -3
- package/kits/templates/system-fragments/venue.md +24 -0
- package/package.json +10 -5
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
- package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
- package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
- 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(通过
|
|
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(
|
|
28
|
-
const peerKey =
|
|
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(
|
|
36
|
-
const filePath = this.getFilePath(
|
|
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(
|
|
51
|
-
const cached = this.get(
|
|
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(
|
|
60
|
-
const typeMatch = agentMd.match(/^type:\s*["']?(\
|
|
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
|
|
79
|
+
source,
|
|
77
80
|
};
|
|
78
|
-
const filePath = this.getFilePath(
|
|
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: ${
|
|
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(
|
|
95
|
+
static touchLastChecked(channelType, peerId, agentDir, cached) {
|
|
93
96
|
const updated = { ...cached, lastCheckedAt: Date.now() };
|
|
94
|
-
const filePath = this.getFilePath(
|
|
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(
|
|
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(
|
|
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: ${
|
|
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
|
|
132
|
+
* @param channelType 渠道类型(如 'aun')
|
|
130
133
|
* @param peerId 对端 ID(AUN 是 AID)
|
|
131
134
|
* @param agentDir agent 数据根目录
|
|
132
|
-
* @param
|
|
135
|
+
* @param store AIDStore 实例(由调用方提供,负责 checkAgentMd + downloadAgentMd)
|
|
133
136
|
* @param forceRefresh 强制刷新(忽略缓存时效)
|
|
134
|
-
* @returns PeerIdentity
|
|
135
137
|
*/
|
|
136
|
-
static async resolve(
|
|
138
|
+
static async resolve(channelType, peerId, agentDir, store, forceRefresh = false) {
|
|
137
139
|
// 1. 缓存检查
|
|
138
|
-
if (!forceRefresh && !this.needsRefresh(
|
|
139
|
-
const cached = this.get(
|
|
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: ${
|
|
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.
|
|
147
|
+
// 2. 通过 agentmdSync 拉取(内部走 store.checkAgentMd → store.downloadAgentMd)
|
|
146
148
|
try {
|
|
147
|
-
logger.debug(`[PeerIdentityCache] Syncing agent.md: ${
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
|
162
|
+
// 3. 比较 hash,内容与可信级别都未变时仅 touch
|
|
164
163
|
const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
165
|
-
const cached = this.get(
|
|
166
|
-
if (cached && cached.agentMdHash === newHash && cached.source ===
|
|
167
|
-
return this.touchLastChecked(
|
|
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(
|
|
168
|
+
return this.updateFromAgentMd(channelType, peerId, agentDir, content, verified ? Date.now() : 0, source);
|
|
170
169
|
}
|
|
171
170
|
catch (err) {
|
|
172
|
-
// 4.
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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: ${
|
|
188
|
-
return this.markUnknown(
|
|
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(
|
|
195
|
-
const filePath = this.getFilePath(
|
|
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: ${
|
|
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
|
-
* -
|
|
16
|
-
* -
|
|
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,
|
|
18
|
+
export function chatDirPath(sessionsDir, channelType, channelId, selfAID) {
|
|
21
19
|
if (channelType === 'aun') {
|
|
22
|
-
|
|
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/
|
|
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
|
-
*
|
|
132
|
-
*
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
207
|
-
|
|
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
|
-
|
|
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,
|
|
242
|
-
const dir = chatDirPath(sessionsDir, channelType, channelId,
|
|
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
|
-
|
|
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
|
+
}
|