File size: 4,315 Bytes
f778c12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// Runtime send adapter used by CLI send commands for channel plugins.
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
import type { ChannelId } from "../../channels/plugins/types.public.js";
import { getRuntimeConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { PlatformMessageNotDispatchedError } from "../../infra/outbound/deliver-types.js";
import type { OutboundDeliveryFormattingOptions } from "../../infra/outbound/formatting.js";
import type { OutboundMediaAccess } from "../../media/load-options.js";

type RuntimeSendOpts = {
  cfg?: OpenClawConfig;
  blocks?: unknown;
  mediaUrl?: string;
  mediaAccess?: OutboundMediaAccess;
  mediaLocalRoots?: readonly string[];
  mediaReadFile?: (filePath: string) => Promise<Buffer>;
  accountId?: string;
  threadId?: string | number | null;
  messageThreadId?: string | number;
  threadTs?: string | number;
  replyToId?: string | number | null;
  replyToMessageId?: string | number;
  silent?: boolean;
  forceDocument?: boolean;
  formatting?: OutboundDeliveryFormattingOptions;
  gifPlayback?: boolean;
  gatewayClientScopes?: readonly string[];
  /** @internal Opaque durable intent id for provider-side reconciliation. */
  deliveryQueueId?: string;
  /** @internal Stable provider-send index within one payload. */
  deliveryPartIndex?: number;
  /** @internal Exact provider-send count for one payload. */
  deliveryPartCount?: number;
  /** @internal Refresh durable timing before recipient-visible or finalizing platform I/O. */
  onPlatformSendDispatch?: () => Promise<void>;
  textMode?: "markdown" | "html";
};

function resolveRuntimeThreadId(opts: RuntimeSendOpts): string | number | undefined {
  return opts.messageThreadId ?? opts.threadId ?? opts.threadTs ?? undefined;
}

function resolveRuntimeReplyToId(opts: RuntimeSendOpts): string | undefined {
  const raw = opts.replyToMessageId ?? opts.replyToId;
  return raw == null ? undefined : normalizeOptionalString(String(raw));
}

/** Create a send runtime that dispatches text, media, or rich blocks through a channel plugin. */
export function createChannelOutboundRuntimeSend(params: {
  channelId: ChannelId;
  unavailableMessage: string;
}) {
  return {
    sendMessage: async (to: string, text: string, opts: RuntimeSendOpts = {}) => {
      const outbound = await loadChannelOutboundAdapter(params.channelId);
      const threadId = resolveRuntimeThreadId(opts);
      const replyToId = resolveRuntimeReplyToId(opts);
      // Build context lazily so text/media/block branches share identical delivery metadata.
      const buildContext = () => ({
        cfg: opts.cfg ?? getRuntimeConfig(),
        to,
        text,
        mediaUrl: opts.mediaUrl,
        mediaAccess: opts.mediaAccess,
        mediaLocalRoots: opts.mediaLocalRoots,
        mediaReadFile: opts.mediaReadFile,
        accountId: opts.accountId,
        threadId,
        replyToId,
        silent: opts.silent,
        forceDocument: opts.forceDocument,
        formatting:
          opts.formatting ?? (opts.textMode === "html" ? { parseMode: "HTML" } : undefined),
        gifPlayback: opts.gifPlayback,
        gatewayClientScopes: opts.gatewayClientScopes,
        deliveryQueueId: opts.deliveryQueueId,
        deliveryPartIndex: opts.deliveryPartIndex,
        deliveryPartCount: opts.deliveryPartCount,
        onPlatformSendDispatch: opts.onPlatformSendDispatch,
      });
      const hasMedia = Boolean(opts.mediaUrl);
      if (opts.blocks && outbound?.sendPayload) {
        return await outbound.sendPayload({
          ...buildContext(),
          payload: {
            text,
            channelData: {
              [params.channelId]: {
                blocks: opts.blocks,
              },
            },
          },
        });
      }
      if (hasMedia && outbound?.sendMedia) {
        return await outbound.sendMedia(buildContext());
      }
      if (!outbound?.sendText) {
        const cause = new Error(params.unavailableMessage);
        throw new PlatformMessageNotDispatchedError(params.unavailableMessage, { cause });
      }
      return await outbound.sendText(buildContext());
    },
  };
}