@zaofan/dsh-qqbot 1.5.10 → 1.5.14

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 (50) hide show
  1. package/README.md +33 -62
  2. package/README.md.bak-final-1790552188493 +274 -0
  3. package/README.md.bak-ghnote-1790548468037 +269 -0
  4. package/client/qqbot-settings.js +4876 -4785
  5. package/dist/config.d.ts +4 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +2 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/features/contextless-store.d.ts +33 -0
  10. package/dist/features/contextless-store.d.ts.map +1 -0
  11. package/dist/features/contextless-store.js +253 -0
  12. package/dist/features/contextless-store.js.map +1 -0
  13. package/dist/features/contextless-trim.d.ts +47 -0
  14. package/dist/features/contextless-trim.d.ts.map +1 -0
  15. package/dist/features/contextless-trim.js +304 -0
  16. package/dist/features/contextless-trim.js.map +1 -0
  17. package/dist/features/local-embed.d.ts +8 -0
  18. package/dist/features/local-embed.d.ts.map +1 -1
  19. package/dist/features/local-embed.js +58 -7
  20. package/dist/features/local-embed.js.map +1 -1
  21. package/dist/features/qq-approval.d.ts.map +1 -1
  22. package/dist/features/qq-approval.js +14 -2
  23. package/dist/features/qq-approval.js.map +1 -1
  24. package/dist/gateway/bootstrap.d.ts.map +1 -1
  25. package/dist/gateway/bootstrap.js +11 -0
  26. package/dist/gateway/bootstrap.js.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +70 -9
  29. package/dist/index.js.map +1 -1
  30. package/dist/session/session-manager.d.ts +6 -0
  31. package/dist/session/session-manager.d.ts.map +1 -1
  32. package/dist/session/session-manager.js +95 -0
  33. package/dist/session/session-manager.js.map +1 -1
  34. package/dist/transport/inbound.d.ts.map +1 -1
  35. package/dist/transport/inbound.js +20 -0
  36. package/dist/transport/inbound.js.map +1 -1
  37. package/docs/USER-GUIDE.md +113 -0
  38. package/entry.js +57 -0
  39. package/package.json +136 -125
  40. package/settings-host.js +132 -11
  41. package/client/qqbot-settings.js.bak-clamp-195559 +0 -4780
  42. package/client/qqbot-settings.js.bak-ga-041943 +0 -4775
  43. package/dist/commands/approve.d.ts +0 -17
  44. package/dist/commands/approve.d.ts.map +0 -1
  45. package/dist/commands/approve.js +0 -24
  46. package/dist/commands/approve.js.map +0 -1
  47. package/dist/features/approval-switch.d.ts +0 -22
  48. package/dist/features/approval-switch.d.ts.map +0 -1
  49. package/dist/features/approval-switch.js +0 -10
  50. package/dist/features/approval-switch.js.map +0 -1
