/// /** * Qiaoqiao (敲敲) backend agent authentication. * * When the clawdbot-qiaoqiao plugin is running with Qiaoqiao credentials, * this module verifies the agent identity against the Qiaoqiao backend * before allowing messages to be processed. */ export type QiaoqiaoAgentInfo = { agentId: string; appId: string; username: string; }; export type QiaoqiaoAuthConfig = { appId: string; appSecret: string; apiBase?: string; }; const DEFAULT_QIAOQIAO_API_BASE = "https://qiaoqiao.social/api"; function resolveApiBase(preferred?: string): string { const raw = (preferred || DEFAULT_QIAOQIAO_API_BASE).trim(); if (!raw) return DEFAULT_QIAOQIAO_API_BASE; 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(/\/+$/, ""); } } function resolveAgentVerifyUrl(apiBase: string): string { const normalized = resolveApiBase(apiBase); if (normalized.endsWith("/api")) { return `${normalized}/ws/verify-agent`; } return `${normalized}/api/ws/verify-agent`; } /** * Normalize Qiaoqiao credentials from explicit config. * Returns null if credentials are incomplete. */ export function resolveQiaoqiaoCredentials(preferred?: Partial | null): QiaoqiaoAuthConfig | null { const appId = preferred?.appId?.trim(); const appSecret = preferred?.appSecret?.trim(); if (!appId || !appSecret) return null; return { appId, appSecret, apiBase: resolveApiBase(preferred?.apiBase), }; } /** * Verify Qiaoqiao agent credentials against the backend. * Returns agent info on success, throws on failure. */ export async function verifyQiaoqiaoAgent( config: QiaoqiaoAuthConfig, log: (msg: string) => void = console.log, ): Promise { const appId = config.appId?.trim(); const appSecret = config.appSecret?.trim(); if (!appId || !appSecret) { throw new Error("[qiaoqiao] Agent verification requires both appId and appSecret"); } const apiBase = resolveApiBase(config.apiBase); const url = resolveAgentVerifyUrl(apiBase); log(`[qiaoqiao] Verifying agent identity: appId=${appId} url=${url}`); const res = await fetch(url, { method: "GET", headers: { "X-App-ID": appId, "X-App-Secret": appSecret, "Content-Type": "application/json", }, }); if (!res.ok) { const body = await res.text().catch(() => ""); throw new Error( `[qiaoqiao] Agent verification failed (HTTP ${res.status}): ${body}`, ); } const data = (await res.json()) as { success: boolean; data?: QiaoqiaoAgentInfo; error?: string; }; if (!data.success || !data.data) { throw new Error(`[qiaoqiao] Agent verification rejected: ${data.error ?? "unknown error"}`); } if (data.data.appId !== appId) { throw new Error( `[qiaoqiao] Agent verification appId mismatch: expected=${appId}, actual=${data.data.appId}`, ); } log(`[qiaoqiao] Agent verified: ${data.data.username} (${data.data.agentId})`); return data.data; } /** * Notify the Qiaoqiao backend when a Qiaoqiao conversation message is processed. * This allows the backend to track conversations tied to this agent. */ export async function notifyQiaoqiaoMessage( config: QiaoqiaoAuthConfig, payload: { agentId: string; senderId: string; chatId: string; content: string; }, ): Promise { const { appId, appSecret } = config; const apiBase = resolveApiBase(config.apiBase); const url = `${apiBase}/agent/qiaoqiao-message`; try { await fetch(url, { method: "POST", headers: { "X-App-ID": appId, "X-App-Secret": appSecret, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); } catch { // Non-critical: just log, don't fail message processing } }