mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-21 16:41:56 +00:00
refactor: move channel capability diagnostics into plugins
This commit is contained in:
@@ -223,6 +223,8 @@ channel-specific UX and routing behavior:
|
||||
- `messaging.enableInteractiveReplies`: channel-owned reply normalization toggles
|
||||
(for example Slack interactive replies)
|
||||
- `messaging.resolveOutboundSessionRoute`: channel-owned outbound session routing
|
||||
- `status.formatCapabilitiesProbe` / `status.buildCapabilitiesDiagnostics`: channel-owned
|
||||
`/channels capabilities` probe display and extra audits/scopes
|
||||
- `threading.resolveAutoThreadId`: channel-owned same-conversation auto-threading
|
||||
- `threading.resolveReplyTransport`: channel-owned reply-vs-thread delivery mapping
|
||||
- `actions.requiresTrustedRequesterSender`: channel-owned privileged action trust gates
|
||||
|
||||
@@ -38,8 +38,11 @@ import {
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
|
||||
import { normalizeMessageChannel } from "../../../src/utils/message-channel.js";
|
||||
import { isDiscordExecApprovalClientEnabled } from "./exec-approvals.js";
|
||||
import type { DiscordProbe } from "./probe.js";
|
||||
import { getDiscordRuntime } from "./runtime.js";
|
||||
import { fetchChannelPermissionsDiscord } from "./send.js";
|
||||
import { createDiscordSetupWizardProxy, discordSetupAdapter } from "./setup-core.js";
|
||||
import { parseDiscordTarget } from "./targets.js";
|
||||
import { DiscordUiContainer } from "./ui.js";
|
||||
|
||||
type DiscordSendFn = ReturnType<
|
||||
@@ -47,11 +50,27 @@ type DiscordSendFn = ReturnType<
|
||||
>["channel"]["discord"]["sendMessageDiscord"];
|
||||
|
||||
const meta = getChatChannelMeta("discord");
|
||||
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||
|
||||
async function loadDiscordChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
function formatDiscordIntents(intents?: {
|
||||
messageContent?: string;
|
||||
guildMembers?: string;
|
||||
presence?: string;
|
||||
}) {
|
||||
if (!intents) {
|
||||
return "unknown";
|
||||
}
|
||||
return [
|
||||
`messageContent=${intents.messageContent ?? "unknown"}`,
|
||||
`guildMembers=${intents.guildMembers ?? "unknown"}`,
|
||||
`presence=${intents.presence ?? "unknown"}`,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
const discordMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: (ctx) =>
|
||||
getDiscordRuntime().channel.discord.messageActions?.listActions?.(ctx) ?? [],
|
||||
@@ -355,6 +374,91 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
getDiscordRuntime().channel.discord.probeDiscord(account.token, timeoutMs, {
|
||||
includeApplication: true,
|
||||
}),
|
||||
formatCapabilitiesProbe: ({ probe }) => {
|
||||
const discordProbe = probe as DiscordProbe | undefined;
|
||||
const lines = [];
|
||||
if (discordProbe?.bot?.username) {
|
||||
const botId = discordProbe.bot.id ? ` (${discordProbe.bot.id})` : "";
|
||||
lines.push({ text: `Bot: @${discordProbe.bot.username}${botId}` });
|
||||
}
|
||||
if (discordProbe?.application?.intents) {
|
||||
lines.push({ text: `Intents: ${formatDiscordIntents(discordProbe.application.intents)}` });
|
||||
}
|
||||
return lines;
|
||||
},
|
||||
buildCapabilitiesDiagnostics: async ({ account, timeoutMs, target }) => {
|
||||
if (!target?.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
const parsedTarget = parseDiscordTarget(target.trim(), { defaultKind: "channel" });
|
||||
const details: Record<string, unknown> = {
|
||||
target: {
|
||||
raw: target,
|
||||
normalized: parsedTarget?.normalized,
|
||||
kind: parsedTarget?.kind,
|
||||
channelId: parsedTarget?.kind === "channel" ? parsedTarget.id : undefined,
|
||||
},
|
||||
};
|
||||
if (!parsedTarget || parsedTarget.kind !== "channel") {
|
||||
return {
|
||||
details,
|
||||
lines: [
|
||||
{
|
||||
text: "Permissions: Target looks like a DM user; pass channel:<id> to audit channel permissions.",
|
||||
tone: "error",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const token = account.token?.trim();
|
||||
if (!token) {
|
||||
return {
|
||||
details,
|
||||
lines: [
|
||||
{
|
||||
text: "Permissions: Discord bot token missing for permission audit.",
|
||||
tone: "error",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
try {
|
||||
const perms = await fetchChannelPermissionsDiscord(parsedTarget.id, {
|
||||
token,
|
||||
accountId: account.accountId ?? undefined,
|
||||
});
|
||||
const missingRequired = REQUIRED_DISCORD_PERMISSIONS.filter(
|
||||
(permission) => !perms.permissions.includes(permission),
|
||||
);
|
||||
details.permissions = {
|
||||
channelId: perms.channelId,
|
||||
guildId: perms.guildId,
|
||||
isDm: perms.isDm,
|
||||
channelType: perms.channelType,
|
||||
permissions: perms.permissions,
|
||||
missingRequired,
|
||||
raw: perms.raw,
|
||||
};
|
||||
return {
|
||||
details,
|
||||
lines: [
|
||||
{
|
||||
text: `Permissions (${perms.channelId}): ${perms.permissions.length ? perms.permissions.join(", ") : "none"}`,
|
||||
},
|
||||
missingRequired.length > 0
|
||||
? { text: `Missing required: ${missingRequired.join(", ")}`, tone: "warn" }
|
||||
: { text: "Missing required: none", tone: "success" },
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
details.permissions = { channelId: parsedTarget.id, error: message };
|
||||
return {
|
||||
details,
|
||||
lines: [{ text: `Permissions: ${message}`, tone: "error" }],
|
||||
};
|
||||
}
|
||||
},
|
||||
auditAccount: async ({ account, timeoutMs, cfg }) => {
|
||||
const { channelIds, unresolvedChannels } = collectDiscordAuditChannelIds({
|
||||
cfg,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "openclaw/plugin-sdk/msteams";
|
||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
||||
import type { ProbeMSTeamsResult } from "./probe.js";
|
||||
import {
|
||||
normalizeMSTeamsMessagingTarget,
|
||||
normalizeMSTeamsUserInput,
|
||||
@@ -47,6 +48,16 @@ const meta = {
|
||||
order: 60,
|
||||
} as const;
|
||||
|
||||
const TEAMS_GRAPH_PERMISSION_HINTS: Record<string, string> = {
|
||||
"ChannelMessage.Read.All": "channel history",
|
||||
"Chat.Read.All": "chat history",
|
||||
"Channel.ReadBasic.All": "channel list",
|
||||
"Team.ReadBasic.All": "team list",
|
||||
"TeamsActivity.Read.All": "teams activity",
|
||||
"Sites.Read.All": "files (SharePoint)",
|
||||
"Files.Read.All": "files (OneDrive)",
|
||||
};
|
||||
|
||||
async function loadMSTeamsChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
@@ -435,6 +446,40 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
}),
|
||||
probeAccount: async ({ cfg }) =>
|
||||
await (await loadMSTeamsChannelRuntime()).probeMSTeams(cfg.channels?.msteams),
|
||||
formatCapabilitiesProbe: ({ probe }) => {
|
||||
const teamsProbe = probe as ProbeMSTeamsResult | undefined;
|
||||
const lines: Array<{ text: string; tone?: "error" }> = [];
|
||||
const appId = typeof teamsProbe?.appId === "string" ? teamsProbe.appId.trim() : "";
|
||||
if (appId) {
|
||||
lines.push({ text: `App: ${appId}` });
|
||||
}
|
||||
const graph = teamsProbe?.graph;
|
||||
if (graph) {
|
||||
const roles = Array.isArray(graph.roles)
|
||||
? graph.roles.map((role) => String(role).trim()).filter(Boolean)
|
||||
: [];
|
||||
const scopes = Array.isArray(graph.scopes)
|
||||
? graph.scopes.map((scope) => String(scope).trim()).filter(Boolean)
|
||||
: [];
|
||||
const formatPermission = (permission: string) => {
|
||||
const hint = TEAMS_GRAPH_PERMISSION_HINTS[permission];
|
||||
return hint ? `${permission} (${hint})` : permission;
|
||||
};
|
||||
if (graph.ok === false) {
|
||||
lines.push({ text: `Graph: ${graph.error ?? "failed"}`, tone: "error" });
|
||||
} else if (roles.length > 0 || scopes.length > 0) {
|
||||
if (roles.length > 0) {
|
||||
lines.push({ text: `Graph roles: ${roles.map(formatPermission).join(", ")}` });
|
||||
}
|
||||
if (scopes.length > 0) {
|
||||
lines.push({ text: `Graph scopes: ${scopes.map(formatPermission).join(", ")}` });
|
||||
}
|
||||
} else if (graph.ok === true) {
|
||||
lines.push({ text: "Graph: ok" });
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
},
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
||||
accountId: account.accountId,
|
||||
enabled: account.enabled,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type ResolvedSignalAccount,
|
||||
} from "openclaw/plugin-sdk/signal";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
|
||||
import type { SignalProbe } from "./probe.js";
|
||||
import { getSignalRuntime } from "./runtime.js";
|
||||
import { createSignalSetupWizardProxy, signalSetupAdapter } from "./setup-core.js";
|
||||
|
||||
@@ -220,6 +221,10 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
const baseUrl = account.baseUrl;
|
||||
return await getSignalRuntime().channel.signal.probeSignal(baseUrl, timeoutMs);
|
||||
},
|
||||
formatCapabilitiesProbe: ({ probe }) =>
|
||||
(probe as SignalProbe | undefined)?.version
|
||||
? [{ text: `Signal daemon: ${(probe as SignalProbe).version}` }]
|
||||
: [],
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
||||
...buildBaseAccountStatusSnapshot({ account, runtime, probe }),
|
||||
baseUrl: account.baseUrl,
|
||||
|
||||
@@ -36,7 +36,10 @@ import {
|
||||
} from "openclaw/plugin-sdk/slack";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
|
||||
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
|
||||
import { parseSlackBlocksInput } from "./blocks-input.js";
|
||||
import type { SlackProbe } from "./probe.js";
|
||||
import { getSlackRuntime } from "./runtime.js";
|
||||
import { fetchSlackScopes } from "./scopes.js";
|
||||
import { createSlackSetupWizardProxy, slackSetupAdapter } from "./setup-core.js";
|
||||
import { parseSlackTarget } from "./targets.js";
|
||||
|
||||
@@ -126,6 +129,21 @@ function resolveSlackAutoThreadId(params: {
|
||||
return context.currentThreadTs;
|
||||
}
|
||||
|
||||
function formatSlackScopeDiagnostic(params: {
|
||||
tokenType: "bot" | "user";
|
||||
result: Awaited<ReturnType<typeof fetchSlackScopes>>;
|
||||
}) {
|
||||
const source = params.result.source ? ` (${params.result.source})` : "";
|
||||
const label = params.tokenType === "user" ? "User scopes" : "Bot scopes";
|
||||
if (params.result.ok && params.result.scopes?.length) {
|
||||
return { text: `${label}${source}: ${params.result.scopes.join(", ")}` } as const;
|
||||
}
|
||||
return {
|
||||
text: `${label}: ${params.result.error ?? "scope lookup failed"}`,
|
||||
tone: "error",
|
||||
} as const;
|
||||
}
|
||||
|
||||
const slackConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveSlackAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedSlackAccount) => account.dm?.allowFrom,
|
||||
@@ -285,6 +303,17 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
normalizeTarget: normalizeSlackMessagingTarget,
|
||||
enableInteractiveReplies: ({ cfg, accountId }) =>
|
||||
isSlackInteractiveRepliesEnabled({ cfg, accountId }),
|
||||
hasStructuredReplyPayload: ({ payload }) => {
|
||||
const slackData = payload.channelData?.slack;
|
||||
if (!slackData || typeof slackData !== "object" || Array.isArray(slackData)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return Boolean(parseSlackBlocksInput((slackData as { blocks?: unknown }).blocks)?.length);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeSlackTargetId,
|
||||
hint: "<channelId|user:ID|channel:ID>",
|
||||
@@ -429,6 +458,35 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
}
|
||||
return await getSlackRuntime().channel.slack.probeSlack(token, timeoutMs);
|
||||
},
|
||||
formatCapabilitiesProbe: ({ probe }) => {
|
||||
const slackProbe = probe as SlackProbe | undefined;
|
||||
const lines = [];
|
||||
if (slackProbe?.bot?.name) {
|
||||
lines.push({ text: `Bot: @${slackProbe.bot.name}` });
|
||||
}
|
||||
if (slackProbe?.team?.name || slackProbe?.team?.id) {
|
||||
const id = slackProbe.team?.id ? ` (${slackProbe.team.id})` : "";
|
||||
lines.push({ text: `Team: ${slackProbe.team?.name ?? "unknown"}${id}` });
|
||||
}
|
||||
return lines;
|
||||
},
|
||||
buildCapabilitiesDiagnostics: async ({ account, timeoutMs }) => {
|
||||
const lines = [];
|
||||
const details: Record<string, unknown> = {};
|
||||
const botToken = account.botToken?.trim();
|
||||
const userToken = account.config.userToken?.trim();
|
||||
const botScopes = botToken
|
||||
? await fetchSlackScopes(botToken, timeoutMs)
|
||||
: { ok: false, error: "Slack bot token missing." };
|
||||
lines.push(formatSlackScopeDiagnostic({ tokenType: "bot", result: botScopes }));
|
||||
details.botScopes = botScopes;
|
||||
if (userToken) {
|
||||
const userScopes = await fetchSlackScopes(userToken, timeoutMs);
|
||||
lines.push(formatSlackScopeDiagnostic({ tokenType: "user", result: userScopes }));
|
||||
details.userScopes = userScopes;
|
||||
}
|
||||
return { lines, details };
|
||||
},
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
||||
const mode = account.config.mode ?? "socket";
|
||||
const configured =
|
||||
|
||||
@@ -54,7 +54,6 @@ import { sendTypingTelegram } from "./send.js";
|
||||
import { telegramSetupAdapter } from "./setup-core.js";
|
||||
import { telegramSetupWizard } from "./setup-surface.js";
|
||||
import { parseTelegramTarget } from "./targets.js";
|
||||
import { deleteTelegramUpdateOffset } from "./update-offset-store.js";
|
||||
|
||||
type TelegramSendFn = ReturnType<
|
||||
typeof getTelegramRuntime
|
||||
@@ -334,10 +333,12 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
const previousToken = resolveTelegramAccount({ cfg: prevCfg, accountId }).token.trim();
|
||||
const nextToken = resolveTelegramAccount({ cfg: nextCfg, accountId }).token.trim();
|
||||
if (previousToken !== nextToken) {
|
||||
const { deleteTelegramUpdateOffset } = await import("./update-offset-store.js");
|
||||
await deleteTelegramUpdateOffset({ accountId });
|
||||
}
|
||||
},
|
||||
onAccountRemoved: async ({ accountId }) => {
|
||||
const { deleteTelegramUpdateOffset } = await import("./update-offset-store.js");
|
||||
await deleteTelegramUpdateOffset({ accountId });
|
||||
},
|
||||
},
|
||||
@@ -515,6 +516,30 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
proxyUrl: account.config.proxy,
|
||||
network: account.config.network,
|
||||
}),
|
||||
formatCapabilitiesProbe: ({ probe }) => {
|
||||
const lines = [];
|
||||
if (probe?.bot?.username) {
|
||||
const botId = probe.bot.id ? ` (${probe.bot.id})` : "";
|
||||
lines.push({ text: `Bot: @${probe.bot.username}${botId}` });
|
||||
}
|
||||
const flags: string[] = [];
|
||||
if (typeof probe?.bot?.canJoinGroups === "boolean") {
|
||||
flags.push(`joinGroups=${probe.bot.canJoinGroups}`);
|
||||
}
|
||||
if (typeof probe?.bot?.canReadAllGroupMessages === "boolean") {
|
||||
flags.push(`readAllGroupMessages=${probe.bot.canReadAllGroupMessages}`);
|
||||
}
|
||||
if (typeof probe?.bot?.supportsInlineQueries === "boolean") {
|
||||
flags.push(`inlineQueries=${probe.bot.supportsInlineQueries}`);
|
||||
}
|
||||
if (flags.length > 0) {
|
||||
lines.push({ text: `Flags: ${flags.join(" ")}` });
|
||||
}
|
||||
if (probe?.webhook?.url !== undefined) {
|
||||
lines.push({ text: `Webhook: ${probe.webhook.url || "none"}` });
|
||||
}
|
||||
return lines;
|
||||
},
|
||||
auditAccount: async ({ account, timeoutMs, probe, cfg }) => {
|
||||
const groups =
|
||||
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mattermostPlugin } from "../../../extensions/mattermost/src/channel.js";
|
||||
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
|
||||
import { discordOutbound } from "../../channels/plugins/outbound/discord.js";
|
||||
import { imessageOutbound } from "../../channels/plugins/outbound/imessage.js";
|
||||
import { signalOutbound } from "../../channels/plugins/outbound/signal.js";
|
||||
@@ -540,7 +541,11 @@ const defaultRegistry = createTestRegistry([
|
||||
},
|
||||
{
|
||||
pluginId: "slack",
|
||||
plugin: createOutboundTestPlugin({ id: "slack", outbound: slackOutbound, label: "Slack" }),
|
||||
plugin: {
|
||||
...createOutboundTestPlugin({ id: "slack", outbound: slackOutbound, label: "Slack" }),
|
||||
messaging: slackPlugin.messaging,
|
||||
threading: slackPlugin.threading,
|
||||
},
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
* across multiple providers.
|
||||
*/
|
||||
|
||||
import { parseSlackBlocksInput } from "../../../extensions/slack/src/blocks-input.js";
|
||||
import { isSlackInteractiveRepliesEnabled } from "../../../extensions/slack/src/interactive-replies.js";
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { resolveEffectiveMessagesConfig } from "../../agents/identity.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
@@ -101,9 +99,10 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
|
||||
: cfg.messages?.responsePrefix;
|
||||
const normalized = normalizeReplyPayload(payload, {
|
||||
responsePrefix,
|
||||
enableSlackInteractiveReplies:
|
||||
plugin?.messaging?.enableInteractiveReplies?.({ cfg, accountId }) ??
|
||||
(channel === "slack" ? isSlackInteractiveRepliesEnabled({ cfg, accountId }) : false),
|
||||
enableSlackInteractiveReplies: plugin?.messaging?.enableInteractiveReplies?.({
|
||||
cfg,
|
||||
accountId,
|
||||
}),
|
||||
});
|
||||
if (!normalized) {
|
||||
return { ok: true };
|
||||
@@ -121,23 +120,9 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
|
||||
: [];
|
||||
const replyToId = externalPayload.replyToId;
|
||||
const hasInteractive = (externalPayload.interactive?.blocks.length ?? 0) > 0;
|
||||
let hasChannelData =
|
||||
externalPayload.channelData != null && Object.keys(externalPayload.channelData).length > 0;
|
||||
if (
|
||||
channel === "slack" &&
|
||||
externalPayload.channelData?.slack &&
|
||||
typeof externalPayload.channelData.slack === "object" &&
|
||||
!Array.isArray(externalPayload.channelData.slack)
|
||||
) {
|
||||
try {
|
||||
hasChannelData = Boolean(
|
||||
parseSlackBlocksInput((externalPayload.channelData.slack as { blocks?: unknown }).blocks)
|
||||
?.length,
|
||||
);
|
||||
} catch {
|
||||
hasChannelData = false;
|
||||
}
|
||||
}
|
||||
const hasChannelData = plugin?.messaging?.hasStructuredReplyPayload?.({
|
||||
payload: externalPayload,
|
||||
});
|
||||
|
||||
// Skip empty replies.
|
||||
if (!text.trim() && mediaUrls.length === 0 && !hasInteractive && !hasChannelData) {
|
||||
|
||||
@@ -25,6 +25,8 @@ const discordPlugin: ChannelPlugin = {
|
||||
actions: {
|
||||
listActions: () => ["kick"],
|
||||
supportsAction: ({ action }) => action === "kick",
|
||||
requiresTrustedRequesterSender: ({ action, toolContext }) =>
|
||||
Boolean(action === "kick" && toolContext),
|
||||
handleAction,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,23 +6,13 @@ import type { ChannelMessageActionContext, ChannelMessageActionName } from "./ty
|
||||
|
||||
type ChannelActions = NonNullable<NonNullable<ReturnType<typeof getChannelPlugin>>["actions"]>;
|
||||
|
||||
const trustedRequesterRequiredByChannel: Readonly<
|
||||
Partial<Record<string, ReadonlySet<ChannelMessageActionName>>>
|
||||
> = {
|
||||
discord: new Set<ChannelMessageActionName>(["timeout", "kick", "ban"]),
|
||||
};
|
||||
|
||||
function requiresTrustedRequesterSender(ctx: ChannelMessageActionContext): boolean {
|
||||
const plugin = getChannelPlugin(ctx.channel);
|
||||
const fromPlugin = plugin?.actions?.requiresTrustedRequesterSender?.({
|
||||
action: ctx.action,
|
||||
toolContext: ctx.toolContext,
|
||||
});
|
||||
if (fromPlugin != null) {
|
||||
return fromPlugin;
|
||||
}
|
||||
return Boolean(
|
||||
trustedRequesterRequiredByChannel[ctx.channel]?.has(ctx.action) && ctx.toolContext,
|
||||
plugin?.actions?.requiresTrustedRequesterSender?.({
|
||||
action: ctx.action,
|
||||
toolContext: ctx.toolContext,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,22 @@ export type ChannelExecApprovalForwardTarget = {
|
||||
source?: "session" | "target";
|
||||
};
|
||||
|
||||
export type ChannelCapabilitiesDisplayTone = "default" | "muted" | "success" | "warn" | "error";
|
||||
|
||||
export type ChannelCapabilitiesDisplayLine = {
|
||||
text: string;
|
||||
tone?: ChannelCapabilitiesDisplayTone;
|
||||
};
|
||||
|
||||
export type ChannelCapabilitiesDiagnostics = {
|
||||
lines?: ChannelCapabilitiesDisplayLine[];
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type BivariantCallback<T extends (...args: never[]) => unknown> = {
|
||||
bivarianceHack: T;
|
||||
}["bivarianceHack"];
|
||||
|
||||
export type ChannelSetupAdapter = {
|
||||
resolveAccountId?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -153,12 +169,25 @@ export type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unkno
|
||||
timeoutMs: number;
|
||||
cfg: OpenClawConfig;
|
||||
}) => Promise<Probe>;
|
||||
formatCapabilitiesProbe?: BivariantCallback<
|
||||
(params: { probe: Probe }) => ChannelCapabilitiesDisplayLine[]
|
||||
>;
|
||||
auditAccount?: (params: {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: OpenClawConfig;
|
||||
probe?: Probe;
|
||||
}) => Promise<Audit>;
|
||||
buildCapabilitiesDiagnostics?: BivariantCallback<
|
||||
(params: {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: OpenClawConfig;
|
||||
probe?: Probe;
|
||||
audit?: Audit;
|
||||
target?: string;
|
||||
}) => Promise<ChannelCapabilitiesDiagnostics | undefined>
|
||||
>;
|
||||
buildAccountSnapshot?: (params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TopLevelComponents } from "@buape/carbon";
|
||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { PollInput } from "../../polls.js";
|
||||
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
||||
@@ -349,6 +350,7 @@ export type ChannelMessagingAdapter = {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => boolean;
|
||||
hasStructuredReplyPayload?: (params: { payload: ReplyPayload }) => boolean;
|
||||
targetResolver?: {
|
||||
looksLikeId?: (raw: string, normalized?: string) => boolean;
|
||||
hint?: string;
|
||||
|
||||
@@ -9,6 +9,9 @@ export type { ChannelMessageCapability } from "./message-capabilities.js";
|
||||
export type {
|
||||
ChannelAuthAdapter,
|
||||
ChannelCommandAdapter,
|
||||
ChannelCapabilitiesDiagnostics,
|
||||
ChannelCapabilitiesDisplayLine,
|
||||
ChannelCapabilitiesDisplayTone,
|
||||
ChannelConfigAdapter,
|
||||
ChannelDirectoryAdapter,
|
||||
ChannelExecApprovalAdapter,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isSlackInteractiveRepliesEnabled } from "../../extensions/slack/src/interactive-replies.js";
|
||||
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js";
|
||||
import {
|
||||
extractShortModelName,
|
||||
@@ -55,11 +54,7 @@ export function createReplyPrefixContext(params: {
|
||||
? (getChannelPlugin(params.channel)?.messaging?.enableInteractiveReplies?.({
|
||||
cfg,
|
||||
accountId: params.accountId,
|
||||
}) ??
|
||||
(params.channel === "slack"
|
||||
? isSlackInteractiveRepliesEnabled({ cfg, accountId: params.accountId })
|
||||
: undefined) ??
|
||||
undefined)
|
||||
}) ?? undefined)
|
||||
: undefined,
|
||||
responsePrefixContextProvider: () => prefixContext,
|
||||
onModelSelected,
|
||||
|
||||
@@ -321,12 +321,6 @@ export async function channelsAddCommand(
|
||||
});
|
||||
}
|
||||
|
||||
let previousTelegramToken = "";
|
||||
if (channel === "telegram") {
|
||||
const { resolveTelegramAccount } = await import("../../../extensions/telegram/src/accounts.js");
|
||||
previousTelegramToken = resolveTelegramAccount({ cfg: prevConfig, accountId }).token.trim();
|
||||
}
|
||||
|
||||
nextConfig = applyChannelAccountConfig({
|
||||
cfg: nextConfig,
|
||||
channel,
|
||||
@@ -340,16 +334,6 @@ export async function channelsAddCommand(
|
||||
accountId,
|
||||
runtime,
|
||||
});
|
||||
if (channel === "telegram") {
|
||||
const [{ resolveTelegramAccount }, { deleteTelegramUpdateOffset }] = await Promise.all([
|
||||
import("../../../extensions/telegram/src/accounts.js"),
|
||||
import("../../../extensions/telegram/src/update-offset-store.js"),
|
||||
]);
|
||||
const nextTelegramToken = resolveTelegramAccount({ cfg: nextConfig, accountId }).token.trim();
|
||||
if (previousTelegramToken !== nextTelegramToken) {
|
||||
await deleteTelegramUpdateOffset({ accountId });
|
||||
}
|
||||
}
|
||||
|
||||
await writeConfigFile(nextConfig);
|
||||
runtime.log(`Added ${channelLabel(channel)} account "${accountId}".`);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
process.env.NO_COLOR = "1";
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fetchSlackScopes } from "../../../extensions/slack/src/scopes.js";
|
||||
import { getChannelPlugin, listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import { channelsCapabilitiesCommand } from "./capabilities.js";
|
||||
@@ -21,10 +20,6 @@ vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getChannelPlugin: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../extensions/slack/src/scopes.js", () => ({
|
||||
fetchSlackScopes: vi.fn(),
|
||||
}));
|
||||
|
||||
const runtime = {
|
||||
log: (...args: unknown[]) => {
|
||||
logs.push(args.map(String).join(" "));
|
||||
@@ -95,14 +90,22 @@ describe("channelsCapabilitiesCommand", () => {
|
||||
},
|
||||
probe: { ok: true, bot: { name: "openclaw" }, team: { name: "team" } },
|
||||
});
|
||||
plugin.status = {
|
||||
...plugin.status,
|
||||
formatCapabilitiesProbe: () => [{ text: "Bot: @openclaw" }, { text: "Team: team" }],
|
||||
buildCapabilitiesDiagnostics: async () => ({
|
||||
lines: [
|
||||
{ text: "Bot scopes (auth.scopes): chat:write" },
|
||||
{ text: "User scopes (auth.scopes): users:read" },
|
||||
],
|
||||
details: {
|
||||
botScopes: { ok: true, scopes: ["chat:write"], source: "auth.scopes" },
|
||||
userScopes: { ok: true, scopes: ["users:read"], source: "auth.scopes" },
|
||||
},
|
||||
}),
|
||||
};
|
||||
vi.mocked(listChannelPlugins).mockReturnValue([plugin]);
|
||||
vi.mocked(getChannelPlugin).mockReturnValue(plugin);
|
||||
vi.mocked(fetchSlackScopes).mockImplementation(async (token: string) => {
|
||||
if (token === "xoxp-user") {
|
||||
return { ok: true, scopes: ["users:read"], source: "auth.scopes" };
|
||||
}
|
||||
return { ok: true, scopes: ["chat:write"], source: "auth.scopes" };
|
||||
});
|
||||
|
||||
await channelsCapabilitiesCommand({ channel: "slack" }, runtime);
|
||||
|
||||
@@ -111,8 +114,6 @@ describe("channelsCapabilitiesCommand", () => {
|
||||
expect(output).toContain("User scopes");
|
||||
expect(output).toContain("chat:write");
|
||||
expect(output).toContain("users:read");
|
||||
expect(fetchSlackScopes).toHaveBeenCalledWith("xoxb-bot", expect.any(Number));
|
||||
expect(fetchSlackScopes).toHaveBeenCalledWith("xoxp-user", expect.any(Number));
|
||||
});
|
||||
|
||||
it("prints Teams Graph permission hints when present", async () => {
|
||||
@@ -127,6 +128,15 @@ describe("channelsCapabilitiesCommand", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
plugin.status = {
|
||||
...plugin.status,
|
||||
formatCapabilitiesProbe: () => [
|
||||
{ text: "App: app-id" },
|
||||
{
|
||||
text: "Graph roles: ChannelMessage.Read.All (channel history), Files.Read.All (files (OneDrive))",
|
||||
},
|
||||
],
|
||||
};
|
||||
vi.mocked(listChannelPlugins).mockReturnValue([plugin]);
|
||||
vi.mocked(getChannelPlugin).mockReturnValue(plugin);
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { fetchChannelPermissionsDiscord } from "../../../extensions/discord/src/send.js";
|
||||
import { parseDiscordTarget } from "../../../extensions/discord/src/targets.js";
|
||||
import { fetchSlackScopes, type SlackScopesResult } from "../../../extensions/slack/src/scopes.js";
|
||||
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
|
||||
import { getChannelPlugin, listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import type { ChannelCapabilities, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type {
|
||||
ChannelCapabilities,
|
||||
ChannelCapabilitiesDiagnostics,
|
||||
ChannelCapabilitiesDisplayLine,
|
||||
ChannelPlugin,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { danger } from "../../globals.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
@@ -18,24 +20,6 @@ export type ChannelsCapabilitiesOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
type DiscordTargetSummary = {
|
||||
raw?: string;
|
||||
normalized?: string;
|
||||
kind?: "channel" | "user";
|
||||
channelId?: string;
|
||||
};
|
||||
|
||||
type DiscordPermissionsReport = {
|
||||
channelId?: string;
|
||||
guildId?: string;
|
||||
isDm?: boolean;
|
||||
channelType?: number;
|
||||
permissions?: string[];
|
||||
missingRequired?: string[];
|
||||
raw?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type ChannelCapabilitiesReport = {
|
||||
channel: string;
|
||||
accountId: string;
|
||||
@@ -45,24 +29,7 @@ type ChannelCapabilitiesReport = {
|
||||
support?: ChannelCapabilities;
|
||||
actions?: string[];
|
||||
probe?: unknown;
|
||||
slackScopes?: Array<{
|
||||
tokenType: "bot" | "user";
|
||||
result: SlackScopesResult;
|
||||
}>;
|
||||
target?: DiscordTargetSummary;
|
||||
channelPermissions?: DiscordPermissionsReport;
|
||||
};
|
||||
|
||||
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||
|
||||
const TEAMS_GRAPH_PERMISSION_HINTS: Record<string, string> = {
|
||||
"ChannelMessage.Read.All": "channel history",
|
||||
"Chat.Read.All": "chat history",
|
||||
"Channel.ReadBasic.All": "channel list",
|
||||
"Team.ReadBasic.All": "team list",
|
||||
"TeamsActivity.Read.All": "teams activity",
|
||||
"Sites.Read.All": "files (SharePoint)",
|
||||
"Files.Read.All": "files (OneDrive)",
|
||||
diagnostics?: ChannelCapabilitiesDiagnostics;
|
||||
};
|
||||
|
||||
function normalizeTimeout(raw: unknown, fallback = 10_000) {
|
||||
@@ -117,221 +84,35 @@ function formatSupport(capabilities?: ChannelCapabilities) {
|
||||
return bits.length ? bits.join(" ") : "none";
|
||||
}
|
||||
|
||||
function summarizeDiscordTarget(raw?: string): DiscordTargetSummary | undefined {
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
||||
if (!target) {
|
||||
return { raw };
|
||||
}
|
||||
if (target.kind === "channel") {
|
||||
return {
|
||||
raw,
|
||||
normalized: target.normalized,
|
||||
kind: "channel",
|
||||
channelId: target.id,
|
||||
};
|
||||
}
|
||||
if (target.kind === "user") {
|
||||
return {
|
||||
raw,
|
||||
normalized: target.normalized,
|
||||
kind: "user",
|
||||
};
|
||||
}
|
||||
return { raw, normalized: target.normalized };
|
||||
}
|
||||
|
||||
function formatDiscordIntents(intents?: {
|
||||
messageContent?: string;
|
||||
guildMembers?: string;
|
||||
presence?: string;
|
||||
}) {
|
||||
if (!intents) {
|
||||
return "unknown";
|
||||
}
|
||||
return [
|
||||
`messageContent=${intents.messageContent ?? "unknown"}`,
|
||||
`guildMembers=${intents.guildMembers ?? "unknown"}`,
|
||||
`presence=${intents.presence ?? "unknown"}`,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function formatProbeLines(channelId: string, probe: unknown): string[] {
|
||||
const lines: string[] = [];
|
||||
function formatGenericProbeLines(probe: unknown): ChannelCapabilitiesDisplayLine[] {
|
||||
if (!probe || typeof probe !== "object") {
|
||||
return lines;
|
||||
return [];
|
||||
}
|
||||
const probeObj = probe as Record<string, unknown>;
|
||||
|
||||
if (channelId === "discord") {
|
||||
const bot = probeObj.bot as { id?: string | null; username?: string | null } | undefined;
|
||||
if (bot?.username) {
|
||||
const botId = bot.id ? ` (${bot.id})` : "";
|
||||
lines.push(`Bot: ${theme.accent(`@${bot.username}`)}${botId}`);
|
||||
}
|
||||
const app = probeObj.application as { intents?: Record<string, unknown> } | undefined;
|
||||
if (app?.intents) {
|
||||
lines.push(`Intents: ${formatDiscordIntents(app.intents)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (channelId === "telegram") {
|
||||
const bot = probeObj.bot as { username?: string | null; id?: number | null } | undefined;
|
||||
if (bot?.username) {
|
||||
const botId = bot.id ? ` (${bot.id})` : "";
|
||||
lines.push(`Bot: ${theme.accent(`@${bot.username}`)}${botId}`);
|
||||
}
|
||||
const flags: string[] = [];
|
||||
const canJoinGroups = (bot as { canJoinGroups?: boolean | null })?.canJoinGroups;
|
||||
const canReadAll = (bot as { canReadAllGroupMessages?: boolean | null })
|
||||
?.canReadAllGroupMessages;
|
||||
const inlineQueries = (bot as { supportsInlineQueries?: boolean | null })
|
||||
?.supportsInlineQueries;
|
||||
if (typeof canJoinGroups === "boolean") {
|
||||
flags.push(`joinGroups=${canJoinGroups}`);
|
||||
}
|
||||
if (typeof canReadAll === "boolean") {
|
||||
flags.push(`readAllGroupMessages=${canReadAll}`);
|
||||
}
|
||||
if (typeof inlineQueries === "boolean") {
|
||||
flags.push(`inlineQueries=${inlineQueries}`);
|
||||
}
|
||||
if (flags.length > 0) {
|
||||
lines.push(`Flags: ${flags.join(" ")}`);
|
||||
}
|
||||
const webhook = probeObj.webhook as { url?: string | null } | undefined;
|
||||
if (webhook?.url !== undefined) {
|
||||
lines.push(`Webhook: ${webhook.url || "none"}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (channelId === "slack") {
|
||||
const bot = probeObj.bot as { name?: string } | undefined;
|
||||
const team = probeObj.team as { name?: string; id?: string } | undefined;
|
||||
if (bot?.name) {
|
||||
lines.push(`Bot: ${theme.accent(`@${bot.name}`)}`);
|
||||
}
|
||||
if (team?.name || team?.id) {
|
||||
const id = team?.id ? ` (${team.id})` : "";
|
||||
lines.push(`Team: ${team?.name ?? "unknown"}${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (channelId === "signal") {
|
||||
const version = probeObj.version as string | null | undefined;
|
||||
if (version) {
|
||||
lines.push(`Signal daemon: ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (channelId === "msteams") {
|
||||
const appId = typeof probeObj.appId === "string" ? probeObj.appId.trim() : "";
|
||||
if (appId) {
|
||||
lines.push(`App: ${theme.accent(appId)}`);
|
||||
}
|
||||
const graph = probeObj.graph as
|
||||
| { ok?: boolean; roles?: unknown; scopes?: unknown; error?: string }
|
||||
| undefined;
|
||||
if (graph) {
|
||||
const roles = Array.isArray(graph.roles)
|
||||
? graph.roles.map((role) => String(role).trim()).filter(Boolean)
|
||||
: [];
|
||||
const scopes =
|
||||
typeof graph.scopes === "string"
|
||||
? graph.scopes
|
||||
.split(/\s+/)
|
||||
.map((scope) => scope.trim())
|
||||
.filter(Boolean)
|
||||
: Array.isArray(graph.scopes)
|
||||
? graph.scopes.map((scope) => String(scope).trim()).filter(Boolean)
|
||||
: [];
|
||||
if (graph.ok === false) {
|
||||
lines.push(`Graph: ${theme.error(graph.error ?? "failed")}`);
|
||||
} else if (roles.length > 0 || scopes.length > 0) {
|
||||
const formatPermission = (permission: string) => {
|
||||
const hint = TEAMS_GRAPH_PERMISSION_HINTS[permission];
|
||||
return hint ? `${permission} (${hint})` : permission;
|
||||
};
|
||||
if (roles.length > 0) {
|
||||
lines.push(`Graph roles: ${roles.map(formatPermission).join(", ")}`);
|
||||
}
|
||||
if (scopes.length > 0) {
|
||||
lines.push(`Graph scopes: ${scopes.map(formatPermission).join(", ")}`);
|
||||
}
|
||||
} else if (graph.ok === true) {
|
||||
lines.push("Graph: ok");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ok = typeof probeObj.ok === "boolean" ? probeObj.ok : undefined;
|
||||
if (ok === true && lines.length === 0) {
|
||||
lines.push("Probe: ok");
|
||||
if (ok === true) {
|
||||
return [{ text: "Probe: ok" }];
|
||||
}
|
||||
if (ok === false) {
|
||||
const error =
|
||||
typeof probeObj.error === "string" && probeObj.error ? ` (${probeObj.error})` : "";
|
||||
lines.push(`Probe: ${theme.error(`failed${error}`)}`);
|
||||
return [{ text: `Probe: failed${error}`, tone: "error" }];
|
||||
}
|
||||
return lines;
|
||||
return [];
|
||||
}
|
||||
|
||||
async function buildDiscordPermissions(params: {
|
||||
account: { token?: string; accountId?: string };
|
||||
target?: string;
|
||||
}): Promise<{ target?: DiscordTargetSummary; report?: DiscordPermissionsReport }> {
|
||||
const target = summarizeDiscordTarget(params.target?.trim());
|
||||
if (!target) {
|
||||
return {};
|
||||
}
|
||||
if (target.kind !== "channel" || !target.channelId) {
|
||||
return {
|
||||
target,
|
||||
report: {
|
||||
error: "Target looks like a DM user; pass channel:<id> to audit channel permissions.",
|
||||
},
|
||||
};
|
||||
}
|
||||
const token = params.account.token?.trim();
|
||||
if (!token) {
|
||||
return {
|
||||
target,
|
||||
report: {
|
||||
channelId: target.channelId,
|
||||
error: "Discord bot token missing for permission audit.",
|
||||
},
|
||||
};
|
||||
}
|
||||
try {
|
||||
const perms = await fetchChannelPermissionsDiscord(target.channelId, {
|
||||
token,
|
||||
accountId: params.account.accountId ?? undefined,
|
||||
});
|
||||
const missing = REQUIRED_DISCORD_PERMISSIONS.filter(
|
||||
(permission) => !perms.permissions.includes(permission),
|
||||
);
|
||||
return {
|
||||
target,
|
||||
report: {
|
||||
channelId: perms.channelId,
|
||||
guildId: perms.guildId,
|
||||
isDm: perms.isDm,
|
||||
channelType: perms.channelType,
|
||||
permissions: perms.permissions,
|
||||
missingRequired: missing.length ? missing : [],
|
||||
raw: perms.raw,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
target,
|
||||
report: {
|
||||
channelId: target.channelId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
},
|
||||
};
|
||||
function renderDisplayLine(line: ChannelCapabilitiesDisplayLine) {
|
||||
switch (line.tone) {
|
||||
case "muted":
|
||||
return theme.muted(line.text);
|
||||
case "success":
|
||||
return theme.success(line.text);
|
||||
case "warn":
|
||||
return theme.warn(line.text);
|
||||
case "error":
|
||||
return theme.error(line.text);
|
||||
default:
|
||||
return line.text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,41 +159,16 @@ async function resolveChannelReports(params: {
|
||||
}
|
||||
}
|
||||
|
||||
let slackScopes: ChannelCapabilitiesReport["slackScopes"];
|
||||
if (plugin.id === "slack" && configured && enabled) {
|
||||
const botToken = (resolvedAccount as { botToken?: string }).botToken?.trim();
|
||||
const userToken = (resolvedAccount as { userToken?: string }).userToken?.trim();
|
||||
const scopeReports: NonNullable<ChannelCapabilitiesReport["slackScopes"]> = [];
|
||||
if (botToken) {
|
||||
scopeReports.push({
|
||||
tokenType: "bot",
|
||||
result: await fetchSlackScopes(botToken, timeoutMs),
|
||||
});
|
||||
} else {
|
||||
scopeReports.push({
|
||||
tokenType: "bot",
|
||||
result: { ok: false, error: "Slack bot token missing." },
|
||||
});
|
||||
}
|
||||
if (userToken) {
|
||||
scopeReports.push({
|
||||
tokenType: "user",
|
||||
result: await fetchSlackScopes(userToken, timeoutMs),
|
||||
});
|
||||
}
|
||||
slackScopes = scopeReports;
|
||||
}
|
||||
|
||||
let discordTarget: DiscordTargetSummary | undefined;
|
||||
let discordPermissions: DiscordPermissionsReport | undefined;
|
||||
if (plugin.id === "discord" && params.target) {
|
||||
const perms = await buildDiscordPermissions({
|
||||
account: resolvedAccount as { token?: string; accountId?: string },
|
||||
target: params.target,
|
||||
});
|
||||
discordTarget = perms.target;
|
||||
discordPermissions = perms.report;
|
||||
}
|
||||
const diagnostics =
|
||||
configured && enabled
|
||||
? await plugin.status?.buildCapabilitiesDiagnostics?.({
|
||||
account: resolvedAccount,
|
||||
timeoutMs,
|
||||
cfg,
|
||||
probe,
|
||||
target: params.target,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
reports.push({
|
||||
channel: plugin.id,
|
||||
@@ -425,10 +181,8 @@ async function resolveChannelReports(params: {
|
||||
enabled,
|
||||
support: plugin.capabilities,
|
||||
probe,
|
||||
target: discordTarget,
|
||||
channelPermissions: discordPermissions,
|
||||
actions,
|
||||
slackScopes,
|
||||
diagnostics,
|
||||
});
|
||||
}
|
||||
return reports;
|
||||
@@ -451,8 +205,8 @@ export async function channelsCapabilitiesCommand(
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (rawTarget && rawChannel !== "discord") {
|
||||
runtime.error(danger("--target requires --channel discord."));
|
||||
if (rawTarget && (!rawChannel || rawChannel === "all")) {
|
||||
runtime.error(danger("--target requires a specific --channel."));
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -484,7 +238,7 @@ export async function channelsCapabilitiesCommand(
|
||||
cfg,
|
||||
timeoutMs,
|
||||
accountOverride,
|
||||
target: rawTarget && plugin.id === "discord" ? rawTarget : undefined,
|
||||
target: rawTarget || undefined,
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -513,39 +267,17 @@ export async function channelsCapabilitiesCommand(
|
||||
const enabledLabel = report.enabled === false ? "disabled" : "enabled";
|
||||
lines.push(`Status: ${configuredLabel}, ${enabledLabel}`);
|
||||
}
|
||||
const probeLines = formatProbeLines(report.channel, report.probe);
|
||||
const probeLines =
|
||||
getChannelPlugin(report.channel)?.status?.formatCapabilitiesProbe?.({
|
||||
probe: report.probe,
|
||||
}) ?? formatGenericProbeLines(report.probe);
|
||||
if (probeLines.length > 0) {
|
||||
lines.push(...probeLines);
|
||||
lines.push(...probeLines.map(renderDisplayLine));
|
||||
} else if (report.configured && report.enabled) {
|
||||
lines.push(theme.muted("Probe: unavailable"));
|
||||
}
|
||||
if (report.channel === "slack" && report.slackScopes) {
|
||||
for (const entry of report.slackScopes) {
|
||||
const source = entry.result.source ? ` (${entry.result.source})` : "";
|
||||
const label = entry.tokenType === "user" ? "User scopes" : "Bot scopes";
|
||||
if (entry.result.ok && entry.result.scopes?.length) {
|
||||
lines.push(`${label}${source}: ${entry.result.scopes.join(", ")}`);
|
||||
} else if (entry.result.error) {
|
||||
lines.push(`${label}: ${theme.error(entry.result.error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (report.channel === "discord" && report.channelPermissions) {
|
||||
const perms = report.channelPermissions;
|
||||
if (perms.error) {
|
||||
lines.push(`Permissions: ${theme.error(perms.error)}`);
|
||||
} else {
|
||||
const list = perms.permissions?.length ? perms.permissions.join(", ") : "none";
|
||||
const label = perms.channelId ? ` (${perms.channelId})` : "";
|
||||
lines.push(`Permissions${label}: ${list}`);
|
||||
if (perms.missingRequired && perms.missingRequired.length > 0) {
|
||||
lines.push(`${theme.warn("Missing required:")} ${perms.missingRequired.join(", ")}`);
|
||||
} else {
|
||||
lines.push(theme.success("Missing required: none"));
|
||||
}
|
||||
}
|
||||
} else if (report.channel === "discord" && rawTarget && !report.channelPermissions) {
|
||||
lines.push(theme.muted("Permissions: skipped (no target)."));
|
||||
if (report.diagnostics?.lines?.length) {
|
||||
lines.push(...report.diagnostics.lines.map(renderDisplayLine));
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
@@ -118,11 +118,6 @@ export async function channelsRemoveCommand(
|
||||
accountId: resolvedAccountId,
|
||||
runtime,
|
||||
});
|
||||
if (channel === "telegram") {
|
||||
const { deleteTelegramUpdateOffset } =
|
||||
await import("../../../extensions/telegram/src/update-offset-store.js");
|
||||
await deleteTelegramUpdateOffset({ accountId: resolvedAccountId });
|
||||
}
|
||||
} else {
|
||||
if (!plugin.config.setAccountEnabled) {
|
||||
runtime.error(`Channel ${channel} does not support disable.`);
|
||||
|
||||
Reference in New Issue
Block a user