dsh-task-dispatch-table 0.1.11 → 0.1.12

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.
@@ -10,10 +10,12 @@ export declare class DispatchPreconditionError extends Error {
10
10
  constructor(reason: string, message: string);
11
11
  }
12
12
  /**
13
- * 工作区名 → 工作区实体(决策 22:target.workspace 语义是**工作区**,不是工作目录;
14
- * 目录由实体 path 派生)。registry 无按 name 查询 API,遍历 list() 比对:
15
- * title 精确匹配优先,id 兜底。匹配不到即抛错 ⇒ 任务判失败,绝不落到「未分组」或随便找个目录跑。
16
- * (Loop A 落库前的前置检查用;Loop B 发动走 resolveWorkspaceByPath——快照里存的是 path。)
13
+ * 工作区标识 → 工作区实体(决策 22:target.workspace 语义是**工作区**,不是工作目录;
14
+ * 目录由实体 path 派生)。**新任务存 id**(宿主随机 UUID,改名不变);老任务存 title,
15
+ * 所以先按 title 精确匹配、再按 id 兜底(用户 2026-11-06 向下兼容)。
16
+ * registry 无按 name 查询 API,只能遍历 list()。匹配不到即抛错 ⇒ 任务判失败,
17
+ * 绝不落到「未分组」或随便找个目录跑。(Loop A 落库前的前置检查用;Loop B 发动走
18
+ * resolveWorkspaceByPath——快照里存的是 path。)
17
19
  */
18
20
  export declare function resolveWorkspace(ctx: HostContext, name: string): HostWorkspace;
19
21
  /**
package/dist/dispatch.js CHANGED
@@ -20,10 +20,12 @@ export class DispatchPreconditionError extends Error {
20
20
  }
21
21
  }
22
22
  /**
23
- * 工作区名 → 工作区实体(决策 22:target.workspace 语义是**工作区**,不是工作目录;
24
- * 目录由实体 path 派生)。registry 无按 name 查询 API,遍历 list() 比对:
25
- * title 精确匹配优先,id 兜底。匹配不到即抛错 ⇒ 任务判失败,绝不落到「未分组」或随便找个目录跑。
26
- * (Loop A 落库前的前置检查用;Loop B 发动走 resolveWorkspaceByPath——快照里存的是 path。)
23
+ * 工作区标识 → 工作区实体(决策 22:target.workspace 语义是**工作区**,不是工作目录;
24
+ * 目录由实体 path 派生)。**新任务存 id**(宿主随机 UUID,改名不变);老任务存 title,
25
+ * 所以先按 title 精确匹配、再按 id 兜底(用户 2026-11-06 向下兼容)。
26
+ * registry 无按 name 查询 API,只能遍历 list()。匹配不到即抛错 ⇒ 任务判失败,
27
+ * 绝不落到「未分组」或随便找个目录跑。(Loop A 落库前的前置检查用;Loop B 发动走
28
+ * resolveWorkspaceByPath——快照里存的是 path。)
27
29
  */
