📄 chat-enhanced.ts  •  30272 bytes
/**
 * CmdCode V0.5 - 增强版对话引擎
 * 整合 Plan-Act-Verify-Respond 循环
 * 
 * 与原有 chat.ts 完全兼容,同时启用新功能
 */
import OpenAI from 'openai'
import { loadConfig, type Config } from './config.js'
import { ALL_TOOLS, executeTool, type ToolCall, type ToolResult } from './tools.js'
import { rotateChatApiKey, getChatKeyPoolStatus } from './apikeys.js'
import { t } from './i18n.js'
import { searchMemory } from './memory/memoryManager.js'
import { SYSTEM_PROMPT_TEMPLATE } from './system-prompt.js'
import { buildSystemPrompt, getSystemPrompt } from './chat.js'

// 🚀 新增:导入 Plan-Act-Verify-Respond 模块
import { processUserInput, processUserInputAsync, classifyIntent, type IntentResult } from './intent/index.js'
import { generatePlan, executePlan, type ExecutionPlan } from './planner/index.js'
import { verify, type VerificationResult } from './verifier/index.js'
import { classifyError, withRetry, withTimeout } from './resilience/index.js'
import { showTitle, showNotice, showDivider, ProgressBar } from './ui/index.js'

/** 配置开关:是否启用 Plan-Act-Verify-Respond 循环 */
let ENABLE_PAVR_LOOP = true

export function setPAVREnabled(enabled: boolean): void {
  ENABLE_PAVR_LOOP = enabled
}

export function isPAVREnabled(): boolean {
  return ENABLE_PAVR_LOOP
}

export interface Message {
  role: 'system' | 'user' | 'assistant' | 'tool'
  content: string
  tool_calls?: ToolCall[]
  tool_call_id?: string
}

/** PAVR 循环状态 */
interface PAVRState {
  intent: IntentResult | null
  plan: ExecutionPlan | null
  verification: VerificationResult | null
  toolResults: ToolResult[]
  errors: any[]
  startTime: number
}

/** 系统提示词统一由 chat.ts 的 buildSystemPrompt / getSystemPrompt 提供 */

/**
 * 🚀 增强版对话引擎
 * 支持 Plan-Act-Verify-Respond 循环
 */
export class EnhancedChatEngine {
  private client: OpenAI
  private config: Config
  private messages: Message[]
  private maxTurns: number
  private pavrState: PAVRState

  private maskKeys(text: string): string {
    return text
      .replace(/\bark-[a-z0-9\-]{20,60}\b/gi, '****')
      .replace(/\bsk-[a-zA-Z0-9\-]{20,80}\b/g, '****')
  }

  constructor(config?: Config, systemPrompt?: string, workspaceDir?: string) {
    this.config = config || loadConfig()
    this.client = new OpenAI({
      apiKey: this.config.apiKey,
      baseURL: this.config.baseUrl,
      timeout: Math.min(this.config.timeoutMs, 120_000),
      maxRetries: 1,
    })
    this.maxTurns = 80 // SWE 任务复杂,最多 80 轮(原 40)
    this.messages = [
      { role: 'system', content: systemPrompt || buildSystemPrompt(workspaceDir) },
    ]
    this.pavrState = {
      intent: null,
      plan: null,
      verification: null,
      toolResults: [],
      errors: [],
      startTime: 0,
    }
  }

  /**
   * 智能上下文截断:token感知,丢弃最旧的工具结果消息
   * MiniMax-M2.7 128K context,扣10K输出预留,输入上限约110K tokens
   */
  private truncateIfNeeded(): void {
    const maxTokens = 110_000
    // 估算总 token
    let chars = 0
    for (const m of this.messages) {
      chars += m.content.length + 20
      if (m.role === 'tool') chars += 30
    }
    let tokens = Math.ceil(chars / 1.5)
    if (tokens <= maxTokens) return

    // 从最旧的非system消息开始丢弃(优先丢弃工具结果)
    const systemMsg = this.messages.find(m => m.role === 'system')
    const nonSystem = this.messages.filter(m => m.role !== 'system')
    let dropCount = 0
    for (let i = 0; i < nonSystem.length && tokens > maxTokens; i++) {
      // 优先丢弃 tool 角色的大消息(工具结果)
      if (nonSystem[i].role === 'tool' || nonSystem[i].content.length > 500) {
        tokens -= Math.ceil(nonSystem[i].content.length / 1.5) + 30
        dropCount++
      }
    }
    // 如果还不够,强制按顺序丢弃
    if (tokens > maxTokens) {
      for (let i = 0; i < nonSystem.length && tokens > maxTokens; i++) {
        if (![...nonSystem.slice(0, i)].some((m, j) => j < i && m.role === 'tool')) {
          tokens -= Math.ceil(nonSystem[i].content.length / 1.5) + 30
          dropCount++
        }
      }
    }
    if (dropCount > 0) {
      const threshold = nonSystem[dropCount - 1]
      const idx = this.messages.indexOf(threshold)
      this.messages = this.messages.slice(idx + 1)
      process.stdout.write(`  \x1b[90m[截断] Token超限 → 丢弃最早${dropCount}条消息\x1b[0m\n`)
    }
  }

