feat(portable): 虾盘云插上即用 — 自动开户送试用额度 + Config 首选卡片
解决「插上 U 盘选虾盘云模型却 Invalid token」:之前客户端只生成 sk-uc-指纹 key 写进配置,但后台从没注册该 token。 - xiapan-client.mjs 加 provisionApiKey():把设备指纹送到虾盘云后台 (POST api.u-claw.org/internal/token/provision) 自动开一个带试用额度 的 token,返回真正可用的 key。 - bootstrap-xiapan.mjs 改:首启(配置无 cloud key 时)调 provision 拿真 key 写配置;已有 key 则幂等跳过;provision 失败回退指纹 key 不阻塞启动。 后端按 deviceId 幂等,刷不出额度。 - Config.html:模型网格加「虾盘云」首选卡片(一个 key 用全部模型,无需 申请,选中直接启动跳过填 Key);更新过时型号(MiniMax-M2/kimi-k2/ qwen-plus/doubao-seed-1.6/gpt-5.4/claude-opus-4-6/deepseek-v4); banner 文案改为「已含试用额度」。 注:开户/送额度的商业逻辑在闭源服务端(token-key-service),开源仓只放 调用机制。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -181,55 +181,60 @@ input:focus { border-color: #ff6b35; }
|
|||||||
<!-- Step 1: Select Model -->
|
<!-- Step 1: Select Model -->
|
||||||
<div class="section active" id="step1">
|
<div class="section active" id="step1">
|
||||||
<h2>选择 AI 模型</h2>
|
<h2>选择 AI 模型</h2>
|
||||||
<p class="desc">选择你要使用的模型,推荐国内用户用 MiniMax / Kimi / DeepSeek</p>
|
<p class="desc">最省心:直接用「虾盘云」一个 Key 用全部模型,插上即用、已含试用额度。也可用下面各家自己的 Key。</p>
|
||||||
|
|
||||||
<div class="model-grid">
|
<div class="model-grid">
|
||||||
<div class="model-card" data-provider="minimax" data-base="https://api.minimax.chat/v1" data-model="MiniMax-Text-01">
|
<div class="model-card" data-provider="uclaw-cloud" data-base="https://api.u-claw.org/v1" data-model="deepseek-v4-flash" data-zerokey="1">
|
||||||
<span class="check">✓</span>
|
<span class="check">✓</span>
|
||||||
<h4>MiniMax <span class="tag hot">推荐</span><span class="tag cn">国内</span></h4>
|
<h4>虾盘云 <span class="tag hot">首选</span><span class="tag free">含试用额度</span></h4>
|
||||||
<p>MiniMax-Text-01,速度快,性价比高</p>
|
<p>插上即用,无需申请 Key。一个账号用 DeepSeek / Claude / GPT / 通义等全部模型</p>
|
||||||
<a class="buy-link" href="https://platform.minimaxi.com/" target="_blank">→ 获取 API Key</a>
|
|
||||||
</div>
|
|
||||||
<div class="model-card" data-provider="kimi" data-base="https://api.moonshot.cn/v1" data-model="moonshot-v1-auto">
|
|
||||||
<span class="check">✓</span>
|
|
||||||
<h4>Kimi (月之暗面) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
|
||||||
<p>moonshot-v1-auto,智能选模型</p>
|
|
||||||
<a class="buy-link" href="https://platform.moonshot.cn/" target="_blank">→ 获取 API Key</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="model-card" data-provider="deepseek" data-base="https://api.deepseek.com/v1" data-model="deepseek-chat">
|
<div class="model-card" data-provider="deepseek" data-base="https://api.deepseek.com/v1" data-model="deepseek-chat">
|
||||||
<span class="check">✓</span>
|
<span class="check">✓</span>
|
||||||
<h4>DeepSeek <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
|
<h4>DeepSeek <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
|
||||||
<p>deepseek-chat,超低价格</p>
|
<p>deepseek-chat / deepseek-v4,超低价格</p>
|
||||||
<a class="buy-link" href="https://platform.deepseek.com/" target="_blank">→ 获取 API Key</a>
|
<a class="buy-link" href="https://platform.deepseek.com/" target="_blank">→ 获取 API Key</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="model-card" data-provider="minimax" data-base="https://api.minimax.chat/v1" data-model="MiniMax-M2">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>MiniMax <span class="tag cn">国内</span></h4>
|
||||||
|
<p>MiniMax-M2,速度快,性价比高</p>
|
||||||
|
<a class="buy-link" href="https://platform.minimaxi.com/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
|
<div class="model-card" data-provider="kimi" data-base="https://api.moonshot.cn/v1" data-model="kimi-k2-turbo-preview">
|
||||||
|
<span class="check">✓</span>
|
||||||
|
<h4>Kimi (月之暗面) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||||||
|
<p>kimi-k2,长上下文强</p>
|
||||||
|
<a class="buy-link" href="https://platform.moonshot.cn/" target="_blank">→ 获取 API Key</a>
|
||||||
|
</div>
|
||||||
<div class="model-card" data-provider="zai" data-base="" data-model="glm-5">
|
<div class="model-card" data-provider="zai" data-base="" data-model="glm-5">
|
||||||
<span class="check">✓</span>
|
<span class="check">✓</span>
|
||||||
<h4>智谱 GLM <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
<h4>智谱 GLM <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
||||||
<p>glm-5(zai provider),免费额度可用</p>
|
<p>glm-5(zai provider),免费额度可用</p>
|
||||||
<a class="buy-link" href="https://open.bigmodel.cn/" target="_blank">→ 获取 API Key</a>
|
<a class="buy-link" href="https://open.bigmodel.cn/" target="_blank">→ 获取 API Key</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="model-card" data-provider="qwen" data-base="https://dashscope.aliyuncs.com/compatible-mode/v1" data-model="qwen-turbo">
|
<div class="model-card" data-provider="qwen" data-base="https://dashscope.aliyuncs.com/compatible-mode/v1" data-model="qwen-plus">
|
||||||
<span class="check">✓</span>
|
<span class="check">✓</span>
|
||||||
<h4>通义千问 <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
<h4>通义千问 <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
||||||
<p>qwen-turbo,阿里云出品</p>
|
<p>qwen-plus / qwen3-max,阿里云出品</p>
|
||||||
<a class="buy-link" href="https://dashscope.console.aliyun.com/" target="_blank">→ 获取 API Key</a>
|
<a class="buy-link" href="https://dashscope.console.aliyun.com/" target="_blank">→ 获取 API Key</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="model-card" data-provider="doubao" data-base="https://ark.cn-beijing.volces.com/api/v3" data-model="doubao-1.5-pro-32k">
|
<div class="model-card" data-provider="doubao" data-base="https://ark.cn-beijing.volces.com/api/v3" data-model="doubao-seed-1-6-250615">
|
||||||
<span class="check">✓</span>
|
<span class="check">✓</span>
|
||||||
<h4>豆包 (字节) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
<h4>豆包 (字节) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||||||
<p>doubao-1.5-pro,字节跳动</p>
|
<p>doubao-seed-1.6,字节跳动</p>
|
||||||
<a class="buy-link" href="https://console.volcengine.com/ark" target="_blank">→ 获取 API Key</a>
|
<a class="buy-link" href="https://console.volcengine.com/ark" target="_blank">→ 获取 API Key</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="model-card" data-provider="openai" data-base="https://api.openai.com/v1" data-model="gpt-4o">
|
<div class="model-card" data-provider="openai" data-base="https://api.openai.com/v1" data-model="gpt-5.4">
|
||||||
<span class="check">✓</span>
|
<span class="check">✓</span>
|
||||||
<h4>OpenAI <span class="tag hot">强</span></h4>
|
<h4>OpenAI <span class="tag hot">强</span></h4>
|
||||||
<p>GPT-4o,需翻墙或中转</p>
|
<p>GPT-5.4,需翻墙或中转</p>
|
||||||
<a class="buy-link" href="https://platform.openai.com/" target="_blank">→ 获取 API Key</a>
|
<a class="buy-link" href="https://platform.openai.com/" target="_blank">→ 获取 API Key</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="model-card" data-provider="anthropic" data-base="https://api.anthropic.com/v1" data-model="claude-sonnet-4-20250514">
|
<div class="model-card" data-provider="anthropic" data-base="https://api.anthropic.com/v1" data-model="claude-opus-4-6">
|
||||||
<span class="check">✓</span>
|
<span class="check">✓</span>
|
||||||
<h4>Claude <span class="tag hot">强</span></h4>
|
<h4>Claude <span class="tag hot">强</span></h4>
|
||||||
<p>Claude Sonnet 4,需翻墙或中转</p>
|
<p>Claude Opus 4.6 / Sonnet 4.6,需翻墙或中转</p>
|
||||||
<a class="buy-link" href="https://console.anthropic.com/" target="_blank">→ 获取 API Key</a>
|
<a class="buy-link" href="https://console.anthropic.com/" target="_blank">→ 获取 API Key</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="model-card" data-provider="groq" data-base="https://api.groq.com/openai/v1" data-model="llama-3.3-70b-versatile">
|
<div class="model-card" data-provider="groq" data-base="https://api.groq.com/openai/v1" data-model="llama-3.3-70b-versatile">
|
||||||
@@ -427,8 +432,8 @@ async function loadXiapanStatus() {
|
|||||||
const tokens = (bal.remainingTokens || 0).toLocaleString();
|
const tokens = (bal.remainingTokens || 0).toLocaleString();
|
||||||
document.getElementById('xpBalance').textContent = `$${usd} (${tokens} tokens)`;
|
document.getElementById('xpBalance').textContent = `$${usd} (${tokens} tokens)`;
|
||||||
document.getElementById('xpHint').textContent = bal.remainingUsd > 0
|
document.getElementById('xpHint').textContent = bal.remainingUsd > 0
|
||||||
? '可直接使用 deepseek-chat / qwen-plus / qwen-turbo'
|
? '已含试用额度,可直接用 deepseek-v4 / claude / gpt / 通义 等全部模型'
|
||||||
: '余额为 0,点击右侧前往充值';
|
: '试用额度已用完,点击右侧充值即可继续,秒到账';
|
||||||
} else if (bal.reason && bal.reason.includes('401')) {
|
} else if (bal.reason && bal.reason.includes('401')) {
|
||||||
document.getElementById('xpBalance').textContent = '未注册';
|
document.getElementById('xpBalance').textContent = '未注册';
|
||||||
document.getElementById('xpHint').textContent = '首次使用:点「前往充值」即可在虾盘云页面注册并充值,秒到账';
|
document.getElementById('xpHint').textContent = '首次使用:点「前往充值」即可在虾盘云页面注册并充值,秒到账';
|
||||||
@@ -456,6 +461,7 @@ loadXiapanStatus();
|
|||||||
let selectedProvider = null;
|
let selectedProvider = null;
|
||||||
let selectedBase = '';
|
let selectedBase = '';
|
||||||
let selectedModel = '';
|
let selectedModel = '';
|
||||||
|
let selectedZeroKey = false; // 虾盘云等已自动开户、无需用户填 Key 的 provider
|
||||||
|
|
||||||
// --- Model cards ---
|
// --- Model cards ---
|
||||||
document.querySelectorAll('#step1 .model-card').forEach(card => {
|
document.querySelectorAll('#step1 .model-card').forEach(card => {
|
||||||
@@ -466,11 +472,22 @@ document.querySelectorAll('#step1 .model-card').forEach(card => {
|
|||||||
selectedProvider = card.dataset.provider;
|
selectedProvider = card.dataset.provider;
|
||||||
selectedBase = card.dataset.base;
|
selectedBase = card.dataset.base;
|
||||||
selectedModel = card.dataset.model;
|
selectedModel = card.dataset.model;
|
||||||
document.getElementById('nextStep1').disabled = false;
|
selectedZeroKey = card.dataset.zerokey === '1';
|
||||||
|
const nextBtn = document.getElementById('nextStep1');
|
||||||
|
nextBtn.disabled = false;
|
||||||
|
// 虾盘云已自动开户,跳过填 Key,按钮直接变「启动」
|
||||||
|
nextBtn.textContent = selectedZeroKey ? '直接启动 🚀' : '下一步 →';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('nextStep1').addEventListener('click', () => goStep(2));
|
document.getElementById('nextStep1').addEventListener('click', () => {
|
||||||
|
if (selectedZeroKey) {
|
||||||
|
// 虾盘云:已有自动开户的 key(在 uclaw-cloud provider 里),无需填,直接保存启动
|
||||||
|
saveZeroKeyConfig();
|
||||||
|
} else {
|
||||||
|
goStep(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- Steps ---
|
// --- Steps ---
|
||||||
function goStep(n) {
|
function goStep(n) {
|
||||||
@@ -568,6 +585,42 @@ function buildOpenClawConfig(provider, base, model, apiKey) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 虾盘云:已自动开户,无需填 Key。只把默认模型指回 uclaw-cloud 并保存 ---
|
||||||
|
async function saveZeroKeyConfig() {
|
||||||
|
try {
|
||||||
|
// 读现有配置(bootstrap 已写入 uclaw-cloud provider + 自动开户的 key)
|
||||||
|
const cur = await fetch('/api/config').then(r => r.ok ? r.json() : {}).catch(() => ({}));
|
||||||
|
const config = (cur && typeof cur === 'object') ? cur : {};
|
||||||
|
|
||||||
|
// 确保 uclaw-cloud provider 存在;若缺(极少数 bootstrap 失败),提示去充值页
|
||||||
|
const hasCloud = config.models && config.models.providers && config.models.providers['uclaw-cloud'] && config.models.providers['uclaw-cloud'].apiKey;
|
||||||
|
if (!hasCloud) {
|
||||||
|
showToast('虾盘云尚未就绪,请重启 U-Claw 或检查网络后重试', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把默认模型指回虾盘云
|
||||||
|
config.agents = config.agents || {};
|
||||||
|
config.agents.defaults = config.agents.defaults || {};
|
||||||
|
config.agents.defaults.model = config.agents.defaults.model || {};
|
||||||
|
config.agents.defaults.model.primary = 'uclaw-cloud/' + (selectedModel || 'deepseek-v4-flash');
|
||||||
|
|
||||||
|
const res = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('保存失败');
|
||||||
|
|
||||||
|
showToast('已选择虾盘云,启动中…');
|
||||||
|
goStep(3);
|
||||||
|
const port = getPort();
|
||||||
|
document.getElementById('gatewayUrl').textContent = 'http://127.0.0.1:' + port;
|
||||||
|
} catch (e) {
|
||||||
|
showToast('保存失败: ' + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Save config ---
|
// --- Save config ---
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
const apiKey = document.getElementById('apiKey').value.trim();
|
const apiKey = document.getElementById('apiKey').value.trim();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { getFingerprint } from './fingerprint.mjs';
|
import { getFingerprint } from './fingerprint.mjs';
|
||||||
import { buildApiKey } from './xiapan-client.mjs';
|
import { buildApiKey, provisionApiKey } from './xiapan-client.mjs';
|
||||||
|
|
||||||
const PROVIDER_ID = 'uclaw-cloud';
|
const PROVIDER_ID = 'uclaw-cloud';
|
||||||
|
|
||||||
@@ -95,41 +95,42 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = {
|
|||||||
return { ok: false, reason: 'fingerprint-failed' };
|
return { ok: false, reason: 'fingerprint-failed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = buildApiKey(fingerprintInfo.fingerprint);
|
|
||||||
|
|
||||||
const config = readJsonSafe(configPath) || { gateway: { mode: 'local', auth: { token: 'uclaw' } } };
|
const config = readJsonSafe(configPath) || { gateway: { mode: 'local', auth: { token: 'uclaw' } } };
|
||||||
const providers = ensureModelsContainer(config);
|
const providers = ensureModelsContainer(config);
|
||||||
const existing = providers[PROVIDER_ID];
|
const existing = providers[PROVIDER_ID];
|
||||||
|
|
||||||
if (existing && typeof existing === 'object') {
|
// 幂等:配置里已有 uclaw-cloud apiKey → 保持不动,不再打网络 provision。
|
||||||
if (existing.apiKey && existing.apiKey !== apiKey) {
|
// (后端 provision 本身也按 deviceId 幂等,但客户端先短路可避免每次启动都请求。)
|
||||||
log.info?.(
|
if (existing && typeof existing === 'object' && existing.apiKey) {
|
||||||
`[bootstrap-xiapan] uclaw-cloud apiKey already configured (different fingerprint). `
|
const changedDefaults = ensureAgentsDefaults(config);
|
||||||
+ `Current source=${fingerprintInfo.source}. Use Config UI to rebind if needed.`,
|
if (changedDefaults) {
|
||||||
);
|
writeJson(configPath, config);
|
||||||
// Still ensure agents.defaults exists so the runtime knows to use uclaw-cloud
|
log.info?.('[bootstrap-xiapan] Added agents.defaults to existing config');
|
||||||
const changedDefaults = ensureAgentsDefaults(config);
|
|
||||||
if (changedDefaults) writeJson(configPath, config);
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
action: 'kept',
|
|
||||||
source: fingerprintInfo.source,
|
|
||||||
apiKey: existing.apiKey,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (existing.apiKey === apiKey) {
|
return {
|
||||||
const changedDefaults = ensureAgentsDefaults(config);
|
ok: true,
|
||||||
if (changedDefaults) {
|
action: changedDefaults ? 'agents-defaults-added' : 'noop',
|
||||||
writeJson(configPath, config);
|
source: fingerprintInfo.source,
|
||||||
log.info?.('[bootstrap-xiapan] Added agents.defaults to existing config');
|
apiKey: existing.apiKey,
|
||||||
}
|
};
|
||||||
return {
|
}
|
||||||
ok: true,
|
|
||||||
action: changedDefaults ? 'agents-defaults-added' : 'noop',
|
// 首启(配置里还没有 cloud key):静默开户。把指纹送到虾盘云后台自动开一个带
|
||||||
source: fingerprintInfo.source,
|
// 试用额度的 token。成功 → 用后端真正注册过的 key(可直接聊天,解决 Invalid token)。
|
||||||
apiKey,
|
// 失败(离线/后端不可达)→ 回退到指纹派生 key,至少配置不空、不阻塞启动。
|
||||||
};
|
let apiKey = buildApiKey(fingerprintInfo.fingerprint);
|
||||||
|
let provisioned = false;
|
||||||
|
try {
|
||||||
|
const prov = await provisionApiKey(fingerprintInfo.fingerprint, { source: 'uclaw-portable' });
|
||||||
|
if (prov.ok && prov.apiKey) {
|
||||||
|
apiKey = prov.apiKey;
|
||||||
|
provisioned = true;
|
||||||
|
log.info?.(`[bootstrap-xiapan] Provisioned cloud token (reused=${prov.reused})`);
|
||||||
|
} else {
|
||||||
|
log.info?.(`[bootstrap-xiapan] Provision skipped (${prov.reason}); using fingerprint key`);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.info?.(`[bootstrap-xiapan] Provision failed (${err.message}); using fingerprint key`);
|
||||||
}
|
}
|
||||||
|
|
||||||
providers[PROVIDER_ID] = {
|
providers[PROVIDER_ID] = {
|
||||||
@@ -144,11 +145,13 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = {
|
|||||||
|
|
||||||
writeJson(configPath, config);
|
writeJson(configPath, config);
|
||||||
log.info?.(
|
log.info?.(
|
||||||
`[bootstrap-xiapan] Wrote uclaw-cloud provider (source=${fingerprintInfo.source}, key=${apiKey.slice(0, 12)}…)`,
|
`[bootstrap-xiapan] Wrote uclaw-cloud provider (source=${fingerprintInfo.source}, `
|
||||||
|
+ `provisioned=${provisioned}, key=${apiKey.slice(0, 12)}…)`,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: existing ? 'updated' : 'created',
|
action: 'created',
|
||||||
|
provisioned,
|
||||||
source: fingerprintInfo.source,
|
source: fingerprintInfo.source,
|
||||||
apiKey,
|
apiKey,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,3 +89,30 @@ export function getRechargeUrl(apiKey) {
|
|||||||
// Page already has #recharge anchor; preserve it
|
// Page already has #recharge anchor; preserve it
|
||||||
return `${url.toString()}#recharge`;
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user