@iamsamyiok/agents-chat 3.19.0 → 3.19.2
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/cards.js +31 -11
- package/app/public/cards.html +18 -7
- package/app/public/index.html +1 -1
- package/app/server.js +17 -15
- package/package.json +1 -1
package/app/lib/cards.js
CHANGED
|
@@ -227,6 +227,8 @@ class CardRunner {
|
|
|
227
227
|
this.running = false;
|
|
228
228
|
this.timer = null;
|
|
229
229
|
this.procs = new Map(); // cardId -> { pid, child, lastActive, status }
|
|
230
|
+
this.followups = new Set(); // 追加聊天中的卡牌(并发防护)
|
|
231
|
+
this.baseline = null; // 本轮编排开始时的 done/failed 基线(all_done 报增量)
|
|
230
232
|
}
|
|
231
233
|
isRunning() { return this.running; }
|
|
232
234
|
|
|
@@ -271,6 +273,9 @@ class CardRunner {
|
|
|
271
273
|
if (this.running) return;
|
|
272
274
|
this.running = true;
|
|
273
275
|
this.token++;
|
|
276
|
+
// 记录基线:all_done 时报告本轮增量(成功/失败数),避免混入历史任务
|
|
277
|
+
const all0 = CardStore.list();
|
|
278
|
+
this.baseline = { done: all0.filter(c => c.status === 'done').length, failed: all0.filter(c => c.status === 'failed').length };
|
|
274
279
|
broadcast({ type: 'runner_started' });
|
|
275
280
|
this.tick();
|
|
276
281
|
}
|
|
@@ -283,9 +288,11 @@ class CardRunner {
|
|
|
283
288
|
if (!eligible.length) {
|
|
284
289
|
if (this.active.size === 0) {
|
|
285
290
|
this.running = false;
|
|
286
|
-
//
|
|
287
|
-
const
|
|
288
|
-
const
|
|
291
|
+
// 完成通知附本轮增量统计(成功/失败数),前端据此提醒
|
|
292
|
+
const all = CardStore.list();
|
|
293
|
+
const done = Math.max(0, all.filter(c => c.status === 'done').length - (this.baseline ? this.baseline.done : 0));
|
|
294
|
+
const failed = Math.max(0, all.filter(c => c.status === 'failed').length - (this.baseline ? this.baseline.failed : 0));
|
|
295
|
+
this.baseline = null;
|
|
289
296
|
broadcast({ type: 'all_done', done, failed });
|
|
290
297
|
}
|
|
291
298
|
return;
|
|
@@ -349,6 +356,9 @@ class CardRunner {
|
|
|
349
356
|
let sesId = ocSessionId;
|
|
350
357
|
|
|
351
358
|
let child = null;
|
|
359
|
+
// 进程登记:spawn 前占位(执行中即可见于进程条),spawn 后回填真实 PID
|
|
360
|
+
this.procs.set(card.id, { pid: null, child: null, lastActive: Date.now() });
|
|
361
|
+
broadcast({ type: 'proc', cardId: card.id, pid: null });
|
|
352
362
|
try {
|
|
353
363
|
await new Promise((resolve) => {
|
|
354
364
|
child = oc.chatSolo(kind, runner, {
|
|
@@ -378,15 +388,15 @@ class CardRunner {
|
|
|
378
388
|
resolve();
|
|
379
389
|
}
|
|
380
390
|
});
|
|
391
|
+
// chatSolo 同步返回 child(spawn 已完成):立即回填真实 PID,执行中即可见于进程条
|
|
392
|
+
const proc0 = this.procs.get(card.id);
|
|
393
|
+
if (proc0 && child) { proc0.pid = child.pid; proc0.child = child; }
|
|
394
|
+
broadcast({ type: 'proc', cardId: card.id, pid: child ? child.pid : null });
|
|
381
395
|
});
|
|
382
396
|
} catch (err) {
|
|
383
397
|
doneError = String((err && err.message) || err).slice(0, 2000);
|
|
384
398
|
}
|
|
385
399
|
|
|
386
|
-
// 登记进程信息(PID + 是否工作中),并广播给前端
|
|
387
|
-
this.procs.set(card.id, { pid: child ? child.pid : null, child, lastActive: Date.now() });
|
|
388
|
-
broadcast({ type: 'proc', cardId: card.id, pid: child ? child.pid : null });
|
|
389
|
-
|
|
390
400
|
const finalText = order.map(id => texts.get(id)).join('\n\n').trim();
|
|
391
401
|
const stopped = myToken !== this.token;
|
|
392
402
|
if (finalText) {
|
|
@@ -396,8 +406,9 @@ class CardRunner {
|
|
|
396
406
|
CardStore.update(card.id, {
|
|
397
407
|
status,
|
|
398
408
|
ocSessionId: sesId,
|
|
399
|
-
|
|
400
|
-
|
|
409
|
+
// 手动停止回到待执行:清掉残留,避免 pending 卡带着脏结果/错误
|
|
410
|
+
result: stopped ? '' : (doneError ? `执行出错:${doneError}` : finalText).slice(0, 20000),
|
|
411
|
+
error: stopped ? '' : (doneError || ''),
|
|
401
412
|
finishedAt: Date.now()
|
|
402
413
|
});
|
|
403
414
|
broadcast({ type: 'task_done', cardId: card.id, status, title: card.title });
|
|
@@ -408,6 +419,8 @@ class CardRunner {
|
|
|
408
419
|
async runOne(cardId) {
|
|
409
420
|
const card = CardStore.get(cardId);
|
|
410
421
|
if (!card || card.status === 'running') return false;
|
|
422
|
+
// 并发防护:追加聊天进行中的卡不可同时执行(避免同一会话被两条链路并发续写)
|
|
423
|
+
if (this.followups.has(cardId)) return false;
|
|
411
424
|
this.active.add(cardId);
|
|
412
425
|
const myToken = this.token;
|
|
413
426
|
await this.runCard(card, myToken).catch(() => {});
|
|
@@ -421,8 +434,8 @@ class CardRunner {
|
|
|
421
434
|
const card = CardStore.get(cardId);
|
|
422
435
|
if (!card) return { ok: false, error: '任务不存在' };
|
|
423
436
|
if (card.status === 'running' || card.status === 'pending') return { ok: false, error: '任务尚未执行完成,先运行任务再追加聊天' };
|
|
424
|
-
if (this.
|
|
425
|
-
if (
|
|
437
|
+
if (this.active.has(cardId)) return { ok: false, error: '任务正在执行中,稍后再追加聊天' };
|
|
438
|
+
if (this.followups.has(cardId)) return { ok: false, error: '该任务已有追加聊天进行中' };
|
|
426
439
|
this.followups.add(cardId);
|
|
427
440
|
|
|
428
441
|
const runner = resolveRunner();
|
|
@@ -443,6 +456,8 @@ class CardRunner {
|
|
|
443
456
|
if (!userText) return { ok: false, error: '请输入追加内容' };
|
|
444
457
|
store.addMessage({ role: 'user', agentId: 'solo', agentName: '我', actor: 'user', phase: 'followup', taskId: cardId, content: userText.slice(0, 20000) });
|
|
445
458
|
broadcast({ type: 'followup_start', cardId: cardId });
|
|
459
|
+
// 进程登记:spawn 前占位,spawn 后回填 PID(追加聊天执行中亦可见于进程条)
|
|
460
|
+
this.procs.set(cardId, { pid: null, child: null, lastActive: Date.now() });
|
|
446
461
|
|
|
447
462
|
const texts = new Map();
|
|
448
463
|
const order = [];
|
|
@@ -475,9 +490,14 @@ class CardRunner {
|
|
|
475
490
|
resolve();
|
|
476
491
|
}
|
|
477
492
|
});
|
|
493
|
+
const proc0 = this.procs.get(cardId);
|
|
494
|
+
if (proc0 && child) { proc0.pid = child.pid; proc0.child = child; }
|
|
495
|
+
broadcast({ type: 'proc', cardId, pid: child ? child.pid : null });
|
|
478
496
|
});
|
|
479
497
|
} catch (err) {
|
|
480
498
|
doneError = String((err && err.message) || err).slice(0, 2000);
|
|
499
|
+
} finally {
|
|
500
|
+
this.procs.delete(cardId);
|
|
481
501
|
}
|
|
482
502
|
|
|
483
503
|
const finalText = order.map(id => texts.get(id)).join('\n\n').trim();
|
package/app/public/cards.html
CHANGED
|
@@ -525,8 +525,15 @@ function fuSetState(running, tipText){
|
|
|
525
525
|
btn.textContent = running ? '回复中…' : '发送';
|
|
526
526
|
if(tipText!==undefined) $('#fuTip').textContent = tipText;
|
|
527
527
|
}
|
|
528
|
+
let DETAIL_SEQ = 0; // 详情加载序号:快速切换任务时只认最新请求(响应乱序防护)
|
|
528
529
|
async function openDetail(id){
|
|
529
|
-
|
|
530
|
+
if(!id) return;
|
|
531
|
+
const seq = ++DETAIL_SEQ;
|
|
532
|
+
OPEN_ID = id;
|
|
533
|
+
const data = await api('/api/cards/'+encodeURIComponent(id)+'/log');
|
|
534
|
+
if (seq !== DETAIL_SEQ || OPEN_ID !== id) return; // 已切换到其他任务或关闭弹窗:丢弃过期响应
|
|
535
|
+
if(!data || !data.success || !data.card){ toast('任务不存在或已被删除'); return; }
|
|
536
|
+
const card=data.card; const msgs=data.messages||[];
|
|
530
537
|
$('#dTitle').textContent=card.title;
|
|
531
538
|
$('#dMeta').innerHTML=badge(card)+`<span class="badge">${card.status}</span>`;
|
|
532
539
|
const depTitles = (card.dependsOn||[]).map(d=>{const c=CARDS.find(x=>x.id===d);return c?c.title:d;});
|
|
@@ -540,6 +547,8 @@ async function openDetail(id){
|
|
|
540
547
|
if(!msgs.length) log.innerHTML='<span class="empty">暂无过程记录</span>';
|
|
541
548
|
for(const m of msgs){
|
|
542
549
|
if(m.role==='user') appendLog(log,'👤 '+m.content,'msg-user');
|
|
550
|
+
else if(m.phase==='archive') appendLog(log,'📦 '+m.content,'msg-tool');
|
|
551
|
+
else if(m.phase==='system') appendLog(log,'⚠ '+m.content,'msg-err');
|
|
543
552
|
else if(/工具|执行完成/.test(m.content||'')) appendLog(log,'🔧 '+m.content,'msg-tool');
|
|
544
553
|
else if(/出错|失败/.test(m.content||'')) appendLog(log,'⚠ '+m.content,'msg-err');
|
|
545
554
|
else appendLog(log,'🤖 '+(m.content||'').slice(0,4000),'msg-assistant');
|
|
@@ -558,7 +567,7 @@ async function openDetail(id){
|
|
|
558
567
|
fuSetState(FU_RUNNING);
|
|
559
568
|
$('#maskDetail').classList.add('show');
|
|
560
569
|
}
|
|
561
|
-
$('#dClose').onclick=()=>{ OPEN_ID=null; $('#maskDetail').classList.remove('show'); };
|
|
570
|
+
$('#dClose').onclick=()=>{ OPEN_ID=null; DETAIL_SEQ++; $('#maskDetail').classList.remove('show'); };
|
|
562
571
|
$('#dRun').onclick=async()=>{ if(!OPEN_ID)return; await runOne(OPEN_ID); openDetail(OPEN_ID); };
|
|
563
572
|
|
|
564
573
|
// ---- 追加聊天:复用会话第二轮输入 ----
|
|
@@ -586,7 +595,7 @@ let G_SIG=''; // 结构+状态指纹:无变化时跳过重绘(避免流式
|
|
|
586
595
|
function renderGraph(force){
|
|
587
596
|
const box=$('#graphBody');
|
|
588
597
|
const cards=CARDS;
|
|
589
|
-
const sig=cards.map(c=>c.id+':'+c.status+':'+(c.dependsOn||[]).join(',')).join('|');
|
|
598
|
+
const sig=cards.map(c=>c.id+':'+c.status+':'+(c.dependsOn||[]).join(',')+':'+c.title+':'+c.mode).join('|');
|
|
590
599
|
if(!force && sig===G_SIG && box.querySelector('svg')) return;
|
|
591
600
|
G_SIG=sig;
|
|
592
601
|
if(!cards.length){ box.innerHTML='<div class="empty" style="padding:24px 8px">暂无任务<br/>新建任务后此处展示依赖关系</div>'; return; }
|
|
@@ -764,22 +773,24 @@ es.onmessage = (e)=>{
|
|
|
764
773
|
}
|
|
765
774
|
if(ev.type==='ws_warning'){ toast('⚠ 工作区路径无效:'+ev.path+',任务将在默认目录执行'); }
|
|
766
775
|
if(ev.type==='followup_start'||ev.type==='followup_done'){ loadCards(); }
|
|
767
|
-
|
|
776
|
+
// text 为正文快照(不改变卡片状态):仅实时渲染,不触发全量刷新,避免流式期间频繁重建 DOM
|
|
777
|
+
if(['task_start','tool','task_done','proc'].includes(ev.type)){
|
|
768
778
|
loadCards();
|
|
769
779
|
refreshProcs();
|
|
770
780
|
if(OPEN_ID && ev.cardId===OPEN_ID){
|
|
771
781
|
const log=$('#dLog'); if(log.querySelector('.empty')) log.innerHTML='';
|
|
772
782
|
if(ev.type==='task_start') TEXT_PARTS.clear();
|
|
773
|
-
if(ev.type==='text') renderSnapText(ev);
|
|
774
783
|
else if(ev.type==='tool'){ appendLog(log,'🔧 ['+ev.name+'] '+(ev.summary||''),'msg-tool'); log.scrollTop=log.scrollHeight; }
|
|
775
|
-
else if(ev.type==='task_done'){ loadCards().then(()=>
|
|
784
|
+
else if(ev.type==='task_done'){ loadCards().then(()=>{ if(OPEN_ID===ev.cardId) openDetail(ev.cardId); }); }
|
|
776
785
|
}
|
|
777
786
|
}
|
|
787
|
+
// 正文快照独立渲染(不依赖上面的状态事件)
|
|
788
|
+
if(ev.type==='text' && OPEN_ID===ev.cardId) renderSnapText(ev);
|
|
778
789
|
if(ev.type==='followup_start' && OPEN_ID===ev.cardId) TEXT_PARTS.clear();
|
|
779
790
|
if(ev.type==='followup_done' && OPEN_ID===ev.cardId){
|
|
780
791
|
fuSetState(false);
|
|
781
792
|
if(ev.error){ appendLog($('#dLog'),'⚠ '+ev.error,'msg-err'); toast('追加聊天失败:'+errSummary(ev.error)); }
|
|
782
|
-
loadCards().then(()=>
|
|
793
|
+
loadCards().then(()=>{ if(OPEN_ID===ev.cardId) openDetail(ev.cardId); });
|
|
783
794
|
}
|
|
784
795
|
};
|
|
785
796
|
es.onerror=()=>{};
|
package/app/public/index.html
CHANGED
|
@@ -634,7 +634,7 @@
|
|
|
634
634
|
</div>
|
|
635
635
|
|
|
636
636
|
<script>
|
|
637
|
-
const PAGE_VERSION = '3.19.
|
|
637
|
+
const PAGE_VERSION = '3.19.2'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
|
|
638
638
|
const AV_COLORS = ['#5b8def','#07c160','#fa9d3b','#10aeff','#8a6fe8','#fa5151','#e8a33d','#3aa7a3'];
|
|
639
639
|
const PHASE_LABEL = { plan: '调度规划', work: '执行', review: '验收', report: '汇总', talk: '圆桌发言', task: '任务执行' };
|
|
640
640
|
// 职业图标库(智能体配置可选)
|
package/app/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Agents Chat Portable - 零依赖 HTTP 服务
|
|
2
2
|
// 启动:node app/server.js [--port 3456]
|
|
3
|
-
const APP_VERSION = '3.19.
|
|
3
|
+
const APP_VERSION = '3.19.2'; // 页面与服务端版本互检,不一致提示强刷
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
@@ -968,22 +968,24 @@ const server = http.createServer(async (req, res) => {
|
|
|
968
968
|
}
|
|
969
969
|
if (p === '/api/cards/config' && req.method === 'POST') {
|
|
970
970
|
const body = await readBody(req);
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const ws = (cfg.workspace || '').trim();
|
|
975
|
-
if (body.workspace !== undefined && ws) {
|
|
976
|
-
try { if (!fs.existsSync(ws) || !fs.statSync(ws).isDirectory()) warning = `工作区路径当前不存在或不是目录:${ws},任务执行时将回退到默认目录`; } catch { warning = `工作区路径无法访问:${ws}`; }
|
|
977
|
-
}
|
|
978
|
-
// 并行度(1-8):热更新,调度器每轮 tick 动态读取
|
|
971
|
+
// 按 patch 语义合并:仅写入请求中显式出现的字段(避免单项更新时清空另一项)
|
|
972
|
+
const patch = {};
|
|
973
|
+
if (body.workspace !== undefined) patch.workspace = String(body.workspace || '').trim().slice(0, 500);
|
|
979
974
|
let parallelBad = false;
|
|
980
975
|
if (body.maxParallel !== undefined) {
|
|
981
976
|
const n = Number(body.maxParallel);
|
|
982
|
-
if (n >= 1 && n <= 8)
|
|
977
|
+
if (n >= 1 && n <= 8) patch.maxParallel = Math.floor(n);
|
|
983
978
|
else parallelBad = true;
|
|
984
979
|
}
|
|
985
980
|
if (parallelBad) { json(res, 400, { success: false, error: 'maxParallel 需在 1-8 之间' }); return; }
|
|
986
|
-
|
|
981
|
+
const cfg = Object.keys(patch).length ? CardStore.setConfig(patch) : CardStore.getConfig();
|
|
982
|
+
// 工作区校验:保存允许(可能是尚未创建的目录),但路径不存在时显式警告
|
|
983
|
+
let warning = '';
|
|
984
|
+
const ws = (cfg.workspace || '').trim();
|
|
985
|
+
if (patch.workspace !== undefined && ws) {
|
|
986
|
+
try { if (!fs.existsSync(ws) || !fs.statSync(ws).isDirectory()) warning = `工作区路径当前不存在或不是目录:${ws},任务执行时将回退到默认目录`; } catch { warning = `工作区路径无法访问:${ws}`; }
|
|
987
|
+
}
|
|
988
|
+
json(res, 200, { success: true, config: cfg, warning });
|
|
987
989
|
return;
|
|
988
990
|
}
|
|
989
991
|
|
|
@@ -1109,9 +1111,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1109
1111
|
if (body.title !== undefined) patch.title = String(body.title).slice(0, 500);
|
|
1110
1112
|
if (body.content !== undefined) patch.content = String(body.content).slice(0, 20000);
|
|
1111
1113
|
if (body.priority !== undefined) patch.priority = Number(body.priority) || 999;
|
|
1112
|
-
if (body.mode !== undefined) patch.mode = body.mode;
|
|
1113
|
-
if (body.chainId !== undefined) patch.chainId = body.chainId;
|
|
1114
|
-
if (Array.isArray(body.dependsOn)) patch.dependsOn = body.dependsOn.map(String);
|
|
1114
|
+
if (body.mode !== undefined) patch.mode = ['new', 'continue', 'parallel'].includes(body.mode) ? body.mode : 'new';
|
|
1115
|
+
if (body.chainId !== undefined) patch.chainId = String(body.chainId).slice(0, 100);
|
|
1116
|
+
if (Array.isArray(body.dependsOn)) patch.dependsOn = body.dependsOn.map(String).slice(0, 50);
|
|
1115
1117
|
if (body.model !== undefined) patch.model = String(body.model).slice(0, 80);
|
|
1116
1118
|
// 状态手动复位:failed/pending -> pending 可重跑
|
|
1117
1119
|
if (body.status === 'pending') { patch.status = 'pending'; patch.result = ''; patch.error = ''; patch.ocSessionId = ''; }
|
|
@@ -1152,7 +1154,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1152
1154
|
// SSE:实时推送卡牌生命周期事件(task_start/text/tool/task_done/all_done/runner_*)
|
|
1153
1155
|
const send = sse(req, res);
|
|
1154
1156
|
const unsub = sseSubscribe(send);
|
|
1155
|
-
send({ type: 'init', running: cardRunner.isRunning(), maxParallel:
|
|
1157
|
+
send({ type: 'init', running: cardRunner.isRunning(), maxParallel: cardRunner.maxP() });
|
|
1156
1158
|
req.on('close', unsub);
|
|
1157
1159
|
return;
|
|
1158
1160
|
}
|