@ian2018cs/agenthub 0.1.0 → 0.1.2
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/dist/assets/index-BITEl9tD.js +154 -0
- package/dist/assets/index-BWbEF217.css +32 -0
- package/dist/assets/{vendor-icons-CJV4dnDL.js → vendor-icons-q7OlK-Uk.js} +76 -61
- package/dist/index.html +3 -3
- package/package.json +2 -1
- package/server/builtin-skills/.gitkeep +0 -0
- package/server/builtin-skills/deploy-frontend/SKILL.md +220 -0
- package/server/builtin-skills/deploy-frontend/scripts/cleanup.py +158 -0
- package/server/builtin-skills/deploy-frontend/scripts/deploy.py +216 -0
- package/server/cli.js +7 -6
- package/server/database/db.js +262 -17
- package/server/database/init.sql +35 -3
- package/server/middleware/auth.js +5 -4
- package/server/routes/admin.js +113 -2
- package/server/routes/auth.js +113 -38
- package/server/routes/skills.js +8 -0
- package/server/services/builtin-skills.js +147 -0
- package/server/services/email.js +90 -0
- package/server/services/user-directories.js +4 -0
- package/dist/assets/index-B4ru3EJb.css +0 -32
- package/dist/assets/index-DDFuyrpY.js +0 -154
package/server/routes/auth.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import bcrypt from 'bcrypt';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
import { userDb,
|
|
4
|
+
import { userDb, verificationDb, domainWhitelistDb } from '../database/db.js';
|
|
5
5
|
import { generateToken, authenticateToken } from '../middleware/auth.js';
|
|
6
6
|
import { initUserDirectories } from '../services/user-directories.js';
|
|
7
|
+
import { sendVerificationCode, isSmtpConfigured } from '../services/email.js';
|
|
7
8
|
|
|
8
9
|
const router = express.Router();
|
|
9
10
|
|
|
@@ -11,9 +12,10 @@ const router = express.Router();
|
|
|
11
12
|
router.get('/status', async (req, res) => {
|
|
12
13
|
try {
|
|
13
14
|
const hasUsers = await userDb.hasUsers();
|
|
14
|
-
res.json({
|
|
15
|
+
res.json({
|
|
15
16
|
needsSetup: !hasUsers,
|
|
16
|
-
isAuthenticated: false // Will be overridden by frontend if token exists
|
|
17
|
+
isAuthenticated: false, // Will be overridden by frontend if token exists
|
|
18
|
+
smtpConfigured: isSmtpConfigured()
|
|
17
19
|
});
|
|
18
20
|
} catch (error) {
|
|
19
21
|
console.error('Auth status error:', error);
|
|
@@ -21,37 +23,106 @@ router.get('/status', async (req, res) => {
|
|
|
21
23
|
}
|
|
22
24
|
});
|
|
23
25
|
|
|
24
|
-
//
|
|
25
|
-
router.post('/
|
|
26
|
+
// Send verification code to email
|
|
27
|
+
router.post('/send-code', async (req, res) => {
|
|
26
28
|
try {
|
|
27
|
-
const {
|
|
29
|
+
const { email } = req.body;
|
|
30
|
+
const ipAddress = req.ip || req.socket?.remoteAddress;
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// Validate email format
|
|
33
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
34
|
+
if (!email || !emailRegex.test(email)) {
|
|
35
|
+
return res.status(400).json({ error: '请输入有效的邮箱地址' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if SMTP is configured
|
|
39
|
+
if (!isSmtpConfigured()) {
|
|
40
|
+
return res.status(500).json({ error: 'SMTP 未配置,请联系管理员' });
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
// Check rate limit
|
|
44
|
+
const rateCheck = verificationDb.canSendCode(email);
|
|
45
|
+
if (!rateCheck.allowed) {
|
|
46
|
+
if (rateCheck.error === 'rate_limit_email') {
|
|
47
|
+
return res.status(429).json({
|
|
48
|
+
error: '发送过于频繁,请稍后再试',
|
|
49
|
+
waitSeconds: rateCheck.waitSeconds
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return res.status(429).json({ error: '今日发送次数已达上限' });
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const uuid = uuidv4();
|
|
55
|
+
// Determine if this is login or registration
|
|
56
|
+
const existingUser = userDb.getUserByEmail(email);
|
|
57
|
+
const type = existingUser ? 'login' : 'register';
|
|
43
58
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
// For new registrations, check domain whitelist
|
|
60
|
+
if (type === 'register') {
|
|
61
|
+
if (!domainWhitelistDb.isEmailAllowed(email)) {
|
|
62
|
+
return res.status(403).json({ error: '该邮箱域名不在允许注册的列表中' });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Generate and store code
|
|
67
|
+
const { code } = verificationDb.createCode(email, type, ipAddress);
|
|
68
|
+
|
|
69
|
+
// Send email
|
|
70
|
+
await sendVerificationCode(email, code);
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
res.json({
|
|
73
|
+
success: true,
|
|
74
|
+
type, // 'login' or 'register' - frontend can show appropriate message
|
|
75
|
+
message: '验证码已发送'
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Send code error:', error);
|
|
80
|
+
res.status(500).json({ error: '发送验证码失败,请稍后再试' });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Verify code and login/register
|
|
85
|
+
router.post('/verify-code', async (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const { email, code } = req.body;
|
|
88
|
+
|
|
89
|
+
if (!email || !code) {
|
|
90
|
+
return res.status(400).json({ error: '邮箱和验证码不能为空' });
|
|
91
|
+
}
|
|
50
92
|
|
|
51
|
-
//
|
|
52
|
-
|
|
93
|
+
// Verify the code
|
|
94
|
+
const verification = verificationDb.verifyCode(email, code);
|
|
53
95
|
|
|
54
|
-
|
|
96
|
+
if (!verification.valid) {
|
|
97
|
+
// Increment attempts for failed verification
|
|
98
|
+
verificationDb.incrementAttempts(email, code);
|
|
99
|
+
|
|
100
|
+
if (verification.error === 'max_attempts') {
|
|
101
|
+
return res.status(429).json({ error: '验证码尝试次数过多,请重新获取' });
|
|
102
|
+
}
|
|
103
|
+
return res.status(401).json({ error: '验证码无效或已过期' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let user = userDb.getUserByEmail(email);
|
|
107
|
+
|
|
108
|
+
// If user doesn't exist, create new user (registration)
|
|
109
|
+
if (!user) {
|
|
110
|
+
const userCount = userDb.getUserCount();
|
|
111
|
+
const role = userCount === 0 ? 'admin' : 'user';
|
|
112
|
+
const uuid = uuidv4();
|
|
113
|
+
|
|
114
|
+
user = userDb.createUserWithEmail(email, uuid, role);
|
|
115
|
+
|
|
116
|
+
// Initialize user directories
|
|
117
|
+
await initUserDirectories(uuid);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check if user is disabled
|
|
121
|
+
if (user.status === 'disabled') {
|
|
122
|
+
return res.status(403).json({ error: '账户已被禁用' });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Generate token with 30-day expiration
|
|
55
126
|
const token = generateToken(user);
|
|
56
127
|
|
|
57
128
|
// Update last login
|
|
@@ -61,7 +132,8 @@ router.post('/register', async (req, res) => {
|
|
|
61
132
|
success: true,
|
|
62
133
|
user: {
|
|
63
134
|
id: user.id,
|
|
64
|
-
|
|
135
|
+
email: user.email,
|
|
136
|
+
username: user.username || user.email,
|
|
65
137
|
uuid: user.uuid,
|
|
66
138
|
role: user.role
|
|
67
139
|
},
|
|
@@ -69,37 +141,38 @@ router.post('/register', async (req, res) => {
|
|
|
69
141
|
});
|
|
70
142
|
|
|
71
143
|
} catch (error) {
|
|
72
|
-
console.error('
|
|
73
|
-
|
|
74
|
-
res.status(409).json({ error: 'Username already exists' });
|
|
75
|
-
} else {
|
|
76
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
77
|
-
}
|
|
144
|
+
console.error('Verify code error:', error);
|
|
145
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
78
146
|
}
|
|
79
147
|
});
|
|
80
148
|
|
|
81
|
-
// User login
|
|
149
|
+
// User login with username/password (for admin-created accounts)
|
|
82
150
|
router.post('/login', async (req, res) => {
|
|
83
151
|
try {
|
|
84
152
|
const { username, password } = req.body;
|
|
85
153
|
|
|
86
154
|
if (!username || !password) {
|
|
87
|
-
return res.status(400).json({ error: '
|
|
155
|
+
return res.status(400).json({ error: '用户名和密码不能为空' });
|
|
88
156
|
}
|
|
89
157
|
|
|
90
158
|
const user = userDb.getUserByUsername(username);
|
|
91
159
|
if (!user) {
|
|
92
|
-
return res.status(401).json({ error: '
|
|
160
|
+
return res.status(401).json({ error: '用户名或密码错误' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if user has a password (admin-created account)
|
|
164
|
+
if (!user.password_hash) {
|
|
165
|
+
return res.status(401).json({ error: '此账户不支持密码登录' });
|
|
93
166
|
}
|
|
94
167
|
|
|
95
168
|
// Check if user is disabled
|
|
96
169
|
if (user.status === 'disabled') {
|
|
97
|
-
return res.status(403).json({ error: '
|
|
170
|
+
return res.status(403).json({ error: '账户已被禁用' });
|
|
98
171
|
}
|
|
99
172
|
|
|
100
173
|
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
|
101
174
|
if (!isValidPassword) {
|
|
102
|
-
return res.status(401).json({ error: '
|
|
175
|
+
return res.status(401).json({ error: '用户名或密码错误' });
|
|
103
176
|
}
|
|
104
177
|
|
|
105
178
|
const token = generateToken(user);
|
|
@@ -110,6 +183,7 @@ router.post('/login', async (req, res) => {
|
|
|
110
183
|
user: {
|
|
111
184
|
id: user.id,
|
|
112
185
|
username: user.username,
|
|
186
|
+
email: user.email,
|
|
113
187
|
uuid: user.uuid,
|
|
114
188
|
role: user.role
|
|
115
189
|
},
|
|
@@ -127,7 +201,8 @@ router.get('/user', authenticateToken, (req, res) => {
|
|
|
127
201
|
res.json({
|
|
128
202
|
user: {
|
|
129
203
|
id: req.user.id,
|
|
130
|
-
username: req.user.username,
|
|
204
|
+
username: req.user.username || req.user.email,
|
|
205
|
+
email: req.user.email,
|
|
131
206
|
uuid: req.user.uuid,
|
|
132
207
|
role: req.user.role
|
|
133
208
|
}
|
|
@@ -141,4 +216,4 @@ router.post('/logout', authenticateToken, (req, res) => {
|
|
|
141
216
|
res.json({ success: true, message: 'Logged out successfully' });
|
|
142
217
|
});
|
|
143
218
|
|
|
144
|
-
export default router;
|
|
219
|
+
export default router;
|
package/server/routes/skills.js
CHANGED
|
@@ -5,6 +5,7 @@ import { spawn } from 'child_process';
|
|
|
5
5
|
import multer from 'multer';
|
|
6
6
|
import AdmZip from 'adm-zip';
|
|
7
7
|
import { getUserPaths, getPublicPaths, DATA_DIR } from '../services/user-directories.js';
|
|
8
|
+
import { markBuiltinSkillRemoved, isBuiltinSkillPath } from '../services/builtin-skills.js';
|
|
8
9
|
|
|
9
10
|
const router = express.Router();
|
|
10
11
|
|
|
@@ -238,6 +239,8 @@ router.get('/', async (req, res) => {
|
|
|
238
239
|
if (repoMatch) {
|
|
239
240
|
repository = `${repoMatch[1]}/${repoMatch[2]}`;
|
|
240
241
|
}
|
|
242
|
+
} else if (realPath.includes('/builtin-skills/')) {
|
|
243
|
+
source = 'builtin';
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
|
|
@@ -374,12 +377,14 @@ router.delete('/:name', async (req, res) => {
|
|
|
374
377
|
// Check the symlink target to determine source
|
|
375
378
|
let realPath = null;
|
|
376
379
|
let isImported = false;
|
|
380
|
+
let isBuiltin = false;
|
|
377
381
|
|
|
378
382
|
try {
|
|
379
383
|
const stat = await fs.lstat(linkPath);
|
|
380
384
|
if (stat.isSymbolicLink()) {
|
|
381
385
|
realPath = await fs.realpath(linkPath);
|
|
382
386
|
isImported = realPath.includes('/skills-import/');
|
|
387
|
+
isBuiltin = isBuiltinSkillPath(realPath);
|
|
383
388
|
}
|
|
384
389
|
} catch (err) {
|
|
385
390
|
return res.status(404).json({ error: 'Skill not found' });
|
|
@@ -395,6 +400,9 @@ router.delete('/:name', async (req, res) => {
|
|
|
395
400
|
} catch (err) {
|
|
396
401
|
console.error('Error removing imported skill files:', err);
|
|
397
402
|
}
|
|
403
|
+
} else if (isBuiltin) {
|
|
404
|
+
// Mark as removed so it won't be re-added on next sync
|
|
405
|
+
await markBuiltinSkillRemoved(userUuid, name);
|
|
398
406
|
}
|
|
399
407
|
|
|
400
408
|
res.json({ success: true, message: 'Skill deleted' });
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { getUserPaths } from './user-directories.js';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
// Path to built-in skills directory
|
|
10
|
+
const BUILTIN_SKILLS_DIR = path.join(__dirname, '../builtin-skills');
|
|
11
|
+
|
|
12
|
+
// State file version for future migrations
|
|
13
|
+
const STATE_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get list of all available built-in skills
|
|
17
|
+
*/
|
|
18
|
+
export async function getBuiltinSkills() {
|
|
19
|
+
const skills = [];
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const entries = await fs.readdir(BUILTIN_SKILLS_DIR, { withFileTypes: true });
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
26
|
+
const skillPath = path.join(BUILTIN_SKILLS_DIR, entry.name);
|
|
27
|
+
|
|
28
|
+
// Check for SKILLS.md or SKILL.md
|
|
29
|
+
const skillsFile = path.join(skillPath, 'SKILLS.md');
|
|
30
|
+
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Try SKILLS.md first, then SKILL.md
|
|
34
|
+
let found = false;
|
|
35
|
+
try {
|
|
36
|
+
await fs.access(skillsFile);
|
|
37
|
+
found = true;
|
|
38
|
+
} catch {
|
|
39
|
+
await fs.access(skillFile);
|
|
40
|
+
found = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (found) {
|
|
44
|
+
skills.push({
|
|
45
|
+
name: entry.name,
|
|
46
|
+
path: skillPath
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Skip if neither file exists
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err.code !== 'ENOENT') {
|
|
56
|
+
console.error('Error reading builtin skills:', err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return skills;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get path to user's builtin skills state file
|
|
65
|
+
*/
|
|
66
|
+
function getStatePath(userUuid) {
|
|
67
|
+
const userPaths = getUserPaths(userUuid);
|
|
68
|
+
return path.join(userPaths.claudeDir, '.builtin-skills-state.json');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load user's builtin skills state
|
|
73
|
+
*/
|
|
74
|
+
export async function loadBuiltinSkillsState(userUuid) {
|
|
75
|
+
try {
|
|
76
|
+
const statePath = getStatePath(userUuid);
|
|
77
|
+
const content = await fs.readFile(statePath, 'utf-8');
|
|
78
|
+
return JSON.parse(content);
|
|
79
|
+
} catch {
|
|
80
|
+
return {
|
|
81
|
+
version: STATE_VERSION,
|
|
82
|
+
removedSkills: []
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Save user's builtin skills state
|
|
89
|
+
*/
|
|
90
|
+
export async function saveBuiltinSkillsState(userUuid, state) {
|
|
91
|
+
const statePath = getStatePath(userUuid);
|
|
92
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Initialize built-in skills for a user
|
|
97
|
+
* Creates symlinks for all built-in skills not in user's removed list
|
|
98
|
+
*/
|
|
99
|
+
export async function initBuiltinSkills(userUuid) {
|
|
100
|
+
const userPaths = getUserPaths(userUuid);
|
|
101
|
+
const builtinSkills = await getBuiltinSkills();
|
|
102
|
+
const state = await loadBuiltinSkillsState(userUuid);
|
|
103
|
+
|
|
104
|
+
for (const skill of builtinSkills) {
|
|
105
|
+
// Skip if user has explicitly removed this skill
|
|
106
|
+
if (state.removedSkills.includes(skill.name)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const linkPath = path.join(userPaths.skillsDir, skill.name);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Check if link already exists
|
|
114
|
+
await fs.lstat(linkPath);
|
|
115
|
+
// Link exists, skip
|
|
116
|
+
} catch {
|
|
117
|
+
// Link doesn't exist, create it
|
|
118
|
+
try {
|
|
119
|
+
await fs.symlink(skill.path, linkPath);
|
|
120
|
+
console.log(`Created builtin skill symlink: ${skill.name}`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error(`Error creating builtin skill symlink ${skill.name}:`, err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Mark a built-in skill as removed by user
|
|
130
|
+
*/
|
|
131
|
+
export async function markBuiltinSkillRemoved(userUuid, skillName) {
|
|
132
|
+
const state = await loadBuiltinSkillsState(userUuid);
|
|
133
|
+
|
|
134
|
+
if (!state.removedSkills.includes(skillName)) {
|
|
135
|
+
state.removedSkills.push(skillName);
|
|
136
|
+
await saveBuiltinSkillsState(userUuid, state);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a path is a built-in skill
|
|
142
|
+
*/
|
|
143
|
+
export function isBuiltinSkillPath(realPath) {
|
|
144
|
+
return realPath?.includes('/builtin-skills/') ?? false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { BUILTIN_SKILLS_DIR };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
|
|
3
|
+
// SMTP configuration from environment variables
|
|
4
|
+
const getSmtpConfig = () => {
|
|
5
|
+
const config = {
|
|
6
|
+
host: process.env.SMTP_SERVER,
|
|
7
|
+
port: parseInt(process.env.SMTP_PORT) || 587,
|
|
8
|
+
secure: process.env.SMTP_USE_TLS === 'true', // true for 465, false for other ports
|
|
9
|
+
auth: {
|
|
10
|
+
user: process.env.SMTP_USERNAME,
|
|
11
|
+
pass: process.env.SMTP_PASSWORD,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Handle opportunistic TLS (STARTTLS)
|
|
16
|
+
if (process.env.SMTP_OPPORTUNISTIC_TLS === 'true') {
|
|
17
|
+
config.secure = false;
|
|
18
|
+
config.tls = {
|
|
19
|
+
rejectUnauthorized: false, // Allow self-signed certificates
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return config;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Check if SMTP is configured
|
|
27
|
+
export const isSmtpConfigured = () => {
|
|
28
|
+
return !!(process.env.SMTP_SERVER && process.env.SMTP_USERNAME && process.env.SMTP_PASSWORD);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Create transporter lazily (only when needed)
|
|
32
|
+
let transporter = null;
|
|
33
|
+
|
|
34
|
+
const getTransporter = () => {
|
|
35
|
+
if (!transporter) {
|
|
36
|
+
if (!isSmtpConfigured()) {
|
|
37
|
+
throw new Error('SMTP is not configured. Please set SMTP_SERVER, SMTP_USERNAME, and SMTP_PASSWORD environment variables.');
|
|
38
|
+
}
|
|
39
|
+
transporter = nodemailer.createTransport(getSmtpConfig());
|
|
40
|
+
}
|
|
41
|
+
return transporter;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Send verification code email
|
|
45
|
+
export const sendVerificationCode = async (email, code) => {
|
|
46
|
+
const transport = getTransporter();
|
|
47
|
+
const fromAddress = process.env.SMTP_FROM || process.env.SMTP_USERNAME;
|
|
48
|
+
|
|
49
|
+
const mailOptions = {
|
|
50
|
+
from: fromAddress,
|
|
51
|
+
to: email,
|
|
52
|
+
subject: 'AgentHub 登录验证码',
|
|
53
|
+
html: `
|
|
54
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
55
|
+
<div style="text-align: center; margin-bottom: 30px;">
|
|
56
|
+
<h1 style="color: #1a1a1a; font-size: 24px; margin: 0;">AgentHub</h1>
|
|
57
|
+
</div>
|
|
58
|
+
<div style="background-color: #f8f9fa; border-radius: 8px; padding: 30px; text-align: center;">
|
|
59
|
+
<h2 style="color: #1a1a1a; font-size: 20px; margin: 0 0 20px 0;">您的验证码</h2>
|
|
60
|
+
<div style="font-size: 36px; font-weight: bold; color: #2563eb; letter-spacing: 8px; margin: 20px 0; font-family: monospace;">
|
|
61
|
+
${code}
|
|
62
|
+
</div>
|
|
63
|
+
<p style="color: #666; font-size: 14px; margin: 20px 0 0 0;">
|
|
64
|
+
验证码有效期为 <strong>5 分钟</strong>,请尽快使用。
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; text-align: center;">
|
|
68
|
+
<p style="color: #999; font-size: 12px; margin: 0;">
|
|
69
|
+
如果您没有请求此验证码,请忽略此邮件。
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
`,
|
|
74
|
+
text: `您的 AgentHub 登录验证码是:${code},有效期 5 分钟。如果您没有请求此验证码,请忽略此邮件。`,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return transport.sendMail(mailOptions);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Verify SMTP connection
|
|
81
|
+
export const verifySmtpConnection = async () => {
|
|
82
|
+
try {
|
|
83
|
+
const transport = getTransporter();
|
|
84
|
+
await transport.verify();
|
|
85
|
+
return { success: true };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('SMTP connection verification failed:', error);
|
|
88
|
+
return { success: false, error: error.message };
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { initBuiltinSkills } from './builtin-skills.js';
|
|
3
4
|
|
|
4
5
|
// Base data directory (configurable via env)
|
|
5
6
|
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data');
|
|
@@ -85,6 +86,9 @@ export async function initUserDirectories(userUuid) {
|
|
|
85
86
|
await fs.writeFile(usageScanStatePath, JSON.stringify(scanState, null, 2));
|
|
86
87
|
console.log(`Created .usage-scan-state.json for user ${userUuid}`);
|
|
87
88
|
|
|
89
|
+
// Initialize built-in skills
|
|
90
|
+
await initBuiltinSkills(userUuid);
|
|
91
|
+
|
|
88
92
|
return paths;
|
|
89
93
|
}
|
|
90
94
|
|