@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/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
- json(res, 200, { success: true, added, warnings, mode, runner, tasks: store.getTasks() });
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
- await new Promise((resolve) => {
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 <= Date.now());
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)); }
@@ -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.30.0",
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
  }
@@ -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 (f.endsWith('.html')) assets[f] = fs.readFileSync(path.join(PUB, f), 'utf8');
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('全部构建完成');