refactor: 聊天室 Agent 重构 — ToolLoopAgent + 论坛式布局

后端:
- 删除 agent-stream.ts,新建 alfred-agent.ts (ToolLoopAgent 工厂)
- 新建 get-current-time.ts 工具 (zod schema)
- 重构 send.ts: createAgentUIStreamResponse + onFinish 可靠持久化

前端:
- 删除 MessageBubble.tsx,新建 ToolCallCard.tsx (四态)
- 重构 ChatPanel.tsx: 论坛式 Card 布局 + PartRenderer 分派
- 移除 @ant-design/x 依赖,改用 antd 组件 + streamdown

依赖:
+ zod + streamdown
- @ant-design/x - @ant-design/x-markdown

测试: 306 pass, typecheck/lint 0 errors
This commit is contained in:
2026-05-31 17:25:29 +08:00
parent f83f434863
commit 6eeb4ced7b
16 changed files with 698 additions and 322 deletions

View File

@@ -1,10 +1,15 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import type { IncomingMessage } from "../../ai/agent-stream";
import { createAgentUIStreamResponse, type UIMessage } from "ai";
import { eq } from "drizzle-orm";
import { agentStream, extractTextContent } from "../../ai/agent-stream";
import type { RuntimeMode } from "../../../shared/api";
import { createAlfredAgent } from "../../ai/agents/alfred-agent";
import { buildProviderRegistry } from "../../ai/registry";
import { wrap } from "../../db/connection";
import { createMessage, getConversation, updateConversationTimestamp } from "../../db/conversations";
import { models, providers } from "../../db/schema";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
@@ -15,7 +20,7 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo
const validated = validateIdParam(projectId ?? "", mode);
if (validated instanceof Response) return validated;
let body: { conversationId?: string; messages?: IncomingMessage[] };
let body: { conversationId?: string; messages?: UIMessage[] };
try {
body = (await req.json()) as typeof body;
} catch {
@@ -46,44 +51,62 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo
return jsonResponse(createApiError("会话不属于该项目", 403), { mode, status: 403 });
}
for (const msg of body.messages ?? []) {
const lastMsg = body.messages[body.messages.length - 1];
if (lastMsg?.role === "user") {
const content =
lastMsg.parts
?.filter((p) => p.type === "text")
.map((p) => p.text)
.join("") ?? "";
createMessage(db, {
content: extractTextContent(msg),
content,
conversationId: conversation.id,
role: (msg.role ?? "user") as "assistant" | "system" | "user",
parts: JSON.stringify(lastMsg.parts ?? []),
role: "user",
});
}
updateConversationTimestamp(db, conversation.id);
let model;
try {
const result = agentStream({
db,
messages: body.messages,
modelDbId: conversation.modelId,
const d = wrap(db);
const modelRow = d.select().from(models).where(eq(models.id, conversation.modelId)).get();
if (!modelRow) {
return jsonResponse(createApiError(`模型不存在: ${conversation.modelId}`, 500), { mode, status: 500 });
}
const providerRow = d.select().from(providers).where(eq(providers.id, modelRow.providerId)).get();
if (!providerRow) {
return jsonResponse(createApiError(`供应商不存在: ${modelRow.providerId}`, 500), { mode, status: 500 });
}
const registry = buildProviderRegistry(db);
model = registry.languageModel(`${providerRow.id}:${modelRow.modelId}`);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return jsonResponse(createApiError(`模型初始化失败:${msg}`, 500), { mode, status: 500 });
}
try {
const agent = createAlfredAgent(model);
return await createAgentUIStreamResponse({
agent,
onFinish: ({ responseMessage }) => {
const text = responseMessage.parts
.filter((p): p is { text: string; type: "text" } => p.type === "text")
.map((p) => p.text)
.join("");
createMessage(db, {
content: text,
conversationId: conversation.id,
parts: JSON.stringify(responseMessage.parts),
role: "assistant",
});
updateConversationTimestamp(db, conversation.id);
},
uiMessages: body.messages,
});
const stream = result.toUIMessageStreamResponse();
const saveReply = async () => {
try {
const fullContent = await result.text;
if (fullContent) {
createMessage(db, {
content: fullContent,
conversationId: conversation.id,
role: "assistant",
});
updateConversationTimestamp(db, conversation.id);
}
} catch {
// stream ended without content, nothing to persist
}
};
void saveReply();
return stream;
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return jsonResponse(createApiError(`AI 调用失败:${msg}`, 500), { mode, status: 500 });