feat: 内置虾盘云 + 设备指纹绑定 + Release 流水线
- 新增 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 增加内置虾盘云说明 + 直接下载发行版指引
This commit is contained in:
@@ -48,16 +48,51 @@ h1 .lobster { color: #ff6b35; }
|
||||
.buy-link:hover { text-decoration: underline; }
|
||||
|
||||
/* Forms */
|
||||
.form-group { margin-bottom: 16px; }
|
||||
label { display: block; color: #ccc; margin-bottom: 6px; font-size: 0.9em; }
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%; padding: 11px 14px; background: #222; border: 1px solid #444; border-radius: 8px;
|
||||
color: #fff; font-size: 0.95em; outline: none; transition: border-color 0.2s;
|
||||
.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; }
|
||||
.input-wrap input { padding-right: 44px; }
|
||||
.toggle-pw { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #888; cursor: pointer; font-size: 0.85em; }
|
||||
.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; }
|
||||
@@ -116,6 +151,26 @@ input:focus { border-color: #ff6b35; }
|
||||
<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>
|
||||
@@ -353,16 +408,60 @@ input:focus { border-color: #ff6b35; }
|
||||
<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('.model-card').forEach(card => {
|
||||
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('.model-card').forEach(c => c.classList.remove('selected'));
|
||||
document.querySelectorAll('#step1 .model-card').forEach(c => c.classList.remove('selected'));
|
||||
card.classList.add('selected');
|
||||
selectedProvider = card.dataset.provider;
|
||||
selectedBase = card.dataset.base;
|
||||
@@ -375,7 +474,7 @@ document.getElementById('nextStep1').addEventListener('click', () => goStep(2));
|
||||
|
||||
// --- Steps ---
|
||||
function goStep(n) {
|
||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
||||
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) => {
|
||||
@@ -393,7 +492,7 @@ function setupStep2() {
|
||||
|
||||
const providerNames = {
|
||||
minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
|
||||
zhipu: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
|
||||
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
|
||||
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
|
||||
siliconflow: '硅基流动', custom: '自定义'
|
||||
};
|
||||
@@ -401,7 +500,7 @@ function setupStep2() {
|
||||
document.getElementById('step2desc').textContent = `请填写 ${name} 的 API Key`;
|
||||
|
||||
// Buy links
|
||||
const card = document.querySelector(`.model-card[data-provider="${selectedProvider}"]`);
|
||||
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 = '';
|
||||
|
||||
Reference in New Issue
Block a user