diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index a20226d35441..0bec19087892 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -8bb175b965c6fab4512f110cd584ba601074885fb49375f2dd18e832882d61a5 plugin-sdk-api-baseline.json -5a1548ecb0cf8dffb67591b41b9e1a3c04ee7fe0cb22ac2f3bcfa24e319f4f47 plugin-sdk-api-baseline.jsonl +598c5f1bb39362de6b11b4cd3403551f99fb7c3bbf85d5e3dcf1b1408a8cec24 plugin-sdk-api-baseline.json +d87a296d6d449c2e01b63eeb1e1743d1f11dcdf4091520328c54798ac18f149f plugin-sdk-api-baseline.jsonl diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 1eddc02694cc..888c5e364252 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -791,6 +791,18 @@ major release. Every entry maps the old API to its canonical replacement. + + **Old**: return `{ ok, messageId, error }` through + `ChannelSendRawResult` and normalize it with + `createRawChannelSendResultAdapter(...)`. + + **New**: return `OutboundDeliveryResult` fields and attach the channel with + `createAttachedChannelResultAdapter(...)`. Failed sends should throw instead + of returning an error string. The raw result type remains available until + the next plugin-SDK major release. + + + Two legacy type aliases still exported from `src/plugins/runtime/types.ts`: diff --git a/extensions/telegram/src/inline-buttons.test.ts b/extensions/telegram/src/inline-buttons.test.ts index 0dc0a82baced..dd446c4ce832 100644 --- a/extensions/telegram/src/inline-buttons.test.ts +++ b/extensions/telegram/src/inline-buttons.test.ts @@ -1,5 +1,5 @@ // Telegram tests cover inline buttons plugin behavior. -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { describe, expect, it } from "vitest"; import { buildTelegramInteractiveButtons } from "./button-types.js"; import { describeTelegramInteractiveButtonBehavior } from "./button-types.test-helpers.js"; diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 330b8bf8ebf7..e8e4b38960ea 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -14,15 +14,18 @@ import { createChatChannelPlugin, type ChannelPlugin, } from "openclaw/plugin-sdk/channel-core"; -import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound"; +import { + defineChannelMessageAdapter, + type MessageReceipt, +} from "openclaw/plugin-sdk/channel-outbound"; import { buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyWarning, createOpenProviderGroupPolicyWarningCollector, } from "openclaw/plugin-sdk/channel-policy"; import { + createAttachedChannelResultAdapter, createEmptyChannelResult, - createRawChannelSendResultAdapter, } from "openclaw/plugin-sdk/channel-send-result"; import { buildTokenChannelStatusSummary } from "openclaw/plugin-sdk/channel-status"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; @@ -82,27 +85,32 @@ const zaloSetupWizard = createZaloSetupWizardProxy( ); const zaloTextChunkLimit = 2000; -const zaloRawSendResultAdapter = createRawChannelSendResultAdapter({ +async function sendZaloDelivery(ctx: { + cfg: OpenClawConfig; + to: string; + text: string; + accountId?: string | null; + mediaUrl?: string; +}): Promise<{ messageId: string; receipt: MessageReceipt }> { + const result = await ( + await loadZaloChannelRuntime() + ).sendZaloText({ + to: ctx.to, + text: ctx.text, + accountId: ctx.accountId ?? undefined, + mediaUrl: ctx.mediaUrl, + cfg: ctx.cfg, + }); + if (!result.ok) { + throw new Error(result.error ?? `Failed to send Zalo ${ctx.mediaUrl ? "media" : "message"}`); + } + return { messageId: result.messageId ?? "", receipt: result.receipt }; +} + +const zaloSendResultAdapter = createAttachedChannelResultAdapter({ channel: "zalo", - sendText: async ({ to, text, accountId, cfg }) => - await ( - await loadZaloChannelRuntime() - ).sendZaloText({ - to, - text, - accountId: accountId ?? undefined, - cfg, - }), - sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => - await ( - await loadZaloChannelRuntime() - ).sendZaloText({ - to, - text, - accountId: accountId ?? undefined, - mediaUrl, - cfg, - }), + sendText: sendZaloDelivery, + sendMedia: sendZaloDelivery, }); export const zaloMessageAdapter = defineChannelMessageAdapter({ @@ -115,25 +123,8 @@ export const zaloMessageAdapter = defineChannelMessageAdapter({ }, }, send: { - text: async ({ to, text, accountId, cfg }) => - await ( - await loadZaloChannelRuntime() - ).sendZaloText({ - to, - text, - accountId: accountId ?? undefined, - cfg, - }), - media: async ({ to, text, mediaUrl, accountId, cfg }) => - await ( - await loadZaloChannelRuntime() - ).sendZaloText({ - to, - text, - accountId: accountId ?? undefined, - mediaUrl, - cfg, - }), + text: sendZaloDelivery, + media: sendZaloDelivery, }, }); @@ -303,11 +294,11 @@ export const zaloPlugin: ChannelPlugin = ctx, textChunkLimit: zaloTextChunkLimit, chunker: chunkTextForOutbound, - sendText: (nextCtx) => zaloRawSendResultAdapter.sendText!(nextCtx), - sendMedia: (nextCtx) => zaloRawSendResultAdapter.sendMedia!(nextCtx), + sendText: (nextCtx) => zaloSendResultAdapter.sendText!(nextCtx), + sendMedia: (nextCtx) => zaloSendResultAdapter.sendMedia!(nextCtx), emptyResult: createEmptyChannelResult("zalo"), onResult: ctx.onDeliveryResult, }), - ...zaloRawSendResultAdapter, + ...zaloSendResultAdapter, }, }); diff --git a/extensions/zalo/src/outbound-payload.contract.test.ts b/extensions/zalo/src/outbound-payload.contract.test.ts index 0ab2a6cf7676..d48e8800e3b3 100644 --- a/extensions/zalo/src/outbound-payload.contract.test.ts +++ b/extensions/zalo/src/outbound-payload.contract.test.ts @@ -6,9 +6,16 @@ import { } from "openclaw/plugin-sdk/channel-contract-testing"; import { createMessageReceiptFromOutboundResults, + sendDurableMessageBatch, verifyChannelMessageAdapterCapabilityProofs, } from "openclaw/plugin-sdk/channel-outbound"; -import { describe, expect, it, vi } from "vitest"; +import { + createEmptyPluginRegistry, + createTestRegistry, + resetPluginRuntimeStateForTest, + setActivePluginRegistry, +} from "openclaw/plugin-sdk/plugin-test-runtime"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { zaloMessageAdapter, zaloPlugin } from "./channel.js"; const { sendZaloTextMock } = vi.hoisted(() => ({ @@ -47,9 +54,21 @@ function requireZaloMediaSender(): NonNullable { return media; } +function requireZaloOutboundTextSender(): NonNullable { + const sendText = zaloPlugin.outbound?.sendText; + if (!sendText) { + throw new Error("Expected Zalo outbound text sender"); + } + return sendText; +} + function createZaloHarness(params: OutboundPayloadHarnessParams) { const sendZalo = vi.fn(); - primeChannelOutboundSendMock(sendZalo, { ok: true, messageId: "zl-1" }, params.sendResults); + primeChannelOutboundSendMock( + sendZalo, + { ok: true, messageId: "zl-1" }, + params.sendResults?.map((result) => ({ ok: true, ...result })), + ); sendZaloTextMock.mockReset().mockImplementation( async (nextCtx: { to: string; text: string; mediaUrl?: string }) => await sendZalo(nextCtx.to, nextCtx.text, { @@ -70,6 +89,11 @@ function createZaloHarness(params: OutboundPayloadHarnessParams) { }; } +afterEach(() => { + resetPluginRuntimeStateForTest(); + setActivePluginRegistry(createEmptyPluginRegistry()); +}); + describe("Zalo outbound payload contract", () => { installChannelOutboundPayloadContractSuite({ channel: "zalo", @@ -77,6 +101,52 @@ describe("Zalo outbound payload contract", () => { createHarness: createZaloHarness, }); + it("throws legacy raw send failures at the typed delivery boundary", async () => { + sendZaloTextMock.mockReset().mockResolvedValue({ + ok: false, + error: "Zalo rejected the message", + receipt: createMessageReceiptFromOutboundResults({ results: [], kind: "text" }), + }); + + await expect( + requireZaloOutboundTextSender()({ cfg: {}, to: "123456789", text: "hello" }), + ).rejects.toThrow("Zalo rejected the message"); + }); + + it.each([ + { kind: "text", payload: { text: "hello" } }, + { + kind: "media", + payload: { text: "image", mediaUrl: "https://example.com/image.png" }, + }, + ] as const)( + "reports $kind message-adapter failures to durable delivery", + async ({ kind, payload }) => { + setActivePluginRegistry( + createTestRegistry([{ pluginId: "zalo", source: "test", plugin: zaloPlugin }]), + ); + sendZaloTextMock.mockReset().mockResolvedValue({ + ok: false, + error: `Zalo rejected the ${kind}`, + receipt: createMessageReceiptFromOutboundResults({ results: [], kind }), + }); + + const result = await sendDurableMessageBatch({ + cfg: {}, + channel: "zalo", + to: "123456789", + payloads: [payload], + skipQueue: true, + }); + + expect(result.status).toBe("failed"); + if (result.status !== "failed") { + throw new Error(`Expected failed Zalo ${kind} delivery`); + } + expect(result.error).toMatchObject({ message: `Zalo rejected the ${kind}` }); + }, + ); + it("declares message adapter durable text and media with receipt proofs", async () => { sendZaloTextMock.mockReset().mockImplementation(async (ctx: { mediaUrl?: string }) => ctx.mediaUrl diff --git a/scripts/check-pairing-account-scope.mjs b/scripts/check-pairing-account-scope.mjs index e3370931be09..8fc875ee5497 100644 --- a/scripts/check-pairing-account-scope.mjs +++ b/scripts/check-pairing-account-scope.mjs @@ -55,14 +55,6 @@ function findViolations(content, filePath) { reason: "readChannelAllowFromStore call must pass explicit accountId as 3rd arg", }); } - } else if ( - callName === "readLegacyChannelAllowFromStore" || - callName === "readLegacyChannelAllowFromStoreSync" - ) { - violations.push({ - line: toLine(sourceFile, node), - reason: `${callName} is legacy-only; use account-scoped readChannelAllowFromStore* APIs`, - }); } else if (callName === "upsertChannelPairingRequest") { const firstArg = node.arguments[0]; if (!firstArg || !hasRequiredAccountIdProperty(firstArg)) { diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index 23759606826d..e9b1c8c39639 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -104,7 +104,6 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({ "channel-streaming": 49, "approval-gateway-runtime": 1, "approval-handler-runtime": 1, - "approval-reaction-runtime": 1, "approval-reply-runtime": 3, "approval-runtime": 1, "config-runtime": 123, @@ -125,6 +124,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({ "ssrf-runtime": 1, "media-runtime": 2, "text-runtime": 191, + "agent-core": 1, "agent-runtime": 7, "plugin-runtime": 13, "channel-secret-runtime": 23, @@ -158,6 +158,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({ // landed on main (#105802) without entrypoint pins; not touched by this PR. "channel-pairing": 1, "conversation-runtime": 4, + "channel-send-result": 1, "channel-policy": 8, "channel-route": 5, "session-store-runtime": 4, @@ -203,20 +204,17 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { ), publicExports: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", - 10644, + 10640, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", - 5358, + 5354, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS", - // 3279 + 5 deprecated pairing/conversation exports added on main by the - // SQLite pairing migration (#105802) without a pin bump (its changed-path - // set skipped this lane); sources are byte-identical to main here. - 3284, + 3282, env, ), publicWildcardReexports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts index 623752622e36..142fd98c53d4 100644 --- a/src/pairing/pairing-store.ts +++ b/src/pairing/pairing-store.ts @@ -210,14 +210,6 @@ async function updateAllowFromStoreEntry(params: { }, sqliteOptionsForEnv(env)); } -/** @deprecated Reads the canonical default-account SQLite rows. */ -export async function readLegacyChannelAllowFromStore( - channel: PairingChannel, - env: NodeJS.ProcessEnv = process.env, -): Promise { - return readAllowFromState(channel, env, DEFAULT_ACCOUNT_ID); -} - export async function readChannelAllowFromStore( channel: PairingChannel, env: NodeJS.ProcessEnv = process.env, @@ -226,14 +218,6 @@ export async function readChannelAllowFromStore( return readAllowFromState(channel, env, accountId); } -/** @deprecated Reads the canonical default-account SQLite rows. */ -export function readLegacyChannelAllowFromStoreSync( - channel: PairingChannel, - env: NodeJS.ProcessEnv = process.env, -): string[] { - return readAllowFromState(channel, env, DEFAULT_ACCOUNT_ID); -} - export function readChannelAllowFromStoreSync( channel: PairingChannel, env: NodeJS.ProcessEnv = process.env, @@ -242,9 +226,6 @@ export function readChannelAllowFromStoreSync( return readAllowFromState(channel, env, accountId); } -/** @deprecated SQLite reads are uncached; retained for the shipped SDK test surface. */ -export function clearPairingAllowFromReadCacheForTest(): void {} - type AllowFromStoreEntryUpdateParams = { channel: PairingChannel; entry: string | number; diff --git a/src/plugin-sdk/approval-reaction-runtime.test.ts b/src/plugin-sdk/approval-reaction-runtime.test.ts index b431464d9a21..5ac455e44bf8 100644 --- a/src/plugin-sdk/approval-reaction-runtime.test.ts +++ b/src/plugin-sdk/approval-reaction-runtime.test.ts @@ -14,7 +14,6 @@ import { listApprovalReactionBindings, normalizeApprovalReactionEmoji, resolveApprovalReactionDecision, - resolveApprovalReactionTarget, resolveTypedApprovalReactionTarget, shouldSuppressLocalNativeExecApprovalPrompt, } from "./approval-reaction-runtime.js"; @@ -145,23 +144,6 @@ describe("plugin-sdk/approval-reaction-runtime", () => { }); }); - it("preserves deprecated id-based kind inference", () => { - expect( - resolveApprovalReactionTarget({ - target: { - approvalId: "plugin:legacy-id", - allowedDecisions: ["deny"], - }, - reactionKey: "👎", - }), - ).toEqual({ - approvalId: "plugin:legacy-id", - approvalKind: "plugin", - decision: "deny", - normalizedEmoji: "👎", - }); - }); - it("builds canonical exec reaction prompts without presentation controls", () => { const payload = buildApprovalReactionPromptPayloadForRequest({ request: execRequest, diff --git a/src/plugin-sdk/approval-reaction-runtime.ts b/src/plugin-sdk/approval-reaction-runtime.ts index 1510647cf8fc..6b3f482071ed 100644 --- a/src/plugin-sdk/approval-reaction-runtime.ts +++ b/src/plugin-sdk/approval-reaction-runtime.ts @@ -242,20 +242,6 @@ function resolveApprovalReactionTargetInternal(params: { }; } -/** - * Resolve a stored target while retaining shipped id-based ownership inference. - * @deprecated Use resolveTypedApprovalReactionTarget with an explicit approvalKind. - */ -export function resolveApprovalReactionTarget(params: { - target: ApprovalReactionTargetRecord | null | undefined; - reactionKey: string; -}): ApprovalReactionTargetResolution | null { - return resolveApprovalReactionTargetInternal({ - ...params, - allowLegacyKindInference: true, - }); -} - /** Resolve an explicitly typed target without deriving ownership from its id. */ export function resolveTypedApprovalReactionTarget(params: { target: diff --git a/src/plugin-sdk/channel-send-result.ts b/src/plugin-sdk/channel-send-result.ts index c8957b0f0660..01929966477f 100644 --- a/src/plugin-sdk/channel-send-result.ts +++ b/src/plugin-sdk/channel-send-result.ts @@ -6,7 +6,11 @@ import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js"; export type { ChannelOutboundAdapter } from "../channels/plugins/outbound.types.js"; export type { OutboundDeliveryResult } from "../infra/outbound/deliver.js"; -/** Legacy raw send result shape accepted from channel SDK adapters. */ +/** + * Legacy raw send result shape accepted from channel SDK adapters. + * @deprecated Return `OutboundDeliveryResult` and use + * `createAttachedChannelResultAdapter`. Removal with the next plugin-SDK major. + */ export type ChannelSendRawResult = { /** Whether the channel send operation succeeded. */ ok: boolean;