📄 runner.ts • 6435 bytes
/**
* 执行计划 - 计划执行器
* Phase 2: 分步执行 + 状态追踪
*/
import { type ExecutionPlan, type PlanStep, type StepStatus } from './types'
import { executeTool as realExecuteTool, type ToolCall, type ToolResult } from '../tools.js'
/** 步骤执行结果 */
export interface StepResult {
stepId: number
success: boolean
output: string
error?: string
duration: number
}
/** 执行器选项 */
export interface RunnerOptions {
stopOnError: boolean // 遇错停止
confirmEachStep: boolean // 每步确认
verbose: boolean // 详细输出
}
/** 默认选项 */
const DEFAULT_OPTIONS: RunnerOptions = {
stopOnError: true,
confirmEachStep: false,
verbose: true,
}
/** 工具执行计数器(用于生成唯一ID) */
let toolCallCounter = 0
/**
* 真实工具执行 — 将 PlanStep 的工具调用桥接到 tools.ts 的真实执行器
* PlanStep.params (object) → ToolCall.arguments (JSON string)
*/
async function executeTool(
tool: string,
params: Record<string, any>,
verbose: boolean
): Promise<{ success: boolean; output: string; error?: string }> {
if (verbose) {
console.log(` \x1b[38;5;240m🔧 执行: ${tool} ${JSON.stringify(params).slice(0, 80)}...\x1b[0m`)
}
// 构造 ToolCall 格式(与 tools.ts 的 ToolCall 接口对齐)
const toolCall: ToolCall = {
id: `pavr_${++toolCallCounter}_${Date.now()}`,
name: tool,
arguments: JSON.stringify(params), // ToolCall.arguments 是 JSON 字符串
}
try {
const result: ToolResult = await realExecuteTool(toolCall)
// 判断结果是否为错误
const isError = result.content.startsWith('Error:')
return {
success: !isError,
output: result.content,
error: isError ? result.content : undefined,
}
} catch (err: any) {
return {
success: false,
output: '',
error: err.message || String(err),
}
}
}
/**
* 执行单个步骤
*/
async function executeStep(
step: PlanStep,
options: RunnerOptions
): Promise<StepResult> {
const startTime = Date.now()
try {
// 更新状态为执行中
step.status = 'running'
// 执行工具
const result = await executeTool(step.tool, step.params, options.verbose)
const duration = Date.now() - startTime
if (result.success) {
step.status = 'completed'
step.result = result.output
return {
stepId: step.id,
success: true,
output: result.output,
duration,
}
} else {
step.status = 'failed'
step.error = result.error
return {
stepId: step.id,
success: false,
output: '',
error: result.error,
duration,
}
}
} catch (err: any) {
step.status = 'failed'
step.error = err.message
return {
stepId: step.id,
success: false,
output: '',
error: err.message,
duration: Date.now() - startTime,
}
}
}
/**
* 执行计划
* @param plan 执行计划
* @param options 执行选项
* @param onStepComplete 每步完成回调
* @returns 执行结果
*/
export async function executePlan(
plan: ExecutionPlan,
options: Partial<RunnerOptions> = {},
onStepComplete?: (step: PlanStep, result: StepResult) => void
): Promise<{
success: boolean
completedSteps: number
failedSteps: number
totalDuration: number
}> {
const opts = { ...DEFAULT_OPTIONS, ...options }
const startTime = Date.now()
plan.status = 'executing'
let completedSteps = 0
let failedSteps = 0
console.log('')
console.log(` \x1b[38;5;220m▶️ 开始执行计划\x1b[0m`)
console.log(` \x1b[38;5;240m步骤总数: ${plan.steps.length}\x1b[0m`)
console.log('')
for (let i = 0; i < plan.steps.length; i++) {
const step = plan.steps[i]
plan.currentStep = i + 1
if (opts.verbose) {
console.log(` \x1b[38;5;220m[${step.id}/${plan.steps.length}]\x1b[0m ${step.action}`)
}
// 执行步骤
const result = await executeStep(step, opts)
if (result.success) {
completedSteps++
if (opts.verbose) {
console.log(` \x1b[38;5;208m✅ 完成 (${result.duration}ms)\x1b[0m`)
}
} else {
failedSteps++
if (opts.verbose) {
console.log(` \x1b[38;5;196m❌ 失败: ${result.error}\x1b[0m`)
}
// 如果配置了遇错停止
if (opts.stopOnError && step.retryCount === 0) {
// 尝试重试
step.retryCount++
step.status = 'pending'
console.log(` \x1b[38;5;208m🔄 重试中...\x1b[0m`)
const retryResult = await executeStep(step, opts)
if (retryResult.success) {
completedSteps++
failedSteps--
if (opts.verbose) {
console.log(` \x1b[38;5;208m✅ 重试成功\x1b[0m`)
}
} else if (opts.stopOnError) {
console.log(` \x1b[38;5;196m⛔ 停止执行\x1b[0m`)
break
}
} else if (opts.stopOnError) {
break
}
}
// 回调
if (onStepComplete) {
onStepComplete(step, result)
}
console.log('')
}
// 更新计划状态
if (failedSteps === 0) {
plan.status = 'completed'
} else if (completedSteps > 0) {
plan.status = 'executing' // 部分完成
} else {
plan.status = 'cancelled'
}
const totalDuration = Date.now() - startTime
console.log('')
console.log(` \x1b[38;5;220m🏁 执行完成\x1b[0m`)
console.log(` \x1b[38;5;208m✅ 成功: ${completedSteps}\x1b[0m`)
if (failedSteps > 0) {
console.log(` \x1b[38;5;196m❌ 失败: ${failedSteps}\x1b[0m`)
}
console.log(` \x1b[38;5;240m⏱️ 总耗时: ${totalDuration}ms\x1b[0m`)
console.log('')
return {
success: failedSteps === 0,
completedSteps,
failedSteps,
totalDuration,
}
}
/**
* 获取计划执行摘要
*/
export function getPlanSummary(plan: ExecutionPlan): string {
const completed = plan.steps.filter(s => s.status === 'completed').length
const failed = plan.steps.filter(s => s.status === 'failed').length
const pending = plan.steps.filter(s => s.status === 'pending').length
return `
📊 **计划执行摘要**
- 总步骤: ${plan.steps.length}
- ✅ 完成: ${completed}
- ❌ 失败: ${failed}
- ⏳ 待执行: ${pending}
- 当前: 第 ${plan.currentStep} 步
`
}