feat: print3d_model에 작업실 프로젝트 하위폴더 저장 옵션 추가
project_name을 주면 print3d/<프로젝트명>/ 하위폴더에 STL/gcode를 저장해 '작업실' 대시보드 프로젝트와 파일을 묶는다. 프로젝트명 매칭은 workshop-project.ts의 부분일치 규칙 재사용(0건/2건 이상이면 에러). project_name 생략 시 기존처럼 공용 print3d/ 폴더(변경 없음). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+40
-3
@@ -2,11 +2,36 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ToolResult } from '../types.js';
|
||||
import { getWorkspacePath } from '../config/paths.js';
|
||||
import { isPathInsideDir } from './image.js';
|
||||
import { loadWorkshop } from '../gateway/routes/workshop-storage.js';
|
||||
import {
|
||||
K2_HOST, K2_MOONRAKER_PORT, PRUSA_SLICER_BIN, MATERIAL_PRESETS,
|
||||
buildBaseArgs, runPrusaSlicer, uploadToMoonraker, parsePrusaTime, fmtDuration,
|
||||
} from './k2-slicer-core.js';
|
||||
|
||||
// "작업실"(workshop_project) 프로젝트에 묶어서 받은 STL이면 print3d/<프로젝트명>/ 하위폴더에
|
||||
// 저장한다(2026-09-20) — project_name 없이 부르면 예전처럼 print3d/ 바로 아래(공용). 프로젝트
|
||||
// 이름 매칭은 workshop-project.ts의 findProjectIndex와 동일한 부분일치 규칙을 그대로 따른다.
|
||||
function findWorkshopProjectName(workspacePath: string, query: string): { ok: true; name: string } | { ok: false; error: string } {
|
||||
const data = loadWorkshop(workspacePath);
|
||||
const q = query.trim().toLowerCase();
|
||||
const matches = data.projects.filter((pr) => pr.name.toLowerCase().includes(q));
|
||||
if (matches.length === 0) {
|
||||
const available = data.projects.map((p) => p.name).join(', ') || '(없음)';
|
||||
return { ok: false, error: `"작업실"에 "${query}"와 일치하는 프로젝트가 없습니다. 현재 프로젝트: ${available}` };
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
return { ok: false, error: `"${query}"에 여러 프로젝트가 일치합니다: ${matches.map((p) => p.name).join(', ')} — 더 구체적으로 지정하세요.` };
|
||||
}
|
||||
return { ok: true, name: matches[0].name };
|
||||
}
|
||||
|
||||
// 프로젝트 이름을 폴더 세그먼트로 — 슬래시/역슬래시/NUL만 막고 나머지(한글 포함)는 그대로 둔다.
|
||||
function sanitizeFolderName(name: string): string {
|
||||
const cleaned = name.replace(/[\/\\\0]/g, '_').trim().replace(/^\.+/, '');
|
||||
return cleaned || 'project';
|
||||
}
|
||||
|
||||
// K2 콤보(Creality, Klipper+Moonraker)로 STL을 찾아서 바로 슬라이싱→업로드까지 자동화.
|
||||
// 2026-09-20: STL URL을 받아 슬라이싱하고 K2의 Moonraker(192.168.0.114:7125)에 업로드한다.
|
||||
// 실제 출력 시작은 반드시 confirm:true 재호출을 거쳐야 함(schedule_job의 확인 패턴과 동일)
|
||||
@@ -40,7 +65,7 @@ async function downloadStl(url: string, outPath: string): Promise<{ ok: true } |
|
||||
|
||||
export const print3dModelTool = {
|
||||
name: 'print3d_model',
|
||||
description: 'STL 3D모델 URL을 받아 K2 콤보 프린터용으로 슬라이싱하고 프린터(Moonraker)에 업로드한다(action="prepare"), 이후 사용자가 확인하면 실제 출력을 시작한다(action="start", confirm=true 필수). STL 자체를 찾는 건 web_search로 먼저 하고, 확보한 직접 다운로드 URL을 여기 넘길 것. prepare는 예상 출력시간/필라멘트 사용량을 돌려주니 사용자에게 보여주고 "출력 시작할까요?" 확인부터 받은 뒤에만 start를 호출할 것 — 재료/시간이 드는 물리적 동작이라 사용자 승인 없이 절대 start 호출 금지.',
|
||||
description: 'STL 3D모델 URL을 받아 K2 콤보 프린터용으로 슬라이싱하고 프린터(Moonraker)에 업로드한다(action="prepare"), 이후 사용자가 확인하면 실제 출력을 시작한다(action="start", confirm=true 필수). STL 자체를 찾는 건 web_search로 먼저 하고, 확보한 직접 다운로드 URL을 여기 넘길 것. prepare는 예상 출력시간/필라멘트 사용량을 돌려주니 사용자에게 보여주고 "출력 시작할까요?" 확인부터 받은 뒤에만 start를 호출할 것 — 재료/시간이 드는 물리적 동작이라 사용자 승인 없이 절대 start 호출 금지. 이 부품이 "작업실" 대시보드의 특정 프로젝트(로봇, 3D프린팅 등) 것이면 project_name을 같이 넘겨서 그 프로젝트 하위폴더에 정리할 것.',
|
||||
schema: { action: 'string', stl_url: 'string' },
|
||||
jsonSchema: {
|
||||
type: 'object',
|
||||
@@ -48,6 +73,7 @@ export const print3dModelTool = {
|
||||
properties: {
|
||||
action: { type: 'string', enum: ['prepare', 'start'], description: 'prepare: STL 다운로드+슬라이싱+프린터 업로드(출력은 시작 안 함). start: 실제 출력 시작(반드시 confirm=true, 사용자가 명시적으로 승인한 뒤에만 호출)' },
|
||||
stl_url: { type: 'string', description: '(prepare 필수) STL 파일 직접 다운로드 URL(http/https). Thingiverse는 인증 없이 다운로드가 막혀있어 안 됨 — Printables, GitHub raw 등 직접 링크를 쓸 것.' },
|
||||
project_name: { type: 'string', description: '(prepare, 선택) 이 STL이 "작업실" 대시보드의 어느 프로젝트 부품인지(일부만 일치해도 됨, 예: "로봇"). 주면 print3d/<프로젝트명>/ 하위폴더에 저장해 그 프로젝트 작업물끼리 묶는다. 생략하면 기존처럼 공용 print3d/ 폴더(프로젝트와 무관한 일회성 출력)에 저장 — 작업실 프로젝트와 상관없는 요청이면 비워둘 것.' },
|
||||
layer_height: { type: 'number', description: '(prepare, 선택) 레이어 높이 mm. 기본 0.2' },
|
||||
infill_percent: { type: 'number', description: '(prepare, 선택) 인필 밀도 %. 기본 15' },
|
||||
wall_count: { type: 'number', description: '(prepare, 선택) 벽 겹수. 기본 2' },
|
||||
@@ -106,7 +132,17 @@ export const print3dModelTool = {
|
||||
const wallCount = Number.isFinite(Number(args?.wall_count)) ? Math.max(1, Math.round(Number(args.wall_count))) : 2;
|
||||
|
||||
const workspacePath = getWorkspacePath(args);
|
||||
const outDir = path.join(workspacePath, 'print3d');
|
||||
const baseDir = path.join(workspacePath, 'print3d');
|
||||
const projectNameArg = String(args?.project_name || '').trim();
|
||||
let outDir = baseDir;
|
||||
let projectFolderName = '';
|
||||
if (projectNameArg) {
|
||||
const found = findWorkshopProjectName(workspacePath, projectNameArg);
|
||||
if (!found.ok) return { success: false, error: found.error };
|
||||
projectFolderName = found.name;
|
||||
outDir = path.join(baseDir, sanitizeFolderName(projectFolderName));
|
||||
if (!isPathInsideDir(baseDir, outDir)) return { success: false, error: '프로젝트 이름으로 만든 경로가 print3d 디렉토리 밖입니다.' };
|
||||
}
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
const stamp = Date.now();
|
||||
const stlPath = path.join(outDir, `model_${stamp}.stl`);
|
||||
@@ -136,12 +172,13 @@ export const print3dModelTool = {
|
||||
|
||||
const timeStr = printTimeSec != null ? fmtDuration(printTimeSec) : '알 수 없음';
|
||||
const materialNote = material.verified ? '' : ' (⚠️ 온도값은 검증되지 않은 일반 추정치입니다)';
|
||||
const projectNote = projectFolderName ? `\n- 작업실 프로젝트: "${projectFolderName}" (print3d/${projectFolderName}/ 폴더에 저장)` : '';
|
||||
return {
|
||||
success: true,
|
||||
stdout: `K2 콤보에 업로드 완료: **${gcodeFilename}**\n` +
|
||||
`- 소재: ${materialKey}${materialNote}\n` +
|
||||
`- 예상 출력시간: ${timeStr}\n` +
|
||||
`- 설정: 레이어 ${layerHeight}mm, 벽 ${wallCount}겹, 인필 ${infillPercent}%\n\n` +
|
||||
`- 설정: 레이어 ${layerHeight}mm, 벽 ${wallCount}겹, 인필 ${infillPercent}%${projectNote}\n\n` +
|
||||
`사용자에게 위 정보를 보여주고 출력을 시작할지 확인하세요. 승인하면 print3d_model(action="start", filename="${gcodeFilename}", confirm=true)로 출력을 시작하세요.`,
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user