Files
jif2.zhang ec6c9c7677
Docker Build and Push / build-image (push) Successful in 4m34s
feat(AI接入): 内置 MCP 服务与个人 Token 管理
- 新增 /api/mcp (Streamable HTTP) 与 41 个工具: 项目/看板列/任务/清单/打卡; - 新增 McpToken 模型与迁移, DB 仅存 SHA-256 哈希, token 绑定用户; - 新增 /api/mcp-tokens 增删改查, 支持独立命名 (如 R1); - 「我的」页新增 AI 接入 (MCP) 区块: 地址复制/新建/一次性展示/删除; - 补充 docs/MCP.md 与样式
2026-11-09 10:41:08 +08:00

289 lines
11 KiB
JavaScript

// MCP 工具: 检查清单 (清单 / 检查项)
import { z } from 'zod'
import { prisma } from '../../db.js'
import { ensureBuiltin } from '../../routes/checklists.js'
import { tool, reqStr } from '../helpers.js'
const LIST_INCLUDE = { items: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] } }
function withProgress(list) {
const items = list.items || []
const done = items.filter((i) => i.done).length
return {
...list,
items,
total: items.length,
done,
percent: items.length ? done / items.length : 0,
completed: items.length > 0 && done === items.length,
}
}
async function requireList(listId, userId) {
const list = await prisma.checkList.findFirst({ where: { id: Number(listId), userId } })
if (!list) throw new Error('清单不存在')
return list
}
function normalizeSeedItems(items) {
return (Array.isArray(items) ? items : [])
.map((i) => (typeof i === 'string' ? { title: i } : i))
.map((i) => ({ title: String(i?.title || '').trim(), note: String(i?.note || '') }))
.filter((i) => i.title)
}
export function registerChecklistTools(server, ctx) {
const userId = ctx.userId
tool(server, 'list_checklists', {
title: '查询清单',
description: '列出当前用户的检查清单 (出差/回老家等), 含完成进度。',
inputSchema: {
archived: z.boolean().optional().describe('true 查询已归档清单, 默认 false'),
},
annotations: { readOnlyHint: true },
}, async ({ archived = false }) => {
const rows = await prisma.checkList.findMany({
where: { userId, archived: !!archived },
include: LIST_INCLUDE,
orderBy: [{ updatedAt: 'desc' }],
})
return { items: rows.map(withProgress) }
})
tool(server, 'get_checklist', {
title: '查询清单详情',
description: '读取单个清单及其全部检查项、完成进度。',
inputSchema: { listId: z.coerce.number().int().positive().describe('清单 ID') },
annotations: { readOnlyHint: true },
}, async ({ listId }) => {
const row = await prisma.checkList.findFirst({
where: { id: Number(listId), userId },
include: LIST_INCLUDE,
})
if (!row) throw new Error('清单不存在')
return { item: withProgress(row) }
})
tool(server, 'create_checklist', {
title: '新建清单',
description: '新建检查清单, 可直接带检查项, 也可从系统模板/自建模板复制 (传 templateId)。',
inputSchema: {
title: z.string().describe('清单名称'),
scene: z.string().optional().describe('场景, 如 出差 / 回老家'),
note: z.string().optional().describe('备注'),
items: z.array(z.union([
z.string(),
z.object({ title: z.string(), note: z.string().optional() }),
])).optional().describe('初始检查项'),
templateId: z.coerce.number().int().positive().optional().describe('模板 ID, 在没有 items 时使用'),
},
}, async ({ title, scene = '', note = '', items = [], templateId }) => {
const cleanTitle = reqStr(title, '清单名称')
let seed = normalizeSeedItems(items)
if (!seed.length && templateId) {
await ensureBuiltin()
const tpl = await prisma.checkListTemplate.findFirst({
where: { id: Number(templateId), OR: [{ builtin: true }, { userId }] },
})
if (tpl && Array.isArray(tpl.items)) seed = normalizeSeedItems(tpl.items)
}
const row = await prisma.checkList.create({
data: {
userId,
title: cleanTitle,
scene: String(scene || ''),
note: String(note || ''),
items: { create: seed.map((i, idx) => ({ title: i.title, note: i.note, sortOrder: idx })) },
},
include: LIST_INCLUDE,
})
return { item: withProgress(row) }
})
tool(server, 'update_checklist', {
title: '修改清单',
description: '修改清单名称、场景、备注或归档状态。',
inputSchema: {
listId: z.coerce.number().int().positive().describe('清单 ID'),
title: z.string().optional().describe('新名称'),
scene: z.string().optional().describe('新场景'),
note: z.string().optional().describe('新备注'),
archived: z.boolean().optional().describe('是否归档'),
},
}, async ({ listId, title, scene, note, archived }) => {
await requireList(listId, userId)
const data = {}
if (title !== undefined) data.title = reqStr(title, '清单名称')
if (scene !== undefined) data.scene = String(scene)
if (note !== undefined) data.note = String(note)
if (archived !== undefined) data.archived = !!archived
const row = await prisma.checkList.update({
where: { id: Number(listId) }, data, include: LIST_INCLUDE,
})
return { item: withProgress(row) }
})
tool(server, 'delete_checklist', {
title: '删除清单',
description: '永久删除清单及其全部检查项。',
inputSchema: { listId: z.coerce.number().int().positive().describe('清单 ID') },
annotations: { destructiveHint: true },
}, async ({ listId }) => {
await requireList(listId, userId)
await prisma.checkListItem.deleteMany({ where: { listId: Number(listId) } })
await prisma.checkList.delete({ where: { id: Number(listId) } })
return { ok: true }
})
tool(server, 'duplicate_checklist', {
title: '复制清单',
description: '复制一份清单, 勾选状态重置。',
inputSchema: { listId: z.coerce.number().int().positive().describe('清单 ID') },
}, async ({ listId }) => {
const src = await prisma.checkList.findFirst({
where: { id: Number(listId), userId }, include: LIST_INCLUDE,
})
if (!src) throw new Error('清单不存在')
const row = await prisma.checkList.create({
data: {
userId,
title: src.title + ' 副本',
scene: src.scene,
note: src.note,
items: {
create: src.items.map((i, idx) => ({
title: i.title, note: i.note, done: false, sortOrder: idx,
})),
},
},
include: LIST_INCLUDE,
})
return { item: withProgress(row) }
})
tool(server, 'add_checklist_item', {
title: '添加检查项',
description: '在清单末尾添加一个检查项。',
inputSchema: {
listId: z.coerce.number().int().positive().describe('清单 ID'),
title: z.string().describe('检查项内容, 最多 40 字'),
note: z.string().optional().describe('备注'),
},
}, async ({ listId, title, note = '' }) => {
await requireList(listId, userId)
const cleanTitle = reqStr(title, '检查项', 40)
const max = await prisma.checkListItem.findFirst({
where: { listId: Number(listId) }, orderBy: { sortOrder: 'desc' },
})
const row = await prisma.checkListItem.create({
data: {
listId: Number(listId),
title: cleanTitle,
note: String(note || ''),
sortOrder: (max?.sortOrder ?? -1) + 1,
},
})
return { item: row }
})
tool(server, 'update_checklist_item', {
title: '修改检查项',
description: '修改检查项内容、备注、完成状态或排序值。',
inputSchema: {
itemId: z.coerce.number().int().positive().describe('检查项 ID'),
title: z.string().optional().describe('新内容, 最多 40 字'),
note: z.string().optional().describe('新备注'),
done: z.boolean().optional().describe('是否完成'),
sortOrder: z.coerce.number().int().optional().describe('排序值'),
},
}, async ({ itemId, title, note, done, sortOrder }) => {
const exist = await prisma.checkListItem.findFirst({
where: { id: Number(itemId), list: { userId } },
})
if (!exist) throw new Error('检查项不存在')
const data = {}
if (title !== undefined) data.title = reqStr(title, '检查项', 40)
if (note !== undefined) data.note = String(note)
if (sortOrder !== undefined) data.sortOrder = Number(sortOrder) || 0
if (done !== undefined) {
data.done = !!done
data.doneAt = done ? new Date() : null
}
const row = await prisma.checkListItem.update({ where: { id: Number(itemId) }, data })
return { item: row }
})
tool(server, 'delete_checklist_item', {
title: '删除检查项',
description: '永久删除一个检查项。',
inputSchema: { itemId: z.coerce.number().int().positive().describe('检查项 ID') },
annotations: { destructiveHint: true },
}, async ({ itemId }) => {
const exist = await prisma.checkListItem.findFirst({
where: { id: Number(itemId), list: { userId } },
})
if (!exist) throw new Error('检查项不存在')
await prisma.checkListItem.delete({ where: { id: Number(itemId) } })
return { ok: true }
})
tool(server, 'reorder_checklist_items', {
title: '检查项排序',
description: '按传入的检查项 ID 数组重排清单内检查项, 数组顺序即新顺序。',
inputSchema: {
listId: z.coerce.number().int().positive().describe('清单 ID'),
ids: z.array(z.coerce.number().int().positive()).describe('检查项 ID 的新顺序'),
},
}, async ({ listId, ids }) => {
await requireList(listId, userId)
const uniq = [...new Set(ids.map(Number))]
if (uniq.length !== ids.length) throw new Error('ids 不能重复')
const rows = await prisma.checkListItem.findMany({
where: { listId: Number(listId), id: { in: uniq } }, select: { id: true },
})
if (rows.length !== uniq.length) throw new Error('包含不属于该清单的检查项')
await prisma.$transaction(
ids.map((itemId, idx) => prisma.checkListItem.update({
where: { id: Number(itemId) }, data: { sortOrder: idx },
})),
)
return { ok: true, orderedIds: ids.map(Number) }
})
tool(server, 'check_all_checklist_items', {
title: '清单一键勾选',
description: '把清单内全部检查项设为完成或未完成。',
inputSchema: {
listId: z.coerce.number().int().positive().describe('清单 ID'),
done: z.boolean().describe('true 全部完成, false 全部取消'),
},
}, async ({ listId, done }) => {
await requireList(listId, userId)
await prisma.checkListItem.updateMany({
where: { listId: Number(listId) },
data: { done: !!done, doneAt: done ? new Date() : null },
})
const row = await prisma.checkList.findFirst({
where: { id: Number(listId) }, include: LIST_INCLUDE,
})
return { item: withProgress(row) }
})
tool(server, 'list_checklist_templates', {
title: '查询清单模板',
description: '列出系统预置和当前用户自建的清单模板, 可配合 create_checklist 使用。',
inputSchema: {},
annotations: { readOnlyHint: true },
}, async () => {
await ensureBuiltin()
const rows = await prisma.checkListTemplate.findMany({
where: { OR: [{ builtin: true }, { userId }] },
orderBy: [{ builtin: 'desc' }, { name: 'asc' }],
})
return { items: rows }
})
}