package/entry.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 包入口守卫(永远随包发布,不参与编译)。
3
+ *
4
+ * 为什么需要它:
5
+ * 仓库不提交编译产物 `dist/`,而 dsh 的插件页支持"填 GitHub 仓库地址安装"。
6
+ * 从 git 直装时若 pnpm 没有执行 `prepare`(新版 pnpm 默认拦截 git 依赖的构建脚本:
7
+ * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`),装出来的包里就没有 `dist/`,
8
+ * 宿主只会打印一句很难懂的 `failed to import`。
9
+ *
10
+ * 这里在入口处先自检,把"真正的原因 + 怎么办"直接打到控制台。
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { dirname, join } from 'node:path';
15
+
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ const missing = [];
18
+ for (const rel of ['dist/index.js', 'client/qqbot-settings.js', 'settings-host.js', 'cordis.patch.yml']) {
19
+ if (!existsSync(join(here, rel))) missing.push(rel);
20
+ }
21
+
22
+ if (missing.length > 0) {
23
+ const isDistMissing = missing.includes('dist/index.js');
24
+ console.error('');
25
+ console.error(' ┌──────────────────────────────────────────────────────────────┐');
26
+ console.error(' │ dsh-qqbot 安装不完整,无法启动 │');
27
+ console.error(' └──────────────────────────────────────────────────────────────┘');
28
+ console.error(' 缺少文件: ' + missing.join(', '));
29
+ console.error(' 安装目录: ' + here);
30
+ console.error('');
31
+ if (isDistMissing) {
32
+ console.error(' 【最可能的原因】你是用 "GitHub 仓库地址" 安装的。');
33
+ console.error(' 仓库不提交编译产物 dist/,而新版 pnpm 默认不允许 git 依赖执行构建脚本');
34
+ console.error(' (报错类似 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED),于是 dist/ 没被编译出来。');
35
+ console.error('');
36
+ console.error(' 【最简单的解决办法】');
37
+ console.error(' 把依赖换成本包在 npm 上的发布版(自带 dist,不需要任何构建):');
38
+ console.error(' pnpm add @zaofan/dsh-qqbot');
39
+ console.error(' 或者在 dsh 插件页里填【包名】 @zaofan/dsh-qqbot (而不是 GitHub 地址)。');
40
+ console.error('');
41
+ console.error(' 【想继续用 git 源的化】');
42
+ console.error(' 在上面那个安装目录里手动编译一次:');
43
+ console.error(' npm install && npm run build');
44
+ console.error(' 如果报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED,需要在 profile 的');
45
+ console.error(' pnpm-workspace.yaml 里允许它执行脚本(错误信息里会给出确切的 allowBuilds 写法)。');
46
+ }
47
+ console.error('');
48
+ throw new Error('dsh-qqbot: 安装不完整(缺少 ' + missing.join(', ') + '),请按上面的提示处理');
49
+ }
50
+
51
+ // 自检通过 → 正常导出
52
+ // ⚠️ 2026-09-28 修:原来这里多写了一句 `export { default } from './dist/index.js'`,
53
+ // 但 dist/index.js 并没有 default 导出 → 触发
54
+ // SyntaxError: The requested module './dist/index.js' does not provide an export named 'default'
55
+ // → 整个插件 import 失败(宿主只报一句难懂的 "failed to import")。
56
+ // 这是个语法级错误:哪怕自检全过,插件也永远加载不起来。
57
+ export * from './dist/index.js';
package/package.json CHANGED
@@ -1,125 +1,136 @@
1
- {
2
- "name": "@zaofan/dsh-qqbot",
3
- "version": "1.5.10",
4
- "description": "QQ Bot IM channel plugin for deepseek-harness (dsh)",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- },
13
- "./channel-tools": {
14
- "types": "./dist/channel-tools.d.ts",
15
- "import": "./dist/channel-tools.js"
16
- },
17
- "./sticker-store": {
18
- "types": "./dist/features/sticker-store.d.ts",
19
- "import": "./dist/features/sticker-store.js"
20
- },
21
- "./schedule-store": {
22
- "types": "./dist/features/schedule-store.d.ts",
23
- "import": "./dist/features/schedule-store.js"
24
- },
25
- "./client": "./client/qqbot-settings.js"
26
- },
27
- "scripts": {
28
- "build": "tsc",
29
- "dev": "tsc --watch",
30
- "prepublishOnly": "npm run build && node scripts/check-package.mjs",
31
- "check:package": "node scripts/check-package.mjs",
32
- "lint": "eslint src/",
33
- "test": "vitest run",
34
- "clean": "rm -rf dist",
35
- "ci:prepare-test-pkg": "node scripts/prepare-test-pkg.js"
36
- },
37
- "repository": {
38
- "type": "git",
39
- "url": "git+https://github.com/gcry13067381632-jpg/dsh-qqbot.git"
40
- },
41
- "homepage": "https://github.com/gcry13067381632-jpg/dsh-qqbot#readme",
42
- "bugs": {
43
- "url": "https://github.com/gcry13067381632-jpg/dsh-qqbot/issues"
44
- },
45
- "keywords": [
46
- "deepseek-harness",
47
- "dsh",
48
- "dsh-plugin",
49
- "qqbot",
50
- "qq-bot",
51
- "chatbot",
52
- "ai-agent",
53
- "cordis",
54
- "plugin",
55
- "llm"
56
- ],
57
- "license": "MIT",
58
- "files": [
59
- "dist",
60
- "docs/USER-GUIDE.md",
61
- "client",
62
- "cordis.patch.yml",
63
- "settings-host.js",
64
- "README.md",
65
- "README_EN.md",
66
- "LICENSE",
67
- "voice-convert.mjs",
68
- "scripts/download-model.mjs",
69
- "scripts/fix-dsh-016-presets.ps1"
70
- ],
71
- "dsh": {
72
- "bundle": {
73
- "patch": "./cordis.patch.yml"
74
- },
75
- "client": { "platform": "web", "inject": ["@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-ui-slots"], "immediately": true },
76
- "engines": {
77
- "dsh": "0.1.5-rc.2 || 0.1.6-alpha.2 || 0.1.7-rc.1"
78
- }
79
- },
80
- "dependencies": {
81
- "@tencent-connect/qqbot-connector": "1.2.0",
82
- "@tencent-connect/qqbot-nodejs": "^1.0.4",
83
- "js-yaml": "^4.1.0",
84
- "silk-wasm": "^3.7.1",
85
- "lamejs-fixed": "^1.2.2"
86
- },
87
- "peerDependencies": {
88
- "@deepseek-ai/cordis": ">=4.0.1",
89
- "@deepseek-ai/dsh-agent": ">=0.1.0-rc.6",
90
- "@deepseek-ai/dsh-llm": ">=0.1.0-rc.6",
91
- "@deepseek-ai/dsh-session": ">=0.1.0-rc.6",
92
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
93
- "@deepseek-ai/schemastery": ">=3.18.1",
94
- "@tencent-connect/qqbot-connector": "1.2.0"
95
- },
96
- "peerDependenciesMeta": {
97
- "@deepseek-ai/cordis": {
98
- "optional": false
99
- },
100
- "@deepseek-ai/schemastery": {
101
- "optional": false
102
- },
103
- "@deepseek-ai/dsh-agent": {
104
- "optional": false
105
- },
106
- "@deepseek-ai/dsh-session": {
107
- "optional": false
108
- },
109
- "@deepseek-ai/dsh-llm": {
110
- "optional": false
111
- }
112
- },
113
- "devDependencies": {
114
- "@deepseek-ai/cordis": "4.0.1",
115
- "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
116
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
117
- "@deepseek-ai/dsh-session": "0.1.0-rc.6",
118
- "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
119
- "@deepseek-ai/schemastery": "3.18.1",
120
- "@types/js-yaml": "^4.0.9",
121
- "@types/node": "^26.2.0",
122
- "typescript": "^5.5.0",
123
- "vitest": "^4.1.8"
124
- }
125
- }
1
+ {
2
+ "name": "@zaofan/dsh-qqbot",
3
+ "version": "1.5.14",
4
+ "description": "QQ Bot IM channel plugin for deepseek-harness (dsh)",
5
+ "type": "module",
6
+ "main": "./entry.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./entry.js"
12
+ },
13
+ "./channel-tools": {
14
+ "types": "./dist/channel-tools.d.ts",
15
+ "import": "./dist/channel-tools.js"
16
+ },
17
+ "./sticker-store": {
18
+ "types": "./dist/features/sticker-store.d.ts",
19
+ "import": "./dist/features/sticker-store.js"
20
+ },
21
+ "./schedule-store": {
22
+ "types": "./dist/features/schedule-store.d.ts",
23
+ "import": "./dist/features/schedule-store.js"
24
+ },
25
+ "./client": "./client/qqbot-settings.js"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "dev": "tsc --watch",
30
+ "prepublishOnly": "npm run build && node scripts/check-package.mjs",
31
+ "check:package": "node scripts/check-package.mjs",
32
+ "lint": "eslint src/",
33
+ "test": "vitest run",
34
+ "clean": "rm -rf dist",
35
+ "ci:prepare-test-pkg": "node scripts/prepare-test-pkg.js"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/gcry13067381632-jpg/dsh-qqbot.git"
40
+ },
41
+ "homepage": "https://github.com/gcry13067381632-jpg/dsh-qqbot#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/gcry13067381632-jpg/dsh-qqbot/issues"
44
+ },
45
+ "keywords": [
46
+ "deepseek-harness",
47
+ "dsh",
48
+ "dsh-plugin",
49
+ "qqbot",
50
+ "qq-bot",
51
+ "chatbot",
52
+ "ai-agent",
53
+ "cordis",
54
+ "plugin",
55
+ "llm"
56
+ ],
57
+ "license": "MIT",
58
+ "files": [
59
+ "dist",
60
+ "docs/USER-GUIDE.md",
61
+ "client",
62
+ "cordis.patch.yml",
63
+ "settings-host.js",
64
+ "README.md",
65
+ "README_EN.md",
66
+ "LICENSE",
67
+ "voice-convert.mjs",
68
+ "scripts/download-model.mjs",
69
+ "scripts/fix-dsh-016-presets.ps1",
70
+ "entry.js"
71
+ ],
72
+ "dsh": {
73
+ "bundle": {
74
+ "patch": "./cordis.patch.yml"
75
+ },
76
+ "client": {
77
+ "platform": "web",
78
+ "inject": [
79
+ "@deepseek-ai/dsh-client-ui-settings",
80
+ "@deepseek-ai/dsh-client-ui-slots"
81
+ ],
82
+ "immediately": true
83
+ },
84
+ "engines": {
85
+ "dsh": "0.1.5-rc.2 || 0.1.6-alpha.2 || 0.1.7-rc.1"
86
+ },
87
+ "manifestVersion": 1
88
+ },
89
+ "dependencies": {
90
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.1",
91
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.1",
92
+ "@deepseek-ai/schemastery": "3.18.4",
93
+ "@tencent-connect/qqbot-connector": "1.2.0",
94
+ "@tencent-connect/qqbot-nodejs": "^1.0.4",
95
+ "js-yaml": "^4.1.0",
96
+ "lamejs-fixed": "^1.2.2",
97
+ "silk-wasm": "^3.7.1"
98
+ },
99
+ "peerDependencies": {
100
+ "@deepseek-ai/cordis": ">=4.0.1",
101
+ "@deepseek-ai/dsh-agent": ">=0.1.0-rc.6",
102
+ "@deepseek-ai/dsh-llm": ">=0.1.0-rc.6",
103
+ "@deepseek-ai/dsh-session": ">=0.1.0-rc.6",
104
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
105
+ "@tencent-connect/qqbot-connector": "1.2.0"
106
+ },
107
+ "peerDependenciesMeta": {
108
+ "@deepseek-ai/cordis": {
109
+ "optional": false
110
+ },
111
+ "@deepseek-ai/schemastery": {
112
+ "optional": false
113
+ },
114
+ "@deepseek-ai/dsh-agent": {
115
+ "optional": false
116
+ },
117
+ "@deepseek-ai/dsh-session": {
118
+ "optional": false
119
+ },
120
+ "@deepseek-ai/dsh-llm": {
121
+ "optional": false
122
+ }
123
+ },
124
+ "devDependencies": {
125
+ "@deepseek-ai/cordis": "4.0.1",
126
+ "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
127
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
128
+ "@deepseek-ai/dsh-session": "0.1.0-rc.6",
129
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
130
+ "@deepseek-ai/schemastery": "3.18.1",
131
+ "@types/js-yaml": "^4.0.9",
132
+ "@types/node": "^26.2.0",
133
+ "typescript": "^5.5.0",
134
+ "vitest": "^4.1.8"
135
+ }
136
+ }
package/settings-host.js CHANGED
@@ -380,6 +380,66 @@ export function apply(ctx) {
380
380
  // 台账文件 {store.dataDir}/known-chats.jsonl 由 dsh-qqbot 中间件 append(见 dsh-qqbot src/features/chat-ledger.ts)。
381
381
  // 用途: Web 设置④区下拉点选目标(免抄 openid)——群无群名, 用最近发言者昵称帮主人辨认。
382
382
  // 多账号: ?dataDir= 指定账号图库目录(台账与之同目录); 缺省 primary。
383
+ // ── 推断本插件的 dataRoot(与插件 src/gateway/data-root.ts 的算法保持一致)──
384
+ // 为什么要这么做:桥和插件在运行时可能是**两份模块实例**(各自的模块级变量不共享)。
385
+ // 插件在 bootstrap 里 initContextlessStore(dataRoot) 用的是正确路径;桥若没 init,
386
+ // contextless-store 的 ensurePath() 只能兜底到 {DSH_HOME},于是"面板保存"写一个文件、
387
+ // "插件实际读取"读另一个文件 → 表现为保存无效 / 开关显示不一致(2026-09-27 实测)。
388
+ function resolveContextlessDataRoot() {
389
+ try {
390
+ const parsed = parsePatch();
391
+ const bots = (parsed && Array.isArray(parsed.bots)) ? parsed.bots : [];
392
+ // 优先取"未禁用"的实例,其次任意一个有 cwd 的
393
+ const pick = bots.filter((b) => b && !b.disabled && b.cfg && (b.cfg.cwd || b.cfg.dataRoot));
394
+ const list = pick.length ? pick : bots.filter((b) => b && b.cfg && (b.cfg.cwd || b.cfg.dataRoot));
395
+ for (const b of list) {
396
+ const cfg = b.cfg || {};
397
+ if (cfg.dataRoot) return String(cfg.dataRoot);
398
+ if (cfg.cwd) return join(String(cfg.cwd), 'dshqqbot'); // 与 DEFAULT_DATA_SUBDIR 一致
399
+ }
400
+ } catch { /* 推不出来就交给 ensurePath 兜底 */ }
401
+ return '';
402
+ }
403
+ /** 确保 contextless-store 绑定到正确的数据根(幂等;路径变了会重新绑定) */
404
+ async function loadContextlessStore() {
405
+ const mod = await import('./dist/features/contextless-store.js'); // ← 函数体内用真正的动态 import(不能调自己)
406
+ try {
407
+ const dr = resolveContextlessDataRoot();
408
+ if (dr) mod.initContextlessStore(dr);
409
+ } catch { /* ignore */ }
410
+ return mod;
411
+ }
412
+
413
+ // ── 无上下文模式(2026-09-27 主人定)──
414
+ // 会话级开关:dock 面板里对**某个会话**开启;开启后该会话每轮只带
415
+ // 「@ 之前 N 条群消息 + 系统规则」,历史对话全部丢弃(大幅省 token)。
416
+ // 开关存 {dataRoot}/.qqbot/contextless.json(由插件在 bootstrap 时初始化)。
417
+ route(ctx, 'GET', '/api/qqbot-settings/contextless', async (req, res) => {
418
+ try {
419
+ const u = new URL(req.url ?? '/', 'http://x');
420
+ const key = (u.searchParams.get('sessionKey') || '').trim();
421
+ const mod = await loadContextlessStore(); // 先 init 到同一个 dataRoot,避免两份模块实例各写一个文件
422
+ if (key) return writeJson(res, 200, mod.getContextless(key));
423
+ return writeJson(res, 200, { list: mod.listContextless() });
424
+ } catch (e) {
425
+ return writeJson(res, 500, { error: String((e && e.message) || e) });
426
+ }
427
+ });
428
+ route(ctx, 'POST', '/api/qqbot-settings/contextless', async (req, res) => {
429
+ try {
430
+ const body = await readJsonBody(req);
431
+ if (!body || typeof body !== 'object') return writeJson(res, 400, { error: 'body must be JSON object' });
432
+ const key = String(body.sessionKey || '').trim();
433
+ if (!key) return writeJson(res, 400, { error: 'sessionKey required' });
434
+ const mod = await loadContextlessStore(); // 先 init 到同一个 dataRoot,避免两份模块实例各写一个文件
435
+ if (body.remove === true) { mod.clearContextless(key); return writeJson(res, 200, { ok: true, removed: true }); }
436
+ const next = mod.setContextless(key, { enabled: body.enabled, window: body.window });
437
+ return writeJson(res, 200, { ok: true, value: next });
438
+ } catch (e) {
439
+ return writeJson(res, 500, { error: String((e && e.message) || e) });
440
+ }
441
+ });
442
+
383
443
  route(ctx, 'GET', '/api/qqbot-settings/known-chats', async (req, res) => {
384
444
  try {
385
445
  const u = new URL(req.url ?? '/', 'http://x');
@@ -776,18 +836,26 @@ export function apply(ctx) {
776
836
  writeJson(res, 200, {
777
837
  hasFile,
778
838
  instances: bots.map((b) => {
779
- const cwd = b.cfg?.cwd || '';
839
+ // ⚠️ 2026-09-28 修:保存时 cwd/preset/appId 只写【自有存储】(见 saveInstances 的 2026-09-24 注释),
840
+ // 但回显原先只读 patch → 界面上填过的 cwd/preset 保存后就"变空"了(前端拿到空串)。
841
+ // 这里把自有存储的值一并合并,与写路径对称(patch 优先,其次自有存储)。
842
+ let ownCfg = {};
843
+ try { ownCfg = readOwnSettings(String(b.id)) || {}; } catch { ownCfg = {}; }
844
+ const cwd = b.cfg?.cwd || ownCfg.cwd || '';
845
+ const presetMerged = b.cfg?.preset || ownCfg.preset || '';
846
+ const appIdMerged = b.cfg?.appId || ownCfg.appId || '';
847
+ const hasSecretMerged = !!(b.cfg?.appSecret || ownCfg.appSecret);
780
848
  // 数据根与 dist 侧 dataRootOf 对齐: 未配 dataRoot → `{cwd}/dshqqbot`(2026-09-12 新默认)
781
849
  const droot = b.cfg?.dataRoot || (cwd ? join(cwd, 'dshqqbot') : '');
782
850
  return {
783
851
  id: b.id,
784
852
  ns: b.id, // settings 命名空间 = 实例 id(主 im-qqbot; 非主实例 render 已写 settingsNs=id)
785
- appId: b.cfg?.appId || '',
786
- appSecret: b.cfg?.appSecret ? SECRET_MASK : '', // 掩码回显(借鉴 panel: masked)
787
- hasSecret: !!b.cfg?.appSecret,
788
- preset: b.cfg?.preset || '',
853
+ appId: appIdMerged,
854
+ appSecret: hasSecretMerged ? SECRET_MASK : '', // 掩码回显(借鉴 panel: masked)
855
+ hasSecret: hasSecretMerged,
856
+ preset: presetMerged,
789
857
  cwd,
790
- disabled: !!b.disabled,
858
+ disabled: !!b.disabled || ownCfg.disabled === true,
791
859
  online: typeof reg.isBotOnline === 'function' ? reg.isBotOnline(b.id) : false, // 在线状态(bot ws ready 事件驱动)
792
860
  // 账号数据目录(各号各库各定时): 图库={cwd}/表情包, 定时={cwd}/.qqbot
793
861
  dataDir: droot ? join(droot, '表情包') : '',
@@ -1672,16 +1740,50 @@ export function apply(ctx) {
1672
1740
  // ── ⑥QQ 群管理面板(P3, 2026-09-05): 多群管理 host 路由(读/写走 GroupAdminClient; 面板=主人直发免 QQ 审批) ──
1673
1741
  // 数据: {cwd}/.qqbot/{groups.json 群注册表, join-pending.json 待审, group-audit.jsonl 审计日志}
1674
1742
  // 鉴权: 与全路由一致走 route() 同源 loopback fence; 面板主人级鉴权(P4 加强)。
1743
+ /**
1744
+ * 把【插件自有存储】的值合并进 patch 解析出的实例列表。
1745
+ *
1746
+ * ⚠️ 2026-09-28 根治同类问题:
1747
+ * 自 2026-09-24 起,已有账号的 appId / appSecret / preset / cwd 只写自有存储、不写 patch
1748
+ * (避免多条目 insert 时块边界写歪 YAML)。但桥里有若干地方**只读 patch** 取账号配置,
1749
+ * 于是用户存进自有存储的值读不到 —— 表现为:
1750
+ * · 账号页填了「工作目录」保存后回显为空
1751
+ * · nsBot() 返回 null → 群管理/图库/无上下文/Excel 导出全部走错路径
1752
+ * 这里统一合并(patch 优先,其次自有存储),所有取账号配置的入口都应走它。
1753
+ */
1754
+ function mergeOwnIntoCfg(cfg, nsLike) {
1755
+ let own = {};
1756
+ try { own = readOwnSettings(String(nsLike)) || {}; } catch { own = {}; }
1757
+ const pick = (k) => (cfg && cfg[k] !== undefined && cfg[k] !== '' ? cfg[k] : (own[k] !== undefined && own[k] !== '' ? own[k] : (cfg ? cfg[k] : undefined)));
1758
+ return {
1759
+ ...cfg,
1760
+ appId: pick('appId'),
1761
+ appSecret: pick('appSecret'),
1762
+ preset: pick('preset'),
1763
+ cwd: pick('cwd'),
1764
+ dataRoot: pick('dataRoot'),
1765
+ };
1766
+ }
1767
+
1675
1768
  function nsBot(ns) {
1676
1769
  const { bots } = parsePatch();
1677
1770
  const id = ns && ns !== 'im-qqbot' ? String(ns) : 'im-qqbot';
1678
1771
  const b = bots.find((x) => x.id === id);
1679
1772
  if (!b) return null;
1680
- const appId = b.cfg?.appId || '';
1681
- const appSecret = b.cfg?.appSecret || '';
1773
+ // ⚠️ 2026-09-28 修(同类问题根治):自 2026-09-24 起,已有账号的 appId/appSecret/preset/cwd
1774
+ // 只写【插件自有存储】,不再写 profile patch(避免多条目 insert 时块边界写歪 YAML)。
1775
+ // 但这里原先只读 patch → 用户存进自有存储的 cwd 读不到 → 数据目录算成空 →
1776
+ // 群管理 / 图库 / 无上下文 / Excel 导出 等**所有**依赖 nsBot 的接口全部走错路径
1777
+ // (症状:报"找不到该账号实例(请先在账号页配置 appId/appSecret)"、图库与定时目录消失)。
1778
+ // 现在先合并自有存储(patch 优先,其次自有存储),与写路径对称。
1779
+ let own = {};
1780
+ try { own = readOwnSettings(String(b.cfg?.settingsNs || id)) || {}; } catch { own = {}; }
1781
+ const appId = b.cfg?.appId || own.appId || '';
1782
+ const appSecret = b.cfg?.appSecret || own.appSecret || '';
1682
1783
  // 数据根: dataRoot(新) > `{cwd}/dshqqbot`(**2026-09-12 新默认**, 与 dist 侧 dataRootOf 对齐);
1683
1784
  // nsBot 的 cwd 字段按数据根返回(全为数据目录用途) —— 未配 dataRoot 的实例不再落到工作目录。
1684
- const cwd = (b.cfg?.dataRoot || (b.cfg?.cwd ? join(b.cfg.cwd, 'dshqqbot') : ''));
1785
+ const cwdCfg = b.cfg?.cwd || own.cwd || '';
1786
+ const cwd = (b.cfg?.dataRoot || own.dataRoot || (cwdCfg ? join(cwdCfg, 'dshqqbot') : ''));
1685
1787
  if (!appId || !appSecret) return null;
1686
1788
  return { id, appId, appSecret, cwd, ns: id };
1687
1789
  }
@@ -1794,8 +1896,11 @@ export function apply(ctx) {
1794
1896
  const { bots } = parsePatch();
1795
1897
  const botByNs = new Map();
1796
1898
  for (const bot of bots) {
1797
- if (bot.disabled || !bot.cfg?.appId || !bot.cfg?.cwd) continue;
1798
- botByNs.set(bot.id, bot);
1899
+ // ⚠️ 2026-09-28:appId/cwd 可能只在【自有存储】里(2026-09-24 起的保存策略),
1900
+ // 合并后再判断,否则这类账号会被误跳过。
1901
+ const mcfg = mergeOwnIntoCfg(bot.cfg || {}, bot.cfg?.settingsNs || bot.id);
1902
+ if (bot.disabled || !mcfg.appId || !mcfg.cwd) continue;
1903
+ botByNs.set(bot.id, { ...bot, cfg: mcfg });
1799
1904
  }
1800
1905
  const hits = [];
1801
1906
  const seen = new Set();
@@ -1978,6 +2083,20 @@ export function apply(ctx) {
1978
2083
  req.setTimeout(180000, () => req.destroy(new Error('下载超时(180s)')));
1979
2084
  });
1980
2085
  }
2086
+ // ── 本地小模型:强制重置加载状态(面板「🔄 重新检测」调用)──
2087
+ // 背景:local-embed 曾有"一次失败永久关闭"的缺陷,用户点重新检测时希望能立刻再试一次。
2088
+ // ⚠️ 桥与插件在同进程:动态 import 同一份 dist 模块,拿到的就是插件正在用的那个实例表。
2089
+ route(ctx, 'POST', '/api/qqbot-settings/local-model/reset', async (req, res) => {
2090
+ try {
2091
+ const mod = await import('./dist/features/local-embed.js');
2092
+ let n = 0;
2093
+ try { n = typeof mod.resetAllEmbedders === 'function' ? mod.resetAllEmbedders() : 0; } catch (e) { n = -1; }
2094
+ return writeJson(res, 200, { ok: true, reset: n });
2095
+ } catch (e) {
2096
+ return writeJson(res, 500, { error: String((e && e.message) || e) });
2097
+ }
2098
+ });
2099
+
1981
2100
  route(ctx, 'GET', '/api/qqbot-settings/local-model/status', async (req, res) => {
1982
2101
  try {
1983
2102
  const u = new URL(req.url ?? '/', 'http://x');
@@ -3672,6 +3791,8 @@ const WHY_MAP = { busy: '目标会话回合活跃,已等待至回合结束仍超
3672
3791
  const { bots } = parsePatch();
3673
3792
  const out = [];
3674
3793
  for (const bot of bots) {
3794
+ // ⚠️ 2026-09-28:凭据可能只在【自有存储】里(2026-09-24 起的保存策略),合并后再判断。
3795
+ bot.cfg = mergeOwnIntoCfg(bot.cfg || {}, bot.cfg?.settingsNs || bot.id);
3675
3796
  if (bot.disabled || !bot.cfg?.appId || !bot.cfg?.appSecret) continue;
3676
3797
  try {
3677
3798
  const ns = bot.id;