skill-base 2.0.15 → 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.15",
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
 
@@ -1,9 +1,18 @@
1
1
  const db = require('../database');
2
+ const modelCache = require('../utils/model-cache');
3
+
4
+ function queryById(id) {
5
+ return db.prepare('SELECT id, username, name, role, status, created_at, updated_at FROM users WHERE id = ?').get(id);
6
+ }
2
7
 
3
8
  const UserModel = {
4
9
  // 根据 ID 查询用户
5
10
  findById(id) {
6
- return db.prepare('SELECT id, username, name, role, status, created_at, updated_at FROM users WHERE id = ?').get(id);
11
+ return modelCache.remember(
12
+ modelCache.keys.userBasic(id),
13
+ () => queryById(id),
14
+ modelCache.refs.user
15
+ );
7
16
  },
8
17
 
9
18
  // 根据用户名查询(含 password_hash,用于登录验证)
@@ -14,7 +23,8 @@ const UserModel = {
14
23
  // 创建用户
15
24
  create(username, passwordHash, role = 'developer') {
16
25
  const result = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)').run(username, passwordHash, role);
17
- return this.findById(result.lastInsertRowid);
26
+ modelCache.invalidateUser(result.lastInsertRowid);
27
+ return queryById(result.lastInsertRowid);
18
28
  },
19
29
 
20
30
  // 列出用户(支持分页和搜索)
@@ -51,6 +61,9 @@ const UserModel = {
51
61
  const result = db.prepare(
52
62
  "UPDATE users SET username = ?, updated_at = datetime('now') WHERE id = ?"
53
63
  ).run(username, id);
64
+ if (result.changes > 0) {
65
+ modelCache.invalidateUser(id);
66
+ }
54
67
  return result.changes > 0;
55
68
  },
56
69
 
@@ -59,6 +72,9 @@ const UserModel = {
59
72
  const result = db.prepare(
60
73
  "UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?"
61
74
  ).run(passwordHash, id);
75
+ if (result.changes > 0) {
76
+ modelCache.invalidateUser(id);
77
+ }
62
78
  return result.changes > 0;
63
79
  },
64
80
 
@@ -81,6 +97,9 @@ const UserModel = {
81
97
  params.push(id);
82
98
 
83
99
  const result = db.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...params);
100
+ if (result.changes > 0) {
101
+ modelCache.invalidateUser(id);
102
+ }
84
103
  return result.changes > 0;
85
104
  },
86
105
 
@@ -89,6 +108,9 @@ const UserModel = {
89
108
  const result = db.prepare(
90
109
  "UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?"
91
110
  ).run(passwordHash, id);
111
+ if (result.changes > 0) {
112
+ modelCache.invalidateUser(id);
113
+ }
92
114
  return result.changes > 0;
93
115
  },
94
116
 
@@ -123,6 +145,9 @@ const UserModel = {
123
145
  params.push(id);
124
146
 
125
147
  const result = db.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...params);
148
+ if (result.changes > 0) {
149
+ modelCache.invalidateUser(id);
150
+ }
126
151
  return result.changes > 0;
127
152
  }
128
153
  };
@@ -1,4 +1,5 @@
1
1
  const db = require('../database');
2
+ const modelCache = require('../utils/model-cache');
2
3
 
