@goodandready/dsh-voice 0.9.26 → 0.9.28

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 CHANGED
@@ -219,23 +219,28 @@ Registers `transcribe_audio(file_path, language?)` in `ctx.tools`, allowing agen
219
219
  ### Internal HTTP Endpoints
220
220
  * `POST /dsh-voice/transcribe` — `{ dataBase64, mimeType, mode }` → `{ ok, text, provider, tookMs }`
221
221
  * `POST /dsh-voice/polish` — `{ text }` → `{ ok, text }`
222
- * `GET /dsh-voice/status` — Returns daemon status, active chains, SenseVoice.
222
+ * `GET /dsh-voice/status` — Returns daemon status, active chains, SenseVoice and `effectiveSensevoiceProvider`.
223
223
  * `GET /dsh-voice/config` — Returns live plugin configuration snapshot.
224
224
  * `PUT /dsh-voice/config` — Updates and persists plugin configuration across network.
225
225
  * `GET/POST /dsh-voice/sensevoice-installer` — SenseVoice 1-click model installation status and trigger (protected by `isTrustedCaller`, model paths redacted).
226
226
  * `POST /api/dsh-voice/update` — Triggers plugin self-update to latest compatible npm version (protected by local caller verification and profile lock check).
227
227
  * `GET /api/dsh-voice/update` — Returns update check status (current version, latest version, updateAvailable).
228
228
 
229
- ### 🔒 Profile Lock & Operator Recovery Policy
230
- When updating or installing plugins, DeepSeek Harness profiles coordinate concurrent operations using `<profile-dir>/package.json.lock`.
229
+ ### 🔒 Profile Lock & Controlled Operator Recovery Policy
230
+ When updating or installing plugins, DeepSeek Harness profiles coordinate concurrent operations using `<profile-dir>/package.json.lock` (for example, `~/.dsh/profiles/<profile>/package.json.lock`).
231
231
  * **Canonical Contender Policy**: The contender process never deletes or unlinks an existing lockfile. Automatic stale lock removal by contenders is prohibited to eliminate TOCTOU (Time-of-Check to Time-of-Use) race conditions and prevent unintended lockfile clobbering.
232
232
  * **Diagnostics**: If `<profile-dir>/package.json.lock` is present, update attempts return `409 Conflict` with clear diagnostics:
233
233
  - If held by an active process, the PID is reported.
234
- - If filesystem inspection encounters access errors (`EACCES`, `EIO`), the operation fails closed with the error code preserved.
235
- * **Operator Recovery**: If a prior installation process was forcefully killed or terminated unexpectedly (e.g. system OOM or power outage), manual operator recovery is required:
236
- ```bash
237
- rm /home/vadim/.dsh/profiles/web/package.json.lock
238
- ```
234
+ - If filesystem inspection encounters access errors (`EACCES`, `EIO`), the operation fails closed with the error code preserved. Filesystem errors require inspecting filesystem health, permissions, or mount options — **do not** remove the lockfile in response to `EACCES`/`EIO`.
235
+ * **Controlled Operator Recovery Workflow**:
236
+ If a previous installation was forcefully interrupted (e.g. system crash, OOM kill) leaving a stale lockfile, recovery is strictly an operator action and must follow this controlled procedure:
237
+ 1. **Establish Exclusive Maintenance**: Ensure no concurrent processes, CLI commands (`dsh plugin add`), web UI operations, or automated updaters are running or scheduled to start for the target profile.
238
+ 2. **Verify Quiescence and Ownership**: Check for active processes (`pgrep`, `ps`, `fuser`, or `lsof`) to confirm that no live process is currently performing package operations in the target profile directory. Lock age alone or observing an unparsed PID is **not** sufficient justification to delete a lock while another installer could start. If quiescence or non-ownership cannot be verified, do not remove the lock.
239
+ 3. **Remove Confirmed Orphan Lock**: Only after confirming the lock is an orphan and exclusive maintenance is established, remove the specific profile lock without recursive or force flags:
240
+ ```bash
241
+ rm ~/.dsh/profiles/<profile>/package.json.lock
242
+ ```
243
+ 4. **Resume Operations**: Re-enable installations and retry the plugin update.
239
244
 
