fix(release): isolate bundled config docs loading

This commit is contained in:
Vincent Koc
2026-03-18 16:01:31 -07:00
parent 7d8d3d9d77
commit 757c2cc2de
39 changed files with 568 additions and 74 deletions

View File

@@ -15230,7 +15230,7 @@
"network"
],
"label": "Feishu",
"help": "飞书/Lark enterprise messaging.",
"help": "飞书/Lark enterprise messaging with doc/wiki/drive tools.",
"hasChildren": true
},
{
@@ -17232,7 +17232,7 @@
"network"
],
"label": "Google Chat",
"help": "Google Workspace Chat app with HTTP webhook.",
"help": "Google Workspace Chat app via HTTP webhooks.",
"hasChildren": true
},
{
@@ -22069,7 +22069,7 @@
"network"
],
"label": "Matrix",
"help": "open protocol; configure a homeserver + access token.",
"help": "open protocol; install the plugin to enable.",
"hasChildren": true
},
{
@@ -26190,7 +26190,7 @@
"network"
],
"label": "Nostr",
"help": "Decentralized DMs via Nostr relays (NIP-04)",
"help": "Decentralized protocol; encrypted DMs via NIP-04.",
"hasChildren": true
},
{
@@ -30798,7 +30798,7 @@
"network"
],
"label": "Synology Chat",
"help": "Connect your Synology NAS Chat to OpenClaw",
"help": "Connect your Synology NAS Chat to OpenClaw with full agent capabilities.",
"hasChildren": true
},
{
@@ -34814,7 +34814,7 @@
"network"
],
"label": "Tlon",
"help": "Decentralized messaging on Urbit",
"help": "decentralized messaging on Urbit; install the plugin to enable.",
"hasChildren": true
},
{

View File

@@ -1352,7 +1352,7 @@
{"recordType":"path","path":"channels.discord.voice.tts.provider","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.summaryModel","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.feishu","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Feishu","help":"飞书/Lark enterprise messaging.","hasChildren":true}
{"recordType":"path","path":"channels.feishu","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Feishu","help":"飞书/Lark enterprise messaging with doc/wiki/drive tools.","hasChildren":true}
{"recordType":"path","path":"channels.feishu.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.feishu.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.feishu.accounts.*.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -1532,7 +1532,7 @@
{"recordType":"path","path":"channels.feishu.webhookHost","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.feishu.webhookPath","kind":"channel","type":"string","required":true,"defaultValue":"/feishu/events","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.feishu.webhookPort","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.googlechat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Google Chat","help":"Google Workspace Chat app with HTTP webhook.","hasChildren":true}
{"recordType":"path","path":"channels.googlechat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Google Chat","help":"Google Workspace Chat app via HTTP webhooks.","hasChildren":true}
{"recordType":"path","path":"channels.googlechat.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.googlechat.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.googlechat.accounts.*.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -1980,7 +1980,7 @@
{"recordType":"path","path":"channels.line.secretFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.line.tokenFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.line.webhookPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Matrix","help":"open protocol; configure a homeserver + access token.","hasChildren":true}
{"recordType":"path","path":"channels.matrix","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Matrix","help":"open protocol; install the plugin to enable.","hasChildren":true}
{"recordType":"path","path":"channels.matrix.accessToken","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.matrix.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.matrix.accounts.*","kind":"channel","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2362,7 +2362,7 @@
{"recordType":"path","path":"channels.nextcloud-talk.webhookPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.webhookPort","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk.webhookPublicUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nostr","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Nostr","help":"Decentralized DMs via Nostr relays (NIP-04)","hasChildren":true}
{"recordType":"path","path":"channels.nostr","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Nostr","help":"Decentralized protocol; encrypted DMs via NIP-04.","hasChildren":true}
{"recordType":"path","path":"channels.nostr.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.nostr.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nostr.defaultAccount","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2779,7 +2779,7 @@
{"recordType":"path","path":"channels.slack.userToken.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.slack.userTokenReadOnly","kind":"channel","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":["auth","channels","network","security"],"label":"Slack User Token Read Only","help":"When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes.","hasChildren":false}
{"recordType":"path","path":"channels.slack.webhookPath","kind":"channel","type":"string","required":true,"defaultValue":"/slack/events","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.synology-chat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Synology Chat","help":"Connect your Synology NAS Chat to OpenClaw","hasChildren":true}
{"recordType":"path","path":"channels.synology-chat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Synology Chat","help":"Connect your Synology NAS Chat to OpenClaw with full agent capabilities.","hasChildren":true}
{"recordType":"path","path":"channels.synology-chat.*","kind":"channel","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram","help":"simplest way to get started — register a bot with @BotFather and get going.","hasChildren":true}
{"recordType":"path","path":"channels.telegram.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -3139,7 +3139,7 @@
{"recordType":"path","path":"channels.telegram.webhookSecret.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram.webhookSecret.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.telegram.webhookUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.tlon","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Tlon","help":"Decentralized messaging on Urbit","hasChildren":true}
{"recordType":"path","path":"channels.tlon","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Tlon","help":"decentralized messaging on Urbit; install the plugin to enable.","hasChildren":true}
{"recordType":"path","path":"channels.tlon.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.tlon.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.tlon.accounts.*.allowPrivateNetwork","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}

View File

@@ -1,4 +1,6 @@
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { bluebubblesPlugin } from "./src/channel.js";
import { bluebubblesSetupPlugin } from "./src/channel.setup.js";
export default defineSetupPluginEntry(bluebubblesPlugin);
export { bluebubblesSetupPlugin } from "./src/channel.setup.js";
export default defineSetupPluginEntry(bluebubblesSetupPlugin);

View File

@@ -1,5 +1,6 @@
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { createAccountListHelpers, type OpenClawConfig } from "./runtime-api.js";
import { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers";
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";

View File

@@ -0,0 +1,76 @@
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
import {
listBlueBubblesAccountIds,
type ResolvedBlueBubblesAccount,
resolveBlueBubblesAccount,
resolveDefaultBlueBubblesAccountId,
} from "./accounts.js";
import { BlueBubblesConfigSchema } from "./config-schema.js";
import { blueBubblesSetupAdapter } from "./setup-core.js";
import { blueBubblesSetupWizard } from "./setup-surface.js";
import { normalizeBlueBubblesHandle } from "./targets.js";
const meta = {
id: "bluebubbles",
label: "BlueBubbles",
selectionLabel: "BlueBubbles (macOS app)",
detailLabel: "BlueBubbles",
docsPath: "/channels/bluebubbles",
docsLabel: "bluebubbles",
blurb: "iMessage via the BlueBubbles mac app + REST API.",
systemImage: "bubble.left.and.text.bubble.right",
aliases: ["bb"],
order: 75,
preferOver: ["imessage"],
} as const;
const bluebubblesConfigAdapter = createScopedChannelConfigAdapter<ResolvedBlueBubblesAccount>({
sectionKey: "bluebubbles",
listAccountIds: listBlueBubblesAccountIds,
resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultBlueBubblesAccountId,
clearBaseFields: ["serverUrl", "password", "name", "webhookPath"],
resolveAllowFrom: (account: ResolvedBlueBubblesAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
allowFrom,
normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
}),
});
export const bluebubblesSetupPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
id: "bluebubbles",
meta: {
...meta,
aliases: [...meta.aliases],
preferOver: [...meta.preferOver],
},
capabilities: {
chatTypes: ["direct", "group"],
media: true,
reactions: true,
edit: true,
unsend: true,
reply: true,
effects: true,
groupManagement: true,
},
reload: { configPrefixes: ["channels.bluebubbles"] },
configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema),
setupWizard: blueBubblesSetupWizard,
config: {
...bluebubblesConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
baseUrl: account.baseUrl,
}),
},
setup: blueBubblesSetupAdapter,
};

View File

@@ -1,4 +1,5 @@
import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "./runtime-api.js";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
type BlueBubblesConfigPatch = {
serverUrl?: string;

View File

@@ -3,9 +3,10 @@ import {
buildCatchallMultiAccountChannelSchema,
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
ToolPolicySchema,
} from "openclaw/plugin-sdk/channel-config-schema";
import { z } from "zod";
import { MarkdownConfigSchema, ToolPolicySchema } from "./runtime-api.js";
import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";
const bluebubblesActionSchema = z

View File

@@ -1,9 +1,12 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
import { normalizeWebhookPath, type OpenClawConfig } from "./runtime-api.js";
import { getBlueBubblesRuntime } from "./runtime.js";
import type { BlueBubblesAccountConfig } from "./types.js";
export { normalizeWebhookPath };
export {
DEFAULT_WEBHOOK_PATH,
normalizeWebhookPath,
resolveWebhookPathFromConfig,
} from "./webhook-shared.js";
export type BlueBubblesRuntimeEnv = {
log?: (message: string) => void;
@@ -29,13 +32,3 @@ export type WebhookTarget = {
path: string;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
};
export const DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook";
export function resolveWebhookPathFromConfig(config?: BlueBubblesAccountConfig): string {
const raw = config?.webhookPath?.trim();
if (raw) {
return normalizeWebhookPath(raw);
}
return DEFAULT_WEBHOOK_PATH;
}

View File

@@ -1,10 +1,9 @@
import {
buildSecretInputSchema,
hasConfiguredSecretInput,
normalizeResolvedSecretInputString,
normalizeSecretInputString,
} from "./runtime-api.js";
} from "openclaw/plugin-sdk/secret-input-runtime";
import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input-schema";
export {
buildSecretInputSchema,
hasConfiguredSecretInput,

View File

@@ -3,7 +3,7 @@ import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/chan
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { DEFAULT_WEBHOOK_PATH } from "./monitor-shared.js";
import { DEFAULT_WEBHOOK_PATH } from "./webhook-shared.js";
async function createBlueBubblesConfigureAdapter() {
const { blueBubblesSetupAdapter, blueBubblesSetupWizard } = await import("./setup-surface.js");

View File

@@ -14,7 +14,6 @@ import {
resolveDefaultBlueBubblesAccountId,
} from "./accounts.js";
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
import { DEFAULT_WEBHOOK_PATH } from "./monitor-shared.js";
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
import {
blueBubblesSetupAdapter,
@@ -23,6 +22,7 @@ import {
} from "./setup-core.js";
import { parseBlueBubblesAllowTarget } from "./targets.js";
import { normalizeBlueBubblesServerUrl } from "./types.js";
import { DEFAULT_WEBHOOK_PATH } from "./webhook-shared.js";
const channel = "bluebubbles" as const;
const CONFIGURE_CUSTOM_WEBHOOK_FLAG = "__bluebubblesConfigureCustomWebhookPath";

View File

@@ -1,11 +1,11 @@
import { isAllowedParsedChatSender } from "openclaw/plugin-sdk/allow-from";
import {
isAllowedParsedChatSender,
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
type ParsedChatTarget,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
} from "./runtime-api.js";
} from "openclaw/plugin-sdk/imessage-core";
export type BlueBubblesService = "imessage" | "sms" | "auto";

View File

@@ -1,6 +1,6 @@
import type { DmPolicy, GroupPolicy } from "./runtime-api.js";
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/setup";
export type { DmPolicy, GroupPolicy } from "./runtime-api.js";
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/setup";
export type BlueBubblesGroupConfig = {
/** If true, only respond in this group when mentioned. */

View File

@@ -0,0 +1,14 @@
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-path";
import type { BlueBubblesAccountConfig } from "./types.js";
export { normalizeWebhookPath };
export const DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook";
export function resolveWebhookPathFromConfig(config?: BlueBubblesAccountConfig): string {
const raw = config?.webhookPath?.trim();
if (raw) {
return normalizeWebhookPath(raw);
}
return DEFAULT_WEBHOOK_PATH;
}

View File

@@ -8,6 +8,16 @@
"./index.ts"
],
"setupEntry": "./setup-entry.ts",
"channel": {
"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"
},
"release": {
"publishToNpm": true
}

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, DiscordConfigSchema } from "openclaw/plugin-sdk/discord-core";
export const DiscordChannelConfigSchema = buildChannelConfigSchema(DiscordConfigSchema);

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, GoogleChatConfigSchema } from "../runtime-api.js";
export const GoogleChatChannelConfigSchema = buildChannelConfigSchema(GoogleChatConfigSchema);

View File

@@ -8,6 +8,19 @@
"extensions": [
"./index.ts"
],
"setupEntry": "./setup-entry.ts"
"setupEntry": "./setup-entry.ts",
"channel": {
"id": "imessage",
"label": "iMessage",
"selectionLabel": "iMessage (imsg)",
"detailLabel": "iMessage",
"docsPath": "/channels/imessage",
"docsLabel": "imessage",
"blurb": "this is still a work in progress.",
"aliases": [
"imsg"
],
"systemImage": "message.fill"
}
}
}

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, IMessageConfigSchema } from "openclaw/plugin-sdk/imessage-core";
export const IMessageChannelConfigSchema = buildChannelConfigSchema(IMessageConfigSchema);

View File

@@ -10,6 +10,16 @@
"extensions": [
"./index.ts"
],
"setupEntry": "./setup-entry.ts"
"setupEntry": "./setup-entry.ts",
"channel": {
"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"
}
}
}

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, LineConfigSchema } from "../api.js";
export const LineChannelConfigSchema = buildChannelConfigSchema(LineConfigSchema);

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, MSTeamsConfigSchema } from "../runtime-api.js";
export const MSTeamsChannelConfigSchema = buildChannelConfigSchema(MSTeamsConfigSchema);

