// 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 } }) }