autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
177 lines (176 loc) • 7.83 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import winston from 'winston';
import pathGuard from '../../shared/PathGuard.js';
// Agent 系统相关标签 — 终端高亮显示
const AGENT_TAGS = [
'AgentRuntime',
'AgentFactory',
'ToolRegistry',
'SignalCollector',
'SkillAdvisor',
'CircuitBreaker',
'EventAggregator',
];
const MUTED_PREFIXES = ['Tool registered:'];
// ANSI 颜色常量 — 保证深色终端可读性
const C = {
reset: '\x1b[0m',
dim: '\x1b[2m', // 真正的 dim(用于次要信息)
bold: '\x1b[1m',
// 前景色 — 使用亮色变体,深色终端更清晰
gray: '\x1b[37m', // 白色(替代 90 暗灰)
cyan: '\x1b[96m', // 亮青
green: '\x1b[92m', // 亮绿
yellow: '\x1b[93m', // 亮黄
red: '\x1b[91m', // 亮红
magenta: '\x1b[95m', // 亮洋红
blue: '\x1b[94m', // 亮蓝
dimGray: '\x1b[2;37m', // dim 白色 — 比 90 在深色背景上更可读
};
const LEVEL_COLORS = {
error: C.red,
warn: C.yellow,
info: C.green,
debug: C.blue,
};
/**
* 静音过滤器(winston format)
* 通过 transform 返回 false 彻底丢弃匹配消息,避免空行。
* 注意:printf 返回 '' 并不会被 winston 跳过,Console transport 仍会写 '\n'。
*/
const muteFilter = winston.format((info) => {
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI escape sequence stripping
const rawLevel = info.level.replace(/\u001b\[\d+m/g, '');
if (rawLevel === 'info' && MUTED_PREFIXES.some((p) => info.message.startsWith(p))) {
return false;
}
return info;
});
/**
* 精简 Console 格式
* - Agent 相关日志: 高亮 cyan/magenta,显示完整信息
* - warn/error: 醒目颜色完整显示
* - HTTP 日志: 精简并降低视觉权重
* - 其他 info/debug: 一行精简格式
*/
const compactConsoleFormat = winston.format.printf(({ level, message, timestamp, ...meta }) => {
const ts = new Date(timestamp).toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI escape sequence stripping
const rawLevel = level.replace(/\u001b\[\d+m/g, ''); // 去 ANSI
const lc = LEVEL_COLORS[rawLevel] || C.gray;
// 判断是否为 Agent 相关日志
const isAgentLog = AGENT_TAGS.some((tag) => message.includes(tag) || message.startsWith(`[${tag}]`));
if (isAgentLog) {
// Agent 日志 — 高亮显示
const metaStr = Object.keys(meta).length > 0
? ` ${JSON.stringify(meta, null, 0).replace(/"/g, '').replace(/,/g, ', ')}`
: '';
return `${C.cyan}${ts}${C.reset} ${C.magenta}⚡ ${message}${C.reset}${metaStr ? `${C.dimGray}${metaStr}${C.reset}` : ''}`;
}
// HTTP 请求日志 — 精简格式,降低视觉权重
if (message === 'HTTP' && meta.method) {
const { method, path: reqPath, statusCode, duration } = meta;
const status = Number(statusCode);
const sc = status >= 500 ? C.red : status >= 400 ? C.yellow : C.dimGray;
const dur = parseInt(String(duration)) > 1000
? `${C.yellow}${duration}${C.reset}`
: `${C.dimGray}${duration}${C.reset}`;
return `${C.dimGray}${ts}${C.reset} ${lc}${rawLevel}${C.reset} ${C.dimGray}${method}${C.reset} ${C.gray}${reqPath}${C.reset} ${sc}${statusCode}${C.reset} ${dur}`;
}
if (rawLevel === 'warn') {
const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
return `${C.gray}${ts}${C.reset} ${C.yellow}${C.bold}warn${C.reset} ${C.yellow}${message}${C.reset}${metaStr ? `${C.dimGray}${metaStr}${C.reset}` : ''}`;
}
if (rawLevel === 'error') {
const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
return `${C.gray}${ts}${C.reset} ${C.red}${C.bold}error${C.reset} ${C.red}${message}${C.reset}${metaStr ? `${C.dimGray}${metaStr}${C.reset}` : ''}`;
}
// 普通 info/debug — 精简一行,但保证可读
return `${C.dimGray}${ts}${C.reset} ${lc}${rawLevel}${C.reset} ${C.gray}${message}${C.reset}`;
});
/**
* Logger - 统一日志系统
*
* 环境变量:
* ASD_LOG_LEVEL — 覆盖日志级别 (debug/info/warn/error)
* ASD_MCP_MODE=1 — MCP 模式下禁用 Console transport
*
* MCP 模式(ASD_MCP_MODE=1)下 Console transport 输出到 stderr 并禁用彩色,
* 避免污染 stdout JSON-RPC 通道。
*/
export class Logger {
static instance = null;
static getInstance(config = {}) {
if (!this.instance) {
const rawLogsDir = config.file?.path || './.autosnippet/logs';
// 与 DatabaseConnection 一致:相对路径按 PathGuard.projectRoot 解析,避免 MCP cwd 非项目目录时写到错误位置
const projectRoot = pathGuard.projectRoot;
const logsDir = projectRoot && !path.isAbsolute(rawLogsDir)
? path.resolve(projectRoot, rawLogsDir)
: path.resolve(rawLogsDir);
// 确保日志目录存在
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const isMcpMode = process.env.ASD_MCP_MODE === '1';
const logLevel = process.env.ASD_LOG_LEVEL || config.level || 'info';
const transports = [];
// Console transport — MCP 模式下完全禁用(任何 stderr 输出都会被 Cursor 标记为 [error])
if (config.console !== false && !isMcpMode) {
transports.push(new winston.transports.Console({
stderrLevels: ['error', 'warn', 'info', 'debug'],
format: winston.format.combine(winston.format.timestamp(), muteFilter(), compactConsoleFormat),
}));
}
// File transports
if (config.file?.enabled !== false) {
transports.push(new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
level: 'error',
format: winston.format.json(),
}));
transports.push(new winston.transports.File({
filename: path.join(logsDir, 'combined.log'),
format: winston.format.json(),
}));
// audit 独立通道 — 不受 LOG_LEVEL 影响,业务关键事件永不丢失
transports.push(new winston.transports.File({
filename: path.join(logsDir, 'audit.log'),
level: 'info',
format: winston.format.combine(winston.format((info) => {
return info.audit === true ? info : false;
})(), winston.format.timestamp(), winston.format.json()),
}));
}
this.instance = winston.createLogger({
level: logLevel,
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports,
});
}
return this.instance;
}
static debug(message, meta = {}) {
this.getInstance().debug(message, meta);
}
static info(message, meta = {}) {
this.getInstance().info(message, meta);
}
static warn(message, meta = {}) {
this.getInstance().warn(message, meta);
}
static error(message, meta = {}) {
this.getInstance().error(message, meta);
}
/** 审计日志 — 写入独立 audit.log,不受 LOG_LEVEL 控制 */
static audit(event, meta = {}) {
this.getInstance().info(event, { ...meta, audit: true });
}
}
export default Logger;