1. 从“plugins”这个标题说起:它到底指什么
“plugins”这个词看起来简单,但放在当下的开发语境里,它其实是一个高度浓缩的入口。你可能是从 Cursor 的插件市场点进来的,也可能是在某个 CLI 工具里看到plugin.json这个配置文件,又或者是在排查 “failed to load plugins” 这类报错时搜到了这里。不管你是哪一种,核心问题都是一样的:插件系统到底是怎么运转的,我该怎么用它,出了问题又该怎么查。
我自己第一次认真研究插件机制,是因为一个很具体的需求:团队里几个人用不同的编辑器,有人用 Cursor,有人用 VS Code,还有人习惯在终端里用 CLI 工具跑任务。我们希望把一套代码检查规则、几个常用的代码片段生成器、以及一个内部 API 的调用封装,做成大家都能用的东西。最开始的方案是每个人自己装一遍,结果版本对不上、配置路径不一致、有人装完不生效,折腾了一下午。后来才意识到,与其手动同步,不如直接做成插件,用统一的plugin.json来描述能力,让宿主环境自己去加载。
所以这篇内容,我想把“plugins”这件事从头到尾讲清楚。它适合谁看?如果你是刚接触 Cursor 或者某个 CLI 工具的新手,想搞明白插件是怎么装、怎么配、怎么排错的,那这篇就是写给你的。如果你已经用过一些插件,但遇到 “failed to load plugins” 或者 “entries did not activate” 这类问题不知道怎么下手,那这篇里的排查思路和速查表应该能帮到你。如果你是想自己写一个插件、把内部工具封装成 TypeScript SDK 的开发者,那关于plugin.json结构、CLI 加载流程、以及 SDK 设计取舍的部分,会是我重点展开的内容。
需要先说明一点:插件系统不是一个孤立的东西,它一定依附于某个宿主。Cursor 的插件、VS Code 的插件、某个 CLI 工具的插件,虽然都叫 plugins,但加载机制、配置格式、生命周期钩子可能完全不同。我下面会尽量把共性抽出来,同时把差异点标清楚,这样你不管面对哪个宿主,都能有一套自己的分析框架。
2. 插件系统的整体设计与核心思路拆解
2.1 为什么要有插件:从“改源码”到“挂载能力”
在没有插件机制之前,扩展一个工具的能力通常只有两条路:要么改源码重新编译,要么在外部写脚本做胶水层。改源码的问题很明显,升级一次就冲突一次,维护成本极高。外部脚本的问题则是拿不到宿主内部的上下文,比如你没法在编辑器保存文件的瞬间触发逻辑,也没法在 CLI 解析参数之前插入自己的处理。
插件系统的本质,是把宿主的一部分能力开放出来,定义成稳定的接口,让外部代码可以在不修改宿主源码的前提下挂载进去。这个“挂载”通常发生在几个关键节点:启动时加载、命令执行前、文件事件触发时、以及退出前清理。宿主负责调用,插件负责实现,双方通过一份约定好的描述文件来对齐——这份描述文件,在大多数现代工具里就是plugin.json。
我自己的理解是,插件系统解决的核心问题是“能力扩展的标准化”。它把原来靠文档和口头约定维持的扩展方式,变成了有 schema、有校验、有生命周期的东西。你写一个插件,只要plugin.json写对了,宿主就知道该在什么时候调用你、传什么参数、期望你返回什么。这比“你自己看着办”要可靠得多。
2.2 plugin.json 的角色:插件的“身份证”加“说明书”
plugin.json这个文件,我习惯把它理解成插件的身份证加说明书。身份证的部分,是告诉宿主“我是谁、我叫什么、我版本多少”;说明书的部分,是告诉宿主“我能做什么、我在什么条件下被触发、我需要什么权限”。
一个典型的plugin.json通常包含这几类字段:基础信息(name、version、description、author)、入口声明(main 或者 entry,指向实际执行的代码文件)、能力声明(commands、hooks、menus 等)、以及依赖与权限(dependencies、permissions)。不同宿主的字段名会有差异,但结构逻辑是相通的。
这里有个很容易踩的坑:很多人写plugin.json的时候只填了 name 和 version,入口随便指一个文件,结果插件加载了但什么都不发生。原因就是能力声明缺失——宿主不知道你这个插件要在什么时候被调用。我见过最常见的错误是 hooks 写成了 hook,或者 commands 的数组里每个元素少了 id 字段,宿主解析时直接跳过,日志里只留下一句 “entry did not activate”,不仔细看根本找不到原因。
2.3 TypeScript SDK 的取舍:为什么很多插件用 TS 写
现在很多插件系统的官方推荐语言是 TypeScript,配套一个 TypeScript SDK。这个选择不是随意的。插件代码运行在宿主环境里,最怕的就是类型不匹配导致宿主崩溃。TypeScript 的静态类型检查可以在编译阶段就发现大部分接口误用,比如你把一个应该返回 Promise 的 hook 写成了同步返回,SDK 的类型定义会直接报错,而不是等到运行时才炸。
另外,TypeScript SDK 通常会提供一套封装好的基类和工具函数,比如createPlugin、registerCommand、onFileSave这类。你用 SDK 写,相当于站在宿主官方维护的抽象层上,宿主升级接口时,SDK 会跟着更新,你的迁移成本会低很多。我自己对比过纯 JavaScript 写插件和用 TypeScript SDK 写插件的体验,后者在调试阶段省下来的时间,远远超过配置 tsconfig 的那几分钟。
当然,TypeScript SDK 也不是没有代价。它引入了构建步骤,你需要把 TS 编译成 JS 才能被宿主加载。如果你的插件很简单,就是一个几十行的脚本,那直接用 JS 写、手动维护类型注释,可能更轻量。这个取舍取决于插件的复杂度和你的维护周期。
2.4 CLI 与插件的配合:命令行里的插件加载链路
CLI 工具里的插件机制,和编辑器里的插件机制有一个明显区别:CLI 通常是短生命周期的,执行完一条命令就退出。这意味着插件加载必须足够快,不能因为加载插件让命令启动慢好几秒。所以 CLI 的插件系统往往会做懒加载——只有当你执行的命令确实需要某个插件时,才去加载它。
这个链路大致是这样的:CLI 启动,解析全局配置,找到插件目录,读取每个插件的plugin.json,但此时不执行插件代码,只建立索引。当你输入的命令匹配到某个插件声明的 command 时,CLI 才去 require 或者 import 那个插件的入口文件,执行注册逻辑,然后调用对应的处理函数。执行完毕后,进程退出,插件也随之卸载。
理解这个链路很重要,因为它直接决定了你排查问题的方向。如果插件根本没被索引到,那问题在plugin.json或者插件目录配置;如果索引到了但命令没反应,那问题在命令匹配规则或者入口文件的注册逻辑;如果命令执行到一半报错,那才是插件业务代码本身的问题。
3. 核心细节解析与实操要点
3.1 plugin.json 字段逐个拆解与常见写法
我把plugin.json里最常出现的字段整理成了一张表,你可以对照自己的文件检查。需要说明的是,不同宿主的字段命名和必填项会有差异,下面这张表是基于我接触过的几类主流插件系统总结的共性部分。
| 字段名 | 是否必填 | 作用 | 常见错误 |
|---|---|---|---|
| name | 是 | 插件唯一标识 | 用了中文或空格,导致加载失败 |
| version | 是 | 版本号,用于依赖解析 | 写成 v1.0 而不是 1.0.0 |
| description | 否 | 插件描述,显示在插件列表 | 留空导致列表里一片空白 |
| main / entry | 是 | 入口文件路径 | 路径写错,或漏了扩展名 |
| commands | 否 | 声明的命令列表 | 数组元素缺 id 或 handler |
| hooks | 否 | 生命周期钩子 | 写成 hook,或事件名拼错 |
| permissions | 否 | 需要的权限声明 | 声明了但实际没用到,审核被拒 |
| dependencies | 否 | 依赖的其他插件或包 | 版本范围写太宽导致冲突 |
关于 name 字段,我特别想强调一下:它不仅是显示用的,很多时候还是插件之间互相引用的键。如果你用了大写字母或者特殊符号,在某些宿主里会被规范化成小写加连字符,结果你代码里按原名去引用就找不到了。稳妥的做法是全程小写,用连字符分隔单词,比如my-code-helper。
version 字段建议严格遵循语义化版本,也就是主版本.次版本.修订号。有些宿主在解析依赖时会做版本比较,如果你写成1.0或者v1.0.0,解析器可能直接抛异常。这个坑我在早期项目里踩过,日志里只报了一句 “invalid version format”,排查了半天才发现是版本号写法问题。
3.2 入口文件的注册逻辑:插件被加载后发生了什么
入口文件被宿主加载后,第一件事通常是调用 SDK 提供的注册函数。以 TypeScript SDK 为例,常见写法是导出一个默认对象,或者调用createPlugin并传入配置。宿主拿到这个对象后,会读取里面声明的 commands 和 hooks,把它们注册到自己的调度中心。
这里有个细节值得展开:注册是同步的还是异步的。如果宿主在启动阶段同步加载所有插件,那你的入口文件里就不能有顶层 await,否则加载会卡住甚至失败。我遇到过一种情况,插件入口里写了一个顶层 await 去请求远程配置,结果宿主启动时直接超时,报 “failed to load plugins”。后来改成在 hook 触发时再去请求,问题就解决了。
另一个细节是注册的幂等性。有些宿主在开发模式下会热重载插件,如果你的注册逻辑没有做去重,同一个命令可能被注册两次,执行时触发两遍。稳妥的做法是在注册前检查一下命令是否已存在,或者用 SDK 提供的dispose机制在重载前清理旧注册。
3.3 命令与钩子的区别:什么时候用哪个
命令和钩子是插件扩展能力的两种主要形式,但它们的触发方式完全不同。命令是用户主动发起的,比如你在 Cursor 的命令面板里输入某个指令,或者在 CLI 里敲了某个子命令。钩子则是宿主在特定事件发生时被动调用的,比如文件保存、项目打开、命令执行前后。
选择哪种形式,取决于你的需求是“用户想用的时候才用”还是“每次发生某件事都要用”。举个例子,如果你要做一个代码格式化工具,那应该做成命令,用户选中代码后主动触发。如果你要做一个保存时自动补全 import 的工具,那就必须用钩子,挂在文件保存事件上。
我见过有人把本该做成钩子的功能硬做成命令,结果用户每次保存都要手动敲一遍命令,体验很差。反过来,把本该做成命令的功能做成钩子,每次打开文件都自动跑一遍,又慢又烦。这个判断标准其实很简单:问自己一句“这个动作是用户想控制时机,还是系统事件驱动”。
3.4 权限与沙箱:插件能碰什么,不能碰什么
插件运行在宿主环境里,理论上可以访问宿主能访问的一切。但出于安全和稳定考虑,很多宿主会引入权限声明和沙箱机制。你在plugin.json里声明了permissions,宿主在加载时会检查,如果插件尝试访问未声明的能力,可能会被拦截甚至直接卸载。
常见的权限项包括文件系统读写、网络请求、执行子进程、访问剪贴板等。我的建议是遵循最小权限原则:只声明你真正用到的权限。一方面,声明过多权限会让用户在安装时犹豫;另一方面,某些宿主会对高权限插件做额外审核,声明了用不到反而增加麻烦。
沙箱方面,不同宿主的实现差异很大。有的宿主把插件跑在独立的进程里,通过 IPC 通信,插件崩溃不会影响宿主;有的宿主则直接在宿主进程里执行插件代码,插件死循环会卡死整个应用。如果你要写一个可能耗时的插件,最好先确认宿主的沙箱模型,必要时把重活放到子进程或者 worker 里。
4. 实操过程与核心环节实现
4.1 从零写一个最小可用插件
我下面用一个具体的例子来走一遍完整流程。假设我们要做一个插件,功能是在 CLI 里提供一个hello命令,输出当前项目的基本信息。这个例子足够简单,但覆盖了plugin.json编写、入口注册、命令实现、本地调试这几个关键环节。
第一步是创建目录结构。我习惯这样组织:
my-plugin/ plugin.json src/ index.ts package.json tsconfig.json第二步是写plugin.json。这里我声明一个命令,id 叫hello,handler 指向入口文件里导出的函数名。
{ "name": "my-hello-plugin", "version": "1.0.0", "description": "一个输出项目信息的示例插件", "main": "dist/index.js", "commands": [ { "id": "hello", "title": "输出项目信息", "handler": "runHello" } ], "permissions": ["fs:read"] }第三步是写入口文件。用 TypeScript SDK 的话,大致是这样:
import { createPlugin, CommandContext } from '@example/plugin-sdk'; import { readFileSync } from 'fs'; import { join } from 'path'; export async function runHello(ctx: CommandContext) { const pkgPath = join(ctx.workspaceRoot, 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); ctx.output(`项目名称: ${pkg.name}`); ctx.output(`版本: ${pkg.version}`); } export default createPlugin({ commands: { hello: runHello, }, });第四步是编译和本地加载。用tsc把src/index.ts编译到dist/index.js,然后在宿主的插件目录配置里指向这个插件的根目录。不同宿主的加载方式不一样,有的是把插件目录软链到指定位置,有的是在配置文件里写插件路径。这一步建议先看宿主的官方文档,确认加载入口。
4.2 参数计算与配置选择:插件目录和加载顺序
插件目录的配置看起来是个小事,但它直接影响加载顺序和优先级。大多数宿主会按目录名的字母序加载插件,这意味着如果你的插件依赖另一个插件提供的命令,而那个插件的目录名排在你后面,就可能出现依赖找不到的情况。
我的做法是给插件目录加数字前缀,比如10-core-plugin、20-feature-plugin,这样加载顺序一目了然。如果宿主支持在配置里显式指定加载顺序,那就更稳妥,直接按数组顺序加载,不依赖文件系统排序。
另一个需要计算的是超时时间。如果宿主对插件加载有超时限制,比如 5 秒,那你的插件入口执行时间必须控制在这个范围内。我一般会把入口逻辑压到 100 毫秒以内,只做注册,不做实际业务。业务逻辑放到命令触发时再执行,这样既快又不容易超时。
4.3 实操现场:一次完整的插件加载与执行记录
我把一次实际的加载执行过程记录下来,你可以对照自己的日志看。宿主启动后,日志里会依次出现这几行:
[plugin] scanning plugin directory: /path/to/plugins [plugin] found 3 entries [plugin] loading my-hello-plugin@1.0.0 [plugin] registered command: hello [plugin] 3 entries activated如果中间某一步断了,比如只出现 “found 3 entries” 但没有 “loading”,那说明plugin.json解析失败,宿主跳过了这个插件。如果出现 “loading” 但没有 “registered command”,那说明入口文件执行了,但命令注册没成功,可能是 handler 名字对不上,或者 SDK 版本不匹配。
执行hello命令时,日志会变成:
[cli] resolving command: hello [cli] matched plugin: my-hello-plugin [cli] invoking handler: runHello 项目名称: my-project 版本: 2.3.1 [cli] command completed in 45ms这套日志结构是我自己调试时最依赖的东西。它把加载、注册、匹配、执行四个阶段分得很清楚,哪一步出问题一目了然。如果你的宿主日志没有这么细,可以考虑在插件入口里自己加日志,至少把 “entry loaded” 和 “command registered” 打出来。
4.4 用 CLI 做插件管理:安装、启用、禁用、卸载
很多宿主除了自动扫描插件目录,还会提供一个 CLI 来做插件管理。常见命令包括plugin install、plugin enable、plugin disable、plugin list、plugin uninstall。这些命令背后做的事情,其实就是操作插件目录和一份状态文件。
我建议你在手动管理插件之前,先搞清楚宿主的状态文件放在哪里。有的宿主把启用状态写在全局配置里,有的写在插件目录下的.state文件里。如果你手动删了插件目录但没更新状态文件,下次启动时宿主可能会报 “plugin not found” 或者 “entry did not activate”。
禁用插件的时候,我一般不会直接删目录,而是先 disable,观察一段时间确认没有副作用,再考虑卸载。因为有些插件之间可能有隐式依赖,你禁用了 A,B 插件可能就报错了。先 disable 可以快速回滚,删了就麻烦了。
5. 常见问题与排查技巧实录
5.1 failed to load plugins 的排查路径
“failed to load plugins” 这个报错信息很笼统,它可能对应好几种不同的原因。我整理了一条排查路径,按顺序走一遍,基本能定位到问题。
| 排查步骤 | 检查内容 | 常见问题 |
|---|---|---|
| 1 | 插件目录是否存在 | 路径配置错误,目录被误删 |
| 2 | plugin.json 是否合法 | JSON 语法错误,字段缺失 |
| 3 | 入口文件是否存在 | main 路径写错,编译产物没生成 |
| 4 | 入口文件是否可执行 | 有语法错误,依赖没安装 |
| 5 | 权限是否声明 | 用了 fs 但没声明 fs:read |
| 6 | 版本是否兼容 | SDK 版本与宿主不匹配 |
我遇到最多的是第 2 步和第 3 步。plugin.json里多了一个逗号、少了一个引号,JSON 解析直接失败,宿主只会报一句 “failed to load”,不会告诉你具体哪一行。这时候可以用node -e "JSON.parse(require('fs').readFileSync('plugin.json','utf-8'))"快速验证 JSON 合法性。入口文件的问题通常是编译产物路径和main字段对不上,比如你编译到dist/index.js,但main写的是index.js。
5.2 entries did not activate 的典型场景
“entries did not activate” 这个提示比 “failed to load” 更具体一点,它说明插件文件被找到了,但激活过程没完成。常见场景有这么几个。
第一种是命令 id 冲突。两个插件声明了同一个命令 id,宿主可能只激活其中一个,另一个就被跳过了。排查方法是把所有插件的plugin.json里的 commands 列出来,看看有没有重复。
第二种是 hook 事件名拼写错误。宿主支持的事件名是固定的,你写了一个不存在的事件名,注册时不会报错,但永远不会被触发。这种情况最隐蔽,因为日志里看起来一切正常。我的做法是先把事件名复制到宿主的官方文档里搜一下,确认存在再写。
第三种是入口文件抛了异常但被宿主吞掉了。有些宿主在加载插件时会 try-catch,异常只写进调试日志,不显示在控制台。这时候需要把宿主的日志级别调到 debug,才能看到真正的错误堆栈。
5.3 插件冲突与加载顺序问题
插件冲突是多人协作项目里很常见的问题。两个人各自写了一个插件,单独用都没问题,一起加载就出问题。冲突的类型主要有三种:命令 id 冲突、hook 执行顺序冲突、以及全局状态污染。
命令 id 冲突好解决,改个名字就行。hook 执行顺序冲突麻烦一些,比如插件 A 在文件保存时格式化代码,插件 B 在文件保存时做 lint 检查,如果 B 先执行,检查的是未格式化的代码,结果可能不准。这时候需要宿主支持指定 hook 优先级,或者把两个逻辑合并到一个插件里。
全局状态污染是最难查的。插件 A 往全局对象上挂了一个属性,插件 B 也挂了同名属性,互相覆盖。排查方法是尽量让插件不碰全局对象,所有状态都放在插件自己的闭包里。如果必须共享状态,通过宿主提供的 context 对象传递,而不是直接挂全局。
5.4 性能问题:插件拖慢启动怎么办
插件多了之后,启动变慢是很自然的事。我做过一个粗略的测量,每多加载一个插件,启动时间大概增加 20 到 80 毫秒,取决于插件入口的复杂度。如果装了二十个插件,启动慢一两秒是正常的。
优化方向有几个。第一是懒加载,把非必要的初始化逻辑从入口移到命令触发时。第二是减少同步 IO,入口里不要读大文件、不要做网络请求。第三是合并插件,把功能相近的小插件合并成一个,减少加载次数。第四是禁用不常用的插件,用的时候再启用。
我自己的习惯是定期清理插件列表,三个月没用过的就禁用掉。插件不是越多越好,每个插件都是一份维护负担和性能开销。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| failed to load plugins | plugin.json 语法错误 | 用 JSON 解析器验证 |
| failed to load plugins | 入口文件路径错误 | 检查 main 字段与实际产物 |
| entries did not activate | 命令 id 重复 | 重命名冲突的命令 |
| entries did not activate | hook 事件名错误 | 对照官方文档核对事件名 |
| 命令无响应 | handler 名字不匹配 | 检查 plugin.json 与入口导出名 |
| 命令执行报错 | 权限未声明 | 在 permissions 里补充 |
| 启动变慢 | 插件过多或入口太重 | 懒加载、合并、禁用 |
| 热重载后命令重复 | 注册未做幂等 | 注册前检查或实现 dispose |
6. 插件开发的经验心得与扩展思路
6.1 我踩过的三个坑
第一个坑是版本号格式。早期我写plugin.json的时候,version 随手写了1.0,本地测试没问题,但发布到团队共享目录后,别人的宿主加载时报 “invalid version”。后来统一改成三段式1.0.0,再没出过这个问题。这个坑的教训是:不要假设宿主对格式宽容,按最严格的规范写。
第二个坑是入口文件的副作用。我在入口文件顶层写了一个console.log用来调试,忘了删,结果每次启动都打一行日志,用户以为出问题了。更严重的是,我在另一个插件入口里做了一次同步的文件读取,用来加载配置,结果那个文件不存在时直接抛异常,整个插件加载失败。后来改成在命令触发时再读配置,并且加了 try-catch,问题才解决。
第三个坑是 hook 的异步处理。我写了一个文件保存时的 hook,里面做了异步的格式化操作,但没有返回 Promise,宿主以为 hook 执行完了,实际上格式化还在后台跑。结果用户连续保存两次,两次格式化并发执行,文件内容错乱。后来改成返回 Promise,让宿主等待完成,问题消失。这个坑的教训是:hook 里如果有异步操作,一定要正确返回 Promise,让宿主知道什么时候算完成。
6.2 插件设计的三条实用原则
第一条原则是单一职责。一个插件只做一件事,做深做透。我见过一个插件同时做格式化、lint、代码生成、API 调用,结果任何一个功能出问题都要重新加载整个插件,而且用户想只用其中一个功能也没办法。拆成四个插件后,每个都更稳定,用户也能按需启用。
第二条原则是配置外置。插件的行为参数不要硬编码在代码里,放到配置文件或者环境变量里。这样用户不用改代码就能调整行为,你也不用为了改一个参数重新发版。配置的读取时机建议放在命令触发时,而不是入口加载时,避免因为配置文件缺失导致插件加载失败。
第三条原则是失败可恢复。插件里的任何操作都要考虑失败情况,尤其是文件读写和网络请求。失败时不要直接抛异常让宿主崩溃,而是捕获后给出清晰的错误提示,让用户知道发生了什么、该怎么处理。我在插件里统一用了一个safeExecute包装函数,所有可能失败的操作都走它,出错时输出友好提示并返回默认值。
6.3 后续可以扩展的方向
如果你已经把基础插件跑通了,接下来可以往几个方向扩展。一个是把插件发布到团队内部的插件市场,让其他人也能安装使用。这需要你补充 README、变更日志、以及版本发布流程。另一个是给插件加上配置界面,让用户通过图形界面调整参数,而不是手动改配置文件。还有一个方向是做插件之间的组合,比如一个插件提供数据,另一个插件消费数据,通过宿主的事件机制串联起来。
我最近在尝试的一个方向是把插件和 CLI 的批处理能力结合起来。比如写一个插件,声明一个命令,这个命令可以接收一批文件路径,对每个文件执行同样的操作,最后汇总输出。这种批处理场景在 CLI 里很常见,用插件来实现比写一次性脚本更可维护。
6.4 关于 Cursor 和 CLI 场景的一些补充
Cursor 这类编辑器里的插件,和纯 CLI 的插件,在使用体验上有一些差异。编辑器插件通常有 UI 层面的交互,比如命令面板、右键菜单、状态栏提示,这些在plugin.json里通过 menus 或者 ui 字段声明。CLI 插件则更纯粹,输入输出都在终端里,交互靠参数和标准输出。
如果你同时维护两个场景的插件,建议把核心逻辑抽成一个独立的包,编辑器插件和 CLI 插件都依赖这个包,只是外壳不同。这样业务逻辑只写一遍,两边的差异只在适配层。我自己用这种方式维护过一套代码检查规则,编辑器里通过 hook 触发,CLI 里通过命令触发,核心的检查逻辑完全复用,省了很多重复工作。
另外,Cursor 的中文设置、注册流程、免费额度这些问题,和插件机制本身关系不大,属于工具使用层面的问题。如果你在配置 Cursor 的过程中遇到插件不生效的情况,先确认插件是否已经启用,再检查插件的兼容版本是否匹配你当前的 Cursor 版本。版本不匹配是插件不生效的常见原因之一,尤其是在工具更新比较频繁的阶段。
插件这件事,说到底就是把重复的事情标准化,把个人的经验沉淀成团队可复用的能力。我自己的体会是,写插件的过程也是梳理自己工作流的过程,你会被迫想清楚哪些步骤是必要的、哪些是可以自动化的、哪些是应该交给用户控制的。这个思考过程本身,比插件代码更有价值。