crewx 0.9.0-rc.2 → 0.9.0-rc.21
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/dist/assets/MarketPage-DDPL2d7x.js +36 -0
- package/dist/assets/{PromptTab-hdXDbbT6.js → PromptTab-UC3K4qx2.js} +3 -3
- package/dist/assets/{arc--CBVVcyH.js → arc-DdP3-fUR.js} +1 -1
- package/dist/assets/{architectureDiagram-UYN6MBPD-DUiYwqJn.js → architectureDiagram-UYN6MBPD-D16VON1i.js} +1 -1
- package/dist/assets/{blockDiagram-ZHA2E4KO-DV86vAgG.js → blockDiagram-ZHA2E4KO-qs1dIRkl.js} +4 -4
- package/dist/assets/{c4Diagram-6F5ED5ID-CvWE7Cx_.js → c4Diagram-6F5ED5ID-BeDy8akQ.js} +1 -1
- package/dist/assets/channel-DtrxBSkc.js +1 -0
- package/dist/assets/{chunk-5HRBRIJM-DAxM5Ul_.js → chunk-5HRBRIJM-BC1zIb11.js} +1 -1
- package/dist/assets/{chunk-7U56Z5CX-BuYaEj8L.js → chunk-7U56Z5CX-BuGil3QD.js} +1 -1
- package/dist/assets/{chunk-ASOPGD6M-2Kx0e45C.js → chunk-ASOPGD6M-DmKdDtBQ.js} +1 -1
- package/dist/assets/{chunk-KFBOBJHC-CR4QS6Rt.js → chunk-KFBOBJHC-D3BvHLPa.js} +1 -1
- package/dist/assets/{chunk-T2TOU4HS-CffcwNHx.js → chunk-T2TOU4HS-B5MoUp-N.js} +1 -1
- package/dist/assets/{chunk-TMUBEWPD-BGpYtekN.js → chunk-TMUBEWPD-AvWe_lsV.js} +1 -1
- package/dist/assets/classDiagram-LNE6IOMH-BMT5B_JB.js +1 -0
- package/dist/assets/classDiagram-v2-MQ7JQ4JX-BMT5B_JB.js +1 -0
- package/dist/assets/{dagre-4EVJKHTY-yBWukkrE.js → dagre-4EVJKHTY-B6DewFyM.js} +1 -1
- package/dist/assets/{diagram-QW4FP2JN-CIr1PLq3.js → diagram-QW4FP2JN-BTsQ8JSd.js} +1 -1
- package/dist/assets/{erDiagram-6RL3IURR-xG7SygyU.js → erDiagram-6RL3IURR-BM5nvgVR.js} +1 -1
- package/dist/assets/{flowDiagram-7ASYPVHJ-CKalfOqp.js → flowDiagram-7ASYPVHJ-D1UCCuOG.js} +1 -1
- package/dist/assets/{ganttDiagram-NTVNEXSI-Bnx7zrX4.js → ganttDiagram-NTVNEXSI-CNIGs7Cn.js} +1 -1
- package/dist/assets/{gitGraph-YCYPL57B-TQlF0fiA.js → gitGraph-YCYPL57B-DSLjx1yQ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-NRZ2UAAF-DxlaFkwO.js → gitGraphDiagram-NRZ2UAAF-B11zX1Fa.js} +1 -1
- package/dist/assets/{infoDiagram-A4XQUW5V-B1-arkqa.js → infoDiagram-A4XQUW5V-DkjvODYl.js} +1 -1
- package/dist/assets/{journeyDiagram-G5WM74LC-Cj439GUX.js → journeyDiagram-G5WM74LC-BkCQPMn6.js} +1 -1
- package/dist/assets/{kanban-definition-QRCXZQQD-BvrIwEIc.js → kanban-definition-QRCXZQQD-_OX7pNfJ.js} +1 -1
- package/dist/assets/{linear-BKYNK0ri.js → linear-BYjwVAR6.js} +1 -1
- package/dist/assets/main-CZdKLdUl.css +10 -0
- package/dist/assets/main-CkkZNCy-.js +1561 -0
- package/dist/assets/{mindmap-definition-GWI6TPTV-BExfJY0G.js → mindmap-definition-GWI6TPTV-BzYpSnv8.js} +1 -1
- package/dist/assets/ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm +0 -0
- package/dist/assets/{pieDiagram-YF2LJOPJ-RPWpvrC8.js → pieDiagram-YF2LJOPJ-CEFg6ao0.js} +1 -1
- package/dist/assets/{quadrantDiagram-OS5C2QUG-DmbBzk5Q.js → quadrantDiagram-OS5C2QUG-_5w2x3VV.js} +1 -1
- package/dist/assets/{requirementDiagram-MIRIMTAZ-DFQuFXq3.js → requirementDiagram-MIRIMTAZ-B_VQYReZ.js} +1 -1
- package/dist/assets/{sankeyDiagram-Y46BX6SQ-p_bw56lJ.js → sankeyDiagram-Y46BX6SQ-CgmOlLhh.js} +1 -1
- package/dist/assets/{sequenceDiagram-G6AWOVSC-DTbBkLeD.js → sequenceDiagram-G6AWOVSC-MGVt0Ai2.js} +1 -1
- package/dist/assets/{stateDiagram-MAYHULR4-DBvC6UcF.js → stateDiagram-MAYHULR4-DaZjgTu-.js} +1 -1
- package/dist/assets/stateDiagram-v2-4JROLMXI-DN1wGYkV.js +1 -0
- package/dist/assets/{timeline-definition-U7ZMHBDA-B6utvIg1.js → timeline-definition-U7ZMHBDA-CusetmK6.js} +1 -1
- package/dist/assets/transformers.web-BN8Ug43_.js +45 -0
- package/dist/assets/{xychartDiagram-6QU3TZC5-BHG0s1Ye.js → xychartDiagram-6QU3TZC5-CTndf-if.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/app.module.js +4 -0
- package/dist-server/bootstrap/codex-writable-roots.js +88 -0
- package/dist-server/bootstrap/crewx-server.js +10 -2
- package/dist-server/common/decorator/skip-request-log.decorator.js +8 -0
- package/dist-server/common/interceptor/http-logging.interceptor.js +17 -4
- package/dist-server/common/task-log/stdout-json-fallback.js +200 -0
- package/dist-server/domain/agent/agent.controller.js +14 -0
- package/dist-server/domain/agent/agent.service.js +246 -18
- package/dist-server/domain/agent/dto/create-agent.dto.js +12 -0
- package/dist-server/domain/agent/dto/remote-provider.dto.js +82 -0
- package/dist-server/domain/agent/dto/reorder-agents.dto.js +51 -0
- package/dist-server/domain/agent/dto/update-agent.dto.js +3 -1
- package/dist-server/domain/agent/provider.controller.js +12 -1
- package/dist-server/domain/doc/doc.controller.js +43 -9
- package/dist-server/domain/doc/doc.service.js +80 -6
- package/dist-server/domain/doc/image-constants.js +11 -1
- package/dist-server/domain/events/workspace-events.controller.js +2 -0
- package/dist-server/domain/message/dto/send-message.dto.js +49 -1
- package/dist-server/domain/message/message.controller.js +1 -1
- package/dist-server/domain/message/message.module.js +2 -1
- package/dist-server/domain/message/message.service.js +83 -13
- package/dist-server/domain/settings/settings.service.js +188 -1
- package/dist-server/domain/system/system.controller.js +41 -0
- package/dist-server/domain/system/system.module.js +19 -0
- package/dist-server/domain/task/task.constants.js +8 -1
- package/dist-server/domain/task/task.service.js +59 -67
- package/dist-server/domain/thread/dto/list-threads.dto.js +18 -0
- package/dist-server/domain/thread/overdrive.types.js +38 -0
- package/dist-server/domain/thread/thread.service.js +83 -70
- package/dist-server/domain/usage/usage.controller.js +18 -0
- package/dist-server/domain/usage/usage.module.js +1 -0
- package/dist-server/domain/usage/usage.service.js +246 -4
- package/dist-server/domain/usage-report/dto/usage-report.dto.js +2 -0
- package/dist-server/domain/usage-report/usage-report.controller.js +72 -0
- package/dist-server/domain/usage-report/usage-report.module.js +23 -0
- package/dist-server/domain/usage-report/usage-report.service.js +135 -0
- package/dist-server/domain/wi/wi.service.js +45 -3
- package/dist-server/main.js +1 -0
- package/dist-server/repository/repository.module.js +2 -0
- package/dist-server/repository/task.repository.js +56 -0
- package/dist-server/repository/thread.repository.js +13 -0
- package/dist-server/repository/usage-report.repository.js +27 -0
- package/package.json +9 -9
- package/packages/cli/dist/bootstrap/codex-writable-roots.d.ts +45 -0
- package/packages/cli/dist/bootstrap/codex-writable-roots.js +88 -0
- package/packages/cli/dist/bootstrap/crewx-cli.js +2 -0
- package/packages/cli/dist/commands/execute.js +10 -2
- package/packages/cli/dist/commands/parse-common-flags.d.ts +5 -1
- package/packages/cli/dist/commands/parse-common-flags.js +9 -2
- package/packages/cli/dist/commands/ps.js +51 -2
- package/packages/cli/dist/commands/query.js +10 -2
- package/packages/cli/dist/commands/restart.js +20 -6
- package/packages/cli/dist/commands/write-output.d.ts +3 -0
- package/packages/cli/dist/commands/write-output.js +24 -0
- package/packages/cli/dist/main.js +7 -0
- package/packages/cli/package.json +1 -1
- package/dist/assets/MarketPage-T1o_oIrw.js +0 -41
- package/dist/assets/channel-7q16D7P9.js +0 -1
- package/dist/assets/classDiagram-LNE6IOMH-C6qjk4uc.js +0 -1
- package/dist/assets/classDiagram-v2-MQ7JQ4JX-C6qjk4uc.js +0 -1
- package/dist/assets/main-C5mwv79r.css +0 -10
- package/dist/assets/main-DmvFaz-d.js +0 -1486
- package/dist/assets/stateDiagram-v2-4JROLMXI-CCMApx_v.js +0 -1
|
@@ -17,6 +17,8 @@ const sdk_1 = require("@crewx/sdk");
|
|
|
17
17
|
const thread_repository_js_1 = require("../../repository/thread.repository.js");
|
|
18
18
|
const crewx_pool_service_js_1 = require("../../modules/crewx-pool.service.js");
|
|
19
19
|
const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
|
|
20
|
+
const stdout_json_fallback_js_1 = require("../../common/task-log/stdout-json-fallback.js");
|
|
21
|
+
const overdrive_types_js_1 = require("./overdrive.types.js");
|
|
20
22
|
let ThreadService = ThreadService_1 = class ThreadService {
|
|
21
23
|
threadRepo;
|
|
22
24
|
crewxPool;
|
|
@@ -45,7 +47,7 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
45
47
|
// Sort
|
|
46
48
|
const sort = params.sort || 'updatedAt';
|
|
47
49
|
const order = params.order || 'desc';
|
|
48
|
-
|
|
50
|
+
const sortCompare = (a, b) => {
|
|
49
51
|
let cmp = 0;
|
|
50
52
|
if (sort === 'messageCount') {
|
|
51
53
|
cmp = (a.message_count ?? 0) - (b.message_count ?? 0);
|
|
@@ -57,12 +59,27 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
57
59
|
cmp = new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime();
|
|
58
60
|
}
|
|
59
61
|
return order === 'asc' ? cmp : -cmp;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
+
};
|
|
63
|
+
filtered.sort(sortCompare);
|
|
62
64
|
const total = filtered.length;
|
|
63
65
|
const limit = this.normalizeLimit(params.limit, 50, 200);
|
|
64
66
|
const offset = this.normalizeOffset(params.offset);
|
|
65
|
-
|
|
67
|
+
let paged;
|
|
68
|
+
if (params.pinnedFirst === false) {
|
|
69
|
+
// Pure sort order (e.g. sidebar recent list) — pinned threads are not
|
|
70
|
+
// separated out, so the page reflects a plain updatedAt/createdAt slice.
|
|
71
|
+
paged = filtered.slice(offset, offset + limit);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Pinned threads must always be visible regardless of the pagination cutoff —
|
|
75
|
+
// otherwise threads pinned long ago silently fall out of the top-N window as
|
|
76
|
+
// newer threads accumulate. Pinned rows are shown in full, sorted by the user's
|
|
77
|
+
// manual pinOrder; only the unpinned remainder is paginated by limit/offset.
|
|
78
|
+
const pinnedRows = filtered.filter((r) => !!(r.pinned ?? 0));
|
|
79
|
+
const unpinnedRows = filtered.filter((r) => !(r.pinned ?? 0));
|
|
80
|
+
pinnedRows.sort((a, b) => this.getPinOrder(a) - this.getPinOrder(b));
|
|
81
|
+
paged = [...pinnedRows, ...unpinnedRows.slice(offset, offset + limit)];
|
|
82
|
+
}
|
|
66
83
|
// Batch-fetch tasks for paged threads
|
|
67
84
|
const threadIds = paged.map((r) => r.id);
|
|
68
85
|
const tasksByThread = this.threadRepo.batchFetchTasks(threadIds, workspaceId);
|
|
@@ -101,6 +118,7 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
101
118
|
updatedAt: threadRow.updated_at,
|
|
102
119
|
pinned: !!(threadRow.pinned ?? 0),
|
|
103
120
|
starred: !!(threadRow.starred ?? 0),
|
|
121
|
+
overdrive: (0, overdrive_types_js_1.parseOverdriveSummary)(threadRow.metadata),
|
|
104
122
|
totals: {
|
|
105
123
|
taskCount: stats.taskCount,
|
|
106
124
|
inputTokens: stats.inputTokens,
|
|
@@ -195,11 +213,11 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
195
213
|
callerAgentId: row.caller_agent_id || undefined,
|
|
196
214
|
};
|
|
197
215
|
if (wantLogs) {
|
|
198
|
-
result.logs = this.parseLogs(row.logs, limit, this.extractAdapterKey(row));
|
|
216
|
+
result.logs = this.parseLogs(row.logs, limit, this.extractAdapterKey(row)).logs;
|
|
199
217
|
}
|
|
200
218
|
if (wantChildren && children) {
|
|
201
219
|
result.childTasks = children.map((child) => {
|
|
202
|
-
const logs = this.parseLogs(child.logs, 1, this.extractAdapterKey(child));
|
|
220
|
+
const { logs } = this.parseLogs(child.logs, 1, this.extractAdapterKey(child));
|
|
203
221
|
return {
|
|
204
222
|
taskId: child.id,
|
|
205
223
|
agentId: child.agent_id || '',
|
|
@@ -241,12 +259,28 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
241
259
|
const newTaskId = (0, sdk_1.generateId)('tsk');
|
|
242
260
|
const agentRef = `@${task.agent_id}`;
|
|
243
261
|
const mode = task.mode ?? 'execute';
|
|
262
|
+
const originalMetadata = this.parseTaskMetadata(task.metadata);
|
|
263
|
+
const originalProvider = this.readString(originalMetadata.provider) ?? undefined;
|
|
264
|
+
const originalWasOverdrive = originalMetadata.overdrive === true;
|
|
265
|
+
const originalOverdriveMode = originalMetadata.overdriveState === 'latch' ? 'latch' : 'count';
|
|
244
266
|
const crewx = await this.crewxPool.getForCurrentRequest();
|
|
245
267
|
const sdkOptions = {
|
|
246
268
|
threadId,
|
|
247
269
|
taskId: newTaskId,
|
|
248
270
|
platform: 'cli',
|
|
249
|
-
|
|
271
|
+
model: task.model ?? undefined,
|
|
272
|
+
provider: originalProvider,
|
|
273
|
+
...(originalWasOverdrive ? { overdrive: { mode: originalOverdriveMode } } : {}),
|
|
274
|
+
metadata: {
|
|
275
|
+
thread: threadId,
|
|
276
|
+
parentTaskId: null,
|
|
277
|
+
retriedFromTaskId: taskId,
|
|
278
|
+
...(originalWasOverdrive ? {
|
|
279
|
+
overdrive: true,
|
|
280
|
+
overdriveState: originalOverdriveMode,
|
|
281
|
+
overdriveMode: mode,
|
|
282
|
+
} : {}),
|
|
283
|
+
},
|
|
250
284
|
cwd: (0, workspace_context_store_js_1.getWorkspacePath)(),
|
|
251
285
|
};
|
|
252
286
|
const sdkCall = mode === 'execute'
|
|
@@ -309,6 +343,7 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
309
343
|
?? sessionId
|
|
310
344
|
?? this.extractProviderSessionIdFromLogs(task.logs);
|
|
311
345
|
const providerSessionReady = this.hasProviderResumableContext(task.logs, provider);
|
|
346
|
+
const { logs: taskLogs, progressSteps } = this.parseLogs(task.logs, 50, this.extractAdapterKey(task));
|
|
312
347
|
messages.push({
|
|
313
348
|
id: `${task.id}-assistant`,
|
|
314
349
|
role: 'assistant',
|
|
@@ -330,7 +365,8 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
330
365
|
totalCostUsd,
|
|
331
366
|
sessionId,
|
|
332
367
|
error: task.error || null,
|
|
333
|
-
logs:
|
|
368
|
+
logs: taskLogs,
|
|
369
|
+
progressSteps,
|
|
334
370
|
callerAgentId: task.caller_agent_id ?? null,
|
|
335
371
|
metadata: {
|
|
336
372
|
...taskMetadata,
|
|
@@ -369,7 +405,6 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
369
405
|
return cleanLines.join('\n').trim();
|
|
370
406
|
}
|
|
371
407
|
toSummary = (row) => {
|
|
372
|
-
const meta = row.metadata ? JSON.parse(row.metadata) : {};
|
|
373
408
|
return {
|
|
374
409
|
threadId: row.id,
|
|
375
410
|
platform: row.platform || 'cli',
|
|
@@ -379,65 +414,24 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
379
414
|
updatedAt: row.updated_at,
|
|
380
415
|
pinned: !!(row.pinned ?? 0),
|
|
381
416
|
starred: !!(row.starred ?? 0),
|
|
382
|
-
pinOrder:
|
|
417
|
+
pinOrder: this.getPinOrder(row),
|
|
418
|
+
overdrive: (0, overdrive_types_js_1.parseOverdriveSummary)(row.metadata),
|
|
383
419
|
};
|
|
384
420
|
};
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Returns true when the raw stdout line is a known metadata-only JSON event that
|
|
392
|
-
* should be silently dropped rather than shown as raw text.
|
|
393
|
-
*
|
|
394
|
-
* Covers Claude, Codex, and OpenCode stream-json protocols.
|
|
395
|
-
* Only called after parseStdoutEvent() returns [] (no displayable entries).
|
|
396
|
-
* Partial / truncated JSON → JSON.parse throws → returns false (keep raw text fallback).
|
|
397
|
-
*
|
|
398
|
-
* @see TaskService.shouldSkipRawStdoutJsonFallback — keep in sync
|
|
399
|
-
*/
|
|
400
|
-
shouldSkipRawStdoutJsonFallback(message) {
|
|
401
|
-
const trimmed = message.trimStart();
|
|
402
|
-
if (!trimmed.startsWith('{'))
|
|
403
|
-
return false;
|
|
404
|
-
let obj;
|
|
421
|
+
getPinOrder(row) {
|
|
422
|
+
if (!row.metadata)
|
|
423
|
+
return 0;
|
|
405
424
|
try {
|
|
406
|
-
|
|
425
|
+
return JSON.parse(row.metadata).pinOrder ?? 0;
|
|
407
426
|
}
|
|
408
427
|
catch {
|
|
409
|
-
return
|
|
410
|
-
}
|
|
411
|
-
const type = obj.type;
|
|
412
|
-
if (typeof type !== 'string')
|
|
413
|
-
return false;
|
|
414
|
-
// Claude stream-json metadata events
|
|
415
|
-
if (type === 'assistant' ||
|
|
416
|
-
type === 'user' ||
|
|
417
|
-
type === 'system' ||
|
|
418
|
-
type === 'result' ||
|
|
419
|
-
type === 'rate_limit_event') {
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
422
|
-
// Codex metadata-only events
|
|
423
|
-
if (type === 'thread.started' ||
|
|
424
|
-
type === 'turn.started' ||
|
|
425
|
-
type === 'turn.completed' ||
|
|
426
|
-
type === 'turn.failed' ||
|
|
427
|
-
type === 'item.updated') {
|
|
428
|
-
return true;
|
|
429
|
-
}
|
|
430
|
-
// Codex item.started/item.completed reach this function only after
|
|
431
|
-
// parseStdoutEvent() returned no displayable entries. Skip raw fallback while
|
|
432
|
-
// preserving the original line in tasks.logs for future reprocessing.
|
|
433
|
-
if (type === 'item.started' || type === 'item.completed') {
|
|
434
|
-
return true;
|
|
428
|
+
return 0;
|
|
435
429
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
430
|
+
}
|
|
431
|
+
validateId(id) {
|
|
432
|
+
if (!/^[\w\p{L}\p{N}._-]+$/u.test(id)) {
|
|
433
|
+
throw new common_1.BadRequestException('Invalid ID format');
|
|
439
434
|
}
|
|
440
|
-
return false;
|
|
441
435
|
}
|
|
442
436
|
normalizeLimit(limit, fallback, max) {
|
|
443
437
|
const num = Number(limit);
|
|
@@ -459,16 +453,16 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
459
453
|
/** @see TaskService.parseLogs — 동일 역할, ACP 파싱 로직 동기화 필요 */
|
|
460
454
|
parseLogs(logsJson, logLimit, providerId) {
|
|
461
455
|
if (!logsJson)
|
|
462
|
-
return [];
|
|
456
|
+
return { logs: [], progressSteps: [] };
|
|
463
457
|
let rawLogs;
|
|
464
458
|
try {
|
|
465
459
|
rawLogs = JSON.parse(logsJson);
|
|
466
460
|
}
|
|
467
461
|
catch {
|
|
468
|
-
return [];
|
|
462
|
+
return { logs: [], progressSteps: [] };
|
|
469
463
|
}
|
|
470
464
|
if (!Array.isArray(rawLogs))
|
|
471
|
-
return [];
|
|
465
|
+
return { logs: [], progressSteps: [] };
|
|
472
466
|
const entries = [];
|
|
473
467
|
for (const log of rawLogs) {
|
|
474
468
|
if (log.level === 'stdout') {
|
|
@@ -480,7 +474,7 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
480
474
|
}
|
|
481
475
|
// Parsed successfully but no displayable entries.
|
|
482
476
|
// Skip fallback if the raw message is a known metadata-only stream-json event type.
|
|
483
|
-
if (
|
|
477
|
+
if ((0, stdout_json_fallback_js_1.shouldSkipRawStdoutJsonFallback)(log.message, providerId))
|
|
484
478
|
continue;
|
|
485
479
|
}
|
|
486
480
|
catch { /* adapter parse failure — fallback to text */ }
|
|
@@ -490,13 +484,12 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
490
484
|
entries.push(...(0, sdk_1.defaultParseEvent)(log.message, log.timestamp));
|
|
491
485
|
}
|
|
492
486
|
}
|
|
493
|
-
// Pipeline: merge consecutive text → deduplicate tool_use → logLimit slice
|
|
487
|
+
// Pipeline: merge consecutive text → deduplicate tool_use → [branch: text-only slice(-200) = progressSteps] → logLimit slice = logs
|
|
494
488
|
const merged = this.mergeConsecutiveText(entries);
|
|
495
489
|
const deduped = this.deduplicateToolUse(merged);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
return deduped;
|
|
490
|
+
const progressSteps = deduped.filter((e) => e.type === 'text').slice(-200);
|
|
491
|
+
const logs = deduped.length > logLimit ? deduped.slice(deduped.length - logLimit) : deduped;
|
|
492
|
+
return { logs, progressSteps };
|
|
500
493
|
}
|
|
501
494
|
/** Merge consecutive type=text entries into one while preserving message boundaries. */
|
|
502
495
|
mergeConsecutiveText(entries) {
|
|
@@ -596,6 +589,11 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
596
589
|
if (claudeSessionId) {
|
|
597
590
|
return claudeSessionId;
|
|
598
591
|
}
|
|
592
|
+
// OpenCode: every event carries a top-level `sessionID` (e.g. step_start/tool_use/text).
|
|
593
|
+
const opencodeSessionId = this.readString(message.sessionID);
|
|
594
|
+
if (opencodeSessionId) {
|
|
595
|
+
return opencodeSessionId;
|
|
596
|
+
}
|
|
599
597
|
}
|
|
600
598
|
catch {
|
|
601
599
|
// Ignore non-JSON log lines.
|
|
@@ -631,6 +629,9 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
631
629
|
if (providerKey === 'codex' && this.isCodexResumableEvent(message)) {
|
|
632
630
|
return true;
|
|
633
631
|
}
|
|
632
|
+
if (providerKey === 'opencode' && this.isOpencodeResumableEvent(message)) {
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
634
635
|
}
|
|
635
636
|
catch {
|
|
636
637
|
// Ignore non-JSON log lines.
|
|
@@ -638,6 +639,18 @@ let ThreadService = ThreadService_1 = class ThreadService {
|
|
|
638
639
|
}
|
|
639
640
|
return false;
|
|
640
641
|
}
|
|
642
|
+
isOpencodeResumableEvent(message) {
|
|
643
|
+
const type = typeof message.type === 'string' ? message.type : '';
|
|
644
|
+
if (type === 'tool_use') {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
if (type === 'text') {
|
|
648
|
+
const part = message.part;
|
|
649
|
+
const text = part?.text;
|
|
650
|
+
return typeof text === 'string' && text.trim().length > 0;
|
|
651
|
+
}
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
641
654
|
isClaudeResumableEvent(message) {
|
|
642
655
|
if (message.type !== 'assistant') {
|
|
643
656
|
return false;
|
|
@@ -46,6 +46,14 @@ let UsageController = class UsageController {
|
|
|
46
46
|
const data = this.usageService.getProviderUsage(from, to, workspace);
|
|
47
47
|
return { success: true, data };
|
|
48
48
|
}
|
|
49
|
+
// USG.MODEL: 모델별 사용량 집계
|
|
50
|
+
getModelUsage(from, to, workspace) {
|
|
51
|
+
if (!from || !to) {
|
|
52
|
+
throw new common_1.BadRequestException('Both "from" and "to" query parameters are required (ISO 8601)');
|
|
53
|
+
}
|
|
54
|
+
const data = this.usageService.getModelUsage(from, to, workspace);
|
|
55
|
+
return { success: true, data };
|
|
56
|
+
}
|
|
49
57
|
// USG.LIMITS: 구독 한도 조회
|
|
50
58
|
async getSubscriptionLimits() {
|
|
51
59
|
const data = await this.usageService.getSubscriptionLimits();
|
|
@@ -125,6 +133,16 @@ __decorate([
|
|
|
125
133
|
__metadata("design:paramtypes", [String, String, String]),
|
|
126
134
|
__metadata("design:returntype", void 0)
|
|
127
135
|
], UsageController.prototype, "getProviderUsage", null);
|
|
136
|
+
__decorate([
|
|
137
|
+
(0, swagger_1.ApiOperation)({ operationId: 'USG.MODEL', summary: 'Get per-model usage aggregation' }),
|
|
138
|
+
(0, common_1.Get)('models'),
|
|
139
|
+
__param(0, (0, common_1.Query)('from')),
|
|
140
|
+
__param(1, (0, common_1.Query)('to')),
|
|
141
|
+
__param(2, (0, common_1.Query)('workspace')),
|
|
142
|
+
__metadata("design:type", Function),
|
|
143
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
144
|
+
__metadata("design:returntype", void 0)
|
|
145
|
+
], UsageController.prototype, "getModelUsage", null);
|
|
128
146
|
__decorate([
|
|
129
147
|
(0, swagger_1.ApiOperation)({ operationId: 'USG.LIMITS', summary: 'Get subscription limit windows (Codex + Claude)' }),
|
|
130
148
|
(0, common_1.Get)('limits'),
|
|
@@ -62,7 +62,14 @@ const CACHE_MAX_SIZE = 10;
|
|
|
62
62
|
const CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000; // 5분
|
|
63
63
|
const CLAUDE_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
|
|
64
64
|
const GLM_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
|
|
65
|
+
const CODEX_CACHE_TTL = 5 * 60 * 1000; // 5분
|
|
65
66
|
const OAUTH_FAIL_CACHE_TTL = 30 * 1000; // 30s — short TTL for null (failed) responses
|
|
67
|
+
// --- USR: Usage-report-only aggregation constants ---
|
|
68
|
+
// Rework 판정 임계 — thread 연속호출 간격(분). 실DB 검증: docs/research/ai-skill-report.md §9/§10 (core_dev, WI-20260704-020)
|
|
69
|
+
const REWORK_THRESHOLD_MINUTES = 5;
|
|
70
|
+
// failed 3-way 분류 패턴 (WI-20260704-020 §Side-effect risk areas)
|
|
71
|
+
const USER_STOP_ERROR_RE = /Stopped by user|사용자에 의해 중지됨|SIGTERM 143/i;
|
|
72
|
+
const AGENT_BUG_ERROR_RE = /ACP|orphaned|AGENT_NOT_FOUND|프로세스\s*오류/i;
|
|
66
73
|
let UsageService = UsageService_1 = class UsageService {
|
|
67
74
|
taskRepository;
|
|
68
75
|
projectRepository;
|
|
@@ -72,6 +79,7 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
72
79
|
cleanupInterval;
|
|
73
80
|
claudeOAuthCache = null;
|
|
74
81
|
glmCache = null;
|
|
82
|
+
codexCache = null;
|
|
75
83
|
constructor(taskRepository, projectRepository, snapshotRepo) {
|
|
76
84
|
this.taskRepository = taskRepository;
|
|
77
85
|
this.projectRepository = projectRepository;
|
|
@@ -231,6 +239,179 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
231
239
|
};
|
|
232
240
|
return { from, to, providers, totals };
|
|
233
241
|
}
|
|
242
|
+
// --- USG.MODEL: Model usage aggregation ---
|
|
243
|
+
getModelUsage(from, to, workspace) {
|
|
244
|
+
const rows = this.taskRepository.getModelUsage(from, to, workspace);
|
|
245
|
+
const grandTotalTokens = rows.reduce((sum, r) => sum + r.totalTokens, 0);
|
|
246
|
+
const models = rows
|
|
247
|
+
.map((r) => ({
|
|
248
|
+
model: r.model,
|
|
249
|
+
provider: r.provider,
|
|
250
|
+
totalTokens: r.totalTokens,
|
|
251
|
+
inputTokens: r.inputTokens,
|
|
252
|
+
outputTokens: r.outputTokens,
|
|
253
|
+
cachedInputTokens: r.cachedInputTokens,
|
|
254
|
+
totalCost: r.costUsd,
|
|
255
|
+
taskCount: r.totalTasks,
|
|
256
|
+
activeDurationMs: r.activeDurationMs,
|
|
257
|
+
sharePercent: grandTotalTokens > 0
|
|
258
|
+
? Math.round((r.totalTokens / grandTotalTokens) * 10000) / 100
|
|
259
|
+
: 0,
|
|
260
|
+
lastActiveAt: r.lastActiveAt,
|
|
261
|
+
}))
|
|
262
|
+
.sort((a, b) => b.totalCost - a.totalCost);
|
|
263
|
+
const totals = {
|
|
264
|
+
totalTokens: grandTotalTokens,
|
|
265
|
+
totalCost: rows.reduce((sum, r) => sum + r.costUsd, 0),
|
|
266
|
+
totalTasks: rows.reduce((sum, r) => sum + r.totalTasks, 0),
|
|
267
|
+
totalActiveDurationMs: rows.reduce((sum, r) => sum + r.activeDurationMs, 0),
|
|
268
|
+
};
|
|
269
|
+
return { from, to, models, totals };
|
|
270
|
+
}
|
|
271
|
+
// --- USR: Rework rate (thread consecutive-call heuristic) ---
|
|
272
|
+
//
|
|
273
|
+
// Not a direct DB column — best-effort heuristic per docs/research/ai-skill-report.md §2.1:
|
|
274
|
+
// "재작업률"은 절대값이 아니라 상대·추세로만 해석해야 한다.
|
|
275
|
+
getReworkRate(from, to, workspace) {
|
|
276
|
+
const rows = this.taskRepository.getTasksForReport(from, to, workspace);
|
|
277
|
+
const qualifying = rows.filter((r) => r.agentId !== 'workflow' && (r.status === 'success' || r.status === 'completed') && r.threadId !== null);
|
|
278
|
+
// Rows already ordered by started_at ASC (SQL ORDER BY) — group while preserving order.
|
|
279
|
+
const lastSeenByThread = new Map();
|
|
280
|
+
let reworkCount = 0;
|
|
281
|
+
for (const row of qualifying) {
|
|
282
|
+
const threadId = row.threadId;
|
|
283
|
+
const prevAt = lastSeenByThread.get(threadId);
|
|
284
|
+
if (prevAt) {
|
|
285
|
+
const gapMinutes = (Date.parse(row.startedAt) - Date.parse(prevAt)) / 60000;
|
|
286
|
+
if (gapMinutes <= REWORK_THRESHOLD_MINUTES)
|
|
287
|
+
reworkCount++;
|
|
288
|
+
}
|
|
289
|
+
lastSeenByThread.set(threadId, row.startedAt);
|
|
290
|
+
}
|
|
291
|
+
const threadedTasks = qualifying.length;
|
|
292
|
+
const reworkPct = threadedTasks > 0 ? Math.round((reworkCount / threadedTasks) * 1000) / 10 : 0;
|
|
293
|
+
return { threadedTasks, reworkCount, reworkPct };
|
|
294
|
+
}
|
|
295
|
+
// --- USR: Hourly usage (KST 4-slot) ---
|
|
296
|
+
getHourlyUsage(from, to, workspace) {
|
|
297
|
+
const rows = this.taskRepository.getTasksForReport(from, to, workspace).filter((r) => r.agentId !== 'workflow');
|
|
298
|
+
const buckets = {
|
|
299
|
+
dawn: { total: 0, failed: 0 },
|
|
300
|
+
morning: { total: 0, failed: 0 },
|
|
301
|
+
afternoon: { total: 0, failed: 0 },
|
|
302
|
+
night: { total: 0, failed: 0 },
|
|
303
|
+
};
|
|
304
|
+
for (const row of rows) {
|
|
305
|
+
const kstHour = new Date(Date.parse(row.startedAt) + 9 * 60 * 60 * 1000).getUTCHours();
|
|
306
|
+
const slot = this.kstHourToSlot(kstHour);
|
|
307
|
+
buckets[slot].total++;
|
|
308
|
+
if (row.status === 'failed')
|
|
309
|
+
buckets[slot].failed++;
|
|
310
|
+
}
|
|
311
|
+
const slotOrder = ['dawn', 'morning', 'afternoon', 'night'];
|
|
312
|
+
const slots = slotOrder.map((slot) => {
|
|
313
|
+
const b = buckets[slot];
|
|
314
|
+
return {
|
|
315
|
+
slot,
|
|
316
|
+
total: b.total,
|
|
317
|
+
failPct: b.total > 0 ? Math.round((b.failed / b.total) * 1000) / 10 : 0,
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
return { slots };
|
|
321
|
+
}
|
|
322
|
+
kstHourToSlot(kstHour) {
|
|
323
|
+
if (kstHour >= 6 && kstHour <= 11)
|
|
324
|
+
return 'morning';
|
|
325
|
+
if (kstHour >= 12 && kstHour <= 17)
|
|
326
|
+
return 'afternoon';
|
|
327
|
+
if (kstHour >= 18 && kstHour <= 23)
|
|
328
|
+
return 'night';
|
|
329
|
+
return 'dawn';
|
|
330
|
+
}
|
|
331
|
+
// --- USR: Failure 3-way breakdown ---
|
|
332
|
+
//
|
|
333
|
+
// user_stop / agent_bug are excluded from the failure rate — only true_fail counts
|
|
334
|
+
// (docs/wi/WI-20260704-020.md §Side-effect risk areas).
|
|
335
|
+
getFailureBreakdown(from, to, workspace) {
|
|
336
|
+
const rows = this.taskRepository.getTasksForReport(from, to, workspace).filter((r) => r.agentId !== 'workflow');
|
|
337
|
+
let userStop = 0;
|
|
338
|
+
let agentBug = 0;
|
|
339
|
+
let trueFail = 0;
|
|
340
|
+
for (const row of rows) {
|
|
341
|
+
if (row.status !== 'failed')
|
|
342
|
+
continue;
|
|
343
|
+
const error = row.error ?? '';
|
|
344
|
+
if (USER_STOP_ERROR_RE.test(error)) {
|
|
345
|
+
userStop++;
|
|
346
|
+
}
|
|
347
|
+
else if (AGENT_BUG_ERROR_RE.test(error)) {
|
|
348
|
+
agentBug++;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
trueFail++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const total = rows.length;
|
|
355
|
+
const failPct = total > 0 ? Math.round((trueFail / total) * 1000) / 10 : 0;
|
|
356
|
+
return { total, userStop, agentBug, trueFail, failPct };
|
|
357
|
+
}
|
|
358
|
+
// --- USR: Agent success rate + rework rate (WI-20260705-001) ---
|
|
359
|
+
//
|
|
360
|
+
// Per-agent breakdown for the "partner-fit" section — reuses the exact same 3-way
|
|
361
|
+
// classification as getFailureBreakdown (agent_bug/user_stop excluded from the failure
|
|
362
|
+
// count, only true_fail counts) so that per-agent rates stay consistent with the global
|
|
363
|
+
// failureBreakdown. Rework detection reuses the same REWORK_THRESHOLD_MINUTES heuristic
|
|
364
|
+
// as getReworkRate (thread-partitioned, globally ordered), attributing each detected
|
|
365
|
+
// rework event to the agent of the later (follow-up) call.
|
|
366
|
+
getAgentSuccess(from, to, workspace) {
|
|
367
|
+
const rows = this.taskRepository.getTasksForReport(from, to, workspace).filter((r) => r.agentId !== 'workflow');
|
|
368
|
+
const byAgent = new Map();
|
|
369
|
+
const getAcc = (agentId) => {
|
|
370
|
+
let acc = byAgent.get(agentId);
|
|
371
|
+
if (!acc) {
|
|
372
|
+
acc = { taskCount: 0, trueFail: 0, reworkCount: 0 };
|
|
373
|
+
byAgent.set(agentId, acc);
|
|
374
|
+
}
|
|
375
|
+
return acc;
|
|
376
|
+
};
|
|
377
|
+
for (const row of rows) {
|
|
378
|
+
const acc = getAcc(row.agentId);
|
|
379
|
+
acc.taskCount++;
|
|
380
|
+
if (row.status === 'failed') {
|
|
381
|
+
const error = row.error ?? '';
|
|
382
|
+
if (!USER_STOP_ERROR_RE.test(error) && !AGENT_BUG_ERROR_RE.test(error)) {
|
|
383
|
+
acc.trueFail++;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Rows already ordered by started_at ASC (SQL ORDER BY) — same qualifying filter as
|
|
388
|
+
// getReworkRate, thread-partitioned globally (not per-agent).
|
|
389
|
+
const qualifying = rows.filter((r) => (r.status === 'success' || r.status === 'completed') && r.threadId !== null);
|
|
390
|
+
const lastSeenByThread = new Map();
|
|
391
|
+
for (const row of qualifying) {
|
|
392
|
+
const threadId = row.threadId;
|
|
393
|
+
const prevAt = lastSeenByThread.get(threadId);
|
|
394
|
+
if (prevAt) {
|
|
395
|
+
const gapMinutes = (Date.parse(row.startedAt) - Date.parse(prevAt)) / 60000;
|
|
396
|
+
if (gapMinutes <= REWORK_THRESHOLD_MINUTES) {
|
|
397
|
+
getAcc(row.agentId).reworkCount++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
lastSeenByThread.set(threadId, row.startedAt);
|
|
401
|
+
}
|
|
402
|
+
const agents = Array.from(byAgent.entries()).map(([agentId, acc]) => {
|
|
403
|
+
const successCount = acc.taskCount - acc.trueFail;
|
|
404
|
+
return {
|
|
405
|
+
agentId,
|
|
406
|
+
taskCount: acc.taskCount,
|
|
407
|
+
successCount,
|
|
408
|
+
successPct: acc.taskCount > 0 ? Math.round((successCount / acc.taskCount) * 1000) / 10 : 0,
|
|
409
|
+
reworkCount: acc.reworkCount,
|
|
410
|
+
reworkPct: acc.taskCount > 0 ? Math.round((acc.reworkCount / acc.taskCount) * 1000) / 10 : 0,
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
return { agents };
|
|
414
|
+
}
|
|
234
415
|
/** Convert UTC ISO string to local date string (YYYY-MM-DD) using timezone. */
|
|
235
416
|
utcToLocalDate(utcIso, tz) {
|
|
236
417
|
try {
|
|
@@ -518,6 +699,15 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
518
699
|
return { fromIso: from.toISOString(), toIso };
|
|
519
700
|
}
|
|
520
701
|
async parseCodexLimits() {
|
|
702
|
+
const now = Date.now();
|
|
703
|
+
if (this.oauthCacheValid(this.codexCache, now, CODEX_CACHE_TTL)) {
|
|
704
|
+
return this.codexCache.data;
|
|
705
|
+
}
|
|
706
|
+
const result = this.computeCodexLimits();
|
|
707
|
+
this.codexCache = { data: result, ts: now };
|
|
708
|
+
return result;
|
|
709
|
+
}
|
|
710
|
+
computeCodexLimits() {
|
|
521
711
|
try {
|
|
522
712
|
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
523
713
|
const jsonlFile = this.findLatestCodexJsonl(sessionsDir);
|
|
@@ -536,10 +726,7 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
536
726
|
const rateLimits = payload?.['rate_limits'];
|
|
537
727
|
if (!rateLimits)
|
|
538
728
|
continue;
|
|
539
|
-
return
|
|
540
|
-
fiveHour: this.extractCodexWindow(rateLimits['primary']),
|
|
541
|
-
weekly: this.extractCodexWindow(rateLimits['secondary']),
|
|
542
|
-
};
|
|
729
|
+
return this.classifyCodexLimits(rateLimits);
|
|
543
730
|
}
|
|
544
731
|
catch {
|
|
545
732
|
// partial write — try next line
|
|
@@ -587,6 +774,61 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
587
774
|
return null;
|
|
588
775
|
}
|
|
589
776
|
}
|
|
777
|
+
/**
|
|
778
|
+
* Classify Codex rate_limits.primary/secondary into fiveHour/weekly by window_minutes
|
|
779
|
+
* (WI-20260713-001). Codex CLI payload slot order (primary/secondary) is not stable —
|
|
780
|
+
* some accounts send primary=weekly with secondary=null. window_minutes value is the
|
|
781
|
+
* only reliable discriminator:
|
|
782
|
+
* - 299~300 -> fiveHour
|
|
783
|
+
* - 10079~10080 -> weekly
|
|
784
|
+
* Legacy fallback (positional primary->fiveHour, secondary->weekly) is applied only
|
|
785
|
+
* when window_minutes is entirely absent AND both primary+secondary are present
|
|
786
|
+
* (old dual-slot format). A single present slot with no window_minutes, or a
|
|
787
|
+
* present-but-unrecognized window_minutes value, is left unclassified rather than
|
|
788
|
+
* guessed.
|
|
789
|
+
*/
|
|
790
|
+
classifyCodexLimits(rateLimits) {
|
|
791
|
+
const primary = rateLimits['primary'];
|
|
792
|
+
const secondary = rateLimits['secondary'];
|
|
793
|
+
const primaryIsObj = typeof primary === 'object' && primary !== null;
|
|
794
|
+
const secondaryIsObj = typeof secondary === 'object' && secondary !== null;
|
|
795
|
+
let fiveHour = null;
|
|
796
|
+
let weekly = null;
|
|
797
|
+
const classifySlot = (slot) => {
|
|
798
|
+
const wm = slot['window_minutes'];
|
|
799
|
+
if (typeof wm !== 'number')
|
|
800
|
+
return 'absent';
|
|
801
|
+
if (wm >= 299 && wm <= 300)
|
|
802
|
+
return 'fiveHour';
|
|
803
|
+
if (wm >= 10079 && wm <= 10080)
|
|
804
|
+
return 'weekly';
|
|
805
|
+
return 'unrecognized';
|
|
806
|
+
};
|
|
807
|
+
if (primaryIsObj) {
|
|
808
|
+
const cls = classifySlot(primary);
|
|
809
|
+
if (cls === 'fiveHour')
|
|
810
|
+
fiveHour = this.extractCodexWindow(primary);
|
|
811
|
+
else if (cls === 'weekly')
|
|
812
|
+
weekly = this.extractCodexWindow(primary);
|
|
813
|
+
}
|
|
814
|
+
if (secondaryIsObj) {
|
|
815
|
+
const cls = classifySlot(secondary);
|
|
816
|
+
if (cls === 'fiveHour')
|
|
817
|
+
fiveHour = this.extractCodexWindow(secondary);
|
|
818
|
+
else if (cls === 'weekly')
|
|
819
|
+
weekly = this.extractCodexWindow(secondary);
|
|
820
|
+
}
|
|
821
|
+
// Absent-only legacy fallback — requires both slots present (old dual-slot shape).
|
|
822
|
+
if (primaryIsObj && secondaryIsObj) {
|
|
823
|
+
if (fiveHour === null && classifySlot(primary) === 'absent') {
|
|
824
|
+
fiveHour = this.extractCodexWindow(primary);
|
|
825
|
+
}
|
|
826
|
+
if (weekly === null && classifySlot(secondary) === 'absent') {
|
|
827
|
+
weekly = this.extractCodexWindow(secondary);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return { fiveHour, weekly };
|
|
831
|
+
}
|
|
590
832
|
extractCodexWindow(raw) {
|
|
591
833
|
if (!raw || typeof raw !== 'object')
|
|
592
834
|
return null;
|