@goodandready/dsh-voice 0.9.11 → 0.9.12
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/lib/client.js +173 -86
- package/lib/config-routes.js +38 -11
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -62,7 +62,7 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
'hide': 'Hide',
|
|
63
63
|
'title': 'Voice',
|
|
64
64
|
'recordSlot': 'Voice recording',
|
|
65
|
-
'browserHint': '
|
|
65
|
+
'browserHint': 'Web Speech recognition (uses page/browser language when auto, Chrome sends audio to Google)',
|
|
66
66
|
'openaiHint': 'whisper-1 · key OPENAI_API_KEY',
|
|
67
67
|
'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
|
|
68
68
|
'deepinfraHint': 'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY',
|
|
@@ -89,6 +89,12 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
'hotkeyHint1': 'Hold it to record, release to send to the agent, Esc cancels. ',
|
|
90
90
|
'hotkeyHint2': 'Any key will do: a letter, an F-key or a modifier.',
|
|
91
91
|
'language': 'Language',
|
|
92
|
+
'langAuto': 'Auto-detect',
|
|
93
|
+
'langRu': 'Russian (ru)',
|
|
94
|
+
'langEn': 'English (en)',
|
|
95
|
+
'langZh': 'Chinese (zh)',
|
|
96
|
+
'langUk': 'Ukrainian (uk)',
|
|
97
|
+
'langDe': 'German (de)',
|
|
92
98
|
'dictationHint': 'Speech is cut at pauses and the text is appended to the input.',
|
|
93
99
|
'pauseMs': 'Pause that ends a phrase, ms',
|
|
94
100
|
'pauseHint': 'Lower means more frequent chunks and faster text, but a higher risk of cutting a word',
|
|
@@ -204,6 +210,9 @@ window.__ModuleLoader__.load({
|
|
|
204
210
|
'updating': 'Updating…',
|
|
205
211
|
'updateSuccess': 'Updated successfully! Restart DSH to apply.',
|
|
206
212
|
'updateFailed': 'Update failed. Check logs.',
|
|
213
|
+
'updateCheckFailed': 'Update check failed',
|
|
214
|
+
'checkingUpdates': 'Checking updates…',
|
|
215
|
+
'updateStatusUnknown': 'Version status unknown',
|
|
207
216
|
'sensevoiceTitle': 'SenseVoice Local ASR (1-Click Setup)',
|
|
208
217
|
'sensevoiceDesc': 'Ultra-fast offline speech recognition on CPU (50–100ms latency, Russian/English).',
|
|
209
218
|
'installSensevoice': 'Install SenseVoice-Small (1-Click)',
|
|
@@ -256,7 +265,7 @@ window.__ModuleLoader__.load({
|
|
|
256
265
|
'hide': '隐藏',
|
|
257
266
|
'title': '语音',
|
|
258
267
|
'recordSlot': '语音录制',
|
|
259
|
-
'browserHint': '
|
|
268
|
+
'browserHint': 'Web Speech 语音识别 (设为 auto 时使用页面或浏览器语言,Chrome 会上传音频至 Google)',
|
|
260
269
|
'openaiHint': 'whisper-1 · 密钥 OPENAI_API_KEY',
|
|
261
270
|
'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
|
|
262
271
|
'deepinfraHint': 'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY',
|
|
@@ -283,6 +292,12 @@ window.__ModuleLoader__.load({
|
|
|
283
292
|
'hotkeyHint1': '按住录音,松开发送给智能体,按 Esc 取消。',
|
|
284
293
|
'hotkeyHint2': '支持任意按键:字母键、功能键 (F1-F12) 或修饰键。',
|
|
285
294
|
'language': '识别语言',
|
|
295
|
+
'langAuto': '自动检测',
|
|
296
|
+
'langRu': '俄语 (ru)',
|
|
297
|
+
'langEn': '英语 (en)',
|
|
298
|
+
'langZh': '中文 (zh)',
|
|
299
|
+
'langUk': '乌克兰语 (uk)',
|
|
300
|
+
'langDe': '德语 (de)',
|
|
286
301
|
'dictationHint': '语音按停顿自动切句,文字实时追加至输入框。',
|
|
287
302
|
'pauseMs': '断句静音停顿时长(毫秒)',
|
|
288
303
|
'pauseHint': '数值越小切分越频繁、出字越快,但可能切断未说完的词',
|
|
@@ -398,6 +413,9 @@ window.__ModuleLoader__.load({
|
|
|
398
413
|
'updating': '正在更新…',
|
|
399
414
|
'updateSuccess': '更新成功!请重启 DSH 生效。',
|
|
400
415
|
'updateFailed': '更新失败,请查看日志。',
|
|
416
|
+
'updateCheckFailed': '检查更新失败',
|
|
417
|
+
'checkingUpdates': '正在检查更新…',
|
|
418
|
+
'updateStatusUnknown': '版本状态未知',
|
|
401
419
|
'sensevoiceTitle': 'SenseVoice 本地 ASR (一键部署)',
|
|
402
420
|
'sensevoiceDesc': 'CPU 上超低延迟离线语音识别 (50–100ms,中英俄多语种)。',
|
|
403
421
|
'installSensevoice': '一键安装 SenseVoice-Small',
|
|
@@ -507,13 +525,34 @@ window.__ModuleLoader__.load({
|
|
|
507
525
|
subscribe(l) { this.listeners.add(l); return () => this.listeners.delete(l) },
|
|
508
526
|
}
|
|
509
527
|
|
|
510
|
-
|
|
511
|
-
window
|
|
512
|
-
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
528
|
+
function registerGlobalLifecycle() {
|
|
529
|
+
if (typeof window === 'undefined') return () => {}
|
|
530
|
+
const onTtsStart = () => voice.set({ isTtsSpeaking: true })
|
|
531
|
+
const onTtsStop = () => voice.set({ isTtsSpeaking: false })
|
|
532
|
+
const onSettingsSaved = () => { voice._chainsPromise = null }
|
|
533
|
+
const onPageUnload = () => {
|
|
534
|
+
if (voice.rec && typeof voice.teardownRec === 'function') voice.teardownRec(voice.rec)
|
|
535
|
+
if (voice.browser) {
|
|
536
|
+
try { voice.browser.abort() } catch (_) {}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
window.addEventListener('dsh:tts:start', onTtsStart)
|
|
541
|
+
window.addEventListener('dsh:tts:stop', onTtsStop)
|
|
542
|
+
window.addEventListener('dsh-voice:settings-saved', onSettingsSaved)
|
|
543
|
+
window.addEventListener('pagehide', onPageUnload)
|
|
544
|
+
window.addEventListener('beforeunload', onPageUnload)
|
|
545
|
+
|
|
546
|
+
return () => {
|
|
547
|
+
window.removeEventListener('dsh:tts:start', onTtsStart)
|
|
548
|
+
window.removeEventListener('dsh:tts:stop', onTtsStop)
|
|
549
|
+
window.removeEventListener('dsh-voice:settings-saved', onSettingsSaved)
|
|
550
|
+
window.removeEventListener('pagehide', onPageUnload)
|
|
551
|
+
window.removeEventListener('beforeunload', onPageUnload)
|
|
552
|
+
onPageUnload()
|
|
553
|
+
if (typeof voice.clearGlobalHotkey === 'function') voice.clearGlobalHotkey()
|
|
554
|
+
voice.set({ phase: 'idle', caption: '', interimTranscript: '', isTtsSpeaking: false })
|
|
555
|
+
}
|
|
517
556
|
}
|
|
518
557
|
|
|
519
558
|
function triggerBargeIn() {
|
|
@@ -871,6 +910,21 @@ window.__ModuleLoader__.load({
|
|
|
871
910
|
return speechRecognitionCtor() !== null
|
|
872
911
|
}
|
|
873
912
|
|
|
913
|
+
function resolveBrowserRecognitionLang(lang) {
|
|
914
|
+
if (lang && lang !== 'auto') {
|
|
915
|
+
if (lang.includes('-')) return lang
|
|
916
|
+
const BCP47 = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', uk: 'uk-UA', de: 'de-DE' }
|
|
917
|
+
return BCP47[lang.toLowerCase()] || lang
|
|
918
|
+
}
|
|
919
|
+
if (typeof document !== 'undefined' && document.documentElement && document.documentElement.lang) {
|
|
920
|
+
return document.documentElement.lang
|
|
921
|
+
}
|
|
922
|
+
if (typeof navigator !== 'undefined' && navigator.language) {
|
|
923
|
+
return navigator.language
|
|
924
|
+
}
|
|
925
|
+
return 'en-US'
|
|
926
|
+
}
|
|
927
|
+
|
|
874
928
|
/**
|
|
875
929
|
* @param options {{lang: string, continuous: boolean, onInterim, onFinal, onError}}
|
|
876
930
|
* @returns {{stop: Function, abort: Function}}
|
|
@@ -878,7 +932,7 @@ window.__ModuleLoader__.load({
|
|
|
878
932
|
function startBrowserRecognition(options) {
|
|
879
933
|
const Ctor = speechRecognitionCtor()
|
|
880
934
|
const recognition = new Ctor()
|
|
881
|
-
recognition.lang = options.lang
|
|
935
|
+
recognition.lang = resolveBrowserRecognitionLang(options.lang)
|
|
882
936
|
recognition.continuous = options.continuous !== false
|
|
883
937
|
recognition.interimResults = true
|
|
884
938
|
let stopped = false
|
|
@@ -939,22 +993,19 @@ window.__ModuleLoader__.load({
|
|
|
939
993
|
|
|
940
994
|
// Fetch the mode chain from the host once. Only needed to decide whether
|
|
941
995
|
|
|
942
|
-
voice._core = { tidyPhrase, applyVoiceCommands, VOICE_COMMANDS, undoLastInsert, insertHistory, insertDraftText, appendDraft }
|
|
943
|
-
|
|
944
|
-
if (typeof window !== 'undefined') {
|
|
945
|
-
window.addEventListener('dsh-voice:settings-saved', () => { chainsPromise = null })
|
|
946
|
-
}
|
|
996
|
+
voice._core = { tidyPhrase, applyVoiceCommands, VOICE_COMMANDS, undoLastInsert, insertHistory, insertDraftText, appendDraft, resolveBrowserRecognitionLang }
|
|
997
|
+
voice._chainsPromise = null
|
|
947
998
|
function modeChain(mode) {
|
|
948
|
-
if (!
|
|
949
|
-
|
|
999
|
+
if (!voice._chainsPromise) {
|
|
1000
|
+
voice._chainsPromise = fetch('/dsh-voice/status', { cache: 'no-store' })
|
|
950
1001
|
.then((res) => res.json())
|
|
951
1002
|
.then((data) => (data && data.modes) || {})
|
|
952
1003
|
.catch(() => {
|
|
953
|
-
|
|
1004
|
+
voice._chainsPromise = null
|
|
954
1005
|
return {}
|
|
955
1006
|
})
|
|
956
1007
|
}
|
|
957
|
-
return
|
|
1008
|
+
return voice._chainsPromise.then((modes) => {
|
|
958
1009
|
const row = modes[mode] || {}
|
|
959
1010
|
return {
|
|
960
1011
|
chain: Array.isArray(row.chain) ? row.chain.map((e) => e && e.provider) : [],
|
|
@@ -971,16 +1022,7 @@ window.__ModuleLoader__.load({
|
|
|
971
1022
|
if (rec.analyser) { try { rec.analyser.disconnect() } catch (e) { /* already disconnected */ } }
|
|
972
1023
|
if (rec.audioCtx) { try { rec.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
973
1024
|
}
|
|
974
|
-
|
|
975
|
-
const onPageUnload = () => {
|
|
976
|
-
if (voice.rec) teardown(voice.rec)
|
|
977
|
-
if (voice.browser) {
|
|
978
|
-
try { voice.browser.abort() } catch (e) { /* ignore */ }
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
window.addEventListener('pagehide', onPageUnload)
|
|
982
|
-
window.addEventListener('beforeunload', onPageUnload)
|
|
983
|
-
}
|
|
1025
|
+
voice.teardownRec = teardown
|
|
984
1026
|
function waitStop(recorder) {
|
|
985
1027
|
if (!recorder || recorder.state === 'inactive') return Promise.resolve()
|
|
986
1028
|
return new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
|
|
@@ -1279,6 +1321,7 @@ window.__ModuleLoader__.load({
|
|
|
1279
1321
|
window.__dsh_voice_hotkey_cleanup = null
|
|
1280
1322
|
}
|
|
1281
1323
|
}
|
|
1324
|
+
voice.clearGlobalHotkey = clearGlobalHotkey
|
|
1282
1325
|
function installHotkey(ctx, keyName, mode) {
|
|
1283
1326
|
clearGlobalHotkey()
|
|
1284
1327
|
if (typeof document === 'undefined' || !keyName) return () => {}
|
|
@@ -2104,34 +2147,42 @@ window.__ModuleLoader__.load({
|
|
|
2104
2147
|
if (key) return t(key)
|
|
2105
2148
|
return MODEL_HINT_STATIC[provider] || ''
|
|
2106
2149
|
}
|
|
2107
|
-
const
|
|
2150
|
+
const SUPPORTED_LANGS = [
|
|
2151
|
+
{ code: '', labelKey: 'langAuto', defaultLabel: 'auto' },
|
|
2152
|
+
{ code: 'ru', labelKey: 'langRu', defaultLabel: 'ru' },
|
|
2153
|
+
{ code: 'en', labelKey: 'langEn', defaultLabel: 'en' },
|
|
2154
|
+
{ code: 'zh', labelKey: 'langZh', defaultLabel: 'zh' },
|
|
2155
|
+
{ code: 'uk', labelKey: 'langUk', defaultLabel: 'uk' },
|
|
2156
|
+
{ code: 'de', labelKey: 'langDe', defaultLabel: 'de' },
|
|
2157
|
+
]
|
|
2158
|
+
const LANGS = SUPPORTED_LANGS.map((o) => o.code)
|
|
2108
2159
|
|
|
2109
2160
|
const SET_CSS =
|
|
2110
|
-
'.cb-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}' +
|
|
2111
|
-
'.cb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}' +
|
|
2112
|
-
'.cb-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
2113
|
-
'.cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
|
|
2114
|
-
'.cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}' +
|
|
2115
|
-
'.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}' +
|
|
2116
|
-
'.cb-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
2117
|
-
'.cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}' +
|
|
2118
|
-
'.cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}' +
|
|
2119
|
-
'.cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}' +
|
|
2120
|
-
'.cb-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
2121
|
-
'.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
2122
|
-
'.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
2123
|
-
'.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
2124
|
-
'.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}' +
|
|
2125
|
-
'.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88}' +
|
|
2126
|
-
'.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}' +
|
|
2127
|
-
'.cb-btn-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:13px}' +
|
|
2128
|
-
'.cb-btn-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}' +
|
|
2129
|
-
'.cb-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
2130
|
-
'.cb-field select,.cb-field input[type="text"],.cb-field input[type="number"],.cb-field textarea{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
2131
|
-
'.cb-field textarea{height:auto;padding:8px 10px;font-family:inherit}' +
|
|
2132
|
-
'.cb-check-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}' +
|
|
2133
|
-
'.cb-check-label input[type="checkbox"]{width:16px;height:16px;cursor:pointer}' +
|
|
2134
|
-
'.cb-alert-err{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px}' +
|
|
2161
|
+
'.dvo-settings-root.cb-page,.dvo-settings-root .cb-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}' +
|
|
2162
|
+
'.dvo-settings-root .cb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}' +
|
|
2163
|
+
'.dvo-settings-root .cb-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
2164
|
+
'.dvo-settings-root .cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
|
|
2165
|
+
'.dvo-settings-root .cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}' +
|
|
2166
|
+
'.dvo-settings-root .cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}' +
|
|
2167
|
+
'.dvo-settings-root .cb-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
2168
|
+
'.dvo-settings-root .cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}' +
|
|
2169
|
+
'.dvo-settings-root .cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}' +
|
|
2170
|
+
'.dvo-settings-root .cb-badge-bad,.dvo-settings-root .cb-badge-err{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}' +
|
|
2171
|
+
'.dvo-settings-root .cb-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
2172
|
+
'.dvo-settings-root .cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
2173
|
+
'.dvo-settings-root .cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
2174
|
+
'.dvo-settings-root .cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
2175
|
+
'.dvo-settings-root .cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}' +
|
|
2176
|
+
'.dvo-settings-root .cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88}' +
|
|
2177
|
+
'.dvo-settings-root .cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}' +
|
|
2178
|
+
'.dvo-settings-root .cb-btn-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:13px}' +
|
|
2179
|
+
'.dvo-settings-root .cb-btn-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}' +
|
|
2180
|
+
'.dvo-settings-root .cb-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
2181
|
+
'.dvo-settings-root .cb-field select,.dvo-settings-root .cb-field input[type="text"],.dvo-settings-root .cb-field input[type="number"],.dvo-settings-root .cb-field textarea{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
2182
|
+
'.dvo-settings-root .cb-field textarea{height:auto;padding:8px 10px;font-family:inherit}' +
|
|
2183
|
+
'.dvo-settings-root .cb-check-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}' +
|
|
2184
|
+
'.dvo-settings-root .cb-check-label input[type="checkbox"]{width:16px;height:16px;cursor:pointer}' +
|
|
2185
|
+
'.dvo-settings-root .cb-alert-err{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px}' +
|
|
2135
2186
|
'.dvs-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0;max-width:960px}' +
|
|
2136
2187
|
'.dvs-block{display:flex;flex-direction:column;gap:10px}' +
|
|
2137
2188
|
'.dvs-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
@@ -2153,12 +2204,19 @@ window.__ModuleLoader__.load({
|
|
|
2153
2204
|
'.dvo-pcardOpen .dvo-pchev{transform:rotate(180deg)}' +
|
|
2154
2205
|
'.dvo-pbody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}'
|
|
2155
2206
|
const setCssId = 'dsh-voice/settings.module.css'
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
tag
|
|
2159
|
-
tag
|
|
2160
|
-
|
|
2161
|
-
|
|
2207
|
+
function installSettingsCss() {
|
|
2208
|
+
if (typeof document === 'undefined') return () => {}
|
|
2209
|
+
let tag = document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + setCssId + '"]')
|
|
2210
|
+
if (!tag) {
|
|
2211
|
+
tag = document.createElement('style')
|
|
2212
|
+
tag.textContent = SET_CSS
|
|
2213
|
+
tag.setAttribute('data-dsh-plugin', 'dsh-voice')
|
|
2214
|
+
tag.dataset.pluginCss = setCssId
|
|
2215
|
+
document.head.appendChild(tag)
|
|
2216
|
+
}
|
|
2217
|
+
return () => {
|
|
2218
|
+
if (tag && tag.parentNode) tag.parentNode.removeChild(tag)
|
|
2219
|
+
}
|
|
2162
2220
|
}
|
|
2163
2221
|
|
|
2164
2222
|
function createErrorBoundary() {
|
|
@@ -2671,8 +2729,12 @@ window.__ModuleLoader__.load({
|
|
|
2671
2729
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
2672
2730
|
const data = await res.json()
|
|
2673
2731
|
setUpdaterStatus(data)
|
|
2732
|
+
if (data && data.latestCheckFailed) {
|
|
2733
|
+
setUpdaterMsg(t('updateCheckFailed'))
|
|
2734
|
+
}
|
|
2674
2735
|
} catch (e) {
|
|
2675
|
-
|
|
2736
|
+
setUpdaterStatus(null)
|
|
2737
|
+
setUpdaterMsg(t('updateCheckFailed') + ': ' + (e?.message || String(e)))
|
|
2676
2738
|
} finally {
|
|
2677
2739
|
setUpdaterLoading(false)
|
|
2678
2740
|
}
|
|
@@ -2854,16 +2916,24 @@ window.__ModuleLoader__.load({
|
|
|
2854
2916
|
),
|
|
2855
2917
|
React.createElement('div', { className: 'cb-section-desc' }, t('versionHint')),
|
|
2856
2918
|
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px' } },
|
|
2857
|
-
|
|
2858
|
-
'
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
? React.createElement('span', { className: 'cb-badge
|
|
2862
|
-
|
|
2863
|
-
)
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2919
|
+
updaterStatus && updaterStatus.currentVersion
|
|
2920
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, 'v' + updaterStatus.currentVersion)
|
|
2921
|
+
: null,
|
|
2922
|
+
updaterLoading && (!updaterStatus || !updaterStatus.currentVersion)
|
|
2923
|
+
? React.createElement('span', { className: 'cb-badge' }, '⏳ ' + t('checkingUpdates'))
|
|
2924
|
+
: !updaterStatus
|
|
2925
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-warn' }, '❓ ' + t('updateStatusUnknown'))
|
|
2926
|
+
: updaterStatus.updateAvailable
|
|
2927
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
2928
|
+
t('updateAvailable') + ': v' + updaterStatus.latestVersion
|
|
2929
|
+
)
|
|
2930
|
+
: updaterStatus.latestCheckFailed
|
|
2931
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
2932
|
+
'⚠️ ' + t('updateCheckFailed')
|
|
2933
|
+
)
|
|
2934
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
2935
|
+
t('upToDate')
|
|
2936
|
+
),
|
|
2867
2937
|
updaterStatus && updaterStatus.updateAvailable
|
|
2868
2938
|
? React.createElement('button', {
|
|
2869
2939
|
type: 'button', className: 'cb-btn cb-btn-primary', style: { fontSize: '12px', padding: '4px 10px' },
|
|
@@ -2903,6 +2973,7 @@ window.__ModuleLoader__.load({
|
|
|
2903
2973
|
const [err, setErr] = React.useState('')
|
|
2904
2974
|
const [serverConfig, setServerConfig] = React.useState(null)
|
|
2905
2975
|
const [configLoading, setConfigLoading] = React.useState(true)
|
|
2976
|
+
const [revision, setRevision] = React.useState(null)
|
|
2906
2977
|
|
|
2907
2978
|
React.useEffect(() => {
|
|
2908
2979
|
let alive = true
|
|
@@ -2911,6 +2982,7 @@ window.__ModuleLoader__.load({
|
|
|
2911
2982
|
.then((data) => {
|
|
2912
2983
|
if (alive && data && data.ok && data.config) {
|
|
2913
2984
|
setServerConfig(data.config)
|
|
2985
|
+
if (typeof data.revision === 'number') setRevision(data.revision)
|
|
2914
2986
|
setDraft((curr) => curr || deepClone(data.config))
|
|
2915
2987
|
}
|
|
2916
2988
|
})
|
|
@@ -3079,14 +3151,15 @@ window.__ModuleLoader__.load({
|
|
|
3079
3151
|
const res = await fetch('/dsh-voice/config', {
|
|
3080
3152
|
method: 'PUT',
|
|
3081
3153
|
headers: { 'Content-Type': 'application/json' },
|
|
3082
|
-
body: JSON.stringify({ config: draft }),
|
|
3154
|
+
body: JSON.stringify({ config: draft, expectedRevision: (typeof revision === 'number' ? revision : undefined) }),
|
|
3083
3155
|
})
|
|
3084
3156
|
const data = await res.json()
|
|
3085
3157
|
if (res.ok && data && data.ok) {
|
|
3086
3158
|
httpOk = true
|
|
3087
|
-
if (data.config)
|
|
3088
|
-
|
|
3089
|
-
|
|
3159
|
+
if (data.config) setServerConfig(data.config)
|
|
3160
|
+
if (typeof data.revision === 'number') setRevision(data.revision)
|
|
3161
|
+
} else if (res.status === 409) {
|
|
3162
|
+
httpError = data?.error?.message || 'Settings conflict: parallel changes detected'
|
|
3090
3163
|
} else {
|
|
3091
3164
|
httpError = data?.error?.message || data?.error || res.statusText || 'Save failed'
|
|
3092
3165
|
}
|
|
@@ -3094,7 +3167,7 @@ window.__ModuleLoader__.load({
|
|
|
3094
3167
|
httpError = e?.message || String(e)
|
|
3095
3168
|
}
|
|
3096
3169
|
|
|
3097
|
-
if (scope && typeof scope.set === 'function') {
|
|
3170
|
+
if (httpOk && scope && typeof scope.set === 'function') {
|
|
3098
3171
|
for (const k of Object.keys(draft)) {
|
|
3099
3172
|
try {
|
|
3100
3173
|
await scope.set(k, draft[k])
|
|
@@ -3157,13 +3230,22 @@ window.__ModuleLoader__.load({
|
|
|
3157
3230
|
.filter((k) => k && BUILTIN.indexOf(k) < 0),
|
|
3158
3231
|
)
|
|
3159
3232
|
|
|
3160
|
-
const langField = (mode) =>
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3233
|
+
const langField = (mode) => {
|
|
3234
|
+
const val = modeVal(mode, 'language', '')
|
|
3235
|
+
const opts = SUPPORTED_LANGS.slice()
|
|
3236
|
+
if (val && !opts.some((o) => o.code === val)) {
|
|
3237
|
+
opts.push({ code: val, labelKey: val, defaultLabel: val })
|
|
3238
|
+
}
|
|
3239
|
+
return React.createElement('div', { className: 'cb-field' },
|
|
3240
|
+
React.createElement('label', null, t('language')),
|
|
3241
|
+
React.createElement('select', {
|
|
3242
|
+
value: val, disabled: !writable,
|
|
3243
|
+
onChange: (e) => setIn(mode, 'language', e.target.value),
|
|
3244
|
+
}, opts.map((o) => React.createElement('option', { key: o.code || 'auto', value: o.code },
|
|
3245
|
+
t(o.labelKey) !== o.labelKey ? t(o.labelKey) : o.defaultLabel
|
|
3246
|
+
))),
|
|
3247
|
+
)
|
|
3248
|
+
}
|
|
3167
3249
|
|
|
3168
3250
|
const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
3169
3251
|
React.createElement('label', null, label),
|
|
@@ -3180,7 +3262,7 @@ window.__ModuleLoader__.load({
|
|
|
3180
3262
|
const whisperRunning = !!(statusData && statusData.whisperRunning)
|
|
3181
3263
|
const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
|
|
3182
3264
|
|
|
3183
|
-
return React.createElement('div', { className: 'cb-page' },
|
|
3265
|
+
return React.createElement('div', { className: 'cb-page dvo-settings-root' },
|
|
3184
3266
|
// 1. Connection & Engine Status Card
|
|
3185
3267
|
React.createElement(ConnectionStatusCard, {
|
|
3186
3268
|
t: t,
|
|
@@ -3456,13 +3538,14 @@ window.__ModuleLoader__.load({
|
|
|
3456
3538
|
),
|
|
3457
3539
|
React.createElement('span', { className: 'dvo-pchev' }, chevronIcon()),
|
|
3458
3540
|
),
|
|
3459
|
-
(page || open) ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
|
|
3541
|
+
(page || open) ? React.createElement('div', { className: 'dvo-pbody dvo-settings-root' }, React.createElement(VoiceSection, props)) : null,
|
|
3460
3542
|
),
|
|
3461
3543
|
)
|
|
3462
3544
|
}
|
|
3463
3545
|
|
|
3464
3546
|
function registerSettings(ctx) {
|
|
3465
3547
|
ctx.effect(() => {
|
|
3548
|
+
const offCss = installSettingsCss()
|
|
3466
3549
|
const offItem = ctx.slots.inject('plugins.item', () => ctx.slots.register(
|
|
3467
3550
|
{
|
|
3468
3551
|
name: 'plugins.item',
|
|
@@ -3493,6 +3576,7 @@ window.__ModuleLoader__.load({
|
|
|
3493
3576
|
PluginCard,
|
|
3494
3577
|
))
|
|
3495
3578
|
return () => {
|
|
3579
|
+
if (typeof offCss === 'function') offCss()
|
|
3496
3580
|
if (typeof offItem === 'function') offItem()
|
|
3497
3581
|
if (typeof offRow === 'function') offRow()
|
|
3498
3582
|
if (typeof offLegacy === 'function') offLegacy()
|
|
@@ -3516,6 +3600,9 @@ window.__ModuleLoader__.load({
|
|
|
3516
3600
|
const undo = [addLocale('en', en), addLocale('zh', zh)]
|
|
3517
3601
|
return () => { for (const off of undo) off() }
|
|
3518
3602
|
}, 'dsh-voice: locale dictionaries')
|
|
3603
|
+
ctx.effect(() => {
|
|
3604
|
+
return registerGlobalLifecycle()
|
|
3605
|
+
}, 'dsh-voice: global lifecycle')
|
|
3519
3606
|
moduleT = ctx.locale.bind(NS)
|
|
3520
3607
|
registerComposer(ctx)
|
|
3521
3608
|
registerSettings(ctx)
|
package/lib/config-routes.js
CHANGED
|
@@ -27,9 +27,14 @@ export function registerConfigRoutes(ctx, {
|
|
|
27
27
|
return
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// 3. GET: return live configuration snapshot
|
|
30
|
+
// 3. GET: return live configuration snapshot with revision
|
|
31
31
|
if (req.method === 'GET') {
|
|
32
|
-
|
|
32
|
+
let rev
|
|
33
|
+
try {
|
|
34
|
+
const settingsService = getSettingsService?.()
|
|
35
|
+
rev = settingsService?.describe?.()?.find?.((r) => r.ns === NS)?.revision
|
|
36
|
+
} catch { /* ignore revision lookup errors */ }
|
|
37
|
+
writeJson(res, 200, { ok: true, config: live(), revision: rev })
|
|
33
38
|
return
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -62,6 +67,9 @@ export function registerConfigRoutes(ctx, {
|
|
|
62
67
|
const rawConfig = payload && typeof payload.config === 'object' && payload.config !== null
|
|
63
68
|
? payload.config
|
|
64
69
|
: payload
|
|
70
|
+
const expectedRevision = (typeof payload?.expectedRevision === 'number')
|
|
71
|
+
? payload.expectedRevision
|
|
72
|
+
: ((typeof payload?.revision === 'number') ? payload.revision : undefined)
|
|
65
73
|
|
|
66
74
|
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
67
75
|
writeJson(res, 400, { ok: false, error: { code: 'body', message: 'body must be a JSON object' } })
|
|
@@ -88,21 +96,35 @@ export function registerConfigRoutes(ctx, {
|
|
|
88
96
|
try {
|
|
89
97
|
let saved = false
|
|
90
98
|
if (settingsService) {
|
|
91
|
-
let
|
|
99
|
+
let currentRev
|
|
92
100
|
try {
|
|
93
|
-
|
|
101
|
+
currentRev = settingsService.describe?.()?.find?.((r) => r.ns === NS)?.revision
|
|
94
102
|
} catch { /* ignore revision lookup errors */ }
|
|
95
103
|
|
|
104
|
+
if (typeof expectedRevision === 'number' && typeof currentRev === 'number' && expectedRevision !== currentRev) {
|
|
105
|
+
writeJson(res, 409, {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: {
|
|
108
|
+
code: 'conflict',
|
|
109
|
+
message: `Settings revision conflict: expected ${expectedRevision}, current is ${currentRev}`,
|
|
110
|
+
currentRevision: currentRev,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const targetRev = (typeof expectedRevision === 'number') ? expectedRevision : currentRev
|
|
117
|
+
|
|
96
118
|
if (typeof settingsService.replace === 'function') {
|
|
97
|
-
await settingsService.replace(NS, merged,
|
|
119
|
+
await settingsService.replace(NS, merged, targetRev)
|
|
98
120
|
saved = true
|
|
99
121
|
} else if (typeof settingsService.update === 'function') {
|
|
100
|
-
await settingsService.update(NS, merged,
|
|
122
|
+
await settingsService.update(NS, merged, targetRev)
|
|
101
123
|
saved = true
|
|
102
124
|
}
|
|
103
125
|
}
|
|
104
126
|
|
|
105
|
-
if (scope) {
|
|
127
|
+
if (!saved && scope) {
|
|
106
128
|
if (typeof scope.replace === 'function') {
|
|
107
129
|
await scope.replace(merged)
|
|
108
130
|
saved = true
|
|
@@ -110,10 +132,11 @@ export function registerConfigRoutes(ctx, {
|
|
|
110
132
|
await scope.patch(merged)
|
|
111
133
|
saved = true
|
|
112
134
|
} else if (typeof scope.set === 'function') {
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
const entries = Object.entries(merged)
|
|
136
|
+
for (const [k, v] of entries) {
|
|
137
|
+
await scope.set(k, v)
|
|
115
138
|
}
|
|
116
|
-
saved = true
|
|
139
|
+
if (entries.length > 0) saved = true
|
|
117
140
|
}
|
|
118
141
|
}
|
|
119
142
|
|
|
@@ -126,7 +149,11 @@ export function registerConfigRoutes(ctx, {
|
|
|
126
149
|
triggerAutostart()
|
|
127
150
|
}
|
|
128
151
|
|
|
129
|
-
|
|
152
|
+
let newRev
|
|
153
|
+
try {
|
|
154
|
+
newRev = settingsService?.describe?.()?.find?.((r) => r.ns === NS)?.revision
|
|
155
|
+
} catch {}
|
|
156
|
+
writeJson(res, 200, { ok: true, config: live(), revision: newRev })
|
|
130
157
|
} catch (err) {
|
|
131
158
|
writeJson(res, 500, { ok: false, error: { code: 'save', message: String(err?.message || err) } })
|
|
132
159
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|