evolclaw 2.8.2 → 3.0.0
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/README.md +21 -12
- package/dist/agents/claude-runner.js +105 -30
- package/dist/agents/codex-runner.js +15 -7
- package/dist/agents/gemini-runner.js +14 -5
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1064 -279
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/baseagent-loader.js +48 -0
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +1090 -838
- package/dist/core/evolagent-registry.js +191 -360
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +326 -145
- package/dist/core/message/message-queue.js +5 -5
- package/dist/core/permission.js +21 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +704 -775
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/{templates → data}/prompts.md +34 -1
- package/dist/index.js +437 -273
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -576
- package/dist/core/agent-loader.js +0 -39
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { logger } from '../../utils/logger.js';
|
|
3
|
-
const DEFAULT_AGENT_NAME = '
|
|
3
|
+
const DEFAULT_AGENT_NAME = '<unknown>';
|
|
4
4
|
export class MessageQueue {
|
|
5
5
|
queues = new Map();
|
|
6
6
|
processing = new Set();
|
|
@@ -92,13 +92,13 @@ export class MessageQueue {
|
|
|
92
92
|
// 单聊:保留中断行为
|
|
93
93
|
logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt`);
|
|
94
94
|
this.eventBus?.publish({
|
|
95
|
-
type: '
|
|
95
|
+
type: 'task:interrupted',
|
|
96
96
|
sessionId: sessionKey,
|
|
97
97
|
reason: 'new_message',
|
|
98
98
|
agentName: this.processingAgent.get(queueKey),
|
|
99
99
|
});
|
|
100
100
|
if (this.interruptCallback) {
|
|
101
|
-
this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
|
|
101
|
+
this.interruptCallback(sessionKey, this.currentAgentId, this.processingAgent.get(queueKey)).catch(() => { });
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
@@ -276,13 +276,13 @@ export class MessageQueue {
|
|
|
276
276
|
const sessionKey = this.currentSessionKey.split('::')[0];
|
|
277
277
|
logger.info(`[Queue] Recalled active message ${messageId}, interrupting session ${sessionKey}`);
|
|
278
278
|
this.eventBus?.publish({
|
|
279
|
-
type: '
|
|
279
|
+
type: 'task:interrupted',
|
|
280
280
|
sessionId: sessionKey,
|
|
281
281
|
reason: 'recalled',
|
|
282
282
|
agentName: this.processingAgent.get(this.currentSessionKey),
|
|
283
283
|
});
|
|
284
284
|
if (this.interruptCallback) {
|
|
285
|
-
this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
|
|
285
|
+
this.interruptCallback(sessionKey, this.currentAgentId, this.processingAgent.get(this.currentSessionKey)).catch(() => { });
|
|
286
286
|
}
|
|
287
287
|
return true;
|
|
288
288
|
}
|
package/dist/core/permission.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import { renderActionAsText } from './interaction-router.js';
|
|
3
|
+
import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
|
|
2
4
|
// 危险命令黑名单(正则表达式)
|
|
3
5
|
const DANGEROUS_PATTERNS = [
|
|
4
6
|
// Unix
|
|
@@ -192,29 +194,40 @@ export class PermissionGateway {
|
|
|
192
194
|
},
|
|
193
195
|
channelId: context?.channelId || '',
|
|
194
196
|
sessionId,
|
|
197
|
+
initiatorId: context?.userId,
|
|
198
|
+
fallback: { command: 'perm' },
|
|
195
199
|
};
|
|
196
|
-
//
|
|
200
|
+
// 尝试富交互(走统一 adapter.send 入口)
|
|
197
201
|
let interactionSent = false;
|
|
198
|
-
if (context?.adapter
|
|
202
|
+
if (context?.adapter && context.channelId) {
|
|
199
203
|
try {
|
|
200
|
-
const
|
|
204
|
+
const envelope = buildEnvelope({
|
|
205
|
+
taskId: context.taskId,
|
|
206
|
+
channel: context.channel ?? context.adapter.channelName,
|
|
207
|
+
channelId: context.channelId,
|
|
208
|
+
agentName: context.agentName,
|
|
209
|
+
chatmode: context.chatmode,
|
|
210
|
+
replyContext: context.replyContext,
|
|
211
|
+
});
|
|
212
|
+
const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm allow 同意 / /perm always 始终允许 / /perm deny 拒绝`;
|
|
213
|
+
const result = await sendInteractionPayload(context.adapter, envelope, interaction, fallbackText, context.replyContext);
|
|
201
214
|
interactionSent = !!result;
|
|
202
215
|
}
|
|
203
216
|
catch (err) {
|
|
204
|
-
//
|
|
217
|
+
// sendInteractionPayload 已内部捕获,但保险起见再 try/catch
|
|
205
218
|
}
|
|
206
219
|
}
|
|
207
220
|
// fallback 到文本
|
|
208
221
|
if (!interactionSent) {
|
|
209
|
-
await sendPrompt(
|
|
222
|
+
await sendPrompt(renderActionAsText(interaction));
|
|
210
223
|
}
|
|
211
224
|
return new Promise((resolve) => {
|
|
212
225
|
this.pending.set(requestId, { sessionId, toolName, resolve, timer: setTimeout(() => { }, 0) });
|
|
213
|
-
//
|
|
214
|
-
if (
|
|
226
|
+
// 注册到 InteractionRouter(卡片和文本降级都注册,统一路由)
|
|
227
|
+
if (context?.interactionRouter) {
|
|
215
228
|
context.interactionRouter.register(requestId, sessionId, (action) => {
|
|
216
229
|
this.resolvePermission(sessionId, requestId, action);
|
|
217
|
-
});
|
|
230
|
+
}, { initiatorId: context?.userId, fallbackCommand: 'perm' });
|
|
218
231
|
}
|
|
219
232
|
});
|
|
220
233
|
}
|
|
@@ -3,12 +3,31 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Reads Codex thread data from ~/.codex/state_*.sqlite (read-only)
|
|
5
5
|
* and Codex rollout JSONL files for detailed session info.
|
|
6
|
+
*
|
|
7
|
+
* `node:sqlite` 是 Node 22.5+ 的实验性内置模块。低版本 Node 上懒加载会失败,
|
|
8
|
+
* adapter 自动降级到只读 rollout JSONL —— `checkExists/getFileInfo/readFirstMessage/
|
|
9
|
+
* readLastUserMessage` 仍可工作;`scanCliSessions/listSdkSessions` 因依赖 DB 索引
|
|
10
|
+
* 会返回空数组。
|
|
6
11
|
*/
|
|
7
|
-
import {
|
|
12
|
+
import { createRequire } from 'module';
|
|
8
13
|
import { logger } from '../../../utils/logger.js';
|
|
9
14
|
import path from 'path';
|
|
10
15
|
import fs from 'fs';
|
|
11
16
|
import os from 'os';
|
|
17
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
18
|
+
let sqliteModule; // undefined = not tried, null = unavailable
|
|
19
|
+
function loadSqlite() {
|
|
20
|
+
if (sqliteModule !== undefined)
|
|
21
|
+
return sqliteModule;
|
|
22
|
+
try {
|
|
23
|
+
sqliteModule = requireFromHere('node:sqlite');
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
logger.warn(`[CodexAdapter] node:sqlite unavailable (Node < 22.5?): ${e?.message || e}. Codex session listing falls back to rollout JSONL.`);
|
|
27
|
+
sqliteModule = null;
|
|
28
|
+
}
|
|
29
|
+
return sqliteModule;
|
|
30
|
+
}
|
|
12
31
|
export class CodexSessionFileAdapter {
|
|
13
32
|
agentId = 'codex';
|
|
14
33
|
db = null;
|
|
@@ -39,11 +58,14 @@ export class CodexSessionFileAdapter {
|
|
|
39
58
|
if (this.dbInitialized)
|
|
40
59
|
return this.db;
|
|
41
60
|
this.dbInitialized = true;
|
|
61
|
+
const sqlite = loadSqlite();
|
|
62
|
+
if (!sqlite)
|
|
63
|
+
return null;
|
|
42
64
|
const dbPath = this.resolveStateDbPath();
|
|
43
65
|
if (!dbPath)
|
|
44
66
|
return null;
|
|
45
67
|
try {
|
|
46
|
-
this.db = new DatabaseSync(dbPath, { readOnly: true });
|
|
68
|
+
this.db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
47
69
|
logger.debug(`[CodexAdapter] Opened state DB: ${dbPath}`);
|
|
48
70
|
}
|
|
49
71
|
catch (error) {
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// 文件系统非法字符(Windows 最严格):< > : " / \ | ? *
|
|
4
|
+
// 还有控制字符 0x00-0x1F。我们把这些字符编码为 %XX(hex 大写)。
|
|
5
|
+
// `%` 本身也要转义为 %25,保证可逆。
|
|
6
|
+
const UNSAFE_CHARS_RE = /[<>:"/\\|?*\x00-\x1F%]/g;
|
|
7
|
+
function encodeSegment(s) {
|
|
8
|
+
return s.replace(UNSAFE_CHARS_RE, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
9
|
+
}
|
|
10
|
+
function decodeSegment(s) {
|
|
11
|
+
return s.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 计算 chat 目录的完整路径。
|
|
15
|
+
* - AUN:sessionsDir/aun/<urlEncode(selfId)>/<urlEncode(channelId)>/
|
|
16
|
+
* - 其它:sessionsDir/<channelType>/<urlEncode(channelId)>/
|
|
17
|
+
*
|
|
18
|
+
* 注:channelType 自身不编码(限定枚举值,不含非法字符)。
|
|
19
|
+
*/
|
|
20
|
+
export function chatDirPath(sessionsDir, channelType, channelId, selfId) {
|
|
21
|
+
if (channelType === 'aun') {
|
|
22
|
+
const self = selfId || '_unknown';
|
|
23
|
+
return path.join(sessionsDir, 'aun', encodeSegment(self), encodeSegment(channelId));
|
|
24
|
+
}
|
|
25
|
+
return path.join(sessionsDir, channelType, encodeSegment(channelId));
|
|
26
|
+
}
|
|
27
|
+
/** 解码目录段(用于扫描时把目录名还原为原始 channelId/selfId) */
|
|
28
|
+
export function decodeDirSegment(seg) {
|
|
29
|
+
return decodeSegment(seg);
|
|
30
|
+
}
|
|
31
|
+
export function generateSessionId(now) {
|
|
32
|
+
const ts = now ?? Date.now();
|
|
33
|
+
const d = new Date(ts);
|
|
34
|
+
const yyyy = d.getFullYear();
|
|
35
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
36
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
37
|
+
return `meta_${yyyy}${mm}${dd}_${ts}`;
|
|
38
|
+
}
|
|
39
|
+
export function formatTimestamp(epochMs) {
|
|
40
|
+
const d = new Date(epochMs);
|
|
41
|
+
const yyyy = d.getFullYear();
|
|
42
|
+
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
43
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
44
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
45
|
+
const mi = String(d.getMinutes()).padStart(2, '0');
|
|
46
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
47
|
+
return `${yyyy}-${mo}-${dd} ${hh}:${mi}:${ss}`;
|
|
48
|
+
}
|
|
49
|
+
export function atomicWriteJson(filePath, data) {
|
|
50
|
+
const tmpPath = filePath + '.tmp';
|
|
51
|
+
const content = JSON.stringify(data, null, 2) + '\n';
|
|
52
|
+
const fd = fs.openSync(tmpPath, 'w');
|
|
53
|
+
fs.writeSync(fd, content);
|
|
54
|
+
fs.fsyncSync(fd);
|
|
55
|
+
fs.closeSync(fd);
|
|
56
|
+
try {
|
|
57
|
+
fs.unlinkSync(filePath);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
if (e.code !== 'ENOENT')
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
fs.renameSync(tmpPath, filePath);
|
|
64
|
+
}
|
|
65
|
+
export function appendJsonl(filePath, record) {
|
|
66
|
+
const line = JSON.stringify(record) + '\n';
|
|
67
|
+
const fd = fs.openSync(filePath, 'a');
|
|
68
|
+
fs.writeSync(fd, line);
|
|
69
|
+
fs.fsyncSync(fd);
|
|
70
|
+
fs.closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
export function readJsonFile(filePath) {
|
|
73
|
+
try {
|
|
74
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
75
|
+
return JSON.parse(content);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (e.code === 'ENOENT')
|
|
79
|
+
return undefined;
|
|
80
|
+
if (e instanceof SyntaxError)
|
|
81
|
+
return undefined;
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function readLastJsonlLine(filePath) {
|
|
86
|
+
try {
|
|
87
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
88
|
+
const lines = content.trimEnd().split('\n');
|
|
89
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
90
|
+
const line = lines[i].trim();
|
|
91
|
+
if (!line)
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(line);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (e.code === 'ENOENT')
|
|
104
|
+
return undefined;
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function readAllJsonlLines(filePath) {
|
|
109
|
+
try {
|
|
110
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
111
|
+
const results = [];
|
|
112
|
+
for (const line of content.split('\n')) {
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
if (!trimmed)
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
results.push(JSON.parse(trimmed));
|
|
118
|
+
}
|
|
119
|
+
catch { /* skip corrupt line */ }
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e.code === 'ENOENT')
|
|
125
|
+
return [];
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 扫描所有 chat 目录。
|
|
131
|
+
* 顶层是 channelType 目录;aun 下面再有一层 selfId 目录。
|
|
132
|
+
* 返回每个 chat 的:channelType、selfId(仅 aun 有)、channelId、dirPath。
|
|
133
|
+
*/
|
|
134
|
+
export function scanChatDirs(sessionsDir) {
|
|
135
|
+
const results = [];
|
|
136
|
+
let typeEntries;
|
|
137
|
+
try {
|
|
138
|
+
typeEntries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (e.code === 'ENOENT')
|
|
142
|
+
return [];
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
for (const typeEntry of typeEntries) {
|
|
146
|
+
if (!typeEntry.isDirectory())
|
|
147
|
+
continue;
|
|
148
|
+
const channelType = typeEntry.name;
|
|
149
|
+
const typeDir = path.join(sessionsDir, channelType);
|
|
150
|
+
if (channelType === 'aun') {
|
|
151
|
+
// aun 下还有一层 selfId
|
|
152
|
+
let selfEntries;
|
|
153
|
+
try {
|
|
154
|
+
selfEntries = fs.readdirSync(typeDir, { withFileTypes: true });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
for (const selfEntry of selfEntries) {
|
|
160
|
+
if (!selfEntry.isDirectory())
|
|
161
|
+
continue;
|
|
162
|
+
const selfDir = path.join(typeDir, selfEntry.name);
|
|
163
|
+
let chatEntries;
|
|
164
|
+
try {
|
|
165
|
+
chatEntries = fs.readdirSync(selfDir, { withFileTypes: true });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
for (const chatEntry of chatEntries) {
|
|
171
|
+
if (!chatEntry.isDirectory())
|
|
172
|
+
continue;
|
|
173
|
+
results.push({
|
|
174
|
+
channelType,
|
|
175
|
+
selfId: decodeSegment(selfEntry.name),
|
|
176
|
+
channelId: decodeSegment(chatEntry.name),
|
|
177
|
+
dirPath: path.join(selfDir, chatEntry.name),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// 通用 channel:sessionsDir/{channelType}/{encodedChannelId}/
|
|
184
|
+
let chatEntries;
|
|
185
|
+
try {
|
|
186
|
+
chatEntries = fs.readdirSync(typeDir, { withFileTypes: true });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
for (const chatEntry of chatEntries) {
|
|
192
|
+
if (!chatEntry.isDirectory())
|
|
193
|
+
continue;
|
|
194
|
+
results.push({
|
|
195
|
+
channelType,
|
|
196
|
+
selfId: null,
|
|
197
|
+
channelId: decodeSegment(chatEntry.name),
|
|
198
|
+
dirPath: path.join(typeDir, chatEntry.name),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
export function scanMetaFiles(chatDir) {
|
|
206
|
+
try {
|
|
207
|
+
const entries = fs.readdirSync(chatDir);
|
|
208
|
+
return entries
|
|
209
|
+
.filter(f => f.startsWith('meta_') && f.endsWith('.jsonl'))
|
|
210
|
+
.sort();
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
if (e.code === 'ENOENT')
|
|
214
|
+
return [];
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function ensureChatDir(sessionsDir, channelType, channelId, selfId) {
|
|
219
|
+
const dir = chatDirPath(sessionsDir, channelType, channelId, selfId);
|
|
220
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
221
|
+
fs.mkdirSync(path.join(dir, '_threads'), { recursive: true });
|
|
222
|
+
fs.mkdirSync(path.join(dir, '_trash'), { recursive: true });
|
|
223
|
+
return dir;
|
|
224
|
+
}
|
|
225
|
+
export function readThreadIndex(chatDir) {
|
|
226
|
+
return readJsonFile(path.join(chatDir, '_threads', 'thread-index.json')) || {};
|
|
227
|
+
}
|
|
228
|
+
export function writeThreadIndex(chatDir, index) {
|
|
229
|
+
atomicWriteJson(path.join(chatDir, '_threads', 'thread-index.json'), index);
|
|
230
|
+
}
|