feat: refactor assistant execution summary handling and improve type definitions

This commit is contained in:
hy committed 2026-07-29 12:41:43 +08:00
1 parent 0caadf1ed5
commit a2b5d4ef99
4 files changed
+502 -391

No files matched your search

@@ -14,10 +14,15 @@ const getFileExtension = (fileName: string): string => {
.pop()
if (!lastSegment) return ''
const dotIndex = lastSegment.lastIndexOf('.')
return dotIndex > -1 ? lastSegment.slice(dotIndex + 1).trim().toLowerCase() : ''
return dotIndex > -1
? lastSegment
.slice(dotIndex + 1)
.trim()
.toLowerCase()
: ''
}
export const createAttachmentFileReference = (fileName: string): FileReference => ({
const createAttachmentFileReference = (fileName: string): FileReference => ({
kind: 'file',
workspaceId: '',
nodeId: `attachment:${fileName}`,
+11 -353
View File
@@ -14,6 +14,12 @@ import {
getRuntimeSkillDisplayName,
type RuntimeToolName
} from '../../../../shared/runtimeCapabilities'
import {
getAssistantMixedStatus as getMixedStatus,
getAssistantToolIconKind as getToolIconKind,
getAssistantToolSummaryText as getToolSummaryText,
getAssistantToolUiKind as getToolUiKind
} from '../../../../shared/assistantExecutionSummary'
import {
WORKBENCH_PREFERENCE_AI_PREFERENCE_STORAGE_KEY,
WORKBENCH_PREFERENCE_MODE_STORAGE_KEY,
@@ -196,85 +202,11 @@ const getSkillDisplayName = (skillId?: RuntimeSkillId): string => {
return skillId ? getRuntimeSkillDisplayName(skillId) || '新能力' : '新能力'
}
const TOOL_UI_KINDS = {
terminal_run: 'terminal',
terminal_read: 'terminal',
terminal_stop: 'terminal',
text_file_read: 'file-read',
text_file_edit: 'file-edit',
kb_search: 'kb',
kb_list_documents: 'kb',
docx_read_outline: 'docx-read',
docx_search: 'docx-read',
docx_read_blocks: 'docx-read',
docx_edit: 'docx-edit',
pdf_inspect: 'pdf-read',
pdf_search: 'pdf-read',
pdf_read_pages: 'pdf-read',
pdf_read_visuals: 'pdf-read',
ppt_read_outline: 'ppt-read',
ppt_read_slides: 'ppt-read',
ppt_search: 'ppt-search',
ppt_read_visuals: 'ppt-read',
ppt_edit: 'ppt-edit',
excel_read_outline: 'excel-read',
excel_search: 'excel-read',
excel_read_cells: 'excel-read',
excel_edit: 'excel-edit',
web_search: 'web-search',
web_crawl: 'web-crawl',
browser_use: 'browser',
wait_time: 'wait',
wait_for_user_takeover: 'user-takeover',
image_analyze: 'image-recognition',
image_generate: 'image-generation'
} as const satisfies Record<RuntimeToolName, AssistantProcessSkillKey>
const getToolUiKind = (toolName?: AssistantToolName): AssistantProcessSkillKey =>
toolName && toolName !== 'unnamed' ? TOOL_UI_KINDS[toolName] : 'generic'
const getDisplaySkillKey = (display: AssistantProcessDisplay): AssistantProcessSkillKey => {
if (display.kind === 'tool') return getToolUiKind(normalizeToolName(display.toolName))
return 'generic'
}
const getToolIconKind = (skillKey: AssistantProcessSkillKey): AssistantProcessIconKind => {
if (skillKey === 'docx-edit') return 'edit'
if (skillKey === 'docx-read' || skillKey === 'pdf-read' || skillKey === 'ppt-read') {
return 'document'
}
if (skillKey === 'file-read') return 'document'
if (skillKey === 'file-edit') return 'edit'
if (skillKey === 'ppt-search') return 'search'
if (skillKey === 'ppt-edit') return 'edit'
if (skillKey === 'excel-read') return 'document'
if (skillKey === 'excel-edit') return 'edit'
if (skillKey === 'kb') return 'knowledge'
if (skillKey === 'web-search') return 'search'
if (skillKey === 'web-crawl') return 'web'
if (skillKey === 'browser') return 'browser'
if (skillKey === 'image-recognition' || skillKey === 'image-generation') return 'image'
if (skillKey === 'wait') return 'wait'
if (skillKey === 'user-takeover') return 'browser'
if (skillKey === 'terminal') return 'terminal'
return 'default'
}
const getMixedStatus = (
statuses: AssistantProcessStatus[],
fallback: AssistantProcessStatus = 'completed'
): AssistantProcessStatus => {
if (statuses.length === 0) return fallback
if (statuses.some((status) => status === 'running')) return 'running'
const hasCompleted = statuses.some((status) => status === 'completed')
const hasFailed = statuses.some((status) => status === 'failed')
const hasPartialFailed = statuses.some((status) => status === 'partial_failed')
if (hasPartialFailed) return 'partial_failed'
if (hasCompleted && hasFailed) return 'partial_failed'
if (hasFailed) return 'failed'
return 'completed'
}
const getDisplayIconKind = (display: AssistantProcessDisplay): AssistantProcessIconKind => {
if (display.kind === 'thinking') return 'default'
if (display.kind === 'learning') return 'learning'
@@ -284,148 +216,6 @@ const getDisplayIconKind = (display: AssistantProcessDisplay): AssistantProcessI
return getToolIconKind(getDisplaySkillKey(display))
}
const getToolSummaryText = (
skillKey: AssistantProcessSkillKey,
status: AssistantProcessStatus,
count = 1
): AssistantRichTextSegment[] => {
const normalizedCount = String(Math.max(1, count))
if (skillKey === 'docx-read') {
if (status === 'running') return sentence(text(`正在读取 ${normalizedCount} 个 docx 文件`))
if (status === 'completed') return sentence(text(`已读取 ${normalizedCount} 个 docx 文件`))
if (status === 'partial_failed') {
return sentence(text(`部分读取 ${normalizedCount} 个 docx 文件失败`))
}
return sentence(text(`读取 ${normalizedCount} 个 docx 文件失败`))
}
if (skillKey === 'docx-edit') {
if (status === 'running') return sentence(text(`正在编辑 ${normalizedCount} 个 docx 文件`))
if (status === 'completed') return sentence(text(`已编辑 ${normalizedCount} 个 docx 文件`))
if (status === 'partial_failed') {
return sentence(text(`部分编辑 ${normalizedCount} 个 docx 文件失败`))
}
return sentence(text(`编辑 ${normalizedCount} 个 docx 文件失败`))
}
if (skillKey === 'file-read') {
if (status === 'running') return sentence(text(`正在读取 ${normalizedCount} 个文件`))
if (status === 'completed') return sentence(text(`已读取 ${normalizedCount} 个文件`))
if (status === 'partial_failed') {
return sentence(text(`部分读取 ${normalizedCount} 个文件失败`))
}
return sentence(text(`读取 ${normalizedCount} 个文件失败`))
}
if (skillKey === 'file-edit') {
if (status === 'running') return sentence(text(`正在编辑 ${normalizedCount} 个文件`))
if (status === 'completed') return sentence(text(`已编辑 ${normalizedCount} 个文件`))
if (status === 'partial_failed') {
return sentence(text(`部分编辑 ${normalizedCount} 个文件失败`))
}
return sentence(text(`编辑 ${normalizedCount} 个文件失败`))
}
if (skillKey === 'pdf-read') {
if (status === 'running') return sentence(text(`正在读取 ${normalizedCount} 个 PDF 文件`))
if (status === 'completed') return sentence(text(`已读取 ${normalizedCount} 个 PDF 文件`))
if (status === 'partial_failed')
return sentence(text(`部分读取 ${normalizedCount} 个 PDF 文件失败`))
return sentence(text(`读取 ${normalizedCount} 个 PDF 文件失败`))
}
if (skillKey === 'ppt-read') {
if (status === 'running') return sentence(text(`正在读取 ${normalizedCount} 个 PPT 文件`))
if (status === 'completed') return sentence(text(`已读取 ${normalizedCount} 个 PPT 文件`))
if (status === 'partial_failed')
return sentence(text(`部分读取 ${normalizedCount} 个 PPT 文件失败`))
return sentence(text(`读取 ${normalizedCount} 个 PPT 文件失败`))
}
if (skillKey === 'ppt-search') {
if (status === 'running') return sentence(text('正在搜索 PPT'))
if (status === 'completed') return sentence(text('已搜索 PPT'))
if (status === 'partial_failed') return sentence(text('部分 PPT 搜索失败'))
return sentence(text('搜索 PPT 失败'))
}
if (skillKey === 'ppt-edit') {
if (status === 'running') return sentence(text(`正在编辑 ${normalizedCount} 个 PPT 文件`))
if (status === 'completed') return sentence(text(`已编辑 ${normalizedCount} 个 PPT 文件`))
if (status === 'partial_failed') {
return sentence(text(`部分编辑 ${normalizedCount} 个 PPT 文件失败`))
}
return sentence(text(`编辑 ${normalizedCount} 个 PPT 文件失败`))
}
if (skillKey === 'excel-read') {
if (status === 'running') return sentence(text(`正在读取 ${normalizedCount} 个 Excel 文件`))
if (status === 'completed') return sentence(text(`已读取 ${normalizedCount} 个 Excel 文件`))
if (status === 'partial_failed') {
return sentence(text(`部分读取 ${normalizedCount} 个 Excel 文件失败`))
}
return sentence(text(`读取 ${normalizedCount} 个 Excel 文件失败`))
}
if (skillKey === 'excel-edit') {
if (status === 'running') return sentence(text(`正在编辑 ${normalizedCount} 个 Excel 文件`))
if (status === 'completed') return sentence(text(`已编辑 ${normalizedCount} 个 Excel 文件`))
if (status === 'partial_failed') {
return sentence(text(`部分编辑 ${normalizedCount} 个 Excel 文件失败`))
}
return sentence(text(`编辑 ${normalizedCount} 个 Excel 文件失败`))
}
if (skillKey === 'kb') {
if (status === 'running') return sentence(text('正在检索知识库'))
if (status === 'completed') return sentence(text('已检索知识库'))
if (status === 'partial_failed') return sentence(text('部分知识库检索失败'))
return sentence(text('检索知识库失败'))
}
if (skillKey === 'terminal') {
if (status === 'running') return sentence(text('正在执行终端指令'))
if (status === 'completed') return sentence(text('已执行终端指令'))
if (status === 'partial_failed') return sentence(text('部分终端指令执行失败'))
return sentence(text('执行终端指令失败'))
}
if (skillKey === 'web-search') {
if (status === 'running') return sentence(text('正在搜索网页'))
if (status === 'completed') return sentence(text('已搜索网页'))
if (status === 'partial_failed') return sentence(text('部分网页搜索失败'))
return sentence(text('搜索网页失败'))
}
if (skillKey === 'web-crawl') {
if (status === 'running') return sentence(text('正在读取网页'))
if (status === 'completed') return sentence(text('已读取网页'))
if (status === 'partial_failed') return sentence(text('部分网页读取失败'))
return sentence(text('读取网页失败'))
}
if (skillKey === 'browser') {
if (status === 'running') return sentence(text('正在操作浏览器'))
if (status === 'completed') return sentence(text(`已操作浏览器 ${normalizedCount} 次`))
if (status === 'partial_failed') return sentence(text('部分浏览器操作失败'))
return sentence(text('浏览器操作失败'))
}
if (skillKey === 'image-recognition') {
if (status === 'running') return sentence(text('正在识别图片'))
if (status === 'completed') return sentence(text('已识别图片'))
if (status === 'partial_failed') return sentence(text('部分图片识别失败'))
return sentence(text('图片识别失败'))
}
if (skillKey === 'image-generation') {
if (status === 'running') return sentence(text('正在生成图片'))
if (status === 'completed') return sentence(text('已生成图片'))
if (status === 'partial_failed') return sentence(text('部分图片生成失败'))
return sentence(text('图片生成失败'))
}
if (skillKey === 'wait') {
if (status === 'running') return sentence(text('正在等待'))
if (status === 'completed') return sentence(text('已等待'))
if (status === 'partial_failed') return sentence(text('部分等待失败'))
return sentence(text('等待失败'))
}
if (skillKey === 'user-takeover') {
if (status === 'running') return sentence(text('正在等待用户接管浏览器'))
if (status === 'completed') return sentence(text('用户已完成浏览器操作'))
if (status === 'partial_failed') return sentence(text('等待用户接管浏览器时部分失败'))
return sentence(text('等待用户接管浏览器失败'))
}
if (status === 'running') return sentence(text('正在处理任务'))
if (status === 'completed') return sentence(text('已处理任务'))
if (status === 'partial_failed') return sentence(text('部分任务处理失败'))
return sentence(text('处理任务失败'))
}
type ToolItemRenderer = (input?: IpcJsonObject) => AssistantRichTextSegment[]
const TOOL_ITEM_RENDERERS = {
@@ -545,143 +335,11 @@ const getDisplayItemText = (
return getDisplayGroupText(display, status)
}
type ExecutionActionSummary = {
kind: AssistantProcessSkillKey
fileLabels: Set<string>
count: number
statuses: AssistantProcessStatus[]
waitLabels: Set<string>
}
const getExecutionStatus = (groups: AssistantProcessGroup[]): AssistantProcessStatus => {
return getMixedStatus(groups.map((group) => group.status))
}
const getSummaryActionLabel = (kind: AssistantProcessSkillKey): string => {
if (
kind === 'docx-read' ||
kind === 'file-read' ||
kind === 'pdf-read' ||
kind === 'ppt-read' ||
kind === 'excel-read' ||
kind === 'web-crawl'
) {
return '读取'
}
if (
kind === 'docx-edit' ||
kind === 'file-edit' ||
kind === 'ppt-edit' ||
kind === 'excel-edit'
) {
return '编辑'
}
if (kind === 'ppt-search' || kind === 'web-search') return '搜索'
if (kind === 'kb') return '检索'
if (kind === 'terminal') return '终端'
if (kind === 'browser' || kind === 'user-takeover') return '浏览器'
if (kind === 'image-recognition' || kind === 'image-generation') return '图片'
if (kind === 'wait') return '等待'
return '处理'
}
const getSummaryActionLabelText = (summaries: readonly ExecutionActionSummary[]): string => {
const labels = Array.from(
new Set(summaries.map((summary) => getSummaryActionLabel(summary.kind)))
)
if (labels.length === 0) return ''
return `${labels.slice(0, 3).join('、')}${summaries.length > 3 ? '等' : ''}`
}
export const buildAssistantExecutionSummaryText = (
groups: AssistantProcessGroup[]
): AssistantRichTextSegment[] => {
const summaries = new Map<AssistantProcessSkillKey, ExecutionActionSummary>()
for (const group of groups) {
const entries =
group.items.length > 0
? group.items.map((item) => ({
toolName: item.toolName,
targetLabel: item.targetLabel,
input: item.input
}))
: [{ toolName: undefined, targetLabel: undefined, input: undefined }]
for (const entry of entries) {
const kind = entry.toolName ? getToolUiKind(entry.toolName) : group.skillKey
const current =
summaries.get(kind) ||
({
kind,
fileLabels: new Set<string>(),
count: 0,
statuses: [],
waitLabels: new Set<string>()
} satisfies ExecutionActionSummary)
current.count += 1
current.statuses.push(group.status)
if (
kind === 'docx-read' ||
kind === 'docx-edit' ||
kind === 'ppt-read' ||
kind === 'ppt-edit' ||
kind === 'excel-read' ||
kind === 'excel-edit'
) {
current.fileLabels.add(entry.targetLabel || group.id)
}
if (kind === 'wait') current.waitLabels.add(readWaitDurationLabel(entry.input))
summaries.set(kind, current)
}
}
const status = getExecutionStatus(groups)
const ordered = [...summaries.values()]
if (ordered.length === 0) return sentence(text('正在处理任务'))
const totalCount = ordered.reduce((sum, summary) => sum + summary.count, 0)
if (ordered.length > 3) {
const actionText = getSummaryActionLabelText(ordered)
const suffix = actionText ? `:${actionText}` : ''
if (status === 'running') return sentence(text(`正在执行 ${totalCount} 项操作${suffix}`))
if (status === 'completed') return sentence(text(`已完成 ${totalCount} 项操作${suffix}`))
if (status === 'partial_failed')
return sentence(text(`${totalCount} 项操作中有部分失败${suffix}`))
return sentence(text(`${totalCount} 项操作执行失败${suffix}`))
}
return ordered.flatMap((summary, index) => {
const count = summary.fileLabels.size || summary.count || 1
const summaryStatus = getMixedStatus(summary.statuses, status)
const prefix =
index === 0
? ''
: summaryStatus === 'failed' || summaryStatus === 'partial_failed'
? '但'
: '并'
if (summary.kind === 'wait' && summary.waitLabels.size === 1) {
const waitLabel = [...summary.waitLabels][0]
if (summaryStatus === 'running') return sentence(text(prefix), text(`正在等待 ${waitLabel}`))
if (summaryStatus === 'completed') return sentence(text(prefix), text(`等待 ${waitLabel}`))
}
return sentence(text(prefix), ...getToolSummaryText(summary.kind, summaryStatus, count))
})
}
export const resolveAssistantExecutionIcon = (
groups: AssistantProcessGroup[]
): AssistantProcessIconKind => {
const kinds = new Set(
groups.flatMap((group) =>
group.items.length > 0
? group.items.map((item) => (item.toolName ? getToolUiKind(item.toolName) : group.skillKey))
: [group.skillKey]
)
)
if (kinds.size !== 1) return 'workflow'
return getToolIconKind([...kinds][0] || 'generic')
}
export const resolveAssistantExecutionStatus = getExecutionStatus
export {
buildAssistantExecutionSummaryText,
resolveAssistantExecutionIcon,
resolveAssistantExecutionStatus
} from '../../../../shared/assistantExecutionSummary'
const createProcessGroup = (input: {
id: string
+465
View File
@@ -0,0 +1,465 @@
import type { IpcJsonObject, IpcJsonValue } from './types'
import type { RuntimeToolName } from './runtimeCapabilities'
export type AssistantExecutionStatus = 'running' | 'completed' | 'failed' | 'partial_failed'
export type AssistantExecutionIconKind =
| 'search'
| 'browser'
| 'wait'
| 'web'
| 'image'
| 'knowledge'
| 'workflow'
| 'terminal'
| 'document'
| 'edit'
| 'retry'
| 'learning'
| 'compaction'
| 'finalizing'
| 'default'
export type AssistantExecutionSkillKey =
| 'docx-read'
| 'docx-edit'
| 'file-read'
| 'file-edit'
| 'pdf-read'
| 'ppt-read'
| 'ppt-search'
| 'ppt-edit'
| 'excel-read'
| 'excel-edit'
| 'kb'
| 'terminal'
| 'web-search'
| 'web-crawl'
| 'browser'
| 'image-recognition'
| 'image-generation'
| 'wait'
| 'user-takeover'
| 'generic'
export type AssistantExecutionTextSegment = {
text: string
emphasis?: boolean
iconKind?: AssistantExecutionIconKind
}
export type AssistantExecutionGroup = {
id: string
skillKey: AssistantExecutionSkillKey
status: AssistantExecutionStatus
items: readonly {
toolName?: RuntimeToolName | 'unnamed'
targetLabel?: string
input?: IpcJsonObject
}[]
}
const text = (value: string): AssistantExecutionTextSegment => ({ text: value })
const sentence = (...segments: AssistantExecutionTextSegment[]): AssistantExecutionTextSegment[] =>
segments
const TOOL_UI_KINDS = {
terminal_run: 'terminal',
terminal_read: 'terminal',
terminal_stop: 'terminal',
text_file_read: 'file-read',
text_file_edit: 'file-edit',
kb_search: 'kb',
kb_list_documents: 'kb',
docx_read_outline: 'docx-read',
docx_search: 'docx-read',
docx_read_blocks: 'docx-read',
docx_edit: 'docx-edit',
pdf_inspect: 'pdf-read',
pdf_search: 'pdf-read',
pdf_read_pages: 'pdf-read',
pdf_read_visuals: 'pdf-read',
ppt_read_outline: 'ppt-read',
ppt_read_slides: 'ppt-read',
ppt_search: 'ppt-search',
ppt_read_visuals: 'ppt-read',
ppt_edit: 'ppt-edit',
excel_read_outline: 'excel-read',
excel_search: 'excel-read',
excel_read_cells: 'excel-read',
excel_edit: 'excel-edit',
web_search: 'web-search',
web_crawl: 'web-crawl',
browser_use: 'browser',
wait_time: 'wait',
wait_for_user_takeover: 'user-takeover',
image_analyze: 'image-recognition',
image_generate: 'image-generation'
} as const satisfies Record<RuntimeToolName, AssistantExecutionSkillKey>
export const getAssistantToolUiKind = (
toolName?: RuntimeToolName | 'unnamed'
): AssistantExecutionSkillKey =>
toolName && toolName !== 'unnamed' ? TOOL_UI_KINDS[toolName] : 'generic'
export const getAssistantToolIconKind = (
skillKey: AssistantExecutionSkillKey
): AssistantExecutionIconKind => {
if (skillKey === 'docx-edit' || skillKey === 'file-edit') return 'edit'
if (
skillKey === 'docx-read' ||
skillKey === 'file-read' ||
skillKey === 'pdf-read' ||
skillKey === 'ppt-read' ||
skillKey === 'excel-read'
) {
return 'document'
}
if (skillKey === 'ppt-search' || skillKey === 'web-search') return 'search'
if (skillKey === 'ppt-edit' || skillKey === 'excel-edit') return 'edit'
if (skillKey === 'kb') return 'knowledge'
if (skillKey === 'web-crawl') return 'web'
if (skillKey === 'browser' || skillKey === 'user-takeover') return 'browser'
if (skillKey === 'image-recognition' || skillKey === 'image-generation') return 'image'
if (skillKey === 'wait') return 'wait'
if (skillKey === 'terminal') return 'terminal'
return 'default'
}
export const getAssistantMixedStatus = (
statuses: readonly AssistantExecutionStatus[],
fallback: AssistantExecutionStatus = 'completed'
): AssistantExecutionStatus => {
if (statuses.length === 0) return fallback
if (statuses.some((status) => status === 'running')) return 'running'
const hasCompleted = statuses.some((status) => status === 'completed')
const hasFailed = statuses.some((status) => status === 'failed')
const hasPartialFailed = statuses.some((status) => status === 'partial_failed')
if (hasPartialFailed || (hasCompleted && hasFailed)) return 'partial_failed'
if (hasFailed) return 'failed'
return 'completed'
}
type StatusText = Record<AssistantExecutionStatus, string>
const statusText = (
status: AssistantExecutionStatus,
values: StatusText
): AssistantExecutionTextSegment[] => sentence(text(values[status]))
export const getAssistantToolSummaryText = (
skillKey: AssistantExecutionSkillKey,
status: AssistantExecutionStatus,
count = 1
): AssistantExecutionTextSegment[] => {
const normalizedCount = String(Math.max(1, count))
if (skillKey === 'docx-read') {
return statusText(status, {
running: `正在读取 ${normalizedCount} 个 docx 文件`,
completed: `已读取 ${normalizedCount} 个 docx 文件`,
partial_failed: `部分读取 ${normalizedCount} 个 docx 文件失败`,
failed: `读取 ${normalizedCount} 个 docx 文件失败`
})
}
if (skillKey === 'docx-edit') {
return statusText(status, {
running: `正在编辑 ${normalizedCount} 个 docx 文件`,
completed: `已编辑 ${normalizedCount} 个 docx 文件`,
partial_failed: `部分编辑 ${normalizedCount} 个 docx 文件失败`,
failed: `编辑 ${normalizedCount} 个 docx 文件失败`
})
}
if (skillKey === 'file-read') {
return statusText(status, {
running: `正在读取 ${normalizedCount} 个文件`,
completed: `已读取 ${normalizedCount} 个文件`,
partial_failed: `部分读取 ${normalizedCount} 个文件失败`,
failed: `读取 ${normalizedCount} 个文件失败`
})
}
if (skillKey === 'file-edit') {
return statusText(status, {
running: `正在编辑 ${normalizedCount} 个文件`,
completed: `已编辑 ${normalizedCount} 个文件`,
partial_failed: `部分编辑 ${normalizedCount} 个文件失败`,
failed: `编辑 ${normalizedCount} 个文件失败`
})
}
if (skillKey === 'pdf-read') {
return statusText(status, {
running: `正在读取 ${normalizedCount} 个 PDF 文件`,
completed: `已读取 ${normalizedCount} 个 PDF 文件`,
partial_failed: `部分读取 ${normalizedCount} 个 PDF 文件失败`,
failed: `读取 ${normalizedCount} 个 PDF 文件失败`
})
}
if (skillKey === 'ppt-read') {
return statusText(status, {
running: `正在读取 ${normalizedCount} 个 PPT 文件`,
completed: `已读取 ${normalizedCount} 个 PPT 文件`,
partial_failed: `部分读取 ${normalizedCount} 个 PPT 文件失败`,
failed: `读取 ${normalizedCount} 个 PPT 文件失败`
})
}
if (skillKey === 'ppt-search') {
return statusText(status, {
running: '正在搜索 PPT',
completed: '已搜索 PPT',
partial_failed: '部分 PPT 搜索失败',
failed: '搜索 PPT 失败'
})
}
if (skillKey === 'ppt-edit') {
return statusText(status, {
running: `正在编辑 ${normalizedCount} 个 PPT 文件`,
completed: `已编辑 ${normalizedCount} 个 PPT 文件`,
partial_failed: `部分编辑 ${normalizedCount} 个 PPT 文件失败`,
failed: `编辑 ${normalizedCount} 个 PPT 文件失败`
})
}
if (skillKey === 'excel-read') {
return statusText(status, {
running: `正在读取 ${normalizedCount} 个 Excel 文件`,
completed: `已读取 ${normalizedCount} 个 Excel 文件`,
partial_failed: `部分读取 ${normalizedCount} 个 Excel 文件失败`,
failed: `读取 ${normalizedCount} 个 Excel 文件失败`
})
}
if (skillKey === 'excel-edit') {
return statusText(status, {
running: `正在编辑 ${normalizedCount} 个 Excel 文件`,
completed: `已编辑 ${normalizedCount} 个 Excel 文件`,
partial_failed: `部分编辑 ${normalizedCount} 个 Excel 文件失败`,
failed: `编辑 ${normalizedCount} 个 Excel 文件失败`
})
}
if (skillKey === 'kb') {
return statusText(status, {
running: '正在检索知识库',
completed: '已检索知识库',
partial_failed: '部分知识库检索失败',
failed: '检索知识库失败'
})
}
if (skillKey === 'terminal') {
return statusText(status, {
running: '正在执行终端指令',
completed: '已执行终端指令',
partial_failed: '部分终端指令执行失败',
failed: '执行终端指令失败'
})
}
if (skillKey === 'web-search') {
return statusText(status, {
running: '正在搜索网页',
completed: '已搜索网页',
partial_failed: '部分网页搜索失败',
failed: '搜索网页失败'
})
}
if (skillKey === 'web-crawl') {
return statusText(status, {
running: '正在读取网页',
completed: '已读取网页',
partial_failed: '部分网页读取失败',
failed: '读取网页失败'
})
}
if (skillKey === 'browser') {
return statusText(status, {
running: '正在操作浏览器',
completed: `已操作浏览器 ${normalizedCount} 次`,
partial_failed: '部分浏览器操作失败',
failed: '浏览器操作失败'
})
}
if (skillKey === 'image-recognition') {
return statusText(status, {
running: '正在识别图片',
completed: '已识别图片',
partial_failed: '部分图片识别失败',
failed: '图片识别失败'
})
}
if (skillKey === 'image-generation') {
return statusText(status, {
running: '正在生成图片',
completed: '已生成图片',
partial_failed: '部分图片生成失败',
failed: '图片生成失败'
})
}
if (skillKey === 'wait') {
return statusText(status, {
running: '正在等待',
completed: '已等待',
partial_failed: '部分等待失败',
failed: '等待失败'
})
}
if (skillKey === 'user-takeover') {
return statusText(status, {
running: '正在等待用户接管浏览器',
completed: '用户已完成浏览器操作',
partial_failed: '等待用户接管浏览器时部分失败',
failed: '等待用户接管浏览器失败'
})
}
return statusText(status, {
running: '正在处理任务',
completed: '已处理任务',
partial_failed: '部分任务处理失败',
failed: '处理任务失败'
})
}
const readWaitDurationLabel = (input?: IpcJsonObject): string => {
if (!input) return '一段时间'
const seconds = (input as Record<string, IpcJsonValue>).seconds
if (typeof seconds === 'number' && Number.isFinite(seconds)) return `${String(seconds)} 秒`
if (typeof seconds === 'string' && seconds.trim()) return `${seconds.trim()} 秒`
return '一段时间'
}
type ExecutionActionSummary = {
kind: AssistantExecutionSkillKey
fileLabels: Set<string>
count: number
statuses: AssistantExecutionStatus[]
waitLabels: Set<string>
}
const getExecutionStatus = (groups: readonly AssistantExecutionGroup[]): AssistantExecutionStatus =>
getAssistantMixedStatus(groups.map((group) => group.status))
const getSummaryActionLabel = (kind: AssistantExecutionSkillKey): string => {
if (
kind === 'docx-read' ||
kind === 'file-read' ||
kind === 'pdf-read' ||
kind === 'ppt-read' ||
kind === 'excel-read' ||
kind === 'web-crawl'
) {
return '读取'
}
if (
kind === 'docx-edit' ||
kind === 'file-edit' ||
kind === 'ppt-edit' ||
kind === 'excel-edit'
) {
return '编辑'
}
if (kind === 'ppt-search' || kind === 'web-search') return '搜索'
if (kind === 'kb') return '检索'
if (kind === 'terminal') return '终端'
if (kind === 'browser' || kind === 'user-takeover') return '浏览器'
if (kind === 'image-recognition' || kind === 'image-generation') return '图片'
if (kind === 'wait') return '等待'
return '处理'
}
const getSummaryActionLabelText = (summaries: readonly ExecutionActionSummary[]): string => {
const labels = Array.from(
new Set(summaries.map((summary) => getSummaryActionLabel(summary.kind)))
)
if (labels.length === 0) return ''
return `${labels.slice(0, 3).join('、')}${summaries.length > 3 ? '等' : ''}`
}
export const buildAssistantExecutionSummaryText = (
groups: readonly AssistantExecutionGroup[]
): AssistantExecutionTextSegment[] => {
const summaries = new Map<AssistantExecutionSkillKey, ExecutionActionSummary>()
for (const group of groups) {
const entries =
group.items.length > 0
? group.items.map((item) => ({
toolName: item.toolName,
targetLabel: item.targetLabel,
input: item.input
}))
: [{ toolName: undefined, targetLabel: undefined, input: undefined }]
for (const entry of entries) {
const kind = entry.toolName ? getAssistantToolUiKind(entry.toolName) : group.skillKey
const current =
summaries.get(kind) ||
({
kind,
fileLabels: new Set<string>(),
count: 0,
statuses: [],
waitLabels: new Set<string>()
} satisfies ExecutionActionSummary)
current.count += 1
current.statuses.push(group.status)
if (
kind === 'docx-read' ||
kind === 'docx-edit' ||
kind === 'ppt-read' ||
kind === 'ppt-edit' ||
kind === 'excel-read' ||
kind === 'excel-edit'
) {
current.fileLabels.add(entry.targetLabel || group.id)
}
if (kind === 'wait') current.waitLabels.add(readWaitDurationLabel(entry.input))
summaries.set(kind, current)
}
}
const status = getExecutionStatus(groups)
const ordered = [...summaries.values()]
if (ordered.length === 0) return sentence(text('正在处理任务'))
const totalCount = ordered.reduce((sum, summary) => sum + summary.count, 0)
if (ordered.length > 3) {
const actionText = getSummaryActionLabelText(ordered)
const suffix = actionText ? `:${actionText}` : ''
if (status === 'running') return sentence(text(`正在执行 ${totalCount} 项操作${suffix}`))
if (status === 'completed') return sentence(text(`已完成 ${totalCount} 项操作${suffix}`))
if (status === 'partial_failed') {
return sentence(text(`${totalCount} 项操作中有部分失败${suffix}`))
}
return sentence(text(`${totalCount} 项操作执行失败${suffix}`))
}
return ordered.flatMap((summary, index) => {
const count = summary.fileLabels.size || summary.count || 1
const summaryStatus = getAssistantMixedStatus(summary.statuses, status)
const prefix =
index === 0
? ''
: summaryStatus === 'failed' || summaryStatus === 'partial_failed'
? '但'
: '并'
if (summary.kind === 'wait' && summary.waitLabels.size === 1) {
const waitLabel = [...summary.waitLabels][0]
if (summaryStatus === 'running') return sentence(text(prefix), text(`正在等待 ${waitLabel}`))
if (summaryStatus === 'completed') return sentence(text(prefix), text(`等待 ${waitLabel}`))
}
return sentence(
text(prefix),
...getAssistantToolSummaryText(summary.kind, summaryStatus, count)
)
})
}
export const resolveAssistantExecutionIcon = (
groups: readonly AssistantExecutionGroup[]
): AssistantExecutionIconKind => {
const kinds = new Set(
groups.flatMap((group) =>
group.items.length > 0
? group.items.map((item) =>
item.toolName ? getAssistantToolUiKind(item.toolName) : group.skillKey
)
: [group.skillKey]
)
)
if (kinds.size !== 1) return 'workflow'
return getAssistantToolIconKind([...kinds][0] || 'generic')
}
export const resolveAssistantExecutionStatus = getExecutionStatus
@@ -2,47 +2,30 @@ import { describe, expect, it } from 'vitest'
import {
buildAssistantExecutionSummaryText,
resolveAssistantExecutionIcon
} from '../../../../src/renderer/src/components/workbench/utils'
import type { AssistantProcessGroup } from '../../../../src/renderer/src/components/workbench/types'
resolveAssistantExecutionIcon,
type AssistantExecutionGroup
} from '../../../../src/shared/assistantExecutionSummary'
const toPlainText = (segments: ReturnType<typeof buildAssistantExecutionSummaryText>): string =>
segments.map((segment) => segment.text).join('')
const createToolGroup = (toolName: AssistantProcessGroup['items'][number]['toolName'], index = 1) =>
[
{
id: `group-${toolName}`,
kind: 'skill',
skillKey: 'generic',
iconKind: 'default',
status: 'completed',
runningText: [],
completedText: [],
failedText: [],
partialFailedText: [],
createdAt: 1,
sourceEventType: 'assistant_process_group_completed',
items: [
{
id: `item-${toolName}-${String(index)}`,
status: 'completed',
runningText: [],
completedText: [],
failedText: [],
partialFailedText: [],
createdAt: index,
sourceEventType: 'assistant_process_item_completed',
toolName,
targetLabel: `file-${String(index)}`
}
]
}
] satisfies AssistantProcessGroup[]
type ToolName = NonNullable<AssistantExecutionGroup['items'][number]['toolName']>
const createToolGroups = (
toolNames: AssistantProcessGroup['items'][number]['toolName'][]
): AssistantProcessGroup[] =>
const createToolGroup = (toolName: ToolName, index = 1): AssistantExecutionGroup[] => [
{
id: `group-${toolName}`,
skillKey: 'generic',
status: 'completed',
items: [
{
toolName,
targetLabel: `file-${String(index)}`
}
]
}
]
const createToolGroups = (toolNames: ToolName[]): AssistantExecutionGroup[] =>
toolNames.flatMap((toolName, index) => createToolGroup(toolName, index + 1))
describe('workbench assistant process icons', () => {