@goodandready/dsh-voice 0.8.38 → 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 +13 -1
- 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,6 +215,7 @@ 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
|
|
|
@@ -369,6 +371,7 @@ export function apply(ctx, baseConfig) {
|
|
|
369
371
|
}
|
|
370
372
|
|
|
371
373
|
ctx.inject(['settings'], (sctx) => {
|
|
374
|
+
settingsService = sctx.settings
|
|
372
375
|
try {
|
|
373
376
|
scope = sctx.settings.register(NS, Config, { base: baseConfig })
|
|
374
377
|
if (scope && typeof scope.get === 'function') {
|
|
@@ -380,7 +383,7 @@ export function apply(ctx, baseConfig) {
|
|
|
380
383
|
})
|
|
381
384
|
}
|
|
382
385
|
} catch { /* modern DSH handles settings via SettingsForms */ }
|
|
383
|
-
sctx.effect(() => () => { getConfig = readEntryConfig; scope = null })
|
|
386
|
+
sctx.effect(() => () => { getConfig = readEntryConfig; scope = null; settingsService = null })
|
|
384
387
|
triggerAutostart()
|
|
385
388
|
})
|
|
386
389
|
|
|
@@ -664,5 +667,14 @@ export function apply(ctx, baseConfig) {
|
|
|
664
667
|
},
|
|
665
668
|
startSensevoice,
|
|
666
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')
|
|
667
679
|
}
|
|
668
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",
|