← Back to home@arrow1031-dev

dsh-qq-bot

把 QQ 接到 DSH Agent 的插件(社区化维护):经 NapCat / OneBot 11 收发消息,每个 QQ 会话一个 DSH Agent。

Stars
0
Language
JavaScript
Created
Sep 20, 2026
Updated
Oct 26, 2026

Introduction

dsh-qq-bot —— 把 QQ 接到 DSH Agent

当前版本 0.11.x:要求 DSH 0.2.0-rc.2 或更新的 0.2.x 版本(工具走 ctx.get('tools') 注入契约;DSH 0.2.1 系列 rc.2 / rc.3 已实测适配)。

⚠️ 设置页入口变了:不再是「设置 → QQ 机器人」,改为在首页侧边栏打开 插件 → 已安装 → dsh-qq-bot。 配置项与落点(profile 的 cordis.patch.yml)都没变,现有配置无需迁移。 0.10 起一套插件接五种协议:OneBot 11(默认)/ OneBot 12 / Satori / Milky / QQ 官方机器人,见「三、协议与连接方式」。 本仓库就是维护主干 arrow1031/dsh-qq-bot,已解除封版: 安装跟随 main、功能与修复走 PR。需要兼容 DSH 0.1.6 及更早的旧版本(0.6.x)请自行选择早期 tag 下载; 早期版本同时想用新功能,请自行处理。

维护者 @arrow1031 · 核心贡献者 @nd002723 · 许可 MIT · AI 生成声明 AI-DISCLOSURE.md · 更新日志 CHANGELOG.md

⚠️ 行为约定(0.6.8 起沿用,0.8.x 不变):bot 不自动附带聊天上下文

被 @ / 回复 bot 时,不再自动把群内「最近相关发言」硬塞进会话上下文(省 token、上下文更精简)。 需要结合群内近期消息/图片作答时,Agent 会主动调 qq_history 工具拉取(见 qq_history 小节)。 可感知影响:bot 对群内前文的「记忆力」取决于模型是否自觉调工具;若群成员觉得 bot 变迟钝,这是预期行为而非故障。

一个可以装进 DSH profile 的插件:QQ 消息进来 → 交给 DSH Agent → 回复发回 QQ。

复用现成生态,不自造协议:

QQ 客户端 / Koishi / QQ 开放平台
   │ 各家协议(OneBot 11/12、Satori、Milky、官方 API v2)
   ▼
