evolclaw 2.0.3 → 2.0.5
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/LICENSE +21 -0
- package/README.md +143 -89
- package/data/evolclaw.sample.json +8 -17
- package/dist/channels/feishu.js +68 -10
- package/dist/channels/wechat.js +238 -5
- package/dist/cli.js +108 -78
- package/dist/core/agent-runner.js +3 -2
- package/dist/core/command-handler.js +3 -3
- package/dist/core/message-processor.js +6 -2
- package/dist/core/session-manager.js +4 -3
- package/dist/index.js +32 -9
- package/dist/paths.js +3 -1
- package/dist/utils/init.js +66 -50
- package/dist/utils/markdown-to-feishu.js +58 -2
- package/dist/utils/permission.js +7 -0
- package/dist/utils/platform.js +175 -0
- package/package.json +3 -4
- package/bin/evolclaw +0 -10
|
@@ -109,7 +109,7 @@ export class MessageProcessor {
|
|
|
109
109
|
});
|
|
110
110
|
try {
|
|
111
111
|
await Promise.race([
|
|
112
|
-
this._processMessageInternal(message, resetTimer),
|
|
112
|
+
this._processMessageInternal(message, resetTimer, isGroup),
|
|
113
113
|
timeoutPromise
|
|
114
114
|
]);
|
|
115
115
|
}
|
|
@@ -172,7 +172,7 @@ export class MessageProcessor {
|
|
|
172
172
|
await adapter.sendText(channelId, `⚠️ 检测到异常(${consecutiveErrors}/${safeModeThreshold})\n\n如果问题持续,系统将自动进入安全模式。建议使用 /status 查看状态。`);
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
-
async _processMessageInternal(message, resetTimer) {
|
|
175
|
+
async _processMessageInternal(message, resetTimer, isGroup) {
|
|
176
176
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
177
177
|
const channelInfo = this.channels.get(message.channel);
|
|
178
178
|
if (!channelInfo) {
|
|
@@ -215,6 +215,7 @@ export class MessageProcessor {
|
|
|
215
215
|
// 创建 StreamFlusher,传入文件标记模式用于自动过滤
|
|
216
216
|
// 使用动态判断,确保切换项目后不会继续输出
|
|
217
217
|
let firstReply = true;
|
|
218
|
+
const messageIsGroup = isGroup; // 捕获 isGroup 供闭包使用
|
|
218
219
|
const flusher = new StreamFlusher(async (text, isFinal) => {
|
|
219
220
|
// 动态判断是否是后台任务
|
|
220
221
|
const currentActiveSession = await this.sessionManager.getActiveSession(message.channel, message.channelId);
|
|
@@ -537,6 +538,9 @@ export class MessageProcessor {
|
|
|
537
538
|
// 纯标点/特殊字符(非路径字符)
|
|
538
539
|
if (/^[.\s…]+$/.test(filePath))
|
|
539
540
|
return true;
|
|
541
|
+
// 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
|
|
542
|
+
if (/[\\[\]{}*+?|^$]/.test(filePath))
|
|
543
|
+
return true;
|
|
540
544
|
return false;
|
|
541
545
|
}
|
|
542
546
|
}
|
|
@@ -2,6 +2,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
2
2
|
import { ensureDir } from '../config.js';
|
|
3
3
|
import { resolvePaths } from '../paths.js';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { encodePath } from '../utils/platform.js';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import fs from 'fs';
|
|
7
8
|
import os from 'os';
|
|
@@ -16,7 +17,7 @@ export class SessionManager {
|
|
|
16
17
|
return this.db;
|
|
17
18
|
}
|
|
18
19
|
getProjectDirName(projectPath) {
|
|
19
|
-
return projectPath
|
|
20
|
+
return encodePath(projectPath);
|
|
20
21
|
}
|
|
21
22
|
getSessionFilePath(projectPath, sessionId) {
|
|
22
23
|
const homeDir = os.homedir();
|
|
@@ -61,13 +62,13 @@ export class SessionManager {
|
|
|
61
62
|
}
|
|
62
63
|
extractUserMessageText(messageContent) {
|
|
63
64
|
if (typeof messageContent === 'string') {
|
|
64
|
-
const text = messageContent.trim();
|
|
65
|
+
const text = messageContent.trim().replace(/\s+/g, ' ');
|
|
65
66
|
return text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
|
66
67
|
}
|
|
67
68
|
else if (Array.isArray(messageContent)) {
|
|
68
69
|
const textContent = messageContent.find((c) => c.type === 'text');
|
|
69
70
|
if (textContent?.text) {
|
|
70
|
-
const text = textContent.text.trim();
|
|
71
|
+
const text = textContent.text.trim().replace(/\s+/g, ' ');
|
|
71
72
|
return text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
|
72
73
|
}
|
|
73
74
|
}
|
package/dist/index.js
CHANGED
|
@@ -89,6 +89,9 @@ async function main() {
|
|
|
89
89
|
const fileMatches = [...text.matchAll(fileMarkerPattern)];
|
|
90
90
|
for (const match of fileMatches) {
|
|
91
91
|
const filePath = match[1].trim();
|
|
92
|
+
// 跳过占位符/代码片段中的伪路径
|
|
93
|
+
if (!filePath || /[\\[\]{}*+?|^$]/.test(filePath))
|
|
94
|
+
continue;
|
|
92
95
|
const session = await sessionManager.getActiveSession(channel, channelId);
|
|
93
96
|
const projectPath = session?.projectPath || process.cwd();
|
|
94
97
|
const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
|
|
@@ -132,7 +135,7 @@ async function main() {
|
|
|
132
135
|
isGroupChat: (channelId) => feishu.getChatMode(channelId).then(m => m === 'group'),
|
|
133
136
|
};
|
|
134
137
|
const feishuOptions = {
|
|
135
|
-
systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE
|
|
138
|
+
systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt] 路径支持相对路径(相对项目目录)或绝对路径。系统会自动上传并发送。',
|
|
136
139
|
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
137
140
|
supportsImages: true,
|
|
138
141
|
};
|
|
@@ -155,11 +158,23 @@ async function main() {
|
|
|
155
158
|
baseUrl: config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com',
|
|
156
159
|
token: config.channels.wechat.token,
|
|
157
160
|
});
|
|
161
|
+
// 设置项目路径提供器(用于接收文件保存)
|
|
162
|
+
wechat.onProjectPathRequest(async (channelId) => {
|
|
163
|
+
const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
|
|
164
|
+
return path.isAbsolute(session.projectPath)
|
|
165
|
+
? session.projectPath
|
|
166
|
+
: path.resolve(process.cwd(), session.projectPath);
|
|
167
|
+
});
|
|
158
168
|
const wechatAdapter = {
|
|
159
169
|
name: 'wechat',
|
|
160
170
|
sendText: (channelId, text) => wechat.sendMessage(channelId, text),
|
|
171
|
+
sendFile: (channelId, filePath) => wechat.sendFile(channelId, filePath),
|
|
161
172
|
};
|
|
162
|
-
|
|
173
|
+
const wechatOptions = {
|
|
174
|
+
systemPromptAppend: '[系统功能] 你可以发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt]',
|
|
175
|
+
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
176
|
+
};
|
|
177
|
+
processor.registerChannel(wechatAdapter, wechatOptions);
|
|
163
178
|
cmdHandler.registerAdapter(wechatAdapter);
|
|
164
179
|
// Session 过期通知(通过 Feishu 等其他渠道告知用户)
|
|
165
180
|
wechat.onSessionExpiredNotify(async (message) => {
|
|
@@ -177,7 +192,7 @@ async function main() {
|
|
|
177
192
|
logger.warn(`[WeChat] ${message}`);
|
|
178
193
|
}
|
|
179
194
|
});
|
|
180
|
-
wechat.onMessage(async (channelId, content, userId) => {
|
|
195
|
+
wechat.onMessage(async (channelId, content, userId, images) => {
|
|
181
196
|
content = content.trim();
|
|
182
197
|
// 首次交互自动绑定主人
|
|
183
198
|
if (userId && !config.channels?.wechat?.owner) {
|
|
@@ -203,12 +218,12 @@ async function main() {
|
|
|
203
218
|
// 获取当前项目路径
|
|
204
219
|
const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
|
|
205
220
|
// 普通消息进入队列
|
|
206
|
-
await messageQueue.enqueue(`wechat-${channelId}`, { channel: 'wechat', channelId, content, timestamp: Date.now(), userId }, session.projectPath);
|
|
221
|
+
await messageQueue.enqueue(`wechat-${channelId}`, { channel: 'wechat', channelId, content, images, timestamp: Date.now(), userId }, session.projectPath);
|
|
207
222
|
});
|
|
208
223
|
}
|
|
209
224
|
// Feishu 消息处理
|
|
210
225
|
if (feishu) {
|
|
211
|
-
feishu.onMessage(async (chatId, content, images, userId, userName, messageId) => {
|
|
226
|
+
feishu.onMessage(async (chatId, content, images, userId, userName, messageId, mentions) => {
|
|
212
227
|
content = content.trim();
|
|
213
228
|
// 首次交互自动绑定主人
|
|
214
229
|
if (userId && !config.channels?.feishu?.owner) {
|
|
@@ -239,7 +254,7 @@ async function main() {
|
|
|
239
254
|
content = `[${userName}] ${content}`;
|
|
240
255
|
}
|
|
241
256
|
// 普通消息进入队列
|
|
242
|
-
await messageQueue.enqueue(`feishu-${chatId}`, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group' }, session.projectPath);
|
|
257
|
+
await messageQueue.enqueue(`feishu-${chatId}`, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions }, session.projectPath);
|
|
243
258
|
});
|
|
244
259
|
}
|
|
245
260
|
// AUN 消息处理
|
|
@@ -269,13 +284,21 @@ async function main() {
|
|
|
269
284
|
// 连接渠道
|
|
270
285
|
const channels = [];
|
|
271
286
|
const channelInstances = [
|
|
272
|
-
...(feishu ? [{ name: 'Feishu', instance: feishu }] : []),
|
|
287
|
+
...(feishu ? [{ name: 'Feishu', instance: feishu, timeout: 5000 }] : []),
|
|
273
288
|
...(aun ? [{ name: 'AUN', instance: aun }] : []),
|
|
274
289
|
...(wechat ? [{ name: 'WeChat', instance: wechat }] : []),
|
|
275
290
|
];
|
|
276
|
-
for (const { name, instance } of channelInstances) {
|
|
291
|
+
for (const { name, instance, timeout } of channelInstances) {
|
|
277
292
|
try {
|
|
278
|
-
|
|
293
|
+
if (timeout) {
|
|
294
|
+
await Promise.race([
|
|
295
|
+
instance.connect(),
|
|
296
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), timeout))
|
|
297
|
+
]);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
await instance.connect();
|
|
301
|
+
}
|
|
279
302
|
logger.info(`✓ ${name} connected`);
|
|
280
303
|
channels.push(name);
|
|
281
304
|
}
|
package/dist/paths.js
CHANGED
|
@@ -41,5 +41,7 @@ export function ensureDataDirs() {
|
|
|
41
41
|
fs.mkdirSync(p.logs, { recursive: true });
|
|
42
42
|
}
|
|
43
43
|
export function getPackageRoot() {
|
|
44
|
-
|
|
44
|
+
// import.meta.dirname is available in Node.js 21.2+ and always returns
|
|
45
|
+
// the correct OS-native path, regardless of Git Bash or MSYS2 environment.
|
|
46
|
+
return path.resolve(import.meta.dirname, '..');
|
|
45
47
|
}
|
package/dist/utils/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { execFileSync } from 'child_process';
|
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import { execFile } from 'child_process';
|
|
9
9
|
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from '../paths.js';
|
|
10
|
+
import { isWindows, commandExists } from './platform.js';
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
// ==================== Helpers ====================
|
|
12
13
|
function ask(rl, question) {
|
|
@@ -18,6 +19,9 @@ async function npmInstallGlobal(pkg) {
|
|
|
18
19
|
}
|
|
19
20
|
catch (e) {
|
|
20
21
|
if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES')) {
|
|
22
|
+
if (isWindows) {
|
|
23
|
+
throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
|
|
24
|
+
}
|
|
21
25
|
await execFileAsync('sudo', ['npm', 'install', '-g', pkg], { timeout: 120000 });
|
|
22
26
|
}
|
|
23
27
|
else {
|
|
@@ -38,6 +42,9 @@ async function sudoExec(cmd, args) {
|
|
|
38
42
|
}
|
|
39
43
|
catch (e) {
|
|
40
44
|
if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES') || e.code === 'EACCES') {
|
|
45
|
+
if (isWindows) {
|
|
46
|
+
throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
|
|
47
|
+
}
|
|
41
48
|
await execFileAsync('sudo', [cmd, ...args], { timeout: 120000, env });
|
|
42
49
|
}
|
|
43
50
|
else {
|
|
@@ -57,15 +64,15 @@ async function checkEnvironment(rl) {
|
|
|
57
64
|
console.log(` ✗ Node.js v${process.versions.node} — 需要 >= 22(node:sqlite 依赖)`);
|
|
58
65
|
// 检测 nvm
|
|
59
66
|
// 检测 bash 是否存在(nvm 和 n 都依赖 bash)
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
execFileSync('which', ['bash'], { encoding: 'utf-8' });
|
|
63
|
-
hasBash = true;
|
|
64
|
-
}
|
|
65
|
-
catch { }
|
|
67
|
+
const hasBash = commandExists('bash');
|
|
66
68
|
if (!hasBash) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
if (isWindows) {
|
|
70
|
+
console.log(' ⚠ Windows 环境,请从 https://nodejs.org 下载安装 Node.js 22+');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(' ⚠ 当前环境没有 bash(Alpine 容器?),无法自动升级 Node.js');
|
|
74
|
+
console.log(' → 请手动升级: apk add nodejs-current 或重建容器使用 node:22-alpine');
|
|
75
|
+
}
|
|
69
76
|
return false;
|
|
70
77
|
}
|
|
71
78
|
const hasNvm = !!process.env.NVM_DIR && fs.existsSync(process.env.NVM_DIR);
|
|
@@ -91,12 +98,7 @@ async function checkEnvironment(rl) {
|
|
|
91
98
|
}
|
|
92
99
|
else {
|
|
93
100
|
// 检测 n
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
execFileSync('which', ['n'], { encoding: 'utf-8' });
|
|
97
|
-
hasN = true;
|
|
98
|
-
}
|
|
99
|
-
catch { }
|
|
101
|
+
const hasN = commandExists('n');
|
|
100
102
|
if (hasN) {
|
|
101
103
|
const answer = (await ask(rl, ' → 是否通过 n 升级到 Node.js 22?[Y/n] ')).trim().toLowerCase();
|
|
102
104
|
if (answer === 'n' || answer === 'no') {
|
|
@@ -138,48 +140,49 @@ async function checkEnvironment(rl) {
|
|
|
138
140
|
}
|
|
139
141
|
// claude CLI >= 2.1.32
|
|
140
142
|
const MIN_CLAUDE_VER = [2, 1, 32];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.log(` ✓ claude CLI v${verMatch[1]}`);
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
console.log(` ✗ claude CLI v${verMatch[1]} — 需要 >= ${MIN_CLAUDE_VER.join('.')}`);
|
|
157
|
-
const answer = (await ask(rl, ' → 是否升级 claude CLI?[Y/n] ')).trim().toLowerCase();
|
|
158
|
-
if (answer === 'n' || answer === 'no') {
|
|
159
|
-
console.log(' 已取消');
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
console.log(' 正在升级 claude CLI...');
|
|
163
|
-
try {
|
|
164
|
-
await npmInstallGlobal('@anthropic-ai/claude-code@latest');
|
|
165
|
-
console.log(' ✓ claude CLI 升级完成');
|
|
143
|
+
const claudeInstalled = commandExists('claude');
|
|
144
|
+
if (claudeInstalled) {
|
|
145
|
+
try {
|
|
146
|
+
const verOutput = execFileSync('claude', ['--version'], { encoding: 'utf-8' }).trim();
|
|
147
|
+
const verMatch = verOutput.match(/^(\d+\.\d+\.\d+)/);
|
|
148
|
+
if (verMatch) {
|
|
149
|
+
const parts = verMatch[1].split('.').map(Number);
|
|
150
|
+
const isOk = parts[0] > MIN_CLAUDE_VER[0]
|
|
151
|
+
|| (parts[0] === MIN_CLAUDE_VER[0] && parts[1] > MIN_CLAUDE_VER[1])
|
|
152
|
+
|| (parts[0] === MIN_CLAUDE_VER[0] && parts[1] === MIN_CLAUDE_VER[1] && parts[2] >= MIN_CLAUDE_VER[2]);
|
|
153
|
+
if (isOk) {
|
|
154
|
+
console.log(` ✓ claude CLI v${verMatch[1]}`);
|
|
166
155
|
}
|
|
167
|
-
|
|
168
|
-
console.log(` ✗
|
|
169
|
-
|
|
156
|
+
else {
|
|
157
|
+
console.log(` ✗ claude CLI v${verMatch[1]} — 需要 >= ${MIN_CLAUDE_VER.join('.')}`);
|
|
158
|
+
const answer = (await ask(rl, ' → 是否升级 claude CLI?[Y/n] ')).trim().toLowerCase();
|
|
159
|
+
if (answer === 'n' || answer === 'no') {
|
|
160
|
+
console.log(' 已取消');
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
console.log(' 正在升级 claude CLI...');
|
|
164
|
+
try {
|
|
165
|
+
await npmInstallGlobal('@anthropic-ai/claude-code@latest');
|
|
166
|
+
console.log(' ✓ claude CLI 升级完成');
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.log(` ✗ 升级失败: ${e.message?.slice(0, 200) || e}`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
170
172
|
}
|
|
171
173
|
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(` ✓ claude CLI (${verOutput})`);
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
catch {
|
|
179
|
+
// claude command exists but --version failed
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
182
|
+
else {
|
|
183
|
+
console.log(' ✗ claude CLI 未找到');
|
|
184
|
+
console.log(' → 请先安装: npm install -g @anthropic-ai/claude-code');
|
|
185
|
+
return false;
|
|
183
186
|
}
|
|
184
187
|
// @anthropic-ai/claude-agent-sdk >= 0.2.75
|
|
185
188
|
let sdkAction = 'ok';
|
|
@@ -226,6 +229,19 @@ async function checkEnvironment(rl) {
|
|
|
226
229
|
}
|
|
227
230
|
// ==================== Shell Profile ====================
|
|
228
231
|
function setupEnvVar(home) {
|
|
232
|
+
if (isWindows) {
|
|
233
|
+
// Windows: use setx to set user environment variable
|
|
234
|
+
try {
|
|
235
|
+
execFileSync('setx', ['EVOLCLAW_HOME', home], { encoding: 'utf-8', stdio: 'pipe' });
|
|
236
|
+
console.log(` ✓ 已设置用户环境变量: EVOLCLAW_HOME=${home}`);
|
|
237
|
+
console.log(' ⚠ 请重新打开终端使其生效');
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
console.log(` ⚠ 设置环境变量失败: ${e.message?.slice(0, 100) || e}`);
|
|
241
|
+
console.log(` → 请手动设置环境变量 EVOLCLAW_HOME=${home}`);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
229
245
|
const exportLine = `export EVOLCLAW_HOME="${home}"`;
|
|
230
246
|
const candidates = [
|
|
231
247
|
path.join(os.homedir(), '.zshrc'),
|
|
@@ -2,6 +2,59 @@
|
|
|
2
2
|
* Markdown 到飞书富文本格式转换工具
|
|
3
3
|
* 使用飞书 post 格式的 md tag 原生渲染 Markdown
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* 计算字符串的显示宽度(CJK 字符按 2 宽度计算)
|
|
7
|
+
*/
|
|
8
|
+
function displayWidth(str) {
|
|
9
|
+
let width = 0;
|
|
10
|
+
for (const ch of str) {
|
|
11
|
+
const code = ch.codePointAt(0);
|
|
12
|
+
// CJK Unified Ideographs, CJK Compatibility, Fullwidth Forms, etc.
|
|
13
|
+
if ((code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified
|
|
14
|
+
(code >= 0x3400 && code <= 0x4DBF) || // CJK Extension A
|
|
15
|
+
(code >= 0xF900 && code <= 0xFAFF) || // CJK Compatibility
|
|
16
|
+
(code >= 0xFF01 && code <= 0xFF60) || // Fullwidth Forms
|
|
17
|
+
(code >= 0x3000 && code <= 0x303F) // CJK Symbols
|
|
18
|
+
) {
|
|
19
|
+
width += 2;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
width += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return width;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 用空格填充字符串到指定显示宽度
|
|
29
|
+
*/
|
|
30
|
+
function padToWidth(str, targetWidth) {
|
|
31
|
+
const current = displayWidth(str);
|
|
32
|
+
const padding = Math.max(0, targetWidth - current);
|
|
33
|
+
return str + ' '.repeat(padding);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 将 Markdown 表格转换为代码块内的对齐文本
|
|
37
|
+
* 飞书 post md tag 不支持标准 markdown 表格,会静默丢弃内容
|
|
38
|
+
* 用代码块 + 等宽对齐保留二维结构
|
|
39
|
+
*/
|
|
40
|
+
function convertTablesToText(text) {
|
|
41
|
+
const tableRegex = /^(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)+)/gm;
|
|
42
|
+
return text.replace(tableRegex, (_match, headerLine, _sep, bodyBlock) => {
|
|
43
|
+
const parseRow = (line) => line.split('|').slice(1, -1).map((c) => c.trim());
|
|
44
|
+
const headers = parseRow(headerLine);
|
|
45
|
+
const rows = bodyBlock.trim().split('\n').map(parseRow);
|
|
46
|
+
// 计算每列最大显示宽度
|
|
47
|
+
const colWidths = headers.map((h, i) => {
|
|
48
|
+
const cellWidths = rows.map(r => displayWidth(r[i] || ''));
|
|
49
|
+
return Math.max(displayWidth(h), ...cellWidths);
|
|
50
|
+
});
|
|
51
|
+
// 构建对齐的表格文本
|
|
52
|
+
const headerStr = headers.map((h, i) => padToWidth(h, colWidths[i])).join(' ');
|
|
53
|
+
const sepStr = colWidths.map(w => '-'.repeat(w)).join(' ');
|
|
54
|
+
const rowStrs = rows.map(r => headers.map((_, i) => padToWidth(r[i] || '', colWidths[i])).join(' '));
|
|
55
|
+
return '```\n' + [headerStr, sepStr, ...rowStrs].join('\n') + '\n```';
|
|
56
|
+
});
|
|
57
|
+
}
|
|
5
58
|
/**
|
|
6
59
|
* 将 Markdown 文本转换为飞书 post 消息格式
|
|
7
60
|
* 利用 md tag 让飞书原生渲染,支持代码高亮、嵌套列表、引用等全部语法
|
|
@@ -9,7 +62,9 @@
|
|
|
9
62
|
export function markdownToFeishuPost(markdown, defaultTitle) {
|
|
10
63
|
const match = markdown.match(/^# (.+)$/m);
|
|
11
64
|
const title = match?.[1] ?? defaultTitle ?? '';
|
|
12
|
-
|
|
65
|
+
let body = match ? markdown.replace(/^# .+\n?/, '') : markdown;
|
|
66
|
+
// 转换飞书不支持的 markdown 表格
|
|
67
|
+
body = convertTablesToText(body);
|
|
13
68
|
return {
|
|
14
69
|
zh_cn: {
|
|
15
70
|
title,
|
|
@@ -32,7 +87,8 @@ export function hasMarkdownSyntax(text) {
|
|
|
32
87
|
/```[\s\S]*?```/, // 代码块
|
|
33
88
|
/\[.*?\]\(.*?\)/, // 链接
|
|
34
89
|
/^[\s]*[-*+]\s/m, // 无序列表
|
|
35
|
-
/^[\s]*\d+\.\s/m // 有序列表
|
|
90
|
+
/^[\s]*\d+\.\s/m, // 有序列表
|
|
91
|
+
/^\|.+\|$/m // 表格
|
|
36
92
|
];
|
|
37
93
|
return markdownPatterns.some(pattern => pattern.test(text));
|
|
38
94
|
}
|
package/dist/utils/permission.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 危险命令黑名单(正则表达式)
|
|
2
2
|
const DANGEROUS_PATTERNS = [
|
|
3
|
+
// Unix
|
|
3
4
|
/\brm\s+-\w*r\w*f/, // rm -rf
|
|
4
5
|
/\bsudo\b/, // sudo
|
|
5
6
|
/\bmkfs\b/, // mkfs (格式化文件系统)
|
|
@@ -8,6 +9,12 @@ const DANGEROUS_PATTERNS = [
|
|
|
8
9
|
/>\s*\/dev\//, // 重定向到设备文件
|
|
9
10
|
/\bshutdown\b/, // 关机
|
|
10
11
|
/\breboot\b/, // 重启
|
|
12
|
+
// Windows
|
|
13
|
+
/\bformat\s+[a-zA-Z]:/i, // format C: (格式化磁盘)
|
|
14
|
+
/\brd\s+\/s/i, // rd /s (递归删除目录)
|
|
15
|
+
/\bdel\s+\/[sfq]/i, // del /f, /s, /q (强制删除)
|
|
16
|
+
/\breg\s+delete/i, // reg delete (删除注册表)
|
|
17
|
+
/\bnet\s+stop/i, // net stop (停止服务)
|
|
11
18
|
];
|
|
12
19
|
/**
|
|
13
20
|
* 权限检查回调函数
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { execFileSync, execFile, spawn } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export const isWindows = process.platform === 'win32';
|
|
8
|
+
/**
|
|
9
|
+
* Encode project path as directory name (Claude SDK convention).
|
|
10
|
+
* Replace all path separators with '-'.
|
|
11
|
+
* e.g. /home/user/project -> -home-user-project
|
|
12
|
+
* C:\Users\project -> C-Users-project
|
|
13
|
+
*/
|
|
14
|
+
export function encodePath(projectPath) {
|
|
15
|
+
return projectPath.replace(/[/\\]/g, '-');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Cross-platform process liveness check.
|
|
19
|
+
*/
|
|
20
|
+
export function isProcessRunning(pid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
// ESRCH = process not found; EPERM = exists but no permission
|
|
27
|
+
return e.code === 'EPERM';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Cross-platform process termination.
|
|
32
|
+
*/
|
|
33
|
+
export function killProcess(pid, force = false) {
|
|
34
|
+
if (isWindows && force) {
|
|
35
|
+
try {
|
|
36
|
+
execFileSync('taskkill', ['/PID', String(pid), '/F']);
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, force ? 'SIGKILL' : 'SIGTERM');
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Cross-platform process search by command line pattern.
|
|
49
|
+
* Returns list of matching PIDs.
|
|
50
|
+
*/
|
|
51
|
+
export function findProcesses(pattern) {
|
|
52
|
+
try {
|
|
53
|
+
if (isWindows) {
|
|
54
|
+
const output = execFileSync('wmic', ['process', 'where', `CommandLine like '%${pattern}%'`, 'get', 'ProcessId'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
55
|
+
return output.split('\n')
|
|
56
|
+
.map(line => parseInt(line.trim(), 10))
|
|
57
|
+
.filter(pid => !isNaN(pid) && pid !== process.pid);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const output = execFileSync('pgrep', ['-f', pattern], { encoding: 'utf-8' }).trim();
|
|
61
|
+
return output ? output.split('\n').map(Number).filter(pid => pid !== process.pid) : [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function getProcessInfo(pid) {
|
|
69
|
+
try {
|
|
70
|
+
if (isWindows) {
|
|
71
|
+
// Use wmic on Windows
|
|
72
|
+
const output = execFileSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'WorkingSetSize,CreationDate'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
73
|
+
const lines = output.trim().split('\n').filter(l => l.trim());
|
|
74
|
+
if (lines.length >= 2) {
|
|
75
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
76
|
+
const memKB = parts[1] ? Math.round(parseInt(parts[1], 10) / 1024) : undefined;
|
|
77
|
+
return { memory: memKB ? `${memKB}` : undefined };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const uptime = execFileSync('ps', ['-p', String(pid), '-o', 'etime='], { encoding: 'utf-8' }).trim();
|
|
82
|
+
const cpu = execFileSync('ps', ['-p', String(pid), '-o', '%cpu='], { encoding: 'utf-8' }).trim();
|
|
83
|
+
const mem = execFileSync('ps', ['-p', String(pid), '-o', 'rss='], { encoding: 'utf-8' }).trim();
|
|
84
|
+
return { uptime, cpu, memory: mem };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Cross-platform command existence check.
|
|
92
|
+
*/
|
|
93
|
+
export function commandExists(cmd) {
|
|
94
|
+
try {
|
|
95
|
+
if (isWindows) {
|
|
96
|
+
execFileSync('where', [cmd], { encoding: 'utf-8', stdio: 'pipe' });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
execFileSync('which', [cmd], { encoding: 'utf-8', stdio: 'pipe' });
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Cross-platform live log tailing (replaces tail -f).
|
|
109
|
+
* Returns an abort function.
|
|
110
|
+
*/
|
|
111
|
+
export function tailFile(filePath) {
|
|
112
|
+
if (!isWindows) {
|
|
113
|
+
// Unix: use tail -f (more efficient)
|
|
114
|
+
const child = spawn('tail', ['-f', filePath], { stdio: 'inherit' });
|
|
115
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
116
|
+
return { abort: () => child.kill() };
|
|
117
|
+
}
|
|
118
|
+
// Windows: Node.js-based implementation
|
|
119
|
+
// Output last 20 lines of existing content
|
|
120
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
121
|
+
const lines = content.split('\n');
|
|
122
|
+
const lastLines = lines.slice(-20);
|
|
123
|
+
process.stdout.write(lastLines.join('\n'));
|
|
124
|
+
let position = fs.statSync(filePath).size;
|
|
125
|
+
const watcher = fs.watch(filePath, () => {
|
|
126
|
+
const stat = fs.statSync(filePath);
|
|
127
|
+
if (stat.size > position) {
|
|
128
|
+
const fd = fs.openSync(filePath, 'r');
|
|
129
|
+
const buffer = Buffer.alloc(stat.size - position);
|
|
130
|
+
fs.readSync(fd, buffer, 0, buffer.length, position);
|
|
131
|
+
fs.closeSync(fd);
|
|
132
|
+
process.stdout.write(buffer.toString('utf-8'));
|
|
133
|
+
position = stat.size;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return { abort: () => watcher.close() };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolve file path from import.meta.url (cross-platform safe).
|
|
140
|
+
* Replaces unsafe `new URL('.', import.meta.url).pathname` usage.
|
|
141
|
+
*/
|
|
142
|
+
export function dirFromImportMeta(importMetaUrl) {
|
|
143
|
+
return path.dirname(fileURLToPath(importMetaUrl));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if current file is the main entry script (cross-platform safe).
|
|
147
|
+
* Replaces unsafe `import.meta.url === \`file://\${process.argv[1]}\`` check.
|
|
148
|
+
*/
|
|
149
|
+
export function isMainScript(importMetaUrl) {
|
|
150
|
+
const argv1 = process.argv[1];
|
|
151
|
+
if (!argv1)
|
|
152
|
+
return false;
|
|
153
|
+
try {
|
|
154
|
+
const selfPath = fileURLToPath(importMetaUrl);
|
|
155
|
+
const argvPath = fs.realpathSync(argv1);
|
|
156
|
+
return selfPath === argvPath || fs.realpathSync(selfPath) === argvPath;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Register graceful shutdown signal handlers (cross-platform safe).
|
|
164
|
+
*/
|
|
165
|
+
export function onShutdown(callback) {
|
|
166
|
+
process.on('SIGINT', callback);
|
|
167
|
+
// SIGTERM is not fully supported on Windows, but Node.js can still emit it
|
|
168
|
+
// in some scenarios (e.g., process managers), so register it anyway
|
|
169
|
+
process.on('SIGTERM', callback);
|
|
170
|
+
if (isWindows) {
|
|
171
|
+
// On Windows, also handle SIGHUP for graceful shutdown
|
|
172
|
+
// when the process is terminated via Task Manager or similar
|
|
173
|
+
process.on('SIGHUP', callback);
|
|
174
|
+
}
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolclaw",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"evolclaw": "./
|
|
8
|
+
"evolclaw": "./dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/",
|
|
12
12
|
"!dist/experimental/",
|
|
13
|
-
"bin/",
|
|
14
13
|
"data/evolclaw.sample.json"
|
|
15
14
|
],
|
|
16
15
|
"scripts": {
|
|
17
16
|
"dev": "tsx watch src/index.ts",
|
|
18
|
-
"build": "tsc",
|
|
17
|
+
"build": "tsc && node -e \"const f='dist/cli.js',c=require('fs').readFileSync(f,'utf8');if(!c.startsWith('#!'))require('fs').writeFileSync(f,'#!/usr/bin/env node\\n'+c)\" && node -e \"try{require('child_process').execFileSync('chmod',['+x','dist/cli.js'])}catch{}\"",
|
|
19
18
|
"start": "node dist/index.js",
|
|
20
19
|
"test": "vitest run",
|
|
21
20
|
"test:watch": "vitest",
|
package/bin/evolclaw
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { dirname, join } from 'path';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
8
|
-
|
|
9
|
-
const { main } = await import(join(__dirname, '..', 'dist', 'cli.js'));
|
|
10
|
-
main(process.argv.slice(2));
|