evolclaw 2.1.2 → 2.3.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 +59 -30
- package/data/evolclaw.sample.json +15 -4
- package/dist/agents/claude-runner.js +685 -0
- package/dist/agents/codex-runner.js +315 -0
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +580 -10
- package/dist/channels/feishu.js +888 -135
- package/dist/channels/wechat.js +127 -21
- package/dist/cli.js +519 -136
- package/dist/config.js +277 -25
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +67 -0
- package/dist/core/command-handler.js +1537 -392
- package/dist/core/event-bus.js +32 -0
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/message/message-processor.js +1028 -0
- package/dist/core/message/message-queue.js +240 -0
- package/dist/core/message/stream-debouncer.js +122 -0
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
- package/dist/core/permission.js +259 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-manager.js +1072 -0
- package/dist/index.js +402 -252
- package/dist/ipc.js +106 -0
- package/dist/paths.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/{platform.js → cross-platform.js} +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +190 -53
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/stats-collector.js +102 -0
- package/package.json +4 -2
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-processor.js +0 -604
- package/dist/core/message-queue.js +0 -116
- package/dist/core/message-stream.js +0 -59
- package/dist/core/session-manager.js +0 -664
- package/dist/index.js.bak +0 -340
- package/dist/utils/init-feishu.js +0 -261
- package/dist/utils/init-wechat.js +0 -170
- package/dist/utils/markdown-to-feishu.js +0 -94
- package/dist/utils/permission.js +0 -43
- package/dist/utils/session-file-health.js +0 -68
- /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini SessionFileAdapter
|
|
3
|
+
*
|
|
4
|
+
* Reads Gemini CLI session files from ~/.gemini/tmp/{project}/chats/.
|
|
5
|
+
* Gemini stores sessions as JSON files with naming convention:
|
|
6
|
+
* session-{YYYY-MM-DDTHH-mm}-{uuid-prefix}.json
|
|
7
|
+
*
|
|
8
|
+
* Each file contains:
|
|
9
|
+
* { sessionId, projectHash, startTime, lastUpdated, messages, kind }
|
|
10
|
+
*
|
|
11
|
+
* Messages have type: 'user' | 'gemini' (not 'role').
|
|
12
|
+
*/
|
|
13
|
+
import { logger } from '../../../utils/logger.js';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
export class GeminiSessionFileAdapter {
|
|
18
|
+
agentId = 'gemini';
|
|
19
|
+
getGeminiHome() {
|
|
20
|
+
return path.join(os.homedir(), '.gemini');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve Gemini project key for a project path.
|
|
24
|
+
* Priority:
|
|
25
|
+
* 1. ~/.gemini/projects.json explicit mapping
|
|
26
|
+
* 2. .project_root file content match under ~/.gemini/tmp/<project-key>/
|
|
27
|
+
* 3. basename(projectPath) fallback
|
|
28
|
+
*/
|
|
29
|
+
resolveProjectKey(projectPath) {
|
|
30
|
+
const geminiHome = this.getGeminiHome();
|
|
31
|
+
const projectsPath = path.join(geminiHome, 'projects.json');
|
|
32
|
+
try {
|
|
33
|
+
if (fs.existsSync(projectsPath)) {
|
|
34
|
+
const data = JSON.parse(fs.readFileSync(projectsPath, 'utf-8'));
|
|
35
|
+
const mapped = data?.projects?.[projectPath];
|
|
36
|
+
if (typeof mapped === 'string' && mapped.trim())
|
|
37
|
+
return mapped;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.debug('[GeminiAdapter] Failed to read projects.json:', error);
|
|
42
|
+
}
|
|
43
|
+
const tmpDir = path.join(geminiHome, 'tmp');
|
|
44
|
+
if (fs.existsSync(tmpDir)) {
|
|
45
|
+
try {
|
|
46
|
+
for (const entry of fs.readdirSync(tmpDir, { withFileTypes: true })) {
|
|
47
|
+
if (!entry.isDirectory())
|
|
48
|
+
continue;
|
|
49
|
+
const rootFile = path.join(tmpDir, entry.name, '.project_root');
|
|
50
|
+
if (!fs.existsSync(rootFile))
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(rootFile, 'utf-8').trim();
|
|
54
|
+
if (content === projectPath)
|
|
55
|
+
return entry.name;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// ignore broken .project_root
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.debug('[GeminiAdapter] Failed to scan tmp project roots:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return path.basename(projectPath);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the Gemini chats directory for a given project path.
|
|
70
|
+
*/
|
|
71
|
+
resolveChatsDir(projectPath) {
|
|
72
|
+
const geminiHome = this.getGeminiHome();
|
|
73
|
+
const projectKey = this.resolveProjectKey(projectPath);
|
|
74
|
+
return path.join(geminiHome, 'tmp', projectKey, 'chats');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Find the session file matching a given session UUID.
|
|
78
|
+
* Gemini files are named session-{date}-{uuid-prefix}.json where
|
|
79
|
+
* uuid-prefix is the first 8 chars of the session UUID.
|
|
80
|
+
*/
|
|
81
|
+
findSessionFile(projectPath, agentSessionId) {
|
|
82
|
+
const chatsDir = this.resolveChatsDir(projectPath);
|
|
83
|
+
if (!fs.existsSync(chatsDir))
|
|
84
|
+
return null;
|
|
85
|
+
const uuidPrefix = agentSessionId.substring(0, 8);
|
|
86
|
+
try {
|
|
87
|
+
const files = fs.readdirSync(chatsDir);
|
|
88
|
+
const match = files.find(f => f.includes(uuidPrefix) && f.endsWith('.json'));
|
|
89
|
+
return match ? path.join(chatsDir, match) : null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
readSessionData(projectPath, agentSessionId) {
|
|
96
|
+
const filePath = this.findSessionFile(projectPath, agentSessionId);
|
|
97
|
+
if (!filePath)
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
logger.warn(`[GeminiAdapter] Failed to read session file: ${filePath}`, error);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
checkExists(projectPath, agentSessionId) {
|
|
108
|
+
return this.findSessionFile(projectPath, agentSessionId) !== null;
|
|
109
|
+
}
|
|
110
|
+
getFileInfo(projectPath, agentSessionId) {
|
|
111
|
+
const data = this.readSessionData(projectPath, agentSessionId);
|
|
112
|
+
if (!data)
|
|
113
|
+
return { turns: 0 };
|
|
114
|
+
const msgs = data.messages || [];
|
|
115
|
+
const userTurns = msgs.filter((m) => m.type === 'user').length;
|
|
116
|
+
// Extract title from first user message
|
|
117
|
+
let title;
|
|
118
|
+
const firstUser = msgs.find((m) => m.type === 'user');
|
|
119
|
+
if (firstUser) {
|
|
120
|
+
const text = Array.isArray(firstUser.content)
|
|
121
|
+
? firstUser.content[0]?.text || ''
|
|
122
|
+
: String(firstUser.content || '');
|
|
123
|
+
title = text.substring(0, 50).trim() || undefined;
|
|
124
|
+
}
|
|
125
|
+
return { turns: userTurns, title };
|
|
126
|
+
}
|
|
127
|
+
readFirstMessage(projectPath, agentSessionId) {
|
|
128
|
+
return this.readUserMessage(projectPath, agentSessionId, 'first');
|
|
129
|
+
}
|
|
130
|
+
readLastUserMessage(projectPath, agentSessionId) {
|
|
131
|
+
return this.readUserMessage(projectPath, agentSessionId, 'last');
|
|
132
|
+
}
|
|
133
|
+
scanCliSessions(projectPath) {
|
|
134
|
+
const chatsDir = this.resolveChatsDir(projectPath);
|
|
135
|
+
if (!fs.existsSync(chatsDir))
|
|
136
|
+
return [];
|
|
137
|
+
try {
|
|
138
|
+
const files = fs.readdirSync(chatsDir)
|
|
139
|
+
.filter(f => f.startsWith('session-') && f.endsWith('.json'))
|
|
140
|
+
.map(f => {
|
|
141
|
+
const filePath = path.join(chatsDir, f);
|
|
142
|
+
const stat = fs.statSync(filePath);
|
|
143
|
+
// Extract UUID from filename: session-{date}-{uuid-prefix}.json
|
|
144
|
+
// Read the file to get full UUID
|
|
145
|
+
try {
|
|
146
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
147
|
+
return { uuid: data.sessionId, mtime: stat.mtimeMs };
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
.filter((e) => e !== null)
|
|
154
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
155
|
+
.slice(0, 10);
|
|
156
|
+
return files;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
logger.warn('[GeminiAdapter] scanCliSessions failed:', error);
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
readUserMessage(projectPath, agentSessionId, which) {
|
|
164
|
+
const data = this.readSessionData(projectPath, agentSessionId);
|
|
165
|
+
if (!data)
|
|
166
|
+
return null;
|
|
167
|
+
const msgs = (data.messages || []).filter((m) => m.type === 'user');
|
|
168
|
+
if (msgs.length === 0)
|
|
169
|
+
return null;
|
|
170
|
+
const msg = which === 'first' ? msgs[0] : msgs[msgs.length - 1];
|
|
171
|
+
const text = Array.isArray(msg.content)
|
|
172
|
+
? msg.content[0]?.text || ''
|
|
173
|
+
: String(msg.content || '');
|
|
174
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
175
|
+
return trimmed.substring(0, 50) + (trimmed.length > 50 ? '...' : '');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fsPromises from 'fs/promises';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* 检查会话文件健康度(接收完整文件路径)
|
|
5
|
+
*/
|
|
6
|
+
export async function checkSessionFile(sessionFile) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
try {
|
|
9
|
+
const stats = await fsPromises.stat(sessionFile);
|
|
10
|
+
const sizeMB = stats.size / (1024 * 1024);
|
|
11
|
+
if (stats.size > 50 * 1024 * 1024) {
|
|
12
|
+
issues.push(`会话文件过大: ${sizeMB.toFixed(1)}MB`);
|
|
13
|
+
}
|
|
14
|
+
const content = await fsPromises.readFile(sessionFile, 'utf-8');
|
|
15
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
try {
|
|
18
|
+
JSON.parse(lines[i]);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
issues.push(`会话文件格式损坏(第 ${i + 1} 行)`);
|
|
22
|
+
return { healthy: false, issues, corrupt: true, fileSize: stats.size };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
healthy: issues.length === 0,
|
|
27
|
+
issues,
|
|
28
|
+
fileSize: stats.size
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger.error('[SessionFileHealth] Check failed:', error);
|
|
33
|
+
issues.push(`文件读取失败: ${error.message}`);
|
|
34
|
+
return { healthy: false, issues, corrupt: true };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 备份单个会话文件(在同目录下创建 .bak 副本)
|
|
39
|
+
*/
|
|
40
|
+
export async function backupSessionFile(sessionFile) {
|
|
41
|
+
const backupPath = `${sessionFile}.bak-${Date.now()}`;
|
|
42
|
+
await fsPromises.copyFile(sessionFile, backupPath);
|
|
43
|
+
logger.info(`[SessionFileHealth] Backup created: ${backupPath}`);
|
|
44
|
+
return backupPath;
|
|
45
|
+
}
|