  /**
   * 🚀 Plan-Act-Verify-Respond 主循环
   */
  async chatPAVR(userInput: string, onText?: (text: string) => void): Promise<string> {
    this.messages.push({ role: 'user', content: userInput })
    this.truncateIfNeeded()
    this.pavrState.startTime = Date.now()
    this.pavrState.errors = []
    this.pavrState.toolResults = []

    if (ENABLE_PAVR_LOOP) {
      showDivider()
      showTitle('🚀 Plan-Act-Verify-Respond 循环', 1)
      
      // ===== 🎯 Phase 1: 意图识别 =====
      await this.phaseIntentRecognition(userInput)
      
      // ===== 📋 Phase 2: 执行计划 =====
      await this.phaseExecutionPlan(userInput)
    }

    // ===== ⚡ Phase 3: 执行(原聊天逻辑)=====
    const finalText = await this.phaseExecution(onText)
    
    // ===== 🔍 Phase 4: 验证 =====
    if (ENABLE_PAVR_LOOP) {
      await this.phaseVerification(onText)
    }

    // ===== 💬 Phase 5: 响应 =====
    this.phaseResponse()

    return finalText
  }

  /**
   * 🎯 Phase 1: 意图识别
   */
  private async phaseIntentRecognition(userInput: string): Promise<void> {
    showTitle('🎯 意图识别', 2)
    
    try {
      const result = await withRetry(
        () => processUserInputAsync(userInput),
        { maxAttempts: 2, initialDelay: 100 }
      )
      
      if (result.success && result.intent) {
        this.pavrState.intent = result.intent
        const intent = result.intent.intent
        const confidence = intent?.confidence || 0
        
        // 低置信度(<30%)视为闲聊,不执行 PAVR
        if (confidence < 0.1) {
          showNotice(`意图置信度过低 (${Math.round(confidence * 100)}%),跳过 PAVR`, 'warning')
          this.pavrState.intent = null // 清除意图,后续跳过计划
          return
        }
        
        showNotice(
          `类型: ${intent?.type || 'unknown'} | 置信度: ${Math.round(confidence * 100)}%`,
          'success'
        )
        showNotice(`通道: ${result.intent.mode} | 路由: ${result.intent.route?.name}`, 'info')
      } else {
        showNotice('意图识别失败,使用默认路由', 'warning')
      }
    } catch (error: any) {
      const classified = classifyError(error)
      this.pavrState.errors.push(classified)
      showNotice(`意图识别错误: ${classified.message}`, 'error')
    }
  }

  /**
   * 📋 Phase 2: 执行计划
   */
  private async phaseExecutionPlan(userInput: string): Promise<void> {
    showTitle('📋 执行计划', 2)
    
    try {
      const intentData = this.pavrState.intent?.intent
      
      const result = await withRetry(
        () => generatePlan(userInput, intentData),
        { maxAttempts: 2, initialDelay: 100 }
      )
      
      if (result.success && result.data) {
        this.pavrState.plan = result.data
        
        if (result.data.steps && result.data.steps.length > 0) {
          showNotice(`生成了 ${result.data.steps.length} 个执行步骤`, 'success')
          
          // 显示计划摘要
          if (result.data.risks && result.data.risks.length > 0) {
            const riskDescriptions = result.data.risks.map((r: any) => 
              typeof r === 'string' ? r : r.description || r.level || '未知风险'
            ).join(', ')
            showNotice(`风险: ${riskDescriptions}`, 'warning')
          }
          
          // 自动确认执行
          showNotice('自动执行计划...', 'info')
        } else {
          // 无执行步骤,跳过 PAVR,直接调用 LLM
          showNotice('无执行步骤,跳过 PAVR', 'warning')
          this.pavrState.plan = null
        }
      }
    } catch (error: any) {
      const classified = classifyError(error)
      this.pavrState.errors.push(classified)
      showNotice(`计划生成错误: ${classified.message}`, 'error')
    }
  }

