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:
2026-05-31 21:56:50 +08:00
parent 3e1f3b554d
commit f2e3d84fb1
15 changed files with 536 additions and 122 deletions

View File

@@ -2,7 +2,7 @@ import type Database from "bun:sqlite";
import { desc, eq } from "drizzle-orm";
import type { Conversation, Message } from "../../shared/api";
import type { Conversation, Message, UpdateConversationRequest } from "../../shared/api";
import { paginateQuery, wrap } from "./connection";
import { conversations, messages, models } from "./schema";
@@ -150,6 +150,33 @@ export function listMessages(
});
}
export function updateConversation(
raw: Database,
id: string,
data: UpdateConversationRequest,
): { conversation: Conversation } | { error: string; status: number } {
const db = wrap(raw);
const existing = db.select().from(conversations).where(eq(conversations.id, id)).get();
if (!existing) return { error: "会话不存在", status: 404 };
const updates: { modelId?: string; title?: string; updatedAt: string } = { updatedAt: new Date().toISOString() };
if (data.modelId !== undefined) {
const model = db.select().from(models).where(eq(models.id, data.modelId)).get();
if (!model) return { error: "模型不存在", status: 400 };
updates.modelId = data.modelId;
}
if (data.title !== undefined) {
updates.title = data.title;
}
db.update(conversations).set(updates).where(eq(conversations.id, id)).run();
const row = db.select().from(conversations).where(eq(conversations.id, id)).get();
return { conversation: toConversation(row!) };
}
export function updateConversationTimestamp(raw: Database, id: string): void {
const db = wrap(raw);
db.update(conversations).set({ updatedAt: new Date().toISOString() }).where(eq(conversations.id, id)).run();

View File

@@ -0,0 +1,47 @@
import type Database from "bun:sqlite";
import type { RuntimeMode, UpdateConversationRequest } from "../../../shared/api";
import { getConversation, updateConversation } from "../../db/conversations";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export async function handleUpdateConversation(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
const url = new URL(req.url);
const parts = url.pathname.split("/");
const projectId = parts[3];
const conversationId = parts[5];
const validatedProject = validateIdParam(projectId ?? "", mode);
if (validatedProject instanceof Response) return validatedProject;
const validatedConv = validateIdParam(conversationId ?? "", mode);
if (validatedConv instanceof Response) return validatedConv;
const existing = getConversation(db, validatedConv.id);
if ("error" in existing) {
return jsonResponse(createApiError(existing.error, existing.status), { mode, status: existing.status });
}
if (existing.conversation.projectId !== validatedProject.id) {
return jsonResponse(createApiError("会话不属于该项目", 403), { mode, status: 403 });
}
let body: UpdateConversationRequest;
try {
body = (await req.json()) as UpdateConversationRequest;
} catch {
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
if (body.modelId === undefined && body.title === undefined) {
return jsonResponse(createApiError("至少需要传 modelId 或 title", 400), { mode, status: 400 });
}
const result = updateConversation(db, validatedConv.id, body);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
return jsonResponse({ conversation: result.conversation }, { mode });
}

View File

@@ -201,6 +201,14 @@ export function startServer(options: StartServerOptions) {
mode,
logger,
),
PATCH: withErrorHandler(
async (req) => {
const { handleUpdateConversation } = await import("./routes/chat/update");
return handleUpdateConversation(req, db, mode);
},
mode,
logger,
),
},
"/api/projects/:id/conversations/:cid/messages": {
GET: withErrorHandler(