skill-base 2.0.3 → 2.0.6

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.
Files changed (46) hide show
  1. package/README.md +189 -85
  2. package/bin/skill-base.js +33 -7
  3. package/package.json +3 -1
  4. package/src/cappy.js +416 -0
  5. package/src/database.js +11 -0
  6. package/src/index.js +87 -24
  7. package/src/middleware/auth.js +96 -32
  8. package/src/routes/auth.js +1 -1
  9. package/src/routes/skills.js +10 -5
  10. package/src/utils/zip.js +15 -4
  11. package/static/android-chrome-192x192.png +0 -0
  12. package/static/android-chrome-512x512.png +0 -0
  13. package/static/apple-touch-icon.png +0 -0
  14. package/static/assets/index-BgwubB87.css +1 -0
  15. package/static/assets/index-DBHCo8Mz.js +230 -0
  16. package/static/favicon-16x16.png +0 -0
  17. package/static/favicon-32x32.png +0 -0
  18. package/static/favicon.ico +0 -0
  19. package/static/favicon.svg +14 -0
  20. package/static/index.html +18 -248
  21. package/static/site.webmanifest +1 -0
  22. package/static/admin/users.html +0 -593
  23. package/static/cli-code.html +0 -203
  24. package/static/css/.gitkeep +0 -0
  25. package/static/css/style.css +0 -1567
  26. package/static/diff.html +0 -466
  27. package/static/file.html +0 -443
  28. package/static/js/.gitkeep +0 -0
  29. package/static/js/admin/users.js +0 -346
  30. package/static/js/app.js +0 -508
  31. package/static/js/auth.js +0 -151
  32. package/static/js/cli-code.js +0 -184
  33. package/static/js/collaborators.js +0 -283
  34. package/static/js/diff.js +0 -540
  35. package/static/js/file.js +0 -619
  36. package/static/js/i18n.js +0 -739
  37. package/static/js/index.js +0 -168
  38. package/static/js/publish.js +0 -718
  39. package/static/js/settings.js +0 -124
  40. package/static/js/setup.js +0 -157
  41. package/static/js/skill.js +0 -808
  42. package/static/login.html +0 -82
  43. package/static/publish.html +0 -459
  44. package/static/settings.html +0 -163
  45. package/static/setup.html +0 -101
  46. 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 正在热机,路径 ${basePath} 已准备好。`
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 正在看着 ${basePath} 路径发呆。`,
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,9 +1,33 @@
1
+ const fs = require('fs');
1
2
  const path = require('path');
2
3
  const fastify = require('fastify')({
3
4
  logger: false,
4
5
  // 设置 body 大小限制为 100MB(支持大 zip 上传)
5
6
  bodyLimit: 100 * 1024 * 1024
6
7
  });
8
+ const CappyMascot = require('./cappy');
9
+
10
+ // 1. 规范化部署前缀 (APP_BASE_PATH)
11
+ let APP_BASE_PATH = process.env.APP_BASE_PATH || '/';
12
+ // 确保以 / 开头,以 / 结尾
13
+ if (!APP_BASE_PATH.startsWith('/')) APP_BASE_PATH = '/' + APP_BASE_PATH;
14
+ if (!APP_BASE_PATH.endsWith('/')) APP_BASE_PATH = APP_BASE_PATH + '/';
15
+ // 将多个连续斜杠替换为单个
16
+ APP_BASE_PATH = APP_BASE_PATH.replace(/\/+/g, '/');
17
+
18
+ const STATIC_ROOT = path.join(__dirname, '../static');
19
+ const INDEX_HTML_PATH = path.join(STATIC_ROOT, 'index.html');
20
+
21
+ function renderSpaHtml() {
22
+ const html = fs.readFileSync(INDEX_HTML_PATH, 'utf8');
23
+ const baseTag = ` <base href="${APP_BASE_PATH}">`;
24
+
25
+ if (html.includes('<base href=')) {
26
+ return html.replace(/<base href="[^"]*">/, baseTag.trim());
27
+ }
28
+
29
+ return html.replace('<head>', `<head>\n${baseTag}`);
30
+ }
7
31
 
8
32
  // 主启动函数
9
33
  async function start() {
@@ -25,10 +49,25 @@ async function start() {
25
49
  }
26
50
  });
27
51
 
52
+ // 必须在 static 之前:index:false 时「目录 + 尾斜杠」会走 send 的 403,不会落到 notFoundHandler
53
+ fastify.route({
54
+ method: ['GET', 'HEAD'],
55
+ url: APP_BASE_PATH,
56
+ async handler(request, reply) {
57
+ reply.type('text/html; charset=utf-8');
58
+ if (request.method === 'HEAD') {
59
+ return reply.send();
60
+ }
61
+ return reply.send(renderSpaHtml());
62
+ }
63
+ });
64
+
28
65
  // @fastify/static — 静态文件服务(指向 static/ 目录)
29
66
  await fastify.register(require('@fastify/static'), {
30
- root: path.join(__dirname, '../static'),
31
- prefix: '/'
67
+ root: STATIC_ROOT,
68
+ prefix: APP_BASE_PATH,
69
+ wildcard: true,
70
+ index: false
32
71
  });
33
72
 
34
73
  // 2. 注册自定义中间件
