2026-06-26 22:42:23 +08:00
|
|
|
import {
|
|
|
|
|
parseAiDocumentDetailHref
|
|
|
|
|
} from '../../utils/aiDocumentDetailReference.js'
|
|
|
|
|
|
2026-06-24 22:58:59 +08:00
|
|
|
const DRAFT_DELETION_ACTION_PATTERN = /删除|删掉|删了|移除|作废|撤销/
|
|
|
|
|
const DRAFT_DELETION_TARGET_PATTERN = (
|
|
|
|
|
/草稿|这个单据|这张单据|当前单据|当前申请|当前报销|刚才保存的草稿|刚才的草稿|上面的单据|最近的单据|申请单|报销单/
|
|
|
|
|
)
|
|
|
|
|
const NON_DRAFT_DELETE_TARGET_PATTERN = /附件|票据|发票|图片|文件|明细|费用行/
|
|
|
|
|
const DELETABLE_DRAFT_STATUS = new Set(['', 'draft', 'pending', '待提交', '草稿'])
|
|
|
|
|
const SUBMITTED_OR_FINAL_STATUS = new Set([
|
|
|
|
|
'submitted',
|
|
|
|
|
'approved',
|
|
|
|
|
'completed',
|
|
|
|
|
'paid',
|
|
|
|
|
'archived',
|
|
|
|
|
'deleted',
|
|
|
|
|
'rejected',
|
|
|
|
|
'returned',
|
|
|
|
|
'审批中',
|
|
|
|
|
'已审批',
|
|
|
|
|
'已完成',
|
|
|
|
|
'已付款',
|
|
|
|
|
'已归档',
|
|
|
|
|
'已删除',
|
|
|
|
|
'已驳回',
|
|
|
|
|
'已退回'
|
|
|
|
|
])
|
2026-06-26 22:42:23 +08:00
|
|
|
const DOCUMENT_DETAIL_LINK_RE = /<a\b[^>]*href="([^"]*#ai-open-document-detail:[^"]+)"[^>]*>(.*?)<\/a>/g
|
|
|
|
|
const DOCUMENT_COMMAND_ACTION_LABELS = {
|
|
|
|
|
delete: '删除',
|
|
|
|
|
approve: '审核通过',
|
|
|
|
|
reject: '驳回/退回'
|
|
|
|
|
}
|
|
|
|
|
const DOCUMENT_COMMAND_DETAIL_LABELS = {
|
|
|
|
|
delete: '进入详情确认删除',
|
|
|
|
|
approve: '进入详情确认审核',
|
|
|
|
|
reject: '进入详情确认驳回'
|
|
|
|
|
}
|
2026-06-24 22:58:59 +08:00
|
|
|
|
|
|
|
|
function normalizeCompactText(value = '') {
|
|
|
|
|
return String(value || '').replace(/\s+/g, '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 22:42:23 +08:00
|
|
|
function normalizeText(value = '') {
|
|
|
|
|
return String(value || '').replace(/\s+/g, ' ').trim()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 22:58:59 +08:00
|
|
|
function normalizeDraftDocumentType(payload = {}, claimNo = '') {
|
|
|
|
|
const rawType = String(payload.document_type || payload.documentType || payload.draft_type || payload.draftType || '').trim()
|
|
|
|
|
if (/application|expense_application|申请/.test(rawType)) {
|
|
|
|
|
return 'application'
|
|
|
|
|
}
|
|
|
|
|
if (/reimbursement|expense|报销/.test(rawType)) {
|
|
|
|
|
return 'reimbursement'
|
|
|
|
|
}
|
|
|
|
|
return /^A/i.test(String(claimNo || '').trim()) ? 'application' : 'reimbursement'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeDraftPayload(payload = null, sourceText = '') {
|
|
|
|
|
if (!payload || typeof payload !== 'object') {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const claimId = String(payload.claim_id || payload.claimId || payload.id || '').trim()
|
|
|
|
|
const claimNo = String(payload.claim_no || payload.claimNo || payload.document_no || payload.documentNo || '').trim()
|
|
|
|
|
if (!claimId && !claimNo) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const status = String(payload.status || payload.status_label || payload.statusLabel || '').trim()
|
|
|
|
|
if (SUBMITTED_OR_FINAL_STATUS.has(status.toLowerCase()) || SUBMITTED_OR_FINAL_STATUS.has(status)) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
if (!DELETABLE_DRAFT_STATUS.has(status.toLowerCase()) && !DELETABLE_DRAFT_STATUS.has(status) && !/草稿/.test(sourceText)) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
claimId,
|
|
|
|
|
claimNo,
|
|
|
|
|
status: status || 'draft',
|
|
|
|
|
documentType: normalizeDraftDocumentType(payload, claimNo)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractDraftPayloadFromSuggestedActions(message = {}) {
|
|
|
|
|
const actions = Array.isArray(message?.suggestedActions) ? message.suggestedActions : []
|
|
|
|
|
for (const action of [...actions].reverse()) {
|
|
|
|
|
const actionType = String(action?.action_type || action?.actionType || '').trim()
|
|
|
|
|
if (actionType !== 'open_application_detail') {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const payload = normalizeDraftPayload(action.payload, String(message.content || message.text || ''))
|
|
|
|
|
if (payload) {
|
|
|
|
|
return payload
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 22:42:23 +08:00
|
|
|
function stripHtml(value = '') {
|
|
|
|
|
return normalizeText(String(value || '').replace(/<[^>]*>/g, ''))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeDocumentCommandCandidate(detailReference = null, rawLabel = '') {
|
|
|
|
|
if (!detailReference || typeof detailReference !== 'object') {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const claimId = String(detailReference.claimId || detailReference.claim_id || '').trim()
|
|
|
|
|
const claimNo = String(detailReference.claimNo || detailReference.claim_no || detailReference.reference || '').trim()
|
|
|
|
|
if (!claimId && !claimNo) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
claimId,
|
|
|
|
|
claimNo,
|
|
|
|
|
documentType: normalizeDraftDocumentType(detailReference, claimNo),
|
|
|
|
|
actionLabel: stripHtml(rawLabel) || '查看详情'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractDocumentCommandCandidatesFromContent(content = '') {
|
|
|
|
|
const text = String(content || '')
|
|
|
|
|
const candidates = []
|
|
|
|
|
const seen = new Set()
|
|
|
|
|
for (const match of text.matchAll(DOCUMENT_DETAIL_LINK_RE)) {
|
|
|
|
|
const candidate = normalizeDocumentCommandCandidate(
|
|
|
|
|
parseAiDocumentDetailHref(match[1]),
|
|
|
|
|
match[2]
|
|
|
|
|
)
|
|
|
|
|
if (!candidate) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const key = `${candidate.claimId || ''}:${candidate.claimNo || ''}`
|
|
|
|
|
if (seen.has(key)) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen.add(key)
|
|
|
|
|
candidates.push(candidate)
|
|
|
|
|
}
|
|
|
|
|
return candidates
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function canReuseDocumentCommandContext(content = '', commandFrame = {}) {
|
|
|
|
|
const action = String(commandFrame?.action || '').trim()
|
|
|
|
|
if (!['approve', 'reject'].includes(action)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (String(commandFrame?.safetyLevel || '').trim() !== 'confirm_required') {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return /ai-document-card--approval-task|待我审核|待审|待审批|待审核|确认审核|进入详情确认审核/.test(String(content || ''))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 22:58:59 +08:00
|
|
|
export function isWorkbenchDraftDeletionIntent(prompt = '') {
|
|
|
|
|
const compact = normalizeCompactText(prompt)
|
|
|
|
|
if (!compact || !DRAFT_DELETION_ACTION_PATTERN.test(compact)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (NON_DRAFT_DELETE_TARGET_PATTERN.test(compact) && !/草稿|单据|申请单|报销单/.test(compact)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return DRAFT_DELETION_TARGET_PATTERN.test(compact)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveLatestWorkbenchDraftPayload(messages = []) {
|
|
|
|
|
const safeMessages = Array.isArray(messages) ? messages : []
|
|
|
|
|
for (const message of [...safeMessages].reverse()) {
|
|
|
|
|
const sourceText = String(message?.content || message?.text || '')
|
|
|
|
|
const actionPayload = extractDraftPayloadFromSuggestedActions(message)
|
|
|
|
|
if (actionPayload) {
|
|
|
|
|
return actionPayload
|
|
|
|
|
}
|
|
|
|
|
const draftPayload = normalizeDraftPayload(message?.draftPayload, sourceText)
|
|
|
|
|
if (draftPayload) {
|
|
|
|
|
return draftPayload
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 22:42:23 +08:00
|
|
|
export function resolveLatestWorkbenchDocumentCommandContext(messages = [], commandFrame = {}) {
|
|
|
|
|
const safeMessages = Array.isArray(messages) ? messages : []
|
|
|
|
|
for (const message of [...safeMessages].reverse()) {
|
|
|
|
|
if (String(message?.role || '').trim() !== 'assistant') {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const content = String(message?.content || message?.text || '')
|
|
|
|
|
if (!canReuseDocumentCommandContext(content, commandFrame)) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const candidates = extractDocumentCommandCandidatesFromContent(content)
|
|
|
|
|
if (!candidates.length) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
sourceMessageId: String(message?.id || '').trim(),
|
|
|
|
|
action: String(commandFrame?.action || '').trim(),
|
|
|
|
|
candidates
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 22:58:59 +08:00
|
|
|
export function buildWorkbenchDraftDeletionGuidance(draftPayload = {}) {
|
|
|
|
|
const claimNo = String(draftPayload.claimNo || draftPayload.claim_no || '').trim()
|
|
|
|
|
const claimId = String(draftPayload.claimId || draftPayload.claim_id || '').trim()
|
|
|
|
|
const documentType = String(draftPayload.documentType || draftPayload.document_type || 'reimbursement').trim()
|
|
|
|
|
const reference = claimNo || claimId || '最近这张草稿'
|
|
|
|
|
return {
|
|
|
|
|
content: [
|
|
|
|
|
'### 已识别到您想删除草稿',
|
|
|
|
|
`我找到了最近这张草稿:**${reference}**。`,
|
|
|
|
|
'删除草稿会影响单据和附件关联,我不会直接替您删除。请先打开详情页,在详情页点击 **删除草稿** 并完成二次确认。'
|
|
|
|
|
].join('\n\n'),
|
|
|
|
|
suggestedActions: [{
|
|
|
|
|
label: claimNo ? `查看草稿 ${claimNo}` : '查看草稿详情',
|
|
|
|
|
description: '打开详情页后可点击删除草稿并二次确认。',
|
|
|
|
|
icon: 'mdi mdi-open-in-new',
|
|
|
|
|
action_type: 'open_application_detail',
|
|
|
|
|
payload: {
|
|
|
|
|
claim_id: claimId,
|
|
|
|
|
claim_no: claimNo,
|
|
|
|
|
document_type: documentType
|
|
|
|
|
}
|
|
|
|
|
}]
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-26 22:42:23 +08:00
|
|
|
|
|
|
|
|
export function buildWorkbenchDocumentCommandFollowupGuidance(context = {}, commandFrame = {}) {
|
|
|
|
|
const action = String(commandFrame?.action || context?.action || '').trim()
|
|
|
|
|
const actionLabel = DOCUMENT_COMMAND_ACTION_LABELS[action] || '处理'
|
|
|
|
|
const detailLabel = DOCUMENT_COMMAND_DETAIL_LABELS[action] || '进入详情确认'
|
|
|
|
|
const candidates = Array.isArray(context?.candidates) ? context.candidates : []
|
|
|
|
|
const visibleCandidates = candidates.slice(0, 8)
|
|
|
|
|
const candidateLines = visibleCandidates.map((candidate, index) => {
|
|
|
|
|
const reference = candidate.claimNo || candidate.claimId || `候选 ${index + 1}`
|
|
|
|
|
return `${index + 1}. ${reference}`
|
|
|
|
|
})
|
|
|
|
|
const overflowText = candidates.length > visibleCandidates.length
|
|
|
|
|
? `\n\n还有 ${candidates.length - visibleCandidates.length} 张候选未展示,请先补充更具体条件。`
|
|
|
|
|
: ''
|
|
|
|
|
return {
|
|
|
|
|
content: [
|
|
|
|
|
'### 已接上刚才查询到的待审单据',
|
|
|
|
|
`您想继续执行 **${actionLabel}**。这属于高风险审批动作,我不会直接替您通过或驳回。`,
|
|
|
|
|
'请先从刚才的候选单据中选择一张,进入详情页核对风险、金额和审批节点后再确认。',
|
|
|
|
|
candidateLines.length ? candidateLines.join('\n') : '',
|
|
|
|
|
overflowText
|
|
|
|
|
].filter(Boolean).join('\n\n'),
|
|
|
|
|
suggestedActions: visibleCandidates.map((candidate) => {
|
|
|
|
|
const reference = candidate.claimNo || candidate.claimId || '单据'
|
|
|
|
|
return {
|
|
|
|
|
label: `${detailLabel} ${reference}`,
|
|
|
|
|
description: '打开详情页核对后,再完成审批确认。',
|
|
|
|
|
icon: 'mdi mdi-open-in-new',
|
|
|
|
|
action_type: 'open_application_detail',
|
|
|
|
|
payload: {
|
|
|
|
|
claim_id: candidate.claimId,
|
|
|
|
|
claim_no: candidate.claimNo,
|
|
|
|
|
document_type: candidate.documentType,
|
|
|
|
|
command_action: action
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|