mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix(telegram): stabilize topic dispatch runtime
This commit is contained in:
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/update: move ClawHub-preferred externalized plugin installs back to ClawHub after an earlier npm fallback once the ClawHub package becomes available. Thanks @vincentkoc.
|
||||
- Plugins/update: clean stale bundled load paths for already-externalized pinned npm and ClawHub plugin installs, so release-channel sync does not leave removed bundled paths ahead of the installed external package. Thanks @vincentkoc.
|
||||
- Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
|
||||
- Telegram: accept plugin-owned numeric forum-topic targets in the agent message tool and keep reply-dispatch provider chunks behind a real stable runtime alias during in-place package updates. Fixes #77137. Thanks @richardmqq.
|
||||
- Google Meet: preserve `realtime.introMessage: ""` so realtime Chrome joins can stay silent instead of restoring the default spoken intro. Thanks @vincentkoc.
|
||||
- Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant.
|
||||
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
|
||||
@@ -777,11 +777,12 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- `channels.telegram.dms["<user_id>"].historyLimit`
|
||||
- `channels.telegram.retry` config applies to Telegram send helpers (CLI/tools/actions) for recoverable outbound API errors. Inbound final-reply delivery also uses a bounded safe-send retry for Telegram pre-connect failures, but it does not retry ambiguous post-send network envelopes that could duplicate visible messages.
|
||||
|
||||
CLI send target can be numeric chat ID or username:
|
||||
CLI and message-tool send targets can be numeric chat ID, username, or a forum topic target:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel telegram --target 123456789 --message "hi"
|
||||
openclaw message send --channel telegram --target @name --message "hi"
|
||||
openclaw message send --channel telegram --target -1001234567890:topic:42 --message "hi topic"
|
||||
```
|
||||
|
||||
Telegram polls use `openclaw message poll` and support forum topics:
|
||||
|
||||
@@ -27,7 +27,7 @@ Channel selection:
|
||||
Target formats (`--target`):
|
||||
|
||||
- WhatsApp: E.164, group JID, or WhatsApp Channel/Newsletter JID (`...@newsletter`)
|
||||
- Telegram: chat id or `@username`
|
||||
- Telegram: chat id, `@username`, or forum topic target (`-1001234567890:topic:42`, or `--thread-id 42`)
|
||||
- Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels)
|
||||
- Google Chat: `spaces/<spaceId>` or `users/<userId>`
|
||||
- Slack: `channel:<id>` or `user:<id>` (raw channel id is accepted)
|
||||
|
||||
@@ -41,9 +41,9 @@ const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [
|
||||
["server-close-DsVPJDIx.js", "server-close.runtime.js"],
|
||||
["server-close-DvAvfgr8.js", "server-close.runtime.js"],
|
||||
// v2026.5.3 beta reply-dispatch lazy chunks.
|
||||
["provider-dispatcher-6EQEtc-t.js", "provider-dispatcher.js"],
|
||||
["provider-dispatcher-BpL2E92x.js", "provider-dispatcher.js"],
|
||||
["provider-dispatcher-JG96SkLX.js", "provider-dispatcher.js"],
|
||||
["provider-dispatcher-6EQEtc-t.js", "provider-dispatcher.runtime.js"],
|
||||
["provider-dispatcher-BpL2E92x.js", "provider-dispatcher.runtime.js"],
|
||||
["provider-dispatcher-JG96SkLX.js", "provider-dispatcher.runtime.js"],
|
||||
];
|
||||
const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [
|
||||
{
|
||||
|
||||
@@ -427,6 +427,34 @@ describe("message tool path passthrough", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("message tool Telegram topic targets", () => {
|
||||
it("passes numeric forum topic targets and thread ids to outbound resolution", async () => {
|
||||
mockSendResult({ to: "telegram:-1001234567890:topic:42" });
|
||||
|
||||
const call = await executeSend({
|
||||
toolOptions: {
|
||||
currentChannelProvider: "telegram",
|
||||
currentChannelId: "telegram:-1001234567890:topic:42",
|
||||
},
|
||||
action: {
|
||||
channel: "telegram",
|
||||
target: "-1001234567890:topic:42",
|
||||
threadId: "42",
|
||||
message: "topic hello",
|
||||
},
|
||||
});
|
||||
|
||||
expect(call?.params).toEqual(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
target: "-1001234567890:topic:42",
|
||||
threadId: "42",
|
||||
message: "topic hello",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("message tool schema scoping", () => {
|
||||
const telegramPlugin = createChannelPlugin({
|
||||
id: "telegram",
|
||||
|
||||
1
src/auto-reply/reply/provider-dispatcher.runtime.ts
Normal file
1
src/auto-reply/reply/provider-dispatcher.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./provider-dispatcher.js";
|
||||
@@ -123,4 +123,36 @@ describe("runMessageAction core send routing", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts Telegram numeric forum topic targets through plugin-owned grammar", async () => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
|
||||
const result = await runMessageAction({
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "telegram",
|
||||
target: "-1001234567890:topic:42",
|
||||
message: "topic hello",
|
||||
},
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
if (result.kind !== "send") {
|
||||
throw new Error(`Expected send result, got ${result.kind}`);
|
||||
}
|
||||
expect(result.to).toBe("telegram:-1001234567890:topic:42");
|
||||
expect(result.payload).toEqual(
|
||||
expect.objectContaining({
|
||||
to: "telegram:-1001234567890:topic:42",
|
||||
dryRun: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
const getLoadedChannelPluginMock = vi.hoisted(() => vi.fn());
|
||||
const getChannelPluginMock = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginChannelRegistryVersionMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -15,7 +16,11 @@ let normalizeTargetForProvider: TargetNormalizationModule["normalizeTargetForPro
|
||||
let resetTargetNormalizerCacheForTests: TargetNormalizationModule["__testing"]["resetTargetNormalizerCacheForTests"];
|
||||
|
||||
vi.mock("../../channels/plugins/registry-loaded-read.js", () => ({
|
||||
getLoadedChannelPluginForRead: (...args: unknown[]) => getChannelPluginMock(...args),
|
||||
getLoadedChannelPluginForRead: (...args: unknown[]) => getLoadedChannelPluginMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/runtime.js", () => ({
|
||||
@@ -38,6 +43,7 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
getLoadedChannelPluginMock.mockReset();
|
||||
getChannelPluginMock.mockReset();
|
||||
getActivePluginChannelRegistryVersionMock.mockReset();
|
||||
resetTargetNormalizerCacheForTests();
|
||||
@@ -58,6 +64,7 @@ describe("normalizeTargetForProvider", () => {
|
||||
{
|
||||
provider: "unknown",
|
||||
setup: () => {
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
getChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
},
|
||||
expected: "raw-id",
|
||||
@@ -66,6 +73,7 @@ describe("normalizeTargetForProvider", () => {
|
||||
provider: "alpha",
|
||||
setup: () => {
|
||||
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(1);
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
getChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
},
|
||||
expected: "raw-id",
|
||||
@@ -85,7 +93,7 @@ describe("normalizeTargetForProvider", () => {
|
||||
.mockReturnValueOnce(10)
|
||||
.mockReturnValueOnce(10)
|
||||
.mockReturnValueOnce(11);
|
||||
getChannelPluginMock
|
||||
getLoadedChannelPluginMock
|
||||
.mockReturnValueOnce({
|
||||
messaging: { normalizeTarget: firstNormalizer },
|
||||
})
|
||||
@@ -97,14 +105,30 @@ describe("normalizeTargetForProvider", () => {
|
||||
expect(normalizeTargetForProvider("alpha", " def ")).toBe("DEF");
|
||||
expect(normalizeTargetForProvider("alpha", " ghi ")).toBe("next:ghi");
|
||||
|
||||
expect(getChannelPluginMock).toHaveBeenCalledTimes(2);
|
||||
expect(getLoadedChannelPluginMock).toHaveBeenCalledTimes(2);
|
||||
expect(getChannelPluginMock).not.toHaveBeenCalled();
|
||||
expect(firstNormalizer).toHaveBeenCalledTimes(2);
|
||||
expect(secondNormalizer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses bundled/catalog target normalization when the channel is not loaded", () => {
|
||||
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(30);
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
normalizeTarget: (raw: string) =>
|
||||
raw.trim() === "-1001234567890:topic:42" ? "telegram:-1001234567890:topic:42" : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(normalizeTargetForProvider("telegram", " -1001234567890:topic:42 ")).toBe(
|
||||
"telegram:-1001234567890:topic:42",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns undefined when the provider normalizer resolves to an empty value", () => {
|
||||
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(20);
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
normalizeTarget: () => "",
|
||||
},
|
||||
@@ -121,7 +145,7 @@ describe("resolveNormalizedTargetInput", () => {
|
||||
|
||||
it("returns raw and normalized values", () => {
|
||||
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(1);
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
normalizeTarget: (raw: string) => raw.trim().toUpperCase(),
|
||||
},
|
||||
@@ -137,7 +161,7 @@ describe("resolveNormalizedTargetInput", () => {
|
||||
describe("looksLikeTargetId", () => {
|
||||
it("uses plugin looksLikeId when available", () => {
|
||||
const pluginLooksLikeId = vi.fn((raw: string, normalized: string) => raw !== normalized);
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: pluginLooksLikeId,
|
||||
@@ -158,17 +182,38 @@ describe("looksLikeTargetId", () => {
|
||||
it.each(["channel:C123", "@alice", "#general", "+15551234567", "conversation:abc", "foo@thread"])(
|
||||
"falls back to built-in id-like heuristics for %s",
|
||||
(raw) => {
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
getChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
expect(looksLikeTargetId({ channel: "workspace", raw })).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("uses bundled/catalog target id detection when the channel is not loaded", () => {
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: (raw: string, normalized?: string) =>
|
||||
raw === "-1001234567890:topic:42" && normalized === "telegram:-1001234567890:topic:42",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
looksLikeTargetId({
|
||||
channel: "telegram",
|
||||
raw: "-1001234567890:topic:42",
|
||||
normalized: "telegram:-1001234567890:topic:42",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeResolvePluginMessagingTarget", () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
|
||||
it("returns undefined when requireIdLike is set and the target is not id-like", async () => {
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: () => false,
|
||||
@@ -194,7 +239,7 @@ describe("maybeResolvePluginMessagingTarget", () => {
|
||||
kind: "group",
|
||||
display: "general",
|
||||
});
|
||||
getChannelPluginMock
|
||||
getLoadedChannelPluginMock
|
||||
.mockReturnValueOnce({
|
||||
messaging: {
|
||||
normalizeTarget: (raw: string) => raw.trim().toUpperCase(),
|
||||
@@ -234,7 +279,7 @@ describe("maybeResolvePluginMessagingTarget", () => {
|
||||
describe("buildTargetResolverSignature", () => {
|
||||
it("builds stable signatures from resolver hint and looksLikeId source", () => {
|
||||
const looksLikeId = (value: string) => value.startsWith("C");
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
hint: "Use channel id",
|
||||
@@ -244,7 +289,7 @@ describe("buildTargetResolverSignature", () => {
|
||||
});
|
||||
|
||||
const first = buildTargetResolverSignature("workspace");
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
hint: "Use channel id",
|
||||
@@ -258,7 +303,7 @@ describe("buildTargetResolverSignature", () => {
|
||||
});
|
||||
|
||||
it("changes when resolver metadata changes", () => {
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
hint: "Use channel id",
|
||||
@@ -268,7 +313,7 @@ describe("buildTargetResolverSignature", () => {
|
||||
});
|
||||
const first = buildTargetResolverSignature("workspace");
|
||||
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
hint: "Use user id",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import { getLoadedChannelPluginForRead } from "../../channels/plugins/registry-loaded-read.js";
|
||||
import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js";
|
||||
import type { ChannelDirectoryEntryKind, ChannelId } from "../../channels/plugins/types.public.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { getActivePluginChannelRegistryVersion } from "../../plugins/runtime.js";
|
||||
@@ -19,6 +21,10 @@ type TargetNormalizerCacheEntry = {
|
||||
|
||||
const targetNormalizerCacheByChannelId = new Map<string, TargetNormalizerCacheEntry>();
|
||||
|
||||
function resolveChannelPluginForTargetRead(channelId: ChannelId): ChannelPlugin | undefined {
|
||||
return getLoadedChannelPluginForRead(channelId) ?? getChannelPlugin(channelId);
|
||||
}
|
||||
|
||||
function resetTargetNormalizerCacheForTests(): void {
|
||||
targetNormalizerCacheByChannelId.clear();
|
||||
}
|
||||
@@ -33,7 +39,7 @@ function resolveTargetNormalizer(channelId: ChannelId): TargetNormalizer {
|
||||
if (cached && cached.version === version) {
|
||||
return cached.normalizer;
|
||||
}
|
||||
const plugin = getLoadedChannelPluginForRead(channelId);
|
||||
const plugin = resolveChannelPluginForTargetRead(channelId);
|
||||
const normalizer = plugin?.messaging?.normalizeTarget;
|
||||
targetNormalizerCacheByChannelId.set(channelId, {
|
||||
version,
|
||||
@@ -85,7 +91,7 @@ export function looksLikeTargetId(params: {
|
||||
}): boolean {
|
||||
const normalizedInput =
|
||||
params.normalized ?? normalizeTargetForProvider(params.channel, params.raw);
|
||||
const lookup = getLoadedChannelPluginForRead(params.channel)?.messaging?.targetResolver
|
||||
const lookup = resolveChannelPluginForTargetRead(params.channel)?.messaging?.targetResolver
|
||||
?.looksLikeId;
|
||||
if (lookup) {
|
||||
return lookup(params.raw, normalizedInput ?? params.raw);
|
||||
@@ -117,7 +123,7 @@ export async function maybeResolvePluginMessagingTarget(params: {
|
||||
if (!normalizedInput) {
|
||||
return undefined;
|
||||
}
|
||||
const resolver = getLoadedChannelPluginForRead(params.channel)?.messaging?.targetResolver;
|
||||
const resolver = resolveChannelPluginForTargetRead(params.channel)?.messaging?.targetResolver;
|
||||
if (!resolver?.resolveTarget) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -150,7 +156,7 @@ export async function maybeResolvePluginMessagingTarget(params: {
|
||||
}
|
||||
|
||||
export function buildTargetResolverSignature(channel: ChannelId): string {
|
||||
const plugin = getLoadedChannelPluginForRead(channel);
|
||||
const plugin = resolveChannelPluginForTargetRead(channel);
|
||||
const resolver = plugin?.messaging?.targetResolver;
|
||||
const hint = resolver?.hint ?? "";
|
||||
const looksLike = resolver?.looksLikeId;
|
||||
|
||||
@@ -14,17 +14,18 @@ const mocks = vi.hoisted(() => ({
|
||||
listGroupsLive: vi.fn(),
|
||||
resolveTarget: vi.fn(),
|
||||
getChannelPlugin: vi.fn(),
|
||||
getLoadedChannelPlugin: vi.fn(),
|
||||
getActivePluginChannelRegistryVersion: vi.fn(() => 1),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getLoadedChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args),
|
||||
getLoadedChannelPlugin: (...args: unknown[]) => mocks.getLoadedChannelPlugin(...args),
|
||||
getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args),
|
||||
normalizeChannelId: (value: string) => value,
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/registry-loaded-read.js", () => ({
|
||||
getLoadedChannelPluginForRead: (...args: unknown[]) => mocks.getChannelPlugin(...args),
|
||||
getLoadedChannelPluginForRead: (...args: unknown[]) => mocks.getLoadedChannelPlugin(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/runtime.js", () => ({
|
||||
@@ -45,6 +46,10 @@ beforeEach(() => {
|
||||
mocks.listGroupsLive.mockReset();
|
||||
mocks.resolveTarget.mockReset();
|
||||
mocks.getChannelPlugin.mockReset();
|
||||
mocks.getLoadedChannelPlugin.mockReset();
|
||||
mocks.getLoadedChannelPlugin.mockImplementation((...args: unknown[]) =>
|
||||
mocks.getChannelPlugin(...args),
|
||||
);
|
||||
mocks.getActivePluginChannelRegistryVersion.mockReset();
|
||||
mocks.getActivePluginChannelRegistryVersion.mockReturnValue(1);
|
||||
resetDirectoryCache();
|
||||
@@ -153,6 +158,39 @@ describe("resolveMessagingTarget (directory fallback)", () => {
|
||||
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses catalog plugin target grammar for unloaded numeric topic ids", async () => {
|
||||
mocks.getLoadedChannelPlugin.mockReturnValue(undefined);
|
||||
mocks.getChannelPlugin.mockReturnValue({
|
||||
messaging: {
|
||||
normalizeTarget: (raw: string) =>
|
||||
raw.trim() === "-1001234567890:topic:42"
|
||||
? "telegram:-1001234567890:topic:42"
|
||||
: raw.trim() || undefined,
|
||||
inferTargetChatType: ({ to }: { to: string }) => (to.includes("-100") ? "group" : "direct"),
|
||||
targetResolver: {
|
||||
looksLikeId: (_raw: string, normalized?: string) =>
|
||||
normalized === "telegram:-1001234567890:topic:42",
|
||||
hint: "<chatId>",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await expectOkResolution({
|
||||
cfg,
|
||||
channel: "telegram",
|
||||
input: "-1001234567890:topic:42",
|
||||
});
|
||||
|
||||
expect(result.target).toEqual({
|
||||
to: "telegram:-1001234567890:topic:42",
|
||||
kind: "group",
|
||||
display: "telegram:-1001234567890:topic:42",
|
||||
source: "normalized",
|
||||
});
|
||||
expect(mocks.listGroups).not.toHaveBeenCalled();
|
||||
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses plugin chat-type inference for directory lookups and plugin fallback on miss", async () => {
|
||||
mocks.getChannelPlugin.mockReturnValue({
|
||||
directory: {
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("tsdown config", () => {
|
||||
"media-understanding/apply.runtime",
|
||||
"index",
|
||||
"commands/status.summary.runtime",
|
||||
"auto-reply/reply/provider-dispatcher",
|
||||
"provider-dispatcher.runtime",
|
||||
"plugins/provider-discovery.runtime",
|
||||
"plugins/provider-runtime.runtime",
|
||||
"plugins/runtime/index",
|
||||
@@ -112,12 +112,12 @@ describe("tsdown config", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps reply dispatcher lazy runtime behind one stable dist entry", () => {
|
||||
it("keeps reply dispatcher lazy runtime behind one root stable dist entry", () => {
|
||||
const distGraph = unifiedDistGraph();
|
||||
|
||||
expect(entrySources(distGraph as TsdownConfigEntry)).toEqual(
|
||||
expect.objectContaining({
|
||||
"auto-reply/reply/provider-dispatcher": "src/auto-reply/reply/provider-dispatcher.ts",
|
||||
"provider-dispatcher.runtime": "src/auto-reply/reply/provider-dispatcher.runtime.ts",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,12 +15,12 @@ export type { ReplyPayload } from "./reply-payload.js";
|
||||
export const dispatchReplyWithBufferedBlockDispatcher: DispatchReplyWithBufferedBlockDispatcher =
|
||||
async (params) => {
|
||||
const { dispatchReplyWithBufferedBlockDispatcher: dispatch } =
|
||||
await import("../auto-reply/reply/provider-dispatcher.js");
|
||||
await import("../auto-reply/reply/provider-dispatcher.runtime.js");
|
||||
return await dispatch(params);
|
||||
};
|
||||
|
||||
export const dispatchReplyWithDispatcher: DispatchReplyWithDispatcher = async (params) => {
|
||||
const { dispatchReplyWithDispatcher: dispatch } =
|
||||
await import("../auto-reply/reply/provider-dispatcher.js");
|
||||
await import("../auto-reply/reply/provider-dispatcher.runtime.js");
|
||||
return await dispatch(params);
|
||||
};
|
||||
|
||||
@@ -227,6 +227,38 @@ describe("runtime postbuild static assets", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rewrites reply-dispatch imports to the stable provider dispatcher runtime alias", async () => {
|
||||
const rootDir = createTempDir("openclaw-runtime-postbuild-");
|
||||
const distDir = path.join(rootDir, "dist");
|
||||
await fs.mkdir(distDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(distDir, "provider-dispatcher.runtime-NewHash.js"),
|
||||
'export * from "./provider-dispatcher-ImplHash.js";\n',
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(distDir, "reply-dispatch-runtime-OldHash.js"),
|
||||
['const dispatcher = () => import("./provider-dispatcher.runtime-NewHash.js");', ""].join(
|
||||
"\n",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
rewriteRootRuntimeImportsToStableAliases({ rootDir });
|
||||
writeStableRootRuntimeAliases({ rootDir });
|
||||
writeLegacyRootRuntimeCompatAliases({ rootDir });
|
||||
|
||||
expect(await fs.readFile(path.join(distDir, "reply-dispatch-runtime-OldHash.js"), "utf8")).toBe(
|
||||
['const dispatcher = () => import("./provider-dispatcher.runtime.js");', ""].join("\n"),
|
||||
);
|
||||
expect(await fs.readFile(path.join(distDir, "provider-dispatcher.runtime.js"), "utf8")).toBe(
|
||||
'export * from "./provider-dispatcher.runtime-NewHash.js";\n',
|
||||
);
|
||||
expect(await fs.readFile(path.join(distDir, "provider-dispatcher-6EQEtc-t.js"), "utf8")).toBe(
|
||||
'export * from "./provider-dispatcher.runtime.js";\n',
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps hashed imports when a stable runtime alias would collide", async () => {
|
||||
const rootDir = createTempDir("openclaw-runtime-postbuild-");
|
||||
const distDir = path.join(rootDir, "dist");
|
||||
@@ -294,8 +326,8 @@ describe("runtime postbuild static assets", () => {
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(distDir, "provider-dispatcher.js"),
|
||||
'export * from "./provider-dispatcher-NewHash.js";\n',
|
||||
path.join(distDir, "provider-dispatcher.runtime.js"),
|
||||
'export * from "./provider-dispatcher.runtime-NewHash.js";\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
@@ -308,7 +340,7 @@ describe("runtime postbuild static assets", () => {
|
||||
await fs.readFile(path.join(distDir, "runtime-plugins.runtime-CNAfmQRG.js"), "utf8"),
|
||||
).toBe('export * from "./runtime-plugins.runtime.js";\n');
|
||||
expect(await fs.readFile(path.join(distDir, "provider-dispatcher-6EQEtc-t.js"), "utf8")).toBe(
|
||||
'export * from "./provider-dispatcher.js";\n',
|
||||
'export * from "./provider-dispatcher.runtime.js";\n',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ function buildCoreDistEntries(): Record<string, string> {
|
||||
"agents/model-catalog.runtime": "src/agents/model-catalog.runtime.ts",
|
||||
"agents/models-config.runtime": "src/agents/models-config.runtime.ts",
|
||||
"cli/gateway-lifecycle.runtime": "src/cli/gateway-cli/lifecycle.runtime.ts",
|
||||
"provider-dispatcher.runtime": "src/auto-reply/reply/provider-dispatcher.runtime.ts",
|
||||
"server-close.runtime": "src/gateway/server-close.runtime.ts",
|
||||
"plugins/memory-state": "src/plugins/memory-state.ts",
|
||||
"subagent-registry.runtime": "src/agents/subagent-registry.runtime.ts",
|
||||
@@ -242,7 +243,6 @@ function buildDockerE2eHarnessEntries(): Record<string, string> {
|
||||
"src/agents/pi-embedded-runner/effective-tool-policy.ts",
|
||||
"agents/pi-embedded-runner/run/runtime-context-prompt":
|
||||
"src/agents/pi-embedded-runner/run/runtime-context-prompt.ts",
|
||||
"auto-reply/reply/provider-dispatcher": "src/auto-reply/reply/provider-dispatcher.ts",
|
||||
"auto-reply/reply/commands-crestodian": "src/auto-reply/reply/commands-crestodian.ts",
|
||||
"cli/run-main": "src/cli/run-main.ts",
|
||||
"commitments/runtime": "src/commitments/runtime.ts",
|
||||
|
||||
Reference in New Issue
Block a user