feat: 统一 AI 设计 Workspace 与生成任务链路
需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。 实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
This commit is contained in:
@@ -1,24 +1,21 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { Readable } from 'node:stream';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import {
|
||||
IMAGE_WORKSPACE_API_PATH,
|
||||
IMAGE_WORKSPACE_UNAVAILABLE_CODE,
|
||||
IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE,
|
||||
type ImageWorkspaceGenerationSettings,
|
||||
type ImageWorkspaceReferenceUploadInput,
|
||||
type ImageWorkspaceSendMessageInput,
|
||||
type ImageWorkspaceSnapshot,
|
||||
type DesignConfirmGenerationInput,
|
||||
type DesignCreateWorkspaceInput,
|
||||
type DesignRenameWorkspaceInput,
|
||||
type DesignSubmitMessageInput,
|
||||
} from '../../../shared/image-workspace';
|
||||
import { LocalImageWorkspaceError } from '../../image-workspace/local-workspace';
|
||||
import { DesignWorkspaceModuleError } from '../../image-workspace/module';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { parseJsonBody, sendJson } from '../route-utils';
|
||||
|
||||
const IMAGE_WORKSPACE_ROUTE_PREFIX = '/api/works/image-workspace';
|
||||
|
||||
function sendWorkspace(res: ServerResponse, workspace: ImageWorkspaceSnapshot): void {
|
||||
sendJson(res, 200, { success: true, status: 200, workspace });
|
||||
}
|
||||
|
||||
function decodedSegments(pathname: string): string[] | null {
|
||||
const suffix = pathname.slice(IMAGE_WORKSPACE_ROUTE_PREFIX.length).replace(/^\/+/, '');
|
||||
const suffix = pathname.slice(IMAGE_WORKSPACE_API_PATH.length).replace(/^\/+/, '');
|
||||
if (!suffix) return [];
|
||||
try {
|
||||
return suffix.split('/').map((segment) => decodeURIComponent(segment));
|
||||
@@ -31,26 +28,26 @@ function asString(value: unknown): string {
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
|
||||
function asInteger(value: unknown): number {
|
||||
return typeof value === 'number' && Number.isInteger(value) ? value : -1;
|
||||
}
|
||||
|
||||
function asStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) && value.every((item) => typeof item === 'string')
|
||||
? value
|
||||
: [];
|
||||
}
|
||||
|
||||
function asSettings(value: unknown): ImageWorkspaceGenerationSettings {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return {};
|
||||
const source = value as Record<string, unknown>;
|
||||
return {
|
||||
modeId: asString(source.modeId) || undefined,
|
||||
modelId: asString(source.modelId) || undefined,
|
||||
aspectRatioId: asString(source.aspectRatioId) || undefined,
|
||||
resolutionId: asString(source.resolutionId) || undefined,
|
||||
outputCountId: asString(source.outputCountId) || undefined,
|
||||
};
|
||||
function sendData(res: ServerResponse, data: unknown): void {
|
||||
sendJson(res, 200, { success: true, status: 200, data });
|
||||
}
|
||||
|
||||
function sendRouteError(res: ServerResponse, error: unknown): void {
|
||||
if (error instanceof LocalImageWorkspaceError) {
|
||||
if (res.headersSent) {
|
||||
res.destroy(error instanceof Error ? error : undefined);
|
||||
return;
|
||||
}
|
||||
if (error instanceof DesignWorkspaceModuleError) {
|
||||
sendJson(res, error.status, {
|
||||
success: false,
|
||||
status: error.status,
|
||||
@@ -71,19 +68,51 @@ function sendRouteError(res: ServerResponse, error: unknown): void {
|
||||
sendJson(res, 500, {
|
||||
success: false,
|
||||
status: 500,
|
||||
code: 'IMAGE_WORKSPACE_LOCAL_REQUEST_FAILED',
|
||||
error: error instanceof Error ? error.message : '创作空间请求失败',
|
||||
code: 'IMAGE_WORKSPACE_REQUEST_FAILED',
|
||||
error: error instanceof Error ? error.message : 'AI 设计请求失败',
|
||||
});
|
||||
}
|
||||
|
||||
async function relayAssetContent(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
ctx: HostApiContext,
|
||||
workspaceId: string,
|
||||
assetId: string,
|
||||
): Promise<void> {
|
||||
const content = await ctx.imageWorkspace!.openAssetContent(
|
||||
workspaceId,
|
||||
assetId,
|
||||
typeof req.headers.range === 'string' ? req.headers.range : undefined,
|
||||
);
|
||||
res.statusCode = content.status;
|
||||
for (const header of [
|
||||
'accept-ranges',
|
||||
'cache-control',
|
||||
'content-length',
|
||||
'content-range',
|
||||
'content-type',
|
||||
'etag',
|
||||
'last-modified',
|
||||
]) {
|
||||
const value = content.headers.get(header);
|
||||
if (value) res.setHeader(header, value);
|
||||
}
|
||||
if (!content.body || req.method === 'HEAD') {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
await pipeline(Readable.fromWeb(content.body), res);
|
||||
}
|
||||
|
||||
export async function handleImageWorkspaceRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
url: URL,
|
||||
ctx: HostApiContext,
|
||||
): Promise<boolean> {
|
||||
if (url.pathname !== IMAGE_WORKSPACE_ROUTE_PREFIX
|
||||
&& !url.pathname.startsWith(`${IMAGE_WORKSPACE_ROUTE_PREFIX}/`)) {
|
||||
if (url.pathname !== IMAGE_WORKSPACE_API_PATH
|
||||
&& !url.pathname.startsWith(`${IMAGE_WORKSPACE_API_PATH}/`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -99,55 +128,99 @@ export async function handleImageWorkspaceRoutes(
|
||||
|
||||
const segments = decodedSegments(url.pathname);
|
||||
if (!segments) {
|
||||
sendJson(res, 400, { success: false, status: 400, error: '无效的绘画空间路径' });
|
||||
sendJson(res, 400, { success: false, status: 400, error: '无效的 AI 设计路径' });
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (segments.length === 0 && req.method === 'GET') {
|
||||
sendWorkspace(res, await ctx.imageWorkspace.getSnapshot());
|
||||
sendData(res, await ctx.imageWorkspace.bootstrap());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segments.length === 1 && segments[0] === 'projects' && req.method === 'POST') {
|
||||
const body = await parseJsonBody<{ name?: unknown }>(req);
|
||||
sendWorkspace(res, await ctx.imageWorkspace.createProject(asString(body.name)));
|
||||
if (segments.length === 1 && segments[0] === 'workspaces' && req.method === 'POST') {
|
||||
const body = await parseJsonBody<Record<string, unknown>>(req);
|
||||
const input: DesignCreateWorkspaceInput = {
|
||||
clientWorkspaceId: asString(body.clientWorkspaceId),
|
||||
title: asString(body.title),
|
||||
};
|
||||
sendData(res, await ctx.imageWorkspace.createWorkspace(input));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segments.length === 1 && segments[0] === 'local-data' && req.method === 'DELETE') {
|
||||
sendWorkspace(res, await ctx.imageWorkspace.reset());
|
||||
if (!ctx.imageWorkspace.reset) {
|
||||
throw new DesignWorkspaceModuleError(
|
||||
405,
|
||||
'IMAGE_WORKSPACE_RESET_NOT_ALLOWED',
|
||||
'云端 AI 设计不支持清空本地数据',
|
||||
);
|
||||
}
|
||||
sendData(res, await ctx.imageWorkspace.reset());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segments.length === 3 && segments[0] === 'projects' && segments[2] === 'agents' && req.method === 'POST') {
|
||||
sendWorkspace(res, await ctx.imageWorkspace.addAgent(segments[1]));
|
||||
if (segments.length === 2 && segments[0] === 'workspaces' && req.method === 'GET') {
|
||||
sendData(res, await ctx.imageWorkspace.getWorkspace(segments[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segments.length === 3 && segments[0] === 'projects' && segments[2] === 'messages' && req.method === 'POST') {
|
||||
if (segments.length === 2 && segments[0] === 'workspaces' && req.method === 'PATCH') {
|
||||
const body = await parseJsonBody<Record<string, unknown>>(req);
|
||||
const input: ImageWorkspaceSendMessageInput = {
|
||||
projectId: segments[1],
|
||||
agentId: asString(body.agentId),
|
||||
prompt: asString(body.prompt),
|
||||
referenceImageIds: asStringArray(body.referenceImageIds),
|
||||
settings: asSettings(body.settings),
|
||||
const input: DesignRenameWorkspaceInput = {
|
||||
workspaceId: segments[1],
|
||||
title: asString(body.title),
|
||||
};
|
||||
sendWorkspace(res, await ctx.imageWorkspace.sendMessage(input));
|
||||
sendData(res, await ctx.imageWorkspace.renameWorkspace(input));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segments.length === 3 && segments[0] === 'projects' && segments[2] === 'references' && req.method === 'POST') {
|
||||
if (segments.length === 3
|
||||
&& segments[0] === 'workspaces'
|
||||
&& segments[2] === 'messages'
|
||||
&& req.method === 'POST') {
|
||||
const body = await parseJsonBody<Record<string, unknown>>(req);
|
||||
const input: ImageWorkspaceReferenceUploadInput = {
|
||||
projectId: segments[1],
|
||||
fileName: asString(body.fileName),
|
||||
mimeType: asString(body.mimeType),
|
||||
contentBase64: asString(body.contentBase64),
|
||||
const input: DesignSubmitMessageInput = {
|
||||
workspaceId: segments[1],
|
||||
clientTurnId: asString(body.clientTurnId),
|
||||
expectedTurnRevision: asInteger(body.expectedTurnRevision),
|
||||
message: asString(body.message),
|
||||
attachmentAssetIds: asStringArray(body.attachmentAssetIds),
|
||||
};
|
||||
const result = await ctx.imageWorkspace.uploadReference(input);
|
||||
sendJson(res, 200, { success: true, status: 200, ...result });
|
||||
sendData(res, await ctx.imageWorkspace.submitMessage(input));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segments.length === 3
|
||||
&& segments[0] === 'workspaces'
|
||||
&& segments[2] === 'tasks'
|
||||
&& req.method === 'GET') {
|
||||
sendData(res, await ctx.imageWorkspace.listTasks(segments[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segments.length === 5
|
||||
&& segments[0] === 'workspaces'
|
||||
&& segments[2] === 'quotes'
|
||||
&& segments[4] === 'confirm'
|
||||
&& req.method === 'POST') {
|
||||
const body = await parseJsonBody<Record<string, unknown>>(req);
|
||||
const input: DesignConfirmGenerationInput = {
|
||||
workspaceId: segments[1],
|
||||
quoteId: segments[3],
|
||||
clientTurnId: asString(body.clientTurnId),
|
||||
expectedTurnRevision: asInteger(body.expectedTurnRevision),
|
||||
};
|
||||
sendData(res, await ctx.imageWorkspace.confirmGeneration(input));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segments.length === 5
|
||||
&& segments[0] === 'workspaces'
|
||||
&& segments[2] === 'assets'
|
||||
&& segments[4] === 'content'
|
||||
&& (req.method === 'GET' || req.method === 'HEAD')) {
|
||||
await relayAssetContent(req, res, ctx, segments[1], segments[3]);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -155,7 +228,7 @@ export async function handleImageWorkspaceRoutes(
|
||||
success: false,
|
||||
status: 404,
|
||||
code: 'IMAGE_WORKSPACE_ROUTE_NOT_FOUND',
|
||||
error: `没有对应的绘画空间接口:${req.method ?? 'GET'} ${url.pathname}`,
|
||||
error: `没有对应的 AI 设计接口:${req.method ?? 'GET'} ${url.pathname}`,
|
||||
});
|
||||
} catch (error) {
|
||||
sendRouteError(res, error);
|
||||
|
||||
Reference in New Issue
Block a user