dsh-task-dispatch-table 0.0.0 → 0.1.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 +95 -31
- package/README.zh.md +113 -0
- package/assets/icon-scheduler.svg +1 -0
- package/cordis.patch.yml +9 -0
- package/dist/attachment-allowlist.d.ts +24 -0
- package/dist/attachment-allowlist.js +41 -0
- package/dist/calendar-plan.d.ts +55 -0
- package/dist/calendar-plan.js +104 -0
- package/dist/client.js +72655 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +80 -0
- package/dist/deps.d.ts +25 -0
- package/dist/deps.js +54 -0
- package/dist/dispatch.d.ts +133 -0
- package/dist/dispatch.js +509 -0
- package/dist/event-bus.d.ts +16 -0
- package/dist/event-bus.js +67 -0
- package/dist/event-catalog.d.ts +42 -0
- package/dist/event-catalog.js +60 -0
- package/dist/host.d.ts +299 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +0 -0
- package/dist/receipt.d.ts +38 -0
- package/dist/receipt.js +236 -0
- package/dist/reconcile.d.ts +105 -0
- package/dist/reconcile.js +641 -0
- package/dist/runtime-index.d.ts +102 -0
- package/dist/runtime-index.js +340 -0
- package/dist/schedule-next.d.ts +44 -0
- package/dist/schedule-next.js +216 -0
- package/dist/scheduler.d.ts +82 -0
- package/dist/scheduler.js +639 -0
- package/dist/store.d.ts +461 -0
- package/dist/store.js +845 -0
- package/dist/submit.d.ts +1 -0
- package/dist/submit.js +60 -0
- package/dist/task-assets.d.ts +124 -0
- package/dist/task-assets.js +380 -0
- package/dist/task-sort.d.ts +27 -0
- package/dist/task-sort.js +59 -0
- package/dist/tasks.d.ts +191 -0
- package/dist/tasks.js +527 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +67 -6
- package/index.js +0 -7
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// schedule-next.ts — **cron / once → 下次执行时刻**的纯计算核(服务端与客户端**同一份**,2026-10-05 抽出)。
|
|
2
|
+
//
|
|
3
|
+
// **为什么单独成文件**(本仓规矩:同一个东西被写第二遍就是违规):
|
|
4
|
+
// 「下次执行」此前只有服务端能算(`src/tasks.ts`,但它 import 了 node:fs / zod,进不了客户端
|
|
5
|
+
// bundle)⇒ 右侧栏「查看档」要基于**当前草稿**实时推算「预计下次执行」时没有可用实现,
|
|
6
|
+
// 只能在客户端再抄一份 cron 解析 —— 这正是要杜绝的第二实现。
|
|
7
|
+
// ⇒ 把 tasks.ts 里与「算时刻」有关的纯函数原样迁到这里:**零 node 内建依赖**、唯一外部依赖
|
|
8
|
+
// `cron-parser`(已在 dependencies,client bundle 由 tsdown 内联;chrome99 的 `Intl.formatToParts`
|
|
9
|
+
// + 时区数据可用,`tzOffsetMs` 的前提成立)。`src/tasks.ts` 从这里 re-export,服务端调用面零改动。
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ 与 [`deps.ts`](./deps.ts) 同一条设计约束:本模块不许 import `node:*`、宿主模块或 zod;
|
|
12
|
+
// 类型用本文件声明的**最小结构**(服务端 `TaskDefinition` 与客户端草稿组装物都天然形状兼容)。
|
|
13
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
14
|
+
/** 取某时刻在指定时区的日历日(logical date 归属用计划时刻,state-machine §9)。 */
|
|
15
|
+
export function logicalDateOf(date, timeZone) {
|
|
16
|
+
return new Intl.DateTimeFormat('en-CA', {
|
|
17
|
+
timeZone: timeZone ?? undefined,
|
|
18
|
+
year: 'numeric',
|
|
19
|
+
month: '2-digit',
|
|
20
|
+
day: '2-digit',
|
|
21
|
+
}).format(date);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 认「间隔型」cron:分钟档 = `*/N * * * <dow>`;小时档 = `M */N * * <dow>`。
|
|
25
|
+
* 认不出返回 null(走普通 cron 路径)。
|
|
26
|
+
*/
|
|
27
|
+
function intervalSpecOf(task) {
|
|
28
|
+
const cron = task.schedule.cron;
|
|
29
|
+
if (cron === undefined)
|
|
30
|
+
return null;
|
|
31
|
+
const parts = cron.trim().split(/\s+/);
|
|
32
|
+
if (parts.length !== 5)
|
|
33
|
+
return null;
|
|
34
|
+
const [minute, hour, dom, mon, dow] = parts;
|
|
35
|
+
if (dom !== '*' || mon !== '*')
|
|
36
|
+
return null;
|
|
37
|
+
const mStep = /^\*\/(\d+)$/.exec(minute);
|
|
38
|
+
const hStep = /^\*\/(\d+)$/.exec(hour);
|
|
39
|
+
let unit;
|
|
40
|
+
let step;
|
|
41
|
+
if (mStep !== null && hour === '*') {
|
|
42
|
+
unit = 'minute';
|
|
43
|
+
step = Number(mStep[1]);
|
|
44
|
+
}
|
|
45
|
+
else if (hStep !== null && /^\d+$/.test(minute)) {
|
|
46
|
+
unit = 'hour';
|
|
47
|
+
step = Number(hStep[1]);
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
return null;
|
|
51
|
+
if (!Number.isFinite(step) || step <= 0)
|
|
52
|
+
return null;
|
|
53
|
+
if (dow === '*')
|
|
54
|
+
return { unit, step, dows: null };
|
|
55
|
+
const dows = dow.split(',').map(Number)
|
|
56
|
+
.filter(n => Number.isInteger(n) && n >= 0 && n <= 7)
|
|
57
|
+
.map(n => (n === 0 ? 7 : n));
|
|
58
|
+
return dows.length === 0 ? null : { unit, step, dows };
|
|
59
|
+
}
|
|
60
|
+
/** 某时刻在任务时区里的 ISO 星期(周一 = 1 … 周日 = 7)。 */
|
|
61
|
+
function isoWeekdayOf(date, timezone) {
|
|
62
|
+
const day = logicalDateOf(date, timezone);
|
|
63
|
+
const wd = new Date(`${day}T00:00:00Z`).getUTCDay();
|
|
64
|
+
return wd === 0 ? 7 : wd;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 间隔型刻度 = **锚点(任务开始时间)+ k×步长**(用户 2026-09-30 拍板):
|
|
68
|
+
* 「18:03 起每 10 分钟」就该是 18:03 / 18:13 / 18:23…,而不是 cron `*/10` 的**整点对齐**(:00/:10/…)。
|
|
69
|
+
* 锚点缺失(老数据 / 手写 JSON)⇒ 返回 null,退回 cron 行为(不猜)。
|
|
70
|
+
*/
|
|
71
|
+
function anchoredIntervalSlots(task, iv, from, to, cap) {
|
|
72
|
+
const startIso = task.schedule.start;
|
|
73
|
+
if (startIso === undefined || startIso === '')
|
|
74
|
+
return null;
|
|
75
|
+
const anchor = wallClockToAbsolute(startIso, task.schedule.timezone);
|
|
76
|
+
if (anchor === null)
|
|
77
|
+
return null;
|
|
78
|
+
const stepMs = (iv.unit === 'minute' ? iv.step : iv.step * 60) * 60_000;
|
|
79
|
+
const anchorMs = anchor.getTime();
|
|
80
|
+
// 直接跳到「第一个 ≥ from 的刻度」(O(1),不做线性扫描);锚点之前不产刻度。
|
|
81
|
+
let k = Math.max(0, Math.ceil((from.getTime() - anchorMs) / stepMs));
|
|
82
|
+
const out = [];
|
|
83
|
+
for (let i = 0; i < cap; i++, k++) {
|
|
84
|
+
const t = anchorMs + k * stepMs;
|
|
85
|
+
if (t >= to.getTime())
|
|
86
|
+
break;
|
|
87
|
+
const slot = new Date(t);
|
|
88
|
+
if (iv.dows !== null && !iv.dows.includes(isoWeekdayOf(slot, task.schedule.timezone)))
|
|
89
|
+
continue;
|
|
90
|
+
out.push(slot);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* cron 在 `[from, to)` 区间内的**全部刻度**(决策 25 的核心改动)。
|
|
96
|
+
*
|
|
97
|
+
* 旧实现 `scheduledAtFor` 按「天」只取第一个匹配 ⇒ **每小时 / 每几分钟的 cron 一天只能出
|
|
98
|
+
* 一条**,是功能缺陷。刻度**由 cron 表达式决定**,不是「上一次 + 间隔」⇒ 迟到 / 重启 /
|
|
99
|
+
* 多跑几轮都不会漂移(8:01 才跑,记的仍是 `08:00` 这个槽)。
|
|
100
|
+
*
|
|
101
|
+
* @param cap 迭代上限(防御 cron 表达成极小间隔导致死循环);超出即截断。
|
|
102
|
+
*/
|
|
103
|
+
export function scheduledSlotsFor(task, from, to, cap = 20_000) {
|
|
104
|
+
const cron = task.schedule.cron;
|
|
105
|
+
if (cron === undefined)
|
|
106
|
+
return []; // once 任务不走 cron(互斥校验保证恰有其一)
|
|
107
|
+
// 间隔型:按「锚点 + k×步长」生成(见 anchoredIntervalSlots)。锚点缺失才退回下面的 cron 对齐。
|
|
108
|
+
const iv = intervalSpecOf(task);
|
|
109
|
+
if (iv !== null) {
|
|
110
|
+
const anchored = anchoredIntervalSlots(task, iv, from, to, cap);
|
|
111
|
+
if (anchored !== null)
|
|
112
|
+
return filterSlotsBySchedule(task, anchored);
|
|
113
|
+
}
|
|
114
|
+
const interval = CronExpressionParser.parse(cron, {
|
|
115
|
+
// -1ms:保证恰好落在 from 上的刻度不会被漏掉(cron-parser 的 next() 是严格大于)。
|
|
116
|
+
currentDate: new Date(from.getTime() - 1),
|
|
117
|
+
tz: task.schedule.timezone,
|
|
118
|
+
});
|
|
119
|
+
const raw = [];
|
|
120
|
+
for (let i = 0; i < cap; i++) {
|
|
121
|
+
const next = interval.next().toDate();
|
|
122
|
+
if (next.getTime() >= to.getTime())
|
|
123
|
+
break;
|
|
124
|
+
if (next.getTime() >= from.getTime())
|
|
125
|
+
raw.push(next);
|
|
126
|
+
}
|
|
127
|
+
return filterSlotsBySchedule(task, raw);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 周期任务的锚点 / 步长过滤(「开始时间」+「每 N 周」):
|
|
131
|
+
* - `start`:首跑下界(不早于此时);也是「每 N 周」取模的参考周(start 所在周 = 第 0 周)。
|
|
132
|
+
* - `everyNWeeks`:每周档的重复步长。cron 没有「第几周」位 ⇒ 用「刻度与 start 的整周差 % N」
|
|
133
|
+
* 过滤,天然覆盖用户诉求:开始时间设下周一、每 2 周 ⇒ 下周一跑、下下周跳过、再下一周跑。
|
|
134
|
+
*/
|
|
135
|
+
export function filterSlotsBySchedule(task, slots) {
|
|
136
|
+
const startIso = task.schedule.start;
|
|
137
|
+
const startAbs = startIso === undefined ? undefined : wallClockToAbsolute(startIso, task.schedule.timezone);
|
|
138
|
+
const start = startAbs === null ? undefined : startAbs;
|
|
139
|
+
const nWeeks = task.schedule.everyNWeeks;
|
|
140
|
+
if (start === undefined && (nWeeks === undefined || nWeeks === null || nWeeks <= 1))
|
|
141
|
+
return slots;
|
|
142
|
+
const ref = start ?? slots[0];
|
|
143
|
+
if (ref === undefined)
|
|
144
|
+
return slots;
|
|
145
|
+
const WEEK = 7 * 24 * 3600 * 1000;
|
|
146
|
+
return slots.filter(slot => {
|
|
147
|
+
if (start !== undefined && slot.getTime() < start.getTime())
|
|
148
|
+
return false;
|
|
149
|
+
if (nWeeks !== undefined && nWeeks !== null && nWeeks > 1) {
|
|
150
|
+
const weeks = Math.floor((slot.getTime() - ref.getTime()) / WEEK);
|
|
151
|
+
if (((weeks % nWeeks) + nWeeks) % nWeeks !== 0)
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** "YYYY-MM-DDTHH:mm" 按某时区墙上时间 → 绝对时刻(与 once 同款解释;缺省 = 宿主本地时区)。 */
|
|
158
|
+
function wallClockToAbsolute(iso, tz) {
|
|
159
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(iso);
|
|
160
|
+
if (match === null)
|
|
161
|
+
return null;
|
|
162
|
+
const [, y, mo, d, h, mi] = match;
|
|
163
|
+
const wallAsUTC = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi));
|
|
164
|
+
if (tz === undefined)
|
|
165
|
+
return new Date(`${iso}:00`);
|
|
166
|
+
let t = new Date(wallAsUTC);
|
|
167
|
+
t = new Date(wallAsUTC - tzOffsetMs(t, tz));
|
|
168
|
+
t = new Date(wallAsUTC - tzOffsetMs(t, tz));
|
|
169
|
+
return t;
|
|
170
|
+
}
|
|
171
|
+
/** 给定时刻之后的下一个刻度(面板展示「下次执行」用)。 */
|
|
172
|
+
export function nextSlotAfter(task, from) {
|
|
173
|
+
if (task.schedule.once !== undefined)
|
|
174
|
+
return onceScheduledAt(task, task.schedule.once.slice(0, 10));
|
|
175
|
+
const slots = scheduledSlotsFor(task, from, new Date(from.getTime() + 366 * 24 * 3600_000), 2_000);
|
|
176
|
+
return slots[0];
|
|
177
|
+
}
|
|
178
|
+
/** 某时区在给定绝对时刻的 UTC 偏移毫秒(DST 敏感)。 */
|
|
179
|
+
function tzOffsetMs(date, timeZone) {
|
|
180
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
181
|
+
timeZone, hour12: false,
|
|
182
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
183
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
184
|
+
}).formatToParts(date);
|
|
185
|
+
const value = {};
|
|
186
|
+
for (const part of parts)
|
|
187
|
+
value[part.type] = part.value;
|
|
188
|
+
const asUTC = Date.UTC(Number(value.year), Number(value.month) - 1, Number(value.day), Number(value.hour === '24' ? '0' : value.hour), Number(value.minute), Number(value.second));
|
|
189
|
+
return asUTC - date.getTime();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 一次性任务的计划时刻(决策 18):`once` = "YYYY-MM-DDTHH:mm",
|
|
193
|
+
* 按 schedule.timezone 的墙上时间解释(缺省 = 宿主时区)。
|
|
194
|
+
* 仅当 once 的日期部分 == day 时返回该时刻,其余日期返回 undefined ——
|
|
195
|
+
* 实例身份 = task_id + logical_date,once 只有一个日期 ⇒ 只生成一条实例,
|
|
196
|
+
* 跑完(终态)后行已存在、日期已过 ⇒ 天然「跑完自动停」,次年不再触发。
|
|
197
|
+
*/
|
|
198
|
+
export function onceScheduledAt(task, day) {
|
|
199
|
+
const once = task.schedule.once;
|
|
200
|
+
if (once === undefined || once.slice(0, 10) !== day)
|
|
201
|
+
return undefined;
|
|
202
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(once);
|
|
203
|
+
if (match === null)
|
|
204
|
+
return undefined; // checkedTask 已保证格式,防御兜底
|
|
205
|
+
const [, y, mo, d, h, mi] = match;
|
|
206
|
+
const tz = task.schedule.timezone;
|
|
207
|
+
if (tz === undefined)
|
|
208
|
+
return new Date(`${once}:00`); // 无时区串 = 按宿主本地时区解释
|
|
209
|
+
// 目标 = 「tz 墙上时间等于 once」的绝对时刻:wall 当 UTC 减去 tz 偏移;
|
|
210
|
+
// 偏移依赖结果本身(DST 边界),迭代两次即收敛。
|
|
211
|
+
const wallAsUTC = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi));
|
|
212
|
+
let t = new Date(wallAsUTC);
|
|
213
|
+
t = new Date(wallAsUTC - tzOffsetMs(t, tz));
|
|
214
|
+
t = new Date(wallAsUTC - tzOffsetMs(t, tz));
|
|
215
|
+
return t;
|
|
216
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type AssetPaths } from './task-assets.js';
|
|
2
|
+
import type { HostContext, HostLogger } from './host.js';
|
|
3
|
+
import { type TaskDefinition } from './tasks.js';
|
|
4
|
+
import type { ResolvedDependency, TaskStore } from './store.js';
|
|
5
|
+
import type { Reconciler } from './reconcile.js';
|
|
6
|
+
import type { RuntimeIndex } from './runtime-index.js';
|
|
7
|
+
import type { PluginConfig } from './config.js';
|
|
8
|
+
/**
|
|
9
|
+
* 「立即执行」(手动触发一次调度,2026-10-03 用户拍板)结果。
|
|
10
|
+
* `ok: false` 时:`error` = 机器码(前端翻人话),`detail` = 可选参数(工作区名 / 附件名等)。
|
|
11
|
+
* 语义与正常调度**完全一致**——前置未达标 / 工作区缺失 / 附件缺失都**不建执行记录**,
|
|
12
|
+
* 只是额外把原因回给前端(用户手动触发看不到后台日志)。
|
|
13
|
+
*/
|
|
14
|
+
export type RunNowResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
instanceId: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
error: string;
|
|
20
|
+
detail?: string;
|
|
21
|
+
};
|
|
22
|
+
export interface Scheduler {
|
|
23
|
+
tick: () => void;
|
|
24
|
+
getTasks: () => Map<string, TaskDefinition>;
|
|
25
|
+
/**
|
|
26
|
+
* 立即执行(2026-10-03):按 id 取定义(**绕过 enabled**——用户拍板「不看开关」)、
|
|
27
|
+
* 逐项复用 Loop A 的预条件判定(串行互斥 → 前置 → 工作区 → 附件),全通过则写一条
|
|
28
|
+
* `dispatched` + `run_type='manual'` 的执行记录,等下一轮 Loop B 发动(=提前触发一次)。
|
|
29
|
+
*/
|
|
30
|
+
runNow: (taskId: string) => RunNowResult;
|
|
31
|
+
/** 启动诊断(决策 31):报告自上次起到现在的「错过刻度」计数(不补跑、只记日志)。 */
|
|
32
|
+
startupDiagnostics: () => void;
|
|
33
|
+
}
|
|
34
|
+
export type Judgement = 'ready' | 'blocked';
|
|
35
|
+
/** 依赖判定结果(决策 33):`staleNotes` = 复用旧产出的告警,只提示不拦;
|
|
36
|
+
* `resolved` = 放行时固化的上游实例解析(决策 43),阻塞为空数组。 */
|
|
37
|
+
export interface DependencyVerdict {
|
|
38
|
+
ready: boolean;
|
|
39
|
+
staleNotes: string[];
|
|
40
|
+
resolved: ResolvedDependency[];
|
|
41
|
+
/** 阻塞原因:放行时无;用于把日志分成 dep_blocked / dep_missing。
|
|
42
|
+
* 2026-10-06 用户拍板:上游**停用不再阻塞**——判定只看上游最近一次执行是否成功,
|
|
43
|
+
* 不看 enabled(上游跑完一次就被关停的「一次性上游」是正常用法)。 */
|
|
44
|
+
reason?: 'upstream-not-succeeded' | 'upstream-missing';
|
|
45
|
+
}
|
|
46
|
+
export declare function judgeDependencies(store: TaskStore, task: TaskDefinition, logicalDate: string, scheduledAt: string,
|
|
47
|
+
/**
|
|
48
|
+
* 上游任务定义表(可选):给了就能区分「上游还没成功 / 上游已删除」两种阻塞,
|
|
49
|
+
* 日志里不再混成一句 dep_blocked(2026-09-30 评审 P4;2026-10-06 起**不看 enabled**)。
|
|
50
|
+
*/
|
|
51
|
+
upstreams?: ReadonlyMap<string, TaskDefinition>): DependencyVerdict;
|
|
52
|
+
/**
|
|
53
|
+
* **一次性任务的「过期未执行」记录**(用户 2026-09-30 拍板 A 的配套)。
|
|
54
|
+
*
|
|
55
|
+
* 一次性任务加了窗口闸门(`dueSlot`)之后「出窗口就再也不跑」;若那一槽从来没跑过(典型:那一刻它
|
|
56
|
+
* 被上游堵着 / 附件缺失,一直拖到窗口过完),它会**无声无息地消失** —— 面板显示「没有下次了」、
|
|
57
|
+
* 执行记录里一条都没有。这里补一条终态 `skipped`(`attempt=0` ⇒ 天然不重试),把「该跑没跑、而且
|
|
58
|
+
* 再也不会跑」**留在用户看得见的地方**:卡片标红 + 执行记录一条 + 原因挂在该行事件上。
|
|
59
|
+
*
|
|
60
|
+
* 门禁与「补记前一个槽」**完全一致**(`gateMs` = 本进程启动 / 任务创建 的较晚者):**停机期间**跨过
|
|
61
|
+
* 那一刻的不补(用户口径:服务没跑的那段时间不用管),否则用户新建一个「上一刻已过期」的一次性任务
|
|
62
|
+
* 会被凭空标红。返回「本槽是否按过期处理」(调用方据此决定要不要清掉悬浮原因)。
|
|
63
|
+
*/
|
|
64
|
+
export declare function recordExpiredOnce(store: TaskStore, runtime: RuntimeIndex | null, task: TaskDefinition, nowMs: number, gateMs: number): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* 附加文件存在性校验(2026-09-30):返回**缺失**的展示名。
|
|
67
|
+
* upload 型按任务目录绝对路径;link 型按**附件来源工作区**(item.workspace)解析——
|
|
68
|
+
* 附件可以选自任意工作区,拿任务目标工作区的 path 去判会误报(评审 P1#8)。
|
|
69
|
+
* 拿不到基准(资产根未就绪 / 来源工作区不存在)⇒ 跳过不判,**绝不猜**。
|
|
70
|
+
*/
|
|
71
|
+
export declare function missingAttachments(ctx: HostContext, task: TaskDefinition, workspacePath: string, assets: AssetPaths | null): string[];
|
|
72
|
+
export declare function createScheduler(opts: {
|
|
73
|
+
ctx: HostContext;
|
|
74
|
+
logger: HostLogger;
|
|
75
|
+
store: TaskStore;
|
|
76
|
+
reconciler: Reconciler;
|
|
77
|
+
config: () => PluginConfig;
|
|
78
|
+
/** 任务文件资产根(附加文件存在性校验用);未就绪传 null ⇒ 跳过 upload 型校验。 */
|
|
79
|
+
assets?: () => AssetPaths | null;
|
|
80
|
+
/** 主界面运行态内存索引(2026-09-30);未装配则跳过(不影响调度)。 */
|
|
81
|
+
runtime?: RuntimeIndex;
|
|
82
|
+
}): Scheduler;
|