mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 15:18:58 +00:00
fix(channels): keep bundled setup entries dependency-light
This commit is contained in:
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Codex harness: route native `request_user_input` prompts back to the originating chat, preserve queued follow-up answers, and honor newer app-server command approval amendment decisions.
|
||||
- Codex harness/context-engine: redact context-engine assembly failures before logging, so fallback warnings do not serialize raw error objects. (#70809) Thanks @jalehman.
|
||||
- WhatsApp/onboarding: keep first-run setup entry loading off the Baileys runtime dependency path, so packaged QuickStart installs can show WhatsApp setup before runtime deps are staged. Fixes #70932.
|
||||
- Block streaming: suppress final assembled text after partial block-delivery aborts when the already-sent text chunks exactly cover the final reply, preventing duplicate replies without dropping unrelated short messages. Fixes #70921.
|
||||
- Codex harness/Windows: resolve npm-installed `codex.cmd` shims through PATHEXT before starting the native app-server, so `codex/*` models work without a manual `.exe` shim. Fixes #70913.
|
||||
- Slack/groups: classify MPIM group DMs as group chat context and suppress verbose tool/plan progress on Slack non-DM surfaces, so internal "Working…" traces no longer leak into rooms. Fixes #70912.
|
||||
|
||||
@@ -3,8 +3,8 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
|
||||
export default defineBundledChannelSetupEntry({
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
exportName: "googlechatPlugin",
|
||||
specifier: "./setup-plugin-api.js",
|
||||
exportName: "googlechatSetupPlugin",
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./secret-contract-api.js",
|
||||
|
||||
3
extensions/googlechat/setup-plugin-api.ts
Normal file
3
extensions/googlechat/setup-plugin-api.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Keep bundled setup entry imports narrow so setup loads do not pull the
|
||||
// broader Google Chat runtime plugin surface.
|
||||
export { googlechatSetupPlugin } from "./src/channel.setup.js";
|
||||
92
extensions/googlechat/src/channel.setup.ts
Normal file
92
extensions/googlechat/src/channel.setup.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
||||
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
|
||||
import {
|
||||
adaptScopedAccountAccessor,
|
||||
createScopedChannelConfigAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
listGoogleChatAccountIds,
|
||||
resolveDefaultGoogleChatAccountId,
|
||||
resolveGoogleChatAccount,
|
||||
type ResolvedGoogleChatAccount,
|
||||
} from "./accounts.js";
|
||||
import { googlechatSetupAdapter } from "./setup-core.js";
|
||||
import { googlechatSetupWizard } from "./setup-surface.js";
|
||||
|
||||
const formatGoogleChatAllowFromEntry = (entry: string) =>
|
||||
normalizeLowercaseStringOrEmpty(
|
||||
entry
|
||||
.trim()
|
||||
.replace(/^(googlechat|google-chat|gchat):/i, "")
|
||||
.replace(/^user:/i, "")
|
||||
.replace(/^users\//i, ""),
|
||||
);
|
||||
|
||||
const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleChatAccount>({
|
||||
sectionKey: "googlechat",
|
||||
listAccountIds: listGoogleChatAccountIds,
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveGoogleChatAccount),
|
||||
defaultAccountId: resolveDefaultGoogleChatAccountId,
|
||||
clearBaseFields: [
|
||||
"serviceAccount",
|
||||
"serviceAccountFile",
|
||||
"audienceType",
|
||||
"audience",
|
||||
"webhookPath",
|
||||
"webhookUrl",
|
||||
"botUser",
|
||||
"name",
|
||||
],
|
||||
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
formatNormalizedAllowFromEntries({
|
||||
allowFrom,
|
||||
normalizeEntry: formatGoogleChatAllowFromEntry,
|
||||
}),
|
||||
resolveDefaultTo: (account) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export const googlechatSetupPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
|
||||
id: "googlechat",
|
||||
meta: {
|
||||
id: "googlechat",
|
||||
label: "Google Chat",
|
||||
selectionLabel: "Google Chat (Chat API)",
|
||||
docsPath: "/channels/googlechat",
|
||||
docsLabel: "googlechat",
|
||||
blurb: "Google Workspace Chat app with HTTP webhook.",
|
||||
aliases: ["gchat", "google-chat"],
|
||||
order: 55,
|
||||
detailLabel: "Google Chat",
|
||||
systemImage: "message.badge",
|
||||
markdownCapable: true,
|
||||
},
|
||||
setup: googlechatSetupAdapter,
|
||||
setupWizard: googlechatSetupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group", "thread"],
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
nativeCommands: false,
|
||||
blockStreaming: true,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.googlechat"] },
|
||||
config: {
|
||||
...googleChatConfigAdapter,
|
||||
isConfigured: (account) => account.credentialSource !== "none",
|
||||
describeAccount: (account) =>
|
||||
describeAccountSnapshot({
|
||||
account,
|
||||
configured: account.credentialSource !== "none",
|
||||
extra: {
|
||||
credentialSource: account.credentialSource,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -3,8 +3,8 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
|
||||
export default defineBundledChannelSetupEntry({
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./channel-plugin-api.js",
|
||||
exportName: "matrixPlugin",
|
||||
specifier: "./setup-plugin-api.js",
|
||||
exportName: "matrixSetupPlugin",
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./secret-contract-api.js",
|
||||
|
||||
3
extensions/matrix/setup-plugin-api.ts
Normal file
3
extensions/matrix/setup-plugin-api.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Keep bundled setup entry imports narrow so setup loads do not pull the
|
||||
// broader Matrix runtime plugin surface.
|
||||
export { matrixSetupPlugin } from "./src/channel.setup.js";
|
||||
49
extensions/matrix/src/channel.setup.ts
Normal file
49
extensions/matrix/src/channel.setup.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
||||
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-primitives";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { matrixConfigAdapter } from "./config-adapter.js";
|
||||
import { MatrixConfigSchema } from "./config-schema.js";
|
||||
import { resolveMatrixAccount, type ResolvedMatrixAccount } from "./matrix/accounts.js";
|
||||
import { createMatrixSetupWizardProxy, matrixSetupAdapter } from "./setup-core.js";
|
||||
|
||||
const matrixSetupWizard = createMatrixSetupWizardProxy(async () => ({
|
||||
matrixSetupWizard: (await import("./setup-surface.js")).matrixSetupWizard,
|
||||
}));
|
||||
|
||||
export const matrixSetupPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
id: "matrix",
|
||||
meta: {
|
||||
id: "matrix",
|
||||
label: "Matrix",
|
||||
selectionLabel: "Matrix (plugin)",
|
||||
docsPath: "/channels/matrix",
|
||||
docsLabel: "matrix",
|
||||
blurb: "open protocol; configure a homeserver + access token.",
|
||||
order: 70,
|
||||
quickstartAllowFrom: true,
|
||||
},
|
||||
setupWizard: matrixSetupWizard,
|
||||
setup: matrixSetupAdapter,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group", "thread"],
|
||||
polls: true,
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.matrix"] },
|
||||
configSchema: buildChannelConfigSchema(MatrixConfigSchema),
|
||||
config: {
|
||||
...matrixConfigAdapter,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) =>
|
||||
describeAccountSnapshot({
|
||||
account,
|
||||
configured: account.configured,
|
||||
extra: {
|
||||
baseUrl: account.homeserver,
|
||||
},
|
||||
}),
|
||||
hasConfiguredState: ({ cfg }) => resolveMatrixAccount({ cfg }).configured,
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
|
||||
export default defineBundledChannelSetupEntry({
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
exportName: "nostrPlugin",
|
||||
specifier: "./setup-plugin-api.js",
|
||||
exportName: "nostrSetupPlugin",
|
||||
},
|
||||
});
|
||||
|
||||
3
extensions/nostr/setup-plugin-api.ts
Normal file
3
extensions/nostr/setup-plugin-api.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Keep bundled setup entry imports narrow so setup loads do not pull the
|
||||
// broader Nostr runtime plugin surface.
|
||||
export { nostrSetupPlugin } from "./src/channel.setup.js";
|
||||
231
extensions/nostr/src/channel.setup.ts
Normal file
231
extensions/nostr/src/channel.setup.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { patchTopLevelChannelConfigSection } from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
createDelegatedSetupWizardProxy,
|
||||
createStandardChannelSetupStatus,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
type ChannelSetupAdapter,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { buildChannelConfigSchema, type ChannelPlugin } from "./channel-api.js";
|
||||
import { NostrConfigSchema } from "./config-schema.js";
|
||||
import { DEFAULT_RELAYS } from "./default-relays.js";
|
||||
|
||||
const channel = "nostr" as const;
|
||||
|
||||
type NostrAccountConfig = {
|
||||
enabled?: boolean;
|
||||
name?: string;
|
||||
defaultAccount?: string;
|
||||
privateKey?: unknown;
|
||||
relays?: string[];
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
allowFrom?: Array<string | number>;
|
||||
profile?: unknown;
|
||||
};
|
||||
|
||||
type ResolvedNostrSetupAccount = {
|
||||
accountId: string;
|
||||
name?: string;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
relays: string[];
|
||||
profile?: unknown;
|
||||
config: NostrAccountConfig;
|
||||
};
|
||||
|
||||
function getNostrConfig(cfg: OpenClawConfig): NostrAccountConfig | undefined {
|
||||
return (cfg.channels as Record<string, unknown> | undefined)?.nostr as
|
||||
| NostrAccountConfig
|
||||
| undefined;
|
||||
}
|
||||
|
||||
function listSetupNostrAccountIds(cfg: OpenClawConfig): string[] {
|
||||
const nostrCfg = getNostrConfig(cfg);
|
||||
const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : "";
|
||||
if (!privateKey) {
|
||||
return [];
|
||||
}
|
||||
return [resolveDefaultSetupNostrAccountId(cfg)];
|
||||
}
|
||||
|
||||
function resolveDefaultSetupNostrAccountId(cfg: OpenClawConfig): string {
|
||||
const configured = getNostrConfig(cfg)?.defaultAccount;
|
||||
return typeof configured === "string" && configured.trim()
|
||||
? configured.trim()
|
||||
: DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
function resolveSetupNostrAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): ResolvedNostrSetupAccount {
|
||||
const nostrCfg = getNostrConfig(params.cfg);
|
||||
const accountId = params.accountId?.trim() || resolveDefaultSetupNostrAccountId(params.cfg);
|
||||
const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : "";
|
||||
const configured = Boolean(privateKey);
|
||||
return {
|
||||
accountId,
|
||||
name: typeof nostrCfg?.name === "string" ? nostrCfg.name : undefined,
|
||||
enabled: nostrCfg?.enabled !== false,
|
||||
configured,
|
||||
privateKey,
|
||||
publicKey: "",
|
||||
relays: nostrCfg?.relays ?? DEFAULT_RELAYS,
|
||||
profile: nostrCfg?.profile,
|
||||
config: {
|
||||
enabled: nostrCfg?.enabled,
|
||||
name: nostrCfg?.name,
|
||||
privateKey: nostrCfg?.privateKey,
|
||||
relays: nostrCfg?.relays,
|
||||
dmPolicy: nostrCfg?.dmPolicy,
|
||||
allowFrom: nostrCfg?.allowFrom,
|
||||
profile: nostrCfg?.profile,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildNostrSetupPatch(accountId: string, patch: Record<string, unknown>) {
|
||||
return {
|
||||
...(accountId !== DEFAULT_ACCOUNT_ID ? { defaultAccount: accountId } : {}),
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
function parseRelayUrls(raw: string): { relays: string[]; error?: string } {
|
||||
const entries = raw
|
||||
.split(/[,\n]/)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
const relays: string[] = [];
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
const parsed = new URL(entry);
|
||||
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
||||
return { relays: [], error: `Relay must use ws:// or wss:// (${entry})` };
|
||||
}
|
||||
} catch {
|
||||
return { relays: [], error: `Invalid relay URL: ${entry}` };
|
||||
}
|
||||
relays.push(entry);
|
||||
}
|
||||
return { relays: [...new Set(relays)] };
|
||||
}
|
||||
|
||||
function looksLikeNostrPrivateKey(privateKey: string): boolean {
|
||||
return privateKey.startsWith("nsec1") || /^[0-9a-fA-F]{64}$/.test(privateKey);
|
||||
}
|
||||
|
||||
const nostrSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ cfg, accountId }) =>
|
||||
accountId?.trim() || resolveDefaultSetupNostrAccountId(cfg),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
patch: buildNostrSetupPatch(accountId, name?.trim() ? { name: name.trim() } : {}),
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
const typedInput = input as {
|
||||
useEnv?: boolean;
|
||||
privateKey?: string;
|
||||
relayUrls?: string;
|
||||
};
|
||||
if (!typedInput.useEnv) {
|
||||
const privateKey = typedInput.privateKey?.trim();
|
||||
if (!privateKey) {
|
||||
return "Nostr requires --private-key or --use-env.";
|
||||
}
|
||||
if (!looksLikeNostrPrivateKey(privateKey)) {
|
||||
return "Nostr private key must be valid nsec or 64-character hex.";
|
||||
}
|
||||
}
|
||||
if (typedInput.relayUrls?.trim()) {
|
||||
return parseRelayUrls(typedInput.relayUrls).error ?? null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const typedInput = input as {
|
||||
useEnv?: boolean;
|
||||
privateKey?: string;
|
||||
relayUrls?: string;
|
||||
};
|
||||
const relayResult = typedInput.relayUrls?.trim()
|
||||
? parseRelayUrls(typedInput.relayUrls)
|
||||
: { relays: [] };
|
||||
return patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
enabled: true,
|
||||
clearFields: typedInput.useEnv ? ["privateKey"] : undefined,
|
||||
patch: buildNostrSetupPatch(accountId, {
|
||||
...(typedInput.useEnv ? {} : { privateKey: typedInput.privateKey?.trim() }),
|
||||
...(relayResult.relays.length > 0 ? { relays: relayResult.relays } : {}),
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const nostrSetupWizard = createDelegatedSetupWizardProxy({
|
||||
channel,
|
||||
loadWizard: async () => (await import("./setup-surface.js")).nostrSetupWizard,
|
||||
status: {
|
||||
...createStandardChannelSetupStatus({
|
||||
channelLabel: "Nostr",
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs private key",
|
||||
configuredHint: "configured",
|
||||
unconfiguredHint: "needs private key",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
includeStatusLine: true,
|
||||
resolveConfigured: ({ cfg, accountId }) =>
|
||||
resolveSetupNostrAccount({ cfg, accountId }).configured,
|
||||
resolveExtraStatusLines: ({ cfg }) => {
|
||||
const account = resolveSetupNostrAccount({ cfg });
|
||||
return [`Relays: ${account.relays.length || DEFAULT_RELAYS.length}`];
|
||||
},
|
||||
}),
|
||||
},
|
||||
resolveShouldPromptAccountIds: () => false,
|
||||
delegatePrepare: true,
|
||||
delegateFinalize: true,
|
||||
});
|
||||
|
||||
export const nostrSetupPlugin: ChannelPlugin<ResolvedNostrSetupAccount> = {
|
||||
id: channel,
|
||||
meta: {
|
||||
id: channel,
|
||||
label: "Nostr",
|
||||
selectionLabel: "Nostr",
|
||||
docsPath: "/channels/nostr",
|
||||
docsLabel: "nostr",
|
||||
blurb: "Decentralized DMs via Nostr relays (NIP-04)",
|
||||
order: 100,
|
||||
},
|
||||
capabilities: {
|
||||
chatTypes: ["direct"],
|
||||
media: false,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.nostr"] },
|
||||
configSchema: buildChannelConfigSchema(NostrConfigSchema),
|
||||
setup: nostrSetupAdapter,
|
||||
setupWizard: nostrSetupWizard,
|
||||
config: {
|
||||
listAccountIds: listSetupNostrAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveSetupNostrAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultSetupNostrAccountId,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) =>
|
||||
describeAccountSnapshot({
|
||||
account,
|
||||
configured: account.configured,
|
||||
extra: {
|
||||
publicKey: account.publicKey,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
|
||||
export default defineBundledChannelSetupEntry({
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
specifier: "./setup-plugin-api.js",
|
||||
exportName: "qqbotSetupPlugin",
|
||||
},
|
||||
});
|
||||
|
||||
3
extensions/qqbot/setup-plugin-api.ts
Normal file
3
extensions/qqbot/setup-plugin-api.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Keep bundled setup entry imports narrow so setup loads do not pull the
|
||||
// broader QQ Bot runtime plugin surface.
|
||||
export { qqbotSetupPlugin } from "./src/channel.setup.js";
|
||||
@@ -1,12 +1,79 @@
|
||||
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
||||
import {
|
||||
adaptScopedAccountAccessor,
|
||||
createScopedChannelConfigAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { type ResolvedSlackAccount } from "./accounts.js";
|
||||
import {
|
||||
listSlackAccountIds,
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
} from "./accounts.js";
|
||||
import { type ChannelPlugin } from "./channel-api.js";
|
||||
import { slackSetupAdapter } from "./setup-core.js";
|
||||
import { slackSetupWizard } from "./setup-surface.js";
|
||||
import { createSlackPluginBase } from "./shared.js";
|
||||
import { SlackChannelConfigSchema } from "./config-schema.js";
|
||||
import { slackSetupAdapter, createSlackSetupWizardProxy } from "./setup-core.js";
|
||||
import {
|
||||
describeSlackSetupAccount,
|
||||
isSlackSetupAccountConfigured,
|
||||
SLACK_CHANNEL,
|
||||
} from "./setup-shared.js";
|
||||
|
||||
const slackSetupWizard = createSlackSetupWizardProxy(async () => ({
|
||||
slackSetupWizard: (await import("./setup-surface.js")).slackSetupWizard,
|
||||
}));
|
||||
|
||||
const slackSetupConfigAdapter = createScopedChannelConfigAdapter<ResolvedSlackAccount>({
|
||||
sectionKey: SLACK_CHANNEL,
|
||||
listAccountIds: listSlackAccountIds,
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
|
||||
defaultAccountId: resolveDefaultSlackAccountId,
|
||||
clearBaseFields: ["botToken", "appToken", "name"],
|
||||
resolveAllowFrom: (account) => account.dm?.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: (account) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export const slackSetupPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
...createSlackPluginBase({
|
||||
setupWizard: slackSetupWizard,
|
||||
setup: slackSetupAdapter,
|
||||
}),
|
||||
id: SLACK_CHANNEL,
|
||||
meta: {
|
||||
id: SLACK_CHANNEL,
|
||||
label: "Slack",
|
||||
selectionLabel: "Slack (Socket Mode)",
|
||||
detailLabel: "Slack Bot",
|
||||
docsPath: "/channels/slack",
|
||||
docsLabel: "slack",
|
||||
blurb: "supported (Socket Mode).",
|
||||
systemImage: "number",
|
||||
markdownCapable: true,
|
||||
preferSessionLookupForAnnounceTarget: true,
|
||||
},
|
||||
setupWizard: slackSetupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "channel", "thread"],
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
nativeCommands: true,
|
||||
},
|
||||
commands: {
|
||||
nativeCommandsAutoEnabled: false,
|
||||
nativeSkillsAutoEnabled: false,
|
||||
resolveNativeCommandName: ({ commandKey, defaultName }) =>
|
||||
commandKey === "status" ? "agentstatus" : defaultName,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.slack"] },
|
||||
configSchema: SlackChannelConfigSchema,
|
||||
config: {
|
||||
...slackSetupConfigAdapter,
|
||||
hasConfiguredState: ({ env }) =>
|
||||
["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some(
|
||||
(key) => typeof env?.[key] === "string" && env[key]?.trim().length > 0,
|
||||
),
|
||||
isConfigured: (account) => isSlackSetupAccountConfigured(account),
|
||||
describeAccount: (account) => describeSlackSetupAccount(account),
|
||||
},
|
||||
setup: slackSetupAdapter,
|
||||
};
|
||||
|
||||
@@ -24,10 +24,10 @@ import { inspectSlackAccount } from "./account-inspect.js";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import {
|
||||
buildSlackSetupLines,
|
||||
SLACK_CHANNEL as channel,
|
||||
isSlackSetupAccountConfigured,
|
||||
SLACK_CHANNEL as channel,
|
||||
setSlackChannelAllowlist,
|
||||
} from "./shared.js";
|
||||
} from "./setup-shared.js";
|
||||
|
||||
function enableSlackAccount(cfg: OpenClawConfig, accountId: string): OpenClawConfig {
|
||||
return patchChannelConfigForAccount({
|
||||
|
||||
131
extensions/slack/src/setup-shared.ts
Normal file
131
extensions/slack/src/setup-shared.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
||||
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
import { patchChannelConfigForAccount } from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import type { ResolvedSlackAccount } from "./accounts.js";
|
||||
import type { OpenClawConfig } from "./channel-api.js";
|
||||
|
||||
export const SLACK_CHANNEL = "slack" as const;
|
||||
|
||||
function buildSlackManifest(botName: string) {
|
||||
const safeName = botName.trim() || "OpenClaw";
|
||||
const manifest = {
|
||||
display_information: {
|
||||
name: safeName,
|
||||
description: `${safeName} connector for OpenClaw`,
|
||||
},
|
||||
features: {
|
||||
bot_user: {
|
||||
display_name: safeName,
|
||||
always_online: true,
|
||||
},
|
||||
app_home: {
|
||||
messages_tab_enabled: true,
|
||||
messages_tab_read_only_enabled: false,
|
||||
},
|
||||
slash_commands: [
|
||||
{
|
||||
command: "/openclaw",
|
||||
description: "Send a message to OpenClaw",
|
||||
should_escape: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
oauth_config: {
|
||||
scopes: {
|
||||
bot: [
|
||||
"app_mentions:read",
|
||||
"assistant:write",
|
||||
"channels:history",
|
||||
"channels:read",
|
||||
"chat:write",
|
||||
"commands",
|
||||
"emoji:read",
|
||||
"files:read",
|
||||
"files:write",
|
||||
"groups:history",
|
||||
"groups:read",
|
||||
"im:history",
|
||||
"im:read",
|
||||
"im:write",
|
||||
"mpim:history",
|
||||
"mpim:read",
|
||||
"mpim:write",
|
||||
"pins:read",
|
||||
"pins:write",
|
||||
"reactions:read",
|
||||
"reactions:write",
|
||||
"users:read",
|
||||
],
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
socket_mode_enabled: true,
|
||||
event_subscriptions: {
|
||||
bot_events: [
|
||||
"app_mention",
|
||||
"channel_rename",
|
||||
"member_joined_channel",
|
||||
"member_left_channel",
|
||||
"message.channels",
|
||||
"message.groups",
|
||||
"message.im",
|
||||
"message.mpim",
|
||||
"pin_added",
|
||||
"pin_removed",
|
||||
"reaction_added",
|
||||
"reaction_removed",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
return JSON.stringify(manifest, null, 2);
|
||||
}
|
||||
|
||||
export function buildSlackSetupLines(botName = "OpenClaw"): string[] {
|
||||
return [
|
||||
"1) Slack API -> Create App -> From scratch or From manifest (with the JSON below)",
|
||||
"2) Add Socket Mode + enable it to get the app-level token (xapp-...)",
|
||||
"3) Install App to workspace to get the xoxb- bot token",
|
||||
"4) Enable Event Subscriptions (socket) for message events",
|
||||
"5) App Home -> enable the Messages tab for DMs",
|
||||
"Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.",
|
||||
`Docs: ${formatDocsLink("/slack", "slack")}`,
|
||||
"",
|
||||
"Manifest (JSON):",
|
||||
buildSlackManifest(botName),
|
||||
];
|
||||
}
|
||||
|
||||
export function setSlackChannelAllowlist(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
channelKeys: string[],
|
||||
): OpenClawConfig {
|
||||
const channels = Object.fromEntries(channelKeys.map((key) => [key, { enabled: true }]));
|
||||
return patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel: SLACK_CHANNEL,
|
||||
accountId,
|
||||
patch: { channels },
|
||||
});
|
||||
}
|
||||
|
||||
export function isSlackSetupAccountConfigured(account: ResolvedSlackAccount): boolean {
|
||||
const hasConfiguredBotToken =
|
||||
Boolean(account.botToken?.trim()) || hasConfiguredSecretInput(account.config.botToken);
|
||||
const hasConfiguredAppToken =
|
||||
Boolean(account.appToken?.trim()) || hasConfiguredSecretInput(account.config.appToken);
|
||||
return hasConfiguredBotToken && hasConfiguredAppToken;
|
||||
}
|
||||
|
||||
export function describeSlackSetupAccount(account: ResolvedSlackAccount) {
|
||||
return describeAccountSnapshot({
|
||||
account,
|
||||
configured: isSlackSetupAccountConfigured(account),
|
||||
extra: {
|
||||
botTokenSource: account.botTokenSource,
|
||||
appTokenSource: account.appTokenSource,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveWebCredsPath } from "./creds-files.js";
|
||||
import { BufferJSON } from "./session.runtime.js";
|
||||
|
||||
const CREDS_FILE_MODE = 0o600;
|
||||
const CREDS_SAVE_FLUSH_TIMEOUT_MS = 15_000;
|
||||
@@ -11,6 +10,11 @@ const credsSaveQueues = new Map<string, Promise<void>>();
|
||||
|
||||
export type CredsQueueWaitResult = "drained" | "timed_out";
|
||||
|
||||
async function stringifyCreds(creds: unknown): Promise<string> {
|
||||
const { BufferJSON } = await import("./session.runtime.js");
|
||||
return JSON.stringify(creds, BufferJSON.replacer);
|
||||
}
|
||||
|
||||
async function syncDirectory(dirPath: string): Promise<void> {
|
||||
let handle: Awaited<ReturnType<typeof fs.open>> | undefined;
|
||||
try {
|
||||
@@ -28,7 +32,7 @@ async function syncDirectory(dirPath: string): Promise<void> {
|
||||
export async function writeCredsJsonAtomically(authDir: string, creds: unknown): Promise<void> {
|
||||
const credsPath = resolveWebCredsPath(authDir);
|
||||
const tempPath = path.join(authDir, `.creds.${process.pid}.${randomUUID()}.tmp`);
|
||||
const json = JSON.stringify(creds, BufferJSON.replacer);
|
||||
const json = await stringifyCreds(creds);
|
||||
|
||||
let handle: Awaited<ReturnType<typeof fs.open>> | undefined;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user