evolclaw 2.8.3 → 3.1.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/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -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 +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- 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 +740 -777
- 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/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- 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/prompts.md +0 -104
- 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
- package/dist/utils/upgrade.js +0 -100
|
@@ -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();
|
|
@@ -80,7 +80,8 @@ export class MessageQueue {
|
|
|
80
80
|
}
|
|
81
81
|
const queueKey = this.getQueueKey(sessionKey, projectPath);
|
|
82
82
|
const agentName = options?.agentName || DEFAULT_AGENT_NAME;
|
|
83
|
-
|
|
83
|
+
const isProcessing = this.processing.has(queueKey);
|
|
84
|
+
logger.info(`[Queue] enqueue: key=${queueKey} processing=${isProcessing} queueLen=${this.queues.get(queueKey)?.length ?? 0} agent=${agentName}`);
|
|
84
85
|
return new Promise((resolve, reject) => {
|
|
85
86
|
if (!this.queues.has(queueKey)) {
|
|
86
87
|
this.queues.set(queueKey, []);
|
|
@@ -92,7 +93,7 @@ export class MessageQueue {
|
|
|
92
93
|
// 单聊:保留中断行为
|
|
93
94
|
logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt`);
|
|
94
95
|
this.eventBus?.publish({
|
|
95
|
-
type: '
|
|
96
|
+
type: 'task:interrupted',
|
|
96
97
|
sessionId: sessionKey,
|
|
97
98
|
reason: 'new_message',
|
|
98
99
|
agentName: this.processingAgent.get(queueKey),
|
|
@@ -104,6 +105,12 @@ export class MessageQueue {
|
|
|
104
105
|
else {
|
|
105
106
|
// 群聊:FIFO,不打断
|
|
106
107
|
logger.debug(`[Queue] ${queueKey} is processing, message queued (FIFO)`);
|
|
108
|
+
this.eventBus?.publish({
|
|
109
|
+
type: 'task:queued',
|
|
110
|
+
channel: message.channel,
|
|
111
|
+
channelId: message.channelId,
|
|
112
|
+
replyContext: message.replyContext,
|
|
113
|
+
});
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
else {
|
|
@@ -114,7 +121,7 @@ export class MessageQueue {
|
|
|
114
121
|
}
|
|
115
122
|
async processNext(queueKey) {
|
|
116
123
|
this.processing.add(queueKey);
|
|
117
|
-
logger.
|
|
124
|
+
logger.info(`[Queue] processNext: start key=${queueKey}`);
|
|
118
125
|
while (true) {
|
|
119
126
|
// 等待外部锁释放(/compact, /clear 等快速命令)
|
|
120
127
|
const lock = this.getExternalLock(queueKey);
|
|
@@ -124,7 +131,7 @@ export class MessageQueue {
|
|
|
124
131
|
}
|
|
125
132
|
const queue = this.queues.get(queueKey);
|
|
126
133
|
if (!queue || queue.length === 0) {
|
|
127
|
-
logger.
|
|
134
|
+
logger.info(`[Queue] processNext: queue empty, releasing key=${queueKey}`);
|
|
128
135
|
this.processing.delete(queueKey);
|
|
129
136
|
this.processingAgent.delete(queueKey);
|
|
130
137
|
this.currentSessionKey = undefined;
|
|
@@ -276,7 +283,7 @@ export class MessageQueue {
|
|
|
276
283
|
const sessionKey = this.currentSessionKey.split('::')[0];
|
|
277
284
|
logger.info(`[Queue] Recalled active message ${messageId}, interrupting session ${sessionKey}`);
|
|
278
285
|
this.eventBus?.publish({
|
|
279
|
-
type: '
|
|
286
|
+
type: 'task:interrupted',
|
|
280
287
|
sessionId: sessionKey,
|
|
281
288
|
reason: 'recalled',
|
|
282
289
|
agentName: this.processingAgent.get(this.currentSessionKey),
|
package/dist/core/permission.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { renderActionAsText } from './interaction-router.js';
|
|
4
|
+
import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
|
|
2
5
|
// 危险命令黑名单(正则表达式)
|
|
3
6
|
const DANGEROUS_PATTERNS = [
|
|
4
7
|
// Unix
|
|
@@ -87,9 +90,15 @@ export function summarizeToolInput(toolName, input) {
|
|
|
87
90
|
return '';
|
|
88
91
|
const extractors = {
|
|
89
92
|
'Read': (i) => i.file_path,
|
|
90
|
-
'Edit': (i) => i
|
|
93
|
+
'Edit': (i) => formatEditSummary(i),
|
|
91
94
|
'Write': (i) => i.file_path,
|
|
92
|
-
'Bash': (i) =>
|
|
95
|
+
'Bash': (i) => {
|
|
96
|
+
const cmd = i.command?.substring(0, 80) || '';
|
|
97
|
+
const desc = i.description;
|
|
98
|
+
if (desc && cmd)
|
|
99
|
+
return `${cmd} | ${desc}`;
|
|
100
|
+
return cmd || desc;
|
|
101
|
+
},
|
|
93
102
|
'Grep': (i) => `pattern: ${i.pattern}`,
|
|
94
103
|
'Glob': (i) => `pattern: ${i.pattern}`,
|
|
95
104
|
'Agent': (i) => i.description || i.prompt?.substring(0, 80),
|
|
@@ -108,6 +117,8 @@ export function summarizeToolInput(toolName, input) {
|
|
|
108
117
|
},
|
|
109
118
|
'TaskCreate': (i) => i.subject || i.description?.substring(0, 80),
|
|
110
119
|
'TaskUpdate': (i) => i.status ? `${i.taskId} → ${i.status}` : i.taskId,
|
|
120
|
+
'TaskOutput': (i) => `${i.task_id || '?'}${i.block === false ? ' (non-blocking)' : ''}${i.timeout ? ` timeout=${i.timeout}ms` : ''}`,
|
|
121
|
+
'TaskStop': (i) => i.task_id || i.shell_id || '?',
|
|
111
122
|
'NotebookEdit': (i) => i.notebook_path,
|
|
112
123
|
'WebFetch': (i) => i.url,
|
|
113
124
|
'WebSearch': (i) => i.query?.substring(0, 80),
|
|
@@ -129,6 +140,81 @@ export function summarizeToolInput(toolName, input) {
|
|
|
129
140
|
|| input.url
|
|
130
141
|
|| '';
|
|
131
142
|
}
|
|
143
|
+
/** 为 Edit 工具生成 diff 风格摘要 */
|
|
144
|
+
function formatEditSummary(input) {
|
|
145
|
+
const filePath = input.file_path || '';
|
|
146
|
+
const oldStr = typeof input.old_string === 'string' ? input.old_string : '';
|
|
147
|
+
const newStr = typeof input.new_string === 'string' ? input.new_string : '';
|
|
148
|
+
if (!oldStr && !newStr)
|
|
149
|
+
return filePath;
|
|
150
|
+
const MAX_DIFF_LINES = 14;
|
|
151
|
+
const oldLines = oldStr.split('\n');
|
|
152
|
+
const newLines = newStr.split('\n');
|
|
153
|
+
// 尝试从文件中定位 old_string 的起始行号
|
|
154
|
+
let startLine = 0; // 0-based; 0 means unknown
|
|
155
|
+
if (filePath && oldStr) {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
158
|
+
const idx = content.indexOf(oldStr);
|
|
159
|
+
if (idx >= 0) {
|
|
160
|
+
startLine = content.slice(0, idx).split('\n').length; // 1-based
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// 文件不可读,行号留空
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const diffLines = [];
|
|
168
|
+
// 找公共前缀行数
|
|
169
|
+
let prefixLen = 0;
|
|
170
|
+
while (prefixLen < oldLines.length && prefixLen < newLines.length && oldLines[prefixLen] === newLines[prefixLen]) {
|
|
171
|
+
prefixLen++;
|
|
172
|
+
}
|
|
173
|
+
// 找公共后缀行数
|
|
174
|
+
let suffixLen = 0;
|
|
175
|
+
while (suffixLen < oldLines.length - prefixLen &&
|
|
176
|
+
suffixLen < newLines.length - prefixLen &&
|
|
177
|
+
oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]) {
|
|
178
|
+
suffixLen++;
|
|
179
|
+
}
|
|
180
|
+
const CONTEXT = 2;
|
|
181
|
+
// 计算行号宽度(用于对齐)
|
|
182
|
+
const maxLineNo = startLine > 0 ? startLine + oldLines.length - 1 : 0;
|
|
183
|
+
const newMaxLineNo = startLine > 0 ? startLine + prefixLen + (newLines.length - suffixLen - prefixLen) - 1 : 0;
|
|
184
|
+
const padWidth = startLine > 0 ? Math.max(maxLineNo, newMaxLineNo).toString().length : 0;
|
|
185
|
+
// 格式化一行:行号 + 标记 + 内容
|
|
186
|
+
// 使用 Unicode 符号避免飞书 Markdown 将 "- " 解析为列表
|
|
187
|
+
const fmtLine = (lineNo, marker, text) => {
|
|
188
|
+
if (startLine > 0) {
|
|
189
|
+
return `${lineNo.toString().padStart(padWidth)} ${marker} ${text}`;
|
|
190
|
+
}
|
|
191
|
+
return `${marker} ${text}`;
|
|
192
|
+
};
|
|
193
|
+
// 上下文前缀(最多 CONTEXT 行)
|
|
194
|
+
const ctxStart = Math.max(0, prefixLen - CONTEXT);
|
|
195
|
+
for (let i = ctxStart; i < prefixLen; i++) {
|
|
196
|
+
diffLines.push(fmtLine(startLine + i, ' ', oldLines[i]));
|
|
197
|
+
}
|
|
198
|
+
// 删除行
|
|
199
|
+
const removedEnd = oldLines.length - suffixLen;
|
|
200
|
+
for (let i = prefixLen; i < removedEnd && diffLines.length < MAX_DIFF_LINES; i++) {
|
|
201
|
+
diffLines.push(fmtLine(startLine + i, '−', oldLines[i]));
|
|
202
|
+
}
|
|
203
|
+
// 新增行(行号从 prefixLen 位置开始递增)
|
|
204
|
+
const addedEnd = newLines.length - suffixLen;
|
|
205
|
+
for (let i = prefixLen; i < addedEnd && diffLines.length < MAX_DIFF_LINES; i++) {
|
|
206
|
+
diffLines.push(fmtLine(startLine + i, '+', newLines[i]));
|
|
207
|
+
}
|
|
208
|
+
// 上下文后缀(最多 CONTEXT 行)
|
|
209
|
+
const ctxEnd = Math.min(oldLines.length, removedEnd + CONTEXT);
|
|
210
|
+
for (let i = removedEnd; i < ctxEnd && diffLines.length < MAX_DIFF_LINES + 2; i++) {
|
|
211
|
+
diffLines.push(fmtLine(startLine + i, ' ', oldLines[i]));
|
|
212
|
+
}
|
|
213
|
+
if (diffLines.length > MAX_DIFF_LINES + 2) {
|
|
214
|
+
diffLines.splice(MAX_DIFF_LINES, diffLines.length, ' ...');
|
|
215
|
+
}
|
|
216
|
+
return `${filePath}\n\`\`\`\n${diffLines.join('\n')}\n\`\`\``;
|
|
217
|
+
}
|
|
132
218
|
export class PermissionGateway {
|
|
133
219
|
pending = new Map();
|
|
134
220
|
timeout = 5 * 60 * 1000;
|
|
@@ -192,29 +278,48 @@ export class PermissionGateway {
|
|
|
192
278
|
},
|
|
193
279
|
channelId: context?.channelId || '',
|
|
194
280
|
sessionId,
|
|
281
|
+
initiatorId: context?.userId,
|
|
282
|
+
fallback: { command: 'perm' },
|
|
195
283
|
};
|
|
196
|
-
//
|
|
284
|
+
// 尝试富交互(走统一 adapter.send 入口)
|
|
197
285
|
let interactionSent = false;
|
|
198
|
-
if (context?.adapter
|
|
286
|
+
if (context?.adapter && context.channelId) {
|
|
199
287
|
try {
|
|
200
|
-
const
|
|
288
|
+
const envelope = buildEnvelope({
|
|
289
|
+
taskId: context.taskId,
|
|
290
|
+
channel: context.channel ?? context.adapter.channelName,
|
|
291
|
+
channelId: context.channelId,
|
|
292
|
+
agentName: context.agentName,
|
|
293
|
+
chatmode: context.chatmode,
|
|
294
|
+
replyContext: context.replyContext,
|
|
295
|
+
});
|
|
296
|
+
const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm allow 同意 / /perm always 始终允许 / /perm deny 拒绝`;
|
|
297
|
+
const result = await sendInteractionPayload(context.adapter, envelope, interaction, fallbackText, context.replyContext);
|
|
201
298
|
interactionSent = !!result;
|
|
202
299
|
}
|
|
203
300
|
catch (err) {
|
|
204
|
-
//
|
|
301
|
+
// sendInteractionPayload 已内部捕获,但保险起见再 try/catch
|
|
205
302
|
}
|
|
206
303
|
}
|
|
207
304
|
// fallback 到文本
|
|
208
305
|
if (!interactionSent) {
|
|
209
|
-
await sendPrompt(
|
|
306
|
+
await sendPrompt(renderActionAsText(interaction));
|
|
210
307
|
}
|
|
211
308
|
return new Promise((resolve) => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
309
|
+
const timer = setTimeout(() => {
|
|
310
|
+
const pending = this.pending.get(requestId);
|
|
311
|
+
if (!pending)
|
|
312
|
+
return;
|
|
313
|
+
this.pending.delete(requestId);
|
|
314
|
+
this.eventBus?.publish({ type: 'permission:timeout', sessionId, requestId, toolName });
|
|
315
|
+
pending.resolve('deny');
|
|
316
|
+
}, this.timeout);
|
|
317
|
+
this.pending.set(requestId, { sessionId, toolName, resolve, timer });
|
|
318
|
+
// 注册到 InteractionRouter(卡片和文本降级都注册,统一路由)
|
|
319
|
+
if (context?.interactionRouter) {
|
|
215
320
|
context.interactionRouter.register(requestId, sessionId, (action) => {
|
|
216
321
|
this.resolvePermission(sessionId, requestId, action);
|
|
217
|
-
});
|
|
322
|
+
}, { initiatorId: context?.userId, fallbackCommand: 'perm' });
|
|
218
323
|
}
|
|
219
324
|
});
|
|
220
325
|
}
|
|
@@ -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
|
+
}
|