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]>
205 lines
8.4 KiB
TypeScript
205 lines
8.4 KiB
TypeScript
import { z } from "zod";
|
|
import { AgentBudget, AgentRunStatus } from "./agent.ts";
|
|
import { KnownCapabilityId } from "./capability.ts";
|
|
import { Timestamp } from "./common.ts";
|
|
|
|
// 定时任务统一注册表(W7-C,docs/runbook/schedules.md):
|
|
// 各模块的周期逻辑登记为 scheduled_job,由 io worker 上唯一的调度器(PG 咨询锁)按 next_run_at 触发;
|
|
// 看板 /console/schedules 展示与调整(开关、间隔/cron、立即执行、跳过本次、历史)。
|
|
// kind=agent_check 是"Agent 巡检":到点按目标模板入队 runAgent(能力白名单 + 预算)。
|
|
|
|
/** 看板分组:采集 / 下载 / 整理 / 维护 / Agent 巡检 */
|
|
export const ScheduleGroup = z.enum(["collect", "download", "organize", "maintain", "agent"]);
|
|
export type ScheduleGroup = z.infer<typeof ScheduleGroup>;
|
|
|
|
export const ScheduleKind = z.enum(["task", "agent_check"]);
|
|
export type ScheduleKind = z.infer<typeof ScheduleKind>;
|
|
|
|
export const ScheduleRunStatus = z.enum(["ok", "failed", "skipped", "running"]);
|
|
export type ScheduleRunStatus = z.infer<typeof ScheduleRunStatus>;
|
|
|
|
export const ScheduleTrigger = z.enum(["schedule", "manual", "skip"]);
|
|
export type ScheduleTrigger = z.infer<typeof ScheduleTrigger>;
|
|
|
|
/** 用户可设的间隔下限(内置默认值可以更短,如测试) */
|
|
export const MIN_INTERVAL_MS = 10_000;
|
|
export const MAX_INTERVAL_MS = 30 * 24 * 3600_000;
|
|
|
|
/** 5 段 cron(分 时 日 月 周),支持 * , - / */
|
|
export const CronExpr = z
|
|
.string()
|
|
.trim()
|
|
.regex(/^(\S+\s+){4}\S+$/, "cron 需要 5 段:分 时 日 月 周");
|
|
|
|
export const ScheduleSpec = z.discriminatedUnion("type", [
|
|
z.object({ type: z.literal("interval"), everyMs: z.number().int().positive() }),
|
|
z.object({ type: z.literal("cron"), cron: CronExpr }),
|
|
]);
|
|
export type ScheduleSpec = z.infer<typeof ScheduleSpec>;
|
|
|
|
/** 用户提交的调度:间隔有上下限 */
|
|
export const ScheduleSpecInput = z.discriminatedUnion("type", [
|
|
z.object({ type: z.literal("interval"), everyMs: z.number().int().min(MIN_INTERVAL_MS).max(MAX_INTERVAL_MS) }),
|
|
z.object({ type: z.literal("cron"), cron: CronExpr }),
|
|
]);
|
|
export type ScheduleSpecInput = z.infer<typeof ScheduleSpecInput>;
|
|
|
|
/**
|
|
* 巡检触发条件:到点先按业务表判断,不满足记 skipped,不唤醒模型。
|
|
* always 无条件;open_decisions 有未关闭待处理;stalled_downloads 有无进展/失败的下载;
|
|
* subscriptions_active 有追踪中的订阅。
|
|
*/
|
|
export const AgentCheckTrigger = z.enum(["always", "open_decisions", "stalled_downloads", "subscriptions_active"]);
|
|
export type AgentCheckTrigger = z.infer<typeof AgentCheckTrigger>;
|
|
|
|
export const AgentCheckConfig = z.object({
|
|
/** 目标模板:{{now}} {{date}} {{openDecisions}} {{stalledDownloads}} {{activeSubscriptions}} 会被替换 */
|
|
goalTemplate: z.string().trim().min(4).max(4000),
|
|
/** 能力白名单(空 = 只读类全部) */
|
|
capabilityAllowlist: z.array(KnownCapabilityId).max(40).default([]),
|
|
budget: AgentBudget.default({}),
|
|
triggerCondition: AgentCheckTrigger.default("always"),
|
|
/** 允许授权内恢复(task.retry / download.recheck):仍需有效持续授权,否则降级只读 */
|
|
allowRecover: z.boolean().default(false),
|
|
/** 结果写成一条待处理(info)事项 */
|
|
reportAsDecision: z.boolean().default(false),
|
|
});
|
|
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),
|
|
description: z.string().nullable(),
|
|
group: ScheduleGroup,
|
|
kind: ScheduleKind,
|
|
/** 内置(代码登记)还是用户新建的自定义巡检 */
|
|
builtin: z.boolean(),
|
|
/** false = 占位(如备份未配置),不会执行 */
|
|
available: z.boolean(),
|
|
enabled: z.boolean(),
|
|
schedule: ScheduleSpec,
|
|
defaultSchedule: ScheduleSpec,
|
|
/** 用户改过开关或调度(之后模块登记不再覆盖) */
|
|
customized: z.boolean(),
|
|
config: z.record(z.string(), z.unknown()),
|
|
lastRunAt: Timestamp.nullable(),
|
|
lastStatus: ScheduleRunStatus.nullable(),
|
|
lastDurationMs: z.number().int().nonnegative().nullable(),
|
|
lastError: z.string().nullable(),
|
|
nextRunAt: Timestamp.nullable(),
|
|
runRequestedAt: Timestamp.nullable(),
|
|
runningSince: Timestamp.nullable(),
|
|
runCount: z.number().int().nonnegative(),
|
|
failCount: z.number().int().nonnegative(),
|
|
createdAt: Timestamp,
|
|
updatedAt: Timestamp,
|
|
/** 只有 kind=agent_check 且在看板列表(GET /schedules)里带 */
|
|
agentRuns: AgentCheckRunStats.optional(),
|
|
});
|
|
export type ScheduledJob = z.infer<typeof ScheduledJob>;
|
|
|
|
export const ScheduledJobRun = z.object({
|
|
id: z.uuid(),
|
|
jobId: z.string(),
|
|
trigger: ScheduleTrigger,
|
|
status: ScheduleRunStatus,
|
|
startedAt: Timestamp,
|
|
finishedAt: Timestamp.nullable(),
|
|
durationMs: z.number().int().nonnegative().nullable(),
|
|
error: z.string().nullable(),
|
|
summary: z.string().nullable(),
|
|
agentRunId: z.uuid().nullable(),
|
|
/** 巡检入队的 agent_run 结束后回填其终态 */
|
|
agentStatus: z.string().nullable(),
|
|
createdAt: Timestamp,
|
|
});
|
|
export type ScheduledJobRun = z.infer<typeof ScheduledJobRun>;
|
|
|
|
export const ScheduleBoard = z.object({
|
|
items: z.array(ScheduledJob),
|
|
stats: z.object({
|
|
total: z.number().int().nonnegative(),
|
|
enabled: z.number().int().nonnegative(),
|
|
failed: z.number().int().nonnegative(),
|
|
running: z.number().int().nonnegative(),
|
|
}),
|
|
/** 有没有调度器持有锁(io worker 在线) */
|
|
schedulerOnline: z.boolean(),
|
|
});
|
|
export type ScheduleBoard = z.infer<typeof ScheduleBoard>;
|
|
|
|
export const ScheduleIdParam = z.object({ id: z.string().min(1).max(200) });
|
|
|
|
export const ScheduleUpdate = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
schedule: ScheduleSpecInput.optional(),
|
|
/** agent_check 为 AgentCheckConfig(整体替换);task 为任务参数(浅合并) */
|
|
config: z.record(z.string(), z.unknown()).optional(),
|
|
/** 恢复默认开关与调度 */
|
|
reset: z.boolean().optional(),
|
|
/** 只有自定义巡检可改名称与说明 */
|
|
name: z.string().trim().min(1).max(60).optional(),
|
|
description: z.string().trim().max(200).optional(),
|
|
})
|
|
.refine((v) => Object.keys(v).length > 0, "至少修改一项");
|
|
export type ScheduleUpdate = z.infer<typeof ScheduleUpdate>;
|
|
|
|
export const AgentCheckCreate = z.object({
|
|
name: z.string().trim().min(1).max(60),
|
|
description: z.string().trim().max(200).optional(),
|
|
enabled: z.boolean().default(false),
|
|
schedule: ScheduleSpecInput,
|
|
config: AgentCheckConfig,
|
|
});
|
|
export type AgentCheckCreate = z.input<typeof AgentCheckCreate>;
|
|
|
|
export const ScheduleRunNowResult = z.object({
|
|
job: ScheduledJob,
|
|
/** false:已在排队或运行中,本次请求被合并 */
|
|
queued: z.boolean(),
|
|
});
|
|
export type ScheduleRunNowResult = z.infer<typeof ScheduleRunNowResult>;
|
|
|
|
export const ScheduleRunsQuery = z.object({ limit: z.coerce.number().int().min(1).max(100).default(30) });
|
|
|
|
/** SSE:调度器每次开始/结束一次运行、用户修改后发出 */
|
|
export const SCHEDULE_CHANGED_EVENT = "schedule.changed";
|
|
export const ScheduleChangedPayload = z.object({ jobId: z.string(), status: ScheduleRunStatus.nullable() });
|