feat(web): AI 文档详情引用解析与查询卡片增强
- 新增 aiDocumentDetailReference,统一解析 #ai-open-document-detail / #ai-open-application-detail 引用,兼容 A/R/D 短格式与 AP-/RE-/AD- 旧格式单号,提供 isBusinessDocumentReference 判定 - aiDocumentQueryModel 文档卡片接入详情引用,按申请单/报销单生成对应 href,HTML 渲染器识别单据记录表格并生成卡片链接 - PersonalWorkbenchAiMode 处理文档详情点击跳转,卡片样式重构为结构化布局并更新背景资源 - expenseApplicationPreview 补充事由字段,同步新增/更新 ai-document-detail-reference、document-query-model、html-renderer、workbench-ai-mode 等测试 - 更新公司通信费报销规则表
This commit is contained in:
@@ -727,8 +727,15 @@ import {
|
||||
buildAiDocumentQueryConditionSummary,
|
||||
buildAiDocumentQueryMessage,
|
||||
filterAiDocumentQueryRecords,
|
||||
mergeAiDocumentQueryPayloads,
|
||||
resolveAiDocumentQueryIntent
|
||||
} from '../../utils/aiDocumentQueryModel.js'
|
||||
import {
|
||||
AI_APPLICATION_DETAIL_HREF_PREFIX,
|
||||
buildAiDocumentDetailRequest,
|
||||
parseAiApplicationDetailHref,
|
||||
parseAiDocumentDetailHref
|
||||
} from '../../utils/aiDocumentDetailReference.js'
|
||||
import {
|
||||
buildRequiredApplicationActions,
|
||||
buildRequiredApplicationMissingText,
|
||||
@@ -780,9 +787,6 @@ const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6
|
||||
const INLINE_ANSWER_STREAM_DELAY_MS = 24
|
||||
const INLINE_AUTO_SCROLL_THRESHOLD = 96
|
||||
const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260
|
||||
const AI_DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-detail:'
|
||||
const AI_APPLICATION_DETAIL_HREF_PREFIX = '#ai-open-application-detail:'
|
||||
|
||||
const {
|
||||
applicationPreviewEditor,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
@@ -1269,6 +1273,27 @@ function normalizeInlineApplicationResultTableCell(value, fallback = '-') {
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
const INLINE_APPLICATION_STATUS_LABELS = {
|
||||
draft: '草稿',
|
||||
submitted: '审批中',
|
||||
pending: '待处理',
|
||||
approved: '已审批',
|
||||
completed: '已完成',
|
||||
archived: '已归档',
|
||||
returned: '已退回',
|
||||
rejected: '已驳回',
|
||||
pending_payment: '待付款',
|
||||
paid: '已付款'
|
||||
}
|
||||
|
||||
function normalizeInlineApplicationStatusLabel(value, fallback = '') {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return fallback
|
||||
}
|
||||
return INLINE_APPLICATION_STATUS_LABELS[text.toLowerCase()] || text
|
||||
}
|
||||
|
||||
function buildInlineApplicationActionDetailHref(reference = '') {
|
||||
const source = reference && typeof reference === 'object' ? reference : { reference }
|
||||
const claimId = String(source.claimId || source.claim_id || source.id || '').trim()
|
||||
@@ -1289,11 +1314,63 @@ function buildInlineApplicationActionDetailHref(reference = '') {
|
||||
|
||||
function resolveInlineApplicationActionDocumentInfo(draftPayload = {}) {
|
||||
const source = draftPayload && typeof draftPayload === 'object' ? draftPayload : {}
|
||||
const body = String(source.body || source.markdown || '').trim()
|
||||
const resolveBodyField = (labels = []) => {
|
||||
for (const label of labels) {
|
||||
const pattern = new RegExp(`${label}\\s*[::]\\s*([^\\n|]+)`, 'u')
|
||||
const match = body.match(pattern)
|
||||
if (match?.[1]) {
|
||||
return String(match[1]).replace(/\*\*/g, '').trim()
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
const startDate = String(source.start_date || source.startDate || source.trip_start_date || source.tripStartDate || '').trim()
|
||||
const endDate = String(source.end_date || source.endDate || source.trip_end_date || source.tripEndDate || '').trim()
|
||||
const dateText = String(
|
||||
source.business_time ||
|
||||
source.businessTime ||
|
||||
source.time ||
|
||||
source.occurred_at ||
|
||||
source.occurredAt ||
|
||||
source.apply_time ||
|
||||
source.applyTime ||
|
||||
''
|
||||
).trim()
|
||||
const rangeText = startDate && endDate && startDate !== endDate
|
||||
? `${startDate} 至 ${endDate}`
|
||||
: startDate || endDate
|
||||
return {
|
||||
claimNo: String(source.claim_no || source.claimNo || source.document_no || source.documentNo || '').trim(),
|
||||
claimId: String(source.claim_id || source.claimId || source.id || '').trim(),
|
||||
statusLabel: String(source.status_label || source.statusLabel || source.status || '').trim(),
|
||||
statusLabel: normalizeInlineApplicationStatusLabel(source.status_label || source.statusLabel || source.status),
|
||||
approvalStage: String(source.approval_stage || source.approvalStage || '').trim(),
|
||||
dateLabel: rangeText || dateText || resolveBodyField(['时间', '日期', '申请时间']) || '待补充',
|
||||
locationLabel: String(
|
||||
source.location ||
|
||||
source.application_location ||
|
||||
source.applicationLocation ||
|
||||
source.destination ||
|
||||
source.destination_city ||
|
||||
source.destinationCity ||
|
||||
''
|
||||
).trim() || resolveBodyField(['地点', '目的地']) || '待补充',
|
||||
reasonLabel: String(
|
||||
source.reason ||
|
||||
source.business_reason ||
|
||||
source.businessReason ||
|
||||
source.description ||
|
||||
source.title ||
|
||||
''
|
||||
).trim() || resolveBodyField(['事由', '事件', '申请事由']) || '待补充',
|
||||
amountLabel: String(
|
||||
source.amount ||
|
||||
source.application_amount ||
|
||||
source.applicationAmount ||
|
||||
source.estimated_amount ||
|
||||
source.estimatedAmount ||
|
||||
''
|
||||
).trim() || resolveBodyField(['金额', '预计金额', '申请金额']) || '-',
|
||||
documentTypeLabel: String(
|
||||
source.document_type_label ||
|
||||
source.documentTypeLabel ||
|
||||
@@ -1311,10 +1388,11 @@ function buildInlineApplicationResultTable(draftPayload = {}, options = {}) {
|
||||
const reference = info.claimNo || info.claimId
|
||||
const href = buildInlineApplicationActionDetailHref(info)
|
||||
const actionText = href ? `[查看](${href})` : '-'
|
||||
const statusLabel = normalizeInlineApplicationStatusLabel(info.statusLabel, options.statusLabel)
|
||||
return [
|
||||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 操作 |',
|
||||
'| --- | --- | --- | --- | --- |',
|
||||
`| ${normalizeInlineApplicationResultTableCell(info.documentTypeLabel || options.documentTypeLabel, '出差申请')} | ${normalizeInlineApplicationResultTableCell(reference)} | ${normalizeInlineApplicationResultTableCell(info.statusLabel || options.statusLabel)} | ${normalizeInlineApplicationResultTableCell(info.approvalStage || options.stageLabel)} | ${actionText} |`
|
||||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 日期 | 地点 | 事由 | 金额 | 操作 |',
|
||||
'| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
|
||||
`| ${normalizeInlineApplicationResultTableCell(info.documentTypeLabel || options.documentTypeLabel, '出差申请')} | ${normalizeInlineApplicationResultTableCell(reference)} | ${normalizeInlineApplicationResultTableCell(statusLabel)} | ${normalizeInlineApplicationResultTableCell(info.approvalStage || options.stageLabel)} | ${normalizeInlineApplicationResultTableCell(info.dateLabel)} | ${normalizeInlineApplicationResultTableCell(info.locationLabel)} | ${normalizeInlineApplicationResultTableCell(info.reasonLabel)} | ${normalizeInlineApplicationResultTableCell(info.amountLabel, '-')} | ${actionText} |`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
@@ -1331,7 +1409,7 @@ function buildInlineApplicationPreviewActionResultText(actionType, payload = {})
|
||||
stageLabel: approvalStage || '直属领导审批',
|
||||
documentTypeLabel: '出差申请'
|
||||
}),
|
||||
'需要查看完整详情时,请点击列表最后一列的“查看”进入单据详情。'
|
||||
'需要查看完整详情时,请点击卡片“操作”行的“查看”进入单据详情。'
|
||||
].filter(Boolean).join('\n\n')
|
||||
}
|
||||
return [
|
||||
@@ -1342,7 +1420,7 @@ function buildInlineApplicationPreviewActionResultText(actionType, payload = {})
|
||||
stageLabel: '待提交',
|
||||
documentTypeLabel: '出差申请'
|
||||
}),
|
||||
'后续请点击表格最后一列的“查看”进入详情页继续核对。'
|
||||
'后续请点击卡片“操作”行的“查看”进入详情页继续核对。'
|
||||
].filter(Boolean).join('\n\n')
|
||||
}
|
||||
|
||||
@@ -1935,74 +2013,6 @@ async function fetchInlineStewardPlan(messageId, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseAiDocumentDetailHref(href = '') {
|
||||
const value = String(href || '').trim()
|
||||
if (!value.startsWith(AI_DOCUMENT_DETAIL_HREF_PREFIX)) {
|
||||
return null
|
||||
}
|
||||
const encodedReference = value.slice(AI_DOCUMENT_DETAIL_HREF_PREFIX.length)
|
||||
if (!encodedReference) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const reference = decodeURIComponent(encodedReference).trim()
|
||||
return reference ? { reference } : null
|
||||
} catch {
|
||||
return { reference: encodedReference }
|
||||
}
|
||||
}
|
||||
|
||||
function parseAiApplicationDetailHref(href = '') {
|
||||
const value = String(href || '').trim()
|
||||
if (!value.startsWith(AI_APPLICATION_DETAIL_HREF_PREFIX)) {
|
||||
return null
|
||||
}
|
||||
const encodedReference = value.slice(AI_APPLICATION_DETAIL_HREF_PREFIX.length)
|
||||
if (!encodedReference) {
|
||||
return null
|
||||
}
|
||||
let reference = ''
|
||||
try {
|
||||
reference = decodeURIComponent(encodedReference).trim()
|
||||
} catch {
|
||||
reference = encodedReference.trim()
|
||||
}
|
||||
if (!reference) {
|
||||
return null
|
||||
}
|
||||
const params = new URLSearchParams(reference)
|
||||
const claimId = String(params.get('claim_id') || '').trim()
|
||||
const claimNo = String(params.get('claim_no') || '').trim()
|
||||
if (claimId || claimNo) {
|
||||
return {
|
||||
reference: claimNo || claimId,
|
||||
claimId,
|
||||
claimNo
|
||||
}
|
||||
}
|
||||
return { reference }
|
||||
}
|
||||
|
||||
function buildAiDocumentDetailRequest(detailReference = {}) {
|
||||
const reference = String(detailReference.reference || '').trim()
|
||||
const claimId = String(detailReference.claimId || detailReference.claim_id || '').trim()
|
||||
const claimNo = String(detailReference.claimNo || detailReference.claim_no || '').trim()
|
||||
const lookupReference = claimId || reference
|
||||
const displayReference = claimNo || reference
|
||||
const isApplication = /^APP?-/i.test(displayReference) || Boolean(claimId || claimNo)
|
||||
return {
|
||||
id: lookupReference,
|
||||
claimId: claimId || reference,
|
||||
claimNo: claimNo || reference,
|
||||
documentNo: displayReference,
|
||||
documentType: isApplication ? 'application' : 'reimbursement',
|
||||
documentTypeCode: isApplication ? 'application' : 'reimbursement',
|
||||
detailLookupOnly: true,
|
||||
source: 'workbench',
|
||||
returnTo: 'workbench'
|
||||
}
|
||||
}
|
||||
|
||||
function handleAiAnswerMarkdownClick(event) {
|
||||
const target = event?.target
|
||||
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
||||
@@ -2055,6 +2065,47 @@ function failAiDocumentQueryEvents(events) {
|
||||
}))
|
||||
}
|
||||
|
||||
function resolveAiDocumentQueryFetchPendingText(intent = {}) {
|
||||
if (intent.source === 'approval') {
|
||||
return '等待调用待我审核单据接口。'
|
||||
}
|
||||
if (intent.source === 'mine') {
|
||||
return '等待调用我名下单据接口。'
|
||||
}
|
||||
return '等待同时调用我名下单据和待我审核单据接口。'
|
||||
}
|
||||
|
||||
function resolveAiDocumentQueryFetchRunningText(intent = {}) {
|
||||
if (intent.source === 'approval') {
|
||||
return '正在查询待我审核的单据,接口范围为待办/待审单据列表。'
|
||||
}
|
||||
if (intent.source === 'mine') {
|
||||
return '正在查询我名下的单据,接口范围为当前用户本人单据列表。'
|
||||
}
|
||||
return '正在查询我可见的单据,接口范围包含我名下单据和待我审核单据列表。'
|
||||
}
|
||||
|
||||
async function fetchAiDocumentQueryPayload(intent = {}) {
|
||||
const requestParams = { page: 1, pageSize: 100 }
|
||||
if (intent.source === 'approval') {
|
||||
return fetchApprovalExpenseClaims(requestParams)
|
||||
}
|
||||
if (intent.source === 'mine') {
|
||||
return fetchExpenseClaims(requestParams)
|
||||
}
|
||||
const [ownPayload, approvalPayload] = await Promise.all([
|
||||
fetchExpenseClaims(requestParams),
|
||||
fetchApprovalExpenseClaims(requestParams)
|
||||
])
|
||||
return mergeAiDocumentQueryPayloads(
|
||||
ownPayload,
|
||||
{
|
||||
items: extractExpenseClaimItems(approvalPayload),
|
||||
querySource: 'approval'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||
const intent = resolveAiDocumentQueryIntent(prompt)
|
||||
if (!intent) {
|
||||
@@ -2072,7 +2123,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||
{
|
||||
eventId: 'document-query-fetch',
|
||||
title: '查询业务单据接口',
|
||||
content: intent.source === 'approval' ? '等待调用待我审核单据接口。' : '等待调用我名下单据接口。',
|
||||
content: resolveAiDocumentQueryFetchPendingText(intent),
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
@@ -2090,9 +2141,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||
event.eventId === 'document-query-fetch'
|
||||
? {
|
||||
...event,
|
||||
content: intent.source === 'approval'
|
||||
? '正在查询待我审核的单据,接口范围为待办/待审单据列表。'
|
||||
: '正在查询我名下的单据,接口范围为当前用户可见单据列表。',
|
||||
content: resolveAiDocumentQueryFetchRunningText(intent),
|
||||
status: 'running'
|
||||
}
|
||||
: event
|
||||
@@ -2100,9 +2149,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||
await updateAiDocumentQueryThinking(pendingMessage, thinkingEvents)
|
||||
|
||||
try {
|
||||
const payload = intent.source === 'approval'
|
||||
? await fetchApprovalExpenseClaims({ page: 1, pageSize: 100 })
|
||||
: await fetchExpenseClaims({ page: 1, pageSize: 100 })
|
||||
const payload = await fetchAiDocumentQueryPayload(intent)
|
||||
const rawCount = extractExpenseClaimItems(payload).length
|
||||
const filteredRecords = filterAiDocumentQueryRecords(payload, intent)
|
||||
thinkingEvents = completeAiDocumentQueryEvent(
|
||||
|
||||
Reference in New Issue
Block a user