@flowingspring/dsh-voco 0.2.9 → 0.3.1
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/README.md +1 -0
- package/README.zh.md +1 -0
- package/cordis.patch.yml +2 -1
- package/lib/client.js +15 -15
- package/lib/plugins/voice-assistant.js +215 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ The plugin uses `XingChenAGI/XingChenASR-V3.2-Ultra` for cloud speech recognitio
|
|
|
37
37
|
- Speaks a short contextual acknowledgement before starting delegated Agent work.
|
|
38
38
|
- Keeps full task reports in the task UI while speaking a concise, purpose-written result.
|
|
39
39
|
- Supports interruption, browser navigation, reconnects, and restored conversation history.
|
|
40
|
+
- Pure voice sessions get a concise title from the first meaningful voice request, with the transcription as a fallback when the title model is unavailable.
|
|
40
41
|
- Ships the server and browser surfaces as one public npm package.
|
|
41
42
|
|
|
42
43
|
## Configuration
|
package/README.zh.md
CHANGED
package/cordis.patch.yml
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
name: '@flowingspring/dsh-voco/voice-assistant'
|
|
25
25
|
config:
|
|
26
26
|
maxPendingObservations: 64
|
|
27
|
-
restoreConversation: true
|
|
27
|
+
restoreConversation: true
|
|
28
|
+
memoryRecallTimeoutMs: 250
|
|
28
29
|
maxRestoredUtterances: 24
|
|
29
30
|
taskSessionPolicy: continuous
|
|
30
31
|
completedAnnouncement: 任务已完成。
|
package/lib/client.js
CHANGED
|
@@ -18,11 +18,11 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
document.head.appendChild(tag);
|
|
19
19
|
}
|
|
20
20
|
var VoiceControl_module_css_default = {
|
|
21
|
+
"interruptButton": "TFZsua_interruptButton",
|
|
22
|
+
"active": "TFZsua_active",
|
|
21
23
|
"controls": "TFZsua_controls",
|
|
22
|
-
"voice-pulse": "TFZsua_voice-pulse",
|
|
23
24
|
"button": "TFZsua_button",
|
|
24
|
-
"
|
|
25
|
-
"interruptButton": "TFZsua_interruptButton"
|
|
25
|
+
"voice-pulse": "TFZsua_voice-pulse"
|
|
26
26
|
};
|
|
27
27
|
//#endregion
|
|
28
28
|
//#region ../ui-voice/src/client/VoiceControl.tsx
|
|
@@ -141,23 +141,23 @@ window.__ModuleLoader__.load({
|
|
|
141
141
|
document.head.appendChild(tag);
|
|
142
142
|
}
|
|
143
143
|
var VoiceNodeViews_module_css_default = {
|
|
144
|
-
"
|
|
144
|
+
"taskDot": "Kt2smW_taskDot",
|
|
145
145
|
"miniWave": "Kt2smW_miniWave",
|
|
146
|
+
"taskInput": "Kt2smW_taskInput",
|
|
147
|
+
"taskUpdate": "Kt2smW_taskUpdate",
|
|
148
|
+
"taskCard": "Kt2smW_taskCard",
|
|
149
|
+
"taskTitle": "Kt2smW_taskTitle",
|
|
146
150
|
"taskSummary": "Kt2smW_taskSummary",
|
|
147
|
-
"meta": "Kt2smW_meta",
|
|
148
151
|
"bubble": "Kt2smW_bubble",
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"taskLink": "Kt2smW_taskLink",
|
|
152
|
+
"meta": "Kt2smW_meta",
|
|
153
|
+
"taskChevron": "Kt2smW_taskChevron",
|
|
152
154
|
"taskDetails": "Kt2smW_taskDetails",
|
|
153
|
-
"
|
|
155
|
+
"taskLink": "Kt2smW_taskLink",
|
|
154
156
|
"utterance": "Kt2smW_utterance",
|
|
155
|
-
"voiceBadge": "Kt2smW_voiceBadge",
|
|
156
|
-
"taskCard": "Kt2smW_taskCard",
|
|
157
157
|
"taskCancel": "Kt2smW_taskCancel",
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
158
|
+
"taskActions": "Kt2smW_taskActions",
|
|
159
|
+
"voiceBadge": "Kt2smW_voiceBadge",
|
|
160
|
+
"taskStatus": "Kt2smW_taskStatus"
|
|
161
161
|
};
|
|
162
162
|
//#endregion
|
|
163
163
|
//#region ../ui-voice/src/client/VoiceNodeViews.tsx
|
|
@@ -286,9 +286,9 @@ window.__ModuleLoader__.load({
|
|
|
286
286
|
}
|
|
287
287
|
var VoiceOverlay_module_css_default = {
|
|
288
288
|
"wave": "GPPB2G_wave",
|
|
289
|
+
"stop": "GPPB2G_stop",
|
|
289
290
|
"root": "GPPB2G_root",
|
|
290
291
|
"voice-wave": "GPPB2G_voice-wave",
|
|
291
|
-
"stop": "GPPB2G_stop",
|
|
292
292
|
"status": "GPPB2G_status"
|
|
293
293
|
};
|
|
294
294
|
//#endregion
|
|
@@ -129,7 +129,8 @@ const Config = z.object({
|
|
|
129
129
|
completedAnnouncement: z.string().default("任务已完成。"),
|
|
130
130
|
failedAnnouncement: z.string().default("任务失败了,请查看屏幕上的错误信息。"),
|
|
131
131
|
cancelledAnnouncement: z.string().default("任务已取消。"),
|
|
132
|
-
interruptedAnnouncement: z.string().default("上次任务因服务关闭而中断,没有自动重放。你可以告诉我是否继续。")
|
|
132
|
+
interruptedAnnouncement: z.string().default("上次任务因服务关闭而中断,没有自动重放。你可以告诉我是否继续。"),
|
|
133
|
+
memoryRecallTimeoutMs: z.natural().min(1).default(250)
|
|
133
134
|
});
|
|
134
135
|
/** Plugin-owned durable session event types, registered with core at load. */
|
|
135
136
|
const VOICE_SESSION_EVENT_TYPES = [
|
|
@@ -159,6 +160,41 @@ function optionalWorkspaceMemory(ctx) {
|
|
|
159
160
|
return;
|
|
160
161
|
}
|
|
161
162
|
}
|
|
163
|
+
function optionalSessionTitle(ctx) {
|
|
164
|
+
try {
|
|
165
|
+
return ctx.get("sessionTitle");
|
|
166
|
+
} catch {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function recallWithSoftTimeout(operation, timeoutMs) {
|
|
171
|
+
const deadline = Math.max(1, timeoutMs);
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
let settled = false;
|
|
174
|
+
const timer = setTimeout(() => {
|
|
175
|
+
if (settled) return;
|
|
176
|
+
settled = true;
|
|
177
|
+
resolve({ kind: "timeout" });
|
|
178
|
+
}, deadline);
|
|
179
|
+
operation().then((value) => {
|
|
180
|
+
if (settled) return;
|
|
181
|
+
settled = true;
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
resolve({
|
|
184
|
+
kind: "resolved",
|
|
185
|
+
value
|
|
186
|
+
});
|
|
187
|
+
}, (error) => {
|
|
188
|
+
if (settled) return;
|
|
189
|
+
settled = true;
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
resolve({
|
|
192
|
+
kind: "error",
|
|
193
|
+
error
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
162
198
|
function workspaceMemoryReference(memory) {
|
|
163
199
|
const sections = [];
|
|
164
200
|
if (memory.summary.trim() !== "") sections.push(`稳定摘要:\n${memory.summary.trim()}`);
|
|
@@ -193,13 +229,64 @@ const REWRITE_EVENT_INSTRUCTIONS = {
|
|
|
193
229
|
warning: "这是警告信息。简洁说明风险、影响以及用户需要知道的事项。",
|
|
194
230
|
error: "这是失败信息。明确说明没有完成、主要原因以及可行的下一步。"
|
|
195
231
|
};
|
|
232
|
+
const VOICE_TITLE_SYSTEM_PROMPT = `你是语音会话标题生成器。根据用户第一条有实际内容的语音请求,生成一个简短、准确、便于在会话列表中识别的中文标题。
|
|
233
|
+
|
|
234
|
+
只输出标题本身,不要解释,不要引号,不要 Markdown,不要句末标点,不要使用“语音会话”“新会话”等泛化名称,不超过 12 个汉字。不要加入文件大小、行数、路径等执行细节。`;
|
|
235
|
+
function voiceTitleCandidate(value) {
|
|
236
|
+
const normalized = value.replace(/```[\s\S]*?```/gu, "").replace(/^\s*(?:标题|title)\s*[::]\s*/iu, "").replace(/["'「」『』]/gu, "").split(/\r?\n/u, 1)[0].replace(/[。!?!?;;,,::]+$/u, "").replace(/\s+/gu, " ").trim();
|
|
237
|
+
return Array.from(normalized).slice(0, 12).join("");
|
|
238
|
+
}
|
|
239
|
+
function isMeaningfulVoiceTitleInput(value) {
|
|
240
|
+
const normalized = value.replace(/\s+/gu, "").trim();
|
|
241
|
+
if (Array.from(normalized).length < 4) return false;
|
|
242
|
+
return !/^(?:你好|您好|嗨|哈喽|喂|在吗|有人吗|测试|谢谢|好的)[,。!?!?、]*$/u.test(normalized);
|
|
243
|
+
}
|
|
244
|
+
async function generateVoiceSessionTitle(ctx, session, input, titleService) {
|
|
245
|
+
let candidate = "";
|
|
246
|
+
try {
|
|
247
|
+
const llm = ctx.get("llm");
|
|
248
|
+
if (llm !== void 0) {
|
|
249
|
+
const selection = ctx.agentDefaultModel.currentSelection();
|
|
250
|
+
const message = createUserMessage({
|
|
251
|
+
content: [{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: `用户第一条有效语音请求:${input}`
|
|
254
|
+
}],
|
|
255
|
+
source: {
|
|
256
|
+
kind: "plugin",
|
|
257
|
+
plugin: "voice-assistant"
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
let output = "";
|
|
261
|
+
for await (const chunk of llm.stream({
|
|
262
|
+
provider: selection.provider,
|
|
263
|
+
model: selection.model,
|
|
264
|
+
...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort },
|
|
265
|
+
messages: [message],
|
|
266
|
+
system: VOICE_TITLE_SYSTEM_PROMPT
|
|
267
|
+
})) if (chunk.type === "text-delta") output += chunk.text;
|
|
268
|
+
candidate = voiceTitleCandidate(output);
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
272
|
+
}
|
|
273
|
+
if (candidate === "") candidate = voiceTitleCandidate(input);
|
|
274
|
+
if (candidate === "" || titleService.get(session) !== void 0) return;
|
|
275
|
+
try {
|
|
276
|
+
titleService.rename(session, candidate);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
196
281
|
/** Install the driver. @param ctx - composed Agent and voice context. @param config - driver copy and queue bounds. */
|
|
197
282
|
function apply(ctx, config = {}) {
|
|
198
283
|
const bindings = /* @__PURE__ */ new Map();
|
|
199
284
|
const taskBindings = /* @__PURE__ */ new Map();
|
|
200
285
|
const handles = /* @__PURE__ */ new Map();
|
|
286
|
+
const audioResponsesSeen = /* @__PURE__ */ new Set();
|
|
201
287
|
const maxPending = config.maxPendingObservations ?? 64;
|
|
202
288
|
const maxRestoredUtterances = config.maxRestoredUtterances ?? 24;
|
|
289
|
+
const memoryRecallTimeoutMs = config.memoryRecallTimeoutMs ?? 250;
|
|
203
290
|
const loadConversationMemory = async (sessionId) => {
|
|
204
291
|
const events = ctx.sessions.get(sessionId)?.events ?? (await ctx.get("sessionPersistence")?.inspect(sessionId))?.events;
|
|
205
292
|
if (events === void 0) return void 0;
|
|
@@ -223,6 +310,7 @@ function apply(ctx, config = {}) {
|
|
|
223
310
|
interactionMode: void 0,
|
|
224
311
|
voiceAttached: false,
|
|
225
312
|
voiceTurnMarked: false,
|
|
313
|
+
voiceTitleRequested: false,
|
|
226
314
|
active: void 0,
|
|
227
315
|
continuousTaskAgent: void 0,
|
|
228
316
|
lastTerminalTaskId: void 0,
|
|
@@ -716,13 +804,22 @@ function apply(ctx, config = {}) {
|
|
|
716
804
|
};
|
|
717
805
|
const endUtterance = (binding, utteranceId, role, state, finalText, responseId) => {
|
|
718
806
|
const utterance = beginUtterance(binding, utteranceId, role, responseId);
|
|
719
|
-
requireSourceSession(binding)
|
|
807
|
+
const sourceSession = requireSourceSession(binding);
|
|
808
|
+
sourceSession.append("voice/utterance-end", {
|
|
720
809
|
utteranceId,
|
|
721
810
|
role,
|
|
722
811
|
text: finalText ?? utterance.text,
|
|
723
812
|
state,
|
|
724
813
|
...responseId === void 0 ? {} : { responseId }
|
|
725
814
|
});
|
|
815
|
+
const utteranceText = finalText ?? utterance.text;
|
|
816
|
+
if (role === "user" && state === "completed" && isMeaningfulVoiceTitleInput(utteranceText)) {
|
|
817
|
+
const titleService = optionalSessionTitle(ctx);
|
|
818
|
+
if (titleService !== void 0 && !binding.voiceTitleRequested && titleService.get(sourceSession) === void 0) {
|
|
819
|
+
binding.voiceTitleRequested = true;
|
|
820
|
+
generateVoiceSessionTitle(ctx, sourceSession, utteranceText, titleService);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
726
823
|
binding.utterances.delete(utteranceId);
|
|
727
824
|
};
|
|
728
825
|
const interruptAssistantUtterances = (binding, responseId) => {
|
|
@@ -799,26 +896,69 @@ function apply(ctx, config = {}) {
|
|
|
799
896
|
});
|
|
800
897
|
return;
|
|
801
898
|
}
|
|
899
|
+
const routeStartedAt = Date.now();
|
|
900
|
+
const input = call.command.input;
|
|
901
|
+
debugVoiceLatency("route-command-received", {
|
|
902
|
+
callId: String(call.id),
|
|
903
|
+
inputLength: input.length
|
|
904
|
+
});
|
|
802
905
|
let route;
|
|
803
906
|
try {
|
|
804
907
|
let memoryReference = "";
|
|
805
908
|
const memory = optionalWorkspaceMemory(ctx);
|
|
806
|
-
if (memory !== void 0)
|
|
807
|
-
|
|
909
|
+
if (memory !== void 0) {
|
|
910
|
+
const memoryStartedAt = Date.now();
|
|
911
|
+
debugVoiceLatency("memory-recall-start", { callId: String(call.id) });
|
|
912
|
+
const recallResult = await recallWithSoftTimeout(() => memory.recall({
|
|
808
913
|
sessionId: binding.sessionId,
|
|
809
|
-
query:
|
|
914
|
+
query: input,
|
|
810
915
|
maxBytes: 5e3
|
|
811
|
-
}));
|
|
812
|
-
|
|
813
|
-
|
|
916
|
+
}), memoryRecallTimeoutMs);
|
|
917
|
+
if (recallResult.kind === "resolved") {
|
|
918
|
+
memoryReference = workspaceMemoryReference(recallResult.value);
|
|
919
|
+
debugVoiceLatency("memory-recall-end", {
|
|
920
|
+
callId: String(call.id),
|
|
921
|
+
durationMs: Date.now() - memoryStartedAt,
|
|
922
|
+
referenceLength: memoryReference.length
|
|
923
|
+
});
|
|
924
|
+
} else if (recallResult.kind === "timeout") debugVoiceLatency("memory-recall-timeout", {
|
|
925
|
+
callId: String(call.id),
|
|
926
|
+
durationMs: Date.now() - memoryStartedAt,
|
|
927
|
+
timeoutMs: memoryRecallTimeoutMs,
|
|
928
|
+
fallback: "empty-reference"
|
|
929
|
+
});
|
|
930
|
+
else {
|
|
931
|
+
debugVoiceLatency("memory-recall-error", {
|
|
932
|
+
callId: String(call.id),
|
|
933
|
+
durationMs: Date.now() - memoryStartedAt,
|
|
934
|
+
error: String(recallResult.error)
|
|
935
|
+
});
|
|
936
|
+
ctx.logger.warn(recallResult.error instanceof Error ? recallResult.error : new Error(String(recallResult.error)));
|
|
937
|
+
}
|
|
814
938
|
}
|
|
815
|
-
route = await routeFrontendInput(ctx, requireSourceSession(binding).events,
|
|
939
|
+
route = await routeFrontendInput(ctx, requireSourceSession(binding).events, input, memoryReference);
|
|
940
|
+
debugVoiceLatency("route-decision", {
|
|
941
|
+
callId: String(call.id),
|
|
942
|
+
action: route.action,
|
|
943
|
+
durationMs: Date.now() - routeStartedAt
|
|
944
|
+
});
|
|
816
945
|
} catch (error) {
|
|
946
|
+
debugVoiceLatency("route-error", {
|
|
947
|
+
callId: String(call.id),
|
|
948
|
+
durationMs: Date.now() - routeStartedAt,
|
|
949
|
+
error: String(error)
|
|
950
|
+
});
|
|
817
951
|
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
818
|
-
route = fallbackDelegation(
|
|
952
|
+
route = fallbackDelegation(input, recentConversationText(requireSourceSession(binding).events));
|
|
819
953
|
}
|
|
820
954
|
if (route.action === "delegate") {
|
|
821
955
|
const taskId = VoiceTaskId(randomUUID());
|
|
956
|
+
debugVoiceLatency("ack-queued", {
|
|
957
|
+
callId: String(call.id),
|
|
958
|
+
taskId,
|
|
959
|
+
routeDurationMs: Date.now() - routeStartedAt,
|
|
960
|
+
textLength: route.acknowledgement.length
|
|
961
|
+
});
|
|
822
962
|
speakFragment(binding, taskId, route.acknowledgement);
|
|
823
963
|
await onTaskCommand(binding, voiceSessionId, {
|
|
824
964
|
id: call.id,
|
|
@@ -849,6 +989,12 @@ function apply(ctx, config = {}) {
|
|
|
849
989
|
const taskId = delegationOverride?.taskId ?? VoiceTaskId(randomUUID());
|
|
850
990
|
const requestText = delegationOverride?.requestText ?? call.command.input;
|
|
851
991
|
const continuous = (config.taskSessionPolicy ?? "isolated") === "continuous";
|
|
992
|
+
const delegationStartedAt = Date.now();
|
|
993
|
+
debugVoiceLatency("delegation-init-start", {
|
|
994
|
+
callId: String(call.id),
|
|
995
|
+
taskId,
|
|
996
|
+
policy: continuous ? "continuous" : "isolated"
|
|
997
|
+
});
|
|
852
998
|
let created;
|
|
853
999
|
try {
|
|
854
1000
|
if (continuous) created = await ensureContinuousTaskAgent(binding);
|
|
@@ -860,7 +1006,19 @@ function apply(ctx, config = {}) {
|
|
|
860
1006
|
...await createTaskAgent(binding, taskSessionId, selection)
|
|
861
1007
|
};
|
|
862
1008
|
}
|
|
1009
|
+
debugVoiceLatency("delegation-init-end", {
|
|
1010
|
+
callId: String(call.id),
|
|
1011
|
+
taskId,
|
|
1012
|
+
taskSessionId: created.taskSessionId,
|
|
1013
|
+
durationMs: Date.now() - delegationStartedAt
|
|
1014
|
+
});
|
|
863
1015
|
} catch (error) {
|
|
1016
|
+
debugVoiceLatency("delegation-init-error", {
|
|
1017
|
+
callId: String(call.id),
|
|
1018
|
+
taskId,
|
|
1019
|
+
durationMs: Date.now() - delegationStartedAt,
|
|
1020
|
+
error: String(error)
|
|
1021
|
+
});
|
|
864
1022
|
backendUnavailable(error);
|
|
865
1023
|
return;
|
|
866
1024
|
}
|
|
@@ -1087,6 +1245,11 @@ function apply(ctx, config = {}) {
|
|
|
1087
1245
|
});
|
|
1088
1246
|
return;
|
|
1089
1247
|
case "transcription.completed":
|
|
1248
|
+
debugVoiceLatency("transcription-completed", {
|
|
1249
|
+
voiceSessionId: String(session.id),
|
|
1250
|
+
utteranceId: String(event.utteranceId),
|
|
1251
|
+
textLength: event.text.length
|
|
1252
|
+
});
|
|
1090
1253
|
enqueue(binding, async () => {
|
|
1091
1254
|
endUtterance(binding, event.utteranceId, "user", "completed", event.text);
|
|
1092
1255
|
if (session.interactionMode === "speech-shell" && binding.voiceSessionId === session.id) await onTranscription(binding, event.text);
|
|
@@ -1125,7 +1288,17 @@ function apply(ctx, config = {}) {
|
|
|
1125
1288
|
debugVoiceLatency("audio-started");
|
|
1126
1289
|
return;
|
|
1127
1290
|
case "output_audio.delta":
|
|
1291
|
+
if (!audioResponsesSeen.has(String(event.responseId))) {
|
|
1292
|
+
audioResponsesSeen.add(String(event.responseId));
|
|
1293
|
+
debugVoiceLatency("audio-first-delta", {
|
|
1294
|
+
responseId: String(event.responseId),
|
|
1295
|
+
audioBytes: event.audio.byteLength
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
return;
|
|
1128
1299
|
case "output_audio.done":
|
|
1300
|
+
audioResponsesSeen.delete(String(event.responseId));
|
|
1301
|
+
return;
|
|
1129
1302
|
case "task.observation":
|
|
1130
1303
|
case "error":
|
|
1131
1304
|
case "closed": return;
|
|
@@ -1313,6 +1486,16 @@ async function routeFrontendInput(ctx, events, input, workspaceMemory = "") {
|
|
|
1313
1486
|
const recentConversation = recentConversationText(events);
|
|
1314
1487
|
if (llm === void 0) return fallbackDelegation(input, recentConversation);
|
|
1315
1488
|
const selection = ctx.agentDefaultModel.currentSelection();
|
|
1489
|
+
const routeLlmStartedAt = Date.now();
|
|
1490
|
+
let routeLlmFirstTextAt;
|
|
1491
|
+
debugVoiceLatency("route-llm-start", {
|
|
1492
|
+
provider: selection.provider,
|
|
1493
|
+
model: selection.model,
|
|
1494
|
+
reasoningEffort: selection.reasoningEffort,
|
|
1495
|
+
inputLength: input.length,
|
|
1496
|
+
recentConversationLength: recentConversation.length,
|
|
1497
|
+
workspaceMemoryLength: workspaceMemory.length
|
|
1498
|
+
});
|
|
1316
1499
|
const message = createUserMessage({
|
|
1317
1500
|
content: [{
|
|
1318
1501
|
type: "text",
|
|
@@ -1354,13 +1537,28 @@ async function routeFrontendInput(ctx, events, input, workspaceMemory = "") {
|
|
|
1354
1537
|
}
|
|
1355
1538
|
});
|
|
1356
1539
|
let output = "";
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1540
|
+
try {
|
|
1541
|
+
for await (const chunk of llm.stream({
|
|
1542
|
+
provider: selection.provider,
|
|
1543
|
+
model: selection.model,
|
|
1544
|
+
...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort },
|
|
1545
|
+
messages: [message],
|
|
1546
|
+
system: "你是语音前台路由器。严格按要求输出一个 JSON 对象。不要把普通对话委派给后台编码 Agent。"
|
|
1547
|
+
})) {
|
|
1548
|
+
if (chunk.type !== "text-delta") continue;
|
|
1549
|
+
if (routeLlmFirstTextAt === void 0) {
|
|
1550
|
+
routeLlmFirstTextAt = Date.now();
|
|
1551
|
+
debugVoiceLatency("route-llm-first-text", { delayMs: routeLlmFirstTextAt - routeLlmStartedAt });
|
|
1552
|
+
}
|
|
1553
|
+
output += chunk.text;
|
|
1554
|
+
}
|
|
1555
|
+
} finally {
|
|
1556
|
+
debugVoiceLatency("route-llm-end", {
|
|
1557
|
+
durationMs: Date.now() - routeLlmStartedAt,
|
|
1558
|
+
firstTextDelayMs: routeLlmFirstTextAt === void 0 ? void 0 : routeLlmFirstTextAt - routeLlmStartedAt,
|
|
1559
|
+
outputLength: output.length
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1364
1562
|
const normalized = output.trim().replace(/^```(?:json)?\s*/iu, "").replace(/\s*```$/u, "");
|
|
1365
1563
|
const parsed = JSON.parse(normalized);
|
|
1366
1564
|
if (parsed.action === "chat" && typeof parsed.reply === "string" && parsed.reply.trim() !== "") return {
|
package/package.json
CHANGED