skill-base 2.0.1
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 +141 -0
- package/bin/skill-base.js +53 -0
- package/data/.gitkeep +0 -0
- package/package.json +36 -0
- package/src/database.js +119 -0
- package/src/index.js +88 -0
- package/src/middleware/.gitkeep +0 -0
- package/src/middleware/admin.js +23 -0
- package/src/middleware/auth.js +96 -0
- package/src/middleware/error.js +23 -0
- package/src/models/.gitkeep +0 -0
- package/src/models/skill.js +57 -0
- package/src/models/user.js +130 -0
- package/src/models/version.js +57 -0
- package/src/routes/.gitkeep +0 -0
- package/src/routes/auth.js +173 -0
- package/src/routes/collaborators.js +260 -0
- package/src/routes/init.js +86 -0
- package/src/routes/publish.js +108 -0
- package/src/routes/skills.js +119 -0
- package/src/routes/users.js +169 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/crypto.js +35 -0
- package/src/utils/permission.js +45 -0
- package/src/utils/zip.js +35 -0
- package/static/admin/users.html +593 -0
- package/static/cli-code.html +203 -0
- package/static/css/.gitkeep +0 -0
- package/static/css/style.css +1567 -0
- package/static/diff.html +466 -0
- package/static/file.html +443 -0
- package/static/index.html +251 -0
- package/static/js/.gitkeep +0 -0
- package/static/js/admin/users.js +346 -0
- package/static/js/app.js +508 -0
- package/static/js/auth.js +151 -0
- package/static/js/cli-code.js +184 -0
- package/static/js/collaborators.js +283 -0
- package/static/js/diff.js +540 -0
- package/static/js/file.js +619 -0
- package/static/js/i18n.js +739 -0
- package/static/js/index.js +168 -0
- package/static/js/publish.js +718 -0
- package/static/js/settings.js +124 -0
- package/static/js/setup.js +157 -0
- package/static/js/skill.js +808 -0
- package/static/login.html +82 -0
- package/static/publish.html +459 -0
- package/static/settings.html +163 -0
- package/static/setup.html +101 -0
- package/static/skill.html +851 -0
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Skill Base
|
|
2
|
+
|
|
3
|
+
轻量级 AI Agent Skill 管理平台,支持 Web 端和命令行工具。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **Skill 管理** - 搜索、安装、更新、发布 AI Agent Skills
|
|
8
|
+
- **版本控制** - 每个 Skill 支持多版本管理
|
|
9
|
+
- **协作者管理** - 支持多人协作维护 Skill
|
|
10
|
+
- **双端支持** - Web 界面 + CLI 命令行工具
|
|
11
|
+
|
|
12
|
+
## 快速开始
|
|
13
|
+
|
|
14
|
+
### 使用 npx 一键启动
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 直接运行(默认端口 8000)
|
|
18
|
+
npx skill-base
|
|
19
|
+
|
|
20
|
+
# 指定端口
|
|
21
|
+
npx skill-base -p 3000
|
|
22
|
+
|
|
23
|
+
# 仅本地访问
|
|
24
|
+
npx skill-base --host 127.0.0.1
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Web 端使用
|
|
28
|
+
|
|
29
|
+
1. 访问平台首页,注册/登录账号
|
|
30
|
+
2. 浏览或搜索需要的 Skill
|
|
31
|
+
3. 点击 Skill 查看详情和历史版本
|
|
32
|
+
4. 点击下载按钮获取 Skill 文件
|
|
33
|
+
|
|
34
|
+
**发布 Skill:**
|
|
35
|
+
1. 登录后点击「发布」
|
|
36
|
+
2. 选择包含 `SKILL.md` 的文件夹
|
|
37
|
+
3. 填写版本说明后提交
|
|
38
|
+
|
|
39
|
+
### CLI 使用
|
|
40
|
+
|
|
41
|
+
安装 CLI 工具:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g skill-base-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
配置服务器地址(默认 localhost:8000):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
export SKB_BASE_URL=https://your-server.com
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
常用命令:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# 登录
|
|
57
|
+
skb login
|
|
58
|
+
|
|
59
|
+
# 搜索 Skill
|
|
60
|
+
skb search vue
|
|
61
|
+
|
|
62
|
+
# 安装 Skill
|
|
63
|
+
skb install vue-best-practices
|
|
64
|
+
skb install vue-best-practices@v20260115.120000 # 指定版本
|
|
65
|
+
skb install vue-best-practices -d ./my-skills # 指定目录
|
|
66
|
+
|
|
67
|
+
# 更新 Skill
|
|
68
|
+
skb update vue-best-practices
|
|
69
|
+
|
|
70
|
+
# 发布 Skill
|
|
71
|
+
skb publish ./my-skill --changelog "初始版本"
|
|
72
|
+
|
|
73
|
+
# 登出
|
|
74
|
+
skb logout
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 部署服务端
|
|
78
|
+
|
|
79
|
+
### 环境要求
|
|
80
|
+
|
|
81
|
+
- Node.js >= 18.0.0
|
|
82
|
+
|
|
83
|
+
### 本地运行
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# 安装依赖
|
|
87
|
+
npm install
|
|
88
|
+
|
|
89
|
+
# 开发模式
|
|
90
|
+
npm run dev
|
|
91
|
+
|
|
92
|
+
# 生产模式
|
|
93
|
+
npm start
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 初始化管理员账号
|
|
97
|
+
|
|
98
|
+
首次启动时,如果系统中还没有管理员账号,访问任何页面会自动跳转到初始化设置页面,请按提示设置管理员用户名和密码。
|
|
99
|
+
|
|
100
|
+
> **安全提示**:请妥善保管管理员凭据,建议使用强密码。
|
|
101
|
+
|
|
102
|
+
### Docker 部署
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
docker build -t skill-base .
|
|
106
|
+
docker run -p 8000:8000 -v ./data:/app/data skill-base
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 项目结构
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
skill-base/
|
|
113
|
+
├── cli/ # CLI 命令行工具
|
|
114
|
+
├── src/ # 服务端源码
|
|
115
|
+
├── static/ # Web 前端
|
|
116
|
+
├── data/ # 数据存储
|
|
117
|
+
└── docs/ # 文档
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## SKILL.md 规范
|
|
121
|
+
|
|
122
|
+
每个 Skill 必须包含 `SKILL.md` 文件,平台会自动解析:
|
|
123
|
+
|
|
124
|
+
- **name**: 第一个 `#` 标题
|
|
125
|
+
- **description**: 标题后的第一段文本
|
|
126
|
+
|
|
127
|
+
示例:
|
|
128
|
+
|
|
129
|
+
```markdown
|
|
130
|
+
# Vue Best Practices
|
|
131
|
+
|
|
132
|
+
Vue.js 开发最佳实践指南,包含组件设计、状态管理等内容。
|
|
133
|
+
|
|
134
|
+
## 使用方法
|
|
135
|
+
|
|
136
|
+
...
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill Base CLI Entry
|
|
5
|
+
* 启动 Skill Base Web 服务
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// 解析命令行参数
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
let port = 8000;
|
|
13
|
+
let host = '0.0.0.0';
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
if ((args[i] === '-p' || args[i] === '--port') && args[i + 1]) {
|
|
17
|
+
port = parseInt(args[i + 1], 10);
|
|
18
|
+
i++;
|
|
19
|
+
} else if ((args[i] === '-h' || args[i] === '--host') && args[i + 1]) {
|
|
20
|
+
host = args[i + 1];
|
|
21
|
+
i++;
|
|
22
|
+
} else if (args[i] === '--help') {
|
|
23
|
+
console.log(`
|
|
24
|
+
Skill Base - 内网轻量版 Skill 管理平台
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
npx skill-base [options]
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
-p, --port <port> 指定端口号 (默认: 8000)
|
|
31
|
+
-h, --host <host> 指定监听地址 (默认: 0.0.0.0)
|
|
32
|
+
--help 显示帮助信息
|
|
33
|
+
--version 显示版本号
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
npx skill-base # 启动服务 (端口 8000)
|
|
37
|
+
npx skill-base -p 3000 # 使用端口 3000
|
|
38
|
+
npx skill-base --host 127.0.0.1 # 仅本地访问
|
|
39
|
+
`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
} else if (args[i] === '--version') {
|
|
42
|
+
const pkg = require('../package.json');
|
|
43
|
+
console.log(pkg.version);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 设置环境变量
|
|
49
|
+
process.env.PORT = port;
|
|
50
|
+
process.env.HOST = host;
|
|
51
|
+
|
|
52
|
+
// 启动服务
|
|
53
|
+
require('../src/index.js');
|
package/data/.gitkeep
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skill-base",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Skill Base - 内网轻量版 Skill 管理平台",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skill-base": "./bin/skill-base.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"static",
|
|
13
|
+
"data/.gitkeep"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js",
|
|
17
|
+
"dev": "nodemon src/index.js",
|
|
18
|
+
"test": "node --test tests/**/*.test.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"fastify": "^4.26.0",
|
|
22
|
+
"@fastify/static": "^6.12.0",
|
|
23
|
+
"@fastify/multipart": "^8.0.0",
|
|
24
|
+
"@fastify/cookie": "^9.3.0",
|
|
25
|
+
"@fastify/cors": "^9.0.0",
|
|
26
|
+
"better-sqlite3": "^9.4.0",
|
|
27
|
+
"bcryptjs": "^2.4.3",
|
|
28
|
+
"uuid": "^9.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"nodemon": "^3.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/database.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const Database = require('better-sqlite3');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// 数据库文件路径,支持环境变量配置
|
|
6
|
+
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../data/skills.db');
|
|
7
|
+
|
|
8
|
+
// 创建数据库连接
|
|
9
|
+
const db = new Database(dbPath);
|
|
10
|
+
|
|
11
|
+
// 开启 WAL 模式提升并发性能
|
|
12
|
+
db.pragma('journal_mode = WAL');
|
|
13
|
+
|
|
14
|
+
// 开启外键约束
|
|
15
|
+
db.pragma('foreign_keys = ON');
|
|
16
|
+
|
|
17
|
+
// 建表 SQL
|
|
18
|
+
const createTablesSql = `
|
|
19
|
+
-- 用户表
|
|
20
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
username TEXT UNIQUE NOT NULL,
|
|
23
|
+
password_hash TEXT,
|
|
24
|
+
role TEXT DEFAULT 'developer',
|
|
25
|
+
name TEXT,
|
|
26
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- CLI 临时验证码表
|
|
30
|
+
CREATE TABLE IF NOT EXISTS cli_auth_codes (
|
|
31
|
+
code TEXT PRIMARY KEY,
|
|
32
|
+
user_id INTEGER NOT NULL,
|
|
33
|
+
expires_at DATETIME NOT NULL,
|
|
34
|
+
used BOOLEAN DEFAULT FALSE,
|
|
35
|
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- 长效访问令牌表 (PAT)
|
|
39
|
+
CREATE TABLE IF NOT EXISTS personal_access_tokens (
|
|
40
|
+
token TEXT PRIMARY KEY,
|
|
41
|
+
user_id INTEGER NOT NULL,
|
|
42
|
+
description TEXT,
|
|
43
|
+
last_used_at DATETIME,
|
|
44
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- Skill 主表
|
|
49
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
50
|
+
id TEXT PRIMARY KEY,
|
|
51
|
+
name TEXT NOT NULL,
|
|
52
|
+
description TEXT,
|
|
53
|
+
latest_version TEXT,
|
|
54
|
+
owner_id INTEGER NOT NULL,
|
|
55
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
56
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
57
|
+
FOREIGN KEY (owner_id) REFERENCES users(id)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- 版本表
|
|
61
|
+
CREATE TABLE IF NOT EXISTS skill_versions (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
skill_id TEXT NOT NULL,
|
|
64
|
+
version TEXT NOT NULL,
|
|
65
|
+
changelog TEXT,
|
|
66
|
+
zip_path TEXT NOT NULL,
|
|
67
|
+
uploader_id INTEGER NOT NULL,
|
|
68
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
69
|
+
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE,
|
|
70
|
+
FOREIGN KEY (uploader_id) REFERENCES users(id),
|
|
71
|
+
UNIQUE(skill_id, version)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
-- Skill 协作者表
|
|
75
|
+
CREATE TABLE IF NOT EXISTS skill_collaborators (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
skill_id TEXT NOT NULL,
|
|
78
|
+
user_id INTEGER NOT NULL,
|
|
79
|
+
role TEXT NOT NULL DEFAULT 'collaborator',
|
|
80
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
81
|
+
created_by INTEGER NOT NULL,
|
|
82
|
+
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE,
|
|
83
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
84
|
+
FOREIGN KEY (created_by) REFERENCES users(id),
|
|
85
|
+
UNIQUE(skill_id, user_id)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
-- 索引
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_versions_skill_id ON skill_versions(skill_id);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_cli_codes_user ON cli_auth_codes(user_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_pat_tokens_user ON personal_access_tokens(user_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_collaborators_skill ON skill_collaborators(skill_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_collaborators_user ON skill_collaborators(user_id);
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
// 执行建表语句
|
|
97
|
+
db.exec(createTablesSql);
|
|
98
|
+
|
|
99
|
+
// 安全地添加新字段(如果不存在)
|
|
100
|
+
try { db.exec("ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active'"); } catch(e) {}
|
|
101
|
+
// SQLite 不支持带 CURRENT_TIMESTAMP 默认值的 ALTER TABLE,需要分两步
|
|
102
|
+
try {
|
|
103
|
+
db.exec("ALTER TABLE users ADD COLUMN updated_at DATETIME");
|
|
104
|
+
db.exec("UPDATE users SET updated_at = datetime('now') WHERE updated_at IS NULL");
|
|
105
|
+
} catch(e) {}
|
|
106
|
+
try { db.exec("ALTER TABLE users ADD COLUMN created_by INTEGER REFERENCES users(id)"); } catch(e) {}
|
|
107
|
+
try { db.exec("ALTER TABLE users ADD COLUMN name TEXT"); } catch(e) {}
|
|
108
|
+
|
|
109
|
+
// 数据迁移:为已有 Skills 的 owner 插入 skill_collaborators 记录
|
|
110
|
+
const existingSkills = db.prepare('SELECT id, owner_id FROM skills').all();
|
|
111
|
+
const insertCollaborator = db.prepare(`
|
|
112
|
+
INSERT OR IGNORE INTO skill_collaborators (skill_id, user_id, role, created_by)
|
|
113
|
+
VALUES (?, ?, 'owner', ?)
|
|
114
|
+
`);
|
|
115
|
+
for (const skill of existingSkills) {
|
|
116
|
+
insertCollaborator.run(skill.id, skill.owner_id, skill.owner_id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = db;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fastify = require('fastify')({
|
|
3
|
+
logger: true,
|
|
4
|
+
// 设置 body 大小限制为 100MB(支持大 zip 上传)
|
|
5
|
+
bodyLimit: 100 * 1024 * 1024
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// 主启动函数
|
|
9
|
+
async function start() {
|
|
10
|
+
try {
|
|
11
|
+
// 1. 注册插件
|
|
12
|
+
// @fastify/cors — 允许跨域
|
|
13
|
+
await fastify.register(require('@fastify/cors'), {
|
|
14
|
+
origin: true,
|
|
15
|
+
credentials: true
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// @fastify/cookie — Cookie 支持
|
|
19
|
+
await fastify.register(require('@fastify/cookie'));
|
|
20
|
+
|
|
21
|
+
// @fastify/multipart — 文件上传支持
|
|
22
|
+
await fastify.register(require('@fastify/multipart'), {
|
|
23
|
+
limits: {
|
|
24
|
+
fileSize: 100 * 1024 * 1024 // 100MB
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// @fastify/static — 静态文件服务(指向 static/ 目录)
|
|
29
|
+
await fastify.register(require('@fastify/static'), {
|
|
30
|
+
root: path.join(__dirname, '../static'),
|
|
31
|
+
prefix: '/'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 2. 注册自定义中间件
|
|
35
|
+
// 错误处理
|
|
36
|
+
await fastify.register(require('./middleware/error'));
|
|
37
|
+
// 认证(注册 authenticate、createSession 等装饰器)
|
|
38
|
+
await fastify.register(require('./middleware/auth'));
|
|
39
|
+
// 管理员权限(注册 requireAdmin 装饰器)
|
|
40
|
+
await fastify.register(require('./middleware/admin'));
|
|
41
|
+
|
|
42
|
+
// 3. 注册 API 路由(前缀 /api/v1)
|
|
43
|
+
await fastify.register(require('./routes/init'), { prefix: '/api/v1/init' });
|
|
44
|
+
await fastify.register(require('./routes/auth'), { prefix: '/api/v1/auth' });
|
|
45
|
+
await fastify.register(require('./routes/skills'), { prefix: '/api/v1/skills' });
|
|
46
|
+
await fastify.register(require('./routes/publish'), { prefix: '/api/v1/skills' });
|
|
47
|
+
await fastify.register(require('./routes/collaborators'), { prefix: '/api/v1/skills' });
|
|
48
|
+
await fastify.register(require('./routes/users'), { prefix: '/api/v1/users' });
|
|
49
|
+
|
|
50
|
+
// 4. 页面路由 fallback(SPA 风格路由支持)
|
|
51
|
+
fastify.setNotFoundHandler(async (request, reply) => {
|
|
52
|
+
// API 路由返回 JSON 404
|
|
53
|
+
if (request.url.startsWith('/api/')) {
|
|
54
|
+
return reply.code(404).send({ detail: 'Not found' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 页面路由映射到对应 HTML 文件
|
|
58
|
+
const url = request.url.split('?')[0]; // 去掉 query string
|
|
59
|
+
|
|
60
|
+
if (url === '/setup') return reply.sendFile('setup.html');
|
|
61
|
+
if (url === '/login') return reply.sendFile('login.html');
|
|
62
|
+
if (url === '/publish') return reply.sendFile('publish.html');
|
|
63
|
+
if (url === '/cli-code') return reply.sendFile('cli-code.html');
|
|
64
|
+
if (url === '/admin/users') return reply.sendFile('admin/users.html');
|
|
65
|
+
if (url.match(/^\/skill\/[^/]+\/file\//)) return reply.sendFile('file.html');
|
|
66
|
+
if (url.match(/^\/skill\/[^/]+\/diff/)) return reply.sendFile('diff.html');
|
|
67
|
+
if (url.match(/^\/skill\/[^/]+$/)) return reply.sendFile('skill.html');
|
|
68
|
+
|
|
69
|
+
// 其他未匹配路由返回首页
|
|
70
|
+
return reply.sendFile('index.html');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// 5. 确保数据库已初始化
|
|
74
|
+
require('./database');
|
|
75
|
+
|
|
76
|
+
// 6. 启动服务
|
|
77
|
+
const PORT = process.env.PORT || 8000;
|
|
78
|
+
const HOST = process.env.HOST || '0.0.0.0';
|
|
79
|
+
|
|
80
|
+
await fastify.listen({ port: PORT, host: HOST });
|
|
81
|
+
console.log(`Skill Base server running at http://${HOST}:${PORT}`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
fastify.log.error(err);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
start();
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fp = require('fastify-plugin');
|
|
2
|
+
|
|
3
|
+
async function adminPlugin(fastify, options) {
|
|
4
|
+
fastify.decorate('requireAdmin', async function(request, reply) {
|
|
5
|
+
// 1. 先调用 authenticate 确保已登录
|
|
6
|
+
await fastify.authenticate(request, reply);
|
|
7
|
+
if (reply.sent) return;
|
|
8
|
+
|
|
9
|
+
// 2. 检查管理员角色
|
|
10
|
+
if (request.user.role !== 'admin') {
|
|
11
|
+
return reply.code(403).send({
|
|
12
|
+
ok: false,
|
|
13
|
+
error: 'forbidden',
|
|
14
|
+
detail: '需要管理员权限'
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = fp(adminPlugin, {
|
|
21
|
+
name: 'admin-plugin',
|
|
22
|
+
dependencies: ['auth-plugin']
|
|
23
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const fp = require('fastify-plugin');
|
|
2
|
+
const db = require('../database');
|
|
3
|
+
|
|
4
|
+
// 内存 Session 存储(简单实现,生产可换 Redis)
|
|
5
|
+
const sessions = new Map();
|
|
6
|
+
|
|
7
|
+
// 创建 Session
|
|
8
|
+
function createSession(userId) {
|
|
9
|
+
const { generateSessionId } = require('../utils/crypto');
|
|
10
|
+
const sessionId = generateSessionId();
|
|
11
|
+
sessions.set(sessionId, { userId, createdAt: Date.now() });
|
|
12
|
+
return sessionId;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 销毁 Session
|
|
16
|
+
function destroySession(sessionId) {
|
|
17
|
+
sessions.delete(sessionId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 认证中间件装饰器 —— 注册为 Fastify 的 decorate + preHandler
|
|
21
|
+
// 用法:在路由中通过 { preHandler: [fastify.authenticate] } 使用
|
|
22
|
+
async function authPlugin(fastify, options) {
|
|
23
|
+
// 将 sessions 暴露出去供路由使用
|
|
24
|
+
fastify.decorate('sessions', sessions);
|
|
25
|
+
fastify.decorate('createSession', createSession);
|
|
26
|
+
fastify.decorate('destroySession', destroySession);
|
|
27
|
+
|
|
28
|
+
// 认证装饰器
|
|
29
|
+
fastify.decorate('authenticate', async function(request, reply) {
|
|
30
|
+
// 1. 先尝试 Cookie Session
|
|
31
|
+
const sessionId = request.cookies?.session_id;
|
|
32
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
33
|
+
const session = sessions.get(sessionId);
|
|
34
|
+
const user = db.prepare('SELECT id, username, name, role, status FROM users WHERE id = ?').get(session.userId);
|
|
35
|
+
if (!user || user.status === 'disabled') {
|
|
36
|
+
return reply.code(401).send({
|
|
37
|
+
ok: false,
|
|
38
|
+
error: 'account_disabled',
|
|
39
|
+
detail: '账号已被禁用'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
request.user = user;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. 再尝试 Bearer Token(PAT)
|
|
47
|
+
const authHeader = request.headers.authorization;
|
|
48
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
49
|
+
const token = authHeader.slice(7);
|
|
50
|
+
const pat = db.prepare('SELECT user_id FROM personal_access_tokens WHERE token = ?').get(token);
|
|
51
|
+
if (pat) {
|
|
52
|
+
const user = db.prepare('SELECT id, username, name, role, status FROM users WHERE id = ?').get(pat.user_id);
|
|
53
|
+
if (!user || user.status === 'disabled') {
|
|
54
|
+
return reply.code(401).send({
|
|
55
|
+
ok: false,
|
|
56
|
+
error: 'account_disabled',
|
|
57
|
+
detail: '账号已被禁用'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// 更新 last_used_at
|
|
61
|
+
db.prepare('UPDATE personal_access_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE token = ?').run(token);
|
|
62
|
+
request.user = user;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. 未认证
|
|
68
|
+
reply.code(401).send({ detail: 'Authentication required' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 可选认证(不强制,有则解析)
|
|
72
|
+
fastify.decorate('optionalAuth', async function(request, reply) {
|
|
73
|
+
try {
|
|
74
|
+
// 复用 authenticate 逻辑,但不抛错
|
|
75
|
+
const sessionId = request.cookies?.session_id;
|
|
76
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
77
|
+
const session = sessions.get(sessionId);
|
|
78
|
+
const user = db.prepare('SELECT id, username, name, role, status FROM users WHERE id = ?').get(session.userId);
|
|
79
|
+
if (user && user.status !== 'disabled') { request.user = user; return; }
|
|
80
|
+
}
|
|
81
|
+
const authHeader = request.headers.authorization;
|
|
82
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
83
|
+
const token = authHeader.slice(7);
|
|
84
|
+
const pat = db.prepare('SELECT user_id FROM personal_access_tokens WHERE token = ?').get(token);
|
|
85
|
+
if (pat) {
|
|
86
|
+
const user = db.prepare('SELECT id, username, name, role, status FROM users WHERE id = ?').get(pat.user_id);
|
|
87
|
+
if (user && user.status !== 'disabled') { request.user = user; return; }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (e) { /* ignore */ }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = fp(authPlugin, {
|
|
95
|
+
name: 'auth-plugin'
|
|
96
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Fastify 错误处理插件
|
|
2
|
+
async function errorHandler(fastify, options) {
|
|
3
|
+
fastify.setErrorHandler(function (error, request, reply) {
|
|
4
|
+
const statusCode = error.statusCode || 500;
|
|
5
|
+
|
|
6
|
+
// 记录错误日志
|
|
7
|
+
if (statusCode >= 500) {
|
|
8
|
+
request.log.error(error);
|
|
9
|
+
} else {
|
|
10
|
+
request.log.warn(error.message);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
reply.code(statusCode).send({
|
|
14
|
+
detail: error.message || 'Internal Server Error',
|
|
15
|
+
...(process.env.NODE_ENV === 'development' ? { stack: error.stack } : {})
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// 注意: 404 处理统一在 index.js 中的 setNotFoundHandler 设置
|
|
20
|
+
// 包含 API 路由返回 JSON 404 和页面路由返回对应 HTML 的逻辑
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = errorHandler;
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const db = require('../database');
|
|
2
|
+
|
|
3
|
+
const SkillModel = {
|
|
4
|
+
// 根据 ID 查询 Skill(附带 owner 信息)
|
|
5
|
+
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);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
// 搜索/列出 Skills(支持关键词搜索 name 或 description)
|
|
15
|
+
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();
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// 创建新 Skill
|
|
35
|
+
create(id, name, description, ownerId) {
|
|
36
|
+
db.prepare(`
|
|
37
|
+
INSERT INTO skills (id, name, description, owner_id)
|
|
38
|
+
VALUES (?, ?, ?, ?)
|
|
39
|
+
`).run(id, name, description || '', ownerId);
|
|
40
|
+
return this.findById(id);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// 更新 latest_version 和 updated_at
|
|
44
|
+
updateLatestVersion(id, version) {
|
|
45
|
+
db.prepare(`
|
|
46
|
+
UPDATE skills SET latest_version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
|
47
|
+
`).run(version, id);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// 检查 Skill 是否存在
|
|
51
|
+
exists(id) {
|
|
52
|
+
const row = db.prepare('SELECT 1 FROM skills WHERE id = ?').get(id);
|
|
53
|
+
return !!row;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = SkillModel;
|