skill-base 2.0.4 → 2.0.7
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 +177 -115
- package/bin/skill-base.js +29 -3
- package/package.json +4 -1
- package/src/cappy.js +416 -0
- package/src/database.js +11 -0
- package/src/index.js +125 -25
- package/src/middleware/auth.js +96 -32
- package/src/routes/auth.js +1 -1
- package/src/routes/skills.js +10 -5
- package/src/utils/zip.js +15 -4
- package/static/android-chrome-192x192.png +0 -0
- package/static/android-chrome-512x512.png +0 -0
- package/static/apple-touch-icon.png +0 -0
- package/static/assets/index-BkwByEEp.css +1 -0
- package/static/assets/index-CB4Diul3.js +209 -0
- package/static/favicon-16x16.png +0 -0
- package/static/favicon-32x32.png +0 -0
- package/static/favicon.ico +0 -0
- package/static/favicon.svg +14 -0
- package/static/index.html +18 -248
- package/static/site.webmanifest +1 -0
- package/static/admin/users.html +0 -593
- package/static/cli-code.html +0 -203
- package/static/css/.gitkeep +0 -0
- package/static/css/style.css +0 -1567
- package/static/diff.html +0 -466
- package/static/file.html +0 -443
- package/static/js/.gitkeep +0 -0
- package/static/js/admin/users.js +0 -346
- package/static/js/app.js +0 -508
- package/static/js/auth.js +0 -151
- package/static/js/cli-code.js +0 -184
- package/static/js/collaborators.js +0 -283
- package/static/js/diff.js +0 -540
- package/static/js/file.js +0 -619
- package/static/js/i18n.js +0 -739
- package/static/js/index.js +0 -168
- package/static/js/publish.js +0 -718
- package/static/js/settings.js +0 -124
- package/static/js/setup.js +0 -157
- package/static/js/skill.js +0 -808
- package/static/login.html +0 -82
- package/static/publish.html +0 -459
- package/static/settings.html +0 -163
- package/static/setup.html +0 -101
- package/static/skill.html +0 -851
package/src/cappy.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
|
|
3
|
+
class CappyMascot {
|
|
4
|
+
constructor(port, basePath = '/') {
|
|
5
|
+
this.port = port;
|
|
6
|
+
this.basePath = basePath;
|
|
7
|
+
this.frameTimer = null;
|
|
8
|
+
this.sceneTimer = null;
|
|
9
|
+
this.lastRenderHeight = 0;
|
|
10
|
+
this.currentSceneKey = null;
|
|
11
|
+
this.isRunning = false;
|
|
12
|
+
this.isStopped = false;
|
|
13
|
+
this.cursorHidden = false;
|
|
14
|
+
|
|
15
|
+
this.colors = {
|
|
16
|
+
warm: '\x1b[38;5;221m',
|
|
17
|
+
orange: '\x1b[38;5;214m',
|
|
18
|
+
cyan: '\x1b[38;5;117m',
|
|
19
|
+
pink: '\x1b[38;5;218m',
|
|
20
|
+
soft: '\x1b[38;5;188m',
|
|
21
|
+
reset: '\x1b[0m'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// 数据很简单:scene 决定人格,frame 决定动作,message 决定台词。
|
|
25
|
+
this.scenes = {
|
|
26
|
+
intro: {
|
|
27
|
+
frameDelay: 240,
|
|
28
|
+
loops: 1,
|
|
29
|
+
messages: [
|
|
30
|
+
`Skill Base 正在热机,端口 ${port} 已点亮。`
|
|
31
|
+
],
|
|
32
|
+
frames: [
|
|
33
|
+
{ color: 'warm', sprite: this.createSprite('o o', '___', 'paw') },
|
|
34
|
+
{ color: 'orange', sprite: this.createSprite('- -', '___', 'paw') },
|
|
35
|
+
{ color: 'warm', sprite: this.createSprite('^ ^', '___', 'still') }
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
idle: {
|
|
39
|
+
weight: 5,
|
|
40
|
+
frameDelay: 320,
|
|
41
|
+
loops: 4,
|
|
42
|
+
messages: [
|
|
43
|
+
`Cappy 正在看着 ${port} 号端口发呆。`,
|
|
44
|
+
'一切正常。没有过度设计,就没有运行时焦虑。',
|
|
45
|
+
'技能仓库很安静,直白的代码才能带来这种安宁。',
|
|
46
|
+
'系统稳定。Cappy 鄙视无谓的复杂度。'
|
|
47
|
+
],
|
|
48
|
+
frames: [
|
|
49
|
+
{ color: 'warm', sprite: this.createSprite('o o', '___', 'still') },
|
|
50
|
+
{ color: 'warm', sprite: this.createSprite('o o', '___', 'breath') },
|
|
51
|
+
{ color: 'warm', sprite: this.createSprite('o o', '___', 'still') }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
blink: {
|
|
55
|
+
weight: 2,
|
|
56
|
+
frameDelay: 180,
|
|
57
|
+
loops: 2,
|
|
58
|
+
messages: [
|
|
59
|
+
'缓慢地眨了下眼。不是摸鱼,是在进行低成本巡检。',
|
|
60
|
+
'与其写一堆监控脚本,不如把代码写得简单点。'
|
|
61
|
+
],
|
|
62
|
+
frames: [
|
|
63
|
+
{ color: 'warm', sprite: this.createSprite('o o', '___', 'still') },
|
|
64
|
+
{ color: 'warm', sprite: this.createSprite('- -', '___', 'still') },
|
|
65
|
+
{ color: 'warm', sprite: this.createSprite('o o', '___', 'still') }
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
think: {
|
|
69
|
+
weight: 1,
|
|
70
|
+
frameDelay: 280,
|
|
71
|
+
loops: 3,
|
|
72
|
+
messages: [
|
|
73
|
+
'简单的架构才是最好的。真正的稳定性是不需要花哨设计的。',
|
|
74
|
+
'思考中。直接写代码,比写那些自作聪明的抽象层靠谱多了。'
|
|
75
|
+
],
|
|
76
|
+
frames: [
|
|
77
|
+
{ color: 'cyan', sprite: this.createSprite('o o', '___', 'think-left') },
|
|
78
|
+
{ color: 'cyan', sprite: this.createSprite('^ ^', '___', 'think-mid') },
|
|
79
|
+
{ color: 'cyan', sprite: this.createSprite('o o', '___', 'think-right') }
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
soak: {
|
|
83
|
+
weight: 1,
|
|
84
|
+
frameDelay: 340,
|
|
85
|
+
loops: 3,
|
|
86
|
+
messages: [
|
|
87
|
+
'数据结构对了,逻辑自然就像水一样顺畅。',
|
|
88
|
+
'泡一下就想明白了。别去猜未来的需求,YAGNI。'
|
|
89
|
+
],
|
|
90
|
+
frames: [
|
|
91
|
+
{ color: 'pink', sprite: this.createSprite('^ ^', '~~~', 'steam-left') },
|
|
92
|
+
{ color: 'pink', sprite: this.createSprite('- -', '~~~', 'steam-mid') },
|
|
93
|
+
{ color: 'pink', sprite: this.createSprite('^ ^', '~~~', 'steam-right') }
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
scout: {
|
|
97
|
+
weight: 1,
|
|
98
|
+
frameDelay: 220,
|
|
99
|
+
loops: 4,
|
|
100
|
+
messages: [
|
|
101
|
+
'短距离散步。确认没有被哪个聪明人搞出过度设计。',
|
|
102
|
+
'没有多余步骤,只有必要移动。代码也该如此。'
|
|
103
|
+
],
|
|
104
|
+
frames: [
|
|
105
|
+
{ color: 'cyan', sprite: this.createSprite('o o', '___', 'step-left') },
|
|
106
|
+
{ color: 'cyan', sprite: this.createSprite('o o', '___', 'step-mid') },
|
|
107
|
+
{ color: 'cyan', sprite: this.createSprite('o o', '___', 'step-right') }
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
work: {
|
|
111
|
+
frameDelay: 180,
|
|
112
|
+
loops: 6,
|
|
113
|
+
messages: [
|
|
114
|
+
'收到任务,Cappy 正在用最直接的方式处理。'
|
|
115
|
+
],
|
|
116
|
+
frames: [
|
|
117
|
+
{ color: 'cyan', sprite: this.createSprite('> <', '===', 'spark-left') },
|
|
118
|
+
{ color: 'orange', sprite: this.createSprite('> <', '===', 'spark-right') }
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.boundStop = this.stop.bind(this);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
start() {
|
|
127
|
+
if (this.isRunning) return;
|
|
128
|
+
|
|
129
|
+
this.isRunning = true;
|
|
130
|
+
this.isStopped = false;
|
|
131
|
+
this.hideCursor();
|
|
132
|
+
process.once('SIGINT', this.boundStop);
|
|
133
|
+
process.once('SIGTERM', this.boundStop);
|
|
134
|
+
|
|
135
|
+
this.playScene('intro', {
|
|
136
|
+
onDone: () => this.scheduleNextIdle(600)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
action(message) {
|
|
141
|
+
if (!this.isRunning || this.isStopped) return;
|
|
142
|
+
|
|
143
|
+
this.playScene('work', {
|
|
144
|
+
message: message || '有新动作发生了,但卡皮巴拉依然很稳。',
|
|
145
|
+
onDone: () => this.scheduleNextIdle(600)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
stop() {
|
|
150
|
+
if (this.isStopped) return;
|
|
151
|
+
|
|
152
|
+
this.isStopped = true;
|
|
153
|
+
this.isRunning = false;
|
|
154
|
+
clearInterval(this.frameTimer);
|
|
155
|
+
clearTimeout(this.sceneTimer);
|
|
156
|
+
|
|
157
|
+
this.clearRender();
|
|
158
|
+
const goodbye = [
|
|
159
|
+
`${this.colors.soft} ╭──────────────────────────────╮${this.colors.reset}`,
|
|
160
|
+
`${this.colors.soft} │ Cappy 下班了,明天继续值守。 │${this.colors.reset}`,
|
|
161
|
+
`${this.colors.soft} ╰──────────────┬───────────────╯${this.colors.reset}`,
|
|
162
|
+
`${this.colors.warm} \\${this.colors.reset}`,
|
|
163
|
+
`${this.colors.warm} __${this.colors.reset}`,
|
|
164
|
+
`${this.colors.warm} ___( ; ;)___${this.colors.reset}`,
|
|
165
|
+
`${this.colors.warm} /__ _\\${this.colors.reset}`,
|
|
166
|
+
`${this.colors.warm} /_/\\_\\${this.colors.reset}`
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
process.stdout.write(`${goodbye.join('\n')}\n`);
|
|
170
|
+
this.showCursor();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
scheduleNextIdle(delay = this.randomBetween(1400, 2800)) {
|
|
175
|
+
clearTimeout(this.sceneTimer);
|
|
176
|
+
this.sceneTimer = setTimeout(() => {
|
|
177
|
+
const nextSceneKey = this.pickIdleScene();
|
|
178
|
+
this.playScene(nextSceneKey, {
|
|
179
|
+
onDone: () => this.scheduleNextIdle()
|
|
180
|
+
});
|
|
181
|
+
}, delay);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
playScene(sceneKey, options = {}) {
|
|
185
|
+
const scene = this.scenes[sceneKey];
|
|
186
|
+
if (!scene || this.isStopped) return;
|
|
187
|
+
|
|
188
|
+
clearInterval(this.frameTimer);
|
|
189
|
+
clearTimeout(this.sceneTimer);
|
|
190
|
+
|
|
191
|
+
this.currentSceneKey = sceneKey;
|
|
192
|
+
|
|
193
|
+
const frames = scene.frames;
|
|
194
|
+
const frameDelay = scene.frameDelay || 240;
|
|
195
|
+
const totalLoops = options.loops || scene.loops || 1;
|
|
196
|
+
const message = options.message || this.pick(scene.messages);
|
|
197
|
+
let index = 0;
|
|
198
|
+
let remainingLoops = totalLoops;
|
|
199
|
+
|
|
200
|
+
const tick = () => {
|
|
201
|
+
const frame = frames[index];
|
|
202
|
+
this.render(frame, message);
|
|
203
|
+
index += 1;
|
|
204
|
+
|
|
205
|
+
if (index >= frames.length) {
|
|
206
|
+
index = 0;
|
|
207
|
+
remainingLoops -= 1;
|
|
208
|
+
|
|
209
|
+
if (remainingLoops <= 0) {
|
|
210
|
+
clearInterval(this.frameTimer);
|
|
211
|
+
this.frameTimer = null;
|
|
212
|
+
|
|
213
|
+
if (typeof options.onDone === 'function') {
|
|
214
|
+
options.onDone();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
tick();
|
|
221
|
+
this.frameTimer = setInterval(tick, frameDelay);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
render(frame, message) {
|
|
225
|
+
const lines = [
|
|
226
|
+
...this.buildBubble(message),
|
|
227
|
+
...frame.sprite.map((line) => ` ${this.colors[frame.color]}${line}${this.colors.reset}`),
|
|
228
|
+
` ${this.colors.soft}http://localhost:${this.port}${this.basePath} | Cappy on duty${this.colors.reset}`
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
this.clearRender();
|
|
232
|
+
process.stdout.write(lines.join('\n'));
|
|
233
|
+
this.lastRenderHeight = lines.length;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
clearRender() {
|
|
237
|
+
if (!this.lastRenderHeight) return;
|
|
238
|
+
|
|
239
|
+
readline.cursorTo(process.stdout, 0);
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < this.lastRenderHeight; i += 1) {
|
|
242
|
+
readline.clearLine(process.stdout, 0);
|
|
243
|
+
|
|
244
|
+
if (i < this.lastRenderHeight - 1) {
|
|
245
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
readline.cursorTo(process.stdout, 0);
|
|
250
|
+
this.lastRenderHeight = 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
buildBubble(message) {
|
|
254
|
+
const text = this.fit(message, 34);
|
|
255
|
+
const width = text.length;
|
|
256
|
+
const line = '─'.repeat(width + 2);
|
|
257
|
+
|
|
258
|
+
return [
|
|
259
|
+
`${this.colors.soft} ╭${line}╮${this.colors.reset}`,
|
|
260
|
+
`${this.colors.soft} │ ${text} │${this.colors.reset}`,
|
|
261
|
+
`${this.colors.soft} ╰${line}╯${this.colors.reset}`,
|
|
262
|
+
`${this.colors.soft} \\${this.colors.reset}`
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
pickIdleScene() {
|
|
267
|
+
const entries = Object.entries(this.scenes)
|
|
268
|
+
.filter(([key, scene]) => scene.weight)
|
|
269
|
+
.filter(([key]) => key !== this.currentSceneKey);
|
|
270
|
+
|
|
271
|
+
const totalWeight = entries.reduce((sum, [, scene]) => sum + scene.weight, 0);
|
|
272
|
+
let cursor = Math.random() * totalWeight;
|
|
273
|
+
|
|
274
|
+
for (const [key, scene] of entries) {
|
|
275
|
+
cursor -= scene.weight;
|
|
276
|
+
if (cursor <= 0) return key;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return 'idle';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
pick(list) {
|
|
283
|
+
return list[Math.floor(Math.random() * list.length)];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fit(text, width) {
|
|
287
|
+
if (text.length <= width) {
|
|
288
|
+
return text.padEnd(width, ' ');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return `${text.slice(0, width - 1)}…`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
randomBetween(min, max) {
|
|
295
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
hideCursor() {
|
|
299
|
+
if (this.cursorHidden) return;
|
|
300
|
+
process.stdout.write('\x1B[?25l');
|
|
301
|
+
this.cursorHidden = true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
showCursor() {
|
|
305
|
+
if (!this.cursorHidden) return;
|
|
306
|
+
process.stdout.write('\x1B[?25h');
|
|
307
|
+
this.cursorHidden = false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
createSprite(eyes, waterline, pose) {
|
|
311
|
+
const variants = {
|
|
312
|
+
still: [
|
|
313
|
+
' __',
|
|
314
|
+
` _____/ \\`,
|
|
315
|
+
` _( ${eyes} )__`,
|
|
316
|
+
`/__ __\\`,
|
|
317
|
+
` ${waterline} /_/ \\_\\`
|
|
318
|
+
],
|
|
319
|
+
breath: [
|
|
320
|
+
' __',
|
|
321
|
+
` ______/ \\`,
|
|
322
|
+
` _( ${eyes} )__`,
|
|
323
|
+
`/__ __\\`,
|
|
324
|
+
` ${waterline} /_/ \\_\\`
|
|
325
|
+
],
|
|
326
|
+
paw: [
|
|
327
|
+
' __',
|
|
328
|
+
` _____/ \\`,
|
|
329
|
+
` _( ${eyes} )__`,
|
|
330
|
+
`/__ __ __\\`,
|
|
331
|
+
` ${waterline} /_/ \\_\\`
|
|
332
|
+
],
|
|
333
|
+
'orange-left': [
|
|
334
|
+
' 🍊 __',
|
|
335
|
+
` _____/ \\`,
|
|
336
|
+
` _( ${eyes} )__`,
|
|
337
|
+
`/__ __\\`,
|
|
338
|
+
` ${waterline} /_/ \\_\\`
|
|
339
|
+
],
|
|
340
|
+
'orange-mid': [
|
|
341
|
+
' __',
|
|
342
|
+
` ___🍊_/ \\`,
|
|
343
|
+
` _( ${eyes} )__`,
|
|
344
|
+
`/__ __\\`,
|
|
345
|
+
` ${waterline} /_/ \\_\\`
|
|
346
|
+
],
|
|
347
|
+
'orange-right': [
|
|
348
|
+
' __ 🍊',
|
|
349
|
+
` _____/ \\`,
|
|
350
|
+
` _( ${eyes} )__`,
|
|
351
|
+
`/__ __\\`,
|
|
352
|
+
` ${waterline} /_/ \\_\\`
|
|
353
|
+
],
|
|
354
|
+
'steam-left': [
|
|
355
|
+
' ♨️ ♨️',
|
|
356
|
+
` _____/ \\`,
|
|
357
|
+
` _( ${eyes} )__`,
|
|
358
|
+
`/__ __\\`,
|
|
359
|
+
` ${waterline} /_/ \\_\\`
|
|
360
|
+
],
|
|
361
|
+
'steam-mid': [
|
|
362
|
+
' ♨️ ♨️',
|
|
363
|
+
` _____/ \\`,
|
|
364
|
+
` _( ${eyes} )__`,
|
|
365
|
+
`/__ __\\`,
|
|
366
|
+
` ${waterline} /_/ \\_\\`
|
|
367
|
+
],
|
|
368
|
+
'steam-right': [
|
|
369
|
+
' ♨️ ♨️',
|
|
370
|
+
` _____/ \\`,
|
|
371
|
+
` _( ${eyes} )__`,
|
|
372
|
+
`/__ __\\`,
|
|
373
|
+
` ${waterline} /_/ \\_\\`
|
|
374
|
+
],
|
|
375
|
+
'step-left': [
|
|
376
|
+
' __',
|
|
377
|
+
` _____/ \\`,
|
|
378
|
+
`(_ ${eyes} )__`,
|
|
379
|
+
` /__ __\\`,
|
|
380
|
+
` ${waterline} /_/ \\_\\`
|
|
381
|
+
],
|
|
382
|
+
'step-mid': [
|
|
383
|
+
' __',
|
|
384
|
+
` _____/ \\`,
|
|
385
|
+
` _( ${eyes} )__`,
|
|
386
|
+
`/__ __\\`,
|
|
387
|
+
` ${waterline} /_/ \\_\\`
|
|
388
|
+
],
|
|
389
|
+
'step-right': [
|
|
390
|
+
' __',
|
|
391
|
+
` _____/ \\`,
|
|
392
|
+
` _( ${eyes} )__`,
|
|
393
|
+
` /__ __\\`,
|
|
394
|
+
` ${waterline} /_/ \\_\\`
|
|
395
|
+
],
|
|
396
|
+
'spark-left': [
|
|
397
|
+
' ⚡ __',
|
|
398
|
+
` _____/ \\`,
|
|
399
|
+
` _( ${eyes} )__`,
|
|
400
|
+
`/__ __ __\\`,
|
|
401
|
+
` ${waterline} /_/ \\_\\`
|
|
402
|
+
],
|
|
403
|
+
'spark-right': [
|
|
404
|
+
' __ ⚡',
|
|
405
|
+
` _____/ \\`,
|
|
406
|
+
` _( ${eyes} )__`,
|
|
407
|
+
`/__ __ __\\`,
|
|
408
|
+
` ${waterline} /_/ \\_\\`
|
|
409
|
+
]
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return variants[pose] || variants.still;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
module.exports = CappyMascot;
|
package/src/database.js
CHANGED
|
@@ -85,12 +85,23 @@ CREATE TABLE IF NOT EXISTS skill_collaborators (
|
|
|
85
85
|
UNIQUE(skill_id, user_id)
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
+
-- Session 表(可选,通过 SESSION_STORE=sqlite 启用)
|
|
89
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
90
|
+
session_id TEXT PRIMARY KEY,
|
|
91
|
+
user_id INTEGER NOT NULL,
|
|
92
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
93
|
+
expires_at DATETIME NOT NULL,
|
|
94
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
95
|
+
);
|
|
96
|
+
|
|
88
97
|
-- 索引
|
|
89
98
|
CREATE INDEX IF NOT EXISTS idx_versions_skill_id ON skill_versions(skill_id);
|
|
90
99
|
CREATE INDEX IF NOT EXISTS idx_cli_codes_user ON cli_auth_codes(user_id);
|
|
91
100
|
CREATE INDEX IF NOT EXISTS idx_pat_tokens_user ON personal_access_tokens(user_id);
|
|
92
101
|
CREATE INDEX IF NOT EXISTS idx_collaborators_skill ON skill_collaborators(skill_id);
|
|
93
102
|
CREATE INDEX IF NOT EXISTS idx_collaborators_user ON skill_collaborators(user_id);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
94
105
|
`;
|
|
95
106
|
|
|
96
107
|
// 执行建表语句
|
package/src/index.js
CHANGED
|
@@ -1,22 +1,59 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
1
2
|
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const isDebug = process.env.DEBUG === 'true';
|
|
5
|
+
|
|
2
6
|
const fastify = require('fastify')({
|
|
3
|
-
logger:
|
|
7
|
+
logger: isDebug,
|
|
4
8
|
// 设置 body 大小限制为 100MB(支持大 zip 上传)
|
|
5
9
|
bodyLimit: 100 * 1024 * 1024
|
|
6
10
|
});
|
|
11
|
+
const CappyMascot = require('./cappy');
|
|
12
|
+
|
|
13
|
+
if (isDebug) {
|
|
14
|
+
console.log('DEBUG: Debug mode is enabled.');
|
|
15
|
+
console.log('DEBUG: PORT:', process.env.PORT);
|
|
16
|
+
console.log('DEBUG: HOST:', process.env.HOST);
|
|
17
|
+
console.log('DEBUG: APP_BASE_PATH:', process.env.APP_BASE_PATH);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 1. 规范化部署前缀 (APP_BASE_PATH)
|
|
21
|
+
let APP_BASE_PATH = process.env.APP_BASE_PATH || '/';
|
|
22
|
+
// 确保以 / 开头,以 / 结尾
|
|
23
|
+
if (!APP_BASE_PATH.startsWith('/')) APP_BASE_PATH = '/' + APP_BASE_PATH;
|
|
24
|
+
if (!APP_BASE_PATH.endsWith('/')) APP_BASE_PATH = APP_BASE_PATH + '/';
|
|
25
|
+
// 将多个连续斜杠替换为单个
|
|
26
|
+
APP_BASE_PATH = APP_BASE_PATH.replace(/\/+/g, '/');
|
|
27
|
+
|
|
28
|
+
const STATIC_ROOT = path.join(__dirname, '../static');
|
|
29
|
+
const INDEX_HTML_PATH = path.join(STATIC_ROOT, 'index.html');
|
|
30
|
+
|
|
31
|
+
function renderSpaHtml() {
|
|
32
|
+
const html = fs.readFileSync(INDEX_HTML_PATH, 'utf8');
|
|
33
|
+
const baseTag = ` <base href="${APP_BASE_PATH}">`;
|
|
34
|
+
|
|
35
|
+
if (html.includes('<base href=')) {
|
|
36
|
+
return html.replace(/<base href="[^"]*">/, baseTag.trim());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return html.replace('<head>', `<head>\n${baseTag}`);
|
|
40
|
+
}
|
|
7
41
|
|
|
8
42
|
// 主启动函数
|
|
9
43
|
async function start() {
|
|
10
44
|
try {
|
|
45
|
+
if (isDebug) console.log('DEBUG: Registering core plugins...');
|
|
11
46
|
// 1. 注册插件
|
|
12
47
|
// @fastify/cors — 允许跨域
|
|
13
48
|
await fastify.register(require('@fastify/cors'), {
|
|
14
49
|
origin: true,
|
|
15
50
|
credentials: true
|
|
16
51
|
});
|
|
52
|
+
if (isDebug) console.log('DEBUG: Registered @fastify/cors');
|
|
17
53
|
|
|
18
54
|
// @fastify/cookie — Cookie 支持
|
|
19
55
|
await fastify.register(require('@fastify/cookie'));
|
|
56
|
+
if (isDebug) console.log('DEBUG: Registered @fastify/cookie');
|
|
20
57
|
|
|
21
58
|
// @fastify/multipart — 文件上传支持
|
|
22
59
|
await fastify.register(require('@fastify/multipart'), {
|
|
@@ -24,50 +61,76 @@ async function start() {
|
|
|
24
61
|
fileSize: 100 * 1024 * 1024 // 100MB
|
|
25
62
|
}
|
|
26
63
|
});
|
|
64
|
+
if (isDebug) console.log('DEBUG: Registered @fastify/multipart');
|
|
65
|
+
|
|
66
|
+
// 必须在 static 之前:index:false 时「目录 + 尾斜杠」会走 send 的 403,不会落到 notFoundHandler
|
|
67
|
+
fastify.route({
|
|
68
|
+
method: ['GET', 'HEAD'],
|
|
69
|
+
url: APP_BASE_PATH,
|
|
70
|
+
async handler(request, reply) {
|
|
71
|
+
reply.type('text/html; charset=utf-8');
|
|
72
|
+
if (request.method === 'HEAD') {
|
|
73
|
+
return reply.send();
|
|
74
|
+
}
|
|
75
|
+
return reply.send(renderSpaHtml());
|
|
76
|
+
}
|
|
77
|
+
});
|
|
27
78
|
|
|
28
79
|
// @fastify/static — 静态文件服务(指向 static/ 目录)
|
|
29
80
|
await fastify.register(require('@fastify/static'), {
|
|
30
|
-
root:
|
|
31
|
-
prefix:
|
|
81
|
+
root: STATIC_ROOT,
|
|
82
|
+
prefix: APP_BASE_PATH,
|
|
83
|
+
wildcard: true,
|
|
84
|
+
index: false
|
|
32
85
|
});
|
|
86
|
+
if (isDebug) console.log('DEBUG: Registered @fastify/static at', STATIC_ROOT);
|
|
33
87
|
|
|
34
88
|
// 2. 注册自定义中间件
|
|
89
|
+
if (isDebug) console.log('DEBUG: Registering custom middlewares...');
|
|
35
90
|
// 错误处理
|
|
36
91
|
await fastify.register(require('./middleware/error'));
|
|
37
92
|
// 认证(注册 authenticate、createSession 等装饰器)
|
|
38
93
|
await fastify.register(require('./middleware/auth'));
|
|
39
94
|
// 管理员权限(注册 requireAdmin 装饰器)
|
|
40
95
|
await fastify.register(require('./middleware/admin'));
|
|
96
|
+
if (isDebug) console.log('DEBUG: Custom middlewares registered.');
|
|
97
|
+
|
|
98
|
+
// 3. 注册 API 路由
|
|
99
|
+
const API_PREFIX = (APP_BASE_PATH + 'api/v1').replace(/\/+/g, '/');
|
|
100
|
+
if (isDebug) console.log('DEBUG: Registering API routes with prefix:', API_PREFIX);
|
|
41
101
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await fastify.register(require('./routes/
|
|
48
|
-
await fastify.register(require('./routes/
|
|
102
|
+
// 健康检查接口
|
|
103
|
+
fastify.get(`${API_PREFIX}/health`, async () => {
|
|
104
|
+
return { status: 'ok', timestamp: new Date().toISOString() };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await fastify.register(require('./routes/init'), { prefix: `${API_PREFIX}/init` });
|
|
108
|
+
await fastify.register(require('./routes/auth'), { prefix: `${API_PREFIX}/auth` });
|
|
109
|
+
await fastify.register(require('./routes/skills'), { prefix: `${API_PREFIX}/skills` });
|
|
110
|
+
await fastify.register(require('./routes/publish'), { prefix: `${API_PREFIX}/skills` });
|
|
111
|
+
await fastify.register(require('./routes/collaborators'), { prefix: `${API_PREFIX}/skills` });
|
|
112
|
+
await fastify.register(require('./routes/users'), { prefix: `${API_PREFIX}/users` });
|
|
113
|
+
if (isDebug) console.log('DEBUG: API routes registered.');
|
|
49
114
|
|
|
50
115
|
// 4. 页面路由 fallback(SPA 风格路由支持)
|
|
51
116
|
fastify.setNotFoundHandler(async (request, reply) => {
|
|
117
|
+
const requestPath = request.url.split('?')[0];
|
|
118
|
+
|
|
52
119
|
// API 路由返回 JSON 404
|
|
53
|
-
if (
|
|
120
|
+
if (requestPath.startsWith(API_PREFIX)) {
|
|
54
121
|
return reply.code(404).send({ detail: 'Not found' });
|
|
55
122
|
}
|
|
56
123
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (url === '/admin/users') return reply.sendFile('admin/users.html');
|
|
65
|
-
if (url.match(/^\/skill\/[^/]+\/file\//)) return reply.sendFile('file.html');
|
|
66
|
-
if (url.match(/^\/skill\/[^/]+\/diff/)) return reply.sendFile('diff.html');
|
|
67
|
-
if (url.match(/^\/skill\/[^/]+$/)) return reply.sendFile('skill.html');
|
|
124
|
+
// 已知静态资源缺失时直接返回 404,别把 HTML 假装成 JS/CSS
|
|
125
|
+
if (
|
|
126
|
+
requestPath.startsWith(`${APP_BASE_PATH}assets/`) ||
|
|
127
|
+
requestPath === `${APP_BASE_PATH}favicon.ico`
|
|
128
|
+
) {
|
|
129
|
+
return reply.code(404).send({ detail: 'Not found' });
|
|
130
|
+
}
|
|
68
131
|
|
|
69
|
-
//
|
|
70
|
-
return reply.
|
|
132
|
+
// 所有非 API 请求回退到入口 HTML,由前端 Vue Router 处理
|
|
133
|
+
return reply.type('text/html; charset=utf-8').send(renderSpaHtml());
|
|
71
134
|
});
|
|
72
135
|
|
|
73
136
|
// 5. 确保数据库已初始化
|
|
@@ -76,9 +139,46 @@ async function start() {
|
|
|
76
139
|
// 6. 启动服务
|
|
77
140
|
const PORT = process.env.PORT || 8000;
|
|
78
141
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
142
|
+
|
|
143
|
+
// 默认禁用 Cappy,除非显式设为 'true'
|
|
144
|
+
const enableCappy = process.env.ENABLE_CAPPY === 'true';
|
|
145
|
+
let cappy = null;
|
|
146
|
+
|
|
147
|
+
if (enableCappy) {
|
|
148
|
+
if (isDebug) console.log('DEBUG: CappyMascot is enabled.');
|
|
149
|
+
// 初始化 Cappy 水豚(必须在 listen 之前注册装饰器)
|
|
150
|
+
cappy = new CappyMascot(PORT, APP_BASE_PATH);
|
|
151
|
+
fastify.decorate('cappy', cappy);
|
|
152
|
+
|
|
153
|
+
// 优雅解耦:通过 Fastify 的全局生命周期钩子来驱动 Cappy 动画,完全不污染业务路由
|
|
154
|
+
fastify.addHook('onResponse', (request, reply, done) => {
|
|
155
|
+
// 只有成功请求才触发,不理会报错
|
|
156
|
+
if (reply.statusCode >= 200 && reply.statusCode < 300) {
|
|
157
|
+
const method = request.method;
|
|
158
|
+
const url = request.url.split('?')[0];
|
|
159
|
+
|
|
160
|
+
if (method === 'POST' && url === `${API_PREFIX}/users`) {
|
|
161
|
+
cappy.action('新用户被添加了。又多了一个打工人,系统依旧稳定。');
|
|
162
|
+
} else if (method === 'POST' && url === `${API_PREFIX}/skills/publish`) {
|
|
163
|
+
cappy.action('有新的 Skill/版本 发布了。希望它的代码没有过度设计。');
|
|
164
|
+
} else if (method === 'GET' && url.match(new RegExp(`^${API_PREFIX}/skills/[^/]+/versions/[^/]+/download/?$`))) {
|
|
165
|
+
cappy.action('有人拉取了 Skill。代码开始流通,Cappy 觉得很赞。');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
done();
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
if (isDebug) console.log('DEBUG: CappyMascot is disabled.');
|
|
172
|
+
fastify.decorate('cappy', { action: () => {} });
|
|
173
|
+
}
|
|
79
174
|
|
|
80
175
|
await fastify.listen({ port: PORT, host: HOST });
|
|
81
|
-
console.log(
|
|
176
|
+
console.log(`\n📦 Skill Base Engine Initialized at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}${APP_BASE_PATH}\n`);
|
|
177
|
+
|
|
178
|
+
if (enableCappy && cappy) {
|
|
179
|
+
// 启动 Cappy 守护进程
|
|
180
|
+
cappy.start();
|
|
181
|
+
}
|
|
82
182
|
} catch (err) {
|
|
83
183
|
console.error(err);
|
|
84
184
|
process.exit(1);
|