View File

@@ -8,6 +8,16 @@
"extensions": [
"./index.ts"
],
"setupEntry": "./setup-entry.ts"
"setupEntry": "./setup-entry.ts",
"channel": {
"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"
}
}
}

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, SignalConfigSchema } from "openclaw/plugin-sdk/signal-core";
export const SignalChannelConfigSchema = buildChannelConfigSchema(SignalConfigSchema);

View File

@@ -8,6 +8,16 @@
"extensions": [
"./index.ts"
],
"setupEntry": "./setup-entry.ts"
"setupEntry": "./setup-entry.ts",
"channel": {
"id": "slack",
"label": "Slack",
"selectionLabel": "Slack (Socket Mode)",
"detailLabel": "Slack Bot",
"docsPath": "/channels/slack",
"docsLabel": "slack",
"blurb": "supported (Socket Mode).",
"systemImage": "number"
}
}
}

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, SlackConfigSchema } from "openclaw/plugin-sdk/slack-core";
export const SlackChannelConfigSchema = buildChannelConfigSchema(SlackConfigSchema);

View File

@@ -0,0 +1,4 @@
import { z } from "zod";
import { buildChannelConfigSchema } from "../api.js";
export const SynologyChatChannelConfigSchema = buildChannelConfigSchema(z.object({}).passthrough());