3
4
  const VersionModel = {
4
5
  // 创建新版本
@@ -7,6 +8,7 @@ const VersionModel = {
7
8
  INSERT INTO skill_versions (skill_id, version, changelog, zip_path, uploader_id, description)
8
9
  VALUES (?, ?, ?, ?, ?, ?)
9
10
  `).run(skillId, version, changelog || '', zipPath, uploaderId, description || '');
11
+ modelCache.invalidateSkill(skillId);
10
12
  return this.findById(result.lastInsertRowid);
11
13
  },
12
14
 
@@ -22,44 +24,60 @@ const VersionModel = {
22
24
 
23
25
  // 根据 skill_id 和 version 查询
24
26
  findByVersion(skillId, version) {
25
- return db.prepare(`
26
- SELECT sv.*, u.username as uploader_username, u.name as uploader_name
27
- FROM skill_versions sv
28
- LEFT JOIN users u ON sv.uploader_id = u.id
29
- WHERE sv.skill_id = ? AND sv.version = ?
30
- `).get(skillId, version);
27
+ return modelCache.remember(
28
+ modelCache.keys.skillVersion(skillId, version),
29
+ () => db.prepare(`
30
+ SELECT sv.*, u.username as uploader_username, u.name as uploader_name
31
+ FROM skill_versions sv
32
+ LEFT JOIN users u ON sv.uploader_id = u.id
33
+ WHERE sv.skill_id = ? AND sv.version = ?
34
+ `).get(skillId, version),
35
+ modelCache.refs.version
36
+ );
31
37
  },
32
38
 
33
39
  // 列出某 Skill 的所有版本(按创建时间倒序)
34
40
  listBySkillId(skillId) {
35
- return db.prepare(`
36
- SELECT sv.*, u.username as uploader_username, u.name as uploader_name
37
- FROM skill_versions sv
38
- LEFT JOIN users u ON sv.uploader_id = u.id
39
- WHERE sv.skill_id = ?
40
- ORDER BY sv.created_at DESC
41
- `).all(skillId);
41
+ return modelCache.remember(
42
+ modelCache.keys.skillVersions(skillId),
43
+ () => db.prepare(`
44
+ SELECT sv.*, u.username as uploader_username, u.name as uploader_name
45
+ FROM skill_versions sv
46
+ LEFT JOIN users u ON sv.uploader_id = u.id
47
+ WHERE sv.skill_id = ?
48
+ ORDER BY sv.created_at DESC, sv.id DESC
49
+ `).all(skillId),
50
+ (versions) => modelCache.refs.versionList(skillId, versions)
51
+ );
42
52
  },
43
53
 
44
54
  // 获取某 Skill 的最新版本
45
55
  getLatest(skillId) {
46
- return db.prepare(`
47
- SELECT sv.*, u.username as uploader_username, u.name as uploader_name
48
- FROM skill_versions sv
49
- LEFT JOIN users u ON sv.uploader_id = u.id
50
- WHERE sv.skill_id = ?
51
- ORDER BY sv.created_at DESC
52
- LIMIT 1
53
- `).get(skillId);
56
+ return modelCache.remember(
57
+ modelCache.keys.skillLatest(skillId),
58
+ () => db.prepare(`
59
+ SELECT sv.*, u.username as uploader_username, u.name as uploader_name
60
+ FROM skill_versions sv
61
+ LEFT JOIN users u ON sv.uploader_id = u.id
62
+ WHERE sv.skill_id = ?
63
+ ORDER BY sv.created_at DESC, sv.id DESC
64
+ LIMIT 1
65
+ `).get(skillId),
66
+ modelCache.refs.version
67
+ );
54
68
  },
55
69
 
56
70
  // 更新版本描述和更新日志
57
71
  update(id, description, changelog) {
72
+ const existing = this.findById(id);
58
73
  db.prepare(`
59
74
  UPDATE skill_versions
60
75
  SET description = ?, changelog = ?
61
76
  WHERE id = ?
62
77
  `).run(description, changelog, id);
78
+ if (existing) {
79
+ modelCache.invalidateSkill(existing.skill_id);
80
+ }
63
81
  return this.findById(id);
64
82
  }
65
83
  };
@@ -1,6 +1,7 @@
1
1
  const db = require('../database');
2
2
  const { canManageSkill } = require('../utils/permission');
3
3
  const UserModel = require('../models/user');
4
+ const { invalidateSkill } = require('../utils/model-cache');
4
5
 
5
6
  async function collaboratorsRoutes(fastify, options) {
6
7
 
@@ -84,6 +85,7 @@ async function collaboratorsRoutes(fastify, options) {
84
85
  const result = db.prepare(
85
86
  'INSERT INTO skill_collaborators (skill_id, user_id, role, created_by) VALUES (?, ?, ?, ?)'
86
87
  ).run(skill_id, targetUser.id, 'collaborator', request.user.id);
88
+ invalidateSkill(skill_id);
87
89
 
88
90
  return reply.code(201).send({
89
91
  ok: true,
@@ -129,6 +131,7 @@ async function collaboratorsRoutes(fastify, options) {
129
131
 
130
132
  db.prepare('DELETE FROM skill_collaborators WHERE skill_id = ? AND user_id = ?')
131
133
  .run(skill_id, parseInt(user_id));
134
+ invalidateSkill(skill_id);
132
135
 
133
136
  return reply.send({ ok: true, message: 'Collaborator removed' });
134
137
  });
@@ -192,6 +195,7 @@ async function collaboratorsRoutes(fastify, options) {
192
195
  });
193
196
 
194
197
  transferTx();
198
+ invalidateSkill(skill_id);
195
199
 
196
200
  return reply.send({
197
201
  ok: true,
@@ -239,6 +243,7 @@ async function collaboratorsRoutes(fastify, options) {
239
243
  });
240
244
 
241
245
  deleteSkillTx();
246
+ invalidateSkill(skill_id);
242
247
 
243
248
  // 删除文件系统中的文件
244
249
  const fs = require('fs');
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const db = require('../database');
3
3
  const SkillModel = require('../models/skill');
4
4
  const VersionModel = require('../models/version');
5
+ const { invalidateSkill } = require('../utils/model-cache');
5
6
  const { ensureSkillDir, generateVersionNumber, getZipPath, getZipRelativePath } = require('../utils/zip');
6
7
  const { canPublishSkill } = require('../utils/permission');
7
8
 
@@ -96,6 +97,7 @@ async function publishRoutes(fastify, options) {
96
97
 
97
98
  // 更新 skill 的最新版本
98
99
  SkillModel.updateLatestVersion(skill_id, version);
100
+ invalidateSkill(skill_id);
99
101
 
100
102
  return {
101
103
  ok: true,
@@ -0,0 +1,160 @@
1
+ class LruCache {
2
+ constructor({ maxBytes = 50 * 1024 * 1024, defaultTtlMs = 0 } = {}) {
3
+ this.maxBytes = Number.isFinite(maxBytes) ? Math.max(0, Math.floor(maxBytes)) : 0;
4
+ this.defaultTtlMs = Number.isFinite(defaultTtlMs) ? Math.max(0, defaultTtlMs) : 0;
5
+ this.map = new Map();
6
+ this.totalBytes = 0;
7
+ this.hits = 0;
8
+ this.misses = 0;
9
+ this.evictions = 0;
10
+ }
11
+
12
+ static estimateSize(value) {
13
+ try {
14
+ const serialized = JSON.stringify(value);
15
+ return Buffer.byteLength(serialized === undefined ? 'null' : serialized, 'utf8');
16
+ } catch (error) {
17
+ return 0;
18
+ }
19
+ }
20
+
21
+ has(key) {
22
+ const entry = this.map.get(key);
23
+ if (!entry) {
24
+ return false;
25
+ }
26
+
27
+ if (this.#isExpired(entry)) {
28
+ this.#deleteEntry(key, entry);
29
+ return false;
30
+ }
31
+
32
+ return true;
33
+ }
34
+
35
+ get(key) {
36
+ const entry = this.map.get(key);
37
+ if (!entry) {
38
+ this.misses += 1;
39
+ return undefined;
40
+ }
41
+
42
+ if (this.#isExpired(entry)) {
43
+ this.#deleteEntry(key, entry);
44
+ this.misses += 1;
45
+ return undefined;
46
+ }
47
+
48
+ this.map.delete(key);
49
+ this.map.set(key, entry);
50
+ this.hits += 1;
51
+ return entry.value;
52
+ }
53
+
54
+ set(key, value, options = {}) {
55
+ if (this.maxBytes <= 0) {
56
+ return false;
57
+ }
58
+
59
+ const size = Number.isFinite(options.size) ? Math.max(0, Math.floor(options.size)) : LruCache.estimateSize(value);
60
+ if (size > this.maxBytes) {
61
+ this.delete(key);
62
+ return false;
63
+ }
64
+
65
+ const ttlMs = Number.isFinite(options.ttlMs) ? Math.max(0, options.ttlMs) : this.defaultTtlMs;
66
+ const expiresAt = ttlMs > 0 ? Date.now() + ttlMs : null;
67
+ const entry = {
68
+ value,
69
+ size,
70
+ refs: new Set(options.refs || []),
71
+ expiresAt
72
+ };
73
+
74
+ const existing = this.map.get(key);
75
+ if (existing) {
76
+ this.#deleteEntry(key, existing);
77
+ }
78
+
79
+ this.map.set(key, entry);
80
+ this.totalBytes += size;
81
+ this.#evictIfNeeded();
82
+ return true;
83
+ }
84
+
85
+ delete(key) {
86
+ const entry = this.map.get(key);
87
+ if (!entry) {
88
+ return false;
89
+ }
90
+
91
+ this.#deleteEntry(key, entry);
92
+ return true;
93
+ }
94
+
95
+ clear() {
96
+ this.map.clear();
97
+ this.totalBytes = 0;
98
+ }
99
+
100
+ clearByPrefix(prefix) {
101
+ const keys = [];
102
+ for (const key of this.map.keys()) {
103
+ if (key.startsWith(prefix)) {
104
+ keys.push(key);
105
+ }
106
+ }
107
+
108
+ for (const key of keys) {
109
+ this.delete(key);
110
+ }
111
+
112
+ return keys.length;
113
+ }
114
+
115
+ clearByRef(ref) {
116
+ const keys = [];
117
+ for (const [key, entry] of this.map.entries()) {
118
+ if (entry.refs.has(ref)) {
119
+ keys.push(key);
120
+ }
121
+ }
122
+
123
+ for (const key of keys) {
124
+ this.delete(key);
125
+ }
126
+
127
+ return keys.length;
128
+ }
129
+
130
+ stats() {
131
+ return {
132
+ entries: this.map.size,
133
+ totalBytes: this.totalBytes,
134
+ maxBytes: this.maxBytes,
135
+ hits: this.hits,
136
+ misses: this.misses,
137
+ evictions: this.evictions
138
+ };
139
+ }
140
+
141
+ #isExpired(entry) {
142
+ return entry.expiresAt !== null && Date.now() > entry.expiresAt;
143
+ }
144
+
145
+ #deleteEntry(key, entry) {
146
+ this.map.delete(key);
147
+ this.totalBytes = Math.max(0, this.totalBytes - entry.size);
148
+ }
149
+
150
+ #evictIfNeeded() {
151
+ while (this.totalBytes > this.maxBytes && this.map.size > 0) {
152
+ const oldestKey = this.map.keys().next().value;
153
+ const oldestEntry = this.map.get(oldestKey);
154
+ this.#deleteEntry(oldestKey, oldestEntry);
155
+ this.evictions += 1;
156
+ }
157
+ }
158
+ }
159
+
160
+ module.exports = LruCache;
@@ -0,0 +1,158 @@
1
+ const LruCache = require('./lru-cache');
2
+
3
+ const DEFAULT_CACHE_MAX_MB = 50;
4
+
5
+ function parseCacheMaxBytes() {
6
+ const raw = process.env.CACHE_MAX_MB;
7
+ if (raw === undefined || raw === null || raw === '') {
8
+ return DEFAULT_CACHE_MAX_MB * 1024 * 1024;
9
+ }
10
+
11
+ const parsed = Number(raw);
12
+ if (!Number.isFinite(parsed) || parsed < 0) {
13
+ return DEFAULT_CACHE_MAX_MB * 1024 * 1024;
14
+ }
15
+
16
+ return Math.floor(parsed * 1024 * 1024);
17
+ }
18
+
19
+ const cache = new LruCache({ maxBytes: parseCacheMaxBytes() });
20
+
21
+ function uniqueRefs(refs) {
22
+ return Array.from(new Set((refs || []).filter(Boolean)));
23
+ }
24
+
25
+ function remember(key, loader, refsBuilder, options = {}) {
26
+ const cachedValue = cache.get(key);
27
+ if (cachedValue !== undefined) {
28
+ return cachedValue;
29
+ }
30
+
31
+ const value = loader();
32
+ if (value === undefined) {
33
+ return value;
34
+ }
35
+
36
+ const refs = typeof refsBuilder === 'function' ? refsBuilder(value) : refsBuilder;
37
+ cache.set(key, value, {
38
+ refs: uniqueRefs(refs),
39
+ ttlMs: options.ttlMs
40
+ });
41
+ return value;
42
+ }
43
+
44
+ function skillKey(skillId) {
45
+ return `skill:${skillId}`;
46
+ }
47
+
48
+ function skillSearchKey(query) {
49
+ return `skill-search:${query || ''}`;
50
+ }
51
+
52
+ function skillExistsKey(skillId) {
53
+ return `skill-exists:${skillId}`;
54
+ }
55
+
56
+ function skillVersionsKey(skillId) {
57
+ return `skill-versions:${skillId}`;
58
+ }
59
+
60
+ function skillVersionKey(skillId, version) {
61
+ return `skill-version:${skillId}:${version}`;
62
+ }
63
+
64
+ function skillLatestKey(skillId) {
65
+ return `skill-latest:${skillId}`;
66
+ }
67
+
68
+ function userBasicKey(userId) {
69
+ return `user-basic:${userId}`;
70
+ }
71
+
72
+ function skillRefs(skill) {
73
+ if (!skill) {
74
+ return [];
75
+ }
76
+ return [`skill:${skill.id}`, `user:${skill.owner_id}`];
77
+ }
78
+
79
+ function skillSearchRefs(skills) {
80
+ const refs = ['collection:skill-search'];
81
+ for (const skill of skills || []) {
82
+ refs.push(...skillRefs(skill));
83
+ }
84
+ return refs;
85
+ }
86
+
87
+ function versionRefs(version) {
88
+ if (!version) {
89
+ return [];
90
+ }
91
+ return [
92
+ `skill:${version.skill_id}`,
93
+ `version:${version.skill_id}:${version.version}`,
94
+ `user:${version.uploader_id}`
95
+ ];
96
+ }
97
+
98
+ function versionListRefs(skillId, versions) {
99
+ const refs = [`skill:${skillId}`];
100
+ for (const version of versions || []) {
101
+ refs.push(...versionRefs(version));
102
+ }
103
+ return refs;
104
+ }
105
+
106
+ function userRefs(user) {
107
+ if (!user) {
108
+ return [];
109
+ }
110
+ return [`user:${user.id}`];
111
+ }
112
+
113
+ function invalidateSkill(skillId) {
114
+ cache.delete(skillKey(skillId));
115
+ cache.delete(skillExistsKey(skillId));
116
+ cache.delete(skillVersionsKey(skillId));
117
+ cache.delete(skillLatestKey(skillId));
118
+ cache.clearByPrefix(`skill-version:${skillId}:`);
119
+ cache.clearByRef(`skill:${skillId}`);
120
+ cache.clearByRef('collection:skill-search');
121
+ }
122
+
123
+ function invalidateUser(userId) {
124
+ cache.delete(userBasicKey(userId));
125
+ cache.clearByRef(`user:${userId}`);
126
+ }
127
+
128
+ function invalidateAllSkillSearches() {
129
+ cache.clearByRef('collection:skill-search');
130
+ }
131
+
132
+ function getStats() {
133
+ return cache.stats();
134
+ }
135
+
136
+ module.exports = {
137
+ remember,
138
+ getStats,
139
+ invalidateSkill,
140
+ invalidateUser,
141
+ invalidateAllSkillSearches,
142
+ keys: {
143
+ skill: skillKey,
144
+ skillSearch: skillSearchKey,
145
+ skillExists: skillExistsKey,
146
+ skillVersions: skillVersionsKey,
147
+ skillVersion: skillVersionKey,
148
+ skillLatest: skillLatestKey,
149
+ userBasic: userBasicKey
150
+ },
151
+ refs: {
152
+ skill: skillRefs,
153
+ skillSearch: skillSearchRefs,
154
+ version: versionRefs,
155
+ versionList: versionListRefs,
156
+ user: userRefs
157
+ }
158
+ };