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:
143
portable/lib/bootstrap-xiapan.mjs
Normal file
143
portable/lib/bootstrap-xiapan.mjs
Normal file
@@ -0,0 +1,143 @@
|
||||
// Bootstrap: ensure data/.openclaw/openclaw.json contains the uclaw-cloud provider
|
||||
// pointing to the device-bound apiKey derived from the local fingerprint.
|
||||
//
|
||||
// Idempotent: if the provider already exists with the correct apiKey, do nothing.
|
||||
// If it exists but the apiKey differs (USB swapped, machine changed), leave the
|
||||
// existing entry alone and log a hint — never overwrite user data silently.
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { getFingerprint } from './fingerprint.mjs';
|
||||
import { buildApiKey } from './xiapan-client.mjs';
|
||||
|
||||
const PROVIDER_ID = 'uclaw-cloud';
|
||||
|
||||
const DEFAULT_PROVIDER_TEMPLATE = {
|
||||
baseUrl: 'https://api.u-claw.org/v1',
|
||||
api: 'openai-completions',
|
||||
models: [
|
||||
{ id: 'deepseek-chat', label: 'DeepSeek Chat' },
|
||||
{ id: 'qwen-plus', label: 'Qwen Plus' },
|
||||
{ id: 'qwen-turbo', label: 'Qwen Turbo' },
|
||||
],
|
||||
};
|
||||
|
||||
function readJsonSafe(filePath) {
|
||||
if (!existsSync(filePath)) return null;
|
||||
try {
|
||||
const raw = readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
process.stderr.write(`[bootstrap-xiapan] Cannot parse ${filePath}: ${err.message}\n`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(filePath, data) {
|
||||
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function ensureModelsContainer(config) {
|
||||
if (!config.models || typeof config.models !== 'object') {
|
||||
config.models = { mode: 'merge', providers: {} };
|
||||
}
|
||||
if (!config.models.mode) config.models.mode = 'merge';
|
||||
if (!config.models.providers || typeof config.models.providers !== 'object') {
|
||||
config.models.providers = {};
|
||||
}
|
||||
return config.models.providers;
|
||||
}
|
||||
|
||||
export async function bootstrapXiapan({ configPath, appRoot, log = console } = {}) {
|
||||
if (!configPath) {
|
||||
throw new Error('bootstrapXiapan: configPath is required.');
|
||||
}
|
||||
const root = appRoot || process.cwd();
|
||||
|
||||
let fingerprintInfo;
|
||||
try {
|
||||
fingerprintInfo = await getFingerprint(root);
|
||||
} catch (err) {
|
||||
log.warn?.(`[bootstrap-xiapan] Fingerprint detection failed: ${err.message}`);
|
||||
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.`,
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
action: 'kept',
|
||||
source: fingerprintInfo.source,
|
||||
apiKey: existing.apiKey,
|
||||
};
|
||||
}
|
||||
if (existing.apiKey === apiKey) {
|
||||
return {
|
||||
ok: true,
|
||||
action: 'noop',
|
||||
source: fingerprintInfo.source,
|
||||
apiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
providers[PROVIDER_ID] = {
|
||||
...DEFAULT_PROVIDER_TEMPLATE,
|
||||
...(existing && typeof existing === 'object' ? existing : {}),
|
||||
baseUrl: existing?.baseUrl || DEFAULT_PROVIDER_TEMPLATE.baseUrl,
|
||||
api: existing?.api || DEFAULT_PROVIDER_TEMPLATE.api,
|
||||
apiKey,
|
||||
models: existing?.models?.length ? existing.models : DEFAULT_PROVIDER_TEMPLATE.models,
|
||||
};
|
||||
|
||||
writeJson(configPath, config);
|
||||
log.info?.(
|
||||
`[bootstrap-xiapan] Wrote uclaw-cloud provider (source=${fingerprintInfo.source}, key=${apiKey.slice(0, 12)}…)`,
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
action: existing ? 'updated' : 'created',
|
||||
source: fingerprintInfo.source,
|
||||
apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// CLI:
|
||||
// node bootstrap-xiapan.mjs <config-path>
|
||||
// env UCLAW_CONFIG_PATH=... node bootstrap-xiapan.mjs
|
||||
import { pathToFileURL } from 'node:url';
|
||||
const isMain = (() => {
|
||||
try {
|
||||
if (!process.argv[1]) return false;
|
||||
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (isMain) {
|
||||
const configPath = process.argv[2] || process.env.UCLAW_CONFIG_PATH;
|
||||
if (!configPath) {
|
||||
process.stderr.write('Usage: node bootstrap-xiapan.mjs <openclaw.json path>\n');
|
||||
process.exit(2);
|
||||
}
|
||||
const appRoot = process.env.UCLAW_APP_ROOT || resolve(configPath, '../../..');
|
||||
bootstrapXiapan({ configPath, appRoot })
|
||||
.then((res) => {
|
||||
process.stdout.write(`${JSON.stringify(res)}\n`);
|
||||
})
|
||||
.catch((err) => {
|
||||
process.stderr.write(`bootstrap-xiapan error: ${err.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user