- 新增 portable/lib/{fingerprint,xiapan-client,bootstrap-xiapan}.mjs:
跨平台设备指纹(Win USB/disk + Mac UUID + Linux machine-id + seed 兜底)
生成 sk-uc-{fingerprint} 形式的虾盘云 apiKey,启动时 merge 到 openclaw.json
- Config.html 顶部新增「已绑定虾盘云」横幅:指纹来源 + Key + 余额 + 充值/解绑
- config-server 增加 /api/xiapan/{status,bind,unbind} 三个端点
- Electron app 在 whenReady 调 bootstrap,新增 sync-lib.js 让 build 前自动同步
- 锁 OpenClaw 版本到 2026.4.29(OPENCLAW_VERSION 单一来源)
- 删除死代码 portable/充值.html
- 新增 .github/workflows/release.yml:tag 触发,出 portable zip + Electron exe/dmg
- README 增加内置虾盘云说明 + 直接下载发行版指引
723 lines
34 KiB
HTML
723 lines
34 KiB
HTML
<!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">选择你要使用的模型,推荐国内用户用 MiniMax / Kimi / DeepSeek</p>
|
||
|
||
<div class="model-grid">
|
||
<div class="model-card" data-provider="minimax" data-base="https://api.minimax.chat/v1" data-model="MiniMax-Text-01">
|
||
<span class="check">✓</span>
|
||
<h4>MiniMax <span class="tag hot">推荐</span><span class="tag cn">国内</span></h4>
|
||
<p>MiniMax-Text-01,速度快,性价比高</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="moonshot-v1-auto">
|
||
<span class="check">✓</span>
|
||
<h4>Kimi (月之暗面) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||
<p>moonshot-v1-auto,智能选模型</p>
|
||
<a class="buy-link" href="https://platform.moonshot.cn/" target="_blank">→ 获取 API Key</a>
|
||
</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,超低价格</p>
|
||
<a class="buy-link" href="https://platform.deepseek.com/" 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-5(zai 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-turbo">
|
||
<span class="check">✓</span>
|
||
<h4>通义千问 <span class="tag cn">国内</span><span class="tag free">有免费</span></h4>
|
||
<p>qwen-turbo,阿里云出品</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-1.5-pro-32k">
|
||
<span class="check">✓</span>
|
||
<h4>豆包 (字节) <span class="tag cn">国内</span><span class="tag fast">快</span></h4>
|
||
<p>doubao-1.5-pro,字节跳动</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-4o">
|
||
<span class="check">✓</span>
|
||
<h4>OpenAI <span class="tag hot">强</span></h4>
|
||
<p>GPT-4o,需翻墙或中转</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-sonnet-4-20250514">
|
||
<span class="check">✓</span>
|
||
<h4>Claude <span class="tag hot">强</span></h4>
|
||
<p>Claude Sonnet 4,需翻墙或中转</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-chat / qwen-plus / qwen-turbo'
|
||
: '余额为 0,点击右侧前往充值';
|
||
} 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 = '';
|
||
|
||
// --- 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;
|
||
document.getElementById('nextStep1').disabled = false;
|
||
});
|
||
});
|
||
|
||
document.getElementById('nextStep1').addEventListener('click', () => 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 } } }
|
||
};
|
||
}
|
||
|
||
// --- 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>
|