diff --git a/package.json b/package.json index 7d0d2cb4..c11bfff7 100644 --- a/package.json +++ b/package.json @@ -268,6 +268,11 @@ } }, "description": "Settings for VSCode Language Model API" + }, + "coolcline.debugMode": { + "type": "boolean", + "default": false, + "description": "启用调试模式,将在输出面板显示详细日志" } } } diff --git a/src/core/CoolCline.ts b/src/core/CoolCline.ts index 6239a574..4fc43ac7 100644 --- a/src/core/CoolCline.ts +++ b/src/core/CoolCline.ts @@ -63,6 +63,7 @@ import crypto from "crypto" import { insertGroups } from "./diff/insert-groups" import { EXPERIMENT_IDS, experiments as Experiments } from "../shared/experiments" import { CheckpointService } from "../services/checkpoints/CheckpointService" +import { logger } from "../utils/logging" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution @@ -241,34 +242,95 @@ export class CoolCline { await this.saveCoolClineMessages() } + private static readonly MAX_MESSAGE_SIZE = 1000000 // 1MB 限制 + private static readonly MAX_MESSAGE_PREVIEW_SIZE = 100000 // 预览大小限制 + private async saveCoolClineMessages() { try { const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages) - await fs.writeFile(filePath, JSON.stringify(this.coolclineMessages)) - // combined as they are in ChatView - const apiMetrics = getApiMetrics( - combineApiRequests(combineCommandSequences(this.coolclineMessages.slice(1))), - ) - const taskMessage = this.coolclineMessages[0] // first message is always the task say - const lastRelevantMessage = - this.coolclineMessages[ - findLastIndex( - this.coolclineMessages, - (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"), - ) - ] - await this.providerRef.deref()?.updateTaskHistory({ - id: this.taskId, - ts: lastRelevantMessage.ts, - task: taskMessage.text ?? "", - tokensIn: apiMetrics.totalTokensIn, - tokensOut: apiMetrics.totalTokensOut, - cacheWrites: apiMetrics.totalCacheWrites, - cacheReads: apiMetrics.totalCacheReads, - totalCost: apiMetrics.totalCost, + + // 处理大型消息 + const processedMessages = this.coolclineMessages.map((msg) => { + if (!msg.text) return msg + + // 检查消息大小 + if (msg.text.length > CoolCline.MAX_MESSAGE_SIZE) { + logger.warn("消息内容过大,将被截断", { + ctx: "coolcline", + messageType: msg.type, + messageLength: msg.text.length, + limit: CoolCline.MAX_MESSAGE_SIZE, + }) + + // 对于命令输出类型的消息,添加特殊提示 + if (msg.type === "say" && msg.say === "command_output") { + return { + ...msg, + text: + msg.text.substring(0, CoolCline.MAX_MESSAGE_PREVIEW_SIZE) + + "\n\n... [输出内容过长,已截断。建议使用其他工具查看完整输出,比如将输出重定向到文件:command > output.txt]", + } + } + + // 其他类型消息的通用处理 + return { + ...msg, + text: msg.text.substring(0, CoolCline.MAX_MESSAGE_PREVIEW_SIZE) + "\n... [内容过长已截断]", + } + } + return msg }) + + // 分块写入文件 + try { + await fs.writeFile(filePath, JSON.stringify(processedMessages), "utf8") + + // 更新任务历史 + const apiMetrics = getApiMetrics( + combineApiRequests(combineCommandSequences(processedMessages.slice(1))), + ) + const taskMessage = processedMessages[0] + const lastRelevantMessage = + processedMessages[ + findLastIndex( + processedMessages, + (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"), + ) + ] + + await this.providerRef.deref()?.updateTaskHistory({ + id: this.taskId, + ts: lastRelevantMessage.ts, + task: taskMessage.text ?? "", + tokensIn: apiMetrics.totalTokensIn, + tokensOut: apiMetrics.totalTokensOut, + cacheWrites: apiMetrics.totalCacheWrites, + cacheReads: apiMetrics.totalCacheReads, + totalCost: apiMetrics.totalCost, + }) + } catch (error) { + // 如果写入失败,尝试只保存最近的消息 + logger.error("保存完整消息失败,尝试只保存最近的消息", { + ctx: "coolcline", + error: error instanceof Error ? error.message : String(error), + }) + + const recentMessages = processedMessages.slice(-10) // 只保留最近的10条消息 + await fs.writeFile(filePath, JSON.stringify(recentMessages), "utf8") + + // 通知用户 + vscode.window.showWarningMessage( + "由于消息内容过大,只保存了最近的消息。建议清理历史记录或开启新的任务。", + ) + } } catch (error) { - console.error("Failed to save coolcline messages:", error) + logger.error("保存消息完全失败", { + ctx: "coolcline", + error: error instanceof Error ? error.message : String(error), + }) + + // 通知用户但继续运行 + vscode.window.showErrorMessage("保存消息历史失败。建议保存重要内容到文件并重新开始任务。") } } diff --git a/src/extension.ts b/src/extension.ts index 977c118a..997c61c4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode" +import { logger, initializeLogger } from "./utils/logging" import { CoolClineProvider } from "./core/webview/CoolClineProvider" import { createCoolClineAPI } from "./exports" @@ -21,12 +22,31 @@ let extensionContext: vscode.ExtensionContext // This method is called when your extension is activated. // Your extension is activated the very first time the command is executed. -export function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { extensionContext = context outputChannel = vscode.window.createOutputChannel("CoolCline") context.subscriptions.push(outputChannel) outputChannel.appendLine("CoolCline extension activated") + // 初始化日志系统 + try { + await initializeLogger(context) + logger.info("CoolCline extension activated", { ctx: "extension" }) + } catch (error) { + const errorMessage = `日志系统初始化失败: ${error instanceof Error ? error.message : String(error)}` + outputChannel.appendLine(errorMessage) + console.error(errorMessage) + + // 尝试记录更多诊断信息 + try { + const storageUri = context.globalStorageUri + outputChannel.appendLine(`存储路径: ${storageUri.fsPath}`) + console.log("存储路径:", storageUri.fsPath) + } catch (e) { + outputChannel.appendLine(`无法获取存储路径: ${e instanceof Error ? e.message : String(e)}`) + } + } + // Get default commands from configuration. const defaultCommands = vscode.workspace.getConfiguration("coolcline").get("allowedCommands") || [] diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts index 3a77ea27..df719c41 100644 --- a/src/integrations/terminal/TerminalProcess.ts +++ b/src/integrations/terminal/TerminalProcess.ts @@ -1,6 +1,7 @@ import { EventEmitter } from "events" import stripAnsi from "strip-ansi" import * as vscode from "vscode" +import { logger } from "../../utils/logging" const PROCESS_HOT_TIMEOUT_NORMAL = 2_000 const PROCESS_HOT_TIMEOUT_COMPILING = 15_000 @@ -34,17 +35,60 @@ export class TerminalProcess extends EventEmitter { generic: ["$", ">", "#", "❯", "→", "➜"], } + private static readonly MAX_OUTPUT_LENGTH = 1000000 // 1MB 限制 + private static readonly MAX_OUTPUT_PREVIEW_LENGTH = 100000 // 预览长度限制 + async run(terminal: vscode.Terminal, command: string) { this.command = command + const commandPreview = command.length > 30 ? command.substring(0, 30) + "..." : command + logger.debug("开始执行命令", { + ctx: "terminal", + command: commandPreview, + fullCommand: command, + hasShellIntegration: !!terminal.shellIntegration, + hasExecuteCommand: !!terminal.shellIntegration?.executeCommand, + terminalType: terminal.name, // 终端类型(如 bash, zsh 等) + }) try { if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) { + logger.debug("使用 shellIntegration 执行命令", { + ctx: "terminal", + terminalName: terminal.name, + }) const execution = terminal.shellIntegration.executeCommand(command) const stream = execution.read() let isFirstChunk = true let didOutputNonCommand = false let didEmitEmptyLine = false + this.fullOutput = "" // 重置输出 for await (let data of stream) { + // 检查数据大小 + if (this.fullOutput.length + data.length > TerminalProcess.MAX_OUTPUT_LENGTH) { + logger.warn("命令输出超过限制", { + ctx: "terminal", + currentLength: this.fullOutput.length, + newDataLength: data.length, + limit: TerminalProcess.MAX_OUTPUT_LENGTH, + }) + + // 发送截断警告 + this.emit( + "line", + "\n... [输出内容过长,已截断。建议使用其他工具查看完整输出," + + "比如将输出重定向到文件:command > output.txt]", + ) + break + } + + const dataPreview = data.length > 30 ? data.substring(0, 30) + "..." : data + logger.debug("收到命令输出块", { + ctx: "terminal", + length: data.length, + preview: dataPreview, + isFirstChunk, + hasNonCommand: didOutputNonCommand, + }) if (isFirstChunk) { const outputBetweenSequences = this.removeLastLineArtifacts( data.match(/\]633;C([\s\S]*?)\]633;D/)?.[1] || "", @@ -141,6 +185,12 @@ export class TerminalProcess extends EventEmitter { } } + logger.debug("命令执行完成", { + ctx: "terminal", + totalOutputLength: this.fullOutput.length, + lastRetrievedIndex: this.lastRetrievedIndex, + }) + this.emitRemainingBufferIfListening() if (this.hotTimer) { clearTimeout(this.hotTimer) @@ -149,27 +199,45 @@ export class TerminalProcess extends EventEmitter { this.emit("completed") this.emit("continue") } else { + // 记录为什么不能使用 Shell Integration + logger.debug("无法使用 shellIntegration", { + ctx: "terminal", + reason: !terminal.shellIntegration ? "Shell Integration 未启用" : "executeCommand 方法不可用", + terminalName: terminal.name, + commandType: this.getCommandType(command), + }) + // 传统方式 + logger.debug("使用传统方式执行命令", { ctx: "terminal" }) this.fullOutput = "" this.buffer = "" this.outputBuffer = [] - // 先发送命令,让命令开始执行 + // 发送命令 terminal.sendText(command, true) - - // 等待一小段时间让命令开始执行 await new Promise((resolve) => setTimeout(resolve, 100)) - // 尝试获取输出 + // 使用改进后的方法获取输出 const output = await this.waitForCommandCompletion() if (output) { - // 处理输出前发出开始信号 - this.emit("line", "") - this.processOutput(output) - } + // 检查输出大小 + if (output.length > TerminalProcess.MAX_OUTPUT_LENGTH) { + logger.warn("命令输出超过限制", { + ctx: "terminal", + length: output.length, + limit: TerminalProcess.MAX_OUTPUT_LENGTH, + }) + + const truncatedOutput = + output.substring(0, TerminalProcess.MAX_OUTPUT_PREVIEW_LENGTH) + + "\n\n... [输出内容过长,已截断。建议使用其他工具查看完整输出," + + "比如将输出重定向到文件:command > output.txt]" - this.isHot = false - if (this.hotTimer) { - clearTimeout(this.hotTimer) + this.emit("line", "") + this.emit("line", truncatedOutput) + } else { + this.emit("line", "") + this.processOutput(output) + } } this.emit("no_shell_integration") @@ -177,7 +245,10 @@ export class TerminalProcess extends EventEmitter { this.emit("continue") } } catch (error) { - console.error(`Error executing command in terminal: ${error}`) + logger.error("命令执行失败", { + ctx: "terminal", + error: error instanceof Error ? error : new Error(String(error)), + }) this.emit("error", error instanceof Error ? error : new Error(String(error))) } } @@ -189,32 +260,65 @@ export class TerminalProcess extends EventEmitter { let stableCount = 0 let waitTime = TerminalProcess.OUTPUT_CHECK_CONFIG.minWaitMs - // 给命令执行一些初始时间 + logger.debug("开始等待命令完成", { + ctx: "terminal", + command: this.command.length > 30 ? this.command.substring(0, 30) + "..." : this.command, + maxAttempts: TerminalProcess.OUTPUT_CHECK_CONFIG.maxAttempts, + initialWaitTime: waitTime, + }) + await new Promise((resolve) => setTimeout(resolve, 200)) while (attemptCount < TerminalProcess.OUTPUT_CHECK_CONFIG.maxAttempts) { try { const newOutput = await TerminalProcess.getTerminalContents() + const outputPreview = newOutput?.length > 30 ? newOutput.substring(0, 30) + "..." : newOutput + logger.debug("轮询检查输出", { + ctx: "terminal", + attempt: attemptCount + 1, + hasNewOutput: !!newOutput, + outputLength: newOutput?.length || 0, + preview: outputPreview, + waitTime, + stableCount, + }) + if (newOutput) { if (newOutput !== lastOutput) { output = newOutput lastOutput = newOutput stableCount = 0 - // 动态调整等待时间 if (this.isCompiling(newOutput)) { waitTime = Math.min(500, waitTime * 1.5) + logger.debug("检测到编译中,增加等待时间", { + ctx: "terminal", + newWaitTime: waitTime, + outputPreview: outputPreview, + }) } else { waitTime = TerminalProcess.OUTPUT_CHECK_CONFIG.minWaitMs } } else { stableCount++ + logger.debug("输出稳定", { + ctx: "terminal", + stableCount, + outputPreview: outputPreview, + }) if (stableCount >= TerminalProcess.OUTPUT_CHECK_CONFIG.stableCount) { - // 在认为命令完成之前,再次检查最后一次输出 await new Promise((resolve) => setTimeout(resolve, 100)) const finalCheck = await TerminalProcess.getTerminalContents() + const finalPreview = + finalCheck?.length > 30 ? finalCheck.substring(0, 30) + "..." : finalCheck if (finalCheck === output && this.checkCommandCompletion(finalCheck)) { + logger.debug("命令执行完成", { + ctx: "terminal", + totalAttempts: attemptCount + 1, + finalOutputLength: output.length, + outputPreview: finalPreview, + }) return output } } @@ -224,12 +328,22 @@ export class TerminalProcess extends EventEmitter { attemptCount++ await new Promise((resolve) => setTimeout(resolve, waitTime)) } catch (error) { - console.error("Failed to get terminal contents:", error) + logger.error("获取终端内容失败", { + ctx: "terminal", + error: error instanceof Error ? error.message : String(error), + command: this.command.length > 30 ? this.command.substring(0, 30) + "..." : this.command, + }) attemptCount++ waitTime = Math.min(waitTime * 1.5, TerminalProcess.OUTPUT_CHECK_CONFIG.maxWaitMs) } } + logger.debug("达到最大尝试次数", { + ctx: "terminal", + finalOutputLength: output.length, + totalAttempts: attemptCount, + outputPreview: output.length > 30 ? output.substring(0, 30) + "..." : output, + }) return output } @@ -237,51 +351,64 @@ export class TerminalProcess extends EventEmitter { const maxRetries = 3 let lastError: Error | undefined let originalClipboard: string | undefined + let content = "" try { - // 首先保存原始剪贴板内容 + // 保存原始剪贴板内容 originalClipboard = await vscode.env.clipboard.readText() for (let attempt = 0; attempt < maxRetries; attempt++) { try { - // 确保清除任何现有选择 + // 清除现有选择 await vscode.commands.executeCommand("workbench.action.terminal.clearSelection") await new Promise((resolve) => setTimeout(resolve, 50)) - // 根据参数选择不同的选择策略 - if (commands < 0) { - await vscode.commands.executeCommand("workbench.action.terminal.selectAll") - } else { - await vscode.commands.executeCommand("workbench.action.terminal.selectToPreviousCommand") - } - - // 给足够的时间让选择完成 - await new Promise((resolve) => setTimeout(resolve, 150)) + // 使用 selectToPreviousCommand 只选择最后一个命令的输出 + await vscode.commands.executeCommand("workbench.action.terminal.selectToPreviousCommand") + await new Promise((resolve) => setTimeout(resolve, 200)) - // 复制选中内容到剪贴板 + // 复制选中内容 await vscode.commands.executeCommand("workbench.action.terminal.copySelection") - - // 确保复制操作完成 - await new Promise((resolve) => setTimeout(resolve, 150)) + await new Promise((resolve) => setTimeout(resolve, 200)) // 获取复制的内容 - const content = await vscode.env.clipboard.readText() + const newContent = await vscode.env.clipboard.readText() - // 立即清除选择避免影响视觉 + // 清除选择 await vscode.commands.executeCommand("workbench.action.terminal.clearSelection") - // 如果获取到了新内容 - if (content && content !== originalClipboard) { + if (newContent && newContent !== originalClipboard) { + content = newContent + + // 检查内容长度 + if (content.length > TerminalProcess.MAX_OUTPUT_LENGTH) { + logger.warn("终端输出超过限制", { + ctx: "terminal", + length: content.length, + limit: TerminalProcess.MAX_OUTPUT_LENGTH, + }) + + // 截取内容并添加警告信息 + content = + content.substring(0, TerminalProcess.MAX_OUTPUT_PREVIEW_LENGTH) + + "\n\n... [输出内容过长,已截断。建议使用其他工具查看完整输出," + + "比如将输出重定向到文件:command > output.txt]\n" + break + } + return content } - // 如果还有重试机会,等待后重试 if (attempt < maxRetries - 1) { await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 200)) continue } } catch (error) { - console.error(`Terminal content retrieval attempt ${attempt + 1} failed:`, error) + logger.error("获取终端内容失败", { + ctx: "terminal", + attempt: attempt + 1, + error: error instanceof Error ? error.message : String(error), + }) lastError = error instanceof Error ? error : new Error(String(error)) if (attempt < maxRetries - 1) { @@ -295,14 +422,17 @@ export class TerminalProcess extends EventEmitter { throw lastError } - return "" + return content || "" } finally { - // 确保在所有情况下都恢复原始剪贴板内容 + // 恢复原始剪贴板内容 if (originalClipboard !== undefined) { try { await vscode.env.clipboard.writeText(originalClipboard) } catch (error) { - console.error("Failed to restore clipboard:", error) + logger.error("恢复剪贴板失败", { + ctx: "terminal", + error: error instanceof Error ? error.message : String(error), + }) } } } @@ -503,6 +633,16 @@ export class TerminalProcess extends EventEmitter { } return lines.join("\n").trimEnd() } + + // 添加辅助方法来分析命令类型 + private getCommandType(command: string): string { + if (command.includes("|")) return "pipe" + if (command.includes(">") || command.includes("<")) return "redirect" + if (command.includes("&&") || command.includes("||")) return "conditional" + if (command.includes(";")) return "multiple" + if (command.startsWith("sudo ")) return "sudo" + return "simple" + } } // Similar to execa's ResultPromise, this lets us create a mixin of both a TerminalProcess and a Promise diff --git a/src/utils/logging/CompactTransport.ts b/src/utils/logging/CompactTransport.ts index 59988e92..c53db63f 100644 --- a/src/utils/logging/CompactTransport.ts +++ b/src/utils/logging/CompactTransport.ts @@ -2,8 +2,8 @@ * @fileoverview 实现紧凑日志传输系统,支持文件输出功能 */ -import { writeFileSync, mkdirSync } from "fs" -import { dirname } from "path" +import * as fs from "fs" +import * as path from "path" import { CompactTransportConfig, ICompactTransport, CompactLogEntry, LogLevel, LOG_LEVELS } from "./types" /** @@ -23,46 +23,106 @@ function isLevelEnabled(configLevel: LogLevel, entryLevel: string): boolean { * @implements {ICompactTransport} */ export class CompactTransport implements ICompactTransport { - private sessionStart: number - private lastTimestamp: number - private filePath: string - private initialized: boolean = false - private initError: Error | null = null + private logStream: fs.WriteStream | null = null + private logDir: string + private logPath: string + private writeQueue: string[] = [] + private isWriting: boolean = false /** * 创建新的 CompactTransport 实例 * @param config - 传输配置 */ - constructor(readonly config: CompactTransportConfig) { - this.sessionStart = Date.now() - this.lastTimestamp = this.sessionStart - this.filePath = config.filePath + constructor(private config: CompactTransportConfig) { + this.logDir = path.dirname(config.filePath) + this.logPath = config.filePath + this.initializeLogFile() } - /** - * 确保日志文件已初始化,包括创建必要的目录结构和会话开始标记 - * @private - */ - private ensureInitialized(): void { - if (this.initialized || this.initError) return - + private async initializeLogFile() { try { - mkdirSync(dirname(this.filePath), { recursive: true }) - writeFileSync(this.filePath, "", { flag: "w" }) - - const sessionStart = { - t: 0, - l: "info", - m: "日志会话已开始", - d: { timestamp: new Date(this.sessionStart).toISOString() }, + console.log("正在初始化日志文件:", { + logDir: this.logDir, + logPath: this.logPath, + exists: fs.existsSync(this.logDir), + }) + + // 确保日志目录存在 + if (!fs.existsSync(this.logDir)) { + console.log("创建日志目录:", this.logDir) + await fs.promises.mkdir(this.logDir, { recursive: true }) + } + + // 验证目录权限 + try { + await fs.promises.access(this.logDir, fs.constants.W_OK) + console.log("日志目录权限验证成功:", this.logDir) + } catch (error) { + console.error("日志目录权限验证失败:", { + dir: this.logDir, + error: error instanceof Error ? error.message : String(error), + }) + throw new Error(`没有日志目录的写入权限: ${this.logDir}`) } - writeFileSync(this.filePath, JSON.stringify(sessionStart) + "\n", { flag: "w" }) - this.initialized = true - } catch (err) { - this.initError = new Error(`初始化日志文件失败: ${(err as Error).message}`) - // 不抛出错误,而是记录初始化失败 - console.error(this.initError.message) + // 创建或打开日志文件流 + console.log("创建日志文件流:", this.logPath) + this.logStream = fs.createWriteStream(this.logPath, { + flags: "a", // append 模式 + encoding: "utf8", + mode: 0o644, // 设置文件权限 + }) + + // 等待流准备就绪 + await new Promise((resolve, reject) => { + if (!this.logStream) { + reject(new Error("日志流创建失败")) + return + } + + const timeoutId = setTimeout(() => { + reject(new Error("日志流初始化超时")) + }, 5000) // 5秒超时 + + this.logStream.once("error", (error) => { + clearTimeout(timeoutId) + reject(error) + }) + + this.logStream.once("ready", () => { + clearTimeout(timeoutId) + this.logStream?.removeListener("error", reject) + console.log("日志流准备就绪") + resolve() + }) + }) + + // 处理错误事件 + this.logStream.on("error", (error) => { + console.error("日志文件写入错误:", { + path: this.logPath, + error: error instanceof Error ? error.message : String(error), + }) + // 尝试重新初始化 + this.logStream = null + setTimeout(() => this.initializeLogFile(), 1000) + }) + + console.log("日志文件初始化成功:", this.logPath) + + // 如果有待写入的日志,开始处理 + if (this.writeQueue.length > 0) { + console.log(`处理${this.writeQueue.length}条待写入日志`) + await this.processWriteQueue() + } + } catch (error) { + const errorMessage = `初始化日志文件失败: ${error instanceof Error ? error.message : String(error)}` + console.error(errorMessage, { + logDir: this.logDir, + logPath: this.logPath, + error, + }) + throw new Error(errorMessage) } } @@ -71,32 +131,52 @@ export class CompactTransport implements ICompactTransport { * @param entry - 要写入的日志条目 */ write(entry: CompactLogEntry): void { - // 首先检查日志级别 - if (!isLevelEnabled(this.config.level, entry.l)) { + const logLine = JSON.stringify(entry) + "\n" + + if (!this.logStream) { + // 如果日志流还未初始化,加入队列 + this.writeQueue.push(logLine) return } - const deltaT = entry.t - this.lastTimestamp - this.lastTimestamp = entry.t + this.writeQueue.push(logLine) + if (!this.isWriting) { + this.processWriteQueue() + } + } - const compact = { - ...entry, - t: deltaT, + private async processWriteQueue() { + if (this.isWriting || this.writeQueue.length === 0 || !this.logStream) { + return } - const output = JSON.stringify(compact) + "\n" + this.isWriting = true - // 写入控制台 - process.stdout.write(output) + try { + while (this.writeQueue.length > 0) { + const line = this.writeQueue.shift() + if (line) { + // 使用 Promise 包装写入操作 + await new Promise((resolve, reject) => { + if (!this.logStream) { + reject(new Error("日志流未初始化")) + return + } - // 尝试写入文件 - this.ensureInitialized() - if (this.initialized && !this.initError) { - try { - writeFileSync(this.filePath, output, { flag: "a" }) - } catch (err) { - console.error(`写入日志文件失败: ${(err as Error).message}`) + this.logStream.write(line, (error) => { + if (error) { + reject(error) + } else { + resolve() + } + }) + }) + } } + } catch (error) { + console.error("处理日志写入队列时出错:", error) + } finally { + this.isWriting = false } } @@ -104,18 +184,9 @@ export class CompactTransport implements ICompactTransport { * 关闭传输并写入会话结束标记 */ close(): void { - if (this.initialized && !this.initError) { - try { - const sessionEnd = { - t: Date.now() - this.lastTimestamp, - l: "info", - m: "日志会话已结束", - d: { timestamp: new Date().toISOString() }, - } - writeFileSync(this.filePath, JSON.stringify(sessionEnd) + "\n", { flag: "a" }) - } catch (err) { - console.error(`写入会话结束标记失败: ${(err as Error).message}`) - } + if (this.logStream) { + this.logStream.end() + this.logStream = null } } } diff --git a/src/utils/logging/index.ts b/src/utils/logging/index.ts index c26a36fb..9513bc9e 100644 --- a/src/utils/logging/index.ts +++ b/src/utils/logging/index.ts @@ -2,20 +2,96 @@ * @fileoverview 日志记录器入口文件,导出默认实例和测试用的空日志记录器 */ +import * as path from "path" +import * as fs from "fs" +import * as vscode from "vscode" import { CompactLogger } from "./CompactLogger" import { CompactTransport } from "./CompactTransport" import { ILogger } from "./types" -// 创建默认的日志传输实例 -const defaultTransport = new CompactTransport({ - level: "info", - filePath: "./logs/app.log", -}) +let defaultTransport: CompactTransport | null = null +let defaultLogger: CompactLogger | null = null -// 创建并导出默认的日志记录器实例 -export const logger = new CompactLogger(defaultTransport) +// 初始化日志系统 +export async function initializeLogger(context: vscode.ExtensionContext) { + try { + // 获取正确的存储路径 + const extensionDir = context.globalStorageUri.fsPath + console.log("扩展目录路径:", extensionDir) -// 导出一个空的日志记录器,用于生产环境 + // 确保扩展目录存在 + if (!fs.existsSync(extensionDir)) { + console.log("创建扩展目录:", extensionDir) + await fs.promises.mkdir(extensionDir, { recursive: true }) + } + + // 创建日志目录 + const logDir = path.join(extensionDir, "logs") + console.log("日志目录路径:", logDir) + if (!fs.existsSync(logDir)) { + console.log("创建日志目录:", logDir) + await fs.promises.mkdir(logDir, { recursive: true }) + } + + const logPath = path.join(logDir, "coolcline.log") + console.log("日志文件完整路径:", logPath) + + // 验证目录权限 + try { + await fs.promises.access(logDir, fs.constants.W_OK) + console.log("日志目录权限验证成功") + } catch (error) { + console.error("日志目录权限验证失败:", error) + throw error + } + + // 创建默认的日志传输实例 + defaultTransport = new CompactTransport({ + level: "debug", + filePath: logPath, + }) + + // 创建并导出默认的日志记录器实例 + defaultLogger = new CompactLogger(defaultTransport) + + // 写入初始日志以验证系统正常工作 + defaultLogger.info("日志系统初始化成功", { + ctx: "logging", + logPath, + extensionDir, + }) + + return defaultLogger + } catch (error) { + console.error("日志系统初始化失败:", error) + // 记录更详细的错误信息 + if (error instanceof Error) { + console.error("错误详情:", { + message: error.message, + stack: error.stack, + name: error.name, + }) + } + throw error + } +} + +// 导出日志记录器实例 +export const logger: ILogger = { + debug: (...args) => defaultLogger?.debug(...args) ?? noopLogger.debug(...args), + info: (...args) => defaultLogger?.info(...args) ?? noopLogger.info(...args), + warn: (...args) => defaultLogger?.warn(...args) ?? noopLogger.warn(...args), + error: (...args) => defaultLogger?.error(...args) ?? noopLogger.error(...args), + fatal: (...args) => defaultLogger?.fatal(...args) ?? noopLogger.fatal(...args), + child: (meta) => defaultLogger?.child(meta) || noopLogger, + close: () => { + defaultLogger?.close() + defaultLogger = null + defaultTransport = null + }, +} + +// 导出一个空的日志记录器,用于生产环境或初始化失败时 export const noopLogger: ILogger = { debug: () => {}, info: () => {},