import type { MessengerAdapter, MessageContext } from '../../../core/types.js'; import type { DingTalkConfig } from './types.js'; /** Result of flattening a DingTalk richText payload. */ export interface RichTextFlatten { /** Joined + trimmed text from all text segments. */ text: string; textPartCount: number; pictureCount: number; /** Which candidate location the segment array came from (audit). */ source: string; listLen: number; } /** * Flatten a DingTalk `msgtype:'richText'` payload into plain text. * * The segment array has shipped in several shapes across SDK versions * and chat contexts. v1.2.104 confirmed from a real Stream-SDK 1:1 * payload that the live shape is `msg.content.richText` AS AN ARRAY * DIRECTLY (segments like `{ text: "…" }`, no `type` field), NOT nested * under `.msgList`. We probe array-direct shapes first, then the older * `.msgList`-nested shapes as fallbacks. * * Pure + exported so the multi-shape probe is unit-testable without * standing up the whole adapter. */ export declare function flattenDingtalkRichText(msg: Record): RichTextFlatten; export declare const ACK_GLYPHS: { readonly emoji: { readonly received: "⏳"; readonly working: "🤖"; readonly done: "✅"; readonly failed: "❌"; }; readonly kaomoji: { readonly received: "(´・ω・`)"; readonly working: "(。•̀ᴗ-)"; readonly done: "(•̀ᴗ•́)"; readonly failed: "(╥﹏╥)"; }; }; /** * Batch 4 (#3) — pure account-resolution helper. Merge accounts dict + * top-level credentials into the set of (accountId → DingTalkConfig) * pairs the adapter should start. Disabled accounts are filtered out. * Each entry's policy / ack-reaction fields fall back to the top-level * values when the account-block doesn't override them. * * Single-account legacy configs (top-level credentials, no `accounts`) * resolve to `{'default': }` so the adapter sees the same code * path regardless of multi-bot vs single-bot deployments. */ export declare function planDingTalkAccounts(cfg: DingTalkConfig): Map; /** * Batch 1 (#4) — pure access-policy evaluator. Decoupled from the adapter * class so unit tests can hit it without the SDK. Returns: * - allowed: should the message be dispatched to the agent? * - pairedNewSender: when defined, the caller should persist this sender * into allowFrom because the policy is `pairing` and this is the first * inbound DM. */ export declare function evalAccessPolicy(cfg: Pick, msg: { isGroup: boolean; senderStaffId: string; }): { allowed: boolean; pairedNewSender?: string; }; export declare class DingTalkAdapter implements MessengerAdapter { readonly name = "dingtalk"; private clients; private threadOwner; private effectiveConfigs; private config; private messageHandler?; private seenMessages; start(): Promise; /** Internal wrapper that defers to the pure {@link planDingTalkAccounts} * helper exported from this module so unit tests can exercise account * resolution without spinning up the SDK. */ private planAccounts; stop(): Promise; onMessage(handler: (ctx: MessageContext) => Promise): void; sendMessage(threadId: string, text: string): Promise; /** Batch 4 (#3) — pick the client that owns this thread, falling back * to 'default' if we never saw the thread before (manual notify path). */ private clientForThread; /** Returns true on duplicate; otherwise records and prunes. Same shape * as the Feishu adapter's dedup helper. */ private isDuplicate; /** Build the threadId we expose to the rest of agim. 1:1 routes by * sender staffId; group chats route by openConversationId so any * member triggers a shared session in that group. */ private threadIdFor; private handleInbound; /** Image inbound: exchange downloadCode → CDN bytes → save under * ~/.agim/media/dingtalk//, then dispatch a Message whose * text contains an `[图片附件:]` marker. claude-code's Read * tool already understands that marker — it'll fetch the file off * disk and pass it to the multimodal model the user is talking to. * * On download failure we still dispatch (with a `[图片附件下载失败]` * marker) so the user's interaction isn't silently dropped. */ private handleImageInbound; /** Voice inbound: download AMR (or whatever the CDN serves) → transcribe * via the shared whisper provider → dispatch the transcript as the * user's text. The saved source file path is appended so the agent can * reference it (replay, archive, etc.) — same shape as the Telegram * voice path so claude-code's prompts work uniformly. * * Surfaces failures inline so the user gets feedback: * - no downloadCode → `[语音附件无下载凭据]` * - download failed → `[语音附件下载失败:…]` * - no provider set → `[语音附件未转写(未配置 OPENAI_API_KEY 或 AGIM_WHISPERCPP_BIN):path]` * - transcribe threw → `[语音转写失败(provider: reason)\n源文件:path]` */ private handleVoiceInbound; /** Build a filesystem-safe sub-path under MEDIA_ROOT/dingtalk/. * Uses senderStaffId for 1:1 and openConversationId for groups; both * are alphanumeric+`=` from DingTalk so we sanitise the `=` (and any * other path-unsafe chars) to `_` to avoid surprises on Windows ports * or shell quoting. */ private subPathFor; /** DingTalk msgIds are base64-shaped (`msgABC/DEF+xyz==`) and contain * `/` + `+` + `=` — all reserved for path or shell quoting. Replace * every non-[A-Za-z0-9_-] char with `_` so the resulting filename * passes saveDingTalkMedia's safety check and is portable. */ private safeFilenameStem; /** Batch 1 (#4) + Batch 4 (#3) — per-account policy gate. Uses the * effective config for `accountId` so each bot can have its own * dmPolicy / allowlist. */ private passesAccessPolicy; /** Batch 1 (#7) + Batch 4 (#3) — per-account ack glyph. Reads the * ackReaction mode from the account that owns the inbound thread. */ private sendAckReaction; /** Shared text-dispatch path used by text / markdown msgtypes. */ private dispatchText; } export { tryExtractLatLng, type MapUrlSource } from './link-coords.js'; export declare const dingtalkAdapter: DingTalkAdapter; //# sourceMappingURL=dingtalk-adapter.d.ts.map