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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
|
+
const db = require('../database');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 系统初始化路由
|
|
6
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
7
|
+
*/
|
|
8
|
+
async function initRoutes(fastify) {
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/v1/init/status
|
|
11
|
+
* 检查系统是否需要初始化(是否存在管理员用户)
|
|
12
|
+
*/
|
|
13
|
+
fastify.get('/status', async (request, reply) => {
|
|
14
|
+
const adminCount = db.prepare(
|
|
15
|
+
"SELECT COUNT(*) as count FROM users WHERE role = 'admin'"
|
|
16
|
+
).get();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
initialized: adminCount.count > 0
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* POST /api/v1/init/setup
|
|
25
|
+
* 初始化系统管理员账号
|
|
26
|
+
* Body: { username, password }
|
|
27
|
+
*/
|
|
28
|
+
fastify.post('/setup', async (request, reply) => {
|
|
29
|
+
// 检查是否已经初始化
|
|
30
|
+
const adminCount = db.prepare(
|
|
31
|
+
"SELECT COUNT(*) as count FROM users WHERE role = 'admin'"
|
|
32
|
+
).get();
|
|
33
|
+
|
|
34
|
+
if (adminCount.count > 0) {
|
|
35
|
+
return reply.code(400).send({
|
|
36
|
+
error: 'System already initialized'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { username, password } = request.body || {};
|
|
41
|
+
|
|
42
|
+
// 验证输入
|
|
43
|
+
if (!username || !password) {
|
|
44
|
+
return reply.code(400).send({
|
|
45
|
+
error: 'Username and password are required'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (username.length < 3 || username.length > 50) {
|
|
50
|
+
return reply.code(400).send({
|
|
51
|
+
error: 'Username must be 3-50 characters'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (password.length < 6) {
|
|
56
|
+
return reply.code(400).send({
|
|
57
|
+
error: 'Password must be at least 6 characters'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 检查用户名是否已存在
|
|
62
|
+
const existingUser = db.prepare(
|
|
63
|
+
'SELECT id FROM users WHERE username = ?'
|
|
64
|
+
).get(username);
|
|
65
|
+
|
|
66
|
+
if (existingUser) {
|
|
67
|
+
return reply.code(400).send({
|
|
68
|
+
error: 'Username already exists'
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 创建管理员账号
|
|
73
|
+
const passwordHash = bcrypt.hashSync(password, 10);
|
|
74
|
+
const result = db.prepare(
|
|
75
|
+
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
|
|
76
|
+
).run(username, passwordHash, 'admin');
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
message: 'Admin account created successfully',
|
|
81
|
+
userId: result.lastInsertRowid
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = initRoutes;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const db = require('../database');
|
|
3
|
+
const SkillModel = require('../models/skill');
|
|
4
|
+
const VersionModel = require('../models/version');
|
|
5
|
+
const { ensureSkillDir, generateVersionNumber, getZipPath, getZipRelativePath } = require('../utils/zip');
|
|
6
|
+
const { canPublishSkill } = require('../utils/permission');
|
|
7
|
+
|
|
8
|
+
async function publishRoutes(fastify, options) {
|
|
9
|
+
// POST /publish - 发布新版本
|
|
10
|
+
fastify.post('/publish', {
|
|
11
|
+
preHandler: [fastify.authenticate]
|
|
12
|
+
}, async (request, reply) => {
|
|
13
|
+
const fields = {};
|
|
14
|
+
let zipBuffer = null;
|
|
15
|
+
let zipFilename = null;
|
|
16
|
+
|
|
17
|
+
// 解析 multipart 数据
|
|
18
|
+
const parts = request.parts();
|
|
19
|
+
for await (const part of parts) {
|
|
20
|
+
if (part.type === 'file') {
|
|
21
|
+
if (part.fieldname === 'zip_file') {
|
|
22
|
+
// 读取文件到 buffer
|
|
23
|
+
const chunks = [];
|
|
24
|
+
for await (const chunk of part.file) {
|
|
25
|
+
chunks.push(chunk);
|
|
26
|
+
}
|
|
27
|
+
zipBuffer = Buffer.concat(chunks);
|
|
28
|
+
zipFilename = part.filename;
|
|
29
|
+
}
|
|
30
|
+
} else if (part.type === 'field') {
|
|
31
|
+
fields[part.fieldname] = part.value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 检查必须的 zip 文件
|
|
36
|
+
if (!zipBuffer) {
|
|
37
|
+
return reply.code(400).send({ detail: 'zip_file is required' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { skill_id, name, description, changelog } = fields;
|
|
41
|
+
|
|
42
|
+
// 检查 skill_id
|
|
43
|
+
if (!skill_id) {
|
|
44
|
+
return reply.code(400).send({ detail: 'skill_id is required' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 检查发布权限
|
|
48
|
+
if (!canPublishSkill(request.user, skill_id)) {
|
|
49
|
+
return reply.code(403).send({
|
|
50
|
+
ok: false,
|
|
51
|
+
error: 'permission_denied',
|
|
52
|
+
detail: 'You do not have publish permission for this Skill'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 检查 skill 是否存在
|
|
57
|
+
const skillExists = SkillModel.exists(skill_id);
|
|
58
|
+
|
|
59
|
+
// 如果 skill 不存在,需要 name 字段来创建新 skill
|
|
60
|
+
if (!skillExists) {
|
|
61
|
+
if (!name) {
|
|
62
|
+
return reply.code(400).send({ detail: 'name is required for new skill' });
|
|
63
|
+
}
|
|
64
|
+
// 使用事务创建新 skill 和添加 owner 协作者记录
|
|
65
|
+
const createSkillTx = db.transaction(() => {
|
|
66
|
+
SkillModel.create(skill_id, name, description || '', request.user.id);
|
|
67
|
+
db.prepare(
|
|
68
|
+
'INSERT INTO skill_collaborators (skill_id, user_id, role, created_by) VALUES (?, ?, ?, ?)'
|
|
69
|
+
).run(skill_id, request.user.id, 'owner', request.user.id);
|
|
70
|
+
});
|
|
71
|
+
createSkillTx();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 生成版本号
|
|
75
|
+
const version = generateVersionNumber();
|
|
76
|
+
|
|
77
|
+
// 确保目录存在
|
|
78
|
+
ensureSkillDir(skill_id);
|
|
79
|
+
|
|
80
|
+
// 写入 zip 文件
|
|
81
|
+
const zipPath = getZipPath(skill_id, version);
|
|
82
|
+
fs.writeFileSync(zipPath, zipBuffer);
|
|
83
|
+
|
|
84
|
+
// 获取相对路径(存入数据库)
|
|
85
|
+
const zipRelativePath = getZipRelativePath(skill_id, version);
|
|
86
|
+
|
|
87
|
+
// 创建版本记录
|
|
88
|
+
const versionRecord = VersionModel.create(
|
|
89
|
+
skill_id,
|
|
90
|
+
version,
|
|
91
|
+
changelog || '',
|
|
92
|
+
zipRelativePath,
|
|
93
|
+
request.user.id
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// 更新 skill 的最新版本
|
|
97
|
+
SkillModel.updateLatestVersion(skill_id, version);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
skill_id,
|
|
102
|
+
version,
|
|
103
|
+
created_at: versionRecord.created_at
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = publishRoutes;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const SkillModel = require('../models/skill');
|
|
4
|
+
const VersionModel = require('../models/version');
|
|
5
|
+
const { getZipPath } = require('../utils/zip');
|
|
6
|
+
|
|
7
|
+
// 格式化 skill,将 owner 转为对象
|
|
8
|
+
function formatSkill(skill) {
|
|
9
|
+
if (!skill) return null;
|
|
10
|
+
return {
|
|
11
|
+
id: skill.id,
|
|
12
|
+
name: skill.name,
|
|
13
|
+
description: skill.description,
|
|
14
|
+
latest_version: skill.latest_version,
|
|
15
|
+
owner: {
|
|
16
|
+
id: skill.owner_id,
|
|
17
|
+
username: skill.owner_username,
|
|
18
|
+
name: skill.owner_name
|
|
19
|
+
},
|
|
20
|
+
created_at: skill.created_at,
|
|
21
|
+
updated_at: skill.updated_at
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 格式化 version,将 uploader 转为对象
|
|
26
|
+
function formatVersion(version) {
|
|
27
|
+
if (!version) return null;
|
|
28
|
+
return {
|
|
29
|
+
id: version.id,
|
|
30
|
+
skill_id: version.skill_id,
|
|
31
|
+
version: version.version,
|
|
32
|
+
changelog: version.changelog,
|
|
33
|
+
zip_path: version.zip_path,
|
|
34
|
+
uploader: {
|
|
35
|
+
id: version.uploader_id,
|
|
36
|
+
username: version.uploader_username,
|
|
37
|
+
name: version.uploader_name
|
|
38
|
+
},
|
|
39
|
+
created_at: version.created_at
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function skillsRoutes(fastify, options) {
|
|
44
|
+
// GET / - 获取 skills 列表
|
|
45
|
+
fastify.get('/', async (request, reply) => {
|
|
46
|
+
const { q } = request.query;
|
|
47
|
+
const skills = SkillModel.search(q);
|
|
48
|
+
const formattedSkills = skills.map(formatSkill);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
skills: formattedSkills,
|
|
52
|
+
total: formattedSkills.length
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// GET /:skill_id - 获取单个 skill
|
|
57
|
+
fastify.get('/:skill_id', async (request, reply) => {
|
|
58
|
+
const { skill_id } = request.params;
|
|
59
|
+
const skill = SkillModel.findById(skill_id);
|
|
60
|
+
|
|
61
|
+
if (!skill) {
|
|
62
|
+
return reply.code(404).send({ detail: 'Skill not found' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return formatSkill(skill);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// GET /:skill_id/versions - 获取 skill 的所有版本
|
|
69
|
+
fastify.get('/:skill_id/versions', async (request, reply) => {
|
|
70
|
+
const { skill_id } = request.params;
|
|
71
|
+
|
|
72
|
+
// 先检查 skill 是否存在
|
|
73
|
+
if (!SkillModel.exists(skill_id)) {
|
|
74
|
+
return reply.code(404).send({ detail: 'Skill not found' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const versions = VersionModel.listBySkillId(skill_id);
|
|
78
|
+
const formattedVersions = versions.map(formatVersion);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
skill_id,
|
|
82
|
+
versions: formattedVersions
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// GET /:skill_id/versions/:version/download - 下载版本 zip 文件
|
|
87
|
+
fastify.get('/:skill_id/versions/:version/download', async (request, reply) => {
|
|
88
|
+
const { skill_id, version } = request.params;
|
|
89
|
+
|
|
90
|
+
let versionRecord;
|
|
91
|
+
|
|
92
|
+
if (version === 'latest') {
|
|
93
|
+
versionRecord = VersionModel.getLatest(skill_id);
|
|
94
|
+
} else {
|
|
95
|
+
versionRecord = VersionModel.findByVersion(skill_id, version);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!versionRecord) {
|
|
99
|
+
return reply.code(404).send({ detail: 'Version not found' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 获取文件路径
|
|
103
|
+
const zipPath = getZipPath(skill_id, versionRecord.version);
|
|
104
|
+
|
|
105
|
+
// 检查文件是否存在
|
|
106
|
+
if (!fs.existsSync(zipPath)) {
|
|
107
|
+
return reply.code(404).send({ detail: 'Version not found' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 设置响应头并返回文件流
|
|
111
|
+
const fileName = `${skill_id}-${versionRecord.version}.zip`;
|
|
112
|
+
reply.header('Content-Type', 'application/zip');
|
|
113
|
+
reply.header('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
114
|
+
|
|
115
|
+
return fs.createReadStream(zipPath);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = skillsRoutes;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const UserModel = require('../models/user');
|
|
2
|
+
const { hashPassword } = require('../utils/crypto');
|
|
3
|
+
const db = require('../database');
|
|
4
|
+
|
|
5
|
+
async function usersRoutes(fastify, options) {
|
|
6
|
+
// GET /search - 用户搜索(仅需登录,不需要管理员权限)
|
|
7
|
+
// 注意:必须在 /:user_id 之前注册
|
|
8
|
+
fastify.get('/search', {
|
|
9
|
+
preHandler: [fastify.authenticate]
|
|
10
|
+
}, async (request, reply) => {
|
|
11
|
+
const { q } = request.query;
|
|
12
|
+
|
|
13
|
+
if (!q || q.trim().length < 1) {
|
|
14
|
+
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Search keyword must be at least 1 character' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pattern = `%${q.trim()}%`;
|
|
18
|
+
const users = db.prepare(`
|
|
19
|
+
SELECT id, username, name, status
|
|
20
|
+
FROM users
|
|
21
|
+
WHERE (username LIKE ? OR name LIKE ?) AND status = 'active'
|
|
22
|
+
LIMIT 10
|
|
23
|
+
`).all(pattern, pattern);
|
|
24
|
+
|
|
25
|
+
return reply.send({ users });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 以下路由都需要管理员权限
|
|
29
|
+
fastify.register(async function adminRoutes(fastify) {
|
|
30
|
+
fastify.addHook('preHandler', fastify.requireAdmin);
|
|
31
|
+
|
|
32
|
+
// GET / - 用户列表
|
|
33
|
+
fastify.get('/', async (request, reply) => {
|
|
34
|
+
const { q, status, page = 1, limit = 20 } = request.query;
|
|
35
|
+
const result = UserModel.list({
|
|
36
|
+
q,
|
|
37
|
+
status,
|
|
38
|
+
page: parseInt(page) || 1,
|
|
39
|
+
limit: Math.min(parseInt(limit) || 20, 100)
|
|
40
|
+
});
|
|
41
|
+
return reply.send(result);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// POST / - 创建用户
|
|
45
|
+
fastify.post('/', async (request, reply) => {
|
|
46
|
+
const { username, password, role = 'developer', name } = request.body || {};
|
|
47
|
+
|
|
48
|
+
if (!username || !password) {
|
|
49
|
+
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Username and password are required' });
|
|
50
|
+
}
|
|
51
|
+
if (password.length < 6) {
|
|
52
|
+
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Password must be at least 6 characters' });
|
|
53
|
+
}
|
|
54
|
+
if (!['admin', 'developer'].includes(role)) {
|
|
55
|
+
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Role must be admin or developer' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if username already exists
|
|
59
|
+
const existing = UserModel.findByUsername(username.trim());
|
|
60
|
+
if (existing) {
|
|
61
|
+
return reply.code(400).send({ ok: false, error: 'username_exists', detail: 'Username already exists' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const passwordHash = await hashPassword(password);
|
|
65
|
+
// 创建用户时记录 created_by
|
|
66
|
+
const result = db.prepare(
|
|
67
|
+
"INSERT INTO users (username, password_hash, role, name, status, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, datetime('now'), datetime('now'))"
|
|
68
|
+
).run(username.trim(), passwordHash, role, name || null, request.user.id);
|
|
69
|
+
|
|
70
|
+
const user = UserModel.findById(result.lastInsertRowid);
|
|
71
|
+
return reply.code(201).send({ ok: true, user });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// GET /:user_id - 用户详情
|
|
75
|
+
fastify.get('/:user_id', async (request, reply) => {
|
|
76
|
+
const { user_id } = request.params;
|
|
77
|
+
const user = UserModel.findByIdWithCreator(parseInt(user_id));
|
|
78
|
+
|
|
79
|
+
if (!user) {
|
|
80
|
+
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Format created_by
|
|
84
|
+
const result = {
|
|
85
|
+
id: user.id,
|
|
86
|
+
username: user.username,
|
|
87
|
+
name: user.name,
|
|
88
|
+
role: user.role,
|
|
89
|
+
status: user.status,
|
|
90
|
+
created_at: user.created_at,
|
|
91
|
+
updated_at: user.updated_at
|
|
92
|
+
};
|
|
93
|
+
if (user.creator_id) {
|
|
94
|
+
result.created_by = { id: user.creator_id, username: user.creator_username };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return reply.send(result);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// PATCH /:user_id - 更新用户
|
|
101
|
+
fastify.patch('/:user_id', async (request, reply) => {
|
|
102
|
+
const userId = parseInt(request.params.user_id);
|
|
103
|
+
const { role, status, name } = request.body || {};
|
|
104
|
+
|
|
105
|
+
const user = UserModel.findById(userId);
|
|
106
|
+
if (!user) {
|
|
107
|
+
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Self-protection for admins
|
|
111
|
+
if (userId === request.user.id) {
|
|
112
|
+
if (status === 'disabled') {
|
|
113
|
+
return reply.code(400).send({ ok: false, error: 'self_protection', detail: 'Cannot disable your own account' });
|
|
114
|
+
}
|
|
115
|
+
if (role === 'developer') {
|
|
116
|
+
return reply.code(400).send({ ok: false, error: 'self_protection', detail: 'Cannot downgrade your own role' });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Validate field values
|
|
121
|
+
const fields = {};
|
|
122
|
+
if (role !== undefined) {
|
|
123
|
+
if (!['admin', 'developer'].includes(role)) {
|
|
124
|
+
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Role must be admin or developer' });
|
|
125
|
+
}
|
|
126
|
+
fields.role = role;
|
|
127
|
+
}
|
|
128
|
+
if (status !== undefined) {
|
|
129
|
+
if (!['active', 'disabled'].includes(status)) {
|
|
130
|
+
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Status must be active or disabled' });
|
|
131
|
+
}
|
|
132
|
+
fields.status = status;
|
|
133
|
+
}
|
|
134
|
+
if (name !== undefined) {
|
|
135
|
+
fields.name = name;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (Object.keys(fields).length === 0) {
|
|
139
|
+
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'No fields to update' });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
UserModel.update(userId, fields);
|
|
143
|
+
const updated = UserModel.findById(userId);
|
|
144
|
+
return reply.send({ ok: true, user: updated });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// POST /:user_id/reset-password - 重置密码
|
|
148
|
+
fastify.post('/:user_id/reset-password', async (request, reply) => {
|
|
149
|
+
const userId = parseInt(request.params.user_id);
|
|
150
|
+
const { new_password } = request.body || {};
|
|
151
|
+
|
|
152
|
+
if (!new_password || new_password.length < 6) {
|
|
153
|
+
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'New password must be at least 6 characters' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const user = UserModel.findById(userId);
|
|
157
|
+
if (!user) {
|
|
158
|
+
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const passwordHash = await hashPassword(new_password);
|
|
162
|
+
UserModel.resetPassword(userId, passwordHash);
|
|
163
|
+
|
|
164
|
+
return reply.send({ ok: true, message: 'Password has been reset' });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = usersRoutes;
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
|
|
4
|
+
// 密码哈希(10 rounds)
|
|
5
|
+
function hashPassword(password) {
|
|
6
|
+
return bcrypt.hashSync(password, 10);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 验证密码
|
|
10
|
+
function verifyPassword(password, hash) {
|
|
11
|
+
return bcrypt.compareSync(password, hash);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 生成 PAT Token,格式:sk-base-{uuid去掉横线}
|
|
15
|
+
function generatePAT() {
|
|
16
|
+
return 'sk-base-' + uuidv4().replace(/-/g, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 生成 CLI 验证码,格式:XXXX-XXXX(大写字母+数字)
|
|
20
|
+
function generateCliCode() {
|
|
21
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // 排除易混淆字符
|
|
22
|
+
let code = '';
|
|
23
|
+
for (let i = 0; i < 8; i++) {
|
|
24
|
+
if (i === 4) code += '-';
|
|
25
|
+
code += chars[Math.floor(Math.random() * chars.length)];
|
|
26
|
+
}
|
|
27
|
+
return code;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 生成 Session ID
|
|
31
|
+
function generateSessionId() {
|
|
32
|
+
return uuidv4();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { hashPassword, verifyPassword, generatePAT, generateCliCode, generateSessionId };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const db = require('../database');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 检查用户是否有 Skill 的指定权限
|
|
5
|
+
* @param {object} user - request.user 对象
|
|
6
|
+
* @param {string} skillId - Skill ID
|
|
7
|
+
* @param {string} requiredRole - 'owner' | 'collaborator' | 'any'
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
function hasSkillPermission(user, skillId, requiredRole = 'any') {
|
|
11
|
+
// 管理员拥有所有权限
|
|
12
|
+
if (user.role === 'admin') return true;
|
|
13
|
+
|
|
14
|
+
const collaborator = db.prepare(
|
|
15
|
+
'SELECT role FROM skill_collaborators WHERE skill_id = ? AND user_id = ?'
|
|
16
|
+
).get(skillId, user.id);
|
|
17
|
+
|
|
18
|
+
if (!collaborator) return false;
|
|
19
|
+
if (requiredRole === 'any') return true;
|
|
20
|
+
if (requiredRole === 'owner') return collaborator.role === 'owner';
|
|
21
|
+
if (requiredRole === 'collaborator') return true; // owner 也有 collaborator 权限
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 检查用户是否可以发布 Skill
|
|
27
|
+
*/
|
|
28
|
+
function canPublishSkill(user, skillId) {
|
|
29
|
+
const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skillId);
|
|
30
|
+
if (!skill) return true; // 新 Skill,任何登录用户都可创建
|
|
31
|
+
return hasSkillPermission(user, skillId, 'any');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 检查用户是否可以管理协作者/删除 Skill
|
|
36
|
+
*/
|
|
37
|
+
function canManageSkill(user, skillId) {
|
|
38
|
+
return hasSkillPermission(user, skillId, 'owner');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
hasSkillPermission,
|
|
43
|
+
canPublishSkill,
|
|
44
|
+
canManageSkill
|
|
45
|
+
};
|
package/src/utils/zip.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
// 获取 zip 存储目录
|
|
5
|
+
function getDataDir() {
|
|
6
|
+
return process.env.DATA_DIR || path.join(__dirname, '../../data');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 确保 skill 的存储目录存在
|
|
10
|
+
function ensureSkillDir(skillId) {
|
|
11
|
+
const dir = path.join(getDataDir(), skillId);
|
|
12
|
+
if (!fs.existsSync(dir)) {
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 生成时间戳版本号 vYYYYMMDD.HHMMSS
|
|
19
|
+
function generateVersionNumber() {
|
|
20
|
+
const now = new Date();
|
|
21
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
22
|
+
return `v${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}.${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 获取 zip 文件的完整路径
|
|
26
|
+
function getZipPath(skillId, version) {
|
|
27
|
+
return path.join(getDataDir(), skillId, `${version}.zip`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 获取 zip 文件相对路径(存入数据库)
|
|
31
|
+
function getZipRelativePath(skillId, version) {
|
|
32
|
+
return `${skillId}/${version}.zip`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { getDataDir, ensureSkillDir, generateVersionNumber, getZipPath, getZipRelativePath };
|