evolclaw 2.1.1 → 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 +571 -223
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/error-utils.js +4 -2
- 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
package/dist/config.js
CHANGED
|
@@ -15,6 +15,38 @@ function loadClaudeSettings() {
|
|
|
15
15
|
catch { }
|
|
16
16
|
return {};
|
|
17
17
|
}
|
|
18
|
+
function loadCodexSettings() {
|
|
19
|
+
try {
|
|
20
|
+
// Read auth.json for API key
|
|
21
|
+
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
22
|
+
let apiKey;
|
|
23
|
+
if (fs.existsSync(authPath)) {
|
|
24
|
+
const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
25
|
+
apiKey = auth.OPENAI_API_KEY;
|
|
26
|
+
}
|
|
27
|
+
// Read config.toml for model and baseUrl (simple TOML parsing)
|
|
28
|
+
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
29
|
+
let model;
|
|
30
|
+
let baseUrl;
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
33
|
+
const modelMatch = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
34
|
+
if (modelMatch)
|
|
35
|
+
model = modelMatch[1];
|
|
36
|
+
// Extract base_url from model_providers section
|
|
37
|
+
const providerMatch = content.match(/^model_provider\s*=\s*"([^"]+)"/m);
|
|
38
|
+
if (providerMatch) {
|
|
39
|
+
const provider = providerMatch[1];
|
|
40
|
+
const baseUrlMatch = content.match(new RegExp(`\\[model_providers\\.${provider}\\][\\s\\S]*?base_url\\s*=\\s*"([^"]+)"`, 'm'));
|
|
41
|
+
if (baseUrlMatch)
|
|
42
|
+
baseUrl = baseUrlMatch[1];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { apiKey, baseUrl, model };
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
18
50
|
export function resolveAnthropicConfig(config) {
|
|
19
51
|
const settings = loadClaudeSettings();
|
|
20
52
|
// 过滤占位符,视为未配置
|
|
@@ -42,6 +74,32 @@ export function resolveAnthropicConfig(config) {
|
|
|
42
74
|
|| undefined;
|
|
43
75
|
return { apiKey, baseUrl, model, effort };
|
|
44
76
|
}
|
|
77
|
+
export function resolveOpenaiConfig(config) {
|
|
78
|
+
const codexSettings = loadCodexSettings();
|
|
79
|
+
// 过滤占位符,视为未配置
|
|
80
|
+
const configApiKey = config.agents?.openai?.apiKey;
|
|
81
|
+
const isPlaceholderKey = !configApiKey ||
|
|
82
|
+
configApiKey.includes('your-') ||
|
|
83
|
+
configApiKey.includes('placeholder');
|
|
84
|
+
const apiKey = (isPlaceholderKey ? null : configApiKey)
|
|
85
|
+
|| process.env.OPENAI_API_KEY
|
|
86
|
+
|| codexSettings.apiKey;
|
|
87
|
+
if (!apiKey) {
|
|
88
|
+
throw new Error('No OpenAI API key found. Set one of: agents.openai.apiKey, env OPENAI_API_KEY, or ~/.codex/auth.json');
|
|
89
|
+
}
|
|
90
|
+
// baseUrl 也过滤占位符(与 anthropic 保持一致:只检查默认域名)
|
|
91
|
+
const configBaseUrl = config.agents?.openai?.baseUrl;
|
|
92
|
+
const isPlaceholderUrl = configBaseUrl?.includes('api.openai.com');
|
|
93
|
+
const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
|
|
94
|
+
|| process.env.OPENAI_BASE_URL
|
|
95
|
+
|| codexSettings.baseUrl
|
|
96
|
+
|| undefined;
|
|
97
|
+
const model = config.agents?.openai?.model
|
|
98
|
+
|| codexSettings.model
|
|
99
|
+
|| 'gpt-5.2-codex';
|
|
100
|
+
const reasoning = config.agents?.openai?.reasoning || undefined;
|
|
101
|
+
return { apiKey, baseUrl, model, reasoning };
|
|
102
|
+
}
|
|
45
103
|
export function loadConfig(configPath = resolvePaths().config) {
|
|
46
104
|
if (!fs.existsSync(configPath)) {
|
|
47
105
|
throw new Error(`Config file not found: ${configPath}`);
|
|
@@ -81,10 +139,15 @@ function validateConfig(config) {
|
|
|
81
139
|
logger.warn('⚠ Feishu appSecret not configured (Feishu channel will be disabled)');
|
|
82
140
|
}
|
|
83
141
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
142
|
+
// AUN 配置可选,但如果配置了就要有 domain 和 agentName
|
|
143
|
+
if (config.channels?.aun?.enabled !== false && config.channels?.aun) {
|
|
144
|
+
if (!config.channels.aun.domain) {
|
|
145
|
+
logger.warn('⚠ AUN domain not configured (AUN channel will be disabled)');
|
|
146
|
+
}
|
|
147
|
+
if (!config.channels.aun.agentName) {
|
|
148
|
+
logger.warn('⚠ AUN agentName not configured (AUN channel will be disabled)');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
88
151
|
if (!config.projects?.defaultPath)
|
|
89
152
|
throw new Error('Missing projects.defaultPath');
|
|
90
153
|
// WeChat 配置可选,但如果启用了就需要 token
|
|
@@ -97,3 +160,38 @@ export function ensureDir(dirPath) {
|
|
|
97
160
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
98
161
|
}
|
|
99
162
|
}
|
|
163
|
+
// agents.defaultAgent → config key 映射
|
|
164
|
+
const agentKeyMap = { claude: 'anthropic', codex: 'openai' };
|
|
165
|
+
/**
|
|
166
|
+
* 配置结构完整性校验(不校验凭据有效性)。
|
|
167
|
+
* 要求 agents/channels/projects 三段同时具备必要的锚点字段。
|
|
168
|
+
*/
|
|
169
|
+
export function validateConfigIntegrity(config) {
|
|
170
|
+
const reasons = [];
|
|
171
|
+
// agents
|
|
172
|
+
const defaultAgent = config.agents?.defaultAgent;
|
|
173
|
+
if (!defaultAgent) {
|
|
174
|
+
reasons.push('Missing agents.defaultAgent');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const key = agentKeyMap[defaultAgent] || defaultAgent;
|
|
178
|
+
if (!config.agents?.[key]) {
|
|
179
|
+
reasons.push(`agents.defaultAgent='${defaultAgent}' but agents.${key} does not exist`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// channels
|
|
183
|
+
const defaultChannel = config.channels?.defaultChannel;
|
|
184
|
+
if (!defaultChannel) {
|
|
185
|
+
reasons.push('Missing channels.defaultChannel');
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
if (!config.channels?.[defaultChannel]) {
|
|
189
|
+
reasons.push(`channels.defaultChannel='${defaultChannel}' but channels.${defaultChannel} does not exist`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// projects
|
|
193
|
+
if (!config.projects?.defaultPath) {
|
|
194
|
+
reasons.push('Missing projects.defaultPath');
|
|
195
|
+
}
|
|
196
|
+
return { valid: reasons.length === 0, reasons };
|
|
197
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude SessionFileAdapter
|
|
3
|
+
*
|
|
4
|
+
* Reads Claude Agent SDK session files from ~/.claude/projects/{encodedPath}/{sessionId}.jsonl
|
|
5
|
+
* and wraps sdkListSessions for name synchronization.
|
|
6
|
+
*/
|
|
7
|
+
import { listSessions as sdkListSessions } from '@anthropic-ai/claude-agent-sdk';
|
|
8
|
+
import { encodePath } from '../../utils/cross-platform.js';
|
|
9
|
+
import { logger } from '../../utils/logger.js';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
export class ClaudeSessionFileAdapter {
|
|
14
|
+
agentId = 'claude';
|
|
15
|
+
getSessionFilePath(projectPath, agentSessionId) {
|
|
16
|
+
const homeDir = os.homedir();
|
|
17
|
+
const encodedPath = encodePath(projectPath);
|
|
18
|
+
return path.join(homeDir, '.claude', 'projects', encodedPath, `${agentSessionId}.jsonl`);
|
|
19
|
+
}
|
|
20
|
+
checkExists(projectPath, agentSessionId) {
|
|
21
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
22
|
+
return fs.existsSync(sessionFile);
|
|
23
|
+
}
|
|
24
|
+
getFileInfo(projectPath, agentSessionId) {
|
|
25
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
26
|
+
if (!fs.existsSync(sessionFile))
|
|
27
|
+
return { turns: 0 };
|
|
28
|
+
try {
|
|
29
|
+
const content = fs.readFileSync(sessionFile, 'utf-8');
|
|
30
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
31
|
+
let turns = 0;
|
|
32
|
+
let title;
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const event = JSON.parse(line);
|
|
35
|
+
if (event.type === 'user' && event.message?.role === 'user') {
|
|
36
|
+
const msgContent = event.message.content;
|
|
37
|
+
const isToolResult = Array.isArray(msgContent) && msgContent.every((c) => c.type === 'tool_result');
|
|
38
|
+
if (!isToolResult) {
|
|
39
|
+
turns++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (event.title && !title) {
|
|
43
|
+
title = event.title;
|
|
44
|
+
}
|
|
45
|
+
if (event.sessionTitle && !title) {
|
|
46
|
+
title = event.sessionTitle;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { turns, title };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger.warn(`[ClaudeAdapter] Failed to read session file info: ${sessionFile}`, error);
|
|
53
|
+
return { turns: 0 };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
readFirstMessage(projectPath, agentSessionId) {
|
|
57
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
58
|
+
if (!fs.existsSync(sessionFile))
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
const content = fs.readFileSync(sessionFile, 'utf-8');
|
|
62
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const event = JSON.parse(line);
|
|
65
|
+
if (event.type === 'user' && event.message?.role === 'user') {
|
|
66
|
+
const text = this.extractUserMessageText(event.message.content);
|
|
67
|
+
if (text)
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.warn(`[ClaudeAdapter] Failed to read session file: ${sessionFile}`, error);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
readLastUserMessage(projectPath, agentSessionId) {
|
|
78
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
79
|
+
if (!fs.existsSync(sessionFile))
|
|
80
|
+
return null;
|
|
81
|
+
try {
|
|
82
|
+
const content = fs.readFileSync(sessionFile, 'utf-8');
|
|
83
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
84
|
+
let lastMessage = null;
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const event = JSON.parse(line);
|
|
87
|
+
if (event.type === 'user' && event.message?.role === 'user') {
|
|
88
|
+
lastMessage = this.extractUserMessageText(event.message.content) ?? lastMessage;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return lastMessage;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger.warn(`[ClaudeAdapter] Failed to read last message from session file: ${sessionFile}`, error);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
scanCliSessions(projectPath) {
|
|
99
|
+
const homeDir = os.homedir();
|
|
100
|
+
const encodedPath = encodePath(projectPath);
|
|
101
|
+
const sessionDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
102
|
+
if (!fs.existsSync(sessionDir))
|
|
103
|
+
return [];
|
|
104
|
+
const files = fs.readdirSync(sessionDir)
|
|
105
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
106
|
+
.filter(f => !f.startsWith('agent-'))
|
|
107
|
+
.map(f => {
|
|
108
|
+
const filePath = path.join(sessionDir, f);
|
|
109
|
+
const stat = fs.statSync(filePath);
|
|
110
|
+
return { uuid: f.replace('.jsonl', ''), mtime: stat.mtimeMs, size: stat.size };
|
|
111
|
+
})
|
|
112
|
+
.filter(f => f.size > 0)
|
|
113
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
114
|
+
.slice(0, 10);
|
|
115
|
+
return files.map(f => ({ uuid: f.uuid, mtime: f.mtime }));
|
|
116
|
+
}
|
|
117
|
+
async listSdkSessions(projectPath) {
|
|
118
|
+
try {
|
|
119
|
+
const sessions = await sdkListSessions({ dir: projectPath });
|
|
120
|
+
return sessions.map(s => ({
|
|
121
|
+
sessionId: s.sessionId,
|
|
122
|
+
title: s.customTitle || undefined,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
logger.debug('[ClaudeAdapter] SDK listSessions failed (non-critical):', error);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
extractUserMessageText(messageContent) {
|
|
131
|
+
if (typeof messageContent === 'string') {
|
|
132
|
+
const text = messageContent.trim().replace(/\s+/g, ' ');
|
|
133
|
+
return text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
|
134
|
+
}
|
|
135
|
+
else if (Array.isArray(messageContent)) {
|
|
136
|
+
const textContent = messageContent.find((c) => c.type === 'text');
|
|
137
|
+
if (textContent?.text) {
|
|
138
|
+
const text = textContent.text.trim().replace(/\s+/g, ' ');
|
|
139
|
+
return text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex SessionFileAdapter
|
|
3
|
+
*
|
|
4
|
+
* Reads Codex thread data from ~/.codex/state_*.sqlite (read-only)
|
|
5
|
+
* and Codex rollout JSONL files for detailed session info.
|
|
6
|
+
*/
|
|
7
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
8
|
+
import { logger } from '../../utils/logger.js';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
export class CodexSessionFileAdapter {
|
|
13
|
+
agentId = 'codex';
|
|
14
|
+
db = null;
|
|
15
|
+
dbInitialized = false;
|
|
16
|
+
/**
|
|
17
|
+
* 动态发现最新的 state_*.sqlite 文件
|
|
18
|
+
* Codex 使用 sqlx 迁移,DB 文件名含版本号(state_5, state_6, ...)
|
|
19
|
+
*/
|
|
20
|
+
resolveStateDbPath() {
|
|
21
|
+
const codexHome = path.join(os.homedir(), '.codex');
|
|
22
|
+
if (!fs.existsSync(codexHome))
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const files = fs.readdirSync(codexHome)
|
|
26
|
+
.filter(f => /^state_\d+\.sqlite$/.test(f))
|
|
27
|
+
.sort((a, b) => {
|
|
28
|
+
const va = parseInt(a.match(/state_(\d+)/)?.[1] || '0');
|
|
29
|
+
const vb = parseInt(b.match(/state_(\d+)/)?.[1] || '0');
|
|
30
|
+
return vb - va;
|
|
31
|
+
});
|
|
32
|
+
return files.length > 0 ? path.join(codexHome, files[0]) : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
getDb() {
|
|
39
|
+
if (this.dbInitialized)
|
|
40
|
+
return this.db;
|
|
41
|
+
this.dbInitialized = true;
|
|
42
|
+
const dbPath = this.resolveStateDbPath();
|
|
43
|
+
if (!dbPath)
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
this.db = new DatabaseSync(dbPath, { readOnly: true });
|
|
47
|
+
logger.debug(`[CodexAdapter] Opened state DB: ${dbPath}`);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
logger.warn(`[CodexAdapter] Failed to open state DB: ${dbPath}`, error);
|
|
51
|
+
this.db = null;
|
|
52
|
+
}
|
|
53
|
+
return this.db;
|
|
54
|
+
}
|
|
55
|
+
checkExists(projectPath, agentSessionId) {
|
|
56
|
+
const db = this.getDb();
|
|
57
|
+
if (!db)
|
|
58
|
+
return false;
|
|
59
|
+
try {
|
|
60
|
+
const row = db.prepare('SELECT 1 FROM threads WHERE id = ? AND archived = 0').get(agentSessionId);
|
|
61
|
+
return !!row;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.warn(`[CodexAdapter] checkExists failed:`, error);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
getFileInfo(projectPath, agentSessionId) {
|
|
69
|
+
const db = this.getDb();
|
|
70
|
+
if (!db)
|
|
71
|
+
return { turns: 0 };
|
|
72
|
+
try {
|
|
73
|
+
const row = db.prepare('SELECT title, rollout_path FROM threads WHERE id = ?').get(agentSessionId);
|
|
74
|
+
if (!row)
|
|
75
|
+
return { turns: 0 };
|
|
76
|
+
const title = row.title || undefined;
|
|
77
|
+
const turns = this.countTurnsFromRollout(row.rollout_path);
|
|
78
|
+
return { turns, title };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logger.warn(`[CodexAdapter] getFileInfo failed:`, error);
|
|
82
|
+
return { turns: 0 };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
readFirstMessage(projectPath, agentSessionId) {
|
|
86
|
+
const db = this.getDb();
|
|
87
|
+
if (!db)
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
const row = db.prepare('SELECT first_user_message FROM threads WHERE id = ?').get(agentSessionId);
|
|
91
|
+
if (!row?.first_user_message)
|
|
92
|
+
return null;
|
|
93
|
+
const text = row.first_user_message.trim().replace(/\s+/g, ' ');
|
|
94
|
+
return text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
logger.warn(`[CodexAdapter] readFirstMessage failed:`, error);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
readLastUserMessage(projectPath, agentSessionId) {
|
|
102
|
+
const db = this.getDb();
|
|
103
|
+
if (!db)
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
const row = db.prepare('SELECT rollout_path FROM threads WHERE id = ?').get(agentSessionId);
|
|
107
|
+
if (!row?.rollout_path || !fs.existsSync(row.rollout_path))
|
|
108
|
+
return null;
|
|
109
|
+
const content = fs.readFileSync(row.rollout_path, 'utf-8');
|
|
110
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
111
|
+
let lastMessage = null;
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
try {
|
|
114
|
+
const event = JSON.parse(line);
|
|
115
|
+
if (event.type === 'event_msg' && event.payload?.type === 'user_message' && event.payload.message) {
|
|
116
|
+
const text = event.payload.message.trim().replace(/\s+/g, ' ');
|
|
117
|
+
lastMessage = text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { /* skip malformed line */ }
|
|
121
|
+
}
|
|
122
|
+
return lastMessage;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
logger.warn(`[CodexAdapter] readLastUserMessage failed:`, error);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
scanCliSessions(projectPath) {
|
|
130
|
+
const db = this.getDb();
|
|
131
|
+
if (!db)
|
|
132
|
+
return [];
|
|
133
|
+
try {
|
|
134
|
+
const rows = db.prepare('SELECT id, updated_at FROM threads WHERE cwd = ? AND archived = 0 ORDER BY updated_at DESC LIMIT 10').all(projectPath);
|
|
135
|
+
return rows.map(r => ({
|
|
136
|
+
uuid: r.id,
|
|
137
|
+
mtime: r.updated_at, // Codex uses Unix timestamp (seconds)
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logger.warn(`[CodexAdapter] scanCliSessions failed:`, error);
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async listSdkSessions(projectPath) {
|
|
146
|
+
const db = this.getDb();
|
|
147
|
+
if (!db)
|
|
148
|
+
return [];
|
|
149
|
+
try {
|
|
150
|
+
const rows = db.prepare('SELECT id, title FROM threads WHERE cwd = ? AND archived = 0 ORDER BY updated_at DESC').all(projectPath);
|
|
151
|
+
return rows.map(r => ({
|
|
152
|
+
sessionId: r.id,
|
|
153
|
+
title: r.title || undefined,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
logger.warn(`[CodexAdapter] listSdkSessions failed:`, error);
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
close() {
|
|
162
|
+
if (this.db) {
|
|
163
|
+
try {
|
|
164
|
+
this.db.close();
|
|
165
|
+
}
|
|
166
|
+
catch { /* ignore close errors */ }
|
|
167
|
+
this.db = null;
|
|
168
|
+
}
|
|
169
|
+
this.dbInitialized = false;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 从 rollout JSONL 文件计算轮数(数 turn_context 行)
|
|
173
|
+
*/
|
|
174
|
+
countTurnsFromRollout(rolloutPath) {
|
|
175
|
+
if (!rolloutPath || !fs.existsSync(rolloutPath))
|
|
176
|
+
return 0;
|
|
177
|
+
try {
|
|
178
|
+
const content = fs.readFileSync(rolloutPath, 'utf-8');
|
|
179
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
180
|
+
let turns = 0;
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
try {
|
|
183
|
+
const event = JSON.parse(line);
|
|
184
|
+
if (event.type === 'turn_context') {
|
|
185
|
+
turns++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch { /* skip malformed line */ }
|
|
189
|
+
}
|
|
190
|
+
return turns;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Plugin System
|
|
3
|
+
*
|
|
4
|
+
* Provides a lightweight plugin interface for agent integration.
|
|
5
|
+
*/
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
/**
|
|
8
|
+
* Agent Loader
|
|
9
|
+
*
|
|
10
|
+
* Manages agent plugin registration and creation.
|
|
11
|
+
*/
|
|
12
|
+
export class AgentLoader {
|
|
13
|
+
plugins = new Map();
|
|
14
|
+
register(plugin) {
|
|
15
|
+
if (this.plugins.has(plugin.name)) {
|
|
16
|
+
throw new Error(`Agent plugin '${plugin.name}' already registered`);
|
|
17
|
+
}
|
|
18
|
+
this.plugins.set(plugin.name, plugin);
|
|
19
|
+
logger.debug(`Registered agent plugin: ${plugin.name}`);
|
|
20
|
+
}
|
|
21
|
+
createAll(config, callbacks) {
|
|
22
|
+
const instances = [];
|
|
23
|
+
for (const [name, plugin] of this.plugins) {
|
|
24
|
+
if (!plugin.isEnabled(config)) {
|
|
25
|
+
logger.info(`Agent '${name}' is disabled, skipping`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const instance = plugin.createAgent(config, callbacks);
|
|
30
|
+
instances.push(instance);
|
|
31
|
+
logger.info(`✓ Agent '${name}' instance created`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logger.error(`✗ Failed to create agent '${name}':`, error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return instances;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Plugin System
|
|
3
|
+
*
|
|
4
|
+
* Provides a lightweight plugin interface for channel integration.
|
|
5
|
+
* Plugins are responsible for creating channel instances only.
|
|
6
|
+
* The main service (index.ts) handles registration and message flow wiring.
|
|
7
|
+
*/
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
/**
|
|
10
|
+
* Channel Loader
|
|
11
|
+
*
|
|
12
|
+
* Manages channel plugin registration and lifecycle.
|
|
13
|
+
*/
|
|
14
|
+
export class ChannelLoader {
|
|
15
|
+
plugins = new Map();
|
|
16
|
+
register(plugin) {
|
|
17
|
+
if (this.plugins.has(plugin.name)) {
|
|
18
|
+
throw new Error(`Channel plugin '${plugin.name}' already registered`);
|
|
19
|
+
}
|
|
20
|
+
this.plugins.set(plugin.name, plugin);
|
|
21
|
+
logger.debug(`Registered channel plugin: ${plugin.name}`);
|
|
22
|
+
}
|
|
23
|
+
async createAll(config) {
|
|
24
|
+
const instances = [];
|
|
25
|
+
for (const [name, plugin] of this.plugins) {
|
|
26
|
+
if (!plugin.isEnabled(config)) {
|
|
27
|
+
logger.info(`Channel '${name}' is disabled, skipping`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const instance = await plugin.createChannel(config);
|
|
32
|
+
instances.push(instance);
|
|
33
|
+
logger.info(`✓ Channel '${name}' instance created`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.error(`✗ Failed to create channel '${name}':`, error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return instances;
|
|
40
|
+
}
|
|
41
|
+
async connectAll(instances) {
|
|
42
|
+
const results = await Promise.allSettled(instances.map(async (inst) => {
|
|
43
|
+
await inst.connect();
|
|
44
|
+
return inst.adapter.name;
|
|
45
|
+
}));
|
|
46
|
+
const connected = results
|
|
47
|
+
.filter((r) => r.status === 'fulfilled')
|
|
48
|
+
.map((r) => r.value);
|
|
49
|
+
const failed = results
|
|
50
|
+
.filter((r) => r.status === 'rejected')
|
|
51
|
+
.map((r) => r.reason);
|
|
52
|
+
if (failed.length > 0) {
|
|
53
|
+
logger.warn(`Some channels failed to connect:`, failed);
|
|
54
|
+
}
|
|
55
|
+
return connected;
|
|
56
|
+
}
|
|
57
|
+
async disconnectAll(instances) {
|
|
58
|
+
await Promise.allSettled(instances.map((inst) => inst.disconnect()));
|
|
59
|
+
}
|
|
60
|
+
}
|