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/README.md +18 -18
- package/dist/channels/feishu.js +68 -10
- package/dist/channels/wechat.js +238 -5
- package/dist/cli.js +107 -82
- 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 +21 -6
- 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 +2 -2
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,
|
|
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',
|
|
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
|
-
|
|
26
|
-
process.kill(pid, 0);
|
|
30
|
+
if (platform.isProcessRunning(pid)) {
|
|
27
31
|
return pid;
|
|
28
32
|
}
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
+
platform.killProcess(pid);
|
|
265
263
|
await new Promise((resolve) => {
|
|
266
264
|
let waited = 0;
|
|
267
265
|
const check = setInterval(() => {
|
|
268
266
|
waited++;
|
|
269
|
-
|
|
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
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
63
|
-
const sessionFile = path.join(homeDir, '.claude', 'projects',
|
|
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
|
|
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),
|
|
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
|
-
|
|
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
|
}
|