@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 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 done = all.filter(c => c.status === 'done').length;
288
- const failed = all.filter(c => c.status === 'failed').length;
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
- result: (doneError ? `执行出错:${doneError}` : finalText).slice(0, 20000),
400
- error: doneError || '',
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.followups && this.followups.has(cardId)) return { ok: false, error: '该任务已有追加聊天进行中' };
425
- if (!this.followups) this.followups = new Set();
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();
@@ -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
- OPEN_ID=id; const data=await api('/api/cards/'+id+'/log'); const card=data.card; const msgs=data.messages||[];
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
- if(['task_start','text','tool','task_done','proc'].includes(ev.type)){
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(()=>openDetail(OPEN_ID)); }
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(()=>openDetail(OPEN_ID));
793
+ loadCards().then(()=>{ if(OPEN_ID===ev.cardId) openDetail(ev.cardId); });
783
794
  }
784
795
  };
785
796
  es.onerror=()=>{};
@@ -634,7 +634,7 @@
634
634
  </div>
635
635
 
636
636
  <script>
637
- const PAGE_VERSION = '3.19.0'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
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.0'; // 页面与服务端版本互检,不一致提示强刷
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
- const cfg = CardStore.setConfig({ workspace: String(body.workspace || '').trim().slice(0, 500) });
972
- // 工作区校验:保存允许(可能是尚未创建的目录),但路径不存在时显式警告
973
- let warning = '';
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) CardStore.setConfig({ maxParallel: Math.floor(n) });
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
- json(res, 200, { success: true, config: CardStore.getConfig(), warning });
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: MAX_PARALLEL });
1157
+ send({ type: 'init', running: cardRunner.isRunning(), maxParallel: cardRunner.maxP() });
1156
1158
  req.on('close', unsub);
1157
1159
  return;
1158
1160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamsamyiok/agents-chat",
3
- "version": "3.19.0",
3
+ "version": "3.19.2",
4
4
  "description": "多智能体群聊工具 - 支持 OpenCode/Claude Code/Codex/pi 内核,微信风格聊天界面",
5
5
  "main": "lib/start.js",
6
6
  "bin": {