@zaofan/dsh-qqbot 1.5.5 → 1.5.7

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
@@ -194,6 +194,46 @@ pwsh -File scripts/fix-dsh-016-presets.ps1 -Apply
194
194
 
195
195
  dsh 0.1.6 起默认模型为 `deepseek-official/deepseek-flash`(旧的 `deepseek-v4-flash` 已移出默认模型列表)。插件在「什么都没配」时的兜底也已同步为该值;你在设置里显式指定的 provider/model 优先级更高。
196
196
 
197
+ ### 升级 dsh 0.1.7 后,恢复会话报「预设缺失」/ 机器人不理人?
198
+
199
+ dsh **0.1.7** 把 Agent 预设从「目录里的 yml 文件」换成了「**profile 配置里的声明行**」:
200
+ 由 `@deepseek-ai/dsh-agent-preset-registry` + 每个预设一行 `@deepseek-ai/dsh-agent-preset` 组成。
201
+ 官方明确:**注册表不扫描目录、不接受 preset 路径、没有任何接口接受 YAML 写回** —— 所以旧目录预设**必须迁移**,否则会话日志里记着的 preset ID 找不到定义,**恢复会话会被拒绝**。
202
+
203
+ **一键迁移**(仓库里 `scripts/migrate-presets-017.ps1`):
204
+
205
+ ```powershell
206
+ # ① 先看看会迁移哪些预设(不写任何文件)
207
+ pwsh -File scripts/migrate-presets-017.ps1
208
+
209
+ # ② 生成「原 patch + 迁移片段」的合并版,核对一眼
210
+ pwsh -File scripts/migrate-presets-017.ps1 -Merge -Out "$env:TEMP\cordis.patch.yml.0.1.7-ready"
211
+
212
+ # ③ 升级 dsh 到 ≥0.1.7 之后启用(⚠️ 顺序不可反!0.1.6 读不了新声明会加载失败)
213
+ cd ~/.dsh/profiles/web
214
+ Copy-Item cordis.patch.yml cordis.patch.yml.bak-0.1.6
215
+ Move-Item "$env:TEMP\cordis.patch.yml.0.1.7-ready" cordis.patch.yml -Force
216
+ # 然后重启 dsh
217
+ ```
218
+
219
+ > ⚠️ 两个坑(脚本已自动处理):patch 顶层若是空数组 `[]` 要**先删掉**再粘;新增条目必须用 `- insert:`(写成 `- id:` 会报 `entry not found`)。
220
+ > ⚠️ 0.1.7 会把**会话日志升级为 V4(不可回退)**、`settings.yaml` 也只导入一次 —— 升级前记得备份整个 `~/.dsh`。
221
+
222
+ ### 升级 dsh 0.1.7 后,某些插件被「禁用 / 跳过」?
223
+
224
+ 0.1.7 新增了**插件版本兼容检查**(比对各插件的 `peerDependencies`)。钉死在旧次版本的包会被跳过,或整行被禁用:
225
+
226
+ ```
227
+ dsh: disabling profile plugin row "mcp-chrome": Plugin ... is incompatible with dsh 0.1.7-rc.1
228
+ ```
229
+
230
+ 常见两例:
231
+
232
+ - `@deepseek-ai/dsh-mcp-client@0.0.1-rc.1` → 让所有 `mcp-*` 行失效。**注意官方这个包的 `latest` 标签还停在旧版,`next` 才是新版** → 要显式装 `@deepseek-ai/dsh-mcp-client@0.1.7-rc.1`
233
+ - `@dhicoc/dsh-reverse-skill@1.0.5` → 跳过(作者尚未适配 0.1.7)
234
+
235
+ 想强行运行,可用官方提供的**确切版本例外**(`dsh plugin allow-version`,或插件管理页里授予),但官方警告"可能崩溃或数据丢失",自行权衡。
236
+
197
237
  ## 支持这个项目
198
238
 
199
239
  如果这个插件帮你省了 token、或者让你家的鲸鱼更活蹦乱跳 —— **给个 ⭐ Star** 就是最实在的支持;有 bug / 想要的功能,欢迎开 [Issue](https://github.com/gcry13067381632-jpg/dsh-qqbot/issues)。
@@ -1392,9 +1392,11 @@ window.__ModuleLoader__.load({
1392
1392
  }).then(function (r) { return r.json() }).then(function (d) {
1393
1393
  setBusy('')
1394
1394
  if (d && d.error) { setMsg('复制失败: ' + d.error); return }
1395
- setMsg('✅ 新预设已生成: ' + d.newId + (copy.fromStandard ? ' (复制自标准模式, 已带 QQ 工具)' : '') + '。去「打开文件夹」改人设吧。')
1395
+ setMsg('✅ 新预设已生成: ' + d.newId + (copy.fromStandard ? ' (复制自标准模式)' : '') + '。稍等片刻即可在列表看到;要改人设点「人格」。')
1396
1396
  setCopy(null); setCopyId(''); setCopyName('')
1397
1397
  load(true)
1398
+ // dsh 0.1.7:预设写在 profile patch 里,由宿主 HMR 重载后才出现在列表中 → 稍后再拉一次
1399
+ setTimeout(function () { load(true) }, 1200)
1398
1400
  }).catch(function (e) { setBusy(''); setMsg('复制异常: ' + e.message) })
1399
1401
  }
