1235 lines
36 KiB
TypeScript
1235 lines
36 KiB
TypeScript
import { existsSync, mkdirSync, appendFileSync } from 'fs'
|
||
import { join } from 'path'
|
||
import { randomUUID } from 'crypto'
|
||
import chalk from 'chalk'
|
||
import envPaths from 'env-paths'
|
||
import { PRODUCT_COMMAND } from '../constants/product'
|
||
import { SESSION_ID } from './log'
|
||
import type { Message } from '../types/conversation'
|
||
|
||
// 调试日志级别
|
||
export enum LogLevel {
|
||
TRACE = 'TRACE',
|
||
DEBUG = 'DEBUG',
|
||
INFO = 'INFO',
|
||
WARN = 'WARN',
|
||
ERROR = 'ERROR',
|
||
FLOW = 'FLOW',
|
||
API = 'API',
|
||
STATE = 'STATE',
|
||
REMINDER = 'REMINDER', // 新增:系统提醒事件
|
||
}
|
||
|
||
// 调试模式检测
|
||
const isDebugMode = () =>
|
||
process.argv.includes('--debug') || process.argv.includes('--debug-verbose')
|
||
const isVerboseMode = () => process.argv.includes('--verbose')
|
||
const isDebugVerboseMode = () => process.argv.includes('--debug-verbose')
|
||
|
||
// 终端日志级别配置 - 显示关键信息
|
||
const TERMINAL_LOG_LEVELS = new Set([
|
||
LogLevel.ERROR,
|
||
LogLevel.WARN,
|
||
LogLevel.INFO, // 添加 INFO 级别,显示关键系统状态
|
||
LogLevel.REMINDER, // 系统提醒事件,用户应该看到
|
||
])
|
||
|
||
// 在调试详细模式下显示更多日志级别
|
||
const DEBUG_VERBOSE_TERMINAL_LOG_LEVELS = new Set([
|
||
LogLevel.ERROR,
|
||
LogLevel.WARN,
|
||
LogLevel.FLOW,
|
||
LogLevel.API,
|
||
LogLevel.STATE,
|
||
LogLevel.INFO,
|
||
LogLevel.REMINDER, // 系统提醒在详细模式下也显示
|
||
])
|
||
|
||
// 用户友好的日志级别 - 简化的高级日志
|
||
const USER_FRIENDLY_LEVELS = new Set([
|
||
'SESSION_START',
|
||
'QUERY_START',
|
||
'QUERY_PROGRESS',
|
||
'QUERY_COMPLETE',
|
||
'TOOL_EXECUTION',
|
||
'ERROR_OCCURRED',
|
||
'PERFORMANCE_SUMMARY',
|
||
])
|
||
|
||
// 启动时间戳用于文件命名
|
||
const STARTUP_TIMESTAMP = new Date().toISOString().replace(/[:.]/g, '-')
|
||
const REQUEST_START_TIME = Date.now()
|
||
|
||
// 路径配置
|
||
const paths = envPaths(PRODUCT_COMMAND)
|
||
function getProjectDir(cwd: string): string {
|
||
return cwd.replace(/[^a-zA-Z0-9]/g, '-')
|
||
}
|
||
|
||
const DEBUG_PATHS = {
|
||
base: () => join(paths.cache, getProjectDir(process.cwd()), 'debug'),
|
||
detailed: () => join(DEBUG_PATHS.base(), `${STARTUP_TIMESTAMP}-detailed.log`),
|
||
flow: () => join(DEBUG_PATHS.base(), `${STARTUP_TIMESTAMP}-flow.log`),
|
||
api: () => join(DEBUG_PATHS.base(), `${STARTUP_TIMESTAMP}-api.log`),
|
||
state: () => join(DEBUG_PATHS.base(), `${STARTUP_TIMESTAMP}-state.log`),
|
||
}
|
||
|
||
// 确保调试目录存在
|
||
function ensureDebugDir() {
|
||
const debugDir = DEBUG_PATHS.base()
|
||
if (!existsSync(debugDir)) {
|
||
mkdirSync(debugDir, { recursive: true })
|
||
}
|
||
}
|
||
|
||
// 日志条目接口
|
||
interface LogEntry {
|
||
timestamp: string
|
||
level: LogLevel
|
||
phase: string
|
||
requestId?: string
|
||
data: any
|
||
elapsed?: number
|
||
}
|
||
|
||
// 当前请求上下文
|
||
class RequestContext {
|
||
public readonly id: string
|
||
public readonly startTime: number
|
||
private phases: Map<string, number> = new Map()
|
||
|
||
constructor() {
|
||
this.id = randomUUID().slice(0, 8)
|
||
this.startTime = Date.now()
|
||
}
|
||
|
||
markPhase(phase: string) {
|
||
this.phases.set(phase, Date.now() - this.startTime)
|
||
}
|
||
|
||
getPhaseTime(phase: string): number {
|
||
return this.phases.get(phase) || 0
|
||
}
|
||
|
||
getAllPhases(): Record<string, number> {
|
||
return Object.fromEntries(this.phases)
|
||
}
|
||
}
|
||
|
||
// 全局请求上下文管理
|
||
const activeRequests = new Map<string, RequestContext>()
|
||
let currentRequest: RequestContext | null = null
|
||
|
||
// 核心日志记录函数
|
||
function writeToFile(filePath: string, entry: LogEntry) {
|
||
if (!isDebugMode()) return
|
||
|
||
try {
|
||
ensureDebugDir()
|
||
const logLine =
|
||
JSON.stringify(
|
||
{
|
||
...entry,
|
||
sessionId: SESSION_ID,
|
||
pid: process.pid,
|
||
uptime: Date.now() - REQUEST_START_TIME,
|
||
},
|
||
null,
|
||
2,
|
||
) + ',\n'
|
||
|
||
appendFileSync(filePath, logLine)
|
||
} catch (error) {
|
||
// 静默失败,避免调试日志影响主功能
|
||
}
|
||
}
|
||
|
||
// 日志去重机制
|
||
const recentLogs = new Map<string, number>()
|
||
const LOG_DEDUPE_WINDOW_MS = 5000 // 5秒内相同日志视为重复
|
||
|
||
// 生成日志去重键
|
||
function getDedupeKey(level: LogLevel, phase: string, data: any): string {
|
||
// 对于配置相关的日志,使用文件路径和操作类型作为键
|
||
if (phase.startsWith('CONFIG_')) {
|
||
const file = data?.file || ''
|
||
return `${level}:${phase}:${file}`
|
||
}
|
||
|
||
// 对于其他日志,使用阶段作为键
|
||
return `${level}:${phase}`
|
||
}
|
||
|
||
// 检查是否应该记录日志(去重)
|
||
function shouldLogWithDedupe(
|
||
level: LogLevel,
|
||
phase: string,
|
||
data: any,
|
||
): boolean {
|
||
const key = getDedupeKey(level, phase, data)
|
||
const now = Date.now()
|
||
const lastLogTime = recentLogs.get(key)
|
||
|
||
// 如果是第一次记录,或者超过去重时间窗口,则允许记录
|
||
if (!lastLogTime || now - lastLogTime > LOG_DEDUPE_WINDOW_MS) {
|
||
recentLogs.set(key, now)
|
||
|
||
// 清理过期的日志记录
|
||
for (const [oldKey, oldTime] of recentLogs.entries()) {
|
||
if (now - oldTime > LOG_DEDUPE_WINDOW_MS) {
|
||
recentLogs.delete(oldKey)
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
function formatMessages(messages: any): string {
|
||
if (Array.isArray(messages)) {
|
||
// 只显示最近 5 条消息
|
||
const recentMessages = messages.slice(-5)
|
||
return recentMessages
|
||
.map((msg, index) => {
|
||
const role = msg.role || 'unknown'
|
||
let content = ''
|
||
|
||
if (typeof msg.content === 'string') {
|
||
// 每条消息最长 300 字符,超出省略
|
||
content =
|
||
msg.content.length > 300
|
||
? msg.content.substring(0, 300) + '...'
|
||
: msg.content
|
||
} else if (typeof msg.content === 'object') {
|
||
content = '[complex_content]'
|
||
} else {
|
||
content = String(msg.content || '')
|
||
}
|
||
|
||
const totalIndex = messages.length - recentMessages.length + index
|
||
return `[${totalIndex}] ${chalk.dim(role)}: ${content}`
|
||
})
|
||
.join('\n ')
|
||
}
|
||
|
||
if (typeof messages === 'string') {
|
||
try {
|
||
const parsed = JSON.parse(messages)
|
||
if (Array.isArray(parsed)) {
|
||
return formatMessages(parsed) // 递归处理解析后的数组
|
||
}
|
||
} catch {
|
||
// 如果解析失败,返回截断的字符串
|
||
}
|
||
}
|
||
|
||
// 对于非消息数组的长字符串,也进行截断
|
||
if (typeof messages === 'string' && messages.length > 200) {
|
||
return messages.substring(0, 200) + '...'
|
||
}
|
||
|
||
return typeof messages === 'string' ? messages : JSON.stringify(messages)
|
||
}
|
||
|
||
// 判断是否应该在终端显示日志
|
||
function shouldShowInTerminal(level: LogLevel): boolean {
|
||
if (!isDebugMode()) return false
|
||
|
||
// 在调试详细模式下显示更多日志级别
|
||
if (isDebugVerboseMode()) {
|
||
return DEBUG_VERBOSE_TERMINAL_LOG_LEVELS.has(level)
|
||
}
|
||
|
||
// 默认只显示错误和警告
|
||
return TERMINAL_LOG_LEVELS.has(level)
|
||
}
|
||
|
||
// 终端彩色输出
|
||
function logToTerminal(entry: LogEntry) {
|
||
// 使用新的过滤逻辑
|
||
if (!shouldShowInTerminal(entry.level)) return
|
||
|
||
const { level, phase, data, requestId, elapsed } = entry
|
||
const timestamp = new Date().toISOString().slice(11, 23) // HH:mm:ss.SSS
|
||
|
||
let prefix = ''
|
||
let color = chalk.gray
|
||
|
||
switch (level) {
|
||
case LogLevel.FLOW:
|
||
prefix = '🔄'
|
||
color = chalk.cyan
|
||
break
|
||
case LogLevel.API:
|
||
prefix = '🌐'
|
||
color = chalk.yellow
|
||
break
|
||
case LogLevel.STATE:
|
||
prefix = '📊'
|
||
color = chalk.blue
|
||
break
|
||
case LogLevel.ERROR:
|
||
prefix = '❌'
|
||
color = chalk.red
|
||
break
|
||
case LogLevel.WARN:
|
||
prefix = '⚠️'
|
||
color = chalk.yellow
|
||
break
|
||
case LogLevel.INFO:
|
||
prefix = 'ℹ️'
|
||
color = chalk.green
|
||
break
|
||
case LogLevel.TRACE:
|
||
prefix = '📈'
|
||
color = chalk.magenta
|
||
break
|
||
default:
|
||
prefix = '🔍'
|
||
color = chalk.gray
|
||
}
|
||
|
||
const reqId = requestId ? chalk.dim(`[${requestId}]`) : ''
|
||
const elapsedStr = elapsed !== undefined ? chalk.dim(`+${elapsed}ms`) : ''
|
||
|
||
// 特殊处理一些数据格式
|
||
let dataStr = ''
|
||
if (typeof data === 'object' && data !== null) {
|
||
if (data.messages) {
|
||
// 格式化消息数组
|
||
const formattedMessages = formatMessages(data.messages)
|
||
dataStr = JSON.stringify(
|
||
{
|
||
...data,
|
||
messages: `\n ${formattedMessages}`,
|
||
},
|
||
null,
|
||
2,
|
||
)
|
||
} else {
|
||
dataStr = JSON.stringify(data, null, 2)
|
||
}
|
||
} else {
|
||
dataStr = typeof data === 'string' ? data : JSON.stringify(data)
|
||
}
|
||
|
||
console.log(
|
||
`${color(`[${timestamp}]`)} ${prefix} ${color(phase)} ${reqId} ${dataStr} ${elapsedStr}`,
|
||
)
|
||
}
|
||
|
||
// 主要调试日志函数
|
||
export function debugLog(
|
||
level: LogLevel,
|
||
phase: string,
|
||
data: any,
|
||
requestId?: string,
|
||
) {
|
||
if (!isDebugMode()) return
|
||
|
||
// 检查是否应该记录(去重检查)
|
||
if (!shouldLogWithDedupe(level, phase, data)) {
|
||
return // 跳过重复的日志
|
||
}
|
||
|
||
const entry: LogEntry = {
|
||
timestamp: new Date().toISOString(),
|
||
level,
|
||
phase,
|
||
data,
|
||
requestId: requestId || currentRequest?.id,
|
||
elapsed: currentRequest ? Date.now() - currentRequest.startTime : undefined,
|
||
}
|
||
|
||
// 写入对应的日志文件
|
||
writeToFile(DEBUG_PATHS.detailed(), entry)
|
||
|
||
switch (level) {
|
||
case LogLevel.FLOW:
|
||
writeToFile(DEBUG_PATHS.flow(), entry)
|
||
break
|
||
case LogLevel.API:
|
||
writeToFile(DEBUG_PATHS.api(), entry)
|
||
break
|
||
case LogLevel.STATE:
|
||
writeToFile(DEBUG_PATHS.state(), entry)
|
||
break
|
||
}
|
||
|
||
// 终端输出(也会被过滤)
|
||
logToTerminal(entry)
|
||
}
|
||
|
||
// 便捷的日志函数
|
||
export const debug = {
|
||
flow: (phase: string, data: any, requestId?: string) =>
|
||
debugLog(LogLevel.FLOW, phase, data, requestId),
|
||
|
||
api: (phase: string, data: any, requestId?: string) =>
|
||
debugLog(LogLevel.API, phase, data, requestId),
|
||
|
||
state: (phase: string, data: any, requestId?: string) =>
|
||
debugLog(LogLevel.STATE, phase, data, requestId),
|
||
|
||
info: (phase: string, data: any, requestId?: string) =>
|
||
debugLog(LogLevel.INFO, phase, data, requestId),
|
||
|
||
warn: (phase: string, data: any, requestId?: string) =>
|
||
debugLog(LogLevel.WARN, phase, data, requestId),
|
||
|
||
error: (phase: string, data: any, requestId?: string) =>
|
||
debugLog(LogLevel.ERROR, phase, data, requestId),
|
||
|
||
trace: (phase: string, data: any, requestId?: string) =>
|
||
debugLog(LogLevel.TRACE, phase, data, requestId),
|
||
|
||
// 新增UI相关的调试函数 (只记录到文件,不显示在终端)
|
||
ui: (phase: string, data: any, requestId?: string) =>
|
||
debugLog(LogLevel.STATE, `UI_${phase}`, data, requestId),
|
||
|
||
// 新增Statsig事件追踪
|
||
statsig: (phase: string, data: any) => debugLog(LogLevel.TRACE, phase, data),
|
||
}
|
||
|
||
// 请求生命周期管理
|
||
export function startRequest(): RequestContext {
|
||
const ctx = new RequestContext()
|
||
currentRequest = ctx
|
||
activeRequests.set(ctx.id, ctx)
|
||
|
||
debug.flow('REQUEST_START', {
|
||
requestId: ctx.id,
|
||
activeRequests: activeRequests.size,
|
||
})
|
||
|
||
return ctx
|
||
}
|
||
|
||
export function endRequest(ctx?: RequestContext) {
|
||
const request = ctx || currentRequest
|
||
if (!request) return
|
||
|
||
debug.flow('REQUEST_END', {
|
||
requestId: request.id,
|
||
totalTime: Date.now() - request.startTime,
|
||
phases: request.getAllPhases(),
|
||
})
|
||
|
||
activeRequests.delete(request.id)
|
||
if (currentRequest === request) {
|
||
currentRequest = null
|
||
}
|
||
}
|
||
|
||
export function getCurrentRequest(): RequestContext | null {
|
||
return currentRequest
|
||
}
|
||
|
||
// 阶段标记函数
|
||
export function markPhase(phase: string, data?: any) {
|
||
if (!currentRequest) return
|
||
|
||
currentRequest.markPhase(phase)
|
||
debug.flow(`PHASE_${phase.toUpperCase()}`, {
|
||
requestId: currentRequest.id,
|
||
elapsed: currentRequest.getPhaseTime(phase),
|
||
data,
|
||
})
|
||
}
|
||
|
||
// 新增:Reminder 事件日志记录
|
||
export function logReminderEvent(
|
||
eventType: string,
|
||
reminderData: any,
|
||
agentId?: string,
|
||
) {
|
||
if (!isDebugMode()) return
|
||
|
||
debug.info('REMINDER_EVENT_TRIGGERED', {
|
||
eventType,
|
||
agentId: agentId || 'default',
|
||
reminderType: reminderData.type || 'unknown',
|
||
reminderCategory: reminderData.category || 'general',
|
||
reminderPriority: reminderData.priority || 'medium',
|
||
contentLength: reminderData.content ? reminderData.content.length : 0,
|
||
timestamp: Date.now(),
|
||
})
|
||
}
|
||
|
||
// API错误日志功能
|
||
export function logAPIError(context: {
|
||
model: string
|
||
endpoint: string
|
||
status: number
|
||
error: any
|
||
request?: any
|
||
response?: any
|
||
provider?: string
|
||
}) {
|
||
const errorDir = join(paths.cache, getProjectDir(process.cwd()), 'logs', 'error', 'api')
|
||
|
||
// 确保目录存在
|
||
if (!existsSync(errorDir)) {
|
||
try {
|
||
mkdirSync(errorDir, { recursive: true })
|
||
} catch (err) {
|
||
console.error('Failed to create error log directory:', err)
|
||
return // Exit early if we can't create the directory
|
||
}
|
||
}
|
||
|
||
// 生成文件名
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||
const sanitizedModel = context.model.replace(/[^a-zA-Z0-9-_]/g, '_')
|
||
const filename = `${sanitizedModel}_${timestamp}.log`
|
||
const filepath = join(errorDir, filename)
|
||
|
||
// 准备完整的日志内容(文件中保存所有信息)
|
||
const fullLogContent = {
|
||
timestamp: new Date().toISOString(),
|
||
sessionId: SESSION_ID,
|
||
requestId: getCurrentRequest()?.id,
|
||
model: context.model,
|
||
provider: context.provider,
|
||
endpoint: context.endpoint,
|
||
status: context.status,
|
||
error: context.error,
|
||
request: context.request, // 保存完整请求
|
||
response: context.response, // 保存完整响应
|
||
environment: {
|
||
nodeVersion: process.version,
|
||
platform: process.platform,
|
||
cwd: process.cwd(),
|
||
}
|
||
}
|
||
|
||
// 写入文件(保存完整信息)
|
||
try {
|
||
appendFileSync(filepath, JSON.stringify(fullLogContent, null, 2) + '\n')
|
||
appendFileSync(filepath, '='.repeat(80) + '\n\n')
|
||
} catch (err) {
|
||
console.error('Failed to write API error log:', err)
|
||
}
|
||
|
||
// 在调试模式下记录到系统日志
|
||
if (isDebugMode()) {
|
||
debug.error('API_ERROR', {
|
||
model: context.model,
|
||
status: context.status,
|
||
error: typeof context.error === 'string' ? context.error : context.error?.message || 'Unknown error',
|
||
endpoint: context.endpoint,
|
||
logFile: filename,
|
||
})
|
||
}
|
||
|
||
// 优雅的终端显示(仅在verbose模式下)
|
||
if (isVerboseMode() || isDebugVerboseMode()) {
|
||
console.log()
|
||
console.log(chalk.red('━'.repeat(60)))
|
||
console.log(chalk.red.bold('⚠️ API Error'))
|
||
console.log(chalk.red('━'.repeat(60)))
|
||
|
||
// 显示关键信息
|
||
console.log(chalk.white(' Model: ') + chalk.yellow(context.model))
|
||
console.log(chalk.white(' Status: ') + chalk.red(context.status))
|
||
|
||
// 格式化错误消息
|
||
let errorMessage = 'Unknown error'
|
||
if (typeof context.error === 'string') {
|
||
errorMessage = context.error
|
||
} else if (context.error?.message) {
|
||
errorMessage = context.error.message
|
||
} else if (context.error?.error?.message) {
|
||
errorMessage = context.error.error.message
|
||
}
|
||
|
||
// 错误消息换行显示
|
||
console.log(chalk.white(' Error: ') + chalk.red(errorMessage))
|
||
|
||
// 如果有响应体,显示格式化的响应
|
||
if (context.response) {
|
||
console.log()
|
||
console.log(chalk.gray(' Response:'))
|
||
const responseStr = typeof context.response === 'string'
|
||
? context.response
|
||
: JSON.stringify(context.response, null, 2)
|
||
|
||
// 缩进显示响应内容
|
||
responseStr.split('\n').forEach(line => {
|
||
console.log(chalk.gray(' ' + line))
|
||
})
|
||
}
|
||
|
||
console.log()
|
||
console.log(chalk.dim(` 📁 Full log: ${filepath}`))
|
||
console.log(chalk.red('━'.repeat(60)))
|
||
console.log()
|
||
}
|
||
}
|
||
|
||
// 新增:LLM 交互核心调试信息
|
||
export function logLLMInteraction(context: {
|
||
systemPrompt: string
|
||
messages: any[]
|
||
response: any
|
||
usage?: { inputTokens: number; outputTokens: number }
|
||
timing: { start: number; end: number }
|
||
apiFormat?: 'anthropic' | 'openai'
|
||
}) {
|
||
if (!isDebugMode()) return
|
||
|
||
const duration = context.timing.end - context.timing.start
|
||
|
||
console.log('\n' + chalk.blue('🧠 LLM CALL DEBUG'))
|
||
console.log(chalk.gray('━'.repeat(60)))
|
||
|
||
// 显示上下文基本信息
|
||
console.log(chalk.yellow('📊 Context Overview:'))
|
||
console.log(` Messages Count: ${context.messages.length}`)
|
||
console.log(` System Prompt Length: ${context.systemPrompt.length} chars`)
|
||
console.log(` Duration: ${duration.toFixed(0)}ms`)
|
||
|
||
if (context.usage) {
|
||
console.log(
|
||
` Token Usage: ${context.usage.inputTokens} → ${context.usage.outputTokens}`,
|
||
)
|
||
}
|
||
|
||
// 显示真实发送给 LLM API 的 messages(完整还原API调用)
|
||
const apiLabel = context.apiFormat
|
||
? ` (${context.apiFormat.toUpperCase()})`
|
||
: ''
|
||
console.log(chalk.cyan(`\n💬 Real API Messages${apiLabel} (last 10):`))
|
||
|
||
// 这里展示的是真正发送给LLM API的messages,不是内部处理的版本
|
||
const recentMessages = context.messages.slice(-10)
|
||
recentMessages.forEach((msg, index) => {
|
||
const globalIndex = context.messages.length - recentMessages.length + index
|
||
const roleColor =
|
||
msg.role === 'user'
|
||
? 'green'
|
||
: msg.role === 'assistant'
|
||
? 'blue'
|
||
: msg.role === 'system'
|
||
? 'yellow'
|
||
: 'gray'
|
||
|
||
let content = ''
|
||
let isReminder = false
|
||
|
||
if (typeof msg.content === 'string') {
|
||
// 检查是否是 system-reminder
|
||
if (msg.content.includes('<system-reminder>')) {
|
||
isReminder = true
|
||
// 提取 reminder 的核心内容,显示更多字符,记得加省略号
|
||
const reminderContent = msg.content
|
||
.replace(/<\/?system-reminder>/g, '')
|
||
.trim()
|
||
content = `🔔 ${reminderContent.length > 800 ? reminderContent.substring(0, 800) + '...' : reminderContent}`
|
||
} else {
|
||
// 增加普通消息的显示字符数 - 用户消息和系统消息显示更多
|
||
const maxLength =
|
||
msg.role === 'user' ? 1000 : msg.role === 'system' ? 1200 : 800
|
||
content =
|
||
msg.content.length > maxLength
|
||
? msg.content.substring(0, maxLength) + '...'
|
||
: msg.content
|
||
}
|
||
} else if (Array.isArray(msg.content)) {
|
||
// Anthropic格式:content是对象数组
|
||
const textBlocks = msg.content.filter(
|
||
(block: any) => block.type === 'text',
|
||
)
|
||
const toolBlocks = msg.content.filter(
|
||
(block: any) => block.type === 'tool_use',
|
||
)
|
||
if (textBlocks.length > 0) {
|
||
const text = textBlocks[0].text || ''
|
||
// Assistant消息显示更多内容
|
||
const maxLength = msg.role === 'assistant' ? 1000 : 800
|
||
content =
|
||
text.length > maxLength ? text.substring(0, maxLength) + '...' : text
|
||
}
|
||
if (toolBlocks.length > 0) {
|
||
content += ` [+ ${toolBlocks.length} tool calls]`
|
||
}
|
||
if (textBlocks.length === 0 && toolBlocks.length === 0) {
|
||
content = `[${msg.content.length} blocks: ${msg.content.map(b => b.type || 'unknown').join(', ')}]`
|
||
}
|
||
} else {
|
||
content = '[complex_content]'
|
||
}
|
||
|
||
// 根据消息类型使用不同的显示样式 - 更友好的视觉格式
|
||
if (isReminder) {
|
||
console.log(
|
||
` [${globalIndex}] ${chalk.magenta('🔔 REMINDER')}: ${chalk.dim(content)}`,
|
||
)
|
||
} else {
|
||
// 为不同角色添加图标
|
||
const roleIcon =
|
||
msg.role === 'user'
|
||
? '👤'
|
||
: msg.role === 'assistant'
|
||
? '🤖'
|
||
: msg.role === 'system'
|
||
? '⚙️'
|
||
: '📄'
|
||
console.log(
|
||
` [${globalIndex}] ${(chalk as any)[roleColor](roleIcon + ' ' + msg.role.toUpperCase())}: ${content}`,
|
||
)
|
||
}
|
||
|
||
// 显示工具调用信息(Anthropic格式)- 更清晰的格式
|
||
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
||
const toolCalls = msg.content.filter(
|
||
(block: any) => block.type === 'tool_use',
|
||
)
|
||
if (toolCalls.length > 0) {
|
||
console.log(
|
||
chalk.cyan(
|
||
` 🔧 → Tool calls (${toolCalls.length}): ${toolCalls.map((t: any) => t.name).join(', ')}`,
|
||
),
|
||
)
|
||
// 显示每个工具的详细参数
|
||
toolCalls.forEach((tool: any, idx: number) => {
|
||
const inputStr = JSON.stringify(tool.input || {})
|
||
const maxLength = 200
|
||
const displayInput =
|
||
inputStr.length > maxLength
|
||
? inputStr.substring(0, maxLength) + '...'
|
||
: inputStr
|
||
console.log(
|
||
chalk.dim(` [${idx}] ${tool.name}: ${displayInput}`),
|
||
)
|
||
})
|
||
}
|
||
}
|
||
// OpenAI格式的工具调用
|
||
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
||
console.log(
|
||
chalk.cyan(
|
||
` 🔧 → Tool calls (${msg.tool_calls.length}): ${msg.tool_calls.map((t: any) => t.function.name).join(', ')}`,
|
||
),
|
||
)
|
||
msg.tool_calls.forEach((tool: any, idx: number) => {
|
||
const inputStr = tool.function.arguments || '{}'
|
||
const maxLength = 200
|
||
const displayInput =
|
||
inputStr.length > maxLength
|
||
? inputStr.substring(0, maxLength) + '...'
|
||
: inputStr
|
||
console.log(
|
||
chalk.dim(` [${idx}] ${tool.function.name}: ${displayInput}`),
|
||
)
|
||
})
|
||
}
|
||
})
|
||
|
||
// 显示 LLM 响应核心信息 - 更详细友好的格式
|
||
console.log(chalk.magenta('\n🤖 LLM Response:'))
|
||
|
||
// Handle different response formats (Anthropic vs OpenAI)
|
||
let responseContent = ''
|
||
let toolCalls: any[] = []
|
||
|
||
if (Array.isArray(context.response.content)) {
|
||
// Anthropic format: content is array of blocks
|
||
const textBlocks = context.response.content.filter(
|
||
(block: any) => block.type === 'text',
|
||
)
|
||
responseContent = textBlocks.length > 0 ? textBlocks[0].text || '' : ''
|
||
toolCalls = context.response.content.filter(
|
||
(block: any) => block.type === 'tool_use',
|
||
)
|
||
} else if (typeof context.response.content === 'string') {
|
||
// OpenAI format: content might be string
|
||
responseContent = context.response.content
|
||
// Tool calls are separate in OpenAI format
|
||
toolCalls = context.response.tool_calls || []
|
||
} else {
|
||
responseContent = JSON.stringify(context.response.content || '')
|
||
}
|
||
|
||
// 显示更多响应内容
|
||
const maxResponseLength = 1000
|
||
const displayContent =
|
||
responseContent.length > maxResponseLength
|
||
? responseContent.substring(0, maxResponseLength) + '...'
|
||
: responseContent
|
||
console.log(` Content: ${displayContent}`)
|
||
|
||
if (toolCalls.length > 0) {
|
||
const toolNames = toolCalls.map(
|
||
(t: any) => t.name || t.function?.name || 'unknown',
|
||
)
|
||
console.log(
|
||
chalk.cyan(
|
||
` 🔧 Tool Calls (${toolCalls.length}): ${toolNames.join(', ')}`,
|
||
),
|
||
)
|
||
toolCalls.forEach((tool: any, index: number) => {
|
||
const toolName = tool.name || tool.function?.name || 'unknown'
|
||
const toolInput = tool.input || tool.function?.arguments || '{}'
|
||
const inputStr =
|
||
typeof toolInput === 'string' ? toolInput : JSON.stringify(toolInput)
|
||
// 显示更多工具参数内容
|
||
const maxToolInputLength = 300
|
||
const displayInput =
|
||
inputStr.length > maxToolInputLength
|
||
? inputStr.substring(0, maxToolInputLength) + '...'
|
||
: inputStr
|
||
console.log(chalk.dim(` [${index}] ${toolName}: ${displayInput}`))
|
||
})
|
||
}
|
||
|
||
console.log(
|
||
` Stop Reason: ${context.response.stop_reason || context.response.finish_reason || 'unknown'}`,
|
||
)
|
||
console.log(chalk.gray('━'.repeat(60)))
|
||
}
|
||
|
||
// 新增:系统提示构建过程调试
|
||
export function logSystemPromptConstruction(construction: {
|
||
basePrompt: string
|
||
kodeContext?: string
|
||
reminders: string[]
|
||
finalPrompt: string
|
||
}) {
|
||
if (!isDebugMode()) return
|
||
|
||
console.log('\n' + chalk.yellow('📝 SYSTEM PROMPT CONSTRUCTION'))
|
||
console.log(` Base Prompt: ${construction.basePrompt.length} chars`)
|
||
|
||
if (construction.kodeContext) {
|
||
console.log(` + Kode Context: ${construction.kodeContext.length} chars`)
|
||
}
|
||
|
||
if (construction.reminders.length > 0) {
|
||
console.log(
|
||
` + Dynamic Reminders: ${construction.reminders.length} items`,
|
||
)
|
||
construction.reminders.forEach((reminder, index) => {
|
||
console.log(chalk.dim(` [${index}] ${reminder.substring(0, 80)}...`))
|
||
})
|
||
}
|
||
|
||
console.log(` = Final Length: ${construction.finalPrompt.length} chars`)
|
||
}
|
||
|
||
// 新增:上下文压缩过程调试
|
||
export function logContextCompression(compression: {
|
||
beforeMessages: number
|
||
afterMessages: number
|
||
trigger: string
|
||
preservedFiles: string[]
|
||
compressionRatio: number
|
||
}) {
|
||
if (!isDebugMode()) return
|
||
|
||
console.log('\n' + chalk.red('🗜️ CONTEXT COMPRESSION'))
|
||
console.log(` Trigger: ${compression.trigger}`)
|
||
console.log(
|
||
` Messages: ${compression.beforeMessages} → ${compression.afterMessages}`,
|
||
)
|
||
console.log(
|
||
` Compression Ratio: ${(compression.compressionRatio * 100).toFixed(1)}%`,
|
||
)
|
||
|
||
if (compression.preservedFiles.length > 0) {
|
||
console.log(` Preserved Files: ${compression.preservedFiles.join(', ')}`)
|
||
}
|
||
}
|
||
|
||
// 新增:用户友好的日志显示
|
||
export function logUserFriendly(type: string, data: any, requestId?: string) {
|
||
if (!isDebugMode()) return
|
||
|
||
const timestamp = new Date().toLocaleTimeString()
|
||
let message = ''
|
||
let color = chalk.gray
|
||
let icon = '•'
|
||
|
||
switch (type) {
|
||
case 'SESSION_START':
|
||
icon = '🚀'
|
||
color = chalk.green
|
||
message = `Session started with ${data.model || 'default model'}`
|
||
break
|
||
case 'QUERY_START':
|
||
icon = '💭'
|
||
color = chalk.blue
|
||
message = `Processing query: "${data.query?.substring(0, 50)}${data.query?.length > 50 ? '...' : ''}"`
|
||
break
|
||
case 'QUERY_PROGRESS':
|
||
icon = '⏳'
|
||
color = chalk.yellow
|
||
message = `${data.phase} (${data.elapsed}ms)`
|
||
break
|
||
case 'QUERY_COMPLETE':
|
||
icon = '✅'
|
||
color = chalk.green
|
||
message = `Query completed in ${data.duration}ms - Cost: $${data.cost} - ${data.tokens} tokens`
|
||
break
|
||
case 'TOOL_EXECUTION':
|
||
icon = '🔧'
|
||
color = chalk.cyan
|
||
message = `${data.toolName}: ${data.action} ${data.target ? '→ ' + data.target : ''}`
|
||
break
|
||
case 'ERROR_OCCURRED':
|
||
icon = '❌'
|
||
color = chalk.red
|
||
message = `${data.error} ${data.context ? '(' + data.context + ')' : ''}`
|
||
break
|
||
case 'PERFORMANCE_SUMMARY':
|
||
icon = '📊'
|
||
color = chalk.magenta
|
||
message = `Session: ${data.queries} queries, $${data.totalCost}, ${data.avgResponseTime}ms avg`
|
||
break
|
||
default:
|
||
message = JSON.stringify(data)
|
||
}
|
||
|
||
const reqId = requestId ? chalk.dim(`[${requestId.slice(0, 8)}]`) : ''
|
||
console.log(`${color(`[${timestamp}]`)} ${icon} ${color(message)} ${reqId}`)
|
||
}
|
||
|
||
// 初始化日志系统
|
||
export function initDebugLogger() {
|
||
if (!isDebugMode()) return
|
||
|
||
debug.info('DEBUG_LOGGER_INIT', {
|
||
startupTimestamp: STARTUP_TIMESTAMP,
|
||
sessionId: SESSION_ID,
|
||
debugPaths: {
|
||
detailed: DEBUG_PATHS.detailed(),
|
||
flow: DEBUG_PATHS.flow(),
|
||
api: DEBUG_PATHS.api(),
|
||
state: DEBUG_PATHS.state(),
|
||
},
|
||
})
|
||
|
||
// 显示终端输出过滤信息
|
||
const terminalLevels = isDebugVerboseMode()
|
||
? Array.from(DEBUG_VERBOSE_TERMINAL_LOG_LEVELS).join(', ')
|
||
: Array.from(TERMINAL_LOG_LEVELS).join(', ')
|
||
|
||
console.log(
|
||
chalk.dim(`[DEBUG] Terminal output filtered to: ${terminalLevels}`),
|
||
)
|
||
console.log(
|
||
chalk.dim(`[DEBUG] Complete logs saved to: ${DEBUG_PATHS.base()}`),
|
||
)
|
||
if (!isDebugVerboseMode()) {
|
||
console.log(
|
||
chalk.dim(
|
||
`[DEBUG] Use --debug-verbose for detailed system logs (FLOW, API, STATE)`,
|
||
),
|
||
)
|
||
}
|
||
}
|
||
|
||
// 新增:错误诊断和恢复建议系统
|
||
interface ErrorDiagnosis {
|
||
errorType: string
|
||
category:
|
||
| 'NETWORK'
|
||
| 'API'
|
||
| 'PERMISSION'
|
||
| 'CONFIG'
|
||
| 'SYSTEM'
|
||
| 'USER_INPUT'
|
||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
|
||
description: string
|
||
suggestions: string[]
|
||
debugSteps: string[]
|
||
relatedLogs?: string[]
|
||
}
|
||
|
||
export function diagnoseError(error: any, context?: any): ErrorDiagnosis {
|
||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||
const errorStack = error instanceof Error ? error.stack : undefined
|
||
|
||
// AbortController 相关错误
|
||
if (
|
||
errorMessage.includes('aborted') ||
|
||
errorMessage.includes('AbortController')
|
||
) {
|
||
return {
|
||
errorType: 'REQUEST_ABORTED',
|
||
category: 'SYSTEM',
|
||
severity: 'MEDIUM',
|
||
description:
|
||
'Request was aborted, often due to user cancellation or timeout',
|
||
suggestions: [
|
||
'检查是否按下了 ESC 键取消请求',
|
||
'检查网络连接是否稳定',
|
||
'验证 AbortController 状态: isActive 和 signal.aborted 应该一致',
|
||
'查看是否有重复的请求导致冲突',
|
||
],
|
||
debugSteps: [
|
||
'使用 --debug-verbose 模式查看详细的请求流程',
|
||
'检查 debug 日志中的 BINARY_FEEDBACK_* 事件',
|
||
'验证 REQUEST_START 和 REQUEST_END 日志配对',
|
||
'查看 QUERY_ABORTED 事件的触发原因',
|
||
],
|
||
}
|
||
}
|
||
|
||
// API 密钥相关错误
|
||
if (
|
||
errorMessage.includes('api-key') ||
|
||
errorMessage.includes('authentication') ||
|
||
errorMessage.includes('401')
|
||
) {
|
||
return {
|
||
errorType: 'API_AUTHENTICATION',
|
||
category: 'API',
|
||
severity: 'HIGH',
|
||
description: 'API authentication failed - invalid or missing API key',
|
||
suggestions: [
|
||
'运行 /login 重新设置 API 密钥',
|
||
'检查 ~/.kode/ 配置文件中的 API 密钥',
|
||
'验证 API 密钥是否已过期或被撤销',
|
||
'确认使用的 provider 设置正确 (anthropic/opendev/bigdream)',
|
||
],
|
||
debugSteps: [
|
||
'检查 CONFIG_LOAD 日志中的 provider 和 API 密钥状态',
|
||
'运行 kode doctor 检查系统健康状态',
|
||
'查看 API_ERROR 日志了解详细错误信息',
|
||
'使用 kode config 命令查看当前配置',
|
||
],
|
||
}
|
||
}
|
||
|
||
// 网络连接错误
|
||
if (
|
||
errorMessage.includes('ECONNREFUSED') ||
|
||
errorMessage.includes('ENOTFOUND') ||
|
||
errorMessage.includes('timeout')
|
||
) {
|
||
return {
|
||
errorType: 'NETWORK_CONNECTION',
|
||
category: 'NETWORK',
|
||
severity: 'HIGH',
|
||
description: 'Network connection failed - unable to reach API endpoint',
|
||
suggestions: [
|
||
'检查网络连接是否正常',
|
||
'确认防火墙没有阻止相关端口',
|
||
'检查 proxy 设置是否正确',
|
||
'尝试切换到不同的网络环境',
|
||
'验证 baseURL 配置是否正确',
|
||
],
|
||
debugSteps: [
|
||
'检查 API_REQUEST_START 和相关网络日志',
|
||
'查看 LLM_REQUEST_ERROR 中的详细错误信息',
|
||
'使用 ping 或 curl 测试 API 端点连通性',
|
||
'检查企业网络是否需要代理设置',
|
||
],
|
||
}
|
||
}
|
||
|
||
// 权限相关错误
|
||
if (
|
||
errorMessage.includes('permission') ||
|
||
errorMessage.includes('EACCES') ||
|
||
errorMessage.includes('denied')
|
||
) {
|
||
return {
|
||
errorType: 'PERMISSION_DENIED',
|
||
category: 'PERMISSION',
|
||
severity: 'MEDIUM',
|
||
description: 'Permission denied - insufficient access rights',
|
||
suggestions: [
|
||
'检查文件和目录的读写权限',
|
||
'确认当前用户有足够的系统权限',
|
||
'查看是否需要管理员权限运行',
|
||
'检查工具权限设置是否正确配置',
|
||
],
|
||
debugSteps: [
|
||
'查看 PERMISSION_* 日志了解权限检查过程',
|
||
'检查文件系统权限: ls -la',
|
||
'验证工具审批状态',
|
||
'查看 TOOL_* 相关的调试日志',
|
||
],
|
||
}
|
||
}
|
||
|
||
// LLM 响应格式错误
|
||
if (
|
||
errorMessage.includes('substring is not a function') ||
|
||
errorMessage.includes('content')
|
||
) {
|
||
return {
|
||
errorType: 'RESPONSE_FORMAT',
|
||
category: 'API',
|
||
severity: 'MEDIUM',
|
||
description: 'LLM response format mismatch between different providers',
|
||
suggestions: [
|
||
'检查当前使用的 provider 是否与期望一致',
|
||
'验证响应格式处理逻辑',
|
||
'确认不同 provider 的响应格式差异',
|
||
'检查是否需要更新响应解析代码',
|
||
],
|
||
debugSteps: [
|
||
'查看 LLM_CALL_DEBUG 中的响应格式',
|
||
'检查 provider 配置和实际使用的 API',
|
||
'对比 Anthropic 和 OpenAI 响应格式差异',
|
||
'验证 logLLMInteraction 函数的格式处理',
|
||
],
|
||
}
|
||
}
|
||
|
||
// 上下文窗口溢出
|
||
if (
|
||
errorMessage.includes('too long') ||
|
||
errorMessage.includes('context') ||
|
||
errorMessage.includes('token')
|
||
) {
|
||
return {
|
||
errorType: 'CONTEXT_OVERFLOW',
|
||
category: 'SYSTEM',
|
||
severity: 'MEDIUM',
|
||
description: 'Context window exceeded - conversation too long',
|
||
suggestions: [
|
||
'运行 /compact 手动压缩对话历史',
|
||
'检查自动压缩设置是否正确配置',
|
||
'减少单次输入的内容长度',
|
||
'清理不必要的上下文信息',
|
||
],
|
||
debugSteps: [
|
||
'查看 AUTO_COMPACT_* 日志检查压缩触发',
|
||
'检查 token 使用量和阈值',
|
||
'查看 CONTEXT_COMPRESSION 相关日志',
|
||
'验证模型的最大 token 限制',
|
||
],
|
||
}
|
||
}
|
||
|
||
// 配置相关错误
|
||
if (
|
||
errorMessage.includes('config') ||
|
||
(errorMessage.includes('undefined') && context?.configRelated)
|
||
) {
|
||
return {
|
||
errorType: 'CONFIGURATION',
|
||
category: 'CONFIG',
|
||
severity: 'MEDIUM',
|
||
description: 'Configuration error - missing or invalid settings',
|
||
suggestions: [
|
||
'运行 kode config 检查配置设置',
|
||
'删除损坏的配置文件重新初始化',
|
||
'检查 JSON 配置文件语法是否正确',
|
||
'验证环境变量设置',
|
||
],
|
||
debugSteps: [
|
||
'查看 CONFIG_LOAD 和 CONFIG_SAVE 日志',
|
||
'检查配置文件路径和权限',
|
||
'验证 JSON 格式: cat ~/.kode/config.json | jq',
|
||
'查看配置缓存相关的调试信息',
|
||
],
|
||
}
|
||
}
|
||
|
||
// 通用错误兜底
|
||
return {
|
||
errorType: 'UNKNOWN',
|
||
category: 'SYSTEM',
|
||
severity: 'MEDIUM',
|
||
description: `Unexpected error: ${errorMessage}`,
|
||
suggestions: [
|
||
'重新启动应用程序',
|
||
'检查系统资源是否充足',
|
||
'查看完整的错误日志获取更多信息',
|
||
'如果问题持续,请报告此错误',
|
||
],
|
||
debugSteps: [
|
||
'使用 --debug-verbose 获取详细日志',
|
||
'检查 error.log 中的完整错误信息',
|
||
'查看系统资源使用情况',
|
||
'收集重现步骤和环境信息',
|
||
],
|
||
relatedLogs: errorStack ? [errorStack] : undefined,
|
||
}
|
||
}
|
||
|
||
export function logErrorWithDiagnosis(
|
||
error: any,
|
||
context?: any,
|
||
requestId?: string,
|
||
) {
|
||
if (!isDebugMode()) return
|
||
|
||
const diagnosis = diagnoseError(error, context)
|
||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||
|
||
// 记录标准错误日志
|
||
debug.error(
|
||
'ERROR_OCCURRED',
|
||
{
|
||
error: errorMessage,
|
||
errorType: diagnosis.errorType,
|
||
category: diagnosis.category,
|
||
severity: diagnosis.severity,
|
||
context,
|
||
},
|
||
requestId,
|
||
)
|
||
|
||
// 在终端显示诊断信息
|
||
console.log('\n' + chalk.red('🚨 ERROR DIAGNOSIS'))
|
||
console.log(chalk.gray('━'.repeat(60)))
|
||
|
||
console.log(chalk.red(`❌ ${diagnosis.errorType}`))
|
||
console.log(
|
||
chalk.dim(
|
||
`Category: ${diagnosis.category} | Severity: ${diagnosis.severity}`,
|
||
),
|
||
)
|
||
console.log(`\n${diagnosis.description}`)
|
||
|
||
console.log(chalk.yellow('\n💡 Recovery Suggestions:'))
|
||
diagnosis.suggestions.forEach((suggestion, index) => {
|
||
console.log(` ${index + 1}. ${suggestion}`)
|
||
})
|
||
|
||
console.log(chalk.cyan('\n🔍 Debug Steps:'))
|
||
diagnosis.debugSteps.forEach((step, index) => {
|
||
console.log(` ${index + 1}. ${step}`)
|
||
})
|
||
|
||
if (diagnosis.relatedLogs && diagnosis.relatedLogs.length > 0) {
|
||
console.log(chalk.magenta('\n📋 Related Information:'))
|
||
diagnosis.relatedLogs.forEach((log, index) => {
|
||
const truncatedLog =
|
||
log.length > 200 ? log.substring(0, 200) + '...' : log
|
||
console.log(chalk.dim(` ${truncatedLog}`))
|
||
})
|
||
}
|
||
|
||
const debugPath = DEBUG_PATHS.base()
|
||
console.log(chalk.gray(`\n📁 Complete logs: ${debugPath}`))
|
||
console.log(chalk.gray('━'.repeat(60)))
|
||
}
|
||
export function getDebugInfo() {
|
||
return {
|
||
isDebugMode: isDebugMode(),
|
||
isVerboseMode: isVerboseMode(),
|
||
isDebugVerboseMode: isDebugVerboseMode(),
|
||
startupTimestamp: STARTUP_TIMESTAMP,
|
||
sessionId: SESSION_ID,
|
||
currentRequest: currentRequest?.id,
|
||
activeRequests: Array.from(activeRequests.keys()),
|
||
terminalLogLevels: isDebugVerboseMode()
|
||
? Array.from(DEBUG_VERBOSE_TERMINAL_LOG_LEVELS)
|
||
: Array.from(TERMINAL_LOG_LEVELS),
|
||
debugPaths: {
|
||
detailed: DEBUG_PATHS.detailed(),
|
||
flow: DEBUG_PATHS.flow(),
|
||
api: DEBUG_PATHS.api(),
|
||
state: DEBUG_PATHS.state(),
|
||
},
|
||
}
|
||
}
|