@yeaft/webchat-agent 1.0.76 → 1.0.78

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.
@@ -1,274 +0,0 @@
1
- /**
2
- * Crew — UI 消息辅助函数
3
- * sendCrewMessage, sendCrewOutput, sendStatusUpdate, endRoleStreaming, findActiveRole
4
- */
5
- import ctx from '../context.js';
6
-
7
- /**
8
- * 发送 crew 消息到 server(透传到 Web)
9
- */
10
- export function sendCrewMessage(msg) {
11
- if (ctx.sendToServer) {
12
- ctx.sendToServer(msg);
13
- }
14
- }
15
-
16
- /** Format role label: "icon displayName" or just "displayName" if no icon */
17
- function roleLabel(r) {
18
- return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
19
- }
20
-
21
- /**
22
- * 结束指定角色的最后一条 streaming 消息(反向搜索)
23
- */
24
- export function endRoleStreaming(session, roleName) {
25
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
26
- if (session.uiMessages[i].role === roleName && session.uiMessages[i]._streaming) {
27
- delete session.uiMessages[i]._streaming;
28
- break;
29
- }
30
- }
31
- }
32
-
33
- /**
34
- * 找到当前活跃的角色(最近一个 turnActive 的)
35
- */
36
- export function findActiveRole(session) {
37
- for (const [name, state] of session.roleStates) {
38
- if (state.turnActive) return name;
39
- }
40
- return null;
41
- }
42
-
43
- /**
44
- * 发送角色输出到 Web
45
- */
46
- export function sendCrewOutput(session, roleName, outputType, rawMessage, extra = {}) {
47
- const role = session.roles.get(roleName);
48
- const roleIcon = role?.icon || '';
49
- const displayName = role?.displayName || roleName;
50
- const isDecisionMaker = !!(role && role.isDecisionMaker);
51
-
52
- // 从 extra 或 roleState 获取当前 task 信息(extra 优先)
53
- const roleState = session.roleStates.get(roleName);
54
- const taskId = extra.taskId || roleState?.currentTask?.taskId || null;
55
- const taskTitle = extra.taskTitle || roleState?.currentTask?.taskTitle || null;
56
-
57
- // 清除 extra 中的 taskId/taskTitle 避免重复展开到 WebSocket 消息
58
- const { taskId: _tid, taskTitle: _tt, ...extraRest } = extra;
59
-
60
- sendCrewMessage({
61
- type: 'crew_output',
62
- sessionId: session.id,
63
- role: roleName,
64
- roleIcon,
65
- roleName: displayName,
66
- outputType, // 'text' | 'tool_use' | 'tool_result' | 'route' | 'system'
67
- data: rawMessage,
68
- taskId,
69
- taskTitle,
70
- ...extraRest
71
- });
72
-
73
- // ★ 累积 feature 到持久化列表
74
- if (taskId && taskTitle && !session.features.has(taskId)) {
75
- session.features.set(taskId, { taskId, taskTitle, createdAt: Date.now() });
76
- }
77
-
78
- // ★ 记录精简 UI 消息用于恢复(跳过 tool_use/tool_result,只记录可见内容)
79
- if (outputType === 'text') {
80
- const content = rawMessage?.message?.content;
81
- let text = '';
82
- if (typeof content === 'string') {
83
- text = content;
84
- } else if (Array.isArray(content)) {
85
- text = content.filter(b => b.type === 'text').map(b => b.text).join('');
86
- }
87
- if (!text) return;
88
- // ★ 反向搜索该角色最后一条 _streaming 消息
89
- let found = false;
90
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
91
- const msg = session.uiMessages[i];
92
- if (msg.role === roleName && msg.type === 'text' && msg._streaming) {
93
- msg.content += text;
94
- found = true;
95
- break;
96
- }
97
- }
98
- if (!found) {
99
- session.uiMessages.push({
100
- role: roleName, roleIcon, roleName: displayName,
101
- type: 'text', content: text, _streaming: true,
102
- taskId, taskTitle, isDecisionMaker,
103
- timestamp: Date.now()
104
- });
105
- }
106
- } else if (outputType === 'route') {
107
- // 结束该角色前一条 streaming
108
- endRoleStreaming(session, roleName);
109
- session.uiMessages.push({
110
- role: roleName, roleIcon, roleName: displayName,
111
- type: 'route', routeTo: extra.routeTo,
112
- routeSummary: extra.routeSummary || '',
113
- round: session.round || 0,
114
- content: `→ @${extra.routeTo} ${extra.routeSummary || ''}`,
115
- taskId, taskTitle, isDecisionMaker,
116
- timestamp: Date.now()
117
- });
118
- } else if (outputType === 'system') {
119
- const content = rawMessage?.message?.content;
120
- let text = '';
121
- if (typeof content === 'string') {
122
- text = content;
123
- } else if (Array.isArray(content)) {
124
- text = content.filter(b => b.type === 'text').map(b => b.text).join('');
125
- }
126
- if (!text) return;
127
- session.uiMessages.push({
128
- role: roleName, roleIcon, roleName: displayName,
129
- type: 'system', content: text,
130
- timestamp: Date.now()
131
- });
132
- } else if (outputType === 'tool_use') {
133
- // 结束该角色前一条 streaming
134
- endRoleStreaming(session, roleName);
135
- const content = rawMessage?.message?.content;
136
- if (Array.isArray(content)) {
137
- for (const block of content) {
138
- if (block.type === 'tool_use') {
139
- // Save trimmed toolInput for restore
140
- const input = block.input || {};
141
- let savedInput;
142
- if (block.name === 'TodoWrite' || block.name === 'AskUserQuestion') {
143
- savedInput = input;
144
- } else {
145
- const trimmedInput = {};
146
- if (input.file_path) trimmedInput.file_path = input.file_path;
147
- if (input.command) trimmedInput.command = input.command.substring(0, 200);
148
- if (input.pattern) trimmedInput.pattern = input.pattern;
149
- if (input.old_string) trimmedInput.old_string = input.old_string.substring(0, 100);
150
- if (input.new_string) trimmedInput.new_string = input.new_string.substring(0, 100);
151
- if (input.url) trimmedInput.url = input.url;
152
- if (input.query) trimmedInput.query = input.query;
153
- savedInput = Object.keys(trimmedInput).length > 0 ? trimmedInput : null;
154
- }
155
- session.uiMessages.push({
156
- role: roleName, roleIcon, roleName: displayName,
157
- type: 'tool',
158
- toolName: block.name,
159
- toolId: block.id,
160
- toolInput: savedInput,
161
- content: `${block.name} ${block.input?.file_path || block.input?.command?.substring(0, 60) || ''}`,
162
- hasResult: false,
163
- taskId, taskTitle, isDecisionMaker,
164
- timestamp: Date.now()
165
- });
166
- }
167
- }
168
- }
169
- } else if (outputType === 'tool_result') {
170
- // 标记对应 tool 的 hasResult
171
- const toolId = rawMessage?.message?.tool_use_id;
172
- if (toolId) {
173
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
174
- if (session.uiMessages[i].type === 'tool' && session.uiMessages[i].toolId === toolId) {
175
- session.uiMessages[i].hasResult = true;
176
- break;
177
- }
178
- }
179
- }
180
- // Check for image blocks in tool_result content
181
- const resultContent = rawMessage?.message?.content;
182
- if (Array.isArray(resultContent)) {
183
- for (const item of resultContent) {
184
- if (item.type === 'image' && item.source?.type === 'base64') {
185
- sendCrewMessage({
186
- type: 'crew_image',
187
- sessionId: session.id,
188
- role: roleName,
189
- roleIcon,
190
- roleName: displayName,
191
- toolId: toolId || '',
192
- mimeType: item.source.media_type,
193
- data: item.source.data,
194
- taskId, taskTitle
195
- });
196
- session.uiMessages.push({
197
- role: roleName, roleIcon, roleName: displayName,
198
- type: 'image', toolId: toolId || '',
199
- mimeType: item.source.media_type,
200
- taskId, taskTitle, isDecisionMaker,
201
- timestamp: Date.now()
202
- });
203
- // ★ Collect turn images for auto-attach on ROUTE (last 3 per turn)
204
- const roleState = session.roleStates.get(roleName);
205
- if (roleState) {
206
- if (!roleState.turnImages) roleState.turnImages = [];
207
- roleState.turnImages.push({
208
- mimeType: item.source.media_type,
209
- data: item.source.data
210
- });
211
- // Cap at last 3 images per turn
212
- if (roleState.turnImages.length > 3) {
213
- roleState.turnImages = roleState.turnImages.slice(-3);
214
- }
215
- }
216
- }
217
- }
218
- }
219
- }
220
- // tool 只保存精简信息(toolName + 摘要),不存完整 toolInput/toolResult
221
- }
222
-
223
- /**
224
- * 从当前活跃 messages(不含历史 shard)中提取有消息的 features。
225
- * 避免发送全量 features 到前端导致 feature panel 渲染卡顿。
226
- */
227
- function getActiveFeatures(session) {
228
- const activeTaskIds = new Set();
229
- for (const m of session.uiMessages) {
230
- if (m.taskId) activeTaskIds.add(m.taskId);
231
- }
232
- return Array.from(session.features.values())
233
- .filter(f => activeTaskIds.has(f.taskId));
234
- }
235
-
236
- /**
237
- * 发送 session 状态更新
238
- */
239
- export function sendStatusUpdate(session) {
240
- const currentRole = findActiveRole(session);
241
-
242
- sendCrewMessage({
243
- type: 'crew_status',
244
- sessionId: session.id,
245
- status: session.status,
246
- currentRole,
247
- round: session.round,
248
- costUsd: session.costUsd,
249
- totalInputTokens: session.totalInputTokens,
250
- totalOutputTokens: session.totalOutputTokens,
251
- roles: Array.from(session.roles.values()).map(r => ({
252
- name: r.name,
253
- displayName: r.displayName,
254
- icon: r.icon,
255
- description: r.description,
256
- isDecisionMaker: r.isDecisionMaker || false,
257
- model: r.model,
258
- roleType: r.roleType,
259
- groupIndex: r.groupIndex
260
- })),
261
- activeRoles: Array.from(session.roleStates.entries())
262
- .filter(([, s]) => s.turnActive)
263
- .map(([name]) => name),
264
- currentToolByRole: Object.fromEntries(
265
- Array.from(session.roleStates.entries())
266
- .filter(([, s]) => s.turnActive && s.currentTool)
267
- .map(([name, s]) => [name, s.currentTool])
268
- ),
269
- features: getActiveFeatures(session),
270
- initProgress: session.initProgress || null
271
- });
272
- // Persist is NOT called here — callers that change persistent state
273
- // (status, roles, cost, messages) must call saveSessionMeta explicitly.
274
- }
package/crew/worktree.js DELETED
@@ -1,130 +0,0 @@
1
- /**
2
- * Crew — Git Worktree 管理
3
- * 为开发组创建/清理 git worktrees
4
- */
5
- import { promises as fs } from 'fs';
6
- import { join } from 'path';
7
- import { execFile as execFileCb } from 'child_process';
8
- import { promisify } from 'util';
9
-
10
- const execFile = promisify(execFileCb);
11
-
12
- /**
13
- * 为开发组创建 git worktree
14
- * 每个 groupIndex 对应一个 worktree,同组的 dev/rev/test 共享
15
- *
16
- * @param {string} projectDir - 主项目目录
17
- * @param {Array} roles - 展开后的角色列表
18
- * @returns {Map<number, string>} groupIndex → worktree 路径
19
- */
20
- export async function initWorktrees(projectDir, roles) {
21
- const groupIndices = [...new Set(roles.filter(r => r.groupIndex > 0).map(r => r.groupIndex))];
22
- if (groupIndices.length === 0) return new Map();
23
-
24
- const worktreeBase = join(projectDir, '.worktrees');
25
- await fs.mkdir(worktreeBase, { recursive: true });
26
-
27
- // 获取 git 已知的 worktree 列表
28
- let knownWorktrees = new Set();
29
- try {
30
- const { stdout } = await execFile('git', ['worktree', 'list', '--porcelain'], { cwd: projectDir, windowsHide: true });
31
- for (const line of stdout.split('\n')) {
32
- if (line.startsWith('worktree ')) {
33
- knownWorktrees.add(line.slice('worktree '.length).trim());
34
- }
35
- }
36
- } catch {
37
- // git worktree list 失败,视为空集
38
- }
39
-
40
- const worktreeMap = new Map();
41
-
42
- for (const idx of groupIndices) {
43
- const wtDir = join(worktreeBase, `dev-${idx}`);
44
- const branch = `crew/dev-${idx}`;
45
-
46
- // 检查目录是否存在
47
- let dirExists = false;
48
- try {
49
- await fs.access(wtDir);
50
- dirExists = true;
51
- } catch {}
52
-
53
- if (dirExists) {
54
- if (knownWorktrees.has(wtDir)) {
55
- // 目录存在且 git 记录中也有,直接复用
56
- console.log(`[Crew] Worktree already exists: ${wtDir}`);
57
- worktreeMap.set(idx, wtDir);
58
- continue;
59
- } else {
60
- // 孤立目录:目录存在但 git 不认识,先删除再重建
61
- console.warn(`[Crew] Orphaned worktree dir, removing: ${wtDir}`);
62
- await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
63
- }
64
- }
65
-
66
- try {
67
- // 创建分支(如果不存在)
68
- try {
69
- await execFile('git', ['branch', branch], { cwd: projectDir, windowsHide: true });
70
- } catch {
71
- // 分支已存在,忽略
72
- }
73
-
74
- // 创建 worktree
75
- await execFile('git', ['worktree', 'add', wtDir, branch], { cwd: projectDir, windowsHide: true });
76
- console.log(`[Crew] Created worktree: ${wtDir} on branch ${branch}`);
77
- worktreeMap.set(idx, wtDir);
78
- } catch (e) {
79
- console.error(`[Crew] Failed to create worktree for group ${idx}:`, e.message);
80
- }
81
- }
82
-
83
- return worktreeMap;
84
- }
85
-
86
- /**
87
- * 清理 session 的 git worktrees
88
- * @param {string} projectDir - 主项目目录
89
- */
90
- export async function cleanupWorktrees(projectDir) {
91
- const worktreeBase = join(projectDir, '.worktrees');
92
-
93
- try {
94
- await fs.access(worktreeBase);
95
- } catch {
96
- return; // .worktrees 目录不存在,无需清理
97
- }
98
-
99
- try {
100
- const entries = await fs.readdir(worktreeBase);
101
- for (const entry of entries) {
102
- if (!entry.startsWith('dev-')) continue;
103
- const wtDir = join(worktreeBase, entry);
104
- const branch = `crew/${entry}`;
105
-
106
- try {
107
- await execFile('git', ['worktree', 'remove', wtDir, '--force'], { cwd: projectDir, windowsHide: true });
108
- console.log(`[Crew] Removed worktree: ${wtDir}`);
109
- } catch (e) {
110
- console.warn(`[Crew] Failed to remove worktree ${wtDir}:`, e.message);
111
- }
112
-
113
- try {
114
- await execFile('git', ['branch', '-D', branch], { cwd: projectDir, windowsHide: true });
115
- console.log(`[Crew] Deleted branch: ${branch}`);
116
- } catch (e) {
117
- console.warn(`[Crew] Failed to delete branch ${branch}:`, e.message);
118
- }
119
- }
120
-
121
- // 尝试删除 .worktrees 目录(如果已空)
122
- try {
123
- await fs.rmdir(worktreeBase);
124
- } catch {
125
- // 目录不空或其他原因,忽略
126
- }
127
- } catch (e) {
128
- console.error(`[Crew] Failed to cleanup worktrees:`, e.message);
129
- }
130
- }