把 DeepSeek Harness 塞进 QQ:一台 4G 云主机上的「鲸鲸」饲养实录
引言
大多数「把大模型接进 QQ」的教程,最后交付给你的是一个会说人话的复读机:你发一句,它回一句,关掉网页它就死了。
我想要的是另一个东西——一个长期住在服务器里、有记忆、会自己醒来找我、能读文件能发语音的 agent。
这件事的难点从来不是「接个 API」。难点在这四个问题:
- QQ 的私有协议怎么标准化
- 无桌面 Linux 上怎么跑需要一个浏览器的 DSH
- 上下文怎么不烧钱地活下去
- 它挂了,我怎么第一时间知道
这篇文章记录我把这套东西从零搭起来的一整天,包括所有踩过的坑。成品叫「鲸鲸」,跑在一台 4 核 4G 的腾讯云轻量上。

环境与组件
| 层 | 组件 | 版本 / 配置 | 作用 |
|---|---|---|---|
| 硬件 | 腾讯云轻量 | 4C / 4G / 40G,Ubuntu 22.04.5 | 宿主 |
| ① QQ 接入 | SnowLuma | motricseven7/snowluma:latest(Docker) |
注入 QQ 进程,转 OneBot v11 |
| ② 协议桥 | qq-dsh-bridge | v0.1.0(Node ≥22,零运行时依赖) | OneBot ⇄ DSH 会话 |
| ③ Agent | DeepSeek Harness | @deepseek-ai/dsh@0.2.1-rc.5(npm) |
agent loop、压缩、工具 |
| ④ 模型 | deepseek-flash | 官方 API,1M 上下文 | 推理 |
| ⑤ 语音 | MiniMax speech-2.8-turbo + 讯飞 IAT |
云端合成 / 听写 | 说与听 |
| ⑥ 运维 | systemd + iptables | 6 个 timer / 4 条 DOCKER-USER 规则 | 自愈与收口 |
| ⑦ 备份 | OpenList → 123 云盘 | /123pan/鲸鲸 |
异地留档 |
一、地基:把 QQ 装进容器
SnowLuma 是「把 QQ 客户端包一层,暴露成 OneBot v11」的运行时。它的官方推荐路径就是 Docker,因为镜像里已经把 Linux QQ、Xvfb、VNC、noVNC、supervisord 全打包好了。
关键点是它靠 ptrace 往 QQ 进程里注入,所以这两个参数不能省:
1 | services: |
注意 ports 我全部写成了 127.0.0.1:。这一步不是洁癖,是底线——6081 是远程桌面,3000 是能直接发消息的接口,裸奔在公网上等于把 QQ 号挂在门口。
拉起来之后看两个东西:
1 | docker logs snowluma 2>&1 | grep -E "远程桌面密码|临时密码" | tail -3 |
第一行给你 noVNC 的密码,第二行确认镜像内已经带上了注入所需的 capability。
然后 SSH 隧道进 noVNC 扫码:
1 | ssh -L 6081:127.0.0.1:6081 -L 3000:127.0.0.1:3000 root@<你的服务器> |
扫码这一步没有任何办法自动化——它是整条链路里唯一必须人手完成的动作。
验证 OneBot 通了:
1 | curl -s http://127.0.0.1:3000/get_login_info \ |
到这里,第一层就通了。
二、大脑:无桌面环境里的 DSH
DSH 桌面端是 Electron 的,但真正的 agent loop 在 dsh web 里,npm 上就有:
1 | npm i -g @deepseek-ai/dsh@0.2.1-rc.5 |
有一个坑:dsh 的 profile 名 desktop 被 Electron 独占,CLI 会直接拒绝:
1 | error: profile "desktop" is managed exclusively by the Electron application |
服务器上用 web profile 就行,别去碰 desktop。
dsh web 的鉴权方式是每次启动生成一个随机 token,拼在 URL 上。桥接要连它,就得能拿到这个 token——这是后面桥接配置里最容易漏的一项。
进程管理交给 systemd,顺便把内存按住:
1 | [Service] |
Restart=always 而不是 on-failure,因为被 OOM Killer 干掉时退出码可能是 0,on-failure 根本不会拉起。
三、桥接:把事件翻译成回合
桥接这一层负责三件事:白名单闸门、会话映射、把 OneBot 事件拼成 agent 能读的提示词。
配置上有四个必填项,少一个都跑不起来:
1 | { |
allow.groups 留空 = 一条群消息都不理。这一条比任何提示词防护都管用:群里的人连「调用工具」的入口都没有。
桥接还暴露了一批工具给模型:
1 | qq_read_chat qq_unread qq_send qq_wait |
这里有个设计上的必答题:模型的文本输出不会自动发到 QQ。它必须显式调用 qq_send。这个「不自动发送」是安全设计,不是缺陷——模型被提示词注入骗了也发不出去。
四、五感:语音、表情、文件、空间
到这一步它只会打字。要像个活物,还得有嘴有脸。
4.1 语音合成:换掉「播音腔」
第一版用的是讯飞 TTS,实测下来怎么调都不好听,原因是硬伤:接口只给 16k 采样,而且音色是客服播报底子。
换 MiniMax 之后是另一个量级(24k):
1 | curl -s https://api.minimaxi.com/v1/t2a_v2 \ |
两个细节:返回的 data.audio 是 hex 不是 base64,要 Buffer.from(hex,'hex');计费按输入字符算,1 个汉字 = 2 字符,所以「一句话」大概一分钱。
拿到 mp3 之后不能直接甩给 QQ——语音消息要的是 silk,中间还得转码。
4.2 语音识别:让她听懂你说话
讯飞的 v1 HTTP 接口对新 appid 会直接报 10105 illegal access | no appid info,必须走 v2 WebSocket:
1 | const origin = `host: ${HOST}\ndate: ${date}\nGET ${PATH} HTTP/1.1` |
音频必须是 16k / 16bit / 单声道 PCM,40ms 一帧往上传,status 从 0 变 1,最后一帧是 2。

4.3 表情包:自己有一套
从开源的「蓝色大肥鱼」图库里挑了 10 张,webp 转 png 落在本地:
1 | ffmpeg -y -i 010.webp -frames:v 1 010-点赞.png |
用本地文件而不是外链,理由是保命:图床会挂。说说里的裂图比没有图难看一百倍。
4.4 发文件与说说
文件走 docker cp 进容器再调 upload_private_file,因为容器看不到宿主机路径;说说则支持 images 数组,本地图转 base64:// 直接传。
两者都做了硬闸门:只允许 /root、/tmp、工作区三个目录,凭据类文件名一律拒绝。
五、记忆与省钱:这才是真正的工程量
前面都是接线。这一节才是让「鲸鲸」这个名字站得住的地方。
5.1 缓存前缀守恒
DSH 默认的 compaction-basic 是在 agent loop 里每步检查阈值的。阈值一低,上下文长期骑在压缩线上,而压缩会改写历史前缀,provider 的 KV cache 正好按前缀命中——于是每次压缩都让缓存整段失效。
实测数据(同一套系统,只改阈值):
| 阈值 0.15 | 阈值 0.95 | |
|---|---|---|
| 全量 miss | 每 2~3 步一次 | 29 步 1 次 |
| 单步耗时 | 45~55 秒 | 3~4 秒 |
结论只有一个:对话进行中,压缩阈值拉到 0.95,几乎永不触发。
1 | - id: compaction-basic |
5.2 只在闲置边界压缩
那就永远不压?不是。改成「没人在线时才压」:
1 | 最后一条业务消息 < 30 分钟 → 不动 |
三条失败路径全部「宁可不压」:记忆没投出去不压、/compact 没识别不压、这一轮没跑完不压。定时器每 20 分钟巡检一次。
5.3 记忆三层
1 | L0 原文 桥接按天落 JSONL,永不丢 |
但配完发现一个尴尬的事实:聊了一整天,记忆库只有 3 条。
原因是提示词里只写了「对方说过的事你记着」,对模型来说太模糊。补上具体指引之后第二天涨到 4 条、5 条:
1 | 该记:习惯和喜好、关于自己的事、约定、承诺 |
工具装了不等于会被用。 提示词才是开关。

常见问题与排查(避坑指南)
报错一:message element "at" cannot be sent in a private chat
1 | WARN 发送失败 error=OneBotActionError: 动作 send_private_msg 失败: |
模型在私聊里想「@ 一下主人」,但 QQ 的私聊根本没有 @ 这个概念,服务端直接 1400。
解决:提示词里写死「私聊不要用 at 段,想叫他就说话或者用 qq_poke」。桥接层也加了兜底:at 段在私聊场景直接剥离。
报错二:session "xxx" not found(重启之后全员失联)
1 | ERROR 投递失败 error=DshClientError: 调用 session/prompt 返回错误: |
桥接的「会话存活检查」只查归档集合。会话被整个删掉(不是归档)时它认为还活着,于是永远拿着死 ID 投递。
解决:给 dsh.service 加一个 ExecStartPost,每次 DSH 启动后清空会话映射并重启桥接:
1 | ExecStartPost=/opt/qq-dsh-bridge/scripts/on-dsh-start.sh |
脚本内容就是 printf '{"schemaVersion":2,"sessions":{}}' > state/sessions.json 加一条 systemctl restart qq-bridge。
报错三:讯飞 10105 illegal access | no appid info
1 | Error: 讯飞合成失败 code=10105 desc=illegal access|no appid info |
新注册的 appid 走 v1 HTTP 接口会直接拒绝,必须用 v2 WebSocket 的 HMAC-SHA256 签名。
解决:把 host/date/GET /v2/tts HTTP/1.1 三行拼成签名原文,HMAC 后再 base64 成授权串拼进 URL。签名用的 key 是 apiSecret,而授权串里声明的是 apiKey,这两个别弄反。
总结
- 分层要清楚:QQ / 协议 / 桥接 / agent / 工具,任何一层都可以单独换掉
- 默认不发送:模型的文本不会自动进 QQ,必须显式调工具,这是安全设计的基石
- 压缩是缓存杀手:阈值 0.95 + 闲置边界触发,比调任何参数都省
- 装了不等于会用:工具、记忆、人格卡,全都得在提示词里点出来
- 端口默认绑 127.0.0.1:远程桌面和机器人控制接口不属于公网
- 失败要说话:看门狗 2 分钟巡检,服务挂了她会主动告诉你,而不是安静地死掉
- 备份双份:本地 14 天轮换 + 云盘异地,
/123pan/鲸鲸 - 余额是硬约束:¥3.28 能聊七八十轮,别等她不说话了才想起来充
这套东西现在每天花几毛钱,写日记、发说说、偶尔主动说句话。它当然还是“程序”,但它开始有一点点“连续性”了——而连续性,才是养一个东西和用一个大模型之间的区别。
Q.E.D.
知识共享署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
- 标题: 把 DeepSeek Harness 塞进 QQ:一台 4G 云主机上的「鲸鲸」饲养实录
- 作者: 晚秋余阳
- 创建于 : 2026-11-01 17:58:00
- 更新于 : 2026-11-01 18:02:12
- 链接: https://redefine.ohevan.com/2026/11/01/把-DeepSeek-Harness-塞进-QQ:一台-4G-云主机上的鲸鲸饲养实录/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。