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
113 lines
5.0 KiB
JavaScript
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,
|
|
};
|
|
}
|