# dsh-channel-qq 服务器部署方案 > 编写时间:2026-10-14 | 依据:本机实测 + dsh 官方文档(`F:\aegent\oss\deepseek-harness`,v0.2.1-alpha.2) > 标注约定:**【实测】**= 本机已验证;**【文档】**= dsh 官方文档原文;**【调研】**= 联网查到的社区结论,需自行确认。 > > **阅读提示**:§1–§7 是当前有效的部署与运维说明;**§8–§13 是历史实录**(2026-10-14 起逐轮排错的过程记录)。 > 历史段落里提到的部分一次性脚本(`feature-test.mjs`、`media-placeholder-test.mjs`、 > `analyze-turns.mjs`、`dump-session.mjs`、`ssh-run.mjs`、`http-push-fast.mjs`)已在开源整理时删除, > 保留文字只为记录当时的方法与结论。产物目录自 2026-10-18 起为 `build/lib`(旧布局是 `lib/`)。 --- ## 1. 目标架构 ``` QQ 客户端(真机/NTQQ) ←登录→ NapCatQQ(协议端) │ OneBot v11 over WebSocket ▼ dsh-channel-qq(本插件,跑在 dsh 进程内) │ Cordis 服务注入 ▼ DeepSeek Harness (dsh) —— profile: web / headless │ workspace(记忆/日记/知识库/表情库) ``` 关键点:**本插件不是独立进程**,它是 dsh 的一个 Cordis 插件。服务器上必须同时具备 **dsh 本体** 和 **NapCatQQ**,插件才跑得起来。 --- ## 2. 前置条件 | 项 | 要求 | 说明 | |---|---|---| | Node.js | ≥ 22(本机 22.19.0 实测可用) | `zstdDecompressSync` 需要 22.15+ | | pnpm | 任意版本 | **只有** `dsh plugin` 安装插件时用;可用 `corepack pnpm`,无需单独装【实测】 | | dsh | 0.2.1-alpha.2 | 仓库 `github.com/deepseek-ai/deepseek-harness`,**developer preview,会破坏性变更** | | NapCatQQ | 最新版 | 走 OneBot v11;Windows 需真 NTQQ 客户端,Linux 推荐 Docker | | ffmpeg | 建议装 | MP3→silk 语音转码依赖它,缺失时 `wasmDecodeMp3ToPCM` 是空实现会直接失败(见 §5-9) | | 一个 QQ 小号 | **不要用常用号** | 见 §6 风控 | --- ## 3. 安装 dsh 本体(二选一) **A. npm 快速跑(最省事)** ```bash npx @deepseek-ai/dsh web --no-open ``` **B. 源码部署(推荐用于服务器,便于固定版本 + 插件 link 解析)**【文档】 ```bash git clone https://github.com/deepseek-ai/deepseek-harness.git /srv/dsh/dsh cd /srv/dsh/dsh && pnpm install && pnpm run build pnpm dsh web --no-open ``` > 目录约定:把本插件放在 **dsh checkout 的兄弟目录**,即 `/srv/dsh/dsh-channel-qq`。 > 原因:`package.json` 的 devDependencies 用 `link:..\dsh\vendor\cordis` 和 `link:..\dsh\packages\llm\llm` > 指向兄弟目录的 dsh 源码,这正是上一套部署(`C:\dsh\dsh` + `C:\dsh\dsh-channel-qq`)的布局。 --- ## 4. 部署插件 ### 4.1 安装运行期依赖 **⚠️ 不要直接 `npm install`**【实测】:`package.json` 的 `link:` 协议会让 npm 报 `EUNSUPPORTEDPROTOCOL: Unsupported URL Type "link:"`,即使加 `--omit=dev` 也一样。 用 pnpm(走 lockfile,自动跳过 devDependencies)【实测通过】: ```bash cd /srv/dsh/dsh-channel-qq corepack pnpm install --prod --ignore-scripts # 结果:只装 silk-wasm + ws,devDependencies: skipped ``` 如果不想用 corepack:`npm install ws@^8.18.0 silk-wasm@^3.7.1 --no-save` 也可(实测通过)。 ### 4.2 注册进 profile【文档】 ```bash dsh plugin --profile web add file:/srv/dsh/dsh-channel-qq # 或 link: 形式(源码布局下更贴合) dsh plugin --profile web add link:/srv/dsh/dsh-channel-qq ``` `dsh plugin` 把参数转发给 profile 目录里的 pnpm,并登记所有导出了 `dsh.bundle` 层的包。 本插件的 `package.json` 已有 `dsh.bundle.patch: ./cordis.patch.yml`,会被自动登记。 ### 4.3 编译(可选) `build/lib`(旧布局是 `lib/`)是**已编译且与 `src/` 同步**的产物(实测:`lib/*.js` 时间戳晚于对应 `src/*.ts`,且包含最新工具 `qq_search_music` / `qq_kugou_login` / `qq_netease_login` / `qq_search_history` 等)。 所以**服务器上不编译也能直接跑**。需要改源码时再 `pnpm install && pnpm run build`(需 `tsc` + 兄弟目录 dsh 源码)。 > ⚠️ 未验证项:peer 依赖 `@deepseek-ai/cordis`(4.0.5-alpha.1) 与 `@deepseek-ai/dsh-llm`(0.2.1-alpha.2) 在 > profile 里的解析方式。若启动报 "Cannot find module @deepseek-ai/cordis",在 profile 目录补装: > `dsh plugin --profile web add @deepseek-ai/cordis @deepseek-ai/dsh-llm`,或改用源码布局让 pnpm workspace 按名解析。 ### 4.4 写入部署配置【文档】 持久配置在 **`$DSH_HOME/profiles//cordis.patch.yml`**(用户层会**整行替换** `config`,所以要把整段写全)。 模板(键名均来自 `src/config.ts`,已核对): ```yaml - id: channel-qq config: connection: mode: forward # forward=插件连 NapCat;reverse=插件开服等 NapCat 连 url: ws://127.0.0.1:3001 # NapCat 正向 WS 默认端口 accessToken: "<换成你自己的长随机串>" selfId: <机器人QQ号> admins: [<师父QQ号>] primaryGroup: <主群号> requireMention: true talkValue: 0.6 workspacePath: /path/to/workspace agentPreset: standard permissionPreset: workspace-write affectionPort: 8789 # 好感动看板,仅 loopback groupEnabled: true rateLimitMs: 300 maxMessageLength: 4000 segmentation: { enabled: true, style: natural, minLength: 20, maxSegments: 8, removeTrailingPeriod: true } batching: { windowMs: 20000, maxMessages: 20, maxChars: 4000 } injectMode: { enabled: false, wakeIdle: true } imageRecognition: { enabled: true, maxPerMessage: 2 } poke: { enabled: true, patience: 3, windowSec: 300, pokeBack: true } stt: { enabled: true, baseUrl: "https://api.groq.com/openai/v1", apiKey: "$GROQ_API_KEY", model: "whisper-large-v3" } groupModel: { provider: null, model: null } keywordTriggers: [] keywordReactions: [] emoji: { steal: true, autoSend: false, autoSendProbability: 0.3, dir: null, registeredDir: null } debug: false ``` 环境变量:`DSH_HOME`(默认 `~/.dsh`)、`TAOTOKEN_API_KEY`(知识提取)、`KUGOU_API_BASE`、`STT_PROXY`、`GROQ_API_KEY`。 ### 4.5 部署协议端:SnowLuma(默认) > 2026-10-19 起项目默认协议端是 **SnowLuma**(Docker 镜像内置 Linux QQ + noVNC)。 > 下面 4.5.1 的 NapCat 方案保留作**回滚**与离线参考。 用仓库里的 compose 模板: ```bash docker compose -f deploy/snowluma/compose.yaml up -d ``` - OneBot WS 在 **3001**(容器内 `wsServers[0]`,`role: Universal`);**accessToken 必须与 插件配置一致**,否则插件握手会被拒。首次启动后改 `/app/data/config/onebot.json` 再 `docker restart snowluma`。 - WebUI `127.0.0.1:5099`、noVNC `127.0.0.1:6081` 都只发布到本机,远程用 SSH 隧道: `ssh -p <端口> -L 5099:127.0.0.1:5099 -L 6081:127.0.0.1:6081 root@` - 初始 WebUI 口令在 `docker logs snowluma` 的 `initial credentials` 一行;**没改密码就 重启会重新生成**,登录后先改密。VNC 密码在容器的 `/app/data/config/vnc-password`。 - **QQ 未登录时 3001 不会在容器内真正监听**(宿主映射在、listener 没有)——插件侧 表现为每 60s 一次 `dialling`,属正常,扫码后自动恢复。 - **「自动登录」必须在扫码页当场勾上,别去猜配置文件**:2026-10-19 实测—— 全局配置 `~/.config/QQ/global/nt_data/mmkv/nt_mmkv_global_misc` 里 `kAutoLoginUid` 非空 **并不代表**「自动登录」已启用:该键当时已指向当前账号, 但扫码页上的勾选框仍是空的,`docker restart` 后 QQ 照样停在登录页 (`[Hook] awaiting login` 一直不结束,3000/3001 不监听)。 勾选框的真实状态在 QQ 的登录记录库 `global/nt_db/login.db`(加密),读不出来。 **判断方法只有一个:重启一次看能不能自己进**。 - **实测有效的流程**:在 noVNC 里把扫码页的「自动登录」勾上(蓝色对勾),再扫码。 之后 `docker restart snowluma` → `[Hook] login identity discovered`(约 5 秒) → `login detected`(约 60 秒)→ 3000/3001 重新监听,全程无需扫码。 这次实测后 `kAutoLoginUid` 没变,所以别拿它当开关的判据。 - **登录态存在哪**:三个固定卷名的卷(`qq-client-config` 存 `.config/QQ` 与 `auth/login.enc`,`qq-client-data` 存 `.local/share`,`qq-gateway-data` 存 `machine-id` 与 OneBot 配置)。只要不删卷、不改卷名,`docker restart` 乃至保留卷的 `up -d` 重建都不会丢登录态;QQ 服务端判会话失效时会自己弹新的二维码。 - **SnowLuma 自己没有「自动登录」开关**:它只负责起 QQ,然后在 `[Hook]` 里等 `login detected`。免不免扫码完全由 QQ 客户端决定,WebUI 里没有这个选项。 - **无头勾选**:镜像里没有 xdotool,`deploy/snowluma/xclick.py` 用 python3 + libXtst 发 X11 点击。1920x1080 下「自动登录」勾选框约 `(935,670)`、「刷新」约 `(958,578)`: `docker cp deploy/snowluma/xclick.py snowluma:/tmp/ && docker exec snowluma python3 -I /tmp/xclick.py :1 935 670` 点完用 `ffmpeg -f x11grab -video_size 1920x1080 -i :1 -frames:v 1 /tmp/s.png` 截图确认。 扫码页的二维码同样可用这条命令截出来(约 2 分钟过期)。 - 回滚:`docker stop snowluma && docker start napcat`。 ### 4.5.1 NapCat(备选 / 回滚) **Linux / Docker(推荐)**【调研】 ```bash docker run -d --name napcat --restart=always \ -p 127.0.0.1:3001:3001 -p 127.0.0.1:6099:6099 \ -e MODE=ws -e WEBUI_TOKEN='<强随机串>' \ -v /opt/napcat/data:/app/.config/QQ \ mlikiowa/napcat-docker:v4.15.0 ``` - 正向 WS 端口 **3001**、WebUI 端口 **6099**,与插件默认配置一致。 - **务必改掉默认 token**:老版本默认口令是 `napcat`,公网暴露等于把 QQ 号送人【调研】。 - 端口只绑 `127.0.0.1`;若 dsh 与 NapCat 不同容器/主机,反向 WS 需要 `server.listen` 监听 `0.0.0.0`(当前代码是无 host 绑定,即全网卡)——此时**必须**靠 `accessToken` 把关。 - 首次启动进 WebUI 扫码登录 QQ。 - 掉线重连:NapCat 侧 `reconnectInterval` 默认 5000ms;插件侧正向模式自带指数退避重连(1s→60s)。 **Windows(若坚持用 Windows 服务器)** - 需要真实 QQ NT 客户端 + NapCat 注入;上一套部署就是 `C:\dsh` 这套布局。 - 建议注册计划任务/服务实现开机自启(上一套用了 Windows 计划任务跑 KuGouMusicApi)。 ### 4.6 验证清单 1. `dsh --profile web --dump-config` → 能看到 `channel-qq` 行且 config 是你写的值。 2. 启动后看 `~/.dsh/channel-qq.log`:应有 `starting (mode=forward, ...)` → `forward WS connected` → `QQ link ready`。 3. 群内 @ 机器人一句 → 应回复。 4. `curl -H 'Authorization: Bearer '` 连 `ws://127.0.0.1:3001` 跑一次 `get_login_info`(仓库里有现成脚本 `napcat-alive.mjs`,改 token/群号即可)。 5. 好感动看板:`ssh -L 8789:127.0.0.1:8789` 后本机访问 `http://127.0.0.1:8789/?key=<启动日志里的 token>`。 6. 发送侧探针:`curl -fsS http://127.0.0.1:8790/api/healthz` → 200 且 `"ok":true`; 非本机来源一律 403。详细判据见 §6.6。 ### 4.7 远程访问 dsh Web UI【文档】 dsh 默认只监听 loopback `127.0.0.1:3080`。远程访问三种方式: - SSH 隧道:`ssh -L 3080:127.0.0.1:3080 server`(最简单、最安全) - 反代:`dsh --profile web --public-url https://app.example/ui/ --trusted-host app.example`,反代需保留原始 `Host`、剥掉 `/ui` 前缀、转发 WS 升级、改 cookie `Path` - 原生 TLS:`--tls-cert --tls-key ` --- ## 5. 上服务器前必须修的移植性问题 **目标环境已确认为 Linux**(用户 2026-10-14 确认),所以下表中 1–6 条是**硬阻塞**:不修则在 Linux 上酷狗/网易云登录无法持久化、语音转文字整体失效、聊天记录搜索必然报错。 以下是**代码级已确认**的缺陷(Windows 专用 / 硬编码路径),不修会在新服务器上直接失效: | # | 位置 | 问题 | 影响 | |---|---|---|---| | 1 | `src/kugou-api.ts:14` | `process.env.DSH_HOME ?? 'C:/Users/Administrator/.dsh'` | Linux 下 cookie/dfid 写到相对目录 `C:/...`,**酷狗登录无法持久化** | | 2 | `src/netease-vip.ts:14` | 同上 | **网易云 VIP cookie 无法持久化** | | 3 | `src/tools.ts:658` | `process.env.DSH_HOME ?? 'C:\\Users\\Administrator\\.dsh'` | 同上(`qq_list_emojis` 等) | | 4 | `src/tools.ts:659-660` | 会话日志路径硬编码 `sessions\--D-OpenClaw-.openclaw-workspace--\\session.v3.jsonl.zstd`,且 sid 默认 `qq-group-1084963697` | **`qq_search_history` 在任何新部署上必然失败**;应改为从 dsh 会话存储 API 取,或至少按 workspace 名动态拼 | | 5 | `src/stt.ts:46` | `execFile('curl.exe', ...)` | Linux 无 `curl.exe` → **语音转文字整体失效**;应改用 `curl` 或走 dsh 的代理配置 | | 6 | `src/stt.ts:31` | `STT_PROXY ?? 'http://127.0.0.1:7890'` | 默认假设本机有 Clash;服务器上需设 `STT_PROXY` 或直连 | | 7 | `src/scheduler.ts:204` | 天气固定 `wttr.in/Lianjiang,Guangdong` | 换地点/服务器时早报天气不对,应做成配置项 | | 8 | `src/scheduler.ts:199` | 读 `${workspace}/课表排表规矩.md` | 工作区没这文件时早报显示"课表文件未找到"(非致命) | | 9 | `src/scheduler.ts:100,108` | 定时 `node skills/uumit-agent/scripts/*.js` | 工作区专属脚本,缺失时每 15/30 分钟报错一次,建议做成可选 | | 10 | `src/audio-convert.ts:438` | `wasmDecodeMp3ToPCM` 是空实现(返回 null) | 无 ffmpeg 时 **MP3→语音条失败**;服务器务必装 ffmpeg | | 11 | `cordis.patch.yml:23` | `historyLimit: 10` 不在 `config.ts` 里 | 无效键,删掉免得误导 | | 12 | `src/index.ts:95` | `affectionPort` 只监听 loopback | 远程服务器看不到看板,需 SSH 隧道(可接受) | | 13 | `src/audio-convert.ts:155/292/309` | `resolveTTSConfig` / `textToSilk` / `audioFileToSilkBase64` **无任何调用点** | 死代码(约 9KB);实际 TTS 走 NapCat `send_group_ai_record`,建议删除或接上 edge-tts | 另外建议把 1–4 抽成一个 `resolveDshHome()` 工具函数(`src/index.ts:33` 已有一份正确实现可复用),一次性消掉全部硬编码。 --- ## 6. 运维与风险 ### 6.1 进程守护 - dsh 本身没有服务化封装,用 **systemd** 包一层(`ExecStart=/usr/bin/node /srv/dsh/dsh/apps/... ` 或 `pnpm dsh web --no-open`),`Restart=always`。 - NapCat 用 Docker `--restart=always`(官方推荐 Docker;裸机 systemd 需 `xvfb-run`)【调研】。 - 本插件的**主动巡逻/做梦/进化定时器都在进程内**,进程重启会重置"今天是否已做梦"状态,可能重复做梦——可接受,但要知道。 ### 6.2 风控(头号风险)【调研】 - 官方明确:Bot 号**不要和常用号同 IP / 同设备**;掉线可通过换账号或换 IP 改善。 - 社区 issue 大量案例:新版 NapCat 被风控频繁掉线;账号二次触发风控可能需要**实名+人脸**。 - 建议:独立小号、独立 IP、先稳跑几天观察、不要频繁升级协议端。 - **NapCat 会"静默离线"**(WS 假活、不报 KickedOffLine)。插件已有 30s ping / 65s 无 pong 拆连接的半开检测(`src/onebot.ts:195-210`),但**没有告警**——建议加一条"离线超过 N 分钟推送到师父私聊/邮件"的兜底。 - 不要桌面 QQ 与 NapCat 共用登录数据目录,否则反复掉登录。 ### 6.3 官方 QQ Bot 方案对比【调研】 若只做"群里 @ 才回答"的场景,官方开放平台**零封号**,但限制硬: - 群消息只推 `GROUP_AT_MESSAGE_CREATE`(必须被 @),拿不到全量群消息,也**无法自由主动发言**; - 群被动消息 5 分钟内有效、每条最多回 5 次;主动消息有 qpm 与每群每天 1000 条上限; - 需企业/个人主体入驻审核。 → 本项目的"养成 + 主动巡逻 + 偷表情 + 点歌"玩法**只能走 NapCat**,代价是风控。 ### 6.4 协议端选型【调研】 | 方案 | 现状 | 结论 | |---|---|---| | **NapCatQQ** | 11k★,2026-10 仍活跃,OneBot v11 双向 WS | **继续用** | | Lagrange.Core | V2 已弃 OneBot 11,转推 Milky | 不迁移 | | LLOneBot → LuckyLilliaBot | 已基于 LagrangeV2,改推 Milky/Satori | 不迁移 | | go-cqhttp | 2024-05 起停更,README 自述无力维护 | 不要用 | | Milky / OneBot v12 | Milky v1.3 规范、生态小;v12 仍是草案 | 短期不迁移 | --- ### 6.5 出口 MTU 黑洞:2026-10-17 复测(P7.6) **结论:今天复测不出来,因此不改任何网络参数。** 排查记录留在这里,供下次再犯时对照。 历史症状(2026-10-15 记录,见 `deploy/http-push-fast.mjs` 的头注释):SSH 长命令 ~1000 字符能过、 ~2000 字符失败(`Unable to exec`);SSH 大文件写入卡死;HTTP POST 到 8790 超过 ~1200 字节稳定在 19s 后 `ECONNRESET`。19s 是 TCP 重传超时,典型路径 MTU 黑洞(第二个包永远到不了)。 2026-10-17 在同一台机器上复测: ```text $ ip -o link show ens17 mtu 1500(标准,无隧道/巨帧) $ for s in 1472 1400 1200 1000 800; do ping -M do -c 1 -W 3 -s $s 1.1.1.1; done 1472 OK / 1400 OK / 1200 OK / 1000 OK / 800 OK ← 1500 字节包(DF 置位)能通 $ sysctl net.ipv4.tcp_mtu_probing 0 ← MTU 探测是**关**的 $ nft list ruleset | grep -i mss (无 MSS clamping 规则) $ SFTP 上传 1.4MB 产物 sha256 与本地逐字节一致 $ ~2000 字符的 SSH 命令 成功 ``` 即:**出口方向没有黑洞**(至少对 1.1.1.1 这条路径),长命令与大文件今天都能过。 **留着的那条风险**:`net.ipv4.tcp_mtu_probing = 0`。真遇到黑洞时 TCP 只能靠重传超时(就是那个 19s), 把探测打开会让内核自己找路径 MTU。候选修法(**需要单独批准,因为这会影响这台机器上的其他业务**): ```bash # 临时(重启失效): sysctl -w net.ipv4.tcp_mtu_probing=1 # 永久:写 /etc/sysctl.d/99-dsh-mtu.conf 后 sysctl --system ``` ### 6.6 发送侧健康探测(`/api/healthz`) **问题**:端口通 ≠ 消息发得出去。2026-10-19 的事故里 3000/3001 一直能连、 日志里没有异常,但 06:00–12:50 一条消息都发不出去——端口探活和连接日志都 回答不了"发送侧还活着吗"。 **做法**:后台 HTTP 面上加一条只读路由 `/api/healthz`,内部对 OneBot 做一次 `get_status` 往返(**不产生任何消息**),并附上 socket 状态与最近收帧时间。 ```bash curl -fsS http://127.0.0.1:8790/api/healthz | jq . # { "ok": true, "connected": true, "online": true, "good": true, # "latencyMs": 12, "lastActivityAt": ..., "selfId": 1000000001, "detail": "" } ``` - 探测通过 → 200;失败 → 503(`detail` 给出原因:socket 未连接 / `get_status` 失败 / `online=false` / `good=false`)。脚本用 `curl -fsS` 一把判断。 - **门禁是来源地址**:只接受 loopback。面板即使被显式绑到 `0.0.0.0`,这条路由 对公网仍然 403。 - 探针**不会**"发一条消息试试":那会打扰人;而且一条消息送达只能证明那一刻通, 证明不了链路稳定(同一天两次踩过)。判"恢复"仍然看 `dialling/reconnecting` 循环有没有停。 运维脚本里最小的用法: ```bash if curl -fsS --max-time 8 http://127.0.0.1:8790/api/healthz >/dev/null; then echo "🟢 发送侧探针通过" else echo "🔴 发送侧不通(先看 detail,再决定是查登录态还是扫码)" fi ``` --- ### 6.7 无人值守的三条防挂起 1. **安装 bubblewrap**(`apt-get install -y bubblewrap`):宿主 bash 的 `workspace-write` 沙箱后端需要它;缺了会让普通写文件动作被当成"需要升级沙箱", 进而弹出一个没人能答的审批。 2. **审批策略按预设关掉**:在 profile 里把 `permission` 预设的 `workspace-write.approval` 设为 `never`(**不要**改 `approval` 的全局 policy, 否则 permission-presets 匹配不到预设、channel-qq 起不来)。 3. **回合看门狗**:`sessionReset.maxTurnMinutes`(默认 10)——任何回合超时未结束 就强制换血,避免一个挂起的工具调用堵死整个房间。 ## 7. 安全清单 - [ ] NapCat `WEBUI_TOKEN` / OneBot `accessToken` 换成强随机串,**不要用默认值** - [ ] 3001 / 6099 端口只绑 `127.0.0.1` 或用防火墙限制来源 - [ ] dsh Web UI 走 SSH 隧道或反代 + `--trusted-host`,别裸暴露 3080 - [ ] 仓库里的 `napcat-alive.mjs` 明文写着真实 token(`jw_VfZZsK22UT1Oe`)和真实群号,**上服务器前务必清理或加进 .gitignore** - [x] 网易云/酷狗 cookie 与 dfid 存在 `/etc/dsh/secrets/`(600),不再混在 `/channel-qq/` 的数据目录里(P7.4);旧位置的文件会在首次读取时自动搬过去 --- ## 8. 实际部署实录(2026-10-14,全部实测) > 本节记录**真实跑起来之后**的状态,与上面第 3~6 节的规划有出入的地方以本节为准。 > 实测环境:`YOUR_HOST:22`(KVM 8 vCPU / 7.9G RAM / Ubuntu 24.04.1 / 磁盘 39G)。 ### 8.1 目录与进程 | 项 | 实际值 | |---|---| | Node | 22.23.3,独立装在 `/opt/node22`(系统 node 18 未动) | | dsh 本体 | `/srv/dsh/dsh`,tag `dsh-v0.2.1-alpha.2` | | 插件 | `/srv/dsh/dsh-channel-qq`(源码 + 预编译 `build/lib`) | | Agent 工作区 | `/path/to/workspace` | | 启动 | `systemctl restart dsh`(`/etc/dsh/env` 里带 `TZ=Asia/Shanghai`) | | 日志 | `~/.dsh/channel-qq.log`(插件自己追加,带 ISO 时间戳) | 之所以用「源码检出目录」布局(`/srv/dsh/dsh` + `/srv/dsh/dsh-channel-qq` 并列),是因为 `package.json` 的 devDependencies 用了 `link:..\dsh\vendor\cordis` / `link:..\dsh\packages\llm\llm`。 安装依赖必须用 `corepack pnpm install --prod`,**不能用 npm**(`link:` 协议会让 npm 直接 `EUNSUPPORTEDPROTOCOL` 失败)。服务器上 `node_modules/.bin/tsc` 是 5.9.3,编译用 `./node_modules/.bin/tsc -p tsconfig.json`。 ### 8.2 端口与防火墙 | 端口 | 用途 | 监听 | |---|---|---| | 3001 | NapCat OneBot v11 WS | 127.0.0.1 | | 6099 | NapCat WebUI | 127.0.0.1 | | 8789 | 好感度悬浮窗(token 门禁) | 127.0.0.1 | | 8790 | 管理后台(密码门禁) | **仅 127.0.0.1**(P0 起;远程访问走 SSH 隧道) | | 3080 | dsh Web UI | 127.0.0.1 | ### 8.3 部署流程(实际使用的) 1. 本地打 tar:`tar -czf deploy/plugin-src.tar.gz -C F:\dsh-channel-qq src package.json tsconfig.json` 2. 传到服务器。**SFTP 上传会超时**,改用 base64 分块写进命令流 (`scripts` 见 `deploy/`,每批 ~16KB,写进 `/tmp/dsh_bNNN` 再 `cat | base64 -d`)。 注意这台机器上 SSH 的 "Unable to exec" 是**间歇性**的,脚本必须自带重试。 3. 解包 → `tsc --noEmit` 先看类型错误 → `rm -rf lib && tsc` 产出 → `systemctl restart dsh`。 4. **每条 SSH 命令是独立的 exec 会话**,`cd` 不跨行保持;多行逻辑必须写进脚本文件。 ### 8.3.1 发布流程(P7.2 起:原子发布 / 回滚) 从 P7.2 开始,服务器上的插件目录是**软链接 + 版本目录**,发布不再是"解包覆盖": ```text /srv/dsh/dsh-channel-qq → 软链接(dsh 加载的就是这个路径) /srv/dsh/dsh-channel-qq-rel-<版本>/ 真实发布目录(**必须与主目录同深度**,见下) /srv/dsh/releases.json 发布索引(当前版本 + 历史) /srv/dsh/backups/workspace/<日期>.tar.gz 工作区每日备份(P7.5) ``` 发布一条命令(本地): ```bash node deploy/pack-release.mjs # 构建 + 打包 + 生成 MANIFEST.sha256 / RELEASE.json # 传 .deploy-tmp/release.tar.gz 上去(SFTP;不要用大 POST,见 §8.3) ``` 服务器侧: ```bash # 宿主机没有系统 node 时(本项目用 /opt/node22),一定要带上 PATH: # 发布闸门会在新目录里跑测试,其中有用例要 spawn `node`。 export PATH=/opt/node22/bin:$PATH node deploy/release.mjs install /root/release.tar.gz # 解到新目录 → 逐文件 sha256 验签 → 冒烟 → 切换 → 重启 node deploy/release.mjs list # 有哪些版本、当前是哪个 node deploy/release.mjs rollback # 回上一版(也可 --to <版本>) node deploy/release.mjs diff <版本A> <版本B> # 两版之间差了什么 node deploy/release.mjs prune # 只保留最近 5 版 ``` **两个容易误会的地方**: - **`install` 不会自动清理**:`KEEP = 5` 只在手动跑 `prune` 时生效。每版约 15M (`node_modules` 走硬链接,不重复占盘),堆着不危险,但 `/srv/dsh` 会慢慢涨。 - **`rollback` 只回代码**:配置开关(`/channel-qq/admin-config.json`)、 记忆库(工作区 `knowledge/`)、会话(`/sessions/`)都不在版本目录里。 要回到"某个开关还没打开"的状态,是两步:`rollback` + 换回对应的配置备份。 **为什么发布目录必须与 `/srv/dsh/dsh-channel-qq` 同深度**:`node_modules/@deepseek-ai/` 下面是指向 `../../../dsh/vendor/cordis` 的**相对**软链接(宿主 monorepo 的 vendor 目录)。 多一层目录(比如 `/srv/dsh/releases/<版本>`)之后这三条相对链接会指到 `/srv/dsh/releases/dsh/vendor/cordis`——构建能过、启动必炸。同深度的 `/srv/dsh/dsh-channel-qq-rel-<版本>` 才安全。 **发布记录怎么追溯**(P7.1):每个版本目录里带 `RELEASE.json`(本地 git commit、 构建时间、文件数)+ `MANIFEST.sha256`(逐文件 sha256),`/srv/dsh/releases.json` 记着每次安装的来源 tarball sha256 与时间。要回答"哪次发布改了什么":服务器上 `release.mjs diff <旧版本> <新版本>` 看**部署了什么**,本地按 `RELEASE.json` 里的 commit 看**为什么改**。服务器刻意不留 `.git`(产物里没有,也不该有)。 ### 8.4 管理后台(新增) - 地址:**只监听 `127.0.0.1:8790`**(P0 起)。远程访问走 SSH 隧道: `ssh -p 22 -L 8790:127.0.0.1:8790 root@YOUR_HOST`。 - 密码来自环境变量 `CHANNEL_QQ_ADMIN_PASSWORD`(`/etc/dsh/env`,600 权限)。代码里没有默认口令, 两者都为空时登录一律拒绝。把 `admin.host` 改成非本机地址会被拒绝保存,除非显式设 `CHANNEL_QQ_ADMIN_ALLOW_REMOTE=1`。 - 登录尝试按 IP 限流(5 分钟 10 次);会话是 HttpOnly Cookie + **12 小时** TTL(P0 起,原为 7 天)。 - `cruises`(工作区脚本定时任务)从后台保存会被拒(403):它会在本机执行脚本。 确实需要改,设 `CHANNEL_QQ_ADMIN_ALLOW_CRUISE_EDITS=1`。 - 覆盖层文件:`~/.dsh/channel-qq/admin-config.json`。配置解析顺序是 `DEFAULT_CONFIG ← cordis patch ← admin-config.json`,**后台保存过之后以后者为准**。 - 配置编辑是**全热生效**的:`restartRequired` 现在是**空列表**,改任何一项都不用重启。 分两层实现: 1. **就地合并**——所有写入都合并进活对象(子对象/数组保持同一引用),因为 `EmojiLibrary` 持有 `config.emoji`、`RssPoller` 每轮读 `config.rss.feeds`。 剩下的"读一次就缓存"的地方也改成了 getter 或每轮重读:表情目录 (`emoji.dir`/`registeredDir`)、RSS 轮询间隔、工作区路径、好感度面板端口。 2. **显式副作用**——纯合并做不到的那些由 `index.ts` 的 `applyEffects()` 执行: `admin.port/host/enabled/password` → 重新监听(先绑新端口再放旧端口; 同端口改 host 则先释放再重试 12 次,失败自动回滚到原监听,**绝不把运营者锁在门外**); `connection.*`/`accessToken` → `OneBotClient.reconnect()` 就地换传输, 不重建客户端(90 个工具和 bridge 都持有这个实例); `emoji.*` → 重读表情索引;`affectionPort` → 重新监听悬浮窗。 保存响应里带 `effects: string[]`,逐条说明实际做了什么,运营者不用猜。 - 页面是单文件、免构建的原生 JS;配置表单由 schema 生成(含 `keywordReactions`、 `groupModel.provider/model`、`tts.provider` 等全部可配项);另有资源表 (表情库/语录/记忆/提醒/订阅/口癖/发送记录/歌单/日记/好感度)、日志尾部、 以及一个「给机器人下指令」的注入框(等价于师父私聊一句话,用来做端到端自检最方便)。 - **前端防呆**:登录框在 HTML 里默认可见(不是靠 JS 去 un-hide),并且任何脚本异常都会 通过 `window.onerror` / `unhandledrejection` / 元素缺失检查显示在登录框里。 早期版本因为引用了不存在的 `#shownav`,`DOMContentLoaded` 里抛异常中断, 两个面板都没被显示,表现为**全黑页面**——现在有回归测试专门守这条 (静态校验脚本里每个 `$('#id')` 都必须在 HTML 中存在)。 ### 8.5 功能模块与工具数 插件注册 **90 个工具**(核心 58 + 各功能模块 32,取自 `channel-qq.log` 的 `registered N QQ tools` / `registered N/M tools` 行): | 模块 | 文件 | 工具 | |---|---|---| | 链接解析 | `link-resolve.ts` | `qq_resolve_link` `qq_send_link_card` | | 表情包配字 | `meme.ts` | `qq_make_meme` `qq_send_meme` `qq_emoji_sticker_text` | | 语音合成 | `tts.ts` | `qq_list_voices` `qq_tts_voice` `qq_tts_save` | | 群语录 | `quotes.ts` | `qq_save_quote` `qq_random_quote` `qq_search_quote` `qq_quote_stats` `qq_delete_quote` | | 提醒 | `reminders.ts` | `qq_remind` `qq_remind_at` `qq_remind_list` `qq_remind_cancel` | | RSS 订阅 | `rss.ts` | `qq_rss_add` `qq_rss_list` `qq_rss_remove` `qq_rss_preview` | | AI 生图 | `imagegen.ts` | `qq_generate_image` `qq_generate_image_save` `qq_imagegen_models` | | 防刷屏 | `flood.ts` | `qq_flood_stats` `qq_flood_exempt` `qq_group_mute_user` | | 口癖学习 | `learn.ts` | `qq_learned_phrases` `qq_learn_phrase` `qq_forget_phrase` `qq_learn_stats` | | 记忆召回(并入核心) | `tools.ts` | `qq_recall_memory` `qq_memorize_fact` `qq_memory_search` `qq_send_sticker` | 后台 runner:口癖 flush(5 分钟)、提醒轮询(30 秒)、RSS 轮询(默认 15 分钟,首次延迟 20 秒)。 群事件监控在 `bridge.ts` 的 `handleMonitoredNotice`(进门/退群/禁言/撤回/改名/加好友, 按 `eventMonitor.windowSec` 合并成一条话题素材给模型裁决)。上下文压缩与步数预算在 `bridge.ts` 的 `compactContext` / `installStepBudget`(后者用 `agent/pre-step` 中间件, 返回 `{kind:'reject'}` 干净结束回合)。 ### 8.6 外部接口实测结论(重要,决定了实现方式) | 目标 | 结果 | 处理 | |---|---|---| | B站 `x/web-interface/view` | **HTTP 412**(机房 IP 被风控,UA/Referer/buvid3/带 page referer/HTTP/XFF 全试过) | 三级回退:`view` → `pagelist`+`archive/desc`+`player/v2`(**可用**,给标题/时长/aid,无封面)→ 网页 og: | | B站 `player/pagelist` `player/v2` `archive/desc` `card` `b23.tv` | **200 可用** | 作为回退层;封面仍缺,需在后台填 `bilibiliCookie` | | B站 `search/all/v2` | 200,但**是关键词匹配**,按 bvid 精确匹配命中不到 | 不采用 | | edge-tts WebSocket | **握手 403**(7 种 Origin/GEC 组合全试;`voices/list` 却 200) | `tts.provider=auto`:edge → 百度/谷歌免费 HTTP 接口 | | 百度 `fanyi.baidu.com/gettts` | **200 + audio/mpeg**(长文本 118KB / 14.8s,ffprobe 验过) | 中文回退首选 | | Google `translate_tts` | **200 + audio/mpeg**(139KB / 17.4s) | 非中文回退首选 | | pollinations 生图(不带 `model`) | 200 + 真 JPEG | 默认生图后端 | | pollinations(带 `model=`) | **402 立即返回**(命名模型需要 token) | 代码里**绝不**拼 `model=`,并对 402/429/5xx 做 3 次退避重试 | | 本机 LLM 网关 `/v1/images/generations` | **404**(网关只有 chat) | 生图只能走外部端点 | | RSS:BBC / 阮一峰 / 少数派 | 全部 200 且解析出条目 | 手写正则解析 RSS2.0+Atom | | 中文字体 | 首次部署时**不存在** | `apt-get install fonts-noto-cjk fonts-noto-color-emoji fonts-wqy-microhei` | ### 8.7 实测证据 - **功能模块 20/20 通过**(`lib/` 编译产物 + 真实网络 + 真实 ffmpeg,见 `deploy/feature-test.mjs`): B站三级回退、网易云、网页 og、`lossless` 无损 JSON 直测(见下)、字体探测、 中文配字出 PNG(23KB)、中文语音(auto 回退到 baidu,41.7KB / 5.22s mp3)、 强制 edge 时如实报错、pollinations 出图(28KB JPEG)、3 个真实 RSS、 `parseWhen` 8 条算例全中、语录增删查重、口癖 flush、风控复读触发+豁免+冷却。 - **管理后台 27/27 通过**(`deploy/admin-test.mjs`):登录/401/429、未登录拒绝、 snapshot、配置热生效(改 `imageGen.model` 后 `status` 立刻变)、 **`restartRequired` 恒为空 + `effects` 逐条说明**、 **端口热切换 8790→8791→8790 全无需重启且登录态保持**(用裸 TCP 连接判定新旧端口, 避免 undici 的 keep-alive 连接池把"旧端口还在答"误判成"还在监听")、 落盘、日志、10 个资源表、写入+删除、rescan 动作、HTML 里客户端 JS 可编译、 **每个 `$('#id')` 都在 HTML 中存在**(白屏回归)、**登录框默认可见**、公网端口 200。 - **机器人端到端**:通过后台注入指令让机器人依次调 `qq_get_group_list`、 `qq_recall_memory`、`qq_learn_stats`、`qq_flood_stats`、`qq_imagegen_models` 并私聊回报; 会话日志里 `qq_recall_memory` 被引用 36 次(含多轮),`channel-qq.log` 里 `not lossless JSON` 出现 **0** 次。 ### 8.7.1 修掉的两个真实缺陷(用户实测发现) 1. **管理后台全黑页**。`admin-html.ts` 的客户端脚本里有一行 `$('#shownav').onclick = …`,但 HTML 里从来没有这个元素 —— `null.onclick` 抛 TypeError,整个 `DOMContentLoaded` 回调中断,`#login` 与 `#app` 都停留在 `hidden` 状态,表现为「服务器在跑但页面一片黑」。修法:删掉那行;`#login` 在标记里默认可见; 所有 `$('#x').onclick = …` 改走 `on()` 辅助函数(缺元素时把错误显示到登录框上); 加 `window.onerror` / `unhandledrejection` 兜底;并补上「每个 `$('#id')` 必须存在」 的静态回归测试。同时把漏接的「工具与指令」页 `renderTools()` 接进 `showTab()`。 2. **`qq_recall_memory` 在有命中时报 `invalid output: value is not lossless JSON`**。 返回体里写了 `note: 有命中 ? undefined : '…'`,harness 的严格 JSON 校验不接受 `undefined` 字段 —— 空库时 `note` 是字符串所以正常,**只有查出结果才炸**, 于是所有"空库测试"都是绿的。修法有两层:把 `qq_recall_memory` 改成条件展开 (不再产生 `undefined`),并在 `tool-kit.ts` 加 `lossless()` 统一清理 `undefined`/`NaN`/`Infinity`/函数/`Date`(数组里的 `undefined` 转 `null` 以免移位), `tools.ts` 和 `registerSpecs` 两条注册路径都过它 —— 从根上消灭这一类问题, 而不是只补这一处。 ### 8.8 遗留与取舍 - **B站封面**在机房 IP 上拿不到(`view` 被风控)。填入 `bilibiliCookie` 即可恢复, 否则卡片是「标题 + 时长 + 简介 + 链接」的纯文本形式(不报错、不丢链接)。 - **edge-tts 音色**在本机不可用(微软对机房 IP 的 WS 升级一律 403)。 想要真正的多音色,需要换 IP 或自建 TTS 端点;当前的百度/谷歌回退**不区分音色**, 语速由 `tts.rate` 换算。 - **pollinations** 匿名队列按 IP 限流,偶发 402,已有 3 次退避重试; 要稳定就接自己的 OpenAI 兼容生图端点(后台可配 `imageGen.baseUrl`)。 - `get_group_member_info` 查机器人自己仍返回 `failed ""`(NapCat 侧行为,不影响其它功能)。 --- ## 9. 第三轮:管理后台重做 + 占位符 bug(2026-10-14 晚) ### 9.1 管理后台前端整体重做 旧的 `admin-html.ts` 是手写内联页面,功能在但手机上没法用。现在源码拆成 **`src/admin-ui.html`(页面)+ `scripts/build-admin-html.mjs`(生成器)**, `src/admin-html.ts` 变成生成产物(顶部有 `GENERATED FILE` 标记,不要手改)。 `npm run build` 会先跑生成器再跑 `tsc`。 生成器不是花架子——它把三类「只有浏览器才会暴露」的 bug 挡在构建阶段: 1. `