import type { ClawdbotConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./account-id.js"; import type { QiaoqiaoConfig, QiaoqiaoAccountConfig, QiaoqiaoDomain, ResolvedQiaoqiaoAccount } from "./types.js"; function normalizeQiaoqiaoApiBase(rawBase?: string): string { const raw = String(rawBase || "").trim(); if (!raw) return "https://qiaoqiao.social/api"; try { const url = new URL(raw); const normalizedPath = url.pathname.replace(/\/+$/, ""); if (!normalizedPath) { url.pathname = "/api"; } else { url.pathname = normalizedPath; } return url.toString().replace(/\/+$/, ""); } catch { return raw.replace(/\/+$/, ""); } } /** * List all configured account IDs from the accounts field. */ function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] { const accounts = (cfg.channels?.qiaoqiao as QiaoqiaoConfig)?.accounts; if (!accounts || typeof accounts !== "object") { return []; } return [...new Set( Object.keys(accounts) .filter((accountId) => accountId.trim()) .map((accountId) => normalizeAccountId(accountId)), )]; } /** * List all Qiaoqiao account IDs. * If no accounts are configured, returns [DEFAULT_ACCOUNT_ID] for backward compatibility. */ export function listQiaoqiaoAccountIds(cfg: ClawdbotConfig): string[] { const ids = listConfiguredAccountIds(cfg); if (ids.length === 0) { // Backward compatibility: no accounts configured, use default return [DEFAULT_ACCOUNT_ID]; } return [...ids].sort((a, b) => a.localeCompare(b)); } /** * Resolve the default account ID. */ export function resolveDefaultQiaoqiaoAccountId(cfg: ClawdbotConfig): string { const ids = listQiaoqiaoAccountIds(cfg); if (ids.includes(DEFAULT_ACCOUNT_ID)) { return DEFAULT_ACCOUNT_ID; } return ids[0] ?? DEFAULT_ACCOUNT_ID; } /** * Resolve the configured account key for a normalized account id. * Preserves the original config key casing so write paths can avoid shadow entries. */ export function resolveConfiguredQiaoqiaoAccountKey( cfg: ClawdbotConfig, accountId: string, ): string | undefined { const accounts = (cfg.channels?.qiaoqiao as QiaoqiaoConfig)?.accounts; if (!accounts || typeof accounts !== "object") { return undefined; } if (Object.prototype.hasOwnProperty.call(accounts, accountId)) { return accountId; } const normalizedId = normalizeAccountId(accountId); return Object.keys(accounts).find((key) => normalizeAccountId(key) === normalizedId); } /** * Get the raw account-specific config. * Preserves mixed-case config keys by resolving through the normalized account id. */ function resolveAccountConfig( cfg: ClawdbotConfig, accountId: string, ): QiaoqiaoAccountConfig | undefined { const accounts = (cfg.channels?.qiaoqiao as QiaoqiaoConfig)?.accounts; const matchedKey = resolveConfiguredQiaoqiaoAccountKey(cfg, accountId); return matchedKey && accounts ? accounts[matchedKey] : undefined; } /** * Merge top-level config with account-specific config. * Account-specific fields override top-level fields. */ function mergeQiaoqiaoAccountConfig( cfg: ClawdbotConfig, accountId: string, ): QiaoqiaoConfig { const qiaoqiaoCfg = cfg.channels?.qiaoqiao as QiaoqiaoConfig | undefined; // Extract base config (exclude accounts field to avoid recursion) const { accounts: _ignored, ...base } = qiaoqiaoCfg ?? {}; // Get account-specific overrides const account = resolveAccountConfig(cfg, accountId) ?? {}; // Merge: account config overrides base config return { ...base, ...account } as QiaoqiaoConfig; } /** * Resolve Qiaoqiao credentials from a config. */ export function resolveQiaoqiaoCredentials(cfg?: QiaoqiaoConfig): { appId: string; appSecret: string; apiBase: string; encryptKey?: string; verificationToken?: string; domain: QiaoqiaoDomain; } | null { const appId = cfg?.appId?.trim() || process.env.QIAOQIAO_APP_ID?.trim(); const appSecret = cfg?.appSecret?.trim() || process.env.QIAOQIAO_APP_SECRET?.trim(); const apiBase = normalizeQiaoqiaoApiBase( cfg?.apiBase?.trim() || process.env.QIAOQIAO_API_BASE?.trim() || "https://qiaoqiao.social/api", ); if (!appId || !appSecret) return null; return { appId, appSecret, apiBase, encryptKey: cfg?.encryptKey?.trim() || undefined, verificationToken: cfg?.verificationToken?.trim() || undefined, domain: cfg?.domain ?? "qiaoqiao", }; } /** * Resolve a complete Qiaoqiao account with merged config. */ export function resolveQiaoqiaoAccount(params: { cfg: ClawdbotConfig; accountId?: string | null; }): ResolvedQiaoqiaoAccount { const accountId = normalizeAccountId(params.accountId); const qiaoqiaoCfg = params.cfg.channels?.qiaoqiao as QiaoqiaoConfig | undefined; // Base enabled state (top-level) const baseEnabled = qiaoqiaoCfg?.enabled !== false; // Merge configs const merged = mergeQiaoqiaoAccountConfig(params.cfg, accountId); // Account-level enabled state const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; // Resolve credentials from merged config const creds = resolveQiaoqiaoCredentials(merged); return { accountId, enabled, configured: Boolean(creds), name: (merged as QiaoqiaoAccountConfig).name?.trim() || undefined, appId: creds?.appId, appSecret: creds?.appSecret, encryptKey: creds?.encryptKey, verificationToken: creds?.verificationToken, domain: creds?.domain ?? "qiaoqiao", config: merged, }; } /** * List all enabled and configured accounts. */ export function listEnabledQiaoqiaoAccounts(cfg: ClawdbotConfig): ResolvedQiaoqiaoAccount[] { return listQiaoqiaoAccountIds(cfg) .map((accountId) => resolveQiaoqiaoAccount({ cfg, accountId })) .filter((account) => account.enabled && account.configured); }