dsh-notify-say 0.4.21 → 0.4.25
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 +216 -176
- package/docs/audio-audit/README.md +164 -0
- package/docs/audio-audit/fetch-snapshots.ps1 +76 -0
- package/docs/audio-audit/snapshots/ameharu-hau.txt +89 -0
- package/docs/audio-audit/snapshots/goki-nuigurumi.txt +73 -0
- package/docs/audio-audit/snapshots/kasukabe-tsumugi.txt +197 -0
- package/docs/audio-audit/snapshots/manbetsu-hanamaru.txt +27 -0
- package/docs/audio-audit/snapshots/meimei-meria.txt +84 -0
- package/docs/audio-audit/snapshots/mochiko.txt +27 -0
- package/docs/audio-audit/snapshots/namiine-ritsu.txt +76 -0
- package/docs/audio-audit/snapshots/shirakami-kotaro.txt +158 -0
- package/docs/audio-audit/snapshots/sss-anonymous-common.txt +142 -0
- package/docs/audio-audit/snapshots/sss-zundamon.txt +142 -0
- package/docs/audio-audit/snapshots/voicevox-software.txt +41 -0
- package/docs/audio-audit/snapshots/voicevox-vvm-terms.txt +385 -0
- package/docs/audio-audit/snapshots/yurei-chan.txt +112 -0
- package/docs//345/276/205/345/201/232/346/270/205/345/215/225.md +218 -0
- package/docs//350/257/255/351/237/263/345/214/205/345/210/206/345/217/221/346/226/271/346/241/210-/345/276/205/345/256/241/351/230/205.md +378 -0
- package/lib/client.js +50 -8
- package/lib/config-route.js +251 -209
- package/lib/http-guard.js +222 -0
- package/lib/index.js +1822 -1768
- package/lib/session-bridge.js +138 -117
- package/package.json +11 -2
- package/test/audio-source.test.mjs +184 -0
- package/test/audio-wiring.test.mjs +287 -0
- package/test/client-bundle.test.mjs +671 -0
- package/test/client-contract.test.mjs +118 -0
- package/test/config-coverage.test.mjs +109 -0
- package/test/config-route.test.mjs +212 -0
- package/test/config-schema.test.mjs +147 -0
- package/test/config-store.test.mjs +256 -0
- package/test/dev-reload.test.mjs +132 -0
- package/test/effective-for-client.test.mjs +142 -0
- package/test/fail-route.test.mjs +128 -0
- package/test/fail-voice.test.mjs +193 -0
- package/test/http-guard.test.mjs +143 -0
- package/test/http-integration.test.mjs +284 -0
- package/test/idle-gate.test.mjs +167 -0
- package/test/preview.test.mjs +92 -0
- package/test/real-cordis.test.mjs +204 -0
- package/test/release-voice-guard.test.mjs +185 -0
- package/test/reload-live.test.mjs +261 -0
- package/test/reload-verification.test.mjs +218 -0
- package/test/route-loader-register.mjs +22 -0
- package/test/scalar-guard.test.mjs +125 -0
- package/test/session-jump.test.mjs +266 -0
- package/test/stub-child-process.mjs +43 -0
- package/test/tts-lines.test.mjs +112 -0
- package/test/version-consistency.test.mjs +98 -0
- package/test/voice-availability.test.mjs +90 -0
- package/test/wav-gain.test.mjs +138 -0
- package/test/wiring.test.mjs +287 -0
- package/tools/notify-config.ps1 +472 -471
package/lib/config-route.js
CHANGED
|
@@ -1,209 +1,251 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 配置读写 route(host 半侧):给 client 半侧的设置卡片提供真正的读写能力。
|
|
3
|
-
*
|
|
4
|
-
* ## 为什么需要它
|
|
5
|
-
*
|
|
6
|
-
* 官方 `Config` schema 生成的是**插件配置编辑器**里的表单;本插件以 bundle 形态
|
|
7
|
-
* 挂载时,那个表单并不出现在设置页的分区里(实测:分区里只有我们自己画的卡)。
|
|
8
|
-
* 用户要的是"点一下就能改音量",所以卡片必须自带控件。
|
|
9
|
-
*
|
|
10
|
-
* 而 client 跑在沙箱渲染进程里(无 process、无 fs),改不了 storages 下的文件,
|
|
11
|
-
* 所以要开一条 host route:client `fetch` 读写,host 落地到
|
|
12
|
-
* `$DSH_HOME/storages/dsh-notify-desktop/config.json`。
|
|
13
|
-
*
|
|
14
|
-
* ## 为什么写这个文件是安全的
|
|
15
|
-
*
|
|
16
|
-
* - 只写**本插件自己的**状态文件(与 config-store 同一路径、同一白名单校验);
|
|
17
|
-
* - 不碰 `cordis.patch.yml`、不碰凭据、不碰别的插件;
|
|
18
|
-
* - 写入前 `sanitize()` 过滤:未知键丢弃、越界值拒绝;
|
|
19
|
-
* - 文件损坏时 `writeConfig` 拒绝覆盖(保留用户原文件供人工救)。
|
|
20
|
-
*
|
|
21
|
-
* ## 与三源合并的关系
|
|
22
|
-
*
|
|
23
|
-
* 这里写的是**第 4 层(最高优先级)**。插件每轮播报前 `refreshConfig()` 重读,
|
|
24
|
-
* 所以**改完立即生效,不需要重启**。想交还控制权给设置页 / YAML:`{ clear: true }`。
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { existsSync, unlinkSync } from 'node:fs';
|
|
28
|
-
|
|
29
|
-
import {
|
|
30
|
-
readConfig, writeConfig, sanitize, configPath, CONFIG_KEYS,
|
|
31
|
-
} from './config-store.js';
|
|
32
|
-
import { ROUTE_PREFIX } from './session-bridge.js';
|
|
33
|
-
|
|
34
|
-
/** 读配置。 */
|
|
35
|
-
export const ROUTE_CONFIG = ROUTE_PREFIX + '/config';
|
|
36
|
-
/** 写配置。 */
|
|
37
|
-
export const ROUTE_CONFIG_SET = ROUTE_PREFIX + '/config/set';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
req.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 配置读写 route(host 半侧):给 client 半侧的设置卡片提供真正的读写能力。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
*
|
|
6
|
+
* 官方 `Config` schema 生成的是**插件配置编辑器**里的表单;本插件以 bundle 形态
|
|
7
|
+
* 挂载时,那个表单并不出现在设置页的分区里(实测:分区里只有我们自己画的卡)。
|
|
8
|
+
* 用户要的是"点一下就能改音量",所以卡片必须自带控件。
|
|
9
|
+
*
|
|
10
|
+
* 而 client 跑在沙箱渲染进程里(无 process、无 fs),改不了 storages 下的文件,
|
|
11
|
+
* 所以要开一条 host route:client `fetch` 读写,host 落地到
|
|
12
|
+
* `$DSH_HOME/storages/dsh-notify-desktop/config.json`。
|
|
13
|
+
*
|
|
14
|
+
* ## 为什么写这个文件是安全的
|
|
15
|
+
*
|
|
16
|
+
* - 只写**本插件自己的**状态文件(与 config-store 同一路径、同一白名单校验);
|
|
17
|
+
* - 不碰 `cordis.patch.yml`、不碰凭据、不碰别的插件;
|
|
18
|
+
* - 写入前 `sanitize()` 过滤:未知键丢弃、越界值拒绝;
|
|
19
|
+
* - 文件损坏时 `writeConfig` 拒绝覆盖(保留用户原文件供人工救)。
|
|
20
|
+
*
|
|
21
|
+
* ## 与三源合并的关系
|
|
22
|
+
*
|
|
23
|
+
* 这里写的是**第 4 层(最高优先级)**。插件每轮播报前 `refreshConfig()` 重读,
|
|
24
|
+
* 所以**改完立即生效,不需要重启**。想交还控制权给设置页 / YAML:`{ clear: true }`。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
readConfig, writeConfig, sanitize, configPath, CONFIG_KEYS,
|
|
31
|
+
} from './config-store.js';
|
|
32
|
+
import { ROUTE_PREFIX } from './session-bridge.js';
|
|
33
|
+
|
|
34
|
+
/** 读配置。 */
|
|
35
|
+
export const ROUTE_CONFIG = ROUTE_PREFIX + '/config';
|
|
36
|
+
/** 写配置。 */
|
|
37
|
+
export const ROUTE_CONFIG_SET = ROUTE_PREFIX + '/config/set';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 请求体上限(字节)。
|
|
41
|
+
*
|
|
42
|
+
* 配置写入的实际负载是几百字节的 JSON;给 64 KB 已经宽松 100 倍。
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ 2026-10-18 外部审阅指出:原实现只有「3 秒超时」,**没有大小上限**。
|
|
45
|
+
* 超时不等于限流 —— 攻击者可以在 3 秒内灌几十 MB,`chunks.push` 无上限累积,
|
|
46
|
+
* 先吃满内存再被超时收割。现在两者都要有。
|
|
47
|
+
*/
|
|
48
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 从 Node 的 req 上把 body 读完,**带累计大小上限**。
|
|
52
|
+
*
|
|
53
|
+
* 超限行为:立即停止累积、返回 `{ __tooLarge: true }`,
|
|
54
|
+
* 由调用方回 413。不抛异常,避免把大小问题伪装成"解析失败"。
|
|
55
|
+
*
|
|
56
|
+
* @param {object} req
|
|
57
|
+
* @returns {Promise<object>}
|
|
58
|
+
*/
|
|
59
|
+
function readJsonBody(req) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
try {
|
|
62
|
+
if (!req) return resolve({});
|
|
63
|
+
if (req.body != null && typeof req.body === 'object') return resolve(req.body);
|
|
64
|
+
|
|
65
|
+
const chunks = [];
|
|
66
|
+
let total = 0;
|
|
67
|
+
let done = false;
|
|
68
|
+
const finish = (value) => {
|
|
69
|
+
if (done) return;
|
|
70
|
+
done = true;
|
|
71
|
+
if (value !== undefined) return resolve(value);
|
|
72
|
+
try {
|
|
73
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
74
|
+
resolve(text ? JSON.parse(text) : {});
|
|
75
|
+
} catch {
|
|
76
|
+
resolve({});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (typeof req.on !== 'function') return resolve({});
|
|
80
|
+
|
|
81
|
+
req.on('data', (c) => {
|
|
82
|
+
if (done) return;
|
|
83
|
+
try {
|
|
84
|
+
const buf = Buffer.isBuffer(c) ? c : Buffer.from(String(c));
|
|
85
|
+
total += buf.length;
|
|
86
|
+
if (total > MAX_BODY_BYTES) {
|
|
87
|
+
// 停止累积并让调用方回 413;不再继续吃内存
|
|
88
|
+
chunks.length = 0;
|
|
89
|
+
finish({ __tooLarge: true });
|
|
90
|
+
// ⚠️ 不要 req.destroy():它会断开连接,导致即将发出的 413 响应
|
|
91
|
+
// 还没写完就被切断,客户端看到的是"连接重置"而不是 413。
|
|
92
|
+
// 正确做法:停止累积、暂停流入,让调用方把 413 写完。
|
|
93
|
+
try { req.pause?.(); } catch { /* ignore */ }
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
chunks.push(buf);
|
|
97
|
+
} catch { /* ignore */ }
|
|
98
|
+
});
|
|
99
|
+
req.on('end', () => finish());
|
|
100
|
+
req.on('error', () => finish());
|
|
101
|
+
setTimeout(() => finish(), 3000); // 防御:3 秒还没 end 就放弃
|
|
102
|
+
} catch {
|
|
103
|
+
resolve({});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 写入响应(兼容两种 res 形态)。
|
|
110
|
+
*/
|
|
111
|
+
function send(res, code, body) {
|
|
112
|
+
try {
|
|
113
|
+
const text = body === undefined ? '' : JSON.stringify(body);
|
|
114
|
+
if (res && typeof res.writeHead === 'function') {
|
|
115
|
+
res.writeHead(code, {
|
|
116
|
+
'content-type': 'application/json; charset=utf-8',
|
|
117
|
+
'cache-control': 'no-store',
|
|
118
|
+
});
|
|
119
|
+
res.end(text);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (res && typeof res.setHeader === 'function' && typeof res.end === 'function') {
|
|
123
|
+
res.statusCode = code;
|
|
124
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
125
|
+
res.setHeader('cache-control', 'no-store');
|
|
126
|
+
res.end(text);
|
|
127
|
+
}
|
|
128
|
+
} catch { /* 响应失败不影响后续 */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 清空运行时覆盖层:删除本插件的 config.json。
|
|
133
|
+
* 与 `notify-config.ps1 -Clear` 语义一致。
|
|
134
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
135
|
+
*/
|
|
136
|
+
export function clearOverrides() {
|
|
137
|
+
try {
|
|
138
|
+
const p = configPath();
|
|
139
|
+
if (!p) return { ok: false, reason: 'no-home' };
|
|
140
|
+
if (existsSync(p)) unlinkSync(p);
|
|
141
|
+
return { ok: true };
|
|
142
|
+
} catch (ex) {
|
|
143
|
+
return { ok: false, reason: String((ex && ex.message) || ex) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 创建配置 route 的 handler。
|
|
149
|
+
*
|
|
150
|
+
* @param {object} [opts]
|
|
151
|
+
* @param {() => void} [opts.onChanged] 配置变更后的回调(让插件立即重读)
|
|
152
|
+
* @param {() => object} [opts.getEffective]
|
|
153
|
+
* 返回**当前生效的完整配置**(三层合并结果)。
|
|
154
|
+
*
|
|
155
|
+
* ⚠️ 为什么必须有它(独立审查 2026-10-17 指出,会导致用户配置被改坏):
|
|
156
|
+
* `readConfig()` 只读第 4 层(`storages/.../config.json` 的覆盖值)。
|
|
157
|
+
* 但实际生效值是「schema 默认 → cordis.patch.yml → 设置页 → 文件」四层合并。
|
|
158
|
+
* 于是 GUI 打开时看到的是**文件层**(常常是空的),比如 patch 里 `volume: 100`
|
|
159
|
+
* 而文件没存过 volume,GUI 就显示硬编码默认 80;用户一拖滑块,
|
|
160
|
+
* 就把真实的 100 覆盖成 80 —— 他以为自己只是"确认一下"。
|
|
161
|
+
*
|
|
162
|
+
* @returns {{ handler: Function }}
|
|
163
|
+
*/
|
|
164
|
+
export function createConfigRoute(opts) {
|
|
165
|
+
const onChanged = opts && typeof opts.onChanged === 'function' ? opts.onChanged : null;
|
|
166
|
+
const getEffective = opts && typeof opts.getEffective === 'function' ? opts.getEffective : null;
|
|
167
|
+
const notifyChanged = () => { try { if (onChanged) onChanged(); } catch { /* ignore */ } };
|
|
168
|
+
|
|
169
|
+
/** 生效值(拿不到就退回文件层,保证有值可显示)。 */
|
|
170
|
+
const effective = () => {
|
|
171
|
+
try {
|
|
172
|
+
if (getEffective) {
|
|
173
|
+
const v = getEffective();
|
|
174
|
+
if (v && typeof v === 'object') return v;
|
|
175
|
+
}
|
|
176
|
+
} catch { /* 落到文件层 */ }
|
|
177
|
+
try { return readConfig().value || {}; } catch { return {}; }
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
async function handler(...args) {
|
|
181
|
+
// 兼容 {request,response} 与 (req,res) 两种 register 风格(与 session-bridge 一致)
|
|
182
|
+
const req = args[0] && args[0].request ? args[0].request
|
|
183
|
+
: (args[0] && args[0].req ? args[0].req : args[0]);
|
|
184
|
+
const res = args[0] && args[0].response ? args[0].response
|
|
185
|
+
: (args[0] && args[0].res ? args[0].res : args[1]);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const url = String((req && (req.url || req.path)) || '');
|
|
189
|
+
const method = String((req && req.method) || 'GET').toUpperCase();
|
|
190
|
+
// 只认 /config/set 为写;不再把"POST 到 /config"也算写(语义更自洽)。
|
|
191
|
+
const isWrite = /\/config\/set(\/|$)/.test(url.split('?')[0]);
|
|
192
|
+
|
|
193
|
+
if (isWrite && method !== 'POST') {
|
|
194
|
+
return send(res, 405, { ok: false, reason: 'method-not-allowed' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isWrite) {
|
|
198
|
+
const body = await readJsonBody(req);
|
|
199
|
+
|
|
200
|
+
// 请求体超限:413 而不是 400 —— 让调用方区分"太大"和"格式错"
|
|
201
|
+
if (body && body.__tooLarge === true) {
|
|
202
|
+
return send(res, 413, {
|
|
203
|
+
ok: false,
|
|
204
|
+
reason: 'body-too-large',
|
|
205
|
+
limitBytes: MAX_BODY_BYTES,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 交还控制权:删掉文件层覆盖,回到 patch/schema 的值
|
|
210
|
+
if (body && body.clear === true) {
|
|
211
|
+
const c = clearOverrides();
|
|
212
|
+
if (!c.ok) return send(res, 500, { ok: false, reason: c.reason });
|
|
213
|
+
notifyChanged();
|
|
214
|
+
// 清空后返回的是**生效值**(即 patch 里的值),不是空对象
|
|
215
|
+
return send(res, 200, { ok: true, values: effective() });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const patch = body && body.values && typeof body.values === 'object' ? body.values : body;
|
|
219
|
+
const s = sanitize(patch);
|
|
220
|
+
if (!s.ok) return send(res, 400, { ok: false, reason: 'bad-input', dropped: s.dropped });
|
|
221
|
+
if (Object.keys(s.value).length === 0) {
|
|
222
|
+
return send(res, 400, { ok: false, reason: 'nothing-to-write', dropped: s.dropped });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const w = writeConfig(s.value);
|
|
226
|
+
if (!w.ok) return send(res, 409, { ok: false, reason: w.reason });
|
|
227
|
+
notifyChanged();
|
|
228
|
+
// ⚠️ 回**生效值**而不是 `w.value`(文件层快照)。
|
|
229
|
+
// 否则:改一个开关后,没存过的键(volume 等)会从响应里消失,
|
|
230
|
+
// client 用它整体替换本地态 → 滑块/下拉立刻退回硬编码默认值。
|
|
231
|
+
return send(res, 200, { ok: true, values: effective() });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 读:返回生效值 + 文件层覆盖值(后者用于判断"哪些是用户显式设过的")
|
|
235
|
+
const cur = readConfig();
|
|
236
|
+
let overrides = {};
|
|
237
|
+
try { overrides = cur.ok ? (cur.value || {}) : {}; } catch { overrides = {}; }
|
|
238
|
+
return send(res, 200, {
|
|
239
|
+
ok: true,
|
|
240
|
+
values: effective(),
|
|
241
|
+
overrides,
|
|
242
|
+
corrupt: cur.ok ? false : cur.reason === 'corrupt',
|
|
243
|
+
keys: CONFIG_KEYS,
|
|
244
|
+
});
|
|
245
|
+
} catch {
|
|
246
|
+
return send(res, 500, { ok: false, reason: 'internal' });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { handler };
|
|
251
|
+
}
|