Files
dingMCP/scripts/e2e-approval-mcp.mjs
gitea-admin 504145aec6
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
feat: initial public release
2026-08-17 03:04:09 +00:00

113 lines
5.0 KiB
JavaScript

#!/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,
};
}