Super Productivity 重复任务(Recurring Events)实现方案解析:扁平字段向 RRULE 规范化演进的引擎与同步安全设计
2026/9/13 14:47:37 网站建设 项目流程

Super Productivity 重复任务(Recurring Events)实现方案解析:扁平字段向 RRULE 规范化演进的引擎与同步安全设计

【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity

本文是 Super Productivity 仓库中docs/research/recurring-events-implementation-plan.md的设计论证(design rationale)指南。它围绕“重复任务(recurring tasks)的模型与引擎如何演进”这一核心主题展开:从约 14 个互相耦合的扁平字段(repeatCyclerepeatEvery、7 个星期布尔量、monthlyWeekOfMonth等),演进到与 RFC 5545 RRULE 同构的规范化模型,同时保证多端 op-log 同步不破坏、旧客户端兼容不破坏、确定性 ID 不漂移。读完本文,你将掌握该方案的三条核心前提、已废弃的“类型化模型”设计及其仍具约束力的正确性约束、当前已实现的能力清单、Phase 1–3 的落地路径、迁移子系统选型(op-log schema 而非pfapi-config.js),以及 RFC 5545 RRULE 的完整速查。

阅读前置:本计划是feat/rrule-epic分支的配套文档,而非全新提案。分支上已实现的内容(正向/反向 RRULE 转换器、旧客户端兼容契约、默认关闭的按设备引擎开关)比本文更接近当前事实;如需规划或构建,请先以该分支的 roadmap 为准。文档已在 2026-08-21 与分支做法对齐:分支采用“原始rrule字符串”设计并保留(见下文 Decision 说明)。


1. 三条核心前提(Net Premises)

文档在多次多轴评审后收敛为三条驱动全篇的前提,全部可以对照当前仓库源码验证:

  1. RRULE 引擎已在仓库中ical.js@2.2.1已存在于根package.json(第 294 行),且是devDependency而非运行时依赖;它通过 ical-lazy-loader.ts 懒加载,并在两处实际展开 RRULE:一是 get-relevant-events-from-ical.ts(日历集成时展开 VEVENT 的rrule,见其 403–444 行的iter.next()循环与 EXDATE 过滤),二是packages/plugin-dev/caldav-calendar-provider/src/plugin.ts(CalDAV 提供者插件)。caldav-client.service.ts只借用 ical.js 解析 VTODO,不做RRULE 展开。早期草案建议“不要引入 rrule.js”,这一点后来被 epic 分支推翻(分支固定引入rrule@2.8.1作为引擎),详见 Decision 说明。
  2. 曾经的“关键缺口”已上线。第 N 个星期几(issue #6040)、月末最后一天(issue #7726)、EXDATE 跳过实例(deletedInstanceDates)均已实现;早期 gap-analysis 文档已过时并被并入本文附录 A/B。
  3. TaskRepeatCfg是同步状态。模型变更必须走 op-log schema 迁移体系(packages/shared-schema/src/migrations/),保持确定性 IDrpt_${repeatCfgId}_${dueDay}稳定,且不得破坏跨版本同步。这是整个方案风险的主导项。

2. 决策记录(已废弃):类型化、与 RRULE 同构的复发模型

已于 2026-08-21 废弃。现行决策是 epic 分支的:在传统扁平字段旁附加一个rrule?: string,扁平字段作为线格式永远双写(dual-write)。下文是已废弃的类型化模型方案,保留它是因为其正确性约束(DTSTART/正午锚定、EXDATE 按日字符串匹配、UNTIL包含性、WKST、月度锚点优先级、奇偶校验门、线格式不重命名与迁移警告)对分支的序列化器与引擎同样适用——只是持久化形态不同。

被废弃的设想:把复发模式变成一个单一的、带类型的结构化字段——一个与RFC 5545 一一对应的判别联合(discriminated union),取代约 14 个互相依赖的扁平字段(repeatCyclerepeatEvery、7 个星期布尔量、monthlyWeekOfMonthmonthlyWeekdaymonthlyLastDayquickSetting)。

RFC 5545 的RRULE 字符串只在边界处产生/解析.ics导出、CalDAV)。原始字符串永远不是持久化/同步字段。

2.1 为什么“拒绝原始字符串”的结论已过时(2026-08-21 调和)

