skill-base 2.0.14 → 2.0.16

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 CHANGED
@@ -9,6 +9,18 @@
9
9
 
10
10
  ---
11
11
 
12
+ ## 界面预览
13
+
14
+ ![Skill Base 首页](docs/images/skill-base-home.png)
15
+
16
+ *首页:浏览与检索团队 Skill*
17
+
18
+ ![Skill 详情页](docs/images/skill-detail.png)
19
+
20
+ *Skill 详情:版本与内容*
21
+
22
+ ---
23
+
12
24
  ## 🤔 什么是 Agent Skill?
13
25
 
14
26
  在 AI 辅助开发时代,AI 虽然懂通用代码,但**不懂你们公司的业务上下文**。
@@ -78,8 +90,15 @@ npx skill-base -d ./skill-data -p 8000
78
90
  npx skill-base # 默认启动 (端口 8000, 数据存放在 npm 缓存)
79
91
  npx skill-base --host 127.0.0.1 # 仅限本地访问,增强安全性
80
92
  npx skill-base --base-path /skills/ # 部署在子路径下 (例如: /skills/)
93
+ npx skill-base --cache-max-mb 100 # 将进程内缓存上限调到 100MB
81
94
  ```
82
95
 
96
+ **常用环境变量:**
97
+
98
+ | 环境变量 | 说明 | 默认值 |
99
+ |------|------|--------|
100
+ | `CACHE_MAX_MB` | 进程内 LRU 缓存的总容量上限,缓存 skill/version/user 基础信息,超过后按 LRU 淘汰 | `50` |
101
+
83
102
  **完整参数说明:**
84
103
  | 参数 | 简写 | 说明 | 默认值 |
85
104
  |------|------|------|--------|
@@ -87,11 +106,14 @@ npx skill-base --base-path /skills/ # 部署在子路径下 (例如: /skills/)
87
106
  | `--host` | `-h` | 指定监听地址 | 0.0.0.0 |
88
107
  | `--data-dir` | `-d` | 指定数据目录 | 包内 data/ |
89
108
  | `--base-path` | - | 指定部署前缀 | / |
109
+ | `--cache-max-mb` | - | 指定进程内 LRU 缓存总容量,单位 MB | 50 |
90
110
  | `--no-cappy` | - | 禁用 Cappy 水豚吉祥物 | 启用 |
91
111
  | `--verbose` | `-v` | 启用调试日志 | 禁用 |
92
112
  | `--help` | - | 显示帮助信息 | - |
93
113
  | `--version` | - | 显示版本号 | - |
94
114
 
115
+ `GET /api/v1/health` 现在会返回简化的缓存统计,包括是否启用、容量上限、当前条目数、已用字节、命中/未命中与淘汰次数,方便部署后确认缓存是否在工作。
116
+
95
117
  > 🔐 **首次运行须知**:系统启动后,首次访问 Web 端将自动跳转至**初始化页面**,请根据提示设置系统管理员账号与密码。
96
118
 
97
119
  ---
package/bin/skill-base.js CHANGED
@@ -16,6 +16,7 @@ let dataDir = null;
16
16
  let basePath = '/';
17
17
  let enableCappy = true;
18
18
  let debug = false;
19
+ let cacheMaxMb = '50';
19
20
 
20
21
  for (let i = 0; i < args.length; i++) {
21
22
  if ((args[i] === '-p' || args[i] === '--port') && args[i + 1]) {
@@ -30,6 +31,9 @@ for (let i = 0; i < args.length; i++) {
30
31
  } else if (args[i] === '--base-path' && args[i + 1]) {
31
32
  basePath = args[i + 1];
32
33
  i++;
34
+ } else if (args[i] === '--cache-max-mb' && args[i + 1]) {
35
+ cacheMaxMb = args[i + 1];
36
+ i++;
33
37
  } else if (args[i] === '--no-cappy') {
34
38
  enableCappy = false;
35
39
  } else if (args[i] === '-v' || args[i] === '--verbose') {
@@ -46,6 +50,7 @@ Options:
46
50
  -h, --host <host> 指定监听地址 (默认: 0.0.0.0)
47
51
  -d, --data-dir <path> 指定数据目录 (默认: 包内 data/)
48
52
  --base-path <path> 指定部署前缀 (默认: /,例如: /skills/)
53
+ --cache-max-mb <mb> 指定进程内 LRU 缓存总容量,单位 MB (默认: 50)
49
54
  --no-cappy 禁用 Cappy 水豚吉祥物
50
55
  -v, --verbose 启用调试信息
51
56
  --help 显示帮助信息
@@ -58,6 +63,7 @@ Examples:
58
63
  npx skill-base -d ./data # 数据存储到当前目录的 data 文件夹
59
64
  npx skill-base -d . -p 3000 # 数据存储到当前目录
60
65
  npx skill-base --base-path /skills/ # 部署在子路径下
66
+ npx skill-base --cache-max-mb 100 # 将 LRU 缓存上限调整为 100MB
61
67
  npx skill-base --no-cappy # 禁用吉祥物
62
68
  `);
