@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.
Files changed (39) hide show
  1. package/bundle/dist/application/qgrid/oauth.js +1 -1
  2. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +166 -0
  3. package/bundle/dist/application/qgrid/qgrid.frame.js +28 -36
  4. package/bundle/dist/application/qgrid/qgrid.types.js +5 -16
  5. package/bundle/dist/i18n/sd.generated.js +2 -2
  6. package/bundle/dist/sonamu.config.js +6 -9
  7. package/bundle/src/application/qgrid/oauth.ts +1 -1
  8. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +184 -0
  9. package/bundle/src/application/qgrid/qgrid.frame.ts +28 -50
  10. package/bundle/src/application/qgrid/qgrid.types.ts +2 -14
  11. package/bundle/src/application/sonamu.generated.http +2 -1
  12. package/bundle/src/i18n/sd.generated.ts +1 -1
  13. package/bundle/src/sonamu.config.ts +4 -7
  14. package/bundle/web-dist/client/assets/index-D8eEjsZj.js +78 -0
  15. package/bundle/web-dist/client/assets/{logs-KoJtGoEG.js → logs-CKnJzuH6.js} +1 -1
  16. package/bundle/web-dist/client/assets/{routes-CqzIoGYg.js → routes-DQKecVGa.js} +1 -1
  17. package/bundle/web-dist/client/assets/{sd.generated-Duipc8HN.js → sd.generated-NHYHS_-0.js} +1 -1
  18. package/bundle/web-dist/client/assets/services.generated-DDVXMid7.js +1 -0
  19. package/bundle/web-dist/client/assets/{show-BuMxiSr1.js → show-D2z1qxJ5.js} +1 -1
  20. package/bundle/web-dist/client/assets/tokens-BaywaQkm.js +1 -0
  21. package/bundle/web-dist/client/index.html +2 -2
  22. package/bundle/web-dist/server/assets/{copy-BPyN_0Dp.js → copy-BVWDOV2T.js} +1 -1
  23. package/bundle/web-dist/server/assets/{lazyRouteComponent-Bmjwy0OA.js → lazyRouteComponent-BJmwxFyJ.js} +2 -2
  24. package/bundle/web-dist/server/assets/{logs-BgcF7jVg.js → logs-DgIbufNf.js} +3 -3
  25. package/bundle/web-dist/server/assets/{routes-DVA-nsZ-.js → routes-x7-hqUm0.js} +2 -2
  26. package/bundle/web-dist/server/assets/{sd.generated-CDVQy4CJ.js → sd.generated-BZPIp4uL.js} +1 -1
  27. package/bundle/web-dist/server/assets/{services.generated-DWyvb1vb.js → services.generated-BA0gbdmd.js} +5 -4
  28. package/bundle/web-dist/server/assets/{shim-CqVZY710.js → shim-DGrmZkac.js} +1 -1
  29. package/bundle/web-dist/server/assets/{show-BnKnfNFP.js → show-Bk3NayMH.js} +4 -4
  30. package/bundle/web-dist/server/assets/{tokens-7z55hPRV.js → tokens-CVSwt0x0.js} +6 -17
  31. package/bundle/web-dist/server/entry-server.generated.js +7 -7
  32. package/package.json +1 -1
  33. package/bundle/dist/application/qgrid/pool.js +0 -128
  34. package/bundle/dist/application/qgrid/worker.js +0 -189
  35. package/bundle/src/application/qgrid/pool.ts +0 -165
  36. package/bundle/src/application/qgrid/worker.ts +0 -240
  37. package/bundle/web-dist/client/assets/index-BnqlZB1V.js +0 -78
  38. package/bundle/web-dist/client/assets/services.generated-Ccs6tru4.js +0 -1
  39. 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 { type ClaudePool, getPool } from "./pool";
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(prompt: string, system?: string, timeout?: number): Promise<CliResult> {
33
- const pool = getPool();
34
- const result = await pool.query({ system, prompt }, timeout);
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: pool.getLastUsedWorkerName(),
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 getPool().getStats();
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
- getPool().createWorkers(token, name);
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
- pool.destroyWorkers(token);
101
- pool.createWorkers(newToken, name ?? entry.name);
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
- const pool = getPool();
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
- pool.destroyWorkers(token);
113
+ QgridDispatcher.removeToken(token);
114
114
  return { removed: true };
115
115
  }
116
116
 
117
117
  /**
118
- *
119
118
  * @param id token_id
120
- * 토큰 활성화/비활성화 토글 (pool 워커 생성/제거 & DB의 active 필드 업데이트)
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
- for (const old of oldEntries) {
182
- pool.destroyWorkers(old.token);
183
- await TokenModel.del([old.id]);
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
- pool.createWorkers(tokens.accessToken, pending.name);
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(pool, entry);
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(pool, entry);
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(pool: ClaudePool, token: RefreshTokenParams): Promise<string> {
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
- pool.destroyWorkers(token.token);
259
- pool.createWorkers(refreshed.accessToken, token.name);
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
- const pool = getPool();
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>;
@@ -128,7 +128,8 @@ Content-Type: application/json
128
128
  {
129
129
  "prompt": "PROMPT",
130
130
  "system": "SYSTEM",
131
- "timeout": 0
131
+ "timeout": 0,
132
+ "model": "MODEL"
132
133
  }
133
134
 
134
135
  ###
@@ -11,7 +11,7 @@ function getCurrentLocale(): (typeof SUPPORTED_LOCALES)[number] {
11
11
  try {
12
12
  const ctx = Sonamu.getContext();
13
13
  return (ctx.locale as (typeof SUPPORTED_LOCALES)[number]) ?? DEFAULT_LOCALE;
14
- } catch {
14
+ } catch (_) {
15
15
  return DEFAULT_LOCALE;
16
16
  }
17
17
  }
@@ -219,19 +219,16 @@ export default defineConfig({
219
219
  console.warn(`⚠ Migration skipped: ${(e as Error).message}`);
220
220
  }
221
221
 
222
- // Pool 초기화
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 tokens = entries.map((e) => ({ token: e.token, name: e.name }));
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} (${pool.workers.size} tokens loaded)`,
228
+ `🌲 Server listening on http://${host}:${port} (${QgridDispatcher.tokens.size} tokens loaded)`,
231
229
  );
232
230
  } catch (e) {
233
- console.warn(`⚠ Pool init failed: ${(e as Error).message}`);
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
  },