From 6eb54f8ea348486425ad7d1b8056fc191624e4d5 Mon Sep 17 00:00:00 2001 From: hfshfg <38004547@qq.com> Date: Mon, 15 Jun 2026 10:52:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(portable):=20bug=20=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E4=B8=8A=E6=8A=A5=20+=20=E8=87=AA=E5=8A=A8=E5=B4=A9=E6=BA=83?= =?UTF-8?q?=E4=B8=8A=E6=8A=A5=20+=20openclaw=20=E5=8D=87=E7=BA=A7=202026.6?= =?UTF-8?q?.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- OPENCLAW_VERSION | 2 +- portable/Mac-Start.command | 17 ++ portable/Windows-Start.bat | 11 ++ portable/config-server/public/index.html | 46 +++++ portable/config-server/server.js | 31 ++++ portable/lib/report-bug.mjs | 207 +++++++++++++++++++++++ portable/setup.sh | 6 +- 7 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 portable/lib/report-bug.mjs diff --git a/OPENCLAW_VERSION b/OPENCLAW_VERSION index 8f8eb0c..1ec3607 100644 --- a/OPENCLAW_VERSION +++ b/OPENCLAW_VERSION @@ -1 +1 @@ -2026.4.29 +2026.6.6 diff --git a/portable/Mac-Start.command b/portable/Mac-Start.command index 6ea100b..db5a21d 100755 --- a/portable/Mac-Start.command +++ b/portable/Mac-Start.command @@ -103,6 +103,8 @@ if [ ! -d "$CORE_DIR/node_modules" ]; then echo " Falling back to npm install (USB drives may take 20+ min)." echo " TIP: re-download u-claw-portable-*.zip with bundled deps." cd "$CORE_DIR" + # 把 npm 缓存留在盘内,避免污染系统 ~/.npm(拔盘不留痕) + npm_config_cache="$APP_DIR/.npm-cache" \ "$NODE_BIN" "$NODE_DIR/bin/npm" install --registry=https://registry.npmmirror.com --ignore-scripts --no-audit --no-fund --omit=dev 2>&1 echo -e " ${GREEN}Dependencies installed${NC}" echo "" @@ -130,6 +132,9 @@ while lsof -i :$PORT >/dev/null 2>&1; do PORT=$((PORT + 1)) if [ $PORT -gt 18799 ]; then echo -e " ${RED}No available port (18789-18799)${NC}" + # 自动上报:端口全占,gateway 无法启动(后台、静默、失败不影响) + UCLAW_APP_ROOT="$UCLAW_DIR" "$NODE_BIN" "$UCLAW_DIR/lib/report-bug.mjs" \ + --auto --title "gateway-no-free-port" --desc "Ports 18789-18799 all in use" --root "$UCLAW_DIR" >/dev/null 2>&1 & read -p " Press Enter to exit..." exit 1 fi @@ -183,3 +188,15 @@ cleanup() { trap cleanup INT TERM wait $GW_PID +GW_EXIT=$? + +# 自动上报:gateway 异常退出。Ctrl+C 走 trap cleanup(exit 0)不会到这; +# 走到这里说明 gateway 自己退了。退出码非 0 才上报,避免噪音。后台、静默、失败不影响。 +if [ "$GW_EXIT" -ne 0 ]; then + echo -e " ${YELLOW}OpenClaw exited unexpectedly (code $GW_EXIT), reporting...${NC}" + UCLAW_APP_ROOT="$UCLAW_DIR" "$NODE_BIN" "$UCLAW_DIR/lib/report-bug.mjs" \ + --auto --title "gateway-exited-code-$GW_EXIT" --desc "Gateway exited with code $GW_EXIT on port $PORT" --root "$UCLAW_DIR" >/dev/null 2>&1 & +fi +kill $CONFIG_PID 2>/dev/null +echo "" +echo -e " 🦞 U-Claw stopped." diff --git a/portable/Windows-Start.bat b/portable/Windows-Start.bat index 836dcf0..a312e0a 100644 --- a/portable/Windows-Start.bat +++ b/portable/Windows-Start.bat @@ -78,6 +78,8 @@ if not exist "%CORE_DIR%\node_modules" ( echo File system: NTFS recommended. exFAT/FAT32 will be very slow. echo. cd /d "%CORE_DIR%" + REM 把 npm 缓存留在盘内,避免污染系统 %APPDATA%\npm-cache(拔盘不留痕) + set "npm_config_cache=%APP_DIR%\.npm-cache" call "%NPM_BIN%" install --registry=https://registry.npmmirror.com --ignore-scripts --no-audit --no-fund --omit=dev echo. echo Dependencies installed! @@ -123,6 +125,8 @@ if %errorlevel%==0 ( set /a PORT+=1 if %PORT% gtr 18799 ( echo No available port 18789-18799 + REM 自动上报:端口全被占,gateway 无法启动(detach、静默、失败不影响) + start /B "" "%NODE_BIN%" "%UCLAW_DIR%lib\report-bug.mjs" --auto --title "gateway-no-free-port" --desc "Ports 18789-18799 all in use" --root "%UCLAW_DIR%." >nul 2>&1 pause exit /b 1 ) @@ -157,7 +161,14 @@ echo. cd /d "%CORE_DIR%" set "OPENCLAW_MJS=%CORE_DIR%\node_modules\openclaw\openclaw.mjs" "%NODE_BIN%" "%OPENCLAW_MJS%" gateway run --allow-unconfigured --force --port %PORT% +set "GW_EXIT=%errorlevel%" echo. +REM 自动上报:gateway 异常退出(退出码非 0 且非 Ctrl+C/0xC000013A=-1073741510) +REM 用户正常 Ctrl+C 停止不上报,避免噪音。detach、静默、失败不影响。 +if not "%GW_EXIT%"=="0" if not "%GW_EXIT%"=="-1073741510" ( + echo OpenClaw exited unexpectedly (code %GW_EXIT%), reporting... + start /B "" "%NODE_BIN%" "%UCLAW_DIR%lib\report-bug.mjs" --auto --title "gateway-exited-code-%GW_EXIT%" --desc "Gateway exited with code %GW_EXIT% on port %PORT%" --root "%UCLAW_DIR%." >nul 2>&1 +) echo OpenClaw stopped. pause diff --git a/portable/config-server/public/index.html b/portable/config-server/public/index.html index 53efc95..8cb3c4d 100644 --- a/portable/config-server/public/index.html +++ b/portable/config-server/public/index.html @@ -410,6 +410,26 @@ input:focus { border-color: #ff6b35; } + +
+

