> ## Documentation Index
> Fetch the complete documentation index at: https://dev.marginnote.com.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# 一次提交多步卡片改动

> 一个动作要改动多项时使用该接口：改标题、加批注、删批注、摘标签、记插件字段，写在一次提交里
提交，在撤销栈上只占一步。

MarginNote 先按顺序模拟整批操作，任何一步不通过，整批操作都不写入。一批最多 32 步。删批注必须排在
摘标签之前，否则返回 `422 INVALID_ORDER`。写入之后 MarginNote 重新读取核对，不一致就把改动过的卡
整体恢复并返回 `500`。

`syncReview` 打开时，写完照摘录那一刻的自动入组规则把该卡补进复习卡组，与本次写入同一步撤销。

这是插件唯一的原子多步写入口。旧版 MarginNote 没有注册该接口时返回 `404`，可以用作能力探测。




## OpenAPI

````yaml /openapi/plugin-api.yaml post /notes/{noteid}/edits
openapi: 3.1.0
info:
  title: 场景插件 API
  summary: 场景插件在网页面板里可以调用的接口。
  description: >
    场景插件的面板是一个网页。页面里向 `/bridge/v1` 发请求，就能读写 MarginNote 里的数据。

    请求由 MarginNote 处理，与外部命令行工具共用同一套处理代码。外部工具要带令牌，面板不必带。

    面板页面从 `mnpanel://panel/index.html` 加载，因此 `fetch('/bridge/v1' + path)`
    属于同源请求。


    面板调用受这些条件约束：


    - 请求不带令牌，本文件也不声明安全方案。

    - 页面能调用哪些接口，取决于两份名单。模板里的 `plugins` 名单和沉浸态决定面板能不能建好，
      网页面板白名单决定页面能调哪些接口。不在白名单里的请求返回 `403`，
      错误码为 `IN_PROCESS_NOT_ALLOWED`。
    - `GET /capabilities` 里的 `requiresMax` 只约束外部 Agent。面板的请求在 MarginNote 进程内处理，
      不受令牌、订阅状态与跨设备批准的限制，能否调用只看该路径是否在白名单内。
    - 写操作里的 `addon`，MarginNote 会改写为当前面板所属插件的 id；`confirm` 由 MarginNote 补上。
      页面填的值不会生效。

    用 `mn.api(path, opts)` 读取数据。该方法始终将 Response Body 按 JSON 解析，相当于

    `fetch('/bridge/v1' + path, opts).then(r => r.json())`。


    响应的内容不是 JSON 的接口，改用 `fetch`：


    - `GET /guide` 返回 Markdown 正文。

    - `GET /notes/{noteid}/render` 返回卡片长图或 PDF。

    - `GET /documents/{bookmd5}/pages/{index}/content/{contentId}` 返回图片。

    - `GET /bundles/{bundleId}/files` 带 `?path=` 时返回该文件的原始字节。


    调用失败时 Promise 不会进入 reject。MarginNote 把错误对象

    `{"error":{"code":"…","message":"…"}}` 当作 Response Body 返回，因此结果落在 resolve 里。

    判断调用结果看响应里是否带 `error`，写操作另看响应里的 `persisted`。


    接口清单、各项上限与 Request Body 结构随 MarginNote 的版本变化，运行时以 `GET /capabilities` 与

    `GET /guide` 返回的内容为准。
  version: '0.91'
servers: []
security: []
tags:
  - name: 能力与状态
    description: 读取 MarginNote 的能力、版本与当前上下文。调用其他接口之前先读这组。
  - name: 卡片与摘录
    description: 卡片的读取与写入、摘录的创建、批注、标签、元数据与留白。
  - name: 手写会话
    description: 手写层、作答纸、练习准备清单与会话记录的读取与提交。
  - name: 学习集与卡组
    description: 学习集结构、脑图树、子脑图、色板、快照与复习卡组。
  - name: 文档与页面
    description: 文档目录、页文本、页内注释面、摘录清单与同步诊断。
  - name: 导出与 Anki
    description: 文档导出计划、导出文件、Anki 导出能力、作业与产物。
  - name: 查找
    description: 跨库搜索、文件夹列表与学习集列表。
  - name: 界面与模板
    description: 读取当前界面状态与沉浸模板。
  - name: 插件辅助
    description: 面板辅助脚本、已配对设备与本机 helper 信息。
  - name: 任务包与同步诊断
    description: 拆书任务包与旧 CloudKit 同步诊断。
