DingTalk bot unbind was routed through the generic adapter config update,
but undefined credential fields are dropped during JSON serialization. That
left stored Client ID and Secret values intact after clicking the unbind
button. Match the working WeChat path with a dedicated server endpoint and
frontend API call so credential removal is explicit.
Constraint: JSON request bodies cannot preserve undefined fields for generic config clearing
Rejected: Keep using updateConfig with undefined fields | serialized requests omit the fields before the server can clear them
Confidence: high
Scope-risk: narrow
Directive: Keep credential-clearing flows on explicit unbind endpoints rather than relying on partial config merges
Tested: bun test src/server/__tests__/adapters.test.ts
Tested: cd desktop && bun run test src/stores/adapterStore.test.ts
Tested: bun run check:desktop
Tested: bun run check:server
Not-tested: bun run quality:pr is blocked by repository policy requiring allow-cli-core-change approval for the broader branch impact
This captures the pending worktree fixes before applying them to the
current local main. The changes tighten IM adapter path and credential
handling, preserve retry behavior for failed desktop notifications, and
make Azure/OpenAI provider auth and stop reasons reflect actual runtime
state.
Constraint: Worktree was detached from an older local main with pending uncommitted fixes
Rejected: Merge the detached HEAD directly | would also replay unrelated stale history
Rejected: Leave notification dedupe as fire-and-forget | failed sends consumed retry keys
Confidence: high
Scope-risk: broad
Directive: Keep adapter absolute-path matching constrained to configured work roots
Tested: git diff --check
Not-tested: full quality gate before local main integration
WeChat and DingTalk were using different pairing, attachment, and response
state paths, which made the new IM channels behave differently from Feishu
and Telegram. Align the shared pairing model, wire inbound media into the
existing attachment bridge, and map platform response capabilities to their
real APIs: WeChat block streaming plus typing, DingTalk AI Card streaming.
Constraint: WeChat iLink exposes typing and block streaming, but no editable message/card streaming API
Constraint: DingTalk streaming depends on the AI Card create/deliver/stream/finalize lifecycle
Rejected: Fake DingTalk typing with standalone markdown | it would add chat noise instead of platform state
Rejected: Auto-pair WeChat after QR login | it bypasses the shared IM pairing model
Confidence: high
Scope-risk: moderate
Directive: Keep WeChat streaming as block-send unless iLink adds editable messages; keep DingTalk streaming on AI Card APIs
Tested: bun run check:adapters; bun run check:server; cd desktop && bun run test src/stores/adapterStore.test.ts; cd desktop && bun run build; bunx tsc -p adapters/tsconfig.json --noEmit; git diff --check
Not-tested: Live WeChat and DingTalk platform smoke with real production credentials
DingTalk uses QR registration to store client credentials, then reuses the existing IM pairing and session bridge. The merged main implementation keeps the existing WeChat QR binding path intact while adding DingTalk as a peer IM platform. The default IM workdir now falls back to the local user working directory so a newly bound chat can start immediately even when recent-project history is empty.
Constraint: Local main already carries WeChat IM, so the merge keeps WeChat config, QR binding, sidecar args, and unbind behavior intact while adding DingTalk.
Rejected: Keep empty defaultProjectDir as project-picker-only | newly bound IM users can hit a dead end with no recent projects.
Confidence: high
Scope-risk: moderate
Directive: Keep IM platform unions synchronized across config, pairing, sidecar args, desktop settings, and docs.
Tested: bun test common/ dingtalk/ wechat/; bun test src/server/__tests__/adapters.test.ts; cd adapters && bunx tsc --noEmit; bun run check:policy; bun run check:server; bun run check:desktop; bun run check:adapters; bun run check:native; bun run check:docs
Not-tested: quality:pr full gate was blocked by existing local main CLI-core diff requiring allow-cli-core-change maintainer approval; live post-fix DingTalk second-message delivery was not repeated after the merge.
WeChat needs a QR-paired path instead of bot-token setup, so the adapter layer now includes the iLink protocol calls, desktop pairing UI, server-side bind/unbind APIs, and shared IM command behavior. Empty project history falls back to the user's default work directory so mobile /new works without pre-opening a desktop project.
Constraint: Tencent iLink login returns a URL that the desktop UI must render as a QR image locally
Constraint: IM adapters should keep /new, /projects, status, permission, and default workdir behavior consistent across WeChat, Feishu, and Telegram
Rejected: Require users to paste absolute project paths for first WeChat sessions | mobile onboarding should work from the default user working directory
Confidence: high
Scope-risk: moderate
Directive: Do not change WeChat polling back to overlapping intervals; getupdates is a long-poll endpoint and must remain serialized
Tested: Real WeChat QR scan, inbound /status, outbound reply, and unbind E2E
Tested: bun run check:adapters
Tested: bun run quality:pr
Not-tested: Re-scan live WeChat after the default-workdir fallback tweak; covered by adapter config tests and PR gate
Wires the new claude-sidecar adapters mode into the actual desktop UX:
* On app launch, Tauri main process now spawns the adapter sidecar right
after the server sidecar comes up. The sidecar reads ~/.claude/adapters.json
and connects whichever of Feishu / Telegram has credentials configured;
if neither does, it warns + skips + exits cleanly (treated as expected).
* When the user saves credentials in the existing AdapterSettings page,
the frontend store invokes restart_adapters_sidecar after the PUT
/api/adapters succeeds. Tauri kills the old child and spawns a new one,
which picks up the fresh config and establishes the WebSocket connection
to Feishu / Telegram immediately — no app restart needed.
End-to-end behavior is now: install app → open → configure credentials in
settings → click save → IM bot is live.
Implementation
==============
* desktop/src-tauri/capabilities/default.json: replace the stale
binaries/claude-server allowlist with binaries/claude-sidecar across
shell:allow-execute, shell:allow-spawn, plus a new shell:allow-kill
entry needed for the restart path. (P2 changed externalBin to
claude-sidecar but missed updating capabilities, which is why the prior
bundle worked at all in dev mode but would have failed in production.)
* desktop/src-tauri/src/lib.rs:
- new AdapterState that holds an Option<CommandChild>
- start_adapters_sidecar() spawns `claude-sidecar adapters --feishu --telegram`
with ADAPTER_SERVER_URL env var pointing at the dynamic server port
(converted http://→ws:// since WsBridge does `new WebSocket(url)`
directly without protocol translation)
- spawn_and_track_adapters_sidecar() handles spawn + state insertion
- stop_adapters_sidecar() kills + clears state
- new #[tauri::command] restart_adapters_sidecar that calls stop+spawn
- sidecar Terminated events are info-logged, not treated as errors,
so the credential-missing path doesn't show up as a crash
- setup() spawns the adapter sidecar after server startup completes
- RunEvent::Exit cleanup also kills adapter sidecar
* desktop/src/stores/adapterStore.ts: after every successful PUT
/api/adapters, dynamic-import @tauri-apps/api/core and call
invoke('restart_adapters_sidecar'). Wrapped in try/catch so non-Tauri
test environments fall through quietly. Triggers on every config
change (including pairing code generation, paired-user removal) by
design — keeps the rule simple and guarantees any save takes effect.
* desktop/src/pages/AdapterSettings.tsx: removed the stale "Server URL"
text input. The field defaulted to ws://127.0.0.1:3456 but the actual
server uses a dynamic port chosen at startup. Even when filled in
correctly, loadConfig() in adapters/common/config.ts gives env var
priority over file value, so this UI control had zero effect inside
the desktop app. Standalone-mode adapter users can still edit the
field directly in adapters.json if they need to.
Bundle size: unchanged at 88 MB .app / 37 MB DMG. The Rust changes
add only a few KB to the desktop main binary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use rejection sampling to ensure uniform distribution across the
safe alphabet. Values >= 232 are rejected and resampled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement IM adapters allowing users to chat with Claude Code from Telegram
and Feishu/Lark. Includes persistent session management (chatId→sessionId
mapping), project selection via /projects command, and a web UI settings page
for configuring bot tokens, allowed users, and default project directory.
Key changes:
- adapters/: Telegram and Feishu adapter scripts with shared common modules
(WsBridge, MessageBuffer, SessionStore, HttpClient, config, formatting)
- Backend: adapterService + REST API (GET/PUT /api/adapters) with secret masking
- Frontend: AdapterSettings page in Settings tab with i18n support
- DirectoryPicker: use React Portal for dropdown to fix overflow clipping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>