早期修订版从三个角度拒绝把原始rrule字符串作为规范字段。对照feat/rrule-epic实际构建的内容,每个异议都被一个当初未预见的决策回答了:

  • “不可查询。”分支是增量式添加rrule?: string,并且永远双写传统扁平字段——它们是旧客户端的线格式(roadmap “Dual-engine endgame” 第 4 节)。选择器与任何字段级消费方都保有结构化表示,投影时无需解析字符串。
  • “不可 diff / 不可修复。”同样靠双写:op-log 仍对传统字段做 diff,data-repair.ts仍修复它们。对字符串本身,分支用显式策略替代静默损坏:引擎是 fail-soft 的(格式错误的rrule→ 记录日志并返回null,绝不 throw);isRRuleValid门控路由(无效 → 回退传统引擎);在后传统引擎时代,对无效字符串的既定行为是暂停 + 修复提示,绝不静默重新排程。
  • “热路径性能。”该异议假设展开引擎是 ical.js(异步、懒加载、仅前向)。分支的引擎是rrule.js(rrule@2.8.1,精确锁定版本)——同步,放在专用的按日粒度、基于 UTC 的出现日工具(store/rrule-occurrence.util.ts)里,运行在现有同步计算器内部;选择器路径不引入任何异步依赖。

旧推理唯一权衡正确的一点是:rrule是一个新的根运行时依赖,项目规则通常禁止。维护者拥有该 epic,故意接受它并把 rrule.js 当作“引擎”对待——锁定精确版本,并以差异规范测试集作为升级绊线(设备间解析器版本漂移会制造重复任务,见 roadmap 的风险模型)。这一取舍已定,请勿再争论,也不要解除版本锁定

2.2 对目标的诚实告诫(按此设计)

  1. “数据模型更小”只是部分的。模式子模型确实从 ~14 个字段塌缩为 1 个类型化recurrence字段,但TaskRepeatCfg仍保持约 18 个字段(任务模板 + SP 扩展 + 追踪字段不可削减)。真正的收益是不变量消除——判别联合让“字段互相矛盾”变得不可表示,删除了隐式优先级这一类 bug(例如“Nth-weekday 锚点压过monthlyLastDay”),并大幅缩小data-repair.ts。应当推销这一点,而不是字节数。
  2. “覆盖一切”只有一个例外。RFC 5545 无法表达“完成后的 N 天”,因此repeatFromCompletionDate(SP 的差异化能力)保持为独立的非 RRULE 表示。模型是“RRULE 同构 + 一个例外”,引擎保留两种模式。

3. 类型化模型(已废弃——保留作为理由记录)

已废弃——见上方 Decision 说明。保留它,是因为下面的不变量分析是对传统字段隐式优先级规则的最佳记录。

该草图把TaskRepeatCfgCopytask-repeat-cfg.model.ts——请编辑TaskRepeatCfgCopy,而不是Readonly别名)中的扁平模式字段替换为一个判别联合加一个结束条件。草图(最终命名待定):

type Weekday = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU'; type RecurrencePattern = | { freq: 'DAILY'; interval: number } | { freq: 'WEEKLY'; interval: number; byDay: Weekday[] } // WKST derived, never persisted — see 1.3 | { freq: 'MONTHLY'; interval: number; on: { monthDay: number } } // BYMONTHDAY=n | { freq: 'MONTHLY'; interval: number; on: { lastDay: true } } // BYMONTHDAY=-1 | { freq: 'MONTHLY'; interval: number; on: { week: 1 | 2 | 3 | 4 | -1; day: Weekday } } // BYDAY=nDD | { freq: 'YEARLY'; interval: number; month: number; day: number }; type RecurrenceEnd = | { type: 'never' } | { type: 'count'; count: number } // COUNT | { type: 'until'; until: string }; // UNTIL — DbDateStr, inclusive end-of-day interface RecurrenceConfigPart { // canonical, RRULE-isomorphic, persisted/synced: recurrence: RecurrencePattern; end: RecurrenceEnd; deletedInstanceDates: string[]; // wire name kept verbatim — `exDates` would be exactly the forbidden rename // SP carve-out — not expressible in RFC 5545: repeatFromCompletionDate?: boolean; }
  • 判别符freq(加上月度的on形态)。非法组合(例如在年度配置上设置星期布尔量)变得不可表示。
  • 不持久化任何派生字段。UI 表现物(星期复选框行、“Ends”控件、quickSetting)在表单打开时由recurrence/end计算、保存时写回——纯视图模型。这化解了文档记录的 formly “整模型 emit” 陷阱(不再有第二个表示可漂移)。
  • repeatFromCompletionDate选择例外引擎(见下文)。