paths:
  /notes/{noteid}/edits:
    post:
      tags:
        - 卡片与摘录
      summary: 一次提交多步卡片改动
      description: |
        一个动作要改动多项时使用该接口：改标题、加批注、删批注、摘标签、记插件字段，写在一次提交里
        提交，在撤销栈上只占一步。

        MarginNote 先按顺序模拟整批操作，任何一步不通过，整批操作都不写入。一批最多 32 步。删批注必须排在
        摘标签之前，否则返回 `422 INVALID_ORDER`。写入之后 MarginNote 重新读取核对，不一致就把改动过的卡
        整体恢复并返回 `500`。

        `syncReview` 打开时，写完照摘录那一刻的自动入组规则把该卡补进复习卡组，与本次写入同一步撤销。

        这是插件唯一的原子多步写入口。旧版 MarginNote 没有注册该接口时返回 `404`，可以用作能力探测。
      operationId: postNotesNoteidEdits
      parameters:
        - $ref: '#/components/parameters/Noteid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NoteEditsRequest'
            examples:
              writeBack:
                summary: 更新标题、添加释义、移除待办标签、记录状态
                value:
                  ops:
                    - op: setTitle
                      text: photosynthesis
                      expectedPrefix: photo
                    - op: appendComment
                      text: 光合作用：植物把光能转成化学能的过程。
                    - op: deleteComment
                      index: 2
                      expectedPrefix: 旧释义
                    - op: removeHashtags
                      hashtags:
                        - '#VocabPending'
                    - op: setAddonValues
                      values:
                        status: done
                        wrote:
                          - 1
                  confirm: true
                  syncReview: false
      responses:
        '200':
          description: 提交响应。可能还有未列出的字段。
          content:
            application/json:
              schema:
                type: object
                properties:
                  noteid:
                    type: string
                  comments:
                    type: array
                    items:
                      type: object
                      properties:
                        index:
                          type: integer
                        text:
                          type: string
                      required:
                        - index
                        - text
                  applied:
                    type: integer
                    description: 实际执行的步数。
                  hashtags:
                    type: array
                    items:
                      type: string
                  title:
                    type: string
                    description: 写入之后的卡片标题。
                  persisted:
                    type: boolean
                  addonValues:
                    type: object
                    properties: {}
                  bridgeApiVersion:
                    type: string
                  review:
                    type: object
                    description: ⚠️带 `syncReview` 时返回的入组结果。
                    additionalProperties: true
              examples:
                assembled:
                  summary: 构造示例，未实测
                  value:
                    noteid: 5880F73A-3A6D-4464-9D6F-88F60285401E
                    applied: 2
                    title: photosynthesis
                measured:
                  summary: 实测于 2026-10-07，App 0.91
                  value:
                    noteid: D3085422-2CB9-4B68-A78D-DEE351CF9C38
                    comments:
                      - index: 2
                        text: 探针改过的批注。
                      - index: 3
                        text: '#探针'
                    applied: 1
                    hashtags:
                      - '#探针'
                    title: 探针事务标题
                    persisted: true
                    addonValues: {}
                    bridgeApiVersion: '0.91'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalError'
      x-codeSamples:
        - lang: javascript
          label: 用一次提交写完一张卡
          source: |
            const r = await mn.api('/notes/' + noteid + '/edits', {
              method: 'POST',
              body: {
                ops: [
                  // 删批注必须排在摘标签之前，否则 422 INVALID_ORDER
                  { op: 'deleteComment', index: 2, expectedPrefix: '旧释义' },
                  { op: 'setTitle', text: 'photosynthesis', expectedPrefix: 'photo' },
                  { op: 'appendComment', text: '光合作用：植物把光能转成化学能的过程。' },
                  { op: 'removeHashtags', hashtags: ['#VocabPending'] },
                  // addon 由 MarginNote 设为本面板的 owner
                  { op: 'setAddonValues', values: { status: 'done' } }
                ],
                confirm: true,
                syncReview: false
              }
            });
            if (r.error) mn.log(r.error.code);

            // 探测 MarginNote 是否注册了该接口
            if (r.error && r.error.code === 'NOT_FOUND') {
              // 旧版 MarginNote 上改用逐条写入的接口
            }
