|
|
|
|
@ -1,3 +1,4 @@
|
|
|
|
|
import fs from "node:fs";
|
|
|
|
|
import type { ClawdbotConfig } from "../../config/config.js";
|
|
|
|
|
import {
|
|
|
|
|
listDiscordAccountIds,
|
|
|
|
|
@ -35,10 +36,36 @@ import { formatAge } from "./format.js";
|
|
|
|
|
export type ProviderRow = {
|
|
|
|
|
provider: string;
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
configured: boolean;
|
|
|
|
|
state: "ok" | "setup" | "warn" | "off";
|
|
|
|
|
detail: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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()]
|
|
|
|
|
.sort((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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
|
|
|
|
|
rows: ProviderRow[];
|
|
|
|
|
details: Array<{
|
|
|
|
|
@ -67,11 +94,11 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
|
|
|
|
|
rows.push({
|
|
|
|
|
provider: "WhatsApp",
|
|
|
|
|
enabled: waEnabled,
|
|
|
|
|
configured: waLinked,
|
|
|
|
|
state: !waEnabled ? "off" : waLinked ? "ok" : "setup",
|
|
|
|
|
detail: waEnabled
|
|
|
|
|
? waLinked
|
|
|
|
|
? `linked${waSelf ? ` ${waSelf}` : ""}${waAuthAgeMs ? ` · auth ${formatAge(waAuthAgeMs)}` : ""} · accounts ${waAccounts.length || 1}`
|
|
|
|
|
: "not linked"
|
|
|
|
|
: "not linked (run clawdbot login)"
|
|
|
|
|
: "disabled",
|
|
|
|
|
});
|
|
|
|
|
if (waLinked) {
|
|
|
|
|
@ -96,7 +123,7 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
|
|
|
|
|
Account: account.name?.trim()
|
|
|
|
|
? `${account.accountId} (${account.name.trim()})`
|
|
|
|
|
: account.accountId,
|
|
|
|
|
Status: account.enabled ? "OK" : "WARN",
|
|
|
|
|
Status: account.enabled ? "OK" : "OFF",
|
|
|
|
|
Notes: notes.join(" · "),
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
@ -108,15 +135,43 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
|
|
|
|
|
const tgAccounts = listTelegramAccountIds(cfg).map((accountId) =>
|
|
|
|
|
resolveTelegramAccount({ cfg, accountId }),
|
|
|
|
|
);
|
|
|
|
|
const tgConfigured = tgAccounts.some((a) => Boolean(a.token?.trim()));
|
|
|
|
|
const tgEnabledAccounts = tgAccounts.filter((a) => a.enabled);
|
|
|
|
|
const tgTokenAccounts = tgEnabledAccounts.filter((a) => a.token?.trim());
|
|
|
|
|
const tgSources = summarizeSources(tgTokenAccounts.map((a) => a.tokenSource));
|
|
|
|
|
const tgMissingFiles: string[] = [];
|
|
|
|
|
const tgGlobalTokenFileExists = existsSyncMaybe(cfg.telegram?.tokenFile);
|
|
|
|
|
if (
|
|
|
|
|
tgEnabled &&
|
|
|
|
|
cfg.telegram?.tokenFile?.trim() &&
|
|
|
|
|
tgGlobalTokenFileExists === false
|
|
|
|
|
) {
|
|
|
|
|
tgMissingFiles.push("telegram.tokenFile");
|
|
|
|
|
}
|
|
|
|
|
for (const accountId of listTelegramAccountIds(cfg)) {
|
|
|
|
|
const tokenFile =
|
|
|
|
|
cfg.telegram?.accounts?.[accountId]?.tokenFile?.trim() || "";
|
|
|
|
|
const ok = existsSyncMaybe(tokenFile);
|
|
|
|
|
if (tgEnabled && tokenFile && ok === false) {
|
|
|
|
|
tgMissingFiles.push(`telegram.accounts.${accountId}.tokenFile`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const tgMisconfigured = tgMissingFiles.length > 0;
|
|
|
|
|
rows.push({
|
|
|
|
|
provider: "Telegram",
|
|
|
|
|
enabled: tgEnabled,
|
|
|
|
|
configured: tgEnabled && tgConfigured,
|
|
|
|
|
state: !tgEnabled
|
|
|
|
|
? "off"
|
|
|
|
|
: tgMisconfigured
|
|
|
|
|
? "warn"
|
|
|
|
|
: tgTokenAccounts.length > 0
|
|
|
|
|
? "ok"
|
|
|
|
|
: "setup",
|
|
|
|
|
detail: tgEnabled
|
|
|
|
|
? tgConfigured
|
|
|
|
|
? `accounts ${tgAccounts.filter((a) => a.token?.trim()).length}`
|
|
|
|
|
: "not configured"
|
|
|
|
|
? tgMisconfigured
|
|
|
|
|
? `token file missing (${tgMissingFiles[0]})`
|
|
|
|
|
: tgTokenAccounts.length > 0
|
|
|
|
|
? `bot token ${tgSources.label} · accounts ${tgTokenAccounts.length}/${tgEnabledAccounts.length || 1}`
|
|
|
|
|
: "no bot token (TELEGRAM_BOT_TOKEN / telegram.botToken)"
|
|
|
|
|
: "disabled",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@ -125,15 +180,17 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
|
|
|
|
|
const dcAccounts = listDiscordAccountIds(cfg).map((accountId) =>
|
|
|
|
|
resolveDiscordAccount({ cfg, accountId }),
|
|
|
|
|
);
|
|
|
|
|
const dcConfigured = dcAccounts.some((a) => Boolean(a.token?.trim()));
|
|
|
|
|
const dcEnabledAccounts = dcAccounts.filter((a) => a.enabled);
|
|
|
|
|
const dcTokenAccounts = dcEnabledAccounts.filter((a) => a.token?.trim());
|
|
|
|
|
const dcSources = summarizeSources(dcTokenAccounts.map((a) => a.tokenSource));
|
|
|
|
|
rows.push({
|
|
|
|
|
provider: "Discord",
|
|
|
|
|
enabled: dcEnabled,
|
|
|
|
|
configured: dcEnabled && dcConfigured,
|
|
|
|
|
state: !dcEnabled ? "off" : dcTokenAccounts.length > 0 ? "ok" : "setup",
|
|
|
|
|
detail: dcEnabled
|
|
|
|
|
? dcConfigured
|
|
|
|
|
? `accounts ${dcAccounts.filter((a) => a.token?.trim()).length}`
|
|
|
|
|
: "not configured"
|
|
|
|
|
? dcTokenAccounts.length > 0
|
|
|
|
|
? `bot token ${dcSources.label} · accounts ${dcTokenAccounts.length}/${dcEnabledAccounts.length || 1}`
|
|
|
|
|
: "no bot token (DISCORD_BOT_TOKEN / discord.token)"
|
|
|
|
|
: "disabled",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@ -142,17 +199,42 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
|
|
|
|
|
const slAccounts = listSlackAccountIds(cfg).map((accountId) =>
|
|
|
|
|
resolveSlackAccount({ cfg, accountId }),
|
|
|
|
|
);
|
|
|
|
|
const slConfigured = slAccounts.some(
|
|
|
|
|
const slEnabledAccounts = slAccounts.filter((a) => a.enabled);
|
|
|
|
|
const slReady = slEnabledAccounts.filter(
|
|
|
|
|
(a) => Boolean(a.botToken?.trim()) && Boolean(a.appToken?.trim()),
|
|
|
|
|
);
|
|
|
|
|
const slPartial = slEnabledAccounts.filter(
|
|
|
|
|
(a) =>
|
|
|
|
|
(a.botToken?.trim() && !a.appToken?.trim()) ||
|
|
|
|
|
(!a.botToken?.trim() && a.appToken?.trim()),
|
|
|
|
|
);
|
|
|
|
|
const slHasAnyToken = slEnabledAccounts.some(
|
|
|
|
|
(a) => Boolean(a.botToken?.trim()) || Boolean(a.appToken?.trim()),
|
|
|
|
|
);
|
|
|
|
|
const slBotSources = summarizeSources(
|
|
|
|
|
slReady.map((a) => a.botTokenSource ?? "none"),
|
|
|
|
|
);
|
|
|
|
|
const slAppSources = summarizeSources(
|
|
|
|
|
slReady.map((a) => a.appTokenSource ?? "none"),
|
|
|
|
|
);
|
|
|
|
|
rows.push({
|
|
|
|
|
provider: "Slack",
|
|
|
|
|
enabled: slEnabled,
|
|
|
|
|
configured: slEnabled && slConfigured,
|
|
|
|
|
state: !slEnabled
|
|
|
|
|
? "off"
|
|
|
|
|
: slPartial.length > 0
|
|
|
|
|
? "warn"
|
|
|
|
|
: slReady.length > 0
|
|
|
|
|
? "ok"
|
|
|
|
|
: "setup",
|
|
|
|
|
detail: slEnabled
|
|
|
|
|
? slConfigured
|
|
|
|
|
? `accounts ${slAccounts.filter((a) => a.botToken?.trim() && a.appToken?.trim()).length}`
|
|
|
|
|
: "not configured"
|
|
|
|
|
? slPartial.length > 0
|
|
|
|
|
? `partial tokens (need bot+app) · accounts ${slPartial.length}`
|
|
|
|
|
: slReady.length > 0
|
|
|
|
|
? `tokens ok (bot ${slBotSources.label}, app ${slAppSources.label}) · accounts ${slReady.length}/${slEnabledAccounts.length || 1}`
|
|
|
|
|
: slHasAnyToken
|
|
|
|
|
? "tokens incomplete (need bot+app)"
|
|
|
|
|
: "no tokens (SLACK_BOT_TOKEN + SLACK_APP_TOKEN)"
|
|
|
|
|
: "disabled",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@ -161,15 +243,20 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
|
|
|
|
|
const siAccounts = listSignalAccountIds(cfg).map((accountId) =>
|
|
|
|
|
resolveSignalAccount({ cfg, accountId }),
|
|
|
|
|
);
|
|
|
|
|
const siConfigured = siAccounts.some((a) => a.configured);
|
|
|
|
|
const siEnabledAccounts = siAccounts.filter((a) => a.enabled);
|
|
|
|
|
const siConfiguredAccounts = siEnabledAccounts.filter((a) => a.configured);
|
|
|
|
|
rows.push({
|
|
|
|
|
provider: "Signal",
|
|
|
|
|
enabled: siEnabled,
|
|
|
|
|
configured: siEnabled && siConfigured,
|
|
|
|
|
state: !siEnabled
|
|
|
|
|
? "off"
|
|
|
|
|
: siConfiguredAccounts.length > 0
|
|
|
|
|
? "ok"
|
|
|
|
|
: "setup",
|
|
|
|
|
detail: siEnabled
|
|
|
|
|
? siConfigured
|
|
|
|
|
? `accounts ${siAccounts.filter((a) => a.configured).length}`
|
|
|
|
|
: "not configured"
|
|
|
|
|
? siConfiguredAccounts.length > 0
|
|
|
|
|
? `configured · accounts ${siConfiguredAccounts.length}/${siEnabledAccounts.length || 1}`
|
|
|
|
|
: "default config (no overrides)"
|
|
|
|
|
: "disabled",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@ -178,29 +265,55 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
|
|
|
|
|
const imAccounts = listIMessageAccountIds(cfg).map((accountId) =>
|
|
|
|
|
resolveIMessageAccount({ cfg, accountId }),
|
|
|
|
|
);
|
|
|
|
|
const imConfigured = imAccounts.some((a) => a.configured);
|
|
|
|
|
const imEnabledAccounts = imAccounts.filter((a) => a.enabled);
|
|
|
|
|
const imConfiguredAccounts = imEnabledAccounts.filter((a) => a.configured);
|
|
|
|
|
rows.push({
|
|
|
|
|
provider: "iMessage",
|
|
|
|
|
enabled: imEnabled,
|
|
|
|
|
configured: imEnabled && imConfigured,
|
|
|
|
|
state: !imEnabled
|
|
|
|
|
? "off"
|
|
|
|
|
: imConfiguredAccounts.length > 0
|
|
|
|
|
? "ok"
|
|
|
|
|
: "setup",
|
|
|
|
|
detail: imEnabled
|
|
|
|
|
? imConfigured
|
|
|
|
|
? `accounts ${imAccounts.length}`
|
|
|
|
|
: "not configured"
|
|
|
|
|
? imConfiguredAccounts.length > 0
|
|
|
|
|
? `configured · accounts ${imConfiguredAccounts.length}/${imEnabledAccounts.length || 1}`
|
|
|
|
|
: "default config (no overrides)"
|
|
|
|
|
: "disabled",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// MS Teams
|
|
|
|
|
const msEnabled = cfg.msteams?.enabled !== false;
|
|
|
|
|
const msConfigured = Boolean(resolveMSTeamsCredentials(cfg.msteams));
|
|
|
|
|
const msCreds = resolveMSTeamsCredentials(cfg.msteams);
|
|
|
|
|
const msAppId =
|
|
|
|
|
cfg.msteams?.appId?.trim() || process.env.MSTEAMS_APP_ID?.trim();
|
|
|
|
|
const msAppPassword =
|
|
|
|
|
cfg.msteams?.appPassword?.trim() ||
|
|
|
|
|
process.env.MSTEAMS_APP_PASSWORD?.trim();
|
|
|
|
|
const msTenantId =
|
|
|
|
|
cfg.msteams?.tenantId?.trim() || process.env.MSTEAMS_TENANT_ID?.trim();
|
|
|
|
|
const msMissing = [
|
|
|
|
|
!msAppId ? "appId" : null,
|
|
|
|
|
!msAppPassword ? "appPassword" : null,
|
|
|
|
|
!msTenantId ? "tenantId" : null,
|
|
|
|
|
].filter(Boolean) as string[];
|
|
|
|
|
const msAnyPresent = Boolean(msAppId || msAppPassword || msTenantId);
|
|
|
|
|
rows.push({
|
|
|
|
|
provider: "MS Teams",
|
|
|
|
|
enabled: msEnabled,
|
|
|
|
|
configured: msEnabled && msConfigured,
|
|
|
|
|
state: !msEnabled
|
|
|
|
|
? "off"
|
|
|
|
|
: msCreds
|
|
|
|
|
? "ok"
|
|
|
|
|
: msAnyPresent
|
|
|
|
|
? "warn"
|
|
|
|
|
: "setup",
|
|
|
|
|
detail: msEnabled
|
|
|
|
|
? msConfigured
|
|
|
|
|
? "credentials present"
|
|
|
|
|
: "not configured"
|
|
|
|
|
? msCreds
|
|
|
|
|
? "credentials set"
|
|
|
|
|
: msAnyPresent
|
|
|
|
|
? `credentials incomplete (missing ${msMissing.join(", ")})`
|
|
|
|
|
: "no credentials (MSTEAMS_APP_ID / _PASSWORD / _TENANT_ID)"
|
|
|
|
|
: "disabled",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|