- 新增 /api/mcp (Streamable HTTP) 与 41 个工具: 项目/看板列/任务/清单/打卡; - 新增 McpToken 模型与迁移, DB 仅存 SHA-256 哈希, token 绑定用户; - 新增 /api/mcp-tokens 增删改查, 支持独立命名 (如 R1); - 「我的」页新增 AI 接入 (MCP) 区块: 地址复制/新建/一次性展示/删除; - 补充 docs/MCP.md 与样式
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// AI 接入 (MCP): 个人授权 token 管理 (登录用户本人)
|
||||
import { Router } from 'express'
|
||||
import { prisma } from '../db.js'
|
||||
import { authRequired } from '../middleware.js'
|
||||
import { generateMcpToken, hashMcpToken } from '../mcp/auth.js'
|
||||
|
||||
const router = Router()
|
||||
router.use(authRequired)
|
||||
router.use((_req, res, next) => {
|
||||
res.set('Cache-Control', 'no-store')
|
||||
next()
|
||||
})
|
||||
|
||||
const NAME_MAX = 32
|
||||
|
||||
function publicToken(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
prefix: row.prefix,
|
||||
lastUsedAt: row.lastUsedAt,
|
||||
createdAt: row.createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
function cleanName(v) {
|
||||
const name = String(v || '').trim()
|
||||
if (!name) return { error: 'Token 名称不能为空' }
|
||||
if (name.length > NAME_MAX) return { error: `Token 名称不能超过 ${NAME_MAX} 字` }
|
||||
return { name }
|
||||
}
|
||||
|
||||
// GET /api/mcp-tokens 当前用户的全部 token (不含明文)
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const rows = await prisma.mcpToken.findMany({
|
||||
where: { userId: req.user.sub },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})
|
||||
res.json({ items: rows.map(publicToken) })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// POST /api/mcp-tokens { name } 新建 token, 明文仅在此响应中返回一次
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.sub
|
||||
const { name, error } = cleanName(req.body?.name)
|
||||
if (error) return res.status(400).json({ error })
|
||||
|
||||
const dup = await prisma.mcpToken.findFirst({ where: { userId, name } })
|
||||
if (dup) return res.status(409).json({ error: `Token 名称「${name}」已存在` })
|
||||
|
||||
const raw = generateMcpToken()
|
||||
const row = await prisma.mcpToken.create({
|
||||
data: {
|
||||
userId,
|
||||
name,
|
||||
prefix: raw.slice(0, 12),
|
||||
tokenHash: hashMcpToken(raw),
|
||||
},
|
||||
})
|
||||
res.json({ item: publicToken(row), token: raw })
|
||||
} catch (e) {
|
||||
if (e?.code === 'P2002') return res.status(409).json({ error: 'Token 名称已存在' })
|
||||
next(e)
|
||||
}
|
||||
})
|
||||
|
||||
// PATCH /api/mcp-tokens/:id { name } 重命名
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.sub
|
||||
const id = Number(req.params.id)
|
||||
const { name, error } = cleanName(req.body?.name)
|
||||
if (error) return res.status(400).json({ error })
|
||||
|
||||
const exist = await prisma.mcpToken.findFirst({ where: { id, userId } })
|
||||
if (!exist) return res.status(404).json({ error: 'Token 不存在' })
|
||||
|
||||
const dup = await prisma.mcpToken.findFirst({ where: { userId, name, NOT: { id } } })
|
||||
if (dup) return res.status(409).json({ error: `Token 名称「${name}」已存在` })
|
||||
|
||||
const row = await prisma.mcpToken.update({ where: { id }, data: { name } })
|
||||
res.json({ item: publicToken(row) })
|
||||
} catch (e) {
|
||||
if (e?.code === 'P2002') return res.status(409).json({ error: 'Token 名称已存在' })
|
||||
next(e)
|
||||
}
|
||||
})
|
||||
|
||||
// DELETE /api/mcp-tokens/:id 删除后立即失效
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.sub
|
||||
const id = Number(req.params.id)
|
||||
const exist = await prisma.mcpToken.findFirst({ where: { id, userId } })
|
||||
if (!exist) return res.status(404).json({ error: 'Token 不存在' })
|
||||
await prisma.mcpToken.delete({ where: { id } })
|
||||
res.json({ ok: true })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user