@flowingspring/dsh-voco 0.3.0 → 0.3.2

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 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
+ "controls": "TFZsua_controls",
22
+ "button": "TFZsua_button",
21
23
  "interruptButton": "TFZsua_interruptButton",
22
- "active": "TFZsua_active",
23
24
  "voice-pulse": "TFZsua_voice-pulse",
24
- "button": "TFZsua_button",
25
- "controls": "TFZsua_controls"
25
+ "active": "TFZsua_active"
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",
144
+ "taskCancel": "Kt2smW_taskCancel",
145
+ "taskSummary": "Kt2smW_taskSummary",
146
+ "meta": "Kt2smW_meta",
147
147
  "taskDot": "Kt2smW_taskDot",
148
- "bubble": "Kt2smW_bubble",
149
- "voiceBadge": "Kt2smW_voiceBadge",
148
+ "taskStatus": "Kt2smW_taskStatus",
149
+ "taskInput": "Kt2smW_taskInput",
150
+ "taskTitle": "Kt2smW_taskTitle",
150
151
  "taskActions": "Kt2smW_taskActions",
152
+ "taskLink": "Kt2smW_taskLink",
153
+ "voiceBadge": "Kt2smW_voiceBadge",
154
+ "bubble": "Kt2smW_bubble",
151
155
  "taskChevron": "Kt2smW_taskChevron",
152
- "taskSummary": "Kt2smW_taskSummary",
153
- "miniWave": "Kt2smW_miniWave",
154
- "meta": "Kt2smW_meta",
155
- "taskDetails": "Kt2smW_taskDetails",
156
156
  "taskUpdate": "Kt2smW_taskUpdate",
157
- "taskCancel": "Kt2smW_taskCancel",
158
- "taskInput": "Kt2smW_taskInput",
159
- "taskStatus": "Kt2smW_taskStatus",
160
- "taskTitle": "Kt2smW_taskTitle"
157
+ "taskCard": "Kt2smW_taskCard",
158
+ "taskDetails": "Kt2smW_taskDetails",
159
+ "miniWave": "Kt2smW_miniWave",
160
+ "utterance": "Kt2smW_utterance"
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
- "wave": "GPPB2G_wave",
291
288
  "voice-wave": "GPPB2G_voice-wave",
292
- "stop": "GPPB2G_stop"
289
+ "wave": "GPPB2G_wave",
290
+ "root": "GPPB2G_root",
291
+ "stop": "GPPB2G_stop",
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) try {
873
- memoryReference = workspaceMemoryReference(await memory.recall({
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: call.command.input,
914
+ query: input,
876
915
  maxBytes: 5e3
877
- }));
878
- } catch (error) {
879
- ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
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, call.command.input, memoryReference);
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(call.command.input, recentConversationText(requireSourceSession(binding).events));
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;
@@ -1301,7 +1408,7 @@ function speechFragments(text, flush) {
1301
1408
  rest
1302
1409
  };
1303
1410
  }
1304
- const DEFAULT_DELEGATION_ACKNOWLEDGEMENT = "我先查看一下。";
1411
+ const DEFAULT_DELEGATION_ACKNOWLEDGEMENT = "我看看。";
1305
1412
  const MAX_DELEGATION_ACKNOWLEDGEMENT_LENGTH = 40;
1306
1413
  const MAX_DELEGATED_TASK_LENGTH = 1e3;
1307
1414
  const MAX_DELEGATION_BACKGROUND_LENGTH = 2e3;
@@ -1379,20 +1486,30 @@ 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",
1385
1502
  text: [
1386
1503
  "结合最近对话判断当前用户原话应该由语音前台聊天、轻量工具还是后台编码 Agent 处理。",
1387
- "普通寒暄、日常对话、解释性知识以及无需外部事实或工具即可回答的问题,选择 chat,并直接给出自然简洁的中文回复。",
1504
+ "只有普通寒暄、日常对话,以及无需外部事实或工具就能可靠回答的稳定知识,才选择 chat,并直接给出自然简洁的中文回复。",
1388
1505
  "时间、日期等会随现实变化的事实不能猜测。查询本机当前时间、日期或星期时必须选择 tool=local_datetime。",
1389
- "其他必须查询才能确认的动态事实,如果没有对应的前台工具,应选择 delegate,不能选择 chat 编造答案。",
1506
+ "凡是需要搜索、查询、核实、最新信息,或涉及陌生、不确定、可能过时的术语和事实,一律选择 delegate;不能选择 chat 编造答案。",
1390
1507
  "需要查看或修改工作区文件、运行命令、测试、安装依赖或执行其他复杂工具操作时,选择 delegate。",
1391
1508
  "选择 delegate 时,必须结合最近对话补全省略指代,输出自包含的 task、只用于消歧的 background,并原样复制 user_request。",
1392
1509
  "task 是后台当前唯一要执行的任务;background 不能包含新的要求,也不要把旧任务写成待办。",
1393
- "同时给出一句简短自然的 acknowledgement,表示接下来要做什么。不能声称任务已经完成、已经找到结果,也不要提后台 Agent、工具或路由。",
1394
- "acknowledgement 只能有一句,最多 40 个字符;不要固定使用“好的”或其他相同开头,要根据任务自然变化,并尽量避免重复最近对话中的句式。",
1395
- "可以使用“我先核实一下”“我来检查一下”“我去确认一下”等自然表达,只有语气合适时才使用“好的”。",
1510
+ "同时给出一句非常短、自然口语化的 acknowledgement,只说接下来要做的动作,不复述用户问题,不解释原因,不使用书面汇报语气。不能声称任务已经完成、已经找到结果,也不要提后台 Agent、工具或路由。",
1511
+ "acknowledgement 尽量简短,通常不超过 16 个汉字,硬上限为 40 个字符;避免使用“核实”“确认”“具体含义”“相关内容”等书面化表达,也不要把用户的术语或问题改写进去。",
1512
+ "根据任务语境使用日常说法,例如“我去查查”“我看看”“我帮你找找”“我去搜一下”。不要固定使用同一句,也不要强行添加“哈”。",
1396
1513
  "任务边界与写操作规则:task 只能重述用户明确表达的目标,不得擅自增加新的交付物。",
1397
1514
  "搜索、查询、核实、阅读和分析类任务默认只读,不创建或修改工作区文件。",
1398
1515
  "“给我一个结果”“给我一个产物”“整理一下”“形成说明”等模糊表达,不代表允许写文件。",
@@ -1420,13 +1537,28 @@ async function routeFrontendInput(ctx, events, input, workspaceMemory = "") {
1420
1537
  }
1421
1538
  });
1422
1539
  let output = "";
1423
- for await (const chunk of llm.stream({
1424
- provider: selection.provider,
1425
- model: selection.model,
1426
- ...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort },
1427
- messages: [message],
1428
- system: "你是语音前台路由器。严格按要求输出一个 JSON 对象。不要把普通对话委派给后台编码 Agent。"
1429
- })) if (chunk.type === "text-delta") output += chunk.text;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flowingspring/dsh-voco",
3
3
  "description": "Persistent voice conversations for DSH with cloud speech recognition, Edge TTS, and background Agent delegation",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",