@iamsamyiok/agents-chat 3.30.0 → 3.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +26 -0
- package/app/lib/agent.js +4 -1
- package/app/lib/cards.js +2 -0
- package/app/lib/notify.js +26 -0
- package/app/lib/oc.js +10 -3
- package/app/lib/orchestrator.js +31 -26
- package/app/lib/store.js +91 -1
- package/app/lib/worktree.js +192 -0
- package/app/mock/mock-agent.js +7 -2
- package/app/public/cards.html +20 -1
- package/app/public/icon.svg +6 -0
- package/app/public/index.html +259 -12
- package/app/public/manifest.json +17 -0
- package/app/server.js +119 -8
- package/bin/agents-chat.js +107 -0
- package/package.json +4 -3
- package/scripts/build-exe.js +13 -2
package/app/server.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 启动:node app/server.js [--port 3456]
|
|
3
3
|
const APP_VERSION = require('../package.json').version; // 单源版本:与 package.json 始终一致(页面互检/更新检查共用)
|
|
4
4
|
const { checkLatest, UPDATE_COMMAND } = require('./lib/updatecheck');
|
|
5
|
+
const { notifyDone } = require('./lib/notify');
|
|
5
6
|
const http = require('http');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
@@ -326,6 +327,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
326
327
|
serveStatic(res, path.join(PUBLIC_DIR, 'cards.html'));
|
|
327
328
|
return;
|
|
328
329
|
}
|
|
330
|
+
if (req.method === 'GET' && p === '/manifest.json') {
|
|
331
|
+
// PWA 清单(添加到主屏幕/手机访问)
|
|
332
|
+
serveStatic(res, path.join(PUBLIC_DIR, 'manifest.json'));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
329
335
|
if (req.method === 'GET' && p.startsWith('/static/')) {
|
|
330
336
|
const safe = path.normalize(p.slice('/static/'.length)).replace(/^(\.\.[/\\])+/, '');
|
|
331
337
|
serveStatic(res, path.join(PUBLIC_DIR, safe));
|
|
@@ -360,6 +366,30 @@ const server = http.createServer(async (req, res) => {
|
|
|
360
366
|
return;
|
|
361
367
|
}
|
|
362
368
|
|
|
369
|
+
// ---------- 历史检索 / 用量 / 空间 ----------
|
|
370
|
+
if (p === '/api/search' && req.method === 'GET') {
|
|
371
|
+
const q = String(parsed.query.q || '');
|
|
372
|
+
const limit = Math.min(200, Math.max(1, Number(parsed.query.limit) || 50));
|
|
373
|
+
json(res, 200, { success: true, results: store.searchMessages(q, { limit }) });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (p === '/api/usage' && req.method === 'GET') {
|
|
377
|
+
json(res, 200, { success: true, usage: store.getUsageStats() });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (p === '/api/data/stats' && req.method === 'GET') {
|
|
381
|
+
json(res, 200, { success: true, stats: store.dataStats(), dataDir: store.DATA_DIR });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (p === '/api/data/prune' && req.method === 'POST') {
|
|
385
|
+
// 手动清理:与每日自动清理同一套逻辑(已完结任务/过期会话/产出存档等),返回各项清理量
|
|
386
|
+
const body = await readBody(req);
|
|
387
|
+
const days = Math.max(1, Number(body.days) || PRUNE_DAYS);
|
|
388
|
+
const stat = store.pruneOldData(days);
|
|
389
|
+
json(res, 200, { success: true, days, stat });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
363
393
|
if (p === '/api/stop' && req.method === 'POST') {
|
|
364
394
|
// 手动停止:kill 对应作用域的全部子进程;编排循环检测令牌后跳过剩余工作
|
|
365
395
|
const body = await readBody(req);
|
|
@@ -599,8 +629,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
599
629
|
}
|
|
600
630
|
const mode = body.mode === 'scheduled' ? 'scheduled' : 'sequential';
|
|
601
631
|
const runner = body.runner === 'solo' ? 'solo' : '';
|
|
602
|
-
const { added, warnings } = store.importTasks(body.text, mode, runner, body.model);
|
|
603
|
-
|
|
632
|
+
const { added, warnings, addedTasks } = store.importTasks(body.text, mode, runner, body.model);
|
|
633
|
+
// Git 隔离执行:为每个新任务创建独立 worktree(工作目录是 git 仓库时生效)
|
|
634
|
+
let isolated = 0;
|
|
635
|
+
if (body.isolated && Array.isArray(addedTasks)) {
|
|
636
|
+
const worktree = require('./lib/worktree');
|
|
637
|
+
for (const t of addedTasks) {
|
|
638
|
+
const wt = await worktree.createForTask(t.id);
|
|
639
|
+
if (wt) { store.updateTask(t.id, { worktree: wt }); isolated++; }
|
|
640
|
+
}
|
|
641
|
+
if (!isolated) warnings.push('统一工作目录不是 Git 仓库(或没有任何提交),任务将按共享目录执行');
|
|
642
|
+
}
|
|
643
|
+
json(res, 200, { success: true, added, warnings, mode, runner, isolated, tasks: store.getTasks() });
|
|
604
644
|
return;
|
|
605
645
|
}
|
|
606
646
|
|
|
@@ -617,14 +657,62 @@ const server = http.createServer(async (req, res) => {
|
|
|
617
657
|
}
|
|
618
658
|
|
|
619
659
|
if (p === '/api/tasks/delete' && req.method === 'POST') {
|
|
620
|
-
//
|
|
660
|
+
// 删除任务及其会话消息;Git 隔离区一并清理
|
|
621
661
|
const body = await readBody(req);
|
|
622
662
|
if (!body.id) { json(res, 400, { success: false, error: 'id 不能为空' }); return; }
|
|
663
|
+
const t = store.getTasks().find(x => x.id === String(body.id));
|
|
664
|
+
if (t && t.worktree) {
|
|
665
|
+
require('./lib/worktree').removeForTask(t.worktree).catch(() => {});
|
|
666
|
+
}
|
|
623
667
|
store.deleteTask(String(body.id));
|
|
624
668
|
json(res, 200, { success: true, tasks: store.getTasks() });
|
|
625
669
|
return;
|
|
626
670
|
}
|
|
627
671
|
|
|
672
|
+
// ---------- Git 隔离区:改动查看 / 合并 / 丢弃 ----------
|
|
673
|
+
if (p === '/api/tasks/diff' && req.method === 'GET') {
|
|
674
|
+
const t = store.getTasks().find(x => x.id === String(parsed.query.id || ''));
|
|
675
|
+
if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
|
|
676
|
+
try {
|
|
677
|
+
const d = await require('./lib/worktree').diff(t.worktree, { withPatch: true });
|
|
678
|
+
json(res, 200, { success: true, task: { id: t.id, title: t.title, status: t.status }, diff: d });
|
|
679
|
+
} catch (e) {
|
|
680
|
+
json(res, 500, { success: false, error: String((e && e.message) || e) });
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (p === '/api/tasks/merge' && req.method === 'POST') {
|
|
686
|
+
const body = await readBody(req);
|
|
687
|
+
const t = store.getTasks().find(x => x.id === String(body.id || ''));
|
|
688
|
+
if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
|
|
689
|
+
if (runLocks.tasks) { json(res, 409, { success: false, error: '任务批次执行中,暂不能合并' }); return; }
|
|
690
|
+
try {
|
|
691
|
+
const r = await require('./lib/worktree').mergeToMain(t.worktree, t.title);
|
|
692
|
+
if (r.ok) store.updateTask(t.id, { mergedAt: Date.now() });
|
|
693
|
+
json(res, 200, { success: r.ok, already: !!r.already, error: r.error || '' });
|
|
694
|
+
} catch (e) {
|
|
695
|
+
json(res, 500, { success: false, error: String((e && e.message) || e) });
|
|
696
|
+
}
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (p === '/api/tasks/discard' && req.method === 'POST') {
|
|
701
|
+
// 丢弃隔离区:删除 worktree 与分支,任务保留(可重跑重建)
|
|
702
|
+
const body = await readBody(req);
|
|
703
|
+
const t = store.getTasks().find(x => x.id === String(body.id || ''));
|
|
704
|
+
if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
|
|
705
|
+
if (runLocks.tasks) { json(res, 409, { success: false, error: '任务批次执行中,暂不能丢弃' }); return; }
|
|
706
|
+
try {
|
|
707
|
+
const r = await require('./lib/worktree').removeForTask(t.worktree);
|
|
708
|
+
if (r.ok) store.updateTask(t.id, { worktree: null });
|
|
709
|
+
json(res, 200, { success: r.ok, error: r.error || '' });
|
|
710
|
+
} catch (e) {
|
|
711
|
+
json(res, 500, { success: false, error: String((e && e.message) || e) });
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
628
716
|
if (p === '/api/tasks/run' && req.method === 'POST') {
|
|
629
717
|
// 顺序执行任务:scope='solo' 时由 OpenCode 单体逐个执行(每个任务独立一次性对话);
|
|
630
718
|
// 默认(群聊)每个任务 = 独立会话 + 一次完整管家调度
|
|
@@ -656,6 +744,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
656
744
|
} else {
|
|
657
745
|
await executeTaskBatch(selected, send, myToken);
|
|
658
746
|
}
|
|
747
|
+
// 群聊批次整体完成通知(单任务通知由 orchestrator 消息流承担,批次级聚合在此)
|
|
748
|
+
notifyDone({ kind: 'batch', title: `${soloScope ? '单聊' : '群聊'}任务批次(${selected.length} 个)`, status: 'done', snippet: selected.map(t => t.title).join('、') });
|
|
659
749
|
} catch (err) {
|
|
660
750
|
console.error('[tasks/run] 编排异常:', err && (err.stack || err));
|
|
661
751
|
send({ type: 'error', content: `任务编排异常:${err && err.message || err}` });
|
|
@@ -1282,6 +1372,10 @@ function shutdown(code) {
|
|
|
1282
1372
|
process.exit(code);
|
|
1283
1373
|
}
|
|
1284
1374
|
process.on('SIGINT', () => shutdown(0));
|
|
1375
|
+
// 兜底:async 路由内的同步异常会以 unhandledRejection 形式出现——记录日志防静默丢错(请求层应自行捕获)
|
|
1376
|
+
process.on('unhandledRejection', (err) => {
|
|
1377
|
+
console.error('[unhandledRejection]', err && (err.stack || err));
|
|
1378
|
+
});
|
|
1285
1379
|
process.on('SIGTERM', () => shutdown(0));
|
|
1286
1380
|
// Windows 关闭控制台窗口(CTRL_CLOSE_EVENT)在 Node 上映射为 SIGHUP,系统给约 5 秒处理宽限:
|
|
1287
1381
|
// 同步 killTree 足够完成,正在执行的 AI 子进程不再变孤儿
|
|
@@ -1308,7 +1402,7 @@ async function executeTaskBatch(selected, send, myToken) {
|
|
|
1308
1402
|
const persist = (m) => store.addMessage({ ...m, timestamp: new Date().toISOString() });
|
|
1309
1403
|
await runTasks(
|
|
1310
1404
|
selected, butler, subAgents,
|
|
1311
|
-
{ getHistory: (tid) => buildHistoryText(tid), resolveAssign, scope: 'tasks', isStopped: () => stopTokens.tasks !== myToken, approval: approvalSetting(), requestApproval: makeRequestApproval() },
|
|
1405
|
+
{ getHistory: (tid) => buildHistoryText(tid), resolveAssign, scope: 'tasks', isStopped: () => stopTokens.tasks !== myToken, approval: approvalSetting(), requestApproval: makeRequestApproval(), taskCwd: (task) => (task.worktree && task.worktree.dir) || '' },
|
|
1312
1406
|
send, persist,
|
|
1313
1407
|
// 任务会话首条消息:任务本身(用户视角)
|
|
1314
1408
|
(task) => {
|
|
@@ -1353,6 +1447,7 @@ async function executeSoloTaskBatch(selected, send, myToken) {
|
|
|
1353
1447
|
if (runner.kind === 'missing') {
|
|
1354
1448
|
store.updateTask(task.id, { status: 'failed', result: missingHint(runner).slice(0, 2000) });
|
|
1355
1449
|
send({ type: 'task_done', taskId: task.id, title: task.title, status: 'failed' });
|
|
1450
|
+
notifyDone({ kind: 'task', title: task.title, status: 'failed', snippet: missingHint(runner) });
|
|
1356
1451
|
return '';
|
|
1357
1452
|
}
|
|
1358
1453
|
|
|
@@ -1360,7 +1455,11 @@ async function executeSoloTaskBatch(selected, send, myToken) {
|
|
|
1360
1455
|
const order = [];
|
|
1361
1456
|
let doneError = '';
|
|
1362
1457
|
let sesId = ocSessionId || '';
|
|
1363
|
-
|
|
1458
|
+
// 任务隔离 worktree:整任务执行期间注入 cwd 上下文(ALS,与并发请求互不干扰)
|
|
1459
|
+
const worktreeMod = require('./lib/worktree');
|
|
1460
|
+
const wtDir = task.worktree && task.worktree.dir ? task.worktree.dir : '';
|
|
1461
|
+
if (wtDir) send({ type: 'notice', content: `🌿 本任务在 Git 隔离区执行:${wtDir}`, taskId: task.id });
|
|
1462
|
+
await worktreeMod.runWithTaskCwd(wtDir, () => new Promise((resolve) => {
|
|
1364
1463
|
const kind = runner.kind === 'demo' ? 'demo' : (runner.kind === 'opencode' ? 'opencode' : 'fallback');
|
|
1365
1464
|
oc.chatSolo(kind, runner, {
|
|
1366
1465
|
prompt: cont
|
|
@@ -1385,7 +1484,7 @@ async function executeSoloTaskBatch(selected, send, myToken) {
|
|
|
1385
1484
|
resolve();
|
|
1386
1485
|
}
|
|
1387
1486
|
});
|
|
1388
|
-
});
|
|
1487
|
+
}));
|
|
1389
1488
|
|
|
1390
1489
|
const finalText = order.map(id => texts.get(id)).join('\n\n').trim();
|
|
1391
1490
|
if (finalText) {
|
|
@@ -1397,6 +1496,7 @@ async function executeSoloTaskBatch(selected, send, myToken) {
|
|
|
1397
1496
|
result: (doneError ? `执行出错:${doneError}` : finalText).slice(0, 2000)
|
|
1398
1497
|
});
|
|
1399
1498
|
send({ type: 'task_done', taskId: task.id, title: task.title, status: stopped ? 'pending' : (doneError ? 'failed' : 'done') });
|
|
1499
|
+
notifyDone({ kind: 'task', title: task.title, status: doneError ? 'failed' : 'done', snippet: finalText || doneError });
|
|
1400
1500
|
return stopped ? '' : sesId;
|
|
1401
1501
|
};
|
|
1402
1502
|
|
|
@@ -1430,12 +1530,19 @@ async function executeSoloTaskBatch(selected, send, myToken) {
|
|
|
1430
1530
|
// 执行过程照常持久化到对应任务会话,用户打开会话即可查看全过程
|
|
1431
1531
|
const SCHED_INTERVAL_MS = 15000;
|
|
1432
1532
|
function startScheduler() {
|
|
1533
|
+
let firstScan = true; // 启动首扫:识别停机期间错过的定时任务并说明补跑
|
|
1433
1534
|
const timer = setInterval(() => {
|
|
1434
1535
|
if (runLocks.tasks) return; // 手动批次执行中,下轮再查
|
|
1435
1536
|
if (store.getSchedEnabled() === false) return; // 用户关闭了定时调度总开关
|
|
1537
|
+
const now = Date.now();
|
|
1436
1538
|
const due = store.getTasks().filter(t =>
|
|
1437
|
-
t.kind === 'scheduled' && t.status === 'pending' && t.scheduledAt && t.scheduledAt <=
|
|
1438
|
-
if (!due.length) return;
|
|
1539
|
+
t.kind === 'scheduled' && t.status === 'pending' && t.scheduledAt && t.scheduledAt <= now);
|
|
1540
|
+
if (!due.length) { firstScan = false; return; }
|
|
1541
|
+
const missed = due.filter(t => now - t.scheduledAt > 60000); // 触发点已过 1 分钟以上 = 停机期间错过
|
|
1542
|
+
if (firstScan && missed.length) {
|
|
1543
|
+
console.log(`[scheduler] 检测到 ${missed.length} 个停机期间错过的定时任务,将立即补跑:${missed.map(t => t.title).join('、')}`);
|
|
1544
|
+
}
|
|
1545
|
+
firstScan = false;
|
|
1439
1546
|
runLocks.tasks = true;
|
|
1440
1547
|
const myToken = stopTokens.tasks;
|
|
1441
1548
|
console.log(`[scheduler] 定时触发 ${due.length} 个任务:${due.map(t => t.title).join('、')}`);
|
|
@@ -1458,6 +1565,10 @@ const PRUNE_DAYS = Number(process.env.AGENTS_CHAT_PRUNE_DAYS) > 0 ? Number(proce
|
|
|
1458
1565
|
function pruneOldDataQuiet() {
|
|
1459
1566
|
try {
|
|
1460
1567
|
const stat = store.pruneOldData(PRUNE_DAYS);
|
|
1568
|
+
// Git 隔离区同步治理:无主(任务已删)或完结超期的 worktree 一并清理
|
|
1569
|
+
require('./lib/worktree').pruneStale(PRUNE_DAYS, store.getTasks())
|
|
1570
|
+
.then(ws => { if (ws.worktrees) { stat.worktrees = ws.worktrees; console.log('[maintenance] 清理过期 Git 隔离区:', ws.worktrees); } })
|
|
1571
|
+
.catch(() => {});
|
|
1461
1572
|
const touched = Object.values(stat).reduce((a, b) => a + b, 0);
|
|
1462
1573
|
if (touched) console.log(`[maintenance] 自动清理超 ${PRUNE_DAYS} 天的历史数据:`, JSON.stringify(stat));
|
|
1463
1574
|
} catch (e) { console.error('[maintenance] 自动清理失败:', e && (e.message || e)); }
|
package/bin/agents-chat.js
CHANGED
|
@@ -275,6 +275,104 @@ function showVersion() {
|
|
|
275
275
|
console.log(`agents-chat v${PKG.version}`);
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
// ---------- 开机自启(定时任务抗重启)----------
|
|
279
|
+
// Windows: HKCU Run 键 + VBS 静默启动(无黑窗闪烁);exe 形态直接注册 exe 路径
|
|
280
|
+
// macOS: ~/Library/LaunchAgents/com.agents-chat.plist(登录即拉起)
|
|
281
|
+
// Linux: crontab @reboot 行
|
|
282
|
+
const AUTOSTART_KEY = 'AgentsChat';
|
|
283
|
+
function autostartTarget() {
|
|
284
|
+
// 单文件 exe:直接跑 exe(已无窗口);npm 形态:VBS 静默执行 agents-chat start
|
|
285
|
+
if (process.env.AGENTS_CHAT_STANDALONE === '1' || process.versions.bun) {
|
|
286
|
+
return { kind: 'exe', cmd: `"${process.execPath}"` };
|
|
287
|
+
}
|
|
288
|
+
return { kind: 'npm', cmd: null };
|
|
289
|
+
}
|
|
290
|
+
function autostartOn() {
|
|
291
|
+
const t = autostartTarget();
|
|
292
|
+
const home = os.homedir();
|
|
293
|
+
if (process.platform === 'win32') {
|
|
294
|
+
const run = t.kind === 'exe'
|
|
295
|
+
? t.cmd
|
|
296
|
+
: `wscript.exe "${path.join(home, '.agents-chat', 'autostart.vbs')}"`;
|
|
297
|
+
if (t.kind === 'npm') {
|
|
298
|
+
fs.mkdirSync(path.join(home, '.agents-chat'), { recursive: true });
|
|
299
|
+
// 0 = 隐藏窗口;npm bin 已在用户 PATH(npm 安装时配置), explorer 启动的进程可继承
|
|
300
|
+
fs.writeFileSync(path.join(home, '.agents-chat', 'autostart.vbs'),
|
|
301
|
+
`CreateObject("Wscript.Shell").Run "cmd /c agents-chat start", 0, False\r\n`);
|
|
302
|
+
}
|
|
303
|
+
execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v ${AUTOSTART_KEY} /t REG_SZ /d "${run}" /f`, { stdio: 'ignore' });
|
|
304
|
+
console.log('✓ 开机自启已开启(当前用户级,无需管理员)');
|
|
305
|
+
} else if (process.platform === 'darwin') {
|
|
306
|
+
const plist = path.join(home, 'Library', 'LaunchAgents', 'com.agents-chat.plist');
|
|
307
|
+
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
308
|
+
const exe = t.kind === 'exe' ? process.execPath : process.execPath;
|
|
309
|
+
const arg = t.kind === 'exe' ? [] : [path.join(__dirname, '..', 'app', 'server.js'), '--port', String(PORT)];
|
|
310
|
+
const envData = ` <key>EnvironmentVariables</key>\n <dict><key>AGENTS_CHAT_DATA</key><string>${DATA_DIR}</string></dict>`;
|
|
311
|
+
fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
|
|
312
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
313
|
+
<plist version="1.0"><dict>
|
|
314
|
+
<key>Label</key><string>com.agents-chat</string>
|
|
315
|
+
<key>ProgramArguments</key>
|
|
316
|
+
<array><string>${exe}</string>${arg.map(a => `<string>${a}</string>`).join('')}</array>
|
|
317
|
+
${envData}
|
|
318
|
+
<key>RunAtLoad</key><true/>
|
|
319
|
+
<key>StandardOutPath</key><string>${path.join(home, '.agents-chat', 'autostart.log')}</string>
|
|
320
|
+
<key>StandardErrorPath</key><string>${path.join(home, '.agents-chat', 'autostart.log')}</string>
|
|
321
|
+
</dict></plist>`);
|
|
322
|
+
try { execSync(`launchctl unload "${plist}" 2>/dev/null; launchctl load "${plist}"`); } catch { /* 下次登录生效 */ }
|
|
323
|
+
console.log('✓ 开机自启已开启(LaunchAgent,登录即启动)');
|
|
324
|
+
} else {
|
|
325
|
+
// Linux: crontab @reboot
|
|
326
|
+
let cron = '';
|
|
327
|
+
try { cron = execSync('crontab -l', { stdio: ['pipe', 'pipe', 'ignore'] }).toString(); } catch { /* 无 crontab */ }
|
|
328
|
+
const line = t.kind === 'exe'
|
|
329
|
+
? `@reboot ${process.execPath}`
|
|
330
|
+
: `@reboot ${process.execPath} ${path.join(__dirname, '..', 'app', 'server.js')} --port ${PORT}`;
|
|
331
|
+
if (cron.includes(AUTOSTART_KEY)) {
|
|
332
|
+
cron = cron.split('\n').filter(l => l && !l.includes(AUTOSTART_KEY)).join('\n');
|
|
333
|
+
}
|
|
334
|
+
cron = (cron ? cron.trimEnd() + '\n' : '') + `${line} # ${AUTOSTART_KEY} env AGENTS_CHAT_DATA=${DATA_DIR}`;
|
|
335
|
+
// @reboot 行无法带 env 前缀于部分 cron 实现,数据目录路径直接写进 server 启动参数不可行时靠默认 ~/.agents-chat
|
|
336
|
+
execSync('crontab -', { input: cron.replace(/ # [^\n]*/, '') + '\n' });
|
|
337
|
+
console.log('✓ 开机自启已开启(crontab @reboot,数据目录 ~/.agents-chat)');
|
|
338
|
+
}
|
|
339
|
+
console.log('关闭方式: agents-chat autostart off');
|
|
340
|
+
}
|
|
341
|
+
function autostartOff() {
|
|
342
|
+
try {
|
|
343
|
+
if (process.platform === 'win32') {
|
|
344
|
+
execSync(`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v ${AUTOSTART_KEY} /f`, { stdio: 'ignore' });
|
|
345
|
+
} else if (process.platform === 'darwin') {
|
|
346
|
+
const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.agents-chat.plist');
|
|
347
|
+
try { execSync(`launchctl unload "${plist}"`); } catch { /* ignore */ }
|
|
348
|
+
fs.unlinkSync(plist);
|
|
349
|
+
} else {
|
|
350
|
+
let cron = '';
|
|
351
|
+
try { cron = execSync('crontab -l', { stdio: ['pipe', 'pipe', 'ignore'] }).toString(); } catch { }
|
|
352
|
+
const kept = cron.split('\n').filter(l => l && !(l.includes('@reboot') && (l.includes('app/server.js') || l.includes('agents-chat'))));
|
|
353
|
+
execSync('crontab -', { input: kept.join('\n') + (kept.length ? '\n' : '') });
|
|
354
|
+
}
|
|
355
|
+
console.log('✓ 开机自启已关闭');
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error('关闭失败:', err.message);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function autostartStatus() {
|
|
361
|
+
try {
|
|
362
|
+
if (process.platform === 'win32') {
|
|
363
|
+
execSync(`reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v ${AUTOSTART_KEY}`, { stdio: 'pipe' });
|
|
364
|
+
} else if (process.platform === 'darwin') {
|
|
365
|
+
fs.accessSync(path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.agents-chat.plist'));
|
|
366
|
+
} else {
|
|
367
|
+
const cron = execSync('crontab -l', { stdio: ['pipe', 'pipe', 'ignore'] }).toString();
|
|
368
|
+
if (!/@reboot.*agents-chat|@reboot.*app\/server\.js/.test(cron)) throw new Error('not set');
|
|
369
|
+
}
|
|
370
|
+
console.log('开机自启: 已开启');
|
|
371
|
+
} catch {
|
|
372
|
+
console.log('开机自启: 未开启(agents-chat autostart on 开启)');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
278
376
|
// 主逻辑
|
|
279
377
|
const command = process.argv[2] || 'start';
|
|
280
378
|
|
|
@@ -301,6 +399,12 @@ switch (command) {
|
|
|
301
399
|
case '--version':
|
|
302
400
|
showVersion();
|
|
303
401
|
break;
|
|
402
|
+
case 'autostart':
|
|
403
|
+
if (process.argv[3] === 'off') autostartOff();
|
|
404
|
+
else if (process.argv[3] === 'status' || !process.argv[3]) autostartStatus();
|
|
405
|
+
else if (process.argv[3] === 'on') autostartOn();
|
|
406
|
+
else { console.error('用法: agents-chat autostart on|off|status'); process.exit(1); }
|
|
407
|
+
break;
|
|
304
408
|
case 'help':
|
|
305
409
|
case '--help':
|
|
306
410
|
case '-h':
|
|
@@ -315,6 +419,9 @@ Agents Chat CLI v${PKG.version}
|
|
|
315
419
|
agents-chat open 仅打开浏览器
|
|
316
420
|
agents-chat update 检查并一键升级到 npm 最新版
|
|
317
421
|
agents-chat version 查看当前版本
|
|
422
|
+
agents-chat autostart on|off|status
|
|
423
|
+
开机自启管理(定时任务抗机器重启,Windows 用户级注册表/
|
|
424
|
+
macOS LaunchAgent/Linux crontab)
|
|
318
425
|
|
|
319
426
|
环境变量:
|
|
320
427
|
AGENTS_CHAT_PORT 端口号 (默认: 3456)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iamsamyiok/agents-chat",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.32.0",
|
|
4
4
|
"description": "多智能体群聊工具 - 支持 OpenCode/Claude Code/Codex/pi 内核,微信风格聊天界面",
|
|
5
5
|
"main": "lib/start.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
12
|
"app",
|
|
13
|
-
"scripts"
|
|
13
|
+
"scripts",
|
|
14
|
+
".env.example"
|
|
14
15
|
],
|
|
15
16
|
"keywords": [
|
|
16
17
|
"ai",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"homepage": "https://github.com/iamsamyiok/agents-chat",
|
|
36
37
|
"scripts": {
|
|
37
38
|
"start": "node app/server.js",
|
|
38
|
-
"test": "node --test \"test/*.test.js\"",
|
|
39
|
+
"test": "node --test --test-force-exit \"test/*.test.js\"",
|
|
39
40
|
"build:exe": "node scripts/build-exe.js",
|
|
40
41
|
"postinstall": "node scripts/postinstall.js"
|
|
41
42
|
}
|
package/scripts/build-exe.js
CHANGED
|
@@ -30,10 +30,10 @@ for (const t of targets) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// 1. 生成内嵌资源模块(HTML → JSON 字符串常量,杜绝转义问题)
|
|
33
|
+
// 1. 生成内嵌资源模块(HTML/JSON/SVG → JSON 字符串常量,杜绝转义问题)
|
|
34
34
|
const assets = {};
|
|
35
35
|
for (const f of fs.readdirSync(PUB)) {
|
|
36
|
-
if (
|
|
36
|
+
if (/\.(html|json|svg)$/.test(f)) assets[f] = fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
37
37
|
}
|
|
38
38
|
fs.writeFileSync(EMBED, [
|
|
39
39
|
'// 本文件由 scripts/build-exe.js 构建时自动生成,勿手工编辑、勿提交仓库',
|
|
@@ -71,4 +71,15 @@ if (failed.length) {
|
|
|
71
71
|
console.error(`失败平台: ${failed.join(', ')}`);
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
// 4. 生成校验清单(Release 附上,供核对下载完整性;exe 未签名场景尤为重要)
|
|
76
|
+
const { createHash } = require('crypto');
|
|
77
|
+
const lines = [];
|
|
78
|
+
for (const f of fs.readdirSync(DIST).sort()) {
|
|
79
|
+
const fp = path.join(DIST, f);
|
|
80
|
+
const h = createHash('sha256').update(fs.readFileSync(fp)).digest('hex');
|
|
81
|
+
lines.push(`${h} ${f}`);
|
|
82
|
+
}
|
|
83
|
+
fs.writeFileSync(path.join(DIST, 'checksums.txt'), lines.join('\n') + '\n');
|
|
84
|
+
console.log('校验清单: dist/checksums.txt');
|
|
74
85
|
console.log('全部构建完成');
|