4. 已修正的当前状态(已上线能力)

src/app/features/task-repeat-cfg/中验证:

能力状态位置
每日 / 每周 / 每月 / 每年 +repeatEvery间隔get-next-repeat-occurrence.util.ts
星期选择(每周)7 个布尔量,task-repeat-cfg.model.ts
每月第 N 个星期几(“第 2 个周二”、“最后一个周五”)✅ #6040monthlyWeekOfMonth+monthlyWeekday;get-nth-weekday-of-month.util.ts
每月最后一天✅ #7726monthlyLastDay;get-next-repeat-occurrence.util.ts 中的月末钳制
每月第一天快捷设置MONTHLY_FIRST_DAY
跳过某次出现(EXDATE)deletedInstanceDates: string[]
完成后复发✅(SP 独有)repeatFromCompletionDate+ get-effective-repeat-start-date.util.ts
等待完成(不堆积)waitForCompletion
跳过逾期实例skipOverdue
暂停 / 恢复isPaused
子任务模板(+ 继承 / 自动更新标志)subTaskTemplatesshouldInheritSubtasksdisableAutoUpdateSubtasks
DST 安全计算全程本地正午锚定(见 1.3)
确定性多设备 IDrpt_${repeatCfgId}_${dueDay},get-repeatable-task-id.util.ts
人类可读描述get-task-repeat-info-text.util.ts
“下次到期”预览 + 历史热力图repeat-cfg-preview/repeat-task-heatmap/

真正缺失(Phase 3 交付):结束条件(COUNT/UNTIL)、每月多日(BYMONTHDAY=1,15)、.ics/CalDAV RRULE 生成(Phase 1)。延迟 / YAGNI:RDATERECURRENCE-IDBYWEEKNOBYYEARDAY、亚日频率、完整双向.ics导入。BYSETPOS:仅序列化器的月末钳制惯用法需要(见 Phase-1 映射表);一般引擎侧展开仍延迟。


5. 引擎决策:保留同步有界引擎

已调和:对传统/开关关闭的配置成立;开关打开的rrule配置路由到分支的同步 rrule.js 引擎(见 2.1)。ical.js 无论哪种方式都只在边界使用——下文记录原因,且仍然成立。

出现日运行时保持现有的同步有界循环(get-next-repeat-occurrence.util.ts、get-newest-possible-due-date.util.ts),改为读取新的类型化recurrence字段而非扁平字段。ical.js用于在导出/CalDAV 边界序列化/解析 RRULE 字符串——绝不出现在出现日热路径上。

后果:

  • 同步选择器中不引入懒加载模块的异步依赖;引导/投影路径上没有约 76 KB 的开销(ical-lazy-loader.ts 的注释明确记录 ical.js 约 76 KB)。
  • 出现日逻辑几乎不变(同样的FREQ/INTERVAL/BYxxx数学,只是输入形状不同),因此确定性 ID 的奇偶风险很小,离线 golden-master 测试就足够——不需要生产影子模式
  • 新的常见模式(每月多日、结束条件)是对有界引擎的小扩展。奇异 RRULE 部件(通用BYSETPOSBYWEEKNO)并不免费;延迟它们,若确有需要,就通过 ical.js 在热路径之外扩展这些稀有配置。

6. Phase 1 —— 类型化模型 + RRULE 序列化器 + 奇偶校验架

可独立交付;序列化器解锁日历双向同步路线图中的“SP 不生成 RRULE”关键路径项。

6.1 添加类型化recurrence/end字段(增量式,尚非规范)

在现有字段旁添加联合。由于校验使用 typiacreateValidate(容忍多余属性,而非createValidateEquals——在validation-fn.ts中验证),旧客户端读取新字段时既不会拒绝也不会剥离它们——结构性前向兼容。确认data-repair.ts没有任何 pass 会删除它们,并添加前向兼容回归规格。

