MokiDown开发者文档GitHub
MOKIDOWN / DEVELOPERS

让你的想法,
成为编辑器的一部分。

用一个独立插件,扩展写作的每一步。
从 Markdown 样式到文档工具,让能力自然融入 MokiDown。

01自然融入编辑器

状态、差异与版本,使用编辑器现有界面。

02独立开发与分发

本机热重载,一个文件完成打包和交付。

03随时启用与卸载

独立运行环境,关闭即撤销样式与界面贡献。

01 / GET STARTED

从一个可运行的插件开始

准备 Node.js 22.13+,在 MokiDown 源码目录安装依赖。脚手架会生成带配置、状态栏、选区操作和 Markdown 样式的阅读助手。

Terminal
npm ci
npm run plugin:create -- ./my-plugins/reading example.reading
npm run plugin:dev -- ./my-plugins/reading
  1. 1
    连接开发服务

    打开「设置 → 语言服务 → 插件开发」,填写终端打印的 http://127.0.0.1:3141/package,点击连接。

  2. 2
    保存,即刻看到变化

    编辑 src/index.mjs 或样式。开发服务重新构建,编辑器校验新包并重载运行环境,通常约 2 秒生效。

  3. 3
    打包与交付

    执行下方命令,在 releases/plugins 取得带版本号和摘要的单文件包,可直接导入或发布到 GitHub Release。

Terminal
npm run plugin:pack -- ./my-plugins/reading
# 或进入生成的项目后执行 npm run dev / npm run build
开发会话只连接本机

支持 127.0.0.1、localhost 和 IPv6 回环 HTTP 地址。会话不自动恢复,增加权限后需断开并重新连接;失败构建保留上次可用包。暂停插件即停止轮询,断开会话同时卸载运行环境。

02 / PROJECT

小项目,完整结构

Project
reading/
├─ manifest.json       # 元数据、权限和配置声明
├─ package.json        # dev / build 与本地 SDK 类型
├─ .moki.mjs           # 本地工具入口
└─ src/
   ├─ index.mjs        # definePlugin 入口
   └─ reading.css     # 可撤销的 Markdown 样式

插件只导入 @mokidown/sdk,不引用编辑器内部组件或 store。开发工具随 MokiDown 源码提供;移动插件目录后可设置 MOKIDOWN_TOOLS 指向此源码的 scripts 目录。执行 npm install 可安装脚手架声明的本地 SDK,供编辑器类型提示使用。

用清单说明插件需要什么

只声明实际使用的权限。fileExtensions 限定工作区读写类型;没有文件操作的样式插件可以省略它。配置表单由宿主生成,保存配置会重启插件并保留启用状态。

manifest.json
{
  "id": "example.reading",
  "name": "阅读助手",
  "version": "1.0.0",
  "entry": "src/index.mjs",
  "permissions": ["editor.read", "ui.statusBar", "styles.markdown"],
  "configuration": {
    "fields": [
      {"key": "fontSize", "label": "正文字号", "type": "number", "default": 16, "min": 12, "max": 28}
    ]
  }
}

配置类型:text、number、boolean、select、url。secret 使用固定键 credential,directory 使用 workspace。访问密钥由宿主保存,不返回给插件;目录使用工作区 API 访问,不暴露绝对根路径。

03 / API REFERENCE

能力,就在需要的地方

SDK 1.3.0 保持 apiVersion: 1。先查询能力,再调用新接口;旧插件保持兼容。卡片上的权限名称对应清单声明。

Capability discovery
const context = await sdk.context();
if (await sdk.capabilities.has('editor.selection')) {
  const selection = await sdk.editor.getSelection();
  // selection: { documentId, from, to, text, checksum }
}

文档快照

editor.read

读取当前或指定已打开文档,获取未保存正文、语言及 SHA-256。

sdk.editor.getActiveDocument()sdk.editor.getDocument(documentId)sdk.editor.listDocuments()sdk.editor.onDidChangeDocuments(callback)sdk.editor.onDidChangeContent(callback)sdk.editor.onDidSave(callback)

选区读取

editor.read

读取源码编辑器的 UTF-16 选区与选中文本。

sdk.editor.getSelection(documentId?)

文本编辑与定位

editor.write

通过校验和编辑正文、聚焦标签与设置选区,保留撤销历史。

