dsh-dafeiyu-emotion 0.2.7 → 0.2.10
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/README.md +3 -3
- package/docs/MARKETPLACE.md +1 -1
- package/docs/RELEASE-0.2.10.md +20 -0
- package/docs/RELEASE-0.2.8.md +16 -0
- package/docs/RELEASE-0.2.9.md +17 -0
- package/package.json +1 -1
- package/scripts/test-analyze-handler.mjs +15 -1
- package/scripts/test-config-schema.mjs +42 -1
- package/src/index.js +43 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.10
|
|
4
|
+
|
|
5
|
+
- Removed `settings` from the hard inject list. A hard `settings` dependency can deadlock host startup while the settings service is still building plugin `Config` projections.
|
|
6
|
+
- Settings are now resolved with `ctx.inject(['settings'], ...)` after the host is up; `/config` is read-only until then and becomes writable when the service arrives.
|
|
7
|
+
- Added a regression assertion that `settings` is not a hard dependency.
|
|
8
|
+
|
|
9
|
+
## 0.2.9
|
|
10
|
+
|
|
11
|
+
- Fixed the host startup timeout: `apply` synchronously called `settings.describe()` while the host was still mounting plugins, which could deadlock startup for 60 seconds.
|
|
12
|
+
- `apply` now uses the patch config only; settings are read/written through `/config` after the host is up.
|
|
13
|
+
- Added cycle/depth guards to `plainConfig()` so an unexpected volatile-ref cycle cannot hang startup or a settings request.
|
|
14
|
+
- Added a regression test where `settings.describe()` throws if called during `apply`.
|
|
15
|
+
|
|
16
|
+
## 0.2.8
|
|
17
|
+
|
|
18
|
+
- Fixed `backendRoot` showing as `[object Object]` on the settings page. DSH projects volatile `Config` fields as `{ get() }` refs; `readSettings()` and `resolveOptions()` now unwrap them before path/string/number handling.
|
|
19
|
+
- Added `plainConfig` regression coverage so a volatile ref can never be stringified into a fake path again.
|
|
20
|
+
|
|
3
21
|
## 0.2.7
|
|
4
22
|
|
|
5
23
|
- Fixed a host activation failure: `apply` read `ctx.settings` while the plugin only declared `webServer`. Cordis throws `cannot get property "settings" without inject`, so the whole host plugin failed to mount and `/plugins/dsh-dafeiyu-emotion/config` returned 404.
|
package/README.md
CHANGED
|
@@ -111,9 +111,9 @@ dsh plugin --profile desktop add github:LiDu-china/dsh-dafeiyu-emotion
|
|
|
111
111
|
### 通过 Release tarball
|
|
112
112
|
|
|
113
113
|
```bash
|
|
114
|
-
curl -L -o dsh-dafeiyu-emotion-0.2.
|
|
115
|
-
https://github.com/LiDu-china/dsh-dafeiyu-emotion/releases/download/v0.2.
|
|
116
|
-
dsh plugin --profile desktop add "D:/path/to/dsh-dafeiyu-emotion-0.2.
|
|
114
|
+
curl -L -o dsh-dafeiyu-emotion-0.2.10.tgz \
|
|
115
|
+
https://github.com/LiDu-china/dsh-dafeiyu-emotion/releases/download/v0.2.10/dsh-dafeiyu-emotion-0.2.10.tgz
|
|
116
|
+
dsh plugin --profile desktop add "D:/path/to/dsh-dafeiyu-emotion-0.2.10.tgz"
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
> DSH Desktop 的 profile 由 Electron 应用管理;命令行安装请使用桌面应用自带的 `dsh` 命令。
|
package/docs/MARKETPLACE.md
CHANGED
|
@@ -72,7 +72,7 @@ https://github.com/LiDu-china/dsh-dafeiyu-emotion/releases/download/v0.2.3/dsh-d
|
|
|
72
72
|
dsh plugin --profile desktop add dsh-dafeiyu-emotion
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Published as `dsh-dafeiyu-emotion@0.2.
|
|
75
|
+
Published as `dsh-dafeiyu-emotion@0.2.9` on npm. The 1024Store entry refreshes its one-click install automatically. GitHub-source and Release-tarball installs remain available as fallbacks.
|
|
76
76
|
|
|
77
77
|
## Privacy
|
|
78
78
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## 0.2.10
|
|
2
|
+
|
|
3
|
+
修复 Host 启动阶段的依赖死锁,并修正 volatile 配置解包。
|
|
4
|
+
|
|
5
|
+
- 移除 `settings` 硬依赖。改为 `ctx.inject(['settings'], ...)` 动态获取,避免 settings 服务尚未就绪时插件 fiber 一直 pending。
|
|
6
|
+
- `apply` 不再调用 `settings.describe()`,只在 `/config` 请求时读取运行时配置。
|
|
7
|
+
- `plainConfig()` 改为递归栈 + `finally` 清理,修复共享对象被误判为循环引用而丢失的问题。
|
|
8
|
+
- 新增共享引用、真实循环、`settings.describe()` 不应在启动阶段调用的回归测试。
|
|
9
|
+
|
|
10
|
+
## 注意
|
|
11
|
+
|
|
12
|
+
当前环境如果仍无法启动,请先确认 `magpie-mcp-git` / `magpie-mcp-anysearch` 没有阻塞 Host 启动。这两个 MCP 客户端在连接不可用时会等待重试,可能把 Host 启动拖过 60 秒。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
dsh plugin --profile desktop add dsh-dafeiyu-emotion
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
升级后请完全退出并重新启动 the agent harness。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
## 0.2.8
|
|
2
|
+
|
|
3
|
+
修复设置页把增强后端目录显示成 `[object Object]` 的问题。
|
|
4
|
+
|
|
5
|
+
- DSH 的 settings 服务会把 `Config` 的 volatile 字段投影成 `{ get() }` 引用。
|
|
6
|
+
- 0.2.7 直接对引用做 `String()`,得到 `[object Object]`,再被 `resolve()` 拼成 `C:\...\profiles\desktop\[object Object]`,于是状态卡显示“后端目录不存在”。
|
|
7
|
+
- 0.2.8 在 `readSettings()` 和 `resolveOptions()` 中统一用 `plainConfig()` 递归解包,再处理路径、语言和数字。
|
|
8
|
+
- 新增回归测试,确保 volatile ref 不会被字符串化成假路径。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
dsh plugin --profile desktop add dsh-dafeiyu-emotion
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
升级后请完全退出并重新启动 the agent harness,让 Host 重新读取 0.2.8。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## 0.2.9
|
|
2
|
+
|
|
3
|
+
修复“the agent harness 打不开 / Host startup exceeded 60 seconds”。
|
|
4
|
+
|
|
5
|
+
- 根因:0.2.8 在 `apply` 期间同步调用 `settings.describe()`。此时 Host 还在挂载插件,遍历所有插件配置可能造成启动死锁,最终 60 秒超时。
|
|
6
|
+
- 0.2.9 的 `apply` 只读取 `cordis.patch.yml` 的 patch 配置,不再在启动事务里调用 `settings.describe()`。
|
|
7
|
+
- 设置页的读取/写入仍通过 `/plugins/dsh-dafeiyu-emotion/config`;`PATCH` 会更新共享运行时配置并调用 `settings.update()` 持久化。
|
|
8
|
+
- `plainConfig()` 增加循环引用和深度保护,避免异常 volatile ref 卡死启动或设置请求。
|
|
9
|
+
- 新增回归测试:`settings.describe()` 在 `apply` 期间如果被调用会直接抛错。
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
dsh plugin --profile desktop add dsh-dafeiyu-emotion
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
升级后请完全退出并重新启动 the agent harness。若之前因 0.2.8 无法启动,可先进入安全模式或临时移除插件,再安装 0.2.9。
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { Readable } from 'node:stream'
|
|
|
5
5
|
import { apply, inject } from '../src/index.js'
|
|
6
6
|
|
|
7
7
|
assert.ok(inject.includes('webServer'), 'webServer must be a declared dependency')
|
|
8
|
-
assert.
|
|
8
|
+
assert.equal(inject.includes('settings'), false, 'settings must be resolved dynamically, not as a hard dependency')
|
|
9
9
|
|
|
10
10
|
const ANALYZE = '/plugins/dsh-dafeiyu-emotion/analyze'
|
|
11
11
|
const HEALTH = '/plugins/dsh-dafeiyu-emotion/health'
|
|
@@ -14,6 +14,12 @@ function makeCtx(speechToText) {
|
|
|
14
14
|
const handlers = new Map()
|
|
15
15
|
const ctx = {
|
|
16
16
|
logger: { debug() {}, info() {}, error() {} },
|
|
17
|
+
settings: {
|
|
18
|
+
async update() {},
|
|
19
|
+
describe() {
|
|
20
|
+
throw new Error('settings.describe must not run during host startup')
|
|
21
|
+
},
|
|
22
|
+
},
|
|
17
23
|
effect(run) {
|
|
18
24
|
const dispose = run()
|
|
19
25
|
return typeof dispose === 'function' ? dispose : () => {}
|
|
@@ -40,6 +46,14 @@ function makeCtx(speechToText) {
|
|
|
40
46
|
return typeof dispose === 'function' ? dispose : () => {}
|
|
41
47
|
},
|
|
42
48
|
})
|
|
49
|
+
} else if (dependencies.includes('settings')) {
|
|
50
|
+
callback({
|
|
51
|
+
settings: ctx.settings,
|
|
52
|
+
effect(run) {
|
|
53
|
+
const dispose = run()
|
|
54
|
+
return typeof dispose === 'function' ? dispose : () => {}
|
|
55
|
+
},
|
|
56
|
+
})
|
|
43
57
|
}
|
|
44
58
|
},
|
|
45
59
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
|
|
3
|
-
import { Config } from '../src/index.js'
|
|
3
|
+
import { Config, plainConfig, resolveOptions } from '../src/index.js'
|
|
4
4
|
|
|
5
5
|
// The DSH settings service parses the schema with its defaults before a plugin
|
|
6
6
|
// row can become configurable. A default that violates its own step/min/max
|
|
@@ -50,4 +50,45 @@ assert.throws(
|
|
|
50
50
|
'unknown languages must be rejected by the schema',
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
+
// DSH projects volatile fields as { get() } refs. They must be unwrapped
|
|
54
|
+
// before path resolution; otherwise backendRoot becomes "[object Object]".
|
|
55
|
+
{
|
|
56
|
+
const ref = (value) => ({ get: () => value })
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
plainConfig({
|
|
59
|
+
backendRoot: ref('D:/voice-sense'),
|
|
60
|
+
nested: { language: ref('auto') },
|
|
61
|
+
}),
|
|
62
|
+
{
|
|
63
|
+
backendRoot: 'D:/voice-sense',
|
|
64
|
+
nested: { language: 'auto' },
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
const options = resolveOptions({
|
|
68
|
+
backendRoot: ref('D:/voice-sense'),
|
|
69
|
+
pythonPath: ref(''),
|
|
70
|
+
language: ref('auto'),
|
|
71
|
+
maxAudioBytes: ref(8 * 1024 * 1024),
|
|
72
|
+
})
|
|
73
|
+
assert.equal(options.backendRoot.includes('[object Object]'), false)
|
|
74
|
+
assert.ok(options.backendRoot.endsWith('voice-sense'))
|
|
75
|
+
assert.equal(options.language, 'auto')
|
|
76
|
+
assert.equal(options.maxAudioBytes, 8 * 1024 * 1024)
|
|
77
|
+
|
|
78
|
+
const shared = { x: 1 }
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
plainConfig({ a: shared, b: shared }),
|
|
81
|
+
{ a: { x: 1 }, b: { x: 1 } },
|
|
82
|
+
'a shared object must survive in both branches',
|
|
83
|
+
)
|
|
84
|
+
assert.deepEqual(
|
|
85
|
+
plainConfig({ a: [shared, shared] }),
|
|
86
|
+
{ a: [{ x: 1 }, { x: 1 }] },
|
|
87
|
+
'a shared array element must survive twice',
|
|
88
|
+
)
|
|
89
|
+
const cycle = {}
|
|
90
|
+
cycle.self = cycle
|
|
91
|
+
assert.deepEqual(plainConfig(cycle), { self: undefined }, 'a real cycle must be cut')
|
|
92
|
+
}
|
|
93
|
+
|
|
53
94
|
console.log('config schema tests passed')
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
8
8
|
import Schema from '@deepseek-ai/schemastery'
|
|
9
9
|
|
|
10
10
|
export const name = 'dsh-dafeiyu-emotion'
|
|
11
|
-
export const inject = ['webServer'
|
|
11
|
+
export const inject = ['webServer']
|
|
12
12
|
|
|
13
13
|
const ANALYZE_ENDPOINT = '/plugins/dsh-dafeiyu-emotion/analyze'
|
|
14
14
|
const HEALTH_ENDPOINT = '/plugins/dsh-dafeiyu-emotion/health'
|
|
@@ -137,17 +137,39 @@ function asNumber(value, fallback) {
|
|
|
137
137
|
return Number.isFinite(numeric) ? numeric : fallback
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* DSH settings projects volatile Config fields as live refs (`{ get() }`).
|
|
142
|
+
* Unwrap them before path/string/number handling, otherwise String(ref)
|
|
143
|
+
* becomes "[object Object]" and a path is resolved into the profile cwd.
|
|
144
|
+
*/
|
|
145
|
+
function plainConfig(value, stack = new WeakSet(), depth = 0) {
|
|
146
|
+
if (depth > 20) return undefined
|
|
147
|
+
if (value && typeof value === 'object') {
|
|
148
|
+
if (stack.has(value)) return undefined
|
|
149
|
+
stack.add(value)
|
|
150
|
+
try {
|
|
151
|
+
if (typeof value.get === 'function') return plainConfig(value.get(), stack, depth + 1)
|
|
152
|
+
if (Array.isArray(value)) return value.map((child) => plainConfig(child, stack, depth + 1))
|
|
153
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, plainConfig(child, stack, depth + 1)]))
|
|
154
|
+
} finally {
|
|
155
|
+
stack.delete(value)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return value
|
|
159
|
+
}
|
|
160
|
+
|
|
140
161
|
function resolveOptions(raw = {}) {
|
|
162
|
+
const value = plainConfig(raw)
|
|
141
163
|
return {
|
|
142
|
-
enabled:
|
|
143
|
-
backendRoot: String(
|
|
144
|
-
? resolve(String(
|
|
164
|
+
enabled: value.enabled !== false,
|
|
165
|
+
backendRoot: String(value.backendRoot || process.env.DSH_DAFEIYU_EMOTION_BACKEND || DEFAULTS.backendRoot).trim()
|
|
166
|
+
? resolve(String(value.backendRoot || process.env.DSH_DAFEIYU_EMOTION_BACKEND || DEFAULTS.backendRoot))
|
|
145
167
|
: '',
|
|
146
|
-
pythonPath: String(
|
|
147
|
-
language: ['zh', 'en', 'auto'].includes(
|
|
148
|
-
maxAudioBytes: Math.min(32 * 1024 * 1024, Math.max(256 * 1024, asNumber(
|
|
149
|
-
startupTimeoutMs: Math.min(600000, Math.max(10000, asNumber(
|
|
150
|
-
requestTimeoutMs: Math.min(600000, Math.max(10000, asNumber(
|
|
168
|
+
pythonPath: String(value.pythonPath || '').trim(),
|
|
169
|
+
language: ['zh', 'en', 'auto'].includes(value.language) ? value.language : DEFAULTS.language,
|
|
170
|
+
maxAudioBytes: Math.min(32 * 1024 * 1024, Math.max(256 * 1024, asNumber(value.maxAudioBytes, DEFAULTS.maxAudioBytes))),
|
|
171
|
+
startupTimeoutMs: Math.min(600000, Math.max(10000, asNumber(value.startupTimeoutMs, DEFAULTS.startupTimeoutMs))),
|
|
172
|
+
requestTimeoutMs: Math.min(600000, Math.max(10000, asNumber(value.requestTimeoutMs, DEFAULTS.requestTimeoutMs))),
|
|
151
173
|
}
|
|
152
174
|
}
|
|
153
175
|
|
|
@@ -209,7 +231,7 @@ function readSettings(service) {
|
|
|
209
231
|
try {
|
|
210
232
|
const entry = service.describe().find((row) => row?.ns === SETTINGS_NAMESPACE)
|
|
211
233
|
if (!entry) return null
|
|
212
|
-
return { ...(entry.value || {}), ...(entry.user || {}) }
|
|
234
|
+
return plainConfig({ ...(entry.value || {}), ...(entry.user || {}) })
|
|
213
235
|
} catch {
|
|
214
236
|
return null
|
|
215
237
|
}
|
|
@@ -936,12 +958,16 @@ function createAnalyzeHandler(runtime, backend, getSpeechToText) {
|
|
|
936
958
|
|
|
937
959
|
export function apply(ctx, rawConfig = {}) {
|
|
938
960
|
const logger = ctx.logger ?? console
|
|
939
|
-
|
|
961
|
+
// Resolve settings through ctx.inject below instead of a hard dependency.
|
|
962
|
+
// A hard `settings` inject can deadlock host startup while the settings
|
|
963
|
+
// service is still building the plugin Config projections.
|
|
964
|
+
let settingsService
|
|
940
965
|
const runtime = {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
966
|
+
// Do not call settings.describe() here: this runs inside the host's
|
|
967
|
+
// startup transaction and describing every plugin can deadlock the host.
|
|
968
|
+
// The patch config is authoritative at mount time; PATCH updates the
|
|
969
|
+
// shared runtime object and settings.update persists it for the next boot.
|
|
970
|
+
options: resolveOptions(rawConfig),
|
|
945
971
|
}
|
|
946
972
|
const backend = new VoiceBackend(runtime.options, logger)
|
|
947
973
|
if (runtime.options.backendRoot && !existsSync(runtime.options.backendRoot)) {
|
|
@@ -950,11 +976,6 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
950
976
|
let speechToText
|
|
951
977
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
952
978
|
settingsService = settingsCtx.settings
|
|
953
|
-
const stored = readSettings(settingsService)
|
|
954
|
-
if (!stored) return
|
|
955
|
-
const before = { ...runtime.options }
|
|
956
|
-
Object.assign(runtime.options, resolveOptions({ ...runtime.options, ...stored }))
|
|
957
|
-
backend.syncOptions(before)
|
|
958
979
|
})
|
|
959
980
|
ctx.inject(['speechToText'], (speechCtx) => {
|
|
960
981
|
speechToText = speechCtx.speechToText
|
|
@@ -1032,6 +1053,8 @@ export {
|
|
|
1032
1053
|
createConfigHandler,
|
|
1033
1054
|
decorateAnalysis,
|
|
1034
1055
|
normalizeConfigPatch,
|
|
1056
|
+
plainConfig,
|
|
1035
1057
|
publicConfig,
|
|
1058
|
+
resolveOptions,
|
|
1036
1059
|
transcribeWithSpeech,
|
|
1037
1060
|
}
|