6.2 双向序列化器(类型化 ⇄ RRULE 字符串)

纯模块(例如task-repeat-cfg/rrule/)。typed → RRULE是简单字符串组装(或ICAL.Recur.fromData({...}).toString());RRULE → typed(用于.ics导入)使用 ical.js 解析。字段映射——必须覆盖一切:

类型化模型RRULE
{freq, interval}FREQ=...;INTERVAL=...
WEEKLYbyDayBYDAY=MO,WE,...(WKST:见 6.3)
MONTHLYon.monthDayBYMONTHDAY=<n>(n ≤ 28);29–31 → 钳制惯用法BYMONTHDAY=<n>,-1;BYSETPOS=1(SP 钳制到月末,纯BYMONTHDAY会跳过)
MONTHLYon.lastDayBYMONTHDAY=-1
MONTHLYon.{week,day}BYDAY=<week><DD>-1=最后)
YEARLY{month, day}BYMONTH=<m>;BYMONTHDAY=<d>;2 月 29 日 → 同一钳制惯用法(BYMONTH=2;BYMONTHDAY=29,-1;BYSETPOS=1,非闰年钳到 2 月 28 日)
end.count/end.untilCOUNT=/UNTIL=(UTC 当日结束)
deletedInstanceDatesEXDATE(仅导出;线字段名保持原样)
repeatFromCompletionDate不可表达—— 序列化器拒绝/标记;此类配置天然不兼容导出
startTimeremindAtwaitForCompletionskipOverdue、子任务标志、orderSP 扩展,在 RRULE 带外 —— 保留
isPaused无 RRULE 等价物 —— 序列化器跳过或标记暂停配置(见下)
lastTaskCreationDay/lastTaskCreation内部创建游标,在 RRULE 带外 —— 永不导出,也永不由导入的规则派生

isPaused会短路所有出现日生成(task-repeat-cfg.selectors.ts 中两个选择器都先过滤isPaused),因此把暂停配置导出为活跃 RRULE 会承诺 SP 永远不会创建的出现日。

三行 MONTHLY并非独立——引擎按固定优先级解析一个锚点:nth-weekday(monthlyWeekOfMonth+monthlyWeekday,在 get-next-repeat-occurrence.util.ts 首先检查)→monthlyLastDaystartDate的日序。逐行序列化对携带多个锚点的配置发出多个部件,会产生BYMONTHDAY=-1;BYDAY=2TU——在 RFC 5545 中是交集,通常是空集。必须只序列化一个锚点,按上述优先级顺序(epic 分支的legacyTaskRepeatCfgToRRuleswitch 正是这样做的)。

6.3 DTSTART / 日期基准正确性(最容易咬人的部分)

  • DTSTART 是锚定日的本地正午,去掉时间分量。传统引擎做日期数学时从不使用startTime——它在setHours(12,…)处锚定(get-next-repeat-occurrence.util.ts)。非正午的 DTSTART 会让 ical.js 在另一个瞬时产生出现日,可能跨日/DST 边界滚到不同的日历日→ 奇偶被破坏、ID 偏移。startTime保持为展开后的任务模板字段,不参与 DTSTART 日期数学。
  • EXDATE 按日字符串匹配,而非瞬时相等:用getDbDateStr(occurrence)对照exDates过滤生成的出现日。
  • UNTIL是包含当日的结束。
  • WKST= 生效 startDate 的星期——或干脆不发出WKST并重新锚定 DTSTART,正如 epic 分支的getAlignedStartDate不是用户的firstDayOfWeek:该设置仅用于显示(src/app/core/date-time-format/custom-date-adapter.ts:19)。引擎按从 startDate 星期起算的滚动 7 天块计数(getDiffInWeeks(startDate, d) % repeatEvery,get-next-repeat-occurrence.util.ts),因此日历周的WKST会移动双周(INTERVAL=2)出现日。反例:startDate 为周三 2026-01-07、INTERVAL=2BYDAY=MO,FR——SP 触发周一 01-12;WKST=MO触发周一 01-19。

6.4 出现日奇偶 golden master(闸门)

