> 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 预设把这些间隔转换为到期时间；中间件位于两者之间，可以调整目标保留率或间隔、管理学习步骤，以及维护模型从未见过的额外卡片数据。各部分的职责见[整体架构](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/architecture.md)。

:::note 中间件边界
调度器由模型、chrono 与中间件组合而成。让模型只负责记忆状态更新与间隔计算，让 chrono 负责时间值与天数运算；中间件在核心操作之前提供输入、在其后调整结果，并承载这两者未定义的字段。
:::


For your Agent

实现或解释一个中间件策略

复制 Prompt

使用当前 ts-fsrs Middleware 契约完成以下任务：[描述任务]。

1. 阅读 https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/middleware/index.md、https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/middleware/lifecycle.md 和 https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/middleware/built-in.md，了解中间件如何扩展调度器。
2. 在定义行为前，先声明 config、card、revlog 和 cardInitInput 的 schema。
3. 将中间件限制为 schema、defaultValue、自定义状态的 scheduleStatus 声明以及 review/rollback/nextInterval 处理函数；让模型负责记忆状态更新与间隔计算，让 chrono 负责时间值与天数运算。
4. 恰好调用一次 next()。在它之前设置 ctx.candidate.desiredRetention[ctx.input.grade] 等输入；在它之后读取或调整计算结果。
5. 撤销时可以读取 ctx.config、ctx.input.card 和 ctx.input.revlog，只写入 ctx.result.card，并按配置的策略恢复复习处理函数保存的旧值。
6. 不要在处理函数中写数据库、发通知或执行其他外部操作；由应用保存最终选定的复习结果。
7. 当内置策略已能覆盖该行为时，优先复用。
8. 使用当前公共 API，并为所写的处理函数添加聚焦测试。

## 定义字段与行为

`Middleware` 定义包含必填的 `name`，以及可选的 schema、默认值和处理函数。`defineMiddleware()` 根据 schema 推导处理函数中的类型。可以从 `ts-fsrs` 导入；内置策略也可从 `ts-fsrs/middlewares` 导入。

| 定义                                      | 用途                                                                                                                                    |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `schema.config`                         | 校验传给 `create({ config })` 的配置。中间件共用调度器的配置对象。                                                                                          |
| `schema.card`                           | 声明并校验输入、输出卡片上的字段。                                                                                                                     |
| `schema.revlog`                         | 声明并校验复习日志字段，包括撤销所需的旧值。                                                                                                                |
| `schema.cardInitInput`                  | 声明 `newCard()` 接受的额外输入，例如卡片 ID。                                                                                                       |
| `scheduleStatus`                        | 注册自定义调度状态，例如 `scheduleStatus: ['buried']`，使组合后的类型和结果校验接受这些状态。                                                                         |
| `handlers.nextInterval`                 | 在 `next()` 前调整 `ctx.candidate.desiredRetention[ctx.input.grade]`，或在其后调整 `ctx.scheduledDays`。复习和直接调用 `scheduler.nextInterval()` 时都会执行。 |
| `defaultValue.card`                     | 在创建或重置卡片时提供字段值，通过 `ctx.operation` 区分这两种操作。                                                                                            |
| `handlers.review` / `handlers.rollback` | 在调用下一个处理函数前后，更新 `ctx.result`。运行方式见[生命周期](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/middleware/lifecycle.md)。       |

Schema 同时提供运行时校验与 TypeScript 类型推导。下面使用实现了 Standard Schema 的 Zod。在自己的应用中运行示例时，除 ts-fsrs 外还需要安装它：


```sh [npm]
npm install zod
```

```sh [yarn]
yarn add zod
```

```sh [pnpm]
pnpm add zod
```

```sh [bun]
bun add zod
```

```sh [deno]
deno add npm:zod
```

## 添加支持撤销的连续答对计数

示例中，除 Again 外的评分都会让 `streak` 加一，最多累积到 `streakLimit`；Again 会将它清零。复习处理函数把原计数保存到 revlog，因此即使清零后撤销，也能恢复之前的值：

```ts twoslash file="../../../snippets/run-code/middleware.ts" fold height=520
import { dateChrono, defineMiddleware, defineScheduler, Rating } from 'ts-fsrs'
import { FSRS6_DEFAULT_WEIGHTS, FSRS6Model } from 'ts-fsrs/models/fsrs-6'
import { z } from 'zod'

const streakFields = z.object({ streak: z.int().nonnegative() })
const streakMiddleware = defineMiddleware({
  name: Symbol('guide.streak'),
  schema: {
    config: z.object({ streakLimit: z.int().positive() }),
    card: streakFields,
    revlog: streakFields,
  },
  defaultValue: { card: () => ({ streak: 0 }) },
  handlers: {
    review(ctx, next) {
      next()
      const { card, grade } = ctx.input
      ctx.result.revlog.streak = card.streak
      ctx.result.card.streak =
        grade === Rating.Again
          ? 0
          : Math.min(card.streak + 1, ctx.config.streakLimit)
    },
    rollback(ctx, next) {
      next()
      ctx.result.card.streak = ctx.input.revlog.streak
    },
  },
})

const base = defineScheduler({ model: FSRS6Model, chrono: dateChrono })
const scheduler = base.use(streakMiddleware).create({
  config: {
    weights: FSRS6_DEFAULT_WEIGHTS,
    enableShortTerm: false,
    numRelearningSteps: 0,
    streakLimit: 2,
  },
})

const now = new Date('2026-01-01T00:00:00.000Z')
const initial = scheduler.newCard({ now })
const good = scheduler.review({ card: initial, grade: Rating.Good, now })
const again = scheduler.review({
  card: good.card,
  grade: Rating.Again,
  now: good.card.dueAt,
})
const restored = scheduler.rollback(again)
const preview = Array.from(
  scheduler.preview({ card: restored, now: good.card.dueAt })
)

console.log(
  JSON.stringify(
    {
      initial: initial.streak,
      afterGood: good.card.streak,
      afterAgain: again.card.streak,
      savedBeforeAgain: again.revlog.streak,
      restored: restored.streak,
      preview: preview.map(({ card }) => card.streak),
      afterPreview: restored.streak,
    },
    null,
    2
  )
)

```


▶ 运行代码

计数从 `0` 增加到 `1`，评为 Again 后回到 `0`，撤销后恢复为 `1`。预览中，Again、Hard、Good、Easy 对应的计数分别是 `0、2、2、2`，输入卡片的计数仍为 `1`。

`base.use(streakMiddleware)` 返回新的调度器定义，原来的 `base` 不变。需要保存返回值，或像示例一样继续调用 `.create()`。由于中间件通过 `use` 组合，处理函数会按固定顺序环绕调度器核心——详见[生命周期](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/middleware/lifecycle.md)。

现成的策略覆盖了常见场景，`DefaultScheduler` 已组合了它们。见[内置策略](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/middleware/built-in.md)；直接使用现成调度器见[快速开始](https://open-spaced-repetition.github.io/ts-fsrs/zh-CN/guide/quick-start.md)。
