From 8a49ae40d5c3285ecfff74162c25a7f2ebb7036a Mon Sep 17 00:00:00 2001 From: chenhaoli Date: Sat, 22 Mar 2025 18:54:40 +0800 Subject: [PATCH] feat(agent-tars): support "view logs" for trouble shooting --- apps/agent-tars/package.json | 1 + .../agent-tars/src/main/customTools/search.ts | 12 +- apps/agent-tars/src/main/index.ts | 20 ++- apps/agent-tars/src/main/ipcRoutes/action.ts | 37 ++++- apps/agent-tars/src/main/ipcRoutes/agent.ts | 3 +- .../src/main/ipcRoutes/filesystem.ts | 5 +- apps/agent-tars/src/main/ipcRoutes/llm.ts | 37 ++++- apps/agent-tars/src/main/ipcRoutes/search.ts | 11 +- .../src/main/llmProvider/ProviderFactory.ts | 3 +- apps/agent-tars/src/main/llmProvider/index.ts | 16 +- apps/agent-tars/src/main/menu.ts | 138 +++++++++++++++ apps/agent-tars/src/main/store/setting.ts | 3 +- .../src/main/utils/extractToolNames.ts | 13 ++ apps/agent-tars/src/main/utils/logger.ts | 157 ++++++++++++++++++ .../src/main/utils/maskSensitiveData.ts | 47 ++++++ .../src/main/utils/normalizeOmegaData.ts | 4 +- .../src/main/utils/systemPermissions.ts | 7 +- pnpm-lock.yaml | 7 +- 18 files changed, 485 insertions(+), 36 deletions(-) create mode 100644 apps/agent-tars/src/main/menu.ts create mode 100644 apps/agent-tars/src/main/utils/extractToolNames.ts create mode 100644 apps/agent-tars/src/main/utils/logger.ts create mode 100644 apps/agent-tars/src/main/utils/maskSensitiveData.ts diff --git a/apps/agent-tars/package.json b/apps/agent-tars/package.json index 2a487104a..f9ae1e9c0 100644 --- a/apps/agent-tars/package.json +++ b/apps/agent-tars/package.json @@ -54,6 +54,7 @@ "openai": "^4.86.2", "dotenv": "16.4.7", "@agent-infra/shared": "workspace:*", + "@agent-infra/logger": "workspace:*", "@agent-infra/mcp-client": "workspace:*", "@agent-infra/search": "workspace:*", "ws": "8.18.1", diff --git a/apps/agent-tars/src/main/customTools/search.ts b/apps/agent-tars/src/main/customTools/search.ts index 83609fe96..10f314dad 100644 --- a/apps/agent-tars/src/main/customTools/search.ts +++ b/apps/agent-tars/src/main/customTools/search.ts @@ -6,13 +6,16 @@ import { import { MCPToolResult } from '@main/type'; import { tavily as tavilyCore } from '@tavily/core'; import { SettingStore } from '@main/store/setting'; +import { logger } from '@main/utils/logger'; +import { maskSensitiveData } from '@main/utils/maskSensitiveData'; export const tavily = tavilyCore; const searchByTavily = async (options: { count: number; query: string }) => { const currentSearchConfig = SettingStore.get('search'); + const apiKey = process.env.TAVILY_API_KEY || currentSearchConfig?.apiKey; const client = tavily({ - apiKey: process.env.TAVILY_API_KEY || currentSearchConfig?.apiKey, + apiKey, }); const searchOptions = { maxResults: options.count, @@ -33,6 +36,11 @@ export async function search(toolCall: ToolCall): Promise { const args = JSON.parse(toolCall.function.arguments); try { + logger.info( + 'Search query:', + maskSensitiveData({ query: args.query, count: args.count }), + ); + if (!currentSearchConfig) { const client = new SearchClient({ provider: SearchProviderEnum.DuckduckgoSearch, @@ -105,7 +113,7 @@ export async function search(toolCall: ToolCall): Promise { }, ]; } catch (e) { - console.error('Search error:', e); + logger.error('Search error:', e); return [ { isError: true, diff --git a/apps/agent-tars/src/main/index.ts b/apps/agent-tars/src/main/index.ts index 5f01a00de..1a966613f 100644 --- a/apps/agent-tars/src/main/index.ts +++ b/apps/agent-tars/src/main/index.ts @@ -5,6 +5,8 @@ import { updateElectronApp, UpdateSourceType } from 'update-electron-app'; import { electronApp, optimizer, is } from '@electron-toolkit/utils'; import { ipcRoutes } from './ipcRoutes'; import icon from '../../resources/icon.png?asset'; +import MenuBuilder from './menu'; +import { logger } from './utils/logger'; class AppUpdater { constructor() { @@ -65,6 +67,7 @@ function createWindow(): void { mainWindow.on('ready-to-show', () => { mainWindow.show(); + logger.info('Application window is ready and shown'); }); mainWindow.webContents.setWindowOpenHandler((details) => { @@ -79,16 +82,21 @@ function createWindow(): void { } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')); } + // Set up the application menu + const menuBuilder = new MenuBuilder(mainWindow); + menuBuilder.buildMenu(); } const initializeApp = async () => { + logger.info('Initializing application'); + if (process.platform === 'darwin') { app.setAccessibilitySupportEnabled(true); const { ensurePermissions } = await import('@main/utils/systemPermissions'); const ensureScreenCapturePermission = ensurePermissions(); - console.info( - 'ensureScreenCapturePermission', + logger.info( + 'Screen capture permission status:', ensureScreenCapturePermission, ); } @@ -98,6 +106,8 @@ const initializeApp = async () => { // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(async () => { + logger.info('Application is ready'); + // Set app user model id for windows electronApp.setAppUserModelId('com.electron'); @@ -113,7 +123,10 @@ app.whenReady().then(async () => { await initializeApp(); // IPC test - ipcMain.on('ping', () => console.log('pong')); + ipcMain.on('ping', () => { + logger.info('Received ping event'); + logger.info('pong'); + }); registerIpcMain(ipcRoutes); createWindow(); @@ -129,6 +142,7 @@ app.whenReady().then(async () => { // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { + logger.info('All windows closed'); if (process.platform !== 'darwin') { app.quit(); } diff --git a/apps/agent-tars/src/main/ipcRoutes/action.ts b/apps/agent-tars/src/main/ipcRoutes/action.ts index 0f3d71c6d..be405d9b8 100644 --- a/apps/agent-tars/src/main/ipcRoutes/action.ts +++ b/apps/agent-tars/src/main/ipcRoutes/action.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { MCPServerName, ToolCall } from '@agent-infra/shared'; import { executeCustomTool, listCustomTools } from '@main/customTools'; import { createMcpClient, getOmegaDir } from '@main/mcp/client'; @@ -15,6 +16,8 @@ import { normalizeMessages, parseArtifacts, } from '@main/utils/normalizeOmegaData'; +import { logger } from '@main/utils/logger'; +import { extractToolNames } from '@main/utils/extractToolNames'; export interface MCPTool { id: string; @@ -44,7 +47,7 @@ export const actionRoute = t.router({ listTools: t.procedure.handle(async () => { const mcpClient = await createMcpClient(); const tools = mcpToolsToAzureTools(await mcpClient.listTools()); - console.log('toolstools', tools); + logger.info('[actionRoute.listTools] tools', extractToolNames(tools)); const customTools = listCustomTools(); return [ ...tools.map((tool) => tool.function), @@ -74,7 +77,11 @@ export const actionRoute = t.router({ for (const toolCall of input.toolCalls) { const mcpTool = toolUseToMcpTool(tools, toolCall); if (mcpTool) { - console.log('i will execute tool', mcpTool.name, mcpTool.inputSchema); + logger.info( + '[actionRoute.executeTool] i will execute mcp tool', + mcpTool.name, + mcpTool.inputSchema || {}, + ); try { const result = await mcpClient.callTool({ client: mcpTool.serverName as MCPServerName, @@ -83,16 +90,26 @@ export const actionRoute = t.router({ }); results.push(result); } catch (e) { - console.error('execute tool error', mcpTool, e); + logger.error( + '[actionRoute.executeTool] execute tool error', + mcpTool.name, + e, + ); results.push({ isError: true, content: [JSON.stringify(e)], }); } } else { - console.log('executeCustomTool_toolCall', toolCall); + logger.info( + '[actionRoute.executeTool] executeCustomTool_toolCall', + toolCall, + ); const result = await executeCustomTool(toolCall); - console.log('executeCustomTool_result', result); + logger.info( + '[actionRoute.executeTool] executeCustomTool_result', + result ? 'success' : 'no result', + ); if (result) { results.push(...result); } @@ -102,6 +119,7 @@ export const actionRoute = t.router({ }), saveBrowserSnapshot: t.procedure.input().handle(async () => { + logger.info('[actionRoute.saveBrowserSnapshot] start'); const mcpClient = await createMcpClient(); try { const result = await mcpClient.callTool({ @@ -128,7 +146,10 @@ export const actionRoute = t.router({ await fs.writeFile(filepath, imageBuffer); return { filepath }; } catch (e) { - console.error('Failed to save screenshot:', e); + logger.error( + '[actionRoute.saveBrowserSnapshot] Failed to save screenshot:', + e, + ); throw e; } }), @@ -183,7 +204,7 @@ export const actionRoute = t.router({ await fs.remove(tempPath); if (!res.ok) { - console.error('Upload failed:', await res.text()); + logger.error('Upload failed:', await res.text()); throw new Error('文件上传失败'); } @@ -195,7 +216,7 @@ export const actionRoute = t.router({ await shell.openExternal(data.url); return data.url; } catch (error) { - console.error('Upload failed:', error); + logger.error('Upload failed:', error); throw error; } } else { diff --git a/apps/agent-tars/src/main/ipcRoutes/agent.ts b/apps/agent-tars/src/main/ipcRoutes/agent.ts index 30e24b9d8..47a1a8392 100644 --- a/apps/agent-tars/src/main/ipcRoutes/agent.ts +++ b/apps/agent-tars/src/main/ipcRoutes/agent.ts @@ -1,10 +1,11 @@ import { initIpc } from '@ui-tars/electron-ipc/main'; +import { logger } from '@main/utils/logger'; const t = initIpc.create(); export const agentRoute = t.router({ runAgent: t.procedure.input().handle(async () => { - console.log('runAgent'); + logger.info('runAgent'); return 'Hello'; }), }); diff --git a/apps/agent-tars/src/main/ipcRoutes/filesystem.ts b/apps/agent-tars/src/main/ipcRoutes/filesystem.ts index 424710b1c..a5d3b7dd9 100644 --- a/apps/agent-tars/src/main/ipcRoutes/filesystem.ts +++ b/apps/agent-tars/src/main/ipcRoutes/filesystem.ts @@ -3,6 +3,7 @@ import { setAllowedDirectories, getAllowedDirectories } from '@main/mcp/client'; import path from 'path'; import os from 'os'; import fs from 'fs-extra'; +import { logger } from '@main/utils/logger'; const t = initIpc.create(); @@ -40,7 +41,7 @@ export const fileSystemRoute = t.router({ const content = await fs.readFile(input.filePath, 'utf8'); return content; } catch (error) { - console.error('Failed to read file:', error); + logger.error('Failed to read file:', error); return null; } }), @@ -48,7 +49,7 @@ export const fileSystemRoute = t.router({ try { return await getAllowedDirectories(); } catch (error) { - console.error('Failed to get allowed directories:', error); + logger.error('Failed to get allowed directories:', error); const omegaDir = path.join(os.homedir(), '.omega'); return [omegaDir]; } diff --git a/apps/agent-tars/src/main/ipcRoutes/llm.ts b/apps/agent-tars/src/main/ipcRoutes/llm.ts index ec69ef942..4f33e3d8b 100644 --- a/apps/agent-tars/src/main/ipcRoutes/llm.ts +++ b/apps/agent-tars/src/main/ipcRoutes/llm.ts @@ -10,6 +10,9 @@ import { BrowserWindow } from 'electron'; import { createLLM, LLMConfig } from '@main/llmProvider'; import { ProviderFactory } from '@main/llmProvider/ProviderFactory'; import { SettingStore } from '@main/store/setting'; +import { logger } from '@main/utils/logger'; +import { maskSensitiveData } from '@main/utils/maskSensitiveData'; +import { extractToolNames } from '@main/utils/extractToolNames'; const t = initIpc.create(); @@ -42,6 +45,7 @@ export const llmRoute = t.router({ requestId: string; }>() .handle(async ({ input }) => { + logger.info('[llmRoute.askLLMText] input', input); const messages = input.messages.map((msg) => new Message(msg)); const llm = createLLM(currentLLMConfigRef.current); const response = await llm.askLLMText({ @@ -59,11 +63,18 @@ export const llmRoute = t.router({ requestId: string; }>() .handle(async ({ input }) => { + logger.info('[llmRoute.askLLMTool] input', input); const messages = input.messages.map((msg) => new Message(msg)); const llm = createLLM(currentLLMConfigRef.current); - console.log('current llm config', currentLLMConfigRef.current); - console.log('current search config', SettingStore.get('search')); - console.log('input.tools', input.tools); + logger.info( + '[llmRoute.askLLMTool] Current LLM Config', + maskSensitiveData(currentLLMConfigRef.current), + ); + logger.info( + '[llmRoute.askLLMTool] Current Search Config', + maskSensitiveData(SettingStore.get('search')), + ); + logger.info('[llmRoute.askLLMTool] tools', extractToolNames(input.tools)); const response = await llm.askTool({ messages, tools: input.tools, @@ -80,9 +91,13 @@ export const llmRoute = t.router({ requestId: string; }>() .handle(async ({ input }) => { + logger.info('[llmRoute.askLLMTextStream] input', input); const messages = input.messages.map((msg) => new Message(msg)); const { requestId } = input; - console.log('current llm config', currentLLMConfigRef.current); + logger.info( + '[llmRoute.askLLMTextStream] Current LLM Config', + maskSensitiveData(currentLLMConfigRef.current), + ); const llm = createLLM(currentLLMConfigRef.current); (async () => { @@ -120,12 +135,16 @@ export const llmRoute = t.router({ updateLLMConfig: t.procedure .input() .handle(async ({ input }) => { + logger.info('[llmRoute.updateLLMConfig] input', maskSensitiveData(input)); try { SettingStore.set('model', input); currentLLMConfigRef.current = getLLMProviderConfig(input); return true; } catch (error) { - console.error('Failed to update LLM configuration:', error); + logger.error( + '[llmRoute.updateLLMConfig] Failed to update LLM configuration:', + error, + ); return false; } }), @@ -134,19 +153,23 @@ export const llmRoute = t.router({ try { return ProviderFactory.getAvailableProviders(); } catch (error) { - console.error('Failed to get available providers:', error); + logger.error( + '[llmRoute.getAvailableProviders] Failed to get available providers:', + error, + ); return []; } }), abortRequest: t.procedure .input<{ requestId: string }>() .handle(async ({ input }) => { + logger.info('[llmRoute.abortRequest] input', input); try { const llm = createLLM(currentLLMConfigRef.current); llm.abortRequest(input.requestId); return true; } catch (error) { - console.error('Failed to abort request:', error); + logger.error('[llmRoute.abortRequest] Failed to abort request:', error); return false; } }), diff --git a/apps/agent-tars/src/main/ipcRoutes/search.ts b/apps/agent-tars/src/main/ipcRoutes/search.ts index 60cfe572a..d07933833 100644 --- a/apps/agent-tars/src/main/ipcRoutes/search.ts +++ b/apps/agent-tars/src/main/ipcRoutes/search.ts @@ -1,6 +1,8 @@ import { initIpc } from '@ui-tars/electron-ipc/main'; import { SearchSettings } from '@agent-infra/shared'; import { SettingStore } from '@main/store/setting'; +import { logger } from '@main/utils/logger'; +import { maskSensitiveData } from '@main/utils/maskSensitiveData'; const t = initIpc.create(); @@ -9,10 +11,17 @@ export const searchRoute = t.router({ .input() .handle(async ({ input }) => { try { + logger.info( + '[searchRoute.updateSearchConfig] Updating search configuration:', + maskSensitiveData(input), + ); SettingStore.set('search', input); return true; } catch (error) { - console.error('Failed to update search configuration:', error); + logger.error( + '[searchRoute.updateSearchConfig] Failed to update search configuration:', + error, + ); return false; } }), diff --git a/apps/agent-tars/src/main/llmProvider/ProviderFactory.ts b/apps/agent-tars/src/main/llmProvider/ProviderFactory.ts index 65ca72563..510e1f343 100644 --- a/apps/agent-tars/src/main/llmProvider/ProviderFactory.ts +++ b/apps/agent-tars/src/main/llmProvider/ProviderFactory.ts @@ -4,6 +4,7 @@ import { AnthropicProvider } from './providers/AnthropicProvider'; import { AzureOpenAIProvider } from './providers/AzureOpenAIProvider'; import { GeminiProvider } from './providers/GeminiProvider'; import { MistralProvider } from './providers/MistralProvider'; +import { logger } from '@main/utils/logger'; // Define model prefixes that will be used to determine the provider const MODEL_PREFIXES = { @@ -63,7 +64,7 @@ export class ProviderFactory { } // Default to OpenAI if model doesn't match any known prefix - console.warn( + logger.warn( `Unknown model prefix: ${model}. Defaulting to OpenAI provider.`, ); return new OpenAIProvider(config); diff --git a/apps/agent-tars/src/main/llmProvider/index.ts b/apps/agent-tars/src/main/llmProvider/index.ts index b5321301f..e55c3887b 100644 --- a/apps/agent-tars/src/main/llmProvider/index.ts +++ b/apps/agent-tars/src/main/llmProvider/index.ts @@ -11,6 +11,8 @@ import { LLMResponse, ToolChoice, } from './interfaces/LLMProvider'; +import { logger } from '@main/utils/logger'; +import { maskSensitiveData } from '@main/utils/maskSensitiveData'; // Load environment variables dotenv.config(); @@ -93,14 +95,19 @@ export class LLM { ); const allTools = [...tools, ...normalizeMcpTools, ...customTools]; - return this.provider.askTool({ + return await this.provider.askTool({ messages, tools: allTools, requestId, toolChoice: toolChoice || 'auto', }); - } catch (error: any) { - throw new Error(`Failed to get tool response from LLM: ${error}`); + } catch (error) { + const errorMessage = + error instanceof Error + ? `Failed to get tool response from LLM: ${error.message}` + : JSON.stringify(error); + logger.error(errorMessage); + throw new Error(errorMessage); } } @@ -135,6 +142,7 @@ export class LLM { } } -export function createLLM(config: LLMConfig = {}) { +export function createLLM(config: LLMConfig): LLM { + logger.info('[LLM] Creating LLM with config:', maskSensitiveData(config)); return new LLM(config); } diff --git a/apps/agent-tars/src/main/menu.ts b/apps/agent-tars/src/main/menu.ts new file mode 100644 index 000000000..dac49c03c --- /dev/null +++ b/apps/agent-tars/src/main/menu.ts @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2025 Bytedance, Inc. and its affiliates. + * SPDX-License-Identifier: Apache-2.0 + */ +import { + Menu, + shell, + BrowserWindow, + MenuItemConstructorOptions, +} from 'electron'; +import { openLogFile, openLogDir } from './utils/logger'; + +export default class MenuBuilder { + mainWindow: BrowserWindow; + + constructor(mainWindow: BrowserWindow) { + this.mainWindow = mainWindow; + } + + buildMenu(): Menu { + if ( + process.env.NODE_ENV === 'development' || + process.env.DEBUG_PROD === 'true' + ) { + this.setupDevelopmentEnvironment(); + } + + const template = this.buildDefaultTemplate(); + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); + + return menu; + } + + setupDevelopmentEnvironment(): void { + this.mainWindow.webContents.on('context-menu', (_, props) => { + const { x, y } = props; + Menu.buildFromTemplate([ + { + label: 'Inspect Element', + click: () => { + this.mainWindow.webContents.inspectElement(x, y); + }, + }, + ]).popup({ window: this.mainWindow }); + }); + } + + buildDefaultTemplate(): MenuItemConstructorOptions[] { + const subMenuHelp: MenuItemConstructorOptions = { + label: 'Help', + submenu: [ + { + label: 'View Logs', + click: async () => { + await openLogFile(); + }, + }, + { + label: 'Open Log Directory', + click: async () => { + await openLogDir(); + }, + }, + { type: 'separator' }, + { + label: 'Learn More', + click() { + shell.openExternal('https://github.com/bytedance/UI-TARS-desktop'); + }, + }, + ], + }; + + const subMenuFile: MenuItemConstructorOptions = { + label: 'File', + submenu: [ + { + label: 'Close', + accelerator: 'CmdOrCtrl+W', + click: () => { + this.mainWindow.close(); + }, + }, + ], + }; + + const subMenuEdit: MenuItemConstructorOptions = { + label: 'Edit', + submenu: [ + { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, + { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, + { type: 'separator' }, + { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, + { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, + { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, + { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }, + ], + }; + + const subMenuView: MenuItemConstructorOptions = { + label: 'View', + submenu: [ + { + label: 'Reload', + accelerator: 'CmdOrCtrl+R', + click: () => { + this.mainWindow.webContents.reload(); + }, + }, + { + label: 'Toggle Full Screen', + accelerator: process.platform === 'darwin' ? 'Ctrl+Cmd+F' : 'F11', + click: () => { + this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); + }, + }, + { + label: 'Toggle Developer Tools', + accelerator: + process.platform === 'darwin' ? 'Alt+Cmd+I' : 'Ctrl+Shift+I', + click: () => { + this.mainWindow.webContents.toggleDevTools(); + }, + }, + ], + }; + + const templateDefault = [ + subMenuFile, + subMenuEdit, + subMenuView, + subMenuHelp, + ]; + + return templateDefault; + } +} diff --git a/apps/agent-tars/src/main/store/setting.ts b/apps/agent-tars/src/main/store/setting.ts index 73a46da48..d77c0f8ec 100644 --- a/apps/agent-tars/src/main/store/setting.ts +++ b/apps/agent-tars/src/main/store/setting.ts @@ -12,6 +12,7 @@ import { FileSystemSettings, AppSettings, } from '@agent-infra/shared'; +import { logger } from '@main/utils/logger'; const DEFAULT_MODEL_SETTINGS: ModelSettings = { provider: ModelProvider.OPENAI, @@ -47,7 +48,7 @@ export class SettingStore { }); SettingStore.instance.onDidAnyChange((newValue, oldValue) => { - console.log( + logger.info( `SettingStore: ${JSON.stringify(oldValue)} changed to ${JSON.stringify(newValue)}`, ); // Notify that value updated diff --git a/apps/agent-tars/src/main/utils/extractToolNames.ts b/apps/agent-tars/src/main/utils/extractToolNames.ts new file mode 100644 index 000000000..3a2f33c9e --- /dev/null +++ b/apps/agent-tars/src/main/utils/extractToolNames.ts @@ -0,0 +1,13 @@ +import { ChatCompletionTool } from 'openai/resources/index.mjs'; + +/** + * Extract tool name list from tool object to avoid redundant logging + */ +export function extractToolNames(tools: ChatCompletionTool[]): string[] { + return tools.map((tool) => { + if (tool.type === 'function' && tool.function?.name) { + return tool.function.name; + } + return 'unknown_tool'; + }); +} diff --git a/apps/agent-tars/src/main/utils/logger.ts b/apps/agent-tars/src/main/utils/logger.ts new file mode 100644 index 000000000..a61980e50 --- /dev/null +++ b/apps/agent-tars/src/main/utils/logger.ts @@ -0,0 +1,157 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import path from 'path'; +import fs from 'fs-extra'; +import { shell } from 'electron'; +import { ConsoleLogger, LogLevel } from '@agent-infra/logger'; +import { getOmegaDir } from '../mcp/client'; + +// Ensure log directory exists +const ensureLogDir = async (): Promise => { + const omegaDir = await getOmegaDir(); + const logDir = path.join(omegaDir, 'logs'); + await fs.ensureDir(logDir); + return logDir; +}; + +// Create log file path +const createLogFilePath = async (): Promise => { + const logDir = await ensureLogDir(); + const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD + return path.join(logDir, `agent-tars-${date}.log`); +}; + +// File logger +class FileLogger extends ConsoleLogger { + private logFilePath: string | null = null; + private initPromise: Promise | null = null; + + constructor(prefix = '', level: LogLevel = LogLevel.INFO) { + super(prefix, level); + // Don't call async methods directly in constructor + // Initialize on first use instead + } + + private async initLogFile(): Promise { + // If initialization is already in progress, return that Promise + if (this.initPromise) { + return this.initPromise; + } + + // Create new initialization Promise + this.initPromise = (async () => { + try { + // If already initialized, return + if (this.logFilePath) return; + + this.logFilePath = await createLogFilePath(); + // Ensure log file exists + await fs.ensureFile(this.logFilePath); + + // Add startup marker + const timestamp = new Date().toISOString(); + await fs.appendFile( + this.logFilePath, + `\n\n--- Agent TARS started at ${timestamp} ---\n\n`, + ); + } catch (error) { + console.error('Failed to initialize log file:', error); + // Reset Promise to allow retry on next attempt + this.initPromise = null; + } + })(); + + return this.initPromise; + } + + private async writeToFile(message: string) { + // Ensure log file is initialized + await this.initLogFile(); + + if (this.logFilePath) { + try { + const timestamp = new Date().toISOString(); + await fs.appendFile(this.logFilePath, `[${timestamp}] ${message}\n`); + } catch (error) { + console.error('Failed to write to log file:', error); + } + } + } + + override log(...args: any[]): void { + super.log(...args); + this.writeToFile(`[LOG] ${args.join(' ')}`); + } + + override info(...args: any[]): void { + super.info(...args); + this.writeToFile( + `[INFO] ${args.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg)).join(' ')}`, + ); + } + + override warn(...args: any[]): void { + super.warn(...args); + this.writeToFile(`[WARN] ${args.join(' ')}`); + } + + override error(...args: any[]): void { + super.error(...args); + this.writeToFile(`[ERROR] ${args.join(' ')}`); + } + + override success(message: string): void { + super.success(message); + this.writeToFile(`[SUCCESS] ${message}`); + } + + override infoWithData( + message: string, + data?: T, + transformer?: (value: T) => any, + ): void { + super.infoWithData(message, data, transformer); + const transformedData = data + ? transformer + ? transformer(data) + : data + : ''; + this.writeToFile(`[INFO] ${message} ${JSON.stringify(transformedData)}`); + } + + // Get log file path + async getLogPath(): Promise { + await this.initLogFile(); + return this.logFilePath || ''; + } + + // Get log directory + async getLogDir(): Promise { + return ensureLogDir(); + } + + // Open log file + async openLogFile(): Promise { + const logPath = await this.getLogPath(); + if (logPath) { + await shell.openPath(logPath); + } + } + + // Open log directory + async openLogDir(): Promise { + const logDir = await this.getLogDir(); + await shell.openPath(logDir); + } +} + +// Create global logger instance +export const logger = new FileLogger('[Agent-TARS]', LogLevel.INFO); + +// Export convenience methods +export const openLogFile = async () => { + await logger.openLogFile(); +}; + +export const openLogDir = async () => { + await logger.openLogDir(); +}; diff --git a/apps/agent-tars/src/main/utils/maskSensitiveData.ts b/apps/agent-tars/src/main/utils/maskSensitiveData.ts new file mode 100644 index 000000000..15f6a5375 --- /dev/null +++ b/apps/agent-tars/src/main/utils/maskSensitiveData.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Mask sensitive information in objects + * Primarily used to process configuration objects before logging to prevent sensitive information leakage + */ +export function maskSensitiveData>( + data: T, + sensitiveKeys: string[] = ['apiKey', 'token', 'secret', 'password', 'key'], +): T { + if (!data || typeof data !== 'object') { + return data; + } + + // Create a deep copy to avoid modifying the original object + const maskedData = JSON.parse(JSON.stringify(data)) as T; + + // Process objects recursively + const maskObject = (obj: Record) => { + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + // Check if it's a sensitive key + if ( + sensitiveKeys.some((sk) => + key.toLowerCase().includes(sk.toLowerCase()), + ) + ) { + if (typeof obj[key] === 'string' && obj[key].length > 0) { + // Keep the first 4 and last 4 characters, replace the middle with asterisks + const value = obj[key]; + if (value.length <= 8) { + obj[key] = '********'; + } else { + obj[key] = + `${value.substring(0, 4)}${'*'.repeat(value.length - 8)}${value.substring(value.length - 4)}`; + } + } + } else if (obj[key] && typeof obj[key] === 'object') { + // Process nested objects recursively + maskObject(obj[key]); + } + } + } + }; + + maskObject(maskedData); + return maskedData; +} diff --git a/apps/agent-tars/src/main/utils/normalizeOmegaData.ts b/apps/agent-tars/src/main/utils/normalizeOmegaData.ts index b16083f87..5193c65f3 100644 --- a/apps/agent-tars/src/main/utils/normalizeOmegaData.ts +++ b/apps/agent-tars/src/main/utils/normalizeOmegaData.ts @@ -1,5 +1,7 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { readFile } from 'fs-extra'; import path from 'path'; +import { logger } from './logger'; interface ImageItem { type: 'image'; @@ -130,7 +132,7 @@ export async function parseArtifacts(messages: Array) { artifacts[fileName] = { content }; } } catch (error) { - console.error(`Failed to read file: ${artifactPath}`, error); + logger.error(`Failed to read file: ${artifactPath}`, error); } }), ); diff --git a/apps/agent-tars/src/main/utils/systemPermissions.ts b/apps/agent-tars/src/main/utils/systemPermissions.ts index d61780bb5..4dd5cda95 100644 --- a/apps/agent-tars/src/main/utils/systemPermissions.ts +++ b/apps/agent-tars/src/main/utils/systemPermissions.ts @@ -3,19 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ import permissions from '@computer-use/node-mac-permissions'; +import { logger } from './logger'; let hasAccessibilityPermission = false; const wrapWithWarning = (message, nativeFunction) => (...args) => { - console.warn(message); + logger.warn(message); return nativeFunction(...args); }; const askForAccessibility = (nativeFunction, functionName) => { const accessibilityStatus = permissions.getAuthStatus('accessibility'); - console.info('[accessibilityStatus]', accessibilityStatus); + logger.info('[accessibilityStatus]', accessibilityStatus); if (accessibilityStatus === 'authorized') { hasAccessibilityPermission = true; @@ -44,7 +45,7 @@ export const ensurePermissions = (): { askForAccessibility(() => {}, 'execute accessibility'); - console.info('hasAccessibilityPermission', hasAccessibilityPermission); + logger.info('hasAccessibilityPermission', hasAccessibilityPermission); return { accessibility: hasAccessibilityPermission, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76c1c714a..a8957bd88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: specifier: '*' version: 4.19.3 devDependencies: + '@agent-infra/logger': + specifier: workspace:* + version: link:../../packages/agent-infra/logger '@agent-infra/mcp-client': specifier: workspace:* version: link:../../packages/agent-infra/mcp-client @@ -1346,7 +1349,7 @@ importers: version: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.5)(happy-dom@17.1.1)(jiti@2.4.2)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.0(@types/node@22.13.10)(typescript@5.7.3))(sass-embedded@1.83.4)(sass@1.85.1)(tsx@4.19.2)(yaml@2.7.0) vitest-browser-react: specifier: ^0.1.1 - version: 0.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(@vitest/browser@3.0.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vitest@3.0.5) + version: 0.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(@vitest/browser@3.0.5(@types/node@22.13.10)(bufferutil@4.0.9)(playwright@1.50.1)(typescript@5.7.3)(utf-8-validate@6.0.5)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(sass-embedded@1.83.4)(sass@1.85.1)(tsx@4.19.2)(yaml@2.7.0))(vitest@3.0.5))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.5)(happy-dom@17.1.1)(jiti@2.4.2)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.0(@types/node@22.13.10)(typescript@5.7.3))(sass-embedded@1.83.4)(sass@1.85.1)(tsx@4.19.2)(yaml@2.7.0)) packages/ui-tars/shared: devDependencies: @@ -24234,7 +24237,7 @@ snapshots: tsx: 4.19.3 yaml: 2.7.0 - vitest-browser-react@0.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(@vitest/browser@3.0.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vitest@3.0.5): + vitest-browser-react@0.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(@vitest/browser@3.0.5(@types/node@22.13.10)(bufferutil@4.0.9)(playwright@1.50.1)(typescript@5.7.3)(utf-8-validate@6.0.5)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(sass-embedded@1.83.4)(sass@1.85.1)(tsx@4.19.2)(yaml@2.7.0))(vitest@3.0.5))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.5)(happy-dom@17.1.1)(jiti@2.4.2)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.0(@types/node@22.13.10)(typescript@5.7.3))(sass-embedded@1.83.4)(sass@1.85.1)(tsx@4.19.2)(yaml@2.7.0)): dependencies: '@vitest/browser': 3.0.5(@types/node@22.13.10)(bufferutil@4.0.9)(playwright@1.50.1)(typescript@5.7.3)(utf-8-validate@6.0.5)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(sass-embedded@1.83.4)(sass@1.85.1)(tsx@4.19.2)(yaml@2.7.0))(vitest@3.0.5) react: 18.3.1