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:
hfshfg
2026-06-15 16:01:35 +08:00
parent 2d0f33d5a0
commit b1468d455c
3 changed files with 139 additions and 56 deletions

View File

@@ -89,3 +89,30 @@ export function getRechargeUrl(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 };
}
}