From b5634494b74578e50097b65681194ec4f55c1035 Mon Sep 17 00:00:00 2001 From: hjjjj <1311711287@qq.com> Date: Tue, 14 Apr 2026 10:19:16 +0800 Subject: [PATCH] fix: fix the name consistency --- server.js | 134 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/server.js b/server.js index 41632cf..0d64e45 100644 --- a/server.js +++ b/server.js @@ -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)