mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 19:01:44 +00:00
fix: repair ci test and loader regressions
This commit is contained in:
@@ -2,6 +2,12 @@ import { Type } from "@sinclair/typebox";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import { createChannelReplyPipeline } from "../runtime-api.js";
|
||||
|
||||
vi.mock("../../../src/config/bundled-channel-config-runtime.js", () => ({
|
||||
getBundledChannelRuntimeMap: () => new Map(),
|
||||
getBundledChannelConfigSchemaMap: () => new Map(),
|
||||
}));
|
||||
|
||||
const { sendMessageMattermostMock, mockFetchGuard } = vi.hoisted(() => ({
|
||||
sendMessageMattermostMock: vi.fn(),
|
||||
mockFetchGuard: vi.fn(async (p: { url: string; init?: RequestInit }) => {
|
||||
@@ -19,14 +25,15 @@ vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
|
||||
return { ...original, fetchWithSsrFGuard: mockFetchGuard };
|
||||
});
|
||||
|
||||
import { mattermostPlugin } from "./channel.js";
|
||||
import { resetMattermostReactionBotUserCacheForTests } from "./mattermost/reactions.js";
|
||||
import {
|
||||
createMattermostReactionFetchMock,
|
||||
createMattermostTestConfig,
|
||||
withMockedGlobalFetch,
|
||||
} from "./mattermost/reactions.test-helpers.js";
|
||||
|
||||
let mattermostPlugin: typeof import("./channel.js").mattermostPlugin;
|
||||
let resetMattermostReactionBotUserCacheForTests: typeof import("./mattermost/reactions.js").resetMattermostReactionBotUserCacheForTests;
|
||||
|
||||
type MattermostHandleAction = NonNullable<
|
||||
NonNullable<typeof mattermostPlugin.actions>["handleAction"]
|
||||
>;
|
||||
@@ -93,7 +100,10 @@ function createMattermostActionContext(
|
||||
}
|
||||
|
||||
describe("mattermostPlugin", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ mattermostPlugin } = await import("./channel.js"));
|
||||
({ resetMattermostReactionBotUserCacheForTests } = await import("./mattermost/reactions.js"));
|
||||
sendMessageMattermostMock.mockReset();
|
||||
sendMessageMattermostMock.mockResolvedValue({
|
||||
messageId: "post-1",
|
||||
|
||||
@@ -28,11 +28,15 @@ vi.mock("./client.js", () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { listMattermostDirectoryGroups, listMattermostDirectoryPeers } from "./directory.js";
|
||||
let listMattermostDirectoryGroups: typeof import("./directory.js").listMattermostDirectoryGroups;
|
||||
let listMattermostDirectoryPeers: typeof import("./directory.js").listMattermostDirectoryPeers;
|
||||
|
||||
describe("mattermost directory", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
({ listMattermostDirectoryGroups, listMattermostDirectoryPeers } =
|
||||
await import("./directory.js"));
|
||||
});
|
||||
|
||||
it("deduplicates channels across enabled accounts and skips failing accounts", async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChannelSetupAdapter } from "openclaw/plugin-sdk/channel-setup";
|
||||
import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup";
|
||||
import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { resolveMattermostAccount, type ResolvedMattermostAccount } from "./mattermost/accounts.js";
|
||||
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js";
|
||||
import plugin from "../index.js";
|
||||
import type { OpenClawConfig, OpenClawPluginApi } from "../runtime-api.js";
|
||||
import { mattermostSetupWizard } from "./setup-surface.js";
|
||||
|
||||
vi.mock("../../../src/config/bundled-channel-config-runtime.js", () => ({
|
||||
getBundledChannelRuntimeMap: () => new Map(),
|
||||
getBundledChannelConfigSchemaMap: () => new Map(),
|
||||
}));
|
||||
|
||||
const resolveMattermostAccount = vi.hoisted(() => vi.fn());
|
||||
const normalizeMattermostBaseUrl = vi.hoisted(() => vi.fn((value: string | undefined) => value));
|
||||
@@ -51,7 +54,16 @@ function createApi(
|
||||
});
|
||||
}
|
||||
|
||||
let plugin: typeof import("../index.js").default;
|
||||
let mattermostSetupWizard: typeof import("./setup-surface.js").mattermostSetupWizard;
|
||||
|
||||
describe("mattermost setup", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ default: plugin } = await import("../index.js"));
|
||||
({ mattermostSetupWizard } = await import("./setup-surface.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resolveMattermostAccount.mockReset();
|
||||
normalizeMattermostBaseUrl.mockReset();
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { nextcloudTalkPlugin } from "./channel.js";
|
||||
import { NextcloudTalkConfigSchema } from "./config-schema.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
escapeNextcloudTalkMarkdown,
|
||||
formatNextcloudTalkCodeBlock,
|
||||
@@ -28,6 +26,11 @@ import {
|
||||
} from "./signature.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
vi.mock("../../../src/config/bundled-channel-config-runtime.js", () => ({
|
||||
getBundledChannelRuntimeMap: () => new Map(),
|
||||
getBundledChannelConfigSchemaMap: () => new Map(),
|
||||
}));
|
||||
|
||||
const fetchWithSsrFGuard = vi.hoisted(() => vi.fn());
|
||||
const readFileSync = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -39,6 +42,14 @@ vi.mock("../runtime-api.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
fetchWithSsrFGuard,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
return {
|
||||
@@ -48,6 +59,14 @@ vi.mock("node:fs", async (importOriginal) => {
|
||||
});
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
let nextcloudTalkPlugin: typeof import("./channel.js").nextcloudTalkPlugin;
|
||||
let NextcloudTalkConfigSchema: typeof import("./config-schema.js").NextcloudTalkConfigSchema;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ nextcloudTalkPlugin } = await import("./channel.js"));
|
||||
({ NextcloudTalkConfigSchema } = await import("./config-schema.js"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
fetchWithSsrFGuard.mockReset();
|
||||
@@ -406,6 +425,7 @@ describe("nextcloud talk core", () => {
|
||||
});
|
||||
|
||||
it("resolves direct rooms from the room info endpoint", async () => {
|
||||
vi.resetModules();
|
||||
const release = vi.fn(async () => {});
|
||||
fetchWithSsrFGuard.mockResolvedValue({
|
||||
response: {
|
||||
@@ -445,6 +465,7 @@ describe("nextcloud talk core", () => {
|
||||
});
|
||||
|
||||
it("reads the api password from a file and logs non-ok room info responses", async () => {
|
||||
vi.resetModules();
|
||||
const release = vi.fn(async () => {});
|
||||
const log = vi.fn();
|
||||
const error = vi.fn();
|
||||
@@ -480,6 +501,7 @@ describe("nextcloud talk core", () => {
|
||||
});
|
||||
|
||||
it("returns undefined from room info without credentials or base url", async () => {
|
||||
vi.resetModules();
|
||||
const { resolveNextcloudTalkRoomKind } = await import("./room-info.js");
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -2,21 +2,18 @@ import type { ChannelSetupAdapter, ChannelSetupInput } from "openclaw/plugin-sdk
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
createSetupInputPresenceValidator,
|
||||
patchScopedAccountConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
mergeAllowFromEntries,
|
||||
createTopLevelChannelDmPolicy,
|
||||
promptParsedAllowFromForAccount,
|
||||
resolveSetupAccountId,
|
||||
setSetupChannelEnabled,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type { ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup";
|
||||
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup";
|
||||
import type { WizardPrompter } from "openclaw/plugin-sdk/setup";
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { applyAccountNameToChannelSection, patchScopedAccountConfig } from "../runtime-api.js";
|
||||
import {
|
||||
listNextcloudTalkAccountIds,
|
||||
resolveDefaultNextcloudTalkAccountId,
|
||||
|
||||
@@ -14,19 +14,14 @@ import {
|
||||
startAccountAndTrackLifecycle,
|
||||
waitForStartedMocks,
|
||||
} from "../../../test/helpers/extensions/start-account-lifecycle.js";
|
||||
import { resolveNextcloudTalkAccount, type ResolvedNextcloudTalkAccount } from "./accounts.js";
|
||||
import { nextcloudTalkPlugin } from "./channel.js";
|
||||
import {
|
||||
clearNextcloudTalkAccountFields,
|
||||
nextcloudTalkDmPolicy,
|
||||
nextcloudTalkSetupAdapter,
|
||||
normalizeNextcloudTalkBaseUrl,
|
||||
setNextcloudTalkAccountConfig,
|
||||
validateNextcloudTalkBaseUrl,
|
||||
} from "./setup-core.js";
|
||||
import { nextcloudTalkSetupWizard } from "./setup-surface.js";
|
||||
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
vi.mock("../../../src/config/bundled-channel-config-runtime.js", () => ({
|
||||
getBundledChannelRuntimeMap: () => new Map(),
|
||||
getBundledChannelConfigSchemaMap: () => new Map(),
|
||||
}));
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
monitorNextcloudTalkProvider: vi.fn(),
|
||||
loadConfig: vi.fn(),
|
||||
@@ -49,9 +44,13 @@ vi.mock("./monitor.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getNextcloudTalkRuntime: () => createSendCfgThreadingRuntime(hoisted),
|
||||
}));
|
||||
vi.mock("./runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./runtime.js")>();
|
||||
return {
|
||||
...actual,
|
||||
getNextcloudTalkRuntime: () => createSendCfgThreadingRuntime(hoisted),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./accounts.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./accounts.js")>();
|
||||
@@ -80,7 +79,17 @@ vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
|
||||
const accountsActual = await vi.importActual<typeof import("./accounts.js")>("./accounts.js");
|
||||
hoisted.resolveNextcloudTalkAccount.mockImplementation(accountsActual.resolveNextcloudTalkAccount);
|
||||
|
||||
import { sendMessageNextcloudTalk, sendReactionNextcloudTalk } from "./send.js";
|
||||
let resolveNextcloudTalkAccount: typeof import("./accounts.js").resolveNextcloudTalkAccount;
|
||||
let nextcloudTalkPlugin: typeof import("./channel.js").nextcloudTalkPlugin;
|
||||
let clearNextcloudTalkAccountFields: typeof import("./setup-core.js").clearNextcloudTalkAccountFields;
|
||||
let nextcloudTalkDmPolicy: typeof import("./setup-core.js").nextcloudTalkDmPolicy;
|
||||
let nextcloudTalkSetupAdapter: typeof import("./setup-core.js").nextcloudTalkSetupAdapter;
|
||||
let normalizeNextcloudTalkBaseUrl: typeof import("./setup-core.js").normalizeNextcloudTalkBaseUrl;
|
||||
let setNextcloudTalkAccountConfig: typeof import("./setup-core.js").setNextcloudTalkAccountConfig;
|
||||
let validateNextcloudTalkBaseUrl: typeof import("./setup-core.js").validateNextcloudTalkBaseUrl;
|
||||
let nextcloudTalkSetupWizard: typeof import("./setup-surface.js").nextcloudTalkSetupWizard;
|
||||
let sendMessageNextcloudTalk: typeof import("./send.js").sendMessageNextcloudTalk;
|
||||
let sendReactionNextcloudTalk: typeof import("./send.js").sendReactionNextcloudTalk;
|
||||
|
||||
function buildAccount(): ResolvedNextcloudTalkAccount {
|
||||
return {
|
||||
@@ -114,6 +123,25 @@ function startNextcloudAccount(abortSignal?: AbortSignal) {
|
||||
}
|
||||
|
||||
describe("nextcloud talk setup", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ resolveNextcloudTalkAccount } = await import("./accounts.js"));
|
||||
({ nextcloudTalkPlugin } = await import("./channel.js"));
|
||||
({
|
||||
clearNextcloudTalkAccountFields,
|
||||
nextcloudTalkDmPolicy,
|
||||
nextcloudTalkSetupAdapter,
|
||||
normalizeNextcloudTalkBaseUrl,
|
||||
setNextcloudTalkAccountConfig,
|
||||
validateNextcloudTalkBaseUrl,
|
||||
} = await import("./setup-core.js"));
|
||||
({ nextcloudTalkSetupWizard } = await import("./setup-surface.js"));
|
||||
({ sendMessageNextcloudTalk, sendReactionNextcloudTalk } = await import("./send.js"));
|
||||
hoisted.resolveNextcloudTalkAccount.mockImplementation(
|
||||
accountsActual.resolveNextcloudTalkAccount,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
hoisted.resolveNextcloudTalkAccount.mockImplementation(
|
||||
|
||||
@@ -5,20 +5,18 @@ import {
|
||||
createPatchedAccountSetupAdapter,
|
||||
createSetupInputPresenceValidator,
|
||||
createTopLevelChannelDmPolicy,
|
||||
normalizeE164,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForAccount,
|
||||
setAccountAllowFromForChannel,
|
||||
setSetupChannelEnabled,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type {
|
||||
ChannelSetupAdapter,
|
||||
ChannelSetupWizard,
|
||||
ChannelSetupWizardTextInput,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
type ChannelSetupAdapter,
|
||||
type ChannelSetupWizard,
|
||||
type ChannelSetupWizardTextInput,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatCliCommand, formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getChatChannelMeta,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/setup";
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sendMessageSlackMock = vi.fn();
|
||||
const hasHooksMock = vi.fn();
|
||||
const runMessageSendingMock = vi.fn();
|
||||
const sendMessageSlackMock = vi.hoisted(() => vi.fn());
|
||||
const hasHooksMock = vi.hoisted(() => vi.fn());
|
||||
const runMessageSendingMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
sendMessageSlack: (...args: unknown[]) => sendMessageSlackMock(...args),
|
||||
@@ -15,7 +15,7 @@ vi.mock("openclaw/plugin-sdk/plugin-runtime", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
import { slackOutbound } from "./outbound-adapter.js";
|
||||
let slackOutbound: typeof import("./outbound-adapter.js").slackOutbound;
|
||||
|
||||
describe("slackOutbound", () => {
|
||||
const cfg = {
|
||||
@@ -27,11 +27,13 @@ describe("slackOutbound", () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
sendMessageSlackMock.mockReset();
|
||||
hasHooksMock.mockReset();
|
||||
runMessageSendingMock.mockReset();
|
||||
hasHooksMock.mockReturnValue(false);
|
||||
({ slackOutbound } = await import("./outbound-adapter.js"));
|
||||
});
|
||||
|
||||
it("sends payload media first, then finalizes with blocks", async () => {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
|
||||
const sendMessageSlackMock = vi.hoisted(() => vi.fn());
|
||||
const getGlobalHookRunnerMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
sendMessageSlack: vi.fn().mockResolvedValue({ messageId: "1234.5678", channelId: "C123" }),
|
||||
sendMessageSlack: sendMessageSlackMock,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/plugin-runtime", () => ({
|
||||
getGlobalHookRunner: vi.fn(),
|
||||
getGlobalHookRunner: getGlobalHookRunnerMock,
|
||||
}));
|
||||
|
||||
import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import { slackOutbound } from "./outbound-adapter.js";
|
||||
import { sendMessageSlack } from "./send.js";
|
||||
let slackOutbound: typeof import("./outbound-adapter.js").slackOutbound;
|
||||
|
||||
type SlackSendTextCtx = {
|
||||
to: string;
|
||||
@@ -64,12 +65,19 @@ const expectSlackSendCalledWith = (
|
||||
cfg: expect.any(Object),
|
||||
...(options?.identity ? { identity: expect.objectContaining(options.identity) } : {}),
|
||||
};
|
||||
expect(sendMessageSlack).toHaveBeenCalledWith("C123", text, expect.objectContaining(expected));
|
||||
expect(sendMessageSlackMock).toHaveBeenCalledWith(
|
||||
"C123",
|
||||
text,
|
||||
expect.objectContaining(expected),
|
||||
);
|
||||
};
|
||||
|
||||
describe("slack outbound hook wiring", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
sendMessageSlackMock.mockResolvedValue({ messageId: "1234.5678", channelId: "C123" });
|
||||
({ slackOutbound } = await import("./outbound-adapter.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -77,14 +85,14 @@ describe("slack outbound hook wiring", () => {
|
||||
});
|
||||
|
||||
it("calls send without hooks when no hooks registered", async () => {
|
||||
vi.mocked(getGlobalHookRunner).mockReturnValue(null);
|
||||
getGlobalHookRunnerMock.mockReturnValue(null);
|
||||
|
||||
await sendSlackTextWithDefaults({ text: "hello" });
|
||||
expectSlackSendCalledWith("hello");
|
||||
});
|
||||
|
||||
it("forwards identity opts when present", async () => {
|
||||
vi.mocked(getGlobalHookRunner).mockReturnValue(null);
|
||||
getGlobalHookRunnerMock.mockReturnValue(null);
|
||||
|
||||
await sendSlackTextWithDefaults({
|
||||
text: "hello",
|
||||
@@ -101,7 +109,7 @@ describe("slack outbound hook wiring", () => {
|
||||
});
|
||||
|
||||
it("forwards icon_emoji only when icon_url is absent", async () => {
|
||||
vi.mocked(getGlobalHookRunner).mockReturnValue(null);
|
||||
getGlobalHookRunnerMock.mockReturnValue(null);
|
||||
|
||||
await sendSlackTextWithDefaults({
|
||||
text: "hello",
|
||||
@@ -118,7 +126,7 @@ describe("slack outbound hook wiring", () => {
|
||||
hasHooks: vi.fn().mockReturnValue(true),
|
||||
runMessageSending: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
vi.mocked(getGlobalHookRunner).mockReturnValue(mockRunner as never);
|
||||
getGlobalHookRunnerMock.mockReturnValue(mockRunner);
|
||||
|
||||
await sendSlackTextWithDefaults({ text: "hello" });
|
||||
|
||||
@@ -135,11 +143,11 @@ describe("slack outbound hook wiring", () => {
|
||||
hasHooks: vi.fn().mockReturnValue(true),
|
||||
runMessageSending: vi.fn().mockResolvedValue({ cancel: true }),
|
||||
};
|
||||
vi.mocked(getGlobalHookRunner).mockReturnValue(mockRunner as never);
|
||||
getGlobalHookRunnerMock.mockReturnValue(mockRunner);
|
||||
|
||||
const result = await sendSlackTextWithDefaults({ text: "hello" });
|
||||
|
||||
expect(sendMessageSlack).not.toHaveBeenCalled();
|
||||
expect(sendMessageSlackMock).not.toHaveBeenCalled();
|
||||
expect(result.channel).toBe("slack");
|
||||
});
|
||||
|
||||
@@ -148,7 +156,7 @@ describe("slack outbound hook wiring", () => {
|
||||
hasHooks: vi.fn().mockReturnValue(true),
|
||||
runMessageSending: vi.fn().mockResolvedValue({ content: "modified" }),
|
||||
};
|
||||
vi.mocked(getGlobalHookRunner).mockReturnValue(mockRunner as never);
|
||||
getGlobalHookRunnerMock.mockReturnValue(mockRunner);
|
||||
|
||||
await sendSlackTextWithDefaults({ text: "original" });
|
||||
expectSlackSendCalledWith("modified");
|
||||
@@ -159,11 +167,11 @@ describe("slack outbound hook wiring", () => {
|
||||
hasHooks: vi.fn().mockReturnValue(false),
|
||||
runMessageSending: vi.fn(),
|
||||
};
|
||||
vi.mocked(getGlobalHookRunner).mockReturnValue(mockRunner as never);
|
||||
getGlobalHookRunnerMock.mockReturnValue(mockRunner);
|
||||
|
||||
await sendSlackTextWithDefaults({ text: "hello" });
|
||||
|
||||
expect(mockRunner.runMessageSending).not.toHaveBeenCalled();
|
||||
expect(sendMessageSlack).toHaveBeenCalled();
|
||||
expect(sendMessageSlackMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,15 +2,29 @@ import type { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/ch
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const createChannelPairingChallengeIssuerMock = vi.hoisted(() => vi.fn());
|
||||
const upsertChannelPairingRequestMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const upsertChannelPairingRequestMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ code: "123456", created: true })),
|
||||
);
|
||||
const withTelegramApiErrorLoggingMock = vi.hoisted(() => vi.fn(async ({ fn }) => await fn()));
|
||||
const createPairingPrefixStripperMock = vi.hoisted(
|
||||
() => (prefix: RegExp, normalize: (value: string) => string) => (value: string) =>
|
||||
normalize(value.replace(prefix, "")),
|
||||
);
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-pairing", () => ({
|
||||
createChannelPairingChallengeIssuer: createChannelPairingChallengeIssuerMock,
|
||||
createPairingPrefixStripper: createPairingPrefixStripperMock,
|
||||
createLoggedPairingApprovalNotifier: () => undefined,
|
||||
createTextPairingAdapter: () => undefined,
|
||||
createChannelPairingController: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
|
||||
upsertChannelPairingRequest: upsertChannelPairingRequestMock,
|
||||
createStaticReplyToModeResolver: (mode: string) => () => mode,
|
||||
createTopLevelChannelReplyToModeResolver: () => () => "off",
|
||||
createScopedAccountReplyToModeResolver: () => () => "off",
|
||||
resolvePinnedMainDmOwnerFromAllowlist: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("./api-logging.js", () => ({
|
||||
@@ -56,6 +70,7 @@ describe("enforceTelegramDmAccess", () => {
|
||||
accountId: "main",
|
||||
bot: bot as never,
|
||||
logger: { info: vi.fn() },
|
||||
upsertPairingRequest: upsertChannelPairingRequestMock,
|
||||
});
|
||||
|
||||
expect(allowed).toBe(true);
|
||||
@@ -72,6 +87,7 @@ describe("enforceTelegramDmAccess", () => {
|
||||
accountId: "main",
|
||||
bot: { api: { sendMessage: vi.fn(async () => undefined) } } as never,
|
||||
logger: { info: vi.fn() },
|
||||
upsertPairingRequest: upsertChannelPairingRequestMock,
|
||||
});
|
||||
|
||||
expect(allowed).toBe(false);
|
||||
@@ -87,6 +103,7 @@ describe("enforceTelegramDmAccess", () => {
|
||||
accountId: "main",
|
||||
bot: { api: { sendMessage: vi.fn(async () => undefined) } } as never,
|
||||
logger: { info: vi.fn() },
|
||||
upsertPairingRequest: upsertChannelPairingRequestMock,
|
||||
});
|
||||
|
||||
expect(allowed).toBe(true);
|
||||
@@ -116,6 +133,7 @@ describe("enforceTelegramDmAccess", () => {
|
||||
accountId: "main",
|
||||
bot: { api: { sendMessage } } as never,
|
||||
logger,
|
||||
upsertPairingRequest: upsertChannelPairingRequestMock,
|
||||
});
|
||||
|
||||
expect(allowed).toBe(false);
|
||||
|
||||
@@ -56,20 +56,24 @@ vi.mock("undici", () => ({
|
||||
setGlobalDispatcher,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
createSubsystemLogger: () => ({
|
||||
info: loggerInfo,
|
||||
debug: loggerDebug,
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
child: () => ({
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/runtime-env")>();
|
||||
return {
|
||||
...actual,
|
||||
createSubsystemLogger: () => ({
|
||||
info: loggerInfo,
|
||||
debug: loggerDebug,
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
child: () => ({
|
||||
info: loggerInfo,
|
||||
debug: loggerDebug,
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
let resolveFetch: typeof import("../../../src/infra/fetch.js").resolveFetch;
|
||||
let resolveTelegramFetch: typeof import("./fetch.js").resolveTelegramFetch;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import { buildChannelConfigSchema } from "../src/channels/plugins/config-schema.js";
|
||||
import {
|
||||
buildPluginLoaderJitiOptions,
|
||||
resolvePluginSdkAliasFile,
|
||||
resolvePluginSdkScopedAliasMap,
|
||||
} from "../src/plugins/sdk-alias.js";
|
||||
|
||||
function isBuiltChannelConfigSchema(
|
||||
value: unknown,
|
||||
@@ -182,9 +189,88 @@ export async function loadChannelConfigSurfaceModule(
|
||||
options?: { repoRoot?: string },
|
||||
): Promise<{ schema: Record<string, unknown>; uiHints?: Record<string, unknown> } | null> {
|
||||
const repoRoot = options?.repoRoot ?? resolveRepoRoot();
|
||||
const loaderRepoRoot = resolveRepoRoot();
|
||||
const bunBuildChannelConfigSchemaUrl = pathToFileURL(
|
||||
path.join(loaderRepoRoot, "src/channels/plugins/config-schema.ts"),
|
||||
).href;
|
||||
const loadViaBun = (candidatePath: string) => {
|
||||
const script = `
|
||||
import { pathToFileURL } from "node:url";
|
||||
const { buildChannelConfigSchema } = await import(${JSON.stringify(bunBuildChannelConfigSchemaUrl)});
|
||||
const modulePath = process.env.OPENCLAW_CONFIG_SURFACE_MODULE;
|
||||
if (!modulePath) {
|
||||
throw new Error("missing OPENCLAW_CONFIG_SURFACE_MODULE");
|
||||
}
|
||||
const imported = await import(pathToFileURL(modulePath).href);
|
||||
const isBuilt = (value) => Boolean(
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
value.schema &&
|
||||
typeof value.schema === "object"
|
||||
);
|
||||
const resolve = (mod) => {
|
||||
for (const [name, value] of Object.entries(mod)) {
|
||||
if (name.endsWith("ChannelConfigSchema") && isBuilt(value)) return value;
|
||||
}
|
||||
for (const [name, value] of Object.entries(mod)) {
|
||||
if (!name.endsWith("ConfigSchema") || name.endsWith("AccountConfigSchema")) continue;
|
||||
if (isBuilt(value)) return value;
|
||||
if (value && typeof value === "object") return buildChannelConfigSchema(value);
|
||||
}
|
||||
for (const value of Object.values(mod)) {
|
||||
if (isBuilt(value)) return value;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
process.stdout.write(JSON.stringify(resolve(imported)));
|
||||
`;
|
||||
const result = spawnSync("bun", ["-e", script], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_CONFIG_SURFACE_MODULE: path.resolve(candidatePath),
|
||||
},
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || result.stdout || `bun loader failed for ${candidatePath}`);
|
||||
}
|
||||
return JSON.parse(result.stdout || "null") as {
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, unknown>;
|
||||
} | null;
|
||||
};
|
||||
const loadViaJiti = (candidatePath: string) => {
|
||||
const resolvedPath = path.resolve(candidatePath);
|
||||
const pluginSdkAlias = resolvePluginSdkAliasFile({
|
||||
srcFile: "root-alias.cjs",
|
||||
distFile: "root-alias.cjs",
|
||||
modulePath: resolvedPath,
|
||||
pluginSdkResolution: "src",
|
||||
});
|
||||
const aliasMap = {
|
||||
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
|
||||
...resolvePluginSdkScopedAliasMap({
|
||||
modulePath: resolvedPath,
|
||||
pluginSdkResolution: "src",
|
||||
}),
|
||||
};
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
interopDefault: true,
|
||||
tryNative: false,
|
||||
moduleCache: false,
|
||||
fsCache: false,
|
||||
});
|
||||
return jiti(resolvedPath) as Record<string, unknown>;
|
||||
};
|
||||
|
||||
try {
|
||||
const imported = (await import(pathToFileURL(modulePath).href)) as Record<string, unknown>;
|
||||
const bunLoaded = loadViaBun(modulePath);
|
||||
if (bunLoaded && isBuiltChannelConfigSchema(bunLoaded)) {
|
||||
return bunLoaded;
|
||||
}
|
||||
const imported = loadViaJiti(modulePath);
|
||||
return resolveConfigSchemaExport(imported);
|
||||
} catch (error) {
|
||||
if (!shouldRetryViaIsolatedCopy(error)) {
|
||||
@@ -193,9 +279,11 @@ export async function loadChannelConfigSurfaceModule(
|
||||
|
||||
const isolatedCopy = copyModuleImportGraphWithoutNodeModules({ modulePath, repoRoot });
|
||||
try {
|
||||
const imported = (await import(
|
||||
`${pathToFileURL(isolatedCopy.copiedModulePath).href}?isolated=${Date.now()}`
|
||||
)) as Record<string, unknown>;
|
||||
const bunLoaded = loadViaBun(isolatedCopy.copiedModulePath);
|
||||
if (bunLoaded && isBuiltChannelConfigSchema(bunLoaded)) {
|
||||
return bunLoaded;
|
||||
}
|
||||
const imported = loadViaJiti(isolatedCopy.copiedModulePath);
|
||||
return resolveConfigSchemaExport(imported);
|
||||
} finally {
|
||||
isolatedCopy.cleanup();
|
||||
|
||||
135
src/channels/chat-meta.ts
Normal file
135
src/channels/chat-meta.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "./ids.js";
|
||||
import type { ChannelMeta } from "./plugins/types.js";
|
||||
|
||||
export type ChatChannelMeta = ChannelMeta;
|
||||
|
||||
const WEBSITE_URL = "https://openclaw.ai";
|
||||
|
||||
const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
|
||||
telegram: {
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
selectionLabel: "Telegram (Bot API)",
|
||||
detailLabel: "Telegram Bot",
|
||||
docsPath: "/channels/telegram",
|
||||
docsLabel: "telegram",
|
||||
blurb: "simplest way to get started — register a bot with @BotFather and get going.",
|
||||
systemImage: "paperplane",
|
||||
selectionDocsPrefix: "",
|
||||
selectionDocsOmitLabel: true,
|
||||
selectionExtras: [WEBSITE_URL],
|
||||
},
|
||||
whatsapp: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp (QR link)",
|
||||
detailLabel: "WhatsApp Web",
|
||||
docsPath: "/channels/whatsapp",
|
||||
docsLabel: "whatsapp",
|
||||
blurb: "works with your own number; recommend a separate phone + eSIM.",
|
||||
systemImage: "message",
|
||||
},
|
||||
discord: {
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
selectionLabel: "Discord (Bot API)",
|
||||
detailLabel: "Discord Bot",
|
||||
docsPath: "/channels/discord",
|
||||
docsLabel: "discord",
|
||||
blurb: "very well supported right now.",
|
||||
systemImage: "bubble.left.and.bubble.right",
|
||||
},
|
||||
irc: {
|
||||
id: "irc",
|
||||
label: "IRC",
|
||||
selectionLabel: "IRC (Server + Nick)",
|
||||
detailLabel: "IRC",
|
||||
docsPath: "/channels/irc",
|
||||
docsLabel: "irc",
|
||||
blurb: "classic IRC networks with DM/channel routing and pairing controls.",
|
||||
systemImage: "network",
|
||||
},
|
||||
googlechat: {
|
||||
id: "googlechat",
|
||||
label: "Google Chat",
|
||||
selectionLabel: "Google Chat (Chat API)",
|
||||
detailLabel: "Google Chat",
|
||||
docsPath: "/channels/googlechat",
|
||||
docsLabel: "googlechat",
|
||||
blurb: "Google Workspace Chat app with HTTP webhook.",
|
||||
systemImage: "message.badge",
|
||||
},
|
||||
slack: {
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
selectionLabel: "Slack (Socket Mode)",
|
||||
detailLabel: "Slack Bot",
|
||||
docsPath: "/channels/slack",
|
||||
docsLabel: "slack",
|
||||
blurb: "supported (Socket Mode).",
|
||||
systemImage: "number",
|
||||
},
|
||||
signal: {
|
||||
id: "signal",
|
||||
label: "Signal",
|
||||
selectionLabel: "Signal (signal-cli)",
|
||||
detailLabel: "Signal REST",
|
||||
docsPath: "/channels/signal",
|
||||
docsLabel: "signal",
|
||||
blurb: 'signal-cli linked device; more setup (David Reagans: "Hop on Discord.").',
|
||||
systemImage: "antenna.radiowaves.left.and.right",
|
||||
},
|
||||
imessage: {
|
||||
id: "imessage",
|
||||
label: "iMessage",
|
||||
selectionLabel: "iMessage (imsg)",
|
||||
detailLabel: "iMessage",
|
||||
docsPath: "/channels/imessage",
|
||||
docsLabel: "imessage",
|
||||
blurb: "this is still a work in progress.",
|
||||
systemImage: "message.fill",
|
||||
},
|
||||
line: {
|
||||
id: "line",
|
||||
label: "LINE",
|
||||
selectionLabel: "LINE (Messaging API)",
|
||||
detailLabel: "LINE Bot",
|
||||
docsPath: "/channels/line",
|
||||
docsLabel: "line",
|
||||
blurb: "LINE Messaging API webhook bot.",
|
||||
systemImage: "message",
|
||||
},
|
||||
};
|
||||
|
||||
export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = {
|
||||
imsg: "imessage",
|
||||
"internet-relay-chat": "irc",
|
||||
"google-chat": "googlechat",
|
||||
gchat: "googlechat",
|
||||
};
|
||||
|
||||
function normalizeChannelKey(raw?: string | null): string | undefined {
|
||||
const normalized = raw?.trim().toLowerCase();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
export function listChatChannels(): ChatChannelMeta[] {
|
||||
return CHAT_CHANNEL_ORDER.map((id) => CHAT_CHANNEL_META[id]);
|
||||
}
|
||||
|
||||
export function listChatChannelAliases(): string[] {
|
||||
return Object.keys(CHAT_CHANNEL_ALIASES);
|
||||
}
|
||||
|
||||
export function getChatChannelMeta(id: ChatChannelId): ChatChannelMeta {
|
||||
return CHAT_CHANNEL_META[id];
|
||||
}
|
||||
|
||||
export function normalizeChatChannelId(raw?: string | null): ChatChannelId | null {
|
||||
const normalized = normalizeChannelKey(raw);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const resolved = CHAT_CHANNEL_ALIASES[normalized] ?? normalized;
|
||||
return CHAT_CHANNEL_ORDER.includes(resolved) ? resolved : null;
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import {
|
||||
CHAT_CHANNEL_ALIASES,
|
||||
getChatChannelMeta,
|
||||
listChatChannelAliases,
|
||||
listChatChannels,
|
||||
normalizeChatChannelId,
|
||||
type ChatChannelMeta,
|
||||
} from "./chat-meta.js";
|
||||
import { CHANNEL_IDS, CHAT_CHANNEL_ORDER, type ChatChannelId } from "./ids.js";
|
||||
import type { ChannelMeta } from "./plugins/types.js";
|
||||
import type { ChannelId } from "./plugins/types.js";
|
||||
export { CHANNEL_IDS, CHAT_CHANNEL_ORDER } from "./ids.js";
|
||||
export type { ChatChannelId } from "./ids.js";
|
||||
|
||||
export type ChatChannelMeta = ChannelMeta;
|
||||
|
||||
const WEBSITE_URL = "https://openclaw.ai";
|
||||
|
||||
type RegisteredChannelPluginEntry = {
|
||||
plugin: {
|
||||
id?: string | null;
|
||||
@@ -36,134 +39,17 @@ function findRegisteredChannelPluginEntry(
|
||||
});
|
||||
}
|
||||
|
||||
const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
|
||||
telegram: {
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
selectionLabel: "Telegram (Bot API)",
|
||||
detailLabel: "Telegram Bot",
|
||||
docsPath: "/channels/telegram",
|
||||
docsLabel: "telegram",
|
||||
blurb: "simplest way to get started — register a bot with @BotFather and get going.",
|
||||
systemImage: "paperplane",
|
||||
selectionDocsPrefix: "",
|
||||
selectionDocsOmitLabel: true,
|
||||
selectionExtras: [WEBSITE_URL],
|
||||
},
|
||||
whatsapp: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp (QR link)",
|
||||
detailLabel: "WhatsApp Web",
|
||||
docsPath: "/channels/whatsapp",
|
||||
docsLabel: "whatsapp",
|
||||
blurb: "works with your own number; recommend a separate phone + eSIM.",
|
||||
systemImage: "message",
|
||||
},
|
||||
discord: {
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
selectionLabel: "Discord (Bot API)",
|
||||
detailLabel: "Discord Bot",
|
||||
docsPath: "/channels/discord",
|
||||
docsLabel: "discord",
|
||||
blurb: "very well supported right now.",
|
||||
systemImage: "bubble.left.and.bubble.right",
|
||||
},
|
||||
irc: {
|
||||
id: "irc",
|
||||
label: "IRC",
|
||||
selectionLabel: "IRC (Server + Nick)",
|
||||
detailLabel: "IRC",
|
||||
docsPath: "/channels/irc",
|
||||
docsLabel: "irc",
|
||||
blurb: "classic IRC networks with DM/channel routing and pairing controls.",
|
||||
systemImage: "network",
|
||||
},
|
||||
googlechat: {
|
||||
id: "googlechat",
|
||||
label: "Google Chat",
|
||||
selectionLabel: "Google Chat (Chat API)",
|
||||
detailLabel: "Google Chat",
|
||||
docsPath: "/channels/googlechat",
|
||||
docsLabel: "googlechat",
|
||||
blurb: "Google Workspace Chat app with HTTP webhook.",
|
||||
systemImage: "message.badge",
|
||||
},
|
||||
slack: {
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
selectionLabel: "Slack (Socket Mode)",
|
||||
detailLabel: "Slack Bot",
|
||||
docsPath: "/channels/slack",
|
||||
docsLabel: "slack",
|
||||
blurb: "supported (Socket Mode).",
|
||||
systemImage: "number",
|
||||
},
|
||||
signal: {
|
||||
id: "signal",
|
||||
label: "Signal",
|
||||
selectionLabel: "Signal (signal-cli)",
|
||||
detailLabel: "Signal REST",
|
||||
docsPath: "/channels/signal",
|
||||
docsLabel: "signal",
|
||||
blurb: 'signal-cli linked device; more setup (David Reagans: "Hop on Discord.").',
|
||||
systemImage: "antenna.radiowaves.left.and.right",
|
||||
},
|
||||
imessage: {
|
||||
id: "imessage",
|
||||
label: "iMessage",
|
||||
selectionLabel: "iMessage (imsg)",
|
||||
detailLabel: "iMessage",
|
||||
docsPath: "/channels/imessage",
|
||||
docsLabel: "imessage",
|
||||
blurb: "this is still a work in progress.",
|
||||
systemImage: "message.fill",
|
||||
},
|
||||
line: {
|
||||
id: "line",
|
||||
label: "LINE",
|
||||
selectionLabel: "LINE (Messaging API)",
|
||||
detailLabel: "LINE Bot",
|
||||
docsPath: "/channels/line",
|
||||
docsLabel: "line",
|
||||
blurb: "LINE Messaging API webhook bot.",
|
||||
systemImage: "message",
|
||||
},
|
||||
};
|
||||
|
||||
export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = {
|
||||
imsg: "imessage",
|
||||
"internet-relay-chat": "irc",
|
||||
"google-chat": "googlechat",
|
||||
gchat: "googlechat",
|
||||
};
|
||||
|
||||
const normalizeChannelKey = (raw?: string | null): string | undefined => {
|
||||
const normalized = raw?.trim().toLowerCase();
|
||||
return normalized || undefined;
|
||||
};
|
||||
|
||||
export function listChatChannels(): ChatChannelMeta[] {
|
||||
return CHAT_CHANNEL_ORDER.map((id) => CHAT_CHANNEL_META[id]);
|
||||
}
|
||||
|
||||
export function listChatChannelAliases(): string[] {
|
||||
return Object.keys(CHAT_CHANNEL_ALIASES);
|
||||
}
|
||||
|
||||
export function getChatChannelMeta(id: ChatChannelId): ChatChannelMeta {
|
||||
return CHAT_CHANNEL_META[id];
|
||||
}
|
||||
|
||||
export function normalizeChatChannelId(raw?: string | null): ChatChannelId | null {
|
||||
const normalized = normalizeChannelKey(raw);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const resolved = CHAT_CHANNEL_ALIASES[normalized] ?? normalized;
|
||||
return CHAT_CHANNEL_ORDER.includes(resolved) ? resolved : null;
|
||||
}
|
||||
export {
|
||||
CHAT_CHANNEL_ALIASES,
|
||||
getChatChannelMeta,
|
||||
listChatChannelAliases,
|
||||
listChatChannels,
|
||||
normalizeChatChannelId,
|
||||
};
|
||||
|
||||
// Channel docking: prefer this helper in shared code. Importing from
|
||||
// `src/channels/plugins/*` can eagerly load channel implementations.
|
||||
|
||||
@@ -26,6 +26,10 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
|
||||
// `node --import tsx` can emit `__name(...)` wrappers for function expressions.
|
||||
// Keep a local no-op helper so direct TS loads (used by config-surface tooling) stay stable.
|
||||
const __name = <T extends Function>(value: T): T => value;
|
||||
|
||||
export {
|
||||
authorizeConfigWrite,
|
||||
canBypassConfigWritePolicy,
|
||||
|
||||
@@ -22,4 +22,4 @@ export {
|
||||
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
|
||||
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
|
||||
|
||||
export { getChatChannelMeta } from "../channels/registry.js";
|
||||
export { getChatChannelMeta } from "../channels/chat-meta.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getChatChannelMeta } from "../channels/chat-meta.js";
|
||||
import {
|
||||
createScopedAccountReplyToModeResolver,
|
||||
createTopLevelChannelReplyToModeResolver,
|
||||
@@ -10,20 +11,18 @@ import type {
|
||||
import type {
|
||||
ChannelMessagingAdapter,
|
||||
ChannelOutboundSessionRoute,
|
||||
ChannelPollResult,
|
||||
ChannelThreadingAdapter,
|
||||
} from "../channels/plugins/types.core.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
import { getChatChannelMeta } from "../channels/registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ReplyToMode } from "../config/types.base.js";
|
||||
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
|
||||
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
|
||||
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import type { OpenClawPluginApi, OpenClawPluginConfigSchema } from "../plugins/types.js";
|
||||
import { createScopedDmSecurityResolver } from "./channel-config-helpers.js";
|
||||
import { createTextPairingAdapter } from "./channel-pairing.js";
|
||||
import { createAttachedChannelResultAdapter } from "./channel-send-result.js";
|
||||
import { definePluginEntry } from "./plugin-entry.js";
|
||||
|
||||
export type {
|
||||
AnyAgentTool,
|
||||
@@ -75,6 +74,25 @@ export type {
|
||||
ChannelOutboundSessionRoute,
|
||||
ChannelMessagingAdapter,
|
||||
} from "../channels/plugins/types.core.js";
|
||||
|
||||
function createInlineTextPairingAdapter(params: {
|
||||
idLabel: string;
|
||||
message: string;
|
||||
normalizeAllowEntry?: ChannelPairingAdapter["normalizeAllowEntry"];
|
||||
notify: (
|
||||
params: Parameters<NonNullable<ChannelPairingAdapter["notifyApproval"]>>[0] & {
|
||||
message: string;
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
}): ChannelPairingAdapter {
|
||||
return {
|
||||
idLabel: params.idLabel,
|
||||
normalizeAllowEntry: params.normalizeAllowEntry,
|
||||
notifyApproval: async (ctx) => {
|
||||
await params.notify({ ...ctx, message: params.message });
|
||||
},
|
||||
};
|
||||
}
|
||||
export type {
|
||||
ProviderUsageSnapshot,
|
||||
UsageProviderId,
|
||||
@@ -103,7 +121,7 @@ export {
|
||||
formatPairingApproveHint,
|
||||
parseOptionalDelimitedEntries,
|
||||
} from "../channels/plugins/helpers.js";
|
||||
export { getChatChannelMeta } from "../channels/registry.js";
|
||||
export { getChatChannelMeta } from "../channels/chat-meta.js";
|
||||
export {
|
||||
channelTargetSchema,
|
||||
channelTargetsSchema,
|
||||
@@ -200,7 +218,12 @@ type DefineChannelPluginEntryOptions<TPlugin = ChannelPlugin> = {
|
||||
registerFull?: (api: OpenClawPluginApi) => void;
|
||||
};
|
||||
|
||||
type DefinedChannelPluginEntry<TPlugin> = ReturnType<typeof definePluginEntry> & {
|
||||
type DefinedChannelPluginEntry<TPlugin> = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
configSchema: OpenClawPluginConfigSchema;
|
||||
register: (api: OpenClawPluginApi) => void;
|
||||
channelPlugin: TPlugin;
|
||||
setChannelRuntime?: (runtime: PluginRuntime) => void;
|
||||
};
|
||||
@@ -257,11 +280,12 @@ export function defineChannelPluginEntry<TPlugin>({
|
||||
setRuntime,
|
||||
registerFull,
|
||||
}: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin> {
|
||||
const entry = definePluginEntry({
|
||||
const resolvedConfigSchema = typeof configSchema === "function" ? configSchema() : configSchema;
|
||||
const entry = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
configSchema,
|
||||
configSchema: resolvedConfigSchema,
|
||||
register(api: OpenClawPluginApi) {
|
||||
setRuntime?.(api.runtime);
|
||||
api.registerChannel({ plugin: plugin as ChannelPlugin });
|
||||
@@ -270,7 +294,7 @@ export function defineChannelPluginEntry<TPlugin>({
|
||||
}
|
||||
registerFull?.(api);
|
||||
},
|
||||
});
|
||||
};
|
||||
return {
|
||||
...entry,
|
||||
channelPlugin: plugin,
|
||||
@@ -320,7 +344,11 @@ type ChatChannelPairingOptions = {
|
||||
idLabel: string;
|
||||
message: string;
|
||||
normalizeAllowEntry?: ChannelPairingAdapter["normalizeAllowEntry"];
|
||||
notify: Parameters<typeof createTextPairingAdapter>[0]["notify"];
|
||||
notify: (
|
||||
params: Parameters<NonNullable<ChannelPairingAdapter["notifyApproval"]>>[0] & {
|
||||
message: string;
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -346,9 +374,47 @@ type ChatChannelThreadingOptions<TResolvedAccount> =
|
||||
|
||||
type ChatChannelAttachedOutboundOptions = {
|
||||
base: Omit<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll">;
|
||||
attachedResults: Parameters<typeof createAttachedChannelResultAdapter>[0];
|
||||
attachedResults: {
|
||||
channel: string;
|
||||
sendText?: (
|
||||
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendText"]>>[0],
|
||||
) => MaybePromise<Omit<OutboundDeliveryResult, "channel">>;
|
||||
sendMedia?: (
|
||||
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendMedia"]>>[0],
|
||||
) => MaybePromise<Omit<OutboundDeliveryResult, "channel">>;
|
||||
sendPoll?: (
|
||||
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0],
|
||||
) => MaybePromise<Omit<ChannelPollResult, "channel">>;
|
||||
};
|
||||
};
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
function createInlineAttachedChannelResultAdapter(
|
||||
params: ChatChannelAttachedOutboundOptions["attachedResults"],
|
||||
) {
|
||||
return {
|
||||
sendText: params.sendText
|
||||
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendText"]>>[0]) => ({
|
||||
channel: params.channel,
|
||||
...(await params.sendText!(ctx)),
|
||||
})
|
||||
: undefined,
|
||||
sendMedia: params.sendMedia
|
||||
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendMedia"]>>[0]) => ({
|
||||
channel: params.channel,
|
||||
...(await params.sendMedia!(ctx)),
|
||||
})
|
||||
: undefined,
|
||||
sendPoll: params.sendPoll
|
||||
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0]) => ({
|
||||
channel: params.channel,
|
||||
...(await params.sendPoll!(ctx)),
|
||||
})
|
||||
: undefined,
|
||||
} satisfies Pick<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll">;
|
||||
}
|
||||
|
||||
function resolveChatChannelSecurity<TResolvedAccount extends { accountId?: string | null }>(
|
||||
security:
|
||||
| ChannelSecurityAdapter<TResolvedAccount>
|
||||
@@ -376,7 +442,7 @@ function resolveChatChannelPairing(
|
||||
if (!("text" in pairing)) {
|
||||
return pairing;
|
||||
}
|
||||
return createTextPairingAdapter(pairing.text);
|
||||
return createInlineTextPairingAdapter(pairing.text);
|
||||
}
|
||||
|
||||
function resolveChatChannelThreading<TResolvedAccount>(
|
||||
@@ -415,7 +481,7 @@ function resolveChatChannelOutbound(
|
||||
}
|
||||
return {
|
||||
...outbound.base,
|
||||
...createAttachedChannelResultAdapter(outbound.attachedResults),
|
||||
...createInlineAttachedChannelResultAdapter(outbound.attachedResults),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export {
|
||||
export {
|
||||
applyAccountNameToChannelSection,
|
||||
applySetupAccountConfigPatch,
|
||||
createSetupInputPresenceValidator,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "../channels/plugins/setup-helpers.js";
|
||||
export { createAccountStatusSink } from "./channel-lifecycle.js";
|
||||
|
||||
@@ -27,11 +27,14 @@ export {
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
export {
|
||||
applyAccountNameToChannelSection,
|
||||
createSetupInputPresenceValidator,
|
||||
patchScopedAccountConfig,
|
||||
} from "../channels/plugins/setup-helpers.js";
|
||||
export { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
|
||||
export type { ChannelGroupContext, ChannelSetupInput } from "../channels/plugins/types.js";
|
||||
export type { ChannelSetupDmPolicy } from "../channels/plugins/setup-wizard-types.js";
|
||||
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
export type { ChannelSetupWizard } from "../channels/plugins/setup-wizard.js";
|
||||
export { createChannelReplyPipeline } from "./channel-reply-pipeline.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export { mapAllowFromEntries } from "./channel-config-helpers.js";
|
||||
@@ -78,12 +81,12 @@ export type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
export type { OpenClawPluginApi } from "../plugins/types.js";
|
||||
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
export type { RuntimeEnv } from "../runtime.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export {
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveDmGroupAccessWithCommandGate,
|
||||
} from "../security/dm-policy-shared.js";
|
||||
export { formatDocsLink } from "../terminal/links.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export {
|
||||
listConfiguredAccountIds,
|
||||
resolveAccountWithDefaultFallback,
|
||||
@@ -103,3 +106,9 @@ export {
|
||||
buildBaseChannelStatusSummary,
|
||||
buildRuntimeAccountStatusSnapshot,
|
||||
} from "./status-helpers.js";
|
||||
export {
|
||||
createTopLevelChannelDmPolicy,
|
||||
promptParsedAllowFromForAccount,
|
||||
resolveSetupAccountId,
|
||||
setSetupChannelEnabled,
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
|
||||
@@ -5,22 +5,38 @@ export type { ChannelSetupDmPolicy } from "../channels/plugins/setup-wizard-type
|
||||
export type {
|
||||
ChannelSetupWizard,
|
||||
ChannelSetupWizardAllowFromEntry,
|
||||
ChannelSetupWizardTextInput,
|
||||
} from "../channels/plugins/setup-wizard.js";
|
||||
|
||||
export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
|
||||
export { createEnvPatchedAccountSetupAdapter } from "../channels/plugins/setup-helpers.js";
|
||||
export {
|
||||
createEnvPatchedAccountSetupAdapter,
|
||||
createPatchedAccountSetupAdapter,
|
||||
createSetupInputPresenceValidator,
|
||||
} from "../channels/plugins/setup-helpers.js";
|
||||
|
||||
export {
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createTopLevelChannelDmPolicy,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
createStandardChannelSetupStatus,
|
||||
mergeAllowFromEntries,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
promptParsedAllowFromForAccount,
|
||||
resolveEntriesWithOptionalToken,
|
||||
resolveSetupAccountId,
|
||||
setAccountAllowFromForChannel,
|
||||
setSetupChannelEnabled,
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
|
||||
export { createAllowlistSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js";
|
||||
export {
|
||||
createCliPathTextInput,
|
||||
createDelegatedTextInputShouldPrompt,
|
||||
} from "../channels/plugins/setup-wizard-binary.js";
|
||||
export { createDelegatedSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js";
|
||||
|
||||
@@ -11,6 +11,11 @@ export type {
|
||||
OpenClawPluginApi,
|
||||
PluginRuntime,
|
||||
} from "./channel-plugin-common.js";
|
||||
export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js";
|
||||
export type {
|
||||
ChannelSetupWizard,
|
||||
ChannelSetupWizardTextInput,
|
||||
} from "../channels/plugins/setup-wizard.js";
|
||||
export {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
@@ -24,6 +29,10 @@ export {
|
||||
normalizeAccountId,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "./channel-plugin-common.js";
|
||||
export {
|
||||
createPatchedAccountSetupAdapter,
|
||||
createSetupInputPresenceValidator,
|
||||
} from "../channels/plugins/setup-helpers.js";
|
||||
export { formatCliCommand } from "../cli/command-format.js";
|
||||
export { formatDocsLink } from "../terminal/links.js";
|
||||
|
||||
@@ -42,6 +51,18 @@ export { SignalConfigSchema } from "../config/zod-schema.providers-core.js";
|
||||
|
||||
export { normalizeE164 } from "../utils.js";
|
||||
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
|
||||
export {
|
||||
createCliPathTextInput,
|
||||
createDelegatedTextInputShouldPrompt,
|
||||
} from "../channels/plugins/setup-wizard-binary.js";
|
||||
export { createDelegatedSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js";
|
||||
export {
|
||||
createTopLevelChannelDmPolicy,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForAccount,
|
||||
setAccountAllowFromForChannel,
|
||||
setSetupChannelEnabled,
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
|
||||
export {
|
||||
buildBaseAccountStatusSnapshot,
|
||||
|
||||
Reference in New Issue
Block a user