@goodandready/dsh-tts 0.4.22 → 0.4.24
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/CHANGELOG.md +18 -0
- package/lib/client.js +125 -0
- package/lib/engines/manager.js +14 -2
- package/lib/http-util.js +70 -15
- package/lib/routes.js +7 -0
- package/package.json +7 -6
- package/lib/client-src/00-open.js +0 -89
- package/lib/client-src/10-styles.js +0 -55
- package/lib/client-src/20-player.js +0 -424
- package/lib/client-src/30-locales.js +0 -374
- package/lib/client-src/40-editors.js +0 -422
- package/lib/client-src/50-settings.js +0 -399
- package/lib/client-src/60-card-dock.js +0 -278
- package/scripts/build-client.mjs +0 -19
- package/scripts/f5_daemon.py +0 -40
- package/scripts/publish-github.sh +0 -184
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.24
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Peer gate on DSH 0.2.0-rc.1** (#58): DSH skips a profile bundle whose `peerDependencies` exclude the running version, so this plugin was absent from the profile with no error in the UI. Every `@deepseek-ai/dsh-*` peer now names both the 0.1.7-rc.2 and 0.2.0-rc.1 lines, because semver does not admit a prerelease of the next minor into a range that does not name it.
|
|
7
|
+
|
|
3
8
|
Notable changes to `@goodandready/dsh-tts`.
|
|
4
9
|
|
|
10
|
+
## 0.4.23
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- **Strict Settings Request Trust Policy**: Eliminated `same-site` and arbitrary auth header bypasses in `isTrustedSettingsRequest`. Rejects non-same-origin callers, validates matching origin/host pair with loopback remote address, and requires explicit token verification against `DSH_AUTH_TOKEN`/`DSH_TOKEN` (#119).
|
|
14
|
+
- **Protected Model Status & Path Redaction**: `GET /dsh-tts/models/status` now requires `isTrustedSettingsRequest` authorization (returns 403 for untrusted/cross-site requests) and completely redacts absolute filesystem paths from model state descriptors (#139).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **One-Click Updater in Settings Card**: Integrated a unified live updater block (`PluginUpdaterBlock`) into the settings card, displaying current version, latest npm release badge, update execution button, and DSH restart instructions connected to `/dsh-tts/updater` and `/api/dsh-tts/update` (#120).
|
|
18
|
+
- **Durable Model Download Failure Tracking**: `modelManager` now tracks sanitized download failure states across engines, reports errors through protected status (`failed: true`, `error: sanitizedMessage`), and clears errors on retry or deletion (#141).
|
|
19
|
+
|
|
20
|
+
### Packaging
|
|
21
|
+
- **Clean Runtime-Only Package Boundary**: Updated `package.json` files allowlist to exclude `scripts/` (build/release tooling) and `lib/client-src/` from published npm archive, preserving strict runtime-only surface (#140).
|
|
22
|
+
|
|
5
23
|
## 0.4.22
|
|
6
24
|
|
|
7
25
|
### Fixed
|
package/lib/client.js
CHANGED
|
@@ -750,6 +750,16 @@ async function pollPending() {
|
|
|
750
750
|
'messengerInstallHint': 'To enable voice replies in messengers, install the plugin with',
|
|
751
751
|
'messengerTtsEnabled': 'Voice replies in messengers',
|
|
752
752
|
'messengerTtsEnabledHint': 'Allow the messenger gateway to deliver replies as voice notes.',
|
|
753
|
+
'updaterTitle': 'Plugin Updates',
|
|
754
|
+
'updaterCheck': 'Check updates',
|
|
755
|
+
'updaterChecking': 'Checking…',
|
|
756
|
+
'updaterCurrent': 'Current version',
|
|
757
|
+
'updaterLatest': 'Latest version',
|
|
758
|
+
'updaterUpToDate': 'Up to date',
|
|
759
|
+
'updaterUpdateNow': 'Update to v{version}',
|
|
760
|
+
'updaterUpdating': 'Updating…',
|
|
761
|
+
'updaterSuccess': 'Plugin updated successfully! Please restart DSH service.',
|
|
762
|
+
'updaterFailed': 'Update check failed: ',
|
|
753
763
|
}
|
|
754
764
|
|
|
755
765
|
const zh = {
|
|
@@ -939,6 +949,16 @@ async function pollPending() {
|
|
|
939
949
|
'messengerInstallHint': '如需向聊天软件发送语音回复,请运行:',
|
|
940
950
|
'messengerTtsEnabled': '群组语音回复',
|
|
941
951
|
'messengerTtsEnabledHint': '允许网关将回复以语音便签 (Voice Note) 形式发送。',
|
|
952
|
+
'updaterTitle': '插件更新',
|
|
953
|
+
'updaterCheck': '检查更新',
|
|
954
|
+
'updaterChecking': '正在检查…',
|
|
955
|
+
'updaterCurrent': '当前版本',
|
|
956
|
+
'updaterLatest': '最新版本',
|
|
957
|
+
'updaterUpToDate': '已是最新',
|
|
958
|
+
'updaterUpdateNow': '一键更新至 v{version}',
|
|
959
|
+
'updaterUpdating': '正在更新…',
|
|
960
|
+
'updaterSuccess': '插件更新成功!请重启 DSH 服务以生效。',
|
|
961
|
+
'updaterFailed': '更新检查失败:',
|
|
942
962
|
}
|
|
943
963
|
function ChainEditor(props) {
|
|
944
964
|
// Translator comes from the card: the slot passes props.t only to it.
|
|
@@ -1339,6 +1359,110 @@ async function pollPending() {
|
|
|
1339
1359
|
)
|
|
1340
1360
|
}
|
|
1341
1361
|
|
|
1362
|
+
|
|
1363
|
+
// Plugin updater block: check updates, show badge, and trigger one-click update
|
|
1364
|
+
function PluginUpdaterBlock(props) {
|
|
1365
|
+
const t = props.t || ((key) => key)
|
|
1366
|
+
const [status, setStatus] = React.useState(null)
|
|
1367
|
+
const [loading, setLoading] = React.useState(false)
|
|
1368
|
+
const [updating, setUpdating] = React.useState(false)
|
|
1369
|
+
const [msg, setMsg] = React.useState(null)
|
|
1370
|
+
|
|
1371
|
+
const checkUpdate = React.useCallback(async () => {
|
|
1372
|
+
setLoading(true)
|
|
1373
|
+
setMsg(null)
|
|
1374
|
+
try {
|
|
1375
|
+
const res = await fetch('/api/dsh-tts/update', {
|
|
1376
|
+
headers: { 'x-dsh-plugin-update': '1' },
|
|
1377
|
+
})
|
|
1378
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
1379
|
+
const data = await res.json()
|
|
1380
|
+
setStatus(data)
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
setMsg({ ok: false, text: (t('updaterFailed') || 'Update check failed: ') + (e && e.message || String(e)) })
|
|
1383
|
+
} finally {
|
|
1384
|
+
setLoading(false)
|
|
1385
|
+
}
|
|
1386
|
+
}, [t])
|
|
1387
|
+
|
|
1388
|
+
const onUpdateNow = async () => {
|
|
1389
|
+
setUpdating(true)
|
|
1390
|
+
setMsg(null)
|
|
1391
|
+
try {
|
|
1392
|
+
const res = await fetch('/api/dsh-tts/update', {
|
|
1393
|
+
method: 'POST',
|
|
1394
|
+
headers: {
|
|
1395
|
+
'x-dsh-plugin-update': '1',
|
|
1396
|
+
'content-type': 'application/json',
|
|
1397
|
+
},
|
|
1398
|
+
})
|
|
1399
|
+
const data = await res.json()
|
|
1400
|
+
if (!res.ok) throw new Error((data && (data.error || data.message)) || 'HTTP ' + res.status)
|
|
1401
|
+
setStatus(data)
|
|
1402
|
+
setMsg({ ok: true, text: t('updaterSuccess') || 'Plugin updated successfully! Please restart DSH service.' })
|
|
1403
|
+
} catch (e) {
|
|
1404
|
+
setMsg({ ok: false, text: e && e.message || String(e) })
|
|
1405
|
+
} finally {
|
|
1406
|
+
setUpdating(false)
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
React.useEffect(() => {
|
|
1411
|
+
checkUpdate()
|
|
1412
|
+
}, [checkUpdate])
|
|
1413
|
+
|
|
1414
|
+
const currentVer = (status && status.currentVersion) || '0.4.23'
|
|
1415
|
+
const latestVer = status && status.latestVersion
|
|
1416
|
+
const updateAvailable = Boolean(status && status.updateAvailable && latestVer && latestVer !== currentVer)
|
|
1417
|
+
|
|
1418
|
+
return React.createElement(
|
|
1419
|
+
'div',
|
|
1420
|
+
{ className: 'dts-block', style: { gap: '10px' } },
|
|
1421
|
+
React.createElement(
|
|
1422
|
+
'div',
|
|
1423
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px' } },
|
|
1424
|
+
React.createElement(
|
|
1425
|
+
'div',
|
|
1426
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1427
|
+
React.createElement('span', { className: 'dts-title' }, '🔄 ' + t('updaterTitle')),
|
|
1428
|
+
React.createElement('span', { className: 'dts-badge ' + (updateAvailable ? 'dts-badge-warn' : 'dts-badge-on') },
|
|
1429
|
+
updateAvailable ? 'v' + latestVer + ' available' : 'v' + currentVer + ' (' + t('updaterUpToDate') + ')'
|
|
1430
|
+
)
|
|
1431
|
+
),
|
|
1432
|
+
React.createElement(
|
|
1433
|
+
'div',
|
|
1434
|
+
{ style: { display: 'flex', gap: '8px' } },
|
|
1435
|
+
updateAvailable
|
|
1436
|
+
? React.createElement(
|
|
1437
|
+
'button',
|
|
1438
|
+
{
|
|
1439
|
+
type: 'button',
|
|
1440
|
+
className: 'dts-btn',
|
|
1441
|
+
style: { borderColor: 'var(--dsw-alias-state-warning-primary)', color: 'var(--dsw-alias-state-warning-primary)' },
|
|
1442
|
+
onClick: onUpdateNow,
|
|
1443
|
+
disabled: updating || loading,
|
|
1444
|
+
},
|
|
1445
|
+
updating ? (t('updaterUpdating') || 'Updating…') : (t('updaterUpdateNow') || 'Update to v{version}').replace('{version}', latestVer)
|
|
1446
|
+
)
|
|
1447
|
+
: null,
|
|
1448
|
+
React.createElement(
|
|
1449
|
+
'button',
|
|
1450
|
+
{
|
|
1451
|
+
type: 'button',
|
|
1452
|
+
className: 'dts-btn',
|
|
1453
|
+
onClick: checkUpdate,
|
|
1454
|
+
disabled: loading || updating,
|
|
1455
|
+
},
|
|
1456
|
+
loading ? (t('updaterChecking') || 'Checking…') : (t('updaterCheck') || 'Check updates')
|
|
1457
|
+
)
|
|
1458
|
+
)
|
|
1459
|
+
),
|
|
1460
|
+
msg
|
|
1461
|
+
? React.createElement('div', { className: msg.ok ? 'dts-alert dts-alert-ok' : 'dts-alert dts-alert-err' }, msg.text)
|
|
1462
|
+
: null
|
|
1463
|
+
)
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1342
1466
|
// Synthesis stats panel: live host counters.
|
|
1343
1467
|
function StatsPanel(props) {
|
|
1344
1468
|
const t = props.t || ((key) => key)
|
|
@@ -1668,6 +1792,7 @@ async function pollPending() {
|
|
|
1668
1792
|
React.createElement('div', { className: 'dts-h' }, t('statsTitle')),
|
|
1669
1793
|
React.createElement(StatsPanel, { t: t }),
|
|
1670
1794
|
),
|
|
1795
|
+
React.createElement(PluginUpdaterBlock, { t: t }),
|
|
1671
1796
|
React.createElement('div', { className: 'dts-block' },
|
|
1672
1797
|
React.createElement('div', { className: 'dts-h' }, t('chainTitle')),
|
|
1673
1798
|
React.createElement('div', { className: 'dts-sub' },
|
package/lib/engines/manager.js
CHANGED
|
@@ -36,6 +36,7 @@ export const MODEL_SPECS = {
|
|
|
36
36
|
export function createModelManager({ root = process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), fetchImpl = fetch } = {}) {
|
|
37
37
|
const baseDir = path.join(String(root), 'data', 'dsh-tts', 'models')
|
|
38
38
|
const activeDownloads = new Map()
|
|
39
|
+
const lastErrors = new Map()
|
|
39
40
|
|
|
40
41
|
function getModelPath(engineId) {
|
|
41
42
|
return path.join(baseDir, engineId)
|
|
@@ -76,6 +77,8 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
76
77
|
const dir = getModelPath(engineId)
|
|
77
78
|
const active = activeDownloads.get(engineId)
|
|
78
79
|
|
|
80
|
+
const lastErr = lastErrors.get(engineId)
|
|
81
|
+
|
|
79
82
|
if (active) {
|
|
80
83
|
return {
|
|
81
84
|
id: engineId,
|
|
@@ -84,10 +87,11 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
84
87
|
requiresGpu: spec.requiresGpu,
|
|
85
88
|
installed: false,
|
|
86
89
|
downloading: true,
|
|
90
|
+
failed: false,
|
|
91
|
+
error: null,
|
|
87
92
|
progress: active.progress || 0,
|
|
88
93
|
downloadedBytes: active.downloadedBytes || 0,
|
|
89
94
|
totalBytes: active.totalBytes || 0,
|
|
90
|
-
path: dir,
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -109,9 +113,10 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
109
113
|
requiresGpu: spec.requiresGpu,
|
|
110
114
|
installed: allFilesExist,
|
|
111
115
|
downloading: false,
|
|
116
|
+
failed: !allFilesExist && Boolean(lastErr),
|
|
117
|
+
error: !allFilesExist && lastErr ? lastErr.message : null,
|
|
112
118
|
progress: allFilesExist ? 100 : 0,
|
|
113
119
|
sizeBytes,
|
|
114
|
-
path: dir,
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
|
@@ -130,6 +135,7 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
130
135
|
if (activeDownloads.has(engineId)) {
|
|
131
136
|
throw new Error(`Download already in progress for ${engineId}`)
|
|
132
137
|
}
|
|
138
|
+
lastErrors.delete(engineId)
|
|
133
139
|
|
|
134
140
|
const customFetch = options.fetchImpl || fetchImpl
|
|
135
141
|
const dir = getModelPath(engineId)
|
|
@@ -208,6 +214,7 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
208
214
|
|
|
209
215
|
downloadState.progress = 100
|
|
210
216
|
activeDownloads.delete(engineId)
|
|
217
|
+
lastErrors.delete(engineId)
|
|
211
218
|
return await getStatus(engineId)
|
|
212
219
|
} catch (err) {
|
|
213
220
|
// Clean up tmp files
|
|
@@ -215,6 +222,9 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
215
222
|
const tmpFile = path.join(dir, `${f.name}.tmp`)
|
|
216
223
|
await fs.promises.rm(tmpFile, { force: true }).catch(() => {})
|
|
217
224
|
}
|
|
225
|
+
const rawMsg = String(err?.message || err || 'Installation failed')
|
|
226
|
+
const sanitized = rawMsg.replace(/https?:\/\/[^\s]+/g, '[url]')
|
|
227
|
+
lastErrors.set(engineId, { message: sanitized, failedAt: Date.now() })
|
|
218
228
|
throw err
|
|
219
229
|
} finally {
|
|
220
230
|
activeDownloads.delete(engineId)
|
|
@@ -226,12 +236,14 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
226
236
|
if (active && active.controller) {
|
|
227
237
|
active.controller.abort()
|
|
228
238
|
activeDownloads.delete(engineId)
|
|
239
|
+
lastErrors.delete(engineId)
|
|
229
240
|
return true
|
|
230
241
|
}
|
|
231
242
|
return false
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
async function deleteModel(engineId) {
|
|
246
|
+
lastErrors.delete(engineId)
|
|
235
247
|
const spec = MODEL_SPECS[engineId]
|
|
236
248
|
if (!spec) throw new Error(`Unknown engine: ${engineId}`)
|
|
237
249
|
await cancelDownload(engineId)
|
package/lib/http-util.js
CHANGED
|
@@ -21,36 +21,91 @@ export function readBody(req, maxBytes) {
|
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function isLoopback(address) {
|
|
25
|
-
if (!address) return false
|
|
26
|
-
return
|
|
24
|
+
export function isLoopback(address) {
|
|
25
|
+
if (!address || typeof address !== 'string') return false
|
|
26
|
+
return (
|
|
27
|
+
address === '127.0.0.1' ||
|
|
28
|
+
address === '::1' ||
|
|
29
|
+
address === '::ffff:127.0.0.1' ||
|
|
30
|
+
address.startsWith('127.') ||
|
|
31
|
+
address === 'localhost'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseCookies(cookieHeader) {
|
|
36
|
+
if (!cookieHeader || typeof cookieHeader !== 'string') return {}
|
|
37
|
+
const out = {}
|
|
38
|
+
for (const part of cookieHeader.split(';')) {
|
|
39
|
+
const idx = part.indexOf('=')
|
|
40
|
+
if (idx > 0) {
|
|
41
|
+
const k = part.slice(0, idx).trim()
|
|
42
|
+
const v = part.slice(idx + 1).trim()
|
|
43
|
+
out[k] = v
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
/**
|
|
30
50
|
* Strict fail-closed check for administrative/settings requests.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
51
|
+
* Only accepts loopback with matching origin/host, same-origin context,
|
|
52
|
+
* or valid DSH auth tokens. Arbitrary headers, unverified tokens, and same-site requests are rejected.
|
|
33
53
|
*/
|
|
34
54
|
export function isTrustedSettingsRequest(request) {
|
|
35
55
|
if (!request || !request.headers) return false
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
// Fail-closed: unconditionally reject cross-site and non-same-origin Sec-Fetch-Site (e.g. same-site, cross-site)
|
|
58
|
+
const secFetch = request.headers['sec-fetch-site']
|
|
59
|
+
if (secFetch && secFetch !== 'same-origin') {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
39
62
|
|
|
63
|
+
// If origin is present, ensure it matches request host
|
|
64
|
+
const origin = request.headers['origin']
|
|
65
|
+
const host = request.headers['host']
|
|
66
|
+
if (origin && host) {
|
|
67
|
+
try {
|
|
68
|
+
const originHost = new URL(origin).host
|
|
69
|
+
if (originHost !== host) return false
|
|
70
|
+
} catch {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Verify explicit authorization token against DSH environment tokens if defined
|
|
76
|
+
const expectedToken = process.env.DSH_AUTH_TOKEN || process.env.DSH_TOKEN
|
|
77
|
+
const rawAuth = request.headers['authorization'] || request.headers['x-dsh-auth']
|
|
78
|
+
const cookieHeader = request.headers['cookie']
|
|
79
|
+
|
|
80
|
+
if (expectedToken) {
|
|
81
|
+
if (typeof rawAuth === 'string') {
|
|
82
|
+
const token = rawAuth.startsWith('Bearer ') ? rawAuth.slice(7).trim() : rawAuth.trim()
|
|
83
|
+
if (token && token === expectedToken) return true
|
|
84
|
+
}
|
|
85
|
+
if (typeof cookieHeader === 'string') {
|
|
86
|
+
const cookies = parseCookies(cookieHeader)
|
|
87
|
+
const token = cookies['token'] || cookies['dsh_token']
|
|
88
|
+
if (token && token === expectedToken) return true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Remote IP loopback check
|
|
40
93
|
const remoteAddr = request.socket?.remoteAddress || request.connection?.remoteAddress
|
|
41
|
-
if (remoteAddr
|
|
94
|
+
if (remoteAddr) {
|
|
95
|
+
return isLoopback(remoteAddr)
|
|
96
|
+
}
|
|
42
97
|
|
|
43
|
-
|
|
44
|
-
if (
|
|
98
|
+
// Internal/mock caller without socket: only allow if sec-fetch-site is explicitly same-origin or origin matches host
|
|
99
|
+
if (secFetch === 'same-origin') {
|
|
45
100
|
return true
|
|
46
101
|
}
|
|
47
|
-
|
|
48
|
-
const host = request.headers['host']
|
|
102
|
+
|
|
49
103
|
if (origin && host) {
|
|
50
104
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
105
|
+
return new URL(origin).host === host
|
|
106
|
+
} catch {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
54
109
|
}
|
|
55
110
|
|
|
56
111
|
return false
|
package/lib/routes.js
CHANGED
|
@@ -80,6 +80,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
80
80
|
path: '/dsh-tts/models/status',
|
|
81
81
|
handler: async (req, res) => {
|
|
82
82
|
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
83
|
+
if (!isTrustedSettingsRequest(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } }); return }
|
|
83
84
|
try {
|
|
84
85
|
const list = await modelManager.listStatus()
|
|
85
86
|
writeJson(res, 200, { ok: true, models: list })
|
|
@@ -428,4 +429,10 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
428
429
|
packageName: '@goodandready/dsh-tts',
|
|
429
430
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
430
431
|
})
|
|
432
|
+
|
|
433
|
+
registerPluginUpdater(ctx, {
|
|
434
|
+
endpoint: '/api/dsh-tts/update',
|
|
435
|
+
packageName: '@goodandready/dsh-tts',
|
|
436
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
437
|
+
})
|
|
431
438
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.24",
|
|
4
4
|
"description": "Text-to-speech for DeepSeek Harness: speak agent replies in the Web UI with a provider fallback chain (OpenAI, ElevenLabs, Google, Azure, Groq, Deepgram, OpenRouter, Edge, Piper, eSpeak).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"lib/",
|
|
16
|
-
"
|
|
16
|
+
"!lib/client-src/**",
|
|
17
|
+
"!lib/client-src",
|
|
17
18
|
"cordis.patch.yml",
|
|
18
19
|
"CHANGELOG.md",
|
|
19
20
|
"README.md",
|
|
@@ -62,10 +63,10 @@
|
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
64
65
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
65
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.
|
|
66
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
69
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
70
71
|
},
|
|
71
72
|
"publishConfig": {
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({
|
|
2
|
-
id: '@goodandready/dsh-tts',
|
|
3
|
-
factory: (require) => {
|
|
4
|
-
var module = { exports: {} }
|
|
5
|
-
var exports = module.exports
|
|
6
|
-
const React = require('react')
|
|
7
|
-
|
|
8
|
-
// Chevron comes from core UI primitives; local SVG is a fallback for
|
|
9
|
-
// builds without the package. Icon always points down; open state rotates.
|
|
10
|
-
let ChevronIcon = null
|
|
11
|
-
try {
|
|
12
|
-
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
13
|
-
ChevronIcon = primitives && primitives.IconChevronDownOutline14
|
|
14
|
-
} catch (noPrimitives) {
|
|
15
|
-
ChevronIcon = null
|
|
16
|
-
}
|
|
17
|
-
const NS = 'dsh-tts'
|
|
18
|
-
// Plugins page row seat: key = '<package name>#<row id>', row id as cordis.patch.yml declares it.
|
|
19
|
-
const PKG = '@goodandready/dsh-tts'
|
|
20
|
-
const ROW_ID = 'dsh-tts'
|
|
21
|
-
const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
|
|
22
|
-
const PROVIDERS = [
|
|
23
|
-
'openai', 'elevenlabs', 'google', 'azure', 'groq', 'deepgram', 'openrouter',
|
|
24
|
-
'edge', 'piper', 'espeak',
|
|
25
|
-
]
|
|
26
|
-
const CLOUD = {
|
|
27
|
-
openai: 1, elevenlabs: 1, google: 1, azure: 1, groq: 1, deepgram: 1, openrouter: 1, custom: 1,
|
|
28
|
-
}
|
|
29
|
-
// Hints stay generic: concrete provider model IDs rot quickly.
|
|
30
|
-
const MODEL_HINT = {
|
|
31
|
-
openai: 'provider default',
|
|
32
|
-
elevenlabs: 'provider default',
|
|
33
|
-
google: 'provider default',
|
|
34
|
-
azure: 'region voice name',
|
|
35
|
-
groq: 'provider default',
|
|
36
|
-
deepgram: 'provider default (English-only voices)',
|
|
37
|
-
openrouter: 'provider/model id',
|
|
38
|
-
edge: 'BCP-47 voice, e.g. ru-RU-SvetlanaNeural',
|
|
39
|
-
piper: 'path to .onnx',
|
|
40
|
-
espeak: 'language code, e.g. ru',
|
|
41
|
-
kokoro: 'not bundled — offline neural runtime unavailable',
|
|
42
|
-
f5: 'not bundled — offline neural runtime unavailable',
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
function createErrorBoundary() {
|
|
47
|
-
if (!React || typeof React.Component !== 'function') {
|
|
48
|
-
return function NoopBoundary(props) { return (props && props.children) || null }
|
|
49
|
-
}
|
|
50
|
-
return class ErrorBoundary extends React.Component {
|
|
51
|
-
constructor(props) {
|
|
52
|
-
super(props)
|
|
53
|
-
this.state = { hasError: false, error: null }
|
|
54
|
-
}
|
|
55
|
-
static getDerivedStateFromError(error) {
|
|
56
|
-
return { hasError: true, error }
|
|
57
|
-
}
|
|
58
|
-
componentDidCatch(error, errorInfo) {
|
|
59
|
-
console.error('[dsh-tts] React Error:', error, errorInfo)
|
|
60
|
-
}
|
|
61
|
-
render() {
|
|
62
|
-
if (this.state.hasError) {
|
|
63
|
-
return React.createElement(
|
|
64
|
-
'div',
|
|
65
|
-
{
|
|
66
|
-
className: 'dts-alert dts-alert-err',
|
|
67
|
-
style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
|
|
68
|
-
},
|
|
69
|
-
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ TTS UI Error:'),
|
|
70
|
-
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String((this.state.error && this.state.error.message) || this.state.error)),
|
|
71
|
-
React.createElement(
|
|
72
|
-
'button',
|
|
73
|
-
{
|
|
74
|
-
type: 'button',
|
|
75
|
-
className: 'dts-btn',
|
|
76
|
-
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
77
|
-
onClick: () => this.setState({ hasError: false, error: null }),
|
|
78
|
-
},
|
|
79
|
-
'Retry',
|
|
80
|
-
),
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
return (this.props && this.props.children) || null
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const ErrorBoundary = createErrorBoundary()
|
|
88
|
-
const SNAPSHOT_READY = Object.freeze({ status: 'ready', value: {} })
|
|
89
|
-
const SNAPSHOT_LOADING = Object.freeze({ status: 'loading', value: {} })
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
const SET_CSS =
|
|
3
|
-
'.dts-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:900px}' +
|
|
4
|
-
'.dts-header{display:flex;flex-direction:column;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
|
|
5
|
-
'.dts-page{padding:0}' +
|
|
6
|
-
'.dts-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}' +
|
|
7
|
-
'.dts-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}' +
|
|
8
|
-
'.dts-block{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}' +
|
|
9
|
-
'.dts-h{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
10
|
-
'.dts-sub{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
|
|
11
|
-
'.dts-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
12
|
-
'.dts-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
13
|
-
'.dts-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
14
|
-
'.dts-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
15
|
-
'.dts-chev{margin-left:auto;color:var(--dsw-alias-label-secondary);flex:none;transition:transform .16s}' +
|
|
16
|
-
'.dts-chevOpen{transform:rotate(180deg)}' +
|
|
17
|
-
'.dts-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0}' +
|
|
18
|
-
'.dts-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
19
|
-
'.dts-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}' +
|
|
20
|
-
'.dts-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
21
|
-
'.dts-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:14px 0 4px}' +
|
|
22
|
-
'.dts-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:6px 16px;font-size:13px;font-weight:500;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);transition:all .15s ease}' +
|
|
23
|
-
'.dts-save:hover:not(:disabled){opacity:0.88}' +
|
|
24
|
-
'.dts-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}' +
|
|
25
|
-
'.dts-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))}' +
|
|
26
|
-
'.dts-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}' +
|
|
27
|
-
'.dts-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}' +
|
|
28
|
-
'.dts-entry{display:flex;flex-direction:column;gap:8px;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}' +
|
|
29
|
-
'.dts-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
30
|
-
'.dts-row .dts-model{flex:1;min-width:120px}' +
|
|
31
|
-
'.dts-row .dts-key{flex:1;min-width:180px}' +
|
|
32
|
-
'.dts-mini{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);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;font-size:13px;transition:all .15s ease}' +
|
|
33
|
-
'.dts-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2))}' +
|
|
34
|
-
'.dts-ok{font-size:12px;color:var(--dsw-alias-state-success-primary);font-weight:500}' +
|
|
35
|
-
'.dts-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);font-weight:500}' +
|
|
36
|
-
'.dts-badge{font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
37
|
-
'.dts-badge-on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}' +
|
|
38
|
-
'.dts-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}' +
|
|
39
|
-
'.dts-badge-bad{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}' +
|
|
40
|
-
'.dts-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0;text-decoration:underline}' +
|
|
41
|
-
'.dts-link:hover:not(:disabled){color:var(--dsw-alias-label-primary)}' +
|
|
42
|
-
'.dts-alert{padding:10px 14px;border-radius:8px;font-size:13px}' +
|
|
43
|
-
'.dts-alert-err{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 20%, transparent)}' +
|
|
44
|
-
'.dts-alert-warn{background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 10%, transparent);color:var(--dsw-alias-state-warning-primary);border:1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary) 20%, transparent)}' +
|
|
45
|
-
'.dts-alert-ok{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);color:var(--dsw-alias-state-success-primary);border:1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 20%, transparent)}' +
|
|
46
|
-
'.dts-grow{flex:1;min-width:100px}'
|
|
47
|
-
|
|
48
|
-
const setCssId = 'dsh-tts/settings.module.css'
|
|
49
|
-
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-tts"]')) {
|
|
50
|
-
const tag = document.createElement('style')
|
|
51
|
-
tag.textContent = SET_CSS
|
|
52
|
-
tag.setAttribute('data-dsh-plugin', 'dsh-tts')
|
|
53
|
-
tag.dataset.pluginCss = setCssId
|
|
54
|
-
document.head.appendChild(tag)
|
|
55
|
-
}
|