Skip to main content
Glama
by OpaqueGlass
flashCard.ts10.9 kB
import { addRiffCards, getRiffDecks, queryAPI, removeRiffCards } from "@/syapi"; import { getBlockDBItem, isValidDeck, QUICK_DECK_ID } from "@/syapi/custom"; import { isValidStr } from "@/utils/commonCheck"; import { createErrorResponse, createJsonResponse, createSuccessResponse } from "@/utils/mcpResponse"; import { createNewDocWithParentId } from "./sharedFunction"; import { McpToolsProvider } from "./baseToolProvider"; import { z } from "zod"; import { useWsIndexQueue } from "@/utils/wsMainHelper"; import { TASK_STATUS, taskManager } from "@/utils/historyTaskHelper"; import { filterBlock } from "@/utils/filterCheck"; const TYPE_VALID_LIST = ["h1", "h2", "h3", "h4", "h5", "highlight", "superBlock"] as const; export class FlashcardToolProvider extends McpToolsProvider<any> { async getTools(): Promise<McpTool<any>[]> { return [{ name: "siyuan_create_flashcards_with_new_doc", description: "Create New Document, and Make Flashcards with Specific Method", schema: { parentId: z.string().describe("The ID of the parent document where the new document will be created."), docTitle: z.string().describe("The title of the new document that will contain the flashcards."), type: z.enum(TYPE_VALID_LIST).describe("The block type to use when formatting flashcards (e.g., heading or highlight)."), deckId: z.string().optional().describe("The ID of the flashcard deck to which the new content belongs."), markdownContent: z.string().describe("The Markdown-formatted content to append at the end of the new document."), }, handler: addFlashCardMarkdown, title: "Create Flashcards with New Doc", annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, } }, { name: "siyuan_create_flashcards", description: "Create flashcards from one or more block IDs.", schema: { blockIds: z.array(z.string()).describe("The IDs of the blocks to be converted into flashcards."), deckId: z.string().optional().describe("The ID of the deck to add the cards to. If not provided, a default deck will be used."), }, handler: createFlashcardsHandler, title: "Create Flashcards", annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, } }, { name: "siyuan_delete_flashcards", description: "Delete flashcards from a deck using their corresponding block IDs.", schema: { blockIds: z.array(z.string()).describe("The IDs of the blocks corresponding to the flashcards to be deleted."), deckId: z.string().optional().describe("The ID of the deck to remove the cards from. If not provided, a default deck will be used."), }, handler: deleteFlashcardsHandler, title: "Delete Flashcards", annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, } }]; } } async function addFlashCardMarkdown(params, extra) { let { parentId, docTitle, type, deckId, markdownContent } = params; let { sendNotification, _meta} = extra; if (await filterBlock(parentId, null)) { return createErrorResponse("The specified document or block is excluded by the user settings, so cannot create a new note under it."); } // 默认deck if (!isValidStr(deckId)) { deckId = QUICK_DECK_ID; } if (!await isValidDeck(deckId)) { return createErrorResponse("制卡失败:卡包DeckId不存在,如果用户没有明确指定卡包名称或ID,可以将参数deckId设置为\"\""); } if (type === "highlight" && !window.siyuan.config.editor.markdown.inlineMath) { return createErrorResponse("制卡失败:高亮内容制卡需要用户启用Markdown标记语法,请提醒用户开启此功能(设置-编辑器-Markdown行级标记语法)"); } const {result, newDocId} = await createNewDocWithParentId(parentId, docTitle, markdownContent); if (result) { taskManager.insert(newDocId, markdownContent, "createNewNoteWithFlashCard", {}, TASK_STATUS.APPROVED); } if (result) { let progressInterval: any; if (_meta?.progressToken) { let currentProgress = 0; const maxDuration = 120 * 1000; // 2 minutes in ms const updateInterval = 200; // ms const progressIncrement = updateInterval / maxDuration; progressInterval = setInterval(() => { currentProgress += progressIncrement; if (currentProgress < 0.95) { sendNotification({ method: "notifications/progress", params: { progress: currentProgress, progressToken: _meta.progressToken } }); } }, updateInterval); } try { // 需要等待索引完成,自 https://github.com/siyuan-note/siyuan/issues/15390 更新后,此等待索引方式实际无效,变为sleep const addCardsResult = await useWsIndexQueue()?.enqueue(async ()=>{ return await parseDocAddCards(newDocId, type, deckId); }); if (_meta?.progressToken) { await sendNotification({ method: "notifications/progress", params: { progress: 1, progressToken: _meta.progressToken } }); } return createSuccessResponse(`成功添加了 ${addCardsResult} 张闪卡`); } finally { if (progressInterval) { clearInterval(progressInterval); } } } else { return createErrorResponse("制卡失败:创建闪卡文档时遇到未知问题"); } } async function createFlashcardsHandler(params, extra) { let { blockIds, deckId } = params; let { sendNotification, _meta} = extra; if (!isValidStr(deckId)) { deckId = QUICK_DECK_ID; } if (!await isValidDeck(deckId)) { return createErrorResponse("Card creation failed: The DeckId does not exist. If the user has not specified a deck name or ID, set the deckId parameter to an empty string."); } const filteredIds = []; for (let i = 0; i < blockIds.length; i++) { const blockId = blockIds[i]; const dbItem = await getBlockDBItem(blockId); if (dbItem == null) { return createErrorResponse(`Invalid block ID: ${blockId}. Please check if the ID exists and is correct.`); } if (await filterBlock(blockId, dbItem)) { continue; } filteredIds.push(blockId); if (_meta?.progressToken) { await sendNotification({ method: "notifications/progress", params: { progress: (i + 1) / blockIds.length + 2, progressToken: _meta.progressToken } }); } } const addCardsResult = await addRiffCards(filteredIds, deckId); if (addCardsResult === null) { return createErrorResponse("Failed to create flashcards."); } return createSuccessResponse(`Successfully added ${filteredIds.length} flashcards.`); } async function deleteFlashcardsHandler(params, extra) { let { blockIds, deckId } = params; if (!isValidStr(deckId)) { deckId = ""; } if (!await isValidDeck(deckId) && deckId !== "") { return createErrorResponse("Card deletion failed: The DeckId does not exist. If the user has not specified a deck name or ID, set the deckId parameter to an empty string."); } const removeResult = await removeRiffCards(blockIds, deckId); if (removeResult === null) { return createErrorResponse("Failed to delete flashcards."); } return createSuccessResponse(`Successfully removed flashcards corresponding to ${blockIds.length} blocks.`); } async function parseDocAddCards(docId:string, addType: string, deckId: string) { const functionDict = { "h1": provideHeadingIds.bind(this, docId, addType), "h2": provideHeadingIds.bind(this, docId, addType), "h3": provideHeadingIds.bind(this, docId, addType), "h4": provideHeadingIds.bind(this, docId, addType), "h5": provideHeadingIds.bind(this, docId, addType), "highlight": provideHighlightBlockIds.bind(this, docId), "superBlock": provideSuperBlockIds.bind(this, docId), } const blockIds = await functionDict[addType](); let afterAddSize = await addRiffCards(blockIds, deckId); return blockIds.length; } async function listDeck(params, extra) { if (!window.siyuan.config.flashcard.deck) { return createSuccessResponse("用户禁用了卡包,在调用其他工具时,可以直接将参数deckId设置为\"\""); } const deckResponse = await getRiffDecks(); return createJsonResponse(deckResponse); } // async function addFlashCardFromExistBlock(params, extra) { // const { blockId, deckId } = params; // // 确认入参 // } function isValidType(type) { return TYPE_VALID_LIST.includes(type); } function getIdFromSqlItem(sqlResponse) { sqlResponse = sqlResponse ?? []; return sqlResponse.map(item=>item.id); } async function provideHeadingIds(docId: string, headingType: string) { let queryResult = await queryAPI(`select id from blocks where root_id = '${docId}' and type = 'h' and subtype = '${headingType}';`); return getIdFromSqlItem(queryResult); } async function provideSuperBlockIds(docId:string) { let queryResult = await queryAPI(`select * from blocks where root_id = '${docId}' and type = 's'`); return getIdFromSqlItem(queryResult); } async function provideHighlightBlockIds(docId:string) { let queryResult = await queryAPI(`SELECT * FROM blocks WHERE root_id = '${docId}' AND type = "p" AND markdown regexp '==.*=='`); let finalResult = new Array(); queryResult.forEach((oneResult) => { let oneContent = oneResult.markdown; // logPush(`[正则检查]原内容`, oneContent); oneContent = oneContent.replace(new RegExp("(?!<\\\\)`[^`]*`(?!`)", "g"), ""); // logPush(`[正则检查]移除行内代码`, oneContent); let regExp = new RegExp("(?<!\\\\)==[^=]*[^\\\\]=="); // logPush(`[正则检查]重新匹配高亮`, oneContent.match(regExp)); if (oneContent.match(regExp) != null) { finalResult.push(oneResult); } }); return getIdFromSqlItem(queryResult); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/OpaqueGlass/syplugin-anMCPServer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server