Files
u-claw/portable/Config.html
hfshfg b1468d455c 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>
2026-06-15 16:01:35 +08:00

776 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>U-Claw Pro — 模型配置</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #0a0a0a; color: #e0e0e0; min-height: 100vh; }
.container { max-width: 680px; margin: 0 auto; padding: 30px 20px 60px; }
h1 { text-align: center; font-size: 1.8em; margin-bottom: 4px; }
h1 .lobster { color: #ff6b35; }
.subtitle { text-align: center; color: #888; margin-bottom: 24px; font-size: 0.92em; }
/* Steps */
.steps { display: flex; gap: 0; margin-bottom: 24px; background: #1a1a1a; border-radius: 10px; overflow: hidden; border: 1px solid #333; }
.step { flex: 1; text-align: center; padding: 12px 8px; font-size: 0.85em; color: #666; cursor: pointer; transition: all 0.2s; }
.step.active { color: #ff6b35; background: rgba(255,107,53,0.08); font-weight: 600; }
.step.done { color: #4caf50; }
.step .num { display: inline-block; width: 22px; height: 22px; line-height: 22px; border-radius: 50%; background: #333; color: #888; font-size: 0.8em; margin-right: 4px; }
.step.active .num { background: #ff6b35; color: #fff; }
.step.done .num { background: #4caf50; color: #fff; }
/* Section */
.section { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 24px; margin-bottom: 16px; display: none; }
.section.active { display: block; }
.section h2 { color: #ff6b35; font-size: 1.15em; margin-bottom: 6px; }
.section .desc { color: #888; font-size: 0.88em; margin-bottom: 16px; }
/* Model cards */
.model-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
.model-card { background: #222; border: 2px solid #333; border-radius: 10px; padding: 14px; cursor: pointer; transition: all 0.15s; position: relative; }
.model-card:hover { border-color: #555; transform: translateY(-1px); }
.model-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.06); }
.model-card h4 { color: #fff; font-size: 0.95em; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.model-card p { color: #888; font-size: 0.78em; line-height: 1.4; }
.tag { font-size: 0.68em; padding: 2px 7px; border-radius: 10px; font-weight: 500; }
.tag.hot { background: rgba(255,107,53,0.2); color: #ff6b35; }
.tag.free { background: rgba(76,175,80,0.2); color: #4caf50; }
.tag.cn { background: rgba(33,150,243,0.2); color: #2196f3; }
.tag.fast { background: rgba(156,39,176,0.2); color: #ce93d8; }
.tag.cheap { background: rgba(255,193,7,0.2); color: #ffd54f; }
.check { position: absolute; top: 8px; right: 10px; color: #ff6b35; font-size: 1.2em; display: none; }
.model-card.selected .check { display: block; }
/* Buy link */
.buy-link { display: inline-flex; align-items: center; gap: 4px; color: #ff6b35; text-decoration: none; font-size: 0.82em; margin-top: 6px; }
.buy-link:hover { text-decoration: underline; }
/* Forms */
.form-group { display: block; margin-bottom: 16px; clear: both; }
.form-group label {
display: block;
width: auto;
float: none;
color: #ccc;
margin-bottom: 8px;
font-size: 0.9em;
}
.form-group input,
input[type="text"],
input[type="password"] {
display: block;
width: 100%;
min-width: 0;
min-height: 46px;
padding: 11px 14px;
background: #222;
border: 1px solid #444;
border-radius: 8px;
color: #fff;
font-size: 0.95em;
line-height: 1.3;
outline: none;
transition: border-color 0.2s;
}
input:focus { border-color: #ff6b35; }
.input-wrap { position: relative; display: block; width: 100%; min-width: 0; }
.input-wrap input { padding-right: 64px; }
.toggle-pw {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
min-width: 38px;
height: 30px;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 6px;
color: #ccc;
cursor: pointer;
font-size: 0.82em;
line-height: 28px;
text-align: center;
}
.toggle-pw:hover { color: #ccc; }
.hint { color: #888; font-size: 0.8em; margin-top: 6px; line-height: 1.5; }
.hint a { color: #ff6b35; text-decoration: none; }
.hint a:hover { text-decoration: underline; }
/* API Key info box */
.api-info { background: #1e1e2e; border: 1px solid #333; border-radius: 8px; padding: 14px; margin-bottom: 16px; }
.api-info h4 { color: #ff6b35; font-size: 0.9em; margin-bottom: 8px; }
.api-info .provider-links { display: flex; flex-direction: column; gap: 6px; }
.api-info a { color: #82aaff; text-decoration: none; font-size: 0.85em; display: flex; align-items: center; gap: 6px; }
.api-info a:hover { color: #ff6b35; }
.api-info a .price { color: #888; font-size: 0.85em; }
/* Buttons */
.btn { display: inline-block; padding: 12px 28px; border: none; border-radius: 8px; font-size: 0.95em; cursor: pointer; transition: all 0.15s; font-weight: 500; }
.btn-primary { background: #ff6b35; color: #fff; }
.btn-primary:hover { background: #e55a25; }
.btn-primary:disabled { background: #444; color: #888; cursor: not-allowed; }
.btn-secondary { background: #333; color: #ccc; }
.btn-secondary:hover { background: #444; }
.btn-row { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }
/* Result */
.result { text-align: center; padding: 40px 20px; }
.result .icon { font-size: 3em; margin-bottom: 12px; }
.result h2 { color: #4caf50; margin-bottom: 8px; }
.result p { color: #888; font-size: 0.92em; line-height: 1.6; }
.result .url { color: #ff6b35; font-size: 1.1em; font-weight: 600; margin: 12px 0; }
/* Toast */
.toast { position: fixed; top: 20px; right: 20px; background: #4caf50; color: #fff; padding: 12px 20px; border-radius: 8px; font-size: 0.9em; opacity: 0; transform: translateY(-10px); transition: all 0.3s; z-index: 999; }
.toast.show { opacity: 1; transform: translateY(0); }
.toast.error { background: #f44336; }
/* Channel form */
.channel-card { transition: all 0.2s; }
.channel-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.06); }
.channel-form { padding-top: 12px; margin-top: 10px; border-top: 1px solid #333; max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; padding-top: 0; }
.channel-form.open { max-height: 300px; padding-top: 12px; }
.channel-form .form-group { margin-bottom: 10px; }
.channel-form label { font-size: 0.82em; color: #aaa; margin-bottom: 4px; }
.channel-form input { font-size: 0.88em; padding: 9px 12px; }
.channel-form .hint { font-size: 0.75em; margin-top: 4px; }
/* Skills section */
.skills-section { margin-top: 20px; }
.skill-list { display: flex; flex-direction: column; gap: 8px; }
.skill-item { background: #222; border: 1px solid #333; border-radius: 8px; padding: 12px; display: flex; align-items: center; justify-content: space-between; }
.skill-item .info h4 { color: #fff; font-size: 0.9em; }
.skill-item .info p { color: #888; font-size: 0.78em; }
.skill-item .status { font-size: 0.8em; color: #4caf50; }
</style>
</head>
<body>
<div class="container">
<h1><span class="lobster">🦞</span> U-Claw Pro</h1>
<p class="subtitle">便携版 — 选择模型,填入 API Key一键启动</p>
<!-- Xiapan Cloud bound banner: shown when bootstrap has injected uclaw-cloud -->
<div class="xp-banner" id="xpBanner" style="display:none; background:linear-gradient(135deg,#2a1f1f,#1f2a1f); border:1px solid #ff6b35; border-radius:10px; padding:16px 18px; margin-bottom:18px;">
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
<div style="flex:1; min-width:240px;">
<div style="font-size:0.95em; color:#ff6b35; font-weight:600;">🔗 已绑定虾盘云 · 开箱即用</div>
<div style="font-size:0.82em; color:#bbb; margin-top:4px;">
指纹来源: <span id="xpSource"></span>
· Key: <code id="xpKeyShort" style="background:#222; padding:2px 6px; border-radius:4px;"></code>
· 余额: <span id="xpBalance"></span>
</div>
<div id="xpHint" style="font-size:0.75em; color:#888; margin-top:4px;">余额为 0 时无法调用 AI请先充值</div>
</div>
<div style="display:flex; gap:8px;">
<button class="btn" id="xpRecharge" style="background:#ff6b35; color:#fff; padding:8px 16px; font-size:0.85em;">前往充值 →</button>
<button class="btn" id="xpRefresh" style="background:#333; color:#ccc; padding:8px 12px; font-size:0.85em;">刷新</button>
<button class="btn" id="xpRebind" style="background:#222; color:#888; padding:8px 12px; font-size:0.8em;" title="清除绑定后重启 U-Claw 即可重新生成 Key">解绑</button>
</div>
</div>
</div>
<!-- Steps -->
<div class="steps">
<div class="step active" data-step="1"><span class="num">1</span>选模型</div>
<div class="step" data-step="2"><span class="num">2</span>填 Key</div>
<div class="step" data-step="3"><span class="num">3</span>启动</div>
</div>
<!-- Step 1: Select Model -->
<div class="section active" id="step1">
<h2>选择 AI 模型</h2>
<p class="desc">最省心:直接用「虾盘云」一个 Key 用全部模型,插上即用、已含试用额度。也可用下面各家自己的 Key。</p>
<div class="model-grid">
<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>
<h4>虾盘云 <span class="tag hot">首选</span><span class="tag free">含试用额度</span></h4>
<p>插上即用,无需申请 Key。一个账号用 DeepSeek / Claude / GPT / 通义等全部模型</p>
</div>
<div class="model-card" data-provider="deepseek" data-base="https://api.deepseek.com/v1" data-model="deepseek-chat">
<span class="check"></span>
<h4>DeepSeek <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
<p>deepseek-chat / deepseek-v4超低价格</p>
<a class="buy-link" href="https://platform.deepseek.com/" target="_blank">→ 获取 API Key</a>
</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">
<span class="check"></span>
<h4>智谱 GLM <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
<p>glm-5zai provider免费额度可用</p>
<a class="buy-link" href="https://open.bigmodel.cn/" target="_blank">→ 获取 API Key</a>
</div>
<div class="model-card" data-provider="qwen" data-base="https://dashscope.aliyuncs.com/compatible-mode/v1" data-model="qwen-plus">
<span class="check"></span>
<h4>通义千问 <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
<p>qwen-plus / qwen3-max阿里云出品</p>
<a class="buy-link" href="https://dashscope.console.aliyun.com/" target="_blank">→ 获取 API Key</a>
</div>
<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>
<h4>豆包 (字节) <span class="tag cn">国内</span><span class="tag fast"></span></h4>
<p>doubao-seed-1.6,字节跳动</p>
<a class="buy-link" href="https://console.volcengine.com/ark" target="_blank">→ 获取 API Key</a>
</div>
<div class="model-card" data-provider="openai" data-base="https://api.openai.com/v1" data-model="gpt-5.4">
<span class="check"></span>
<h4>OpenAI <span class="tag hot"></span></h4>
<p>GPT-5.4,需翻墙或中转</p>
<a class="buy-link" href="https://platform.openai.com/" target="_blank">→ 获取 API Key</a>
</div>
<div class="model-card" data-provider="anthropic" data-base="https://api.anthropic.com/v1" data-model="claude-opus-4-6">
<span class="check"></span>
<h4>Claude <span class="tag hot"></span></h4>
<p>Claude Opus 4.6 / Sonnet 4.6,需翻墙或中转</p>
<a class="buy-link" href="https://console.anthropic.com/" target="_blank">→ 获取 API Key</a>
</div>
<div class="model-card" data-provider="groq" data-base="https://api.groq.com/openai/v1" data-model="llama-3.3-70b-versatile">
<span class="check"></span>
<h4>Groq <span class="tag fast">极快</span><span class="tag free">有免费</span></h4>
<p>Llama 3.3 70B超高速推理</p>
<a class="buy-link" href="https://console.groq.com/" target="_blank">→ 获取 API Key</a>
</div>
<div class="model-card" data-provider="siliconflow" data-base="https://api.siliconflow.cn/v1" data-model="Qwen/Qwen2.5-72B-Instruct">
<span class="check"></span>
<h4>硅基流动 <span class="tag cn">国内</span><span class="tag cheap">便宜</span></h4>
<p>多模型聚合,价格低</p>
<a class="buy-link" href="https://cloud.siliconflow.cn/" target="_blank">→ 获取 API Key</a>
</div>
<div class="model-card" data-provider="custom" data-base="" data-model="">
<span class="check"></span>
<h4>自定义 <span class="tag">OpenAI 兼容</span></h4>
<p>填写任意 OpenAI 兼容 API 地址</p>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" id="nextStep1" disabled>下一步 →</button>
</div>
</div>
<!-- Step 2: API Key -->
<div class="section" id="step2">
<h2>填写 API Key</h2>
<p class="desc" id="step2desc">请填写对应模型的 API Key</p>
<div class="api-info" id="buyInfo">
<h4>📌 如何获取 API Key</h4>
<div class="provider-links" id="providerLinks"></div>
</div>
<!-- Custom fields (hidden by default) -->
<div id="customFields" style="display:none;">
<div class="form-group">
<label>API 地址 (Base URL)</label>
<input type="text" id="customBase" placeholder="https://api.example.com/v1">
</div>
<div class="form-group">
<label>模型名称</label>
<input type="text" id="customModel" placeholder="gpt-4o">
</div>
</div>
<div class="form-group">
<label>API Key</label>
<div class="input-wrap">
<input type="password" id="apiKey" placeholder="sk-...">
<button class="toggle-pw" onclick="togglePw()">👁</button>
</div>
<p class="hint">你的 Key 仅保存在本地 U 盘,不会上传到任何服务器</p>
</div>
<div class="btn-row">
<button class="btn btn-secondary" onclick="goStep(1)">← 返回</button>
<button class="btn btn-primary" id="saveBtn" onclick="saveConfig()">保存并启动 🚀</button>
</div>
</div>
<!-- Step 3: Done -->
<div class="section" id="step3">
<div class="result">
<div class="icon">🦞</div>
<h2>配置完成!</h2>
<p>模型和 API Key 已保存,准备好后点击下方按钮启动</p>
<div class="url" id="gatewayUrl">http://127.0.0.1:18789</div>
<div style="margin-top: 24px; text-align: left;">
<h3 style="color: #ff6b35; font-size: 0.95em; margin-bottom: 12px;">📱 接入更多渠道(可选)</h3>
<p style="color: #888; font-size: 0.82em; margin-bottom: 12px;">点击展开卡片填写配置,启动时自动写入。不填也可以直接启动:</p>
<div class="model-grid">
<div class="model-card channel-card" onclick="toggleChannel(this, 'telegram')">
<h4>✈️ Telegram</h4>
<p>接入 Telegram Bot</p>
<div class="channel-form" id="form-telegram" onclick="event.stopPropagation()">
<div class="form-group">
<label>Bot Token</label>
<input type="text" id="ch-telegram-token" placeholder="123456:ABC-DEF...">
<p class="hint"><a href="https://t.me/BotFather" target="_blank">→ 从 @BotFather 获取</a></p>
</div>
</div>
</div>
<div class="model-card channel-card" onclick="toggleChannel(this, 'qqbot')">
<h4>🐧 QQ Bot</h4>
<p>接入 QQ 群/私聊机器人</p>
<div class="channel-form" id="form-qqbot" onclick="event.stopPropagation()">
<div class="form-group">
<label>App ID</label>
<input type="text" id="ch-qqbot-appid" placeholder="应用 ID">
</div>
<div class="form-group">
<label>Client Secret</label>
<input type="text" id="ch-qqbot-secret" placeholder="应用密钥">
<p class="hint"><a href="https://q.qq.com" target="_blank">→ 从 QQ 开放平台获取</a></p>
</div>
</div>
</div>
<div class="model-card channel-card" onclick="toggleChannel(this, 'feishu')">
<h4>📘 飞书</h4>
<p>接入飞书机器人</p>
<div class="channel-form" id="form-feishu" onclick="event.stopPropagation()">
<div class="form-group">
<label>App ID</label>
<input type="text" id="ch-feishu-appid" placeholder="cli_xxx...">
</div>
<div class="form-group">
<label>App Secret</label>
<input type="text" id="ch-feishu-secret" placeholder="应用密钥">
<p class="hint"><a href="https://open.feishu.cn/app" target="_blank">→ 从飞书开放平台获取</a></p>
</div>
</div>
</div>
<div class="model-card channel-card" onclick="toggleChannel(this, 'wecom')">
<h4>🏢 企业微信</h4>
<p>接入企业微信机器人</p>
<div class="channel-form" id="form-wecom" onclick="event.stopPropagation()">
<div class="form-group">
<label>Bot ID</label>
<input type="text" id="ch-wecom-botid" placeholder="aib...">
</div>
<div class="form-group">
<label>Secret</label>
<input type="text" id="ch-wecom-secret" placeholder="密钥">
<p class="hint"><a href="https://work.weixin.qq.com" target="_blank">→ 从企业微信管理后台获取</a></p>
</div>
</div>
</div>
</div>
</div>
<div style="margin-top: 28px; text-align: center;">
<button class="btn btn-primary" onclick="launchOpenClaw()" style="font-size: 1.15em; padding: 16px 48px;">🚀 启动 OpenClaw</button>
<p style="color: #888; font-size: 0.8em; margin-top: 10px;">启动后会自动打开控制台,渠道配置会自动写入 openclaw.json</p>
</div>
<div class="btn-row" style="justify-content: center; margin-top: 16px;">
<button class="btn btn-secondary" onclick="goStep(1)">重新配置模型</button>
</div>
</div>
</div>
<!-- Skills info -->
<div class="section active skills-section" style="display:block; margin-top: 24px;">
<h2>📦 技能 & 文档</h2>
<p class="desc">技能存放在 skills/ 目录,文档存放在 docs/ 目录,随 U 盘携带</p>
<div class="skill-list" id="skillList">
<div class="skill-item">
<div class="info">
<h4>skills/ — 技能目录</h4>
<p>放入 .md 技能文件OpenClaw 会自动加载</p>
</div>
<span class="status">📁</span>
</div>
<div class="skill-item">
<div class="info">
<h4>docs/ — 文档目录</h4>
<p>存放使用说明、帮助文档等</p>
</div>
<span class="status">📁</span>
</div>
<div class="skill-item">
<div class="info">
<h4>data/ — 配置和聊天记录</h4>
<p>API Key、聊天历史等保存在此</p>
</div>
<span class="status">🔒</span>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
// --- Xiapan Cloud banner ---
async function loadXiapanStatus() {
const banner = document.getElementById('xpBanner');
try {
const res = await fetch('/api/xiapan/status');
if (!res.ok) return;
const data = await res.json();
if (!data || !data.apiKey) return;
document.getElementById('xpSource').textContent = ({
usb: 'U 盘', disk: '硬盘', mac: 'Mac', linux: 'Linux', seed: '本机种子', test: '测试',
})[data.source] || data.source;
document.getElementById('xpKeyShort').textContent = data.apiKey.slice(0, 14) + '…';
const bal = data.balance || {};
if (bal.ok) {
const usd = bal.remainingUsd != null ? bal.remainingUsd.toFixed(2) : '0.00';
const tokens = (bal.remainingTokens || 0).toLocaleString();
document.getElementById('xpBalance').textContent = `$${usd} (${tokens} tokens)`;
document.getElementById('xpHint').textContent = bal.remainingUsd > 0
? '已含试用额度,可直接用 deepseek-v4 / claude / gpt / 通义 等全部模型'
: '试用额度已用完,点击右侧充值即可继续,秒到账';
} else if (bal.reason && bal.reason.includes('401')) {
document.getElementById('xpBalance').textContent = '未注册';
document.getElementById('xpHint').textContent = '首次使用:点「前往充值」即可在虾盘云页面注册并充值,秒到账';
} else {
document.getElementById('xpBalance').textContent = '查询失败';
document.getElementById('xpHint').textContent = '请检查网络或 api.u-claw.org 服务状态';
}
document.getElementById('xpRecharge').onclick = () => window.open(data.rechargeUrl, '_blank');
document.getElementById('xpRefresh').onclick = () => loadXiapanStatus();
document.getElementById('xpRebind').onclick = async () => {
if (!confirm('解绑后下次启动 U-Claw 会重新生成 Key基于当前设备指纹。\n现有 Key 的余额仍归属当前指纹,更换 U 盘 / 电脑后才需要解绑。\n\n确认解绑')) return;
await fetch('/api/xiapan/unbind', { method: 'POST' });
showToast('已解绑,请重启 U-Claw');
};
banner.style.display = 'block';
} catch (err) {
// network error or endpoint missing — leave banner hidden
}
}
loadXiapanStatus();
// --- State ---
let selectedProvider = null;
let selectedBase = '';
let selectedModel = '';
let selectedZeroKey = false; // 虾盘云等已自动开户、无需用户填 Key 的 provider
// --- Model cards ---
document.querySelectorAll('#step1 .model-card').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.closest('.buy-link')) return; // don't select when clicking buy link
document.querySelectorAll('#step1 .model-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedProvider = card.dataset.provider;
selectedBase = card.dataset.base;
selectedModel = card.dataset.model;
selectedZeroKey = card.dataset.zerokey === '1';
const nextBtn = document.getElementById('nextStep1');
nextBtn.disabled = false;
// 虾盘云已自动开户,跳过填 Key按钮直接变「启动」
nextBtn.textContent = selectedZeroKey ? '直接启动 🚀' : '下一步 →';
});
});
document.getElementById('nextStep1').addEventListener('click', () => {
if (selectedZeroKey) {
// 虾盘云:已有自动开户的 key在 uclaw-cloud provider 里),无需填,直接保存启动
saveZeroKeyConfig();
} else {
goStep(2);
}
});
// --- Steps ---
function goStep(n) {
document.querySelectorAll('.section:not(.skills-section)').forEach(s => s.classList.remove('active'));
document.getElementById('step' + n).classList.add('active');
// Keep skills section visible
document.querySelectorAll('.steps .step').forEach((s, i) => {
s.classList.remove('active', 'done');
if (i + 1 < n) s.classList.add('done');
if (i + 1 === n) s.classList.add('active');
});
if (n === 2) setupStep2();
}
function setupStep2() {
const isCustom = selectedProvider === 'custom';
document.getElementById('customFields').style.display = isCustom ? 'block' : 'none';
const providerNames = {
minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
siliconflow: '硅基流动', custom: '自定义'
};
const name = providerNames[selectedProvider] || selectedProvider;
document.getElementById('step2desc').textContent = `请填写 ${name} 的 API Key`;
// Buy links
const card = document.querySelector(`#step1 .model-card[data-provider="${selectedProvider}"]`);
const buyLink = card ? card.querySelector('.buy-link') : null;
const linksDiv = document.getElementById('providerLinks');
linksDiv.innerHTML = '';
if (buyLink) {
linksDiv.innerHTML = `
<a href="${buyLink.href}" target="_blank">🔑 ${name} — 获取 API Key</a>
`;
}
// Add common alternatives
linksDiv.innerHTML += `
<a href="https://platform.minimaxi.com/" target="_blank">🔑 MiniMax — 国内推荐,注册送额度</a>
<a href="https://open.bigmodel.cn/" target="_blank">🔑 智谱 GLM — 有免费额度</a>
<a href="https://cloud.siliconflow.cn/" target="_blank">🔑 硅基流动 — 多模型聚合,便宜</a>
`;
}
// --- Toggle password ---
function togglePw() {
const inp = document.getElementById('apiKey');
inp.type = inp.type === 'password' ? 'text' : 'password';
}
// --- Generate correct OpenClaw config ---
function buildOpenClawConfig(provider, base, model, apiKey) {
// Base config that OpenClaw expects
const baseConfig = {
gateway: { auth: { mode: 'token', token: 'uclaw', scopes: ['operator.admin'] } },
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
meta: { lastTouchedVersion: '2026.3.13', lastTouchedAt: new Date().toISOString() }
};
// 智谱 GLM uses the built-in zai provider
if (provider === 'zai') {
return {
...baseConfig,
env: { ZAI_API_KEY: apiKey },
agents: { defaults: { model: { primary: 'zai/' + model } } }
};
}
// All other providers use models.providers (OpenAI-compatible)
const providerName = provider || 'myProvider';
return {
...baseConfig,
models: {
mode: 'merge',
providers: {
[providerName]: {
baseUrl: base,
apiKey: apiKey,
api: 'openai-completions',
models: [{
id: model,
name: model,
reasoning: false,
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192
}]
}
}
},
agents: { defaults: { model: { primary: providerName + '/' + model } } }
};
}
// --- 虾盘云:已自动开户,无需填 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();
if (!apiKey) { showToast('请填写 API Key', true); return; }
let base = selectedBase;
let model = selectedModel;
if (selectedProvider === 'custom') {
base = document.getElementById('customBase').value.trim();
model = document.getElementById('customModel').value.trim();
if (!base || !model) { showToast('请填写 API 地址和模型名称', true); return; }
}
const openclawConfig = buildOpenClawConfig(selectedProvider, base, model, apiKey);
try {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(openclawConfig)
});
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);
}
}
// --- Toast ---
function showToast(msg, isError) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast show' + (isError ? ' error' : '');
setTimeout(() => t.className = 'toast', 3000);
}
// --- Load existing config ---
async function loadConfig() {
try {
const res = await fetch('/api/config');
const cfg = await res.json();
// Detect zai provider (智谱 GLM)
if (cfg.env && cfg.env.ZAI_API_KEY) {
const card = document.querySelector('.model-card[data-provider="zai"]');
if (card) card.click();
document.getElementById('apiKey').value = cfg.env.ZAI_API_KEY;
return;
}
// Detect models.providers format
if (cfg.models && cfg.models.providers) {
const providerName = Object.keys(cfg.models.providers)[0];
if (providerName) {
const p = cfg.models.providers[providerName];
document.querySelectorAll('.model-card').forEach(card => {
if (card.dataset.provider === providerName) card.click();
});
if (p.apiKey) document.getElementById('apiKey').value = p.apiKey;
}
}
} catch(e) {}
}
loadConfig();
// Allow direct step preview via URL param, e.g. ?step=3
const stepParam = new URLSearchParams(window.location.search).get('step');
if (stepParam) goStep(parseInt(stepParam));
// --- Step 3: Quick entry buttons ---
function getPort() {
const params = new URLSearchParams(window.location.search);
return params.get('port') || '18789';
}
function openDashboard() {
const port = getPort();
window.open('http://127.0.0.1:' + port + '/#token=uclaw', '_blank');
}
function openOnboard() {
const port = getPort();
window.open('http://127.0.0.1:' + port + '/onboarding', '_blank');
}
function openChannel(type) {
const port = getPort();
window.open('http://127.0.0.1:' + port + '/#/channels', '_blank');
}
function toggleChannel(card, type) {
const form = document.getElementById('form-' + type);
const isOpen = form.classList.contains('open');
if (isOpen) {
form.classList.remove('open');
card.classList.remove('selected');
} else {
form.classList.add('open');
card.classList.add('selected');
// Focus first input
const firstInput = form.querySelector('input');
if (firstInput) setTimeout(() => firstInput.focus(), 100);
}
}
async function launchOpenClaw() {
// 1. Collect channel configs
const channels = {};
const tgToken = document.getElementById('ch-telegram-token')?.value.trim();
if (tgToken) channels.telegram = { enabled: true, token: tgToken, dmPolicy: 'pairing' };
const qqId = document.getElementById('ch-qqbot-appid')?.value.trim();
const qqSecret = document.getElementById('ch-qqbot-secret')?.value.trim();
if (qqId && qqSecret) channels.qqbot = { enabled: true, appId: qqId, clientSecret: qqSecret };
const fsId = document.getElementById('ch-feishu-appid')?.value.trim();
const fsSecret = document.getElementById('ch-feishu-secret')?.value.trim();
if (fsId && fsSecret) channels.feishu = { enabled: true, appId: fsId, appSecret: fsSecret };
const wcBotId = document.getElementById('ch-wecom-botid')?.value.trim();
const wcSecret = document.getElementById('ch-wecom-secret')?.value.trim();
if (wcBotId && wcSecret) channels.wecom = { enabled: true, botId: wcBotId, secret: wcSecret };
// 2. Save channel config if any (merge into existing config)
if (Object.keys(channels).length > 0) {
try {
const res = await fetch('/api/config');
const existing = res.ok ? await res.json() : {};
// 清除旧版废弃键,防止 OpenClaw 报 "agent.* was moved" 错误
delete existing.agent;
existing.channels = Object.assign(existing.channels || {}, channels);
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(existing)
});
} catch(e) {}
}
// 3. Launch
showToast('配置已保存!请关闭此页面,回到启动窗口使用 U-Claw');
try { await fetch('/api/done', { method: 'POST' }); } catch(e) {}
}
</script>
</body>
</html>