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 与样式
289 lines
11 KiB
JavaScript
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 }
|
|
})
|
|
}
|