sdk.editor.applyEdits(id, checksum, edits)sdk.editor.setSelection(id, checksum, from, to?)sdk.editor.focusDocument(id)sdk.editor.reloadDocument(id) — also workspace.read

差异与版本

editor.read

注入差异操作和历史列表,业务策略留在插件。

sdk.editor.showDiff(options)sdk.editor.showVersions(options)sdk.versions.set(source) / remove(path)sdk.versions.onDidSelect(callback)sdk.versions.onDidRequestRefresh(callback)

授权目录

workspace.read

列出和读取授权目录内声明的文件类型。

sdk.workspace.list(path?)sdk.workspace.read(path)

文件写入

workspace.write

通过预期校验和创建、更新或删除文件,拒绝覆盖未保存内容。

sdk.workspace.write(path, content, expectedHash?)sdk.workspace.delete(path, expectedHash)

插件存储

storage

独立持久存储,支持读取、写入、列举及删除键。

sdk.storage.get(key) / set(key, value)sdk.storage.keys() / delete(key)

服务请求

network

向配置的服务发送 JSON 请求,密钥由宿主附加。

sdk.net.request('/path', { method, body })

命令

commands

注册异步命令并返回操作结果,卸载时清理。

sdk.commands.register(id, title, asyncCallback) → unsubscribe

状态与任务

ui.statusBar

注入文档状态、菜单动作和任务进度。

sdk.statusBar.set(id, item) / remove(id)sdk.tasks.set(id, task) / remove(id)

文件图标与标记

ui.fileIcons

按扩展名或路径注入 SVG 图标和状态标记。

sdk.fileIcons.set(id, decoration) / remove(id)sdk.fileBadges.set(id, decoration) / remove(id)

编辑器通知

ui.notifications

发送使用编辑器主题的限时通知。

sdk.notifications.show(message, { tone: 'success', duration: 4000 })

Markdown 样式

styles.markdown

注入限定到 Markdown 内容区域的样式,卸载即撤销。

sdk.styles.setMarkdown(id, css) / remove(id)

主题与生命周期

无需额外权限

查询深浅主题、监听切换,通过 AbortSignal 与清理回调释放资源。

sdk.context()sdk.capabilities.get() / has(name)sdk.theme.get() / onDidChange(callback)sdk.onDispose(callback) / signal

插件配置

无需额外权限

读取已声明的非密钥配置;不返回访问密钥和目录绝对路径。

sdk.configuration.get()
下载完整 TypeScript 类型 ↗

读取、选择,再原子编辑

文档快照包含未保存正文和校验和。编辑及设置选区都要求 expectedChecksum,避免异步返回覆盖用户后续输入。偏移使用 UTF-16,与 JavaScript 字符串一致;多个编辑范围不得重叠。

src/index.mjs
import { definePlugin } from '@mokidown/sdk';

definePlugin(async (sdk) => {
  const stop = await sdk.commands.register('reading.quote', '添加引用', async () => {
    const selected = await sdk.editor.getSelection();
    if (!selected?.text) return;
    await sdk.editor.applyEdits(selected.documentId, selected.checksum, [{
      from: selected.from, to: selected.to,
      text: '> ' + selected.text,
    }]);
  });
  sdk.onDispose(stop);
});
源码与富文本

读取文档和样式注入支持两种模式。选区读取、选区定位及文本编辑当前使用源码编辑器,富文本模式下返回 SOURCE_EDITOR_REQUIRED。编辑走原生编辑器事务,可撤销;应用修改不会自动保存磁盘文件。

onDidChangeContent 在输入暂停约 120ms 后发送 {documentIds},不携带整篇正文。按需调用 getDocument(id) 读取;onDidChangeDocuments 负责标签打开、切换与关闭,onDidSave 负责保存事件。

融入界面,把业务留在插件

插件在隐藏的隔离运行环境里执行。通过 SDK 注册编辑器贡献:状态栏可绑定文档,任务显示进度,文件图标支持路径或扩展名,历史源进入版本列表。无需创建独立插件页面。

Document status & diff
await sdk.statusBar.set('reading.state', {
  documentId: doc.id, text: '检查完成', tone: 'success',
  command: 'reading.review',
});
await sdk.editor.showDiff({
  path: doc.path, content: referenceText, label: '修改建议',
  actions: [{id: 'apply.edits', title: '应用修改', command: 'reading.apply'}],
});