  /**
   * ⚡ Phase 3: 执行(工具调用循环)
   * PAVR模式:将计划步骤作为结构化指引注入上下文,由LLM驱动真实执行
   * 非PAVR模式:直接走executeLegacy
   */
  private async phaseExecution(onText?: (text: string) => void): Promise<string> {
    showTitle('⚡ 智能执行', 2)
    
    // PAVR模式:如果有计划,将步骤作为执行指引注入消息上下文
    if (this.pavrState.plan && this.pavrState.plan.steps && this.pavrState.plan.steps.length > 0) {
      const plan = this.pavrState.plan
      const stepsText = plan.steps.map((s, i) => 
        `${i + 1}. [${s.tool}] ${s.action} (预期: ${s.expected})`
      ).join('\n')
      
      const planContext = `[PAVR 执行指引 - 请按以下步骤执行,使用对应的工具调用完成每一步]\n${stepsText}\n\n请严格按照以上步骤执行,使用 file_write/bash_run/file_read 等工具完成每一步。`
      
      this.messages.push({ role: 'user', content: planContext })
    }
    
    // 统一走LLM驱动执行(executeLegacy会调用真实工具执行器)
    return this.executeLegacy(onText)
  }

  /**
   * ⚡ 使用 runner 执行计划(新方法)
   */
  private async executeWithRunner(onText?: (text: string) => void): Promise<string> {
    const plan = this.pavrState.plan!
    let finalText = ''
    
    // 使用 runner 执行计划
    const { executePlan } = await import('./planner/runner.js')
    
    const result = await executePlan(plan, {
      verbose: true,
      maxRetries: 2,
      stopOnError: false,
    }, (step, stepResult) => {
      // 步骤完成回调
      if (!onText) {
        const status = stepResult.success ? '✅' : '❌'
        process.stdout.write(`  ${status} 步骤 ${step.id}: ${step.action}\n`)
      }
      
      // 更新步骤状态
      if (stepResult.success) {
        step.status = 'completed'
        step.result = stepResult.output
      } else {
        step.status = 'failed'
        step.error = stepResult.error
      }
    })
    
    // 收集执行结果
    finalText = `计划执行完成: ${result.completedSteps}/${plan.steps.length} 步骤成功`
    
    // 添加到消息历史
    this.messages.push({ role: 'assistant', content: finalText })
    
    return finalText
  }

