@olares/lares-core 0.15.17 → 0.15.18

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.
Files changed (60) hide show
  1. package/brand/identity.js +25 -2
  2. package/i18n/chat-model-switch.js +8 -2
  3. package/i18n/voice.js +78 -2
  4. package/i18n/webhook.js +39 -0
  5. package/larepass/mux.js +1 -0
  6. package/larepass/rpc.js +4 -3
  7. package/larepass/runtime.js +4 -1
  8. package/larepass/settings.js +11 -8
  9. package/package.json +29 -2
  10. package/router/catalog-cache.js +1 -1
  11. package/router/catalog-coordinator.js +60 -0
  12. package/router/catalog-sync.js +17 -62
  13. package/router/catalog.d.ts +3 -1
  14. package/router/catalog.js +6 -13
  15. package/router/default-selection.d.ts +3 -0
  16. package/router/default-selection.js +22 -0
  17. package/router/model-authorization.js +53 -0
  18. package/router/model-state.js +35 -0
  19. package/router/models.js +2 -6
  20. package/router/session-model.js +5 -18
  21. package/router/stt.js +2 -1
  22. package/router/tts.js +13 -20
  23. package/voice/activity.js +10 -0
  24. package/voice/agent-session.js +4 -0
  25. package/voice/browser-capture.js +72 -6
  26. package/voice/browser-playback.js +16 -20
  27. package/voice/client.js +28 -14
  28. package/voice/composite-gateway.js +7 -3
  29. package/voice/config.js +6 -0
  30. package/voice/controller.js +19 -8
  31. package/voice/conversation.js +48 -15
  32. package/voice/device-input.js +14 -9
  33. package/voice/dialogue.js +4 -3
  34. package/voice/host-service.js +7 -2
  35. package/voice/interruption.js +44 -0
  36. package/voice/lares-policy.js +12 -0
  37. package/voice/options.js +2 -1
  38. package/voice/panel.js +61 -20
  39. package/voice/pcm-player.js +51 -0
  40. package/voice/pcm-worklet.js +71 -0
  41. package/voice/preferences.js +89 -0
  42. package/voice/presentation.js +13 -1
  43. package/voice/reply-stream.js +97 -0
  44. package/voice/router-audio.js +23 -6
  45. package/voice/script.js +27 -36
  46. package/voice/service.js +46 -33
  47. package/voice/shortcuts.js +54 -5
  48. package/voice/speech-segmenter.js +119 -0
  49. package/voice/speech.js +60 -13
  50. package/voice/spoken-text.js +196 -0
  51. package/voice/startup.js +9 -0
  52. package/voice/stream-client.js +165 -0
  53. package/voice/stream-gateway.js +146 -0
  54. package/voice/tts-preview.js +27 -0
  55. package/voice/tts-settings.js +79 -0
  56. package/voice/tts-voice.js +20 -0
  57. package/webhook/client.js +34 -0
  58. package/webhook/config.js +55 -0
  59. package/webhook/public.js +11 -0
  60. package/webhook/reply.js +12 -0
package/brand/identity.js CHANGED
@@ -10,6 +10,11 @@ export const UPSTREAM_TITLE = "DeepSeek Harness";
10
10
 
11
11
  export const THEME_COLOR = "#6366F1";
12
12
 
13
+ /** Letter-by-letter, so a model cannot drop the "a" and emit "olores". */
14
+ function platformSpelling() {
15
+ return `Spell the platform name exactly ${PLATFORM_NAME} (${[...PLATFORM_NAME].join(", ")}).`;
16
+ }
17
+
13
18
  export function replaceProductTitle(title) {
14
19
  return String(title ?? "").split(UPSTREAM_TITLE).join(PRODUCT_NAME);
15
20
  }
