// Xiapan Cloud (虾盘云) client for U-Claw open-source edition. // Provides only: apiKey derivation, balance lookup, recharge URL. // Intentionally does NOT call /recharge/activate — open-source users do not get free quota. const DEFAULT_API_BASE = 'https://api.u-claw.org/v1'; const DEFAULT_RECHARGE_PAGE = 'https://u-claw.org/cloud.html'; const QUOTA_PER_USD = 500_000; // 1 USD = 500k tokens (matches new-api convention) const REQUEST_TIMEOUT_MS = 10_000; // sk-uc- prefix marks keys generated by the u-claw open-source edition. // ClawX commercial keys use plain sk- and the cloud.html flow uses sk-xp-, // so the three namespaces never collide and the backend can audit by prefix. export function buildApiKey(fingerprint) { if (!fingerprint || !/^[0-9a-f]{64}$/i.test(fingerprint)) { throw new Error('Fingerprint must be 64-character hex.'); } return `sk-uc-${fingerprint.toLowerCase()}`; } function getApiBase() { return (process.env.UCLAW_CLOUD_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, ''); } function getRechargePage() { return process.env.UCLAW_CLOUD_RECHARGE_PAGE || DEFAULT_RECHARGE_PAGE; } async function fetchWithTimeout(url, init) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { return await fetch(url, { ...init, signal: controller.signal }); } finally { clearTimeout(timer); } } export async function getBalance(apiKey) { if (!apiKey) throw new Error('apiKey is required.'); const base = getApiBase(); const headers = { Authorization: `Bearer ${apiKey}` }; const [subRes, usageRes] = await Promise.all([ fetchWithTimeout(`${base}/dashboard/billing/subscription`, { headers }).catch(() => null), fetchWithTimeout( `${base}/dashboard/billing/usage?start_date=2020-01-01&end_date=${new Date().toISOString().slice(0, 10)}`, { headers }, ).catch(() => null), ]); if (!subRes || !subRes.ok) { return { ok: false, reason: subRes ? `subscription HTTP ${subRes.status}` : 'subscription request failed', hardLimitUsd: 0, usedUsd: 0, remainingUsd: 0, remainingTokens: 0, }; } const subscription = await subRes.json().catch(() => ({})); let usedUsd = 0; if (usageRes && usageRes.ok) { const usage = await usageRes.json().catch(() => ({})); // total_usage is in cents (USD * 100), per new-api convention usedUsd = Number(usage.total_usage || 0) / 100; } const hardLimitUsd = Number(subscription.hard_limit_usd || 0); const remainingUsd = Math.max(0, hardLimitUsd - usedUsd); const remainingTokens = Math.round(remainingUsd * QUOTA_PER_USD); return { ok: true, reason: null, hardLimitUsd, usedUsd, remainingUsd, remainingTokens, }; } export function getRechargeUrl(apiKey) { if (!apiKey) throw new Error('apiKey is required.'); const page = getRechargePage(); const url = new URL(page); url.searchParams.set('key', apiKey); // Page already has #recharge anchor; preserve it return `${url.toString()}#recharge`; }