freeform-modeling-mcp 1.0.27 → 1.0.29

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  - 🧰 丰富工具集(`src/tools/`包含50+建模工具)
6
6
  - 基础建模工具(立方体/球体/圆环创建)
7
7
  - KJL资源库集成(资产搜索/置入/替换)
8
- - Tripo3D AI生成
8
+ - 通用 GLB/GLTF 导入
9
9
  - 材质替换
10
10
 
11
11
  ## 快速开始
@@ -54,7 +54,7 @@ npm i -g freeform-modeling-mcp@1.0.26 --registry=https://registry.npmjs.org
54
54
 
55
55
  ## 开发指南
56
56
  1. 工具扩展:在`src/tools/`创建新工具类
57
- 2. API集成:参考`tripo3d-tools.ts`接入第三方服务
57
+ 2. API集成:参考`import-tools.ts`接入第三方服务
58
58
  3. 客户端协议:遵循`llmClient.ts`定义的消息格式
59
59
 
60
60
  ## 贡献者协议
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freeform-modeling-mcp",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "KooMaster MCP Server with WebSocket Bridge",
5
5
  "main": "index.cjs",
6
6
  "type": "module",
package/src/prompt.ts CHANGED
@@ -72,7 +72,7 @@ export const MainStrategy = `\n# KooMaster(酷大师) User Guide
72
72
  ## VI. Asset Generation Tool Priority
73
73
  ### 1. Tool Priority
74
74
  1. **KJL (Kujiale) Asset Library (First Choice)**: Prioritize searching and using high-quality assets from the KJL library.
75
- 2. **Tripo3D (Backup)**: Use only when suitable assets are unavailable in the KJL library.
75
+ 2. **Generated GLB/GLTF import (Backup)**: Use only when suitable assets are unavailable in the KJL library. Obtain a publicly reachable GLB/GLTF URL from any generator, then import with import_generated_asset.
76
76
  3. **Basic Creation Tools (Last Resort)**: Use only when explicitly instructed or when the first two methods fail.
77
77
 
78
78
  ---
@@ -104,13 +104,12 @@ export const MainStrategy = `\n# KooMaster(酷大师) User Guide
104
104
 
105
105
  ---
106
106
 
107
- ## VIII. Tripo3D Fallback Workflow
107
+ ## VIII. Generated GLB/GLTF Fallback Workflow
108
108
  ### 1. Applicable Scenarios
109
109
  • Execute only when no suitable assets are available in the KJL (Kujiale) asset library.
110
110
  ### 2. Workflow
111
- 1. **Build complex geometric models**.
112
- 2. **Monitor generation progress**.
113
- 3. **Import and Transform**:
111
+ 1. **Obtain a GLB/GLTF URL** from any 3D generation service or local pipeline that exposes a public HTTP(S) model file. Prefer \`.glb\` / \`.gltf\` over archives such as \`.zip\`.
112
+ 2. **Import and Transform** with \`import_generated_asset\` (\`model_url\` = that GLB/GLTF URL), then poll \`poll_import_status\`:
114
113
  • **Pre-Placement Rotation Analysis**:
115
114
  • Analyze model bounding box dimensions (width, depth, height) before placement.
116
115
  • For elongated objects (like wall panels, shelves, counters), determine proper orientation based on function and typical placement.
@@ -122,7 +121,7 @@ export const MainStrategy = `\n# KooMaster(酷大师) User Guide
122
121
  • **Rotation Verification**: Confirm whether model needs pre-rotation before finalizing placement parameters.
123
122
  • **Size Calibration**: Set model dimensions according to real-world standard sizes.
124
123
  • **Inter-object Spacing Planning**: Ensure appropriate distances from other objects to avoid clipping.
125
- 4. **Completeness Check**:
124
+ 3. **Completeness Check**:
126
125
  • Immediately perform comprehensive spatial position and collision detection after import.
127
126
  • Check spatial relationships with all existing objects to ensure no clipping.
128
127
  • Verify model dimensions conform to real-world proportions.
package/src/server.ts CHANGED
@@ -20,7 +20,7 @@ import { getObjectsScreenshotTool, getSceneScreenshotTool } from './tools/screen
20
20
  import { createAuxiliaryCurve } from './tools/modeling/auxiliary-curve-tool';
21
21
  import { sweepTool } from './tools/modeling/sweep-tool';
22
22
  import { findFaceByMultiplePointsTool } from './tools/modeling/find-face';
23
- // ... 其他工具导入
23
+ import { importGeneratedAssetTool, pollImportStatusTool } from './tools/import-tools';
24
24
 
25
25
  // 创建 FastMCP 服务器
