feat: add official AI-CAD iteration and modeling tools

This commit is contained in:
Your Name 2026-04-06 23:55:22 +08:00
parent b542a80f89
commit 5763ed1c94
10 changed files with 1752 additions and 107 deletions

View File

@ -1,5 +1,7 @@
import { AiCadWorkbench } from "@/components/ai-cad/ai-cad-workbench";
export const dynamic = "force-dynamic";
export default function AiCadPage() {
return <AiCadWorkbench />;
}

View File

@ -0,0 +1,122 @@
import { randomUUID } from "node:crypto";
import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import {
getKittyCadModelingCommandError,
isKittyCadModelingCommandSuccessful,
sendKittyCadModelingCommand,
takeKittyCadModelingSnapshot,
} from "@/lib/kittycad/modeling-session";
export const runtime = "nodejs";
export const maxDuration = 60;
type ModelingCommandRequest = {
captureSnapshot?: boolean;
cmd?: Record<string, unknown>;
cmdId?: string;
sessionId?: string;
snapshotFormat?: "jpeg" | "png";
};
function readOptionalRecord(value: unknown) {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: "未登录" },
{ status: 401 }
);
}
const body = (await request.json()) as ModelingCommandRequest;
const sessionId = String(body.sessionId || "").trim();
if (!sessionId) {
return NextResponse.json(
{ success: false, error: "缺少官方建模会话 ID" },
{ status: 400 }
);
}
const cmd = readOptionalRecord(body.cmd);
const cmdType = String(cmd?.type || "").trim();
if (!cmd || !cmdType) {
return NextResponse.json(
{ success: false, error: "缺少有效的官方建模命令" },
{ status: 400 }
);
}
const cmdId = String(body.cmdId || "").trim() || randomUUID();
const response = await sendKittyCadModelingCommand({
cmd: {
...cmd,
type: cmdType,
},
cmdId,
sessionId,
});
if (!isKittyCadModelingCommandSuccessful(response)) {
return NextResponse.json(
{
success: false,
error: getKittyCadModelingCommandError(response),
payload: response.errors || null,
},
{ status: 400 }
);
}
let snapshotUrl: string | null = null;
let snapshotError: string | null = null;
const shouldCaptureSnapshot =
body.captureSnapshot === true && cmdType !== "take_snapshot";
if (cmdType === "take_snapshot") {
const contents = String(response.resp?.data?.contents || "").trim();
const snapshotFormat = body.snapshotFormat || "png";
snapshotUrl = contents
? `data:${snapshotFormat === "jpeg" ? "image/jpeg" : "image/png"};base64,${contents}`
: null;
} else if (shouldCaptureSnapshot) {
try {
const snapshot = await takeKittyCadModelingSnapshot({
format: body.snapshotFormat || "png",
sessionId,
});
snapshotUrl = snapshot.snapshotUrl;
} catch (error) {
snapshotError =
error instanceof Error ? error.message : "获取官方建模快照失败";
}
}
return NextResponse.json({
success: true,
data: {
raw: response,
requestId: response.request_id || cmdId,
responseData: response.resp?.data || null,
responseType: response.resp?.type || cmdType,
snapshotError,
snapshotUrl,
},
});
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "发送官方建模命令失败",
},
{ status: 500 }
);
}
}

View File

@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { closeKittyCadModelingSession } from "@/lib/kittycad/modeling-session";
export const runtime = "nodejs";
export const maxDuration = 60;
type RouteParams = {
params: Promise<{
sessionId: string;
}>;
};
export async function DELETE(_request: Request, { params }: RouteParams) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: "未登录" },
{ status: 401 }
);
}
const { sessionId } = await params;
const normalizedSessionId = String(sessionId || "").trim();
if (!normalizedSessionId) {
return NextResponse.json(
{ success: false, error: "缺少 sessionId" },
{ status: 400 }
);
}
const result = await closeKittyCadModelingSession(normalizedSessionId);
return NextResponse.json({
success: true,
data: result,
});
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "关闭官方建模会话失败",
},
{ status: 500 }
);
}
}

