feat: 统一 AI 设计 Workspace 与生成任务链路

需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。

实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
This commit is contained in:
2026-07-31 13:56:55 +08:00
parent 80e8386fa6
commit 3d9dd14918
22 changed files with 2506 additions and 1707 deletions

View File

@@ -1,24 +1,24 @@
import { AppError } from '@/lib/error-model';
import { hostApiFetch } from '@/lib/host-api';
import {
ensureHostApiToken,
getHostApiBase,
hostApiFetch,
} from '@/lib/host-api';
import {
IMAGE_WORKSPACE_API_PATH,
IMAGE_WORKSPACE_UNAVAILABLE_CODE,
IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE,
type ImageWorkspaceImage,
type ImageWorkspaceReferenceUploadInput,
type ImageWorkspaceReferenceUploadResult,
type ImageWorkspaceSendMessageInput,
type ImageWorkspaceSnapshot,
type DesignGenerationTask,
type DesignWorkspace,
type DesignWorkspaceBootstrap,
} from '../../shared/image-workspace';
const IMAGE_WORKSPACE_API_PATH = '/api/works/image-workspace';
type ImageWorkspaceEnvelope = {
type ImageWorkspaceEnvelope<T> = {
success?: boolean;
status?: number;
code?: string;
error?: string;
workspace?: ImageWorkspaceSnapshot;
reference?: ImageWorkspaceImage;
data?: T;
};
export const IMAGE_WORKSPACE_CREATE_PROJECT_EVENT = 'niancode:image-workspace:create-project';
@@ -35,6 +35,12 @@ export class ImageWorkspaceApiError extends Error {
}
}
function createClientId(prefix: string): string {
const id = globalThis.crypto?.randomUUID?.()
?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
return `${prefix}-${id}`;
}
function getErrorStatus(error: unknown): number {
if (error instanceof AppError && typeof error.details?.status === 'number') {
return error.details.status;
@@ -42,105 +48,119 @@ function getErrorStatus(error: unknown): number {
return 502;
}
async function requestImageWorkspace(
accessToken: string | null | undefined,
path: string,
init: RequestInit = {},
): Promise<ImageWorkspaceSnapshot> {
const headers = accessToken?.trim()
? { 'X-NianCode-Access-Token': accessToken.trim(), ...init.headers }
: init.headers;
let response: ImageWorkspaceEnvelope;
function getErrorCode(error: unknown, status: number): string {
if (error instanceof AppError && typeof error.details?.backendCode === 'string') {
return error.details.backendCode;
}
if (status === 501) return IMAGE_WORKSPACE_UNAVAILABLE_CODE;
return 'IMAGE_WORKSPACE_REQUEST_FAILED';
}
async function requestData<T>(path: string, init: RequestInit = {}): Promise<T> {
let response: ImageWorkspaceEnvelope<T>;
try {
response = await hostApiFetch<ImageWorkspaceEnvelope>(path, {
...init,
...(headers ? { headers } : {}),
});
response = await hostApiFetch<ImageWorkspaceEnvelope<T>>(path, init);
} catch (error) {
const status = getErrorStatus(error);
throw new ImageWorkspaceApiError(
status,
status === 501 ? IMAGE_WORKSPACE_UNAVAILABLE_CODE : 'IMAGE_WORKSPACE_REQUEST_FAILED',
getErrorCode(error, status),
error instanceof Error ? error.message : IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE,
);
}
if (!response.success || !response.workspace) {
if (!response.success || response.data === undefined) {
throw new ImageWorkspaceApiError(
response.status ?? 502,
response.code ?? 'IMAGE_WORKSPACE_REQUEST_FAILED',
response.error ?? '创作空间请求失败',
response.error ?? 'AI 设计请求失败',
);
}
return response.workspace;
return response.data;
}
export function fetchImageWorkspace(accessToken?: string | null): Promise<ImageWorkspaceSnapshot> {
return requestImageWorkspace(accessToken, IMAGE_WORKSPACE_API_PATH);
export function fetchImageWorkspace(): Promise<DesignWorkspaceBootstrap> {
return requestData(IMAGE_WORKSPACE_API_PATH);
}
export function createImageWorkspaceProject(
accessToken: string | null | undefined,
name: string,
): Promise<ImageWorkspaceSnapshot> {
return requestImageWorkspace(accessToken, `${IMAGE_WORKSPACE_API_PATH}/projects`, {
title: string,
clientWorkspaceId = createClientId('workspace'),
): Promise<DesignWorkspace> {
return requestData(`${IMAGE_WORKSPACE_API_PATH}/workspaces`, {
method: 'POST',
body: JSON.stringify({ name: name.trim() }),
body: JSON.stringify({
clientWorkspaceId,
title: title.trim(),
}),
});
}
export function addImageWorkspaceAgent(
accessToken: string | null | undefined,
projectId: string,
): Promise<ImageWorkspaceSnapshot> {
return requestImageWorkspace(
accessToken,
`${IMAGE_WORKSPACE_API_PATH}/projects/${encodeURIComponent(projectId)}/agents`,
{ method: 'POST', body: JSON.stringify({}) },
export function renameImageWorkspaceProject(
workspaceId: string,
title: string,
): Promise<DesignWorkspace> {
return requestData(
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}`,
{
method: 'PATCH',
body: JSON.stringify({ title: title.trim() }),
},
);
}
export function fetchImageWorkspaceProject(workspaceId: string): Promise<DesignWorkspace> {
return requestData(
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}`,
);
}
export function sendImageWorkspaceMessage(
accessToken: string | null | undefined,
input: ImageWorkspaceSendMessageInput,
): Promise<ImageWorkspaceSnapshot> {
return requestImageWorkspace(
accessToken,
`${IMAGE_WORKSPACE_API_PATH}/projects/${encodeURIComponent(input.projectId)}/messages`,
{ method: 'POST', body: JSON.stringify(input) },
workspaceId: string,
expectedTurnRevision: number,
message: string,
): Promise<DesignWorkspace> {
return requestData(
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/messages`,
{
method: 'POST',
body: JSON.stringify({
clientTurnId: createClientId('turn'),
expectedTurnRevision,
message: message.trim(),
attachmentAssetIds: [],
}),
},
);
}
export function uploadImageWorkspaceReference(
accessToken: string | null | undefined,
input: ImageWorkspaceReferenceUploadInput,
): Promise<ImageWorkspaceReferenceUploadResult> {
const headers = accessToken?.trim()
? { 'X-NianCode-Access-Token': accessToken.trim() }
: undefined;
return hostApiFetch<ImageWorkspaceEnvelope>(
`${IMAGE_WORKSPACE_API_PATH}/projects/${encodeURIComponent(input.projectId)}/references`,
export function confirmImageWorkspaceGeneration(
workspaceId: string,
expectedTurnRevision: number,
quoteId: string,
): Promise<DesignWorkspace> {
return requestData(
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/quotes/${encodeURIComponent(quoteId)}/confirm`,
{
method: 'POST',
...(headers ? { headers } : {}),
body: JSON.stringify(input),
body: JSON.stringify({
clientTurnId: createClientId('turn'),
expectedTurnRevision,
}),
},
).then((response) => {
if (!response.success || !response.workspace || !response.reference) {
throw new ImageWorkspaceApiError(
response.status ?? 502,
response.code ?? 'IMAGE_WORKSPACE_REQUEST_FAILED',
response.error ?? '参考图上传失败',
);
}
return { workspace: response.workspace, reference: response.reference };
}).catch((error: unknown) => {
if (error instanceof ImageWorkspaceApiError) throw error;
const status = getErrorStatus(error);
throw new ImageWorkspaceApiError(
status,
status === 501 ? IMAGE_WORKSPACE_UNAVAILABLE_CODE : 'IMAGE_WORKSPACE_REQUEST_FAILED',
error instanceof Error ? error.message : IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE,
);
});
);
}
export function fetchImageWorkspaceTasks(
workspaceId: string,
): Promise<DesignGenerationTask[]> {
return requestData(
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/tasks`,
);
}
export async function resolveImageWorkspaceAssetUrl(contentPath: string): Promise<string> {
const token = await ensureHostApiToken();
const separator = contentPath.includes('?') ? '&' : '?';
return `${getHostApiBase()}${contentPath}${separator}token=${encodeURIComponent(token)}`;
}