skill-base 2.0.16 → 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.
Files changed (109) hide show
  1. package/README.md +8 -2
  2. package/bin/skill-base.js +73 -28
  3. package/package.json +2 -2
  4. package/src/cappy.js +162 -50
  5. package/src/database.js +17 -17
  6. package/src/index.js +75 -22
  7. package/src/middleware/admin.js +3 -3
  8. package/src/middleware/auth.js +22 -22
  9. package/src/middleware/error.js +4 -4
  10. package/src/models/skill.js +6 -6
  11. package/src/models/user.js +10 -10
  12. package/src/models/version.js +6 -6
  13. package/src/routes/auth.js +17 -17
  14. package/src/routes/collaborators.js +28 -28
  15. package/src/routes/init.js +7 -7
  16. package/src/routes/publish.js +15 -15
  17. package/src/routes/skills.js +13 -13
  18. package/src/routes/users.js +9 -9
  19. package/src/utils/crypto.js +6 -6
  20. package/src/utils/detect-language.js +56 -0
  21. package/src/utils/permission.js +7 -7
  22. package/src/utils/zip.js +6 -6
  23. package/static/assets/{index-BHB0vddE.js → index-BVgsNsqr.js} +43 -43
  24. package/static/assets/index-ByONPaqz.css +1 -0
  25. package/static/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  26. package/static/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  27. package/static/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  28. package/static/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  29. package/static/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  30. package/static/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  31. package/static/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  32. package/static/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  33. package/static/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  34. package/static/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  35. package/static/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  36. package/static/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  37. package/static/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  38. package/static/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  39. package/static/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  40. package/static/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  41. package/static/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  42. package/static/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  43. package/static/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  44. package/static/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  45. package/static/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  46. package/static/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  47. package/static/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  48. package/static/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  49. package/static/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  50. package/static/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  51. package/static/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  52. package/static/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  53. package/static/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  54. package/static/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  55. package/static/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  56. package/static/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  57. package/static/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  58. package/static/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  59. package/static/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  60. package/static/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  61. package/static/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  62. package/static/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  63. package/static/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  64. package/static/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  65. package/static/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  66. package/static/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  67. package/static/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  68. package/static/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  69. package/static/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  70. package/static/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  71. package/static/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  72. package/static/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  73. package/static/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  74. package/static/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  75. package/static/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  76. package/static/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  77. package/static/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  78. package/static/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  79. package/static/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  80. package/static/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  81. package/static/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  82. package/static/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  83. package/static/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  84. package/static/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  85. package/static/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  86. package/static/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  87. package/static/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  88. package/static/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  89. package/static/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  90. package/static/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  91. package/static/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  92. package/static/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  93. package/static/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  94. package/static/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  95. package/static/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  96. package/static/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  97. package/static/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  98. package/static/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  99. package/static/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  100. package/static/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  101. package/static/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  102. package/static/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  103. package/static/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  104. package/static/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  105. package/static/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  106. package/static/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  107. package/static/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  108. package/static/index.html +2 -5
  109. package/static/assets/index-EVWfLxoq.css +0 -1
@@ -3,7 +3,7 @@ const UserModel = require('../models/user');
3
3
  const { verifyPassword, hashPassword, generateCliCode, generatePAT } = require('../utils/crypto');
4
4
 