View File

@@ -8,6 +8,16 @@
"extensions": [
"./index.ts"
],
"setupEntry": "./setup-entry.ts"
"setupEntry": "./setup-entry.ts",
"channel": {
"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"
}
}
}

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, TelegramConfigSchema } from "openclaw/plugin-sdk/telegram-core";
export const TelegramChannelConfigSchema = buildChannelConfigSchema(TelegramConfigSchema);

View File

@@ -12,6 +12,16 @@
"openclaw": {
"extensions": [
"./index.ts"
]
],
"channel": {
"id": "twitch",
"label": "Twitch",
"selectionLabel": "Twitch (Chat)",
"docsPath": "/channels/twitch",
"blurb": "Twitch chat integration",
"aliases": [
"twitch-chat"
]
}
}
}

View File

@@ -8,6 +8,16 @@
"extensions": [
"./index.ts"
],
"setupEntry": "./setup-entry.ts"
"setupEntry": "./setup-entry.ts",
"channel": {
"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"
}
}
}

View File

@@ -0,0 +1,3 @@
import { buildChannelConfigSchema, WhatsAppConfigSchema } from "openclaw/plugin-sdk/whatsapp-core";
export const WhatsAppChannelConfigSchema = buildChannelConfigSchema(WhatsAppConfigSchema);

