mirror of https://github.com/openclaw/openclaw.git
refactor: tighten plugin sdk channel seams
This commit is contained in:
parent
7a09255361
commit
f11589b311
|
|
@ -1,5 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AcpRuntimeError } from "../../acp/runtime/errors.js";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "../../commands/channel-test-helpers.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
|
|
@ -395,6 +396,7 @@ async function runInternalAcpCommand(params: {
|
|||
|
||||
describe("/acp command", () => {
|
||||
beforeEach(() => {
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
acpManagerTesting.resetAcpSessionManagerForTests();
|
||||
hoisted.listAcpSessionEntriesMock.mockReset().mockResolvedValue([]);
|
||||
hoisted.callGatewayMock.mockReset().mockResolvedValue({ ok: true });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { buildFeishuConversationId } from "../../../../extensions/feishu/src/conversation-id.js";
|
||||
import {
|
||||
buildTelegramTopicConversationId,
|
||||
normalizeConversationText,
|
||||
|
|
@ -7,6 +6,7 @@ import {
|
|||
import { DISCORD_THREAD_BINDING_CHANNEL } from "../../../channels/thread-bindings-policy.js";
|
||||
import { resolveConversationIdFromTargets } from "../../../infra/outbound/conversation-id.js";
|
||||
import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
|
||||
import { buildFeishuConversationId } from "../../../plugin-sdk/feishu.js";
|
||||
import { parseAgentSessionKey } from "../../../routing/session-key.js";
|
||||
import type { HandleCommandsParams } from "../commands-types.js";
|
||||
import { parseDiscordParentChannelFromSessionKey } from "../discord-parent-channel.js";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { StickerMetadata } from "../../extensions/telegram/src/bot/types.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type {
|
||||
MediaUnderstandingDecision,
|
||||
MediaUnderstandingOutput,
|
||||
} from "../media-understanding/types.js";
|
||||
import type { StickerMetadata } from "../plugin-sdk/telegram.js";
|
||||
import type { InputProvenance } from "../sessions/input-provenance.js";
|
||||
import type { InternalMessageChannel } from "../utils/message-channel.js";
|
||||
import type { CommandArgs } from "./commands-registry.types.js";
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// Shim: re-exports from extension
|
||||
// Public entrypoint for the Discord channel action adapter.
|
||||
export * from "../../../../extensions/discord/src/channel-actions.js";
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
// Public entrypoint for the Telegram channel action adapter.
|
||||
export * from "../../../../extensions/telegram/src/channel-actions.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import { bluebubblesPlugin } from "../../../extensions/bluebubbles/src/channel.js";
|
||||
import { discordPlugin } from "../../../extensions/discord/src/channel.js";
|
||||
import { discordSetupPlugin } from "../../../extensions/discord/src/channel.setup.js";
|
||||
import { setDiscordRuntime } from "../../../extensions/discord/src/runtime.js";
|
||||
import { feishuPlugin } from "../../../extensions/feishu/src/channel.js";
|
||||
import { googlechatPlugin } from "../../../extensions/googlechat/src/channel.js";
|
||||
import { imessagePlugin } from "../../../extensions/imessage/src/channel.js";
|
||||
import { imessageSetupPlugin } from "../../../extensions/imessage/src/channel.setup.js";
|
||||
import { ircPlugin } from "../../../extensions/irc/src/channel.js";
|
||||
import { linePlugin } from "../../../extensions/line/src/channel.js";
|
||||
import { lineSetupPlugin } from "../../../extensions/line/src/channel.setup.js";
|
||||
import { setLineRuntime } from "../../../extensions/line/src/runtime.js";
|
||||
import { matrixPlugin } from "../../../extensions/matrix/src/channel.js";
|
||||
import { mattermostPlugin } from "../../../extensions/mattermost/src/channel.js";
|
||||
import { msteamsPlugin } from "../../../extensions/msteams/src/channel.js";
|
||||
import { nextcloudTalkPlugin } from "../../../extensions/nextcloud-talk/src/channel.js";
|
||||
import { nostrPlugin } from "../../../extensions/nostr/src/channel.js";
|
||||
import { signalPlugin } from "../../../extensions/signal/src/channel.js";
|
||||
import { signalSetupPlugin } from "../../../extensions/signal/src/channel.setup.js";
|
||||
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
|
||||
import { slackSetupPlugin } from "../../../extensions/slack/src/channel.setup.js";
|
||||
import { synologyChatPlugin } from "../../../extensions/synology-chat/src/channel.js";
|
||||
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
|
||||
import { telegramSetupPlugin } from "../../../extensions/telegram/src/channel.setup.js";
|
||||
import { setTelegramRuntime } from "../../../extensions/telegram/src/runtime.js";
|
||||
import { tlonPlugin } from "../../../extensions/tlon/src/channel.js";
|
||||
import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js";
|
||||
import { whatsappSetupPlugin } from "../../../extensions/whatsapp/src/channel.setup.js";
|
||||
import { zaloPlugin } from "../../../extensions/zalo/src/channel.js";
|
||||
import { zalouserPlugin } from "../../../extensions/zalouser/src/channel.js";
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
|
||||
export const bundledChannelPlugins = [
|
||||
bluebubblesPlugin,
|
||||
discordPlugin,
|
||||
feishuPlugin,
|
||||
googlechatPlugin,
|
||||
imessagePlugin,
|
||||
ircPlugin,
|
||||
linePlugin,
|
||||
matrixPlugin,
|
||||
mattermostPlugin,
|
||||
msteamsPlugin,
|
||||
nextcloudTalkPlugin,
|
||||
nostrPlugin,
|
||||
signalPlugin,
|
||||
slackPlugin,
|
||||
synologyChatPlugin,
|
||||
telegramPlugin,
|
||||
tlonPlugin,
|
||||
whatsappPlugin,
|
||||
zaloPlugin,
|
||||
zalouserPlugin,
|
||||
] as ChannelPlugin[];
|
||||
|
||||
export const bundledChannelSetupPlugins = [
|
||||
telegramSetupPlugin,
|
||||
whatsappSetupPlugin,
|
||||
discordSetupPlugin,
|
||||
ircPlugin,
|
||||
googlechatPlugin,
|
||||
slackSetupPlugin,
|
||||
signalSetupPlugin,
|
||||
imessageSetupPlugin,
|
||||
lineSetupPlugin,
|
||||
] as ChannelPlugin[];
|
||||
|
||||
const bundledChannelPluginsById = new Map(
|
||||
bundledChannelPlugins.map((plugin) => [plugin.id, plugin] as const),
|
||||
);
|
||||
|
||||
export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
|
||||
return bundledChannelPluginsById.get(id);
|
||||
}
|
||||
|
||||
export function requireBundledChannelPlugin(id: ChannelId): ChannelPlugin {
|
||||
const plugin = getBundledChannelPlugin(id);
|
||||
if (!plugin) {
|
||||
throw new Error(`missing bundled channel plugin: ${id}`);
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
export const bundledChannelRuntimeSetters = {
|
||||
setDiscordRuntime,
|
||||
setLineRuntime,
|
||||
setTelegramRuntime,
|
||||
};
|
||||
|
|
@ -1,33 +1,11 @@
|
|||
import { expect, vi } from "vitest";
|
||||
import { bluebubblesPlugin } from "../../../../extensions/bluebubbles/src/channel.js";
|
||||
import { discordPlugin } from "../../../../extensions/discord/src/channel.js";
|
||||
import { setDiscordRuntime } from "../../../../extensions/discord/src/runtime.js";
|
||||
import { feishuPlugin } from "../../../../extensions/feishu/src/channel.js";
|
||||
import { googlechatPlugin } from "../../../../extensions/googlechat/src/channel.js";
|
||||
import { imessagePlugin } from "../../../../extensions/imessage/src/channel.js";
|
||||
import { ircPlugin } from "../../../../extensions/irc/src/channel.js";
|
||||
import { linePlugin } from "../../../../extensions/line/src/channel.js";
|
||||
import { setLineRuntime } from "../../../../extensions/line/src/runtime.js";
|
||||
import { matrixPlugin } from "../../../../extensions/matrix/src/channel.js";
|
||||
import { mattermostPlugin } from "../../../../extensions/mattermost/src/channel.js";
|
||||
import { msteamsPlugin } from "../../../../extensions/msteams/src/channel.js";
|
||||
import { nextcloudTalkPlugin } from "../../../../extensions/nextcloud-talk/src/channel.js";
|
||||
import { nostrPlugin } from "../../../../extensions/nostr/src/channel.js";
|
||||
import { signalPlugin } from "../../../../extensions/signal/src/channel.js";
|
||||
import { slackPlugin } from "../../../../extensions/slack/src/channel.js";
|
||||
import { synologyChatPlugin } from "../../../../extensions/synology-chat/src/channel.js";
|
||||
import { telegramPlugin } from "../../../../extensions/telegram/src/channel.js";
|
||||
import { setTelegramRuntime } from "../../../../extensions/telegram/src/runtime.js";
|
||||
import { tlonPlugin } from "../../../../extensions/tlon/src/channel.js";
|
||||
import { whatsappPlugin } from "../../../../extensions/whatsapp/src/channel.js";
|
||||
import { zaloPlugin } from "../../../../extensions/zalo/src/channel.js";
|
||||
import { zalouserPlugin } from "../../../../extensions/zalouser/src/channel.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
resolveDefaultLineAccountId,
|
||||
resolveLineAccount,
|
||||
listLineAccountIds,
|
||||
} from "../../../line/accounts.js";
|
||||
import { bundledChannelRuntimeSetters, requireBundledChannelPlugin } from "../bundled.js";
|
||||
import type { ChannelPlugin } from "../types.js";
|
||||
|
||||
type PluginContractEntry = {
|
||||
|
|
@ -84,7 +62,7 @@ const telegramGetCapabilitiesMock = vi.fn();
|
|||
const discordListActionsMock = vi.fn();
|
||||
const discordGetCapabilitiesMock = vi.fn();
|
||||
|
||||
setTelegramRuntime({
|
||||
bundledChannelRuntimeSetters.setTelegramRuntime({
|
||||
channel: {
|
||||
telegram: {
|
||||
messageActions: {
|
||||
|
|
@ -95,7 +73,7 @@ setTelegramRuntime({
|
|||
},
|
||||
} as never);
|
||||
|
||||
setDiscordRuntime({
|
||||
bundledChannelRuntimeSetters.setDiscordRuntime({
|
||||
channel: {
|
||||
discord: {
|
||||
messageActions: {
|
||||
|
|
@ -106,7 +84,7 @@ setDiscordRuntime({
|
|||
},
|
||||
} as never);
|
||||
|
||||
setLineRuntime({
|
||||
bundledChannelRuntimeSetters.setLineRuntime({
|
||||
channel: {
|
||||
line: {
|
||||
listLineAccountIds,
|
||||
|
|
@ -118,32 +96,32 @@ setLineRuntime({
|
|||
} as never);
|
||||
|
||||
export const pluginContractRegistry: PluginContractEntry[] = [
|
||||
{ id: "bluebubbles", plugin: bluebubblesPlugin },
|
||||
{ id: "discord", plugin: discordPlugin },
|
||||
{ id: "feishu", plugin: feishuPlugin },
|
||||
{ id: "googlechat", plugin: googlechatPlugin },
|
||||
{ id: "imessage", plugin: imessagePlugin },
|
||||
{ id: "irc", plugin: ircPlugin },
|
||||
{ id: "line", plugin: linePlugin },
|
||||
{ id: "matrix", plugin: matrixPlugin },
|
||||
{ id: "mattermost", plugin: mattermostPlugin },
|
||||
{ id: "msteams", plugin: msteamsPlugin },
|
||||
{ id: "nextcloud-talk", plugin: nextcloudTalkPlugin },
|
||||
{ id: "nostr", plugin: nostrPlugin },
|
||||
{ id: "signal", plugin: signalPlugin },
|
||||
{ id: "slack", plugin: slackPlugin },
|
||||
{ id: "synology-chat", plugin: synologyChatPlugin },
|
||||
{ id: "telegram", plugin: telegramPlugin },
|
||||
{ id: "tlon", plugin: tlonPlugin },
|
||||
{ id: "whatsapp", plugin: whatsappPlugin },
|
||||
{ id: "zalo", plugin: zaloPlugin },
|
||||
{ id: "zalouser", plugin: zalouserPlugin },
|
||||
{ id: "bluebubbles", plugin: requireBundledChannelPlugin("bluebubbles") },
|
||||
{ id: "discord", plugin: requireBundledChannelPlugin("discord") },
|
||||
{ id: "feishu", plugin: requireBundledChannelPlugin("feishu") },
|
||||
{ id: "googlechat", plugin: requireBundledChannelPlugin("googlechat") },
|
||||
{ id: "imessage", plugin: requireBundledChannelPlugin("imessage") },
|
||||
{ id: "irc", plugin: requireBundledChannelPlugin("irc") },
|
||||
{ id: "line", plugin: requireBundledChannelPlugin("line") },
|
||||
{ id: "matrix", plugin: requireBundledChannelPlugin("matrix") },
|
||||
{ id: "mattermost", plugin: requireBundledChannelPlugin("mattermost") },
|
||||
{ id: "msteams", plugin: requireBundledChannelPlugin("msteams") },
|
||||
{ id: "nextcloud-talk", plugin: requireBundledChannelPlugin("nextcloud-talk") },
|
||||
{ id: "nostr", plugin: requireBundledChannelPlugin("nostr") },
|
||||
{ id: "signal", plugin: requireBundledChannelPlugin("signal") },
|
||||
{ id: "slack", plugin: requireBundledChannelPlugin("slack") },
|
||||
{ id: "synology-chat", plugin: requireBundledChannelPlugin("synology-chat") },
|
||||
{ id: "telegram", plugin: requireBundledChannelPlugin("telegram") },
|
||||
{ id: "tlon", plugin: requireBundledChannelPlugin("tlon") },
|
||||
{ id: "whatsapp", plugin: requireBundledChannelPlugin("whatsapp") },
|
||||
{ id: "zalo", plugin: requireBundledChannelPlugin("zalo") },
|
||||
{ id: "zalouser", plugin: requireBundledChannelPlugin("zalouser") },
|
||||
];
|
||||
|
||||
export const actionContractRegistry: ActionsContractEntry[] = [
|
||||
{
|
||||
id: "slack",
|
||||
plugin: slackPlugin,
|
||||
plugin: requireBundledChannelPlugin("slack"),
|
||||
unsupportedAction: "poll",
|
||||
cases: [
|
||||
{
|
||||
|
|
@ -217,7 +195,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
|
|||
},
|
||||
{
|
||||
id: "mattermost",
|
||||
plugin: mattermostPlugin,
|
||||
plugin: requireBundledChannelPlugin("mattermost"),
|
||||
unsupportedAction: "poll",
|
||||
cases: [
|
||||
{
|
||||
|
|
@ -265,7 +243,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
|
|||
},
|
||||
{
|
||||
id: "telegram",
|
||||
plugin: telegramPlugin,
|
||||
plugin: requireBundledChannelPlugin("telegram"),
|
||||
cases: [
|
||||
{
|
||||
name: "forwards runtime-backed Telegram actions and capabilities",
|
||||
|
|
@ -283,7 +261,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
|
|||
},
|
||||
{
|
||||
id: "discord",
|
||||
plugin: discordPlugin,
|
||||
plugin: requireBundledChannelPlugin("discord"),
|
||||
cases: [
|
||||
{
|
||||
name: "forwards runtime-backed Discord actions and capabilities",
|
||||
|
|
@ -304,7 +282,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
|
|||
export const setupContractRegistry: SetupContractEntry[] = [
|
||||
{
|
||||
id: "slack",
|
||||
plugin: slackPlugin,
|
||||
plugin: requireBundledChannelPlugin("slack"),
|
||||
cases: [
|
||||
{
|
||||
name: "default account stores tokens and enables the channel",
|
||||
|
|
@ -334,7 +312,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
|
|||
},
|
||||
{
|
||||
id: "mattermost",
|
||||
plugin: mattermostPlugin,
|
||||
plugin: requireBundledChannelPlugin("mattermost"),
|
||||
cases: [
|
||||
{
|
||||
name: "default account stores token and normalized base URL",
|
||||
|
|
@ -363,7 +341,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
|
|||
},
|
||||
{
|
||||
id: "line",
|
||||
plugin: linePlugin,
|
||||
plugin: requireBundledChannelPlugin("line"),
|
||||
cases: [
|
||||
{
|
||||
name: "default account stores token and secret",
|
||||
|
|
@ -396,7 +374,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
|
|||
export const statusContractRegistry: StatusContractEntry[] = [
|
||||
{
|
||||
id: "slack",
|
||||
plugin: slackPlugin,
|
||||
plugin: requireBundledChannelPlugin("slack"),
|
||||
cases: [
|
||||
{
|
||||
name: "configured account produces a configured status snapshot",
|
||||
|
|
@ -424,7 +402,7 @@ export const statusContractRegistry: StatusContractEntry[] = [
|
|||
},
|
||||
{
|
||||
id: "mattermost",
|
||||
plugin: mattermostPlugin,
|
||||
plugin: requireBundledChannelPlugin("mattermost"),
|
||||
cases: [
|
||||
{
|
||||
name: "configured account preserves connectivity details in the snapshot",
|
||||
|
|
@ -455,7 +433,7 @@ export const statusContractRegistry: StatusContractEntry[] = [
|
|||
},
|
||||
{
|
||||
id: "line",
|
||||
plugin: linePlugin,
|
||||
plugin: requireBundledChannelPlugin("line"),
|
||||
cases: [
|
||||
{
|
||||
name: "configured account produces a webhook status snapshot",
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
// Shim: re-exports from extension
|
||||
export * from "../../../../extensions/discord/src/normalize.js";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "../../../../extensions/telegram/src/normalize.js";
|
||||
|
|
@ -1,2 +1,25 @@
|
|||
// Shim: re-exports from extensions/whatsapp/src/normalize.ts
|
||||
export * from "../../../../extensions/whatsapp/src/normalize.js";
|
||||
import { normalizeWhatsAppTarget } from "../../../whatsapp/normalize.js";
|
||||
import { looksLikeHandleOrPhoneTarget, trimMessagingTarget } from "./shared.js";
|
||||
|
||||
export function normalizeWhatsAppMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = trimMessagingTarget(raw);
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeWhatsAppTarget(trimmed) ?? undefined;
|
||||
}
|
||||
|
||||
export function normalizeWhatsAppAllowFromEntries(allowFrom: Array<string | number>): string[] {
|
||||
return allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter((entry): entry is string => Boolean(entry))
|
||||
.map((entry) => (entry === "*" ? entry : normalizeWhatsAppTarget(entry)))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
}
|
||||
|
||||
export function looksLikeWhatsAppTargetId(raw: string): boolean {
|
||||
return looksLikeHandleOrPhoneTarget({
|
||||
raw,
|
||||
prefixPattern: /^whatsapp:/i,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,9 @@
|
|||
import { discordSetupPlugin } from "../../../extensions/discord/src/channel.setup.js";
|
||||
import { googlechatPlugin } from "../../../extensions/googlechat/src/channel.js";
|
||||
import { imessageSetupPlugin } from "../../../extensions/imessage/src/channel.setup.js";
|
||||
import { ircPlugin } from "../../../extensions/irc/src/channel.js";
|
||||
import { lineSetupPlugin } from "../../../extensions/line/src/channel.setup.js";
|
||||
import { signalSetupPlugin } from "../../../extensions/signal/src/channel.setup.js";
|
||||
import { slackSetupPlugin } from "../../../extensions/slack/src/channel.setup.js";
|
||||
import { telegramSetupPlugin } from "../../../extensions/telegram/src/channel.setup.js";
|
||||
import { whatsappSetupPlugin } from "../../../extensions/whatsapp/src/channel.setup.js";
|
||||
import {
|
||||
getActivePluginRegistryVersion,
|
||||
requireActivePluginRegistry,
|
||||
} from "../../plugins/runtime.js";
|
||||
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js";
|
||||
import { bundledChannelSetupPlugins } from "./bundled.js";
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
|
||||
type CachedChannelSetupPlugins = {
|
||||
|
|
@ -28,18 +20,6 @@ const EMPTY_CHANNEL_SETUP_CACHE: CachedChannelSetupPlugins = {
|
|||
|
||||
let cachedChannelSetupPlugins = EMPTY_CHANNEL_SETUP_CACHE;
|
||||
|
||||
const BUNDLED_CHANNEL_SETUP_PLUGINS = [
|
||||
telegramSetupPlugin,
|
||||
whatsappSetupPlugin,
|
||||
discordSetupPlugin,
|
||||
ircPlugin,
|
||||
googlechatPlugin,
|
||||
slackSetupPlugin,
|
||||
signalSetupPlugin,
|
||||
imessageSetupPlugin,
|
||||
lineSetupPlugin,
|
||||
] as ChannelPlugin[];
|
||||
|
||||
function dedupeSetupPlugins(plugins: ChannelPlugin[]): ChannelPlugin[] {
|
||||
const seen = new Set<string>();
|
||||
const resolved: ChannelPlugin[] = [];
|
||||
|
|
@ -77,7 +57,7 @@ function resolveCachedChannelSetupPlugins(): CachedChannelSetupPlugins {
|
|||
|
||||
const registryPlugins = (registry.channelSetups ?? []).map((entry) => entry.plugin);
|
||||
const sorted = sortChannelSetupPlugins(
|
||||
registryPlugins.length > 0 ? registryPlugins : BUNDLED_CHANNEL_SETUP_PLUGINS,
|
||||
registryPlugins.length > 0 ? registryPlugins : bundledChannelSetupPlugins,
|
||||
);
|
||||
const byId = new Map<string, ChannelPlugin>();
|
||||
for (const plugin of sorted) {
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
// Shim: re-exports from extension
|
||||
export * from "../../../../extensions/discord/src/status-issues.js";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "../../../../extensions/telegram/src/status-issues.js";
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
// Shim: re-exports from extensions/whatsapp/src/status-issues.ts
|
||||
export * from "../../../../extensions/whatsapp/src/status-issues.js";
|
||||
|
|
@ -1,10 +1,4 @@
|
|||
import { discordPlugin } from "../../extensions/discord/src/channel.js";
|
||||
import { feishuPlugin } from "../../extensions/feishu/src/channel.js";
|
||||
import { imessagePlugin } from "../../extensions/imessage/src/channel.js";
|
||||
import { signalPlugin } from "../../extensions/signal/src/channel.js";
|
||||
import { slackPlugin } from "../../extensions/slack/src/channel.js";
|
||||
import { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
||||
import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js";
|
||||
import { requireBundledChannelPlugin } from "../channels/plugins/bundled.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { getChannelSetupWizardAdapter } from "./channel-setup/registry.js";
|
||||
|
|
@ -27,13 +21,13 @@ type PatchedSetupAdapterFields = {
|
|||
|
||||
export function setDefaultChannelPluginRegistryForTests(): void {
|
||||
const channels = [
|
||||
{ pluginId: "discord", plugin: discordPlugin, source: "test" },
|
||||
{ pluginId: "feishu", plugin: feishuPlugin, source: "test" },
|
||||
{ pluginId: "slack", plugin: slackPlugin, source: "test" },
|
||||
{ pluginId: "telegram", plugin: telegramPlugin, source: "test" },
|
||||
{ pluginId: "whatsapp", plugin: whatsappPlugin, source: "test" },
|
||||
{ pluginId: "signal", plugin: signalPlugin, source: "test" },
|
||||
{ pluginId: "imessage", plugin: imessagePlugin, source: "test" },
|
||||
{ pluginId: "discord", plugin: requireBundledChannelPlugin("discord"), source: "test" },
|
||||
{ pluginId: "feishu", plugin: requireBundledChannelPlugin("feishu"), source: "test" },
|
||||
{ pluginId: "slack", plugin: requireBundledChannelPlugin("slack"), source: "test" },
|
||||
{ pluginId: "telegram", plugin: requireBundledChannelPlugin("telegram"), source: "test" },
|
||||
{ pluginId: "whatsapp", plugin: requireBundledChannelPlugin("whatsapp"), source: "test" },
|
||||
{ pluginId: "signal", plugin: requireBundledChannelPlugin("signal"), source: "test" },
|
||||
{ pluginId: "imessage", plugin: requireBundledChannelPlugin("imessage"), source: "test" },
|
||||
] as unknown as Parameters<typeof createTestRegistry>[0];
|
||||
setActivePluginRegistry(createTestRegistry(channels));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { hasAnyWhatsAppAuth } from "../../extensions/whatsapp/src/accounts.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import {
|
||||
getChannelPluginCatalogEntry,
|
||||
|
|
@ -9,6 +8,7 @@ import {
|
|||
listChatChannels,
|
||||
normalizeChatChannelId,
|
||||
} from "../channels/registry.js";
|
||||
import { hasAnyWhatsAppAuth } from "../plugin-sdk/whatsapp.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
type PluginManifestRegistry,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS,
|
||||
DISCORD_DEFAULT_LISTENER_TIMEOUT_MS,
|
||||
} from "../../extensions/discord/src/monitor/timeouts.js";
|
||||
} from "../plugin-sdk/discord.js";
|
||||
import { MEDIA_AUDIO_FIELD_HELP } from "./media-audio-field-metadata.js";
|
||||
import { IRC_FIELD_HELP } from "./schema.irc.js";
|
||||
import { describeTalkSilenceTimeoutDefaults } from "./talk-defaults.js";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { normalizeExplicitDiscordSessionKey } from "../../../extensions/discord/src/session-key-normalization.js";
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import { normalizeExplicitDiscordSessionKey } from "../../plugin-sdk/discord.js";
|
||||
|
||||
type ExplicitSessionKeyNormalizer = (sessionKey: string, ctx: MsgContext) => string;
|
||||
type ExplicitSessionKeyNormalizerEntry = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { DiscordPluralKitConfig } from "../../extensions/discord/src/pluralkit.js";
|
||||
import type { DiscordPluralKitConfig } from "../plugin-sdk/discord.js";
|
||||
import type {
|
||||
BlockStreamingChunkConfig,
|
||||
BlockStreamingCoalesceConfig,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { resolveWhatsAppAccount } from "../../../extensions/whatsapp/src/accounts.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
|
|
@ -14,6 +13,7 @@ import {
|
|||
resolveSessionDeliveryTarget,
|
||||
} from "../../infra/outbound/targets.js";
|
||||
import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js";
|
||||
import { resolveWhatsAppAccount } from "../../plugin-sdk/whatsapp.js";
|
||||
import { buildChannelAccountBindings } from "../../routing/bindings.js";
|
||||
import { normalizeAccountId, normalizeAgentId } from "../../routing/session-key.js";
|
||||
import { normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** Resolve an account by id, then fall back to the default account when the primary lacks credentials. */
|
||||
export function resolveAccountWithDefaultFallback<TAccount>(params: {
|
||||
accountId?: string | null;
|
||||
normalizeAccountId: (accountId?: string | null) => string;
|
||||
|
|
@ -23,6 +24,7 @@ export function resolveAccountWithDefaultFallback<TAccount>(params: {
|
|||
return fallback;
|
||||
}
|
||||
|
||||
/** List normalized configured account ids from a raw channel account record map. */
|
||||
export function listConfiguredAccountIds(params: {
|
||||
accounts: Record<string, unknown> | undefined;
|
||||
normalizeAccountId: (accountId: string) => string;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export type AgentMediaPayload = {
|
|||
MediaTypes?: string[];
|
||||
};
|
||||
|
||||
/** Convert outbound media descriptors into the legacy agent payload field layout. */
|
||||
export function buildAgentMediaPayload(
|
||||
mediaList: Array<{ path: string; contentType?: string | null }>,
|
||||
): AgentMediaPayload {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** Lowercase and optionally strip prefixes from allowlist entries before sender comparisons. */
|
||||
export function formatAllowFromLowercase(params: {
|
||||
allowFrom: Array<string | number>;
|
||||
stripPrefixRe?: RegExp;
|
||||
|
|
@ -9,6 +10,7 @@ export function formatAllowFromLowercase(params: {
|
|||
.map((entry) => entry.toLowerCase());
|
||||
}
|
||||
|
||||
/** Normalize allowlist entries through a channel-provided parser or canonicalizer. */
|
||||
export function formatNormalizedAllowFromEntries(params: {
|
||||
allowFrom: Array<string | number>;
|
||||
normalizeEntry: (entry: string) => string | undefined | null;
|
||||
|
|
@ -20,6 +22,7 @@ export function formatNormalizedAllowFromEntries(params: {
|
|||
.filter((entry): entry is string => Boolean(entry));
|
||||
}
|
||||
|
||||
/** Check whether a sender id matches a simple normalized allowlist with wildcard support. */
|
||||
export function isNormalizedSenderAllowed(params: {
|
||||
senderId: string | number;
|
||||
allowFrom: Array<string | number>;
|
||||
|
|
@ -45,6 +48,7 @@ type ParsedChatAllowTarget =
|
|||
| { kind: "chat_identifier"; chatIdentifier: string }
|
||||
| { kind: "handle"; handle: string };
|
||||
|
||||
/** Match chat-aware allowlist entries against sender, chat id, guid, or identifier fields. */
|
||||
export function isAllowedParsedChatSender<TParsed extends ParsedChatAllowTarget>(params: {
|
||||
allowFrom: Array<string | number>;
|
||||
sender: string;
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ function applyAccountScopedAllowlistConfigEdit(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Build the default account-scoped allowlist editor used by channel plugins with config-backed lists. */
|
||||
export function buildAccountScopedAllowlistConfigEditor(params: {
|
||||
channelId: ChannelId;
|
||||
normalize: (params: {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export type BasicAllowlistResolutionEntry = {
|
|||
note?: string;
|
||||
};
|
||||
|
||||
/** Clone allowlist resolution entries into a plain serializable shape for UI and docs output. */
|
||||
export function mapBasicAllowlistResolutionEntries(
|
||||
entries: BasicAllowlistResolutionEntry[],
|
||||
): BasicAllowlistResolutionEntry[] {
|
||||
|
|
@ -18,6 +19,7 @@ export function mapBasicAllowlistResolutionEntries(
|
|||
}));
|
||||
}
|
||||
|
||||
/** Map allowlist inputs sequentially so resolver side effects stay ordered and predictable. */
|
||||
export async function mapAllowlistResolutionInputs<T>(params: {
|
||||
inputs: string[];
|
||||
mapInput: (input: string) => Promise<T> | T;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** Read loose boolean params from tool input that may arrive as booleans or "true"/"false" strings. */
|
||||
export function readBooleanParam(
|
||||
params: Record<string, unknown>,
|
||||
key: string,
|
||||
|
|
|
|||
|
|
@ -10,16 +10,19 @@ import type { OpenClawConfig } from "../config/config.js";
|
|||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
|
||||
/** Coerce mixed allowlist config values into plain strings without trimming or deduping. */
|
||||
export function mapAllowFromEntries(
|
||||
allowFrom: Array<string | number> | null | undefined,
|
||||
): string[] {
|
||||
return (allowFrom ?? []).map((entry) => String(entry));
|
||||
}
|
||||
|
||||
/** Normalize user-facing allowlist entries the same way config and doctor flows expect. */
|
||||
export function formatTrimmedAllowFromEntries(allowFrom: Array<string | number>): string[] {
|
||||
return normalizeStringEntries(allowFrom);
|
||||
}
|
||||
|
||||
/** Collapse nullable config scalars into a trimmed optional string. */
|
||||
export function resolveOptionalConfigString(
|
||||
value: string | number | null | undefined,
|
||||
): string | undefined {
|
||||
|
|
@ -30,6 +33,7 @@ export function resolveOptionalConfigString(
|
|||
return normalized || undefined;
|
||||
}
|
||||
|
||||
/** Build the shared allowlist/default target adapter surface for account-scoped channel configs. */
|
||||
export function createScopedAccountConfigAccessors<ResolvedAccount>(params: {
|
||||
resolveAccount: (params: { cfg: OpenClawConfig; accountId?: string | null }) => ResolvedAccount;
|
||||
resolveAllowFrom: (account: ResolvedAccount) => Array<string | number> | null | undefined;
|
||||
|
|
@ -59,6 +63,7 @@ export function createScopedAccountConfigAccessors<ResolvedAccount>(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Build the common CRUD/config helpers for channels that store multiple named accounts. */
|
||||
export function createScopedChannelConfigBase<
|
||||
ResolvedAccount,
|
||||
Config extends OpenClawConfig = OpenClawConfig,
|
||||
|
|
@ -104,6 +109,7 @@ export function createScopedChannelConfigBase<
|
|||
};
|
||||
}
|
||||
|
||||
/** Convert account-specific DM security fields into the shared runtime policy resolver shape. */
|
||||
export function createScopedDmSecurityResolver<
|
||||
ResolvedAccount extends { accountId?: string | null },
|
||||
>(params: {
|
||||
|
|
@ -143,6 +149,7 @@ export function createScopedDmSecurityResolver<
|
|||
});
|
||||
}
|
||||
|
||||
/** Read the effective WhatsApp allowlist through the active plugin contract. */
|
||||
export function resolveWhatsAppConfigAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
|
@ -153,10 +160,12 @@ export function resolveWhatsAppConfigAllowFrom(params: {
|
|||
: [];
|
||||
}
|
||||
|
||||
/** Format WhatsApp allowlist entries with the same normalization used by the channel plugin. */
|
||||
export function formatWhatsAppConfigAllowFromEntries(allowFrom: Array<string | number>): string[] {
|
||||
return normalizeWhatsAppAllowFromEntries(allowFrom);
|
||||
}
|
||||
|
||||
/** Resolve the effective WhatsApp default recipient after account and root config fallback. */
|
||||
export function resolveWhatsAppConfigDefaultTo(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
|
@ -167,6 +176,7 @@ export function resolveWhatsAppConfigDefaultTo(params: {
|
|||
return (account?.defaultTo ?? root?.defaultTo)?.trim() || undefined;
|
||||
}
|
||||
|
||||
/** Read iMessage allowlist entries from the active plugin's resolved account view. */
|
||||
export function resolveIMessageConfigAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
|
@ -178,6 +188,7 @@ export function resolveIMessageConfigAllowFrom(params: {
|
|||
return mapAllowFromEntries(account.config.allowFrom);
|
||||
}
|
||||
|
||||
/** Resolve the effective iMessage default recipient from the plugin-resolved account config. */
|
||||
export function resolveIMessageConfigDefaultTo(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ type PassiveAccountLifecycleParams<Handle> = {
|
|||
onStop?: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
/** Bind a fixed account id into a status writer so lifecycle code can emit partial snapshots. */
|
||||
export function createAccountStatusSink(params: {
|
||||
accountId: string;
|
||||
setStatus: (next: ChannelAccountSnapshot) => void;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export type ChannelSendRawResult = {
|
|||
error?: string | null;
|
||||
};
|
||||
|
||||
/** Normalize raw channel send results into the shape shared outbound callers expect. */
|
||||
export function buildChannelSendResult(channel: string, result: ChannelSendRawResult) {
|
||||
return {
|
||||
channel,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export type ResolveSenderCommandAuthorizationWithRuntimeParams = Omit<
|
|||
runtime: CommandAuthorizationRuntime;
|
||||
};
|
||||
|
||||
/** Fast-path DM command authorization when only policy and sender allowlist state matter. */
|
||||
export function resolveDirectDmAuthorizationOutcome(params: {
|
||||
isGroup: boolean;
|
||||
dmPolicy: string;
|
||||
|
|
@ -50,6 +51,7 @@ export function resolveDirectDmAuthorizationOutcome(params: {
|
|||
return "allowed";
|
||||
}
|
||||
|
||||
/** Runtime-backed wrapper around sender command authorization for grouped helper surfaces. */
|
||||
export async function resolveSenderCommandAuthorizationWithRuntime(
|
||||
params: ResolveSenderCommandAuthorizationWithRuntimeParams,
|
||||
): ReturnType<typeof resolveSenderCommandAuthorization> {
|
||||
|
|
@ -60,6 +62,7 @@ export async function resolveSenderCommandAuthorizationWithRuntime(
|
|||
});
|
||||
}
|
||||
|
||||
/** Compute effective allowlists and command authorization for one inbound sender. */
|
||||
export async function resolveSenderCommandAuthorization(
|
||||
params: ResolveSenderCommandAuthorizationParams,
|
||||
): Promise<{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
/** Resolve the config path prefix for a channel account, falling back to the root channel section. */
|
||||
export function resolveChannelAccountConfigBasePath(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channelKey: string;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ type DiscordSendMediaOptionInput = DiscordSendOptionInput & {
|
|||
mediaLocalRoots?: readonly string[];
|
||||
};
|
||||
|
||||
/** Build the common Discord send options from SDK-level reply payload fields. */
|
||||
export function buildDiscordSendOptions(input: DiscordSendOptionInput) {
|
||||
return {
|
||||
verbose: false,
|
||||
|
|
@ -20,6 +21,7 @@ export function buildDiscordSendOptions(input: DiscordSendOptionInput) {
|
|||
};
|
||||
}
|
||||
|
||||
/** Extend the base Discord send options with media-specific fields. */
|
||||
export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput) {
|
||||
return {
|
||||
...buildDiscordSendOptions(input),
|
||||
|
|
@ -28,6 +30,7 @@ export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput)
|
|||
};
|
||||
}
|
||||
|
||||
/** Stamp raw Discord send results with the channel id expected by shared outbound flows. */
|
||||
export function tagDiscordChannelResult(result: DiscordSendResult) {
|
||||
return { channel: "discord" as const, ...result };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,15 @@ export {
|
|||
looksLikeDiscordTargetId,
|
||||
normalizeDiscordMessagingTarget,
|
||||
normalizeDiscordOutboundTarget,
|
||||
} from "../channels/plugins/normalize/discord.js";
|
||||
} from "../../extensions/discord/src/normalize.js";
|
||||
export { collectDiscordAuditChannelIds } from "../../extensions/discord/src/audit.js";
|
||||
export { collectDiscordStatusIssues } from "../channels/plugins/status-issues/discord.js";
|
||||
export { collectDiscordStatusIssues } from "../../extensions/discord/src/status-issues.js";
|
||||
export {
|
||||
DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS,
|
||||
DISCORD_DEFAULT_LISTENER_TIMEOUT_MS,
|
||||
} from "../../extensions/discord/src/monitor/timeouts.js";
|
||||
export { normalizeExplicitDiscordSessionKey } from "../../extensions/discord/src/session-key-normalization.js";
|
||||
export type { DiscordPluralKitConfig } from "../../extensions/discord/src/pluralkit.js";
|
||||
|
||||
export {
|
||||
resolveDefaultGroupPolicy,
|
||||
|
|
|
|||
|
|
@ -4,18 +4,21 @@ export const pluginSdkEntrypoints = [...pluginSdkEntryList];
|
|||
|
||||
export const pluginSdkSubpaths = pluginSdkEntrypoints.filter((entry) => entry !== "index");
|
||||
|
||||
/** Map every SDK entrypoint name to its source file path inside the repo. */
|
||||
export function buildPluginSdkEntrySources() {
|
||||
return Object.fromEntries(
|
||||
pluginSdkEntrypoints.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]),
|
||||
);
|
||||
}
|
||||
|
||||
/** List the public package specifiers that should resolve to plugin SDK entrypoints. */
|
||||
export function buildPluginSdkSpecifiers() {
|
||||
return pluginSdkEntrypoints.map((entry) =>
|
||||
entry === "index" ? "openclaw/plugin-sdk" : `openclaw/plugin-sdk/${entry}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Build the package.json exports map for all plugin SDK subpaths. */
|
||||
export function buildPluginSdkPackageExports() {
|
||||
return Object.fromEntries(
|
||||
pluginSdkEntrypoints.map((entry) => [
|
||||
|
|
@ -28,6 +31,7 @@ export function buildPluginSdkPackageExports() {
|
|||
);
|
||||
}
|
||||
|
||||
/** List the dist artifacts expected for every generated plugin SDK entrypoint. */
|
||||
export function listPluginSdkDistArtifacts() {
|
||||
return pluginSdkEntrypoints.flatMap((entry) => [
|
||||
`dist/plugin-sdk/${entry}.js`,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ export {
|
|||
createDefaultChannelRuntimeState,
|
||||
} from "./status-helpers.js";
|
||||
export { withTempDownloadPath } from "./temp-path.js";
|
||||
export {
|
||||
buildFeishuConversationId,
|
||||
parseFeishuConversationId,
|
||||
} from "../../extensions/feishu/src/conversation-id.js";
|
||||
export {
|
||||
createFixedWindowRateLimiter,
|
||||
createWebhookAnomalyTracker,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ function isAuthFailureStatus(status: number): boolean {
|
|||
return status === 401 || status === 403;
|
||||
}
|
||||
|
||||
/** Retry a fetch with bearer tokens from the provided scopes when the unauthenticated attempt fails. */
|
||||
export async function fetchWithBearerAuthScopeFallback(params: {
|
||||
url: string;
|
||||
scopes: readonly string[];
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ async function releaseHeldLock(normalizedFile: string): Promise<void> {
|
|||
await fs.rm(current.lockPath, { force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
/** Acquire a re-entrant process-local file lock backed by a `.lock` sidecar file. */
|
||||
export async function acquireFileLock(
|
||||
filePath: string,
|
||||
options: FileLockOptions,
|
||||
|
|
@ -147,6 +148,7 @@ export async function acquireFileLock(
|
|||
throw new Error(`file lock timeout for ${normalizedFile}`);
|
||||
}
|
||||
|
||||
/** Run an async callback while holding a file lock, always releasing the lock afterward. */
|
||||
export async function withFileLock<T>(
|
||||
filePath: string,
|
||||
options: FileLockOptions,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export type MatchedGroupAccessDecision = {
|
|||
reason: MatchedGroupAccessReason;
|
||||
};
|
||||
|
||||
/** Downgrade sender-scoped group policy to open mode when no allowlist is configured. */
|
||||
export function resolveSenderScopedGroupPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
groupAllowFrom: string[];
|
||||
|
|
@ -50,6 +51,7 @@ export function resolveSenderScopedGroupPolicy(params: {
|
|||
return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
|
||||
}
|
||||
|
||||
/** Evaluate route-level group access after policy, route match, and enablement checks. */
|
||||
export function evaluateGroupRouteAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
routeAllowlistConfigured: boolean;
|
||||
|
|
@ -96,6 +98,7 @@ export function evaluateGroupRouteAccessForPolicy(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Evaluate generic allowlist match state for channels that compare derived group identifiers. */
|
||||
export function evaluateMatchedGroupAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
allowlistConfigured: boolean;
|
||||
|
|
@ -142,6 +145,7 @@ export function evaluateMatchedGroupAccessForPolicy(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Evaluate sender access for an already-resolved group policy and allowlist. */
|
||||
export function evaluateSenderGroupAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
providerMissingFallbackApplied?: boolean;
|
||||
|
|
@ -184,6 +188,7 @@ export function evaluateSenderGroupAccessForPolicy(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Resolve provider fallback policy first, then evaluate sender access against that result. */
|
||||
export function evaluateSenderGroupAccess(params: {
|
||||
providerConfigPresent: boolean;
|
||||
configuredGroupPolicy?: GroupPolicy;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type InboundRouteResolveParams<TConfig, TPeer extends RoutePeerLike> = {
|
|||
peer: TPeer;
|
||||
};
|
||||
|
||||
/** Create an envelope formatter bound to one resolved route and session store. */
|
||||
export function createInboundEnvelopeBuilder<TConfig, TEnvelope>(params: {
|
||||
cfg: TConfig;
|
||||
route: RouteLike;
|
||||
|
|
@ -54,6 +55,7 @@ export function createInboundEnvelopeBuilder<TConfig, TEnvelope>(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Resolve a route first, then return both the route and a formatter for future inbound messages. */
|
||||
export function resolveInboundRouteEnvelopeBuilder<
|
||||
TConfig,
|
||||
TEnvelope,
|
||||
|
|
@ -111,6 +113,7 @@ type InboundRouteEnvelopeRuntime<
|
|||
};
|
||||
};
|
||||
|
||||
/** Runtime-driven variant of inbound envelope resolution for plugins that already expose grouped helpers. */
|
||||
export function resolveInboundRouteEnvelopeBuilderWithRuntime<
|
||||
TConfig,
|
||||
TEnvelope,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type DispatchReplyWithBufferedBlockDispatcherFn =
|
|||
|
||||
type ReplyDispatchFromConfigOptions = Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
||||
|
||||
/** Run `dispatchReplyFromConfig` with a dispatcher that always gets its settled callback. */
|
||||
export async function dispatchReplyFromConfigWithSettledDispatcher(params: {
|
||||
cfg: OpenClawConfig;
|
||||
ctxPayload: FinalizedMsgContext;
|
||||
|
|
@ -40,6 +41,7 @@ export async function dispatchReplyFromConfigWithSettledDispatcher(params: {
|
|||
});
|
||||
}
|
||||
|
||||
/** Assemble the common inbound reply dispatch dependencies for a resolved route. */
|
||||
export function buildInboundReplyDispatchBase(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
|
|
@ -80,6 +82,7 @@ type RecordInboundSessionAndDispatchReplyParams = Parameters<
|
|||
typeof recordInboundSessionAndDispatchReply
|
||||
>[0];
|
||||
|
||||
/** Resolve the shared dispatch base and immediately record + dispatch one inbound reply turn. */
|
||||
export async function dispatchInboundReplyWithBase(
|
||||
params: BuildInboundReplyDispatchBaseParams &
|
||||
Pick<
|
||||
|
|
@ -97,6 +100,7 @@ export async function dispatchInboundReplyWithBase(
|
|||
});
|
||||
}
|
||||
|
||||
/** Record the inbound session first, then dispatch the reply using normalized outbound delivery. */
|
||||
export async function recordInboundSessionAndDispatchReply(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import fs from "node:fs";
|
|||
import { writeJsonAtomic } from "../infra/json-files.js";
|
||||
import { safeParseJson } from "../utils.js";
|
||||
|
||||
/** Read JSON from disk and fall back cleanly when the file is missing or invalid. */
|
||||
export async function readJsonFileWithFallback<T>(
|
||||
filePath: string,
|
||||
fallback: T,
|
||||
|
|
@ -22,6 +23,7 @@ export async function readJsonFileWithFallback<T>(
|
|||
}
|
||||
}
|
||||
|
||||
/** Write JSON with secure file permissions and atomic replacement semantics. */
|
||||
export async function writeJsonFileAtomically(filePath: string, value: unknown): Promise<void> {
|
||||
await writeJsonAtomic(filePath, value, {
|
||||
mode: 0o600,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export type KeyedAsyncQueueHooks = {
|
|||
onSettle?: () => void;
|
||||
};
|
||||
|
||||
/** Serialize async work per key while allowing unrelated keys to run concurrently. */
|
||||
export function enqueueKeyedTask<T>(params: {
|
||||
tails: Map<string, Promise<void>>;
|
||||
key: string;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { createHash, randomBytes } from "node:crypto";
|
||||
|
||||
/** Encode a flat object as application/x-www-form-urlencoded form data. */
|
||||
export function toFormUrlEncoded(data: Record<string, string>): string {
|
||||
return Object.entries(data)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
}
|
||||
|
||||
/** Generate a PKCE verifier/challenge pair suitable for OAuth authorization flows. */
|
||||
export function generatePkceVerifierChallenge(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("base64url");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export type OutboundMediaLoadOptions = {
|
|||
mediaLocalRoots?: readonly string[];
|
||||
};
|
||||
|
||||
/** Load outbound media from a remote URL or approved local path using the shared web-media policy. */
|
||||
export async function loadOutboundMediaFromUrl(
|
||||
mediaUrl: string,
|
||||
options: OutboundMediaLoadOptions = {},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ type ScopedUpsertInput = Omit<
|
|||
"channel" | "accountId"
|
||||
>;
|
||||
|
||||
/** Scope pairing store operations to one channel/account pair for plugin-facing helpers. */
|
||||
export function createScopedPairingAccess(params: {
|
||||
core: PluginRuntime;
|
||||
channel: ChannelId;
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ function pruneData(
|
|||
});
|
||||
}
|
||||
|
||||
/** Create a dedupe helper that combines in-memory fast checks with a lock-protected disk store. */
|
||||
export function createPersistentDedupe(options: PersistentDedupeOptions): PersistentDedupe {
|
||||
const ttlMs = Math.max(0, Math.floor(options.ttlMs));
|
||||
const memoryMaxSize = Math.max(0, Math.floor(options.memoryMaxSize));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { AuthProfileCredential } from "../agents/auth-profiles/types.js";
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ProviderAuthResult } from "../plugins/types.js";
|
||||
|
||||
/** Build the standard auth result payload for OAuth-style provider login flows. */
|
||||
export function buildOauthProviderAuthResult(params: {
|
||||
providerId: string;
|
||||
defaultModel: string;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export type OutboundReplyPayload = {
|
|||
replyToId?: string;
|
||||
};
|
||||
|
||||
/** Extract the supported outbound reply fields from loose tool or agent payload objects. */
|
||||
export function normalizeOutboundReplyPayload(
|
||||
payload: Record<string, unknown>,
|
||||
): OutboundReplyPayload {
|
||||
|
|
@ -24,6 +25,7 @@ export function normalizeOutboundReplyPayload(
|
|||
};
|
||||
}
|
||||
|
||||
/** Wrap a deliverer so callers can hand it arbitrary payloads while channels receive normalized data. */
|
||||
export function createNormalizedOutboundDeliverer(
|
||||
handler: (payload: OutboundReplyPayload) => Promise<void>,
|
||||
): (payload: unknown) => Promise<void> {
|
||||
|
|
@ -36,6 +38,7 @@ export function createNormalizedOutboundDeliverer(
|
|||
};
|
||||
}
|
||||
|
||||
/** Prefer multi-attachment payloads, then fall back to the legacy single-media field. */
|
||||
export function resolveOutboundMediaUrls(payload: {
|
||||
mediaUrls?: string[];
|
||||
mediaUrl?: string;
|
||||
|
|
@ -49,6 +52,7 @@ export function resolveOutboundMediaUrls(payload: {
|
|||
return [];
|
||||
}
|
||||
|
||||
/** Send media-first payloads intact, or chunk text-only payloads through the caller's transport hooks. */
|
||||
export async function sendPayloadWithChunkedTextAndMedia<
|
||||
TContext extends { payload: object },
|
||||
TResult,
|
||||
|
|
@ -90,6 +94,7 @@ export async function sendPayloadWithChunkedTextAndMedia<
|
|||
return lastResult!;
|
||||
}
|
||||
|
||||
/** Detect numeric-looking target ids for channels that distinguish ids from handles. */
|
||||
export function isNumericTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -98,6 +103,7 @@ export function isNumericTargetId(raw: string): boolean {
|
|||
return /^\d{3,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
/** Append attachment links to plain text when the channel cannot send media inline. */
|
||||
export function formatTextWithAttachmentLinks(
|
||||
text: string | undefined,
|
||||
mediaUrls: string[],
|
||||
|
|
@ -118,6 +124,7 @@ export function formatTextWithAttachmentLinks(
|
|||
return `${trimmedText}\n\n${mediaBlock}`;
|
||||
}
|
||||
|
||||
/** Send a caption with only the first media item, mirroring caption-limited channel transports. */
|
||||
export async function sendMediaWithLeadingCaption(params: {
|
||||
mediaUrls: string[];
|
||||
caption: string;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** Extract a string URL from the common request-like inputs accepted by fetch helpers. */
|
||||
export function resolveRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** Format a short note that separates successfully resolved targets from unresolved passthrough values. */
|
||||
export function formatResolvedUnresolvedNote(params: {
|
||||
resolved: string[];
|
||||
unresolved: string[];
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type PluginCommandRunOptions = {
|
|||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
/** Run a plugin-managed command with timeout handling and normalized stdout/stderr results. */
|
||||
export async function runPluginCommandWithTimeout(
|
||||
options: PluginCommandRunOptions,
|
||||
): Promise<PluginCommandRunResult> {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** Create a tiny mutable runtime slot with strict access when the runtime has not been initialized. */
|
||||
export function createPluginRuntimeStore<T>(errorMessage: string): {
|
||||
setRuntime: (next: T) => void;
|
||||
clearRuntime: () => void;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ type LoggerLike = {
|
|||
error: (message: string) => void;
|
||||
};
|
||||
|
||||
/** Adapt a simple logger into the RuntimeEnv contract used by shared plugin SDK helpers. */
|
||||
export function createLoggerBackedRuntime(params: {
|
||||
logger: LoggerLike;
|
||||
exitError?: (code: number) => Error;
|
||||
|
|
@ -23,6 +24,7 @@ export function createLoggerBackedRuntime(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Reuse an existing runtime when present, otherwise synthesize one from the provided logger. */
|
||||
export function resolveRuntimeEnv(params: {
|
||||
runtime?: RuntimeEnv;
|
||||
logger: LoggerLike;
|
||||
|
|
@ -31,6 +33,7 @@ export function resolveRuntimeEnv(params: {
|
|||
return params.runtime ?? createLoggerBackedRuntime(params);
|
||||
}
|
||||
|
||||
/** Resolve a runtime that treats exit requests as unsupported errors instead of process termination. */
|
||||
export function resolveRuntimeEnvWithUnavailableExit(params: {
|
||||
runtime?: RuntimeEnv;
|
||||
logger: LoggerLike;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
SECRET_PROVIDER_ALIAS_PATTERN,
|
||||
} from "../secrets/ref-contract.js";
|
||||
|
||||
/** Build the shared zod schema for secret inputs accepted by plugin auth/config surfaces. */
|
||||
export function buildSecretInputSchema() {
|
||||
const providerSchema = z
|
||||
.string()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ function readSlackBlocksParam(actionParams: Record<string, unknown>) {
|
|||
return parseSlackBlocksInput(actionParams.blocks) as Record<string, unknown>[] | undefined;
|
||||
}
|
||||
|
||||
/** Translate generic channel action requests into Slack-specific tool invocations and payload shapes. */
|
||||
export async function handleSlackMessageAction(params: {
|
||||
providerId: string;
|
||||
ctx: ChannelMessageActionContext;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ function isHostnameAllowedBySuffixAllowlist(
|
|||
return allowlist.some((entry) => normalized === entry || normalized.endsWith(`.${entry}`));
|
||||
}
|
||||
|
||||
/** Normalize suffix-style host allowlists into lowercase canonical entries with wildcard collapse. */
|
||||
export function normalizeHostnameSuffixAllowlist(
|
||||
input?: readonly string[],
|
||||
defaults?: readonly string[],
|
||||
|
|
@ -39,6 +40,7 @@ export function normalizeHostnameSuffixAllowlist(
|
|||
return Array.from(new Set(normalized));
|
||||
}
|
||||
|
||||
/** Check whether a URL is HTTPS and its hostname matches the normalized suffix allowlist. */
|
||||
export function isHttpsUrlAllowedByHostnameSuffixAllowlist(
|
||||
url: string,
|
||||
allowlist: readonly string[],
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ type RuntimeLifecycleSnapshot = {
|
|||
lastOutboundAt?: number | null;
|
||||
};
|
||||
|
||||
/** Create the baseline runtime snapshot shape used by channel/account status stores. */
|
||||
export function createDefaultChannelRuntimeState<T extends Record<string, unknown>>(
|
||||
accountId: string,
|
||||
extra?: T,
|
||||
|
|
@ -29,6 +30,7 @@ export function createDefaultChannelRuntimeState<T extends Record<string, unknow
|
|||
};
|
||||
}
|
||||
|
||||
/** Normalize a channel-level status summary so missing lifecycle fields become explicit nulls. */
|
||||
export function buildBaseChannelStatusSummary(snapshot: {
|
||||
configured?: boolean | null;
|
||||
running?: boolean | null;
|
||||
|
|
@ -45,6 +47,7 @@ export function buildBaseChannelStatusSummary(snapshot: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Extend the base summary with probe fields while preserving stable null defaults. */
|
||||
export function buildProbeChannelStatusSummary<TExtra extends Record<string, unknown>>(
|
||||
snapshot: {
|
||||
configured?: boolean | null;
|
||||
|
|
@ -65,6 +68,7 @@ export function buildProbeChannelStatusSummary<TExtra extends Record<string, unk
|
|||
};
|
||||
}
|
||||
|
||||
/** Build the standard per-account status payload from config metadata plus runtime state. */
|
||||
export function buildBaseAccountStatusSnapshot(params: {
|
||||
account: {
|
||||
accountId: string;
|
||||
|
|
@ -87,6 +91,7 @@ export function buildBaseAccountStatusSnapshot(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Convenience wrapper when the caller already has flattened account fields instead of an account object. */
|
||||
export function buildComputedAccountStatusSnapshot(params: {
|
||||
accountId: string;
|
||||
name?: string;
|
||||
|
|
@ -108,6 +113,7 @@ export function buildComputedAccountStatusSnapshot(params: {
|
|||
});
|
||||
}
|
||||
|
||||
/** Normalize runtime-only account state into the shared status snapshot fields. */
|
||||
export function buildRuntimeAccountStatusSnapshot(params: {
|
||||
runtime?: RuntimeLifecycleSnapshot | null;
|
||||
probe?: unknown;
|
||||
|
|
@ -122,6 +128,7 @@ export function buildRuntimeAccountStatusSnapshot(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Build token-based channel status summaries with optional mode reporting. */
|
||||
export function buildTokenChannelStatusSummary(
|
||||
snapshot: {
|
||||
configured?: boolean | null;
|
||||
|
|
@ -151,6 +158,7 @@ export function buildTokenChannelStatusSummary(
|
|||
};
|
||||
}
|
||||
|
||||
/** Convert account runtime errors into the generic channel status issue format. */
|
||||
export function collectStatusIssuesFromLastError(
|
||||
channel: string,
|
||||
accounts: Array<{ accountId: string; lastError?: unknown }>,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export {
|
|||
export {
|
||||
looksLikeTelegramTargetId,
|
||||
normalizeTelegramMessagingTarget,
|
||||
} from "../channels/plugins/normalize/telegram.js";
|
||||
} from "../../extensions/telegram/src/normalize.js";
|
||||
export {
|
||||
parseTelegramReplyToMessageId,
|
||||
parseTelegramThreadId,
|
||||
|
|
@ -58,7 +58,7 @@ export {
|
|||
normalizeTelegramAllowFromEntry,
|
||||
} from "../../extensions/telegram/src/allow-from.js";
|
||||
export { fetchTelegramChatId } from "../../extensions/telegram/src/api-fetch.js";
|
||||
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
||||
export { collectTelegramStatusIssues } from "../../extensions/telegram/src/status-issues.js";
|
||||
export { sendTelegramPayloadMessages } from "../../extensions/telegram/src/outbound-adapter.js";
|
||||
export {
|
||||
buildBrowseProvidersButton,
|
||||
|
|
@ -72,6 +72,7 @@ export {
|
|||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
} from "../../extensions/telegram/src/exec-approvals.js";
|
||||
export type { StickerMetadata } from "../../extensions/telegram/src/bot/types.js";
|
||||
|
||||
export {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ function isNodeErrorWithCode(err: unknown, code: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/** Build a unique temp file path with sanitized prefix/extension parts. */
|
||||
export function buildRandomTempFilePath(params: {
|
||||
prefix: string;
|
||||
extension?: string;
|
||||
|
|
@ -58,6 +59,7 @@ export function buildRandomTempFilePath(params: {
|
|||
return path.join(resolveTempRoot(params.tmpDir), `${prefix}-${now}-${uuid}${extension}`);
|
||||
}
|
||||
|
||||
/** Create a temporary download directory, run the callback, then clean it up best-effort. */
|
||||
export async function withTempDownloadPath<T>(
|
||||
params: {
|
||||
prefix: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { chunkTextByBreakResolver } from "../shared/text-chunking.js";
|
||||
|
||||
/** Chunk outbound text while preferring newline boundaries over spaces. */
|
||||
export function chunkTextForOutbound(text: string, limit: number): string[] {
|
||||
return chunkTextByBreakResolver(text, limit, (window) => {
|
||||
const lastNewline = window.lastIndexOf("\n");
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** Extract the canonical send target fields from tool arguments when the action matches. */
|
||||
export function extractToolSend(
|
||||
args: Record<string, unknown>,
|
||||
expectedAction = "sendMessage",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export type WebhookAnomalyTracker = {
|
|||
clear: () => void;
|
||||
};
|
||||
|
||||
/** Create a simple fixed-window rate limiter for in-memory webhook protection. */
|
||||
export function createFixedWindowRateLimiter(options: {
|
||||
windowMs: number;
|
||||
maxRequests: number;
|
||||
|
|
@ -104,6 +105,7 @@ export function createFixedWindowRateLimiter(options: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Count keyed events in memory with optional TTL pruning and bounded cardinality. */
|
||||
export function createBoundedCounter(options: {
|
||||
maxTrackedKeys: number;
|
||||
ttlMs?: number;
|
||||
|
|
@ -161,6 +163,7 @@ export function createBoundedCounter(options: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Track repeated webhook failures and emit sampled logs for suspicious request patterns. */
|
||||
export function createWebhookAnomalyTracker(options?: {
|
||||
maxTrackedKeys?: number;
|
||||
ttlMs?: number;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** Normalize webhook paths into the canonical registry form used by route lookup. */
|
||||
export function normalizeWebhookPath(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -10,6 +11,7 @@ export function normalizeWebhookPath(raw: string): string {
|
|||
return withSlash;
|
||||
}
|
||||
|
||||
/** Resolve the effective webhook path from explicit path, URL, or default fallback. */
|
||||
export function resolveWebhookPath(params: {
|
||||
webhookPath?: string;
|
||||
webhookUrl?: string;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ function respondWebhookBodyReadError(params: {
|
|||
return { ok: false };
|
||||
}
|
||||
|
||||
/** Create an in-memory limiter that caps concurrent webhook handlers per key. */
|
||||
export function createWebhookInFlightLimiter(options?: {
|
||||
maxInFlightPerKey?: number;
|
||||
maxTrackedKeys?: number;
|
||||
|
|
@ -127,6 +128,7 @@ export function createWebhookInFlightLimiter(options?: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Detect JSON content types, including structured syntax suffixes like `application/ld+json`. */
|
||||
export function isJsonContentType(value: string | string[] | undefined): boolean {
|
||||
const first = Array.isArray(value) ? value[0] : value;
|
||||
if (!first) {
|
||||
|
|
@ -136,6 +138,7 @@ export function isJsonContentType(value: string | string[] | undefined): boolean
|
|||
return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json"));
|
||||
}
|
||||
|
||||
/** Apply method, rate-limit, and content-type guards before a webhook handler reads the body. */
|
||||
export function applyBasicWebhookRequestGuards(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
|
|
@ -176,6 +179,7 @@ export function applyBasicWebhookRequestGuards(params: {
|
|||
return true;
|
||||
}
|
||||
|
||||
/** Start the shared webhook request lifecycle and return a release hook for in-flight tracking. */
|
||||
export function beginWebhookRequestPipelineOrReject(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
|
|
@ -226,6 +230,7 @@ export function beginWebhookRequestPipelineOrReject(params: {
|
|||
};
|
||||
}
|
||||
|
||||
/** Read a webhook request body with bounded size/time limits and translate failures into responses. */
|
||||
export async function readWebhookBodyOrReject(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
|
|
@ -260,6 +265,7 @@ export async function readWebhookBodyOrReject(params: {
|
|||
}
|
||||
}
|
||||
|
||||
/** Read and parse a JSON webhook body, rejecting malformed or oversized payloads consistently. */
|
||||
export async function readJsonWebhookBodyOrReject(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export type RegisterWebhookPluginRouteOptions = Omit<
|
|||
"path" | "fallbackPath"
|
||||
>;
|
||||
|
||||
/** Register a webhook target and lazily install the matching plugin HTTP route on first use. */
|
||||
export function registerWebhookTargetWithPluginRoute<T extends { path: string }>(params: {
|
||||
targetsByPath: Map<string, T[]>;
|
||||
target: T;
|
||||
|
|
@ -54,6 +55,7 @@ function getPathTeardownMap<T>(targetsByPath: Map<string, T[]>): Map<string, ()
|
|||
return created;
|
||||
}
|
||||
|
||||
/** Add a normalized target to a path bucket and clean up route state when the last target leaves. */
|
||||
export function registerWebhookTarget<T extends { path: string }>(
|
||||
targetsByPath: Map<string, T[]>,
|
||||
target: T,
|
||||
|
|
@ -99,6 +101,7 @@ export function registerWebhookTarget<T extends { path: string }>(
|
|||
return { target: normalizedTarget, unregister };
|
||||
}
|
||||
|
||||
/** Resolve all registered webhook targets for the incoming request path. */
|
||||
export function resolveWebhookTargets<T>(
|
||||
req: IncomingMessage,
|
||||
targetsByPath: Map<string, T[]>,
|
||||
|
|
@ -112,6 +115,7 @@ export function resolveWebhookTargets<T>(
|
|||
return { path, targets };
|
||||
}
|
||||
|
||||
/** Run common webhook guards, then dispatch only when the request path resolves to live targets. */
|
||||
export async function withResolvedWebhookRequestPipeline<T>(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
|
|
@ -183,6 +187,7 @@ function finalizeMatchedWebhookTarget<T>(matched: T | undefined): WebhookTargetM
|
|||
return { kind: "single", target: matched };
|
||||
}
|
||||
|
||||
/** Match exactly one synchronous target or report whether resolution was empty or ambiguous. */
|
||||
export function resolveSingleWebhookTarget<T>(
|
||||
targets: readonly T[],
|
||||
isMatch: (target: T) => boolean,
|
||||
|
|
@ -201,6 +206,7 @@ export function resolveSingleWebhookTarget<T>(
|
|||
return finalizeMatchedWebhookTarget(matched);
|
||||
}
|
||||
|
||||
/** Async variant of single-target resolution for auth checks that need I/O. */
|
||||
export async function resolveSingleWebhookTargetAsync<T>(
|
||||
targets: readonly T[],
|
||||
isMatch: (target: T) => Promise<boolean>,
|
||||
|
|
@ -219,6 +225,7 @@ export async function resolveSingleWebhookTargetAsync<T>(
|
|||
return finalizeMatchedWebhookTarget(matched);
|
||||
}
|
||||
|
||||
/** Resolve an authorized target and send the standard unauthorized or ambiguous response on failure. */
|
||||
export async function resolveWebhookTargetWithAuthOrReject<T>(params: {
|
||||
targets: readonly T[];
|
||||
res: ServerResponse;
|
||||
|
|
@ -234,6 +241,7 @@ export async function resolveWebhookTargetWithAuthOrReject<T>(params: {
|
|||
return resolveWebhookTargetMatchOrReject(params, match);
|
||||
}
|
||||
|
||||
/** Synchronous variant of webhook auth resolution for cheap in-memory match checks. */
|
||||
export function resolveWebhookTargetWithAuthOrRejectSync<T>(params: {
|
||||
targets: readonly T[];
|
||||
res: ServerResponse;
|
||||
|
|
@ -270,6 +278,7 @@ function resolveWebhookTargetMatchOrReject<T>(
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Reject non-POST webhook requests with the conventional Allow header. */
|
||||
export function rejectNonPostWebhookRequest(req: IncomingMessage, res: ServerResponse): boolean {
|
||||
if (req.method === "POST") {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ export {
|
|||
listWhatsAppDirectoryGroupsFromConfig,
|
||||
listWhatsAppDirectoryPeersFromConfig,
|
||||
} from "../channels/plugins/directory-config.js";
|
||||
export {
|
||||
hasAnyWhatsAppAuth,
|
||||
listEnabledWhatsAppAccounts,
|
||||
resolveWhatsAppAccount,
|
||||
} from "../../extensions/whatsapp/src/accounts.js";
|
||||
export { normalizeWhatsAppAllowFromEntries } from "../channels/plugins/normalize/whatsapp.js";
|
||||
export {
|
||||
collectAllowlistProviderGroupPolicyWarnings,
|
||||
collectOpenGroupPolicyRouteAllowlistWarnings,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ function isFilePath(candidate: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
/** Resolve a Windows command name through PATH and PATHEXT so wrapper inspection sees the real file. */
|
||||
export function resolveWindowsExecutablePath(command: string, env: NodeJS.ProcessEnv): string {
|
||||
if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) {
|
||||
return command;
|
||||
|
|
@ -188,6 +189,7 @@ function resolveEntrypointFromPackageJson(
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Resolve the safest direct spawn candidate for Windows wrappers, scripts, and binaries. */
|
||||
export function resolveWindowsSpawnProgramCandidate(
|
||||
params: ResolveWindowsSpawnProgramCandidateParams,
|
||||
): WindowsSpawnProgramCandidate {
|
||||
|
|
@ -250,6 +252,7 @@ export function resolveWindowsSpawnProgramCandidate(
|
|||
};
|
||||
}
|
||||
|
||||
/** Apply shell-fallback policy when Windows wrapper resolution could not find a direct entrypoint. */
|
||||
export function applyWindowsSpawnProgramPolicy(params: {
|
||||
candidate: WindowsSpawnProgramCandidate;
|
||||
allowShellFallback?: boolean;
|
||||
|
|
@ -275,6 +278,7 @@ export function applyWindowsSpawnProgramPolicy(params: {
|
|||
);
|
||||
}
|
||||
|
||||
/** Resolve the final Windows spawn program after candidate discovery and fallback policy. */
|
||||
export function resolveWindowsSpawnProgram(
|
||||
params: ResolveWindowsSpawnProgramParams,
|
||||
): WindowsSpawnProgram {
|
||||
|
|
@ -285,6 +289,7 @@ export function resolveWindowsSpawnProgram(
|
|||
});
|
||||
}
|
||||
|
||||
/** Combine a resolved Windows spawn program with call-site argv for actual process launch. */
|
||||
export function materializeWindowsSpawnProgram(
|
||||
program: WindowsSpawnProgram,
|
||||
argv: string[],
|
||||
|
|
|
|||
|
|
@ -51,7 +51,13 @@ function buildPrompter(): WizardPrompter {
|
|||
intro: async () => {},
|
||||
outro: async () => {},
|
||||
note: async () => {},
|
||||
select: async <T>(params: WizardSelectParams<T>) => params.options[0].value,
|
||||
select: async <T>(params: WizardSelectParams<T>) => {
|
||||
const option = params.options[0];
|
||||
if (!option) {
|
||||
throw new Error("missing select option");
|
||||
}
|
||||
return option.value;
|
||||
},
|
||||
multiselect: async <T>(params: WizardMultiSelectParams<T>) => params.initialValues ?? [],
|
||||
text: async () => "",
|
||||
confirm: async () => false,
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ function requireProvider(providers: ProviderPlugin[], providerId: string) {
|
|||
return provider;
|
||||
}
|
||||
|
||||
function buildTestModel(params: { id: string; name: string }): ModelDefinitionConfig {
|
||||
function createModelConfig(id: string, name = id): ModelDefinitionConfig {
|
||||
return {
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
id,
|
||||
name,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
|
|
@ -75,7 +75,6 @@ function buildTestModel(params: { id: string; name: string }): ModelDefinitionCo
|
|||
maxTokens: 8_192,
|
||||
};
|
||||
}
|
||||
|
||||
describe("provider discovery contract", () => {
|
||||
afterEach(() => {
|
||||
resolveCopilotApiTokenMock.mockReset();
|
||||
|
|
@ -244,7 +243,7 @@ describe("provider discovery contract", () => {
|
|||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "http://ollama-host:11434/v1/",
|
||||
models: [buildTestModel({ id: "llama3.2", name: "llama3.2" })],
|
||||
models: [createModelConfig("llama3.2")],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -257,7 +256,7 @@ describe("provider discovery contract", () => {
|
|||
baseUrl: "http://ollama-host:11434",
|
||||
api: "ollama",
|
||||
apiKey: "ollama-local",
|
||||
models: [expect.objectContaining({ id: "llama3.2", name: "llama3.2" })],
|
||||
models: [createModelConfig("llama3.2")],
|
||||
},
|
||||
});
|
||||
expect(buildOllamaProviderMock).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { auditDiscordChannelPermissions } from "../../../extensions/discord/src/audit.js";
|
||||
import { discordMessageActions } from "../../../extensions/discord/src/channel-actions.js";
|
||||
import {
|
||||
listDiscordDirectoryGroupsLive,
|
||||
listDiscordDirectoryPeersLive,
|
||||
|
|
@ -29,7 +30,6 @@ import {
|
|||
sendTypingDiscord,
|
||||
unpinMessageDiscord,
|
||||
} from "../../../extensions/discord/src/send.js";
|
||||
import { discordMessageActions } from "../../channels/plugins/actions/discord.js";
|
||||
import { createDiscordTypingLease } from "./runtime-discord-typing.js";
|
||||
import type { PluginRuntimeChannel } from "./types-channel.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
auditTelegramGroupMembership,
|
||||
collectTelegramUnmentionedGroupIds,
|
||||
} from "../../../extensions/telegram/src/audit.js";
|
||||
import { telegramMessageActions } from "../../../extensions/telegram/src/channel-actions.js";
|
||||
import { monitorTelegramProvider } from "../../../extensions/telegram/src/monitor.js";
|
||||
import { probeTelegram } from "../../../extensions/telegram/src/probe.js";
|
||||
import {
|
||||
|
|
@ -20,7 +21,6 @@ import {
|
|||
setTelegramThreadBindingMaxAgeBySessionKey,
|
||||
} from "../../../extensions/telegram/src/thread-bindings.js";
|
||||
import { resolveTelegramToken } from "../../../extensions/telegram/src/token.js";
|
||||
import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js";
|
||||
import { createTelegramTypingLease } from "./runtime-telegram-typing.js";
|
||||
import type { PluginRuntimeChannel } from "./types-channel.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export type PluginRuntimeChannel = {
|
|||
shouldHandleTextCommands: typeof import("../../auto-reply/commands-registry.js").shouldHandleTextCommands;
|
||||
};
|
||||
discord: {
|
||||
messageActions: typeof import("../../channels/plugins/actions/discord.js").discordMessageActions;
|
||||
messageActions: typeof import("../../../extensions/discord/src/channel-actions.js").discordMessageActions;
|
||||
auditChannelPermissions: typeof import("../../../extensions/discord/src/audit.js").auditDiscordChannelPermissions;
|
||||
listDirectoryGroupsLive: typeof import("../../../extensions/discord/src/directory-live.js").listDiscordDirectoryGroupsLive;
|
||||
listDirectoryPeersLive: typeof import("../../../extensions/discord/src/directory-live.js").listDiscordDirectoryPeersLive;
|
||||
|
|
@ -147,7 +147,7 @@ export type PluginRuntimeChannel = {
|
|||
sendMessageTelegram: typeof import("../../../extensions/telegram/src/send.js").sendMessageTelegram;
|
||||
sendPollTelegram: typeof import("../../../extensions/telegram/src/send.js").sendPollTelegram;
|
||||
monitorTelegramProvider: typeof import("../../../extensions/telegram/src/monitor.js").monitorTelegramProvider;
|
||||
messageActions: typeof import("../../channels/plugins/actions/telegram.js").telegramMessageActions;
|
||||
messageActions: typeof import("../../../extensions/telegram/src/channel-actions.js").telegramMessageActions;
|
||||
threadBindings: {
|
||||
setIdleTimeoutBySessionKey: typeof import("../../../extensions/telegram/src/thread-bindings.js").setTelegramThreadBindingIdleTimeoutBySessionKey;
|
||||
setMaxAgeBySessionKey: typeof import("../../../extensions/telegram/src/thread-bindings.js").setTelegramThreadBindingMaxAgeBySessionKey;
|
||||
|
|
|
|||
Loading…
Reference in New Issue