#!/usr/bin/env node import { parseArgs } from "node:util"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const [mode, ...optionArgs] = process.argv.slice(2); if (!mode || !["dingtalk", "xft"].includes(mode)) throw new Error(usage()); const { values } = parseArgs({ args: optionArgs, options: { "business-no": { type: "string" }, "execute-real-write": { type: "boolean", default: false }, }, strict: true }); const endpoint = process.env.DINGTALK_APPROVAL_MCP_URL; if (!endpoint) throw new Error("缺少 DINGTALK_APPROVAL_MCP_URL;脚本不会默认连接任何生产网关"); const endpointUrl = new URL(endpoint); if (endpointUrl.protocol !== "https:" || endpointUrl.pathname !== "/mcp") { throw new Error("DINGTALK_APPROVAL_MCP_URL 必须是 HTTPS /mcp 地址"); } const businessNo = String(values["business-no"] ?? "").trim(); const execute = values["execute-real-write"] === true; if (execute && !businessNo) throw new Error("真实写入必须同时提供 --business-no 精确指定审批"); const client = new Client({ name: "dingmcp-e2e", version: "1.0.0" }); const transport = new StdioClientTransport({ command: process.execPath, args: [new URL("../dist/src/bridge.js", import.meta.url).pathname, "--url", endpointUrl.toString()], stderr: "pipe", }); await client.connect(transport); try { await runWorkflow(); } finally { await client.close(); } async function runWorkflow() { const listed = result(await client.callTool({ name: "approval_list_todos", arguments: { source: mode, query: "", limit: 200 }, })); const candidates = Array.isArray(listed.items) ? listed.items : []; if (!businessNo) { console.log(JSON.stringify({ mode, writeExecuted: false, total: candidates.length, items: candidates.map(summaryItem), next: "如需验证单张详情,重新运行并传 --business-no;除非明确获批,不要传 --execute-real-write。", }, null, 2)); return; } const selected = candidates.find((item) => item?.businessNo === businessNo); if (!selected) throw new Error(`${mode} 实时待办中不存在 businessNo=${businessNo}`); const approvalId = selected.approvalId; const detail = result(await client.callTool({ name: "approval_get_detail", arguments: { approvalId, view: mode === "xft" ? "expense" : "summary" }, })); const actions = result(await client.callTool({ name: "approval_get_actions", arguments: { approvalId } })); const approve = Array.isArray(actions.actions) && actions.actions.find((action) => action?.action === "approve"); if (!execute) { console.log(JSON.stringify({ mode, writeExecuted: false, selected: summaryItem(selected), detail: summarizeDetail(detail), actions: actions.actions, }, null, 2)); return; } if (!approve?.allowed) throw new Error(`${mode} 指定待办实时动作不允许 approve`); if (!Array.isArray(selected.availableActions) || !selected.availableActions.includes("approve") || selected.manualAction) { throw new Error(`${mode} 指定待办不能通过 MCP 自动同意`); } const processed = result(await client.callTool({ name: "approval_process_batch", arguments: { action: "approve", remark: "同意", items: [{ approvalId }] }, })); const verified = result(await client.callTool({ name: "approval_list_todos", arguments: { source: mode, query: "", limit: 200 }, })); const stillPresent = Array.isArray(verified.items) && verified.items.some((item) => item?.businessNo === businessNo); console.log(JSON.stringify({ mode, writeExecuted: true, selected: summaryItem(selected), detail: summarizeDetail(detail), actions: actions.actions, processed, verification: { stillPresent, remaining: Array.isArray(verified.items) ? verified.items.length : undefined }, }, null, 2)); if (processed.succeeded !== 1 || stillPresent) process.exitCode = 2; } function usage() { return "usage: DINGTALK_APPROVAL_MCP_URL=https://host/mcp e2e-approval-mcp.mjs dingtalk|xft [--business-no EXACT] [--execute-real-write]"; } function result(call) { const value = call?.structuredContent?.result; if (!value || typeof value !== "object") throw new Error("MCP 工具没有返回 structuredContent.result"); return value; } function summaryItem(item) { return { businessNo: item?.businessNo, title: item?.title, applicant: item?.applicant, amount: item?.amount, submittedAt: item?.submittedAt, availableActions: item?.availableActions, }; } function summarizeDetail(value) { const detail = value?.detail && typeof value.detail === "object" ? value.detail : {}; return { source: value.source, sourceType: value.sourceType, view: value.view, applicant: detail.applicant, amount: detail.amount, submittedAt: detail.submittedAt, summary: detail.summary, fields: Array.isArray(detail.formFields) ? detail.formFields.slice(0, 12) : undefined, expenseItems: Array.isArray(detail.expenseItems) ? detail.expenseItems.slice(0, 5) : undefined, }; }