5
5
  async function authRoutes(fastify, options) {
6
- // POST /login - 用户登录
6
+ // POST /login - User login
7
7
  fastify.post('/login', async (request, reply) => {
8
8
  const { username, password } = request.body || {};
9
9
 
@@ -11,7 +11,7 @@ async function authRoutes(fastify, options) {
11
11
  return reply.code(400).send({ detail: 'Username and password are required' });
12
12
  }
13
13
 
14
- // 查找用户
14
+ // Find user
15
15
  const user = UserModel.findByUsername(username);
16
16
  if (!user) {
17
17
  return reply.code(401).send({ detail: 'Invalid username or password' });
@@ -38,7 +38,7 @@ async function authRoutes(fastify, options) {
38
38
  return { ok: true, user: { id: user.id, username: user.username, name: user.name || null, role: user.role } };
39
39
  });
40
40
 
41
- // POST /logout - 用户登出
41
+ // POST /logout - User logout
42
42
  fastify.post('/logout', async (request, reply) => {
43
43
  const sessionId = request.cookies?.session_id;
44
44
  if (sessionId) {
@@ -48,14 +48,14 @@ async function authRoutes(fastify, options) {
48
48
  return { ok: true };
49
49
  });
50
50
 
51
- // POST /cli-code/generate - 生成 CLI 验证码
51
+ // POST /cli-code/generate - Generate CLI verification code
52
52
  fastify.post('/cli-code/generate', {
53
53
  preHandler: [fastify.authenticate]
54
54
  }, async (request, reply) => {
55
55
  const code = generateCliCode();
56
56
  const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
57
57
 
58
- // 写入 cli_auth_codes
58
+ // Write to cli_auth_codes table
59
59
  db.prepare(`
60
60
  INSERT INTO cli_auth_codes (code, user_id, expires_at, used)
61
61
  VALUES (?, ?, ?, FALSE)
@@ -64,7 +64,7 @@ async function authRoutes(fastify, options) {
64
64
  return { ok: true, code, expires_at: expiresAt };
65
65
  });
66
66
 
67
- // POST /cli-code/verify - 验证 CLI 验证码
67
+ // POST /cli-code/verify - Verify CLI verification code
68
68
  fastify.post('/cli-code/verify', async (request, reply) => {
69
69
  const { code } = request.body || {};
70
70
 
@@ -72,7 +72,7 @@ async function authRoutes(fastify, options) {
72
72
  return reply.code(400).send({ detail: 'Code is required' });
73
73
  }
74
74
 
75
- // 查找验证码:未使用且未过期
75
+ // Find verification code: unused and not expired
76
76
  const codeRecord = db.prepare(`
77
77
  SELECT * FROM cli_auth_codes
78
78
  WHERE code = ? AND used = FALSE AND expires_at > datetime('now')
@@ -82,17 +82,17 @@ async function authRoutes(fastify, options) {
82
82
  return reply.code(401).send({ detail: 'Invalid or expired code' });
83
83
  }
84
84
 
85
- // 标记为已使用
85
+ // Mark as used
86
86
  db.prepare('UPDATE cli_auth_codes SET used = TRUE WHERE code = ?').run(code);
87
87
 
88
- // 生成 PAT
88
+ // Generate PAT
89
89
  const token = generatePAT();
90
90
  db.prepare(`
91
91
  INSERT INTO personal_access_tokens (token, user_id, description)
92
92
  VALUES (?, ?, ?)
93
93
  `).run(token, codeRecord.user_id, 'CLI generated token');
94
94
 
95
- // 获取用户信息
95
+ // Get user info
96
96
  const user = UserModel.findById(codeRecord.user_id);
97
97
 
98
98
  return {
@@ -102,31 +102,31 @@ async function authRoutes(fastify, options) {
102
102
  };
103
103
  });
104
104
 
105
- // GET /me - 获取当前用户信息
105
+ // GET /me - Get current user info
106
106
  fastify.get('/me', {
107
107
  preHandler: [fastify.authenticate]
108
108
  }, async (request, reply) => {
109
109
  return request.user;
110
110
  });
111
111
 
112
- // PATCH /me - 更新个人信息(用户名和姓名)
112
+ // PATCH /me - Update personal info (username and name)
113
113
  fastify.patch('/me', {
114
114
  preHandler: [fastify.authenticate]
115
115
  }, async (request, reply) => {
116
116
  const { username, name } = request.body || {};
117
117
 
118
- // 至少需要提供一个字段
118
+ // At least one field must be provided
119
119
  if (username === undefined && name === undefined) {
120
120
  return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'At least one field must be provided' });
121
121
  }
122
122
 
123
- // 验证用户名
123
+ // Validate username
124
124
  if (username !== undefined) {
125
125
  if (typeof username !== 'string' || username.trim().length === 0) {
126
126
  return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Username cannot be empty' });
127
127
  }
128
128
  const trimmed = username.trim();
129
- // 检查用户名是否已存在(排除自己)
129
+ // Check if username already exists (exclude self)
130
130
  const existing = UserModel.findByUsername(trimmed);
131
131
  if (existing && existing.id !== request.user.id) {
132
132
  return reply.code(400).send({ ok: false, error: 'username_exists', detail: 'Username already exists' });
@@ -142,7 +142,7 @@ async function authRoutes(fastify, options) {
142
142
  return reply.send({ ok: true, user: updated });
143
143
  });
144
144
 
145
- // POST /me/change-password - 修改密码
145
+ // POST /me/change-password - Change password
146
146
  fastify.post('/me/change-password', {
147
147
  preHandler: [fastify.authenticate]
148
148
  }, async (request, reply) => {
@@ -156,7 +156,7 @@ async function authRoutes(fastify, options) {
156
156
  return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'New password must be at least 6 characters' });
157
157
  }
158
158
 
159
- // 验证旧密码 - 需要获取含密码的用户信息
159
+ // Verify old password - need to get user info with password
160
160
  const user = UserModel.findByUsername(request.user.username);
161
161
 
162
162
  if (!verifyPassword(old_password, user.password_hash)) {
@@ -5,11 +5,11 @@ const { invalidateSkill } = require('../utils/model-cache');
5
5
 
6
6
  async function collaboratorsRoutes(fastify, options) {
7
7
 
8
- // GET /:skill_id/collaborators - 获取协作者列表(公开)
8
+ // GET /:skill_id/collaborators - Get collaborators list (public)
9
9
  fastify.get('/:skill_id/collaborators', async (request, reply) => {
10
10
  const { skill_id } = request.params;
11
11
 
12
- // 检查 Skill 是否存在
12
+ // Check if Skill exists
13
13
  const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skill_id);
14
14
  if (!skill) {
15
15
  return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
@@ -42,25 +42,25 @@ async function collaboratorsRoutes(fastify, options) {
42
42
  return reply.send({ skill_id, collaborators: result });
43
43
  });
44
44
 
45
- // POST /:skill_id/collaborators - 添加协作者(owner/admin
45
+ // POST /:skill_id/collaborators - Add collaborator (owner/admin)
46
46
  fastify.post('/:skill_id/collaborators', {
47
47
  preHandler: [fastify.authenticate]
48
48
  }, async (request, reply) => {
49
49
  const { skill_id } = request.params;
50
50
  const { user_id, username } = request.body || {};
51
51
 
52
- // 检查 Skill 是否存在
52
+ // Check if Skill exists
53
53
  const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skill_id);
54
54
  if (!skill) {
55
55
  return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
56
56
  }
57
57
 
58
- // 权限检查
58
+ // Permission check
59
59
  if (!canManageSkill(request.user, skill_id)) {
60
60
  return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
61
61
  }
62
62
 
63
- // 查找目标用户
63
+ // Find target user
64
64
  let targetUser;
65
65
  if (user_id) {
66
66
  targetUser = UserModel.findById(parseInt(user_id));
@@ -72,7 +72,7 @@ async function collaboratorsRoutes(fastify, options) {
72
72
  return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
73
73
  }
74
74
 
75
- // 检查是否已是协作者
75
+ // Check if already a collaborator
76
76
  const existing = db.prepare(
77
77
  'SELECT id FROM skill_collaborators WHERE skill_id = ? AND user_id = ?'
78
78
  ).get(skill_id, targetUser.id);
@@ -81,7 +81,7 @@ async function collaboratorsRoutes(fastify, options) {
81
81
  return reply.code(400).send({ ok: false, error: 'already_collaborator', detail: 'User is already a collaborator' });
82
82
  }
83
83
 
84
- // 添加协作者
84
+ // Add collaborator
85
85
  const result = db.prepare(
86
86
  'INSERT INTO skill_collaborators (skill_id, user_id, role, created_by) VALUES (?, ?, ?, ?)'
87
87
  ).run(skill_id, targetUser.id, 'collaborator', request.user.id);
@@ -98,24 +98,24 @@ async function collaboratorsRoutes(fastify, options) {
98
98
  });
99
99
  });
100
100
 
101
- // DELETE /:skill_id/collaborators/:user_id - 移除协作者(owner/admin
101
+ // DELETE /:skill_id/collaborators/:user_id - Remove collaborator (owner/admin)
102
102
  fastify.delete('/:skill_id/collaborators/:user_id', {
103
103
  preHandler: [fastify.authenticate]
104
104
  }, async (request, reply) => {
105
105
  const { skill_id, user_id } = request.params;
106
106
 
107
- // 检查 Skill 是否存在
107
+ // Check if Skill exists
108
108
  const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skill_id);
109
109
  if (!skill) {
110
110
  return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
111
111
  }
112
112
 
113
- // 权限检查
113
+ // Permission check
114
114
  if (!canManageSkill(request.user, skill_id)) {
115
115
  return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
116
116
  }
117
117
 
118
- // 检查协作者记录
118
+ // Check collaborator record
119
119
  const collaborator = db.prepare(
120
120
  'SELECT id, role FROM skill_collaborators WHERE skill_id = ? AND user_id = ?'
121
121
  ).get(skill_id, parseInt(user_id));
@@ -124,7 +124,7 @@ async function collaboratorsRoutes(fastify, options) {
124
124
  return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Collaborator not found' });
125
125
  }
126
126
 
127
- // 不能移除所有者
127
+ // Cannot remove owner
128
128
  if (collaborator.role === 'owner') {
129
129
  return reply.code(400).send({ ok: false, error: 'cannot_remove_owner', detail: 'Cannot remove the owner' });
130
130
  }
@@ -136,7 +136,7 @@ async function collaboratorsRoutes(fastify, options) {
136
136
  return reply.send({ ok: true, message: 'Collaborator removed' });
137
137
  });
138
138
 
139
- // POST /:skill_id/transfer-ownership - 转移所有权(owner/admin,事务)
139
+ // POST /:skill_id/transfer-ownership - Transfer ownership (owner/admin, transaction)
140
140
  fastify.post('/:skill_id/transfer-ownership', {
141
141
  preHandler: [fastify.authenticate]
142
142
  }, async (request, reply) => {
@@ -147,13 +147,13 @@ async function collaboratorsRoutes(fastify, options) {
147
147
  return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'New owner must be specified' });
148
148
  }
149
149
 
150
- // 检查 Skill 并获取当前所有者
150
+ // Check Skill and get current owner
151
151
  const skill = db.prepare('SELECT id, owner_id FROM skills WHERE id = ?').get(skill_id);
152
152
  if (!skill) {
153
153
  return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
154
154
  }
155
155
 
156
- // 权限检查
156
+ // Permission check
157
157
  if (!canManageSkill(request.user, skill_id)) {
158
158
  return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
159
159
  }
@@ -164,23 +164,23 @@ async function collaboratorsRoutes(fastify, options) {
164
164
  return reply.code(400).send({ ok: false, error: 'same_owner', detail: 'New owner is the same as current owner' });
165
165
  }
166
166
 
167
- // 检查新所有者是否存在
167
+ // Check if new owner exists
168
168
  const newOwner = UserModel.findById(newOwnerId);
169
169
  if (!newOwner) {
170
170
  return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
171
171
  }
172
172
 
173
- // 事务操作
173
+ // Transaction operation
174
174
  const transferTx = db.transaction(() => {
175
- // 1. 更新 skills owner_id
175
+ // 1. Update skills table owner_id
176
176
  db.prepare('UPDATE skills SET owner_id = ?, updated_at = datetime("now") WHERE id = ?')
177
177
  .run(newOwnerId, skill_id);
178
178
 
179
- // 2. 原所有者降级为 collaborator
179
+ // 2. Demote original owner to collaborator
180
180
  db.prepare('UPDATE skill_collaborators SET role = "collaborator" WHERE skill_id = ? AND user_id = ?')
181
181
  .run(skill_id, skill.owner_id);
182
182
 
183
- // 3. 新所有者升级为 owner(如不存在则新增)
183
+ // 3. Promote new owner to owner (insert if not exists)
184
184
  const existing = db.prepare(
185
185
  'SELECT id FROM skill_collaborators WHERE skill_id = ? AND user_id = ?'
186
186
  ).get(skill_id, newOwnerId);
@@ -204,14 +204,14 @@ async function collaboratorsRoutes(fastify, options) {
204
204
  });
205
205
  });
206
206
 
207
- // DELETE /:skill_id - 删除 Skillowner/admin,需 confirm 参数,事务)
207
+ // DELETE /:skill_id - Delete Skill (owner/admin, requires confirm parameter, transaction)
208
208
  fastify.delete('/:skill_id', {
209
209
  preHandler: [fastify.authenticate]
210
210
  }, async (request, reply) => {
211
211
  const { skill_id } = request.params;
212
212
  const { confirm } = request.query;
213
213
 
214
- // 确认参数校验
214
+ // Confirm parameter validation
215
215
  if (confirm !== skill_id) {
216
216
  return reply.code(400).send({
217
217
  ok: false,
@@ -220,22 +220,22 @@ async function collaboratorsRoutes(fastify, options) {
220
220
  });
221
221
  }
222
222
 
223
- // 检查 Skill 是否存在
223
+ // Check if Skill exists
224
224
  const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skill_id);
225
225
  if (!skill) {
226
226
  return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
227
227
  }
228
228
 
229
- // 权限检查
229
+ // Permission check
230
230
  if (!canManageSkill(request.user, skill_id)) {
231
231
  return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
232
232
  }
233
233
 
234
- // 获取版本数量(用于响应)
234
+ // Get versions count (for response)
235
235
  const versionsCount = db.prepare('SELECT COUNT(*) as count FROM skill_versions WHERE skill_id = ?')
236
236
  .get(skill_id).count;
237
237
 
238
- // 事务删除
238
+ // Transaction delete
239
239
  const deleteSkillTx = db.transaction(() => {
240
240
  db.prepare('DELETE FROM skill_versions WHERE skill_id = ?').run(skill_id);
241
241
  db.prepare('DELETE FROM skill_collaborators WHERE skill_id = ?').run(skill_id);
@@ -245,7 +245,7 @@ async function collaboratorsRoutes(fastify, options) {
245
245
  deleteSkillTx();
246
246
  invalidateSkill(skill_id);
247
247
 
248
- // 删除文件系统中的文件
248
+ // Delete files from filesystem
249
249
  const fs = require('fs');
250
250
  const path = require('path');
251
251
  const { getDataDir } = require('../utils/zip');
@@ -2,13 +2,13 @@ const bcrypt = require('bcryptjs');
2
2
  const db = require('../database');
3
3
 
4
4
  /**
5
- * 系统初始化路由
5
+ * System initialization routes
6
6
  * @param {import('fastify').FastifyInstance} fastify
7
7
  */
8
8
  async function initRoutes(fastify) {
9
9
  /**
10
10
  * GET /api/v1/init/status
11
- * 检查系统是否需要初始化(是否存在管理员用户)
11
+ * Check if system needs initialization (whether admin user exists)
12
12
  */
13
13
  fastify.get('/status', async (request, reply) => {
14
14
  const adminCount = db.prepare(
@@ -22,11 +22,11 @@ async function initRoutes(fastify) {
22
22
 
23
23
  /**
24
24
  * POST /api/v1/init/setup
25
- * 初始化系统管理员账号
25
+ * Initialize system admin account
26
26
  * Body: { username, password }
27
27
  */
28
28
  fastify.post('/setup', async (request, reply) => {
29
- // 检查是否已经初始化
29
+ // Check if already initialized
30
30
  const adminCount = db.prepare(
31
31
  "SELECT COUNT(*) as count FROM users WHERE role = 'admin'"
32
32
  ).get();
@@ -39,7 +39,7 @@ async function initRoutes(fastify) {
39
39
 
40
40
  const { username, password } = request.body || {};
41
41
 
42
- // 验证输入
42
+ // Validate input
43
43
  if (!username || !password) {
44
44
  return reply.code(400).send({
45
45
  error: 'Username and password are required'
@@ -58,7 +58,7 @@ async function initRoutes(fastify) {
58
58
  });
59
59
  }
60
60
 
61
- // 检查用户名是否已存在
61
+ // Check if username already exists
62
62
  const existingUser = db.prepare(
63
63
  'SELECT id FROM users WHERE username = ?'
64
64
  ).get(username);
@@ -69,7 +69,7 @@ async function initRoutes(fastify) {
69
69
  });
70
70
  }
71
71
 
72
- // 创建管理员账号
72
+ // Create admin account
73
73
  const passwordHash = bcrypt.hashSync(password, 10);
74
74
  const result = db.prepare(
75
75
  'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
@@ -7,7 +7,7 @@ const { ensureSkillDir, generateVersionNumber, getZipPath, getZipRelativePath }
7
7
  const { canPublishSkill } = require('../utils/permission');
8
8
 
9
9
  async function publishRoutes(fastify, options) {
10
- // POST /publish - 发布新版本
10
+ // POST /publish - Publish new version
11
11
  fastify.post('/publish', {
12
12
  preHandler: [fastify.authenticate]
13
13
  }, async (request, reply) => {
@@ -15,12 +15,12 @@ async function publishRoutes(fastify, options) {
15
15
  let zipBuffer = null;
16
16
  let zipFilename = null;
17
17
 
18
- // 解析 multipart 数据
18
+ // Parse multipart data
19
19
  const parts = request.parts();
20
20
  for await (const part of parts) {
21
21
  if (part.type === 'file') {
22
22
  if (part.fieldname === 'zip_file') {
23
- // 读取文件到 buffer
23
+ // Read file into buffer
24
24
  const chunks = [];
25
25
  for await (const chunk of part.file) {
26
26
  chunks.push(chunk);
@@ -33,19 +33,19 @@ async function publishRoutes(fastify, options) {
33
33
  }
34
34
  }
35
35
 
36
- // 检查必须的 zip 文件
36
+ // Check required zip file
37
37
  if (!zipBuffer) {
38
38
  return reply.code(400).send({ detail: 'zip_file is required' });
39
39
  }
40
40
 
41
41
  const { skill_id, name, description, changelog } = fields;
42
42
 
43
- // 检查 skill_id
43
+ // Check skill_id
44
44
  if (!skill_id) {
45
45
  return reply.code(400).send({ detail: 'skill_id is required' });
46
46
  }
47
47
 
48
- // 检查发布权限
48
+ // Check publish permission
49
49
  if (!canPublishSkill(request.user, skill_id)) {
50
50
  return reply.code(403).send({
51
51
  ok: false,
@@ -54,15 +54,15 @@ async function publishRoutes(fastify, options) {
54
54
  });
55
55
  }
56
56
 
57
- // 检查 skill 是否存在
57
+ // Check if skill exists
58
58
  const skillExists = SkillModel.exists(skill_id);
59
59
 
60
- // 如果 skill 不存在,需要 name 字段来创建新 skill
60
+ // If skill does not exist, name field is required to create new skill
61
61
  if (!skillExists) {
62
62
  if (!name) {
63
63
  return reply.code(400).send({ detail: 'name is required for new skill' });
64
64
  }
65
- // 使用事务创建新 skill 和添加 owner 协作者记录
65
+ // Use transaction to create new skill and add owner collaborator record
66
66
  const createSkillTx = db.transaction(() => {
67
67
  SkillModel.create(skill_id, name, description || '', request.user.id);
68
68
  db.prepare(
@@ -72,20 +72,20 @@ async function publishRoutes(fastify, options) {
72
72
  createSkillTx();
73
73
  }
74
74
 
75
- // 生成版本号
75
+ // Generate version number
76
76
  const version = generateVersionNumber();
77
77
 
78
- // 确保目录存在
78
+ // Ensure directory exists
79
79
  ensureSkillDir(skill_id);
80
80
 
81
- // 写入 zip 文件
81
+ // Write zip file
82
82
  const zipPath = getZipPath(skill_id, version);
83
83
  fs.writeFileSync(zipPath, zipBuffer);
84
84
 
85
- // 获取相对路径(存入数据库)
85
+ // Get relative path (stored in database)
86
86
  const zipRelativePath = getZipRelativePath(skill_id, version);
87
87
 
88
- // 创建版本记录
88
+ // Create version record
89
89
  const versionRecord = VersionModel.create(
90
90
  skill_id,
91
91
  version,
@@ -95,7 +95,7 @@ async function publishRoutes(fastify, options) {
95
95
  description || ''
96
96
  );
97
97
 
98
- // 更新 skill 的最新版本
98
+ // Update skill's latest version
99
99
  SkillModel.updateLatestVersion(skill_id, version);
100
100
  invalidateSkill(skill_id);
101
101
 
@@ -5,7 +5,7 @@ const VersionModel = require('../models/version');
5
5
  const { getZipPath, resolveZipPath } = require('../utils/zip');
6
6
  const { canManageSkill } = require('../utils/permission');
7
7
 
8
- // 格式化 skill,将 owner 转为对象
8
+ // Format skill, convert owner to object
9
9
  function formatSkill(skill, currentUser) {
10
10
  if (!skill) return null;
11
11
  const result = {
@@ -41,7 +41,7 @@ function formatSkill(skill, currentUser) {
41
41
  return result;
42
42
  }
43
43
 
44
- // 格式化 version,将 uploader 转为对象
44
+ // Format version, convert uploader to object
45
45
  function formatVersion(version) {
46
46
  if (!version) return null;
47
47
  return {
@@ -61,7 +61,7 @@ function formatVersion(version) {
61
61
  }
62
62
 
63
63
  async function skillsRoutes(fastify, options) {
64
- // GET / - 获取 skills 列表
64
+ // GET / - Get skills list
65
65
  fastify.get('/', { preHandler: [fastify.optionalAuth] }, async (request, reply) => {
66
66
  const { q } = request.query;
67
67
  const skills = SkillModel.search(q);
@@ -73,7 +73,7 @@ async function skillsRoutes(fastify, options) {
73
73
  };
74
74
  });
75
75
 
76
- // GET /:skill_id - 获取单个 skill
76
+ // GET /:skill_id - Get single skill
77
77
  fastify.get('/:skill_id', { preHandler: [fastify.optionalAuth] }, async (request, reply) => {
78
78
  const { skill_id } = request.params;
79
79
  const skill = SkillModel.findById(skill_id);
@@ -85,11 +85,11 @@ async function skillsRoutes(fastify, options) {
85
85
  return formatSkill(skill, request.user);
86
86
  });
87
87
 
88
- // GET /:skill_id/versions - 获取 skill 的所有版本
88
+ // GET /:skill_id/versions - Get all versions of a skill
89
89
  fastify.get('/:skill_id/versions', async (request, reply) => {
90
90
  const { skill_id } = request.params;
91
91
 
92
- // 先检查 skill 是否存在
92
+ // First check if skill exists
93
93
  if (!SkillModel.exists(skill_id)) {
94
94
  return reply.code(404).send({ detail: 'Skill not found' });
95
95
  }
@@ -103,7 +103,7 @@ async function skillsRoutes(fastify, options) {
103
103
  };
104
104
  });
105
105
 
106
- // GET /:skill_id/versions/:version/download - 下载版本 zip 文件
106
+ // GET /:skill_id/versions/:version/download - Download version zip file
107
107
  fastify.get('/:skill_id/versions/:version/download', async (request, reply) => {
108
108
  const { skill_id, version } = request.params;
109
109
 
@@ -119,11 +119,11 @@ async function skillsRoutes(fastify, options) {
119
119
  return reply.code(404).send({ detail: 'Version not found' });
120
120
  }
121
121
 
122
- // 优先使用数据库里的 zip_path,兼容历史数据;缺失时再回退到规则路径
122
+ // Prefer using zip_path from database for backward compatibility; fallback to rule-based path if missing
123
123
  const zipPath = resolveZipPath(versionRecord.zip_path, skill_id, versionRecord.version);
124
124
  const fallbackZipPath = getZipPath(skill_id, versionRecord.version);
125
125
 
126
- // 检查文件是否存在
126
+ // Check if file exists
127
127
  if (!fs.existsSync(zipPath)) {
128
128
  if (!fs.existsSync(fallbackZipPath)) {
129
129
  return reply.code(404).send({ detail: 'Version not found' });
@@ -132,7 +132,7 @@ async function skillsRoutes(fastify, options) {
132
132
 
133
133
  const finalZipPath = fs.existsSync(zipPath) ? zipPath : fallbackZipPath;
134
134
 
135
- // 设置响应头并返回文件流
135
+ // Set response headers and return file stream
136
136
  const fileName = `${skill_id}-${versionRecord.version}.zip`;
137
137
  reply.header('Content-Type', 'application/zip');
138
138
  reply.header('Content-Disposition', `attachment; filename="${fileName}"`);
@@ -140,7 +140,7 @@ async function skillsRoutes(fastify, options) {
140
140
  return fs.createReadStream(finalZipPath);
141
141
  });
142
142
 
143
- // PUT /:skill_id - 更新 skill 的基本信息
143
+ // PUT /:skill_id - Update skill basic info
144
144
  fastify.put('/:skill_id', {
145
145
  preHandler: [fastify.authenticate]
146
146
  }, async (request, reply) => {
@@ -159,7 +159,7 @@ async function skillsRoutes(fastify, options) {
159
159
  return formatSkill(updated, request.user);
160
160
  });
161
161
 
162
- // PUT /:skill_id/head - 设置 skill 的最新版本 (Head 指针)
162
+ // PUT /:skill_id/head - Set skill's latest version (Head pointer)
163
163
  fastify.put('/:skill_id/head', {
164
164
  preHandler: [fastify.authenticate]
165
165
  }, async (request, reply) => {
@@ -187,7 +187,7 @@ async function skillsRoutes(fastify, options) {
187
187
  return { ok: true, skill_id, latest_version: version };
188
188
  });
189
189
 
190
- // PATCH /:skill_id/versions/:version - 更新指定版本的介绍和日志
190
+ // PATCH /:skill_id/versions/:version - Update description and changelog for specified version
191
191
  fastify.patch('/:skill_id/versions/:version', {
192
192
  preHandler: [fastify.authenticate]
193
193
  }, async (request, reply) => {
@@ -3,8 +3,8 @@ const { hashPassword } = require('../utils/crypto');
3
3
  const db = require('../database');
4
4
 
5
5
  async function usersRoutes(fastify, options) {
6
- // GET /search - 用户搜索(仅需登录,不需要管理员权限)
7
- // 注意:必须在 /:user_id 之前注册
6
+ // GET /search - User search (login required only, no admin permission needed)
7
+ // Note: Must be registered before /:user_id
8
8
  fastify.get('/search', {
9
9
  preHandler: [fastify.authenticate]
10
10
  }, async (request, reply) => {
@@ -25,11 +25,11 @@ async function usersRoutes(fastify, options) {
25
25
  return reply.send({ users });
26
26
  });
27
27
 
28
- // 以下路由都需要管理员权限
28
+ // Routes below require admin permission
29
29
  fastify.register(async function adminRoutes(fastify) {
30
30
  fastify.addHook('preHandler', fastify.requireAdmin);
31
31
 
32
- // GET / - 用户列表
32
+ // GET / - User list
33
33
  fastify.get('/', async (request, reply) => {
34
34
  const { q, status, page = 1, limit = 20 } = request.query;
35
35
  const result = UserModel.list({
@@ -41,7 +41,7 @@ async function usersRoutes(fastify, options) {
41
41
  return reply.send(result);
42
42
  });
43
43
 
44
- // POST / - 创建用户
44
+ // POST / - Create user
45
45
  fastify.post('/', async (request, reply) => {
46
46
  const { username, password, role = 'developer', name } = request.body || {};
47
47
 
@@ -62,7 +62,7 @@ async function usersRoutes(fastify, options) {
62
62
  }
63
63
 
64
64
  const passwordHash = await hashPassword(password);
65
- // 创建用户时记录 created_by
65
+ // Record created_by when creating user
66
66
  const result = db.prepare(
67
67
  "INSERT INTO users (username, password_hash, role, name, status, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, datetime('now'), datetime('now'))"
68
68
  ).run(username.trim(), passwordHash, role, name || null, request.user.id);
@@ -71,7 +71,7 @@ async function usersRoutes(fastify, options) {
71
71
  return reply.code(201).send({ ok: true, user });
72
72
  });
73
73
 
74
- // GET /:user_id - 用户详情
74
+ // GET /:user_id - User details
75
75
  fastify.get('/:user_id', async (request, reply) => {
76
76
  const { user_id } = request.params;
77
77
  const user = UserModel.findByIdWithCreator(parseInt(user_id));
@@ -97,7 +97,7 @@ async function usersRoutes(fastify, options) {
97
97
  return reply.send(result);
98
98
  });
99
99
 
100
- // PATCH /:user_id - 更新用户
100
+ // PATCH /:user_id - Update user
101
101
  fastify.patch('/:user_id', async (request, reply) => {
102
102
  const userId = parseInt(request.params.user_id);
103
103
  const { role, status, name } = request.body || {};
@@ -144,7 +144,7 @@ async function usersRoutes(fastify, options) {
144
144
  return reply.send({ ok: true, user: updated });
145
145
  });
146
146
 
147
- // POST /:user_id/reset-password - 重置密码
147
+ // POST /:user_id/reset-password - Reset password
148
148
  fastify.post('/:user_id/reset-password', async (request, reply) => {
149
149
  const userId = parseInt(request.params.user_id);
150
150
  const { new_password } = request.body || {};