require('dotenv').config() const express = require('express') const cors = require('cors') const { MongoClient, ObjectId } = require('mongodb') const { createAuthRoutes } = require('./routes/auth') const app = express() const PORT = process.env.PORT || 3001 const MONGO_URL = process.env.MONGO_URL || 'mongodb://localhost:27017' const DB_NAME = process.env.DB_NAME || 'skills_market' app.use(cors()) app.use(express.json({ limit: '50mb' })) let db let skillsCollection let authRoutes async function connectDB() { const client = new MongoClient(MONGO_URL) await client.connect() db = client.db(DB_NAME) skillsCollection = db.collection('skills') authRoutes = createAuthRoutes(db) console.log(`[MongoDB] Connected to ${DB_NAME}`) await skillsCollection.createIndex({ name: 1 }, { unique: true }) await skillsCollection.createIndex({ owner: 1 }) await skillsCollection.createIndex({ is_public: 1 }) const usersCollection = db.collection('users') await usersCollection.createIndex({ email: 1 }, { unique: true }) const codesCollection = db.collection('verification_codes') await codesCollection.createIndex({ email: 1 }) await codesCollection.createIndex({ expires_at: 1 }, { expireAfterSeconds: 0 }) } function extractDescription(files) { const skillFile = files.find(f => f.path === 'SKILL.md' || f.path.endsWith('SKILL.md')) if (!skillFile) return '' const content = skillFile.content const fmMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/) if (fmMatch) { const descMatch = fmMatch[1].match(/^description:\s*(.+)$/m) if (descMatch) { return descMatch[1].trim().replace(/^["']|["']$/g, '') } } const lines = content.split('\n') let inFrontmatter = false for (const line of lines) { const trimmed = line.trim() if (trimmed === '---') { inFrontmatter = !inFrontmatter continue } if (inFrontmatter) continue if (!trimmed) continue if (trimmed.startsWith('#')) continue return trimmed.length > 120 ? trimmed.slice(0, 120) + '...' : trimmed } return '' } app.post('/api/auth/send-code', async (req, res) => { await authRoutes.sendCode(req, res) }) app.post('/api/auth/login', async (req, res) => { await authRoutes.login(req, res) }) app.get('/api/auth/profile', async (req, res, next) => { authRoutes.verifyToken(req, res, async () => { await authRoutes.getProfile(req, res) }) }) app.put('/api/auth/profile', async (req, res, next) => { authRoutes.verifyToken(req, res, async () => { await authRoutes.updateProfile(req, res) }) }) app.get('/api/skills', async (req, res) => { try { const { query, offset = 0, limit = 50 } = req.query let filter = { is_public: true } if (query && query.trim()) { const q = query.trim().toLowerCase() filter.$or = [ { name: { $regex: q, $options: 'i' } }, { description: { $regex: q, $options: 'i' } }, { owner: { $regex: q, $options: 'i' } } ] } const total = await skillsCollection.countDocuments(filter) const skills = await skillsCollection .find(filter, { projection: { name: 1, description: 1, owner: 1, downloads: 1, updated_at: 1, tags: 1 } }) .sort({ updated_at: -1 }) .skip(parseInt(offset)) .limit(parseInt(limit)) .toArray() res.json({ success: true, total, skills: skills.map(s => ({ id: s._id, name: s.name, description: s.description, owner: s.owner, downloads: s.downloads || 0, updated_at: s.updated_at, tags: s.tags || [] })) }) } catch (err) { console.error('[API] List skills error:', err) res.status(500).json({ success: false, error: err.message }) } }) app.get('/api/skills/:name', async (req, res) => { try { const skill = await skillsCollection.findOne({ name: req.params.name, is_public: true }) if (!skill) { return res.status(404).json({ success: false, error: 'Skill not found' }) } res.json({ success: true, skill: { id: skill._id, name: skill.name, description: skill.description, owner: skill.owner, downloads: skill.downloads || 0, files: skill.files, versions: skill.versions || [], tags: skill.tags || [], created_at: skill.created_at, updated_at: skill.updated_at } }) } catch (err) { console.error('[API] Get skill error:', err) res.status(500).json({ success: false, error: err.message }) } }) app.get('/api/skills/:name/download', async (req, res) => { try { const skill = await skillsCollection.findOne({ name: req.params.name, is_public: true }) if (!skill) { return res.status(404).json({ success: false, error: 'Skill not found' }) } await skillsCollection.updateOne( { _id: skill._id }, { $inc: { downloads: 1 } } ) res.json({ success: true, files: skill.files, name: skill.name, description: skill.description }) } catch (err) { console.error('[API] Download skill error:', err) res.status(500).json({ success: false, error: err.message }) } }) app.post('/api/skills/:name/publish', async (req, res) => { authRoutes.verifyToken(req, res, async () => { try { const { files, description, tags, localModifiedAt } = req.body const userName = req.user.nickname || req.user.email if (!files || !Array.isArray(files) || files.length === 0) { return res.status(400).json({ success: false, error: 'No files provided' }) } const skillName = req.params.name.toLowerCase().replace(/[^a-z0-9-]/g, '-') const skillDescription = description || extractDescription(files) const now = new Date() const existingSkill = await skillsCollection.findOne({ name: skillName }) if (existingSkill) { const remoteModifiedTime = new Date(existingSkill.updated_at).getTime() const localModifiedTime = localModifiedAt ? new Date(localModifiedAt).getTime() : 0 if (localModifiedTime < remoteModifiedTime) { return res.status(409).json({ success: false, conflict: true, conflictInfo: { remote_updated_at: existingSkill.updated_at, local_modified_at: localModifiedAt, remote_updated_by: existingSkill.updated_by || existingSkill.owner, message: '远程有新版本,发布会丢失远程的修改' } }) } const versionEntry = { version: (existingSkill.versions?.length || 0) + 1, description: `Updated by ${userName}`, files: existingSkill.files, created_at: now, created_by: userName } const updateData = { $set: { files, description: skillDescription, updated_at: now, updated_by: userName, tags: tags || existingSkill.tags || [] }, $push: { versions: versionEntry } } await skillsCollection.updateOne( { _id: existingSkill._id }, updateData ) res.json({ success: true, action: 'updated', name: skillName, version: versionEntry.version }) } else { const newSkill = { name: skillName, description: skillDescription, owner: userName, files, downloads: 0, is_public: true, tags: tags || [], versions: [{ version: 1, description: 'Initial version', files, created_at: now, created_by: userName }], created_at: now, updated_at: now, created_by: userName, updated_by: userName } await skillsCollection.insertOne(newSkill) res.json({ success: true, action: 'created', name: skillName, version: 1 }) } } catch (err) { console.error('[API] Publish skill error:', err) if (err.code === 11000) { return res.status(409).json({ success: false, error: 'Skill name already exists' }) } res.status(500).json({ success: false, error: err.message }) } }) }) app.get('/api/skills/:name/versions', async (req, res) => { try { const skill = await skillsCollection.findOne( { name: req.params.name }, { projection: { versions: 1 } } ) if (!skill) { return res.status(404).json({ success: false, error: 'Skill not found' }) } const versions = (skill.versions || []).map(v => ({ version: v.version, description: v.description, created_at: v.created_at, created_by: v.created_by })) res.json({ success: true, versions }) } catch (err) { console.error('[API] Get versions error:', err) res.status(500).json({ success: false, error: err.message }) } }) app.get('/api/skills/:name/versions/:version', async (req, res) => { try { const versionNum = parseInt(req.params.version) const skill = await skillsCollection.findOne( { name: req.params.name }, { projection: { versions: 1 } } ) if (!skill) { return res.status(404).json({ success: false, error: 'Skill not found' }) } const version = (skill.versions || []).find(v => v.version === versionNum) if (!version) { return res.status(404).json({ success: false, error: 'Version not found' }) } res.json({ success: true, version: { version: version.version, description: version.description, files: version.files, created_at: version.created_at, created_by: version.created_by } }) } catch (err) { console.error('[API] Get version error:', err) res.status(500).json({ success: false, error: err.message }) } }) app.delete('/api/skills/:name', async (req, res) => { authRoutes.verifyToken(req, res, async () => { try { const skill = await skillsCollection.findOne({ name: req.params.name }) if (!skill) { return res.status(404).json({ success: false, error: 'Skill not found' }) } await skillsCollection.deleteOne({ _id: skill._id }) res.json({ success: true }) } catch (err) { console.error('[API] Delete skill error:', err) res.status(500).json({ success: false, error: err.message }) } }) }) app.get('/api/health', (req, res) => { res.json({ success: true, status: 'healthy', timestamp: new Date() }) }) app.get('/api/stats', async (req, res) => { try { const totalSkills = await skillsCollection.countDocuments({ is_public: true }) const totalDownloads = await skillsCollection.aggregate([ { $match: { is_public: true } }, { $group: { _id: null, total: { $sum: '$downloads' } } } ]).toArray() res.json({ success: true, stats: { total_skills: totalSkills, total_downloads: totalDownloads[0]?.total || 0 } }) } catch (err) { console.error('[API] Get stats error:', err) res.status(500).json({ success: false, error: err.message }) } }) async function start() { try { await connectDB() app.listen(PORT, () => { console.log(`[Server] Skills Market API running on http://localhost:${PORT}`) }) } catch (err) { console.error('[Server] Failed to start:', err) process.exit(1) } } start()