hjjjj e4c1ff7900
Some checks failed
Deploy skills-market-server / deploy (push) Has been cancelled
feat(admin): 添加管理员面板和权限管理功能
新增管理员面板的静态文件支持,提供用户列表、角色切换和权限配置功能。更新了环境变量示例以包含管理员邮箱,并在auth.js中实现了管理员登录和权限审计日志功能。更新README文档以说明管理员面板的使用和相关接口。
2026-03-24 16:57:33 +08:00

373 lines
12 KiB
JavaScript

/**
* Chat sessions & messages (MongoDB). Auth: Bearer JWT (same as skills publish).
* Session _id is client-generated string (e.g. nanoid) for stable IDs across local/cloud.
*/
const { ObjectId } = require('mongodb')
const AGENT_ONLY_BLOCK_TYPES = new Set([
'tool_use',
'tool_result',
'thinking',
'redacted_thinking',
'server_tool_use',
'web_search_tool_result'
])
function stripContentForUserView(contentStr) {
if (typeof contentStr !== 'string') return contentStr
try {
const parsed = JSON.parse(contentStr)
if (!Array.isArray(parsed)) return contentStr
const filtered = parsed.filter((block) => {
if (!block || typeof block !== 'object') return true
const t = block.type
if (AGENT_ONLY_BLOCK_TYPES.has(t)) return false
return true
})
if (filtered.length === parsed.length) return contentStr
return JSON.stringify(filtered.length ? filtered : [{ type: 'text', text: '' }])
} catch {
return contentStr
}
}
function mapMessageDoc(doc, view) {
const row = {
id: doc._id,
session_id: doc.session_id,
role: doc.role,
content: view === 'user' ? stripContentForUserView(doc.content) : doc.content,
created_at: doc.created_at,
usage: doc.usage ?? null,
sort_order: doc.sort_order,
model_id: doc.model_id ?? null,
provider_id: doc.provider_id ?? null
}
return row
}
function registerChatRoutes(app, db, authRoutes) {
const sessionsCol = db.collection('chat_sessions')
const messagesCol = db.collection('chat_messages')
const withAuth = (handler) => (req, res) => {
authRoutes.verifyToken(req, res, () => handler(req, res))
}
const userObjectId = (req) => new ObjectId(req.user.id)
const assertModeAllowed = (req, res, mode) => {
if (!mode) return true
if (authRoutes.isModeAllowed(req.user, mode)) return true
res.status(403).json({ success: false, error: `无权使用模式: ${mode}` })
return false
}
// --- Sessions ---
app.get(
'/api/chat/sessions',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const list = await sessionsCol.find({ user_id: uid }).sort({ updated_at: -1 }).toArray()
const sessionIds = list.map((s) => s._id)
let countMap = new Map()
if (sessionIds.length > 0) {
const counts = await messagesCol
.aggregate([
{ $match: { user_id: uid, session_id: { $in: sessionIds } } },
{ $group: { _id: '$session_id', c: { $sum: 1 } } }
])
.toArray()
countMap = new Map(counts.map((x) => [x._id, x.c]))
}
res.json({
success: true,
sessions: list.map((s) => ({
id: s._id,
title: s.title,
icon: s.icon ?? null,
mode: s.mode,
created_at: s.created_at,
updated_at: s.updated_at,
project_id: s.project_id ?? null,
working_folder: s.working_folder ?? null,
ssh_connection_id: s.ssh_connection_id ?? null,
pinned: !!s.pinned,
plugin_id: s.plugin_id ?? null,
provider_id: s.provider_id ?? null,
model_id: s.model_id ?? null,
message_count: countMap.get(s._id) ?? 0
}))
})
} catch (err) {
console.error('[Chat] list sessions:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
app.post(
'/api/chat/sessions',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const b = req.body || {}
const id = typeof b.id === 'string' && b.id ? b.id : null
if (!id) {
return res.status(400).json({ success: false, error: 'id required' })
}
const now = Date.now()
if (!assertModeAllowed(req, res, b.mode ?? 'chat')) return
const doc = {
_id: id,
user_id: uid,
title: b.title ?? 'New Conversation',
icon: b.icon ?? null,
mode: b.mode ?? 'chat',
created_at: b.created_at ?? now,
updated_at: b.updated_at ?? now,
project_id: b.project_id ?? null,
working_folder: b.working_folder ?? null,
ssh_connection_id: b.ssh_connection_id ?? null,
pinned: !!b.pinned,
plugin_id: b.plugin_id ?? null,
provider_id: b.provider_id ?? null,
model_id: b.model_id ?? null
}
await sessionsCol.replaceOne({ _id: id, user_id: uid }, doc, { upsert: true })
res.json({ success: true, session: { id } })
} catch (err) {
console.error('[Chat] create session:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
app.patch(
'/api/chat/sessions/:id',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const id = req.params.id
const patch = req.body || {}
const allowed = [
'title',
'icon',
'mode',
'updated_at',
'project_id',
'working_folder',
'ssh_connection_id',
'pinned',
'provider_id',
'model_id'
]
const $set = {}
for (const k of allowed) {
if (patch[k] !== undefined) {
if (k === 'pinned') $set.pinned = !!patch[k]
else $set[k] = patch[k]
}
}
if (patch.mode !== undefined && !assertModeAllowed(req, res, patch.mode)) return
if (Object.keys($set).length === 0) {
return res.json({ success: true })
}
const r = await sessionsCol.updateOne({ _id: id, user_id: uid }, { $set })
if (r.matchedCount === 0) {
return res.status(404).json({ success: false, error: 'Session not found' })
}
res.json({ success: true })
} catch (err) {
console.error('[Chat] patch session:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
app.delete(
'/api/chat/sessions/:id',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const id = req.params.id
await messagesCol.deleteMany({ session_id: id, user_id: uid })
const r = await sessionsCol.deleteOne({ _id: id, user_id: uid })
if (r.deletedCount === 0) {
return res.status(404).json({ success: false, error: 'Session not found' })
}
res.json({ success: true })
} catch (err) {
console.error('[Chat] delete session:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
// --- Messages ---
app.get(
'/api/chat/sessions/:id/messages',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const sessionId = req.params.id
const view = req.query.view === 'user' ? 'user' : 'full'
const limit = Math.min(parseInt(req.query.limit, 10) || 200, 500)
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0)
const sess = await sessionsCol.findOne({ _id: sessionId, user_id: uid })
if (!sess) {
return res.status(404).json({ success: false, error: 'Session not found' })
}
const docs = await messagesCol
.find({ session_id: sessionId, user_id: uid })
.sort({ sort_order: 1 })
.skip(offset)
.limit(limit)
.toArray()
res.json({
success: true,
messages: docs.map((d) => mapMessageDoc(d, view))
})
} catch (err) {
console.error('[Chat] list messages:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
app.post(
'/api/chat/sessions/:id/messages',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const sessionId = req.params.id
const m = req.body || {}
const sess = await sessionsCol.findOne({ _id: sessionId, user_id: uid })
if (!sess) {
return res.status(404).json({ success: false, error: 'Session not found' })
}
const id = typeof m.id === 'string' && m.id ? m.id : null
if (!id) {
return res.status(400).json({ success: false, error: 'message id required' })
}
const doc = {
_id: id,
session_id: sessionId,
user_id: uid,
role: m.role,
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''),
created_at: m.created_at ?? Date.now(),
usage: m.usage ?? null,
sort_order: m.sort_order ?? 0,
model_id: m.model_id ?? null,
provider_id: m.provider_id ?? null
}
await messagesCol.replaceOne(
{ _id: id, session_id: sessionId, user_id: uid },
doc,
{ upsert: true }
)
await sessionsCol.updateOne(
{ _id: sessionId, user_id: uid },
{ $set: { updated_at: Date.now() } }
)
res.json({ success: true })
} catch (err) {
console.error('[Chat] add message:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
app.patch(
'/api/chat/messages/:messageId',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const messageId = req.params.messageId
const patch = req.body || {}
const $set = {}
if (patch.content !== undefined) {
$set.content = typeof patch.content === 'string' ? patch.content : JSON.stringify(patch.content)
}
if (patch.usage !== undefined) $set.usage = patch.usage
if (Object.keys($set).length === 0) {
return res.json({ success: true })
}
const r = await messagesCol.updateOne({ _id: messageId, user_id: uid }, { $set })
if (r.matchedCount === 0) {
return res.status(404).json({ success: false, error: 'Message not found' })
}
res.json({ success: true })
} catch (err) {
console.error('[Chat] patch message:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
app.delete(
'/api/chat/sessions/:id/messages/from/:fromSort',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const sessionId = req.params.id
const fromSort = parseInt(req.params.fromSort, 10)
if (!Number.isFinite(fromSort)) {
return res.status(400).json({ success: false, error: 'from_sort_order required' })
}
await messagesCol.deleteMany({
session_id: sessionId,
user_id: uid,
sort_order: { $gte: fromSort }
})
await sessionsCol.updateOne(
{ _id: sessionId, user_id: uid },
{ $set: { updated_at: Date.now() } }
)
res.json({ success: true })
} catch (err) {
console.error('[Chat] truncate messages:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
app.delete(
'/api/chat/sessions/:id/messages/all',
withAuth(async (req, res) => {
try {
const uid = userObjectId(req)
const sessionId = req.params.id
await messagesCol.deleteMany({ session_id: sessionId, user_id: uid })
await sessionsCol.updateOne(
{ _id: sessionId, user_id: uid },
{ $set: { updated_at: Date.now() } }
)
res.json({ success: true })
} catch (err) {
console.error('[Chat] clear messages:', err)
res.status(500).json({ success: false, error: err.message })
}
})
)
return { sessionsCol, messagesCol }
}
async function ensureChatIndexes(db) {
const sessionsCol = db.collection('chat_sessions')
const messagesCol = db.collection('chat_messages')
await sessionsCol.createIndex({ user_id: 1, updated_at: -1 })
await messagesCol.createIndex({ session_id: 1, sort_order: 1 })
await messagesCol.createIndex({ user_id: 1, session_id: 1 })
}
module.exports = { registerChatRoutes, ensureChatIndexes }