| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- /**
- * ChannelOutboundAdapter — used by OpenClaw core whenever it wants to
- * send a message through wechat-agent outside the main inbound/reply
- * pipeline (e.g. agent-initiated pings, scheduled broadcasts, tool
- * results that return to the channel, approval notifications).
- *
- * For messages generated in response to an inbound (the common path),
- * the delivery actually happens inside monitor.ts's `deliver` callback,
- * which bypasses this adapter. Both paths call the same client.sendText
- * under the hood.
- */
- import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-contract";
- import { getRuntime } from "./runtime.js";
- import { createClient } from "./client.js";
- import { CHANNEL_ID, getChannelCfg, resolveApiBase } from "./config.js";
- /** Strip any provider prefix and return the bare wxid. */
- function toBareWxid(target: string): string {
- return target
- .replace(/^(?:wxid|wechat-agent|wechat|weixin|user):/i, "")
- .trim();
- }
- export const outbound: ChannelOutboundAdapter = {
- deliveryMode: "direct",
- // Core helpers for text chunking. Using runtime lookup (vs. importing the
- // utility) keeps us forward-compatible when the SDK changes chunking impls.
- chunker: (text: string, limit: number) =>
- getRuntime().channel.text.chunkMarkdownText(text, limit),
- chunkerMode: "markdown",
- textChunkLimit: 4000,
- sendText: async (ctx) => {
- const channelCfg = getChannelCfg(ctx.cfg);
- const apiBase = resolveApiBase(channelCfg);
- const bare = toBareWxid(ctx.to);
- if (!bare) {
- throw new Error(
- `[${CHANNEL_ID}] invalid target "${ctx.to}" \u2014 expected wxid:<wxid>`
- );
- }
- const client = createClient(apiBase);
- const result = await client.sendText({
- toWxid: bare,
- content: ctx.text,
- });
- if (!result.ok) {
- throw new Error(
- `[${CHANNEL_ID}] send-text failed: ${result.msg ?? `ret=${String(result.ret)}`}`
- );
- }
- return {
- channel: CHANNEL_ID,
- messageId: result.syntheticMessageId,
- conversationId: bare,
- };
- },
- sendMedia: async (ctx) => {
- // MVP: the wechat-agent backend we target does not expose a stable media
- // endpoint across wcf/xbot builds. Fall back to sending the caption as
- // text so the user still gets context. Proper image/voice/file support
- // will land in v0.2 once we fingerprint the backend build.
- const channelCfg = getChannelCfg(ctx.cfg);
- const apiBase = resolveApiBase(channelCfg);
- const bare = toBareWxid(ctx.to);
- if (!bare) {
- throw new Error(
- `[${CHANNEL_ID}] invalid target "${ctx.to}" \u2014 expected wxid:<wxid>`
- );
- }
- const caption =
- ctx.text?.trim() ||
- "[media message \u2014 wechat-agent v0.1 does not yet support media outbound]";
- const client = createClient(apiBase);
- const result = await client.sendText({
- toWxid: bare,
- content: caption,
- });
- if (!result.ok) {
- throw new Error(
- `[${CHANNEL_ID}] send-media(fallback-to-text) failed: ${result.msg ?? `ret=${String(result.ret)}`}`
- );
- }
- return {
- channel: CHANNEL_ID,
- messageId: result.syntheticMessageId,
- conversationId: bare,
- meta: { mediaFallback: "caption-only" },
- };
- },
- };
|