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.
@@ -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.replace(/\//g, '-');
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:/path/to/file.txt] 系统会自动上传并发送。',
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
- processor.registerChannel(wechatAdapter);
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
- await instance.connect();
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
- return path.resolve(new URL('.', import.meta.url).pathname, '..');
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
  }
@@ -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
- let hasBash = false;
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
- console.log(' ⚠ 当前环境没有 bash(Alpine 容器?),无法自动升级 Node.js');
68
- console.log(' 请手动升级: apk add nodejs-current 或重建容器使用 node:22-alpine');
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
- let hasN = false;
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
- let claudeInstalled = false;
142
- try {
143
- execFileSync('which', ['claude'], { encoding: 'utf-8' });
144
- claudeInstalled = true;
145
- const verOutput = execFileSync('claude', ['--version'], { encoding: 'utf-8' }).trim();
146
- const verMatch = verOutput.match(/^(\d+\.\d+\.\d+)/);
147
- if (verMatch) {
148
- const parts = verMatch[1].split('.').map(Number);
149
- const isOk = parts[0] > MIN_CLAUDE_VER[0]
150
- || (parts[0] === MIN_CLAUDE_VER[0] && parts[1] > MIN_CLAUDE_VER[1])
151
- || (parts[0] === MIN_CLAUDE_VER[0] && parts[1] === MIN_CLAUDE_VER[1] && parts[2] >= MIN_CLAUDE_VER[2]);
152
- if (isOk) {
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
- catch (e) {
168
- console.log(` ✗ 升级失败: ${e.message?.slice(0, 200) || e}`);
169
- return false;
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
- else {
174
- console.log(` ✓ claude CLI (${verOutput})`);
178
+ catch {
179
+ // claude command exists but --version failed
175
180
  }
176
181
  }
177
- catch {
178
- if (!claudeInstalled) {
179
- console.log(' claude CLI 未找到');
180
- console.log(' → 请先安装: npm install -g @anthropic-ai/claude-code');
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
- const body = match ? markdown.replace(/^# .+\n?/, '') : markdown;
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
  }
@@ -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",
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": "./bin/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));