@zaofan/dsh-qqbot 1.10.35 → 1.10.36

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/settings-host.js +61 -37
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaofan/dsh-qqbot",
3
- "version": "1.10.35",
3
+ "version": "1.10.36",
4
4
  "description": "QQ Bot IM channel plugin for deepseek-harness (dsh)",
5
5
  "type": "module",
6
6
  "main": "./entry.js",
package/settings-host.js CHANGED
@@ -243,6 +243,9 @@ function viewOf(settings, ns) {
243
243
  * 位置:{DSH_HOME}/qqbot-settings/<instanceNs>.json
244
244
  */
245
245
  const OWN_SETTINGS_DIR = join(DSH_HOME_DIR, 'qqbot-settings');
246
+ /** appSecret 的回显掩码:前端看到它 / 空值 = 保留原值(不覆盖)。
247
+ * 2026-10-25 提到模块级:/read 与 /accounts 都要用。 */
248
+ const SECRET_MASK = '********';
246
249
 
247
250
  /**
248
251
  * 路径按 profile 隔离(2026-10-11;规则必须与 `src/features/ns-guard.ts` 的 `nsPathsOf()` 保持一致,改一处同步另一处):
@@ -258,36 +261,36 @@ const OWN_SETTINGS_DIR = join(DSH_HOME_DIR, 'qqbot-settings');
258
261
  * 现在与 `src/shared/utils.ts` 的 `resolveProfileName()` **同规则**:环境变量 → `PROFILE_ROOT` 目录名
259
262
  * (要求其父目录名为 `profiles`,挡住源码 checkout 目录)→ 落 `_default`。改一处务必同步另一处。
260
263
  */
261
- function resolveProfileName() {
262
- // ── 按维护手册 §「profile 定位的正解:回写环境变量,别改推断逻辑」 ──
263
- // 宿主的 profileContext 是权威答案,src/index.ts 的 apply() 已经把它回写到:
264
- // process.env.DSH_PROFILE / DSH_PROFILE_DIR
265
- // globalThis.__dshProfileName / __dshProfileContext
266
- // 这里只要**读**它,别自己猜路径。
267
-
268
- // ① 环境变量(apply 阶段回写,最直接)
269
- const env = String(process.env.DSH_PROFILE || '').trim();
270
- if (env) return env;
271
-
272
- // ② apply() 落下的全局变量 —— **这是关键补的一层**:
273
- // settings-host.js 有可能在 apply() 之前就被加载(模块顶层求值),
274
- // 那时环境变量还没回写;但真正用到 profile 名是在请求处理阶段(惰性化后),
275
- // 那时 __dshProfileName 早就有了。
276
- try {
277
- const g = globalThis.__dshProfileName;
278
- if (typeof g === 'string' && g.trim()) return g.trim();
279
- } catch { /* 下一层 */ }
280
-
281
- // ③ 老宿主没有 profileContext 服务 → 只能从 PROFILE_ROOT 目录名反推
282
- try {
283
- if (basename(dirname(PROFILE_ROOT)) === 'profiles') {
284
- const n = basename(PROFILE_ROOT);
285
- if (n && n !== 'profiles') return n;
286
- }
287
- } catch { /* 放弃 */ }
288
-
289
- return '';
290
- }
264
+ function resolveProfileName() {
265
+ // ── 按维护手册 §「profile 定位的正解:回写环境变量,别改推断逻辑」 ──
266
+ // 宿主的 profileContext 是权威答案,src/index.ts 的 apply() 已经把它回写到:
267
+ // process.env.DSH_PROFILE / DSH_PROFILE_DIR
268
+ // globalThis.__dshProfileName / __dshProfileContext
269
+ // 这里只要**读**它,别自己猜路径。
270
+
271
+ // ① 环境变量(apply 阶段回写,最直接)
272
+ const env = String(process.env.DSH_PROFILE || '').trim();
273
+ if (env) return env;
274
+
275
+ // ② apply() 落下的全局变量 —— **这是关键补的一层**:
276
+ // settings-host.js 有可能在 apply() 之前就被加载(模块顶层求值),
277
+ // 那时环境变量还没回写;但真正用到 profile 名是在请求处理阶段(惰性化后),
278
+ // 那时 __dshProfileName 早就有了。
279
+ try {
280
+ const g = globalThis.__dshProfileName;
281
+ if (typeof g === 'string' && g.trim()) return g.trim();
282
+ } catch { /* 下一层 */ }
283
+
284
+ // ③ 老宿主没有 profileContext 服务 → 只能从 PROFILE_ROOT 目录名反推
285
+ try {
286
+ if (basename(dirname(PROFILE_ROOT)) === 'profiles') {
287
+ const n = basename(PROFILE_ROOT);
288
+ if (n && n !== 'profiles') return n;
289
+ }
290
+ } catch { /* 放弃 */ }
291
+
292
+ return '';
293
+ }
291
294
  /**
292
295
  * 当前 profile 对应的存档子目录名。
293
296
  *
@@ -297,12 +300,12 @@ function resolveProfileName() {
297
300
  * (真实存档在 qqbot-settings/desktop/xxx.json)⇒ 凭据读不到 ⇒ nsBot() null。
298
301
  * 改成每次调用时现算(开销是几次字符串/目录读,API 请求级,可忽略)。
299
302
  */
300
- function nsProfileDir() {
301
- // 惰性求值(issue #20 的核心修复):原来这是个模块顶层 const,
302
- // 算的时候 apply() 还没回写 DSH_PROFILE → 永远落 _default。
303
- const n = String(resolveProfileName()).trim().replace(/[^A-Za-z0-9_-]/g, '_');
304
- if (n) return n;
305
- return '_default';
303
+ function nsProfileDir() {
304
+ // 惰性求值(issue #20 的核心修复):原来这是个模块顶层 const,
305
+ // 算的时候 apply() 还没回写 DSH_PROFILE → 永远落 _default。
306
+ const n = String(resolveProfileName()).trim().replace(/[^A-Za-z0-9_-]/g, '_');
307
+ if (n) return n;
308
+ return '_default';
306
309
  }
307
310
  function safeNsOf(ns) {
308
311
  return String(ns || NS).replace(/[^A-Za-z0-9_-]/g, '_') || 'im-qqbot';
@@ -579,6 +582,15 @@ export function apply(ctx) {
579
582
  const own = readOwnSettings(ns);
580
583
  // 自有存储优先:它就是用户在设置页保存的那份
581
584
  const value = Object.assign({}, base.value || {}, own);
585
+ // ── 2026-10-25 修「保存一次 appSecret 就变成旧值」──────────────────────
586
+ // 原来把 appSecret 明文一起回给前端。value = Object.assign(base.value, own),
587
+ // 而 base 来自 cordis.patch.yml(**历史旧值**,2026-09-24 起就不再写它了)。
588
+ // 一旦 own 里缺 appSecret,patch 的旧值就露出来;前端把它当"当前值",
589
+ // 下次保存原样提交回来 → 盖掉用户存的那份 → 每次保存都被顶回去。
590
+ // 修法:这个接口永远只回掩码,前端已有的「掩码/空 = 保留原值」逻辑会正确处理。
591
+ if (value && typeof value === 'object' && value.appSecret) {
592
+ value.appSecret = SECRET_MASK;
593
+ }
582
594
  // 2026-10-11: 顺手把"这个槽是谁在写"告诉前端 —— 被**别的 profile**占用时设置页能一眼看到(判据同 writeOwnSettings)
583
595
  const w = readNsWriter(ns);
584
596
  const conflict = nsConflictOf(ns);
@@ -1246,7 +1258,7 @@ export function apply(ctx) {
1246
1258
  // 账号列表(多账号: 每条带 settings ns 与数据目录, 供二级 UI 按账号读写)
1247
1259
  // 借鉴 zhengjy01/dsh-qqbot-panel(2026-09-06): appSecret 只回显掩码(不泄露明文),
1248
1260
  // 并带 hasSecret 供前端区分"已保存"与"真空"; 保存时空值/掩码 = 保留原 secret。
1249
- const SECRET_MASK = '********';
1261
+
1250
1262
  route(ctx, 'GET', '/api/qqbot-settings/accounts', async (_req, res) => {
1251
1263
  try {
1252
1264
  const { bots, hasFile } = parsePatch();
@@ -2188,6 +2200,18 @@ export function apply(ctx) {
2188
2200
  function mergeOwnIntoCfg(cfg, nsLike) {
2189
2201
  let own = {};
2190
2202
  try { own = readOwnSettings(String(nsLike)) || {}; } catch { own = {}; }
2203
+ // ── 2026-10-25 修「保存一次密钥就变」的另一半 ──────────────────────
2204
+ // patch 里的 appSecret 是**历史旧值**(2026-09-24 起已不再写 patch)。
2205
+ // 原来自有存储缺 appSecret 时就一直回退到 patch ⇒ patch 一旦被写脏,
2206
+ // 用户怎么保存都会被它顶回去。改成:**只在自有存储没有时继承一次并落盘**,
2207
+ // 之后永远以自有存储为准。
2208
+ try {
2209
+ if ((!own || !own.appSecret) && cfg && cfg.appSecret) {
2210
+ const seeded = Object.assign({}, own, { appSecret: String(cfg.appSecret) });
2211
+ writeOwnSettings(String(nsLike), seeded);
2212
+ own = seeded;
2213
+ }
2214
+ } catch { /* 继承失败不影响本次读取 */ }
2191
2215
  const pick = (k) => {
2192
2216
  const o = own[k];
2193
2217
  if (o !== undefined && o !== '') return o; // ← 自有存储优先(用户最新保存的)