feat: add output types and schemas for various AI tools; enhance test coverage and improve layout in PPT tests

This commit is contained in:
Yoan Huang committed 2026-08-30 16:23:08 +08:00
1 parent a80b0e09ba
commit e8598fda19
17 files changed
+232 -206

No files matched your search

+1 -1
View File
@@ -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`
@@ -0,0 +1,16 @@
export type LearnSkillOutput = Record<string, never>
export type UpdatePlanOutput = Record<string, never>
export type ReadImageItem = {
path: string
mimeType: string
sizeBytes: number
width: number
height: number
}
export type ReadImageOutput = {
message: string
images: ReadImageItem[]
}
@@ -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<string, never>
export const UPDATE_PLAN_SCHEMA = z.object({
plan: z.array(humanPlanItemSchema).min(1).describe('任务计划条目'),
progress: z.string().optional().describe('当前进度摘要')
})
export type UpdatePlanOutput = Record<string, never>
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'
@@ -0,0 +1 @@
export type ImageGenerateOutput = { outputPath: string } | { error: string }
@@ -19,6 +19,4 @@ export const imageGenerateSchema = z
export type ImageGenerateInput = z.infer<typeof imageGenerateSchema>
export type ImageGenerateOutput =
| { outputPath: string }
| { error: string }
export type { ImageGenerateOutput } from './outputTypes'
@@ -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
}
@@ -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<typeof jsonObjectSchema>
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'
@@ -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
@@ -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'
@@ -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<string, never>
@@ -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<string, never>
export type FileReadInput = z.infer<typeof TEXT_FILE_READ_SCHEMA>
export type FileReadOutput = TextFileReadOutput
export type FileEditInput = z.infer<typeof TEXT_FILE_EDIT_SCHEMA>
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<typeof TEXT_FILE_READ_SCHEMA>
export type FileEditInput = z.infer<typeof TEXT_FILE_EDIT_SCHEMA>
export type FileSearchInput = z.infer<typeof TEXT_FILE_SEARCH_SCHEMA>
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'
+3 -19
View File
@@ -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 => ({
@@ -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
@@ -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'
@@ -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 }
}
]
@@ -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 }
}
]
@@ -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('<learned_skills>'),
{
name: 'session-skills-persist-across-runs-within-tool-budget',
message: '同一会话应跨 run 共享技能,并在加载 docx-write 后按 FIFO 淘汰最早技能。',
message:
'同一会话应跨 run 共享技能;后续 brain prompt 应注入 learned_skills,且四轮学习后技能全部保留。',
data: result
}
)