// 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`; } // 首启静默开户:把设备指纹送到虾盘云后台,自动创建一个 token 并附带试用额度。 // 后端按 deviceId 幂等(同一台机器永远拿同一个 token,刷不出额度),不重启容器。 // 返回 { ok, apiKey, reused } —— apiKey 是后端真正注册过的 key(可直接聊天)。 // 失败时返回 { ok:false, reason },调用方应回退到指纹派生 key(至少配置不空)。 export async function provisionApiKey(fingerprint, { source = 'uclaw-portable' } = {}) { if (!fingerprint || !/^[0-9a-f]{64}$/i.test(fingerprint)) { return { ok: false, reason: 'bad-fingerprint' }; } const base = getApiBase(); // https://api.u-claw.org/v1 const root = base.replace(/\/v1$/, ''); // https://api.u-claw.org const url = `${root}/internal/token/provision`; try { const res = await fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deviceId: fingerprint.toLowerCase(), source }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.success || !data.data || !data.data.apiKey) { return { ok: false, reason: data.message || `http-${res.status}` }; } return { ok: true, apiKey: data.data.apiKey, reused: !!data.reused }; } catch (err) { return { ok: false, reason: err.message }; } }