@iamsamyiok/agents-chat 3.19.2 → 3.20.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/cards.js CHANGED
@@ -209,6 +209,24 @@ function isEligible(card, all) {
209
209
  return true;
210
210
  }
211
211
 
212
+ // 环检测:把 cardId 的依赖改为 newDeps 后,沿依赖边 DFS 是否能回到 cardId 自身
213
+ // 用于 PUT 写入拦截(间接环也拦截,如 A→B→C→A)
214
+ function wouldCycle(cardId, newDeps) {
215
+ const all = CardStore.list();
216
+ const byId = new Map(all.filter(c => c.id !== cardId).map(c => [c.id, c]));
217
+ const seen = new Set();
218
+ const stack = [...(newDeps || [])];
219
+ while (stack.length) {
220
+ const cur = stack.pop();
221
+ if (cur === cardId) return true;
222
+ if (seen.has(cur)) continue;
223
+ seen.add(cur);
224
+ const c = byId.get(cur);
225
+ if (c) stack.push(...(c.dependsOn || []));
226
+ }
227
+ return false;
228
+ }
229
+
212
230
  function pickEligible(all, activeIds) {
213
231
  const eligible = all.filter(c => isEligible(c, all) && !activeIds.has(c.id));
214
232
  eligible.sort((a, b) => {
@@ -529,4 +547,4 @@ function isValidDir(p) {
529
547
 
530
548
  const runner = new CardRunner();
531
549
 
532
- module.exports = { CardStore, CardRunner, runner, sseSubscribe, buildCardPrompt, isEligible, pickEligible, MAX_PARALLEL, CARDS_PATH, CARDS_CFG_PATH };
550
+ module.exports = { CardStore, CardRunner, runner, sseSubscribe, buildCardPrompt, isEligible, pickEligible, wouldCycle, MAX_PARALLEL, CARDS_PATH, CARDS_CFG_PATH };
@@ -310,10 +310,20 @@ function errSummary(err){
310
310
  if(!err) return '';
311
311
  return String(err).split('\n').map(s=>s.trim()).filter(Boolean)[0] || '';
312
312
  }
313
- // 依赖阻塞判定:pending 卡的依赖存在未完成(等待)或已失败(阻塞)
313
+ // 依赖阻塞判定:pending 卡的依赖存在未完成(等待)或已失败(阻塞);依赖链成环时明确提示
314
314
  function blockedInfo(card){
315
315
  if(card.status!=='pending' || !(card.dependsOn&&card.dependsOn.length)) return null;
316
316
  const byId=Object.fromEntries(CARDS.map(c=>[c.id,c]));
317
+ // 环检测:沿依赖边 DFS,回到自身即成环(历史数据兜底,正常写入已被服务端拦截)
318
+ const seen=new Set(), stack=[...card.dependsOn];
319
+ while(stack.length){
320
+ const cur=stack.pop();
321
+ if(cur===card.id) return {kind:'fail',text:'循环依赖:依赖链最终依赖本任务,全量执行时将被跳过'};
322
+ if(seen.has(cur)) continue;
323
+ seen.add(cur);
324
+ const c=byId[cur];
325
+ if(c) stack.push(...(c.dependsOn||[]));
326
+ }
317
327
  const failedDeps=[], waitDeps=[];
318
328
  for(const d of card.dependsOn){
319
329
  const dc=byId[d];
@@ -634,7 +634,7 @@
634
634
  </div>
635
635
 
636
636
  <script>
637
- const PAGE_VERSION = '3.19.2'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
637
+ const PAGE_VERSION = '3.20.0'; // 与服务端 /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
  // 职业图标库(智能体配置可选)
@@ -2319,7 +2319,6 @@ async function doImport() {
2319
2319
  (data.warnings || []).forEach(w => chatMode === 'solo' ? addSoloSys('⚠ ' + w) : addSys('⚠ ' + w));
2320
2320
  closeImport();
2321
2321
  importText.value = '';
2322
- if (importMode === 'scheduled' && schedOpen) renderSchedList();
2323
2322
  } else toast(data.error || '导入失败');
2324
2323
  }
2325
2324
 
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.2'; // 页面与服务端版本互检,不一致提示强刷
3
+ const APP_VERSION = '3.20.0'; // 页面与服务端版本互检,不一致提示强刷
4
4
  const http = require('http');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
@@ -38,7 +38,7 @@ const { runAgent, stopScope, stopAllChildren } = require('./lib/agent');
38
38
  const { runButler, runMentioned, runRoundtable, runTasks, prepareRerun } = require('./lib/orchestrator');
39
39
  const oc = require('./lib/oc');
40
40
  const memoryMod = require('./lib/memory');
41
- const { CardStore, runner: cardRunner, sseSubscribe, MAX_PARALLEL } = require('./lib/cards');
41
+ const { CardStore, runner: cardRunner, sseSubscribe, MAX_PARALLEL, wouldCycle } = require('./lib/cards');
42
42
 
43
43
  // ---------- 人工审批关卡:orchestrator 暂停等待用户放行(方案/交付),SSE 断线后可经 /api/approvals 恢复 ----------
44
44
  const pendingApprovals = new Map(); // approvalId -> {kind,label,taskId,resolve,timer}
@@ -1071,16 +1071,22 @@ const server = http.createServer(async (req, res) => {
1071
1071
  const card = CardStore.get(id);
1072
1072
  if (!card) { json(res, 404, { success: false, error: '卡牌不存在' }); return; }
1073
1073
  const msgs = store.getMessages(id);
1074
+ // 中文友好的日期时间(YYYY-MM-DD HH:mm,避免随系统 locale 变成英文格式)
1075
+ const fmtDateTime = (t) => {
1076
+ const d = new Date(t || Date.now());
1077
+ const p2 = (n) => String(n).padStart(2, '0');
1078
+ return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`;
1079
+ };
1074
1080
  const roleOf = (m) => (m.role === 'user' ? '用户' : (m.phase === 'archive' ? '归档' : (m.phase === 'system' ? '系统' : 'Agent')));
1075
1081
  const lines = [
1076
1082
  `# ${card.title}`, '',
1077
1083
  `- 状态:${card.status}|模式:${card.mode}|优先级:P${card.priority}`,
1078
- `- 创建:${new Date(card.createdAt).toLocaleString()}${card.finishedAt ? `|完成:${new Date(card.finishedAt).toLocaleString()}` : ''}`,
1084
+ `- 创建:${fmtDateTime(card.createdAt)}${card.finishedAt ? `|完成:${fmtDateTime(card.finishedAt)}` : ''}`,
1079
1085
  card.error ? `- 失败原因:${card.error.replace(/\n/g, ' ')}` : '',
1080
1086
  '', '## 任务内容', '', String(card.content || '').trim(), '', '## 执行过程', ''
1081
1087
  ];
1082
1088
  for (const m of msgs) {
1083
- lines.push(`### ${new Date(m.timestamp || Date.now()).toLocaleString()} · ${roleOf(m)}`, '');
1089
+ lines.push(`### ${fmtDateTime(m.timestamp || Date.now())} · ${roleOf(m)}`, '');
1084
1090
  lines.push(String(m.content || '').trim() || '(无内容)');
1085
1091
  lines.push('');
1086
1092
  }
@@ -1113,7 +1119,12 @@ const server = http.createServer(async (req, res) => {
1113
1119
  if (body.priority !== undefined) patch.priority = Number(body.priority) || 999;
1114
1120
  if (body.mode !== undefined) patch.mode = ['new', 'continue', 'parallel'].includes(body.mode) ? body.mode : 'new';
1115
1121
  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);
1122
+ if (Array.isArray(body.dependsOn)) {
1123
+ const deps = body.dependsOn.map(String).slice(0, 50);
1124
+ // 环检测:修改依赖后若形成(直接或间接)循环依赖则拒绝写入
1125
+ if (wouldCycle(id, deps)) { json(res, 409, { success: false, error: '循环依赖:该任务的依赖链最终会依赖它自己,请调整依赖关系' }); return; }
1126
+ patch.dependsOn = deps;
1127
+ }
1117
1128
  if (body.model !== undefined) patch.model = String(body.model).slice(0, 80);
1118
1129
  // 状态手动复位:failed/pending -> pending 可重跑
1119
1130
  if (body.status === 'pending') { patch.status = 'pending'; patch.result = ''; patch.error = ''; patch.ocSessionId = ''; }
@@ -1125,6 +1136,7 @@ const server = http.createServer(async (req, res) => {
1125
1136
 
1126
1137
  if (p.startsWith('/api/cards/') && req.method === 'DELETE') {
1127
1138
  const id = p.slice('/api/cards/'.length);
1139
+ if (!CardStore.get(id)) { json(res, 404, { success: false, error: '卡牌不存在' }); return; }
1128
1140
  cardRunner.killCard(id);
1129
1141
  CardStore.remove(id);
1130
1142
  json(res, 200, { success: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamsamyiok/agents-chat",
3
- "version": "3.19.2",
3
+ "version": "3.20.0",
4
4
  "description": "多智能体群聊工具 - 支持 OpenCode/Claude Code/Codex/pi 内核,微信风格聊天界面",
5
5
  "main": "lib/start.js",
6
6
  "bin": {