From 304bbe1fd41e4d2186a50e2031e62653659c678f Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Sat, 20 Jun 2026 10:17:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0=20AI?= =?UTF-8?q?=20=E6=A8=A1=E5=BC=8F=E6=8A=A5=E9=94=80=E9=A2=84=E5=AE=A1?= =?UTF-8?q?=E4=B8=8E=E6=96=87=E6=A1=A3=E6=9F=A5=E8=AF=A2=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 aiApplicationPrecheckModel/aiDocumentQueryModel/aiApplicationPreviewActions/aiConversationHtmlRenderer 四个独立模型与服务,按职责从主组件拆出 - PersonalWorkbenchAiMode 接入拆分后的预审、文档查询与 HTML 渲染逻辑,配合 markdown 工具增强结构化展示 - 文档中心与归档筛选、风险可见性、申请预览等工具同步适配,补充对应单元测试 - 新增 AI 文档卡片背景资源 --- web/src/assets/ai-document-card-bg.png | Bin 0 -> 98846 bytes .../components/personal-workbench-ai-mode.css | 715 +++++++++++++++- .../business/PersonalWorkbenchAiMode.vue | 531 ++++++++++-- .../services/aiApplicationPreviewActions.js | 136 +++ web/src/utils/aiApplicationPrecheckModel.js | 345 ++++++++ web/src/utils/aiConversationHtmlRenderer.js | 647 +++++++++++++++ web/src/utils/aiDocumentQueryModel.js | 784 ++++++++++++++++++ web/src/utils/archiveCenterListFilters.js | 29 +- web/src/utils/expenseApplicationPreview.js | 26 +- web/src/utils/markdown.js | 97 ++- web/src/utils/riskVisibility.js | 10 +- web/src/views/DocumentsCenterView.vue | 12 +- web/src/views/PersonalWorkbenchView.vue | 1 + .../views/scripts/TravelRequestDetailView.js | 23 +- .../scripts/useApplicationPreviewEditor.js | 1 + .../ai-application-precheck-model.test.mjs | 114 +++ .../ai-application-preview-actions.test.mjs | 127 +++ .../ai-conversation-html-renderer.test.mjs | 100 +++ web/tests/ai-document-query-model.test.mjs | 181 ++++ .../archive-center-list-filters.test.mjs | 35 +- .../documents-center-status-filter.test.mjs | 2 +- .../expense-application-fast-preview.test.mjs | 29 +- web/tests/risk-visibility.test.mjs | 84 ++ ...travel-request-detail-risk-advice.test.mjs | 4 +- ...ench-ai-mode-expense-scene-action.test.mjs | 37 +- web/tests/workbench-ai-mode-switch.test.mjs | 21 +- 26 files changed, 3974 insertions(+), 117 deletions(-) create mode 100644 web/src/assets/ai-document-card-bg.png create mode 100644 web/src/services/aiApplicationPreviewActions.js create mode 100644 web/src/utils/aiApplicationPrecheckModel.js create mode 100644 web/src/utils/aiConversationHtmlRenderer.js create mode 100644 web/src/utils/aiDocumentQueryModel.js create mode 100644 web/tests/ai-application-precheck-model.test.mjs create mode 100644 web/tests/ai-application-preview-actions.test.mjs create mode 100644 web/tests/ai-conversation-html-renderer.test.mjs create mode 100644 web/tests/ai-document-query-model.test.mjs diff --git a/web/src/assets/ai-document-card-bg.png b/web/src/assets/ai-document-card-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..74a7e0c08d9b8a0a4b0bb6e12b6aa35d423dfcf1 GIT binary patch literal 98846 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 OR(o*je( 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�sDrP2~Dmt0X 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

}o6{*8()2mFF!$0Z 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 DPr<)H literal 0 HcmV?d00001 diff --git a/web/src/assets/styles/components/personal-workbench-ai-mode.css b/web/src/assets/styles/components/personal-workbench-ai-mode.css index c5b120c..f89cd42 100644 --- a/web/src/assets/styles/components/personal-workbench-ai-mode.css +++ b/web/src/assets/styles/components/personal-workbench-ai-mode.css @@ -928,6 +928,11 @@ letter-spacing: 0; } +.workbench-ai-answer-markdown :deep(.ai-html-flow) { + display: grid; + gap: 16px; +} + .workbench-ai-answer-markdown :deep(h1), .workbench-ai-answer-markdown :deep(h2), .workbench-ai-answer-markdown :deep(h3), @@ -987,7 +992,331 @@ color: #475569; } -.workbench-ai-answer-markdown :deep(.markdown-table-wrap) { +.workbench-ai-answer-markdown :deep(.ai-html-callout) { + margin: 0; + padding: 14px 16px; + border-left: 3px solid rgba(37, 99, 235, 0.5); + border-radius: 12px; + background: rgba(239, 246, 255, 0.62); + color: #475569; +} + +.workbench-ai-answer-markdown :deep(.ai-html-focus-grid) { + display: grid; + gap: 0; + margin: 2px 0 18px; + padding-left: 22px; + border-left: 3px solid rgba(96, 165, 250, 0.66); +} + +.workbench-ai-answer-markdown :deep(.ai-html-focus-card) { + padding: 11px 0 16px; + border: 0; + border-radius: 0; + background: transparent; +} + +.workbench-ai-answer-markdown :deep(.ai-html-focus-card + .ai-html-focus-card) { + border-top: 1px solid rgba(226, 232, 240, 0.92); +} + +.workbench-ai-answer-markdown :deep(.ai-html-focus-label) { + display: block; + margin-bottom: 4px; + color: #1d4ed8; + font-size: 15px; + font-weight: 900; +} + +.workbench-ai-answer-markdown :deep(.ai-html-focus-card p) { + color: #475569; + font-size: 16px; + font-weight: 650; + line-height: 1.72; +} + +.workbench-ai-answer-markdown :deep(.ai-html-steps), +.workbench-ai-answer-markdown :deep(.ai-html-list) { + display: grid; + gap: 12px; + margin: 0; + padding: 0; + list-style: none; +} + +.workbench-ai-answer-markdown :deep(.ai-html-steps li) { + display: grid; + grid-template-columns: 34px minmax(0, 1fr); + gap: 16px; + align-items: start; +} + +.workbench-ai-answer-markdown :deep(.ai-html-step-index) { + width: 34px; + min-height: 28px; + display: grid; + align-items: start; + justify-items: start; + padding-top: 1px; + border-radius: 0; + background: transparent; + color: #1d4ed8; + font-size: 17px; + font-weight: 900; + line-height: 1.45; +} + +.workbench-ai-answer-markdown :deep(.ai-html-step-copy) { + display: grid; + gap: 5px; +} + +.workbench-ai-answer-markdown :deep(.ai-html-step-copy > strong) { + color: #0f172a; + font-size: 17px; + line-height: 1.45; +} + +.workbench-ai-answer-markdown :deep(.ai-html-step-copy > p) { + color: #475569; + font-size: 16px; + font-weight: 620; + line-height: 1.72; +} + +.workbench-ai-answer-markdown :deep(.ai-html-list:not(.ai-html-steps)) { + padding-left: 18px; + list-style: disc; +} + +.workbench-ai-answer-markdown :deep(.ai-html-list--ordered) { + padding-left: 22px; + list-style: decimal; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card-list) { + display: grid; + gap: 12px; + margin-top: 18px; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card) { + position: relative; + display: grid; + gap: 12px; + padding: 16px 18px; + border: 1px solid rgba(226, 232, 240, 0.9); + border-left: 3px solid #cbd5e1; + border-radius: 12px; + background: #ffffff; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04); + color: #334155; + animation: workbenchDocumentCardReveal 360ms cubic-bezier(0.2, 0.8, 0.2, 1) both; + transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card:hover) { + border-color: rgba(148, 163, 184, 0.7); + box-shadow: 0 2px 4px rgba(15, 23, 42, 0.05), 0 8px 20px rgba(15, 23, 42, 0.07); + transform: translateY(-1px); +} + +.workbench-ai-answer-markdown :deep(.ai-document-card:nth-child(2)) { + animation-delay: 40ms; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card:nth-child(3)) { + animation-delay: 80ms; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card:nth-child(4)) { + animation-delay: 120ms; +} + +/* 状态语义色:左侧边条颜色随状态变化,一眼判断当前阶段 */ +.workbench-ai-answer-markdown :deep(.ai-document-card.is-pending) { + border-left-color: #2563eb; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card.is-success) { + border-left-color: #16a34a; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning) { + border-left-color: #d97706; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger) { + border-left-color: #dc2626; +} + +/* 卡片头部:状态 + 类型(左) · 单据编号(右) */ +.workbench-ai-answer-markdown :deep(.ai-document-card__head) { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__head-left) { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex-wrap: wrap; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__status) { + display: inline-flex; + align-items: center; + min-height: 22px; + padding: 0 9px; + border-radius: 6px; + background: rgba(148, 163, 184, 0.16); + color: #475569; + font-size: 12px; + font-weight: 700; + line-height: 1.2; + white-space: nowrap; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card.is-pending .ai-document-card__status) { + background: rgba(37, 99, 235, 0.1); + color: #1d4ed8; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__status) { + background: rgba(22, 163, 74, 0.1); + color: #15803d; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__status) { + background: rgba(217, 119, 6, 0.1); + color: #b45309; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger .ai-document-card__status) { + background: rgba(220, 38, 38, 0.1); + color: #b91c1c; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__type) { + color: #64748b; + font-size: 12px; + font-weight: 500; + line-height: 1.3; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__number) { + flex: 0 0 auto; + color: #94a3b8; + font-size: 12px; + font-weight: 500; + line-height: 1.3; + overflow-wrap: anywhere; +} + +/* 卡片主体:事由(主焦点) + 申请人/部门(次焦点) */ +.workbench-ai-answer-markdown :deep(.ai-document-card__body) { + display: grid; + gap: 6px; + min-width: 0; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__reason) { + display: -webkit-box; + color: #0f172a; + font-size: 16px; + font-weight: 700; + line-height: 1.45; + overflow: hidden; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__owner-line) { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + min-width: 0; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__owner) { + color: #1e293b; + font-size: 13px; + font-weight: 600; + line-height: 1.3; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__dept) { + color: #64748b; + font-size: 13px; + font-weight: 500; + line-height: 1.3; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__dot) { + color: #cbd5e1; + font-size: 12px; + font-weight: 700; +} + +/* 卡片底部:辅助元信息(左) · 金额(右) · 操作 */ +.workbench-ai-answer-markdown :deep(.ai-document-card__foot) { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-top: 12px; + border-top: 1px solid rgba(226, 232, 240, 0.9); +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__meta) { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + min-width: 0; + flex: 1 1 auto; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__meta-item) { + color: #64748b; + font-size: 12px; + font-weight: 500; + line-height: 1.3; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__amount-block) { + display: grid; + justify-items: end; + gap: 1px; + flex: 0 0 auto; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__amount-label) { + color: #94a3b8; + font-size: 11px; + font-weight: 500; + line-height: 1.2; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__amount) { + color: #0f172a; + font-size: 17px; + font-weight: 700; + line-height: 1.2; + white-space: nowrap; +} + +.workbench-ai-answer-markdown :deep(.ai-document-card__action) { + flex: 0 0 auto; +} + +.workbench-ai-answer-markdown :deep(.markdown-table-wrap), +.workbench-ai-answer-markdown :deep(.ai-html-table-wrap) { overflow-x: auto; margin-top: 18px; border: 1px solid rgba(226, 232, 240, 0.9); @@ -1013,6 +1342,384 @@ font-weight: 850; } +.workbench-ai-answer-markdown :deep(.ai-html-image-frame) { + margin: 0; + overflow: hidden; + border: 1px solid rgba(226, 232, 240, 0.9); + border-radius: 16px; + background: rgba(248, 250, 252, 0.74); +} + +.workbench-ai-answer-markdown :deep(.ai-html-image), +.workbench-ai-answer-markdown :deep(.ai-html-inline-image) { + max-width: 100%; + height: auto; + display: block; +} + +.workbench-ai-answer-markdown :deep(.ai-html-image) { + width: 100%; + object-fit: contain; +} + +.workbench-ai-answer-markdown :deep(.ai-html-inline-image) { + max-height: 220px; + margin: 8px 0; + border-radius: 12px; +} + +.workbench-ai-answer-markdown :deep(.ai-html-image-caption) { + display: block; + padding: 8px 12px; + color: #64748b; + font-size: 12px; + font-weight: 700; +} + +.workbench-ai-answer-markdown :deep(.markdown-action-link), +.workbench-ai-answer-markdown :deep(.ai-html-action-link) { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + background: rgba(37, 99, 235, 0.1); + color: #1d4ed8; + font-size: 13px; + font-weight: 850; + line-height: 1.2; + text-decoration: none; + white-space: nowrap; +} + +.workbench-ai-answer-markdown :deep(.markdown-action-link:hover), +.workbench-ai-answer-markdown :deep(.ai-html-action-link:hover) { + background: rgba(37, 99, 235, 0.16); + color: #1e40af; +} + +@keyframes workbenchDocumentCardReveal { + from { + opacity: 0; + transform: translateY(10px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@media (prefers-reduced-motion: reduce) { + .workbench-ai-answer-markdown :deep(.ai-document-card) { + animation: none; + transition: none; + } +} + +@media (max-width: 720px) { + .workbench-ai-answer-markdown :deep(.ai-document-card) { + padding: 14px; + } + + .workbench-ai-answer-markdown :deep(.ai-document-card__head) { + align-items: flex-start; + } + + .workbench-ai-answer-markdown :deep(.ai-document-card__number) { + flex-basis: 100%; + text-align: left; + } + + .workbench-ai-answer-markdown :deep(.ai-document-card__foot) { + flex-wrap: wrap; + } + + .workbench-ai-answer-markdown :deep(.ai-document-card__amount-block) { + justify-items: start; + order: 2; + } + + .workbench-ai-answer-markdown :deep(.ai-document-card__action) { + order: 3; + margin-left: auto; + } +} + +.workbench-ai-application-preview { + min-width: 0; + display: grid; + gap: 16px; + margin-top: 18px; +} + +.structured-card-reveal-enter-active, +.structured-card-reveal-leave-active { + transition: + opacity 260ms cubic-bezier(0.2, 0.8, 0.2, 1), + transform 260ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.structured-card-reveal-enter-from, +.structured-card-reveal-leave-to { + opacity: 0; + transform: translateY(10px) scale(0.99); +} + +.structured-card-reveal-enter-to, +.structured-card-reveal-leave-from { + opacity: 1; + transform: translateY(0) scale(1); +} + +.application-preview-table { + display: grid; + overflow: hidden; + border: 1px solid rgba(191, 219, 254, 0.72); + border-radius: 14px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.96)), + #ffffff; + box-shadow: + 0 16px 34px rgba(15, 23, 42, 0.07), + inset 0 1px 0 rgba(255, 255, 255, 0.98); + color: #334155; + font-size: 15px; +} + +.application-preview-row { + position: relative; + display: grid; + grid-template-columns: 148px minmax(0, 1fr); + min-height: 48px; + border-top: 1px solid rgba(226, 232, 240, 0.96); +} + +.structured-card-reveal-enter-active .application-preview-row { + animation: workbenchApplicationRowReveal 260ms cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.structured-card-reveal-enter-active .application-preview-row:nth-child(2) { + animation-delay: 35ms; +} + +.structured-card-reveal-enter-active .application-preview-row:nth-child(3) { + animation-delay: 70ms; +} + +.structured-card-reveal-enter-active .application-preview-row:nth-child(4) { + animation-delay: 105ms; +} + +.structured-card-reveal-enter-active .application-preview-row:nth-child(5) { + animation-delay: 140ms; +} + +.structured-card-reveal-enter-active .application-preview-row:nth-child(n + 6) { + animation-delay: 165ms; +} + +.application-preview-row:first-child { + border-top: 0; +} + +.application-preview-row.head { + min-height: 42px; + background: linear-gradient(180deg, rgba(239, 246, 255, 0.92), rgba(248, 250, 252, 0.98)); + color: #334155; + font-size: 13px; + font-weight: 900; +} + +.application-preview-row > span { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + padding: 10px 16px; +} + +.application-preview-label { + border-right: 1px solid rgba(226, 232, 240, 0.96); + background: rgba(248, 250, 252, 0.72); + color: #475569; + font-weight: 820; +} + +.application-preview-value { + position: relative; + color: #0f172a; + font-weight: 700; +} + +.application-preview-row.editable { + cursor: pointer; +} + +.application-preview-row.editable:hover, +.application-preview-row.editable:hover .application-preview-label, +.application-preview-row.editable:hover .application-preview-value { + background: rgba(239, 246, 255, 0.58); +} + +.application-preview-row.editable:focus-visible { + z-index: 1; + outline: 2px solid rgba(37, 99, 235, 0.42); + outline-offset: -2px; +} + +.application-preview-row.highlight .application-preview-label { + background: rgba(219, 234, 254, 0.76); + color: #1d4ed8; +} + +.application-preview-row.highlight .application-preview-value { + background: rgba(219, 234, 254, 0.44); + color: #1e40af; + font-weight: 850; +} + +.application-preview-row.missing { + background: rgba(37, 99, 235, 0.035); + box-shadow: inset 3px 0 0 rgba(37, 99, 235, 0.5); +} + +.application-preview-row.missing .application-preview-label { + background: rgba(219, 234, 254, 0.78); + color: #1d4ed8; + font-weight: 900; +} + +.application-preview-row.missing .application-preview-value { + background: rgba(239, 246, 255, 0.74); + font-weight: 850; +} + +.application-preview-text { + min-width: 0; + overflow-wrap: anywhere; + line-height: 1.48; +} + +.application-preview-input { + width: 100%; + min-width: 0; + min-height: 34px; + padding: 0 10px; + border: 1px solid rgba(37, 99, 235, 0.46); + border-radius: 8px; + outline: none; + background: #ffffff; + color: #0f172a; + font: inherit; + font-weight: 720; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.11); +} + +.application-preview-select { + cursor: pointer; +} + +.application-preview-edit-btn { + flex: 0 0 auto; + width: 28px; + height: 28px; + display: inline-grid; + place-items: center; + border: 1px solid rgba(37, 99, 235, 0.18); + border-radius: 8px; + background: rgba(239, 246, 255, 0.92); + color: #1d4ed8; + cursor: pointer; + opacity: 0; + transition: + opacity 160ms ease, + border-color 160ms ease, + background 160ms ease, + transform 160ms ease; +} + +.application-preview-edit-btn i { + font-size: 15px; +} + +.application-preview-row:hover .application-preview-edit-btn, +.application-preview-edit-btn:focus-visible { + opacity: 1; +} + +.application-preview-edit-btn:hover, +.application-preview-edit-btn:focus-visible { + border-color: rgba(37, 99, 235, 0.38); + background: rgba(219, 234, 254, 0.98); + transform: translateY(-1px); +} + +.application-preview-footer { + color: #334155; + font-size: 15px; + font-weight: 720; + line-height: 1.78; +} + +.application-preview-footer.workbench-ai-answer-markdown { + margin-top: 0; +} + +.application-preview-footer-missing { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 6px; + padding: 2px 0 0; + color: #334155; + font-size: 15px; + font-weight: 760; + line-height: 1.75; +} + +.application-preview-missing-prefix, +.application-preview-missing-suffix { + color: #334155; + font-weight: 850; +} + +.application-preview-missing-list { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; +} + +.application-preview-missing-chip { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 8px; + border-radius: 8px; + background: rgba(37, 99, 235, 0.1); + color: #1d4ed8; + font-size: 13px; + font-weight: 900; +} + +.application-preview-missing-separator { + color: #1d4ed8; + font-weight: 820; +} + +@keyframes workbenchApplicationRowReveal { + from { + opacity: 0; + transform: translateY(6px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + .workbench-ai-suggested-actions { display: flex; flex-wrap: wrap; @@ -1445,10 +2152,16 @@ .workbench-ai-panel-swap-leave-active, .workbench-ai-thinking-collapse-enter-active, .workbench-ai-thinking-collapse-leave-active, + .structured-card-reveal-enter-active, + .structured-card-reveal-leave-active, .workbench-ai-confirm-fade-enter-active, .workbench-ai-confirm-fade-leave-active, .workbench-ai-confirm-fade-enter-active .workbench-ai-confirm-dialog, .workbench-ai-confirm-fade-leave-active .workbench-ai-confirm-dialog { transition: none; } + + .structured-card-reveal-enter-active .application-preview-row { + animation: none; + } } diff --git a/web/src/components/business/PersonalWorkbenchAiMode.vue b/web/src/components/business/PersonalWorkbenchAiMode.vue index 312f415..d56e249 100644 --- a/web/src/components/business/PersonalWorkbenchAiMode.vue +++ b/web/src/components/business/PersonalWorkbenchAiMode.vue @@ -303,10 +303,121 @@

