Files
u-claw/portable/lib/report-bug.mjs
hfshfg 6eb54f8ea3 feat(portable): bug 一键上报 + 自动崩溃上报 + openclaw 升级 2026.6.6
Bug 上报(方便服务器侧排查便携版问题):
- 新增 lib/report-bug.mjs:算设备指纹→sk-uc apiKey,收集 openclaw 版本/
  系统信息/最近日志(gzip+base64),POST 到 api.u-claw.org/recharge/bug/submit。
  静默失败,绝不影响主流程。复用 fingerprint.mjs + xiapan-client.mjs。
- config-server 加 POST /api/report-bug:本地补齐信息后转发,配置中心加
  「🐛 报 Bug」表单(标题/描述/是否附日志)。
- Windows-Start.bat / Mac-Start.command:gateway 端口耗尽或异常退出时
  自动后台上报(Ctrl+C 正常停止不上报)。

openclaw 升级:
- OPENCLAW_VERSION 2026.4.29 → 2026.6.6(X 盘真实 USB 实测 gateway
  启动/ready、Dashboard 200、旧配置兼容、虾盘云 status 均通过)。

便携性改进(借鉴 OpenClaw-Windows-Portable):
- setup.sh / 启动脚本的 npm install 设 npm_config_cache 指向盘内
  app/.npm-cache,避免污染系统缓存,拔盘不留痕。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:46:42 +08:00

208 lines
7.3 KiB
JavaScript
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.

