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:
@@ -8,7 +8,7 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { getFingerprint } from './fingerprint.mjs';
|
||||
import { buildApiKey } from './xiapan-client.mjs';
|
||||
import { buildApiKey, provisionApiKey } from './xiapan-client.mjs';
|
||||
|
||||
const PROVIDER_ID = 'uclaw-cloud';
|
||||
|
||||
@@ -95,41 +95,42 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = {
|
||||
return { ok: false, reason: 'fingerprint-failed' };
|
||||
}
|
||||
|
||||
const apiKey = buildApiKey(fingerprintInfo.fingerprint);
|
||||
|
||||
const config = readJsonSafe(configPath) || { gateway: { mode: 'local', auth: { token: 'uclaw' } } };
|
||||
const providers = ensureModelsContainer(config);
|
||||
const existing = providers[PROVIDER_ID];
|
||||
|
||||
if (existing && typeof existing === 'object') {
|
||||
if (existing.apiKey && existing.apiKey !== apiKey) {
|
||||
log.info?.(
|
||||
`[bootstrap-xiapan] uclaw-cloud apiKey already configured (different fingerprint). `
|
||||
+ `Current source=${fingerprintInfo.source}. Use Config UI to rebind if needed.`,
|
||||
);
|
||||
// Still ensure agents.defaults exists so the runtime knows to use uclaw-cloud
|
||||
const changedDefaults = ensureAgentsDefaults(config);
|
||||
if (changedDefaults) writeJson(configPath, config);
|
||||
return {
|
||||
ok: true,
|
||||
action: 'kept',
|
||||
source: fingerprintInfo.source,
|
||||
apiKey: existing.apiKey,
|
||||
};
|
||||
// 幂等:配置里已有 uclaw-cloud apiKey → 保持不动,不再打网络 provision。
|
||||
// (后端 provision 本身也按 deviceId 幂等,但客户端先短路可避免每次启动都请求。)
|
||||
if (existing && typeof existing === 'object' && existing.apiKey) {
|
||||
const changedDefaults = ensureAgentsDefaults(config);
|
||||
if (changedDefaults) {
|
||||
writeJson(configPath, config);
|
||||
log.info?.('[bootstrap-xiapan] Added agents.defaults to existing config');
|
||||
}
|
||||
if (existing.apiKey === apiKey) {
|
||||
const changedDefaults = ensureAgentsDefaults(config);
|
||||
if (changedDefaults) {
|
||||
writeJson(configPath, config);
|
||||
log.info?.('[bootstrap-xiapan] Added agents.defaults to existing config');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
action: changedDefaults ? 'agents-defaults-added' : 'noop',
|
||||
source: fingerprintInfo.source,
|
||||
apiKey,
|
||||
};
|
||||
return {
|
||||
ok: true,
|
||||
action: changedDefaults ? 'agents-defaults-added' : 'noop',
|
||||
source: fingerprintInfo.source,
|
||||
apiKey: existing.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// 首启(配置里还没有 cloud key):静默开户。把指纹送到虾盘云后台自动开一个带
|
||||
// 试用额度的 token。成功 → 用后端真正注册过的 key(可直接聊天,解决 Invalid token)。
|
||||
// 失败(离线/后端不可达)→ 回退到指纹派生 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] = {
|
||||
@@ -144,11 +145,13 @@ export async function bootstrapXiapan({ configPath, appRoot, log = console } = {
|
||||
|
||||
writeJson(configPath, config);
|
||||
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 {
|
||||
ok: true,
|
||||
action: existing ? 'updated' : 'created',
|
||||
action: 'created',
|
||||
provisioned,
|
||||
source: fingerprintInfo.source,
|
||||
apiKey,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user