@zhushanwen/pi-scheduler

定时任务调度扩展:按 duration(`5m` / `2h` / `1d`)间隔或 cron 表达式,在指定时间向 agent 注入消息。支持一次性提醒(once)与过期策略(expires)。创建入口两路:**人侧 `/schedule` 命令打开创建表单**(表单异步打开,填表时长不受命令通道超时约束),**模型侧 `schedule` tool 直建**(不再弹确认表单;参数不完整时必须先向用户澄清——见「创建方式」)。任务随 owner session 持久化,resume 后继续触发。

Packages

Package details

extensionskill

Install @zhushanwen/pi-scheduler from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@zhushanwen/pi-scheduler
Package
@zhushanwen/pi-scheduler
Version
0.13.1
Published
Nov 3, 2026
Downloads
2,070/mo · 715/wk
Author
zhushanwen321
License
unknown
Types
extension, skill
Size
450.3 KB
Dependencies
6 dependencies · 4 peers
Pi manifest JSON
{
  "skills": [
    "./skills"
  ],
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

scheduler

定时任务调度扩展:按 duration(5m / 2h / 1d)间隔或 cron 表达式,在指定时间向 agent 注入消息。支持一次性提醒(once)与过期策略(expires)。创建入口两路:人侧 /schedule 命令打开创建表单(表单异步打开,填表时长不受命令通道超时约束),模型侧 schedule tool 直建(不再弹确认表单;参数不完整时必须先向用户澄清——见「创建方式」)。任务随 owner session 持久化,resume 后继续触发。

产品定位

pi-scheduler 是 session 存活期间的 AI 提醒器——在 pi 进程运行、session 打开时,按计划向当前 session 注入消息。

非系统级 cron:

  • pi 进程不开 = 不触发(与常驻后台的系统 cron daemon 不同,本扩展不是后台守护进程)
  • 电脑睡眠 / 关机 = 不触发
  • 不依赖系统 crontab,不注册任何开机自启

任务归属创建它的 session:任务物理存储为独立 JSON 快照分片(按 owner sessionId 分片,见「数据存储位置」),只在 owner session 打开(继续对话 / resume)时才触发。

这是设计决策,不是 bug(D9):本扩展定位是 session 级 AI 提醒器——任务归属创建它的 session,只在 owner session 存活时触发。如果你每天开新 session,昨天建的"明早检查 CI"任务今天不会响,除非你 resume 昨天创建该任务的那个 session。这不是"任务丢了":任务随 session 持久化,session 不打开就不调度。用户若发现「昨天建的任务今天没响」,这是预期行为,请 resume 创建该任务的 session。

简介与安装

pi-scheduler 是 taiji 的 mandatory 扩展(packages/shared/src/mandatory-extensions.json,tier: feature)——taiji 启动时自动安装并启用,无需手动操作。

独立 pi 环境手动安装:

npm install @zhushanwen/pi-scheduler

安装后扩展在 pi 会话启动时自动装配(见下节),无需额外配置。

工具激活

schedule / schedule_control 两工具均为 direct 常驻——注册不声明 exposure 字段(pi SDK 缺省 direct),工具声明随会话常驻、对模型默认可见,无需经 tool_search 检索激活,独立 pi 开箱即用。

激活方式

扩展 factory(src/index.ts)监听 pi 的 session 生命周期事件,在 session_start 时装配完整调用链:

session_start
  ├─ 推导任务库分片路径:schedulerStoreFilePath(getAgentDir(), ctx.cwd, shardKey)
  │    (shardKey = sessionId;--no-session 等空/非法值归一为 'no-session')
  ├─ PiSchedulerBackend(投递端口 dispatch + 快照文件读写 + 时间源)
  │    └─ SchedulerRuntime(内存态 + 30s tick 调度 + 限流)
  │         └─ SchedulerService(tool/command 唯一业务入口)
  ├─ runtime.loadTasks(backend.loadTasks())  ← 一次 JSON.parse 直读快照分片重建任务集
  │                                             (分片不存在 = 空任务集,正常路径;
  │                                               fork 新 sessionId → 新分片,天然不继承)
  ├─ runtime.startScheduler()                  ← 启动 30s tick
  └─ 注册 scheduler widget(每个 tick 后刷新)

运行时每次任务集变更(创建 / 更新 / 推进 / 启停 / 删除)都把内存任务集全量快照原子落盘到该 session 的分片文件(persistTasks:tmp + fsync + rename 原子替换,见「数据存储位置」)。会话 JSONL 只承载 dispatch 注入消息与用户对话,不承载任务数据(任务库与会话事实分离,2026-10-30 裁决)。

/schedule 命令用法

注册为 /schedule。无参数 → 打开创建表单(空草稿,默认 循环 + 每天 09:00);带参 <schedule> <prompt> → 打开预填表单;第一个参数匹配子命令关键词则走子命令分支。命令 handler 异步打开表单后立即返回——用户在表单里停留多久都不会触发命令通道的 60s 超时(会话级提醒器不该因为「用户想了两分钟」而报错)。

子命令

子命令 行为
/schedule 打开创建表单(空草稿;默认循环 + 每天 09:00)
/schedule <schedule> <prompt> 打开预填时间/提示词的创建表单(如 /schedule 5m 'check build')
/schedule list 列出所有任务(id、名称、调度、下次执行时间)
/schedule on <id> 启用任务
/schedule off <id> 停用任务(推荐临时暂停用 off,不用 rm)
/schedule rm <id> 删除任务
/schedule run <id> 立即执行任务

一次性(once)与 cron 的形态由表单内选择/填写(执行模式切换 + 自定义 cron 输入),不再有 once / cron 关键字子命令。任务 id 由 8 位 hex 自动生成,list 后从输出中获取。

引号转义

参数用 shell 风格引号解析(tokenizeQuoted,src/commands.ts):

  • 单引号 '...' 或双引号 "..." 内的内容作为一个 token,引号字符本身被剥离
  • 含空格的多词参数(cron 表达式、prompt)必须加引号,否则会被拆成多个 token

例如 prompt check build 写成 /schedule 5m 'check build'(预填表单的提示词字段)。引号只包住含空格的那个参数本身,不要在外层再套引号(嵌套引号会拆出错误 token)。

子命令补全

输入 /schedule 后 Tab 补全子命令关键词(list/on/off/rm/run);on/off/rm/run 之后补全当前任务 id。

schedule 语法

duration(间隔调度)

<数字><单位>,数字与单位间不留空格,大小写不敏感:

单位 含义 乘数
s / sec / second / seconds 秒 1,000 ms
m / min / minute / minutes 分 60,000 ms
h / hr / hour / hours 时 3,600,000 ms
d / day / days 天 86,400,000 ms

例如:5m、2h、1d、30seconds、2hours。非法输入(裸数字、未知单位、空串、负值)解析失败:schedule tool 在预校验即报错(Invalid parameters: unrecognized schedule "...",修正参数后重调);/schedule 命令在 json/print 模式带参直建时报 Invalid schedule: "..."(rpc/tui 模式则由表单提交后报错)。

cron(时间点调度)

标准 cron 表达式,支持 5 字段与 6 字段:

  • 5 字段(分 时 日 月 周):自动补 0 秒字段(如 0 9 * * 1-5 → 0 0 9 * * 1-5)
  • 6 字段(秒 分 时 日 月 周):原样使用

含空格自动走 cron 分支:schedule 输入中不含空格 → 按 duration 解析;含空格 → 按 cron 解析。因此 cron 表达式必须包含空格(正常写法天然如此),duration 不得含空格。

选项语义

任务创建与管理由两个 tool 承担:schedule(prompt / schedule / kind / name / expires / model)与 schedule_control(action: list / toggle / delete / run,附 id / enabled)。schedule 的参数即创建参数:调用直接创建任务,无确认表单(见「创建方式」)。下表为 schedule tool 的选项语义(/schedule 表单的 once / cron 形态对应 kind / 自定义 cron):

选项 取值 语义
kind recurring(默认)/ once recurring 每次触发后按 schedule 重算下次时间;once 触发一次后自动删除
name 字符串 任务可读名称,缺省从 prompt 自动生成(≤30 字原样,超长截前 27 字加省略号)
expires duration 字符串 / never recurring 任务的过期时间:now + duration;never 永不过期;缺省 7 天。once 任务忽略 expires 参数(触发即删,传不传都不生效)
model scoped model id(provider/model) 任务执行所用模型;缺省跟随会话当前模型

创建方式(触发反转)

两条路径,职责不同:

人侧 /schedule:打开创建表单(GUI 由 FormOverlay 的 ScheduleForm 渲染,TUI 由 ScheduleCreateComponent),用户在表单里自选模式/时间/模型/提示词后创建。命令 handler 异步打开表单后立即返回——填表时长不受命令通道超时约束。模式矩阵:

形态 会话模式 交互
GUI 表单 rpc 统一表单协议(uiFormInteract + marker select 通道):FormOverlay 弹出 schedule 表单,预填草稿经 initial 直传;命令立即返回,交互在回包窗口之外续跑
TUI 表单 tui ctx.ui.custom 挂 ScheduleCreateComponent:模式 → 时间 → 模型 → 提示词 → 提交 五 tab 逐项确认,两段 Esc 取消
非交互 json / print 不做交互:带参 <schedule> <prompt> → 直建(成功看 session entry,/schedule list 可验)。无参或带参失败(非法表达式 / 缺 prompt)→ 抛错(该模式 ctx.ui.notify 是 no-op、handler 返回值被丢弃,stderr 是唯一可见通道)

模型侧 schedule tool:直接创建(无确认表单、无 headless「未经确认」附注)。参数不完整时必须先经 ask-user / 对话澄清,禁止猜测频率后静默创建(promptGuidelines 硬性条目)。创建成功的 tool result 含任务 id 与后续运行时刻,模型应复述给用户以便核对/撤销。

表单通道/协议的失败折叠(命令路径):

态 触发 结果
cancelled 用户取消表单 不创建、无 toast(取消不是错误)
channel-error RPC 交互通道不可用:select reject,或回包回显请求 payload(宿主不识别表单协议的旧组合) ctx.ui.notify(..., 'error') 提示通道不可用 / 升级宿主;本会话后续 /schedule 直接给同样提示,不重复试探
non-json 回包形状非法(非协议 JSON / 非 ScheduleFormResult)= 协议版本错配 ctx.ui.notify(..., 'error') 提示协议版本错配
abort session_shutdown(quit/reload/new/resume/fork)→ 命令路径自持 AbortController abort 不创建、无 toast(taiji 内切换会话不触发,表单保留且仍有效)

取消/超时不是错误:不创建、无 toast。命令路径的错误出口统一按 mode 分流——rpc / tui 走 notify(..., 'error')(pi 会把 handler 异常折成 renderer 不消费的 extension.error,throw 在 rpc 下静默丢弃);json / print 一律 rethrow。

文案与语言(L2 本地化)

命令反馈(创建/列表/启停/删除/用法/错误提示)与 TUI widget 文本、托盘标题均由扩展侧词典(src/i18n.ts)按当前界面语言渲染;语言经 <dataDir>/ui-preferences.json(runtime 写、扩展只读,见 data-source-registry 登记行)就地读取,文件缺失/损坏回落 en-US。/schedule 的 registerCommand.description 是注册期静态串、托盘标题随 widget 刷新(≤30s)跟进语言——两者切语言后不会立即热更(已接受滞后)。

模型可见文案(schedule tool 的 description / promptGuidelines / tool result)保持英文,不经 L2 词典——两受众不串(本地化不得污染 tool result)。

示例

recurring 间隔任务(/schedule 打开预填表单,确认后创建):

/schedule 5m 'check build'

交互创建(无参打开空表单,表单里选每天 09:00):

/schedule

一次性提醒:在 /schedule 表单里把执行模式切到「一次性」并选时刻。

cron 任务:在 /schedule 表单里选自定义 cron 并填表达式(如 0 9 * * 1-5)。

非交互创建(json/print 模式,无表单可开,带参直建):

pi -p "/schedule 5m 'check build'"

立即执行(schedule_control tool 调用:马上 dispatch 现有任务):

{"action": "run", "id": "<task-id>"}

永不过期(schedule tool 调用:直接创建长期 recurring 任务):

{"prompt": "monthly report", "schedule": "1d", "expires": "never"}

限制与运行时行为

限制/行为 值 说明
任务上限 50(MAX_TASKS) 超过抛 Task limit reached (50),需先删除任务
触发频率上限 6 次/分钟(RATE_LIMIT_PER_MINUTE) 滑动 60s 窗口。/schedule run 超限返回 not dispatched(disabled, rate-limited, or dispatch in flight);tick 自动 dispatch 超限静默跳过
tick 间隔 30s(TICK_INTERVAL_MS) 到期任务在下一个 tick 被 dispatch;实际触发时间可能比计划晚最多 30s
默认过期 7 天(DEFAULT_EXPIRY_MS) recurring 任务缺省 expires 时;expires: 'never' 关闭
once 任务 触发后自动删除 不参与后续调度
cron 失效 任务停用 + lastStatus=failed + lastError='cron expression invalid' 不会用 now() 兜底导致每 tick 重触发死循环
忙时 dispatch steer 直投(scheduler-steer-direct-dispatch) busy 时消息插入当前 turn、idle 时开新 turn({deliverAs:'steer', triggerTurn:true}),受理即记账不排队;同任务 in-flight 检查防双投
任务级模型切换 dispatch 前 setModel(task.model),完成后恢复原模型 task.model 非空且 ≠ 会话当前模型时按「互斥校验 → setModel → dispatch」执行,busy 亦生效(steer turn 开新 turn 取模型快照);恢复挂 agent_settled(run 完全沉降)+ tick 事实查询兜底;全部 setModel 经模型 op 串行队列防并发交错;模型切换秩序设计(design D3)原文未归档,裁决内容与背景以 commit 1def857fe(初版与 P-MODEL 探针修订)/ 005fb44bf(模型 op 串行化收敛)/ 8159e3fe7(agent_settled 恢复锚点)message 为溯源锚
widget 推送节流 指纹跳推(任务集不变即零推送) widget 行内容静态化(不含随时间流逝变化的元素)是跳推前提;每次推送前对比任务集指纹,不变即跳过本次推送(判定在 index.ts 的 refreshWidget);widget 推送修正设计原文未归档,裁决内容与背景以 commit 4bd690a54 message 为溯源锚
history 保留最近 20 条执行记录 超出丢弃最旧(上限由 appendExecutionRecord 在 runtime 记账侧单点裁剪);快照写入口不裁剪——只剥离运行时标记 pending 后逐项拷贝 history 全量序列化
持久化 任务集全量快照原子落盘(每次任务集变更即写,tmp+fsync+rename) 任务随 owner session 分片持久化,resume 后直读恢复;落盘失败 warn + 内存态照常推进、下写点重试
交付语义 受理即记账(投递端口 at-most-once 注入) dispatch 经投递端口受理后注入失败首败即弃(forget,2026-10-25 裁决:丢一轮 << 重投造成重复提醒);端口拒收(rejected)走 failed 记账、下个 tick 单通道重试
延迟写入窗口 无(写盘不依赖 pi 会话首写门控) 任务库独立于会话 JSONL,每次变更即落盘——旧「pi 首条 user 或 assistant 消息前不 flush」的丢失窗口已随任务库分离消除
触发条件 pi 进程需存活且 session 打开 电脑睡眠 / pi 进程未运行 = 不触发(非系统 cron,无后台守护)

错误语义(无错误码;双受众分派——message 英文回退供 tool result / messageKey+params 供命令层词典渲染,见「文案与语言」):schedule tool 创建时参数预校验失败(prompt 空 / schedule 非法)→ 直接 throw Invalid parameters: ...(修正参数后重调);/schedule 命令 json/print 模式带参解析失败 → throw 词典文案(en: Invalid schedule: "...");run/toggle/delete 引用不存在的 id / run 时任务 disabled / rate-limited / 同任务在途 / 任务数超上限 → 命令层 toast 按词典本地化(en 示例:Task <id> not found / Task <id> not dispatched (disabled, rate-limited, or dispatch in flight) / Task limit reached (50) — delete one first)。表单交互的取消/abort 不是错误(不创建、无 toast);通道失败/回包非法在 rpc/tui 走 notify(..., 'error')、在 json/print 走 throw——见「创建方式」。

数据存储位置

当前机制(独立 JSON 快照分片)

任务存储为独立 JSON 快照文件(scheduler-redesign 方案 B:任务库与会话事实分离,2026-10-30 裁决;方案 B 设计原文未归档,裁决内容与背景以 commit 73073b169 message 为溯源锚),路径由 schedulerStoreFilePath(getAgentDir(), cwd, sessionId) 推导:

<agentDir>/scheduler/<root>/<segments>/<sessionId>.json
  • agentDir = pi 的 getAgentDir() 官方数据目录
  • <root>/<segments> = owner session 工作目录(cwd)的路径隔离层——同 agentDir 下不同 cwd 互不干扰
  • <sessionId>.json = 分片文件:持久会话按 sessionId 分片(resume 同 id 同文件恢复;fork 新 id → 新分片空任务集,天然不继承,等价旧 fork owner 过滤语义);--no-session 等空/非法 sessionId 归一为固定分片 no-session(任务仍持久化,跨重启命中同一分片恢复)

文件形状:{ schemaVersion: 2, sessionFile?: string, tasks: ScheduledTask[] }——tasks 为内存任务集全量快照(写入口统一剥离运行时标记 pending、逐项拷贝 history);sessionFile 为冗余排障字段。

原子替换写

每次任务集 mutation 全量落盘(backend.persistTasks → writeSchedulerStore):序列化到 tmp 文件 → fsync → rename(POSIX rename 原子性)。崩溃时要么旧文件完整、要么新文件完整,无半写形态。目录不存在则递归创建(首写建目录)。写失败 throw,由 runtime 的 persistTasksSafe 捕获:warn + 内存态照常推进,下一次任务集变化的写点重试。

读路径三分支

backend.loadTasks(装配点 session_start 调用,非接口成员)→ readSchedulerStore 一次 JSON.parse:

  • 文件不存在(missing)→ 空任务集(新 session / fork / 从未建任务,正常路径)
  • 损坏(corrupt:解析或形状校验失败)→ 改名 <path>.corrupt-<ts> 留证 + warn,按空任务集继续(调度不中断);改名自身失败不抛,仍返回 corrupt
  • 其他读错误(EACCES 等)→ 上抛,由装配点显式降级(warn + 空任务集继续装配,恢复失败计数累计可见)

会话 JSONL 的边界

会话 JSONL 只承载 dispatch 注入消息(到点注入时会话必已含 user 或 assistant 消息——任务只能经用户命令或模型 tool 创建,二者都以 user 或 assistant 消息为前提,pi 首写门控早已通过)与用户对话;任务定义与调度记账不写会话 JSONL——无首写门控依赖,任务库写盘独立于会话流。旧 event sourcing 写链(pi-scheduler: custom entry 族 + replay 重放折叠)已随 plan B 整体删除。

单一写者 + 同源单实现

分片键 = sessionId 保证单写者:同一 session 同一时刻只有一个 pi 进程,分片文件天然无并发写。已知例外:--no-session 等空/非法 sessionId 归一为固定分片 no-session——同 cwd 并存多个 pi --no-session 进程时共写同一文件,写侧整文件 RMW(tmp → fsync → rename)无跨进程锁,后写覆盖先写。store 模块(@zhushanwen/extension-protocol/scheduler-store)是扩展(pi 进程内,唯一写者)与 taiji scheduler-manager 面板(runtime worker,只读者)的同源单实现——路径推导与文件形状两侧构造性一致,双实现会漂移。

不进入 LLM context

任务数据存独立文件,天然不进 pi 的 context 构建——零 token 污染,不依赖 pi 对 custom entry 的过滤行为。

归属即结构性质

任务物理存在于 owner session 的分片文件。分片文件删除 = 任务消失,无残留、无需 GC。但 session 删除不回收分片:分片在 <agentDir>/scheduler/...(pi agent 数据目录)不在 session 存储树内,无 owner、无清理路径(已知例外,见 data-source-registry #88)——fork 出的 session 用新 sessionId → 新分片(不加载、不执行继承的任务副本,原 session resume 照常),subagent 也不受主 session 任务干扰。

旧 store(v1)处置

旧版 store(cwd 共享的 scheduler.json,v1 schema)与新分片同目录不同名、互不冲突;按用户裁决不迁移、留盘为孤儿——升级后旧文件原样保留,不读不写。

依赖的 pi 行为清单

本扩展的存储与投递方案(独立快照分片 + 投递端口 dispatch)依赖以下 pi 源码行为。这些是实测存在但非 SDK 契约承诺的隐式行为,pi 升级后需逐条复核。各条锚点实测自 pi v1.0.0:路径相对 pi-mono 仓库 packages/coding-agent/src/(core/ 前缀照写;config.ts 在包根,agent.ts 取自 packages/agent/src/),行号为该版本源码行;pi 升级后按文件名 + 符号名重新定位行号复核(行号会漂移,符号名是稳定锚点):

  1. getAgentDir() 返回进程级数据目录:任务库分片根路径来源。实测:PI_CODING_AGENT_DIR 环境变量覆盖优先、缺省 ~/.pi/agent(config.ts:563-569;常量名定义 config.ts:543)。runtime 注入 pi 进程的 PI_CODING_AGENT_DIR 与 taiji 面板侧 getPiAgentDir() 必须指向同一目录,否则两侧分片路径漂移(同源单实现的前提)
  2. sessionManager.getSessionId() / getSessionFile() 稳定语义:sessionId 是分片键(resume 同 id / fork 新 id 的恢复语义全系于此);getSessionFile() 为空 = --no-session 模式的判据(归一 no-session 分片)。实测:两 getter 直读字段(core/session-manager.ts:1152-1158);resume 加载时 sessionId 取自文件 header(core/session-manager.ts:1090),fork / 新 session 换新 id(core/session-manager.ts:1713、1745)。若 pi 改变 sessionId 的稳定性(如 resume 换 id),任务恢复会失效
  3. host.sendMessage steer 档注入形态:dispatch 经投递端口翻译为 steer 档 sendMessage(busy 时消息插入当前 turn、idle 时开新 turn,{deliverAs:'steer', triggerTurn:true})。实测:扩展 sendMessage 绑定到 sendCustomMessage(core/agent-session.ts:3341-3349);isStreaming 时 steer 档转入 agent.steer、非 streaming 且 triggerTurn 时开新 turn(core/agent-session.ts:2259-2272);steer 消息在当前 assistant turn 的工具调用结束后、下一次 LLM 调用前注入(agent.ts:299-301 入队,注入时机注释 core/agent-session.ts:2156-2158)。若 pi 改变 steer 语义(注入失败形态、triggerTurn 行为),dispatch 交付语义需重新评估
  4. ctx.isIdle() 同步事实查询(U4 dispatch 模型切换恢复判定锚点):agent_settled 事件的「run 完全沉降」复核依赖它。实测:接口声明 core/extensions/types.ts:348,ctx 绑定经 runner 转发(core/extensions/runner.ts:909-912),getter 实装 = !_isAgentRunActive && !isCompacting(core/agent-session.ts:1435-1437)。若语义变化,模型恢复判定需重新评估
  5. 会话首写门控(user 或 assistant 消息):会话 JSONL 文件在会话首次含 user 或 assistant 消息时才创建(首 flush);setup entry(model / thinkingLevel / system prompt)与扩展注入的 custom message 单独存在时不落盘。任务库写盘不依赖该门控(任务库独立于会话 JSONL),但「任务创建后会话文件已存在」的推断以它为前提。实测:_hasConversation() 判据 = fileEntries 中类型 message 且 role 为 user 或 assistant(core/session-manager.ts:1166-1170),_persist 首次 flush 在该判据成立时以 openSync(..., 'wx') 建文件(core/session-manager.ts:1172-1177),注释原文「Starting at the user message (not the first assistant reply)」(core/session-manager.ts:1161-1164;pi 包 dist 编译产物对应 core/session-manager.js:786-798)。若 pi 改变门控(如首条任意 entry 即落盘,或扩展 custom message 也算会话事实),依赖该前提的叙述与验收需重新评估

任一条行为变更都需重新验证验收场景(尤其 resume、fork、dispatch 场景)。

开发

pnpm test          # 运行全部 vitest 测试(等价 npx vitest run)
npx vitest run src/__tests__/<file>.test.ts   # 单个文件

测试策略:

  • 依赖反转:SchedulerRuntime 只依赖 SchedulerBackend 接口(deliverDispatch / persistTasks / now),不触碰 FS/pi。测试注入 MockSchedulerBackend(src/__tests__/mock-backend.ts 测试专用实现,记录 deliverDispatch/persistTasks 调用、支持注入固定时间与落盘失败)实现零副作用测试;backend.loadTasks 类方法由 store 模块的真实读写覆盖——持久化只经 scheduler-store 纯 FS 读写(无进程内状态),测试注入临时目录路径即可全行为覆盖
  • 纯函数:parseDuration / formatDuration / parseSchedule / computeNextRunAt / computeNextRuns(src/parsing.ts)无副作用,可直接断言
  • 快照读写:store 模块三分支读(ok / missing / corrupt 改名留证)+ 原子替换写 + 写入口 pending 剥离与 history 逐项拷贝(不裁剪——20 条上限属 appendExecutionRecord,@zhushanwen/extension-protocol scheduler-store 子出口测试)
  • 投递链:backend deliverDispatch 的端口受理/拒收/幂等键路径(src/__tests__/backend-delivery.test.ts)
  • 直建流:handleSchedule 预校验 / abort / 参数透传(src/__tests__/tool-create-flow.test.ts);命令路径表单(无参 / 带参预填 / 取消 / 提交 / channel-error / non-json / echo / 模式矩阵 / 异步不阻塞 / json·print throw,src/__tests__/commands-form.test.ts);交互生命周期(草稿构造 + AbortController 注册表,src/__tests__/interaction.test.ts)

扩展内部结构:backend.ts(后端抽象:投递端口 dispatch + 快照读写)→ runtime.ts(调度核心,任务集 mutation 后 persistTasksSafe 全量快照落盘)→ service.ts(业务入口)→ tool.ts(tool 直建流)/ commands.ts(/schedule 命令适配层)→ interaction.ts(表单交互 / 协议错配 / AbortController 生命周期)→ create-form-component.ts(TUI 创建表单组件,由 interaction.ts 消费)→ widget.ts(双模状态栏 widget:GUI 结构化 meta + TUI 文本行)→ i18n.ts(L2 词典 / locale 读取 / 结果渲染)。快照文件读写实装在 @zhushanwen/extension-protocol 的 scheduler-store 子出口(packages/extension-protocol/src/extensions/scheduler/store.ts,与 taiji scheduler-manager 面板同源单实现)。