// Bug 上报客户端(便携版)
//
// 流程:
// 1. 算设备指纹 -> sk-uc- apiKey用来在服务器侧关联同一台设备的多条上报
// 2. 收集 openclaw 版本号、设备类型、系统信息、最近日志gzip+base64
// 3. POST 到 https://api.u-claw.org/recharge/bug/submit
//
// 设计原则(对齐 check-update.mjs
// - 静默失败:上报失败只 console.error绝不影响 OpenClaw 主流程
// - 异步:调用方应 detach 跑Windows-Start.bat 用 start /B不阻塞启动
// - 无第三方依赖fetch + node:zlib 都是内置
//
// 两种用法:
// 手动(网页一键,由 config-server 转发import { submitBugReport } from './report-bug.mjs'
// 自动崩溃node lib/report-bug.mjs --auto --title "gateway-start-failed" [--log <文件>]
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { gzipSync } from 'node:zlib';
import { resolve, join } from 'node:path';
import { platform, release, arch, tmpdir } from 'node:os';
import { pathToFileURL } from 'node:url';
import { getFingerprint } from './fingerprint.mjs';
import { buildApiKey } from './xiapan-client.mjs';
const DEFAULT_API_BASE = 'https://api.u-claw.org/v1';
const REQUEST_TIMEOUT_MS = 10_000;
const MAX_LOG_BYTES = 256 * 1024; // 只取日志尾部 256KB避免上报体过大
function log(level, msg) {
const stream = level === 'error' ? process.stderr : process.stdout;
stream.write(`[report-bug] ${msg}\n`);
}
// API base 默认带 /v1虾盘云 API 中转bug 上报走独立的 /bug 路径,所以去掉尾部 /v1。
// 例https://api.u-claw.org/v1 -> https://api.u-claw.org/bug/submit
function getSubmitUrl() {
const base = (process.env.UCLAW_CLOUD_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, '');
const root = base.replace(/\/v1$/, '');
return `${root}/bug/submit`;
}
async function fetchWithTimeout(url, init) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
return await fetch(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
// 读 OPENCLAW_VERSION优先环境变量再找盘内文件
function readOpenclawVersion(appRoot) {
if (process.env.OPENCLAW_VERSION) return process.env.OPENCLAW_VERSION.trim();
const candidates = [
process.env.UCLAW_VERSION_FILE,
appRoot && resolve(appRoot, 'OPENCLAW_VERSION'),
appRoot && resolve(appRoot, '..', 'OPENCLAW_VERSION'),
].filter(Boolean);
for (const p of candidates) {
try {
if (existsSync(p)) {
const v = readFileSync(p, 'utf8').trim();
if (v) return v;
}
} catch { /* 静默 */ }
}
return null;
}
function collectSystemInfo() {
return JSON.stringify({
platform: platform(),
arch: arch(),
osRelease: release(),
node: process.version,
time: new Date().toISOString(),
});
}
// 读最近一份 openclaw 日志的尾部gzip+base64。失败返回 null。
// 日志默认在 %LOCALAPPDATA%\Temp\openclaw\ 或 $TMPDIR/openclaw/,文件名形如 openclaw-{date}.log
function collectLogsB64(explicitLogPath) {
try {
let logFile = null;
if (explicitLogPath && existsSync(explicitLogPath)) {
logFile = explicitLogPath;
} else {
const logDir = process.env.OPENCLAW_LOG_DIR || join(tmpdir(), 'openclaw');
if (existsSync(logDir)) {
const logs = readdirSync(logDir)
.filter((f) => /\.log$/i.test(f))
.map((f) => join(logDir, f))
.map((p) => ({ p, m: safeMtime(p) }))
.filter((x) => x.m > 0)
.sort((a, b) => b.m - a.m);
if (logs.length) logFile = logs[0].p;
}
}
if (!logFile) return null;
let raw = readFileSync(logFile);
if (raw.length > MAX_LOG_BYTES) raw = raw.subarray(raw.length - MAX_LOG_BYTES);
return gzipSync(raw).toString('base64');
} catch (err) {
log('error', `collect logs failed: ${err.message}`);
return null;
}
}
function safeMtime(p) {
try { return statSync(p).mtimeMs; } catch { return 0; }
}
/**
* 上报一个 bug。所有现场信息指纹/版本/系统/日志)由本函数自动补齐,
* 调用方只需给 title必填和 description可选
*
* @param {object} opts
* @param {string} opts.title 必填bug 标题
* @param {string} [opts.description] 描述(用户填写或堆栈)
* @param {string} [opts.appRoot] 便携版根目录,用于算指纹和找版本文件,默认 cwd
* @param {string} [opts.logPath] 指定日志文件;不给则自动找最近的 openclaw 日志
* @param {boolean}[opts.includeLogs] 是否附带日志,默认 true
* @returns {Promise<{ok:boolean, id?:number, reason?:string}>}
*/
export async function submitBugReport(opts = {}) {
const { title, description, appRoot, logPath, includeLogs = true } = opts;
if (!title || typeof title !== 'string' || title.trim().length < 3) {
return { ok: false, reason: 'title-required' };
}
const root = appRoot || process.cwd();
// 指纹失败不阻止上报,匿名提交即可
let api_key = null;
let device_source = null;
try {
const fp = await getFingerprint(root);
device_source = fp.source;
api_key = buildApiKey(fp.fingerprint);
} catch (err) {
log('error', `fingerprint failed (上报为匿名): ${err.message}`);
}
const payload = {
api_key,
device_source,
openclaw_version: readOpenclawVersion(root),
title: title.trim(),
description: description || null,
logs_b64: includeLogs ? collectLogsB64(logPath) : null,
system_info: collectSystemInfo(),
};
try {
const res = await fetchWithTimeout(getSubmitUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
log('error', `submit failed: HTTP ${res.status} ${data.error || ''}`);
return { ok: false, reason: data.error || `http-${res.status}` };
}
log('info', `submitted bug #${data.id}`);
return { ok: true, id: data.id };
} catch (err) {
log('error', `submit failed: ${err.message}`);
return { ok: false, reason: err.message };
}
}
// CLI 入口(自动崩溃上报用):
// node report-bug.mjs --auto --title "gateway-start-failed" [--desc "..."] [--log <文件>] [--root <便携版根>]
const isMain = (() => {
try {
if (!process.argv[1]) return false;
return import.meta.url === pathToFileURL(process.argv[1]).href;
} catch {
return false;
}
})();
if (isMain) {
const args = process.argv.slice(2);
const getArg = (name) => {
const i = args.indexOf(name);
return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
};
const title = getArg('--title') || 'auto-report';
const description = getArg('--desc');
const logPath = getArg('--log');
const appRoot = getArg('--root') || process.env.UCLAW_APP_ROOT || process.cwd();
submitBugReport({ title, description, appRoot, logPath })
.then((res) => {
process.stdout.write(`${JSON.stringify(res)}\n`);
process.exit(res.ok ? 0 : 1);
})
.catch((err) => {
// 即便崩了也别让上报本身把启动脚本拖挂
process.stderr.write(`report-bug fatal: ${err && err.message ? err.message : err}\n`);
process.exit(1);
});
}