Files
OpenClaw-bot-review/app/api/gateway-logs/route.ts
T
Dan LeeandClaude Sonnet 4.6 c826424add feat: openclaw.json 自動備份、偵測損毀與還原 UI
## 備份機制
- 新增 lib/config-backup.ts:以 SHA-256 hash 偵測 openclaw.json 變更,自動備份
- Hash 持久化至 ~/.openclaw/backups/config/.last-hash,Next.js 重啟後仍能正確偵測變更
- 備份保留策略:滾動 8 個 + 昨天錨點 + 上週錨點(各取最後一個正常備份,≥ 1 KB)
- 新增 API:GET /api/config-backup(列出備份)、POST /api/config-backup(還原)

## Dashboard 偵測與還原 UI(gateway-status.tsx)
- Gateway 第一次失敗即觸發備份清單抓取,不等 30 秒
- 備份清單顯示大小、[建議] / [可能損毀] 標籤,並自動推薦第一個正常備份
- 還原後自動重啟 Gateway,並倒數 5 秒後自動重新整理頁面
- 提示使用者點選機器人卡片「測試」確認是否正常
- 「查看錯誤日誌」按鈕固定顯示於面板,可展開/收起最後 30 行 log
- config 損毀導致頁面載入失敗時,錯誤頁仍顯示 GatewayStatus 還原入口

## 多語系支援
- 所有新增文字支援繁中/简中/English(i18n.tsx 新增 8 個 key)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:15:35 +08:00

56 lines
1.7 KiB
TypeScript

import { NextResponse } from "next/server";
import { readFileSync } from "fs";
import os from "os";
import path from "path";
const LOG_PATH = path.join(os.homedir(), ".openclaw/logs/gateway.err.log");
const TAIL_LINES = 120;
const STALL_RECENT_MS = 10 * 60 * 1000; // consider stall "active" if within last 10 min
const PATTERNS = [
{ re: /Polling stall detected/, issue: "telegram_stall" },
{ re: /sendChatAction failed: Network request/, issue: "telegram_network" },
{ re: /gateway timeout after \d+ms/, issue: "subagent_timeout" },
] as const;
export async function GET() {
try {
const content = readFileSync(LOG_PATH, "utf8");
const lines = content.split("\n").filter(Boolean).slice(-TAIL_LINES);
const issues = new Set<string>();
for (const line of lines) {
for (const { re, issue } of PATTERNS) {
if (re.test(line)) issues.add(issue);
}
}
// Find timestamp of most recent stall line
let lastStallAt: string | null = null;
for (let i = lines.length - 1; i >= 0; i--) {
if (/Polling stall detected/.test(lines[i])) {
const m = lines[i].match(/^(\d{4}-\d{2}-\d{2}T[\d:.+]+)/);
if (m) { lastStallAt = m[1]; break; }
}
}
// Only treat stall as active if it happened recently
const stallActive = lastStallAt
? Date.now() - new Date(lastStallAt).getTime() < STALL_RECENT_MS
: false;
// 回傳最後 30 行作為原始紀錄供 UI 顯示
const recentLines = lines.slice(-30);
return NextResponse.json({
ok: true,
issues: [...issues],
lastStallAt,
stallActive,
recentLines,
});
} catch {
return NextResponse.json({ ok: false, issues: [], lastStallAt: null, stallActive: false });
}
}