fix: list unadapted XFT approval tasks safely
This commit is contained in:
+62
-7
@@ -7,6 +7,7 @@ import { ApprovalService, type ApprovalHistoryItem, type ProcessApprovalItem, ty
|
||||
import type { ApprovalHistoryRange, ApprovalHistoryScope, AuthenticatedDingTalkUser, XftApprovalProvider, XftHistoryItem, XftSubmissionField, XftSubmissionKind, XftSubmissionOptionKind, XftSubmissionTemplate, XftTodoItem } from "./xft.js";
|
||||
import { browseRawDetail, compactApprovalDetail, type ApprovalDetailOptions, type ApprovalDetailView } from "./approval-detail.js";
|
||||
import { sensitiveLabel } from "./approval-metadata.js";
|
||||
import { isRecord } from "./json.js";
|
||||
import type { InvoiceDeduplicationService } from "./invoice-dedup.js";
|
||||
|
||||
export type UnifiedSource = "dingtalk" | "xft";
|
||||
@@ -235,7 +236,12 @@ export class UnifiedApprovalService {
|
||||
if (!this.xft) sourceStatus.xft = { status: "disabled" };
|
||||
else tasks.push(this.xft.listMyTodos(user, limit, query).then((result) => {
|
||||
xftItems = result.items.map((item) => this.normalizeXftTodo(user, item));
|
||||
sourceStatus.xft = { status: "ok", count: result.items.length };
|
||||
sourceStatus.xft = {
|
||||
status: "ok",
|
||||
count: result.items.length,
|
||||
...(typeof result.diagnostics?.billApiCount === "number" ? { billApiCount: result.diagnostics.billApiCount } : {}),
|
||||
...(typeof result.diagnostics?.clientOnlyCount === "number" ? { clientOnlyCount: result.diagnostics.clientOnlyCount } : {}),
|
||||
};
|
||||
}).catch(async (error) => {
|
||||
if (error instanceof XftSessionExpiredError && this.xftChallenges) {
|
||||
try { sourceStatus.xft = { status: "auth_required", authChallenge: await this.xftChallenges.getOrStart(user) }; }
|
||||
@@ -251,10 +257,15 @@ export class UnifiedApprovalService {
|
||||
: interleave(dingTalkItems, xftItems);
|
||||
const returnedItems = items.slice(0, limit);
|
||||
const sourcesHealthy = requestedSourcesHealthy(source, sourceStatus);
|
||||
const containsClientOnly = returnedItems.some((item) => isRecord(item)
|
||||
&& item.source === "xft"
|
||||
&& isRecord(item.manualAction)
|
||||
&& item.manualAction.reason === "unsupported_xft_task_type");
|
||||
const selectionUnavailableReason = !issueSelection ? "selection_not_requested"
|
||||
: returnedItems.length === 0 ? "empty_result"
|
||||
: !sourcesHealthy ? "requested_source_unhealthy"
|
||||
: returnedItems.length > APPROVAL_BATCH_LIMIT ? "result_exceeds_batch_limit"
|
||||
: containsClientOnly ? "contains_client_only_item"
|
||||
: undefined;
|
||||
const selectionAvailable = selectionUnavailableReason === undefined;
|
||||
return {
|
||||
@@ -710,6 +721,20 @@ export class UnifiedApprovalService {
|
||||
const ref = this.decodeApproval(user, approvalId);
|
||||
const view = options.view ?? "summary";
|
||||
if (ref.provider === "xft") {
|
||||
if (ref.clientOnly) {
|
||||
return {
|
||||
approvalId,
|
||||
source: "xft" as const,
|
||||
sourceType: "unadapted_xft_task",
|
||||
view,
|
||||
status: "client_required",
|
||||
detailCapability: {
|
||||
status: "client_only",
|
||||
message: "该薪福通待办已在列表中显示,但尚未确认正式详情接口兼容性,请在薪福通客户端查看详情",
|
||||
},
|
||||
diagnostics: { realtime: true, persistedBusinessData: false, upstreamDetailRequested: false },
|
||||
};
|
||||
}
|
||||
if (!this.xft || !ref.billId) throw new ApprovalSafetyError("薪福通审批源未启用");
|
||||
try {
|
||||
const raw = redactSensitive(ref.historyScope
|
||||
@@ -772,6 +797,18 @@ export class UnifiedApprovalService {
|
||||
};
|
||||
}
|
||||
if (ref.provider === "official_oa") return this.getOfficialOaActions(user, approvalId, ref);
|
||||
if (ref.clientOnly) {
|
||||
const message = "该薪福通待办已列出,但尚未确认正式审批接口兼容性,请在薪福通客户端处理";
|
||||
return {
|
||||
approvalId,
|
||||
actions: [
|
||||
{ action: "approve" as const, allowed: false, clientRequired: true, unavailableReason: "provider_not_supported", message },
|
||||
{ action: "reject" as const, allowed: false, clientRequired: true, unavailableReason: "provider_not_supported", message },
|
||||
{ action: "return" as const, allowed: false, clientRequired: true, unavailableReason: "provider_not_supported", message },
|
||||
],
|
||||
manualAction: { required: true, reason: "unsupported_xft_task_type", message },
|
||||
};
|
||||
}
|
||||
if (!this.xft || !ref.billId) throw new ApprovalSafetyError("薪福通审批源未启用");
|
||||
let returned;
|
||||
try { returned = await this.xft.listReturnNodes(user, ref.billId); }
|
||||
@@ -1060,6 +1097,12 @@ export class UnifiedApprovalService {
|
||||
if (ref.historyScope) {
|
||||
throw new ApprovalReferenceValidationError("history_reference_read_only", "历史审批引用仅用于读取,不能提交审批动作");
|
||||
}
|
||||
if (ref.provider === "xft" && ref.clientOnly) {
|
||||
throw new ApprovalReferenceValidationError(
|
||||
"provider_not_supported",
|
||||
"该薪福通待办尚未确认正式审批接口兼容性,不能通过 MCP 处理,请在薪福通客户端完成",
|
||||
);
|
||||
}
|
||||
if (ref.provider === "xft" && action === "approve" && ref.allowApprove === false) {
|
||||
throw new ApprovalReferenceValidationError("action_not_allowed", "该薪福通待办当前不允许同意");
|
||||
}
|
||||
@@ -1194,19 +1237,24 @@ export class UnifiedApprovalService {
|
||||
}
|
||||
|
||||
private normalizeXftTodo(user: AuthenticatedDingTalkUser, item: XftTodoItem) {
|
||||
const billApiSupported = item.capability !== "list_only" && Boolean(item.billId);
|
||||
const referenceId = billApiSupported ? item.billId : item.taskId;
|
||||
if (!referenceId) throw new ApprovalSafetyError("薪福通待办缺少安全引用标识");
|
||||
const ref: ApprovalReference = {
|
||||
kind: "approval",
|
||||
userId: user.userId,
|
||||
provider: "xft",
|
||||
billId: item.billId,
|
||||
...(item.allowPass !== undefined ? { allowApprove: item.allowPass } : {}),
|
||||
...(item.allowReject !== undefined ? { allowReject: item.allowReject } : {}),
|
||||
billId: billApiSupported ? item.billId : `task:${referenceId}`,
|
||||
...(billApiSupported ? {} : { clientOnly: true }),
|
||||
...(billApiSupported && item.allowPass !== undefined ? { allowApprove: item.allowPass } : { allowApprove: false }),
|
||||
...(billApiSupported && item.allowReject !== undefined ? { allowReject: item.allowReject } : { allowReject: false }),
|
||||
exp: expiresInMinutes(30),
|
||||
};
|
||||
const clientOnlyMessage = "该薪福通待办已列出,但尚未确认正式详情和审批接口兼容性,请在薪福通客户端查看和处理";
|
||||
return {
|
||||
approvalId: this.signPortableApprovalReference(ref),
|
||||
source: "xft",
|
||||
businessNo: item.billId,
|
||||
...(billApiSupported ? { businessNo: item.billId } : {}),
|
||||
title: item.title ?? "薪福通审批",
|
||||
applicant: {
|
||||
...(item.applicantName ? { name: item.applicantName } : {}),
|
||||
@@ -1215,11 +1263,18 @@ export class UnifiedApprovalService {
|
||||
...(item.applyTime ? { submittedAt: localTime(item.applyTime) } : {}),
|
||||
...(item.amount !== undefined ? { amount: { value: item.amount, currency: item.currency ?? "CNY" } } : {}),
|
||||
...(item.reason ? { summary: item.reason } : {}),
|
||||
availableActions: [
|
||||
availableActions: billApiSupported ? [
|
||||
...(item.allowPass === false ? [] : ["approve"]),
|
||||
...(item.allowReject === false ? [] : ["reject"]),
|
||||
"return",
|
||||
],
|
||||
] : [],
|
||||
...(billApiSupported ? {} : {
|
||||
manualAction: {
|
||||
required: true,
|
||||
reason: "unsupported_xft_task_type",
|
||||
message: clientOnlyMessage,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user