@olares/lares-core 0.15.17 → 0.15.19
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/i18n/chat-model-switch.js +8 -2
- package/i18n/voice.js +78 -2
- package/i18n/webhook.js +39 -0
- package/larepass/mux.js +1 -0
- package/larepass/rpc.js +4 -3
- package/larepass/runtime.js +4 -1
- package/larepass/settings.js +11 -8
- package/package.json +29 -2
- package/router/catalog-cache.js +1 -1
- package/router/catalog-coordinator.js +60 -0
- package/router/catalog-sync.js +17 -62
- package/router/catalog.d.ts +3 -1
- package/router/catalog.js +6 -13
- package/router/default-selection.d.ts +3 -0
- package/router/default-selection.js +22 -0
- package/router/model-authorization.js +53 -0
- package/router/model-state.js +35 -0
- package/router/models.js +2 -6
- package/router/session-model.js +5 -18
- package/router/stt.js +2 -1
- package/router/tts.js +13 -20
- package/voice/activity.js +10 -0
- package/voice/agent-session.js +4 -0
- package/voice/browser-capture.js +72 -6
- package/voice/browser-playback.js +16 -20
- package/voice/client.js +28 -14
- package/voice/composite-gateway.js +7 -3
- package/voice/config.js +6 -0
- package/voice/controller.js +19 -8
- package/voice/conversation.js +48 -15
- package/voice/device-input.js +14 -9
- package/voice/dialogue.js +4 -3
- package/voice/host-service.js +7 -2
- package/voice/interruption.js +44 -0
- package/voice/lares-policy.js +12 -0
- package/voice/options.js +2 -1
- package/voice/panel.js +61 -20
- package/voice/pcm-player.js +51 -0
- package/voice/pcm-worklet.js +71 -0
- package/voice/preferences.js +89 -0
- package/voice/presentation.js +13 -1
- package/voice/reply-stream.js +99 -0
- package/voice/router-audio.js +23 -6
- package/voice/script.js +27 -36
- package/voice/service.js +46 -33
- package/voice/shortcuts.js +54 -5
- package/voice/speech-segmenter.js +119 -0
- package/voice/speech.js +60 -13
- package/voice/spoken-text.js +196 -0
- package/voice/startup.js +9 -0
- package/voice/stream-client.js +165 -0
- package/voice/stream-gateway.js +146 -0
- package/voice/tts-preview.js +27 -0
- package/voice/tts-settings.js +79 -0
- package/voice/tts-voice.js +20 -0
- package/webhook/client.js +34 -0
- package/webhook/config.js +55 -0
- package/webhook/public.js +11 -0
- package/webhook/reply.js +12 -0
|
@@ -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": "
|
|
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.
|
|
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.
|
|
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",
|
package/i18n/webhook.js
ADDED
|
@@ -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`;
|
|
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
|
-
//
|
|
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 {
|
package/larepass/runtime.js
CHANGED
|
@@ -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") {
|
package/larepass/settings.js
CHANGED
|
@@ -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
|
|
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/
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.15.19",
|
|
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/
|
|
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
|
}
|
package/router/catalog-cache.js
CHANGED
|
@@ -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
|
+
}
|
package/router/catalog-sync.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
return () =>
|
|
17
|
+
catalogCoordinator.listeners.add(listener);
|
|
18
|
+
return () => catalogCoordinator.listeners.delete(listener);
|
|
34
19
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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 });
|
package/router/catalog.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ export interface RouterCatalogRow {
|
|
|
8
8
|
supportsStt: boolean;
|
|
9
9
|
/** Speech playback, from `tts`. */
|
|
10
10
|
supportsTts: boolean;
|
|
11
|
-
|
|
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. */
|
package/router/catalog.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
const SAFE_MODEL_ID = /^[^\u0000-\u001f\u007f]{1,512}$/;
|
|
2
|
-
/** pi-ai's thinking vocabulary; a Router level outside it has no wire spelling to declare. */
|
|
3
|
-
const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* The capability flags one catalog row declares, named without Router's
|
|
@@ -23,20 +21,12 @@ function capabilities(item) {
|
|
|
23
21
|
return flags;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
/**
|
|
27
|
-
* The effort levels Router advertises for one model, keyed by pi-ai level with
|
|
28
|
-
* Router's own spelling as the wire value. A dict offering nothing beyond `off`
|
|
29
|
-
* is no capability at all, and pi-ai rejects it.
|
|
30
|
-
*/
|
|
24
|
+
/** Keep Router identifiers and their declared order independent of adapter vocabulary. */
|
|
31
25
|
function reasoningEfforts(item, flags) {
|
|
32
26
|
if (!flags.has("reasoning_effort")) return null;
|
|
33
27
|
const options = Array.isArray(item.reasoning_effort?.options) ? item.reasoning_effort.options : [];
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const level = String(option ?? "").trim().toLowerCase();
|
|
37
|
-
if (THINKING_LEVELS.has(level)) efforts[level] = level;
|
|
38
|
-
}
|
|
39
|
-
return Object.keys(efforts).some((level) => level !== "off") ? efforts : null;
|
|
28
|
+
const entries = options.filter((value) => typeof value === "string" && value.length > 0);
|
|
29
|
+
return entries.length ? Object.fromEntries(entries.map((value) => [value, value])) : null;
|
|
40
30
|
}
|
|
41
31
|
|
|
42
32
|
/**
|
|
@@ -80,7 +70,10 @@ export function routerCatalogRows(payload) {
|
|
|
80
70
|
supportsVision: flags.has("vision"),
|
|
81
71
|
supportsStt: flags.has("stt") || flags.has("stt_stream"),
|
|
82
72
|
supportsTts: flags.has("tts"),
|
|
73
|
+
supportsSttStream: flags.has("stt_stream"),
|
|
83
74
|
reasoningEfforts: reasoningEfforts(item, flags),
|
|
75
|
+
...(item.reasoning_effort?.default && (item.reasoning_effort.options ?? []).includes(item.reasoning_effort.default)
|
|
76
|
+
? { reasoningDefault: item.reasoning_effort.default } : {}),
|
|
84
77
|
contextWindow,
|
|
85
78
|
maxTokens: outputTokenBudget(tokenCount(item.max_output_tokens), contextWindow),
|
|
86
79
|
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export interface ModelSelection { provider: string; model: string; reasoningEffort?: string }
|
|
2
|
+
export function resolveDefaultSelection<T extends ModelSelection>(current: T, models: { id: string }[]): T | ModelSelection;
|
|
3
|
+
export function ensureDefaultSelection(service: { currentSelection(): ModelSelection; saveSelection(next: ModelSelection, options: { expected: ModelSelection }): Promise<void> } | undefined, models: { id: string }[]): Promise<void>;
|