@goodandready/dsh-voice 0.8.37 → 0.9.0
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/README.md +2 -0
- package/README.zh.md +10 -0
- package/lib/client.js +74 -13
- package/lib/config-routes.js +135 -0
- package/lib/index.js +76 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,6 +217,8 @@ Registers `transcribe_audio(file_path, language?)` in `ctx.tools`, allowing agen
|
|
|
217
217
|
* `POST /dsh-voice/transcribe` — `{ dataBase64, mimeType, mode }` → `{ ok, text, provider, tookMs }`
|
|
218
218
|
* `POST /dsh-voice/polish` — `{ text }` → `{ ok, text }`
|
|
219
219
|
* `GET /dsh-voice/status` — Returns daemon status, active chains, SenseVoice.
|
|
220
|
+
* `GET /dsh-voice/config` — Returns live plugin configuration snapshot.
|
|
221
|
+
* `PUT /dsh-voice/config` — Updates and persists plugin configuration across network.
|
|
220
222
|
|
|
221
223
|
---
|
|
222
224
|
|
package/README.zh.md
CHANGED
|
@@ -129,6 +129,16 @@ v0.8.18 质量审查后的修复批次(Gitea #79–#87,PR #88):
|
|
|
129
129
|
|
|
130
130
|
---
|
|
131
131
|
|
|
132
|
+
## 🤖 智能体工具与 HTTP 端点
|
|
133
|
+
|
|
134
|
+
* `POST /dsh-voice/transcribe` — 音频转写:`{ dataBase64, mimeType, mode }` → `{ ok, text, provider, tookMs }`
|
|
135
|
+
* `POST /dsh-voice/polish` — 文本模型润色:`{ text }` → `{ ok, text }`
|
|
136
|
+
* `GET /dsh-voice/status` — 查询后端状态、服务商健康度与 SenseVoice
|
|
137
|
+
* `GET /dsh-voice/config` — 获取插件运行时配置快照
|
|
138
|
+
* `PUT /dsh-voice/config` — 跨网络安全持久化更新插件配置
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
132
142
|
## 📦 安装指南
|
|
133
143
|
|
|
134
144
|
```bash
|
package/lib/client.js
CHANGED
|
@@ -1222,6 +1222,15 @@ window.__ModuleLoader__.load({
|
|
|
1222
1222
|
if (event.repeat) return
|
|
1223
1223
|
// A modifier hotkey does not fight the text field: modifiers do not
|
|
1224
1224
|
// type. A plain letter key must not be hijacked while typing.
|
|
1225
|
+
const target = event.target
|
|
1226
|
+
const isEditable = target && (
|
|
1227
|
+
target.tagName === 'INPUT' ||
|
|
1228
|
+
target.tagName === 'TEXTAREA' ||
|
|
1229
|
+
target.isContentEditable ||
|
|
1230
|
+
(typeof target.getAttribute === 'function' && target.getAttribute('contenteditable') === 'true')
|
|
1231
|
+
)
|
|
1232
|
+
if (isEditable && !['Control', 'Alt', 'Shift'].includes(keyName)) return
|
|
1233
|
+
|
|
1225
1234
|
if (hotkeyMatches(event, keyName)) beginHold(mode)
|
|
1226
1235
|
}
|
|
1227
1236
|
const up = (event) => {
|
|
@@ -2174,6 +2183,25 @@ window.__ModuleLoader__.load({
|
|
|
2174
2183
|
const [draft, setDraft] = React.useState(null)
|
|
2175
2184
|
const [saved, setSaved] = React.useState(false)
|
|
2176
2185
|
const [err, setErr] = React.useState('')
|
|
2186
|
+
const [serverConfig, setServerConfig] = React.useState(null)
|
|
2187
|
+
const [configLoading, setConfigLoading] = React.useState(true)
|
|
2188
|
+
|
|
2189
|
+
React.useEffect(() => {
|
|
2190
|
+
let alive = true
|
|
2191
|
+
fetch('/dsh-voice/config', { cache: 'no-store' })
|
|
2192
|
+
.then((r) => r.json())
|
|
2193
|
+
.then((data) => {
|
|
2194
|
+
if (alive && data && data.ok && data.config) {
|
|
2195
|
+
setServerConfig(data.config)
|
|
2196
|
+
setDraft((curr) => curr || deepClone(data.config))
|
|
2197
|
+
}
|
|
2198
|
+
})
|
|
2199
|
+
.catch(() => {})
|
|
2200
|
+
.finally(() => {
|
|
2201
|
+
if (alive) setConfigLoading(false)
|
|
2202
|
+
})
|
|
2203
|
+
return () => { alive = false }
|
|
2204
|
+
}, [])
|
|
2177
2205
|
|
|
2178
2206
|
// Hotkey picker: not a name field — press the key you want.
|
|
2179
2207
|
const [catching, setCatching] = React.useState(false)
|
|
@@ -2207,9 +2235,9 @@ window.__ModuleLoader__.load({
|
|
|
2207
2235
|
.catch(() => {})
|
|
2208
2236
|
}, [])
|
|
2209
2237
|
|
|
2210
|
-
const ready = !!snap && snap.status === 'ready'
|
|
2211
|
-
const value = ready && snap.value ? snap.value : {}
|
|
2212
|
-
const writable = ready
|
|
2238
|
+
const ready = !!serverConfig || (!!snap && snap.status === 'ready')
|
|
2239
|
+
const value = (snap && snap.status === 'ready' && snap.value) ? snap.value : (serverConfig || {})
|
|
2240
|
+
const writable = ready
|
|
2213
2241
|
|
|
2214
2242
|
React.useEffect(() => {
|
|
2215
2243
|
if (ready) return undefined
|
|
@@ -2455,7 +2483,7 @@ window.__ModuleLoader__.load({
|
|
|
2455
2483
|
}, [checkUpdater])
|
|
2456
2484
|
|
|
2457
2485
|
if (!ready) {
|
|
2458
|
-
const waiting = !snap || snap.status === 'loading'
|
|
2486
|
+
const waiting = configLoading || (!snap || snap.status === 'loading')
|
|
2459
2487
|
return React.createElement('div', { className: 'cb-page' },
|
|
2460
2488
|
React.createElement('div', { className: 'dvs-wait' }, waiting
|
|
2461
2489
|
? t('loadingSettings')
|
|
@@ -2476,13 +2504,40 @@ window.__ModuleLoader__.load({
|
|
|
2476
2504
|
setErr(''); setSaved(false)
|
|
2477
2505
|
if (!draft) return
|
|
2478
2506
|
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2507
|
+
let httpOk = false
|
|
2508
|
+
let httpError = null
|
|
2509
|
+
try {
|
|
2510
|
+
const res = await fetch('/dsh-voice/config', {
|
|
2511
|
+
method: 'PUT',
|
|
2512
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2513
|
+
body: JSON.stringify({ config: draft }),
|
|
2514
|
+
})
|
|
2515
|
+
const data = await res.json()
|
|
2516
|
+
if (res.ok && data && data.ok) {
|
|
2517
|
+
httpOk = true
|
|
2518
|
+
if (data.config) {
|
|
2519
|
+
setServerConfig(data.config)
|
|
2520
|
+
}
|
|
2521
|
+
} else {
|
|
2522
|
+
httpError = data?.error?.message || data?.error || res.statusText || 'Save failed'
|
|
2485
2523
|
}
|
|
2524
|
+
} catch (e) {
|
|
2525
|
+
httpError = e?.message || String(e)
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
if (scope && typeof scope.set === 'function') {
|
|
2529
|
+
for (const k of Object.keys(draft)) {
|
|
2530
|
+
try {
|
|
2531
|
+
await scope.set(k, draft[k])
|
|
2532
|
+
} catch (e) {
|
|
2533
|
+
/* best effort on loopback */
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
if (!httpOk) {
|
|
2539
|
+
setErr(t('saveFailed') + ': ' + httpError)
|
|
2540
|
+
return
|
|
2486
2541
|
}
|
|
2487
2542
|
|
|
2488
2543
|
const d = draft || {}
|
|
@@ -2501,11 +2556,17 @@ window.__ModuleLoader__.load({
|
|
|
2501
2556
|
noiseGateDb: Number(d.noiseGateDb !== undefined ? d.noiseGateDb : -45),
|
|
2502
2557
|
contextGlossary: d.contextGlossary !== false,
|
|
2503
2558
|
visualizerStyle: d.visualizerStyle || 'liquid-wave',
|
|
2559
|
+
gatedTurnTaking: d.gatedTurnTaking !== false,
|
|
2560
|
+
autoSendVisualRing: d.autoSendVisualRing !== false,
|
|
2561
|
+
voiceActions: d.voiceActions !== false,
|
|
2562
|
+
techJargonCorrection: d.techJargonCorrection !== false,
|
|
2563
|
+
liveInterimPreview: d.liveInterimPreview !== false,
|
|
2564
|
+
structuredPromptVoice: d.structuredPromptVoice !== false,
|
|
2504
2565
|
})
|
|
2505
2566
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
2506
2567
|
|
|
2507
|
-
|
|
2508
|
-
|
|
2568
|
+
setSaved(true)
|
|
2569
|
+
setTimeout(() => setSaved(false), 2000)
|
|
2509
2570
|
fetchStatus()
|
|
2510
2571
|
}
|
|
2511
2572
|
|
|
@@ -3047,7 +3108,7 @@ window.__ModuleLoader__.load({
|
|
|
3047
3108
|
React.createElement('div', { className: 'cb-section-desc' }, t('versionHint')),
|
|
3048
3109
|
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px' } },
|
|
3049
3110
|
React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
3050
|
-
'v' + ((updaterStatus && updaterStatus.currentVersion) || '0.
|
|
3111
|
+
'v' + ((updaterStatus && updaterStatus.currentVersion) || '0.9.0')
|
|
3051
3112
|
),
|
|
3052
3113
|
updaterStatus && updaterStatus.updateAvailable
|
|
3053
3114
|
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// lib/config-routes.js — Host route for plugin configuration persistence over network.
|
|
2
|
+
// Solves DSH network limitation where client-side persistence is blocked in non-loopback environments (Gitea #149).
|
|
3
|
+
|
|
4
|
+
import { writeJson, readBody, isTrustedCaller } from './http-util.js'
|
|
5
|
+
|
|
6
|
+
export function registerConfigRoutes(ctx, {
|
|
7
|
+
NS,
|
|
8
|
+
live,
|
|
9
|
+
getSettingsService,
|
|
10
|
+
getScope,
|
|
11
|
+
validateConfig,
|
|
12
|
+
triggerAutostart,
|
|
13
|
+
}) {
|
|
14
|
+
return ctx.webServer.register({
|
|
15
|
+
kind: 'exact',
|
|
16
|
+
path: '/dsh-voice/config',
|
|
17
|
+
handler: async (req, res) => {
|
|
18
|
+
// 1. Method check: only GET, PUT, POST allowed
|
|
19
|
+
if (req.method !== 'GET' && req.method !== 'PUT' && req.method !== 'POST') {
|
|
20
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET, PUT, or POST only' } })
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Security check: loopback or same-origin
|
|
25
|
+
if (!isTrustedCaller(req)) {
|
|
26
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } })
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. GET: return live configuration snapshot
|
|
31
|
+
if (req.method === 'GET') {
|
|
32
|
+
writeJson(res, 200, { ok: true, config: live() })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 4. PUT / POST: check settings service availability
|
|
37
|
+
const settingsService = getSettingsService?.()
|
|
38
|
+
const scope = getScope?.()
|
|
39
|
+
if (!settingsService && !scope) {
|
|
40
|
+
writeJson(res, 503, { ok: false, error: { code: 'unavailable', message: 'settings service not ready' } })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 5. Read request body
|
|
45
|
+
let raw
|
|
46
|
+
try {
|
|
47
|
+
raw = await readBody(req, 1024 * 1024)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 6. Parse JSON payload
|
|
54
|
+
let payload
|
|
55
|
+
try {
|
|
56
|
+
payload = JSON.parse(raw.toString('utf8') || '{}')
|
|
57
|
+
} catch {
|
|
58
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rawConfig = payload && typeof payload.config === 'object' && payload.config !== null
|
|
63
|
+
? payload.config
|
|
64
|
+
: payload
|
|
65
|
+
|
|
66
|
+
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
67
|
+
writeJson(res, 400, { ok: false, error: { code: 'body', message: 'body must be a JSON object' } })
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 7. Validate and merge configuration
|
|
72
|
+
let merged
|
|
73
|
+
try {
|
|
74
|
+
const current = live() || {}
|
|
75
|
+
const candidate = {
|
|
76
|
+
...current,
|
|
77
|
+
...rawConfig,
|
|
78
|
+
dictation: { ...(current.dictation || {}), ...(rawConfig.dictation || {}) },
|
|
79
|
+
message: { ...(current.message || {}), ...(rawConfig.message || {}) },
|
|
80
|
+
}
|
|
81
|
+
merged = validateConfig ? validateConfig(candidate) : candidate
|
|
82
|
+
} catch (err) {
|
|
83
|
+
writeJson(res, 400, { ok: false, error: { code: 'validation', message: String(err?.message || err) } })
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 8. Persist to host settings
|
|
88
|
+
try {
|
|
89
|
+
let saved = false
|
|
90
|
+
if (settingsService) {
|
|
91
|
+
let rev
|
|
92
|
+
try {
|
|
93
|
+
rev = settingsService.describe?.()?.find?.((r) => r.ns === NS)?.revision
|
|
94
|
+
} catch { /* ignore revision lookup errors */ }
|
|
95
|
+
|
|
96
|
+
if (typeof settingsService.replace === 'function') {
|
|
97
|
+
await settingsService.replace(NS, merged, rev)
|
|
98
|
+
saved = true
|
|
99
|
+
} else if (typeof settingsService.update === 'function') {
|
|
100
|
+
await settingsService.update(NS, merged, rev)
|
|
101
|
+
saved = true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (scope) {
|
|
106
|
+
if (typeof scope.replace === 'function') {
|
|
107
|
+
await scope.replace(merged)
|
|
108
|
+
saved = true
|
|
109
|
+
} else if (typeof scope.patch === 'function') {
|
|
110
|
+
await scope.patch(merged)
|
|
111
|
+
saved = true
|
|
112
|
+
} else if (typeof scope.set === 'function') {
|
|
113
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
114
|
+
await scope.set(k, v).catch(() => {})
|
|
115
|
+
}
|
|
116
|
+
saved = true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!saved) {
|
|
121
|
+
writeJson(res, 503, { ok: false, error: { code: 'unavailable', message: 'failed to persist settings' } })
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof triggerAutostart === 'function') {
|
|
126
|
+
triggerAutostart()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
writeJson(res, 200, { ok: true, config: live() })
|
|
130
|
+
} catch (err) {
|
|
131
|
+
writeJson(res, 500, { ok: false, error: { code: 'save', message: String(err?.message || err) } })
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { buildProviderOrder, sessionCommand } from './transcribe-core.js'
|
|
|
29
29
|
import { registerPluginUpdater } from './updater.js'
|
|
30
30
|
import { registerSensevoiceInstaller } from './sensevoice-installer.js'
|
|
31
31
|
import { createConfigReader } from './config-compat.js'
|
|
32
|
+
import { registerConfigRoutes } from './config-routes.js'
|
|
32
33
|
|
|
33
34
|
function isAutoLang(lang) {
|
|
34
35
|
return !lang || lang === 'auto' || String(lang).includes(',')
|
|
@@ -214,19 +215,10 @@ export function apply(ctx, baseConfig) {
|
|
|
214
215
|
// while caching unwrapped values across requests.
|
|
215
216
|
const readEntryConfig = createConfigReader(baseConfig, ctx)
|
|
216
217
|
let scope = null
|
|
218
|
+
let settingsService = null
|
|
217
219
|
let getConfig = readEntryConfig
|
|
218
220
|
const live = () => BaseConfig(structuredClone(getConfig() ?? {})) ?? readEntryConfig()
|
|
219
221
|
|
|
220
|
-
ctx.inject(['settings'], (sctx) => {
|
|
221
|
-
try {
|
|
222
|
-
scope = sctx.settings.register(NS, Config, { base: baseConfig })
|
|
223
|
-
if (scope && typeof scope.get === 'function') {
|
|
224
|
-
getConfig = () => scope.get() ?? readEntryConfig()
|
|
225
|
-
}
|
|
226
|
-
} catch { /* modern DSH handles settings via SettingsForms */ }
|
|
227
|
-
sctx.effect(() => () => { getConfig = readEntryConfig; scope = null })
|
|
228
|
-
})
|
|
229
|
-
|
|
230
222
|
async function resolveKey(ref) {
|
|
231
223
|
try {
|
|
232
224
|
const resolved = await ctx.credentials.resolve(credentialRef(ref))
|
|
@@ -269,12 +261,27 @@ export function apply(ctx, baseConfig) {
|
|
|
269
261
|
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
270
262
|
})
|
|
271
263
|
child = ctx.shell.start(spec)
|
|
272
|
-
|
|
264
|
+
if (child && typeof child.on === 'function') {
|
|
265
|
+
child.on('error', (err) => {
|
|
266
|
+
whisperError = err?.message || String(err)
|
|
267
|
+
})
|
|
268
|
+
child.on('exit', (code) => {
|
|
269
|
+
if (code !== 0 && code !== null) {
|
|
270
|
+
whisperError = `whisper server process exited with code ${code}`
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
// Wait for it to become healthy (up to 30s for cold model loading on Metal/CPU).
|
|
275
|
+
for (let i = 0; i < 60; i++) {
|
|
273
276
|
await new Promise((r) => setTimeout(r, 500))
|
|
274
277
|
if (await whisperAlive()) {
|
|
275
278
|
whisperError = null
|
|
276
279
|
return true
|
|
277
280
|
}
|
|
281
|
+
if (child && child.exitCode !== null && child.exitCode !== undefined) {
|
|
282
|
+
whisperError = `whisper server process exited prematurely with code ${child.exitCode}`
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
278
285
|
}
|
|
279
286
|
whisperError = 'whisper server process started but failed health check on port 8001'
|
|
280
287
|
return false
|
|
@@ -285,8 +292,6 @@ export function apply(ctx, baseConfig) {
|
|
|
285
292
|
finally { startingWhisper = false }
|
|
286
293
|
}
|
|
287
294
|
|
|
288
|
-
startWhisper().catch(() => {})
|
|
289
|
-
|
|
290
295
|
// SenseVoice-ONNX / Sherpa-ONNX: autostart and health check.
|
|
291
296
|
let sensevoiceChild = null
|
|
292
297
|
let sensevoiceError = null
|
|
@@ -325,12 +330,27 @@ export function apply(ctx, baseConfig) {
|
|
|
325
330
|
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
326
331
|
})
|
|
327
332
|
sensevoiceChild = ctx.shell.start(spec)
|
|
328
|
-
|
|
333
|
+
if (sensevoiceChild && typeof sensevoiceChild.on === 'function') {
|
|
334
|
+
sensevoiceChild.on('error', (err) => {
|
|
335
|
+
sensevoiceError = err?.message || String(err)
|
|
336
|
+
})
|
|
337
|
+
sensevoiceChild.on('exit', (code) => {
|
|
338
|
+
if (code !== 0 && code !== null) {
|
|
339
|
+
sensevoiceError = `sensevoice process exited with code ${code}`
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
// Wait for it to become healthy (up to 30s).
|
|
344
|
+
for (let i = 0; i < 60; i++) {
|
|
329
345
|
await new Promise((r) => setTimeout(r, 500))
|
|
330
346
|
if (await sensevoiceAlive()) {
|
|
331
347
|
sensevoiceError = null
|
|
332
348
|
return true
|
|
333
349
|
}
|
|
350
|
+
if (sensevoiceChild && sensevoiceChild.exitCode !== null && sensevoiceChild.exitCode !== undefined) {
|
|
351
|
+
sensevoiceError = `sensevoice process exited prematurely with code ${sensevoiceChild.exitCode}`
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
334
354
|
}
|
|
335
355
|
sensevoiceError = 'sensevoice process started but failed health check'
|
|
336
356
|
return false
|
|
@@ -341,7 +361,39 @@ export function apply(ctx, baseConfig) {
|
|
|
341
361
|
finally { startingSensevoice = false }
|
|
342
362
|
}
|
|
343
363
|
|
|
344
|
-
|
|
364
|
+
function triggerAutostart() {
|
|
365
|
+
startWhisper().catch((err) => {
|
|
366
|
+
ctx.logger?.debug?.('whisper autostart failed: %s', err?.message || err)
|
|
367
|
+
})
|
|
368
|
+
startSensevoice().catch((err) => {
|
|
369
|
+
ctx.logger?.debug?.('sensevoice autostart failed: %s', err?.message || err)
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
ctx.inject(['settings'], (sctx) => {
|
|
374
|
+
settingsService = sctx.settings
|
|
375
|
+
try {
|
|
376
|
+
scope = sctx.settings.register(NS, Config, { base: baseConfig })
|
|
377
|
+
if (scope && typeof scope.get === 'function') {
|
|
378
|
+
getConfig = () => scope.get() ?? readEntryConfig()
|
|
379
|
+
}
|
|
380
|
+
if (scope && typeof scope.watch === 'function') {
|
|
381
|
+
scope.watch(() => {
|
|
382
|
+
triggerAutostart()
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
} catch { /* modern DSH handles settings via SettingsForms */ }
|
|
386
|
+
sctx.effect(() => () => { getConfig = readEntryConfig; scope = null; settingsService = null })
|
|
387
|
+
triggerAutostart()
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
if (typeof ctx.on === 'function') {
|
|
391
|
+
ctx.on('loader/volatile-update', () => triggerAutostart())
|
|
392
|
+
ctx.on('settings/document-updated', () => triggerAutostart())
|
|
393
|
+
ctx.on('config', () => triggerAutostart())
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
triggerAutostart()
|
|
345
397
|
|
|
346
398
|
// Provider health and latency stats (Latency & Health Dashboard).
|
|
347
399
|
const statsTracker = createStatsTracker()
|
|
@@ -615,5 +667,14 @@ export function apply(ctx, baseConfig) {
|
|
|
615
667
|
},
|
|
616
668
|
startSensevoice,
|
|
617
669
|
}), 'dsh-voice: sensevoice installer route')
|
|
670
|
+
|
|
671
|
+
ctx.effect(() => registerConfigRoutes(ctx, {
|
|
672
|
+
NS,
|
|
673
|
+
live,
|
|
674
|
+
getSettingsService: () => settingsService,
|
|
675
|
+
getScope: () => scope,
|
|
676
|
+
validateConfig: (val) => BaseConfig(structuredClone(val)),
|
|
677
|
+
triggerAutostart,
|
|
678
|
+
}), 'dsh-voice: config route')
|
|
618
679
|
}
|
|
619
680
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|