240
245
  ---
241
246
 
package/README.ru.md CHANGED
@@ -219,20 +219,27 @@ dsh plugin --profile web add @goodandready/dsh-voice
219
219
  ### Внутренние HTTP эндпоинты
220
220
  * `POST /dsh-voice/transcribe` — `{ dataBase64, mimeType, mode }` → `{ ok, text, provider, tookMs }`
221
221
  * `POST /dsh-voice/polish` — `{ text }` → `{ ok, text }`
222
- * `GET /dsh-voice/status` — состояние демонов, цепочки, конфигурация SenseVoice и реалтайма.
222
+ * `GET /dsh-voice/status` — состояние демонов, цепочки, конфигурация SenseVoice, реалтайма и effectiveSensevoiceProvider.
223
223
  * `GET/PUT /dsh-voice/config` — получение и сохранение конфигурации плагина.
224
224
  * `GET/POST /dsh-voice/sensevoice-installer` — статус и установка модели SenseVoice в 1 клик.
225
225
  * `POST /api/dsh-voice/update` — запуск обновления плагина до актуальной версии из npm (с проверкой прав и блокировки профиля).
226
226
  * `GET /api/dsh-voice/update` — проверка наличия обновлений.
227
227
 
228
- ### 🔒 Политика блокировок профиля и ручное восстановление оператором
229
- При установке и обновлении плагинов DeepSeek Harness синхронизирует параллельные процессы через `<profile-dir>/package.json.lock`.
230
- * **Канонический принцип претендента**: Процесс-претендент никогда не удаляет существующий lock-файл. Автоматическое удаление чужих блокировок исключено для предотвращения TOCTOU-гонок.
231
- * **Диагностика**: При наличии lock-файла запрос на обновление возвращает `409 Conflict` с подробной информацией (PID процесса либо код ошибки доступа к файловой системе `EACCES`/`EIO`).
232
- * **Восстановление оператором**: Если предыдущий процесс установки аварийно завершился (например, OOM или отключение питания) и оставил зависший lock-файл, снятие блокировки выполняет оператор вручную:
233
- ```bash
234
- rm /home/vadim/.dsh/profiles/web/package.json.lock
235
- ```
228
+ ### 🔒 Политика блокировок профиля и контролируемое восстановление оператором
229
+ При установке и обновлении плагинов DeepSeek Harness синхронизирует параллельные процессы через `<profile-dir>/package.json.lock` (например, `~/.dsh/profiles/<profile>/package.json.lock`).
230
+ * **Канонический принцип претендента**: Процесс-претендент никогда не удаляет чужой lock-файл. Автоматическое удаление чужих блокировок претендентом запрещено для исключения TOCTOU-гонок и повреждения состояния профиля.
231
+ * **Диагностика**: При наличии lock-файла запрос на обновление возвращает `409 Conflict` с подробной информацией:
232
+ - Если блокировка удерживается активным процессом, выводится его PID.
233
+ - При ошибках файловой системы (`EACCES`, `EIO`) операция завершается безопасно (fail-closed) с сохранением кода ошибки. Ошибки ФС требуют проверки прав доступа, состояния диска или опций монтирования — **не удаляйте** lock-файл при ошибках `EACCES`/`EIO`.
234
+ * **Регламент контролируемого восстановления оператором**:
235
+ Если предыдущий процесс установки аварийно оборвался (например, OOM или сбой питания) и оставил зависший lock-файл, снятие блокировки выполняет оператор по следующему регламенту:
236
+ 1. **Обеспечение эксклюзивного окна обслуживания**: Убедитесь, что никакие параллельные процессы, команды CLI (`dsh plugin add`), веб-интерфейс или фоновые апдейтеры не выполняют установку и не могут запуститься для данного профиля.
237
+ 2. **Проверка отсутствия живого владельца (quiescence)**: Проверьте с помощью `pgrep`, `ps`, `fuser` или `lsof`, что в каталоге профиля действительно нет активных процессов-установщиков. Сам по себе возраст файла или нераспарсенный PID **не являются** достаточным основанием для удаления lock-файла, пока не исключён параллельный запуск другого процесса. Если состояние покоя не подтверждено, удаление запрещено.
238
+ 3. **Удаление только подтверждённого осиротевшего lock-файла**: Только после подтверждения статуса сироты удалите конкретный файл блокировки без рекурсивных и force-флагов:
239
+ ```bash
240
+ rm ~/.dsh/profiles/<profile>/package.json.lock
241
+ ```
242
+ 4. **Возобновление работы**: Разрешите запуск процессов установки и повторите обновление плагина.
236
243
 
