@yeaft/webchat-agent 0.0.187 → 0.0.196

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.
Files changed (2) hide show
  1. package/crew.js +233 -36
  2. package/package.json +1 -1
package/crew.js CHANGED
@@ -324,10 +324,12 @@ async function saveSessionMeta(session) {
324
324
  userId: session.userId,
325
325
  username: session.username,
326
326
  agentId: session.agentId || null,
327
+ teamType: session.teamType || 'dev',
327
328
  costUsd: session.costUsd,
328
329
  totalInputTokens: session.totalInputTokens,
329
330
  totalOutputTokens: session.totalOutputTokens,
330
- features: Array.from(session.features.values())
331
+ features: Array.from(session.features.values()),
332
+ _completedTaskIds: Array.from(session._completedTaskIds || [])
331
333
  };
332
334
  await fs.writeFile(join(session.sharedDir, 'session.json'), JSON.stringify(meta, null, 2));
333
335
  // 保存 UI 消息历史(用于恢复时重放)
@@ -548,9 +550,11 @@ export async function resumeCrewSession(msg) {
548
550
  waitingHumanContext: null,
549
551
  pendingRoutes: [],
550
552
  features: new Map((meta.features || []).map(f => [f.taskId, f])),
553
+ _completedTaskIds: new Set(meta._completedTaskIds || []),
551
554
  userId: userId || meta.userId,
552
555
  username: username || meta.username,
553
556
  agentId: meta.agentId || ctx.CONFIG?.agentName || null,
557
+ teamType: meta.teamType || 'dev',
554
558
  createdAt: meta.createdAt || Date.now()
555
559
  };
556
560
  crewSessions.set(sessionId, session);
@@ -635,6 +639,7 @@ export async function createCrewSession(msg) {
635
639
  sharedKnowledge,
636
640
  roles: rawRoles = [], // [{ name, displayName, icon, description, claudeMd, model, budget, isDecisionMaker, count }]
637
641
  maxRounds = 20,
642
+ teamType = 'dev',
638
643
  userId,
639
644
  username
640
645
  } = msg;