差分测试架:读取类型化字段的引擎,与读取扁平字段的现有引擎产生逐字节一致的出现日,覆盖每种配置形态(每日、每周多日、repeatEvery>1、按日每月、每月第 N 个星期几、每月最后一天、每年、2 月 29 日),跨多年窗口、两个 CI 时区、跨越 DST 边界。限制每种形态的出现次数上限,防止以后加入亚日频率时测试爆炸。基于完成的配置超出测试架范围(不同引擎)。迁移以 100% 奇偶为门槛。序列化器往返(typed → RRULE → typed)做属性测试。


7. Phase 2 —— 带版本迁移(经 op-log schema 体系)

已修正机制。不是pfapi-config.js——该文件是@deprecated LEGACY CODE(其CROSS_MODEL_VERSION是过时的4.4,且它require./migrate/cross-model-migrations路径已不存在)。

经由活跃的 op-log schema 体系迁移:

  • packages/shared-schema/src/migrations/中添加vN → vN+1条目(注册表 index.ts),同时提供migrateState(快照)与migrateOperation(在途操作),并提升CURRENT_SCHEMA_VERSION(schema-version.ts)。由src/app/op-log/persistence/schema-migration.service.ts/remote-ops-processing.service.ts应用。转换本身是纯 O(1) 的字符串/结构组装,即使配置很多也很便宜;迁移不得按配置展开出现日。

  • 跨版本叙事(解决旧客户端矛盾)。2026-08 修正——见 #9664。早期修订把MIN_SUPPORTED_SCHEMA_VERSION称为“强制更新闸门”,会推动旧客户端走VERSION_UNSUPPORTED流程。那是反的。它是施加于_本客户端读取的数据_上的下限(remote-ops-processing.service.ts:177operation-log-sync.service.ts:2318verify-decrypted-op-integrity.ts:139migrate.ts:49,115);发送方只盖CURRENT_SCHEMA_VERSION章,且不存在服务端客户端版本闸门。提升它会把已更新的客户端卡死——阻塞发生在上传前的周期内(sync-wrapper.service.ts:595-606),游标不前进,而VERSION_UNSUPPORTED提示故意不带任何补救措施(remote-ops-processing.service.ts:540-546,“更新本设备无济于事”)。它永远不会提示旧设备。这里没有版本闸门。

    而且CURRENT_SCHEMA_VERSION的提升也不能为_当前已发布_的设备群提供闸门:master 是 4,提升落到 5——在 v17.0.0–v18.14.0 的容忍带(2 + 3)内——这些客户端会未迁移地应用操作;提升到 6 会阻塞它们,但仍推进其游标,永久跳过这些操作。(v18.14.0 之后的接收端_确实_会安全阻塞,因此一旦该群体淘汰,提升就会成为真正的围栏。)参见 schema-version.ts 与 operation-log-architecture.md §A.7.11 的 Bump Policy 完整说明。

  • 真正解决问题的机制已经在feat/rrule-epic上建好了——不要重新推导。传统排程字段与新表示一起保持填充,作为旧客户端的线格式(util/legacy-cfg-to-rrule.util.ts),遵循精确或空(exact-or-null)契约:规则在传统表达能力范围内时,传统字段在相同日期触发;超出范围时(COUNT/UNTIL、季节性BYMONTHBYWEEKNO/BYYEARDAY、多日列表、联合外序数),写入LEGACY_NEVER_FIRES_FALLBACK哨兵——一个全 false 的WEEKLY配置,每个已发布版本都确定性地永不触发。旧客户端什么也不创建,而不是在错误日期创建会同步回传的任务。getAlignedStartDate处理startDate的双重职责——既作为月/年日序编码,又作为间隔锚点。默认关闭的按设备开关(RRuleFeatureFlagService、localStorage、永不同步)在 epic 未完成时保持传统引擎权威——是那个开关,而不是 schema 闸门,让半成品阶段变安全

  • 绝不在线格式上重命名deletedInstanceDates保持同步字段名;exDates是内存/类型化名称,EXDATE是导出名称。在整实体 LWW 下,赢得冲突的旧客户端会在没有重命名字段的情况下重新发出实体,从而把跳过列表在设备群内销毁;部分更新浅合并路径是第二个销毁向量。(如果更偏好保留字面属性名,也可以那样做——要点是:不要重命名持久化键。)


8. Phase 3 —— RRULE 原生功能