-
+ +
+
+
+ 字段 + 内容 +
+
+ {{ row.label }} + + + + + +
+
+ + + +
+
+ +
小财管家正在识别任务、拆解流程并准备下一步建议...
@@ -523,7 +634,7 @@ import { loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation } from '../../utils/aiWorkbenchConversationStore.js' -import { renderMarkdown } from '../../utils/markdown.js' +import { renderAiConversationHtml } from '../../utils/aiConversationHtmlRenderer.js' import { mergeComposerPrefill, resolveSuggestedActionPrefill @@ -549,25 +660,39 @@ import { isAiExpenseDraftComplete } from '../../utils/aiExpenseDraftModel.js' import { - applyAiApplicationAnswer, - buildAiApplicationStepPrompt, - buildAiApplicationSummary, - createAiApplicationDraft, - isAiApplicationDraftComplete -} from '../../utils/aiApplicationDraftModel.js' + buildApplicationPreviewFooterMessage, + buildApplicationPreviewRows, + buildApplicationTemplatePreview, + buildLocalApplicationPreview, + buildLocalApplicationPreviewMessage, + normalizeApplicationPreview +} from '../../utils/expenseApplicationPreview.js' +import { useApplicationPreviewEditor } from '../../views/scripts/useApplicationPreviewEditor.js' +import { + buildAiDocumentQueryConditionSummary, + buildAiDocumentQueryMessage, + filterAiDocumentQueryRecords, + resolveAiDocumentQueryIntent +} from '../../utils/aiDocumentQueryModel.js' import { buildRequiredApplicationActions, buildRequiredApplicationMissingText, buildRequiredApplicationSelectionText, filterRequiredApplicationCandidates } from '../../views/scripts/travelReimbursementApplicationLinkModel.js' -import { fetchExpenseClaims } from '../../services/reimbursements.js' +import { + calculateTravelReimbursement, + extractExpenseClaimItems, + fetchApprovalExpenseClaims, + fetchExpenseClaims +} from '../../services/reimbursements.js' const props = defineProps({ sidebarCommand: { type: Object, default: () => ({ seq: 0, type: '', payload: null }) } }) -const emit = defineEmits(['conversation-change', 'conversation-history-change']) +const emit = defineEmits(['conversation-change', 'conversation-history-change', 'open-document']) +const AI_DOCUMENT_QUERY_STEP_DELAY_MS = 320 const { currentUser } = useSystemState() const { toast } = useToast() const assistantDraft = ref('') @@ -584,7 +709,6 @@ const activeConversationTitle = ref('') const sending = ref(false) const stewardState = ref(null) const aiExpenseDraft = ref(null) -const aiApplicationDraft = ref(null) const thinkingExpandedMessageIds = ref(new Set()) const thinkingCollapsedMessageIds = ref(new Set()) const deleteDialogOpen = ref(false) @@ -594,6 +718,24 @@ 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 { + applicationPreviewEditor, + resolveApplicationPreviewEditorControl, + resolveApplicationPreviewEditorOptions, + refreshApplicationPreviewEstimate, + isApplicationPreviewEditing, + openApplicationPreviewEditor, + commitApplicationPreviewEditor, + cancelApplicationPreviewEditor, + handleApplicationPreviewEditorKeydown +} = useApplicationPreviewEditor({ + persistSessionState: () => persistCurrentConversation(), + toast, + calculateTravelReimbursement, + currentUser +}) const { workbenchDatePickerOpen, @@ -753,6 +895,8 @@ function createInlineMessage(role, content, options = {}) { feedback: String(options.feedback || ''), stewardPlan: options.stewardPlan || null, suggestedActions: Array.isArray(options.suggestedActions) ? options.suggestedActions : [], + applicationPreview: options.applicationPreview || null, + text: options.text || normalizedContent, createdAt: options.createdAt || Date.now() } } @@ -807,7 +951,9 @@ function normalizeRuntimeMessage(message = {}) { pending: false, feedback: message.feedback || '', stewardPlan: message.stewardPlan || null, - suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [] + suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [], + applicationPreview: message.applicationPreview || null, + text: message.text || message.content || '' }) } @@ -816,9 +962,11 @@ function serializeRuntimeMessage(message = {}) { id: message.id, role: message.role, content: message.content, + text: message.text || message.content || '', feedback: message.feedback || '', stewardPlan: message.stewardPlan || null, - suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [] + suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [], + applicationPreview: message.applicationPreview || null } } @@ -887,8 +1035,59 @@ function activateInlineConversation(options = {}) { emit('conversation-change', { id: conversationId.value, title: activeConversationTitle.value }) } -function renderInlineMarkdown(content) { - return renderMarkdown(content) +function renderInlineConversationHtml(content) { + return renderAiConversationHtml(content) +} + +function resolveInlineApplicationPreviewRows(message) { + return buildApplicationPreviewRows(message?.applicationPreview || {}) +} + +function resolveInlineApplicationPreviewMissingFields(message) { + return normalizeApplicationPreview(message?.applicationPreview || {}).missingFields || [] +} + +function resolveInlineApplicationPreviewEditorControl(fieldKey) { + const control = resolveApplicationPreviewEditorControl(fieldKey) + return control === 'date' ? 'text' : control +} + +function syncInlineApplicationPreviewMessageContent(message) { + if (!message?.applicationPreview) { + return + } + const nextContent = buildLocalApplicationPreviewMessage(message.applicationPreview) + message.content = nextContent + message.text = nextContent +} + +async function commitInlineApplicationPreviewEditor(message) { + const committed = await commitApplicationPreviewEditor(message) + syncInlineApplicationPreviewMessageContent(message) + persistCurrentConversation() + return committed +} + +function handleInlineApplicationPreviewEditorKeydown(event, message) { + if (event.key === 'Enter') { + event.preventDefault() + void commitInlineApplicationPreviewEditor(message) + return + } + if (event.key === 'Escape') { + event.preventDefault() + cancelApplicationPreviewEditor() + return + } + handleApplicationPreviewEditorKeydown(event, message) +} + +function buildInlineApplicationPreviewFooterText(message) { + const normalized = normalizeApplicationPreview(message?.applicationPreview || {}) + if (normalized.validationIssues?.length || normalized.missingFields?.length) { + return buildApplicationPreviewFooterMessage(normalized) + } + return '申请核对表已补齐,费用测算已同步。你仍可点击表格继续修改;确认无误后,可以回复“保存草稿”或“提交申请”。' } function resolveInlineThinkingEvents(message) { @@ -1033,18 +1232,17 @@ function buildAiRequiredApplicationGateAutoMessage(normalizedPlan, flow) { return baseText } -function continueAiRequiredApplicationGateFromPlan(normalizedPlan) { +function continueAiRequiredApplicationGateFromPlan(normalizedPlan, prompt = '') { const flow = resolveRequiredApplicationGateContinuationFlow(normalizedPlan) if (!flow) { return false } if (flow.flowId === 'travel_application') { aiExpenseDraft.value = null - startAiApplicationDraft('travel', '差旅费') + void startAiApplicationPreview('travel', '差旅费', prompt) return true } if (flow.flowId === 'travel_reimbursement') { - aiApplicationDraft.value = null startAiExpenseDraft('travel', '差旅费', true) return true } @@ -1125,6 +1323,191 @@ 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 buildAiDocumentDetailRequest(detailReference = {}) { + const reference = String(detailReference.reference || '').trim() + const isApplication = /^APP?-/i.test(reference) + return { + id: reference, + claimId: reference, + claimNo: reference, + documentNo: reference, + documentType: isApplication ? 'application' : 'reimbursement', + documentTypeCode: isApplication ? 'application' : 'reimbursement', + source: 'workbench', + returnTo: 'workbench' + } +} + +function handleAiAnswerMarkdownClick(event) { + const target = event?.target + const link = target?.closest?.('a[href^="#ai-open-document-detail:"]') + if (!link) { + return + } + const detailReference = parseAiDocumentDetailHref(link.getAttribute('href')) + if (!detailReference) { + return + } + event.preventDefault() + event.stopPropagation() + emit('open-document', buildAiDocumentDetailRequest(detailReference)) +} + +function waitForAiDocumentQueryStep() { + return new Promise((resolve) => { + globalThis.setTimeout(resolve, AI_DOCUMENT_QUERY_STEP_DELAY_MS) + }) +} + +async function updateAiDocumentQueryThinking(pendingMessage, thinkingEvents, streamStatus = 'streaming') { + const message = conversationMessages.value.find((item) => item.id === pendingMessage.id) || pendingMessage + message.stewardPlan = { + ...(message.stewardPlan || {}), + streamStatus, + thinkingEvents + } + scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) + await nextTick() +} + +function completeAiDocumentQueryEvent(events, eventId, content = '') { + return events.map((event) => ( + event.eventId === eventId + ? { + ...event, + content: content || event.content, + status: 'completed' + } + : event + )) +} + +function failAiDocumentQueryEvents(events) { + return events.map((event) => ({ + ...event, + status: event.status === 'completed' ? 'completed' : 'failed' + })) +} + +async function handleAiDocumentQueryIntent(prompt, pendingMessage) { + const intent = resolveAiDocumentQueryIntent(prompt) + if (!intent) { + return false + } + + const conditionSummary = buildAiDocumentQueryConditionSummary(intent) + let thinkingEvents = [ + { + eventId: 'document-query-parse', + title: '解析自然语言筛选条件', + content: `正在从你的问题里提取查询来源、单据类型、时间、状态、费用类型、关键词和金额条件。当前识别:${conditionSummary}。`, + status: 'running' + }, + { + eventId: 'document-query-fetch', + title: '查询业务单据接口', + content: intent.source === 'approval' ? '等待调用待我审核单据接口。' : '等待调用我名下单据接口。', + status: 'pending' + }, + { + eventId: 'document-query-filter', + title: '组合筛选单据', + content: '等待接口返回后,再按已识别条件做二次筛选。', + status: 'pending' + } + ] + await updateAiDocumentQueryThinking(pendingMessage, thinkingEvents) + await waitForAiDocumentQueryStep() + + thinkingEvents = completeAiDocumentQueryEvent(thinkingEvents, 'document-query-parse') + thinkingEvents = thinkingEvents.map((event) => ( + event.eventId === 'document-query-fetch' + ? { + ...event, + content: intent.source === 'approval' + ? '正在查询待我审核的单据,接口范围为待办/待审单据列表。' + : '正在查询我名下的单据,接口范围为当前用户可见单据列表。', + status: 'running' + } + : event + )) + await updateAiDocumentQueryThinking(pendingMessage, thinkingEvents) + + try { + const payload = intent.source === 'approval' + ? await fetchApprovalExpenseClaims({ page: 1, pageSize: 100 }) + : await fetchExpenseClaims({ page: 1, pageSize: 100 }) + const rawCount = extractExpenseClaimItems(payload).length + const filteredRecords = filterAiDocumentQueryRecords(payload, intent) + thinkingEvents = completeAiDocumentQueryEvent( + thinkingEvents, + 'document-query-fetch', + `接口返回 ${rawCount} 张候选单据,开始按自然语言条件筛选。` + ) + thinkingEvents = thinkingEvents.map((event) => ( + event.eventId === 'document-query-filter' + ? { + ...event, + content: `按“${conditionSummary}”组合筛选,当前命中 ${filteredRecords.length} 张。`, + status: 'running' + } + : event + )) + await updateAiDocumentQueryThinking(pendingMessage, thinkingEvents) + await waitForAiDocumentQueryStep() + + const finalMessageText = buildAiDocumentQueryMessage(intent, payload) + thinkingEvents = completeAiDocumentQueryEvent( + thinkingEvents, + 'document-query-filter', + `筛选完成,命中 ${filteredRecords.length} 张单据,已生成卡片结果。` + ) + replaceInlineMessage( + pendingMessage.id, + createInlineMessage('assistant', finalMessageText, { + id: pendingMessage.id, + stewardPlan: { + streamStatus: 'completed', + thinkingEvents + }, + suggestedActions: [] + }) + ) + } catch (error) { + const finalMessageText = error?.message || '查询单据时出现异常,请稍后再试。' + replaceInlineMessage( + pendingMessage.id, + createInlineMessage('assistant', finalMessageText, { + id: pendingMessage.id, + stewardPlan: { + streamStatus: 'failed', + thinkingEvents: failAiDocumentQueryEvents(thinkingEvents) + } + }) + ) + } + + persistCurrentConversation() + return true +} + async function requestInlineAssistantReply(prompt, entry = {}, files = []) { let shouldAutoScrollOnFinish = true const pendingMessage = createInlineMessage('assistant', '', { @@ -1145,6 +1528,11 @@ async function requestInlineAssistantReply(prompt, entry = {}, files = []) { scrollInlineConversationToBottom() try { + if (await handleAiDocumentQueryIntent(prompt, pendingMessage)) { + shouldAutoScrollOnFinish = inlineConversationAutoScrollPinned.value + return + } + const planRequest = buildStewardPlanRequest({ rawText: prompt, files, @@ -1201,7 +1589,7 @@ async function requestInlineAssistantReply(prompt, entry = {}, files = []) { suggestedActions: requiredApplicationContinuationFlow ? [] : buildStewardSuggestedActions(plan) }) ) - if (continueAiRequiredApplicationGateFromPlan(normalizedPlan)) { + if (continueAiRequiredApplicationGateFromPlan(normalizedPlan, prompt)) { shouldAutoScrollOnFinish = inlineConversationAutoScrollPinned.value } persistCurrentConversation() @@ -1243,11 +1631,6 @@ function startInlineConversation(prompt, entry = {}, files = []) { return } - if (aiApplicationDraft.value && !isAiApplicationDraftComplete(aiApplicationDraft.value)) { - advanceAiApplicationDraft(cleanPrompt, files) - return - } - if (conversationId.value === AI_SEARCH_CONVERSATION_ID) { conversationId.value = '' conversationMessages.value = [] @@ -1362,7 +1745,11 @@ function handleInlineSuggestedAction(action = {}) { aiExpenseDraft.value = null const expenseType = String(actionPayload.expense_type || 'travel').trim() || 'travel' const expenseTypeLabel = String(actionPayload.expense_type_label || '差旅费').trim() || '差旅费' - startAiApplicationDraft(expenseType, expenseTypeLabel) + void startAiApplicationPreview( + expenseType, + expenseTypeLabel, + actionPayload.carry_text || resolveLatestInlineUserPrompt() + ) return } if (actionType === 'select_expense_type') { @@ -1382,7 +1769,11 @@ function handleInlineSuggestedAction(action = {}) { aiExpenseDraft.value = null const expenseType = String(action?.payload?.expense_type || '').trim() const expenseTypeLabel = String(action?.payload?.expense_type_label || action?.label || '').trim() - startAiApplicationDraft(expenseType, expenseTypeLabel) + void startAiApplicationPreview( + expenseType, + expenseTypeLabel, + action?.payload?.carry_text || resolveLatestInlineUserPrompt() + ) return } @@ -1423,6 +1814,46 @@ function pushInlineUserMessage(text) { conversationMessages.value.push(createInlineMessage('user', String(text || '').trim())) } +function resolveLatestInlineUserPrompt() { + const latestUserMessage = [...conversationMessages.value].reverse().find((message) => message.role === 'user') + return String(latestUserMessage?.content || '').trim() +} + +function normalizeInlineApplicationTypeLabel(expenseTypeLabel, fallback = '费用申请') { + const label = String(expenseTypeLabel || '').trim() + if (!label) { + return fallback + } + if (label.endsWith('费用申请') || label.endsWith('申请')) { + return label + } + if (label.endsWith('费用')) { + return `${label}申请` + } + if (label.endsWith('费')) { + return `${label.slice(0, -1)}费用申请` + } + return `${label}申请` +} + +function buildInlineApplicationPreview(expenseTypeLabel, sourceText = '') { + const rawText = String(sourceText || '').trim() + const preview = rawText + ? buildLocalApplicationPreview(rawText, currentUser.value || {}) + : buildApplicationTemplatePreview(currentUser.value || {}) + const normalized = normalizeApplicationPreview(preview) + return normalizeApplicationPreview({ + ...normalized, + fields: { + ...(normalized.fields || {}), + applicationType: normalizeInlineApplicationTypeLabel( + expenseTypeLabel, + normalized.fields?.applicationType || '费用申请' + ) + } + }) +} + // 选定报销类型后,在当前对话页内启动逐项收集流程; // 差旅/招待需先查申请单,其余类型直接进入字段填写。 function startAiExpenseDraft(expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement) { @@ -1537,32 +1968,28 @@ function linkAiExpenseApplication(application = {}) { scrollInlineConversationToBottom() } -// 进入申请草稿:在当前 AI 对话页内逐项收集出差申请要点, -// 不跳工作台、不调用旧 applyGuided 流程。 -function startAiApplicationDraft(expenseType, expenseTypeLabel) { - pushInlineUserMessage('在当前对话里先发起申请') - const draft = createAiApplicationDraft(expenseType, expenseTypeLabel) - aiApplicationDraft.value = draft - conversationMessages.value.push(createInlineMessage('assistant', buildAiApplicationStepPrompt(draft))) - persistCurrentConversation() - scrollInlineConversationToBottom() -} - -function advanceAiApplicationDraft(answer, files = []) { - const fileNames = Array.from(files || []) - pushInlineUserMessage(answer || (fileNames.length ? `上传 ${fileNames.length} 份附件` : '')) - assistantDraft.value = '' - clearAiModeFiles() - - const next = applyAiApplicationAnswer(aiApplicationDraft.value, answer, fileNames) - aiApplicationDraft.value = next - - if (isAiApplicationDraftComplete(next)) { - conversationMessages.value.push(createInlineMessage('assistant', buildAiApplicationSummary(next))) - aiApplicationDraft.value = null - } else { - conversationMessages.value.push(createInlineMessage('assistant', buildAiApplicationStepPrompt(next))) +// 进入申请核对表:复用原有申请预览模型,一次性展示可编辑表格和自动测算结果。 +async function startAiApplicationPreview(expenseType, expenseTypeLabel, sourceText = '', options = {}) { + if (!conversationStarted.value) { + activateInlineConversation({ title: String(expenseTypeLabel || expenseType || '申请').trim().slice(0, 18) || '申请' }) } + const previewSourceText = String(sourceText || resolveLatestInlineUserPrompt()).trim() + aiExpenseDraft.value = null + assistantDraft.value = '' + removeWorkbenchDateTag() + closeWorkbenchDatePicker() + clearAiModeFiles() + if (options.pushUserMessage !== false) { + pushInlineUserMessage(options.userMessage || '确认发起出差申请') + } + const preview = await refreshApplicationPreviewEstimate( + buildInlineApplicationPreview(expenseTypeLabel || expenseType, previewSourceText) + ) + const content = buildLocalApplicationPreviewMessage(preview) + conversationMessages.value.push(createInlineMessage('assistant', content, { + applicationPreview: preview, + text: content + })) persistCurrentConversation() scrollInlineConversationToBottom() } diff --git a/web/src/services/aiApplicationPreviewActions.js b/web/src/services/aiApplicationPreviewActions.js new file mode 100644 index 0000000..079f87c --- /dev/null +++ b/web/src/services/aiApplicationPreviewActions.js @@ -0,0 +1,136 @@ +import { runOrchestrator } from './orchestrator.js' +import { + buildApplicationPreviewRows, + buildApplicationPreviewSubmitText, + normalizeApplicationPreview +} from '../utils/expenseApplicationPreview.js' + +export const AI_APPLICATION_ACTION_SAVE_DRAFT = 'ai_application_save_draft' +export const AI_APPLICATION_ACTION_SUBMIT = 'ai_application_submit' + +function normalizeText(value) { + return String(value || '').trim() +} + +function resolveUserValue(user = {}, ...keys) { + for (const key of keys) { + const value = normalizeText(user?.[key]) + if (value) return value + } + return '' +} + +function buildClientTimeContext() { + const now = new Date() + const locale = + typeof navigator !== 'undefined' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN' + + return { + client_now_iso: now.toISOString(), + client_timezone_offset_minutes: now.getTimezoneOffset(), + client_locale: locale + } +} + +function buildApplicationPreviewSaveText(preview = {}) { + const rows = buildApplicationPreviewRows(preview) + return [ + '费用申请保存草稿', + ...rows.map((row) => `${row.label}:${row.value}`), + '', + '保存草稿' + ].join('\n') +} + +export function buildAiApplicationPreviewActionText(actionType, preview = {}) { + const normalized = normalizeApplicationPreview(preview) + return actionType === AI_APPLICATION_ACTION_SUBMIT + ? buildApplicationPreviewSubmitText(normalized) + : buildApplicationPreviewSaveText(normalized) +} + +export function buildAiApplicationPreviewActionPayload({ + actionType, + applicationPreview, + currentUser = {}, + conversationId = '', + draftPayload = null +} = {}) { + const normalizedPreview = normalizeApplicationPreview(applicationPreview || {}) + const message = buildAiApplicationPreviewActionText(actionType, normalizedPreview) + const username = resolveUserValue(currentUser, 'username', 'account', 'email', 'name') || 'anonymous' + const name = resolveUserValue(currentUser, 'name', 'username') + const employeeNo = resolveUserValue(currentUser, 'employeeNo', 'employee_no') + const managerName = resolveUserValue(currentUser, 'managerName', 'manager_name', 'directManagerName', 'direct_manager_name') + const departmentName = resolveUserValue(currentUser, 'departmentName', 'department_name', 'department') + const position = resolveUserValue(currentUser, 'position', 'employeePosition', 'employee_position') + const grade = resolveUserValue(currentUser, 'grade', 'employeeGrade', 'employee_grade') + const roleCodes = Array.isArray(currentUser.roleCodes) + ? currentUser.roleCodes.map((item) => normalizeText(item)).filter(Boolean) + : [] + const draftClaimId = normalizeText(draftPayload?.claim_id || draftPayload?.claimId) + const isSubmit = actionType === AI_APPLICATION_ACTION_SUBMIT + + return { + source: 'user_message', + user_id: username, + conversation_id: normalizeText(conversationId) || null, + message, + context_json: { + role_codes: roleCodes, + is_admin: Boolean(currentUser.isAdmin), + name, + role: resolveUserValue(currentUser, 'role'), + department: departmentName, + department_name: departmentName, + position, + employee_position: position, + employeePosition: position, + grade, + employee_grade: grade, + employeeGrade: grade, + employee_no: employeeNo, + employeeNo, + manager_name: managerName, + managerName, + direct_manager_name: managerName, + directManagerName: managerName, + cost_center: resolveUserValue(currentUser, 'costCenter', 'cost_center'), + finance_owner_name: resolveUserValue(currentUser, 'financeOwnerName', 'finance_owner_name'), + ...buildClientTimeContext(), + session_type: 'application', + entry_source: 'workbench_ai_inline', + source: 'workbench', + document_type: 'expense_application', + application_stage: 'expense_application', + user_input_text: message, + application_preview: normalizedPreview, + ...(isSubmit + ? {} + : { + application_action: 'save_draft', + application_save_mode: true + }), + ...(draftClaimId + ? { + application_edit_claim_id: draftClaimId, + draft_claim_id: draftClaimId, + selected_claim_id: draftClaimId, + application_edit_mode: true + } + : {}) + } + } +} + +export function runAiApplicationPreviewAction(params = {}, options = {}) { + return runOrchestrator(buildAiApplicationPreviewActionPayload(params), { + timeoutMs: params.actionType === AI_APPLICATION_ACTION_SUBMIT ? 120000 : 75000, + timeoutMessage: params.actionType === AI_APPLICATION_ACTION_SUBMIT + ? '申请提交处理超时,请稍后重试。' + : '申请草稿保存超时,请稍后重试。', + ...options + }) +} diff --git a/web/src/utils/aiApplicationPrecheckModel.js b/web/src/utils/aiApplicationPrecheckModel.js new file mode 100644 index 0000000..9fdd174 --- /dev/null +++ b/web/src/utils/aiApplicationPrecheckModel.js @@ -0,0 +1,345 @@ +import { extractExpenseClaimItems } from '../services/reimbursements.js' +import { + isClaimOwnedByCurrentUser, + isExpenseApplicationClaim, + matchesRequiredApplicationExpenseType, + normalizeRequiredApplicationCandidate +} from '../views/scripts/travelReimbursementApplicationLinkModel.js' +import { + normalizeApplicationPreview, + resolveApplicationDateRange +} from './expenseApplicationPreview.js' + +const APPLICATION_BUDGET_REVIEW_THRESHOLD = 90 + +function normalizeText(value) { + return String(value || '').trim() +} + +function normalizeMoney(value) { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : 0 + } + const normalized = normalizeText(value).replace(/,/g, '') + const match = normalized.match(/-?\d+(?:\.\d+)?/) + const amount = match ? Number(match[0]) : 0 + return Number.isFinite(amount) && amount > 0 ? amount : 0 +} + +function formatMoney(value) { + const amount = normalizeMoney(value) + if (!amount) { + return '' + } + return `${new Intl.NumberFormat('zh-CN', { + maximumFractionDigits: Number.isInteger(amount) ? 0 : 2, + minimumFractionDigits: Number.isInteger(amount) ? 0 : 2 + }).format(amount)}元` +} + +function escapeMarkdownCell(value) { + return normalizeText(value).replace(/\|/g, '\\|') || '-' +} + +function buildApplicationDetailHref(item = {}) { + const claimNo = normalizeText(item.claimNo) + const reference = claimNo && claimNo !== '未编号申请单' + ? claimNo + : normalizeText(item.claimId) + return reference ? `#ai-open-application-detail:${encodeURIComponent(reference)}` : '' +} + +function buildApplicationDetailActionCell(item = {}) { + const href = buildApplicationDetailHref(item) + return href ? `[查看](${href})` : '-' +} + +function parseDate(value) { + const dateText = normalizeText(value) + if (!/^\d{4}-\d{2}-\d{2}$/.test(dateText)) { + return null + } + const date = new Date(`${dateText}T00:00:00Z`) + return Number.isNaN(date.getTime()) ? null : date +} + +function resolveDateRange(value, daysText = '') { + const resolved = resolveApplicationDateRange(value, daysText) + if (!resolved) { + return null + } + const startText = normalizeText(resolved.startDate) + const endText = normalizeText(resolved.endDate || resolved.startDate) + const startDate = parseDate(startText) + const endDate = parseDate(endText) + if (!startDate || !endDate) { + return null + } + return startDate <= endDate + ? { startText, endText, startDate, endDate } + : { startText: endText, endText: startText, startDate: endDate, endDate: startDate } +} + +function rangesOverlap(left, right) { + return Boolean(left && right && left.startDate <= right.endDate && right.startDate <= left.endDate) +} + +function resolvePreviewDateRange(preview) { + const fields = normalizeApplicationPreview(preview).fields || {} + return resolveDateRange(fields.time, fields.days) +} + +function resolvePreviewAmount(preview) { + const normalized = normalizeApplicationPreview(preview) + const fields = normalized.fields || {} + const policyEstimate = normalized.policyEstimate && typeof normalized.policyEstimate === 'object' + ? normalized.policyEstimate + : {} + return normalizeMoney( + fields.amount || + fields.policyTotalAmount || + fields.reimbursementAmount || + policyEstimate.system_total_amount + ) +} + +function resolveApplicationClaims(claimsPayload, currentUser, expenseType) { + return extractExpenseClaimItems(claimsPayload) + .filter((claim) => ( + isExpenseApplicationClaim(claim) && + isClaimOwnedByCurrentUser(claim, currentUser) && + matchesRequiredApplicationExpenseType(claim, expenseType) + )) + .map((claim) => normalizeRequiredApplicationCandidate(claim)) +} + +function buildOverlapPrecheck(preview, claimsPayload, currentUser, expenseType) { + const targetRange = resolvePreviewDateRange(preview) + if (!targetRange) { + return { + status: 'unknown', + summary: '暂未识别到完整出差日期,无法判断是否与已有申请时间重叠。' + } + } + + const applications = resolveApplicationClaims(claimsPayload, currentUser, expenseType) + const matches = applications + .map((application) => { + const range = resolveDateRange(application.business_time) + return { + ...application, + range + } + }) + .filter((application) => rangesOverlap(targetRange, application.range)) + .slice(0, 3) + + if (!matches.length) { + return { + status: 'ok', + summary: `未发现 ${targetRange.startText} 至 ${targetRange.endText} 期间已有重叠的差旅申请单。`, + matches: [] + } + } + + return { + status: 'warning', + summary: `发现 ${matches.length} 张同时间段可能重叠的申请单,暂不能继续发起新的出差申请。`, + matches: matches.map((item) => ({ + claimId: item.id || '', + claimNo: item.claim_no || '未编号申请单', + time: item.business_time || '', + statusLabel: item.status_label || '', + reason: item.reason || '' + })) + } +} + +function isBlockingPrecheck(precheck = {}) { + return precheck?.overlap?.status === 'warning' +} + +function buildOverlapMatchTable(matches = []) { + const rows = Array.isArray(matches) ? matches : [] + if (!rows.length) { + return '' + } + return [ + '| 单据编号 | 申请时间 | 状态 | 事由 | 操作 |', + '| --- | --- | --- | --- | --- |', + ...rows.map((item) => [ + escapeMarkdownCell(item.claimNo), + escapeMarkdownCell(item.time), + escapeMarkdownCell(item.statusLabel), + escapeMarkdownCell(item.reason), + buildApplicationDetailActionCell(item) + ].join(' | ')).map((row) => `| ${row} |`) + ].join('\n') +} + +function resolveBudgetNumbers(summary = {}) { + const totalAmount = normalizeMoney(summary.total_amount || summary.totalAmount) + const reservedAmount = normalizeMoney(summary.reserved_amount || summary.reservedAmount) + const consumedAmount = normalizeMoney(summary.consumed_amount || summary.consumedAmount) + const availableAmount = normalizeMoney(summary.available_amount || summary.availableAmount) + return { + totalAmount, + reservedAmount, + consumedAmount, + availableAmount, + usedAmount: reservedAmount + consumedAmount + } +} + +function buildBudgetPrecheck(preview, budgetSummary) { + const amount = resolvePreviewAmount(preview) + const missingFields = normalizeApplicationPreview(preview).missingFields || [] + if (!amount) { + const reason = missingFields.includes('出行方式') + ? '当前还缺出行方式,交通费用和申请总额暂未完成测算。' + : '当前申请总额暂未完成测算。' + return { + status: 'pending', + requiresBudgetReview: false, + summary: `${reason}补齐后会刷新预算占用;若达到 ${APPLICATION_BUDGET_REVIEW_THRESHOLD}% 预算复核线或超预算,系统会增加预算管理者审核。` + } + } + + if (!budgetSummary || typeof budgetSummary !== 'object') { + return { + status: 'unknown', + requiresBudgetReview: false, + summary: `本次预计申请金额 ${formatMoney(amount)}。预算接口暂未返回,以提交时系统预算复核为准;若达到 ${APPLICATION_BUDGET_REVIEW_THRESHOLD}% 复核线或超预算,会增加预算管理者审核。` + } + } + + const budget = resolveBudgetNumbers(budgetSummary) + if (!budget.totalAmount) { + return { + status: 'unknown', + requiresBudgetReview: false, + summary: `本次预计申请金额 ${formatMoney(amount)}。当前部门预算总额暂未配置或暂未返回,提交时会继续做预算归口复核。` + } + } + + const afterUsed = budget.usedAmount + amount + const afterUsageRate = Number(((afterUsed / budget.totalAmount) * 100).toFixed(2)) + if (amount > budget.availableAmount) { + return { + status: 'warning', + requiresBudgetReview: true, + summary: `本次预计申请金额 ${formatMoney(amount)},当前可用预算 ${formatMoney(budget.availableAmount)},预计超出 ${formatMoney(amount - budget.availableAmount)},提交后需要预算管理者审核。` + } + } + if (afterUsageRate >= APPLICATION_BUDGET_REVIEW_THRESHOLD) { + return { + status: 'warning', + requiresBudgetReview: true, + summary: `本次预计申请金额 ${formatMoney(amount)},审批后预算占用约 ${afterUsageRate}%,达到 ${APPLICATION_BUDGET_REVIEW_THRESHOLD}% 复核线,提交后需要预算管理者审核。` + } + } + + return { + status: 'ok', + requiresBudgetReview: false, + summary: `本次预计申请金额 ${formatMoney(amount)},审批后预算占用约 ${afterUsageRate}%,未达到 ${APPLICATION_BUDGET_REVIEW_THRESHOLD}% 复核线。` + } +} + +export function buildAiApplicationPrecheck(preview = {}, { + claimsPayload = null, + budgetSummary = null, + currentUser = {}, + expenseType = 'travel', + budgetError = null +} = {}) { + const normalizedPreview = normalizeApplicationPreview(preview) + const budget = budgetError + ? { + status: 'unknown', + requiresBudgetReview: false, + summary: `预算接口暂未返回:${normalizeText(budgetError?.message || budgetError) || '当前无可用预算数据'}。提交时系统仍会按预算余额、风险规则判断是否增加预算管理者审核。` + } + : buildBudgetPrecheck(normalizedPreview, budgetSummary) + return { + overlap: buildOverlapPrecheck(normalizedPreview, claimsPayload, currentUser, expenseType), + budget, + missingFields: Array.isArray(normalizedPreview.missingFields) ? normalizedPreview.missingFields : [] + } +} + +export function buildAiApplicationPrecheckThinkingEvents(precheck = {}) { + const blocked = isBlockingPrecheck(precheck) + return [ + { + eventId: 'application-precheck-overlap', + title: '核查同时间段申请单', + content: precheck?.overlap?.summary || '已完成已有申请单核查。', + status: precheck?.overlap?.status === 'warning' ? 'completed' : 'completed' + }, + { + eventId: 'application-precheck-budget', + title: '评估预算与审批影响', + content: precheck?.budget?.summary || '已完成预算影响评估。', + status: 'completed' + }, + { + eventId: 'application-precheck-form', + title: blocked ? '暂停生成申请表' : '生成申请表草稿', + content: blocked + ? '因发现同时间段已有申请单,已暂停生成新的申请表,等待用户核对申请时间。' + : '已将识别到的时间、地点、事由和申请人信息预填到申请表。', + status: 'completed' + } + ] +} + +export function buildAiApplicationPrecheckMessage(preview = {}, precheck = {}) { + if (isBlockingPrecheck(precheck)) { + const matchTable = buildOverlapMatchTable(precheck?.overlap?.matches) + const lines = [ + '### 发现同时间段已有申请单', + '', + '**我已完成发起前的单据重叠核查**,当前不能继续生成新的出差申请表。', + '', + `> **时间重叠提醒**:${precheck?.overlap?.summary || '发现同时间段已有申请单,暂不能继续发起新的出差申请。'}`, + ] + if (matchTable) { + lines.push('', matchTable) + } + lines.push( + '', + '> **请先核对**:请先检查本次申请时间是否填写正确。若日期填错,请直接回复正确的出发时间和返回时间,我会重新查询;若日期无误,请先处理或关联已有申请单,避免重复申请。', + '', + '我会先暂停本次申请表生成,不会开放保存草稿或提交入口。' + ) + return lines.join('\n') + } + + const normalized = normalizeApplicationPreview(preview) + const missingFields = Array.isArray(normalized.missingFields) ? normalized.missingFields : [] + const missingText = missingFields.length ? missingFields.join('、') : '暂无' + const budgetPrefix = precheck?.budget?.requiresBudgetReview ? '**预算管理者审核提示**' : '**预算与审批影响**' + const overlapPrefix = precheck?.overlap?.status === 'warning' ? '**时间重叠提醒**' : '**单据重叠核查**' + const lines = [ + '### 出差申请表草稿已生成', + '', + '**我已完成发起前的单据与预算预审**,并为您生成一张完整的出差申请表。', + '', + `> ${overlapPrefix}:${precheck?.overlap?.summary || '已完成已有单据核查。'}`, + '', + `> ${budgetPrefix}:${precheck?.budget?.summary || '已完成预算影响评估。'}`, + '', + `> **仍需补充**:${missingText}`, + '', + '请直接点击表格中的字段补充或修改;费用测算会根据地点、天数和出行方式自动更新。' + ] + + if (missingFields.length) { + lines.push('', `当前还需要补充:**${missingText}**。`) + } else { + lines.push('', '信息已基本齐全,您可以保存草稿,或直接提交进入审批。') + } + + return lines.join('\n') +} diff --git a/web/src/utils/aiConversationHtmlRenderer.js b/web/src/utils/aiConversationHtmlRenderer.js new file mode 100644 index 0000000..83f5e76 --- /dev/null +++ b/web/src/utils/aiConversationHtmlRenderer.js @@ -0,0 +1,647 @@ +const ALLOWED_COLON_HEADING_TITLES = new Set([ + '基础信息识别结果', + '报销测算参考', + '补充信息' +]) + +const BUSINESS_FIELD_LABELS = new Set([ + '时间', + '地点', + '事由', + '金额', + '费用类型', + '报销类型', + '商户', + '商户/开票方', + '客户', + '客户/项目对象', + '附件', + '附件/凭证', + '出行方式' +]) + +const APPLICATION_DETAIL_HREF_PREFIX = '#ai-open-application-detail:' +const DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-detail:' +const TRUSTED_HTML_BLOCK_RE = /\s*([\s\S]*?)\s*/g +const TRUSTED_HTML_PLACEHOLDER_PREFIX = 'AI_TRUSTED_HTML_BLOCK_' +const TRUSTED_HTML_ALLOWED_TAGS = new Set([ + 'section', + 'article', + 'header', + 'footer', + 'div', + 'span', + 'strong', + 'a' +]) +const TRUSTED_HTML_ALLOWED_ATTRS = new Set([ + 'aria-label', + 'class', + 'data-ai-action', + 'href' +]) + +function escapeHtml(value = '') { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function sanitizeHref(href = '') { + const value = String(href || '').trim() + if (/^(https?:\/\/|#)/i.test(value)) { + return escapeHtml(value) + } + return '#' +} + +function isApplicationDetailHref(href = '') { + return String(href || '').trim().startsWith(APPLICATION_DETAIL_HREF_PREFIX) +} + +function isDocumentDetailHref(href = '') { + return String(href || '').trim().startsWith(DOCUMENT_DETAIL_HREF_PREFIX) +} + +function sanitizeImageSrc(src = '') { + const value = String(src || '').trim() + if (/^(https?:\/\/|blob:|\/)/i.test(value)) { + return escapeHtml(value) + } + if (/^data:image\/(?:png|jpe?g|webp|gif);base64,[a-z0-9+/=]+$/i.test(value)) { + return escapeHtml(value) + } + return '' +} + +function renderLinkHtml(label = '', href = '') { + const sanitizedHref = sanitizeHref(href) + if (isApplicationDetailHref(href)) { + return [ + `
', + label, + '' + ].join('') + } + if (isDocumentDetailHref(href)) { + return [ + `', + label, + '' + ].join('') + } + return `${label}` +} + +function renderInlineImageHtml(alt = '', src = '') { + const sanitizedSrc = sanitizeImageSrc(src) + if (!sanitizedSrc) { + return escapeHtml(alt || src) + } + return [ + `${escapeHtml(alt)}` + ].join('') +} + +function renderInlineHtml(value = '') { + let html = escapeHtml(value) + html = html.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_match, alt, src) => ( + renderInlineImageHtml(alt, src) + )) + html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+|#[^\s)]+)\)/g, (_match, label, href) => ( + renderLinkHtml(label, href) + )) + html = html.replace(/`([^`]+)`/g, '$1') + html = html.replace(/\*\*([^*]+)\*\*/g, '$1') + html = html.replace(/__([^_]+)__/g, '$1') + return html +} + +function splitColonHeadingLine(line) { + const rawLine = String(line || '') + const trimmed = rawLine.trim() + if (!trimmed || trimmed.startsWith('|') || /^#{1,6}\s/.test(trimmed)) { + return [rawLine] + } + + const chineseColonIndex = trimmed.indexOf(':') + const asciiColonIndex = trimmed.indexOf(':') + const colonIndexes = [chineseColonIndex, asciiColonIndex].filter((index) => index > 0) + if (!colonIndexes.length) { + return [rawLine] + } + + const colonIndex = Math.min(...colonIndexes) + const title = trimmed.slice(0, colonIndex) + const body = trimmed.slice(colonIndex + 1).trim() + if (!ALLOWED_COLON_HEADING_TITLES.has(title)) { + return [rawLine] + } + return body ? [`### ${title}`, '', body] : [`### ${title}`] +} + +function normalizeBusinessFieldLine(line) { + const rawLine = String(line || '') + const trimmed = rawLine.trim() + if ( + !trimmed || + trimmed.startsWith('|') || + /^[-*+]\s/.test(trimmed) || + /^#{1,6}\s/.test(trimmed) + ) { + return rawLine + } + + const match = trimmed.match(/^([^::\n]{1,16})[::]\s*(.+)$/u) + if (!match) { + return rawLine + } + const label = match[1].trim() + const value = match[2].trim() + if (!BUSINESS_FIELD_LABELS.has(label) || !value) { + return rawLine + } + return `- **${label}**:${value}` +} + +function normalizeConversationText(text = '') { + const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n') + const normalizedLines = [] + let inFence = false + + lines.forEach((line) => { + if (/^\s*(```|~~~)/.test(line)) { + inFence = !inFence + normalizedLines.push(line) + return + } + if (inFence) { + normalizedLines.push(line) + return + } + + const nextLines = splitColonHeadingLine(line) + if (nextLines[0]?.startsWith('### ') && normalizedLines.length) { + const previousLine = normalizedLines[normalizedLines.length - 1] + if (String(previousLine || '').trim()) { + normalizedLines.push('') + } + } + normalizedLines.push(...nextLines.map((nextLine) => normalizeBusinessFieldLine(nextLine))) + }) + + return normalizedLines.join('\n').replace(/\n{3,}/g, '\n\n').trim() +} + +function hasOnlyTrustedHtmlTags(html = '') { + const tagPattern = /<\/?([a-z][\w-]*)([^>]*)>/gi + let match = tagPattern.exec(html) + while (match) { + const tagName = String(match[1] || '').toLowerCase() + if (!TRUSTED_HTML_ALLOWED_TAGS.has(tagName)) { + return false + } + const attrText = String(match[2] || '') + const attrPattern = /\s([:@\w-]+)\s*=/g + let attrMatch = attrPattern.exec(attrText) + while (attrMatch) { + const attrName = String(attrMatch[1] || '').toLowerCase() + if (!TRUSTED_HTML_ALLOWED_ATTRS.has(attrName)) { + return false + } + attrMatch = attrPattern.exec(attrText) + } + match = tagPattern.exec(html) + } + return true +} + +function sanitizeTrustedHtmlBlock(html = '') { + const value = String(html || '').trim() + if (!value || !value.includes('class="ai-document-card-list"')) { + return '' + } + if (/<(?:script|style|iframe|object|embed|link|meta|form|input|button|textarea|select)\b/i.test(value)) { + return '' + } + if (/\son[a-z]+\s*=/i.test(value) || /javascript\s*:/i.test(value)) { + return '' + } + if (!hasOnlyTrustedHtmlTags(value)) { + return '' + } + const hrefs = [...value.matchAll(/\shref="([^"]*)"/gi)].map((match) => String(match[1] || '').trim()) + if (hrefs.some((href) => !href.startsWith(DOCUMENT_DETAIL_HREF_PREFIX))) { + return '' + } + return value +} + +function extractTrustedHtmlBlocks(text = '') { + const trustedHtmlBlocks = [] + const content = String(text || '').replace(TRUSTED_HTML_BLOCK_RE, (_match, html) => { + const sanitizedHtml = sanitizeTrustedHtmlBlock(html) + if (!sanitizedHtml) { + return '' + } + const placeholder = `${TRUSTED_HTML_PLACEHOLDER_PREFIX}${trustedHtmlBlocks.length}` + trustedHtmlBlocks.push(sanitizedHtml) + return `\n\n${placeholder}\n\n` + }) + return { content, trustedHtmlBlocks } +} + +function restoreTrustedHtmlBlocks(html = '', trustedHtmlBlocks = []) { + return trustedHtmlBlocks.reduce((nextHtml, block, index) => { + const placeholder = `${TRUSTED_HTML_PLACEHOLDER_PREFIX}${index}` + const paragraphPattern = new RegExp(`

${placeholder}

`, 'g') + return nextHtml + .replace(paragraphPattern, block) + .replaceAll(placeholder, block) + }, html) +} + +function isFenceLine(line = '') { + return /^\s*(```|~~~)/.test(String(line || '')) +} + +function isHeadingLine(line = '') { + return /^#{1,6}\s+/.test(String(line || '').trim()) +} + +function isQuoteLine(line = '') { + return /^>\s?/.test(String(line || '').trim()) +} + +function isUnorderedListLine(line = '') { + return /^[-*+]\s+/.test(String(line || '').trim()) +} + +function isOrderedListLine(line = '') { + return /^\d+\.\s+/.test(String(line || '').trim()) +} + +function isHorizontalRuleLine(line = '') { + return /^(-{3,}|\*{3,}|_{3,})$/.test(String(line || '').trim()) +} + +function isTableDivider(line = '') { + const cells = parseTableRow(line) + return cells.length > 1 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim())) +} + +function isTableStart(lines, index) { + const current = String(lines[index] || '').trim() + const next = String(lines[index + 1] || '').trim() + return current.startsWith('|') && next.startsWith('|') && isTableDivider(next) +} + +function parseImageLine(line = '') { + const match = String(line || '').trim().match(/^!\[([^\]]*)\]\(([^)\s]+)\)$/) + if (!match) { + return null + } + const src = sanitizeImageSrc(match[2]) + if (!src) { + return null + } + return { + alt: String(match[1] || '').trim(), + src + } +} + +function parseTableRow(line = '') { + const trimmed = String(line || '').trim() + if (!trimmed.startsWith('|')) { + return [] + } + return trimmed + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map((cell) => cell.trim()) +} + +function splitLabelAndBody(rawText = '') { + const text = String(rawText || '').trim() + const strongMatch = text.match(/^\*\*([^*]+)\*\*[::]\s*(.*)$/u) + if (strongMatch) { + return { + label: strongMatch[1].trim(), + body: strongMatch[2].trim() + } + } + + const plainText = text.replace(/\*\*/g, '') + const match = plainText.match(/^([^::\n]{2,20})[::]\s*(.*)$/u) + if (!match) { + return null + } + return { + label: match[1].trim(), + body: match[2].trim() + } +} + +function isSpecialBlockStart(lines, index) { + const line = String(lines[index] || '').trim() + return ( + !line || + isFenceLine(line) || + isHeadingLine(line) || + isQuoteLine(line) || + isUnorderedListLine(line) || + isOrderedListLine(line) || + isHorizontalRuleLine(line) || + Boolean(parseImageLine(line)) || + isTableStart(lines, index) + ) +} + +function nextNonEmptyLineMatches(lines, index, predicate) { + let cursor = index + 1 + while (cursor < lines.length) { + const nextLine = String(lines[cursor] || '').trim() + if (nextLine) { + return predicate(nextLine) + } + cursor += 1 + } + return false +} + +function renderHeading(line = '') { + const match = String(line || '').trim().match(/^(#{1,6})\s+(.+)$/) + if (!match) { + return '' + } + const level = Math.min(Math.max(match[1].length, 2), 4) + const className = level === 3 ? 'ai-html-title' : `ai-html-title ai-html-title--level-${level}` + return `${renderInlineHtml(match[2])}` +} + +function renderParagraph(lines = []) { + const text = lines.map((line) => String(line || '').trim()).filter(Boolean).join(' ') + return text ? `

${renderInlineHtml(text)}

` : '' +} + +function renderImageBlock(line = '') { + const image = parseImageLine(line) + if (!image) { + return '' + } + return [ + '
', + `${escapeHtml(image.alt)}`, + image.alt ? `
${escapeHtml(image.alt)}
` : '', + '
' + ].join('') +} + +function renderQuoteBlock(items = []) { + const normalizedItems = items + .map((item) => String(item || '').replace(/^>\s?/, '').trim()) + .filter(Boolean) + if (!normalizedItems.length) { + return '' + } + + const focusItems = normalizedItems + .map((item) => splitLabelAndBody(item)) + .filter(Boolean) + if (focusItems.length === normalizedItems.length) { + return [ + '
', + ...focusItems.map((item) => [ + '
', + `${renderInlineHtml(item.label)}`, + `

${renderInlineHtml(item.body)}

`, + '
' + ].join('')), + '
' + ].join('') + } + + return [ + '' + ].join('') +} + +function renderUnorderedList(items = []) { + const parsedItems = items + .map((item) => String(item || '').trim().replace(/^[-*+]\s+/, '').trim()) + .filter(Boolean) + const structuredItems = parsedItems + .map((item) => splitLabelAndBody(item)) + .filter(Boolean) + + if (structuredItems.length === parsedItems.length && parsedItems.length > 0) { + return [ + '
    ', + ...structuredItems.map((item, index) => [ + '
  • ', + `${index + 1}`, + '
    ', + `${renderInlineHtml(item.label)}`, + item.body ? `

    ${renderInlineHtml(item.body)}

    ` : '', + '
    ', + '
  • ' + ].join('')), + '
' + ].join('') + } + + return [ + '
    ', + ...parsedItems.map((item) => `
  • ${renderInlineHtml(item)}
  • `), + '
' + ].join('') +} + +function renderOrderedList(items = []) { + const parsedItems = items + .map((item) => String(item || '').trim().replace(/^\d+\.\s+/, '').trim()) + .filter(Boolean) + return [ + '
    ', + ...parsedItems.map((item) => `
  1. ${renderInlineHtml(item)}
  2. `), + '
' + ].join('') +} + +function renderTable(lines = []) { + const rows = lines.map((line) => parseTableRow(line)).filter((row) => row.length) + if (rows.length < 2) { + return '' + } + const header = rows[0] + const bodyRows = rows.slice(2) + + return [ + '
', + '', + '', + ...header.map((cell) => ``), + '', + '', + ...bodyRows.map((row) => [ + '', + ...header.map((_cell, index) => ``), + '' + ].join('')), + '', + '
${renderInlineHtml(cell)}
${renderInlineHtml(row[index] || '')}
', + '
' + ].join('') +} + +function renderCodeBlock(lines = []) { + const code = lines.join('\n').replace(/\n$/, '') + return `
${escapeHtml(code)}
` +} + +export function renderAiConversationHtml(content = '') { + const extracted = extractTrustedHtmlBlocks(content) + const normalized = normalizeConversationText(extracted.content) + if (!normalized) { + return '' + } + + const lines = normalized.split('\n') + const blocks = [] + let index = 0 + + while (index < lines.length) { + const line = String(lines[index] || '') + const trimmed = line.trim() + + if (!trimmed) { + index += 1 + continue + } + + if (isFenceLine(trimmed)) { + index += 1 + const codeLines = [] + while (index < lines.length && !isFenceLine(lines[index])) { + codeLines.push(lines[index]) + index += 1 + } + if (index < lines.length) { + index += 1 + } + blocks.push(renderCodeBlock(codeLines)) + continue + } + + if (isHeadingLine(trimmed)) { + blocks.push(renderHeading(trimmed)) + index += 1 + continue + } + + if (isTableStart(lines, index)) { + const tableLines = [] + while (index < lines.length && String(lines[index] || '').trim().startsWith('|')) { + tableLines.push(lines[index]) + index += 1 + } + blocks.push(renderTable(tableLines)) + continue + } + + if (parseImageLine(trimmed)) { + blocks.push(renderImageBlock(trimmed)) + index += 1 + continue + } + + if (isQuoteLine(trimmed)) { + const quoteItems = [] + while (index < lines.length) { + const current = String(lines[index] || '').trim() + if (isQuoteLine(current)) { + quoteItems.push(current) + index += 1 + continue + } + if (!current && isQuoteLine(String(lines[index + 1] || '').trim())) { + index += 1 + continue + } + break + } + blocks.push(renderQuoteBlock(quoteItems)) + continue + } + + if (isUnorderedListLine(trimmed)) { + const listItems = [] + while (index < lines.length) { + const current = String(lines[index] || '').trim() + if (isUnorderedListLine(current)) { + listItems.push(lines[index]) + index += 1 + continue + } + if (!current && nextNonEmptyLineMatches(lines, index, isUnorderedListLine)) { + index += 1 + continue + } + break + } + blocks.push(renderUnorderedList(listItems)) + continue + } + + if (isOrderedListLine(trimmed)) { + const listItems = [] + while (index < lines.length) { + const current = String(lines[index] || '').trim() + if (isOrderedListLine(current)) { + listItems.push(lines[index]) + index += 1 + continue + } + if (!current && nextNonEmptyLineMatches(lines, index, isOrderedListLine)) { + index += 1 + continue + } + break + } + blocks.push(renderOrderedList(listItems)) + continue + } + + if (isHorizontalRuleLine(trimmed)) { + blocks.push('
') + index += 1 + continue + } + + const paragraphLines = [] + while (index < lines.length && !isSpecialBlockStart(lines, index)) { + paragraphLines.push(lines[index]) + index += 1 + } + blocks.push(renderParagraph(paragraphLines)) + } + + return restoreTrustedHtmlBlocks( + `
${blocks.filter(Boolean).join('')}
`, + extracted.trustedHtmlBlocks + ) +} diff --git a/web/src/utils/aiDocumentQueryModel.js b/web/src/utils/aiDocumentQueryModel.js new file mode 100644 index 0000000..26e6c7e --- /dev/null +++ b/web/src/utils/aiDocumentQueryModel.js @@ -0,0 +1,784 @@ +import { extractExpenseClaimItems } from '../services/reimbursements.js' + +const DOCUMENT_QUERY_LIMIT = 8 + +const STATUS_LABELS = { + draft: '草稿', + submitted: '审批中', + pending: '待处理', + approved: '已审批', + completed: '已完成', + archived: '已归档', + returned: '已退回', + rejected: '已驳回', + pending_payment: '待付款', + paid: '已付款' +} + +const TYPE_LABELS = { + travel: '差旅费', + travel_application: '差旅费用申请', + expense_application: '费用申请', + application: '费用申请', + office: '办公用品费', + transport: '交通费', + hotel: '住宿费', + meal: '业务招待费', + entertainment: '业务招待费', + meeting: '会务费', + training: '培训费', + software: '软件服务费', + other: '其他费用' +} + +const STATUS_FILTERS = [ + { label: '草稿', keys: ['draft'], pattern: /草稿|未提交/ }, + { label: '审批中', keys: ['submitted', 'pending'], pattern: /审批中|审核中|待审批|待审核|待审|已提交/ }, + { label: '已审批', keys: ['approved'], pattern: /已审批|审批通过|已通过|已批准/ }, + { label: '已完成', keys: ['completed'], pattern: /已完成|完成/ }, + { label: '已归档', keys: ['archived'], pattern: /已归档|归档/ }, + { label: '已退回', keys: ['returned'], pattern: /已退回|退回|待补充/ }, + { label: '已驳回', keys: ['rejected'], pattern: /已驳回|驳回|拒绝/ }, + { label: '待付款', keys: ['pending_payment'], pattern: /待付款|待支付/ }, + { label: '已付款', keys: ['paid'], pattern: /已付款|已支付/ } +] + +const EXPENSE_TYPE_FILTERS = [ + { label: '差旅费', codes: ['travel', 'travel_application'], pattern: /差旅|出差|差旅费|差旅费用/ }, + { label: '交通费', codes: ['transport'], pattern: /交通|火车|机票|打车|出租|网约车/ }, + { label: '住宿费', codes: ['hotel'], pattern: /住宿|酒店|宾馆/ }, + { label: '业务招待费', codes: ['meal', 'entertainment'], pattern: /招待|餐饮|宴请|客户餐/ }, + { label: '办公用品费', codes: ['office'], pattern: /办公|办公用品|采购/ }, + { label: '会务费', codes: ['meeting'], pattern: /会务|会议/ }, + { label: '培训费', codes: ['training'], pattern: /培训/ }, + { label: '软件服务费', codes: ['software'], pattern: /软件|服务费|订阅/ } +] + +const MONEY_FORMATTER = new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + minimumFractionDigits: 2, + maximumFractionDigits: 2 +}) + +function normalizeText(value) { + return String(value ?? '').trim() +} + +function escapeHtml(value = '') { + return String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function compactText(value) { + return normalizeText(value).replace(/\s+/g, '') +} + +function normalizeDateText(value) { + const text = normalizeText(value) + const matched = text.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})/) + if (!matched) { + return '' + } + return [ + matched[1], + String(matched[2]).padStart(2, '0'), + String(matched[3]).padStart(2, '0') + ].join('-') +} + +function parseDate(value) { + const text = normalizeDateText(value) + if (!text) { + return null + } + const date = new Date(`${text}T00:00:00Z`) + return Number.isNaN(date.getTime()) ? null : date +} + +function formatDate(date) { + return date.toISOString().slice(0, 10) +} + +function resolveToday(options = {}) { + return parseDate(options.today) || new Date() +} + +function lastDayOfMonth(year, month) { + return new Date(Date.UTC(year, month, 0)).getUTCDate() +} + +function buildMonthRange(year, month) { + const normalizedMonth = String(month).padStart(2, '0') + return { + start: `${year}-${normalizedMonth}-01`, + end: `${year}-${normalizedMonth}-${String(lastDayOfMonth(year, month)).padStart(2, '0')}`, + label: `${year}年${month}月` + } +} + +function resolveTimeRange(prompt, options = {}) { + const text = compactText(prompt) + const today = resolveToday(options) + const todayText = formatDate(today) + + const explicitMonth = text.match(/(?:(?20\d{2})年?)?(?\d{1,2})月(?!\d{1,2})/) + if (explicitMonth?.groups) { + const year = Number(explicitMonth.groups.year || today.getUTCFullYear()) + const month = Number(explicitMonth.groups.month) + if (month >= 1 && month <= 12) { + return buildMonthRange(year, month) + } + } + + const explicitRange = text.match(/(?:(?20\d{2})年?)?(?\d{1,2})月(?\d{1,2})日?(?:至|到|~|-|—|–)(?:(?\d{1,2})月)?(?\d{1,2})日?/) + if (explicitRange?.groups) { + const year = Number(explicitRange.groups.year || today.getUTCFullYear()) + const startMonth = Number(explicitRange.groups.startMonth) + const endMonth = Number(explicitRange.groups.endMonth || startMonth) + const start = `${year}-${String(startMonth).padStart(2, '0')}-${String(explicitRange.groups.startDay).padStart(2, '0')}` + const end = `${year}-${String(endMonth).padStart(2, '0')}-${String(explicitRange.groups.endDay).padStart(2, '0')}` + return { start, end, label: `${start} 至 ${end}` } + } + + const explicitDay = text.match(/(?:(?20\d{2})年?)?(?\d{1,2})月(?\d{1,2})日?/) + if (explicitDay?.groups) { + const year = Number(explicitDay.groups.year || today.getUTCFullYear()) + const value = `${year}-${String(explicitDay.groups.month).padStart(2, '0')}-${String(explicitDay.groups.day).padStart(2, '0')}` + return { start: value, end: value, label: value } + } + + if (/今天|今日/.test(text)) { + return { start: todayText, end: todayText, label: '今天' } + } + + if (/昨天/.test(text)) { + const date = new Date(today.getTime()) + date.setUTCDate(date.getUTCDate() - 1) + const value = formatDate(date) + return { start: value, end: value, label: '昨天' } + } + + if (/本月|这个月|当月/.test(text)) { + return buildMonthRange(today.getUTCFullYear(), today.getUTCMonth() + 1) + } + + if (/上月|上个月/.test(text)) { + const date = new Date(today.getTime()) + date.setUTCMonth(date.getUTCMonth() - 1) + return buildMonthRange(date.getUTCFullYear(), date.getUTCMonth() + 1) + } + + if (/今年|本年/.test(text)) { + const year = today.getUTCFullYear() + return { start: `${year}-01-01`, end: `${year}-12-31`, label: `${year}年` } + } + + const recent = text.match(/近(?\d{1,3})天/) + if (recent?.groups?.days) { + const days = Math.max(1, Number(recent.groups.days)) + const start = new Date(today.getTime()) + start.setUTCDate(start.getUTCDate() - days + 1) + return { start: formatDate(start), end: todayText, label: `近${days}天` } + } + + return null +} + +function resolveDocumentType(prompt) { + const text = compactText(prompt) + if (/申请单|申请类单据|申请类/.test(text)) { + return 'application' + } + if (/报销单|报销类单据|报销类/.test(text)) { + return 'reimbursement' + } + return 'all' +} + +function resolveStatusFilter(prompt) { + const text = compactText(prompt) + return STATUS_FILTERS.find((item) => item.pattern.test(text)) || null +} + +function resolveExpenseTypeFilter(prompt) { + const text = compactText(prompt) + return EXPENSE_TYPE_FILTERS.find((item) => item.pattern.test(text)) || null +} + +function normalizeAmountText(value = '') { + const matched = compactText(value).replace(/,/g, '').match(/-?\d+(?:\.\d+)?/) + if (!matched) { + return null + } + const amount = Number(matched[0]) + return Number.isFinite(amount) ? amount : null +} + +function resolveAmountFilter(prompt) { + const text = compactText(prompt) + const range = text.match(/金额(?:在|为)?(?\d+(?:\.\d+)?)(?:元)?(?:到|至|~|-|—|–)(?\d+(?:\.\d+)?)(?:元)?/) + if (range?.groups) { + const min = normalizeAmountText(range.groups.min) + const max = normalizeAmountText(range.groups.max) + if (min !== null && max !== null) { + return { + min: Math.min(min, max), + max: Math.max(min, max), + label: `${Math.min(min, max)}-${Math.max(min, max)}元` + } + } + } + + const minMatch = text.match(/(?:金额)?(?:大于|超过|高于|不少于|不低于|>=|以上)(?\d+(?:\.\d+)?)(?:元)?/) + || text.match(/(?\d+(?:\.\d+)?)(?:元)?以上/) + if (minMatch?.groups?.amount) { + const min = normalizeAmountText(minMatch.groups.amount) + return min === null ? null : { min, max: null, label: `不少于${min}元` } + } + + const maxMatch = text.match(/(?:金额)?(?:小于|低于|少于|不超过|<=|以下)(?\d+(?:\.\d+)?)(?:元)?/) + || text.match(/(?\d+(?:\.\d+)?)(?:元)?以下/) + if (maxMatch?.groups?.amount) { + const max = normalizeAmountText(maxMatch.groups.amount) + return max === null ? null : { min: null, max, label: `不超过${max}元` } + } + return null +} + +function normalizeKeywordCandidate(value = '') { + return normalizeText(value) + .replace(/^(的|是|为|包含|含有)+/u, '') + .replace(/(相关的?|有关的?|单据|单子|申请单|报销单|审核单|审批单|有哪些|有吗|查询|查看)$/u, '') + .replace(/的$/u, '') + .trim() +} + +function resolveKeywordFilter(prompt) { + const text = normalizeText(prompt) + const compact = compactText(prompt) + const explicitMatch = text.match(/(?:关于|有关|包含|含有|关键词|关键字|事由(?:是|为|包含|含有)?)[::\s]*(?[\u4e00-\u9fa5A-Za-z0-9_-]{2,32})/u) + const relatedMatch = compact.match(/(?[\u4e00-\u9fa5A-Za-z0-9_-]{2,32})相关(?:的)?(?:单据|单子|申请单|报销单|审核单|审批单)/u) + const keyword = normalizeKeywordCandidate(explicitMatch?.groups?.keyword || relatedMatch?.groups?.keyword || '') + if (!keyword || /^(现在|当前|哪些|审核|审批|申请|报销|单据|单子)$/u.test(keyword)) { + return null + } + return { keyword, label: keyword } +} + +function resolveSource(prompt) { + const text = compactText(prompt) + if (/审核单|审批单|待审|待审核|待审批|我审批|我审核/.test(text)) { + return { + source: 'approval', + sourceLabel: '待我审核的单据' + } + } + return { + source: 'mine', + sourceLabel: '我的单据' + } +} + +export function resolveAiDocumentQueryIntent(prompt, options = {}) { + const text = compactText(prompt) + if (!text || !/(单据|单子|申请单|报销单|审核单|审批单|待审|待审批|待审核)/.test(text)) { + return null + } + if (/(发起|创建|新增|填写|生成|申请出差|我要报销|提交).*(单据|申请单|报销单)?/.test(text)) { + return null + } + const source = resolveSource(text) + const documentType = resolveDocumentType(text) + const statusFilter = resolveStatusFilter(text) + const expenseTypeFilter = resolveExpenseTypeFilter(text) + const keywordFilter = resolveKeywordFilter(prompt) + const amountFilter = resolveAmountFilter(text) + return { + ...source, + documentType, + documentTypeLabel: documentType === 'application' + ? '申请单' + : documentType === 'reimbursement' + ? '报销单' + : '全部单据', + timeRange: resolveTimeRange(text, options), + statusFilter, + expenseTypeFilter, + keywordFilter, + amountFilter + } +} + +function resolveDocumentNo(claim = {}) { + return normalizeText(claim.claim_no || claim.claimNo || claim.documentNo || claim.id || claim.claim_id) +} + +function resolveClaimId(claim = {}) { + return normalizeText(claim.id || claim.claim_id || claim.claimId || resolveDocumentNo(claim)) +} + +function resolveDocumentTypeCode(claim = {}) { + const explicitType = normalizeText( + claim.document_type_code + || claim.documentTypeCode + || claim.document_type + || claim.documentType + ).toLowerCase() + const expenseType = normalizeText(claim.expense_type || claim.expenseType || claim.typeCode).toLowerCase() + const documentNo = resolveDocumentNo(claim).toUpperCase() + if ( + explicitType === 'application' + || explicitType === 'expense_application' + || expenseType === 'application' + || expenseType.endsWith('_application') + || documentNo.startsWith('AP-') + || documentNo.startsWith('APP-') + ) { + return 'application' + } + return 'reimbursement' +} + +function resolveStatusLabel(claim = {}) { + const key = normalizeText(claim.status || claim.state || claim.approval_status || claim.approvalStatus).toLowerCase() + return normalizeText(claim.status_label || claim.statusLabel || claim.approval_stage || claim.approvalStage) || STATUS_LABELS[key] || '待确认' +} + +// 状态语义化分类,驱动卡片着色:进行中 / 正向终态 / 需关注 / 异常终态 +function resolveStatusTone(statusLabel = '') { + const text = normalizeText(statusLabel) + if (/草稿|已退回|退回|待补充/.test(text)) { + return 'is-warning' + } + if (/已驳回|驳回|已拒绝|拒绝/.test(text)) { + return 'is-danger' + } + if (/已批准|已审批|已完成|已付款|已支付|已归档|已报销/.test(text)) { + return 'is-success' + } + return 'is-pending' +} + +function resolveStatusKey(claim = {}) { + return normalizeText(claim.status || claim.state || claim.approval_status || claim.approvalStatus).toLowerCase() +} + +function resolveReason(claim = {}) { + return normalizeText(claim.reason || claim.business_reason || claim.description || claim.title || claim.note) || '未填写事由' +} + +function resolveExpenseTypeLabel(claim = {}) { + const key = normalizeText(claim.expense_type || claim.expenseType || claim.type_code || claim.typeCode).toLowerCase() + return TYPE_LABELS[key] || TYPE_LABELS.other +} + +function resolveExpenseTypeCode(claim = {}) { + return normalizeText(claim.expense_type || claim.expenseType || claim.type_code || claim.typeCode).toLowerCase() +} + +function pickText(source = {}, keys = [], fallback = '') { + for (const key of keys) { + const value = normalizeText(source[key]) + if (value) { + return value + } + } + return fallback +} + +function pickRawValue(source = {}, keys = []) { + for (const key of keys) { + const value = source[key] + if (value !== undefined && value !== null && normalizeText(value)) { + return value + } + } + return null +} + +function normalizeMoneyValue(value) { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : null + } + const normalized = normalizeText(value).replace(/,/g, '') + if (!normalized) { + return null + } + const matched = normalized.match(/-?\d+(?:\.\d+)?/) + if (!matched) { + return null + } + const amount = Number(matched[0]) + return Number.isFinite(amount) ? amount : null +} + +function resolveAmountLabel(claim = {}) { + const rawValue = pickRawValue(claim, [ + 'amount', + 'total_amount', + 'totalAmount', + 'claimed_amount', + 'claimedAmount', + 'application_amount', + 'applicationAmount', + 'budget_amount', + 'budgetAmount', + 'estimated_amount', + 'estimatedAmount' + ]) + const amount = normalizeMoneyValue(rawValue) + return amount === null ? '待确认' : MONEY_FORMATTER.format(amount) +} + +function resolveAmountValue(claim = {}) { + return normalizeMoneyValue(pickRawValue(claim, [ + 'amount', + 'total_amount', + 'totalAmount', + 'claimed_amount', + 'claimedAmount', + 'application_amount', + 'applicationAmount', + 'budget_amount', + 'budgetAmount', + 'estimated_amount', + 'estimatedAmount' + ])) +} + +function resolveOwnerLabel(claim = {}) { + return pickText(claim, [ + 'applicant_name', + 'applicantName', + 'employee_name', + 'employeeName', + 'claimant_name', + 'claimantName', + 'created_by_name', + 'createdByName', + 'user_name', + 'userName', + 'applicant', + 'employee' + ], '未显示') +} + +function resolveDepartmentLabel(claim = {}) { + return pickText(claim, [ + 'department_name', + 'departmentName', + 'dept_name', + 'deptName', + 'org_name', + 'orgName', + 'department' + ], '未显示') +} + +function resolveLocationLabel(claim = {}) { + return pickText(claim, [ + 'location', + 'destination', + 'destination_city', + 'destinationCity', + 'city', + 'business_location', + 'businessLocation', + 'place' + ]) +} + +function resolveUpdatedDate(claim = {}) { + return normalizeDateText( + claim.updated_at + || claim.updatedAt + || claim.submitted_at + || claim.submittedAt + || claim.created_at + || claim.createdAt + ) +} + +function resolveRecordDate(claim = {}) { + return normalizeDateText( + claim.occurred_at + || claim.occurredAt + || claim.business_time + || claim.businessTime + || claim.submitted_at + || claim.submittedAt + || claim.created_at + || claim.createdAt + || claim.updated_at + || claim.updatedAt + ) +} + +function resolveTimeLabel(claim = {}, fallbackDate = '') { + const businessTime = pickText(claim, [ + 'business_time', + 'businessTime', + 'trip_time', + 'tripTime', + 'travel_time', + 'travelTime' + ]) + if (businessTime) { + return businessTime + } + + const startDate = normalizeDateText( + claim.start_date + || claim.startDate + || claim.trip_start_date + || claim.tripStartDate + || claim.departure_date + || claim.departureDate + ) + const endDate = normalizeDateText( + claim.end_date + || claim.endDate + || claim.trip_end_date + || claim.tripEndDate + || claim.return_date + || claim.returnDate + ) + if (startDate && endDate && startDate !== endDate) { + return `${startDate} 至 ${endDate}` + } + return startDate || endDate || fallbackDate || '待补充' +} + +function dateInRange(dateText, range) { + if (!range || !range.start || !range.end) { + return true + } + if (!dateText) { + return false + } + return dateText >= range.start && dateText <= range.end +} + +function toTimestamp(dateText) { + const date = parseDate(dateText) + return date ? date.getTime() : 0 +} + +function normalizeRecord(claim = {}) { + const documentType = resolveDocumentTypeCode(claim) + const documentNo = resolveDocumentNo(claim) + const date = resolveRecordDate(claim) + const updatedDate = resolveUpdatedDate(claim) + const reason = resolveReason(claim) + const expenseTypeCode = resolveExpenseTypeCode(claim) + const typeLabel = resolveExpenseTypeLabel(claim) + const statusLabel = resolveStatusLabel(claim) + const ownerLabel = resolveOwnerLabel(claim) + const departmentLabel = resolveDepartmentLabel(claim) + const locationLabel = resolveLocationLabel(claim) + return { + id: resolveClaimId(claim), + claimId: resolveClaimId(claim), + claimNo: documentNo, + documentNo, + documentType, + documentTypeLabel: documentType === 'application' ? '申请单' : '报销单', + expenseTypeCode, + typeLabel, + time: resolveTimeLabel(claim, date), + dateKey: date, + updatedTime: updatedDate || '未显示', + statusKey: resolveStatusKey(claim), + statusLabel, + statusTone: resolveStatusTone(statusLabel), + reason, + amountLabel: resolveAmountLabel(claim), + amountValue: resolveAmountValue(claim), + ownerLabel, + departmentLabel, + locationLabel, + searchableText: compactText([ + documentNo, + reason, + ownerLabel, + departmentLabel, + locationLabel, + typeLabel, + statusLabel + ].join(' ')) + } +} + +function matchesStatusFilter(record = {}, statusFilter = null) { + if (!statusFilter) { + return true + } + return statusFilter.keys.includes(record.statusKey) || statusFilter.label === record.statusLabel +} + +function matchesExpenseTypeFilter(record = {}, expenseTypeFilter = null) { + if (!expenseTypeFilter) { + return true + } + return expenseTypeFilter.codes.includes(record.expenseTypeCode) +} + +function matchesKeywordFilter(record = {}, keywordFilter = null) { + if (!keywordFilter?.keyword) { + return true + } + return record.searchableText.includes(compactText(keywordFilter.keyword)) +} + +function matchesAmountFilter(record = {}, amountFilter = null) { + if (!amountFilter) { + return true + } + if (record.amountValue === null || record.amountValue === undefined) { + return false + } + if (amountFilter.min !== null && amountFilter.min !== undefined && record.amountValue < amountFilter.min) { + return false + } + if (amountFilter.max !== null && amountFilter.max !== undefined && record.amountValue > amountFilter.max) { + return false + } + return true +} + +export function filterAiDocumentQueryRecords(claimsPayload, intent = {}) { + const rows = extractExpenseClaimItems(claimsPayload) + .map((claim) => normalizeRecord(claim)) + .filter((record) => ( + !intent?.documentType || + intent.documentType === 'all' || + record.documentType === intent.documentType + )) + .filter((record) => dateInRange(record.dateKey, intent?.timeRange)) + .filter((record) => matchesStatusFilter(record, intent?.statusFilter)) + .filter((record) => matchesExpenseTypeFilter(record, intent?.expenseTypeFilter)) + .filter((record) => matchesKeywordFilter(record, intent?.keywordFilter)) + .filter((record) => matchesAmountFilter(record, intent?.amountFilter)) + .sort((left, right) => toTimestamp(right.dateKey) - toTimestamp(left.dateKey)) + + return rows +} + +function buildDocumentDetailHref(record = {}) { + const reference = normalizeText(record.documentNo || record.claimNo || record.claimId || record.id) + return reference ? `#ai-open-document-detail:${encodeURIComponent(reference)}` : '' +} + +function buildDocumentCardHtml(record = {}) { + const href = buildDocumentDetailHref(record) + const typeClass = record.documentType === 'application' ? 'application' : 'reimbursement' + const statusTone = record.statusTone || 'is-pending' + const amountLabel = record.documentType === 'application' ? '预计金额' : '报销金额' + + // footer 左侧辅助元信息:业务地点(可选)+ 时间 + const metaParts = [] + if (record.locationLabel) { + metaParts.push(`${escapeHtml(record.locationLabel)}`) + } + metaParts.push(`${escapeHtml(record.time || '待补充')}`) + const metaHtml = `
${metaParts.join('·')}
` + + return [ + `
`, + '
', + '
', + `${escapeHtml(record.statusLabel)}`, + `${escapeHtml(record.documentTypeLabel)} · ${escapeHtml(record.typeLabel)}`, + '
', + `${escapeHtml(record.documentNo || '未编号单据')}`, + '
', + '
', + `${escapeHtml(record.reason)}`, + '
', + `${escapeHtml(record.ownerLabel)}`, + '·', + `${escapeHtml(record.departmentLabel)}`, + '
', + '
', + '
', + metaHtml, + '
', + `${escapeHtml(amountLabel)}`, + `${escapeHtml(record.amountLabel)}`, + '
', + href + ? `查看详情` + : '', + '
', + '
' + ].join('') +} + +function buildDocumentCardsHtml(records = []) { + return [ + '', + '
', + ...records.map((record) => buildDocumentCardHtml(record)), + '
', + '' + ].join('\n') +} + +function buildQueryScopeText(intent = {}) { + return [ + intent.sourceLabel || '相关单据', + intent.documentTypeLabel && intent.documentTypeLabel !== '全部单据' ? intent.documentTypeLabel : '', + intent.timeRange?.label || '', + intent.statusFilter?.label ? `状态:${intent.statusFilter.label}` : '', + intent.expenseTypeFilter?.label ? `费用类型:${intent.expenseTypeFilter.label}` : '', + intent.keywordFilter?.label ? `关键词:${intent.keywordFilter.label}` : '', + intent.amountFilter?.label ? `金额:${intent.amountFilter.label}` : '' + ].filter(Boolean).join(' / ') +} + +export function buildAiDocumentQueryConditionSummary(intent = {}) { + const conditions = [ + `查询来源:${intent.sourceLabel || '相关单据'}`, + `单据类型:${intent.documentTypeLabel || '全部单据'}`, + `时间范围:${intent.timeRange?.label || '不限'}`, + intent.statusFilter?.label ? `状态:${intent.statusFilter.label}` : '', + intent.expenseTypeFilter?.label ? `费用类型:${intent.expenseTypeFilter.label}` : '', + intent.keywordFilter?.label ? `关键词:${intent.keywordFilter.label}` : '', + intent.amountFilter?.label ? `金额:${intent.amountFilter.label}` : '' + ].filter(Boolean) + return conditions.join(';') +} + +export function buildAiDocumentQueryMessage(intent = {}, claimsPayload = []) { + const records = filterAiDocumentQueryRecords(claimsPayload, intent) + const visibleRecords = records.slice(0, DOCUMENT_QUERY_LIMIT) + const scopeText = buildQueryScopeText(intent) + + if (!records.length) { + return [ + '### 未查询到相关单据', + '', + `**查询范围**:${scopeText || '相关单据'}。`, + '', + '当前没有匹配的单据。可以继续告诉我更具体的单据类型、时间范围或状态,我会重新筛选。' + ].join('\n') + } + + const lines = [ + '### 已查询到相关单据', + '', + `**查询范围**:${scopeText || '相关单据'};共找到 **${records.length}** 张,先展示最近 **${visibleRecords.length}** 张。`, + '', + buildDocumentCardsHtml(visibleRecords) + ] + + if (records.length > visibleRecords.length) { + lines.push('', `还有 ${records.length - visibleRecords.length} 张未展示;可以继续补充时间、类型或状态缩小范围。`) + } + return lines.join('\n') +} diff --git a/web/src/utils/archiveCenterListFilters.js b/web/src/utils/archiveCenterListFilters.js index 9d994f5..c2a5a3d 100644 --- a/web/src/utils/archiveCenterListFilters.js +++ b/web/src/utils/archiveCenterListFilters.js @@ -3,13 +3,33 @@ import { isRiskSummaryWithRisk, normalizeRiskFlagTone } from './riskFlags.js' +import { canViewRiskForContext } from './riskVisibility.js' export const ARCHIVE_FILTER_ALL = 'all' -export function countClaimRisks(riskFlags, riskSummary) { +// 按当前查看者可见性过滤风险 flag,确保列表与详情页对同一用户展示一致的风险口径。 +// viewerOptions 为空时(如未提供用户上下文)原样返回,保持向后兼容。 +function filterRiskFlagsForViewer(riskFlags, viewerOptions) { + const flags = Array.isArray(riskFlags) ? riskFlags : [] + if (!viewerOptions || !viewerOptions.request) { + return flags + } + return flags.filter((flag) => { + if (!isActionableRiskFlag(flag)) { + return false + } + if (flag && typeof flag === 'object') { + return canViewRiskForContext(flag, viewerOptions) + } + return true + }) +} + +export function countClaimRisks(riskFlags, riskSummary, viewerOptions) { let count = 0 - for (const flag of Array.isArray(riskFlags) ? riskFlags : []) { + const visibleFlags = filterRiskFlagsForViewer(riskFlags, viewerOptions) + for (const flag of visibleFlags) { if (!isActionableRiskFlag(flag)) { continue } @@ -44,10 +64,11 @@ export function countClaimRisks(riskFlags, riskSummary) { return count } -export function resolveArchiveRiskTone(riskFlags, riskSummary) { +export function resolveArchiveRiskTone(riskFlags, riskSummary, viewerOptions) { let tone = 'low' - for (const flag of Array.isArray(riskFlags) ? riskFlags : []) { + const visibleFlags = filterRiskFlagsForViewer(riskFlags, viewerOptions) + for (const flag of visibleFlags) { if (!isActionableRiskFlag(flag)) { continue } diff --git a/web/src/utils/expenseApplicationPreview.js b/web/src/utils/expenseApplicationPreview.js index 4f3d22f..d69a811 100644 --- a/web/src/utils/expenseApplicationPreview.js +++ b/web/src/utils/expenseApplicationPreview.js @@ -756,14 +756,6 @@ export function buildApplicationPolicyEstimateRequest(preview = {}, currentUser const transportMode = String(fields.transportMode || '').trim() const shouldEstimate = /差旅|住宿|交通/.test(applicationType) || Boolean(transportMode) - if (/差旅|出差/.test(applicationType) && !transportMode) { - return { - canCalculate: false, - reason: '缺少出行方式', - payload: null - } - } - if (!shouldEstimate || !days || !location) { return { canCalculate: false, @@ -794,12 +786,8 @@ export function buildApplicationPolicyEstimateRequest(preview = {}, currentUser } export function applyApplicationPolicyEstimateResult(preview = {}, result = {}, currentUser = {}) { - const resultTransportMode = String(result?.transport_mode || '').trim() const fields = { - ...(preview?.fields || {}), - ...(!String(preview?.fields?.transportMode || '').trim() && resultTransportMode - ? { transportMode: resultTransportMode } - : {}) + ...(preview?.fields || {}) } const hotelRate = formatPolicyMoney(result?.hotel_rate) const hotelAmount = formatPolicyMoney(result?.hotel_amount) @@ -808,6 +796,11 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {}, const matchedCity = String(result?.matched_city || fields.location || '').trim() const grade = String(result?.grade || fields.grade || resolveCurrentUserGrade(currentUser)).trim() if (isTravelApplicationType(fields.applicationType) && !String(fields.transportMode || '').trim()) { + const days = Number(result?.days) || parseApplicationDaysValue(fields.days) || 1 + const baseTotalAmount = parseMoneyNumber(result?.hotel_amount) + parseMoneyNumber(result?.allowance_amount) + const baseTotalDisplay = Number.isFinite(baseTotalAmount) && baseTotalAmount > 0 + ? formatPolicyMoney(baseTotalAmount) + : '' return normalizeApplicationPreview({ ...preview, fields: { @@ -816,7 +809,10 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {}, lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate), subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate), transportPolicy: APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT, - policyEstimate: APPLICATION_POLICY_PENDING_TEXT, + policyEstimate: baseTotalDisplay + ? `交通待补充 + 住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${baseTotalDisplay}元(${days}天,不含交通)` + : APPLICATION_POLICY_PENDING_TEXT, + amount: baseTotalDisplay ? `${baseTotalDisplay}元(不含交通)` : fields.amount, matchedCity, ruleName: String(result?.rule_name || '').trim(), ruleVersion: String(result?.rule_version || '').trim(), @@ -827,7 +823,7 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {}, transportQueryLatencyMs: '', transportEstimateSource: '', transportEstimateConfidence: '', - policyTotalAmount: '' + policyTotalAmount: baseTotalDisplay ? `${baseTotalDisplay}元(不含交通)` : '' }, policyEstimateStatus: 'pending' }) diff --git a/web/src/utils/markdown.js b/web/src/utils/markdown.js index 0a4425b..17507c0 100644 --- a/web/src/utils/markdown.js +++ b/web/src/utils/markdown.js @@ -25,6 +25,25 @@ const ACTION_LINK_CLASS_BY_HREF = { '#review-quick-edit': 'markdown-action-link-edit', '#review-risk-panel': 'markdown-action-link-risk' } +const DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-detail:' +const TRUSTED_HTML_BLOCK_RE = /\s*([\s\S]*?)\s*/g +const TRUSTED_HTML_PLACEHOLDER_PREFIX = 'AI_TRUSTED_HTML_BLOCK_' +const TRUSTED_HTML_ALLOWED_TAGS = new Set([ + 'section', + 'article', + 'header', + 'footer', + 'div', + 'span', + 'strong', + 'a' +]) +const TRUSTED_HTML_ALLOWED_ATTRS = new Set([ + 'aria-label', + 'class', + 'data-ai-action', + 'href' +]) function escapeHtml(text) { return String(text || '') @@ -43,6 +62,9 @@ function renderRiskText(text) { function resolveActionLinkClass(href) { const normalizedHref = String(href || '').trim() + if (normalizedHref.startsWith(DOCUMENT_DETAIL_HREF_PREFIX)) { + return 'markdown-action-link-document' + } return ACTION_LINK_CLASS_BY_HREF[normalizedHref] || '' } @@ -214,7 +236,76 @@ function normalizeColonHeadings(text) { return normalizedLines.join('\n').replace(/\n{3,}/g, '\n\n') } -export function renderMarkdown(text = '') { - const normalized = normalizeColonHeadings(text).trim() - return normalized ? markdown.render(normalized) : '' +function hasOnlyTrustedHtmlTags(html = '') { + const tagPattern = /<\/?([a-z][\w-]*)([^>]*)>/gi + let match = tagPattern.exec(html) + while (match) { + const tagName = String(match[1] || '').toLowerCase() + if (!TRUSTED_HTML_ALLOWED_TAGS.has(tagName)) { + return false + } + const attrText = String(match[2] || '') + const attrPattern = /\s([:@\w-]+)\s*=/g + let attrMatch = attrPattern.exec(attrText) + while (attrMatch) { + const attrName = String(attrMatch[1] || '').toLowerCase() + if (!TRUSTED_HTML_ALLOWED_ATTRS.has(attrName)) { + return false + } + attrMatch = attrPattern.exec(attrText) + } + match = tagPattern.exec(html) + } + return true +} + +function sanitizeTrustedHtmlBlock(html = '') { + const value = String(html || '').trim() + if (!value || !value.includes('class="ai-document-card-list"')) { + return '' + } + if (/<(?:script|style|iframe|object|embed|link|meta|form|input|button|textarea|select)\b/i.test(value)) { + return '' + } + if (/\son[a-z]+\s*=/i.test(value) || /javascript\s*:/i.test(value)) { + return '' + } + if (!hasOnlyTrustedHtmlTags(value)) { + return '' + } + const hrefs = [...value.matchAll(/\shref="([^"]*)"/gi)].map((match) => String(match[1] || '').trim()) + if (hrefs.some((href) => !href.startsWith(DOCUMENT_DETAIL_HREF_PREFIX))) { + return '' + } + return value +} + +function extractTrustedHtmlBlocks(text = '') { + const trustedHtmlBlocks = [] + const content = String(text || '').replace(TRUSTED_HTML_BLOCK_RE, (_match, html) => { + const sanitizedHtml = sanitizeTrustedHtmlBlock(html) + if (!sanitizedHtml) { + return '' + } + const placeholder = `${TRUSTED_HTML_PLACEHOLDER_PREFIX}${trustedHtmlBlocks.length}` + trustedHtmlBlocks.push(sanitizedHtml) + return `\n\n${placeholder}\n\n` + }) + return { content, trustedHtmlBlocks } +} + +function restoreTrustedHtmlBlocks(html = '', trustedHtmlBlocks = []) { + return trustedHtmlBlocks.reduce((nextHtml, block, index) => { + const placeholder = `${TRUSTED_HTML_PLACEHOLDER_PREFIX}${index}` + const paragraphPattern = new RegExp(`

${placeholder}

\\n?`, 'g') + return nextHtml + .replace(paragraphPattern, block) + .replaceAll(placeholder, block) + }, html) +} + +export function renderMarkdown(text = '') { + const { content, trustedHtmlBlocks } = extractTrustedHtmlBlocks(text) + const normalized = normalizeColonHeadings(content).trim() + return normalized ? restoreTrustedHtmlBlocks(markdown.render(normalized), trustedHtmlBlocks) : '' } diff --git a/web/src/utils/riskVisibility.js b/web/src/utils/riskVisibility.js index b79412b..7396cb4 100644 --- a/web/src/utils/riskVisibility.js +++ b/web/src/utils/riskVisibility.js @@ -121,9 +121,6 @@ export function resolveRiskActionability(flag, options = {}) { if (source === 'attachment_analysis') { return 'fixable_by_submitter' } - if (stage === 'expense_application') { - return 'review_decision' - } if (['policy', 'invoice', 'trip', 'amount'].includes(domain)) { return 'fixable_by_submitter' } @@ -147,9 +144,6 @@ export function resolveRiskVisibilityScope(flag, options = {}) { if (actionability === 'fixable_by_submitter') { return 'submitter' } - if (stage === 'expense_application') { - return 'leader' - } return 'finance' } @@ -226,8 +220,10 @@ export function canViewRiskForContext(flag, options = {}) { return false } if (stage === 'expense_application') { + // 申请单阶段:申请人可见可自行整改的风险(信息完整性/差旅/金额等), + // 以便申请时知晓风险及原因;预算类仅预算审批人可见;其余(画像/审批流程)仅领导/审批人可见。 if (context.isCurrentApplicant) { - return false + return actionability === 'fixable_by_submitter' || visibilityScope === 'submitter' } if (riskDomain === 'budget' || actionability === 'budget_governance' || visibilityScope === 'budget_manager') { return context.isBudgetReviewer diff --git a/web/src/views/DocumentsCenterView.vue b/web/src/views/DocumentsCenterView.vue index 2084b6c..d7288ff 100644 --- a/web/src/views/DocumentsCenterView.vue +++ b/web/src/views/DocumentsCenterView.vue @@ -258,6 +258,7 @@ import EnterprisePagination from '../components/shared/EnterprisePagination.vue' import TableEmptyState from '../components/shared/TableEmptyState.vue' import TableLoadingState from '../components/shared/TableLoadingState.vue' import { useMinimumVisibleState } from '../composables/useMinimumVisibleState.js' +import { useSystemState } from '../composables/useSystemState.js' import { mapExpenseClaimToRequest } from '../composables/useRequests.js' import { extractExpenseClaimItems, @@ -377,6 +378,8 @@ const emit = defineEmits([ 'summary-change' ]) +const { currentUser } = useSystemState() + function readDocumentCenterQueryText(key) { const value = route.query?.[key] return String(Array.isArray(value) ? value[0] || '' : value || '').trim() @@ -781,7 +784,12 @@ function resolveDocumentRiskFlags(row) { function buildDocumentRiskMeta(row) { const riskFlags = resolveDocumentRiskFlags(row) const riskSummary = row?.riskSummary || row?.risk - const count = countClaimRisks(riskFlags, riskSummary) + // 列表风险标签按当前查看者可见性过滤,与详情页口径一致: + // 申请人看不到的预算治理等风险不计入列表展示的风险等级。 + const viewerOptions = currentUser.value + ? { request: row || {}, currentUser: currentUser.value } + : null + const count = countClaimRisks(riskFlags, riskSummary, viewerOptions) if (!count) { const meta = RISK_TONE_META.none return { @@ -791,7 +799,7 @@ function buildDocumentRiskMeta(row) { } } - const tone = resolveArchiveRiskTone(riskFlags, riskSummary) + const tone = resolveArchiveRiskTone(riskFlags, riskSummary, viewerOptions) const meta = RISK_TONE_META[tone] || RISK_TONE_META.medium return { ...meta, diff --git a/web/src/views/PersonalWorkbenchView.vue b/web/src/views/PersonalWorkbenchView.vue index 90b154d..913f06b 100644 --- a/web/src/views/PersonalWorkbenchView.vue +++ b/web/src/views/PersonalWorkbenchView.vue @@ -6,6 +6,7 @@ :sidebar-command="aiSidebarCommand" @conversation-change="emit('ai-conversation-change', $event)" @conversation-history-change="emit('ai-conversation-history-change', $event)" + @open-document="emit('open-document', $event)" /> 0) - || (!isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value && hasVisibleRiskCards.value) + || (!isEditableRequest.value && isCurrentApplicant.value && hasVisibleRiskCards.value) )) function normalizeRiskDomId(value) { @@ -1750,21 +1750,24 @@ export default { } const aiAdviceTitle = computed(() => { - if (!isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value) { - return '风险提示' + if (!isEditableRequest.value && isCurrentApplicant.value) { + return isApplicationDocument.value ? '申请风险提示' : '风险提示' } if (isEditableRequest.value && isApplicationDocument.value) { return '表单自查提示' } return isEditableRequest.value ? 'AI建议' : '风险提示' }) - const aiAdviceHint = computed(() => ( - !isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value - ? '展示票据、行程、金额等可自行修正的风险点,便于提交人先整改,减少后续退单。' - : isEditableRequest.value - ? (isApplicationDocument.value ? '仅提示申请表单本身需要补充的内容,不展示预算治理细节。' : '系统会在草稿保存和附件识别后自动更新检测结果。') - : '展示系统已识别的风险点,便于审批和后续整改。' - )) + const aiAdviceHint = computed(() => { + if (!isEditableRequest.value && isCurrentApplicant.value) { + return isApplicationDocument.value + ? '展示申请单已识别的风险点及原因,请逐条确认或补充说明后再提交给领导审批。' + : '展示票据、行程、金额等可自行修正的风险点,便于提交人先整改,减少后续退单。' + } + return isEditableRequest.value + ? (isApplicationDocument.value ? '仅提示申请表单本身需要补充的内容,不展示预算治理细节。' : '系统会在草稿保存和附件识别后自动更新检测结果。') + : '展示系统已识别的风险点,便于审批和后续整改。' + }) const submitActionLabel = computed(() => { return resolveSubmitActionLabel({ diff --git a/web/src/views/scripts/useApplicationPreviewEditor.js b/web/src/views/scripts/useApplicationPreviewEditor.js index dddf541..80e6494 100644 --- a/web/src/views/scripts/useApplicationPreviewEditor.js +++ b/web/src/views/scripts/useApplicationPreviewEditor.js @@ -252,6 +252,7 @@ export function useApplicationPreviewEditor({ resolveApplicationPreviewRows, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, + refreshApplicationPreviewEstimate, isApplicationPreviewEditing, isApplicationPreviewDateEditorOpen, openApplicationPreviewEditor, diff --git a/web/tests/ai-application-precheck-model.test.mjs b/web/tests/ai-application-precheck-model.test.mjs new file mode 100644 index 0000000..aecf1ca --- /dev/null +++ b/web/tests/ai-application-precheck-model.test.mjs @@ -0,0 +1,114 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + buildAiApplicationPrecheck, + buildAiApplicationPrecheckMessage, + buildAiApplicationPrecheckThinkingEvents +} from '../src/utils/aiApplicationPrecheckModel.js' + +const preview = { + fields: { + applicationType: '差旅费用申请', + time: '2026-02-20 至 2026-02-23', + location: '上海', + reason: '辅助国网仿生产服务器部署', + amount: '2,120元', + days: '4天', + transportMode: '火车' + }, + missingFields: [] +} + +test('application precheck blocks application generation when existing application overlaps', () => { + const precheck = buildAiApplicationPrecheck(preview, { + currentUser: { name: '曹笑竹', departmentName: '技术部' }, + claimsPayload: { + items: [ + { + claim_no: 'AP-OVERLAP', + document_type: 'expense_application', + expense_type: 'travel_application', + employee_name: '曹笑竹', + status: 'submitted', + risk_flags_json: [ + { + source: 'application_detail', + application_detail: { + business_time: '2026-02-21 至 2026-02-22', + reason: '同时间段现场支持', + location: '上海' + } + } + ] + } + ] + }, + budgetSummary: { + total_amount: 10000, + reserved_amount: 8000, + consumed_amount: 500, + available_amount: 1500 + } + }) + + assert.equal(precheck.overlap.status, 'warning') + assert.match(precheck.overlap.summary, /可能重叠/) + assert.equal(precheck.overlap.matches[0].claimNo, 'AP-OVERLAP') + assert.equal(precheck.budget.status, 'warning') + assert.equal(precheck.budget.requiresBudgetReview, true) + assert.match(precheck.budget.summary, /预算管理者审核/) + + const message = buildAiApplicationPrecheckMessage(preview, precheck) + assert.match(message, /### 发现同时间段已有申请单/) + assert.match(message, /时间重叠提醒/) + assert.match(message, /AP-OVERLAP/) + assert.match(message, /\| 单据编号 \| 申请时间 \| 状态 \| 事由 \| 操作 \|/) + assert.match(message, /\| AP-OVERLAP \| 2026-02-21 至 2026-02-22 \| 审批中 \| 同时间段现场支持 \| \[查看\]\(#ai-open-application-detail:AP-OVERLAP\) \|/) + assert.match(message, /2026-02-21 至 2026-02-22/) + assert.match(message, /同时间段现场支持/) + assert.match(message, /请先检查本次申请时间是否填写正确/) + assert.doesNotMatch(message, /出差申请表草稿已生成/) +}) + +test('application precheck emits thinking events for overlap, budget, and form generation', () => { + const precheck = buildAiApplicationPrecheck(preview, { + currentUser: { name: '曹笑竹' }, + claimsPayload: [], + budgetSummary: { + total_amount: 10000, + reserved_amount: 1000, + consumed_amount: 1000, + available_amount: 8000 + } + }) + const events = buildAiApplicationPrecheckThinkingEvents(precheck) + + assert.equal(events.length, 3) + assert.deepEqual( + events.map((event) => event.eventId), + ['application-precheck-overlap', 'application-precheck-budget', 'application-precheck-form'] + ) + assert.match(events[1].content, /预算/) +}) + +test('application precheck ignores application candidates without parseable business time', () => { + const precheck = buildAiApplicationPrecheck(preview, { + currentUser: { name: '曹笑竹' }, + claimsPayload: { + items: [ + { + claim_no: 'AP-NO-TIME', + document_type: 'expense_application', + expense_type: 'travel_application', + employee_name: '曹笑竹', + status: 'submitted' + } + ] + }, + budgetSummary: {} + }) + + assert.equal(precheck.overlap.status, 'ok') + assert.deepEqual(precheck.overlap.matches, []) +}) diff --git a/web/tests/ai-application-preview-actions.test.mjs b/web/tests/ai-application-preview-actions.test.mjs new file mode 100644 index 0000000..256a6cb --- /dev/null +++ b/web/tests/ai-application-preview-actions.test.mjs @@ -0,0 +1,127 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + AI_APPLICATION_ACTION_SAVE_DRAFT, + AI_APPLICATION_ACTION_SUBMIT, + buildAiApplicationPreviewActionPayload +} from '../src/services/aiApplicationPreviewActions.js' +import { + applyApplicationPolicyEstimateResult, + buildApplicationPolicyEstimateRequest, + buildLocalApplicationPreview +} from '../src/utils/expenseApplicationPreview.js' + +const applicationPreview = { + fields: { + applicationType: '差旅费用申请', + applicant: '曹笑竹', + grade: 'P5', + department: '技术部', + position: '财务智能化产品经理', + managerName: '向万红', + time: '2026-02-20 至 2026-02-23', + location: '上海', + reason: '辅助国网仿生产服务器部署', + days: '4天', + transportMode: '火车', + lodgingDailyCap: '250元/天', + subsidyDailyCap: '100元/天', + transportPolicy: '按交通费用预估表暂估', + policyEstimate: '交通 720元 + 住宿 1,000元 + 补贴 400元 = 2,120元(4天)', + amount: '2,120元' + } +} + +const currentUser = { + username: 'caoxiaozhu@xf.com', + name: '曹笑竹', + departmentName: '技术部', + position: '财务智能化产品经理', + grade: 'P5', + managerName: '向万红', + roleCodes: ['employee'] +} + +test('save application preview payload uses save draft action without submit wording', () => { + const payload = buildAiApplicationPreviewActionPayload({ + actionType: AI_APPLICATION_ACTION_SAVE_DRAFT, + applicationPreview, + currentUser, + conversationId: 'inline-1' + }) + + assert.equal(payload.user_id, 'caoxiaozhu@xf.com') + assert.equal(payload.conversation_id, 'inline-1') + assert.equal(payload.context_json.session_type, 'application') + assert.equal(payload.context_json.review_action, undefined) + assert.equal(payload.context_json.application_action, 'save_draft') + assert.equal(payload.context_json.application_preview.fields.transportMode, '火车') + assert.match(payload.message, /费用申请保存草稿/) + assert.match(payload.message, /保存草稿/) + assert.doesNotMatch(payload.message, /确认提交/) +}) + +test('submit application preview payload keeps existing draft id for resubmission', () => { + const payload = buildAiApplicationPreviewActionPayload({ + actionType: AI_APPLICATION_ACTION_SUBMIT, + applicationPreview, + currentUser, + conversationId: 'inline-1', + draftPayload: { + claim_id: 'draft-001', + claim_no: 'AP-202602200001' + } + }) + + assert.equal(payload.context_json.review_action, undefined) + assert.equal(payload.context_json.application_edit_claim_id, 'draft-001') + assert.equal(payload.context_json.draft_claim_id, 'draft-001') + assert.match(payload.message, /费用申请确认提交/) + assert.match(payload.message, /确认提交/) +}) + +test('travel application preview calculates base standards before transport mode is selected', () => { + const preview = buildLocalApplicationPreview( + '2月20-23日去上海出差,辅助国网仿生产服务器部署', + { name: '曹笑竹', grade: 'P5', location: '武汉' }, + { today: '2026-06-20' } + ) + const request = buildApplicationPolicyEstimateRequest(preview, { grade: 'P5', location: '武汉' }) + + assert.equal(request.canCalculate, true) + assert.deepEqual(request.payload, { + days: 4, + location: '上海', + grade: 'P5', + transport_mode: null, + origin_location: '武汉', + travel_date: '2026-02-20' + }) + + const estimatedPreview = applyApplicationPolicyEstimateResult(preview, { + days: 4, + location: '上海', + matched_city: '上海', + grade: 'P5', + hotel_rate: 450, + hotel_amount: 1800, + total_allowance_rate: 100, + allowance_amount: 400, + transport_mode: '火车', + transport_origin: '武汉', + transport_destination: '上海', + transport_estimated_amount: 720, + total_amount: 2200, + rule_name: '公司差旅费报销规则', + rule_version: 'v1.0.0' + }, { grade: 'P5', location: '武汉' }) + + assert.equal(estimatedPreview.fields.transportMode, '') + assert.equal(estimatedPreview.missingFields.includes('出行方式'), true) + assert.equal(estimatedPreview.fields.lodgingDailyCap, '450元/天') + assert.equal(estimatedPreview.fields.subsidyDailyCap, '100元/天') + assert.equal(estimatedPreview.fields.transportPolicy, '选择火车、飞机或轮船后自动预估交通费用') + assert.equal(estimatedPreview.fields.policyEstimate, '交通待补充 + 住宿 1,800元 + 补贴 400元 = 2,200元(4天,不含交通)') + assert.equal(estimatedPreview.fields.amount, '2,200元(不含交通)') +}) diff --git a/web/tests/ai-conversation-html-renderer.test.mjs b/web/tests/ai-conversation-html-renderer.test.mjs new file mode 100644 index 0000000..5a1cadd --- /dev/null +++ b/web/tests/ai-conversation-html-renderer.test.mjs @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js' + +test('AI conversation renderer turns business copy into spacious semantic HTML', () => { + const rendered = renderAiConversationHtml([ + '### 出差申请办理确认', + '', + '**我已在您的输入中提取到关键信息**,如下表所示:', + '', + '> **前置查询结果**:我已查询您名下可关联的差旅申请单,当前未查到可关联单据。', + '', + '> **需要您确认**:发起新的出差申请属于业务操作,需要您手动确认后我再继续办理。', + '', + '点击下方 **确认发起出差申请** 后,我会继续完成:', + '', + '- **单据重叠核查**:检查同一时间段是否已有申请单,避免重复申请。', + '- **预算与审批预审**:查看部门预算影响,判断是否可能增加预算管理者审核。' + ].join('\n')) + + assert.match(rendered, /
/) + assert.match(rendered, /

出差申请办理确认<\/h3>/) + assert.match(rendered, /
/) + assert.match(rendered, /
[\s\S]*前置查询结果[\s\S]*当前未查到可关联单据/) + assert.match(rendered, /
[\s\S]*需要您确认[\s\S]*需要您手动确认后我再继续办理/) + assert.match(rendered, /
    [\s\S]*单据重叠核查[\s\S]*预算与审批预审/) + assert.doesNotMatch(rendered, /
    /) + assert.doesNotMatch(rendered, /
      \s*
    • /) +}) + +test('AI conversation renderer supports tables and escapes unsafe HTML', () => { + const rendered = renderAiConversationHtml([ + '### 查询结果', + '', + '| 字段 | 内容 |', + '| --- | --- |', + '| 事由 | 辅助 部署 |', + '| 地点 | 上海 |' + ].join('\n')) + + assert.match(rendered, /
      /) + assert.match(rendered, /字段<\/th>/) + assert.match(rendered, /<script>alert\(1\)<\/script>/) + assert.doesNotMatch(rendered, /
', + '
', + '' + ].join('\n')) + + assert.match(rendered, /

查询结果<\/h3>/) + assert.doesNotMatch(rendered, /ai-document-card-list/) + assert.doesNotMatch(rendered, /