@@ -655,23 +660,10 @@ export async function createCrewSession(msg) {
655
660
  ? sharedDirRel
656
661
  : join(projectDir, sharedDirRel || '.crew');
657
662
 
658
- // 初始化共享区
659
- await initSharedDir(sharedDir, goal, roles, projectDir, sharedKnowledge);
660
-
661
- // 初始化 git worktrees(所有 EXPANDABLE_ROLES 都会获得独立 worktree)
662
- const worktreeMap = await initWorktrees(projectDir, roles);
663
- // 回填 workDir:同组的 dev/rev/test 共享同一个 worktree
664
- for (const role of roles) {
665
- if (role.groupIndex > 0 && worktreeMap.has(role.groupIndex)) {
666
- role.workDir = worktreeMap.get(role.groupIndex);
667
- // 重新写入 CLAUDE.md(加入工作目录信息)
668
- await writeRoleClaudeMd(sharedDir, role);
669
- }
670
- }
671
-
672
663
  // 找到决策者
673
664
  const decisionMaker = roles.find(r => r.isDecisionMaker)?.name || roles[0]?.name || null;
674
665
 
666
+ // ★ 阶段1:立即构建 session 并通知前端,让 UI 先显示
675
667
  const session = {
676
668
  id: sessionId,
677
669
  projectDir,
@@ -682,7 +674,7 @@ export async function createCrewSession(msg) {
682
674
  roles: new Map(roles.map(r => [r.name, r])),
683
675
  roleStates: new Map(),
684
676
  decisionMaker,
685
- status: 'running', // running | paused | waiting_human | completed | stopped
677
+ status: 'initializing', // 新增初始化状态
686
678
  round: 0,
687
679
  maxRounds,
688
680
  costUsd: 0,
@@ -694,15 +686,18 @@ export async function createCrewSession(msg) {
694
686
  waitingHumanContext: null, // { fromRole, reason, message }
695
687
  pendingRoutes: [], // [{ fromRole, route }] — 暂停时未完成的路由
696
688
  features: new Map(), // taskId → { taskId, taskTitle, createdAt } — 持久化 feature 列表
689
+ _completedTaskIds: new Set(), // 已完成的 taskId 集合(用于检测新完成的任务)
690
+ initProgress: null, // 'roles' | 'worktrees' | null — 初始化阶段
697
691
  userId,
698
692
  username,
699
693
  agentId: ctx.CONFIG?.agentName || null,
694
+ teamType,
700
695
  createdAt: Date.now()
701
696
  };
702
697
 
703
698
  crewSessions.set(sessionId, session);
704
699
 
705
- // 通知 server
700
+ // 立即通知前端:session 已创建,可以显示 UI
706
701
  sendCrewMessage({
707
702
  type: 'crew_session_created',
708
703
  sessionId,
@@ -727,20 +722,66 @@ export async function createCrewSession(msg) {
727
722
  username
728
723
  });
729
724
 
730
- // 发送状态
731
725
  sendStatusUpdate(session);
732
726
 
733
- // 持久化到索引和 session.json
734
- await upsertCrewIndex(session);
735
- await saveSessionMeta(session);
727
+ // 阶段2:异步完成文件系统和 worktree 初始化
728
+ try {
729
+ // 初始化共享区(角色目录 + CLAUDE.md)
730
+ session.initProgress = 'roles';
731
+ sendStatusUpdate(session);
732
+ await initSharedDir(sharedDir, goal, roles, projectDir, sharedKnowledge);
733
+
734
+ // 初始化 git worktrees
735
+ const groupIndices = [...new Set(roles.filter(r => r.groupIndex > 0).map(r => r.groupIndex))];
736
+ if (groupIndices.length > 0) {
737
+ session.initProgress = 'worktrees';
738
+ sendStatusUpdate(session);
739
+ }
740
+ const worktreeMap = await initWorktrees(projectDir, roles);
741
+
742
+ // 回填 workDir
743
+ for (const role of roles) {
744
+ if (role.groupIndex > 0 && worktreeMap.has(role.groupIndex)) {
745
+ role.workDir = worktreeMap.get(role.groupIndex);
746
+ await writeRoleClaudeMd(sharedDir, role);
747
+ }
748
+ }
749
+
750
+ // 持久化
751
+ await upsertCrewIndex(session);
752
+ await saveSessionMeta(session);
753
+
754
+ // 初始化完成,仅在 initializing 状态下切换到 running(避免覆盖用户手动暂停/停止)
755
+ if (session.status === 'initializing') {
756
+ session.status = 'running';
757
+ }
758
+ session.initProgress = null;
759
+ sendStatusUpdate(session);
736
760
 
737
- // 如果有目标,自动启动第一个角色;否则等待用户输入
738
- if (goal && roles.length > 0) {
739
- const firstRole = roles.find(r => r.name === 'pm') || roles[0];
740
- if (firstRole) {
741
- const initialPrompt = buildInitialTask(goal, firstRole, roles);
742
- await dispatchToRole(session, firstRole.name, initialPrompt, 'system');
761
+ // 如果有目标且状态为 running,自动启动第一个角色
762
+ if (goal && roles.length > 0 && session.status === 'running') {
763
+ const firstRole = roles.find(r => r.name === 'pm') || roles[0];
764
+ if (firstRole) {
765
+ const initialPrompt = buildInitialTask(goal, firstRole, roles);
766
+ await dispatchToRole(session, firstRole.name, initialPrompt, 'system');
767
+ }
768
+ }
769
+ } catch (e) {
770
+ console.error('[Crew] Session initialization failed:', e);
771
+ if (session.status === 'initializing') {
772
+ session.status = 'running';
743
773
  }
774
+ session.initProgress = null;
775
+ sendStatusUpdate(session);
776
+ sendCrewMessage({
777
+ type: 'crew_output',
778
+ sessionId,
779
+ roleName: 'system',
780
+ roleIcon: 'S',
781
+ roleDisplayName: '系统',
782
+ content: `工作环境初始化失败: ${e.message}`,
783
+ isTurnEnd: true
784
+ });
744
785
  }
745
786
 
746
787
  return session;
@@ -1076,6 +1117,9 @@ ${summary}
1076
1117
  }
1077
1118
 
1078
1119
  console.log(`[Crew] Task file created: ${taskId} (${taskTitle})`);
1120
+
1121
+ // 更新 feature 索引
1122
+ updateFeatureIndex(session).catch(e => console.warn('[Crew] Failed to update feature index:', e.message));
1079
1123
  }
1080
1124
 
1081
1125
  /**
@@ -1113,6 +1157,112 @@ async function readTaskFile(session, taskId) {
1113
1157
  }
1114
1158
  }
1115
1159
 
1160
+ /**
1161
+ * 从 TASKS block 文本中提取已完成任务的 taskId 集合
1162
+ */
1163
+ function parseCompletedTasks(text) {
1164
+ const ids = new Set();
1165
+ const match = text.match(/---TASKS---([\s\S]*?)---END_TASKS---/);
1166
+ if (!match) return ids;
1167
+ for (const line of match[1].split('\n')) {
1168
+ const m = line.match(/^-\s*\[[xX]\]\s*.+#(\S+)/);
1169
+ if (m) ids.add(m[1]);
1170
+ }
1171
+ return ids;
1172
+ }
1173
+
1174
+ /**
1175
+ * 更新 feature 索引文件 context/features/index.md
1176
+ * 全量重建:根据 session.features 和 session._completedTaskIds 生成分类表格
1177
+ */
1178
+ async function updateFeatureIndex(session) {
1179
+ const featuresDir = join(session.sharedDir, 'context', 'features');
1180
+ await fs.mkdir(featuresDir, { recursive: true });
1181
+
1182
+ const completed = session._completedTaskIds || new Set();
1183
+ const allFeatures = Array.from(session.features.values());
1184
+
1185
+ // 按创建时间排序
1186
+ allFeatures.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
1187
+
1188
+ const inProgress = allFeatures.filter(f => !completed.has(f.taskId));
1189
+ const done = allFeatures.filter(f => completed.has(f.taskId));
1190
+
1191
+ const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
1192
+ let content = `# Feature Index\n> 最后更新: ${now}\n`;
1193
+
1194
+ content += `\n## 进行中 (${inProgress.length})\n`;
1195
+ if (inProgress.length > 0) {
1196
+ content += '| task-id | 标题 | 创建时间 |\n|---------|------|----------|\n';
1197
+ for (const f of inProgress) {
1198
+ const date = f.createdAt ? new Date(f.createdAt).toLocaleDateString('zh-CN') : '-';
1199
+ content += `| ${f.taskId} | ${f.taskTitle} | ${date} |\n`;
1200
+ }
1201
+ }
1202
+
1203
+ content += `\n## 已完成 (${done.length})\n`;
1204
+ if (done.length > 0) {
1205
+ content += '| task-id | 标题 | 创建时间 |\n|---------|------|----------|\n';
1206
+ for (const f of done) {
1207
+ const date = f.createdAt ? new Date(f.createdAt).toLocaleDateString('zh-CN') : '-';
1208
+ content += `| ${f.taskId} | ${f.taskTitle} | ${date} |\n`;
1209
+ }
1210
+ }
1211
+
1212
+ await fs.writeFile(join(featuresDir, 'index.md'), content);
1213
+ console.log(`[Crew] Feature index updated: ${inProgress.length} in progress, ${done.length} completed`);
1214
+ }
1215
+
1216
+ /**
1217
+ * 追加完成汇总到 context/changelog.md
1218
+ * 从 feature 文件的工作记录中提取最后一条记录作为摘要
1219
+ */
1220
+ async function appendChangelog(session, taskId, taskTitle) {
1221
+ const contextDir = join(session.sharedDir, 'context');
1222
+ await fs.mkdir(contextDir, { recursive: true });
1223
+ const changelogPath = join(contextDir, 'changelog.md');
1224
+
1225
+ // 读取 feature 文件提取最后一条工作记录作为摘要
1226
+ const taskContent = await readTaskFile(session, taskId);
1227
+ let summaryText = '';
1228
+ if (taskContent) {
1229
+ // 提取最后一个 ### 块作为摘要
1230
+ const records = taskContent.split(/\n### /);
1231
+ if (records.length > 1) {
1232
+ const lastRecord = records[records.length - 1];
1233
+ // 取第一行之后的内容作为摘要(第一行是角色名和时间)
1234
+ const lines = lastRecord.split('\n');
1235
+ summaryText = lines.slice(1).join('\n').trim();
1236
+ }
1237
+ }
1238
+ if (!summaryText) {
1239
+ summaryText = '(无详细摘要)';
1240
+ }
1241
+
1242
+ // 限制摘要长度
1243
+ if (summaryText.length > 500) {
1244
+ summaryText = summaryText.substring(0, 497) + '...';
1245
+ }
1246
+
1247
+ const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
1248
+ const entry = `\n## ${taskId}: ${taskTitle}\n- 完成时间: ${now}\n- 摘要: ${summaryText}\n`;
1249
+
1250
+ // 如果文件不存在,先写 header
1251
+ let exists = false;
1252
+ try {
1253
+ await fs.access(changelogPath);
1254
+ exists = true;
1255
+ } catch {}
1256
+
1257
+ if (!exists) {
1258
+ await fs.writeFile(changelogPath, `# Changelog\n${entry}`);
1259
+ } else {
1260
+ await fs.appendFile(changelogPath, entry);
1261
+ }
1262
+
1263
+ console.log(`[Crew] Changelog appended: ${taskId} (${taskTitle})`);
1264
+ }
1265
+
1116
1266
  // =====================================================================
1117
1267
  // Session Persistence
1118
1268
  // =====================================================================
@@ -1311,21 +1461,37 @@ ${routeTargets.map(r => `- ${r.name}: ${roleLabel(r)} — ${r.description}`).joi
1311
1461
 
1312
1462
  // 决策者额外 prompt
1313
1463
  if (role.isDecisionMaker) {
1464
+ const isDevTeam = session.teamType === 'dev';
1314
1465
 
1315
1466
  prompt += `\n\n# 工具使用
1316
- PM 可以使用所有工具,包括 Read、Grep、Glob、Bash、Edit、Write。代码文件的改动仍建议 ROUTE 给 developer 执行,但不做硬性限制。`;
1467
+ PM 可以使用所有工具,包括 Read、Grep、Glob、Bash、Edit、Write。${isDevTeam ? '代码文件的改动仍建议 ROUTE 给 developer 执行,但不做硬性限制。' : ''}`;
1317
1468
 
1318
1469
  prompt += `\n\n# 决策者职责
1319
1470
  你是团队的决策者。其他角色遇到不确定的情况会请求你的决策。
1320
1471
  - 如果你有足够的信息做出决策,直接决定并 @相关角色执行
1321
1472
  - 如果你需要更多信息,@具体角色请求补充
1322
1473
  - 如果问题超出你的能力范围或需要业务判断,@human 请人类决定
1323
- - 你可以随时审查其他角色的工作并给出反馈
1474
+ - 你可以随时审查其他角色的工作并给出反馈`;
1475
+
1476
+ if (isDevTeam) {
1477
+ prompt += `
1324
1478
  - PM 不做代码分析。收到需求后直接将原始需求 ROUTE 给空闲 dev 做技术分析,dev 分析完返回 PM,PM 再拆分任务并直接分配执行。
1325
1479
  - PM 拥有 commit + push + tag 的自主权。只要修改没有大的 regression 影响(测试全通过),PM 可以自行决定 commit、push 和 tag,无需等待人工确认。只有当改动会直接影响对话交互逻辑时,才需要人工介入审核。`;
1480
+ }
1481
+
1482
+ // 非开发团队:注入讨论模式 prompt
1483
+ if (!isDevTeam) {
1484
+ prompt += `\n\n# 协作模式
1485
+ 这是一个协作讨论团队,不走严格的 PM→执行→审查→测试 工作流。
1486
+ - 角色之间可以自由讨论、相互请教、提出不同意见
1487
+ - 不需要严格的"分配→执行→审查"流程,鼓励角色之间直接对话
1488
+ - 当一个角色需要另一个角色的输入时,直接 ROUTE 给对方并说明需要什么
1489
+ - 决策者负责把控整体方向和最终决策,但日常讨论不需要经过决策者中转
1490
+ - 每次 ROUTE 仍建议包含 task 和 taskTitle 字段,用于消息按 feature 分组显示`;
1491
+ }
1326
1492
 
1327
- // 多实例模式:注入开发组状态和调度规则
1328
- if (hasMultiInstance) {
1493
+ // 多实例模式(仅开发团队使用):注入开发组状态和调度规则
1494
+ if (isDevTeam && hasMultiInstance) {
1329
1495
  // 构建开发组实时状态
1330
1496
  const maxGroup = Math.max(...allRoles.map(r => r.groupIndex));
1331
1497
  const groupLines = [];
@@ -1375,9 +1541,8 @@ summary: 请实现注册页面,包括邮箱验证
1375
1541
  prompt += `\n
1376
1542
  # 工作流终结点
1377
1543
  团队的工作流有明确的结束条件。当以下任一条件满足时,你应该给出总结并结束当前工作流:
1378
- 1. **代码已提交** - 所有代码修改已经 commit(如需要,可让 developer 执行 git commit
1379
- 2. **需要用户输入** - 遇到需要用户决定的问题时,@human 提出具体问题,等待用户回复
1380
- 3. **任务完成** - 所有任务已完成,给出完成总结(列出完成了什么、变更了哪些文件、还有什么后续建议)
1544
+ ${isDevTeam ? '1. **代码已提交** - 所有代码修改已经 commit(如需要,可让 developer 执行 git commit)\n' : ''}${isDevTeam ? '2' : '1'}. **需要用户输入** - 遇到需要用户决定的问题时,@human 提出具体问题,等待用户回复
1545
+ ${isDevTeam ? '3' : '2'}. **任务完成** - 所有任务已完成,给出完成总结(列出完成了什么${isDevTeam ? '、变更了哪些文件' : ''}、还有什么后续建议)
1381
1546
 
1382
1547
  重要:不要无限循环地在角色之间传递。当工作实质性完成时,主动给出总结并结束。
1383
1548
 
@@ -1405,6 +1570,10 @@ summary: 请实现注册页面,包括邮箱验证
1405
1570
  - PM 分配任务时自动创建文件(包含 task-id、标题、需求描述)
1406
1571
  - 每次 ROUTE 传递时自动追加工作记录(角色名、时间、summary)
1407
1572
  - 你收到的消息中会包含 <task-context> 标签,里面是该任务的完整工作记录
1573
+
1574
+ 系统还维护以下文件(自动更新,无需手动管理):
1575
+ - \`context/features/index.md\`:所有 feature 的索引(进行中/已完成分类),快速查看项目状态
1576
+ - \`context/changelog.md\`:已完成任务的变更记录,每个任务完成时自动追加摘要
1408
1577
  你不需要手动创建或更新这些文件,专注于你的本职工作即可。`;
1409
1578
 
1410
1579
  // 执行者角色的组绑定 prompt(count > 1 时)
@@ -1651,6 +1820,33 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
1651
1820
 
1652
1821
  // 解析路由(支持多 ROUTE 块)
1653
1822
  const routes = parseRoutes(roleState.accumulatedText);
1823
+
1824
+ // ★ 决策者 turn 完成:检测 TASKS block 中新完成的任务
1825
+ const roleConfig = session.roles.get(roleName);
1826
+ if (roleConfig?.isDecisionMaker) {
1827
+ const nowCompleted = parseCompletedTasks(roleState.accumulatedText);
1828
+ if (nowCompleted.size > 0) {
1829
+ const prev = session._completedTaskIds || new Set();
1830
+ const newlyDone = [];
1831
+ for (const tid of nowCompleted) {
1832
+ if (!prev.has(tid)) {
1833
+ prev.add(tid);
1834
+ newlyDone.push(tid);
1835
+ }
1836
+ }
1837
+ session._completedTaskIds = prev;
1838
+ if (newlyDone.length > 0) {
1839
+ // 更新索引 + 追加 changelog(fire-and-forget)
1840
+ updateFeatureIndex(session).catch(e => console.warn('[Crew] Failed to update feature index:', e.message));
1841
+ for (const tid of newlyDone) {
1842
+ const feature = session.features.get(tid);
1843
+ const title = feature?.taskTitle || tid;
1844
+ appendChangelog(session, tid, title).catch(e => console.warn(`[Crew] Failed to append changelog for ${tid}:`, e.message));
1845
+ }
1846
+ }
1847
+ }
1848
+ }
1849
+
1654
1850
  roleState.accumulatedText = '';
1655
1851
  roleState.turnActive = false;
1656
1852
 
@@ -1988,7 +2184,7 @@ function buildRoutePrompt(fromRole, summary, session) {
1988
2184
  * 向角色发送消息
1989
2185
  */
1990
2186
  async function dispatchToRole(session, roleName, content, fromSource, taskId, taskTitle) {
1991
- if (session.status === 'paused' || session.status === 'stopped') {
2187
+ if (session.status === 'paused' || session.status === 'stopped' || session.status === 'initializing') {
1992
2188
  console.log(`[Crew] Session ${session.status}, skipping dispatch to ${roleName}`);
1993
2189
  return;
1994
2190
  }
@@ -2769,7 +2965,8 @@ function sendStatusUpdate(session) {
2769
2965
  .filter(([, s]) => s.turnActive && s.currentTool)
2770
2966
  .map(([name, s]) => [name, s.currentTool])
2771
2967
  ),
2772
- features: Array.from(session.features.values())
2968
+ features: Array.from(session.features.values()),
2969
+ initProgress: session.initProgress || null
2773
2970
  });
2774
2971
 
2775
2972
  // 异步更新持久化
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.187",
3
+ "version": "0.0.196",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",