类型化模型成为规范后,新模式 = 类型化联合的新增 + 有界引擎的小扩展:

  • 结束条件。end: {type:'count'|'until'},在出现日循环中作为守卫执行(越过边界返回null)。UI “Ends” 控件由end派生,不多持久化任何东西。标签经T/TranslateService(仅en.json)。
  • 每月多日BYMONTHDAY=1,15)等,随模型/引擎成长。

Phase 3 测试

  • 无超过COUNT/UNTIL的出现日,每种频率、两个 CI 时区。
  • 决定并测试COUNTvsexDates被跳过的实例是否消耗一个计数?(ical.js 在 EXDATE 之前计数;SP 在生成后过滤——刻意选择“10 个实际任务” vs “10 个计划”,并测试之。)
  • UNTIL边界:结束日包含,次日排除。

9.repeatFromCompletionDate例外

这不是一个独立的_引擎_——它运行同样的FREQ/INTERVAL计算,但每个周期把开始日期重新锚定到lastTaskCreationDay(见 get-effective-repeat-start-date.util.ts 的源码实现:当repeatFromCompletionDate为真且有lastTaskCreationDay时返回后者)。所以真正的风险是喂入错误的 DTSTART/锚点,而不是“错误的引擎”:

  • 没有稳定的 DTSTART→ 不可 RRULE 表达;序列化器拒绝它(天然不兼容导出)。
  • 建模为联合变体 +repeatFromCompletionDate标志;在任何固定锚点路径之前路由到动态锚定计算。
  • 诚实的代码削减核算:只有固定排程的管道被删除;完成路径保留。

10. 风险登记表