237
244
  ---
238
245
 
package/README.zh.md CHANGED
@@ -138,21 +138,28 @@ v0.8.18 质量审查后的修复批次(Gitea #79–#87,PR #88):
138
138
 
139
139
  * `POST /dsh-voice/transcribe` — 音频转写:`{ dataBase64, mimeType, mode }` → `{ ok, text, provider, tookMs }`
140
140
  * `POST /dsh-voice/polish` — 文本模型润色:`{ text }` → `{ ok, text }`
141
- * `GET /dsh-voice/status` — 查询后端状态、服务商健康度与 SenseVoice
141
+ * `GET /dsh-voice/status` — 查询后端状态、服务商健康度、SenseVoice 及 effectiveSensevoiceProvider
142
142
  * `GET /dsh-voice/config` — 获取插件运行时配置快照
143
143
  * `PUT /dsh-voice/config` — 跨网络安全持久化更新插件配置
144
144
  * `GET/POST /dsh-voice/sensevoice-installer` — SenseVoice 一键模型安装状态与触发接口(受 `isTrustedCaller` 保护,绝对路径脱敏)
145
145
  * `POST /api/dsh-voice/update` — 触发插件自动更新至 npm 最新兼容版本(受本地调用与锁校验保护)
146
146
  * `GET /api/dsh-voice/update` — 获取更新检查状态与版本信息
147
147
 
148
- ### 🔒 配置文件锁与操作员恢复机制
149
- 在安装或更新插件时,DeepSeek Harness 使用 `<profile-dir>/package.json.lock` 协调并发操作。
150
- * **规范竞争者策略**: 竞争者进程绝不删除现有的锁定文件。禁止竞争者自动清理残留锁,以杜绝 TOCTOU 竞态条件。
151
- * **诊断信息**: 若存在 lock 文件,更新请求将返回 `409 Conflict` 并附带诊断说明(关联 PID 或文件系统访问错误码 `EACCES`/`EIO`)。
152
- * **操作员手动恢复**: 若先前安装进程异常终止(如 OOM 或系统断电)留下僵死锁,需由操作员手动清理:
153
- ```bash
154
- rm /home/vadim/.dsh/profiles/web/package.json.lock
155
- ```
148
+ ### 🔒 配置文件锁与受控操作员恢复机制
149
+ 在安装或更新插件时,DeepSeek Harness 使用 `<profile-dir>/package.json.lock`(例如 `~/.dsh/profiles/<profile>/package.json.lock`)协调并发操作。
150
+ * **规范竞争者策略**: 竞争者进程绝不删除现有的锁定文件。禁止竞争者自动清理残留锁,以杜绝 TOCTOU 竞态条件并避免破坏配置文件状态。
151
+ * **诊断信息**: 若存在 lock 文件,更新请求将返回 `409 Conflict` 并附带诊断说明:
152
+ - 若由活跃进程持有,则报告其 PID。
153
+ - 若遭遇文件系统访问错误(`EACCES`、`EIO`),则安全失败(fail-closed)并保留错误码。文件系统错误需要检查磁盘健康度、目录权限或挂载选项——**请勿**因 `EACCES`/`EIO` 错误而删除锁文件。
154
+ * **受控操作员恢复流程**:
155
+ 若先前安装进程异常终止(如 OOM 终止或系统断电)留下僵死锁,必须由操作员按以下受控流程进行清理:
156
+ 1. **建立独占维护窗口**: 确保目标配置文件当前没有正在运行或计划启动的并发安装任务(CLI 命令 `dsh plugin add`、Web UI 交互或自动更新器)。
157
+ 2. **确认静止状态与无活跃属主**: 通过 `pgrep`、`ps`、`fuser` 或 `lsof` 确认目标配置文件目录下确实没有正在执行包操作的活跃进程。仅凭锁文件存在时间较长或 PID 未能解析**不足以**作为删除依据,前提必须是彻底排除其他进程启动的可能。若无法确认静止状态,切勿删除。
158
+ 3. **安全删除已确认的孤立锁**: 只有在完全确认锁文件已孤立且处于维护窗口期时,才允许由操作员手动删除该特定锁文件(严禁使用递归或 force 标志):
159
+ ```bash
160
+ rm ~/.dsh/profiles/<profile>/package.json.lock
161
+ ```
162
+ 4. **恢复正常运行**: 重新开启安装通道并重试插件更新。
156
163
 
157
164
  ---
158
165
 
package/lib/client.js CHANGED
@@ -219,9 +219,9 @@ window.__ModuleLoader__.load({
219
219
  'updateSuccess': 'Updated successfully! Restart DSH to apply.',
220
220
  'updateFailed': 'Update failed. Check logs.',
221
221
  'updateCheckFailed': 'Update check failed',
222
- 'profileLockedPid': 'Profile is locked by process (PID {pid}). Operator recovery is required if stale: remove package.json.lock.',
223
- 'profileLockedExisting': 'Profile is locked by an existing lockfile. Operator recovery is required if stale: remove package.json.lock.',
224
- 'profileLockCheckError': 'Profile lock check failed. Filesystem inspection or operator recovery required.',
222
+ 'profileLockedPid': 'Profile is locked by process (PID {pid}). Follow documented controlled operator recovery procedure if stale.',
223
+ 'profileLockedExisting': 'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if stale.',
224
+ 'profileLockCheckError': 'Profile lock check failed. Filesystem inspection required (do not remove lockfile).',
225
225
  'checkingUpdates': 'Checking updates…',
226
226
  'updateStatusUnknown': 'Version status unknown',
227
227
  'sensevoiceTitle': 'SenseVoice Local ASR (1-Click Setup)',
@@ -437,9 +437,9 @@ window.__ModuleLoader__.load({
437
437
  'updateSuccess': '更新成功!请重启 DSH 生效。',
438
438
  'updateFailed': '更新失败,请查看日志。',
439
439
  'updateCheckFailed': '检查更新失败',
440
- 'profileLockedPid': '配置文件已被进程锁定 (PID {pid})。若残留请由操作员手动处理:删除 package.json.lock。',
441
- 'profileLockedExisting': '配置文件已被现有锁定文件锁定。若残留请由操作员手动处理:删除 package.json.lock。',
442
- 'profileLockCheckError': '检查配置文件锁失败。需要排查文件系统或由操作员恢复。',
440
+ 'profileLockedPid': '配置文件已被进程锁定 (PID {pid})。若残留请按受控操作员恢复流程处理。',
441
+ 'profileLockedExisting': '配置文件已被现有锁定文件锁定。若残留请按受控操作员恢复流程处理。',
442
+ 'profileLockCheckError': '检查配置文件锁失败。需排查文件系统,请勿直接删除锁文件。',
443
443
  'checkingUpdates': '正在检查更新…',
444
444
  'updateStatusUnknown': '版本状态未知',
445
445
  'sensevoiceTitle': 'SenseVoice 本地 ASR (一键部署)',
@@ -3997,6 +3997,10 @@ voice._chainsPromise = null
3997
3997
  stopCurrent,
3998
3998
  submitPending,
3999
3999
  loadWebGpuWhisper,
4000
+ waitStop,
4001
+ currentLevel,
4002
+ accentColor,
4003
+ softColor,
4000
4004
  }
4001
4005
  return module.exports
4002
4006
  },
@@ -1,5 +1,6 @@
1
1
  // lib/host-routes.js — Host HTTP routes for dsh-voice status, polish, and transcribe
2
2
  import { writeJson, readBody, isTrustedCaller } from './http-util.js'
3
+ import { resolveSensevoiceProvider } from './sensevoice-installer.js'
3
4
 
4
5
  export function registerHostRoutes(ctx, {
5
6
  live,
@@ -63,6 +64,7 @@ export function registerHostRoutes(ctx, {
63
64
  sensevoiceUrl: cfg.sensevoiceUrl,
64
65
  sensevoiceAutostart: !!cfg.sensevoiceAutostart,
65
66
  sensevoiceProvider: cfg.sensevoiceProvider || 'cpu',
67
+ effectiveSensevoiceProvider: resolveSensevoiceProvider(cfg),
66
68
  sensevoiceThreads: cfg.sensevoiceThreads || 4,
67
69
  webgpuModel: cfg.webgpuModel || 'onnx-community/whisper-tiny',
68
70
  deepgramBaseUrl: cfg.deepgramBaseUrl || 'https://api.deepgram.com',
package/lib/index.js CHANGED
@@ -47,6 +47,23 @@ export const Config = typeof z.number().volatile === 'function'
47
47
  ? makeVolatile(BaseConfig)
48
48
  : BaseConfig
49
49
 
50
+ export function isVolatileRef(value) {
51
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && typeof value.get === 'function'
52
+ }
53
+
54
+ export function plainConfig(value) {
55
+ while (typeof value === 'function') {
56
+ value = value()
57
+ }
58
+ if (isVolatileRef(value)) return plainConfig(value.get())
59
+ if (value === null || typeof value !== 'object') return value
60
+ if (value instanceof Date || value instanceof RegExp || (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))) {
61
+ return value
62
+ }
63
+ if (Array.isArray(value)) return value.map(plainConfig)
64
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plainConfig(v)]))
65
+ }
66
+
50
67
  export function apply(ctx, baseConfig) {
51
68
  let settingsService = null
52
69
  let scope = null
@@ -55,7 +72,25 @@ export function apply(ctx, baseConfig) {
55
72
  const readEntryConfig = createConfigReader(baseConfig, ctx)
56
73
  getConfig = readEntryConfig
57
74
 
58
- const live = () => BaseConfig(structuredClone(getConfig() ?? {})) ?? readEntryConfig()
75
+ const describeRow = () => {
76
+ if (typeof settingsService?.describe !== 'function') return null
77
+ try {
78
+ return settingsService.describe().find((r) => r && r.ns === NS) || null
79
+ } catch {
80
+ return null
81
+ }
82
+ }
83
+
84
+ const live = () => {
85
+ try {
86
+ const raw = getConfig() ?? {}
87
+ const unwrapped = plainConfig(raw)
88
+ const validated = BaseConfig(unwrapped)
89
+ return plainConfig(validated) ?? plainConfig(readEntryConfig())
90
+ } catch {
91
+ return plainConfig(readEntryConfig())
92
+ }
93
+ }
59
94
 
60
95
  const resolveKey = async (ref) => {
61
96
  if (!ref || !ref.type) return ''
@@ -81,14 +116,19 @@ export function apply(ctx, baseConfig) {
81
116
  try {
82
117
  scope = sctx.settings.register(NS, Config, { base: baseConfig })
83
118
  if (scope && typeof scope.get === 'function') {
84
- getConfig = () => scope.get() ?? readEntryConfig()
119
+ getConfig = () => scope.get() ?? describeRow()?.config ?? readEntryConfig()
120
+ } else {
121
+ getConfig = () => describeRow()?.config ?? readEntryConfig()
85
122
  }
86
123
  if (scope && typeof scope.watch === 'function') {
87
124
  scope.watch(() => {
88
125
  triggerAutostart()
89
126
  })
90
127
  }
91
- } catch { /* modern DSH handles settings via SettingsForms */ }
128
+ } catch {
129
+ // Modern DSH 0.2 handles settings via SettingsForms and describe/update/replace
130
+ getConfig = () => describeRow()?.config ?? readEntryConfig()
131
+ }
92
132
  sctx.effect(() => () => { getConfig = readEntryConfig; scope = null; settingsService = null })
93
133
  triggerAutostart()
94
134
  })
@@ -231,6 +231,31 @@ export function registerSensevoiceInstaller(ctx, { liveConfig, isAlive, updateCo
231
231
  })
232
232
  }
233
233
 
234
+ export function resolveSensevoiceProvider(cfg) {
235
+ let modelPath = String(cfg?.sensevoiceModel || '')
236
+ const rawProv = String(cfg?.sensevoiceProvider || '').trim().toLowerCase()
237
+ let normProv = rawProv === 'rknpu' ? 'rknn' : rawProv
238
+
239
+ if (normProv === 'rknn') {
240
+ const ext = path.extname(modelPath).toLowerCase()
241
+ if (ext === '.onnx') {
242
+ const modelDir = modelPath ? path.dirname(modelPath) : getDefaultModelDir()
243
+ const rknnCandidate = path.join(modelDir, path.basename(modelPath, ext) + '.rknn')
244
+ const defaultRknn = path.join(modelDir, 'model.rknn')
245
+ if (existsSync(rknnCandidate) || existsSync(defaultRknn)) {
246
+ return 'rknn'
247
+ }
248
+ // Guard: ordinary ONNX model cannot be loaded by sherpa-onnx RKNN runtime (#221)
249
+ return 'cpu'
250
+ }
251
+ if (ext === '.rknn') {
252
+ return 'rknn'
253
+ }
254
+ }
255
+
256
+ return normProv || 'cpu'
257
+ }
258
+
234
259
  export function buildSensevoiceArgs(cfg, port) {
235
260
  let modelPath = String(cfg?.sensevoiceModel || '')
236
261
  const modelDir = modelPath ? path.dirname(modelPath) : getDefaultModelDir()
package/lib/updater.js CHANGED
@@ -211,12 +211,12 @@ export function checkProfileLock(profileDir) {
211
211
  export function formatLockDiagnostic(lockStatus) {
212
212
  if (!lockStatus?.locked) return null;
213
213
  if (lockStatus.error) {
214
- return `Profile lock check failed with error ${lockStatus.error}. Operator recovery or filesystem inspection required.`;
214
+ return `Profile lock check failed with error ${lockStatus.error}. Filesystem inspection required (do not remove lockfile).`;
215
215
  }
216
216
  if (lockStatus.pid) {
217
- return `Profile is locked by process (PID ${lockStatus.pid}). If previous installation crashed, operator recovery is required: remove package.json.lock manually.`;
217
+ return `Profile is locked by process (PID ${lockStatus.pid}). Follow documented controlled operator recovery procedure if the process terminated abnormally.`;
218
218
  }
219
- return 'Profile is locked by an existing lockfile. If previous installation crashed, operator recovery is required: remove package.json.lock manually.';
219
+ return 'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if the process terminated abnormally.';
220
220
  }
221
221
 
222
222
  async function installExact(target, packageSpec, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.9.26",
3
+ "version": "0.9.28",
4
4
  "description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
5
5
  "license": "MIT",
6
6
  "type": "module",