@cartanova/qgrid-cli 1.5.6 → 1.6.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/bundle/dist/application/qgrid/oauth.js +1 -1
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +166 -0
- package/bundle/dist/application/qgrid/qgrid.frame.js +28 -36
- package/bundle/dist/application/qgrid/qgrid.types.js +5 -16
- package/bundle/dist/i18n/sd.generated.js +2 -2
- package/bundle/dist/sonamu.config.js +6 -9
- package/bundle/src/application/qgrid/oauth.ts +1 -1
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +184 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +28 -50
- package/bundle/src/application/qgrid/qgrid.types.ts +2 -14
- package/bundle/src/application/sonamu.generated.http +2 -1
- package/bundle/src/i18n/sd.generated.ts +1 -1
- package/bundle/src/sonamu.config.ts +4 -7
- package/bundle/web-dist/client/assets/index-D8eEjsZj.js +78 -0
- package/bundle/web-dist/client/assets/{logs-KoJtGoEG.js → logs-CKnJzuH6.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-CqzIoGYg.js → routes-DQKecVGa.js} +1 -1
- package/bundle/web-dist/client/assets/{sd.generated-Duipc8HN.js → sd.generated-NHYHS_-0.js} +1 -1
- package/bundle/web-dist/client/assets/services.generated-DDVXMid7.js +1 -0
- package/bundle/web-dist/client/assets/{show-BuMxiSr1.js → show-D2z1qxJ5.js} +1 -1
- package/bundle/web-dist/client/assets/tokens-BaywaQkm.js +1 -0
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{copy-BPyN_0Dp.js → copy-BVWDOV2T.js} +1 -1
- package/bundle/web-dist/server/assets/{lazyRouteComponent-Bmjwy0OA.js → lazyRouteComponent-BJmwxFyJ.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-BgcF7jVg.js → logs-DgIbufNf.js} +3 -3
- package/bundle/web-dist/server/assets/{routes-DVA-nsZ-.js → routes-x7-hqUm0.js} +2 -2
- package/bundle/web-dist/server/assets/{sd.generated-CDVQy4CJ.js → sd.generated-BZPIp4uL.js} +1 -1
- package/bundle/web-dist/server/assets/{services.generated-DWyvb1vb.js → services.generated-BA0gbdmd.js} +5 -4
- package/bundle/web-dist/server/assets/{shim-CqVZY710.js → shim-DGrmZkac.js} +1 -1
- package/bundle/web-dist/server/assets/{show-BnKnfNFP.js → show-Bk3NayMH.js} +4 -4
- package/bundle/web-dist/server/assets/{tokens-7z55hPRV.js → tokens-CVSwt0x0.js} +6 -17
- package/bundle/web-dist/server/entry-server.generated.js +7 -7
- package/package.json +1 -1
- package/bundle/dist/application/qgrid/pool.js +0 -128
- package/bundle/dist/application/qgrid/worker.js +0 -189
- package/bundle/src/application/qgrid/pool.ts +0 -165
- package/bundle/src/application/qgrid/worker.ts +0 -240
- package/bundle/web-dist/client/assets/index-BnqlZB1V.js +0 -78
- package/bundle/web-dist/client/assets/services.generated-Ccs6tru4.js +0 -1
- package/bundle/web-dist/client/assets/tokens--z-eVto5.js +0 -1
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
generatePKCE,
|
|
12
12
|
refreshAccessToken,
|
|
13
13
|
} from "./oauth";
|
|
14
|
-
import {
|
|
14
|
+
import { QgridDispatcher } from "./qgrid.dispatcher";
|
|
15
15
|
import {
|
|
16
16
|
type CliResult,
|
|
17
17
|
type HealthResponse,
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
type UsageResponse,
|
|
21
21
|
} from "./qgrid.types";
|
|
22
22
|
|
|
23
|
-
// PKCE 세션 메모리 저장 (state → { codeVerifier, name, redirectUri })
|
|
24
23
|
const pendingOAuth = new Map<string, { codeVerifier: string; name: string; redirectUri: string }>();
|
|
25
24
|
|
|
26
25
|
class QgridFrameClass extends BaseFrameClass {
|
|
@@ -29,14 +28,17 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
32
|
-
async query(
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
async query(
|
|
32
|
+
prompt: string,
|
|
33
|
+
system?: string,
|
|
34
|
+
timeout?: number,
|
|
35
|
+
model?: string,
|
|
36
|
+
): Promise<CliResult> {
|
|
37
|
+
const result = await QgridDispatcher.query({ system, prompt, model }, timeout);
|
|
35
38
|
|
|
36
|
-
// 로그 기록 실패해도 쿼리 결과는 반환
|
|
37
39
|
RequestLogModel.save([
|
|
38
40
|
{
|
|
39
|
-
token_name:
|
|
41
|
+
token_name: result.tokenName,
|
|
40
42
|
query: system ? `[System]\n${system}\n\n[User]\n${prompt}` : prompt,
|
|
41
43
|
response: result.text,
|
|
42
44
|
input_tokens: result.usage.input_tokens,
|
|
@@ -53,7 +55,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
53
55
|
|
|
54
56
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
55
57
|
async stats(): Promise<TokenStats[]> {
|
|
56
|
-
return
|
|
58
|
+
return QgridDispatcher.getStats();
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
@@ -70,7 +72,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
70
72
|
...(refreshToken && refreshToken.length > 0 ? { refresh_token: refreshToken } : {}),
|
|
71
73
|
},
|
|
72
74
|
]);
|
|
73
|
-
|
|
75
|
+
QgridDispatcher.addToken(token, name);
|
|
74
76
|
return { added: true };
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -81,7 +83,6 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
81
83
|
newToken?: string,
|
|
82
84
|
refreshToken?: string,
|
|
83
85
|
): Promise<{ updated: boolean }> {
|
|
84
|
-
const pool = getPool();
|
|
85
86
|
const entry = await TokenModel.findByToken("A", token);
|
|
86
87
|
if (!entry) return { updated: false };
|
|
87
88
|
|
|
@@ -97,52 +98,44 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
97
98
|
]);
|
|
98
99
|
|
|
99
100
|
if (hasNewToken) {
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
QgridDispatcher.removeToken(token);
|
|
102
|
+
QgridDispatcher.addToken(newToken, name ?? entry.name);
|
|
102
103
|
}
|
|
103
104
|
return { updated: true };
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
107
108
|
async removeToken(token: string): Promise<{ removed: boolean }> {
|
|
108
|
-
|
|
109
|
-
if (!pool.workers.has(token)) return { removed: false };
|
|
109
|
+
if (!QgridDispatcher.hasToken(token)) return { removed: false };
|
|
110
110
|
|
|
111
111
|
const entry = await TokenModel.findByToken("A", token);
|
|
112
112
|
if (entry) await TokenModel.del([entry.id]);
|
|
113
|
-
|
|
113
|
+
QgridDispatcher.removeToken(token);
|
|
114
114
|
return { removed: true };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
119
118
|
* @param id token_id
|
|
120
|
-
* 토큰 활성화/비활성화 토글 (
|
|
121
|
-
*
|
|
119
|
+
* 토큰 활성화/비활성화 토글 (dispatcher 등록/제거 & DB의 active 필드 업데이트)
|
|
122
120
|
* @returns 토큰 활성화 여부
|
|
123
121
|
*/
|
|
124
122
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
125
123
|
async toggleToken(id: number): Promise<{ active: boolean }> {
|
|
126
|
-
const pool = getPool();
|
|
127
124
|
const entry = await TokenModel.findOne("A", { id });
|
|
128
125
|
if (!entry) return { active: false };
|
|
129
126
|
|
|
130
127
|
const newActive = !entry.active;
|
|
131
128
|
await TokenModel.save([{ id, token: entry.token, active: newActive, name: entry.name }]);
|
|
132
129
|
|
|
133
|
-
// 토큰의 새로운 상태가 활성화면/비활성화면
|
|
134
130
|
if (newActive) {
|
|
135
|
-
|
|
136
|
-
pool.createWorkers(entry.token, entry.name);
|
|
131
|
+
QgridDispatcher.addToken(entry.token, entry.name);
|
|
137
132
|
} else {
|
|
138
|
-
|
|
139
|
-
pool.destroyWorkers(entry.token);
|
|
133
|
+
QgridDispatcher.removeToken(entry.token);
|
|
140
134
|
}
|
|
141
135
|
|
|
142
136
|
return { active: newActive };
|
|
143
137
|
}
|
|
144
138
|
|
|
145
|
-
// OAuth 로그인: authUrl 생성 (프론트에서 이 API 호출 후 authUrl로 리다이렉트)
|
|
146
139
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
147
140
|
async oauthStart(name: string): Promise<OAuthStartResult> {
|
|
148
141
|
const { codeVerifier, codeChallenge, state } = generatePKCE();
|
|
@@ -151,16 +144,13 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
151
144
|
const redirectUri = `http://localhost:${serverPort}/callback`;
|
|
152
145
|
const authUrl = buildAuthUrl(codeChallenge, state, redirectUri);
|
|
153
146
|
|
|
154
|
-
// PKCE를 메모리에 저장 (5분 TTL)
|
|
155
147
|
pendingOAuth.set(state, { codeVerifier, name, redirectUri });
|
|
156
148
|
setTimeout(() => pendingOAuth.delete(state), 300_000);
|
|
157
149
|
|
|
158
150
|
return { authUrl };
|
|
159
151
|
}
|
|
160
152
|
|
|
161
|
-
// OAuth 콜백 처리 — sonamu.config.ts의 custom 라우트에서 호출됨
|
|
162
153
|
async handleOAuthCallback(code: string, state: string, reply: FastifyReply): Promise<void> {
|
|
163
|
-
const pool = getPool();
|
|
164
154
|
const pending = pendingOAuth.get(state);
|
|
165
155
|
if (!pending) {
|
|
166
156
|
return reply.redirect("/?oauth=error&reason=invalid_state");
|
|
@@ -175,16 +165,14 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
175
165
|
pending.redirectUri,
|
|
176
166
|
);
|
|
177
167
|
|
|
178
|
-
// 같은 계정의 이전 토큰이 있으면 교체
|
|
179
168
|
if (tokens.accountUuid) {
|
|
180
169
|
const oldEntries = await TokenModel.findByAccountUuid("A", tokens.accountUuid);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await TokenModel.del(
|
|
170
|
+
if (oldEntries.length > 0) {
|
|
171
|
+
for (const old of oldEntries) QgridDispatcher.removeToken(old.token);
|
|
172
|
+
await TokenModel.del(oldEntries.map((o) => o.id));
|
|
184
173
|
}
|
|
185
174
|
}
|
|
186
175
|
|
|
187
|
-
// 새 토큰 저장 + pool에 등록
|
|
188
176
|
await TokenModel.save([
|
|
189
177
|
{
|
|
190
178
|
token: tokens.accessToken,
|
|
@@ -194,7 +182,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
194
182
|
account_uuid: tokens.accountUuid,
|
|
195
183
|
},
|
|
196
184
|
]);
|
|
197
|
-
|
|
185
|
+
QgridDispatcher.addToken(tokens.accessToken, pending.name);
|
|
198
186
|
|
|
199
187
|
return reply.redirect(`/?oauth=success&name=${encodeURIComponent(pending.name)}`);
|
|
200
188
|
} catch (e) {
|
|
@@ -202,10 +190,8 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
202
190
|
}
|
|
203
191
|
}
|
|
204
192
|
|
|
205
|
-
// 토큰 사용량 조회 (OAuth usage API)
|
|
206
193
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
207
194
|
async usage(tokenName?: string): Promise<UsageResponse> {
|
|
208
|
-
const pool = getPool();
|
|
209
195
|
const { rows: allTokens } = await TokenModel.findMany("A");
|
|
210
196
|
const entry = tokenName
|
|
211
197
|
? allTokens.find((e) => e.name === tokenName)
|
|
@@ -213,26 +199,23 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
213
199
|
|
|
214
200
|
if (!entry) return { error: "NOT_FOUND" };
|
|
215
201
|
|
|
216
|
-
// 만료 확인 + refresh 시도
|
|
217
202
|
let accessToken = entry.token;
|
|
218
203
|
const isExpired = entry.expires_at && Number(entry.expires_at) < Date.now();
|
|
219
204
|
|
|
220
205
|
if (isExpired && entry.refresh_token) {
|
|
221
206
|
try {
|
|
222
|
-
accessToken = await this.refreshToken(
|
|
207
|
+
accessToken = await this.refreshToken(entry);
|
|
223
208
|
} catch (e) {
|
|
224
209
|
console.warn(`[usage] refresh failed for ${entry.name}: ${(e as Error).message}`);
|
|
225
210
|
return { error: "re-login required" };
|
|
226
211
|
}
|
|
227
212
|
}
|
|
228
213
|
|
|
229
|
-
// usage 호출
|
|
230
214
|
const result = await fetchUsage(accessToken);
|
|
231
215
|
|
|
232
|
-
// 인증 에러면 refresh_token으로 재시도
|
|
233
216
|
if (result.error && entry.refresh_token) {
|
|
234
217
|
try {
|
|
235
|
-
accessToken = await this.refreshToken(
|
|
218
|
+
accessToken = await this.refreshToken(entry);
|
|
236
219
|
return await fetchUsage(accessToken);
|
|
237
220
|
} catch (e) {
|
|
238
221
|
console.warn(`[usage] refresh failed for ${entry.name}: ${(e as Error).message}`);
|
|
@@ -243,7 +226,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
243
226
|
return result;
|
|
244
227
|
}
|
|
245
228
|
|
|
246
|
-
async refreshToken(
|
|
229
|
+
async refreshToken(token: RefreshTokenParams): Promise<string> {
|
|
247
230
|
if (!token.refresh_token) throw new Error("No refresh token");
|
|
248
231
|
const refreshed = await refreshAccessToken(token.refresh_token);
|
|
249
232
|
await TokenModel.save([
|
|
@@ -255,19 +238,14 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
255
238
|
name: token.name ?? "",
|
|
256
239
|
},
|
|
257
240
|
]);
|
|
258
|
-
|
|
259
|
-
|
|
241
|
+
QgridDispatcher.removeToken(token.token);
|
|
242
|
+
QgridDispatcher.addToken(refreshed.accessToken, token.name);
|
|
260
243
|
return refreshed.accessToken;
|
|
261
244
|
}
|
|
262
245
|
|
|
263
246
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
264
247
|
async health(): Promise<HealthResponse> {
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
status: "ok",
|
|
268
|
-
workers: [...pool.workers.values()].flat().length,
|
|
269
|
-
activeTokens: pool.workers.size - pool.quotaExhausted.size,
|
|
270
|
-
};
|
|
248
|
+
return { status: "ok", activeTokens: QgridDispatcher.tokens.size };
|
|
271
249
|
}
|
|
272
250
|
}
|
|
273
251
|
|
|
@@ -5,12 +5,14 @@ import { z } from "zod";
|
|
|
5
5
|
export const QueryInput = z.object({
|
|
6
6
|
system: z.string().optional(),
|
|
7
7
|
prompt: z.string(),
|
|
8
|
+
model: z.string().optional(),
|
|
8
9
|
timeout: z.number().optional(),
|
|
9
10
|
});
|
|
10
11
|
export type QueryInput = z.infer<typeof QueryInput>;
|
|
11
12
|
|
|
12
13
|
export const CliResult = z.object({
|
|
13
14
|
text: z.string(),
|
|
15
|
+
tokenName: z.string().optional(),
|
|
14
16
|
usage: z.object({
|
|
15
17
|
input_tokens: z.number(),
|
|
16
18
|
output_tokens: z.number(),
|
|
@@ -22,18 +24,6 @@ export const CliResult = z.object({
|
|
|
22
24
|
});
|
|
23
25
|
export type CliResult = z.infer<typeof CliResult>;
|
|
24
26
|
|
|
25
|
-
// ─── Pool Config ───
|
|
26
|
-
|
|
27
|
-
export const PoolConfig = z.object({
|
|
28
|
-
tokens: z.array(z.object({ token: z.string(), name: z.string() })),
|
|
29
|
-
size: z.number().optional(),
|
|
30
|
-
model: z.string().optional(),
|
|
31
|
-
timeout: z.number().optional(),
|
|
32
|
-
cwd: z.string().optional(),
|
|
33
|
-
maxCalls: z.number().optional(),
|
|
34
|
-
});
|
|
35
|
-
export type PoolConfig = z.infer<typeof PoolConfig>;
|
|
36
|
-
|
|
37
27
|
// ─── Token Management ───
|
|
38
28
|
|
|
39
29
|
export const AddTokenInput = z.object({
|
|
@@ -51,7 +41,6 @@ export const TokenStats = z.object({
|
|
|
51
41
|
token: z.string(),
|
|
52
42
|
name: z.string(),
|
|
53
43
|
requests: z.number(),
|
|
54
|
-
active: z.boolean(),
|
|
55
44
|
});
|
|
56
45
|
export type TokenStats = z.infer<typeof TokenStats>;
|
|
57
46
|
|
|
@@ -93,7 +82,6 @@ export type UsageResponse = z.infer<typeof UsageResponse>;
|
|
|
93
82
|
|
|
94
83
|
export const HealthResponse = z.object({
|
|
95
84
|
status: z.string(),
|
|
96
|
-
workers: z.number(),
|
|
97
85
|
activeTokens: z.number(),
|
|
98
86
|
});
|
|
99
87
|
export type HealthResponse = z.infer<typeof HealthResponse>;
|
|
@@ -219,19 +219,16 @@ export default defineConfig({
|
|
|
219
219
|
console.warn(`⚠ Migration skipped: ${(e as Error).message}`);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
const { initPool } = await import("./application/qgrid/pool");
|
|
222
|
+
const { QgridDispatcher } = await import("./application/qgrid/qgrid.dispatcher");
|
|
224
223
|
try {
|
|
225
224
|
const { TokenModel } = await import("./application/token/token.model");
|
|
226
225
|
const entries = await TokenModel.findActive("A");
|
|
227
|
-
const
|
|
228
|
-
const pool = initPool(tokens);
|
|
226
|
+
for (const e of entries) QgridDispatcher.addToken(e.token, e.name);
|
|
229
227
|
console.log(
|
|
230
|
-
`🌲 Server listening on http://${host}:${port} (${
|
|
228
|
+
`🌲 Server listening on http://${host}:${port} (${QgridDispatcher.tokens.size} tokens loaded)`,
|
|
231
229
|
);
|
|
232
230
|
} catch (e) {
|
|
233
|
-
console.warn(`⚠
|
|
234
|
-
initPool([]);
|
|
231
|
+
console.warn(`⚠ Dispatcher init failed: ${(e as Error).message}`);
|
|
235
232
|
console.log(`🌲 Server listening on http://${host}:${port} (no tokens)`);
|
|
236
233
|
}
|
|
237
234
|
},
|