@iamsamyiok/agents-chat 3.24.0 → 3.26.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/lib/store.js +37 -0
- package/app/lib/updatecheck.js +43 -0
- package/app/public/cards.html +102 -1
- package/app/public/index.html +98 -6
- package/app/server.js +83 -24
- package/bin/agents-chat.js +74 -12
- package/package.json +1 -1
package/app/lib/store.js
CHANGED
|
@@ -508,6 +508,13 @@ function getMessages(taskId) {
|
|
|
508
508
|
return Array.isArray(list) ? list : [];
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
// 单会话消息上限:防止定时任务长跑数月将会话分片膨胀到读写不可承受
|
|
512
|
+
// 默认 500 条,AGENTS_CHAT_MSG_LIMIT 可调(0 = 不限制);主会话豁免(已有 epoch + 过期清理机制)
|
|
513
|
+
const MSG_LIMIT = (() => {
|
|
514
|
+
const n = Number(process.env.AGENTS_CHAT_MSG_LIMIT);
|
|
515
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 500;
|
|
516
|
+
})();
|
|
517
|
+
|
|
511
518
|
function addMessage(msg) {
|
|
512
519
|
migrateLegacyMessages();
|
|
513
520
|
fs.mkdirSync(MSG_DIR, { recursive: true });
|
|
@@ -529,9 +536,38 @@ function addMessage(msg) {
|
|
|
529
536
|
timestamp: msg.timestamp || new Date().toISOString()
|
|
530
537
|
};
|
|
531
538
|
msgs.push(rec);
|
|
539
|
+
// 滚动清理:超上限裁掉最旧消息,开头留一条系统标记说明去向(标记本身也会随时间被裁)
|
|
540
|
+
if (key !== '' && MSG_LIMIT > 0 && msgs.length > MSG_LIMIT) {
|
|
541
|
+
const kept = msgs.slice(msgs.length - MSG_LIMIT);
|
|
542
|
+
const first = kept[0];
|
|
543
|
+
if (!(first && first.role === 'sys' && /已按上限自动清理/.test(String(first.content || '')))) {
|
|
544
|
+
kept.unshift({ role: 'sys', content: `(已达单会话消息上限 ${MSG_LIMIT} 条,更早的过程消息已自动滚动清理,任务结果以最终结果区为准)`, taskId: key, timestamp: new Date().toISOString() });
|
|
545
|
+
}
|
|
546
|
+
msgs.length = 0;
|
|
547
|
+
msgs.push(...kept);
|
|
548
|
+
}
|
|
532
549
|
writeJson(msgShardPath(key), msgs);
|
|
533
550
|
}
|
|
534
551
|
|
|
552
|
+
// 按会话统计消息数(逐分片累加,不合并大数组:统计/导出场景低内存)
|
|
553
|
+
function countMessagesByTask() {
|
|
554
|
+
migrateLegacyMessages();
|
|
555
|
+
const counts = {};
|
|
556
|
+
let main = 0;
|
|
557
|
+
let files = [];
|
|
558
|
+
try { files = fs.readdirSync(MSG_DIR); } catch { return { counts, main }; }
|
|
559
|
+
for (const name of files) {
|
|
560
|
+
if (!name.endsWith('.json') || name.startsWith('.')) continue;
|
|
561
|
+
let list;
|
|
562
|
+
try { list = JSON.parse(fs.readFileSync(path.join(MSG_DIR, name), 'utf8')); } catch { continue; }
|
|
563
|
+
if (!Array.isArray(list) || !list.length) continue;
|
|
564
|
+
const tid = list[0].taskId || '';
|
|
565
|
+
if (tid) counts[tid] = (counts[tid] || 0) + list.length;
|
|
566
|
+
else main += list.length;
|
|
567
|
+
}
|
|
568
|
+
return { counts, main };
|
|
569
|
+
}
|
|
570
|
+
|
|
535
571
|
function clearMessages() {
|
|
536
572
|
migrateLegacyMessages();
|
|
537
573
|
// 清空全部会话消息:删除所有分片,主会话分片重置为空数组
|
|
@@ -754,6 +790,7 @@ module.exports = {
|
|
|
754
790
|
getMessages,
|
|
755
791
|
addMessage,
|
|
756
792
|
clearMessages,
|
|
793
|
+
countMessagesByTask,
|
|
757
794
|
addFlowEvent,
|
|
758
795
|
getFlow,
|
|
759
796
|
listFlowRuns,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// npm 最新版本检查:供启动横幅、/api/health、CLI update/status 共用
|
|
2
|
+
// Node 18+ 自带全局 fetch;3 秒超时 + 1 小时结果缓存,失败静默(离线/内网不影响启动)
|
|
3
|
+
|
|
4
|
+
const PKG = require('../../package.json');
|
|
5
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PKG.name}/latest`;
|
|
6
|
+
|
|
7
|
+
let cached = { at: 0, latest: null }; // 缓存上次查询结果(1 小时内复用)
|
|
8
|
+
|
|
9
|
+
// 简化 semver 比较:a > b 返回 true(仅支持 x.y.z 数字段, prerelease 忽略比较)
|
|
10
|
+
function semverGt(a, b) {
|
|
11
|
+
const pa = String(a || '').split('-')[0].split('.').map(n => parseInt(n, 10) || 0);
|
|
12
|
+
const pb = String(b || '').split('-')[0].split('.').map(n => parseInt(n, 10) || 0);
|
|
13
|
+
for (let i = 0; i < 3; i++) {
|
|
14
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0);
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 查询 npm registry 最新版本;force=true 跳过缓存(CLI update 用)
|
|
20
|
+
async function checkLatest({ force = false } = {}) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (!force && cached.latest && now - cached.at < 3600 * 1000) {
|
|
23
|
+
return { latest: cached.latest, updateAvailable: semverGt(cached.latest, PKG.version) };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const ctrl = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => ctrl.abort(), 3000);
|
|
28
|
+
const res = await fetch(REGISTRY_URL, { signal: ctrl.signal, headers: { 'accept': 'application/json' } });
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
if (!res.ok) return null;
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
const latest = String(data.version || '');
|
|
33
|
+
if (!latest) return null;
|
|
34
|
+
cached = { at: now, latest };
|
|
35
|
+
return { latest, updateAvailable: semverGt(latest, PKG.version) };
|
|
36
|
+
} catch {
|
|
37
|
+
return null; // 网络不可达/超时:静默,不阻塞调用方
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const UPDATE_COMMAND = `npm install -g ${PKG.name}@latest`;
|
|
42
|
+
|
|
43
|
+
module.exports = { semverGt, checkLatest, UPDATE_COMMAND, currentVersion: PKG.version };
|
package/app/public/cards.html
CHANGED
|
@@ -190,6 +190,18 @@
|
|
|
190
190
|
/* 空看板引导 */
|
|
191
191
|
.board-guide{grid-column:1/-1;text-align:center;padding:40px 10px;color:var(--dim)}
|
|
192
192
|
.board-guide .bg-actions{display:flex;gap:10px;justify-content:center;margin-top:14px}
|
|
193
|
+
/* 使用帮助 */
|
|
194
|
+
.help-sec{margin:0 0 14px}
|
|
195
|
+
.help-sec h4{margin:0 0 6px;font-size:13.5px;color:var(--acc)}
|
|
196
|
+
.help-sec ul,.help-sec ol{margin:0;padding-left:20px}
|
|
197
|
+
.help-sec li{margin:4px 0;font-size:12.5px;line-height:1.65;color:#333}
|
|
198
|
+
.help-sec code{background:var(--bg2);padding:1px 5px;border-radius:4px;font-size:11.5px;color:var(--acc)}
|
|
199
|
+
.help-sec p{font-size:12.5px;line-height:1.65;color:#333;margin:6px 0}
|
|
200
|
+
/* 版本徽标与更新提示条 */
|
|
201
|
+
.ver-chip{font-size:11px;color:var(--dim);border:1px solid var(--line);border-radius:10px;padding:2px 8px;white-space:nowrap}
|
|
202
|
+
.update-bar{position:fixed;top:0;left:0;right:0;z-index:60;background:#fffbe6;border-bottom:1px solid #ffe58f;color:#614700;font-size:12.5px;padding:7px 14px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
203
|
+
.update-bar button{border:1px solid #d8b94a;background:#fff;border-radius:5px;padding:2px 10px;font-size:12px;cursor:pointer;color:#614700}
|
|
204
|
+
.update-bar button:hover{background:#fff7d6}
|
|
193
205
|
</style>
|
|
194
206
|
</head>
|
|
195
207
|
<body>
|
|
@@ -216,6 +228,8 @@
|
|
|
216
228
|
<button class="btn primary" id="btnRun" title="按依赖与优先级调度全部待执行任务">▶ 开始编排</button>
|
|
217
229
|
<button class="btn danger" id="btnStop" disabled title="停止编排:进行中的任务复位为待执行(不影响追加聊天),未开始的任务保留">⏹ 停止</button>
|
|
218
230
|
<button class="btn" id="btnClear">清空</button>
|
|
231
|
+
<button class="btn" id="btnHelp" title="多任务编排使用帮助">❓ 帮助</button>
|
|
232
|
+
<span class="ver-chip" id="verChip" title="当前版本"></span>
|
|
219
233
|
</header>
|
|
220
234
|
|
|
221
235
|
<div class="kernel-banner" id="kernelBanner"></div>
|
|
@@ -316,6 +330,14 @@
|
|
|
316
330
|
</div>
|
|
317
331
|
|
|
318
332
|
<!-- 垃圾桶 -->
|
|
333
|
+
<!-- 使用帮助 -->
|
|
334
|
+
<div class="mask" id="helpMask">
|
|
335
|
+
<div class="modal" style="width:min(720px,94vw);max-height:86vh;overflow-y:auto">
|
|
336
|
+
<h3>❓ 多任务编排使用帮助</h3>
|
|
337
|
+
<div id="helpBody"></div>
|
|
338
|
+
<div class="foot"><button class="btn primary" id="helpClose">关闭</button></div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
319
341
|
<div class="mask" id="trashMask">
|
|
320
342
|
<div class="modal">
|
|
321
343
|
<h3>🗑️ 垃圾桶 <span style="font-size:12px;color:var(--dim)">(保留 30 天内删除的任务快照)</span></h3>
|
|
@@ -360,6 +382,7 @@ async function loadCards(){
|
|
|
360
382
|
if(data.config) $('#wsInput').value = data.config.workspace || '';
|
|
361
383
|
if(data.maxParallel) $('#parallelSel').value = String(data.maxParallel);
|
|
362
384
|
renderKernelBanner();
|
|
385
|
+
renderUpdateInfo(data);
|
|
363
386
|
render();
|
|
364
387
|
renderGraph();
|
|
365
388
|
loadTrashCount();
|
|
@@ -753,7 +776,8 @@ async function openDetail(id){
|
|
|
753
776
|
const log=$('#dLog'); log.innerHTML=''; TEXT_PARTS.clear();
|
|
754
777
|
if(!msgs.length) log.innerHTML='<span class="empty">暂无过程记录</span>';
|
|
755
778
|
for(const m of msgs){
|
|
756
|
-
if(m.role==='
|
|
779
|
+
if(m.role==='sys') appendLog(log,'ℹ '+m.content,'msg-tool');
|
|
780
|
+
else if(m.role==='user') appendLog(log,'👤 '+m.content,'msg-user');
|
|
757
781
|
else if(m.phase==='archive') appendLog(log,'📦 '+m.content,'msg-tool');
|
|
758
782
|
else if(m.phase==='system') appendLog(log,'⚠ '+m.content,'msg-err');
|
|
759
783
|
else if(/工具|执行完成/.test(m.content||'')) appendLog(log,'🔧 '+m.content,'msg-tool');
|
|
@@ -929,6 +953,83 @@ async function openTrash(){
|
|
|
929
953
|
}
|
|
930
954
|
$('#trashFab').onclick=openTrash;
|
|
931
955
|
$('#trashClose').onclick=()=>$('#trashMask').classList.remove('show');
|
|
956
|
+
|
|
957
|
+
// ---------- 使用帮助 ----------
|
|
958
|
+
function openHelp(){
|
|
959
|
+
$('#helpBody').innerHTML = `
|
|
960
|
+
<div class="help-sec"><h4>🗂 这是什么</h4>
|
|
961
|
+
<p>把要做的事写成一张张「任务卡牌」,程序按<b>依赖关系与优先级</b>自动调度 AI 执行:上游完成才启动下游,无依赖的任务并行跑。适合批量任务、流水线作业(翻译→润色→排版)与多分支并行调研。</p>
|
|
962
|
+
<p>与群聊的关系:群聊是「对话式」由管家现场拆解调度;本页是「清单式」由你预先定义好每张卡,执行过程可暂停、追问、重跑。</p>
|
|
963
|
+
</div>
|
|
964
|
+
<div class="help-sec"><h4>🚀 快速开始(三步)</h4>
|
|
965
|
+
<ol>
|
|
966
|
+
<li>「+ 新建任务」:写标题与任务内容(描述越具体越好,支持 Markdown)</li>
|
|
967
|
+
<li>(可选)在表单里选「依赖任务」:被依赖的卡完成后本卡才执行</li>
|
|
968
|
+
<li>「▶ 开始编排」:按依赖拓扑 + 优先级自动调度全部待执行卡牌</li>
|
|
969
|
+
</ol>
|
|
970
|
+
</div>
|
|
971
|
+
<div class="help-sec"><h4>⚙ 三种执行模式(新建任务表单)</h4>
|
|
972
|
+
<ul>
|
|
973
|
+
<li><b>新进程(new)</b>:独立会话执行,互不影响——默认</li>
|
|
974
|
+
<li><b>同会话续聊(continue)</b>:接续某张卡(续聊链首)的会话继续,AI 记得之前的成果,适合「初稿→修改→定稿」链</li>
|
|
975
|
+
<li><b>并行独立进程(parallel)</b>:与前后卡同时开跑,适合批量同类任务(同时查 5 个城市天气)</li>
|
|
976
|
+
</ul>
|
|
977
|
+
</div>
|
|
978
|
+
<div class="help-sec"><h4>🔀 依赖、优先级与并行度</h4>
|
|
979
|
+
<ul>
|
|
980
|
+
<li><b>依赖任务</b>可多选:全部完成后本卡才可执行(失败/跳过的依赖会阻断)</li>
|
|
981
|
+
<li><b>优先级</b>数字越小越先执行;拖拽看板卡到新位置会按新顺序自动重写优先级</li>
|
|
982
|
+
<li>顶栏 <b>⚡ 并行</b>(1-8):同时执行的最大任务数,改完即时生效</li>
|
|
983
|
+
<li>左下角「▶ 依赖图」可视化全部依赖关系,可放大查看</li>
|
|
984
|
+
</ul>
|
|
985
|
+
</div>
|
|
986
|
+
<div class="help-sec"><h4>📌 看板四列</h4>
|
|
987
|
+
<ul>
|
|
988
|
+
<li>待执行 → 执行中 → 完成 / 失败;卡牌角标显示该卡过程消息条数</li>
|
|
989
|
+
<li>失败列的「↻ 全部重跑」把失败卡重置为待执行并立即重新编排</li>
|
|
990
|
+
<li>顶栏搜索框按标题/内容即时过滤看板;统计条实时显示总数与完成率</li>
|
|
991
|
+
<li>删除的卡进入回收站,可随时恢复(清空需二次确认)</li>
|
|
992
|
+
</ul>
|
|
993
|
+
</div>
|
|
994
|
+
<div class="help-sec"><h4>💬 追问(完成后继续聊)</h4>
|
|
995
|
+
<ul>
|
|
996
|
+
<li>点开已完成的卡,详情页底部可输入追加消息:同一会话继续对话(让它改改措辞、补个章节都行)</li>
|
|
997
|
+
<li>追问期间不影响其他卡牌执行;追问结果计入该卡日志</li>
|
|
998
|
+
</ul>
|
|
999
|
+
</div>
|
|
1000
|
+
<div class="help-sec"><h4>📄 详情、日志与导出</h4>
|
|
1001
|
+
<ul>
|
|
1002
|
+
<li>点任意卡看执行过程(实时滚动,含工具调用)、最终结果与失败原因</li>
|
|
1003
|
+
<li>「⬇ 导出」把该卡导出为 Markdown(元信息 + 过程 + 结果),适合汇报存档</li>
|
|
1004
|
+
<li>「🗑 清空」只清看板卡牌,群聊/单聊数据不受影响</li>
|
|
1005
|
+
</ul>
|
|
1006
|
+
</div>
|
|
1007
|
+
<div class="help-sec"><h4>📁 工作区与内核</h4>
|
|
1008
|
+
<ul>
|
|
1009
|
+
<li>顶栏「工作文件夹」:选定后 Agent 在该目录读写相关文件(写代码、改文档直接落到你指定的目录),可选</li>
|
|
1010
|
+
<li>执行内核与群聊共用同一套(opencode / Claude Code / Codex / pi),在群聊页「⚙ 智能体配置」里切换</li>
|
|
1011
|
+
<li>头部徽标显示当前内核;未检测到内核时顶部横幅会给出安装指引</li>
|
|
1012
|
+
</ul>
|
|
1013
|
+
</div>`;
|
|
1014
|
+
$('#helpMask').classList.add('show');
|
|
1015
|
+
}
|
|
1016
|
+
$('#btnHelp').onclick=openHelp;
|
|
1017
|
+
$('#helpClose').onclick=()=>$('#helpMask').classList.remove('show');
|
|
1018
|
+
$('#helpMask').addEventListener('click',e=>{ if(e.target.id==='helpMask') e.target.classList.remove('show'); });
|
|
1019
|
+
|
|
1020
|
+
// ---------- 版本徽标与更新提示 ----------
|
|
1021
|
+
function renderUpdateInfo(data){
|
|
1022
|
+
if(data.version) $('#verChip').textContent='v'+data.version;
|
|
1023
|
+
if(!data.updateAvailable) return;
|
|
1024
|
+
if(document.getElementById('updateBar')) return;
|
|
1025
|
+
const bar=document.createElement('div');
|
|
1026
|
+
bar.id='updateBar'; bar.className='update-bar';
|
|
1027
|
+
bar.innerHTML='📦 发现新版本 <b>v'+esc(data.latestVersion)+'</b>(当前 v'+esc(data.version)+'):终端运行 <code>agents-chat update</code> 一键升级,或执行 <code>'+esc(data.updateCommand)+'</code>'
|
|
1028
|
+
+' <button id="updateCopy">复制升级命令</button>';
|
|
1029
|
+
document.body.prepend(bar);
|
|
1030
|
+
document.body.style.paddingTop='34px';
|
|
1031
|
+
$('#updateCopy').onclick=()=>copyText(data.updateCommand||('npm install -g @iamsamyiok/agents-chat@latest'));
|
|
1032
|
+
}
|
|
932
1033
|
$('#trashEmpty').onclick=async()=>{ if(!confirm('确认清空垃圾桶?所有快照及其过程日志将被永久删除,不可恢复。'))return; const r=await post('/api/cards/trash/empty'); toast('已清空 '+r.cleared+' 项'); await loadCards(); };
|
|
933
1034
|
|
|
934
1035
|
// ---- 进程状态条(PID + 工作状态,SSE 事件驱动 + 2s 轮询兜底) ----
|
package/app/public/index.html
CHANGED
|
@@ -84,6 +84,9 @@
|
|
|
84
84
|
#jumpBtn { position: absolute; right: 18px; bottom: 100px; z-index: 5; display: none; border: 1px solid #dcdcdc; background: #fff; color: #333; border-radius: 16px; padding: 6px 14px; font-size: 12px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,.15); animation: fade-in .2s ease; transition: color .15s, border-color .15s; }
|
|
85
85
|
#jumpBtn:hover { color: #07c160; border-color: #07c160; }
|
|
86
86
|
.sys-tip { text-align: center; margin: 10px 0; }
|
|
87
|
+
/* 长会话增量渲染:加载更早入口 */
|
|
88
|
+
.load-earlier { display: block; margin: 4px auto 16px; border: 1px solid #dcdcdc; background: #fff; color: #888; border-radius: 14px; padding: 5px 14px; font-size: 12px; cursor: pointer; transition: color .15s, border-color .15s; }
|
|
89
|
+
.load-earlier:hover { color: #07c160; border-color: #07c160; }
|
|
87
90
|
.approval-card { max-width: 480px; margin: 10px auto; background: #fff8e6; border: 1px solid #e8a33d; border-radius: 10px; padding: 10px 14px; text-align: left; }
|
|
88
91
|
.approval-card .ap-title { font-weight: 600; font-size: 13px; color: #8a5a00; margin-bottom: 4px; }
|
|
89
92
|
.approval-card .ap-label { font-size: 12px; color: #555; margin-bottom: 8px; word-break: break-all; }
|
|
@@ -437,6 +440,7 @@
|
|
|
437
440
|
<button class="cfg-btn" id="agentsCfgBtn" onclick="openAgents()">⚙ 智能体配置</button>
|
|
438
441
|
<button class="cfg-btn" id="cardsNavBtn" onclick="openCards()" title="多任务编排 · 卡牌:把任务写成卡牌,Agent 干完一件自动注入下一件">🗂️ 多任务编排</button>
|
|
439
442
|
<span class="runner-tag" id="runnerTag" style="display:none"></span>
|
|
443
|
+
<span class="ver-chip" id="verChip" title="当前版本" style="font-size:11px;color:#999;border:1px solid #e2e2e2;border-radius:10px;padding:2px 8px;white-space:nowrap"></span>
|
|
440
444
|
<button class="cfg-btn" id="helpBtn" onclick="openHelp()" title="使用帮助">❓ 帮助</button>
|
|
441
445
|
</div>
|
|
442
446
|
</header>
|
|
@@ -662,7 +666,7 @@
|
|
|
662
666
|
</div>
|
|
663
667
|
|
|
664
668
|
<script>
|
|
665
|
-
const PAGE_VERSION = '3.
|
|
669
|
+
const PAGE_VERSION = '3.26.0'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
|
|
666
670
|
const AV_COLORS = ['#5b8def','#07c160','#fa9d3b','#10aeff','#8a6fe8','#fa5151','#e8a33d','#3aa7a3'];
|
|
667
671
|
const PHASE_LABEL = { plan: '调度规划', work: '执行', review: '验收', report: '汇总', talk: '圆桌发言', task: '任务执行' };
|
|
668
672
|
// 职业图标库(智能体配置可选)
|
|
@@ -756,6 +760,7 @@ async function init() {
|
|
|
756
760
|
if (health.version && health.version !== PAGE_VERSION) {
|
|
757
761
|
addSys(`⚠ 页面版本(${PAGE_VERSION})与服务端(${health.version})不一致:浏览器缓存了旧页面,请按 Ctrl+F5 强制刷新`);
|
|
758
762
|
}
|
|
763
|
+
renderUpdateInfo(health);
|
|
759
764
|
|
|
760
765
|
input.addEventListener('input', handleAtInput);
|
|
761
766
|
input.addEventListener('blur', () => setTimeout(hideAt, 150));
|
|
@@ -960,9 +965,36 @@ function soloRenderMessages() {
|
|
|
960
965
|
return;
|
|
961
966
|
}
|
|
962
967
|
box.innerHTML = '';
|
|
963
|
-
|
|
968
|
+
const all = getSess(curOcId);
|
|
969
|
+
soloShownFrom = Math.max(0, all.length - RENDER_BATCH);
|
|
970
|
+
if (soloShownFrom > 0) box.appendChild(makeLoadEarlierBtn(loadEarlierSolo));
|
|
971
|
+
for (let i = soloShownFrom; i < all.length; i++) {
|
|
972
|
+
const n = soloMsgNode(all[i]);
|
|
973
|
+
all[i]._dom = n;
|
|
974
|
+
box.appendChild(n);
|
|
975
|
+
}
|
|
964
976
|
soloScroll();
|
|
965
977
|
}
|
|
978
|
+
function loadEarlierSolo() {
|
|
979
|
+
const box = document.getElementById('soloMessages');
|
|
980
|
+
const all = getSess(curOcId);
|
|
981
|
+
// 单聊消息节点构造器不同,这里专用批次回填
|
|
982
|
+
const fromIdx = soloShownFrom;
|
|
983
|
+
if (fromIdx <= 0) return;
|
|
984
|
+
const prevHeight = box.scrollHeight, prevTop = box.scrollTop;
|
|
985
|
+
const from = Math.max(0, fromIdx - RENDER_BATCH);
|
|
986
|
+
const frag = document.createDocumentFragment();
|
|
987
|
+
const oldBtn = box.querySelector('.load-earlier'); if (oldBtn) oldBtn.remove();
|
|
988
|
+
if (from > 0) frag.appendChild(makeLoadEarlierBtn(loadEarlierSolo));
|
|
989
|
+
for (let i = from; i < fromIdx; i++) {
|
|
990
|
+
const n = soloMsgNode(all[i]);
|
|
991
|
+
all[i]._dom = n;
|
|
992
|
+
frag.appendChild(n);
|
|
993
|
+
}
|
|
994
|
+
box.insertBefore(frag, box.firstChild);
|
|
995
|
+
box.scrollTop = prevTop + (box.scrollHeight - prevHeight);
|
|
996
|
+
soloShownFrom = from;
|
|
997
|
+
}
|
|
966
998
|
|
|
967
999
|
function pushSoloMsg(key, m) {
|
|
968
1000
|
getSess(key).push(m);
|
|
@@ -1795,15 +1827,52 @@ function msgNode(m) {
|
|
|
1795
1827
|
}
|
|
1796
1828
|
|
|
1797
1829
|
// 全量重绘当前会话(切换会话 / 载入历史时)
|
|
1830
|
+
// ---------- 长会话增量渲染:初始只渲染最近 RENDER_BATCH 条,更早的按需加载 ----------
|
|
1831
|
+
// 超长会话(数千条)全量渲染会拖慢浏览器;批次化后 DOM 数量恒定,加载更早时保持视口不跳动
|
|
1832
|
+
const RENDER_BATCH = 100;
|
|
1833
|
+
function makeLoadEarlierBtn(onClick) {
|
|
1834
|
+
const b = document.createElement('button');
|
|
1835
|
+
b.className = 'load-earlier';
|
|
1836
|
+
b.textContent = '加载更早的消息';
|
|
1837
|
+
b.onclick = onClick;
|
|
1838
|
+
return b;
|
|
1839
|
+
}
|
|
1840
|
+
function loadEarlierInto(box, all, fromIdx, onClick) {
|
|
1841
|
+
if (fromIdx <= 0) return 0;
|
|
1842
|
+
const prevHeight = box.scrollHeight, prevTop = box.scrollTop;
|
|
1843
|
+
const from = Math.max(0, fromIdx - RENDER_BATCH);
|
|
1844
|
+
const frag = document.createDocumentFragment();
|
|
1845
|
+
const oldBtn = box.querySelector('.load-earlier'); if (oldBtn) oldBtn.remove();
|
|
1846
|
+
if (from > 0) frag.appendChild(makeLoadEarlierBtn(onClick));
|
|
1847
|
+
for (let i = from; i < fromIdx; i++) {
|
|
1848
|
+
const node = msgNode(all[i]);
|
|
1849
|
+
all[i]._dom = node;
|
|
1850
|
+
frag.appendChild(node);
|
|
1851
|
+
finalizeMsgLinks(all[i]);
|
|
1852
|
+
}
|
|
1853
|
+
box.insertBefore(frag, box.firstChild);
|
|
1854
|
+
box.scrollTop = prevTop + (box.scrollHeight - prevHeight); // 视口停在原消息处
|
|
1855
|
+
return from;
|
|
1856
|
+
}
|
|
1857
|
+
let mainShownFrom = 0; // 主聊天区已渲染起始下标
|
|
1858
|
+
let soloShownFrom = 0; // 单聊区已渲染起始下标
|
|
1859
|
+
function loadEarlierMain() {
|
|
1860
|
+
const box = document.getElementById('messages');
|
|
1861
|
+
mainShownFrom = loadEarlierInto(box, getSess(curKey()), mainShownFrom, loadEarlierMain);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1798
1864
|
function renderMessages() {
|
|
1799
1865
|
const box = document.getElementById('messages');
|
|
1800
1866
|
box.classList.add('bulk'); // 批量重建:抑制逐条入场动画
|
|
1801
1867
|
box.innerHTML = '';
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1868
|
+
const all = getSess(curKey());
|
|
1869
|
+
mainShownFrom = Math.max(0, all.length - RENDER_BATCH);
|
|
1870
|
+
if (mainShownFrom > 0) box.appendChild(makeLoadEarlierBtn(loadEarlierMain));
|
|
1871
|
+
for (let i = mainShownFrom; i < all.length; i++) {
|
|
1872
|
+
const node = msgNode(all[i]);
|
|
1873
|
+
all[i]._dom = node;
|
|
1805
1874
|
box.appendChild(node);
|
|
1806
|
-
finalizeMsgLinks(
|
|
1875
|
+
finalizeMsgLinks(all[i]); // 定稿消息中的 .md 路径变成可点击预览链接
|
|
1807
1876
|
}
|
|
1808
1877
|
scrollBottom();
|
|
1809
1878
|
requestAnimationFrame(() => requestAnimationFrame(() => box.classList.remove('bulk')));
|
|
@@ -3182,8 +3251,31 @@ function openCards() {
|
|
|
3182
3251
|
window.open('/cards', '_blank');
|
|
3183
3252
|
}
|
|
3184
3253
|
|
|
3254
|
+
// 版本徽标与更新提示条(health 下发;无新版静默)
|
|
3255
|
+
function renderUpdateInfo(health) {
|
|
3256
|
+
if (!health) return;
|
|
3257
|
+
const chip = document.getElementById('verChip');
|
|
3258
|
+
if (chip && health.version) chip.textContent = 'v' + health.version;
|
|
3259
|
+
if (!health.updateAvailable || document.getElementById('updateBar')) return;
|
|
3260
|
+
const bar = document.createElement('div');
|
|
3261
|
+
bar.id = 'updateBar';
|
|
3262
|
+
bar.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:60;background:#fffbe6;border-bottom:1px solid #ffe58f;color:#614700;font-size:12.5px;padding:7px 14px;display:flex;align-items:center;gap:8px;flex-wrap:wrap';
|
|
3263
|
+
bar.innerHTML = `📦 发现新版本 <b>v${esc(health.latestVersion)}</b>(当前 v${esc(health.version)}):终端运行 <code>agents-chat update</code> 一键升级,或执行 <code>${esc(health.updateCommand)}</code> <button id="updateCopy" style="border:1px solid #d8b94a;background:#fff;border-radius:5px;padding:2px 10px;font-size:12px;cursor:pointer;color:#614700">复制升级命令</button>`;
|
|
3264
|
+
document.body.prepend(bar);
|
|
3265
|
+
document.body.style.paddingTop = '34px';
|
|
3266
|
+
document.getElementById('updateCopy').onclick = () => copyText(health.updateCommand || 'npm install -g @iamsamyiok/agents-chat@latest');
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3185
3269
|
function openHelp() {
|
|
3186
3270
|
document.getElementById('helpBody').innerHTML = kernelBanner() + `
|
|
3271
|
+
<div class="help-sec"><h4>🧭 三种使用模式(先选对界面)</h4>
|
|
3272
|
+
<ul>
|
|
3273
|
+
<li><b>👥 群聊</b>(本页默认):一句话发给整个团队——管家理解需求、拆解任务、调度子智能体协作完成;也可以 <code>@</code> 点名。适合不确定怎么拆、需要 AI 自己规划的事</li>
|
|
3274
|
+
<li><b>👤 单聊</b>(左上角开关切换):与一个 AI 一对一连续对话,像用普通 AI 助手;同一会话多轮续聊,顶部可选默认模型。适合问答、写作、改代码这类单人事务</li>
|
|
3275
|
+
<li><b>🗂️ 多任务编排</b>(顶栏按钮进入):把任务写成卡牌清单,按依赖与优先级自动批量执行。适合一次布置一批任务、流水线作业。编排在独立页面,有专属帮助(页面右上角 ❓)</li>
|
|
3276
|
+
</ul>
|
|
3277
|
+
<p>三者数据互通(同一数据目录、同一套内核),随时切换界面,互不干扰。</p>
|
|
3278
|
+
</div>
|
|
3187
3279
|
<div class="help-sec"><h4>🚀 快速开始</h4>
|
|
3188
3280
|
<ol>
|
|
3189
3281
|
<li>本机安装任一执行内核并登录模型(模型与密钥由内核自己管理,本程序不做任何 API 配置):<br>
|
package/app/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Agents Chat Portable - 零依赖 HTTP 服务
|
|
2
2
|
// 启动:node app/server.js [--port 3456]
|
|
3
|
-
const APP_VERSION = '
|
|
3
|
+
const APP_VERSION = require('../package.json').version; // 单源版本:与 package.json 始终一致(页面互检/更新检查共用)
|
|
4
|
+
const { checkLatest, UPDATE_COMMAND } = require('./lib/updatecheck');
|
|
4
5
|
const http = require('http');
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const path = require('path');
|
|
@@ -16,7 +17,14 @@ loadEnv(path.join(ROOT_DIR, '.env'));
|
|
|
16
17
|
const LOG_DIR = process.env.AGENTS_CHAT_DATA || path.join(ROOT_DIR, '.data');
|
|
17
18
|
try { fs.mkdirSync(LOG_DIR, { recursive: true }); } catch { /* ignore */ }
|
|
18
19
|
const LOG_PATH = path.join(LOG_DIR, 'server.log');
|
|
19
|
-
|
|
20
|
+
const LOG_MAX_BYTES = 5 * 1024 * 1024; // 单文件 5MB 滚动:常驻/定时任务长跑不无限增长
|
|
21
|
+
function logRotateIfNeeded() {
|
|
22
|
+
try {
|
|
23
|
+
const st = fs.statSync(LOG_PATH);
|
|
24
|
+
if (st.size > LOG_MAX_BYTES) fs.renameSync(LOG_PATH, LOG_PATH + '.1'); // 保留一代,旧的 .1 被覆盖
|
|
25
|
+
} catch { /* 无文件 */ }
|
|
26
|
+
}
|
|
27
|
+
try { logRotateIfNeeded(); fs.writeFileSync(LOG_PATH, `=== Agents Chat started ${new Date().toISOString()} ===\n`); } catch { /* ignore */ }
|
|
20
28
|
function teeWrite(args) {
|
|
21
29
|
try { fs.appendFileSync(LOG_PATH, args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ') + '\n'); } catch { /* ignore */ }
|
|
22
30
|
}
|
|
@@ -123,12 +131,19 @@ function sse(req, res) {
|
|
|
123
131
|
Connection: 'keep-alive'
|
|
124
132
|
});
|
|
125
133
|
sseConns++;
|
|
134
|
+
// 背压保护:慢客户端的写缓冲积压超 1MB 时主动断开(防内存无限堆积),客户端重连后经 init 补拉状态
|
|
126
135
|
const send = (obj) => {
|
|
127
|
-
try {
|
|
136
|
+
try {
|
|
137
|
+
if (res.writableLength > 1024 * 1024) { try { res.destroy(); } catch { /* ignore */ } return; }
|
|
138
|
+
res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
|
139
|
+
} catch { /* closed */ }
|
|
128
140
|
};
|
|
129
141
|
// 心跳防止代理断开
|
|
130
142
|
const hb = setInterval(() => {
|
|
131
|
-
try {
|
|
143
|
+
try {
|
|
144
|
+
if (res.writableLength > 1024 * 1024) { res.destroy(); clearInterval(hb); return; }
|
|
145
|
+
res.write(': hb\n\n');
|
|
146
|
+
} catch { clearInterval(hb); }
|
|
132
147
|
}, 15000);
|
|
133
148
|
let closed = false;
|
|
134
149
|
const onClose = () => {
|
|
@@ -288,9 +303,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
288
303
|
const { resolveRunner, detectKernels } = require('./lib/agent');
|
|
289
304
|
const runner = resolveRunner();
|
|
290
305
|
const kernels = detectKernels();
|
|
306
|
+
// 更新检查(1 小时缓存,失败静默返回 null 字段)
|
|
307
|
+
const upd = await checkLatest().catch(() => null);
|
|
291
308
|
json(res, 200, {
|
|
292
309
|
success: true,
|
|
293
310
|
version: APP_VERSION,
|
|
311
|
+
latestVersion: upd ? upd.latest : '',
|
|
312
|
+
updateAvailable: upd ? upd.updateAvailable : false,
|
|
313
|
+
updateCommand: upd && upd.updateAvailable ? UPDATE_COMMAND : '',
|
|
294
314
|
runner: runner.kind,
|
|
295
315
|
kernelLabel: runner.kernel ? runner.kernel.label : '',
|
|
296
316
|
kernelCmd: runner.cmd || '',
|
|
@@ -871,7 +891,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
871
891
|
|
|
872
892
|
// ---------- 历史管理:一键清空全部会话 / 导出 sessions.md ----------
|
|
873
893
|
if (p === '/api/history/clear' && req.method === 'POST') {
|
|
874
|
-
const
|
|
894
|
+
const msgStat = store.countMessagesByTask();
|
|
895
|
+
const msgCount = msgStat.main + Object.values(msgStat.counts).reduce((a, b) => a + b, 0);
|
|
875
896
|
let outDirs = 0;
|
|
876
897
|
store.clearMessages();
|
|
877
898
|
// 单聊会话记录一并清空(消息已清,保留空会话列表无意义)
|
|
@@ -891,18 +912,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
891
912
|
}
|
|
892
913
|
|
|
893
914
|
if (p === '/api/history/export' && req.method === 'GET') {
|
|
894
|
-
|
|
915
|
+
// 统计与头部:逐分片计数(低内存);正文按会话逐段拼接(导出为显式用户动作)
|
|
916
|
+
const stat = store.countMessagesByTask();
|
|
895
917
|
const tasksAll = store.getTasks();
|
|
896
918
|
const ocAll = store.getOcSessions();
|
|
897
919
|
const fmtTs = (t) => { const d = new Date(t); const p2 = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`; };
|
|
898
920
|
const roleOf = (m) => m.role === 'user' ? '👤 用户' : (m.agentName || '智能体') + (m.phase ? `(${m.phase})` : '');
|
|
921
|
+
const total = stat.main + Object.values(stat.counts).reduce((a, b) => a + b, 0);
|
|
899
922
|
const lines = [
|
|
900
923
|
'# Agents Chat 会话导出', '',
|
|
901
924
|
`- 导出时间:${fmtTs(Date.now())}`,
|
|
902
|
-
`- 会话数:${1 + tasksAll.filter(t =>
|
|
903
|
-
`- 消息总数:${
|
|
925
|
+
`- 会话数:${1 + tasksAll.filter(t => (stat.counts[t.id] || 0) > 0).length + ocAll.filter(s => (stat.counts[s.id] || 0) > 0).length}(主会话 + 任务会话 + 单聊会话)`,
|
|
926
|
+
`- 消息总数:${total}`, ''
|
|
904
927
|
];
|
|
905
|
-
const main =
|
|
928
|
+
const main = store.getMessages('');
|
|
906
929
|
lines.push('---', '', '## 主会话', '');
|
|
907
930
|
for (const m of main) {
|
|
908
931
|
lines.push(`### ${fmtTs(m.timestamp)} · ${roleOf(m)}`, '');
|
|
@@ -945,11 +968,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
945
968
|
// ---------- 事项管控(卡牌)API ----------
|
|
946
969
|
if (p === '/api/cards' && req.method === 'GET') {
|
|
947
970
|
const cards = CardStore.list();
|
|
948
|
-
// 附带每个卡牌当前过程消息条数(供 UI
|
|
949
|
-
|
|
950
|
-
try {
|
|
951
|
-
for (const m of store.getMessages()) if (m.taskId) counts[m.taskId] = (counts[m.taskId] || 0) + 1;
|
|
952
|
-
} catch { /* ignore */ }
|
|
971
|
+
// 附带每个卡牌当前过程消息条数(供 UI 角标展示):逐分片计数,不合并全量消息
|
|
972
|
+
let counts = {};
|
|
973
|
+
try { counts = store.countMessagesByTask().counts; } catch { /* ignore */ }
|
|
953
974
|
// 执行内核状态(前端据此显示内核徽标/缺失警告)与追加聊天进行中的卡
|
|
954
975
|
let runnerInfo = { kind: 'unknown', label: '' };
|
|
955
976
|
try {
|
|
@@ -958,9 +979,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
958
979
|
runnerInfo = { kind: r.kind, label: r.kernel ? r.kernel.label : '' };
|
|
959
980
|
} catch { /* ignore */ }
|
|
960
981
|
const { getCorruptedFiles } = require('./lib/cards');
|
|
982
|
+
// 版本与更新信息(cards 页无 health 轮询,随主列表接口下发;1 小时缓存)
|
|
983
|
+
const upd = await checkLatest().catch(() => null);
|
|
961
984
|
json(res, 200, {
|
|
962
985
|
success: true, cards, running: cardRunner.isRunning(), maxParallel: cardRunner.maxP(), msgCounts: counts, config: CardStore.getConfig(),
|
|
963
|
-
followupIds: cardRunner.getFollowupIds(), runner: runnerInfo, corrupted: getCorruptedFiles()
|
|
986
|
+
followupIds: cardRunner.getFollowupIds(), runner: runnerInfo, corrupted: getCorruptedFiles(),
|
|
987
|
+
version: APP_VERSION, latestVersion: upd ? upd.latest : '', updateAvailable: upd ? upd.updateAvailable : false,
|
|
988
|
+
updateCommand: upd && upd.updateAvailable ? UPDATE_COMMAND : ''
|
|
964
989
|
});
|
|
965
990
|
return;
|
|
966
991
|
}
|
|
@@ -1204,20 +1229,35 @@ const server = http.createServer(async (req, res) => {
|
|
|
1204
1229
|
});
|
|
1205
1230
|
|
|
1206
1231
|
// 优雅退出:停掉全部子进程、把执行中任务复位为待执行,避免残留与假死状态
|
|
1232
|
+
// 幂等防重入:信号钩子与 exit 钩子可能先后触发,清理只执行一次
|
|
1233
|
+
let shutdownDone = false;
|
|
1207
1234
|
function shutdown(code) {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1235
|
+
if (!shutdownDone) {
|
|
1236
|
+
shutdownDone = true;
|
|
1237
|
+
try {
|
|
1238
|
+
if (termProc) { try { termProc.kill(); } catch { /* ignore */ } termProc = null; }
|
|
1239
|
+
const n = stopAllChildren();
|
|
1240
|
+
const m = store.resetRunningTasks();
|
|
1241
|
+
const mc = CardStore.resetRunning();
|
|
1242
|
+
if (n || m || mc) console.log(`退出清理:终止 ${n} 个子进程,复位 ${m} 个执行中任务、${mc} 张卡牌`);
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
console.error('[shutdown] 清理失败:', err && (err.stack || err));
|
|
1245
|
+
}
|
|
1216
1246
|
}
|
|
1217
1247
|
process.exit(code);
|
|
1218
1248
|
}
|
|
1219
1249
|
process.on('SIGINT', () => shutdown(0));
|
|
1220
1250
|
process.on('SIGTERM', () => shutdown(0));
|
|
1251
|
+
// Windows 关闭控制台窗口(CTRL_CLOSE_EVENT)在 Node 上映射为 SIGHUP,系统给约 5 秒处理宽限:
|
|
1252
|
+
// 同步 killTree 足够完成,正在执行的 AI 子进程不再变孤儿
|
|
1253
|
+
process.on('SIGHUP', () => shutdown(0));
|
|
1254
|
+
// 兜底:显式 process.exit 路径(exit 钩子内仅允许同步操作,killTree 为同步调用)
|
|
1255
|
+
process.on('exit', () => {
|
|
1256
|
+
if (!shutdownDone) {
|
|
1257
|
+
shutdownDone = true;
|
|
1258
|
+
try { stopAllChildren(); } catch { /* ignore */ }
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1221
1261
|
|
|
1222
1262
|
// ---------- 任务批次执行(SSE 手动触发与定时调度共用) ----------
|
|
1223
1263
|
// send: 事件推送(SSE 为真实推送,定时触发为 no-op,消息仍会持久化)
|
|
@@ -1426,8 +1466,27 @@ server.listen(PORT, () => {
|
|
|
1426
1466
|
startScheduler();
|
|
1427
1467
|
startAutoStop();
|
|
1428
1468
|
startPruneTimer();
|
|
1429
|
-
console.log(`Agents Chat 已启动: http://localhost:${PORT}`);
|
|
1469
|
+
console.log(`Agents Chat v${APP_VERSION} 已启动: http://localhost:${PORT}`);
|
|
1430
1470
|
console.log(`运行内核: ${kindText}`);
|
|
1471
|
+
console.log('退出提示: 请用 Ctrl+C(或 agents-chat stop)退出,会自动清理执行中的 AI 子进程;直接关闭窗口可能残留正在执行的进程');
|
|
1431
1472
|
console.log(`本机可用内核: ${avail}(配置页可切换)`);
|
|
1432
1473
|
console.log(`数据目录: ${store.DATA_DIR}`);
|
|
1474
|
+
// 开发自检:页面内 PAGE_VERSION 落后于服务端 → 提示同步,避免互检误报强刷
|
|
1475
|
+
try {
|
|
1476
|
+
for (const f of ['index.html', 'cards.html']) {
|
|
1477
|
+
const html = fs.readFileSync(path.join(PUBLIC_DIR, f), 'utf8');
|
|
1478
|
+
const m = html.match(/PAGE_VERSION\s*=\s*'([^']+)'/);
|
|
1479
|
+
if (m && m[1] !== APP_VERSION) console.warn(`[版本自检] ${f} 的 PAGE_VERSION(${m[1]})与服务端(${APP_VERSION})不一致,发版前请同步`);
|
|
1480
|
+
}
|
|
1481
|
+
} catch { /* ignore */ }
|
|
1482
|
+
// 异步更新检查:不阻塞启动,有新版打印一键升级提示
|
|
1483
|
+
checkLatest().then((upd) => {
|
|
1484
|
+
if (upd && upd.updateAvailable) {
|
|
1485
|
+
console.log(`──────────────────────────────────────────────`);
|
|
1486
|
+
console.log(`📦 发现新版本 v${upd.latest}(当前 v${APP_VERSION})`);
|
|
1487
|
+
console.log(` 命令行一键升级: agents-chat update`);
|
|
1488
|
+
console.log(` 或手动执行: ${UPDATE_COMMAND}`);
|
|
1489
|
+
console.log(`──────────────────────────────────────────────`);
|
|
1490
|
+
}
|
|
1491
|
+
}).catch(() => { /* ignore */ });
|
|
1433
1492
|
});
|
package/bin/agents-chat.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* agents-chat CLI 入口
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* 用法:
|
|
6
|
-
* agents-chat
|
|
7
|
-
* agents-chat start
|
|
8
|
-
* agents-chat stop
|
|
9
|
-
* agents-chat status
|
|
10
|
-
* agents-chat open
|
|
6
|
+
* agents-chat # 启动服务(后台运行)
|
|
7
|
+
* agents-chat start # 同上
|
|
8
|
+
* agents-chat stop # 停止服务
|
|
9
|
+
* agents-chat status # 查看服务状态(含版本与更新检查)
|
|
10
|
+
* agents-chat open # 仅打开浏览器
|
|
11
|
+
* agents-chat update # 检查并一键升级到 npm 最新版
|
|
12
|
+
* agents-chat version # 查看当前版本
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
const { spawn, execSync } = require('child_process');
|
|
@@ -15,6 +17,9 @@ const fs = require('fs');
|
|
|
15
17
|
const path = require('path');
|
|
16
18
|
const os = require('os');
|
|
17
19
|
|
|
20
|
+
const PKG = require('../package.json');
|
|
21
|
+
const { checkLatest, UPDATE_COMMAND } = require('../app/lib/updatecheck');
|
|
22
|
+
|
|
18
23
|
const PORT = parseInt(process.env.AGENTS_CHAT_PORT || '3456', 10);
|
|
19
24
|
const PID_FILE = path.join(os.homedir(), '.agents-chat.pid');
|
|
20
25
|
const LOG_FILE = path.join(os.homedir(), '.agents-chat.log');
|
|
@@ -216,6 +221,52 @@ function showStatus() {
|
|
|
216
221
|
console.log('发现残留进程,已清理');
|
|
217
222
|
}
|
|
218
223
|
}
|
|
224
|
+
console.log(`当前版本: v${PKG.version}`);
|
|
225
|
+
checkLatest().then((upd) => {
|
|
226
|
+
if (!upd) { console.log('更新检查: 网络不可用,跳过'); return; }
|
|
227
|
+
if (upd.updateAvailable) {
|
|
228
|
+
console.log(`可用更新: v${upd.latest}(运行 agents-chat update 一键升级)`);
|
|
229
|
+
} else {
|
|
230
|
+
console.log(`版本状态: 已是最新(npm 最新 v${upd.latest})`);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 一键升级:检测新版 → 停服务(避免文件占用/旧进程残留)→ npm 全局安装最新版
|
|
236
|
+
async function updateSelf() {
|
|
237
|
+
console.log(`当前版本: v${PKG.version}`);
|
|
238
|
+
console.log('正在检查 npm 最新版本...');
|
|
239
|
+
const upd = await checkLatest({ force: true });
|
|
240
|
+
if (!upd) {
|
|
241
|
+
console.error('无法访问 npm registry(网络超时或离线),请稍后重试或手动执行:');
|
|
242
|
+
console.log(` ${UPDATE_COMMAND}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
if (!upd.updateAvailable) {
|
|
246
|
+
console.log(`已是最新版本 v${upd.latest},无需升级`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
console.log(`发现新版本 v${upd.latest},开始升级...`);
|
|
250
|
+
const check = isRunning();
|
|
251
|
+
if (check.running) {
|
|
252
|
+
console.log('先停止运行中的服务(升级完成后需手动重新启动)...');
|
|
253
|
+
stopServer();
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
execSync(UPDATE_COMMAND, { stdio: 'inherit' });
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(`升级完成: v${PKG.version} -> v${upd.latest}`);
|
|
259
|
+
console.log('运行 agents-chat 重新启动服务');
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error('升级失败:', err.message);
|
|
262
|
+
console.log('可手动执行:');
|
|
263
|
+
console.log(` ${UPDATE_COMMAND}`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function showVersion() {
|
|
269
|
+
console.log(`agents-chat v${PKG.version}`);
|
|
219
270
|
}
|
|
220
271
|
|
|
221
272
|
// 主逻辑
|
|
@@ -235,18 +286,29 @@ switch (command) {
|
|
|
235
286
|
case 'open':
|
|
236
287
|
openBrowser();
|
|
237
288
|
break;
|
|
289
|
+
case 'update':
|
|
290
|
+
case 'upgrade':
|
|
291
|
+
updateSelf();
|
|
292
|
+
break;
|
|
293
|
+
case 'version':
|
|
294
|
+
case '-v':
|
|
295
|
+
case '--version':
|
|
296
|
+
showVersion();
|
|
297
|
+
break;
|
|
238
298
|
case 'help':
|
|
239
299
|
case '--help':
|
|
240
300
|
case '-h':
|
|
241
301
|
console.log(`
|
|
242
|
-
Agents Chat CLI
|
|
302
|
+
Agents Chat CLI v${PKG.version}
|
|
243
303
|
|
|
244
304
|
用法:
|
|
245
|
-
agents-chat
|
|
246
|
-
agents-chat start
|
|
247
|
-
agents-chat stop
|
|
248
|
-
agents-chat status
|
|
249
|
-
agents-chat open
|
|
305
|
+
agents-chat 启动服务(后台运行,自动打开浏览器)
|
|
306
|
+
agents-chat start 同上
|
|
307
|
+
agents-chat stop 停止服务
|
|
308
|
+
agents-chat status 查看服务状态(含版本与更新检查)
|
|
309
|
+
agents-chat open 仅打开浏览器
|
|
310
|
+
agents-chat update 检查并一键升级到 npm 最新版
|
|
311
|
+
agents-chat version 查看当前版本
|
|
250
312
|
|
|
251
313
|
环境变量:
|
|
252
314
|
AGENTS_CHAT_PORT 端口号 (默认: 3456)
|