fix: fix the name consistency
Some checks failed
Deploy skills-market-server / deploy (push) Has been cancelled

This commit is contained in:
hjjjj 2026-04-14 10:19:16 +08:00
parent de65426d64
commit b5634494b7

134
server.js
View File

@ -158,6 +158,11 @@ function normalizeTagList(tags) {
return out
}
/** Slug rules must match publish — DB `name` is always lowercased and sanitized. */
function normalizeResourceNameParam(raw) {
return String(raw || '').toLowerCase().replace(/[^a-z0-9-]/g, '-')
}
const ROOT_MODE_TAGS = ['clarify', 'cowork', 'create', 'video', 'code', 'like']
function getAllowedModeTagsFromUser(user) {
@ -194,6 +199,43 @@ function canUserAccessTaggedResource(user, resourceTags) {
return tags.some((tag) => allowedTags.includes(tag))
}
/**
* 强制 frontmatter `name:` 与路由/库里的规范 id 一致避免模型写成英文标题导致与 Mongo.name 不一致
*/
function normalizeAgentMarkdownContent(content, canonicalName) {
if (typeof content !== 'string' || !canonicalName) return content
const fmMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/)
if (!fmMatch) {
return `---\nname: ${canonicalName}\n---\n\n${content}`
}
const lines = fmMatch[1].split(/\r?\n/)
let replaced = false
const out = lines.map((line) => {
if (!replaced && /^name:\s*/.test(line)) {
replaced = true
return `name: ${canonicalName}`
}
return line
})
if (!replaced) {
out.unshift(`name: ${canonicalName}`)
}
return `---\n${out.join('\n')}\n---` + content.slice(fmMatch[0].length)
}
/** SKILL.md frontmatter `name:` 与路由/库 id 对齐(多文件技能包内只改这一条路径)。 */
function normalizeSkillMdFilesName(files, canonicalName) {
if (!Array.isArray(files)) return files
return files.map((f) => {
if (!f || typeof f.content !== 'string') return f
const p = f.path || ''
if (p === 'SKILL.md' || p.endsWith('/SKILL.md')) {
return { ...f, content: normalizeAgentMarkdownContent(f.content, canonicalName) }
}
return f
})
}
function extractDescription(files) {
const skillFile = files.find(f => f.path === 'SKILL.md' || f.path.endsWith('SKILL.md'))
if (!skillFile) return ''
@ -327,8 +369,9 @@ app.get('/api/skills', async (req, res) => {
app.get('/api/skills/:name', async (req, res) => {
try {
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({
name: req.params.name,
name: nameKey,
is_public: PUBLIC_VISIBILITY_FILTER
})
@ -359,8 +402,9 @@ app.get('/api/skills/:name', async (req, res) => {
app.get('/api/skills/:name/download', async (req, res) => {
try {
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({
name: req.params.name,
name: nameKey,
is_public: PUBLIC_VISIBILITY_FILTER
})
@ -392,8 +436,9 @@ app.post('/api/skills/:name/execute', requireAuth(async (req, res) => {
return res.status(400).json({ success: false, error: 'scriptPath is required' })
}
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({
name: req.params.name,
name: nameKey,
is_public: PUBLIC_VISIBILITY_FILTER
})
@ -413,7 +458,7 @@ app.post('/api/skills/:name/execute', requireAuth(async (req, res) => {
const fs = require('fs')
const { execSync } = require('child_process')
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `skill-${req.params.name}-`))
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `skill-${nameKey}-`))
const tempScript = path.join(tempDir, path.basename(scriptPath))
fs.writeFileSync(tempScript, script.content, 'utf-8')
@ -460,8 +505,9 @@ app.post('/api/skills/:name/execute', requireAuth(async (req, res) => {
app.get('/api/skills/:name/files/*', async (req, res) => {
try {
const filePath = req.params[0] // 捕获通配符部分
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({
name: req.params.name,
name: nameKey,
is_public: PUBLIC_VISIBILITY_FILTER
})
@ -492,7 +538,8 @@ app.post('/api/skills/:name/lock', async (req, res) => {
if (!authRoutes.hasPermission(req.user, 'canViewSkillsPage')) {
return res.status(403).json({ success: false, error: '无权编辑技能' })
}
const skill = await skillsCollection.findOne({ name: req.params.name })
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({ name: nameKey })
if (!skill) {
return res.status(404).json({ success: false, error: 'Skill not found' })
}
@ -517,7 +564,8 @@ app.delete('/api/skills/:name/lock', async (req, res) => {
if (!authRoutes.hasPermission(req.user, 'canViewSkillsPage')) {
return res.status(403).json({ success: false, error: '无权编辑技能' })
}
const skill = await skillsCollection.findOne({ name: req.params.name })
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({ name: nameKey })
if (!skill) {
return res.status(404).json({ success: false, error: 'Skill not found' })
}
@ -590,8 +638,9 @@ app.patch('/api/skills/:name/tags', async (req, res) => {
const allowedModeTags = getAllowedModeTagsFromUser(req.user)
const nextTags = normalizeTagList(req.body?.tags).filter((tag) => allowedModeTags.includes(tag))
const now = new Date()
const nameKey = normalizeResourceNameParam(req.params.name)
const result = await skillsCollection.findOneAndUpdate(
{ name: req.params.name },
{ name: nameKey },
{
$set: {
tags: nextTags,
@ -644,8 +693,7 @@ app.post('/api/skills/:name/publish', async (req, res) => {
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 skillName = normalizeResourceNameParam(req.params.name)
const now = new Date()
const existingSkill = await skillsCollection.findOne({ name: skillName })
@ -684,6 +732,8 @@ app.post('/api/skills/:name/publish', async (req, res) => {
const nextFiles = useReplaceAll
? files
: mergeSkillFilesByPath(existingSkill.files || [], files)
const nextFilesNorm = normalizeSkillMdFilesName(nextFiles, skillName)
const skillDescription = description || extractDescription(nextFilesNorm)
const versionEntry = {
version: (existingSkill.versions?.length || 0) + 1,
@ -696,7 +746,7 @@ app.post('/api/skills/:name/publish', async (req, res) => {
const updateData = {
$set: {
files: nextFiles,
files: nextFilesNorm,
description: skillDescription,
updated_at: now,
updated_by: userId,
@ -718,19 +768,21 @@ app.post('/api/skills/:name/publish', async (req, res) => {
version: versionEntry.version
})
} else {
const filesNorm = normalizeSkillMdFilesName(files, skillName)
const skillDescription = description || extractDescription(filesNorm)
const newSkill = {
name: skillName,
description: skillDescription,
owner: userId,
owner_nickname: userNickname,
files,
files: filesNorm,
downloads: 0,
is_public: true,
tags: normalizedTags,
versions: [{
version: 1,
description: 'Initial version',
files,
files: filesNorm,
created_at: now,
created_by: userId,
created_by_nickname: userNickname
@ -767,8 +819,9 @@ app.post('/api/skills/:name/publish', async (req, res) => {
app.get('/api/skills/:name/versions', async (req, res) => {
try {
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne(
{ name: req.params.name },
{ name: nameKey },
{ projection: { versions: 1 } }
)
@ -793,9 +846,9 @@ app.get('/api/skills/:name/versions', async (req, res) => {
app.get('/api/skills/:name/versions/:version', async (req, res) => {
try {
const versionNum = parseInt(req.params.version)
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne(
{ name: req.params.name },
{ name: nameKey },
{ projection: { versions: 1 } }
)
@ -827,7 +880,8 @@ app.get('/api/skills/:name/versions/:version', async (req, res) => {
app.delete('/api/skills/:name', requireAdmin(async (req, res) => {
try {
const skill = await skillsCollection.findOne({ name: req.params.name })
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({ name: nameKey })
if (!skill) {
return res.status(404).json({ success: false, error: 'Skill not found' })
@ -946,7 +1000,8 @@ app.get('/api/agents/mine', async (req, res, next) => {
app.get('/api/agents/:name', async (req, res) => {
try {
const agent = await agentsCollection.findOne({ name: req.params.name, is_public: PUBLIC_VISIBILITY_FILTER })
const nameKey = normalizeResourceNameParam(req.params.name)
const agent = await agentsCollection.findOne({ name: nameKey, is_public: PUBLIC_VISIBILITY_FILTER })
if (!agent || !canUserAccessTaggedResource(req.user, agent.tags)) {
return res.status(404).json({ success: false, error: 'Agent not found' })
}
@ -969,7 +1024,8 @@ app.post('/api/agents/:name/publish', async (req, res) => {
const normalizedTags = normalizeTagList(tags).filter((tag) => allowedModeTags.includes(tag))
if (!content) return res.status(400).json({ success: false, error: 'No content provided' })
const agentName = req.params.name.toLowerCase().replace(/[^a-z0-9-]/g, '-')
const agentName = normalizeResourceNameParam(req.params.name)
const contentNormalized = normalizeAgentMarkdownContent(content, agentName)
const now = new Date()
const existing = await agentsCollection.findOne({ name: agentName })
@ -994,12 +1050,18 @@ app.post('/api/agents/:name/publish', async (req, res) => {
remote_updated_at: existing.updated_at
})
}
const versionEntry = { version: (existing.versions?.length || 0) + 1, content: existing.content, created_at: now, created_by: userId, created_by_nickname: userNickname }
const versionEntry = {
version: (existing.versions?.length || 0) + 1,
content: existing.content,
created_at: now,
created_by: userId,
created_by_nickname: userNickname
}
await agentsCollection.updateOne(
{ _id: existing._id },
{
$set: {
content,
content: contentNormalized,
description: description || existing.description,
updated_at: now,
updated_by: userId,
@ -1016,10 +1078,18 @@ app.post('/api/agents/:name/publish', async (req, res) => {
description: description || '',
owner: userId,
owner_nickname: userNickname,
content,
content: contentNormalized,
is_public: true,
tags: normalizedTags,
versions: [{ version: 1, content, created_at: now, created_by: userId, created_by_nickname: userNickname }],
versions: [
{
version: 1,
content: contentNormalized,
created_at: now,
created_by: userId,
created_by_nickname: userNickname
}
],
created_at: now,
updated_at: now,
created_by: userId,
@ -1045,8 +1115,9 @@ app.patch('/api/agents/:name/tags', async (req, res) => {
const allowedModeTags = getAllowedModeTagsFromUser(req.user)
const nextTags = normalizeTagList(req.body?.tags).filter((tag) => allowedModeTags.includes(tag))
const now = new Date()
const nameKey = normalizeResourceNameParam(req.params.name)
const result = await agentsCollection.findOneAndUpdate(
{ name: req.params.name },
{ name: nameKey },
{
$set: {
tags: nextTags,
@ -1069,7 +1140,8 @@ app.patch('/api/agents/:name/tags', async (req, res) => {
app.delete('/api/agents/:name', requireAdmin(async (req, res) => {
try {
const agent = await agentsCollection.findOne({ name: req.params.name })
const nameKey = normalizeResourceNameParam(req.params.name)
const agent = await agentsCollection.findOne({ name: nameKey })
if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
await agentsCollection.deleteOne({ _id: agent._id })
res.json({ success: true })
@ -1084,7 +1156,8 @@ app.post('/api/agents/:name/lock', async (req, res, next) => {
if (!authRoutes.hasPermission(req.user, 'canViewAgentsPage')) {
return res.status(403).json({ success: false, error: '无权编辑智能体' })
}
const agent = await agentsCollection.findOne({ name: req.params.name })
const nameKey = normalizeResourceNameParam(req.params.name)
const agent = await agentsCollection.findOne({ name: nameKey })
if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
const activeLock = getActiveLock(agent)
if (activeLock && !sameUserId(activeLock.userId, req.user.id)) {
@ -1104,7 +1177,8 @@ app.delete('/api/agents/:name/lock', async (req, res, next) => {
if (!authRoutes.hasPermission(req.user, 'canViewAgentsPage')) {
return res.status(403).json({ success: false, error: '无权编辑智能体' })
}
const agent = await agentsCollection.findOne({ name: req.params.name })
const nameKey = normalizeResourceNameParam(req.params.name)
const agent = await agentsCollection.findOne({ name: nameKey })
if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
const activeLock = getActiveLock(agent)
const isAdmin = req.user.role === 'admin'
@ -1121,7 +1195,8 @@ app.delete('/api/agents/:name/lock', async (req, res, next) => {
app.get('/api/agents/:name/versions', async (req, res) => {
try {
const agent = await agentsCollection.findOne({ name: req.params.name }, { projection: { versions: 1 } })
const nameKey = normalizeResourceNameParam(req.params.name)
const agent = await agentsCollection.findOne({ name: nameKey }, { projection: { versions: 1 } })
if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
const versions = (agent.versions || []).map((v) => ({ version: v.version, created_at: v.created_at, created_by: v.created_by, created_by_nickname: v.created_by_nickname }))
res.json({ success: true, versions })
@ -1132,7 +1207,8 @@ app.get('/api/agents/:name/versions', async (req, res) => {
app.get('/api/agents/:name/versions/:version', async (req, res) => {
try {
const agent = await agentsCollection.findOne({ name: req.params.name }, { projection: { versions: 1 } })
const nameKey = normalizeResourceNameParam(req.params.name)
const agent = await agentsCollection.findOne({ name: nameKey }, { projection: { versions: 1 } })
if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
const versionNum = parseInt(req.params.version, 10)
const version = (agent.versions || []).find((v) => v.version === versionNum)