openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
350 lines (349 loc) • 14.6 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { v as parseStrictInteger } from "./number-coercion-CJQ8TR--.js";
import { t as formatDocsLink } from "./links-CsLBrRff.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { n as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js";
import "./number-runtime-DBLVDypr.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { a as listCombinedAccountIds, c as resolveMergedAccountConfig } from "./account-helpers-pcH3lGFY.js";
import { r as resolveDangerousNameMatchingEnabled } from "./dangerous-name-matching-D4QRC91G.js";
import { t as createSetupTranslator } from "./i18n-7g_f4oaD.js";
import { J as setSetupChannelEnabled, Q as splitSetupEntries, a as createAllowFromSection, f as createStandardChannelSetupStatus, v as mergeAllowFromEntries } from "./setup-wizard-helpers-Dv-t1nb8.js";
import "./setup-DHfdBmUU.js";
import "./account-resolution-DBB0e3KP.js";
import "./dangerous-name-runtime-zP2lSdMQ.js";
//#region extensions/synology-chat/src/accounts.ts
/**
* Account resolution: reads config from channels.synology-chat,
* merges per-account overrides, falls back to environment variables.
*/
/** Extract the channel config from the full OpenClaw config object. */
function getChannelConfig$1(cfg) {
return cfg?.channels?.["synology-chat"];
}
function resolveImplicitAccountId(channelCfg) {
return channelCfg.token || process.env.SYNOLOGY_CHAT_TOKEN ? DEFAULT_ACCOUNT_ID : void 0;
}
function getRawAccountConfig$1(channelCfg, accountId) {
if (accountId === "default") return channelCfg;
return channelCfg.accounts?.[accountId] ?? {};
}
function hasExplicitWebhookPath(rawAccount) {
return typeof rawAccount?.webhookPath === "string" && rawAccount.webhookPath.trim().length > 0;
}
function resolveWebhookPathSource(params) {
if (hasExplicitWebhookPath(params.rawAccount)) return "explicit";
if (params.accountId !== "default" && hasExplicitWebhookPath(params.channelCfg)) return "inherited-base";
return "default";
}
/** Parse allowedUserIds from string or array to string[]. */
function parseAllowedUserIds(raw) {
if (!raw) return [];
if (Array.isArray(raw)) return raw.filter(Boolean);
return normalizeStringEntries(raw.split(","));
}
function normalizeRateLimitPerMinuteValue(raw) {
if (typeof raw === "number") return Number.isSafeInteger(raw) && raw >= 0 ? raw : void 0;
if (typeof raw !== "string") return;
const trimmed = raw.trim();
if (!/^\d+$/.test(trimmed)) return;
const parsed = parseStrictInteger(trimmed);
return parsed != null && parsed >= 0 ? parsed : void 0;
}
function parseRateLimitPerMinute(raw) {
return normalizeRateLimitPerMinuteValue(raw) ?? 30;
}
/**
* List all configured account IDs for this channel.
* Returns ["default"] if there's a base config, plus any named accounts.
*/
function listAccountIds(cfg) {
const channelCfg = getChannelConfig$1(cfg);
if (!channelCfg) return [];
return listCombinedAccountIds({
configuredAccountIds: Object.keys(channelCfg.accounts ?? {}),
implicitAccountId: resolveImplicitAccountId(channelCfg)
});
}
/**
* Resolve a specific account by ID with full defaults applied.
* Falls back to env vars for the "default" account.
*/
function resolveAccount(cfg, accountId) {
const channelCfg = getChannelConfig$1(cfg) ?? {};
const id = accountId || "default";
const accountOverrides = id === "default" ? void 0 : channelCfg.accounts?.[id] ?? void 0;
const rawAccount = getRawAccountConfig$1(channelCfg, id);
const merged = resolveMergedAccountConfig({
channelConfig: channelCfg,
accounts: channelCfg.accounts,
accountId: id
});
const envToken = process.env.SYNOLOGY_CHAT_TOKEN ?? "";
const envIncomingUrl = process.env.SYNOLOGY_CHAT_INCOMING_URL ?? "";
const envNasHost = process.env.SYNOLOGY_NAS_HOST ?? "localhost";
const envAllowedUserIds = process.env.SYNOLOGY_ALLOWED_USER_IDS ?? "";
const envRateLimitValue = parseRateLimitPerMinute(process.env.SYNOLOGY_RATE_LIMIT);
const envBotName = process.env.OPENCLAW_BOT_NAME ?? "OpenClaw";
const webhookPathSource = resolveWebhookPathSource({
accountId: id,
channelCfg,
rawAccount
});
const dangerouslyAllowInheritedWebhookPath = rawAccount.dangerouslyAllowInheritedWebhookPath ?? channelCfg.dangerouslyAllowInheritedWebhookPath ?? false;
return {
accountId: id,
enabled: merged.enabled ?? true,
token: merged.token ?? envToken,
incomingUrl: merged.incomingUrl ?? envIncomingUrl,
nasHost: merged.nasHost ?? envNasHost,
webhookPath: merged.webhookPath ?? "/webhook/synology",
webhookPathSource,
dangerouslyAllowNameMatching: resolveDangerousNameMatchingEnabled({
providerConfig: channelCfg,
accountConfig: accountOverrides
}),
dangerouslyAllowInheritedWebhookPath,
dmPolicy: merged.dmPolicy ?? "allowlist",
allowedUserIds: parseAllowedUserIds(merged.allowedUserIds ?? envAllowedUserIds),
rateLimitPerMinute: normalizeRateLimitPerMinuteValue(merged.rateLimitPerMinute) ?? envRateLimitValue,
botName: merged.botName ?? envBotName,
allowInsecureSsl: merged.allowInsecureSsl ?? false
};
}
//#endregion
//#region extensions/synology-chat/src/setup-surface.ts
const t = createSetupTranslator();
const channel = "synology-chat";
const DEFAULT_WEBHOOK_PATH = "/webhook/synology";
const SYNOLOGY_SETUP_HELP_LINES = [
t("wizard.synologyChat.helpIncomingWebhook"),
t("wizard.synologyChat.helpOutgoingWebhook"),
t("wizard.synologyChat.helpPointWebhook", { path: DEFAULT_WEBHOOK_PATH }),
t("wizard.synologyChat.helpAllowedUsers"),
`Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`
];
const SYNOLOGY_ALLOW_FROM_HELP_LINES = [
t("wizard.synologyChat.allowlistIntro"),
t("wizard.synologyChat.examples"),
"- 123456",
"- synology-chat:123456",
t("wizard.synologyChat.multipleEntries"),
`Docs: ${formatDocsLink("/channels/synology-chat", "channels/synology-chat")}`
];
function getChannelConfig(cfg) {
return cfg.channels?.[channel] ?? {};
}
function getRawAccountConfig(cfg, accountId) {
const channelConfig = getChannelConfig(cfg);
if (accountId === "default") return channelConfig;
return channelConfig.accounts?.[accountId] ?? {};
}
function patchSynologyChatAccountConfig(params) {
const channelConfig = getChannelConfig(params.cfg);
if (params.accountId === "default") {
const nextChannelConfig = { ...channelConfig };
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 };
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, accountId) {
const account = resolveAccount(cfg, accountId);
return Boolean(account.token.trim() && account.incomingUrl.trim());
}
function validateWebhookUrl(value) {
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.";
}
}
function validateWebhookPath(value) {
const trimmed = value.trim();
if (!trimmed) return;
return trimmed.startsWith("/") ? void 0 : "Webhook path must start with /.";
}
function parseSynologyUserId(value) {
const cleaned = value.replace(/^synology(?:[-_]?chat)?:/i, "").trim();
return /^\d+$/.test(cleaned) ? cleaned : null;
}
function normalizeSynologyAllowedUserId(value) {
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return `${value}`.trim();
return "";
}
function resolveExistingAllowedUserIds(cfg, accountId) {
const raw = getRawAccountConfig(cfg, accountId).allowedUserIds;
if (Array.isArray(raw)) return raw.map(normalizeSynologyAllowedUserId).filter(Boolean);
return normalizeStringEntries(normalizeSynologyAllowedUserId(raw).split(","));
}
const synologyChatSetupAdapter = {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId) ?? "default",
validateInput: ({ accountId, input }) => {
if (input.useEnv && accountId !== "default") 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"] : void 0,
patch: {
...input.useEnv ? {} : { token: input.token?.trim() },
incomingUrl: input.url?.trim(),
...input.webhookPath?.trim() ? { webhookPath: input.webhookPath.trim() } : {}
}
})
};
const synologyChatSetupWizard = {
channel,
status: createStandardChannelSetupStatus({
channelLabel: "Synology Chat",
configuredLabel: t("wizard.channels.statusConfigured"),
unconfiguredLabel: t("wizard.channels.statusNeedsTokenIncomingWebhook"),
configuredHint: t("wizard.channels.statusConfigured"),
unconfiguredHint: t("wizard.channels.statusNeedsTokenIncomingWebhook"),
configuredScore: 1,
unconfiguredScore: 0,
includeStatusLine: true,
resolveConfigured: ({ cfg, accountId }) => accountId ? isSynologyChatConfigured(cfg, accountId) : listAccountIds(cfg).some((candidateAccountId) => isSynologyChatConfigured(cfg, candidateAccountId)),
resolveExtraStatusLines: ({ cfg }) => [`Accounts: ${listAccountIds(cfg).length || 0}`]
}),
introNote: {
title: t("wizard.synologyChat.setupTitle"),
lines: SYNOLOGY_SETUP_HELP_LINES
},
credentials: [{
inputKey: "token",
providerHint: channel,
credentialLabel: "outgoing webhook token",
preferredEnvVar: "SYNOLOGY_CHAT_TOKEN",
helpTitle: t("wizard.synologyChat.webhookTokenTitle"),
helpLines: SYNOLOGY_SETUP_HELP_LINES,
envPrompt: t("wizard.synologyChat.tokenEnvPrompt"),
keepPrompt: t("wizard.synologyChat.tokenKeep"),
inputPrompt: t("wizard.synologyChat.tokenInput"),
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(normalizeOptionalString(raw.token)),
resolvedValue: normalizeOptionalString(account.token),
envValue: accountId === "default" ? normalizeOptionalString(process.env.SYNOLOGY_CHAT_TOKEN) : void 0
};
},
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: t("wizard.synologyChat.incomingWebhookUrlPrompt"),
placeholder: "https://nas.example.com/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming...",
helpTitle: t("wizard.synologyChat.incomingWebhookTitle"),
helpLines: [t("wizard.synologyChat.incomingWebhookHelpUseUrl"), t("wizard.synologyChat.incomingWebhookHelpReplies")],
currentValue: ({ cfg, accountId }) => getRawAccountConfig(cfg, accountId).incomingUrl?.trim(),
keepPrompt: (value) => t("wizard.synologyChat.incomingWebhookKeep", { value }),
validate: ({ value }) => validateWebhookUrl(value),
applySet: async ({ cfg, accountId, value }) => patchSynologyChatAccountConfig({
cfg,
accountId,
enabled: true,
patch: { incomingUrl: value.trim() }
})
}, {
inputKey: "webhookPath",
message: t("wizard.synologyChat.outgoingWebhookPathPrompt"),
placeholder: DEFAULT_WEBHOOK_PATH,
required: false,
applyEmptyValue: true,
helpTitle: t("wizard.synologyChat.outgoingWebhookPathTitle"),
helpLines: [t("wizard.synologyChat.defaultPath", { path: DEFAULT_WEBHOOK_PATH }), t("wizard.synologyChat.outgoingWebhookPathHelp")],
currentValue: ({ cfg, accountId }) => getRawAccountConfig(cfg, accountId).webhookPath?.trim(),
keepPrompt: (value) => t("wizard.synologyChat.outgoingWebhookPathKeep", { value }),
validate: ({ value }) => validateWebhookPath(value),
applySet: async ({ cfg, accountId, value }) => patchSynologyChatAccountConfig({
cfg,
accountId,
enabled: true,
clearFields: value.trim() ? void 0 : ["webhookPath"],
patch: value.trim() ? { webhookPath: value.trim() } : {}
})
}],
allowFrom: createAllowFromSection({
helpTitle: t("wizard.synologyChat.allowlistTitle"),
helpLines: SYNOLOGY_ALLOW_FROM_HELP_LINES,
message: t("wizard.synologyChat.allowedUserIdsPrompt"),
placeholder: "123456, 987654",
invalidWithoutCredentialNote: t("wizard.synologyChat.allowedUserIdsInvalid"),
parseInputs: splitSetupEntries,
parseId: parseSynologyUserId,
apply: async ({ cfg, accountId, allowFrom }) => patchSynologyChatAccountConfig({
cfg,
accountId,
enabled: true,
patch: {
dmPolicy: "allowlist",
allowedUserIds: mergeAllowFromEntries(resolveExistingAllowedUserIds(cfg, accountId), allowFrom)
}
})
}),
completionNote: {
title: t("wizard.synologyChat.accessControlTitle"),
lines: [
`Default outgoing webhook path: ${DEFAULT_WEBHOOK_PATH}`,
"Set allowed user IDs, or manually switch `channels.synology-chat.dmPolicy` to `\"open\"` with `allowedUserIds: [\"*\"]` 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)
};
//#endregion
export { resolveAccount as i, synologyChatSetupWizard as n, listAccountIds as r, synologyChatSetupAdapter as t };