@flowingspring/dsh-voco 0.3.0 → 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/cordis.patch.yml +2 -1
- package/lib/client.js +17 -17
- package/lib/plugins/voice-assistant.js +148 -16
- package/package.json +1 -1
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
|
@@ -20,9 +20,9 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
var VoiceControl_module_css_default = {
|
|
21
21
|
"interruptButton": "TFZsua_interruptButton",
|
|
22
22
|
"active": "TFZsua_active",
|
|
23
|
-
"
|
|
23
|
+
"controls": "TFZsua_controls",
|
|
24
24
|
"button": "TFZsua_button",
|
|
25
|
-
"
|
|
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
|
-
"taskLink": "Kt2smW_taskLink",
|
|
145
|
-
"taskCard": "Kt2smW_taskCard",
|
|
146
|
-
"utterance": "Kt2smW_utterance",
|
|
147
144
|
"taskDot": "Kt2smW_taskDot",
|
|
148
|
-
"bubble": "Kt2smW_bubble",
|
|
149
|
-
"voiceBadge": "Kt2smW_voiceBadge",
|
|
150
|
-
"taskActions": "Kt2smW_taskActions",
|
|
151
|
-
"taskChevron": "Kt2smW_taskChevron",
|
|
152
|
-
"taskSummary": "Kt2smW_taskSummary",
|
|
153
145
|
"miniWave": "Kt2smW_miniWave",
|
|
146
|
+
"taskInput": "Kt2smW_taskInput",
|
|
147
|
+
"taskUpdate": "Kt2smW_taskUpdate",
|
|
148
|
+
"taskCard": "Kt2smW_taskCard",
|
|
149
|
+
"taskTitle": "Kt2smW_taskTitle",
|
|
150
|
+
"taskSummary": "Kt2smW_taskSummary",
|
|
151
|
+
"bubble": "Kt2smW_bubble",
|
|
154
152
|
"meta": "Kt2smW_meta",
|
|
153
|
+
"taskChevron": "Kt2smW_taskChevron",
|
|
155
154
|
"taskDetails": "Kt2smW_taskDetails",
|
|
156
|
-
"
|
|
155
|
+
"taskLink": "Kt2smW_taskLink",
|
|
156
|
+
"utterance": "Kt2smW_utterance",
|
|
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
|
|
@@ -285,11 +285,11 @@ window.__ModuleLoader__.load({
|
|
|
285
285
|
document.head.appendChild(tag);
|
|
286
286
|
}
|
|
287
287
|
var VoiceOverlay_module_css_default = {
|
|
288
|
-
"root": "GPPB2G_root",
|
|
289
|
-
"status": "GPPB2G_status",
|
|
290
288
|
"wave": "GPPB2G_wave",
|
|
289
|
+
"stop": "GPPB2G_stop",
|
|
290
|
+
"root": "GPPB2G_root",
|
|
291
291
|
"voice-wave": "GPPB2G_voice-wave",
|
|
292
|
-
"
|
|
292
|
+
"status": "GPPB2G_status"
|
|
293
293
|
};
|
|
294
294
|
//#endregion
|
|
295
295
|
//#region ../ui-voice/src/client/VoiceOverlay.tsx
|
|
@@ -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 = [
|
|
@@ -166,6 +167,34 @@ function optionalSessionTitle(ctx) {
|
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
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
|
+
}
|
|
169
198
|
function workspaceMemoryReference(memory) {
|
|
170
199
|
const sections = [];
|
|
171
200
|
if (memory.summary.trim() !== "") sections.push(`稳定摘要:\n${memory.summary.trim()}`);
|
|
@@ -254,8 +283,10 @@ function apply(ctx, config = {}) {
|
|
|
254
283
|
const bindings = /* @__PURE__ */ new Map();
|
|
255
284
|
const taskBindings = /* @__PURE__ */ new Map();
|
|
256
285
|
const handles = /* @__PURE__ */ new Map();
|
|
286
|
+
const audioResponsesSeen = /* @__PURE__ */ new Set();
|
|
257
287
|
const maxPending = config.maxPendingObservations ?? 64;
|
|
258
288
|
const maxRestoredUtterances = config.maxRestoredUtterances ?? 24;
|
|
289
|
+
const memoryRecallTimeoutMs = config.memoryRecallTimeoutMs ?? 250;
|
|
259
290
|
const loadConversationMemory = async (sessionId) => {
|
|
260
291
|
const events = ctx.sessions.get(sessionId)?.events ?? (await ctx.get("sessionPersistence")?.inspect(sessionId))?.events;
|
|
261
292
|
if (events === void 0) return void 0;
|
|
@@ -865,26 +896,69 @@ function apply(ctx, config = {}) {
|
|
|
865
896
|
});
|
|
866
897
|
return;
|
|
867
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
|
+
});
|
|
868
905
|
let route;
|
|
869
906
|
try {
|
|
870
907
|
let memoryReference = "";
|
|
871
908
|
const memory = optionalWorkspaceMemory(ctx);
|
|
872
|
-
if (memory !== void 0)
|
|
873
|
-
|
|
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({
|
|
874
913
|
sessionId: binding.sessionId,
|
|
875
|
-
query:
|
|
914
|
+
query: input,
|
|
876
915
|
maxBytes: 5e3
|
|
877
|
-
}));
|
|
878
|
-
|
|
879
|
-
|
|
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
|
+
}
|
|
880
938
|
}
|
|
881
|
-
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
|
+
});
|
|
882
945
|
} catch (error) {
|
|
946
|
+
debugVoiceLatency("route-error", {
|
|
947
|
+
callId: String(call.id),
|
|
948
|
+
durationMs: Date.now() - routeStartedAt,
|
|
949
|
+
error: String(error)
|
|
950
|
+
});
|
|
883
951
|
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
884
|
-
route = fallbackDelegation(
|
|
952
|
+
route = fallbackDelegation(input, recentConversationText(requireSourceSession(binding).events));
|
|
885
953
|
}
|
|
886
954
|
if (route.action === "delegate") {
|
|
887
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
|
+
});
|
|
888
962
|
speakFragment(binding, taskId, route.acknowledgement);
|
|
889
963
|
await onTaskCommand(binding, voiceSessionId, {
|
|
890
964
|
id: call.id,
|
|
@@ -915,6 +989,12 @@ function apply(ctx, config = {}) {
|
|
|
915
989
|
const taskId = delegationOverride?.taskId ?? VoiceTaskId(randomUUID());
|
|
916
990
|
const requestText = delegationOverride?.requestText ?? call.command.input;
|
|
917
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
|
+
});
|
|
918
998
|
let created;
|
|
919
999
|
try {
|
|
920
1000
|
if (continuous) created = await ensureContinuousTaskAgent(binding);
|
|
@@ -926,7 +1006,19 @@ function apply(ctx, config = {}) {
|
|
|
926
1006
|
...await createTaskAgent(binding, taskSessionId, selection)
|
|
927
1007
|
};
|
|
928
1008
|
}
|
|
1009
|
+
debugVoiceLatency("delegation-init-end", {
|
|
1010
|
+
callId: String(call.id),
|
|
1011
|
+
taskId,
|
|
1012
|
+
taskSessionId: created.taskSessionId,
|
|
1013
|
+
durationMs: Date.now() - delegationStartedAt
|
|
1014
|
+
});
|
|
929
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
|
+
});
|
|
930
1022
|
backendUnavailable(error);
|
|
931
1023
|
return;
|
|
932
1024
|
}
|
|
@@ -1153,6 +1245,11 @@ function apply(ctx, config = {}) {
|
|
|
1153
1245
|
});
|
|
1154
1246
|
return;
|
|
1155
1247
|
case "transcription.completed":
|
|
1248
|
+
debugVoiceLatency("transcription-completed", {
|
|
1249
|
+
voiceSessionId: String(session.id),
|
|
1250
|
+
utteranceId: String(event.utteranceId),
|
|
1251
|
+
textLength: event.text.length
|
|
1252
|
+
});
|
|
1156
1253
|
enqueue(binding, async () => {
|
|
1157
1254
|
endUtterance(binding, event.utteranceId, "user", "completed", event.text);
|
|
1158
1255
|
if (session.interactionMode === "speech-shell" && binding.voiceSessionId === session.id) await onTranscription(binding, event.text);
|
|
@@ -1191,7 +1288,17 @@ function apply(ctx, config = {}) {
|
|
|
1191
1288
|
debugVoiceLatency("audio-started");
|
|
1192
1289
|
return;
|
|
1193
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;
|
|
1194
1299
|
case "output_audio.done":
|
|
1300
|
+
audioResponsesSeen.delete(String(event.responseId));
|
|
1301
|
+
return;
|
|
1195
1302
|
case "task.observation":
|
|
1196
1303
|
case "error":
|
|
1197
1304
|
case "closed": return;
|
|
@@ -1379,6 +1486,16 @@ async function routeFrontendInput(ctx, events, input, workspaceMemory = "") {
|
|
|
1379
1486
|
const recentConversation = recentConversationText(events);
|
|
1380
1487
|
if (llm === void 0) return fallbackDelegation(input, recentConversation);
|
|
1381
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
|
+
});
|
|
1382
1499
|
const message = createUserMessage({
|
|
1383
1500
|
content: [{
|
|
1384
1501
|
type: "text",
|
|
@@ -1420,13 +1537,28 @@ async function routeFrontendInput(ctx, events, input, workspaceMemory = "") {
|
|
|
1420
1537
|
}
|
|
1421
1538
|
});
|
|
1422
1539
|
let output = "";
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
+
}
|
|
1430
1562
|
const normalized = output.trim().replace(/^```(?:json)?\s*/iu, "").replace(/\s*```$/u, "");
|
|
1431
1563
|
const parsed = JSON.parse(normalized);
|
|
1432
1564
|
if (parsed.action === "chat" && typeof parsed.reply === "string" && parsed.reply.trim() !== "") return {
|
package/package.json
CHANGED