diff --git a/portable/Config.html b/portable/Config.html index f40d93e..71f2eb7 100644 --- a/portable/Config.html +++ b/portable/Config.html @@ -181,55 +181,60 @@ input:focus { border-color: #ff6b35; }

选择 AI 模型

-

选择你要使用的模型,推荐国内用户用 MiniMax / Kimi / DeepSeek

+

最省心:直接用「虾盘云」一个 Key 用全部模型,插上即用、已含试用额度。也可用下面各家自己的 Key。

-
+
-

MiniMax 推荐国内

-

MiniMax-Text-01,速度快,性价比高

- → 获取 API Key -
-
- -

Kimi (月之暗面) 国内

-

moonshot-v1-auto,智能选模型

- → 获取 API Key +

虾盘云 首选含试用额度

+

插上即用,无需申请 Key。一个账号用 DeepSeek / Claude / GPT / 通义等全部模型

DeepSeek 国内便宜

-

deepseek-chat,超低价格

+

deepseek-chat / deepseek-v4,超低价格

→ 获取 API Key
+
+ +

MiniMax 国内

+

MiniMax-M2,速度快,性价比高

+ → 获取 API Key +
+
+ +

Kimi (月之暗面) 国内

+

kimi-k2,长上下文强

+ → 获取 API Key +

智谱 GLM 国内有免费

glm-5(zai provider),免费额度可用

→ 获取 API Key
-
+

通义千问 国内有免费

-

qwen-turbo,阿里云出品

+

qwen-plus / qwen3-max,阿里云出品

→ 获取 API Key
-
+

豆包 (字节) 国内

-

doubao-1.5-pro,字节跳动

+

doubao-seed-1.6,字节跳动

→ 获取 API Key
-
+

OpenAI

-

GPT-4o,需翻墙或中转

+

GPT-5.4,需翻墙或中转

→ 获取 API Key
-
+

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 }; + } +}