mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 18:36:08 +00:00
When no accountId was provided, resolveTelegramAccount and resolveTelegramToken resolved the implicit default account first. Any leftover TELEGRAM_BOT_TOKEN or top-level channels.telegram.botToken then shadowed the configured channels.telegram.defaultAccount and default-agent binding, so gateway sends, heartbeats, and message actions went out through the wrong bot and typically failed with chat not found in groups the stale bot never joined. Message actions could also fail with a missing-token error when only the named default account had a token. Resolve the configured default account id up front in both optional-account entry points, matching the nextcloud-talk resolver and createTelegramActionGate.
215 lines
7.2 KiB
TypeScript
215 lines
7.2 KiB
TypeScript
// Telegram plugin module implements token behavior.
|
|
import { resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-core";
|
|
import type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
|
|
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/channel-core";
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
import type { TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
|
|
import {
|
|
DEFAULT_ACCOUNT_ID,
|
|
normalizeAccountId,
|
|
normalizeOptionalAccountId,
|
|
} from "openclaw/plugin-sdk/routing";
|
|
import {
|
|
normalizeSecretInputString,
|
|
resolveSecretInputString,
|
|
} from "openclaw/plugin-sdk/secret-input";
|
|
import { resolveDefaultTelegramAccountId } from "./account-selection.js";
|
|
|
|
type TelegramTokenSource = "env" | "tokenFile" | "config" | "none";
|
|
|
|
export type TelegramTokenResolution = BaseTokenResolution & {
|
|
source: TelegramTokenSource;
|
|
};
|
|
|
|
type RuntimeTokenValueResolution =
|
|
| { status: "available"; value: string }
|
|
| { status: "configured_unavailable" }
|
|
| { status: "missing" };
|
|
|
|
function resolveEnvSecretRefValue(params: {
|
|
cfg?: Pick<OpenClawConfig, "secrets">;
|
|
provider: string;
|
|
id: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
}): string | undefined {
|
|
const providerConfig = params.cfg?.secrets?.providers?.[params.provider];
|
|
if (providerConfig) {
|
|
if (providerConfig.source !== "env") {
|
|
throw new Error(
|
|
`Secret provider "${params.provider}" has source "${providerConfig.source}" but ref requests "env".`,
|
|
);
|
|
}
|
|
if (providerConfig.allowlist && !providerConfig.allowlist.includes(params.id)) {
|
|
throw new Error(
|
|
`Environment variable "${params.id}" is not allowlisted in secrets.providers.${params.provider}.allowlist.`,
|
|
);
|
|
}
|
|
} else if (
|
|
params.provider !== resolveDefaultSecretProviderAlias({ secrets: params.cfg?.secrets }, "env")
|
|
) {
|
|
throw new Error(
|
|
`Secret provider "${params.provider}" is not configured (ref: env:${params.provider}:${params.id}).`,
|
|
);
|
|
}
|
|
return normalizeSecretInputString((params.env ?? process.env)[params.id]);
|
|
}
|
|
|
|
function resolveRuntimeTokenValue(params: {
|
|
cfg?: Pick<OpenClawConfig, "secrets">;
|
|
value: unknown;
|
|
path: string;
|
|
}): RuntimeTokenValueResolution {
|
|
const resolved = resolveSecretInputString({
|
|
value: params.value,
|
|
path: params.path,
|
|
defaults: params.cfg?.secrets?.defaults,
|
|
mode: "inspect",
|
|
});
|
|
if (resolved.status === "available") {
|
|
return {
|
|
status: "available",
|
|
value: resolved.value,
|
|
};
|
|
}
|
|
if (resolved.status === "missing") {
|
|
return { status: "missing" };
|
|
}
|
|
if (resolved.ref.source === "env") {
|
|
const envValue = resolveEnvSecretRefValue({
|
|
cfg: params.cfg,
|
|
provider: resolved.ref.provider,
|
|
id: resolved.ref.id,
|
|
});
|
|
if (envValue) {
|
|
return {
|
|
status: "available",
|
|
value: envValue,
|
|
};
|
|
}
|
|
return { status: "configured_unavailable" };
|
|
}
|
|
// Runtime resolution stays strict for non-env SecretRefs.
|
|
resolveSecretInputString({
|
|
value: params.value,
|
|
path: params.path,
|
|
defaults: params.cfg?.secrets?.defaults,
|
|
mode: "strict",
|
|
});
|
|
return { status: "configured_unavailable" };
|
|
}
|
|
|
|
type ResolveTelegramTokenOpts = {
|
|
envToken?: string | null;
|
|
accountId?: string | null;
|
|
logMissingFile?: (message: string) => void;
|
|
};
|
|
|
|
export function resolveTelegramToken(
|
|
cfg?: OpenClawConfig,
|
|
opts: ResolveTelegramTokenOpts = {},
|
|
): TelegramTokenResolution {
|
|
const requestedAccountId = normalizeOptionalAccountId(opts.accountId);
|
|
const accountId =
|
|
requestedAccountId ?? (cfg ? resolveDefaultTelegramAccountId(cfg) : DEFAULT_ACCOUNT_ID);
|
|
const telegramCfg = cfg?.channels?.telegram;
|
|
|
|
// Account IDs are normalized for routing (e.g. lowercased). Config keys may not
|
|
// be normalized, so resolve per-account config by matching normalized IDs.
|
|
const resolveAccountCfg = (id: string): TelegramAccountConfig | undefined => {
|
|
const accounts = telegramCfg?.accounts;
|
|
return Array.isArray(accounts)
|
|
? undefined
|
|
: resolveNormalizedAccountEntry(accounts, id, normalizeAccountId);
|
|
};
|
|
|
|
const accountCfg = resolveAccountCfg(
|
|
accountId !== DEFAULT_ACCOUNT_ID ? accountId : DEFAULT_ACCOUNT_ID,
|
|
);
|
|
|
|
// When a non-default accountId is explicitly specified but not found in config,
|
|
// decide whether to fall through to channel-level defaults based on whether
|
|
// the config has an explicit accounts section (multi-bot setup).
|
|
//
|
|
// Multi-bot: accounts section exists with entries → block fallthrough to prevent
|
|
// routing via the wrong bot's token.
|
|
//
|
|
// Single-bot: no accounts section (or empty) → allow fallthrough so that
|
|
// binding-created accountIds inherit the channel-level token.
|
|
// See: https://github.com/openclaw/openclaw/issues/53876
|
|
if (accountId !== DEFAULT_ACCOUNT_ID && !accountCfg) {
|
|
const accounts = telegramCfg?.accounts;
|
|
const hasConfiguredAccounts =
|
|
Boolean(accounts) &&
|
|
typeof accounts === "object" &&
|
|
!Array.isArray(accounts) &&
|
|
Object.keys(accounts).length > 0;
|
|
if (hasConfiguredAccounts) {
|
|
opts.logMissingFile?.(
|
|
`channels.telegram.accounts: unknown accountId "${accountId}" — not found in config, refusing channel-level fallback`,
|
|
);
|
|
return { token: "", source: "none" };
|
|
}
|
|
}
|
|
|
|
const accountTokenFile = accountCfg?.tokenFile?.trim();
|
|
if (accountTokenFile) {
|
|
const token = tryReadSecretFileSync(
|
|
accountTokenFile,
|
|
`channels.telegram.accounts.${accountId}.tokenFile`,
|
|
{ rejectSymlink: true },
|
|
);
|
|
if (token) {
|
|
return { token, source: "tokenFile" };
|
|
}
|
|
opts.logMissingFile?.(
|
|
`channels.telegram.accounts.${accountId}.tokenFile not found or unreadable: ${accountTokenFile}`,
|
|
);
|
|
return { token: "", source: "none" };
|
|
}
|
|
|
|
const accountToken = resolveRuntimeTokenValue({
|
|
cfg,
|
|
value: accountCfg?.botToken,
|
|
path: `channels.telegram.accounts.${accountId}.botToken`,
|
|
});
|
|
if (accountToken.status === "available") {
|
|
return { token: accountToken.value, source: "config" };
|
|
}
|
|
if (accountToken.status === "configured_unavailable") {
|
|
return { token: "", source: "none" };
|
|
}
|
|
|
|
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
|
const tokenFile = telegramCfg?.tokenFile?.trim();
|
|
if (tokenFile) {
|
|
const token = tryReadSecretFileSync(tokenFile, "channels.telegram.tokenFile", {
|
|
rejectSymlink: true,
|
|
});
|
|
if (token) {
|
|
return { token, source: "tokenFile" };
|
|
}
|
|
opts.logMissingFile?.(`channels.telegram.tokenFile not found or unreadable: ${tokenFile}`);
|
|
return { token: "", source: "none" };
|
|
}
|
|
|
|
const configToken = resolveRuntimeTokenValue({
|
|
cfg,
|
|
value: telegramCfg?.botToken,
|
|
path: "channels.telegram.botToken",
|
|
});
|
|
if (configToken.status === "available") {
|
|
return { token: configToken.value, source: "config" };
|
|
}
|
|
if (configToken.status === "configured_unavailable") {
|
|
return { token: "", source: "none" };
|
|
}
|
|
|
|
const envToken = allowEnv ? (opts.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim() : "";
|
|
if (envToken) {
|
|
return { token: envToken, source: "env" };
|
|
}
|
|
|
|
return { token: "", source: "none" };
|
|
}
|