skill-base 2.0.17 → 2.0.18
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 +8 -2
- package/bin/skill-base.js +73 -28
- package/package.json +2 -2
- package/src/cappy.js +162 -50
- package/src/database.js +17 -17
- package/src/index.js +75 -22
- package/src/middleware/admin.js +3 -3
- package/src/middleware/auth.js +22 -22
- package/src/middleware/error.js +4 -4
- package/src/models/skill.js +6 -6
- package/src/models/user.js +10 -10
- package/src/models/version.js +6 -6
- package/src/routes/auth.js +17 -17
- package/src/routes/collaborators.js +28 -28
- package/src/routes/init.js +7 -7
- package/src/routes/publish.js +15 -15
- package/src/routes/skills.js +13 -13
- package/src/routes/users.js +9 -9
- package/src/utils/crypto.js +6 -6
- package/src/utils/detect-language.js +56 -0
- package/src/utils/permission.js +7 -7
- package/src/utils/zip.js +6 -6
- package/static/assets/{index-Bw_H1j6L.js → index-BVgsNsqr.js} +3 -3
- package/static/assets/{index-B3cIFt5P.css → index-ByONPaqz.css} +1 -1
- package/static/index.html +2 -2
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ skb login
|
|
|
141
141
|
skb search vue # 搜索
|
|
142
142
|
skb install vue-best-practices # 安装最新版
|
|
143
143
|
skb install vue-best-practices@v20260115 # 安装指定版本
|
|
144
|
-
skb install vue-best-practices -d ./.cursor/
|
|
144
|
+
skb install vue-best-practices -d ./.cursor/skills # 安装到 Cursor Skill 目录
|
|
145
145
|
|
|
146
146
|
# 发布团队内部的新 Skill
|
|
147
147
|
skb publish ./my-business-skill --changelog "新增了报表组件的使用规范"
|
|
@@ -155,12 +155,18 @@ skb publish ./my-business-skill --changelog "新增了报表组件的使用规
|
|
|
155
155
|
skb install my-team-rules
|
|
156
156
|
|
|
157
157
|
# 或者使用快捷参数,直接注入当前项目的 AI 上下文
|
|
158
|
-
skb install team-vue-rules --ide cursor # 自动生成到 .cursor/
|
|
158
|
+
skb install team-vue-rules --ide cursor # 自动生成到 .cursor/skills/
|
|
159
159
|
skb install team-vue-rules --ide qoder # 自动生成到 .qoder/skills/
|
|
160
160
|
skb install team-vue-rules --ide copilot # 自动生成到 .github/instructions/
|
|
161
161
|
|
|
162
162
|
# 支持全局安装通用素养规则
|
|
163
163
|
skb install git-commit-rules --ide cursor --global
|
|
164
|
+
|
|
165
|
+
# 查看本地通过 skb 安装过的所有 Skill,并继续更新/删除/清记录
|
|
166
|
+
skb list
|
|
167
|
+
|
|
168
|
+
# 更新时可先选版本,再批量勾选已安装目录
|
|
169
|
+
skb update team-vue-rules
|
|
164
170
|
```
|
|
165
171
|
|
|
166
172
|
### 🌐 Web 端操作
|
package/bin/skill-base.js
CHANGED
|
@@ -7,6 +7,72 @@
|
|
|
7
7
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const fs = require('fs');
|
|
10
|
+
const { detectSystemLanguage } = require('../src/utils/detect-language');
|
|
11
|
+
|
|
12
|
+
const appLanguage = detectSystemLanguage();
|
|
13
|
+
|
|
14
|
+
function pickMessage(message) {
|
|
15
|
+
if (typeof message === 'string') return message;
|
|
16
|
+
if (!message || typeof message !== 'object') return '';
|
|
17
|
+
return message[appLanguage] || message.en || message.zh || '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const helpText = {
|
|
21
|
+
zh: `
|
|
22
|
+
Skill Base - 内网轻量版 Skill 管理平台
|
|
23
|
+
|
|
24
|
+
用法:
|
|
25
|
+
npx skill-base [options]
|
|
26
|
+
|
|
27
|
+
选项:
|
|
28
|
+
-p, --port <port> 指定端口号 (默认: 8000)
|
|
29
|
+
-h, --host <host> 指定监听地址 (默认: 0.0.0.0)
|
|
30
|
+
-d, --data-dir <path> 指定数据目录 (默认: 包内 data/)
|
|
31
|
+
--base-path <path> 指定部署前缀 (默认: /,例如: /skills/)
|
|
32
|
+
--cache-max-mb <mb> 指定进程内 LRU 缓存总容量,单位 MB (默认: 50)
|
|
33
|
+
--no-cappy 禁用 Cappy 水豚吉祥物
|
|
34
|
+
-v, --verbose 启用调试信息
|
|
35
|
+
--help 显示帮助信息
|
|
36
|
+
--version 显示版本号
|
|
37
|
+
|
|
38
|
+
示例:
|
|
39
|
+
npx skill-base # 启动服务 (端口 8000)
|
|
40
|
+
npx skill-base -p 3000 # 使用端口 3000
|
|
41
|
+
npx skill-base --host 127.0.0.1 # 仅本地访问
|
|
42
|
+
npx skill-base -d ./data # 数据存储到当前目录的 data 文件夹
|
|
43
|
+
npx skill-base -d . -p 3000 # 数据存储到当前目录
|
|
44
|
+
npx skill-base --base-path /skills/ # 部署在子路径下
|
|
45
|
+
npx skill-base --cache-max-mb 100 # 将 LRU 缓存上限调整为 100MB
|
|
46
|
+
npx skill-base --no-cappy # 禁用吉祥物
|
|
47
|
+
`,
|
|
48
|
+
en: `
|
|
49
|
+
Skill Base - Lightweight skill management platform
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
npx skill-base [options]
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
-p, --port <port> Set the port number (default: 8000)
|
|
56
|
+
-h, --host <host> Set the listen address (default: 0.0.0.0)
|
|
57
|
+
-d, --data-dir <path> Set the data directory (default: bundled data/)
|
|
58
|
+
--base-path <path> Set the deploy prefix (default: /, for example: /skills/)
|
|
59
|
+
--cache-max-mb <mb> Set total in-process LRU cache size in MB (default: 50)
|
|
60
|
+
--no-cappy Disable the Cappy mascot
|
|
61
|
+
-v, --verbose Enable debug logs
|
|
62
|
+
--help Show help
|
|
63
|
+
--version Show version
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
npx skill-base # Start the server on port 8000
|
|
67
|
+
npx skill-base -p 3000 # Use port 3000
|
|
68
|
+
npx skill-base --host 127.0.0.1 # Local access only
|
|
69
|
+
npx skill-base -d ./data # Store data in ./data
|
|
70
|
+
npx skill-base -d . -p 3000 # Store data in the current directory
|
|
71
|
+
npx skill-base --base-path /skills/ # Deploy under a sub path
|
|
72
|
+
npx skill-base --cache-max-mb 100 # Raise LRU cache limit to 100MB
|
|
73
|
+
npx skill-base --no-cappy # Disable the mascot
|
|
74
|
+
`
|
|
75
|
+
};
|
|
10
76
|
|
|
11
77
|
// 解析命令行参数
|
|
12
78
|
const args = process.argv.slice(2);
|
|
@@ -39,33 +105,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
39
105
|
} else if (args[i] === '-v' || args[i] === '--verbose') {
|
|
40
106
|
debug = true;
|
|
41
107
|
} else if (args[i] === '--help') {
|
|
42
|
-
console.log(
|
|
43
|
-
Skill Base - 内网轻量版 Skill 管理平台
|
|
44
|
-
|
|
45
|
-
Usage:
|
|
46
|
-
npx skill-base [options]
|
|
47
|
-
|
|
48
|
-
Options:
|
|
49
|
-
-p, --port <port> 指定端口号 (默认: 8000)
|
|
50
|
-
-h, --host <host> 指定监听地址 (默认: 0.0.0.0)
|
|
51
|
-
-d, --data-dir <path> 指定数据目录 (默认: 包内 data/)
|
|
52
|
-
--base-path <path> 指定部署前缀 (默认: /,例如: /skills/)
|
|
53
|
-
--cache-max-mb <mb> 指定进程内 LRU 缓存总容量,单位 MB (默认: 50)
|
|
54
|
-
--no-cappy 禁用 Cappy 水豚吉祥物
|
|
55
|
-
-v, --verbose 启用调试信息
|
|
56
|
-
--help 显示帮助信息
|
|
57
|
-
--version 显示版本号
|
|
58
|
-
|
|
59
|
-
Examples:
|
|
60
|
-
npx skill-base # 启动服务 (端口 8000)
|
|
61
|
-
npx skill-base -p 3000 # 使用端口 3000
|
|
62
|
-
npx skill-base --host 127.0.0.1 # 仅本地访问
|
|
63
|
-
npx skill-base -d ./data # 数据存储到当前目录的 data 文件夹
|
|
64
|
-
npx skill-base -d . -p 3000 # 数据存储到当前目录
|
|
65
|
-
npx skill-base --base-path /skills/ # 部署在子路径下
|
|
66
|
-
npx skill-base --cache-max-mb 100 # 将 LRU 缓存上限调整为 100MB
|
|
67
|
-
npx skill-base --no-cappy # 禁用吉祥物
|
|
68
|
-
`);
|
|
108
|
+
console.log(pickMessage(helpText));
|
|
69
109
|
process.exit(0);
|
|
70
110
|
} else if (args[i] === '--version') {
|
|
71
111
|
const pkg = require('../package.json');
|
|
@@ -90,7 +130,12 @@ if (dataDir) {
|
|
|
90
130
|
}
|
|
91
131
|
process.env.DATA_DIR = dataDir;
|
|
92
132
|
process.env.DATABASE_PATH = path.join(dataDir, 'skills.db');
|
|
93
|
-
console.log(
|
|
133
|
+
console.log(
|
|
134
|
+
pickMessage({
|
|
135
|
+
zh: `数据目录: ${dataDir}`,
|
|
136
|
+
en: `Data directory: ${dataDir}`
|
|
137
|
+
})
|
|
138
|
+
);
|
|
94
139
|
}
|
|
95
140
|
|
|
96
141
|
// 启动服务
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-base",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Skill Base - 私有部署的轻量级 Skill 管理平台",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dev": "nodemon src/index.js",
|
|
18
18
|
"build": "cd web && pnpm build",
|
|
19
19
|
"web:dev": "cd web && pnpm dev",
|
|
20
|
-
"test": "node --test tests/**/*.test.js"
|
|
20
|
+
"test": "node --test tests/**/*.test.js tests/cli/*.test.mjs"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@fastify/cookie": "^11.0.2",
|
package/src/cappy.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
const readline = require('readline');
|
|
2
|
+
const { detectSystemLanguage } = require('./utils/detect-language');
|
|
2
3
|
|
|
3
4
|
class CappyMascot {
|
|
4
|
-
|
|
5
|
+
static detectSystemLanguage() {
|
|
6
|
+
return detectSystemLanguage();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
constructor(port, basePath = '/', language = detectSystemLanguage()) {
|
|
5
10
|
this.port = port;
|
|
6
11
|
this.basePath = basePath;
|
|
12
|
+
this.lang = language === 'zh' ? 'zh' : 'en';
|
|
7
13
|
this.frameTimer = null;
|
|
8
14
|
this.sceneTimer = null;
|
|
9
15
|
this.lastRenderHeight = 0;
|
|
@@ -21,14 +27,93 @@ class CappyMascot {
|
|
|
21
27
|
reset: '\x1b[0m'
|
|
22
28
|
};
|
|
23
29
|
|
|
30
|
+
this.text = {
|
|
31
|
+
intro: {
|
|
32
|
+
zh: `Skill Base 正在预热,端口 ${port} 已就绪。`,
|
|
33
|
+
en: `Skill Base is warming up; port ${port} is live.`
|
|
34
|
+
},
|
|
35
|
+
idle: [
|
|
36
|
+
{
|
|
37
|
+
zh: `Cappy 正盯着端口 ${port}。`,
|
|
38
|
+
en: `Cappy is staring at port ${port}.`
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
zh: '一切安静。没有过度设计,就没有运行时焦虑。',
|
|
42
|
+
en: 'All quiet. No over-engineering, no runtime anxiety.'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
zh: '技能库很平静。代码直接,系统就稳。',
|
|
46
|
+
en: 'The skill library is calm. Straightforward code brings that peace.'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
zh: '系统稳定。Cappy 讨厌没有意义的复杂度。',
|
|
50
|
+
en: 'System stable. Cappy despises pointless complexity.'
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
blink: [
|
|
54
|
+
{
|
|
55
|
+
zh: '慢慢眨眼。不是摸鱼,是低成本巡逻。',
|
|
56
|
+
en: 'Slow blink. Not slacking—low-cost patrolling.'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
zh: '写简单代码,比堆一堆监控脚本更靠谱。',
|
|
60
|
+
en: 'Better to write simple code than to pile on monitoring scripts.'
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
think: [
|
|
64
|
+
{
|
|
65
|
+
zh: '简单架构才会赢。真正的稳定不靠花哨设计。',
|
|
66
|
+
en: 'Simple architecture wins. Real stability needs no fancy design.'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
zh: '在思考。直接把代码写明白,胜过自作聪明的抽象层。',
|
|
70
|
+
en: 'Thinking. Writing code directly beats clever layers of abstraction.'
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
soak: [
|
|
74
|
+
{
|
|
75
|
+
zh: '数据结构一旦对了,逻辑自然像水一样流动。',
|
|
76
|
+
en: 'Get the data structures right and the logic flows like water.'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
zh: '先泡一会儿。别猜未来需求,YAGNI 就够了。',
|
|
80
|
+
en: 'Let it soak in. Do not guess future needs—YAGNI.'
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
scout: [
|
|
84
|
+
{
|
|
85
|
+
zh: '出去转一圈,确认没人又开始过度设计。',
|
|
86
|
+
en: 'Short walk. Checking no one over-engineered anything.'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
zh: '不要多余动作,只做有意义的移动。代码也该这样。',
|
|
90
|
+
en: 'No extra steps. Only moves that matter. Code should be the same.'
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
work: {
|
|
94
|
+
zh: '收到任务。Cappy 会用最朴素的办法处理。',
|
|
95
|
+
en: 'Task received. Cappy is handling it the plain way.'
|
|
96
|
+
},
|
|
97
|
+
actionFallback: {
|
|
98
|
+
zh: '有新情况发生了,Cappy 继续稳住。',
|
|
99
|
+
en: 'Something new happened; Cappy stays steady.'
|
|
100
|
+
},
|
|
101
|
+
goodbye: {
|
|
102
|
+
zh: 'Cappy 下班了,明天见。',
|
|
103
|
+
en: 'Cappy is off duty. See you tomorrow.'
|
|
104
|
+
},
|
|
105
|
+
footer: {
|
|
106
|
+
zh: 'Cappy 值班中',
|
|
107
|
+
en: 'Cappy on duty'
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
24
111
|
// Scenes: personality + frames + messages.
|
|
25
112
|
this.scenes = {
|
|
26
113
|
intro: {
|
|
27
114
|
frameDelay: 240,
|
|
28
115
|
loops: 1,
|
|
29
|
-
messages: [
|
|
30
|
-
`Skill Base is warming up; port ${port} is live.`
|
|
31
|
-
],
|
|
116
|
+
messages: [this.resolveMessage(this.text.intro)],
|
|
32
117
|
frames: [
|
|
33
118
|
{ color: 'warm', sprite: this.createSprite('o o', '___', 'paw') },
|
|
34
119
|
{ color: 'orange', sprite: this.createSprite('- -', '___', 'paw') },
|
|
@@ -39,12 +124,7 @@ class CappyMascot {
|
|
|
39
124
|
weight: 5,
|
|
40
125
|
frameDelay: 320,
|
|
41
126
|
loops: 4,
|
|
42
|
-
messages:
|
|
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
|
-
],
|
|
127
|
+
messages: this.resolveMessages(this.text.idle),
|
|
48
128
|
frames: [
|
|
49
129
|
{ color: 'warm', sprite: this.createSprite('o o', '___', 'still') },
|
|
50
130
|
{ color: 'warm', sprite: this.createSprite('o o', '___', 'breath') },
|
|
@@ -55,10 +135,7 @@ class CappyMascot {
|
|
|
55
135
|
weight: 2,
|
|
56
136
|
frameDelay: 180,
|
|
57
137
|
loops: 2,
|
|
58
|
-
messages:
|
|
59
|
-
'Slow blink. Not slacking—low-cost patrolling.',
|
|
60
|
-
'Better to write simple code than to pile on monitoring scripts.'
|
|
61
|
-
],
|
|
138
|
+
messages: this.resolveMessages(this.text.blink),
|
|
62
139
|
frames: [
|
|
63
140
|
{ color: 'warm', sprite: this.createSprite('o o', '___', 'still') },
|
|
64
141
|
{ color: 'warm', sprite: this.createSprite('- -', '___', 'still') },
|
|
@@ -69,10 +146,7 @@ class CappyMascot {
|
|
|
69
146
|
weight: 1,
|
|
70
147
|
frameDelay: 280,
|
|
71
148
|
loops: 3,
|
|
72
|
-
messages:
|
|
73
|
-
'Simple architecture wins. Real stability needs no fancy design.',
|
|
74
|
-
'Thinking. Writing code directly beats clever layers of abstraction.'
|
|
75
|
-
],
|
|
149
|
+
messages: this.resolveMessages(this.text.think),
|
|
76
150
|
frames: [
|
|
77
151
|
{ color: 'cyan', sprite: this.createSprite('o o', '___', 'think-left') },
|
|
78
152
|
{ color: 'cyan', sprite: this.createSprite('^ ^', '___', 'think-mid') },
|
|
@@ -83,10 +157,7 @@ class CappyMascot {
|
|
|
83
157
|
weight: 1,
|
|
84
158
|
frameDelay: 340,
|
|
85
159
|
loops: 3,
|
|
86
|
-
messages:
|
|
87
|
-
'Get the data structures right and the logic flows like water.',
|
|
88
|
-
'Let it soak in. Do not guess future needs—YAGNI.'
|
|
89
|
-
],
|
|
160
|
+
messages: this.resolveMessages(this.text.soak),
|
|
90
161
|
frames: [
|
|
91
162
|
{ color: 'pink', sprite: this.createSprite('^ ^', '~~~', 'steam-left') },
|
|
92
163
|
{ color: 'pink', sprite: this.createSprite('- -', '~~~', 'steam-mid') },
|
|
@@ -97,10 +168,7 @@ class CappyMascot {
|
|
|
97
168
|
weight: 1,
|
|
98
169
|
frameDelay: 220,
|
|
99
170
|
loops: 4,
|
|
100
|
-
messages:
|
|
101
|
-
'Short walk. Checking no one over-engineered anything.',
|
|
102
|
-
'No extra steps. Only moves that matter. Code should be the same.'
|
|
103
|
-
],
|
|
171
|
+
messages: this.resolveMessages(this.text.scout),
|
|
104
172
|
frames: [
|
|
105
173
|
{ color: 'cyan', sprite: this.createSprite('o o', '___', 'step-left') },
|
|
106
174
|
{ color: 'cyan', sprite: this.createSprite('o o', '___', 'step-mid') },
|
|
@@ -110,9 +178,7 @@ class CappyMascot {
|
|
|
110
178
|
work: {
|
|
111
179
|
frameDelay: 180,
|
|
112
180
|
loops: 6,
|
|
113
|
-
messages: [
|
|
114
|
-
'Task received. Cappy is handling it the plain way.'
|
|
115
|
-
],
|
|
181
|
+
messages: [this.resolveMessage(this.text.work)],
|
|
116
182
|
frames: [
|
|
117
183
|
{ color: 'cyan', sprite: this.createSprite('> <', '===', 'spark-left') },
|
|
118
184
|
{ color: 'orange', sprite: this.createSprite('> <', '===', 'spark-right') }
|
|
@@ -141,7 +207,7 @@ class CappyMascot {
|
|
|
141
207
|
if (!this.isRunning || this.isStopped) return;
|
|
142
208
|
|
|
143
209
|
this.playScene('work', {
|
|
144
|
-
message: message ||
|
|
210
|
+
message: this.resolveMessage(message || this.text.actionFallback),
|
|
145
211
|
onDone: () => this.scheduleNextIdle(600)
|
|
146
212
|
});
|
|
147
213
|
}
|
|
@@ -155,14 +221,15 @@ class CappyMascot {
|
|
|
155
221
|
clearTimeout(this.sceneTimer);
|
|
156
222
|
|
|
157
223
|
this.clearRender();
|
|
158
|
-
const goodbyeMsg =
|
|
159
|
-
const
|
|
160
|
-
const
|
|
224
|
+
const goodbyeMsg = this.resolveMessage(this.text.goodbye);
|
|
225
|
+
const goodbyeWidth = this.getDisplayWidth(goodbyeMsg);
|
|
226
|
+
const gBorder = '─'.repeat(goodbyeWidth + 2);
|
|
227
|
+
const gSeg = goodbyeWidth + 1;
|
|
161
228
|
const gLeft = Math.floor(gSeg / 2);
|
|
162
229
|
const gRight = gSeg - gLeft;
|
|
163
230
|
const goodbye = [
|
|
164
231
|
`${this.colors.soft} ╭${gBorder}╮${this.colors.reset}`,
|
|
165
|
-
`${this.colors.soft} │ ${goodbyeMsg} │${this.colors.reset}`,
|
|
232
|
+
`${this.colors.soft} │ ${this.padDisplay(goodbyeMsg, goodbyeWidth)} │${this.colors.reset}`,
|
|
166
233
|
`${this.colors.soft} ╰${'─'.repeat(gLeft)}┬${'─'.repeat(gRight)}╯${this.colors.reset}`,
|
|
167
234
|
`${this.colors.warm} \\${this.colors.reset}`,
|
|
168
235
|
`${this.colors.warm} __${this.colors.reset}`,
|
|
@@ -230,7 +297,7 @@ class CappyMascot {
|
|
|
230
297
|
const lines = [
|
|
231
298
|
...this.buildBubble(message),
|
|
232
299
|
...frame.sprite.map((line) => ` ${this.colors[frame.color]}${line}${this.colors.reset}`),
|
|
233
|
-
` ${this.colors.soft}http://localhost:${this.port}${this.basePath} |
|
|
300
|
+
` ${this.colors.soft}http://localhost:${this.port}${this.basePath} | ${this.resolveMessage(this.text.footer)}${this.colors.reset}`
|
|
234
301
|
];
|
|
235
302
|
|
|
236
303
|
this.clearRender();
|
|
@@ -258,11 +325,11 @@ class CappyMascot {
|
|
|
258
325
|
buildBubble(message) {
|
|
259
326
|
const maxLineWidth = 44;
|
|
260
327
|
const lines = this.wrapText(String(message), maxLineWidth);
|
|
261
|
-
const innerWidth = Math.max(1, ...lines.map((
|
|
328
|
+
const innerWidth = Math.max(1, ...lines.map((line) => this.getDisplayWidth(line)));
|
|
262
329
|
const border = '─'.repeat(innerWidth + 2);
|
|
263
330
|
const body = lines.map(
|
|
264
331
|
(line) =>
|
|
265
|
-
`${this.colors.soft} │ ${
|
|
332
|
+
`${this.colors.soft} │ ${this.padDisplay(line, innerWidth)} │${this.colors.reset}`
|
|
266
333
|
);
|
|
267
334
|
|
|
268
335
|
return [
|
|
@@ -299,23 +366,23 @@ class CappyMascot {
|
|
|
299
366
|
for (const word of words) {
|
|
300
367
|
if (!word.length) continue;
|
|
301
368
|
|
|
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
369
|
const next = line.length ? `${line} ${word}` : word;
|
|
314
|
-
if (next
|
|
370
|
+
if (this.getDisplayWidth(next) <= maxWidth) {
|
|
315
371
|
line = next;
|
|
316
372
|
} else {
|
|
317
373
|
flush();
|
|
318
|
-
|
|
374
|
+
if (this.getDisplayWidth(word) <= maxWidth) {
|
|
375
|
+
line = word;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let rest = word;
|
|
380
|
+
while (this.getDisplayWidth(rest) > maxWidth) {
|
|
381
|
+
const chunk = this.takeByWidth(rest, maxWidth);
|
|
382
|
+
out.push(chunk);
|
|
383
|
+
rest = rest.slice(chunk.length);
|
|
384
|
+
}
|
|
385
|
+
line = rest;
|
|
319
386
|
}
|
|
320
387
|
}
|
|
321
388
|
flush();
|
|
@@ -324,6 +391,51 @@ class CappyMascot {
|
|
|
324
391
|
return out.length ? out : [''];
|
|
325
392
|
}
|
|
326
393
|
|
|
394
|
+
resolveMessage(message) {
|
|
395
|
+
if (typeof message === 'string') return message;
|
|
396
|
+
if (!message || typeof message !== 'object') return '';
|
|
397
|
+
return message[this.lang] || message.en || message.zh || '';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
resolveMessages(messages) {
|
|
401
|
+
return messages.map((message) => this.resolveMessage(message));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getDisplayWidth(text) {
|
|
405
|
+
let width = 0;
|
|
406
|
+
|
|
407
|
+
for (const char of text) {
|
|
408
|
+
width += this.getCharWidth(char);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return width;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getCharWidth(char) {
|
|
415
|
+
return /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff01-\uff60\uffe0-\uffe6]/u.test(char)
|
|
416
|
+
? 2
|
|
417
|
+
: 1;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
takeByWidth(text, maxWidth) {
|
|
421
|
+
let width = 0;
|
|
422
|
+
let index = 0;
|
|
423
|
+
|
|
424
|
+
for (const char of text) {
|
|
425
|
+
const charWidth = this.getCharWidth(char);
|
|
426
|
+
if (index > 0 && width + charWidth > maxWidth) break;
|
|
427
|
+
width += charWidth;
|
|
428
|
+
index += char.length;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return text.slice(0, index || 1);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
padDisplay(text, targetWidth) {
|
|
435
|
+
const padding = Math.max(0, targetWidth - this.getDisplayWidth(text));
|
|
436
|
+
return text + ' '.repeat(padding);
|
|
437
|
+
}
|
|
438
|
+
|
|
327
439
|
pickIdleScene() {
|
|
328
440
|
const entries = Object.entries(this.scenes)
|
|
329
441
|
.filter(([key, scene]) => scene.weight)
|
package/src/database.js
CHANGED
|
@@ -2,21 +2,21 @@ const Database = require('better-sqlite3');
|
|
|
2
2
|
const bcrypt = require('bcryptjs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Database file path, supports environment variable configuration
|
|
6
6
|
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../data/skills.db');
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// Create database connection
|
|
9
9
|
const db = new Database(dbPath);
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Enable WAL mode for better concurrency performance
|
|
12
12
|
db.pragma('journal_mode = WAL');
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Enable foreign key constraints
|
|
15
15
|
db.pragma('foreign_keys = ON');
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// Table creation SQL
|
|
18
18
|
const createTablesSql = `
|
|
19
|
-
--
|
|
19
|
+
-- Users table
|
|
20
20
|
CREATE TABLE IF NOT EXISTS users (
|
|
21
21
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
22
|
username TEXT UNIQUE NOT NULL,
|
|
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|
|
26
26
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
-
-- CLI
|
|
29
|
+
-- CLI temporary auth code table
|
|
30
30
|
CREATE TABLE IF NOT EXISTS cli_auth_codes (
|
|
31
31
|
code TEXT PRIMARY KEY,
|
|
32
32
|
user_id INTEGER NOT NULL,
|
|
@@ -35,7 +35,7 @@ CREATE TABLE IF NOT EXISTS cli_auth_codes (
|
|
|
35
35
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
36
36
|
);
|
|
37
37
|
|
|
38
|
-
--
|
|
38
|
+
-- Personal Access Token table (PAT)
|
|
39
39
|
CREATE TABLE IF NOT EXISTS personal_access_tokens (
|
|
40
40
|
token TEXT PRIMARY KEY,
|
|
41
41
|
user_id INTEGER NOT NULL,
|
|
@@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS personal_access_tokens (
|
|
|
45
45
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
-- Skill
|
|
48
|
+
-- Skill main table
|
|
49
49
|
CREATE TABLE IF NOT EXISTS skills (
|
|
50
50
|
id TEXT PRIMARY KEY,
|
|
51
51
|
name TEXT NOT NULL,
|
|
@@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS skills (
|
|
|
57
57
|
FOREIGN KEY (owner_id) REFERENCES users(id)
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
-
--
|
|
60
|
+
-- Version table
|
|
61
61
|
CREATE TABLE IF NOT EXISTS skill_versions (
|
|
62
62
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
63
|
skill_id TEXT NOT NULL,
|
|
@@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS skill_versions (
|
|
|
71
71
|
UNIQUE(skill_id, version)
|
|
72
72
|
);
|
|
73
73
|
|
|
74
|
-
-- Skill
|
|
74
|
+
-- Skill collaborators table
|
|
75
75
|
CREATE TABLE IF NOT EXISTS skill_collaborators (
|
|
76
76
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
77
|
skill_id TEXT NOT NULL,
|
|
@@ -85,7 +85,7 @@ CREATE TABLE IF NOT EXISTS skill_collaborators (
|
|
|
85
85
|
UNIQUE(skill_id, user_id)
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
-
-- Session
|
|
88
|
+
-- Session table (optional, enabled via SESSION_STORE=sqlite)
|
|
89
89
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
90
90
|
session_id TEXT PRIMARY KEY,
|
|
91
91
|
user_id INTEGER NOT NULL,
|
|
@@ -94,7 +94,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
94
94
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
95
95
|
);
|
|
96
96
|
|
|
97
|
-
--
|
|
97
|
+
-- Indexes
|
|
98
98
|
CREATE INDEX IF NOT EXISTS idx_versions_skill_id ON skill_versions(skill_id);
|
|
99
99
|
CREATE INDEX IF NOT EXISTS idx_cli_codes_user ON cli_auth_codes(user_id);
|
|
100
100
|
CREATE INDEX IF NOT EXISTS idx_pat_tokens_user ON personal_access_tokens(user_id);
|
|
@@ -104,12 +104,12 @@ CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
|
104
104
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
105
105
|
`;
|
|
106
106
|
|
|
107
|
-
//
|
|
107
|
+
// Execute table creation statements
|
|
108
108
|
db.exec(createTablesSql);
|
|
109
109
|
|
|
110
|
-
//
|
|
110
|
+
// Safely add new columns (if not exists)
|
|
111
111
|
try { db.exec("ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active'"); } catch(e) {}
|
|
112
|
-
// SQLite
|
|
112
|
+
// SQLite does not support ALTER TABLE with CURRENT_TIMESTAMP default, requires two steps
|
|
113
113
|
try {
|
|
114
114
|
db.exec("ALTER TABLE users ADD COLUMN updated_at DATETIME");
|
|
115
115
|
db.exec("UPDATE users SET updated_at = datetime('now') WHERE updated_at IS NULL");
|
|
@@ -118,7 +118,7 @@ try { db.exec("ALTER TABLE users ADD COLUMN created_by INTEGER REFERENCES users(
|
|
|
118
118
|
try { db.exec("ALTER TABLE users ADD COLUMN name TEXT"); } catch(e) {}
|
|
119
119
|
try { db.exec("ALTER TABLE skill_versions ADD COLUMN description TEXT"); } catch(e) {}
|
|
120
120
|
|
|
121
|
-
//
|
|
121
|
+
// Data migration: insert skill_collaborators record for existing Skills owners
|
|
122
122
|
const existingSkills = db.prepare('SELECT id, owner_id FROM skills').all();
|
|
123
123
|
const insertCollaborator = db.prepare(`
|
|
124
124
|
INSERT OR IGNORE INTO skill_collaborators (skill_id, user_id, role, created_by)
|