feat: 统一 AI 设计 Workspace 与生成任务链路
需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。 实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
This commit is contained in:
@@ -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)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user