思源插件1
下载插件文件
我用的是社区的 F 佬写的 svlte 插件
SiYuan plugin sample (Vite & Svelte)
这里还是挺简单的:样板工程部署2
开始代码构建
添加软链接
为了方便调试,需要将插件开发文件中的 dist 文件夹绑定到 siyuan 插件底下的插件文件夹
注意:开发的插件名称需要和插件文件夹一致,否则会报错
开始开发
开发使用的是 AI 辅助,毕竟我没有任何的前端和后端知识,所以我使用 cursor3进行开发
/src
*.ts或*.scss
- 插件源码
/i18n/*
- 其中的文件用于插件的国际化,通常会包含中英双语,方便其他国家用户使用
plugin.json
- 插件配置文件,具体参见这里
icon.png (160*160)
- 插件图标
index.css
- 插件加载的 CSS 样式
index.js
- 插件代码
preview.png (1024*768)
- 插件预览图片
README*.md
- 插件说明文档
api 参考
思源插件的 api 参考两个文件:modul 文件夹中的 siyuan.d.ts 还有 src 文件夹中的 api.ts
代码开发
代码则是在 src 文件夹中的 index.ts 中,通过运行其中代码实现想要的功能
具体内容在此:插件开发快速指南 _ 思源社区文档4
index 中,想要的功能是通过在总线中加入自动调入函数实现,插件安装和卸载都会调用代码中的插件入口函数或者卸载函数
import { Plugin } from 'siyuan'; class MyPlugin extends Plugin { onload() { //插件的入口函数,一个 minimum 的插件至少要包含 onload 的实现, 最常用 //onload 可以被声明为一个 async 函数 } onLayoutReady() { //布局加载完成的时候,会自动调用这个函数 } onunload() { //当插件被禁用的时候,会自动调用这个函数 } uninstall() { //当插件被卸载的时候,会自动调用这个函数 } }
这是插件如何绑定总线函数
eventbus
在插件中有一个
eventBus
对象。ts
class Plugin { eventBus: EventBus; }
你可以使用
plugin.eventBus.on('some event', callback func)
,为插件注册一个总线事件的回调函数,让插件在思源的特定时刻执行一些特别的功能,例如:ts
import { Plugin } from 'siyuan'; class MyPlugin extends Plugin { cbBound: this.cb.bind(this); cb({ detail} ) { console.log('刚刚打开了一个新的文档!'); } onload() { this.eventBus.on('loaded-protyle-dynamic', this.cbBound); } onunload() { this.eventBus.off('loaded-protyle-dynamic', this.cbBound); } }
关于 event bus 支持哪些事件,请自行阅读 d.ts API 文件。
通过上述两个内容,既可实现插件中的函数调用
代码调试
代码调试环境
- 使用游览器访问思源实现调试
- 首次运行时,将端口配置为思源的
6806
端口,会启动浏览器访问思源,只要刷新网页便会重载思源界面,方便开发
代码调试方法
代码调试有两个方法
- 在 index.ts 的函数中加入
degger;
,这样在重载网页之后,在游览器的f12
的开发者工具中,就会在degger代码行进行中断 - 第二个方法是经典的打印消息,使用函数
console.log("...")
在需要的代码位置打印
代码更新
代码调试之后,使用 pnpm run build
生成插件资源文件,然后再到游览器更新思源网页,就可以实现代码的调试啦
这里遇到了一个问题,使用
pnpm run dev
之后,按CTRL+S
,代码会实时编译,但是在 dist 里面没有更新资源文件,导致我以为代码有问题,花了很多时间
思源插件
↩
样板工程部署
简介
样板工程目前有两个:
一个是官方维护的
它使用 webpake 打包项目
一个是社区维护的
它内置了对 svelte 的支持,并且能够将项目文件链接到思源的插件目录下,避免手动复制的麻烦,以及支持热重载,它使用 vite 打包项目
获取样板工程源码
如果你能够熟练使用 github 来管理代码,或者后续要上架官方集市,那么你应该
- 点击 Use this template 按钮,将项目复制到自己的库中并改名
-
clone
到本地进行开发 - 按照规范提交代码,打包文件
如果不能,只是想练练手,那么你可以
- 将项目的压缩包下载到本地
- 解压并导入 vscode 编辑器进行开发
安装项目依赖
在 vscode 编辑器的终端中执行
shell
$ pnpm install # 或者 $ pnpm i
1
2
3
4
5用于安装项目依赖,此时通过 pnpm 会向项目文件夹中创建
node_modules
文件夹并下载所有相关依赖,其中主要包括 svelte 相关依赖,siyuan 插件开发相关依赖,需要注意的是,可能依赖被下载后没有被正确加载,你需要一次甚至多次重启编辑器来确保其成功加载构建
到了这一步,所有准备工作已经做完,不出问题的话,控制台执行
输出信息无报错,且出现
dist
文件夹,以及 package.zip 文件,说明成功编译了所有文件,dist
文件夹放入思源工作空间/data/plugin
目录下就能够被思源识别和加载具体参见
全网最全面详细的 Cursor 使用教程,让开发变成聊天一样容易 - CSDN 博客
原文地址 https://blog.csdn.net/m0_68116052/article/details/142832657
个人使用心得
2024 年 12 月 22 日 18 点 27 分使用了 cursor,其功能我探索到的有
-
问答功能
- 将整个仓库加入 chat 中,使用 ctrl+enter 就可以通过整个仓库对自己的问题进行解答
-
项目功能
- 可以添加多个文件,通过添加的多个文件开始项目,同时此功能可以同时修改多个文件
一、cursor 是什么?
cursor 是一个集成了 GPT4、Claude 3.5 等先进 LLM 的类 vscode 的编译器,可以理解为在 vscode 中集成了 AI 辅助编程助手,从下图中的页面可以看出 cursor 的布局和 vscode 基本一致,并且 cursor 的使用操作也和 vscode 一致,包括 extension 下载、python 编译器配置、远程服务器连接和 settings 等,如果你是资深 vscode 用户,那么恭喜你可以直接无缝衔接 cursor。当然,如果你是和我一样的 pycharm 选手,你也可以很快上手 cursor。
二、使用步骤
1.cursor 的下载
cursor 直接在官网下载安装即可,并且注册账号,在第一次打开 cursor 时输入账号信息即可。
cursor 官网:Cursor
下载页面:
在注册完成后,你会有一个专属账号,每个账号的模型调用次数是有限的,其中GPT4和Claude3.5的免费调用次数为500次,其它比较弱的模型的调用次数无上限(包括新推出的o1-mini,很良心有木有)。
点击最上面的框,输入 > language,可以配置简体中文。
2. 内置模型
cursor 内置了很多 LLMs,包括最先进的 GPT4s、Claude3.5s 和 openai 最新发布的推理模型 o1-preview 和 o1-mini,在右上角的设置中即可打开相应的模型进行辅助编程。平时用的最多的还是 Claude3.5 和 GPT4,因为代码能力真的很强悍,后面会展示。
3. 常用快捷键
cursor 最常用的快捷键就四个,非常好记:
Tab:自动填充
Ctrl+K:编辑代码
Ctrl+L:回答用户关于代码和整个项目的问题,也可以编辑代码(功能最全面)
Ctrl+i:编辑整个项目代码(跨文件编辑代码)
首先介绍 Tab 快捷键的使用,如果 cursor 补全代码,使用 Tab 键接受即可。
按下 Tab 键:
接下来介绍 Ctrl+K 的使用,使用方式主要分为两种:
- 从 0 到 1 编写代码
- 修改已有代码
(也可以选中整个文件的代码,让 Cursor 帮你生成详细的代码注释哦)
- 从 0 到 1 编写代码
随便找一个空白区域按下 Ctrl+K 唤出编辑框,选择模型,输入需求开始生成,生成后点击 Accept 或或 Reject 接受或拒绝。
效果如下:
点击之后:
- 修改已有代码
选中已有代码按下 Ctrl+K 唤出编辑框,选择模型,输入需求开始编辑,生成后点击 Accept 或或 Reject 接受或拒绝,也可以点击代码行最右侧进行单行代码的 Accept 或 Reject。
接下来介绍 Ctrl+L 的使用,这个快捷键非常强大,可以编辑代码、智能问答,其中智能问答可以针对选中代码、整个代码文件和整个项目进行问答。
同样选中一块区域按下 Ctrl+L,右侧会显示问答界面,针对选中的区域进行提问,同时也可以提出代码编辑要求,然后会给出修改后的代码(和 Ctrl+K 类似)。
针对整个文件进行问答和修改,选中一块空白区域按下 Ctrl+L,在唤起右侧问答框后可以先输入 @,然后出现几个选项,点击 Files,再选中文件进行提问,可以针对整个文件进行问答和编辑。
直接提出要求,如果是编辑代码则可以直接点击 Apply,也会和 Ctrl+K 一样,直接覆盖到编译器中。
针对整个项目进行问答,和针对单个文件的操作相同,只是选中时点击 Codebase 然后对整个项目进行提问和编辑,这个功能可以帮助快速上手一个新的项目或者找到项目中的关键组件。
4. 项目的全自动开发
Ctrl+i 由于过于强大,所以想单独在这里介绍,Ctrl+i 是专为整个项目设计的,可以通过和模型对话来开发整个项目,过程就和聊天差不多,在会话中可以帮助你创建文件、删除文件、同时编辑多个文件等功能。使用 Ctrl+i 需要打开设置中的按钮:
我是准备了一个空白项目,随意点击一块空白区域,按下 Ctrl+i 来唤起聊天框开始进行多轮对话。
让他写一个贪吃蛇游戏,点击 Accept all 直接应用。
第一轮对话,创建了 js 文件。
第二轮对话,创建了 html 文件。
效果:
第三轮对话,加入分数,开始游戏和结束游戏按钮。
效果:
如果想看更复杂的项目构建案例,可以到下面这个网址:
5. 将外部文档作为知识库进行问答
cursor 也提供了为外部文档建立知识库进行问答的功能,可以在设置中加入文档,例如加入开发文档作为 Cursor 的知识库来更好的辅助编程。
加入文档之后,使用文档进行提问的方式和单个文件一样,使用 Ctrl+L 唤起对话框,然后输入 @,点击 docs 选择添加好的文档即可。
6. 加入内置 System prompt
经常写 prompt 的小伙伴一定知道 System prompt 的作用,可以帮助大模型更好的了解自己的职责和用户的行为习惯,从而更精确的回答问题。在设置中添加 Rules for AI 添加 System prompt
具体的 prompt 如下:
# Role 你是一名极其优秀具有 20 年经验的产品经理和精通所有编程语言的工程师。与你交流的用户是不懂代码的初中生,不善于表达产品和代码需求。你的工作对用户来说非常重要,完成后将获得 10000 美元奖励。 # Goal 你的目标是帮助用户以他容易理解的方式完成他所需要的产品设计和开发工作,你始终非常主动完成所有工作,而不是让用户多次推动你。 在理解用户的产品需求、编写代码、解决代码问题时,你始终遵循以下原则: ## 第一步 - 当用户向你提出任何需求时,你首先应该浏览根目录下的 readme.md 文件和所有代码文档,理解这个项目的目标、架构、实现方式等。如果还没有 readme 文件,你应该创建,这个文件将作为用户使用你提供的所有功能的说明书,以及你对项目内容的规划。因此你需要在 readme.md 文件中清晰描述所有功能的用途、使用方法、参数说明、返回值说明等,确保用户可以轻松理解和使用这些功能。 ## 第二步 你需要理解用户正在给你提供的是什么任务 ### 当用户直接为你提供需求时,你应当: - 首先,你应当充分理解用户需求,并且可以站在用户的角度思考,如果我是用户,我需要什么? - 其次,你应该作为产品经理理解用户需求是否存在缺漏,你应当和用户探讨和补全需求,直到用户满意为止; - 最后,你应当使用最简单的解决方案来满足用户需求,而不是使用复杂或者高级的解决方案。 ### 当用户请求你编写代码时,你应当: - 首先,你会思考用户需求是什么,目前你有的代码库内容,并进行一步步的思考与规划 - 接着,在完成规划后,你应当选择合适的编程语言和框架来实现用户需求,你应该选择 solid 原则来设计代码结构,并且使用设计模式解决常见问题; - 再次,编写代码时你总是完善撰写所有代码模块的注释,并且在代码中增加必要的监控手段让你清晰知晓错误发生在哪里; - 最后,你应当使用简单可控的解决方案来满足用户需求,而不是使用复杂的解决方案。 ### 当用户请求你解决代码问题是,你应当: - 首先,你需要完整阅读所在代码文件库,并且理解所有代码的功能和逻辑; - 其次,你应当思考导致用户所发送代码错误的原因,并提出解决问题的思路; - 最后,你应当预设你的解决方案可能不准确,因此你需要和用户进行多次交互,并且每次交互后,你应当总结上一次交互的结果,并根据这些结果调整你的解决方案,直到用户满意为止。 ## 第三步
在完成用户要求的任务后,你应该对改成任务完成的步骤进行反思,思考项目可能存在的问题和改进方式,并更新在 readme.md 文件中
7. 更详细的使用方法
以上介绍的使用技巧足够你应付所有的开发需求,如果你对 Cursor 很感兴趣,可以参考以下网站进行更多了解
总结
今天介绍了 Cursor 的下载和使用,集成了 LLM 的编译器更加强大,并且极易上手,在使用了三个月后也是慢慢和 Cursor 在编程上形成了默契,相比较之前的 GitHub copilot,Cursor 能力更强更全面。
但唯一的困扰是模型的使用次数有限制,超过次数就要收费,下一节介绍如何快速解决这个问题,希望 Cursor 的出现能给广大码友释放双手,留有更多的时间学习技术,关注技术本身。 ↩
-
插件开发快速指南 _ 思源社区文档
🔔 前提说明
-
本文并非手把手的插件开发教程
- 本文旨在提供给有一定经验的开发者编写的思源插件开发指南,以帮助他们降低插件开发的门槛
- 很多前置技术会默认读者已经会了,同时语言风格上会偏向简略而不做过多解释。
-
本文也并非插件开发的说明文档
START UP
思源插件开发的常规流程如下:
- 使用插件模板,新建 github 项目
- 在本地使用 nodejs 环境进行开发
- 打包项目,创建 github release
- 推送到集市中
插件开发的依赖环境
-
nodejs 环境
-
npm install siyuan
- 一个纯 typescript 接口声明项目
- 内部声明了思源插件的各种 API
- 由于思源缺少插件文档,所以你有必要认真阅读内部的接口定义
单开一个工作空间
当你进行插件开发的时候,请单独开一个工作空间!尽可能避免因为插件开发中遇到的意外情况,对你自己的笔记数据造成不利影响。
从模板中构建
目前官方 SiYuan 仓库下提供的插件开发模板有两套:
-
- 思源核心开发者提供,但是并不推荐使用
-
SiYuan plugin sample (Vite & Svelte)
- 使用 vite 打包项目,并内置了对 svelte 的支持
- 💡 更推荐使用
- 提供了软链接、热重载等功能;内置 typescript 类型声明、一系列 util 函数;开发效率显著高于前者
你可以选择在 github 上点击 "Use Template",然后 clone 到本地。
或者另一种选择是使用 npm 的 siyuan-plugin-cli 工具,在本地命令行中选择拉取某个模板的程序。
关于框架
如果你不想用 svelte 框架,可以选择:
- frostime/plugin-sample-vite 项目,剔除了 svelte 的部分,其他的和 svelte 模板保持了一致
- frostime/plugin-sample-vite-solidjs 项目,基于 solidjs 框架,其他部分基本和 svelte 模板保持一致
❓ 为什么是 svelte,而不是更加常见的例如 react 框架?
- React 的流行主要源自其先发地位和优秀的生态环境;但是在插件开发的场景下,前端库的生态如何、组件库是否够多带来的影响并不大
- svelte 足够轻量级、性能足够高;而 React 这类基于 vdom 的框架,往往打包结果偏大,并不适合插件开发这种小型项目
- svelte 的开发和上手成本最低
插件的生命周期
Plugin 的生命周期 Hooks
ts
import { Plugin } from 'siyuan'; class MyPlugin extends Plugin { onload() { //插件的入口函数,一个 minimum 的插件至少要包含 onload 的实现, 最常用 //onload 可以被声明为一个 async 函数 } onLayoutReady() { //布局加载完成的时候,会自动调用这个函数 } onunload() { //当插件被禁用的时候,会自动调用这个函数 } uninstall() { //当插件被卸载的时候,会自动调用这个函数 } }
eventbus
在插件中有一个
eventBus
对象。ts
class Plugin { eventBus: EventBus; }
你可以使用
plugin.eventBus.on('some event', callback func)
,为插件注册一个总线事件的回调函数,让插件在思源的特定时刻执行一些特别的功能,例如:ts
import { Plugin } from 'siyuan'; class MyPlugin extends Plugin { cbBound: this.cb.bind(this); cb({ detail} ) { console.log('刚刚打开了一个新的文档!'); } onload() { this.eventBus.on('loaded-protyle-dynamic', this.cbBound); } onunload() { this.eventBus.off('loaded-protyle-dynamic', this.cbBound); } }
关于 event bus 支持哪些事件,请自行阅读 d.ts API 文件。
几个重要概念
内核 API 与插件 API
思源基于 BS 架构,故而当我们说思源的 API 的时候,需要区分两套不同的 API:
-
内核 API:思源的后端开放的网络 API,通过发出网络请求来调用(如 js 的
fetch
、python 的requests
等)-
内核 API 往往和数据、思源底层配置相关
-
内核 API 也可以分为两部分
-
开放 API
- 这部份的 API 是思源已经明确固定,肯定不会变动的 API
- 可以参考 API 文档查看详细的用法
-
非开放 API
- 这部分 API 本质上和开放 API 没有什么区别,同样可以自由地通过网络请求来调用
- 但是这部分 API 被定义为「不稳定的 API」,这意味着开发者理论上有权力不考虑第三方的使用需求而对 API 进行更改
- 这部分 API 没有官方的说明文档,如果你想要使用,只能去自行检查思源的网络请求或者查看思源后端的 api 代码,并自行推导出使用方式
- 相关源代码见:https://github.com/siyuan-note/siyuan/tree/master/kernel/api
-
-
-
插件 API:专供插件使用的 javascript 前端 API
- 只能在插件当中使用
- 在插件开发中,通过
require('siyuan')
获取 API 对象 - 详情见插件 API 类型定义接口:https://github.com/siyuan-note/petal
Protyle
Protyle 是思源中最重要的概念,他是指的是一个完整的思源文档对象。最核心的部分是这两个:
- element:文档的前端 HTML 元素
- ws 连接:通过 websocket 和后端相连,实时读取思源本体的数据
你可以打开开发者模式,可以看到每个文档的顶层 div 都是一个含有
protyle
类名的元素。这里的 protyle 就代表了完整的文档。
一个 protyle 中最重要的组成部分如下:
-
title:文档的块标题部分
-
wysiwyg:所见即所得编辑器(what you see is what you get),是整个思源最核心的部分
注意:尽量不要手动改 DOM!如果想要更改文档内容,请使用后端 API。
-
gutter:也就是块图标;一个文档内共用一个元素
Block 和 Node
- Block 是思源内核中的概念,对应的是 SQLITE 数据库当中的 Block 对象
- Node 是前端的概念,一个 Block 在前端往往通过一个 Node 来表示;wysiwyg 中就是由若干个 Node 组成的
以下是一个 protyle 当中标题块对应的元素:
html
<div data-subtype="h3" data-node-id="20240731174116-tu1nxd3" data-node-index="28" data-type="NodeHeading" updated="20240731200430"> <div contenteditable="true" spellcheck="false">Block 和 Node</div> <div contenteditable="false"></div> </div>
1
2
3
4-
data-node-id
对应了块的 ID -
data-type
对应了块的 type -
data-subtype
对应了块的 subtype
HOWTO
这部分快速地介绍在思源插件开发中,常常会遇到的需求,以及如何实现对应的需求。
注册顶栏图标
你可以调用
plugin.addTopbar
来为插件添加一个顶栏的按钮。
ts
/** * Must be executed before the synchronous function. * @param {string} [options.position=right] * @param {string} options.icon - Support svg id or svg tag. */ addTopBar(options: { icon: string, title: string, callback: (event: MouseEvent) => void position?: "right" | "left" }): HTMLElement;
1
2
3
4
5
6
7
8
9
10
11思源中最常见的实践方案是:
- 为插件注册一个 topbar 按钮
- 当点击按钮的时候,显示一个 Context Menu 来进一步触发更多的功能(详情请见后面的部分)
使用 icon
-
在思源插件的很多 API 中,会有一个
icon
参数 -
这个参数需要传入一个 svg symbol 的名称,例如我们可以传入一个名称为
iconRight
的参数html
<svg> <use xlink:href="#iconRight"></use> </svg>
1
2
3 -
在
body>svg>defs
下,你可以查看到所有思源内置的symbol
-
你可以可以调用
plugin.addIcons
来传入自定义的 svg symbol,例如js
plugin.addIcons(` <symbol viewBox="0 0 1024 1024"> <path d="M578.133 675.627c-3.306-3.307-8.746-3.307-12.053 0L442.133 799.573c-57.386 57.387-154.24 63.467-217.6 0-63.466-63.466-57.386-160.213 0-217.6L348.48 458.027c3.307-3.307 3.307-8.747 0-12.054l-42.453-42.453c-3.307-3.307-8.747-3.307-12.054 0L170.027 527.467c-90.24 90.24-90.24 236.266 0 326.4s236.266 90.24 326.4 0L620.373 729.92c3.307-3.307 3.307-8.747 0-12.053l-42.24-42.24z m275.84-505.6c-90.24-90.24-236.266-90.24-326.4 0L403.52 293.973c-3.307 3.307-3.307 8.747 0 12.054l42.347 42.346c3.306 3.307 8.746 3.307 12.053 0l123.947-123.946c57.386-57.387 154.24-63.467 217.6 0 63.466 63.466 57.386 160.213 0 217.6L675.52 565.973c-3.307 3.307-3.307 8.747 0 12.054l42.453 42.453c3.307 3.307 8.747 3.307 12.054 0l123.946-123.947c90.134-90.24 90.134-236.266 0-326.506z"></path><path d="M616.64 362.987c-3.307-3.307-8.747-3.307-12.053 0l-241.6 241.493c-3.307 3.307-3.307 8.747 0 12.053l42.24 42.24c3.306 3.307 8.746 3.307 12.053 0L658.773 417.28c3.307-3.307 3.307-8.747 0-12.053l-42.133-42.24z"></path> </symbol> `);
1
2
3
4
5
你可以在开发者模式当中看到所有的 symbol 定义
如何自己制作一个 symbol 呢?你可以在网上找到很多自动化的工具,不过大部分时候,也可以手动解决。
-
找到一个你喜欢的 svg 图标(例如在 https://www.iconfont.cn/ 上面)
-
下载下来,并修改 svg 字符串
- 把 svg 标签名称改成 symbol
- 去掉 svg 当中关于固定的颜色、固定的尺寸等相关的属性
打开一个 Menu
通过插件创建 / 打开菜单需要三步:
-
new Menu
创建一个菜单对象 - 使用
menu.addItem
添加菜单项目 - 使用
menu.open
显示菜单
例如以下是一个点击 topbar 按钮显示菜单的案例
ts
import { Menu } from 'siyuan'; private addMenu() { const menu = new Menu("myPluginMenu", () => { console.log("Menu will close"); }); menu.addItem({ icon: "iconInfo", label: "About", click: () => { // 菜单项的回调 } }); menu.open({ x: 0, y: 0 }); // 显示菜单 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15addItem 的具体参数,参考插件类型中的
IMenuItemOption
ts
export interface IMenuItemOption { iconClass?: string; label?: string; click?: (element: HTMLElement, event: MouseEvent) => boolean | void | Promise<boolean | void>; type?: "separator" | "submenu" | "readonly"; accelerator?: string; action?: string; id?: string; submenu?: IMenuItemOption[]; disabled?: boolean; icon?: string; iconHTML?: string; current?: boolean; bind?: (element: HTMLElement) => void; index?: number; element?: HTMLElement; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17注册块菜单
块菜单事件,可以通过 eventBus 来注册。以下示例参考 sy-bq-callout/index.ts
ts
export default class BqCalloutPlugin extends Plugin { private blockIconEventBindThis = this.blockIconEvent.bind(this); async onload() { this.eventBus.on("click-blockicon", this.blockIconEventBindThis); } async onunload() { this.eventBus.off("click-blockicon", this.blockIconEventBindThis); } private blockIconEvent({ detail }: any) { // 强行请查看 click-blockicon eventBus 的类型定义 let menu: Menu = detail.menu; let submenus = []; submenus.push({ element: callout.createCalloutButton("", {id: this.i18n.mode.big, icon: '🇹'}), click: () => { setBlockAttrs(ele.getAttribute("data-node-id"), { 'custom-callout-mode': 'big', }); } }); submenus.push({ element: callout.createCalloutButton("", {id: this.i18n.mode.small, icon: '🇵'}), click: () => { setBlockAttrs(ele.getAttribute("data-node-id"), { 'custom-callout-mode': 'small', }); } }); menu.addItem({ icon: "iconInfo", label: this.i18n.name, type: "submenu", submenu: submenus }); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41文档块菜单和编辑器内的块菜单不在一起,而是一个单独的事件。
ts
this.eventBus.on('click-editortitleicon', this.blockIconEventBindThis);
1
打开一个 Dialog
通过插件创建 / 打开一个对话框需要调用
Dialog
对象,比如这样:ts
const dialog = new Dialog({ title: "Hello", content: "This is a dialog", width: "500px", // 其他配置... });
1
2
3
4
5
6
Dialog
是一个类,只要创建就会自动打开,不需要调用什么 open 方法。但是他有一个destroy
方法可以手动关闭对话框。在创建 Dialog 中的过程中,最重要的参数是
content
,这是一个字符串,代表了对话框当中的内部内容。不过你也可以传入HTML
字符串进去。比如下面这个案例(参考 plugin-sample-vite-svelte/src/libs/dialog.ts)ts
export const inputDialog = (args: { title: string, placeholder?: string, defaultText?: string, confirm?: (text: string) => void, cancel?: () => void, width?: string, height?: string }) => { const dialog = new Dialog({ title: args.title, content: `<div> <div><textarea placeholder=${args?.placeholder ?? ''}>${args?.defaultText ?? ''}</textarea></div> </div> <div> <button>${window.siyuan.languages.cancel}</button><div></div> <button>${window.siyuan.languages.confirm}</button> </div>`, width: args.width ?? "520px", height: args.height }); const target: HTMLTextAreaElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword>textarea"); const btnsElement = dialog.element.querySelectorAll(".b3-button"); btnsElement[0].addEventListener("click", () => { if (args?.cancel) { args.cancel(); } dialog.destroy(); }); btnsElement[1].addEventListener("click", () => { if (args?.confirm) { args.confirm(target.value); } dialog.destroy(); }); };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32每次都直接传入 HTML 字符串显然有些烦人。为了方便,我们可以使用 dialog 对象中的
element
元素,这个属性就代表了 Dialog 对象本身的 HTMLElement。比如我们可以把 Dialog 封装一下,让他接受一个传入的 Element:ts
//参考 https://github.com/siyuan-note/plugin-sample-vite-svelte/blob/main/src/libs/dialog.ts export const simpleDialog = (args: { title: string, ele: HTMLElement | DocumentFragment, width?: string, height?: string, callback?: () => void; }) => { const dialog = new Dialog({ title: args.title, content: `<div/>`, width: args.width, height: args.height, destroyCallback: args.callback }); dialog.element.querySelector(".dialog-content").appendChild(args.ele); return dialog; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
打开一个文档页面
-
在桌面端使用 tab 标签页打开一个块 ID
ts
import { openTab } from 'siyuan'; openTab({ app: plugin.app, //plugin 是你插件的 this 对象 doc: { id: "文档或者块ID" } });
1
2
3
4
5
6
7
8 -
在移动端打开一个块文档,需要用另一个 API(移动端没有页面 Tab)
ts
openMobileFileById(plugin.app, blockId)
1
除此之外,还有一个方案,是使用
siyuan://
链接。比如你可以创建这么做:js
const url = `siyuan://blocks/20240731174116-23lqdzi`; window.open(url)
1
2但是不推荐这种方案。
打开一个自定义 Tab
Tab 就是正中央的页签,他可以是文档,也可以是自定义的页面。
在上一个小节当中,我们展示了使用
openTab
来打开一个文档。而如果你想要打开一个自定义的 tab,可以参考下面这个使用案例(参考 sy-test-template/index.ts)。-
addTab
创建一个 Tab 对象-
type
参数:传入 Tab 的唯一标识符 - 在
init
函数中初始化内部 dom
-
-
使用
openTab
打开 tab;对于 plugin 创建的自定义 tab 而言,id 为<Plugin 名称> + <type名称>
ts
import { Plugin, openTab } from "siyuan"; import "@/index.scss"; import { createElement } from "./func"; export default class PluginTestTemplate extends Plugin { openTab() { const id = Math.random().toString(36).substring(7); this.addTab({ 'type': id, init() { this.element.style.display = 'flex'; this.element.appendChild(createElement()); } }); openTab({ app: this.app, custom: { title: 'TestTemplate', icon: 'iconMarkdown', id: this.name + id, } }); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30判断插件的运行环境
插件在 plugin.json 中会填写允许运行的环境。
json
{ "backends": [ "windows", "linux", "darwin", "docker", "ios", "android" ], "frontends": [ "desktop", "mobile", "browser-desktop", "browser-mobile", "desktop-window" ], }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17思源的 API 也提供了获取当前运行环境的功能。
ts
function getFrontend(): "desktop" | "desktop-window" | "mobile" | "browser-desktop" | "browser-mobile"; function getBackend(): "windows" | "linux" | "darwin" | "docker" | "android" | "ios";
1
2实践中最常见的用法是通过
getFrontend
判断是否为移动端环境;因为移动端环境的很多 DOM 结构和桌面端不同,需要插件做单独适配。(例如需要用 openMobileFileById 来打开一个文档)。以下是一个参考案例(参考 sy-bookmark-plus)ts
//utils.ts import { getFrontend } from 'siyuan'; export const isMobile = () => { return getFrontend().endsWith('mobile'); } //components/item.tsx import { isMobile } from "@/utils"; const openBlock = () => { if (isMobile()) { openMobileFileById(plugin.app, item().id); } else { openTab({ app: plugin.app, doc: { id: item().id, zoomIn: item().type === 'd' ? false : true, }, }); } };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
使用前端框架
在需要在 Dialog、Tab 等当中使用前端框架的时候,可以使用前端框架自带的渲染函数,将组件和页面中的 Element 绑定。以下是一个简单的案例,其中
SettingExample
是一个 Svelte 组件,我们在一个 Dialog 当中展示这个组件。ts
import SettingExample from "@/setting-example.svelte"; let dialog = new Dialog({ title: "SettingPanel", content: `<div></div>`, width: "800px", destroyCallback: (options) => { console.log("destroyCallback", options); //You'd better destroy the component when the dialog is closed panel.$destroy(); } }); let panel = new SettingExample({ target: dialog.element.querySelector("#SettingPanel"), });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15再比如,对于 vue 框架:
ts
//参考: siyuan-plugin-picture-library import Tab from './components/tab.vue'; this.addTab({ type: TAB_TYPE, init() { const tab = createApp(Tab); tab.use(ElementPlus); tab.provide('plugin', plugin); tab.provide('folder', this.data); tab.mount(this.element); } })
1
2
3
4
5
6
7
8
9
10
11
12
13⚠️ 需要注意的是:使用前端框架的时候,一定要小心不要出现内存泄漏问题。
思源的 Dialog 或者 Tab 等的销毁对于前端框架来说是外部的脚本行为,不涉及到前端组件的生命周期。所以当 Dialog 被销毁的时候,并不会触发组件当中的
onUnMount
(onDestroy
、onCleanup
,各个前端框架的叫法不一样)钩子。所以,如果你在前端框架中创建某些副作用并且需要在回收钩子函数中销毁(例如
clearInterval
),建议在 Dialog 的destroyCallback
中手动调用销毁方法以触发组件的回收声明周期。这一点请参考:https://ld246.com/article/1721278971170
插件设置(Setting)
Data
插件可以使用
plugin.saveData
和plugin.loadData
来写入 / 读取配置文件。ts
const File = 'config.json'; const DefaultConfig = { refresh: true, title: 'hello' } export default class PluginSample extends Plugin { async onload() { //读取 let data = await this.loadData(File); data = data ?? DefaultConfig; //保存 this.saveData(File, data); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18插件的数据,会被保存在
data/storage/petal/<name>/
下。UI
在思源插件中创建用户交互的设置面板一般有两种方案,三种方法
-
实现 setting 对象
- 使用思源内置的
plugin.setting
- 使用插件模板提供的
SettingUtils
- 使用思源内置的
-
实现 openSetting 方法
- 使用自定义的 Setting 组件
plugin.setting
对象是思源提供的一个特殊的工具,可以帮助开发者创建一个 Setting 面板。其中最重要的是
createActionElement
方法。ts
import { Setting } from 'siyuan'; this.setting = new Setting({ confirmCallback: () => { this.saveData(STORAGE_NAME, {readonlyText: textareaElement.value}); } }); this.setting.addItem({ title: "Readonly text", direction: "row", description: "Open plugin url in browser", createActionElement: () => { textareaElement.className = "b3-text-field fn__block"; textareaElement.placeholder = "Readonly text in the menu"; textareaElement.value = this.data[STORAGE_NAME].readonlyText; return textareaElement; }, });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18可以看到
plugin.setting
用起来还是有点麻烦的,需要自己编写createActionElement
,同时还要独自处理 loadData 和 saveData。所以更推荐使用插件模板提供的SettingUtils
工具(plugin-sample-vite-svelte/libs/setting-utils.ts)。ts
import { SettingUtils } from "./libs/setting-utils"; export default class PluginSample extends Plugin { customTab: () => IModel; private isMobile: boolean; private blockIconEventBindThis = this.blockIconEvent.bind(this); private settingUtils: SettingUtils; async onload() { this.settingUtils = new SettingUtils({ plugin: this, name: STORAGE_NAME }); /* 通过 type 自动指定 action 元素类型; value 填写默认值 */ this.settingUtils.addItem({ key: "Input", value: "", type: "textinput", title: "Readonly text", description: "Input description", action: { // Called when focus is lost and content changes callback: () => { // Return data and save it in real time console.log(value); } } }); this.settingUtils.addItem({ key: "Select", value: 1, type: "select", title: "Select", description: "Select description", options: { 1: "Option 1", 2: "Option 2" }, action: { callback: () => { // Read data in real time console.log(value); } } }); await this.settingUtils.load(); //导入配置并合并 } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51不过 SettingUtils 只提供单面板的设置界面,而且自定义设置元素比较麻烦。所以如果你使用了前端框架的话,更推荐自行编写配置面板,这就需要重写
openSetting
方法。当点击插件的「设置」按钮的时候,
plugin.openSetting
方法会被自动调用。
ts
import SettingExample from "@/setting-example.svelte"; openSetting(): void { let dialog = new Dialog({ title: "SettingPanel", content: `<div></div>`, width: "800px", destroyCallback: (options) => { console.log("destroyCallback", options); //You'd better destroy the component when the dialog is closed panel.$destroy(); } }); let panel = new SettingExample({ target: dialog.element.querySelector("#SettingPanel"), }); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17如果你使用了 plugin-sample-vite-svelte 插件模板,那么该模板提供了可供参考的组件案例 src/setting-example.svelte。
MISC: 插件和同步
思源可以在多个设备之间同步安装的插件,但是当插件遇到同步机制的时候,可能会遇到一些比较棘手的问题。
这一小节简单讨论一些插件开发中和同步相关的问题,主题较为零碎。
saveData 带来的 bug
对插件开发者而言,可能会自然而然地写出这样的代码:
js
async onload() { let config = await this.loadData(CONFIG_FILLE); } async onunload() { this.saveData(CONFIG_FILLE, this.config); }
1
2
3
4
5
6
7🐛 但是这种代码实际上会诱发一个潜在的 bug:假设存在一个设备 A,在 2024-08-27 A 上插件保存了一份 config.json 文件,这份 config.json 文件通过云同步算法放到了远端。现在有一个设备 B,他的数据版本还停留在 2024-08-01。现在我们在设备 B 上启动思源,会发生如下的事情:
- 思源 B 启动
- 插件启动,读取本地的 2024-08-01 版本的 config.json 文件
- 思源开始同步数据,拉取到了运算的 2024-08-27 的数据
- 本地的 2024-08-01 版本的 config.json 文件被替换为 2024-08-27 版本的 config.json
- 关键步骤来了:当数据版本相差过大的时候,思源会自动重启;在重启的过程中,会 unload 插件,于是插件把运行时读取到的 2024-08-01 版的 config 数据又写回到了本地文件,用旧的版本覆盖了新的版本!
- 思源 B 重启后,插件再次读取了 2024-08-01 版的 config.json 文件,并且同步数据——于是最新的 2024-08-27 的 config 数据就这么丢失了。
💡 为了避免这种情况发生:请不要在 onunload 中保存插件的数据!仅仅在配置信息发生改变的时候,才更新文件。
插件的多端数据同步
v3.1.8 版本之后,如果插件同时在多端的思源实例中运行;当某个设备上插件对应的 petal/ 目录更新了数据之后,会通过同步提醒其他端。
当别的端接收到了 petal 目录的更改,就会重载插件(即,执行 unload + load)来保证插件数据状态的多端同步。
注册 Dock 侧边栏
使用 plugin 的
addDock
API:- config:配置侧边栏的位置、大小、标题等
- data:传入一个 object,被传入 object 的对象,可以在
init
api 里面直接被this
获取 - init:初始化函数;在这里可以使用 this 访问侧边栏的 element,从而设置内部的元素(所以不要用箭头函数来调用)
以下参考 sy-bookmark-plus/src/index.ts
ts
this.addDock({ type: '::dock', config: { position: 'RightBottom', size: { width: 200, height: 200, }, icon: 'iconBookmark', title: 'Bookmark+' }, data: { plugin: this, initBookmark: initBookmark, }, init() { this.data.initBookmark(this.element, this.data.plugin); } });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注册快捷键
可以通过
plugin.addCommand
来注册一个快捷键操作。ts
this.addCommand({ langKey: "showDialog", hotkey: "⇧⌘O", callback: () => { this.showDialog(); }, fileTreeCallback: (file: any) => { console.log(file, "fileTreeCallback"); }, editorCallback: (protyle: any) => { console.log(protyle, "editorCallback"); }, dockCallback: (element: HTMLElement) => { console.log(element, "dockCallback"); }, });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16这里面最关键的一个是
hotkey
一个是 callback 方法。 hotkey 必须按照特定的顺序设置才会生效。ts
export interface ICommandOption { langKey: string // 用于区分不同快捷键的 key langText?: string // 快捷键功能描述文本 /** * 目前需使用 MacOS 符号标识,顺序按照 ⌥⇧⌘,入 ⌥⇧⌘A * "Ctrl": "⌘", * "Shift": "⇧", * "Alt": "⌥", * "Tab": "⇥", * "Backspace": "⌫", * "Delete": "⌦", * "Enter": "↩", */ hotkey: string, customHotkey?: string, callback?: () => void // 其余回调存在时将不会触 globalCallback?: () => void // 焦点不在应用内时执行的回调 fileTreeCallback?: (file: any) => void // 焦点在文档树上时执行的回调 editorCallback?: (protyle: any) => void // 焦点在编辑器上时执行的回调 dockCallback?: (element: HTMLElement) => void // 焦点在 dock 上时执行的回调 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21当然,插件注册的 hotkey 是默认的 hotkey,而用户是可以在设置面板里面进行覆盖的。
而如果你想要覆盖思源内置的快捷键,可以把快捷键配置的
custom
字段置空;等到恢复的时候,在从default
中填写回来。以下案例参考 Bookmark+ 插件
ts
const bookmarkKeymap = window.siyuan.config.keymap.general.bookmark; //禁用默认书签快捷键 bookmarkKeymap.custom = ''; //恢复快捷键 bookmarkKeymap.custom = bookmarkKeymap.default;
1
2
3
4
5
6
7
注册
/
命令
/
命令,又称 slash 命令,就是思源中通过/
触发,并快速在编辑器中插入某些元素的命令。一个插件的
/
命令,可以通过设置plugin.protyleSlash
属性来配置。ts
protyleSlash: { filter: string[], html: string, id: string, callback(protyle: Protyle): void, }[];
1
2
3
4
5
6-
filter:指触发命令的关键词
-
html:指在选择面板中显示的元素
-
id:唯一标识符
-
callback:Enter 选择命令项目后,触发的回调函数
- 一般在 callback 当中,通过
protyle.insert
在编辑器中插入元素。
- 一般在 callback 当中,通过
以下是一个案例:
ts
let Templates = { datetime: { filter: ['xz', 'now'], name: 'Now', template: 'yyyy-MM-dd HH:mm:ss' }, date: { filter: ['rq', 'date', 'jt', 'today'], name: 'Date', template: 'yyyy-MM-dd' }, time: { filter: ['sj', 'time'], name: 'Time', template: 'HH:mm:ss' } }; this.protyleSlash = Object.values(Templates).map((template) => { return { filter: template.filter, html: `<span>${template.name} ${formatDateTime(template.template)}</span>`, id: template.name, callback: (protyle: Protyle) => { let strnow = formatDateTime(template.template); console.log(template.name, strnow); protyle.insert(strnow, false); }, //@ts-ignore update() { this.html = `<span>${template.name} ${formatDateTime(template.template)}</span>`; } } });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35效果如下:
ℹ️ Tips:一般使用 slash 命令都是希望在 protyle 里面插入一些什么东西。但是有些情况我们可能不想要插入内容,而是想要做别的操作,这就需要我们手动清理掉输入的
/xxx
了。具体方法是插入一个Lute.Carte
字符,来清空前面的输入。这里给一个参考案例:quick-attr 插件protyle.insert(Lute.Carte);
1
插入自定义样式
大部分情况下,你只需要把样式写在
index.css
文件里面就可以了。但是有时候可能需要使用 JS 插入一些自定义的 style,这时你就会遇到一个问题:插入的自定义样式在导出 PDF 的时候无法生效。解决这个问题的最简单的办法是:插入的 style 标签的 id 要以
snippetCSS
为开头,来模拟一个代码片段样式。例如,Callout 插件中,所有动态更改的样式,都会放在一个
style#snippetCSS-BqCallout
当中,这样导出的 PDF 中,这些动态的样式同样会生效。访问思源内部设置
访问
window.siyuan
变量;在内部中存储了大量思源内部的设置。
⚠️ 请以只读的方式来使用这个变量,不要随意更改内部的值!否则可能会造成意外的错误!
i18n(多语言支持)
在插件目录的 i18n/ 文件夹下的 json 文件会被自动导入。
插件运行时可以通过
plugin.i18n
对象来访问其中的内容。如果嫌麻烦,也可以自行在 js 中处理;
window.siyuan.config.lang
指向了当前思源呈现的语言。比如你可以这么干:ts
const I18N = { zh_CN: { warn: '⚠️ 注意Asset目录已更改!', menuLabel: '同本地 Markdown 文件同步', }, en_US: { warn: '⚠️ Warning: Asset directory has changed!', menuLabel: 'Sync With Local Markdown File', } }; let i18n: typeof I18N.zh_CN = window.siyuan.config.lang in I18N ? I18N[window.siyuan.config.lang] : I18N.en_US; export default i18n;
1
2
3
4
5
6
7
8
9
10
11
12
13
解析 markdown 文本
window 下有一个 Lute 变量,他是思源内部用来处理 markdown 解析的工具。
ts
let lute = window.Lute.New(); lute.Md2HTML('## Hello') // 输出: '<h2>Hello</h2>\n'
1
2
3使用 Node/electron API
桌面端的思源可以直接访问一些 Node 环境的包和 electron API
ts
const nodeFs = window.require('fs') as typeof import('fs'); const nodePath = window.require('path') as typeof import('path'); const electron = window.require('electron');
1
2
3⚠️ 一定不要使用 node fs 写思源的工作空间!
插件或者外部扩展如果有直接读取或者写入 data 下文件的需求,请通过调用内核 API 来实现,不要自行调用
fs
或者其他 electron、nodejs API,否则可能会导致数据同步时分块丢失,造成云端数据损坏。相关 API 见
/api/file/*
(例如/api/file/getFile
等)。 ↩
-
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于