Skip to content

fix(dingtalk): make unauthorized hints actionable with ID and allow command - #9

Closed
echohn wants to merge 7 commits into
marswaveai:mainfrom
echohn:fix/unauthorized-hint-authorization-path
Closed

echohn wants to merge 7 commits into
marswaveai:mainfrom
echohn:fix/unauthorized-hint-authorization-path

Conversation

@echohn

@echohn echohn commented Sep 26, 2026

Copy link
Copy Markdown

Problem

When an unauthorized sender DMs the bot or @-mentions it in an unauthorized group, the plugin replies with a polite dead-end:

我是主人的工作助理,暂时不能为您处理请求。您留言的内容主人暂时看不到……

The owner then has no in-band way to authorize that chat: the conversation ID / sender ID needed for cola channel allow[-group] only shows up in host logs. (Found while testing group @-mentions — the rejection tells you nothing about how to proceed.)

Root cause

The SDK's default unauthorizedHint names the cola channel allow[-group] <pluginId> <targetId> command, but the DingTalk plugin overrode it with a static message that dropped the actionable part.

Fix

  • unauthorizedHint now includes the target ID (target.id) and the matching command for both cases:
    • group → cola channel allow-group dingtalk <groupId>
    • user → cola channel allow dingtalk <userId>
  • All 6 locales updated with the new {{groupId}} / {{userId}} placeholders.
  • New tests/unauthorized-hint.test.ts covers: group hint contains ID + allow-group command, user hint contains ID + allow command, and hints resolve through the SDK i18n layer without missing placeholders.

Testing

  • vitest run: 8 files / 101 tests pass (98 existing + 3 new)
  • tsc --noEmit: clean

Connect Cola to DingTalk via an enterprise internal application robot
using the official Stream mode (outbound WebSocket, no public callback).

- Gateway: DWClient Stream lifecycle (initial backoff loop + SDK
  auto-reconnect), TOPIC_ROBOT message parsing (direct/group branch,
  @bot prefix strip, non-text fallback), msgId-level dedup, real
  connection-state reporting (socket-open based; DingTalk's stream
  gateway does not push REGISTERED system frames)
- Outbound: access-token manager (7200s in-memory cache, refresh
  coalescing), oToMessages/groupMessages send, text/markdown,
  15000-byte UTF-8-safe msgParam guard
- Group chats opt-in (groupEnabled); non-owner group @mentions are
  annotated with sender identity and a scope constraint line
  (configurable ownerStaffId)
- Auth: SDK access gate with polite secretary-style hints, 6 locales
  (en/es/ja/ko/zh-CN/zh-TW)
- Tests: 57 unit tests (parsing, token cache, client payloads,
  dedup, status); all fixtures use fake credentials
- Verified: typecheck/build/lint/fmt/test all green; live-tested
  against a real DingTalk org (direct + group)

Closes nothing; media send/receive planned for a follow-up.
…ections

The dingtalk-stream SDK's internal auto-reconnect swallows errors, so a
half-open TCP connection (sleep/wake, proxy failover) can stay reported
as connected while no frames ever arrive — messages are lost silently
(observed live: 18+ hours unaware).

Track the last inbound frame timestamp (any downstream frame counts)
and re-establish the connection when no frame arrives within 5 minutes;
log a WARN on watchdog-triggered reconnects, clean up the timer on
stop/abort, and re-arm after each successful reconnect.
Send: upload media via the robot media upload API, then deliver via
oToMessages/groupMessages as sampleImageMsg / sampleAudio / sampleFile.
Declares outbound mediaCapabilities (image/file, 20MB DingTalk cap) so
the host validates size client-side. Video is intentionally
unsupported: sampleVideo requires a separate cover-image mediaId that
cannot be derived from a single file.

Receive: image/file messages are downloaded (downloadCode -> temp URL,
50MB cap, filename sanitization) instead of degrading to a text-only
summary.
Image+caption messages arrive as richText payloads. The media parser
only handled picture/audio/video/file, so richText picture nodes (which
carry downloadCode) were dropped: no download was attempted, delivery
text degraded to a bare '[图文]' placeholder, and the agent — never
receiving the media — fabricated a description from session memory
(observed live).

- richText parsing: walk text and picture nodes; build '[图文] <text>'
  with the extracted downloadCode; plain-text richText no longer gets
  any media marker
- delivery annotations: on success the agent is told the attachment was
  downloaded and is available; if media was expected but delivery
  failed, the agent receives an explicit bilingual instruction not to
  guess and to tell the user honestly (plus a WARN log, previously
  silent)
- tests: 95 passing (richText node cases, annotation cases, no-media
  paths, no regressions)
Image messages carry no fileName, so the raw downloadCode (a very long
base64 variant) was used as the file name and every media download
failed with ENAMETOOLONG (observed live).

When no real fileName is present, derive a short deterministic name
from a sha256 of the downloadCode plus an extension inferred from the
media kind; basename length is also capped at 120 characters in the
client layer regardless of source.
The dingtalk.com favicon renders poorly as a plugin icon. Ship the
official app icon (extracted from the DingTalk macOS client, 256px PNG)
in the plugin's assets and point iconUrl at the repository asset URL,
consistent with the host's https-only iconUrl validation.
…ommand

The previous unauthorizedHint override replaced the host's default guidance
(which names the 'cola channel allow[-group]' command) with a polite dead-end
reply. The owner had no in-band way to learn the ID needed for authorization —
it only appeared in host logs.

The hint now always includes the target ID (group or user) and the matching
command, in all locales. Covered by tests/unauthorized-hint.test.ts.
@echohn

echohn commented Sep 26, 2026

Copy link
Copy Markdown
Author

Closing in favor of #8 — the DingTalk plugin itself was already submitted there and this PR unintentionally duplicated it (it was branched from local main, which carries the plugin, not from upstream main).

Both fixes are now pushed onto #8's head branch (feat/dingtalk-channel):

  • actionable unauthorized hints (group/user ID + cola channel allow[-group] command, all locales, tested)
  • group owner matched by senderStaffId instead of the LWCP union senderId (owners were tagged as non-owners in every group message)

@echohn echohn closed this Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant