scheduler
调度器的类型和工具函数:Trigger、Action、Condition、Job 及其 zod schema,计算重复规则的纯函数,便于模型使用的 ScheduleInput 格式,条件判断,以及无人值守运行的限制常量。
调度器负责在指定时间运行任务,例如按时发出提醒,或者每天早上查一次校园卡余额,低于某个值时通知用户。这个模块定义了任务的数据结构,并提供计算重复时间、判断条件和检查运行频率的函数。插件创建定时任务,或者在界面上显示任务的下次运行时间时,会用到这些内容。
源码:packages/sdk/src/scheduler/{types,recurrence,schedule-input,check,policy}.ts。概念介绍见
调度器。这里的代码都不依赖 React Native,所有时间都按设备本地时间计算。
类型与 schema
/** "HH:MM",24 小时制,本地时间。 */
const timeOfDay = z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'time must be HH:MM (24h)');
const triggerSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('at'), at: z.number() /* epoch ms */ }),
z.object({
type: z.literal('every'),
unit: z.enum(['minute', 'hour', 'day', 'week']),
interval: z.number().int().min(1).max(1000),
/** 单位为 day/week 时:每天的时刻;默认为 `startAt` 的时刻。 */
times: z.array(timeOfDay).min(1).max(24).optional(),
/** 单位为 week 时:0 = 周日 … 6 = 周六;默认为 `startAt` 所在的星期几。 */
weekdays: z.array(z.number().int().min(0).max(6)).min(1).max(7).optional(),
startAt: z.number(), // epoch 毫秒;最早可能的一次
/** 整个序列的总次数,达到后停止。 */
count: z.number().int().min(1).optional(),
/** 最晚可能的一次(包含在内),epoch 毫秒。 */
until: z.number().optional(),
}),
]);
type Trigger = z.infer<typeof triggerSchema>;
const conditionSchema = z.object({
/** 数据源结果中的点分路径,例如 "balance"。为空表示整个结果。 */
path: z.string().default(''),
op: z.enum(['<', '<=', '>', '>=', '==', '!=', 'changed', 'always']),
value: z.union([z.number(), z.string(), z.boolean()]).optional(),
});
type Condition = z.infer<typeof conditionSchema>;
const actionSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('notify'), title: z.string().min(1), body: z.string().optional(), url: z.string().optional() }),
z.object({
type: z.literal('check'),
tool: z.string(), // `<plugin>.<tool>`,只读
args: z.record(z.string(), z.unknown()).optional(),
condition: conditionSchema,
title: z.string().min(1), // `{value}` 会替换成检查得到的值
body: z.string().optional(),
url: z.string().optional(),
}),
z.object({ type: z.literal('agent'), prompt: z.string().min(4).max(1000) }),
]);
type Action = z.infer<typeof actionSchema>;
type JobSource = 'user' | 'agent' | 'plugin';
type JobRun = {
at: number;
/** 这次运行对应的计划时间(手动运行时为 null)。 */
scheduledAt: number | null;
ok: boolean;
notified: boolean;
value?: unknown;
error?: string;
ms: number;
manual?: boolean;
/** `agent` 任务:助手的回复(保持沉默时为空)。 */
output?: string;
/** 未运行:延迟超过 MAX_LATE_MS。 */
missed?: boolean;
};
type Job = {
id: string;
title: string;
trigger: Trigger;
action: Action;
enabled: boolean;
source: JobSource;
runs: number;
lastRunAt: number | null;
nextRunAt: number | null;
lastValue?: unknown;
lastError?: string;
history?: JobRun[];
failures?: number;
alertedAt?: number | null;
meta?: Record<string, unknown>;
createdAt: number;
updatedAt: number;
};
/** 为 true 表示任务在 JS 中运行(检查或助手运行);为 false 表示任务以预先设好的通知发出。 */
const runsInJs: (action: Action) => boolean;任务的动作(Action)有三种:notify 直接发出通知;check 调用一个只读工具,满足条件时才通知;agent 让助手按一段提示词运行。
timeOfDay、triggerSchema、conditionSchema 和 actionSchema 都有导出。如果 check 任务调用的工具不是 effect: 'read',宿主会拒绝运行这个任务。
重复规则引擎
这些纯函数根据触发器计算任务在什么时间运行。
type Occurrence = { at: number; index: number };
function nextOccurrence(trigger: Trigger, after: number): Occurrence | null; // `after` 之后(不含)的第一次
function occurrencesAfter(trigger: Trigger, after: number, limit: number, before?: number): Occurrence[];
function lastOccurrence(trigger: Trigger, t: number): Occurrence | null; // t 及之前的最后一次
function runsBy(trigger: Trigger, t: number): number; // 到 t 为止的次数
function totalOccurrences(trigger: Trigger): number | null; // null 表示没有终点
function describeTrigger(trigger: Trigger, now?: number, locale?: 'zh-Hans' | 'en'): string;
function parseTime(t: string): [number, number]; // "08:30" → [8, 30]
const formatTime: (h: number, m: number) => string; // (8, 5) → "08:05"一个重复序列由一串长度相同的周期组成。周期可以是若干分钟或若干小时、一天,或者从周一开始的一周。每个周期里的时间槽都相同,由时刻和星期组合而成。早于 startAt 的时间槽会被跳过,其余的依次编号为 0、1、2……,直到达到 count 或 until 为止。夏令时切换不会让序列错位。
下面的例子是「从 10 月 20 日起,每 3 天早上 9 点一次,共 16 次」:
const every3Days16Times: Trigger = {
type: 'every', unit: 'day', interval: 3, times: ['09:00'], count: 16,
startAt: new Date(2026, 9, 20).getTime(),
};
occurrencesAfter(every3Days16Times, Date.now(), 3); // 接下来三次
totalOccurrences(every3Days16Times); // 16
describeTrigger(every3Days16Times, Date.now(), 'en'); // 例如 "Every 3 days at 09:00 · 16 times"ScheduleInput(方便模型和表单使用)
Trigger 用 epoch 毫秒表示时间,模型和表单不容易直接填写。ScheduleInput 改用 "YYYY-MM-DD HH:mm" 格式的本地时间字符串,再用 toTrigger 转换成 Trigger。
const scheduleInputSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('once'), at: localDateTime }),
z.object({
type: z.literal('repeat'),
unit: z.enum(['minute', 'hour', 'day', 'week']),
interval: z.number().int().min(1), // 每 N 个单位
times: z.array(timeOfDay).optional(), // day/week
weekdays: z.array(z.number().int().min(0).max(6)).optional(), // week:0 = 周日
start: localDateTime.optional(), // 默认为现在
count: z.number().int().min(1).optional(),
until: localDateTime.optional(), // 包含在内
}),
]);
// localDateTime: /^\d{4}-\d{2}-\d{2}[ T]\d{1,2}:\d{2}(:\d{2})?$/ ("YYYY-MM-DD HH:mm")
type ScheduleInput = z.infer<typeof scheduleInputSchema>;
function toTrigger(input: ScheduleInput, now?: number): Trigger;
function toScheduleInput(t: Trigger): ScheduleInput;
function parseLocal(s: string): number; // 日期格式错误或不存在时抛出异常
function formatLocal(ms: number): string; // "2026-10-20 08:00 周二"
function summarize(trigger: Trigger, now?: number, n?: number): {
schedule: string; // describeTrigger
next: string[]; // 接下来 n 次,用 formatLocal 格式化
total: number | null;
finished: boolean;
};建议把 scheduleInputSchema 用作工具的输入,并在工具结果里返回 summarize 的输出。这样模型拿到的是准确的运行时间,可以复述给用户确认。
条件
check 任务用这些函数从工具结果中取值,判断是否需要通知,并生成通知文字。
/** 从工具结果中读取点分路径("data.balance"、"items.0.amount")。 */
function getPath(value: unknown, path: string): unknown;
/** 判断检查得到的值是否满足条件。`previous` 是上一次运行得到的值。 */
function evaluate(cond: Condition, value: unknown, previous?: unknown): boolean;
/** 把通知文字中的 `{value}` 替换成检查得到的值(数字保留 2 位小数)。 */
function fillTemplate(template: string, value: unknown): string;运算符 changed 只在存在上一次的值、并且与这次的值不同(按 JSON 比较)时为真。数值比较运算符会把数字字符串转成数字;无法转成数字的值,比较结果一律为假。
策略常量
这些常量和函数规定了任务在无人值守时的运行限制,例如最多能有多少个任务、检查任务最短多久运行一次、连续失败多少次后停用。
const MAX_ACTIVE_JOBS = 50;
const MIN_CHECK_GAP_MS = 15 * 60_000;
const MIN_AGENT_GAP_MS = 60 * 60_000;
const MAX_AGENT_JOBS = 10;
const AGENT_MAX_STEPS = 8;
const HISTORY_LIMIT = 20;
const FAIL_ALERT_AFTER = 2;
const DISABLE_AFTER = 10;
const ALERT_COOLDOWN_MS = 6 * 3_600_000;
const MAX_LATE_MS = 2 * 3_600_000;
const LATE_MS = 5 * 60_000;
const NO_REPLY = 'NO_REPLY';
function minGap(trigger: Trigger, now: number): number; // 一次性任务返回 Infinity
function checkScheduleProblem(trigger: Trigger, now: number): string | null; // 说明检查任务的计划为什么过于频繁
function recordRun(job: Job, run: JobRun): { patch: Pick<Job, 'history' | 'failures' | 'alertedAt' | 'enabled'>; alert: 'none' | 'failing' | 'disabled' };
const isLate: (run: Pick<JobRun, 'at' | 'scheduledAt'>) => boolean;
const isSilentReply: (text: string) => boolean; // 为空或以 NO_REPLY 开头
function agentJobHeader(job: { id: string; title: string; prompt: string }): string;这些限制由宿主的调度引擎强制执行。插件可以导入这些常量,在用户创建任务之前就说明限制,避免用户创建后才发现不能运行。