@zaofan/dsh-qqbot 1.10.34 → 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.
package/README.md CHANGED
@@ -107,9 +107,9 @@ DSH 前端会把**回复正文里的 markdown 图片**渲染成图;本插件
107
107
  配合表情包库最顺手:`list_stickers` 搜到图 → 把本地路径写进正文 → 图组直接出来,比连发 `send_media` 更像翻漫画。
108
108
 
109
109
  失败策略:图传不上去时会**原样留在文字里**(不阻断本条,不会吞掉正文)。
110
-
111
- ---
112
-
110
+
111
+ ---
112
+
113
113
  ## 📦 扩展包与插件市场:把你的扩展分享给别人
114
114
 
115
115
  单个 `.mjs` 是「扩展文件」,**一个目录就是一个「扩展包」** —— 可以带工具、斜杠命令、封面、版本号,装进别人的面板里就是一张卡片。
@@ -173,9 +173,14 @@ export default {
173
173
  run: async (args) => ({ ok: true, msg: '选中:' + args.options[0] }),
174
174
  }
175
175
 
176
- // ❌ 错:包成标准 JSON Schema,注册时会被拒
176
+ // ✅ 也认 parameters 这个别名
177
+ // (源码 src/features/extension-store.ts:225 是 inputSchema ?? parameters ?? {})
178
+ // ⚠️ 但值同样必须是扁平字段表,不能包 type/properties
179
+
180
+ // ❌ 错:包成标准 JSON Schema
177
181
  // inputSchema: { type: 'object', properties: { options: {...} } }
178
- // → unsupported JSON schema: parameters.type must be a value schema object
182
+ // → dsh 会拿这个对象的顶层键当参数名,于是模型看到的参数是 type / properties,
183
+ // 真正的 options 藏在 properties 里 → 看不见、调不动
179
184
  ```
180
185
 
181
186
  命令的契约也不一样:工具用 **`run`**,命令用 **`handler`**(返回字符串)。
@@ -273,15 +278,15 @@ env.user / env.owners / env.peer · env.log() · env.kernel(全权限遥控器
273
278
 
274
279
  ---
275
280
 
276
- ## 🛒 插件市场与扩展包面板
277
-
281
+ ## 🛒 插件市场与扩展包面板
282
+
278
283
  **插件市场**——两列卡片、封面图、版本号、安装/更新/卸载一键搞定(数据来自扩展索引仓库,支持填代理解决墙内访问);
279
284
  **扩展包面板**——每个包下面直接列出它带的工具和 `/斜杠命令`,点「设置」还能配自定义参数。
280
285
 
281
286
  ![插件市场(卡片网格 / 封面 / 一键安装更新)](docs/images/market.png)
282
287
 
283
288
  ![扩展包面板(工具与斜杠命令按包分组展示)](docs/images/packs.png)
284
-
289
+
285
290
  ## 🆚 官方 Bot 方案 vs onbot / NapCat 等第三方协议端
286
291
 
287
292
  本插件走的是**腾讯官方 QQ 机器人开放平台**(AppID + AppSecret + 官方 SDK),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaofan/dsh-qqbot",
3
- "version": "1.10.34",
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; // ← 自有存储优先(用户最新保存的)