  /**
   * ⚡ 使用原有逻辑执行(兼容方法)
   */
  private async executeLegacy(onText?: (text: string) => void): Promise<string> {
    let finalText = ''
    let turns = 0
    const progress = new ProgressBar({ total: this.maxTurns, label: '执行轮次' })
    progress.start()

    while (turns < this.maxTurns) {
      turns++
      progress.update(turns)

      const openaiMessages = this.messages.map(m => {
        if (m.role === 'tool') {
          return { role: 'tool' as const, content: m.content, tool_call_id: m.tool_call_id! }
        }
        if (m.role === 'assistant' && m.tool_calls) {
          return {
            role: 'assistant' as const,
            content: m.content || null,
            tool_calls: m.tool_calls.map(tc => ({
              id: tc.id,
              type: 'function' as const,
              function: { name: tc.name, arguments: tc.arguments },
            })),
          }
        }
        return { role: m.role as any, content: m.content }
      })

      try {
        const stream = await withTimeout(
          () => this.client.chat.completions.create({
            model: this.config.model,
            messages: openaiMessages as any,
            tools: ALL_TOOLS.map(t => ({
              type: 'function' as const,
              function: { name: t.name, description: t.description, parameters: t.parameters },
            })),
            stream: true,
          }),
          this.config.timeoutMs
        )

        let textContent = ''
        let toolCalls: Map<number, { id: string; name: string; arguments: string }> = new Map()

        // 流式读取添加超时保护
        let streamEnded = false
        const streamTimeout = setTimeout(() => {
          if (!streamEnded) {
            console.error('\n⚠️ 流式响应超时,强制结束')
            stream.controller.abort()
          }
        }, Math.min(this.config.timeoutMs, 300000))

        try {
          for await (const chunk of stream) {
            if (streamEnded) break
            const delta = chunk.choices[0]?.delta
            if (!delta) continue

            if (delta.content) {
              const safeDelta = this.maskKeys(delta.content)
              textContent += safeDelta
              if (onText) onText(safeDelta)
              else process.stdout.write(safeDelta)
            }

            if (delta.tool_calls) {
              for (const tc of delta.tool_calls) {
                const idx = tc.index
                if (!toolCalls.has(idx)) {
                  toolCalls.set(idx, { id: tc.id || '', name: tc.function?.name || '', arguments: '' })
                }
                const existing = toolCalls.get(idx)!
                if (tc.id) existing.id = tc.id
                if (tc.function?.name) existing.name = tc.function.name
                if (tc.function?.arguments) existing.arguments += tc.function.arguments
              }
            }
          }
          streamEnded = true
        } finally {
          clearTimeout(streamTimeout)
        }

        if (textContent && !onText) process.stdout.write('\n')

        if (toolCalls.size === 0) {
          finalText = textContent
          this.messages.push({ role: 'assistant', content: textContent })
          break
        }

        const assistantToolCalls: ToolCall[] = []
        for (const [_, tc] of toolCalls) {
          assistantToolCalls.push({ id: tc.id, name: tc.name, arguments: tc.arguments })
        }

        this.messages.push({
          role: 'assistant',
          content: textContent || '',
          tool_calls: assistantToolCalls,
        })

        // 执行工具调用
        for (const tc of assistantToolCalls) {
          if (!onText) process.stdout.write(`\n🔧 ${tc.name}: ${this.summarizeArgs(tc)}\n`)
          
          try {
            const retryResult = await withRetry(
              () => executeTool(tc),
              { maxAttempts: 2 }
            )
            
            // withRetry 返回 ExecutionResult<ToolResult>,需要提取 .data
            if (!retryResult.success || !retryResult.data) {
              throw new Error(retryResult.error?.message || '工具执行失败')
            }
            const result: ToolResult = retryResult.data
            
            this.pavrState.toolResults.push(result)
            
            if (!onText) {
              const preview = result.content.length > 200 
                ? this.maskKeys(result.content).slice(0, 200) + '...' 
                : this.maskKeys(result.content)
              process.stdout.write(`  → ${preview}\n`)
            }

            this.messages.push({
              role: 'tool',
              content: this.maskKeys(result.content),
              tool_call_id: tc.id,
            })
            this.truncateIfNeeded()
          } catch (error: any) {
            const classified = classifyError(error)
            this.pavrState.errors.push(classified)

            if (!onText) {
              showNotice(`工具执行错误: ${classified.message}`, 'error')
            }

            this.messages.push({
              role: 'tool',
              content: `错误: ${classified.message}`,
              tool_call_id: tc.id,
            })
            this.truncateIfNeeded()
          }
        }

        if (!onText) process.stdout.write('\n')
        finalText = textContent
        
      } catch (e: any) {
        const classified = classifyError(e)
        this.pavrState.errors.push(classified)
        
        // 处理 429 错误
        if (classified.category === 'rate_limit') {
          const poolStatus = getChatKeyPoolStatus()
          if (poolStatus.remaining > 0) {
            const nextKey = rotateChatApiKey()
            if (nextKey && nextKey.name !== this.config.keyName) {
              const apiKeyStr = nextKey.apiKey.toString('utf-8')
              this.config.apiKey = apiKeyStr
              this.config.keyName = nextKey.name
              this.client = new OpenAI({
                apiKey: apiKeyStr,
                baseURL: nextKey.baseUrl,
                timeout: Math.min(this.config.timeoutMs, 120_000),
              })
              continue
            }
          }
        }
        
        throw e
      }
    }

    progress.complete()

    if (turns >= this.maxTurns) {
      const msg = `\n⚠️ Reached max turns (${this.maxTurns})`
      if (!onText) process.stdout.write(msg)
      finalText += msg
    }

    return finalText
  }

