@yeaft/webchat-agent 0.1.430 → 0.1.432
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/human-interaction.js +73 -50
- package/package.json +1 -1
- package/unify/engine.js +29 -0
- package/unify/web-bridge.js +16 -0
|
@@ -6,6 +6,33 @@ import { dispatchToRole } from './routing.js';
|
|
|
6
6
|
import { sendStatusUpdate } from './ui-messages.js';
|
|
7
7
|
import { debouncedSaveSessionMeta } from './persistence.js';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Resolve @role mention from message content.
|
|
11
|
+
* Returns { target, message } if a valid role is found, null otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveAtMention(content, session) {
|
|
14
|
+
const atMatch = content.match(/^@(\S+)\s*([\s\S]*)/);
|
|
15
|
+
if (!atMatch) return null;
|
|
16
|
+
|
|
17
|
+
const atTarget = atMatch[1];
|
|
18
|
+
const message = atMatch[2].trim() || content;
|
|
19
|
+
|
|
20
|
+
for (const [name, role] of session.roles) {
|
|
21
|
+
if (name === atTarget.toLowerCase()) {
|
|
22
|
+
return { target: name, message };
|
|
23
|
+
}
|
|
24
|
+
if (role.displayName === atTarget) {
|
|
25
|
+
return { target: name, message };
|
|
26
|
+
}
|
|
27
|
+
// Fuzzy: compound "name-displayName" pattern (e.g. "dev-1-托瓦兹" or partial displayName)
|
|
28
|
+
const compound = `${name}-${role.displayName}`;
|
|
29
|
+
if (compound.toLowerCase() === atTarget.toLowerCase()) {
|
|
30
|
+
return { target: name, message };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
9
36
|
/**
|
|
10
37
|
* 处理人的输入
|
|
11
38
|
*/
|
|
@@ -38,6 +65,13 @@ export async function handleCrewHumanInput(msg) {
|
|
|
38
65
|
debouncedSaveSessionMeta(session);
|
|
39
66
|
}
|
|
40
67
|
|
|
68
|
+
// Resolve @role mention from content (works regardless of targetRole from frontend)
|
|
69
|
+
const atMention = resolveAtMention(content, session);
|
|
70
|
+
// Effective target: explicit targetRole from frontend > @mention from content > null
|
|
71
|
+
const effectiveTargetRole = targetRole || (atMention ? atMention.target : null);
|
|
72
|
+
// If @mention found, use the stripped message (without @prefix); otherwise use original content
|
|
73
|
+
const effectiveContent = atMention ? atMention.message : content;
|
|
74
|
+
|
|
41
75
|
// Build dispatch content (supports image attachments)
|
|
42
76
|
function buildHumanContent(prefix, text) {
|
|
43
77
|
if (files && files.length > 0) {
|
|
@@ -72,55 +106,35 @@ export async function handleCrewHumanInput(msg) {
|
|
|
72
106
|
// Status changed + new human message — persist (debounced, dispatch will follow)
|
|
73
107
|
debouncedSaveSessionMeta(session);
|
|
74
108
|
|
|
75
|
-
const target =
|
|
76
|
-
|
|
109
|
+
const target = effectiveTargetRole || waitingContext?.fromRole || session.decisionMaker;
|
|
110
|
+
if (effectiveTargetRole) {
|
|
111
|
+
console.log(`[Crew] @mention override in waiting_human: routing to ${target} instead of ${waitingContext?.fromRole || session.decisionMaker}`);
|
|
112
|
+
}
|
|
113
|
+
await dispatchToRole(session, target, buildHumanContent('人工回复:', effectiveContent), 'human');
|
|
77
114
|
return;
|
|
78
115
|
}
|
|
79
116
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
const message = atMatch[2].trim() || content;
|
|
117
|
+
// @role 指令 — effectiveTargetRole already resolved above
|
|
118
|
+
if (atMention && effectiveTargetRole) {
|
|
119
|
+
const target = effectiveTargetRole;
|
|
120
|
+
const message = effectiveContent;
|
|
85
121
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (role.displayName === atTarget) {
|
|
93
|
-
target = name;
|
|
94
|
-
break;
|
|
122
|
+
// 检测纯 skill 命令(如 /context, /simplify),直接发送不加前缀
|
|
123
|
+
if (/^\/[a-zA-Z0-9_-]+(?:\s+.*)?$/s.test(message)) {
|
|
124
|
+
let roleState = session.roleStates.get(target);
|
|
125
|
+
if (!roleState || !roleState.query || !roleState.inputStream) {
|
|
126
|
+
const { createRoleQuery } = await import('./role-query.js');
|
|
127
|
+
roleState = await createRoleQuery(session, target);
|
|
95
128
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
// P1-4: 守卫 stream.enqueue
|
|
107
|
-
try {
|
|
108
|
-
if (roleState.inputStream && !roleState.inputStream.isDone) {
|
|
109
|
-
roleState.inputStream.enqueue({
|
|
110
|
-
type: 'user',
|
|
111
|
-
message: { role: 'user', content: message }
|
|
112
|
-
});
|
|
113
|
-
} else {
|
|
114
|
-
console.warn(`[Crew] Skill dispatch: stream closed for ${target}, recreating`);
|
|
115
|
-
const { createRoleQuery } = await import('./role-query.js');
|
|
116
|
-
roleState = await createRoleQuery(session, target);
|
|
117
|
-
roleState.inputStream.enqueue({
|
|
118
|
-
type: 'user',
|
|
119
|
-
message: { role: 'user', content: message }
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
} catch (enqueueErr) {
|
|
123
|
-
console.error(`[Crew] Skill dispatch enqueue failed for ${target}:`, enqueueErr.message);
|
|
129
|
+
// P1-4: 守卫 stream.enqueue
|
|
130
|
+
try {
|
|
131
|
+
if (roleState.inputStream && !roleState.inputStream.isDone) {
|
|
132
|
+
roleState.inputStream.enqueue({
|
|
133
|
+
type: 'user',
|
|
134
|
+
message: { role: 'user', content: message }
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
console.warn(`[Crew] Skill dispatch: stream closed for ${target}, recreating`);
|
|
124
138
|
const { createRoleQuery } = await import('./role-query.js');
|
|
125
139
|
roleState = await createRoleQuery(session, target);
|
|
126
140
|
roleState.inputStream.enqueue({
|
|
@@ -128,18 +142,27 @@ export async function handleCrewHumanInput(msg) {
|
|
|
128
142
|
message: { role: 'user', content: message }
|
|
129
143
|
});
|
|
130
144
|
}
|
|
131
|
-
|
|
132
|
-
console.
|
|
133
|
-
|
|
145
|
+
} catch (enqueueErr) {
|
|
146
|
+
console.error(`[Crew] Skill dispatch enqueue failed for ${target}:`, enqueueErr.message);
|
|
147
|
+
const { createRoleQuery } = await import('./role-query.js');
|
|
148
|
+
roleState = await createRoleQuery(session, target);
|
|
149
|
+
roleState.inputStream.enqueue({
|
|
150
|
+
type: 'user',
|
|
151
|
+
message: { role: 'user', content: message }
|
|
152
|
+
});
|
|
134
153
|
}
|
|
135
|
-
|
|
154
|
+
sendStatusUpdate(session);
|
|
155
|
+
console.log(`[Crew] Skill command dispatched to ${target}: ${message}`);
|
|
136
156
|
return;
|
|
137
157
|
}
|
|
158
|
+
console.log(`[Crew] @mention routing to ${target}: ${message.substring(0, 50)}...`);
|
|
159
|
+
await dispatchToRole(session, target, buildHumanContent('人工消息:', message), 'human');
|
|
160
|
+
return;
|
|
138
161
|
}
|
|
139
162
|
|
|
140
163
|
// 默认发给决策者
|
|
141
|
-
const target =
|
|
142
|
-
await dispatchToRole(session, target, buildHumanContent('人工消息:',
|
|
164
|
+
const target = effectiveTargetRole || session.decisionMaker;
|
|
165
|
+
await dispatchToRole(session, target, buildHumanContent('人工消息:', effectiveContent), 'human');
|
|
143
166
|
}
|
|
144
167
|
|
|
145
168
|
/**
|
package/package.json
CHANGED
package/unify/engine.js
CHANGED
|
@@ -424,6 +424,20 @@ export class Engine {
|
|
|
424
424
|
responseText,
|
|
425
425
|
});
|
|
426
426
|
|
|
427
|
+
// Emit debug_turn for error path too
|
|
428
|
+
yield {
|
|
429
|
+
type: 'debug_turn',
|
|
430
|
+
turnNumber,
|
|
431
|
+
model: currentModel,
|
|
432
|
+
systemPrompt,
|
|
433
|
+
messages: conversationMessages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 50000) : m.content })),
|
|
434
|
+
response: responseText || `Error: ${err.message}`,
|
|
435
|
+
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
436
|
+
usage: { inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens },
|
|
437
|
+
latencyMs,
|
|
438
|
+
stopReason: 'error',
|
|
439
|
+
};
|
|
440
|
+
|
|
427
441
|
// ─── LLMContextError → force compact → retry ──────
|
|
428
442
|
if (err instanceof LLMContextError && this.#conversationStore && this.#memoryStore) {
|
|
429
443
|
const consolidated = await this.#maybeConsolidate();
|
|
@@ -465,6 +479,21 @@ export class Engine {
|
|
|
465
479
|
responseText,
|
|
466
480
|
});
|
|
467
481
|
|
|
482
|
+
// Emit debug_turn event for web UI debug panel
|
|
483
|
+
// (conversationMessages does NOT yet include the assistant response at this point)
|
|
484
|
+
yield {
|
|
485
|
+
type: 'debug_turn',
|
|
486
|
+
turnNumber,
|
|
487
|
+
model: currentModel,
|
|
488
|
+
systemPrompt,
|
|
489
|
+
messages: conversationMessages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 50000) : m.content })),
|
|
490
|
+
response: responseText,
|
|
491
|
+
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
492
|
+
usage: { inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens },
|
|
493
|
+
latencyMs,
|
|
494
|
+
stopReason,
|
|
495
|
+
};
|
|
496
|
+
|
|
468
497
|
// Append assistant message to conversation
|
|
469
498
|
const assistantMsg = { role: 'assistant', content: responseText };
|
|
470
499
|
if (toolCalls.length > 0) {
|
package/unify/web-bridge.js
CHANGED
|
@@ -214,6 +214,22 @@ export async function handleUnifyChat(msg) {
|
|
|
214
214
|
});
|
|
215
215
|
break;
|
|
216
216
|
|
|
217
|
+
// ── Debug turn data for web debug panel ──
|
|
218
|
+
case 'debug_turn':
|
|
219
|
+
sendUnifyEvent({
|
|
220
|
+
type: 'debug_turn',
|
|
221
|
+
turnNumber: event.turnNumber,
|
|
222
|
+
model: event.model,
|
|
223
|
+
systemPrompt: event.systemPrompt,
|
|
224
|
+
messages: event.messages,
|
|
225
|
+
response: event.response,
|
|
226
|
+
toolCalls: event.toolCalls,
|
|
227
|
+
usage: event.usage,
|
|
228
|
+
latencyMs: event.latencyMs,
|
|
229
|
+
stopReason: event.stopReason,
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
|
|
217
233
|
// ── Errors ──
|
|
218
234
|
case 'error':
|
|
219
235
|
sendUnifyOutput({
|