hjjjj e86959b847 refactor(server): 移除测试用的技能更新端点并清理代码格式
删除临时测试端点/api/test/update-skill/:name,该端点仅用于测试环境不再需要
同时清理了多个文件中的多余空行和代码格式
2026-03-01 12:09:47 +08:00

426 lines
12 KiB
JavaScript

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()