  /**
   * 🔍 Phase 4: 验证
   */
  private async phaseVerification(onText?: (text: string) => void): Promise<void> {
    showTitle('🔍 结果验证', 2)
    
    if (this.pavrState.toolResults.length > 0) {
      try {
        let successCount = 0
        let errorCount = 0
        
        for (const result of this.pavrState.toolResults) {
          // 通过 tool_call_id 匹配原始工具调用,确定工具类型
          const toolCall = this.messages.find(m => 
            m.role === 'assistant' && m.tool_calls?.some(tc => tc.id === result.tool_call_id)
          )?.tool_calls?.find(tc => tc.id === result.tool_call_id)
          
          const toolName = toolCall?.name || 'unknown'
          
          // 验证文件操作
          if (toolName === 'file_write' || toolName === 'file_edit') {
            if (result.content && !result.content.includes('Error') && !result.content.includes('error')) {
              successCount++
              showNotice(`✓ ${toolName} 成功`, 'success')
            } else {
              errorCount++
              showNotice(`✗ ${toolName} 失败: ${result.content.slice(0, 100)}`, 'error')
            }
          }
          // 验证命令执行
          else if (toolName === 'bash_run') {
            if (result.content && !result.content.includes('Error') && !result.content.includes('error')) {
              successCount++
            } else {
              errorCount++
            }
          }
        }
        
        showNotice(`验证完成: ${successCount} 成功, ${errorCount} 失败`, 
          errorCount > 0 ? 'warning' : 'success')

        // 🔍 增强:检测是否为 Bug 修复场景,自动运行相关测试验证
        if (successCount > 0 && this._detectBugFixScenario()) {
          showNotice('🧪 检测到 Bug 修复场景,自动运行测试验证...', 'info')
          try {
            const testCmd = this._inferTestCommand()
            if (testCmd) {
              const testResult = await executeTool({
                id: `pavr_verify_${Date.now()}`,
                name: 'bash_run',
                arguments: JSON.stringify({ command: testCmd, timeout: 30 }),
              })
              const testOutput = testResult.content || ''
              const hasFailures = testOutput.includes('FAILED') || testOutput.includes('failed') || testOutput.includes('Error')
              if (hasFailures) {
                showNotice(`⚠️ 测试验证发现问题: ${testOutput.slice(0, 200)}`, 'warning')
                // 将测试失败信息注入上下文,触发 LLM 自我修复循环
                this.messages.push({
                  role: 'user',
                  content: `[自动验证] 测试运行后发现失败:\n${testOutput.slice(0, 500)}\n\n请分析测试失败原因,调整你的补丁以通过测试。`
                })
                // 递归执行修复轮(最多2次修复尝试,避免无限循环)
                const repairAttempts = this.messages.filter(m => 
                  m.role === 'user' && m.content.includes('[自动验证]')
                ).length
                if (repairAttempts <= 2) {
                  showNotice('🔄 启动自动修复轮...', 'info')
                  try {
                    await this.executeLegacy(onText)
                  } catch (repairErr: any) {
                    showNotice(`修复轮执行错误: ${repairErr.message?.slice(0, 80)}`, 'error')
                  }
                } else {
                  showNotice('⏹️ 已达最大修复尝试次数,跳过', 'warning')
                }
              } else {
                showNotice('✅ 测试验证通过', 'success')
              }
            }
          } catch (verifyErr: any) {
            showNotice(`测试验证跳过: ${verifyErr.message?.slice(0, 80)}`, 'info')
          }
        }
      } catch (error: any) {
        const classified = classifyError(error)
        showNotice(`验证错误: ${classified.message}`, 'warning')
      }
    } else {
      showNotice('无工具调用,跳过验证', 'info')
    }
  }

  /** 检测当前是否为 Bug 修复场景 */
  private _detectBugFixScenario(): boolean {
    const userMsg = this.messages.find(m => m.role === 'user')
    if (!userMsg) return false
    const text = userMsg.content.toLowerCase()
    const bugKeywords = ['bug', 'fix', 'patch', 'issue', 'error', 'defect', 'swe-bench', 'swebench', 'swe', 'swe_bench', 'fail_to_pass', 'test_patch', '修复', '改bug', '补丁', '故障']
    return bugKeywords.some(kw => text.includes(kw))
  }