View File

@@ -494,6 +494,10 @@
"types": "./dist/plugin-sdk/request-url.d.ts",
"default": "./dist/plugin-sdk/request-url.js"
},
"./plugin-sdk/webhook-path": {
"types": "./dist/plugin-sdk/webhook-path.d.ts",
"default": "./dist/plugin-sdk/webhook-path.js"
},
"./plugin-sdk/runtime-store": {
"types": "./dist/plugin-sdk/runtime-store.d.ts",
"default": "./dist/plugin-sdk/runtime-store.js"
@@ -522,6 +526,10 @@
"types": "./dist/plugin-sdk/secret-input-schema.d.ts",
"default": "./dist/plugin-sdk/secret-input-schema.js"
},
"./plugin-sdk/secret-input-runtime": {
"types": "./dist/plugin-sdk/secret-input-runtime.d.ts",
"default": "./dist/plugin-sdk/secret-input-runtime.js"
},
"./cli-entry": "./openclaw.mjs"
},
"scripts": {

View File

@@ -113,11 +113,13 @@
"media-understanding",
"google",
"request-url",
"webhook-path",
"runtime-store",
"web-media",
"speech",
"state-paths",
"temp-path",
"tool-send",
"secret-input-schema"
"secret-input-schema",
"secret-input-runtime"
]

View File