差异动作回调收到 document、draftContent 和 referenceContent。返回成功结果后关闭对比,抛出异常则保留界面。宿主不解释动作标题,也不承担同步、翻译或其他业务策略。编辑器只显示当前标签对应的状态;版本列表的选择和刷新通过 sdk.versions 事件交给插件。

写自己的 Markdown 风格

静态样式在清单声明,动态样式通过 SDK 注入;两者都限定在 Markdown 内容内,不影响设置和工具栏。后启用的插件遵循 CSS 层叠,禁用后全部撤销。

Runtime styles
const { fontSize = 16 } = await sdk.configuration.get();
await sdk.styles.setMarkdown('reading', `
  :scope { font-size: ${fontSize}px; }
  h1, h2 { color: var(--accent); }
  blockquote { border-left: 3px solid var(--accent); }
`);
const stopTheme = sdk.theme.onDidChange(async (theme) => {
  // 可以按深浅主题计算需要的样式变量。
});
sdk.onDispose(stopTheme);

:scope / :root 代表 Markdown 容器。支持普通规则、@media 和 @supports;嵌套语法需预先展开。不支持外部 URL、@import、@font-face、fixed / sticky。单张样式上限 128 KiB;样式 ID 相同会替换。

04 / BUILD WITH CONFIDENCE

明确的生命周期

启用→独立环境→激活 SDK→注入能力→卸载清理

definePlugin 的激活函数可返回清理函数;也可用 onDispose 注册多个清理回调,按注册逆序执行。关闭或更新插件后,宿主移除命令、状态、图标、版本源、样式、事件订阅和运行环境,取消宿主网络请求。

Dispose resources
definePlugin(async (sdk) => {
  const timer = setInterval(() => refresh(), 10000);
  sdk.onDispose(() => clearInterval(timer));
  const stop = sdk.editor.onDidSave(() => refresh());
  sdk.onDispose(stop);
  // 可将 sdk.signal 交给接受 AbortSignal 的异步任务。
  return () => releaseLocalResources();
});

激活失败或未捕获异常会显示运行错误并停用插件;启动未完成会报超时。开发会话在错误运行环境卸载后继续等待构建,修复源码会自动恢复。命令的业务异常仅返回给调用方,不自动停用。不要吞掉业务失败后返回成功。

一次构建,独立交付

打包器将 JavaScript 依赖合并为单文件 IIFE,并收集清单中声明的 CSS。不要从 JavaScript import CSS。生成的文件名含版本和摘要前缀;每次正式变更应增加版本号,不替换已发布版本的资产。

Catalog entry
{
  "id": "example.reading",
  "name": "阅读助手",
  "version": "1.0.0",
  "url": "https://github.com/you/reading/releases/download/v1.0.0/example.reading-1.0.0-DIGEST.mokiplugin.json",
  "sha256": "使用打包器输出的完整摘要",
  "checksumMode": "canonical-json-v1"
}

清单摘要使用规范化 JSON:移除顶层 integrity 后,递归排序对象键,保留数组顺序与字符串内容,再计算 SHA-256。它不是文件原始字节摘要。包、清单、版本和内置资源必须对应;更新会逐项校验,不通过则保留当前版本。

SDK 采用语义版本号,新能力通过 capability 查询。API 1 的既有方法保持兼容;旧宿主缺少 capabilities 时 SDK 返回空列表,插件可降级到已存在的功能。

遇到问题,从这里查起

错误处理方式
PERMISSION_DENIED将所需权限加入清单,重新打包;开发会话增加权限后需重新连接。
DOCUMENT_CHANGED重新读取快照或选区,基于最新 checksum 计算修改。
SOURCE_EDITOR_REQUIRED切换源码模式后执行选区或文本编辑;样式可继续在富文本中使用。
PLUGIN_START_TIMEOUT确认入口调用 definePlugin,输出为完整 IIFE,且没有未打包的依赖。
PLUGIN_CHECKSUM_MISMATCH对齐清单和包版本,使用 canonical-json-v1 摘要,不使用文件原始字节摘要。
开发服务 503查看终端构建错误,修复后保存。运行中的旧包不会因此被替换。

文件读写仅在授权目录内进行,不能逃逸路径或覆盖未保存的编辑器内容。网络 API 仅访问配置的服务地址,RPC 默认超时 35 秒。通知每插件每 10 秒最多 8 条,展示 1.5–10 秒。版本源上限 100 项;文件列表 SVG 由宿主清理。