63
69
  process.exit(0);
@@ -74,6 +80,7 @@ process.env.HOST = host;
74
80
  process.env.APP_BASE_PATH = basePath;
75
81
  process.env.ENABLE_CAPPY = enableCappy;
76
82
  process.env.DEBUG = debug;
83
+ process.env.CACHE_MAX_MB = cacheMaxMb;
77
84
 
78
85
  // 设置数据目录
79
86
  if (dataDir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-base",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
4
4
  "description": "Skill Base - 私有部署的轻量级 Skill 管理平台",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cappy.js CHANGED
@@ -21,13 +21,13 @@ class CappyMascot {
21
21
  reset: '\x1b[0m'
22
22
  };
23
23
 
24
- // 数据很简单:scene 决定人格,frame 决定动作,message 决定台词。
24
+ // Scenes: personality + frames + messages.
25
25
  this.scenes = {
26
26
  intro: {
27
27
  frameDelay: 240,
28
28
  loops: 1,
29
29
  messages: [
30
- `Skill Base 正在热机,端口 ${port} 已点亮。`
30
+ `Skill Base is warming up; port ${port} is live.`
31
31
  ],
32
32
  frames: [
33
33
  { color: 'warm', sprite: this.createSprite('o o', '___', 'paw') },
@@ -40,10 +40,10 @@ class CappyMascot {
40
40
  frameDelay: 320,
41
41
  loops: 4,
42
42
  messages: [
43
- `Cappy 正在看着 ${port} 号端口发呆。`,
44
- '一切正常。没有过度设计,就没有运行时焦虑。',
45
- '技能仓库很安静,直白的代码才能带来这种安宁。',
46
- '系统稳定。Cappy 鄙视无谓的复杂度。'
43
+ `Cappy is staring at port ${port}.`,
44
+ 'All quiet. No over-engineering, no runtime anxiety.',
45
+ 'The skill library is calm. Straightforward code brings that peace.',
46
+ 'System stable. Cappy despises pointless complexity.'
47
47
  ],
48
48
  frames: [
49
49
  { color: 'warm', sprite: this.createSprite('o o', '___', 'still') },
@@ -56,8 +56,8 @@ class CappyMascot {
56
56
  frameDelay: 180,
57
57
  loops: 2,
58
58
  messages: [
59
- '缓慢地眨了下眼。不是摸鱼,是在进行低成本巡检。',
60
- '与其写一堆监控脚本,不如把代码写得简单点。'
59
+ 'Slow blink. Not slacking—low-cost patrolling.',
60
+ 'Better to write simple code than to pile on monitoring scripts.'
61
61
  ],
62
62
  frames: [
63
63
  { color: 'warm', sprite: this.createSprite('o o', '___', 'still') },
@@ -70,8 +70,8 @@ class CappyMascot {
70
70
  frameDelay: 280,
71
71
  loops: 3,
72
72
  messages: [
73
- '简单的架构才是最好的。真正的稳定性是不需要花哨设计的。',
74
- '思考中。直接写代码,比写那些自作聪明的抽象层靠谱多了。'
73
+ 'Simple architecture wins. Real stability needs no fancy design.',
74
+ 'Thinking. Writing code directly beats clever layers of abstraction.'
75
75
  ],
76
76
  frames: [
77
77
  { color: 'cyan', sprite: this.createSprite('o o', '___', 'think-left') },
@@ -84,8 +84,8 @@ class CappyMascot {
84
84
  frameDelay: 340,
85
85
  loops: 3,
86
86
  messages: [
87
- '数据结构对了,逻辑自然就像水一样顺畅。',
88
- '泡一下就想明白了。别去猜未来的需求,YAGNI'
87
+ 'Get the data structures right and the logic flows like water.',
88
+ 'Let it soak in. Do not guess future needs—YAGNI.'
89
89
  ],
90
90
  frames: [
91
91
  { color: 'pink', sprite: this.createSprite('^ ^', '~~~', 'steam-left') },
@@ -98,8 +98,8 @@ class CappyMascot {
98
98
  frameDelay: 220,
99
99
  loops: 4,
100
100
  messages: [
101
- '短距离散步。确认没有被哪个聪明人搞出过度设计。',
102
- '没有多余步骤,只有必要移动。代码也该如此。'
101
+ 'Short walk. Checking no one over-engineered anything.',
102
+ 'No extra steps. Only moves that matter. Code should be the same.'
103
103
  ],
104
104
  frames: [
105
105
  { color: 'cyan', sprite: this.createSprite('o o', '___', 'step-left') },
@@ -111,7 +111,7 @@ class CappyMascot {
111
111
  frameDelay: 180,
112
112
  loops: 6,
113
113
  messages: [
114
- '收到任务,Cappy 正在用最直接的方式处理。'
114
+ 'Task received. Cappy is handling it the plain way.'
115
115
  ],
116
116
  frames: [
117
117
  { color: 'cyan', sprite: this.createSprite('> <', '===', 'spark-left') },
@@ -141,7 +141,7 @@ class CappyMascot {
141
141
  if (!this.isRunning || this.isStopped) return;
142
142
 
143
143
  this.playScene('work', {
144
- message: message || '有新动作发生了,但卡皮巴拉依然很稳。',
144
+ message: message || 'Something new happened; Cappy stays steady.',
145
145
  onDone: () => this.scheduleNextIdle(600)
146
146
  });
147
147
  }
@@ -155,10 +155,15 @@ class CappyMascot {
155
155
  clearTimeout(this.sceneTimer);
156
156
 
157
157
  this.clearRender();
158
+ const goodbyeMsg = 'Cappy is off duty. See you tomorrow.';
159
+ const gBorder = '─'.repeat(goodbyeMsg.length + 2);
160
+ const gSeg = goodbyeMsg.length + 1;
161
+ const gLeft = Math.floor(gSeg / 2);
162
+ const gRight = gSeg - gLeft;
158
163
  const goodbye = [
159
- `${this.colors.soft} ╭──────────────────────────────╮${this.colors.reset}`,
160
- `${this.colors.soft} │ Cappy 下班了,明天继续值守。 │${this.colors.reset}`,
161
- `${this.colors.soft} ╰──────────────┬───────────────╯${this.colors.reset}`,
164
+ `${this.colors.soft} ╭${gBorder}╮${this.colors.reset}`,
165
+ `${this.colors.soft} │ ${goodbyeMsg} │${this.colors.reset}`,
166
+ `${this.colors.soft} ╰${'─'.repeat(gLeft)}┬${'─'.repeat(gRight)}╯${this.colors.reset}`,
162
167
  `${this.colors.warm} \\${this.colors.reset}`,
163
168
  `${this.colors.warm} __${this.colors.reset}`,
164
169
  `${this.colors.warm} ___( ; ;)___${this.colors.reset}`,
@@ -251,18 +256,74 @@ class CappyMascot {
251
256
  }
252
257
 
253
258
  buildBubble(message) {
254
- const text = this.fit(message, 34);
255
- const width = text.length;
256
- const line = '─'.repeat(width + 2);
259
+ const maxLineWidth = 44;
260
+ const lines = this.wrapText(String(message), maxLineWidth);
261
+ const innerWidth = Math.max(1, ...lines.map((l) => l.length));
262
+ const border = '─'.repeat(innerWidth + 2);
263
+ const body = lines.map(
264
+ (line) =>
265
+ `${this.colors.soft} │ ${line.padEnd(innerWidth)} │${this.colors.reset}`
266
+ );
257
267
 
258
268
  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}`,
269
+ `${this.colors.soft} ╭${border}╮${this.colors.reset}`,
270
+ ...body,
271
+ `${this.colors.soft} ╰${border}╯${this.colors.reset}`,
262
272
  `${this.colors.soft} \\${this.colors.reset}`
263
273
  ];
264
274
  }
265
275
 
276
+ wrapText(text, maxWidth) {
277
+ const normalized = text.replace(/\r\n/g, '\n').trim();
278
+ if (!normalized.length) return [''];
279
+
280
+ const paragraphs = normalized.split('\n');
281
+ const out = [];
282
+
283
+ for (const para of paragraphs) {
284
+ if (!para.trim()) {
285
+ out.push('');
286
+ continue;
287
+ }
288
+
289
+ const words = para.split(/\s+/);
290
+ let line = '';
291
+
292
+ const flush = () => {
293
+ if (line.length) {
294
+ out.push(line);
295
+ line = '';
296
+ }
297
+ };
298
+
299
+ for (const word of words) {
300
+ if (!word.length) continue;
301
+
302
+ if (word.length > maxWidth) {
303
+ flush();
304
+ let rest = word;
305
+ while (rest.length > maxWidth) {
306
+ out.push(rest.slice(0, maxWidth));
307
+ rest = rest.slice(maxWidth);
308
+ }
309
+ line = rest;
310
+ continue;
311
+ }
312
+
313
+ const next = line.length ? `${line} ${word}` : word;
314
+ if (next.length <= maxWidth) {
315
+ line = next;
316
+ } else {
317
+ flush();
318
+ line = word;
319
+ }
320
+ }
321
+ flush();
322
+ }
323
+
324
+ return out.length ? out : [''];
325
+ }
326
+
266
327
  pickIdleScene() {
267
328
  const entries = Object.entries(this.scenes)
268
329
  .filter(([key, scene]) => scene.weight)
@@ -283,14 +344,6 @@ class CappyMascot {
283
344
  return list[Math.floor(Math.random() * list.length)];
284
345
  }
285
346
 
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
347
  randomBetween(min, max) {
295
348
  return Math.floor(Math.random() * (max - min + 1)) + min;
296
349
  }
package/src/index.js CHANGED
@@ -1,11 +1,15 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { getStats: getCacheStats } = require('./utils/model-cache');
3
4
 
4
5
  const isDebug = process.env.DEBUG === 'true';
6
+ if (!process.env.CACHE_MAX_MB) {
7
+ process.env.CACHE_MAX_MB = '50';
8
+ }
5
9
 
6
10
  const fastify = require('fastify')({
7
11
  logger: isDebug,
8
- // 设置 body 大小限制为 100MB(支持大 zip 上传)
12
+ // 100MB body limit for large zip uploads
9
13
  bodyLimit: 100 * 1024 * 1024
10
14
  });
11
15
  const CappyMascot = require('./cappy');
@@ -15,14 +19,15 @@ if (isDebug) {
15
19
  console.log('DEBUG: PORT:', process.env.PORT);
16
20
  console.log('DEBUG: HOST:', process.env.HOST);
17
21
  console.log('DEBUG: APP_BASE_PATH:', process.env.APP_BASE_PATH);
22
+ console.log('DEBUG: CACHE_MAX_MB:', process.env.CACHE_MAX_MB);
18
23
  }
19
24
 
20
- // 1. 规范化部署前缀 (APP_BASE_PATH)
25
+ // 1. Normalize deploy prefix (APP_BASE_PATH)
21
26
  let APP_BASE_PATH = process.env.APP_BASE_PATH || '/';
22
- // 确保以 / 开头,以 / 结尾
27
+ // Leading /, trailing /
23
28
  if (!APP_BASE_PATH.startsWith('/')) APP_BASE_PATH = '/' + APP_BASE_PATH;
24
29
  if (!APP_BASE_PATH.endsWith('/')) APP_BASE_PATH = APP_BASE_PATH + '/';
25
- // 将多个连续斜杠替换为单个
30
+ // Collapse repeated slashes
26
31
  APP_BASE_PATH = APP_BASE_PATH.replace(/\/+/g, '/');
27
32
 
28
33
  const STATIC_ROOT = path.join(__dirname, '../static');
@@ -39,23 +44,23 @@ function renderSpaHtml() {
39
44
  return html.replace('<head>', `<head>\n${baseTag}`);
40
45
  }
41
46
 
42
- // 主启动函数
47
+ // Main bootstrap
43
48
  async function start() {
44
49
  try {
45
50
  if (isDebug) console.log('DEBUG: Registering core plugins...');
46
- // 1. 注册插件
47
- // @fastify/cors — 允许跨域
51
+ // 1. Plugins
52
+ // @fastify/cors — CORS
48
53
  await fastify.register(require('@fastify/cors'), {
49
54
  origin: true,
50
55
  credentials: true
51
56
  });
52
57
  if (isDebug) console.log('DEBUG: Registered @fastify/cors');
53
58
 
54
- // @fastify/cookie — Cookie 支持
59
+ // @fastify/cookie
55
60
  await fastify.register(require('@fastify/cookie'));
56
61
  if (isDebug) console.log('DEBUG: Registered @fastify/cookie');
57
62
 
58
- // @fastify/multipart — 文件上传支持
63
+ // @fastify/multipart — uploads
59
64
  await fastify.register(require('@fastify/multipart'), {
60
65
  limits: {
61
66
  fileSize: 100 * 1024 * 1024 // 100MB
@@ -63,7 +68,7 @@ async function start() {
63
68
  });
64
69
  if (isDebug) console.log('DEBUG: Registered @fastify/multipart');
65
70
 
66
- // 必须在 static 之前:index:false 时「目录 + 尾斜杠」会走 send 403,不会落到 notFoundHandler
71
+ // Before static: with index:false, directory + trailing slash can 403 in send and skip notFoundHandler
67
72
  fastify.route({
68
73
  method: ['GET', 'HEAD'],
69
74
  url: APP_BASE_PATH,
@@ -76,7 +81,7 @@ async function start() {
76
81
  }
77
82
  });
78
83
 
79
- // @fastify/static — 静态文件服务(指向 static/ 目录)
84
+ // @fastify/static — serve static/
80
85
  await fastify.register(require('@fastify/static'), {
81
86
  root: STATIC_ROOT,
82
87
  prefix: APP_BASE_PATH,
@@ -85,23 +90,31 @@ async function start() {
85
90
  });
86
91
  if (isDebug) console.log('DEBUG: Registered @fastify/static at', STATIC_ROOT);
87
92
 
88
- // 2. 注册自定义中间件
93
+ // 2. Custom middleware
89
94
  if (isDebug) console.log('DEBUG: Registering custom middlewares...');
90
- // 错误处理
95
+ // Errors
91
96
  await fastify.register(require('./middleware/error'));
92
- // 认证(注册 authenticatecreateSession 等装饰器)
97
+ // Auth (authenticate, createSession, …)
93
98
  await fastify.register(require('./middleware/auth'));
94
- // 管理员权限(注册 requireAdmin 装饰器)
99
+ // Admin (requireAdmin)
95
100
  await fastify.register(require('./middleware/admin'));
96
101
  if (isDebug) console.log('DEBUG: Custom middlewares registered.');
97
102
 
98
- // 3. 注册 API 路由
103
+ // 3. API routes
99
104
  const API_PREFIX = (APP_BASE_PATH + 'api/v1').replace(/\/+/g, '/');
100
105
  if (isDebug) console.log('DEBUG: Registering API routes with prefix:', API_PREFIX);
101
106
 
102
- // 健康检查接口
107
+ // Health
103
108
  fastify.get(`${API_PREFIX}/health`, async () => {
104
- return { status: 'ok', timestamp: new Date().toISOString() };
109
+ return {
110
+ status: 'ok',
111
+ timestamp: new Date().toISOString(),
112
+ cache: {
113
+ enabled: Number(process.env.CACHE_MAX_MB) > 0,
114
+ max_mb: Number(process.env.CACHE_MAX_MB),
115
+ ...getCacheStats()
116
+ }
117
+ };
105
118
  });
106
119
 
107
120
  await fastify.register(require('./routes/init'), { prefix: `${API_PREFIX}/init` });
@@ -112,16 +125,16 @@ async function start() {
112
125
  await fastify.register(require('./routes/users'), { prefix: `${API_PREFIX}/users` });
113
126
  if (isDebug) console.log('DEBUG: API routes registered.');
114
127
 
115
- // 4. 页面路由 fallback(SPA 风格路由支持)
128
+ // 4. SPA fallback for non-API routes
116
129
  fastify.setNotFoundHandler(async (request, reply) => {
117
130
  const requestPath = request.url.split('?')[0];
118
131
 
119
- // API 路由返回 JSON 404
132
+ // API JSON 404
120
133
  if (requestPath.startsWith(API_PREFIX)) {
121
134
  return reply.code(404).send({ detail: 'Not found' });
122
135
  }
123
136
 
124
- // 已知静态资源缺失时直接返回 404,别把 HTML 假装成 JS/CSS
137
+ // Missing assets → 404, not HTML-as-JS/CSS
125
138
  if (
126
139
  requestPath.startsWith(`${APP_BASE_PATH}assets/`) ||
127
140
  requestPath === `${APP_BASE_PATH}favicon.ico`
@@ -129,40 +142,40 @@ async function start() {
129
142
  return reply.code(404).send({ detail: 'Not found' });
130
143
  }
131
144
 
132
- // 所有非 API 请求回退到入口 HTML,由前端 Vue Router 处理
145
+ // Everything else index HTML (Vue Router)
133
146
  return reply.type('text/html; charset=utf-8').send(renderSpaHtml());
134
147
  });
135
148
 
136
- // 5. 确保数据库已初始化
149
+ // 5. DB init
137
150
  require('./database');
138
151
 
139
- // 6. 启动服务
152
+ // 6. Listen
140
153
  const PORT = process.env.PORT || 8000;
141
154
  const HOST = process.env.HOST || '0.0.0.0';
142
155
 
143
- // 默认禁用 Cappy,除非显式设为 'true'
156
+ // Cappy off unless ENABLE_CAPPY=true
144
157
  const enableCappy = process.env.ENABLE_CAPPY === 'true';
145
158
  let cappy = null;
146
159
 
147
160
  if (enableCappy) {
148
161
  if (isDebug) console.log('DEBUG: CappyMascot is enabled.');
149
- // 初始化 Cappy 水豚(必须在 listen 之前注册装饰器)
162
+ // Cappy (decorate before listen)
150
163
  cappy = new CappyMascot(PORT, APP_BASE_PATH);
151
164
  fastify.decorate('cappy', cappy);
152
165
 
153
- // 优雅解耦:通过 Fastify 的全局生命周期钩子来驱动 Cappy 动画,完全不污染业务路由
166
+ // Drive Cappy from onResponse; no route changes
154
167
  fastify.addHook('onResponse', (request, reply, done) => {
155
- // 只有成功请求才触发,不理会报错
168
+ // Only 2xx
156
169
  if (reply.statusCode >= 200 && reply.statusCode < 300) {
157
170
  const method = request.method;
158
171
  const url = request.url.split('?')[0];
159
172
 
160
173
  if (method === 'POST' && url === `${API_PREFIX}/users`) {
161
- cappy.action('新用户被添加了。又多了一个打工人,系统依旧稳定。');
174
+ cappy.action('New user added. Another worker on the roster; the system stays steady.');
162
175
  } else if (method === 'POST' && url === `${API_PREFIX}/skills/publish`) {
163
- cappy.action('有新的 Skill/版本 发布了。希望它的代码没有过度设计。');
176
+ cappy.action('New skill or version published. Hope the code stays simple.');
164
177
  } else if (method === 'GET' && url.match(new RegExp(`^${API_PREFIX}/skills/[^/]+/versions/[^/]+/download/?$`))) {
165
- cappy.action('有人拉取了 Skill。代码开始流通,Cappy 觉得很赞。');
178
+ cappy.action('Someone downloaded a skill. Code is moving—Cappy approves.');
166
179
  }
167
180
  }
168
181
  done();
@@ -176,7 +189,7 @@ async function start() {
176
189
  console.log(`\n📦 Skill Base Engine Initialized at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}${APP_BASE_PATH}\n`);
177
190
 
178
191
  if (enableCappy && cappy) {
179
- // 启动 Cappy 守护进程
192
+ // Start Cappy loop
180
193
  cappy.start();
181
194
  }
182
195
  } catch (err) {
@@ -1,34 +1,51 @@
1
1
  const db = require('../database');
2
+ const modelCache = require('../utils/model-cache');
3
+
4
+ function queryById(id) {
5
+ return db.prepare(`
6
+ SELECT s.*, u.username as owner_username, u.name as owner_name
7
+ FROM skills s
8
+ LEFT JOIN users u ON s.owner_id = u.id
9
+ WHERE s.id = ?
10
+ `).get(id);
11
+ }
2
12
 
3
13
  const SkillModel = {
4
14
  // 根据 ID 查询 Skill(附带 owner 信息)
5
15
  findById(id) {
6
- return db.prepare(`
7
- SELECT s.*, u.username as owner_username, u.name as owner_name
8
- FROM skills s
9
- LEFT JOIN users u ON s.owner_id = u.id
10
- WHERE s.id = ?
11
- `).get(id);
16
+ return modelCache.remember(
17
+ modelCache.keys.skill(id),
18
+ () => queryById(id),
19
+ modelCache.refs.skill
20
+ );
12
21
  },
13
22
 
14
23
  // 搜索/列出 Skills(支持关键词搜索 name 或 description)
15
24
  search(query) {
16
- if (query) {
17
- const pattern = `%${query}%`;
18
- return db.prepare(`
19
- SELECT s.*, u.username as owner_username, u.name as owner_name
20
- FROM skills s
21
- LEFT JOIN users u ON s.owner_id = u.id
22
- WHERE s.name LIKE ? OR s.description LIKE ?
23
- ORDER BY s.updated_at DESC
24
- `).all(pattern, pattern);
25
- }
26
- return db.prepare(`
27
- SELECT s.*, u.username as owner_username, u.name as owner_name
28
- FROM skills s
29
- LEFT JOIN users u ON s.owner_id = u.id
30
- ORDER BY s.updated_at DESC
31
- `).all();
25
+ const normalizedQuery = query ? String(query) : '';
26
+ return modelCache.remember(
27
+ modelCache.keys.skillSearch(normalizedQuery),
28
+ () => {
29
+ if (normalizedQuery) {
30
+ const pattern = `%${normalizedQuery}%`;
31
+ return db.prepare(`
32
+ SELECT s.*, u.username as owner_username, u.name as owner_name
33
+ FROM skills s
34
+ LEFT JOIN users u ON s.owner_id = u.id
35
+ WHERE s.name LIKE ? OR s.description LIKE ?
36
+ ORDER BY s.updated_at DESC
37
+ `).all(pattern, pattern);
38
+ }
39
+
40
+ return db.prepare(`
41
+ SELECT s.*, u.username as owner_username, u.name as owner_name
42
+ FROM skills s
43
+ LEFT JOIN users u ON s.owner_id = u.id
44
+ ORDER BY s.updated_at DESC
45
+ `).all();
46
+ },
47
+ modelCache.refs.skillSearch
48
+ );
32
49
  },
33
50
 
34
51
  // 创建新 Skill
@@ -37,7 +54,8 @@ const SkillModel = {
37
54
  INSERT INTO skills (id, name, description, owner_id)
38
55
  VALUES (?, ?, ?, ?)
39
56
  `).run(id, name, description || '', ownerId);
40
- return this.findById(id);
57
+ modelCache.invalidateSkill(id);
58
+ return queryById(id);
41
59
  },
42
60
 
43
61
  // 更新 Skill
@@ -60,8 +78,9 @@ const SkillModel = {
60
78
  db.prepare(`
61
79
  UPDATE skills SET ${fields.join(', ')} WHERE id = ?
62
80
  `).run(...values);
63
-
64
- return this.findById(id);
81
+
82
+ modelCache.invalidateSkill(id);
83
+ return queryById(id);
65
84
  },
66
85
 
67
86
  // 更新 latest_version 和 updated_at
@@ -69,12 +88,19 @@ const SkillModel = {
69
88
  db.prepare(`
70
89
  UPDATE skills SET latest_version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
71
90
  `).run(version, id);
91
+ modelCache.invalidateSkill(id);
72
92
  },
73
93
 
74
94
  // 检查 Skill 是否存在
75
95
  exists(id) {
76
- const row = db.prepare('SELECT 1 FROM skills WHERE id = ?').get(id);
77
- return !!row;
96
+ return modelCache.remember(
97
+ modelCache.keys.skillExists(id),
98
+ () => {
99
+ const row = db.prepare('SELECT 1 FROM skills WHERE id = ?').get(id);
100
+ return !!row;
101
+ },
102
+ [`skill:${id}`]
103
+ );
78
104
  }
79
105
  };
80
106