feat(schedules): per-check cost and model summary on GET /schedules (INT-3)

agent_check items carry agentRuns (last 100 sessions: last/avg cost, 7-day count and cost,
models, recent 3); plan page and home plan band use it instead of fetching agent runs.
Migration 0052 indexes agent_run(created_by, created_at).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 17:55:47 +08:00
1 parent 82df5662f0
commit e681fcedf6
9 files changed
+200 -78

No files matched your search

+38 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { AgentBudget } from "./agent.ts";
import { AgentBudget, AgentRunStatus } from "./agent.ts";
import { KnownCapabilityId } from "./capability.ts";
import { Timestamp } from "./common.ts";
@@ -67,6 +67,41 @@ export const AgentCheckConfig = z.object({
export type AgentCheckConfig = z.infer<typeof AgentCheckConfig>;
export type AgentCheckConfigInput = z.input<typeof AgentCheckConfig>;
/** 巡检最近一次会话(卡片「最近会话」) */
export const AgentCheckRecentRun = z.object({
id: z.uuid(),
status: AgentRunStatus,
goal: z.string(),
summary: z.string().nullable(),
costUsd: z.number().nullable(),
createdAt: Timestamp,
finishedAt: Timestamp.nullable(),
});
export type AgentCheckRecentRun = z.infer<typeof AgentCheckRecentRun>;
/**
* 巡检会话的费用与模型汇总(INT-3):该巡检最近 100 次会话(agent_run.created_by = schedule:<id>,不含子代理)。
* 计划页卡片与首页计划带直接用,不再各自拉会话列表。
*/
export const AgentCheckRunStats = z.object({
/** 参与汇总的会话数(≤ 100) */
count: z.number().int().nonnegative(),
lastCostUsd: z.number().nullable(),
/** 有费用记录的会话的平均费用;没有为 null */
avgCostUsd: z.number().nullable(),
/** 近 7 天会话数与费用合计(最近 100 次以内) */
weekCount: z.number().int().nonnegative(),
weekCostUsd: z.number().nonnegative(),
/** 最近一次有模型记录的会话用的上游 / 模型 */
lastProvider: z.string().nullable(),
lastModel: z.string().nullable(),
/** 按模型分组的会话数(多 → 少) */
models: z.array(z.object({ provider: z.string().nullable(), model: z.string(), count: z.number().int().positive() })),
/** 最近 3 次会话(新 → 旧) */
recent: z.array(AgentCheckRecentRun),
});
export type AgentCheckRunStats = z.infer<typeof AgentCheckRunStats>;
export const ScheduledJob = z.object({
id: z.string().min(1),
name: z.string().min(1),
@@ -94,6 +129,8 @@ export const ScheduledJob = z.object({
failCount: z.number().int().nonnegative(),
createdAt: Timestamp,
updatedAt: Timestamp,
/** 只有 kind=agent_check 且在看板列表(GET /schedules)里带 */
agentRuns: AgentCheckRunStats.optional(),
});
export type ScheduledJob = z.infer<typeof ScheduledJob>;
@@ -0,0 +1,17 @@
import type { Kysely } from "kysely";
import { execDdl } from "../src/migration-helpers.ts";
// INT-3:计划页 GET /schedules 给每个 Agent 巡检汇总最近 100 次会话(created_by = schedule:<id>)的费用与模型。
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`create index if not exists agent_run_created_by_idx on agent_run (created_by, created_at desc) where created_by is not null;`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(db, `drop index if exists agent_run_created_by_idx;`);
}