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; export const ScheduleKind = z.enum(["task", "agent_check"]); export type ScheduleKind = z.infer; export const ScheduleRunStatus = z.enum(["ok", "failed", "skipped", "running"]); export type ScheduleRunStatus = z.infer; export const ScheduleTrigger = z.enum(["schedule", "manual", "skip"]); export type ScheduleTrigger = z.infer; /** 用户可设的间隔下限(内置默认值可以更短,如测试) */ 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; /** 用户提交的调度:间隔有上下限 */ 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; /** * 巡检触发条件:到点先按业务表判断,不满足记 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; 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; export type AgentCheckConfigInput = z.input; /** 巡检最近一次会话(卡片「最近会话」) */ 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; /** * 巡检会话的费用与模型汇总(INT-3):该巡检最近 100 次会话(agent_run.created_by = schedule:,不含子代理)。 * 计划页卡片与首页计划带直接用,不再各自拉会话列表。 */ 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; 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; 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; 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; 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; 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; export const ScheduleRunNowResult = z.object({ job: ScheduledJob, /** false:已在排队或运行中,本次请求被合并 */ queued: z.boolean(), }); export type ScheduleRunNowResult = z.infer; 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() });