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.
- package/CHANGELOG.md +27 -0
- package/assets/.env.template +4 -0
- package/assets/config.json.template +6 -0
- package/assets/wechat-group-qr.jpeg +0 -0
- package/dist/agents/claude-runner.js +348 -156
- package/dist/agents/kit-renderer.js +211 -42
- package/dist/aun/aid/agentmd.js +75 -139
- package/dist/aun/aid/client.js +1 -14
- package/dist/aun/aid/identity.js +381 -54
- package/dist/aun/aid/index.js +3 -2
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -35
- package/dist/channels/aun.js +92 -144
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +270 -190
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/agent.js +26 -27
- package/dist/cli/bench.js +45 -34
- package/dist/cli/help.js +23 -0
- package/dist/cli/index.js +538 -77
- package/dist/cli/init-channel.js +7 -4
- package/dist/cli/link-rules.js +2 -1
- package/dist/cli/model.js +324 -0
- package/dist/cli/net-check.js +138 -56
- 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/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +87 -93
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -4
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/message-bridge.js +6 -6
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +104 -118
- 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 +78 -44
- 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 +312 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +37 -0
- package/dist/core/trigger/scheduler.js +2 -1
- package/dist/index.js +10 -3
- package/dist/ipc.js +22 -0
- package/dist/paths.js +87 -16
- package/dist/utils/npm-ops.js +18 -11
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +11 -7
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +181 -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 +82 -0
- package/kits/docs/evolclaw/msg.md +86 -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 +11 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +75 -39
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/05-venue.md +2 -2
- package/kits/rules/06-channel.md +30 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +4 -1
- package/kits/templates/system-fragments/identity.md +4 -4
- package/kits/templates/system-fragments/relation.md +8 -5
- package/kits/templates/system-fragments/session.md +27 -0
- package/kits/templates/system-fragments/venue.md +13 -1
- package/package.json +13 -6
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/net-check.js +0 -640
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/dist/watch-msg.js +0 -544
- 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
- package/kits/templates/system-fragments/eckruntime.md +0 -14
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* PeerIdentityCache - 对端身份缓存管理
|
|
3
3
|
*
|
|
4
4
|
* 职责:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
5
|
+
* 1. 通过 agentmdSync 标准流程获取对端 agent.md(check → fetch if changed)
|
|
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
|
*/
|
|
@@ -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(
|
|
27
|
-
const peerKey =
|
|
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(
|
|
35
|
-
const filePath = this.getFilePath(
|
|
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(
|
|
50
|
-
const cached = this.get(
|
|
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(
|
|
60
|
-
|
|
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:
|
|
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: ${
|
|
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(
|
|
105
|
+
static markUnknown(channelType, peerId, agentDir) {
|
|
95
106
|
const identity = {
|
|
96
107
|
aid: peerId,
|
|
97
108
|
type: 'unknown',
|
|
98
|
-
isAgent: true,
|
|
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(
|
|
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: ${
|
|
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
|
-
*
|
|
128
|
+
* 完整流程:缓存检查 → agentmdSync(check+fetch)→ 按 changed 决定是否重写
|
|
117
129
|
*
|
|
118
|
-
* @param
|
|
130
|
+
* @param channelType 渠道类型(如 'aun')
|
|
119
131
|
* @param peerId 对端 ID(AUN 是 AID)
|
|
120
132
|
* @param agentDir agent 数据根目录
|
|
121
|
-
* @param
|
|
133
|
+
* @param store AIDStore 实例(由调用方提供,负责 checkAgentMd + downloadAgentMd)
|
|
122
134
|
* @param forceRefresh 强制刷新(忽略缓存时效)
|
|
123
|
-
* @returns PeerIdentity
|
|
124
135
|
*/
|
|
125
|
-
static async resolve(
|
|
126
|
-
// 1.
|
|
127
|
-
if (!forceRefresh && !this.needsRefresh(
|
|
128
|
-
const cached = this.get(
|
|
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: ${
|
|
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.
|
|
145
|
+
// 2. 通过 agentmdSync 拉取(内部走 store.checkAgentMd → store.downloadAgentMd)
|
|
135
146
|
try {
|
|
136
|
-
logger.debug(`[PeerIdentityCache]
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
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(
|
|
152
|
-
const filePath = this.getFilePath(
|
|
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: ${
|
|
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
|
-
* -
|
|
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
|
+
}
|