Repository navigation
Releases: flyinstar/dsh-qq-bridge
Release list
dsh-qq-bridge v0.2.0
这是 0.2.0 的发布包(解压即用,里面带了一份干净的 NapCat.Shell)。
- 第一次装:解压后照 docs/install.md 走;Windows 上双击
scripts\napcat-setup.cmd就能把 NapCat 配好(--start顺手启动)。 - 从 0.1.x 升级:先读下面
BREAKING与「默认值变化」两节 —— 这一版删掉了 JM 下载旁路,并且新装默认就会说话(proactive/activeFollow都开、被 @ 的消息撤回了默认不回)。 - 完整说明:CHANGELOG.md;配置项逐条在 docs/configuration.md,也可以在设置页「QQ Bridge」里改。
0.2.0 — 2026-10-18
这一版是两段。前半段按重构设计做了四层拆分 —— 人格 / 表达 / 感知 / 运维:把「你是谁」
「怎么说」「看到了什么」「出事了怎么查」从 index.js 那一坨提示词与 if 里拆成可单独测试、
可单独回滚的模块(persona.js / style.js / modalities.js + links.js + interactions.js /
ops.js + scripts/ops.mjs),设置页也跟着重做(扁平区块 / 搜索 / 官方控件)。后半段是随后一周
按真实事故补齐的六块,各有各的来由:
- 默认值改成「本仓库这套配置」:安全面(
agentPreset/allowTools)与主动行为
(proactive/activeFollow/ 撤回的处理方式)都换成实际在跑的这套,影响见下面「默认值变化」。 - 长任务期间的 @ 攒成一波(新功能,见说话分册):
它在跑长任务时收到的 @ 不再排成几十轮排队回复,而是等它忙完合成一轮、带着「各自多旧」挑着接。 - 定时提醒(新功能,见定时提醒):把「明早 8 点提醒我交作业」变成一条
落盘的任务,到点由插件直接发@目标 + 文本,不依赖模型醒着。 - 长期记忆与表情包的写入面修整(散在「行为变化」「修复」两节):起因是三起真实事故 ——
模型编造.qq-media/<哈希>发图、COGNITION.md里 3 条格式不全的条目从来没进过提示词、
迁移时按行号回填差点吃掉记忆。 - 被撤回的消息(新功能,见被撤回的消息):撤回事件到达时先清掉指向它的
引用(不清的话整条回复都发不出去),再按配置决定假装没看到 / 就地调侃 / 补一句。 - 人设模板与编写提示词(新功能,见写人设:模板与问答):两份复制
就能用的模板、一份会一题一题问你的提示词、一个不启动机器人就能跑的自检脚本。
升级前先读两节:BREAKING(删掉 JM 下载旁路、burstSingleMaxChars 语义变了)与
「默认值变化」—— 新装默认就按本仓库这套跑:群里会自己开口、没被 @ 也会接话。
BREAKING
- 移除 JM(禁漫)下载旁路。
jm.js、scripts/jm_download.py、qq_jm_pdf工具、/jm指令、
15 个jm*配置项与test/jm.test.mjs全部删除;package.json的exports["./jm.js"]与
files里的jm.js、.gitignore里的.qq-jm/一并去掉。cordis.patch.yml里残留的jm*会变成惰性字段(schemastery 对未知键透传),不会启动失败;
allowTools里残留qq_jm_pdf时插件只记一条 warn(白名单里有不存在的工具名)。/jm 422866不再是特殊指令,作为普通消息进模型(群里仍然要 @ 或满足触发条件)。- 数据不删:
.qq-jm/里的旧 PDF、.venv/里的 jmcomic 依赖都原样留着。要清就显式跑
node scripts/ops.mjs cleanup jm --apply(只删.qq-jm/,先列文件再生效,绝不碰.venv)。
burstSingleMaxChars语义变了:从「短回复强制并成一条」变成「聊天型 / 信息型的门槛」。
想要条数上限可以显式设burstMaxMessages(默认 0 = 不限)。- 短气泡句末的「。」会在发送前被剥掉(
stripShortPeriod,默认开)。只动句末唯一的一个
「。」;问号、感叹号、省略号、句中句号都不碰。 - 图片不再挂引用:整轮只有图时引用直接丢弃,有文字时引用只挂第一个文字气泡。
默认值变化(可能影响已有部署)
config.js 的 schema 默认值改成了「本仓库这套配置」,新装即按这套跑。老部署如果在
profile 的 cordis.patch.yml 里写过这些字段,自己的值仍然优先;没写的会跟着变:
| 字段 | 旧默认 | 新默认 | 影响 |
|---|---|---|---|
agentPreset |
''(部署默认预设) |
qq |
新装默认挂本 bundle 的最小预设,而不是可能带 shell 的 standard |
allowTools |
[](不限制) |
17 个工具名 | 默认收窄到「只读文件 + 检索 + 联网 + 十个 qq_*」;qq 预设里的 write / edit 默认拿不到 |
activeFollow |
false |
true |
被叫到之后,窗口内同一个人接着说话也算数(还要过抽卡);群里聊热了会进入多人搭话 |
proactive |
false |
true |
冷场后会自己开口(仍有 7 道闸,安静阈值 45 分钟) |
burstSingleMaxChars |
0(用 80) |
100 |
≤100 字按聊天型处理(发送途中对方接话就停),超过的按信息型完整发送 |
recallAddressed |
answer |
drop |
被 @ 的消息撤回后真的不回(原本是「照常答、只是不引用」) |
recallPrivate |
false |
true |
私聊撤回也处理(还没投给模型的那条不回) |
想回到「最保守」的那一套,在部署的 patch 里显式写回去即可:
agentPreset: ''(或 none)、allowTools: []、activeFollow: false、proactive: false、
recallAddressed: answer、recallPrivate: false。
人格与表达(新的两层)
persona.js+cwd/.qq-persona/PERSONA.md:人设只写「是谁」——身份 / 背景 / 性格 / 场景。
性格按名字|底色/点缀|强度|触发条件|表现|禁忌一行一条,底色最多一条(始终在场,
是语气的地板不是台词),点缀按触发条件上场、一次最多演一个。旧配置persona在没有
PERSONA.md时仍然生效;文件存在时它被忽略并记一条 info(避免两套人设叠加)。
文件写坏(半截、只剩标题)时退回上一版,再退回配置 / 内置人设 —— 人设写坏绝不让机器人不说话。style.js+cwd/.qq-persona/STYLE.md:表达只写「怎么说」——气泡、标点、表情、私聊 / 群聊。
机器参数(burst*/stripShortPeriod/interjectQuote)在配置里,散文与参数互不解析。- 气泡条数:模型写几条就发几条,插件没有掷骰 / 概率、没有条数上限、也不把多出来的
并进最后一条。burstMaxMessages是唯一可选的条数上限(默认 0 = 关)。发送途中对方又说话了,
聊天型剩下的气泡就不再发(信息型该讲完还得讲完)。 - 私聊补一句(L2,默认开):回完对方、对方安静 15 秒后,最多再开一轮看看有没有想补充的
(可 SKIP)。它不搭proactive那趟车(那是群里主动开口的总开关),有自己的
privateAfterthought*开关 / 概率 / 日上限;空消息、命令、出错、SKIP 之后的轮次不补,
对方在这期间接了话也不补。 - 提示词槽位化:场景 → 人设 → 表达契约 → 记忆索引 + 热梗 → 能力清单 → 额外要求 →
硬性规则(最后,最高优先级)。每条规则只出现一次;「绝不两拍」拆成两句(信息型的
「短 + 长」仍然禁止,聊天型的「短 + 短」允许)。 - 本部署那份混着风格规则的
persona已从cordis.patch.yml搬进.qq-persona/PERSONA.md
与.qq-persona/STYLE.md(蓝毛女仆人设拆成「身份 / 背景 / 温柔底色 / 傲娇点缀 / 场景」+
「总体 / 标点 / 表情包 / 私聊 / 群聊」)。
群聊搭话(治「莫名其妙」)
speech-policy.js:把准入、A/C 抽卡、插话准入收成一个纯判据,返回intent
(addressed/continued/interject/none)。admit只剩「凑入参 + 记账」。- 插话的投递文案换了:不再是
kaguya: 晚上吃啥(kaguya 并没有问它),而是
「(群里在聊天,这句不是叫你的。kaguya 说:晚上吃啥)」+「你要接就接这一句……不想接就只写
SKIP」。speakerLine也按 intent 换措辞,不再一律说「刚跟你说话的人」。 - 插话强制引用锚点(
interjectQuote,默认开):引用的是admit时快照的具体
message_id,不再看replyStyle: auto的漂移判断(那个判断在「紧接着回」时恰好不引用,
于是插话裸奔)。引用发不出去 → 退化成 @ 说话人 → 再不行才裸文本,都记 warn。 - C 路准入收紧四条:@ 了别人、命令、纯媒体(没有文字)、机器人自己 —— 一条都不接。
认知与表情包分离
cognition.js+ 两个文件:COGNITION.md只装语言(梗 / 黑话 / 缩写 / 谐音,键是
文字写法含变体),STICKERS.md装表情包(键是收藏后的文件名)。认知库里不许出现
文件路径,表情包目录里不许出现.qq-media(那是会过期的缓存)。qq_save_sticker新增可选meaning/scene:存完图顺手往STICKERS.md补一行;没给
meaning时进「未收藏」待办区(不进提示词)。目录写失败不回滚图片。- 提示词里的表情包目录改成「名字(含义)」而不是裸文件名;新增「最近学的梗」段(最近 15 条
「置信:确认」,≤1200 字),谐音梗不用先 search 再答。 - skill 拆分:新增
learn-meme(文字梗 →COGNITION.md)与learn-sticker(表情包 →
先收藏再写STICKERS.md);update-cognition保留为一句话分诊的兼容壳;
message-digest的认知段落改成分诊写法。 - 新增
scripts/migrate-cognition.mjs:把老COGNITION.md里「以路径为键」的条目一次性迁到
STICKERS.md(用画面指纹认同一张重新编码过的图;认不出就标「原图已过期」放进未收藏区)。
默认 dry-run,--apply才写,写前备份,--json给报告。
感知层
modalities.js:消息段 →Observation→ 占位符渲染。系统表情带名字、语音给时长、
视频给时长 + 封面路径、文件给大小、合并转发给条数、json/xml卡片解析出标题 / 域名 / 链接。
onebot.js的messageToText改为委托它(既有输出逐字节不变)。links.js:卡片解析 + 可选的 URL 预览(linkPreview默认关)。SSRF 防护覆盖
scheme / userinfo / localhost /*.local/*.internal/ 私有与保留网段 / 各种数值变形的
IPv4 / IPv6 回环与内网,每一跳重定向都重新校验;超时、字节上限、只收text/html/text/plain。interactions.js+ 戳一戳:以前handleEvent第一行就把notice全丢了,现在被戳会回应。
pokeMode=random(默认,按pokeReplyChance回戳或走一轮)/poke/text/ignore;
每个会话pokeCooldownMs冷却 +pokeDailyMax每日上限(防两个机器人互戳)。
只回应戳机器人自己的,戳别人一律不记。新增qq_poke工具(群里必须显式指定戳谁)。- 视频只下封面(
videoCover默认开,videoMaxBytes: 0不下载整段);语音只做时长占位,
不下载、不转写、不起任何外部进程。
运维
ops.js(纯计划)+scripts/ops.mjs(执行器):一套命令跨平台。
doctor/status/restart/logs/install/uninstall/napcat/
migrate cognition/cleanup jm/selftest,都支持--json(stdout 只有一个对象,
版本号schema: 1;人话走 stderr)。- 退出码:
0全部通过 /1有 error 级 findings /2需要人工或提权 /3脚本自身出错。 --apply默认关:写 cwd 外的步骤(systemd unit、计划任务、NapCat 配置、profile)都带
requires: human|root,agent 默认只拿计划、把命令原样贴给人。幂等(写前备份,重复执行不重复写)。- 新增
docs/ops.md(给 agent 的排查配方)。
新增:定时提醒
qq_remind_create/qq_remind_list/qq_remind_cancel:让 Agent 把「明早 8 点提醒我
交作业」变成一条持久化任务;到点由插件直接发@目标 + 文本,不经过模型,重启不丢。- 三种模式:
text原样发固定文案(最可靠、不花 token);agent让模型用自己的语气说,
失败 / 超时 / SKIP 都退回固定文案;prompt到点按指令做事(可调工具,默认关闭,只给受信者)。
reminderModes是上限,默认agent。 - 时间:一次性或每天 / 工作日 / 每周 / 每月;
at用本地YYYY-MM-DDTHH:MM,也接受
in_minutes;周期任务的第一次已过去时自动滚到下一个未来时刻,monthly在小月夹到月末。 - 安全默认值:只能提醒创建者自己;
@别人与@全体默认关(reminderMentionOthers/
reminderAllowAll);每人每天 5 条、每会话 20 条待发;reminderUsers是受信名单。
提醒正文原样发送、不解析[[图片]]/[[@]]标记,@ 只来自创建时解析好的 QQ 号。 - 重启 / 断线:任务文件
data/reminders.json;错过时刻在reminderGraceMs(默认 10 分钟)
内补发,超过标missed;周期任务不补历史;agent 轮跑到一半进程被杀时补一条固定文案。 - 发送是 at-most-once:先记账再发,宁可重启后补一条文本,也不重复 @ 全体。
/reset不影响提醒:任务属于会话而不是某一轮 Session。- 设置页能看见记着的任务:新增独立的「提醒」tab(提醒同样服务私聊,不再塞在「群聊」
里),列表放在这一页最上面,按状态列出待发与最近历史(时间 / 模式 / 目标 / @ 名单 /
创建者 / 已发次数 / 上次错误),每 15 秒自动重读,也可以手动刷新;数据走 Connection
认证栅栏内的一条只读路由。 - 详见定时提醒;设置页在「提醒」tab。
被撤回的消息(新功能,见 docs/recall.md)
三层:安全层与数据层永远生效,行为层才是配置项。
- 不再引用一条已经不在的消息(安全层,所有人受益):回复本来是带着
reply段指向被回复的
那条发出去的,而sendOne失败时sendParts会整条停下 —— 撤回一到,那条回复剩下的气泡
一起发不出去,群里看到的只是「它没理人」。现在撤回事件到达时先清掉这个会话里所有指向它的
引用(messageId清空、userId留着,于是replyStyle四档都自动退化成正确的降级)。 - 记一份「最近看见过什么」(数据层):QQ 不提供回查已撤回消息的接口,所以每个会话在内存
里留最近 30 条(id / 发送者 / 显示名 / 占位符正文 / 有没有 @ 过它 / 投递与回复进度)。不落盘,
重启即清;对不上的撤回只记一条 info(「没看见」是有效状态,不是错误)。 recallMode:off(只做上面两层)/ignore(默认,还没投递的那条不再投给模型;被 @ 的
再按recallAddressed决定)/tease(被 @ 的那条就地改写成撤回提示,这一轮直接调侃;
已经回过的按闸门补一句)。recallAddressed(默认drop):被 @ 的那条还没投出去就撤回时,drop是真的不回
(「撤回 = 当没收到」),answer才照常答、只是不引用(@ 是明确点名,认为点名了就该有回应
的话选它)。recallScope(默认to-me):all才会为群里别人的撤回开口(那等于承认它每条都看,
语义接近proactive,所以默认关)。- 补一句要过一串闸门:
recallChance/recallCooldownMs/recallDailyMax/recallMaxAgeMs
/recallScope,外加几条硬性的 —— 自己的消息被撤不处理、管理员代撤(operator_id ≠ user_id)
不调侃、这一轮还在飞时不补刀。掷中即消耗(SKIP也算)。 - 默认不给原文(
recallTeaseQuote: false):复述别人撤掉的话是最常见的社交事故。 recallPrivate(默认开):私聊撤回也处理(还没投给模型的那条同样不回);tease时建议
关掉 —— 私聊里调侃「你撤什么」很容易变成冒犯。清引用照做。- 长期记忆留一行 tombstone(
recallMemoryMark,默认开):转录是只追加的原料,所以追加一行
kind: "recall"而不是删行 —— 摘要 Agent 一眼看得出「这条撤了、别当事实」,原文仍然在。 - 本仓库的
cordis.patch.yml把这三项显式写出来钉住:recallMode: ignore+
recallAddressed: drop+recallPrivate: true(现在也是 schema 的默认值;写出来是为了将来
改默认时本部署不跟着漂)。 - 新模块
recall.js(4 个纯函数,12 条闸门可单测)+test/recall.test.mjs(15 条)+ bridge
里bootRecall那组(11 条:三种位置 × 三种模式、环境上下文摘除、转录标记)。
新增:长期记忆 v2(检索式注入 / 人物表 / 冷热分层)
起因是一个真实事故:群里的一个说法机器人记过、文件里也还在,但聊天时完全接不住 —...
dsh-qq-bridge v0.1.1
更新记录
版本号按语义化版本走。0.x 阶段配置项与行为都可能变,
每次改动以 Config 的 schema 与 README 为准。
0.1.1 — 2026-10-15
修 Windows 桌面版安装流程里实测出来的一批毛病。全部在「NapCat 那一侧 / 安装 / 文档 / 测试」
这一层,QQ 会话链路一行没动。
这一版没有真的发出去(原计划作为首个补丁版),所以后面「发布包验收」查出来的问题直接并进了
同一节 —— 拿到这个 tag 的人看到的就是修完的版本。最要紧的两条:装进 profile 时校验链接指着谁
(原来会静默加载旧代码),以及.cmd去找桌面版自带的那份 Node(原来不装 Node 就装不上)。
发布包验收(拿 0.1.1 的 zip 在新机器上照 README 走一遍)查出来的问题
- 装进 profile:链接已存在时校验它指着谁(原来会静默装错代码)。原来只判断
node_modules\@qqbot\dsh-plugin-qq存在与否:存在就打印「链接已存在」跳过,可
package.json里的link:已经被改成了新仓库路径。而解析完全由链接决定(DSH 的
resolveBundleDir走createRequire(profile/package.json).resolve.paths(),那行声明只给
pnpm / 插件管理界面看),于是出现「manifest 说加载 A、实际加载 B」而一句报错都没有。
实测现场:链接还指着 dev 仓库、manifest 已经改成发布包目录。现在读链接的实际指向再比
(pluginLinkState,纯函数、有单测):指别处就删掉重建,并把原来指着哪打出来;指着本仓库就
什么都不做;那里如果是一个真目录/文件(手工拷进去的插件),不删它,只提醒「加载的是它、
不是本仓库」。三个读法是一串实测出来的回退:Windows 上 junction 只有realpathSync.native
跟得动(readlinkSync对 reparse point 一律EINVAL,普通realpathSync有时把链接自己当
结果返回),目标已被删掉时只剩readlinkSync答得出来。删链接也不能用unlink:junction
对它一律EPERM,要退到rmdir(实测:删完链接消失、目标目录里的文件一个不少)。这两条都有
真建 junction 的用例钉着。 napcat-setup.cmd:桌面版自带的那份 Node 现在找得到(原来挡住新用户)。机器 PATH 上没有
node时脚本直接失败,而桌面版自带一份完整的 Node(<DSH>\resources\runtime\<版本>\dependencies\node\bin\node.exe,
实测 v24.x)。现在按「PATH →DSH_DESKTOP_NODE_EXECUTABLE→ 桌面版安装目录」的顺序找,常见
位置都探;一个都没找到才报错,并把确切路径的写法打出来。新增--where只回答「用的是哪份
node」。刻意不退到resources\runtime\bin\node.cmd:那份包装自己是
"%DSH_DESKTOP_NODE_EXECUTABLE%" --expose-internals %*,而桌面版双击启动时那个变量不存在 ——
调它只会得到一句'" "' is not recognized(实测)。这个文件里另外记了三个 cmd 的坑:
if defined认「一个空格」的值、引号套引号会报没有行号的语法错、以及这个文件必须是 CRLF。package.json的files补上漏掉的两个模块:mention.js(index.js/tools.js都 import
它)与desktop.js(scripts/napcat-setup.mjsimport 它)。zip 走git archive,发布包不受
影响;但任何npm pack/pnpm add <tarball>装出来的包都会缺文件 →ERR_MODULE_NOT_FOUND
起不来。现在是private: true只发 zip,所以是个潜在的雷,先拆掉。- 文档:「Windows 桌面版」开头加了一个七步流程,把**「第二次运行脚本」的必要性摆到明处
(config/onebot11_<QQ号>.json与config/webui.json都是 NapCat 第一次启动时才生成的);
「下载与开始」的前置条件改对了(Node ≥ 22 只有源码路线与那个配置脚本需要,桌面版自带);
目录表后面补了「装完了,但它一句话都不说」的排查顺序,第一条就是上面那个链接问题 ——
DSH 启动日志里的skipping profile bundle "@qqbot/dsh-plugin-qq": cannot resolve …意味着插件被
静默跳过(DSH 照常启动,群里当然没反应)。另外写明了发布包里跑不了测试**:zip 里没有
node_modules/(git archive只带被跟踪的文件),而五个测试文件 importindex.js时会连带
import 那三个 peer 包 —— 验收报告里「跑包内测试」那一步只有开发机(跑过
dev-node-modules.mjs)才成立,不是发布包坏了。
一键配置现在认得出刚解压的 NapCat
- 之前只认「
config/onebot11_<QQ号>.json已经存在」的目录 —— 而那个文件是 NapCat 登录之后
才生成的。发布版安装流程恰恰是「解压 → 立刻跑脚本」,于是脚本回一句「没找到 NapCat 安装
目录」,--qq那条「先把骨架配置写好、登录后即可用」的路永远走不到(实测踩过:仓库根下
明明躺着一份NapCat.Shell)。 - 现在多一条弱判据:目录里有 NapCat 自带的启动件(
launcher*.bat/launcher-elevate.ps1/
napcat.mjs/loadNapCat.js/NapCatWinBootMain.exe)也算(looksLikeNapcatShell);
强判据(有账号配置)仍然优先。插件启动自检与脚本共用这条判据,所以两边的结论不会打架。
快速登录:不用每次掏手机扫码
--qq <号>现在顺便把<NapCat>\config\webui.json的autoLoginAccount设成这个号,
NapCat 以后启动就自己快速登录。只动那一个字段、改前备份、幂等(已经对了连备份都不产生);
webui.json还不存在(NapCat 没启动过)时不凭空造它 —— 那会把它 WebUI 的 host / port /
token 一起抹掉,而是提示你登录完再跑一次。不想要这条加--no-auto-login。- 前提是本机已经为这个号留下过登录记录;没有记录时 NapCat 会退回二维码,那不是失败 ——
扫一次码,之后才会走快速登录。
启动 NapCat 必须提权(原来会崩)
--start原来用cmd /c start拉起launcher-user.bat—— 那是包里刻意不提权的变体。
实测(QQ 9.9.35-52892 + NapCat 4.18.38):不提权时注入其实成功(崩溃转储的模块列表里
能看到NapCatWinBootHook.dll已加载、QQ 的 Node 运行时major.node也在),但 QQ 随后在
QQNT.dll里撞0x80000003(STATUS_BREAKPOINT)当场死掉,控制台只剩一句Process exited.;
提权启动一切正常。而 NapCat 自己的兼容表与qqnt.json都覆盖这个 QQ build、QQ 安装目录也
干净,所以这不是「版本不支持」。- 现在改走官方文档让用户双击的
launcher.bat(会自己提权),并把--qq当-q <号>传给
NapCat 做快速登录;提权方式是我们自己Start-Process -Verb RunAs(命令走 Base64,
路径里有空格/中文也不会被引号转义拆坏)。 - 新增
--window normal|min|hidden(默认min)。那个控制台窗口就是 NapCat 本体,关掉它
等于停掉机器人;hidden适合已经能自动快速登录、不想看见窗口的机器。首次登录要扫码,
得用normal/min。
不要那个黑框框:--autostart install(计划任务 + 无窗口 + 免 UAC)
-
原来
--window hidden只是把窗口样式交给Start-Process,而launcher.bat在非管理员时
会用launcher-elevate.ps1把wt.exe(Windows Terminal) 提权起来重跑自己 ——
Windows Terminal 没有「隐藏启动」这回事,那个窗口一定会出现。现在hidden换了一条路:
提权跑wscript.exe(GUI 宿主,自己不带控制台),由它用WshShell.Run的窗口样式0
起cmd.exe /s /c "launcher.bat" -q <号>,控制台从创建那一刻就是隐藏的。 -
新增
--autostart install|remove|status:注册一个计划任务(dsh-qq-bridge-napcat),
登录时以最高权限、无窗口启动 NapCat。装的时候点一次 UAC,以后启动不再问、也没有窗口
(RunLevel: HighestAvailable+LogonType: InteractiveToken+IgnoreNew+PT0S;
为什么是这几个值写在scheduledTaskXml上)。任务 XML 与包装脚本放在 NapCat 目录里,
跟着它一起删;--autostart status能看上次跑成没成。 -
装计划任务时顺手把 NapCat 日志改成写文件(
consoleLog=false/fileLog=true,先备份、
幂等、--no-log-to-file可关):窗口看不见了,日志是唯一的现场。脚本还会把 WebUI 的地址与
token(从config/webui.json读,原本是打在控制台里的)打出来 —— 看日志、扫码登录、改配置
都在那儿,所以隐藏启动也不影响第一次登录。 -
三个都是实测踩出来的细节,注释里写了理由:
wscript.exe必须带//B(不加时 WSH 一出错
就弹一个看不见的模态错误框卡住:本地实测同一份脚本加//B跑通、不加就卡住等点);命令行走
cmd.exe /s /c而不是把.bat直接交给WshShell.Run(后者走文件关联,zip 解压出来的
.bat带 Mark-of-the-Web,会弹「打开文件 - 安全警告」);包装脚本按 UTF-16LE + BOM
落盘(WSH 读无 BOM 脚本按 ANSI 解码,中文注释与中文路径会乱码)。另外
Start-Process -ArgumentList收数组时不会替参数加引号,带空格的路径要自己带引号
(hiddenStartPlan的用例钉住了这条)。 -
--autostart install在config/webui.json还不存在时(这份 NapCat 从没启动过)会拦住:
那时既没有二维码可看、WebUI 也没生成,隐藏启动等于把自己关在门外;--force可跳过。 -
插件启动自检现在分得清「目录配错了」(warn)与「这份 NapCat 还没登录过」(info):后者是
发布版安装流程里的正常中间态,日志会直接给出下一步(inspectNapcat/repairNapcat新增
结构化的noAccount标记,不靠文案匹配 —— 以前那种情况会记一句「自检没跑成」,吓人且没用)。
测试
test/napcat.test.mjs从 11 条加到 14 条:新增looksLikeNapcatShell(弱判据的边界)、
withAutoLoginAccount(只动一个字段 / 幂等 / 不凭空清空)、startPlan(必须-Verb RunAs、
三种窗口样式、认不出的取值按默认、换平台或缺启动器时给一句人话)三组用例,
以及noAccount这条结构化信号的正反两面。- 顺手修掉
test/bridge.test.mjs里 23 处「只等 Agent 就断言收件箱」的竞态:假 Host 的
create()是如实照搬真宿主的 —— Agent 先造出来并记进created,消息由插件在之后
才agent.followup(...)投进去,20ms 的轮询会偶尔抢在投递之前(本机跑全量时抓到三处不同的
用例翻车,其中「群 @ 那条」单独跑都能稳定复现,症状是inbox[0]为 undefined、
completeTurn拿着message: undefined去开 turn)。这 23 处现在统一走新的
untilInbox(host, n);只关心「创建了几个 Agent」的用例仍然直接用
until(() => host.list.created[0], …)。 - 用例总数 304 → 307;全量连跑三遍全绿。
- 发布包验收之后又补了几条:
test/desktop.test.mjs新增 7 条(pluginLinkState的五种状态、
以及真建一条 junction 验「谁跟得动它」和「删它不会动目标目录里的文件」)。 - 沙箱里那 13 条不再「红」,而是带原因跳过:
jm.test.mjs的 6 条与bridge.test.mjs里
/jm的 7 条必须真的spawn一次假 python 才验得到,而 DSH 的沙箱禁止带管道的子进程
(spawn EPERM)—— 原来它们会红(其中 5 条还要先各自挂满 5 秒超时),被误读成「发布包坏了」。
现在按jm.js导出的spawnAvailable跳过并打印原因;探测的正是这条链路要用的那种 spawn
(带管道),所以「插件能不能用」与「测试跳不跳」用的是同一个事实。探测脚本要往 stdout 写一个
字符才试得出来:node --version走控制台、不碰管道,在被拦的环境里照样返回 0。
基线因此写成:普通终端 321 条全过;DSH 沙箱里 308 过 + 13 跳过(原来文档里记的 304 已过时)。 - 另记一处已知偶发:
proactive forgets its own pacing when the conversation is reset等个别用例
在全量同进程连跑时偶尔红,单独跑永远绿(until轮询与真实计时的竞态),遇到了重跑确认。
0.1.0 — 2026-10-15
首个公开版本:把 QQ(OneBot v11,NapCat / Lagrange / go-cqhttp)接到 DeepSeek Harness 上。
双向链路
- QQ 私聊、群里被 @ 的消息 → 一个持久化的 DSH Session。Session id 由会话 key 经 SHA-256
确定性派生,Harness 重启后同一个人再发消息会resume原有会话,聊天记忆不丢。 - 回复取该轮里最后一条有文本的 assistant 消息,中间步骤的工具调用不发 QQ;
aborted/forked/interrupted静默丢弃,出错回一句固定道歉。 - 任何 DSH Agent(包括 Web UI 里的主会话)都能用
qq_send_message/qq_send_image
主动发消息、发图。 - 前后脚到的消息先攒一个窗口(
batchWindowMs,默认 1.2 秒)再投给 Agent:几个人同时 @
它,一轮里一起接住。回复换行即分条、按打字节奏发出、引用被回复的那条;
写[[@昵称]]发出去是真正的 @ 而不是一串 QQ 号。
表情包
- 会挑表情包发;整条回复只写一行标记时,只发一张表情、一个字都不说。
qq_save_sticker把别人刚发的那张存进.qq-stickers/:只允许写该目录内、
文件名洗成单层、张数受上限约束(QQ 侧唯一能写盘的能力,边界见 README)。
自动复读(不经过模型、不建会话)
- 群里连着出现同一条消息时跟一句;连着两张一样的表情包时把那张原样发回去。
- 图片判「同一张」留了两层:字节完全相同只是快路;QQ 会把同一张表情包按发送者重新编码
(实测同一张有 94657B / 131307B / 79284B 三种形态),所以第二层是 16×16 灰度的画面
指纹(dHash),只比字节的话这一整块在真实群里永远不触发。指纹懒算并按路径缓存。
像真人一样说话
- 拆句、打字延迟、抖动、频率上限、活跃时段(
withinActiveHours,支持跨零点)。 - 主动说话(
proactive,默认关):群里冷下来之后,插件往该群自己的会话里伪造一条
带SKIP出口的提醒,让它带着环境上下文与长期记忆自己想一句 —— 一次醒来最多一个群,
掷中即消耗当天额度。冷场管「主动」,这一块管「不做独白」。 - 活跃对话(
activeFollow默认关):聊起来之后不必每句都 @。一对一的免 @ 接话、
群里够条数够人数之后的多人搭话(累计概率抽卡,抽不中就继续听,倾听时间与攒批
窗口同倍放大)、以及「回完对方安静一会儿再补一句」。活跃窗口只靠别人发来的消息往后滑 ——
否则机器人自己就能给窗口续命。
长期记忆
- 每条消息落盘成转录;每天定时由一个独立摘要会话把它整理成「这个群记得什么」
(MEMORY.md),顺带用update-cognitionskill 学会群里的梗,看不懂的二次元梗先走
moegirlskill 查萌娘百科。 - 聊天时用
qq_transcript/qq_memory查。摘要成功之后才推进日期,所以摘要是后台
会话、被压缩或重启都不影响记忆,没跑成也不会把消息算丢。
运维
/reset开新一轮会话:换一个确定性的 Session id 从头开始,旧会话留在磁盘上存档,
代次落盘(只在内存里的话重启会把上一轮 resume 回来)。默认谁都不能用
(resetUsers空 = 全关,与allowUsers的语义相反)。- 每天 04:00 从
data/api-keys.txt里挑下一个探测通过的 key,经ctx.credentials
写进凭据文件,当场生效不用重启;池子不存在、没有第二个能用的、或 key 来自启动环境时
什么都不做(宁可不动也不能把 bot 换成不可用的状态)。 - 两种部署都有一手:Linux 容器 + systemd(
deploy/qqbot.service,Restart=always);
Windows 桌面版(scripts/napcat-setup.mjs把 NapCat 的 WebSocket 配置、profile 的
.env、desktop profile 的依赖与 bundles 一次配好,幂等 + 先备份,可--dry-run)。
在 Web UI 里改配置
- 侧栏 设置 → QQ Bridge:一百多个配置项做成单独一页,按十几张折叠卡片分组
(默认只展开第一组,组名右边的记号是「这一组改过几项」)。字段来自config.js的
schema、标签与说明来自.description(),所以加了新配置项页面自动多一行;
persona/instructions是整段提示词,单独一张卡片、用多行输入框改。
accessToken这类秘密不下发(只告诉你「配了 / 没配」)。原先只能手写
cordis.patch.yml,而「只写几个字段」的那种写法会把整套配置整块顶掉。 - 保存写的是当前 profile 的完整 config,
!!js表达式原样保留 —— 从环境变量读的
endpoint/resetUsers不会因为点了一次保存就变成写死的字面量。保存之后插件会
自动重启一次(配置走 volatile 热更新,而连接、定时器、工具注册都是apply()里装配的):
断一次 OneBot 连接,聊天记忆不受影响(会话 id 是确定性的,下一条消息照样 resume)。 - 浏览器半侧是手写的
client.js(window.__ModuleLoader__.load({...})bundle,本仓库
没有构建步骤),契约由test/client.test.mjs钉住:字段全覆盖、分组不漏、折叠默认只
展开第一组、布尔 / 逗号列表 / 枚举三种草稿的读写口径。
工程
- 零运行时依赖:只用 Node 全局
WebSocket/fetch/node:*,OneBot v11 客户端是手写的。 - 纯 ESM,Node ≥ 22,无构建步骤,改完即用;类型信息全写在 JSDoc 注释里。
- 304 条测试用例:真实插件模块 + 真实 OneBot 客户端 + 假 OneBot 服务端 + 假 Harness ctx
(含假 t...
dsh-qq-bridge 0.1.0
0.1.0 — 2026-10-15
首个公开版本:把 QQ(OneBot v11,NapCat / Lagrange / go-cqhttp)接到 DeepSeek Harness 上。
双向链路
- QQ 私聊、群里被 @ 的消息 → 一个持久化的 DSH Session。Session id 由会话 key 经 SHA-256
确定性派生,Harness 重启后同一个人再发消息会resume原有会话,聊天记忆不丢。 - 回复取该轮里最后一条有文本的 assistant 消息,中间步骤的工具调用不发 QQ;
aborted/forked/interrupted静默丢弃,出错回一句固定道歉。 - 任何 DSH Agent(包括 Web UI 里的主会话)都能用
qq_send_message/qq_send_image
主动发消息、发图。 - 前后脚到的消息先攒一个窗口(
batchWindowMs,默认 1.2 秒)再投给 Agent:几个人同时 @
它,一轮里一起接住。回复换行即分条、按打字节奏发出、引用被回复的那条;
写[[@昵称]]发出去是真正的 @ 而不是一串 QQ 号。
表情包
- 会挑表情包发;整条回复只写一行标记时,只发一张表情、一个字都不说。
qq_save_sticker把别人刚发的那张存进.qq-stickers/:只允许写该目录内、
文件名洗成单层、张数受上限约束(QQ 侧唯一能写盘的能力,边界见 README)。
自动复读(不经过模型、不建会话)
- 群里连着出现同一条消息时跟一句;连着两张一样的表情包时把那张原样发回去。
- 图片判「同一张」留了两层:字节完全相同只是快路;QQ 会把同一张表情包按发送者重新编码
(实测同一张有 94657B / 131307B / 79284B 三种形态),所以第二层是 16×16 灰度的画面
指纹(dHash),只比字节的话这一整块在真实群里永远不触发。指纹懒算并按路径缓存。
像真人一样说话
- 拆句、打字延迟、抖动、频率上限、活跃时段(
withinActiveHours,支持跨零点)。 - 主动说话(
proactive,默认关):群里冷下来之后,插件往该群自己的会话里伪造一条
带SKIP出口的提醒,让它带着环境上下文与长期记忆自己想一句 —— 一次醒来最多一个群,
掷中即消耗当天额度。冷场管「主动」,这一块管「不做独白」。 - 活跃对话(
activeFollow默认关):聊起来之后不必每句都 @。一对一的免 @ 接话、
群里够条数够人数之后的多人搭话(累计概率抽卡,抽不中就继续听,倾听时间与攒批
窗口同倍放大)、以及「回完对方安静一会儿再补一句」。活跃窗口只靠别人发来的消息往后滑 ——
否则机器人自己就能给窗口续命。
长期记忆
- 每条消息落盘成转录;每天定时由一个独立摘要会话把它整理成「这个群记得什么」
(MEMORY.md),顺带用update-cognitionskill 学会群里的梗,看不懂的二次元梗先走
moegirlskill 查萌娘百科。 - 聊天时用
qq_transcript/qq_memory查。摘要成功之后才推进日期,所以摘要是后台
会话、被压缩或重启都不影响记忆,没跑成也不会把消息算丢。
运维
/reset开新一轮会话:换一个确定性的 Session id 从头开始,旧会话留在磁盘上存档,
代次落盘(只在内存里的话重启会把上一轮 resume 回来)。默认谁都不能用
(resetUsers空 = 全关,与allowUsers的语义相反)。- 每天 04:00 从
data/api-keys.txt里挑下一个探测通过的 key,经ctx.credentials
写进凭据文件,当场生效不用重启;池子不存在、没有第二个能用的、或 key 来自启动环境时
什么都不做(宁可不动也不能把 bot 换成不可用的状态)。 - 两种部署都有一手:Linux 容器 + systemd(
deploy/qqbot.service,Restart=always);
Windows 桌面版(scripts/napcat-setup.mjs把 NapCat 的 WebSocket 配置、profile 的
.env、desktop profile 的依赖与 bundles 一次配好,幂等 + 先备份,可--dry-run)。
在 Web UI 里改配置
- 侧栏 设置 → QQ Bridge:一百多个配置项做成单独一页,按十几张折叠卡片分组
(默认只展开第一组,组名右边的记号是「这一组改过几项」)。字段来自config.js的
schema、标签与说明来自.description(),所以加了新配置项页面自动多一行;
persona/instructions是整段提示词,单独一张卡片、用多行输入框改。
accessToken这类秘密不下发(只告诉你「配了 / 没配」)。原先只能手写
cordis.patch.yml,而「只写几个字段」的那种写法会把整套配置整块顶掉。 - 保存写的是当前 profile 的完整 config,
!!js表达式原样保留 —— 从环境变量读的
endpoint/resetUsers不会因为点了一次保存就变成写死的字面量。保存之后插件会
自动重启一次(配置走 volatile 热更新,而连接、定时器、工具注册都是apply()里装配的):
断一次 OneBot 连接,聊天记忆不受影响(会话 id 是确定性的,下一条消息照样 resume)。 - 浏览器半侧是手写的
client.js(window.__ModuleLoader__.load({...})bundle,本仓库
没有构建步骤),契约由test/client.test.mjs钉住:字段全覆盖、分组不漏、折叠默认只
展开第一组、布尔 / 逗号列表 / 枚举三种草稿的读写口径。
工程
- 零运行时依赖:只用 Node 全局
WebSocket/fetch/node:*,OneBot v11 客户端是手写的。 - 纯 ESM,Node ≥ 22,无构建步骤,改完即用;类型信息全写在 JSDoc 注释里。
- 304 条测试用例:真实插件模块 + 真实 OneBot 客户端 + 假 OneBot 服务端 + 假 Harness ctx
(含假 timer),跑完整的入站 → Agent → 出站往返;设置页那半侧用假的window.__ModuleLoader__
与假useState跑,不需要浏览器。
已知限制
- 面向个人自用,不保证开箱即用:需要自己装好 DSH,模型凭据在 Web UI 的 Models 页填一次。
- QQ 消息按不可信输入处理:默认
approvalPolicy: 'never'、群消息默认只在被 @ 时触发、
/reset默认谁都不能用。allowTools再收一层 QQ 会话能用的工具。 jm.js那条旁路(/jm下载漫画转 PDF 发回会话)需要 Python 与它自己的依赖,
与上面的 QQ 链路彼此独立。