evolclaw 2.1.2 → 2.2.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 +10 -3
- package/data/evolclaw.sample.json +9 -1
- package/dist/agents/claude-runner.js +612 -0
- package/dist/agents/codex-runner.js +310 -0
- package/dist/channels/aun.js +416 -9
- package/dist/channels/feishu.js +397 -104
- package/dist/channels/wechat.js +84 -2
- package/dist/cli.js +427 -126
- package/dist/config.js +102 -4
- package/dist/core/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/adapters/codex-session-file-adapter.js +196 -0
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +60 -0
- package/dist/core/command-handler.js +908 -304
- package/dist/core/event-bus.js +32 -0
- package/dist/core/ipc-server.js +71 -0
- package/dist/core/message-bridge.js +187 -0
- package/dist/core/message-processor.js +370 -227
- package/dist/core/message-queue.js +153 -29
- package/dist/core/permission.js +58 -0
- package/dist/core/session-file-adapter.js +7 -0
- package/dist/core/session-manager.js +567 -205
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/init-feishu.js +2 -0
- package/dist/utils/init-wechat.js +2 -0
- package/dist/utils/init.js +285 -53
- package/dist/utils/ipc-client.js +36 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/{permission.js → permission-utils.js} +31 -3
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/session-file-health.js +11 -34
- package/dist/utils/stream-debouncer.js +122 -0
- package/dist/utils/stream-idle-monitor.js +1 -1
- package/package.json +3 -1
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-stream.js +0 -59
- package/dist/index.js.bak +0 -340
- package/dist/utils/markdown-to-feishu.js +0 -94
- /package/dist/utils/{platform.js → cross-platform.js} +0 -0
- /package/dist/{core → utils}/message-cache.js +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Agent Runner
|
|
3
|
+
*
|
|
4
|
+
* Integrates OpenAI Codex SDK (@openai/codex-sdk) as an agent backend.
|
|
5
|
+
* Implements the same interface surface as AgentRunner (claude-runner.ts)
|
|
6
|
+
* so MessageProcessor and CommandHandler can work with it transparently.
|
|
7
|
+
*/
|
|
8
|
+
import { Codex } from '@openai/codex-sdk';
|
|
9
|
+
import { resolveOpenaiConfig } from '../config.js';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
// ── MIME → 扩展名映射 ──
|
|
15
|
+
const MIME_EXT = {
|
|
16
|
+
'image/png': '.png',
|
|
17
|
+
'image/jpeg': '.jpg',
|
|
18
|
+
'image/gif': '.gif',
|
|
19
|
+
'image/webp': '.webp',
|
|
20
|
+
};
|
|
21
|
+
// ── Codex 模型列表 ──
|
|
22
|
+
const CODEX_MODELS = ['gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5-codex', 'gpt-5.2', 'gpt-5.4'];
|
|
23
|
+
// ── Codex Runner ──
|
|
24
|
+
export class CodexRunner {
|
|
25
|
+
name = 'codex';
|
|
26
|
+
capabilities = { clear: false, compact: false, fork: false };
|
|
27
|
+
codex;
|
|
28
|
+
model;
|
|
29
|
+
effort;
|
|
30
|
+
activeAbortControllers = new Map();
|
|
31
|
+
activeStreams = new Map();
|
|
32
|
+
activeSessions = new Map(); // sessionId → threadId
|
|
33
|
+
onSessionIdUpdate;
|
|
34
|
+
constructor(config, callbacks) {
|
|
35
|
+
const resolved = resolveOpenaiConfig(config);
|
|
36
|
+
this.codex = new Codex({
|
|
37
|
+
apiKey: resolved.apiKey,
|
|
38
|
+
baseUrl: resolved.baseUrl,
|
|
39
|
+
});
|
|
40
|
+
this.model = resolved.model;
|
|
41
|
+
if (resolved.reasoning)
|
|
42
|
+
this.effort = resolved.reasoning;
|
|
43
|
+
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
44
|
+
}
|
|
45
|
+
// ── ModelSwitcher ──
|
|
46
|
+
setModel(model) { this.model = model; }
|
|
47
|
+
getModel() { return this.model; }
|
|
48
|
+
listModels() { return CODEX_MODELS; }
|
|
49
|
+
// ── Effort ──
|
|
50
|
+
setEffort(effort) { this.effort = effort; }
|
|
51
|
+
getEffort() { return this.effort; }
|
|
52
|
+
// ── Permission ──
|
|
53
|
+
currentMode = 'default';
|
|
54
|
+
approvalPolicy = 'never';
|
|
55
|
+
setMode(mode) {
|
|
56
|
+
const map = {
|
|
57
|
+
'default': 'never',
|
|
58
|
+
'request': 'on-request',
|
|
59
|
+
'noask': 'untrusted',
|
|
60
|
+
};
|
|
61
|
+
this.approvalPolicy = map[mode] || 'never';
|
|
62
|
+
this.currentMode = mode;
|
|
63
|
+
}
|
|
64
|
+
getMode() { return this.currentMode; }
|
|
65
|
+
listModes() {
|
|
66
|
+
return [
|
|
67
|
+
{ key: 'default', nameZh: '默认', description: '全部自动(受 sandbox 约束)', available: true },
|
|
68
|
+
{ key: 'request', nameZh: '审批', description: '需要审批时询问', available: true },
|
|
69
|
+
{ key: 'noask', nameZh: '静默', description: '只执行已知安全操作', available: true },
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
setSendPrompt(_fn) { }
|
|
73
|
+
setPermissionGateway(_gw) { }
|
|
74
|
+
// ── Stream management (needed by MessageProcessor) ──
|
|
75
|
+
registerStream(key, stream) {
|
|
76
|
+
this.activeStreams.set(key, stream);
|
|
77
|
+
}
|
|
78
|
+
cleanupStream(key) {
|
|
79
|
+
this.activeStreams.delete(key);
|
|
80
|
+
this.activeAbortControllers.delete(key);
|
|
81
|
+
}
|
|
82
|
+
hasActiveStream(key) {
|
|
83
|
+
return this.activeStreams.has(key);
|
|
84
|
+
}
|
|
85
|
+
// ── Core: runQuery ──
|
|
86
|
+
async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager) {
|
|
87
|
+
let agentSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
88
|
+
// 安全模式:跳过 resume,创建新 thread
|
|
89
|
+
if (agentSessionId && sessionManager) {
|
|
90
|
+
const health = await sessionManager.getHealthStatus(sessionId);
|
|
91
|
+
if (health.safeMode) {
|
|
92
|
+
agentSessionId = undefined;
|
|
93
|
+
logger.warn(`[CodexRunner] Safe mode enabled for ${sessionId}, not resuming thread`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const threadOptions = {
|
|
97
|
+
workingDirectory: projectPath,
|
|
98
|
+
model: this.model,
|
|
99
|
+
skipGitRepoCheck: true,
|
|
100
|
+
sandboxMode: 'danger-full-access',
|
|
101
|
+
approvalPolicy: this.approvalPolicy,
|
|
102
|
+
...(this.effort ? { modelReasoningEffort: this.effort } : {}),
|
|
103
|
+
};
|
|
104
|
+
const thread = agentSessionId
|
|
105
|
+
? this.codex.resumeThread(agentSessionId, threadOptions)
|
|
106
|
+
: this.codex.startThread(threadOptions);
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
this.activeAbortControllers.set(sessionId, controller);
|
|
109
|
+
// 构建输入:将 base64 图片写入临时文件,转换为 Codex SDK 的 local_image 格式
|
|
110
|
+
const tempFiles = [];
|
|
111
|
+
let input;
|
|
112
|
+
if (images?.length) {
|
|
113
|
+
const tmpDir = os.tmpdir();
|
|
114
|
+
const parts = [{ type: 'text', text: prompt }];
|
|
115
|
+
for (let i = 0; i < images.length; i++) {
|
|
116
|
+
const img = images[i];
|
|
117
|
+
const ext = MIME_EXT[img.mimeType || ''] || '.jpg';
|
|
118
|
+
const tmpPath = path.join(tmpDir, `evolclaw-img-${Date.now()}-${i}${ext}`);
|
|
119
|
+
fs.writeFileSync(tmpPath, Buffer.from(img.data, 'base64'));
|
|
120
|
+
tempFiles.push(tmpPath);
|
|
121
|
+
parts.push({ type: 'local_image', path: tmpPath });
|
|
122
|
+
}
|
|
123
|
+
input = parts;
|
|
124
|
+
logger.info(`[CodexRunner] Attached ${images.length} image(s) as local_image`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
input = prompt;
|
|
128
|
+
}
|
|
129
|
+
const { events } = await thread.runStreamed(input, { signal: controller.signal });
|
|
130
|
+
// 包装为 AgentEvent 流
|
|
131
|
+
return this.transformStream(events, sessionId, thread, tempFiles);
|
|
132
|
+
}
|
|
133
|
+
// ── Interrupt ──
|
|
134
|
+
async interrupt(sessionKey) {
|
|
135
|
+
const controller = this.activeAbortControllers.get(sessionKey);
|
|
136
|
+
if (controller) {
|
|
137
|
+
controller.abort('User interrupt');
|
|
138
|
+
this.activeAbortControllers.delete(sessionKey);
|
|
139
|
+
this.activeStreams.delete(sessionKey);
|
|
140
|
+
logger.info(`[CodexRunner] Interrupted session: ${sessionKey}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── Session commands ──
|
|
144
|
+
updateSessionId(sessionId, agentSessionId) {
|
|
145
|
+
if (agentSessionId) {
|
|
146
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this.activeSessions.delete(sessionId);
|
|
150
|
+
}
|
|
151
|
+
this.onSessionIdUpdate?.(sessionId, agentSessionId);
|
|
152
|
+
}
|
|
153
|
+
async closeSession(sessionId) {
|
|
154
|
+
this.activeSessions.delete(sessionId);
|
|
155
|
+
this.activeStreams.delete(sessionId);
|
|
156
|
+
this.activeAbortControllers.delete(sessionId);
|
|
157
|
+
}
|
|
158
|
+
resolveSessionFile(agentSessionId, _projectPath) {
|
|
159
|
+
// Codex session 文件: ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
|
|
160
|
+
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
161
|
+
if (!fs.existsSync(sessionsDir))
|
|
162
|
+
return null;
|
|
163
|
+
// 递归搜索文件名包含 threadId 的 JSONL 文件
|
|
164
|
+
const search = (dir) => {
|
|
165
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
const found = search(path.join(dir, entry.name));
|
|
168
|
+
if (found)
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
171
|
+
else if (entry.name.endsWith('.jsonl') && entry.name.includes(agentSessionId)) {
|
|
172
|
+
return path.join(dir, entry.name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
};
|
|
177
|
+
return search(sessionsDir);
|
|
178
|
+
}
|
|
179
|
+
async clearSession(_sessionId, _agentSessionId, _projectPath) {
|
|
180
|
+
// Codex: 清空会话 = 下次 runQuery 不传 resumeId,自动创建新 thread
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
async compactSession(_sessionId, _agentSessionId, _projectPath) {
|
|
184
|
+
// Codex CLI 内部处理 compaction,外部无法触发
|
|
185
|
+
logger.info('[CodexRunner] Compact not supported, Codex handles context internally');
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
async compact(_sessionId, _agentSessionId, _projectPath) {
|
|
189
|
+
return this.compactSession(_sessionId, _agentSessionId, _projectPath);
|
|
190
|
+
}
|
|
191
|
+
setCompactStartCallback(_callback) { }
|
|
192
|
+
// ── Event stream transformation ──
|
|
193
|
+
async *transformStream(events, sessionId, thread, tempFiles) {
|
|
194
|
+
try {
|
|
195
|
+
for await (const event of events) {
|
|
196
|
+
yield* this.mapEvent(event, sessionId, thread);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
this.activeAbortControllers.delete(sessionId);
|
|
201
|
+
// 清理临时图片文件
|
|
202
|
+
if (tempFiles?.length) {
|
|
203
|
+
for (const f of tempFiles) {
|
|
204
|
+
try {
|
|
205
|
+
fs.unlinkSync(f);
|
|
206
|
+
}
|
|
207
|
+
catch { /* ignore */ }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
*mapEvent(event, sessionId, thread) {
|
|
213
|
+
switch (event.type) {
|
|
214
|
+
case 'thread.started': {
|
|
215
|
+
const threadId = event.thread_id;
|
|
216
|
+
this.activeSessions.set(sessionId, threadId);
|
|
217
|
+
this.onSessionIdUpdate?.(sessionId, threadId);
|
|
218
|
+
yield { type: 'session_id', sessionId: threadId };
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case 'item.started': {
|
|
222
|
+
const item = event.item;
|
|
223
|
+
if (item.type === 'command_execution') {
|
|
224
|
+
yield { type: 'tool_use', name: 'Shell', input: { command: item.command } };
|
|
225
|
+
}
|
|
226
|
+
else if (item.type === 'mcp_tool_call') {
|
|
227
|
+
yield { type: 'tool_use', name: `MCP:${item.server}/${item.tool}`, input: item.arguments };
|
|
228
|
+
}
|
|
229
|
+
else if (item.type === 'file_change') {
|
|
230
|
+
const desc = item.changes.map(c => `${c.kind} ${c.path}`).join(', ');
|
|
231
|
+
yield { type: 'tool_use', name: 'FileChange', input: { description: desc } };
|
|
232
|
+
}
|
|
233
|
+
else if (item.type === 'web_search') {
|
|
234
|
+
yield { type: 'tool_use', name: 'WebSearch', input: { query: item.query } };
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case 'item.completed': {
|
|
239
|
+
const item = event.item;
|
|
240
|
+
if (item.type === 'agent_message') {
|
|
241
|
+
yield { type: 'text', text: item.text };
|
|
242
|
+
}
|
|
243
|
+
else if (item.type === 'command_execution') {
|
|
244
|
+
yield {
|
|
245
|
+
type: 'tool_result',
|
|
246
|
+
name: 'Shell',
|
|
247
|
+
result: item.aggregated_output,
|
|
248
|
+
isError: item.exit_code !== 0,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
else if (item.type === 'mcp_tool_call') {
|
|
252
|
+
yield {
|
|
253
|
+
type: 'tool_result',
|
|
254
|
+
name: `MCP:${item.server}/${item.tool}`,
|
|
255
|
+
result: item.result,
|
|
256
|
+
isError: item.status === 'failed',
|
|
257
|
+
error: item.error?.message,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
else if (item.type === 'error') {
|
|
261
|
+
yield { type: 'error', error: item.message, errorType: 'unknown' };
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case 'turn.completed': {
|
|
266
|
+
yield {
|
|
267
|
+
type: 'complete',
|
|
268
|
+
result: undefined,
|
|
269
|
+
costUsd: undefined,
|
|
270
|
+
durationMs: undefined,
|
|
271
|
+
};
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case 'turn.failed': {
|
|
275
|
+
yield { type: 'error', error: event.error.message, errorType: 'unknown' };
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'error': {
|
|
279
|
+
yield { type: 'error', error: event.message, errorType: 'unknown' };
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async dispose() {
|
|
285
|
+
// Abort all active streams
|
|
286
|
+
for (const [key, controller] of this.activeAbortControllers) {
|
|
287
|
+
controller.abort('dispose');
|
|
288
|
+
}
|
|
289
|
+
this.activeAbortControllers.clear();
|
|
290
|
+
this.activeStreams.clear();
|
|
291
|
+
this.activeSessions.clear();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// ── Plugin ──
|
|
295
|
+
export class CodexAgentPlugin {
|
|
296
|
+
name = 'codex';
|
|
297
|
+
isEnabled(config) {
|
|
298
|
+
try {
|
|
299
|
+
const resolved = resolveOpenaiConfig(config);
|
|
300
|
+
return !!resolved.apiKey;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
createAgent(config, callbacks) {
|
|
307
|
+
const resolved = resolveOpenaiConfig(config);
|
|
308
|
+
return { agent: new CodexRunner(config, callbacks) };
|
|
309
|
+
}
|
|
310
|
+
}
|