🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

QQ 机器人桥 FablerTan/dsh-qq-relay

DeepSeek Harness
FablerTan15 天前有提交已提供中文文档扫描:含敏感能力

把 QQ 机器人接入对话环境,收发消息与文件

30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/21 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add FablerTan/dsh-qq-relay
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么[目录]—
dsh 原生插件 · platform
暂无时间戳·判据 →
装得上吗[实装验证]—
本站尚未做安装检查
L4 · 真实安装等级说明 →
数据截至 2026/10/21·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/04·判据 →
还在维护吗[仓库]✓
活跃:最近一次提交在 15 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 103 处,展示前 92 条)
  • 请求外部主机192.168.1.10
    lib/client.js:417
    `;function Ge(){p.useEffect(()=>{if(document.getElementById(pe)!==null)return;const e=document.createElement("style");return e.id=pe,e.textContent=Qe,
  • 请求外部主机192.168.1.10
    lib/client.js:417
    `;function Ge(){p.useEffect(()=>{if(document.getElementById(pe)!==null)return;const e=document.createElement("style");return e.id=pe,e.textContent=Qe,
  • 请求外部主机openrouter.ai
    lib/client.js:417
    `;function Ge(){p.useEffect(()=>{if(document.getElementById(pe)!==null)return;const e=document.createElement("style");return e.id=pe,e.textContent=Qe,
  • 请求外部主机openrouter.ai
    lib/client.js:417
    `;function Ge(){p.useEffect(()=>{if(document.getElementById(pe)!==null)return;const e=document.createElement("style");return e.id=pe,e.textContent=Qe,
  • 请求外部主机openrouter.ai
    lib/client.js:417
    `;function Ge(){p.useEffect(()=>{if(document.getElementById(pe)!==null)return;const e=document.createElement("style");return e.id=pe,e.textContent=Qe,
  • 发起网络请求
    lib/client.js:417
    `;function Ge(){p.useEffect(()=>{if(document.getElementById(pe)!==null)return;const e=document.createElement("style");return e.id=pe,e.textContent=Qe,
  • 请求外部主机openrouter.ai
    lib/client.js:422
    已使用该预设的旧会话保留原组合;之后新建的会话不能再选它。`))return;z(!0),m(null),v(null);const g=await R.deletePreset(c);z(!1),g.ok?(S(g.value.status,!1),F(g.value.presets),D===c
  • 发起网络请求
    lib/client.js:422
    已使用该预设的旧会话保留原组合;之后新建的会话不能再选它。`))return;z(!0),m(null),v(null);const g=await R.deletePreset(c);z(!1),g.ok?(S(g.value.status,!1),F(g.value.presets),D===c
  • 读取本地文件
    lib/model/settings.js:320
    if (!existsSync(filePath)) return {}
  • 读取本地文件
    lib/model/settings.js:321
    const parsed = JSON.parse(readFileSync(filePath, 'utf8'))
  • 写入本地文件
    lib/model/settings.js:337
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/model/settings.js:344
    writeFileSync(tmp, JSON.stringify(payload, null, 2) + '\n', 'utf8')
  • 写入本地文件
    lib/model/settings.js:346
    renameSync(tmp, filePath)
  • 删除本地文件
    lib/model/settings.js:348
    try { unlinkSync(tmp) } catch {}
  • 读取本地文件
    lib/model/presets.js:190
    if (!existsSync(filePath)) return { presets: defaultPresets(), seeded: true, filePath }
  • 读取本地文件
    lib/model/presets.js:191
    const parsed = JSON.parse(readFileSync(filePath, 'utf8'))
  • 写入本地文件
    lib/model/presets.js:204
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/model/presets.js:207
    writeFileSync(tmp, JSON.stringify(payload, null, 2) + '\n', 'utf8')
  • 写入本地文件
    lib/model/presets.js:209
    renameSync(tmp, filePath)
  • 删除本地文件
    lib/model/presets.js:211
    try { unlinkSync(tmp) } catch {}
  • 发起网络请求
    lib/model/jev.js:125
    : (...args) => globalThis.fetch(...args)
  • 读取本地文件
    lib/model/mapping.js:32
    if (existsSync(filePath)) {
  • 读取本地文件
    lib/model/mapping.js:33
    const parsed = JSON.parse(readFileSync(filePath, 'utf8'))
  • 写入本地文件
    lib/model/mapping.js:50
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/model/mapping.js:52
    writeFileSync(tmp, JSON.stringify(load(), null, 2) + '\n', 'utf8')
  • 写入本地文件
    lib/model/mapping.js:54
    renameSync(tmp, filePath)
  • 删除本地文件
    lib/model/mapping.js:56
    try { unlinkSync(tmp) } catch {}
  • 读取本地文件
    lib/model/state.js:128
    if (existsSync(file)) {
  • 读取本地文件
    lib/model/state.js:129
    state = normalizeGroupState(JSON.parse(readFileSync(file, 'utf8')), Number(groupId) || 0)
  • 写入本地文件
    lib/model/state.js:143
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/model/state.js:146
    writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8')
  • 写入本地文件
    lib/model/state.js:148
    renameSync(tmp, file)
  • 删除本地文件
    lib/model/state.js:150
    try { unlinkSync(tmp) } catch {}
  • 读取本地文件
    lib/model/state.js:216
    names = readdirSync(dir)
  • 读取本地文件
    lib/model/state.js:246
    if (existsSync(marker) || !existsSync(legacyFile)) return result
  • 读取本地文件
    lib/model/state.js:249
    parsed = JSON.parse(readFileSync(legacyFile, 'utf8'))
  • 读取本地文件
    lib/model/state.js:258
    if (existsSync(store.fileFor(key))) continue
  • 写入本地文件
    lib/model/state.js:271
    mkdirSync(store.dir, { recursive: true })
  • 写入本地文件
    lib/model/state.js:272
    writeFileSync(marker, JSON.stringify({ migratedAt: Date.now(), groups: result.migrated }) + '\n', 'utf8')
  • 写入本地文件
    lib/model/state.js:295
    mkdirSync(dirname(filePath), { recursive: true })
  • 删除本地文件
    lib/model/state.js:308
    try { rmSync(filePath, { force: true }) } catch {}
  • 删除本地文件
    lib/model/state.js:321
    try { unlinkSync(filePath) } catch {}
  • 读取本地文件
    lib/model/chat-log.js:98
    if (!existsSync(targetDir)) return 0
  • 读取本地文件
    lib/model/chat-log.js:99
    for (const file of readdirSync(targetDir)) {
  • 删除本地文件
    lib/model/chat-log.js:104
    rmSync(join(targetDir, file), { force: true })
  • 写入本地文件
    lib/model/chat-log.js:121
    mkdirSync(targetDir, { recursive: true })
  • 写入本地文件
    lib/model/chat-log.js:122
    appendFileSync(fileFor(groupId, normalized.time), JSON.stringify(normalized) + '\n', 'utf8')
  • 读取本地文件
    lib/model/chat-log.js:135
    if (existsSync(file)) {
  • 读取本地文件
    lib/model/chat-log.js:137
    const text = readFileSync(file, 'utf8')
  • 读取本地文件
    lib/model/chat-log.js:175
    if (dir === '' || !existsSync(dir)) return 0
  • 读取本地文件
    lib/model/chat-log.js:176
    for (const group of readdirSync(dir, { withFileTypes: true })) {
  • 写入本地文件
    lib/model/chat-log.js:245
    mkdirSync(targetDir, { recursive: true })
  • 写入本地文件
    lib/model/chat-log.js:251
    renameSync(source, target)
  • 写入本地文件
    lib/model/chat-log.js:253
    copyFileSync(source, target)
  • 删除本地文件
    lib/model/chat-log.js:254
    unlinkSync(source)
  • 删除本地文件
    lib/model/chat-log.js:263
    if (readdirSync(join(legacyDir, groupId)).length === 0) rmdirSync(join(legacyDir, groupId))
  • 写入本地文件
    lib/model/chat-log.js:269
    mkdirSync(dirname(markerPath), { recursive: true })
  • 写入本地文件
    lib/model/gate-log.js:51
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/model/gate-log.js:52
    appendFileSync(this.fileFor(normalized.time), JSON.stringify(normalized) + '\n', 'utf8')
  • 读取本地文件
    lib/model/gate-log.js:64
    if (existsSync(file)) {
  • 读取本地文件
    lib/model/gate-log.js:66
    for (const line of readFileSync(file, 'utf8').split('\n')) {
  • 读取本地文件
    lib/model/gate-log.js:86
    if (!existsSync(dir)) return 0
  • 读取本地文件
    lib/model/gate-log.js:87
    for (const file of readdirSync(dir)) {
  • 删除本地文件
    lib/model/gate-log.js:92
    rmSync(join(dir, file), { force: true })
  • 写入本地文件
    lib/model/project.js:72
    mkdirSync(relayDir, { recursive: true })
  • 读取本地文件
    lib/model/project.js:74
    if (!existsSync(ignoreFile)) {
  • 写入本地文件
    lib/model/project.js:76
    writeFileSync(ignoreFile, GITIGNORE_CONTENT, 'utf8')
  • 读取本地文件
    lib/controller/comfy.js:211
    const text = readFileSync(join(skillDir, definition.template), 'utf8')
  • 执行外部命令来自配置/变量
    lib/controller/env.js:132
    child = spawn(bin, Array.isArray(args) ? args : [], {
  • 读取本地文件
    lib/controller/env.js:276
    if (!existsSync(path)) continue
  • 读取本地文件
    lib/controller/env.js:277
    const stat = statSync(path)
  • 读取本地文件
    lib/controller/env.js:281
    const parsed = parseComposeFile(readFileSync(path, 'utf8'))
  • 读取本地文件
    lib/controller/env.js:295
    entries = readdirSync(configDir)
  • 读取本地文件
    lib/controller/env.js:304
    const text = readFileSync(file, 'utf8')
  • 写入本地文件
    lib/controller/env.js:560
    mkdirSync(dirAbs, { recursive: true })
  • 写入本地文件
    lib/controller/env.js:561
    writeFileSync(target, renderComposeFile({ webuiToken: config.webuiToken }), 'utf8')
  • 读取本地文件
    lib/controller/env.js:578
    const parsed = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    lib/controller/env.js:606
    copyFileSync(file, backup)
  • 写入本地文件
    lib/controller/env.js:608
    writeFileSync(tmp, JSON.stringify(parsed, null, 2) + '\n', 'utf8')
  • 写入本地文件
    lib/controller/env.js:610
    renameSync(tmp, file)
  • 删除本地文件
    lib/controller/env.js:612
    try { unlinkSync(tmp) } catch {}
  • 写入本地文件
    lib/model/media.js:291
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/model/media.js:294
    writeFileSync(tmp, buffer)
  • 写入本地文件
    lib/model/media.js:296
    renameSync(tmp, target)
  • 删除本地文件
    lib/model/media.js:298
    try { unlinkSync(tmp) } catch {}
  • 读取本地文件
    lib/model/media.js:434
    if (!existsSync(groupDir)) continue
  • 读取本地文件
    lib/model/media.js:435
    for (const entry of readdirSync(groupDir, { withFileTypes: true })) {
  • 删除本地文件递归
    lib/model/media.js:439
    rmSync(join(groupDir, entry.name), { recursive: true, force: true })
  • 读取本地文件
    lib/controller/relay.js:789
    info = await stat(value)
  • 读取本地文件
    lib/controller/relay.js:798
    return await readFile(value)
  • 发起网络请求
    src/viewmodel/env.ts:19
    const api = createRelaySettingsApi({ fetch: (input, init) => globalThis.fetch(input, init) })
  • 发起网络请求
    src/viewmodel/settings.ts:44
    const api = createRelaySettingsApi({ fetch: (input, init) => globalThis.fetch(input, init) })
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
涉及外部主机:192.168.1.10、openrouter.ai

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

README

由 DeepSeek 最新模型翻译生成
dsh-qq-relay 把 DeepSeek Harness(DSH) 接入 QQ 机器人的自研插件(bundle 包,host + client 双半身)。 当前 v0.25.6:P1「@ 直通」已真机验收(2026-10-19);P1.5 基础配置页已落地(v0.17.0 起搬到插件面板里 dsh-qq-relay 组合包自己的页面,拆成「连接 / 环境 / 项目 / 行为 / 画图·视频 / 预设 / 记录」二级页面); P2.1/P2.2 消息闸门已落地(去抖 / 最小间隔 / 慢路 / 决策日志 / 发送队列, P2.3 jev 分类器已接(OpenRouter typesafe/jev-1.14),gate.enabled 默认关);P3 已落地 qq_send 主动推送与 QQ 侧审批;P4 已落地群项目目录 + 聊天记录 .qq-relay/ + prompt 拼接(2026-10-19); P4.1 已落地入向媒体落盘;P5 已落地 QQ 侧命令层(/new / /stop / /status / /help,见 docs/commands.md);P6 已落地回复体验(引用触发消息 / 处理中占位 / 直通连发合并,见 docs/reply.md);P4.2 已落地媒体双向(入向视频 / 语音落盘, qq_send 出向图片 / 文件,见 docs/medi
展开完整文档(README 剩余 1 节)
dsh-qq-relay

把 DeepSeek Harness(DSH) 接入 QQ 机器人的自研插件(bundle 包,host + client 双半身)。
当前 v0.25.6:P1「@ 直通」已真机验收(2026-10-19);P1.5 基础配置页已落地(v0.17.0 起搬到插件面板里 dsh-qq-relay 组合包自己的页面,拆成「连接 / 环境 / 项目 / 行为 / 画图·视频 / 预设 / 记录」二级页面); P2.1/P2.2 消息闸门已落地(去抖 / 最小间隔 / 慢路 / 决策日志 / 发送队列, P2.3 jev 分类器已接(OpenRouter typesafe/jev-1.14),gate.enabled 默认关);P3 已落地 qq_send 主动推送与 QQ 侧审批;P4 已落地群项目目录 + 聊天记录 .qq-relay/ + prompt 拼接(2026-10-19); P4.1 已落地入向媒体落盘;P5 已落地 QQ 侧命令层(/new / /stop / /status / /help,见 docs/commands.md);P6 已落地回复体验(引用触发消息 / 处理中占位 / 直通连发合并,见 docs/reply.md);P4.2 已落地媒体双向(入向视频 / 语音落盘, qq_send 出向图片 / 文件,见 docs/media.md);P4.3 已落地聊天记录清理 (保留天数 + 立即清理,见 docs/chat-context.md §7);P7 已落地client 面板增强 (闸门每群状态 / 最近消息 / 映射管理,见 docs/client.md);P8 已落地并真机验收戳一戳识别与回戳 (被戳接话 / 互戳记录 / qq_poke,见 docs/poke.md);P9 已落地入向群文件 (group_upload → 下载落盘 → 可选直通,见 docs/media.md §5.2);P10 已落地 配置页迁到插件面板 + 二级页面 + 跟随宿主主题(v0.17.0,见 docs/client.md);P13.1 已落地 角色预设与轻量人设(v0.19.0:插件自带「QQ 群聊」agent preset + 配置页可选 preset + prompt 注入人设与平台约定,见 docs/persona.md);v0.20.0 起预设可在配置页新建 / 编辑 / 删除,并逐预设设置模型、思考层级、权限(动态注册,无需改 profile);v0.21.0 加画图能力——探测配置的 ComfyUI,连上后把插件自带的 comfy-image skill 动态注册给 DSH,群里出图 / 改图后产物用 qq_send 发回群;v0.22.0 加视频生成——同一个 ComfyUI 再动态注册 comfy-video skill(MiniMax H3), qq_send 新增 videos(内联视频消息),群里可文生视频 / 首帧 / 参考图转视频(见 docs/comfy.md);v0.23.0 加环境一键配置——配置页「环境」页把 Docker / Colima / NapCat / 扫码登录 / OneBot 配置 / 插件回填收敛成 7 个检查与白名单动作,新机器可从零装到 relay 连上(见 docs/env.md);v0.24.0 把 jev(OpenRouter) API key 收进配置页「行为」页——保存即热启用分类器,环境变量 OPENROUTER_API_KEY 仍可兜底(只写不读);v0.25.0 修「一键修复」——改成逐步补全(装 Docker / 起 Colima / 写 compose / 起 NapCat 容器 / 写 OneBot / 回填),每完成一步自动重探再继续,不再跳过 NapCat 容器, token 两边都空时自动生成;v0.25.1 修探测可读性——docker / colima 失败显示 stderr 原文(不再只有「退出码 1」),docker 直连失败自动改用 --context colima 兜底, Colima 在跑但 daemon 连不上时给「重启容器引擎」,动作没推进就跳过继续后面的步骤;v0.25.3 回滚了 v0.25.2 的「按 endpoint 识别 OrbStack / Docker Desktop」——环境页仍只认 Colima(docker 指向别的引擎时可用 docker context use colima 切换);v0.25.4 修「OneBot 没配好就存不了 WebUI token」——wsUrl / accessToken / selfId 留空只算「待补全」,配置照常保存(relay 暂不启动),新机器可以先存环境页 token 再一键修复;v0.25.5 修 DSH 0.2.1-rc.2 下预设注册失败——组合表按新版 standard 去掉已移除的 toolFilter(旧模板会让整份 preset 变「未注册」),并加「宿主移除字段自动剔除重试」的兜底(见 docs/persona.md §2.2);v0.25.6 把 jev 的模型 id 与分类器地址也搬进「行为」页(gate.model / gate.jevEndpoint,可指向兼容 Decisions 协议的代理,保存即热切换分类器、不重建 relay)。设计见 docs/architecture.md,协议端选型与部署见 docs/onebot-channel.md,配置页契约见 docs/client.md。

v0.1.0:按上级技能 dsh-plugin-project-init 初始化。
本项目不使用 openspec(用户 2026-10-16 决定)——没有 openspec/,也没有 .agents/skills/openspec-;规格与设计随 README 与 docs/ 走。
空骨架唯一的可见副作用:host 半身加载时打一行日志,用来确认 link + bundle 组合生效。
装完 / 改完需重启对应配置档的 GUI(由你执行,本插件不自行 kill / 重启 DSH)。

公开仓库说明:README 与 docs/ 里的 QQ 号、群号、昵称、会话 / 文件路径、内网 IP 均为脱敏示例,不含私有部署信息。

用途(P1 已实现;后续阶段见 roadmap)

DSH 与 QQ 之间的双向桥:

- QQ → DSH:QQ 群里的消息经过「消息闸门」过滤后投给某个 DSH 会话,智能体跑完把回复回发群聊;
- DSH → QQ:会话 / 工具侧主动往 QQ 推消息(通知、任务完成提醒等,P3)。

已定案(2026-10-18):

- 协议端:真实 QQ 号 + NapCat + OneBot 11 正向 WebSocket——DSH 单轮可能超过 5 分钟,官方机器人的群聊被动回复窗口(5 分钟)会让长任务结果只能改走主动消息(受配额限制、可被关闭);真实 QQ 号可随时发送。官方通道方案保留为备选(docs/official-bot-channel.md)。
- 账号:用一个 QQ 小号登录 NapCat 作 bot,主号操作;群聊优先(群 @ 直通 + 群内普通消息经闸门过滤);单聊放 P3。
- 消息闸门:10 分钟快路 + 2 小时慢路 + jev 分类器,@ 直通、最小发言间隔(见 docs/gate.md)。
- 运行形态:host 半身单进程插件;client 半身在插件面板(左栏「插件」图标)的组合包页里提供配置页(不做独立应用);插件不管理配置档。
- 已知代价:非官方协议端有封号风险,需要常开 QQ 客户端 + NapCat。
- 部署形态:NapCat 跑在 Docker(macOS 用 Colima)里,插件连 127.0.0.1:3001;macOS 原生 Shell 在 macOS 26 + arm64 上不可用(见 onebot-channel.md §3.1)。

已交付:OneBot 接入与 @ 直通(P1)、基础配置页(P1.5:连接状态 / 配置编辑 / 群映射)、消息闸门骨架与收尾(P2.1/P2.2:去抖 / 最小间隔 / 慢路 / 状态持久化 / 决策日志 / 发送队列)、 jev 分类器(P2.3:OpenRouter typesafe/jev-1.14,阈值判定 + fail closed;v0.25.6 起模型 id / 地址 / key 可在「行为」页改)、 DSH → QQ 主动推送工具 qq_send(P3)、QQ 侧文本审批(P3:群里回复「同意 / 拒绝」)、群项目目录与聊天记录 .qq-relay/ + prompt 拼接(P4,见 chat-context.md)、入向图片 / 表情落盘与路径引用(P4.1,见 media.md)、 QQ 侧命令层(P5:/new 换会话 / /stop 中断 / /status 状态 / /help,群主 / 管理员可用,见 commands.md)、回复体验(P6:引用触发消息 / 8 秒「处理中」占位 / 400ms 连发合并,见 reply.md)、媒体双向(P4.2:入向视频 / 语音落盘, qq_send 出向图片 / 文件,见 media.md)、聊天记录清理(P4.3:保留天数 + 立即清理,见 chat-context.md §7)、client 面板增强(P7:闸门每群状态 / 最近消息 / 映射增删,见 client.md)、戳一戳识别与回戳(P8:被戳接话 / 互戳记录 / qq_poke,见 poke.md)、入向群文件(P9:group_upload → 下载落盘 → 可选直通,见 media.md §5.2)、触发规则修正 + 代答(v0.18.0:引用别人消息 / @ 别人不再触发;可选「@ 名单里的人且 TA 最近没说话时代 TA 回应」,见 gate.md §2.1/§2.6)、角色预设与轻量人设(v0.19.0:插件自带「QQ 群聊」智能体 preset / 配置页可选 preset / prompt 注入人设与平台约定,见 persona.md)、预设管理与预设级模型 / 思考层级 / 权限(v0.20.0:配置页「预设」页可增删改,presets.json + 动态注册,见 persona.md §2)、画图 / 视频能力(v0.21.0 / v0.22.0:ComfyUI 探测 + 动态注册 comfy-image(出图)/ comfy-video(视频 MiniMax H3)两个 runtime 技能,配置页「画图 / 视频」页 + POST /api/comfy/test; qq_send 支持 images / videos,见 comfy.md)。
仍未定案:群审批(按钮)——先落 docs/ 再实现。

目录结构

dsh-qq-relay/
├── package.json # name + dsh.bundle.patch + dsh.client(link 加载的必要元数据) ├── cordis.patch.yml # bundle patch:把插件行插入 profile roster ├── vite.config.ts # client 半身库模式构建(React / @deepseek-ai 全 external) ├── tsconfig.json # client 半身类型检查(include 只有 src) ├── pnpm-workspace.yaml # pnpm 11 构建设置(放行 esbuild、关自动 peer 安装) ├── lib/ │ ├── index.js # host 半身手写组合根(读配置、单实例锁、装配) │ ├── controller/ │ │ ├── onebot/ # codec(事件编解码)· client(WS)· api(action 封装) │ │ ├── runtime.js # relay 运行实例:整份配置热替换(配置页保存后重建连接) │ │ ├── config-routes.js # 配置页 HTTP 面(status / config / reconnect / rebind / cleanup / messages / mapping / presets / comfy test / env) │ │ ├── http.js # JSON 信封与请求体读取共用件 │ │ ├── gate.js # 闸门调度:去抖批次 / 最小间隔 / 慢路定时(分类器注入) │ │ ├── tools.js # qq_send(文本 / 图片 / 视频 / 文件)/ qq_poke 工具定义(手写 ToolDefinition,零外部依赖) │ │ ├── approval.js # QQ 侧审批桥:approval/request → 群消息 → 文本命令决定 │ │ ├── relay.js # 入向判定 + 会话映射 + 出向回发 + 断线发送队列 + 主动推送 │ │ ├── comfy.js # ComfyUI 探测 + 动态注册 comfy-image / comfy-video 两个技能(v0.21.0 / v0.22.0) │ │ └── env.js # 环境页探测 + 白名单动作(Docker / Colima / compose / NapCat / OneBot / 回填,v0.23.0) │ ├── model/ # 配置 / 设置覆盖 / 会话映射 / 状态+锁 / 群项目目录 / 聊天日志 / 清理 / 面板 / 媒体落盘 / prompt 拼接 / 决策日志 / 回复切分 / 闸门 / jev / 审批 / comfy 配置与技能模板渲染 / environment 配置与步骤求值 │ ├── skills/comfy-image/ # 插件自带画图 / 视频技能(SKILL.md / SKILL-video.md 模板 / 运行器 / 7 个工作流 / 参考),随包发布 │ └── client.js # client 半身构建产物(已入库,clone 即可加载) ├── test/ # host 单测(node:test,零依赖;含配置页 / apply 集成) ├── src/ # client 半身源码:model(HTTP 信封、表单解析)→ viewmodel(呈现态与动作)→ view(组件) │ ├── client.tsx # 组合根:把配置页注册进 plugins.bundle.config(key = 包名) │ ├── model/ # api.ts(HTTP)· form.ts(表单 ↔ patch 纯逻辑) │ ├── viewmodel/ # settings.ts(状态 / 动作 hook、状态文案、二级页面定义)· env.ts(环境页轮询 / 动作) │ └── view/ # SettingsPage(外壳 + 状态行 + 二级页面导航)· controls · theme(CSS)· panels/(含 ComfyPanel / EnvPanel) ├── scripts/ # wrap-client(产物包装)· check-syntax · check-docs ├── docs/ # 设计文档(一文档一主题:架构 / 通道 / 闸门 / 画图 / 路线…)
├── README.md # 本文件 └── AGENTS.md # 本插件开发纪律(文档在哪 + pnpm check 闸门)

文档索引

| 文档 | 写什么 | 何时看 |
| README.md | 总览 / 用途 / 用法 | 任何任务第一步 |
| AGENTS.md | 本插件纪律:文档位置速查 + pnpm check 闸门 | 动手前 |
| docs/architecture.md | 总体架构:形态、模块、事件流、会话映射、安全、运行前提 | 动手前 / 改设计时 |
| docs/client.md | client 半身:插件面板里的配置页(连接 / 环境 / 项目 / 行为 / 画图·视频 / 预设 / 记录)、注册落点、配置层序、HTTP 契约、热更新语义、主题化样式 | 改配置页 / 调保存逻辑时 |
| docs/config.md | 配置总表:每个字段的默认值 / 范围 / 现在能否在配置页改 / 生效方式 + 写死常量清单 + 加字段的改动面 | 调配置 / 决定哪些进配置页时 |
| docs/onebot-channel.md | 真实 QQ 号通道:账号模型、NapCat 部署、WebSocket 事件与 API、封号风险 | 接协议端、部署 NapCat 时 |
| docs/official-bot-channel.md | 备选(当前不采用):官方 QQ 机器人通道事实与限制 | 回退 / 对比时 |
| docs/gate.md | 消息闸门:快路 / 慢路、jev、去重幂等、状态持久化、配置 | 改群聊行为时 |
| docs/chat-context.md | 聊天记录落盘与消息拼接:.qq-relay/ 隐藏目录、群项目目录、prompt 格式、迁移 | 改聊天记录 / prompt 拼接时 |
| docs/media.md | 媒体:入向图片 / 表情 / 视频 / 语音 / 群文件落盘、聊天记录与 prompt 路径、qq_send 出向图片 / 视频 / 文件与画图 / 视频产物 | 改媒体 / 排查媒体收发时 |
| docs/comfy.md | 画图与视频:ComfyUI 配置 / 探测、动态注册 comfy-image / comfy-video 两个技能的生命周期与模板渲染、图片 + 视频工作流、QQ 群用法与安全边界 | 开画图 / 视频、排查生成不生效时 |
| docs/env.md | 环境一键配置(v0.23.0;v0.25.0 起「一键修复」逐步补全):environment 配置段、7 个检查步骤、白名单动作与固定 argv、逐步修复链与 fixPlan、HTTP 契约与安全边界 | 新机器装 NapCat / 排查「环境」页、改环境动作时 |
| docs/commands.md | QQ 侧命令层:/new / /stop / /status / /help、解析与权限、与闸门的关系 | 改命令 / 排查群内命令不生效时 |
| docs/reply.md | 出向回复体验:引用触发消息、处理中占位、直通连发合并、reply 配置 | 改回复行为 / 排查回复对不上号时 |
| docs/tools.md | qq_send 主动推送(文本 / 图片 / 视频 / 文件)/ qq_poke 戳一戳:契约、安全、离线语义、验证 | 改工具 / 排查推送失败时 |
| docs/poke.md | 戳一戳:事件契约、被戳接话、冷却、聊天记录、qq_poke 工具 | 改戳一戳 / 排查戳没反应时 |
| docs/approval.md | QQ 侧审批:群里文本命令、角色限制、超时 / 取消 / 离线语义 | 改审批 / 排查审批不生效时 |
| docs/persona.md | 角色预设与轻量人设:插件自带 qq-relay 智能体预设、配置页选择、prompt 人设行与平台约定 | 调人设 / 选预设 / 排查回复风格时 |
| docs/changelog.md | 版本历史(按版本倒序):每版变更、验收结果与用户前置动作 | 追历史 / 判断某行为何时引入时 |
| docs/roadmap.md | 分期、验收标准、风险与用户前置动作 | 排期 / 决定下一步时 |
| ../../AGENTS.md、../../docs/ | 仓库级纪律、渲染树选座速查 | 跨插件协作、装配置档、选 slot |

激活方式(已链接 dwdev,该配置档已清空为只装本插件)

dsh plugin --profile <profile> add link:/path/to/dsh-qq-relay
装完需重启该 profile 的 GUI 才生效(bundle 组合发生在启动时)

- 回滚:dsh plugin --profile <profile> remove dsh-qq-relay(不影响本仓库)。
- 验证:重启后宿主日志里应出现 dsh-qq-relay 的加载日志;左栏「插件」面板里找到 dsh-qq-relay 组合包,它的页面里能看到配置(连接状态 / 表单 / 群项目目录)。
- 链接现状记录在上级 PROFILES.md。

开发循环

pnpm install # devDependencies:vite / react / typescript(插件运行时零外部依赖)
pnpm build # src/client.tsx -> dist/client.cjs -> lib/client.js(wrap-client 包装)
pnpm watch # watch 模式:改 src 自动 rebuild + wrap,浏览器刷新可见
pnpm check # 验收闸门:语法 + 元数据 + tsc --noEmit + 文档↔源码配对
pnpm test # host 纯逻辑单测(node:test,零依赖)

- lib/index.js 是手写 host 源码(不经构建);lib/client.js 是 client 半身构建产物,已入库——clone 后不装依赖也能被宿主加载。只有改了 src/ 才需要 pnpm build / pnpm watch。
- host / client 热载差异:改 host 半身要重启配置档的 GUI;client 半身由宿主的 client 热换机制在浏览器刷新后生效。

发布(GitHub Release)

1. 改 package.json 的 version,并在 docs/changelog.md 顶部加同版本段(## vX.Y.Z …,这一段就是 Release 正文);
2. 本地 pnpm build 并提交 lib/client.js,然后提交、打 tag、推送:

git tag vX.Y.Z
git push origin vX.Y.Z

3. GitHub Actions 的 Release 工作流 会跑 pnpm check / pnpm test / pnpm build,校验 tag 与 package.json 一致、lib/client.js 已更新,从 docs/changelog.md 取对应版本段作为 Release 正文,并附上 npm pack 的 .tgz。
也可以在 Actions 页手动触发并填 tag;版本号与 changelog 不一致会直接失败。
预览分享卡片长什么样 ↓FablerTan/dsh-qq-relay
分享 · DeepSeek Harness Hub
FablerTan/dsh-qq-relay

把 QQ 机器人接入对话环境,收发消息与文件

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部