Skip to content

Releases: zaofan-make/dsh-qqbot

v1.10.46 · 远程访问修复 + 可搜索下拉

Choose a tag to compare

@zaofan-make zaofan-make released this 25 Oct 16:45

v1.10.46

这次两件事:远程访问终于能用了,以及扩展设置页支持可搜索下拉。


远程访问设置页不再 403(#22)

用 Caddy / Nginx 反代、从另一台机器打开设置页时,之前所有请求都会被拦成:

{"error":"refused: same-origin loopback only"}

原因是插件的同源校验只认 loopback,而反代会保留原始 Host(如 example.com:8443),所以一律判成不可信 —— 但 dsh 宿主本身早就通过 --trusted-host 放行了,两边策略不一致。

现在 isTrusted() 改成 loopback ∪ 宿主 --trusted-host 白名单,白名单直接从 process.argv 解析(插件与 dsh web 同进程,不需要新增任何配置项)。

兼容两种写法:

dsh web --host 127.0.0.1 --port 3080 --trusted-host 127.0.0.1:3080 example.com:8443
dsh web --trusted-host=example.com

带端口精确匹配,不带端口按 hostname 匹配(管任意端口)。

安全检查一项没松:sec-fetch-site: cross-site 仍然拒绝,Origin 与 Host 的一致性校验仍然严格。

另外 /_debug 接口现在会返回诊断字段,下次遇到 403 直接打开看一眼就知道是白名单空着还是 Host 对不上:

{ "trustedHosts": ["127.0.0.1:3080"], "hostHeader": "example.com:8443", "hostTrusted": false }

扩展设置页支持 searchable(可搜索下拉)

select 加一个 searchable: true,会渲染成 <input list="…"> + <datalist> —— 浏览器原生的输入过滤,而且允许手填列表外的值。

选项几十上百条时(比如音色、模型、字体这类)原生 <select> 根本没法找,现在直接打字筛。

默认不启用,只有显式写 searchable: true 才走这条路,其余下拉的样式和行为完全不变。

{ key: 'defaultSpeaker', type: 'select', searchable: true, label: '默认音色',
  options: ['柔骨魂师 2.0|ICL_uranus_zh_female_rouguhunshi_tob', ...] }

顺手补了两个坑的文档

契约文档 docs/extension-contract.md 新增「扩展设置页的字段类型」一节,写清 bool/number/string/list/select 各自的行为,以及两个必须注意的地方:

  1. select 取到的值永远是字符串 —— '24000' 不是 24000。传给要求严格类型的第三方接口前自己转数字
  2. 复合值(中文名|id)读取侧要负责剥离 —— 否则整串会被当真实值用出去

安装

npm i @zaofan/dsh-qqbot@1.10.46

或者从插件市场更新(市场面板会显示版本号对照)。

v1.10.44 — 扩展能力补齐:wakeTo 可用 + 面板版本号 + 包声明最低插件版本

Choose a tag to compare

@zaofan-make zaofan-make released this 25 Oct 15:38

这次的核心是把扩展能力补齐并暴露出来 —— 面板能看版本、扩展包能声明依赖、扩展可以点名唤醒任意会话。


✨ 新增

1. 面板显示插件版本号

扩展设置页的「路径」区下方新增一行:

工作区 /workspace
内置目录 C:\Users\xxx\.dsh\profiles\web\node_modules\@zaofan\dsh-qqbot
扩展包目录 C:\Users\xxx\.dsh\qqbot-settings\extension-packs
插件版本   v1.10.44          ← 新增

以前排查问题只能靠 npm/github 记录猜用户装的哪一版,现在一眼可见。

2. 扩展包可声明「最低插件版本」

包的 qqbot-pack.json 里可以写:

{
  "name": "主动陪伴与私聊关怀",
  "version": "1.0.6",
  "minPluginVersion": "1.10.44"
}

面板会拿它跟当前装的插件版本自动对照:

情况 显示
满足 灰字「需要插件 v1.10.44」
不满足 红字「⚠ 需要插件 ≥ v1.10.44(当前 v1.10.30)」
没写 不显示、不检查

市场卡片和扩展总览两处都有。这样扩展作者用上新能力时可以声明依赖,用户装之前就能看出不兼容,而不是运行期撞 undefined。

3. caps.wakeTo 正式可用(所有扩展点,不再只是钩子)

const ok = await env.caps.wakeTo('c2c', openid, '(系统提示,不是主人说的话)…')
if (!ok) { /* 回退到自己的方案 */ }
  • 不需要「当前会话」,直接点名唤醒某个群/私聊里的 AI
  • 走正常回合:AI 带着该会话的历史与人设自己作答,回复由宿主发到 QQ
  • 会话被回收 / 宿主重启后活跃表为空时,会自动 getOrCreate 重建再注入
  • 返回 true=已排进该会话的回合;false=没送达(不抛错,调用方自己决定回退)
  • ⚠️ true 只代表已排队,不等于 QQ 一定送达

这条是为「主动陪伴」这类扩展准备的:以前只能直发预置文案(死文本、不带人格),现在能让 AI 自己开口。

4. 钩子能拿到身份信息

hooks 的 env 里现在有 appId / appSecret / owners / wakeTo,和命令侧对齐。以前钩子想自己调接口换 token 拿不到密钥,想判主人拿不到白名单。


🐛 修复

caps.wakeTo 在部分扩展点恒返回 false

makeExtCaps() 只消费已知的 deps 字段,而 cells 的构造有六处(命令/钩子/工具/botplay/inbound/outbound)。逐个注入必然漏 —— 实测 tools 和 commands 都漏了,扩展拿到的是个永远返回 false 的函数,看起来像"宿主不支持",实际是能力没接上。

改成两级兜底:先看 deps.wakeTo,没有就动态 import session-registry 自己找 manager。会话不在活跃表里时用 getOrCreateByPeerWeb 重建(只在唯一实例时自动建,多实例下 openid 按应用隔离,乱建会串号)。

账号设置「点两次保存,密码变回 yml 里的旧值」

/accounts/save 回填 secret 时优先取了 cordis.patch.yml 里的值(那是 9-24 的旧密码),把用户新填的覆盖了。改成自有存储优先,patch 只作最后兜底;同时 POST /api/qqbot-settings/update 的响应里把 secret 打码,不再回显明文。

启动失败不再无声

新增 {DSH_HOME}/qqbot-boot-diag.log(需 diagLog: true),记录每个实例的 appId / secret 尾号 / starting bot / bot ready / bot error。

这次就是靠它定位到 im-qqbot-3 报 {"code":100016,"message":"invalid appid or secret"} —— 而 im-qqbot-2 正常 ready。


📝 文档

docs/extension-contract.md 更新,并同步到插件市场仓库(dsh-qqbot-plugins/docs/),扩展作者不用翻插件源码:

  • caps.wakeTo 语义订正(原写「钩子专用」,实际所有扩展点都能用)+ 会话回收会自动重建
  • 新增「扩展包声明最低插件版本」
  • 新增「主动唤醒别人的会话:怎么写 prompt」—— 自报家门 / 说清要它做什么 / 禁止项,并提醒唤醒可能被价值评分拦掉(纯打招呼容易判低价值)

⚠️ 已知问题

  • im-qqbot-3 的凭证无效(appId 1905708994,secret 尾号 YDta),需要换密钥或先禁用该实例。且同一 appId 被桌面端 profile 的 im-qqbot-2 占用,存在跨 profile 冲突
  • 同名扩展文件同时存在于内置目录和扩展包目录时,整个 bot 实例会启动失败(连 starting bot 都不打印),而不是警告跳过 —— 待改
  • tools_reload 只重扫 .qqbot-extensions,不扫 extension-packs/,改了包必须重启宿主

完整变更:v1.10.43...v1.10.44 · npm:@zaofan/dsh-qqbot@1.10.44

v1.10.43 — 扩展身份链路修复

Choose a tag to compare

@zaofan-make zaofan-make released this 25 Oct 09:18

重点:扩展身份链路三连修

如果你写扩展时遇到 「仅主人可用」但发的人明明是主人,这个版本修好了。

三个独立缺陷(都是我们的锅)

# 症状 根因
1 env.owners 恒为 [] 读 config.owners(不存在),实际在 config.groupAdmin.owners
2 发送者 openid 拿不到 读自造的 mc.senderId,官方字段是 author.member_openid / user_openid / id
3 caps.actorOpenid 恒 undefined makeExtCaps 内部算了却没放进返回值

现在承诺什么有什么

env.user.openid     // 发送者 openid
env.user.name       // 昵称
env.user.isOwner    // ★ 已算好布尔值,直接 if (!env.user.isOwner) return
env.isOwner         // 顶层便捷布尔
env.owners          // 白名单数组
env.actorOpenid     // 平铺 openid
env.peer.scope      // group / c2c
env.peer.peerId     // 群 openid / 用户 openid
env.config.appId    // 想自己换 token 调接口时用
env.config.appSecret
env.config.owners

实测验证

/probe 自检命令输出:

ctx.user = { openid: "E9020753…", name: "做早饭", isOwner: true }
ctx.owners = ["E9020753…"]
ctx.caps.actorOpenid = "E9020753…"

/companion 状态面板正常、/companion test 演练成功。

另外

  • 契约文档 docs/extension-contract.md 已按实测重写身份章节 —— 之前那份写的是「设想」,现在是「事实」
  • 市场安装器的下载源改成多源兜底(gh-proxy → raw → ghproxy → jsdelivr),修了「删了重下还是旧代码」

完整改动:git log 1.10.38..v1.10.43

安装:npm i @zaofan/dsh-qqbot@1.10.43

v1.10.38 — 兼容 ctx.args / rawArgs

Choose a tag to compare

@zaofan-make zaofan-make released this 25 Oct 00:02

扩展命令的 ctx 补上 args / rawArgs 兼容别名

起因:proactive-companion 这类扩展命令按 ctx.args || ctx.rawArgs 取参数,
而 dsh 的扩展命令 ctx 里从来没有这两个字段(只有 ctx.command.args / .raw)。

结果它们的子命令全部失效——/companion test 和 /companion 输出一模一样。

与其要求每个作者都改代码,不如我们把别名补上。

改动

const handlerArg = {
  ...ctx,
  command: parsed,
  args:    parsed.raw,   // ← 新增,兼容别名
  rawArgs: parsed.raw,   // ← 新增,兼容别名
};
  • 值统一是「命令名之后、未切分的原始字符串」,与 ctx.command.raw 同源
  • 按钮触发的命令(bootstrap.ts)同步补空串,保持一致
  • 我们自己的命令一律走 ctx.command.args / .raw,不受影响(已 grep 确认无冲突)

效果

装完这一版,proactive-companion 的 /companion test、set 不用改一行代码就能用了。


npm i @zaofan/dsh-qqbot@1.10.38

后端改动,装完需重启引擎。

v1.10.37 — 市场卡片加「作者说明 · README」跳转

Choose a tag to compare

@zaofan-make zaofan-make released this 24 Oct 23:32

市场卡片加「作者说明 · README ↗」

需求:想直接点进 GitHub 上那个包的文件夹,看作者自己写的 README。

效果

每张市场卡片底部(标签行下面)多一个链接:

[插件名] v1.0.3  by 作者            [安装]
插件描述…
标签 标签 标签
作者说明 · README ↗

点开直接到:

https://github.com/zaofan-make/dsh-qqbot-plugins/tree/main/packs/<包id>

地址优先级

  1. manifest 里的 homepage / repo —— 如果指向作者自己的仓库,就用它
  2. 否则 → 市场仓库的 packs/<id>/

兼容处理

  • target=_blank + rel=noreferrer
  • DSH 的 Electron 壳里 _blank 有时被拦,加 onClick 兜底 window.open

纯前端,刷新浏览器即生效。


npm i @zaofan/dsh-qqbot@1.10.37

v1.10.36 — 修 AppSecret 被 patch 旧值顶回去

Choose a tag to compare

@zaofan-make zaofan-make released this 24 Oct 18:15

修:保存一次 AppSecret 就被旧值顶回去

现象(desktop 实测):设置页填好保存 → 成功;再按一次保存 → 密钥变成另一个历史值,怎么存都被顶回去。

根因(两处叠加)

① /read 把 appSecret 明文回给前端

const value = Object.assign({}, base.value || {}, own);
  • base 来自 cordis.patch.yml —— 历史旧值(2026-09-24 起已不再写它)
  • own 是自有存储(用户真正保存的那份)

一旦 own 里缺 appSecret,patch 的旧值就露出来。前端把它当当前值,
下次保存原样提交回来 → 盖掉用户存的那份 → 死循环。

② 自有存储缺 appSecret 时一直回退到 patch

mergeOwnIntoCfg 每次都从 patch 兜底,所以 patch 被写脏一次,之后就永远顶回去。

修法

位置 改动
SECRET_MASK 从 /accounts 路由内提到模块级(/read 也要用)
/read value.appSecret 一律抹成掩码(前端早有「掩码/空 = 保留原值」逻辑)
mergeOwnIntoCfg patch 的 appSecret 改成只继承一次(迁移进自有存储后落盘),之后永远以自有存储为准

这样 patch 再被写脏也影响不到用户存的那份。


npm i @zaofan/dsh-qqbot@1.10.36

改的是后端,装完要重启引擎。

v1.10.35 — 更正扩展工具参数契约(parameters 是合法别名)

Choose a tag to compare

@zaofan-make zaofan-make released this 24 Oct 17:35

更正:扩展工具参数契约

审 PR #2 时我断言「dsh 不认 parameters 字段」,这是错的,而且我把 README 里一句臆测的报错当成实测结论引用了两次。

查源码纠正(src/features/extension-store.ts):

// :216  只有 name + run 是硬要求
if (!name || typeof obj?.run !== 'function') { ... return null }

// :225  parameters 是兼容别名
inputSchema: obj.inputSchema ?? obj.parameters ?? {},

所以真相是

写法 结果
inputSchema: { action: {...} } ✅ 正确
parameters: { action: {...} } ✅ 也对(别名)
任一字段包成 { type:'object', properties:{...} } ❌ 真问题

第三种为什么坏:dsh 取到整个对象后,拿它的顶层键当参数名 → 模型看到的是 type / properties,真正的 action 藏在 properties 里看不见。

README 改了什么

  • 删掉那句没有实测支撑的 unsupported JSON schema: parameters.type must be a value schema object
  • 改成两个 ✅ + 一个 ❌,并写清「参数表错位」这个真实后果
  • 附上源码行号,下次不用再猜

v1.10.34 — 按手册正解:读宿主给的 profile 名(撤回多余的目录猜测)

Choose a tag to compare

@zaofan-make zaofan-make released this 24 Oct 16:38

按维护手册的正解改(我上一版想复杂了)

手册 §「profile 定位的正解:回写环境变量,别改推断逻辑」写得很清楚(v1.10.22 定的):

// src/index.ts 的 apply() 已经把宿主的 profileContext 回写成:
process.env.DSH_PROFILE           = "desktop"
process.env.DSH_PROFILE_DIR       = "...\\profiles\\desktop"
globalThis.__dshProfileName       = "desktop"
globalThis.__dshProfileContext    = { name, dir, patchPath }

settings-host.js 只要读它,不要自己推断路径。

真正的 bug 只有一个

const NS_PROFILE_DIR = resolveProfileName() || '_default';   // 模块顶层求值

它在 apply() 回写之前就求值了 → 永远落 _default。惰性化即可。

上一版(1.10.33)我加的东西是多余的,已删

  • 路径正则 /profiles/<name>/
  • 扫 qqbot-settings/*/ 目录
  • 按 ns 命中哪个目录

这些都是「猜」,而宿主早就把答案告诉我们了。

顺带纠正一个错误说法

我上一条说「desktop 和 web 配了同一个 settingsNs 会冲突,建议换 ns」—— 这是错的。

ownSettingsDir() 一直按 profile 分目录:

qqbot-settings/web/im-qqbot-2.json       ← Web 版
qqbot-settings/desktop/im-qqbot-2.json   ← Desktop 版

同一 ns 在两个 profile 就是两份独立文件,不冲突。 不用换 ns。


npm i @zaofan/dsh-qqbot@1.10.34

v1.10.33 — 多 profile 并存时按 ns 定位存档目录

Choose a tag to compare

@zaofan-make zaofan-make released this 24 Oct 16:35

补:同时装了 Desktop 和 Web 版怎么办

两个 profile 本来就是各存各的目录,天然隔离:

qqbot-settings/
  web/im-qqbot-2.json       ← Web 版
  desktop/im-qqbot-2.json   ← Desktop 版

区分靠 PROFILE_ROOT:Web 走 DSH_PROFILE 环境变量,Desktop 走 .../profiles/desktop。

补的那层

1.10.32 的兜底是「只认唯一子目录」,两个目录并存时不认 —— 会退回 _default。
这个版本补第 5 层:多个 profile 目录并存时,看哪个目录里真的存着这个 ns 的配置。

实测(本机同时装了 web + desktop):

ns=im-qqbot-2  → desktop 和 web 都有 → 不唯一 → 保守回 _default(不串)
ns=im-qqbot    → 只有 web 有     → 返回 web ✅

真正的坑在别处

如果你的两个 profile 配了同一个 settingsNs,那才是真冲突 —— 但那个场景
有 <ns>.writer.json 的 NS_CONFLICT 拦着,不会互相覆盖。

解药:给其中一个 profile 换 ns(如 im-qqbot-2-desktop),
或删掉 <ns>.writer.json 让当前 profile 接管。


npm i @zaofan/dsh-qqbot@1.10.33

v1.10.32 — 修 Desktop 宿主下 profile 名解析(面板全空白)

Choose a tag to compare

@zaofan-make zaofan-make released this 24 Oct 16:32

issue #20:Desktop 宿主下面板全空白

现象:DSH Desktop 宿主 + 非主实例(im-qqbot-2)时,
bot 收发消息完全正常,但评分 / 熟识度 / 群管理 / 图库 / Excel 导出全部空白。

GET /api/qqbot-settings/value-scores?ns=im-qqbot-2
→ {"ok":true,"items":[],"total":0}

而 value-scores.jsonl 明明在写。

根因链

// settings-host.js
const NS_PROFILE_DIR = resolveProfileName() || '_default';   // ← 模块顶层求值
层 问题
① DSH_PROFILE 只在每次 shell/工具调用时临时注入,模块加载那一刻是空的
② 兜底「从 PROFILE_ROOT 目录名反推」在该场景也判不出来 → 落成 _default
③ 而且它是 const,算错一次就永远不重算

于是读 qqbot-settings/_default/im-qqbot-2.json(不存在),真实存档在
qqbot-settings/desktop/im-qqbot-2.json → readOwnSettings() 返回 {} →
nsBot() 拿不到 appId/appSecret 直接 return null → 所有依赖它的面板全空白。

迷惑点:bot 收发消息完全正常,极易被误判成「评分没跑起来」。

修法

① resolveProfileName() 补第 4 层兜底 —— 直接看存档目录的实际形状

凭据就存在 qqbot-settings/<profile>/<ns>.json,
如果那里恰好只有一个真实 profile 目录,那它就是当前 profile,猜都不用猜。

⚠️ 只认「唯一」,不做 desktop/web 优先猜测 ——
一台机器上可能同时有这两个目录,猜错会让 web 用户读到 desktop 的凭据,比原来更糟。

② NS_PROFILE_DIR 常量 → nsProfileDir() 惰性函数

改成每次调用时现算。开销是几次字符串/目录读(API 请求级),可忽略;
换来的是「改完配置重启后下一次请求就用新值」,不再被模块加载期的空值钉死。


npm i @zaofan/dsh-qqbot@1.10.32

改的是后端(settings-host.js),装完要重启引擎。