freeform-modeling-mcp 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/index.cjs +21 -0
- package/package.json +48 -0
- package/src/cli.ts +47 -0
- package/src/commands/start.ts +88 -0
- package/src/commands/status.ts +81 -0
- package/src/dev.ts +24 -0
- package/src/lib/fileproxy/index.ts +505 -0
- package/src/lib/fileproxy/proxy-server.js +347 -0
- package/src/lib/gltf2obj/compress.js +33 -0
- package/src/lib/gltf2obj/engine/core/core.js +34 -0
- package/src/lib/gltf2obj/engine/core/eventnotifier.js +39 -0
- package/src/lib/gltf2obj/engine/core/taskrunner.js +88 -0
- package/src/lib/gltf2obj/engine/export/exporter.js +38 -0
- package/src/lib/gltf2obj/engine/export/exporterbase.js +86 -0
- package/src/lib/gltf2obj/engine/export/exportermodel.js +117 -0
- package/src/lib/gltf2obj/engine/export/exporterobj.js +147 -0
- package/src/lib/gltf2obj/engine/geometry/box3d.js +60 -0
- package/src/lib/gltf2obj/engine/geometry/coord2d.js +35 -0
- package/src/lib/gltf2obj/engine/geometry/coord3d.js +126 -0
- package/src/lib/gltf2obj/engine/geometry/coord4d.js +15 -0
- package/src/lib/gltf2obj/engine/geometry/geometry.js +56 -0
- package/src/lib/gltf2obj/engine/geometry/matrix.js +440 -0
- package/src/lib/gltf2obj/engine/geometry/octree.js +160 -0
- package/src/lib/gltf2obj/engine/geometry/quaternion.js +83 -0
- package/src/lib/gltf2obj/engine/geometry/transformation.js +63 -0
- package/src/lib/gltf2obj/engine/geometry/tween.js +31 -0
- package/src/lib/gltf2obj/engine/import/importer.js +270 -0
- package/src/lib/gltf2obj/engine/import/importerbase.js +115 -0
- package/src/lib/gltf2obj/engine/import/importerfiles.js +139 -0
- package/src/lib/gltf2obj/engine/import/importergltf.js +1045 -0
- package/src/lib/gltf2obj/engine/import/importerutils.js +102 -0
- package/src/lib/gltf2obj/engine/io/binaryreader.js +93 -0
- package/src/lib/gltf2obj/engine/io/binarywriter.js +92 -0
- package/src/lib/gltf2obj/engine/io/bufferutils.js +85 -0
- package/src/lib/gltf2obj/engine/io/externallibs.js +42 -0
- package/src/lib/gltf2obj/engine/io/fileutils.js +120 -0
- package/src/lib/gltf2obj/engine/io/textwriter.js +41 -0
- package/src/lib/gltf2obj/engine/main.js +19 -0
- package/src/lib/gltf2obj/engine/model/color.js +130 -0
- package/src/lib/gltf2obj/engine/model/generator.js +433 -0
- package/src/lib/gltf2obj/engine/model/material.js +243 -0
- package/src/lib/gltf2obj/engine/model/mesh.js +173 -0
- package/src/lib/gltf2obj/engine/model/meshbuffer.js +233 -0
- package/src/lib/gltf2obj/engine/model/meshinstance.js +128 -0
- package/src/lib/gltf2obj/engine/model/meshutils.js +64 -0
- package/src/lib/gltf2obj/engine/model/model.js +195 -0
- package/src/lib/gltf2obj/engine/model/modelfinalization.js +377 -0
- package/src/lib/gltf2obj/engine/model/modelutils.js +117 -0
- package/src/lib/gltf2obj/engine/model/node.js +178 -0
- package/src/lib/gltf2obj/engine/model/object.js +90 -0
- package/src/lib/gltf2obj/engine/model/property.js +86 -0
- package/src/lib/gltf2obj/engine/model/quantities.js +46 -0
- package/src/lib/gltf2obj/engine/model/topology.js +139 -0
- package/src/lib/gltf2obj/engine/model/triangle.js +99 -0
- package/src/lib/gltf2obj/engine/threejs/threemodelloader.js +85 -0
- package/src/lib/gltf2obj/handler.js +63 -0
- package/src/lib/gltf2obj/index.js +47 -0
- package/src/lib/logger.ts +304 -0
- package/src/lib/print.ts +37 -0
- package/src/lib/utils.ts +34 -0
- package/src/lib/wsbridge/bridge.ts +639 -0
- package/src/lib/wsbridge/lock.ts +258 -0
- package/src/llmClient.ts +245 -0
- package/src/prompt.ts +187 -0
- package/src/server.ts +125 -0
- package/src/tools/assets-tool.ts +314 -0
- package/src/tools/basic-tools.ts +531 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/material-tools.ts +88 -0
- package/src/tools/modeling/auxiliary-curve-tool.ts +44 -0
- package/src/tools/modeling/find-face.ts +38 -0
- package/src/tools/modeling/sweep-tool.ts +60 -0
- package/src/tools/modeling/utils.ts +181 -0
- package/src/tools/modeling-tools.ts +135 -0
- package/src/tools/screenshot-tools.ts +107 -0
- package/src/tools/tools-info.ts +107 -0
- package/src/tools/tripo3d-tools.ts +496 -0
- package/src/types/index.ts +25 -0
- package/src/types/tripo3d.ts +38 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { Context } from '../types';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import logger from '../lib/logger';
|
|
4
|
+
import { Tripo3DTaskStatusResponse } from '../types/tripo3d';
|
|
5
|
+
import { llmClient } from '../llmClient';
|
|
6
|
+
import { gltf2obj } from '../lib/gltf2obj/index.js'; // 导入GLB转OBJ的方法
|
|
7
|
+
import { imageContent, UserError } from 'fastmcp';
|
|
8
|
+
|
|
9
|
+
export const apiKey = process.env.TRIPO3D_API_KEY;
|
|
10
|
+
|
|
11
|
+
// Tool: Get Tripo3D Status
|
|
12
|
+
export const getTripo3DStatusTool = {
|
|
13
|
+
name: 'get_tripo3d_status',
|
|
14
|
+
description: 'Check if Tripo3D is configured and available for use by verifying the presence of required API keys and configuration',
|
|
15
|
+
parameters: z.object({}), // No parameters needed
|
|
16
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
17
|
+
logger.info(`Tool executed: get_tripo3d_status`);
|
|
18
|
+
try {
|
|
19
|
+
// Check if Tripo3D API key is provided in environment variables
|
|
20
|
+
const tripo3dApiKey = apiKey;
|
|
21
|
+
|
|
22
|
+
if (!tripo3dApiKey) {
|
|
23
|
+
throw new UserError("Tripo3D is not fully configured. The API key is missing from the environment variables. Please set TRIPO3D_API_KEY in your environment to use Tripo3D features.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return "Tripo3D is properly configured and ready to use. API key is present in the environment.";
|
|
27
|
+
} catch (e) {
|
|
28
|
+
const errorMessage = `Error checking Tripo3D status: ${e instanceof Error ? e.message : String(e)}`;
|
|
29
|
+
logger.error(errorMessage);
|
|
30
|
+
context.log.error(errorMessage);
|
|
31
|
+
return errorMessage;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const createTripo3DJobTool = {
|
|
37
|
+
name: 'create_tripo3d_job',
|
|
38
|
+
description: 'Generate a 3D model using tripo3d service from text or images',
|
|
39
|
+
parameters: z.object({
|
|
40
|
+
// 任务类型标识符 (隐式参数,根据输入自动确定)
|
|
41
|
+
|
|
42
|
+
// 文本到模型的基本参数
|
|
43
|
+
prompt: z.string().optional().describe('Text input that directs the model generation. Required for Text-to-3D mode. Max 1024 characters.'),
|
|
44
|
+
negative_prompt: z.string().optional().describe('Text input that provides a reverse direction to assist in generating content contrasting with the original prompt. Max 255 characters.'),
|
|
45
|
+
|
|
46
|
+
// 图像到模型的基本参数
|
|
47
|
+
// condition_mode: z.enum(['fuse', 'concat']).optional().default('concat').describe('For fuse mode, features from multiple images are fused. For concat mode, multi-view images of the same object are used.'),
|
|
48
|
+
|
|
49
|
+
// 随机种子控制
|
|
50
|
+
model_seed: z.number().int().min(0).max(65535).optional().describe('Random seed for model geometry generation. Using the same seed produces identical models.'),
|
|
51
|
+
image_seed: z.number().int().min(0).max(65535).optional().describe('Random seed used for the process based on the prompt.'),
|
|
52
|
+
texture_seed: z.number().int().min(0).max(65535).optional().describe('Random seed for texture generation. Using the same seed produces identical textures.'),
|
|
53
|
+
|
|
54
|
+
// 输出格式控制
|
|
55
|
+
// output_format: z.enum(['glb', 'usdz', 'fbx', 'obj', 'stl']).optional().default('glb').describe('Format of the output 3D model file.'),
|
|
56
|
+
|
|
57
|
+
// 模型版本
|
|
58
|
+
// model_version: z.enum(['v2.5-20250123', 'v2.0-20240919', 'v1.4-20240625']).optional().default('v2.5-20250123').describe('Tripo3D model version to use for generation.'),
|
|
59
|
+
|
|
60
|
+
// 模型质量控制参数 (v2.0-20240919及以上版本可用)
|
|
61
|
+
face_limit: z.number().int().positive().optional().describe('Limits the number of faces on the output model. If not set, it will be determined adaptively.'),
|
|
62
|
+
// texture: z.boolean().optional().default(true).describe('Enable texturing. Set false to get a base model without textures.'),
|
|
63
|
+
// pbr: z.boolean().optional().default(true).describe('Enable PBR materials. If true, texture will be forced to true.'),
|
|
64
|
+
texture_quality: z.enum(['standard', 'detailed']).optional().default('standard').describe('Controls texture quality. "detailed" provides high-resolution textures.'),
|
|
65
|
+
auto_size: z.boolean().optional().default(false).describe('Automatically scale the model to real-world dimensions (in meters).'),
|
|
66
|
+
style: z.enum([
|
|
67
|
+
'person:person2cartoon',
|
|
68
|
+
'object:clay',
|
|
69
|
+
'object:steampunk',
|
|
70
|
+
'animal:venom',
|
|
71
|
+
'object:barbie',
|
|
72
|
+
'object:christmas',
|
|
73
|
+
'gold',
|
|
74
|
+
'ancient_bronze'
|
|
75
|
+
]).optional().describe(
|
|
76
|
+
'Defines the artistic style to be applied to the 3D model. Available styles:\n' +
|
|
77
|
+
'- person:person2cartoon: Transforms the model into a cartoon-style version of input character\n' +
|
|
78
|
+
'- object:clay: Applies a clay-like appearance to the object\n' +
|
|
79
|
+
'- object:steampunk: Applies a steampunk aesthetic with metallic gears and vintage details\n' +
|
|
80
|
+
'- animal:venom: Applies a venom-like, dark, and glossy appearance to the animal model (warning: may be horrific)\n' +
|
|
81
|
+
'- object:barbie: Applies a barbie style to the object\n' +
|
|
82
|
+
'- object:christmas: Applies a christmas style to the object\n' +
|
|
83
|
+
'- gold: Applies a gold style to the object\n' +
|
|
84
|
+
'- ancient_bronze: Applies a ancient bronze style to the object'
|
|
85
|
+
),
|
|
86
|
+
quad: z.boolean().optional().default(false).describe('Enable quad mesh output. Forces the output to be an FBX model.'),
|
|
87
|
+
|
|
88
|
+
// 姿态控制
|
|
89
|
+
pose: z.enum(['T-pose', 'A-pose']).optional().describe('Set the model in a specific pose.'),
|
|
90
|
+
pose_params: z.string().optional().describe('Custom pose parameters in format "A:B:C:D:E" where A=head-to-body height ratio, B=head-to-body width ratio, C=legs-to-body height ratio, D=arms-to-body length ratio, E=span of two legs(0-15).')
|
|
91
|
+
}),
|
|
92
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
93
|
+
// 记录工具执行
|
|
94
|
+
const logMethod = args.prompt ?
|
|
95
|
+
`text prompt: "${args.prompt.substring(0, 50)}${args.prompt.length > 50 ? '...' : ''}"` :
|
|
96
|
+
`${args.input_image_urls?.length || 0} image(s)`;
|
|
97
|
+
logger.info(`Tool executed: create_tripo3d_job with ${logMethod}`);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// 参数验证
|
|
101
|
+
if (!args.prompt) {
|
|
102
|
+
return "Error: Either 'prompt' (for Text-to-3D) or 'input_image_urls' (for Image-to-3D) must be provided.";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 检查是否有客户端连接
|
|
106
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
107
|
+
logger.warn('No modeling software clients connected');
|
|
108
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 准备请求参数 - 直接传递所有参数
|
|
112
|
+
const params = {
|
|
113
|
+
...args,
|
|
114
|
+
pbr: false,
|
|
115
|
+
texture: true,
|
|
116
|
+
model_version: "v2.5-20250123"
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// 处理特殊姿态参数
|
|
120
|
+
if (args.pose) {
|
|
121
|
+
let poseString = args.pose;
|
|
122
|
+
if (args.pose_params) {
|
|
123
|
+
poseString += `:${args.pose_params}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 如果提示词已存在,将姿态信息附加到提示词末尾
|
|
127
|
+
if (args.prompt) {
|
|
128
|
+
params.prompt = `${args.prompt}, ${poseString}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 从参数中移除这些特殊处理的字段,防止 API 混淆
|
|
132
|
+
delete params.pose;
|
|
133
|
+
delete params.pose_params;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
logger.debug(`Sending create_tripo3d_job command to client with params: ${JSON.stringify(params)}`);
|
|
137
|
+
|
|
138
|
+
// 从 params 提取参数,映射到 Tripo3D API 需要的格式
|
|
139
|
+
const requestData: Record<string, any> = {
|
|
140
|
+
type: 'text_to_model',
|
|
141
|
+
model_version: params.model_version || 'v2.5-20250123', // 默认使用最新版本
|
|
142
|
+
prompt: params.prompt
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// 添加可选参数
|
|
146
|
+
if (params.negative_prompt) {
|
|
147
|
+
requestData.negative_prompt = params.negative_prompt;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (params.image_seed !== undefined) {
|
|
151
|
+
requestData.image_seed = params.image_seed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (params.model_seed !== undefined) {
|
|
155
|
+
requestData.model_seed = params.model_seed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// v2.0及以上版本可用的特殊参数
|
|
159
|
+
if (requestData.model_version && requestData.model_version.startsWith('v2')) {
|
|
160
|
+
if (params.face_limit !== undefined) {
|
|
161
|
+
requestData.face_limit = params.face_limit;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (params.texture !== undefined) {
|
|
165
|
+
requestData.texture = params.texture;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (params.pbr !== undefined) {
|
|
169
|
+
requestData.pbr = params.pbr;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (params.texture_seed !== undefined) {
|
|
173
|
+
requestData.texture_seed = params.texture_seed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (params.texture_quality) {
|
|
177
|
+
requestData.texture_quality = params.texture_quality;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (params.auto_size !== undefined) {
|
|
181
|
+
requestData.auto_size = params.auto_size;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (params.style) {
|
|
185
|
+
requestData.style = params.style;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (params.quad !== undefined) {
|
|
189
|
+
requestData.quad = params.quad;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 处理姿势设置
|
|
194
|
+
// 如果有 TAPose 参数,根据值添加 T-pose 或 A-pose
|
|
195
|
+
if (params.TAPose !== undefined) {
|
|
196
|
+
if (typeof params.TAPose === 'string') {
|
|
197
|
+
// 如果是字符串,直接使用(假设用户已经提供了正确格式如 "T-pose:1:1:1:1:9")
|
|
198
|
+
const poseSuffix = params.TAPose;
|
|
199
|
+
if (!requestData.prompt.includes(poseSuffix)) {
|
|
200
|
+
requestData.prompt = `${requestData.prompt}, ${poseSuffix}`;
|
|
201
|
+
}
|
|
202
|
+
} else if (params.TAPose === true) {
|
|
203
|
+
// 如果是布尔值 true,使用默认 T-pose
|
|
204
|
+
if (!requestData.prompt.includes('T-pose') && !requestData.prompt.includes('A-pose')) {
|
|
205
|
+
requestData.prompt = `${requestData.prompt}, T-pose`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
logger.info('Sending request to Tripo3D API:', requestData);
|
|
211
|
+
|
|
212
|
+
// 调用 Tripo3D API
|
|
213
|
+
const response = await fetch('https://api.tripo3d.ai/v2/openapi/task', {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: {
|
|
216
|
+
'Content-Type': 'application/json',
|
|
217
|
+
'Authorization': `Bearer ${apiKey}`
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify(requestData)
|
|
220
|
+
});
|
|
221
|
+
// 通过 llmClient 发送命令到客户端
|
|
222
|
+
// const result = await llmClient.sendCommand("create_tripo3d_job", params);
|
|
223
|
+
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
const errorData = await response.json();
|
|
226
|
+
throw new Error(errorData.message);
|
|
227
|
+
// return {
|
|
228
|
+
// status: "error",
|
|
229
|
+
// message: `Tripo3D API error: ${response.status} - ${errorData.code || 'Unknown error code'} - ${errorData.message || 'Unknown error message'}`
|
|
230
|
+
// };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result = await response.json();
|
|
234
|
+
|
|
235
|
+
if (result.code !== 0) {
|
|
236
|
+
return `Error creating Tripo3D job, HTTP Status Code${result.code}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return `✅ 3D model generation job submitted successfully!
|
|
240
|
+
|
|
241
|
+
Request ID: ${result.data.task_id}
|
|
242
|
+
|
|
243
|
+
Your 3D model is being generated. This process typically takes 1-5 minutes.
|
|
244
|
+
Use the 'poll_tripo3d_job_status' tool with this task_id to check the status.
|
|
245
|
+
|
|
246
|
+
Generation parameters:
|
|
247
|
+
${params.prompt ? `- Prompt: "${params.prompt}"` : ''}
|
|
248
|
+
${params.input_image_urls ? `- Images: ${params.input_image_urls.length} image(s)` : ''}
|
|
249
|
+
- Mode: Text-to-3D
|
|
250
|
+
- Format: obj
|
|
251
|
+
- Quality: ${params.quality || 'medium'}
|
|
252
|
+
- Material: ${params.material || 'PBR'}
|
|
253
|
+
|
|
254
|
+
When the model is ready, use the 'import_generated_asset' tool to import it into your scene.`
|
|
255
|
+
|
|
256
|
+
} catch (e) {
|
|
257
|
+
// 处理执行期间的任何错误
|
|
258
|
+
const errorMessage = `Error creating Tripo3D job: ${e instanceof Error ? e.message : String(e)}`;
|
|
259
|
+
logger.error(errorMessage);
|
|
260
|
+
context.log.error(errorMessage);
|
|
261
|
+
return errorMessage;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// 工具: 检查 Tripo3D 任务状态 - 仅传递参数
|
|
267
|
+
export const pollTripo3DJobStatusTool = {
|
|
268
|
+
name: 'poll_tripo3d_job_status',
|
|
269
|
+
description: 'Check the status of a Tripo3D 3D model generation job',
|
|
270
|
+
parameters: z.object({
|
|
271
|
+
task_id: z.string().optional().describe('The request ID for FAL_AI mode'),
|
|
272
|
+
}),
|
|
273
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
274
|
+
logger.info(`Tool executed: poll_tripo3d_job_status with params: ${JSON.stringify(args)}`);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// 检查是否有客户端连接
|
|
278
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
279
|
+
logger.warn('No modeling software clients connected');
|
|
280
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 检查是否提供了必要参数
|
|
284
|
+
if (!args.task_id) {
|
|
285
|
+
return "Error: 'task_id' must be provided.";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
logger.debug(`Sending poll_tripo3d_job_status command to client`);
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
const url = `https://api.tripo3d.ai/v2/openapi/task/${args.task_id}`;
|
|
292
|
+
|
|
293
|
+
const options = {
|
|
294
|
+
headers: {
|
|
295
|
+
'Authorization': 'Bearer ' + apiKey
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const result: Tripo3DTaskStatusResponse = await fetch(url, options)
|
|
300
|
+
.then(response => {
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
throw new Error(`HTTP error! status: ${response.status},info : ${response.statusText}`);
|
|
303
|
+
}
|
|
304
|
+
return response.json();
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// 通过 llmClient 发送命令到客户端
|
|
308
|
+
// const result: QueueStatus = await llmClient.sendCommand("poll_tripo3d_job_status", args);
|
|
309
|
+
|
|
310
|
+
logger.info(`Received status response from client`, JSON.stringify(result));
|
|
311
|
+
|
|
312
|
+
if (result.data.status === 'success') {
|
|
313
|
+
return `✅ 3D model generation completed successfully!\nYou can now import this model using the 'import_generated_asset' tool with the task_id.`
|
|
314
|
+
} else {
|
|
315
|
+
return `⏳ 3D model generation is still ${result.data.status === 'running' ? `in progress (progress:${result.data.progress}%), (running left time:${result.data.running_left_time}s)` : 'queued'}.\nPlease wait and check again in a minute. Most models take 1-5 minutes to generate`
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
const errorMessage = `Error checking Tripo3D job status: ${e instanceof Error ? e.message : String(e)}`;
|
|
319
|
+
logger.error(errorMessage);
|
|
320
|
+
context.log.error(errorMessage);
|
|
321
|
+
return errorMessage;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
interface IImportProgress {
|
|
327
|
+
task_id: string;
|
|
328
|
+
modelData: Tripo3DTaskStatusResponse;
|
|
329
|
+
progress: number;
|
|
330
|
+
data?: string;
|
|
331
|
+
error?: Error;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const importProgressMap = new Map<string, IImportProgress>();
|
|
335
|
+
|
|
336
|
+
// 工具: 导入生成的 3D 模型 - 仅传递参数
|
|
337
|
+
export const importGeneratedAssetTool = {
|
|
338
|
+
name: 'import_generated_asset',
|
|
339
|
+
description: 'Import a 3D model generated by Tripo3D into the scene',
|
|
340
|
+
parameters: z.object({
|
|
341
|
+
name: z.string().describe('The name to give to the imported object in the scene'),
|
|
342
|
+
task_id: z.string().describe('For FAL_AI mode: The request ID of the completed Tripo3D job'),
|
|
343
|
+
|
|
344
|
+
// 添加变换参数
|
|
345
|
+
location: z.array(z.number()).describe('[x, y, z] center position coordinates for the imported model'),
|
|
346
|
+
dimensions: z.array(z.number()).describe('Optional [width, height, depth] absolute dimensions for the imported model'),
|
|
347
|
+
rotation: z.array(z.number()).optional().describe('Optional [x, y, z] rotation in degrees. Following the Z-up coordinate system: rotation around X-axis tilts the object left/right (like nodding sideways), rotation around Y-axis tilts the object forward/backward (like nodding), and rotation around Z-axis rotates the object in the horizontal plane (like spinning on the ground). Positive values follow the right-hand rule: point thumb in positive axis direction, fingers show positive rotation direction.'),
|
|
348
|
+
}),
|
|
349
|
+
execute: async (args, context: Context): Promise<any> => {
|
|
350
|
+
logger.info(`Tool executed: import_generated_asset with name "${args.name}"`);
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// 检查是否有客户端连接
|
|
354
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
355
|
+
logger.warn('No modeling software clients connected');
|
|
356
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 检查是否提供了必要参数
|
|
360
|
+
if (!args.task_id) {
|
|
361
|
+
return "Error: 'task_id' must be provided.";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const url = `https://api.tripo3d.ai/v2/openapi/task/${args.task_id}`;
|
|
365
|
+
|
|
366
|
+
const options = {
|
|
367
|
+
headers: {
|
|
368
|
+
'Authorization': 'Bearer ' + apiKey
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const result: Tripo3DTaskStatusResponse = await fetch(url, options)
|
|
373
|
+
.then(response => {
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new Error(`HTTP error! status: ${response.status},info : ${response.statusText}`);
|
|
376
|
+
}
|
|
377
|
+
return response.json();
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if (result.data.status !== 'success') {
|
|
382
|
+
return `Task is not completed yet. Current status: ${result.data.status}, progress: ${result.data.progress}%`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const modelData = result.data.output;
|
|
386
|
+
|
|
387
|
+
if (!modelData.model) {
|
|
388
|
+
return 'Model not found or not yet completed';
|
|
389
|
+
}
|
|
390
|
+
logger.info('Model data:', JSON.stringify(result.data));
|
|
391
|
+
|
|
392
|
+
const importUUID = `${Date.now()}`;
|
|
393
|
+
const baseProgressObj: IImportProgress = {
|
|
394
|
+
task_id: args.task_id,
|
|
395
|
+
modelData: result,
|
|
396
|
+
progress: 0,
|
|
397
|
+
}
|
|
398
|
+
importProgressMap.set(importUUID, baseProgressObj);
|
|
399
|
+
|
|
400
|
+
setImmediate(async () => {
|
|
401
|
+
try {
|
|
402
|
+
const { zipBuffer, boundingBox } = await gltf2obj(modelData.model, (progress) => {
|
|
403
|
+
baseProgressObj.progress = progress;
|
|
404
|
+
});
|
|
405
|
+
// 文件转换完成,此时需要执行置入
|
|
406
|
+
const objectInfo = await llmClient.sendCommand("import_generated_asset", {
|
|
407
|
+
...args,
|
|
408
|
+
zipBuffer: zipBuffer.toString("base64"),
|
|
409
|
+
boundingBox,
|
|
410
|
+
});
|
|
411
|
+
baseProgressObj.data = objectInfo;
|
|
412
|
+
|
|
413
|
+
logger.info("import success:" + objectInfo);
|
|
414
|
+
} catch (e) {
|
|
415
|
+
baseProgressObj.error = e;
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// args.model = modelData.model;
|
|
420
|
+
// args.model = 'https://qhstaticssl.kujiale.com/application/octetstream/1743496618932/3884A1CACC11ACB5264705935D5F121C.glb'
|
|
421
|
+
// args.model = `http://localhost:${fileProxyInstance.port}/proxy?url="${encodeURIComponent(modelData.model)}"`;
|
|
422
|
+
// 直接使用模型文件转换
|
|
423
|
+
// args.model = `http://localhost:${fileProxyInstance.port}/convert/glb-to-obj?url="${encodeURIComponent(modelData.model)}"`;
|
|
424
|
+
// rendered_image
|
|
425
|
+
logger.info("model url:", args.model);
|
|
426
|
+
const content: any[] = [{
|
|
427
|
+
type: 'text', text: `Import process started with importUUID: ${importUUID}.
|
|
428
|
+
Please use the \`poll_tripo3d_import_progress\` tool with this UUID to monitor the conversion progress and get the final model data when ready.
|
|
429
|
+
Call \`poll_tripo3d_import_progress\` with parameter: { "import_uuid": "${importUUID}" }`
|
|
430
|
+
}];
|
|
431
|
+
|
|
432
|
+
if (modelData.rendered_image) {
|
|
433
|
+
content.push(await imageContent({ url: modelData.rendered_image }))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
content
|
|
438
|
+
}
|
|
439
|
+
} catch (e) {
|
|
440
|
+
const errorMessage = `Error importing generated asset: ${e instanceof Error ? e.message : String(e)}`;
|
|
441
|
+
logger.error(errorMessage);
|
|
442
|
+
context.log.error(errorMessage);
|
|
443
|
+
return errorMessage;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
// 工具: 轮询Tripo3D导入状态
|
|
448
|
+
export const pollTripo3DImportStatusTool = {
|
|
449
|
+
name: 'poll_tripo3d_import_status',
|
|
450
|
+
description: 'Query the progress status of importing a Tripo3D-generated 3D model into the Koomaster scene, allowing you to monitor the model conversion and scene placement process',
|
|
451
|
+
parameters: z.object({
|
|
452
|
+
import_uuid: z.string().describe('The unique identifier for the import process to poll'),
|
|
453
|
+
}),
|
|
454
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
455
|
+
logger.info(`Tool executed: poll_tripo3d_import_status with import_uuid "${args.import_uuid}"`);
|
|
456
|
+
try {
|
|
457
|
+
const importUUID = args.import_uuid;
|
|
458
|
+
|
|
459
|
+
// 检查UUID是否存在于进度映射中
|
|
460
|
+
if (!importProgressMap.has(importUUID)) {
|
|
461
|
+
return `Error: No import process found with UUID: ${importUUID}`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 获取当前进度信息
|
|
465
|
+
const progressObj = importProgressMap.get(importUUID);
|
|
466
|
+
|
|
467
|
+
// 检查是否发生错误
|
|
468
|
+
if (progressObj.error) {
|
|
469
|
+
// 从映射中删除此项目,因为它已完成(出错)
|
|
470
|
+
// importProgressMap.delete(importUUID);
|
|
471
|
+
return `Error during model conversion: ${progressObj.error.message || String(progressObj.error)}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 检查是否已完成
|
|
475
|
+
if (progressObj.data) {
|
|
476
|
+
// 模型转换已完成,可以获取数据
|
|
477
|
+
return progressObj.data;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 如果仍在进行中,返回当前进度
|
|
481
|
+
return `Import process in progress: ${Math.max(0, progressObj.progress - 5)}% complete. Please check again in a few moments.`;
|
|
482
|
+
|
|
483
|
+
} catch (e) {
|
|
484
|
+
const errorMessage = `Error polling import status: ${e instanceof Error ? e.message : String(e)}`;
|
|
485
|
+
logger.error(errorMessage);
|
|
486
|
+
context.log.error(errorMessage);
|
|
487
|
+
return errorMessage;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// 导出所有 Tripo3D 相关工具
|
|
493
|
+
export const tripo3dTools = {
|
|
494
|
+
pollTripo3DJobStatusTool,
|
|
495
|
+
importGeneratedAssetTool
|
|
496
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/types/index.ts
|
|
2
|
+
import { Socket } from 'net';
|
|
3
|
+
import { Context as MCPContext } from 'fastmcp';
|
|
4
|
+
|
|
5
|
+
export interface KoomasterCommandParams {
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface KoomasterConnectionOptions {
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
sock?: Socket;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface KoomasterResponse {
|
|
16
|
+
status: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
result?: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 使用 FastMCP 的会话认证类型
|
|
22
|
+
export type SessionAuth = undefined;
|
|
23
|
+
|
|
24
|
+
// 扩展 FastMCP 的 Context 类型
|
|
25
|
+
export type Context = MCPContext<SessionAuth>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Tripo3D 任务状态响应接口
|
|
4
|
+
*/
|
|
5
|
+
export interface Tripo3DTaskStatusResponse {
|
|
6
|
+
code: number;
|
|
7
|
+
data: {
|
|
8
|
+
/** 任务ID */
|
|
9
|
+
task_id: string;
|
|
10
|
+
/** 任务类型 */
|
|
11
|
+
type: string;
|
|
12
|
+
/** 任务状态 */
|
|
13
|
+
status: 'queued' | 'running' | 'success' | 'failed' | 'cancelled' | 'unknown';
|
|
14
|
+
/** 输入参数 */
|
|
15
|
+
input: {
|
|
16
|
+
prompt: string;
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
};
|
|
19
|
+
/** 输出数据 */
|
|
20
|
+
output: {
|
|
21
|
+
/** 模型下载URL */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** 基础模型下载URL */
|
|
24
|
+
base_model?: string;
|
|
25
|
+
/** PBR模型下载URL */
|
|
26
|
+
pbr_model?: string;
|
|
27
|
+
/** 渲染图像URL */
|
|
28
|
+
rendered_image?: string;
|
|
29
|
+
/** 其他可能的输出 */
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
};
|
|
32
|
+
/** 进度(0-100) */
|
|
33
|
+
progress: number;
|
|
34
|
+
running_left_time?: number;
|
|
35
|
+
/** 创建时间(时间戳) */
|
|
36
|
+
create_time: number;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ESNext",
|
|
7
|
+
"DOM"
|
|
8
|
+
],
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationMap": true,
|
|
17
|
+
"esModuleInterop": true
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"src/**/*"
|
|
21
|
+
]
|
|
22
|
+
}
|