evolclaw 2.0.4 → 2.0.6

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/dist/cli.js CHANGED
@@ -1,19 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import { spawn, execFileSync, execFile } from 'child_process';
4
+ import { spawn, execFile } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
7
7
  import { cmdInit } from './utils/init.js';
8
8
  import { cmdInitWechat } from './utils/init-wechat.js';
9
9
  import { cmdInitFeishu } from './utils/init-feishu.js';
10
+ import * as platform from './utils/platform.js';
11
+ // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
12
+ process.removeAllListeners('warning');
13
+ process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
14
+ return; process.stderr.write((w.stack ?? String(w)) + '\n'); });
10
15
  const execFileAsync = promisify(execFile);
11
16
  // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
12
17
  function cleanEnv() {
13
18
  for (const key of [
14
19
  'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT',
15
20
  'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
16
- 'CLAUDE_CONFIG_DIR', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'
21
+ 'CLAUDE_CONFIG_DIR',
17
22
  ]) {
18
23
  delete process.env[key];
19
24
  }
@@ -22,14 +27,11 @@ function isRunning(pidFile) {
22
27
  if (!fs.existsSync(pidFile))
23
28
  return null;
24
29
  const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
25
- try {
26
- process.kill(pid, 0);
30
+ if (platform.isProcessRunning(pid)) {
27
31
  return pid;
28
32
  }
29
- catch {
30
- fs.unlinkSync(pidFile);
31
- return null;
32
- }
33
+ fs.unlinkSync(pidFile);
34
+ return null;
33
35
  }
34
36
  function rotateLogs(logDir) {
35
37
  if (!fs.existsSync(logDir))
@@ -149,7 +151,7 @@ function showHistory(statsFile) {
149
151
  console.log('==================================================');
150
152
  }
151
153
  // ==================== Commands ====================
152
- function cmdStart() {
154
+ async function cmdStart() {
153
155
  const p = resolvePaths();
154
156
  ensureDataDirs();
155
157
  // 检查配置文件
@@ -166,24 +168,17 @@ function cmdStart() {
166
168
  }
167
169
  // 检查是否有残留进程(PID 文件已丢失但进程还在)
168
170
  let hasOrphan = false;
169
- try {
170
- const output = execFileSync('pgrep', ['-f', 'node.*dist/index.js'], { encoding: 'utf-8' }).trim();
171
- if (output) {
172
- const pids = output.split('\n');
173
- console.log(`⚠ 发现 ${pids.length} 个残留进程,正在清理...`);
174
- for (const p of pids) {
175
- try {
176
- process.kill(parseInt(p, 10));
177
- }
178
- catch { }
179
- }
180
- hasOrphan = true;
171
+ const orphanPids = platform.findProcesses('node.*dist/index.js');
172
+ if (orphanPids.length > 0) {
173
+ console.log(`⚠ 发现 ${orphanPids.length} 个残留进程,正在清理...`);
174
+ for (const p of orphanPids) {
175
+ platform.killProcess(p);
181
176
  }
177
+ hasOrphan = true;
182
178
  }
183
- catch { }
184
179
  // 如果清理了残留进程,等待它们退出
185
180
  if (hasOrphan) {
186
- execFileSync('sleep', ['2']);
181
+ await sleep(2000);
187
182
  }
188
183
  console.log('🚀 Starting EvolClaw...');
189
184
  rotateLogs(p.logs);
@@ -197,7 +192,7 @@ function cmdStart() {
197
192
  const out = fs.openSync(stdoutLog, 'a');
198
193
  const err = fs.openSync(stdoutLog, 'a');
199
194
  const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
200
- const child = spawn('node', [appMain], {
195
+ const child = spawn('node', ['--no-warnings=ExperimentalWarning', appMain], {
201
196
  detached: true,
202
197
  stdio: ['ignore', out, err],
203
198
  env: {
@@ -212,19 +207,7 @@ function cmdStart() {
212
207
  // 等待 ready signal(最多 15 秒)
213
208
  const startTime = Date.now();
214
209
  const checkReady = () => {
215
- // 进程已退出
216
- if (!isRunning(p.pid)) {
217
- console.log('❌ Failed to start EvolClaw');
218
- console.log('');
219
- console.log('📝 Error details (last 10 lines of stdout):');
220
- if (fs.existsSync(stdoutLog)) {
221
- const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
222
- console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
223
- }
224
- process.exit(1);
225
- return;
226
- }
227
- // ready signal 出现
210
+ // ready signal 出现(优先检查,避免 Windows 上 isRunning 误判)
228
211
  if (fs.existsSync(p.readySignal)) {
229
212
  const pid = isRunning(p.pid);
230
213
  console.log(`✓ EvolClaw started successfully (PID: ${pid})`);
@@ -249,6 +232,21 @@ function cmdStart() {
249
232
  process.exit(1);
250
233
  return;
251
234
  }
235
+ // 进程已退出且无 ready signal
236
+ if (!isRunning(p.pid)) {
237
+ // 给进程一点时间写 ready signal(可能刚好在写入中)
238
+ if (Date.now() - startTime > 3000) {
239
+ console.log('❌ Failed to start EvolClaw');
240
+ console.log('');
241
+ console.log('📝 Error details (last 10 lines of stdout):');
242
+ if (fs.existsSync(stdoutLog)) {
243
+ const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
244
+ console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
245
+ }
246
+ process.exit(1);
247
+ return;
248
+ }
249
+ }
252
250
  setTimeout(checkReady, 500);
253
251
  };
254
252
  setTimeout(checkReady, 1000);
@@ -261,15 +259,12 @@ async function stopAndWait(pidFile) {
261
259
  if (!pid)
262
260
  return;
263
261
  console.log(`🛑 Stopping EvolClaw (PID: ${pid})...`);
264
- process.kill(pid);
262
+ platform.killProcess(pid);
265
263
  await new Promise((resolve) => {
266
264
  let waited = 0;
267
265
  const check = setInterval(() => {
268
266
  waited++;
269
- try {
270
- process.kill(pid, 0);
271
- }
272
- catch {
267
+ if (!platform.isProcessRunning(pid)) {
273
268
  clearInterval(check);
274
269
  try {
275
270
  fs.unlinkSync(pidFile);
@@ -281,10 +276,7 @@ async function stopAndWait(pidFile) {
281
276
  }
282
277
  if (waited >= 10) {
283
278
  clearInterval(check);
284
- try {
285
- process.kill(pid, 9);
286
- }
287
- catch { }
279
+ platform.killProcess(pid, true);
288
280
  try {
289
281
  fs.unlinkSync(pidFile);
290
282
  }
@@ -318,12 +310,13 @@ async function cmdStatus() {
318
310
  console.log('');
319
311
  console.log('📊 Process Info:');
320
312
  try {
321
- const uptime = execFileSync('ps', ['-p', String(pid), '-o', 'etime='], { encoding: 'utf-8' }).trim();
322
- const cpu = execFileSync('ps', ['-p', String(pid), '-o', '%cpu='], { encoding: 'utf-8' }).trim();
323
- const mem = execFileSync('ps', ['-p', String(pid), '-o', 'rss='], { encoding: 'utf-8' }).trim();
324
- console.log(` Uptime: ${uptime}`);
325
- console.log(` CPU: ${cpu}%`);
326
- console.log(` Memory: ${mem} KB`);
313
+ const info = platform.getProcessInfo(pid);
314
+ if (info.uptime)
315
+ console.log(` Uptime: ${info.uptime}`);
316
+ if (info.cpu)
317
+ console.log(` CPU: ${info.cpu}%`);
318
+ if (info.memory)
319
+ console.log(` Memory: ${info.memory} KB`);
327
320
  }
328
321
  catch { }
329
322
  console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
@@ -338,14 +331,16 @@ async function cmdStatus() {
338
331
  console.log('');
339
332
  console.log('📦 Sessions & Projects:');
340
333
  try {
341
- const output = execFileSync('sqlite3', [p.db,
342
- 'SELECT count(*) FROM sessions; SELECT count(*) FROM sessions WHERE is_active=1; SELECT count(DISTINCT channel_id) FROM sessions; SELECT count(DISTINCT project_path) FROM sessions;'
343
- ], { encoding: 'utf-8' }).trim().split('\n');
344
- if (output.length >= 4) {
345
- console.log(` Total sessions: ${output[0]} (active: ${output[1]})`);
346
- console.log(` Unique chats: ${output[2]}`);
347
- console.log(` Projects: ${output[3]}`);
348
- }
334
+ const Database = await import('node:sqlite');
335
+ const db = new Database.DatabaseSync(p.db);
336
+ const totalSessions = db.prepare('SELECT count(*) as cnt FROM sessions').get();
337
+ const activeSessions = db.prepare('SELECT count(*) as cnt FROM sessions WHERE is_active=1').get();
338
+ const uniqueChats = db.prepare('SELECT count(DISTINCT channel_id) as cnt FROM sessions').get();
339
+ const projects = db.prepare('SELECT count(DISTINCT project_path) as cnt FROM sessions').get();
340
+ db.close();
341
+ console.log(` Total sessions: ${totalSessions.cnt} (active: ${activeSessions.cnt})`);
342
+ console.log(` Unique chats: ${uniqueChats.cnt}`);
343
+ console.log(` Projects: ${projects.cnt}`);
349
344
  }
350
345
  catch { }
351
346
  }
@@ -385,7 +380,42 @@ async function cmdStatus() {
385
380
  }
386
381
  if (config.channels?.wechat?.token) {
387
382
  const tokenPreview = config.channels.wechat.token.slice(0, 20);
388
- console.log(` WeChat: Configured (Token: ${tokenPreview}...)`);
383
+ // Validate token by calling getconfig API
384
+ try {
385
+ const baseUrl = (config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
386
+ const body = JSON.stringify({ base_info: { channel_version: '1.0.0' } });
387
+ const uint32 = (await import('node:crypto')).default.randomBytes(4).readUInt32BE(0);
388
+ const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
389
+ const res = await fetch(`${baseUrl}/ilink/bot/getconfig`, {
390
+ method: 'POST',
391
+ headers: {
392
+ 'Content-Type': 'application/json',
393
+ 'AuthorizationType': 'ilink_bot_token',
394
+ 'Authorization': `Bearer ${config.channels.wechat.token.trim()}`,
395
+ 'X-WECHAT-UIN': wechatUin,
396
+ },
397
+ body,
398
+ signal: AbortSignal.timeout(10_000),
399
+ });
400
+ const resp = JSON.parse(await res.text());
401
+ const isExpired = resp.errcode === -14 || resp.ret === -14;
402
+ if (isExpired) {
403
+ console.log(` WeChat: ✗ Token expired (Token: ${tokenPreview}...)`);
404
+ console.log(' Run: evolclaw init wechat && evolclaw restart');
405
+ }
406
+ else {
407
+ console.log(` WeChat: ✓ Connected (Token: ${tokenPreview}...)`);
408
+ }
409
+ }
410
+ catch (e) {
411
+ const msg = e.message || '';
412
+ if (msg.includes('ETIMEDOUT') || msg.includes('ENETUNREACH') || msg.includes('ENOTFOUND')) {
413
+ console.log(` WeChat: ✗ Connection timeout (Token: ${tokenPreview}...)`);
414
+ }
415
+ else {
416
+ console.log(` WeChat: ✓ Configured (Token: ${tokenPreview}...)`);
417
+ }
418
+ }
389
419
  }
390
420
  else {
391
421
  console.log(' WeChat: - Not configured');
@@ -434,8 +464,16 @@ function cmdLogs() {
434
464
  console.log(`❌ Log file not found: ${mainLog}`);
435
465
  process.exit(1);
436
466
  }
437
- const child = spawn('tail', ['-f', mainLog], { stdio: 'inherit' });
438
- child.on('exit', (code) => process.exit(code || 0));
467
+ if (platform.isWindows) {
468
+ // Windows: use fs.watch for live tail
469
+ const tail = platform.tailFile(mainLog);
470
+ platform.onShutdown(() => tail.abort());
471
+ }
472
+ else {
473
+ // Unix: use tail -f
474
+ const child = spawn('tail', ['-f', mainLog], { stdio: 'inherit' });
475
+ child.on('exit', (code) => process.exit(code || 0));
476
+ }
439
477
  }
440
478
  /**
441
479
  * restart-monitor: 内部命令,由 /restart 命令调用
@@ -468,10 +506,7 @@ async function cmdRestartMonitor() {
468
506
  let waited = 0;
469
507
  const interval = setInterval(() => {
470
508
  waited++;
471
- try {
472
- process.kill(oldPid, 0);
473
- }
474
- catch {
509
+ if (!platform.isProcessRunning(oldPid)) {
475
510
  clearInterval(interval);
476
511
  log(`Process ${oldPid} has exited`);
477
512
  resolve();
@@ -480,10 +515,7 @@ async function cmdRestartMonitor() {
480
515
  if (waited >= 30) {
481
516
  clearInterval(interval);
482
517
  log('ERROR: Process still running after 30s, force killing');
483
- try {
484
- process.kill(oldPid, 9);
485
- }
486
- catch { }
518
+ platform.killProcess(oldPid, true);
487
519
  resolve();
488
520
  }
489
521
  }, 1000);
@@ -559,7 +591,7 @@ async function spawnAndWaitReady(p, log, timeout) {
559
591
  const out = fs.openSync(stdoutLog, 'a');
560
592
  const err = fs.openSync(stdoutLog, 'a');
561
593
  const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
562
- const child = spawn('node', [appMain], {
594
+ const child = spawn('node', ['--no-warnings=ExperimentalWarning', appMain], {
563
595
  detached: true,
564
596
  stdio: ['ignore', out, err],
565
597
  env: {
@@ -590,10 +622,7 @@ async function spawnAndWaitReady(p, log, timeout) {
590
622
  // 超时后杀掉进程
591
623
  const pid = isRunning(p.pid);
592
624
  if (pid) {
593
- try {
594
- process.kill(pid);
595
- }
596
- catch { }
625
+ platform.killProcess(pid);
597
626
  try {
598
627
  fs.unlinkSync(p.pid);
599
628
  }
@@ -772,7 +801,7 @@ export async function main(args) {
772
801
  }
773
802
  break;
774
803
  case 'start':
775
- cmdStart();
804
+ await cmdStart();
776
805
  break;
777
806
  case 'stop':
778
807
  await cmdStop();
@@ -811,10 +840,6 @@ Environment:
811
840
  }
812
841
  }
813
842
  // 直接运行时自动执行(node dist/cli.js ...)
814
- // realpath 解析 symlink,否则 npm link 的 bin 路径与实际文件路径不匹配
815
- const __selfUrl = import.meta.url;
816
- const __argv1 = process.argv[1];
817
- if (__argv1 && (__selfUrl === `file://${__argv1}` ||
818
- __selfUrl === `file://${fs.realpathSync(__argv1)}`)) {
843
+ if (platform.isMainScript(import.meta.url)) {
819
844
  main(process.argv.slice(2));
820
845
  }
@@ -6,6 +6,7 @@ import os from 'os';
6
6
  import { MessageStream } from './message-stream.js';
7
7
  import { logger } from '../utils/logger.js';
8
8
  import { canUseTool } from '../utils/permission.js';
9
+ import { encodePath } from '../utils/platform.js';
9
10
  export class AgentRunner {
10
11
  apiKey;
11
12
  model;
@@ -59,8 +60,8 @@ export class AgentRunner {
59
60
  // 验证会话文件是否存在且有效(仅在非安全模式且有 claudeSessionId 时)
60
61
  if (claudeSessionId && !skipResume) {
61
62
  const homeDir = os.homedir();
62
- const encodedPath = projectPath.replace(/\//g, '-');
63
- const sessionFile = path.join(homeDir, '.claude', 'projects', encodedPath, `${claudeSessionId}.jsonl`);
63
+ const encodedProjectPath = encodePath(projectPath);
64
+ const sessionFile = path.join(homeDir, '.claude', 'projects', encodedProjectPath, `${claudeSessionId}.jsonl`);
64
65
  let isValid = false;
65
66
  if (fs.existsSync(sessionFile)) {
66
67
  try {
@@ -433,7 +433,7 @@ export class CommandHandler {
433
433
  const sessionKey = `${channel}-${channelId}`;
434
434
  const processingProject = this.messageQueue.getProcessingProject(sessionKey);
435
435
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
436
- const normalizePath = (p) => p.replace(/\/+$/, '');
436
+ const normalizePath = (p) => p.replace(/[/\\]+$/, '');
437
437
  for (const [name, projectPath] of Object.entries(this.projects)) {
438
438
  const isCurrent = session?.projectPath === projectPath;
439
439
  const prefix = isCurrent ? ' ✓' : ' ';
@@ -596,7 +596,7 @@ export class CommandHandler {
596
596
  logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
597
597
  }
598
598
  const isGroup = await this.isGroupChat(channel, channelId);
599
- const cliSessions = isGroup
599
+ const cliSessions = (isGroup || !isAdmin)
600
600
  ? []
601
601
  : await this.sessionManager.scanCliSessions(session.projectPath);
602
602
  const dbSessionIds = new Set(currentProjectSessions.map(s => s.claudeSessionId).filter(Boolean));
@@ -655,7 +655,7 @@ export class CommandHandler {
655
655
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
656
656
  }
657
657
  const isGroup = await this.isGroupChat(channel, channelId);
658
- if (!targetSession && sessionName.length === 8 && !isGroup) {
658
+ if (!targetSession && sessionName.length === 8 && !isGroup && isAdmin) {
659
659
  const projectPaths = Object.values(this.projects);
660
660
  if (session) {
661
661
  projectPaths.unshift(session.projectPath);
@@ -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),
172
+ };
173
+ const wechatOptions = {
174
+ systemPromptAppend: '[系统功能] 你可以发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt]',
175
+ fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
161
176
  };
162
- processor.registerChannel(wechatAdapter);
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 消息处理
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
  }