返回 AiToEarn
workflow.handler.ts
根目录 / project / aitoearn-web / src / store / agent / task-instance / workflow.handler.ts
1 /**
2 * TaskInstance - 工作流处理模块
3 * 处理工作流步骤、工具调用等
4 */
5
6 import type { IMessageStep, IWorkflowStep } from '../agent.types'
7 import type { IWorkflowHandlerContext } from './task-instance.types'
8
9 // ============ 步骤管理 ============
10
11 /**
12 * 开始新步骤
13 */
14 export function startNewStep(ctx: IWorkflowHandlerContext): void {
15 // 保存当前步骤(如果有文本内容或工作流步骤)
16 const streamingText = ctx.getStreamingText()
17 const currentStepWorkflow = ctx.getCurrentStepWorkflow()
18
19 if (streamingText.trim() || currentStepWorkflow.length > 0) {
20 saveCurrentStepToMessage(ctx)
21 }
22
23 // 重置当前步骤状态
24 ctx.setStreamingText('')
25 ctx.setCurrentStepWorkflow([])
26 ctx.incrementCurrentStepIndex()
27 }
28
29 /**
30 * 保存当前步骤到消息中
31 */
32 export function saveCurrentStepToMessage(ctx: IWorkflowHandlerContext): void {
33 const streamingText = ctx.getStreamingText()
34 const currentStepWorkflow = ctx.getCurrentStepWorkflow()
35 const currentStepIndex = ctx.getCurrentStepIndex()
36 const currentAssistantMessageId = ctx.getCurrentAssistantMessageId()
37
38 const hasContent = streamingText.trim()
39 const hasWorkflow = currentStepWorkflow.length > 0
40
41 if (currentStepIndex < 0 || (!hasContent && !hasWorkflow)) {
42 return
43 }
44
45 const stepData: IMessageStep = {
46 id: `step-${currentStepIndex}-saved`,
47 content: streamingText,
48 workflowSteps: [...currentStepWorkflow],
49 isActive: false,
50 timestamp: Date.now(),
51 }
52
53 ctx.updateData(data => ({
54 messages: data.messages.map((m) => {
55 if (m.id === currentAssistantMessageId) {
56 const steps = m.steps ? [...m.steps] : []
57 // 查找匹配的步骤
58 const liveStepId = `step-${currentStepIndex}-live`
59 const existingIndex = steps.findIndex(
60 (s: IMessageStep) => s.id === liveStepId || s.id.startsWith(`step-${currentStepIndex}-`),
61 )
62 if (existingIndex >= 0) {
63 const existingStep = steps[existingIndex]
64 steps[existingIndex] = {
65 ...stepData,
66 workflowSteps:
67 stepData.workflowSteps && stepData.workflowSteps.length > 0
68 ? stepData.workflowSteps
69 : existingStep.workflowSteps || [],
70 }
71 }
72 else {
73 steps.push(stepData)
74 }
75 return { ...m, steps }
76 }
77 return m
78 }),
79 }))
80 }
81
82 // ============ 工作流步骤管理 ============
83
84 /**
85 * 添加工作流步骤
86 * 根据 currentStepIndex 找到或创建对应的步骤,避免工具调用被错误地添加到旧步骤中
87 */
88 export function addWorkflowStep(ctx: IWorkflowHandlerContext, step: IWorkflowStep): void {
89 ctx.pushToCurrentStepWorkflow(step)
90 const currentAssistantMessageId = ctx.getCurrentAssistantMessageId()
91 const currentStepIndex = ctx.getCurrentStepIndex()
92
93 // 更新当前任务的工作流步骤(用于UI显示)
94 ctx.updateData(data => ({
95 workflowSteps: [
96 ...data.workflowSteps.map((s: IWorkflowStep) => ({ ...s, isActive: false })),
97 step,
98 ],
99 }))
100
101 // 实时更新消息中的当前步骤的工作流
102 ctx.updateData(data => ({
103 messages: data.messages.map((m) => {
104 if (m.id === currentAssistantMessageId) {
105 const steps = m.steps ? [...m.steps] : []
106
107 // 根据 currentStepIndex 找到对应的步骤
108 const currentStepId = `step-${currentStepIndex}-live`
109 const savedStepId = `step-${currentStepIndex}-saved`
110 const existingIndex = steps.findIndex(
111 (s: IMessageStep) => s.id === currentStepId || s.id === savedStepId,
112 )
113
114 if (existingIndex >= 0) {
115 // 更新已存在的步骤
116 const existingStep = steps[existingIndex]
117 steps[existingIndex] = {
118 ...existingStep,
119 workflowSteps: [...(existingStep.workflowSteps || []), step],
120 }
121 }
122 else {
123 // 创建新步骤
124 steps.push({
125 id: currentStepId,
126 content: '',
127 workflowSteps: [step],
128 isActive: true,
129 timestamp: Date.now(),
130 })
131 }
132
133 return { ...m, steps }
134 }
135 return m
136 }),
137 }))
138 }
139
140 /**
141 * 更新最后一个工作流步骤
142 */
143 export function updateLastWorkflowStep(
144 ctx: IWorkflowHandlerContext,
145 updater: (step: IWorkflowStep) => IWorkflowStep,
146 ): void {
147 const currentStepWorkflow = ctx.getCurrentStepWorkflow()
148 const currentAssistantMessageId = ctx.getCurrentAssistantMessageId()
149
150 // 更新当前步骤的工作流
151 if (currentStepWorkflow.length > 0) {
152 const updatedWorkflow = [...currentStepWorkflow]
153 const lastIndex = updatedWorkflow.length - 1
154 updatedWorkflow[lastIndex] = updater(updatedWorkflow[lastIndex])
155 ctx.setCurrentStepWorkflow(updatedWorkflow)
156 }
157
158 // 更新当前任务的工作流步骤
159 ctx.updateData((data) => {
160 const steps = [...data.workflowSteps]
161 if (steps.length > 0) {
162 steps[steps.length - 1] = updater(steps[steps.length - 1])
163 }
164 return { workflowSteps: steps }
165 })
166
167 // 更新消息中的工作流步骤
168 ctx.updateData(data => ({
169 messages: data.messages.map((m) => {
170 if (m.id === currentAssistantMessageId) {
171 const steps = m.steps ? [...m.steps] : []
172 if (steps.length > 0) {
173 const lastStep = steps[steps.length - 1]
174 if (lastStep.workflowSteps && lastStep.workflowSteps.length > 0) {
175 const workflowSteps = [...lastStep.workflowSteps]
176 workflowSteps[workflowSteps.length - 1] = updater(
177 workflowSteps[workflowSteps.length - 1],
178 )
179 steps[steps.length - 1] = { ...lastStep, workflowSteps }
180 return { ...m, steps }
181 }
182 }
183 }
184 return m
185 }),
186 }))
187 }
188
189 // ============ 工具调用处理 ============
190
191 /**
192 * 处理工具调用完成
193 */
194 export function handleToolCallComplete(
195 ctx: IWorkflowHandlerContext,
196 toolName: string,
197 toolInput: string,
198 ): void {
199 const currentStepWorkflow = ctx.getCurrentStepWorkflow()
200 const currentAssistantMessageId = ctx.getCurrentAssistantMessageId()
201
202 // 更新当前步骤的工作流
203 const stepIndex = currentStepWorkflow.findIndex(
204 s => s.type === 'tool_call' && s.toolName === toolName && s.isActive,
205 )
206 if (stepIndex >= 0) {
207 const updatedWorkflow = [...currentStepWorkflow]
208 updatedWorkflow[stepIndex] = {
209 ...updatedWorkflow[stepIndex],
210 content: toolInput,
211 isActive: false,
212 }
213 ctx.setCurrentStepWorkflow(updatedWorkflow)
214 }
215
216 // 更新当前任务的工作流步骤
217 ctx.updateData((data) => {
218 const steps = [...data.workflowSteps]
219 const globalStepIndex = steps.findIndex(
220 s => s.type === 'tool_call' && s.toolName === toolName && s.isActive,
221 )
222 if (globalStepIndex >= 0) {
223 steps[globalStepIndex] = {
224 ...steps[globalStepIndex],
225 content: toolInput,
226 isActive: false,
227 }
228 }
229 return { workflowSteps: steps }
230 })
231
232 // 记录到 markdown 消息
233 const displayName = toolName.replace(/^mcp__\w+__/, '')
234 ctx.addMarkdownMessage(`🔧 **Tool Call**: \`${displayName}\`\n\`\`\`json\n${toolInput}\n\`\`\``)
235 }
236
237 /**
238 * 处理工具结果
239 * 找到最近的没有 result 的 tool_call 步骤,更新其 result 字段
240 */
241 export function handleToolResult(ctx: IWorkflowHandlerContext, resultText: string): void {
242 const currentStepWorkflow = ctx.getCurrentStepWorkflow()
243 const currentAssistantMessageId = ctx.getCurrentAssistantMessageId()
244
245 // 找到最近的没有 result 的 tool_call 步骤
246 const lastToolCallIndex = [...currentStepWorkflow]
247 .reverse()
248 .findIndex(s => s.type === 'tool_call' && !s.result)
249
250 if (lastToolCallIndex === -1) {
251 console.warn('[WorkflowHandler] No pending tool_call found for tool_result')
252 return
253 }
254
255 // 转换为正向索引
256 const stepIndex = currentStepWorkflow.length - 1 - lastToolCallIndex
257 const toolCallStep = currentStepWorkflow[stepIndex]
258
259 // 更新当前步骤的工作流
260 const updatedWorkflow = [...currentStepWorkflow]
261 updatedWorkflow[stepIndex] = {
262 ...updatedWorkflow[stepIndex],
263 result: resultText,
264 isActive: false,
265 }
266 ctx.setCurrentStepWorkflow(updatedWorkflow)
267
268 // 更新全局 workflowSteps
269 ctx.updateData((data) => {
270 const steps = [...data.workflowSteps]
271 const globalStepIndex = steps.findIndex(
272 s => s.id === toolCallStep.id && s.type === 'tool_call' && !s.result,
273 )
274 if (globalStepIndex >= 0) {
275 steps[globalStepIndex] = {
276 ...steps[globalStepIndex],
277 result: resultText,
278 isActive: false,
279 }
280 }
281 return { workflowSteps: steps }
282 })
283
284 // 更新消息中的工作流步骤
285 ctx.updateData(data => ({
286 messages: data.messages.map((m) => {
287 if (m.id === currentAssistantMessageId) {
288 const steps = m.steps ? [...m.steps] : []
289 if (steps.length > 0) {
290 const lastStep = steps[steps.length - 1]
291 if (lastStep.workflowSteps && lastStep.workflowSteps.length > 0) {
292 const workflowSteps = [...lastStep.workflowSteps]
293 const msgStepIndex = workflowSteps.findIndex(
294 s => s.id === toolCallStep.id && s.type === 'tool_call' && !s.result,
295 )
296 if (msgStepIndex >= 0) {
297 workflowSteps[msgStepIndex] = {
298 ...workflowSteps[msgStepIndex],
299 result: resultText,
300 isActive: false,
301 }
302 steps[steps.length - 1] = { ...lastStep, workflowSteps }
303 return { ...m, steps }
304 }
305 }
306 }
307 }
308 return m
309 }),
310 }))
311
312 // 记录到 markdown 消息
313 const displayResult = resultText.length > 500 ? `${resultText.substring(0, 500)}...` : resultText
314 ctx.addMarkdownMessage(`📋 **Tool Result**:\n\`\`\`\n${displayResult}\n\`\`\``)
315 }
316
317 // ============ 文本增量处理 ============
318
319 /**
320 * 处理文本增量更新
321 */
322 export function handleTextDelta(ctx: IWorkflowHandlerContext): void {
323 const streamingText = ctx.getStreamingText()
324 const currentStepWorkflow = ctx.getCurrentStepWorkflow()
325 const currentStepIndex = ctx.getCurrentStepIndex()
326 const currentAssistantMessageId = ctx.getCurrentAssistantMessageId()
327
328 // 更新当前任务的 streamingText 和 markdownMessages
329 ctx.updateData((data) => {
330 const newMessages = [...data.markdownMessages]
331 if (newMessages.length > 0 && newMessages[newMessages.length - 1].startsWith('🤖 ')) {
332 newMessages[newMessages.length - 1] = `🤖 ${streamingText}`
333 }
334 else {
335 newMessages.push(`🤖 ${streamingText}`)
336 }
337 return {
338 streamingText,
339 markdownMessages: newMessages,
340 }
341 })
342
343 // 更新消息列表中的 assistant 消息
344 ctx.updateData((data) => {
345 return {
346 messages: data.messages.map((m) => {
347 // 使用实例的 currentAssistantMessageId,或回退到最后一个 assistant 消息
348 const targetAssistantId
349 = currentAssistantMessageId
350 || (() => {
351 const msgs = data.messages || []
352 for (let i = msgs.length - 1; i >= 0; i--) {
353 if (msgs[i].role === 'assistant')
354 return msgs[i].id
355 }
356 return ''
357 })()
358
359 if (m.id === targetAssistantId) {
360 const steps = m.steps || []
361 const updatedSteps = [...steps]
362 const currentStepId = `step-${currentStepIndex}-live`
363 const currentStepData: IMessageStep = {
364 id: currentStepId,
365 content: streamingText,
366 workflowSteps: [...currentStepWorkflow],
367 isActive: true,
368 timestamp: Date.now(),
369 }
370
371 const existingStepIndex = updatedSteps.findIndex(
372 (s: IMessageStep) => s.id === currentStepId || s.id === `step-${currentStepIndex}-saved`,
373 )
374
375 if (existingStepIndex >= 0) {
376 updatedSteps[existingStepIndex] = currentStepData
377 }
378 else {
379 updatedSteps.push(currentStepData)
380 }
381
382 const totalContent = updatedSteps.map((s: IMessageStep) => s.content).join('\n\n')
383
384 return {
385 ...m,
386 content: totalContent,
387 status: 'streaming' as const,
388 steps: updatedSteps,
389 }
390 }
391 return m
392 }),
393 }
394 })
395 }
396
396 lines TYPESCRIPT