# 一次提交多步卡片改动
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/一次提交多步卡片改动
/openapi/plugin-api.yaml post /notes/{noteid}/edits
一个动作要改动多项时使用该接口:改标题、加批注、删批注、摘标签、记插件字段,写在一次提交里
提交,在撤销栈上只占一步。
MarginNote 先按顺序模拟整批操作,任何一步不通过,整批操作都不写入。一批最多 32 步。删批注必须排在
摘标签之前,否则返回 `422 INVALID_ORDER`。写入之后 MarginNote 重新读取核对,不一致就把改动过的卡
整体恢复并返回 `500`。
`syncReview` 打开时,写完照摘录那一刻的自动入组规则把该卡补进复习卡组,与本次写入同一步撤销。
这是插件唯一的原子多步写入口。旧版 MarginNote 没有注册该接口时返回 `404`,可以用作能力探测。
# 列出卡片可用插件命令
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/列出卡片可用插件命令
/openapi/plugin-api.yaml get /notes/{noteid}/addon-commands
返回该卡当前菜单上可用的插件命令,以及插件存在该卡上的字段。
命令要出现在菜单上,需要以下各处齐备:插件清单里声明了 `cardCommands`,模板把该命令摆进
某张菜单,页面上监听了卡片命令事件。匹配的命令随卡的状态变化,判断条件由 MarginNote 在菜单
弹出时判定。
`reason` 说明本次没有命令可用的原因。
# 列出卡片批注
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/列出卡片批注
/openapi/plugin-api.yaml get /notes/{noteid}/comments
返回该卡的全部批注,包含作答纸类,同时返回当前版本号。更新与删除批注都要携带该版本号。
序号从 1 开始。不含文字的条目同样占用序号。较早的条目上 `commentId` 可能为 `null`。
# 列出卡片留白带
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/列出卡片留白带
/openapi/plugin-api.yaml get /notes/{noteid}/blank-bands
按追加顺序返回该卡下面的留白带,每一项返回该条带在文档里的位置、是否折叠、高度、
是否带笔迹,以及是否正在使用。
# 创建摘录
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/创建摘录
/openapi/plugin-api.yaml post /excerpts
用一把摘录工具或一个划线快捷项,在文档的指定位置摘一条。摘录走模板配置的流程,
摘出来的卡带上工具配置好的标签、颜色与 AI 归属。
工具序号取自 `GET /immersive/template` 响应里 `excerptTools.tools[].index`,
与 `shortcutIndex` 二者取一。
响应里有两个 id:`noteid` 指向文档笔记本里的记录,`cardNoteId` 指向脑图上的卡片。
插件状态记在后者上。
# 创建留白带
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/创建留白带
/openapi/plugin-api.yaml post /notes/{noteid}/blank-bands
在该卡下面追加一条留白带。MarginNote 无法建立时返回 `409`,响应里带一个稳定的原因码,
例如没有选区、插入被拒、物理保存失败。该情形不得视为已经建立。
# 删除批注
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/删除批注
/openapi/plugin-api.yaml post /notes/{noteid}/comments/{index}/delete
删除第 `index` 条批注。序号从 1 起。`expectedPrefix` 是该条批注当前内容的前 32 个字,
读完到写之间内容已变则返回 `409 COMMENT_CHANGED`。
# 合并子卡
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/合并子卡
/openapi/plugin-api.yaml post /notes/{noteid}/merge-branch
把子卡的内容并进该卡,等同于卡片菜单上的合并分支。默认只并直接子卡,
带 `recursive` 时连整棵子树一起并。
并非所有子卡都会被并入:拒绝接收的卡原样留在树上,数量看响应里的 `notMergedCount`;
摘要卡与子脑图根卡会被自动跳过,原因在 `skipped` 里。
响应里的 `willMerge` 是本次计划并入的卡,可据此先检查再决定是否提交。
# 复制卡片分支
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/复制卡片分支
/openapi/plugin-api.yaml post /notes/copy
把所选的根卡连同该卡在脑图上的全部下层复制到目标父卡下,单次上限为 50 个根。
`reference` 建立与来源保持同步的引用卡,改一处两边一起变;`clone` 建立各自独立的卡片,
并重写分支内部的链接。
该接口的 `confirm` 排在业务校验之后,不带 `confirm` 时先完成整套只读校验,再返回 `422`,
响应里带上已经查证的事实。该路径可以当作一次预演。
# 定位卡片
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/定位卡片
/openapi/plugin-api.yaml post /notes/{noteid}/focus
把界面切到该卡,并连带切到该卡所属的学习集、卡组或文档。同一张卡被多个学习集引用时,
用 `topicid` 指定打开的学习集。
界面切换是异步发生的。该动作本身就是结果,没有需要等待的响应。
# 定位留白带
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/定位留白带
/openapi/plugin-api.yaml post /blank-bands/{blankId}/focus
先把该条带展开,再把文档翻到该条带所在页。该操作只在该文档正开在屏幕上时成立,
否则返回 `409 DOCUMENT_NOT_ON_SCREEN`。
# 将分支折叠为子脑图
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/将分支折叠为子脑图
/openapi/plugin-api.yaml post /notes/{noteid}/fold-into-sub-map
把该卡的整个分支折成一张子脑图。带 `undo` 传 `true` 时恢复为普通分支。
该卡没有下层时返回 `422 NO_CHILDREN`。
# 打开作答纸
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/打开作答纸
/openapi/plugin-api.yaml post /notes/{noteid}/annotation-editor
打开该题卡的图片底图批注稿,也就是作答纸,或者继续已有的一份。
该操作要带 `expectedRevision`,取自 `GET /notes/{noteid}/comments` 或
`POST /notes/batch-get` 响应里的 `commentsRevision`。带 `commentId` 表示继续该份,
不带表示新建。`advance` 等同于在编辑器里按下主按钮。
`practice` 给定时其值只含 `addon` 与 `topic` 两项。面板发起的调用只写 `topic`,
`addon` 取自面板所属插件;
命令行发起的调用须自行写入 `addon`,缺少该字段时返回 `INVALID_INPUT`。不传 `practice`
也能打开编辑器,返回 `200`。
编辑器占用期间该卡会被锁定,再次打开返回 `ANNOTATION_UNAVAILABLE`。这道题不属于当前学习集时返回 `PRACTICE_TOPIC_CHANGED`,
该情形要先刷新重读,原地重试得到同样的结果。
# 批量获取卡片
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/批量获取卡片
/openapi/plugin-api.yaml post /notes/batch-get
按 id 一次读多张卡:标题与预览、文本项、二进制项、样式级别、标签、插件保存的字段、
以及内容的来源。
插件状态记在脑图的卡片上。从文档上摘下来的记录在文档笔记本里,两者不是同一个对象,
用摘录记录的 id 读插件字段只会读到空。
# 折叠或展开卡片批注
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/折叠或展开卡片批注
/openapi/plugin-api.yaml post /notes/{noteid}/comments/fold
修改一条或多条批注的折叠状态。该接口改的是批注列表里的 Comment 级状态,不是脑图分支的
`collapsed` / `mindclose` 状态。
调用前先读取 `GET /notes/{noteid}/comments`。请求里的 `expectedRevision` 必须使用该响应顶层
的 `revision`。`revision` 属于当前卡片的完整批注投影,不在单条批注上。
`targets` 支持两种定位方式:
- `{ "index": 1 }`:按 `GET /notes/{noteid}/comments` 返回的 1 起始序号定位。序号 0 不是批注。
- `{ "ownerNoteId": "...", "commentId": "..." }`:按实际归属卡片与稳定批注身份定位。
较早的批注可能没有 `commentId`,读取时返回 `null`。这类批注首次修改建议用 `index` 定位。
引用卡会显示原卡的共享批注。修改这类批注时,应以当前显示的卡片 `noteid` 调用本接口。
MarginNote 收到请求后,会把改动写回批注实际归属的卡片 `ownerNoteId`。
# 折叠或展开脑图节点
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/折叠或展开脑图节点
/openapi/plugin-api.yaml post /notes/fold
只改脑图上的显示状态,不动内容。单次上限为 200 张卡。这是开销最小的写操作,
但仍然会在撤销栈上占一步。
# 提交作答判定
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/提交作答判定
/openapi/plugin-api.yaml post /notes/{noteid}/practice-verdict
把本次作答的判定写下来:是否掌握,以及错因。
要带 `expectedRevision`,与打开作答纸时用的是同一个版本号。
出现下列情形时判定不会写下:
- `PRACTICE_CONFIG_UNAVAILABLE`:当前场景没有给该插件声明标签前缀与错因清单。该项配置取自 `GET /immersive/template` 的 `plugins[].config`。
- `PRACTICE_TOPIC_CHANGED`:这道题不属于当前学习集。须刷新重读后重新提交,原地重试得到同样的结果。
- `PRACTICE_KEY_INVALID`:身份无法写出。
后两者发生在写入之前,没有任何改动生效。
# 更新卡片插件字段
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/更新卡片插件字段
/openapi/plugin-api.yaml post /notes/addon-values
把插件自己的数据写到卡片上,单次上限为 200 张卡与 50 个键。值写 `null` 表示删掉该键。
面板会把 `addon` 改写为本面板所属插件的 owner,页面填的值不会生效,因此写不到别的
插件名下。`confirm` 由 MarginNote 补上。
响应里的 `persisted` 是独立重新读取、核对一致之后才返回的,据此判断写入是否成功。批量写入不经过
提交,需要在同一步里改动多项时用 `POST /notes/{noteid}/edits`。
# 更新卡片标签
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/更新卡片标签
/openapi/plugin-api.yaml post /notes/hashtags
一次给多张卡加标签、摘标签或替换标签,一次最多 200 张卡、100 个标签。
每个标签写一条完整的叶子路径,层级之间用 `/` 分隔。
摘标签时可以带上 `includeDescendants`,把下层的标签一并摘掉;加与替换不接受该项。
标签不能借改正文的途径写入,正文里出现标签控制行会被拒,响应会注明该用哪个接口。
一次提交多组互不相同的操作时用 `patches`。
# 更新卡片标题
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/更新卡片标题
/openapi/plugin-api.yaml post /notes/{noteid}/title
改该卡的标题。字段名是 `text`,不是 `title`。清空标题传空串。
`expectedPrefix` 是当前标题的前 32 个字,为空传空串。内容已变时返回 `409`。
该接口与改正文走同一个处理器,缺字段时返回的码都是 `INVALID_INPUT`,分辨所缺的项
看 `message`。
# 更新卡片样式级别
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/更新卡片样式级别
/openapi/plugin-api.yaml post /notes/metadata
改摘录的颜色与样式级别,单次上限为 200 张卡。颜色编号取 0 到 15,每一个编号的实际颜色随
学习集色板变化,用 `GET /study-sets/{topicid}/note-palette` 读当前这套色板。
样式级别取 -1 到 2,管的是来源摘录的样式,不是脑图卡片本身的底色。
响应里的 `persisted` 是独立重新读取核对一致之后才给的。写入磁盘之后重新读取不一致会返回 `500`,
该情形不得视为写入成功。
# 更新卡片正文
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/更新卡片正文
/openapi/plugin-api.yaml post /notes/{noteid}/text
改该卡的正文。留白实体的正文也用该接口写入,目标传 `kind` 为留白的那一项的 id。
`expectedPrefix` 是当前正文的前 32 个字,为空传空串。内容已变时返回 `409`。
不要通过该接口写入标签。正文里出现标签控制行会被 `422 HASHTAG_ROUTE_REQUIRED` 拒绝,
响应会注明应当改用哪个接口。修改正文之前须先确认该卡是否为摘录卡,看 `texts[].excerpt`
或内容来源里的 `hasExcerpt`,不依据页码。
# 更新批注
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/更新批注
/openapi/plugin-api.yaml post /notes/{noteid}/comments/{index}
改写第 `index` 条批注。序号从 1 起,序号 0 是卡片自身正文,指向该序号的请求返回 `422`。
`expectedPrefix` 是该条批注当前内容的前 32 个字,读完到写之间内容已变则返回 `409 COMMENT_CHANGED`。
# 更新留白带
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/更新留白带
/openapi/plugin-api.yaml post /blank-bands/{blankId}
折叠、展开或改高一条留白带。带高限定在 50 到 4000 之间。
# 添加批注
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/添加批注
/openapi/plugin-api.yaml post /notes/{noteid}/comments
追加一条批注。正文整条写成一个指向别处的应用内链接时,该条批注成为真正的双向链接。
`format` 只在新增时接受,改写现成的批注时带上该参数会被判非法。样式只有在按富文本写入时有效,
MarginNote 移除样式时响应里的 `styled` 为 `false`。
`field` 与 `fieldLabel` 用来把该条批注标成某个字段的值,便于同一张卡上放多组结构化内容。
# 移动卡片
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/移动卡片
/openapi/plugin-api.yaml post /notes/move
在学习集内部换父卡,不改内容、不删卡。单次合计上限为 50 张。
批量形态用 `moves`,一次提交多组。该接口的 `confirm` 排在业务校验之前,不带时只回
该错误码。
# 获取卡片媒体文件
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/获取卡片媒体文件
/openapi/plugin-api.yaml get /notes/{noteid}/content/{contentId}
按 `contentId` 取回卡片上某一项二进制内容的原始字节。`contentId` 取自
`POST /notes/batch-get` 响应里 `items[].parts[].contentId`。
返回的是内容本身,不是 JSON,因此该接口要用 `fetch` 原语读取。
# 获取卡片渲染图
Source: https://dev.marginnote.com.cn/api-reference/卡片与摘录/获取卡片渲染图
/openapi/plugin-api.yaml get /notes/{noteid}/render
把标题、正文、摘录图、插图与手写合成一张完整的卡片长图或一份 PDF。
返回的是图片或 PDF 本身,不是 JSON,因此该接口要用 `fetch` 原语读取。
像素数超过上限时会被明确拒绝。
# 下载 Anki 作业产物
Source: https://dev.marginnote.com.cn/api-reference/导出与-anki/下载-anki-作业产物
/openapi/plugin-api.yaml get /anki/jobs/{jobId}/files
下载 Anki 作业生成的产物。带 `name` 查询参数指定产物名,例如 `collection.apkg`。
响应不是 JSON,页面里要用 `fetch` 原语读取 Blob。
# 下载文档导出文件
Source: https://dev.marginnote.com.cn/api-reference/导出与-anki/下载文档导出文件
/openapi/plugin-api.yaml get /document-exports/{exportId}/file
下载文档导出的产物。响应不是 JSON,页面里要用 `fetch` 原语读取 Blob。
导出文件是会话内保留的临时产物,保留数量与时长以 `GET /capabilities` 为准。
# 丢弃文档导出计划
Source: https://dev.marginnote.com.cn/api-reference/导出与-anki/丢弃文档导出计划
/openapi/plugin-api.yaml post /document-export-plans/{exportPlanId}/discard
丢弃导出计划及其会话内状态。该操作不会修改文档或学习集。
# 取消文档导出计划
Source: https://dev.marginnote.com.cn/api-reference/导出与-anki/取消文档导出计划
/openapi/plugin-api.yaml post /document-export-plans/{exportPlanId}/cancel
取消正在准备的文档导出计划。已完成或不存在的计划可能返回错误对象。
# 查询 Anki 作业
Source: https://dev.marginnote.com.cn/api-reference/导出与-anki/查询-anki-作业
/openapi/plugin-api.yaml get /anki/jobs/{jobId}
查询 Anki 导出、预览、校验或检查作业的状态。作业产物是会话内对象,重启或清理后可能返回 `404`。
# 查询文档导出计划
Source: https://dev.marginnote.com.cn/api-reference/导出与-anki/查询文档导出计划
/openapi/plugin-api.yaml get /document-export-plans/{exportPlanId}
查询 `POST /documents/{bookmd5}/export-plan` 创建的导出计划状态。计划是会话内对象,
重启或清理后可能返回 `404`。
# 获取 Anki 导出能力
Source: https://dev.marginnote.com.cn/api-reference/导出与-anki/获取-anki-导出能力
/openapi/plugin-api.yaml get /anki/capabilities
返回 Anki 导出相关的格式、上限、作业与上传路由。字段集合随 MarginNote 版本增减,
调用前应先读取该接口,以实际返回的字段为准。
# 保存文档页上的笔迹
Source: https://dev.marginnote.com.cn/api-reference/手写会话/保存文档页上的笔迹
/openapi/plugin-api.yaml post /writing/pencil-document-commit
把文档阅读器里可见页画布上的笔迹存成快照,连续阅读与翻页模式都支持。
目标要从 `POST /writing/context` 返回的 `state.visiblePages[].continuousCommitTarget`
或 `pagedCommitTarget` 原样返回。取到 `null` 表示这条路径当前不可用。
连续阅读的目标里页面清单最多 64 页。
与保存焦点画布一样,返回的是受理,结果用 `POST /writing/pencil-result` 查。
# 保存焦点画布的笔迹
Source: https://dev.marginnote.com.cn/api-reference/手写会话/保存焦点画布的笔迹
/openapi/plugin-api.yaml post /writing/pencil-commit
将当前焦点根画布上的笔迹保存为快照。该接口只作用于当前焦点根画布,不写入整份编辑器,也不切换题目。
该接口立即返回受理,`state` 为 `pending`。受理不是完成,结果要用 `POST /writing/pencil-result`
用同一个 `requestId` 去查。
同一个标识配同一个请求只返回原结果,不会重做;不同的目标共用同一个标识会返回冲突。
每个窗口同时只准有一笔未完成的操作。MarginNote 重启之后这些记录会消失,因此查不到不等于没保存过,
要独立读回确认。
# 保存练习准备选择
Source: https://dev.marginnote.com.cn/api-reference/手写会话/保存练习准备选择
/openapi/plugin-api.yaml post /writing/plan-save
保存这本学习集的练习准备选择。该接口只记录本次意图,不展开留白,不切层,不导航,
也不修改学习集。
一次最多 20 道题,题目不能重复。传空数组表示清空准备选择,不会删掉题目或笔迹。
要带 `expectedRevision`,与读回来的 `plan.revision` 对照。同一个标识换内容,
或者使用旧版本号发新请求,都会返回冲突。
# 准备留白作答空间
Source: https://dev.marginnote.com.cn/api-reference/手写会话/准备留白作答空间
/openapi/plugin-api.yaml post /writing/blank-prepare
为留白作答准备空间,但不等同于已经进入留白页。后续结果看 `POST /writing/transition-result`。
# 列出已打开的学习窗口
Source: https://dev.marginnote.com.cn/api-reference/手写会话/列出已打开的学习窗口
/openapi/plugin-api.yaml get /writing/windows
列出当前打开的学习窗口。每项返回窗口身份、场景身份与该窗口打开的学习集。
窗口身份只在进程内有效,不能跨重启保存使用。
# 创建练习会话
Source: https://dev.marginnote.com.cn/api-reference/手写会话/创建练习会话
/openapi/plugin-api.yaml post /writing/session-create
基于已保存的练习准备清单创建一趟练习会话记录。该族接口的返回是进程内状态,
不能替代重新读取 `POST /writing/session-read`。
# 压缩手写层
Source: https://dev.marginnote.com.cn/api-reference/手写会话/压缩手写层
/openapi/plugin-api.yaml post /writing/layer-compact
收拢手写层的租约存储。只丢弃所属会话已经记录退出的、彻底了结的租约;笔迹、卡片与留白
不受影响。
响应返回租约数与字节占用在整理前后的对照。
# 取消练习会话
Source: https://dev.marginnote.com.cn/api-reference/手写会话/取消练习会话
/openapi/plugin-api.yaml post /writing/session-cancel
取消当前练习会话或指定运行记录。取消之后应重新读取会话状态。
# 在练习会话中开始焦点卡作答
Source: https://dev.marginnote.com.cn/api-reference/手写会话/在练习会话中开始焦点卡作答
/openapi/plugin-api.yaml post /writing/session-focus-begin
会话版焦点卡开始命令,会同时关联练习会话记录。字段细节以运行时 `GET /guide` 为准。
# 在练习会话中开始留白作答
Source: https://dev.marginnote.com.cn/api-reference/手写会话/在练习会话中开始留白作答
/openapi/plugin-api.yaml post /writing/session-blank-begin
会话版留白开始命令,会同时关联练习会话记录。字段细节以运行时 `GET /guide` 为准。
# 在练习会话中进入焦点卡作答
Source: https://dev.marginnote.com.cn/api-reference/手写会话/在练习会话中进入焦点卡作答
/openapi/plugin-api.yaml post /writing/session-focus-enter
会话版焦点卡进入命令。该路由只应由当前练习面板在自己的窗口里调用。
# 在练习会话中进入留白作答
Source: https://dev.marginnote.com.cn/api-reference/手写会话/在练习会话中进入留白作答
/openapi/plugin-api.yaml post /writing/session-blank-enter
会话版留白进入命令。该路由只应由当前练习面板在自己的窗口里调用。
# 对齐练习会话记录
Source: https://dev.marginnote.com.cn/api-reference/手写会话/对齐练习会话记录
/openapi/plugin-api.yaml post /writing/session-reconcile
将练习会话记录与当前窗口观察到的状态对齐。该操作可能返回需要调用方重新读取的状态。
# 开始焦点卡分层练习
Source: https://dev.marginnote.com.cn/api-reference/手写会话/开始焦点卡分层练习
/openapi/plugin-api.yaml post /writing/focus-begin
在受控练习层里开始焦点卡作答。该族接口可能保留手写层租约,失败后不要盲目重试。
# 开始留白分层练习
Source: https://dev.marginnote.com.cn/api-reference/手写会话/开始留白分层练习
/openapi/plugin-api.yaml post /writing/blank-begin
在受控练习层里开始留白作答。调用前应先读取写作能力与当前窗口上下文。
# 恢复练习会话
Source: https://dev.marginnote.com.cn/api-reference/手写会话/恢复练习会话
/openapi/plugin-api.yaml post /writing/session-resume
恢复之前暂停的练习会话。恢复后仍需看 `ready` 与 `canSwitch` 判断当前是否能切题。
# 恢复练习会话手写层
Source: https://dev.marginnote.com.cn/api-reference/手写会话/恢复练习会话手写层
/openapi/plugin-api.yaml post /writing/session-layer-restore
恢复某次练习会话关联的手写层。调用后应重新读取会话记录或查询转场结果。
# 恢复练习手写层
Source: https://dev.marginnote.com.cn/api-reference/手写会话/恢复练习手写层
/openapi/plugin-api.yaml post /writing/layer-restore
将受控练习层恢复到当前窗口。恢复是否完成看转场结果,不要仅凭 HTTP 状态码判断。
# 暂停练习会话
Source: https://dev.marginnote.com.cn/api-reference/手写会话/暂停练习会话
/openapi/plugin-api.yaml post /writing/session-pause
暂停当前练习会话。该命令只写会话记录,不代表当前画布已经保存。
# 查询作答预览
Source: https://dev.marginnote.com.cn/api-reference/手写会话/查询作答预览
/openapi/plugin-api.yaml post /writing/session-preview
以只读方式取某一趟作答中已退出的尝试的专用层中已保存的笔迹,可以附带一张有
尺寸上限的预览图。
输入要精确到趟与尝试。不要传层身份、路径或渲染范围。
`state` 的含义见 `WritingPreviewResponse`。该预览不含未保存的画布、未分层的文字与图片框。
主数据上下文还有没保存的改动时会被拒,这是写入接口里唯一表示稍后重试的错误码。
尝试记录由独立的练习插件保管。从面板发起调用时,只有该练习插件可以查看这些记录。
从命令行发起调用时没有这一限制,可以直接取得预览。
# 查询手写层租约
Source: https://dev.marginnote.com.cn/api-reference/手写会话/查询手写层租约
/openapi/plugin-api.yaml post /writing/layer-leases
只读诊断:返回未收尾的临时手写层占用。上一次操作失联时先读该接口,再决定是否走恢复流程。
`POST /writing/layer-compact` 与该接口同属手写层分组,该组的开关当前是关着的。
# 查询留白与焦点存储身份
Source: https://dev.marginnote.com.cn/api-reference/手写会话/查询留白与焦点存储身份
/openapi/plugin-api.yaml post /writing/content
只读诊断:读某张本地题卡的来源候选、本地合并留白与焦点存储身份。
留白与焦点互斥,不能写在同一份请求里。要限制层用 `layerIds`:不传查全部层,
传一个空串查默认层,传空数组或超过 32 个会被拒。
该接口不创建、不认领、不折叠,响应里没有未保存的画布,记录数也不等于笔划数。
# 查询笔迹保存结果
Source: https://dev.marginnote.com.cn/api-reference/手写会话/查询笔迹保存结果
/openapi/plugin-api.yaml post /writing/pencil-result
用提交时的 `requestId` 查同一笔操作的最终结果。
`settled` 为真只说明返回调已返回,还要分别看 `outcome`、`persistedSnapshotVerified`
与 `snapshotCurrent`。`snapshotCurrent` 只描述返回调返回时的状态。
受理之后才出现的失败码走响应里的 `code`,不走错误对象里的 `code`。
查不到记录时返回 `404`,说明本进程里没有这笔,不等于没有保存过。
# 查询练习会话记录
Source: https://dev.marginnote.com.cn/api-reference/手写会话/查询练习会话记录
/openapi/plugin-api.yaml post /writing/session-read
读这台设备上的练习会话记录:每一趟、每次尝试、每次浏览,以及暂停与历史。
没有记录时返回空数组,状态码仍为 `200`。
记录里的状态是上一次事件留下的事实,不能作为现在可以写入的依据。
# 查询练习准备清单
Source: https://dev.marginnote.com.cn/api-reference/手写会话/查询练习准备清单
/openapi/plugin-api.yaml post /writing/plan-read
读这台设备上这本学习集的练习准备清单。该清单是准备记录,不代表已经进入作答。
# 结束练习会话
Source: https://dev.marginnote.com.cn/api-reference/手写会话/结束练习会话
/openapi/plugin-api.yaml post /writing/session-finish
将当前练习会话标记为完成。完成状态应以重新读取的会话记录为准。
# 获取手写上下文
Source: https://dev.marginnote.com.cn/api-reference/手写会话/获取手写上下文
/openapi/plugin-api.yaml post /writing/context
返回该面板所对的窗口与学习集,以及当前可见的各个工作面。该结果是观察所得,不构成保存或切题的许可,
因此响应里的 `readOnly` 为真、`mutationAuthority` 为假。
面板可以直接传空对象,窗口与身份由 MarginNote 注入,只能看到本面板所在的窗口。
不要自行传入窗口身份以指向其他窗口。窗口不在、学习集已换、窗口已关,都会拒绝。
工作面里可能出现内部留白页,那不是 PDF 页序,不要传给按页取内容的接口。
# 获取手写能力清单
Source: https://dev.marginnote.com.cn/api-reference/手写会话/获取手写能力清单
/openapi/plugin-api.yaml get /writing/capabilities
调用手写会话的其他路由之前须先读取该接口,确认 MarginNote 支持的子能力。
该接口返回能力清单,不构成可写许可。清单里某项开关为假时,对应的那一类操作当前不可用。
# 解析题卡来源
Source: https://dev.marginnote.com.cn/api-reference/手写会话/解析题卡来源
/openapi/plugin-api.yaml post /writing/resolve
只读诊断:传入一道题,返回该题的来源候选、本地合并留白与焦点存储身份。
同样不创建、不认领、不折叠。
# 记录练习评估
Source: https://dev.marginnote.com.cn/api-reference/手写会话/记录练习评估
/openapi/plugin-api.yaml post /writing/session-assess
为当前练习会话或指定尝试记录评估结果。字段细节以运行时 `GET /guide` 为准。
# 进入焦点卡作答
Source: https://dev.marginnote.com.cn/api-reference/手写会话/进入焦点卡作答
/openapi/plugin-api.yaml post /writing/focus-enter
从当前窗口进入焦点卡作答流程。是否真的可进入由运行时的 `ready`、`canSwitch` 与返回结果决定。
# 进入留白作答
Source: https://dev.marginnote.com.cn/api-reference/手写会话/进入留白作答
/openapi/plugin-api.yaml post /writing/blank-enter
进入题目对应的留白作答位置。该接口可能触发窗口转场,调用后应查询转场结果。
# 预留下一次练习尝试
Source: https://dev.marginnote.com.cn/api-reference/手写会话/预留下一次练习尝试
/openapi/plugin-api.yaml post /writing/session-reserve
为当前练习会话预留一次尝试。响应只表示该次命令的处理结果;之后应按 `requestId`
或重新读取会话记录确认。
# 列出已配对设备
Source: https://dev.marginnote.com.cn/api-reference/插件辅助/列出已配对设备
/openapi/plugin-api.yaml get /devices
返回本机已配对设备的摘要。该响应不包含令牌材料,只用于让调用方知道本机记录过哪些设备。
# 获取 PowerShell 刷新脚本
Source: https://dev.marginnote.com.cn/api-reference/插件辅助/获取-powershell-刷新脚本
/openapi/plugin-api.yaml get /skill/refresh.ps1
返回用于刷新本机 helper skill 的 PowerShell 脚本。响应不是 JSON,要用 `fetch` 按文本读取。
脚本只面向本机使用,不应在面板里自动执行。
# 获取 shell 刷新脚本
Source: https://dev.marginnote.com.cn/api-reference/插件辅助/获取-shell-刷新脚本
/openapi/plugin-api.yaml get /skill/refresh.sh
返回用于刷新本机 helper skill 的 shell 脚本。响应不是 JSON,要用 `fetch` 按文本读取。
脚本只面向本机使用,不应在面板里自动执行。
# 获取本机 helper skill
Source: https://dev.marginnote.com.cn/api-reference/插件辅助/获取本机-helper-skill
/openapi/plugin-api.yaml get /skill
返回本机 Agent 可安装的 MarginNote Bridge helper skill 内容与安装提示。该接口只读,不安装文件。
响应可能包含不同 Agent 的安装建议。不要把响应里的本机目录路径显示给最终用户。
# 获取操作上下文
Source: https://dev.marginnote.com.cn/api-reference/能力与状态/获取操作上下文
/openapi/plugin-api.yaml get /context
返回当前打开的学习集与文档、当前页、选中的卡与焦点卡。与 `GET /ui-state` 的分工:
该接口返回整理过的语义,`/ui-state` 返回界面自己的键值。
无法获取信息时仍返回 `200`,`available` 为 `false`,原因见 `reason`。
# 获取能力清单
Source: https://dev.marginnote.com.cn/api-reference/能力与状态/获取能力清单
/openapi/plugin-api.yaml get /capabilities
返回 MarginNote 实际注册的全部路由,以及各主题的上限与语义。
读该接口可取得各项上限与取值枚举,例如标签操作的取值、元数据的取值范围、
手写会话的上限。Request Body 的写法未收录,见 `GET /guide`。
# 获取运行时规程
Source: https://dev.marginnote.com.cn/api-reference/能力与状态/获取运行时规程
/openapi/plugin-api.yaml get /guide
返回一份 Markdown 文档,内容是 MarginNote 写给接入方的运行时规程:连接与鉴权、各接口的 Request Body 写法、
以及一批使用上的陷阱。
整张路由表里只有该接口返回 Markdown,因此 `mn.api` 无法读取。`mn.api` 相当于
`fetch('/bridge/v1' + path, opts).then(r => r.json())`,始终按 JSON 解析,
Markdown 会让 `mn.api` 抛出 `SyntaxError`。读取该接口须改用 `fetch`。
# 获取运行状态
Source: https://dev.marginnote.com.cn/api-reference/能力与状态/获取运行状态
/openapi/plugin-api.yaml get /status
返回 App 名称与版本、接口版本、注册的路由条数、路由目录校验值,以及运行规程的版本与哈希。
校验值变了就说明接口变了。把本地的路由清单和校验值存下来,启动时比对一次,
就能判断要不要重新获取 `GET /capabilities`。
# 使用 AI 辅助创作
Source: https://dev.marginnote.com.cn/zh/ai-dev
使用 AI 进行 Marginnote 模板与插件开发
本文档是基于 [Mintlify](https://mintlify.com/) 构建的 AI Native 文档站,这意味着可以让 AI 充分利用本站提供的各种接口服务进行快速阅读、检索、提问与创作。
复制给 AI 快速开始
请阅读 [https://dev.marginnote.com.cn/zh/ai-dev](https://dev.marginnote.com.cn/zh/ai-dev) ,理解文档、确认用户意图并协助用户进行 Marginnote 的插件和模板的开发。
# AI Native 服务
本文档提供 [`llms.txt`](/llms.txt)、[`llms-full.txt`](/llms-full.txt)、[`skill.md`](/skill.md) 与 MCP 服务,方便 AI 工具从中读取文档,为编写插件和模板提供参考。
## 接入到 AI 工具
点击页面顶部的`复制页面`按钮右侧的小箭头,可以快速将本文档接入到 ChatGPT、Claude 、VSCode等 AI 工具,也可直接以用户 MCP 的形式进行安装,或复制页面文本发送给 AI 工具。
> [!TIP]
>
> 更建议使用 MCP 配合插件开发,这也将帮助我们改进文档
# 按 MN Rails 规范生成代码
虽然您可以按照文档介绍来逐步构建自己的插件,但更建议使用[MN Rails](/zh/mnrails)工具来创建插件工程。
MN Rails 能够快速生成符合 Marginnote 插件规范的工程目录结构,并提供了丰富的模板和脚手架,帮助开发者快速上手插件开发,并引导 AI 进行更有效、更有质量的开发。
# AI 生成提醒
Source: https://dev.marginnote.com.cn/zh/api-ai-warning
> [!WARNING]
>
> API 参考由 AI 测试后自动化生成,暂未完全经过人工校对,请谨慎参考。
> [!NOTE]
>
> 标有`⚠️`的字段为 AI 测试中未见字段,建议实测后使用。
# 最小打包
Source: https://dev.marginnote.com.cn/zh/basic-package
模板包的文件构成、可以导入的最小模板、覆盖规则与各字段的对应文章
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
`场景模板` 决定场景界面上按钮、工具与菜单的组成与位置。应用一份场景模板之后得到的界面称为场景界面(旧称沉浸模式),与之相对的是传统界面。
写完场景插件和插件面板(index.html)之后,还需要一份模板,界面上才会出现对应的入口。下面先说模板包的文件构成,再提供一份可以直接导入的最小模板。
# 模板包的结构
模板包是扩展名为 `.mntemplate` 的压缩文件,解压之后的目录结构如下:
**起步-0.1.0.mntemplate**
**template.json**
**manifest.json**
**localizations.json**
**plugins**
**mn.hello**
**mnaddon.json**
**index.html**
**hello.png**
各文件的用途:
* `template.json`:模板配置。界面元素的组成、偏好的预设值、AI 任务的声明,都写在这份文件里。以下先讲这份文件,再补充 `manifest.json` 与 `localizations.json`。
* `manifest.json`:包目录。记录包的目标应用、包含的插件清单与所需的最低 MarginNote 版本。
* `localizations.json`:模板名称的多语言取值。
* `plugins/<插件id>/`:模板用到的插件,按插件 id 分目录,原样放进包内。
其中 `template.json` 与 `manifest.json` 是 MarginNote 识别模板所必需的。界面上的按钮、菜单项与面板都由插件提供,因此模板与插件一起分发。
# 最小模板
`template.json` 里只有 `id` 与 `name` 是必填字段:
```json template.json theme={null}
{
"id": "u-hello",
"name": "你好世界"
}
```
`manifest.json` 示例:
```json manifest.json theme={null}
{
"format": 1,
"app": "MarginNote",
"appVersion": "4.5.0",
"exportedAt": 1700000000,
"template": {
"id": "u-hello",
"name": "你好世界"
},
"plugins": [
{
"pluginID": "mn.hello",
"folder": "mn.hello",
"version": "0.1.0",
"title": "你好世界"
}
]
}
```
* `format`:包格式的版本号。
* `app` 与 `appVersion`:目标应用与所需的最低 MarginNote 版本。
* `template`:模板的 `id` 与 `name`,需与 `template.json` 一致。
* `plugins`:包内插件的清单。`pluginID` 与 `folder` 取插件的 `addonid`,`version` 与 `title` 取 `mnaddon.json` 中的同名字段。
`localizations.json` 按界面语言给出模板名称:
```json localizations.json theme={null}
{
"zh-Hans": {
"你好世界": "你好世界"
},
"en": {
"你好世界": "Hello World"
}
}
```
> [!NOTE]
>
> 模板界面上的文字(例如模态名称、笔与摘录工具的名称)按界面语言取值,模态的写法见[工具与模式](/zh/template-bottom)。取不到对应语言时改用英文。
# 打包与导入
把上例中的文件按目录结构放进一个文件夹,压缩为 zip,再把扩展名改成 `.mntemplate`,便得到一个模板包。
导入步骤:进入场景界面,打开顶部的 `场景模板` 菜单,选择 `编辑场景模板…`,在左栏底部选择 `管理场景模板`,再选择 `导入场景模板…`。导入之后,模板出现在场景界面顶部的场景模板菜单里,位于 `我的场景模板` 一组中,并立即成为当前模板。带插件的包会一并安装插件:本机未安装的插件执行安装,包内版本高于本机版本的替换本机版本,版本相同或本机版本更高的跳过安装。
> [!NOTE]
>
> 未认证的插件需要先在 `设置` 里允许才能安装;未允许时,整份模板包会被拒绝导入,不会留下半份模板。模板导入成功后插件被卸载或取消允许时,模板仍然存在,模板编辑器里该插件标注为 `(不可用)`。
# 覆盖规则
模板中的每个位置都遵循同一条规则,这条规则称为覆盖规则:字段写出取值时,以写出的内容为准;留空表示不接管,MarginNote 保持现状。
这条规则决定了模板的写法:留空表示 MarginNote 保持现状,不表示清空。想让某个位置的条目比默认更少,必须把要保留的条目显式列出来。
因此不要先复制现成模板再修改,复制会把原模板的声明一并继承下来,且难以察觉。例如复制一份复习模板得到的模板,会继承复习用的标签前缀、摘题按钮,以及一条要求插件写入特定字段的复习规则(标签前缀见[AI 任务](/zh/template-ai),复习规则见[行为设置](/zh/template-settings));如果配套插件不写这个字段,这条规则就永远匹配不上。
> [!WARNING]
>
> 模板里的字段名或代号写错时,MarginNote 不会报错,而是静默忽略该条目,表现为对应的按钮没有出现。字段的取值类型写错时,整份模板无法解析,MarginNote 回退到默认模板。排查办法见[校验与排错](/zh/template-validate)。
# 身份字段
除 `id` 与 `name` 之外,`template.json` 顶层还有以下字段,用于控制模板本身:
| 字段 | 类型 | 说明 |
| - | - | - |
| `id` | `String` | 模板的唯一标识。 |
| `name` | `String` | 模板在界面上的名称。 |
| `schema` | `Int?` | 配置格式的版本号。留空表示不接管。 |
| `minAppVersion` | `String?` | 所需的最低 MarginNote 版本。留空表示不限制 MarginNote 版本。 |
| `base` | `String?` | 这份模板的来源,即复制自的系统场景模板。在模板编辑器(编辑场景模板配置的界面)中复制系统场景模板时,该字段自动产生;手写模板时无需填写。 |
`schema` 与 `minAppVersion` 都是版本检查字段。`schema` 检查配置格式的版本,只在配置格式整体变更时才需要改动;`minAppVersion` 检查 MarginNote 版本,模板用到 MarginNote 在某一版本之后才提供的能力时声明该字段,导入时,版本不足的 MarginNote 会拒绝该模板。文件导入同样会检查,不限于从模板库安装。
# 样例与模板库
模板可以附带一份样例,导入模板的人打开即可看到效果。样例文件放在包内 `sample/` 目录下:
| 字段 | 类型 | 说明 |
| - | - | - |
| `sample` | `Sample?` | 随模板分发的样例。 |
| `gallery` | `GallerySource?` | 该模板来自模板库的记录。 |
`Sample` 的字段:
| 字段 | 类型 | 说明 |
| - | - | - |
| `title` | `String` | 样例的标题。 |
| `summary` | `String?` | 样例的简介。 |
| `file` | `String` | 样例学习集文件在包内 `sample/` 目录下的文件名。 |
| `topicIds` | `[String]?` | 样例包含的学习集标识。 |
| `sizeBytes` | `Int?` | 样例文件的字节数。 |
| `language` | `String?` | 样例内容的语言。 |
`GallerySource` 的字段:
| 字段 | 类型 | 说明 |
| - | - | - |
| `id` | `String` | 模板库中的标识。 |
| `version` | `String` | 模板库中的版本号。 |
> [!WARNING]
>
> 样例中的全部卡片与文档会随模板一起分发,导入模板的人都能读到。添加样例之前需要确认内容可以公开。
# 从最小模板往上加
模板可以逐步扩充:只有 `id` 与 `name` 的模板不接管任何位置,导入后界面不会有任何变化,MarginNote 保持默认行为。每多写一个字段,就多接管一处:
| 想接管的内容 | 要写的字段 | 对应文章 |
| - | - | - |
| 底部工具条 | `bottom` | [工具与模式](/zh/template-bottom) |
| 顶部工作栏、浮窗 | `surfaces` | [顶部工作栏与浮窗](/zh/template-surfaces) |
| 卡片与摘录菜单 | `selectionMenu`、`excerptShortcuts` | [卡片与摘录菜单](/zh/template-menu) |
| 插件入口 | `plugins`、`pluginConfigs`、`addon.` 代号 | [接入插件](/zh/template-plugins) |
| AI 任务 | `aiTasks` | [AI 任务](/zh/template-ai) |
| 进入场景界面后的行为 | `settings`、`home`、`review`、`resume`、`coach` | [行为设置](/zh/template-settings) |
该表只列常用字段,其余顶层字段见上文的字段表。
# 数据交互
Source: https://dev.marginnote.com.cn/zh/data-interact
插件的面板可以与 Marginnote 进行通信来获取信息、改写信息以及执行操作。
# 请求方式
请求方式分为 `GET` 和 `POST`,前者可以直接请求,后者需显示指定 method,例如:
```javascript GET theme={null}
const state = await mn.api('/ui-state');
const topicid = state.state.topicid;
```
```javascript POST theme={null}
await mn.api('/notes/addon-values', {
method: 'POST',
body: { noteIds: [noteid], values: { status: 'done' } }
});
```
# 可用接口
插件可调用的接口由 Marginnote CLI 所有可用接口裁剪而来,所有插件可用的接口见 `API 参考` 。
`/bridge/v1` 之外,页面还能访问面板本地的其他路径,包括读取插件包内的文件、读取图片与调整控制面板尺寸。这些路径不经过 MarginNote,也不受白名单限制,写法见[路径系统](/zh/paths)。
# 错误处理
`mn.api` 不使用 reject 表示失败,而是把错误对象作为正常的响应体返回。这意味着无法使用 `try...catch` 捕获异常的请求。
下面是一个错误的例子:
```json theme={null}
{
"error": {
"code": "IN_PROCESS_NOT_ALLOWED",
"message": "该接口不在网页面板的白名单上。",
"retryable": false
}
}
```
判断一次调用是否成功,应先检查响应中是否包含 `error` 字段,再结合具体的请求进行判断:
```javascript theme={null}
const r = await mn.api('/notes/' + noteid + '/edits', {
method: 'POST',
body: { ops: [{ op: 'setTitle', text: '新的标题' }] }
});
if (r.error) {
mn.log(r.error.code, r.error.message);
} else if (r.persisted) {
mn.log('已写入并核对通过');
}
```
# 返回值非 JSON 的接口
`mn.api(path, opts)` 等效于在路径`/bridge/v1`上的 fetch 行为,并**返回JSON**:
```javascript theme={null}
fetch('/bridge/v1' + path, opts).then(r => r.json())
```
接口返回的内容不是 JSON 时,调用会在解析阶段抛出 `SyntaxError`,无法获取原始内容。这类接口应改用 `fetch`进行调用。例如:
* `GET /guide` 返回 Markdown 正文。
* `GET /notes/{noteid}/render` 返回卡片长图或 PDF。
* `GET /documents/{bookmd5}/pages/{index}/content/{contentId}` 返回图片。
* `GET /bundles/{bundleId}/files` 带 `path` 时返回该文件的原始字节。
按响应内容的类型选择读取方式:
```javascript theme={null}
// 响应的内容是 Markdown
const text = await (await fetch('/bridge/v1/guide')).text();
// 响应的内容是字节流
const shot = await fetch('/bridge/v1/notes/' + noteid + '/render?format=png&width=880');
const blob = await shot.blob();
```
关于路径`/bridge/v1` 的行为以及其他可用路径,参见 [路径系统](/zh/paths) 。
# 完整示例
以摘录制卡(如单词卡)为例Ï,一个通常的处理是:\
插件获取当前上下文的信息后,据此取出卡片数据中的相关信息,最后根据实际需求为处理完成的卡片进行标记。
```javascript theme={null}
mn.on('excerpt', async d => {
// 一、获取当前界面状态,得到学习集与聚焦的卡片
const ui = await mn.api('/ui-state');
const topicid = ui.state.topicid;
// 二、获取该卡片的标题、正文、标签与插件字段
const got = await mn.api('/notes/batch-get', {
method: 'POST',
body: { noteIds: [d.noteid], addons: ['mn.hello'] }
});
const card = got.items[0];
if (!card.found) return;
// 处理数据
// 三、把结果写回本插件自己的字段
await mn.api('/notes/addon-values', {
method: 'POST',
body: { noteIds: [d.noteid], values: { status: 'done', topicid } }
});
});
```
# 事件系统
Source: https://dev.marginnote.com.cn/zh/events
插件面板需要获知 MN 内部发生的事情时,可以通过事件系统接收。事件必须先声明,而后才能在面板中收到来自 MN 的推送。
# 声明事件
事件需要声明后才能被插件接收,否则对其的监听无效。
声明事件需在 `mnaddon.json` 中增加 `events` 字段:
```json mnaddon.json highlight={9} focus={9} theme={null}
{
"addonid": "mn.hello",
"title": "你好世界",
"version": "0.1.0",
"author": "我",
"marginnote_version_min": "4.5.0",
"panel": "index.html",
"icon": "hello.png",
"events": ["excerpt", "comments"]
}
```
目前可以声明的事件如下:
* `excerpt`:创建摘录。
* 返回`{noteid, opened}`。
* 由于创建摘录时会以 mini 模式唤起所有接收符合条件的事件的面板,所以当唤起成功时 `opened` 为 true,失败或者已经打开面板时为 false
* `comments`:卡片批注有变化(包括折叠展开、新增、修改等)。
* 返回`{noteid?,noteids}`。
* 摘录卡片的批注变化时,脑图中的卡片和文档中的标注卡片均会包含在 `noteids` 里,但 `noteid` 仅为被修改的脑图中的卡片
* `practice`:作答纸(整卡截图批注)编辑器关闭。⚠️仅由当前插件打开的作答纸关闭时才可触发。
* 返回 `{noteid, action, saved, commentId}`
* `synced`:同步合并完成。
* 无返回值
* `undo`:撤销或重做。
* 无返回值
* `imported`:卡片被批量添加进学习集。
* 无返回值
# 监听事件
```javascript theme={null}
mn.on('excerpt', function (d) {
mn.api('/notes/batch-get',
{
method: 'POST',
body: { noteIds: [d.noteid] } }
).then(function (r) { render(r); });
});
```
> [!WARNING]
>
> 对于同一事件、同一张卡,在 1.5 秒内的重复事件会被丢弃
> [!NOTE]
>
> 页面加载完成之前推来的事件会进入队列,加载完成后会统一发放
# 摘录事件筛选
面板关闭时用户进行摘录操作,MN 会先用 `mini` 模式打开面板,再推送摘录事件,事件中的 `opened` 为 `true`。如有需要,可以在完成处理后调用 `mn.close()` 进行关闭。
如果只关心某一类摘录,可以用 `excerptClaim` 进行限制:
```json highlight={15-18} focus={15-18} theme={null}
{
"addonid": "mn.vocab",
"author": "MarginNote",
"title": "Reading Vocabulary",
"version": "0.13.1",
"marginnote_version_min": "4.5.0",
"immersiveOnly": true,
"panel": "index.html",
"icon": "vocab2.png",
"panelWidth": 420,
"panelMaxHeight": 620,
"events": [
"excerpt"
],
"excerptClaim": {
"hashtag": "#VocabPending",
"configKey": "pendingTag"
}
}
```
只有包含 `#VocabPending` 标签的新摘录事件才会触发该插件中的`excerpt`事件。
`configKey` 可选,配置之后此标签可以由模板配置覆盖。如上配置时,取值为模板里 `pluginConfigs["mn.vocab"]["pendingTag"]`。
不写 `excerptClaim` 时,所有摘录行为均会唤起插件面板并触发`excerpt` 事件。
# 卡片菜单命令
`cardCommand` 不需要在 `events` 中声明。用户在卡片菜单里点了声明过的命令之后会触发`cardCommand`事件:
```javascript theme={null}
mn.on('cardCommand', function (d) {
var commandID = d.id
var noteID = d.noteID
});
```
卡片命令具体配置参见[卡片与摘录菜单命令](/zh/menu-commands)。
# 简介
Source: https://dev.marginnote.com.cn/zh/introduction
# 准备定制你的Marginnote
在 Marginnote 5 之前(MN3、MN4),对 MN 的主要客制化手段为`经典插件`。而自 MN5 起,您可以通过新的`场景插件` 来进一步实现各种丰富的功能,并使用`Marginnote 场景模板` 将您的插件与各种资源、配置进行组合。
# Marginnote场景模板是什么
`Marginnote 场景模板`(后续会简称为 *MN 场景模板、MN模板、场景模板、模板* 等)是 Marginnote 5 版本开始的一个新增功能,旨在帮助用户构建个人学习工作流并专注于每一个具体的环节和场景,并拓展每个场景的无限可能。
具体来说,每个模板由`模板配置`、`插件`与`样例`构成:
* 模板配置:工具与模式、顶部工具栏、卡片摘录与菜单、浮窗与浮球、插件启用、行为设置等。
* 插件:可选择启用`场景插件`和`经典插件`。插件可自定义行为与界面,实现更具个性化的配置。
* 样例:随模板附带的学习资料或者体验材料,应当能够帮助用户快速上手该模板。
* 除此之外,每个模板都**必须要有**`manifest.json` ,使得MN能够识别并显示该模板。
值得注意的是,以上三个均为可选项。也就是说,一个最小的场景模板可以没有任何实际内容,功能与沉浸模式相同。
# 面板之外
Source: https://dev.marginnote.com.cn/zh/jsaddon
除了使用 Web 技术栈进行标准的场景插件开发外,也可在插件根目录增加 `main.js`,以融合场景插件和经典插件的方式进行插件开发。
使用该方式进行开发的场景包括但不限于:
* 需要访问 MN 的原生对象
* 注册生命周期方法
* 调整面板属性
* 持久化存储(如插件设置)
* 通过 JSB 使用网页环境未提供的原生能力
`main.js` 运行在插件的 JavaScriptCore 环境里,通过 JSB 调用 MN 暴露的原生对象。
# 文件结构
最小结构如下:
**project**
**mnaddon.json**
**index.html**
**main.js**
`mnaddon.json` 仍然用 `panel` 指定 HTML 面板入口:
```json mnaddon.json highlight={8} theme={null}
{
"addonid": "mn.hello",
"title": "示例插件",
"version": "0.1.0",
"author": "插件作者",
"marginnote_version_min": "4.5.0",
"immersiveOnly": true,
"panel": "index.html"
}
```
`main.js` 需要放在插件根目录。MN 加载插件时会**立即**执行该文件,并调用其中的 `JSB.newAddon(mainPath)`。该函数需要返回一个继承自 `JSExtension` 的类。
# 插件入口
加入 `main.js` 后,插件入口将变为 `main.js`。下面是一种经典写法:
```javascript main.js theme={null}
JSB.newAddon = function (mainPath) { // 插件入口。mainPath 表示插件包根目录
var AddonClass = JSB.defineClass(
"HelloAddon : JSExtension", // 插件名 : JSExtension
{
sceneWillConnect: function () {}, // 在插件即将启用时触发
notebookWillOpen: function (topicid) {}, // 在学习集打开时触发
notebookWillClose: function (topicid) {} // 在学习集关闭时触发
// 添加更多实例成员...
},
{
addonDidConnect: function () {},
addonWillDisconnect: function () {},
// 添加更多类成员...
}
);
return AddonClass;
};
```
> [!NOTE]
>
> 在`JSB.defineClass`定义的实例成员中,使用`self`而非`this`来引用 JSB 定义的类(如`JSExtension`)的实例。关于 JSB 定义的类的可用成员详见[使用 MN 原生对象](#使用-mn-原生对象)
# 手动控制面板
加入 `main.js` 后,点击插件按钮将**不再**自动打开面板,面板的创建、加载、显示和清理都要由插件完成。
例如下面定义了一个打开面板并设置最大高度的函数。
```javascript main.js theme={null}
const openPanel = (context) => {
const panel = AddonWebPanel.panel();
panel.maxContentHeight = 760;
panel.loadPanel();
panel.showFloatingIn(Application.sharedInstance().studyController(context.window));
return panel;
};
```
注意,由于显示面板需要传入`studyController`实例,而`studyController`实例又需要通过插件实例获取,因此该函数需要为 `context` 参数传入在 JSB 定义的类的实例成员方法中的 `self` 。
# 双向通信
插件面板与`main.js`之间可以相互调用并传递数据。
> [!WARNING]
>
> 传递的数据必须是可JSON.stringify的
## 从面板调用 main.js
面板调用 `mn.call(name, payload)` 时会触发 `main.js`的 `panel.onMessage(name, payload)`。
`onMessage` 的返回值会进行序列化作为 `mn.call()` 的结果返回页面。
下面是具体例子:
```javascript main.js theme={null}
const bindPanelMessage = (panel, context) => {
panel.onMessage = (name, payload) => {
if (name === "showMessage") {
Application.sharedInstance().showHUD(payload.text, context.window, 2);
return { ok: true };
}
return {};
};
};
```
面板写法如下:
```html index.html theme={null}
```
`mn.call`返回 Promise 的两种情况:
* resolve:正常返回、`ADDON_CALL_FAILED`、`ADDON_BAD_RESULT`
* reject:`ADDON_THREW`
## 从 main.js 调用面板
`main.js` 可以通过调用 `panel.post(event, data)` 向已经加载的面板发送事件。面板通过 `window.addEventListener("mn:", handler)` 接收事件,并从 `event.detail` 读取数据。
下面是具体例子:
```javascript main.js theme={null}
const sendStatusToPanel = (panel, message) => {
panel.post("status", {
message: `main.js发来消息:${message}`
});
};
```
面板写法如下:
```html index.html theme={null}
```
> [!TIP]
>
> `post()` 在页面加载完成前调用时,事件会先被缓存,并在页面就绪后投递。
# 使用 MN 原生对象
`main.js` 可访问 JSB 暴露的原生对象,例如 `Application`、`Database`、`NSFileManager`、`NSUserDefaults`、`NSTimer`、`UIColor`、`UIViewController` 等。
例如:
```javascript theme={null}
// 在 JSB 定义的类的实例成员方法里
const app = Application.sharedInstance();
const studyController = app.studyController(self.window);
const database = Database.sharedInstance();
```
完整对象、属性与方法请参考 [MarginNote 插件开发文档](https://mn-docs.museday.top)。
# 面板对象常用方法
## 创建和加载
### `AddonWebPanel.panel()`
创建一个新的面板对象。
### `panel.loadPanel()`
按 `mnaddon.json` 的 `panel` 字段读取包内 HTML 文件。文件不存在、未声明 `panel` 或路径越界时返回 `false`。这是加载面板的推荐方式。
### `panel.loadHTML(html)`
直接加载一段 HTML 字符串作为面板的源代码。
## 双向通信
### `panel.onMessage = function (name, payload) { ... }`
接收面板侧 `mn.call(name, payload)` 的入口。
### `panel.post(event, data)`
从 `main.js` 向面板页面以 `mn:` 的命名格式发送事件。
## 媒体数据
### `panel.putMedia(key, data, type)`
将媒体数据存入面板。面板可通过 `/media/` 获取该数据。`data` 为空时,对应的数据和键将会被删除。
* `type`: MIME TYPE,如`'image/jpeg'`
### `panel.clearMedia()`
清空当前面板的媒体缓存。较大的媒体数据建议通过媒体缓存传递,避免放入 JSON 或 `post()` 数据。
## 显示和隐藏
### `panel.showFloatingIn(studyController)`
将面板作为常驻浮层显示到学习集控制器中。面板已经显示时,再次调用会将面板置于前台并刷新布局。
### `panel.showFloatingInMode(studyController, mode)`
按指定形态显示面板。`mode` 可使用 `"mini"` 或 `"workspace"`。
### `panel.hideFloating()`
收起浮层,并触发面板清理。插件主动关闭面板时,优先调用该方法。
## 资源清理
### `panel.teardown()`
显式断开面板、WebView、回调和未完成请求之间的引用关系,避免旧面板继续占用资源。`hideFloating()` 会一并执行清理。插件通过其他展示方式关闭面板时,应主动调用 `teardown()`。
# 为插件设置图标
Source: https://dev.marginnote.com.cn/zh/logo
# 图标要求
图标文件需要是 PNG 图片,尺寸建议采用 64×64 ,但也可以根据实际需求采用更大或更小的图标。
MN 绘制图标时会将图标当作 alpha 蒙版,只保留形状轮廓,用界面颜色绘制成单色。因此图标需要保证图形以外的区域全部透明。
# 风格指南
### 1. 聚焦核心剪影
单色渲染完全依赖外轮廓传达语义。设计时应提炼对象最具辨识度的特征,避免冗余的修饰性细节。小尺寸下,复杂的线条容易黏连失真,越清晰简洁的剪影越具有可读性。
### 2. 利用负空间构图
由于不支持多色分层,图标内部的层次、细节与分隔必须通过透明的负空间(镂空)来呈现。镂空缝隙需保持足够间距(建议不小于 2 像素),防止在小尺寸渲染时因抗锯齿而闭合。
### 3. 避免半透明与模糊
图形内容应保持 100% 不透明度。虽然 Alpha 通道支持透明度阶调,但低不透明度像素在与界面前景色混合后容易发虚或显得脏浊。明确的硬边缘才能确保在各类底色上呈现锐利清晰的质感。
### 4. 平衡视觉量感
图标应在 64×64 像素的画布中居中对齐,并保持统一的视觉重量:
* **留白规范**:画布四周建议保留 4-6 像素的安全边距,避免图形撑满画布带来压迫感。
* **光学对齐**:当图标形态不对称时,应依据视觉中心而非几何中心对齐;方形图标需适度内缩,长条形或圆形可适当扩展,以保证并排展示时体量一致。
### 5. 规范笔画与转角
* **笔画宽度**:描边与线条建议维持在 3~5 像素,避免使用细于 3 像素的线条。
* **转角特征**:线条端点与折角宜采用微圆角处理,使视觉质感平滑现代,并与原生界面规范保持一致。
# 配置描述文件
建议在 `mnaddon.json` 中用 `icon` 字段指定图标文件:
```json mnaddon.json highlight={8} focus={8} theme={null}
{
"addonid": "mn.hello",
"title": "你好世界",
"version": "0.1.0",
"author": "我",
"marginnote_version_min": "4.5.0",
"panel": "index.html",
"icon": "hello.png",
"events": ["excerpt", "comments"]
}
```
MN 将按如下顺序采用图标
1. 采用 `icon` 字段中配置的图标。
2. `icon`字段未配置或无效时,采用与 `addonid` 末段同名的 PNG图标。 例如,`addonid` 为 `mn.hello` 时,采用 `hello.png`。
3. 仍未找到时,使用插件包内唯一的 PNG图标。
如果上述流程均失败,插件将无图标。
> [!NOTE]
>
> `icon`字段需相对插件包的根目录书写。路径中出现 `..` 或者使用绝对路径时,MN 会拒绝导入这个插件包。
# 卡片与摘录菜单命令
Source: https://dev.marginnote.com.cn/zh/menu-commands
MN 的弹出菜单(如摘录、卡片)中可以加入插件自己的命令。
# 声明命令
命令在 `mnaddon.json` 的 `cardCommands` 字段中声明。
```json mnaddon.json theme={null}
{
"addonid": "mn.starter",
"title": "起步",
"version": "0.3.0",
"marginnote_version_min": "4.5.0",
"panel": "index.html",
"cardCommands": [
{
"id": "annotate",
"title": { "zh-Hans": "添加批注", "en": "add a comment" },
"when": { "addonValue": "status", "equals": "done" }
}
]
}
```
命令可用的字段如下:
* `id` 是命令的标识,模板依据它引用这条命令。
* `title` 是命令在菜单中显示的文字。可以为字符串或键值分别为语言和内容的字典,字典的键为 `zh-Hans`、`zh-Hant`、`en` 等。
* `when` 用来限定这条命令出现在哪些卡片上。该字段可以不写,不写时每张卡片都会出现这条命令。
## when 条件与组合条件
`when` 决定了命令是否在菜单中显示。判断过程不会执行插件的页面代码,因此其中只能使用卡片上的既有数据,以及插件先前存入卡片的字段。可用的条件分以下几类。
判断插件存入卡片的字段,使用 `addonValue`。它可以比较字段的值,也可以判断字段是否存在:
```json theme={null}
{ "addonValue": "status", "equals": "done" }
{ "addonValue": "status", "in": ["a", "b"] }
{ "addonValue": "status", "exists": false }
```
> [!NOTE]
>
> `addonValue`的添加参见下文 [特殊处理](#特殊处理)
判断卡片自身的状态,使用下列写法:
```json theme={null}
{ "hashtag": "#practice/question" }
{ "hasExcerpt": true }
{ "hasTitle": true }
{ "hasChildren": false }
```
其中 :
* `hashtag` 判断卡片上是否带有指定的标签
* `hasTitle` 判断卡片是否有标题
* `hasChildren` 判断卡片在脑图上是否有子卡。
* `hasExcerpt` 判断卡片是否带着从文档摘来的内容,其含义与 `batch-get` 返回的 `sourceAnchor.hasExcerpt` 相同。
判断卡片在脑图上的位置关系,使用 `parent`、`child` 与 `descendant`,它们分别表示父卡、子卡、任意一张下层卡 满足里面的条件。
### 关系组合
条件可以用 `and`、`or` 与 `not` 组合起来:`and` 要求数组中的条件全部成立;`or` 只要求其中一个成立;`not` 表示不能满足某个条件。其中`and` 与 `or` 接受一个条件数组。\
这几种组合可以互相嵌套,一个数组里的某一条条件本身也可以是另一种组合。
### 例1
以下面 `branch` 命令为例,它要求卡片本身既不带 `#practice/question` 标签,也不带 `#practice/answer` 标签,并且它的子卡同样不带 `#practice/answer` 标签。
```json mnaddon.json theme={null}
{
"id": "branch",
"when": {
"and": [
{ "not": { "hashtag": "#practice/question" } },
{ "not": { "hashtag": "#practice/answer" } },
{ "child": { "not": { "hashtag": "#practice/answer" } } }
]
},
"title": {"zh-Hans": "练习该分支", "en": "Practise this branch"}
}
```
### 例2
以下面 `choose_sense` 命令为例,两种情况满足其中一项时才会出现:卡片的 `status` 字段等于 `pending`,或者卡片没有 `status` 字段并带有 `#VocabPending` 标签:
```json mnaddon.json theme={null}
{
"id": "choose_sense",
"when": {
"or": [
{ "addonValue": "status", "equals": "pending" },
{ "and": [
{ "not": { "addonValue": "status", "exists": true } },
{ "hashtag": "#VocabPending" }
] }
]
},
"title": {
"zh-Hans": "选一个意思",
"zh-Hant": "選一個意思",
"en": "Pick a sense"
}
}
```
### 特殊处理
如果判断依据不属于以上任何一项,可以先将结论存成卡片字段,菜单再依据这个字段来判断:
```javascript theme={null}
await mn.api('/notes/' + id + '/edits',
{
method: 'POST',
body: {
ops: [{
op: 'setAddonValues',
addon: 'mn.starter',
values: {status: 'done'}
}],
confirm: true
}Ï});
```
# 在菜单中添加命令
命令在插件中声明之后,还需要由模板将其添加到菜单中。**模板**的 `selectionMenu` 字段登记了 MN 的各个弹出菜单。
* `cards` 为卡片菜单,在脑图或大纲中点击一张卡片时弹出。
* `documents` 为文档上的卡片菜单,点击完成摘录的文档中的卡片时弹出。
* `text` 为选中文本的菜单,在文档中选中一段文字时弹出。
若要添加命令到指定菜单,需将 `addon.<插件id>.<命令id>:` 添加到对应项的值列表中,**末尾的冒号不能省略**:
```json template.json theme={null}
{
"selectionMenu": {
"cards": [
"noteHighlight:",
"commentNote",
"addon.mn.starter.annotate:"
],
"documents": [
"sendHighlight:",
"noteHighlight:",
"addon.mn.starter.annotate:"
]
}
}
```
# 接收命令
页面不需要在 `events` 中登记 `cardCommand`,直接在页面代码中监听即可:
```javascript index.html theme={null}
mn.on('cardCommand', function (d) {
var commandID = d.id
var noteID = d.noteID
});
```
> [!NOTE]
>
> 用户点击命令时,如果插件面板完全关闭,MN 会先以 `mini` 模式打开面板,再推送命令事件。
# 使用 MN Rails 创建插件工程
Source: https://dev.marginnote.com.cn/zh/mnrails
快速创建、构建、打包与部署 MarginNote 插件
MN Rails 是 MarginNote 插件的现代脚手架。为开发者提供方便的工程创建、构建、打包与部署功能,并提供了丰富的模板和脚手架,帮助开发者快速上手插件开发。
# 创建工程
使用 create 命令创建工程:
```bash npm theme={null}
npm create mn-rails@latest
```
```bash pnpm theme={null}
pnpm create mn-rails@latest
```
```bash yarn theme={null}
yarn create mn-rails
```
```bash bun theme={null}
bun create mn-rails
```
运行后将以交互式方式创建工程:
```
❮ pnpm create mn-rails@latest
? 选择框架
❯ React(React 19 + TypeScript + Vite)
Vue(Vue 3 + TypeScript + Vite)
Svelte(Svelte 5 + TypeScript + Vite)
↑↓ navigate • ⏎ select
```
## 非交互创建
全部参数可以通过命令行传入:
```bash theme={null}
npx mn-rails@latest init my-addon \
--framework react \
--kind panel \
--addonid com.example.my-addon \
--author Example \
--title 我的插件 \
--package-manager pnpm \
--no-git # 不建议设置此项
```
显式指定的目标目录必须为空目录。省略目录参数且当前目录存在可见内容时,创建命令在当前目录下生成一个由用户确认的子目录。
## 框架与形态
| 框架 | 模板内容 |
| - | - |
| React | React 19、TypeScript 与 Vite |
| Vue | Vue 3、TypeScript 与 Vite |
| Svelte | Svelte 5、TypeScript 与 Vite |
工程提供的插件形态如下:
* 纯面板:即标准的`场景插件`,页面入口为 `index.html`,插件面板与菜单由 MarginNote 根据 `mnaddon.json` 生成。
* 混合形态:即混合`场景插件`与`经典插件`,包含 Web 页面与双端通信模块。见[面板之外](/zh/jsaddon)
# 工程结构
**my-addon**
**index.html**
**mnaddon.json**
**eslint.config.mjs**
**vite.config.ts**
**package.json**
**eslint**
**framework.mjs**
**src**
**App.tsx**
**main.tsx**
**bridge.ts**
**public**
**icon.png**
**template**
**template.json**
**localizations.json**
**native**
**main.js**
**PanelBridge.js**
`native/` 只出现在混合形态工程中。纯面板工程不创建这个目录,构建产物也不得包含根目录的 `main.js`。
主要文件与作用:
| 路径 | 作用 |
| - | - |
| `index.html` | 面板入口。 |
| `mnaddon.json` | 插件清单,声明插件标识、名称、版本、入口与事件。 |
| `eslint.config.mjs` | 共享 ESLint 配置。 |
| `eslint/framework.mjs` | React、Vue 或 Svelte 的框架规则。 |
| `src/` | React、Vue 或 Svelte 页面代码。 |
| `public/` | Vite 原样复制到产物根目录的静态资源。 |
| `template/template.json` | 场景模板配置。 |
| `template/localizations.json` | 场景模板名称的本地化内容。 |
| `vite.config.ts` | Vite 配置。 |
# 工程命令
工程提供以下脚本:
| 命令 | 作用 |
| - | - |
| `lint` | 运行 ESLint 与框架推荐规则。 |
| `lint:fix` | 自动修复可修复的 ESLint 问题。 |
| `typecheck` | 运行当前框架的类型检查。 |
| `check` | 依次运行 ESLint 与类型检查。 |
| `build` | 调用 Vite 构建页面,并检查插件目录与 `/pkg/` 引用。 |
| `package` | 生成 `.mnaddon` 与 `.mntemplate` 安装包。 |
| `sync` | 把构建产物同步到本机 MarginNote,并重启应用。 |
| `doctor` | 检查工程、构建产物与 MarginNote 环境。 |
```bash npm theme={null}
npm run lint
npm run lint:fix
npm run typecheck
npm run check
npm run build
npm run package
npm run sync
npm run doctor
```
```bash pnpm theme={null}
pnpm lint
pnpm lint:fix
pnpm typecheck
pnpm check
pnpm build
pnpm package
pnpm sync
pnpm doctor
```
```bash yarn theme={null}
yarn lint
yarn lint:fix
yarn typecheck
yarn check
yarn build
yarn package
yarn sync
yarn doctor
```
# 构建与部署
首次构建:
```bash npm theme={null}
npm run build
```
```bash pnpm theme={null}
pnpm build
```
```bash yarn theme={null}
yarn build
```
构建结果写入 `dist/`。`package` 与 `sync` 都要求 `dist/` 已经存在,并通过产物检查。
本地部署:
```bash npm theme={null}
npm run sync
```
```bash pnpm theme={null}
pnpm sync
```
```bash yarn theme={null}
yarn sync
```
`sync` 执行以下步骤:
1. 检查 `dist/` 是否符合当前插件形态。
2. 定位本机 MarginNote 的插件目录。
3. 使用临时目录完成文件替换,并在失败时恢复原插件。
4. 重启 MarginNote。
本地部署只支持 macOS。
# 生成安装包
```bash npm theme={null}
npm run package
```
```bash pnpm theme={null}
pnpm package
```
```bash yarn theme={null}
yarn package
```
安装包将生成至 `release/`:
* `<插件名称>-<版本>.mnaddon`:插件目录的压缩包。
* `<模板名称>-<版本>.mntemplate`:包含模板配置、本地化文件与插件文件的场景模板包。
模板配置的写法见[最小打包](/zh/basic-package)与[接入插件](/zh/template-plugins)。
# 网络请求限制
Source: https://dev.marginnote.com.cn/zh/network
# 连接范围
插件面板允许 `https:` 与 `wss:` 连接,也允许本机回环地址上的`http:`与 `ws: `连接。
本机连接来源仅限以下地址,端口范围不受限制:
* `http://localhost:<端口>`
* `http://127.0.0.1:<端口>`
* `ws://localhost:<端口>`
* `ws://127.0.0.1:<端口>`
非本机 `http:` 、`ws:`以及`0.0.0.0`、`::1`、局域网 IP 与自定义主机名均禁止访问。
# 跨域请求
面板的地址是 `mnpanel://panel`,在处理跨域请求配置 CORS 时可能会用到此地址。
```text theme={null}
Access-Control-Allow-Origin: mnpanel://panel
```
# 页面跳转
* 用户点击 `http:` 与 `https:` 链接时,地址交给系统浏览器打开,面板不会进行跳转。
* 页面自行修改 `location`、提交表单都不会生效。要向服务端发送数据时,需用 `fetch`。
# 内容安全策略
面板的每条响应都带有内容安全策略(CSP),页面可以加载的资源与可以发起的连接由该策略限定。
* 加载资源:允许面板同源资源、`mnpanel:`、`https:` 与 `localhost`、`127.0.0.1` 的 HTTP 资源。脚本、样式、图片、字体、音视频与 iframe 均受此规则约束。
* 发起连接:允许面板同源资源、`mnpanel:`、`https:`、`wss:`、本机 HTTP、本机 WebSocket、`data:` 与 `blob:`。
* 补充来源:图片与音视频还允许 `data:` 与 `blob:`;字体还允许 `data:`。
* 禁止内容:`object` 标签、`` 标签与表单提交。
策略逐条限定来源,完整取值如下:
```text theme={null}
default-src 'self' mnpanel: https: http://localhost:* http://127.0.0.1:*
connect-src 'self' mnpanel: https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* data: blob:
script-src 'self' mnpanel: https: http://localhost:* http://127.0.0.1:* 'unsafe-inline' 'unsafe-eval'
style-src 'self' mnpanel: https: http://localhost:* http://127.0.0.1:* 'unsafe-inline'
img-src 'self' mnpanel: https: http://localhost:* http://127.0.0.1:* data: blob:
media-src 'self' mnpanel: https: http://localhost:* http://127.0.0.1:* data: blob:
font-src 'self' mnpanel: https: http://localhost:* http://127.0.0.1:* data:
frame-src 'self' mnpanel: https: http://localhost:* http://127.0.0.1:*
object-src 'none'
base-uri 'none'
form-action 'none'
```
# 路径系统
Source: https://dev.marginnote.com.cn/zh/paths
面板以路径方式读取插件包内文件、接口调用、媒体读取、UI 控制等
正如 [数据交互](/zh/data-interact) 一节所述,面板与 MN 的数据交互是使用`mn.api `实现的,而该方法又等效在`/bridge/v1/` 路径上的 fetch 行为。同样地,与 MN 的其他的交互方式也可通过路径系统实现。
# 路径一览
| 路径 | 方法 | 用途 | 失败响应 |
| - | - | - | - |
| `/`、`/index.html` | GET | 面板的页面文件 | `500 PANEL_FILE_MISSING` |
| `/bridge/v1/…` | GET \| POST | MarginNote API 接口 | `403 IN_PROCESS_NOT_ALLOWED` |
| `/addon/<名称>` | POST | 请求 `main.js `上定义的方法 | `500 ADDON_CALL_FAILED`、`500 ADDON_BAD_RESULT`、`500 ADDON_THREW` |
| `/media/<键>` | GET | 返回 `putMedia` 存入的原始字节 | `404 NO_MEDIA` |
| `/ai/run-task` | POST | 调用模板声明的 AI 任务 | `403 ADDON_NOT_ACTIVE`、`501 AI_UNAVAILABLE`、`500 AI_BAD_RESULT` |
| `/pkg/<路径>` | GET | **返回插件包内的文件** | `404 NO_PKG_FILE` |
| `/ui/*` | GET | 控制面板的尺寸、形态与位置 | `{"ok":true}` |
| 其它 | — | — | `404 NOT_FOUND` |
# `window.mn 成员方法对应路径`
| 调用 | 路径 |
| - | - |
| `mn.api(path, opts)` | `/bridge/v1`+ `path` |
| `mn.call(name, data)` | `/addon/` |
| `mn.ai(noteid, taskId, params)` | `/ai/run-task` |
| `mn.height`、`mn.size`、`mn.mode`、`mn.close`、`mn.reload` | `/ui/` 下的对应路径 |
| `mn.log(...)` | `/ui/log?level=log` |
# 使用插件包内文件
使用`/pkg/` 引用插件包根目录的文件,例如资源数据、CSS、图片等。
例如下面插件目录:
**mn-addon**
**assets**
**style.css**
**scripts**
**model.js**
**index.html**
**mnaddon.json**
可以如下引用:
```html theme={null}
```
> [!WARNING]
>
> ".."、绝对路径、包外路径均无法使用
# 媒体资源
使用`putMedia` 将媒体数据传输给面板后(见[面板之外 › 媒体数据](/zh/jsaddon#%E5%AA%92%E4%BD%93%E6%95%B0%E6%8D%AE)),面板可以用 `/media/` 取得该媒体的原始字节。
# 请求 `main.js `上定义的方法
`mn.call(name, data)` 等效于:
* POST `/addon/`
* Request Body: `data`(JSON Format)
参见[面板之外](/zh/jsaddon)
# 执行 AI 任务
`mn.ai(noteid, taskId, params)` 等效于:
* POST `/ai/run-task`
* Request Body:`{noteid, taskId, params}`
参见 [使用 MN 内置 AI](/zh/use-inner-ai)
# UI 相关
| 路径 | 对应调用 |
| - | - |
| `/ui/height?h=` | `mn.height(h)` |
| `/ui/size?w=&h=` | `mn.size(w, h)` |
| `/ui/mode?m=` | `mn.mode('workspace' \| 'mini')` |
| `/ui/move?dx=&dy=&phase=` | — |
| `/ui/close` | `mn.close()` |
| `/ui/reload` | `mn.reload()` |
参见 [UI 控制](/zh/ui)
# 场景插件基础
Source: https://dev.marginnote.com.cn/zh/plugin-base
`场景插件` 是MN5推出的新的插件类型,安装方式与`经典插件` 相同。本文中无特殊声明,所述“插件”均为`场景插件`。
# 插件结构
场景插件相比经典插件要简单得多,下面是一个场景插件的最简目录结构:
**project**
**mnaddon.json**
**index.html**
```json mnaddon.json theme={null}
{
"addonid": "hello.world",
"title": "你好世界",
"version": "0.1.0",
"author": "我",
"marginnote_version_min": "4.5.0",
"immersiveOnly": true,
"panel": "index.html"
}
```
```html index.html theme={null}
```
可见,场景插件本质上是一个在`window`上注入了`mn`对象的 HTML 网页面板。
如果将这些项目下文件打包到 zip 文件根目录,并更改扩展名为`.mnaddon`便是一个符合标准的插件。
如果想使用插件,需点击设置右侧三个点→插件→导入 进行安装,并在场景模板中启用该插件。
> [accordion] 上述插件安装后运行效果
>
> 
# 清单字段
* addonid: 插件的唯一名称,id 相同即为同一插件,id 不同即为不同插件。
* title: 插件在 MN 内显示的名称
* version: 插件在 MN 内显示的版本号
* author: 插件在 MN 内显示的作者名
* marginnote_version_min: 插件要求的最低 MN 版本
* immersiveOnly: 仅在沉浸模式(MN5 的新界面)下可用,默认为 false。建议场景插件开启此选项。
* panel: 指定插件面板的 html 文件位置
* icon: 指定插件图标位置
* panelWidth: 面板宽度,默认为 380
* panelMaxHeight: 页面高度的上限,默认为 560
* events: 声明可注册事件列表,默认为\[]
* excerptClaim: 声明可接收的摘录。`{"hashtag": "#X", "configKey": "k"}`,其中`configKey`可选。详见[事件系统](/zh/events)
* cardCommands:卡片菜单里的命令,详见[卡片与摘录菜单命令](/zh/menu-commands)
以上字段为简单介绍,具体配置请参见对应文档。
# 关于经典插件
经典插件是曾经版本的 MN 所支持的插件类型,特点是插件入口为 main.js 而非场景插件的 panel HTML。
关于经典插件的开发方式可以查阅旧的 [Marginnote 插件开发文档](https://mn-docs.museday.top/)。
经典插件与场景插件并非只能二选一,实际上,二者可以结合进行开发。在新的插件系统中,场景插件的面板和经典插件的脚本之间新增了基于事件的通信机制以及若干控制手段。若要了解场景插件和经典插件的融合开发方式、如何同时使用面板和 main.js,请参阅[面板之外](/zh/jsaddon)
# 使用 window.mn 与MN进行交互
Source: https://dev.marginnote.com.cn/zh/sdk
场景插件在加载时,MN 会向上下文注入 `mn` 实例对象,可以通过访问其上的成员来进行与 MN 的交互。
```typescript theme={null}
window.mn
├── version: number // sdk版本
├── currentMode: "workspace" | "mini"
├── lang: string // App 界面语言
├── locale: string // 系统地区
├── theme: "light" | "dark" // 白色、棕褐色、绿色 = light;灰色、黑色 = dark
├── _drag
├── api(path, opts) => Promise
├── ai(noteid, taskId, params) => Promise
├── call(name, data) => Promise // 向 main.js 发送消息并获取返回值
├── on(event, cb)
├── fit() / height(h) / size(w,h) / mode(m) / close() / reload()
└── log(...args)
```
# 只读信息
以下成员均为 `mn` 对象下的只读成员:
* version:插件 SDK 版本号
* currentMode:`workspace`/`mini`。需要插件自行进行适配
* lang: App 界面语言。
* locale: 系统所在区域。
* theme: `light`/`dark`。白色、棕褐色、绿色为 `light`;灰色、黑色为 `dark`
* \_drag:作用暂不明确
```javascript theme={null}
> [mn.version,mn.currentMode,mn.lang,mn.locale,mn.theme,mn._drag]
< [
1,
"workspace",
"zh-Hans",
"zh-CN",
"dark",
false
]
```
# 方法
## api(path, opts)
对 MN 数据进行 CRUD 操作。此方法调用方式与 Marginnote CLI 相同,但有部分 API 不可用。准确可用 API 需查询 capabilities。
下面代码演示了如何使用 `mn.api` 进行操作
```javascript GET theme={null}
// 😋获取所有 api 可用能力
const capabilities = mn.api("/capabilities")
// 按文档顺序获取该学习集的卡片
const tree = await mn.api("/study-sets/" + topicid + "/tree?sort=document");
// 获取当前打开的学习集 ID
const st = await mn.api("/ui-state");
```
```javascript POST theme={null}
await mn.api('/notes/' + id + '/edits', {method: 'POST', body: {ops: [
{op: 'setTitle', text: '…', expectedPrefix: '当前标题的开头'},
{op: 'appendComment', text: '…'},
{op: 'deleteComment', index: 2, expectedPrefix: '…'},
{op: 'removeHashtags', hashtags: ['#VocabPending']},
{op: 'setAddonValues', addon: '任意', values: {status: 'done'}}
], confirm: true, syncReview: false}});
```
具体实践详见[数据交互](/zh/data-interact)。
## ai(noteid, taskId, params)
`mn.ai`允许你使用 MN 内置的 AI 执行任务。需要注意的是,使用此命令前需在 `template.json` 中定义任务 id。
```json template.json theme={null}
{
"aiTasks": [
{
"id": "starter_explain",
"title": "解释一下",
"trigger": "user",
"scope": "word_sentence",
"tier": 1,
"output": {"reply": "string"},
"required": ["reply"],
"prompt": "用一句中文解释给定的词在学习笔记语境下的意思。"
}
],
...
}
```
下面是调用示范
```javascript theme={null}
const r = await mn.ai(
noteid,
"starter_explain",
{
word: "photosynthesis",
sentence: "..."
}
);
if(r.ok) console.log(r.json.reply);
```
具体实践详见 [使用 MN 内置 AI](/zh/use-inner-ai)
## call(name, data)
用于调用`main.js`中定义的函数并接收回调。
具体实践详见[面板之外](/zh/jsaddon)
## on(event, cb)
`mn.on` 用于监听 MN 发送的事件。你需要先在 `mnaddon.json` 里声明你要监听哪些事件。
```json mnaddon.json highlight={9} focus={9} theme={null}
{
"addonid": "mn.hello",
"title": "你好世界",
"version": "0.1.0",
"author": "我",
"marginnote_version_min": "4.5.0",
"panel": "index.html",
"icon": "hello.png",
"events":["excerpt"]
}
```
```javascript theme={null}
mn.on("excerpt", async (card) => {
console.log(`New excerpt added: ${card.noteid}`)
});
```
具体实践详见 [事件系统](/zh/events)
## UI 相关
* fit(): 自适应面板高度
* height(h): 设置面板高度
* size(w,h): 设置面板宽高
* mode(m): 设置面板模式为`workspace`/`mini`
* close(): 关闭面板
* reload(): 重载面板
具体实践详见[UI 控制](/zh/ui)
## log
记录日志。
可以在插件面板连续点击顶部“插件”**五次**打开日志面板。

# AI 任务
Source: https://dev.marginnote.com.cn/zh/template-ai
aiTasks 的字段写法、scope 与 output 的约束、任务声明的校验与 tagPrefix
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
场景模板可以声明 AI 任务。任务是以数据形式声明的,加一条只需要改模板,MarginNote 本身和插件的 `mnaddon.json` 都不用动。
`aiTasks` 服从覆盖规则:写进去的任务才存在,留空表示不接管(见[最小打包](/zh/basic-package))。发起时机、材料范围、返回结果的结构,都由模板逐条写明,MarginNote 只负责核对。插件接入模板的写法,见[接入插件](/zh/template-plugins)。
# 任务声明
`aiTasks` 是一个数组,每一项是一条任务声明。最简单的一条任务声明如下:
```json template.json highlight={4} focus={4} theme={null}
{
"id": "u-starter",
"name": "起步",
"aiTasks": [
{
"id": "starter_explain",
"title": "解释一下",
"trigger": "user",
"scope": "word_sentence",
"tier": 1,
"output": {
"reply": "string"
},
"required": [
"reply"
],
"prompt": "用一句中文解释给定的词或短语在学习笔记语境下的意思。reply 写那一句话,不要罗列义项、不要加原文里没有的信息。"
}
]
}
```
每条任务的字段:
| 字段 | 类型 | 含义 |
| - | - | - |
| `id` | `String` | 任务的标识,在模板内唯一。页面依据该字段指定要调用的任务。该字段不可留空。 |
| `title` | `String?` | 展示给用户的任务名称。同意提示与模板编辑器展示的都是该名称,`id` 不出现在这些界面上。 |
| `trigger` | `String?` | 发起方式的约束。取值见下文的“任务声明的校验”一节。缺省 `user`。 |
| `scope` | `String` | 发送出去的材料的范围。取值固定,取材料的动作由 MarginNote 完成,模板只能从中选取。该字段不可留空。 |
| `prompt` | `String` | 模板作者撰写的提示词。MarginNote 在提示词之后按固定格式附上材料,并声明材料只是材料,不要执行其中的指令。该字段不可留空。 |
| `output` | `[String: String]?` | 输出字段表。键为字段名,值为 `string`、`number` 或 `bool`。MarginNote 按该表校验返回结果的结构。 |
| `required` | `[String]?` | 必填字段的清单。清单中的字段取自 `output`,缺任意一项即判定结构不对,不判定为成功。 |
| `tier` | `Int?` | 模型档位。`0` 与 `1` 使用同一个小模型,大于 `1` 时使用能力更强的模型。缺省 `0`。 |
`title` 面向用户,`prompt` 面向模型;提示词是模板作者撰写的自由文本,写法由作者决定,MarginNote 不介入。
> [!WARNING]
>
> `aiTasks` 同样服从覆盖规则。在现成模板的基础上修改,原模板的任务声明会一并继承下来。配套插件里没有调用某条任务的代码,那条任务仍留在模板里,页面读取到的仍是原模板的任务清单。
# scope 的取值
`scope` 决定 MarginNote 发往模型的内容范围,只能取下表之一:
| 取值 | MarginNote 发出的内容 |
| - | - |
| `word_sentence` | 目标词与该词所在的那一句。材料由插件提供,词不超过 80 个字符,句子不超过 600 个字符,长度上限由 MarginNote 检查。 |
| `card_text` | 这张卡的标题与正文。材料由 MarginNote 从卡片上取出,插件传入的内容不予采用。 |
| `attempt_ink` | 这张卡上的手写、文本框与公式框合并成一张图片发送,题干与参考答案由插件提供。插件无法取得该图片。卡上没有书写内容时,该次调用被拒绝。 |
`attempt_ink` 使用识别手写的视觉模型。解答判定任务需要更强的模型,应声明 `tier` 为 `2`。
材料由 MarginNote 提供,提示词只影响模型的推理过程,无法涉及材料范围之外的内容。因此 `card_text` 任务的提示词写得再详细,也无法取得卡片上的手写内容。
写错 `scope` 的表现和写错其他字段一样,都是整条声明不存在。排查时对照上表即可。
# output 的结构校验
`output` 的取值只能是 `"string"`、`"number"` 与 `"bool"`,没有其他取值。
把字段的含义写进 `output`,整条声明会被当作不存在:不报错,任务不会出现在页面的任务列表里。
例如下面这条声明不合格:
```json template.json theme={null}
{
"id": "u-starter",
"name": "起步",
"aiTasks": [
{
"id": "starter_explain",
"trigger": "user",
"scope": "word_sentence",
"output": {
"reply": "一句话的中文解释"
},
"prompt": "用一句中文解释给定的词或短语在学习笔记语境下的意思。"
}
]
}
```
字段的含义要写进 `prompt`,`output` 中只保留类型名:
```json template.json theme={null}
{
"id": "u-starter",
"name": "起步",
"aiTasks": [
{
"id": "starter_explain",
"trigger": "user",
"scope": "word_sentence",
"output": {
"reply": "string"
},
"required": [
"reply"
],
"prompt": "用一句中文解释给定的词或短语在学习笔记语境下的意思。reply 写那一句话。"
}
]
}
```
上例的提示词里提到了 `reply`,`output` 中也声明了 `reply`,两边一致。字段名对不上时,模型写出的内容出现在 `output` 未声明的字段上,MarginNote 按 `output` 校验时就会判定结构不对。
`required` 是 `output` 的子集:里面的字段名必须在 `output` 中声明过,否则整条声明失效。运行时 `required` 里有任何一项缺失,返回结果就判定结构不对,不判定为成功。
> [!WARNING]
>
> `output` 的取值写错、写成中文描述、或 `required` 指向 `output` 中不存在的字段,都会让整条声明被当作不存在。MarginNote 不报错,页面只收到一个参数错误。逐条比对字段清单与允许的取值即可核对。
# 任务声明的校验
MarginNote 对每条任务校验以下方面:
| 字段 | 检查的方面 | 说明 |
| - | - | - |
| `scope` | 发出的内容 | 取值固定,取材料的动作由 MarginNote 完成,提示词无法涉及范围之外的内容。 |
| `tier` | 消耗的额度 | `0` 与 `1` 使用同一个小模型,大于 `1` 时使用能力更强的模型,缺省 `0`。 |
| `output` | 返回结果的结构 | 字段表给出结构,`required` 给出必填项,结构不对不判定为成功。 |
| `trigger` | 用户是否知情 | `user` 表示必须由用户手势触发,缺省即此值。`auto` 允许插件自动发起,需要先取得一次同意。 |
这些校验都与提示词无关。提示词决定回答的质量,MarginNote 校验的是范围、额度、结构,以及用户是否知情。
`tier` 的档位与 AI OCR 共用同一套取值。`tier` 为 `0` 或 `1` 时使用同一个小模型,适合输出简短的任务;`tier` 大于 `1` 时改用能力更强的模型,适合判定与长推理。缺省是 `0`,声明之前应先确认默认档位是否满足需要。
除了额度,MarginNote 还会校验调用许可与调用频率限制,这项校验同样不经过提示词,`auto` 的任务无法绕开。
一条声明合格,需要同时满足:`id` 与 `prompt` 不为空,`scope` 是固定取值之一,`trigger` 只能是 `user` 或 `auto`,`output` 中每个值都落在类型清单内,`required` 中的每个字段都出现在 `output` 里。任何一项不通过,这条声明就按不存在处理,模板中其他任务不受影响。
```json template.json theme={null}
{
"id": "u-starter",
"name": "起步",
"aiTasks": [
{
"id": "summarize",
"trigger": "auto",
"scope": "card_text",
"output": {
"summary": "string"
},
"required": [
"summary"
],
"prompt": "用三句话概括这张卡片的内容,写进 summary。"
},
{
"id": "grade",
"trigger": "auto",
"scope": "attempt_ink",
"tier": 2,
"output": {
"verdict": "string",
"score": "number"
},
"required": [
"verdict"
],
"prompt": "批改作答区里的解答。verdict 写判定结论,score 写得分。"
}
]
}
```
上例中 `grade` 的 `required` 只列出 `verdict`,`score` 属于可选字段,`output` 中仍然保留该字段的声明。
> [!TIP]
>
> 写声明时先定 `scope`,再定 `output`,最后写 `prompt`。`scope` 与 `output` 决定 MarginNote 校验的范围与结构,不通过则整条声明不存在;`prompt` 的内容不参与校验,但不能为空。提示词可以在模板安装之后再逐步调整。
# 卡片类型标签的前缀
`tagPrefix` 是卡片类型标签的前缀,与 AI 任务无关,属于模板的标识性设置。
```json template.json highlight={4} focus={4} theme={null}
{
"id": "u-starter",
"name": "起步",
"tagPrefix": "practice"
}
```
写出该字段之后,这份模板参与卡片类型标签的约定:摘题产出时给题卡打 `<前缀>/question`,答案卡打 `<前缀>/answer`;前缀之后的叶名由 MarginNote 固定,除 `question` 与 `answer` 外还有 `todo` 与 `error/`,只有前缀可以改。插件按同一套约定识别卡片,模板与插件使用同一个前缀。
这样用户才能在标签面板里看到这些标签,并自行增加或删除。摘题配不上的答案留作未配对的答案卡,在标签面板里成为待配对清单,由用户自行处理。
留空表示不参与该约定。摘题是通用功能,模板不声明前缀时,摘题照常可用,MarginNote 不会打任何标签。
> [!NOTE]
>
> 从外部模板复制时,前缀会被一并继承,而配套插件如果不写这类标签,该前缀就始终匹配不上。参见[最小打包](/zh/basic-package)一文的覆盖规则。
# 页面调用
页面通过 `mn.ai` 调用模板中已经声明过的任务。调用参数随 `scope` 而定,返回结果的形式与错误码的含义见[使用 MN 内置 AI](/zh/use-inner-ai)。
本文只讲声明部分。页面读取任务清单、处理返回结果的方式,属于插件一侧。模板其余字段与包内文件的用途,见[最小打包](/zh/basic-package)。
# 工具与模式
Source: https://dev.marginnote.com.cn/zh/template-bottom
场景模板的底部工具条:模态的字段、行为推导、工具行与收纳弹层、笔盒与摘录颜色、chipStyle 的显示方式,以及旧字段的兼容
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
场景界面底部有一条工具条,由若干**模态**组成。一个模态由一个标签页与一行工具组成:标签页负责切换,工具行收录这一页可以使用的工具。
模板接管这条工具条的方式是写出 `bottom.modes`。数组的每一项是一个模态,数组顺序即标签页的顺序;留空表示不接管(见[最小打包](/zh/basic-package)的覆盖规则),MarginNote 保持现状,这一点与模板的其余位置一致。
顶部工作栏与右侧浮窗是另一套配置,写法见[顶部工作栏与浮窗](/zh/template-surfaces)。本文只讲底部工具条。
# 模态的结构
`bottom` 只有一个字段 `modes`,类型是模态数组。模态的字段:
| 字段 | 类型 | 说明 |
| - | - | - |
| `id` | `String` | 模态的标识。MarginNote 按标识记住这一模态上次使用的笔。 |
| `title` | `String?` | 标签页上的显示名。留空按 `behavior` 取本地化的 Read / Write / Recall。 |
| `icon` | `String?` | SF Symbol 名。留空按 `behavior` 取 `book` / `pencil.tip` / `brain.head.profile`。 |
| `behavior` | `String?` | 进入该模态时的行为,取 `read` / `write` / `recall`。旧写法,新写的模板改用 `entryTool` 与 `recall`。 |
| `tools` | `[String]?` | 工具行的条目清单。留空表示不接管。 |
| `overflow` | `[String]?` | 只进入工具行末尾收纳弹层的条目。 |
| `entryTool` | `String?` | 进入该模态时激活的工具。 |
| `recall` | `Bool?` | 进入该模态时是否开启回忆会话。 |
| `inlineCount` | `Int?` | 摘录笔盒与手写笔盒共用的常显支数。旧字段。 |
| `inlineExcerpt` | `Int?` | 摘录笔盒常显的支数。 |
| `inlineInk` | `Int?` | 手写笔盒常显的支数。 |
`id` 与 `title` 决定标签页的显示方式。模态已命名但没有选择图标时,标签页上显示的是模态的名称;选择了图标就显示图标,名称只留在模板编辑器里。`icon` 的取值是 SF Symbol 名,在模板编辑器里可以按英文名搜索。
`inline*` 系列字段控制笔盒上常显的支数,超出的笔收进笔盒末尾的 `›`。留空时按窗口宽度自动决定,与现状相同。`inlineCount` 是摘录笔盒与手写笔盒共用一个数值的旧写法,作为 `inlineExcerpt` 与 `inlineInk` 的兜底:某一项留空时,该项取 `inlineCount` 的值。
`recall` 与 `behavior` 的 `recall` 表达同一件事:进入模态时是否开启回忆会话。`recall` 显式写出时以 `recall` 为准。
# 读态、写态与忆态的推导
`behavior` 的取值与进入模态时的动作对应如下:
* `read`:笔与摘录拾取器都关闭。
* `write`:启用笔与摘录拾取器。
* `recall`:进入回忆态。
MarginNote 按工具行的内容推导三态:行上有回忆控件的模态判定为 `recall`;否则,行上直接出现笔盒的模态判定为 `write`;其余模态判定为 `read`。`behavior` 只用于推导 `recall` 与 `entryTool` 的缺省值,写出的 `read` 或 `write` 不改变读态与写态的结果。
该推导持续生效,配置改变后立即重新计算,不只在保存时计算一次。把书写模态的笔盒从工具行上移走,这一行没有笔盒,就变成阅读模态;把笔盒加回去,又恢复成书写模态。没有命名的模态,标签页上的显示名也跟着推导结果变化。
> [!NOTE]
>
> `tools` 留空时,工具行沿用 MarginNote 在该行为下的当前内容,推导结果不受模板影响。模板编辑器给这种模态标着 `默认内容`。
## 进入时激活的工具
`entryTool` 决定进入模态时激活的工具:
* `hand`:激活手型工具。
* `last`:恢复这一模态上次使用的笔。
有以下限制:
* 工具行上没有笔时,模板编辑器只允许选择 `hand`。
* 笔折叠在笔组里时,进入时也不会激活该笔。折叠的笔组代号是 `tool.penGroup` 与 `recall.handwrite`,两者指向同一个按钮,展开后才能看到笔。
> [!NOTE]
>
> `entryTool` 决定进入时激活的工具,`recall` 决定进入时是否开启回忆会话。新写的模板请写 `entryTool` 与 `recall`;`behavior` 保留为兼容输入,只影响前两者的缺省值。
# 工具行与收纳弹层
`tools` 是一个字符串数组,数组顺序即工具行上从左到右的排列顺序。留空表示不接管,工具行显示该行为当前的默认内容。写出取值时完全以写出的内容为准,工具行上不会出现数组以外的条目。
`overflow` 收的是该模态下放不下的条目。这些条目只进工具行末尾的收纳弹层,不占工具行的位置。
常用代号见下表,全部合法代号见[代号词汇表](/zh/template-vocabulary)。
| 类别 | 常用代号 | 界面上的位置 |
| - | - | - |
| 读态开关 | `theme`、`titleLinks`、`blockSelect` | 主题、标题链接、块选择。 |
| 笔盒 | `tool.excerptBox`、`tool.inkBox` | 摘录笔盒、手写笔盒,各自占工具行一个位置。 |
| 墨迹代号 | `tool.ink.pen`、`tool.ink.marker`、`tool.ink.pencil`、`tool.ink.crayon` | 手写笔盒里可用的具体笔。 |
| 摘录代号 | `tool.excerpt.text`、`tool.excerpt.rect`、`tool.excerpt.lasso`、`tool.excerpt.highlight`、`tool.excerpt.blank` | 摘录笔盒里可用的具体摘录方式。 |
| 笔盒自带按钮 | `tool.excerptHand`、`tool.excerptConti`、`tool.hand`、`tool.lasso`、`tool.eraser`、`tool.textbox`、`tool.imagebox` | 笔盒组自带的按钮。 |
| 折叠的笔组 | `tool.penGroup`、`recall.handwrite` | 一个按钮,展开后是笔、套索、橡皮、撤销与完成的子行。 |
| 撤销与重做 | `undoRedo` | 写态固定按钮。 |
| 回忆控件 | `recall.settings`、`recall.reset`、`recall.hint`、`recall.level` | 回忆面板上的开关。 |
| 回忆态开关 | `pref.recall_single_peek`、`pref.recall_structured_mode`、`pref.recall_hint_use_afm`、`pref.recall_cover_all_excerpts`、`pref.recall_force_title_only` | 回忆模态下可加入工具行的开关。 |
墨迹与摘录代号是笔盒里的具体条目;笔盒本身的代号是 `tool.excerptBox` 与 `tool.inkBox`,在工具行上各占一个位置。笔盒自带的按钮需要写进 `tools`,写入后固定显示在笔盒组末尾,位置不由数组顺序决定:手写笔盒带套索、橡皮、手型、文本框、图片框,摘录笔盒带手型与连续摘录。
同一条目写多次只计一次。代号写错时 MarginNote 不会报错,而是静默忽略该条目,表现为某个按钮没有出现,排查办法见[校验与排错](/zh/template-validate)。
> [!TIP]
>
> 不写 `tools` 表示不接管,写成空数组则是把这一行清空,两者不要混淆。
# 笔盒与摘录颜色
模板可以在顶层自带以下内容:
| 字段 | 类型 | 说明 |
| - | - | - |
| `excerptTools` | `JSON?` | 模板自带的摘录工具集。 |
| `excerptColors` | `JSON?` | 模板自带的摘录颜色表,十六色。 |
| `canvasTools` | `JSON?` | 模板自带的手写笔盒。 |
以上字段都是 MarginNote 内部保存的原始数据,模板只做原样复制,不做解释,也不做转换。
留空表示继承,沿用学习集或全局的配置。写出取值后,进入场景界面期间使用这份配置,在场景界面里增加或删除笔也会写回这份配置;退出场景界面或更换模板后,恢复为继承而来的配置。
`excerptColors` 有个例外:学习集配过颜色表时,一律采用学习集的,模板的颜色表只在学习集没配过时生效。
> [!NOTE]
>
> 笔的颜色与粗细不在模板配置里直接设置,而在阅读时的笔盒面板与摘录设置面板里调整,调整结果直接写入这份模板配置。
# chipStyle 的显示方式
`chipStyle` 是顶层的字符串字段,决定笔盒上每一支笔的显示方式:
* `icon`:只显示图标。留空即这一档。
* `labeled`:以文字为主,右下角再放一个小图标,图标带着该工具的颜色。
该字段只影响未命名的笔;已命名的笔照旧显示名称。想让某支笔只显示图标,清空该笔的名称即可。
# 与旧字段的兼容
`toolbar.tabs` 是配置格式早期版本里的模态清单,schema 3 起由 `bottom.modes` 取代。`bottom` 留空时,MarginNote 先检查 `toolbar.tabs` 是否写入,写入时按该清单推导模态,未写入则使用当前的读态、写态与忆态。
旧写法如下:
```json template.json theme={null}
{
"id": "u-legacy",
"name": "旧写法",
"toolbar": {
"tabs": ["read", "write", "recall"]
}
}
```
> [!WARNING]
>
> 新写的模板一律使用 `bottom.modes`。保留 `toolbar.tabs` 只为读懂已有的模板,不适合继续扩充。
# 完整示例
下面是接管底部工具条的最小模板:
```json template.json theme={null}
{
"id": "u-starter",
"name": "起步",
"minAppVersion": "4.5.0",
"bottom": {
"modes": [
{
"id": "collect",
"title": "摘录",
"behavior": "write",
"entryTool": "last",
"inlineExcerpt": 2,
"tools": [
"tool.excerptBox",
"tool.excerptHand",
"undoRedo",
"theme"
]
},
{
"id": "read",
"title": "阅读",
"behavior": "read",
"tools": [
"theme",
"titleLinks"
]
}
]
}
}
```
* `minAppVersion`:模板用到 MarginNote 在某一版本之后才提供的能力时声明该字段,导入时 MarginNote 版本不足会被拒绝。`bottom` 从 schema 3 起可用。
* `bottom.modes[0]`:摘录模态。`id` 是模板内部的标识;`title` 决定标签页上显示的名称;`behavior` 写明这是书写模态;`entryTool: "last"` 表示进入时恢复上次使用的笔;`inlineExcerpt` 固定摘录笔盒的常显支数。
* `tools` 的顺序即工具行上从左到右的排列顺序:摘录笔盒、摘录笔盒自带的手型按钮、撤销与重做、主题开关。
* `bottom.modes[1]`:阅读模态。`tools` 里没有任何笔盒或墨迹条目,因此这一行是纯读态开关。
* 阅读模态的 `tools` 没有写 `overflow`。留空即不接管收纳弹层。
安装这份模板后进入场景界面,底部标签页从 MarginNote 默认的读态、写态、忆态换成摘录与阅读。验证方式如下:
1. 进入摘录页,摘录笔盒常显两支笔,其余笔收在笔盒末尾的 `›` 中。
2. 把摘录笔盒(`tool.excerptBox`)从 `tools` 里删掉,这一页变成阅读页;把摘录笔盒加回 `tools`,书写模态恢复。
配置的修改立即生效,模板不需要保存。
# 卡片与摘录菜单
Source: https://dev.marginnote.com.cn/zh/template-menu
selectionMenu 的清单与作用位置、菜单项代号、插件命令项,以及划线快捷项 excerptShortcuts 的写法
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
场景界面里的弹出菜单由模板顶层的 `selectionMenu` 决定:插件负责命令本身,模板负责菜单上出现的条目。顶部工作栏与浮窗的配置见[顶部工作栏与浮窗](/zh/template-surfaces),本文讲菜单。
`selectionMenu` 是一个对象,每个字段是一份清单。清单是字符串数组,每个元素是一个动作代号。清单列出该菜单允许出现的条目。
字段整体留空表示不接管(见[最小打包](/zh/basic-package)的覆盖规则),菜单沿用当前的收藏。
模板编辑器把这些清单集中在 `卡片与摘录菜单` 这一页。该页顶部的搜索框同时过滤菜单清单、卡片多选工具栏与划线快捷项,只保留名称命中的条目;没有命中时给出提示,清空搜索框后恢复全部条目。
搜索框下方折叠段 `菜单设置说明` 的内容,与下文“最大集语义”一节一致。
# 清单与作用位置
`selectionMenu` 的字段:
| 字段 | 类型 | 作用位置 |
| - | - | - |
| `cards` | `[String]?` | 卡片菜单。在脑图与大纲中共用一份清单。 |
| `documents` | `[String]?` | 文档上的卡片菜单。在 PDF、EPub、视频等界面上共用一份清单。 |
| `text` | `[String]?` | 选中文本时弹出的菜单。在 PDF 与 EPub 上共用一份清单。 |
| `PDFReader` | `[String]?` | 按 MarginNote 界面细分的写法,仅 PDF 阅读器生效。 |
| `EPubReader` | `[String]?` | 按 MarginNote 界面细分的写法,仅 EPub 阅读器生效。 |
| `MindMap` | `[String]?` | 按 MarginNote 界面细分的写法,仅脑图生效。 |
| `Digest` | `[String]?` | 按 MarginNote 界面细分的写法,仅 Digest 界面生效。 |
模板编辑器里只列出卡片菜单、文档上的卡片菜单与选中文本菜单,其余字段按 MarginNote 的界面细分,用来覆盖特定界面。
> [!NOTE]
>
> `text` 的取值范围与其他清单不同。该清单给出的是菜单上允许出现的最大范围:清单之外的动作收进 `More` 子菜单后仍可使用;其余清单之外的条目不出现。
`text` 留空表示沿用当前的收藏,超出快捷按钮条范围的条目收在 `More` 子菜单里。
# 最大集语义
清单给出的是可出现的条目范围,不是逐条打开的开关。菜单弹出时,MarginNote 还会按当时实际可用的操作再过滤一次。比如清单里同时写了删除卡片与删除摘录,菜单上也只会出现当时实际可用的那一项。
* 未接管时,菜单显示当前的收藏;一经勾选即视为接管,此后该菜单的组成由模板决定。
* 场景界面下这些菜单只以场景模板为准:在卡片菜单上添加条目、在选中文本菜单的 `More` 里打开定制入口、长按菜单项收藏或取消收藏,上述操作只修改模板中的对应清单,不再写入学习集或全局设置。传统界面不受影响。
> [!NOTE]
>
> 摘录完成后,场景界面上会浮出一条结果浮条,浮条上的 `摘录操作` 按钮打开摘录菜单。该浮条由 `surfaces.excerptResultBar` 控制,参见[顶部工作栏与浮窗](/zh/template-surfaces)。
## 候选项
候选项是模板编辑器中可供勾选的条目,由默认收藏与该菜单实际出现过的条目组成。
刚接管时清单可能为空。模板编辑器会提示需要进入场景界面打开一次对应的菜单,条目才会登记。菜单在此后出现过的新条目同样会补进候选项。
## 编辑顺序与恢复默认
每份清单每行显示一个功能,已勾选的条目排在前面。清单标题右侧提供 `编辑顺序` 与 `恢复默认` 按钮。`编辑顺序` 打开一个可以拖动排序、增加或删除条目的界面,只在该清单已有候选项时出现;`恢复默认` 在接管之后才出现。
> [!TIP]
>
> 系统场景模板下这几份菜单清单也能在模板编辑器中直接修改,改动只存在当前设备上,不随模板分发。划线快捷项不在此列,说明见下节。
# 菜单项代号
清单里的每个元素是一个动作代号。内置动作的代号取自固定清单,模板编辑器按位置列出可用条目,下面只举几项。
**冒号属于代号本身:`noteHighlight:` 带冒号,`commentNote` 不带冒号。** 书写时按目标清单中的代号原样抄写。
```json template.json theme={null}
{
"id": "u-hello",
"name": "你好世界",
"selectionMenu": {
"cards": [
"noteHighlight:",
"commentNote"
],
"documents": [
"sendHighlight:",
"noteHighlight:"
]
}
}
```
数组中的顺序即菜单中的排列顺序。
同一个功能在不同菜单里的代号可能不同,写入时以目标菜单的清单为准。
> [!WARNING]
>
> 代号拼错时 MarginNote 不报错,该条目被静默忽略。排查办法见[校验与排错](/zh/template-validate)。
# 插件命令项
插件贡献的命令使用另一种格式的代号:`addon.<插件id>.<命令id>:`。末尾的冒号同样不能省略。
这类条目需要插件与模板共同写出:插件在 `mnaddon.json` 的 `cardCommands` 中声明这条命令,模板在对应的清单中写出这个代号;声明与写出都完成之后,该条目才会出现在菜单上。命令的声明写法与接收方式见[卡片与摘录菜单命令](/zh/menu-commands)。
插件本身需先列入模板顶层的 `plugins` 白名单,未列入白名单的插件不会出现。白名单的写法见[接入插件](/zh/template-plugins)。
```json mnaddon.json theme={null}
{
"addonid": "mn.starter",
"title": "起步",
"version": "0.3.0",
"marginnote_version_min": "4.5.0",
"panel": "index.html",
"cardCommands": [
{
"id": "annotate",
"title": { "zh-Hans": "添加批注", "en": "add a comment" }
}
]
}
```
```json template.json theme={null}
{
"id": "u-hello",
"name": "你好世界",
"plugins": ["mn.starter"],
"selectionMenu": {
"cards": [
"noteHighlight:",
"commentNote",
"addon.mn.starter.annotate:"
],
"documents": [
"sendHighlight:",
"noteHighlight:",
"addon.mn.starter.annotate:"
]
}
}
```
菜单上找不到这一项时,对照下表排查:
| 现象 | 排查方向 |
| - | - |
| 菜单上没有这一项 | 插件是否在 `mnaddon.json` 的 `cardCommands` 中声明了该命令。 |
| 声明之后仍不出现 | 插件是否列进了模板顶层的 `plugins` 白名单。 |
| 卡片菜单上出现,文档菜单上没有 | 模板是否在 `documents` 中也写出了同一个代号。 |
| 在任何清单里都不生效 | 代号末尾的冒号是否漏写。 |
| 同一条命令在卡片菜单与文档菜单里表现不同 | 命令在 `mnaddon.json` 中声明的 `when` 条件是否在该菜单所对应的卡片上成立,写法见[卡片与摘录菜单命令](/zh/menu-commands)。 |
# 划线快捷项
选中文本时弹出的菜单上有一排快捷按钮,点击其中一个按钮,选中的文字即按预定的设置摘录成卡片。这排按钮由模板顶层的 `excerptShortcuts` 定义,该字段不在 `selectionMenu` 之内,最多容纳 8 个按钮。
字段的值是对象数组,每个按钮是一个对象。常用字段:
| 字段 | 类型 | 说明 |
| - | - | - |
| `title` | `String` | 按钮上显示的文字。 |
| `hashtag` | `String` | 该项摘录出的卡片所带的标签。 |
| `color_index` | `Int` | 该项使用的颜色序号。 |
| `ai_addon` | `String` | 接收该项 AI 任务的插件 id,对应模板编辑器里的 `AI 交给:……`。取值写法见[使用 MN 内置 AI](/zh/use-inner-ai)。 |
对象与学习集里的划线快捷项同形,除上表字段外还有其他可选键,例如 `toggle_title`、`fill_index`、`addtomap_mode`、`insert_afterfocus`、`occlusion`、`aiocrprompt`。
`text` 清单决定每个快捷按钮在快捷按钮条上占用的槽位:清单中列出某个槽位条目,该槽位才会真正显示在快捷按钮条上。槽位条目与其余菜单清单一样,在模板编辑器中按位置列出。
```json template.json theme={null}
{
"id": "u-hello",
"name": "你好世界",
"excerptShortcuts": [
{ "title": "抓取一句", "hashtag": "", "color_index": 0, "ai_addon": "" },
{ "title": "标记标题", "hashtag": "#重点", "color_index": 1, "ai_addon": "" }
],
"selectionMenu": {
"text": [
"noteHighlight:"
]
}
}
```
`excerptShortcuts` 留空表示跟随当前生效的快捷项设置:学习集中保存过摘录设置时跟随学习集,否则跟随全局。写出取值之后,场景界面期间使用模板中登记的快捷项,在场景界面里新建、编辑、删除快捷项都写回该字段,每个模板各自独立。
模板编辑器中该区域的标题写作 `划线快捷项(选中文本菜单上的快捷按钮,最多 8 个)`,`当前生效` 一行标明当前生效的设置来源(学习集或全局)。
接管之后出现 `用当前这套覆盖场景模板里的` 按钮,同时提供改回跟随学习集或全局的入口,用于放弃接管。每一项右侧有 `编辑` 与 `删除` 按钮,未满上限时底部出现新建快捷项的入口。
装有会接收 AI 任务的插件时,每一项下方还有 `AI 交给:……` 菜单。还没有快捷项时模板编辑器给出提示;从场景界面之外打开模板编辑器读不到当前生效的快捷项,同样给出提示。
系统场景模板下该区域只能查看,复制为 `我的场景模板` 之后才能修改。
# 与多选工具栏的关系
卡片被多选时,底部出现的工具栏由 `surfaces.multiSelect` 控制,与本篇的清单分属不同字段,参见[顶部工作栏与浮窗](/zh/template-surfaces)。
# 完整示例
这份模板接管了卡片菜单、文档上的卡片菜单与选中文本菜单,并加入一条插件命令:
```json template.json theme={null}
{
"id": "u-starter",
"name": "起步",
"minAppVersion": "4.5.0",
"plugins": ["mn.starter"],
"selectionMenu": {
"cards": [
"noteHighlight:",
"commentNote",
"addon.mn.starter.annotate:"
],
"documents": [
"sendHighlight:",
"noteHighlight:",
"addon.mn.starter.annotate:"
],
"text": [
"noteHighlight:"
]
},
"excerptShortcuts": [
{ "title": "抓取一句", "hashtag": "", "color_index": 0, "ai_addon": "" },
{ "title": "标记标题", "hashtag": "#重点", "color_index": 1, "ai_addon": "" }
]
}
```
# 接入插件
Source: https://dev.marginnote.com.cn/zh/template-plugins
说明插件白名单、模板交给插件的配置与插件入口的写法,以及配置生效情况的核对方式
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
插件安装到 MarginNote 之后,面板可以打开,但场景界面上不会出现任何入口。场景界面显示的入口由模板决定,因此插件若要出现在场景界面里,模板需要写上对应的配置。菜单的写法见[卡片与摘录菜单](/zh/template-menu),模板包的结构见[最小打包](/zh/basic-package)。
插件接入模板涉及以下配置。
# 配置一览
以插件 `mn.hello` 为例:
```json template.json highlight={7 8} focus={7 8} theme={null}
{
"id": "u-hello",
"name": "你好世界",
"plugins": [
"mn.hello"
],
"pluginConfigs": {
"mn.hello": { "v": 1 }
},
"bottom": {
"modes": [{
"id": "collect",
"behavior": "write",
"tools": ["tool.excerptBox", "addon.mn.hello.primary", "theme"]
}]
},
"selectionMenu": {
"cards": ["noteHighlight:", "addon.mn.hello.annotate:"]
}
}
```
* `plugins` 是白名单,声明可以在场景界面中出现的插件。
* `pluginConfigs` 是模板给每个插件的一份配置。
* `addon.` 开头的条目把插件的按钮和菜单项呈现到界面上;本例里是一个工具条按钮和一条卡片菜单项。
`bottom.modes` 的完整写法见[工具与模式](/zh/template-bottom),`selectionMenu` 的完整写法见[卡片与摘录菜单](/zh/template-menu),本文只讲与插件有关的部分。
# 白名单
`plugins` 是一个字符串数组,取值为插件的 `addonid`。**只有写进这个数组的插件才能在场景界面中出现**,未写入该数组的插件不会出现。
系统场景模板的 `plugins` 数组恒为空,因此系统场景模板不会因为本机安装了插件而改变行为。
> [!NOTE]
>
> `plugins` 数组的内容来自插件的 `addonid`,该字段的说明见[场景插件基础](/zh/plugin-base)。
# 模板给插件的配置
`pluginConfigs` 是一个对象,键为插件的 `addonid`,值为任意 JSON。模板把这份配置交给插件,插件无需另行存储。
这份配置属于模板:导出模板包、导入模板包、复制模板时都会一并携带。模板编辑器不解释该字段的内容,MarginNote 也不校验。
```json template.json theme={null}
{
"pluginConfigs": {
"mn.vocab": {
"pendingTag": "#Pending",
"dailyLimit": 20
},
"mn.timer": {
"minutes": 25
}
}
}
```
插件读取这份配置的途径如下。
## 经事件声明读取
插件在 `mnaddon.json` 的 `excerptClaim` 中配置 `configKey`,即可让某个标签的取值由模板覆盖:
```json mnaddon.json highlight={12} focus={12} theme={null}
{
"addonid": "mn.vocab",
"title": "生词本",
"version": "0.1.0",
"author": "我",
"marginnote_version_min": "4.5.0",
"immersiveOnly": true,
"panel": "index.html",
"events": ["excerpt"],
"excerptClaim": {
"hashtag": "#X",
"configKey": "pendingTag"
}
}
```
此处的 `configKey` 为 `pendingTag`,因此该标签的取值为 `pluginConfigs["mn.vocab"]["pendingTag"]`。详见[事件系统](/zh/events)。
## 经接口读取
页面调用 `GET /immersive/template`,响应中的 `plugins` 数组列出当前模板放行的插件,每一项包含插件 id、放行状态与该插件的配置:
```javascript index.html theme={null}
var r = await mn.api("/immersive/template");
if (r && r.available) {
var list = r.plugins || [];
var mine = null;
for (var i = 0; i < list.length; i++) {
if (list[i].addon === "mn.vocab") { mine = list[i]; }
}
if (mine && mine.allowed) {
show(mine.config);
}
}
```
> [!NOTE]
>
> 取不到模板时响应里的 `available` 为假,原因在 `reason` 中,这种情况下不要循环重试。
# 插件入口的写法
插件贡献的界面元素以 `addon.` 为前缀,由插件 id 与元素名拼成。写进工具行、顶部导航等位置的代号要匹配 `^addon\.[A-Za-z0-9_.-]+\.[A-Za-z0-9_]+$`;菜单项在末尾多一个冒号,写法见下文的“卡片菜单项”一节。
## 工具条按钮
工具条按钮的代号为 `addon.<插件id>.<名字>`,写入 `bottom.modes[].tools`:
```json template.json highlight={6} focus={6} theme={null}
{
"bottom": {
"modes": [{
"id": "collect",
"behavior": "write",
"tools": ["tool.excerptBox", "addon.mn.hello.primary", "theme"]
}]
}
}
```
其中的 `primary` 是 MarginNote 固定的元素名,表示该插件的胶囊按钮;按钮图标由插件在 `mnaddon.json` 的 `icon` 字段指定。
> [!NOTE]
>
> 插件没有 `main.js` 时,MarginNote 按 `mnaddon.json` 中声明的 `panel` 合成外壳,工具条按钮、卡片命令与摘录事件都由清单声明。这种写法下不需要额外的代码文件。
## 卡片菜单项
菜单项的代号为 `addon.<插件id>.<命令id>:`,**末尾的冒号不可省略**,写入 `selectionMenu` 的对应清单:
```json template.json highlight={6} focus={6} theme={null}
{
"selectionMenu": {
"cards": [
"noteHighlight:",
"commentNote",
"addon.mn.hello.annotate:"
]
}
}
```
其中的 `annotate` 对应插件清单 `cardCommands` 中声明的命令 id。该命令的声明方式见[卡片与摘录菜单命令](/zh/menu-commands)。
## 插件里与模板里都要写入
命令要出现在菜单上,插件和模板两侧都需要写入:插件在 `mnaddon.json` 中声明这条命令,模板在 `selectionMenu` 中写入该命令的代号。缺少任何一侧,菜单上都不会出现。
| 插件里的声明 | 模板里的条目 | 结果 |
| - | - | - |
| 声明 `cardCommands` | 未写入 `selectionMenu` | 菜单上没有该项 |
| 未声明 `cardCommands` | 写入 `addon..:` | 菜单上没有该项 |
| 声明 `cardCommands` | 写入对应条目 | 菜单上出现该项 |
工具条按钮也要两侧同时写入。插件在 `mnaddon.json` 中给出 `icon`,MarginNote 读取该字段生成 `addon.<插件id>.primary` 按钮;模板把这一代号写进 `bottom.modes[].tools`,按钮才会出现。
# 安装后无法使用的情形
原因可能在插件一侧,也可能在模板一侧。
插件一侧的原因:插件清单中没有声明对应的按钮或命令。
模板一侧的原因:模板未把插件 id 写进 `plugins`,或未把对应条目写进 `bottom.modes[].tools` 与 `selectionMenu`,或代号格式写错(例如漏了菜单项末尾的冒号)。
> [!WARNING]
>
> 代号拼写错误与格式错误都不会报错,MarginNote 静默忽略。排查办法见[校验与排错](/zh/template-validate)。
# 运行中核对
面板可以读取当前模板,确认配置是否生效。响应里有每个插件的放行状态和配置内容:
```javascript index.html theme={null}
var r = await mn.api("/immersive/template");
var plugins = r.plugins || [];
var found = false;
plugins.forEach(function (p) {
if (p.addon === "mn.hello") {
found = true;
mn.log("allowed =", p.allowed, " config =", JSON.stringify(p.config));
}
});
if (!found) {
mn.log("当前模板的 plugins 里没有 mn.hello,界面上的入口不会出现");
}
```
这样能确认白名单与配置是否送达。响应里不含各份菜单清单,所以 `selectionMenu` 里的菜单项是否出现,只能在卡片菜单里人工确认。
> [!NOTE]
>
> `plugins` 中每个对象的 `addon` 为插件 id,`allowed` 表示放行状态,`config` 为模板交给该插件的配置内容。
配置没有生效时,按[校验与排错](/zh/template-validate)定位原因。
# 行为设置
Source: https://dev.marginnote.com.cn/zh/template-settings
全局预设、首页类型、自动加入复习、卡片状态的恢复与引导条
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
`bottom` 与 `surfaces` 决定界面元素,写法见[工具与模式](/zh/template-bottom)与[顶部工作栏与浮窗](/zh/template-surfaces)。本篇的字段不产生界面元素,只改变进入场景界面之后的行为:`settings` 决定全局设置的预设值,`home` 决定首页类型,`review` 决定自动加入复习,`resume` 决定卡片状态的恢复方式,`coach` 控制引导条(场景界面上的一条提示)。
这些字段同样遵循[最小打包](/zh/basic-package)的覆盖规则:留空表示不接管。
`settings.global` 的生效时机与界面元素字段不同:只在进入场景界面期间临时生效,退出场景界面或换用其他场景模板即还原。
# 全局预设
`settings.global` 是一个对象,键为能力目录中的代号,值为该能力的预设值。能力目录由 MarginNote 提供。
```json template.json theme={null}
{
"settings": {
"global": {
"pref.titleFirstForNewCard": false,
"pref.addToMap": true,
"choice.insertPosition": 0,
"choice.pageLayout": 0
}
}
}
```
每一项的取值方式按类别而定:开关类写入 `true` 或 `false`,不写入表示跟随;选项类写入具体档位,不写入表示跟随。能力目录中的动作类与面板类条目不作为预设使用。
> [!NOTE]
>
> 键必须取自能力目录。目录中的条目按 action(动作)、toggle(开关)、choice(选项)、panel(面板)分组,完整清单见[代号词汇表](/zh/template-vocabulary)。
## 临时生效与还原
预设值临时生效,退出后还原:
1. 进入场景界面时,MarginNote 先记下全局设置当时的真实值。
2. 把各项调整为模板写入的预设值。
3. 退出场景界面或换用其他场景模板时,把记下的值恢复为原值。
> [!NOTE]
>
> 某一项的预设未生效时,先确认该项是否由学习集单独配置。学习集配置的内容始终优先,模板不改动该项。
## 适合预设的项
以下各项在场景界面中使用频繁,写进 `settings.global` 可以省去手动调整:
* `pref.addToMap` 与 `pref.addBlankToMap`:摘录自动加入脑图、自动添加独立留白到脑图。
* `pref.lockactivenote` 与 `pref.lockfocusnote`:点击脑图时同步文档、点击文档时同步脑图。
* `pref.singleSideSketch` 与 `pref.docsketchontop`:单面手写、手写位于文字上层。
* `choice.noteFont` 与 `choice.baseFontSize`:卡片字体与基本字体尺寸。
* `choice.insertPosition`:脑图插入位置。写入按文档目录分组的档位时,摘录归入书籍目录中对应的章节。
> [!NOTE]
>
> 以下项目不在预设范围内:只显示标题、点击查看全卡、在卡片中显示链接、脑图手写在卡片下层。书写背景由学习集单独配置。
# 首页类型
`home` 决定进入场景界面时显示的首页。
| 值 | 含义 |
| - | - |
| 不写入 | 不接管,沿用全局默认 |
| `studyset` | 学习集主导 |
| `documents` | 文档首页 |
首页跟随场景界面,因此当前激活的场景模板决定首页的形态。系统场景模板均为学习集主导。
# 自动加入复习
`review` 控制回忆模态中的自动加入复习开关,以及自动进入复习的卡片范围。
## 自动加入复习开关
`review.autoAdd` 的取值:
| 值 | 含义 |
| - | - |
| 不写入 | 不接管,保持现状 |
| `offer` | 第一次在某个学习集中使用该模板时,询问一次是否打开 |
| `off` | 开关存在但默认关闭,且不询问 |
> [!WARNING]
>
> 该字段没有 `on` 取值。模板不得静默建立卡组或静默写入学习集。
开关出现的位置由 `bottom.modes` 决定:开关写进某一模态的 `tools` 时出现在该行上,写进 `overflow` 时收进收纳弹层。系统场景模板把开关放在回忆行上,具体位置由系统决定;`我的场景模板` 可以放在任意一行的任意位置。底部工具条上没有这个开关时,这段配置不起作用。
## 自动进入复习的卡片范围
`review.rule` 决定自动进入复习的卡片范围,字段:
| 字段 | 类型 | 说明 |
| - | - | - |
| `excludeToc` | `Bool?` | 目录与章节节点不进入。 |
| `excludeGroupChildren` | `Bool?` | 组内卡片不进入,即已经并入其他卡片的卡片不进入。 |
| `requireTitle` | `Bool?` | 没有标题的卡片不进入。 |
| `requireAddonValue` | `AddonValue?` | 插件元数据判定,条件成立才进入。 |
```json template.json theme={null}
{
"review": {
"autoAdd": "offer",
"rule": {
"excludeToc": true,
"excludeGroupChildren": true,
"requireTitle": false,
"requireAddonValue": {
"addon": "mn.vocab",
"key": "status",
"equals": "done"
}
}
}
}
```
`requireAddonValue` 的字段:
| 字段 | 类型 | 说明 |
| - | - | - |
| `addon` | `String` | 判定所依据的插件 id。 |
| `key` | `String` | 判定所依据的元数据键名。 |
| `equals` | `JSON?` | 期望的取值。 |
判定条件为:卡片的 `options["addon."][key]` 与 `equals` 相等。插件通过写入元数据来标记卡片状态,模板据此决定是否放行。例如只让标记为 `done` 的卡片进入复习。
> [!NOTE]
>
> 从外部导入的模板可能带有附加条件,只放行被特定标记的卡片。要修改需要改动模板本身。
# 恢复与引导
`resume` 决定卡片状态在离开场景界面时的恢复方式,`coach` 控制引导条。
## 卡片状态的恢复
`resume.card` 的取值:
| 值 | 含义 |
| - | - |
| `silent` | 静默处理。 |
| `pending-only` | 仅处理待办项。 |
| `always` | 始终处理。 |
## 引导条
`coach` 的字段:
| 字段 | 类型 | 说明 |
| - | - | - |
| `enabled` | `Bool?` | 是否启用引导条。 |
| `mode` | `String?` | `teach` 为新手模式,含教学文案;`flow` 为过了新手期,无事时完全退场。 |
| `harvestBatch` | `Int?` | 引导条的采集批量。 |
# 完整示例
这份示例汇总了本篇的各组字段:首页为学习集主导;`settings.global` 预设 `pref.addToMap` 与 `pref.titleFirstForNewCard`;自动加入复习开关在第一次使用该模板的学习集中询问一次;目录与章节节点、已经并入其他卡片的卡片不进入复习;卡片状态按待办项恢复。`coach` 未写入。
```json template.json theme={null}
{
"id": "u-hello",
"name": "你好世界",
"home": "studyset",
"settings": {
"global": {
"pref.addToMap": true,
"pref.titleFirstForNewCard": false
}
},
"review": {
"autoAdd": "offer",
"rule": {
"excludeToc": true,
"excludeGroupChildren": true
}
},
"resume": {
"card": "pending-only"
}
}
```
写完配置后,可以按[校验与排错](/zh/template-validate)检查字段名与代号。
# 顶部工作栏与浮窗
Source: https://dev.marginnote.com.cn/zh/template-surfaces
顶部导航、浮球与浮窗、卡片多选工具栏、显示类开关与折叠屏字段的配置
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
底部工具条由 `bottom.modes` 控制,写法见[工具与模式](/zh/template-bottom)。界面上的其余位置由 `surfaces` 控制。`surfaces` 是一个对象,每个字段对应一处位置,比如顶部导航胶囊、右缘浮球坞、卡片多选时的工具栏。
`surfaces` 整体留空表示不接管(见[最小打包](/zh/basic-package)的覆盖规则),界面保持现状。
# 字段定义
`surfaces` 下的字段取值形式分以下几类:字符串数组、名册、布尔值或整数。
字符串数组字段直接列出该位置允许出现的条目,例如 `dockPinned`。名册写成对象,对象含 `visible` 与 `overflow`,两者都是数组;`visible` 列出直接出现在界面上的条目,`overflow` 列出收进 `更多` 弹层的条目。名册这种写法在 MarginNote 内部称为 `Roster`。布尔值与整数直接控制一项设置。
采用名册的字段有 `nav`、`multiSelect`,以及旧字段 `dock`、`bottomRead`、`bottomWrite`、`bottomRecall`、`mapColumn`。
> [!NOTE]
>
> 后文提到的 `act.`、`op.`、`multi.` 等代号,各自有固定的清单。完整清单见[代号词汇表](/zh/template-vocabulary)。
# 顶部导航
`nav` 控制顶部胶囊上出现的按钮,写成名册:
```json template.json highlight={5} focus={5} theme={null}
{
"id": "u-hello",
"name": "你好世界",
"surfaces": {
"nav": {
"visible": [
"act.doc.toggle",
"act.doc.list",
"act.map.toggle",
"act.nav.search"
],
"overflow": [
"act.doc.export",
"act.exportNotebook"
]
}
}
}
```
条目按作用对象分属不同前缀:`act.doc.` 是作用于当前文档的动作,`act.map.` 是作用于脑图的动作,`act.nav.search` 是胶囊上的搜索按钮。
场景模板切换按钮本身始终存在,不受 `nav` 控制。在 `nav` 中作出调整之后,模板编辑器里会多出一个 `恢复默认` 按钮。
> [!NOTE]
>
> 部分条目属于候选项。未接管 `nav` 时,候选项不会出现在菜单中,需要在 `nav` 中显式列出才会出现。`act.map.cards`(脑图卡片)、`act.map.review`(脑图复习)、`act.map.docCards`(脑图卡片分组看板)属于此类。
# 浮球与浮窗
常驻球与浮窗展开上限分别由 `dockPinned` 与 `dockMaxOpen` 控制。
## 常驻球
`dockPinned` 列出常驻球。模板加载后,这些浮球立刻出现在右缘,点击展开面板。面板只能收起,收起后回到浮球,关不掉。数组顺序即右缘从上到下的顺序:
```json template.json highlight={5} focus={5} theme={null}
{
"id": "u-hello",
"name": "你好世界",
"surfaces": {
"dockPinned": ["contents", "ai"]
}
}
```
留空或缺省表示没有常驻球,此时只有面板收起之后才出现浮球。可选值与界面名称的对应关系:`contents`(文档与目录)、`ai`(AI)、`research`(研究)、`cards`(卡片)、`review`(复习)、`outline`(大纲)、`docCards`(卡片分组看板)。
部分浮球依赖当前是否处于脑图界面,条件不满足时不显示。该条件与模板无关。
> [!WARNING]
>
> `dock` 是旧字段,MarginNote 不读取其中的条目。早期从系统场景模板复制而来的 `我的场景模板` 中存有 `["contents","ai","research"]`。不要为该字段赋予新语义,常驻球请使用 `dockPinned`。
## 展开上限
`dockMaxOpen` 控制同时展开的浮窗数量上限:
| 值 | 含义 |
| - | - |
| 缺省 | 上限为 2。 |
| `0` | 不限。 |
| 其他正整数 | 该数值为上限。 |
超出上限时,最久未使用的浮窗会被收起。收起方式与手动收起一致:浮球保留,内容真正释放。点击浮球可重新打开。
# 卡片多选工具栏
卡片被多选时,底部出现一条工具栏,由 `multiSelect` 控制。该字段同样写成名册,条目按前缀分属 `multi.` 与 `op.`:`multi.` 是多选操作(删除、合并、改标签等),`op.` 是作用于卡片的操作(折叠、展开、转标题等)。
```json template.json highlight={9} focus={9} theme={null}
{
"id": "u-hello",
"name": "你好世界",
"surfaces": {
"multiSelect": {
"visible": [
"multi.delete",
"multi.tag",
"op.collapseSelection"
]
}
}
}
```
`multiSelectIcons` 为该工具栏上的操作按钮指定图标,写法是一个对象,键为操作代号,取值为 SF Symbol 名称:
```json template.json theme={null}
{
"multiSelectIcons": {
"op.collapseSelection": "arrow.down.right.and.arrow.up.left",
"op.toggleTitle": "textformat.size"
}
}
```
没有指定图标的按钮使用 MarginNote 内置的默认图标。
> [!WARNING]
>
> `multiSelectIcons` 是顶层字段,与 `surfaces` 同级。写进 `surfaces` 内部会被静默忽略。
# 显示类开关
以下字段各控制一项显示设置,缺省值即界面现状。
| 字段 | 缺省行为 | 取 `true` | 取 `false` |
| - | - | - | - |
| `navShowsObjectNames` | 顶部保持紧凑图标 | 显示文档与脑图的名称,并标明多窗受体 | 保持紧凑图标 |
| `navShowsStudySetName` | 不显示 | 在首页旁显示当前学习集名称 | 不显示 |
| `excerptResultBar` | 显示 | 显示摘录结果浮条 | 关闭浮条 |
| `linkingButtonVisible` | 隐藏 | 在文档与脑图之间显示联动按钮 | 隐藏联动按钮 |
| `bottomCapsuleAutoHide` | 只能手动收起 | 闲置一段时间后自动收起 | 只能手动收起 |
| `bottomCapsuleAtTop` | 底部工具条留在底部 | 宽度允许时并入顶部一行 | 留在底部 |
以下开关的细节:
* `navShowsStudySetName` 与 `navShowsObjectNames` 相互独立。空间不足时,学习集的完整名称需在模板菜单中查看。
* `excerptResultBar` 只在场景阅读现场生效。浮条上的 `摘录操作` 按钮打开原来的摘录菜单;关掉浮条后,“摘录结束自动弹出菜单”的原有设置照常起作用。
* `linkingButtonVisible` 只控制联动按钮是否出现,不改变联动模式本身,隐藏按钮不会关闭联动。
* `bottomCapsuleAtTop` 在一行放不下时(手机、窄分屏、顶部手写层栏展开)自动退回底部,宽度恢复后重新并入顶部。
# 折叠设备与侧放
`foldAware` 与 `sideRail` 控制折叠设备的呈现方式,两者都是顶层字段,与 `surfaces` 同级,取值均可为缺省、`true` 或 `false`:
| 值 | 含义 |
| - | - |
| 缺省 | 自动。`foldAware` 按设备是否为折叠设备决定;`sideRail` 按全局手动开关决定。 |
| `true` | 启用。 |
| `false` | 明确关闭。 |
`foldAware` 为自动时,折叠设备会记住折叠状态与展开状态下各自的分屏方式与浮窗位置。`sideRail` 控制顶部胶囊是否侧放成竖条,用于折叠设备的内屏。
> [!WARNING]
>
> `foldAware` 与 `sideRail` 都是顶层字段。写进 `surfaces` 内部会被静默忽略。
# 旧字段
`bottomRead`、`bottomWrite`、`bottomRecall` 写在 `surfaces` 之下,是 `bottom.modes` 之前的写法,分别对应底部工具条的读态、写态与忆态。`bottomWrite.overflow` 即写态下收进 `更多` 弹层的条目。
以上字段收录的键:读态是 `theme`、`titleLinks`、`blockSelect`;写态是 `tool.ink.*` 与 `tool.excerpt.*`;忆态是 `pref.recall_*`。读态与写态会生效,忆态的生效情况尚未确认。
`mapColumn` 用于脑图左竖条的 `mapcol.*` 条目。场景界面下该竖条整体隐藏,把条目写进 `mapColumn` 等于把对应功能移到胶囊或浮球坞。
> [!NOTE]
>
> 新写的模板请使用 `bottom.modes`,写法见[工具与模式](/zh/template-bottom)。
# 完整示例
下面的示例把 `bottom.modes` 与 `surfaces` 的几个字段放在同一份配置里:
```json template.json theme={null}
{
"id": "u-hello",
"name": "你好世界",
"bottom": {
"modes": [
{
"id": "read",
"title": "阅读",
"behavior": "read",
"tools": ["theme", "titleLinks"]
},
{
"id": "collect",
"title": "摘录",
"behavior": "write",
"tools": ["tool.excerptBox", "undoRedo", "theme"]
}
]
},
"surfaces": {
"nav": {
"visible": ["act.doc.toggle", "act.doc.list", "act.nav.search"],
"overflow": ["act.doc.export"]
},
"dockPinned": ["contents", "ai"],
"dockMaxOpen": 2,
"multiSelect": {
"visible": ["multi.delete", "multi.tag"]
},
"navShowsObjectNames": true,
"navShowsStudySetName": true,
"excerptResultBar": true,
"bottomCapsuleAtTop": false
}
}
```
# 校验与排错
Source: https://dev.marginnote.com.cn/zh/template-validate
模板配置失效的几类成因、自查方法,以及按界面现象反查成因
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
配置写错时,模板不会报错。字段名或代号写错时,MarginNote 静默忽略该条目,对应的按钮不会出现;字段的取值类型写错时,整份模板无法解析,MarginNote 回退到默认模板。
界面上的条目缺失时,成因可能是配置写错、尚未生效,或功能本身不可见。合理的顺序是先按本文自查,再导入 MarginNote 确认。
# 静默忽略的成因
上述各类错误在界面上的表现相同:某个条目没有出现。区别在成因。
## 字段名不在所属位置内
每个位置只接受属于该位置的字段。字段名不在该位置的字段表里,读取时就被丢弃。顶层字段也一样:`template.json` 顶层拼错的字段名不会报错,只是不起作用。
拼写错误是常见成因,例如把 `dockPinned` 写成 `dockPinnd`。字段的层级放错同样常见,例如把 `dockPinned` 写进 `bottom`,该字段不属于 `bottom`,写入的值不会被读取。旧字段的说明见[顶部工作栏与浮窗](/zh/template-surfaces)。
## 代号不在该位置的合法代号清单内
每个清单位置都有一份固定的代号清单,清单之外的代号被静默忽略。
拼写错误是常见成因,例如把 `act.doc.toggle` 写成 `act.doc.toggl`。
## 代号存在,但放错了位置
这一类问题最难发现:该代号在另一处确实合法,写在该位置时表面上看不出错误。
例如 `theme` 是合法代号,能放进 `bottom.modes[].tools`,写进 `surfaces.nav` 就会被忽略。
书写时看到的只是一个合法的代号字符串,因此放错位置只能通过对照清单发现。
# 取值类型写错
字段的取值类型与字段表不符时,MarginNote 无法解析整份模板。出错的不只是该字段:整份模板都被丢弃,界面回到默认模板。
常见情形是字段的取值形式与字段表不符。例如 `dockPinned` 写成对象(该字段只接受字符串数组)、`plugins` 写成字符串(该字段只接受数组)、`settings.global` 中某一项的值写成对象(取值只能是布尔值、数字或字符串)。
# 自查清单
写完 `template.json` 后,逐项对照:
1. 字段名:对照各篇的字段表,包括顶层字段。
2. 代号的拼写与所在位置:对照[代号词汇表](/zh/template-vocabulary),每个位置的清单是固定的。菜单条目的代号以模板编辑器列出的为准。
3. 插件条目:代号格式是 `addon.<插件id>.<名字>`,菜单项末尾要带冒号,见[接入插件](/zh/template-plugins)。
4. `settings.global` 的键:必须取自词汇表里的能力目录。
5. `aiTasks`:`id` 与 `prompt` 不为空,`scope`、`trigger` 取固定值,`output` 的值只能是类型名,`required` 里的字段都在 `output` 中,见[AI 任务](/zh/template-ai)。
6. 字段类型:清单里的元素都是字符串,其余字段的类型与字段表一致。
# 现象与成因对照
| 界面现象 | 可能成因 | 排查方向 |
| - | - | - |
| 工具行里预期的工具没有出现 | 代号拼错,或该代号属于其他位置 | 对照词汇表核对拼写与所属位置 |
| 按钮只出现在 `更多` 弹层,工具行上没有 | 该条目写在 `overflow` 里 | 把该条目移入 `tools` |
| 顶部导航缺少某个按钮 | 代号拼错,或写在了 `surfaces.nav` 之外 | 核对 `surfaces.nav` 里的代号 |
| 常驻浮球没有出现在右缘 | 漏写 `dockPinned`,或写进了旧字段 `dock` | 核对字段名 |
| 卡片多选工具栏缺少操作 | 代号属于底部工具条或能力目录,写在了 `surfaces.multiSelect` | 对照词汇表“卡片多选工具栏”一节 |
| `settings.global` 的预设没有变化 | 键不在能力目录内,或该项由学习集单独配置 | 对照词汇表的能力目录,见[行为设置](/zh/template-settings) |
| 某个设置项完全不起作用 | 顶层字段名拼错,或字段放错层级 | 核对顶层字段名 |
| 整份模板都不生效 | 某个字段的取值类型写错 | 对照字段表核对取值类型 |
| 插件按钮出现,点击没有响应 | 按钮的行为由插件实现,与模板配置无关 | 检查插件本身 |
# 确认模板真的生效
自查清单只能确认配置写得正确。导入之后,还需要确认 MarginNote 加载的确实是该模板:导入模板包,切换到这份模板,进入场景界面,逐项对照预期。
带插件的模板可以让面板读取当前模板,确认白名单与配置是否送达,写法见[接入插件](/zh/template-plugins)的“运行中核对”一节。读取结果不含菜单清单,菜单项是否出现,只能在卡片菜单里人工确认。
# 代号词汇表
Source: https://dev.marginnote.com.cn/zh/template-vocabulary
模板各位置可用的完整代号清单,以及插件代号与笔盒中动态引用的写法
> [!WARNING]
>
> 本文由 AI 撰写,暂未经过人工校对,请谨慎参考。
模板的每个位置都有一份固定的代号清单,只有清单里的代号会被 MarginNote 采纳,其余的静默忽略,不会报错。本文按位置列出各名册的合法代号,外加插件代号与笔盒动态引用的写法,供查阅;字段本身的含义和写法见前面各篇。
写模板时若某个按钮或菜单项没有出现,应先核对代号的拼写与所在位置。
# 使用方式
代号要生效,必须在清单内,并且放在该清单对应的位置,缺一不可。
例如 `undoRedo`:该代号在底部工具条的清单内,因此可以写进 `bottom.modes[].tools`;写进 `surfaces.nav` 会被静默忽略,因为顶部导航胶囊的清单不含这条代号。
> [!WARNING]
>
> 代号写错与代号放错位置都不会报错。校验办法见[校验与排错](/zh/template-validate)。
# 代号清单
以下清单按位置分组,每个位置的清单是固定的,本文称为静态清单。同一个代号只在所属位置有效,写进其他位置会被静默忽略。
## 底部工具条的工具行
字段:`bottom.modes[].tools`。该字段决定模态工具行上出现的工具,写法见[工具与模式](/zh/template-bottom)。能力目录中的条目也可以写进该字段。
| id | 名称 |
| - | - |
| `blockSelect` | Block Selection |
| `pref.recall_cover_all_excerpts` | Mask excerpts in the document |
| `pref.recall_force_title_only` | Titles only (hide card bodies) |
| `pref.recall_hint_use_afm` | On-device AI hints |
| `pref.recall_single_peek` | Reveal one at a time |
| `pref.recall_structured_mode` | Structured hints |
| `pref.reviewAutoAdd` | reviewAutoAddToolId |
| `recall.handwrite` | Pen group (folded) |
| `recall.hint` | Recall · AI hint toggle |
| `recall.level` | Recall · MindMap level |
| `recall.reset` | Recall · Reset |
| `recall.settings` | Recall · Settings |
| `theme` | Theme (light / dark) |
| `titleLinks` | Title Links |
| `tool.eraser` | Eraser |
| `tool.excerpt.blank` | Blank |
| `tool.excerpt.highlight` | Highlight Excerpt |
| `tool.excerpt.lasso` | Lasso Excerpt |
| `tool.excerpt.rect` | Rectangle Excerpt |
| `tool.excerpt.text` | Text Excerpt |
| `tool.excerptBox` | excerptBoxId |
| `tool.excerptConti` | Continuous Excerpt |
| `tool.excerptHand` | Hand tool |
| `tool.hand` | Hand tool |
| `tool.imagebox` | ImageBox |
| `tool.ink.crayon` | Crayon |
| `tool.ink.fountainpen` | Fountain Pen |
| `tool.ink.marker` | Marker |
| `tool.ink.monoline` | Monoline |
| `tool.ink.pen` | Pen |
| `tool.ink.pencil` | Pencil |
| `tool.ink.watercolor` | Watercolor |
| `tool.inkBox` | inkBoxId |
| `tool.lasso` | Lasso |
| `tool.penGroup` | Pen group (folded) |
| `tool.textbox` | TextBox |
| `undoRedo` | Undo / Redo |
## 顶部导航胶囊
字段:`surfaces.nav`。该字段决定顶部导航胶囊上出现的条目,写法见[顶部工作栏与浮窗](/zh/template-surfaces)。
| id | 名称 |
| - | - |
| `act.bookmark` | Document · Add Bookmark |
| `act.crop` | Document · Crop |
| `act.doc.aiBreakdown` | Document · AI Breakdown |
| `act.doc.docsPanel` | Document · Documents & Contents |
| `act.doc.export` | Document · Export |
| `act.doc.import` | Document · Import |
| `act.doc.list` | Document · Documents in study set |
| `act.doc.manage` | Document · Manage documents |
| `act.doc.more` | Document · More (N) |
| `act.doc.splitView` | Document · Side by side |
| `act.doc.tempList` | Document · Temporarily opened |
| `act.doc.toggle` | Document · Open / Close |
| `act.editPdfPages` | Document · Edit PDF Pages |
| `act.exportNotebook` | MindMap · Export Study Set |
| `act.goPage` | Document · Go to Page |
| `act.manageDictionaries` | MindMap · Manage Dictionaries |
| `act.map.cards` | MindMap · Cards |
| `act.map.docCards` | MindMap · Card Board |
| `act.map.export` | MindMap · Export |
| `act.map.layers` | MindMap · Layers |
| `act.map.manage` | MindMap · Manage mindmaps |
| `act.map.outline` | MindMap · Outline |
| `act.map.review` | MindMap · Review |
| `act.map.toggle` | MindMap · Open / Close |
| `act.nav.search` | Capsule · Search button |
| `act.ocrSetting` | Document · PDF OCR Settings |
| `act.speechCards` | MindMap · Read Cards Aloud |
| `act.viewBlankNotebook` | Document · Blank Notebook |
## 右缘浮球
字段:`surfaces.dock`。该字段为旧写法,MarginNote 不读取其中的条目;常驻球请使用 `dockPinned`,说明见[顶部工作栏与浮窗](/zh/template-surfaces)。该字段收录的代号如下。
| id | 名称 |
| - | - |
| `ai` | AI |
| `cards` | CardsView |
| `contents` | Documents & Contents |
| `docCards` | Card Board |
| `outline` | Outline |
| `research` | Research |
| `review` | Deck |
## 卡片多选工具栏
字段:`surfaces.multiSelect`。该字段决定卡片多选工具栏上出现的条目,写法见[顶部工作栏与浮窗](/zh/template-surfaces)。
| id | 名称 |
| - | - |
| `multi.chat` | Chat |
| `multi.color` | Color |
| `multi.delete` | Delete |
| `multi.link` | LinkNote |
| `multi.merge` | Merge |
| `multi.more` | More |
| `multi.regroup` | ReGroup |
| `multi.summary` | Summary |
| `multi.tag` | Hashtag |
| `op.addToMindMap` | SendToMap |
| `op.addToReview` | Add to Review |
| `op.cloneToPasteboard` | Clone to Pasteboard |
| `op.collapseBlank` | Collapse Extend |
| `op.collapseSelection` | Collapse |
| `op.copytomap` | CopyToMindMap |
| `op.expandBlank` | Expand Extend |
| `op.expandSelection` | Expand |
| `op.exportCards` | Export Card |
| `op.hideMindMapSketch` | Hide MindMap Sketch |
| `op.multiclearformat1` | Clear Format 1 |
| `op.multiclearformat2` | Clear Format 2 |
| `op.ocrForNote` | Re-recognize |
| `op.removeFromMindMap` | Remove from MindMap |
| `op.selectBranch` | Select Branch |
| `op.showMindMapSketch` | Show MindMap Sketch |
| `op.toggleDraft` | Fold the branch into an Idea Box |
| `op.toggleOcclusion` | Occlusion |
| `op.toggleTextFirst` | Text First |
| `op.toggleTitle` | Text to Title |
| `op.unmerge` | Unmerge highlights |
# 能力目录
能力目录中的条目可以写进 `bottom.modes[].tools`,直接呈现到工具行上;也可以写进 `bottom.modes[].overflow`,收进 `更多` 弹层。开关类与选项类的条目还可以写进 `settings.global`,作为该项能力的预设。字段的写法见[工具与模式](/zh/template-bottom)与[行为设置](/zh/template-settings)。
下表的“作用对象”决定条目归入顶部还是底部。个别条目没有标明作用对象与名称,用“—”表示,这些条目同样有效。
## 动作
该组条目点击后直接执行一次操作。
| id | 作用对象 | 名称 |
| - | - | - |
| `act.aiBreakdown` | doc | AI Breakdown |
| `act.aiExtractQuestions` | doc | Exercise Mode |
| `act.bookmark` | doc | Add bookmark |
| `act.crop` | doc | Crop |
| `act.docsInNotebook` | studyset | In Notebook |
| `act.excerptSettingFromMore` | studyset | Excerpt Card Creation |
| `act.exportDocMenu` | doc | Export Document |
| `act.exportNotebook` | studyset | Export Notebook |
| `act.generateTOC` | doc | Generate AI table of contents |
| `act.goPage` | doc | Go page number |
| `act.manageDictionaries` | map | Manage Dictionaries |
| `act.manageExcerptPenBox` | studyset | Manage Excerpt Box… |
| `act.manageInkPenBox` | studyset | Manage Pen Box… |
| `act.multiView` | doc | Multi views |
| `act.newGroup` | studyset | Import |
| `act.ocrSetting` | doc | PDF OCR Settings |
| `act.openPanelCardbox` | studyset | CardsView |
| `act.openPanelChat` | ai | Ask |
| `act.openPanelResearch` | ai | Research |
| `act.openPanelReview` | studyset | Deck |
| `act.search` | map | Outline |
| `act.searchBook` | doc | Contents & Search |
| `act.searchInBooks` | doc | Search in book(read mode) |
| `act.searchInMaps` | map | Search |
| `act.speechCards` | studyset | CardsToSpeech |
| `act.stopPencil` | doc | Stop Apple Pencil |
| `act.styleSetting` | doc | Styles |
| `act.translateBook` | doc | Translate |
| `act.viewDocNotesMenu` | studyset | Card Board |
| `act.viewLayersMenu` | doc | Show notes from other notebooks |
| `act.viewLinkBooksMenu` | doc | Referenced Documents |
| `act.viewMarginList` | doc | MarginList |
| `act.viewPictures` | doc | View Pictures |
## 开关
该组条目放入清单后,界面上显示当前状态,点击即可切换。
| id | 作用对象 | 名称 |
| - | - | - |
| `act.editPdfPages` | doc | Edit PDF Pages |
| `act.mapDrag` | map | Drag Mode |
| `act.mapLink` | map | Link Mode |
| `act.mapSelect` | map | Batch |
| `act.notebookBar` | doc | Handwriting Layers |
| `act.viewBlankNotebook` | doc | Blank Notebook |
| `pref.addBlankToMap` | doc | AutoAddBlankMindMap |
| `pref.addToMap` | doc | AutoAddMindMap |
| `pref.alwaysSideBySide` | doc | Always side by side |
| `pref.assistExcerpt` | doc | Assistive Excerpt |
| `pref.disable2page` | doc | Two page mode |
| `pref.docsketchontop` | doc | Document: Handwriting Above Text |
| `pref.doubleClickFocus` | map | Double Click Focus (Handwriting) |
| `pref.excerptResultBar` | doc | Excerpt result · Show feedback |
| `pref.hideFocusSketch` | map | Newly created handwriting in the focus is only displayed in the focus (hidden outside the focus) |
| `pref.hidebookscroll` | doc | Show Scroll Bar |
| `pref.immersiveSideRail` | layout | Side rail capsule |
| `pref.lockactivenote` | map | Sync Book on clicking MindMap |
| `pref.lockfocusnote` | map | Sync MindMap on clicking Book |
| `pref.mapSketchGesture` | map | MindMap Sketch Gesture |
| `pref.quickMerge` | map | Quick Merge |
| `pref.reviewAutoAdd` | studyset | Review switch · Auto add |
| `pref.singleSideSketch` | doc | Single-side Handwriting |
| `pref.tabshidden` | doc | Show Document Tabs |
| `pref.titleFirstForNewCard` | map | Title First for New Card |
| `pref.title_link_open_on_hover` | map | Open Title Links on Hover |
| `pref.uppercaseTitle` | card | Title uppercase |
| `tool.excerptConti` | doc | Continuous Excerpt |
| `topic.disableAutoFullCard` | map | Click to See Full Note |
| `topic.hideLinksInMindMapNode` | map | Show Link Comment |
| `topic.mapConfigTitleOnly` | map | Toggle mindmap snippet mode |
| `topic.mindmapBottom` | map | Mind Map: Handwriting Below Cards |
## 选项
该组条目提供若干档位供选择。
| id | 作用对象 | 名称 |
| - | - | - |
| `choice.baseFontSize` | — | — |
| `choice.copyMode` | — | — |
| `choice.docBackground` | — | — |
| `choice.docExtend` | — | — |
| `choice.doubleTap` | — | — |
| `choice.insertPosition` | — | — |
| `choice.mapBackground` | — | — |
| `choice.mapSide` | — | — |
| `choice.noteFont` | — | — |
| `choice.pageLayout` | — | — |
| `choice.splitPanes` | — | — |
| `choice.textSelection` | — | — |
## 面板
该组条目用于调整某个面板的呈现方式。
| id | 作用对象 | 名称 |
| - | - | - |
| `panel.branchStyle` | — | — |
| `panel.childDict` | — | — |
| `panel.sizeScale` | — | — |
# 插件代号
插件贡献的代号由插件 id 动态拼出,不在静态清单内,因此只要求符合格式,没有固定的取值清单。
```text theme={null}
bottom.modes[].tools addon.<插件id>.<名字>
selectionMenu.* addon.<插件id>.<命令id>:
```
末尾的冒号不可省略。详见[接入插件](/zh/template-plugins)与[卡片与摘录菜单](/zh/template-menu)。
# 笔盒中的动态引用
笔盒里按序号引用的工具属于动态引用,同样不在静态清单内:
```text theme={null}
excerpt:<序号>
sketch:<序号>
undo:<序号>
```
早期按支引用的写法 `pen.excerpt.<序号>` 与 `pen.ink.<序号>` 在读取时转换成对应的笔盒条目,新写的模板应直接使用笔盒代号。
核对办法见[校验与排错](/zh/template-validate)。
# UI 控制
Source: https://dev.marginnote.com.cn/zh/ui
# 窗口尺寸
面板首次加载时,其尺寸将采用 `mnaddon.json`中的 `panelWidth` 与 `panelMaxHeight` ,未填写时默认值分别为 380 和 560。
当窗口高度被更改后,MN 会对高度做如下处理:
```text theme={null}
h' = MIN(MAX(h, 54), maxContentHeight)
```
其中`maxContentHeight`默认值为560,如果插件有 main.js 文件,可以通过代码修改此值:
```javascript main.js theme={null}
var p = AddonWebPanel.panel();
p.maxContentHeight = 760
```
关于 main.js 相关信息详见 [面板之外](/zh/jsaddon)
## 顶部拖拽栏
需要注意的是,每个面板顶部都有一个 36px 高的拖拽条。因此在开发时最好不要在顶部放置可交互的 UI 元素。此高度也可以通过读取 CSS 变量`--mn-drag-h` 来获取。
## 更改窗口尺寸
* mn.height(h):修改页面高度。无动画
* mn.fit():将面板**高度**调整为``的高度。页面加载完成后 MN 会自动调用一次,建议后续内容发生变化时再次调用。
* mn.size(w, h):同时指定面板宽度与高度。这是唯一可以修改宽度的接口。有过渡动画
# 面板模式
面板有`workspace`与 `mini`两种模式。建议按此为插件设计两种大小的样式。
## 更改面板模式
`mn.mode(m)` 只修改模式的标记,不改变面板尺寸。因此只调用 `mode` 而不调用 `size`,窗口不会有任何变化,因此需要自行更改窗口大小
```javascript theme={null}
mn.mode('mini');
// then
mn.size(210, 54);
// or
mn.fit();
```
模式被修改后,MN 会通过以下方式通知页面:
* 修改 `` 元素的 `data-mode` 属性
* 修改 `mn.currentMode`
## 适配两种模式
适配面板模式可以用样式区分。因为MN 会修改 `data-mode` 属性,可以利用 CSS 的方式进行自动适配:
```css theme={null}
:root[data-mode="mini"] body { padding: 6px 10px }
:root[data-mode="mini"] .fold { display: none }
```
如果有需要逻辑处理,还可用代码可以读取 `mn.currentMode` ,或在根元素上挂载`MutationObserver`,来监听 `data-mode` 的变化。
# 关闭与重载
可以通过调用`mn.close()`与`mn.reload()` 来关闭与重载插件面板,这将丢失内存中的所有数据。
# 使用 MN 内置 AI
Source: https://dev.marginnote.com.cn/zh/use-inner-ai
MN 内置了 AI 服务。插件无须自行接入模型,也无须自行保管密钥;发送的内容范围、消耗的额度以及用户是否知情,均由 MN 处理。
# 声明 AI 任务
任务声明在模板中,增加一条任务只需改动模板,MN 本身无须改动。`mnaddon.json` 不必为此增加字段。
```json template.json theme={null}
{
"id": "u-hello",
"name": "你好世界",
"aiTasks": [
{
"id": "explain",
"title": "解释一下",
"trigger": "user",
"scope": "word_sentence",
"tier": 1,
"output": { "reply": "string" },
"required": ["reply"],
"prompt": "用一句中文解释给定的词或短语在学习笔记语境下的意思。reply 写那一句话,不要罗列义项、不要加原文里没有的信息。"
}
]
}
```
任务可用的字段如下:
* `id`:任务的标识,依据该字段指定要调用的任务。
* `title`:显示给用户的任务名称。
* `trigger`:说明该任务的发起方式。默认值 `"user"`。
* `"user"` 表示仅在用户触发之后发出
* `"auto"` 表示允许插件自行发起。需要先取得一次同意。
* `scope`:发送出去的材料的范围。
* `word_sentence` 允许插件提供一个词语和该词语所在的句子。词语不超过 80 个字符,句子不超过 600 个字符。
* `card_text` MN 从卡片中取出标题与正文,插件传入的内容不予采用。
* `attempt_ink` 用于读取卡上书写内容的场合:手写、文本框与公式框合并为一张图片发送,交由识别手写的模型处理,卡上没有书写内容时该次调用会被拒绝。
* `prompt`:提示词。
* `output`:返回值的 JSON Schema,取值只能是 `"string"`、`"number"` 与 `"bool"`。
* `required`:`output` 中必须有值的字段。
* `tier`:所用的模型级别,`0` 最快,`2` 最强,默认值 `0`。
# 调用 AI
页面以 `mn.ai(卡片id, 任务id, 参数)` 调用一条已经声明过的任务。
`参数`随 `scope` 而定:
* `word_sentence` 的材料由插件放进参数,需提供 `word` 与 `sentence`。
* `card_text` 的材料由 MN 从卡片上取出,参数不会发送出去,`参数` 中填写的内容不予采用。
* `attempt_ink` 的图片由 MN 从卡上合成,图片之外的材料由插件放进参数,例如题干与参考答案。
下面是一个对上文定义的 AI 任务的调用例子:
```javascript index.html theme={null}
// word_sentence
var r = await mn.ai(noteid, "explain", { word: word, sentence: sentence });
if (r && r.ok) {
show(r.json.reply);
}
// card_text
var r = await mn.ai(noteid, "make_summary", {});
// attempt_ink
// TODO: 补充示例
```
返回结果的形式如下:
* 成功时返回 `{ ok: true, text, json }`。
* `json`:按任务的 `output` 解析出来的对象
* `text`:模型返回的原文。
* 失败时返回 `{ ok: false, error }`,`error` 包括:
* `bad_args` 表示材料不符合要求,或该任务的声明不合格。
* `bad_output` 表示模型返回的字段结构不对,卡片上没有写入任何内容。
* `task_used` 表示同一张卡上的该任务 MN 只发送一次,重复请求会被拒绝。
* `declined` 表示用户在弹出的提示中选择了拒绝。
* `consent_required` 表示自动发起的任务尚未取得同意。
* `max_required` 表示该任务需要 MAX 订阅。
* `no_service` 表示 App 中尚未配置 AI 服务。
* `rate_limited` 表示请求过于频繁。
* `no_ticket` 表示该卡没有交给本插件的 AI。
* `not_owner` 表示该卡上的 AI 交给了别的插件。
* `network` 表示请求没有送达。
* `no_ink` 表示作答区域中没有可以取用的内容。
> [!TIP]
>
> AI 的处理需要数秒。等待期间用户可能已经切换到别的卡片,返回的结果应当写入发起时记录的卡片,不要用当前聚焦的卡片进行处理。
> [!NOTE]
>
> MN 在扣除额度之前会弹出提示征求用户同意。提示尚未关闭时再次发送请求,会被视为拒绝。因此发起期间应当禁用按钮,避免重复点击。
# 指定 AI 交给的插件
模板的摘录工具与划线快捷项各自带有 `ai_addon` 字段,取值为插件的 id,指明用该工具摘下来的卡片交由该插件处理。划线快捷项指在文档中选中一段文字时,菜单条上出现的一排快捷按钮。
在 `编辑场景模板→ 卡片与摘录菜单` 中,添加划线快捷项时可配置`AI 交给:……`。
```json template.json theme={null}
{
"excerptShortcuts": [
{
"title": "收进生词本",
"hashtag": "#Pending",
"ai_addon": "mn.hello"
}
]
}
```
用该摘录工具摘录出的卡片,只有 `ai_addon` 字段指向的插件能对其调用 AI。卡片已经交给别的插件之后,本插件在该卡上调用任务会被拒绝,收到 `not_owner`;该工具没有启用 AI 时收到 `no_ticket`。
# 从模板读取任务列表
页面可以读取当前模板声明的任务,并依据声明生成按钮,这样再往模板中增加任务时不必改动插件:
```javascript index.html theme={null}
var t = await mn.api("/immersive/template");
var tasks = (t && t.aiTasks) || [];
tasks.forEach(function (task) {
addButton(task.title || task.id, function () { runAI(task.id); });
});
```
# 列出任务包
Source: https://dev.marginnote.com.cn/api-reference/任务包与同步诊断/列出任务包
/openapi/plugin-api.yaml get /bundles
列出现存的拆书任务包,包含运行中的作业,以及 App 重启后留在磁盘上的目录。各条目的字段含义见 `BundleEntry`。
条目的时间基准随类别不同。磁盘上的包按目录创建时间计,作业条目按作业
开始时间计。因此运行中的长作业不会因为开始时间早于 `since` 而从增量轮询里消失,
该条目的时间戳也不会随进度前进。
缺少的可选字段视为该字段不存在,不得视为空值。
`jobId` 说明不了作业所属的会话。经 prepare 登记的包都有该字段,App 重启后由磁盘标记
回带的包也有,判断作业是否属于本轮要看 `state`。
响应里不含标题、预设与来源。
# 列出任务包文件
Source: https://dev.marginnote.com.cn/api-reference/任务包与同步诊断/列出任务包文件
/openapi/plugin-api.yaml get /bundles/{bundleId}/files
不带 `path` 时列出包内文件。带 `path` 时返回该文件的原始内容,路径相对包根,层级用
`/` 分隔。
包内除 `manifest.json` 与 `source.json` 之外,还有若干标记文件:`.bridge` 标明
该包经接口创建,导入后另写下 `.imported` 与 `import-receipt.json`。
这些文件都不是供插件写入内容的位置。
带 `path` 时响应不是 JSON,`mn.api` 会把 Response Body 当 JSON 解析并抛出语法错误,必须改用
`fetch` 原语读取。
# 查询旧同步事件
Source: https://dev.marginnote.com.cn/api-reference/任务包与同步诊断/查询旧同步事件
/openapi/plugin-api.yaml get /sync/legacy/events
按本设备的序号增量读取同步事件。一次取 1 到 200 条,默认 50 条。
续读时应将上一次响应中的 `lastSequence` 作为 `afterSequence` 传入。游标字段(`hasMore`、
`historyTruncated`、`cursorAdvancedPastGap`、`cursorAhead`)的含义见 `LegacySyncEventsResponse`。
# 获取旧同步状态
Source: https://dev.marginnote.com.cn/api-reference/任务包与同步诊断/获取旧同步状态
/openapi/plugin-api.yaml get /sync/legacy/status
读整机的旧同步诊断快照。响应里不含笔记正文、文档名、账号与云端记录标识。
旧同步分两条通道:一条携带学习集结构与学习数据,另一条携带普通原始文档字节。
两条会先后完成,其中一条完成不证明另一条也完成。
`cloudKit.errorCount` 是本进程保留的历史诊断数。状态为暂停时要看
`cloudKit.suspensionNeedsAttention`,该字段为假可以是正常增量拉取的临时暂停,为真才
表示本轮暂停里出过错。本轮的错误次数读
`cloudKit.suspensionErrorCount`,不要用历史总数代替。
# 列出卡组复习卡
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/列出卡组复习卡
/openapi/plugin-api.yaml get /decks/{deckid}/cards
列出卡组里的复习卡。每项返回复习卡本身、该卡对应的背面卡片,以及背面卡片是否存在(见 `sourceAlive` 字段)。
传学习集的标识进来会返回 `422 NOT_A_DECK`,反过来则返回 `IS_A_DECK`。
# 列出复习卡组
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/列出复习卡组
/openapi/plugin-api.yaml get /decks
列出复习卡组。卡组与学习集是两种对象,id 不通用,因此列学习集的接口不返回卡组。
`cardCount` 只统计复习卡数量,与笔记记录数不一定相等。
# 列出子脑图
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/列出子脑图
/openapi/plugin-api.yaml get /study-sets/{topicid}/sub-maps
列出该学习集里的子脑图,顺序与界面一致。子脑图可以嵌套,
深度大于 0 表示这张卡套在别的子脑图里,`parentPath` 从外到内列出祖先。
# 列出学习集文档
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/列出学习集文档
/openapi/plugin-api.yaml get /study-sets/{topicid}/documents
列出该学习集里的文档。没有列出全库文档的接口,跨学习集查找文档要用搜索接口。
每一项返回内容标识、标题、可用状态。`sourceState` 说明文件本身是否还在,
`unavailableReason` 说明拿不到的原因。
# 列出快照
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/列出快照
/openapi/plugin-api.yaml get /study-sets/{topicid}/snapshots
列出快照,用于核对打点是否生效。
# 列出文档笔记本
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/列出文档笔记本
/openapi/plugin-api.yaml get /study-sets/{topicid}/notebooks
列出该学习集引用的文档笔记本。
# 获取学习集脑图
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/获取学习集脑图
/openapi/plugin-api.yaml get /study-sets/{topicid}/tree
读整个学习集的脑图结构、标题与逻辑属性。
节点是扁平数组,父子关系靠每一项的 `parent` 自行组合,没有嵌套的下层数组。
要按文档顺序排列时带 `sort=document`。不要自行按页码排序:摘录与摘题产出的卡片没有页码,
页码记在该题的摘录子笔记上。带该参数时由 MarginNote 排序,结果与界面上按页码排序一致,
每个节点还会补上 `docPage` 与 `docBookmd5`。排序在切片之前对完整序列执行,跨页顺序因此正确。
翻页用 `limit` 与 `cursor`,响应里的 `nextCursor` 为 `null` 表示已到末尾。
达到遍历上限时 `walkLimitReached` 为真,为真时才表示读到末页也取不全。
# 获取标签树
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/获取标签树
/openapi/plugin-api.yaml get /study-sets/{topicid}/hashtags
读整个学习集的标签:实际存储的标签、扁平展开的层级、嵌套的层级树。
`nodes` 是扁平数组,`tree` 是嵌套数组,卡片 id 只挂在扁平的 `nodes` 上。
`directCardCount` 只统计明确存了该条完整路径的卡,`cardCount` 还包含所有下层路径
覆盖到的卡。`derived` 的含义见字段说明。
传入 `withNotes=1` 时,卡片 id 的返回规则见参数说明;数量超出 `noteIdLimit` 时仅标记截断,不自动省略。
# 获取色板
Source: https://dev.marginnote.com.cn/api-reference/学习集与卡组/获取色板
/openapi/plugin-api.yaml get /study-sets/{topicid}/note-palette
读该学习集实际使用的那套摘录色板。
每个编号返回两个颜色:`baseColor` 是配置里的原色,`displayColor` 是随当前主题调整后的颜色。
取色或者比对颜色用 `displayColor`,把编号当成固定的颜色名则会出错。
# 查询作答转场结果
Source: https://dev.marginnote.com.cn/api-reference/手写会话/查询作答转场结果
/openapi/plugin-api.yaml post /writing/transition-result
用发起转场时的 `requestId` 查询结果。查不到记录时不代表转场没有发生,只说明本进程没有这笔记录。
# 退出当前练习尝试
Source: https://dev.marginnote.com.cn/api-reference/手写会话/退出当前练习尝试
/openapi/plugin-api.yaml post /writing/session-exit
退出当前练习尝试并记录可恢复状态。退出不等于笔迹一定已经持久化,之后应读取会话记录。
# 列出文档摘录
Source: https://dev.marginnote.com.cn/api-reference/文档与页面/列出文档摘录
/openapi/plugin-api.yaml get /documents/{bookmd5}/excerpts
列出这本书上当前的全部摘录,并标出位置重复的段。
每一项里有两个 id:`noteid` 指向文档笔记本里的记录,`cardNoteId` 指向脑图上的卡片。
插件状态记在后者上。
条数超过上限时 `truncated` 为真。只要重复段时带 `duplicatesOnly=1`。
# 扫描目录候选
Source: https://dev.marginnote.com.cn/api-reference/文档与页面/扫描目录候选
/openapi/plugin-api.yaml get /documents/{bookmd5}/toc-candidates
在端上渲染、识别并跑版面模型,找出可以作为目录条目的标题候选。该接口开销较大,
扫描件上大约每页三秒。
单次跨度上限 60 页,页范围用 PDF 页序。同一页内的候选先后不是阅读顺序。
与读页文本共用两个扫描名额,名额满了会立即返回 `429`,不排队。
扫描页数达到单次跨度上限时 `truncated` 为真,此时仅返回已扫描的部分;`pagesScanned` 为 0 表示页范围越界或
整段为留白,该情形不返回下一页的起点。
# 获取文档同步状态
Source: https://dev.marginnote.com.cn/api-reference/文档与页面/获取文档同步状态
/openapi/plugin-api.yaml get /documents/{bookmd5}/sync-status
核对该文档的数据库引用、云端范围、本地与云端映射、下载队列与陈旧映射。
结论以 `state` 为准,该字段是整体判断。数据库一侧出现查询错误时,
响应里会列出这些错误,此时结论只是部分证据。外部文件提供方都报未核实。
带 `verifyHash=1` 才计算内容 hash。默认不算;打开之后要扫描整份已实体化的文件,大文档很慢。
# 获取文档目录
Source: https://dev.marginnote.com.cn/api-reference/文档与页面/获取文档目录
/openapi/plugin-api.yaml get /documents/{bookmd5}/outline
读文档的章节目录与各自的页范围,用于按章取内容。
响应里的页数是 PDF 页序,与书上印的页码不是一返回事。`pageCount` 仅供参考,
文档离屏打开失败时会给 0。
# 获取文档页文本
Source: https://dev.marginnote.com.cn/api-reference/文档与页面/获取文档页文本
/openapi/plugin-api.yaml get /documents/{bookmd5}/pages
按页范围读文本。没有文本层时 MarginNote 会先渲染再识别,因此该接口可能较慢。
单次请求最多跨 60 页。页范围按 PDF 页序编号,第一页为 1。超出范围的页返回空文本,状态码仍为 `200`。
带 `blocks=1` 时返回版面块的规则见参数说明,每块返回文字、类型与页内坐标。
扫描类接口共用两个名额,名额满了会立即返回 `429`,不排队。
新摘录进入全文索引有约十秒延迟,刚摘下来的内容不一定马上搜得到。
# 获取页面媒体文件
Source: https://dev.marginnote.com.cn/api-reference/文档与页面/获取页面媒体文件
/openapi/plugin-api.yaml get /documents/{bookmd5}/pages/{index}/content/{contentId}
按 `contentId` 取回某一页上某项二进制内容的原始字节,例如手写与留白的合成图。
`contentId` 取自 `GET /documents/{bookmd5}/pages/{index}/parts`。
返回的是图片本身,不是 JSON,因此该接口要用 `fetch` 原语读取。
# 获取页面注释面
Source: https://dev.marginnote.com.cn/api-reference/文档与页面/获取页面注释面
/openapi/plugin-api.yaml get /documents/{bookmd5}/pages/{index}/parts
读该页上的视觉注释面:手写、插入的留白、按选择生成的留白,以及这些内容与 MarginNote
记录的对应关系。
响应里的 `parts` 是可以取图的项,`blanks` 是逻辑留白。一个按选择生成的留白可能包含多张图,
各张图须作为一组一同取用。`mapBacked` 的含义见字段说明。
`studySet` 的过滤规则见参数说明:只调整旧留白映射的解析优先级,不按学习集过滤。
# 列出学习集
Source: https://dev.marginnote.com.cn/api-reference/查找/列出学习集
/openapi/plugin-api.yaml get /library/study-sets
列出资料库里的学习集。该接口不列复习卡组,卡组要用另一个接口列出。
`noteCount` 的统计范围包含每张卡里的摘录,与脑图节点数不一定相等。
# 列出文档文件夹
Source: https://dev.marginnote.com.cn/api-reference/查找/列出文档文件夹
/openapi/plugin-api.yaml get /library/folders
列出文档文件夹。文件夹是文档根目录下的真实目录,层级用 `/` 分隔,根目录传空串。
文件夹与学习集互不影响。给文档分类还有另一种方式是标签,与该接口无关。
# 搜索资料库
Source: https://dev.marginnote.com.cn/api-reference/查找/搜索资料库
/openapi/plugin-api.yaml get /search
在整个资料库里搜索,匹配分卡片、学习集与文档。用 `scope` 限定匹配的类别。
`truncated` 为真表示扫描窗口未覆盖全部结果,不代表资料库中没有匹配项,此时应改用更具体的检索词重新搜索。
新摘录进入全文索引有约十秒延迟。
# 列出沉浸模板
Source: https://dev.marginnote.com.cn/api-reference/界面与模板/列出沉浸模板
/openapi/plugin-api.yaml get /immersive/templates
列出可以切换的全部模板,每项返回标识、名称、是否内置与是否当前在用。
该接口只返回摘要,不展开完整内容。切换后须再次读取当前模板,才能取得新模板。
# 获取沉浸模板
Source: https://dev.marginnote.com.cn/api-reference/界面与模板/获取沉浸模板
/openapi/plugin-api.yaml get /immersive/template
读取当前模板的完整内容:模板摘要、各插件的可用状态与配置、摘录工具、划线快捷项、
复习入组规则、模板预设,以及声明的 AI 任务。
AI 任务默认不返回提示词原文,只返回字数;要读全文需带 `?prompts=1`。任务的输出字段与
必填清单,只在声明过时返回。
取不到模板时响应里的 `available` 为假,原因在 `reason` 里。这种情况不要循环重试。
# 获取界面状态
Source: https://dev.marginnote.com.cn/api-reference/界面与模板/获取界面状态
/openapi/plugin-api.yaml get /ui-state
返回当前界面状态的键值。插件常用的键包括当前学习集的标识、当前焦点卡的标识、
当前文档的内容标识,以及是否处在沉浸态。
键的集合随 MarginNote 的版本增减。带类型包装的值原样读回,不用解析。