feat: expose readable submission choices

This commit is contained in:
2026-08-20 00:40:36 +00:00
parent 34c3c03a5f
commit ecb2f570e0
15 changed files with 454 additions and 114 deletions
+188 -69
View File
@@ -1515,6 +1515,11 @@ function normalizeSubmissionField(value: JsonObject): XftSubmissionField[] {
const name = stringAt(value, ["name"]);
const type = stringAt(value, ["type"]);
if (!fieldNumber || !name || !type || value.isDisplay === false) return [];
const options = Array.isArray(value.options) ? normalizeSubmissionOptions(value.options) : [];
const normalizedType = type.toUpperCase();
const optionSource = isExpenseTypeField(fieldNumber, name) ? expenseTypeOptionSource()
: normalizedType.includes("DEPARTMENT") || normalizedType.includes("ORG") ? departmentOptionSource()
: /城市|city/i.test(`${fieldNumber} ${name}`) ? cityOptionSource() : undefined;
return [{
fieldNumber,
name,
@@ -1522,10 +1527,10 @@ function normalizeSubmissionField(value: JsonObject): XftSubmissionField[] {
required: value.required === true,
...(typeof value.description === "string" && value.description.trim() ? { description: value.description.trim() } : {}),
...(typeof value.multiple === "boolean" ? { multiple: value.multiple } : {}),
...(Array.isArray(value.options) ? { options: value.options.map((option) => sanitizeOption(option)).filter((option) => option !== undefined) } : {}),
...(options.length ? { options } : {}),
...(typeof value.moduleType === "string" && value.moduleType ? { moduleType: value.moduleType } : {}),
...(isExpenseTypeField(fieldNumber, name) ? { optionSource: expenseTypeOptionSource() } : {}),
...submissionFieldShape(value, type),
...(optionSource ? { optionSource } : {}),
...submissionFieldShape(value, type, options, optionSource),
}];
}
@@ -1537,32 +1542,89 @@ function expenseTypeOptionSource() {
return { tool: "approval_get_submission_options" as const, kind: "expense_type" as const };
}
function submissionFieldShape(value: JsonObject, type: string): Pick<XftSubmissionField, "valueFormat" | "itemFields"> {
function cityOptionSource() {
return { tool: "approval_get_submission_options" as const, kind: "city" as const };
}
function departmentOptionSource() {
return { tool: "approval_get_submission_options" as const, kind: "department" as const };
}
const LABEL_ONLY_SELECTION = {
status: "supported" as const,
userInput: "label" as const,
submitValue: "code" as const,
exposeInternalValue: false as const,
};
function clientOnlySelection(reason: string) {
return { status: "client_only" as const, reason };
}
function submissionFieldShape(
value: JsonObject,
type: string,
options: XftSubmissionOption[],
optionSource?: ReturnType<typeof expenseTypeOptionSource> | ReturnType<typeof cityOptionSource> | ReturnType<typeof departmentOptionSource>,
): Pick<XftSubmissionField, "valueFormat" | "itemFields" | "selectionCapability"> {
const normalized = type.toUpperCase();
if (normalized.includes("TABLE")) {
const candidates = [value.columns, value.children, value.fieldList, value.tableColumns]
.find(Array.isArray) as unknown[] | undefined;
const itemFields = (candidates ?? []).filter(isObject).flatMap((column) => {
const titleCode = stringAt(column, ["titleCode", "fieldNumber", "fieldCode", "code"]);
const label = stringAt(column, ["name", "title", "label"]);
const columnType = stringAt(column, ["type", "fieldType"]);
if (!titleCode) return [];
return [{ titleCode, ...(label ? { label } : {}), ...(columnType ? { type: columnType } : {}), required: column.required === true }];
});
const itemFields = (candidates ?? []).filter(isObject).flatMap(normalizeSubmissionTableColumn);
return {
valueFormat: "JSON array: [{sequence:1,columnValues:[{titleCode,value}]}]",
...(itemFields.length ? { itemFields } : {}),
};
}
if (normalized.includes("ATTACH") || normalized.includes("ACCESSOR")) return { valueFormat: "附件编号字符串(须先通过薪福通附件接口取得)" };
if (normalized.includes("PERSON") || normalized.includes("EMP")) return { valueFormat: "薪福通花名册编号 JSON 数组" };
if (normalized.includes("DEPARTMENT") || normalized.includes("ORG")) return { valueFormat: "薪福通组织编码 JSON 数组" };
if (normalized.includes("ATTACH") || normalized.includes("ACCESSOR")) return {
valueFormat: "当前 MCP 无法安全上传附件,请在薪福通客户端补充",
selectionCapability: clientOnlySelection("当前 MCP 没有薪福通附件上传能力"),
};
if (normalized.includes("PERSON") || normalized.includes("EMP")) return {
valueFormat: "当前 MCP 无法按姓名安全选择人员,请在薪福通客户端补充",
selectionCapability: clientOnlySelection("正式接口需要内部花名册值,但当前没有按姓名查询人员的安全接口"),
};
if (normalized.includes("DEPARTMENT") || normalized.includes("ORG")) return {
valueFormat: "向用户展示部门名称,Agent 内部使用所选项",
selectionCapability: LABEL_ONLY_SELECTION,
};
if (normalized === "DATE") return { valueFormat: "yyyy-MM-dd HH:mm" };
if (normalized.includes("SELECT")) return { valueFormat: "使用 options 中的编码" };
if (normalized.includes("SELECT")) {
if (options.length || optionSource) return {
valueFormat: "向用户展示选项名称,Agent 内部使用所选项",
selectionCapability: LABEL_ONLY_SELECTION,
};
return {
valueFormat: "当前模板未返回可读选项,请在薪福通客户端补充",
selectionCapability: clientOnlySelection("模板没有返回可验证的名称与内部值映射"),
};
}
if (normalized.includes("INPUT")) return { valueFormat: "文本字符串" };
return {};
}
function normalizeSubmissionTableColumn(column: JsonObject): Array<Record<string, unknown>> {
const titleCode = stringAt(column, ["titleCode", "fieldNumber", "fieldCode", "code"]);
const label = stringAt(column, ["name", "title", "label"]);
const type = stringAt(column, ["type", "fieldType"]);
if (!titleCode) return [];
const options = Array.isArray(column.options) ? normalizeSubmissionOptions(column.options) : [];
const normalizedType = (type ?? "").toUpperCase();
const optionSource = isExpenseTypeField(titleCode, label ?? "") ? expenseTypeOptionSource()
: normalizedType.includes("DEPARTMENT") || normalizedType.includes("ORG") ? departmentOptionSource()
: /城市|city/i.test(`${titleCode} ${label ?? ""}`) ? cityOptionSource() : undefined;
return [{
titleCode,
...(label ? { label } : {}),
...(type ? { type } : {}),
required: column.required === true,
...(options.length ? { options } : {}),
...(optionSource ? { optionSource } : {}),
...submissionFieldShape(column, type ?? "", options, optionSource),
}];
}
function deduplicateSubmissionFields(fields: XftSubmissionField[]): XftSubmissionField[] {
const seen = new Set<string>();
return fields.filter((field) => {
@@ -1575,13 +1637,16 @@ function deduplicateSubmissionFields(fields: XftSubmissionField[]): XftSubmissio
});
}
function sanitizeOption(value: unknown): unknown {
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
function sanitizeOption(value: unknown): XftSubmissionOption | undefined {
if (!isObject(value)) return undefined;
const label = stringAt(value, ["label", "name", "value", "optionName"]);
const code = stringAt(value, ["code", "number", "optionCode", "key"]);
if (!label && !code) return undefined;
return { ...(label ? { label } : {}), ...(code ? { code } : {}) };
const label = stringAt(value, ["label", "name", "optionName", "displayName", "value"]);
const code = stringAt(value, ["code", "number", "optionCode", "key", "value"]);
if (!label || !code) return undefined;
return { label, code };
}
function normalizeSubmissionOptions(values: unknown[]): XftSubmissionOption[] {
return uniqueOptions(values.map(sanitizeOption).filter((item): item is XftSubmissionOption => Boolean(item)));
}
function submissionInputContract(kind: XftSubmissionKind): Record<string, unknown> {
@@ -1632,13 +1697,67 @@ function submissionInputContractSummary(kind: XftSubmissionKind): Record<string,
return values[kind];
}
function cityField(key: string, label: string, required: boolean): Record<string, unknown> {
return {
key, label, required, type: "string",
description: "向用户展示城市名称,Agent 内部使用所选项",
optionSource: cityOptionSource(),
selectionCapability: LABEL_ONLY_SELECTION,
};
}
function expenseTypeField(key: string, required: boolean): Record<string, unknown> {
return {
key, label: "费用类型", required, type: "string",
description: "向用户展示费用名称,Agent 内部使用所选项",
optionSource: expenseTypeOptionSource(),
selectionCapability: LABEL_ONLY_SELECTION,
};
}
function readableEnumField(
key: string,
label: string,
required: boolean,
labels: Record<string, string>,
extra: Record<string, unknown> = {},
): Record<string, unknown> {
return {
key, label, required, type: "string", enum: Object.keys(labels), enumLabels: labels,
description: "只向用户展示中文选项名称,Agent 内部使用对应值",
selectionCapability: LABEL_ONLY_SELECTION,
...extra,
};
}
function clientOnlyField(
key: string,
label: string,
required: boolean,
reason: string,
extra: Record<string, unknown> = {},
): Record<string, unknown> {
return {
key, label, required, ...extra,
selectionCapability: clientOnlySelection(reason),
description: `${reason};不要向用户索取内部编码或 ID,如需填写请在薪福通客户端完成`,
};
}
const TRAVEL_MODE_LABELS = {
Train: "火车", Bus: "汽车", Ship: "轮船", Hotel: "住宿", RideHailing: "网约车", AirPlane: "飞机", Other: "其他",
} as const;
const ROUND_TRIP_LABELS = { N: "单程", Y: "往返" } as const;
const HALF_DAY_LABELS = { AM: "上午", PM: "下午" } as const;
const CURRENCY_LABELS = { CNY: "人民币" } as const;
function expenseApplicationFields(): Array<Record<string, unknown>> {
return [{
key: "applyExpenses", label: "事前申请费用列表", required: true, type: "array", minItems: 1, itemFields: [
{ key: "expenseNumber", label: "费用类型", required: true, type: "string", description: "向用户展示可读名称,内部填写所选项的 code", optionSource: expenseTypeOptionSource() },
expenseTypeField("expenseNumber", true),
{ key: "amount", label: "申请金额", required: true, type: "decimal", precision: 2 },
{ key: "occurrenceDate", label: "费用发生时间", required: false, type: "datetime", description: "ISO 8601 时间" },
{ key: "payCityCode", label: "消费城市编码", required: false, type: "string" },
cityField("payCityCode", "消费城市", false),
{ key: "participantNum", label: "参与人数", required: false, type: "integer" },
{ key: "merchantName", label: "商户名称", required: false, type: "string" },
{ key: "remark", label: "备注", required: false, type: "string" },
@@ -1650,7 +1769,7 @@ function expenseApplicationFields(): Array<Record<string, unknown>> {
function entertainmentApplicationFields(): Array<Record<string, unknown>> {
return [{
key: "entertainmentApplyApiCreateDTO", label: "招待信息", required: true, type: "object", fields: [
{ key: "entertainmentCityCode", label: "招待城市编码", required: true, type: "string", description: "薪福通城市 cityCode" },
cityField("entertainmentCityCode", "招待城市", true),
{ key: "entertainmentPersonCount", label: "招待人数", required: true, type: "integer", minimum: 1 },
{ key: "entertainmentStartTime", label: "招待开始日期", required: true, type: "date", format: "yyyy-MM-dd" },
{ key: "entertainmentEndTime", label: "招待结束日期", required: true, type: "date", format: "yyyy-MM-dd" },
@@ -1663,44 +1782,44 @@ function travelApplicationFields(): Array<Record<string, unknown>> {
key: "businessTrip", label: "差旅行程", required: true, type: "object", fields: [{
key: "businessTripDetails", label: "行程明细", required: true, type: "array", minItems: 1,
itemFields: [
{ key: "departCityCode", label: "出发城市编码", required: true, type: "string" },
{ key: "destinationCityCode", label: "目的城市编码", required: true, type: "string" },
cityField("departCityCode", "出发城市", true),
cityField("destinationCityCode", "目的城市", true),
{ key: "beginTime", label: "开始时间", required: true, type: "datetime", format: "yyyy-MM-dd HH:mm" },
{ key: "endTime", label: "结束时间", required: true, type: "datetime", format: "yyyy-MM-dd HH:mm" },
{ key: "travelMode", label: "出行方式", required: false, type: "comma_separated_enum", enum: ["Train", "Bus", "Ship", "Hotel", "RideHailing", "AirPlane", "Other"] },
{ key: "roundTrip", label: "是否往返", required: false, type: "string", enum: ["N", "Y"] },
{ key: "beginTimePrecision", label: "开始时间半天精度", required: false, type: "string", enum: ["AM", "PM"] },
{ key: "endTimePrecision", label: "结束时间半天精度", required: false, type: "string", enum: ["AM", "PM"] },
{ key: "customArchiveOptionNumbers", label: "行程自定义档案选项", required: false, type: "array" },
readableEnumField("travelMode", "出行方式", false, TRAVEL_MODE_LABELS, { type: "comma_separated_enum", multiple: true }),
readableEnumField("roundTrip", "是否往返", false, ROUND_TRIP_LABELS),
readableEnumField("beginTimePrecision", "开始时间范围", false, HALF_DAY_LABELS),
readableEnumField("endTimePrecision", "结束时间范围", false, HALF_DAY_LABELS),
clientOnlyField("customArchiveOptionNumbers", "行程自定义档案", false, "当前 MCP 没有按名称查询企业自定义档案的可靠能力", { type: "array" }),
{ key: "estimateExpenses", label: "预计费用", required: false, type: "array", itemFields: [
{ key: "expenseTypeNumber", label: "费用类型", required: true, type: "string", description: "向用户展示可读名称,内部填写所选项的 code", optionSource: expenseTypeOptionSource() },
expenseTypeField("expenseTypeNumber", true),
{ key: "applyAmount", label: "申请金额", required: true, type: "decimal" },
{ key: "remark", label: "备注", required: false, type: "string" },
{ key: "spreadInfo", label: "分摊信息", required: false, type: "object" },
clientOnlyField("spreadInfo", "分摊信息", false, "当前 MCP 无法把企业分摊名称安全映射为正式接口值", { type: "object" }),
] },
],
}] },
{ key: "relatedBillId", label: "关联单据 ID", required: false, type: "string" },
{ key: "peerEmpNumbers", label: "同行人员工号", required: false, type: "array" },
{ key: "billCompanyCode", label: "单据公司编码", required: false, type: "string" },
{ key: "billDepartmentCode", label: "单据部门编码", required: false, type: "string" },
clientOnlyField("relatedBillId", "关联申请单", false, "当前 MCP 没有按业务名称安全选择前置单据的能力", { type: "string" }),
clientOnlyField("peerEmpNumbers", "同行人员", false, "当前 MCP 没有按姓名查询薪福通人员的安全接口", { type: "array" }),
{ key: "billCompanyCode", label: "单据公司", required: false, type: "string", serverManaged: true, userVisible: false, description: "省略后由薪福通按当前登录用户填写" },
{ key: "billDepartmentCode", label: "单据部门", required: false, type: "string", serverManaged: true, userVisible: false, description: "省略后由薪福通按当前登录用户填写" },
];
}
function reimbursementHeaderFields(): Array<Record<string, unknown>> {
return [
{ key: "reimAmount", label: "报销总金额", required: true, type: "decimal", precision: 2 },
{ key: "reimAmountCurrencyCode", label: "报销币种", required: true, type: "string", enum: ["CNY"], description: "正式接口目前仅支持人民币" },
{ key: "outRelId", label: "外部关联 ID", required: false, type: "string", maxLength: 100 },
{ key: "billDepartmentCode", label: "单据部门编码", required: false, type: "string", maxLength: 100 },
{ key: "billCompanyCode", label: "单据公司编码", required: false, type: "string", maxLength: 50 },
{ key: "customFieldValues", label: "单据自定义字段", required: false, type: "array", itemFields: customFieldValueFields() },
{ key: "billCollectionInfo", label: "收款信息", required: false, type: "object", description: "需要付款时填写;包含收款人、银行、账号、金额和币种" },
{ key: "billLendPayRelList", label: "核销借款信息", required: false, type: "array", itemFields: [
readableEnumField("reimAmountCurrencyCode", "报销币种", true, CURRENCY_LABELS, { default: "CNY" }),
{ key: "outRelId", label: "外部关联标识", required: false, type: "string", maxLength: 100, userVisible: false, description: "仅供系统集成使用,不应询问普通用户" },
{ key: "billDepartmentCode", label: "单据部门", required: false, type: "string", maxLength: 100, serverManaged: true, userVisible: false, description: "省略后由薪福通按当前登录用户填写" },
{ key: "billCompanyCode", label: "单据公司", required: false, type: "string", maxLength: 50, serverManaged: true, userVisible: false, description: "省略后由薪福通按当前登录用户填写" },
{ key: "customFieldValues", label: "单据自定义字段", required: false, type: "array", userVisible: false, description: "由 Agent 根据实时 fields 组装,不向用户展示字段编码", itemFields: customFieldValueFields() },
clientOnlyField("billCollectionInfo", "收款信息", false, "当前 MCP 无法完整验证收款账户的正式接口引用", { type: "object" }),
clientOnlyField("billLendPayRelList", "核销借款", false, "当前 MCP 没有按可读单号安全选择本人借款单的能力", { type: "array", itemFields: [
{ key: "lendBillId", label: "借款单号", required: true, type: "string" },
{ key: "currencyCode", label: "核销币种", required: true, type: "string", enum: ["CNY"] },
readableEnumField("currencyCode", "核销币种", true, CURRENCY_LABELS, { default: "CNY" }),
{ key: "originalPayAmount", label: "核销金额", required: true, type: "decimal", precision: 2 },
] },
] }),
];
}
@@ -1712,21 +1831,21 @@ function travelReimbursementFields(): Array<Record<string, unknown>> {
return [
...reimbursementHeaderFields(),
expenseListField(true),
{ key: "peerEmpNumbers", label: "同行人员工号", required: false, type: "array", maxItems: 50 },
clientOnlyField("peerEmpNumbers", "同行人员", false, "当前 MCP 没有按姓名查询薪福通人员的安全接口", { type: "array", maxItems: 50 }),
{
key: "businessTrip", label: "差旅行程", required: true, type: "object", fields: [{
key: "businessTripDetails", label: "行程明细", required: true, type: "array", minItems: 1,
itemFields: [
{ key: "departCityCode", label: "出发城市编码", required: true, type: "string", description: "薪福通城市 cityCode" },
{ key: "destinationCityCode", label: "目的城市编码", required: true, type: "string", description: "薪福通城市 cityCode" },
cityField("departCityCode", "出发城市", true),
cityField("destinationCityCode", "目的城市", true),
{ key: "beginTime", aliases: ["startTime"], label: "出发时间", required: true, type: "datetime", format: "yyyy-MM-dd HH:mm" },
{ key: "endTime", label: "结束时间", required: true, type: "datetime", format: "yyyy-MM-dd HH:mm" },
{ key: "travelMode", label: "出行方式", required: false, type: "comma_separated_enum", enum: ["Train", "Bus", "Ship", "Hotel", "RideHailing", "AirPlane", "Other"] },
{ key: "roundTrip", label: "是否往返", required: false, type: "string", enum: ["N", "Y"], default: "N" },
{ key: "beginTimePrecision", label: "开始时间半天精度", required: false, type: "string", enum: ["AM", "PM"], description: "模板时间精度为半天时必填" },
{ key: "endTimePrecision", label: "结束时间半天精度", required: false, type: "string", enum: ["AM", "PM"], description: "模板时间精度为半天时必填" },
readableEnumField("travelMode", "出行方式", false, TRAVEL_MODE_LABELS, { type: "comma_separated_enum", multiple: true }),
readableEnumField("roundTrip", "是否往返", false, ROUND_TRIP_LABELS, { default: "N" }),
readableEnumField("beginTimePrecision", "开始时间范围", false, HALF_DAY_LABELS, { condition: "模板时间精度为半天时必填" }),
readableEnumField("endTimePrecision", "结束时间范围", false, HALF_DAY_LABELS, { condition: "模板时间精度为半天时必填" }),
{ key: "tripReason", label: "出差事由", required: false, type: "string", maxLength: 200, description: "模板将行程事由设为必填时必须填写" },
{ key: "customArchiveOptionNumbers", label: "行程自定义档案选项", required: false, type: "array" },
clientOnlyField("customArchiveOptionNumbers", "行程自定义档案", false, "当前 MCP 没有按名称查询企业自定义档案的可靠能力", { type: "array" }),
],
}],
},
@@ -1735,25 +1854,25 @@ function travelReimbursementFields(): Array<Record<string, unknown>> {
function expenseListField(travel: boolean): Record<string, unknown> {
const itemFields: Array<Record<string, unknown>> = [
{ key: "expTypeCode", label: "费用类型", required: true, type: "string", maxLength: 100, description: "向用户展示可读名称,内部填写所选项的 code", optionSource: expenseTypeOptionSource() },
{ key: "currencyCode", label: "费用币种", required: true, type: "string", enum: ["CNY"] },
{ ...expenseTypeField("expTypeCode", true), maxLength: 100 },
readableEnumField("currencyCode", "费用币种", true, CURRENCY_LABELS, { default: "CNY" }),
{ key: "originalCurrencyReimAmt", label: "费用金额", required: true, type: "decimal", precision: 2 },
{ key: "wrTim", label: "消费日期", required: false, type: "date", format: "yyyy-MM-dd" },
{ key: "remark", label: "费用备注", required: false, type: "string", maxLength: 500 },
{ key: "merchantName", label: "商户名称", required: false, type: "string", maxLength: 200 },
{ key: "participants", label: "费用参与人员工号", required: false, type: "array", maxItems: 50 },
clientOnlyField("participants", "费用参与人员", false, "当前 MCP 没有按姓名查询薪福通人员的安全接口", { type: "array", maxItems: 50 }),
{ key: "partiNbr", label: "参与人数", required: false, type: "integer" },
{ key: "taxRate", label: "税率", required: false, type: "decimal", precision: 2 },
{ key: "taxFreeAmount", label: "不含税金额", required: false, type: "decimal", precision: 2 },
{ key: "departureCode", label: "费用出发城市编码", required: false, type: "string" },
{ key: "destinationCode", label: "费用目的城市编码", required: false, type: "string" },
cityField("departureCode", "费用出发城市", false),
cityField("destinationCode", "费用目的城市", false),
{ key: "bgnTim", label: "交通出发日期", required: false, type: "date", format: "yyyy-MM-dd" },
{ key: "endTim", label: "交通到达日期", required: false, type: "date", format: "yyyy-MM-dd" },
{ key: "checkInDate", label: "酒店入住日期", required: false, type: "date", format: "yyyy-MM-dd" },
{ key: "checkOutDate", label: "酒店离店日期", required: false, type: "date", format: "yyyy-MM-dd" },
{ key: "payCityCode", label: "消费城市编码", required: false, type: "string" },
{ key: "hotelStar", label: "酒店星级", required: false, type: "string", enum: ["1", "2", "3", "4"], enumLabels: { "1": "五星", "2": "四星", "3": "三星", "4": "其他" } },
{ key: "aircraftCabin", label: "飞机舱位", required: false, type: "string", enum: ["1", "2", "3", "4"] },
cityField("payCityCode", "消费城市", false),
readableEnumField("hotelStar", "酒店星级", false, { "1": "五星", "2": "四星", "3": "三星", "4": "其他" }),
clientOnlyField("aircraftCabin", "飞机舱位", false, "正式接口文档未提供可验证的舱位名称映射", { type: "string" }),
{ key: "airlineNo", label: "航班号", required: false, type: "string" },
{ key: "airlineCom", label: "航空公司", required: false, type: "string" },
{ key: "flightETicketNo", label: "电子客票号", required: false, type: "string" },
@@ -1761,20 +1880,20 @@ function expenseListField(travel: boolean): Record<string, unknown> {
{ key: "civilAviationFund", label: "民航发展基金", required: false, type: "decimal", precision: 2 },
{ key: "discount", label: "机票折扣", required: false, type: "decimal", precision: 2 },
{ key: "trainNo", label: "列车号", required: false, type: "string" },
{ key: "trainCabin", label: "火车席别", required: false, type: "string", description: "使用官方席别编码 1-15" },
clientOnlyField("trainCabin", "火车席别", false, "正式接口文档未提供可验证的席别名称映射", { type: "string" }),
{ key: "trainETicketNo", label: "火车票票号", required: false, type: "string" },
{ key: "shipCabin", label: "轮船舱位", required: false, type: "string", enum: ["1", "2", "3"] },
{ key: "customFieldValues", label: "费用自定义字段", required: false, type: "array", itemFields: customFieldValueFields() },
{ key: "spreadInfo", label: "费用分摊", required: false, type: "object", description: "按企业分摊方式填写" },
clientOnlyField("shipCabin", "轮船舱位", false, "正式接口文档未提供可验证的舱位名称映射", { type: "string" }),
{ key: "customFieldValues", label: "费用自定义字段", required: false, type: "array", userVisible: false, description: "由 Agent 根据实时 fields 组装,不向用户展示字段编码", itemFields: customFieldValueFields() },
clientOnlyField("spreadInfo", "费用分摊", false, "当前 MCP 无法把企业分摊名称安全映射为正式接口值", { type: "object" }),
];
if (travel) itemFields.push({ key: "dstntIndex", label: "归属行程序号", required: false, type: "string", description: "从 0 开始;不填则不归组到行程" });
if (travel) itemFields.push({ key: "dstntIndex", label: "归属行程", required: false, type: "string", userVisible: false, internallyDerived: true, description: "由 Agent 根据用户选择的行程位置内部换算,不向用户展示序号" });
return { key: "expenseList", label: "费用明细", required: true, type: "array", minItems: 1, itemFields };
}
function customFieldValueFields(): Array<Record<string, unknown>> {
return [
{ key: "fieldNumber", label: "自定义字段编码", required: true, type: "string" },
{ key: "fieldValue", label: "自定义字段值", required: true, type: "string", description: "复杂值按字段类型序列化为 JSON 字符串" },
{ key: "fieldNumber", label: "字段引用", required: true, type: "string", userVisible: false, description: "由 Agent 从实时 Schema 内部复制,不向用户展示或索取" },
{ key: "fieldValue", label: "字段值", required: true, type: "string", userVisible: false, description: "由 Agent 根据字段类型内部序列化" },
];
}