dsh-qqchat 架构
August 26, 2026 · View on GitHub
核心边界
dsh-qqchat 是独立的 DSH out-of-tree 插件,不 fork DeepSeek Harness。
dsh-qqchat
QQ protocol / auth / identity
QQ real history
receive policy
group + member memory
tool authority
proactive sending
QQ-specific Client presentation
DSH
Agent loop
Session lifecycle
model routing
tools
Session list
Conversation shell
QQ Chat 不维护第二套 AgentLoop,也不维护第二套聊天导航。
Session 是主导航单位
每个 QQ peer 映射一个正常 DSH Session:
group -> groups.dsh_session_id
c2c -> members.dsh_session_id
sessionId = qqchat-<uuid>
标题通过 DSH sessionTitle 写入:
QQ群 · <群名>
QQ私聊 · <昵称>
Client 不再注册 sidebar.footer.action 会话选择器。QQ Session 直接由 DSH 自己的 Session/Workspace Browser 展示和打开。
QQChat 不再提交内部 bootstrap wake。新建的静默 Session 保持 DSH 原生 blank 状态,因此可以直接使用 DSH 的 Agent Preset 选择器;第一条真正触发 Agent 的 QQ 消息才会创建正常 turn boundary。
插件配置中的 agentPreset 只用于新 Session。Session 被 DSH Web 选择过 preset 后,DSH 会写入 agent-preset/selected 事件;QQChat 恢复 Session 时读取该事件,优先恢复 Session 最近一次选择的 preset。
Settings 边界
settings.section: qqchat 只放控制面:
扫码 / 连接状态
自动回应 / @回复 / 静默记录
群聊消息兼容格式
私聊消息兼容格式
群成员可用工具
Owner stable ID
查看日志
Settings 不包含群列表、私聊列表、聊天记录或记忆主界面。
Conversation 边界
QQ Session 使用 DSH 主 Conversation。
conversation.chat.node
qqchat-message # 仅静默/未触发 Agent 的 QQ transcript
conversation.composer
QQ Session composer # 主动发 QQ,不触发本地 Agent
conversation.session.header.utilities
QQ 记忆
触发 Agent 的 QQ 消息
接收策略决定需要回复时,消息直接走 DSH 原生 user/message:
QQ inbound
-> SQLite
-> context snapshot
-> Agent.followup()
-> user/message
-> DSH Agent loop
-> assistant/tool events
-> QQ API
Source 的 kind 保持 user,同时附带 QQ metadata:
channel=qq
botId
chatType
chatId
senderId
senderName
messageId
mentioned
因此 DSH Conversation 把它当普通 user turn;插件不再为同一条消息额外 append qqchat/message,避免双份显示。
不触发 Agent 的 QQ 消息
@回复 下未 @Bot 的消息、以及 静默记录 下的群消息:
session.append('qqchat/message', ...)
qqchat/message 是 log-only 自定义 Session event:
- Client 可渲染 QQ 群友气泡;
- 不属于 DSH model surface;
- 不会唤醒 Agent;
- 不会调用 LLM。
官方 DSH 当前不会为 out-of-tree 插件事件提供正式注册 API。QQChat 在插件加载时
按兼容约定把 qqchat/message 注册到可达的 DSH KNOWN_SESSION_EVENT_TYPES 副本,
因此正常安装后旧 Session 可以继续恢复;如果某个特殊打包环境无法解析到宿主使用的
事件表,QQChat 会捕获恢复失败并创建新的 QQ Session,完整 QQ 历史仍保存在 SQLite。
插件不再从 SQLite 重放整段历史到 DSH Session。DSH Session 保存它实际经历过的 display/Agent events,完整 QQ 世界历史始终由 SQLite 负责。
Host / Client
DSH Web Client
│
│ Connection RPC /qqchat
▼
┌──────────────────────────────────────┐
│ dsh-qqchat Host │
│ │
│ QQBindService │
│ QQGateway ── QQApiClient │
│ │ │
│ ▼ │
│ QQChatRuntime │
│ │ │ │
│ │ ├── QQChatDatabase │
│ │ ├── MemoryEngine │
│ │ └── QQChatLogger │
│ ▼ │
│ DshQQBridge │
└───┬──────────────────────────────────┘
▼
DSH Agent / Session / Tools
QQ credential、Token、扫码 AES 临时 key 都只存在 Host。
三个数据面
1. QQ 真实数据面
SQLite 是 QQ 世界事实来源:
accounts
groups
members
group_members
messages
memory_documents
reflection_state
plugin_settings
outbox
2. DSH Session / UI 数据面
记录:
- Agent 真正参与的 turn;
- assistant/tool events;
- 不唤醒 Agent 的 log-only QQ transcript;
- Session title 和 turn boundary。
3. Model surface
模型只看到 DSH 正常 surface 以及插件明确注入的 context snapshot。QQChat 的
记忆快照使用 DSH 官方 @deepseek-ai/dsh-system-prompt runtime-context 来源,
由 DSH 的 projection 机制替换旧快照,避免每轮重复追加完整记忆消息。
完整群聊不会因为存在 SQLite 或 QQ UI transcript 就自动进入模型 history。
入站状态机
QQ dispatch
↓
normalize stable identity
↓
upsert member/group
↓
insert SQLite message
↓
receive mode
├─ silent
│ └─ qqchat/message -> stop
│
├─ mention + not @
│ └─ qqchat/message -> stop
│
└─ auto / mentioned / c2c
├─ assemble memory/context
├─ Agent.followup(user message)
├─ DSH tools/model loop
└─ QQ outbound
三种模式都会留下真实 QQ 历史并参与记忆活动;区别只是是否进入 Agent turn。
Identity
稳定身份优先级:
author.user_openid
|| author.member_openid
|| author.id
原则:
senderId -> 身份 / 权限 / 记忆主键
senderName -> 仅展示
不根据昵称推断身份。
Agent context
群聊每次 Agent turn 前从 SQLite 重新装配:
recent group history
+ group profile
+ group summary
+ group memory
+ group daily
+ current member profile
+ current member pattern
+ current member summary
+ stable sender/group metadata
当前 QQ 消息本身以正常 user prompt 进入 turn;可靠身份信息则由 source metadata 和 context snapshot 提供。私聊快照包含 member 的 profile、pattern、summary、daily 和 memory;群聊快照包含群级文档、当前成员文档和近期群聊记录。
工具权限
使用 DSH 官方:
tools/pre-execute
当前 QQ Agent turn 临时记录:
sessionId -> { chatType, senderId }
规则:
非 QQ group turn -> next()
groupMembersCanUseTools == true -> next()
senderId == ownerUserId -> next()
其他 -> deny
不修改 Tool Runtime 或 AgentLoop。
Memory scope
Group
profile
summary
daily
memory
Member
profile
pattern
summary
Group scope 不跨群;同一 Bot 下的 Member scope 可以跨群连续。
反思以 idle debounce / batch threshold 触发,并使用 stable sender ID 做归属。
记忆 UI
QQ Session 顶部 QQ 记忆:
群聊:
group profile / summary / memory / daily
member list
-> 点击成员
-> member profile / pattern / summary / memory
私聊:
member profile / pattern / summary
私聊 daily 和 memory 仍属于 member scope 的持久化文档,参与反思和压缩,但当前 UI 尚未单独展示。
主动发送
QQ Session composer 调用:
chat/send
Host:
group -> /v2/groups/.../messages
c2c -> /v2/users/.../messages
主动发送直接进 QQ,不再作为本地 Agent prompt。
outbox 独立于 Session live 生命周期,留给定时/主动任务。
RPC
Host 注册 loopback RPC:
/qqchat
主要 endpoints:
status
auth/start
auth/poll
settings/get
settings/update
logs/list
chat/send
chat/info
旧的 chats/list / chat/ensure / group/* 仍可作为兼容 API,但当前主导航不再依赖它们。
SQLite 与 DSH persistence 的关系
qqchat.sqlite
QQ 真实历史 / identity / memory / settings / outbox
DSH Session persistence
Agent turns / model output / tools / DSH transcript
二者互不替代。
TypeScript 构建
src/
index.ts / config.ts / types.ts
commands/ DSH 原生命令的 QQ 分发适配
gateway/ QQ API、授权、Gateway、消息解析
session/ Agent bridge、Session runtime
transport/ DSH RPC
storage/ SQLite、记忆
shared/ 日志、DSH 类型扩展
client-src/*.cts
tests/*.test.ts
Host 使用 tsc;Client 编译到临时 CJS 后由 scripts/wrap-client.mjs 包装成 DSH Client factory。
lib/ 是生成物,不作为源码真相。