@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 +40 -0
- package/client/qqbot-settings.js +3 -1
- package/dist/shared/utils.d.ts.map +1 -1
- package/dist/shared/utils.js +3 -1
- package/dist/shared/utils.js.map +1 -1
- package/package.json +4 -1
- package/settings-host.js +443 -9
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)。
|
package/client/qqbot-settings.js
CHANGED
|
@@ -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 ? ' (
|
|
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;
|
|
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"}
|
package/dist/shared/utils.js
CHANGED
|
@@ -76,7 +76,9 @@ export function getProfileDir(baseDir = PLUGIN_ROOT) {
|
|
|
76
76
|
}
|
|
77
77
|
/** 遍历 ~/.dsh/profiles/<name>,返回装着本插件的 profile 根目录(找不到返回 null) */
|
|
78
78
|
function scanDshProfiles() {
|
|
79
|
-
|
|
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);
|
package/dist/shared/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE7D,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AACtC,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAEhD,uCAAuC;AACvC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;AAErC,+BAA+B;AAC/B,MAAM,cAAc,GAAG,iBAAiB,EAAE,CAAC;AAE3C,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAsB,CAAC;QACxG,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB;IACxB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;QACrD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAyB,CAAC;QAC9E,OAAO,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,aAAa,cAAc,UAAU,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC;AACzF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB,WAAW;IACzD,UAAU;IACV,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9D,IAAI,QAAQ,IAAI,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAEtD,oDAAoD;IACpD,MAAM,OAAO,GAAG,eAAe,EAAE,CAAC;IAClC,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE5B,kCAAkC;IAClC,IAAI,GAAG,GAAG,OAAO,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,cAAc;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,iEAAiE;AACjE,SAAS,eAAe;IACtB,MAAM,
|
|
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.
|
|
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
|
-
/**
|
|
59
|
-
const
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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 ?? '');
|