  /** 推断项目的测试命令(利用 bash_run cd 追踪特性) */
  private _inferTestCommand(): string | null {
    // 从文件写入操作中推断项目类型和测试命令
    const toolCalls = this.messages
      .filter(m => m.role === 'assistant' && m.tool_calls)
      .flatMap(m => m.tool_calls!)
      .filter(tc => tc.name === 'file_write' || tc.name === 'file_edit')
    
    if (toolCalls.length === 0) return null

    for (const tc of toolCalls) {
      try {
        const args = JSON.parse(tc.arguments)
        const filePath: string = args.path || ''
        // Python 项目
        if (filePath.endsWith('.py')) {
          // 从文件路径推断 SWE-bench 项目根目录
          // 标准模式: /tmp/swe_{project}/src/... 或任何路径下的 /src/
          let projectRoot: string
          if (filePath.includes('/tmp/')) {
            // SWE-bench 项目通常在 /tmp 下
            const tmpMatch = filePath.match(/(\/tmp\/[^/]+)/)
            projectRoot = tmpMatch ? tmpMatch[1] : '.'
          } else {
            const srcIndex = filePath.indexOf('/src/')
            projectRoot = srcIndex >= 0 ? filePath.substring(0, srcIndex) : 
              filePath.split('/').slice(0, -2).join('/') || '.'
          }
          // 利用 bash_run 的 cd 追踪:先 cd 到项目目录,再运行测试
          return `cd ${projectRoot} && python3 -m pytest -x --tb=short -q 2>&1 | tail -30`
        }
        // Node.js / TypeScript 项目
        if (filePath.endsWith('.ts') || filePath.endsWith('.js')) {
          let projectRoot: string
          if (filePath.includes('/tmp/')) {
            const tmpMatch = filePath.match(/(\/tmp\/[^/]+)/)
            projectRoot = tmpMatch ? tmpMatch[1] : '.'
          } else {
            const srcIndex = filePath.indexOf('/src/')
            projectRoot = srcIndex >= 0 ? filePath.substring(0, srcIndex) : 
              filePath.split('/').slice(0, -2).join('/') || '.'
          }
          return `cd ${projectRoot} && npx jest --no-coverage 2>&1 | tail -30 || npm test 2>&1 | tail -30 || echo "no-test-env"`
        }
      } catch {}
    }
    
    return null
  }

  /**
   * 💬 Phase 5: 响应摘要
   */
  private phaseResponse(): void {
    showTitle('💬 执行摘要', 2)
    
    const duration = Date.now() - this.pavrState.startTime
    
    if (this.pavrState.intent) {
      showNotice(`意图: ${this.pavrState.intent.intent?.type || 'unknown'}`, 'info')
    }
    
    if (this.pavrState.plan?.steps) {
      showNotice(`计划: ${this.pavrState.plan.steps.length} 步骤`, 'info')
    }
    
    showNotice(`工具调用: ${this.pavrState.toolResults.length} 次`, 'info')
    
    if (this.pavrState.errors.length > 0) {
      showNotice(`错误: ${this.pavrState.errors.length} 个`, 'warning')
    } else {
      showNotice(`错误: 0`, 'success')
    }
    
    showNotice(`总耗时: ${duration}ms`, 'info')
    showDivider()
  }

  /**
   * 获取 PAVR 状态
   */
  getPAVRState(): PAVRState {
    return { ...this.pavrState }
  }

  private summarizeArgs(tc: ToolCall): string {
    try {
      const args = JSON.parse(tc.arguments)
      switch (tc.name) {
        case 'file_read': return args.path
        case 'file_write': return args.path
        case 'file_edit': return args.path
        case 'bash_run': return args.command?.slice(0, 80)
        case 'grep_search': return `"${args.pattern}" in ${args.path || '.'}`
        case 'list_dir': return args.path || '.'
        default: return JSON.stringify(args).slice(0, 80)
      }
    } catch {
      return tc.arguments.slice(0, 80)
    }
  }

  // ===== 原有方法保持兼容 =====

  getHistory(): Message[] {
    return [...this.messages]
  }

