feat(aia): Complete AIA V2.0 with universal streaming capabilities
Major Updates: - Add StreamingService with OpenAI Compatible format (backend/common/streaming) - Upgrade Chat component V2 with Ant Design X integration - Implement AIA module with 12 intelligent agents - Create AgentHub with 100% prototype V11 restoration - Create ChatWorkspace with streaming response support - Add ThinkingBlock for deep thinking display - Add useAIStream Hook for OpenAI Compatible stream handling Backend Common Capabilities (~400 lines): - OpenAIStreamAdapter: SSE adapter with OpenAI format - StreamingService: unified streaming service - Support content and reasoning_content dual streams - Deep thinking tag processing (<think>...</think>) Frontend Common Capabilities (~2000 lines): - AIStreamChat: modern streaming chat component - ThinkingBlock: collapsible deep thinking display - ConversationList: conversation management with grouping - useAIStream: OpenAI Compatible stream handler Hook - useConversations: conversation state management Hook - Modern design styles (Ultramodern theme) AIA Module Frontend (~1500 lines): - AgentHub: 12 agent cards with timeline design - ChatWorkspace: fullscreen immersive chat interface - AgentCard: theme-colored cards (blue/yellow/teal/purple) - 5 phases, 12 agents configuration - Responsive layout (desktop + mobile) AIA Module Backend (~900 lines): - agentService: 12 agents config with system prompts - conversationService: refactored with StreamingService - attachmentService: file upload skeleton (30k token limit) - 12 API endpoints with authentication - Full CRUD for conversations and messages Documentation: - AIA module status and development guide - Universal capabilities catalog (11 services) - Quick reference card for developers - System overview updates Testing: - Stream response verified (HTTP 200) - Authentication working correctly - Auto conversation creation working - Deep thinking display working - Message input and send working Status: Core features completed (85%), attachment and history loading pending
This commit is contained in:
196
backend/src/common/streaming/OpenAIStreamAdapter.ts
Normal file
196
backend/src/common/streaming/OpenAIStreamAdapter.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* OpenAI Compatible 流式响应适配器
|
||||
*
|
||||
* 将内部 LLM 响应转换为 OpenAI Compatible 格式
|
||||
* 支持 Ant Design X 的 XRequest 直接消费
|
||||
*/
|
||||
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { OpenAIStreamChunk, StreamOptions, THINKING_TAGS } from './types';
|
||||
import { logger } from '../logging/logger';
|
||||
|
||||
/**
|
||||
* OpenAI 流式响应适配器
|
||||
*/
|
||||
export class OpenAIStreamAdapter {
|
||||
private reply: FastifyReply;
|
||||
private messageId: string;
|
||||
private model: string;
|
||||
private created: number;
|
||||
private isHeaderSent: boolean = false;
|
||||
|
||||
constructor(reply: FastifyReply, model: string = 'deepseek-v3') {
|
||||
this.reply = reply;
|
||||
this.messageId = `chatcmpl-${uuidv4()}`;
|
||||
this.model = model;
|
||||
this.created = Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 SSE 连接
|
||||
*/
|
||||
initSSE(): void {
|
||||
if (this.isHeaderSent) return;
|
||||
|
||||
this.reply.raw.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
});
|
||||
|
||||
this.isHeaderSent = true;
|
||||
logger.debug('[OpenAIStreamAdapter] SSE 连接已初始化');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送内容增量
|
||||
*/
|
||||
sendContentDelta(content: string): void {
|
||||
this.initSSE();
|
||||
|
||||
const chunk: OpenAIStreamChunk = {
|
||||
id: this.messageId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: this.created,
|
||||
model: this.model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { content },
|
||||
finish_reason: null,
|
||||
}],
|
||||
};
|
||||
|
||||
this.writeChunk(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送思考内容增量(DeepSeek 风格)
|
||||
*/
|
||||
sendReasoningDelta(reasoningContent: string): void {
|
||||
this.initSSE();
|
||||
|
||||
const chunk: OpenAIStreamChunk = {
|
||||
id: this.messageId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: this.created,
|
||||
model: this.model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { reasoning_content: reasoningContent },
|
||||
finish_reason: null,
|
||||
}],
|
||||
};
|
||||
|
||||
this.writeChunk(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送角色标识(流开始时)
|
||||
*/
|
||||
sendRoleStart(): void {
|
||||
this.initSSE();
|
||||
|
||||
const chunk: OpenAIStreamChunk = {
|
||||
id: this.messageId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: this.created,
|
||||
model: this.model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { role: 'assistant' },
|
||||
finish_reason: null,
|
||||
}],
|
||||
};
|
||||
|
||||
this.writeChunk(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送完成标识
|
||||
*/
|
||||
sendComplete(usage?: { promptTokens: number; completionTokens: number; totalTokens: number }): void {
|
||||
this.initSSE();
|
||||
|
||||
const chunk: OpenAIStreamChunk = {
|
||||
id: this.messageId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: this.created,
|
||||
model: this.model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: {},
|
||||
finish_reason: 'stop',
|
||||
}],
|
||||
usage: usage ? {
|
||||
prompt_tokens: usage.promptTokens,
|
||||
completion_tokens: usage.completionTokens,
|
||||
total_tokens: usage.totalTokens,
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
this.writeChunk(chunk);
|
||||
|
||||
// 发送 [DONE] 标识
|
||||
this.reply.raw.write('data: [DONE]\n\n');
|
||||
logger.debug('[OpenAIStreamAdapter] 流式响应完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误
|
||||
*/
|
||||
sendError(error: Error | string): void {
|
||||
this.initSSE();
|
||||
|
||||
const errorMessage = typeof error === 'string' ? error : error.message;
|
||||
|
||||
const errorChunk = {
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: 'server_error',
|
||||
code: 'internal_error',
|
||||
},
|
||||
};
|
||||
|
||||
this.reply.raw.write(`data: ${JSON.stringify(errorChunk)}\n\n`);
|
||||
this.reply.raw.write('data: [DONE]\n\n');
|
||||
|
||||
logger.error('[OpenAIStreamAdapter] 流式响应错误', { error: errorMessage });
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束流
|
||||
*/
|
||||
end(): void {
|
||||
if (this.isHeaderSent) {
|
||||
this.reply.raw.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息 ID
|
||||
*/
|
||||
getMessageId(): string {
|
||||
return this.messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入 Chunk
|
||||
*/
|
||||
private writeChunk(chunk: OpenAIStreamChunk): void {
|
||||
this.reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 OpenAI 流式适配器
|
||||
*/
|
||||
export function createOpenAIStreamAdapter(
|
||||
reply: FastifyReply,
|
||||
model?: string
|
||||
): OpenAIStreamAdapter {
|
||||
return new OpenAIStreamAdapter(reply, model);
|
||||
}
|
||||
|
||||
202
backend/src/common/streaming/StreamingService.ts
Normal file
202
backend/src/common/streaming/StreamingService.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 通用流式响应服务
|
||||
*
|
||||
* 封装 LLM 调用 + OpenAI Compatible 输出
|
||||
* 支持深度思考、Token 统计、错误处理
|
||||
*/
|
||||
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { OpenAIStreamAdapter, createOpenAIStreamAdapter } from './OpenAIStreamAdapter';
|
||||
import { StreamOptions, StreamCallbacks, THINKING_TAGS, OpenAIMessage } from './types';
|
||||
import { LLMFactory } from '../llm/adapters/LLMFactory';
|
||||
import type { Message as LLMMessage } from '../llm/adapters/types';
|
||||
import { logger } from '../logging/logger';
|
||||
|
||||
/**
|
||||
* 深度思考标签处理结果
|
||||
*/
|
||||
interface ThinkingParseResult {
|
||||
content: string;
|
||||
thinking: string;
|
||||
inThinking: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式响应服务
|
||||
*/
|
||||
export class StreamingService {
|
||||
private adapter: OpenAIStreamAdapter;
|
||||
private options: StreamOptions;
|
||||
private fullContent: string = '';
|
||||
private thinkingContent: string = '';
|
||||
private isInThinking: boolean = false;
|
||||
|
||||
constructor(reply: FastifyReply, options: StreamOptions = {}) {
|
||||
this.adapter = createOpenAIStreamAdapter(reply, options.model);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流式生成
|
||||
*/
|
||||
async streamGenerate(
|
||||
messages: OpenAIMessage[],
|
||||
callbacks?: StreamCallbacks
|
||||
): Promise<{ content: string; thinking: string; messageId: string }> {
|
||||
const { model = 'deepseek-v3', temperature = 0.7, maxTokens = 4096 } = this.options;
|
||||
|
||||
try {
|
||||
// 获取 LLM 适配器
|
||||
const llm = LLMFactory.getAdapter(model as any);
|
||||
|
||||
// 发送角色开始标识
|
||||
this.adapter.sendRoleStart();
|
||||
|
||||
// 流式生成
|
||||
const stream = llm.chatStream(
|
||||
messages as LLMMessage[],
|
||||
{ temperature, maxTokens }
|
||||
);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.content) {
|
||||
// 处理深度思考标签
|
||||
const { content, thinking, inThinking } = this.processThinkingTags(
|
||||
chunk.content,
|
||||
this.options.enableDeepThinking ?? false
|
||||
);
|
||||
|
||||
// 发送思考内容
|
||||
if (thinking) {
|
||||
this.thinkingContent += thinking;
|
||||
this.adapter.sendReasoningDelta(thinking);
|
||||
callbacks?.onThinking?.(thinking);
|
||||
}
|
||||
|
||||
// 发送正文内容
|
||||
if (content) {
|
||||
this.fullContent += content;
|
||||
this.adapter.sendContentDelta(content);
|
||||
callbacks?.onContent?.(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送完成标识
|
||||
const usage = {
|
||||
promptTokens: this.estimateTokens(messages.map(m => m.content).join('')),
|
||||
completionTokens: this.estimateTokens(this.fullContent),
|
||||
totalTokens: 0,
|
||||
};
|
||||
usage.totalTokens = usage.promptTokens + usage.completionTokens;
|
||||
|
||||
this.adapter.sendComplete(usage);
|
||||
this.adapter.end();
|
||||
|
||||
// 完成回调
|
||||
callbacks?.onComplete?.(this.fullContent, this.thinkingContent);
|
||||
|
||||
logger.info('[StreamingService] 流式生成完成', {
|
||||
conversationId: this.options.conversationId,
|
||||
contentLength: this.fullContent.length,
|
||||
thinkingLength: this.thinkingContent.length,
|
||||
tokens: usage.totalTokens,
|
||||
});
|
||||
|
||||
return {
|
||||
content: this.fullContent,
|
||||
thinking: this.thinkingContent,
|
||||
messageId: this.adapter.getMessageId(),
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '流式生成失败';
|
||||
this.adapter.sendError(errorMessage);
|
||||
this.adapter.end();
|
||||
|
||||
callbacks?.onError?.(error instanceof Error ? error : new Error(errorMessage));
|
||||
|
||||
logger.error('[StreamingService] 流式生成失败', {
|
||||
error,
|
||||
conversationId: this.options.conversationId,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理深度思考标签
|
||||
*/
|
||||
private processThinkingTags(text: string, enableDeepThinking: boolean): ThinkingParseResult {
|
||||
if (!enableDeepThinking) {
|
||||
return { content: text, thinking: '', inThinking: this.isInThinking };
|
||||
}
|
||||
|
||||
let content = '';
|
||||
let thinking = '';
|
||||
let remaining = text;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
if (this.isInThinking) {
|
||||
// 在思考模式中,查找结束标签
|
||||
const endIndex = remaining.indexOf(THINKING_TAGS.END);
|
||||
if (endIndex !== -1) {
|
||||
thinking += remaining.substring(0, endIndex);
|
||||
remaining = remaining.substring(endIndex + THINKING_TAGS.END.length);
|
||||
this.isInThinking = false;
|
||||
} else {
|
||||
thinking += remaining;
|
||||
remaining = '';
|
||||
}
|
||||
} else {
|
||||
// 不在思考模式,查找开始标签
|
||||
const startIndex = remaining.indexOf(THINKING_TAGS.START);
|
||||
if (startIndex !== -1) {
|
||||
content += remaining.substring(0, startIndex);
|
||||
remaining = remaining.substring(startIndex + THINKING_TAGS.START.length);
|
||||
this.isInThinking = true;
|
||||
} else {
|
||||
content += remaining;
|
||||
remaining = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { content, thinking, inThinking: this.isInThinking };
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算 Token 数量(简单实现)
|
||||
*/
|
||||
private estimateTokens(text: string): number {
|
||||
// 中文约 1.5 字符/token,英文约 4 字符/token
|
||||
const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
|
||||
const otherChars = text.length - chineseChars;
|
||||
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建流式响应服务
|
||||
*/
|
||||
export function createStreamingService(
|
||||
reply: FastifyReply,
|
||||
options?: StreamOptions
|
||||
): StreamingService {
|
||||
return new StreamingService(reply, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷方法:直接执行流式生成
|
||||
*/
|
||||
export async function streamChat(
|
||||
reply: FastifyReply,
|
||||
messages: OpenAIMessage[],
|
||||
options?: StreamOptions,
|
||||
callbacks?: StreamCallbacks
|
||||
) {
|
||||
const service = createStreamingService(reply, options);
|
||||
return service.streamGenerate(messages, callbacks);
|
||||
}
|
||||
|
||||
20
backend/src/common/streaming/index.ts
Normal file
20
backend/src/common/streaming/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 通用流式响应服务 - 统一导出
|
||||
*
|
||||
* 提供 OpenAI Compatible 格式的流式响应能力
|
||||
* 支持 Ant Design X 的 XRequest 直接消费
|
||||
*/
|
||||
|
||||
export { OpenAIStreamAdapter, createOpenAIStreamAdapter } from './OpenAIStreamAdapter';
|
||||
export { StreamingService, createStreamingService, streamChat } from './StreamingService';
|
||||
|
||||
export type {
|
||||
OpenAIMessage,
|
||||
OpenAIStreamChunk,
|
||||
StreamOptions,
|
||||
StreamCallbacks,
|
||||
SSEEventType,
|
||||
} from './types';
|
||||
|
||||
export { THINKING_TAGS } from './types';
|
||||
|
||||
95
backend/src/common/streaming/types.ts
Normal file
95
backend/src/common/streaming/types.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 通用流式响应服务 - 类型定义
|
||||
*
|
||||
* 基于 OpenAI Compatible 格式,支持:
|
||||
* - 标准内容流
|
||||
* - 深度思考流(reasoning_content)
|
||||
* - 工具调用(预留)
|
||||
*/
|
||||
|
||||
/**
|
||||
* OpenAI Compatible 消息格式
|
||||
*/
|
||||
export interface OpenAIMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI Compatible 流式 Chunk
|
||||
* 参考: https://platform.openai.com/docs/api-reference/chat/streaming
|
||||
*/
|
||||
export interface OpenAIStreamChunk {
|
||||
id: string;
|
||||
object: 'chat.completion.chunk';
|
||||
created: number;
|
||||
model: string;
|
||||
choices: Array<{
|
||||
index: number;
|
||||
delta: {
|
||||
role?: 'assistant';
|
||||
content?: string;
|
||||
reasoning_content?: string; // DeepSeek 风格的深度思考
|
||||
};
|
||||
finish_reason: 'stop' | 'length' | 'tool_calls' | null;
|
||||
}>;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式生成选项
|
||||
*/
|
||||
export interface StreamOptions {
|
||||
/** 模型名称 */
|
||||
model?: string;
|
||||
/** 温度 */
|
||||
temperature?: number;
|
||||
/** 最大 tokens */
|
||||
maxTokens?: number;
|
||||
/** 是否启用深度思考 */
|
||||
enableDeepThinking?: boolean;
|
||||
/** 系统提示词 */
|
||||
systemPrompt?: string;
|
||||
/** 用户 ID(用于日志) */
|
||||
userId?: string;
|
||||
/** 会话 ID(用于日志) */
|
||||
conversationId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式回调函数类型
|
||||
*/
|
||||
export interface StreamCallbacks {
|
||||
/** 内容增量回调 */
|
||||
onContent?: (content: string) => void;
|
||||
/** 思考内容增量回调 */
|
||||
onThinking?: (content: string) => void;
|
||||
/** 完成回调 */
|
||||
onComplete?: (fullContent: string, thinkingContent: string) => void;
|
||||
/** 错误回调 */
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度思考标签
|
||||
*/
|
||||
export const THINKING_TAGS = {
|
||||
START: '<think>',
|
||||
END: '</think>',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* SSE 事件类型
|
||||
*/
|
||||
export type SSEEventType =
|
||||
| 'message_start'
|
||||
| 'content_delta'
|
||||
| 'reasoning_delta'
|
||||
| 'message_end'
|
||||
| 'error'
|
||||
| 'done';
|
||||
|
||||
Reference in New Issue
Block a user