Appearance
架构说明
English version: architecture.en.md
本文档是 openclaw-channel-dingtalk 的模块职责边界、增量演进规则与架构协作约定的权威来源。
它面向维护者、贡献者以及在本仓库内工作的 AI / 代码代理使用。当 README.md、AGENTS.md 或 CONTRIBUTING* 中出现架构摘要时,以本文和 architecture.en.md 为准。
目标
- 在仓库持续演进、并且存在多个进行中 PR 的情况下,保持功能增长可控。
- 在做大规模物理迁移之前,先明确新代码应该落在哪类模块。
- 降低
src/根目录持续扩散、边界被侵蚀的风险。 - 在不改变既有运行时行为的前提下,支持渐进式重构。
工作规则
遵循 先逻辑分区,后物理迁移。
这意味着:
- 即使现有文件仍平铺在
src/下,新功能也应先遵守本文定义的逻辑领域边界。 - 现有文件不需要为了“先满足目标目录结构”而强制搬迁。
- 修改旧代码时,优先做能改善边界的小步重构,而不是顺手做大规模结构改写。
- 大范围文件移动应尽量与行为改动拆分到不同 PR 中。
核心原则
src/channel.ts是装配根。 它负责 runtime、gateway、outbound 入口和公共导出,不应持续吸收新的业务逻辑。- 领域模块应只回答一类问题。 除非职责天然不可拆分,否则不要在同一模块中混合路由、持久化、目标解析和发送策略。
- 避免新的“杂物间”。 不要默认把新逻辑继续塞进
utils.ts、helpers.ts或新的根级*-service.ts,除非它确实是跨领域复用能力。 - 目标解析优先走确定性数据源。 例如
conversationId这类 ID,必须来自平台回调、持久化索引或明确的人为输入,不能靠模型猜测。 - 保持已有低层模块边界稳定。 已经职责明确的模块,应维持聚焦,而不是继续吸收相邻语义。
逻辑领域
无论仓库当前是否已经物理重排,代码都应优先按以下逻辑领域理解和演进。
Gateway
负责:
- Stream 客户端生命周期
- 回调注册与 ack
- 入站事件入口
- runtime 启停时序
示例:
src/channel.tssrc/inbound-handler.tssrc/connection-manager.ts
不负责:
- 长期目标目录语义
- 与入站投递无关的跨功能持久化结构
- 通用目标查找规则
Targeting
负责:
conversationId与 sender/group 身份处理- session peer 解析
- 大小写敏感 ID 恢复
- 后续群目录和目标 alias 解析能力
示例:
src/session-routing.tssrc/session-peer-store.tssrc/peer-id-registry.ts
不负责:
- 出站消息格式与投递策略
- AI Card 生命周期
- command 领域持久化
Messaging
负责:
- 入站消息内容提取
- reply strategy 选择
- 文本 / markdown / media 出站发送
- 短生命周期消息上下文持久化
示例:
src/message-utils.tssrc/send-service.tssrc/reply-strategy*.tssrc/message-context-store.tssrc/media-utils.ts
Card
负责:
- AI Card 创建 / 流式更新 / 结束态流程
- 待恢复卡片状态与缓存
- 卡片特有的 fallback 行为
示例:
src/card-service.tssrc/card-callback-service.ts
Command
负责:
- slash 命令解析与分发相关领域逻辑
- feedback learning 策略与持久化
- 目标级规则与 target set
- 后续各类扩展 slash 命令能力
示例:
src/learning-command-service.tssrc/feedback-learning-service.tssrc/feedback-learning-store.ts
Platform
负责:
- 配置解析与 schema
- 认证与 token 缓存
- runtime getter / setter
- 共享 logger context
- 公共类型定义
示例:
src/config.tssrc/config-schema.tssrc/auth.tssrc/runtime.tssrc/logger-context.tssrc/types.ts
计划中的目录结构
下面的目录结构是后续渐进迁移的目标态,用于指导新代码落位,不表示需要立即完成整体搬迁。
text
src/
channel.ts
gateway/
inbound-handler.ts
connection-manager.ts
targeting/
session-routing.ts
session-peer-store.ts
peer-id-registry.ts
group-directory-store.ts
group-target-resolver.ts
messaging/
send-service.ts
message-utils.ts
media-utils.ts
message-context-store.ts
reply-strategy.ts
reply-strategy-card.ts
reply-strategy-markdown.ts
reply-strategy-with-reaction.ts
card/
card-service.ts
card-callback-service.ts
command/
learning-command-service.ts
feedback-learning-service.ts
feedback-learning-store.ts
platform/
auth.ts
config.ts
config-schema.ts
runtime.ts
logger-context.ts
types.ts
shared/
persistence-store.ts
dedup.ts
utils.ts说明:
src/channel.ts继续作为装配根和底层公共导出入口。- 即使相邻旧文件还没有迁移,新模块也应优先参考这套领域结构落位。
- 现有文件不需要为了“对齐目录”而强制搬迁,除非这次改动本身确实能显著改善边界或降低耦合。
group-directory-store.ts、group-target-resolver.ts这类文件表示的是计划中的能力落点,不代表当前仓库已经存在这些文件。
重要既有边界
下面这些边界已经形成,应继续保持稳定。
peer-id-registry.ts
用途:
- 当上游 session key 或输入把 DingTalk peer ID 小写化后,恢复其原始大小写
- 从已有
sessions.json预热内存注册表
负责:
lowercased-id -> original-id恢复- 观测到的 ID 的内存注册
- 从 session 文件做一次性 preload
不负责:
- 群显示名查找
- 人工 alias 存储
conversationId -> title目录状态- 模糊目标匹配
session-peer-store.ts
用途:
- 持久化 session peer override,用于合并或重定向 OpenClaw 的会话身份
负责:
sourceKind + sourceId -> logical peerIdoverride- 由 owner 命令控制的会话共享行为
不负责:
- DingTalk 目标发现
groupDisplayName -> conversationId查找- 群元数据目录
- 面向自然语言标签的出站目标解析
后续目标目录能力
凡是涉及以下解析能力:
groupDisplayName -> conversationIdmanual alias -> conversationId- 历史群名变更追踪
都应进入独立的 targeting 模块,例如:
src/group-directory-store.tssrc/group-target-resolver.ts
不要把这些职责继续塞进 peer-id-registry.ts 或 session-peer-store.ts。
新代码落位规则
新增代码时,遵循以下规则:
- 如果代码解决的是“这条消息指向哪个目标”,它属于 targeting
- 如果代码解决的是“目标已确定后如何发送”,它属于 messaging 或 card
- 如果代码只是负责模块装配,就放在
src/channel.ts,并保持轻量 - 如果一个模块同时开始承担“入站 payload 解析”和“持久化检索索引”,应考虑拆分职责
- 如果某个 helper 只对一个领域有意义,应留在该领域内,而不是上提到全局工具文件
渐进迁移策略
当前仓库在 src/ 下仍有较多根级文件,这是过渡期内可接受的状态。
迁移策略如下:
- 不要求贡献者为了交付一个 bug fix 先做全仓文件搬迁
- 新功能应尽量沿着本文定义的目标边界落位
- 只要不会明显扩大 PR 范围,欢迎做机会式的小幅边界整理
- 文件迁移与行为改动最好拆成不同 PR
- 不应仅因为仓库尚未完成物理重排,就阻塞进行中的 PR
Review Checklist
在评审或发起 PR 时,可以先问:
- 这次改动是否把新的业务逻辑继续塞进了
src/channel.ts? - 这份新持久化状态本来属于某个既有领域,还是只是被挂到了“离得最近”的文件上?
- 某个目标解析功能是否被错误地放进了 session 共享或大小写恢复模块?
- 这次改动是否又引入了一个泛化 helper 文件,掩盖了领域边界缺失?
- 这个行为是否可以通过新增一个小模块来实现,而不是继续放大一个无关模块?
相关入口
README.mdCONTRIBUTING.mdCONTRIBUTING.zh-CN.mdAGENTS.md