View File

@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { createKittyCadModelingSession } from "@/lib/kittycad/modeling-session";
export const runtime = "nodejs";
export const maxDuration = 60;
export async function POST() {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: "未登录" },
{ status: 401 }
);
}
const modelingSession = await createKittyCadModelingSession();
return NextResponse.json({
success: true,
data: modelingSession,
});
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "创建官方建模会话失败",
},
{ status: 500 }
);
}
}

View File

@ -16,6 +16,8 @@ type CreateAiCadTaskRequest = {
outputFormat?: string;
projectName?: string;
iterationSourceTaskId?: string;
sourceCode?: string;
sourceRanges?: Record<string, unknown>[];
};
type KittyCadCreateResponse = {
@ -148,12 +150,6 @@ export async function POST(request: Request) {
const body = (await request.json()) as CreateAiCadTaskRequest;
const prompt = String(body.prompt || "").trim();
if (!prompt) {
return NextResponse.json(
{ success: false, error: "请输入 CAD 描述" },
{ status: 400 }
);
}
const pointsConfig = await getPointsSystemConfig();
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DModel))
@ -196,50 +192,76 @@ export async function POST(request: Request) {
const iterationSourceTaskId = String(
body.iterationSourceTaskId || ""
).trim();
const sourceCode = String(body.sourceCode || "");
const normalizedSourceCode = sourceCode.trim();
const sourceRanges = Array.isArray(body.sourceRanges)
? body.sourceRanges.filter(
(item): item is Record<string, unknown> =>
Boolean(item) && typeof item === "object" && !Array.isArray(item)
)
: [];
const config = await getKittyCadProviderConfig();
const generationMode = iterationSourceTaskId ? "iterate" : "create";
const result = iterationSourceTaskId
? await (async () => {
const sourceTask = await kittyCadRequest<KittyCadSourceTaskResponse>(
config,
{
method: "GET",
path: `/user/text-to-cad/${encodeURIComponent(iterationSourceTaskId)}`,
const generationMode =
iterationSourceTaskId || normalizedSourceCode ? "iterate" : "create";
if (generationMode === "create" && !prompt) {
return NextResponse.json(
{ success: false, error: "请输入 CAD 描述" },
{ status: 400 }
);
}
const iterationPrompt =
prompt || "Apply the edited KCL source and rebuild the CAD model.";
const result =
generationMode === "iterate"
? await (async () => {
const originalSourceCode =
normalizedSourceCode ||
(await (async () => {
if (!iterationSourceTaskId) {
return null;
}
const sourceTask =
await kittyCadRequest<KittyCadSourceTaskResponse>(config, {
method: "GET",
path: `/user/text-to-cad/${encodeURIComponent(iterationSourceTaskId)}`,
});
return extractSourceCodeFromTask(sourceTask);
})());
if (!originalSourceCode) {
throw new KittyCadApiError({
message:
"当前参考版本缺少可继续编辑的 KCL 源码,请先等待该版本完成,或重新选择一个 KittyCAD 版本。",
httpStatus: 409,
});
}
);
const originalSourceCode = extractSourceCodeFromTask(sourceTask);
if (!originalSourceCode) {
throw new KittyCadApiError({
message:
"当前参考版本缺少可继续编辑的 KCL 源码,请先等待该版本完成,或重新选择一个 KittyCAD 版本。",
httpStatus: 409,
return kittyCadRequest<KittyCadIterationResponse>(config, {
method: "POST",
path: "/ml/text-to-cad/iteration",
json: {
original_source_code: originalSourceCode,
prompt: iterationPrompt,
source_ranges: sourceRanges,
...(normalizedProjectName
? { project_name: normalizedProjectName }
: {}),
},
});
}
return kittyCadRequest<KittyCadIterationResponse>(config, {
})()
: await kittyCadRequest<KittyCadCreateResponse>(config, {
method: "POST",
path: "/ml/text-to-cad/iteration",
path: `/ai/text-to-cad/${encodeURIComponent(outputFormat)}?kcl=true`,
json: {
original_source_code: originalSourceCode,
prompt,
source_ranges: [],
...(normalizedProjectName
? { project_name: normalizedProjectName }
: {}),
},
});
})()
: await kittyCadRequest<KittyCadCreateResponse>(config, {
method: "POST",
path: `/ai/text-to-cad/${encodeURIComponent(outputFormat)}?kcl=true`,
json: {
prompt,
...(normalizedProjectName
? { project_name: normalizedProjectName }
: {}),
},
});
const taskId = String(result.id || "").trim();
if (!taskId) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,391 @@
import "server-only";
import { randomUUID } from "node:crypto";
import { deserialize } from "bson";
import WebSocket, { type RawData } from "ws";
import { getKittyCadProviderConfig } from "./server";
type KittyCadModelingCommand = Record<string, unknown> & {
type: string;
};
type KittyCadSuccessResponse = {
request_id?: string | null;
resp?: {
data?: Record<string, unknown> | null;
type?: string | null;
} | null;
success: true;
};
type KittyCadFailureResponse = {
errors?: Record<string, unknown>[];
request_id?: string | null;
success: false;
};
export type KittyCadModelingSocketResponse =
| KittyCadSuccessResponse
| KittyCadFailureResponse;
type PendingRequest = {
reject: (error: Error) => void;
resolve: (value: KittyCadModelingSocketResponse) => void;
timeout: NodeJS.Timeout;
};
type ModelingSessionState = {
createdAt: string;
id: string;
lastUsedAt: number;
pending: Map<string, PendingRequest>;
pingTimer: NodeJS.Timeout;
socket: WebSocket;
};
const MODELING_COMMAND_TIMEOUT_MS = 20_000;
const MODELING_PING_INTERVAL_MS = 20_000;
const DEFAULT_WS_PATH = "/ws/modeling/commands";
function getModelingSessions() {
const globalState = globalThis as Record<string, unknown>;
if (!globalState.__kittyCadModelingSessions) {
globalState.__kittyCadModelingSessions = new Map<
string,
ModelingSessionState
>();
}
return globalState.__kittyCadModelingSessions as Map<
string,
ModelingSessionState
>;
}
function toWebSocketUrl(baseUrl: string) {
const parsed = new URL(baseUrl);
parsed.protocol = parsed.protocol === "http:" ? "ws:" : "wss:";
parsed.pathname = DEFAULT_WS_PATH;
parsed.search = "";
parsed.hash = "";
return parsed.toString();
}
function toBuffer(raw: RawData): Buffer {
if (typeof raw === "string") {
return Buffer.from(raw, "utf8");
}
if (Buffer.isBuffer(raw)) {
return raw;
}
if (raw instanceof ArrayBuffer) {
return Buffer.from(raw);
}
if (Array.isArray(raw)) {
return Buffer.concat(raw.map((chunk) => toBuffer(chunk)));
}
return Buffer.from(raw);
}
function parseSocketMessage(raw: RawData): KittyCadModelingSocketResponse {
const buffer = toBuffer(raw);
const text = buffer.toString("utf8").trim();
if (text) {
try {
return JSON.parse(text) as KittyCadModelingSocketResponse;
} catch (_error) {
// Fall through to BSON parsing.
}
}
return deserialize(buffer) as KittyCadModelingSocketResponse;
}
function getSocketErrorMessage(error: unknown) {
return error instanceof Error ? error.message : "官方建模连接异常断开";
}
function formatModelingErrors(errors: unknown) {
if (!Array.isArray(errors) || errors.length === 0) {
return "官方建模命令执行失败";
}
return (
errors
.map((item) => {
if (!item || typeof item !== "object") {
return null;
}
const record = item as Record<string, unknown>;
if (typeof record.message === "string" && record.message.trim()) {
return record.message.trim();
}
if (typeof record.error === "string" && record.error.trim()) {
return record.error.trim();
}
return null;
})
.filter((item): item is string => Boolean(item))
.join(";") || "官方建模命令执行失败"
);
}
function rejectAllPendingRequests(
state: ModelingSessionState,
message: string
) {
for (const [requestId, pending] of state.pending.entries()) {
clearTimeout(pending.timeout);
pending.reject(new Error(message));
state.pending.delete(requestId);
}
}
function destroySession(sessionId: string, reason: string) {
const sessions = getModelingSessions();
const state = sessions.get(sessionId);
if (!state) {
return;
}
clearInterval(state.pingTimer);
rejectAllPendingRequests(state, reason);
sessions.delete(sessionId);
if (
state.socket.readyState === WebSocket.OPEN ||
state.socket.readyState === WebSocket.CONNECTING
) {
state.socket.close();
}
}
function getRequiredSession(sessionId: string) {
const sessions = getModelingSessions();
const state = sessions.get(sessionId);
if (!state) {
throw new Error("官方建模会话不存在或已过期,请重新连接");
}
if (state.socket.readyState !== WebSocket.OPEN) {
destroySession(sessionId, "官方建模会话已断开,请重新连接");
throw new Error("官方建模会话已断开,请重新连接");
}
state.lastUsedAt = Date.now();
return state;
}
export async function createKittyCadModelingSession() {
const config = await getKittyCadProviderConfig();
const sessionId = randomUUID();
const createdAt = new Date().toISOString();
const socketUrl = toWebSocketUrl(config.baseUrl);
const sessionState = await new Promise<ModelingSessionState>(
(resolve, reject) => {
const socket = new WebSocket(socketUrl, {
headers: {
Authorization: `Bearer ${String(config.apiKey || "").replace(/\s+/g, "")}`,
},
});
const cleanup = () => {
socket.removeListener("error", handleError);
socket.removeListener("unexpected-response", handleUnexpectedResponse);
socket.removeListener("open", handleOpen);
};
const handleError = (error: Error) => {
cleanup();
reject(error);
};
const handleUnexpectedResponse = (
_request: unknown,
response: {
on: (event: string, listener: (chunk?: Buffer) => void) => void;
statusCode?: number;
statusMessage?: string;
}
) => {
const chunks: Buffer[] = [];
response.on("data", (chunk) => {
if (chunk) {
chunks.push(Buffer.from(chunk));
}
});
response.on("end", () => {
cleanup();
const responseText = Buffer.concat(chunks).toString("utf8").trim();
reject(
new Error(
responseText ||
`官方建模连接失败(HTTP ${response.statusCode || 502}${response.statusMessage ? ` ${response.statusMessage}` : ""})`
)
);
});
};
const handleOpen = () => {
cleanup();
const pingTimer = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "ping" }));
}
}, MODELING_PING_INTERVAL_MS);
resolve({
createdAt,
id: sessionId,
lastUsedAt: Date.now(),
pending: new Map(),
pingTimer,
socket,
});
};
socket.once("error", handleError);
socket.once("unexpected-response", handleUnexpectedResponse);
socket.once("open", handleOpen);
}
);
sessionState.socket.on("message", (raw: RawData) => {
try {
const response = parseSocketMessage(raw);
const requestId = String(response.request_id || "").trim();
if (!requestId) {
return;
}
const pending = sessionState.pending.get(requestId);
if (!pending) {
return;
}
clearTimeout(pending.timeout);
sessionState.pending.delete(requestId);
pending.resolve(response);
} catch (error) {
console.error("[KittyCADModeling] failed to parse socket message", error);
}
});
sessionState.socket.on("close", () => {
destroySession(sessionId, "官方建模会话已关闭,请重新连接");
});
sessionState.socket.on("error", (error: Error) => {
destroySession(sessionId, getSocketErrorMessage(error));
});
getModelingSessions().set(sessionId, sessionState);
return {
createdAt,
sessionId,
};
}
export async function sendKittyCadModelingCommand(params: {
cmd: KittyCadModelingCommand;
cmdId?: string;
sessionId: string;
}) {
const state = getRequiredSession(params.sessionId);
const requestId = String(params.cmdId || "").trim() || randomUUID();
if (state.pending.has(requestId)) {
throw new Error("官方建模命令 ID 冲突,请重试");
}
const payload = {
cmd: params.cmd,
cmd_id: requestId,
type: "modeling_cmd_req",
};
return await new Promise<KittyCadModelingSocketResponse>(
(resolve, reject) => {
const timeout = setTimeout(() => {
state.pending.delete(requestId);
reject(new Error("官方建模命令超时,请稍后重试"));
}, MODELING_COMMAND_TIMEOUT_MS);
state.pending.set(requestId, {
reject,
resolve,
timeout,
});
state.socket.send(JSON.stringify(payload), (error?: Error) => {
if (!error) {
return;
}
clearTimeout(timeout);
state.pending.delete(requestId);
reject(new Error(getSocketErrorMessage(error)));
});
}
);
}
export async function takeKittyCadModelingSnapshot(params: {
format?: "jpeg" | "png";
sessionId: string;
}) {
const format = params.format || "png";
const response = await sendKittyCadModelingCommand({
cmd: {
format,
type: "take_snapshot",
},
sessionId: params.sessionId,
});
if (!response.success) {
throw new Error(formatModelingErrors(response.errors));
}
const contents = String(response.resp?.data?.contents || "").trim();
if (!contents) {
return {
requestId: response.request_id || null,
snapshotUrl: null,
};
}
const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
return {
requestId: response.request_id || null,
snapshotUrl: `data:${mimeType};base64,${contents}`,
};
}
export function closeKittyCadModelingSession(sessionId: string) {
destroySession(sessionId, "官方建模会话已手动关闭");
return {
sessionId,
};
}
export function isKittyCadModelingCommandSuccessful(
response: KittyCadModelingSocketResponse
): response is KittyCadSuccessResponse {
return response.success;
}
export function getKittyCadModelingCommandError(response: unknown) {
if (!response || typeof response !== "object") {
return "官方建模命令执行失败";
}
const record = response as KittyCadFailureResponse;
return formatModelingErrors(record.errors);
}