风险严重度缓解
模型切换导致出现日偏移 → 实例被重新编号阻断保留现有有界引擎;Phase-1 golden master 门控迁移
跨版本同步:旧客户端读不懂类型化模型阻断不存在版本闸门(#9664)。线格式上按精确或空契约保持传统字段填充;不可表达规则用LEGACY_NEVER_FIRES_FALLBACK;按设备开关门控 epic
重命名deletedInstanceDates线键导致跳过数据丢失不重命名持久化键;EXDATE仅用于导出
repeatFromCompletionDate被喂入固定 DTSTART → 变成固定日历在任何固定锚点路径之前路由完成模式;每周期重新锚定
错误迁移子系统(pfapi-config.js使用packages/shared-schema迁移 +schema-migration.service.ts
异步/仅前向 ical.js 迭代导致热路径回归ical.js 仅做字符串解析/序列化;同步有界引擎保持为运行时
DTSTART 携带startTime→ 日序滚动DTSTART = 锚定日本地正午;startTime在展开后应用
EXDATE 永不匹配(瞬时 vs 正午)getDbDateStr日字符串过滤
双周偏移(WKST 默认值)startDate 派生的 WKST,或省略并重新锚定——见 6.3
UNTIL丢掉最后一天包含当日结束
生产影子模式成本n/a不需要——引擎不变;离线 golden master 覆盖奇偶
新依赖的包体大小n/a类型化模型方案无新依赖(epic 确实添加了锁定版rrule——见 Decision 说明)

11. 可度量的成功标准(闸门)

  1. Phase-1 奇偶:类型化字段引擎 == 扁平字段引擎,覆盖完整配置形态语料,5 年窗口、两个 CI 时区;测试架在 CI 中绿灯。
  2. 序列化器往返typed → RRULE → typed,覆盖每种形态(属性测试),包括每月第 N 个星期几与最后一天。
  3. 无新运行时依赖(仅 ical.js,边界使用)。(已废弃——epic 故意添加锁定版rrule;见 Decision 说明。
  4. 无同步字段键被重命名deletedInstanceDates保持线格式)。
  5. 前向兼容:读取新字段的旧客户端既不报错也不剥离它们(回归规格)。
  6. 迁移后,repeatFromCompletionDatewaitForCompletionskipOverdue、子任务模板与跳过列表行为不变(回归规格绿灯)。
  7. Phase-3 结束条件带测试在两个 CI 时区通过;UI 结束状态是派生的,而非持久化的。

12. 涉及文件(已验证路径)

领域文件
模型task-repeat-cfg.model.ts(TaskRepeatCfgCopy
出现日引擎(保留,重指向类型化字段)store/get-next-repeat-occurrence.util.tsstore/get-newest-possible-due-date.util.tsstore/get-first-repeat-occurrence.util.tsstore/get-nth-weekday-of-month.util.tsstore/get-effective-repeat-start-date.util.tsstore/get-effective-last-task-creation-day.util.ts
确定性 ID(必须保持稳定)get-repeatable-task-id.util.ts
选择器 / 投影task-repeat-cfg.selectors.ts
服务 / 创建task-repeat-cfg.service.ts
快捷设置 / 对话框 UIdialog-edit-task-repeat-cfg/(表单常量、快捷设置更新、构建选项)
人类可读文本src/app/features/tasks/task-detail-panel/get-task-repeat-info-text.util.ts
RRULE 序列化/解析(仅边界)ical-lazy-loader.ts(复用加载器)
迁移(已修正)packages/shared-schema/src/migrations/index.ts(+index.ts)、schema-version.ts(CURRENT_SCHEMA_VERSION——注意MIN_SUPPORTED_SCHEMA_VERSION_不是_跨版本闸门,#9664)、src/app/op-log/persistence/schema-migration.service.ts
旧客户端线兼容(已在feat/rrule-epic上建成)src/app/features/task-repeat-cfg/util/legacy-cfg-to-rrule.util.tslegacyTaskRepeatCfgToRRulerruleToLegacyTaskRepeatCfgLEGACY_NEVER_FIRES_FALLBACKgetAlignedStartDate)、src/app/features/config/rrule-feature-flag.service.ts
校验 / 修复src/app/op-log/validation/createValidatedata-repair.ts
日历写入边界(注意:早于 #6040/#7726/deletedInstanceDatesARCHITECTURE-DECISIONS.md 第 9 条——日历写入位于插件中、按提供者 opt-in。CalDAV VEVENT 展开已随caldav-calendar-provider插件上线;单次出现编辑(RECURRENCE-ID/EXDATE)仍未解决,#8148

附录 A —— 竞品对比

“用户期待什么”的参考(SP 列为 2026-06 时点验证;剩余 ❌ 才是真正的目标——结束条件)。由原recurring-events-gap-analysis.md/recurring-events-industry-standards.md合并而来。

功能Google CalendarTodoistThings 3TickTickSuper Productivity
基础(日/周/月/年)
每 N 间隔
星期选择
每月第 N 个星期几✅(#6040)
每月最后一天✅(#7726)
N 次后结束❌(Phase 3)
指定日期结束❌(Phase 3)
完成后复发✅(例外)
跳过某次出现
自然语言✅(信息文本)
iCal 导出❌(Phase 1)

附录 B —— RFC 5545 RRULE 参考

iCalendar 规范(RFC 5545)的RRULE属性是类型化模型镜像、序列化器瞄准的复发标准。

核心部件

参数含义取值
FREQ(必填)频率YEARLYMONTHLYWEEKLYDAILYHOURLYMINUTELYSECONDLY
INTERVAL迭代间隔正整数(默认 1)
COUNT出现次数正整数
UNTIL结束日期(时间)DATE 或 DATE-TIME
WKST周起始日MOSU(默认MO

BYxxx 部件

参数含义取值
BYDAY星期几MOSU,可选序数前缀(2TU-1FR
BYMONTH月份1–12
BYMONTHDAY月内日序1..31 或 -31..-1(负值 = 从月末倒数)
BYYEARDAY年内日序1..366 / -366..-1
BYWEEKNOISO 周号1..53 / -53..-1
BYSETPOS集合内位置1..366 / -366..-1

BYDAY序数前缀:1MO/+1MO= 第一个周一,-1MO= 最后一个周一,2TU= 第二个周二。

示例

FREQ=DAILY;COUNT=10 # 每天,共 10 次 FREQ=WEEKLY;UNTIL=20241231T235959Z;BYDAY=MO,FR FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR # 每隔一周,周一/三/五 FREQ=MONTHLY;BYMONTHDAY=15 # 每月 15 日 FREQ=MONTHLY;BYMONTHDAY=-1 # 每月最后一天 FREQ=MONTHLY;BYDAY=2TU # 每月第二个周二 FREQ=MONTHLY;BYDAY=-1FR # 每月最后一个周五

例外:EXDATE排除出现日(= SP 的deletedInstanceDates);RDATE增加出现日(已延迟——见“真正缺失”)。

【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询