+
✓
Claude 强
-
Claude Sonnet 4,需翻墙或中转
+
Claude Opus 4.6 / Sonnet 4.6,需翻墙或中转
→ 获取 API Key
@@ -427,8 +432,8 @@ async function loadXiapanStatus() {
const tokens = (bal.remainingTokens || 0).toLocaleString();
document.getElementById('xpBalance').textContent = `$${usd} (${tokens} tokens)`;
document.getElementById('xpHint').textContent = bal.remainingUsd > 0
- ? '可直接使用 deepseek-chat / qwen-plus / qwen-turbo'
- : '余额为 0,点击右侧前往充值';
+ ? '已含试用额度,可直接用 deepseek-v4 / claude / gpt / 通义 等全部模型'
+ : '试用额度已用完,点击右侧充值即可继续,秒到账';
} else if (bal.reason && bal.reason.includes('401')) {
document.getElementById('xpBalance').textContent = '未注册';
document.getElementById('xpHint').textContent = '首次使用:点「前往充值」即可在虾盘云页面注册并充值,秒到账';
@@ -456,6 +461,7 @@ loadXiapanStatus();
let selectedProvider = null;
let selectedBase = '';
let selectedModel = '';
+let selectedZeroKey = false; // 虾盘云等已自动开户、无需用户填 Key 的 provider
// --- Model cards ---
document.querySelectorAll('#step1 .model-card').forEach(card => {
@@ -466,11 +472,22 @@ document.querySelectorAll('#step1 .model-card').forEach(card => {
selectedProvider = card.dataset.provider;
selectedBase = card.dataset.base;
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 ---
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 ---
async function saveConfig() {
const apiKey = document.getElementById('apiKey').value.trim();
diff --git a/portable/lib/bootstrap-xiapan.mjs b/portable/lib/bootstrap-xiapan.mjs
index 3d07edd..2433a17 100644
--- a/portable/lib/bootstrap-xiapan.mjs
+++ b/portable/lib/bootstrap-xiapan.mjs
@@ -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,
};
diff --git a/portable/lib/xiapan-client.mjs b/portable/lib/xiapan-client.mjs
index 94f65d1..930eaec 100644
--- a/portable/lib/xiapan-client.mjs
+++ b/portable/lib/xiapan-client.mjs
@@ -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 };
+ }
+}