feat: 聊天室模型选择器 + 会话更新 API + 消息部件重构
- 新增 PATCH /api/projects/:id/conversations/:cid 端点,支持更新 modelId 和 title - 聊天面板新增模型选择下拉框,切换模型自动持久化 - 新建会话时传入默认文本模型 modelId - 将 ToolCallCard 拆分为 ReasoningPart / TextPart / ToolPart 独立部件 - ToolPart 增加流式状态图标、折叠面板自动展开、错误详情展示 - ReasoningPart 增加思考中/思考完成状态指示 - 补充 PATCH 端点测试:更新成功、跨项目 403、不存在 404、无效 modelId 400
This commit is contained in:
@@ -57,10 +57,15 @@ async function listMessagesViaHandler(req: Request, db: Database): Promise<Respo
|
||||
return h(req, db, MODE);
|
||||
}
|
||||
|
||||
function seedModel(db: Database, providerId: string, modelName = "GPT-4o"): string {
|
||||
async function patchConversationViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleUpdateConversation: h } = await import("../../../src/server/routes/chat/update");
|
||||
return h(req, db, MODE);
|
||||
}
|
||||
|
||||
function seedModel(db: Database, providerId: string, modelName = "GPT-4o", modelId = "gpt-4o"): string {
|
||||
const result = createModel(db, {
|
||||
capabilities: ["text"],
|
||||
modelId: "gpt-4o",
|
||||
modelId,
|
||||
name: modelName,
|
||||
providerId,
|
||||
});
|
||||
@@ -352,6 +357,167 @@ describe("聊天 API 路由", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/projects/:id/conversations/:cid", () => {
|
||||
test("更新会话 modelId 成功", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-model");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
const providerId = seedProvider(db);
|
||||
const modelId1 = seedModel(db, providerId, "GPT-4o", "gpt-4o");
|
||||
const modelId2 = seedModel(db, providerId, "GPT-4o-mini", "gpt-4o-mini");
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations`, {
|
||||
body: JSON.stringify({ modelId: modelId1 }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/${created.id}`, {
|
||||
body: JSON.stringify({ modelId: modelId2 }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { conversation: Conversation };
|
||||
expect(body.conversation.modelId).toBe(modelId2);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("更新会话 title 成功", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-title");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
const providerId = seedProvider(db);
|
||||
seedModel(db, providerId);
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations`, {
|
||||
body: JSON.stringify({}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/${created.id}`, {
|
||||
body: JSON.stringify({ title: "新标题" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { conversation: Conversation };
|
||||
expect(body.conversation.title).toBe("新标题");
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("跨项目更新会话返回 403", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-403");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectA = seedProject(db, "项目A");
|
||||
const projectB = seedProject(db, "项目B");
|
||||
const providerId = seedProvider(db);
|
||||
seedModel(db, providerId);
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectA}/conversations`, {
|
||||
body: JSON.stringify({}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectB}/conversations/${created.id}`, {
|
||||
body: JSON.stringify({ title: "探测" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("不存在的会话返回 404", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-404");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/nonexistent`, {
|
||||
body: JSON.stringify({ title: "探测" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("无效 modelId 返回 400", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-patch-bad-model");
|
||||
try {
|
||||
const db = handle.db;
|
||||
const projectId = seedProject(db);
|
||||
const providerId = seedProvider(db);
|
||||
seedModel(db, providerId);
|
||||
|
||||
const createRes = await createConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations`, {
|
||||
body: JSON.stringify({}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
const created = ((await createRes.json()) as { conversation: Conversation }).conversation;
|
||||
|
||||
const res = await patchConversationViaHandler(
|
||||
new Request(`http://localhost/api/projects/${projectId}/conversations/${created.id}`, {
|
||||
body: JSON.stringify({ modelId: "invalid-model-id" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
}),
|
||||
db,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/projects/:id/conversations/:cid/messages", () => {
|
||||
test("跨项目获取消息返回 403", async () => {
|
||||
const handle = createMigratedMemoryTestDatabase("chat-msg-403");
|
||||
|
||||
Reference in New Issue
Block a user