  static fromHistory(messages: Message[], config?: Config, workspaceDir?: string): EnhancedChatEngine {
    const engine = new EnhancedChatEngine(config, undefined, workspaceDir)
    if (messages.length > 0 && messages[0].role === 'system') {
      engine.messages = messages
    }
    return engine
  }

  async chat(userInput: string, onText?: (text: string) => void): Promise<string> {
    if (ENABLE_PAVR_LOOP) {
      return this.chatPAVR(userInput, onText)
    } else {
      // 兼容原逻辑
      return this.chatLegacy(userInput, onText)
    }
  }

  private async chatLegacy(userInput: string, onText?: (text: string) => void): Promise<string> {
    this.messages.push({ role: 'user', content: userInput })
    let finalText = ''
    let turns = 0

    while (turns < this.maxTurns) {
      turns++
      const openaiMessages = this.messages.map(m => {
        if (m.role === 'tool') {
          return { role: 'tool' as const, content: m.content, tool_call_id: m.tool_call_id! }
        }
        if (m.role === 'assistant' && m.tool_calls) {
          return {
            role: 'assistant' as const,
            content: m.content || null,
            tool_calls: m.tool_calls.map(tc => ({
              id: tc.id,
              type: 'function' as const,
              function: { name: tc.name, arguments: tc.arguments },
            })),
          }
        }
        return { role: m.role as any, content: m.content }
      })

      try {
        const stream = await this.client.chat.completions.create({
          model: this.config.model,
          messages: openaiMessages as any,
          tools: ALL_TOOLS.map(t => ({
            type: 'function' as const,
            function: { name: t.name, description: t.description, parameters: t.parameters },
          })),
          stream: true,
        }, { signal: AbortSignal.timeout(this.config.timeoutMs) })

        let textContent = ''
        let toolCalls: Map<number, { id: string; name: string; arguments: string }> = new Map()

        // PAVR 修复:流式读取添加超时保护,防止卡死
        let streamEnded = false
        const streamTimeout = setTimeout(() => {
          if (!streamEnded) {
            console.error('\n⚠️ 流式响应超时,强制结束')
            stream.controller.abort()
          }
        }, Math.min(this.config.timeoutMs, 300000)) // 最长 5 分钟

        try {
          for await (const chunk of stream) {
            if (streamEnded) break
            const delta = chunk.choices[0]?.delta
            if (!delta) continue

            if (delta.content) {
              const safeDelta = this.maskKeys(delta.content)
              textContent += safeDelta
              if (onText) onText(safeDelta)
              else process.stdout.write(safeDelta)
            }

            if (delta.tool_calls) {
              for (const tc of delta.tool_calls) {
                const idx = tc.index
                if (!toolCalls.has(idx)) {
                  toolCalls.set(idx, { id: tc.id || '', name: tc.function?.name || '', arguments: '' })
                }
                const existing = toolCalls.get(idx)!
                if (tc.id) existing.id = tc.id
                if (tc.function?.name) existing.name = tc.function.name
                if (tc.function?.arguments) existing.arguments += tc.function.arguments
              }
            }
          }
          streamEnded = true
        } finally {
          clearTimeout(streamTimeout)
        }

        if (textContent && !onText) process.stdout.write('\n')

        if (toolCalls.size === 0) {
          finalText = textContent
          this.messages.push({ role: 'assistant', content: textContent })
          break
        }

        const assistantToolCalls: ToolCall[] = []
        for (const [_, tc] of toolCalls) {
          assistantToolCalls.push({ id: tc.id, name: tc.name, arguments: tc.arguments })
        }

        this.messages.push({
          role: 'assistant',
          content: textContent || '',
          tool_calls: assistantToolCalls,
        })

        for (const tc of assistantToolCalls) {
          const result = await executeTool(tc)
          this.messages.push({
            role: 'tool',
            content: this.maskKeys(result.content),
            tool_call_id: tc.id,
          })
        }

        if (!onText) process.stdout.write('\n')
        finalText = textContent
        
      } catch (e: any) {
        throw e
      }
    }

    if (turns >= this.maxTurns) {
      const msg = `\n⚠️ Reached max turns (${this.maxTurns})`
      if (!onText) process.stdout.write(msg)
      finalText += msg
    }

    return finalText
  }
}

// 导出原有类型
export type { ToolCall, ToolResult } from './tools.js'