# 架构 > 状态:**方案定案(2026-10-18);协议端实测通过、P1 已真机验收、P1.5 基础配置页已落地、 > P2.1/P2.2 消息闸门骨架与收尾已落地、P2.3 jev 分类器已接(OpenRouter)、P3 `qq_send` 主动推送与 > QQ 侧审批已落地(2026-10-19);P4 聊天记录与消息拼接(群项目 `.qq-relay/`)与 P4.1 入向媒体落盘已实现(2026-10-19); > P5 QQ 侧命令层(`/new` / `/stop` / `/status` / `/help`)已实现(v0.11.0,见 [commands.md](./commands.md)); > P6 回复体验(引用触发消息 / 处理中占位 / 直通连发合并)已实现(v0.11.1,见 [reply.md](./reply.md)); > P4.2 媒体双向(入向视频 / 语音 + 出向图片 / 文件)已实现(v0.12.0,见 [media.md](./media.md)); > P7 client 面板增强(闸门每群状态 / 最近消息 / 映射管理)已实现(v0.14.0,见 [client.md](./client.md)); > P8 戳一戳识别与回戳(被戳接话 / 互戳记录 / `qq_poke`)已实现(v0.15.0,见 [poke.md](./poke.md)); > P9 入向群文件(`group_upload` → 下载落盘 → 可选直通)已实现(v0.16.0,见 [media.md §5.2](./media.md)); > P10 配置页迁到插件面板(`plugins.bundle.config`,二级页面,样式走宿主 `--dsw-*` token) > 已实现(v0.17.0,见 [client.md](./client.md));角色预设(v0.19.0/v0.20.0,见 [persona.md](./persona.md)); > **v0.21.0 画图能力(ComfyUI 探测 + 动态注册 `comfy-image` runtime skill)已实现,见 > [comfy.md](./comfy.md)**;v0.21.1 修 `qq_send` 出向图片 / 文件转发(runtime 包装补 > `sendImage` / `sendFile`)与 workspace 挂载(失败重试 + 启动补挂,见 §3 会话映射); > **v0.22.0 视频生成(同一个 ComfyUI 动态注册 `comfy-video` skill + `qq_send` 的 `videos` 出向视频) > 已实现,见 [comfy.md](./comfy.md)**;**v0.23.0 环境一键配置(7 步诊断 + 白名单动作 + 一键修复, > 见 [env.md](./env.md))已实现**;**v0.24.0 jev API key 进配置页(`gate.jevApiKey` 只写不读 + > 分类器热切换,见 [gate.md §4](./gate.md))已实现**;**v0.25.0 环境「一键修复」改为逐步补全 > (按计划逐轮执行 + 每步重探 + 系统类动作先确认 + `fixPlan` 下发,见 [env.md §4.1](./env.md))已实现**; > **v0.25.1 环境探测可读化与兜底(docker stderr 原文 / `--context colima` 兜底 / `restart-daemon` / > 不推进就跳过,见 [env.md §4.1](./env.md))已实现**;**v0.25.3 回滚 v0.25.2 的引擎识别 > (环境页只认 Colima,不再提供 OrbStack / Docker Desktop 动作,见 [env.md §1](./env.md))**; > **v0.25.4 配置「待补全」与「无效」分离(连接信息留空可先保存草稿、relay 不启动, > 见 [config.md §2.1](./config.md) 与 [client.md §4](./client.md))**; > **v0.25.6 jev 模型 id / 分类器地址进配置页(`gate.model` / `gate.jevEndpoint` 可读可改, > 保存即热切换分类器,见 [gate.md §4](./gate.md))已实现**。 > 协议端事实见 [onebot-channel.md](./onebot-channel.md),群聊闸门见 [gate.md](./gate.md), > 分期见 [roadmap.md](./roadmap.md)。官方机器人通道已降为备选 > ([official-bot-channel.md](./official-bot-channel.md))。 ## 1. 形态与边界 - **host 半身单进程插件**,随 DSH profile 的 GUI 启动;relay 在线 = 该 GUI 进程在线。 - **client 半身**在**插件面板的组合包页**里提供配置页(P1.5/P4/P7/P10):连接状态、配置编辑、 群项目目录编辑与重新绑定、闸门状态 / 最近消息 / 映射管理、画图 / 视频(ComfyUI)配置与状态、 NapCat Docker 环境诊断与修复,按「连接 / 环境 / 项目 / 行为 / 画图·视频 / 预设 / 记录」分二级页面 (契约见 [client.md](./client.md));审批开关等仍在后续。 - 不引入外部协议端进程(NapCat 跑在用户侧的 Docker 容器里,插件只连 OneBot WS); 插件运行期**零外部依赖**(只用 node: 内置模块与 Node 24 的全局 WebSocket)。 - **插件不管理 profile**:由用户在目标 profile 安装,插件只读取 apply(ctx, config) 的配置。 - 单实例:同一 bot 只允许一个进程连接(文件锁),避免重复事件与账号互踢。 ## 2. 已定决策 | 主题 | 决策 | |---|---| | 协议端 | 真实 QQ 号 + NapCat + OneBot 11 正向 WebSocket | | 账号 | QQ 小号登录 NapCat 作 bot;主号操作 | | 首批场景 | **群聊优先**;私聊放 P3 | | 消息闸门 | 10 分钟快路 + 2 小时慢路 + jev 分类器;见 gate.md | | @ 直通 | @ / 引用回复 / 显式命令不走闸门,直接交给 DSH | | QQ 侧命令 | `/new` / `/stop` / `/status` / `/help` 由命令层截获(先于直通与闸门,群主 / 管理员可用),见 [commands.md](./commands.md) | | 回复体验 | 结果首条引用触发消息;长轮次先回「处理中」占位;直通消息 400ms 内合并,见 [reply.md](./reply.md) | | 回复 | 没有平台回复窗口,turn 结束即可发;受 QQ 风控与频率限制 | | 最小发言间隔 | 默认 45 秒,期间的消息并入下一批 | | 会话映射 | 默认一个 QQ 群对应一个自动创建的 DSH 会话(草案,见第 5 节) | | 审批 / 主动推送 / 媒体 / 面板 | 未定,放 P3 | ### 2.1 配置(P1 + P1.5 配置页) 配置由用户在目标 profile 的插件配置里提供(`apply(ctx, config)` 读取);**token、QQ 号、 群号都不进仓库**,只写在 profile 配置里。字段的操作参考(默认值 / 范围 / 现在能否在 配置页改 / 改完怎么生效)集中在 [config.md](./config.md),下表是设计视角的摘要: | 键 | 类型 | 默认 | 说明 | |---|---|---|---| | `wsUrl` | string | 必填 | NapCat 正向 WS,如 `ws://127.0.0.1:3001` | | `accessToken` | string | 必填 | OneBot token | | `selfId` | number | 必填 | bot 小号 QQ 号,连接后用 `get_login_info` 校验 | | `groups` | number[] | 必填 | 群白名单,未列出的群直接丢弃 | | `workspace` | string | `~/dsh/qq-relay-ws` | 默认项目目录(会话 cwd + 聊天记录落盘位置);可在配置页改(只影响之后新建的会话) | | `groupWorkspaces` | object | `{}` | 群号 → 项目目录;聊天记录写进 `<项目>/.qq-relay/chat/<群号>/`,会话 cwd 同源;优先级高于 `workspace`,低于已钉住的映射(P4,见 [chat-context.md](./chat-context.md)) | | `chatRetentionDays` | number | `0` | 聊天记录保留天数;`0` = 永不自动删除(P4 默认);`7..3650` 时启动与每日清理;配置页可改,另有「立即清理」路由(v0.13.0) | | `sessionTitle` | string | `QQ群:{groupName}` | 自动创建会话的标题模板 | | `replyChunkSize` | number | 1500 | 出向文本切分上限(待实测调整) | | `commandPrefixes` | string[] | `['/dsh']` | 群消息文本以此开头时直通(绕过闸门) | | `commands` | object | `{ enabled: true, bare: true, roles: ['owner','admin'] }` | QQ 侧命令层(P5,v0.11.0):`/new` / `/stop` / `/status` / `/help`;`bare` 控制裸写式,`roles` 控制可执行角色,详见 [commands.md](./commands.md) | | `reply` | object | `{ quote: true, ackDelayMs: 8s, mergeWindowMs: 400ms }` | 出向回复体验(P6,v0.11.1):引用触发消息 / 处理中占位阈值(0 = 关)/ 直通连发合并窗口(0 = 不合并),详见 [reply.md](./reply.md) | | `poke` | object | `{ enabled: true, react: true, cooldownMs: 10s }` | 戳一戳(v0.15.0):是否识别 / 被戳是否接话 / 每群冷却(0 = 不限,上限 10m),详见 [poke.md](./poke.md) | | `files` | object | `{ enabled: true, react: true }` | 入向群文件(v0.16.0):是否识别 `group_upload` / 落盘成功后是否直通起一轮;上限 20 MiB,详见 [media.md §5.2](./media.md) | | `proxyReply` | object | `{ enabled: false, targets: [], silenceWindow: 10m }` | 代答(v0.18.0):@ 名单里的人且 TA 最近没说话时,直通起一轮并让 agent 代 TA 回应;名单可写 QQ 号 / 群名片 / 昵称,详见 [gate.md §2.6](./gate.md) | | `gate` | object | `{ enabled: false, ... }` | 消息闸门(P2.1/P2.3):`enabled` / `mentionBypass` / `fastWindow` / `fastMinReplyInterval` / `debounce` / `slowTimer` / `slowWindow` / `maxBatchMessages` / `classifierTimeoutMs` / `model` / `jevEndpoint` / `jevApiKeyEnv` / `jevApiKey`(v0.24.0,只写不读)/ `jevThreshold`(v0.25.6 起 `model` / `jevEndpoint` 页面可改);时长可写 `45s` / `10m` / `2h` 或毫秒数字,详见 [gate.md §8](./gate.md) | | `approval` | object | `{ enabled: true, timeoutMs: 120s, roles: ['owner','admin'] }` | QQ 侧审批(P3):是否接管、等待时长、允许审批的群角色,详见 [approval.md](./approval.md) | | `agentPreset` | string | `''` | 群会话使用的 DSH agent preset id;空 = 宿主默认。插件自带 `qq-relay`(显示名「QQ 群聊」)。preset 在会话创建时固定,详见 [persona.md](./persona.md) | | `persona` | object | `{ enabled: false, text: '' }` | 轻量人设(v0.19.0):每轮 prompt 头部 `[角色设定:…]`;≤4000 字符,默认关闭 | | `comfy` | object | `{ enabled: false, baseUrl: 'http://192.168.1.10:8188', timeoutMs: 30000, skillEnabled: true }` | 画图 / 视频(v0.21.0 / v0.22.0):ComfyUI 探测开关 / 地址 / 探测超时(3s..120s)/ 连上后是否注册 `comfy-image` 与 `comfy-video` 两个 skill,详见 [comfy.md](./comfy.md) | | `environment` | object | `{ enabled: true, dir: '~/napcat-docker', webuiToken: '' }` | 环境一键配置(v0.23.0):环境页开关 / NapCat Docker 环境目录 / WebUI token(只写不读);非法值只回退默认值不判死配置,详见 [env.md](./env.md) | - 会话映射 / 闸门状态 / 决策日志落在 `~/.dsh/storages/dsh-qq-relay/`;**聊天日志(P4)落在各群项目下的 `<项目>/.qq-relay/chat/<群号>/YYYY-MM-DD.jsonl`**(agent 可直接 read;目录内 `.gitignore` 为 `*` 自我忽略)。 - **配置传递**:写在目标 profile 的 `~/.dsh/profiles//cordis.patch.yml`,用 `- id: dsh-qq-relay` + `config:` 整块替换;token 放 `~/.dsh/.env` 的 `DSH_QQ_RELAY_TOKEN`, patch 里用 `!!js 'process.env.QQ_RELAY_TOKEN'` 引用(注意:`DSH_` 前缀是 DSH 启动环境的保留名,不能写进 `.env`)。改配置后需重启该 profile 的 GUI。 - 临时停用整行:`- id: dsh-qq-relay` + `disabled: true`。 - **插件预设(v0.20.0)**:`presets.json` 存 persona + 模型 / 思考层级 / 权限;host 启动时 `ctx.agentPresets.register()` 动态注册成 DSH agent preset,保存后立即重新注册。组合表来自 `lib/model/preset-template.js`(内置 `standard` 快照;v0.25.5 起按宿主报错自动剔除已移除字段重试), 见 [persona.md](./persona.md)。 每群会话首次派发前应用预设的模型(追加 `model/selection`)与权限(`permissionPresets.set`)。 - **画图 / 视频 comfy controller(v0.21.0;v0.22.0 起两个 skill)**:组合根构造 `createComfyController({ getConfig })` 后 `start()` + 启动探测;`skills` 服务晚于插件就绪 (或不存在)时,`ctx.inject(['skills'], (skillsCtx) => comfyController.attachSkills(skillsCtx.skills))` 再挂载。控制器周期探测 ComfyUI(`/system_stats` + `/queue`),**首次连上**且配置允许时遍历 `COMFY_SKILLS` 逐个调用 `ctx.skills.register({ name, source: 'custom', provider: 'dsh-qq-relay', resourceBase: { kind: 'directory', path: '<插件>/lib/skills/comfy-image' }, content })` 注册 `comfy-image`(`SKILL.md`)与 `comfy-video`(`SKILL-video.md`)两个 runtime skill (共用目录 / resourceBase,各自记录 `skills[].loaded/error`,一个坏了不影响另一个); 探测失败**不注销**(避免目录抖动),显式关闭 `enabled` / `skillEnabled` 才注销全部。 **与 preset 注册是两条独立链路**:preset 走 `ctx.agentPresets.register()`,影响新建会话的 提示词 / 组合表;skill 走 `ctx.skills.register()`,影响所有会话的可用技能。两者都不重启 GUI, 服务缺失时都软失败(`status.comfy.supported = false` / `presetService.supported = false`)。 - **环境 controller(v0.23.0)**:组合根构造 `createEnvController({ getConfig, getRelayInfo, backfill })`; 探测由「环境」页轮询 `GET /api/env` 驱动(`ensureFresh`,无定时器),动作走 `POST /api/env/action`(白名单 + 固定 argv + `shell: false`)。命令类动作后台执行,状态快照 带 `running` 与日志尾部;`backfill-config` 复用配置页的 `saveConfig`(落盘 + 重连)。 - **配置页覆盖(P1.5)**:插件面板 → dsh-qq-relay 组合包页保存的字段写入 `~/.dsh/storages/dsh-qq-relay/settings.json`,叠加在 profile patch 之上——只有 `wsUrl / accessToken / selfId / groups / workspace / groupWorkspaces / chatRetentionDays / sessionTitle / replyChunkSize / commandPrefixes / gate.{debounce,fastMinReplyInterval,jevThreshold,jevApiKey,model,jevEndpoint} / proxyReply.{enabled,targets,silenceWindow} / agentPreset / persona.{enabled,text} / comfy.{enabled,baseUrl,timeoutMs,skillEnabled} / environment.{enabled,dir,webuiToken}` 可覆盖, 其余字段保持 patch 只读。保存后**立即重建连接**,不需要重启 GUI;仅 `comfy` / `environment` 变化时不重建 relay,只重新探测(画图 skill / 环境步骤);`gate.jevApiKey` / `gate.model` / `gate.jevEndpoint` 只换分类器实现(v0.25.6 起模型与地址页面可改),同样不重建连接 (key 本身永不回显)。 `groupWorkspaces` 是整表替换;已映射群改项目后需在配置页点「重新绑定」才会换会话(旧项目聊天记录不搬)。 契约、HTTP 面与安全边界见 [client.md](./client.md)。 ## 3. 模块分层 ~~~text lib/ ├── index.js # 组合根:读配置、门控服务、装配 controller/model、挂配置页路由 ├── controller/ │ ├── onebot/ │ │ ├── client.js # 正向 WS 客户端:连接、鉴权、重连、心跳、echo │ │ ├── api.js # action 调用封装(send_group_msg 等)与 retcode 处理 │ │ └── codec.js # OneBot 事件 / 消息段 → 内部消息模型 │ ├── runtime.js # 运行实例:client+api+relay 三件套,支持整份配置热替换 │ ├── config-routes.js # 配置页 HTTP 面:status / config / reconnect / rebind / cleanup / messages / mapping / presets / comfy test │ ├── http.js # JSON 信封、请求体读取(配置页共用件) │ ├── gate.js # 闸门调度(P2.1):快路去抖 / 慢路定时 / 最小间隔 / 分类器注入 │ ├── tools.js # qq_send(文本 / 图片 / 视频 / 文件)/ qq_poke 工具定义(P3 / v0.15.0 / v0.22.0:手写 ToolDefinition,零外部依赖) │ ├── approval.js # QQ 侧审批桥(P3:approval/request → 群消息 → 文本命令) │ ├── relay.js # 入向投 DSH、出向收 turn 结果并回复、断线发送队列、主动推送、命令层分发(P5)、戳一戳(v0.15.0)、出向视频 sendVideo(v0.22.0) │ ├── comfy.js # 画图 / 视频(v0.21.0 / v0.22.0):探测 ComfyUI / 逐个注册两个 skill / 抖动保持 / 测试连接 │ └── env.js # 环境(v0.23.0,v0.25.0 逐步补全):7 步探测 / 固定 argv 动作 / 后台命令 + 日志 / 逐轮修复链 + fixPlan ├── skills/comfy-image/ # 插件自带画图 / 视频 skill(SKILL.md + SKILL-video.md 模板 / scripts / 7 个 workflows / references 含 video-workflows.md),随包发布 └── model/ ├── config.js # 配置默认值与校验 ├── settings.js # 配置页覆盖:层序合并、token 脱敏、settings.json 原子读写 ├── mapping.js # group_id ↔ sessionId 映射与持久化 ├── chat-log.js # 群消息 JSONL(按群项目路由)/ 读取 / 清理 / 旧日志迁移 ├── cleanup.js # 清理(v0.13.0):立即清理天数校验 + 聊天 / 媒体一次性清理(自动清理共用) ├── panel.js # 配置页面板(v0.14.0):最近消息查询校验 + 聊天记录 → 只读消息视图 ├── media.js # 入向图片 / 表情 / 视频 / 语音 / 群文件落盘与清理、媒体类型识别、出向图片 / 视频 / 文件格式与上限(P4.1 / P4.2 / v0.16.0 / v0.22.0) ├── project.js # 群项目目录解析、`.qq-relay/` 创建与自忽略 .gitignore(P4) ├── prompt.js # 消息拼接:群名 / 时间 / 条数 / @ 与引用标记 / 历史指针(P4) ├── gate-log.js # 闸门决策日志 JSONL(P2.2,不含正文,保留 7 天) ├── gate.js # 闸门纯逻辑(P2.1):配置解析 / 决策 fail closed / 批次 / 时间窗 ├── approval.js # 审批纯逻辑(P3):命令匹配 / 决策角色 / 提示文案 ├── commands.js # QQ 侧命令层纯逻辑(P5):命令解析 / 角色校验 / 回复文案 ├── state.js # 每群状态按群分文件 + 旧 state.json 迁移 + 单实例锁 ├── jev.js # jev 分类器(P2.3):OpenRouter Decisions API / Noul / 阈值 / fail closed ├── reply.js # 回复切分、退避、发送失败分类 ├── comfy.js # 画图 / 视频配置(v0.21.0 / v0.22.0):comfy 段校验 / 脱敏视图 / COMFY_SKILLS / 模板渲染 └── env.js # 环境配置(v0.23.0):environment 段 / compose 生成解析 / OneBot 校验 / 步骤求值 / 动作 argv / 日志缓冲 ~~~ client 半身(`src/`)只做配置页(MVVM 三层):`src/model/{api,form}.ts`(HTTP 信封、表单 ↔ patch 纯逻辑)→ `src/viewmodel/{settings,env}.ts`(状态 / 动作 hook、状态文案、二级页面定义)→ `src/view/` (`SettingsPage` 外壳、`controls`、`theme` 注入的 `--dsw-*` 样式、`panels/*` 七个二级页面); `src/client.tsx` 只做注册与装配(`plugins.bundle.config`,key = 包名)。host 半身只依赖 lib/model/。 P1 实现 `controller/onebot/{client,api,codec}.js`、`controller/relay.js` 与 `model/{config,mapping,state,chat-log,reply}.js`;P1.5 新增 `controller/{runtime,config-routes,http}.js` 与 `model/settings.js`;P2.1/P2.3 新增 `controller/gate.js` 与 `model/{gate,jev,gate-log}.js`; P3 新增 `controller/{tools,approval}.js` 与 `model/approval.js`;v0.21.0 新增 `controller/comfy.js`、`model/comfy.js` 与 `lib/skills/comfy-image/`(画图 runtime skill); v0.22.0 在同一 controller 注册 `comfy-video`,并接出向视频链路(`relay.sendVideo` / `api.sendGroupVideo` / `tools.videos` / `media.isVideoExt`);P4 新增 `model/{project,prompt}.js`, 聊天日志改由群项目路由(`chat-log.js` 的 `rootFor` + `migrateLegacyChatLogs`);P5 新增 `model/commands.js`,命令分发与 `createSessionForGroup` / `sendSystemReply` 在 `controller/relay.js`。 `test/` 放 `node:test` 单测(零依赖,`pnpm test`),当前覆盖:配置校验、映射 / 状态持久化、 单实例锁、聊天日志读写清理、立即清理、面板最近消息、回复切分与退避、设置覆盖与脱敏、配置页路由面、闸门纯逻辑与 调度器(假时钟:去抖合并 / 最小间隔 / 慢路时间窗 / dshBusy 延后 / fail closed / 重启恢复)、 决策日志、断线发送队列、jev 请求 / 响应 fail closed、`qq_send`(含 v0.22.0 `videos`)/ `qq_poke` 工具契约与注册、审批命令匹配与桥接、群项目目录解析 / 聊天日志迁移 / prompt 拼接(P4)、 媒体落盘(P4.1)、群文件下载(v0.16.0)、出向视频 `sendVideo`(v0.22.0)、双 skill 注册 / 单坏隔离 (v0.22.0)、戳一戳识别 / 冷却 / 出向(v0.15.0)、环境页配置 / compose / OneBot 校验 / 步骤求值 / 动作 argv / 日志脱敏 / controller 动作与一键修复(v0.23.0),以及 `apply()` 组合根集成(假 ctx + 临时 HOME)。 ## 4. 事件流 ### 4.1 入向(QQ → DSH) ~~~text OneBot WS 事件 → codec 规范化(message_id / group_id / user_id / sender / message[] / raw_message) → notice 分支:戳一戳(v0.15.0)→ 写聊天记录;被戳时按直通接话(见 poke.md) → notice 分支:群文件(v0.16.0)→ get_group_file_url → 下载落盘;成功且 files.react 时直通(见 media.md §5.2) → 忽略 message_sent、去重(message_id) → 含图片 / 表情时先下载到 /.qq-relay/media/(失败只记 failed) → 落盘 chat log(/.qq-relay/chat/<群号>/,媒体路径写进 media 数组) → 命令层(/new /stop /status /help;只认白名单命令名,命中即执行并回复,不派发 prompt) → 直通(@ bot / 引用 bot 的消息 / 命令;先经 400ms 连发合并窗口) → 代答(v0.18.0:@ 名单内的人且 TA 最近没发言 → 直通,reason=proxy) → @ 别人 / @全体成员:只记日志,结束 → gate(快路 / 慢路;分类器未接时普通消息 fail closed) → relay.dispatch(prompt 拼接:群名 / 时间 / 条数 / @ 与引用标记 / 历史指针) → ctx.sessionController.prompt({sessionId, content, source}) 或 ctx.sessionController.resolveAgent(id) + agent.followup(createUserMessage(...)) → DSH turn ~~~ - 冷会话用 sessionController 的 resolve / prompt 自动恢复;新建会话用 ctx.agents.create。 - 每行消息带发送者显示名(群名片 → 昵称 → QQ 号兜底)与本地时间;@ 与引用分别标注,历史给相对路径指针。 - 关联:用 agent/inbox/claimed 把 user message id 映射到 turn 号,保证只回触发轮次。 ### 4.2 出向(DSH → QQ) ~~~text ctx.on('session/event', (session, event) => ...) ├─ assistant/message(本轮文本 / interrupted) └─ turn/end(reason.kind: completed / aborted / error / ...) ctx.on('agent/error', ...) # 失败提示 → relay 收集本轮文本 → reply 构造消息(切分 / 纯文本降级) → api.send_group_msg ~~~ - 只回「由 QQ 触发」的轮次;GUI 里发起的轮次默认不回发(避免刷屏)。 - 发送不区分被动 / 主动,turn 结束即发;失败按 retcode 退避重试。 - **回复体验(P6)**:结果首条引用触发消息(引用失败自动退回普通发送);turn 超过 8 秒未结束先回 「收到,处理中…」(不刷新闸门窗口);见 [reply.md](./reply.md)。 - **主动推送(P3)**:`qq_send` 工具(见 [tools.md](./tools.md))走同一个发送队列, 供任意会话发通知;白名单双重校验,离线排队。 - **出向媒体(P4.2;v0.22.0 补视频)**:`qq_send` 的 `images` / `videos` / `file` 由 relay 校验 本地文件(大小 / 图片或视频格式)后转 base64,经 `send_group_msg`(image / video 段)与 `upload_group_file` 发送;视频最多 1 个、≤30 MiB、NapCat 内联播放;离线不排队, 见 [media.md §5.1](./media.md) / [tools.md](./tools.md)。 - **QQ 侧审批(P3)**:QQ 会话里的 `approval/request` 由审批桥发到群里,群主 / 管理员回复 「同意 / 拒绝」;见 [approval.md](./approval.md)。 - **戳一戳(v0.15.0)**:入向 `notice.notify.poke` 写聊天记录,戳 bot 时直通起一轮;`qq_poke` 工具让 agent 回戳白名单群成员(`group_poke`,离线报错不排队);见 [poke.md](./poke.md)。 - **画图 / 视频(v0.21.0 / v0.22.0)**:skill 由 skills 服务动态注册,agent 在任意会话 (含 QQ 群会话)可触发;产物落在群项目 `assets/comfy/output/`,图片用 `qq_send` 的 `images`、视频用 `videos` 发回群(见 [comfy.md](./comfy.md) / [media.md §5.3](./media.md))。 - 失败 / 中断:按配置发简短提示或静默。 ## 5. 会话映射 - 键:group_id(群聊);后续私聊用 user_id。 - 值: ~~~json {"sessionId": "qq-group-", "workspacePath": "/abs/path", "title": "QQ群:xxx", "createdAt": 1780000000000} ~~~ - 映射持久化在 `~/.dsh/storages/dsh-qq-relay/mapping.json`;项目目录按 §2.1 的 `groupWorkspaces[群号] → workspace` 解析,创建会话时**钉进 `workspacePath`**,之后聊天目录与 cwd 都以它为准。 - 默认**自动创建**:首次需要接话时 sessionController.create({cwd: 项目目录}) + workspaceRegistry.create(项目目录) + attachSession + sessionTitle.rename;映射持久化。 v0.21.1 起 attach 失败会**丢缓存重试一次**(宿主热重载 workspace 服务会让缓存实体失效), relay 启动时还会按 mapping 把历史会话**补挂**回 workspace;仍失败的记录在 `status.workspace.attachIssues`(对应「未分组」现象)。 - 也支持配置直接绑定已有 sessionId(「遥控现有会话」),绑定后同样受闸门控制。 - 会话被删除 / 失效 → 重新创建并更新映射。 - 一个群 = 一个会话;群内多用户共享上下文,靠昵称前缀区分。 - 会话在 GUI 侧栏可见(通过 workspace attach),与桌面端共享同一会话。 ## 6. 安全 - **群白名单**:按 group_id 白名单,未列出直接丢弃;@ 直通同样校验。 - **账号风险**:非官方协议端有封号风险;使用小号,控制发送频率;文档明示风险由用户承担。 - **权限预设**:relay 会话使用受限权限预设(可配置),降低误操作面。 - **审批**:P3 用文本命令(同意 / 拒绝)实现 QQ 侧审批;v1 先依赖 DSH 现有审批。 - **提示注入**:群成员文本一律视为不可信输入;工作目录限定;操作留审计日志。 - **本地接口**:NapCat WS 只监听回环地址;启用 access_token;不暴露公网。 - **环境动作(v0.23.0)**:只跑白名单命令(`docker` / `docker compose` / `brew` / `colima`), 固定 argv、`shell: false`、不 `sudo`;安装 / 启动引擎 / 写 OneBot 必须显式确认; WebUI token 只写不读,动作日志写入前脱敏;详见 [env.md](./env.md) §8。 - **jev API key(v0.24.0)**:配置页保存的 `gate.jevApiKey` 只写不读(`status` 只给是否配置 / 来源 / 兜底环境变量名),落盘在 `settings.json`;优先于环境变量,日志里不回显。 v0.25.6 起 `gate.model` / `gate.jevEndpoint` 也可在页面改——它们不是密钥,`status` 直接回显 当前生效值(页面回填用),只有 key 保持只写不读。 - **画图 / 视频服务(v0.21.0 / v0.22.0)**:ComfyUI **无鉴权**,任何能访问 `comfy.baseUrl` 的人都能提交任务; `POST /prompt` 可提交任意节点图、自定义节点里能跑任意 Python,等于**远端执行权**。 只放可信网络、不暴露公网;skill 运行器只允许 schema 白名单参数,不要绕过它直连。 - **隐私**:聊天日志落在群项目内的 `.qq-relay/`(自忽略、不进 git),默认**永久保留** (`chatRetentionDays: 0`);需要时配天数自动清理,或在配置页「记录」里「立即清理」(媒体按日期目录一起清)。 决策日志仍在 storage 下按 7 天保留。 ## 7. 运行前提 1. NapCat 跑在 Docker(macOS 用 Colima)里,bot 小号已扫码登录;macOS 原生 Shell 不可用 (原因见 [onebot-channel.md §3.1](./onebot-channel.md))。 2. NapCat 开启 OneBot 11 正向 WS(`127.0.0.1:3001` + token),只监听回环。 3. 目标 profile 装入本插件并填配置;当前约定用 `dwdev`(该 profile 已清空为只装本插件)。 4. 重启该 profile 的 GUI(bundle 组合在启动时发生)。 5. DSH GUI 需常驻;GUI 关闭则 relay 离线。 ## 8. 未决 - ~~jev 的部署形态与模型~~ 已定:OpenRouter Decisions API + `typesafe/jev-1.14`(P2.3);阈值调优待真实群聊数据。 - ~~审批文本命令的具体交互与超时策略~~ 已落地(P3,见 [approval.md](./approval.md))。 - 单聊(private)是否与群共享同一会话映射体系。 - 媒体:入向图片 / 表情 / 视频 / 语音 / 群文件与出向图片 / 视频 / 文件已落地 (v0.12.0 / v0.16.0 / v0.22.0,见 [media.md](./media.md));仍缺入向媒体转 DSH 附件、视频 / 语音转文字。 - ~~DSH → QQ 主动推送工具~~ 已落地为 `qq_send`(见 [tools.md](./tools.md))。 - client 面板增强:映射管理、审批开关、闸门可视化(基础配置页已在 P1.5 落地,见 [client.md](./client.md))。 - 多群并发时的资源上限与队列策略。