fix: list unadapted XFT approval tasks safely
This commit is contained in:
+2
-2
@@ -433,7 +433,7 @@ function parseEndpoint(argv: string[]): string {
|
||||
|
||||
export function createBridgeMcpServer(bridge: RemoteBridge): McpServer {
|
||||
const server = new McpServer({ name: "dingtalk-approval-stdio-bridge", version: APPLICATION_VERSION }, {
|
||||
instructions: "首次使用或登录链接过期时调用approval_login;每次调用会检查登录并在需要时生成有效的新链接,绝不因链接过期而重新安装。工具不会通过其他消息通道自动发送链接,必须把content.text中的登录URL原样写入当前面向用户的回复,不得只声称已生成或已推送。审批引用、调用容量和内部分批不得向用户解释;查看时只回答业务信息,用户明确授权后在授权范围内自动处理。写前校验失败时零写入,任何不确定结果都不得自动重试。提单草稿仅可保存在 Agent 所在机器的当前用户私有目录,不得上传网关;恢复草稿后必须重新读取实时模板 Schema,短期提交引用不得写入草稿。薪福通所有选择型字段只向用户展示业务名称:optionSource 选项展示label,内嵌枚举展示enumLabels;用户选定label后,values中必须填同一项的code而不能填label,code只存在于工具调用内部。不得向用户索取或展示城市码、组织码、工号、档案编号、数字舱位值或其他内部ID;selectionCapability=client_only时停止构造该必填项或允许跳过该可选项。",
|
||||
instructions: "首次使用或登录链接过期时调用approval_login;每次调用会检查登录并在需要时生成有效的新链接,绝不因链接过期而重新安装。工具不会通过其他消息通道自动发送链接,必须把content.text中的登录URL原样写入当前面向用户的回复,不得只声称已生成或已推送。审批引用、调用容量和内部分批不得向用户解释;查看时只回答业务信息,用户明确授权后在授权范围内自动处理。薪福通待办不按单据类型过滤;availableActions=[]且manualAction.reason=unsupported_xft_task_type的项目仍要展示,但只能引导薪福通客户端查看处理,禁止尝试MCP写入。写前校验失败时零写入,任何不确定结果都不得自动重试。提单草稿仅可保存在 Agent 所在机器的当前用户私有目录,不得上传网关;恢复草稿后必须重新读取实时模板 Schema,短期提交引用不得写入草稿。薪福通所有选择型字段只向用户展示业务名称:optionSource 选项展示label,内嵌枚举展示enumLabels;用户选定label后,values中必须填同一项的code而不能填label,code只存在于工具调用内部。不得向用户索取或展示城市码、组织码、工号、档案编号、数字舱位值或其他内部ID;selectionCapability=client_only时停止构造该必填项或允许跳过该可选项。",
|
||||
});
|
||||
server.registerTool("approval_login", {
|
||||
title: "登录审批服务", description: "检查登录;未登录或旧链接已过期时生成新的钉钉授权链接。同一身份同时用于钉钉OA和薪福通,无需重新安装。", inputSchema: {},
|
||||
@@ -443,7 +443,7 @@ export function createBridgeMcpServer(bridge: RemoteBridge): McpServer {
|
||||
return bridgeAuthToolResult(result);
|
||||
});
|
||||
server.registerTool("approval_list_todos", {
|
||||
title: "列出我的审批待办", description: "实时列出当前登录用户本人的钉钉OA和薪福通待办。", inputSchema: {
|
||||
title: "列出我的审批待办", description: "实时列出当前登录用户本人的钉钉OA和薪福通待办;薪福通不按单据类型过滤,未适配项目会明确标记为仅可在薪福通客户端查看处理。", inputSchema: {
|
||||
source: z.enum(["all", "dingtalk", "xft"]).default("all"), query: z.string().max(200).default(""),
|
||||
limit: z.number().int().min(1).max(200).default(50),
|
||||
}, annotations: { readOnlyHint: true, destructiveHint: false },
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ const MCP_SESSION_MAX_PER_USER = 8;
|
||||
const MCP_SESSION_MAX_TOTAL = 1000;
|
||||
|
||||
export const APPROVAL_MCP_INSTRUCTIONS = [
|
||||
"先调用 approval_list_todos 获取当前待办。用户只是在查看时,仅回答标题、申请人、金额、时间等业务信息,不主动讨论审批接口、批处理方式或索取写操作授权。用户明确要求处理后,在其授权范围内内部选择整组引用或自动分批;技术分批不得造成重复确认。",
|
||||
"先调用 approval_list_todos 获取当前待办。薪福通待办不按单据类型过滤;availableActions=[] 且 manualAction.reason=unsupported_xft_task_type 的项目仍要展示给用户,但只能引导到薪福通客户端查看和处理,禁止尝试 MCP 写入。用户只是在查看时,仅回答标题、申请人、金额、时间等业务信息,不主动讨论审批接口、批处理方式或索取写操作授权。用户明确要求处理后,在其授权范围内内部选择整组引用或自动分批;技术分批不得造成重复确认。",
|
||||
"approvalId、selectionId、targetId、单次条数限制、分批方式以及内部引用是否返回均属于 Agent 与 MCP 的内部调用细节,绝不向用户展示、解释或要求用户操作。面向用户只说明目标审批、业务动作、成功结果和需要用户决策的业务失败;不要说引用是否签发、是否失效、是否存在一键全选或如何分组。",
|
||||
"查询本人处理过或本人发起的审批时调用 approval_list_history;默认最近30天,可用 startDate/endDate 指定最长120天的时间窗口。用其只读 approvalId 获取详情;历史引用不能用于 approval_get_actions 或 approval_process_batch,也不会返回 selectionId。",
|
||||
"费用复核优先调用 approval_get_detail(view=expense);summary 用于一般详情,raw 仅用于分页补充字段。",
|
||||
@@ -67,7 +67,7 @@ export function createApprovalMcpServer(
|
||||
|
||||
server.registerTool("approval_list_todos", {
|
||||
title: "列出我的审批待办",
|
||||
description: "审批操作的入口。通过同一登录身份实时列出当前用户在钉钉OA和薪福通中的待办,并取得后续接口必须使用的approvalId;列表尽可能直接包含金额和带时区的提交时间。",
|
||||
description: "审批操作的入口。通过同一登录身份实时列出当前用户在钉钉OA和薪福通中的待办;薪福通不按单据类型静默过滤。已确认正式接口兼容的项目取得可用approvalId,未适配项目也会显示但以availableActions=[]和manualAction明确要求到薪福通客户端查看处理。列表尽可能直接包含金额和带时区的提交时间。",
|
||||
inputSchema: {
|
||||
source: z.enum(["all", "dingtalk", "xft"]).default("all"),
|
||||
query: z.string().max(200).default(""),
|
||||
|
||||
+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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+84
-20
@@ -124,8 +124,9 @@ export class XftOpenApiClient implements XftApprovalProvider {
|
||||
const scanLimit = hasQuery ? 200 : Math.min(200, Math.max(limit, 1));
|
||||
const { items, totalCount, truncated } = await this.loadCurrentTodos(identity, scanLimit);
|
||||
const filtered = items.filter((item) => matchesXftTodoQuery(item, search));
|
||||
const returnedItems = filtered.slice(0, limit);
|
||||
return {
|
||||
items: filtered.slice(0, limit).map(stripRaw),
|
||||
items: returnedItems.map(stripRaw),
|
||||
totalCount: hasQuery ? filtered.length : totalCount,
|
||||
identity: {
|
||||
enterpriseUserId: identity.enterpriseUserId,
|
||||
@@ -135,6 +136,8 @@ export class XftOpenApiClient implements XftApprovalProvider {
|
||||
source: "xft_official_openapi",
|
||||
realtime: true,
|
||||
identityMapping: this.api.dingTalkAppCode,
|
||||
billApiCount: returnedItems.filter((item) => item.capability !== "list_only").length,
|
||||
clientOnlyCount: returnedItems.filter((item) => item.capability === "list_only").length,
|
||||
...(truncated ? { truncated: true, scanLimit } : {}),
|
||||
persistedBusinessData: false,
|
||||
},
|
||||
@@ -818,8 +821,9 @@ export class XftOpenApiClient implements XftApprovalProvider {
|
||||
for (let currentPage = 1; currentPage <= 100 && items.length < limit; currentPage += 1) {
|
||||
const page = await this.loadTodoPage(identity, currentPage, pageSize, billId);
|
||||
for (const item of page.items) {
|
||||
if (seen.has(item.billId)) continue;
|
||||
seen.add(item.billId);
|
||||
const identity = item.billId ? `bill:${item.billId}` : item.taskId ? `task:${item.taskId}` : undefined;
|
||||
if (!identity || seen.has(identity)) continue;
|
||||
seen.add(identity);
|
||||
items.push(item);
|
||||
if (items.length >= limit) break;
|
||||
}
|
||||
@@ -836,6 +840,7 @@ export class XftOpenApiClient implements XftApprovalProvider {
|
||||
for (let currentPage = 1; currentPage <= 100 && items.length < limit; currentPage += 1) {
|
||||
const page = await this.loadTaskPage(identity, currentPage, pageSize, "done");
|
||||
for (const item of page.items) {
|
||||
if (!item.billId || item.capability === "list_only") continue;
|
||||
if (seen.has(item.billId)) continue;
|
||||
seen.add(item.billId);
|
||||
items.push(toHistoryItem(item, "handled"));
|
||||
@@ -982,28 +987,51 @@ export class XftOpenApiClient implements XftApprovalProvider {
|
||||
const detailed = objectRecords(detailResult);
|
||||
const byTaskId = new Map(detailed.map((record) => [stringAt(record, ["todoTaskId"]) ?? "", record]));
|
||||
const todoRecords = pageRecords.map((record) => ({ ...record, ...(byTaskId.get(stringAt(record, ["todoTaskId"]) ?? "") ?? {}) }));
|
||||
const relevant = todoRecords.filter((record) => isSmartExpenseTodo(record));
|
||||
const byBillTodo = new Map<string, JsonObject>();
|
||||
for (const record of relevant) {
|
||||
const id = billIdFromTodo(record);
|
||||
if (id) byBillTodo.set(id, record);
|
||||
const selectedRecords = billId
|
||||
? todoRecords.filter((record) => billIdFromTodo(record) === billId)
|
||||
: todoRecords;
|
||||
const knownBillIds = uniqueStrings(selectedRecords
|
||||
.filter((record) => isSmartExpenseTodo(record))
|
||||
.flatMap((record) => billIdFromTodo(record) ?? []));
|
||||
const probeBillIds = uniqueStrings(selectedRecords
|
||||
.filter((record) => !isSmartExpenseTodo(record))
|
||||
.flatMap((record) => billIdFromTodo(record) ?? []));
|
||||
const knownSummaries = knownBillIds.length ? await this.loadBillSummaries(identity, knownBillIds) : [];
|
||||
let probedSummaries: JsonObject[] = [];
|
||||
let billProbeFailed = false;
|
||||
if (probeBillIds.length) {
|
||||
try { probedSummaries = await this.loadBillSummaries(identity, probeBillIds); }
|
||||
catch { billProbeFailed = true; }
|
||||
}
|
||||
const selectedIds = billId ? [...byBillTodo.keys()].filter((id) => id === billId) : [...byBillTodo.keys()];
|
||||
if (selectedIds.length === 0) return { items: [] as CurrentTodo[], pageRecordCount: pageRecords.length };
|
||||
const summaries = await this.request(
|
||||
"/itrip/xft-api/v1/bills/bill-list",
|
||||
{ billIds: selectedIds.map(businessIdValue), start: 0, limit: selectedIds.length },
|
||||
"POST",
|
||||
identity,
|
||||
);
|
||||
const byId = new Map(findObjectArrays(summaries).flat().flatMap((item) => {
|
||||
const byId = new Map([...knownSummaries, ...probedSummaries].flatMap((item) => {
|
||||
const id = stringAt(item, ["billId", "businessId", "id"]);
|
||||
return id ? [[id, item] as const] : [];
|
||||
}));
|
||||
const items = selectedIds.map((id) => normalizeTodo(id, { ...(byBillTodo.get(id) ?? {}), ...(byId.get(id) ?? {}) }));
|
||||
const items = selectedRecords.flatMap((record) => {
|
||||
const id = billIdFromTodo(record);
|
||||
const summary = id ? byId.get(id) : undefined;
|
||||
if (id && (isSmartExpenseTodo(record) || summary)) {
|
||||
return [normalizeTodo(id, { ...record, ...(summary ?? {}) })];
|
||||
}
|
||||
if (billId) return [];
|
||||
const unsupported = normalizeListOnlyTodo(record, id
|
||||
? billProbeFailed ? "bill_lookup_unavailable" : "unsupported_task_type"
|
||||
: "missing_bill_identifier");
|
||||
return unsupported ? [unsupported] : [];
|
||||
});
|
||||
return { items, pageRecordCount: pageRecords.length };
|
||||
}
|
||||
|
||||
private async loadBillSummaries(identity: XftIdentity, billIds: string[]): Promise<JsonObject[]> {
|
||||
const summaries = await this.request(
|
||||
"/itrip/xft-api/v1/bills/bill-list",
|
||||
{ billIds: billIds.map(businessIdValue), start: 0, limit: billIds.length },
|
||||
"POST",
|
||||
identity,
|
||||
);
|
||||
return findObjectArrays(summaries).flat();
|
||||
}
|
||||
|
||||
private async findCurrentTodo(identity: XftIdentity, billId: string): Promise<CurrentTodo | undefined> {
|
||||
const pageSize = 1000;
|
||||
for (let currentPage = 1; currentPage <= 100; currentPage += 1) {
|
||||
@@ -1026,7 +1054,9 @@ export class XftOpenApiClient implements XftApprovalProvider {
|
||||
const pageSize = 1000;
|
||||
for (let currentPage = 1; currentPage <= 100; currentPage += 1) {
|
||||
const page = await this.loadTodoPage(identity, currentPage, pageSize);
|
||||
for (const item of page.items) if (billIds.has(item.billId)) found.add(item.billId);
|
||||
for (const item of page.items) {
|
||||
if (item.billId && billIds.has(item.billId)) found.add(item.billId);
|
||||
}
|
||||
if (found.size === billIds.size || page.pageRecordCount < pageSize) return found;
|
||||
}
|
||||
throw new ApprovalSafetyError("薪福通待办超过安全分页上限,无法确认单据归属");
|
||||
@@ -1258,6 +1288,7 @@ function normalizeTodo(billId: string, raw: JsonObject): CurrentTodo {
|
||||
return {
|
||||
source: "xft",
|
||||
billId,
|
||||
capability: "bill_api",
|
||||
taskId: stringAt(raw, ["processTaskId", "todoTaskId"]),
|
||||
eventType: stringAt(raw, ["eventType", "billType", "eventCode"]),
|
||||
title: stringAt(raw, ["billName", "title", "eventName", "eventNam", "billTypeName"]) ?? parsedContent.title,
|
||||
@@ -1273,11 +1304,35 @@ function normalizeTodo(billId: string, raw: JsonObject): CurrentTodo {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeListOnlyTodo(
|
||||
raw: JsonObject,
|
||||
unavailableReason: NonNullable<XftTodoItem["unavailableReason"]>,
|
||||
): CurrentTodo | undefined {
|
||||
const taskId = stringAt(raw, ["todoTaskId", "businessId", "groupId", "REFNBR", "referenceNumber"]);
|
||||
if (!taskId) return undefined;
|
||||
const parsedContent = parseTodoContent(stringAt(raw, ["content"]));
|
||||
return {
|
||||
source: "xft",
|
||||
taskId,
|
||||
capability: "list_only",
|
||||
unavailableReason,
|
||||
title: stringAt(raw, ["billName", "title", "eventName", "eventNam", "billTypeName"]) ?? parsedContent.title ?? "薪福通待办",
|
||||
applicantName: stringAt(raw, ["applyUserName", "applicantName", "aplNam", "empName", "applyEmpName"]) ?? parsedContent.applicantName,
|
||||
applicantDepartment: stringAt(raw, ["departmentName", "deptName", "aplDepNam", "applyDeptName"]) ?? parsedContent.department,
|
||||
applyTime: stringAt(raw, ["applyTime", "aplTime", "submitTime", "createTime"]),
|
||||
allowPass: false,
|
||||
allowReject: false,
|
||||
raw,
|
||||
};
|
||||
}
|
||||
|
||||
function objectRecords(value: unknown): JsonObject[] {
|
||||
if (Array.isArray(value)) return value.filter(isObject);
|
||||
return findObjectArrays(value).flat();
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] { return [...new Set(values)]; }
|
||||
|
||||
function isSmartExpenseTodo(record: JsonObject): boolean {
|
||||
const appCode = stringAt(record, ["appCode", "APPCOD"])?.toLowerCase() ?? "";
|
||||
const businessCode = stringAt(record, ["businessCode", "BUSTYP"])?.toUpperCase() ?? "";
|
||||
@@ -1317,12 +1372,21 @@ function stripRaw(item: CurrentTodo): XftTodoItem {
|
||||
}
|
||||
|
||||
function toHistoryItem(item: CurrentTodo, historyScope: ApprovalHistoryScope): CurrentHistory {
|
||||
const { allowPass: _allowPass, allowReject: _allowReject, ...rest } = item;
|
||||
const {
|
||||
allowPass: _allowPass,
|
||||
allowReject: _allowReject,
|
||||
capability: _capability,
|
||||
unavailableReason: _unavailableReason,
|
||||
billId,
|
||||
...rest
|
||||
} = item;
|
||||
if (!billId) throw new ApprovalSafetyError("薪福通历史任务缺少正式单据标识");
|
||||
const statuses = collectStatusValues(item.raw);
|
||||
const status = statuses.billStatus ?? statuses.billSts ?? statuses.status;
|
||||
const completedAt = normalizedHistoryTime(item.raw, ["finishTime", "finishedAt", "completedAt", "dealTime", "approveTime", "endTime"]);
|
||||
return {
|
||||
...rest,
|
||||
billId,
|
||||
historyScope,
|
||||
...(status ? { status, statusName: xftBillStatusName(status) } : {}),
|
||||
...(completedAt ? { completedAt } : {}),
|
||||
|
||||
+5
-2
@@ -6,8 +6,10 @@ export interface AuthenticatedDingTalkUser {
|
||||
|
||||
export interface XftTodoItem {
|
||||
source: "xft";
|
||||
billId: string;
|
||||
billId?: string;
|
||||
taskId?: string;
|
||||
capability?: "bill_api" | "list_only";
|
||||
unavailableReason?: "unsupported_task_type" | "missing_bill_identifier" | "bill_lookup_unavailable";
|
||||
eventSeqId?: string;
|
||||
title?: string;
|
||||
applicantName?: string;
|
||||
@@ -28,7 +30,8 @@ export interface ApprovalHistoryRange {
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export interface XftHistoryItem extends Omit<XftTodoItem, "allowPass" | "allowReject"> {
|
||||
export interface XftHistoryItem extends Omit<XftTodoItem, "allowPass" | "allowReject" | "billId" | "capability" | "unavailableReason"> {
|
||||
billId: string;
|
||||
historyScope: ApprovalHistoryScope;
|
||||
status?: string;
|
||||
statusName?: string;
|
||||
|
||||
Reference in New Issue
Block a user