openskynet / src /commands /status-all /channels.ts
Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import fs from "node:fs";
import {
hasConfiguredUnavailableCredentialStatus,
hasResolvedCredentialValue,
} from "../../channels/account-snapshot-fields.js";
import {
buildChannelAccountSnapshot,
formatChannelAllowFrom,
resolveChannelAccountConfigured,
resolveChannelAccountEnabled,
} from "../../channels/account-summary.js";
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import type {
ChannelAccountSnapshot,
ChannelId,
ChannelPlugin,
} from "../../channels/plugins/types.js";
import { inspectReadOnlyChannelAccount } from "../../channels/read-only-account-inspect.js";
import type { OpenClawConfig } from "../../config/config.js";
import { sha256HexPrefix } from "../../logging/redact-identifier.js";
import { formatTimeAgo } from "./format.js";
export type ChannelRow = {
id: ChannelId;
label: string;
enabled: boolean;
state: "ok" | "setup" | "warn" | "off";
detail: string;
};
type ChannelAccountRow = {
accountId: string;
account: unknown;
enabled: boolean;
configured: boolean;
snapshot: ChannelAccountSnapshot;
};
type ResolvedChannelAccountRowParams = {
plugin: ChannelPlugin;
cfg: OpenClawConfig;
sourceConfig: OpenClawConfig;
accountId: string;
};
const asRecord = (value: unknown): Record<string, unknown> =>
value && typeof value === "object" ? (value as Record<string, unknown>) : {};
function summarizeSources(sources: Array<string | undefined>): {
label: string;
parts: string[];
} {
const counts = new Map<string, number>();
for (const s of sources) {
const key = s?.trim() ? s.trim() : "unknown";
counts.set(key, (counts.get(key) ?? 0) + 1);
}
const parts = [...counts.entries()]
.toSorted((a, b) => b[1] - a[1])
.map(([key, n]) => `${key}${n > 1 ? ${n}` : ""}`);
const label = parts.length > 0 ? parts.join("+") : "unknown";
return { label, parts };
}
function existsSyncMaybe(p: string | undefined): boolean | null {
const path = p?.trim() || "";
if (!path) {
return null;
}
try {
return fs.existsSync(path);
} catch {
return null;
}
}
function formatTokenHint(token: string, opts: { showSecrets: boolean }): string {
const t = token.trim();
if (!t) {
return "empty";
}
if (!opts.showSecrets) {
return `sha256:${sha256HexPrefix(t, 8)} · len ${t.length}`;
}
const head = t.slice(0, 4);
const tail = t.slice(-4);
if (t.length <= 10) {
return `${t} · len ${t.length}`;
}
return `${head}${tail} · len ${t.length}`;
}
function inspectChannelAccount(plugin: ChannelPlugin, cfg: OpenClawConfig, accountId: string) {
return (
plugin.config.inspectAccount?.(cfg, accountId) ??
inspectReadOnlyChannelAccount({
channelId: plugin.id,
cfg,
accountId,
})
);
}
async function resolveChannelAccountRow(
params: ResolvedChannelAccountRowParams,
): Promise<ChannelAccountRow> {
const { plugin, cfg, sourceConfig, accountId } = params;
const sourceInspectedAccount = inspectChannelAccount(plugin, sourceConfig, accountId);
const resolvedInspectedAccount = inspectChannelAccount(plugin, cfg, accountId);
const resolvedInspection = resolvedInspectedAccount as {
enabled?: boolean;
configured?: boolean;
} | null;
const sourceInspection = sourceInspectedAccount as {
enabled?: boolean;
configured?: boolean;
} | null;
const resolvedAccount = resolvedInspectedAccount ?? plugin.config.resolveAccount(cfg, accountId);
const useSourceUnavailableAccount = Boolean(
sourceInspectedAccount &&
hasConfiguredUnavailableCredentialStatus(sourceInspectedAccount) &&
(!hasResolvedCredentialValue(resolvedAccount) ||
(sourceInspection?.configured === true && resolvedInspection?.configured === false)),
);
const account = useSourceUnavailableAccount ? sourceInspectedAccount : resolvedAccount;
const selectedInspection = useSourceUnavailableAccount ? sourceInspection : resolvedInspection;
const enabled =
selectedInspection?.enabled ?? resolveChannelAccountEnabled({ plugin, account, cfg });
const configured =
selectedInspection?.configured ??
(await resolveChannelAccountConfigured({
plugin,
account,
cfg,
readAccountConfiguredField: true,
}));
const snapshot = buildChannelAccountSnapshot({
plugin,
cfg,
accountId,
account,
enabled,
configured,
});
return { accountId, account, enabled, configured, snapshot };
}
const formatAccountLabel = (params: { accountId: string; name?: string }) => {
const base = params.accountId || "default";
if (params.name?.trim()) {
return `${base} (${params.name.trim()})`;
}
return base;
};
const buildAccountNotes = (params: {
plugin: ChannelPlugin;
cfg: OpenClawConfig;
entry: ChannelAccountRow;
}) => {
const { plugin, cfg, entry } = params;
const notes: string[] = [];
const snapshot = entry.snapshot;
if (snapshot.enabled === false) {
notes.push("disabled");
}
if (snapshot.dmPolicy) {
notes.push(`dm:${snapshot.dmPolicy}`);
}
if (snapshot.tokenSource && snapshot.tokenSource !== "none") {
notes.push(`token:${snapshot.tokenSource}`);
}
if (snapshot.botTokenSource && snapshot.botTokenSource !== "none") {
notes.push(`bot:${snapshot.botTokenSource}`);
}
if (snapshot.appTokenSource && snapshot.appTokenSource !== "none") {
notes.push(`app:${snapshot.appTokenSource}`);
}
if (
snapshot.signingSecretSource &&
snapshot.signingSecretSource !== "none" /* pragma: allowlist secret */
) {
notes.push(`signing:${snapshot.signingSecretSource}`);
}
if (hasConfiguredUnavailableCredentialStatus(entry.account)) {
notes.push("secret unavailable in this command path");
}
if (snapshot.baseUrl) {
notes.push(snapshot.baseUrl);
}
if (snapshot.port != null) {
notes.push(`port:${snapshot.port}`);
}
if (snapshot.cliPath) {
notes.push(`cli:${snapshot.cliPath}`);
}
if (snapshot.dbPath) {
notes.push(`db:${snapshot.dbPath}`);
}
const allowFrom =
plugin.config.resolveAllowFrom?.({ cfg, accountId: snapshot.accountId }) ?? snapshot.allowFrom;
if (allowFrom?.length) {
const formatted = formatChannelAllowFrom({
plugin,
cfg,
accountId: snapshot.accountId,
allowFrom,
}).slice(0, 3);
if (formatted.length > 0) {
notes.push(`allow:${formatted.join(",")}`);
}
}
return notes;
};
function resolveLinkFields(summary: unknown): {
linked: boolean | null;
authAgeMs: number | null;
selfE164: string | null;
} {
const rec = asRecord(summary);
const linked = typeof rec.linked === "boolean" ? rec.linked : null;
const authAgeMs = typeof rec.authAgeMs === "number" ? rec.authAgeMs : null;
const self = asRecord(rec.self);
const selfE164 = typeof self.e164 === "string" && self.e164.trim() ? self.e164.trim() : null;
return { linked, authAgeMs, selfE164 };
}
function collectMissingPaths(accounts: ChannelAccountRow[]): string[] {
const missing: string[] = [];
for (const entry of accounts) {
const accountRec = asRecord(entry.account);
const snapshotRec = asRecord(entry.snapshot);
for (const key of [
"tokenFile",
"botTokenFile",
"appTokenFile",
"cliPath",
"dbPath",
"authDir",
]) {
const raw =
(accountRec[key] as string | undefined) ?? (snapshotRec[key] as string | undefined);
const ok = existsSyncMaybe(raw);
if (ok === false) {
missing.push(String(raw));
}
}
}
return missing;
}
function summarizeTokenConfig(params: {
plugin: ChannelPlugin;
cfg: OpenClawConfig;
accounts: ChannelAccountRow[];
showSecrets: boolean;
}): { state: "ok" | "setup" | "warn" | null; detail: string | null } {
const enabled = params.accounts.filter((a) => a.enabled);
if (enabled.length === 0) {
return { state: null, detail: null };
}
const accountRecs = enabled.map((a) => asRecord(a.account));
const hasBotTokenField = accountRecs.some((r) => "botToken" in r);
const hasAppTokenField = accountRecs.some((r) => "appToken" in r);
const hasSigningSecretField = accountRecs.some(
(r) => "signingSecret" in r || "signingSecretSource" in r || "signingSecretStatus" in r,
);
const hasTokenField = accountRecs.some((r) => "token" in r);
if (!hasBotTokenField && !hasAppTokenField && !hasSigningSecretField && !hasTokenField) {
return { state: null, detail: null };
}
const accountIsHttpMode = (rec: Record<string, unknown>) =>
typeof rec.mode === "string" && rec.mode.trim() === "http";
const hasCredentialAvailable = (
rec: Record<string, unknown>,
valueKey: string,
statusKey: string,
) => {
const value = rec[valueKey];
if (typeof value === "string" && value.trim()) {
return true;
}
return rec[statusKey] === "available";
};
if (
hasBotTokenField &&
hasSigningSecretField &&
enabled.every((a) => accountIsHttpMode(asRecord(a.account)))
) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
return (
hasCredentialAvailable(rec, "botToken", "botTokenStatus") &&
hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus")
);
});
const partial = enabled.filter((a) => {
const rec = asRecord(a.account);
const hasBot = hasCredentialAvailable(rec, "botToken", "botTokenStatus");
const hasSigning = hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus");
return (hasBot && !hasSigning) || (!hasBot && hasSigning);
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured http credentials unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (partial.length > 0) {
return {
state: "warn",
detail: `partial credentials (need bot+signing) · accounts ${partial.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no credentials (need bot+signing)" };
}
const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none"));
const signingSources = summarizeSources(
ready.map((a) => a.snapshot.signingSecretSource ?? "none"),
);
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const signingSecret = typeof sample.signingSecret === "string" ? sample.signingSecret : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const signingHint = signingSecret.trim()
? formatTokenHint(signingSecret, { showSecrets: params.showSecrets })
: "";
const hint =
botHint || signingHint ? ` (bot ${botHint || "?"}, signing ${signingHint || "?"})` : "";
return {
state: "ok",
detail: `credentials ok (bot ${botSources.label}, signing ${signingSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
if (hasBotTokenField && hasAppTokenField) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = typeof rec.botToken === "string" ? rec.botToken.trim() : "";
const app = typeof rec.appToken === "string" ? rec.appToken.trim() : "";
return Boolean(bot) && Boolean(app);
});
const partial = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = typeof rec.botToken === "string" ? rec.botToken.trim() : "";
const app = typeof rec.appToken === "string" ? rec.appToken.trim() : "";
const hasBot = Boolean(bot);
const hasApp = Boolean(app);
return (hasBot && !hasApp) || (!hasBot && hasApp);
});
if (partial.length > 0) {
return {
state: "warn",
detail: `partial tokens (need bot+app) · accounts ${partial.length}`,
};
}
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured tokens unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no tokens (need bot+app)" };
}
const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none"));
const appSources = summarizeSources(ready.map((a) => a.snapshot.appTokenSource ?? "none"));
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const appToken = typeof sample.appToken === "string" ? sample.appToken : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const appHint = appToken.trim()
? formatTokenHint(appToken, { showSecrets: params.showSecrets })
: "";
const hint = botHint || appHint ? ` (bot ${botHint || "?"}, app ${appHint || "?"})` : "";
return {
state: "ok",
detail: `tokens ok (bot ${botSources.label}, app ${appSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
if (hasBotTokenField) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = typeof rec.botToken === "string" ? rec.botToken.trim() : "";
return Boolean(bot);
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured bot token unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no bot token" };
}
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const hint = botHint ? ` (${botHint})` : "";
return {
state: "ok",
detail: `bot token config${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
return typeof rec.token === "string" ? Boolean(rec.token.trim()) : false;
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured token unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no token" };
}
const sources = summarizeSources(ready.map((a) => a.snapshot.tokenSource));
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const token = typeof sample.token === "string" ? sample.token : "";
const hint = token.trim()
? ` (${formatTokenHint(token, { showSecrets: params.showSecrets })})`
: "";
return {
state: "ok",
detail: `token ${sources.label}${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
// `status --all` channels table.
// Keep this generic: channel-specific rules belong in the channel plugin.
export async function buildChannelsTable(
cfg: OpenClawConfig,
opts?: { showSecrets?: boolean; sourceConfig?: OpenClawConfig },
): Promise<{
rows: ChannelRow[];
details: Array<{
title: string;
columns: string[];
rows: Array<Record<string, string>>;
}>;
}> {
const showSecrets = opts?.showSecrets === true;
const rows: ChannelRow[] = [];
const details: Array<{
title: string;
columns: string[];
rows: Array<Record<string, string>>;
}> = [];
for (const plugin of listChannelPlugins()) {
const accountIds = plugin.config.listAccountIds(cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
cfg,
accountIds,
});
const resolvedAccountIds = accountIds.length > 0 ? accountIds : [defaultAccountId];
const accounts: ChannelAccountRow[] = [];
const sourceConfig = opts?.sourceConfig ?? cfg;
for (const accountId of resolvedAccountIds) {
accounts.push(
await resolveChannelAccountRow({
plugin,
cfg,
sourceConfig,
accountId,
}),
);
}
const anyEnabled = accounts.some((a) => a.enabled);
const enabledAccounts = accounts.filter((a) => a.enabled);
const configuredAccounts = enabledAccounts.filter((a) => a.configured);
const unavailableConfiguredAccounts = enabledAccounts.filter((a) =>
hasConfiguredUnavailableCredentialStatus(a.account),
);
const defaultEntry = accounts.find((a) => a.accountId === defaultAccountId) ?? accounts[0];
const summary = plugin.status?.buildChannelSummary
? await plugin.status.buildChannelSummary({
account: defaultEntry?.account ?? {},
cfg,
defaultAccountId,
snapshot:
defaultEntry?.snapshot ?? ({ accountId: defaultAccountId } as ChannelAccountSnapshot),
})
: undefined;
const link = resolveLinkFields(summary);
const missingPaths = collectMissingPaths(enabledAccounts);
const tokenSummary = summarizeTokenConfig({
plugin,
cfg,
accounts,
showSecrets,
});
const issues = plugin.status?.collectStatusIssues
? plugin.status.collectStatusIssues(accounts.map((a) => a.snapshot))
: [];
const label = plugin.meta.label ?? plugin.id;
const state = (() => {
if (!anyEnabled) {
return "off";
}
if (missingPaths.length > 0) {
return "warn";
}
if (issues.length > 0) {
return "warn";
}
if (unavailableConfiguredAccounts.length > 0) {
return "warn";
}
if (link.linked === false) {
return "setup";
}
if (tokenSummary.state) {
return tokenSummary.state;
}
if (link.linked === true) {
return "ok";
}
if (configuredAccounts.length > 0) {
return "ok";
}
return "setup";
})();
const detail = (() => {
if (!anyEnabled) {
if (!defaultEntry) {
return "disabled";
}
return plugin.config.disabledReason?.(defaultEntry.account, cfg) ?? "disabled";
}
if (missingPaths.length > 0) {
return `missing file (${missingPaths[0]})`;
}
if (issues.length > 0) {
return issues[0]?.message ?? "misconfigured";
}
if (link.linked !== null) {
const base = link.linked ? "linked" : "not linked";
const extra: string[] = [];
if (link.linked && link.selfE164) {
extra.push(link.selfE164);
}
if (link.linked && link.authAgeMs != null && link.authAgeMs >= 0) {
extra.push(`auth ${formatTimeAgo(link.authAgeMs)}`);
}
if (accounts.length > 1 || plugin.meta.forceAccountBinding) {
extra.push(`accounts ${accounts.length || 1}`);
}
return extra.length > 0 ? `${base} · ${extra.join(" · ")}` : base;
}
if (unavailableConfiguredAccounts.length > 0) {
if (tokenSummary.detail?.includes("unavailable")) {
return tokenSummary.detail;
}
return `configured credentials unavailable in this command path · accounts ${unavailableConfiguredAccounts.length}`;
}
if (tokenSummary.detail) {
return tokenSummary.detail;
}
if (configuredAccounts.length > 0) {
const head = "configured";
if (accounts.length <= 1 && !plugin.meta.forceAccountBinding) {
return head;
}
return `${head} · accounts ${configuredAccounts.length}/${enabledAccounts.length || 1}`;
}
const reason =
defaultEntry && plugin.config.unconfiguredReason
? plugin.config.unconfiguredReason(defaultEntry.account, cfg)
: null;
return reason ?? "not configured";
})();
rows.push({
id: plugin.id,
label,
enabled: anyEnabled,
state,
detail,
});
if (configuredAccounts.length > 0) {
details.push({
title: `${label} accounts`,
columns: ["Account", "Status", "Notes"],
rows: configuredAccounts.map((entry) => {
const notes = buildAccountNotes({ plugin, cfg, entry });
return {
Account: formatAccountLabel({
accountId: entry.accountId,
name: entry.snapshot.name,
}),
Status:
entry.enabled && !hasConfiguredUnavailableCredentialStatus(entry.account)
? "OK"
: "WARN",
Notes: notes.join(" · "),
};
}),
});
}
}
return {
rows,
details,
};
}