feat: add synology chat setup wizard
This commit is contained in:
parent
de503dbcbb
commit
371366e9eb
@ -1,6 +1,6 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/synology-chat";
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/synology-chat";
|
||||
import { createSynologyChatPlugin } from "./src/channel.js";
|
||||
import { synologyChatPlugin } from "./src/channel.js";
|
||||
import { setSynologyRuntime } from "./src/runtime.js";
|
||||
|
||||
const plugin = {
|
||||
@ -10,7 +10,7 @@ const plugin = {
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: OpenClawPluginApi) {
|
||||
setSynologyRuntime(api.runtime);
|
||||
api.registerChannel({ plugin: createSynologyChatPlugin() });
|
||||
api.registerChannel({ plugin: synologyChatPlugin });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"setupEntry": "./setup-entry.ts",
|
||||
"channel": {
|
||||
"id": "synology-chat",
|
||||
"label": "Synology Chat",
|
||||
|
||||
5
extensions/synology-chat/setup-entry.ts
Normal file
5
extensions/synology-chat/setup-entry.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { synologyChatPlugin } from "./src/channel.js";
|
||||
|
||||
export default {
|
||||
plugin: synologyChatPlugin,
|
||||
};
|
||||
@ -22,6 +22,8 @@ describe("createSynologyChatPlugin", () => {
|
||||
expect(plugin.meta).toBeDefined();
|
||||
expect(plugin.capabilities).toBeDefined();
|
||||
expect(plugin.config).toBeDefined();
|
||||
expect(plugin.setup).toBeDefined();
|
||||
expect(plugin.setupWizard).toBeDefined();
|
||||
expect(plugin.security).toBeDefined();
|
||||
expect(plugin.outbound).toBeDefined();
|
||||
expect(plugin.gateway).toBeDefined();
|
||||
|
||||
@ -14,6 +14,7 @@ import { z } from "zod";
|
||||
import { listAccountIds, resolveAccount } from "./accounts.js";
|
||||
import { sendMessage, sendFileUrl } from "./client.js";
|
||||
import { getSynologyRuntime } from "./runtime.js";
|
||||
import { synologyChatSetupAdapter, synologyChatSetupWizard } from "./setup-surface.js";
|
||||
import type { ResolvedSynologyChatAccount } from "./types.js";
|
||||
import { createWebhookHandler } from "./webhook-handler.js";
|
||||
|
||||
@ -68,6 +69,8 @@ export function createSynologyChatPlugin() {
|
||||
reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
|
||||
|
||||
configSchema: SynologyChatConfigSchema,
|
||||
setup: synologyChatSetupAdapter,
|
||||
setupWizard: synologyChatSetupWizard,
|
||||
|
||||
config: {
|
||||
listAccountIds: (cfg: any) => listAccountIds(cfg),
|
||||
@ -377,3 +380,5 @@ export function createSynologyChatPlugin() {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const synologyChatPlugin = createSynologyChatPlugin();
|
||||
|
||||
101
extensions/synology-chat/src/setup-surface.test.ts
Normal file
101
extensions/synology-chat/src/setup-surface.test.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildChannelSetupFlowAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
|
||||
import { synologyChatPlugin } from "./channel.js";
|
||||
import { synologyChatSetupWizard } from "./setup-surface.js";
|
||||
|
||||
function createPrompter(overrides: Partial<WizardPrompter> = {}): WizardPrompter {
|
||||
return {
|
||||
intro: vi.fn(async () => {}),
|
||||
outro: vi.fn(async () => {}),
|
||||
note: vi.fn(async () => {}),
|
||||
select: vi.fn(async ({ options }: { options: Array<{ value: string }> }) => {
|
||||
const first = options[0];
|
||||
if (!first) {
|
||||
throw new Error("no options");
|
||||
}
|
||||
return first.value;
|
||||
}) as WizardPrompter["select"],
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => "") as WizardPrompter["text"],
|
||||
confirm: vi.fn(async () => false),
|
||||
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const synologyChatConfigureAdapter = buildChannelSetupFlowAdapterFromSetupWizard({
|
||||
plugin: synologyChatPlugin,
|
||||
wizard: synologyChatSetupWizard,
|
||||
});
|
||||
|
||||
describe("synology-chat setup wizard", () => {
|
||||
it("configures token and incoming webhook for the default account", async () => {
|
||||
const prompter = createPrompter({
|
||||
text: vi.fn(async ({ message }: { message: string }) => {
|
||||
if (message === "Enter Synology Chat outgoing webhook token") {
|
||||
return "synology-token";
|
||||
}
|
||||
if (message === "Incoming webhook URL") {
|
||||
return "https://nas.example.com/webapi/entry.cgi?token=incoming";
|
||||
}
|
||||
if (message === "Outgoing webhook path (optional)") {
|
||||
return "";
|
||||
}
|
||||
throw new Error(`Unexpected prompt: ${message}`);
|
||||
}) as WizardPrompter["text"],
|
||||
});
|
||||
|
||||
const result = await synologyChatConfigureAdapter.configure({
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: createRuntimeEnv(),
|
||||
prompter,
|
||||
options: {},
|
||||
accountOverrides: {},
|
||||
shouldPromptAccountIds: false,
|
||||
forceAllowFrom: false,
|
||||
});
|
||||
|
||||
expect(result.accountId).toBe("default");
|
||||
expect(result.cfg.channels?.["synology-chat"]?.enabled).toBe(true);
|
||||
expect(result.cfg.channels?.["synology-chat"]?.token).toBe("synology-token");
|
||||
expect(result.cfg.channels?.["synology-chat"]?.incomingUrl).toBe(
|
||||
"https://nas.example.com/webapi/entry.cgi?token=incoming",
|
||||
);
|
||||
});
|
||||
|
||||
it("records allowed user ids when setup forces allowFrom", async () => {
|
||||
const prompter = createPrompter({
|
||||
text: vi.fn(async ({ message }: { message: string }) => {
|
||||
if (message === "Enter Synology Chat outgoing webhook token") {
|
||||
return "synology-token";
|
||||
}
|
||||
if (message === "Incoming webhook URL") {
|
||||
return "https://nas.example.com/webapi/entry.cgi?token=incoming";
|
||||
}
|
||||
if (message === "Outgoing webhook path (optional)") {
|
||||
return "";
|
||||
}
|
||||
if (message === "Allowed Synology Chat user ids") {
|
||||
return "123456, synology-chat:789012";
|
||||
}
|
||||
throw new Error(`Unexpected prompt: ${message}`);
|
||||
}) as WizardPrompter["text"],
|
||||
});
|
||||
|
||||
const result = await synologyChatConfigureAdapter.configure({
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: createRuntimeEnv(),
|
||||
prompter,
|
||||
options: {},
|
||||
accountOverrides: {},
|
||||
shouldPromptAccountIds: false,
|
||||
forceAllowFrom: true,
|
||||
});
|
||||
|
||||
expect(result.cfg.channels?.["synology-chat"]?.dmPolicy).toBe("allowlist");
|
||||
expect(result.cfg.channels?.["synology-chat"]?.allowedUserIds).toEqual(["123456", "789012"]);
|
||||
});
|
||||
});
|
||||
324
extensions/synology-chat/src/setup-surface.ts
Normal file
324
extensions/synology-chat/src/setup-surface.ts
Normal file
@ -0,0 +1,324 @@
|
||||
import {
|
||||
mergeAllowFromEntries,
|
||||
setSetupChannelEnabled,
|
||||
splitSetupEntries,
|
||||
} from "../../../src/channels/plugins/setup-flow-helpers.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import { listAccountIds, resolveAccount } from "./accounts.js";
|
||||
import type { SynologyChatAccountRaw, SynologyChatChannelConfig } from "./types.js";
|
||||
|
||||
const channel = "synology-chat" as const;
|
||||
const DEFAULT_WEBHOOK_PATH = "/webhook/synology";
|
||||
|
||||
const SYNOLOGY_SETUP_HELP_LINES = [
|
||||
"1) Create an incoming webhook in Synology Chat and copy its URL",
|
||||
"2) Create an outgoing webhook and copy its secret token",
|
||||
`3) Point the outgoing webhook to https://<gateway-host>${DEFAULT_WEBHOOK_PATH}`,
|
||||
"4) Keep allowed user IDs handy for DM allowlisting",
|
||||
`Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`,
|
||||
];
|
||||
|
||||
const SYNOLOGY_ALLOW_FROM_HELP_LINES = [
|
||||
"Allowlist Synology Chat DMs by numeric user id.",
|
||||
"Examples:",
|
||||
"- 123456",
|
||||
"- synology-chat:123456",
|
||||
"Multiple entries: comma-separated.",
|
||||
`Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`,
|
||||
];
|
||||
|
||||
function getChannelConfig(cfg: OpenClawConfig): SynologyChatChannelConfig {
|
||||
return (cfg.channels?.[channel] as SynologyChatChannelConfig | undefined) ?? {};
|
||||
}
|
||||
|
||||
function getRawAccountConfig(cfg: OpenClawConfig, accountId: string): SynologyChatAccountRaw {
|
||||
const channelConfig = getChannelConfig(cfg);
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return channelConfig;
|
||||
}
|
||||
return channelConfig.accounts?.[accountId] ?? {};
|
||||
}
|
||||
|
||||
function patchSynologyChatAccountConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
patch: Record<string, unknown>;
|
||||
clearFields?: string[];
|
||||
enabled?: boolean;
|
||||
}): OpenClawConfig {
|
||||
const channelConfig = getChannelConfig(params.cfg);
|
||||
if (params.accountId === DEFAULT_ACCOUNT_ID) {
|
||||
const nextChannelConfig = { ...channelConfig } as Record<string, unknown>;
|
||||
for (const field of params.clearFields ?? []) {
|
||||
delete nextChannelConfig[field];
|
||||
}
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[channel]: {
|
||||
...nextChannelConfig,
|
||||
...(params.enabled ? { enabled: true } : {}),
|
||||
...params.patch,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const nextAccounts = { ...(channelConfig.accounts ?? {}) } as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
const nextAccountConfig = { ...(nextAccounts[params.accountId] ?? {}) };
|
||||
for (const field of params.clearFields ?? []) {
|
||||
delete nextAccountConfig[field];
|
||||
}
|
||||
nextAccounts[params.accountId] = {
|
||||
...nextAccountConfig,
|
||||
...(params.enabled ? { enabled: true } : {}),
|
||||
...params.patch,
|
||||
};
|
||||
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[channel]: {
|
||||
...channelConfig,
|
||||
...(params.enabled ? { enabled: true } : {}),
|
||||
accounts: nextAccounts,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isSynologyChatConfigured(cfg: OpenClawConfig, accountId: string): boolean {
|
||||
const account = resolveAccount(cfg, accountId);
|
||||
return Boolean(account.token.trim() && account.incomingUrl.trim());
|
||||
}
|
||||
|
||||
function validateWebhookUrl(value: string): string | undefined {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return "Incoming webhook must use http:// or https://.";
|
||||
}
|
||||
} catch {
|
||||
return "Incoming webhook must be a valid URL.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function validateWebhookPath(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed.startsWith("/") ? undefined : "Webhook path must start with /.";
|
||||
}
|
||||
|
||||
function parseSynologyUserId(value: string): string | null {
|
||||
const cleaned = value.replace(/^synology-chat:/i, "").trim();
|
||||
return /^\d+$/.test(cleaned) ? cleaned : null;
|
||||
}
|
||||
|
||||
function resolveExistingAllowedUserIds(cfg: OpenClawConfig, accountId: string): string[] {
|
||||
const raw = getRawAccountConfig(cfg, accountId).allowedUserIds;
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.map((value) => String(value).trim()).filter(Boolean);
|
||||
}
|
||||
return String(raw ?? "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export const synologyChatSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID,
|
||||
validateInput: ({ accountId, input }) => {
|
||||
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return "Synology Chat env credentials only support the default account.";
|
||||
}
|
||||
if (!input.useEnv && !input.token?.trim()) {
|
||||
return "Synology Chat requires --token or --use-env.";
|
||||
}
|
||||
if (!input.url?.trim()) {
|
||||
return "Synology Chat requires --url for the incoming webhook.";
|
||||
}
|
||||
const urlError = validateWebhookUrl(input.url.trim());
|
||||
if (urlError) {
|
||||
return urlError;
|
||||
}
|
||||
if (input.webhookPath?.trim()) {
|
||||
return validateWebhookPath(input.webhookPath.trim()) ?? null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) =>
|
||||
patchSynologyChatAccountConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
enabled: true,
|
||||
clearFields: input.useEnv ? ["token"] : undefined,
|
||||
patch: {
|
||||
...(input.useEnv ? {} : { token: input.token?.trim() }),
|
||||
incomingUrl: input.url?.trim(),
|
||||
...(input.webhookPath?.trim() ? { webhookPath: input.webhookPath.trim() } : {}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const synologyChatSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs token + incoming webhook",
|
||||
configuredHint: "configured",
|
||||
unconfiguredHint: "needs token + incoming webhook",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listAccountIds(cfg).some((accountId) => isSynologyChatConfigured(cfg, accountId)),
|
||||
resolveStatusLines: ({ cfg, configured }) => [
|
||||
`Synology Chat: ${configured ? "configured" : "needs token + incoming webhook"}`,
|
||||
`Accounts: ${listAccountIds(cfg).length || 0}`,
|
||||
],
|
||||
},
|
||||
introNote: {
|
||||
title: "Synology Chat webhook setup",
|
||||
lines: SYNOLOGY_SETUP_HELP_LINES,
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
inputKey: "token",
|
||||
providerHint: channel,
|
||||
credentialLabel: "outgoing webhook token",
|
||||
preferredEnvVar: "SYNOLOGY_CHAT_TOKEN",
|
||||
helpTitle: "Synology Chat webhook token",
|
||||
helpLines: SYNOLOGY_SETUP_HELP_LINES,
|
||||
envPrompt: "SYNOLOGY_CHAT_TOKEN detected. Use env var?",
|
||||
keepPrompt: "Synology Chat webhook token already configured. Keep it?",
|
||||
inputPrompt: "Enter Synology Chat outgoing webhook token",
|
||||
allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
|
||||
inspect: ({ cfg, accountId }) => {
|
||||
const account = resolveAccount(cfg, accountId);
|
||||
const raw = getRawAccountConfig(cfg, accountId);
|
||||
return {
|
||||
accountConfigured: isSynologyChatConfigured(cfg, accountId),
|
||||
hasConfiguredValue: Boolean(raw.token?.trim()),
|
||||
resolvedValue: account.token.trim() || undefined,
|
||||
envValue:
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? process.env.SYNOLOGY_CHAT_TOKEN?.trim() || undefined
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
applyUseEnv: async ({ cfg, accountId }) =>
|
||||
patchSynologyChatAccountConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
enabled: true,
|
||||
clearFields: ["token"],
|
||||
patch: {},
|
||||
}),
|
||||
applySet: async ({ cfg, accountId, resolvedValue }) =>
|
||||
patchSynologyChatAccountConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
enabled: true,
|
||||
patch: { token: resolvedValue },
|
||||
}),
|
||||
},
|
||||
],
|
||||
textInputs: [
|
||||
{
|
||||
inputKey: "url",
|
||||
message: "Incoming webhook URL",
|
||||
placeholder:
|
||||
"https://nas.example.com/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming...",
|
||||
helpTitle: "Synology Chat incoming webhook",
|
||||
helpLines: [
|
||||
"Use the incoming webhook URL from Synology Chat integrations.",
|
||||
"This is the URL OpenClaw uses to send replies back to Chat.",
|
||||
],
|
||||
currentValue: ({ cfg, accountId }) => getRawAccountConfig(cfg, accountId).incomingUrl?.trim(),
|
||||
keepPrompt: (value) => `Incoming webhook URL set (${value}). Keep it?`,
|
||||
validate: ({ value }) => validateWebhookUrl(value),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
patchSynologyChatAccountConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
enabled: true,
|
||||
patch: { incomingUrl: value.trim() },
|
||||
}),
|
||||
},
|
||||
{
|
||||
inputKey: "webhookPath",
|
||||
message: "Outgoing webhook path (optional)",
|
||||
placeholder: DEFAULT_WEBHOOK_PATH,
|
||||
required: false,
|
||||
applyEmptyValue: true,
|
||||
helpTitle: "Synology Chat outgoing webhook path",
|
||||
helpLines: [
|
||||
`Default path: ${DEFAULT_WEBHOOK_PATH}`,
|
||||
"Change this only if you need multiple Synology Chat webhook routes.",
|
||||
],
|
||||
currentValue: ({ cfg, accountId }) => getRawAccountConfig(cfg, accountId).webhookPath?.trim(),
|
||||
keepPrompt: (value) => `Outgoing webhook path set (${value}). Keep it?`,
|
||||
validate: ({ value }) => validateWebhookPath(value),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
patchSynologyChatAccountConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
enabled: true,
|
||||
clearFields: value.trim() ? undefined : ["webhookPath"],
|
||||
patch: value.trim() ? { webhookPath: value.trim() } : {},
|
||||
}),
|
||||
},
|
||||
],
|
||||
allowFrom: {
|
||||
helpTitle: "Synology Chat allowlist",
|
||||
helpLines: SYNOLOGY_ALLOW_FROM_HELP_LINES,
|
||||
message: "Allowed Synology Chat user ids",
|
||||
placeholder: "123456, 987654",
|
||||
invalidWithoutCredentialNote: "Synology Chat user ids must be numeric.",
|
||||
parseInputs: splitSetupEntries,
|
||||
parseId: parseSynologyUserId,
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((entry) => {
|
||||
const id = parseSynologyUserId(entry);
|
||||
return {
|
||||
input: entry,
|
||||
resolved: Boolean(id),
|
||||
id,
|
||||
};
|
||||
}),
|
||||
apply: async ({ cfg, accountId, allowFrom }) =>
|
||||
patchSynologyChatAccountConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
enabled: true,
|
||||
patch: {
|
||||
dmPolicy: "allowlist",
|
||||
allowedUserIds: mergeAllowFromEntries(
|
||||
resolveExistingAllowedUserIds(cfg, accountId),
|
||||
allowFrom,
|
||||
),
|
||||
},
|
||||
}),
|
||||
},
|
||||
completionNote: {
|
||||
title: "Synology Chat access control",
|
||||
lines: [
|
||||
`Default outgoing webhook path: ${DEFAULT_WEBHOOK_PATH}`,
|
||||
'Set allowed user IDs, or manually switch `channels.synology-chat.dmPolicy` to `"open"` for public DMs.',
|
||||
'With `dmPolicy="allowlist"`, an empty allowedUserIds list blocks the route from starting.',
|
||||
`Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`,
|
||||
],
|
||||
},
|
||||
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
||||
};
|
||||
@ -111,6 +111,12 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(typeof zaloSdk.zaloSetupAdapter).toBe("object");
|
||||
});
|
||||
|
||||
it("exports Synology Chat helpers", async () => {
|
||||
const synologyChatSdk = await import("openclaw/plugin-sdk/synology-chat");
|
||||
expect(typeof synologyChatSdk.synologyChatSetupWizard).toBe("object");
|
||||
expect(typeof synologyChatSdk.synologyChatSetupAdapter).toBe("object");
|
||||
});
|
||||
|
||||
it("exports Zalouser helpers", async () => {
|
||||
const zalouserSdk = await import("openclaw/plugin-sdk/zalouser");
|
||||
expect(typeof zalouserSdk.zalouserSetupWizard).toBe("object");
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
export { setAccountEnabledInConfigSection } from "../channels/plugins/config-helpers.js";
|
||||
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
|
||||
export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js";
|
||||
export {
|
||||
isRequestBodyLimitError,
|
||||
readRequestBodyWithLimit,
|
||||
@ -10,8 +11,13 @@ export {
|
||||
} from "../infra/http-body.js";
|
||||
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
export { registerPluginHttpRoute } from "../plugins/http-registry.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
export type { OpenClawPluginApi } from "../plugins/types.js";
|
||||
export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
export type { FixedWindowRateLimiter } from "./webhook-memory-guards.js";
|
||||
export { createFixedWindowRateLimiter } from "./webhook-memory-guards.js";
|
||||
export {
|
||||
synologyChatSetupAdapter,
|
||||
synologyChatSetupWizard,
|
||||
} from "../../extensions/synology-chat/src/setup-surface.js";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user