makecc 0.1.0
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/bin/cli.js +111 -0
- package/dist/assets/index-BcaHeFM8.css +1 -0
- package/dist/assets/index-CryYaA6W.js +57 -0
- package/dist/client/assets/index-BkAJX_uj.js +133 -0
- package/dist/client/assets/index-CRHGYTA2.css +1 -0
- package/dist/client/index.html +14 -0
- package/dist/client/vite.svg +1 -0
- package/dist/index.html +14 -0
- package/dist/server/index.js +207 -0
- package/dist/server/services/claudeService.js +153 -0
- package/dist/server/services/fileService.js +193 -0
- package/dist/server/services/skillExecutionService.js +622 -0
- package/dist/server/services/terminalService.js +216 -0
- package/dist/server/services/workflowAIService.js +191 -0
- package/dist/server/services/workflowExecutionService.js +342 -0
- package/dist/server/types.js +1 -0
- package/dist/vite.svg +1 -0
- package/package.json +84 -0
- package/server/index.ts +253 -0
- package/server/services/claudeService.ts +222 -0
- package/server/services/fileService.ts +277 -0
- package/server/services/skillExecutionService.ts +732 -0
- package/server/services/terminalService.ts +266 -0
- package/server/services/workflowAIService.ts +240 -0
- package/server/services/workflowExecutionService.ts +471 -0
- package/server/types.ts +110 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { platform } from 'os';
|
|
3
|
+
import type { ExecutionNode, SubagentNodeData, SkillNodeData, InputNodeData } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface WorkflowEdge {
|
|
6
|
+
id: string;
|
|
7
|
+
source: string;
|
|
8
|
+
target: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TerminalExecutionOptions {
|
|
12
|
+
workflowName: string;
|
|
13
|
+
nodes: ExecutionNode[];
|
|
14
|
+
edges: WorkflowEdge[];
|
|
15
|
+
inputs?: Record<string, string>;
|
|
16
|
+
workingDirectory?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generates a Claude Code prompt from the workflow
|
|
21
|
+
*/
|
|
22
|
+
function generateClaudePrompt(options: TerminalExecutionOptions): string {
|
|
23
|
+
const { workflowName, nodes, edges, inputs } = options;
|
|
24
|
+
|
|
25
|
+
const lines: string[] = [];
|
|
26
|
+
lines.push(`# ${workflowName}`);
|
|
27
|
+
lines.push('');
|
|
28
|
+
|
|
29
|
+
// Collect inputs
|
|
30
|
+
const inputNodes = nodes.filter((n): n is ExecutionNode & { data: InputNodeData } => n.type === 'input');
|
|
31
|
+
if (inputNodes.length > 0 && inputs) {
|
|
32
|
+
lines.push('## Inputs');
|
|
33
|
+
inputNodes.forEach((node) => {
|
|
34
|
+
const value = inputs[node.id] || node.data.value || '';
|
|
35
|
+
if (value) {
|
|
36
|
+
lines.push(`- ${node.data.label}: ${value}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
lines.push('');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Build execution order
|
|
43
|
+
const executionOrder = topologicalSort(nodes, edges);
|
|
44
|
+
|
|
45
|
+
lines.push('## Tasks');
|
|
46
|
+
lines.push('');
|
|
47
|
+
|
|
48
|
+
let stepNum = 1;
|
|
49
|
+
executionOrder.forEach((node) => {
|
|
50
|
+
if (node.type === 'input') return;
|
|
51
|
+
|
|
52
|
+
if (node.type === 'subagent') {
|
|
53
|
+
const data = node.data as SubagentNodeData;
|
|
54
|
+
lines.push(`${stepNum}. **${data.label}** (${data.role})`);
|
|
55
|
+
if (data.description) {
|
|
56
|
+
lines.push(` ${data.description}`);
|
|
57
|
+
}
|
|
58
|
+
if (data.systemPrompt) {
|
|
59
|
+
lines.push(` System: ${data.systemPrompt}`);
|
|
60
|
+
}
|
|
61
|
+
if (data.tools.length > 0) {
|
|
62
|
+
lines.push(` Tools: ${data.tools.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
stepNum++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (node.type === 'skill') {
|
|
69
|
+
const data = node.data as SkillNodeData;
|
|
70
|
+
lines.push(`${stepNum}. **${data.label}** (skill)`);
|
|
71
|
+
if (data.skillId) {
|
|
72
|
+
lines.push(` Execute /${data.skillId}`);
|
|
73
|
+
}
|
|
74
|
+
if (data.mdContent) {
|
|
75
|
+
lines.push(` ${data.mdContent.substring(0, 200)}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push('');
|
|
78
|
+
stepNum++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (node.type === 'output') {
|
|
82
|
+
lines.push(`${stepNum}. Output the final result.`);
|
|
83
|
+
lines.push('');
|
|
84
|
+
stepNum++;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Opens Terminal.app and runs claude -c with the workflow prompt
|
|
93
|
+
*/
|
|
94
|
+
export async function executeInTerminal(
|
|
95
|
+
options: TerminalExecutionOptions
|
|
96
|
+
): Promise<{ success: boolean; message: string }> {
|
|
97
|
+
const prompt = generateClaudePrompt(options);
|
|
98
|
+
const cwd = options.workingDirectory || process.cwd();
|
|
99
|
+
|
|
100
|
+
// Escape for shell
|
|
101
|
+
const escapedPrompt = prompt
|
|
102
|
+
.replace(/\\/g, '\\\\')
|
|
103
|
+
.replace(/"/g, '\\"')
|
|
104
|
+
.replace(/\$/g, '\\$')
|
|
105
|
+
.replace(/`/g, '\\`');
|
|
106
|
+
|
|
107
|
+
const os = platform();
|
|
108
|
+
|
|
109
|
+
if (os === 'darwin') {
|
|
110
|
+
// macOS: Use osascript to open Terminal and run command
|
|
111
|
+
const appleScript = `
|
|
112
|
+
tell application "Terminal"
|
|
113
|
+
activate
|
|
114
|
+
do script "cd \\"${cwd}\\" && claude -c \\"${escapedPrompt}\\""
|
|
115
|
+
end tell
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
const proc = spawn('osascript', ['-e', appleScript]);
|
|
120
|
+
|
|
121
|
+
proc.on('close', (code) => {
|
|
122
|
+
if (code === 0) {
|
|
123
|
+
resolve({
|
|
124
|
+
success: true,
|
|
125
|
+
message: 'Terminal opened with Claude Code',
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
resolve({
|
|
129
|
+
success: false,
|
|
130
|
+
message: `Failed to open Terminal (exit code: ${code})`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
proc.on('error', (err) => {
|
|
136
|
+
resolve({
|
|
137
|
+
success: false,
|
|
138
|
+
message: `Error: ${err.message}`,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
} else if (os === 'linux') {
|
|
143
|
+
// Linux: Try common terminal emulators
|
|
144
|
+
const command = `cd "${cwd}" && claude -c "${escapedPrompt}"`;
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
// Try gnome-terminal first, then xterm
|
|
148
|
+
const proc = spawn('gnome-terminal', ['--', 'bash', '-c', command], {
|
|
149
|
+
detached: true,
|
|
150
|
+
stdio: 'ignore',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
proc.on('error', () => {
|
|
154
|
+
// Fallback to xterm
|
|
155
|
+
const xtermProc = spawn('xterm', ['-e', `bash -c '${command}'`], {
|
|
156
|
+
detached: true,
|
|
157
|
+
stdio: 'ignore',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
xtermProc.on('error', (err) => {
|
|
161
|
+
resolve({
|
|
162
|
+
success: false,
|
|
163
|
+
message: `Could not open terminal: ${err.message}`,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
xtermProc.unref();
|
|
168
|
+
resolve({
|
|
169
|
+
success: true,
|
|
170
|
+
message: 'Terminal opened with Claude Code',
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
proc.unref();
|
|
175
|
+
resolve({
|
|
176
|
+
success: true,
|
|
177
|
+
message: 'Terminal opened with Claude Code',
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
} else if (os === 'win32') {
|
|
181
|
+
// Windows: Use cmd or PowerShell
|
|
182
|
+
const command = `cd /d "${cwd}" && claude -c "${escapedPrompt}"`;
|
|
183
|
+
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const proc = spawn('cmd.exe', ['/c', 'start', 'cmd', '/k', command], {
|
|
186
|
+
detached: true,
|
|
187
|
+
stdio: 'ignore',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
proc.on('error', (err) => {
|
|
191
|
+
resolve({
|
|
192
|
+
success: false,
|
|
193
|
+
message: `Error: ${err.message}`,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
proc.unref();
|
|
198
|
+
resolve({
|
|
199
|
+
success: true,
|
|
200
|
+
message: 'Terminal opened with Claude Code',
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
message: `Unsupported platform: ${os}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Copies the claude command to clipboard (fallback)
|
|
213
|
+
*/
|
|
214
|
+
export function getClaudeCommand(options: TerminalExecutionOptions): string {
|
|
215
|
+
const prompt = generateClaudePrompt(options);
|
|
216
|
+
const escaped = prompt.replace(/"/g, '\\"');
|
|
217
|
+
return `claude -c "${escaped}"`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Topological sort for execution order
|
|
222
|
+
*/
|
|
223
|
+
function topologicalSort<T extends { id: string }>(
|
|
224
|
+
nodes: T[],
|
|
225
|
+
edges: Array<{ source: string; target: string }>
|
|
226
|
+
): T[] {
|
|
227
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
228
|
+
const inDegree = new Map<string, number>();
|
|
229
|
+
const adjList = new Map<string, string[]>();
|
|
230
|
+
|
|
231
|
+
nodes.forEach((node) => {
|
|
232
|
+
inDegree.set(node.id, 0);
|
|
233
|
+
adjList.set(node.id, []);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
edges.forEach((edge) => {
|
|
237
|
+
adjList.get(edge.source)?.push(edge.target);
|
|
238
|
+
inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const queue: string[] = [];
|
|
242
|
+
inDegree.forEach((degree, nodeId) => {
|
|
243
|
+
if (degree === 0) {
|
|
244
|
+
queue.push(nodeId);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const result: T[] = [];
|
|
249
|
+
while (queue.length > 0) {
|
|
250
|
+
const nodeId = queue.shift()!;
|
|
251
|
+
const node = nodeMap.get(nodeId);
|
|
252
|
+
if (node) {
|
|
253
|
+
result.push(node);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
adjList.get(nodeId)?.forEach((neighborId) => {
|
|
257
|
+
const newDegree = (inDegree.get(neighborId) || 0) - 1;
|
|
258
|
+
inDegree.set(neighborId, newDegree);
|
|
259
|
+
if (newDegree === 0) {
|
|
260
|
+
queue.push(neighborId);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
|
|
3
|
+
// AI가 생성하는 워크플로우 결과 타입
|
|
4
|
+
export interface AIWorkflowResult {
|
|
5
|
+
workflowName: string;
|
|
6
|
+
description: string;
|
|
7
|
+
nodes: AIGeneratedNode[];
|
|
8
|
+
edges: { from: number; to: number }[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AIGeneratedNode {
|
|
12
|
+
type: 'input' | 'subagent' | 'skill' | 'mcp' | 'output';
|
|
13
|
+
label: string;
|
|
14
|
+
description: string;
|
|
15
|
+
config: {
|
|
16
|
+
// input
|
|
17
|
+
inputType?: 'text' | 'file' | 'select';
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
// subagent
|
|
20
|
+
role?: string;
|
|
21
|
+
tools?: string[];
|
|
22
|
+
model?: string;
|
|
23
|
+
systemPrompt?: string;
|
|
24
|
+
// skill
|
|
25
|
+
skillType?: 'official' | 'custom';
|
|
26
|
+
skillId?: string;
|
|
27
|
+
skillContent?: string; // 커스텀 스킬 SKILL.md 내용
|
|
28
|
+
// output
|
|
29
|
+
outputType?: 'auto' | 'markdown' | 'document' | 'image';
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 사용 가능한 공식 스킬 목록
|
|
34
|
+
const AVAILABLE_SKILLS = [
|
|
35
|
+
{ id: 'image-gen-nanobanana', name: 'Image Generator', description: 'Google Gemini 기반 AI 이미지 생성' },
|
|
36
|
+
{ id: 'ppt-generator', name: 'PPT Generator', description: '한국어에 최적화된 미니멀 프레젠테이션 생성' },
|
|
37
|
+
{ id: 'video-gen-veo3', name: 'Video Generator', description: 'Google Veo3 기반 AI 영상 생성' },
|
|
38
|
+
{ id: 'pdf', name: 'PDF', description: 'PDF 텍스트 추출, 생성, 병합/분할, 폼 처리' },
|
|
39
|
+
{ id: 'docx', name: 'Word Document', description: 'Word 문서 생성, 편집, 변경 추적' },
|
|
40
|
+
{ id: 'xlsx', name: 'Excel', description: '스프레드시트 생성, 편집, 수식, 데이터 분석' },
|
|
41
|
+
{ id: 'pptx', name: 'PowerPoint', description: 'PPT 생성, 편집, 레이아웃, 스피커 노트' },
|
|
42
|
+
{ id: 'git-commit-push', name: 'Git Commit/Push', description: 'Git 커밋 메시지 작성 및 푸시/PR 생성' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// 사용 가능한 도구 목록
|
|
46
|
+
const AVAILABLE_TOOLS = [
|
|
47
|
+
'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep',
|
|
48
|
+
'WebSearch', 'WebFetch', 'Task', 'TodoWrite',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const SYSTEM_PROMPT = `당신은 Claude Code 워크플로우 설계 전문가입니다.
|
|
52
|
+
|
|
53
|
+
사용자의 워크플로우 설명을 분석하여 적절한 노드들과 연결을 생성합니다.
|
|
54
|
+
|
|
55
|
+
## 사용 가능한 공식 스킬
|
|
56
|
+
${AVAILABLE_SKILLS.map(s => `- ${s.id}: ${s.description}`).join('\n')}
|
|
57
|
+
|
|
58
|
+
## 사용 가능한 도구 (subagent의 tools에 사용)
|
|
59
|
+
${AVAILABLE_TOOLS.join(', ')}
|
|
60
|
+
|
|
61
|
+
## 응답 형식 (JSON)
|
|
62
|
+
반드시 아래 형식의 유효한 JSON으로 응답하세요. 다른 텍스트 없이 JSON만 반환하세요.
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
"workflowName": "워크플로우 이름 (한글 가능)",
|
|
66
|
+
"description": "워크플로우 설명",
|
|
67
|
+
"nodes": [
|
|
68
|
+
{
|
|
69
|
+
"type": "input",
|
|
70
|
+
"label": "입력 노드 이름",
|
|
71
|
+
"description": "입력 설명",
|
|
72
|
+
"config": {
|
|
73
|
+
"inputType": "text",
|
|
74
|
+
"placeholder": "입력 안내"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "subagent",
|
|
79
|
+
"label": "에이전트 이름",
|
|
80
|
+
"description": "에이전트 역할 설명",
|
|
81
|
+
"config": {
|
|
82
|
+
"role": "researcher|writer|analyst|coder|custom",
|
|
83
|
+
"tools": ["Read", "Write"],
|
|
84
|
+
"model": "sonnet",
|
|
85
|
+
"systemPrompt": "에이전트의 구체적인 지시사항"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"type": "skill",
|
|
90
|
+
"label": "스킬 이름",
|
|
91
|
+
"description": "스킬 설명",
|
|
92
|
+
"config": {
|
|
93
|
+
"skillType": "official",
|
|
94
|
+
"skillId": "image-gen-nanobanana"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "skill",
|
|
99
|
+
"label": "커스텀 스킬 이름",
|
|
100
|
+
"description": "커스텀 스킬 설명",
|
|
101
|
+
"config": {
|
|
102
|
+
"skillType": "custom",
|
|
103
|
+
"skillId": "my-custom-skill",
|
|
104
|
+
"skillContent": "---\\nname: my-custom-skill\\ndescription: 커스텀 스킬 설명\\n---\\n\\n# 스킬 내용\\n\\n구체적인 지시사항..."
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"type": "output",
|
|
109
|
+
"label": "출력 이름",
|
|
110
|
+
"description": "출력 설명",
|
|
111
|
+
"config": {
|
|
112
|
+
"outputType": "auto|markdown|document|image"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"edges": [
|
|
117
|
+
{ "from": 0, "to": 1 },
|
|
118
|
+
{ "from": 1, "to": 2 }
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
## 규칙
|
|
123
|
+
1. 항상 input 노드로 시작하고 output 노드로 종료
|
|
124
|
+
2. 기존 공식 스킬로 가능하면 official 스킬 사용
|
|
125
|
+
3. 새로운 기능이 필요하면 custom 스킬 생성 (skillContent에 SKILL.md 형식)
|
|
126
|
+
4. subagent는 복잡한 추론이나 다단계 작업에 사용
|
|
127
|
+
5. edges의 from/to는 nodes 배열의 인덱스 (0부터 시작)
|
|
128
|
+
6. 순차적으로 연결되지 않아도 됨 (병렬 처리, 합류 가능)
|
|
129
|
+
7. systemPrompt는 구체적이고 실행 가능한 지시사항으로 작성
|
|
130
|
+
|
|
131
|
+
## 예시
|
|
132
|
+
|
|
133
|
+
### 예시 1: "이미지 3개 만들어줘"
|
|
134
|
+
{
|
|
135
|
+
"workflowName": "이미지 생성",
|
|
136
|
+
"description": "3개의 이미지를 생성하는 워크플로우",
|
|
137
|
+
"nodes": [
|
|
138
|
+
{ "type": "input", "label": "이미지 설명", "description": "생성할 이미지에 대한 설명", "config": { "inputType": "text", "placeholder": "이미지 프롬프트 입력" } },
|
|
139
|
+
{ "type": "skill", "label": "이미지 생성기", "description": "AI로 이미지 생성", "config": { "skillType": "official", "skillId": "image-gen-nanobanana" } },
|
|
140
|
+
{ "type": "output", "label": "생성된 이미지", "description": "생성된 이미지 결과", "config": { "outputType": "image" } }
|
|
141
|
+
],
|
|
142
|
+
"edges": [{ "from": 0, "to": 1 }, { "from": 1, "to": 2 }]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
### 예시 2: "데이터 분석해서 보고서 만들어줘"
|
|
146
|
+
{
|
|
147
|
+
"workflowName": "데이터 분석 보고서",
|
|
148
|
+
"description": "데이터를 분석하고 보고서를 작성하는 워크플로우",
|
|
149
|
+
"nodes": [
|
|
150
|
+
{ "type": "input", "label": "데이터 파일", "description": "분석할 데이터 파일", "config": { "inputType": "file" } },
|
|
151
|
+
{ "type": "subagent", "label": "데이터 분석가", "description": "데이터를 분석하고 인사이트 도출", "config": { "role": "analyst", "tools": ["Read", "Grep", "Glob"], "model": "sonnet", "systemPrompt": "주어진 데이터를 분석하여 핵심 인사이트를 도출하세요. 통계적 요약, 트렌드, 이상치를 파악하세요." } },
|
|
152
|
+
{ "type": "subagent", "label": "보고서 작성자", "description": "분석 결과로 보고서 작성", "config": { "role": "writer", "tools": ["Read", "Write"], "model": "opus", "systemPrompt": "분석 결과를 바탕으로 경영진을 위한 간결하고 명확한 보고서를 작성하세요." } },
|
|
153
|
+
{ "type": "output", "label": "분석 보고서", "description": "최종 분석 보고서", "config": { "outputType": "document" } }
|
|
154
|
+
],
|
|
155
|
+
"edges": [{ "from": 0, "to": 1 }, { "from": 1, "to": 2 }, { "from": 2, "to": 3 }]
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
export class WorkflowAIService {
|
|
160
|
+
private client: Anthropic;
|
|
161
|
+
|
|
162
|
+
constructor() {
|
|
163
|
+
this.client = new Anthropic({
|
|
164
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async generate(prompt: string): Promise<AIWorkflowResult> {
|
|
169
|
+
const userPrompt = `다음 워크플로우를 설계해주세요: "${prompt}"
|
|
170
|
+
|
|
171
|
+
반드시 JSON만 반환하세요. 마크다운 코드블록 없이 순수 JSON만 응답하세요.`;
|
|
172
|
+
|
|
173
|
+
let responseText = '';
|
|
174
|
+
|
|
175
|
+
// Anthropic SDK 사용
|
|
176
|
+
const response = await this.client.messages.create({
|
|
177
|
+
model: 'claude-sonnet-4-20250514',
|
|
178
|
+
max_tokens: 4096,
|
|
179
|
+
system: SYSTEM_PROMPT,
|
|
180
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// 응답에서 텍스트 추출
|
|
184
|
+
for (const block of response.content) {
|
|
185
|
+
if (block.type === 'text') {
|
|
186
|
+
responseText += block.text;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!responseText) {
|
|
191
|
+
throw new Error('AI 응답을 받지 못했습니다.');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// JSON 블록이 있으면 추출
|
|
195
|
+
const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
196
|
+
const rawJson = jsonMatch ? jsonMatch[1].trim() : responseText.trim();
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const result = JSON.parse(rawJson) as AIWorkflowResult;
|
|
200
|
+
return this.validateAndNormalize(result);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error('Failed to parse AI response:', rawJson);
|
|
203
|
+
throw new Error('AI 응답을 파싱하는데 실패했습니다. 다시 시도해주세요.');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private validateAndNormalize(result: AIWorkflowResult): AIWorkflowResult {
|
|
208
|
+
// 필수 필드 검증
|
|
209
|
+
if (!result.workflowName) {
|
|
210
|
+
result.workflowName = 'AI Generated Workflow';
|
|
211
|
+
}
|
|
212
|
+
if (!result.nodes || result.nodes.length === 0) {
|
|
213
|
+
throw new Error('노드가 생성되지 않았습니다.');
|
|
214
|
+
}
|
|
215
|
+
if (!result.edges) {
|
|
216
|
+
result.edges = [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 노드 정규화
|
|
220
|
+
result.nodes = result.nodes.map((node, index) => ({
|
|
221
|
+
...node,
|
|
222
|
+
label: node.label || `Node ${index + 1}`,
|
|
223
|
+
description: node.description || '',
|
|
224
|
+
config: node.config || {},
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
// 엣지 검증 (범위 체크)
|
|
228
|
+
result.edges = result.edges.filter(
|
|
229
|
+
(edge) =>
|
|
230
|
+
edge.from >= 0 &&
|
|
231
|
+
edge.from < result.nodes.length &&
|
|
232
|
+
edge.to >= 0 &&
|
|
233
|
+
edge.to < result.nodes.length
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const workflowAIService = new WorkflowAIService();
|