mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 13:54:11 +00:00
fix(feishu): use full gateway channel runtime
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
|
||||
import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { RateLimitError } from "../internal/discord.js";
|
||||
@@ -65,6 +66,7 @@ function createTestChannelRuntime(): ChannelRuntimeSurface {
|
||||
},
|
||||
};
|
||||
return {
|
||||
...createPluginRuntimeMock().channel,
|
||||
runtimeContexts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3704,7 +3704,7 @@ describe("createFeishuMessageReceiveHandler media dedupe", () => {
|
||||
});
|
||||
const handler = createFeishuMessageReceiveHandler({
|
||||
cfg: { channels: { feishu: { dmPolicy: "open" } } } as ClawdbotConfig,
|
||||
core,
|
||||
channelRuntime: core.channel,
|
||||
accountId: "receive-media-dedupe",
|
||||
chatHistories: new Map(),
|
||||
handleMessage,
|
||||
|
||||
@@ -440,6 +440,7 @@ export async function handleFeishuMessage(params: {
|
||||
botOpenId?: string;
|
||||
botName?: string;
|
||||
runtime?: RuntimeEnv;
|
||||
channelRuntime?: ReturnType<typeof getFeishuRuntime>["channel"];
|
||||
chatHistories?: Map<string, HistoryEntry[]>;
|
||||
accountId?: string;
|
||||
processingClaimHeld?: boolean;
|
||||
@@ -450,6 +451,7 @@ export async function handleFeishuMessage(params: {
|
||||
botOpenId,
|
||||
botName,
|
||||
runtime,
|
||||
channelRuntime,
|
||||
chatHistories,
|
||||
accountId,
|
||||
processingClaimHeld = false,
|
||||
@@ -709,7 +711,9 @@ export async function handleFeishuMessage(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const core = getFeishuRuntime();
|
||||
const core = {
|
||||
channel: channelRuntime ?? getFeishuRuntime().channel,
|
||||
} as ReturnType<typeof getFeishuRuntime>;
|
||||
const pairing = createChannelPairingController({
|
||||
core,
|
||||
channel: "feishu",
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
isFutureDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
||||
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
|
||||
import { decodeFeishuCardAction, buildFeishuCardActionTextFallback } from "./card-interaction.js";
|
||||
@@ -170,6 +170,7 @@ async function dispatchSyntheticCommand(params: {
|
||||
account: ReturnType<typeof resolveFeishuRuntimeAccount>;
|
||||
botOpenId?: string;
|
||||
runtime?: RuntimeEnv;
|
||||
channelRuntime?: PluginRuntime["channel"];
|
||||
accountId?: string;
|
||||
chatType?: "p2p" | "group";
|
||||
}): Promise<void> {
|
||||
@@ -184,6 +185,7 @@ async function dispatchSyntheticCommand(params: {
|
||||
event: buildSyntheticMessageEvent(params.event, params.command, resolvedChatType),
|
||||
botOpenId: params.botOpenId,
|
||||
runtime: params.runtime,
|
||||
channelRuntime: params.channelRuntime,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
}
|
||||
@@ -342,6 +344,7 @@ export async function handleFeishuCardAction(params: {
|
||||
event: FeishuCardActionEvent;
|
||||
botOpenId?: string;
|
||||
runtime?: RuntimeEnv;
|
||||
channelRuntime?: PluginRuntime["channel"];
|
||||
accountId?: string;
|
||||
}): Promise<void> {
|
||||
const { cfg, event, runtime, accountId } = params;
|
||||
@@ -465,6 +468,7 @@ export async function handleFeishuCardAction(params: {
|
||||
account,
|
||||
botOpenId: params.botOpenId,
|
||||
runtime,
|
||||
channelRuntime: params.channelRuntime,
|
||||
accountId,
|
||||
chatType: envelope.c?.t,
|
||||
});
|
||||
@@ -495,6 +499,7 @@ export async function handleFeishuCardAction(params: {
|
||||
account,
|
||||
botOpenId: params.botOpenId,
|
||||
runtime,
|
||||
channelRuntime: params.channelRuntime,
|
||||
accountId,
|
||||
});
|
||||
completeFeishuCardActionToken({ token: event.token, accountId: account.accountId });
|
||||
|
||||
@@ -31,6 +31,7 @@ import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { PluginRuntime } from "../runtime-api.js";
|
||||
import {
|
||||
inspectFeishuCredentials,
|
||||
listEnabledFeishuAccounts,
|
||||
@@ -1319,6 +1320,9 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
|
||||
return monitorFeishuProvider({
|
||||
config: ctx.cfg,
|
||||
runtime: ctx.runtime,
|
||||
// Gateway provides the full channel runtime here; the public SDK type
|
||||
// stays context-only for external compatibility.
|
||||
channelRuntime: ctx.channelRuntime as PluginRuntime["channel"] | undefined,
|
||||
abortSignal: ctx.abortSignal,
|
||||
accountId: ctx.accountId,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "../runtime-api.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv, HistoryEntry } from "../runtime-api.js";
|
||||
import { raceWithTimeoutAndAbort } from "./async.js";
|
||||
import {
|
||||
handleFeishuMessage,
|
||||
@@ -164,6 +164,7 @@ function normalizeFeishuChatType(value: unknown): FeishuChatType | undefined {
|
||||
type RegisterEventHandlersContext = {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId: string;
|
||||
channelRuntime: PluginRuntime["channel"];
|
||||
runtime?: RuntimeEnv;
|
||||
chatHistories: Map<string, HistoryEntry[]>;
|
||||
fireAndForget?: boolean;
|
||||
@@ -266,7 +267,7 @@ function registerEventHandlers(
|
||||
eventDispatcher: Lark.EventDispatcher,
|
||||
context: RegisterEventHandlersContext,
|
||||
): void {
|
||||
const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
|
||||
const { cfg, accountId, channelRuntime, runtime, chatHistories, fireAndForget } = context;
|
||||
const log = runtime?.log ?? console.log;
|
||||
const error = runtime?.error ?? console.error;
|
||||
const runFeishuHandler = async (params: { task: () => Promise<void>; errorMessage: string }) => {
|
||||
@@ -286,7 +287,7 @@ function registerEventHandlers(
|
||||
eventDispatcher.register({
|
||||
"im.message.receive_v1": createFeishuMessageReceiveHandler({
|
||||
cfg,
|
||||
core: getFeishuRuntime(),
|
||||
channelRuntime,
|
||||
accountId,
|
||||
runtime,
|
||||
chatHistories,
|
||||
@@ -353,6 +354,7 @@ function registerEventHandlers(
|
||||
botOpenId: myBotId,
|
||||
botName: botNames.get(accountId),
|
||||
runtime,
|
||||
channelRuntime,
|
||||
chatHistories,
|
||||
accountId,
|
||||
});
|
||||
@@ -383,6 +385,7 @@ function registerEventHandlers(
|
||||
botOpenId: myBotId,
|
||||
botName: botNames.get(accountId),
|
||||
runtime,
|
||||
channelRuntime,
|
||||
chatHistories,
|
||||
accountId,
|
||||
});
|
||||
@@ -396,6 +399,7 @@ function registerEventHandlers(
|
||||
runtime,
|
||||
chatHistories,
|
||||
fireAndForget,
|
||||
channelRuntime,
|
||||
}),
|
||||
"card.action.trigger": async (data: unknown) => {
|
||||
try {
|
||||
@@ -409,6 +413,7 @@ function registerEventHandlers(
|
||||
event,
|
||||
botOpenId: botOpenIds.get(accountId),
|
||||
runtime,
|
||||
channelRuntime,
|
||||
accountId,
|
||||
});
|
||||
if (fireAndForget) {
|
||||
@@ -432,6 +437,7 @@ export type BotOpenIdSource =
|
||||
export type MonitorSingleAccountParams = {
|
||||
cfg: ClawdbotConfig;
|
||||
account: ResolvedFeishuAccount;
|
||||
channelRuntime?: PluginRuntime["channel"];
|
||||
runtime?: RuntimeEnv;
|
||||
abortSignal?: AbortSignal;
|
||||
botOpenIdSource?: BotOpenIdSource;
|
||||
@@ -473,10 +479,12 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
|
||||
const eventDispatcher = createEventDispatcher(account);
|
||||
const chatHistories = new Map<string, HistoryEntry[]>();
|
||||
threadBindingManager = createFeishuThreadBindingManager({ accountId, cfg });
|
||||
const channelRuntime = params.channelRuntime ?? getFeishuRuntime().channel;
|
||||
|
||||
registerEventHandlers(eventDispatcher, {
|
||||
cfg,
|
||||
accountId,
|
||||
channelRuntime,
|
||||
runtime,
|
||||
chatHistories,
|
||||
fireAndForget: params.fireAndForget ?? true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isRecord, readStringValue as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { ClawdbotConfig, HistoryEntry, RuntimeEnv } from "../runtime-api.js";
|
||||
import type { ClawdbotConfig, HistoryEntry, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
|
||||
import { maybeHandleFeishuQuickActionMenu } from "./card-ux-launcher.js";
|
||||
import {
|
||||
@@ -54,6 +54,7 @@ export function createFeishuBotMenuHandler(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId: string;
|
||||
runtime?: RuntimeEnv;
|
||||
channelRuntime?: PluginRuntime["channel"];
|
||||
chatHistories: Map<string, HistoryEntry[]>;
|
||||
fireAndForget?: boolean;
|
||||
getBotOpenId?: (accountId: string) => string | undefined;
|
||||
@@ -117,6 +118,7 @@ export function createFeishuBotMenuHandler(params: {
|
||||
botOpenId: getBotOpenId(accountId),
|
||||
botName: getBotName(accountId),
|
||||
runtime,
|
||||
channelRuntime: params.channelRuntime,
|
||||
chatHistories,
|
||||
accountId,
|
||||
processingClaimHeld: true,
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { FeishuChatType } from "./types.js";
|
||||
|
||||
type FeishuMessageReceiveHandlerContext = {
|
||||
cfg: ClawdbotConfig;
|
||||
core: PluginRuntime;
|
||||
channelRuntime: PluginRuntime["channel"];
|
||||
accountId: string;
|
||||
runtime?: RuntimeEnv;
|
||||
chatHistories: Map<string, HistoryEntry[]>;
|
||||
@@ -23,6 +23,7 @@ type FeishuMessageReceiveHandlerContext = {
|
||||
botOpenId?: string;
|
||||
botName?: string;
|
||||
runtime?: RuntimeEnv;
|
||||
channelRuntime?: PluginRuntime["channel"];
|
||||
chatHistories?: Map<string, HistoryEntry[]>;
|
||||
accountId?: string;
|
||||
processingClaimHeld?: boolean;
|
||||
@@ -154,7 +155,7 @@ function resolveFeishuDebounceMentions(params: {
|
||||
|
||||
export function createFeishuMessageReceiveHandler({
|
||||
cfg,
|
||||
core,
|
||||
channelRuntime,
|
||||
accountId,
|
||||
runtime,
|
||||
chatHistories,
|
||||
@@ -168,7 +169,7 @@ export function createFeishuMessageReceiveHandler({
|
||||
resolveSequentialKey = ({ accountId, event }) =>
|
||||
`feishu:${accountId}:${event.message.chat_id?.trim() || "unknown"}`,
|
||||
}: FeishuMessageReceiveHandlerContext): (data: unknown) => Promise<void> {
|
||||
const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
|
||||
const inboundDebounceMs = channelRuntime.debounce.resolveInboundDebounceMs({
|
||||
cfg,
|
||||
channel: "feishu",
|
||||
});
|
||||
@@ -196,6 +197,7 @@ export function createFeishuMessageReceiveHandler({
|
||||
botOpenId: getBotOpenId(accountId),
|
||||
botName: getBotName(accountId),
|
||||
runtime,
|
||||
channelRuntime,
|
||||
chatHistories,
|
||||
accountId,
|
||||
processingClaimHeld: true,
|
||||
@@ -238,7 +240,7 @@ export function createFeishuMessageReceiveHandler({
|
||||
}
|
||||
};
|
||||
|
||||
const inboundDebouncer = core.channel.debounce.createInboundDebouncer<FeishuMessageEvent>({
|
||||
const inboundDebouncer = channelRuntime.debounce.createInboundDebouncer<FeishuMessageEvent>({
|
||||
debounceMs: inboundDebounceMs,
|
||||
buildKey: (event) => {
|
||||
const chatId = event.message.chat_id?.trim();
|
||||
@@ -255,7 +257,7 @@ export function createFeishuMessageReceiveHandler({
|
||||
return false;
|
||||
}
|
||||
const text = resolveDebounceText(event);
|
||||
return Boolean(text) && !core.channel.commands.isControlCommandMessage(text, cfg);
|
||||
return Boolean(text) && !channelRuntime.commands.isControlCommandMessage(text, cfg);
|
||||
},
|
||||
onFlush: async (entries) => {
|
||||
const last = entries.at(-1);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { listEnabledFeishuAccounts, resolveFeishuRuntimeAccount } from "./accounts.js";
|
||||
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
|
||||
import {
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
export type MonitorFeishuOpts = {
|
||||
config?: ClawdbotConfig;
|
||||
runtime?: RuntimeEnv;
|
||||
channelRuntime?: PluginRuntime["channel"];
|
||||
abortSignal?: AbortSignal;
|
||||
accountId?: string;
|
||||
};
|
||||
@@ -48,6 +49,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
||||
return monitorSingleAccount({
|
||||
cfg,
|
||||
account,
|
||||
channelRuntime: opts.channelRuntime,
|
||||
runtime: opts.runtime,
|
||||
abortSignal: opts.abortSignal,
|
||||
});
|
||||
@@ -85,6 +87,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
||||
monitorSingleAccount({
|
||||
cfg,
|
||||
account,
|
||||
channelRuntime: opts.channelRuntime,
|
||||
runtime: opts.runtime,
|
||||
abortSignal: opts.abortSignal,
|
||||
botOpenIdSource: { kind: "prefetched", botOpenId, botName },
|
||||
|
||||
@@ -32,11 +32,10 @@ export type ChannelRuntimeContextRegistry = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Minimal channel-runtime surface threaded through gateway/setup flows.
|
||||
* Minimal channel-runtime surface exported through the public plugin SDK.
|
||||
*
|
||||
* Most callers only pass this object through or use `runtimeContexts`.
|
||||
* Keeping this leaf contract small avoids dragging the full plugin runtime
|
||||
* graph into generic channel adapter types.
|
||||
* Gateway startup supplies the full plugin channel runtime, but external callers
|
||||
* may still type context-only helpers against this compatibility surface.
|
||||
*/
|
||||
export type ChannelRuntimeSurface = {
|
||||
runtimeContexts: ChannelRuntimeContextRegistry;
|
||||
|
||||
@@ -248,9 +248,8 @@ export type ChannelGatewayContext<ResolvedAccount = unknown> = {
|
||||
/**
|
||||
* Optional channel runtime helpers for external channel plugins.
|
||||
*
|
||||
* This field provides access to advanced Plugin SDK features that are
|
||||
* available to external plugins but not to built-in channels (which can
|
||||
* directly import internal modules).
|
||||
* This field provides the canonical channel runtime helpers for channel
|
||||
* dispatch, routing, session, reply, and startup context work.
|
||||
*
|
||||
* ## Available Features
|
||||
*
|
||||
@@ -265,7 +264,7 @@ export type ChannelGatewayContext<ResolvedAccount = unknown> = {
|
||||
*
|
||||
* ## Use Cases
|
||||
*
|
||||
* External channel plugins (e.g., email, SMS, custom integrations) that need:
|
||||
* Channel plugins that need:
|
||||
* - AI-powered response generation and delivery
|
||||
* - Advanced text processing and formatting
|
||||
* - Session tracking and management
|
||||
@@ -299,13 +298,9 @@ export type ChannelGatewayContext<ResolvedAccount = unknown> = {
|
||||
* ## Backward Compatibility
|
||||
*
|
||||
* - This field is **optional** - channels that don't need it can ignore it
|
||||
* - Bundled channels typically don't use this field
|
||||
* because they can directly import internal modules
|
||||
* - Gateway startup passes a full `createPluginRuntime().channel` surface
|
||||
* when a runtime resolver is configured
|
||||
* - External plugins should check for undefined before using
|
||||
* - `runtimeContexts` is the stable startup-safe subset. Bundled channels
|
||||
* may receive only that subset during provider boot.
|
||||
* - External channel plugins that need reply/routing/session helpers receive
|
||||
* a full `createPluginRuntime().channel` surface from the Gateway.
|
||||
*
|
||||
* @since Plugin SDK 2026.2.19
|
||||
* @see {@link https://docs.openclaw.ai/plugins/building-plugins | Plugin SDK documentation}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
|
||||
import {
|
||||
type ChannelGatewayContext,
|
||||
type ChannelId,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
} from "../logging/subsystem.js";
|
||||
import { createEmptyPluginRegistry, type PluginRegistry } from "../plugins/registry.js";
|
||||
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createChannelRuntimeContextRegistry } from "../plugins/runtime/channel-runtime-contexts.js";
|
||||
import { createRuntimeChannel } from "../plugins/runtime/runtime-channel.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
@@ -159,9 +157,8 @@ function installTestRegistry(
|
||||
}
|
||||
|
||||
function createManager(options?: {
|
||||
channelRuntime?: ChannelRuntimeSurface;
|
||||
resolveChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
|
||||
resolveStartupChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
|
||||
channelRuntime?: PluginRuntime["channel"];
|
||||
resolveChannelRuntime?: () => PluginRuntime["channel"] | Promise<PluginRuntime["channel"]>;
|
||||
getRuntimeConfig?: () => Record<string, unknown>;
|
||||
channelIds?: ChannelId[];
|
||||
startupTrace?: { measure: <T>(name: string, run: () => T | Promise<T>) => Promise<T> };
|
||||
@@ -186,9 +183,6 @@ function createManager(options?: {
|
||||
...(options?.resolveChannelRuntime
|
||||
? { resolveChannelRuntime: options.resolveChannelRuntime }
|
||||
: {}),
|
||||
...(options?.resolveStartupChannelRuntime
|
||||
? { resolveStartupChannelRuntime: options.resolveStartupChannelRuntime }
|
||||
: {}),
|
||||
...(options?.startupTrace ? { startupTrace: options.startupTrace } : {}),
|
||||
});
|
||||
createdManagers.push({ channelIds, manager });
|
||||
@@ -785,32 +779,31 @@ describe("server-channels auto restart", () => {
|
||||
expect(ctx?.channelRuntime).not.toBe(channelRuntime);
|
||||
});
|
||||
|
||||
it("uses a lightweight startup runtime for bundled channels", async () => {
|
||||
it("passes the full runtime path to bundled channel startup", async () => {
|
||||
const fullRuntime = {
|
||||
...createRuntimeChannel(),
|
||||
marker: "full-channel-runtime",
|
||||
} as PluginRuntime["channel"] & { marker: string };
|
||||
const startupRuntime = {
|
||||
runtimeContexts: createChannelRuntimeContextRegistry(),
|
||||
marker: "startup-channel-runtime",
|
||||
};
|
||||
const resolveChannelRuntime = vi.fn(() => fullRuntime);
|
||||
const resolveStartupChannelRuntime = vi.fn(() => startupRuntime);
|
||||
const startAccount = vi.fn(async (_ctx: ChannelGatewayContext<TestAccount>) => {});
|
||||
|
||||
installTestRegistry({ plugin: createTestPlugin({ startAccount }), origin: "bundled" });
|
||||
const manager = createManager({ resolveChannelRuntime, resolveStartupChannelRuntime });
|
||||
installTestRegistry({
|
||||
plugin: createTestPlugin({ startAccount }),
|
||||
origin: "bundled",
|
||||
});
|
||||
const manager = createManager({ resolveChannelRuntime });
|
||||
|
||||
await manager.startChannels();
|
||||
|
||||
expect(resolveStartupChannelRuntime).toHaveBeenCalledTimes(1);
|
||||
expect(resolveChannelRuntime).not.toHaveBeenCalled();
|
||||
expect(resolveChannelRuntime).toHaveBeenCalledTimes(1);
|
||||
expect(startAccount).toHaveBeenCalledTimes(1);
|
||||
const ctx = firstStartAccountContext(startAccount);
|
||||
expect((ctx?.channelRuntime as { marker?: string } | undefined)?.marker).toBe(
|
||||
"startup-channel-runtime",
|
||||
"full-channel-runtime",
|
||||
);
|
||||
expect(typeof (ctx?.channelRuntime as PluginRuntime["channel"] | undefined)?.inbound.run).toBe(
|
||||
"function",
|
||||
);
|
||||
expect(ctx?.channelRuntime).not.toBe(startupRuntime);
|
||||
});
|
||||
|
||||
it("keeps the full runtime path for non-bundled channels", async () => {
|
||||
@@ -818,20 +811,14 @@ describe("server-channels auto restart", () => {
|
||||
...createRuntimeChannel(),
|
||||
marker: "full-channel-runtime",
|
||||
} as PluginRuntime["channel"] & { marker: string };
|
||||
const startupRuntime = {
|
||||
runtimeContexts: createChannelRuntimeContextRegistry(),
|
||||
marker: "startup-channel-runtime",
|
||||
};
|
||||
const resolveChannelRuntime = vi.fn(() => fullRuntime);
|
||||
const resolveStartupChannelRuntime = vi.fn(() => startupRuntime);
|
||||
const startAccount = vi.fn(async (_ctx: ChannelGatewayContext<TestAccount>) => {});
|
||||
|
||||
installTestRegistry({ plugin: createTestPlugin({ startAccount }), origin: "workspace" });
|
||||
const manager = createManager({ resolveChannelRuntime, resolveStartupChannelRuntime });
|
||||
const manager = createManager({ resolveChannelRuntime });
|
||||
|
||||
await manager.startChannels();
|
||||
|
||||
expect(resolveStartupChannelRuntime).not.toHaveBeenCalled();
|
||||
expect(resolveChannelRuntime).toHaveBeenCalledTimes(1);
|
||||
const ctx = firstStartAccountContext(startAccount);
|
||||
expect((ctx?.channelRuntime as { marker?: string } | undefined)?.marker).toBe(
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
type ChannelId,
|
||||
getChannelPlugin,
|
||||
getLoadedChannelPluginOrigin,
|
||||
listChannelPlugins,
|
||||
} from "../channels/plugins/index.js";
|
||||
import { type ChannelId, getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelAccountSnapshot } from "../channels/plugins/types.public.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { startChannelApprovalHandlerBootstrap } from "../infra/approval-handler-bootstrap.js";
|
||||
@@ -20,6 +14,7 @@ import {
|
||||
} from "../logging/subsystem.js";
|
||||
import { withPluginHttpRouteRegistry } from "../plugins/http-registry.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginRuntimeChannel } from "../plugins/runtime/types-channel.js";
|
||||
import { resolveAccountEntry, resolveNormalizedAccountEntry } from "../routing/account-lookup.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
@@ -144,15 +139,12 @@ type ChannelManagerOptions = {
|
||||
channelLogs: Partial<Record<ChannelId, SubsystemLogger>>;
|
||||
channelRuntimeEnvs: Partial<Record<ChannelId, RuntimeEnv>>;
|
||||
/**
|
||||
* Optional channel runtime helpers for external channel plugins.
|
||||
* Optional channel runtime helpers for channel plugins.
|
||||
*
|
||||
* When provided, this value is passed to all channel plugins via the
|
||||
* `channelRuntime` field in `ChannelGatewayContext`, enabling external
|
||||
* plugins to access advanced Plugin SDK features (AI dispatch, routing,
|
||||
* text processing, etc.).
|
||||
*
|
||||
* Bundled channels typically don't use this because they can directly
|
||||
* import internal modules from the monorepo.
|
||||
* plugins to access Plugin SDK channel features (AI dispatch, routing,
|
||||
* session management, startup runtime contexts, text processing, etc.).
|
||||
*
|
||||
* This field is optional - omitting it maintains backward compatibility
|
||||
* with existing channels. When provided, it must be a real
|
||||
@@ -173,22 +165,16 @@ type ChannelManagerOptions = {
|
||||
* @since Plugin SDK 2026.2.19
|
||||
* @see {@link ChannelGatewayContext.channelRuntime}
|
||||
*/
|
||||
channelRuntime?: ChannelRuntimeSurface;
|
||||
channelRuntime?: PluginRuntimeChannel;
|
||||
/**
|
||||
* Lazily resolves optional channel runtime helpers for external channel plugins.
|
||||
* Lazily resolves optional channel runtime helpers for channel plugins.
|
||||
*
|
||||
* Use this when the caller wants to avoid instantiating the full plugin channel
|
||||
* runtime during gateway startup. The manager only needs the runtime surface once
|
||||
* a channel account actually starts. The resolved value must be a real
|
||||
* `createPluginRuntime().channel` surface.
|
||||
*/
|
||||
resolveChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
|
||||
/**
|
||||
* Lightweight channel runtime used for bundled channel startup. Bundled
|
||||
* channels only need `runtimeContexts` while booting, so this avoids pulling
|
||||
* the full reply/routing/session runtime graph onto the critical path.
|
||||
*/
|
||||
resolveStartupChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
|
||||
resolveChannelRuntime?: () => PluginRuntimeChannel | Promise<PluginRuntimeChannel>;
|
||||
getPluginHttpRouteRegistry?: () => PluginRegistry;
|
||||
startupTrace?: GatewayStartupTrace;
|
||||
deferStartupAccountStartsUntil?: Promise<void>;
|
||||
@@ -238,7 +224,6 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
|
||||
channelRuntimeEnvs,
|
||||
channelRuntime,
|
||||
resolveChannelRuntime,
|
||||
resolveStartupChannelRuntime,
|
||||
getPluginHttpRouteRegistry,
|
||||
startupTrace,
|
||||
} = opts;
|
||||
@@ -347,18 +332,10 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
|
||||
return next;
|
||||
};
|
||||
|
||||
const getChannelRuntime = async (
|
||||
channelId: ChannelId,
|
||||
): Promise<ChannelRuntimeSurface | undefined> => {
|
||||
const getChannelRuntime = async (): Promise<PluginRuntimeChannel | undefined> => {
|
||||
if (channelRuntime) {
|
||||
return channelRuntime;
|
||||
}
|
||||
if (getLoadedChannelPluginOrigin(channelId) === "bundled") {
|
||||
const startupRuntime = await resolveStartupChannelRuntime?.();
|
||||
if (startupRuntime) {
|
||||
return startupRuntime;
|
||||
}
|
||||
}
|
||||
return await resolveChannelRuntime?.();
|
||||
};
|
||||
const measureStartup = async <T>(name: string, run: () => T | Promise<T>): Promise<T> => {
|
||||
@@ -449,8 +426,11 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
|
||||
let handedOffTask = false;
|
||||
const log = ensureChannelLog(channelId);
|
||||
const runtime = ensureChannelRuntime(channelId);
|
||||
let scopedChannelRuntime: ReturnType<typeof createTaskScopedChannelRuntime> | null = null;
|
||||
let channelRuntimeForTask: ChannelRuntimeSurface | undefined;
|
||||
let scopedChannelRuntime: {
|
||||
channelRuntime?: PluginRuntimeChannel;
|
||||
dispose: () => void;
|
||||
} | null = null;
|
||||
let channelRuntimeForTask: PluginRuntimeChannel | undefined;
|
||||
let stopApprovalBootstrap: () => Promise<void> = async () => {};
|
||||
const stopTaskScopedApprovalRuntime = async () => {
|
||||
const scopedRuntime = scopedChannelRuntime;
|
||||
@@ -519,7 +499,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
|
||||
|
||||
scopedChannelRuntime = await measureStartup(`channels.${channelId}.runtime`, async () =>
|
||||
createTaskScopedChannelRuntime({
|
||||
channelRuntime: await getChannelRuntime(channelId),
|
||||
channelRuntime: await getChannelRuntime(),
|
||||
}),
|
||||
);
|
||||
channelRuntimeForTask = scopedChannelRuntime.channelRuntime;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
|
||||
import { getActiveEmbeddedRunCount } from "../agents/embedded-agent-runner/run-state.js";
|
||||
import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js";
|
||||
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
|
||||
import {
|
||||
getLoadedChannelPluginEntryById,
|
||||
listLoadedChannelPlugins,
|
||||
@@ -177,7 +176,6 @@ const logTailscale = log.child("tailscale");
|
||||
const logChannels = log.child("channels");
|
||||
|
||||
let cachedChannelRuntimePromise: Promise<PluginRuntime["channel"]> | null = null;
|
||||
let cachedStartupChannelRuntimePromise: Promise<ChannelRuntimeSurface> | null = null;
|
||||
|
||||
function getChannelRuntime() {
|
||||
cachedChannelRuntimePromise ??= import("../plugins/runtime/runtime-channel.js").then(
|
||||
@@ -186,16 +184,6 @@ function getChannelRuntime() {
|
||||
return cachedChannelRuntimePromise;
|
||||
}
|
||||
|
||||
function getStartupChannelRuntime() {
|
||||
cachedStartupChannelRuntimePromise ??=
|
||||
import("../plugins/runtime/channel-runtime-contexts.js").then(
|
||||
({ createChannelRuntimeContextRegistry }) => ({
|
||||
runtimeContexts: createChannelRuntimeContextRegistry(),
|
||||
}),
|
||||
);
|
||||
return cachedStartupChannelRuntimePromise;
|
||||
}
|
||||
|
||||
async function closeMcpLoopbackServerOnDemand(): Promise<void> {
|
||||
const { closeMcpLoopbackServer } = await import("./mcp-http.js");
|
||||
await closeMcpLoopbackServer();
|
||||
@@ -864,7 +852,6 @@ export async function startGatewayServer(
|
||||
channelLogs,
|
||||
channelRuntimeEnvs,
|
||||
resolveChannelRuntime: getChannelRuntime,
|
||||
resolveStartupChannelRuntime: getStartupChannelRuntime,
|
||||
getPluginHttpRouteRegistry: () => pluginRegistry,
|
||||
startupTrace,
|
||||
deferStartupAccountStartsUntil: startupAccountStartsReady,
|
||||
|
||||
@@ -83,10 +83,10 @@ export function watchChannelRuntimeContexts(
|
||||
});
|
||||
}
|
||||
|
||||
export function createTaskScopedChannelRuntime(params: {
|
||||
channelRuntime?: ChannelRuntimeSurface;
|
||||
export function createTaskScopedChannelRuntime<T extends ChannelRuntimeSurface>(params: {
|
||||
channelRuntime?: T;
|
||||
}): {
|
||||
channelRuntime?: ChannelRuntimeSurface;
|
||||
channelRuntime?: T;
|
||||
dispose: () => void;
|
||||
} {
|
||||
const baseRuntime = params.channelRuntime;
|
||||
@@ -114,7 +114,7 @@ export function createTaskScopedChannelRuntime(params: {
|
||||
};
|
||||
};
|
||||
|
||||
const scopedRuntime: ChannelRuntimeSurface = {
|
||||
const scopedRuntime = {
|
||||
...baseRuntime,
|
||||
runtimeContexts: {
|
||||
...runtimeContexts,
|
||||
@@ -123,7 +123,7 @@ export function createTaskScopedChannelRuntime(params: {
|
||||
return trackLease(lease);
|
||||
},
|
||||
},
|
||||
};
|
||||
} as T;
|
||||
|
||||
return {
|
||||
channelRuntime: scopedRuntime,
|
||||
|
||||
Reference in New Issue
Block a user