@goodandready/dsh-tts 0.4.10 → 0.4.16
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 +0 -1
- package/README.ru.md +0 -1
- package/README.zh.md +0 -1
- package/lib/client-src/20-player.js +6 -6
- package/lib/client-src/40-editors.js +2 -28
- package/lib/client-src/60-card-dock.js +5 -27
- package/lib/client.js +13 -61
- package/lib/http-util.js +32 -2
- package/lib/routes.js +8 -1
- package/lib/text.js +7 -40
- package/lib/updater.js +333 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -214,7 +214,6 @@ Provider **API keys are never stored in plugin settings**. Paste them in the cha
|
|
|
214
214
|
|
|
215
215
|
Config-only (not in the card): `*KeyEnv` fields (`openaiKeyEnv`, `elevenlabsKeyEnv`, …). They only rename the credential slot the plugin looks up. Change them in `settings.yaml` if you must rebind a key name; the defaults match the usual environment variable names.
|
|
216
216
|
|
|
217
|
-
The sidebar `settings.section` entry is a **fallback** and is registered only when `settings.plugin.item` is missing from the build.
|
|
218
217
|
|
|
219
218
|
## 🤖 HTTP Endpoints Reference
|
|
220
219
|
|
package/README.ru.md
CHANGED
|
@@ -197,7 +197,6 @@ dsh-tts:
|
|
|
197
197
|
|
|
198
198
|
Только в конфиге (нет в карточке): поля `*KeyEnv` (`openaiKeyEnv`, `elevenlabsKeyEnv`, …). Они задают **имя** credential-слота; по умолчанию совпадают с привычными именами env-переменных. Меняйте в `settings.yaml`, если нужно перевязать имя ключа.
|
|
199
199
|
|
|
200
|
-
Пункт `settings.section` в боковом меню — **fallback** и регистрируется, только если в сборке нет `settings.plugin.item`.
|
|
201
200
|
|
|
202
201
|
## 🤖 HTTP роуты API
|
|
203
202
|
|
package/README.zh.md
CHANGED
|
@@ -256,7 +256,7 @@
|
|
|
256
256
|
function connectSseStream() {
|
|
257
257
|
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return
|
|
258
258
|
if (sseSource) {
|
|
259
|
-
try { sseSource.close() } catch {}
|
|
259
|
+
try { sseSource.close() } catch (alreadyClosed) { /* intentional cleanup */ }
|
|
260
260
|
sseSource = null
|
|
261
261
|
}
|
|
262
262
|
try {
|
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
drainQueue()
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
|
-
} catch {}
|
|
284
|
+
} catch (parseErr) { /* ignore malformed SSE payload */ }
|
|
285
285
|
})
|
|
286
286
|
sseSource.addEventListener('chime', (e) => {
|
|
287
287
|
sseAttempt = 0
|
|
@@ -291,11 +291,11 @@
|
|
|
291
291
|
player.queue.push(item)
|
|
292
292
|
drainQueue()
|
|
293
293
|
}
|
|
294
|
-
} catch {}
|
|
294
|
+
} catch (parseErr) { /* ignore malformed chime payload */ }
|
|
295
295
|
})
|
|
296
296
|
sseSource.onerror = () => {
|
|
297
297
|
if (sseSource) {
|
|
298
|
-
try { sseSource.close() } catch {}
|
|
298
|
+
try { sseSource.close() } catch (alreadyClosed) { /* intentional cleanup */ }
|
|
299
299
|
sseSource = null
|
|
300
300
|
}
|
|
301
301
|
sseAttempt++
|
|
@@ -307,7 +307,7 @@
|
|
|
307
307
|
connectSseStream()
|
|
308
308
|
}, delay)
|
|
309
309
|
}
|
|
310
|
-
} catch {}
|
|
310
|
+
} catch (esErr) { /* EventSource failed */ }
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
async function pollPending() {
|
|
@@ -413,7 +413,7 @@ async function pollPending() {
|
|
|
413
413
|
document.body.appendChild(a)
|
|
414
414
|
a.click()
|
|
415
415
|
document.body.removeChild(a)
|
|
416
|
-
setTimeout(() => { try { URL.revokeObjectURL(url) } catch {} }, 2000)
|
|
416
|
+
setTimeout(() => { try { URL.revokeObjectURL(url) } catch (alreadyRevoked) { /* already revoked */ } }, 2000)
|
|
417
417
|
})
|
|
418
418
|
.catch(() => {})
|
|
419
419
|
}
|
|
@@ -132,33 +132,7 @@
|
|
|
132
132
|
)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
const CLIENT_IT_TERMS = [
|
|
136
|
-
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
137
|
-
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
138
|
-
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
139
|
-
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
140
|
-
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
141
|
-
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
142
|
-
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
143
|
-
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
144
|
-
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
145
|
-
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
146
|
-
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
147
|
-
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
148
|
-
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
149
|
-
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
150
|
-
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
151
|
-
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
152
|
-
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
153
|
-
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
154
|
-
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
155
|
-
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
156
|
-
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
157
|
-
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
158
|
-
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
159
|
-
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
160
|
-
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
161
|
-
]
|
|
135
|
+
const CLIENT_IT_TERMS = []
|
|
162
136
|
|
|
163
137
|
// Pronunciation dictionary editor: rules apply top-down.
|
|
164
138
|
function PronEditor(props) {
|
|
@@ -187,7 +161,7 @@
|
|
|
187
161
|
document.body.appendChild(a)
|
|
188
162
|
a.click()
|
|
189
163
|
document.body.removeChild(a)
|
|
190
|
-
setTimeout(() => { try { URL.revokeObjectURL(url) } catch {} }, 1000)
|
|
164
|
+
setTimeout(() => { try { URL.revokeObjectURL(url) } catch (alreadyRevoked) { /* already revoked */ } }, 1000)
|
|
191
165
|
}
|
|
192
166
|
|
|
193
167
|
const triggerImport = () => {
|
|
@@ -140,36 +140,14 @@
|
|
|
140
140
|
// Primary settings home is the plugin-settings tab card. The tab looks up
|
|
141
141
|
// a slot by entryKey equal to the namespace name: key must equal NS or
|
|
142
142
|
// the card never appears, with no log error.
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const res = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
146
|
-
{
|
|
147
|
-
name: 'settings.plugin.item',
|
|
148
|
-
key: NS,
|
|
149
|
-
locale: NS,
|
|
150
|
-
inject: () => ({ ctx: ctx }),
|
|
151
|
-
},
|
|
152
|
-
TtsCard,
|
|
153
|
-
))
|
|
154
|
-
if (res === false) return false
|
|
155
|
-
return true
|
|
156
|
-
} catch (noPluginItemSlot) {
|
|
157
|
-
return false
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (tryPluginItem()) return
|
|
161
|
-
// Fallback for builds without settings.plugin.item: keep the sidebar section
|
|
162
|
-
// so settings cannot disappear.
|
|
163
|
-
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
143
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
164
144
|
{
|
|
165
|
-
name: 'settings.
|
|
166
|
-
|
|
167
|
-
order: 32,
|
|
145
|
+
name: 'settings.plugin.item',
|
|
146
|
+
key: NS,
|
|
168
147
|
locale: NS,
|
|
169
|
-
label: () => t('title'),
|
|
170
148
|
inject: () => ({ ctx: ctx }),
|
|
171
149
|
},
|
|
172
|
-
|
|
150
|
+
TtsCard,
|
|
173
151
|
))
|
|
174
152
|
}
|
|
175
153
|
|
|
@@ -182,7 +160,7 @@
|
|
|
182
160
|
if (typeof window === 'undefined') return () => {}
|
|
183
161
|
const onKey = (e) => {
|
|
184
162
|
if (e.ctrlKey && e.key === 'Escape') { togglePause(); e.preventDefault() }
|
|
185
|
-
else if (e.altKey && (e.
|
|
163
|
+
else if (e.altKey && (e.code === 'KeyS' || e.key === 's' || e.key === 'S')) { stopPlayback(); e.preventDefault() }
|
|
186
164
|
else if (e.altKey && e.key === 'ArrowRight') { skipCurrent(); e.preventDefault() }
|
|
187
165
|
}
|
|
188
166
|
window.addEventListener('keydown', onKey)
|
package/lib/client.js
CHANGED
|
@@ -395,7 +395,7 @@ window.__ModuleLoader__.load({
|
|
|
395
395
|
function connectSseStream() {
|
|
396
396
|
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return
|
|
397
397
|
if (sseSource) {
|
|
398
|
-
try { sseSource.close() } catch {}
|
|
398
|
+
try { sseSource.close() } catch (alreadyClosed) { /* intentional cleanup */ }
|
|
399
399
|
sseSource = null
|
|
400
400
|
}
|
|
401
401
|
try {
|
|
@@ -420,7 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
420
420
|
drainQueue()
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
|
-
} catch {}
|
|
423
|
+
} catch (parseErr) { /* ignore malformed SSE payload */ }
|
|
424
424
|
})
|
|
425
425
|
sseSource.addEventListener('chime', (e) => {
|
|
426
426
|
sseAttempt = 0
|
|
@@ -430,11 +430,11 @@ window.__ModuleLoader__.load({
|
|
|
430
430
|
player.queue.push(item)
|
|
431
431
|
drainQueue()
|
|
432
432
|
}
|
|
433
|
-
} catch {}
|
|
433
|
+
} catch (parseErr) { /* ignore malformed chime payload */ }
|
|
434
434
|
})
|
|
435
435
|
sseSource.onerror = () => {
|
|
436
436
|
if (sseSource) {
|
|
437
|
-
try { sseSource.close() } catch {}
|
|
437
|
+
try { sseSource.close() } catch (alreadyClosed) { /* intentional cleanup */ }
|
|
438
438
|
sseSource = null
|
|
439
439
|
}
|
|
440
440
|
sseAttempt++
|
|
@@ -446,7 +446,7 @@ window.__ModuleLoader__.load({
|
|
|
446
446
|
connectSseStream()
|
|
447
447
|
}, delay)
|
|
448
448
|
}
|
|
449
|
-
} catch {}
|
|
449
|
+
} catch (esErr) { /* EventSource failed */ }
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
async function pollPending() {
|
|
@@ -552,7 +552,7 @@ async function pollPending() {
|
|
|
552
552
|
document.body.appendChild(a)
|
|
553
553
|
a.click()
|
|
554
554
|
document.body.removeChild(a)
|
|
555
|
-
setTimeout(() => { try { URL.revokeObjectURL(url) } catch {} }, 2000)
|
|
555
|
+
setTimeout(() => { try { URL.revokeObjectURL(url) } catch (alreadyRevoked) { /* already revoked */ } }, 2000)
|
|
556
556
|
})
|
|
557
557
|
.catch(() => {})
|
|
558
558
|
}
|
|
@@ -1069,33 +1069,7 @@ async function pollPending() {
|
|
|
1069
1069
|
)
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
|
-
const CLIENT_IT_TERMS = [
|
|
1073
|
-
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
1074
|
-
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
1075
|
-
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1076
|
-
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1077
|
-
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
1078
|
-
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
1079
|
-
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
1080
|
-
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
1081
|
-
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
1082
|
-
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
1083
|
-
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
1084
|
-
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
1085
|
-
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
1086
|
-
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
1087
|
-
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
1088
|
-
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
1089
|
-
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
1090
|
-
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
1091
|
-
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
1092
|
-
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
1093
|
-
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
1094
|
-
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
1095
|
-
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
1096
|
-
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
1097
|
-
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
1098
|
-
]
|
|
1072
|
+
const CLIENT_IT_TERMS = []
|
|
1099
1073
|
|
|
1100
1074
|
// Pronunciation dictionary editor: rules apply top-down.
|
|
1101
1075
|
function PronEditor(props) {
|
|
@@ -1124,7 +1098,7 @@ async function pollPending() {
|
|
|
1124
1098
|
document.body.appendChild(a)
|
|
1125
1099
|
a.click()
|
|
1126
1100
|
document.body.removeChild(a)
|
|
1127
|
-
setTimeout(() => { try { URL.revokeObjectURL(url) } catch {} }, 1000)
|
|
1101
|
+
setTimeout(() => { try { URL.revokeObjectURL(url) } catch (alreadyRevoked) { /* already revoked */ } }, 1000)
|
|
1128
1102
|
}
|
|
1129
1103
|
|
|
1130
1104
|
const triggerImport = () => {
|
|
@@ -1924,36 +1898,14 @@ async function pollPending() {
|
|
|
1924
1898
|
// Primary settings home is the plugin-settings tab card. The tab looks up
|
|
1925
1899
|
// a slot by entryKey equal to the namespace name: key must equal NS or
|
|
1926
1900
|
// the card never appears, with no log error.
|
|
1927
|
-
|
|
1928
|
-
try {
|
|
1929
|
-
const res = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
1930
|
-
{
|
|
1931
|
-
name: 'settings.plugin.item',
|
|
1932
|
-
key: NS,
|
|
1933
|
-
locale: NS,
|
|
1934
|
-
inject: () => ({ ctx: ctx }),
|
|
1935
|
-
},
|
|
1936
|
-
TtsCard,
|
|
1937
|
-
))
|
|
1938
|
-
if (res === false) return false
|
|
1939
|
-
return true
|
|
1940
|
-
} catch (noPluginItemSlot) {
|
|
1941
|
-
return false
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
if (tryPluginItem()) return
|
|
1945
|
-
// Fallback for builds without settings.plugin.item: keep the sidebar section
|
|
1946
|
-
// so settings cannot disappear.
|
|
1947
|
-
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1901
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
1948
1902
|
{
|
|
1949
|
-
name: 'settings.
|
|
1950
|
-
|
|
1951
|
-
order: 32,
|
|
1903
|
+
name: 'settings.plugin.item',
|
|
1904
|
+
key: NS,
|
|
1952
1905
|
locale: NS,
|
|
1953
|
-
label: () => t('title'),
|
|
1954
1906
|
inject: () => ({ ctx: ctx }),
|
|
1955
1907
|
},
|
|
1956
|
-
|
|
1908
|
+
TtsCard,
|
|
1957
1909
|
))
|
|
1958
1910
|
}
|
|
1959
1911
|
|
|
@@ -1966,7 +1918,7 @@ async function pollPending() {
|
|
|
1966
1918
|
if (typeof window === 'undefined') return () => {}
|
|
1967
1919
|
const onKey = (e) => {
|
|
1968
1920
|
if (e.ctrlKey && e.key === 'Escape') { togglePause(); e.preventDefault() }
|
|
1969
|
-
else if (e.altKey && (e.
|
|
1921
|
+
else if (e.altKey && (e.code === 'KeyS' || e.key === 's' || e.key === 'S')) { stopPlayback(); e.preventDefault() }
|
|
1970
1922
|
else if (e.altKey && e.key === 'ArrowRight') { skipCurrent(); e.preventDefault() }
|
|
1971
1923
|
}
|
|
1972
1924
|
window.addEventListener('keydown', onKey)
|
package/lib/http-util.js
CHANGED
|
@@ -21,7 +21,37 @@ export function readBody(req, maxBytes) {
|
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
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'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 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.
|
|
33
|
+
*/
|
|
25
34
|
export function isTrustedSettingsRequest(request) {
|
|
26
|
-
|
|
35
|
+
if (!request || !request.headers) return false
|
|
36
|
+
|
|
37
|
+
const authHeader = request.headers['authorization'] || request.headers['x-dsh-auth']
|
|
38
|
+
if (authHeader && authHeader.length > 5) return true
|
|
39
|
+
|
|
40
|
+
const remoteAddr = request.socket?.remoteAddress || request.connection?.remoteAddress
|
|
41
|
+
if (remoteAddr && isLoopback(remoteAddr)) return true
|
|
42
|
+
|
|
43
|
+
const secFetchSite = request.headers['sec-fetch-site']
|
|
44
|
+
if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
const origin = request.headers['origin']
|
|
48
|
+
const host = request.headers['host']
|
|
49
|
+
if (origin && host) {
|
|
50
|
+
try {
|
|
51
|
+
const originHost = new URL(origin).host
|
|
52
|
+
if (originHost === host) return true
|
|
53
|
+
} catch { /* invalid origin URL */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return false
|
|
27
57
|
}
|
package/lib/routes.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { registerPluginUpdater } from './updater.js'
|
|
1
3
|
import { writeJson, readBody, isTrustedSettingsRequest } from './http-util.js'
|
|
2
4
|
import { addStreamSubscriber, removeStreamSubscriber, streamSubscriberCount } from './stream-hub.js'
|
|
3
5
|
import { pendingKeyWrites, stripSecretsFromConfig, needsApiKey } from './keys.js'
|
|
@@ -229,7 +231,6 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
229
231
|
const lang = String(cfg.language || '').toLowerCase()
|
|
230
232
|
let phrase = 'Voice check.'
|
|
231
233
|
if (lang.startsWith('zh')) phrase = '语音合成测试。'
|
|
232
|
-
else if (lang.startsWith('ru')) phrase = 'Проверка голоса.'
|
|
233
234
|
try {
|
|
234
235
|
const probe = Object.assign({}, cfg, {
|
|
235
236
|
chain: [{ provider: body.provider || '', model: body.model || '', voice: body.voice || '' }],
|
|
@@ -410,4 +411,10 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
410
411
|
}
|
|
411
412
|
},
|
|
412
413
|
}), 'dsh-tts: /speak route')
|
|
414
|
+
|
|
415
|
+
registerPluginUpdater(ctx, {
|
|
416
|
+
endpoint: '/dsh-tts/updater',
|
|
417
|
+
packageName: '@goodandready/dsh-tts',
|
|
418
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
419
|
+
})
|
|
413
420
|
}
|
package/lib/text.js
CHANGED
|
@@ -15,14 +15,12 @@ export function blocksToText(content) {
|
|
|
15
15
|
// the configured speech language; {n} is filled with a line/row count.
|
|
16
16
|
export const SPEECH_PHRASES = {
|
|
17
17
|
en: { codeBlock: 'code block, {n} lines', table: 'table, {n} rows', summaryIntro: 'Summary of the reply.' },
|
|
18
|
-
ru: { codeBlock: 'блок кода, {n} строк', table: 'таблица, {n} строк', summaryIntro: 'Пересказ ответа.' },
|
|
19
18
|
zh: { codeBlock: '代码块,共 {n} 行', table: '表格,共 {n} 行', summaryIntro: '回复摘要。' },
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export function speechPhrases(language) {
|
|
23
22
|
const lang = String(language || '').toLowerCase()
|
|
24
23
|
if (lang.startsWith('zh')) return SPEECH_PHRASES.zh
|
|
25
|
-
if (lang.startsWith('ru')) return SPEECH_PHRASES.ru
|
|
26
24
|
return SPEECH_PHRASES.en
|
|
27
25
|
}
|
|
28
26
|
|
|
@@ -43,12 +41,12 @@ export function applyNarrationFilters(text, cfg) {
|
|
|
43
41
|
return out
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
// Rough language heuristic
|
|
44
|
+
// Rough language heuristic using Unicode Script properties.
|
|
47
45
|
export function detectLang(text) {
|
|
48
46
|
const s = String(text || '')
|
|
49
|
-
const cjk = (s.match(/
|
|
50
|
-
const cyr = (s.match(/
|
|
51
|
-
const lat = (s.match(/
|
|
47
|
+
const cjk = (s.match(/\p{Script=Han}/gu) || []).length
|
|
48
|
+
const cyr = (s.match(/\p{Script=Cyrillic}/gu) || []).length
|
|
49
|
+
const lat = (s.match(/\p{Script=Latin}/gu) || []).length
|
|
52
50
|
if (cjk >= cyr && cjk >= lat && cjk > 0) return 'zh'
|
|
53
51
|
return cyr > lat ? 'ru' : 'en'
|
|
54
52
|
}
|
|
@@ -61,33 +59,7 @@ function escapeRe(s) {
|
|
|
61
59
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
export const BUILTIN_IT_DICTIONARY = [
|
|
65
|
-
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
66
|
-
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
67
|
-
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
68
|
-
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
69
|
-
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
70
|
-
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
71
|
-
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
72
|
-
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
73
|
-
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
74
|
-
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
75
|
-
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
76
|
-
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
77
|
-
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
78
|
-
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
79
|
-
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
80
|
-
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
81
|
-
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
82
|
-
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
83
|
-
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
84
|
-
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
85
|
-
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
86
|
-
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
87
|
-
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
88
|
-
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
89
|
-
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
90
|
-
]
|
|
62
|
+
export const BUILTIN_IT_DICTIONARY = []
|
|
91
63
|
|
|
92
64
|
export function applyPronunciation(text, rules, language, enableBuiltinIt = false) {
|
|
93
65
|
let out = String(text || '')
|
|
@@ -152,13 +124,8 @@ export function assistantText(message) {
|
|
|
152
124
|
return blocksToText(message.content)
|
|
153
125
|
}
|
|
154
126
|
|
|
155
|
-
// Protect common abbreviations from false sentence splits
|
|
127
|
+
// Protect common English abbreviations from false sentence splits
|
|
156
128
|
const PROTECTED_ABBREVIATIONS = [
|
|
157
|
-
// Russian abbreviations
|
|
158
|
-
'т.е.', 'т.д.', 'т.п.', 'т.к.', 'руб.', 'коп.', 'ул.', 'пер.', 'пр.', 'просп.',
|
|
159
|
-
'д.', 'корп.', 'стр.', 'кв.', 'г.', 'гг.', 'в.', 'вв.', 'см.', 'ср.', 'напр.',
|
|
160
|
-
'рис.', 'табл.', 'им.', 'акад.', 'проф.', 'доц.', 'млн.', 'млрд.', 'тыс.',
|
|
161
|
-
// English abbreviations
|
|
162
129
|
'e.g.', 'i.e.', 'vs.', 'etc.', 'dr.', 'mr.', 'mrs.', 'ms.', 'prof.',
|
|
163
130
|
'approx.', 'dept.', 'est.', 'min.', 'max.', 'no.', 'vol.', 'p.m.', 'a.m.'
|
|
164
131
|
]
|
|
@@ -173,7 +140,7 @@ export function protectAbbreviations(text) {
|
|
|
173
140
|
// Decimal numbers and versions: e.g. 3.14, v0.4.6, 192.168.0.1
|
|
174
141
|
res = res.replace(/(?<=[vV]?\d+)\.(?=\d+)/g, '\u2024')
|
|
175
142
|
// List numbering at line/word starts: 1. item, 2. item
|
|
176
|
-
res = res.replace(/(?<=^|[\s\n])(\d+)\.(?=\s+[A-Za-z
|
|
143
|
+
res = res.replace(/(?<=^|[\s\n])(\d+)\.(?=\s+[A-Za-z\u4E00-\u9FFF])/g, '$1\u2024')
|
|
177
144
|
// Common file extensions: e.g. package.json, index.js, script.py
|
|
178
145
|
res = res.replace(/(?<=[a-zA-Z0-9_-])\.(?:js|mjs|cjs|ts|tsx|jsx|json|ya?ml|md|py|sh|tgz|tar|gz|zip|html?|css|rs|go|c|cpp|h|txt)(?=[ \t\n"«'\)\]\},;\.\?!]|$)/gi, (m) => m.replace(/\./g, '\u2024'))
|
|
179
146
|
// Hostnames and domains: e.g. goodandready.app, github.com
|
package/lib/updater.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { basename, dirname, isAbsolute, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Host-side one-click updater for @goodandready/dsh-tts.
|
|
9
|
+
* Implements the unified DSH plugin updater specification.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const UPDATE_HEADER = 'x-dsh-plugin-update'
|
|
13
|
+
const UPDATE_TIMEOUT_MS = 10 * 60_000
|
|
14
|
+
const VERSION_CACHE_MS = 5 * 60_000
|
|
15
|
+
let latestCache = undefined
|
|
16
|
+
|
|
17
|
+
function header(request, name) {
|
|
18
|
+
const value = request?.headers?.[name]
|
|
19
|
+
return Array.isArray(value) ? value[0] : value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isLoopback(value) {
|
|
23
|
+
const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
|
|
24
|
+
return (
|
|
25
|
+
address === 'localhost' ||
|
|
26
|
+
address === 'localhost.' ||
|
|
27
|
+
address === '::1' ||
|
|
28
|
+
address?.startsWith('127.') === true ||
|
|
29
|
+
address?.startsWith('::ffff:127.') === true
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isPrivateLan(value) {
|
|
34
|
+
const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
|
|
35
|
+
if (!address) return false
|
|
36
|
+
const ipv4 = address.startsWith('::ffff:') ? address.slice(7) : address
|
|
37
|
+
return (
|
|
38
|
+
ipv4.startsWith('192.168.') ||
|
|
39
|
+
ipv4.startsWith('10.') ||
|
|
40
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ipv4)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isTrustedUpdateRequest(request) {
|
|
45
|
+
if (header(request, UPDATE_HEADER) !== '1') return false
|
|
46
|
+
const remote = request.socket?.remoteAddress
|
|
47
|
+
if (!isLoopback(remote) && !isPrivateLan(remote)) return false
|
|
48
|
+
const site = header(request, 'sec-fetch-site')
|
|
49
|
+
if (site !== undefined && site !== 'same-origin') return false
|
|
50
|
+
const origin = header(request, 'origin')
|
|
51
|
+
const host = header(request, 'host')
|
|
52
|
+
if (origin === undefined || host === undefined) return false
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(origin)
|
|
55
|
+
return (
|
|
56
|
+
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
57
|
+
(isLoopback(url.hostname) || isPrivateLan(url.hostname)) &&
|
|
58
|
+
url.host === host
|
|
59
|
+
)
|
|
60
|
+
} catch {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validProfileName(value) {
|
|
66
|
+
return (
|
|
67
|
+
typeof value === 'string' &&
|
|
68
|
+
value !== '' &&
|
|
69
|
+
value !== '.' &&
|
|
70
|
+
value !== '..' &&
|
|
71
|
+
!value.includes('/') &&
|
|
72
|
+
!value.includes('\\') &&
|
|
73
|
+
!/[\0-\x1f\x7f]/.test(value)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function profileNameFromArgv(argv) {
|
|
78
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
79
|
+
const arg = argv[index]
|
|
80
|
+
if (arg === '--profile' && argv[index + 1] !== undefined) return argv[index + 1]
|
|
81
|
+
if (typeof arg === 'string' && arg.startsWith('--profile=')) return arg.slice(10)
|
|
82
|
+
}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findDshCliEntry() {
|
|
87
|
+
const entry = process.argv[1]
|
|
88
|
+
if (typeof entry !== 'string' || entry === '') return undefined
|
|
89
|
+
let directory = process.cwd()
|
|
90
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
91
|
+
const manifestPath = resolve(directory, 'package.json')
|
|
92
|
+
if (existsSync(manifestPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
95
|
+
const bin =
|
|
96
|
+
typeof manifest.bin === 'string'
|
|
97
|
+
? manifest.bin
|
|
98
|
+
: typeof manifest.bin === 'object' && manifest.bin !== null
|
|
99
|
+
? manifest.bin.dsh
|
|
100
|
+
: undefined
|
|
101
|
+
if (
|
|
102
|
+
manifest.name === '@deepseek-ai/dsh' &&
|
|
103
|
+
typeof bin === 'string' &&
|
|
104
|
+
!isAbsolute(bin) &&
|
|
105
|
+
resolve(directory, bin) === resolve(entry)
|
|
106
|
+
) {
|
|
107
|
+
return entry
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Continue searching parent directories
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const parent = dirname(directory)
|
|
114
|
+
if (parent === directory) return undefined
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function runtime() {
|
|
119
|
+
const profileDir = resolve(
|
|
120
|
+
process.env.DSH_PROFILE_DIR ?? resolve(homedir(), '.dsh', 'profiles', 'web')
|
|
121
|
+
)
|
|
122
|
+
const selected = profileNameFromArgv(process.argv)
|
|
123
|
+
const profileName = validProfileName(selected)
|
|
124
|
+
? selected
|
|
125
|
+
: validProfileName(basename(profileDir))
|
|
126
|
+
? basename(profileDir)
|
|
127
|
+
: 'web'
|
|
128
|
+
const cliEntry = findDshCliEntry()
|
|
129
|
+
return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseSemver(value) {
|
|
133
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
|
|
134
|
+
if (match === null) return undefined
|
|
135
|
+
return {
|
|
136
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
137
|
+
prerelease: match[4]?.split('.') ?? [],
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function comparePrerelease(left, right) {
|
|
142
|
+
if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
|
|
143
|
+
const length = Math.max(left.length, right.length)
|
|
144
|
+
for (let index = 0; index < length; index += 1) {
|
|
145
|
+
const a = left[index]
|
|
146
|
+
const b = right[index]
|
|
147
|
+
if (a === undefined || b === undefined) return a === b ? 0 : a === undefined ? -1 : 1
|
|
148
|
+
if (a === b) continue
|
|
149
|
+
const aNumeric = /^\d+$/.test(a)
|
|
150
|
+
const bNumeric = /^\d+$/.test(b)
|
|
151
|
+
if (aNumeric && bNumeric) {
|
|
152
|
+
const aNumber = BigInt(a)
|
|
153
|
+
const bNumber = BigInt(b)
|
|
154
|
+
if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
|
|
158
|
+
return a > b ? 1 : -1
|
|
159
|
+
}
|
|
160
|
+
return 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isNewerVersion(currentValue, candidateValue) {
|
|
164
|
+
const current = parseSemver(currentValue)
|
|
165
|
+
const candidate = parseSemver(candidateValue)
|
|
166
|
+
if (current === undefined || candidate === undefined) return false
|
|
167
|
+
for (let index = 0; index < 3; index += 1) {
|
|
168
|
+
if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index]
|
|
169
|
+
}
|
|
170
|
+
return comparePrerelease(candidate.prerelease, current.prerelease) > 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function latestVersion(packageName, registry) {
|
|
174
|
+
if (
|
|
175
|
+
latestCache?.packageName === packageName &&
|
|
176
|
+
latestCache.registry === registry &&
|
|
177
|
+
Date.now() < latestCache.expiresAt
|
|
178
|
+
) {
|
|
179
|
+
return latestCache.version
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch(`${registry.replace(/\/$/, '')}/${encodeURIComponent(packageName)}/latest`, {
|
|
183
|
+
signal: typeof AbortSignal !== 'undefined' && AbortSignal.timeout ? AbortSignal.timeout(8000) : undefined,
|
|
184
|
+
})
|
|
185
|
+
if (!response.ok) return undefined
|
|
186
|
+
const value = await response.json()
|
|
187
|
+
if (typeof value?.version !== 'string' || value.version === '') return undefined
|
|
188
|
+
latestCache = { packageName, registry, version: value.version, expiresAt: Date.now() + VERSION_CACHE_MS }
|
|
189
|
+
return value.version
|
|
190
|
+
} catch {
|
|
191
|
+
return undefined
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function currentVersion(manifestUrl) {
|
|
196
|
+
const value = JSON.parse(await readFile(manifestUrl, 'utf8'))
|
|
197
|
+
if (typeof value?.version !== 'string' || value.version === '') {
|
|
198
|
+
throw new Error('Cannot read current plugin version.')
|
|
199
|
+
}
|
|
200
|
+
return value.version
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function status(options, target) {
|
|
204
|
+
const current = await currentVersion(options.manifestUrl)
|
|
205
|
+
const latest = await latestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org')
|
|
206
|
+
return {
|
|
207
|
+
packageName: options.packageName,
|
|
208
|
+
currentVersion: current,
|
|
209
|
+
...(latest === undefined ? {} : { latestVersion: latest }),
|
|
210
|
+
latestCheckFailed: latest === undefined,
|
|
211
|
+
updateAvailable: latest !== undefined && isNewerVersion(current, latest),
|
|
212
|
+
profileName: target.profileName,
|
|
213
|
+
canAutoUpdate: target.cliEntry !== undefined,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function installExact(target, packageSpec, options) {
|
|
218
|
+
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
|
|
219
|
+
await new Promise((resolvePromise, reject) => {
|
|
220
|
+
const child = spawn(
|
|
221
|
+
process.execPath,
|
|
222
|
+
[
|
|
223
|
+
target.cliEntry,
|
|
224
|
+
'plugin',
|
|
225
|
+
'--profile',
|
|
226
|
+
target.profileName,
|
|
227
|
+
'add',
|
|
228
|
+
'--config.minimumReleaseAge=0',
|
|
229
|
+
packageSpec,
|
|
230
|
+
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
231
|
+
],
|
|
232
|
+
{
|
|
233
|
+
cwd: target.profileDir,
|
|
234
|
+
windowsHide: true,
|
|
235
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
236
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
let detail = ''
|
|
240
|
+
child.stdout?.on('data', (chunk) => {
|
|
241
|
+
detail = (detail + String(chunk)).slice(-4000)
|
|
242
|
+
})
|
|
243
|
+
child.stderr?.on('data', (chunk) => {
|
|
244
|
+
detail = (detail + String(chunk)).slice(-4000)
|
|
245
|
+
})
|
|
246
|
+
const timer = setTimeout(() => {
|
|
247
|
+
child.kill()
|
|
248
|
+
reject(new Error('Update timed out; use the normal DSH update flow.'))
|
|
249
|
+
}, UPDATE_TIMEOUT_MS)
|
|
250
|
+
child.once('error', (error) => {
|
|
251
|
+
clearTimeout(timer)
|
|
252
|
+
reject(error)
|
|
253
|
+
})
|
|
254
|
+
child.once('exit', (code) => {
|
|
255
|
+
clearTimeout(timer)
|
|
256
|
+
if (code === 0) resolvePromise()
|
|
257
|
+
else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function json(response, statusCode, value) {
|
|
263
|
+
response.writeHead(statusCode, {
|
|
264
|
+
'content-type': 'application/json; charset=utf-8',
|
|
265
|
+
'cache-control': 'no-store',
|
|
266
|
+
})
|
|
267
|
+
response.end(JSON.stringify(value))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function registerPluginUpdater(ctx, options) {
|
|
271
|
+
let installing = false
|
|
272
|
+
if (typeof ctx?.webServer?.register !== 'function') {
|
|
273
|
+
ctx?.logger?.warn?.('[dsh-tts] webServer service unavailable, updater skipped')
|
|
274
|
+
return () => {}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return ctx.webServer.register({
|
|
278
|
+
kind: 'exact',
|
|
279
|
+
path: options.endpoint,
|
|
280
|
+
handler: async (request, response) => {
|
|
281
|
+
try {
|
|
282
|
+
const target = runtime()
|
|
283
|
+
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
284
|
+
const payload = await status(options, target)
|
|
285
|
+
response.writeHead(200, {
|
|
286
|
+
'content-type': 'application/json; charset=utf-8',
|
|
287
|
+
'cache-control': 'no-store',
|
|
288
|
+
})
|
|
289
|
+
response.end(request.method === 'HEAD' ? undefined : JSON.stringify(payload))
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
if (request.method !== 'POST') {
|
|
293
|
+
response.writeHead(405, { allow: 'GET, HEAD, POST' })
|
|
294
|
+
response.end()
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
if (!isTrustedUpdateRequest(request)) {
|
|
298
|
+
json(response, 403, { error: 'Rejected non-local or cross-origin update request.' })
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (installing) {
|
|
302
|
+
json(response, 409, { error: 'This plugin is already updating.' })
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
installing = true
|
|
306
|
+
try {
|
|
307
|
+
const before = await status(options, target)
|
|
308
|
+
if (before.latestVersion === undefined) {
|
|
309
|
+
json(response, 503, { error: 'The latest version is temporarily unavailable.' })
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
if (!before.updateAvailable) {
|
|
313
|
+
json(response, 200, before)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
await installExact(target, `${options.packageName}@${before.latestVersion}`, options)
|
|
317
|
+
json(response, 200, {
|
|
318
|
+
...before,
|
|
319
|
+
updatedVersion: before.latestVersion,
|
|
320
|
+
restartRequired: true,
|
|
321
|
+
})
|
|
322
|
+
} catch (err) {
|
|
323
|
+
json(response, 503, { error: `Plugin update failed: ${String(err?.message || err)}` })
|
|
324
|
+
} finally {
|
|
325
|
+
installing = false
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
ctx?.logger?.warn?.(`[dsh-tts] plugin updater failed: ${String(error)}`)
|
|
329
|
+
json(response, 503, { error: 'Plugin update failed; see server logs.' })
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
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",
|