1400
1402
  function openPreset(id) {
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAuCA;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,MAAoB,GAAG,MAAM,GAAG,IAAI,CAkB1E;AAyCD;;GAEG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKtE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAOtD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAuCA;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,MAAoB,GAAG,MAAM,GAAG,IAAI,CAkB1E;AA2CD;;GAEG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKtE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAOtD"}
@@ -76,7 +76,9 @@ export function getProfileDir(baseDir = PLUGIN_ROOT) {
76
76
  }
77
77
  /** 遍历 ~/.dsh/profiles/<name>,返回装着本插件的 profile 根目录(找不到返回 null) */
78
78
  function scanDshProfiles() {
79
- const profilesRoot = join(os.homedir(), '.dsh', 'profiles');
79
+ // 与宿主保持一致: 优先环境变量 DSH_HOME, 否则回落 ~/.dsh
80
+ const dshHome = ((process.env.DSH_HOME ?? '').trim()) || join(os.homedir(), '.dsh');
81
+ const profilesRoot = join(dshHome, 'profiles');
80
82
  let entries;
81
83
  try {
82
84
  entries = readdirSync(profilesRoot);
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaofan/dsh-qqbot",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
4
4
  "description": "QQ Bot IM channel plugin for deepseek-harness (dsh)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -75,6 +75,9 @@
75
75
  "client": {
76
76
  "platform": "web",
77
77
  "inject": []
78
+ },
79
+ "engines": {
80
+ "dsh": "0.1.5-rc.2 || 0.1.6-alpha.2 || 0.1.7-rc.1"
78
81
  }
79
82
  },
80
83
  "dependencies": {
package/settings-host.js CHANGED
@@ -55,8 +55,12 @@ function resolveProfileRoot() {
55
55
  const PROFILE_ROOT = resolveProfileRoot();
56
56
  /** cordis.patch.yml = 宿主插件装配文件(账号实例声明处; 改它需重启 dsh 生效) */
57
57
  const PATCH_FILE = join(PROFILE_ROOT, 'cordis.patch.yml');
58
- /** agent 预设根目录(用户可写; 每预设一个子目录, 目录名=预设 id) */
59
- const PRESET_ROOT = join(homedir(), '.dsh', '.agent-presets');
58
+ /** dsh 家目录(与宿主保持一致: 优先环境变量 DSH_HOME, 否则回落 ~/.dsh) */
59
+ const DSH_HOME_DIR = ((process.env.DSH_HOME ?? '').trim()) || join(homedir(), '.dsh');
60
+ /** agent 预设根目录 —— ⚠️ 仅 ≤0.1.6 的**旧机制**(每预设一个子目录, 目录名=预设 id)。
61
+ * dsh 0.1.7 起预设改为「profile 配置里的声明行」(@deepseek-ai/dsh-agent-preset),
62
+ * 既没有目录、也没有写接口, 所以这里只作为旧版回落路径保留。 */
63
+ const PRESET_ROOT = join(DSH_HOME_DIR, '.agent-presets');
60
64
  /** 复制预设时排除的备份/压缩类文件(不把主人的隐私压缩包带进新预设) */
61
65
  const COPY_SKIP = /\.(rar|zip|7z|bak|tmp|log|diag)$/i;
62
66
  /** 预设 id 校验: 文件夹命名规范(字母数字开头, 允许 - _) */
@@ -93,10 +97,70 @@ async function readJsonBody(req) {
93
97
  }
94
98
 
95
99
  /** 读命名空间当前解析值 + revision(redact 防密钥外泄) */
100
+ /**
101
+ * 兜底:从 profile 的 cordis.patch.yml 里读某个 entry 的 config(2026-09-23 适配 dsh 0.1.7)。
102
+ *
103
+ * 背景:0.1.7 把设置机制换成了「从 profile 里插件条目的 config schema 推导」,
104
+ * 注册入口 `installSection` 已移除 → 插件静默降级后 `describe()` 里不再有本实例的 ns,
105
+ * 于是 /read 返回 {value:undefined,revision:undefined}(JSON 序列化后正好是 {}),
106
+ * 前端就显示"读取失败: {}"。
107
+ *
108
+ * 这里只做「块级 + 简单标量」提取(不整文件 YAML 解析,因为含 !!js 标签):
109
+ * 找 `- id: <entryId>` 块的 `config:` 子树,逐行取标量,跳过 !!js 与嵌套子块;
110
+ * secret 类字段按宿主 describe(redactSecrets) 的口径打码。
111
+ */
112
+ function readEntryConfigFromPatch(entryId) {
113
+ try {
114
+ if (!existsSync(PATCH_FILE)) return null;
115
+ const lines = readFileSync(PATCH_FILE, 'utf8').split(/\r\n|\n/);
116
+ const esc = String(entryId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
117
+ const headRe = new RegExp('^\\s*-\\s*id:\\s*[\'"]?' + esc + '[\'"]?\\s*$');
118
+ const start = lines.findIndex((l) => headRe.test(l));
119
+ if (start < 0) return null;
120
+ const headIndent = (lines[start].match(/^\s*/) || [''])[0].length;
121
+ let end = lines.length;
122
+ for (let i = start + 1; i < lines.length; i++) {
123
+ const ind = (lines[i].match(/^\s*/) || [''])[0].length;
124
+ if (/- /.test(lines[i]) && ind <= headIndent) { end = i; break; }
125
+ }
126
+ const block = lines.slice(start + 1, end);
127
+ const ci = block.findIndex((l) => /^\s*config:\s*$/.test(l));
128
+ if (ci < 0) return null;
129
+ const cfgIndent = (block[ci].match(/^\s*/) || [''])[0].length;
130
+ const out = {};
131
+ for (let i = ci + 1; i < block.length; i++) {
132
+ const raw = block[i];
133
+ if (!raw.trim()) continue;
134
+ const ind = (raw.match(/^\s*/) || [''])[0].length;
135
+ if (ind <= cfgIndent) break; // config 子树结束
136
+ if (ind > cfgIndent + 2) continue; // 嵌套子块/数组项 → 跳过
137
+ const m = /^\s*([A-Za-z0-9_-]+):\s*(.*)$/.exec(raw);
138
+ if (!m) continue;
139
+ let v = m[2].trim();
140
+ if (v === '' || v.startsWith('!!js')) continue; // 空值 / 表达式 → 跳过
141
+ v = v.replace(/^['"]|['"]$/g, '');
142
+ if (v === 'true') v = true;
143
+ else if (v === 'false') v = false;
144
+ else if (/^-?\d+$/.test(v)) v = Number(v);
145
+ out[m[1]] = v;
146
+ }
147
+ for (const k of Object.keys(out)) {
148
+ if (/secret|token|password|_key$/i.test(k)) out[k] = '__REDACTED__';
149
+ }
150
+ return Object.keys(out).length ? out : null;
151
+ } catch { return null; }
152
+ }
153
+ /** 读一个实例的配置视图;宿主 describe 拿不到时回落到 profile patch。 */
96
154
  function viewOf(settings, ns) {
97
155
  const target = ns || NS;
98
- const d = settings.describe({ redactSecrets: true }).find((x) => x.ns === target);
99
- return d ? { value: d.value, revision: d.revision } : { value: undefined, revision: undefined };
156
+ let list = [];
157
+ try { list = settings.describe({ redactSecrets: true }) || []; } catch { list = []; }
158
+ const d = list.find((x) => x.ns === target);
159
+ if (d) return { value: d.value, revision: d.revision };
160
+ const fb = readEntryConfigFromPatch(target);
161
+ // revision 用 -1 标记"兜底视图、没有乐观锁基线"(/update 见负数就跳过 expectedRevision)
162
+ if (fb) return { value: fb, revision: -1 };
163
+ return { value: undefined, revision: undefined };
100
164
  }
101
165
 
102
166
  /** 请求里的目录参数(dataDir 显式传; 空=primary 主账号) */
@@ -137,7 +201,9 @@ export function apply(ctx) {
137
201
  // ── 桥装载诊断(2026-09-10 排查路由全 404): 落 ~/.dsh/qqbot-bridge-diag.log ──
138
202
  // 用途: 宿主重启后可确认 apply 是否真的被 cordis 调用(以及当时 ctx 上有哪些服务)。
139
203
  try {
140
- const services = ['settings', 'webServer', 'agentPresets'].map((s) => s + '=' + (ctx && ctx[s] ? 'Y' : 'N')).join(' ');
204
+ // ⚠️ 未在 inject 声明的服务裸访问会抛 "without inject"(0.1.7 实测)→ 逐个 try 探测
205
+ const probe = (n) => { try { return (ctx && ctx[n]) ? 'Y' : 'N'; } catch { return 'NO-INJECT'; } };
206
+ const services = ['settings', 'webServer', 'agentPresets'].map((s) => s + '=' + probe(s)).join(' ');
141
207
  appendFileSync(
142
208
  join(homedir(), '.dsh', 'qqbot-bridge-diag.log'),
143
209
  `[${new Date().toISOString()}] bridge apply 被调用 inject=${JSON.stringify(inject)} ${services}\n`,
@@ -173,7 +239,10 @@ export function apply(ctx) {
173
239
  if (!body || typeof body !== 'object') return writeJson(res, 400, { error: 'body must be JSON object' });
174
240
  const ns = typeof body.ns === 'string' && body.ns ? body.ns : NS;
175
241
  try {
176
- await ctx.settings.update(ns, body.patch ?? {}, typeof body.expectedRevision === 'number' ? body.expectedRevision : undefined);
242
+ // revision = -1 是"兜底视图"标记(宿主 describe 里没有本 ns,如 dsh 0.1.7)
243
+ // → 没有可信基线,跳过乐观锁,否则保存必报 SETTINGS_CONFLICT
244
+ const exp = typeof body.expectedRevision === 'number' && body.expectedRevision >= 0 ? body.expectedRevision : undefined;
245
+ await ctx.settings.update(ns, body.patch ?? {}, exp);
177
246
  writeJson(res, 200, viewOf(ctx.settings, ns));
178
247
  } catch (e) {
179
248
  const code = e?.code ?? '';
@@ -670,12 +739,105 @@ export function apply(ctx) {
670
739
  }
671
740
  return out.sort((a, b) => b.mtime - a.mtime);
672
741
  }
742
+ /**
743
+ * 列出可用预设(双模式,2026-09-23 适配 dsh 0.1.7)。
744
+ * - dsh 0.1.7+:走宿主 `agentPresets.list()` —— 新版预设是 profile 配置里的
745
+ * 「声明行」(@deepseek-ai/dsh-agent-preset),**已无目录**,扫目录只会得到空列表;
746
+ * - dsh ≤0.1.6:回落 `scanPresets()` 扫 {DSH_HOME}/.agent-presets 目录(旧机制)。
747
+ * 统一返回 [{id, name, description, source}],source=registry|dir。
748
+ */
749
+ /**
750
+ * 安全取宿主 agentPresets 服务(2026-09-23 实测修正)。
751
+ * ⚠️ cordis 对**未在 inject 里声明**的服务, 裸访问 `ctx.xxx` 会直接抛
752
+ * "cannot get property \"agentPresets\" without inject"(0.1.7 上实测命中)。
753
+ * 而桥的 inject 只声明 settings/webServer(多声明会让宿主缺服务时整个 apply 不执行),
754
+ * 所以这里必须用 **ctx.get()**(不抛)探测,拿不到返回 null 走旧目录回落。
755
+ */
756
+ function getAgentPresets() {
757
+ try {
758
+ if (ctx && typeof ctx.get === 'function') {
759
+ const svc = ctx.get('agentPresets');
760
+ if (svc) return svc;
761
+ }
762
+ } catch { /* ignore */ }
763
+ try { return (ctx && ctx.agentPresets) || null; } catch { return null; }
764
+ }
765
+ async function listPresets() {
766
+ const ap = getAgentPresets();
767
+ if (ap && typeof ap.list === 'function') {
768
+ try {
769
+ const arr = await ap.list();
770
+ if (Array.isArray(arr) && arr.length) {
771
+ return arr
772
+ .map((p) => ({
773
+ id: String((p && p.id) || ''),
774
+ name: String((p && (p.name || p.id)) || ''),
775
+ description: String((p && p.description) || ''),
776
+ source: 'registry',
777
+ }))
778
+ .filter((p) => p.id);
779
+ }
780
+ } catch { /* registry 不可用 → 回落旧目录 */ }
781
+ }
782
+ return scanPresets().map((p) => ({
783
+ id: p.id,
784
+ name: p.name,
785
+ description: '',
786
+ source: 'dir',
787
+ mtime: p.mtime,
788
+ hasChannelTools: p.hasChannelTools,
789
+ }));
790
+ }
791
+ /**
792
+ * 写操作在新版下的统一提示(2026-09-23)。
793
+ * dsh 0.1.7+ 的注册表「不写入任何声明、没有任何接口接受 YAML 写回」,
794
+ * 新增/覆盖预设只能写 profile 的 bundle 补丁 —— 与其静默失败,不如给出明确指引。
795
+ * 返回 null 表示走旧版目录流程。
796
+ */
797
+ function presetWriteBlocked() {
798
+ const ap = getAgentPresets();
799
+ if (ap && typeof ap.list === 'function') {
800
+ return '当前 dsh 版本的预设是「profile 配置里的声明行」,不再是目录文件(官方注册表不接受任何写回)。'
801
+ + '请在 profile 的 cordis.patch.yml 里用 `- insert:` 添加/修改 `@deepseek-ai/dsh-agent-preset` 行'
802
+ + '(或用创造模式生成 bundle),保存后由 patchReload 热生效。';
803
+ }
804
+ return null;
805
+ }
673
806
  route(ctx, 'GET', '/api/qqbot-settings/presets', async (_req, res) => {
674
- writeJson(res, 200, { root: PRESET_ROOT, presets: scanPresets() });
807
+ try {
808
+ const presets = await listPresets();
809
+ const viaRegistry = presets.some((p) => p.source === 'registry');
810
+ writeJson(res, 200, { root: viaRegistry ? null : PRESET_ROOT, presets, mode: viaRegistry ? 'registry' : 'dir' });
811
+ } catch (e) {
812
+ writeJson(res, 500, { error: String((e && e.message) || e) });
813
+ }
675
814
  });
676
815
  // 复制并双名: {sourceId, newId, newName}
677
816
  route(ctx, 'POST', '/api/qqbot-settings/presets/copy', async (req, res) => {
678
817
  const body = await readJsonBody(req);
818
+ // dsh 0.1.7+:预设是 profile patch 里的声明行 → 复制 = 整段搬运 + 改写 id/name
819
+ if (getAgentPresets()) {
820
+ if (!body || typeof body.sourceId !== 'string' || typeof body.newId !== 'string') {
821
+ return writeJson(res, 400, { error: 'sourceId/newId 必填' });
822
+ }
823
+ const nId = body.newId.trim();
824
+ if (!ID_RE.test(nId)) return writeJson(res, 400, { error: '预设 id 只能字母/数字开头,含 - _' });
825
+ if (nId === String(body.sourceId).trim()) return writeJson(res, 400, { error: '新预设 id 不能与源相同' });
826
+ const nName = String(body.newName ?? nId).trim() || nId;
827
+ try {
828
+ const arr = await getAgentPresets().list();
829
+ if (Array.isArray(arr) && arr.some((x) => x && x.id === nId)) {
830
+ return writeJson(res, 409, { error: '预设 ' + nId + ' 已存在' });
831
+ }
832
+ } catch { /* 列表不可用时靠 patch 自身判断 */ }
833
+ const srcId = String(body.sourceId).trim();
834
+ // 先按 patch 里的原块整段复制(原汁原味、字节保真);
835
+ // patch 里没有(如宿主内置预设 standard)→ 回退用宿主文档读出组成再生成
836
+ let r = copyPresetDecl(srcId, nId, nName);
837
+ if (!r.ok) r = await copyPresetViaDocument(srcId, nId, nName);
838
+ if (!r.ok) return writeJson(res, 404, { error: r.error });
839
+ return writeJson(res, 200, { ok: true, newId: nId, backup: r.backup, note: '已写入 profile patch,热生效(新会话生效)' });
840
+ }
679
841
  if (!body || typeof body.sourceId !== 'string' || typeof body.newId !== 'string') {
680
842
  return writeJson(res, 400, { error: 'sourceId/newId 必填' });
681
843
  }
@@ -704,11 +866,26 @@ export function apply(ctx) {
704
866
  // 复用宿主原生 agentPresets.copy(from,id,name) —— 复制出的正是宿主当前标准版, 保证能跑; 无需自己拼组合。
705
867
  route(ctx, 'POST', '/api/qqbot-settings/presets/new', async (req, res) => {
706
868
  const body = await readJsonBody(req);
869
+ // dsh 0.1.7+:从 profile patch 里的 preset-standard 声明整段复制出一个新预设
870
+ if (getAgentPresets()) {
871
+ const nId = String(body?.id ?? '').trim();
872
+ const nName = String(body?.name ?? '').trim() || nId;
873
+ if (!ID_RE.test(nId)) return writeJson(res, 400, { error: '预设 id 只能字母/数字开头,含 - _' });
874
+ // standard 是**宿主内置**预设(不在用户 patch 里)→ 用宿主的预设文档读它的组成
875
+ let r = await copyPresetViaDocument('standard', nId, nName);
876
+ if (!r.ok) {
877
+ // 兜底:万一 patch 里正好也有一份 preset-standard,就直接整段复制
878
+ const local = copyPresetDecl('standard', nId, nName);
879
+ if (local.ok) r = local;
880
+ }
881
+ if (!r.ok) return writeJson(res, 500, { error: r.error });
882
+ return writeJson(res, 200, { ok: true, newId: nId, backup: r.backup, note: '已从 standard 生成新预设并写入 profile patch,热生效' });
883
+ }
707
884
  const newId = String(body?.id ?? '').trim();
708
885
  const newName = String(body?.name ?? '').trim() || undefined;
709
886
  if (!ID_RE.test(newId)) return writeJson(res, 400, { error: '预设 id 只能字母/数字开头,含 - _(也是文件夹名)' });
710
887
  try {
711
- const ap = (ctx).agentPresets;
888
+ const ap = getAgentPresets();
712
889
  if (!ap || typeof ap.copy !== 'function') {
713
890
  return writeJson(res, 500, { error: '宿主未提供 agentPresets 服务, 无法从标准模式新建' });
714
891
  }
@@ -725,6 +902,8 @@ export function apply(ctx) {
725
902
  });
726
903
  // 打开预设文件夹(本机弹资源管理器; 仅限 presetRoot 内白名单路径)
727
904
  route(ctx, 'POST', '/api/qqbot-settings/presets/open', async (req, res) => {
905
+ const blocked = presetWriteBlocked();
906
+ if (blocked) return writeJson(res, 501, { error: blocked });
728
907
  const body = await readJsonBody(req);
729
908
  const id = String(body?.id ?? '').trim();
730
909
  if (!ID_RE.test(id)) return writeJson(res, 400, { error: '非法的预设 id' });
@@ -742,8 +921,225 @@ export function apply(ctx) {
742
921
 
743
922
  // ── 预设人格文件浏览/编辑(2026-09-07): "展开改写人格"。安全: 仅 PRESET_ROOT/{id} 内白名单文件;
744
923
  // 写权限仅限"复制出来带QQ工具标记"的副本(防误改内置/半成品); 大小上限 200KB。
924
+ //
925
+ // ── 2026-09-24 适配 dsh 0.1.7:预设改为 profile patch 里的声明行后,目录没了,
926
+ // 但「人设正文」仍在 patch 内(persona 插件 config.prefix 的块标量)。
927
+ // 这里为它提供"虚拟文件"读写:只动那一段块标量,其余内容一个字节都不碰。
745
928
  const PRESET_EDIT_EXT = /.(yml|yaml|json|mjs|js|md|txt)$/i;
746
929
  const PRESET_EDIT_MAX = 200 * 1024;
930
+
931
+ /** 定位 profile patch 里某个预设声明块 `- id: preset-<id>`。 */
932
+ function locatePresetBlock(lines, presetId) {
933
+ const esc = String(presetId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
934
+ const re = new RegExp('^\\s*-\\s*id:\\s*[\'"]?preset-' + esc + '[\'"]?\\s*$');
935
+ const head = lines.findIndex((l) => re.test(l));
936
+ if (head < 0) return null;
937
+ const headIndent = (lines[head].match(/^\s*/) || [''])[0].length;
938
+ let end = lines.length;
939
+ for (let i = head + 1; i < lines.length; i++) {
940
+ const ind = (lines[i].match(/^\s*/) || [''])[0].length;
941
+ if (/- /.test(lines[i]) && ind <= headIndent) { end = i; break; }
942
+ }
943
+ return { head, headIndent, end };
944
+ }
945
+
946
+ /**
947
+ * 读预设的人设正文(0.1.7 路径)。
948
+ * 结构: preset 块 → `- id: persona` → `prefix|text|prompt: |-` → 块标量正文
949
+ * @returns {{text:string, indent:number, bodyStart:number, bodyEnd:number}|null}
950
+ */
951
+ function readPresetPersona(presetId, linesOverride) {
952
+ try {
953
+ if (!existsSync(PATCH_FILE)) return null;
954
+ const lines = linesOverride || readFileSync(PATCH_FILE, 'utf8').split(/\r\n|\n/);
955
+ const blk = locatePresetBlock(lines, presetId);
956
+ if (!blk) return null;
957
+ let pi = -1;
958
+ for (let i = blk.head + 1; i < blk.end; i++) {
959
+ if (/^\s*-\s*id:\s*['"]?persona['"]?\s*$/.test(lines[i])) { pi = i; break; }
960
+ }
961
+ if (pi < 0) return null;
962
+ const personaIndent = (lines[pi].match(/^\s*/) || [''])[0].length;
963
+ let ki = -1;
964
+ let indent = 0;
965
+ let inline = false;
966
+ let inlineVal = '';
967
+ for (let i = pi + 1; i < blk.end; i++) {
968
+ const l = lines[i];
969
+ if (/^\s*#/.test(l)) continue; // 注释行跳过
970
+ const mB = /^(\s*)(?:prefix|text|prompt):\s*\|-?\s*$/.exec(l);
971
+ if (mB) { ki = i; indent = mB[1].length; inline = false; break; }
972
+ const mI = /^(\s*)(?:prefix|text|prompt):\s+(\S.*)$/.exec(l); // 行内标量写法
973
+ if (mI) { ki = i; indent = mI[1].length; inline = true; inlineVal = mI[2].trim(); break; }
974
+ const ind = (l.match(/^\s*/) || [''])[0].length;
975
+ if (/- /.test(l) && ind <= personaIndent) break;
976
+ }
977
+ if (ki < 0) return null;
978
+ if (inline) {
979
+ // 行内写法(如 `prefix: 你是一个 xx 助手`)→ 直接返回那一句
980
+ let v = inlineVal;
981
+ if ((v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))) v = v.slice(1, -1);
982
+ return { text: v, indent: indent + 2, bodyStart: ki + 1, bodyEnd: ki + 1, inline: true, lineIndex: ki };
983
+ }
984
+ let bEnd = ki + 1;
985
+ while (bEnd < blk.end) {
986
+ const raw = lines[bEnd];
987
+ if (raw.trim() === '') { bEnd++; continue; }
988
+ if (((raw.match(/^\s*/) || [''])[0]).length <= indent) break;
989
+ bEnd++;
990
+ }
991
+ let last = bEnd;
992
+ while (last > ki + 1 && lines[last - 1].trim() === '') last--;
993
+ const body = lines.slice(ki + 1, last);
994
+ const cuts = body.filter((l) => l.trim()).map((l) => (l.match(/^\s*/) || [''])[0].length);
995
+ const cut = cuts.length ? Math.min(...cuts) : indent + 2;
996
+ // 纯空白行原样保留(既不切片也不补缩进),保证「读出 → 原样写回」字节级一致
997
+ const text = body.map((l) => (l.trim() === '' ? l : l.slice(cut))).join('\n');
998
+ return { text, indent: cut, bodyStart: ki + 1, bodyEnd: last };
999
+ } catch { return null; }
1000
+ }
1001
+
1002
+ /**
1003
+ * 复制一个预设声明(dsh 0.1.7 路径):在 profile patch 里把 `- id: preset-<src>` 整段
1004
+ * 复制一份,只改写三处后追加到顶层数组 —— `- id:` / `config.id` / `config.name`,其余一个字节不动。
1005
+ * 行内换行按原文保真,缩进原样搬运。
1006
+ */
1007
+ function copyPresetDecl(srcId, newId, newName) {
1008
+ try {
1009
+ if (!existsSync(PATCH_FILE)) return { ok: false, error: 'profile patch 不存在' };
1010
+ const raw = readFileSync(PATCH_FILE, 'utf8');
1011
+ const parts = raw.split(/(\r\n|\n)/);
1012
+ const lineTexts = [];
1013
+ const eols = [];
1014
+ for (let i = 0; i < parts.length; i++) {
1015
+ if (i % 2 === 0) lineTexts.push(parts[i]);
1016
+ else eols.push(parts[i]);
1017
+ }
1018
+ const blk = locatePresetBlock(lineTexts, srcId);
1019
+ if (!blk) return { ok: false, error: '在 profile patch 里找不到源预设 preset-' + srcId };
1020
+ const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1021
+ const headRe = new RegExp('id:\\s*[\'"]?preset-' + escRe(srcId) + '[\'"]?');
1022
+ const seg = lineTexts.slice(blk.head, blk.end);
1023
+ const out = [];
1024
+ let cfgIndent = -1;
1025
+ for (let i = 0; i < seg.length; i++) {
1026
+ let l = seg[i];
1027
+ const ind = (l.match(/^\s*/) || [''])[0].length;
1028
+ if (i === 0) {
1029
+ l = l.replace(headRe, 'id: preset-' + newId);
1030
+ } else {
1031
+ const mCfg = /^(\s*)config:\s*$/.exec(l);
1032
+ if (mCfg) { cfgIndent = mCfg[1].length; }
1033
+ else if (cfgIndent >= 0 && ind === cfgIndent + 2) {
1034
+ if (/^\s*id:\s/.test(l)) l = l.replace(/id:\s*.*$/, 'id: ' + newId);
1035
+ else if (/^\s*name:\s/.test(l)) l = l.replace(/name:\s*.*$/, 'name: ' + newName);
1036
+ }
1037
+ }
1038
+ out.push(l);
1039
+ }
1040
+ const eol = eols[blk.end - 1] || eols[blk.head] || '\n';
1041
+ let add = '';
1042
+ for (const l of out) add += l + eol;
1043
+ let text = raw;
1044
+ if (!/[\r\n]$/.test(text)) text += eol;
1045
+ text += add;
1046
+ const bak = PATCH_FILE + '.bak-copy-' + Date.now();
1047
+ try { writeFileSync(bak, raw, 'utf8'); } catch { /* 备份失败不阻塞 */ }
1048
+ writeFileSync(PATCH_FILE, text, 'utf8');
1049
+ return { ok: true, backup: bak, newId };
1050
+ } catch (e) { return { ok: false, error: String((e && e.message) || e) }; }
1051
+ }
1052
+
1053
+ /**
1054
+ * 用宿主的 `agentPresets.readDocument(id)` 读来源预设的组成,生成一条新的 preset 声明写进 patch。
1055
+ * 用途:**内置预设**(如 `standard`,它不在用户 patch 里)或任何 patch 中不存在的预设。
1056
+ * readDocument 官方标注"仅用于查看"——我们只把它当模板读出来,写进**自己的** profile patch。
1057
+ */
1058
+ async function copyPresetViaDocument(srcId, newId, newName) {
1059
+ const ap = getAgentPresets();
1060
+ if (!ap || typeof ap.readDocument !== 'function') {
1061
+ return { ok: false, error: '宿主未提供 agentPresets.readDocument(读不了内置预设的组成)' };
1062
+ }
1063
+ let doc;
1064
+ try { doc = await ap.readDocument(srcId); }
1065
+ catch (e) { return { ok: false, error: '读取源预设 ' + srcId + ' 失败:' + String((e && e.message) || e) }; }
1066
+ const content = String((doc && doc.content) || '');
1067
+ if (!content.trim()) return { ok: false, error: '源预设 ' + srcId + ' 的组成为空' };
1068
+ const pad = ' ';
1069
+ const head = [
1070
+ '# ── 复制自 ' + srcId + '(组成由宿主的预设文档读出)──',
1071
+ '- insert:',
1072
+ ' - id: preset-' + newId,
1073
+ " name: '@deepseek-ai/dsh-agent-preset'",
1074
+ ' config:',
1075
+ ' id: ' + newId,
1076
+ ' name: ' + newName,
1077
+ ' plugins:',
1078
+ ];
1079
+ const bodyLines = content.split(/\r\n|\n/).map((l) => (l.trim() === '' ? '' : pad + l));
1080
+ const raw = readFileSync(PATCH_FILE, 'utf8');
1081
+ const parts = raw.split(/(\r\n|\n)/);
1082
+ const eol = parts[1] ?? '\n';
1083
+ let text = raw;
1084
+ if (!/[\r\n]$/.test(text)) text += eol;
1085
+ text += head.join(eol) + eol + bodyLines.join(eol) + eol;
1086
+ const bak = PATCH_FILE + '.bak-newpreset-' + Date.now();
1087
+ try { writeFileSync(bak, raw, 'utf8'); } catch { /* 备份失败不阻塞 */ }
1088
+ writeFileSync(PATCH_FILE, text, 'utf8');
1089
+ return { ok: true, backup: bak, newId, from: 'document' };
1090
+ }
1091
+
1092
+ /** 写回人设正文(只替换那段块标量,写前自动备份整个 patch)。
1093
+ * ⚠️ 换行符**逐行保真**:patch 可能 LF/CRLF 混用(实测踩过),统一 eol 会改掉别处字节。 */
1094
+ function writePresetPersona(presetId, text) {
1095
+ try {
1096
+ if (!existsSync(PATCH_FILE)) return { ok: false, error: 'profile patch 不存在' };
1097
+ const raw = readFileSync(PATCH_FILE, 'utf8');
1098
+ const parts = raw.split(/(\r\n|\n)/);
1099
+ const lineTexts = [];
1100
+ const eols = [];
1101
+ for (let i = 0; i < parts.length; i++) {
1102
+ if (i % 2 === 0) lineTexts.push(parts[i]);
1103
+ else eols.push(parts[i]);
1104
+ }
1105
+ const found = readPresetPersona(presetId, lineTexts);
1106
+ if (!found) return { ok: false, error: '没找到该预设的人设段落(persona / prefix)' };
1107
+ let pad;
1108
+ let insertAt;
1109
+ let endAt;
1110
+ if (found.inline) {
1111
+ // 行内标量 → 升级成块标量(`prefix: |-` + 缩进正文),这样多行人设也存得下
1112
+ const keyRaw = lineTexts[found.lineIndex];
1113
+ const key = (/(prefix|text|prompt):/.exec(keyRaw) || [])[1] || 'prefix';
1114
+ const keyIndent = (keyRaw.match(/^\s*/) || [''])[0];
1115
+ lineTexts[found.lineIndex] = keyIndent + key + ': |-';
1116
+ pad = keyIndent + ' ';
1117
+ insertAt = found.lineIndex + 1;
1118
+ endAt = found.lineIndex + 1;
1119
+ } else {
1120
+ pad = ' '.repeat(found.indent);
1121
+ insertAt = found.bodyStart;
1122
+ endAt = found.bodyEnd;
1123
+ }
1124
+ const nl = eols[insertAt - 1] || '\n'; // 沿用原段落的换行风格
1125
+ const body = String(text).split(/\r\n|\n|\r/).map((l) => (l.trim() === '' ? l : pad + l));
1126
+ const head = lineTexts.slice(0, insertAt);
1127
+ const tail = lineTexts.slice(endAt);
1128
+ let out = '';
1129
+ for (let i = 0; i < head.length; i++) out += head[i] + (eols[i] ?? '\n');
1130
+ for (const l of body) out += l + nl;
1131
+ for (let i = 0; i < tail.length; i++) {
1132
+ out += tail[i];
1133
+ const ei = endAt + i;
1134
+ if (ei < eols.length) out += eols[ei];
1135
+ }
1136
+ const bak = PATCH_FILE + '.bak-persona-' + Date.now();
1137
+ try { writeFileSync(bak, raw, 'utf8'); } catch { /* 备份失败不阻塞 */ }
1138
+ writeFileSync(PATCH_FILE, out, 'utf8');
1139
+ return { ok: true, backup: bak };
1140
+ } catch (e) { return { ok: false, error: String((e && e.message) || e) }; }
1141
+ }
1142
+
747
1143
  function presetDirSafe(id) {
748
1144
  if (!ID_RE.test(String(id || ''))) return null;
749
1145
  const dir = resolve(PRESET_ROOT, id);
@@ -758,7 +1154,20 @@ export function apply(ctx) {
758
1154
  }
759
1155
  route(ctx, 'GET', '/api/qqbot-settings/presets/files', async (req, res) => {
760
1156
  const q = new URL(req.url, 'http://x').searchParams;
761
- const dir = presetDirSafe(q.get('id') || '');
1157
+ const rid = String(q.get('id') || '').trim();
1158
+ // dsh 0.1.7+:预设声明在 profile patch 里 → 对外虚拟成「人设正文」一个可编辑文件
1159
+ if (getAgentPresets()) {
1160
+ if (!ID_RE.test(rid)) return writeJson(res, 400, { error: '非法的预设 id' });
1161
+ const persona = readPresetPersona(rid);
1162
+ if (!persona) return writeJson(res, 404, { error: '该预设没有内联人设段落(persona / prefix);人格可能在外部 .mjs 文件里' });
1163
+ return writeJson(res, 200, {
1164
+ id: rid,
1165
+ dir: '(profile patch)',
1166
+ writable: true,
1167
+ files: [{ name: 'persona(人设正文)', size: Buffer.byteLength(persona.text, 'utf8'), mtime: 0 }],
1168
+ });
1169
+ }
1170
+ const dir = presetDirSafe(rid);
762
1171
  if (!dir) return writeJson(res, 404, { error: '预设不存在' });
763
1172
  try {
764
1173
  const files = readdirSync(dir, { withFileTypes: true })
@@ -769,6 +1178,31 @@ export function apply(ctx) {
769
1178
  } catch (e) { writeJson(res, 500, { error: String(e?.message ?? e) }); }
770
1179
  });
771
1180
  route(ctx, ['GET', 'PUT'], '/api/qqbot-settings/presets/file', async (req, res) => {
1181
+ // dsh 0.1.7+:只支持「人设正文」这一个虚拟文件的读写(落在 profile patch 的 persona 块标量里)
1182
+ if (getAgentPresets()) {
1183
+ if (req.method === 'PUT') {
1184
+ const body = await readJsonBody(req);
1185
+ const id = String(body?.id ?? '').trim();
1186
+ if (!ID_RE.test(id)) return writeJson(res, 400, { error: '非法的预设 id' });
1187
+ const content = String(body?.content ?? '');
1188
+ if (Buffer.byteLength(content, 'utf8') > PRESET_EDIT_MAX) return writeJson(res, 400, { error: '内容过大(上限 200KB)' });
1189
+ const w = writePresetPersona(id, content);
1190
+ if (!w.ok) return writeJson(res, 500, { error: w.error });
1191
+ return writeJson(res, 200, {
1192
+ ok: true,
1193
+ name: 'persona(人设正文)',
1194
+ size: Buffer.byteLength(content, 'utf8'),
1195
+ backup: w.backup,
1196
+ note: '已写入 profile patch,patchReload 热生效(新会话生效)',
1197
+ });
1198
+ }
1199
+ const q = new URL(req.url, 'http://x').searchParams;
1200
+ const id = String(q.get('id') || '').trim();
1201
+ if (!ID_RE.test(id)) return writeJson(res, 400, { error: '非法的预设 id' });
1202
+ const persona = readPresetPersona(id);
1203
+ if (!persona) return writeJson(res, 404, { error: '该预设没有内联人设段落(persona / prefix)' });
1204
+ return writeJson(res, 200, { id, name: 'persona(人设正文)', content: persona.text, writable: true });
1205
+ }
772
1206
  if (req.method === 'PUT') {
773
1207
  const body = await readJsonBody(req);
774
1208
  const id = String(body?.id ?? '');