26
26
  export function createServer(): FastMCP<SessionAuth> {
@@ -70,12 +70,9 @@ export function createServer(): FastMCP<SessionAuth> {
70
70
  server.addTool(createAuxiliaryCurve)
71
71
  //#endregion
72
72
 
73
- //#region tripo3d
74
- // server.addTool(getTripo3DStatusTool);
75
- // server.addTool(createTripo3DJobTool);
76
- // server.addTool(pollTripo3DJobStatusTool);
77
- // server.addTool(importGeneratedAssetTool);
78
- // server.addTool(pollTripo3DImportStatusTool);
73
+ //#region generated asset import (GLB/GLTF)
74
+ server.addTool(importGeneratedAssetTool);
75
+ server.addTool(pollImportStatusTool);
79
76
  //#endregion
80
77
 
81
78
  server.addPrompt({
@@ -379,7 +379,7 @@ export const deleteObjectsTool = {
379
379
  .min(1, "At least one object name must be provided")
380
380
  .describe('Array of object names to delete'),
381
381
  type: z.enum(['group', 'face', 'auxiliary_curve'])
382
- .describe('Type of objects to delete: "group" for KJL models, Tripo3D models and basic geometry; "face" for individual faces; "auxiliary_curve" for reference curves')
382
+ .describe('Type of objects to delete: "group" for KJL models, generated GLB models and basic geometry; "face" for individual faces; "auxiliary_curve" for reference curves')
383
383
  }),
384
384
  execute: async (args: { names: string[], type: 'group' | 'face' | 'auxiliary_curve' }, context: Context): Promise<string> => {
385
385
  const objectNames = args.names.join(', ');
@@ -424,7 +424,7 @@ export const deleteObjectsTool = {
424
424
  // copyObject 工具
425
425
  export const copyObjectTool = {
426
426
  name: 'copy_object',
427
- description: 'Create a copy of an existing object with a new name and transform properties. Currently only supports copying "group" type objects (KJL models, Tripo3D models, and basic geometry).',
427
+ description: 'Create a copy of an existing object with a new name and transform properties. Currently only supports copying "group" type objects (KJL models, generated GLB models, and basic geometry).',
428
428
  parameters: z.object({
429
429
  name: z.string().describe('Name of the group object to copy'),
430
430
  newName: z.string().describe('Name for the new copied group object'),
@@ -0,0 +1,163 @@
1
+ import { Context } from '../types';
2
+ import { z } from 'zod';
3
+ import logger from '../lib/logger';
4
+ import { llmClient } from '../llmClient';
5
+ import { gltf2obj } from '../lib/gltf2obj/index.js';
6
+ import { imageContent } from 'fastmcp';
7
+
8
+ interface IImportProgress {
9
+ modelUrl: string;
10
+ progress: number;
11
+ data?: string;
12
+ error?: Error;
13
+ }
14
+
15
+ const importProgressMap = new Map<string, IImportProgress>();
16
+
17
+ const UNSUPPORTED_MODEL_EXTENSIONS = ['.zip', '.obj', '.fbx', '.stl', '.usdz', '.3mf'];
18
+
19
+ function getUrlPathname(url: string): string {
20
+ try {
21
+ return new URL(url).pathname.toLowerCase();
22
+ } catch {
23
+ return url.split('?')[0].toLowerCase();
24
+ }
25
+ }
26
+
27
+ function isHttpUrl(url: string): boolean {
28
+ try {
29
+ const protocol = new URL(url).protocol;
30
+ return protocol === 'http:' || protocol === 'https:';
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function isUnsupportedModelUrl(url: string): boolean {
37
+ const pathname = getUrlPathname(url);
38
+ return UNSUPPORTED_MODEL_EXTENSIONS.some((ext) => pathname.endsWith(ext));
39
+ }
40
+
41
+ export const importGeneratedAssetTool = {
42
+ name: 'import_generated_asset',
43
+ description: 'Import a generated GLB/GLTF 3D model into the Koomaster scene from any source that provides a publicly reachable model URL. Converts GLB/GLTF to OBJ and places it asynchronously; poll with poll_import_status.',
44
+ parameters: z.object({
45
+ name: z.string().describe('The name to give to the imported object in the scene'),
46
+ model_url: z.string().describe('Public HTTP(S) URL of a completed GLB/GLTF model. Do not pass archive or other mesh formats such as .zip/.obj/.fbx.'),
47
+ preview_image_url: z.string().optional().describe('Optional preview image URL to show alongside the import job.'),
48
+ location: z.array(z.number()).describe('[x, y, z] center position coordinates for the imported model'),
49
+ dimensions: z.array(z.number()).describe('[width, height, depth] absolute dimensions for the imported model, in millimeters'),
50
+ 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.'),
51
+ }),
52
+ execute: async (args, context: Context): Promise<any> => {
53
+ logger.info(`Tool executed: import_generated_asset with name "${args.name}"`);
54
+
55
+ try {
56
+ if (!llmClient.hasConnectedKoomasterClients()) {
57
+ logger.warn('No modeling software clients connected');
58
+ return "No modeling software clients connected. Please start the software and connect to this server.";
59
+ }
60
+
61
+ if (!args.model_url) {
62
+ return "Error: 'model_url' must be provided. Pass a publicly reachable GLB/GLTF URL.";
63
+ }
64
+
65
+ if (!isHttpUrl(args.model_url)) {
66
+ return "Error: 'model_url' must be an HTTP or HTTPS URL to a GLB/GLTF file.";
67
+ }
68
+
69
+ if (isUnsupportedModelUrl(args.model_url)) {
70
+ return "Error: 'model_url' must point to a GLB/GLTF file. Archive and other mesh formats (.zip/.obj/.fbx/.stl/.usdz/.3mf) are not supported.";
71
+ }
72
+
73
+ logger.info('Importing generated asset from URL:', args.model_url);
74
+
75
+ const importUUID = `${Date.now()}`;
76
+ const baseProgressObj: IImportProgress = {
77
+ modelUrl: args.model_url,
78
+ progress: 0,
79
+ }
80
+ importProgressMap.set(importUUID, baseProgressObj);
81
+
82
+ setImmediate(async () => {
83
+ try {
84
+ const { zipBuffer, boundingBox } = await gltf2obj(args.model_url, (progress) => {
85
+ baseProgressObj.progress = progress;
86
+ });
87
+ const objectInfo = await llmClient.sendCommand("import_generated_asset", {
88
+ name: args.name,
89
+ location: args.location,
90
+ dimensions: args.dimensions,
91
+ rotation: args.rotation,
92
+ zipBuffer: zipBuffer.toString("base64"),
93
+ boundingBox,
94
+ });
95
+ baseProgressObj.data = objectInfo;
96
+
97
+ logger.info("import success:" + objectInfo);
98
+ } catch (e) {
99
+ baseProgressObj.error = e instanceof Error ? e : new Error(String(e));
100
+ logger.error('import_generated_asset background failed', baseProgressObj.error);
101
+ }
102
+ });
103
+
104
+ const content: any[] = [{
105
+ type: 'text', text: `Import process started with importUUID: ${importUUID}.
106
+ Please use the \`poll_import_status\` tool with this UUID to monitor the conversion progress and get the final model data when ready.
107
+ Call \`poll_import_status\` with parameter: { "import_uuid": "${importUUID}" }`
108
+ }];
109
+
110
+ if (args.preview_image_url) {
111
+ content.push(await imageContent({ url: args.preview_image_url }))
112
+ }
113
+
114
+ return {
115
+ content
116
+ }
117
+ } catch (e) {
118
+ const errorMessage = `Error importing generated asset: ${e instanceof Error ? e.message : String(e)}`;
119
+ logger.error(errorMessage);
120
+ context.log.error(errorMessage);
121
+ return errorMessage;
122
+ }
123
+ }
124
+ };
125
+
126
+ export const pollImportStatusTool = {
127
+ name: 'poll_import_status',
128
+ description: 'Query the progress of importing a generated GLB/GLTF model into the Koomaster scene. Pass the import_uuid returned by import_generated_asset.',
129
+ parameters: z.object({
130
+ import_uuid: z.string().describe('The unique identifier returned by import_generated_asset'),
131
+ }),
132
+ execute: async (args, context: Context): Promise<string> => {
133
+ logger.info(`Tool executed: poll_import_status with import_uuid "${args.import_uuid}"`);
134
+ try {
135
+ const importUUID = args.import_uuid;
136
+
137
+ if (!importProgressMap.has(importUUID)) {
138
+ return `Error: No import process found with UUID: ${importUUID}`;
139
+ }
140
+
141
+ const progressObj = importProgressMap.get(importUUID);
142
+ if (!progressObj) {
143
+ return `Error: No import process found with UUID: ${importUUID}`;
144
+ }
145
+
146
+ if (progressObj.error) {
147
+ return `Error during model conversion: ${progressObj.error.message || String(progressObj.error)}`;
148
+ }
149
+
150
+ if (progressObj.data) {
151
+ return progressObj.data;
152
+ }
153
+
154
+ return `Import process in progress: ${Math.max(0, progressObj.progress - 5)}% complete. Please check again in a few moments.`;
155
+
156
+ } catch (e) {
157
+ const errorMessage = `Error polling import status: ${e instanceof Error ? e.message : String(e)}`;
158
+ logger.error(errorMessage);
159
+ context.log.error(errorMessage);
160
+ return errorMessage;
161
+ }
162
+ }
163
+ };
@@ -5,7 +5,7 @@ import { llmClient } from "../llmClient";
5
5
 
6
6
  export const getObjectsScreenshotTool = {
7
7
  name: 'get_objects_screenshot',
8
- description: 'Captures a screenshot of the specific objects from different view angles. Note: Only supports KJL models, Tripo3D models, and basic geometric shapes - face screenshots are not supported.',
8
+ description: 'Captures a screenshot of the specific objects from different view angles. Note: Only supports KJL models, generated GLB models, and basic geometric shapes - face screenshots are not supported.',
9
9
  parameters: z.object({
10
10
  view_angle: z.enum(['front', 'back', 'left', 'right', 'top', 'bottom']).default('top')
11
11
  .describe('Camera angle for the screenshot. Options: standard orthographic views (front, back, left, right, top, bottom) .'),
@@ -22,9 +22,9 @@ export const getToolsInfoTool = {
22
22
  "create_object - Create primitive objects (cube, sphere, etc.)",
23
23
  "modify_object - Change object properties (location, rotation, scale)",
24
24
  "delete_objects - Delete objects from the Koomaster scene by name",
25
- "copy_object - Create a copy of an existing object with a new name and transform properties. Currently only supports copying \"group\" type objects (KJL models, Tripo3D models, and basic geometry)",
25
+ "copy_object - Create a copy of an existing object with a new name and transform properties. Currently only supports copying \"group\" type objects (KJL models, generated GLB models, and basic geometry)",
26
26
  "get_selection - Get detailed information about the currently selected objects in the scene. Supports analyzing selected faces (with vertex coordinates and bounding boxes) or selected groups (with transform and hierarchy information).",
27
- "get_objects_screenshot - Captures a screenshot of the specific objects from different view angles. Note: Only supports KJL models, Tripo3D models, and basic geometric shapes - face screenshots are not supported.",
27
+ "get_objects_screenshot - Captures a screenshot of the specific objects from different view angles. Note: Only supports KJL models, generated GLB models, and basic geometric shapes - face screenshots are not supported.",
28
28
  "get_scene_screenshot - Captures a screenshot of the entire 3D scene from the specified view angle. Note: the screenshot result may appear upside-down compared to the actual scene orientation."
29
29
  ],
30
30
  material: [
@@ -40,13 +40,10 @@ export const getToolsInfoTool = {
40
40
  "create_auxiliary_curve - Creates an auxiliary 3D curve using SVG-like path commands with explicit 3D coordinates.",
41
41
  "sweep - Creates a 3D solid by sweeping a FACE along auxiliary curves. NOTE: This tool operates ONLY on faces, not on entire objects or groups. The face can be either selected by the user or created programmatically. IMPORTANT: The face being swept (profile) must NOT be parallel to the auxiliary curves (path) - the sweep direction must have some component perpendicular to the face normal for the operation to succeed. After the sweep operation completes, the original face (profile) will be automatically deleted from the model.",
42
42
  ],
43
- // tripo3d: [
44
- // "get_tripo3d_status - Check if Tripo3D is configured and available for use by verifying the presence of required API keys and configuration",
45
- // "create_tripo3d_job - Generate a 3D model using Fal.ai Tripo3D service from text or images",
46
- // "poll_tripo3d_job_status - Check the status of a Tripo3D 3D model generation job",
47
- // "import_generated_asset - Import a 3D model generated by Tripo3D into the scene",
48
- // "poll_tripo3d_import_status - Query the progress status of importing a Tripo3D-generated 3D model into the Koomaster scene"
49
- // ],
43
+ generated: [
44
+ "import_generated_asset - Import a generated GLB/GLTF model from any source that provides a public model URL. Requires model_url plus name/location/dimensions.",
45
+ "poll_import_status - Query GLB-to-scene import progress using the import_uuid returned by import_generated_asset."
46
+ ],
50
47
  assets: [
51
48
  "batch_search_kjl_assets - Performs a concurrent batch search for multiple 3D models and assets in the KJL(酷家乐) database. Returns a combined list of models that match each search term, including their names, IDs, categories, and dimensions. Use this when you need to search for multiple different types of models at once.",
52
49
  "batch_get_kjl_asset_snapshot - Retrieves preview images for multiple KJL(酷家乐) asset models concurrently based on their model IDs. This helps visualize multiple assets before placing them in the scene. Use this tool to efficiently examine the appearance and style of several models at once to ensure they match your design requirements.",
@@ -75,8 +72,8 @@ export const getToolsInfoTool = {
75
72
  result += `- ${tool}\n`;
76
73
  });
77
74
 
78
- result += `\n## Tripo3D Integration\n\n`;
79
- toolsInfo.tripo3d.forEach((tool) => {
75
+ result += `\n## Generated Asset Import (GLB/GLTF)\n\n`;
76
+ toolsInfo.generated.forEach((tool) => {
80
77
  result += `- ${tool}\n`;
81
78
  });
82
79
 
@@ -1,496 +0,0 @@
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
- };
@@ -1,38 +0,0 @@
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
- }