refactor: migrate bundled plugins to message lifecycle

This commit is contained in:
Peter Steinberger
2026-05-06 01:40:53 +01:00
parent 2ead1502c9
commit 05eda57b3c
223 changed files with 8568 additions and 1354 deletions

View File

@@ -33,7 +33,7 @@ export {
} from "openclaw/plugin-sdk/channel-status";
export { buildAgentMediaPayload } from "openclaw/plugin-sdk/agent-media-payload";
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
export { createReplyPrefixContext } from "openclaw/plugin-sdk/channel-reply-pipeline";
export { createReplyPrefixContext } from "openclaw/plugin-sdk/channel-message";
export {
evaluateSupplementalContextVisibility,
filterSupplementalContextItems,

View File

@@ -9,6 +9,11 @@ import type {
ChannelMessageToolDiscovery,
} from "openclaw/plugin-sdk/channel-contract";
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
import {
defineChannelMessageAdapter,
type ChannelMessageSendResult,
type MessageReceiptPartKind,
} from "openclaw/plugin-sdk/channel-message";
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
import {
createAllowlistProviderGroupPolicyWarningCollector,
@@ -66,6 +71,7 @@ import { messageActionTargetAliases } from "./message-action-contract.js";
import { resolveFeishuGroupToolPolicy } from "./policy.js";
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
import { collectFeishuSecurityAuditFindings } from "./security-audit.js";
import { createFeishuSendReceipt } from "./send-result.js";
import { resolveFeishuSessionConversation } from "./session-conversation.js";
import { resolveFeishuOutboundSessionRoute } from "./session-route.js";
import { feishuSetupAdapter } from "./setup-core.js";
@@ -131,6 +137,51 @@ const loadFeishuChannelRuntime = createLazyRuntimeNamedExport(
"feishuChannelRuntime",
);
function toFeishuMessageSendResult(
result: { messageId?: string; chatId?: string; receipt?: ChannelMessageSendResult["receipt"] },
kind: MessageReceiptPartKind,
): ChannelMessageSendResult {
const receipt =
result.receipt ??
createFeishuSendReceipt({
messageId: result.messageId,
chatId: result.chatId ?? "",
kind,
});
return {
messageId: result.messageId || receipt.primaryPlatformMessageId,
receipt,
};
}
const feishuMessageAdapter = defineChannelMessageAdapter({
id: "feishu",
durableFinal: {
capabilities: {
text: true,
media: true,
},
},
send: {
text: async (ctx) => {
const runtime = await loadFeishuChannelRuntime();
const sendText = runtime.feishuOutbound.sendText;
if (!sendText) {
throw new Error("Feishu text sending is not available.");
}
return toFeishuMessageSendResult(await sendText(ctx), "text");
},
media: async (ctx) => {
const runtime = await loadFeishuChannelRuntime();
const sendMedia = runtime.feishuOutbound.sendMedia;
if (!sendMedia) {
throw new Error("Feishu media sending is not available.");
}
return toFeishuMessageSendResult(await sendMedia(ctx), "media");
},
},
});
function buildFeishuPresentationCard(params: {
presentation: NonNullable<ReturnType<typeof normalizeMessagePresentation>>;
fallbackText?: string;
@@ -1255,6 +1306,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
});
},
},
message: feishuMessageAdapter,
},
security: {
collectWarnings: projectConfigAccountIdWarningCollector<{

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { Readable } from "node:stream";
import type * as Lark from "@larksuiteoapi/node-sdk";
import type { MessageReceipt } from "openclaw/plugin-sdk/channel-message";
import { mediaKindFromMime } from "openclaw/plugin-sdk/media-mime";
import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS, runFfmpeg } from "openclaw/plugin-sdk/media-runtime";
import {
@@ -15,7 +16,11 @@ import { createFeishuClient } from "./client.js";
import { requestFeishuApi } from "./comment-shared.js";
import { normalizeFeishuExternalKey } from "./external-keys.js";
import { getFeishuRuntime } from "./runtime.js";
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
import {
assertFeishuMessageApiSuccess,
resolveFeishuReceiptKind,
toFeishuSendResult,
} from "./send-result.js";
import { resolveFeishuSendTarget } from "./send-target.js";
const FEISHU_MEDIA_HTTP_TIMEOUT_MS = 120_000;
@@ -399,6 +404,7 @@ export type UploadFileResult = {
export type SendMediaResult = {
messageId: string;
chatId: string;
receipt: MessageReceipt;
voiceIntentDegradedToFile?: boolean;
};
@@ -532,7 +538,7 @@ export async function sendImageFeishu(params: {
{ includeNestedErrorLogId: true },
);
assertFeishuMessageApiSuccess(response, "Feishu image reply failed");
return toFeishuSendResult(response, receiveId);
return toFeishuSendResult(response, receiveId, "media");
}
const response = await requestFeishuApi(
@@ -549,7 +555,7 @@ export async function sendImageFeishu(params: {
{ includeNestedErrorLogId: true },
);
assertFeishuMessageApiSuccess(response, "Feishu image send failed");
return toFeishuSendResult(response, receiveId);
return toFeishuSendResult(response, receiveId, "media");
}
/**
@@ -589,7 +595,7 @@ export async function sendFileFeishu(params: {
{ includeNestedErrorLogId: true },
);
assertFeishuMessageApiSuccess(response, "Feishu file reply failed");
return toFeishuSendResult(response, receiveId);
return toFeishuSendResult(response, receiveId, resolveFeishuReceiptKind(msgType));
}
const response = await requestFeishuApi(
@@ -606,7 +612,7 @@ export async function sendFileFeishu(params: {
{ includeNestedErrorLogId: true },
);
assertFeishuMessageApiSuccess(response, "Feishu file send failed");
return toFeishuSendResult(response, receiveId);
return toFeishuSendResult(response, receiveId, resolveFeishuReceiptKind(msgType));
}
/**

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { verifyChannelMessageAdapterCapabilityProofs } from "openclaw/plugin-sdk/channel-message";
import type { MessagePresentation } from "openclaw/plugin-sdk/interactive-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig } from "../runtime-api.js";
@@ -23,6 +24,8 @@ vi.mock("./media.js", () => ({
}));
vi.mock("./send.js", () => ({
editMessageFeishu: vi.fn(),
getMessageFeishu: vi.fn(),
sendCardFeishu: sendCardFeishuMock,
sendMessageFeishu: sendMessageFeishuMock,
sendMarkdownCardFeishu: sendMarkdownCardFeishuMock,
@@ -69,7 +72,9 @@ vi.mock("./comment-reaction.js", () => ({
cleanupAmbientCommentTypingReaction: cleanupAmbientCommentTypingReactionMock,
}));
import { feishuPlugin } from "./channel.js";
import { feishuOutbound } from "./outbound.js";
import { createFeishuSendReceipt } from "./send-result.js";
const sendText = feishuOutbound.sendText!;
const emptyConfig: ClawdbotConfig = {};
const cardRenderConfig: ClawdbotConfig = {
@@ -99,6 +104,74 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
resetOutboundMocks();
});
it("declares message adapter durable text and media with receipt proofs", async () => {
sendMessageFeishuMock.mockResolvedValue({
messageId: "feishu-text-1",
chatId: "chat-1",
receipt: createFeishuSendReceipt({
messageId: "feishu-text-1",
chatId: "chat-1",
kind: "text",
}),
});
sendMediaFeishuMock.mockResolvedValue({
messageId: "feishu-media-1",
chatId: "chat-1",
receipt: createFeishuSendReceipt({
messageId: "feishu-media-1",
chatId: "chat-1",
kind: "media",
}),
});
await expect(
verifyChannelMessageAdapterCapabilityProofs({
adapterName: "feishu",
adapter: feishuPlugin.message!,
proofs: {
text: async () => {
const result = await feishuPlugin.message?.send?.text?.({
cfg: emptyConfig,
to: "chat:chat-1",
text: "hello",
accountId: "default",
});
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
to: "chat:chat-1",
text: "hello",
accountId: "default",
}),
);
expect(result?.receipt.platformMessageIds).toEqual(["feishu-text-1"]);
},
media: async () => {
const result = await feishuPlugin.message?.send?.media?.({
cfg: emptyConfig,
to: "chat:chat-1",
text: "",
mediaUrl: "https://example.com/image.png",
accountId: "default",
});
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
to: "chat:chat-1",
mediaUrl: "https://example.com/image.png",
accountId: "default",
}),
);
expect(result?.receipt.platformMessageIds).toEqual(["feishu-media-1"]);
},
},
}),
).resolves.toEqual(
expect.arrayContaining([
{ capability: "text", status: "verified" },
{ capability: "media", status: "verified" },
]),
);
});
it("chunks outbound text without requiring Feishu runtime initialization", () => {
const chunker = feishuOutbound.chunker;
if (!chunker) {

View File

@@ -1,5 +1,5 @@
import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";
import {
formatChannelProgressDraftLineForEntry,
isChannelProgressDraftWorkToolName,
@@ -154,7 +154,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
const prefixContext = createReplyPrefixContext({ cfg, agentId });
let typingState: TypingIndicatorState | null = null;
const { typingCallbacks } = createChannelReplyPipeline({
const { typingCallbacks } = createChannelMessageReplyPipeline({
cfg,
agentId,
channel: "feishu",

View File

@@ -1,3 +1,9 @@
import {
createMessageReceiptFromOutboundResults,
type MessageReceipt,
type MessageReceiptPartKind,
} from "openclaw/plugin-sdk/channel-message";
type FeishuMessageApiResponse = {
code?: number;
msg?: string;
@@ -6,6 +12,47 @@ type FeishuMessageApiResponse = {
};
};
export function resolveFeishuReceiptKind(msgType?: string): MessageReceiptPartKind {
switch (msgType) {
case "audio":
return "voice";
case "image":
case "media":
case "file":
return "media";
case "interactive":
return "card";
case "post":
case "text":
return "text";
default:
return "unknown";
}
}
export function createFeishuSendReceipt(params: {
messageId?: string;
chatId: string;
kind?: MessageReceiptPartKind;
}): MessageReceipt {
const messageId = params.messageId?.trim();
const chatId = params.chatId.trim();
return createMessageReceiptFromOutboundResults({
results: messageId
? [
{
channel: "feishu",
messageId,
chatId,
conversationId: chatId,
},
]
: [],
...(chatId ? { threadId: chatId } : {}),
kind: params.kind ?? "unknown",
});
}
export function assertFeishuMessageApiSuccess(
response: FeishuMessageApiResponse,
errorPrefix: string,
@@ -18,12 +65,16 @@ export function assertFeishuMessageApiSuccess(
export function toFeishuSendResult(
response: FeishuMessageApiResponse,
chatId: string,
kind?: MessageReceiptPartKind,
): {
messageId: string;
chatId: string;
receipt: MessageReceipt;
} {
const messageId = response.data?.message_id ?? "unknown";
return {
messageId: response.data?.message_id ?? "unknown",
messageId,
chatId,
receipt: createFeishuSendReceipt({ messageId, chatId, kind }),
};
}

View File

@@ -128,7 +128,8 @@ describe("getMessageFeishu", () => {
channel: "feishu",
});
expect(mockConvertMarkdownTables).toHaveBeenCalledWith("hello", "preserve");
expect(result).toEqual({ messageId: "om_send", chatId: "oc_send" });
expect(result).toMatchObject({ messageId: "om_send", chatId: "oc_send" });
expect(result.receipt.primaryPlatformMessageId).toBe("om_send");
});
it("extracts text content from interactive card elements", async () => {

View File

@@ -11,7 +11,11 @@ import { createFeishuApiError, requestFeishuApi } from "./comment-shared.js";
import type { MentionTarget } from "./mention-target.types.js";
import { buildMentionedCardContent, buildMentionedMessage } from "./mention.js";
import { parsePostContent } from "./post.js";
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
import {
assertFeishuMessageApiSuccess,
resolveFeishuReceiptKind,
toFeishuSendResult,
} from "./send-result.js";
import { resolveFeishuSendTarget } from "./send-target.js";
import type { FeishuChatType, FeishuMessageInfo, FeishuSendResult } from "./types.js";
@@ -132,7 +136,7 @@ async function sendFallbackDirect(
{ includeNestedErrorLogId: true },
);
assertFeishuMessageApiSuccess(response, errorPrefix);
return toFeishuSendResult(response, params.receiveId);
return toFeishuSendResult(response, params.receiveId, resolveFeishuReceiptKind(params.msgType));
}
async function sendReplyOrFallbackDirect(
@@ -188,7 +192,11 @@ async function sendReplyOrFallbackDirect(
return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
}
assertFeishuMessageApiSuccess(response, params.replyErrorPrefix);
return toFeishuSendResult(response, params.directParams.receiveId);
return toFeishuSendResult(
response,
params.directParams.receiveId,
resolveFeishuReceiptKind(params.msgType),
);
}
function normalizeCardTemplateVariable(value: unknown): string | undefined {

View File

@@ -1,3 +1,4 @@
import type { MessageReceipt } from "openclaw/plugin-sdk/channel-message";
import type { BaseProbeResult } from "openclaw/plugin-sdk/core";
import type { FeishuConfigSchema, FeishuAccountConfigSchema, z } from "./config-schema.js";
import type { MentionTarget } from "./mention-target.types.js";
@@ -53,6 +54,7 @@ export type FeishuMessageContext = {
export type FeishuSendResult = {
messageId: string;
chatId: string;
receipt: MessageReceipt;
};
export type FeishuChatType = "p2p" | "group" | "topic_group" | "private";