Docker Build and Push / build-image (push) Successful in 4m34s
- 新增 /api/mcp (Streamable HTTP) 与 41 个工具: 项目/看板列/任务/清单/打卡; - 新增 McpToken 模型与迁移, DB 仅存 SHA-256 哈希, token 绑定用户; - 新增 /api/mcp-tokens 增删改查, 支持独立命名 (如 R1); - 「我的」页新增 AI 接入 (MCP) 区块: 地址复制/新建/一次性展示/删除; - 补充 docs/MCP.md 与样式
105 lines
3.2 KiB
JavaScript
105 lines
3.2 KiB
JavaScript
// 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
|