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:
hfshfg
2026-05-02 17:07:42 +08:00
parent efeec6473e
commit 9ecbbd445c
22 changed files with 1749 additions and 709 deletions

View File

@@ -391,6 +391,71 @@ const server = http.createServer((req, res) => {
return;
}
// API: Xiapan Cloud status (fingerprint + apiKey + balance)
if (req.url === '/api/xiapan/status' && req.method === 'GET') {
(async () => {
try {
const fpMod = await import('../lib/fingerprint.mjs');
const xpMod = await import('../lib/xiapan-client.mjs');
const portableRoot = path.join(__dirname, '..');
const fp = await fpMod.getFingerprint(portableRoot);
const apiKey = xpMod.buildApiKey(fp.fingerprint);
const balance = await xpMod.getBalance(apiKey);
const rechargeUrl = xpMod.getRechargeUrl(apiKey);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
source: fp.source,
apiKey,
rechargeUrl,
balance,
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
})();
return;
}
// API: Re-run bootstrap to inject the uclaw-cloud provider
if (req.url === '/api/xiapan/bind' && req.method === 'POST') {
(async () => {
try {
const mod = await import('../lib/bootstrap-xiapan.mjs');
const portableRoot = path.join(__dirname, '..');
const result = await mod.bootstrapXiapan({
configPath: CONFIG_PATH,
appRoot: portableRoot,
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
})();
return;
}
// API: Remove uclaw-cloud provider so the next bind regenerates it
if (req.url === '/api/xiapan/unbind' && req.method === 'POST') {
try {
if (fs.existsSync(CONFIG_PATH)) {
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
if (config.models && config.models.providers && config.models.providers['uclaw-cloud']) {
delete config.models.providers['uclaw-cloud'];
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
return;
}
// API: Save config
if (req.url === '/api/config' && req.method === 'POST') {
let body = '';