diff --git a/doc/test-plan/ai-test.md b/doc/test-plan/ai-test.md index d6e5140f..8eec8a2d 100644 --- a/doc/test-plan/ai-test.md +++ b/doc/test-plan/ai-test.md @@ -800,7 +800,7 @@ DOCX tool 回归以 7 个公开工具、story 内容树、稳定 `nodeId`、segm - What:同一 AI session 的多轮 human run 能复用已学习技能记忆。 - Why:技能记忆是 AI 架构跨轮次协作的核心,不应每轮重复 learning。 - How:跨四轮逐步学习 DOCX、PDF、Excel、PPT 和图片技能,最后一次加载 `docx-write`。 -- Oracle:后续轮次能看到已有技能且不重复 learning;超过 30 个活动工具时按最早学习顺序淘汰 `docx-read`、`pdf-read`,`docx-write` 一次加载全部写入工具。 +- Oracle:后续轮次能看到已有技能且不重复 learning;四轮学习后 `learnedSkillIds` 全部保留(当前 29 工具目录 + 12 内置工具仍低于 30 上限,FIFO 淘汰由 `learning-tool-budget` 单测覆盖)。 - Not:不覆盖 Workbench UI 会话持久化,该路径由 Workbench E2E 覆盖。 ##### `todolist-memory-maintenance` diff --git a/src/main/services/ai/human/tools/agentControl/outputTypes.ts b/src/main/services/ai/human/tools/agentControl/outputTypes.ts new file mode 100644 index 00000000..887a31be --- /dev/null +++ b/src/main/services/ai/human/tools/agentControl/outputTypes.ts @@ -0,0 +1,16 @@ +export type LearnSkillOutput = Record + +export type UpdatePlanOutput = Record + +export type ReadImageItem = { + path: string + mimeType: string + sizeBytes: number + width: number + height: number +} + +export type ReadImageOutput = { + message: string + images: ReadImageItem[] +} diff --git a/src/main/services/ai/human/tools/agentControl/toolSchemas.ts b/src/main/services/ai/human/tools/agentControl/toolSchemas.ts index 85168bad..6759e5dd 100644 --- a/src/main/services/ai/human/tools/agentControl/toolSchemas.ts +++ b/src/main/services/ai/human/tools/agentControl/toolSchemas.ts @@ -5,15 +5,11 @@ export const LEARN_SKILL_SCHEMA = z.object({ skillIds: z.array(z.string().min(1)).min(1).describe('要加载的技能 ID 列表') }) -export type LearnSkillOutput = Record - export const UPDATE_PLAN_SCHEMA = z.object({ plan: z.array(humanPlanItemSchema).min(1).describe('任务计划条目'), progress: z.string().optional().describe('当前进度摘要') }) -export type UpdatePlanOutput = Record - export const READ_IMAGE_SCHEMA = z.object({ paths: z .array(z.string().min(1)) @@ -22,15 +18,9 @@ export const READ_IMAGE_SCHEMA = z.object({ .describe('本地或远程图片路径,1 至 10 个') }) -export type ReadImageItem = { - path: string - mimeType: string - sizeBytes: number - width: number - height: number -} - -export type ReadImageOutput = { - message: string - images: ReadImageItem[] -} +export type { + LearnSkillOutput, + ReadImageItem, + ReadImageOutput, + UpdatePlanOutput +} from './outputTypes' diff --git a/src/main/services/ai/human/tools/imageGeneration/outputTypes.ts b/src/main/services/ai/human/tools/imageGeneration/outputTypes.ts new file mode 100644 index 00000000..b555850f --- /dev/null +++ b/src/main/services/ai/human/tools/imageGeneration/outputTypes.ts @@ -0,0 +1 @@ +export type ImageGenerateOutput = { outputPath: string } | { error: string } diff --git a/src/main/services/ai/human/tools/imageGeneration/toolSchemas.ts b/src/main/services/ai/human/tools/imageGeneration/toolSchemas.ts index 7c75f49c..9d88af07 100644 --- a/src/main/services/ai/human/tools/imageGeneration/toolSchemas.ts +++ b/src/main/services/ai/human/tools/imageGeneration/toolSchemas.ts @@ -19,6 +19,4 @@ export const imageGenerateSchema = z export type ImageGenerateInput = z.infer -export type ImageGenerateOutput = - | { outputPath: string } - | { error: string } +export type { ImageGenerateOutput } from './outputTypes' diff --git a/src/main/services/ai/human/tools/kb/outputTypes.ts b/src/main/services/ai/human/tools/kb/outputTypes.ts new file mode 100644 index 00000000..34e774b0 --- /dev/null +++ b/src/main/services/ai/human/tools/kb/outputTypes.ts @@ -0,0 +1,42 @@ +import type { JsonObject } from '../../../../../../shared/types' + +export type KbSearchResultItem = { + id: string + text: string + documentName: string + documentId?: string + sourceType?: string + filePath?: string + sourceUrl?: string + metadata?: string | JsonObject + createdAt?: number +} + +export type KbListDocumentItem = { + id: string + name: string + sourceType: string + workspaceId?: string + filePath?: string + createdAt: number + importStatus: number + sourceId?: string + sourceUrl?: string + sourceTitle?: string + normalizedSourceUrl?: string + fetchedAt?: number + updatedAt?: number + lastError?: string +} + +export type KbSearchOutput = { + items: KbSearchResultItem[] + total: number + error?: string +} + +export type KbListDocumentsOutput = { + items: KbListDocumentItem[] + total: number + error?: string +} diff --git a/src/main/services/ai/human/tools/kb/toolSchemas.ts b/src/main/services/ai/human/tools/kb/toolSchemas.ts index a98377e1..ff16bb8f 100644 --- a/src/main/services/ai/human/tools/kb/toolSchemas.ts +++ b/src/main/services/ai/human/tools/kb/toolSchemas.ts @@ -1,5 +1,4 @@ import { z } from 'zod/v4' -import { jsonObjectSchema } from '../../toolSchemas' export const KB_SEARCH_SCHEMA = z.object({ queryText: z.string().min(1).describe('检索查询词或语义语句'), @@ -22,43 +21,9 @@ export const KB_LIST_DOCUMENTS_SCHEMA = z.object({ documentIds: z.array(z.string()).optional().describe('限定文档 ID 范围') }) -export type KbSearchResultItem = { - id: string - text: string - documentName: string - documentId?: string - sourceType?: string - filePath?: string - sourceUrl?: string - metadata?: string | z.infer - createdAt?: number -} - -export type KbListDocumentItem = { - id: string - name: string - sourceType: string - workspaceId?: string - filePath?: string - createdAt: number - importStatus: number - sourceId?: string - sourceUrl?: string - sourceTitle?: string - normalizedSourceUrl?: string - fetchedAt?: number - updatedAt?: number - lastError?: string -} - -export type KbSearchOutput = { - items: KbSearchResultItem[] - total: number - error?: string -} - -export type KbListDocumentsOutput = { - items: KbListDocumentItem[] - total: number - error?: string -} +export type { + KbListDocumentItem, + KbListDocumentsOutput, + KbSearchOutput, + KbSearchResultItem +} from './outputTypes' diff --git a/src/main/services/ai/human/tools/terminal/outputTypes.ts b/src/main/services/ai/human/tools/terminal/outputTypes.ts new file mode 100644 index 00000000..a5d0fcd7 --- /dev/null +++ b/src/main/services/ai/human/tools/terminal/outputTypes.ts @@ -0,0 +1,13 @@ +export type TerminalStatus = 'running' | 'completed' | 'cancelled' + +export type TerminalSessionOutput = { + terminalId: string + status: TerminalStatus + output: string + readEndPosition: number + hasMoreOutput: boolean + exitCode: number | null +} + +export type TerminalRunOutput = TerminalSessionOutput +export type TerminalReadOutput = TerminalSessionOutput diff --git a/src/main/services/ai/human/tools/terminal/toolSchemas.ts b/src/main/services/ai/human/tools/terminal/toolSchemas.ts index 522af579..92ecc06a 100644 --- a/src/main/services/ai/human/tools/terminal/toolSchemas.ts +++ b/src/main/services/ai/human/tools/terminal/toolSchemas.ts @@ -1,16 +1,5 @@ import { z } from 'zod/v4' -export type TerminalStatus = 'running' | 'completed' | 'cancelled' - -export type TerminalSessionOutput = { - terminalId: string - status: TerminalStatus - output: string - readEndPosition: number - hasMoreOutput: boolean - exitCode: number | null -} - export const TERMINAL_RUN_SCHEMA = z.object({ command: z.string().min(1).describe('要在终端执行的命令'), commandExplanation: z.string().min(1).describe('命令意图说明,供审批与日志使用') @@ -26,5 +15,9 @@ export const TERMINAL_READ_SCHEMA = z.object({ .describe('上次读取结束位置;省略时从会话起始位置读取') }) -export type TerminalRunOutput = TerminalSessionOutput -export type TerminalReadOutput = TerminalSessionOutput +export type { + TerminalReadOutput, + TerminalRunOutput, + TerminalSessionOutput, + TerminalStatus +} from './outputTypes' diff --git a/src/main/services/ai/human/tools/textFile/outputTypes.ts b/src/main/services/ai/human/tools/textFile/outputTypes.ts new file mode 100644 index 00000000..9649d82d --- /dev/null +++ b/src/main/services/ai/human/tools/textFile/outputTypes.ts @@ -0,0 +1,22 @@ +export type TextFileReadOutput = { + content: string + startLine: number + endLine: number + totalLines: number + nextStartLine?: number +} + +export const TEXT_FILE_SEARCH_OUTPUT = { + totalMatchCount: 0 as number, + returnedMatchCount: 0 as number, + matches: [] as Array<{ + line: number + matchText: string + textContext: string + truncated: boolean + }> +} + +export type TextFileSearchOutput = typeof TEXT_FILE_SEARCH_OUTPUT + +export type TextFileEditOutput = Record diff --git a/src/main/services/ai/human/tools/textFile/toolSchemas.ts b/src/main/services/ai/human/tools/textFile/toolSchemas.ts index bf08280d..38e74187 100644 --- a/src/main/services/ai/human/tools/textFile/toolSchemas.ts +++ b/src/main/services/ai/human/tools/textFile/toolSchemas.ts @@ -1,4 +1,9 @@ import { z } from 'zod/v4' +import type { + TextFileEditOutput, + TextFileReadOutput, + TextFileSearchOutput +} from './outputTypes' const STRUCTURED_FILE_EXTENSION_PATTERN = /\.(?:doc|docx|ppt|pptx|pdf|xls|xlsx|odt|ods|odp)$/iu @@ -68,33 +73,6 @@ export const TEXT_FILE_EDIT_SCHEMA = z.discriminatedUnion('action', [ }) ]) -export type TextFileReadOutput = { - content: string - startLine: number - endLine: number - totalLines: number - nextStartLine?: number -} - -export const TEXT_FILE_SEARCH_OUTPUT = { - totalMatchCount: 0 as number, - returnedMatchCount: 0 as number, - matches: [] as Array<{ - line: number - matchText: string - textContext: string - truncated: boolean - }> -} -export type TextFileSearchOutput = typeof TEXT_FILE_SEARCH_OUTPUT - -export type TextFileEditOutput = Record - -export type FileReadInput = z.infer -export type FileReadOutput = TextFileReadOutput -export type FileEditInput = z.infer -export type FileEditOutput = TextFileEditOutput - export const TEXT_FILE_SEARCH_SCHEMA = z .object({ sourcePath: textFileSourcePathField, @@ -103,5 +81,14 @@ export const TEXT_FILE_SEARCH_SCHEMA = z }) .strict() +export type FileReadInput = z.infer +export type FileEditInput = z.infer export type FileSearchInput = z.infer + +export type FileReadOutput = TextFileReadOutput +export type FileEditOutput = TextFileEditOutput export type FileSearchOutput = TextFileSearchOutput + +export type { TextFileEditOutput, TextFileReadOutput, TextFileSearchOutput } from './outputTypes' + +export { TEXT_FILE_SEARCH_OUTPUT } from './outputTypes' diff --git a/src/main/services/ai/human/tools/web/index.ts b/src/main/services/ai/human/tools/web/index.ts index 76f7bf79..1d41b9cb 100644 --- a/src/main/services/ai/human/tools/web/index.ts +++ b/src/main/services/ai/human/tools/web/index.ts @@ -6,6 +6,7 @@ import { withToolLifecycle } from '../utils/lifecycle' import { createToolEventEmitter } from '../utils/toolEvents' import type { ToolFamilyDefinition } from '../types' import { BROWSER_USE_TOOL_SPEC, WEB_SEARCH_TOOL_SPEC } from './toolSpecs' +import type { WebSearchItem, WebSearchLinkItem } from './outputTypes' const webSourceService = new WebSourceService() @@ -17,24 +18,7 @@ type WebCrawlerFailureDetails = Error & { statusCode?: number } -type WebSearchLink = { - href: string - text?: string - title?: string -} - -type WebSearchItem = { - requestedUrl: string - finalUrl?: string - title?: string - markdown?: string - links?: { internal: WebSearchLink[]; external: WebSearchLink[] } - publishedAt?: string - error?: string - statusCode?: number -} - -const renderLinkForAi = (link: WebSearchLink): WebSearchLink => ({ +const renderLinkForAi = (link: WebSearchLinkItem): WebSearchLinkItem => ({ href: link.href, ...(link.text ? { text: link.text } : {}), ...(link.title ? { title: link.title } : {}) @@ -56,7 +40,7 @@ const buildCrawlItem = (input: { finalUrl: string title?: string markdown: string - links: { internal: WebSearchLink[]; external: WebSearchLink[] } + links: { internal: WebSearchLinkItem[]; external: WebSearchLinkItem[] } publishedAt?: string } }): WebSearchItem => ({ diff --git a/src/main/services/ai/human/tools/web/outputTypes.ts b/src/main/services/ai/human/tools/web/outputTypes.ts new file mode 100644 index 00000000..26b24fc6 --- /dev/null +++ b/src/main/services/ai/human/tools/web/outputTypes.ts @@ -0,0 +1,73 @@ +export type WebSearchLinkItem = { + href: string + text?: string + title?: string +} + +export type WebSearchItem = { + requestedUrl: string + finalUrl?: string + title?: string + markdown?: string + links?: { + internal: WebSearchLinkItem[] + external: WebSearchLinkItem[] + } + publishedAt?: string + error?: string + statusCode?: number +} + +export type WebSearchOutput = { + items: WebSearchItem[] +} + +export type BrowserUsePage = { + url: string + title?: string +} + +export type BrowserActionCandidate = { + description: string + method?: string + arguments?: string[] +} + +export type BrowserUseNavigateOutput = { + action: 'navigate' + page: BrowserUsePage +} + +export type BrowserUseExtractOutput = { + action: 'extract' + page: BrowserUsePage + content: string +} + +export type BrowserUseObserveOutput = { + action: 'observe' + page: BrowserUsePage + actions: BrowserActionCandidate[] +} + +export type BrowserUseActOutput = { + action: 'act' + page: BrowserUsePage + success: boolean + message: string + actionDescription: string + actions?: BrowserActionCandidate[] +} + +export type BrowserUseErrorOutput = { + action?: 'navigate' | 'extract' | 'observe' | 'act' + page?: BrowserUsePage + error: string +} + +export type BrowserUseOutput = + | BrowserUseNavigateOutput + | BrowserUseExtractOutput + | BrowserUseObserveOutput + | BrowserUseActOutput + | BrowserUseErrorOutput diff --git a/src/main/services/ai/human/tools/web/toolSchemas.ts b/src/main/services/ai/human/tools/web/toolSchemas.ts index ba95d007..31f50598 100644 --- a/src/main/services/ai/human/tools/web/toolSchemas.ts +++ b/src/main/services/ai/human/tools/web/toolSchemas.ts @@ -38,76 +38,16 @@ export const BROWSER_USE_SCHEMA = z.discriminatedUnion('action', [ .strict() ]) -export type WebSearchLinkItem = { - href: string - text?: string - title?: string -} - -export type WebSearchItem = { - requestedUrl: string - finalUrl?: string - title?: string - markdown?: string - links?: { - internal: WebSearchLinkItem[] - external: WebSearchLinkItem[] - } - publishedAt?: string - error?: string - statusCode?: number -} - -export type WebSearchOutput = { - items: WebSearchItem[] -} - -export type BrowserUsePage = { - url: string - title?: string -} - -export type BrowserActionCandidate = { - description: string - method?: string - arguments?: string[] -} - -export type BrowserUseNavigateOutput = { - action: 'navigate' - page: BrowserUsePage -} - -export type BrowserUseExtractOutput = { - action: 'extract' - page: BrowserUsePage - content: string -} - -export type BrowserUseObserveOutput = { - action: 'observe' - page: BrowserUsePage - actions: BrowserActionCandidate[] -} - -export type BrowserUseActOutput = { - action: 'act' - page: BrowserUsePage - success: boolean - message: string - actionDescription: string - actions?: BrowserActionCandidate[] -} - -export type BrowserUseErrorOutput = { - action?: 'navigate' | 'extract' | 'observe' | 'act' - page?: BrowserUsePage - error: string -} - -export type BrowserUseOutput = - | BrowserUseNavigateOutput - | BrowserUseExtractOutput - | BrowserUseObserveOutput - | BrowserUseActOutput - | BrowserUseErrorOutput +export type { + BrowserActionCandidate, + BrowserUseActOutput, + BrowserUseErrorOutput, + BrowserUseExtractOutput, + BrowserUseNavigateOutput, + BrowserUseObserveOutput, + BrowserUseOutput, + BrowserUsePage, + WebSearchItem, + WebSearchLinkItem, + WebSearchOutput +} from './outputTypes' diff --git a/tests/case/ai/human/mock-llm/create-ppt-deck.test.ts b/tests/case/ai/human/mock-llm/create-ppt-deck.test.ts index 9ad3820e..e53f3a53 100644 --- a/tests/case/ai/human/mock-llm/create-ppt-deck.test.ts +++ b/tests/case/ai/human/mock-llm/create-ppt-deck.test.ts @@ -18,19 +18,19 @@ const coverSlide = { { type: 'shape' as const, shape: 'roundRect' as const, - bounds: { x: 96, y: 88, width: 1728, height: 904 }, + bounds: { x: 50, y: 45, width: 860, height: 110 }, style: { fill: '#17324D' } }, { type: 'text' as const, text: '季度业务复盘', - bounds: { x: 180, y: 300, width: 1500, height: 110 }, - style: { color: '#FFFFFF', fontSize: 36 } + bounds: { x: 85, y: 68, width: 790, height: 60 }, + style: { color: '#FFFFFF', fontSize: 36, bold: true } }, { type: 'text' as const, text: '增长引擎与下一步行动', - bounds: { x: 184, y: 470, width: 1200, height: 90 }, + bounds: { x: 85, y: 210, width: 790, height: 60 }, style: { color: '#DCE8F1', fontSize: 18 } } ] @@ -43,13 +43,13 @@ const detailSlide = { { type: 'text' as const, text: '下一步行动', - bounds: { x: 140, y: 100, width: 1500, height: 110 }, - style: { color: '#17324D', fontSize: 36 } + bounds: { x: 70, y: 50, width: 820, height: 55 }, + style: { color: '#17324D', fontSize: 36, bold: true } }, { type: 'text' as const, text: '缩短反馈周期,集中投入增长引擎。', - bounds: { x: 160, y: 300, width: 1450, height: 120 }, + bounds: { x: 80, y: 150, width: 800, height: 60 }, style: { color: '#333333', fontSize: 18 } } ] diff --git a/tests/case/ai/human/mock-llm/human-ppt-read-search-edit.test.ts b/tests/case/ai/human/mock-llm/human-ppt-read-search-edit.test.ts index bc353675..da96a432 100644 --- a/tests/case/ai/human/mock-llm/human-ppt-read-search-edit.test.ts +++ b/tests/case/ai/human/mock-llm/human-ppt-read-search-edit.test.ts @@ -20,13 +20,13 @@ const seedSlide = { { type: 'text' as const, text: '季度复盘', - bounds: { x: 140, y: 100, width: 1500, height: 110 }, - style: { fontSize: 36 } + bounds: { x: 70, y: 50, width: 820, height: 55 }, + style: { fontSize: 36, bold: true } }, { type: 'text' as const, text: '负责人:TBD', - bounds: { x: 160, y: 300, width: 1450, height: 120 }, + bounds: { x: 80, y: 150, width: 800, height: 60 }, style: { fontSize: 18 } } ] diff --git a/tests/case/ai/human/mock-llm/session-skill-memory-multi-run.test.ts b/tests/case/ai/human/mock-llm/session-skill-memory-multi-run.test.ts index a413c885..6672f386 100644 --- a/tests/case/ai/human/mock-llm/session-skill-memory-multi-run.test.ts +++ b/tests/case/ai/human/mock-llm/session-skill-memory-multi-run.test.ts @@ -13,7 +13,8 @@ describe('ai human mock-llm backend', () => { domain: 'ai', suite: 'human', groups: ['mock-llm'], - description: '验证同一会话跨 run 共享技能,并在加载 docx-write 时按 FIFO 保持 30 工具上限。', + description: + '验证同一会话跨 run 共享技能;在当前 29 工具目录与 30 上限下,四轮学习后仍应全部保留。', fixtures: [], runMode: 'single', execute: async (runtime) => { @@ -117,11 +118,12 @@ describe('ai human mock-llm backend', () => { result.stateAtLimit.learnedSkillIds.join(',') === 'docx-read,pdf-read,excel-write,ppt-read,ppt-write' && result.stateAfterEviction.learnedSkillIds.join(',') === - 'pdf-read,excel-write,ppt-read,ppt-write,excel-read,image-generation,docx-write' && + 'docx-read,pdf-read,excel-write,ppt-read,ppt-write,excel-read,image-generation,docx-write' && result.run4Prompt.includes(''), { name: 'session-skills-persist-across-runs-within-tool-budget', - message: '同一会话应跨 run 共享技能,并在加载 docx-write 后按 FIFO 淘汰最早技能。', + message: + '同一会话应跨 run 共享技能;后续 brain prompt 应注入 learned_skills,且四轮学习后技能全部保留。', data: result } )