@@ -0,0 +1,56 @@
import { pathToFileURL } from "node:url";
import { buildChannelConfigSchema } from "../src/channels/plugins/config-schema.js";
function isBuiltChannelConfigSchema(
value: unknown,
): value is { schema: Record<string, unknown>; uiHints?: Record<string, unknown> } {
if (!value || typeof value !== "object") {
return false;
}
const candidate = value as { schema?: unknown };
return Boolean(candidate.schema && typeof candidate.schema === "object");
}
function resolveConfigSchemaExport(
imported: Record<string, unknown>,
): { schema: Record<string, unknown>; uiHints?: Record<string, unknown> } | null {
for (const [name, value] of Object.entries(imported)) {
if (name.endsWith("ChannelConfigSchema") && isBuiltChannelConfigSchema(value)) {
return value;
}
}
for (const [name, value] of Object.entries(imported)) {
if (!name.endsWith("ConfigSchema") || name.endsWith("AccountConfigSchema")) {
continue;
}
if (isBuiltChannelConfigSchema(value)) {
return value;
}
if (value && typeof value === "object") {
return buildChannelConfigSchema(value as never);
}
}
for (const value of Object.values(imported)) {
if (isBuiltChannelConfigSchema(value)) {
return value;
}
}
return null;
}
const modulePath = process.argv[2]?.trim();
if (!modulePath) {
process.exit(2);
}
const imported = (await import(pathToFileURL(modulePath).href)) as Record<string, unknown>;
const resolved = resolveConfigSchemaExport(imported);
if (!resolved) {
process.exit(3);
}
process.stdout.write(JSON.stringify(resolved));
process.exit(0);

View File

