c0de-agent 1.12.0 → 1.13.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/README.md +51 -23
- package/dist/cli/commands/acp.js +26 -8
- package/dist/cli/commands/chat.js +70 -6
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +46 -0
- package/dist/cli/commands/serve.js +11 -2
- package/dist/cli/commands/sessions.js +51 -4
- package/dist/cli/commands/trust.d.ts +11 -0
- package/dist/cli/commands/trust.js +31 -0
- package/dist/cli/commands/update.js +4 -0
- package/dist/cli/deps.d.ts +9 -6
- package/dist/cli/deps.js +66 -19
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +92 -12
- package/dist/cli/modes/acp.js +39 -8
- package/dist/cli/modes/print.d.ts +3 -0
- package/dist/cli/modes/print.js +71 -8
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/agent.js +9 -4
- package/dist/core/agents/builtin.js +2 -2
- package/dist/core/agents/index.d.ts +0 -1
- package/dist/core/agents/index.js +0 -1
- package/dist/core/config.d.ts +43 -1
- package/dist/core/config.js +163 -14
- package/dist/core/loop/segment.d.ts +3 -1
- package/dist/core/loop/segment.js +24 -1
- package/dist/core/loop/subagent.js +65 -32
- package/dist/core/loop.d.ts +7 -0
- package/dist/core/loop.js +90 -6
- package/dist/core/slash.d.ts +10 -0
- package/dist/core/slash.js +150 -20
- package/dist/core/types.d.ts +20 -1
- package/dist/core/workflows/context.js +44 -4
- package/dist/core/workflows/discovery.d.ts +2 -2
- package/dist/core/workflows/discovery.js +5 -7
- package/dist/core/workflows/index.d.ts +2 -1
- package/dist/core/workflows/index.js +2 -1
- package/dist/core/workflows/registry.d.ts +13 -3
- package/dist/core/workflows/registry.js +14 -9
- package/dist/core/workflows/resolve.d.ts +22 -0
- package/dist/core/workflows/resolve.js +23 -0
- package/dist/core/workflows/runtime.d.ts +6 -1
- package/dist/core/workflows/runtime.js +19 -2
- package/dist/db/schema.d.ts +356 -1
- package/dist/db/schema.js +57 -3
- package/dist/kanban/index.d.ts +2 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +84 -2
- package/dist/kanban/store.js +358 -25
- package/dist/llm/registry.d.ts +7 -1
- package/dist/llm/registry.js +7 -1
- package/dist/plugins/init.d.ts +7 -0
- package/dist/plugins/init.js +22 -1
- package/dist/plugins/loader.d.ts +7 -1
- package/dist/plugins/loader.js +12 -4
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/project.d.ts +23 -0
- package/dist/project/project.js +65 -0
- package/dist/project/trust.d.ts +65 -0
- package/dist/project/trust.js +328 -0
- package/dist/server/agent-manager.d.ts +6 -2
- package/dist/server/agent-manager.js +4 -2
- package/dist/server/app.js +45 -30
- package/dist/server/auth-manager.d.ts +15 -4
- package/dist/server/auth-manager.js +41 -24
- package/dist/server/context.js +11 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/middleware/auth.d.ts +11 -1
- package/dist/server/middleware/auth.js +25 -3
- package/dist/server/permission/interactive.d.ts +5 -0
- package/dist/server/permission/interactive.js +4 -1
- package/dist/server/permission/store.d.ts +2 -0
- package/dist/server/permission/store.js +8 -0
- package/dist/server/routes/auth.js +35 -9
- package/dist/server/routes/chat.d.ts +16 -7
- package/dist/server/routes/chat.js +483 -28
- package/dist/server/routes/commands.d.ts +2 -2
- package/dist/server/routes/commands.js +4 -6
- package/dist/server/routes/config.js +81 -4
- package/dist/server/routes/files.js +120 -24
- package/dist/server/routes/kanban.js +152 -22
- package/dist/server/routes/permissions.js +7 -0
- package/dist/server/routes/project.js +76 -22
- package/dist/server/routes/provider.js +6 -0
- package/dist/server/routes/session.js +177 -13
- package/dist/server/routes/tool.js +24 -5
- package/dist/server/routes/update.js +58 -2
- package/dist/server/routes/usage.d.ts +4 -0
- package/dist/server/routes/usage.js +133 -0
- package/dist/server/routes/workflows.js +476 -87
- package/dist/server/server.d.ts +12 -1
- package/dist/server/server.js +194 -33
- package/dist/server/terminal/pty-manager.d.ts +12 -0
- package/dist/server/terminal/pty-manager.js +63 -7
- package/dist/server/types.d.ts +9 -0
- package/dist/session/branch.d.ts +2 -1
- package/dist/session/branch.js +75 -10
- package/dist/session/context.js +28 -2
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/session.d.ts +67 -10
- package/dist/session/session.js +180 -67
- package/dist/session/types.d.ts +2 -0
- package/dist/session/usage.d.ts +62 -0
- package/dist/session/usage.js +194 -0
- package/dist/shared/types/agent.d.ts +19 -3
- package/dist/shared/types/config.d.ts +59 -4
- package/dist/shared/types/kanban.d.ts +4 -2
- package/dist/shared/types/message.d.ts +14 -0
- package/dist/tools/builtin/kanban.js +13 -2
- package/dist/tools/index.d.ts +14 -3
- package/dist/tools/index.js +23 -5
- package/dist/tools/websearch/index.d.ts +5 -1
- package/dist/tools/websearch/index.js +40 -4
- package/dist/tools/websearch/providers/brave.js +2 -2
- package/dist/tools/websearch/providers/duckduckgo.js +2 -1
- package/dist/tools/websearch/providers/tavily.js +2 -2
- package/dist/tools/websearch/types.d.ts +5 -1
- package/dist/tools/websearch/types.js +7 -1
- package/dist/update/scheduler.js +5 -2
- package/dist/update/snapshot.d.ts +17 -3
- package/dist/update/snapshot.js +3 -2
- package/dist/update/updater.d.ts +2 -0
- package/dist/update/updater.js +2 -1
- package/dist/update/version.d.ts +3 -0
- package/dist/update/version.js +9 -3
- package/drizzle/0010_usage_events_ledger.sql +17 -0
- package/drizzle/0011_slow_slapstick.sql +12 -0
- package/drizzle/0012_chief_timeslip.sql +1 -0
- package/drizzle/0013_swift_zeigeist.sql +1 -0
- package/drizzle/0014_flimsy_legion.sql +1 -0
- package/drizzle/meta/0010_snapshot.json +1010 -0
- package/drizzle/meta/0011_snapshot.json +1055 -0
- package/drizzle/meta/0012_snapshot.json +1062 -0
- package/drizzle/meta/0013_snapshot.json +1068 -0
- package/drizzle/meta/0014_snapshot.json +1075 -0
- package/drizzle/meta/_journal.json +35 -0
- package/package.json +24 -23
- package/dist/core/agents/discovery.d.ts +0 -6
- package/dist/core/agents/discovery.js +0 -96
|
@@ -2,6 +2,7 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { getSession } from '../../session/session.js';
|
|
3
3
|
import { getCurrentVersion, manualInstallCommand, performHandoff, performInstall, serializeSessions, } from '../../update/index.js';
|
|
4
4
|
import { apiError } from '../middleware/error.js';
|
|
5
|
+
import { captureForegroundCommand } from '../terminal/pty-manager.js';
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/update — 返回后台调度器缓存的版本检查结果(spec §18.1)。
|
|
7
8
|
* 路由本身不触发 npm registry 请求;调度器周期性 checkNow 写入缓存,
|
|
@@ -26,6 +27,13 @@ function createUpdateRoute(ctx) {
|
|
|
26
27
|
// 顶层 run(主 agent)+ 终端数;子 agent 归父会话,不重复列出。
|
|
27
28
|
const active = ctx.agentManager.listActive();
|
|
28
29
|
const topRuns = active.filter((r) => !r.parentSessionId);
|
|
30
|
+
// P3:受影响会话集合——全部活跃 run 会话 + workflow busy 的发起/工作流会话
|
|
31
|
+
//(工作流运行期发起会话无主 run,但其挂起权限同样会被更新中断)。
|
|
32
|
+
const affectedSessionIds = new Set(active.map((r) => r.sessionId));
|
|
33
|
+
for (const [initiator, wf] of ctx.workflowBusyBySession) {
|
|
34
|
+
affectedSessionIds.add(initiator);
|
|
35
|
+
affectedSessionIds.add(wf);
|
|
36
|
+
}
|
|
29
37
|
const runs = [];
|
|
30
38
|
for (const r of topRuns) {
|
|
31
39
|
let title = r.sessionId.slice(0, 8);
|
|
@@ -41,9 +49,33 @@ function createUpdateRoute(ctx) {
|
|
|
41
49
|
sessionId: r.sessionId,
|
|
42
50
|
title,
|
|
43
51
|
...(r.agentType ? { agentType: r.agentType } : {}),
|
|
52
|
+
...(r.status ? { status: r.status } : {}),
|
|
53
|
+
...(r.currentTool ? { currentTool: r.currentTool } : {}),
|
|
44
54
|
});
|
|
45
55
|
}
|
|
46
|
-
const impact = {
|
|
56
|
+
const impact = {
|
|
57
|
+
runs,
|
|
58
|
+
terminalCount: ctx.ptyManager.list().length,
|
|
59
|
+
// P1:逐项列出终端(标题 + shell + 目录),确认弹窗据此展示——
|
|
60
|
+
// 用户可判断被关闭的终端是否正在跑重要进程。
|
|
61
|
+
terminals: ctx.ptyManager.list().map((t) => {
|
|
62
|
+
const command = captureForegroundCommand(t.pid);
|
|
63
|
+
return {
|
|
64
|
+
id: t.id,
|
|
65
|
+
title: t.title,
|
|
66
|
+
shell: t.shell,
|
|
67
|
+
cwd: t.cwd,
|
|
68
|
+
// P3-7:检测到的前台命令(仅单一前台子进程时非空),供确认框勾选「自动重启」。
|
|
69
|
+
...(command ? { command } : {}),
|
|
70
|
+
};
|
|
71
|
+
}),
|
|
72
|
+
// P3-9:正在等待确认的权限请求——更新会中断其所属 run,弹窗静默失效
|
|
73
|
+
// (隐含按拒绝处理),确认层明示数量让用户知情。
|
|
74
|
+
// P3 修复:仅统计受影响会话(活跃 run 会话 + workflow busy 发起会话)的挂起
|
|
75
|
+
// 请求——此前用全局 store.size(),其他无关会话/已结束 run 的遗留 pending
|
|
76
|
+
// 也计入,数量虚高。
|
|
77
|
+
pendingPermissionCount: ctx.permissionStore.countForSessions(affectedSessionIds),
|
|
78
|
+
};
|
|
47
79
|
// P2-8:update.enabled=false 时无 handoff server,apply 必然 409。
|
|
48
80
|
// 不再触发 checkNow,也不返回 hasUpdate,避免横幅出现一个点了必失败的应用按钮。
|
|
49
81
|
if (ctx.config.update.enabled === false) {
|
|
@@ -74,8 +106,18 @@ function createUpdateRoute(ctx) {
|
|
|
74
106
|
if (!ctx.handoff) {
|
|
75
107
|
return apiError(c, 409, 'HOT_UPDATE_UNAVAILABLE', '热更新仅在独立 serve 进程可用(dev 模式请手动更新)');
|
|
76
108
|
}
|
|
109
|
+
// P3-7:用户勾选「更新后自动重启」的终端 id;命中者在快照中携带前台命令。
|
|
110
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
111
|
+
const rerunIds = new Set(Array.isArray(body.rerunTerminalIds)
|
|
112
|
+
? body.rerunTerminalIds.filter((x) => typeof x === 'string')
|
|
113
|
+
: []);
|
|
77
114
|
const result = await ctx.updateScheduler.checkNow();
|
|
78
115
|
if (!result.hasUpdate) {
|
|
116
|
+
// P3-8:检查失败与「已是最新」分开——网络/registry 异常给可操作的
|
|
117
|
+
// 重试指引,不再误导为「无需更新」。
|
|
118
|
+
if (result.checkError) {
|
|
119
|
+
return apiError(c, 502, 'UPDATE_CHECK_FAILED', '无法检查最新版本(registry 连接失败),请检查网络后重试');
|
|
120
|
+
}
|
|
79
121
|
return apiError(c, 409, 'NO_UPDATE', '已是最新版本,无需热更新');
|
|
80
122
|
}
|
|
81
123
|
// P0 修复:install 先行。失败(网络/权限/unknown 安装方式)时未触碰任何
|
|
@@ -99,10 +141,24 @@ function createUpdateRoute(ctx) {
|
|
|
99
141
|
// 检查跳过重复 append)。工具副作用不回滚。
|
|
100
142
|
const pauseTimeoutMs = ctx.config.update.pauseTimeoutMs ?? 30_000;
|
|
101
143
|
const pauseResult = await ctx.agentManager.pauseAll(pauseTimeoutMs);
|
|
102
|
-
const snapshot = await serializeSessions(ctx.db, ctx.config
|
|
144
|
+
const snapshot = await serializeSessions(ctx.db, ctx.config,
|
|
145
|
+
// P1:终端元信息随快照迁移——新实例按原 id 原位重建 shell,
|
|
146
|
+
// 前端持久化布局重连无感(进程内状态无法续命,这是最优恢复)。
|
|
147
|
+
ctx.ptyManager.list().map((t) => {
|
|
148
|
+
const command = rerunIds.has(t.id) ? captureForegroundCommand(t.pid) : null;
|
|
149
|
+
return {
|
|
150
|
+
id: t.id,
|
|
151
|
+
shell: t.shell,
|
|
152
|
+
cwd: t.cwd,
|
|
153
|
+
title: t.title,
|
|
154
|
+
...(t.projectId ? { projectId: t.projectId } : {}),
|
|
155
|
+
...(command ? { command } : {}),
|
|
156
|
+
};
|
|
157
|
+
}));
|
|
103
158
|
const r = await performHandoff(snapshot, method, {
|
|
104
159
|
handoffPort: ctx.handoff.port,
|
|
105
160
|
port: ctx.port,
|
|
161
|
+
...(ctx.host ? { host: ctx.host } : {}),
|
|
106
162
|
// P2-16:传当前 bootstrap(轮换后 ctx.authToken 可能已过期);
|
|
107
163
|
// 旧实例 handoff 端点经 verifyHandoff 接受当前/历史 bootstrap 与设备 token。
|
|
108
164
|
authToken: ctx.authManager?.bootstrap ?? ctx.authToken,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/server/routes/usage.ts
|
|
2
|
+
// 用量/成本聚合端点(P2 成本聚合视图)。数据源:usage_events 账本表——
|
|
3
|
+
// 与 session 生命周期解耦:会话物理清除(回收站到期宽限期满/彻底删除/临时会话
|
|
4
|
+
// 清理)不抹去已发生花费。cost=null 的调用(价格未知)按 $0 计入并单独计数
|
|
5
|
+
// (unknownCostCalls),前端据此提示预算可能低估。
|
|
6
|
+
// ?projectId= 过滤为「该项目口径」聚合:项目设置页的月度预算按本项目成本对比,
|
|
7
|
+
// 不再被其他项目的花费触发告警。月份键为本地时区 YYYY-MM(与服务端同机)。
|
|
8
|
+
import { eq, sql } from 'drizzle-orm';
|
|
9
|
+
import { Hono } from 'hono';
|
|
10
|
+
import { usageEvents } from '../../db/schema.js';
|
|
11
|
+
import { PRICE_CATALOG_VERSION } from '../../llm/registry.js';
|
|
12
|
+
import { localMonthKey } from '../../session/usage.js';
|
|
13
|
+
const emptyTotals = () => ({
|
|
14
|
+
inputTokens: 0,
|
|
15
|
+
outputTokens: 0,
|
|
16
|
+
cacheRead: 0,
|
|
17
|
+
cost: 0,
|
|
18
|
+
unknownCostCalls: 0,
|
|
19
|
+
calls: 0,
|
|
20
|
+
});
|
|
21
|
+
const add = (t, input, output, cacheRead, cost, unknownCostCalls, calls) => {
|
|
22
|
+
t.inputTokens += input;
|
|
23
|
+
t.outputTokens += output;
|
|
24
|
+
t.cacheRead += cacheRead;
|
|
25
|
+
t.cost += cost;
|
|
26
|
+
t.unknownCostCalls += unknownCostCalls;
|
|
27
|
+
t.calls += calls;
|
|
28
|
+
};
|
|
29
|
+
/** 服务端本地时区名(SQL 月份分组用;与 localMonthKey 的本地口径一致)。
|
|
30
|
+
* IANA 名在 PGLite 的 tzdata 中可用(已实证 Asia/Shanghai 等常见时区);
|
|
31
|
+
* 极端不可用时回退 UTC——分组可能跨月边界漂移,但聚合不再 O(全表)。 */
|
|
32
|
+
function serverTimeZone() {
|
|
33
|
+
try {
|
|
34
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 'UTC';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function createUsageRoute(ctx) {
|
|
41
|
+
const app = new Hono();
|
|
42
|
+
// GET /api/usage/summary?projectId= — 总量 + 按月 + 按模型。
|
|
43
|
+
// projectId 提供时仅聚合该项目的调用(项目设置页预算口径);
|
|
44
|
+
// 省略时全库聚合(含已删除项目产生的无归属调用)。
|
|
45
|
+
// P2 修复:此前全表 select 进内存手工聚合,usage_events 无界增长后每次
|
|
46
|
+
// 打开用量面板 O(全部行) 内存与耗时。现在按(本地月份,provider, model,
|
|
47
|
+
// projectId)SQL GROUP BY 聚合,结果行数 O(月份数 × 模型数)。
|
|
48
|
+
app.get('/summary', async (c) => {
|
|
49
|
+
const projectId = c.req.query('projectId');
|
|
50
|
+
const where = projectId ? eq(usageEvents.projectId, projectId) : undefined;
|
|
51
|
+
// SQL 注入面:tz 名来自本机运行时(Intl),经字符白名单校验后以内联字面量
|
|
52
|
+
// 拼接(白名单已排除单引号,无注入面)。必须内联而非参数占位——SELECT 与
|
|
53
|
+
// GROUP BY 中各出现一次的参数化表达式被 PG 判定为不同表达式而拒绝 group by。
|
|
54
|
+
const tz = serverTimeZone();
|
|
55
|
+
if (!/^[A-Za-z0-9_+\-/]+$/.test(tz)) {
|
|
56
|
+
const monthKey = localMonthKey(Date.now());
|
|
57
|
+
return c.json({
|
|
58
|
+
totals: emptyTotals(),
|
|
59
|
+
byMonth: [],
|
|
60
|
+
byModel: [],
|
|
61
|
+
currentMonth: { key: monthKey, ...emptyTotals() },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const monthExpr = sql `to_char(to_timestamp(${usageEvents.timestamp} / 1000.0) AT TIME ZONE '${sql.raw(tz)}', 'YYYY-MM')`;
|
|
65
|
+
const rows = await ctx.db.db
|
|
66
|
+
.select({
|
|
67
|
+
month: monthExpr,
|
|
68
|
+
provider: usageEvents.provider,
|
|
69
|
+
model: usageEvents.model,
|
|
70
|
+
projectId: usageEvents.projectId,
|
|
71
|
+
input: sql `coalesce(sum(${usageEvents.inputTokens}), 0)::float8`,
|
|
72
|
+
output: sql `coalesce(sum(${usageEvents.outputTokens}), 0)::float8`,
|
|
73
|
+
cacheRead: sql `coalesce(sum(${usageEvents.cacheRead}), 0)::float8`,
|
|
74
|
+
cost: sql `coalesce(sum(${usageEvents.cost}), 0)::float8`,
|
|
75
|
+
calls: sql `count(*)::int`,
|
|
76
|
+
unknownCalls: sql `count(*) filter (where ${usageEvents.cost} is null)::int`,
|
|
77
|
+
})
|
|
78
|
+
.from(usageEvents)
|
|
79
|
+
.where(where ?? sql `true`)
|
|
80
|
+
.groupBy(({ month, provider, model, projectId }) => [month, provider, model, projectId]);
|
|
81
|
+
const totals = emptyTotals();
|
|
82
|
+
const byMonth = new Map();
|
|
83
|
+
const byModel = new Map();
|
|
84
|
+
// P1-3:未归属任何项目的调用(CLI 未绑定会话、孤儿会话)单独聚合——
|
|
85
|
+
// 它们不计入任何项目预算,全局视图需明示这一桶。
|
|
86
|
+
const unassigned = emptyTotals();
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
const modelLabel = `${row.provider ?? '未知'}/${row.model ?? '未知'}`;
|
|
89
|
+
// real(float4)列 SQL 求和引入浮点噪声(0.1 → 0.10000000149011612),
|
|
90
|
+
// 金额口径按 6 位小数规整(USD 分以下精度足够,且与旧 JS 侧求和结果一致)。
|
|
91
|
+
const cost = Math.round((row.cost ?? 0) * 1e6) / 1e6;
|
|
92
|
+
const input = row.input;
|
|
93
|
+
const output = row.output;
|
|
94
|
+
const cacheRead = row.cacheRead;
|
|
95
|
+
const unknownCalls = row.unknownCalls;
|
|
96
|
+
const calls = row.calls;
|
|
97
|
+
add(totals, input, output, cacheRead, cost, unknownCalls, calls);
|
|
98
|
+
const m = byMonth.get(row.month) ?? emptyTotals();
|
|
99
|
+
add(m, input, output, cacheRead, cost, unknownCalls, calls);
|
|
100
|
+
byMonth.set(row.month, m);
|
|
101
|
+
const mdl = byModel.get(modelLabel) ?? emptyTotals();
|
|
102
|
+
add(mdl, input, output, cacheRead, cost, unknownCalls, calls);
|
|
103
|
+
byModel.set(modelLabel, mdl);
|
|
104
|
+
if (row.projectId === null) {
|
|
105
|
+
add(unassigned, input, output, cacheRead, cost, unknownCalls, calls);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// P1-4:本月口径由服务端本地时区计算并下发——此前前端用自己的时区
|
|
109
|
+
// 过滤 byMonth 找「本月」,远程访问/容器时区不同时与预算暂停判定
|
|
110
|
+
// (服务端 budgetOverageParts 同用本地时区口径)不一致,徽标显示未超支却被打断。
|
|
111
|
+
const monthKey = localMonthKey(Date.now());
|
|
112
|
+
const current = byMonth.get(monthKey) ?? emptyTotals();
|
|
113
|
+
return c.json({
|
|
114
|
+
totals,
|
|
115
|
+
// H3:价目版本随响应下发,前端标注估算可能过期。
|
|
116
|
+
priceCatalogVersion: PRICE_CATALOG_VERSION,
|
|
117
|
+
// P1-4:服务端权威本月(key + 聚合),前端徽标/面板不再自行计算。
|
|
118
|
+
currentMonth: { key: monthKey, ...current },
|
|
119
|
+
// P1-3:仅全局视图下发(projectId 过滤时未归属桶与该项目无关)。
|
|
120
|
+
...(projectId ? {} : { unassigned }),
|
|
121
|
+
byMonth: [...byMonth.entries()]
|
|
122
|
+
.sort((a, b) => (a[0] < b[0] ? 1 : -1))
|
|
123
|
+
.slice(0, 12)
|
|
124
|
+
.map(([month, t]) => ({ month, ...t })),
|
|
125
|
+
byModel: [...byModel.entries()]
|
|
126
|
+
.sort((a, b) => b[1].cost - a[1].cost)
|
|
127
|
+
.slice(0, 10)
|
|
128
|
+
.map(([model, t]) => ({ model, ...t })),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
return app;
|
|
132
|
+
}
|
|
133
|
+
export { createUsageRoute };
|