Appearance
Architecture
Chinese version: architecture.zh-CN.md
This document is the canonical source for module boundaries and incremental architecture rules in openclaw-channel-dingtalk.
It is written for maintainers, contributors, and AI/code agents working in this repository. When README.md, AGENTS.md, or CONTRIBUTING* summarize architecture rules, this file takes precedence.
Goals
- Keep feature growth manageable while the repository remains active and PRs are in flight.
- Make it clear where new code should live before doing large physical file moves.
- Reduce accidental boundary erosion, especially in
src/root-level modules. - Preserve current runtime behavior while enabling gradual refactoring.
Working Rule
Use logical domains first, physical moves second.
That means:
- New features should follow the domain boundaries in this document even if some existing files are still flat under
src/. - Existing files do not need to be moved just to satisfy the target layout.
- When touching old code, prefer small boundary-improving changes over broad structural rewrites.
- Large file moves should be separate from behavior changes whenever possible.
Core Principles
src/channel.tsis the assembly root. It wires runtime, gateway, outbound entry points, and public exports. It should not accumulate new business logic.- Domain modules should answer one class of questions. Do not mix routing, persistence, target resolution, and delivery semantics in the same module unless they are inseparable.
- Avoid generic dumping grounds. New code should not default to
utils.ts,helpers.ts, or new root-level*-service.tsfiles unless the logic is truly cross-domain. - Prefer deterministic resolution over model inference. IDs such as
conversationIdmust come from platform payloads, persisted indexes, or explicit operator input, not LLM guessing. - Preserve stable low-level boundaries. Existing focused modules with clear responsibilities should stay focused instead of absorbing adjacent concerns.
Logical Domains
The current and future code should be reasoned about in these domains, even before the repository is physically rearranged.
Gateway
Responsible for:
- Stream client lifecycle
- Callback registration and acknowledgement
- Inbound event entry points
- Runtime startup and stop sequencing
Examples:
src/channel.tssrc/inbound-handler.tssrc/connection-manager.ts
Not responsible for:
- Long-term target-directory semantics
- Cross-feature persistence schemas unrelated to inbound delivery
- General-purpose target lookup rules
Targeting
Responsible for:
conversationIdand sender/group identity handling- Session peer resolution
- Case-preserving ID restoration
- Future group directory and target alias resolution
Examples:
src/session-routing.tssrc/session-peer-store.tssrc/peer-id-registry.ts
Not responsible for:
- Outbound delivery formatting
- AI card lifecycle
- Command-domain persistence
Messaging
Responsible for:
- Inbound content extraction
- Reply strategy selection
- Text/markdown/media outbound delivery
- Short-lived message context persistence
Examples:
src/message-utils.tssrc/send-service.tssrc/reply-strategy*.tssrc/message-context-store.tssrc/media-utils.ts
Card
Responsible for:
- AI card create/stream/finalize flow
- Pending card recovery and caches
- Card-specific fallback behavior
Examples:
src/card-service.tssrc/card-callback-service.ts
Command
Responsible for:
- Slash command parsing and dispatch-oriented domain logic
- Feedback-learning policy and persistence
- Target-scoped learning rules and target sets
- Future extended slash-command capabilities
Examples:
src/learning-command-service.tssrc/feedback-learning-service.tssrc/feedback-learning-store.ts
Platform
Responsible for:
- Config parsing and schema
- Auth and token caching
- Runtime getters/setters
- Shared logger context
- Common type definitions
Examples:
src/config.tssrc/config-schema.tssrc/auth.tssrc/runtime.tssrc/logger-context.tssrc/types.ts
Planned Directory Layout
The following layout is the planned target structure for future incremental migration. It is a direction, not an immediate requirement.
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.tsNotes:
src/channel.tsremains the composition root and public entry for low-level exports.- New modules should prefer this domain layout even if neighboring legacy files have not moved yet.
- Existing files do not need to be relocated unless the change meaningfully improves clarity or reduces coupling.
- Planned modules such as
group-directory-store.tsandgroup-target-resolver.tsdescribe intended placement for future capabilities, not guaranteed current files.
Important Existing Boundaries
These boundaries are already established and should be preserved.
peer-id-registry.ts
Purpose:
- Restore original case-sensitive DingTalk peer IDs when an upstream session key or input has been lowercased.
- Warm the in-memory registry from existing
sessions.jsondata.
It is responsible for:
lowercased-id -> original-idrestoration- In-memory registration of observed IDs
- One-time preload from session files
It is not responsible for:
- Group display name lookup
- Manual alias storage
conversationId -> titledirectory state- Fuzzy target matching
session-peer-store.ts
Purpose:
- Persist session peer overrides used to merge or redirect OpenClaw session identity.
It is responsible for:
sourceKind + sourceId -> logical peerIdoverrides- Session-sharing behavior controlled by owner commands
It is not responsible for:
- DingTalk target discovery
groupDisplayName -> conversationIdlookup- Canonical group metadata storage
- Outbound target resolution for natural-language labels
Future Target Directory
Any new feature that resolves:
groupDisplayName -> conversationIdmanual alias -> conversationId- historical group title changes
should live in a dedicated targeting module, for example:
src/group-directory-store.tssrc/group-target-resolver.ts
Do not extend peer-id-registry.ts or session-peer-store.ts to absorb that responsibility.
Placement Rules For New Code
When adding new code, follow these rules:
- If the code decides which target a message refers to, it belongs to the targeting domain.
- If the code decides how a resolved target is sent to, it belongs to messaging or card.
- If the code only exists to wire modules together, keep it in
src/channel.tsand keep it thin. - If a module starts needing both inbound payload parsing and persistent lookup indexes, split those responsibilities.
- If a helper is only meaningful to one domain, keep it inside that domain instead of moving it to a global utility file.
Incremental Migration Policy
This repository currently has many root-level files under src/. That is acceptable during transition.
The migration policy is:
- No contributor is required to perform a repo-wide file move before shipping a bug fix.
- New features should prefer the target domain boundaries described here.
- Opportunistic refactors are welcome when they reduce confusion without expanding PR scope too much.
- File moves and behavior changes should preferably be separated into different PRs.
- In-flight PRs should not be blocked solely because the repository has not yet been physically reorganized.
Review Checklist
When reviewing or opening a PR, ask:
- Does this change add business logic to
src/channel.tsthat should instead live in a focused module? - Is this new persistence state part of an existing domain, or is it being attached to the nearest convenient file?
- Is a target-resolution feature being incorrectly added to session-sharing or case-restoration code?
- Does the change introduce a new generic helper file that is really hiding missing domain boundaries?
- Could the same behavior be implemented with a small new module instead of widening an unrelated one?
Related Entry Points
README.mdfor project overview and developer entry pointsCONTRIBUTING.mdCONTRIBUTING.zh-CN.mdAGENTS.md