feat: 支持 AI 设计项目多会话交互

需求:用户可在同一设计项目中新建和切换会话,任务列表保持项目级共享。

实现:
- 增加会话选择、新建入口及本地数据迁移
- Conversation 独立消息、Brief、Quote 与流式状态
- 保留项目任务并修复 Task revision 与 ABA 异步竞态
- 保持 Enter 发送、Shift+Enter 换行及 IME 保护
This commit is contained in:
2026-08-06 18:22:15 +08:00
parent 4980894017
commit 03dae62cf3
18 changed files with 1810 additions and 702 deletions

View File

@@ -11,6 +11,7 @@ import {
IMAGE_WORKSPACE_UNAVAILABLE_CODE,
IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE,
type DesignAssetUploadInput,
type DesignCreateConversationInput,
type DesignConfirmGenerationInput,
type DesignCreateWorkspaceInput,
type DesignRenameWorkspaceInput,
@@ -283,6 +284,7 @@ async function relayWorkspaceEvents(
res: ServerResponse,
ctx: HostApiContext,
workspaceId: string,
conversationId: string,
): Promise<void> {
if (!ctx.imageWorkspace?.openWorkspaceEvents) {
throw new DesignWorkspaceModuleError(
@@ -295,6 +297,7 @@ async function relayWorkspaceEvents(
const afterEventId = Array.isArray(header) ? header[0] : header;
const subscription = await ctx.imageWorkspace.openWorkspaceEvents({
workspaceId,
conversationId,
...(afterEventId ? { afterEventId } : {}),
});
let closed = false;
@@ -399,11 +402,35 @@ export async function handleImageWorkspaceRoutes(
if (segments.length === 3
&& segments[0] === 'workspaces'
&& segments[2] === 'messages'
&& segments[2] === 'conversations'
&& req.method === 'POST') {
const body = await parseJsonBody<Record<string, unknown>>(req);
const input: DesignCreateConversationInput = {
workspaceId: segments[1],
clientConversationId: asString(body.clientConversationId),
title: asString(body.title),
};
sendData(res, await ctx.imageWorkspace.createConversation(input));
return true;
}
if (segments.length === 4
&& segments[0] === 'workspaces'
&& segments[2] === 'conversations'
&& req.method === 'GET') {
sendData(res, await ctx.imageWorkspace.getConversation(segments[1], segments[3]));
return true;
}
if (segments.length === 5
&& segments[0] === 'workspaces'
&& segments[2] === 'conversations'
&& segments[4] === 'messages'
&& req.method === 'POST') {
const body = await parseJsonBody<Record<string, unknown>>(req);
const input: DesignSubmitMessageInput = {
workspaceId: segments[1],
conversationId: segments[3],
clientTurnId: asString(body.clientTurnId),
expectedTurnRevision: asInteger(body.expectedTurnRevision),
message: asString(body.message),
@@ -440,23 +467,26 @@ export async function handleImageWorkspaceRoutes(
return true;
}
if (segments.length === 3
if (segments.length === 5
&& segments[0] === 'workspaces'
&& segments[2] === 'events'
&& segments[2] === 'conversations'
&& segments[4] === 'events'
&& req.method === 'GET') {
await relayWorkspaceEvents(req, res, ctx, segments[1]);
await relayWorkspaceEvents(req, res, ctx, segments[1], segments[3]);
return true;
}
if (segments.length === 5
if (segments.length === 7
&& segments[0] === 'workspaces'
&& segments[2] === 'quotes'
&& segments[4] === 'confirm'
&& segments[2] === 'conversations'
&& segments[4] === 'quotes'
&& segments[6] === 'confirm'
&& req.method === 'POST') {
const body = await parseJsonBody<Record<string, unknown>>(req);
const input: DesignConfirmGenerationInput = {
workspaceId: segments[1],
quoteId: segments[3],
conversationId: segments[3],
quoteId: segments[5],
clientTurnId: asString(body.clientTurnId),
expectedTurnRevision: asInteger(body.expectedTurnRevision),
};