feat: 支持 AI 设计项目多会话交互
需求:用户可在同一设计项目中新建和切换会话,任务列表保持项目级共享。 实现: - 增加会话选择、新建入口及本地数据迁移 - Conversation 独立消息、Brief、Quote 与流式状态 - 保留项目任务并修复 Task revision 与 ABA 异步竞态 - 保持 Enter 发送、Shift+Enter 换行及 IME 保护
This commit is contained in:
@@ -3,7 +3,10 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import type {
|
||||
DesignCapabilities,
|
||||
DesignConversation,
|
||||
DesignConversationSummary,
|
||||
DesignConfirmGenerationInput,
|
||||
DesignCreateConversationInput,
|
||||
DesignCreateWorkspaceInput,
|
||||
DesignGenerationQuote,
|
||||
DesignGenerationTask,
|
||||
@@ -12,13 +15,15 @@ import type {
|
||||
DesignSubmitMessageInput,
|
||||
DesignWorkspace,
|
||||
DesignWorkspaceBootstrap,
|
||||
DesignWorkspaceSummary,
|
||||
} from '../../shared/image-workspace';
|
||||
import { designAssetContentPath } from '../../shared/image-workspace';
|
||||
import { DesignWorkspaceModuleError, type DesignWorkspaceModule } from './module';
|
||||
|
||||
const LOCAL_WORKSPACE_SCHEMA_VERSION = 2;
|
||||
const LOCAL_WORKSPACE_SCHEMA_VERSION = 3;
|
||||
const LOCAL_WORKSPACE_DIRECTORY = 'image-workspace-development';
|
||||
const LOCAL_WORKSPACE_FILE = 'design-workspace-v2.json';
|
||||
const LOCAL_WORKSPACE_FILE = 'design-workspace-v3.json';
|
||||
const LEGACY_LOCAL_WORKSPACE_FILE = 'design-workspace-v2.json';
|
||||
const MAX_PROJECT_NAME_LENGTH = 80;
|
||||
const MAX_MESSAGE_LENGTH = 4_000;
|
||||
|
||||
@@ -29,8 +34,12 @@ const LOCAL_CAPABILITIES: DesignCapabilities = {
|
||||
video: true,
|
||||
};
|
||||
|
||||
type PersistedWorkspace = DesignWorkspace & {
|
||||
type PersistedWorkspace = DesignWorkspaceSummary & {
|
||||
clientWorkspaceId: string;
|
||||
};
|
||||
|
||||
type PersistedConversation = DesignConversation & {
|
||||
clientConversationId: string;
|
||||
requestHashes: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -43,6 +52,27 @@ type PersistedAsset = {
|
||||
type PersistedImageWorkspace = {
|
||||
schemaVersion: typeof LOCAL_WORKSPACE_SCHEMA_VERSION;
|
||||
workspaces: PersistedWorkspace[];
|
||||
conversationsByWorkspaceId: Record<string, PersistedConversation[]>;
|
||||
tasksByWorkspaceId: Record<string, DesignGenerationTask[]>;
|
||||
assetsById: Record<string, PersistedAsset>;
|
||||
};
|
||||
|
||||
type LegacyPersistedWorkspace = {
|
||||
workspaceId: string;
|
||||
clientWorkspaceId: string;
|
||||
title: string;
|
||||
turnRevision: number;
|
||||
viewRevision: number;
|
||||
phase: DesignConversation['phase'];
|
||||
brief: DesignConversation['brief'];
|
||||
messages: DesignConversation['messages'];
|
||||
requestHashes: Record<string, string>;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type LegacyPersistedImageWorkspace = {
|
||||
schemaVersion: 2;
|
||||
workspaces: LegacyPersistedWorkspace[];
|
||||
tasksByWorkspaceId: Record<string, DesignGenerationTask[]>;
|
||||
assetsById: Record<string, PersistedAsset>;
|
||||
};
|
||||
@@ -64,6 +94,7 @@ function createEmptyState(): PersistedImageWorkspace {
|
||||
return {
|
||||
schemaVersion: LOCAL_WORKSPACE_SCHEMA_VERSION,
|
||||
workspaces: [],
|
||||
conversationsByWorkspaceId: {},
|
||||
tasksByWorkspaceId: {},
|
||||
assetsById: {},
|
||||
};
|
||||
@@ -81,6 +112,15 @@ function isPersistedWorkspace(value: unknown): value is PersistedImageWorkspace
|
||||
return isRecord(value)
|
||||
&& value.schemaVersion === LOCAL_WORKSPACE_SCHEMA_VERSION
|
||||
&& Array.isArray(value.workspaces)
|
||||
&& isRecord(value.conversationsByWorkspaceId)
|
||||
&& isRecord(value.tasksByWorkspaceId)
|
||||
&& isRecord(value.assetsById);
|
||||
}
|
||||
|
||||
function isLegacyPersistedWorkspace(value: unknown): value is LegacyPersistedImageWorkspace {
|
||||
return isRecord(value)
|
||||
&& value.schemaVersion === 2
|
||||
&& Array.isArray(value.workspaces)
|
||||
&& isRecord(value.tasksByWorkspaceId)
|
||||
&& isRecord(value.assetsById);
|
||||
}
|
||||
@@ -161,6 +201,7 @@ export function getLocalImageWorkspaceDirectory(userDataDir: string): string {
|
||||
export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
private readonly rootDirectory: string;
|
||||
private readonly stateFile: string;
|
||||
private readonly legacyStateFile: string;
|
||||
private readonly now: () => Date;
|
||||
private readonly createId: () => string;
|
||||
private state: PersistedImageWorkspace | null = null;
|
||||
@@ -173,6 +214,7 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
throw new Error('Invalid local AI design workspace directory');
|
||||
}
|
||||
this.stateFile = join(this.rootDirectory, LOCAL_WORKSPACE_FILE);
|
||||
this.legacyStateFile = join(this.rootDirectory, LEGACY_LOCAL_WORKSPACE_FILE);
|
||||
this.now = options.now ?? (() => new Date());
|
||||
this.createId = options.createId ?? randomUUID;
|
||||
}
|
||||
@@ -191,39 +233,71 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
}
|
||||
|
||||
createWorkspace(input: DesignCreateWorkspaceInput): Promise<DesignWorkspace> {
|
||||
return this.mutate((state) => {
|
||||
return this.mutateWorkspace((state) => {
|
||||
const existing = state.workspaces.find(
|
||||
(workspace) => workspace.clientWorkspaceId === input.clientWorkspaceId,
|
||||
);
|
||||
if (existing) return existing;
|
||||
const timestamp = this.now().toISOString();
|
||||
const workspaceId = `local-workspace-${this.createId()}`;
|
||||
const workspace: PersistedWorkspace = {
|
||||
workspaceId: `local-workspace-${this.createId()}`,
|
||||
workspaceId,
|
||||
clientWorkspaceId: input.clientWorkspaceId,
|
||||
title: validateTitle(input.title),
|
||||
turnRevision: 0,
|
||||
viewRevision: 0,
|
||||
conversationCount: 1,
|
||||
phase: 'shaping',
|
||||
brief: {
|
||||
version: 0,
|
||||
status: 'draft',
|
||||
medium: null,
|
||||
summary: '正在建立作品的视觉方向',
|
||||
ready: false,
|
||||
missingDecision: '作品形式',
|
||||
},
|
||||
messages: [],
|
||||
requestHashes: {},
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
const conversation = this.createEmptyConversation(
|
||||
workspaceId,
|
||||
`local-conversation-${this.createId()}`,
|
||||
`local-client-conversation-${this.createId()}`,
|
||||
'新会话',
|
||||
timestamp,
|
||||
);
|
||||
state.workspaces.unshift(workspace);
|
||||
state.tasksByWorkspaceId[workspace.workspaceId] = [];
|
||||
state.conversationsByWorkspaceId[workspaceId] = [conversation];
|
||||
state.tasksByWorkspaceId[workspaceId] = [];
|
||||
return workspace;
|
||||
});
|
||||
}
|
||||
|
||||
createConversation(input: DesignCreateConversationInput): Promise<DesignConversation> {
|
||||
return this.mutateConversation((state) => {
|
||||
const workspace = this.requireWorkspace(state, input.workspaceId);
|
||||
const conversations = state.conversationsByWorkspaceId[input.workspaceId] ?? [];
|
||||
const existing = conversations.find(
|
||||
(conversation) => conversation.clientConversationId === input.clientConversationId,
|
||||
);
|
||||
if (existing) return existing;
|
||||
const timestamp = this.now().toISOString();
|
||||
const conversation = this.createEmptyConversation(
|
||||
input.workspaceId,
|
||||
`local-conversation-${this.createId()}`,
|
||||
input.clientConversationId,
|
||||
input.title,
|
||||
timestamp,
|
||||
);
|
||||
conversations.unshift(conversation);
|
||||
state.conversationsByWorkspaceId[input.workspaceId] = conversations;
|
||||
workspace.conversationCount = conversations.length;
|
||||
workspace.viewRevision += 1;
|
||||
workspace.updatedAt = timestamp;
|
||||
return conversation;
|
||||
});
|
||||
}
|
||||
|
||||
async getConversation(
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
): Promise<DesignConversation> {
|
||||
const state = await this.load();
|
||||
return clone(this.requireConversation(state, workspaceId, conversationId));
|
||||
}
|
||||
|
||||
renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace> {
|
||||
return this.mutate((state) => {
|
||||
return this.mutateWorkspace((state) => {
|
||||
const workspace = this.requireWorkspace(state, input.workspaceId);
|
||||
workspace.title = validateTitle(input.title);
|
||||
workspace.viewRevision += 1;
|
||||
@@ -234,20 +308,25 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
|
||||
async getWorkspace(workspaceId: string): Promise<DesignWorkspace> {
|
||||
const state = await this.load();
|
||||
return clone(this.requireWorkspace(state, workspaceId));
|
||||
return this.workspaceView(state, this.requireWorkspace(state, workspaceId));
|
||||
}
|
||||
|
||||
submitMessage(input: DesignSubmitMessageInput): Promise<DesignWorkspace> {
|
||||
return this.mutate((state) => {
|
||||
submitMessage(input: DesignSubmitMessageInput): Promise<DesignConversation> {
|
||||
return this.mutateConversation((state) => {
|
||||
const workspace = this.requireWorkspace(state, input.workspaceId);
|
||||
const conversation = this.requireConversation(
|
||||
state,
|
||||
input.workspaceId,
|
||||
input.conversationId,
|
||||
);
|
||||
const message = validateMessage(input.message);
|
||||
const hash = messageHash(input);
|
||||
if (this.isRequestReplay(workspace, input.clientTurnId, hash)) return workspace;
|
||||
this.assertRevision(workspace, input.expectedTurnRevision);
|
||||
this.supersedeQuotes(workspace);
|
||||
if (this.isRequestReplay(conversation, input.clientTurnId, hash)) return conversation;
|
||||
this.assertRevision(conversation, input.expectedTurnRevision);
|
||||
this.supersedeQuotes(conversation);
|
||||
|
||||
const timestamp = this.now().toISOString();
|
||||
const nextRevision = workspace.turnRevision + 1;
|
||||
const nextRevision = conversation.turnRevision + 1;
|
||||
const medium = detectMedium(message);
|
||||
const quote: DesignGenerationQuote = {
|
||||
quoteId: `local-quote-${this.createId()}`,
|
||||
@@ -258,7 +337,7 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
quotedDesignPoints: medium === 'video' ? 2 : 1,
|
||||
expiresAt: new Date(this.now().getTime() + 15 * 60 * 1000).toISOString(),
|
||||
};
|
||||
workspace.messages.push(
|
||||
conversation.messages.push(
|
||||
{
|
||||
id: `local-message-${this.createId()}`,
|
||||
role: 'user',
|
||||
@@ -282,10 +361,9 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
createdAt: timestamp,
|
||||
},
|
||||
);
|
||||
workspace.turnRevision = nextRevision;
|
||||
workspace.viewRevision += 1;
|
||||
workspace.phase = 'awaiting_confirmation';
|
||||
workspace.brief = {
|
||||
conversation.turnRevision = nextRevision;
|
||||
conversation.phase = 'awaiting_confirmation';
|
||||
conversation.brief = {
|
||||
version: nextRevision,
|
||||
status: 'ready',
|
||||
medium,
|
||||
@@ -293,19 +371,27 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
ready: true,
|
||||
missingDecision: null,
|
||||
};
|
||||
conversation.updatedAt = timestamp;
|
||||
conversation.requestHashes[input.clientTurnId] = hash;
|
||||
workspace.phase = conversation.phase;
|
||||
workspace.viewRevision += 1;
|
||||
workspace.updatedAt = timestamp;
|
||||
workspace.requestHashes[input.clientTurnId] = hash;
|
||||
return workspace;
|
||||
return conversation;
|
||||
});
|
||||
}
|
||||
|
||||
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignWorkspace> {
|
||||
return this.mutate((state) => {
|
||||
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignConversation> {
|
||||
return this.mutateConversation((state) => {
|
||||
const workspace = this.requireWorkspace(state, input.workspaceId);
|
||||
const conversation = this.requireConversation(
|
||||
state,
|
||||
input.workspaceId,
|
||||
input.conversationId,
|
||||
);
|
||||
const hash = messageHash(input);
|
||||
if (this.isRequestReplay(workspace, input.clientTurnId, hash)) return workspace;
|
||||
this.assertRevision(workspace, input.expectedTurnRevision);
|
||||
const quote = workspace.messages
|
||||
if (this.isRequestReplay(conversation, input.clientTurnId, hash)) return conversation;
|
||||
this.assertRevision(conversation, input.expectedTurnRevision);
|
||||
const quote = conversation.messages
|
||||
.map((message) => message.generationQuote)
|
||||
.find((candidate) => candidate?.quoteId === input.quoteId);
|
||||
if (!quote || quote.status !== 'active') {
|
||||
@@ -318,8 +404,8 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
|
||||
quote.status = 'consumed';
|
||||
const timestamp = this.now().toISOString();
|
||||
const nextRevision = workspace.turnRevision + 1;
|
||||
workspace.messages.push(
|
||||
const nextRevision = conversation.turnRevision + 1;
|
||||
conversation.messages.push(
|
||||
{
|
||||
id: `local-message-${this.createId()}`,
|
||||
role: 'user',
|
||||
@@ -341,16 +427,19 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
createdAt: timestamp,
|
||||
},
|
||||
);
|
||||
workspace.turnRevision = nextRevision;
|
||||
conversation.turnRevision = nextRevision;
|
||||
conversation.phase = 'shaping';
|
||||
conversation.brief = { ...conversation.brief, status: 'confirmed' };
|
||||
conversation.updatedAt = timestamp;
|
||||
conversation.requestHashes[input.clientTurnId] = hash;
|
||||
workspace.phase = conversation.phase;
|
||||
workspace.viewRevision += 1;
|
||||
workspace.phase = 'shaping';
|
||||
workspace.brief = { ...workspace.brief, status: 'confirmed' };
|
||||
workspace.updatedAt = timestamp;
|
||||
workspace.requestHashes[input.clientTurnId] = hash;
|
||||
state.tasksByWorkspaceId[workspace.workspaceId] ??= [];
|
||||
state.tasksByWorkspaceId[workspace.workspaceId].unshift({
|
||||
taskId: `local-task-${this.createId()}`,
|
||||
workspaceId: workspace.workspaceId,
|
||||
conversationId: conversation.conversationId,
|
||||
medium: quote.medium,
|
||||
status: 'queued',
|
||||
briefVersion: quote.briefVersion,
|
||||
@@ -362,10 +451,39 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
return workspace;
|
||||
return conversation;
|
||||
});
|
||||
}
|
||||
|
||||
private createEmptyConversation(
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
clientConversationId: string,
|
||||
title: string,
|
||||
timestamp: string,
|
||||
): PersistedConversation {
|
||||
return {
|
||||
conversationId,
|
||||
clientConversationId,
|
||||
workspaceId,
|
||||
title: validateTitle(title),
|
||||
turnRevision: 0,
|
||||
phase: 'shaping',
|
||||
brief: {
|
||||
version: 0,
|
||||
status: 'draft',
|
||||
medium: null,
|
||||
summary: '正在建立作品的视觉方向',
|
||||
ready: false,
|
||||
missingDecision: '作品形式',
|
||||
},
|
||||
messages: [],
|
||||
requestHashes: {},
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
listTasks(workspaceId: string): Promise<DesignGenerationTask[]> {
|
||||
return this.enqueue(async () => {
|
||||
const state = await this.load();
|
||||
@@ -443,14 +561,25 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
});
|
||||
}
|
||||
|
||||
private mutate(
|
||||
private mutateWorkspace(
|
||||
operation: (state: PersistedImageWorkspace) => PersistedWorkspace,
|
||||
): Promise<DesignWorkspace> {
|
||||
return this.enqueue(async () => {
|
||||
const state = await this.load();
|
||||
const workspace = operation(state);
|
||||
await this.persist(state);
|
||||
return clone(workspace);
|
||||
return this.workspaceView(state, workspace);
|
||||
});
|
||||
}
|
||||
|
||||
private mutateConversation(
|
||||
operation: (state: PersistedImageWorkspace) => PersistedConversation,
|
||||
): Promise<DesignConversation> {
|
||||
return this.enqueue(async () => {
|
||||
const state = await this.load();
|
||||
const conversation = operation(state);
|
||||
await this.persist(state);
|
||||
return this.conversationView(conversation);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -474,7 +603,7 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
this.state = parsed;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
this.state = createEmptyState();
|
||||
this.state = await this.loadLegacyState();
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -482,6 +611,62 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
private async loadLegacyState(): Promise<PersistedImageWorkspace> {
|
||||
try {
|
||||
const parsed = JSON.parse(await readFile(this.legacyStateFile, 'utf8')) as unknown;
|
||||
if (!isLegacyPersistedWorkspace(parsed)) {
|
||||
throw new LocalImageWorkspaceError(
|
||||
500,
|
||||
'IMAGE_WORKSPACE_LOCAL_DATA_INVALID',
|
||||
'AI 设计本地数据暂时无法读取',
|
||||
);
|
||||
}
|
||||
const migrated = this.migrateLegacyState(parsed);
|
||||
await this.persist(migrated);
|
||||
return migrated;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return createEmptyState();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private migrateLegacyState(legacy: LegacyPersistedImageWorkspace): PersistedImageWorkspace {
|
||||
const migrated = createEmptyState();
|
||||
migrated.assetsById = clone(legacy.assetsById);
|
||||
migrated.tasksByWorkspaceId = clone(legacy.tasksByWorkspaceId);
|
||||
for (const legacyWorkspace of legacy.workspaces) {
|
||||
const conversationId = `local-conversation-${this.createId()}`;
|
||||
const createdAt = legacyWorkspace.messages[0]?.createdAt ?? legacyWorkspace.updatedAt;
|
||||
const conversation: PersistedConversation = {
|
||||
conversationId,
|
||||
clientConversationId: `migrated-${legacyWorkspace.workspaceId}`,
|
||||
workspaceId: legacyWorkspace.workspaceId,
|
||||
title: '历史会话',
|
||||
turnRevision: legacyWorkspace.turnRevision,
|
||||
phase: legacyWorkspace.phase,
|
||||
brief: clone(legacyWorkspace.brief),
|
||||
messages: clone(legacyWorkspace.messages),
|
||||
requestHashes: clone(legacyWorkspace.requestHashes),
|
||||
createdAt,
|
||||
updatedAt: legacyWorkspace.updatedAt,
|
||||
};
|
||||
migrated.workspaces.push({
|
||||
workspaceId: legacyWorkspace.workspaceId,
|
||||
clientWorkspaceId: legacyWorkspace.clientWorkspaceId,
|
||||
title: legacyWorkspace.title,
|
||||
viewRevision: legacyWorkspace.viewRevision,
|
||||
conversationCount: 1,
|
||||
phase: legacyWorkspace.phase,
|
||||
updatedAt: legacyWorkspace.updatedAt,
|
||||
});
|
||||
migrated.conversationsByWorkspaceId[legacyWorkspace.workspaceId] = [conversation];
|
||||
migrated.tasksByWorkspaceId[legacyWorkspace.workspaceId] = (
|
||||
migrated.tasksByWorkspaceId[legacyWorkspace.workspaceId] ?? []
|
||||
).map((task) => ({ ...task, conversationId }));
|
||||
}
|
||||
return migrated;
|
||||
}
|
||||
|
||||
private async persist(state: PersistedImageWorkspace): Promise<void> {
|
||||
await mkdir(this.rootDirectory, { recursive: true });
|
||||
const temporaryFile = `${this.stateFile}.${process.pid}.tmp`;
|
||||
@@ -508,22 +693,39 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private assertRevision(workspace: PersistedWorkspace, expectedRevision: number): void {
|
||||
if (workspace.turnRevision !== expectedRevision) {
|
||||
private requireConversation(
|
||||
state: PersistedImageWorkspace,
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
): PersistedConversation {
|
||||
this.requireWorkspace(state, workspaceId);
|
||||
const conversation = (state.conversationsByWorkspaceId[workspaceId] ?? [])
|
||||
.find((item) => item.conversationId === conversationId);
|
||||
if (!conversation) {
|
||||
throw new LocalImageWorkspaceError(404, 'conversation_not_found', '设计会话不存在');
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private assertRevision(
|
||||
conversation: PersistedConversation,
|
||||
expectedRevision: number,
|
||||
): void {
|
||||
if (conversation.turnRevision !== expectedRevision) {
|
||||
throw new LocalImageWorkspaceError(
|
||||
409,
|
||||
'workspace_revision_conflict',
|
||||
'设计项目已在其他位置更新,请刷新后重试',
|
||||
'conversation_revision_conflict',
|
||||
'设计会话已在其他位置更新,请刷新后重试',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private isRequestReplay(
|
||||
workspace: PersistedWorkspace,
|
||||
conversation: PersistedConversation,
|
||||
clientTurnId: string,
|
||||
hash: string,
|
||||
): boolean {
|
||||
const existing = workspace.requestHashes[clientTurnId];
|
||||
const existing = conversation.requestHashes[clientTurnId];
|
||||
if (!existing) return false;
|
||||
if (existing !== hash) {
|
||||
throw new LocalImageWorkspaceError(
|
||||
@@ -535,8 +737,8 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
return true;
|
||||
}
|
||||
|
||||
private supersedeQuotes(workspace: PersistedWorkspace): void {
|
||||
for (const message of workspace.messages) {
|
||||
private supersedeQuotes(conversation: PersistedConversation): void {
|
||||
for (const message of conversation.messages) {
|
||||
if (message.generationQuote?.status === 'active') {
|
||||
message.generationQuote.status = 'superseded';
|
||||
}
|
||||
@@ -546,7 +748,43 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
private bootstrapView(state: PersistedImageWorkspace): DesignWorkspaceBootstrap {
|
||||
return {
|
||||
capabilities: clone(LOCAL_CAPABILITIES),
|
||||
workspaces: state.workspaces.map(({ messages: _messages, clientWorkspaceId: _clientId, requestHashes: _requests, ...summary }) => clone(summary)),
|
||||
workspaces: state.workspaces.map(({ clientWorkspaceId: _clientId, ...summary }) => (
|
||||
clone(summary)
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
private workspaceView(
|
||||
state: PersistedImageWorkspace,
|
||||
workspace: PersistedWorkspace,
|
||||
): DesignWorkspace {
|
||||
const { clientWorkspaceId: _clientId, ...summary } = workspace;
|
||||
return clone({
|
||||
...summary,
|
||||
conversationCount: state.conversationsByWorkspaceId[workspace.workspaceId]?.length ?? 0,
|
||||
conversations: (state.conversationsByWorkspaceId[workspace.workspaceId] ?? [])
|
||||
.map((conversation) => this.conversationSummaryView(conversation)),
|
||||
});
|
||||
}
|
||||
|
||||
private conversationSummaryView(
|
||||
conversation: PersistedConversation,
|
||||
): DesignConversationSummary {
|
||||
const {
|
||||
clientConversationId: _clientId,
|
||||
requestHashes: _requestHashes,
|
||||
messages: _messages,
|
||||
...summary
|
||||
} = conversation;
|
||||
return clone(summary);
|
||||
}
|
||||
|
||||
private conversationView(conversation: PersistedConversation): DesignConversation {
|
||||
const {
|
||||
clientConversationId: _clientId,
|
||||
requestHashes: _requestHashes,
|
||||
...view
|
||||
} = conversation;
|
||||
return clone(view);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user