skill-base 2.0.15 → 2.0.17
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 +22 -0
- package/bin/skill-base.js +7 -0
- package/package.json +1 -1
- package/src/cappy.js +86 -33
- package/src/index.js +45 -32
- package/src/models/skill.js +53 -27
- package/src/models/user.js +27 -2
- package/src/models/version.js +39 -21
- package/src/routes/collaborators.js +5 -0
- package/src/routes/publish.js +2 -0
- package/src/utils/lru-cache.js +160 -0
- package/src/utils/model-cache.js +158 -0
- package/static/assets/index-B3cIFt5P.css +1 -0
- package/static/assets/{index-BHB0vddE.js → index-Bw_H1j6L.js} +43 -43
- package/static/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/static/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/static/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/static/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/static/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/static/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/static/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
- package/static/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
- package/static/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/static/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/static/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/static/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/static/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/static/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/static/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
- package/static/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
- package/static/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/static/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/static/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/static/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/static/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/static/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/static/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
- package/static/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
- package/static/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/static/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/static/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/static/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/static/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/static/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/static/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
- package/static/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
- package/static/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/static/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/static/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/static/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/static/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/static/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/static/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/static/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/static/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/static/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/static/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/static/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/static/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/static/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/static/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
- package/static/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
- package/static/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/static/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/static/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/static/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/static/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/static/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/static/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
- package/static/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
- package/static/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/static/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/static/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/static/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/static/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/static/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/static/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/static/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/static/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/static/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/static/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/static/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/static/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/static/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/static/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/static/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/static/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/static/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/static/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/static/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/static/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/static/index.html +2 -5
- package/static/assets/index-EVWfLxoq.css +0 -1
package/README.md
CHANGED
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## 界面预览
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
*首页:浏览与检索团队 Skill*
|
|
17
|
+
|
|
18
|
+

|
|
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
package/src/cappy.js
CHANGED
|
@@ -21,13 +21,13 @@ class CappyMascot {
|
|
|
21
21
|
reset: '\x1b[0m'
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
//
|
|
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
|
|
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
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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}
|
|
160
|
-
`${this.colors.soft} │
|
|
161
|
-
`${this.colors.soft}
|
|
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
|
|
255
|
-
const
|
|
256
|
-
const
|
|
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} ╭${
|
|
260
|
-
|
|
261
|
-
`${this.colors.soft} ╰${
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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 —
|
|
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
|
-
//
|
|
97
|
+
// Auth (authenticate, createSession, …)
|
|
93
98
|
await fastify.register(require('./middleware/auth'));
|
|
94
|
-
//
|
|
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.
|
|
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 {
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
162
|
+
// Cappy (decorate before listen)
|
|
150
163
|
cappy = new CappyMascot(PORT, APP_BASE_PATH);
|
|
151
164
|
fastify.decorate('cappy', cappy);
|
|
152
165
|
|
|
153
|
-
//
|
|
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('
|
|
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('
|
|
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
|
-
//
|
|
192
|
+
// Start Cappy loop
|
|
180
193
|
cappy.start();
|
|
181
194
|
}
|
|
182
195
|
} catch (err) {
|
package/src/models/skill.js
CHANGED
|
@@ -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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|