# EdgeEver 插件开发指南
[English](plugin-development.md)
EdgeEver 提供客户端插件与无代码主题扩展能力。用户可从官方市场、公开 GitHub 仓库或 Manifest 地址安装扩展。已安装扩展随当前工作区在 Web 与桌面端之间同步,在应用打开期间运行;Android 与 iOS 原生端暂不运行插件。
> [!NOTE]
> **安全模型**:客户端插件采用受信任代码模型(类似 Obsidian)。启用插件即代表信任其在客户端 JavaScript 环境中运行并调用插件 API。清单中的能力声明(Permissions)用于向用户披露插件涉及的功能范围,不构成强制安全沙箱。主题包为纯声明式 JSON,不执行任何 JavaScript。
---
## 目录
- [插件清单(Manifest)](#插件清单manifest)
- [字段说明](#字段说明)
- [可用权限标识](#可用权限标识)
- [插件入口与生命周期](#插件入口与生命周期)
- [API 参考](#api-参考)
- [API 命名空间速查](#api-命名空间速查)
- [1. 笔记与笔记本(Notes & Notebooks)](#1-笔记与笔记本notes--notebooks)
- [2. 模板管理(Templates)](#2-模板管理templates)
- [3. 资源附件(Resources)](#3-资源附件resources)
- [4. 编辑器交互(Editor & Embeds)](#4-编辑器交互editor--embeds)
- [5. 界面交互(UI, Commands & Panels)](#5-界面交互ui-commands--panels)
- [6. 统一设置(Settings)](#6-统一设置settings)
- [7. 数据存储与安全凭据(Storage & Secrets)](#7-数据存储与安全凭据storage--secrets)
- [8. 网络请求(Network)](#8-网络请求network)
- [9. 定时任务(Schedules,仅桌面端)](#9-定时任务schedules仅桌面端)
- [10. 事件监听(Events)](#10-事件监听events)
- [11. 人工智能(AI)](#11-人工智能ai)
- [主题扩展(Theme Manifest)](#主题扩展theme-manifest)
- [打包、分发与本地开发](#打包分发与本地开发)
- [打包规范](#打包规范)
- [GitHub 分发](#github-分发)
- [本地测试与调试](#本地测试与调试)
---
## 插件清单(Manifest)
每个插件必须在根目录提供 `manifest.json`:
```json
{
"type": "plugin",
"id": "com.example.recent-notes",
"name": "Recent Notes",
"version": "1.0.0",
"apiVersion": "2",
"settingsUi": "host",
"description": "查看最近更新的笔记。",
"locales": {
"zh-CN": {
"name": "最近笔记",
"description": "查看最近更新的笔记。"
}
},
"entry": "./main.js",
"platforms": ["web", "desktop"],
"permissions": ["notes:read", "editor:read", "ui:commands", "ui:notices", "ui:panels"]
}
```
### 字段说明
| 字段 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `type` | `"plugin"` | 是 | 固定为 `"plugin"`。 |
| `id` | `string` | 是 | 插件全局唯一标识,推荐反向域名格式(如 `com.example.plugin`)。 |
| `name` | `string` | 是 | 插件默认名称。 |
| `version` | `string` | 是 | 符合 SemVer 的版本号(如 `1.0.0`)。 |
| `apiVersion` | `"2"` | 是 | 插件 API 版本,当前固定为 `"2"`。 |
| `settingsUi` | `"host"` | 是 | 设置界面渲染方式,当前固定为 `"host"`(由宿主统一渲染)。 |
| `entry` | `string` | 是 | 入口脚本路径,GitHub 分发时固定为 `./main.js`。 |
| `description` | `string` | 否 | 插件默认描述。 |
| `locales` | `object` | 否 | 多语言本地化元数据,以 BCP 47 语言标签为键(如 `zh-CN`, `en-US`),覆盖 `name` 与 `description`。 |
| `platforms` | `string[]` | 否 | 支持的平台,可选 `"web"`、`"desktop"`。 |
| `permissions` | `string[]` | 否 | 能力声明列表,用于安装与更新时向用户披露。 |
| `settings` | `object` | 否 | 宿主渲染的配置项 Schema(详见下文)。 |
### 可用权限标识
声明权限用于在安装和更新界面向用户透明披露功能:
- **笔记与内容**:`notes:read`, `notes:write`, `notes:delete`, `templates:read`, `templates:write`, `metadata:read`, `metadata:write`, `resources:read`, `resources:write`
- **编辑器**:`editor:read`, `editor:write`
- **界面交互**:`ui:commands`, `ui:navigation`, `ui:notices`, `ui:panels`, `ui:embeds`
- **存储与系统**:`storage`, `secrets`, `network`, `network:public`, `schedules`
- **AI 能力**:`ai:generate`
---
## 插件入口与生命周期
插件入口文件(`main.js`)需导出一个包含 `activate` 生命周期的默认对象:
```js
export default {
activate(context) {
// 注册命令
const disposeCommand = context.commands.register({
id: "hello-world",
title: "Say Hello",
run() {
context.ui.showNotice("Hello from EdgeEver Plugin!");
}
});
// 返回清理函数(插件停用时执行)
return () => {
disposeCommand();
};
}
};
```
TypeScript 项目可使用 `@edgeever/plugin-api` 提供的辅助工具:
```ts
import { definePlugin } from "@edgeever/plugin-api";
export default definePlugin({
activate(context) {
return context.commands.register({
id: "hello-world",
title: "Say Hello",
run: () => context.ui.showNotice("Hello!")
});
}
});
```
> [!TIP]
> 注册命令、面板、事件等接口均会返回清理函数;插件停用时,宿主也会自动回收其注册的命令与监听器。
---
## API 参考
### API 命名空间速查
插件上下文对象(`context`)提供以下能力接口:
| 命名空间 | 对应权限(披露用) | 核心方法 | 职责说明 |
| :--- | :--- | :--- | :--- |
| **`context.notes`** | `notes:read`
`notes:write`
`notes:delete` | `query()`, `queryContent()`, `get()`, `create()`, `update()`, `editMarkdown()`, `delete()`, `move()`, `pin()`, `restore()`, `revisions` | 笔记查询、正文读取、乐观并发编辑与版本管理 |
| **`context.notebooks`** | `metadata:read`
`metadata:write` | `list()`, `create()`, `update()`, `delete()` | 笔记本树状层级维护与增删改查 |
| **`context.tags`** | `metadata:read`
`metadata:write` | `list()`, `rename()`, `delete()` | 标签全局查询、重命名与删除 |
| **`context.templates`** | `templates:read`
`templates:write` | `list()`, `create()`, `update()`, `delete()`, `use()` | 模板增删改查及一键套用创建笔记 |
| **`context.resources`** | `resources:read`
`resources:write` | `list()`, `read()`, `upload()`, `update()`, `rename()`, `delete()` | 笔记图片及文件附件上传、下载、修改与管理 |
| **`context.editor`** | `editor:read`
`editor:write`
`ui:embeds` | `getSelection()`, `replaceSelection()`, `insertAtCursor()`, `getDocument()`, `editMarkdown()`, `insertEmbed()`, `embeds.register()` | 活动编辑器交互、实时文档修改与自定义块级 Embed |
| **`context.ui`** | `ui:navigation`
`ui:notices`
`ui:panels` | `showNotice()`, `openNote()`, `panels.register()`, `panels.open()` | 宿主全局提示、笔记精准跳转与自定义 DOM 面板 |
| **`context.commands`** | `ui:commands` | `register()` | 注册全局或编辑器命令(命令面板、工具栏快捷菜单) |
| **`context.settings`** | *(无)* | `get()`, `set()`, `remove()` | 读取与修改插件自身的宿主统一配置项 |
| **`context.storage`** | `storage` | `get()`, `set()`, `remove()` | 本地轻量键值存储(游标、偏好缓存,按插件与工作区隔离) |
| **`context.secrets`** | `secrets` | `get()`, `set()`, `remove()` | 本地加密的安全凭据存储(API Key、Token 等敏感数据) |
| **`context.network`** | `network`
`network:public` | `fetch(url, options)` | 网络请求(标准请求或跨域免凭据公开只读) |
| **`context.schedules`** | `schedules` | `upsert()`, `list()`, `remove()` | 桌面端持久化定时任务调度(应用开启期间运行) |
| **`context.events`** | *(无)* | `on(event, handler)` | 订阅工作区笔记、标签、模板及同步完成事件 |
| **`context.ai`** | `ai:generate` | `status()`, `generate()`, `transcribeResource()`, `transcribeMedia()` | 调用工作区配置的 AI 模型生成文本或转录音视频媒体 |
---
### 1. 笔记与笔记本(Notes & Notebooks)
#### 查询笔记
```ts
// 1. 查询轻量摘要列表(用于列表、侧边栏等,每页最多 200 条)
const result = await context.notes.query({
notebookId: "optional-notebook-id",
text: "关键词",
tags: ["todo"],
sort: "updated-desc", // "updated-desc" | "created-desc" | "title-asc"
limit: 50,
offset: 0
});
// 返回 { notes: PluginNoteSummary[], totalCount: number, nextOffset: number | null }
// 2. 查询包含完整 Markdown 内容的笔记(用于 Linter、索引扫描等)
const fullNotes = await context.notes.queryContent({
tags: ["task"],
limit: 20
});
```
#### 增删改查
```ts
// 获取单篇笔记详情
const note = await context.notes.get(noteId);
// 创建笔记
const newNote = await context.notes.create({
notebookId: "target-notebook-id",
title: "新笔记",
contentMarkdown: "# 标题\n正文内容",
tags: ["tag1"]
});
// 更新整篇笔记
await context.notes.update(noteId, {
title: "更新后的标题",
contentMarkdown: "新的正文",
tags: ["updated"]
});
// 局部并发安全编辑(基于 revision 与 contentHash 进行乐观锁校验)
await context.notes.editMarkdown(noteId, {
expectedRevision: note.revision,
expectedContentHash: note.contentHash,
edits: [
{ from: 0, to: 0, insert: "> 插入顶部引用\n\n" } // UTF-16 偏移量,半开区间 [from, to)
]
});
// 移动、置顶与删除
await context.notes.move([noteId], targetNotebookId);
await context.notes.pin([noteId], true);
await context.notes.delete(noteId, { permanent: false }); // 移入回收站
await context.notes.restore(noteId); // 从回收站恢复
// 历史版本
const revisions = await context.notes.revisions.list(noteId);
await context.notes.revisions.restore(noteId, revisionId);
```
#### 笔记本与标签
```ts
// 笔记本
const notebooks = await context.notebooks.list();
const nb = await context.notebooks.create({ name: "新建笔记本", parentId: null });
await context.notebooks.update(nb.id, { name: "新名称" });
await context.notebooks.delete(nb.id);
// 标签
const tags = await context.tags.list();
await context.tags.rename("old-tag", "new-tag");
await context.tags.delete("unused-tag");
```
---
### 2. 模板管理(Templates)
操作工作区内的公共模板或一键套用模板创建新笔记:
```ts
// 查询所有可用模板
const templates = await context.templates.list();
// 创建模板(可直接提供内容,或基于已有 noteId 生成)
const template = await context.templates.create({
name: "会议纪要",
description: "日常项目例会模板",
contentMarkdown: "## 参会人员\n\n## 议题\n\n## 待办行动项\n",
tags: ["meeting"]
});
// 更新模板
await context.templates.update(template.id, {
name: "周会纪要",
description: "每周团队例会"
});
// 套用模板在指定笔记本下创建新笔记
const note = await context.templates.use(template.id, targetNotebookId);
// 删除模板
await context.templates.delete(template.id);
```
---
### 3. 资源附件(Resources)
操作关联到笔记的媒体或文件附件:
```ts
// 获取笔记关联的所有附件
const resources = await context.resources.list(noteId);
// 读取附件 Blob
const blob = await context.resources.read(resourceId);
// 上传新附件
const uploaded = await context.resources.upload(noteId, file);
// 乐观并发更新附件内容(单文件上限 100 MiB)
await context.resources.update(resourceId, {
file: newFile,
expectedContentHash: currentResource.contentHash
});
// 重命名与删除
await context.resources.rename(resourceId, "document.pdf");
await context.resources.delete(resourceId);
```
---
### 4. 编辑器交互(Editor & Embeds)
用于在用户处于编辑状态时操作当前文档:
```ts
// 选区读取与替换
const selection = await context.editor.getSelection();
if (selection && !selection.empty) {
await context.editor.replaceSelection(selection.text.toUpperCase());
}
// 在当前光标处插入内容
await context.editor.insertAtCursor("- [ ] 新建任务\n");
// 实时编辑当前正在编辑的文档(不会丢失未保存的内容)
const doc = await context.editor.getDocument();
if (doc) {
await context.editor.editMarkdown([
{ from: 0, to: 0, insert: "\n" }
]);
}
```
#### 自定义块级 Embed
插件可注册自定义块级渲染器并插入到文档中(保存为通用的 `edgeever-plugin-embed` 块):
```ts
// 1. 注册 Embed 渲染器
context.editor.embeds.register({
type: "my-diagram",
async mount(container, embed) {
const data = embed.data;
container.innerHTML = `