🐛 反馈 / 报 Bug

+

遇到问题?一键反馈给我们,会自动附带版本号和最近日志(不含你的 API Key),方便我们排查。

+
+ + +
+
+ + +
+
+ + +
+
+ +
+
@@ -706,6 +726,32 @@ async function viewConfig() { } } +// --- Bug report --- +async function submitBug() { + var title = document.getElementById('bugTitle').value.trim(); + if (title.length < 3) { showToast('请填写问题标题(至少 3 个字)', true); return; } + var desc = document.getElementById('bugDesc').value.trim(); + var includeLogs = document.getElementById('bugLogs').checked; + var btn = document.getElementById('bugSubmitBtn'); + btn.disabled = true; btn.textContent = '提交中…'; + try { + var res = await fetch('/api/report-bug', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: title, description: desc, includeLogs: includeLogs }) + }); + var data = await res.json(); + if (!res.ok || !data.ok) throw new Error(data.reason || data.error || '提交失败'); + showToast('反馈已提交,编号 #' + data.id + ',谢谢!'); + document.getElementById('bugTitle').value = ''; + document.getElementById('bugDesc').value = ''; + } catch (e) { + showToast('提交失败:' + e.message, true); + } finally { + btn.disabled = false; btn.textContent = '提交反馈'; + } +} + // --- Toast --- function showToast(msg, isError) { var t = document.getElementById('toast'); diff --git a/portable/config-server/server.js b/portable/config-server/server.js index ad71940..1d4697c 100644 --- a/portable/config-server/server.js +++ b/portable/config-server/server.js @@ -524,6 +524,37 @@ const server = http.createServer((req, res) => { return; } + // API: Report a bug — user fills title/description in the web UI; this endpoint + // attaches fingerprint/version/logs locally and forwards to api.u-claw.org. + if (req.url === '/api/report-bug' && req.method === 'POST') { + let body = ''; + req.on('data', chunk => { + body += chunk; + if (body.length > 2 * 1024 * 1024) req.destroy(); // 2MB cap on user input + }); + req.on('end', () => { + (async () => { + try { + const data = body ? JSON.parse(body) : {}; + const mod = await import('../lib/report-bug.mjs'); + const portableRoot = path.join(__dirname, '..'); + const result = await mod.submitBugReport({ + title: data.title, + description: data.description, + appRoot: portableRoot, + includeLogs: data.includeLogs !== false, // 默认带日志,用户可勾掉 + }); + res.writeHead(result.ok ? 200 : 400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: false, error: err.message })); + } + })(); + }); + return; + } + // Serve static files const filePath = req.url === '/' ? path.join(__dirname, 'public/index.html') diff --git a/portable/lib/report-bug.mjs b/portable/lib/report-bug.mjs new file mode 100644 index 0000000..35a0207 --- /dev/null +++ b/portable/lib/report-bug.mjs @@ -0,0 +1,207 @@ +// 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); + }); +} diff --git a/portable/setup.sh b/portable/setup.sh index 8ac1592..000e34d 100755 --- a/portable/setup.sh +++ b/portable/setup.sh @@ -130,10 +130,10 @@ else PKGJSON fi - # Install with China mirror + # Install with China mirror(缓存留盘内,拔盘不留痕) NODE_BIN="$NODE_TARGET/bin/node" NPM_BIN="$NODE_TARGET/bin/npm" - "$NODE_BIN" "$NPM_BIN" install --prefix "$CORE_DIR" --registry="$MIRROR" + npm_config_cache="$APP_DIR/.npm-cache" "$NODE_BIN" "$NPM_BIN" install --prefix "$CORE_DIR" --registry="$MIRROR" echo -e " ${GREEN}✓${NC} OpenClaw 安装完成" fi @@ -145,7 +145,7 @@ else echo -e " ${CYAN}↓${NC} 安装 QQ 插件..." NODE_BIN="$NODE_TARGET/bin/node" NPM_BIN="$NODE_TARGET/bin/npm" - "$NODE_BIN" "$NPM_BIN" install @sliverp/qqbot@latest --prefix "$CORE_DIR" --registry="$MIRROR" 2>/dev/null || true + npm_config_cache="$APP_DIR/.npm-cache" "$NODE_BIN" "$NPM_BIN" install @sliverp/qqbot@latest --prefix "$CORE_DIR" --registry="$MIRROR" 2>/dev/null || true echo -e " ${GREEN}✓${NC} QQ 插件安装完成" fi