mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
refactor: migrate bundled plugins to message lifecycle
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user