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.
Files changed (51) hide show
  1. package/README.md +141 -0
  2. package/bin/skill-base.js +53 -0
  3. package/data/.gitkeep +0 -0
  4. package/package.json +36 -0
  5. package/src/database.js +119 -0
  6. package/src/index.js +88 -0
  7. package/src/middleware/.gitkeep +0 -0
  8. package/src/middleware/admin.js +23 -0
  9. package/src/middleware/auth.js +96 -0
  10. package/src/middleware/error.js +23 -0
  11. package/src/models/.gitkeep +0 -0
  12. package/src/models/skill.js +57 -0
  13. package/src/models/user.js +130 -0
  14. package/src/models/version.js +57 -0
  15. package/src/routes/.gitkeep +0 -0
  16. package/src/routes/auth.js +173 -0
  17. package/src/routes/collaborators.js +260 -0
  18. package/src/routes/init.js +86 -0
  19. package/src/routes/publish.js +108 -0
  20. package/src/routes/skills.js +119 -0
  21. package/src/routes/users.js +169 -0
  22. package/src/utils/.gitkeep +0 -0
  23. package/src/utils/crypto.js +35 -0
  24. package/src/utils/permission.js +45 -0
  25. package/src/utils/zip.js +35 -0
  26. package/static/admin/users.html +593 -0
  27. package/static/cli-code.html +203 -0
  28. package/static/css/.gitkeep +0 -0
  29. package/static/css/style.css +1567 -0
  30. package/static/diff.html +466 -0
  31. package/static/file.html +443 -0
  32. package/static/index.html +251 -0
  33. package/static/js/.gitkeep +0 -0
  34. package/static/js/admin/users.js +346 -0
  35. package/static/js/app.js +508 -0
  36. package/static/js/auth.js +151 -0
  37. package/static/js/cli-code.js +184 -0
  38. package/static/js/collaborators.js +283 -0
  39. package/static/js/diff.js +540 -0
  40. package/static/js/file.js +619 -0
  41. package/static/js/i18n.js +739 -0
  42. package/static/js/index.js +168 -0
  43. package/static/js/publish.js +718 -0
  44. package/static/js/settings.js +124 -0
  45. package/static/js/setup.js +157 -0
  46. package/static/js/skill.js +808 -0
  47. package/static/login.html +82 -0
  48. package/static/publish.html +459 -0
  49. package/static/settings.html +163 -0
  50. package/static/setup.html +101 -0
  51. 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
+ }
@@ -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;