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

@@ -0,0 +1,25 @@
import { describe, expect, test } from "bun:test";
import { formatCurrentTime } from "../../../../src/server/ai/tools/get-current-time";
describe("getCurrentTime 工具", () => {
test("formatCurrentTime 返回 ISO/local/timestamp", () => {
const result = formatCurrentTime();
expect(result).toBeDefined();
expect(typeof result.iso).toBe("string");
expect(typeof result.local).toBe("string");
expect(typeof result.timestamp).toBe("number");
});
test("formatCurrentTime 指定 timezone", () => {
const result = formatCurrentTime("Asia/Shanghai");
expect(result).toBeDefined();
expect(typeof result.local).toBe("string");
});
test("formatCurrentTime 无效 timezone 优雅降级", () => {
const result = formatCurrentTime("Invalid/Zone");
expect(result).toBeDefined();
expect(typeof result.local).toBe("string");
});
});

View File

@@ -12,18 +12,24 @@ import { createMigratedMemoryTestDatabase } from "../../helpers";
const MODE: RuntimeMode = "test";
void mock.module("ai", () => ({
createAgentUIStreamResponse: () =>
Promise.resolve(
new Response(
'data: {"type":"start-step"}\n\ndata: {"type":"text-start","id":"txt-1"}\n\ndata: {"type":"text-delta","id":"txt-1","delta":"test reply from AI"}\n\ndata: {"type":"text-end","id":"txt-1"}\n\ndata: {"type":"finish-step"}\n\ndata: {"type":"finish"}\n\n',
{
headers: { "Content-Type": "text/event-stream" },
},
),
),
createProviderRegistry: () => ({
languageModel: () => ({}),
}),
generateText: () => Promise.resolve({ text: "mock", usage: {} }),
generateText: () => Promise.resolve({ text: "", usage: {} }),
stepCountIs: () => () => true,
streamText: () => ({
text: Promise.resolve("test reply from AI"),
toUIMessageStreamResponse: () =>
new Response('data: {"type":"text","text":"test reply from AI"}\n\n', {
headers: { "Content-Type": "text/event-stream" },
}),
}),
tool: () => ({ execute: async () => await Promise.resolve({}) }),
ToolLoopAgent: function M() {
// no-op: createAgentUIStreamResponse handles streaming
},
}));
async function createConversationViaHandler(req: Request, db: Database): Promise<Response> {