gencow 0.1.109 → 0.1.110
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/lib/readme-codegen.mjs +6 -2
- package/package.json +1 -1
- package/templates/ai-chat/ai.ts +126 -62
- package/templates/ai.ts +126 -62
- package/templates/fullstack/ai.ts +126 -62
package/lib/readme-codegen.mjs
CHANGED
|
@@ -296,16 +296,20 @@ export function buildAiPrompt(apiObj, namespaces) {
|
|
|
296
296
|
md += `- 반드시 useQuery와 useMutation을 사용해서 데이터와 연결해줘.\n`;
|
|
297
297
|
md += `- fetch()를 직접 호출하거나 apiPost() 같은 래퍼를 만들지 마.\n`;
|
|
298
298
|
md += `- gencow/api.ts는 자동 생성된 파일이야. 수동으로 만들지 마.\n`;
|
|
299
|
+
md += `- ⚠️ gencow/ai.ts는 자동 생성 파일이야. 절대 수정하지 마. 에러 발생 시 환경변수(.env)를 확인하거나 \`npx gencow add AI\`로 재설치해.\n`;
|
|
299
300
|
md += `- gencow/index.ts의 re-export는 export * as moduleName from "./moduleName" 패턴을 써.\n`;
|
|
300
301
|
md += ` Module, Mod 같은 접미사를 붙이지 마.\n`;
|
|
301
302
|
md += `\n`;
|
|
302
303
|
md += `⚠️ mutation 제한:\n`;
|
|
303
304
|
md += `- mutation은 10초 이내에 완료되어야 해. 외부 API나 LLM 호출이 길면 단계별로 분리해.\n`;
|
|
304
|
-
md += `-
|
|
305
|
-
md +=
|
|
305
|
+
md += `- ✅ 1순위: 각 단계를 별도 mutation으로 분리하고, 프론트엔드에서 순차 호출해.\n`;
|
|
306
|
+
md += ` 예: 크롤링(Step1) → 필터링(Step2) → 요약(Step3) 각각 별도 mutation으로 분리.\n`;
|
|
307
|
+
md += ` 프론트엔드: await crawl(); await filter(); await summarize();\n`;
|
|
308
|
+
md += `- ⚠️ 보조: ctx.scheduler.runAfter(0, "module.nextStep", { sessionId }) — 로컬 dev에서만 안정적. 클라우드에서는 앱 sleep 시 타이머가 소멸할 수 있어.\n`;
|
|
306
309
|
md += `- 같은 서버의 다른 모듈 함수를 호출할 때는 fetch()가 아닌 직접 import해서 호출해.\n`;
|
|
307
310
|
md += ` 예: import { fetchNews } from "./naverApi"; → const result = await fetchNews.handler(ctx, { keyword });\n`;
|
|
308
311
|
md += ` HTTP self-fetch (fetch("/api/mutation")) 패턴은 불필요한 네트워크 우회이므로 사용하지 마.\n`;
|
|
312
|
+
|
|
309
313
|
md += `\n`;
|
|
310
314
|
md += `크론 잡 (예약 작업):\n`;
|
|
311
315
|
md += ` gencow/crons.ts에서 cronJobs()로 선언\n`;
|
package/package.json
CHANGED
package/templates/ai-chat/ai.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// ⚠️ 이 파일은 Gencow AI 엔진이 자동 생성합니다. 직접 수정하지 마세요.
|
|
2
|
+
// ⚠️ 에러 발생 시 코드를 수정하지 말고 환경변수(.env)를 확인하거나 `npx gencow add AI`로 재설치하세요.
|
|
3
|
+
// ⚠️ PROXY_URL/API_KEY 관련 코드를 삭제하면 클라우드 배포 시 AI가 작동하지 않습니다.
|
|
4
|
+
|
|
1
5
|
import { generateText, streamText, embed, embedMany, generateObject } from "ai";
|
|
2
6
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
7
|
|
|
@@ -40,6 +44,42 @@ function resolveModel(model?: Parameters<typeof generateText>[0]["model"]) {
|
|
|
40
44
|
return model ?? defaultModel;
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/**
|
|
48
|
+
* AI 프록시 에러 진단 — 모든 AI 메서드에서 공통 사용.
|
|
49
|
+
* 에러 발생 시 구체적인 진단 메시지를 출력하고 원본 에러를 다시 throw.
|
|
50
|
+
* ⚠️ 이 함수는 ai.ts 수정 금지를 바이브코더에게 전달하는 핵심 방어 메커니즘.
|
|
51
|
+
*/
|
|
52
|
+
function wrapProxyError(err: unknown): never {
|
|
53
|
+
// S2: HTTP status 코드 우선 확인 (AI SDK 에러 객체 구조)
|
|
54
|
+
const status = (err as { status?: number; statusCode?: number }).status
|
|
55
|
+
?? (err as { status?: number; statusCode?: number }).statusCode;
|
|
56
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
57
|
+
|
|
58
|
+
const isNotFound = status === 404 || (!status && msg.includes("Not Found"));
|
|
59
|
+
const isAuthError = status === 401 || status === 403
|
|
60
|
+
|| (!status && (msg.includes("Unauthenticated") || msg.includes("Unauthorized")));
|
|
61
|
+
|
|
62
|
+
if (isNotFound) {
|
|
63
|
+
const proxyUrl = process.env.GENCOW_AI_PROXY_URL;
|
|
64
|
+
console.error(
|
|
65
|
+
`\n🔴 AI Proxy 연결 실패 (404 Not Found)\n` +
|
|
66
|
+
` GENCOW_AI_PROXY_URL: ${proxyUrl || "(미설정)"}\n` +
|
|
67
|
+
` 가능한 원인:\n` +
|
|
68
|
+
` 1. 플랫폼 서버가 최신 버전으로 배포되지 않았을 수 있습니다.\n` +
|
|
69
|
+
` 2. 환경변수가 올바르게 주입되지 않았을 수 있습니다.\n` +
|
|
70
|
+
` ⚠️ 이 파일(ai.ts)을 수정하지 마세요. npx gencow add AI로 재설치하세요.\n`
|
|
71
|
+
);
|
|
72
|
+
} else if (isAuthError) {
|
|
73
|
+
console.error(
|
|
74
|
+
`\n🔴 AI 인증 실패\n` +
|
|
75
|
+
` API_KEY 설정 여부: ${API_KEY ? "✓ 설정됨" : "✗ 미설정"}\n` +
|
|
76
|
+
` PROXY_URL: ${PROXY_URL || "(미설정 — OpenAI 직접 호출)"}\n` +
|
|
77
|
+
` ⚠️ .env 파일의 OPENAI_API_KEY를 확인하세요.\n`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
43
83
|
/**
|
|
44
84
|
* Gencow AI Engine — Vercel AI SDK 래퍼
|
|
45
85
|
*
|
|
@@ -73,32 +113,36 @@ export const ai = {
|
|
|
73
113
|
responseFormat?: { type: string };
|
|
74
114
|
}) {
|
|
75
115
|
const resolvedModel = resolveModel(options.model);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
try {
|
|
117
|
+
const result = await generateText({
|
|
118
|
+
model: resolvedModel,
|
|
119
|
+
system: options.system,
|
|
120
|
+
messages: options.messages as Parameters<typeof generateText>[0]["messages"],
|
|
121
|
+
tools: options.tools as Parameters<typeof generateText>[0]["tools"],
|
|
122
|
+
...(options.temperature !== undefined && { temperature: options.temperature }),
|
|
123
|
+
...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
|
|
124
|
+
// responseFormat → providerOptions (OpenAI JSON mode)
|
|
125
|
+
...(options.responseFormat?.type === "json_object" && {
|
|
126
|
+
providerOptions: {
|
|
127
|
+
openai: { response_format: { type: "json_object" } },
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
} as Parameters<typeof generateText>[0]);
|
|
131
|
+
// AIResult 호환 객체 반환
|
|
132
|
+
const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
|
|
133
|
+
return {
|
|
134
|
+
text: result.text,
|
|
135
|
+
usage: {
|
|
136
|
+
promptTokens: usage?.promptTokens ?? 0,
|
|
137
|
+
completionTokens: usage?.completionTokens ?? 0,
|
|
138
|
+
totalTokens: usage?.totalTokens ?? 0,
|
|
87
139
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
usage: {
|
|
95
|
-
promptTokens: usage?.promptTokens ?? 0,
|
|
96
|
-
completionTokens: usage?.completionTokens ?? 0,
|
|
97
|
-
totalTokens: usage?.totalTokens ?? 0,
|
|
98
|
-
},
|
|
99
|
-
creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
|
|
100
|
-
model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
|
|
101
|
-
};
|
|
140
|
+
creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
|
|
141
|
+
model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
|
|
142
|
+
};
|
|
143
|
+
} catch (err: unknown) {
|
|
144
|
+
wrapProxyError(err);
|
|
145
|
+
}
|
|
102
146
|
},
|
|
103
147
|
|
|
104
148
|
/**
|
|
@@ -120,11 +164,15 @@ export const ai = {
|
|
|
120
164
|
system?: string;
|
|
121
165
|
messages: { role: string; content: string }[];
|
|
122
166
|
}) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
167
|
+
try {
|
|
168
|
+
return streamText({
|
|
169
|
+
model: resolveModel(options.model),
|
|
170
|
+
system: options.system,
|
|
171
|
+
messages: options.messages as Parameters<typeof streamText>[0]["messages"],
|
|
172
|
+
});
|
|
173
|
+
} catch (err: unknown) {
|
|
174
|
+
wrapProxyError(err);
|
|
175
|
+
}
|
|
128
176
|
},
|
|
129
177
|
|
|
130
178
|
/**
|
|
@@ -134,11 +182,15 @@ export const ai = {
|
|
|
134
182
|
* const vector = await ai.embed("검색할 텍스트");
|
|
135
183
|
*/
|
|
136
184
|
async embed(text: string, model?: Parameters<typeof embed>[0]["model"]) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
185
|
+
try {
|
|
186
|
+
const { embedding } = await embed({
|
|
187
|
+
model: model ?? openaiProvider.embedding("text-embedding-3-small"),
|
|
188
|
+
value: text,
|
|
189
|
+
});
|
|
190
|
+
return embedding;
|
|
191
|
+
} catch (err: unknown) {
|
|
192
|
+
wrapProxyError(err);
|
|
193
|
+
}
|
|
142
194
|
},
|
|
143
195
|
|
|
144
196
|
/**
|
|
@@ -151,11 +203,15 @@ export const ai = {
|
|
|
151
203
|
* const embeddings = await ai.embedMany(["텍스트1", "텍스트2", "텍스트3"]);
|
|
152
204
|
*/
|
|
153
205
|
async embedMany(texts: string[], model?: Parameters<typeof embedMany>[0]["model"]) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
206
|
+
try {
|
|
207
|
+
const { embeddings } = await embedMany({
|
|
208
|
+
model: model ?? openaiProvider.embedding("text-embedding-3-small"),
|
|
209
|
+
values: texts,
|
|
210
|
+
});
|
|
211
|
+
return embeddings;
|
|
212
|
+
} catch (err: unknown) {
|
|
213
|
+
wrapProxyError(err);
|
|
214
|
+
}
|
|
159
215
|
},
|
|
160
216
|
|
|
161
217
|
/**
|
|
@@ -175,12 +231,16 @@ export const ai = {
|
|
|
175
231
|
prompt: string;
|
|
176
232
|
system?: string;
|
|
177
233
|
}) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
234
|
+
try {
|
|
235
|
+
return generateObject({
|
|
236
|
+
model: resolveModel(options.model),
|
|
237
|
+
schema: options.schema,
|
|
238
|
+
prompt: options.prompt,
|
|
239
|
+
system: options.system,
|
|
240
|
+
} as Parameters<typeof generateObject>[0]);
|
|
241
|
+
} catch (err: unknown) {
|
|
242
|
+
wrapProxyError(err);
|
|
243
|
+
}
|
|
184
244
|
},
|
|
185
245
|
|
|
186
246
|
/**
|
|
@@ -207,23 +267,27 @@ export const ai = {
|
|
|
207
267
|
maxSteps?: number;
|
|
208
268
|
onStepFinish?: (step: unknown) => void;
|
|
209
269
|
}) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
270
|
+
try {
|
|
271
|
+
const result = await generateText({
|
|
272
|
+
model: resolveModel(options.model),
|
|
273
|
+
system: options.system,
|
|
274
|
+
messages: options.messages as Parameters<typeof generateText>[0]["messages"],
|
|
275
|
+
tools: options.tools as Parameters<typeof generateText>[0]["tools"],
|
|
276
|
+
maxSteps: options.maxSteps ?? 5,
|
|
277
|
+
onStepFinish: options.onStepFinish,
|
|
278
|
+
} as Parameters<typeof generateText>[0]);
|
|
218
279
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
280
|
+
const r = result as unknown as Record<string, unknown>;
|
|
281
|
+
return {
|
|
282
|
+
text: result.text,
|
|
283
|
+
steps: r.steps ?? [],
|
|
284
|
+
toolCalls: r.toolCalls ?? [],
|
|
285
|
+
toolResults: r.toolResults ?? [],
|
|
286
|
+
usage: r.usage,
|
|
287
|
+
};
|
|
288
|
+
} catch (err: unknown) {
|
|
289
|
+
wrapProxyError(err);
|
|
290
|
+
}
|
|
227
291
|
},
|
|
228
292
|
|
|
229
293
|
/**
|
package/templates/ai.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// ⚠️ 이 파일은 Gencow AI 엔진이 자동 생성합니다. 직접 수정하지 마세요.
|
|
2
|
+
// ⚠️ 에러 발생 시 코드를 수정하지 말고 환경변수(.env)를 확인하거나 `npx gencow add AI`로 재설치하세요.
|
|
3
|
+
// ⚠️ PROXY_URL/API_KEY 관련 코드를 삭제하면 클라우드 배포 시 AI가 작동하지 않습니다.
|
|
4
|
+
|
|
1
5
|
import { generateText, streamText, embed, embedMany, generateObject } from "ai";
|
|
2
6
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
7
|
|
|
@@ -40,6 +44,42 @@ function resolveModel(model?: Parameters<typeof generateText>[0]["model"]) {
|
|
|
40
44
|
return model ?? defaultModel;
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/**
|
|
48
|
+
* AI 프록시 에러 진단 — 모든 AI 메서드에서 공통 사용.
|
|
49
|
+
* 에러 발생 시 구체적인 진단 메시지를 출력하고 원본 에러를 다시 throw.
|
|
50
|
+
* ⚠️ 이 함수는 ai.ts 수정 금지를 바이브코더에게 전달하는 핵심 방어 메커니즘.
|
|
51
|
+
*/
|
|
52
|
+
function wrapProxyError(err: unknown): never {
|
|
53
|
+
// S2: HTTP status 코드 우선 확인 (AI SDK 에러 객체 구조)
|
|
54
|
+
const status = (err as { status?: number; statusCode?: number }).status
|
|
55
|
+
?? (err as { status?: number; statusCode?: number }).statusCode;
|
|
56
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
57
|
+
|
|
58
|
+
const isNotFound = status === 404 || (!status && msg.includes("Not Found"));
|
|
59
|
+
const isAuthError = status === 401 || status === 403
|
|
60
|
+
|| (!status && (msg.includes("Unauthenticated") || msg.includes("Unauthorized")));
|
|
61
|
+
|
|
62
|
+
if (isNotFound) {
|
|
63
|
+
const proxyUrl = process.env.GENCOW_AI_PROXY_URL;
|
|
64
|
+
console.error(
|
|
65
|
+
`\n🔴 AI Proxy 연결 실패 (404 Not Found)\n` +
|
|
66
|
+
` GENCOW_AI_PROXY_URL: ${proxyUrl || "(미설정)"}\n` +
|
|
67
|
+
` 가능한 원인:\n` +
|
|
68
|
+
` 1. 플랫폼 서버가 최신 버전으로 배포되지 않았을 수 있습니다.\n` +
|
|
69
|
+
` 2. 환경변수가 올바르게 주입되지 않았을 수 있습니다.\n` +
|
|
70
|
+
` ⚠️ 이 파일(ai.ts)을 수정하지 마세요. npx gencow add AI로 재설치하세요.\n`
|
|
71
|
+
);
|
|
72
|
+
} else if (isAuthError) {
|
|
73
|
+
console.error(
|
|
74
|
+
`\n🔴 AI 인증 실패\n` +
|
|
75
|
+
` API_KEY 설정 여부: ${API_KEY ? "✓ 설정됨" : "✗ 미설정"}\n` +
|
|
76
|
+
` PROXY_URL: ${PROXY_URL || "(미설정 — OpenAI 직접 호출)"}\n` +
|
|
77
|
+
` ⚠️ .env 파일의 OPENAI_API_KEY를 확인하세요.\n`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
43
83
|
/**
|
|
44
84
|
* Gencow AI Engine — Vercel AI SDK 래퍼
|
|
45
85
|
*
|
|
@@ -73,32 +113,36 @@ export const ai = {
|
|
|
73
113
|
responseFormat?: { type: string };
|
|
74
114
|
}) {
|
|
75
115
|
const resolvedModel = resolveModel(options.model);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
try {
|
|
117
|
+
const result = await generateText({
|
|
118
|
+
model: resolvedModel,
|
|
119
|
+
system: options.system,
|
|
120
|
+
messages: options.messages as Parameters<typeof generateText>[0]["messages"],
|
|
121
|
+
tools: options.tools as Parameters<typeof generateText>[0]["tools"],
|
|
122
|
+
...(options.temperature !== undefined && { temperature: options.temperature }),
|
|
123
|
+
...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
|
|
124
|
+
// responseFormat → providerOptions (OpenAI JSON mode)
|
|
125
|
+
...(options.responseFormat?.type === "json_object" && {
|
|
126
|
+
providerOptions: {
|
|
127
|
+
openai: { response_format: { type: "json_object" } },
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
} as Parameters<typeof generateText>[0]);
|
|
131
|
+
// AIResult 호환 객체 반환
|
|
132
|
+
const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
|
|
133
|
+
return {
|
|
134
|
+
text: result.text,
|
|
135
|
+
usage: {
|
|
136
|
+
promptTokens: usage?.promptTokens ?? 0,
|
|
137
|
+
completionTokens: usage?.completionTokens ?? 0,
|
|
138
|
+
totalTokens: usage?.totalTokens ?? 0,
|
|
87
139
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
usage: {
|
|
95
|
-
promptTokens: usage?.promptTokens ?? 0,
|
|
96
|
-
completionTokens: usage?.completionTokens ?? 0,
|
|
97
|
-
totalTokens: usage?.totalTokens ?? 0,
|
|
98
|
-
},
|
|
99
|
-
creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
|
|
100
|
-
model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
|
|
101
|
-
};
|
|
140
|
+
creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
|
|
141
|
+
model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
|
|
142
|
+
};
|
|
143
|
+
} catch (err: unknown) {
|
|
144
|
+
wrapProxyError(err);
|
|
145
|
+
}
|
|
102
146
|
},
|
|
103
147
|
|
|
104
148
|
/**
|
|
@@ -120,11 +164,15 @@ export const ai = {
|
|
|
120
164
|
system?: string;
|
|
121
165
|
messages: { role: string; content: string }[];
|
|
122
166
|
}) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
167
|
+
try {
|
|
168
|
+
return streamText({
|
|
169
|
+
model: resolveModel(options.model),
|
|
170
|
+
system: options.system,
|
|
171
|
+
messages: options.messages as Parameters<typeof streamText>[0]["messages"],
|
|
172
|
+
});
|
|
173
|
+
} catch (err: unknown) {
|
|
174
|
+
wrapProxyError(err);
|
|
175
|
+
}
|
|
128
176
|
},
|
|
129
177
|
|
|
130
178
|
/**
|
|
@@ -134,11 +182,15 @@ export const ai = {
|
|
|
134
182
|
* const vector = await ai.embed("검색할 텍스트");
|
|
135
183
|
*/
|
|
136
184
|
async embed(text: string, model?: Parameters<typeof embed>[0]["model"]) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
185
|
+
try {
|
|
186
|
+
const { embedding } = await embed({
|
|
187
|
+
model: model ?? openaiProvider.embedding("text-embedding-3-small"),
|
|
188
|
+
value: text,
|
|
189
|
+
});
|
|
190
|
+
return embedding;
|
|
191
|
+
} catch (err: unknown) {
|
|
192
|
+
wrapProxyError(err);
|
|
193
|
+
}
|
|
142
194
|
},
|
|
143
195
|
|
|
144
196
|
/**
|
|
@@ -151,11 +203,15 @@ export const ai = {
|
|
|
151
203
|
* const embeddings = await ai.embedMany(["텍스트1", "텍스트2", "텍스트3"]);
|
|
152
204
|
*/
|
|
153
205
|
async embedMany(texts: string[], model?: Parameters<typeof embedMany>[0]["model"]) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
206
|
+
try {
|
|
207
|
+
const { embeddings } = await embedMany({
|
|
208
|
+
model: model ?? openaiProvider.embedding("text-embedding-3-small"),
|
|
209
|
+
values: texts,
|
|
210
|
+
});
|
|
211
|
+
return embeddings;
|
|
212
|
+
} catch (err: unknown) {
|
|
213
|
+
wrapProxyError(err);
|
|
214
|
+
}
|
|
159
215
|
},
|
|
160
216
|
|
|
161
217
|
/**
|
|
@@ -175,12 +231,16 @@ export const ai = {
|
|
|
175
231
|
prompt: string;
|
|
176
232
|
system?: string;
|
|
177
233
|
}) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
234
|
+
try {
|
|
235
|
+
return generateObject({
|
|
236
|
+
model: resolveModel(options.model),
|
|
237
|
+
schema: options.schema,
|
|
238
|
+
prompt: options.prompt,
|
|
239
|
+
system: options.system,
|
|
240
|
+
} as Parameters<typeof generateObject>[0]);
|
|
241
|
+
} catch (err: unknown) {
|
|
242
|
+
wrapProxyError(err);
|
|
243
|
+
}
|
|
184
244
|
},
|
|
185
245
|
|
|
186
246
|
/**
|
|
@@ -207,23 +267,27 @@ export const ai = {
|
|
|
207
267
|
maxSteps?: number;
|
|
208
268
|
onStepFinish?: (step: unknown) => void;
|
|
209
269
|
}) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
270
|
+
try {
|
|
271
|
+
const result = await generateText({
|
|
272
|
+
model: resolveModel(options.model),
|
|
273
|
+
system: options.system,
|
|
274
|
+
messages: options.messages as Parameters<typeof generateText>[0]["messages"],
|
|
275
|
+
tools: options.tools as Parameters<typeof generateText>[0]["tools"],
|
|
276
|
+
maxSteps: options.maxSteps ?? 5,
|
|
277
|
+
onStepFinish: options.onStepFinish,
|
|
278
|
+
} as Parameters<typeof generateText>[0]);
|
|
218
279
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
280
|
+
const r = result as unknown as Record<string, unknown>;
|
|
281
|
+
return {
|
|
282
|
+
text: result.text,
|
|
283
|
+
steps: r.steps ?? [],
|
|
284
|
+
toolCalls: r.toolCalls ?? [],
|
|
285
|
+
toolResults: r.toolResults ?? [],
|
|
286
|
+
usage: r.usage,
|
|
287
|
+
};
|
|
288
|
+
} catch (err: unknown) {
|
|
289
|
+
wrapProxyError(err);
|
|
290
|
+
}
|
|
227
291
|
},
|
|
228
292
|
|
|
229
293
|
/**
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// ⚠️ 이 파일은 Gencow AI 엔진이 자동 생성합니다. 직접 수정하지 마세요.
|
|
2
|
+
// ⚠️ 에러 발생 시 코드를 수정하지 말고 환경변수(.env)를 확인하거나 `npx gencow add AI`로 재설치하세요.
|
|
3
|
+
// ⚠️ PROXY_URL/API_KEY 관련 코드를 삭제하면 클라우드 배포 시 AI가 작동하지 않습니다.
|
|
4
|
+
|
|
1
5
|
import { generateText, streamText, embed, embedMany, generateObject } from "ai";
|
|
2
6
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
7
|
|
|
@@ -40,6 +44,42 @@ function resolveModel(model?: Parameters<typeof generateText>[0]["model"]) {
|
|
|
40
44
|
return model ?? defaultModel;
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/**
|
|
48
|
+
* AI 프록시 에러 진단 — 모든 AI 메서드에서 공통 사용.
|
|
49
|
+
* 에러 발생 시 구체적인 진단 메시지를 출력하고 원본 에러를 다시 throw.
|
|
50
|
+
* ⚠️ 이 함수는 ai.ts 수정 금지를 바이브코더에게 전달하는 핵심 방어 메커니즘.
|
|
51
|
+
*/
|
|
52
|
+
function wrapProxyError(err: unknown): never {
|
|
53
|
+
// S2: HTTP status 코드 우선 확인 (AI SDK 에러 객체 구조)
|
|
54
|
+
const status = (err as { status?: number; statusCode?: number }).status
|
|
55
|
+
?? (err as { status?: number; statusCode?: number }).statusCode;
|
|
56
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
57
|
+
|
|
58
|
+
const isNotFound = status === 404 || (!status && msg.includes("Not Found"));
|
|
59
|
+
const isAuthError = status === 401 || status === 403
|
|
60
|
+
|| (!status && (msg.includes("Unauthenticated") || msg.includes("Unauthorized")));
|
|
61
|
+
|
|
62
|
+
if (isNotFound) {
|
|
63
|
+
const proxyUrl = process.env.GENCOW_AI_PROXY_URL;
|
|
64
|
+
console.error(
|
|
65
|
+
`\n🔴 AI Proxy 연결 실패 (404 Not Found)\n` +
|
|
66
|
+
` GENCOW_AI_PROXY_URL: ${proxyUrl || "(미설정)"}\n` +
|
|
67
|
+
` 가능한 원인:\n` +
|
|
68
|
+
` 1. 플랫폼 서버가 최신 버전으로 배포되지 않았을 수 있습니다.\n` +
|
|
69
|
+
` 2. 환경변수가 올바르게 주입되지 않았을 수 있습니다.\n` +
|
|
70
|
+
` ⚠️ 이 파일(ai.ts)을 수정하지 마세요. npx gencow add AI로 재설치하세요.\n`
|
|
71
|
+
);
|
|
72
|
+
} else if (isAuthError) {
|
|
73
|
+
console.error(
|
|
74
|
+
`\n🔴 AI 인증 실패\n` +
|
|
75
|
+
` API_KEY 설정 여부: ${API_KEY ? "✓ 설정됨" : "✗ 미설정"}\n` +
|
|
76
|
+
` PROXY_URL: ${PROXY_URL || "(미설정 — OpenAI 직접 호출)"}\n` +
|
|
77
|
+
` ⚠️ .env 파일의 OPENAI_API_KEY를 확인하세요.\n`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
43
83
|
/**
|
|
44
84
|
* Gencow AI Engine — Vercel AI SDK 래퍼
|
|
45
85
|
*
|
|
@@ -73,32 +113,36 @@ export const ai = {
|
|
|
73
113
|
responseFormat?: { type: string };
|
|
74
114
|
}) {
|
|
75
115
|
const resolvedModel = resolveModel(options.model);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
try {
|
|
117
|
+
const result = await generateText({
|
|
118
|
+
model: resolvedModel,
|
|
119
|
+
system: options.system,
|
|
120
|
+
messages: options.messages as Parameters<typeof generateText>[0]["messages"],
|
|
121
|
+
tools: options.tools as Parameters<typeof generateText>[0]["tools"],
|
|
122
|
+
...(options.temperature !== undefined && { temperature: options.temperature }),
|
|
123
|
+
...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
|
|
124
|
+
// responseFormat → providerOptions (OpenAI JSON mode)
|
|
125
|
+
...(options.responseFormat?.type === "json_object" && {
|
|
126
|
+
providerOptions: {
|
|
127
|
+
openai: { response_format: { type: "json_object" } },
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
} as Parameters<typeof generateText>[0]);
|
|
131
|
+
// AIResult 호환 객체 반환
|
|
132
|
+
const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
|
|
133
|
+
return {
|
|
134
|
+
text: result.text,
|
|
135
|
+
usage: {
|
|
136
|
+
promptTokens: usage?.promptTokens ?? 0,
|
|
137
|
+
completionTokens: usage?.completionTokens ?? 0,
|
|
138
|
+
totalTokens: usage?.totalTokens ?? 0,
|
|
87
139
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
usage: {
|
|
95
|
-
promptTokens: usage?.promptTokens ?? 0,
|
|
96
|
-
completionTokens: usage?.completionTokens ?? 0,
|
|
97
|
-
totalTokens: usage?.totalTokens ?? 0,
|
|
98
|
-
},
|
|
99
|
-
creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
|
|
100
|
-
model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
|
|
101
|
-
};
|
|
140
|
+
creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
|
|
141
|
+
model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
|
|
142
|
+
};
|
|
143
|
+
} catch (err: unknown) {
|
|
144
|
+
wrapProxyError(err);
|
|
145
|
+
}
|
|
102
146
|
},
|
|
103
147
|
|
|
104
148
|
/**
|
|
@@ -120,11 +164,15 @@ export const ai = {
|
|
|
120
164
|
system?: string;
|
|
121
165
|
messages: { role: string; content: string }[];
|
|
122
166
|
}) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
167
|
+
try {
|
|
168
|
+
return streamText({
|
|
169
|
+
model: resolveModel(options.model),
|
|
170
|
+
system: options.system,
|
|
171
|
+
messages: options.messages as Parameters<typeof streamText>[0]["messages"],
|
|
172
|
+
});
|
|
173
|
+
} catch (err: unknown) {
|
|
174
|
+
wrapProxyError(err);
|
|
175
|
+
}
|
|
128
176
|
},
|
|
129
177
|
|
|
130
178
|
/**
|
|
@@ -134,11 +182,15 @@ export const ai = {
|
|
|
134
182
|
* const vector = await ai.embed("검색할 텍스트");
|
|
135
183
|
*/
|
|
136
184
|
async embed(text: string, model?: Parameters<typeof embed>[0]["model"]) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
185
|
+
try {
|
|
186
|
+
const { embedding } = await embed({
|
|
187
|
+
model: model ?? openaiProvider.embedding("text-embedding-3-small"),
|
|
188
|
+
value: text,
|
|
189
|
+
});
|
|
190
|
+
return embedding;
|
|
191
|
+
} catch (err: unknown) {
|
|
192
|
+
wrapProxyError(err);
|
|
193
|
+
}
|
|
142
194
|
},
|
|
143
195
|
|
|
144
196
|
/**
|
|
@@ -151,11 +203,15 @@ export const ai = {
|
|
|
151
203
|
* const embeddings = await ai.embedMany(["텍스트1", "텍스트2", "텍스트3"]);
|
|
152
204
|
*/
|
|
153
205
|
async embedMany(texts: string[], model?: Parameters<typeof embedMany>[0]["model"]) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
206
|
+
try {
|
|
207
|
+
const { embeddings } = await embedMany({
|
|
208
|
+
model: model ?? openaiProvider.embedding("text-embedding-3-small"),
|
|
209
|
+
values: texts,
|
|
210
|
+
});
|
|
211
|
+
return embeddings;
|
|
212
|
+
} catch (err: unknown) {
|
|
213
|
+
wrapProxyError(err);
|
|
214
|
+
}
|
|
159
215
|
},
|
|
160
216
|
|
|
161
217
|
/**
|
|
@@ -175,12 +231,16 @@ export const ai = {
|
|
|
175
231
|
prompt: string;
|
|
176
232
|
system?: string;
|
|
177
233
|
}) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
234
|
+
try {
|
|
235
|
+
return generateObject({
|
|
236
|
+
model: resolveModel(options.model),
|
|
237
|
+
schema: options.schema,
|
|
238
|
+
prompt: options.prompt,
|
|
239
|
+
system: options.system,
|
|
240
|
+
} as Parameters<typeof generateObject>[0]);
|
|
241
|
+
} catch (err: unknown) {
|
|
242
|
+
wrapProxyError(err);
|
|
243
|
+
}
|
|
184
244
|
},
|
|
185
245
|
|
|
186
246
|
/**
|
|
@@ -207,23 +267,27 @@ export const ai = {
|
|
|
207
267
|
maxSteps?: number;
|
|
208
268
|
onStepFinish?: (step: unknown) => void;
|
|
209
269
|
}) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
270
|
+
try {
|
|
271
|
+
const result = await generateText({
|
|
272
|
+
model: resolveModel(options.model),
|
|
273
|
+
system: options.system,
|
|
274
|
+
messages: options.messages as Parameters<typeof generateText>[0]["messages"],
|
|
275
|
+
tools: options.tools as Parameters<typeof generateText>[0]["tools"],
|
|
276
|
+
maxSteps: options.maxSteps ?? 5,
|
|
277
|
+
onStepFinish: options.onStepFinish,
|
|
278
|
+
} as Parameters<typeof generateText>[0]);
|
|
218
279
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
280
|
+
const r = result as unknown as Record<string, unknown>;
|
|
281
|
+
return {
|
|
282
|
+
text: result.text,
|
|
283
|
+
steps: r.steps ?? [],
|
|
284
|
+
toolCalls: r.toolCalls ?? [],
|
|
285
|
+
toolResults: r.toolResults ?? [],
|
|
286
|
+
usage: r.usage,
|
|
287
|
+
};
|
|
288
|
+
} catch (err: unknown) {
|
|
289
|
+
wrapProxyError(err);
|
|
290
|
+
}
|
|
227
291
|
},
|
|
228
292
|
|
|
229
293
|
/**
|