@@ -1,7 +1,8 @@
import fs from "node:fs/promises";
import { spawnSync } from "node:child_process";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { fileURLToPath } from "node:url";
import type { ChannelPlugin } from "../channels/plugins/index.js";
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
@@ -27,6 +28,20 @@ type JsonSchemaObject = JsonSchemaNode & {
oneOf?: JsonSchemaObject[];
};
type PackageChannelMetadata = {
id: string;
label: string;
blurb?: string;
};
type ChannelSurfaceMetadata = {
id: string;
label: string;
description?: string;
configSchema?: Record<string, unknown>;
configUiHints?: ConfigSchemaResponse["uiHints"];
};
export type ConfigDocBaselineKind = "core" | "channel" | "plugin";
export type ConfigDocBaselineEntry = {
@@ -65,6 +80,13 @@ export type ConfigDocBaselineStatefileWriteResult = {
const GENERATED_BY = "scripts/generate-config-doc-baseline.ts" as const;
const DEFAULT_JSON_OUTPUT = "docs/.generated/config-baseline.json";
const DEFAULT_STATEFILE_OUTPUT = "docs/.generated/config-baseline.jsonl";
function logConfigDocBaselineDebug(message: string): void {
if (process.env.OPENCLAW_CONFIG_DOC_BASELINE_DEBUG === "1") {
console.error(`[config-doc-baseline] ${message}`);
}
}
function resolveRepoRoot(): string {
const fromPackage = resolveOpenClawPackageRootSync({
cwd: path.dirname(fileURLToPath(import.meta.url)),
@@ -242,10 +264,10 @@ function resolveEntryKind(configPath: string): ConfigDocBaselineKind {
return "core";
}
async function resolveFirstExistingPath(candidates: string[]): Promise<string | null> {
function resolveFirstExistingPath(candidates: string[]): string | null {
for (const candidate of candidates) {
try {
await fs.access(candidate);
fsSync.accessSync(candidate);
return candidate;
} catch {
// Keep scanning for other source file variants.
@@ -254,6 +276,39 @@ async function resolveFirstExistingPath(candidates: string[]): Promise<string |
return null;
}
function loadPackageChannelMetadata(rootDir: string): PackageChannelMetadata | null {
try {
const packageJson = JSON.parse(
fsSync.readFileSync(path.join(rootDir, "package.json"), "utf8"),
) as {
openclaw?: {
channel?: {
id?: unknown;
label?: unknown;
blurb?: unknown;
};
};
};
const channel = packageJson.openclaw?.channel;
if (!channel) {
return null;
}
const id = typeof channel.id === "string" ? channel.id.trim() : "";
const label = typeof channel.label === "string" ? channel.label.trim() : "";
const blurb = typeof channel.blurb === "string" ? channel.blurb.trim() : "";
if (!id || !label) {
return null;
}
return {
id,
label,
...(blurb ? { blurb } : {}),
};
} catch {
return null;
}
}
function isChannelPlugin(value: unknown): value is ChannelPlugin {
if (!value || typeof value !== "object") {
return false;
@@ -262,8 +317,21 @@ function isChannelPlugin(value: unknown): value is ChannelPlugin {
return typeof candidate.id === "string" && typeof candidate.meta === "object";
}
function resolveSetupChannelPlugin(value: unknown): ChannelPlugin | null {
if (!value || typeof value !== "object") {
return null;
}
const candidate = value as { plugin?: unknown };
return isChannelPlugin(candidate.plugin) ? candidate.plugin : null;
}
async function importChannelPluginModule(rootDir: string): Promise<ChannelPlugin> {
const modulePath = await resolveFirstExistingPath([
logConfigDocBaselineDebug(`resolve channel module ${rootDir}`);
const modulePath = resolveFirstExistingPath([
path.join(rootDir, "setup-entry.ts"),
path.join(rootDir, "setup-entry.js"),
path.join(rootDir, "setup-entry.mts"),
path.join(rootDir, "setup-entry.mjs"),
path.join(rootDir, "src", "channel.ts"),
path.join(rootDir, "src", "channel.js"),
path.join(rootDir, "src", "plugin.ts"),
@@ -279,14 +347,23 @@ async function importChannelPluginModule(rootDir: string): Promise<ChannelPlugin
throw new Error(`channel source not found under ${rootDir}`);
}
const imported = (await import(pathToFileURL(modulePath).href)) as Record<string, unknown>;
logConfigDocBaselineDebug(`import channel module ${modulePath}`);
const imported = (await import(modulePath)) as Record<string, unknown>;
logConfigDocBaselineDebug(`imported channel module ${modulePath}`);
for (const value of Object.values(imported)) {
if (isChannelPlugin(value)) {
logConfigDocBaselineDebug(`resolved channel export ${modulePath}`);
return value;
}
const setupPlugin = resolveSetupChannelPlugin(value);
if (setupPlugin) {
logConfigDocBaselineDebug(`resolved setup channel export ${modulePath}`);
return setupPlugin;
}
if (typeof value === "function" && value.length === 0) {
const resolved = value();
if (isChannelPlugin(resolved)) {
logConfigDocBaselineDebug(`resolved channel factory ${modulePath}`);
return resolved;
}
}
@@ -295,6 +372,91 @@ async function importChannelPluginModule(rootDir: string): Promise<ChannelPlugin
throw new Error(`channel plugin export not found in ${modulePath}`);
}
async function importChannelSurfaceMetadata(
rootDir: string,
repoRoot: string,
): Promise<ChannelSurfaceMetadata | null> {
logConfigDocBaselineDebug(`resolve channel config surface ${rootDir}`);
const packageMetadata = loadPackageChannelMetadata(rootDir);
if (!packageMetadata) {
logConfigDocBaselineDebug(`missing package channel metadata ${rootDir}`);
return null;
}
const modulePath = resolveFirstExistingPath([
path.join(rootDir, "src", "config-schema.ts"),
path.join(rootDir, "src", "config-schema.js"),
path.join(rootDir, "src", "config-schema.mts"),
path.join(rootDir, "src", "config-schema.mjs"),
]);
if (!modulePath) {
logConfigDocBaselineDebug(`missing channel config schema module ${rootDir}`);
return null;
}
logConfigDocBaselineDebug(`import channel config schema ${modulePath}`);
try {
logConfigDocBaselineDebug(`spawn channel config schema subprocess ${modulePath}`);
const result = spawnSync(
process.execPath,
[
"--import",
"tsx",
path.join(repoRoot, "scripts", "load-channel-config-surface.ts"),
modulePath,
],
{
cwd: repoRoot,
encoding: "utf8",
timeout: 15_000,
maxBuffer: 10 * 1024 * 1024,
},
);
if (result.status !== 0 || result.error) {
throw result.error ?? new Error(result.stderr || `child exited with status ${result.status}`);
}
logConfigDocBaselineDebug(`completed channel config schema subprocess ${modulePath}`);
const configSchema = JSON.parse(result.stdout) as {
schema: Record<string, unknown>;
uiHints?: ConfigSchemaResponse["uiHints"];
};
return {
id: packageMetadata.id,
label: packageMetadata.label,
description: packageMetadata.blurb,
configSchema: configSchema.schema,
configUiHints: configSchema.uiHints,
};
} catch (error) {
logConfigDocBaselineDebug(
`channel config schema subprocess failed for ${modulePath}: ${String(error)}`,
);
return null;
}
}
async function loadChannelSurfaceMetadata(
rootDir: string,
repoRoot: string,
): Promise<ChannelSurfaceMetadata> {
logConfigDocBaselineDebug(`load channel surface ${rootDir}`);
const configSurface = await importChannelSurfaceMetadata(rootDir, repoRoot);
if (configSurface) {
logConfigDocBaselineDebug(`resolved channel config surface ${rootDir}`);
return configSurface;
}
logConfigDocBaselineDebug(`fallback to channel plugin import ${rootDir}`);
const plugin = await importChannelPluginModule(rootDir);
return {
id: plugin.id,
label: plugin.meta.label,
description: plugin.meta.blurb,
configSchema: plugin.configSchema?.schema,
configUiHints: plugin.configSchema?.uiHints,
};
}
async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse> {
const repoRoot = resolveRepoRoot();
const env = {
@@ -309,14 +471,26 @@ async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse>
env,
config: {},
});
const channelPlugins = await Promise.all(
manifestRegistry.plugins
.filter((plugin) => plugin.origin === "bundled" && plugin.channels.length > 0)
.map(async (plugin) => ({
id: plugin.id,
channel: await importChannelPluginModule(plugin.rootDir),
})),
logConfigDocBaselineDebug(`loaded ${manifestRegistry.plugins.length} bundled plugin manifests`);
const bundledChannelPlugins = manifestRegistry.plugins.filter(
(plugin) => plugin.origin === "bundled" && plugin.channels.length > 0,
);
const loadChannelsSequentiallyForDebug = process.env.OPENCLAW_CONFIG_DOC_BASELINE_DEBUG === "1";
const channelPlugins = loadChannelsSequentiallyForDebug
? await bundledChannelPlugins.reduce<Promise<ChannelSurfaceMetadata[]>>(
async (promise, plugin) => {
const loaded = await promise;
loaded.push(await loadChannelSurfaceMetadata(plugin.rootDir, repoRoot));
return loaded;
},
Promise.resolve([]),
)
: await Promise.all(
bundledChannelPlugins.map(
async (plugin) => await loadChannelSurfaceMetadata(plugin.rootDir, repoRoot),
),
);
logConfigDocBaselineDebug(`imported ${channelPlugins.length} bundled channel plugins`);
return buildConfigSchema({
plugins: manifestRegistry.plugins
@@ -329,11 +503,11 @@ async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse>
configSchema: plugin.configSchema,
})),
channels: channelPlugins.map((entry) => ({
id: entry.channel.id,
label: entry.channel.meta.label,
description: entry.channel.meta.blurb,
configSchema: entry.channel.configSchema?.schema,
configUiHints: entry.channel.configSchema?.uiHints,
id: entry.id,
label: entry.label,
description: entry.description,
configSchema: entry.configSchema,
configUiHints: entry.configUiHints,
})),
});
}
@@ -344,8 +518,20 @@ export function collectConfigDocBaselineEntries(
pathPrefix = "",
required = false,
entries: ConfigDocBaselineEntry[] = [],
visited = new WeakMap<JsonSchemaObject, Set<string>>(),
): ConfigDocBaselineEntry[] {
const normalizedPath = normalizeBaselinePath(pathPrefix);
const visitKey = `${normalizedPath}|${required ? "1" : "0"}`;
const visitedPaths = visited.get(schema);
if (visitedPaths?.has(visitKey)) {
return entries;
}
if (visitedPaths) {
visitedPaths.add(visitKey);
} else {
visited.set(schema, new Set([visitKey]));
}
if (normalizedPath) {
const hint = resolveUiHintMatch(uiHints, normalizedPath);
entries.push({
@@ -373,14 +559,21 @@ export function collectConfigDocBaselineEntries(
continue;
}
const childPath = normalizedPath ? `${normalizedPath}.${key}` : key;
collectConfigDocBaselineEntries(child, uiHints, childPath, requiredKeys.has(key), entries);
collectConfigDocBaselineEntries(
child,
uiHints,
childPath,
requiredKeys.has(key),
entries,
visited,
);
}
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
const wildcard = asSchemaObject(schema.additionalProperties);
if (wildcard) {
const wildcardPath = normalizedPath ? `${normalizedPath}.*` : "*";
collectConfigDocBaselineEntries(wildcard, uiHints, wildcardPath, false, entries);
collectConfigDocBaselineEntries(wildcard, uiHints, wildcardPath, false, entries, visited);
}
}
@@ -391,13 +584,13 @@ export function collectConfigDocBaselineEntries(
continue;
}
const itemPath = normalizedPath ? `${normalizedPath}.*` : "*";
collectConfigDocBaselineEntries(child, uiHints, itemPath, false, entries);
collectConfigDocBaselineEntries(child, uiHints, itemPath, false, entries, visited);
}
} else if (schema.items && typeof schema.items === "object") {
const itemSchema = asSchemaObject(schema.items);
if (itemSchema) {
const itemPath = normalizedPath ? `${normalizedPath}.*` : "*";
collectConfigDocBaselineEntries(itemSchema, uiHints, itemPath, false, entries);
collectConfigDocBaselineEntries(itemSchema, uiHints, itemPath, false, entries, visited);
}
}
@@ -407,7 +600,7 @@ export function collectConfigDocBaselineEntries(
if (!child) {
continue;
}
collectConfigDocBaselineEntries(child, uiHints, normalizedPath, required, entries);
collectConfigDocBaselineEntries(child, uiHints, normalizedPath, required, entries, visited);
}
}
@@ -426,14 +619,22 @@ export function dedupeConfigDocBaselineEntries(
}
export async function buildConfigDocBaseline(): Promise<ConfigDocBaseline> {
const start = Date.now();
logConfigDocBaselineDebug("build baseline start");
const response = await loadBundledConfigSchemaResponse();
const schemaRoot = asSchemaObject(response.schema);
if (!schemaRoot) {
throw new Error("config schema root is not an object");
}
const collectStart = Date.now();
logConfigDocBaselineDebug("collect baseline entries start");
const entries = dedupeConfigDocBaselineEntries(
collectConfigDocBaselineEntries(schemaRoot, response.uiHints),
);
logConfigDocBaselineDebug(
`collect baseline entries done count=${entries.length} elapsedMs=${Date.now() - collectStart}`,
);
logConfigDocBaselineDebug(`build baseline done elapsedMs=${Date.now() - start}`);
return {
generatedBy: GENERATED_BY,
entries,
@@ -443,6 +644,8 @@ export async function buildConfigDocBaseline(): Promise<ConfigDocBaseline> {
export async function renderConfigDocBaselineStatefile(
baseline?: ConfigDocBaseline,
): Promise<ConfigDocBaselineStatefileRender> {
const start = Date.now();
logConfigDocBaselineDebug("render statefile start");
const resolvedBaseline = baseline ?? (await buildConfigDocBaseline());
const json = `${JSON.stringify(resolvedBaseline, null, 2)}\n`;
const metadataLine = JSON.stringify({
@@ -456,6 +659,7 @@ export async function renderConfigDocBaselineStatefile(
...entry,
}),
);
logConfigDocBaselineDebug(`render statefile done elapsedMs=${Date.now() - start}`);
return {
json,
jsonl: `${[metadataLine, ...entryLines].join("\n")}\n`,
@@ -465,7 +669,7 @@ export async function renderConfigDocBaselineStatefile(
async function readIfExists(filePath: string): Promise<string | null> {
try {
return await fs.readFile(filePath, "utf8");
return fsSync.readFileSync(filePath, "utf8");
} catch {
return null;
}
@@ -476,8 +680,8 @@ async function writeIfChanged(filePath: string, next: string): Promise<boolean>
if (current === next) {
return false;
}
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, next, "utf8");
fsSync.mkdirSync(path.dirname(filePath), { recursive: true });
fsSync.writeFileSync(filePath, next, "utf8");
return true;
}
@@ -487,13 +691,23 @@ export async function writeConfigDocBaselineStatefile(params?: {
jsonPath?: string;
statefilePath?: string;
}): Promise<ConfigDocBaselineStatefileWriteResult> {
const start = Date.now();
logConfigDocBaselineDebug("write statefile start");
const repoRoot = params?.repoRoot ?? resolveRepoRoot();
const jsonPath = path.resolve(repoRoot, params?.jsonPath ?? DEFAULT_JSON_OUTPUT);
const statefilePath = path.resolve(repoRoot, params?.statefilePath ?? DEFAULT_STATEFILE_OUTPUT);
const rendered = await renderConfigDocBaselineStatefile();
logConfigDocBaselineDebug(`render statefile done elapsedMs=${Date.now() - start}`);
logConfigDocBaselineDebug(`read current json start ${jsonPath}`);
const currentJson = await readIfExists(jsonPath);
logConfigDocBaselineDebug(`read current json done elapsedMs=${Date.now() - start}`);
logConfigDocBaselineDebug(`read current statefile start ${statefilePath}`);
const currentStatefile = await readIfExists(statefilePath);
logConfigDocBaselineDebug(`read current statefile done elapsedMs=${Date.now() - start}`);
const changed = currentJson !== rendered.json || currentStatefile !== rendered.jsonl;
logConfigDocBaselineDebug(
`compare statefile done changed=${changed} elapsedMs=${Date.now() - start}`,
);
if (params?.check) {
return {

View File

@@ -10,3 +10,4 @@ export {
GroupPolicySchema,
MarkdownConfigSchema,
} from "../config/zod-schema.core.js";
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";

View File

@@ -12,3 +12,10 @@ export {
resolveIMessageConfigDefaultTo,
} from "./channel-config-helpers.js";
export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
export {
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
} from "../../extensions/imessage/src/target-parsing-helpers.js";
export type { ParsedChatTarget } from "../../extensions/imessage/src/target-parsing-helpers.js";

View File

@@ -0,0 +1,5 @@
export {
hasConfiguredSecretInput,
normalizeResolvedSecretInputString,
normalizeSecretInputString,
} from "../config/types.secrets.js";