📄 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'