@@ -39,35 +78,34 @@ async function start() {
39
78
  // 管理员权限(注册 requireAdmin 装饰器)
40
79
  await fastify.register(require('./middleware/admin'));
41
80
 
42
- // 3. 注册 API 路由(前缀 /api/v1)
43
- await fastify.register(require('./routes/init'), { prefix: '/api/v1/init' });
44
- await fastify.register(require('./routes/auth'), { prefix: '/api/v1/auth' });
45
- await fastify.register(require('./routes/skills'), { prefix: '/api/v1/skills' });
46
- await fastify.register(require('./routes/publish'), { prefix: '/api/v1/skills' });
47
- await fastify.register(require('./routes/collaborators'), { prefix: '/api/v1/skills' });
48
- await fastify.register(require('./routes/users'), { prefix: '/api/v1/users' });
81
+ // 3. 注册 API 路由
82
+ const API_PREFIX = (APP_BASE_PATH + 'api/v1').replace(/\/+/g, '/');
83
+ await fastify.register(require('./routes/init'), { prefix: `${API_PREFIX}/init` });
84
+ await fastify.register(require('./routes/auth'), { prefix: `${API_PREFIX}/auth` });
85
+ await fastify.register(require('./routes/skills'), { prefix: `${API_PREFIX}/skills` });
86
+ await fastify.register(require('./routes/publish'), { prefix: `${API_PREFIX}/skills` });
87
+ await fastify.register(require('./routes/collaborators'), { prefix: `${API_PREFIX}/skills` });
88
+ await fastify.register(require('./routes/users'), { prefix: `${API_PREFIX}/users` });
49
89
 
50
90
  // 4. 页面路由 fallback(SPA 风格路由支持)
51
91
  fastify.setNotFoundHandler(async (request, reply) => {
92
+ const requestPath = request.url.split('?')[0];
93
+
52
94
  // API 路由返回 JSON 404
53
- if (request.url.startsWith('/api/')) {
95
+ if (requestPath.startsWith(API_PREFIX)) {
54
96
  return reply.code(404).send({ detail: 'Not found' });
55
97
  }
56
98
 
57
- // 页面路由映射到对应 HTML 文件
58
- const url = request.url.split('?')[0]; // 去掉 query string
59
-
60
- if (url === '/setup') return reply.sendFile('setup.html');
61
- if (url === '/login') return reply.sendFile('login.html');
62
- if (url === '/publish') return reply.sendFile('publish.html');
63
- if (url === '/cli-code') return reply.sendFile('cli-code.html');
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');
99
+ // 已知静态资源缺失时直接返回 404,别把 HTML 假装成 JS/CSS
100
+ if (
101
+ requestPath.startsWith(`${APP_BASE_PATH}assets/`) ||
102
+ requestPath === `${APP_BASE_PATH}favicon.ico`
103
+ ) {
104
+ return reply.code(404).send({ detail: 'Not found' });
105
+ }
68
106
 
69
- // 其他未匹配路由返回首页
70
- return reply.sendFile('index.html');
107
+ // 所有非 API 请求回退到入口 HTML,由前端 Vue Router 处理
108
+ return reply.type('text/html; charset=utf-8').send(renderSpaHtml());
71
109
  });
72
110
 
73
111
  // 5. 确保数据库已初始化
@@ -76,9 +114,34 @@ async function start() {
76
114
  // 6. 启动服务
77
115
  const PORT = process.env.PORT || 8000;
78
116
  const HOST = process.env.HOST || '0.0.0.0';
117
+
118
+ // 初始化 Cappy 水豚(必须在 listen 之前注册装饰器)
119
+ const cappy = new CappyMascot(PORT);
120
+ fastify.decorate('cappy', cappy);
121
+
122
+ // 优雅解耦:通过 Fastify 的全局生命周期钩子来驱动 Cappy 动画,完全不污染业务路由
123
+ fastify.addHook('onResponse', (request, reply, done) => {
124
+ // 只有成功请求才触发,不理会报错
125
+ if (reply.statusCode >= 200 && reply.statusCode < 300) {
126
+ const method = request.method;
127
+ const url = request.url.split('?')[0];
128
+
129
+ if (method === 'POST' && url === `${API_PREFIX}/users`) {
130
+ cappy.action('新用户被添加了。又多了一个打工人,系统依旧稳定。');
131
+ } else if (method === 'POST' && url === `${API_PREFIX}/skills/publish`) {
132
+ cappy.action('有新的 Skill/版本 发布了。希望它的代码没有过度设计。');
133
+ } else if (method === 'GET' && url.match(new RegExp(`^${API_PREFIX}/skills/[^/]+/versions/[^/]+/download/?$`))) {
134
+ cappy.action('有人拉取了 Skill。代码开始流通,Cappy 觉得很赞。');
135
+ }
136
+ }
137
+ done();
138
+ });
79
139
 
80
140
  await fastify.listen({ port: PORT, host: HOST });
81
- console.log(`Skill Base server running at http://${HOST}:${PORT}`);
141
+ console.log(`\n📦 Skill Base Engine Initialized.\n`);
142
+
143
+ // 启动 Cappy 守护进程
144
+ cappy.start();
82
145
  } catch (err) {
83
146
  console.error(err);
84
147
  process.exit(1);