@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 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' },
@@ -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 address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1' || address === 'localhost'
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
- * Accepts loopback, same-origin/same-site sec-fetch-site, matching origin/host, or authorization header.
32
- * Rejects cross-site or unauthenticated requests missing origin context.
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
- const authHeader = request.headers['authorization'] || request.headers['x-dsh-auth']
38
- if (authHeader && authHeader.length > 5) return true
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 && isLoopback(remoteAddr)) return true
94
+ if (remoteAddr) {
95
+ return isLoopback(remoteAddr)
96
+ }
42
97
 
43
- const secFetchSite = request.headers['sec-fetch-site']
44
- if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') {
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
- const origin = request.headers['origin']
48
- const host = request.headers['host']
102
+
49
103
  if (origin && host) {
50
104
  try {
51
- const originHost = new URL(origin).host
52
- if (originHost === host) return true
53
- } catch { /* invalid origin URL */ }
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.22",
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
- "scripts/",
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.6",
66
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
67
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
68
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
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
- }