feat: 实验室新增基础工具用于调试

This commit is contained in:
digua
2026-04-07 23:42:23 +08:00
committed by digua
parent fa3282f625
commit 53208de60e
9 changed files with 618 additions and 8 deletions
+112
View File
@@ -21,6 +21,7 @@ import {
} from '@mariozechner/pi-ai'
import { t } from '../i18n'
import type { ToolContext } from '../ai/tools/types'
import { TOOL_REGISTRY } from '../ai/tools/definitions'
import { getDefaultRulesForLocale, mergeRulesForLocale } from '../ai/preprocessor/builtin-rules'
import type { IpcContext } from './types'
@@ -147,6 +148,29 @@ function formatAIError(error: unknown, provider?: llm.LLMProvider): string {
return friendlyMessage
}
/**
* 递归剥离对象中的 avatar/senderAvatar 字段(base64 大数据)
* 用于工具测试场景,避免传输和序列化大量无用头像数据
*/
function stripAvatarFields(obj: unknown): void {
if (!obj || typeof obj !== 'object') return
if (Array.isArray(obj)) {
for (const item of obj) stripAvatarFields(item)
return
}
const record = obj as Record<string, unknown>
for (const key of Object.keys(record)) {
if ((key === 'avatar' || key === 'senderAvatar') && typeof record[key] === 'string') {
const val = record[key] as string
if (val.length > 200) {
record[key] = '[stripped]'
}
} else if (typeof record[key] === 'object' && record[key] !== null) {
stripAvatarFields(record[key])
}
}
}
export function registerAIHandlers({ win }: IpcContext): void {
console.log('[IPC] Registering AI handlers...')
@@ -820,6 +844,94 @@ export function registerAIHandlers({ win }: IpcContext): void {
}
})
// ==================== 工具测试 API(实验室 - 基础工具) ====================
const activeToolTests = new Map<string, AbortController>()
ipcMain.handle('ai:getToolCatalog', async () => {
try {
return TOOL_REGISTRY.map((entry) => {
const dummyContext: ToolContext = { sessionId: '__catalog__' }
const tool = entry.factory(dummyContext)
const descKey = `ai.tools.${entry.name}.desc`
const translated = t(descKey)
return {
name: entry.name,
category: entry.category,
description: translated !== descKey ? translated : (tool.description ?? ''),
parameters: tool.parameters ?? {},
}
})
} catch (error) {
console.error('Failed to get tool catalog:', error)
return []
}
})
ipcMain.handle(
'ai:executeTool',
async (_, testId: string, toolName: string, params: Record<string, unknown>, sessionId: string) => {
const MAX_RESULT_CHARS = 500_000
const abortController = new AbortController()
activeToolTests.set(testId, abortController)
try {
const entry = TOOL_REGISTRY.find((e) => e.name === toolName)
if (!entry) {
return { success: false, error: `Tool not found: ${toolName}` }
}
const context: ToolContext = { sessionId }
const tool = entry.factory(context)
const startTime = Date.now()
const result = await tool.execute(`test_${Date.now()}`, params)
const elapsed = Date.now() - startTime
if (abortController.signal.aborted) {
return { success: false, error: 'cancelled' }
}
let details = result.details as Record<string, unknown> | undefined
let truncated = false
if (details) {
stripAvatarFields(details)
const raw = JSON.stringify(details)
if (raw.length > MAX_RESULT_CHARS) {
truncated = true
details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) }
}
}
return {
success: true,
elapsed,
content: result.content,
details,
truncated,
}
} catch (error) {
if (abortController.signal.aborted) {
return { success: false, error: 'cancelled' }
}
console.error(`Failed to execute tool ${toolName}:`, error)
return { success: false, error: String(error) }
} finally {
activeToolTests.delete(testId)
}
}
)
ipcMain.handle('ai:cancelToolTest', async (_, testId: string) => {
const controller = activeToolTests.get(testId)
if (controller) {
controller.abort()
activeToolTests.delete(testId)
return { success: true }
}
return { success: false }
})
// ==================== AI Agent API ====================
/**