@nuxtblog/plugin-sdk 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +849 -308
- package/README.zh.md +857 -0
- package/package.json +4 -2
package/README.zh.md
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
# @nuxtblog/plugin-sdk
|
|
2
|
+
|
|
3
|
+
nuxtblog 插件 TypeScript 类型定义与完整开发指南。
|
|
4
|
+
|
|
5
|
+
English(README.md)|(中文文档](README.zh.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目录
|
|
10
|
+
|
|
11
|
+
- [概述](#概述)
|
|
12
|
+
- [插件结构](#插件结构)
|
|
13
|
+
- [清单文件(package.json)](#清单文件-packagejson)
|
|
14
|
+
- [插件参数(settings)](#插件参数-settings)
|
|
15
|
+
- [能力声明(capabilities)](#能力声明-capabilities)
|
|
16
|
+
- [插件 API 参考](#插件-api-参考)
|
|
17
|
+
- [nuxtblog.on — 事件订阅](#nuxtblogon--事件订阅)
|
|
18
|
+
- [nuxtblog.filter — 数据拦截](#nuxtblogfilter--数据拦截)
|
|
19
|
+
- [nuxtblog.http — HTTP 请求](#nuxtbloghttp--http-请求)
|
|
20
|
+
- [nuxtblog.store — 持久化存储](#nuxtblogstore--持久化存储)
|
|
21
|
+
- [nuxtblog.settings — 读取配置](#nuxtblogsettings--读取配置)
|
|
22
|
+
- [nuxtblog.log — 服务端日志](#nuxtbloglog--服务端日志)
|
|
23
|
+
- [声明式 Webhook](#声明式-webhook)
|
|
24
|
+
- [声明式 Pipeline(多步流水线)](#声明式-pipeline多步流水线)
|
|
25
|
+
- [所有事件列表](#所有事件列表)
|
|
26
|
+
- [fire-and-forget 事件(nuxtblog.on)](#fire-and-forget-事件nuxtblogon)
|
|
27
|
+
- [filter 拦截事件(nuxtblog.filter)](#filter-拦截事件nuxtblogfilter)
|
|
28
|
+
- [执行模型与并发](#执行模型与并发)
|
|
29
|
+
- [超时与重试](#超时与重试)
|
|
30
|
+
- [可观测性](#可观测性)
|
|
31
|
+
- [打包与安装](#打包与安装)
|
|
32
|
+
- [TypeScript 支持](#typescript-支持)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 概述
|
|
37
|
+
|
|
38
|
+
插件是运行在**服务器端的 JavaScript 脚本**,由 [goja](https://github.com/dop251/goja) 引擎执行(兼容 ES2015+)。每个插件在完全隔离的 VM 实例中运行,通过全局 `nuxtblog` 对象与博客系统交互。
|
|
39
|
+
|
|
40
|
+
**插件可以做什么:**
|
|
41
|
+
|
|
42
|
+
- 异步订阅系统事件(文章、评论、用户、媒体等),fire-and-forget 模式,不影响主流程
|
|
43
|
+
- 在数据写入数据库之前同步拦截并修改,或拒绝(abort)整个操作
|
|
44
|
+
- 读取管理员在后台配置的参数(API Token、Webhook URL、功能开关等)
|
|
45
|
+
- 向外部服务发起 HTTP 请求(通知、同步、AI 调用等)
|
|
46
|
+
- 在独立的 KV 存储中持久化运行时状态
|
|
47
|
+
- 通过清单文件声明式配置出站 Webhook 和多步异步流水线,无需编写 JS
|
|
48
|
+
|
|
49
|
+
**插件不能做什么:**
|
|
50
|
+
|
|
51
|
+
- 在 `filter` 处理器中调用 `http.fetch`(会阻塞请求处理,应改用 `nuxtblog.on` 处理异步副作用)
|
|
52
|
+
- 访问未在 `capabilities` 中声明的 API(未声明的 API 在 VM 内是 `undefined`,不会抛出错误)
|
|
53
|
+
- 与其他插件共享 VM 状态(每个插件拥有完全独立的运行时)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 插件结构
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
my-plugin/
|
|
61
|
+
├── package.json ← 插件清单(必须,含 "plugin" 字段)
|
|
62
|
+
├── index.js ← 打包后的单文件脚本(服务端加载此文件)
|
|
63
|
+
└── src/
|
|
64
|
+
└── index.ts ← TypeScript 源码(开发用,可选)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
安装包只需包含 `package.json` 和 `index.js`:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
my-plugin.zip
|
|
71
|
+
├── package.json
|
|
72
|
+
└── index.js (或 plugin.entry 中声明的路径)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 清单文件 (`package.json`)
|
|
78
|
+
|
|
79
|
+
清单以标准 `package.json` 格式提供,插件专属配置嵌套在 `"plugin"` 字段下。
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"name": "owner/my-plugin",
|
|
84
|
+
"version": "1.0.0",
|
|
85
|
+
"description": "插件功能简介",
|
|
86
|
+
"author": "owner",
|
|
87
|
+
"license": "MIT",
|
|
88
|
+
"homepage": "https://github.com/owner/my-plugin",
|
|
89
|
+
"keywords": ["nuxtblog-plugin"],
|
|
90
|
+
"plugin": {
|
|
91
|
+
"title": "My Plugin",
|
|
92
|
+
"icon": "i-tabler-plug",
|
|
93
|
+
"entry": "index.js",
|
|
94
|
+
"priority": 10,
|
|
95
|
+
"capabilities": {
|
|
96
|
+
"http": { "allow": ["hooks.slack.com"], "timeout_ms": 5000 },
|
|
97
|
+
"store": { "read": true, "write": true }
|
|
98
|
+
},
|
|
99
|
+
"settings": [
|
|
100
|
+
{ "key": "webhook_url", "label": "Slack Webhook URL", "type": "string", "required": true }
|
|
101
|
+
],
|
|
102
|
+
"webhooks": [],
|
|
103
|
+
"pipelines": []
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 顶层字段(标准 npm 字段)
|
|
109
|
+
|
|
110
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `name` | string | ✅ | 插件唯一 ID,推荐格式 `owner/repo`,安装后不可更改 |
|
|
113
|
+
| `version` | string | ✅ | 版本号,如 `1.0.0` |
|
|
114
|
+
| `description` | string | | 显示在管理后台的简介 |
|
|
115
|
+
| `author` | string | | 作者名 |
|
|
116
|
+
| `license` | string | | 开源协议,如 `MIT` |
|
|
117
|
+
| `homepage` | string | | 插件主页或仓库 URL |
|
|
118
|
+
| `keywords` | string[] | | 分类标签,建议包含 `nuxtblog-plugin` |
|
|
119
|
+
|
|
120
|
+
### `"plugin"` 字段
|
|
121
|
+
|
|
122
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
123
|
+
|---|---|---|---|
|
|
124
|
+
| `title` | string | ✅ | 显示在管理后台的名称 |
|
|
125
|
+
| `icon` | string | | [Tabler Icons](https://tabler.io/icons) 图标名,如 `i-tabler-bell` |
|
|
126
|
+
| `entry` | string | | 入口脚本路径,默认 `index.js` |
|
|
127
|
+
| `priority` | number | | 插件执行顺序,数值越小越先执行,默认 `10` |
|
|
128
|
+
| `capabilities` | object | | API 权限声明,见[能力声明](#能力声明-capabilities) |
|
|
129
|
+
| `settings` | array | | 管理员可配置的参数,见[插件参数](#插件参数-settings) |
|
|
130
|
+
| `webhooks` | array | | 声明式出站 Webhook,见[声明式 Webhook](#声明式-webhook) |
|
|
131
|
+
| `pipelines` | array | | 声明式异步流水线,见[声明式 Pipeline](#声明式-pipeline多步流水线) |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 插件参数 (`settings`)
|
|
136
|
+
|
|
137
|
+
`settings` 数组声明管理员在插件设置界面需要填写的参数。插件通过 `nuxtblog.settings.get(key)` 在运行时读取。
|
|
138
|
+
|
|
139
|
+
### 字段类型
|
|
140
|
+
|
|
141
|
+
| `type` 值 | 渲染控件 | 适用场景 |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `string` | 单行文本输入 | URL、名称、任意字符串 |
|
|
144
|
+
| `password` | 密码输入(遮掩显示) | API Key、Token、Secret |
|
|
145
|
+
| `number` | 数字输入 | 超时时间、最大数量等 |
|
|
146
|
+
| `boolean` | 开关(Switch) | 功能开关 |
|
|
147
|
+
| `select` | 下拉选择 | 枚举值,配合 `options` 使用 |
|
|
148
|
+
| `textarea` | 多行文本 | 模板文本、JSON 配置等长文本 |
|
|
149
|
+
|
|
150
|
+
### 字段属性
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"key": "api_token",
|
|
155
|
+
"label": "API Token",
|
|
156
|
+
"type": "password",
|
|
157
|
+
"required": true,
|
|
158
|
+
"default": "",
|
|
159
|
+
"placeholder": "sk-xxxxxxxx",
|
|
160
|
+
"description": "从服务商控制台复制",
|
|
161
|
+
"options": []
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| 属性 | 类型 | 说明 |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `key` | string | 参数键名,在 `nuxtblog.settings.get(key)` 中使用 |
|
|
168
|
+
| `label` | string | 后台表单的字段标签 |
|
|
169
|
+
| `type` | string | 控件类型,见上表 |
|
|
170
|
+
| `required` | boolean | 是否必填(视觉标记,不强制校验) |
|
|
171
|
+
| `default` | any | 安装时的默认值 |
|
|
172
|
+
| `placeholder` | string | 输入框占位文字 |
|
|
173
|
+
| `description` | string | 字段说明文字,显示在输入框下方 |
|
|
174
|
+
| `options` | string[] | 下拉选项(仅 `type: "select"` 时使用) |
|
|
175
|
+
|
|
176
|
+
### 完整示例
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
"settings": [
|
|
180
|
+
{ "key": "enabled", "label": "启用插件功能", "type": "boolean", "default": true },
|
|
181
|
+
{ "key": "api_token", "label": "API Token", "type": "password", "required": true, "placeholder": "sk-xxxxxxxx", "description": "从服务商控制台获取" },
|
|
182
|
+
{ "key": "webhook_url", "label": "Webhook URL", "type": "string", "placeholder": "https://example.com/hook" },
|
|
183
|
+
{ "key": "timeout", "label": "超时时间(秒)", "type": "number", "default": 10 },
|
|
184
|
+
{ "key": "log_level", "label": "日志级别", "type": "select", "default": "info", "options": ["debug","info","warn","error"] },
|
|
185
|
+
{ "key": "template", "label": "消息模板", "type": "textarea", "placeholder": "新文章:{{title}}\n链接:{{url}}" }
|
|
186
|
+
]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 能力声明 (`capabilities`)
|
|
192
|
+
|
|
193
|
+
能力声明遵循**白名单模型**:只有在 `capabilities` 中显式声明的 API 才会注入到 VM 中。未声明的 API 在 JS 里是 `undefined` — 访问它们不会抛出错误,功能只是不存在。
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
"capabilities": {
|
|
197
|
+
"http": {
|
|
198
|
+
"allow": ["api.openai.com", "hooks.slack.com"],
|
|
199
|
+
"timeout_ms": 8000
|
|
200
|
+
},
|
|
201
|
+
"store": {
|
|
202
|
+
"read": true,
|
|
203
|
+
"write": true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `http`
|
|
209
|
+
|
|
210
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
211
|
+
|---|---|---|---|
|
|
212
|
+
| `allow` | string[] | `[]`(任意域名) | 允许访问的域名白名单。子域名自动匹配(如 `example.com` 同时允许 `api.example.com`)。空列表表示允许任意域名。 |
|
|
213
|
+
| `timeout_ms` | number | `15000` | 每次请求的超时时间(毫秒)。 |
|
|
214
|
+
|
|
215
|
+
### `store`
|
|
216
|
+
|
|
217
|
+
| 属性 | 类型 | 说明 |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `read` | boolean | 授权访问 `nuxtblog.store.get` |
|
|
220
|
+
| `write` | boolean | 授权访问 `nuxtblog.store.set` 和 `nuxtblog.store.delete` |
|
|
221
|
+
|
|
222
|
+
> **注意:** `nuxtblog.on`、`nuxtblog.filter`、`nuxtblog.log`、`nuxtblog.settings` 始终可用,无需声明任何 capability。
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 插件 API 参考
|
|
227
|
+
|
|
228
|
+
### `nuxtblog.on` — 事件订阅
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
nuxtblog.on(event: string, handler: (payload: object) => void): void
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
订阅系统事件,**异步执行**(fire-and-forget)。操作完成后触发,handler 内的错误仅记录日志和错误环形缓冲区,不影响原始操作。
|
|
235
|
+
|
|
236
|
+
**关键约束:**
|
|
237
|
+
|
|
238
|
+
- Handler 超时:**3 秒**
|
|
239
|
+
- 允许在 `nuxtblog.on` 的 handler 中调用 `http.fetch`
|
|
240
|
+
- 多个插件按 `priority` 升序执行(数值小的先执行),同优先级按插件 ID 字母顺序排序
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// 文章发布后推送 Slack 通知
|
|
244
|
+
nuxtblog.on('post.published', (data) => {
|
|
245
|
+
const url = nuxtblog.settings.get('webhook_url') as string
|
|
246
|
+
if (!url) return
|
|
247
|
+
|
|
248
|
+
const res = nuxtblog.http.fetch(url, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
body: { text: `新文章发布:${data.title} — ${data.slug}` },
|
|
251
|
+
})
|
|
252
|
+
if (!res.ok) {
|
|
253
|
+
nuxtblog.log.warn(`Slack 推送失败:HTTP ${res.status}`)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// 记录累计文章数
|
|
258
|
+
nuxtblog.on('post.created', (_data) => {
|
|
259
|
+
const count = ((nuxtblog.store.get('post_count') as number) || 0) + 1
|
|
260
|
+
nuxtblog.store.set('post_count', count)
|
|
261
|
+
nuxtblog.log.info(`累计推送文章数:${count}`)
|
|
262
|
+
})
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### `nuxtblog.filter` — 数据拦截
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
nuxtblog.filter(event: string, handler: (ctx: PluginCtx) => void): void
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
在数据**写入数据库之前同步拦截**。handler 接收 `ctx` 对象,修改 `ctx.data` 可改变最终写入的内容,调用 `ctx.abort(reason)` 可取消整个操作。
|
|
274
|
+
|
|
275
|
+
**关键约束:**
|
|
276
|
+
|
|
277
|
+
- Handler 超时:**50 毫秒**(严格限制,保证请求延迟可预期)
|
|
278
|
+
- `http.fetch` 在 filter handler 中**被强制阻断**,请改用 `nuxtblog.on` 处理异步副作用
|
|
279
|
+
- 所有插件按 `priority` 顺序执行,`ctx.meta` 在同一事件链的所有插件间共享
|
|
280
|
+
|
|
281
|
+
**`ctx` 对象说明:**
|
|
282
|
+
|
|
283
|
+
| 属性 | 类型 | 说明 |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `ctx.event` | string | 当前 filter 事件名,如 `"filter:post.create"` |
|
|
286
|
+
| `ctx.data` | object | 可变的数据载荷,对它的修改会被写入数据库 |
|
|
287
|
+
| `ctx.input` | object | `ctx.data` 的深拷贝快照(链开始前保存),只读,用于差异对比和审计日志 |
|
|
288
|
+
| `ctx.meta` | object | 跨插件 KV 共享存储,先执行的插件写入,后执行的插件可读取 |
|
|
289
|
+
| `ctx.next()` | function | 可选:显式表示当前 handler 已完成。即使不调用,链也会继续,除非调用了 `abort()` |
|
|
290
|
+
| `ctx.abort(reason)` | function | 立即中止整个操作,跳过后续所有插件,调用方收到包含 `reason` 的错误 |
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
// 去除首尾空格,限制标题长度
|
|
294
|
+
nuxtblog.filter('post.create', (ctx) => {
|
|
295
|
+
ctx.data.title = (ctx.data.title as string).trim()
|
|
296
|
+
if ((ctx.data.title as string).length > 200) {
|
|
297
|
+
ctx.abort('标题不能超过 200 个字符')
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
// 写入 meta,供后续插件使用
|
|
301
|
+
ctx.meta.computed_slug = (ctx.data.title as string).toLowerCase().replace(/\s+/g, '-')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// 根据前一个插件计算的 slug 自动填充
|
|
305
|
+
nuxtblog.filter('post.create', (ctx) => {
|
|
306
|
+
if (!ctx.data.slug && ctx.meta.computed_slug) {
|
|
307
|
+
ctx.data.slug = ctx.meta.computed_slug
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// 拦截包含违禁词的评论
|
|
312
|
+
nuxtblog.filter('comment.create', (ctx) => {
|
|
313
|
+
const blocked = ['违禁词', 'spam']
|
|
314
|
+
const content = (ctx.data.content as string).toLowerCase()
|
|
315
|
+
if (blocked.some(w => content.includes(w))) {
|
|
316
|
+
ctx.abort('评论包含违禁内容')
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// 拦截内容渲染,对读者输出进行处理(不影响存储)
|
|
321
|
+
nuxtblog.filter('content.render', (ctx) => {
|
|
322
|
+
// 修改 ctx.data.content 来改变读者看到的内容
|
|
323
|
+
ctx.data.content = (ctx.data.content as string).replace(/foo/g, 'bar')
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
### `nuxtblog.http` — HTTP 请求
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
nuxtblog.http.fetch(url: string, options?: FetchOptions): FetchResult
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
同步 HTTP 请求(非 Promise),立即返回结果,默认超时 15 秒。
|
|
336
|
+
|
|
337
|
+
> 需要在 `capabilities.http` 中声明。
|
|
338
|
+
> **在 `filter` handler 中被强制阻断。** 请在 `nuxtblog.on` handler 或 pipeline `js` 步骤中使用。
|
|
339
|
+
|
|
340
|
+
**options 参数:**
|
|
341
|
+
|
|
342
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
343
|
+
|---|---|---|---|
|
|
344
|
+
| `method` | string | `"GET"` | HTTP 方法:`GET`、`POST`、`PUT`、`PATCH`、`DELETE` |
|
|
345
|
+
| `body` | object \| string | — | 请求体。对象自动 JSON 序列化;字符串原样发送 |
|
|
346
|
+
| `headers` | object | — | 自定义请求头。有 body 时自动添加 `Content-Type: application/json` |
|
|
347
|
+
|
|
348
|
+
**返回值:**
|
|
349
|
+
|
|
350
|
+
| 属性 | 类型 | 说明 |
|
|
351
|
+
|---|---|---|
|
|
352
|
+
| `ok` | boolean | HTTP 状态码 200–299 时为 `true` |
|
|
353
|
+
| `status` | number | HTTP 状态码 |
|
|
354
|
+
| `body` | any | 自动 JSON.parse,解析失败则返回原始字符串 |
|
|
355
|
+
| `error` | string | 请求失败时的错误信息(网络错误、超时、域名不在白名单等) |
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
nuxtblog.on('post.published', (data) => {
|
|
359
|
+
const token = nuxtblog.settings.get('api_token') as string
|
|
360
|
+
if (!token) return
|
|
361
|
+
|
|
362
|
+
const res = nuxtblog.http.fetch('https://api.example.com/notify', {
|
|
363
|
+
method: 'POST',
|
|
364
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
365
|
+
body: { title: data.title, id: data.id },
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
if (res.ok) {
|
|
369
|
+
nuxtblog.log.info(`推送成功,远端 ID:${(res.body as any).id}`)
|
|
370
|
+
} else {
|
|
371
|
+
nuxtblog.log.error(`推送失败:${res.error ?? `HTTP ${res.status}`}`)
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### `nuxtblog.store` — 持久化存储
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
nuxtblog.store.get(key: string): unknown
|
|
382
|
+
nuxtblog.store.set(key: string, value: unknown): void
|
|
383
|
+
nuxtblog.store.delete(key: string): void
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
每个插件独立的键值存储,数据持久保存在数据库 `options` 表中。键名自动按插件 ID 命名空间隔离,插件之间无法互相访问对方的数据。
|
|
387
|
+
|
|
388
|
+
值可以是任意 JSON 可序列化类型:字符串、数字、布尔值、数组、对象。
|
|
389
|
+
|
|
390
|
+
> 需要在 `capabilities.store` 中声明 `read: true` 和/或 `write: true`。
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
// 记录累计推送次数
|
|
394
|
+
nuxtblog.on('post.published', (data) => {
|
|
395
|
+
const count = ((nuxtblog.store.get('push_count') as number) || 0) + 1
|
|
396
|
+
nuxtblog.store.set('push_count', count)
|
|
397
|
+
|
|
398
|
+
// 缓存最近推送的文章
|
|
399
|
+
nuxtblog.store.set('last_post', {
|
|
400
|
+
id: data.id,
|
|
401
|
+
title: data.title,
|
|
402
|
+
at: new Date().toISOString(),
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
// 插件重装时清除缓存状态
|
|
407
|
+
nuxtblog.on('plugin.installed', (_data) => {
|
|
408
|
+
nuxtblog.store.delete('push_count')
|
|
409
|
+
nuxtblog.store.delete('last_post')
|
|
410
|
+
})
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
> **区分 store 和 settings:**
|
|
414
|
+
> `nuxtblog.store` 用于**运行时状态**(计数、缓存、上次执行时间等)。
|
|
415
|
+
> 管理员配置的参数(API Key、URL、开关)请用 `nuxtblog.settings`。
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
### `nuxtblog.settings` — 读取配置
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
nuxtblog.settings.get(key: string): unknown
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
读取管理员在后台配置的参数值。结果缓存 **30 秒**,避免高频事件时频繁查询数据库。管理员修改设置后,下一次缓存过期时自动生效,无需重启插件。
|
|
426
|
+
|
|
427
|
+
始终可用,无需声明任何 capability。
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
const token = nuxtblog.settings.get('api_token') as string | null
|
|
431
|
+
const enabled = nuxtblog.settings.get('enabled') as boolean | null
|
|
432
|
+
const timeout = nuxtblog.settings.get('timeout') as number | null
|
|
433
|
+
|
|
434
|
+
if (!token) {
|
|
435
|
+
nuxtblog.log.warn('未配置 api_token,跳过执行')
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
### `nuxtblog.log` — 服务端日志
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
nuxtblog.log.info(msg: string): void
|
|
446
|
+
nuxtblog.log.warn(msg: string): void
|
|
447
|
+
nuxtblog.log.error(msg: string): void
|
|
448
|
+
nuxtblog.log.debug(msg: string): void
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
写入服务端日志,每条消息自动添加 `[plugin:<id>]` 前缀,在服务端控制台和日志文件中可见。
|
|
452
|
+
|
|
453
|
+
始终可用,无需声明任何 capability。
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
nuxtblog.log.info('插件初始化完成')
|
|
457
|
+
nuxtblog.log.debug(`收到事件数据:${JSON.stringify(data)}`)
|
|
458
|
+
nuxtblog.log.warn('api_token 未配置,部分功能已禁用')
|
|
459
|
+
nuxtblog.log.error(`意外的响应状态:${res.status}`)
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## 声明式 Webhook
|
|
465
|
+
|
|
466
|
+
简单的出站通知可以完全通过清单配置,无需编写 JS。事件触发时,平台将事件 payload 以 JSON 格式 POST 到配置的 URL。
|
|
467
|
+
|
|
468
|
+
Webhook 异步触发,**绝不阻塞**原始请求。失败信息写入插件的错误环形缓冲区,不自动重试。
|
|
469
|
+
|
|
470
|
+
**严禁在清单中硬编码密钥。** 在 `url` 和 header 值中使用 `{{settings.key}}` 占位符,平台在派发时从管理员配置的参数中解析(30 秒缓存)。
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"plugin": {
|
|
475
|
+
"settings": [
|
|
476
|
+
{ "key": "webhook_url", "label": "Webhook URL", "type": "string", "required": true },
|
|
477
|
+
{ "key": "webhook_token", "label": "Webhook Token", "type": "password", "required": true }
|
|
478
|
+
],
|
|
479
|
+
"webhooks": [
|
|
480
|
+
{
|
|
481
|
+
"url": "{{settings.webhook_url}}",
|
|
482
|
+
"events": ["post.published", "comment.created"],
|
|
483
|
+
"headers": {
|
|
484
|
+
"Authorization": "Bearer {{settings.webhook_token}}",
|
|
485
|
+
"X-Source": "nuxtblog"
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"url": "https://example.com/static-endpoint",
|
|
490
|
+
"events": ["user.registered"]
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### `WebhookDef` 字段
|
|
498
|
+
|
|
499
|
+
| 字段 | 类型 | 说明 |
|
|
500
|
+
|---|---|---|
|
|
501
|
+
| `url` | string | POST 目标地址。支持 `{{settings.key}}` 插值 |
|
|
502
|
+
| `events` | string[] | 要匹配的事件名或事件模式 |
|
|
503
|
+
| `headers` | object | 附加 HTTP 请求头。值支持 `{{settings.key}}` 插值 |
|
|
504
|
+
|
|
505
|
+
### 事件匹配模式
|
|
506
|
+
|
|
507
|
+
| 模式 | 匹配范围 |
|
|
508
|
+
|---|---|
|
|
509
|
+
| `"post.published"` | 仅精确匹配 |
|
|
510
|
+
| `"post.*"` | 所有 `post.` 前缀事件:`post.created`、`post.updated` 等 |
|
|
511
|
+
| `"*"` | 所有事件 |
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## 声明式 Pipeline(多步流水线)
|
|
516
|
+
|
|
517
|
+
需要条件分支、重试和多步骤协调的复杂异步工作流,可以完全通过清单声明,无需编写调度逻辑。Pipeline 异步触发,不阻塞原始事件。
|
|
518
|
+
|
|
519
|
+
Pipeline `js` 步骤调用的 JS 函数必须在脚本模块作用域导出(顶层 `function` 声明)。
|
|
520
|
+
|
|
521
|
+
```json
|
|
522
|
+
{
|
|
523
|
+
"plugin": {
|
|
524
|
+
"capabilities": {
|
|
525
|
+
"http": { "allow": ["ai-api.example.com", "hooks.slack.com"] }
|
|
526
|
+
},
|
|
527
|
+
"pipelines": [
|
|
528
|
+
{
|
|
529
|
+
"name": "文章发布流水线",
|
|
530
|
+
"trigger": "post.published",
|
|
531
|
+
"steps": [
|
|
532
|
+
{
|
|
533
|
+
"type": "js",
|
|
534
|
+
"name": "AI 摘要生成",
|
|
535
|
+
"fn": "generateSummary",
|
|
536
|
+
"timeout_ms": 8000,
|
|
537
|
+
"retry": 1
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"type": "condition",
|
|
541
|
+
"name": "按文章类型分流",
|
|
542
|
+
"if": "ctx.data.post_type === 0",
|
|
543
|
+
"then": [
|
|
544
|
+
{ "type": "js", "name": "推送 Slack", "fn": "notifySlack" }
|
|
545
|
+
],
|
|
546
|
+
"else": [
|
|
547
|
+
{ "type": "webhook", "name": "页面 Webhook", "url": "https://hooks.example.com/pages" }
|
|
548
|
+
]
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
}
|
|
552
|
+
]
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
// src/index.ts — Pipeline 步骤调用的函数必须在模块顶层声明
|
|
559
|
+
|
|
560
|
+
function generateSummary(ctx: StepContext) {
|
|
561
|
+
const res = nuxtblog.http.fetch<{ summary: string }>('https://ai-api.example.com/summarize', {
|
|
562
|
+
method: 'POST',
|
|
563
|
+
body: { content: ctx.data.content as string },
|
|
564
|
+
})
|
|
565
|
+
if (res.ok) {
|
|
566
|
+
ctx.data.excerpt = res.body.summary // 传递给后续步骤
|
|
567
|
+
} else {
|
|
568
|
+
ctx.abort(`AI API 错误:HTTP ${res.status}`)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function notifySlack(ctx: StepContext) {
|
|
573
|
+
nuxtblog.http.fetch('https://hooks.slack.com/services/xxx', {
|
|
574
|
+
method: 'POST',
|
|
575
|
+
body: { text: `文章已发布:${ctx.data.title}` },
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### 步骤类型
|
|
581
|
+
|
|
582
|
+
| `type` | 说明 |
|
|
583
|
+
|---|---|
|
|
584
|
+
| `"js"` | 按名称调用导出的 JS 函数(`fn`)。支持 `timeout_ms` 和 `retry` |
|
|
585
|
+
| `"webhook"` | 将 `StepContext.data` 以 JSON POST 到 `url`。支持 `timeout_ms` 和 `retry` |
|
|
586
|
+
| `"condition"` | 对 `if` 中的 JS 布尔表达式求值,根据结果执行 `then` 或 `else` 分支 |
|
|
587
|
+
|
|
588
|
+
### `StepContext` 对象
|
|
589
|
+
|
|
590
|
+
| 属性 | 类型 | 说明 |
|
|
591
|
+
|---|---|---|
|
|
592
|
+
| `ctx.event` | string | 触发事件名 |
|
|
593
|
+
| `ctx.data` | object | 在所有步骤间流动的共享可变载荷 |
|
|
594
|
+
| `ctx.meta` | object | 步骤间的 KV 共享存储 |
|
|
595
|
+
| `ctx.abort(reason)` | function | 终止流水线,后续步骤被跳过 |
|
|
596
|
+
|
|
597
|
+
### 重试退避策略
|
|
598
|
+
|
|
599
|
+
当 `retry` > 0 时,失败的步骤按指数退避重试:
|
|
600
|
+
|
|
601
|
+
| 重试次数 | 等待时间 |
|
|
602
|
+
|---|---|
|
|
603
|
+
| 第 1 次 | 200 ms |
|
|
604
|
+
| 第 2 次 | 400 ms |
|
|
605
|
+
| 第 3 次 | 800 ms |
|
|
606
|
+
| … | 每次翻倍,最大上限 **8 秒** |
|
|
607
|
+
|
|
608
|
+
### 步骤默认值
|
|
609
|
+
|
|
610
|
+
| 属性 | 默认值 |
|
|
611
|
+
|---|---|
|
|
612
|
+
| `timeout_ms` | `5000`(5 秒) |
|
|
613
|
+
| `retry` | `0`(不重试) |
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## 所有事件列表
|
|
618
|
+
|
|
619
|
+
### fire-and-forget 事件(`nuxtblog.on`)
|
|
620
|
+
|
|
621
|
+
#### 文章(post)
|
|
622
|
+
|
|
623
|
+
| 事件 | 触发时机 | payload 字段 |
|
|
624
|
+
|---|---|---|
|
|
625
|
+
| `post.created` | 文章创建后 | `id, title, slug, excerpt, post_type, author_id, status` |
|
|
626
|
+
| `post.updated` | 文章更新后 | `id, title, slug, excerpt, post_type, author_id, status` |
|
|
627
|
+
| `post.published` | 文章 status 变为已发布后 | `id, title, slug, excerpt, post_type, author_id` |
|
|
628
|
+
| `post.deleted` | 文章删除/移入回收站后 | `id, title, slug, post_type, author_id` |
|
|
629
|
+
| `post.viewed` | 文章被浏览后 | `id, user_id` |
|
|
630
|
+
|
|
631
|
+
`post_type`:`0` = 文章,`1` = 页面
|
|
632
|
+
`status`:`0` = 草稿,`1` = 已发布,`2` = 回收站
|
|
633
|
+
|
|
634
|
+
#### 评论(comment)
|
|
635
|
+
|
|
636
|
+
| 事件 | 触发时机 | payload 字段 |
|
|
637
|
+
|---|---|---|
|
|
638
|
+
| `comment.created` | 评论提交后 | `id, status, object_type, object_id, object_title, object_slug, post_author_id, parent_id?, parent_author_id, author_id, author_name, author_email, content` |
|
|
639
|
+
| `comment.deleted` | 评论删除后 | `id, object_type, object_id` |
|
|
640
|
+
| `comment.status_changed` | 评论审核状态变更后 | `id, object_type, object_id, old_status, new_status, moderator_id` |
|
|
641
|
+
| `comment.approved` | 评论通过审核后 | `id, object_type, object_id, moderator_id` |
|
|
642
|
+
|
|
643
|
+
`status`:`0` = 待审核,`1` = 已通过,`2` = 垃圾
|
|
644
|
+
|
|
645
|
+
#### 用户(user)
|
|
646
|
+
|
|
647
|
+
| 事件 | 触发时机 | payload 字段 |
|
|
648
|
+
|---|---|---|
|
|
649
|
+
| `user.registered` | 用户注册后 | `id, username, email, display_name, locale, role` |
|
|
650
|
+
| `user.updated` | 用户信息更新后 | `id, username, email, display_name, locale, role, status` |
|
|
651
|
+
| `user.deleted` | 用户删除后 | `id, username, email` |
|
|
652
|
+
| `user.followed` | 用户关注他人后 | `follower_id, follower_name, follower_avatar, following_id` |
|
|
653
|
+
| `user.login` | 用户登录后 | `id, username, email, role` |
|
|
654
|
+
| `user.logout` | 用户退出后 | `id` |
|
|
655
|
+
|
|
656
|
+
`role`:`0` = 订阅者,`1` = 投稿者,`2` = 编辑,`3` = 管理员
|
|
657
|
+
`status`:`0` = 正常,`1` = 禁用
|
|
658
|
+
|
|
659
|
+
#### 媒体(media)
|
|
660
|
+
|
|
661
|
+
| 事件 | 触发时机 | payload 字段 |
|
|
662
|
+
|---|---|---|
|
|
663
|
+
| `media.uploaded` | 文件上传后 | `id, uploader_id, filename, mime_type, file_size, url, category, width, height` |
|
|
664
|
+
| `media.deleted` | 文件删除后 | `id, uploader_id, filename, mime_type, category` |
|
|
665
|
+
|
|
666
|
+
#### 分类 / 标签(taxonomy / term)
|
|
667
|
+
|
|
668
|
+
| 事件 | 触发时机 | payload 字段 |
|
|
669
|
+
|---|---|---|
|
|
670
|
+
| `taxonomy.created` | 分类/标签关联创建后 | `id, term_id, term_name, term_slug, taxonomy` |
|
|
671
|
+
| `taxonomy.deleted` | 分类/标签关联删除后 | `id, term_name, term_slug, taxonomy` |
|
|
672
|
+
| `term.created` | 词条创建后 | `id, name, slug` |
|
|
673
|
+
| `term.deleted` | 词条删除后 | `id, name, slug` |
|
|
674
|
+
|
|
675
|
+
#### 反应 / 签到
|
|
676
|
+
|
|
677
|
+
| 事件 | 触发时机 | payload 字段 |
|
|
678
|
+
|---|---|---|
|
|
679
|
+
| `reaction.added` | 点赞/收藏后 | `user_id, object_type, object_id, type` |
|
|
680
|
+
| `reaction.removed` | 取消点赞/收藏后 | `user_id, object_type, object_id, type` |
|
|
681
|
+
| `checkin.done` | 用户签到后 | `user_id, streak, already_checked_in` |
|
|
682
|
+
|
|
683
|
+
`type`:`"like"` 或 `"bookmark"`
|
|
684
|
+
|
|
685
|
+
#### 系统
|
|
686
|
+
|
|
687
|
+
| 事件 | 触发时机 | payload 字段 |
|
|
688
|
+
|---|---|---|
|
|
689
|
+
| `option.updated` | 站点配置项更改后 | `key, value` |
|
|
690
|
+
| `plugin.installed` | 插件安装后 | `id, title, version, author` |
|
|
691
|
+
| `plugin.uninstalled` | 插件卸载后 | `id` |
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
### filter 拦截事件(`nuxtblog.filter`)
|
|
696
|
+
|
|
697
|
+
| 事件 | 触发时机 | `ctx.data` 字段 | 备注 |
|
|
698
|
+
|---|---|---|---|
|
|
699
|
+
| `post.create` | 文章写入 DB 前 | `title, slug, content, excerpt, status` | |
|
|
700
|
+
| `post.update` | 文章更新写入 DB 前 | 仅包含本次变更的字段(Partial) | |
|
|
701
|
+
| `post.delete` | 文章删除前 | `id` | `abort()` 可取消删除 |
|
|
702
|
+
| `comment.create` | 评论写入 DB 前 | `content, author_name, author_email` | |
|
|
703
|
+
| `comment.delete` | 评论删除前 | `id` | `abort()` 可取消删除 |
|
|
704
|
+
| `term.create` | 词条写入 DB 前 | `name, slug` | |
|
|
705
|
+
| `user.register` | 用户注册写入 DB 前 | `username, email, display_name` | |
|
|
706
|
+
| `user.update` | 用户信息更新写入 DB 前 | 仅包含本次变更的字段(Partial) | |
|
|
707
|
+
| `media.upload` | 媒体元数据写入 DB 前 | `filename, mime_type, category, alt_text, title` | |
|
|
708
|
+
| `content.render` | 内容渲染给读者前 | `content` | 修改 `ctx.data.content` 改变读者看到的内容,不影响存储 |
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## 执行模型与并发
|
|
713
|
+
|
|
714
|
+
- 每个插件拥有**独立的 goja VM** 和**独立的互斥锁(mutex)**。所有 VM 操作(RunString、函数调用、ToValue 等)均须持锁执行。
|
|
715
|
+
- `nuxtblog.on` 的 handler 超时通过 `vm.Interrupt` 实现(goroutine 安全,无需持锁即可调用)。
|
|
716
|
+
- `nuxtblog.filter` 的 handler 超时同样通过 `vm.Interrupt` 实现,50 ms 上限是故意设计的,用于保证请求延迟可预期。
|
|
717
|
+
- 多个插件按 `priority` 升序执行。同优先级时按插件 ID 字母顺序排序。
|
|
718
|
+
- `inFilter` 是一个原子标志位,在 filter 链执行期间设为 `true`。`http.fetch` 检查此标志,在 filter 中被调用时返回错误对象(不 panic/throw)。
|
|
719
|
+
- Pipeline goroutine 和 Webhook goroutine 均为 fire-and-forget,绝不阻塞 `fanOut`。
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
## 超时与重试
|
|
724
|
+
|
|
725
|
+
| 场景 | 默认超时 | 是否可配置 |
|
|
726
|
+
|---|---|---|
|
|
727
|
+
| `nuxtblog.on` handler | 3 秒 | 计划支持(清单配置) |
|
|
728
|
+
| `nuxtblog.filter` handler | 50 ms | 计划支持(清单配置) |
|
|
729
|
+
| Pipeline `js` 步骤 | 5 秒 | `timeout_ms` 字段 |
|
|
730
|
+
| Pipeline `webhook` 步骤 | 5 秒 | `timeout_ms` 字段 |
|
|
731
|
+
| Pipeline `condition` 步骤 | 50 ms | 不可配置 |
|
|
732
|
+
| `http.fetch` 单次请求 | 15 秒 | `capabilities.http.timeout_ms` |
|
|
733
|
+
| 声明式 Webhook POST | 10 秒 | 不可配置 |
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## 可观测性
|
|
738
|
+
|
|
739
|
+
插件引擎通过服务端内部 API 暴露运行时指标。
|
|
740
|
+
|
|
741
|
+
### 执行统计(`GetStats`)
|
|
742
|
+
|
|
743
|
+
| 字段 | 说明 |
|
|
744
|
+
|---|---|
|
|
745
|
+
| `plugin_id` | 插件 ID |
|
|
746
|
+
| `invocations` | 累计执行次数(handler + filter) |
|
|
747
|
+
| `errors` | 累计错误次数 |
|
|
748
|
+
| `avg_duration_ms` | 平均执行时间(毫秒) |
|
|
749
|
+
| `last_error` | 最近一次错误信息 |
|
|
750
|
+
| `last_error_at` | 最近一次错误时间 |
|
|
751
|
+
|
|
752
|
+
### 滑动窗口历史(`GetHistory`)
|
|
753
|
+
|
|
754
|
+
60 个按分钟划分的时间桶,覆盖最近 1 小时。每个桶包含该分钟的 `invocations` 和 `errors`。无活动的时间桶返回零值,调用方始终得到精确 60 个数据点。
|
|
755
|
+
|
|
756
|
+
### 错误环形缓冲区(`GetErrors`)
|
|
757
|
+
|
|
758
|
+
保存最近 100 条错误记录,满后自动覆盖最旧的条目。每条记录包含:
|
|
759
|
+
|
|
760
|
+
| 字段 | 说明 |
|
|
761
|
+
|---|---|
|
|
762
|
+
| `at` | 错误发生时间 |
|
|
763
|
+
| `event` | 触发错误的事件名 |
|
|
764
|
+
| `message` | 错误信息 |
|
|
765
|
+
| `input_diff` | `ctx.data` 变更的 JSON diff(仅 filter 错误有此字段) |
|
|
766
|
+
|
|
767
|
+
diff 格式:`+key` = 新增,`-key` = 删除,`~key` = 变更(包含 `{ before, after }`)。
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
## 打包与安装
|
|
772
|
+
|
|
773
|
+
### 使用 esbuild 打包(推荐)
|
|
774
|
+
|
|
775
|
+
```bash
|
|
776
|
+
npm install -D esbuild
|
|
777
|
+
|
|
778
|
+
npx esbuild src/index.ts \
|
|
779
|
+
--bundle \
|
|
780
|
+
--platform=neutral \
|
|
781
|
+
--main-fields=browser,module,main \
|
|
782
|
+
--target=es2015 \
|
|
783
|
+
--outfile=index.js
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### 打包成压缩包
|
|
787
|
+
|
|
788
|
+
服务端使用 [mholt/archives](https://github.com/mholt/archives) 解包,支持以下格式:
|
|
789
|
+
|
|
790
|
+
| 格式 | 扩展名 |
|
|
791
|
+
|---|---|
|
|
792
|
+
| ZIP | `.zip` |
|
|
793
|
+
| Tar + Gzip | `.tar.gz` / `.tgz` |
|
|
794
|
+
| Tar + Bzip2 | `.tar.bz2` |
|
|
795
|
+
| Tar + XZ | `.tar.xz` |
|
|
796
|
+
| Tar + Zstd | `.tar.zst` |
|
|
797
|
+
| 7-Zip | `.7z` |
|
|
798
|
+
| RAR | `.rar` |
|
|
799
|
+
|
|
800
|
+
**ZIP(Python):**
|
|
801
|
+
|
|
802
|
+
```python
|
|
803
|
+
import zipfile
|
|
804
|
+
with zipfile.ZipFile("my-plugin.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
|
805
|
+
z.write("package.json")
|
|
806
|
+
z.write("index.js")
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
**tar.gz(Shell):**
|
|
810
|
+
|
|
811
|
+
```bash
|
|
812
|
+
tar -czf my-plugin.tar.gz package.json index.js
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
**PowerShell:**
|
|
816
|
+
|
|
817
|
+
```powershell
|
|
818
|
+
Compress-Archive -Path package.json, index.js -DestinationPath my-plugin.zip
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### 安装方式
|
|
822
|
+
|
|
823
|
+
1. **本地安装** — 管理后台 → 插件 → 安装插件 → 本地 ZIP → 上传
|
|
824
|
+
2. **GitHub 安装** — 管理后台 → 插件 → 安装插件 → GitHub → 填写 `owner/repo`(系统自动下载最新 Release 中名为 `plugin.zip` 的资源)
|
|
825
|
+
|
|
826
|
+
---
|
|
827
|
+
|
|
828
|
+
## TypeScript 支持
|
|
829
|
+
|
|
830
|
+
安装 SDK 包以获得 `nuxtblog` 全局对象的完整类型定义:
|
|
831
|
+
|
|
832
|
+
```bash
|
|
833
|
+
pnpm add -D @nuxtblog/plugin-sdk
|
|
834
|
+
# 或
|
|
835
|
+
npm install -D @nuxtblog/plugin-sdk
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**`tsconfig.json`:**
|
|
839
|
+
|
|
840
|
+
```json
|
|
841
|
+
{
|
|
842
|
+
"extends": "@nuxtblog/plugin-sdk",
|
|
843
|
+
"include": ["src"]
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
**或在入口文件顶部添加引用:**
|
|
848
|
+
|
|
849
|
+
```ts
|
|
850
|
+
/// <reference path="../../node_modules/@nuxtblog/plugin-sdk/index.d.ts" />
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
## License
|
|
856
|
+
|
|
857
|
+
MIT
|