@kne/fastify-live-components-site 0.1.9 → 0.1.10
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/libs/controllers/entry.js +167 -123
- package/libs/controllers/site.js +60 -19
- package/libs/schema/response.js +50 -0
- package/package.json +1 -1
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
2
|
const { allowCors, handleOptions, normalizeHooks } = require('../utils/cors');
|
|
3
|
+
const { str, bool, any, list, obj, success, PERMISSION, SITE_STATUS, nodeBase, treeNode, contentShare } = require('../schema/response');
|
|
4
|
+
|
|
5
|
+
const TAG_SITE = 'Live组件站点-站点信息';
|
|
6
|
+
const TAG_AI = 'Live组件站点-AI';
|
|
7
|
+
const TAG_FILE = 'Live组件站点-文件';
|
|
8
|
+
const TAG_SHARE = 'Live组件站点-内容分享';
|
|
9
|
+
|
|
10
|
+
const ENTRY_HINT = '通过站点短码 shorten 鉴权(站点须存在且为开启状态,关闭返回 403,短码无效返回 401),允许任意 Origin 跨域';
|
|
11
|
+
|
|
12
|
+
const shortenParams = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
required: ['shorten'],
|
|
15
|
+
properties: {
|
|
16
|
+
shorten: { type: 'string', description: '站点公开访问短码(不区分大小写)' }
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const PARENT_ID = '父文件夹节点 ID,为空表示根目录;父节点须为可写文件夹';
|
|
21
|
+
|
|
22
|
+
const fileNode = obj('文件节点', nodeBase);
|
|
3
23
|
|
|
4
24
|
module.exports = fp(async (fastify, options) => {
|
|
5
25
|
const { services } = fastify[options.name];
|
|
@@ -28,14 +48,17 @@ module.exports = fp(async (fastify, options) => {
|
|
|
28
48
|
{
|
|
29
49
|
onRequest: allowCors,
|
|
30
50
|
schema: {
|
|
51
|
+
tags: [TAG_SHARE],
|
|
31
52
|
summary: '通过内容短码直出已保存 content(text/plain,无 code/data;不带站点 shorten)',
|
|
53
|
+
description: '公开访问,无需站点短码;短码无效或已过期返回 401,所属站点关闭返回 403。允许任意 Origin 跨域',
|
|
32
54
|
params: {
|
|
33
55
|
type: 'object',
|
|
34
56
|
required: ['contentShorten'],
|
|
35
57
|
properties: {
|
|
36
|
-
contentShorten: { type: 'string' }
|
|
58
|
+
contentShorten: { type: 'string', description: '内容分享短码(不区分大小写)' }
|
|
37
59
|
}
|
|
38
|
-
}
|
|
60
|
+
},
|
|
61
|
+
response: success('文件已保存的内容原文,Content-Type 为 text/plain; charset=utf-8', { type: 'string' })
|
|
39
62
|
}
|
|
40
63
|
},
|
|
41
64
|
async (request, reply) => {
|
|
@@ -52,14 +75,20 @@ module.exports = fp(async (fastify, options) => {
|
|
|
52
75
|
{
|
|
53
76
|
onRequest: withEntryAuth,
|
|
54
77
|
schema: {
|
|
78
|
+
tags: [TAG_SITE],
|
|
55
79
|
summary: '站点信息(含是否支持 AI)',
|
|
56
|
-
|
|
80
|
+
description: ENTRY_HINT,
|
|
81
|
+
params: shortenParams,
|
|
82
|
+
response: success('站点公开信息', {
|
|
57
83
|
type: 'object',
|
|
58
|
-
|
|
84
|
+
additionalProperties: true,
|
|
59
85
|
properties: {
|
|
60
|
-
|
|
86
|
+
name: str('站点名称'),
|
|
87
|
+
status: str(SITE_STATUS),
|
|
88
|
+
defaultPermission: str(`新建节点默认${PERMISSION}`),
|
|
89
|
+
aiEnabled: bool('是否可用 AI 生成:插件配置了 ai.getAIModel 且站点 options.aiEnabled 不为 false')
|
|
61
90
|
}
|
|
62
|
-
}
|
|
91
|
+
})
|
|
63
92
|
}
|
|
64
93
|
},
|
|
65
94
|
async request => {
|
|
@@ -72,26 +101,38 @@ module.exports = fp(async (fastify, options) => {
|
|
|
72
101
|
{
|
|
73
102
|
onRequest: withEntryAuth,
|
|
74
103
|
schema: {
|
|
104
|
+
tags: [TAG_AI],
|
|
75
105
|
summary: '创建 AI 流式任务,返回 streamToken',
|
|
76
|
-
|
|
106
|
+
description: `${ENTRY_HINT}。站点未开启 AI 返回 403;refine 模式 messages 不能为空。任务暂存于内存,2 分钟内需用 streamToken 调用 ai/stream 消费`,
|
|
107
|
+
params: shortenParams,
|
|
108
|
+
body: {
|
|
77
109
|
type: 'object',
|
|
78
|
-
required: ['shorten'],
|
|
79
110
|
properties: {
|
|
80
|
-
|
|
111
|
+
mode: { type: 'string', description: '任务模式:generate 生成代码,其它值按 refine(完善需求对话)处理' },
|
|
112
|
+
messages: { type: 'array', description: '对话历史,元素形如 { role: user|assistant, content },其它 role 或空 content 会被忽略' },
|
|
113
|
+
content: { type: 'string', description: '编辑器当前整份源码' },
|
|
114
|
+
scope: { type: 'object', description: '编辑器当前 scope(可用变量/组件声明)' },
|
|
115
|
+
props: { description: '编辑器当前组件参数,形如 { name: { type, defaultValue } };非对象时忽略' },
|
|
116
|
+
libs: { type: 'array', description: '可用依赖库标识列表,元素会转为字符串' },
|
|
117
|
+
remotes: { type: 'array', description: '可用远程组件包列表,用于在 stream 阶段拉取组件文档索引' },
|
|
118
|
+
selection: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
description: '编辑器选中片段;传入时 AI 只针对选中部分',
|
|
121
|
+
properties: {
|
|
122
|
+
code: { description: '选中片段源码' },
|
|
123
|
+
startLine: { description: '选中片段起始行' },
|
|
124
|
+
label: { description: '选中片段名称,用于提示' }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
81
127
|
}
|
|
82
128
|
},
|
|
83
|
-
|
|
129
|
+
response: success('任务已创建', {
|
|
84
130
|
type: 'object',
|
|
131
|
+
additionalProperties: true,
|
|
85
132
|
properties: {
|
|
86
|
-
|
|
87
|
-
messages: { type: 'array' },
|
|
88
|
-
content: { type: 'string' },
|
|
89
|
-
scope: { type: 'object' },
|
|
90
|
-
libs: { type: 'array' },
|
|
91
|
-
remotes: { type: 'array' },
|
|
92
|
-
selection: { type: 'object' }
|
|
133
|
+
streamToken: str('一次性流式任务令牌,传给 ai/stream 的 token')
|
|
93
134
|
}
|
|
94
|
-
}
|
|
135
|
+
})
|
|
95
136
|
}
|
|
96
137
|
},
|
|
97
138
|
async request => {
|
|
@@ -104,20 +145,19 @@ module.exports = fp(async (fastify, options) => {
|
|
|
104
145
|
{
|
|
105
146
|
onRequest: withEntryAuth,
|
|
106
147
|
schema: {
|
|
148
|
+
tags: [TAG_AI],
|
|
107
149
|
summary: 'SSE 推送 AI 流式输出',
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
required: ['shorten'],
|
|
111
|
-
properties: {
|
|
112
|
-
shorten: { type: 'string' }
|
|
113
|
-
}
|
|
114
|
-
},
|
|
150
|
+
description: `${ENTRY_HINT}。token 只能消费一次,无效或过期返回 404;成功后以 text/event-stream 推送 data: {...} 事件直到结束`,
|
|
151
|
+
params: shortenParams,
|
|
115
152
|
query: {
|
|
116
153
|
type: 'object',
|
|
117
154
|
required: ['token'],
|
|
118
155
|
properties: {
|
|
119
|
-
token: { type: 'string' }
|
|
156
|
+
token: { type: 'string', description: 'ai/start 返回的 streamToken' }
|
|
120
157
|
}
|
|
158
|
+
},
|
|
159
|
+
response: {
|
|
160
|
+
200: { description: 'text/event-stream 事件流,每条为 data: JSON;出错时推送 { text: 错误信息, done: true } 后结束' }
|
|
121
161
|
}
|
|
122
162
|
}
|
|
123
163
|
},
|
|
@@ -131,14 +171,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
131
171
|
{
|
|
132
172
|
onRequest: withEntryAuth,
|
|
133
173
|
schema: {
|
|
174
|
+
tags: [TAG_FILE],
|
|
134
175
|
summary: '获取站点文件树',
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
properties: {
|
|
139
|
-
shorten: { type: 'string' }
|
|
140
|
-
}
|
|
141
|
-
}
|
|
176
|
+
description: `${ENTRY_HINT}。同级按类型(文件夹在前)、名称排序`,
|
|
177
|
+
params: shortenParams,
|
|
178
|
+
response: success('根节点列表,文件夹带 children', { type: 'array', items: treeNode })
|
|
142
179
|
}
|
|
143
180
|
},
|
|
144
181
|
async request => {
|
|
@@ -151,21 +188,27 @@ module.exports = fp(async (fastify, options) => {
|
|
|
151
188
|
{
|
|
152
189
|
onRequest: withEntryAuth,
|
|
153
190
|
schema: {
|
|
191
|
+
tags: [TAG_FILE],
|
|
154
192
|
summary: '读取文件内容',
|
|
155
|
-
|
|
193
|
+
description: `${ENTRY_HINT}。节点不存在返回 404,非文件返回 400`,
|
|
194
|
+
params: shortenParams,
|
|
195
|
+
query: {
|
|
156
196
|
type: 'object',
|
|
157
|
-
required: ['
|
|
197
|
+
required: ['id'],
|
|
158
198
|
properties: {
|
|
159
|
-
|
|
199
|
+
id: { type: 'string', description: '文件节点 ID' }
|
|
160
200
|
}
|
|
161
201
|
},
|
|
162
|
-
|
|
202
|
+
response: success('文件内容', {
|
|
163
203
|
type: 'object',
|
|
164
|
-
|
|
204
|
+
additionalProperties: true,
|
|
165
205
|
properties: {
|
|
166
|
-
id:
|
|
206
|
+
id: any('节点 ID(字符串)'),
|
|
207
|
+
name: str('文件名'),
|
|
208
|
+
permission: str(PERMISSION),
|
|
209
|
+
content: str('文件内容,未保存过为空字符串')
|
|
167
210
|
}
|
|
168
|
-
}
|
|
211
|
+
})
|
|
169
212
|
}
|
|
170
213
|
},
|
|
171
214
|
async request => {
|
|
@@ -178,22 +221,19 @@ module.exports = fp(async (fastify, options) => {
|
|
|
178
221
|
{
|
|
179
222
|
onRequest: withEntryAuth,
|
|
180
223
|
schema: {
|
|
224
|
+
tags: [TAG_SHARE],
|
|
181
225
|
summary: '创建文件内容分享短链(可设有效期)',
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
required: ['shorten'],
|
|
185
|
-
properties: {
|
|
186
|
-
shorten: { type: 'string' }
|
|
187
|
-
}
|
|
188
|
-
},
|
|
226
|
+
description: `${ENTRY_HINT}。只能分享文件;生成的短链通过 content/:contentShorten 公开访问`,
|
|
227
|
+
params: shortenParams,
|
|
189
228
|
body: {
|
|
190
229
|
type: 'object',
|
|
191
230
|
required: ['id'],
|
|
192
231
|
properties: {
|
|
193
|
-
id: { type: 'string' },
|
|
194
|
-
expiresIn: { type: ['number', 'null'] }
|
|
232
|
+
id: { type: 'string', description: '文件节点 ID' },
|
|
233
|
+
expiresIn: { type: ['number', 'null'], description: '有效期(秒),不传、null 或不大于 0 表示永久有效' }
|
|
195
234
|
}
|
|
196
|
-
}
|
|
235
|
+
},
|
|
236
|
+
response: success('创建的分享短链', contentShare)
|
|
197
237
|
}
|
|
198
238
|
},
|
|
199
239
|
async request => {
|
|
@@ -206,21 +246,18 @@ module.exports = fp(async (fastify, options) => {
|
|
|
206
246
|
{
|
|
207
247
|
onRequest: withEntryAuth,
|
|
208
248
|
schema: {
|
|
249
|
+
tags: [TAG_SHARE],
|
|
209
250
|
summary: '列出文件已有内容分享短链',
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
required: ['shorten'],
|
|
213
|
-
properties: {
|
|
214
|
-
shorten: { type: 'string' }
|
|
215
|
-
}
|
|
216
|
-
},
|
|
251
|
+
description: `${ENTRY_HINT}。按创建时间倒序;已过期的分享会在查询时被删除并移除短码,不出现在结果中`,
|
|
252
|
+
params: shortenParams,
|
|
217
253
|
query: {
|
|
218
254
|
type: 'object',
|
|
219
255
|
required: ['id'],
|
|
220
256
|
properties: {
|
|
221
|
-
id: { type: 'string' }
|
|
257
|
+
id: { type: 'string', description: '文件节点 ID' }
|
|
222
258
|
}
|
|
223
|
-
}
|
|
259
|
+
},
|
|
260
|
+
response: success('未过期的分享短链列表', { type: 'array', items: contentShare })
|
|
224
261
|
}
|
|
225
262
|
},
|
|
226
263
|
async request => {
|
|
@@ -233,21 +270,24 @@ module.exports = fp(async (fastify, options) => {
|
|
|
233
270
|
{
|
|
234
271
|
onRequest: withEntryAuth,
|
|
235
272
|
schema: {
|
|
273
|
+
tags: [TAG_SHARE],
|
|
236
274
|
summary: '删除文件内容分享短链',
|
|
237
|
-
|
|
275
|
+
description: `${ENTRY_HINT}。删除分享记录并移除内容短码;分享不存在返回 404`,
|
|
276
|
+
params: shortenParams,
|
|
277
|
+
body: {
|
|
238
278
|
type: 'object',
|
|
239
279
|
required: ['shorten'],
|
|
240
280
|
properties: {
|
|
241
|
-
shorten: { type: 'string' }
|
|
281
|
+
shorten: { type: 'string', description: '要删除的内容分享短码(不区分大小写)' }
|
|
242
282
|
}
|
|
243
283
|
},
|
|
244
|
-
|
|
284
|
+
response: success('删除成功', {
|
|
245
285
|
type: 'object',
|
|
246
|
-
|
|
286
|
+
additionalProperties: true,
|
|
247
287
|
properties: {
|
|
248
|
-
shorten:
|
|
288
|
+
shorten: str('已删除的内容短码(大写)')
|
|
249
289
|
}
|
|
250
|
-
}
|
|
290
|
+
})
|
|
251
291
|
}
|
|
252
292
|
},
|
|
253
293
|
async request => {
|
|
@@ -260,22 +300,19 @@ module.exports = fp(async (fastify, options) => {
|
|
|
260
300
|
{
|
|
261
301
|
onRequest: withEntryAuth,
|
|
262
302
|
schema: {
|
|
303
|
+
tags: [TAG_FILE],
|
|
263
304
|
summary: '创建文件夹',
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
required: ['shorten'],
|
|
267
|
-
properties: {
|
|
268
|
-
shorten: { type: 'string' }
|
|
269
|
-
}
|
|
270
|
-
},
|
|
305
|
+
description: `${ENTRY_HINT}。同级重名返回 400 DUPLICATE_NAME;新节点权限取站点 defaultPermission`,
|
|
306
|
+
params: shortenParams,
|
|
271
307
|
body: {
|
|
272
308
|
type: 'object',
|
|
273
309
|
required: ['name'],
|
|
274
310
|
properties: {
|
|
275
|
-
parentId: { type: ['string', 'null'] },
|
|
276
|
-
name: { type: 'string' }
|
|
311
|
+
parentId: { type: ['string', 'null'], description: PARENT_ID },
|
|
312
|
+
name: { type: 'string', description: '文件夹名称,去除首尾空格后不能为空' }
|
|
277
313
|
}
|
|
278
|
-
}
|
|
314
|
+
},
|
|
315
|
+
response: success('创建的文件夹', obj('文件夹节点', Object.assign({}, nodeBase, { children: list('子节点,新建时为空数组', obj('子节点')) })))
|
|
279
316
|
}
|
|
280
317
|
},
|
|
281
318
|
async request => {
|
|
@@ -288,23 +325,20 @@ module.exports = fp(async (fastify, options) => {
|
|
|
288
325
|
{
|
|
289
326
|
onRequest: withEntryAuth,
|
|
290
327
|
schema: {
|
|
328
|
+
tags: [TAG_FILE],
|
|
291
329
|
summary: '创建文件',
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
required: ['shorten'],
|
|
295
|
-
properties: {
|
|
296
|
-
shorten: { type: 'string' }
|
|
297
|
-
}
|
|
298
|
-
},
|
|
330
|
+
description: `${ENTRY_HINT}。同级重名返回 400 DUPLICATE_NAME;新节点权限取站点 defaultPermission`,
|
|
331
|
+
params: shortenParams,
|
|
299
332
|
body: {
|
|
300
333
|
type: 'object',
|
|
301
334
|
required: ['name'],
|
|
302
335
|
properties: {
|
|
303
|
-
parentId: { type: ['string', 'null'] },
|
|
304
|
-
name: { type: 'string' },
|
|
305
|
-
content: { type: 'string' }
|
|
336
|
+
parentId: { type: ['string', 'null'], description: PARENT_ID },
|
|
337
|
+
name: { type: 'string', description: '文件名称,去除首尾空格后不能为空' },
|
|
338
|
+
content: { type: 'string', description: '初始文件内容,默认空字符串' }
|
|
306
339
|
}
|
|
307
|
-
}
|
|
340
|
+
},
|
|
341
|
+
response: success('创建的文件', fileNode)
|
|
308
342
|
}
|
|
309
343
|
},
|
|
310
344
|
async request => {
|
|
@@ -317,22 +351,26 @@ module.exports = fp(async (fastify, options) => {
|
|
|
317
351
|
{
|
|
318
352
|
onRequest: withEntryAuth,
|
|
319
353
|
schema: {
|
|
354
|
+
tags: [TAG_FILE],
|
|
320
355
|
summary: '保存文件内容',
|
|
321
|
-
|
|
356
|
+
description: `${ENTRY_HINT}。只读节点返回 403,非文件返回 400`,
|
|
357
|
+
params: shortenParams,
|
|
358
|
+
body: {
|
|
322
359
|
type: 'object',
|
|
323
|
-
required: ['
|
|
360
|
+
required: ['id'],
|
|
324
361
|
properties: {
|
|
325
|
-
|
|
362
|
+
id: { type: 'string', description: '文件节点 ID' },
|
|
363
|
+
content: { type: 'string', description: '新的文件内容,不传保存为空字符串' }
|
|
326
364
|
}
|
|
327
365
|
},
|
|
328
|
-
|
|
366
|
+
response: success('保存后的文件内容', {
|
|
329
367
|
type: 'object',
|
|
330
|
-
|
|
368
|
+
additionalProperties: true,
|
|
331
369
|
properties: {
|
|
332
|
-
id:
|
|
333
|
-
content:
|
|
370
|
+
id: any('节点 ID(字符串)'),
|
|
371
|
+
content: str('保存后的文件内容')
|
|
334
372
|
}
|
|
335
|
-
}
|
|
373
|
+
})
|
|
336
374
|
}
|
|
337
375
|
},
|
|
338
376
|
async request => {
|
|
@@ -345,22 +383,19 @@ module.exports = fp(async (fastify, options) => {
|
|
|
345
383
|
{
|
|
346
384
|
onRequest: withEntryAuth,
|
|
347
385
|
schema: {
|
|
386
|
+
tags: [TAG_FILE],
|
|
348
387
|
summary: '重命名节点',
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
required: ['shorten'],
|
|
352
|
-
properties: {
|
|
353
|
-
shorten: { type: 'string' }
|
|
354
|
-
}
|
|
355
|
-
},
|
|
388
|
+
description: `${ENTRY_HINT}。只读节点返回 403,同级重名返回 400 DUPLICATE_NAME`,
|
|
389
|
+
params: shortenParams,
|
|
356
390
|
body: {
|
|
357
391
|
type: 'object',
|
|
358
392
|
required: ['id', 'name'],
|
|
359
393
|
properties: {
|
|
360
|
-
id: { type: 'string' },
|
|
361
|
-
name: { type: 'string' }
|
|
394
|
+
id: { type: 'string', description: '文件或文件夹节点 ID' },
|
|
395
|
+
name: { type: 'string', description: '新名称,去除首尾空格后不能为空' }
|
|
362
396
|
}
|
|
363
|
-
}
|
|
397
|
+
},
|
|
398
|
+
response: success('重命名后的节点', fileNode)
|
|
364
399
|
}
|
|
365
400
|
},
|
|
366
401
|
async request => {
|
|
@@ -373,22 +408,27 @@ module.exports = fp(async (fastify, options) => {
|
|
|
373
408
|
{
|
|
374
409
|
onRequest: withEntryAuth,
|
|
375
410
|
schema: {
|
|
411
|
+
tags: [TAG_FILE],
|
|
376
412
|
summary: '移动文件或文件夹到目标目录',
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
required: ['shorten'],
|
|
380
|
-
properties: {
|
|
381
|
-
shorten: { type: 'string' }
|
|
382
|
-
}
|
|
383
|
-
},
|
|
413
|
+
description: `${ENTRY_HINT}。目标与当前目录相同、移动到自身或自身子目录返回 400 INVALID_MOVE_TARGET;目标目录下重名返回 400 DUPLICATE_NAME`,
|
|
414
|
+
params: shortenParams,
|
|
384
415
|
body: {
|
|
385
416
|
type: 'object',
|
|
386
417
|
required: ['id'],
|
|
387
418
|
properties: {
|
|
388
|
-
id: { type: 'string' },
|
|
389
|
-
parentId: { type: ['string', 'null'] }
|
|
419
|
+
id: { type: 'string', description: '要移动的节点 ID' },
|
|
420
|
+
parentId: { type: ['string', 'null'], description: '目标文件夹节点 ID,为空表示移动到根目录' }
|
|
390
421
|
}
|
|
391
|
-
}
|
|
422
|
+
},
|
|
423
|
+
response: success(
|
|
424
|
+
'移动后的节点',
|
|
425
|
+
obj(
|
|
426
|
+
'节点',
|
|
427
|
+
Object.assign({}, nodeBase, {
|
|
428
|
+
parentId: any('新的父节点 ID,根目录为 null')
|
|
429
|
+
})
|
|
430
|
+
)
|
|
431
|
+
)
|
|
392
432
|
}
|
|
393
433
|
},
|
|
394
434
|
async request => {
|
|
@@ -401,24 +441,28 @@ module.exports = fp(async (fastify, options) => {
|
|
|
401
441
|
{
|
|
402
442
|
onRequest: withEntryAuth,
|
|
403
443
|
schema: {
|
|
444
|
+
tags: [TAG_FILE],
|
|
404
445
|
summary: '删除节点',
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
required: ['shorten'],
|
|
408
|
-
properties: {
|
|
409
|
-
shorten: { type: 'string' }
|
|
410
|
-
}
|
|
411
|
-
},
|
|
446
|
+
description: `${ENTRY_HINT}。按顺序逐个删除,遇到只读节点(403)或非空文件夹(400)即中止,已删除的不回滚;删除文件时一并删除其内容分享短链`,
|
|
447
|
+
params: shortenParams,
|
|
412
448
|
body: {
|
|
413
449
|
type: 'object',
|
|
414
450
|
required: ['ids'],
|
|
415
451
|
properties: {
|
|
416
452
|
ids: {
|
|
417
453
|
type: 'array',
|
|
418
|
-
items: { type: 'string' }
|
|
454
|
+
items: { type: 'string' },
|
|
455
|
+
description: '要删除的节点 ID 列表'
|
|
419
456
|
}
|
|
420
457
|
}
|
|
421
|
-
}
|
|
458
|
+
},
|
|
459
|
+
response: success('删除成功', {
|
|
460
|
+
type: 'object',
|
|
461
|
+
additionalProperties: true,
|
|
462
|
+
properties: {
|
|
463
|
+
ids: list('已删除的节点 ID 列表', any('节点 ID(字符串)'))
|
|
464
|
+
}
|
|
465
|
+
})
|
|
422
466
|
}
|
|
423
467
|
},
|
|
424
468
|
async request => {
|
package/libs/controllers/site.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
+
const { num, list, success, empty, PERMISSION, SITE_STATUS, site } = require('../schema/response');
|
|
3
|
+
|
|
4
|
+
const TAG = 'Live组件站点管理后台-站点';
|
|
5
|
+
|
|
6
|
+
const siteOptions = {
|
|
7
|
+
type: 'object',
|
|
8
|
+
description: '站点扩展配置(JSON),原样保存',
|
|
9
|
+
properties: {
|
|
10
|
+
aiEnabled: { description: '是否开启 AI 生成,传 false 关闭;未传视为开启(需同时配置插件 ai.getAIModel)' }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
2
13
|
|
|
3
14
|
module.exports = fp(async (fastify, options) => {
|
|
4
15
|
const { services } = fastify[options.name];
|
|
@@ -8,15 +19,33 @@ module.exports = fp(async (fastify, options) => {
|
|
|
8
19
|
{
|
|
9
20
|
onRequest: options.getAuthenticate('site'),
|
|
10
21
|
schema: {
|
|
22
|
+
tags: [TAG],
|
|
11
23
|
summary: '站点列表',
|
|
24
|
+
description: '分页查询站点,按创建时间倒序;可按状态与名称关键字筛选',
|
|
12
25
|
query: {
|
|
13
26
|
type: 'object',
|
|
14
27
|
properties: {
|
|
15
|
-
filter: {
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
filter: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
default: {},
|
|
31
|
+
description: '筛选条件,如 filter[status]=open',
|
|
32
|
+
properties: {
|
|
33
|
+
status: { description: `按状态筛选,${SITE_STATUS};其它值忽略` },
|
|
34
|
+
keyword: { description: '按站点名称模糊匹配' }
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
perPage: { type: 'number', default: 20, description: '每页条数' },
|
|
38
|
+
currentPage: { type: 'number', default: 1, description: '当前页码,从 1 开始' }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
response: success('站点分页列表', {
|
|
42
|
+
type: 'object',
|
|
43
|
+
additionalProperties: true,
|
|
44
|
+
properties: {
|
|
45
|
+
pageData: list('当前页站点列表', site),
|
|
46
|
+
totalCount: num('符合条件的站点总数')
|
|
18
47
|
}
|
|
19
|
-
}
|
|
48
|
+
})
|
|
20
49
|
}
|
|
21
50
|
},
|
|
22
51
|
async request => {
|
|
@@ -29,14 +58,17 @@ module.exports = fp(async (fastify, options) => {
|
|
|
29
58
|
{
|
|
30
59
|
onRequest: options.getAuthenticate('site'),
|
|
31
60
|
schema: {
|
|
61
|
+
tags: [TAG],
|
|
32
62
|
summary: '站点详情',
|
|
63
|
+
description: '按 ID 获取站点信息,含公开入口地址 host;站点不存在返回 404',
|
|
33
64
|
query: {
|
|
34
65
|
type: 'object',
|
|
35
66
|
required: ['id'],
|
|
36
67
|
properties: {
|
|
37
|
-
id: { type: 'string' }
|
|
68
|
+
id: { type: 'string', description: '站点 ID' }
|
|
38
69
|
}
|
|
39
|
-
}
|
|
70
|
+
},
|
|
71
|
+
response: success('站点详情', site)
|
|
40
72
|
}
|
|
41
73
|
},
|
|
42
74
|
async request => {
|
|
@@ -49,17 +81,20 @@ module.exports = fp(async (fastify, options) => {
|
|
|
49
81
|
{
|
|
50
82
|
onRequest: options.getAuthenticate('site:create'),
|
|
51
83
|
schema: {
|
|
84
|
+
tags: [TAG],
|
|
52
85
|
summary: '创建站点并签发 shorten 链接',
|
|
86
|
+
description: '创建站点后通过 shorten 服务签发站点公开访问短码,返回带 host 的站点信息',
|
|
53
87
|
body: {
|
|
54
88
|
type: 'object',
|
|
55
89
|
required: ['name'],
|
|
56
90
|
properties: {
|
|
57
|
-
name: { type: 'string' },
|
|
58
|
-
defaultPermission: { type: 'string', enum: ['r', 'rw'] },
|
|
59
|
-
status: { type: 'string', enum: ['open', 'closed'] },
|
|
60
|
-
options:
|
|
91
|
+
name: { type: 'string', description: '站点名称,去除首尾空格后不能为空' },
|
|
92
|
+
defaultPermission: { type: 'string', enum: ['r', 'rw'], description: `新建节点默认${PERMISSION};默认 rw` },
|
|
93
|
+
status: { type: 'string', enum: ['open', 'closed'], description: `${SITE_STATUS};默认 open` },
|
|
94
|
+
options: siteOptions
|
|
61
95
|
}
|
|
62
|
-
}
|
|
96
|
+
},
|
|
97
|
+
response: success('创建后的站点', site)
|
|
63
98
|
}
|
|
64
99
|
},
|
|
65
100
|
async request => {
|
|
@@ -72,18 +107,21 @@ module.exports = fp(async (fastify, options) => {
|
|
|
72
107
|
{
|
|
73
108
|
onRequest: options.getAuthenticate('site:manage'),
|
|
74
109
|
schema: {
|
|
110
|
+
tags: [TAG],
|
|
75
111
|
summary: '修改站点(含开启/关闭)',
|
|
112
|
+
description: '只更新传入的字段;shorten、时间字段不可修改。关闭后站点公开入口与内容短链返回 403,重新开启后沿用原 host',
|
|
76
113
|
body: {
|
|
77
114
|
type: 'object',
|
|
78
115
|
required: ['id'],
|
|
79
116
|
properties: {
|
|
80
|
-
id: { type: 'string' },
|
|
81
|
-
name: { type: 'string' },
|
|
82
|
-
status: { type: 'string', enum: ['open', 'closed'] },
|
|
83
|
-
defaultPermission: { type: 'string', enum: ['r', 'rw'] },
|
|
84
|
-
options: {
|
|
117
|
+
id: { type: 'string', description: '站点 ID' },
|
|
118
|
+
name: { type: 'string', description: '站点名称,传入时去除首尾空格后不能为空' },
|
|
119
|
+
status: { type: 'string', enum: ['open', 'closed'], description: SITE_STATUS },
|
|
120
|
+
defaultPermission: { type: 'string', enum: ['r', 'rw'], description: `新建节点默认${PERMISSION}` },
|
|
121
|
+
options: Object.assign({}, siteOptions, { description: '站点扩展配置(JSON),整体覆盖原值' })
|
|
85
122
|
}
|
|
86
|
-
}
|
|
123
|
+
},
|
|
124
|
+
response: empty('保存成功')
|
|
87
125
|
}
|
|
88
126
|
},
|
|
89
127
|
async request => {
|
|
@@ -97,14 +135,17 @@ module.exports = fp(async (fastify, options) => {
|
|
|
97
135
|
{
|
|
98
136
|
onRequest: options.getAuthenticate('site:manage'),
|
|
99
137
|
schema: {
|
|
138
|
+
tags: [TAG],
|
|
100
139
|
summary: '删除站点',
|
|
140
|
+
description: '删除站点及其全部文件节点、内容分享记录,并移除站点短码与内容短码',
|
|
101
141
|
body: {
|
|
102
142
|
type: 'object',
|
|
103
143
|
required: ['id'],
|
|
104
144
|
properties: {
|
|
105
|
-
id: { type: 'string' }
|
|
145
|
+
id: { type: 'string', description: '站点 ID' }
|
|
106
146
|
}
|
|
107
|
-
}
|
|
147
|
+
},
|
|
148
|
+
response: empty('删除成功')
|
|
108
149
|
}
|
|
109
150
|
},
|
|
110
151
|
async request => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// 响应 schema 会参与 fast-json-stringify 序列化:对象一律 additionalProperties: true 以免裁掉未声明字段;
|
|
2
|
+
// 用 nullable 而非类型数组(类型数组遇到不匹配的值会直接 500),类型不确定的字段(如 id、日期、JSON 列)只写说明不声明类型
|
|
3
|
+
const str = description => ({ type: 'string', nullable: true, description });
|
|
4
|
+
const num = description => ({ type: 'number', nullable: true, description });
|
|
5
|
+
const bool = description => ({ type: 'boolean', nullable: true, description });
|
|
6
|
+
const any = description => ({ description });
|
|
7
|
+
const list = (description, items) => ({ type: 'array', nullable: true, description, items });
|
|
8
|
+
const obj = (description, properties = {}) => ({ type: 'object', nullable: true, description, additionalProperties: true, properties });
|
|
9
|
+
|
|
10
|
+
const success = (description, schema) => ({
|
|
11
|
+
200: Object.assign({}, schema || { type: 'object' }, { description })
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const empty = (description = '操作成功,返回空对象') => success(description);
|
|
15
|
+
|
|
16
|
+
const PERMISSION = '权限:r 只读,rw 可读写';
|
|
17
|
+
const SITE_STATUS = '站点状态:open 开启,closed 关闭';
|
|
18
|
+
|
|
19
|
+
const site = obj('站点', {
|
|
20
|
+
id: any('站点 ID(字符串)'),
|
|
21
|
+
name: str('站点名称'),
|
|
22
|
+
shorten: str('站点公开访问短码'),
|
|
23
|
+
host: str('站点公开入口地址,由 getSiteHost(shorten) 生成,默认 {prefix}/{shorten}'),
|
|
24
|
+
status: str(SITE_STATUS),
|
|
25
|
+
defaultPermission: str(`新建节点默认${PERMISSION}`),
|
|
26
|
+
options: any('站点扩展配置(JSON),如 aiEnabled: false 关闭 AI'),
|
|
27
|
+
createdAt: any('创建时间'),
|
|
28
|
+
updatedAt: any('更新时间'),
|
|
29
|
+
deletedAt: any('删除时间,未删除为 null')
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const nodeBase = {
|
|
33
|
+
id: any('节点 ID(字符串)'),
|
|
34
|
+
name: str('节点名称'),
|
|
35
|
+
type: str('节点类型:file 文件,directory 文件夹'),
|
|
36
|
+
permission: str(PERMISSION)
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const treeNode = obj('文件树节点', Object.assign({}, nodeBase, { children: list('子节点列表(仅文件夹有),结构同本节点', obj('子节点')) }));
|
|
40
|
+
|
|
41
|
+
const contentShare = obj('内容分享短链', {
|
|
42
|
+
id: any('分享记录 ID(字符串)'),
|
|
43
|
+
nodeId: any('文件节点 ID(字符串)'),
|
|
44
|
+
shorten: str('内容短码(大写)'),
|
|
45
|
+
expires: any('过期时间,null 表示永久有效'),
|
|
46
|
+
url: str('内容访问地址:{getContentHost()}/content/{shorten},不带站点短码'),
|
|
47
|
+
createdAt: any('创建时间')
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
module.exports = { str, num, bool, any, list, obj, success, empty, PERMISSION, SITE_STATUS, site, nodeBase, treeNode, contentShare };
|