/** * Clawlink Plugin — Logger * * 统一日志模块,写入 OpenClaw 标准日志目录。 * 禁止在 Gateway 运行时使用任何 console 输出。 * * 级别控制优先级:环境变量 > 默认 DEBUG(产品发布后改为 INFO) */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/infra-runtime"; // ── 常量 ── const LOG_DIR: string = resolvePreferredOpenClawTmpDir(); const SUBSYSTEM = "gateway/channels/clawlink"; const HOSTNAME: string = os.hostname() || "unknown"; const LEVEL_IDS: Record = { TRACE: 1, DEBUG: 2, INFO: 3, WARN: 4, ERROR: 5, FATAL: 6, }; const DEFAULT_LEVEL = "DEBUG"; // ── 级别控制 ── /** * 解析最终日志级别。 * 优先级:环境变量 > 默认 DEBUG */ function resolveMinLevel(): number { const env = process.env.OPENCLAW_LOG_LEVEL?.toUpperCase(); if (env && env in LEVEL_IDS) return LEVEL_IDS[env]; return LEVEL_IDS[DEFAULT_LEVEL]; } let minLevelId: number = resolveMinLevel(); /** 运行时动态修改日志级别 */ export function setLogLevel(level: string): void { const upper = level.toUpperCase(); if (!(upper in LEVEL_IDS)) { throw new Error(`Invalid log level: ${level}. Valid: ${Object.keys(LEVEL_IDS).join(", ")}`); } minLevelId = LEVEL_IDS[upper]; } // ── 时间格式化 ── function toLocalISO(now: Date): string { const offsetMs = -now.getTimezoneOffset() * 60_000; const sign = offsetMs >= 0 ? "+" : "-"; const abs = Math.abs(now.getTimezoneOffset()); const hh = String(Math.floor(abs / 60)).padStart(2, "0"); const mm = String(abs % 60).padStart(2, "0"); return new Date(now.getTime() + offsetMs).toISOString().replace("Z", `${sign}${hh}:${mm}`); } function resolveLogPath(): string { const dateKey = toLocalISO(new Date()).slice(0, 10); return path.join(LOG_DIR, `openclaw-${dateKey}.log`); } // ── 写入 ── let logDirEnsured = false; function writeLog(level: string, message: string): void { const levelId = LEVEL_IDS[level] ?? LEVEL_IDS.INFO; if (levelId < minLevelId) return; const now = new Date(); const entry = JSON.stringify({ "0": SUBSYSTEM, "1": message, _meta: { runtime: "node", runtimeVersion: process.versions.node, hostname: HOSTNAME, name: SUBSYSTEM, parentNames: ["openclaw"], date: now.toISOString(), logLevelId: levelId, logLevelName: level, }, time: toLocalISO(now), }); try { if (!logDirEnsured) { fs.mkdirSync(LOG_DIR, { recursive: true }); logDirEnsured = true; } fs.appendFileSync(resolveLogPath(), `${entry}\n`, "utf-8"); } catch { // 写日志失败不应阻塞业务 } } // ── 导出 ── export type Logger = { trace(message: string): void; debug(message: string): void; info(message: string): void; warn(message: string): void; error(message: string): void; fatal(message: string): void; getLogFilePath(): string; }; export const logger: Logger = { trace(message: string): void { writeLog("TRACE", message); }, debug(message: string): void { writeLog("DEBUG", message); }, info(message: string): void { writeLog("INFO", message); }, warn(message: string): void { writeLog("WARN", message); }, error(message: string): void { writeLog("ERROR", message); }, fatal(message: string): void { writeLog("FATAL", message); }, getLogFilePath(): string { return resolveLogPath(); }, };