Skip to main content
Glama
by OpaqueGlass
dailynote.ts9.37 kB
import { z } from "zod"; import { createErrorResponse, createJsonResponse, createSuccessResponse } from "../utils/mcpResponse"; import { appendBlockAPI, createDailyNote, getChildBlocks, getNotebookConf, queryAPI, removeBlockAPI, exportMdContent, getFileAPIv2 } from "@/syapi"; import { getPluginInstance } from "@/utils/pluginHelper"; import { isValidStr } from "@/utils/commonCheck"; import { lang } from "@/utils/lang"; import { McpToolsProvider } from "./baseToolProvider"; import { debugPush, logPush, warnPush, errorPush } from "@/logger"; import { getBlockAssets } from "@/syapi/custom"; import { blobToBase64Object } from "@/utils/common"; import { TASK_STATUS, taskManager } from "@/utils/historyTaskHelper"; import { filterNotebook } from "@/utils/filterCheck"; export class DailyNoteToolProvider extends McpToolsProvider<any> { async getTools(): Promise<McpTool<any>[]> { return [{ name: "siyuan_append_to_dailynote", description: lang("tool_append_dailynote"), schema: { markdownContent: z.string().describe("The Markdown-formatted content to append to today's daily note."), notebookId: z.string().describe("The ID of the target notebook where the daily note is located. The notebook must not be in a closed state."), }, handler: appendToDailynoteHandler, title: lang("tool_title_append_to_dailynote"), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, } },{ name: "siyuan_list_notebook", description: `List all notebooks in SiYuan and return their metadata(such as id, open status, dailyNoteSavePath etc.).`, schema: {}, handler: listNotebookHandler, title: lang("tool_title_list_notebook"), annotations: { readOnlyHint: true, } }, // { // name: "siyuan_read_dailynote", // description: "Read the content of a daily note for a specific date.", // schema: { // date: z.string().optional().describe("The date of the daily note in 'yyyyMMdd' format. If not provided, today's date will be used."), // notebookId: z.string().optional().describe("The ID of the notebook to search for the daily note. If not provided, a random notebook will be chosen."), // }, // handler: readDailynoteHandler, // title: lang("tool_title_read_dailynote"), // annotations: { // readOnlyHint: true, // } // } ] } } async function readDailynoteHandler(params, extra) { let { date, notebookId } = params; if (!date) { const now = new Date(); date = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}`; } // 检查 notebookId 和 date 的合法性 if (notebookId && !/^[a-zA-Z0-9\-]+$/.test(notebookId)) { return createErrorResponse("Invalid notebookId format."); } if (!/^\d{8}$/.test(date)) { return createErrorResponse("Invalid date format. Expected 'yyyyMMdd'."); } if (filterNotebook(notebookId)) { return createErrorResponse("The specified notebook is excluded by the user settings."); } const boxCondition = notebookId ? `AND B.box = '${notebookId}'` : ""; // 首先尝试通过 custom attribute 查询 let sql = `SELECT B.id FROM blocks AS B JOIN attributes AS A ON B.id = A.block_id WHERE A.name = 'custom-dailynote-${date}' ${boxCondition} AND B.type = 'd' LIMIT 1`; let queryResult = await queryAPI(sql); // 如果找不到,则回退到通过文档标题查询 if (!queryResult || queryResult.length === 0) { const formattedDate = `${date.substring(0, 4)}-${date.substring(4, 6)}-${date.substring(6, 8)}`; const boxCondition2 = notebookId ? `AND box = '${notebookId}'` : ""; sql = `SELECT id FROM blocks WHERE content = '${formattedDate}' ${boxCondition2} AND type = 'd' LIMIT 1`; queryResult = await queryAPI(sql); } if (!queryResult || queryResult.length === 0) { const notebookInfo = notebookId ? ` in notebook ${notebookId}` : ''; return createErrorResponse(`Daily note for date ${date} not found${notebookInfo}.`); } const docId = queryResult[0].id; let otherImg = []; try { otherImg = await getAssets(docId); } catch (error) { errorPush("Error converting assets to images", error); } const markdown = await exportMdContent({ id: docId, refMode: 4, embedMode: 1, yfm: false }); const content = markdown["content"] || ""; return createJsonResponse({ content: content, docId: docId, }, otherImg); } async function getAssets(id:string) { const assetsInfo = await getBlockAssets(id); const assetsPathList = assetsInfo.map(item=>item.path); const assetsPromise = []; assetsPathList.forEach((pathItem)=>{ if (isSupportedImageOrAudio(pathItem)) { assetsPromise.push(getFileAPIv2("/data/" + pathItem)); } }); const assetsBlobResult = await Promise.all(assetsPromise); const base64ObjPromise = []; let mediaLengthSum = 0; for (let blob of assetsBlobResult) { logPush("type", typeof blob, blob); if (blob.size / 1024 / 1024 > 2) { logPush("File too large, not returning", blob.size); } else if (mediaLengthSum / 1024 / 1024 > 5) { logPush("Total media size too large, not returning more content", mediaLengthSum); break; } else { mediaLengthSum += blob.size; base64ObjPromise.push(blobToBase64Object(blob)); } } return await Promise.all(base64ObjPromise); } function isSupportedImageOrAudio(path) { const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp', 'ico']; const audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac']; const extMatch = path.match(/\.([a-zA-Z0-9]+)$/); if (!extMatch) return false; const ext = extMatch[1].toLowerCase(); if (imageExtensions.includes(ext)) { return 'image'; } else if (audioExtensions.includes(ext)) { return 'audio'; } else { return false; } } async function appendToDailynoteHandler(params, extra) { const {notebookId, markdownContent} = params; debugPush("插入日记API被调用", params); // notebook if (filterNotebook(notebookId)) { return createErrorResponse("The specified notebook is excluded by the user settings."); } // 确认dailynote id const id = await createDailyNote(notebookId, getPluginInstance().app.appId); // 追加写入 let newBlockId = ""; if (isValidStr(id)) { // query先执行,否则可能真更新数据库了 const queryResult = await queryAPI(`SELECT * FROM blocks WHERE id = "${id}"`); const result = await appendBlockAPI(markdownContent, id); if (result == null) { return createErrorResponse("Failed to append to dailynote"); } // 判断块个数,移除存在的唯一块 if (queryResult && queryResult.length == 0) { try { const childList = await getChildBlocks(id); debugPush("貌似是新建日记,检查子块情况", childList); if (childList && childList.length >= 1 && childList[0].type == "p" && !isValidStr(childList[0]["markdown"])) { debugPush("移除子块", childList[0]); removeBlockAPI(childList[0].id); } } catch(err) { warnPush("err", err); } } newBlockId = result.id; } else { return createErrorResponse("Internal Error: failed to create dailynote"); } taskManager.insert(id, markdownContent, "appendToDailyNote", {}, TASK_STATUS.APPROVED); return createSuccessResponse("Successfully created the dailynote, the block ID for the new content is " + newBlockId); } async function listNotebookHandler(params, extra) { const notebooks = window?.siyuan?.notebooks; if (!notebooks) { return createJsonResponse([]); } const augmentedNotebooks = await Promise.all(notebooks.map(async (notebook) => { try { const confData = await getNotebookConf(notebook.id); if (confData && confData.conf) { return { ...notebook, refCreateSaveBox: confData.conf.refCreateSaveBox, refCreateSavePath: confData.conf.refCreateSavePath, docCreateSaveBox: confData.conf.docCreateSaveBox, docCreateSavePath: confData.conf.docCreateSavePath, dailyNoteSavePath: confData.conf.dailyNoteSavePath, dailyNoteTemplatePath: confData.conf.dailyNoteTemplatePath, }; } } catch (error) { warnPush(`Failed to get conf for notebook ${notebook.id}`, error); } return notebook; })); return createJsonResponse(augmentedNotebooks); }

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