@@ -18,10 +23,11 @@ export function replaceProductTitle(title) {
18
23
  export function identityPrompt() {
19
24
  return [
20
25
  `You are ${PRODUCT_NAME}, a helpful assistant running on ${PLATFORM_NAME}.`,
26
+ platformSpelling(),
21
27
  `Prefer olares-cli for ${PLATFORM_NAME} platform tasks when skills apply.`,
22
28
  `olares-cli is on PATH and already signed in as the user: ${PLATFORM_NAME} mounts this application's credential into the container. Never run profile login or profile import — a platform-issued profile refuses both.`,
23
29
  "For image, video, audio, or 3D generation, creation, or image editing — including FlowStudio scenes and workflows (T2V, I2V, R2V) — follow the lares-media-create skill: list that family on Router, then call media_generate, which waits for the result and publishes it. Do not curl FlowStudio. Do not diagnose Router or sync catalogs unless that list is empty.",
24
- "Use read/write/edit for files; use background jobs for long shell work.",
30
+ "To answer a question or look something up, only read: use read, search, and read-only commands such as olares-cli. Do not use write or edit to look something up, take notes, or try a change the user did not ask for. Use write or edit only when the user asked you to change or produce that file, and prefer them over shell for that file work. Use background jobs for long shell work.",
25
31
  "User-visible files belong in the session workspace (cwd) as relative paths. Do not write them under /app: that is the application overlay, and the chat preview cannot open it.",
26
32
  "For the current date or time, run `date`; the process TZ is the user's configured Olares time zone.",
27
33
  "Images the user attaches are already in context; read_image is only for image files that exist on disk, never for an attachment.",
@@ -40,6 +46,7 @@ export function surfacePrompt(webUrl) {
40
46
  `You are interacting with the user through ${PRODUCT_NAME} at ${webUrl}.`,
41
47
  `When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean ${PRODUCT_NAME}.`,
42
48
  "The browser provides no implicit DOM, route, or screenshot context.",
49
+ platformSpelling(),
43
50
  `If asked who you are, answer as ${PRODUCT_NAME} on ${PLATFORM_NAME}.`,
44
51
  ].join(" ");
45
52
  }
@@ -54,7 +61,9 @@ You are helping inside an ${PLATFORM_NAME} workspace via ${PRODUCT_NAME}.
54
61
  - For image / video / audio / 3D generation, including FlowStudio scenes and workflows, follow the lares-media-create skill: list that family, POST the matching Router shim route, land the file. Do not curl FlowStudio.
55
62
  - Stay inside the workspace for file edits unless the user explicitly asks otherwise.
56
63
  - Write files the user should open as workspace-relative paths (session cwd). \`/app\` is the running application and is not previewable.
57
- - Prefer structured fs tools (read / write / edit) over shell for file work.
64
+ - To look something up, only read. Do not use write or edit to look something up, take notes, or try a change the user did not ask for.
65
+ - Use write or edit only when the user asked you to change or produce that file. Prefer those tools over shell for that file work.
66
+ - ${platformSpelling()}
58
67
  - Long-running shell work can use background jobs; check results with job_output.
59
68
  - \`@path\` in a user message is workspace-relative, not absolute; \`/id\` names a skill.
60
69
  - If asked who you are, you are ${PRODUCT_NAME} on ${PLATFORM_NAME}.
@@ -67,6 +76,20 @@ export const LEGACY_AGENTS_SEEDS = [
67
76
 
68
77
  You are helping inside an Olares workspace via Lares.
69
78
 
79
+ - Prefer olares-cli skills for platform tasks (market, cluster, files, router, …).
80
+ - olares-cli is already signed in as the user from the credential Olares mounts into this container. Never run \`profile login\` or \`profile import\`; a platform-issued profile refuses both.
81
+ - For image / video / audio / 3D generation, including FlowStudio scenes and workflows, follow the lares-media-create skill: list that family, POST the matching Router shim route, land the file. Do not curl FlowStudio.
82
+ - Stay inside the workspace for file edits unless the user explicitly asks otherwise.
83
+ - Write files the user should open as workspace-relative paths (session cwd). \`/app\` is the running application and is not previewable.
84
+ - Prefer structured fs tools (read / write / edit) over shell for file work.
85
+ - Long-running shell work can use background jobs; check results with job_output.
86
+ - \`@path\` in a user message is workspace-relative, not absolute; \`/id\` names a skill.
87
+ - If asked who you are, you are Lares on Olares.
88
+ `,
89
+ `# AGENTS.md
90
+
91
+ You are helping inside an Olares workspace via Lares.
92
+
70
93
  - Prefer olares-cli skills for platform tasks (market, cluster, files, router, …).
71
94
  - For image / video / audio / 3D generation, including FlowStudio scenes and workflows, follow the lares-media-create skill: list that family, POST the matching Router shim route, land the file. Do not curl FlowStudio.
72
95
  - Stay inside the workspace for file edits unless the user explicitly asks otherwise.
@@ -1,7 +1,8 @@
1
1
  export const ZH = {
2
- "reasoning.default": "默认",
2
+ "reasoning.default": "使用模型默认",
3
3
  "reasoning.title": "推理强度",
4
4
  "reasoning.switchAria": "切换推理强度,当前 {label}",
5
+ "effort.none": "关闭思考",
5
6
  "effort.off": "关闭",
6
7
  "effort.minimal": "最低",
7
8
  "effort.low": "低",
@@ -12,6 +13,8 @@ export const ZH = {
12
13
  "model.select": "选择模型",
13
14
  "model.switchAria": "切换模型,当前 {label}",
14
15
  "model.menuAria": "模型",
16
+ "menu.model": "模型",
17
+ "menu.effort": "推理等级",
15
18
  "model.refreshing": "正在刷新模型列表…",
16
19
  "model.opFailed": "模型操作失败:{msg}",
17
20
  "model.loadFailed": "{name} 加载失败:{msg}",
@@ -20,9 +23,10 @@ export const ZH = {
20
23
  };
21
24
 
22
25
  export const EN = {
23
- "reasoning.default": "Default",
26
+ "reasoning.default": "Model default",
24
27
  "reasoning.title": "Reasoning effort",
25
28
  "reasoning.switchAria": "Switch reasoning effort, current {label}",
29
+ "effort.none": "No thinking",
26
30
  "effort.off": "Off",
27
31
  "effort.minimal": "Minimal",
28
32
  "effort.low": "Low",
@@ -33,6 +37,8 @@ export const EN = {
33
37
  "model.select": "Select model",
34
38
  "model.switchAria": "Switch model, current {label}",
35
39
  "model.menuAria": "Model",
40
+ "menu.model": "Model",
41
+ "menu.effort": "Effort",
36
42
  "model.refreshing": "Refreshing models…",
37
43
  "model.opFailed": "Model action failed: {msg}",
38
44
  "model.loadFailed": "{name} failed to load: {msg}",
package/i18n/voice.js CHANGED
@@ -1,4 +1,16 @@
1
1
  export const ZH = {
2
+ "readout.error": "朗读失败,请检查语音设置", "readout.dismiss": "关闭提示", "readout.preparing": "准备朗读…", "readout.playing": "正在朗读", "readout.confirming": "确认插话…", "readout.stop": "停止朗读",
3
+ "tts.voice": "音色",
4
+ "tts.default": "模型默认",
5
+ "tts.loading": "正在加载音色…",
6
+ "tts.loadFailed": "音色加载失败,请重试",
7
+ "tts.noPresets": "此模型不提供预设音色,使用模型默认声音。",
8
+ "tts.preview": "试听",
9
+ "tts.stopPreview": "停止试听",
10
+ "tts.previewHint": "播放固定短句,不会开启自动朗读。",
11
+ "tts.previewFailed": "试听失败,请重试",
12
+ "tts.busy": "请先结束录音或暂停语音对话。",
13
+
2
14
  "pc.conversation": "开始语音对话",
3
15
  "pc.pause": "暂停语音对话",
4
16
  "pc.resume": "继续语音对话",
@@ -145,7 +157,8 @@ export const ZH = {
145
157
  "error.voice_gpu_stop_failed": "{app}停不下来,显卡模式没有改变({detail})。请在 Olares 市场查看它的状态,再点重试",
146
158
  "error.voice_gpu_memory": "{app}显存不够,没能启动。请在「显存分片」里调大它的显存,或关掉其他应用后重试",
147
159
  "error.voice_gpu_resume_failed": "{app}没能启动({detail})。请在 Olares 市场查看它的状态,再点重试",
148
- "mic.preparing": "正在准备麦克风…",
160
+ "mic.checking": "正在检查语音能力…",
161
+ "mic.preparing": "正在开启麦克风…",
149
162
  "mic.stop": "点击结束录音",
150
163
  "mic.transcribing": "转写中…",
151
164
  "mic.idle": "语音输入",
@@ -155,7 +168,31 @@ export const ZH = {
155
168
  "settings.refresh": "刷新",
156
169
  "settings.refreshing": "刷新中…",
157
170
  "settings.router": "Router 控制台",
171
+ "preferences.dictationStyle": "听写方式",
172
+ "preferences.save": "保存",
173
+ "preferences.cancel": "取消",
174
+ "preferences.saving": "正在保存…",
175
+ "preferences.shortcuts": "快捷键",
176
+ "preferences.shortcutScope": "仅当前网页获得焦点时生效。可为默认听写或特定预设设置快捷键。",
177
+ "preferences.shortcutRecording": "请按组合键",
178
+ "preferences.shortcutHelp": "松开组合键保存;Esc 取消,Tab 离开。",
179
+ "preferences.unset": "未设置",
180
+ "preferences.clear": "清除",
181
+ "preferences.shortcutError.duplicate": "此组合键已用于其他听写操作,请换一个。",
182
+ "preferences.shortcutError.reserved": "这是浏览器或系统常用快捷键,请换一个。",
183
+ "preferences.shortcutError.invalid": "请使用 Ctrl、⌘ 或 Alt 加字母、数字或功能键的组合。",
184
+ "preferences.shortcutError.save": "保存失败,原快捷键已保留。",
185
+ "preferences.unsaved": "有未保存的预设修改",
186
+ "preferences.unsavedHint": "保存修改后离开,或放弃本次修改。",
187
+ "preferences.continue": "继续编辑",
188
+ "preferences.discard": "放弃修改",
189
+ "preferences.modelsMissing": "暂无可用模型,请前往 Router 控制台配置。",
190
+ "settings.model.none": "未选择模型",
191
+ "settings.model.auto": "自动选择",
192
+ "preferences.unavailable": "当前不可用",
158
193
  "settings.loading": "正在读取语音设置…",
194
+ "settings.diagnosticsLoading": "正在检查模型运行状态…",
195
+ "settings.diagnosticsFailed": "模型状态检查失败,请刷新重试",
159
196
  "settings.loadFailed": "读取语音设置失败",
160
197
  "settings.refreshFailed": "刷新语音模型列表失败",
161
198
  "settings.stt.title": "语音识别",
@@ -230,6 +267,7 @@ export const ZH = {
230
267
  "settings.resumeFailed": "启用失败",
231
268
  "settings.language.title": "识别语言",
232
269
  "settings.language.hint": "指定语言可提升准确度与速度",
270
+ "settings.readAloudSyncFailed": "朗读设置同步失败,当前语音已静音,请重试",
233
271
  "settings.saveFailed": "保存失败",
234
272
  "settings.bluetooth.title": "蓝牙设备",
235
273
  "settings.bluetooth.hint": "连上后可以用这台设备说话,回答也会从它播出来",
@@ -242,6 +280,18 @@ export const ZH = {
242
280
  };
243
281
 
244
282
  export const EN = {
283
+ "readout.error": "Reading failed. Check voice settings.", "readout.dismiss": "Dismiss", "readout.preparing": "Preparing speech…", "readout.playing": "Reading aloud", "readout.confirming": "Checking interruption…", "readout.stop": "Stop reading",
284
+ "tts.voice": "Voice",
285
+ "tts.default": "Model default",
286
+ "tts.loading": "Loading voices…",
287
+ "tts.loadFailed": "Could not load voices. Try again.",
288
+ "tts.noPresets": "This model has no preset voices. Its default voice will be used.",
289
+ "tts.preview": "Preview voice",
290
+ "tts.stopPreview": "Stop preview",
291
+ "tts.previewHint": "Play a sample without enabling automatic read-aloud.",
292
+ "tts.previewFailed": "Preview failed. Try again.",
293
+ "tts.busy": "Finish recording or pause the voice conversation first.",
294
+
245
295
  "pc.conversation": "Start voice conversation",
246
296
  "pc.pause": "Pause voice conversation",
247
297
  "pc.resume": "Resume voice conversation",
@@ -397,7 +447,8 @@ export const EN = {
397
447
  "error.voice_gpu_memory":
398
448
  "{app} ran out of GPU memory and did not start. Give it more memory under Memory slices, or turn other apps off, then retry",
399
449
  "error.voice_gpu_resume_failed": "{app} did not start ({detail}). Check it in the Olares Market, then retry",
400
- "mic.preparing": "Preparing microphone…",
450
+ "mic.checking": "Checking voice capabilities…",
451
+ "mic.preparing": "Opening microphone…",
401
452
  "mic.stop": "Click to stop recording",
402
453
  "mic.transcribing": "Transcribing…",
403
454
  "mic.idle": "Voice input",
@@ -407,7 +458,31 @@ export const EN = {
407
458
  "settings.refresh": "Refresh",
408
459
  "settings.refreshing": "Refreshing…",
409
460
  "settings.router": "Router console",
461
+ "preferences.dictationStyle": "Dictation style",
462
+ "preferences.save": "Save",
463
+ "preferences.cancel": "Cancel",
464
+ "preferences.saving": "Saving…",
465
+ "preferences.shortcuts": "Keyboard shortcuts",
466
+ "preferences.shortcutScope": "Shortcuts work only while this page has focus. Choose the default style or a specific preset.",
467
+ "preferences.shortcutRecording": "Press a shortcut",
468
+ "preferences.shortcutHelp": "Release to save. Esc cancels; Tab moves focus.",
469
+ "preferences.unset": "Not set",
470
+ "preferences.clear": "Clear",
471
+ "preferences.shortcutError.duplicate": "This shortcut is assigned to another dictation action.",
472
+ "preferences.shortcutError.reserved": "This shortcut is reserved by the browser or system.",
473
+ "preferences.shortcutError.invalid": "Use Ctrl, Command or Alt with a letter, digit or function key.",
474
+ "preferences.shortcutError.save": "Save failed. Your previous shortcut is unchanged.",
475
+ "preferences.unsaved": "Unsaved preset changes",
476
+ "preferences.unsavedHint": "Save your changes before leaving, or discard them.",
477
+ "preferences.continue": "Keep editing",
478
+ "preferences.discard": "Discard changes",
479
+ "preferences.modelsMissing": "No models are available. Configure them in the Router console.",
480
+ "settings.model.none": "No model selected",
481
+ "settings.model.auto": "Automatic",
482
+ "preferences.unavailable": "Currently unavailable",
410
483
  "settings.loading": "Loading voice settings…",
484
+ "settings.diagnosticsLoading": "Checking model runtime status…",
485
+ "settings.diagnosticsFailed": "Model diagnostics failed; refresh to retry",
411
486
  "settings.loadFailed": "Failed to load voice settings",
412
487
  "settings.refreshFailed": "Failed to refresh voice models",
413
488
  "settings.stt.title": "Speech recognition",
@@ -482,6 +557,7 @@ export const EN = {
482
557
  "settings.resumeFailed": "Start failed",
483
558
  "settings.language.title": "Recognition language",
484
559
  "settings.language.hint": "Specifying a language improves accuracy and speed",
560
+ "settings.readAloudSyncFailed": "Read-aloud settings could not sync. Voice output is muted; retry to apply them.",
485
561
  "settings.saveFailed": "Failed to save",
486
562
  "settings.bluetooth.title": "Bluetooth devices",
487
563
  "settings.bluetooth.hint": "After connecting, speak into this device and hear answers from it",
@@ -0,0 +1,39 @@
1
+ export const ZH = {
2
+ "settings.nav": "Webhook",
3
+ "settings.title": "Webhook",
4
+ "settings.intro": "给其他应用一个入口:提交一段提示词,Lares 开一个新会话来回答。演示用途,目前不校验调用方身份,只在需要演示时打开。",
5
+ "settings.enabled": "启用",
6
+ "settings.enabledHint": "关闭时提交会被拒绝。已经开始的会话仍可按 id 查询。",
7
+ "settings.workspace": "工作区",
8
+ "settings.workspaceHint": "新会话的目录。留空则使用本机默认工作区。必须是绝对路径。",
9
+ "settings.workspacePlaceholder": "留空使用默认工作区",
10
+ "settings.save": "保存",
11
+ "settings.saving": "正在保存…",
12
+ "settings.saved": "已保存",
13
+ "settings.loadFailed": "载入失败:{msg}",
14
+ "settings.saveFailed": "保存失败:{msg}",
15
+ "settings.contract": "调用方式",
16
+ "settings.contractHint": "POST 立刻返回会话 id。GET 在 status 为 idle 时带上回复文本;running 表示还在回答。地址在 webhook 入口上,和当前页面不是同一个域名。",
17
+ "settings.copy": "复制",
18
+ "settings.copied": "已复制",
19
+ };
20
+
21
+ export const EN = {
22
+ "settings.nav": "Webhook",
23
+ "settings.title": "Webhook",
24
+ "settings.intro": "An entrance for other apps: submit a prompt and Lares opens a new session to answer it. This is for demos and does not check the caller. Turn it on only while you are demonstrating.",
25
+ "settings.enabled": "Enabled",
26
+ "settings.enabledHint": "Submissions are refused while this is off. A session that already started can still be read by id.",
27
+ "settings.workspace": "Workspace",
28
+ "settings.workspaceHint": "Directory for new sessions. Leave empty to use the default workspace. Must be an absolute path.",
29
+ "settings.workspacePlaceholder": "Empty uses the default workspace",
30
+ "settings.save": "Save",
31
+ "settings.saving": "Saving…",
32
+ "settings.saved": "Saved",
33
+ "settings.loadFailed": "Couldn't load: {msg}",
34
+ "settings.saveFailed": "Couldn't save: {msg}",
35
+ "settings.contract": "How to call it",
36
+ "settings.contractHint": "POST returns a session id immediately. GET includes the reply text once status is idle; running means the answer is still in progress. The address is the webhook entrance, a different host from this page.",
37
+ "settings.copy": "Copy",
38
+ "settings.copied": "Copied",
39
+ };
package/larepass/mux.js CHANGED
@@ -195,6 +195,7 @@ export function createSessionFollow() {
195
195
  }
196
196
  attempt.nextIndex += 1;
197
197
  const event = fromChunk(attempt.turn, attempt.step, live.chunk);
198
+ if (event) Object.assign(event.data, { attemptId: attempt.attemptId, chunkIndex: live.index });
198
199
  return { events: event ? [event] : [] };
199
200
  }
200
201
  if (live.type === "end") {
package/larepass/rpc.js CHANGED
@@ -25,7 +25,7 @@ export function hostEndpoint(method) {
25
25
 
26
26
  /**
27
27
  * Wire body for one call. `session.history` is `session/page`. `session/list`
28
- * names its argument `_request`; every other claimed method uses `request`.
28
+ * names its argument `_request`; settings uses direct named parameters.
29
29
  * Prompt requires a `requestId` the call site does not carry.
30
30
  */
31
31
  export function hostCall(method, payload = {}) {
@@ -45,8 +45,9 @@ export function hostCall(method, payload = {}) {
45
45
  };
46
46
  }
47
47
  if (endpoint === "session/list") return { endpoint, args: { _request: body } };
48
- // modelCatalog takes no parameter; a `request` object fails the strict codec.
49
- if (endpoint === "session/modelCatalog") return { endpoint, args: {} };
48
+ // Parameterless endpoints reject even an empty `request` object.
49
+ if (endpoint === "session/modelCatalog" || endpoint === "settings/describe") return { endpoint, args: {} };
50
+ if (endpoint === "settings/update") return { endpoint, args: body };
50
51
  if (endpoint === "$events/result") return { endpoint, args: body };
51
52
  if (endpoint === "session/prompt") {
52
53
  return {
@@ -731,6 +731,9 @@ export function createChatRuntime(client) {
731
731
  emit();
732
732
  return archived;
733
733
  },
734
+ async refreshModelCatalog() {
735
+ await client.settings.refreshModels();
736
+ },
734
737
  modelCatalog() {
735
738
  return client.rpc("session.modelCatalog");
736
739
  },
@@ -862,7 +865,7 @@ export function createChatRuntime(client) {
862
865
  }
863
866
  return client.upload(id, file, options);
864
867
  },
865
- voiceSocket(ticket) { return client.voiceSocket(ticket); },
868
+ voiceSocket(ticket, path) { return client.voiceSocket(ticket, path); },
866
869
  voiceRequest(path, options) { return client.voiceRequest(path, options); },
867
870
  transcribe(blob, language, signal, model) {
868
871
  if (typeof client.transcribe !== "function") {
@@ -89,32 +89,35 @@ export function createHostSettings(request) {
89
89
  ),
90
90
  );
91
91
  },
92
- async voice(force = false) {
92
+ async voice(force = false, { diagnostics = true } = {}) {
93
93
  return voiceStore.load(async () => {
94
94
  const query = force ? "?refresh=1" : "";
95
95
  const [config, status, models] = await Promise.all([
96
96
  request("/api/lares/voice/config"),
97
- request(`/api/lares/voice/status${query}`),
98
- request(`/api/lares/voice/models${query}`),
97
+ diagnostics ? request(`/api/lares/voice/status${query}`) : Promise.resolve(null),
98
+ request(`/api/lares/voice/${diagnostics ? "models" : "capabilities"}${query}`),
99
99
  ]);
100
- const listed = payload(models, "/api/lares/voice/models");
100
+ const listed = payload(models, "/api/lares/voice/capabilities");
101
101
  return {
102
102
  config: payload(config, "/api/lares/voice/config"),
103
- status: payload(status, "/api/lares/voice/status"),
103
+ status: status ? payload(status, "/api/lares/voice/status") : { modelAvailable: Boolean(listed.selected), model: listed.selected || "" },
104
104
  sttModels: modelIds(listed, "stt"),
105
105
  ttsModels: modelIds(listed, "tts"),
106
+ ttsSelected: listed.ttsSelected || "",
107
+ ttsDefault: listed.ttsDefault || "",
106
108
  chatModels: modelIds(listed, "chat"),
107
109
  realtimeModels: modelIds(listed, "realtime"),
108
110
  realtimeReady: modelIds(listed, "realtimeReady"),
109
111
  };
110
112
  }, { force: Boolean(force) });
111
113
  },
112
- async saveVoice(patch) {
113
- payload(
114
+ async saveVoice(patch, { diagnostics = true } = {}) {
115
+ const config = payload(
114
116
  await request("/api/lares/voice/config", { method: "POST", body: patch }),
115
117
  "/api/lares/voice/config",
116
118
  );
117
- const value = await this.voice(true);
119
+ const previous = voiceStore.peek();
120
+ const value = !diagnostics && previous ? voiceStore.remember({ ...previous, config }) : await this.voice(true, { diagnostics });
118
121
  for (const listener of voiceListeners) listener(value);
119
122
  return value;
120
123
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olares/lares-core",
3
- "version": "0.15.17",
3
+ "version": "0.15.18",
4
4
  "description": "Lares 共享业务核心:Router、workspace、文件/媒体、Olares 身份(无 UI、无平台壳)",
5
5
  "type": "module",
6
6
  "repository": {
@@ -28,10 +28,14 @@
28
28
  "search",
29
29
  "tools",
30
30
  "voice",
31
+ "webhook",
31
32
  "workspace",
32
33
  "skills"
33
34
  ],
34
35
  "exports": {
36
+ "./router/catalog-coordinator": "./router/catalog-coordinator.js",
37
+ "./router/model-state": "./router/model-state.js",
38
+ "./router/model-authorization": "./router/model-authorization.js",
35
39
  "./tools/http": "./tools/http.js",
36
40
  "./tools/json-file": "./tools/json-file.js",
37
41
  "./tools/async": "./tools/async.js",
@@ -50,6 +54,7 @@
50
54
  "./i18n/preview": "./i18n/preview.js",
51
55
  "./i18n/mobile": "./i18n/mobile.js",
52
56
  "./i18n/mcp": "./i18n/mcp.js",
57
+ "./i18n/webhook": "./i18n/webhook.js",
53
58
  "./router/catalog": "./router/catalog.js",
54
59
  "./router/catalog-cache": {
55
60
  "types": "./router/catalog-cache.d.ts",
@@ -106,6 +111,10 @@
106
111
  "./drive/url-download": "./drive/url-download.js",
107
112
  "./drive/execute": "./drive/execute.js",
108
113
  "./drive/tools": "./drive/tools.js",
114
+ "./voice/stream-gateway": "./voice/stream-gateway.js",
115
+ "./voice/stream-client": "./voice/stream-client.js",
116
+ "./voice/pcm-player": "./voice/pcm-player.js",
117
+ "./voice/pcm-worklet": "./voice/pcm-worklet.js",
109
118
  "./voice/config": "./voice/config.js",
110
119
  "./voice/languages": "./voice/languages.js",
111
120
  "./voice/service": "./voice/service.js",
@@ -135,6 +144,10 @@
135
144
  "./media/tool": "./media/tool.js",
136
145
  "./mcp/config": "./mcp/config.js",
137
146
  "./mcp/client": "./mcp/client.js",
147
+ "./webhook/config": "./webhook/config.js",
148
+ "./webhook/public": "./webhook/public.js",
149
+ "./webhook/reply": "./webhook/reply.js",
150
+ "./webhook/client": "./webhook/client.js",
138
151
  "./mcp/approval": "./mcp/approval.js",
139
152
  "./olares/identity": "./olares/identity.js",
140
153
  "./olares/session-identity": "./olares/session-identity.js",
@@ -194,10 +207,24 @@
194
207
  "./voice/hold": "./voice/hold.js",
195
208
  "./voice/dictation": "./voice/dictation.js",
196
209
  "./voice/shortcuts": "./voice/shortcuts.js",
210
+ "./voice/tts-settings": "./voice/tts-settings.js",
211
+ "./voice/tts-preview": "./voice/tts-preview.js",
212
+ "./voice/tts-voice": "./voice/tts-voice.js",
213
+ "./voice/activity": "./voice/activity.js",
214
+ "./voice/preferences": "./voice/preferences.js",
215
+ "./voice/lares-policy": "./voice/lares-policy.js",
197
216
  "./voice/panel": "./voice/panel.js",
198
- "./voice/projection": "./voice/projection.js"
217
+ "./voice/startup": "./voice/startup.js",
218
+ "./voice/projection": "./voice/projection.js",
219
+ "./voice/reply-stream": "./voice/reply-stream.js",
220
+ "./router/default-selection": {
221
+ "types": "./router/default-selection.d.ts",
222
+ "default": "./router/default-selection.js"
223
+ }
199
224
  },
200
225
  "dependencies": {
226
+ "emoji-regex": "11.0.0",
227
+ "markdown-it": "14.3.0",
201
228
  "ws": "^8.22.0"
202
229
  }
203
230
  }
@@ -119,7 +119,7 @@ export class CatalogCache {
119
119
  async load() {
120
120
  if (!this.primed) {
121
121
  this.primed = true;
122
- const seeded = readCatalogSeed(this.dataDir);
122
+ const seeded = process.env.LARES_ROUTER_CATALOG_BOOT_FAILED === "1" ? null : readCatalogSeed(this.dataDir);
123
123
  if (seeded) {
124
124
  this.accept(seeded);
125
125
  return this.snapshot();
@@ -0,0 +1,60 @@
1
+ import { ensureDefaultSelection } from "./default-selection.js";
2
+ import { catalogCache } from "./catalog-cache.js";
3
+ import { chatModelsFromRouterCatalog, catalogSettingsOps, LLM_SETTINGS_NS } from "./models.js";
4
+
5
+ export class CatalogCoordinator {
6
+ constructor(cache = catalogCache) {
7
+ this.cache = cache;
8
+ this.committed = null;
9
+ this.signature = null;
10
+ this.revision = 0;
11
+ this.listeners = new Set();
12
+ this.inflight = null;
13
+ }
14
+
15
+ async sync(ports, refresh = false) {
16
+ if (this.inflight) {
17
+ const pending = this.inflight;
18
+ const result = await pending.promise;
19
+ await ensureDefaultSelection(ports.defaultModel, result.models);
20
+ if (!refresh || pending.refresh) return result;
21
+ return this.sync(ports, true);
22
+ }
23
+ const pending = { refresh, promise: null };
24
+ pending.promise = this.perform(ports, refresh).finally(() => {
25
+ if (this.inflight === pending) this.inflight = null;
26
+ });
27
+ this.inflight = pending;
28
+ return pending.promise;
29
+ }
30
+
31
+ async perform(ports, refresh) {
32
+ const previous = this.cache.snapshot();
33
+ try {
34
+ const next = await this.cache.get({ refresh });
35
+ const models = chatModelsFromRouterCatalog(next.payload);
36
+ const signature = JSON.stringify(next.rows);
37
+ // Readers cannot observe new capabilities until dsh accepts the same catalog.
38
+ if (signature !== this.signature) {
39
+ await ports.mutateSettings(LLM_SETTINGS_NS, catalogSettingsOps(models));
40
+ await ensureDefaultSelection(ports.defaultModel, models);
41
+ this.committed = { ...next, models };
42
+ this.signature = signature;
43
+ this.revision++;
44
+ for (const listener of this.listeners) listener(this.revision);
45
+ } else {
46
+ await ensureDefaultSelection(ports.defaultModel, models);
47
+ }
48
+ return this.committed;
49
+ } catch (error) {
50
+ this.cache.restore(previous);
51
+ throw error;
52
+ }
53
+ }
54
+ }
55
+
56
+ export const catalogCoordinator = new CatalogCoordinator();
57
+
58
+ export function synchronizeHostCatalog(ctx, refresh = false) {
59
+ return catalogCoordinator.sync({ mutateSettings: (ns, ops) => ctx.settings.mutate(ns, ops), defaultModel: ctx.agentDefaultModel }, refresh);
60
+ }
@@ -1,14 +1,9 @@
1
- import { createInFlightCoalescer } from "../tools/async.js";
2
- import { catalogCache } from "./catalog-cache.js";
1
+ import { catalogCoordinator } from "./catalog-coordinator.js";
2
+ import { authorizedChatModel } from "./model-authorization.js";
3
3
  import {
4
- LLM_SETTINGS_NS,
5
4
  ROUTER_PROVIDER_ID,
6
- catalogSettingsOps,
7
- chatModelsFromRouterCatalog,
8
- defaultNeedsRepair,
9
5
  listedCatalog,
10
6
  parseDefaultModelRequest,
11
- pickDefaultModel,
12
7
  unregisteredRouterFailure,
13
8
  } from "./models.js";
14
9
  import { forgetSttModel } from "./stt.js";
@@ -17,61 +12,15 @@ function messageOf(err) {
17
12
  return err instanceof Error ? err.message : String(err);
18
13
  }
19
14
 
20
- let adapterRevision = 0;
21
- /** What the last successful refresh wrote, so periodic reconciles stay quiet. */
22
- let publishedModels = null;
23
- /** @type {Set<(revision: number) => void>} */
24
- const revisionListeners = new Set();
25
-
26
- export function catalogRevision() {
27
- return adapterRevision;
28
- }
29
-
30
- /** @param {(revision: number) => void} listener */
15
+ export function catalogRevision() { return catalogCoordinator.revision; }
31
16
  export function onCatalogRevision(listener) {
32
- revisionListeners.add(listener);
33
- return () => revisionListeners.delete(listener);
17
+ catalogCoordinator.listeners.add(listener);
18
+ return () => catalogCoordinator.listeners.delete(listener);
34
19
  }
35
-
36
- function bumpRevision() {
37
- adapterRevision += 1;
38
- for (const listener of revisionListeners) listener(adapterRevision);
39
- }
40
-
41
- /**
42
- * @param {{
43
- * mutateSettings: (ns: string, ops: unknown[]) => Promise<void>,
44
- * currentSelection: () => { provider: string, model: string },
45
- * saveSelection: (next: { provider: string, model: string }) => Promise<void>,
46
- * }} ports
47
- */
48
- async function performRefresh(ports) {
49
- const previous = catalogCache.snapshot();
50
- try {
51
- const { payload } = await catalogCache.get({ refresh: true });
52
- const models = chatModelsFromRouterCatalog(payload);
53
- await ports.mutateSettings(LLM_SETTINGS_NS, catalogSettingsOps(models));
54
- const current = ports.currentSelection();
55
- if (defaultNeedsRepair(current, models)) {
56
- await ports.saveSelection({ provider: ROUTER_PROVIDER_ID, model: pickDefaultModel(models).id });
57
- }
58
- forgetSttModel();
59
- const serialized = JSON.stringify(models);
60
- if (serialized !== publishedModels) {
61
- publishedModels = serialized;
62
- bumpRevision();
63
- }
64
- return models;
65
- } catch (err) {
66
- catalogCache.restore(previous);
67
- throw err;
68
- }
69
- }
70
-
71
- const coalesceRefresh = createInFlightCoalescer();
72
-
73
- export function refreshCatalog(ports) {
74
- return coalesceRefresh(() => performRefresh(ports));
20
+ export async function refreshCatalog(ports) {
21
+ const snapshot = await catalogCoordinator.sync(ports, true);
22
+ forgetSttModel();
23
+ return snapshot.models;
75
24
  }
76
25
 
77
26
  /**
@@ -86,10 +35,14 @@ export async function listCatalog(ports) {
86
35
  return { models: [], failures: unregisteredRouterFailure() };
87
36
  }
88
37
  try {
89
- return { models: listedCatalog(provider, await ports.listModels(ROUTER_PROVIDER_ID)), failures: [] };
38
+ const authorized = await catalogCoordinator.sync(ports);
39
+ return {
40
+ models: listedCatalog(provider, await ports.listModels(ROUTER_PROVIDER_ID)).filter((model) => authorized.rows.some((row) => row.id === model.id)),
41
+ failures: [],
42
+ };
90
43
  } catch (err) {
91
44
  return {
92
- models: [],
45
+ models: listedCatalog(provider, catalogCoordinator.committed?.models ?? []),
93
46
  failures: [{ provider: ROUTER_PROVIDER_ID, name: provider.name, message: messageOf(err) }],
94
47
  };
95
48
  }
@@ -110,6 +63,8 @@ export async function saveDefault(ports, request) {
110
63
  const asked = parseDefaultModelRequest(request);
111
64
  let resolved;
112
65
  try {
66
+ await catalogCoordinator.sync(ports);
67
+ if (!await authorizedChatModel(asked)) throw new Error("Select an authorized chat model");
113
68
  resolved = await ports.resolveCallConfig(asked);
114
69
  } catch (err) {
115
70
  throw Object.assign(new Error(messageOf(err)), { code: "model_unavailable", status: 400 });
@@ -8,8 +8,10 @@ export interface RouterCatalogRow {
8
8
  supportsStt: boolean;
9
9
  /** Speech playback, from `tts`. */
10
10
  supportsTts: boolean;
11
- /** pi-ai level → Router wire spelling, or null when the model takes no effort. */
11
+ supportsSttStream: boolean;
12
+ /** Router identifier → identical wire value, in declared order; null without effort capability. */
12
13
  reasoningEfforts: Record<string, string> | null;
14
+ reasoningDefault?: string;
13
15
  /** Tokens one request may span, or null when Router states none. */
14
16
  contextWindow: number | null;
15
17
  /** Output tokens a turn may reserve: Router's limit capped at a quarter of the window. */