feat: 统一日志系统——结构化滚动文件 + api.log 插件通道 + 73 个 IPC 通道追踪 + pi 事件/渲染层错误归档
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { getLogger } from '../logger'
|
||||
const log = getLogger('browser')
|
||||
|
||||
import { app, BrowserWindow, session as electronSession, WebContentsView, type Rectangle, type Session } from 'electron'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
@@ -331,7 +334,7 @@ export class BrowserController {
|
||||
try {
|
||||
writeFileSync(this.settingsFile, JSON.stringify(this.settings, null, 2), 'utf8')
|
||||
} catch (err) {
|
||||
console.error('[browser] save settings failed:', err)
|
||||
log.error('save settings failed:', { err: err })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +409,7 @@ export class BrowserController {
|
||||
try {
|
||||
fn(state)
|
||||
} catch (err) {
|
||||
console.error('[browser] state listener error:', err)
|
||||
log.error('state listener error:', { err: err })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,7 +542,7 @@ export class BrowserController {
|
||||
try {
|
||||
view.webContents.debugger.attach('1.3')
|
||||
} catch (err) {
|
||||
console.warn('[browser] CDP attach 失败:', err)
|
||||
log.warn('CDP attach 失败:', { err: err })
|
||||
}
|
||||
this.tabs.set(tabId, tab)
|
||||
if (!this.activeTabId) this.activeTabId = tabId
|
||||
@@ -635,7 +638,7 @@ export class BrowserController {
|
||||
tab.view.setVisible(false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[browser] apply layout failed:', err)
|
||||
log.error('apply layout failed:', { err: err })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,7 +692,7 @@ export class BrowserController {
|
||||
const debuggerClient = tab.view.webContents.debugger
|
||||
if (debuggerClient.isAttached()) debuggerClient.detach()
|
||||
debuggerClient.attach('1.3')
|
||||
console.warn(`[browser] ${method} 超时,已重连调试通道`)
|
||||
log.warn(`CDP ${method} timeout, reattached`)
|
||||
} catch {
|
||||
// 页面已关闭
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import { join } from 'node:path'
|
||||
import { registerIpc } from './ipc'
|
||||
import { getPluginHost } from './plugins/plugin-host'
|
||||
import { getBrowserController } from './browser/browser-controller'
|
||||
import { getLogger, initLogger } from './logger'
|
||||
|
||||
initLogger()
|
||||
const log = getLogger('main')
|
||||
|
||||
process.env.APP_ROOT = join(__dirname, '..')
|
||||
|
||||
@@ -22,6 +26,7 @@ export const RENDERER_DIST = join(process.env.APP_ROOT, 'renderer')
|
||||
const APP_ICON = join(app.getAppPath(), 'build', 'icon.ico')
|
||||
|
||||
const agentService = registerIpc()
|
||||
log.info('main init', { version: app.getVersion(), packaged: app.isPackaged, devServer: VITE_DEV_SERVER_URL ?? '-' })
|
||||
|
||||
/** 窗口控制(自绘标题栏按钮走这三个 IPC;不使用系统 WCO,
|
||||
* 否则 overlay 的原生白色背景会盖住标题栏分隔线) */
|
||||
@@ -92,7 +97,7 @@ async function createWindow(): Promise<void> {
|
||||
// pi 会话初始化放在窗口就绪后,错误可以第一时间在 UI 上呈现
|
||||
agentService.ensureReady().catch((err: unknown) => {
|
||||
sessionInitialized = false
|
||||
console.error('[main] pi session init failed:', err)
|
||||
log.error('pi session init failed', { err })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -111,15 +116,17 @@ async function createWindow(): Promise<void> {
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
// 渲染层日志/加载失败转发到主进程 stdout,便于排查白屏/黑屏问题
|
||||
// 渲染层日志(含插件 renderer 模块 / iframe 页的 console)统一归档到日志文件
|
||||
win.webContents.on('console-message', (details) => {
|
||||
console.log(`[renderer:${details.level}] ${details.message} (${details.sourceId}:${details.lineNumber})`)
|
||||
const level = details.level === 'error' ? 'error' : details.level === 'warning' ? 'warn' : 'debug'
|
||||
if (level === 'debug' && !VITE_DEV_SERVER_URL) return
|
||||
log[level]('renderer console', { message: details.message, source: `${details.sourceId}:${details.lineNumber}` })
|
||||
})
|
||||
win.webContents.on('did-fail-load', (_e, code, desc, url) => {
|
||||
console.log(`[renderer:load-fail] ${code} ${desc} ${url}`)
|
||||
log.error('renderer load failed', { code, desc, url })
|
||||
})
|
||||
win.webContents.on('render-process-gone', (_e, details) => {
|
||||
console.log(`[renderer:gone] ${details.reason}`)
|
||||
log.error('renderer gone', { reason: details.reason })
|
||||
})
|
||||
if (process.env.ASO_DEVTOOLS === '1') {
|
||||
win.webContents.openDevTools({ mode: 'detach' })
|
||||
@@ -150,6 +157,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
app.setAppUserModelId('com.inkreach.agent')
|
||||
}
|
||||
Menu.setApplicationMenu(null)
|
||||
log.info('app ready')
|
||||
|
||||
// 插件宿主:扫描/装载 + 协议映射 + 插件启停后重建 agent 运行时
|
||||
const pluginHost = getPluginHost()
|
||||
|
||||
@@ -5,6 +5,26 @@ import { AgentService } from './pi/agent-service'
|
||||
import { getPluginHost } from './plugins/plugin-host'
|
||||
import { getBrowserController } from './browser/browser-controller'
|
||||
import { detectBrowserSources, importBrowserProfile } from './browser/browser-profile'
|
||||
import { getLogger } from './logger'
|
||||
|
||||
const log = getLogger('ipc')
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/** ipcMain.handle 追踪包装:成功 debug(含耗时)、失败 error(含通道/耗时)——Agent 自诊断的数据源之一 */
|
||||
function handle(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void {
|
||||
ipcMain.handle(channel, async (event, ...args: any[]) => {
|
||||
const started = Date.now()
|
||||
try {
|
||||
const result = await listener(event, ...args)
|
||||
log.debug('ipc ok', { channel, ms: Date.now() - started })
|
||||
return result
|
||||
} catch (err) {
|
||||
log.error('ipc failed', { channel, ms: Date.now() - started, err })
|
||||
throw err
|
||||
}
|
||||
})
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* 注册 IPC 通道并把 agent 事件转发到所有渲染窗口。
|
||||
@@ -21,7 +41,7 @@ export function registerIpc(): AgentService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('agent:send', (_event, text: unknown, images: unknown) => {
|
||||
handle('agent:send', (_event, text: unknown, images: unknown) => {
|
||||
if (typeof text !== 'string' || !text.trim()) {
|
||||
throw new Error('empty prompt')
|
||||
}
|
||||
@@ -37,49 +57,49 @@ export function registerIpc(): AgentService {
|
||||
return service.send(text, sendImages)
|
||||
})
|
||||
|
||||
ipcMain.handle('agent:steer', (_event, text: unknown) => {
|
||||
handle('agent:steer', (_event, text: unknown) => {
|
||||
if (typeof text !== 'string' || !text.trim()) {
|
||||
throw new Error('empty steer text')
|
||||
}
|
||||
return service.steer(text)
|
||||
})
|
||||
|
||||
ipcMain.handle('agent:stop', () => service.stop())
|
||||
handle('agent:stop', () => service.stop())
|
||||
|
||||
ipcMain.handle('agent:state', () =>
|
||||
handle('agent:state', () =>
|
||||
service.getState().catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
)
|
||||
|
||||
// ---- 会话管理 ----
|
||||
ipcMain.handle('sessions:list', () =>
|
||||
handle('sessions:list', () =>
|
||||
service.listSessions().catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle('sessions:new', () =>
|
||||
handle('sessions:new', () =>
|
||||
service.newSession().catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle('sessions:open', (_event, path: unknown) => {
|
||||
handle('sessions:open', (_event, path: unknown) => {
|
||||
if (typeof path !== 'string' || !path.trim()) throw new Error('invalid session path')
|
||||
return service.openSession(path).catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('sessions:rename', (_event, name: unknown) => {
|
||||
handle('sessions:rename', (_event, name: unknown) => {
|
||||
if (typeof name !== 'string' || !name.trim()) throw new Error('invalid session name')
|
||||
return service.renameSession(name.trim()).catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('sessions:delete', (_event, path: unknown) => {
|
||||
handle('sessions:delete', (_event, path: unknown) => {
|
||||
if (typeof path !== 'string' || !path.trim()) throw new Error('invalid session path')
|
||||
return service.deleteSession(path).catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
@@ -92,12 +112,12 @@ export function registerIpc(): AgentService {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
|
||||
ipcMain.handle('models:catalog', () => pass(() => service.getModelCatalog()))
|
||||
handle('models:catalog', () => pass(() => service.getModelCatalog()))
|
||||
|
||||
// ---- 自定义 API 提供商(models.json)----
|
||||
ipcMain.handle('models:custom:list', () => service.listCustomProviders())
|
||||
handle('models:custom:list', () => service.listCustomProviders())
|
||||
|
||||
ipcMain.handle('models:custom:save', (_event, config: unknown) => {
|
||||
handle('models:custom:save', (_event, config: unknown) => {
|
||||
if (typeof config !== 'object' || config === null) throw new Error('invalid custom provider')
|
||||
const c = config as Record<string, unknown>
|
||||
if (
|
||||
@@ -120,51 +140,51 @@ export function registerIpc(): AgentService {
|
||||
return pass(() => service.saveCustomProvider(normalized))
|
||||
})
|
||||
|
||||
ipcMain.handle('models:custom:delete', (_event, id: unknown) => {
|
||||
handle('models:custom:delete', (_event, id: unknown) => {
|
||||
if (typeof id !== 'string' || !id.trim()) throw new Error('invalid provider id')
|
||||
return pass(() => service.deleteCustomProvider(id))
|
||||
})
|
||||
|
||||
ipcMain.handle('models:set', (_event, providerId: unknown, modelId: unknown) => {
|
||||
handle('models:set', (_event, providerId: unknown, modelId: unknown) => {
|
||||
if (typeof providerId !== 'string' || typeof modelId !== 'string') throw new Error('invalid model')
|
||||
return pass(() => service.setModel(providerId, modelId))
|
||||
})
|
||||
|
||||
ipcMain.handle('auth:setKey', (_event, providerId: unknown, apiKey: unknown) => {
|
||||
handle('auth:setKey', (_event, providerId: unknown, apiKey: unknown) => {
|
||||
if (typeof providerId !== 'string' || typeof apiKey !== 'string' || !apiKey.trim()) {
|
||||
throw new Error('invalid api key')
|
||||
}
|
||||
return pass(() => service.setApiKey(providerId, apiKey.trim()))
|
||||
})
|
||||
|
||||
ipcMain.handle('auth:logout', (_event, providerId: unknown) => {
|
||||
handle('auth:logout', (_event, providerId: unknown) => {
|
||||
if (typeof providerId !== 'string') throw new Error('invalid provider')
|
||||
return pass(() => service.logoutProvider(providerId))
|
||||
})
|
||||
|
||||
// ---- 思考等级 / 压缩 / 分支摘要(pi 原生能力) ----
|
||||
ipcMain.handle('thinking:set', (_event, level: unknown) => {
|
||||
handle('thinking:set', (_event, level: unknown) => {
|
||||
if (typeof level !== 'string') throw new Error('invalid thinking level')
|
||||
return pass(() => service.setThinkingLevel(level as never))
|
||||
})
|
||||
|
||||
ipcMain.handle('session:compact', (_event, instructions: unknown) => {
|
||||
handle('session:compact', (_event, instructions: unknown) => {
|
||||
if (instructions !== undefined && instructions !== null && typeof instructions !== 'string') {
|
||||
throw new Error('invalid compaction instructions')
|
||||
}
|
||||
return pass(() => service.compactSession(instructions || undefined))
|
||||
})
|
||||
|
||||
ipcMain.handle('session:compact:abort', () => pass(() => service.abortCompaction()))
|
||||
handle('session:compact:abort', () => pass(() => service.abortCompaction()))
|
||||
|
||||
ipcMain.handle('session:autoCompact', (_event, enabled: unknown) => {
|
||||
handle('session:autoCompact', (_event, enabled: unknown) => {
|
||||
if (typeof enabled !== 'boolean') throw new Error('invalid flag')
|
||||
return pass(() => service.setAutoCompactionEnabled(enabled))
|
||||
})
|
||||
|
||||
ipcMain.handle('session:forkPoints', () => pass(() => service.listForkPoints()))
|
||||
handle('session:forkPoints', () => pass(() => service.listForkPoints()))
|
||||
|
||||
ipcMain.handle('session:navigate', (_event, entryId: unknown, options: unknown) => {
|
||||
handle('session:navigate', (_event, entryId: unknown, options: unknown) => {
|
||||
if (typeof entryId !== 'string' || !entryId.trim()) throw new Error('invalid entry id')
|
||||
const o = (options ?? {}) as { summarize?: unknown; customInstructions?: unknown }
|
||||
if (o.summarize !== undefined && typeof o.summarize !== 'boolean') throw new Error('invalid navigate options')
|
||||
@@ -180,33 +200,33 @@ export function registerIpc(): AgentService {
|
||||
})
|
||||
|
||||
// 会话末尾分叉为新会话(pi fork,原会话保留)
|
||||
ipcMain.handle('session:fork', () => pass(() => service.forkSession()))
|
||||
handle('session:fork', () => pass(() => service.forkSession()))
|
||||
|
||||
// 从会话树上的锚点分叉为新会话('at' 含该条为止 / 'before' 到其父条目为止)
|
||||
ipcMain.handle('session:forkAt', (_event, entryId: unknown, position: unknown) => {
|
||||
handle('session:forkAt', (_event, entryId: unknown, position: unknown) => {
|
||||
if (typeof entryId !== 'string' || !entryId.trim()) throw new Error('invalid entry id')
|
||||
if (position !== 'at' && position !== 'before') throw new Error('invalid fork position')
|
||||
return pass(() => service.forkSessionAt(entryId, position))
|
||||
})
|
||||
|
||||
// ---- 应用设置 ----
|
||||
ipcMain.handle('settings:get', () => service.getSettings())
|
||||
handle('settings:get', () => service.getSettings())
|
||||
|
||||
// 当前生效的系统提示词(pi 原生 + 技能注入,只读展示)
|
||||
ipcMain.handle('settings:getSystemPrompt', () => service.getSystemPrompt())
|
||||
handle('settings:getSystemPrompt', () => service.getSystemPrompt())
|
||||
|
||||
ipcMain.handle('settings:setTheme', (_event, id: unknown) => {
|
||||
handle('settings:setTheme', (_event, id: unknown) => {
|
||||
if (typeof id !== 'string' || !id.trim()) throw new Error('invalid theme id')
|
||||
service.setThemeColor(id.trim())
|
||||
})
|
||||
|
||||
ipcMain.handle('settings:setThemeMode', (_event, mode: unknown) => {
|
||||
handle('settings:setThemeMode', (_event, mode: unknown) => {
|
||||
if (mode !== 'light' && mode !== 'dark' && mode !== 'system') throw new Error('invalid theme mode')
|
||||
service.setThemeMode(mode)
|
||||
})
|
||||
|
||||
// 启用/禁用单个技能或命令(重建运行时使发现阶段过滤生效)
|
||||
ipcMain.handle('settings:setResourceEnabled', (_event, kind: unknown, name: unknown, enabled: unknown) => {
|
||||
handle('settings:setResourceEnabled', (_event, kind: unknown, name: unknown, enabled: unknown) => {
|
||||
if (kind !== 'skill' && kind !== 'command') throw new Error('invalid resource kind')
|
||||
if (typeof name !== 'string' || !name.trim() || name.length > 200) throw new Error('invalid resource name')
|
||||
if (typeof enabled !== 'boolean') throw new Error('invalid enabled flag')
|
||||
@@ -214,20 +234,20 @@ export function registerIpc(): AgentService {
|
||||
})
|
||||
|
||||
// ---- 工作区 ----
|
||||
ipcMain.handle('workspaces:list', () =>
|
||||
handle('workspaces:list', () =>
|
||||
service.listWorkspaces().catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle('workspaces:create', (_event, name: unknown) => {
|
||||
handle('workspaces:create', (_event, name: unknown) => {
|
||||
if (typeof name !== 'string' || !name.trim()) throw new Error('invalid workspace name')
|
||||
return service.createWorkspace(name).catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('workspaces:open', (_event, path: unknown) => {
|
||||
handle('workspaces:open', (_event, path: unknown) => {
|
||||
if (typeof path !== 'string' || !path.trim()) throw new Error('invalid workspace path')
|
||||
return service.openWorkspace(path).catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
@@ -235,7 +255,7 @@ export function registerIpc(): AgentService {
|
||||
})
|
||||
|
||||
// 调起系统目录选择器;取消返回 null
|
||||
ipcMain.handle('workspaces:browse', async () => {
|
||||
handle('workspaces:browse', async () => {
|
||||
const win = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]
|
||||
const result = await dialog.showOpenDialog(win, {
|
||||
title: '选择工作区目录',
|
||||
@@ -247,14 +267,14 @@ export function registerIpc(): AgentService {
|
||||
})
|
||||
|
||||
// ---- 工具审批 ----
|
||||
ipcMain.handle('approval:resolve', (_event, id: unknown, approved: unknown) => {
|
||||
handle('approval:resolve', (_event, id: unknown, approved: unknown) => {
|
||||
if (typeof id !== 'string' || typeof approved !== 'boolean') throw new Error('invalid approval')
|
||||
// settle 内部经 onSettle 订阅发出 approval_settled,此处无需重复广播
|
||||
service.approvals.settle(id, approved)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
ipcMain.handle('app:versions', () => ({
|
||||
handle('app:versions', () => ({
|
||||
app: process.env.npm_package_version ?? '0.0.0',
|
||||
electron: process.versions.electron,
|
||||
node: process.versions.node,
|
||||
@@ -263,38 +283,38 @@ export function registerIpc(): AgentService {
|
||||
}))
|
||||
|
||||
// ---- Chatbox 资源(/ 命令 + 技能、@ 工作区文件、+ 附件)----
|
||||
ipcMain.handle('chat:resources', () =>
|
||||
handle('chat:resources', () =>
|
||||
service.listChatResources().catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle('queue:set', (_event, steering: unknown, followUp: unknown) => {
|
||||
handle('queue:set', (_event, steering: unknown, followUp: unknown) => {
|
||||
const norm = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
|
||||
return service.setQueue(norm(steering), norm(followUp))
|
||||
})
|
||||
|
||||
ipcMain.handle('permissions:set', (_event, mode: unknown) => {
|
||||
handle('permissions:set', (_event, mode: unknown) => {
|
||||
if (mode !== 'confirm' && mode !== 'full') throw new Error('invalid permission mode')
|
||||
service.setPermissionMode(mode)
|
||||
})
|
||||
|
||||
// ---- 设置页:技能 / 命令详情 ----
|
||||
ipcMain.handle('resources:details', () =>
|
||||
handle('resources:details', () =>
|
||||
service.listResourceDetails().catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
)
|
||||
|
||||
// 在系统文件管理器中打开(技能/命令来源文件所在目录)
|
||||
ipcMain.handle('shell:openPath', (_event, target: unknown) => {
|
||||
handle('shell:openPath', (_event, target: unknown) => {
|
||||
if (typeof target !== 'string' || !target.trim()) throw new Error('invalid path')
|
||||
return shell.openPath(target)
|
||||
})
|
||||
|
||||
// 打开资源目录:不存在时先创建(skills/prompts 目录通常尚未建立)
|
||||
ipcMain.handle('shell:openDirectory', (_event, target: unknown) => {
|
||||
handle('shell:openDirectory', (_event, target: unknown) => {
|
||||
if (typeof target !== 'string' || !target.trim()) throw new Error('invalid path')
|
||||
try {
|
||||
mkdirSync(target, { recursive: true })
|
||||
@@ -307,13 +327,13 @@ export function registerIpc(): AgentService {
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('files:list', () =>
|
||||
handle('files:list', () =>
|
||||
service.listWorkspaceFiles().catch((err: unknown) => {
|
||||
throw new Error(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle('attachments:pick', async (event) => {
|
||||
handle('attachments:pick', async (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender) ?? BrowserWindow.getAllWindows()[0]
|
||||
return service.pickAttachments(win ?? undefined)
|
||||
})
|
||||
@@ -337,30 +357,30 @@ export function registerIpc(): AgentService {
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle('browser:getState', () => browserPass(() => getBrowserController().getState()))
|
||||
ipcMain.handle('browser:navigate', (_e, input: unknown) =>
|
||||
handle('browser:getState', () => browserPass(() => getBrowserController().getState()))
|
||||
handle('browser:navigate', (_e, input: unknown) =>
|
||||
browserPass(() => getBrowserController().navigateDisplay(typeof input === 'string' ? input : ''))
|
||||
)
|
||||
ipcMain.handle('browser:goBack', () => browserPass(() => getBrowserController().goBack()))
|
||||
ipcMain.handle('browser:goForward', () => browserPass(() => getBrowserController().goForward()))
|
||||
ipcMain.handle('browser:reload', () => browserPass(() => getBrowserController().reload()))
|
||||
ipcMain.handle('browser:newTab', (_e, url: unknown) =>
|
||||
handle('browser:goBack', () => browserPass(() => getBrowserController().goBack()))
|
||||
handle('browser:goForward', () => browserPass(() => getBrowserController().goForward()))
|
||||
handle('browser:reload', () => browserPass(() => getBrowserController().reload()))
|
||||
handle('browser:newTab', (_e, url: unknown) =>
|
||||
browserPass(() => getBrowserController().newTab(typeof url === 'string' && url ? url : undefined, false))
|
||||
)
|
||||
ipcMain.handle('browser:selectTab', (_e, tabId: unknown) =>
|
||||
handle('browser:selectTab', (_e, tabId: unknown) =>
|
||||
browserPass(() => getBrowserController().selectTab(typeof tabId === 'string' ? tabId : ''))
|
||||
)
|
||||
ipcMain.handle('browser:closeTab', (_e, tabId: unknown) =>
|
||||
handle('browser:closeTab', (_e, tabId: unknown) =>
|
||||
browserPass(() => getBrowserController().closeTab(typeof tabId === 'string' ? tabId : undefined))
|
||||
)
|
||||
ipcMain.handle('browser:close', () => browserPass(() => getBrowserController().close()))
|
||||
handle('browser:close', () => browserPass(() => getBrowserController().close()))
|
||||
// 渲染层冻结帧:菜单弹出需暂时隐藏原生视图前,截当前页面画面做占位
|
||||
ipcMain.handle('browser:captureTab', (_e, tabId: unknown) =>
|
||||
handle('browser:captureTab', (_e, tabId: unknown) =>
|
||||
browserPass(() => getBrowserController().captureTabDataUrl(typeof tabId === 'string' && tabId ? tabId : undefined))
|
||||
)
|
||||
ipcMain.handle('browser:acknowledgeRisk', () => browserPass(() => getBrowserController().acknowledgeRisk()))
|
||||
handle('browser:acknowledgeRisk', () => browserPass(() => getBrowserController().acknowledgeRisk()))
|
||||
// 渲染层 BrowserSlot 量出的占位矩形(原生 WebContentsView 对齐到 DOM 布局)
|
||||
ipcMain.handle('browser:layout', (_e, layout: unknown) => {
|
||||
handle('browser:layout', (_e, layout: unknown) => {
|
||||
const l = layout as { tabId?: unknown; visible?: unknown; revision?: unknown; bounds?: unknown }
|
||||
const { tabId, visible, revision } = l
|
||||
const bounds = l.bounds as { x?: unknown; y?: unknown; width?: unknown; height?: unknown } | undefined
|
||||
@@ -383,8 +403,8 @@ export function registerIpc(): AgentService {
|
||||
})
|
||||
|
||||
// ---- 本地浏览器配置导入 ----
|
||||
ipcMain.handle('browser:detectSources', () => browserPass(() => detectBrowserSources()))
|
||||
ipcMain.handle('browser:importProfile', (_e, sourceId: unknown, profileDir: unknown) => {
|
||||
handle('browser:detectSources', () => browserPass(() => detectBrowserSources()))
|
||||
handle('browser:importProfile', (_e, sourceId: unknown, profileDir: unknown) => {
|
||||
if (typeof sourceId !== 'string' || typeof profileDir !== 'string') throw new Error('invalid import source')
|
||||
return browserPass(() => importBrowserProfile(sourceId, profileDir))
|
||||
})
|
||||
@@ -395,32 +415,32 @@ export function registerIpc(): AgentService {
|
||||
getPluginHost().setRuntimeRebuilder(() => service.rebuildRuntime())
|
||||
})
|
||||
|
||||
ipcMain.handle('plugins:list', () => getPluginHost().list())
|
||||
ipcMain.handle('plugins:uiState', () => getPluginHost().uiState())
|
||||
handle('plugins:list', () => getPluginHost().list())
|
||||
handle('plugins:uiState', () => getPluginHost().uiState())
|
||||
// 外置插件来源目录(设置页「打开目录」)
|
||||
ipcMain.handle('plugins:dirs', () => getPluginHost().pluginDirs())
|
||||
handle('plugins:dirs', () => getPluginHost().pluginDirs())
|
||||
// 外置插件渲染层模块源码(渲染层以 Blob URL import,绕开 dev http 源对 plugin:// 的 CORS 限制)
|
||||
ipcMain.handle('plugins:readRenderer', (_e, id: unknown) => {
|
||||
handle('plugins:readRenderer', (_e, id: unknown) => {
|
||||
if (typeof id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(id)) throw new Error('invalid plugin id')
|
||||
return getPluginHost().readRendererSource(id)
|
||||
})
|
||||
// ---- 插件设置表单(声明式 schema;值持久化与动作回推在 plugin-host) ----
|
||||
ipcMain.handle('plugins:settingsSchemas', () => getPluginHost().settingsSchemasList())
|
||||
ipcMain.handle('plugins:settingsValues', () => getPluginHost().settingsValuesList())
|
||||
ipcMain.handle('plugins:setSettingValue', (_e, id: unknown, key: unknown, value: unknown) => {
|
||||
handle('plugins:settingsSchemas', () => getPluginHost().settingsSchemasList())
|
||||
handle('plugins:settingsValues', () => getPluginHost().settingsValuesList())
|
||||
handle('plugins:setSettingValue', (_e, id: unknown, key: unknown, value: unknown) => {
|
||||
if (typeof id !== 'string' || typeof key !== 'string' || !key) throw new Error('invalid setting value')
|
||||
getPluginHost().setSettingValue(id, key, value)
|
||||
})
|
||||
ipcMain.handle('plugins:setSettingsNav', (_e, id: unknown, on: unknown) => {
|
||||
handle('plugins:setSettingsNav', (_e, id: unknown, on: unknown) => {
|
||||
if (typeof id !== 'string' || typeof on !== 'boolean') throw new Error('invalid settings nav toggle')
|
||||
return getPluginHost().setSettingsNav(id, on)
|
||||
})
|
||||
ipcMain.handle('plugins:settingsAction', (_e, id: unknown, key: unknown) => {
|
||||
handle('plugins:settingsAction', (_e, id: unknown, key: unknown) => {
|
||||
if (typeof id !== 'string' || typeof key !== 'string') throw new Error('invalid settings action')
|
||||
getPluginHost().settingsAction(id, key)
|
||||
})
|
||||
// path/image 控件的系统选择框
|
||||
ipcMain.handle('plugins:pickPath', async (event, mode: unknown) => {
|
||||
handle('plugins:pickPath', async (event, mode: unknown) => {
|
||||
if (mode !== 'file' && mode !== 'directory' && mode !== 'image') throw new Error('invalid path mode')
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
const filters =
|
||||
@@ -435,7 +455,7 @@ export function registerIpc(): AgentService {
|
||||
return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0]
|
||||
})
|
||||
// image 控件缩略图:渲染层(dev 是 http 源)不能直接 <img src="file://">,回传 data URL
|
||||
ipcMain.handle('plugins:readImage', (_e, path: unknown) => {
|
||||
handle('plugins:readImage', (_e, path: unknown) => {
|
||||
if (typeof path !== 'string' || !path || !/\.(png|jpe?g|gif|webp|bmp|svg|ico|avif)$/i.test(path)) return null
|
||||
try {
|
||||
const buf = readFileSync(path)
|
||||
@@ -446,17 +466,17 @@ export function registerIpc(): AgentService {
|
||||
return null
|
||||
}
|
||||
})
|
||||
ipcMain.handle('plugins:setEnabled', (_e, id: unknown, enabled: unknown) => {
|
||||
handle('plugins:setEnabled', (_e, id: unknown, enabled: unknown) => {
|
||||
if (typeof id !== 'string' || typeof enabled !== 'boolean') throw new Error('invalid plugin toggle')
|
||||
return getPluginHost().setEnabled(id, enabled)
|
||||
})
|
||||
ipcMain.handle('plugins:reload', () => getPluginHost().reload())
|
||||
ipcMain.handle('plugins:openView', (_e, view: unknown, params: unknown) => {
|
||||
handle('plugins:reload', () => getPluginHost().reload())
|
||||
handle('plugins:openView', (_e, view: unknown, params: unknown) => {
|
||||
if (typeof view !== 'string' || !view) throw new Error('invalid view')
|
||||
getPluginHost().openView(view, params)
|
||||
})
|
||||
// 插件页(iframe)经渲染层桥转发来的状态胶囊写请求;pluginId 键控防越权
|
||||
ipcMain.handle('plugins:setStatus', (_e, pluginId: unknown, itemId: unknown, item: unknown) => {
|
||||
handle('plugins:setStatus', (_e, pluginId: unknown, itemId: unknown, item: unknown) => {
|
||||
if (typeof pluginId !== 'string' || typeof itemId !== 'string') throw new Error('invalid status item')
|
||||
getPluginHost().setStatusItem(pluginId, itemId, (item ?? null) as never)
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 统一日志基座(主进程):
|
||||
* - 结构化行:`<ISO时间> <LEVEL> [<scope>] <message> k=v ...`,文件按天滚动
|
||||
* (userData/logs/main-YYYY-MM-DD.log,保留 14 天)
|
||||
* - 级别:dev 默认 debug、打包默认 info;ASO_LOG_LEVEL=debug|info|warn|error 可覆盖
|
||||
* - 子 logger:getLogger(scope) —— 各模块用固定 scope(plugin-host / agent-service / ipc / renderer /
|
||||
* browser),插件日志经 plugin-host 注入 scope=plugin 且带 pluginId 字段
|
||||
* - 全局兜底:uncaughtException / unhandledRejection / will-quit 落盘
|
||||
*
|
||||
* Agent 自诊断约定:日志文件路径固定,pi 侧可直接读文件排查(见 docs/plugins/06-self-diagnosis.md)。
|
||||
*/
|
||||
import { app } from 'electron'
|
||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||
|
||||
const LEVEL_ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 }
|
||||
const RETAIN_DAYS = 14
|
||||
const MAX_LINE = 8 * 1024
|
||||
|
||||
let level: LogLevel = 'info'
|
||||
let logDir: string | null = null
|
||||
/** 当天文件名(含日期)缓存;跨天自动切换 */
|
||||
let fileDay = ''
|
||||
let fileReady = false
|
||||
/** 文件不可用时保留最近日志(进程内仍可读) */
|
||||
const memoryTail: string[] = []
|
||||
|
||||
function resolveLevel(): LogLevel {
|
||||
const env = process.env.ASO_LOG_LEVEL
|
||||
if (env === 'debug' || env === 'info' || env === 'warn' || env === 'error') return env
|
||||
return app.isPackaged ? 'info' : 'debug'
|
||||
}
|
||||
|
||||
function ensureFile(now: Date): string | null {
|
||||
const day = now.toISOString().slice(0, 10)
|
||||
if (fileReady && fileDay === day && logDir) return join(logDir, `main-${day}.log`)
|
||||
try {
|
||||
if (!logDir) logDir = join(app.getPath('userData'), 'logs')
|
||||
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
|
||||
pruneOldLogs(logDir, day)
|
||||
fileDay = day
|
||||
fileReady = true
|
||||
return join(logDir, `main-${day}.log`)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function pruneOldLogs(dir: string, today: string): void {
|
||||
try {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const m = /^main-(\d{4}-\d{2}-\d{2})\.log$/.exec(name)
|
||||
if (!m) continue
|
||||
const ageDays = (Date.now() - statSync(join(dir, name)).mtimeMs) / 86_400_000
|
||||
if (m[1] !== today && (ageDays > RETAIN_DAYS || m[1] > today)) unlinkSync(join(dir, name))
|
||||
}
|
||||
} catch {
|
||||
// 清理失败不影响写日志
|
||||
}
|
||||
}
|
||||
|
||||
function fmtValue(v: unknown): string {
|
||||
if (v === undefined) return 'undefined'
|
||||
if (v === null) return 'null'
|
||||
if (v instanceof Error) return v.stack ? v.stack.replace(/\s+/g, ' ').slice(0, 600) : v.message
|
||||
if (typeof v === 'object') {
|
||||
try {
|
||||
return JSON.stringify(v).slice(0, 400)
|
||||
} catch {
|
||||
return '[object]'
|
||||
}
|
||||
}
|
||||
return String(v).slice(0, 400)
|
||||
}
|
||||
|
||||
function write(level_: LogLevel, scope: string, message: string, kv?: Record<string, unknown>): void {
|
||||
if (LEVEL_ORDER[level_] < LEVEL_ORDER[level]) return
|
||||
const now = new Date()
|
||||
const parts = [
|
||||
now.toISOString(),
|
||||
level_.toUpperCase().padEnd(5),
|
||||
`[${scope}]`,
|
||||
message.slice(0, 500)
|
||||
]
|
||||
if (kv) {
|
||||
for (const [k, v] of Object.entries(kv)) parts.push(`${k}=${fmtValue(v)}`)
|
||||
}
|
||||
const line = parts.join(' ').slice(0, MAX_LINE)
|
||||
|
||||
// 控制台镜像(dev 终端可读)
|
||||
// eslint-disable-next-line no-console
|
||||
;(level_ === 'error' ? console.error : level_ === 'warn' ? console.warn : console.log)(line)
|
||||
|
||||
memoryTail.push(line)
|
||||
if (memoryTail.length > 500) memoryTail.shift()
|
||||
const file = ensureFile(now)
|
||||
if (file) {
|
||||
try {
|
||||
appendFileSync(file, line + '\n', 'utf8')
|
||||
} catch {
|
||||
fileReady = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
debug(message: string, kv?: Record<string, unknown>): void
|
||||
info(message: string, kv?: Record<string, unknown>): void
|
||||
warn(message: string, kv?: Record<string, unknown>): void
|
||||
error(message: string, kv?: Record<string, unknown>): void
|
||||
}
|
||||
|
||||
export function getLogger(scope: string): Logger {
|
||||
return {
|
||||
debug: (message, kv) => write('debug', scope, message, kv),
|
||||
info: (message, kv) => write('info', scope, message, kv),
|
||||
warn: (message, kv) => write('warn', scope, message, kv),
|
||||
error: (message, kv) => write('error', scope, message, kv)
|
||||
}
|
||||
}
|
||||
|
||||
/** 最近日志(内存尾部;诊断 IPC 预留) */
|
||||
export function recentLines(count = 100): string[] {
|
||||
return memoryTail.slice(-count)
|
||||
}
|
||||
|
||||
export function initLogger(): void {
|
||||
level = resolveLevel()
|
||||
const log = getLogger('logger')
|
||||
process.on('uncaughtException', (err) => {
|
||||
log.error('uncaughtException', { err })
|
||||
})
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
log.error('unhandledRejection', { err: reason })
|
||||
})
|
||||
app.on('will-quit', () => {
|
||||
log.info('app quit')
|
||||
})
|
||||
log.info('logger init', { level, userData: app.getPath('userData') })
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import { getLogger } from '../logger'
|
||||
const log = getLogger('agent-service')
|
||||
|
||||
import { app, dialog, shell } from 'electron'
|
||||
import { nativeTheme } from 'electron'
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
||||
@@ -170,7 +173,7 @@ export class AgentService {
|
||||
try {
|
||||
mkdirSync(this.defaultWorkspaceDir, { recursive: true })
|
||||
} catch (err) {
|
||||
console.error('[agent-service] init workspaces failed:', err)
|
||||
log.error('init workspaces failed:', { err: err })
|
||||
}
|
||||
const saved = this.settings.workspace
|
||||
this.workspaceDir = saved && existsSync(saved) ? saved : this.defaultWorkspaceDir
|
||||
@@ -194,7 +197,7 @@ export class AgentService {
|
||||
seen.add(path.toLowerCase())
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[agent-service] list workspaces failed:', err)
|
||||
log.error('list workspaces failed:', { err: err })
|
||||
}
|
||||
// 目录选择器添加的根目录之外的工作区
|
||||
for (const p of this.settings.customWorkspaces ?? []) {
|
||||
@@ -247,7 +250,7 @@ export class AgentService {
|
||||
try {
|
||||
writeFileSync(this.settingsFile, JSON.stringify(this.settings, null, 2), 'utf8')
|
||||
} catch (err) {
|
||||
console.error('[agent-service] save settings failed:', err)
|
||||
log.error('save settings failed:', { err: err })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +264,7 @@ export class AgentService {
|
||||
try {
|
||||
fn(event)
|
||||
} catch (err) {
|
||||
console.error('[agent-service] listener error:', err)
|
||||
log.error('listener error:', { err: err })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -775,7 +778,7 @@ export class AgentService {
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[agent-service] load skill details failed:', err)
|
||||
log.error('load skill details failed:', { err: err })
|
||||
}
|
||||
const commands: CommandDetail[] = []
|
||||
try {
|
||||
@@ -793,7 +796,7 @@ export class AgentService {
|
||||
}))
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('[agent-service] list command details failed:', err)
|
||||
log.error('list command details failed:', { err: err })
|
||||
}
|
||||
skills.sort((a, b) => a.name.localeCompare(b.name))
|
||||
commands.sort((a, b) => a.name.localeCompare(b.name))
|
||||
@@ -882,7 +885,7 @@ export class AgentService {
|
||||
}
|
||||
return { name, path, mimeType, data: readFileSync(path).toString('base64') }
|
||||
} catch (err) {
|
||||
console.error('[agent-service] read attachment failed:', err)
|
||||
log.error('read attachment failed:', { err: err })
|
||||
return { name, path, mimeType: '', data: '' }
|
||||
}
|
||||
}
|
||||
@@ -897,7 +900,7 @@ export class AgentService {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[agent-service] read attachment failed:', err)
|
||||
log.error('read attachment failed:', { err: err })
|
||||
}
|
||||
return { name, path, mimeType: '', data: '' }
|
||||
}
|
||||
@@ -1272,7 +1275,7 @@ export class AgentService {
|
||||
this.attached = null
|
||||
if (old) {
|
||||
await old.dispose().catch((err: unknown) => {
|
||||
console.error('[agent-service] runtime dispose error:', err)
|
||||
log.error('runtime dispose error:', { err: err })
|
||||
})
|
||||
}
|
||||
let runtime: AgentSessionRuntime
|
||||
|
||||
@@ -36,6 +36,10 @@ import type {
|
||||
} from '@aso/plugin-sdk'
|
||||
import type { PluginInfo, PluginStatusEntry, PluginToolbarEntry, PluginsUiState } from '@shared/types'
|
||||
import { BUILTIN_PLUGINS, type BuiltinPlugin } from './builtin'
|
||||
import { getLogger } from '../logger'
|
||||
|
||||
const log = getLogger('plugin-host')
|
||||
const pluginLog = getLogger('plugin')
|
||||
|
||||
/** harness 插件契约版本(manifest.minContract 与之比对;破坏性变更时提升) */
|
||||
export const HARNESS_CONTRACT_VERSION = '0.2.0'
|
||||
@@ -122,6 +126,10 @@ class PluginHost {
|
||||
} catch {
|
||||
// 已存在或创建失败都继续(扫描时再报)
|
||||
}
|
||||
log.info('plugin host init', {
|
||||
externalRoot: this.externalRoot,
|
||||
externalLoadingEnabled: EXTERNAL_PLUGINS_ENABLED
|
||||
})
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(this.settingsFile, 'utf8')) as { disabled?: unknown; settingsNav?: unknown }
|
||||
if (Array.isArray(raw.disabled)) this.disabled = new Set(raw.disabled.filter((d): d is string => typeof d === 'string'))
|
||||
@@ -234,6 +242,13 @@ class PluginHost {
|
||||
}
|
||||
this.send('plugins:settingsSchemas', this.settingsSchemasSnapshot())
|
||||
await this.loadExternalMains()
|
||||
const externalCount = [...byId.values()].filter((p) => p.source === 'external').length
|
||||
log.info('scan complete', {
|
||||
builtin: byId.size - externalCount,
|
||||
external: externalCount,
|
||||
disabled: this.disabled.size,
|
||||
projects: this.projectExtensionsDir ?? '-'
|
||||
})
|
||||
this.send('plugins:listChanged', null)
|
||||
}
|
||||
|
||||
@@ -279,27 +294,33 @@ class PluginHost {
|
||||
const mod = (await import(url)) as { default?: unknown }
|
||||
if (typeof mod.default !== 'function') {
|
||||
plugin.error = 'main.mjs 默认导出必须是插件工厂函数'
|
||||
log.error('external main invalid export', { pluginId: plugin.manifest.id, file })
|
||||
continue
|
||||
}
|
||||
plugin.externalFactory = mod.default as HarnessPluginFactory
|
||||
plugin.error = undefined
|
||||
log.info('external main loaded', { pluginId: plugin.manifest.id, version: plugin.manifest.version })
|
||||
} catch (err) {
|
||||
plugin.externalFactory = undefined
|
||||
plugin.error = `main 加载失败: ${err instanceof Error ? err.message : String(err)}`
|
||||
log.error('external main load failed', { pluginId: plugin.manifest.id, file, err })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 重新扫描 + 重新导入 + 清 UI 状态 + 重建 runtime(= pi /reload 等效) */
|
||||
async reload(): Promise<void> {
|
||||
log.info('reload begin')
|
||||
this.statusItems.clear()
|
||||
this.toolbarButtons.clear()
|
||||
await this.scan()
|
||||
this.send('plugins:uiState', this.uiState())
|
||||
if (this.rebuildRuntime) await this.rebuildRuntime()
|
||||
log.info('reload done')
|
||||
}
|
||||
|
||||
async setEnabled(id: string, enabled: boolean): Promise<void> {
|
||||
log.info('set plugin enabled', { pluginId: id, enabled })
|
||||
if (enabled) this.disabled.delete(id)
|
||||
else this.disabled.add(id)
|
||||
this.persistPluginPrefs()
|
||||
@@ -310,6 +331,7 @@ class PluginHost {
|
||||
async setSettingsNav(id: string, on: boolean): Promise<void> {
|
||||
const plugin = this.resolved.get(id)
|
||||
if (!plugin || !this.settingsSchemas.has(id)) return
|
||||
log.info('set settings nav', { pluginId: id, on })
|
||||
if (on) this.settingsNavSet.add(id)
|
||||
else this.settingsNavSet.delete(id)
|
||||
this.persistPluginPrefs()
|
||||
@@ -324,7 +346,7 @@ class PluginHost {
|
||||
'utf8'
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('[plugins] save settings failed:', err)
|
||||
log.error('save plugin prefs failed', { err })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,17 +399,18 @@ class PluginHost {
|
||||
try {
|
||||
fn({ ...(this.settingsValues.get(id) ?? {}) })
|
||||
} catch (err) {
|
||||
console.error('[plugins] settings listener error:', err)
|
||||
log.error('settings listener error', { pluginId: id, err })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settingsAction(id: string, key: string): void {
|
||||
log.debug('settings action', { pluginId: id, key })
|
||||
for (const fn of this.settingsActions.get(id) ?? []) {
|
||||
try {
|
||||
fn(key)
|
||||
} catch (err) {
|
||||
console.error('[plugins] settings action error:', err)
|
||||
log.error('settings action error', { pluginId: id, key, err })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,7 +424,7 @@ class PluginHost {
|
||||
try {
|
||||
writeFileSync(this.valuesFile, JSON.stringify(doc, null, 2), 'utf8')
|
||||
} catch (err) {
|
||||
console.error('[plugins] save values failed:', err)
|
||||
log.error('save values failed', { err })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
@@ -554,7 +577,7 @@ class PluginHost {
|
||||
try {
|
||||
writeFileSync(this.storageFile, JSON.stringify(doc, null, 2), 'utf8')
|
||||
} catch (err) {
|
||||
console.error('[plugins] save storage failed:', err)
|
||||
log.error('save storage failed', { err })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
@@ -574,6 +597,7 @@ class PluginHost {
|
||||
this.resetSettingsCallbacks(plugin.manifest.id)
|
||||
try {
|
||||
const result = factory(this.buildApi(plugin, pi))
|
||||
log.info('plugin activated', { pluginId: plugin.manifest.id, source: plugin.source })
|
||||
if (typeof result === 'function') {
|
||||
disposers.push(result)
|
||||
} else if (result && typeof (result as Promise<unknown>).then === 'function') {
|
||||
@@ -583,6 +607,7 @@ class PluginHost {
|
||||
}
|
||||
} catch (err) {
|
||||
plugin.error = `激活失败: ${err instanceof Error ? err.message : String(err)}`
|
||||
log.error('plugin activation failed', { pluginId: plugin.manifest.id, err })
|
||||
this.clearPluginUi(plugin.manifest.id)
|
||||
this.send('plugins:listChanged', null)
|
||||
}
|
||||
@@ -592,7 +617,7 @@ class PluginHost {
|
||||
try {
|
||||
dispose()
|
||||
} catch (err) {
|
||||
console.error('[plugins] dispose error:', err)
|
||||
log.error('plugin dispose error', { err })
|
||||
}
|
||||
}
|
||||
disposers.length = 0
|
||||
@@ -623,6 +648,12 @@ class PluginHost {
|
||||
pi,
|
||||
manifest: plugin.manifest,
|
||||
storage: this.storageFor(id),
|
||||
log: {
|
||||
debug: (message: string, kv?: Record<string, unknown>) => pluginLog.debug(message, { pluginId: id, ...kv }),
|
||||
info: (message: string, kv?: Record<string, unknown>) => pluginLog.info(message, { pluginId: id, ...kv }),
|
||||
warn: (message: string, kv?: Record<string, unknown>) => pluginLog.warn(message, { pluginId: id, ...kv }),
|
||||
error: (message: string, kv?: Record<string, unknown>) => pluginLog.error(message, { pluginId: id, ...kv })
|
||||
},
|
||||
ui: {
|
||||
openView: (view: string, params?: unknown) => this.openView(view, params),
|
||||
setStatusItem: (itemId: string, item: StatusItem) => this.setStatusItem(id, itemId, item),
|
||||
@@ -721,7 +752,7 @@ class PluginHost {
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[plugins] serve failed:', file, err)
|
||||
log.error('plugin:// serve failed', { file, err })
|
||||
return new Response('serve error', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 渲染层全局错误采集:window.onerror / unhandledrejection / 未处理的 React 边界外错误
|
||||
* 统一走 console.error,由宿主 console-message 桥归档进日志文件(main/index.ts)。
|
||||
* Agent 自诊断依赖:任何渲染层异常都必须能在 userData/logs/ 里看到。
|
||||
*/
|
||||
window.addEventListener('error', (e) => {
|
||||
// 资源加载失败(img/script)没有 error 对象,也一并记录
|
||||
const detail = e.error instanceof Error ? `${e.error.stack ?? e.error.message}` : `${e.message} @ ${e.filename}:${e.lineno}:${e.colno}`
|
||||
console.error(`[window.onerror] ${detail}`)
|
||||
})
|
||||
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
const reason = e.reason instanceof Error ? (e.reason.stack ?? e.reason.message) : String(e.reason)
|
||||
console.error(`[unhandledrejection] ${reason}`)
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './styles/globals.css'
|
||||
import './lib/error-report'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
|
||||
@@ -14,6 +14,15 @@ export interface PluginStorage {
|
||||
delete(key: string): void
|
||||
}
|
||||
|
||||
/** 插件日志通道:宿主注入,写入统一日志文件(userData/logs/,带 pluginId 字段)。
|
||||
* Agent 自诊断依赖该通道——插件应当把装载/激活/关键决策与失败都记到这里。 */
|
||||
export interface PluginLogger {
|
||||
debug(message: string, kv?: Record<string, unknown>): void
|
||||
info(message: string, kv?: Record<string, unknown>): void
|
||||
warn(message: string, kv?: Record<string, unknown>): void
|
||||
error(message: string, kv?: Record<string, unknown>): void
|
||||
}
|
||||
|
||||
/** harness 侧 UI 能力(Electron 版槽位,语义对位 pi TUI 的 ctx.ui.*)。
|
||||
* 内联槽位全部数据驱动;插件页内 UI 由插件页自理,不经此 API。 */
|
||||
export interface HarnessUiApi {
|
||||
@@ -42,6 +51,8 @@ export interface HarnessPluginApi {
|
||||
pi: ExtensionAPI
|
||||
ui: HarnessUiApi
|
||||
storage: PluginStorage
|
||||
/** 结构化日志(scope=plugin,自动带 pluginId;error 级别会进日志文件) */
|
||||
log: PluginLogger
|
||||
manifest: PluginManifest
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user