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 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'] const ROOT_MODE_TAGS = ['clarify', 'cowork', 'create', 'video', 'code', 'like']
function getAllowedModeTagsFromUser(user) { function getAllowedModeTagsFromUser(user) {
@ -194,6 +199,43 @@ function canUserAccessTaggedResource(user, resourceTags) {
return tags.some((tag) => allowedTags.includes(tag)) 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) { function extractDescription(files) {
const skillFile = files.find(f => f.path === 'SKILL.md' || f.path.endsWith('SKILL.md')) const skillFile = files.find(f => f.path === 'SKILL.md' || f.path.endsWith('SKILL.md'))
if (!skillFile) return '' if (!skillFile) return ''
@ -327,8 +369,9 @@ app.get('/api/skills', async (req, res) => {
app.get('/api/skills/:name', async (req, res) => { app.get('/api/skills/:name', async (req, res) => {
try { try {
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({ const skill = await skillsCollection.findOne({
name: req.params.name, name: nameKey,
is_public: PUBLIC_VISIBILITY_FILTER 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) => { app.get('/api/skills/:name/download', async (req, res) => {
try { try {
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({ const skill = await skillsCollection.findOne({
name: req.params.name, name: nameKey,
is_public: PUBLIC_VISIBILITY_FILTER 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' }) return res.status(400).json({ success: false, error: 'scriptPath is required' })
} }
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({ const skill = await skillsCollection.findOne({
name: req.params.name, name: nameKey,
is_public: PUBLIC_VISIBILITY_FILTER is_public: PUBLIC_VISIBILITY_FILTER
}) })
@ -413,7 +458,7 @@ app.post('/api/skills/:name/execute', requireAuth(async (req, res) => {
const fs = require('fs') const fs = require('fs')
const { execSync } = require('child_process') 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)) const tempScript = path.join(tempDir, path.basename(scriptPath))
fs.writeFileSync(tempScript, script.content, 'utf-8') 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) => { app.get('/api/skills/:name/files/*', async (req, res) => {
try { try {
const filePath = req.params[0] // 捕获通配符部分 const filePath = req.params[0] // 捕获通配符部分
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne({ const skill = await skillsCollection.findOne({
name: req.params.name, name: nameKey,
is_public: PUBLIC_VISIBILITY_FILTER is_public: PUBLIC_VISIBILITY_FILTER
}) })
@ -492,7 +538,8 @@ app.post('/api/skills/:name/lock', async (req, res) => {
if (!authRoutes.hasPermission(req.user, 'canViewSkillsPage')) { if (!authRoutes.hasPermission(req.user, 'canViewSkillsPage')) {
return res.status(403).json({ success: false, error: '无权编辑技能' }) 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) { if (!skill) {
return res.status(404).json({ success: false, error: 'Skill not found' }) 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')) { if (!authRoutes.hasPermission(req.user, 'canViewSkillsPage')) {
return res.status(403).json({ success: false, error: '无权编辑技能' }) 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) { if (!skill) {
return res.status(404).json({ success: false, error: 'Skill not found' }) 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 allowedModeTags = getAllowedModeTagsFromUser(req.user)
const nextTags = normalizeTagList(req.body?.tags).filter((tag) => allowedModeTags.includes(tag)) const nextTags = normalizeTagList(req.body?.tags).filter((tag) => allowedModeTags.includes(tag))
const now = new Date() const now = new Date()
const nameKey = normalizeResourceNameParam(req.params.name)
const result = await skillsCollection.findOneAndUpdate( const result = await skillsCollection.findOneAndUpdate(
{ name: req.params.name }, { name: nameKey },
{ {
$set: { $set: {
tags: nextTags, 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' }) return res.status(400).json({ success: false, error: 'No files provided' })
} }
const skillName = req.params.name.toLowerCase().replace(/[^a-z0-9-]/g, '-') const skillName = normalizeResourceNameParam(req.params.name)
const skillDescription = description || extractDescription(files)
const now = new Date() const now = new Date()
const existingSkill = await skillsCollection.findOne({ name: skillName }) const existingSkill = await skillsCollection.findOne({ name: skillName })
@ -684,6 +732,8 @@ app.post('/api/skills/:name/publish', async (req, res) => {
const nextFiles = useReplaceAll const nextFiles = useReplaceAll
? files ? files
: mergeSkillFilesByPath(existingSkill.files || [], files) : mergeSkillFilesByPath(existingSkill.files || [], files)
const nextFilesNorm = normalizeSkillMdFilesName(nextFiles, skillName)
const skillDescription = description || extractDescription(nextFilesNorm)
const versionEntry = { const versionEntry = {
version: (existingSkill.versions?.length || 0) + 1, version: (existingSkill.versions?.length || 0) + 1,
@ -696,7 +746,7 @@ app.post('/api/skills/:name/publish', async (req, res) => {
const updateData = { const updateData = {
$set: { $set: {
files: nextFiles, files: nextFilesNorm,
description: skillDescription, description: skillDescription,
updated_at: now, updated_at: now,
updated_by: userId, updated_by: userId,
@ -718,19 +768,21 @@ app.post('/api/skills/:name/publish', async (req, res) => {
version: versionEntry.version version: versionEntry.version
}) })
} else { } else {
const filesNorm = normalizeSkillMdFilesName(files, skillName)
const skillDescription = description || extractDescription(filesNorm)
const newSkill = { const newSkill = {
name: skillName, name: skillName,
description: skillDescription, description: skillDescription,
owner: userId, owner: userId,
owner_nickname: userNickname, owner_nickname: userNickname,
files, files: filesNorm,
downloads: 0, downloads: 0,
is_public: true, is_public: true,
tags: normalizedTags, tags: normalizedTags,
versions: [{ versions: [{
version: 1, version: 1,
description: 'Initial version', description: 'Initial version',
files, files: filesNorm,
created_at: now, created_at: now,
created_by: userId, created_by: userId,
created_by_nickname: userNickname 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) => { app.get('/api/skills/:name/versions', async (req, res) => {
try { try {
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne( const skill = await skillsCollection.findOne(
{ name: req.params.name }, { name: nameKey },
{ projection: { versions: 1 } } { 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) => { app.get('/api/skills/:name/versions/:version', async (req, res) => {
try { try {
const versionNum = parseInt(req.params.version) const versionNum = parseInt(req.params.version)
const nameKey = normalizeResourceNameParam(req.params.name)
const skill = await skillsCollection.findOne( const skill = await skillsCollection.findOne(
{ name: req.params.name }, { name: nameKey },
{ projection: { versions: 1 } } { 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) => { app.delete('/api/skills/:name', requireAdmin(async (req, res) => {
try { 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) { if (!skill) {
return res.status(404).json({ success: false, error: 'Skill not found' }) 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) => { app.get('/api/agents/:name', async (req, res) => {
try { 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)) { if (!agent || !canUserAccessTaggedResource(req.user, agent.tags)) {
return res.status(404).json({ success: false, error: 'Agent not found' }) 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)) const normalizedTags = normalizeTagList(tags).filter((tag) => allowedModeTags.includes(tag))
if (!content) return res.status(400).json({ success: false, error: 'No content provided' }) 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 now = new Date()
const existing = await agentsCollection.findOne({ name: agentName }) 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 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( await agentsCollection.updateOne(
{ _id: existing._id }, { _id: existing._id },
{ {
$set: { $set: {
content, content: contentNormalized,
description: description || existing.description, description: description || existing.description,
updated_at: now, updated_at: now,
updated_by: userId, updated_by: userId,
@ -1016,10 +1078,18 @@ app.post('/api/agents/:name/publish', async (req, res) => {
description: description || '', description: description || '',
owner: userId, owner: userId,
owner_nickname: userNickname, owner_nickname: userNickname,
content, content: contentNormalized,
is_public: true, is_public: true,
tags: normalizedTags, 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, created_at: now,
updated_at: now, updated_at: now,
created_by: userId, created_by: userId,
@ -1045,8 +1115,9 @@ app.patch('/api/agents/:name/tags', async (req, res) => {
const allowedModeTags = getAllowedModeTagsFromUser(req.user) const allowedModeTags = getAllowedModeTagsFromUser(req.user)
const nextTags = normalizeTagList(req.body?.tags).filter((tag) => allowedModeTags.includes(tag)) const nextTags = normalizeTagList(req.body?.tags).filter((tag) => allowedModeTags.includes(tag))
const now = new Date() const now = new Date()
const nameKey = normalizeResourceNameParam(req.params.name)
const result = await agentsCollection.findOneAndUpdate( const result = await agentsCollection.findOneAndUpdate(
{ name: req.params.name }, { name: nameKey },
{ {
$set: { $set: {
tags: nextTags, tags: nextTags,
@ -1069,7 +1140,8 @@ app.patch('/api/agents/:name/tags', async (req, res) => {
app.delete('/api/agents/:name', requireAdmin(async (req, res) => { app.delete('/api/agents/:name', requireAdmin(async (req, res) => {
try { 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' }) if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
await agentsCollection.deleteOne({ _id: agent._id }) await agentsCollection.deleteOne({ _id: agent._id })
res.json({ success: true }) res.json({ success: true })
@ -1084,7 +1156,8 @@ app.post('/api/agents/:name/lock', async (req, res, next) => {
if (!authRoutes.hasPermission(req.user, 'canViewAgentsPage')) { if (!authRoutes.hasPermission(req.user, 'canViewAgentsPage')) {
return res.status(403).json({ success: false, error: '无权编辑智能体' }) 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' }) if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
const activeLock = getActiveLock(agent) const activeLock = getActiveLock(agent)
if (activeLock && !sameUserId(activeLock.userId, req.user.id)) { 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')) { if (!authRoutes.hasPermission(req.user, 'canViewAgentsPage')) {
return res.status(403).json({ success: false, error: '无权编辑智能体' }) 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' }) if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
const activeLock = getActiveLock(agent) const activeLock = getActiveLock(agent)
const isAdmin = req.user.role === 'admin' 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) => { app.get('/api/agents/:name/versions', async (req, res) => {
try { 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' }) 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 })) 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 }) 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) => { app.get('/api/agents/:name/versions/:version', async (req, res) => {
try { 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' }) if (!agent) return res.status(404).json({ success: false, error: 'Agent not found' })
const versionNum = parseInt(req.params.version, 10) const versionNum = parseInt(req.params.version, 10)
const version = (agent.versions || []).find((v) => v.version === versionNum) const version = (agent.versions || []).find((v) => v.version === versionNum)