feat(agent): 实现流式输出取消功能
- 添加 AbortController 管理,支持取消正在进行的请求 - Agent.chat() 新增 abortSignal 参数,传递给 streamText/generateText - 取消时保存用户消息和 AI 已输出的部分内容 - cancelProcessing 实际调用 abort() 中止流式请求
This commit is contained in:
@@ -23,6 +23,14 @@ import { getProviderRegistry, resolveApiKey } from '../provider/index.js';
|
||||
import { getHookManager } from '../hooks/index.js';
|
||||
import { getGitManager } from '../git/index.js';
|
||||
|
||||
/**
|
||||
* Agent.chat() 选项
|
||||
*/
|
||||
export interface AgentChatOptions {
|
||||
onStream?: (text: string) => void;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
export class Agent {
|
||||
private getModel: (model: string) => LanguageModel;
|
||||
private config: AgentConfig;
|
||||
@@ -310,10 +318,13 @@ export class Agent {
|
||||
/**
|
||||
* 发送消息并处理响应(流式)
|
||||
* @param userMessage 用户消息文本或包含图片的 UserInput
|
||||
* @param onStream 流式输出回调
|
||||
* @param options 选项,包含 onStream 回调和 abortSignal
|
||||
* @returns ChatResult 包含最终文本和完整的响应消息链
|
||||
*/
|
||||
async chat(userMessage: string | UserInput, onStream?: (text: string) => void): Promise<ChatResult> {
|
||||
async chat(userMessage: string | UserInput, options?: AgentChatOptions | ((text: string) => void)): Promise<ChatResult> {
|
||||
// 兼容旧的 onStream 参数
|
||||
const opts: AgentChatOptions = typeof options === 'function' ? { onStream: options } : (options || {});
|
||||
const { onStream, abortSignal } = opts;
|
||||
// 处理带图片的消息
|
||||
let processedMessage = userMessage;
|
||||
|
||||
@@ -391,6 +402,7 @@ export class Agent {
|
||||
tools: vercelTools,
|
||||
maxOutputTokens: this.config.maxTokens,
|
||||
stopWhen: stepCountIs(10), // 允许最多 10 轮工具调用
|
||||
abortSignal, // 支持取消
|
||||
onChunk: ({ chunk }) => {
|
||||
if (chunk.type === 'tool-call') {
|
||||
onStream(`\n[调用工具: ${chunk.toolName}]\n`);
|
||||
@@ -413,14 +425,50 @@ export class Agent {
|
||||
});
|
||||
|
||||
// 流式输出文本
|
||||
for await (const chunk of result.textStream) {
|
||||
fullResponse += chunk;
|
||||
onStream(chunk);
|
||||
}
|
||||
let aborted = false;
|
||||
try {
|
||||
for await (const chunk of result.textStream) {
|
||||
// 检查是否已中止
|
||||
if (abortSignal?.aborted) {
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
fullResponse += chunk;
|
||||
onStream(chunk);
|
||||
}
|
||||
|
||||
// 等待完成并获取完整的响应消息(包括工具调用和结果)
|
||||
const response = await result.response;
|
||||
responseMessages = response.messages as ModelMessage[];
|
||||
// 如果是手动中止(通过 break 退出),保存已收到的内容
|
||||
if (aborted) {
|
||||
onStream?.('\n[已取消]\n');
|
||||
if (fullResponse) {
|
||||
this.conversationHistory.push({
|
||||
role: 'assistant',
|
||||
content: fullResponse + '\n[已取消]',
|
||||
} as ModelMessage);
|
||||
await this.persistSession();
|
||||
}
|
||||
return { text: fullResponse, messages: [] };
|
||||
}
|
||||
|
||||
// 等待完成并获取完整的响应消息(包括工具调用和结果)
|
||||
const response = await result.response;
|
||||
responseMessages = response.messages as ModelMessage[];
|
||||
} catch (error) {
|
||||
// 如果是中止错误(AbortController.abort() 抛出),优雅处理
|
||||
if (error instanceof Error && (error.name === 'AbortError' || abortSignal?.aborted)) {
|
||||
onStream?.('\n[已取消]\n');
|
||||
// 取消时也要保存已收到的内容
|
||||
if (fullResponse) {
|
||||
this.conversationHistory.push({
|
||||
role: 'assistant',
|
||||
content: fullResponse + '\n[已取消]',
|
||||
} as ModelMessage);
|
||||
await this.persistSession();
|
||||
}
|
||||
return { text: fullResponse, messages: [] };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// 非流式模式
|
||||
const result = await generateText({
|
||||
@@ -430,6 +478,7 @@ export class Agent {
|
||||
tools: vercelTools,
|
||||
maxOutputTokens: this.config.maxTokens,
|
||||
stopWhen: stepCountIs(10), // 允许最多 10 轮工具调用
|
||||
abortSignal, // 支持取消
|
||||
});
|
||||
|
||||
fullResponse = result.text;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { Agent } from './core/agent.js';
|
||||
export type { AgentChatOptions } from './core/agent.js';
|
||||
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
|
||||
export { loadConfig, saveConfig, getConfig, loadVisionConfig, ConfigurationError } from './utils/config.js';
|
||||
export type { VisionConfig } from './utils/config.js';
|
||||
|
||||
Reference in New Issue
Block a user