@yeaft/webchat-agent 0.1.353 → 0.1.354
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/crew/role-output.js +5 -1
- package/crew/routing.js +58 -13
- package/crew/ui-messages.js +13 -0
- package/package.json +1 -1
package/crew/role-output.js
CHANGED
|
@@ -185,6 +185,10 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
|
|
|
185
185
|
if (routes.length > 0) {
|
|
186
186
|
session.round++;
|
|
187
187
|
|
|
188
|
+
// ★ Collect turn images for auto-attach (last 3, then clear)
|
|
189
|
+
const turnImages = roleState.turnImages || [];
|
|
190
|
+
roleState.turnImages = [];
|
|
191
|
+
|
|
188
192
|
const currentTask = roleState.currentTask;
|
|
189
193
|
for (const route of routes) {
|
|
190
194
|
if (!route.taskId && currentTask) {
|
|
@@ -203,7 +207,7 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
|
|
|
203
207
|
});
|
|
204
208
|
|
|
205
209
|
const results = await Promise.allSettled(routes.map(route =>
|
|
206
|
-
executeRoute(session, roleName, route)
|
|
210
|
+
executeRoute(session, roleName, route, turnImages)
|
|
207
211
|
));
|
|
208
212
|
for (const r of results) {
|
|
209
213
|
if (r.status === 'rejected') {
|
package/crew/routing.js
CHANGED
|
@@ -14,6 +14,24 @@ function roleLabel(r) {
|
|
|
14
14
|
return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Append text to content — works for both string and multimodal array content.
|
|
19
|
+
* For arrays, appends to the last text block (or adds a new one).
|
|
20
|
+
*/
|
|
21
|
+
function _appendTextToContent(content, text) {
|
|
22
|
+
if (typeof content === 'string') return content + text;
|
|
23
|
+
// Multimodal array: find last text block and append
|
|
24
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
25
|
+
if (content[i].type === 'text') {
|
|
26
|
+
content[i].text += text;
|
|
27
|
+
return content;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// No text block found — add one
|
|
31
|
+
content.push({ type: 'text', text });
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* 从累积文本中解析所有 ROUTE 块(支持多 ROUTE + task 字段)
|
|
19
37
|
* @returns {Array<{ to, summary, taskId, taskTitle }>}
|
|
@@ -110,8 +128,9 @@ export function resolveRoleName(to, session, fromRole) {
|
|
|
110
128
|
|
|
111
129
|
/**
|
|
112
130
|
* 执行路由
|
|
131
|
+
* @param {Array<{mimeType, data}>} [turnImages] - auto-attached images from the turn (max 3)
|
|
113
132
|
*/
|
|
114
|
-
export async function executeRoute(session, fromRole, route) {
|
|
133
|
+
export async function executeRoute(session, fromRole, route, turnImages = []) {
|
|
115
134
|
const { to, summary, taskId, taskTitle } = route;
|
|
116
135
|
|
|
117
136
|
// 如果 session 已暂停或停止,保存为 pendingRoutes
|
|
@@ -156,7 +175,12 @@ export async function executeRoute(session, fromRole, route) {
|
|
|
156
175
|
sendCrewOutput(session, fromRole, 'route', null, {
|
|
157
176
|
routeTo: to, routeSummary: summary,
|
|
158
177
|
taskId: taskId || undefined,
|
|
159
|
-
taskTitle: taskTitle || undefined
|
|
178
|
+
taskTitle: taskTitle || undefined,
|
|
179
|
+
// ★ Auto-attach turn images (base64) — server will cache and convert to fileId/previewToken
|
|
180
|
+
routeImages: turnImages.length > 0 ? turnImages.map(img => ({
|
|
181
|
+
mimeType: img.mimeType,
|
|
182
|
+
data: img.data
|
|
183
|
+
})) : undefined
|
|
160
184
|
});
|
|
161
185
|
|
|
162
186
|
// 路由到 human
|
|
@@ -187,7 +211,7 @@ export async function executeRoute(session, fromRole, route) {
|
|
|
187
211
|
const { processHumanQueue } = await import('./human-interaction.js');
|
|
188
212
|
await processHumanQueue(session);
|
|
189
213
|
} else {
|
|
190
|
-
const taskPrompt = buildRoutePrompt(fromRole, summary, session);
|
|
214
|
+
const taskPrompt = buildRoutePrompt(fromRole, summary, session, turnImages);
|
|
191
215
|
await dispatchToRole(session, resolvedTo, taskPrompt, fromRole, taskId, taskTitle);
|
|
192
216
|
}
|
|
193
217
|
} else {
|
|
@@ -198,12 +222,27 @@ export async function executeRoute(session, fromRole, route) {
|
|
|
198
222
|
}
|
|
199
223
|
|
|
200
224
|
/**
|
|
201
|
-
* 构建路由转发的 prompt
|
|
225
|
+
* 构建路由转发的 prompt(支持多模态 — 自动附加 turn 截图)
|
|
226
|
+
* @param {Array<{mimeType, data}>} [turnImages] - auto-attached images
|
|
227
|
+
* @returns {string|Array} text string, or multimodal content array when images present
|
|
202
228
|
*/
|
|
203
|
-
export function buildRoutePrompt(fromRole, summary, session) {
|
|
229
|
+
export function buildRoutePrompt(fromRole, summary, session, turnImages = []) {
|
|
204
230
|
const fromRoleConfig = session.roles.get(fromRole);
|
|
205
231
|
const fromName = fromRoleConfig ? roleLabel(fromRoleConfig) : fromRole;
|
|
206
|
-
|
|
232
|
+
const text = `来自 ${fromName} 的消息:\n${summary}\n\n请开始你的工作。完成后通过 ROUTE 块传递给下一个角色。`;
|
|
233
|
+
|
|
234
|
+
if (turnImages.length === 0) return text;
|
|
235
|
+
|
|
236
|
+
// Build multimodal content: images first, then text
|
|
237
|
+
const blocks = [];
|
|
238
|
+
for (const img of turnImages) {
|
|
239
|
+
blocks.push({
|
|
240
|
+
type: 'image',
|
|
241
|
+
source: { type: 'base64', media_type: img.mimeType, data: img.data }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
blocks.push({ type: 'text', text });
|
|
245
|
+
return blocks;
|
|
207
246
|
}
|
|
208
247
|
|
|
209
248
|
/**
|
|
@@ -229,38 +268,44 @@ export async function dispatchToRole(session, roleName, content, fromSource, tas
|
|
|
229
268
|
|
|
230
269
|
// Task 上下文注入
|
|
231
270
|
const effectiveTaskId = taskId || roleState.currentTask?.taskId;
|
|
232
|
-
if (effectiveTaskId
|
|
271
|
+
if (effectiveTaskId) {
|
|
233
272
|
const taskContent = await readTaskFile(session, effectiveTaskId);
|
|
234
273
|
if (taskContent) {
|
|
235
|
-
|
|
274
|
+
const ctx = `\n\n---\n<task-context file=".crew/context/features/${effectiveTaskId}.md">\n${taskContent}\n</task-context>`;
|
|
275
|
+
content = _appendTextToContent(content, ctx);
|
|
236
276
|
}
|
|
237
277
|
}
|
|
238
278
|
|
|
239
279
|
// 看板上下文注入(角色重启后知道全局状态)
|
|
240
|
-
|
|
280
|
+
{
|
|
241
281
|
const kanbanContent = await readKanban(session);
|
|
242
282
|
if (kanbanContent) {
|
|
243
|
-
|
|
283
|
+
const ctx = `\n\n---\n<kanban file=".crew/context/kanban.md">\n${kanbanContent}\n</kanban>`;
|
|
284
|
+
content = _appendTextToContent(content, ctx);
|
|
244
285
|
}
|
|
245
286
|
}
|
|
246
287
|
|
|
247
288
|
// 最近路由消息注入(帮助 clear 后的角色恢复上下文)
|
|
248
|
-
if (
|
|
289
|
+
if (session.messageHistory.length > 0) {
|
|
249
290
|
const recentRoutes = session.messageHistory
|
|
250
291
|
.filter(m => m.from !== 'system')
|
|
251
292
|
.slice(-5)
|
|
252
293
|
.map(m => `[${m.from} → ${m.to}${m.taskId ? ` (${m.taskId})` : ''}] ${m.content}`)
|
|
253
294
|
.join('\n');
|
|
254
295
|
if (recentRoutes) {
|
|
255
|
-
|
|
296
|
+
const ctx = `\n\n---\n<recent-routes>\n${recentRoutes}\n</recent-routes>`;
|
|
297
|
+
content = _appendTextToContent(content, ctx);
|
|
256
298
|
}
|
|
257
299
|
}
|
|
258
300
|
|
|
259
301
|
// 记录消息历史
|
|
302
|
+
const historyContent = typeof content === 'string'
|
|
303
|
+
? content.substring(0, 200)
|
|
304
|
+
: (Array.isArray(content) ? content.filter(b => b.type === 'text').map(b => b.text).join('').substring(0, 200) + (content.some(b => b.type === 'image') ? ' [+images]' : '') : '...');
|
|
260
305
|
session.messageHistory.push({
|
|
261
306
|
from: fromSource,
|
|
262
307
|
to: roleName,
|
|
263
|
-
content:
|
|
308
|
+
content: historyContent,
|
|
264
309
|
taskId: taskId || roleState.currentTask?.taskId || null,
|
|
265
310
|
timestamp: Date.now()
|
|
266
311
|
});
|
package/crew/ui-messages.js
CHANGED
|
@@ -200,6 +200,19 @@ export function sendCrewOutput(session, roleName, outputType, rawMessage, extra
|
|
|
200
200
|
taskId, taskTitle, isDecisionMaker,
|
|
201
201
|
timestamp: Date.now()
|
|
202
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
|
+
}
|
|
203
216
|
}
|
|
204
217
|
}
|
|
205
218
|
}
|