合并 AI 设计多会话客户端
整合 Enter 发送与多会话交互,保留服务端持久 Conversation Session,并补齐迁移、回归、Electron E2E 与 canonical 文档。
This commit is contained in:
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import type {
|
||||
DesignAsset,
|
||||
DesignAssetUploadInput,
|
||||
DesignCapabilities,
|
||||
DesignConversation,
|
||||
DesignCreateConversationInput,
|
||||
DesignConfirmGenerationInput,
|
||||
DesignCreateWorkspaceInput,
|
||||
DesignGenerationTask,
|
||||
@@ -19,6 +21,7 @@ export type DesignWorkspaceEventSubscription = {
|
||||
|
||||
export type DesignWorkspaceEventSubscriptionInput = {
|
||||
workspaceId: string;
|
||||
conversationId: string;
|
||||
afterEventId?: string;
|
||||
};
|
||||
|
||||
@@ -45,8 +48,10 @@ export interface DesignWorkspaceModule {
|
||||
createWorkspace(input: DesignCreateWorkspaceInput): Promise<DesignWorkspace>;
|
||||
renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace>;
|
||||
getWorkspace(workspaceId: string): Promise<DesignWorkspace>;
|
||||
submitMessage(input: DesignSubmitMessageInput): Promise<DesignWorkspace>;
|
||||
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignWorkspace>;
|
||||
createConversation(input: DesignCreateConversationInput): Promise<DesignConversation>;
|
||||
getConversation(workspaceId: string, conversationId: string): Promise<DesignConversation>;
|
||||
submitMessage(input: DesignSubmitMessageInput): Promise<DesignConversation>;
|
||||
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignConversation>;
|
||||
listTasks(workspaceId: string): Promise<DesignGenerationTask[]>;
|
||||
uploadAsset?(input: DesignAssetUploadInput): Promise<DesignAsset>;
|
||||
openWorkspaceEvents?(
|
||||
|
||||
@@ -4,11 +4,14 @@ import type {
|
||||
DesignAssistantDeltaEvent,
|
||||
DesignBrief,
|
||||
DesignCapabilities,
|
||||
DesignConversation,
|
||||
DesignConversationSnapshotEvent,
|
||||
DesignConversationSummary,
|
||||
DesignConfirmGenerationInput,
|
||||
DesignCreateConversationInput,
|
||||
DesignCreateWorkspaceInput,
|
||||
DesignGenerationQuote,
|
||||
DesignGenerationTask,
|
||||
DesignGenerationTasksSnapshotEvent,
|
||||
DesignGenerationTaskUpdatedEvent,
|
||||
DesignMessage,
|
||||
DesignRenameWorkspaceInput,
|
||||
@@ -18,7 +21,6 @@ import type {
|
||||
DesignWorkspaceEvent,
|
||||
DesignWorkspaceSummary,
|
||||
} from '../../shared/image-workspace';
|
||||
import { createHash } from 'node:crypto';
|
||||
import WebSocket from 'ws';
|
||||
import { designAssetContentPath } from '../../shared/image-workspace';
|
||||
import { WORKS_SQUARE_CONFIG } from '../api/works-config';
|
||||
@@ -26,7 +28,6 @@ import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||
import { getValidWorksSquareAccessToken } from '../services/works-square-session';
|
||||
import {
|
||||
DesignWorkspaceModuleError,
|
||||
type CloseEventSessionsOptions,
|
||||
type DesignWorkspaceEventSubscription,
|
||||
type DesignWorkspaceEventSubscriptionInput,
|
||||
type DesignWorkspaceModule,
|
||||
@@ -64,17 +65,36 @@ type ServerMessage = {
|
||||
type ServerWorkspaceSummary = {
|
||||
workspace_id: string;
|
||||
title: string;
|
||||
turn_revision: number;
|
||||
view_revision: number;
|
||||
conversation_count?: number;
|
||||
phase: DesignWorkspaceSummary['phase'];
|
||||
brief: ServerBrief;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type ServerWorkspace = ServerWorkspaceSummary & {
|
||||
type ServerConversationSummary = {
|
||||
conversation_id: string;
|
||||
workspace_id: string;
|
||||
agent_session_id: string | null;
|
||||
title: string;
|
||||
turn_revision: number;
|
||||
phase: DesignConversationSummary['phase'];
|
||||
brief: ServerBrief;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type ServerConversation = ServerConversationSummary & {
|
||||
messages: ServerMessage[];
|
||||
};
|
||||
|
||||
type ServerWorkspace = ServerWorkspaceSummary & {
|
||||
conversations?: ServerConversationSummary[];
|
||||
};
|
||||
|
||||
type ServerWorkspaceCreation = ServerWorkspace & {
|
||||
initial_conversation?: ServerConversation;
|
||||
};
|
||||
|
||||
type ServerAsset = {
|
||||
asset_id: string;
|
||||
media_type: DesignAsset['mediaType'];
|
||||
@@ -88,6 +108,7 @@ type ServerAsset = {
|
||||
type ServerTask = {
|
||||
task_id: string;
|
||||
workspace_id: string;
|
||||
conversation_id?: string | null;
|
||||
medium: DesignGenerationTask['medium'];
|
||||
status: DesignGenerationTask['status'];
|
||||
brief_version: number;
|
||||
@@ -108,17 +129,7 @@ type ServerErrorDetail = {
|
||||
type WorksSquareDesignWorkspaceOptions = {
|
||||
apiBaseUrl?: string;
|
||||
fetchImpl?: typeof fetch;
|
||||
clientInstanceId?: string;
|
||||
webSocketFactory?: AgentWebSocketFactory;
|
||||
eventSessionClientIdStore?: {
|
||||
getOrCreate(workspaceId: string): Promise<string>;
|
||||
rotate(workspaceId: string): Promise<string>;
|
||||
};
|
||||
};
|
||||
|
||||
type ServerAgentSession = {
|
||||
session_id: string;
|
||||
status: 'active' | 'closed';
|
||||
};
|
||||
|
||||
type ServerAgentStreamTicket = {
|
||||
@@ -155,6 +166,8 @@ type ServerAgentRun = {
|
||||
|
||||
type AgentDesignTurnSubmission = {
|
||||
workspaceId: string;
|
||||
conversationId: string;
|
||||
agentSessionId: string;
|
||||
clientTurnId: string;
|
||||
expectedTurnRevision: number;
|
||||
message: string;
|
||||
@@ -233,19 +246,42 @@ function mapWorkspaceSummary(workspace: ServerWorkspaceSummary): DesignWorkspace
|
||||
return {
|
||||
workspaceId: workspace.workspace_id,
|
||||
title: workspace.title,
|
||||
turnRevision: workspace.turn_revision,
|
||||
viewRevision: workspace.view_revision,
|
||||
conversationCount: workspace.conversation_count ?? 0,
|
||||
phase: workspace.phase,
|
||||
brief: mapBrief(workspace.brief),
|
||||
updatedAt: workspace.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
function mapWorkspace(workspace: ServerWorkspace): DesignWorkspace {
|
||||
const conversations = workspace.conversations ?? [];
|
||||
return {
|
||||
...mapWorkspaceSummary(workspace),
|
||||
messages: workspace.messages.map((message, index) => ({
|
||||
id: `${workspace.workspace_id}:${message.turn_revision}:${message.role}:${index}`,
|
||||
conversationCount: workspace.conversation_count ?? conversations.length,
|
||||
conversations: conversations.map(mapConversationSummary),
|
||||
};
|
||||
}
|
||||
|
||||
function mapConversationSummary(
|
||||
conversation: ServerConversationSummary,
|
||||
): DesignConversationSummary {
|
||||
return {
|
||||
conversationId: conversation.conversation_id,
|
||||
workspaceId: conversation.workspace_id,
|
||||
title: conversation.title,
|
||||
turnRevision: conversation.turn_revision,
|
||||
phase: conversation.phase,
|
||||
brief: mapBrief(conversation.brief),
|
||||
createdAt: conversation.created_at,
|
||||
updatedAt: conversation.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
function mapConversation(conversation: ServerConversation): DesignConversation {
|
||||
return {
|
||||
...mapConversationSummary(conversation),
|
||||
messages: conversation.messages.map((message, index) => ({
|
||||
id: `${conversation.conversation_id}:${message.turn_revision}:${message.role}:${index}`,
|
||||
role: message.role,
|
||||
kind: message.kind,
|
||||
text: message.text,
|
||||
@@ -261,6 +297,7 @@ function mapTask(task: ServerTask): DesignGenerationTask {
|
||||
return {
|
||||
taskId: task.task_id,
|
||||
workspaceId: task.workspace_id,
|
||||
conversationId: task.conversation_id ?? null,
|
||||
medium: task.medium,
|
||||
status: task.status,
|
||||
briefVersion: task.brief_version,
|
||||
@@ -288,24 +325,14 @@ function mapAsset(workspaceId: string, asset: ServerAsset): DesignAsset {
|
||||
};
|
||||
}
|
||||
|
||||
function createClientId(prefix: string): string {
|
||||
const id = globalThis.crypto?.randomUUID?.()
|
||||
?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
return `${prefix}-${id}`;
|
||||
}
|
||||
|
||||
function stableWorkspaceSessionClientId(clientInstanceId: string, workspaceId: string): string {
|
||||
const digest = createHash('sha256')
|
||||
.update(`${clientInstanceId}\0${workspaceId}`)
|
||||
.digest('hex');
|
||||
return `design-stream-${digest}`;
|
||||
}
|
||||
|
||||
function isServerTask(value: unknown): value is ServerTask {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const task = value as Record<string, unknown>;
|
||||
return typeof task.task_id === 'string'
|
||||
&& typeof task.workspace_id === 'string'
|
||||
&& (task.conversation_id === undefined
|
||||
|| task.conversation_id === null
|
||||
|| typeof task.conversation_id === 'string')
|
||||
&& (task.medium === 'image' || task.medium === 'video')
|
||||
&& ['queued', 'running', 'succeeded', 'failed', 'cancelled'].includes(String(task.status))
|
||||
&& typeof task.brief_version === 'number'
|
||||
@@ -315,17 +342,18 @@ function isServerTask(value: unknown): value is ServerTask {
|
||||
&& typeof task.updated_at === 'string';
|
||||
}
|
||||
|
||||
function isServerWorkspace(value: unknown): value is ServerWorkspace {
|
||||
function isServerConversation(value: unknown): value is ServerConversation {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const workspace = value as Record<string, unknown>;
|
||||
const brief = workspace.brief as Record<string, unknown> | null;
|
||||
return typeof workspace.workspace_id === 'string'
|
||||
&& typeof workspace.title === 'string'
|
||||
&& Number.isInteger(workspace.turn_revision)
|
||||
&& Number(workspace.turn_revision) >= 0
|
||||
&& Number.isInteger(workspace.view_revision)
|
||||
&& Number(workspace.view_revision) >= 0
|
||||
&& ['shaping', 'awaiting_confirmation', 'blocked'].includes(String(workspace.phase))
|
||||
const conversation = value as Record<string, unknown>;
|
||||
const brief = conversation.brief as Record<string, unknown> | null;
|
||||
return typeof conversation.conversation_id === 'string'
|
||||
&& typeof conversation.workspace_id === 'string'
|
||||
&& (conversation.agent_session_id === null
|
||||
|| typeof conversation.agent_session_id === 'string')
|
||||
&& typeof conversation.title === 'string'
|
||||
&& Number.isInteger(conversation.turn_revision)
|
||||
&& Number(conversation.turn_revision) >= 0
|
||||
&& ['shaping', 'awaiting_confirmation', 'blocked'].includes(String(conversation.phase))
|
||||
&& Boolean(brief)
|
||||
&& Number.isInteger(brief?.version)
|
||||
&& ['draft', 'ready', 'confirmed'].includes(String(brief?.status))
|
||||
@@ -333,8 +361,8 @@ function isServerWorkspace(value: unknown): value is ServerWorkspace {
|
||||
&& typeof brief?.summary === 'string'
|
||||
&& typeof brief?.ready === 'boolean'
|
||||
&& (brief?.missing_decision === null || typeof brief?.missing_decision === 'string')
|
||||
&& Array.isArray(workspace.messages)
|
||||
&& workspace.messages.every((message) => {
|
||||
&& Array.isArray(conversation.messages)
|
||||
&& conversation.messages.every((message) => {
|
||||
if (!message || typeof message !== 'object' || Array.isArray(message)) return false;
|
||||
const item = message as Record<string, unknown>;
|
||||
return (item.role === 'user' || item.role === 'assistant')
|
||||
@@ -347,13 +375,15 @@ function isServerWorkspace(value: unknown): value is ServerWorkspace {
|
||||
&& Number(item.turn_revision) >= 0
|
||||
&& typeof item.created_at === 'string';
|
||||
})
|
||||
&& typeof workspace.updated_at === 'string';
|
||||
&& typeof conversation.created_at === 'string'
|
||||
&& typeof conversation.updated_at === 'string';
|
||||
}
|
||||
|
||||
function normalizeWorkspaceEvent(
|
||||
value: unknown,
|
||||
sessionId: string,
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
): DesignWorkspaceEvent | null {
|
||||
try {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
@@ -371,6 +401,7 @@ function normalizeWorkspaceEvent(
|
||||
const payload = event.payload as Record<string, unknown>;
|
||||
if (event.type === 'design.assistant.delta') {
|
||||
if (payload.workspace_id !== workspaceId
|
||||
|| payload.conversation_id !== conversationId
|
||||
|| typeof payload.client_turn_id !== 'string'
|
||||
|| payload.client_turn_id.length < 1
|
||||
|| payload.client_turn_id.length > 128
|
||||
@@ -387,6 +418,7 @@ function normalizeWorkspaceEvent(
|
||||
id: `${sessionId}:${event.sequence}`,
|
||||
type: 'design.assistant.delta',
|
||||
workspaceId,
|
||||
conversationId,
|
||||
clientTurnId: payload.client_turn_id,
|
||||
turnRevision: Number(payload.turn_revision),
|
||||
chunkIndex: Number(payload.chunk_index),
|
||||
@@ -411,15 +443,19 @@ function normalizeWorkspaceEvent(
|
||||
} satisfies DesignGenerationTaskUpdatedEvent;
|
||||
}
|
||||
|
||||
if (event.type !== 'design.workspace.updated'
|
||||
|| !isServerWorkspace(payload.workspace)
|
||||
if ((event.type !== 'design.conversation.updated'
|
||||
&& event.type !== 'design.workspace.updated')
|
||||
|| payload.workspace_id !== workspaceId
|
||||
|| payload.conversation_id !== conversationId
|
||||
|| !isServerConversation(payload.conversation)
|
||||
|| !Array.isArray(payload.generation_tasks)) {
|
||||
return null;
|
||||
}
|
||||
const workspace = payload.workspace;
|
||||
if (workspace.workspace_id !== workspaceId
|
||||
|| !Number.isInteger(workspace.view_revision)
|
||||
|| Number(workspace.view_revision) < 0
|
||||
const conversation = payload.conversation;
|
||||
if (conversation.workspace_id !== workspaceId
|
||||
|| conversation.conversation_id !== conversationId
|
||||
|| !Number.isInteger(payload.workspace_view_revision)
|
||||
|| Number(payload.workspace_view_revision) < 0
|
||||
|| !payload.generation_tasks.every(
|
||||
(task) => isServerTask(task) && task.workspace_id === workspaceId,
|
||||
)) {
|
||||
@@ -427,12 +463,13 @@ function normalizeWorkspaceEvent(
|
||||
}
|
||||
return {
|
||||
id: `${sessionId}:${event.sequence}`,
|
||||
type: 'design.generation_tasks.snapshot',
|
||||
type: 'design.conversation.snapshot',
|
||||
workspaceId,
|
||||
workspaceViewRevision: Number(workspace.view_revision),
|
||||
workspace: mapWorkspace(workspace),
|
||||
conversationId,
|
||||
workspaceViewRevision: Number(payload.workspace_view_revision),
|
||||
conversation: mapConversation(conversation),
|
||||
generationTasks: payload.generation_tasks.map((task) => mapTask(task as ServerTask)),
|
||||
} satisfies DesignGenerationTasksSnapshotEvent;
|
||||
} satisfies DesignConversationSnapshotEvent;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -669,15 +706,17 @@ function agentRunErrorStatus(code: string): number {
|
||||
return 502;
|
||||
}
|
||||
|
||||
function isStaleAgentSessionError(error: unknown): error is DesignWorkspaceModuleError {
|
||||
return error instanceof DesignWorkspaceModuleError
|
||||
&& (error.code === 'agent_session_not_found'
|
||||
|| error.code === 'agent_session_closed');
|
||||
}
|
||||
|
||||
export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
private readonly apiBaseUrl: string;
|
||||
private readonly fetchImpl: typeof fetch;
|
||||
private readonly webSocketFactory: AgentWebSocketFactory;
|
||||
private readonly eventSessionClientIdStore: NonNullable<
|
||||
WorksSquareDesignWorkspaceOptions['eventSessionClientIdStore']
|
||||
>;
|
||||
private readonly eventSessions = new Map<string, Promise<ServerAgentSession>>();
|
||||
private readonly eventSessionClientIds = new Map<string, string>();
|
||||
private readonly conversationSessionIds = new Map<string, string>();
|
||||
private readonly eventSubscriptionClosers = new Map<string, Set<() => void>>();
|
||||
private readonly activeRunEventStreams = new Map<string, number>();
|
||||
private readonly terminalAgentRuns = new Map<string, ServerAgentRun>();
|
||||
@@ -689,13 +728,6 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
this.apiBaseUrl = (options.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, '');
|
||||
this.fetchImpl = options.fetchImpl ?? proxyAwareFetch;
|
||||
this.webSocketFactory = options.webSocketFactory ?? defaultAgentWebSocketFactory;
|
||||
const clientInstanceId = options.clientInstanceId?.trim() || createClientId('process');
|
||||
this.eventSessionClientIdStore = options.eventSessionClientIdStore ?? {
|
||||
getOrCreate: async (workspaceId) => (
|
||||
stableWorkspaceSessionClientId(clientInstanceId, workspaceId)
|
||||
),
|
||||
rotate: async () => createClientId('design-stream-rotated'),
|
||||
};
|
||||
}
|
||||
|
||||
async bootstrap(): Promise<DesignWorkspaceBootstrap> {
|
||||
@@ -715,14 +747,15 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
}
|
||||
|
||||
async createWorkspace(input: DesignCreateWorkspaceInput): Promise<DesignWorkspace> {
|
||||
const workspace = await this.requestJson<ServerWorkspace>('/api/design/workspaces', {
|
||||
const workspace = await this.requestJson<ServerWorkspaceCreation>('/api/design/workspaces', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_workspace_id: input.clientWorkspaceId,
|
||||
title: input.title,
|
||||
}),
|
||||
});
|
||||
return mapWorkspace(workspace);
|
||||
if (workspace.initial_conversation) this.rememberConversation(workspace.initial_conversation);
|
||||
return this.getWorkspace(workspace.workspace_id);
|
||||
}
|
||||
|
||||
async renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace> {
|
||||
@@ -733,19 +766,53 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
body: JSON.stringify({ title: input.title }),
|
||||
},
|
||||
);
|
||||
return mapWorkspace(workspace);
|
||||
return this.getWorkspace(workspace.workspace_id);
|
||||
}
|
||||
|
||||
async getWorkspace(workspaceId: string): Promise<DesignWorkspace> {
|
||||
const workspace = await this.requestJson<ServerWorkspace>(
|
||||
`/api/design/workspaces/${encodeURIComponent(workspaceId)}`,
|
||||
);
|
||||
return mapWorkspace(workspace);
|
||||
const [workspace, conversations] = await Promise.all([
|
||||
this.requestJson<ServerWorkspace>(
|
||||
`/api/design/workspaces/${encodeURIComponent(workspaceId)}`,
|
||||
),
|
||||
this.requestJson<ServerConversationSummary[]>(
|
||||
`/api/design/workspaces/${encodeURIComponent(workspaceId)}/conversations?limit=100&offset=0`,
|
||||
),
|
||||
]);
|
||||
return mapWorkspace({ ...workspace, conversations });
|
||||
}
|
||||
|
||||
async submitMessage(input: DesignSubmitMessageInput): Promise<DesignWorkspace> {
|
||||
async createConversation(input: DesignCreateConversationInput): Promise<DesignConversation> {
|
||||
const conversation = await this.requestJson<ServerConversation>(
|
||||
`/api/design/workspaces/${encodeURIComponent(input.workspaceId)}/conversations`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_conversation_id: input.clientConversationId,
|
||||
title: input.title,
|
||||
}),
|
||||
},
|
||||
);
|
||||
this.rememberConversation(conversation);
|
||||
return mapConversation(conversation);
|
||||
}
|
||||
|
||||
async getConversation(
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
): Promise<DesignConversation> {
|
||||
const conversation = await this.requestJson<ServerConversation>(
|
||||
`/api/design/workspaces/${encodeURIComponent(workspaceId)}/conversations/${encodeURIComponent(conversationId)}`,
|
||||
);
|
||||
this.rememberConversation(conversation);
|
||||
return mapConversation(conversation);
|
||||
}
|
||||
|
||||
async submitMessage(input: DesignSubmitMessageInput): Promise<DesignConversation> {
|
||||
const agentSessionId = await this.getAgentSessionId(input.workspaceId, input.conversationId);
|
||||
return this.executeAgentTurn({
|
||||
workspaceId: input.workspaceId,
|
||||
conversationId: input.conversationId,
|
||||
agentSessionId,
|
||||
clientTurnId: input.clientTurnId,
|
||||
expectedTurnRevision: input.expectedTurnRevision,
|
||||
message: input.message,
|
||||
@@ -754,9 +821,12 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
});
|
||||
}
|
||||
|
||||
async confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignWorkspace> {
|
||||
async confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignConversation> {
|
||||
const agentSessionId = await this.getAgentSessionId(input.workspaceId, input.conversationId);
|
||||
return this.executeAgentTurn({
|
||||
workspaceId: input.workspaceId,
|
||||
conversationId: input.conversationId,
|
||||
agentSessionId,
|
||||
clientTurnId: input.clientTurnId,
|
||||
expectedTurnRevision: input.expectedTurnRevision,
|
||||
message: '确认生成',
|
||||
@@ -768,22 +838,23 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
});
|
||||
}
|
||||
|
||||
private async executeAgentTurn(input: AgentDesignTurnSubmission): Promise<DesignWorkspace> {
|
||||
let session = await this.ensureEventSession(input.workspaceId);
|
||||
let command: ServerAgentCommand;
|
||||
private async executeAgentTurn(input: AgentDesignTurnSubmission): Promise<DesignConversation> {
|
||||
try {
|
||||
command = await this.submitTurnCommand(session.session_id, input);
|
||||
return await this.executeAgentTurnOnce(input);
|
||||
} catch (error) {
|
||||
if (!(error instanceof DesignWorkspaceModuleError)
|
||||
|| (error.code !== 'agent_session_not_found'
|
||||
&& error.code !== 'agent_session_closed')) {
|
||||
throw error;
|
||||
}
|
||||
await this.invalidateEventSession(input.workspaceId);
|
||||
session = await this.ensureEventSession(input.workspaceId);
|
||||
command = await this.submitTurnCommand(session.session_id, input);
|
||||
if (!isStaleAgentSessionError(error)) throw error;
|
||||
this.forgetConversation(input.workspaceId, input.conversationId);
|
||||
const agentSessionId = await this.getAgentSessionId(
|
||||
input.workspaceId,
|
||||
input.conversationId,
|
||||
);
|
||||
return this.executeAgentTurnOnce({ ...input, agentSessionId });
|
||||
}
|
||||
const run = await this.waitForAgentRun(session.session_id, command.run_id);
|
||||
}
|
||||
|
||||
private async executeAgentTurnOnce(input: AgentDesignTurnSubmission): Promise<DesignConversation> {
|
||||
const command = await this.submitTurnCommand(input.agentSessionId, input);
|
||||
const run = await this.waitForAgentRun(input.agentSessionId, command.run_id);
|
||||
if (run.status !== 'succeeded') {
|
||||
const code = run.error?.code ?? (
|
||||
run.status === 'cancelled' ? 'design_agent_run_cancelled' : 'design_agent_run_failed'
|
||||
@@ -799,7 +870,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
userFacingErrorMessage(code, fallback),
|
||||
);
|
||||
}
|
||||
return this.getWorkspace(input.workspaceId);
|
||||
return this.getConversation(input.workspaceId, input.conversationId);
|
||||
}
|
||||
|
||||
private submitTurnCommand(
|
||||
@@ -936,18 +1007,24 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
async openWorkspaceEvents(
|
||||
input: DesignWorkspaceEventSubscriptionInput,
|
||||
): Promise<DesignWorkspaceEventSubscription> {
|
||||
let session = await this.ensureEventSession(input.workspaceId);
|
||||
if (!this.eventSessionsEnabled) {
|
||||
throw new DesignWorkspaceModuleError(
|
||||
503,
|
||||
'DESIGN_EVENT_STREAM_PAUSED',
|
||||
'AI 设计任务状态流已暂停',
|
||||
);
|
||||
}
|
||||
let sessionId = await this.getAgentSessionId(input.workspaceId, input.conversationId);
|
||||
let ticket: ServerAgentStreamTicket;
|
||||
try {
|
||||
ticket = await this.createEventStreamTicket(session.session_id);
|
||||
ticket = await this.createEventStreamTicket(sessionId);
|
||||
} catch (error) {
|
||||
if (!(error instanceof DesignWorkspaceModuleError)
|
||||
|| (error.status !== 404 && error.status !== 409)) {
|
||||
if (!isStaleAgentSessionError(error)) {
|
||||
throw error;
|
||||
}
|
||||
await this.invalidateEventSession(input.workspaceId);
|
||||
session = await this.ensureEventSession(input.workspaceId);
|
||||
ticket = await this.createEventStreamTicket(session.session_id);
|
||||
this.forgetConversation(input.workspaceId, input.conversationId);
|
||||
sessionId = await this.getAgentSessionId(input.workspaceId, input.conversationId);
|
||||
ticket = await this.createEventStreamTicket(sessionId);
|
||||
}
|
||||
const streamUrl = new URL(ticket.stream_url, `${this.apiBaseUrl}/`);
|
||||
if (streamUrl.origin !== new URL(this.apiBaseUrl).origin) {
|
||||
@@ -966,7 +1043,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
}
|
||||
streamUrl.searchParams.set(
|
||||
'after_sequence',
|
||||
String(eventSequence(input.afterEventId, session.session_id)),
|
||||
String(eventSequence(input.afterEventId, sessionId)),
|
||||
);
|
||||
streamUrl.protocol = streamUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
|
||||
@@ -1010,7 +1087,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
if (heartbeat !== null) clearInterval(heartbeat);
|
||||
heartbeat = null;
|
||||
unregister();
|
||||
if (didOpen) this.unregisterRunEventStream(session.session_id);
|
||||
if (didOpen) this.unregisterRunEventStream(sessionId);
|
||||
try {
|
||||
await connection.dispose?.();
|
||||
} catch {
|
||||
@@ -1053,7 +1130,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
socket.onopen = () => {
|
||||
if (ending) return;
|
||||
didOpen = true;
|
||||
this.registerRunEventStream(session.session_id);
|
||||
this.registerRunEventStream(sessionId);
|
||||
heartbeat = setInterval(() => {
|
||||
if (ending || socket.readyState !== AGENT_WEBSOCKET_OPEN) return;
|
||||
try {
|
||||
@@ -1072,16 +1149,17 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
const agentEvent = agentEventFromWebSocketFrame(data);
|
||||
const event = normalizeWorkspaceEvent(
|
||||
agentEvent,
|
||||
session.session_id,
|
||||
sessionId,
|
||||
input.workspaceId,
|
||||
input.conversationId,
|
||||
);
|
||||
if (event) {
|
||||
latestWorkspaceEventDelivery = boundedTaskEventDelivery(queue.push(event));
|
||||
}
|
||||
const run = normalizeAgentRunEvent(agentEvent, session.session_id);
|
||||
const run = normalizeAgentRunEvent(agentEvent, sessionId);
|
||||
if (run) {
|
||||
const deliveryBarrier = latestWorkspaceEventDelivery;
|
||||
void deliveryBarrier.then(() => this.publishAgentRun(session.session_id, run));
|
||||
void deliveryBarrier.then(() => this.publishAgentRun(sessionId, run));
|
||||
}
|
||||
};
|
||||
socket.onerror = () => {
|
||||
@@ -1091,23 +1169,19 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
};
|
||||
socket.onclose = ({ code }) => {
|
||||
const error = webSocketCloseError(code);
|
||||
if (code === 4409) {
|
||||
beginEnd(error, async () => {
|
||||
try {
|
||||
await this.closeEventSession(session.session_id);
|
||||
} finally {
|
||||
await this.invalidateEventSession(input.workspaceId);
|
||||
}
|
||||
if (code === 4409 || code === 4404) {
|
||||
beginEnd(error, () => {
|
||||
this.forgetConversation(input.workspaceId, input.conversationId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (code === 4404) {
|
||||
beginEnd(error, () => this.invalidateEventSession(input.workspaceId));
|
||||
return;
|
||||
}
|
||||
beginEnd(error);
|
||||
};
|
||||
unregister = this.registerEventSubscription(input.workspaceId, close);
|
||||
unregister = this.registerEventSubscription(
|
||||
input.workspaceId,
|
||||
input.conversationId,
|
||||
close,
|
||||
);
|
||||
await opened;
|
||||
return {
|
||||
events: queue.events,
|
||||
@@ -1115,53 +1189,13 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
};
|
||||
}
|
||||
|
||||
async closeEventSessions(options: CloseEventSessionsOptions = {}): Promise<void> {
|
||||
async closeEventSessions(): Promise<void> {
|
||||
this.eventSessionsEnabled = false;
|
||||
const activeSubscriptions = [...this.eventSubscriptionClosers.values()]
|
||||
.flatMap((closers) => [...closers]);
|
||||
this.eventSubscriptionClosers.clear();
|
||||
for (const close of activeSubscriptions) close();
|
||||
const pendingSessions = [...this.eventSessions.entries()];
|
||||
this.eventSessions.clear();
|
||||
const sessions = await Promise.all(pendingSessions.map(async ([workspaceId, pending]) => {
|
||||
try {
|
||||
return { workspaceId, session: await pending, creationError: null as unknown };
|
||||
} catch (error) {
|
||||
return { workspaceId, session: null, creationError: error };
|
||||
}
|
||||
}));
|
||||
const closeResults = await Promise.all(sessions.map(async ({
|
||||
workspaceId,
|
||||
session,
|
||||
creationError,
|
||||
}) => {
|
||||
let remoteError = creationError;
|
||||
if (session) {
|
||||
try {
|
||||
await this.closeEventSession(session.session_id, options.accessToken);
|
||||
} catch (error) {
|
||||
remoteError = error;
|
||||
}
|
||||
}
|
||||
|
||||
let rotationError: unknown = null;
|
||||
try {
|
||||
await this.rotateEventSession(workspaceId);
|
||||
} catch (error) {
|
||||
rotationError = error;
|
||||
}
|
||||
return { remoteError, rotationError };
|
||||
}));
|
||||
const failed = closeResults.filter(({ remoteError, rotationError }) => (
|
||||
Boolean(rotationError)
|
||||
|| (!options.tolerateRemoteFailure
|
||||
&& Boolean(remoteError)
|
||||
&& !(remoteError instanceof DesignWorkspaceModuleError
|
||||
&& remoteError.code === 'DESIGN_EVENT_SESSION_CLOSED'))
|
||||
)).length;
|
||||
if (failed > 0) {
|
||||
throw new Error(`Failed to close ${failed} AI design Agent Session(s)`);
|
||||
}
|
||||
this.conversationSessionIds.clear();
|
||||
}
|
||||
|
||||
openAssetContent(workspaceId: string, assetId: string, range?: string): Promise<Response> {
|
||||
@@ -1171,11 +1205,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
);
|
||||
}
|
||||
|
||||
private async requestJson<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
accessToken?: string,
|
||||
): Promise<T> {
|
||||
private async requestJson<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
const response = await this.authorizedFetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
@@ -1185,7 +1215,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
: {}),
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
}, accessToken);
|
||||
});
|
||||
const payload = await readPayload(response);
|
||||
if (!response.ok) {
|
||||
const detail = asErrorDetail(payload);
|
||||
@@ -1204,12 +1234,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
private async authorizedFetch(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
accessToken?: string,
|
||||
): Promise<Response> {
|
||||
if (accessToken) return this.fetchWithToken(path, accessToken, init);
|
||||
private async authorizedFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
||||
let token = await getValidWorksSquareAccessToken({ fetchImpl: this.fetchImpl });
|
||||
if (!token) {
|
||||
throw new DesignWorkspaceModuleError(401, 'AUTH_REQUIRED', '请先登录后再使用 AI 设计');
|
||||
@@ -1239,50 +1264,40 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
});
|
||||
}
|
||||
|
||||
private ensureEventSession(workspaceId: string): Promise<ServerAgentSession> {
|
||||
if (!this.eventSessionsEnabled) {
|
||||
private conversationKey(workspaceId: string, conversationId: string): string {
|
||||
return `${workspaceId}:${conversationId}`;
|
||||
}
|
||||
|
||||
private rememberConversation(conversation: ServerConversationSummary): void {
|
||||
if (typeof conversation.agent_session_id !== 'string'
|
||||
|| !conversation.agent_session_id.trim()) return;
|
||||
this.conversationSessionIds.set(
|
||||
this.conversationKey(conversation.workspace_id, conversation.conversation_id),
|
||||
conversation.agent_session_id,
|
||||
);
|
||||
}
|
||||
|
||||
private forgetConversation(workspaceId: string, conversationId: string): void {
|
||||
this.conversationSessionIds.delete(this.conversationKey(workspaceId, conversationId));
|
||||
}
|
||||
|
||||
private async getAgentSessionId(
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
): Promise<string> {
|
||||
const key = this.conversationKey(workspaceId, conversationId);
|
||||
const existing = this.conversationSessionIds.get(key);
|
||||
if (existing) return existing;
|
||||
await this.getConversation(workspaceId, conversationId);
|
||||
const refreshed = this.conversationSessionIds.get(key);
|
||||
if (!refreshed) {
|
||||
throw new DesignWorkspaceModuleError(
|
||||
503,
|
||||
'DESIGN_EVENT_STREAM_PAUSED',
|
||||
'AI 设计任务状态流已暂停',
|
||||
'DESIGN_CONVERSATION_SESSION_UNAVAILABLE',
|
||||
'设计会话暂时无法连接,请稍后重试',
|
||||
);
|
||||
}
|
||||
const existing = this.eventSessions.get(workspaceId);
|
||||
if (existing) return existing;
|
||||
const pending = (async () => {
|
||||
const clientSessionId = this.eventSessionClientIds.get(workspaceId)
|
||||
?? await this.eventSessionClientIdStore.getOrCreate(workspaceId);
|
||||
this.eventSessionClientIds.set(workspaceId, clientSessionId);
|
||||
const session = await this.requestJson<ServerAgentSession>('/api/agents/sessions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_session_id: clientSessionId,
|
||||
runtime: 'design',
|
||||
runtime_version: 'v1',
|
||||
binding: {
|
||||
kind: 'design_workspace',
|
||||
key: workspaceId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (session.status !== 'active') {
|
||||
throw new DesignWorkspaceModuleError(
|
||||
409,
|
||||
'DESIGN_EVENT_SESSION_CLOSED',
|
||||
'AI 设计任务状态会话已关闭',
|
||||
);
|
||||
}
|
||||
return session;
|
||||
})().catch(async (error) => {
|
||||
this.eventSessions.delete(workspaceId);
|
||||
if (error instanceof DesignWorkspaceModuleError
|
||||
&& error.code === 'DESIGN_EVENT_SESSION_CLOSED') {
|
||||
await this.rotateEventSession(workspaceId);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
this.eventSessions.set(workspaceId, pending);
|
||||
return pending;
|
||||
return refreshed;
|
||||
}
|
||||
|
||||
private createEventStreamTicket(sessionId: string): Promise<ServerAgentStreamTicket> {
|
||||
@@ -1295,13 +1310,18 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
);
|
||||
}
|
||||
|
||||
private registerEventSubscription(workspaceId: string, close: () => void): () => void {
|
||||
const closers = this.eventSubscriptionClosers.get(workspaceId) ?? new Set<() => void>();
|
||||
private registerEventSubscription(
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
close: () => void,
|
||||
): () => void {
|
||||
const key = this.conversationKey(workspaceId, conversationId);
|
||||
const closers = this.eventSubscriptionClosers.get(key) ?? new Set<() => void>();
|
||||
closers.add(close);
|
||||
this.eventSubscriptionClosers.set(workspaceId, closers);
|
||||
this.eventSubscriptionClosers.set(key, closers);
|
||||
return () => {
|
||||
closers.delete(close);
|
||||
if (closers.size === 0) this.eventSubscriptionClosers.delete(workspaceId);
|
||||
if (closers.size === 0) this.eventSubscriptionClosers.delete(key);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1339,28 +1359,4 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
for (const resolve of waiters) resolve(run);
|
||||
}
|
||||
|
||||
private async closeEventSession(sessionId: string, accessToken?: string): Promise<void> {
|
||||
try {
|
||||
await this.requestJson<ServerAgentSession>(
|
||||
`/api/agents/sessions/${encodeURIComponent(sessionId)}`,
|
||||
{ method: 'DELETE' },
|
||||
accessToken,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DesignWorkspaceModuleError
|
||||
&& (error.status === 404 || error.status === 409)) return;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private invalidateEventSession(workspaceId: string): Promise<void> {
|
||||
return this.rotateEventSession(workspaceId).then(() => undefined);
|
||||
}
|
||||
|
||||
private async rotateEventSession(workspaceId: string): Promise<string> {
|
||||
this.eventSessions.delete(workspaceId);
|
||||
const clientSessionId = await this.eventSessionClientIdStore.rotate(workspaceId);
|
||||
this.eventSessionClientIds.set(workspaceId, clientSessionId);
|
||||
return clientSessionId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,6 @@ import { logger } from '../utils/logger';
|
||||
import { warmupNetworkOptimization } from '../utils/uv-env';
|
||||
import { resolvePythonRuntime, resolveUvRuntime } from '../utils/python-runtime';
|
||||
import { initTelemetry } from '../utils/telemetry';
|
||||
import {
|
||||
getOrCreateAgentGatewaySessionClientId,
|
||||
rotateAgentGatewaySessionClientId,
|
||||
} from '../utils/store';
|
||||
|
||||
import { isQuitting, setQuitting } from './app-state';
|
||||
import { applyProxySettings } from './proxy';
|
||||
@@ -456,12 +452,7 @@ async function initialize(): Promise<void> {
|
||||
});
|
||||
const imageWorkspace = localImageWorkspaceEnabled
|
||||
? new LocalImageWorkspace({ userDataDir: app.getPath('userData') })
|
||||
: new WorksSquareDesignWorkspace({
|
||||
eventSessionClientIdStore: {
|
||||
getOrCreate: getOrCreateAgentGatewaySessionClientId,
|
||||
rotate: rotateAgentGatewaySessionClientId,
|
||||
},
|
||||
});
|
||||
: new WorksSquareDesignWorkspace();
|
||||
imageWorkspaceModule = imageWorkspace;
|
||||
if (consumeWorksSquareStartupRuntimeCleanupRequired()) {
|
||||
await clearManagedWorksSquareRuntimeBestEffort({
|
||||
|
||||
Reference in New Issue
Block a user