feat: 完善图像工作区与创作工具体验
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-16 14:08:02 +08:00
parent bfcb88cfef
commit 26b52d76e3
92 changed files with 16678 additions and 2975 deletions

View File

@@ -0,0 +1,158 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { HostApiContext } from '../context';
import { sendJson } from '../route-utils';
import { WORKS_SQUARE_CONFIG } from '../works-config';
import { getValidWorksSquareAccessToken } from '../../services/works-square-session';
import { proxyAwareFetch } from '../../utils/proxy-fetch';
const LOCAL_ROOT = '/api/works/image-prompt-museum';
const UPSTREAM_ROOT = '/api/image-prompt-museum';
const LIST_QUERY_KEYS = ['q', 'use_case', 'style', 'subject', 'language', 'model', 'cursor', 'limit'] as const;
const MAX_QUERY_VALUE_LENGTH = 256;
type PromptMuseumRouteDependencies = {
fetchImpl?: typeof fetch;
getAccessToken?: typeof getValidWorksSquareAccessToken;
apiBaseUrl?: string;
};
function isMuseumPath(pathname: string): boolean {
return pathname === LOCAL_ROOT || /^\/api\/works\/image-prompt-museum\/[^/]+$/.test(pathname);
}
function upstreamPath(pathname: string): string | null {
if (pathname === LOCAL_ROOT) return UPSTREAM_ROOT;
const entryId = pathname.slice(`${LOCAL_ROOT}/`.length);
if (!entryId) return null;
return `${UPSTREAM_ROOT}/${encodeURIComponent(decodeURIComponent(entryId))}`;
}
function copyAllowedQuery(source: URL): string {
const query = new URLSearchParams();
for (const key of LIST_QUERY_KEYS) {
const value = source.searchParams.get(key)?.trim();
if (!value || value.length > MAX_QUERY_VALUE_LENGTH) continue;
query.set(key, value);
}
const encoded = query.toString();
return encoded ? `?${encoded}` : '';
}
async function readPayload(response: Response): Promise<unknown> {
try {
return await response.json();
} catch {
return null;
}
}
function errorMessage(payload: unknown, fallback: string): string {
if (typeof payload === 'object' && payload !== null && !Array.isArray(payload)) {
const value = (payload as Record<string, unknown>).error;
if (typeof value === 'string' && value.trim()) return value;
}
return fallback;
}
export function createImagePromptMuseumRouteHandler(
dependencies: PromptMuseumRouteDependencies = {},
) {
const fetchImpl = dependencies.fetchImpl ?? proxyAwareFetch;
const getAccessToken = dependencies.getAccessToken ?? getValidWorksSquareAccessToken;
const apiBaseUrl = (dependencies.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, '');
return async function handleImagePromptMuseumRoutes(
req: IncomingMessage,
res: ServerResponse,
url: URL,
_ctx: HostApiContext,
): Promise<boolean> {
if (!isMuseumPath(url.pathname)) return false;
if (req.method !== 'GET') {
sendJson(res, 405, {
success: false,
code: 'PROMPT_MUSEUM_METHOD_NOT_ALLOWED',
error: '提示词博物馆只支持读取',
});
return true;
}
const path = upstreamPath(url.pathname);
if (!path) {
sendJson(res, 404, {
success: false,
code: 'PROMPT_MUSEUM_NOT_FOUND',
error: '提示词条目不存在',
});
return true;
}
try {
const token = await getAccessToken({ fetchImpl });
if (!token) {
sendJson(res, 401, {
success: false,
status: 401,
code: 'PROMPT_MUSEUM_AUTH_REQUIRED',
error: '请先登录后再获取灵感',
});
return true;
}
const request = (accessToken: string) => fetchImpl(
`${apiBaseUrl}${path}${url.pathname === LOCAL_ROOT ? copyAllowedQuery(url) : ''}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
redirect: 'manual',
},
);
let response = await request(token);
if (response.status === 401) {
await response.body?.cancel().catch(() => undefined);
const refreshed = await getAccessToken({ fetchImpl, forceRefresh: true });
if (refreshed) response = await request(refreshed);
}
const payload = await readPayload(response);
if (!response.ok) {
sendJson(res, response.status, {
success: false,
status: response.status,
code: typeof payload === 'object' && payload !== null && !Array.isArray(payload)
&& typeof (payload as Record<string, unknown>).code === 'string'
? (payload as Record<string, unknown>).code
: response.status === 404 ? 'PROMPT_MUSEUM_NOT_FOUND' : 'PROMPT_MUSEUM_UNAVAILABLE',
error: errorMessage(payload, '提示词博物馆暂时不可用'),
});
return true;
}
if (payload === null) {
sendJson(res, 502, {
success: false,
status: 502,
code: 'PROMPT_MUSEUM_INVALID_RESPONSE',
error: '提示词博物馆返回了无效数据',
});
return true;
}
sendJson(res, response.status, payload);
return true;
} catch (error) {
sendJson(res, 502, {
success: false,
status: 502,
code: 'PROMPT_MUSEUM_UNAVAILABLE',
error: error instanceof Error ? error.message : '提示词博物馆暂时不可用',
});
return true;
}
};
}
export const handleImagePromptMuseumRoutes = createImagePromptMuseumRouteHandler();

View File

@@ -14,6 +14,7 @@ import {
type DesignCreateConversationInput,
type DesignConfirmGenerationInput,
type DesignCreateWorkspaceInput,
type DesignGenerationParameters,
type DesignRenameWorkspaceInput,
type DesignSubmitMessageInput,
} from '../../../shared/image-workspace';
@@ -50,6 +51,25 @@ function asStringArray(value: unknown): string[] {
: [];
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function generationParametersFromBody(body: Record<string, unknown>): DesignGenerationParameters {
return {
model: asString(body.model),
resolution: asString(body.resolution),
aspectRatio: asString(body.aspectRatio),
durationSeconds: body.durationSeconds === null
? null
: typeof body.durationSeconds === 'number' && Number.isFinite(body.durationSeconds)
? body.durationSeconds
: null,
};
}
const DESIGN_IMAGE_UPLOAD_MIME_TYPES = new Set([
'image/jpeg',
'image/png',
@@ -390,6 +410,11 @@ export async function handleImageWorkspaceRoutes(
return true;
}
if (segments.length === 2 && segments[0] === 'workspaces' && req.method === 'DELETE') {
sendData(res, await ctx.imageWorkspace.deleteWorkspace(segments[1]));
return true;
}
if (segments.length === 2 && segments[0] === 'workspaces' && req.method === 'PATCH') {
const body = await parseJsonBody<Record<string, unknown>>(req);
const input: DesignRenameWorkspaceInput = {
@@ -448,6 +473,20 @@ export async function handleImageWorkspaceRoutes(
return true;
}
if (segments.length === 4
&& segments[0] === 'workspaces'
&& segments[2] === 'generation-quotes'
&& req.method === 'PATCH') {
const body = await parseJsonBody<Record<string, unknown>>(req);
sendData(res, await ctx.imageWorkspace.updateGenerationQuote({
workspaceId: segments[1],
quoteId: segments[3],
finalPrompt: typeof body.finalPrompt === 'string' ? body.finalPrompt : '',
generationParameters: generationParametersFromBody(body),
}));
return true;
}
if (segments.length === 3
&& segments[0] === 'workspaces'
&& segments[2] === 'assets'
@@ -489,6 +528,8 @@ export async function handleImageWorkspaceRoutes(
quoteId: segments[5],
clientTurnId: asString(body.clientTurnId),
expectedTurnRevision: asInteger(body.expectedTurnRevision),
finalPrompt: typeof body.finalPrompt === 'string' ? body.finalPrompt : '',
generationParameters: generationParametersFromBody(asRecord(body.generationParameters)),
};
sendData(res, await ctx.imageWorkspace.confirmGeneration(input));
return true;

View File

@@ -9,6 +9,7 @@ import { handleAppRoutes } from './routes/app';
import { handleAuthRoutes } from './routes/auth';
import { handleOpencodeRoutes } from './routes/opencode';
import { handleImageWorkspaceRoutes } from './routes/image-workspace';
import { handleImagePromptMuseumRoutes } from './routes/image-prompt-museum';
import { handleWorksRoutes } from './routes/works';
import { handleUserSyncRoutes } from './routes/user-sync';
import { handleSettingsRoutes } from './routes/settings';
@@ -34,6 +35,7 @@ const coreRouteHandlers: RouteHandler[] = [
handleAppRoutes,
handleAuthRoutes,
handleImageWorkspaceRoutes,
handleImagePromptMuseumRoutes,
handleWorksRoutes,
handleAgentBrowserRoutes,
handleUserSyncRoutes,

View File

@@ -8,7 +8,12 @@ import type {
DesignConfirmGenerationInput,
DesignCreateConversationInput,
DesignCreateWorkspaceInput,
DesignDeleteWorkspaceResult,
DesignGenerationOption,
DesignGenerationOptions,
DesignGenerationParameters,
DesignGenerationQuote,
DesignGenerationQuoteUpdateInput,
DesignGenerationTask,
DesignMessage,
DesignMedium,
@@ -28,6 +33,224 @@ const LEGACY_LOCAL_WORKSPACE_FILE = 'design-workspace-v2.json';
const MAX_PROJECT_NAME_LENGTH = 80;
const MAX_MESSAGE_LENGTH = 4_000;
const LOCAL_GENERATION_OPTIONS: Record<DesignMedium, DesignGenerationOptions> = {
image: {
models: [{
value: 'local-image-preview',
label: '本地图片预览',
default: true,
disabled: false,
multiplier: 1,
}],
resolutions: [{
value: '1024x1024',
label: '1024 × 1024',
default: true,
disabled: false,
multiplier: 1,
}],
durations: [],
aspectRatios: [
{
value: '1:1',
label: '1:1',
default: true,
disabled: false,
multiplier: 1,
},
{
value: '16:9',
label: '16:9',
default: false,
disabled: false,
multiplier: 1,
},
],
},
video: {
models: [{
value: 'local-video-preview',
label: '本地视频预览',
default: true,
disabled: false,
multiplier: 1,
}],
resolutions: [
{
value: '720P',
label: '720P',
default: false,
disabled: false,
multiplier: 0.8,
},
{
value: '1080P',
label: '1080P',
default: true,
disabled: false,
multiplier: 1,
},
],
durations: [
{
value: 4,
label: '4 秒',
default: false,
disabled: false,
multiplier: 0.8,
},
{
value: 6,
label: '6 秒',
default: true,
disabled: false,
multiplier: 1,
},
{
value: 8,
label: '8 秒',
default: false,
disabled: true,
multiplier: 1.3,
},
],
aspectRatios: [
{
value: '16:9',
label: '16:9',
default: true,
disabled: false,
multiplier: 1,
},
{
value: '9:16',
label: '9:16',
default: false,
disabled: false,
multiplier: 1,
},
],
},
};
const LOCAL_PRICING_SCHEMA = 'local-design-pricing-v1';
function defaultOption<TValue extends string | number>(
options: DesignGenerationOption<TValue>[],
): DesignGenerationOption<TValue> {
return options.find((option) => option.default && !option.disabled)
?? options.find((option) => !option.disabled)
?? options[0]!;
}
function localGenerationParameters(medium: DesignMedium): DesignGenerationParameters {
const options = LOCAL_GENERATION_OPTIONS[medium];
return {
model: defaultOption(options.models).value,
resolution: defaultOption(options.resolutions).value,
aspectRatio: defaultOption(options.aspectRatios).value,
durationSeconds: options.durations.length > 0
? defaultOption(options.durations).value
: null,
};
}
function localGenerationPricing(
medium: DesignMedium,
parameters: DesignGenerationParameters,
): number {
const options = LOCAL_GENERATION_OPTIONS[medium];
const model = options.models.find((option) => option.value === parameters.model);
const resolution = options.resolutions.find((option) => option.value === parameters.resolution);
const aspectRatio = options.aspectRatios.find((option) => option.value === parameters.aspectRatio);
const duration = parameters.durationSeconds === null
? null
: options.durations.find((option) => option.value === parameters.durationSeconds);
if (!model || model.disabled || !resolution || resolution.disabled
|| !aspectRatio || aspectRatio.disabled
|| (options.durations.length > 0 && (!duration || duration.disabled))
|| (options.durations.length === 0 && parameters.durationSeconds !== null)) {
throw new LocalImageWorkspaceError(
422,
'generation_option_invalid',
'当前生成参数不可用,请重新选择',
);
}
const baseAmount = medium === 'video' ? 2 : 1;
return Math.ceil(baseAmount * model.multiplier * resolution.multiplier * (duration?.multiplier ?? 1));
}
function createLocalQuote(
medium: DesignMedium,
briefVersion: number,
briefSummary: string,
finalPrompt: string,
quoteId: string,
expiresAt: string,
): DesignGenerationQuote {
const generationParameters = localGenerationParameters(medium);
const amount = localGenerationPricing(medium, generationParameters);
return {
quoteId,
status: 'active',
medium,
briefVersion,
briefSummary,
finalPrompt,
promptMode: 'guided',
generationParameters,
generationOptions: clone(LOCAL_GENERATION_OPTIONS[medium]),
pricing: {
schema: LOCAL_PRICING_SCHEMA,
amount,
rounding: 'ceil',
},
quotedDesignPoints: amount,
expiresAt,
};
}
function sameGenerationParameters(
left: DesignGenerationParameters,
right: DesignGenerationParameters,
): boolean {
return left.model === right.model
&& left.resolution === right.resolution
&& left.aspectRatio === right.aspectRatio
&& left.durationSeconds === right.durationSeconds;
}
function normalizePersistedGenerationQuotes(
state: PersistedImageWorkspace,
): PersistedImageWorkspace {
for (const conversations of Object.values(state.conversationsByWorkspaceId)) {
for (const conversation of conversations) {
for (const message of conversation.messages) {
const quote = message.generationQuote;
if (!quote || (quote.finalPrompt !== undefined
&& quote.generationParameters?.aspectRatio !== undefined
&& quote.generationOptions?.aspectRatios !== undefined
&& quote.pricing)) continue;
const finalPrompt = quote.finalPrompt ?? quote.briefSummary;
const normalized = createLocalQuote(
quote.medium,
quote.briefVersion,
quote.briefSummary,
finalPrompt,
quote.quoteId,
quote.expiresAt,
);
Object.assign(quote, {
...normalized,
status: quote.status,
quotedDesignPoints: quote.quotedDesignPoints,
});
}
}
}
return state;
}
const LOCAL_CAPABILITIES: DesignCapabilities = {
conversation: true,
generation: true,
@@ -272,6 +495,25 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
});
}
deleteWorkspace(workspaceId: string): Promise<DesignDeleteWorkspaceResult> {
return this.enqueue(async () => {
const state = await this.load();
this.requireWorkspace(state, workspaceId);
const nextState = clone(state);
nextState.workspaces = nextState.workspaces.filter(
(workspace) => workspace.workspaceId !== workspaceId,
);
delete nextState.conversationsByWorkspaceId[workspaceId];
delete nextState.tasksByWorkspaceId[workspaceId];
for (const [assetId, asset] of Object.entries(nextState.assetsById)) {
if (asset.workspaceId === workspaceId) delete nextState.assetsById[assetId];
}
await this.persist(nextState);
return { workspaceId, deleted: true };
});
}
createConversation(input: DesignCreateConversationInput): Promise<DesignConversation> {
return this.mutateConversation((state) => {
const workspace = this.requireWorkspace(state, input.workspaceId);
@@ -337,15 +579,14 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
const timestamp = this.now().toISOString();
const nextRevision = conversation.turnRevision + 1;
const medium = detectMedium(message);
const quote: DesignGenerationQuote = {
quoteId: `local-quote-${this.createId()}`,
status: 'active',
const quote = createLocalQuote(
medium,
briefVersion: nextRevision,
briefSummary: message,
quotedDesignPoints: medium === 'video' ? 2 : 1,
expiresAt: new Date(this.now().getTime() + 15 * 60 * 1000).toISOString(),
};
nextRevision,
message,
message,
`local-quote-${this.createId()}`,
new Date(this.now().getTime() + 15 * 60 * 1000).toISOString(),
);
conversation.messages.push(
{
id: `local-message-${this.createId()}`,
@@ -389,6 +630,46 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
});
}
updateGenerationQuote(
input: DesignGenerationQuoteUpdateInput,
): Promise<DesignGenerationQuote> {
return this.enqueue(async () => {
const state = await this.load();
const workspace = this.requireWorkspace(state, input.workspaceId);
const conversation = (state.conversationsByWorkspaceId[input.workspaceId] ?? [])
.find((candidate) => candidate.messages.some((message) => (
message.generationQuote?.quoteId === input.quoteId
)));
const quote = conversation?.messages
.map((message) => message.generationQuote)
.find((candidate) => candidate?.quoteId === input.quoteId);
if (!conversation || !quote || quote.status !== 'active') {
throw new LocalImageWorkspaceError(409, 'generation_quote_invalid', '当前生成报价已失效');
}
if (new Date(quote.expiresAt).getTime() <= this.now().getTime()) {
quote.status = 'expired';
throw new LocalImageWorkspaceError(409, 'generation_quote_expired', '当前生成报价已过期');
}
if (!input.finalPrompt.trim()) {
throw new LocalImageWorkspaceError(422, 'generation_prompt_invalid', '提示词不能为空');
}
const amount = localGenerationPricing(quote.medium, input.generationParameters);
quote.finalPrompt = input.finalPrompt;
quote.generationParameters = clone(input.generationParameters);
quote.pricing = {
...quote.pricing,
amount,
};
quote.quotedDesignPoints = amount;
const timestamp = this.now().toISOString();
conversation.updatedAt = timestamp;
workspace.viewRevision += 1;
workspace.updatedAt = timestamp;
await this.persist(state);
return clone(quote);
});
}
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignConversation> {
return this.mutateConversation((state) => {
const workspace = this.requireWorkspace(state, input.workspaceId);
@@ -410,6 +691,14 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
quote.status = 'expired';
throw new LocalImageWorkspaceError(409, 'generation_quote_expired', '当前生成报价已过期');
}
if (quote.finalPrompt !== input.finalPrompt
|| !sameGenerationParameters(quote.generationParameters, input.generationParameters)) {
throw new LocalImageWorkspaceError(
409,
'generation_quote_changed',
'生成参数已变更,请等待重新报价完成',
);
}
quote.status = 'consumed';
const timestamp = this.now().toISOString();
@@ -609,7 +898,7 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
'AI 设计本地数据暂时无法读取',
);
}
this.state = parsed;
this.state = normalizePersistedGenerationQuotes(parsed);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
this.state = await this.loadLegacyState();
@@ -631,6 +920,7 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
);
}
const migrated = this.migrateLegacyState(parsed);
normalizePersistedGenerationQuotes(migrated);
await this.persist(migrated);
return migrated;
} catch (error) {

View File

@@ -6,6 +6,9 @@ import type {
DesignCreateConversationInput,
DesignConfirmGenerationInput,
DesignCreateWorkspaceInput,
DesignDeleteWorkspaceResult,
DesignGenerationQuote,
DesignGenerationQuoteUpdateInput,
DesignGenerationTask,
DesignRenameWorkspaceInput,
DesignSubmitMessageInput,
@@ -46,11 +49,13 @@ export interface DesignWorkspaceModule {
bootstrap(): Promise<DesignWorkspaceBootstrap>;
getCapabilities(): Promise<DesignCapabilities>;
createWorkspace(input: DesignCreateWorkspaceInput): Promise<DesignWorkspace>;
deleteWorkspace(workspaceId: string): Promise<DesignDeleteWorkspaceResult>;
renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace>;
getWorkspace(workspaceId: string): Promise<DesignWorkspace>;
createConversation(input: DesignCreateConversationInput): Promise<DesignConversation>;
getConversation(workspaceId: string, conversationId: string): Promise<DesignConversation>;
submitMessage(input: DesignSubmitMessageInput): Promise<DesignConversation>;
updateGenerationQuote(input: DesignGenerationQuoteUpdateInput): Promise<DesignGenerationQuote>;
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignConversation>;
listTasks(workspaceId: string): Promise<DesignGenerationTask[]>;
uploadAsset?(input: DesignAssetUploadInput): Promise<DesignAsset>;

View File

@@ -10,6 +10,9 @@ import type {
DesignConfirmGenerationInput,
DesignCreateConversationInput,
DesignCreateWorkspaceInput,
DesignDeleteWorkspaceResult,
DesignGenerationOption,
DesignGenerationQuoteUpdateInput,
DesignGenerationQuote,
DesignGenerationTask,
DesignGenerationTaskUpdatedEvent,
@@ -48,6 +51,25 @@ type ServerQuote = {
medium: DesignGenerationQuote['medium'];
brief_version: number;
brief_summary: string;
final_prompt: string;
prompt_mode: string;
generation_parameters: {
model: string;
resolution: string;
aspect_ratio: string;
duration_seconds: number | null;
};
generation_options: {
models: Array<DesignGenerationOption<string>>;
resolutions: Array<DesignGenerationOption<string>>;
durations: Array<DesignGenerationOption<number>>;
aspect_ratios: Array<DesignGenerationOption<string>>;
};
pricing: {
schema: string;
amount: number;
rounding: string;
};
quoted_design_points: number;
expires_at: string;
};
@@ -176,6 +198,13 @@ type AgentDesignTurnSubmission = {
action: null | {
type: 'confirm_generation';
quote_id: string;
final_prompt?: string;
aspect_ratio?: string;
generation_parameters?: {
model: string;
resolution: string;
duration_seconds: number | null;
};
};
};
@@ -260,6 +289,25 @@ function mapQuote(quote: ServerQuote | null): DesignGenerationQuote | null {
medium: quote.medium,
briefVersion: quote.brief_version,
briefSummary: quote.brief_summary,
finalPrompt: quote.final_prompt,
promptMode: quote.prompt_mode,
generationParameters: {
model: quote.generation_parameters.model,
resolution: quote.generation_parameters.resolution,
aspectRatio: quote.generation_parameters.aspect_ratio,
durationSeconds: quote.generation_parameters.duration_seconds,
},
generationOptions: {
models: quote.generation_options.models,
resolutions: quote.generation_options.resolutions,
durations: quote.generation_options.durations,
aspectRatios: quote.generation_options.aspect_ratios,
},
pricing: {
schema: quote.pricing.schema,
amount: quote.pricing.amount,
rounding: quote.pricing.rounding,
},
quotedDesignPoints: quote.quoted_design_points,
expiresAt: quote.expires_at,
};
@@ -810,6 +858,15 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
return this.getWorkspace(workspace.workspace_id);
}
async deleteWorkspace(workspaceId: string): Promise<DesignDeleteWorkspaceResult> {
await this.requestJson<unknown>(
`/api/design/workspaces/${encodeURIComponent(workspaceId)}`,
{ method: 'DELETE' },
);
this.forgetWorkspace(workspaceId);
return { workspaceId, deleted: true };
}
async renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace> {
const workspace = await this.requestJson<ServerWorkspace>(
`/api/design/workspaces/${encodeURIComponent(input.workspaceId)}`,
@@ -873,8 +930,43 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
});
}
async updateGenerationQuote(
input: DesignGenerationQuoteUpdateInput,
): Promise<DesignGenerationQuote> {
const quote = await this.requestJson<ServerQuote>(
`/api/design/workspaces/${encodeURIComponent(input.workspaceId)}/generation-quotes/${encodeURIComponent(input.quoteId)}`,
{
method: 'PATCH',
body: JSON.stringify({
final_prompt: input.finalPrompt,
model: input.generationParameters.model,
resolution: input.generationParameters.resolution,
aspect_ratio: input.generationParameters.aspectRatio,
duration_seconds: input.generationParameters.durationSeconds,
}),
},
);
return mapQuote(quote)!;
}
async confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignConversation> {
const agentSessionId = await this.getAgentSessionId(input.workspaceId, input.conversationId);
const action = input.finalPrompt === undefined || input.generationParameters === undefined
? {
type: 'confirm_generation' as const,
quote_id: input.quoteId,
}
: {
type: 'confirm_generation' as const,
quote_id: input.quoteId,
final_prompt: input.finalPrompt,
aspect_ratio: input.generationParameters.aspectRatio,
generation_parameters: {
model: input.generationParameters.model,
resolution: input.generationParameters.resolution,
duration_seconds: input.generationParameters.durationSeconds,
},
};
return this.executeAgentTurn({
workspaceId: input.workspaceId,
conversationId: input.conversationId,
@@ -883,10 +975,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
expectedTurnRevision: input.expectedTurnRevision,
message: '确认生成',
attachmentAssetIds: [],
action: {
type: 'confirm_generation',
quote_id: input.quoteId,
},
action,
});
}
@@ -1438,6 +1527,30 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
this.conversationSessionIds.delete(this.conversationKey(workspaceId, conversationId));
}
private forgetWorkspace(workspaceId: string): void {
const keyPrefix = `${workspaceId}:`;
const sessionIds = new Set<string>();
for (const [key, sessionId] of this.conversationSessionIds) {
if (!key.startsWith(keyPrefix)) continue;
sessionIds.add(sessionId);
this.conversationSessionIds.delete(key);
}
const activeSubscriptions: Array<() => void> = [];
for (const [key, closers] of this.eventSubscriptionClosers) {
if (!key.startsWith(keyPrefix)) continue;
activeSubscriptions.push(...closers);
this.eventSubscriptionClosers.delete(key);
}
for (const close of activeSubscriptions) close();
for (const key of this.terminalAgentRuns.keys()) {
if ([...sessionIds].some((sessionId) => key.startsWith(`${sessionId}:`))) {
this.terminalAgentRuns.delete(key);
}
}
}
private async getAgentSessionId(
workspaceId: string,
conversationId: string,

View File

@@ -118,7 +118,15 @@ async function getSettingsStore() {
*/
export async function getSetting<K extends keyof AppSettings>(key: K): Promise<AppSettings[K]> {
const store = await getSettingsStore();
return store.get(key);
const value = store.get(key);
if (key === 'language') {
const resolvedLanguage = resolveSupportedLanguage(value as string);
if (value !== resolvedLanguage) {
store.set(key, resolvedLanguage);
}
return resolvedLanguage as AppSettings[K];
}
return value;
}
/**
@@ -129,6 +137,10 @@ export async function setSetting<K extends keyof AppSettings>(
value: AppSettings[K]
): Promise<void> {
const store = await getSettingsStore();
if (key === 'language') {
store.set(key, resolveSupportedLanguage(value as string));
return;
}
store.set(key, value);
}
@@ -170,7 +182,12 @@ export function rotateAgentGatewaySessionClientId(workspaceId: string): Promise<
*/
export async function getAllSettings(): Promise<AppSettings> {
const store = await getSettingsStore();
return store.store;
const settings = store.store;
const language = resolveSupportedLanguage(settings.language);
if (settings.language !== language) {
store.set('language', language);
}
return { ...settings, language };
}
/**