refactor: reduce approval context overhead
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
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
This commit is contained in:
+30
-2
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
@@ -488,11 +489,38 @@ export async function startBridge(endpoint: string, statePath = process.env.DING
|
||||
}
|
||||
|
||||
async function runPostInstallLogin(endpoint: string, statePath: string): Promise<void> {
|
||||
syncBundledSkillForLegacyUpdate(statePath);
|
||||
const bridge = new RemoteBridge(endpoint, resolve(statePath));
|
||||
bridge.ensureOnboardingPending();
|
||||
process.stdout.write(`${JSON.stringify(await bridge.loginChallenge())}\n`);
|
||||
}
|
||||
|
||||
export function syncBundledSkillForLegacyUpdate(
|
||||
statePath: string,
|
||||
bundled = resolve(dirname(fileURLToPath(import.meta.url)), "../../skill/dingtalk-approval"),
|
||||
): void {
|
||||
const resolvedState = resolve(statePath);
|
||||
if (basename(resolvedState) !== "session.json" || basename(dirname(resolvedState)) !== "state") return;
|
||||
if (!existsSync(join(bundled, "SKILL.md")) || !existsSync(join(bundled, "references", "create-approval.md"))) return;
|
||||
const target = resolve(dirname(resolvedState), "../skill/dingtalk-approval");
|
||||
const parent = dirname(target);
|
||||
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
||||
const staging = join(parent, `.dingtalk-approval.staging-${process.pid}`);
|
||||
const backup = join(parent, `.dingtalk-approval.backup-${process.pid}`);
|
||||
rmSync(staging, { recursive: true, force: true });
|
||||
rmSync(backup, { recursive: true, force: true });
|
||||
try {
|
||||
cpSync(bundled, staging, { recursive: true });
|
||||
if (existsSync(target)) renameSync(target, backup);
|
||||
renameSync(staging, target);
|
||||
rmSync(backup, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
if (!existsSync(target) && existsSync(backup)) renameSync(backup, target);
|
||||
rmSync(staging, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDirectEntrypoint(import.meta.url, process.argv[1])) {
|
||||
const args = process.argv.slice(2);
|
||||
const endpoint = parseEndpoint(args);
|
||||
|
||||
+24
-3
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash, createPublicKey, randomUUID, verify as verifySignature } from "node:crypto";
|
||||
import {
|
||||
chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync,
|
||||
chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync,
|
||||
readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync,
|
||||
} from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
@@ -301,8 +301,7 @@ async function installClient(paths: ClientPaths, args: ParsedArgs, context: Comm
|
||||
}
|
||||
renameSync(staging, versionDir);
|
||||
}
|
||||
mkdirPrivate(paths.skillDir);
|
||||
writeAtomic(paths.skillFile, skillBody, 0o600);
|
||||
installBundledSkill(paths, join(versionDir, "node_modules", "dingtalk-approval-client", "skill", "dingtalk-approval"), skillBody);
|
||||
writeAtomic(paths.publicKeyFile, publicArtifact, 0o600);
|
||||
writeLaunchers(paths, context.platform);
|
||||
const state: ClientInstallState = {
|
||||
@@ -590,6 +589,28 @@ function mcpConfig(paths: ClientPaths): Record<string, unknown> {
|
||||
return { mcpServers: { "dingtalk-approval": { type: "stdio", command: paths.bridgeCommand, args: [] } } };
|
||||
}
|
||||
|
||||
function installBundledSkill(paths: ClientPaths, bundledSkillDir: string, skillBody: Buffer): void {
|
||||
const reference = join(bundledSkillDir, "references", "create-approval.md");
|
||||
if (!existsSync(join(bundledSkillDir, "SKILL.md")) || !existsSync(reference)) {
|
||||
throw new Error("客户端包缺少完整 Skill 资源");
|
||||
}
|
||||
const parent = dirname(paths.skillDir);
|
||||
mkdirPrivate(parent);
|
||||
const staging = join(parent, `.dingtalk-approval.staging-${process.pid}-${randomUUID()}`);
|
||||
const backup = join(parent, `.dingtalk-approval.backup-${process.pid}-${randomUUID()}`);
|
||||
try {
|
||||
cpSync(bundledSkillDir, staging, { recursive: true });
|
||||
writeAtomic(join(staging, "SKILL.md"), skillBody, 0o600);
|
||||
if (existsSync(paths.skillDir)) renameSync(paths.skillDir, backup);
|
||||
renameSync(staging, paths.skillDir);
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
if (!existsSync(paths.skillDir) && existsSync(backup)) renameSync(backup, paths.skillDir);
|
||||
if (existsSync(staging)) rmSync(staging, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function writeLaunchers(paths: ClientPaths, platform: NodeJS.Platform): void {
|
||||
mkdirPrivate(paths.binDir);
|
||||
const launcher = `#!/usr/bin/env node\nimport { readFileSync } from "node:fs";\nimport { dirname, join, resolve } from "node:path";\nimport { fileURLToPath } from "node:url";\nimport { spawn } from "node:child_process";\nconst mode = process.argv[2];\nconst root = resolve(dirname(fileURLToPath(import.meta.url)), "..");\nconst state = JSON.parse(readFileSync(join(root, "install-state.json"), "utf8"));\nconst runtime = resolve(root, state.runtimeDir);\nconst target = join(runtime, "node_modules", "dingtalk-approval-client", "dist", "src", mode === "bridge" ? "bridge.js" : "client-installer.js");\nconst args = mode === "bridge" ? ["--url", state.gatewayUrl, ...process.argv.slice(3)] : [...process.argv.slice(3), "--app-root", root];\nconst env = mode === "bridge" ? { ...process.env, DINGTALK_APPROVAL_BRIDGE_STATE: join(root, "state", "session.json") } : process.env;\nconst child = spawn(process.execPath, [target, ...args], { stdio: "inherit", env });\nchild.on("exit", (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; });\nchild.on("error", (error) => { console.error(error.message); process.exitCode = 1; });\n`;
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ export function createApprovalMcpServer(
|
||||
|
||||
server.registerTool("approval_list_todos", {
|
||||
title: "列出我的审批待办",
|
||||
description: "审批操作的入口。通过同一登录身份实时列出当前用户在钉钉OA和薪福通中的待办,并取得后续接口必须使用的approvalId;列表尽可能直接包含金额、UTC提交时间及Asia/Shanghai本地时间。",
|
||||
description: "审批操作的入口。通过同一登录身份实时列出当前用户在钉钉OA和薪福通中的待办,并取得后续接口必须使用的approvalId;列表尽可能直接包含金额和带时区的提交时间。",
|
||||
inputSchema: {
|
||||
source: z.enum(["all", "dingtalk", "xft"]).default("all"),
|
||||
query: z.string().max(200).default(""),
|
||||
|
||||
@@ -1149,21 +1149,18 @@ export class UnifiedApprovalService {
|
||||
return {
|
||||
approvalId: this.signPortableApprovalReference(ref),
|
||||
source: "dingtalk",
|
||||
sourceType: item.source,
|
||||
businessNo: item.processInstanceId,
|
||||
title: item.title ?? item.processName ?? "钉钉审批",
|
||||
applicant: {
|
||||
...(item.applicantName ? { name: item.applicantName } : {}),
|
||||
...(item.applicantDepartment ? { department: item.applicantDepartment } : {}),
|
||||
},
|
||||
...(item.submittedAt ? { submittedAt: item.submittedAt } : {}),
|
||||
...(item.submittedAt ? localTimeFields(item.submittedAt) : {}),
|
||||
...(item.submittedAt ? { submittedAt: localTime(item.submittedAt) } : {}),
|
||||
...(item.amount !== undefined ? { amount: { value: item.amount, currency: item.currency ?? "CNY" } } : {}),
|
||||
...(item.summary ? { summary: item.summary } : {}),
|
||||
availableActions: [
|
||||
...(clientOnly ? [] : ["approve", "reject"]),
|
||||
],
|
||||
actionsHint: "approve/reject 可直接批量提交,网关会在写入前逐张实时复核;只有退回需要调用 approval_get_actions 获取 targetId。",
|
||||
...(clientOnly ? {
|
||||
manualAction: {
|
||||
required: true,
|
||||
@@ -1188,15 +1185,13 @@ export class UnifiedApprovalService {
|
||||
return {
|
||||
approvalId: this.signPortableApprovalReference(ref),
|
||||
source: "xft",
|
||||
sourceType: "smart_expense",
|
||||
businessNo: item.billId,
|
||||
title: item.title ?? "薪福通审批",
|
||||
applicant: {
|
||||
...(item.applicantName ? { name: item.applicantName } : {}),
|
||||
...(item.applicantDepartment ? { department: item.applicantDepartment } : {}),
|
||||
},
|
||||
...(item.applyTime ? { submittedAt: item.applyTime } : {}),
|
||||
...(item.applyTime ? localTimeFields(item.applyTime) : {}),
|
||||
...(item.applyTime ? { submittedAt: localTime(item.applyTime) } : {}),
|
||||
...(item.amount !== undefined ? { amount: { value: item.amount, currency: item.currency ?? "CNY" } } : {}),
|
||||
...(item.reason ? { summary: item.reason } : {}),
|
||||
availableActions: [
|
||||
@@ -1848,6 +1843,10 @@ function localTimeFields(value: string): Record<string, string> {
|
||||
return { submittedAtLocal: local, submittedTimeZone: "Asia/Shanghai" };
|
||||
}
|
||||
|
||||
function localTime(value: string): string {
|
||||
return localTimeFields(value).submittedAtLocal ?? value;
|
||||
}
|
||||
|
||||
function dingTalkSubmissionTemplateAllowed(template: { dirName?: string; name?: string }): boolean {
|
||||
if (!template.dirName || !template.name) return false;
|
||||
const names = DINGTALK_SUBMISSION_TEMPLATES[template.dirName as keyof typeof DINGTALK_SUBMISSION_TEMPLATES];
|
||||
|
||||
Reference in New Issue
Block a user