View File

@ -59,6 +59,7 @@
"@xenova/transformers": "^2.17.0",
"ai": "^5.0.52",
"bcrypt-ts": "^5.0.2",
"bson": "^6.10.4",
"class-variance-authority": "^0.7.1",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
@ -116,6 +117,7 @@
"tokenlens": "1.3.0",
"use-stick-to-bottom": "^1.1.1",
"usehooks-ts": "^3.1.0",
"ws": "^8.18.3",
"xlsx": "^0.18.5",
"zod": "^3.25.76",
"zustand": "5.0.4"
@ -135,6 +137,7 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@types/sharp": "^0.32.0",
"@types/three": "^0.182.0",
"@types/ws": "^8.18.1",
"drizzle-kit": "^0.25.0",
"postcss": "^8",
"tailwindcss": "^4.1.13",

31
pnpm-lock.yaml generated
View File

@ -125,6 +125,9 @@ importers:
bcrypt-ts:
specifier: ^5.0.2
version: 5.0.3
bson:
specifier: ^6.10.4
version: 6.10.4
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -296,6 +299,9 @@ importers:
usehooks-ts:
specifier: ^3.1.0
version: 3.1.1(react@19.0.0-rc-45804af1-20241021)
ws:
specifier: ^8.18.3
version: 8.20.0(bufferutil@4.0.9)
xlsx:
specifier: ^0.18.5
version: 0.18.5
@ -348,6 +354,9 @@ importers:
'@types/three':
specifier: ^0.182.0
version: 0.182.0
'@types/ws':
specifier: ^8.18.1
version: 8.18.1
drizzle-kit:
specifier: ^0.25.0
version: 0.25.0
@ -3216,6 +3225,9 @@ packages:
'@types/webxr@0.5.24':
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@typespec/ts-http-runtime@0.3.4':
resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==}
engines: {node: '>=20.0.0'}
@ -3455,6 +3467,10 @@ packages:
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
bson@6.10.4:
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
engines: {node: '>=16.20.1'}
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
@ -6067,8 +6083,8 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.18.1:
resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
ws@8.20.0:
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@ -9298,6 +9314,10 @@ snapshots:
'@types/webxr@0.5.24': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.13.10
'@typespec/ts-http-runtime@0.3.4':
dependencies:
http-proxy-agent: 7.0.2
@ -9356,7 +9376,7 @@ snapshots:
dependencies:
'@neondatabase/serverless': 0.9.5
bufferutil: 4.0.9
ws: 8.18.1(bufferutil@4.0.9)
ws: 8.20.0(bufferutil@4.0.9)
transitivePeerDependencies:
- utf-8-validate
optional: true
@ -9550,6 +9570,8 @@ snapshots:
dependencies:
balanced-match: 1.0.2
bson@6.10.4: {}
buffer-crc32@0.2.13: {}
buffer-equal-constant-time@1.0.1: {}
@ -12619,10 +12641,9 @@ snapshots:
wrappy@1.0.2: {}
ws@8.18.1(bufferutil@4.0.9):
ws@8.20.0(bufferutil@4.0.9):
optionalDependencies:
bufferutil: 4.0.9
optional: true
wsl-utils@0.1.0:
dependencies:

View File

@ -39,6 +39,20 @@ export type AiCadWorkspaceDocument = {
updatedAt?: string | null;
};
export type AiCadModelingSession = {
sessionId: string;
createdAt?: string | null;
};
export type AiCadModelingCommandPayload = {
requestId?: string | null;
responseType?: string | null;
responseData?: Record<string, any> | null;
snapshotUrl?: string | null;
snapshotError?: string | null;
raw?: any;
};
export const aiCadGenerationService = {
async listWorkspaces(): Promise<ApiResponse<AiCadWorkspaceSummary[]>> {
try {
@ -136,10 +150,12 @@ export const aiCadGenerationService = {
},
async createTask(params: {
prompt: string;
prompt?: string;
outputFormat?: string;
projectName?: string;
iterationSourceTaskId?: string;
sourceCode?: string;
sourceRanges?: Record<string, unknown>[];
}): Promise<
ApiResponse<{
taskId: string;
@ -165,6 +181,55 @@ export const aiCadGenerationService = {
}
},
async createModelingSession(): Promise<ApiResponse<AiCadModelingSession>> {
try {
const response = await fetch("/api/ai-cad/modeling/session", {
method: "POST",
});
const data = await response.json();
return data;
} catch (_error) {
return { success: false, error: "创建官方建模会话失败" };
}
},
async closeModelingSession(
sessionId: string
): Promise<ApiResponse<{ sessionId: string }>> {
try {
const response = await fetch(
`/api/ai-cad/modeling/session/${encodeURIComponent(sessionId)}`,
{
method: "DELETE",
}
);
const data = await response.json();
return data;
} catch (_error) {
return { success: false, error: "关闭官方建模会话失败" };
}
},
async sendModelingCommand(params: {
sessionId: string;
cmd: Record<string, unknown>;
cmdId?: string;
captureSnapshot?: boolean;
snapshotFormat?: "jpeg" | "png";
}): Promise<ApiResponse<AiCadModelingCommandPayload>> {
try {
const response = await fetch("/api/ai-cad/modeling/command", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (_error) {
return { success: false, error: "发送官方建模命令失败" };
}
},
async getTask(taskId: string): Promise<
ApiResponse<{
taskId: string;