--- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/index.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # 介绍 `ts-fsrs` 是用于构建 FSRS 间隔重复系统的 TypeScript 工具包。声明式 API 将模型、时间系统和可选 middleware 组合成可复用的调度器定义。 ## 组成部分 **模型**是 FSRS 算法及其参数。`FSRS7Model` 是当前版本;FSRS-3 到 FSRS-6 仍然保留,用于基于它们积累的复习历史。 **时间系统**决定时间如何表示。`dateChrono` 使用 `Date`,`numericChrono` 使用以天为单位的数字,`temporalInstantChrono` 使用 `Temporal.Instant`。 **Middleware** 负责调度策略,例如 `schedulerLearningStepsMiddleware`、`schedulerFuzzingMiddleware`、`schedulerMaximumIntervalMiddleware`、`schedulerLeechMiddleware`。在创建调度器前用 `.use(...)` 组合它们。 `defineScheduler` 将这些组合成一个定义。`.create()` 会校验配置并返回独立的调度器,因此一个定义可以支撑多个调度器。 For your Agent 在 TypeScript 应用中集成 ts-fsrs 复制 Prompt 在我的 TypeScript 应用中集成 ts-fsrs。 1. 阅读文档索引 https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt 和快速开始 https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/quick-start.md。先检查项目是否已使用 ts-fsrs:若涉及 v5 → v6 或 FSRS-6 → FSRS-7 升级,必须阅读迁移指南 https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/migration.md,按其中的步骤和验收要求完成迁移,保留已有卡片和日志,迁移时不重新安排卡片的下次复习时间;以下安装和新卡示例仅用于新接入。 2. 安装 `ts-fsrs` package。 3. 使用 `defineScheduler`、`FSRS7Model` 和 `dateChrono` 创建 FSRS-7 调度器。 4. 创建一张新卡片,并使用 `Rating.Good` 完成一次复习。 5. 保持实现最小化,并使用当前 ts-fsrs 公共 API。 接下来前往[快速开始](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/quick-start.md),安装 package 并在浏览器中运行调度器。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/quick-start.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # 快速开始 已有应用升级请参阅 [v5 → v6 与 FSRS-7 迁移指南](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/migration.md)。 ## 安装 `ts-fsrs` 需要 Node.js 24 或更高版本。 ```sh [npm] npm install ts-fsrs ``` ```sh [yarn] yarn add ts-fsrs ``` ```sh [pnpm] pnpm add ts-fsrs ``` ```sh [bun] bun add ts-fsrs ``` ```sh [deno] deno add npm:ts-fsrs ``` 只有在训练 FSRS 参数或转换复习日志 CSV 时,才需要额外安装 `@open-spaced-repetition/binding`。 ## 从 DefaultScheduler 开始 `DefaultScheduler()` 是接入 FSRS 调度最快的方式。它默认使用 FSRS-7,并包含目标保留率、统计、学习步骤、最大间隔、单调间隔和已调度天数等标准策略;模糊可通过选项启用。 ```ts twoslash file="../../snippets/run-code/default-scheduler.ts" import { DefaultScheduler, Rating } from 'ts-fsrs' const now = new Date('2026-01-01T00:00:00.000Z') const scheduler = await DefaultScheduler({ version: 'FSRS-7', desiredRetention: 0.9, enableShortTerm: true, learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, maximumInterval: 36_500, }) const card = scheduler.newCard({ cardId: 'quick-start', now }) const result = scheduler.review({ card, grade: Rating.Good, now }) console.log( JSON.stringify( { state: result.card.state, dueAt: result.card.dueAt, stability: result.card.stability, stabilityFast: result.card.stabilityFast, difficulty: result.card.difficulty, rating: result.revlog.rating, }, null, 2 ) ) ``` ▶ 运行代码 示例显式列出了常用默认值。可以根据产品调整 `desiredRetention`、学习和重新学习步骤、模糊或 `maximumInterval`;省略 `weights` 即使用所选 FSRS 版本的预设权重。 ## 创建调度器 需要选择模型或时间系统,或者自行组合 middleware 时,再使用 `defineScheduler`。它会创建可复用的 definition;`.create()` 会校验配置并返回独立的调度器。 `chrono` 定义时间的表示方式、在卡片和复习日志中的存储方式,以及如何参与调度计算,支持使用 JavaScript `Date`、Temporal 或数值表示时间。 `chrono.normalize` 使用 `ChronoTimeNormalizer` schema,将卡片或复习日志的时间字段转换为 `{ previous, current }`,供调度和撤销使用。 ```ts twoslash file="../../snippets/run-code/define-scheduler.ts" import { dateChrono, defineScheduler, Rating } from 'ts-fsrs' import { FSRS7_DEFAULT_WEIGHTS, FSRS7Model } from 'ts-fsrs/models/fsrs-7' // Reuse this definition as the program's scheduling preset. const schedulerDefinition = defineScheduler({ model: FSRS7Model, chrono: dateChrono, }) // Create an independent scheduler when a user's config is available. const scheduler = schedulerDefinition.create({ config: { weights: FSRS7_DEFAULT_WEIGHTS, fractionalDays: true, }, }) const now = new Date('2026-01-01T00:00:00.000Z') const card = scheduler.newCard({ now }) const result = scheduler.review({ card, grade: Rating.Good, now }) console.log( JSON.stringify( { state: result.card.state, dueAt: result.card.dueAt, stability: result.card.stability, difficulty: result.card.difficulty, stabilityFast: result.card.stabilityFast, rating: result.revlog.rating, }, null, 2 ) ) ``` ▶ 运行代码 :::note 复用调度器定义 将 `defineScheduler(...)` 的结果(包括通过 `.use(...)` 组合好的 middleware)保留为程序级预设。用户或集合需要调度器时,再调用 `schedulerDefinition.create({ config })`;每次调用都会校验该配置并返回独立实例。 ::: 代码块自带的复制按钮会复制原始源码;也可以直接在专用 Worker 中运行。固定的 `now` 让输出保持可复现。 - `newCard()` 创建符合该调度器默认结构的新卡片。 - `review()` 返回新的卡片和复习日志,不会修改输入卡片。 - 在可复用 definition 上通过 `.use(...)` 组合策略 middleware,再创建用户专属的调度器。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/migration.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # 迁移到 v6 与 FSRS-7 包版本与模型版本是两件事:**ts-fsrs v5 已使用 FSRS-6**。先将应用代码和存储记录迁移到 v6 API,继续使用 FSRS-6;再单独迁移模型。本文对应本站的 v6 API,安装时应锁定与本文接口一致的 v6 版本。 :::warning 迁移后的代码不得使用 legacy API 使用 `defineScheduler` 和明确的模型入口。迁移后的代码不得使用 `fsrs()`、`FSRS`、`createEmptyCard`、`generatorParameters`、旧调度器类和策略,或根入口导出的旧 `Card`、`CardInput`、`ReviewLog`、`FSRSParameters` 等 legacy 类型。部分符号仍为兼容而导出,不代表它们是 v6 的接入方式。下文旧名称仅用于静态对照;可运行示例不使用 legacy API 或类型。 ::: For your Agent 让 Agent 完成迁移 复制 Prompt 请帮助我将当前应用迁移到 ts-fsrs v6(defineScheduler)。先询问我选择哪一个目标,并等待我的明确回答,不要默认升级模型,也不要在回答前修改代码或数据: A. 仅升级到 ts-fsrs v6,继续使用 FSRS-6,保留已有 FSRS-6 权重和记忆状态。 B. 升级到 ts-fsrs v6,并将模型从 FSRS-6 升级到 FSRS-7。 用户选定目标后,先只读执行第 1 步,结合所选目标梳理实际需要变动的点,用 - [ ] 格式输出按执行顺序排列的 TODO List。每项说明涉及的文件或模块、当前行为与计划变更、数据影响及验证方式。将清单交给用户确认并等待明确回复;确认迁移目标不等于确认执行清单,清单确认前不得修改代码、配置或数据,也不得运行有写入副作用的迁移命令。 用户确认 TODO List 后,才实际完成清单范围内的代码、必要的数据迁移脚本和测试,完成一项勾选一项;若出现清单之外的范围变化,先更新清单并再次确认,再执行新增部分。选择 A 时跳过第 6–8 步及第 9 步中仅适用于 FSRS-7 的验证,不新增 stabilityFast,也不重放历史重建 FSRS-7 状态;选择 B 时完成全部适用步骤。 1. 先阅读迁移指南:https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/migration.md,以及调度器说明:https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/scheduler/index.md、FSRS-7 说明:https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-7.md。检查仓库规则、锁定的依赖版本、全部调度调用点、配置、卡片和日志存储格式;确认实际起点,跳过已完成的阶段,不要降级已有 v6/FSRS-7 实现。 2. 先完成 v6 API 迁移;仅选择 B 时再进行模型升级,并分别验证。使用 defineScheduler({ model: FSRS6Model, chrono })、.use(...) 和 .create({ config }) 迁移 API,保留实际 FSRS-6 权重、时间策略、学习步骤、模糊及统计行为;裸定义不包含完整旧策略,按指南组合所需 middleware。类型从新调度器推导。 3. 迁移后的代码不得使用 legacy API 或类型,包括 fsrs()、FSRS、createEmptyCard、generatorParameters、旧调度器/strategy,以及从根入口导入的旧 Card、CardInput、ReviewLog、FSRSParameters;不要新增长期兼容层。保持改动聚焦,保留无关工作。 4. 迁移卡片、配置和方法调用,保留稳定 cardId 和应用元数据,校验反序列化后的时间。明确处理 scheduleStatus、State.New 的 lastReviewAt: null、字段改名、{ card, revlog } 返回值,以及 clearStatsOnForget 的默认值差异。 5. 严格区分日志时间:v6 revlog.lastReviewAt 是复习前上次复习时间,dueAt 是复习前到期时间,reviewTime 是本次实际复习时间(不是 reviewAt)。v5 普通日志的 due 来自 last_review || due,不能统一改名为 dueAt;缺失原快照时不得伪造可精确撤销的 v6 日志。 6. 仅选择 B 时:再切换 FSRS7Model,使用其独立 34 个默认权重或重新训练的 FSRS-7 权重,设置 fractionalDays: true。不得复用 FSRS-6 权重,也不得用 0.8 * stability 补造 stabilityFast。enableShortTerm 只控制显式学习步骤,不关闭 FSRS-7 快轨迹。DefaultScheduler 对 FSRS-7 默认使用 learningSteps: ['1m', '10m']、relearningSteps: ['10m'] 和 allowModelOverride: true。请显式保留原学习步骤,不要依赖默认值;验证最终间隔小于一天/至少一天、关闭/空/完成步骤及无中间件时的状态边界。独立 curve(weights, elapsedDays, state) 从 ts-fsrs/models/fsrs-7 导入,预先校验权重并读取 retrievability;total 是混合权重之和。也可使用 model.forgettingCurve 获取数值回忆概率。 7. 仅选择 B 时:对已学习卡片选取从创建或最近一次记忆重置以来的完整历史,映射 rating 和实际 reviewTime,用 Reschedule.replay() 重建 stability、stabilityFast、difficulty,不传旧模型 initialState。重建结果只更新当前 card 的 memoryState;历史 revlog 仅作为只读输入,不得修改、覆盖或补写任何历史数据。迁移时只更新当前卡片的记忆状态,不重新安排已有卡片的下次复习时间(dueAt);之后复习时再由 FSRS-7 计算新的时间。lastReviewAt、cardId、学习进度和统计保持不变。Manual 过滤不等于处理记忆重置,完整性不能仅凭行数或最后时间戳判断。 8. 仅选择 B 时:新卡使用 FSRS-7 默认记忆状态;缺失、截断、空、仅 Manual 或不一致的已学习卡片历史必须明确报错/跳过,保留 FSRS-6 状态与配置并按模型版本路由,不静默清零。保留原始日志;模型、权重配置与记忆状态一起持久化,设置跨模型撤销边界。 9. 若需修改持久化数据,提供备份、试运行和回退方式,生产数据变更遵循仓库既有流程。添加并实际运行聚焦测试:旧数据转换、四种卡片状态、复习/预览/撤销/遗忘、提前和逾期复习的三时间字段、同日小数间隔、Manual 与重置边界、缺失历史处理,以及迁移前后到期时间/ID/学习进度/统计不变。 10. 完成类型检查、相关测试和构建,报告修改文件、实际执行命令与结果、尚未迁移的数据及原因。区分已验证结果与未执行步骤;迁移目标与 TODO List 均必须经用户明确确认。除此之外,遇到其他缺失信息先检查仓库,仅对无法自行确定且影响数据正确性的事项询问用户。 ## 1. 迁移到 v6 API,保持 FSRS-6 ### 定义调度策略 `defineScheduler({ model, chrono })` 创建可复用的定义;`.use(...)` 组合中间件并返回新定义;`.create({ config })` 校验配置并创建独立调度器实例。将定义保留在程序层级,根据用户或牌组配置创建实例。 裸定义**不包含全部 v5 调度策略**。下面沿用当前标准预设的中间件顺序:目标保留率、模糊、统计、已调度天数、学习步骤、最大间隔、单调间隔。请保留实际配置和 FSRS-6 权重。使用 `dateChrono` 时,`fractionalDays: false` 按 UTC 日历日计算间隔;仅迁移 API 时不要未经验证就同时改变时间策略。 ```ts twoslash file="../../snippets/run-code/migration-v6.ts" fold height=520 import { dateChrono, defineScheduler, Rating, State } from 'ts-fsrs' import { schedulerDesiredRetentionMiddleware, schedulerFuzzingMiddleware, schedulerLearningStepsMiddleware, schedulerMaximumIntervalMiddleware, schedulerMonotonicIntervalMiddleware, schedulerScheduledDaysMiddleware, schedulerStatsMiddleware, } from 'ts-fsrs/middlewares' import { FSRS6Model, migrateFSRS6Parameters } from 'ts-fsrs/models/fsrs-6' // Keep the definition at application scope; create an instance per config. export const definition = defineScheduler({ model: FSRS6Model, chrono: dateChrono, }).use( schedulerDesiredRetentionMiddleware, schedulerFuzzingMiddleware, schedulerStatsMiddleware, schedulerScheduledDaysMiddleware, schedulerLearningStepsMiddleware, schedulerMaximumIntervalMiddleware, schedulerMonotonicIntervalMiddleware ) export const scheduler = definition.create({ config: { weights: migrateFSRS6Parameters(), // Pass the user's existing FSRS-6 weights here. fractionalDays: false, enableShortTerm: true, numRelearningSteps: 1, // Keep equal to relearningSteps.length. desiredRetention: 0.9, learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, maximumInterval: 36500, clearStatsOnForget: false, // Match v5 forget's default counter behavior. }, }) export type Card = ReturnType export type Revlog = ReturnType['revlog'] // An application's stored v5 record, not a legacy library type. type StoredV5Card = { due: string last_review?: string | null stability: number difficulty: number state: State scheduled_days: number learning_steps: number reps: number lapses: number } export function convertCard( stored: StoredV5Card, cardId: Card['cardId'] ): Card { const scheduleStatus = { [State.New]: 'new', [State.Learning]: 'learning', [State.Review]: 'review', [State.Relearning]: 'learning', } as const return definition.schema.card.parse({ cardId, dueAt: new Date(stored.due), lastReviewAt: stored.state === State.New || stored.last_review == null ? null : new Date(stored.last_review), stability: stored.stability, difficulty: stored.difficulty, state: stored.state, scheduleStatus: scheduleStatus[stored.state], scheduledDays: stored.scheduled_days, learningStep: stored.learning_steps, reps: stored.reps, lapses: stored.lapses, }) } export const storedCard = { due: '2026-01-03T00:00:00.000Z', last_review: '2026-01-01T00:00:00.000Z', stability: 2, difficulty: 5, state: State.Review, scheduled_days: 2, learning_steps: 0, reps: 3, lapses: 0, } satisfies StoredV5Card const card = convertCard(storedCard, 'card-1') // An overdue review: previous review, original due date and now are distinct. const now = new Date('2026-01-04T00:00:00.000Z') const result = scheduler.review({ card, grade: Rating.Good, now }) const restored = scheduler.rollback({ card: result.card, revlog: result.revlog, }) console.log( JSON.stringify( { cardId: result.card.cardId, rating: result.revlog.rating, reps: result.card.reps, revlog: { lastReviewAt: result.revlog.lastReviewAt, dueAt: result.revlog.dueAt, reviewTime: result.revlog.reviewTime, }, rollbackDueAt: restored.dueAt, }, null, 2 ) ) ``` ▶ 运行代码 局部类型 `StoredV5Card` 描述应用存储格式,并非从 legacy 库导入。将数据库现有稳定 ID 作为 `cardId`,应用自有字段单独保留。组合后的卡片 schema 会校验转换后的日期和字段;其他应用字段也应在存储读取边界校验。 ### 配置对照 | v5 配置 | v6 配置 | 迁移说明 | | ------------------------------------- | ----------------------------------- | --------------------------------------- | | `w` | `weights` | 此阶段保留用户的 FSRS-6 权重;示例为方便运行才使用默认值。 | | `request_retention` | `desiredRetention` | 需要目标保留率中间件。 | | `maximum_interval` | `maximumInterval` | 需要最大间隔中间件;单调间隔策略也使用此上限。 | | `enable_fuzz` | `enableFuzz` | 需要模糊中间件,保留旧设置。 | | `enable_short_term` | `enableShortTerm` | 对 FSRS-6,同时影响模型短期行为和显式学习步骤。 | | `learning_steps` / `relearning_steps` | `learningSteps` / `relearningSteps` | 保留配置的步骤数组和已有学习进度。 | | 重新学习步骤数量 | `numRelearningSteps` | FSRS-6 需设为 `relearningSteps.length`。 | | `forget(..., reset_count)` | `clearStatsOnForget` | 改为实例配置;v5 默认 `false`,v6 统计中间件默认 `true`。 | ### 卡片、日志与操作 | v5 存储字段 | v6 字段或处理方式 | | ------------------------------------------------ | -------------------------------------------------------------------------------- | | `due` | 卡片 `dueAt`,反序列化为 `Date`。 | | `elapsed_days` / 日志 `last_elapsed_days` | 已移除,使用所选 chrono 从时间戳计算经过天数;FSRS-7 重放不要使用这些旧的取整值。 | | `last_review` | `lastReviewAt`,缺失或 null 统一为 `null`。 | | `scheduled_days` | `scheduledDays`,由中间件提供。 | | `learning_steps` | `learningStep`,由中间件提供。 | | `stability`、`difficulty`、`state`、`reps`、`lapses` | API 迁移时保留;统计字段需要中间件。 | | 应用记录 ID | `cardId`,卡片和日志使用同一 ID。 | | 无对应字段 | `scheduleStatus`:New → `new`,Learning/Relearning → `learning`,Review → `review`。 | | 复习日志 `review` | `reviewTime`,即实际复习时间,不是卡片到期时间。 | | 结果 `{ card, log }` | `{ card, revlog }`。 | 像示例一样从调度器推导类型。卡片和日志字段取决于所选模型、chrono 和中间件。自定义 v5 策略需改写为相应的[中间件](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/middleware.md) hook,不能仅改名或继续继承旧调度器。 | v5 操作(仅供对照) | v6 操作 | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `createEmptyCard(now)` | `scheduler.newCard({ now, cardId })` | | `f.next(card, now, grade)` | `scheduler.review({ card, now, grade })` | | `f.repeat(card, now)` | `scheduler.preview({ card, now })` 返回可迭代对象,通过 `Symbol.iterator` 提供 iterator,可用 `for...of` 遍历。每个候选项包含 `{ grade, card, revlog }`,通过 `candidate.grade` 选择,不再按评分键索引对象。 | | `f.rollback(card, log)` | `scheduler.rollback({ card, revlog })`,使用返回的 v6 日志。 | | `f.forget(card, now, reset_count)` | `scheduler.forget({ card, now })`;返回卡片,不是 `{ card, log }`,手动操作日志由应用记录。 | | `f.get_retrievability(card, now, false)` | 对有 `lastReviewAt` 的已学习卡片,调用 `scheduler.model.forgettingCurve(card, scheduler.chrono.difference(card.lastReviewAt, now))`,返回数值;新卡片单独处理,百分比由应用格式化。 | `review()` 返回新数据,应同时持久化 `result.card` 和 `result.revlog`。新日志保存撤销所需的复习前字段,包括 `dueAt`、`lastReviewAt`。不能将旧日志的 `due` 简单改名就用于 v6 撤销:旧日志可能缺少必要的复习前快照。保留原始历史记录,仅转换评分和实际复习时间用于模型重放。跨版本撤销需要独立的应用快照或撤销边界。 即使模型和配置相同,也不保证所有学习步骤或模糊场景的间隔与 v5 完全一致。切换写入前应检查 New、Learning、Review、Relearning 的代表卡片。[DefaultScheduler](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/scheduler/default-scheduler.md) 是另一种预设入口,但默认模型为 FSRS-7;若在此阶段采用它,应明确指定 FSRS-6。 ### 为什么日志中的三个时间不能直接改名 `dateChrono` 的字段名是 **`reviewTime`,不是 `reviewAt`**。以逾期复习为例:卡片上次在 1 月 1 日复习,原定 1 月 3 日到期,实际在 1 月 4 日复习。 | 含义 | v5 普通复习日志 | v6 `revlog` | | ---------------- | --------------------------------------------- | -------------- | | 上次复习时间:1 月 1 日 | `due`(旧实现写入 `card.last_review \|\| card.due`) | `lastReviewAt` | | 原定到期时间:1 月 3 日 | 对此已学习卡片没有单独记录 | `dueAt` | | 本次实际复习时间:1 月 4 日 | `review` | `reviewTime` | `lastReviewAt` 表示**本次复习之前的上次复习时间**,输入与输出的类型有所区别: - 输入 `DateRevlogInputFields` 中是可选的 `lastReviewAt?: Date | null`。省略或传入 `undefined` 时,schema 会将其规范化为 `null`;`dueAt` 和 `reviewTime` 仍必须是有效的 `Date`。 - 输出 `DateRevlogFields` 中始终存在 `lastReviewAt: Date | null`。正常的已学习卡片日志记录复习前的 `card.lastReviewAt`;没有上次复习的新卡片,首次复习日志中的值为 `null`。 - `null` 不会自动推算出缺失的上次复习时间。旧日志缺少该信息时,能通过 schema 校验不等于能精确撤销;不要用 `dueAt` 或 `reviewTime` 代替它。 复习返回的卡片中,`lastReviewAt` 已变为 1 月 4 日,`dueAt` 是新计算的到期时间;日志保存的是**复习前**的时间。使用这张卡片和 v6 日志撤销后,`dueAt` 恢复为 1 月 3 日,`lastReviewAt` 恢复为 1 月 1 日,提前或逾期复习也一样。这正是 chrono 重构时分离「计算经过天数的时间」与「供撤销恢复的原到期时间」的原因。上面的可运行示例会打印这三个字段。 不能统一执行 `oldLog.due → revlog.dueAt`:普通 v5 已学习卡片日志的 `due` 通常是上次复习时间;无 `last_review` 的首次复习日志才存原到期时间,Manual/遗忘日志又有自己的语义。v5 重置后的 New 卡片还可能携带旧 `last_review`,因此卡片转换示例对 `State.New` 统一输出 `lastReviewAt: null`。 只重建模型记忆时,仅映射 `rating` 和 `review → reviewTime` 即可。若要跨版本精确撤销,需要原始复习前卡片快照(包括两个时间字段及中间件字段),否则应设立撤销边界。不能用 `review` 或重新计算的间隔补造过去未记录的到期时间,尤其是在提前复习、学习步骤、模糊或手动调整之后。 ## 2. 从 FSRS-6 迁移到 FSRS-7 ### 权重和状态属于各自的模型 FSRS-6 使用 21 个权重,FSRS-7 需要独立的 **34 个权重**。使用 `FSRS7_DEFAULT_WEIGHTS`,或[重新训练 FSRS-7 权重](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/optimizer.md)。不要给 FSRS-6 参数补值,也不要复用其训练权重。`migrateFSRS7Parameters` 仅为空输入提供默认值,不会转换旧模型的参数布局。 FSRS-7 记忆状态包含 `stability`、`stabilityFast` 和 `difficulty`,三个字段都应通过历史重建。**重建结果只用于更新当前 card 的记忆状态(memoryState);历史 revlog 仅作为只读输入,不得修改、覆盖或补写其中的任何数据,包括过去记录的记忆状态。** **不得为已有卡片直接设置 `stabilityFast = 0.8 * stability`**,也不能保留 FSRS-6 的稳定性、难度后只追加快分量。模型初始化使用的系数不是已演化记忆状态的迁移公式。 设置 `fractionalDays: true`,保留两次复习相隔十分钟等小数天数。FSRS-7 的快轨迹是模型固有部分:安装学习步骤中间件后,`enableShortTerm: false` 仅关闭显式学习步骤,不会关闭快轨迹。FSRS-7 模型没有与 FSRS-6 的 `numRelearningSteps` 对应的配置项。 ### FSRS-7 的默认步骤与学习状态 `DefaultScheduler` 对 FSRS-7 与 FSRS-3~6 一样默认使用学习步骤 `['1m', '10m']` 与重新学习步骤 `['10m']`,并开启 `allowModelOverride`,让更长的模型间隔覆盖当前步骤。显式传入的步骤不会被默认值覆盖。`enableShortTerm` 仍默认 `true`,控制显式步骤,不控制 FSRS-7 快轨迹或按间隔划分的学习状态。 注册 `schedulerLearningStepsMiddleware` 后,状态由**最终调度间隔**决定,即使步骤关闭、为空或已完成: | 复习前状态 | 最终间隔小于 `graduatingInterval`(默认一天) | 最终间隔至少 `graduatingInterval` | | ------------------- | ------------------------------------- | ------------------------------- | | New / Learning | Learning,`scheduleStatus: learning` | Review,`scheduleStatus: review` | | Review / Relearning | Relearning,`scheduleStatus: learning` | Review,`scheduleStatus: review` | 配置了步骤时,没有适用步骤的 Hard、Good、Easy 至少在 `graduatingInterval` 之后到期,因此走完步骤,以及 `Review` 卡片按 Hard、Good 或 Easy,都会进入 `Review`。开启 `allowModelOverride` 时,使用模型间隔的 Again 总是进入 `Learning`/`Relearning`,即使其间隔被限制为 `graduatingInterval`。 没有配置步骤时,不能假定「没有 Learning」「只有 Again 才进入 Relearning」或「Easy 一定进入 Review」。一般来说,Easy 跳过显式步骤,Good 仅在存在适用的下一步骤时推进,时长为 0 的步骤视为没有步骤。`learningStep` 只在卡片留在 Learning/Relearning 时保留下一步骤,卡片进入 Review 时重置为 `0`;它是步骤索引,不是复习次数。 这些转换属于中间件,不属于 `FSRS7Model`。只组合模型和 chrono 的裸调度器仍返回 Review,即使间隔小于一天。迁移应用队列、状态筛选和统计时,需要同步调整依赖旧行为的判断。 [“重放记忆,保留调度安排”一节的完整迁移示例](#重放记忆保留调度安排)**显式保留已有学习步骤**,没有依赖默认值。若产品决定完全采用模型驱动的步骤,可明确配置 `learningSteps: []`、`relearningSteps: []`;这不等于移除学习步骤中间件。改变步骤策略应单独验证,不能当作卡片字段迁移顺便执行。 ### 迁移回忆概率调用 使用 `scheduler.model.forgettingCurve(state, elapsedDays)` 获取数值回忆概率,或 `scheduler.model.algorithm.curve(elapsedDays, state)` 检查快慢分量。两者均需完整 FSRS-7 状态:`stability`、`stabilityFast`、`difficulty`,不能只传 FSRS-6 的单个稳定性值。 也可以从 `ts-fsrs/models/fsrs-7` 导入独立函数 `curve`,调用 `curve(weights, elapsedDays, state)`,无需创建算法实例;其中 `weights` 必须是有效的 34 个 FSRS-7 权重。**该函数不执行权重校验**:对存储或用户输入先做 schema/参数校验,不要用它探测权重是否有效。使用已迁移的记忆状态与同一份 FSRS-7 权重,`elapsedDays` 保留小数天数。 曲线结果包括 `retrievability`、`derivative`、`fast`、`slow`、`fastWeight`、`slowWeight` 和 `total`;`total = fastWeight + slowWeight` 是混合系数之和,不是总回忆概率。替代旧回忆概率调用时读取 `retrievability`,百分比由应用格式化。算法实例的曲线结果也包含 `total`。这些曲线接口只计算概率与分量,不迁移卡片、生成日志或安排到期时间。 ### 重放记忆,保留调度安排 下面是独立可运行示例。它接收已转换为 v6 结构的 FSRS-6 卡片,将存储日志的 `review` 转成 `reviewTime`,调用 `Reschedule.replay()`,不传入旧模型的 `initialState`。**迁移时只重建记忆状态,不重新安排已有卡片的下次复习时间(`dueAt`)**。之后用户复习该卡片时,再由 FSRS-7 计算新的下次复习时间。ID、上次复习时间、学习进度与统计也保持不变。 ```ts twoslash file="../../snippets/run-code/migration-fsrs7.ts" fold height=520 import { dateChrono, defineScheduler, Rating, State } from 'ts-fsrs' import { schedulerDesiredRetentionMiddleware, schedulerFuzzingMiddleware, schedulerLearningStepsMiddleware, schedulerMaximumIntervalMiddleware, schedulerMonotonicIntervalMiddleware, schedulerScheduledDaysMiddleware, schedulerStatsMiddleware, } from 'ts-fsrs/middlewares' import { FSRS7_DEFAULT_WEIGHTS, FSRS7Model } from 'ts-fsrs/models/fsrs-7' import { Reschedule } from 'ts-fsrs/reschedule' export const definition = defineScheduler({ model: FSRS7Model, chrono: dateChrono, }).use( schedulerDesiredRetentionMiddleware, schedulerFuzzingMiddleware, schedulerStatsMiddleware, schedulerScheduledDaysMiddleware, schedulerLearningStepsMiddleware, schedulerMaximumIntervalMiddleware, schedulerMonotonicIntervalMiddleware ) export const scheduler = definition.create({ config: { weights: FSRS7_DEFAULT_WEIGHTS, fractionalDays: true, enableShortTerm: true, // Controls learning steps, not the model's fast trace. desiredRetention: 0.9, // Preserve existing steps explicitly instead of inheriting preset defaults. learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, maximumInterval: 36500, clearStatsOnForget: false, }, }) type Card = ReturnType type PreviousCard = Omit type StoredReview = { rating: Rating; review: string } // completeHistory means all reviews since creation or the latest memory reset. // Decide this from application records, not from the number of rows alone. export function migrateMemory( card: PreviousCard, logs: readonly StoredReview[], completeHistory: boolean ): Card { if (card.state === State.New) { const { stability, stabilityFast, difficulty } = scheduler.newCard({ cardId: card.cardId, }) return definition.schema.card.parse({ ...card, stability, stabilityFast, difficulty, }) } if (!completeHistory) throw new Error('Keep this card on FSRS-6: incomplete history') const history = logs .filter(({ rating }) => rating !== Rating.Manual) .map(({ rating, review }) => ({ rating, reviewTime: new Date(review), })) // replay validates dates/ratings and sorts the non-manual history. // Empty or Manual-only histories throw instead of clearing memory. const { memoryState } = new Reschedule(scheduler).replay({ history }) const latestReview = history.reduce( (latest, { reviewTime }) => Math.max(latest, reviewTime.getTime()), -Infinity ) if (card.lastReviewAt?.getTime() !== latestReview) { throw new Error( 'Keep this card on FSRS-6: history does not match lastReviewAt' ) } // Only the model memory changes; dueAt, counters and learning progress survive. return definition.schema.card.parse({ ...card, ...memoryState }) } export const previousCard: PreviousCard = { cardId: 'card-1', dueAt: new Date('2026-01-05T00:00:00.000Z'), lastReviewAt: new Date('2026-01-02T00:00:00.000Z'), stability: 2, difficulty: 5, state: State.Review, scheduleStatus: 'review', scheduledDays: 3, learningStep: 0, reps: 3, lapses: 0, } export const storedHistory: StoredReview[] = [ { rating: Rating.Good, review: '2026-01-01T00:00:00.000Z' }, { rating: Rating.Good, review: '2026-01-01T00:10:00.000Z' }, { rating: Rating.Good, review: '2026-01-02T00:00:00.000Z' }, ] const migrated = migrateMemory(previousCard, storedHistory, true) console.log( JSON.stringify( { model: 'FSRS-7', cardId: migrated.cardId, dueAt: migrated.dueAt, reps: migrated.reps, stabilityFast: migrated.stabilityFast, }, null, 2 ) ) ``` ▶ 运行代码 | 卡片或历史情况 | 处理方式 | | ----------------------------------- | ----------------------------------------- | | 新卡片(`State.New`),含主动重置后的卡片 | 使用 FSRS-7 默认记忆状态,保留调度安排与统计。 | | 已学习卡片,具有从创建或最近一次记忆重置以来的完整历史 | 使用 FSRS-7 权重重放,保存重建状态并保留调度安排。 | | 历史缺失或被截断 | 卡片及其匹配配置继续使用 FSRS-6;不得静默清零,也不能仅凭行数宣称历史完整。 | | 已学习卡片的历史为空或仅有 Manual | 重放抛错,卡片继续使用 FSRS-6。 | | 日期或评分无效,或最后一次复习与 `lastReviewAt` 不匹配 | 停止该卡片迁移,先核对记录。 | `replay()` 按时间排序有效复习并过滤 `Rating.Manual`,但不会解释手动遗忘或重置事件。应用必须选取**最近一次记忆重置之后**的完整历史,排除更早记录;仅手动调整到期时间不等于重置记忆。同一时间戳的复习应保留记录顺序。使用 `dateChrono` 时,时间需反序列化为有效 `Date`。首条保留的复习从未初始化的记忆状态开始,经过天数为零。 示例的历史完整标志由应用判断,最后时间戳匹配并不能证明更早的复习没有缺失。迁移失败的卡片应按持久化的模型版本继续路由到 FSRS-6,不要将它们传给 FSRS-7。 ### 如果确实需要重新安排到期时间 `new Reschedule(scheduler).reschedule({ history, initialCard })` 会通过整套调度策略重放,返回 `{ card, results }`,可能改变到期时间、学习进度和统计。应以保留原 `cardId`、使用首条复习时间的 **FSRS-7 新卡片**作为 `initialCard`,不能在已有已学习卡片上再重放完整历史。生成的日志是重新计算的结果,不应替换原始历史。本文默认使用 `replay()`,迁移时不重新安排已有卡片的下次复习时间。 ## 持久化与验收 1. 备份卡片、原始日志和配置。为每张卡片或与之可靠关联的配置保存模型版本,以及重放实际使用的权重。 2. 增加 `stabilityFast` 和 v6 字段的存储,将重建记忆、模型版本及对应权重/配置引用一并提交,避免混用 FSRS-6 状态和 FSRS-7 权重。保留应用 ID 与元数据。 3. 保持原始日志不变,并建立撤销边界:旧模型日志不得直接用于 FSRS-7 `rollback()`。新的 FSRS-7 日志应保存 `stabilityFast` 和复习前字段。 4. 使用代表卡片验证复习、预览、撤销、同日复习、学习步骤及配置的模糊行为。仅迁移记忆时,断言到期时间、ID、学习进度和统计不变。处理失败后再切换相应卡片的模型。 模型细节见 [FSRS-7](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-7.md),API 组成见[调度器定义](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/scheduler.md)、[Chrono](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/chrono.md) 与[中间件](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/middleware.md)。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/architecture.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # 架构 ts-fsrs v6 的调度架构围绕 Model、Chrono、Middleware 和 Scheduler 组织,将记忆模型、时间处理与调度策略分开,并由 Scheduler 协调执行。 ## 职责划分 | 组成部分 | 职责 | | ---------- | ------------------------------------------------------------------------ | | Model | 通过 `step` 更新记忆状态,通过 `nextInterval` 计算基础间隔;FSRS-6 的记忆状态包含稳定性和难度。 | | Chrono | 定义时间表示与时间运算。`dateChrono` 使用 JavaScript `Date`,提供时间差计算和日期加减。 | | Middleware | 添加调度策略,并可扩展 config、card 与 revlog 的 schema,例如目标保留率、学习步骤和最大间隔。 | | Scheduler | 组合 schema 与默认值,校验配置和输入,协调 `newCard`、`review`、`preview` 和 `rollback` 等操作。 | Model 的记忆状态(如稳定性和难度)不同于 New、Learning 等卡片状态。卡片的学习状态与调度策略由 Scheduler 和 Middleware 共同处理。 应用负责选卡、保存返回的 card 与 revlog,以及管理用户和集合。调度器不会自动持久化结果。 ## 调度器定义与实例 `defineScheduler({ model, chrono })` 创建定义。`.use(...)` 返回包含新增 middleware 及其推导类型的新定义,原定义仍可复用。`.create({ config })` 校验用户配置并创建独立的调度器实例。 :::note 复用完整定义 将包含 `.use(...)` 组合的定义保留为程序级预设,在用户或集合的配置可用时创建实例。 ::: ```ts twoslash file="../../snippets/run-code/architecture.ts" import { dateChrono, defineScheduler, Rating } from 'ts-fsrs' import { schedulerDesiredRetentionMiddleware, schedulerMaximumIntervalMiddleware, } from 'ts-fsrs/middlewares' import { FSRS6_DEFAULT_WEIGHTS, FSRS6Model } from 'ts-fsrs/models/fsrs-6' // Keep the model, chronology, and policies as a reusable program preset. const schedulerDefinition = defineScheduler({ model: FSRS6Model, chrono: dateChrono, }).use(schedulerDesiredRetentionMiddleware, schedulerMaximumIntervalMiddleware) // Supply each user's configuration when creating their scheduler. const scheduler = schedulerDefinition.create({ config: { weights: FSRS6_DEFAULT_WEIGHTS, enableShortTerm: true, numRelearningSteps: 1, desiredRetention: 0.9, maximumInterval: 1, }, }) const now = new Date('2026-01-01T00:00:00.000Z') const card = scheduler.newCard({ now }) const result = scheduler.review({ card, grade: Rating.Good, now }) console.log( JSON.stringify( { originalState: card.state, state: result.card.state, dueAt: result.card.dueAt, stability: result.card.stability, rating: result.revlog.rating, }, null, 2 ) ) ``` ▶ 运行代码 示例刻意将最大间隔设为一天。模型仍计算出 `2.3065` 的稳定性,最大间隔策略将到期时间调整为 1 月 2 日。原卡片保持 New,返回的卡片为 Review。这个最小预设未加入学习步骤、统计或 fuzzing。 ## 一次 review 的数据流 1. 将 `card`、`grade` 和 `now` 传给 `review`。调度器校验输入,并通过 Chrono 计算已过时间。 2. Middleware 围绕核心操作运行。目标保留率 middleware 在计算间隔前提供保留率目标。 3. 核心操作计算新的记忆状态,并在需要时调用模型计算基础间隔;Middleware 可以提供或调整最终调度间隔。 4. 整条链返回后,调度器通过 Chrono,根据复习时间和最终间隔计算 `dueAt`,返回新的 card 与 revlog。Card 包含新的记忆状态;revlog 保留复习前的记忆状态,以及本次评分和复习时间。 ## Middleware 顺序 对于 `.use(A, B)`,当各 review handler 都调用 `next()` 时,执行顺序为: `A 的 next() 前 → B 的 next() 前 → 核心操作 → B 的 next() 后 → A 的 next() 后` `next()` 前的代码准备操作,之后的代码读取下游处理结果。因此注册顺序会影响哪个策略最终生效。示例中,目标保留率在核心操作前设置,最大间隔在其后限制结果。加入学习步骤时,应将最大间隔 middleware 注册在学习步骤 middleware 之后,学习步骤 middleware 会在返回阶段恢复配置的学习间隔,避免它被按天计算的间隔策略覆盖。 卡片生命周期见[状态转换](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/state-transitions.md),数学细节见 [FSRS-6 公式](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-6.md)。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/state-transitions.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # 状态转换 `State` 表示卡片当前所处的 FSRS 学习阶段。通过 `scheduler.newCard()` 创建的卡片初始状态为 `New`。每次复习时,`DefaultScheduler` 会结合 FSRS 模型结果与学习步骤策略,确定下一个状态和到期时间。 ![DefaultScheduler 状态转换流程图](/ts-fsrs/static/svg/state-transitions.zh-CN.e6d23aec57.svg) 图表按显式步骤和间隔中间件处理后的最终调度间隔分类。评分会影响间隔,但不能单独决定下一个状态。 `graduatingInterval`(默认 `1`,单位为天)是卡片处于 `Review` 时的最短间隔。最终间隔短于 `graduatingInterval` 时进入或保持 `Learning`/`Relearning`;达到 `graduatingInterval` 时进入 `Review`。步骤被关闭或为空、由模型提供间隔时,也适用这一规则。 有一个例外:Again 使用模型间隔覆盖显式步骤时,即使最终间隔达到 `graduatingInterval`,卡片仍保持 `Learning`/`Relearning`。流程图展示的是一般的间隔映射规则,此例外优先于图中的规则。 零时长步骤(如 `0m`,或按秒四舍五入后为零的时长)视为没有步骤,不支持负数时长。 ## 四种状态 | 状态 | 含义 | 默认队列 | | ------------ | ------------------------------------------------- | ---------- | | `New` | 卡片是新卡,或已通过 `scheduler.forget()` 重置。 | `new` | | `Learning` | 卡片处于初始学习阶段,需要再次复习,间隔通常短于 `graduatingInterval`。 | `learning` | | `Review` | 卡片不处于短期学习阶段,间隔至少为 `graduatingInterval`。 | `review` | | `Relearning` | 已复习过的卡片正在重新学习,需要再次复习,间隔通常短于 `graduatingInterval`。 | `learning` | ## 评分如何改变状态 评分首先决定是否使用显式步骤间隔。下表假设 `enableShortTerm: true`,且当前状态使用的步骤列表非空:`New`/`Learning` 使用 `learningSteps`,`Review`/`Relearning` 使用 `relearningSteps`。如果该列表为空,即使另一列表配置了步骤,调度器也使用模型间隔而不应用毕业下限。 下文的毕业行为假设 `maximumInterval >= graduatingInterval`,且没有其他中间件将间隔缩短到 `graduatingInterval` 以下。 | 评分 | `New` / `Learning` / `Relearning` | `Review` | | ------- | ---------------------------------------------------------- | --------------------------------------------- | | `Again` | 回到第一个步骤,`learningStep` 重置为 `0`。 | 使用第一个重新学习步骤,`learningStep` 重置为 `0`。 | | `Hard` | 保持当前 `learningStep`,使用 Hard 步骤间隔。 | 使用模型间隔,但不短于 `graduatingInterval`;保持 `Review`。 | | `Good` | 存在下一步时推进;否则毕业进入 `Review`,使用模型间隔,但不短于 `graduatingInterval`。 | 使用模型间隔,但不短于 `graduatingInterval`;保持 `Review`。 | | `Easy` | 跳过显式步骤,毕业进入 `Review`,使用模型间隔,但不短于 `graduatingInterval`。 | 使用模型间隔,但不短于 `graduatingInterval`;保持 `Review`。 | Hard 步骤间隔为前两个步骤时长的平均值;只有一个步骤时,为该步骤时长的 1.5 倍。步骤为空或 `enableShortTerm: false` 时,所有评分都使用模型间隔,只适用下方的状态映射。`learningStep` 只在卡片留在 `Learning`/`Relearning` 时保留下一步骤,卡片进入 `Review` 时重置为 `0`。 开启 `allowModelOverride`(`DefaultScheduler` 对 FSRS-7 默认开启)时,如果模型间隔比当前步骤长,调度器改用模型间隔。Again 的模型间隔不超过 `graduatingInterval`,使用模型间隔的 Again 总是进入 `Learning`/`Relearning`。Hard 和 Good 的间隔达到 `graduatingInterval` 时,卡片不必走完剩余步骤即毕业进入 `Review`。模型间隔仍可能被其他间隔中间件调整。 如果 `maximumInterval < graduatingInterval`,上限可能阻止毕业。例如,设置 `graduatingInterval: 2` 和 `maximumInterval: 1` 时,走完学习步骤后的模型间隔被限制为 1 天,因此卡片仍处于 `Learning`。 间隔确定后,`schedulerLearningStepsMiddleware` 按下表决定状态。上文的 Again 模型覆盖例外优先于此表。下表描述状态转换,不代表各评分使用相同的间隔选择规则: | 当前状态 | 最终间隔 \< `graduatingInterval` | 最终间隔 ≥ `graduatingInterval` | | ------------ | ---------------------------- | --------------------------- | | `New` | `Learning` | `Review` | | `Learning` | `Learning` | `Review` | | `Review` | `Relearning` | `Review` | | `Relearning` | `Relearning` | `Review` | 有适用的显式步骤时,调度器保留该步骤配置的时长,除非 `allowModelOverride` 选用了更长的模型间隔。达到 `graduatingInterval` 的步骤仍会让卡片进入 `Review`。 :::warning Learning 状态依赖中间件 `DefaultScheduler` 自动注册 `schedulerLearningStepsMiddleware`,且 `enableShortTerm` 默认为 `true`,FSRS-7 也一样。对于 FSRS-7,该开关控制显式 `learningSteps` 和 `relearningSteps`,不关闭模型内在的短期记忆,也不控制按间隔进行的状态映射。即使 `enableShortTerm: false`,模型间隔小于 `graduatingInterval` 时仍会进入 `Learning` 或 `Relearning`。使用 `defineScheduler` 时,需注册 `.use(schedulerLearningStepsMiddleware)` 才会应用此状态映射;只组合 FSRS-7 模型而未注册该中间件时,即使调度间隔小于一天,`state` 仍为 `Review`,`scheduleStatus` 仍为 `review`。模型负责记忆状态和间隔计算,学习状态转换由中间件负责。如果要让 FSRS-7 调度后显示 `Learning`/`Relearning`,请挂载 `schedulerLearningStepsMiddleware`,或自行实现负责状态转换的 middleware。 ::: `Rating.Manual` 不是复习评分。调用 `review()` 时应传入 `Again`、`Hard`、`Good` 或 `Easy`。 ## 运行状态转换 下方的可运行示例使用默认 FSRS-7 模型和以下配置: ```ts const scheduler = await DefaultScheduler({ learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, allowModelOverride: false, }) ``` `graduatingInterval` 使用默认值 **1 天**。卡片初始状态为 `New`,`learningStep: 0`。`learningStep` 是从零开始的步骤索引,不是复习次数;学习和重新学习都使用同一个字段记录步骤进度。 设置 `allowModelOverride: false` 时,示例产生以下转换: | 当前状态 | 当前 learningStep | 评分 | 转换后状态 | 转换后 learningStep | 间隔 | | ---------- | --------------- | ----- | ---------- | ---------------- | ------ | | New | 0 | Again | Learning | 0 | 1 分钟 | | Learning | 0 | Good | Learning | 1 | 10 分钟 | | Learning | 1 | Hard | Learning | 1 | 5.5 分钟 | | Learning | 1 | Again | Learning | 0 | 1 分钟 | | Learning | 1 | Good | Review | 0 | 1 天 | | Review | 0 | Again | Relearning | 0 | 10 分钟 | | Relearning | 0 | Good | Review | 0 | 1 天 | `learningStep: 1` 的 Hard、Again 和 Good 行是对同一张卡片的不同评分分支,并非连续复习。重新学习分支从另一张通过 Easy 进入 `Review` 的卡片开始。 关闭 `allowModelOverride` 会保留配置的步骤间隔。开启后,更长的模型间隔可以替代显式步骤。例如,假设模型间隔为 30 分钟,原本 10 分钟的 Good 步骤就会延长到 30 分钟;如果模型间隔更短,则该步骤保持不变。即使设置 `allowModelOverride: false`,调度器在步骤完成后或没有适用步骤时仍会使用模型间隔。 下方完整示例使用公开的 `DefaultSchedulerCard`、`Grade` 和 `State` 类型,输出间隔以分钟显示,保留两位小数。 :::note dueAt 不是 now `dueAt` 表示卡片应从何时起复习,`now` 是实际复习时间,通常应满足 `now >= dueAt`。为了让输出可重复且不混淆两者,本例在每张卡片的 `dueAt` 后 30 秒进行复习。 ::: ```ts twoslash file="../../snippets/run-code/state-transitions.ts" import { DefaultScheduler, type DefaultSchedulerCard, type Grade, Rating, State, } from 'ts-fsrs' const scheduler = await DefaultScheduler({ learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, allowModelOverride: false, }) const stateNames: Record = { [State.New]: 'New', [State.Learning]: 'Learning', [State.Review]: 'Review', [State.Relearning]: 'Relearning', } const reviewAfterDue = (card: DefaultSchedulerCard, grade: Grade) => { const reviewedAt = new Date(card.dueAt.getTime() + 30_000) return scheduler.review({ card, grade, now: reviewedAt }).card } const newCard = scheduler.newCard({ cardId: 'state-transitions', now: new Date('2026-01-01T00:00:00.000Z'), }) const learning = reviewAfterDue(newCard, Rating.Again) const nextLearningStep = reviewAfterDue(learning, Rating.Good) const afterLearningSteps = reviewAfterDue(nextLearningStep, Rating.Good) const repeated = reviewAfterDue(nextLearningStep, Rating.Hard) const restarted = reviewAfterDue(nextLearningStep, Rating.Again) const reviewed = reviewAfterDue(newCard, Rating.Easy) const relearning = reviewAfterDue(reviewed, Rating.Again) const afterRelearningSteps = reviewAfterDue(relearning, Rating.Good) for (const [from, rating, to] of [ [newCard, 'Again', learning], [learning, 'Good', nextLearningStep], [nextLearningStep, 'Hard', repeated], [nextLearningStep, 'Again', restarted], [nextLearningStep, 'Good', afterLearningSteps], [newCard, 'Easy', reviewed], [reviewed, 'Again', relearning], [relearning, 'Good', afterRelearningSteps], ] as const) { console.log( `${stateNames[from.state]} --${rating}--> ${stateNames[to.state]}; ` + `learningStep: ${from.learningStep} -> ${to.learningStep}; ` + `interval: ${(to.scheduledDays * 1440).toFixed(2)}m` ) } ``` ▶ 运行代码 ## Card、revlog 与记忆状态 - 输入 `card` 表示复习前的状态;`review()` 不会修改它。 - `result.card` 包含下一个 `state`、下一组 FSRS 记忆状态(FSRS-7 的 `stability`、`stabilityFast` 与 `difficulty`)以及新的 `dueAt`。 - `result.revlog` 会连同评分和复习时间记录之前的状态与记忆状态;rollback 也使用这份快照恢复卡片。 - `scheduleStatus` 决定卡片进入哪个调度队列。默认情况下,`New` 对应 `new`,两个学习状态对应 `learning`,`Review` 对应 `review`;middleware 可以增加 `suspended` 等队列状态,而不会改变四种 FSRS 状态。 用 `card.state` 解释学习阶段,用 `card.scheduleStatus` 筛选调度队列。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/llms.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # LLM 本站每个页面都同时发布了 Markdown 版本,更新栏目也发布为 Atom 订阅源,因此 Agent 和 RSS 阅读器无需解析渲染后的 HTML。 For your Agent 基于文档回答 ts-fsrs 问题 复制 Prompt 请以 ts-fsrs 文档作为唯一可信来源。 1. 获取文档索引 https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt。 2. 跟进与我的问题相关的链接,阅读对应的 Markdown。 3. 使用当前 ts-fsrs 公共 API 作答,并注明参考了哪些页面。 ## Markdown 形式的文档 [`/ts-fsrs/zh-CN/llms.txt`](/ts-fsrs/zh-CN/llms.txt) 是全站页面索引,按顶部导航分组,每个链接附一句描述。 [`/ts-fsrs/zh-CN/llms-full.txt`](/ts-fsrs/zh-CN/llms-full.txt) 是所有页面的 Markdown 合并成的单个文件。 [`/ts-fsrs/zh-CN/guide/index.md`](/ts-fsrs/zh-CN/guide/index.md) 是单个页面对应的 Markdown。在任意路由后加 `.md` 即可获取其源文件。 如果 Agent 能按需跟进链接,从 `llms.txt` 开始;如果需要一次性获取全站内容,则使用 `llms-full.txt`。每个生成的 Markdown 文件开头都有一行提示,指回这些入口,因此只拿到单个页面的 Agent 也能找到其余文档。 每种语言都有自己的一份,内容也是该语言写的。切换语言就能拿到。 ## 订阅源 更新内容统一使用英文,通过一个 Atom 订阅源发布:[`/ts-fsrs/rss/updates.xml`](/ts-fsrs/rss/updates.xml)。更新页面也会在文档 head 中声明该订阅源,供支持自动发现的阅读器使用。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/index.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # 模型概览 FSRS 模型根据评分和经过时间计算新的记忆状态,通过遗忘曲线估算回忆概率,并根据记忆状态与目标保留率计算基础间隔。 本节的 FSRS-3 至 FSRS-6 术语与公式依据 [awesome-fsrs Wiki](https://github.com/open-spaced-repetition/awesome-fsrs/wiki) 校对,主要参考其中的 [The Algorithm](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm)。TypeScript API 与框架职责边界以 ts-fsrs 本身为准。 :::note 模型边界 模型只负责记忆状态更新、遗忘曲线计算和基础间隔计算。scheduler 负责组织复习流程,`chrono` 负责日期与经过时间,middleware 负责学习步骤、模糊处理、最大间隔限制和队列状态策略。 ::: For your Agent 实现或解释 SRS 模型 复制 Prompt 使用当前 ts-fsrs Model 契约完成以下任务:[描述任务]。 1. 阅读 https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/index.md 以及所选模型版本的页面。 2. 可按需对照 FSRS-6(https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-6.md)、FSRS-5(https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-5.md)、FSRS-4.5(https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-4dot5.md)、FSRS-4(https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-4.md)和 FSRS-3(https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-3.md)的公式与说明。 FSRS-7: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-7.md. 3. 实现模型前先定义配置 schema 和记忆状态 schema。 4. 将模型限制为 step、nextInterval、forgettingCurve 和 forward 行为。 5. 向 nextInterval 显式传入目标保留率。 6. 由 scheduler 组织复习流程,由 chrono 处理时间,由 middleware 应用学习步骤、模糊处理、最大间隔限制和队列状态策略。 7. 仅在配置已经完成 migrate、clip 和 check 后使用 bypass。 8. 使用当前公共 API,并为每个实现的公式添加聚焦测试。 ## 公共符号 | 符号 | 含义 | | ------------------------------- | --------------------------------------------------------------- | | `math:D` | 难度,范围为 1(最简单)至 10(最困难)。 | | `math:S` | FSRS-3 至 FSRS-6 的稳定性,单位为天;当 `math:t = S` 时回忆概率为 90%。 | | `math:R` | 经过 `math:t` 天后能够回忆的估计概率。 | | `math:G` | 评分:1 为 Again、2 为 Hard、3 为 Good、4 为 Easy。 | | `math:t` | 距离上次复习经过的天数。 | | `math:r` | `nextInterval` 使用的目标保留率。 | | `math:I(r, S)` | 根据稳定性 `math:S` 和目标保留率 `math:r` 计算的基础间隔,单位为天,尚未应用 middleware 策略。 | | `math:S_{\min}`、`math:S_{\max}` | 模型允许的稳定性下限和上限。 | | `math:D_{\min}`、`math:D_{\max}` | 模型允许的难度下限和上限。 | | `math:w_i` | 训练得到的模型权重。 | [FSRS-7](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-7.md) 根据其实现快照单独说明:它增加快稳定性,使用混合曲线并返回小数天间隔。上表中稳定性的 90% 定义和 `I(r, S)` 记法适用于 FSRS-3 至 FSRS-6。 ## 使用版本化模型 `DefaultScheduler` 默认使用 FSRS-7,可通过 `version` 选择任一支持的 FSRS 版本。也可以直接创建并调用各版本模型;下面以 FSRS-6 为例: ```ts twoslash file="../../../snippets/run-code/model.ts" import { Rating } from 'ts-fsrs' import { FSRS6_DEFAULT_WEIGHTS, FSRS6Model } from 'ts-fsrs/models/fsrs-6' const model = FSRS6Model.create({ config: { weights: FSRS6_DEFAULT_WEIGHTS, enableShortTerm: true, numRelearningSteps: 1, // Constrains weights; middleware runs the steps. }, }) const memoryState = model.step({ memoryState: null, rating: Rating.Good, elapsedDays: 0, }) const desiredRetention = 0.9 const interval = model.nextInterval(memoryState, desiredRetention) const retrievabilityAtInterval = model.forgettingCurve(memoryState, interval) console.log( JSON.stringify( { memoryState, desiredRetention, interval, retrievabilityAtInterval, }, null, 2 ) ) ``` ▶ 运行代码 示例先根据 90% 的目标保留率计算复习间隔,再通过 `forgettingCurve` 估算到达该间隔时的回忆概率。由于 FSRS-6 的 `nextInterval` 返回整数天,结果可能与 90% 略有差异。 在 FSRS-6 配置中,`enableShortTerm` 控制模型的同日稳定性更新。`numRelearningSteps` 只参与权重裁剪与检查;重新学习步骤仍由 middleware 调度。 ## 定义模型 `defineModel` 会保留具体的配置、记忆状态和算法类型,并把 schema、默认值与模型核心工厂组织为一个定义。它本身不会解析配置: 下面通过实现一个 [SM-2 变体](https://github.com/open-spaced-repetition/srs-benchmark/blob/main/models/sm2_trainable.py),演示如何定义模型。 ```ts twoslash fold height=420 file="../../../snippets/run-code/custom-model.ts" import { clamp, defineModel, type ModelForwardInput, type ModelStepInput, Rating, } from 'ts-fsrs' import { z } from 'zod' const configSchema = z.object({ weights: z.tuple([ z.number().min(0.01).max(100), z.number().min(0.01).max(100), z.number().min(1.3).max(10), z.number().nonnegative(), z.number().min(5), z.number(), ]), }) const memoryStateSchema = z.object({ interval: z.number().min(0).max(36500), easeFactor: z.number().min(1.3).max(10), repetitions: z.number().int().nonnegative(), }) type MemoryState = z.output const SM2Model = defineModel({ name: 'sm2', schema: { config: configSchema, memoryState: memoryStateSchema }, defaultValue: { memoryState: ({ config }) => ({ interval: 0, easeFactor: config.weights[2], repetitions: 0, }), }, create({ config }) { const parsed = configSchema.parse(config) const w = parsed.weights const step = ({ memoryState, rating }: ModelStepInput) => { const previous = memoryState ?? { interval: 0, easeFactor: parsed.weights[2], repetitions: 0, } // Again / Hard / Good / Easy -> SM-2 quality 2 / 3 / 4 / 5. const quality = rating + 1 const repetitions = rating > Rating.Again ? previous.repetitions + 1 : 0 const interval = clamp( repetitions <= 1 ? w[0] : repetitions === 2 ? w[1] : previous.interval * previous.easeFactor, 0.01, 36500 ) const easeFactor = clamp( previous.easeFactor - w[3] * (quality - w[4]) ** 2 + w[5], 1.3, 10 ) return { interval, easeFactor, repetitions } } const forward = ({ history, initialState, }: ModelForwardInput) => { const states: MemoryState[] = [] let memoryState = initialState ?? null for (const review of history) { memoryState = step({ memoryState, rating: review.rating, elapsedDays: review.deltaT, }) states.push(memoryState) } return states } return { config: parsed, bounds: { iMin: 0.01, iMax: 36500, eMin: 1.3, eMax: 10, rMin: 0, rMax: Infinity, }, algorithm: null, step, // Invert the benchmark curve to satisfy the Model interval contract. nextInterval: (state: Readonly, desiredRetention: number) => Math.max( 1, Math.round( (state.interval * Math.log(desiredRetention)) / Math.log(0.9) ) ), forgettingCurve: (state: Readonly, elapsedDays: number) => 0.9 ** (elapsedDays / state.interval), forward, } }, }) const model = SM2Model.create({ config: { weights: [1, 6, 2.5, 0.02, 7, 0.18] }, }) const states = model.forward({ history: [ { rating: Rating.Good, deltaT: 0 }, { rating: Rating.Good, deltaT: 1 }, { rating: Rating.Good, deltaT: 6 }, { rating: Rating.Again, deltaT: 15 }, { rating: Rating.Good, deltaT: 1 }, { rating: Rating.Good, deltaT: 1 }, ], }) console.log(JSON.stringify(states, null, 2)) // Intervals: 1, 6, 15, 1, 1, 6 days. ``` ▶ 运行代码 Schema 描述公共边界,并在 scheduler 组合 schema 时使用。直接调用 `create` 时,具体实现必须先完成所需的解析和规范化,再返回 `config`、`bounds`、`algorithm`、`step`、`nextInterval`、`forgettingCurve` 和 `forward`。该核心应保持确定性,且不包含调度策略。 :::warning bypass 仅用于已处理的配置 只有调用方已经明确完成该模型的 migrate、clip、schema 校验和范围检查后,才能使用 `FSRS6Model.create({ config, bypass: true })`。它会跳过这些 create 阶段的处理以及冻结。不要对来自用户、存储或网络的原始配置使用 `bypass`。 ::: ## 模型版本 - [FSRS-7](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-7.md) — 双分量实现参考,34 个权重与数值求间隔。 - [FSRS-6](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-6.md) — 支持可训练衰减与同日更新。 - [FSRS-5](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-5.md) — 使用固定幂函数遗忘曲线,并支持同日更新。 - [FSRS-4.5](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-4dot5.md) — 使用固定幂函数曲线,不包含同日公式。 - [FSRS-4](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-4.md) — 本项目中首个使用幂函数曲线的版本。 - [FSRS-3](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-3.md) — 使用较早的指数遗忘曲线。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-7.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # FSRS-7 :::tip 迁移指南 只重建记忆状态、不重新安排下次复习时间的迁移步骤见 [v6 与 FSRS-7 迁移指南](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/migration.md)。 ::: FSRS-7 使用 34 个权重和两个记忆分量。记忆状态包含 `stability`(慢分量)、`stabilityFast`(快分量)和 `difficulty`。 ## 记忆状态与符号 沿用[公共符号](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model.md#%E5%85%AC%E5%85%B1%E7%AC%A6%E5%8F%B7),以 `math:S_s` 表示 `stability`,`math:S_f` 表示 `stabilityFast`。两者单位均为天,但都不等于混合回忆概率降至 90% 时的间隔。难度同时影响慢分量曲线及其混合权重。 公式使用经过边界限制的复习前状态:两种稳定性均在 `[0.0001, 36500]` 内,难度在 `[1, 10]` 内。经过时间 `math:t` 使用小数天,曲线计算采用 `math:\max(t,0)`。“快”和“慢”是分量名称,并不保证两种稳定性的大小顺序。 ## 初始记忆 对于新记忆状态,令 `math:C_s` 和 `math:C_f` 分别表示慢、快稳定性的边界裁剪,`math:C_D` 表示裁剪到 `[1, 10]`。 ```math S_{s,0}=C_s(w_{G-1}),\qquad S_{f,0}=C_f(0.8S_{s,0}) ``` ```math D_0(G)=w_4-e^{w_5(G-1)}+1,\qquad D_{initial}=C_D(D_0(G)) ``` ## 双分量遗忘曲线 快分量: ```math d_f=-\operatorname{clamp}(w_{23}S_f^{w_{33}-0.3},0.01,0.95) ``` ```math F_f=\exp\left(\min\left(\frac{\ln w_{25}}{d_f},60\right)\right)-1 ``` ```math R_f(t)=\left(1+F_f\frac{t}{S_f}\right)^{d_f} ``` 慢分量: ```math d_s=-\operatorname{clamp}(w_{24},0.01,0.95) ``` ```math F_s=(w_{26}^{1/d_s}-1)e^{(D-5)(w_{32}-0.3)} ``` ```math R_s(t)=\left(1+F_s\frac{t}{S_s}\right)^{d_s} ``` ## 混合权重与回忆概率 两个权重均为正的未归一化系数,取决于记忆状态,而非经过时间。分量的原始回忆概率不等于它对混合结果的贡献。 ```math a_f=w_{27}S_f^{-w_{29}},\qquad a_s=w_{28}S_s^{w_{30}}e^{(D-5)(w_{31}-0.5)} ``` ```math R(t)=\frac{a_fR_f(t)+a_sR_s(t)}{a_f+a_s}(1-2\varepsilon)+\varepsilon,\qquad\varepsilon=10^{-5} ``` `algorithm.curve(t, state)` 返回 `fast`、`slow`、`fastWeight`、`slowWeight`、`retrievability` 和 `derivative`。前两项是原始回忆概率;组合时应先归一化权重。最后的微小调整使概率避开 0 和 1,因此 `math:R(0)=0.99999`。 ## 复习后的稳定性 对于某一分量的稳定性 `math:x`、回忆概率输入 `math:q` 和参数偏移 `math:k`,定义遗忘后的值和成功回忆后的更新: ```math L_k(x,q)=\min\left(x,w_{k+3}((x+1)^{w_{k+4}}-1)e^{(1-q)w_{k+5}}\right) ``` ```math H_{k,G}=\begin{cases}w_{k+6},&G=2\\1,&G\ne2\end{cases},\qquad E_{k,G}=\begin{cases}w_{k+7},&G=4\\1,&G\ne4\end{cases} ``` ```math U_k(x,q,G)=x\left[1+e^{w_k-1.5}(11-D)x^{-w_{k+1}}(e^{(1-q)w_{k+2}}-1)H_{k,G}E_{k,G}\right] ``` ```math T_k(x,q,G)=\begin{cases}L_k(x,q),&G=1\\\max(L_k(x,q),U_k(x,q,G)),&G\in\{2,3,4\}\end{cases} ``` 慢分量使用 `math:k=7` 和混合概率 `math:R`;快分量使用 `math:k=15` 和原始快分量概率 `math:R_f`。所有更新均使用复习前的难度。显式传入 `retrievability` 会替代慢分量更新和难度更新使用的 `math:R`,但不会替代快分量的 `math:R_f`。 评分为 Again 时,先将新的快稳定性限制为新的慢稳定性的 80%,再应用状态边界: ```math \widetilde S_s=T_7(S_s,R,G),\qquad \widetilde S_f=T_{15}(S_f,R_f,G) ``` ```math S'_s=C_s(\widetilde S_s),\qquad S'_f=C_f\left(\begin{cases}\min(\widetilde S_f,0.8\widetilde S_s),&G=1\\\widetilde S_f,&G\ge2\end{cases}\right) ``` ## 复习后的难度 Again 按混合回忆概率缩放难度变化。均值回归系数固定为 `0.01`: ```math \Delta D=-w_6(G-3)\begin{cases}R+0.1,&G=1\\1,&G\ge2\end{cases} ``` ```math D'=C_D\left(0.01D_0(4)+0.99\left(D+\frac{10-D}{9}\Delta D\right)\right) ``` 实现将三个新状态值四舍五入至小数点后八位,再裁剪到各自边界。FSRS-7 对小数经过时间(包括零)使用同一套更新,没有单独受 `enableShortTerm` 控制的同日公式。学习步骤调度仍属于 middleware 策略。 ## 间隔计算 `nextInterval(state, r)` 通过数值方法求出混合曲线达到目标概率的经过时间。它依赖全部三个状态字段,返回小数天。 ```math R(I;S_s,S_f,D)=r ``` 实现先校验 `math:0 For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # FSRS-6 FSRS-6 有 21 个可训练权重,包括可训练的遗忘曲线衰减和同日稳定性参数。 公共符号和 Model 契约见[模型概览](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model.md)。 :::info 公式来源 已依据 [awesome-fsrs Wiki — FSRS-6](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-6) 核对。 ::: ## 遗忘曲线与间隔 令正衰减值 `math:d = w_{20}`,并选取 `math:F`,使 `math:R(S, S) = 0.9`: ```math F=0.9^{-1/d}-1,\qquad R(t,S)=\left(1+F\frac{t}{S}\right)^{-d} ``` ```math I(r,S)=\frac{S}{F}\left(r^{-1/d}-1\right) ``` `nextInterval` 会把结果舍入为整数天,并保证至少为一天。 ## 初始记忆 ```math S_0(G)=\operatorname{clamp}\left(w_{G-1},S_{min},S_{max}\right) ``` ```math D_0(G)=w_4-e^{w_5(G-1)}+1,\qquad D_{initial}=\operatorname{clamp}\left(D_0(G),1,10\right) ``` ## 难度 ```math \Delta D=-w_6(G-3),\qquad D^*=D+\Delta D\frac{10-D}{9} ``` ```math D'=\operatorname{clamp}\left(w_7D_0(4)+(1-w_7)D^*,1,10\right) ``` ## 复习后的稳定性 当 `math:t > 0` 或 `enableShortTerm` 未启用时,评分决定使用以下哪个分支。 公式使用复习前的 `math:D` 和 `math:S`。Hard 时 `math:H_G = w_{15}`,否则为 `math:1`;Easy 时 `math:E_G = w_{16}`,否则为 `math:1`。 启用 `enableShortTerm` 时,`math:C` 等于复习前的稳定性乘以 `math:\exp(-w_{17}w_{18})`;否则 `math:C = S`: ```math S_f=\min\left(w_{11}D^{-w_{12}}\left((S+1)^{w_{13}}-1\right)e^{w_{14}(1-R)},C\right) ``` ```math S_r=S\left[1+e^{w_8}(11-D)S^{-w_9}\left(e^{w_{10}(1-R)}-1\right)H_GE_G\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` `math:C` 是当前实现对遗忘后稳定性的上限,并不是单独配置的稳定性下限。最后,模型通过 `clamp` 将 `math:S'_{\mathrm{raw}}` 限制在 `math:S_{\min}` 到 `math:S_{\max}` 之间。 ## 同日更新 这里的“同日”指模型收到 `elapsedDays = 0`,模型本身不检查日历日期。经过时间由直接调用方或 scheduler 的 `chrono` 决定。 当 `enableShortTerm` 已启用且 `math:t = 0` 时: ```math s_{inc}=S^{-w_{19}}e^{w_{17}(G-3+w_{18})} ``` ```math S'=\operatorname{clamp}\left(Sm,S_{min},S_{max}\right), \qquad m=\begin{cases} s_{inc},&G=1\\ \max(s_{inc},1),&G\ge2 \end{cases} ``` Hard、Good 和 Easy 不会让稳定性低于当前值;Again 可以降低稳定性。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-5.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # FSRS-5 FSRS-5 有 19 个可训练权重。它保留 FSRS-4.5 的固定遗忘曲线,并增加根据评分变化的同日稳定性更新。 公共符号和 Model 契约见[模型概览](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model.md)。 :::info 公式来源 已依据 [awesome-fsrs Wiki — FSRS-5](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-5) 核对。 ::: ## 遗忘曲线与间隔 ```math R(t,S)=\left(1+\frac{19t}{81S}\right)^{-1/2} ``` ```math I(r,S)=\frac{81S}{19}\left(r^{-2}-1\right) ``` ## 初始记忆 ```math S_0(G)=\operatorname{clamp}\left(w_{G-1},S_{min},S_{max}\right) ``` ```math D_0(G)=w_4-e^{w_5(G-1)}+1,\qquad D_{initial}=\operatorname{clamp}\left(D_0(G),1,10\right) ``` ## 难度 ```math \Delta D=-w_6(G-3),\qquad D^*=D+\Delta D\frac{10-D}{9} ``` ```math D'=\operatorname{clamp}\left(w_7D_0(4)+(1-w_7)D^*,1,10\right) ``` ## 复习后的稳定性 当 `math:t > 0` 或 `enableShortTerm` 未启用时,评分决定使用以下哪个分支。 Hard 时 `math:H_G = w_{15}`,否则为 `math:1`;Easy 时 `math:E_G = w_{16}`,否则为 `math:1`。 启用 `enableShortTerm` 时,`math:C` 等于复习前的稳定性乘以 `math:\exp(-w_{17}w_{18})`;否则 `math:C = S`: ```math S_f=\min\left(w_{11}D^{-w_{12}}\left((S+1)^{w_{13}}-1\right)e^{w_{14}(1-R)},C\right) ``` ```math S_r=S\left[1+e^{w_8}(11-D)S^{-w_9}\left(e^{w_{10}(1-R)}-1\right)H_GE_G\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` `math:C` 是当前实现对遗忘后稳定性的上限,并不是单独配置的稳定性下限。最后,模型通过 `clamp` 将 `math:S'_{\mathrm{raw}}` 限制在 `math:S_{\min}` 到 `math:S_{\max}` 之间。 ## 同日更新 这里的“同日”指模型收到 `elapsedDays = 0`,模型本身不检查日历日期。经过时间由直接调用方或 scheduler 的 `chrono` 决定。 当 `enableShortTerm` 已启用且 `math:t = 0` 时: ```math S'=\operatorname{clamp}\left( S e^{w_{17}(G-3+w_{18})},S_{min},S_{max} \right) ``` --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-4dot5.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # FSRS-4.5 FSRS-4.5 有 17 个可训练权重。它修改了 FSRS-4 的遗忘曲线,同时保留相同的难度和成功回忆更新结构。 公共符号和 Model 契约见[模型概览](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model.md)。 :::info 公式来源 已依据 [awesome-fsrs Wiki — FSRS-4.5](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-45) 核对。 ::: ## 遗忘曲线与间隔 ```math R(t,S)=\left(1+\frac{19t}{81S}\right)^{-1/2} ``` ```math I(r,S)=\frac{81S}{19}\left(r^{-2}-1\right) ``` ## 初始记忆 ```math S_0(G)=\operatorname{clamp}\left(w_{G-1},S_{min},S_{max}\right) ``` ```math D_0(G)=\operatorname{clamp}\left(w_4-(G-3)w_5,D_{min},D_{max}\right) ``` ## 难度 ```math D^*=D-w_6(G-3) ``` ```math D'=\operatorname{clamp}\left(w_7w_4+(1-w_7)D^*,D_{min},D_{max}\right) ``` ## 复习后的稳定性 Hard 时 `math:H_G = w_{15}`,否则为 `math:1`;Easy 时 `math:E_G = w_{16}`,否则为 `math:1`。 ```math S_f=\min\left(w_{11}D^{-w_{12}}\left((S+1)^{w_{13}}-1\right)e^{w_{14}(1-R)},S\right) ``` ```math S_r=S\left[1+e^{w_8}(11-D)S^{-w_9}\left(e^{w_{10}(1-R)}-1\right)H_GE_G\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` Again 分支会避免遗忘后的稳定性超过复习前的 `math:S`。最后,模型通过 `clamp` 将 `math:S'_{\mathrm{raw}}` 限制在 `math:S_{\min}` 到 `math:S_{\max}` 之间。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-4.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # FSRS-4 FSRS-4 有 17 个可训练权重,并使用幂函数遗忘曲线取代 FSRS-3 的指数曲线。 公共符号和 Model 契约见[模型概览](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model.md)。 :::info 公式来源 已依据 [awesome-fsrs Wiki — FSRS-4](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-v4) 核对。 ::: ## 遗忘曲线与间隔 ```math R(t,S)=\left(1+\frac{t}{9S}\right)^{-1} ``` ```math I(r,S)=9S\left(\frac{1}{r}-1\right) ``` ## 初始记忆 ```math S_0(G)=\operatorname{clamp}\left(w_{G-1},S_{min},S_{max}\right) ``` ```math D_0(G)=\operatorname{clamp}\left(w_4-(G-3)w_5,D_{min},D_{max}\right) ``` ## 难度 ```math D^*=D-w_6(G-3) ``` ```math D'=\operatorname{clamp}\left(w_7w_4+(1-w_7)D^*,D_{min},D_{max}\right) ``` ## 复习后的稳定性 Hard 时 `math:H_G = w_{15}`,否则为 `math:1`;Easy 时 `math:E_G = w_{16}`,否则为 `math:1`。 ```math S_f=w_{11}D^{-w_{12}}\left((S+1)^{w_{13}}-1\right)e^{w_{14}(1-R)} ``` ```math S_r=S\left[1+e^{w_8}(11-D)S^{-w_9}\left(e^{w_{10}(1-R)}-1\right)H_GE_G\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` 最后,模型通过 `clamp` 将 `math:S'_{\mathrm{raw}}` 限制在 `math:S_{\min}` 到 `math:S_{\max}` 之间。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model/fsrs-3.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # FSRS-3 FSRS-3 有 13 个可训练权重,并使用指数遗忘曲线。其稳定性更新使用刚计算出的新难度 `math:D'`。 公共符号和 Model 契约见[模型概览](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/model.md)。 :::info 公式来源 已依据 [awesome-fsrs Wiki — FSRS-3](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-v3) 核对。 ::: ## 遗忘曲线与间隔 ```math R(t,S)=0.9^{t/S} ``` ```math I(r,S)=S\frac{\ln r}{\ln 0.9} ``` ## 初始记忆 ```math S_0(G)=\operatorname{clamp}\left(w_0+(G-1)w_1,S_{min},S_{max}\right) ``` ```math D_0(G)=\operatorname{clamp}\left(w_2+(G-3)w_3,D_{min},D_{max}\right) ``` ## 难度 ```math D^*=D+w_4(G-3) ``` ```math D'=\operatorname{clamp}\left(w_5w_2+(1-w_5)D^*,D_{min},D_{max}\right) ``` ## 复习后的稳定性 ```math S_f=w_9(D')^{w_{10}}S^{w_{11}}e^{w_{12}(1-R)} ``` ```math S_r=S\left[1+e^{w_6}(11-D')S^{w_7}\left(e^{w_8(1-R)}-1\right)\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` 最后,模型通过 `clamp` 将 `math:S'_{\mathrm{raw}}` 限制在 `math:S_{\min}` 到 `math:S_{\max}` 之间。 --- url: https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/chrono/index.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/llms-full.txt. # Chrono 概览 Chrono 决定调度器如何表示时间、计算经过的天数,以及确定下次到期时间。模型使用以天为单位的间隔,Chrono 负责把这些间隔与应用中的时间值连接起来。 ## 选择预设 从 `ts-fsrs` 导入 `dateChrono`、`numericChrono`、`temporalInstantChrono` 和 `defineChrono`。 | 预设 | 时间值 | 天数运算 | | ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------ | | `dateChrono` | JavaScript `Date` | `difference` 默认计算 UTC 日期差;`fractionalDays: true` 时按固定 24 小时计算经过的小数天数。`add` 始终按固定 24 小时增加天数,支持小数。 | | `numericChrono` | 以天为单位的数字 | `difference(from, to)` 为 `to - from`;`add(from, days)` 为 `from + days`。 | | `temporalInstantChrono` | `Temporal.Instant` | 支持配置时区,并可选择是否保留时间差的小数天数。 | 各预设的详细说明: [Date](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/chrono/date.md)、[Numeric](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/chrono/numeric.md)、[Temporal Instant](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/chrono/temporal-instant.md)。 ## 定义与运行时操作 `Chrono` 定义包含 `schema`、`normalize`、`defaultValue` 和 `create`。将定义传给 `defineScheduler({ model, chrono })`;需要单独进行时间运算时,调用定义的 `create({ config })`。调度器也通过 `scheduler.chrono` 暴露配置完成的运行时对象。 Date 和 Temporal 需要向 `create({ config })` 传入完整配置;只有 Numeric 等不需要配置的预设可以无参调用 `create()`。通过调度器创建实例时,会校验配置并补齐 schema 默认值。 | 操作 | 含义 | | ---------------------- | --------------------------------------------- | | `now()` | 调用方省略 `now` 时提供时间值。 | | `difference(from, to)` | 返回模型使用的经过天数。 | | `add(from, days)` | 将最终的天数间隔转换为到期时间。 | | `compare(left, right)` | 可选的精确时间比较,返回负数、零或正数;Date 和 Temporal 预设均提供此操作。 | ## normalize 的作用 `normalize` 是一个 `ChronoTimeNormalizer` Standard Schema,输出类型为 `ChronoNormalizedTime