mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
refactor(plugin-sdk): prune eligible deprecated exports (#106010)
* refactor(plugin-sdk): remove unshipped compatibility exports * refactor(zalo): use typed outbound delivery results * chore(plugin-sdk): ratchet deprecated surface budgets * chore(changelog): defer release note to release automation * fix(zalo): reject failed message adapter sends * refactor(zalo): share typed send boundary * chore(plugin-sdk): account for main deprecation drift
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -791,6 +791,18 @@ major release. Every entry maps the old API to its canonical replacement.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Raw channel send results -> OutboundDeliveryResult">
|
||||
**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.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Subagent session messages types renamed">
|
||||
Two legacy type aliases still exported from `src/plugins/runtime/types.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";
|
||||
|
||||
@@ -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<ResolvedZaloAccount, ZaloProbeResult> =
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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<ZaloMessageSender["media"]> {
|
||||
return media;
|
||||
}
|
||||
|
||||
function requireZaloOutboundTextSender(): NonNullable<ZaloOutbound["sendText"]> {
|
||||
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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<string[]> {
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -242,20 +242,6 @@ function resolveApprovalReactionTargetInternal<TRoute>(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a stored target while retaining shipped id-based ownership inference.
|
||||
* @deprecated Use resolveTypedApprovalReactionTarget with an explicit approvalKind.
|
||||
*/
|
||||
export function resolveApprovalReactionTarget<TRoute = unknown>(params: {
|
||||
target: ApprovalReactionTargetRecord<TRoute> | null | undefined;
|
||||
reactionKey: string;
|
||||
}): ApprovalReactionTargetResolution<TRoute> | null {
|
||||
return resolveApprovalReactionTargetInternal({
|
||||
...params,
|
||||
allowLegacyKindInference: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve an explicitly typed target without deriving ownership from its id. */
|
||||
export function resolveTypedApprovalReactionTarget<TRoute = unknown>(params: {
|
||||
target:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user