WAPC2 实现企微审批只读 POC

这个提交包含在:
2026-05-19 10:07:10 +08:00
父节点 d02442606c
当前提交 08987ba921
共修改 15 个文件,包含 1389 行新增和 44 行删除
@@ -0,0 +1,507 @@
#!/usr/bin/env node
require("dotenv/config");
const fs = require("node:fs");
const path = require("node:path");
const { getConfig } = require("../config");
const {
APPROVAL_POC_TARGETS,
fetchWecomApprovalDetail,
fetchWecomApprovalTemplateDetail,
maskApprovalIdentifier,
} = require("../services/integrations/wecomApproval");
const DEFAULT_REPORT_ROOT = path.resolve(__dirname, "../../../artifacts/qa-flow");
const REPORT_PRIVACY =
"approval type/counts/masked template and sp_no prefixes only; no phone/email/token/secret/full userid/full sp_no/form values/attachments/comments/raw payload";
const TEMPLATE_ENV = Object.freeze({
leave: "WECOM_APPROVAL_LEAVE_TEMPLATE_ID",
missed_punch: "WECOM_APPROVAL_MISSED_PUNCH_TEMPLATE_ID",
});
const SP_NO_ENV = Object.freeze({
leave: "WECOM_APPROVAL_LEAVE_TEST_SP_NO",
missed_punch: "WECOM_APPROVAL_MISSED_PUNCH_TEST_SP_NO",
});
function printHelp() {
process.stdout.write(`Usage: node src/scripts/check-wecom-approval-poc.js [options]
QA-only 企业微信审批模板与低风险 sp_no 只读 POC。
默认不写 OA/企微资料,不接真实回调,不进入审批镜像。
Options:
--live 启用企微 live 只读检查。
--format=text|json 输出格式,默认 text。
--report-dir=<path> 报告目录,默认 artifacts/qa-flow/wecom-approval-poc-<timestamp>。
--template=leave:<id> 指定请假模板 ID。
--template=missed_punch:<id> 指定补卡模板 ID。
--sp-no=leave:<sp_no> 指定请假低风险测试审批单号。
--sp-no=missed_punch:<sp_no> 指定补卡低风险测试审批单号。
--help 显示帮助。
Secret 固定规则:
审批模板详情/审批详情:WECOM_AGENT_SECRET。
不使用 WECOM_CONTACT_SECRET、WECOM_CHECKIN_SECRET 或 WECOM_HR_ASSISTANT_SECRET 兜底。
隐私边界:
报告只输出模板类型、计数、状态、脱敏模板 ID / sp_no 前缀和错误分类。
不输出手机号、邮箱、token、secret、完整 userid、完整 sp_no、表单值、附件、评论或企微原始 payload。
`);
}
function timestamp() {
return new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
}
function toSafeString(value, maxLength = 500) {
if (value == null) return "";
return String(value).trim().slice(0, maxLength);
}
function safeMessage(value) {
return toSafeString(value, 500)
.replace(/access_token=[^&\s]+/gi, "access_token=***")
.replace(/[A-Za-z0-9_-]{24,}/g, "***")
.slice(0, 240);
}
function supportedTargetKeys() {
return APPROVAL_POC_TARGETS.map((item) => item.key);
}
function parseTargetPair(value, flagName) {
const raw = toSafeString(value, 500);
const separatorIndex = raw.indexOf(":");
if (separatorIndex <= 0) {
throw new Error(`${flagName} requires <target>:<value>`);
}
const target = raw.slice(0, separatorIndex).trim();
const itemValue = raw.slice(separatorIndex + 1).trim();
if (!supportedTargetKeys().includes(target)) {
throw new Error(`${flagName} unsupported target: ${target}`);
}
if (!itemValue) {
throw new Error(`${flagName} requires non-empty value`);
}
return { target, value: itemValue };
}
function parseArgs(argv) {
const options = {
live: process.env.WECOM_APPROVAL_POC_LIVE === "1",
format: "text",
reportDir: process.env.WECOM_APPROVAL_POC_REPORT_DIR
|| path.join(DEFAULT_REPORT_ROOT, `wecom-approval-poc-${timestamp()}`),
templates: {},
spNos: {},
help: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--help" || arg === "-h") {
options.help = true;
} else if (arg === "--live") {
options.live = true;
} else if (arg === "--no-live") {
options.live = false;
} else if (arg === "--format") {
options.format = argv[index + 1] || options.format;
index += 1;
} else if (arg.startsWith("--format=")) {
options.format = arg.slice("--format=".length);
} else if (arg === "--report-dir") {
options.reportDir = argv[index + 1] || options.reportDir;
index += 1;
} else if (arg.startsWith("--report-dir=")) {
options.reportDir = arg.slice("--report-dir=".length);
} else if (arg === "--template") {
const pair = parseTargetPair(argv[index + 1], "--template");
options.templates[pair.target] = pair.value;
index += 1;
} else if (arg.startsWith("--template=")) {
const pair = parseTargetPair(arg.slice("--template=".length), "--template");
options.templates[pair.target] = pair.value;
} else if (arg === "--sp-no") {
const pair = parseTargetPair(argv[index + 1], "--sp-no");
options.spNos[pair.target] = pair.value;
index += 1;
} else if (arg.startsWith("--sp-no=")) {
const pair = parseTargetPair(arg.slice("--sp-no=".length), "--sp-no");
options.spNos[pair.target] = pair.value;
} else {
throw new Error(`Unsupported argument: ${arg}`);
}
}
if (!["text", "json"].includes(options.format)) {
throw new Error(`Unsupported --format: ${options.format}`);
}
return options;
}
function envValue(env, name) {
return toSafeString(env[name], 500);
}
function buildInputs(options, env = process.env) {
return APPROVAL_POC_TARGETS.map((target) => {
const cliTemplateId = toSafeString(options.templates[target.key], 500);
const cliSpNo = toSafeString(options.spNos[target.key], 500);
const envTemplateId = envValue(env, TEMPLATE_ENV[target.key]);
const envSpNo = envValue(env, SP_NO_ENV[target.key]);
const templateId = cliTemplateId || envTemplateId;
const spNo = cliSpNo || envSpNo;
return {
key: target.key,
label: target.label,
templateId,
templateIdPrefix: maskApprovalIdentifier(templateId),
templateSource: templateId ? (cliTemplateId ? "cli" : TEMPLATE_ENV[target.key]) : null,
templateMissingEnv: templateId ? [] : [TEMPLATE_ENV[target.key]],
spNo,
spNoPrefix: maskApprovalIdentifier(spNo),
spNoSource: spNo ? (cliSpNo ? "cli" : SP_NO_ENV[target.key]) : null,
spNoMissingEnv: spNo ? [] : [SP_NO_ENV[target.key]],
};
});
}
function buildSecretCheck(config) {
const missing = [];
if (!config.wecomCorpId) missing.push("WECOM_CORP_ID");
if (!config.wecomAgentSecret) missing.push("WECOM_AGENT_SECRET");
return {
id: "approval_secret_matrix",
source: "WECOM_AGENT_SECRET",
endpoints: [
"POST /cgi-bin/oa/gettemplatedetail",
"POST /cgi-bin/oa/getapprovaldetail",
],
status: missing.length ? "BLOCKED" : "OK",
missing,
noFallback: [
"WECOM_CONTACT_SECRET",
"WECOM_CHECKIN_SECRET",
"WECOM_HR_ASSISTANT_SECRET",
],
};
}
function summarizeApiResult(result) {
if (!result || typeof result !== "object") return null;
const summary = {};
for (const key of ["errcode", "errmsg", "fieldCount", "fieldTypeCounts", "spStatus", "applyFieldCount", "approvalNodeCount", "notifyerCount", "commentCount"]) {
if (result[key] != null) {
summary[key] = key === "errmsg" ? safeMessage(result[key]) : result[key];
}
}
return Object.keys(summary).length ? summary : null;
}
function buildMissingTemplateCheck(input) {
return {
id: "approval_template_detail",
target: input.key,
label: input.label,
endpoint: "POST /cgi-bin/oa/gettemplatedetail",
source: "WECOM_AGENT_SECRET",
status: "NEEDS_CONFIG",
missingEnv: input.templateMissingEnv,
templateIdPrefix: null,
summary: null,
};
}
function buildMissingApprovalCheck(input) {
return {
id: "approval_detail",
target: input.key,
label: input.label,
endpoint: "POST /cgi-bin/oa/getapprovaldetail",
source: "WECOM_AGENT_SECRET",
status: "NEEDS_CONFIG",
missingEnv: input.spNoMissingEnv,
spNoPrefix: null,
summary: null,
};
}
function buildDryRunTemplateCheck(input) {
return {
id: "approval_template_detail",
target: input.key,
label: input.label,
endpoint: "POST /cgi-bin/oa/gettemplatedetail",
source: "WECOM_AGENT_SECRET",
status: "DRY_RUN_READY",
templateIdPrefix: input.templateIdPrefix,
configuredFrom: input.templateSource,
summary: null,
};
}
function buildDryRunApprovalCheck(input) {
return {
id: "approval_detail",
target: input.key,
label: input.label,
endpoint: "POST /cgi-bin/oa/getapprovaldetail",
source: "WECOM_AGENT_SECRET",
status: "DRY_RUN_READY",
spNoPrefix: input.spNoPrefix,
configuredFrom: input.spNoSource,
summary: null,
};
}
function buildLiveTemplateCheck(input, result) {
return {
id: "approval_template_detail",
target: input.key,
label: input.label,
endpoint: "POST /cgi-bin/oa/gettemplatedetail",
source: result.tokenSource || "WECOM_AGENT_SECRET",
status: result.ok ? "OK" : "BLOCKED",
reason: result.ok ? null : result.code,
message: result.message ? safeMessage(result.message) : null,
templateIdPrefix: input.templateIdPrefix,
configuredFrom: input.templateSource,
summary: result.summary,
response: summarizeApiResult(result.response),
};
}
function buildLiveApprovalCheck(input, result) {
return {
id: "approval_detail",
target: input.key,
label: input.label,
endpoint: "POST /cgi-bin/oa/getapprovaldetail",
source: result.tokenSource || "WECOM_AGENT_SECRET",
status: result.ok ? "OK" : "BLOCKED",
reason: result.ok ? null : result.code,
message: result.message ? safeMessage(result.message) : null,
spNoPrefix: input.spNoPrefix,
configuredFrom: input.spNoSource,
summary: result.summary,
response: summarizeApiResult(result.response),
};
}
async function runTargetChecks({
config,
input,
options,
fetchTemplateDetailImpl = fetchWecomApprovalTemplateDetail,
fetchApprovalDetailImpl = fetchWecomApprovalDetail,
}) {
let template = null;
let approval = null;
if (!input.templateId) {
template = buildMissingTemplateCheck(input);
} else if (!options.live) {
template = buildDryRunTemplateCheck(input);
} else {
template = buildLiveTemplateCheck(input, await fetchTemplateDetailImpl({
config,
templateId: input.templateId,
}));
}
if (!input.spNo) {
approval = buildMissingApprovalCheck(input);
} else if (!options.live) {
approval = buildDryRunApprovalCheck(input);
} else {
approval = buildLiveApprovalCheck(input, await fetchApprovalDetailImpl({
config,
spNo: input.spNo,
}));
}
return { target: input.key, label: input.label, template, approval };
}
function collectConfigGaps(targetChecks) {
const gaps = [];
for (const item of targetChecks) {
for (const check of [item.template, item.approval]) {
if (check?.status === "NEEDS_CONFIG") {
gaps.push({
id: check.id,
target: check.target,
status: check.status,
missingEnv: check.missingEnv || [],
});
}
}
}
return gaps;
}
function collectMainBlockers(secretCheck, targetChecks) {
const blockers = [];
if (secretCheck.status === "BLOCKED") {
blockers.push({
id: secretCheck.id,
reason: "missing_required_wecom_approval_config",
missing: secretCheck.missing,
});
}
for (const item of targetChecks) {
for (const check of [item.template, item.approval]) {
if (check?.status === "BLOCKED") {
blockers.push({
id: check.id,
target: check.target,
reason: check.reason || "wecom_approval_api_blocked",
missing: [],
});
}
}
}
return blockers;
}
function evaluateConclusion({ mainBlockers, configGaps, verificationGaps }) {
if (mainBlockers.length) {
return {
status: "SKIP",
conclusion: "BLOCKED_BY_EXTERNAL_CONFIG",
};
}
if (configGaps.length || verificationGaps.length) {
return {
status: "PARTIAL_APPROVAL_POC_NEEDS_CONFIG",
conclusion: "PARTIAL_APPROVAL_POC_NEEDS_CONFIG",
};
}
return {
status: "PASS_APPROVAL_POC",
conclusion: "PASS_APPROVAL_POC",
};
}
async function executeApprovalPoc(options, deps = {}) {
const env = deps.env || process.env;
const generatedAt = deps.generatedAt || new Date().toISOString();
const config = deps.config || getConfig();
const inputs = buildInputs(options, env);
const secretCheck = buildSecretCheck(config);
const targetChecks = [];
for (const input of inputs) {
targetChecks.push(await runTargetChecks({
config,
input,
options,
fetchTemplateDetailImpl: deps.fetchTemplateDetailImpl,
fetchApprovalDetailImpl: deps.fetchApprovalDetailImpl,
}));
}
const configGaps = collectConfigGaps(targetChecks);
const verificationGaps = options.live ? [] : [{
id: "live_read",
status: "DRY_RUN",
reason: "run_with_--live_for_public_readonly_gate",
}];
const mainBlockers = collectMainBlockers(secretCheck, targetChecks);
const conclusion = evaluateConclusion({ mainBlockers, configGaps, verificationGaps });
return {
mode: "qa_only",
generatedAt,
status: conclusion.status,
conclusion: conclusion.conclusion,
privacy: REPORT_PRIVACY,
scope: {
targets: APPROVAL_POC_TARGETS.map((item) => item.key),
writeBehavior: "no OA writes, no WeCom writes, no approval callbacks, no mirror import",
liveRead: Boolean(options.live),
deepMirror: "not_started",
},
inputs: {
templates: Object.fromEntries(inputs.map((item) => [item.key, {
configured: Boolean(item.templateId),
configuredFrom: item.templateSource,
templateIdPrefix: item.templateIdPrefix,
}])),
spNos: Object.fromEntries(inputs.map((item) => [item.key, {
configured: Boolean(item.spNo),
configuredFrom: item.spNoSource,
spNoPrefix: item.spNoPrefix,
}])),
},
checks: {
secretMatrix: secretCheck,
targets: targetChecks,
},
configGaps,
verificationGaps,
mainBlockers,
};
}
function ensureReportDir(reportDir) {
fs.mkdirSync(reportDir, { recursive: true });
}
function writeReport(reportDir, report) {
ensureReportDir(reportDir);
const filePath = path.join(reportDir, "summary.json");
fs.writeFileSync(filePath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
return filePath;
}
function printText(report, reportPath) {
process.stdout.write(`[wecom-approval-poc] status=${report.status} conclusion=${report.conclusion}\n`);
process.stdout.write(`[wecom-approval-poc] report=${reportPath}\n`);
process.stdout.write(`[wecom-approval-poc] privacy=${report.privacy}\n`);
process.stdout.write(`[wecom-approval-poc] liveRead=${report.scope.liveRead} targets=${report.scope.targets.join(",")}\n`);
if (report.configGaps?.length) {
process.stdout.write(`[wecom-approval-poc] configGaps=${JSON.stringify(report.configGaps)}\n`);
}
if (report.mainBlockers?.length) {
process.stdout.write(`[wecom-approval-poc] mainBlockers=${JSON.stringify(report.mainBlockers)}\n`);
}
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
return;
}
const report = await executeApprovalPoc(options);
const reportPath = writeReport(options.reportDir, report);
if (options.format === "json") {
process.stdout.write(`${JSON.stringify({ ...report, reportPath }, null, 2)}\n`);
} else {
printText(report, reportPath);
}
}
if (require.main === module) {
main().catch((error) => {
process.stderr.write(`[wecom-approval-poc][FAIL] ${safeMessage(error?.message || error)}\n`);
process.exitCode = 1;
});
}
module.exports = {
buildInputs,
buildSecretCheck,
evaluateConclusion,
executeApprovalPoc,
parseArgs,
summarizeApiResult,
};
@@ -0,0 +1,345 @@
const { getWecomAppAccessToken, sanitizeWecomResponse } = require("./wecomAccessToken");
const WECOM_API_BASE = "https://qyapi.weixin.qq.com";
const APPROVAL_POC_TARGETS = Object.freeze([
{ key: "leave", label: "请假" },
{ key: "missed_punch", label: "补卡" },
]);
class WecomApprovalPocError extends Error {
constructor(code, message, details = {}) {
super(message);
this.name = "WecomApprovalPocError";
this.code = code;
this.details = details;
}
}
function toSafeString(value, maxLength = 500) {
if (value == null) return "";
return String(value).trim().slice(0, maxLength);
}
function sanitizeErrorMessage(value) {
return toSafeString(value, 500)
.replace(/access_token=[^&\s]+/gi, "access_token=***")
.replace(/[A-Za-z0-9_-]{24,}/g, "***")
.slice(0, 240);
}
function maskApprovalIdentifier(value) {
const raw = toSafeString(value, 160);
if (!raw) return null;
if (raw.length <= 6) return `${raw.slice(0, 2)}***`;
return `${raw.slice(0, Math.min(8, raw.length))}***`;
}
function countBy(items, selector) {
const counts = {};
for (const item of items) {
const key = toSafeString(selector(item), 80) || "unknown";
counts[key] = (counts[key] || 0) + 1;
}
return counts;
}
function normalizeControls(value) {
if (!value) return [];
if (Array.isArray(value)) return value.filter((item) => item && typeof item === "object");
if (typeof value === "object") {
for (const key of ["controls", "contents", "items", "children"]) {
if (Array.isArray(value[key])) return normalizeControls(value[key]);
}
}
return [];
}
function controlType(control) {
if (!control || typeof control !== "object") return "unknown";
return (
control.control
|| control.control_type
|| control.controlType
|| control.type
|| control.property?.control
|| control.property?.control_type
|| control.property?.type
|| "unknown"
);
}
function summarizeTemplatePayload(payload) {
const templateId = payload?.template_id ?? payload?.templateId;
const controls = normalizeControls(payload?.template_content?.controls)
.concat(normalizeControls(payload?.template_content?.contents))
.concat(normalizeControls(payload?.controls));
const dedupedControls = controls.length ? controls : normalizeControls(payload?.template_content);
return {
templateIdPrefix: maskApprovalIdentifier(templateId),
fieldCount: dedupedControls.length,
fieldTypeCounts: countBy(dedupedControls, controlType),
hasTemplateNames: Array.isArray(payload?.template_names) && payload.template_names.length > 0,
};
}
function summarizeApprovalPayload(payload) {
const info = payload?.info && typeof payload.info === "object" ? payload.info : {};
const applyData = info.apply_data && typeof info.apply_data === "object" ? info.apply_data : {};
const contents = Array.isArray(applyData.contents) ? applyData.contents : [];
const records = Array.isArray(info.sp_record) ? info.sp_record : [];
const notifyers = Array.isArray(info.notifyer) ? info.notifyer : [];
const comments = Array.isArray(info.comments) ? info.comments : [];
return {
spNoPrefix: maskApprovalIdentifier(info.sp_no ?? payload?.sp_no),
templateIdPrefix: maskApprovalIdentifier(info.template_id),
spStatus: info.sp_status == null ? null : toSafeString(info.sp_status, 40),
applyFieldCount: contents.length,
applyFieldTypeCounts: countBy(contents, controlType),
approvalNodeCount: records.length,
notifyerCount: notifyers.length,
commentCount: comments.length,
};
}
function sanitizeApprovalApiResponse(payload) {
if (!payload || typeof payload !== "object") return null;
const result = sanitizeWecomResponse(payload) ?? {};
if (payload.template_content) {
const summary = summarizeTemplatePayload(payload);
result.fieldCount = summary.fieldCount;
result.fieldTypeCounts = summary.fieldTypeCounts;
}
if (payload.info) {
const summary = summarizeApprovalPayload(payload);
result.spStatus = summary.spStatus;
result.applyFieldCount = summary.applyFieldCount;
result.approvalNodeCount = summary.approvalNodeCount;
result.notifyerCount = summary.notifyerCount;
result.commentCount = summary.commentCount;
}
return Object.keys(result).length ? result : null;
}
function classifyWecomApprovalResult(result, fallbackCode = "wecom_approval_api_error") {
if (result?.networkError) {
return {
code: result.errorMessage === "timeout" ? "wecom_approval_timeout" : "wecom_approval_network_error",
message: result.errorMessage || "WeCom approval API network error",
response: null,
};
}
const errcode = Number(result?.payload?.errcode);
if (Number.isFinite(errcode) && errcode !== 0) {
return {
code: "wecom_approval_api_error",
message: `WeCom approval API errcode ${errcode}`,
response: sanitizeApprovalApiResponse(result.payload),
};
}
return {
code: result?.httpStatus ? `wecom_approval_http_${result.httpStatus}` : fallbackCode,
message: "WeCom approval API failed",
response: sanitizeApprovalApiResponse(result?.payload),
};
}
async function fetchWecomApprovalJson(pathName, accessToken, body, options = {}) {
const url = new URL(`${WECOM_API_BASE}${pathName}`);
url.searchParams.set("access_token", accessToken);
const timeoutMs = Math.max(500, Number(options.timeoutMs ?? 10000));
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await (options.fetchImpl ?? fetch)(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body ?? {}),
signal: controller.signal,
});
const payload = await response.json().catch(() => null);
const errcode = payload && typeof payload === "object" ? Number(payload.errcode) : NaN;
return {
ok: response.ok && Number.isFinite(errcode) && errcode === 0,
httpStatus: response.status,
payload,
};
} catch (error) {
return {
ok: false,
httpStatus: null,
payload: null,
networkError: true,
errorMessage: error?.name === "AbortError" ? "timeout" : sanitizeErrorMessage(error?.message ?? error),
};
} finally {
clearTimeout(timer);
}
}
async function getApprovalAccessToken({ config, fetchImpl, timeoutMs }) {
const tokenResult = await getWecomAppAccessToken({
config: {
...config,
wecomRequireAgentId: false,
},
timeoutMs,
fetchImpl,
});
if (!tokenResult.ok) {
throw new WecomApprovalPocError(tokenResult.code, tokenResult.message || "WeCom app token unavailable", {
response: tokenResult.response,
});
}
return {
accessToken: tokenResult.accessToken,
tokenSource: "WECOM_AGENT_SECRET",
response: tokenResult.response,
};
}
async function fetchWecomApprovalTemplateDetail({
config,
templateId,
fetchImpl = fetch,
timeoutMs = config?.wecomRequestTimeoutMs ?? 10000,
}) {
const resolvedTemplateId = toSafeString(templateId, 160);
if (!resolvedTemplateId) {
return {
ok: false,
skipped: true,
retryable: false,
code: "approval_template_id_missing",
message: "Missing approval template id",
tokenSource: "WECOM_AGENT_SECRET",
response: null,
summary: null,
};
}
try {
const token = await getApprovalAccessToken({ config, fetchImpl, timeoutMs });
const result = await fetchWecomApprovalJson("/cgi-bin/oa/gettemplatedetail", token.accessToken, {
template_id: resolvedTemplateId,
}, { fetchImpl, timeoutMs });
if (!result.ok) {
const error = classifyWecomApprovalResult(result, "wecom_approval_template_detail_failed");
return {
ok: false,
skipped: false,
retryable: false,
code: error.code,
message: error.message,
tokenSource: token.tokenSource,
response: error.response,
summary: null,
};
}
return {
ok: true,
skipped: false,
retryable: false,
code: "wecom_approval_template_detail_fetched",
message: null,
tokenSource: token.tokenSource,
response: sanitizeApprovalApiResponse(result.payload),
summary: summarizeTemplatePayload(result.payload),
};
} catch (error) {
return {
ok: false,
skipped: false,
retryable: Boolean(error?.details?.retryable),
code: error?.code || "wecom_approval_template_detail_failed",
message: sanitizeErrorMessage(error?.message || error),
tokenSource: "WECOM_AGENT_SECRET",
response: error?.details?.response || null,
summary: null,
};
}
}
async function fetchWecomApprovalDetail({
config,
spNo,
fetchImpl = fetch,
timeoutMs = config?.wecomRequestTimeoutMs ?? 10000,
}) {
const resolvedSpNo = toSafeString(spNo, 160);
if (!resolvedSpNo) {
return {
ok: false,
skipped: true,
retryable: false,
code: "approval_sp_no_missing",
message: "Missing approval sp_no",
tokenSource: "WECOM_AGENT_SECRET",
response: null,
summary: null,
};
}
try {
const token = await getApprovalAccessToken({ config, fetchImpl, timeoutMs });
const result = await fetchWecomApprovalJson("/cgi-bin/oa/getapprovaldetail", token.accessToken, {
sp_no: resolvedSpNo,
}, { fetchImpl, timeoutMs });
if (!result.ok) {
const error = classifyWecomApprovalResult(result, "wecom_approval_detail_failed");
return {
ok: false,
skipped: false,
retryable: false,
code: error.code,
message: error.message,
tokenSource: token.tokenSource,
response: error.response,
summary: null,
};
}
return {
ok: true,
skipped: false,
retryable: false,
code: "wecom_approval_detail_fetched",
message: null,
tokenSource: token.tokenSource,
response: sanitizeApprovalApiResponse(result.payload),
summary: summarizeApprovalPayload(result.payload),
};
} catch (error) {
return {
ok: false,
skipped: false,
retryable: Boolean(error?.details?.retryable),
code: error?.code || "wecom_approval_detail_failed",
message: sanitizeErrorMessage(error?.message || error),
tokenSource: "WECOM_AGENT_SECRET",
response: error?.details?.response || null,
summary: null,
};
}
}
module.exports = {
APPROVAL_POC_TARGETS,
WecomApprovalPocError,
fetchWecomApprovalDetail,
fetchWecomApprovalTemplateDetail,
maskApprovalIdentifier,
sanitizeApprovalApiResponse,
summarizeApprovalPayload,
summarizeTemplatePayload,
};