components:
  parameters:
    Noteid:
      name: noteid
      in: path
      required: true
      description: >-
        卡片的 noteid。取自卡片命令响应的 `noteid`、`POST /notes/batch-get` 响应里
        `items[].noteid`，或 `GET /ui-state` 的 `state.focusnoteid`。
      schema:
        type: string
  schemas:
    NoteEditsRequest:
      type: object
      required:
        - ops
        - confirm
      description: |
        一次提交里对同一张卡做的多步写入。MarginNote 先按顺序模拟整批操作，任何一步不通过，整串都不
        写入；写入之后重新读取核对，不一致就把改动过的卡整体恢复。一批最多 32 步，撤销栈上
        只占一步。
      properties:
        ops:
          type: array
          maxItems: 32
          description: >-
            按顺序执行的步骤。`deleteComment` 必须排在 `removeHashtags` 之前，否则返回 `422
            INVALID_ORDER`。
          items:
            $ref: '#/components/schemas/EditOp'
        confirm:
          type: boolean
          description: 必填。由 MarginNote 自动补上，页面不传也生效。
        syncReview:
          type: boolean
          description: 写完之后按摘录时的自动入组规则把该卡补进复习卡组，与本次写入同一步撤销。
    EditOp:
      oneOf:
        - $ref: '#/components/schemas/EditSetTitleOp'
        - $ref: '#/components/schemas/EditAppendCommentOp'
        - $ref: '#/components/schemas/EditDeleteCommentOp'
        - $ref: '#/components/schemas/EditRemoveHashtagsOp'
        - $ref: '#/components/schemas/EditSetAddonValuesOp'
      discriminator:
        propertyName: op
        mapping:
          setTitle: '#/components/schemas/EditSetTitleOp'
          appendComment: '#/components/schemas/EditAppendCommentOp'
          deleteComment: '#/components/schemas/EditDeleteCommentOp'
          removeHashtags: '#/components/schemas/EditRemoveHashtagsOp'
          setAddonValues: '#/components/schemas/EditSetAddonValuesOp'
    ErrorEnvelope:
      type: object
      description: >-
        失败时的 Response Body。MarginNote 将其当作正常 Response Body 返回，因此落在 resolve 里而不是
        reject 里。
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorObject'
    EditSetTitleOp:
      type: object
      required:
        - op
        - text
      properties:
        op:
          type: string
          enum:
            - setTitle
        text:
          type: string
          description: 新标题。清空标题传空串。
        expectedPrefix:
          type: string
          description: 当前标题的前 32 个字，为空传空串。这是乐观锁，锁的是读完到写之间这段时间。
    EditAppendCommentOp:
      type: object
      required:
        - op
        - text
      properties:
        op:
          type: string
          enum:
            - appendComment
        text:
          type: string
          description: 追加的批注正文。
    EditDeleteCommentOp:
      type: object
      required:
        - op
        - index
      properties:
        op:
          type: string
          enum:
            - deleteComment
        index:
          type: integer
          minimum: 1
          description: 要删的批注序号，与 `POST /notes/{noteid}/comments` 读到的一致。
        expectedPrefix:
          type: string
          description: 该条批注的前 32 个字。内容已变时返回 `409 COMMENT_CHANGED`。
    EditRemoveHashtagsOp:
      type: object
      required:
        - op
        - hashtags
      properties:
        op:
          type: string
          enum:
            - removeHashtags
        hashtags:
          type: array
          items:
            type: string
          description: 要摘掉的标签，每项写完整路径。
    EditSetAddonValuesOp:
      type: object
      required:
        - op
        - values
      properties:
        op:
          type: string
          enum:
            - setAddonValues
        addon:
          type: string
          description: 插件公开 id。MarginNote 会改写为面板自己的 owner，页面填的值不会生效。
        values:
          type: object
          description: 要写入的键值。值为 `null` 表示删掉该键。
          additionalProperties: true
    ErrorObject:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: 错误码。判断写入是否发生看该字段，不看 HTTP 状态码。
        message:
          type: string
          description: 人可读的说明。文案随 MarginNote 的版本变化，不要用该字段做分支判断。
        retryable:
          type: boolean
          description: >-
            是否明确可以稍后再试。只有外部调用的响应带该字段，面板本地调用的响应只有 `code` 与
            `message`。该布尔值表达不了结果未定，这类情况按各接口的说明处理，例如手写会话族看响应里的
            `layerLeaseUnresolved`。
        details:
          type: object
          description: >-
            附加事实。例如 `SCAN_BUSY` 带 `retryAfterSeconds`，`HASHTAG_ROUTE_REQUIRED` 带
            `writeRoute`。
          additionalProperties: true
  responses:
    NotFound:
      description: 目标对象不存在，或调用方所在的旧版 MarginNote 没有注册该接口。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            notFound:
              summary: 目标不存在或接口未注册
              value:
                error:
                  code: NOT_FOUND
                  message: 找不到目标对象。
    Conflict:
      description: 读取到写入之间的这段时间里目标被改过，本次调用没有写入任何内容。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            contentChanged:
              summary: 内容已被改过
              value:
                error:
                  code: CONTENT_CHANGED
                  message: 目标内容在这期间被改过，请重读后再发。
    UnprocessableEntity:
      description: 字段合法但组合不成立，例如缺少 `confirm`、序号越界、或写入顺序违反约定。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            confirmRequired:
              summary: 没有带 confirm
              value:
                error:
                  code: CONFIRM_REQUIRED
                  message: 本次调用会改动数据，需要带 confirm。
            invalidOrder:
              summary: 写入顺序不成立
              value:
                error:
                  code: INVALID_ORDER
                  message: deleteComment 必须排在 removeHashtags 之前。
    InternalError:
      description: 写入后重新读取核对不通过，或写入过程报错。返回该响应表示写入未成功。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            saveFailed:
              summary: 写入磁盘后重新读取不一致
              value:
                error:
                  code: SAVE_FAILED
                  message: 写入之后重新读取核对不通过，碰过的卡已整体恢复。
                  retryable: false

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.