lib/adapters/*.mjs(协议翻译层,零 npm 依赖,独立进程)
   │ stdin/stdout 换行分隔 JSON(契约见 lib/bridge/CONTRACT.md)
   ▼
lib/bridge/(中间表示:一套消息段 + 一套 message 帧)
   ▼
lib/index.js(Cordis 插件入口)
   ├ lib/config.js         Config schema / 默认值 / 深合并
   ├ lib/session.js        建会话、固定命名、访客工具闸
   ├ lib/tools.js          qqbot / qqconfig / qqgroup / qq_websearch / qq_webfetch / qq_imgsearch / qq_draw / qq_history
   ├ lib/admin.js          指令解析与三路权限
   ├ lib/owner-settings.js 拥有者设置通道(白名单 + 两步确认 + 限流 + 审计)
   ├ lib/workspace.js      QQ_msg 工作区根解析
   ├ lib/http.js           fetch + 代理隧道 + 公网地址校验
   ├ lib/websearch.js      多引擎搜索
   ├ lib/imgsearch.js      图床 + 反搜引擎
   ├ lib/imagegen.js       生图(qq_draw)
   └ lib/moli.js           dsh-moli 渲染抓取(可选)
   ▲ 设置面板读写(lib/client.js)  ← 插件面板「插件 → 已安装 → dsh-qq-bot」
   │ sessionController → agent.followup → whenIdle
   ▼
DSH Agent(默认模型 + preset 人格 + 工具)

一、已验证的东西(不是设计稿)

全部在本机实跑:

测试命令结果
OneBot 11 正向适配器node test/test-adapter.mjs19/19
OneBot 11 反向适配器node test/test-adapter-reverse.mjs12/12
OneBot 12 适配器node test/test-adapter-onebot12.mjs30/30
Satori 适配器node test/test-adapter-satori.mjs70/70
Milky 适配器node test/test-adapter-milky.mjs79/79
QQ 官方适配器node test/test-adapter-qq-official.mjs87/87
打包后插件端到端(Host)node test/test-plugin.mjs76/76
客户端设置页node test/test-client.mjs80/80
权限 · 群管理 · 访客工具闸node test/test-admin.mjs32/32
HTTP / 代理层node test/test-http.mjs17/17
配置合并与还原node test/test-config.mjs11/11
多引擎搜索结果解析node test/test-websearch.mjs5/5
生图引擎node test/test-imagegen.mjs16/16
会话 id 回退node test/test-session-id.mjs5/5
运行时兼容自检node test/test-compat.mjs14/14
历史消息鉴权与定向node test/test-history.mjs23/23
主动发言node test/test-proactive.mjs49/49
拥有者设置通道node test/test-owner-settings.mjs58/58
一键全量node test/run-all.mjs18/18 套件 · 683 项
test-plugin.mjs 用假 ctx 加载真正的 lib/index.js + 真适配器 + 零依赖 OneBot 模拟器,跑通完整链路(无需 DSH / QQ / 网络)。

二、快速开始

1. 起一个 OneBot 11 实现(推荐 NapCat)

NapCat 官方 Docker 镜像(支持 amd64 与 arm64):

docker run -d --name napcat --restart=always \
  -e NAPCAT_GID=$(id -g) -e NAPCAT_UID=$(id -u) \
  -p 3000:3000 -p 3001:3001 -p 6099:6099 \
  mlikiowa/napcat-docker:latest
  • WebUI:http://<主机IP>:6099/webui,用手机 QQ 扫码登录;登录 token 默认在 docker logs napcat 里。
  • 容器内配置目录 /app/napcat/config,QQ 数据目录 /app/.config/QQ(想固化登录就挂这两个卷)。
  • 在 NapCat 的「网络配置」里确认 OneBot 11:
    • HTTP 服务器 3000 —— 用来发消息
    • 正向 WebSocket 3001 —— 用来收事件
    • 若设了 access_token,记下来填进插件配置
  • 端口默认就是 3000/3001,和本插件默认值一致,不用改代码。

2. 安装插件

本插件不在 npm 上,从本仓库或 Release 附件装(维护仓库:https://github.com/arrow1031-dev/dsh-qq-bot)。

方式一:在 DSH 界面里装(推荐)

在首页侧边栏打开 插件 → 添加插件,输入框里填写下面这行包名,确认后等它装完:

github:arrow1031-dev/dsh-qq-bot

装完重启 DSH(桌面版重启应用 / CLI 重启 dsh web), 再到 插件 → 已安装 → dsh-qq-bot 打开配置面板。

方式二:命令行装

# 跟随 main(已解除封版:升级即最新,不锁 tag)
# 升级已有安装:dsh plugin --profile web update
dsh plugin --profile web add github:arrow1031-dev/dsh-qq-bot

# 本地 tarball(自己改了代码之后)
npm pack
dsh plugin --profile web add ./dsh-qq-bot-<版本号>.tgz

--profile 换成你实际在用的 profile:桌面版是 --profile desktop,dsh web 部署一般是 --profile web。

dsh plugin 本质是 pnpm 的前置封装:装完包后,它会自动把声明了 dsh.bundle.patch 的依赖加入 profile 的层栈(本包的 package.json 已声明),并连同唯一的运行时依赖 @deepseek-ai/schemastery 一起装上。装完**重启 DSH(桌面版重启应用 / CLI 重启 dsh web)**生效。

版本要求:0.11.x 要求 DSH ≥ 0.2.0-rc.2;DSH 0.2.1-rc.3 已实测(见下文「DSH 0.2 兼容」)。

升级注意:dsh plugin add 走 pnpm,按 tarball 完整性哈希缓存——改了代码要打成新版本号文件名再装(0.8.0 → 0.8.1),沿用旧名会被判「已最新」跳过。 lib/client.js 是浏览器侧代码,装完重启后还要刷新浏览器页面才加载新 bundle。

3. 在 QQ 里试

  • 群聊:默认要 @机器人 才有反应,@ 会被去掉再交给 Agent。
    • 回复(引用)机器人的消息也视同 @ 处理(reply.botReply,默认开启),带图照常走识图流程。
    • 需要结合群内近期消息/图片作答时,AI 会主动调 qq_history 工具自取(不再自动硬塞上下文)——识别窗口锚定触发本条回复的那条消息往前 scopeMessages 条,最多 imageLimit 张图(默认 5/3,设置页可调)。
    • 回复别人 + 正文 @bot:同样触发(引用回复),上下文同样交给 AI 自取。
    • bot 被点名时(@ / 回复 bot / 命令前缀 / 管理指令):回复统一引用你的消息(QQ 引用回复)。
  • 私聊:默认直接回。
  • 长回复按换行切成 ≤1200 字多条发送。
  • 每个群/每个人是独立的 DSH 会话(id 形如 qqbot-group-33333),会落盘并出现在 Web UI 会话列表里,可以像普通会话一样打开继续聊。
  • 新会话的显示名固定为 群聊_<群号> / 私聊_<QQ号>(session.fixedName,默认开启),不再让 LLM 自动起标题;已建会话保持原名。

三、协议与连接方式

插件内核不感知协议。设置里选「协议」,适配器会自动切换并重启:

protocol需要的协议端传输备注
onebot11(默认)NapCat / go-cqhttp / Lagrange正向/反向 WS + HTTP与 0.9 的配置完全兼容
onebot12OneBot 12 实现端正向/反向 WS + HTTP@ 是 mention、回复是 reply、图片只有 file_id
satoriKoishi(Satori 服务端)WS /v1/events + HTTP RPC消息体是类 XHTML 元素字符串,适配器内置 parser
milkyNapCat / Yogurt / Lagrange.Milky/event WS + /api HTTP无 echo;消息 id 是 message_seq
qq-officialQQ 开放平台机器人Webhook 或 WS 网关AppID/AppSecret 鉴权;群/单聊用 openid

想加新协议:读 lib/bridge/CONTRACT.md,写一个 stdin/stdout 适配器脚本, 再到 lib/index.js 的 ADAPTER_FILES 里登记一行。

OneBot 11/12 的两种连接模式

场景用哪个配置
协议端和 DSH 在同一台机器/同一容器正向 WS(默认)mode: forward,我们连 ws://127.0.0.1:3001
协议端在别的容器/主机,或网络方向只允许它连出来反向 WSmode: reverse,我们在 0.0.0.0:6199 监听,协议端填 ws://<DSH主机IP>:6199/

反向 WS 的鉴权:请求头 Authorization: Bearer <token> 或 URL 上的 ?access_token=<token> 都接受(后者是因为标准 WebSocket 客户端无法自定义请求头)。

其它协议的连接参数(插件设置页同名)

协议参数
onebot12protocols.onebot12.{mode,wsUrl,listen,httpUrl,accessToken}
satoriprotocols.satori.{wsUrl,httpUrl,token,platform,selfId}
milkyprotocols.milky.{wsUrl,httpUrl,accessToken}
qq-officialprotocols.qqOfficial.{mode,appId,appSecret,botSecret,listen,path};mode=webhook 时端口只能 80/443/8080/8443

各协议的能力边界

  • qq-official:官方 API 没有踢人/禁言/改名片这些群管理动作,qqgroup 会明确回「不支持」;主动消息受平台配额与被动回复窗口约束。
  • onebot12:图片段只有 file_id,要 URL 得再调 get_file;需要视觉识别的场景建议用 OneBot 11。
  • satori / milky:协议层不定义限流,主动发言的节流仍由插件的 proactive 配置执行。

四、配置

方式一:在插件设置页里改(推荐)

入口:在首页侧边栏打开 插件 → 已安装 → dsh-qq-bot。 这不是原来的「设置 → QQ 机器人」——设置页在 0.11.0 搬进了插件自己的面板。 本版要求 DSH 0.2.0-rc.2 或更新的 0.2.x 版本。现有配置无需迁移。

重启 DSH 后面板分四档,字段按功能归位:

标签二级标签放什么
连接—连接方式(OneBot 11 正向 / 反向、OneBot 12、Satori、Milky、QQ 官方)、WS/HTTP 地址、access_token、各协议参数
基本准入与名单群聊 / 私聊开关、@ 要求、白名单、命令前缀、拥有者与高级管理员、访客工具白名单
基本发言与管理回复策略(分片长度、慢回复提示、来源前缀、@ 回复、上下文头)、群管理指令与可管理群
能力识图与绘图图片视觉(开关 / provider / model / 最多张数 / 下载超时)、生图(接口地址 / key / 模型 / 重试与轮询)
能力搜索与抓取联网搜索(代理 / 端点 / key)、图片反搜、qq_webfetch 与 moli 渲染
能力对话与人格人格(开关 / 名字 / 设定 / 自定义 preset id)、主动发言(判断用 provider / 模型下拉)、聊天记录读取
高级—会话(独立会话、固定命名、preset 与 owner/guest 分流 preset)、逐帧日志、重启退避
  • 改完点「保存」;连接方式那几项(模式/地址/token)保存后会自动重启适配器,其它项下一条消息生效。
  • 类型映射:布尔是勾选框;白名单是逗号分隔的文本框;连接方式是下拉框。
  • 配置落点(本插件要求 DSH 0.2.0-rc.2 起):插件把自己的 Config schema 交给 DSH 当设置表单,界面上的改动由 DSH 写进 profile 的 cordis.patch.yml(也就是下面「方式二」里那个文件);0.1.6 时代的独立 settings.yaml 文档已经不存在了。
  • 设置命名空间就是条目 id(默认 dsh-qq-bot);条目被改名时,客户端会回退匹配「以 qq-bot 结尾」的那个命名空间。
  • 连接状态不在插件面板里显示,在对话里让 Agent 调用 qqbot 工具(action=status)看。
  • access_token 是只写字段:已设置时页面显示「已设置;留空则保持不变」,只有输入新值才会覆盖。 Host 侧把它声明成 schema 的 secret,值不会下发到浏览器,保存时也不会被空值清掉。

工作区(自动管理,无需配置)

Agent 会话的工作目录不需要任何配置:插件读取 DSH 官方默认 Workspace 的位置 (<系统 Documents>/deepseek-harness),在它的同级目录自动创建 QQ_msg 作为工作区根, 并注册为 DSH workspace——Web UI 会话侧栏的「QQ 消息」分组,新 QQ 会话自动归入该分组:

  • Windows:C:\Users\<用户名>\Documents\deepseek-harness\QQ_msg
  • Linux / macOS:~/Documents/deepseek-harness/QQ_msg(系统没有 Documents 目录时退到 ~/deepseek-harness/QQ_msg)

带图消息的原图缓存放在 QQ_msg/qq-images/<会话id>/,会话结算后自动删除该缓存, 会话与工作区本身保留。旧的 workspace 配置项已废弃移除。

方式二:改 composition 行配置(当 base 层用)

不要改包内的 cordis.patch.yml(升级会覆盖)。写进你自己的 $DSH_HOME/profiles/web/cordis.patch.yml —— 它在所有 bundle 之后应用,按 id 定向覆盖:

- id: dsh-qq-bot
  config:
    protocol: onebot11            # onebot11 | onebot12 | satori | milky | qq-official(见「三、协议与连接方式」)
    mode: forward                 # forward | reverse(仅 onebot11/12)
    onebot:
      wsUrl: 'ws://127.0.0.1:3001'      # mode=forward 用
      listen: '0.0.0.0:6199'            # mode=reverse 用
      httpUrl: 'http://127.0.0.1:3000'  # 留空则改用 WS action 发消息
      accessToken: ''
    group:
      enabled: true
      requireAt: true             # 群里是否必须 @
      allow: []                   # 群号白名单,空数组 = 所有群
    private:
      enabled: true
      allow: []                   # 用户号白名单,空数组 = 所有人
    commandPrefix: ''             # 例如 '/ai',空串 = 不限制
    reply:
      maxChars: 1200
      ackAfterMs: 8000            # 慢回合先回「正在处理」;0 关闭
      ackText: '正在处理,请稍候…'
      contextHeader: true         # 给消息加 "[QQ群 123 · 小明(22222)]" 前缀
      firstTurnHint: '(提示:这是一次 QQ 聊天。请用简体中文、简洁自然的口语作答;不要输出 Markdown 表格,代码块保持简短。)'
    auth:                         # 权限,见「五、权限与群管理」
      owners: []                  # 拥有者 QQ:全权限 + 永久授权 + 可走独立会话
      superAdmins: []             # 后台设定的高级管理员(不可永久授权)
      ownerPreset: ''             # 填了才把拥有者/非拥有者分成两条会话
      guestPreset: ''             # 非拥有者用的 preset(仅人格分离,非安全边界)
      guestDshTools: false        # 是否允许访客用 DSH 内部工具(默认否)
      guestToolAllow: ['qqbot', 'qqgroup', 'qq_websearch', 'qq_webfetch', 'qq_imgsearch', 'qq_history']   # 访客仍可用的工具白名单
      grantTtlMinutes: 60         # 群管理员动态授权默认时长
      grantMaxTtlMinutes: 1440    # 群管理员单次可授最长
    admin:
      enabled: true
      manageGroups: []            # 可管理的群;空 = 回退用 group.allow
      requireGroupAdmin: true     # 是否承认「本群管理员」这一路权限
      prefix: '/'                 # 指令前缀(必须在消息首位)
      dryRun: false               # true = 只回复将执行什么
      allowConsole: false         # 允许 Web UI 会话直接调用管理工具
    session:
      fixedName: true             # 固定新建会话名为 群聊_<群号> / 私聊_<QQ号>(不再 LLM 自动起标题)
    perChatSession: true          # true=每个 QQ 会话一个 DSH 会话
    agentPreset: ''               # ''=部署默认 preset;可填自定义 preset id 换人格/工具
    adapterPath: ''               # ''=用包内自带适配器
    nodePath: ''                  # ''=自动解析 node
    logAdapterFrames: false       # 排查时开,会打印每一帧 JSON
    vision:
      enabled: true               # 图片视觉:收到图片时用视觉模型识别并并入回复
      provider: ''                # 视觉 provider 路由(留空 = 会话模型;复用 DSH 自定义 providers)
      model: ''                   # 视觉模型 id(留空 = 会话模型;官方如 deepseek-flash)
      maxImages: 3                # 单条消息最多送入的图片数
      downloadTimeoutMs: 10000    # 图片下载超时(毫秒)
    webSearch:                    # qq_websearch 联网搜索
      proxy: ''                   # http://host:port;留空 = 直连;搜索与 url 抓取都走它
      baseUrl: ''                 # 留空 = https://api.deepseek.com/anthropic/v1(仅 multiEngine=false 用)
      apiKeyEnv: ''               # 留空 = DEEPSEEK_BALANCE_API_KEY(自动读 DSH 凭据服务)
      maxUses: 5                  # 每次搜索最多触发次数
      multiEngine: true           # 多引擎组合搜索(默认开):Tavily → DuckDuckGo → Yandex/Bing/Wikipedia
                                  # 全部走上方 proxy;关闭则落回官方 DeepSeek web_search
    moli:                         # dsh-moli 接管(qq_webfetch 用 Moli 渲染)
      enabled: false              # 默认关;开启后 qq_webfetch 改用 Moli 无头浏览器渲染抓取(可抓 SPA/JS 页;需已安装 moli 引擎,否则无效)
      proxy: ''                   # http://host:port;留空自动透传 webSearch.proxy
      exePath: ''                 # moli 可执行文件路径;留空自动探测 ~/.local/bin/moli 与 PATH
    history:                      # qq_history 聊天记录(AI 自取)
      scopeMessages: 5              # 识别窗口:触发消息往前最多多少条
      imageLimit: 3                 # 窗口内最多识别多少张图(最近的优先)
    proactive:                    # 主动发言(可选,默认关)
      enabled: false              # 打开且下面列了群号才生效
      groups: []                  # 生效群号;留空 = 不启用
      intervalMinutes: 60         # 检查间隔(最小 5)
      minNewMessages: 6           # 至少多少条新消息才考虑接话
      quietHours: ['23:00-08:00'] # 免打扰时段,可多段
      maxPerHour: 2               # 每小时上限
      maxPerDay: 8                # 每天上限
      minGapMinutes: 15           # 两次发言最小间隔
      eventGuard: true            # 事件去重:跳过过气事件 / 只有应答的插话 / 刚聊过的话题
      staleMinutes: 15            # 最新消息超过这么久视为事件已过气,不接话
      topicCooldownMinutes: 120   # 同一话题在这么久内不再重复
      provider: ''                # 留空 = 跟随群会话模型(走 DSH 已配置的渠道,零密钥)
      model: ''                   # 留空 = 跟随群会话模型;设置页是下拉,同一份模型目录
      maxChars: 120               # 单条最大字数
      interests: ''               # 话题偏好(可空;接什么仍由模型决定)
      dryRun: false               # 演练:只记日志不发送
      verbose: false              # 日志打印每次判断
    imageGen:                     # qq_draw 生图(自填接口,插件不带 key)
      enabled: false              # 打开后 AI 才能画图
      baseUrl: ''                 # OpenAI 兼容,例 https://api.example.com/v1(请求 地址/images/generations)
      apiKey: ''                  # 只写字段;留空则读下面那个环境变量
      apiKeyEnv: IMAGE_API_KEY
      model: seedream-5.0-lite
      size: 1024x1024
      timeoutMs: 180000          # 单次请求超时
      maxAttempts: 4             # 失败重试次数(含首次;429/5xx/网络/空结果)
      retryIntervalMs: 5000
      poll: true                 # 供应商返回 task_id 时继续轮询
      pollIntervalMs: 5000
      maxPolls: 30
    persona:                      # 自定义人格:每个新会话第一轮附一次
      enabled: false
      name: ''
      text: ''
    agentPresetCustom: ''         # 自定义 preset id(如 qq);非空时优先于 agentPreset
    imgSearch:                    # qq_imgsearch 图片反搜
      enabled: true               # 默认开;关闭后工具返回「已关闭」
      maxResults: 5               # 每次反搜最多返回结果数
      saucenaoKey: ''             # SauceNAO API key(saucenao.com 注册可得);留空不启用
      weightedShuffle: false      # 有代理时按体量权重随机重排引擎顺序(避免固定引擎一直打头)
                                  # 请求全走上方 webSearch.proxy:无代理 = 直连引擎按可用性优先
                                  # (TraceMoe → IQDB);有代理 = 放开全部可用引擎按用户体量优先
                                  # (Yandex → Tineye → Copyseeker → SauceNAO → IQDB → TraceMoe)

agentPreset 是换人格的正规做法:给 QQ 机器人做一个专属 preset,把 id 填进来。

图片视觉(vision)

收到图片时用视觉模型识别并并入 Agent 输入(原图缓存到 <QQ_msg>/qq-images/<会话id>/,会话结算后删除)。 provider / model 留空 = 会话模型;也可以给视觉单独指定 provider / model。

官方视觉模型(DeepSeek API):deepseek-flash 接受图片与文本(OpenAI 兼容,base64 data URL 或图片 URL 均可,支持 JPEG / PNG / GIF / WebP)。 旧的 deepseek-v4-flash-vision-exp 名字已退役,但请求仍被接受(由最新 Flash 服务)。见 DeepSeek Vision 文档。

其它 OpenAI 兼容通道:选一个在 DSH providers 里声明了 input: [text, image] 的模型即可; 注意 deepseek-v4-flash-0731 这类默认模型实测不真正支持图片(图不进上下文、会编造解释),换视觉模型时务必确认声明了图片输入。

五、权限与群管理

权限是三路并行取并集(不是"先命中先返回"):

路径谁能做什么
静态名单 auth.owners拥有者全部权限 + 永久授权 + 可走独立的拥有者会话
静态名单 auth.superAdmins后台设定的高级管理员群管理权限(不能永久授权)
动态授权被临时授权的人仅被授予的那几项,仅限本群、带时限
本群身份该群的 admin / owner群管理权限(可用 admin.requireGroupAdmin 关掉这一路)

任一路给到某项权限即拥有它;来源会在 /whoami、/perms 里如实列出,便于排查"他为什么有权"。

权限项:query(查询)、mute(禁言)、kick(踢人)、recall(撤回)、settings(群名/名片/管理员/全员禁言)、grant(给本群成员授权),拥有者另有 permanent(永久授权)。

动态授权(群维度,不可跨群)

  • 拥有者或本群管理员可以给本群成员临时授权:/grant @某人 mute 30(30 分钟)
  • 只有拥有者能永久授权:/grant @某人 kick forever(非拥有者发这条会被拒)
  • 群管理员单次最长 auth.grantMaxTtlMinutes(默认 1440 分钟)
  • 授权键是「群号 + QQ 号」:在 A 群授的权,在 B 群一律无效
  • /revoke @某人 [权限] 撤销、/grants 列出本群全部授权

非拥有者不使用 DSH 内部功能:执行级工具闸

用一个 tools.guard 硬闸,只在建会话时注册一次,判据是闭包里的可变标志位:

  • 注册在 agent.ctx 上 → 只作用于该会话的 agent
  • 每次工具执行时才求值 → 拥有者/访客交替说话不需要增删限制
  • 用白名单(auth.guestToolAllow,默认 qqbot,qqgroup,qq_websearch,qq_webfetch,qq_imgsearch,qq_history)→ run_code(PTC 保留传输)这类"不在全局层"的特殊工具也会被自动挡住;访客联网统一走 qq_websearch(搜索)与 qq_webfetch(抓网页),DSH 自带的 web_fetch 已从默认白名单移除
  • 被拦时返回拒绝原因,模型会把它转达给用户(最多多一次"被拒绝的调用"往返)

因此它同时满足:0 额外 token(不注入提示词、不改工具表、不记 request/header)、单会话单历史、不改 DSH 本身。

想放开就设 auth.guestDshTools: true(不建议)。

为什么不用"每条消息增删 tools.restrict":restrict 是静态过滤器,想按说话人变就得反复增删,工具面抖动会让 agent loop 多记 request/header,反而更费 token、代码也更多。guard 是每次执行才求值的函数,配一个可变标志位就够。

会话分流(可选,仅供人格分离)

auth.ownerPreset / auth.guestPreset 都留空时,拥有者与非拥有者共用一条会话(默认,也是推荐)。填了 preset id 才分成两条:

  • 拥有者走 qqbot-group-<群号>,挂 ownerPreset
  • 非拥有者走 ...-guest,挂 guestPreset

这只是换人格,不是安全边界——工具隔离由上面的 guard 负责。分流要付代价:同一个群会有两条历史,拥有者看不到访客说了什么。除非确实要给访客不同的说话风格,否则别开。

全局 agentPreset(设置页下拉:部署默认 / standard / ptc / minimal / cordis)决定新会话用什么 preset,已建会话不变;分流时某方 preset 留空会自动回落 agentPreset。

上下文头会带身份,例如 [QQ群 33333 · 小明(22222) · 身份:访客|本群管理员+动态授权[mute 至 ...]],让 Agent 自己决定怎么对待这个请求。

指令的严格性(防"聊天内容夹带指令样内容"被误执行)

  1. 前缀必须位于消息首位(前面只允许空白)——你好 /mute @x 不会被当成指令
  2. 动词必须完全匹配已知指令,未知动词照常交给模型
  3. token 数量必须落在该指令声明的 [min,max] 内,多余内容判为用法错误、不执行
  4. 权限不足直接拒绝,且不做任何动作

指令不需要 @ 机器人(刻意放在 requireAt 之前),但上述规则一条都不放宽。

指令表

指令权限作用
/help—列出全部指令
/whoami—看我在本群的身份与权限来源
/perms [@某人]query看某人在本群的权限来源(三路)
/grantsgrant列出本群的动态授权
/grant @某人 权限[,权限] [分钟|forever]grant临时授权(仅本群)
/revoke @某人 [权限]grant撤销授权
/membersquery成员列表(最多列 30 条)
/infoquery群信息
/mute @某人 [分钟=10]mute禁言(0 = 解禁)
/unmute @某人mute解除禁言
/kick @某人kick移出本群
/recall [消息id]recall撤回;回复某条消息再发 /recall 可省略 id
/banall on|offsettings全员禁言开关
/card @某人 新名片settings改群名片
/admin @某人 on|offsettings设/撤管理员
/rename 新群名settings改群名

Agent 工具(与指令共用同一套鉴权)

qqgroup 的 action:只读 group-list/group-info/member-list/member-info,写操作 mute/unmute/kick/whole-ban/set-card/set-admin/rename/recall/poke。

工具的"说话人"取自本会话最近一条消息,所以群里普通成员没法靠一句话驱动管理动作——这一点是提示注入的主要防线。group_id 留空时默认用当前会话所在的群。

群事件

适配器会把完整事件字段透传上来(operator_id 谁操作的、duration 禁言时长、target_id、card,以及被引用消息的 reply_to)。当前版本只做透传与计数(qqbot status 里能看到),自动治理尚未实现。

建议的上线顺序

  1. 在设置里填 auth.owners(至少填自己)
  2. 打开 admin.dryRun,在群里发几条指令确认解析与回执
  3. 关掉 dryRun,再按需加 auth.superAdmins
  4. 非拥有者用 DSH 功能由访客工具闸默认拦截,无需额外配置;确实想放开再改 auth.guestDshTools

六、Agent 能用的工具

插件向 DSH 注册 8 个工具(访客只能看到 auth.guestToolAllow 里的那几个)。先看通用的 qqbot:

  • {action:'status'} —— 连接状态、模式、适配器 pid/端口、各 QQ 会话的回合数、统计计数
  • {action:'send', message_type:'group'|'private', target_id:'123', text:'...', image?} —— 主动发一条 QQ 消息;image 传本地图片绝对路径(只允许 <QQ_msg>/qq-images 目录内),插件转 base64 后与文本一起发出,用于「发自拍 / 自画像 / 刚生成的图」这类场景。
  • {action:'interrupt', conversation:'group-123'|'all'} —— 中断正在跑的回合

qq_websearch(联网搜索)

访客白名单内可直接调用:

  • {query, max_uses?} —— 按关键词搜索,返回 {ok, count, sources, text};后端走多引擎组合搜索(multiEngine 默认开):Tavily(配 TAVILY_API_KEY 才启用,环境变量或 DSH 凭据 refs)→ DuckDuckGo(html→lite,串行 + 限流记忆)→ Yandex / Bing / Wikipedia 并发兜底、聚合去重(相关性闸门防无关链接);关闭 multiEngine 则落回官方 DeepSeek web_search。抓取网页请用 qq_webfetch。

搜索请求都走 webSearch.proxy(非空时经 HTTP CONNECT 隧道 + TLS,含境内/境外分流;留空直连)。webSearch 配置段:代理 / 端点 / key 环境变量名(默认 DEEPSEEK_BALANCE_API_KEY,自动读 DSH 凭据服务)/ 每次最多触发次数 / 多引擎开关(默认开)。

qq_webfetch(抓取网页)

访客白名单内可直接调用:

  • {url, max_chars?} —— 抓取指定网页原文/渲染文本(http/https,仅公网),返回 {ok, url, status|via, text};请求受 webSearch.proxy 管控(留空直连、非空走 HTTP CONNECT 隧道),抓取前校验目标为公网 IP,内网/回环/私有地址一律拒绝(防经代理扫内网)。
  • 配置 moli.enabled(默认关)后改用 Moli 无头浏览器渲染抓取(可抓 SPA/JS 渲染页);代理自动透传 webSearch.proxy(可被 moli.proxy 覆盖);未检测到 moli 引擎时开关无效,工具返回 moliUnavailable 明确报错。

qq_imgsearch(图片反搜)

访客白名单内可直接调用:

  • {url, max_results?} 或 {file, max_results?} —— 给定公网图片 URL 或本地图片文件路径,返回 {ok, count, sources, via, text};来源带 url/title/snippet。
  • 本地文件流程:先上传图床(zixiaoyun/scdn/uguu 三床按实测质量加权随机排优先度,依次试到第一个成功,一律走 webSearch.proxy 管控)拿公网 URL → 再反搜;上传失败回退:不经过图床、直接把文件发给 TraceMoe(文件直传搜图)。file 路径限 qq-images 目录内(访客不能读任意文件)。

引擎组合(全部请求走 webSearch.proxy,与 qq_websearch 同一套代理管控,本插件独立实现):

  • 无代理(留空直连):按可用性优先 —— TraceMoe(动画截图识别,URL 模式 JSON,直连最快)→ IQDB(ACG 图库)→(有 key 时 SauceNAO)→ Soutubot
  • 有代理:放开全部可用引擎按用户体量优先 —— Yandex(全球搜索引擎)→ Tineye(全球图片搜索)→ Copyseeker(聚合,结果最多)→(有 key 时 SauceNAO)→ Soutubot(搜图Bot酱,多库:pixiv/danbooru/gelbooru 等)→ IQDB → TraceMoe;聚合去重,cap 满即停,单引擎失败自动继续下一引擎。
  • 开启 imgSearch.weightedShuffle 后,有代理时按体量权重随机重排(Yandex/Tineye/Copyseeker 权重 30/25/25,SauceNAO 15,Soutubot 8,IQDB/TraceMoe 5):体量大引擎大概率先跑,但顺序每次不同,避免固定引擎一直打头。
  • Soutubot(soutubot.moe,开源 imsearch):API 只收文件 → 引擎先下载图片(二进制走代理)再上传 /api/search 拿 result_id → 查 /api/results 多库结果(pixiv 直链 / danbooru / gelbooru / zerochan 镜像 + 画师/角色标签)。
  • kind=comic 漫画模式(qq_imgsearch 参数):漫画/本子截图优先 Soutubot → SauceNAO(有 key)→ IQDB → TraceMoe(无代理同链),有代理时再 Yandex/Tineye/Copyseeker 兜底;weightedShuffle 开启时漫画权重 Soutubot 40 / SauceNAO 30 / IQDB 20 / TraceMoe 10 / 通用 5。kind=comic + 本地文件(file)时不上图床:Soutubot 直接传文件(soutubot API 只收文件),失败/无结果回退 TraceMoe 文件直传。

引擎实测(2026-10,PicImageSearch 图床 URL 全矩阵交叉验证):直连可用 TraceMoe / IQDB / Soutubot;代理可用 Copyseeker(184 条)/ Yandex(15 条)/ Tineye(图床新图未收录时 0 条,收录后正常);SauceNAO 需 saucenaoKey;其余(ascii2d / anime_trace / bing / google_lens / ehentai)不可用。imgSearch 配置段:enabled(默认开)/ maxResults(默认 5)/ saucenaoKey / weightedShuffle。

qq_history(聊天记录,AI 自取)

访客白名单内可直接调用(群聊里被 @ 或需要结合群内近期消息/图片作答时,Agent 会主动调它):

  • {group_id?, user_id?, count?, with_images?} —— 拉取当前群最近消息,返回 {ok, group_id, messages:[{time, user_id, nickname, text, images:[{url, description}]}]};group_id 默认当前群,user_id 可只筛某人的发言,count 默认 20、上限 50,with_images 默认开。
  • 识别窗口锚定「最新提及」:只对触发本条回复的那条消息(@ bot / 回复 bot / 命令前缀)往前 scopeMessages 条(默认 5)内的图片做识别,最多 imageLimit 张(默认 3,最近的优先);更早的 @ 不锚定、其附近图片不因「也是被提及」而识别,避免重复拉取/重复识图。
  • 窗口内图片用插件的视觉链路识别(下载 + askVision),返回 URL + 描述;识别失败只给 URL 不阻塞。配置见 history 段(scopeMessages / imageLimit)。

qq_draw(生图,可直接发到 QQ)

在 插件 → 已安装 → dsh-qq-bot → 能力 → 识图与绘图 里填好接口后,AI 就能按描述画图:

  • {prompt, model?, size?, reference?, caption?, send?} —— prompt 越具体越好;reference 可给公网 URL 或 qq-images 下的本地图(保持同一形象做「自拍」); send 默认 true,直接把图画成 QQ 图片段发到当前会话,caption 是随图发的文字。
  • 失败策略:429 / 5xx / 网络错 / 空结果 按 retryIntervalMs 重试到 maxAttempts;4xx(参数错、额度不足)直接报错、不重试。
  • 异步供应商:响应里是 task_id 时按 pollIntervalMs 轮询(依次试 .../images/generations/<id>、.../images/tasks/<id>、.../tasks/<id>),最多 maxPolls 次;关掉 poll 就只尝试一次。
  • 图片落盘:<QQ_msg>/qq-images/gen/;qq_draw 不在访客默认白名单(会花钱),要放开就自己加到 auth.guestToolAllow。
  • 接口约定:OpenAI 兼容 POST <baseUrl>/images/generations,请求体 {model, prompt, n:1, size?, image?},返回 data[0].url 或 data[0].b64_json(实测 seedream-5.0-lite 返回 url、约 40 秒)。

人格(人设)也在设置里换

两种做法,互不冲突:

  • 自定义人格:设置页「自定义人格」开关 + 名字 + 设定,开一次管所有新会话(第一轮末尾附一次身份设定,零额外 token);已建会话不变,想立刻生效就删掉旧会话重开。
  • 整档 preset:Agent preset 下拉选内置 standard / ptc / minimal / cordis;自建 preset(例如 qq)填进「自定义 agent preset id」,非空时优先生效——这条路换的不只是人格,还有工具面。

web_fetch(抓公网原文,仅拥有者)

DSH 自带的 web_fetch 工具仍然注册,但已从访客默认白名单移除——访客要抓原文请用 qq_webfetch(走 webSearch.proxy 代理、同样带公网 IP 过滤,moli.enabled 时可改用 Moli 渲染)。web_fetch 本身只允许公网 IP,内网/回环地址由 DSH 直接拒绝;拥有者不受访客白名单限制,照常可用。

qqgroup(群管理)

见「五、权限与群管理」一节。工具路径的授权依据是「当前会话最近一条消息是否来自管理员白名单」。

qqconfig(拥有者专用:在 QQ 里改设置)

0.10 起。让拥有者本人在私聊里直接改一部分插件设置,不用去翻 profile 的 cordis.patch.yml:

  • {action:'show'} —— 列出可改项、当前值和配置修订号
  • {action:'propose', path:'group.allow', value:'[\"488136808\"]'} —— 只校验不写盘,返回改动预览与一次性 token
  • {action:'apply', token:'...'} —— 凭 token 真正落盘
  • {action:'cancel', token:'...'} —— 撤销预约

可改的是一组白名单字段:群/私聊开关与白名单、命令前缀、群管理开关、主动发言参数、图片识别、人设。 密钥、onebot.*、auth.*(拥有者名单)、模型名这类改不了——机器人无法借这条通道给自己扩权。

写盘要过三道:只能是拥有者本人在私聊里(身份取自当前会话发言人,不接受参数)→ propose 只生成 300 秒有效的一次性确认码 → apply 时若配置已被别处改过就作废。落盘只走 DSH settings 服务(schema 校验 + 原子写 + 热重载),不手改 YAML; 另有每小时 6 次 / 每天 30 次的限流与 QQ_msg/qqbot-config-audit.jsonl 审计。完整威胁模型见 SECURITY.md。

总开关 auth.ownerSettings(默认开),关掉后全部动作拒绝。

七、排错

现象检查
日志一直「OneBot 未连接」NapCat 的「正向 WebSocket」是否开在 onebot.wsUrl;reverse 模式下 NapCat 是否指向了 ws://<DSH>:6199/;防火墙;token 是否一致
能连上但收不到消息OneBot 版本要选 v11;不能只开 HTTP,必须开正向或反向 WS
群里 @ 了也不回group.enabled;group.allow 白名单;self_id 与消息里的 self_id 是否一致(logAdapterFrames: true 能看到原始帧)
回「(本轮没有产生文字回复:...)」括号里是从会话日志读出的 turn/end 原因,据此定位(模型路由、额度、工具报错等)
发送失败NapCat 的 HTTP 服务器(默认 3000)是否开着;accessToken;看日志里的「发送失败」
日志在哪插件前缀 [dsh-qq-bot],适配器前缀 [dsh-qq-bot]/adapter,都打到 DSH Host 的 stdout

八、无 QQ 号自测

先装依赖(Config schema 需要 schemastery):

npm install

装好之后裸克隆即可运行:需要 DSH 运行时才能成立的少数断言(动态工具注册、qqgroup 工具路径) 会自动跳过,而不是判失败;想看全,把 DSH_PROFILE_MODULES 指向一个装了 DSH 运行时的 profile 的 node_modules。 整套 628 项(18 个套件):

npm test

或分开跑:

# 协议适配器回归(每家一个套件,全部用本地假协议端)
node test/test-adapter.mjs              # OneBot 11 正向(19 项)
node test/test-adapter-reverse.mjs      # OneBot 11 反向(12 项)
node test/test-adapter-onebot12.mjs     # OneBot 12(30 项)
node test/test-adapter-satori.mjs       # Satori(70 项)
node test/test-adapter-milky.mjs        # Milky(79 项)
node test/test-adapter-qq-official.mjs  # QQ 官方(87 项)

# 插件本体(假 ctx + 真适配器 + 假协议端)
node test/test-plugin.mjs     # Host 半边(76 项,含 Config 契约与热更新)
node test/test-client.mjs     # 客户端设置页(80 项)
node test/test-admin.mjs      # 权限·群管理·访客工具闸(32 项)
node test/test-http.mjs       # HTTP / 代理隧道(17 项)
node test/test-config.mjs     # 配置合并与还原(11 项)
node test/test-websearch.mjs  # 多引擎搜索结果解析(5 项)
node test/test-imagegen.mjs   # 生图引擎:重试 / 轮询 / 响应解析(16 项)
node test/test-session-id.mjs # 会话 id 回退(5 项)
node test/test-compat.mjs     # 运行时兼容自检(14 项)
node test/test-history.mjs    # 历史消息鉴权与定向(23 项)
node test/test-proactive.mjs  # 主动发言(49 项)
node test/run-all.mjs         # 一键全量(18 套件)

也可以手动拿假 OneBot 试(端口正好是 NapCat 默认值):

node test/mock-onebot.mjs --http-port 3000 --ws-port 3001
# 另开一个终端,伪造一条 @机器人的群消息
node -e "fetch('http://127.0.0.1:3000/__inject',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({message_type:'group',group_id:33333,user_id:22222,nickname:'小明',message:[{type:'at',data:{qq:'10000'}},{type:'text',data:{text:'你好'}}]})}).then(r=>r.json()).then(console.log)"
# 看机器人回了什么
node -e "fetch('http://127.0.0.1:3000/__sent').then(r=>r.json()).then(j=>console.log(JSON.stringify(j.messages,null,2)))"

九、关于 AstrBot

AstrBot 是 OneBot 的消费方(内置 aiocqhttp 反向 WS 服务端),和本插件角色重叠。 用 DSH 当大脑就别再接 AstrBot,否则同一条消息会被两个大脑各回一次。 对端只要是「OneBot 11 实现」(NapCat / OneDisc / Lagrange / LLOneBot 等)就行。

十、在手机容器里跑 NapCat 的注意事项

  • NapCat 官方镜像有 arm64 版本,所以理论上能在 arm64 容器里跑;但容器里再跑 Docker 通常需要 privileged 权限,不一定允许。
  • 备选:不用 Docker,直接在容器里跑 NapCat.Shell(需要对应的 Linux QQ + NapCat 版本)。
  • 同容器时用 forward 模式连 127.0.0.1:3001 最省事;如果 NapCat 在别的容器,用 reverse 模式,让 NapCat 连到 ws://<DSH主机IP>:6199/。
  • 端口 6099 是 NapCat 的 WebUI(扫码登录用),不要暴露到公网。

十一、文件清单

文件作用
lib/index.jsCordis 插件入口(Host 侧)。导出 name / inject / Config / apply;组合导入下面各模块
lib/config.jsConfig schema、DEFAULTS、深合并 mergeConfig、volatile 还原 plainConfig
lib/session.js会话创建/复用、固定命名、访客工具闸(ctx.get('tools'))
lib/bridge/适配器共享层:手写 WebSocket、stdio 契约、消息段归一(CONTRACT.md 是写新适配器的入口)
lib/adapters/协议适配器:onebot11 / onebot12 / satori / milky / qq-official,各自可独立执行
lib/owner-settings.js拥有者设置通道:白名单校验、propose/apply 两步确认、限流、审计
lib/workspace.jsQQ_msg 工作区根解析(index 与 imgsearch 共用,避免 0.9.5 的 ReferenceError)
lib/tools.js8 个模型工具(qqbot/qqconfig/qqgroup/qq_websearch/qq_webfetch/qq_imgsearch/qq_draw/qq_history);注册统一走 registerTool(ctx, make)(DSH 0.2 契约)
lib/admin.js群管理指令、三路权限、动态授权
lib/http.jsfetch + HTTP CONNECT 代理隧道 + 公网地址校验
lib/websearch.jsTavily / DuckDuckGo / Yandex / Bing / Wikipedia 组合搜索
lib/imgsearch.js图床(zixiaoyun / scdn / uguu)+ TraceMoe / IQDB / SauceNAO / Soutubot 等反搜
lib/moli.jsdsh-moli 渲染抓取探测(可选)
lib/client.js客户端半边(手写 bundle):在「设置」里加「QQ 机器人」选项卡(命名空间 = 条目 id dsh-qq-bot)
lib/onebot-adapter.mjsOneBot 11 传输适配器。零依赖(Node 22 内置 WebSocket/fetch),正向/反向 WS + HTTP,自动重连、发送限速
cordis.patch.ymlbundle 补丁:把插件插入 profile 组合树
test/mock-onebot.mjs零依赖 OneBot 11 模拟器(手写 RFC 6455 帧编解码)
test/test-adapter.mjs正向 WS 适配器回归(19 项)
test/test-adapter-reverse.mjs反向 WS 适配器回归(12 项)
test/test-plugin.mjsHost 插件端到端(76 项,假 ctx + 真适配器 + Config 契约 + 配置热更新)
test/test-client.mjs客户端设置页(80 项,模拟模块加载器 + 最小 React 渲染 + 四档标签/二级分类 + 只写密钥护栏 + 命名空间回退)
test/test-admin.mjs权限·群管理·访客工具闸专项(32 项;dsh-tools 可解析时另含工具路径断言)
test/test-http.mjsHTTP / 代理隧道(17 项)
test/test-config.mjs配置合并与 volatile 还原(11 项)
test/test-websearch.mjs多引擎搜索结果解析(5 项)
test/run-all.mjs一键全量(8 套件)
CONTRIBUTING.md / CODE_OF_CONDUCT.md / SECURITY.md贡献流程、行为准则、安全报告
.github/issue / PR 模板、CODEOWNERS
examples/dynamic-package-host.js同一个桥接的「动态 Cordis Package」版本:不想装包、只想在当前 DSH 进程里临时跑时用
ARCHITECTURE.md改动指南(给要 fork 的人):文件职责、数据流、设计决策原因、分步改动指南、调试手册、术语表
AI-DISCLOSURE.mdAI 生成内容声明、验证情况、尚未验证清单、第三方归属、安全免责
CHANGELOG.md版本演进(0.1.0 → 0.8.2,含 0.8.0 合并下游分支与 0.8.1 输入框修复)
LICENSEMIT

十二、设计说明(踩过的坑)

  • 为什么中间要子进程? 动态插件沙箱没有 fetch/WebSocket/require,用 ctx.subprocess 拉真 Node 进程、换行分隔 JSON 通信;协议层可独立测试,换 QQ 框架不影响插件。
  • 为什么用 sessionController 建会话? 直接 agentLoop.create() 建的 Agent 没装模型路由(首次请求必败);sessionController.create() + resolveAgent() 才是官方组装路径(装 provider/model + preset)。
  • 为什么用 whenIdle() + deriveMessages() 取回复? 回合串行排队,回合后会话日志最新一条有文本的 assistant 消息就是回复,比订阅流式事件简单稳。
  • 访客工具闸为什么用 guard? 拆会话破坏对话一致性;restrict 是静态过滤器需反复增删。tools.guard 注册一次、每次执行才求值:0 额外 token、单会话单历史;标志位须在串行回合内部设置,否则排队消息互相覆盖权限。
  • 设置契约为什么跟 DSH 0.1.7 改? 0.1.7 删了 settings.register(),改为读插件导出的 schemastery Config(可编辑字段必须 .volatile());volatile 变更不重挂插件,需监听 loader/volatile-update 自己刷新配置。
  • 为什么动态 import('@deepseek-ai/dsh-tools')? peerDependency 由 DSH 提供;动态 import + try/catch,包不在时桥接照样工作,只是少一个 qqbot 工具。

十三、不想重启 DSH 也能先跑起来(动态 Cordis Package)

装包要重启 dsh web;想在当前进程里立刻试,用 examples/dynamic-package-host.js 作动态 Package: 交给 cordis_define(code.host)再 cordis_run 即可,与常驻插件共用同一份磁盘源码。 优点:立刻生效、不用重启;限制:DSH 一重启就没了,长期在线还得装包。 动态沙箱里没有 require/fetch/setTimeout,传输层必须靠 ctx.subprocess 拉子进程 (这也是 lib/onebot-adapter.mjs 存在的原因)。

十四、许可与维护

许可与作者

MIT,见 LICENSE。维护者与贡献者见顶部(维护 @arrow1031,核心贡献者 @nd002723)。

维护状态:社区化维护中(不封版)

本仓库就是主干:0.8.0 把下游开发者 @nd002723 的分支 (模块化重构 + qq_websearch / qq_webfetch / qq_imgsearch / qq_history)并入 main,同时保留 0.7.0 的 DSH 0.2 兼容改造;0.8.1 修掉设置页列表输入框吞逗号的问题; 0.10.0 把协议层拆成 lib/bridge/ + lib/adapters/,一套插件接 OneBot 11/12、Satori、Milky、QQ 官方机器人。

  • 安装跟随 main,不再锁 tag;每次合并后在 main 打 tag 发 Release
  • 提 issue / PR / Discussions 都欢迎,活跃贡献者会被邀请为协作者(当前协作者:@arrow1031、@nd002723)
  • 0.6.x 及更早(兼容 DSH ≤ 0.1.6)保留为历史 tag,需要旧版请显式指定 tag

联系

为了让你改得快,这些文档是为你写的:

文档内容
ARCHITECTURE.md改动指南:分层图、模块职责、关键数据流、设计取舍、已知边界
AI-DISCLOSURE.mdAI 生成方式、人类决策点、验证到什么程度、尚未验证的清单、第三方归属、安全免责
CHANGELOG.md0.1.0 → 0.10.0 的完整演进

致谢

  • NapCat —— 提供灵感、OneBot v11 接口与文档。 没有它把 NTQQ 的能力按 OneBot 规范暴露出来,这个项目根本无从起步。
  • 梁圣 —— 忠!橙!

AI 生成内容声明见 AI-DISCLOSURE.md,使用前请务必读一遍(尤其"已知限制"一节)。

运行时依赖:@deepseek-ai/schemastery(Config schema 用);@deepseek-ai/dsh-tools 可选(仅用于注册动态工具,缺失时自动降级)。适配器只用 Node 22 内置的 WebSocket 与 fetch——本插件本身保持零 npm 依赖。唯一的可选功能例外:qq_webfetch 的 Moli 渲染模式(moli.enabled)需要另外安装 dsh-moli 插件 + moli 引擎(未装时开关无效、工具返回 moliUnavailable 并回退普通 http 抓取),不影响其它功能。

十五、附录

跨对话检索(跨群 / 跨会话)

两套互补的能力,都在设置里配、由 Agent 自己决定何时用:

  • QQ 层:qq_history(只读,不改任何东西)
    • 默认读当前会话(群聊 / 私聊都行);before_seq 翻更早的页,返回的 next_before 就是下一页游标。
    • 跨会话要权限:拥有者 / 高级管理员可读任意群和私聊;群管理员只能读「自己是该群管理员(或已授权)且该群在可管理范围」的群;别人的私聊只有拥有者/高级管理员能读。当前会话不受限,AI 回答群内问题照常读。
    • 私聊历史用 peer_id 指定对象;读别人的私聊要带 before_seq(NapCat 需要一条该会话里的消息当锚点)。
    • 拉不到时给明确原因(无权限 / 需要锚点),不会静默返回空。
  • 会话层:session_search 系列(DSH 的 @deepseek-ai/dsh-tool-session-query,5 个只读工具: session_search / session_event_search / session_trace / session_event_trace / session_event_read)。授权规则是「目标会话的 cwd 与调用方完全一致」—— QQ 的每个群/私聊会话都落在同一个 QQ_msg 工作区,所以它们之间可以互相检索。在 qq preset 里挂一行即可:
- id: preset-qq
  name: "@deepseek-ai/dsh-agent-preset"
  config:
    id: qq
    plugins:
      - id: tool-session-query
        name: "@deepseek-ai/dsh-tool-session-query"
        # 可选:maxSearchResults / searchTimeoutMs

不挂这一行就没有这 5 个工具,但 qq_history 的跨群读取不受影响。

主动发言(可选)

默认关闭。打开后按 proactive.intervalMinutes 看一次群里最近的消息,由模型自己判断要不要接、接什么话题;判断标准写在提示词里(宁可不说话;不接刷屏 / 吵架 / 敏感话题 / 广告 / 看不懂的梗 / 只针对某人的没头没尾对话),发言前还会过一遍长度、链接、``、重复校验。频率、时段、群范围全部由配置控制。

  • 只有 enabled: true 且 groups 显式列了群号才生效;留空 = 完全不跑。
  • 免打扰时段 quietHours、每小时 / 每天上限、两次发言最小间隔会先拦一道;不足 minNewMessages 条新消息则不进入判断。
  • 模型返回 speak=false 就安静待着;不会 @ 人、不发链接;dryRun: true 只记日志,方便先观察。
  • 判断用哪个模型:和识图完全一条路——都走 ctx.llm.stream()。provider / model 留空就跟随该群会话的模型, 零密钥、不需要另配端点;设置页里两项都是下拉(provider 列出 DSH 的 provider,model 跟随 provider 列模型), 想用便宜模型就显式选一个。群里还没有会话时按全局 agent preset 建同一条会话再取,不会因为「还没人说话」直接失败。 启动日志会打出「模型来源=跟随群会话模型 / 指定模型 x/y」;配置不全时明确告警,不会静默不发言。
  • 事件去重(eventGuard,默认开):模型很容易对一件早就聊完的事再回一次。开着的效果:
    • 最新消息超过 staleMinutes 分钟(默认 15)就当你已经错过接话窗口,不接;
    • 最新一条只是「行」「OK」这类应答(短、且不是提问)就不插话;
    • 最近几条只有两个人在来回说,视为他们的对话,不硬插;
    • 同一话题在 topicCooldownMinutes(默认 120)内说过一次就不再说,换说法也算同一件事。 关掉(eventGuard: false)退回旧行为:只做频率与文本去重,可能重复回复。
  • 关闭:设置页把「启用主动发言」关掉,或配置里 enabled: false。

向前兼容(旧会话不会被删)

旧版本建的 QQ 会话可能绑在别的工作区(0.7 时代是 /root,0.8 起是 <Documents>/deepseek-harness/QQ_msg)。 DSH 不允许跨工作区复用同一个会话 id,插件现在的处理是:

  1. 检测到 belongs to "..." 时不动旧会话(历史完整保留在原工作区);
  2. 用工作区限定的 id(qqbot-group-<群号>-ws<6位哈希>)新建一条,重试一次即成功;
  3. 日志里写明「改用 xxx;旧会话保留」,方便排查。

DSH 0.2 兼容

0.8.0 起,工具注册与访客工具闸统一走 Cordis 的注入契约:

  • 注册:先 ctx.get('tools'),未就绪时用 ctx.inject(['tools'], ...) 动态挂载;
  • 闸门:agent.ctx.get('tools').guard(...),取不到才回退 agent.ctx.tools(0.1.x 形态)。

tools 仍是可选服务:没有它时插件照常收发消息,只是少几个模型工具。 要求 DSH >= 0.2.0-rc.2;0.2.1-rc.2 与 0.2.1-rc.3 上都实测 8 个工具注册齐全(qqbot / qqconfig / qqgroup / qq_websearch / qq_webfetch / qq_imgsearch / qq_draw / qq_history)。

版本支持不写死:插件启动时读取当前实际加载的 DSH 版本并判定,日志形如 [dsh-qq-bot] 运行于 DSH 0.2.1-rc.3(在声明支持范围 0.1.7-0 <= v < 0.3.0-0 内); qqbot 工具 action="status" 返回的 runtime 字段给出同一结论。支持范围只维护 lib/compat.js 里的 SUPPORT_FLOOR / SUPPORT_CEILING 两个常量,DSH 出新版本不用改代码、也不用补版本表; 版本识别失败或超出范围只提示,不影响收发消息。