28
30
  export function resolveWorkspace(ctx, name) {
29
31
  const workspaces = ctx.workspaceRegistry.list();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 核心 `adv/explorer` 的文件系统 HTTP 路由(宿主注册 / 客户端连接共用一条常量)。
3
+ *
4
+ * 核心 `FileBrowser` 只吃 `FsFace`(绝对路径);宿主侧由 `serveFsRoute` 提供该路由,
5
+ * 客户端由 `connectFs({ path })` 建 HTTP face。路径与插件路由同前缀,避免与其他插件撞车。
6
+ */
7
+ export declare const FS_ROUTE = "/api/task-dispatch-table/fs";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 核心 `adv/explorer` 的文件系统 HTTP 路由(宿主注册 / 客户端连接共用一条常量)。
3
+ *
4
+ * 核心 `FileBrowser` 只吃 `FsFace`(绝对路径);宿主侧由 `serveFsRoute` 提供该路由,
5
+ * 客户端由 `connectFs({ path })` 建 HTTP face。路径与插件路由同前缀,避免与其他插件撞车。
6
+ */
7
+ export const FS_ROUTE = '/api/task-dispatch-table/fs';
package/dist/index.js CHANGED
Binary file
@@ -37,6 +37,7 @@ export interface TaskOverviewRow {
37
37
  timezone: string | null;
38
38
  start: string | null;
39
39
  everyNWeeks: number | null;
40
+ everyNDays: number | null;
40
41
  window: string;
41
42
  /**
42
43
  * 结构化排期(新建 / 编辑时双写的 `schedule.ui`,2026-09-30 用户拍板):
@@ -9,6 +9,7 @@ function scheduleKeyOf(task) {
9
9
  s.timezone ?? '',
10
10
  s.start ?? '',
11
11
  s.everyNWeeks === undefined ? '' : String(s.everyNWeeks),
12
+ s.everyNDays === undefined ? '' : String(s.everyNDays),
12
13
  // ⚠️ `window`(允许延迟)**必须在这里**(2026-10-07 审计 🟡):它是「该冻还是该前移」与候选槽
13
14
  // 搜索的核心输入,改了它 nextSlotAt 就得重算。原先漏了它 ⇒ `overviewKey` 会 bump(界面拿到新行)
14
15
  // 但 `nextSlotAt` 仍按**旧 window** 算出来 ⇒ 卡片「下次执行」与调度器实际判定分叉,直到刻度过期才自愈。
@@ -35,7 +36,7 @@ function overviewKeyOf(task) {
35
36
  provider: task.target.provider ?? '',
36
37
  model: task.target.model ?? '',
37
38
  prompt: task.target.prompt,
38
- schedule: { cron: s.cron ?? '', once: s.once ?? '', timezone: s.timezone ?? '', start: s.start ?? '', everyNWeeks: s.everyNWeeks ?? 0, window: s.window, ui: s.ui ?? null },
39
+ schedule: { cron: s.cron ?? '', once: s.once ?? '', timezone: s.timezone ?? '', start: s.start ?? '', everyNWeeks: s.everyNWeeks ?? 0, everyNDays: s.everyNDays ?? 0, window: s.window, ui: s.ui ?? null },
39
40
  retry: task.retry?.maxAttempts ?? 1,
40
41
  attachments: (task.attachments ?? []).map(a => `${a.name}:${a.kind}:${a.ref}`),
41
42
  depends: (task.depends_on ?? []).map(d => d.task),
@@ -298,6 +299,7 @@ export function createRuntimeIndex() {
298
299
  timezone: task.schedule.timezone ?? null,
299
300
  start: task.schedule.start ?? null,
300
301
  everyNWeeks: task.schedule.everyNWeeks ?? null,
302
+ everyNDays: task.schedule.everyNDays ?? null,
301
303
  // 防御取值:overview 是展示面,畸形定义也不能让它崩(zod 有默认值的字段仍按可选读)。
302
304
  window: task.schedule.window ?? 'PT0S',
303
305
  ui: task.schedule.ui ?? null,
@@ -8,6 +8,7 @@ export interface ScheduleNextSchedule {
8
8
  once?: string;
9
9
  start?: string;
10
10
  everyNWeeks?: number | null;
11
+ everyNDays?: number | null;
11
12
  }
12
13
  /** 最小任务形状:只要求带 `schedule`。 */
13
14
  export interface ScheduleNextTask {
@@ -26,10 +27,12 @@ export declare function logicalDateOf(date: Date, timeZone: string | undefined):
26
27
  */
27
28
  export declare function scheduledSlotsFor(task: ScheduleNextTask, from: Date, to: Date, cap?: number): Date[];
28
29
  /**
29
- * 周期任务的锚点 / 步长过滤(「开始时间」+「每 N 周」):
30
- * - `start`:首跑下界(不早于此时);也是「每 N 周」取模的参考周(start 所在周 = 第 0 周)。
30
+ * 周期任务的锚点 / 步长过滤(「开始时间」+「每 N 周」/「每 N 天」):
31
+ * - `start`:首跑下界(不早于此时);也是步长取模的参考(start 所在周 / 日 = 第 0 个)。
31
32
  * - `everyNWeeks`:每周档的重复步长。cron 没有「第几周」位 ⇒ 用「刻度与 start 的整周差 % N」
32
33
  * 过滤,天然覆盖用户诉求:开始时间设下周一、每 2 周 ⇒ 下周一跑、下下周跳过、再下一周跑。
34
+ * - `everyNDays`:每天档的重复步长。cron 的日位 `*/N` 是「每月第 N 天」(月底会重数)⇒ 同样取模,
35
+ * 但按**日历日**差算(DST 那天不是 24h,用毫秒会把相位推歪)。
33
36
  */
34
37
  export declare function filterSlotsBySchedule(task: ScheduleNextTask, slots: Date[]): Date[];
35
38
  /** 给定时刻之后的下一个刻度(面板展示「下次执行」用)。 */
@@ -127,30 +127,38 @@ export function scheduledSlotsFor(task, from, to, cap = 20_000) {
127
127
  return filterSlotsBySchedule(task, raw);
128
128
  }
129
129
  /**
130
- * 周期任务的锚点 / 步长过滤(「开始时间」+「每 N 周」):
131
- * - `start`:首跑下界(不早于此时);也是「每 N 周」取模的参考周(start 所在周 = 第 0 周)。
130
+ * 周期任务的锚点 / 步长过滤(「开始时间」+「每 N 周」/「每 N 天」):
131
+ * - `start`:首跑下界(不早于此时);也是步长取模的参考(start 所在周 / 日 = 第 0 个)。
132
132
  * - `everyNWeeks`:每周档的重复步长。cron 没有「第几周」位 ⇒ 用「刻度与 start 的整周差 % N」
133
133
  * 过滤,天然覆盖用户诉求:开始时间设下周一、每 2 周 ⇒ 下周一跑、下下周跳过、再下一周跑。
134
+ * - `everyNDays`:每天档的重复步长。cron 的日位 `*/N` 是「每月第 N 天」(月底会重数)⇒ 同样取模,
135
+ * 但按**日历日**差算(DST 那天不是 24h,用毫秒会把相位推歪)。
134
136
  */
135
137
  export function filterSlotsBySchedule(task, slots) {
136
138
  const startIso = task.schedule.start;
137
139
  const startAbs = startIso === undefined ? undefined : wallClockToAbsolute(startIso, task.schedule.timezone);
138
140
  const start = startAbs === null ? undefined : startAbs;
139
- const nWeeks = task.schedule.everyNWeeks;
140
- if (start === undefined && (nWeeks === undefined || nWeeks === null || nWeeks <= 1))
141
+ const nWeeks = task.schedule.everyNWeeks ?? 1;
142
+ const nDays = task.schedule.everyNDays ?? 1;
143
+ const weekly = nWeeks > 1;
144
+ // 无锚点(老数据 / 手写 JSON)⇒ 不算相位,按「每天」跑(不猜)。
145
+ const daily = start !== undefined && nDays > 1;
146
+ if (start === undefined && !weekly)
141
147
  return slots;
142
148
  const ref = start ?? slots[0];
143
149
  if (ref === undefined)
144
150
  return slots;
145
151
  const WEEK = 7 * 24 * 3600 * 1000;
152
+ const dayIndex = (date) => Date.parse(`${logicalDateOf(date, task.schedule.timezone)}T00:00:00Z`) / 86_400_000;
153
+ const refDay = dayIndex(ref);
154
+ const mod = (value, n) => ((value % n) + n) % n;
146
155
  return slots.filter(slot => {
147
156
  if (start !== undefined && slot.getTime() < start.getTime())
148
157
  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
- }
158
+ if (weekly && mod(Math.floor((slot.getTime() - ref.getTime()) / WEEK), nWeeks) !== 0)
159
+ return false;
160
+ if (daily && mod(dayIndex(slot) - refDay, nDays) !== 0)
161
+ return false;
154
162
  return true;
155
163
  });
156
164
  }
@@ -79,4 +79,9 @@ export declare function createScheduler(opts: {
79
79
  assets?: () => AssetPaths | null;
80
80
  /** 主界面运行态内存索引(2026-09-30);未装配则跳过(不影响调度)。 */
81
81
  runtime?: RuntimeIndex;
82
+ /**
83
+ * 老任务存的工作区是**标题**(用户 2026-11-06):按标题解析成功后就地改存 id 并落盘。
84
+ * 未传 ⇒ 只解析不改写(目录模式 / 测试)。
85
+ */
86
+ healWorkspaceId?: (taskId: string, workspaceId: string) => void;
82
87
  }): Scheduler;
package/dist/scheduler.js CHANGED
@@ -367,7 +367,9 @@ function dispatchNewSlots(ctx, logger, store, tasks, verdictLog, upstreams, asse
367
367
  /** 主界面运行态内存索引(2026-09-30):落库即顺手标记「运行中」,不额外查库。 */
368
368
  runtime,
369
369
  /** 本进程启动时刻(决策 54):补记「未执行」只补**启动之后**的槽 ⇒ 停机期间漏的不补。 */
370
- startedAtMs) {
370
+ startedAtMs,
371
+ /** 老任务存工作区标题:按标题解析成功后把 id 写回定义(用户 2026-11-06 向下兼容自愈)。 */
372
+ healWorkspaceId) {
371
373
  const nowMs = Date.now();
372
374
  for (const task of tasks) {
373
375
  // 停用 ⇒ **顺手清掉「被挡住」的原因**(2026-09-30 复核 P1):否则一张**已关掉**的卡片悬停时
@@ -469,6 +471,11 @@ startedAtMs) {
469
471
  runtime?.markBlocked(task.id, workspaceMissing);
470
472
  continue;
471
473
  }
474
+ // 老任务存的是标题 ⇒ 解析成功后**就地改存 id**(用户 2026-11-06:改名后才不会再失联)。
475
+ if (task.target.workspace !== workspace.id) {
476
+ healWorkspaceId?.(task.id, workspace.id);
477
+ task.target.workspace = workspace.id;
478
+ }
472
479
  // 附加文件校验(2026-09-30):缺失 ⇒ **不建行、不执行**,只记 error。
473
480
  // 与「预条件不过不建行」同源;且不判 failed——重试也不会自己长出来,白耗重试额度。
474
481
  // 日志走「结论变化才记」的同一张签名表,缺着不修就不会每 tick 刷一条。
@@ -506,7 +513,7 @@ startedAtMs) {
506
513
  }
507
514
  }
508
515
  export function createScheduler(opts) {
509
- const { ctx, logger, store, reconciler, config, assets, runtime } = opts;
516
+ const { ctx, logger, store, reconciler, config, assets, runtime, healWorkspaceId } = opts;
510
517
  const verdictLog = new Map();
511
518
  let taskMap = new Map();
512
519
  /** 进程启动时刻(决策 54):补记「未执行」的门禁 —— 停机期间漏掉的槽**不补**。 */
@@ -524,7 +531,7 @@ export function createScheduler(opts) {
524
531
  // 故这里把全量写进 taskMap,让前端前置列表能选到停用任务。
525
532
  taskMap = new Map(allTasks.map((t) => [t.id, t]));
526
533
  // Loop A:先处理新刻度(懒建行 + 不回看 + 不补跑 + 落库即止,决策 41)
527
- dispatchNewSlots(ctx, logger, store, tasks, verdictLog, upstreams, assets === undefined ? null : assets(), runtime ?? null, startedAtMs);
534
+ dispatchNewSlots(ctx, logger, store, tasks, verdictLog, upstreams, assets === undefined ? null : assets(), runtime ?? null, startedAtMs, healWorkspaceId);
528
535
  // Loop B:再收口全部执行记录(发动本 tick 新落库的行 + 追问 / 重试 / 租约——
529
536
  // 只读执行记录 + 快照,决策 41)。放在 Loop A 之后 = 新行当 tick 即被发动,时延不退化。
530
537
  reconciler.sweep();
@@ -574,6 +581,11 @@ export function createScheduler(opts) {
574
581
  catch {
575
582
  return { ok: false, error: 'workspace-missing', detail: task.target.workspace };
576
583
  }
584
+ // 与 Loop A 同口径:老任务存标题 ⇒ 解析成功后就地改存 id(用户 2026-11-06)。
585
+ if (task.target.workspace !== workspace.id) {
586
+ healWorkspaceId?.(task.id, workspace.id);
587
+ task.target.workspace = workspace.id;
588
+ }
577
589
  const missing = missingAttachments(ctx, task, workspace.path, assets === undefined ? null : assets());
578
590
  if (missing.length > 0)
579
591
  return { ok: false, error: 'attachment-missing', detail: missing.join('、') };
package/dist/tasks.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare const taskDefinitionSchema: z.ZodObject<{
23
23
  once: z.ZodOptional<z.ZodString>;
24
24
  start: z.ZodOptional<z.ZodString>;
25
25
  everyNWeeks: z.ZodOptional<z.ZodNumber>;
26
+ everyNDays: z.ZodOptional<z.ZodNumber>;
26
27
  ui: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
27
28
  }, z.core.$strip>;
28
29
  target: z.ZodObject<{
@@ -132,7 +133,7 @@ export interface SaveValidation {
132
133
  * ④ 前置任务必须指向已存在的任务(**停用可以、不存在不行**)。
133
134
  * `title` / `code` **不做**格式与唯一性校验(决策 30:它们只是人读字段)。
134
135
  */
135
- export declare function validateDefinitionForSave(def: TaskDefinitionInput, existingIds: ReadonlySet<string>): SaveValidation;
136
+ export declare function validateDefinitionForSave(def: TaskDefinitionInput, existingIds: ReadonlySet<string>, knownWorkspaces?: ReadonlySet<string>): SaveValidation;
136
137
  export interface UpsertResult {
137
138
  json: string;
138
139
  mode: 'create' | 'update';
@@ -167,6 +168,17 @@ export interface SetEnabledResult {
167
168
  * 定义不存在 ⇒ error(404 语义);值没变 ⇒ changed=false(不落库不审计)。
168
169
  */
169
170
  export declare function setEnabledDefinitionInline(raw: string, id: string, enabled: boolean): SetEnabledResult;
171
+ export interface SetWorkspaceResult {
172
+ json: string;
173
+ changed: boolean;
174
+ error: string | null;
175
+ }
176
+ /**
177
+ * 把一条任务的 `target.workspace` 从「工作区标题」改存为「工作区 id」(用户 2026-11-06:
178
+ * 向下兼容自愈 —— 老任务存标题,执行时按标题解析成功后就地写成 id,下次改名不再失联)。
179
+ * 定义不存在 / 表非法 / 值没变 ⇒ 原样返回(不落库)。
180
+ */
181
+ export declare function setWorkspaceDefinitionInline(raw: string, id: string, workspace: string): SetWorkspaceResult;
170
182
  /** 把 ISO 8601 时长解析成毫秒。 */
171
183
  export declare function durationMs(iso: string): number;
172
184
  /** 用 Intl 验证 IANA 时区名(schedule.timezone 缺省 = 宿主时区)。 */
package/dist/tasks.js CHANGED
@@ -10,7 +10,7 @@ import { isSafeAttachmentRef } from './attachment-allowlist.js';
10
10
  // 「cron / once → 时刻」纯核已于 2026-10-05 抽出至 [`./schedule-next.ts`](./schedule-next.ts)
11
11
  //(零 node 依赖,客户端 bundle 与服务端**同一份**)。这里 import 供内部使用(firstSlotOnDay 等),
12
12
  // 并 re-export 保持既有调用面(runtime-index / scheduler / index)零改动。
13
- import { filterSlotsBySchedule, logicalDateOf, nextSlotAfter, onceScheduledAt, scheduledSlotsFor, } from './schedule-next.js';
13
+ import { logicalDateOf, onceScheduledAt, scheduledSlotsFor, } from './schedule-next.js';
14
14
  export { filterSlotsBySchedule, logicalDateOf, nextSlotAfter, onceScheduledAt, scheduledSlotsFor, } from './schedule-next.js';
15
15
  /** ISO 8601 时长(如 PT4H),只支持 H/M/S 组合——窗口与新鲜度够用。 */
16
16
  const isoDuration = z.string().regex(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/, 'ISO 8601 时长,如 PT4H');
@@ -44,6 +44,8 @@ export const taskDefinitionSchema = z.object({
44
44
  start: z.string().optional(),
45
45
  /** 每周档的重复步长(周):1 = 每周;≥2 = 每 N 周。cron 无隔周位 ⇒ 用锚点 + 取模实现(决策)。 */
46
46
  everyNWeeks: z.number().int().min(1).optional(),
47
+ /** 每天档的重复步长(天):1 = 每天;≥2 = 每 N 天。cron 的日位 `*&#47;N` 是「每月第 N 天」(月底会重数)⇒ 同「每 N 周」用锚点 + 日历日差取模。 */
48
+ everyNDays: z.number().int().min(1).optional(),
47
49
  /**
48
50
  * 结构化排期的**编辑态快照**(2026-09-30 双写,data-model §5.4):表单控件原样留档,
49
51
  * 供「编辑现有任务」反解。**执行永不读它**——排期真源恒为 `cron` / `once`。
@@ -51,7 +53,8 @@ export const taskDefinitionSchema = z.object({
51
53
  ui: z.record(z.string(), z.unknown()).optional(),
52
54
  }),
53
55
  target: z.object({
54
- // 工作区(非工作目录,决策 22):按 registry 的 title 精确匹配、id 兜底;目录由工作区 path 派生。
56
+ // 工作区(非工作目录,决策 22):存 workspace id(宿主创建时的随机 UUID,重命名不变);
57
+ // 老任务存的是 title,执行 / 保存时按标题解析成功后**就地改存 id**(用户 2026-11-06 向下兼容)。
55
58
  workspace: z.string().min(1),
56
59
  /** 派发模型(决策 22 漏斗第①层):provider 与 model 成对;都留空则漏到插件配置 / 宿主默认。 */
57
60
  provider: z.string().optional(),
@@ -236,13 +239,19 @@ export function applyIdentity(def) {
236
239
  * ④ 前置任务必须指向已存在的任务(**停用可以、不存在不行**)。
237
240
  * `title` / `code` **不做**格式与唯一性校验(决策 30:它们只是人读字段)。
238
241
  */
239
- export function validateDefinitionForSave(def, existingIds) {
242
+ export function validateDefinitionForSave(def, existingIds, knownWorkspaces) {
240
243
  const prompt = typeof def.target?.prompt === 'string' ? def.target.prompt.trim() : '';
241
244
  if (prompt === '')
242
245
  return { ok: false, error: '提示词不能为空' };
243
246
  const workspace = typeof def.target?.workspace === 'string' ? def.target.workspace.trim() : '';
244
247
  if (workspace === '')
245
248
  return { ok: false, error: '工作区不能为空' };
249
+ // 工作区必须**现在真实存在**(按 id 或 title 任一命中):只判非空会让「工作区被改名 / 删除后
250
+ // 表单里留着的旧值」照样保存成功,等到执行时才报「工作区未找到」。registry 未就绪(降级)时
251
+ // 不传该集合 = 跳过(不能拿「拿不到列表」当成「工作区不存在」)。
252
+ if (knownWorkspaces !== undefined && !knownWorkspaces.has(workspace)) {
253
+ return { ok: false, error: `工作区不存在或已被重命名 / 删除:「${workspace}」,请重新选择工作区` };
254
+ }
246
255
  const cron = typeof def.schedule?.cron === 'string' && def.schedule.cron.trim() !== '' ? def.schedule.cron.trim() : undefined;
247
256
  const once = typeof def.schedule?.once === 'string' && def.schedule.once.trim() !== '' ? def.schedule.once.trim() : undefined;
248
257
  if (cron === undefined && once === undefined)
@@ -372,6 +381,38 @@ export function setEnabledDefinitionInline(raw, id, enabled) {
372
381
  row.enabled = enabled;
373
382
  return { json: JSON.stringify(rows, null, 2), changed: true, error: null };
374
383
  }
384
+ /**
385
+ * 把一条任务的 `target.workspace` 从「工作区标题」改存为「工作区 id」(用户 2026-11-06:
386
+ * 向下兼容自愈 —— 老任务存标题,执行时按标题解析成功后就地写成 id,下次改名不再失联)。
387
+ * 定义不存在 / 表非法 / 值没变 ⇒ 原样返回(不落库)。
388
+ */
389
+ export function setWorkspaceDefinitionInline(raw, id, workspace) {
390
+ const text = raw.trim();
391
+ let data;
392
+ if (text === '')
393
+ return { json: raw, changed: false, error: 'task-not-found' };
394
+ try {
395
+ data = JSON.parse(text);
396
+ }
397
+ catch {
398
+ return { json: raw, changed: false, error: '现有任务表不是合法 JSON,无法修改' };
399
+ }
400
+ if (!Array.isArray(data))
401
+ return { json: raw, changed: false, error: '现有任务表不是数组,无法修改' };
402
+ const rows = data;
403
+ const row = rows.find(item => item !== null && typeof item === 'object' && item.id === id);
404
+ if (row === undefined)
405
+ return { json: raw, changed: false, error: 'task-not-found' };
406
+ const target = row.target;
407
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) {
408
+ return { json: raw, changed: false, error: '任务缺少 target,无法修改工作区' };
409
+ }
410
+ const targetObj = target;
411
+ if (targetObj.workspace === workspace)
412
+ return { json: raw, changed: false, error: null };
413
+ targetObj.workspace = workspace;
414
+ return { json: JSON.stringify(rows, null, 2), changed: true, error: null };
415
+ }
375
416
  /** 把 ISO 8601 时长解析成毫秒。 */
376
417
  export function durationMs(iso) {
377
418
  const match = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/.exec(iso);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-task-dispatch-table",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Scheduled task dispatcher: a dsh host plugin that drives recurring agent tasks from a single task table, dispatching each run as an independent dsh session (scheduling itself uses zero model calls)",
5
5
  "icon": "./assets/icon-scheduler.svg",
6
6
  "keywords": [
@@ -34,12 +34,13 @@
34
34
  "LICENSE"
35
35
  ],
36
36
  "scripts": {
37
- "build": "npm run build:host && npm run build:client",
37
+ "build": "pnpm run build:host && pnpm run build:client",
38
38
  "build:host": "tsc",
39
39
  "build:client": "tsc --noEmit -p tsconfig.client.json && tsdown --config tsdown.client.config.ts",
40
40
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.client.json",
41
41
  "smoke": "node scripts/smoke.mjs"
42
42
  },
43
+ "packageManager": "pnpm@12.3.4",
43
44
  "engines": {
44
45
  "node": ">=22.5"
45
46
  },
@@ -59,6 +60,7 @@
59
60
  "@lezer/highlight": "^1.2.3",
60
61
  "@uiw/react-codemirror": "^4.25.12",
61
62
  "cron-parser": "^5.10.1",
63
+ "dsh-plugin-core": "^0.35.8",
62
64
  "zod": "^4.6.5"
63
65
  },
64
66
  "devDependencies": {