Mirrors the feishu one-click flow: scan a QR with the DingTalk app,
approve, and the bot's client_id / client_secret get auto-filled instead
of forcing the user through the open-dev console. Saves about seven
manual steps per channel setup.
Backend
- Bump dingtalk-stream from 1.3.5 to 1.3.12. Diff against the classes we
depend on (OpenDingTalkStreamClient, ChatbotMessage, MessageContent,
GenericEventListener) is empty — pure point-release bumps, no API churn.
- New DingTalkAppRegistrationService: synchronously runs init + begin
against /app/registration/{init,begin} on oapi.dingtalk.com to obtain
the device_code and verification URL, then spawns a daemon worker that
polls /app/registration/poll every 5s until SUCCESS / FAIL / EXPIRED is
returned. Sessions evict after 7 minutes, worker has a 6-minute hard
runtime cap, transient HTTP errors do not terminate the loop. Same
shape as the feishu service, but written from scratch because the
dingtalk-stream SDK doesn't wrap this OAuth device flow.
- Two new endpoints under /api/v1/channels/webhook:
POST /dingtalk/register/begin returns session_id;
GET /dingtalk/register/status returns status + qrcode_img (data URI
PNG, ZXing-encoded from the verification URL, matching the feishu and
weixin flows). Status surface: waiting / confirmed / expired / denied.
Frontend
- channelApi.dingtalkRegisterBegin / dingtalkRegisterStatus.
- New useDingTalkAppRegister composable, structurally identical to
useFeishuAppRegister minus the domain argument. Stops polling on
terminal status, fires onConfirmed with {clientId, clientSecret}.
- ChannelEditModal: dingtalk-register-card rendered when channelType is
dingtalk, scoped DingTalk blue (#1f79ff) to differentiate from feishu's
indigo. onConfirmed writes channelConfig.client_id / client_secret so
the existing form fields update reactively.
- i18n: channels.dingtalkRegister.* keys for title / hint / button states
/ scan / confirmed / expired / denied / startFailed.
Saves the user the entire 'go to the open platform -> create an enterprise
app -> copy App ID and Secret' detour. Click a button in the channel form,
scan the QR code, confirm authorization, credentials are auto-filled.
Backend
- Bump com.larksuite.oapi:oapi-sdk from 2.5.3 to 2.6.1, which adds the
scene/registration package wrapping the device-code flow.
- New FeishuAppRegistrationService: each begin() creates a sessionId,
spawns a worker thread, runs the SDK's blocking RegisterApp.register
with onQRCode and onStatusChange wired into a per-session state machine
(PENDING -> WAITING -> CONFIRMED / EXPIRED / DENIED / ERROR). The
session caches the QR data URI so ZXing only encodes once per attempt.
Sessions evict after 5 minutes so closed browsers don't leak the map.
- Two new webhook endpoints under /api/v1/channels/webhook/feishu:
POST /register/begin returns session_id, GET /register/status returns
status + qrcode_img (data URI base64 PNG, ZXing-encoded from the SDK's
verification URL — the raw URL would render as a broken image, so the
encoding step matches the WeCom flow).
- SDK detail caught the hard way: don't pass .domain() or .larkDomain().
The SDK defaults are accounts.feishu.cn / accounts.larksuite.com (the
registration endpoints). open.feishu.cn is the open-API endpoint, a
completely different service. Passing the wrong one makes the SDK parse
HTML as JSON and emit invalid_response.
Frontend
- channelApi: feishuRegisterBegin / feishuRegisterStatus.
- New useFeishuAppRegister composable: state machine that begins the
session, polls status every 2s, prefers qrcode_img over qrcode_url for
the <img> src, stops on terminal status, fires onConfirmed with
{appId, appSecret}.
- ChannelEditModal: a new feishu-register-card above the wecom one. The
composable's onConfirmed writes channelConfig.app_id / app_secret, so
the existing form fields update reactively.
- i18n: channels.feishuRegister.* keys for title / hint / button states /
scan / confirmed / expired / denied / error.
Backend (FeishuChannelAdapter):
- Default connection_mode flips webhook -> websocket on doStart and doReconnect.
- Stale event filter: drop events whose message.create_time is older than
stale_event_threshold_seconds (default 30s) so SDK reconnect replays do not
re-trigger the agent.
- Silent disconnect watchdog runs every 60s; if no events arrive for
silent_disconnect_threshold_seconds (default 1800s) after the first event,
call onDisconnected to force a reconnect cycle. Setting the threshold to 0
disables the watchdog. The watchdog is scheduled before wsClient.start() on
the bring-up path because that call blocks indefinitely.
- Quoted message context: when a reply has parent_id set, fetch the parent
via GET /open-apis/im/v1/messages/{id}, summarize per msg_type (text / post
first paragraph / [Image]/[File]/[Audio]/[Video] placeholders, capped at
200 chars), and prepend [Quoted: ...] to both content text and the first
content part. LRU-cached (200) per message_id.
- AbstractChannelAdapter gains getConfigLong helper for numeric config keys.
Frontend:
- types/index.ts feishu fields: default connection_mode is websocket; the
recommended option moves to the top; verification_token and encrypt_key
get showIf so they only render in webhook mode; new enable_quoted_context
switch (default on) exposes the quoted-message feature.
- ChannelEditModal builds a feishu-specific WEBHOOK_GUIDES path that picks
webhookStep vs websocketStep based on connection_mode, so users only see
steps for the mode they're using.
- i18n: split feishu.step3/step4 into webhookStep/websocketStep, rename
step5 to permissionStep. Channel type labels in zh-CN drop bilingual
prefix (e.g. 'Feishu / Lark (飞书)' -> '飞书').
Migrations:
- V52 was a no-op the first time it ran (matched compact JSON only) and
Flyway refused to re-run after the SQL was fixed. V52 is documented as a
no-op; V53 carries the actual UPDATE with REPLACE covering both compact
and pretty-printed JSON, and an idempotent WHERE for rows already on
websocket. h2 and mysql variants stay in lockstep.
- Extract create/edit modal into ChannelEditModal.vue (defineAsyncComponent),
shrinking Channels.vue from 1438 to 370 lines and dropping ~30KB from the
initial route chunk.
- Move side-effect logic into composables: useWeixinQrcodePoll (QR + 2s status
poll, auto-cleanup) and useWecomBotAuth (lazy SDK script with module-level
promise dedupe). Pure config-JSON helpers move to utils/channelConfigJson.ts.
- Switch i18n locales from static imports to dynamic import keyed by current
locale; applyLocale becomes async to avoid first-render flicker.
- /channels route opts into keep-alive (meta.keepAlive=true). Channels.vue
pauses status polling in onDeactivated and resumes in onActivated, with an
isActive guard to prevent late-resolving timers from leaking after navigation.
- Initial load goes from serial 3-RTT to Promise.all + 4-card el-skeleton.