feat(web): AI 工作台多 task 串行推进与会话适配
- useWorkbenchAiApplicationPreviewFlow/useWorkbenchAiActionRouter/useWorkbenchAiCommandIntents 支持 task1 完成后自动推进 task2,确认按钮直接拉起申请预览,草稿/提交成功后继续推进下一 task - workbenchAiIntentPlannerModel/workbenchAiMessageModel/workbenchAiCommandIntentModel 适配多 task 意图规划与消息结构 - aiApplicationPreviewActions/aiApplicationPrecheckModel/aiExpenseDraftModel/aiWorkbenchConversationStore 草稿与会话存储适配 - PersonalWorkbenchAiMode 与样式适配,更新 preview-actions/expense-draft/conversation-store/fast-preview/action-router/command-intent/intent-planner 测试
This commit is contained in:
@@ -1274,71 +1274,71 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(li::marker) {
|
||||
color: #2563eb;
|
||||
font-weight: 850;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(strong) {
|
||||
color: #0f172a;
|
||||
font-weight: 850;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(hr) {
|
||||
margin: 26px 0;
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(226, 232, 240, 0.9);
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(blockquote) {
|
||||
margin: 18px 0 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;
|
||||
border-left: 3px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.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;
|
||||
border-left: 3px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.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);
|
||||
padding-left: 20px;
|
||||
border-left: 3px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-focus-card) {
|
||||
padding: 11px 0 16px;
|
||||
padding: 8px 0 12px;
|
||||
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);
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-focus-label) {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #1d4ed8;
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-focus-card p) {
|
||||
color: #475569;
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
line-height: 1.72;
|
||||
color: #1e293b;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-steps),
|
||||
@@ -1366,9 +1366,9 @@
|
||||
padding-top: 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #1d4ed8;
|
||||
font-size: 17px;
|
||||
font-weight: 900;
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@@ -1482,34 +1482,27 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
||||
--ai-document-card-head-bg: rgba(37, 99, 235, 0.075);
|
||||
--ai-document-card-head-bg: rgba(241, 245, 249, 0.5);
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background-color: #ffffff;
|
||||
background-image:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.9)),
|
||||
url("../../ai-document-card-bg.png");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(203, 213, 225, 0.5),
|
||||
0 1px 2px rgba(15, 23, 42, 0.035),
|
||||
0 14px 34px rgba(15, 23, 42, 0.05);
|
||||
0 1px 2px 0 rgba(15, 23, 42, 0.05);
|
||||
color: #334155;
|
||||
animation: workbenchDocumentCardReveal 360ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||
transition: box-shadow 180ms ease, transform 180ms ease;
|
||||
transition: box-shadow 180ms ease, border-color 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card:hover) {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(148, 163, 184, 0.46),
|
||||
0 1px 2px rgba(15, 23, 42, 0.04),
|
||||
0 18px 38px rgba(15, 23, 42, 0.07);
|
||||
0 4px 6px -1px rgba(15, 23, 42, 0.08),
|
||||
0 2px 4px -2px rgba(15, 23, 42, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -1532,8 +1525,9 @@
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 13px 18px 13px 20px;
|
||||
padding: 12px 18px;
|
||||
background: var(--ai-document-card-head-bg);
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__status) {
|
||||
@@ -1543,31 +1537,31 @@
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #1d4ed8;
|
||||
font-size: 15px;
|
||||
font-weight: 860;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__head) {
|
||||
background: rgba(22, 163, 74, 0.08);
|
||||
background: rgba(240, 253, 250, 0.6);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__head) {
|
||||
background: rgba(217, 119, 6, 0.09);
|
||||
background: rgba(254, 243, 199, 0.6);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger .ai-document-card__head) {
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
background: rgba(254, 226, 226, 0.6);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-pending .ai-document-card__status) {
|
||||
color: #1d4ed8;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__status) {
|
||||
color: #15803d;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__status) {
|
||||
@@ -1588,9 +1582,9 @@
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__reason) {
|
||||
display: -webkit-box;
|
||||
min-width: 0;
|
||||
color: #1e40af;
|
||||
color: #1e293b;
|
||||
font-size: 15px;
|
||||
font-weight: 760;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 1;
|
||||
@@ -1598,19 +1592,19 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__reason) {
|
||||
color: #166534;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__reason) {
|
||||
color: #92400e;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger .ai-document-card__reason) {
|
||||
color: #991b1b;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--application) {
|
||||
--ai-document-card-head-bg: rgba(37, 99, 235, 0.075);
|
||||
--ai-document-card-head-bg: rgba(239, 246, 255, 0.5);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--application .ai-document-card__head) {
|
||||
@@ -1618,11 +1612,11 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--application .ai-document-card__reason) {
|
||||
color: #1e40af;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--reimbursement) {
|
||||
--ai-document-card-head-bg: rgba(13, 148, 136, 0.075);
|
||||
--ai-document-card-head-bg: rgba(240, 253, 250, 0.5);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--reimbursement .ai-document-card__head) {
|
||||
@@ -1630,15 +1624,11 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--reimbursement .ai-document-card__reason) {
|
||||
color: #0f766e;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--approval-task) {
|
||||
--ai-document-card-head-bg: rgba(245, 158, 11, 0.1);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(245, 158, 11, 0.18),
|
||||
0 1px 2px rgba(120, 53, 15, 0.04),
|
||||
0 14px 34px rgba(120, 53, 15, 0.06);
|
||||
--ai-document-card-head-bg: rgba(254, 243, 199, 0.5);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--approval-task .ai-document-card__head) {
|
||||
@@ -1646,15 +1636,17 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--approval-task .ai-document-card__reason) {
|
||||
color: #92400e;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--approval-task .ai-document-card__status) {
|
||||
min-height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(217, 119, 6, 0.1);
|
||||
color: #b45309;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__summary),
|
||||
@@ -1666,7 +1658,7 @@
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__summary) {
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid rgba(203, 213, 225, 0.76);
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__details) {
|
||||
@@ -1690,26 +1682,26 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__label) {
|
||||
color: #8a94a6;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 640;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__value) {
|
||||
min-width: 0;
|
||||
color: #334155;
|
||||
color: #1e293b;
|
||||
font-size: 14px;
|
||||
font-weight: 720;
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__amount) {
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1717,33 +1709,30 @@
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__number) {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 740;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-attachment-association-card) {
|
||||
background-image:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(248, 250, 252, 0.94)),
|
||||
url("../../ai-document-card-bg.png");
|
||||
background-image: none;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-attachment-association-card .ai-document-card__head) {
|
||||
background: linear-gradient(90deg, rgba(219, 234, 254, 0.92), rgba(240, 253, 250, 0.82));
|
||||
background: rgba(241, 245, 249, 0.5);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-ocr-recognition-card) {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(147, 197, 253, 0.42),
|
||||
0 1px 2px rgba(15, 23, 42, 0.03),
|
||||
0 12px 28px rgba(37, 99, 235, 0.045);
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 1px 2px 0 rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-ocr-recognition-card .ai-document-card__head) {
|
||||
background: linear-gradient(90deg, rgba(219, 234, 254, 0.92), rgba(239, 246, 255, 0.74));
|
||||
background: rgba(239, 246, 255, 0.5);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-ocr-recognition-card .ai-document-card__reason) {
|
||||
color: #1d4ed8;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-ocr-recognition-card .ai-document-card__status) {
|
||||
@@ -1781,16 +1770,16 @@
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #1d4ed8;
|
||||
color: #2563eb;
|
||||
font-size: 14px;
|
||||
font-weight: 820;
|
||||
font-weight: 600;
|
||||
box-shadow: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__action:hover) {
|
||||
background: transparent;
|
||||
color: #1e40af;
|
||||
color: #1d4ed8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -1798,10 +1787,10 @@
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-table-wrap) {
|
||||
overflow-x: auto;
|
||||
margin-top: 18px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.9);
|
||||
border-radius: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(table) {
|
||||
@@ -1813,7 +1802,7 @@
|
||||
.workbench-ai-answer-markdown :deep(th),
|
||||
.workbench-ai-answer-markdown :deep(td) {
|
||||
padding: 11px 14px;
|
||||
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -1922,6 +1911,149 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-mode {
|
||||
--ai-ink: #111827;
|
||||
--ai-text: #334155;
|
||||
--ai-muted: #64748b;
|
||||
--ai-line: #d8dee8;
|
||||
--ai-blue: #475569;
|
||||
--ai-blue-deep: #334155;
|
||||
--ai-purple: #64748b;
|
||||
--ai-cyan: #64748b;
|
||||
background:
|
||||
linear-gradient(180deg, #f8fafc 0%, #ffffff 56%, #f8fafc 100%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-mode::after {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(148, 163, 184, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(180deg, rgba(148, 163, 184, 0.06) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
opacity: 0.36;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-mode.has-conversation {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-mode.has-conversation::after {
|
||||
opacity: 0.22;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-orb {
|
||||
width: clamp(96px, 7vw, 112px);
|
||||
height: clamp(96px, 7vw, 112px);
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: #475569;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-composer,
|
||||
[data-theme-mode="enterprise"] .workbench-ai-composer--inline {
|
||||
border-color: #d8dee8;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 10px 26px rgba(15, 23, 42, 0.06),
|
||||
0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-icon-btn:hover,
|
||||
[data-theme-mode="enterprise"] .workbench-ai-icon-btn.active {
|
||||
color: #334155;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-send-btn {
|
||||
background: #334155;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-send-btn:hover:not(:disabled) {
|
||||
background: #1f2937;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-message {
|
||||
animation-duration: 220ms;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-user-bubble {
|
||||
border-radius: 8px 8px 3px;
|
||||
background: #334155;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-answer-card {
|
||||
border-color: #d8dee8;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-thinking-panel {
|
||||
border-color: #d8dee8;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-thinking-toggle {
|
||||
border-radius: 8px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-thinking-toggle:hover {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-thinking-toggle strong,
|
||||
[data-theme-mode="enterprise"] .workbench-ai-thinking-item strong {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-thinking-dot {
|
||||
background: #64748b;
|
||||
box-shadow: 0 0 0 4px rgba(100, 116, 139, 0.12);
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-suggested-actions button {
|
||||
border-color: #d8dee8;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
[data-theme-mode="enterprise"] .workbench-ai-suggested-actions button:hover:not(:disabled) {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
[data-theme-mode="intelligent"] .workbench-ai-mode {
|
||||
--ai-blue: #5f6f9f;
|
||||
--ai-blue-deep: #465275;
|
||||
--ai-purple: #6d6a9f;
|
||||
--ai-cyan: #477c9e;
|
||||
background:
|
||||
radial-gradient(circle at 16% 0%, rgba(95, 111, 159, 0.1), transparent 36%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.94)),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
[data-theme-mode="intelligent"] .workbench-ai-mode.has-conversation {
|
||||
background:
|
||||
radial-gradient(circle at 12% 0%, rgba(95, 111, 159, 0.1), transparent 34%),
|
||||
linear-gradient(180deg, #ffffff, #f8fafc);
|
||||
}
|
||||
|
||||
[data-theme-mode="intelligent"] .workbench-ai-composer,
|
||||
[data-theme-mode="intelligent"] .workbench-ai-composer--inline,
|
||||
[data-theme-mode="intelligent"] .workbench-ai-answer-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
||||
animation: none;
|
||||
|
||||
@@ -22,3 +22,9 @@ const {
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-ai-mode.css"></style>
|
||||
<style scoped>
|
||||
/* Force styling cache invalidation and hot update */
|
||||
.force-reload-css-cache-v1 {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -258,7 +258,8 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
resolveLatestInlineUserPrompt,
|
||||
scrollInlineConversationToBottom,
|
||||
sending,
|
||||
toast
|
||||
toast,
|
||||
onApplicationActionCompleted: startModelPlannedNextTask
|
||||
})
|
||||
|
||||
const expenseFlow = useWorkbenchAiExpenseFlow({
|
||||
@@ -710,6 +711,46 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
return pendingMessage
|
||||
}
|
||||
|
||||
function buildModelPlannedNextTaskAction(remainingTasks = []) {
|
||||
const tasks = Array.isArray(remainingTasks) ? remainingTasks : []
|
||||
const nextTask = tasks[0]
|
||||
if (!nextTask || typeof nextTask !== 'object') {
|
||||
return null
|
||||
}
|
||||
const taskType = String(nextTask.task_type || nextTask.taskType || '').trim()
|
||||
const assignedAgent = String(nextTask.assigned_agent || nextTask.assignedAgent || '').trim()
|
||||
const isApplication = taskType === 'expense_application' || assignedAgent === 'application_assistant'
|
||||
const isReimbursement = taskType === 'reimbursement' || assignedAgent === 'reimbursement_assistant'
|
||||
if (!isApplication && !isReimbursement) {
|
||||
return null
|
||||
}
|
||||
const ontologyFields = nextTask.ontology_fields || nextTask.ontologyFields || {}
|
||||
const flowId = isApplication ? 'travel_application' : 'travel_reimbursement'
|
||||
const taskLabel = isApplication ? '出差申请' : '费用报销'
|
||||
return {
|
||||
label: `继续处理${taskLabel}`,
|
||||
action_type: 'steward_continue_next_task',
|
||||
payload: {
|
||||
steward_confirm_flow: true,
|
||||
flow_id: flowId,
|
||||
steward_current_task: nextTask,
|
||||
expense_type: String(ontologyFields.expense_type || 'travel').trim() || 'travel',
|
||||
expense_type_label: String(ontologyFields.expense_type_label || '差旅费').trim() || '差旅费',
|
||||
ontology_fields: ontologyFields,
|
||||
original_message: String(nextTask.summary || nextTask.title || `继续处理${taskLabel}`).trim(),
|
||||
steward_remaining_tasks: tasks.slice(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startModelPlannedNextTask(remainingTasks = []) {
|
||||
const nextTaskAction = buildModelPlannedNextTaskAction(remainingTasks)
|
||||
if (!nextTaskAction) {
|
||||
return
|
||||
}
|
||||
actionRouter.handleInlineSuggestedAction(nextTaskAction)
|
||||
}
|
||||
|
||||
function startModelPlannedApplicationPreview(travelApplicationRequest, plannerPendingMessage = null) {
|
||||
void applicationFlow.startAiApplicationPreview(
|
||||
travelApplicationRequest.expenseType,
|
||||
@@ -723,7 +764,10 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
autoSubmit: travelApplicationRequest.autoSubmit,
|
||||
autoSaveDraft: travelApplicationRequest.autoSaveDraft,
|
||||
requestedSubmit: travelApplicationRequest.requestedSubmit,
|
||||
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation
|
||||
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation,
|
||||
stewardRemainingTasks: travelApplicationRequest.stewardRemainingTasks,
|
||||
onPreviewReadyForNextTask: startModelPlannedNextTask,
|
||||
onApplicationActionCompleted: startModelPlannedNextTask
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -741,7 +785,8 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
autoSubmit: travelApplicationRequest.autoSubmit,
|
||||
autoSaveDraft: travelApplicationRequest.autoSaveDraft,
|
||||
requestedSubmit: travelApplicationRequest.requestedSubmit,
|
||||
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation
|
||||
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation,
|
||||
stewardRemainingTasks: travelApplicationRequest.stewardRemainingTasks
|
||||
}
|
||||
}
|
||||
replaceInlineMessage(plannerPendingMessage.id, createInlineMessage('assistant', confirmText, {
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
} from '../../services/aiApplicationPreviewActions.js'
|
||||
import { executeStewardAction } from '../../services/steward.js'
|
||||
import { buildAiDocumentDetailRequest } from '../../utils/aiDocumentDetailReference.js'
|
||||
import { buildAiExpenseDraftPrefillValues } from '../../utils/aiExpenseDraftModel.js'
|
||||
import { requiresApplicationBeforeReimbursement } from '../../views/scripts/travelReimbursementApplicationLinkModel.js'
|
||||
import {
|
||||
mergeComposerPrefill,
|
||||
resolveSuggestedActionPrefill
|
||||
@@ -82,6 +84,9 @@ export function useWorkbenchAiActionRouter({
|
||||
}
|
||||
if (actionType === 'ai_application_confirm_intent') {
|
||||
aiExpenseDraft.value = null
|
||||
const stewardRemainingTasks = Array.isArray(actionPayload.stewardRemainingTasks)
|
||||
? actionPayload.stewardRemainingTasks
|
||||
: (Array.isArray(actionPayload.steward_remaining_tasks) ? actionPayload.steward_remaining_tasks : [])
|
||||
void applicationFlow.startAiApplicationPreview('travel', '差旅费', String(actionPayload.sourceText || '').trim(), {
|
||||
userMessage: String(actionPayload.sourceText || '').trim() || '确认发起出差申请',
|
||||
pushUserMessage: true,
|
||||
@@ -89,7 +94,20 @@ export function useWorkbenchAiActionRouter({
|
||||
autoSubmit: Boolean(actionPayload.autoSubmit),
|
||||
autoSaveDraft: Boolean(actionPayload.autoSaveDraft),
|
||||
requestedSubmit: Boolean(actionPayload.requestedSubmit),
|
||||
submitRequiresConfirmation: Boolean(actionPayload.submitRequiresConfirmation)
|
||||
submitRequiresConfirmation: Boolean(actionPayload.submitRequiresConfirmation),
|
||||
stewardRemainingTasks,
|
||||
onPreviewReadyForNextTask: (remainingTasks = []) => {
|
||||
const nextTaskAction = buildNextTaskSuggestedAction({ steward_remaining_tasks: remainingTasks })
|
||||
if (nextTaskAction) {
|
||||
handleInlineSuggestedAction(nextTaskAction)
|
||||
}
|
||||
},
|
||||
onApplicationActionCompleted: (remainingTasks = []) => {
|
||||
const nextTaskAction = buildNextTaskSuggestedAction({ steward_remaining_tasks: remainingTasks })
|
||||
if (nextTaskAction) {
|
||||
handleInlineSuggestedAction(nextTaskAction)
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -104,9 +122,21 @@ export function useWorkbenchAiActionRouter({
|
||||
return
|
||||
}
|
||||
if (actionPayload.steward_confirm_flow && actionPayload.flow_id === 'travel_reimbursement') {
|
||||
const expenseType = String(actionPayload.expense_type || 'travel').trim() || 'travel'
|
||||
const expenseTypeLabel = String(actionPayload.expense_type_label || '差旅费').trim() || '差旅费'
|
||||
expenseFlow.startAiExpenseDraft(expenseType, expenseTypeLabel, true)
|
||||
const currentTask = actionPayload.steward_current_task || {}
|
||||
const ontologyFields = currentTask.ontology_fields || currentTask.ontologyFields || actionPayload.ontology_fields || {}
|
||||
const expenseType = String(actionPayload.expense_type || ontologyFields.expense_type || 'travel').trim() || 'travel'
|
||||
const expenseTypeLabel = String(actionPayload.expense_type_label || ontologyFields.expense_type_label || '差旅费').trim() || '差旅费'
|
||||
// 从 task ontology 解析报销语义(金额/时间/事由/地点),预填到报销草稿,
|
||||
// 让 task2(如业务招待费 2000 元)的信息直接落到草稿,而不是丢失。
|
||||
const prefillValues = buildAiExpenseDraftPrefillValues(ontologyFields)
|
||||
const needsApplicationLink = requiresApplicationBeforeReimbursement(expenseType)
|
||||
const stewardRemainingTasks = Array.isArray(actionPayload.steward_remaining_tasks)
|
||||
? actionPayload.steward_remaining_tasks
|
||||
: []
|
||||
expenseFlow.startAiExpenseDraft(expenseType, expenseTypeLabel, needsApplicationLink, {
|
||||
prefillValues,
|
||||
stewardRemainingTasks
|
||||
})
|
||||
return
|
||||
}
|
||||
if (actionPayload.steward_confirm_flow && actionPayload.flow_id === 'travel_application') {
|
||||
@@ -176,7 +206,18 @@ export function useWorkbenchAiActionRouter({
|
||||
|
||||
if (actionType === 'ai_application_start_inline') {
|
||||
aiExpenseDraft.value = null
|
||||
void expenseFlow.startAiApplicationPreviewFromAction(action?.payload || {}, action?.label)
|
||||
// 多 task 推进:从 resolveAiExpenseApplicationLink "查不到申请单"分支过来的按钮,
|
||||
// payload 里带 prefill_values 和 steward_remaining_tasks,这里透传给申请预览,
|
||||
// 保证发起的申请单带着报销语义,且申请单做完后能继续 task2 报销流程。
|
||||
void expenseFlow.startAiApplicationPreviewFromAction({
|
||||
...(action?.payload || {}),
|
||||
expense_type: actionPayload.expense_type,
|
||||
expense_type_label: actionPayload.expense_type_label,
|
||||
carry_text: actionPayload.carry_text || actionPayload.original_message || action?.label,
|
||||
steward_remaining_tasks: Array.isArray(actionPayload.steward_remaining_tasks)
|
||||
? actionPayload.steward_remaining_tasks
|
||||
: []
|
||||
}, action?.label)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -398,6 +439,9 @@ export function useWorkbenchAiActionRouter({
|
||||
const flowId = isApplication ? 'travel_application' : 'travel_reimbursement'
|
||||
const taskLabel = isApplication ? '出差申请' : '费用报销'
|
||||
const ontologyFields = nextTask.ontology_fields || nextTask.ontologyFields || {}
|
||||
// 透传去掉当前 nextTask 之后的剩余 task 列表,保证 task2 完成后 task3 也能继续推进,
|
||||
// 避免 3+ task 场景在 task2 处断链。
|
||||
const furtherRemainingTasks = remainingTasks.slice(1)
|
||||
return {
|
||||
label: `继续处理${taskLabel}`,
|
||||
description: `接下来处理${taskLabel}: ${String(nextTask.summary || nextTask.title || '').slice(0, 40)}`,
|
||||
@@ -410,7 +454,8 @@ export function useWorkbenchAiActionRouter({
|
||||
expense_type: String(ontologyFields.expense_type || 'travel').trim() || 'travel',
|
||||
expense_type_label: String(ontologyFields.expense_type_label || '差旅费').trim() || '差旅费',
|
||||
ontology_fields: ontologyFields,
|
||||
original_message: String(nextTask.summary || nextTask.title || `继续处理${taskLabel}`).trim()
|
||||
original_message: String(nextTask.summary || nextTask.title || `继续处理${taskLabel}`).trim(),
|
||||
steward_remaining_tasks: furtherRemainingTasks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
resolveLatestInlineUserPrompt,
|
||||
scrollInlineConversationToBottom,
|
||||
sending,
|
||||
toast
|
||||
toast,
|
||||
onApplicationActionCompleted = null
|
||||
}) {
|
||||
function isApplicationPreviewEstimatePending(message = {}) {
|
||||
return Boolean(message?.applicationPreview && isApplicationPreviewEstimatePendingPreview(message.applicationPreview))
|
||||
@@ -345,6 +346,9 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
const flowId = isApplication ? 'travel_application' : 'travel_reimbursement'
|
||||
const taskLabel = isApplication ? '出差申请' : '费用报销'
|
||||
const ontologyFields = nextTask.ontology_fields || nextTask.ontologyFields || {}
|
||||
// 透传去掉当前 nextTask 之后的剩余 task 列表,保证 task2 完成后 task3 也能继续推进,
|
||||
// 避免 3+ task 场景在 task2 处断链。
|
||||
const furtherRemainingTasks = remainingTasks.slice(1)
|
||||
return {
|
||||
label: `继续处理${taskLabel}`,
|
||||
description: `接下来处理${taskLabel}:${String(nextTask.summary || nextTask.title || '').slice(0, 40)}`,
|
||||
@@ -357,7 +361,8 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
expense_type: String(ontologyFields.expense_type || 'travel').trim() || 'travel',
|
||||
expense_type_label: String(ontologyFields.expense_type_label || '差旅费').trim() || '差旅费',
|
||||
ontology_fields: ontologyFields,
|
||||
original_message: String(nextTask.summary || nextTask.title || `继续处理${taskLabel}`).trim()
|
||||
original_message: String(nextTask.summary || nextTask.title || `继续处理${taskLabel}`).trim(),
|
||||
steward_remaining_tasks: furtherRemainingTasks
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,6 +463,12 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
targetMessage.suggestedActions = []
|
||||
const detailActions = buildInlineApplicationDetailAction(draftPayload)
|
||||
const nextTaskAction = buildApplicationPreviewNextTaskAction(targetMessage)
|
||||
const shouldAutoContinueNextTask = Boolean(
|
||||
nextTaskAction &&
|
||||
typeof onApplicationActionCompleted === 'function' &&
|
||||
Array.isArray(targetMessage.stewardRemainingTasks) &&
|
||||
targetMessage.stewardRemainingTasks.length
|
||||
)
|
||||
replaceInlineMessage(
|
||||
pendingMessage.id,
|
||||
createInlineMessage('assistant', buildInlineApplicationPreviewActionResultText(actionType, payload), {
|
||||
@@ -466,11 +477,16 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
streamStatus: 'completed',
|
||||
thinkingEvents: completeInlineThinkingEvents(resolveInlineThinkingEvents(pendingMessage))
|
||||
},
|
||||
suggestedActions: nextTaskAction ? [...detailActions, nextTaskAction] : detailActions
|
||||
suggestedActions: shouldAutoContinueNextTask
|
||||
? detailActions
|
||||
: (nextTaskAction ? [...detailActions, nextTaskAction] : detailActions)
|
||||
})
|
||||
)
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
if (shouldAutoContinueNextTask) {
|
||||
onApplicationActionCompleted(targetMessage.stewardRemainingTasks, targetMessage)
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
replaceInlineMessage(
|
||||
@@ -599,6 +615,12 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
skipUserMessage: true,
|
||||
userText: options.userMessage || '保存草稿'
|
||||
})
|
||||
} else if (
|
||||
typeof options.onPreviewReadyForNextTask === 'function' &&
|
||||
Array.isArray(previewMessage.stewardRemainingTasks) &&
|
||||
previewMessage.stewardRemainingTasks.length
|
||||
) {
|
||||
options.onPreviewReadyForNextTask(previewMessage.stewardRemainingTasks, previewMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', error?.message || '申请核对表生成失败,请稍后重试。', {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
buildWorkbenchDocumentCommandFollowupGuidance,
|
||||
buildWorkbenchDraftDeletionGuidance,
|
||||
isWorkbenchDraftDeletionIntent,
|
||||
resolveLatestWorkbenchDocumentCommandContext,
|
||||
resolveLatestWorkbenchDraftPayload
|
||||
} from './workbenchAiCommandIntentModel.js'
|
||||
import { resolveWorkbenchIntentActionRoute } from './workbenchIntentActionPolicy.js'
|
||||
@@ -58,6 +60,9 @@ export function useWorkbenchAiCommandIntents({
|
||||
if (!handlesWorkbenchCommand) {
|
||||
return false
|
||||
}
|
||||
const documentCommandContext = route.nextStep === 'query_candidates'
|
||||
? resolveLatestWorkbenchDocumentCommandContext(conversationMessages.value, frame)
|
||||
: null
|
||||
prepareInlineCommandConversation(cleanPrompt, entry)
|
||||
const draftPayload = frame?.targetMode === 'current_context' || legacyDraftDelete
|
||||
? resolveLatestWorkbenchDraftPayload(conversationMessages.value)
|
||||
@@ -72,6 +77,16 @@ export function useWorkbenchAiCommandIntents({
|
||||
return true
|
||||
}
|
||||
|
||||
if (route.nextStep === 'query_candidates' && documentCommandContext) {
|
||||
const guidance = buildWorkbenchDocumentCommandFollowupGuidance(documentCommandContext, frame)
|
||||
conversationMessages.value.push(createInlineMessage('assistant', guidance.content, {
|
||||
suggestedActions: guidance.suggestedActions
|
||||
}))
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
return true
|
||||
}
|
||||
|
||||
const queryPrompt = route.queryPrompt || frame?.normalizedQuery || '我的草稿单据'
|
||||
const pendingText = frame?.safetyLevel === 'confirm_required'
|
||||
? '正在先筛选候选单据,不会直接执行删除或审核动作...'
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../../services/linkedReimbursementDraftJobs.js'
|
||||
import {
|
||||
applyAiExpenseAnswer,
|
||||
buildAiExpenseDraftPrefillValues,
|
||||
buildAiExpenseStepPrompt,
|
||||
buildAiExpenseSummary,
|
||||
createAiExpenseDraft,
|
||||
@@ -113,6 +114,7 @@ export function useWorkbenchAiExpenseFlow({
|
||||
suggestedActions: Array.isArray(options.suggestedActions) ? options.suggestedActions : [],
|
||||
draftPayload: options.draftPayload || null,
|
||||
linkedReimbursementDraftJob: options.linkedReimbursementDraftJob || null,
|
||||
stewardRemainingTasks: Array.isArray(options.stewardRemainingTasks) ? options.stewardRemainingTasks : [],
|
||||
text: options.text || content
|
||||
})
|
||||
replaceInlineMessage(messageId, nextMessage)
|
||||
@@ -323,7 +325,40 @@ export function useWorkbenchAiExpenseFlow({
|
||||
scrollInlineConversationToBottom()
|
||||
}
|
||||
|
||||
function startAiExpenseDraft(expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement) {
|
||||
// 多 task 推进时,把当前报销流程后续要处理的剩余 task 挂在 draft 上,
|
||||
// 这样关联申请单、发起申请单、生成报销草稿等子流程都能把 remaining tasks 透传下去,
|
||||
// 保证 task2 完成后能继续 task3。draft 被清空时上下文也随之消失。
|
||||
function attachStewardRemainingTasks(draft, stewardRemainingTasks) {
|
||||
if (!draft) {
|
||||
return draft
|
||||
}
|
||||
const tasks = Array.isArray(stewardRemainingTasks) ? stewardRemainingTasks : []
|
||||
draft.stewardRemainingTasks = tasks
|
||||
return draft
|
||||
}
|
||||
|
||||
function resolveStewardRemainingTasks(draft) {
|
||||
const draftTasks = Array.isArray(draft?.stewardRemainingTasks) ? draft.stewardRemainingTasks : []
|
||||
return draftTasks.length ? draftTasks : null
|
||||
}
|
||||
|
||||
// 把 expenseType 解析成"发起 XX 申请"按钮里的 XX,避免对招待费也显示"出差申请"。
|
||||
function resolveRequiredApplicationLabel(expenseType, fallbackLabel = '') {
|
||||
const normalized = String(expenseType || '').trim().toLowerCase()
|
||||
if (normalized === 'meal' || normalized === 'entertainment') {
|
||||
return '业务招待'
|
||||
}
|
||||
if (normalized === 'travel') {
|
||||
return '出差'
|
||||
}
|
||||
const label = String(fallbackLabel || '').trim()
|
||||
if (label) {
|
||||
return label.replace(/费$/, '')
|
||||
}
|
||||
return '费用'
|
||||
}
|
||||
|
||||
function startAiExpenseDraft(expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement, options = {}) {
|
||||
if (!conversationStarted.value) {
|
||||
activateInlineConversation({ title: String(expenseTypeLabel || '报销').trim().slice(0, 18) || '报销' })
|
||||
}
|
||||
@@ -333,12 +368,25 @@ export function useWorkbenchAiExpenseFlow({
|
||||
clearAiModeFiles()
|
||||
pushInlineUserMessage(`选择${expenseTypeLabel || expenseType || '报销'}`)
|
||||
|
||||
const prefillValues = options.prefillValues && typeof options.prefillValues === 'object'
|
||||
? options.prefillValues
|
||||
: null
|
||||
const stewardRemainingTasks = Array.isArray(options.stewardRemainingTasks)
|
||||
? options.stewardRemainingTasks
|
||||
: []
|
||||
|
||||
if (requiresApplicationBeforeReimbursement) {
|
||||
void resolveAiExpenseApplicationLink(expenseType, expenseTypeLabel)
|
||||
void resolveAiExpenseApplicationLink(expenseType, expenseTypeLabel, {
|
||||
prefillValues,
|
||||
stewardRemainingTasks
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const draft = createAiExpenseDraft(expenseType, expenseTypeLabel)
|
||||
const draft = attachStewardRemainingTasks(
|
||||
createAiExpenseDraft(expenseType, expenseTypeLabel, prefillValues),
|
||||
stewardRemainingTasks
|
||||
)
|
||||
aiExpenseDraft.value = draft
|
||||
conversationMessages.value.push(createInlineMessage('assistant', buildAiExpenseStepPrompt(draft)))
|
||||
persistCurrentConversation()
|
||||
@@ -351,7 +399,11 @@ export function useWorkbenchAiExpenseFlow({
|
||||
assistantDraft.value = ''
|
||||
clearAiModeFiles()
|
||||
|
||||
const next = applyAiExpenseAnswer(aiExpenseDraft.value, answer, fileNames)
|
||||
const currentDraft = aiExpenseDraft.value
|
||||
const next = applyAiExpenseAnswer(currentDraft, answer, fileNames)
|
||||
// applyAiExpenseAnswer 不会保留 draft 上的运行时上下文,这里手动透传 remaining tasks,
|
||||
// 保证报销草稿收集完所有字段后,仍能拿到后续 task 列表用于推进 task3。
|
||||
attachStewardRemainingTasks(next, resolveStewardRemainingTasks(currentDraft))
|
||||
aiExpenseDraft.value = next
|
||||
|
||||
if (isAiExpenseDraftComplete(next)) {
|
||||
@@ -364,7 +416,14 @@ export function useWorkbenchAiExpenseFlow({
|
||||
scrollInlineConversationToBottom()
|
||||
}
|
||||
|
||||
async function resolveAiExpenseApplicationLink(expenseType, expenseTypeLabel) {
|
||||
async function resolveAiExpenseApplicationLink(expenseType, expenseTypeLabel, options = {}) {
|
||||
const prefillValues = options.prefillValues && typeof options.prefillValues === 'object'
|
||||
? options.prefillValues
|
||||
: null
|
||||
const stewardRemainingTasks = Array.isArray(options.stewardRemainingTasks)
|
||||
? options.stewardRemainingTasks
|
||||
: []
|
||||
|
||||
let claims = null
|
||||
try {
|
||||
claims = await fetchExpenseClaimsForAi(REIMBURSEMENT_LIST_PREVIEW_PARAMS)
|
||||
@@ -377,18 +436,30 @@ export function useWorkbenchAiExpenseFlow({
|
||||
}
|
||||
|
||||
const candidates = filterRequiredApplicationCandidates(claims, expenseType, currentUser.value || {})
|
||||
aiExpenseDraft.value = createAiExpenseDraft(expenseType, expenseTypeLabel)
|
||||
// 即使后续可能被清空,也先把报销语义 + remaining tasks 上下文挂到 draft 上,
|
||||
// 这样查不到申请单时仍能透传给"发起申请单"按钮,保证 task2 不丢失语义。
|
||||
const draft = attachStewardRemainingTasks(
|
||||
createAiExpenseDraft(expenseType, expenseTypeLabel, prefillValues),
|
||||
stewardRemainingTasks
|
||||
)
|
||||
aiExpenseDraft.value = draft
|
||||
|
||||
if (!candidates.length) {
|
||||
// 查不到可关联申请单:不要让 task2 语义丢失。生成"发起申请单"按钮时,
|
||||
// 按费用类型动态生成 label,带上 ontology 上下文 + remaining tasks,
|
||||
// 让用户发起申请单后能回到 task2 报销流程(见 ai_application_start_inline 分支)。
|
||||
const applicationLabel = resolveRequiredApplicationLabel(expenseType, expenseTypeLabel)
|
||||
conversationMessages.value.push(createInlineMessage('assistant', buildRequiredApplicationMissingText(expenseType), {
|
||||
suggestedActions: [{
|
||||
label: '确认发起出差申请',
|
||||
label: `确认发起${applicationLabel}申请`,
|
||||
description: '生成完整申请表,并预填已识别的时间、地点和事由',
|
||||
icon: 'mdi mdi-file-plus-outline',
|
||||
action_type: 'ai_application_start_inline',
|
||||
payload: {
|
||||
expense_type: expenseType,
|
||||
expense_type_label: expenseTypeLabel
|
||||
expense_type_label: expenseTypeLabel,
|
||||
prefill_values: prefillValues || {},
|
||||
steward_remaining_tasks: stewardRemainingTasks
|
||||
}
|
||||
}]
|
||||
}))
|
||||
@@ -459,7 +530,8 @@ export function useWorkbenchAiExpenseFlow({
|
||||
jobId,
|
||||
pendingMessageId,
|
||||
claimNo = '',
|
||||
initialJob = null
|
||||
initialJob = null,
|
||||
stewardRemainingTasks = []
|
||||
}) {
|
||||
const normalizedJobId = String(jobId || '').trim()
|
||||
if (!normalizedJobId || activeLinkedDraftJobPolls.has(normalizedJobId)) {
|
||||
@@ -479,13 +551,17 @@ export function useWorkbenchAiExpenseFlow({
|
||||
const content = draftClaimNo
|
||||
? `报销草稿 ${draftClaimNo} 已生成,并已关联申请单 ${claimNo || '所选申请单'}。后续可以在草稿详情中上传票据或补充信息。`
|
||||
: `报销草稿已生成,并已关联申请单 ${claimNo || '所选申请单'}。后续可以在草稿详情中上传票据或补充信息。`
|
||||
// 多 task 推进:报销草稿生成成功后,若还有剩余 task,补一个"继续处理"按钮。
|
||||
const nextTaskAction = buildExpenseDraftNextTaskAction(stewardRemainingTasks)
|
||||
replaceInlineAssistantMessage(pendingMessageId, content, {
|
||||
draftPayload,
|
||||
linkedReimbursementDraftJob: {
|
||||
...currentJob,
|
||||
applicationClaimNo: claimNo
|
||||
},
|
||||
suggestedActions: buildLinkedDraftAction(draftPayload)
|
||||
suggestedActions: nextTaskAction
|
||||
? [...buildLinkedDraftAction(draftPayload), nextTaskAction]
|
||||
: buildLinkedDraftAction(draftPayload)
|
||||
})
|
||||
aiExpenseDraft.value = null
|
||||
persistCurrentConversation()
|
||||
@@ -524,7 +600,9 @@ export function useWorkbenchAiExpenseFlow({
|
||||
jobId: job.jobId,
|
||||
pendingMessageId: message.id,
|
||||
claimNo: job.applicationClaimNo,
|
||||
initialJob: job
|
||||
initialJob: job,
|
||||
// 刷新恢复时从消息上读回 remaining tasks,保证报销完成后仍能补出"继续处理"按钮。
|
||||
stewardRemainingTasks: Array.isArray(message.stewardRemainingTasks) ? message.stewardRemainingTasks : []
|
||||
}).catch((error) => {
|
||||
replaceInlineAssistantMessage(message.id, buildLinkedDraftFailedText(error), {
|
||||
linkedReimbursementDraftJob: {
|
||||
@@ -556,6 +634,38 @@ export function useWorkbenchAiExpenseFlow({
|
||||
}]
|
||||
}
|
||||
|
||||
// 报销草稿生成成功后,若有剩余 task,生成"继续处理下一个任务"按钮。
|
||||
// 与 useWorkbenchAiApplicationPreviewFlow.buildApplicationPreviewNextTaskAction 同构,
|
||||
// 但数据源是 draft 上透传过来的 stewardRemainingTasks,保证报销完成后 task3 也能推进。
|
||||
function buildExpenseDraftNextTaskAction(remainingTasks = []) {
|
||||
const tasks = Array.isArray(remainingTasks) ? remainingTasks : []
|
||||
const nextTask = tasks[0]
|
||||
if (!nextTask || !nextTask.task_type) {
|
||||
return null
|
||||
}
|
||||
const taskType = String(nextTask.task_type || '').trim()
|
||||
const isApplication = taskType === 'expense_application'
|
||||
const flowId = isApplication ? 'travel_application' : 'travel_reimbursement'
|
||||
const taskLabel = isApplication ? '出差申请' : '费用报销'
|
||||
const ontologyFields = nextTask.ontology_fields || nextTask.ontologyFields || {}
|
||||
return {
|
||||
label: `继续处理${taskLabel}`,
|
||||
description: `接下来处理${taskLabel}:${String(nextTask.summary || nextTask.title || '').slice(0, 40)}`,
|
||||
icon: isApplication ? 'mdi mdi-file-plus-outline' : 'mdi mdi-receipt-text-plus-outline',
|
||||
action_type: 'steward_continue_next_task',
|
||||
payload: {
|
||||
steward_confirm_flow: true,
|
||||
flow_id: flowId,
|
||||
steward_current_task: nextTask,
|
||||
expense_type: String(ontologyFields.expense_type || 'travel').trim() || 'travel',
|
||||
expense_type_label: String(ontologyFields.expense_type_label || '差旅费').trim() || '差旅费',
|
||||
ontology_fields: ontologyFields,
|
||||
original_message: String(nextTask.summary || nextTask.title || `继续处理${taskLabel}`).trim(),
|
||||
steward_remaining_tasks: tasks.slice(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function linkAiExpenseApplication(application = {}) {
|
||||
const draft = aiExpenseDraft.value || (() => {
|
||||
const resolved = resolveRequiredApplicationReimbursementType(application)
|
||||
@@ -577,9 +687,14 @@ export function useWorkbenchAiExpenseFlow({
|
||||
stepKey: 'attachments'
|
||||
}
|
||||
aiExpenseDraft.value = linked
|
||||
// 关联申请单时,保留 draft 上的 remaining tasks 上下文,透传给后续轮询,
|
||||
// 这样报销草稿生成成功后能补出"继续处理 task3"按钮。
|
||||
const stewardRemainingTasks = resolveStewardRemainingTasks(linked) || []
|
||||
const pendingMessage = createInlineMessage('assistant', `已关联申请单${claimNo ? ` ${claimNo}` : ''},正在生成报销草稿...`, {
|
||||
pending: true,
|
||||
suggestedActions: []
|
||||
suggestedActions: [],
|
||||
// 把 remaining tasks 挂到 pending 消息上,刷新后 resume 轮询能读回并透传给 poll 成功分支。
|
||||
stewardRemainingTasks
|
||||
})
|
||||
conversationMessages.value.push(pendingMessage)
|
||||
const pendingMessageId = pendingMessage.id
|
||||
@@ -607,7 +722,8 @@ export function useWorkbenchAiExpenseFlow({
|
||||
jobId: normalizedJob.jobId,
|
||||
pendingMessageId,
|
||||
claimNo,
|
||||
initialJob: normalizedJob
|
||||
initialJob: normalizedJob,
|
||||
stewardRemainingTasks
|
||||
})
|
||||
} catch (error) {
|
||||
replaceInlineAssistantMessage(
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
parseAiDocumentDetailHref
|
||||
} from '../../utils/aiDocumentDetailReference.js'
|
||||
|
||||
const DRAFT_DELETION_ACTION_PATTERN = /删除|删掉|删了|移除|作废|撤销/
|
||||
const DRAFT_DELETION_TARGET_PATTERN = (
|
||||
/草稿|这个单据|这张单据|当前单据|当前申请|当前报销|刚才保存的草稿|刚才的草稿|上面的单据|最近的单据|申请单|报销单/
|
||||
@@ -22,11 +26,26 @@ const SUBMITTED_OR_FINAL_STATUS = new Set([
|
||||
'已驳回',
|
||||
'已退回'
|
||||
])
|
||||
const DOCUMENT_DETAIL_LINK_RE = /<a\b[^>]*href="([^"]*#ai-open-document-detail:[^"]+)"[^>]*>(.*?)<\/a>/g
|
||||
const DOCUMENT_COMMAND_ACTION_LABELS = {
|
||||
delete: '删除',
|
||||
approve: '审核通过',
|
||||
reject: '驳回/退回'
|
||||
}
|
||||
const DOCUMENT_COMMAND_DETAIL_LABELS = {
|
||||
delete: '进入详情确认删除',
|
||||
approve: '进入详情确认审核',
|
||||
reject: '进入详情确认驳回'
|
||||
}
|
||||
|
||||
function normalizeCompactText(value = '') {
|
||||
return String(value || '').replace(/\s+/g, '').trim()
|
||||
}
|
||||
|
||||
function normalizeText(value = '') {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function normalizeDraftDocumentType(payload = {}, claimNo = '') {
|
||||
const rawType = String(payload.document_type || payload.documentType || payload.draft_type || payload.draftType || '').trim()
|
||||
if (/application|expense_application|申请/.test(rawType)) {
|
||||
@@ -77,6 +96,60 @@ function extractDraftPayloadFromSuggestedActions(message = {}) {
|
||||
return null
|
||||
}
|
||||
|
||||
function stripHtml(value = '') {
|
||||
return normalizeText(String(value || '').replace(/<[^>]*>/g, ''))
|
||||
}
|
||||
|
||||
function normalizeDocumentCommandCandidate(detailReference = null, rawLabel = '') {
|
||||
if (!detailReference || typeof detailReference !== 'object') {
|
||||
return null
|
||||
}
|
||||
const claimId = String(detailReference.claimId || detailReference.claim_id || '').trim()
|
||||
const claimNo = String(detailReference.claimNo || detailReference.claim_no || detailReference.reference || '').trim()
|
||||
if (!claimId && !claimNo) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
claimId,
|
||||
claimNo,
|
||||
documentType: normalizeDraftDocumentType(detailReference, claimNo),
|
||||
actionLabel: stripHtml(rawLabel) || '查看详情'
|
||||
}
|
||||
}
|
||||
|
||||
function extractDocumentCommandCandidatesFromContent(content = '') {
|
||||
const text = String(content || '')
|
||||
const candidates = []
|
||||
const seen = new Set()
|
||||
for (const match of text.matchAll(DOCUMENT_DETAIL_LINK_RE)) {
|
||||
const candidate = normalizeDocumentCommandCandidate(
|
||||
parseAiDocumentDetailHref(match[1]),
|
||||
match[2]
|
||||
)
|
||||
if (!candidate) {
|
||||
continue
|
||||
}
|
||||
const key = `${candidate.claimId || ''}:${candidate.claimNo || ''}`
|
||||
if (seen.has(key)) {
|
||||
continue
|
||||
}
|
||||
seen.add(key)
|
||||
candidates.push(candidate)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
function canReuseDocumentCommandContext(content = '', commandFrame = {}) {
|
||||
const action = String(commandFrame?.action || '').trim()
|
||||
if (!['approve', 'reject'].includes(action)) {
|
||||
return false
|
||||
}
|
||||
if (String(commandFrame?.safetyLevel || '').trim() !== 'confirm_required') {
|
||||
return false
|
||||
}
|
||||
return /ai-document-card--approval-task|待我审核|待审|待审批|待审核|确认审核|进入详情确认审核/.test(String(content || ''))
|
||||
}
|
||||
|
||||
export function isWorkbenchDraftDeletionIntent(prompt = '') {
|
||||
const compact = normalizeCompactText(prompt)
|
||||
if (!compact || !DRAFT_DELETION_ACTION_PATTERN.test(compact)) {
|
||||
@@ -104,6 +177,29 @@ export function resolveLatestWorkbenchDraftPayload(messages = []) {
|
||||
return null
|
||||
}
|
||||
|
||||
export function resolveLatestWorkbenchDocumentCommandContext(messages = [], commandFrame = {}) {
|
||||
const safeMessages = Array.isArray(messages) ? messages : []
|
||||
for (const message of [...safeMessages].reverse()) {
|
||||
if (String(message?.role || '').trim() !== 'assistant') {
|
||||
continue
|
||||
}
|
||||
const content = String(message?.content || message?.text || '')
|
||||
if (!canReuseDocumentCommandContext(content, commandFrame)) {
|
||||
continue
|
||||
}
|
||||
const candidates = extractDocumentCommandCandidatesFromContent(content)
|
||||
if (!candidates.length) {
|
||||
continue
|
||||
}
|
||||
return {
|
||||
sourceMessageId: String(message?.id || '').trim(),
|
||||
action: String(commandFrame?.action || '').trim(),
|
||||
candidates
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function buildWorkbenchDraftDeletionGuidance(draftPayload = {}) {
|
||||
const claimNo = String(draftPayload.claimNo || draftPayload.claim_no || '').trim()
|
||||
const claimId = String(draftPayload.claimId || draftPayload.claim_id || '').trim()
|
||||
@@ -128,3 +224,42 @@ export function buildWorkbenchDraftDeletionGuidance(draftPayload = {}) {
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export function buildWorkbenchDocumentCommandFollowupGuidance(context = {}, commandFrame = {}) {
|
||||
const action = String(commandFrame?.action || context?.action || '').trim()
|
||||
const actionLabel = DOCUMENT_COMMAND_ACTION_LABELS[action] || '处理'
|
||||
const detailLabel = DOCUMENT_COMMAND_DETAIL_LABELS[action] || '进入详情确认'
|
||||
const candidates = Array.isArray(context?.candidates) ? context.candidates : []
|
||||
const visibleCandidates = candidates.slice(0, 8)
|
||||
const candidateLines = visibleCandidates.map((candidate, index) => {
|
||||
const reference = candidate.claimNo || candidate.claimId || `候选 ${index + 1}`
|
||||
return `${index + 1}. ${reference}`
|
||||
})
|
||||
const overflowText = candidates.length > visibleCandidates.length
|
||||
? `\n\n还有 ${candidates.length - visibleCandidates.length} 张候选未展示,请先补充更具体条件。`
|
||||
: ''
|
||||
return {
|
||||
content: [
|
||||
'### 已接上刚才查询到的待审单据',
|
||||
`您想继续执行 **${actionLabel}**。这属于高风险审批动作,我不会直接替您通过或驳回。`,
|
||||
'请先从刚才的候选单据中选择一张,进入详情页核对风险、金额和审批节点后再确认。',
|
||||
candidateLines.length ? candidateLines.join('\n') : '',
|
||||
overflowText
|
||||
].filter(Boolean).join('\n\n'),
|
||||
suggestedActions: visibleCandidates.map((candidate) => {
|
||||
const reference = candidate.claimNo || candidate.claimId || '单据'
|
||||
return {
|
||||
label: `${detailLabel} ${reference}`,
|
||||
description: '打开详情页核对后,再完成审批确认。',
|
||||
icon: 'mdi mdi-open-in-new',
|
||||
action_type: 'open_application_detail',
|
||||
payload: {
|
||||
claim_id: candidate.claimId,
|
||||
claim_no: candidate.claimNo,
|
||||
document_type: candidate.documentType,
|
||||
command_action: action
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,13 +143,37 @@ function normalizeServerApplicationSteps(rawSteps = []) {
|
||||
return [...new Set(mappedSteps)]
|
||||
}
|
||||
|
||||
function resolveModelTasks(rawPlan = {}) {
|
||||
return Array.isArray(rawPlan?.tasks) ? rawPlan.tasks : []
|
||||
}
|
||||
|
||||
function isModelTravelApplicationTask(task = {}) {
|
||||
if (!task || typeof task !== 'object') {
|
||||
return false
|
||||
}
|
||||
const taskType = String(task?.task_type || task?.taskType || '').trim()
|
||||
const assignedAgent = String(task?.assigned_agent || task?.assignedAgent || '').trim()
|
||||
return taskType === 'expense_application' || assignedAgent === 'application_assistant'
|
||||
}
|
||||
|
||||
function findModelTravelApplicationTask(rawPlan = {}) {
|
||||
const tasks = Array.isArray(rawPlan?.tasks) ? rawPlan.tasks : []
|
||||
return tasks.find((task) => {
|
||||
return resolveModelTasks(rawPlan).find(isModelTravelApplicationTask) || null
|
||||
}
|
||||
|
||||
function resolveModelRemainingTasks(rawPlan = {}, selectedTask = null) {
|
||||
const tasks = resolveModelTasks(rawPlan)
|
||||
const selectedIndex = tasks.findIndex((task) => task === selectedTask)
|
||||
if (selectedIndex < 0) {
|
||||
return []
|
||||
}
|
||||
return tasks.slice(selectedIndex + 1).filter((task) => {
|
||||
if (!task || typeof task !== 'object') {
|
||||
return false
|
||||
}
|
||||
const taskType = String(task?.task_type || task?.taskType || '').trim()
|
||||
const assignedAgent = String(task?.assigned_agent || task?.assignedAgent || '').trim()
|
||||
return taskType === 'expense_application' || assignedAgent === 'application_assistant'
|
||||
}) || null
|
||||
return Boolean(taskType || assignedAgent)
|
||||
})
|
||||
}
|
||||
|
||||
function resolveCandidateFlows(rawPlan = {}) {
|
||||
@@ -211,7 +235,7 @@ export function normalizeWorkbenchAiIntentPlan(rawPlan = {}, options = {}) {
|
||||
task.requested_action ||
|
||||
task.requestedAction ||
|
||||
rawPlan.requested_action ||
|
||||
rawPlan.requestedAction ||
|
||||
rawPlan.requestedAction ||
|
||||
''
|
||||
).trim() || normalizePromptAction(prompt)
|
||||
const serverSteps = normalizeServerApplicationSteps(task.action_steps || task.actionSteps)
|
||||
@@ -226,7 +250,8 @@ export function normalizeWorkbenchAiIntentPlan(rawPlan = {}, options = {}) {
|
||||
missingFields: Array.isArray(task.missing_fields || task.missingFields)
|
||||
? task.missing_fields || task.missingFields
|
||||
: [],
|
||||
steps: serverSteps.length ? serverSteps : buildApplicationSteps(requestedAction)
|
||||
steps: serverSteps.length ? serverSteps : buildApplicationSteps(requestedAction),
|
||||
stewardRemainingTasks: resolveModelRemainingTasks(rawPlan, task)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +300,7 @@ export function resolveExecutableTravelApplicationPlan(plan = null) {
|
||||
return null
|
||||
}
|
||||
const requestedSubmit = plan.steps.includes(WORKBENCH_AI_STEP_SUBMIT_APPLICATION)
|
||||
return {
|
||||
const request = {
|
||||
expenseType: 'travel',
|
||||
expenseTypeLabel: '差旅费',
|
||||
sourceText: String(plan.sourceText || '').trim(),
|
||||
@@ -285,6 +310,11 @@ export function resolveExecutableTravelApplicationPlan(plan = null) {
|
||||
requestedSubmit,
|
||||
submitRequiresConfirmation: requestedSubmit
|
||||
}
|
||||
const stewardRemainingTasks = Array.isArray(plan.stewardRemainingTasks) ? plan.stewardRemainingTasks : []
|
||||
if (stewardRemainingTasks.length) {
|
||||
request.stewardRemainingTasks = stewardRemainingTasks
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
export function isLowConfidenceTravelApplicationPlan(plan = null) {
|
||||
|
||||
@@ -156,6 +156,9 @@ export function createWorkbenchAiMessageRuntime() {
|
||||
attachmentAssociationJob: normalizeInlineAttachmentAssociationJob(options.attachmentAssociationJob || null),
|
||||
linkedReimbursementDraftJob: normalizeInlineLinkedReimbursementDraftJob(options.linkedReimbursementDraftJob || null),
|
||||
attachmentOcrDetails: normalizeInlineAttachmentOcrDetails(options.attachmentOcrDetails || null),
|
||||
// 多 task 推进上下文:申请预览/报销草稿消息上挂载剩余 task 列表,
|
||||
// 刷新或消息重建后仍能继续推进,避免 task 链断裂。
|
||||
stewardRemainingTasks: Array.isArray(options.stewardRemainingTasks) ? options.stewardRemainingTasks : [],
|
||||
text: options.text || normalizedContent,
|
||||
createdAt: options.createdAt || Date.now()
|
||||
}
|
||||
@@ -175,6 +178,7 @@ export function createWorkbenchAiMessageRuntime() {
|
||||
attachmentAssociationJob: message.attachmentAssociationJob || null,
|
||||
linkedReimbursementDraftJob: message.linkedReimbursementDraftJob || null,
|
||||
attachmentOcrDetails: message.attachmentOcrDetails || null,
|
||||
stewardRemainingTasks: Array.isArray(message.stewardRemainingTasks) ? message.stewardRemainingTasks : [],
|
||||
text: message.text || message.content || ''
|
||||
})
|
||||
}
|
||||
@@ -194,7 +198,8 @@ export function createWorkbenchAiMessageRuntime() {
|
||||
draftPayload: message.draftPayload || null,
|
||||
attachmentAssociationJob: normalizeInlineAttachmentAssociationJob(message.attachmentAssociationJob || null),
|
||||
linkedReimbursementDraftJob: normalizeInlineLinkedReimbursementDraftJob(message.linkedReimbursementDraftJob || null),
|
||||
attachmentOcrDetails: message.attachmentOcrDetails || null
|
||||
attachmentOcrDetails: message.attachmentOcrDetails || null,
|
||||
stewardRemainingTasks: Array.isArray(message.stewardRemainingTasks) ? message.stewardRemainingTasks : []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ export function buildAiApplicationPreviewActionPayload({
|
||||
: []
|
||||
const draftClaimId = normalizeText(draftPayload?.claim_id || draftPayload?.claimId)
|
||||
const isSubmit = actionType === AI_APPLICATION_ACTION_SUBMIT
|
||||
const applicationEditableFields = Array.isArray(normalizedPreview.editableFields)
|
||||
? normalizedPreview.editableFields.map((field) => normalizeText(field)).filter(Boolean)
|
||||
: []
|
||||
|
||||
return {
|
||||
source: 'user_message',
|
||||
@@ -107,6 +110,9 @@ export function buildAiApplicationPreviewActionPayload({
|
||||
application_stage: 'expense_application',
|
||||
user_input_text: message,
|
||||
application_preview: normalizedPreview,
|
||||
...(applicationEditableFields.length
|
||||
? { application_editable_fields: applicationEditableFields }
|
||||
: {}),
|
||||
...(isSubmit
|
||||
? {}
|
||||
: {
|
||||
|
||||
@@ -313,7 +313,9 @@ export function buildAiApplicationPrecheckMessage(preview = {}, precheck = {}) {
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'> **请先核对**:请先检查本次申请时间是否填写正确。若日期填错,请直接回复正确的出发时间和返回时间,我会重新查询;若日期无误,请先处理或关联已有申请单,避免重复申请。',
|
||||
'**后续行动建议**:',
|
||||
'- 请检查本次申请时间是否填写正确。若日期填错,请直接回复正确的出发时间和返回时间,我会重新查询;',
|
||||
'- 若日期无误,请先处理或关联已有申请单,避免重复申请。',
|
||||
'',
|
||||
'我会先暂停本次申请表生成,不会开放保存草稿或提交入口。'
|
||||
)
|
||||
@@ -323,18 +325,17 @@ export function buildAiApplicationPrecheckMessage(preview = {}, precheck = {}) {
|
||||
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 budgetPrefix = precheck?.budget?.requiresBudgetReview ? '预算管理者审核提示' : '预算与审批影响'
|
||||
const overlapPrefix = precheck?.overlap?.status === 'warning' ? '时间重叠提醒' : '单据重叠核查'
|
||||
const lines = [
|
||||
'### 出差申请表草稿已生成',
|
||||
'',
|
||||
'**我已完成发起前的单据与预算预审**,并为您生成一张完整的出差申请表。',
|
||||
'',
|
||||
`> ${overlapPrefix}:${precheck?.overlap?.summary || '已完成已有单据核查。'}`,
|
||||
'',
|
||||
`> ${budgetPrefix}:${precheck?.budget?.summary || '已完成预算影响评估。'}`,
|
||||
'',
|
||||
`> **仍需补充**:${missingText}`,
|
||||
'**发起前预审结果**:',
|
||||
`- **${overlapPrefix}**:${precheck?.overlap?.summary || '已完成已有单据核查。'}`,
|
||||
`- **${budgetPrefix}**:${precheck?.budget?.summary || '已完成预算影响评估。'}`,
|
||||
`- **仍需补充**:${missingText}`,
|
||||
'',
|
||||
'请直接点击表格中的字段补充或修改;费用测算会根据地点、天数和出行方式自动更新。'
|
||||
]
|
||||
@@ -363,14 +364,16 @@ export function buildAiApplicationSubmitConflictMessage(preview = {}, precheck =
|
||||
'',
|
||||
`> **相同日期提醒**:${precheck?.overlap?.summary || '发现相同日期已有申请单,请先核对后再提交。'}`,
|
||||
'',
|
||||
`> **本次申请时间**:${currentRangeText}`,
|
||||
`**本次申请时间**:${currentRangeText}`,
|
||||
]
|
||||
if (matchTable) {
|
||||
lines.push('', matchTable)
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'> **请先核对**:请先核对申请时间是否填写正确。若日期填错,请直接回复正确的出发时间和返回时间,我会重新查询;若日期无误,请先查看或处理已有申请单,避免重复申请。',
|
||||
'**后续行动建议**:',
|
||||
'- 请核对申请时间是否填写正确。若日期填错,请直接回复正确的出发时间和返回时间,我会重新查询;',
|
||||
'- 若日期无误,请先查看或处理已有申请单,避免重复申请。',
|
||||
'',
|
||||
'我会先暂停本次提交,不会生成新的审批流。'
|
||||
)
|
||||
|
||||
@@ -26,13 +26,59 @@ export function getAiExpenseSteps() {
|
||||
return DEFAULT_FIELD_STEPS
|
||||
}
|
||||
|
||||
export function createAiExpenseDraft(expenseType, expenseTypeLabel) {
|
||||
// 将 task 的 ontology_fields 映射到报销草稿字段。
|
||||
// 只映射草稿能识别的字段(amount/time_range/reason/location),未知字段忽略。
|
||||
export function buildAiExpenseDraftPrefillValues(ontologyFields = {}) {
|
||||
const source = ontologyFields || {}
|
||||
const values = {}
|
||||
const amount = normalizeAnswer(
|
||||
source.amount || source.application_amount || source.applicationAmount || source.estimated_amount
|
||||
)
|
||||
if (amount) {
|
||||
values.amount = amount
|
||||
}
|
||||
const timeRange = normalizeAnswer(
|
||||
source.time_range || source.business_time || source.application_business_time || source.time
|
||||
)
|
||||
if (timeRange) {
|
||||
values.time_range = timeRange
|
||||
}
|
||||
const reason = normalizeAnswer(source.reason || source.application_reason || source.title || source.summary)
|
||||
if (reason) {
|
||||
values.reason = reason
|
||||
}
|
||||
const location = normalizeAnswer(source.location || source.application_location)
|
||||
if (location) {
|
||||
values.location = location
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// 根据已填值推进 stepKey 到第一个未填字段,全部填满则到 summary。
|
||||
function resolveInitialStepKey(values = {}) {
|
||||
for (const step of DEFAULT_FIELD_STEPS) {
|
||||
if (!normalizeAnswer(values[step.key])) {
|
||||
return step.key
|
||||
}
|
||||
}
|
||||
return SUMMARY_STEP_KEY
|
||||
}
|
||||
|
||||
export function createAiExpenseDraft(expenseType, expenseTypeLabel, prefillValues = {}) {
|
||||
const safePrefill = prefillValues && typeof prefillValues === 'object' ? prefillValues : {}
|
||||
const values = {}
|
||||
for (const step of DEFAULT_FIELD_STEPS) {
|
||||
const value = normalizeAnswer(safePrefill[step.key])
|
||||
if (value) {
|
||||
values[step.key] = value
|
||||
}
|
||||
}
|
||||
return {
|
||||
expenseType: normalizeAnswer(expenseType),
|
||||
expenseTypeLabel: normalizeAnswer(expenseTypeLabel),
|
||||
applicationClaim: null,
|
||||
values: {},
|
||||
stepKey: DEFAULT_FIELD_STEPS[0].key
|
||||
values,
|
||||
stepKey: resolveInitialStepKey(values)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -263,7 +263,9 @@ function normalizeMessage(message = {}) {
|
||||
streamStatus: safeString(message.stewardPlan.streamStatus) || 'completed'
|
||||
}
|
||||
: null,
|
||||
suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : []
|
||||
suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [],
|
||||
// 保留多 task 推进上下文,刷新后申请预览/报销草稿消息仍能拿到剩余 task 列表。
|
||||
stewardRemainingTasks: Array.isArray(message.stewardRemainingTasks) ? message.stewardRemainingTasks : []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -292,8 +292,20 @@ export function normalizeApplicationPreview(preview = {}) {
|
||||
...resolveApplicationValidationIssues(fields),
|
||||
...resolveApplicationSourceValidationIssues(preview?.sourceText, fields, preview)
|
||||
]
|
||||
const editableFields = Array.isArray(preview?.editableFields)
|
||||
? preview.editableFields
|
||||
: Array.isArray(preview?.editable_fields)
|
||||
? preview.editable_fields
|
||||
: null
|
||||
return {
|
||||
...preview,
|
||||
...(editableFields
|
||||
? {
|
||||
editableFields: editableFields
|
||||
.map((field) => String(field || '').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
: {}),
|
||||
fields,
|
||||
missingFields,
|
||||
validationIssues,
|
||||
@@ -301,6 +313,37 @@ export function normalizeApplicationPreview(preview = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveApplicationPreviewEditableFields(preview = {}) {
|
||||
const source = Array.isArray(preview?.editableFields)
|
||||
? preview.editableFields
|
||||
: Array.isArray(preview?.editable_fields)
|
||||
? preview.editable_fields
|
||||
: null
|
||||
if (!Array.isArray(source)) {
|
||||
return null
|
||||
}
|
||||
const fields = new Set(
|
||||
source
|
||||
.map((field) => String(field || '').trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
if (fields.has('time')) {
|
||||
fields.add('time_return')
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
function isApplicationPreviewFieldEditable(preview = {}, item = {}, rowKey = '') {
|
||||
if (item.editable === false) {
|
||||
return false
|
||||
}
|
||||
const editableFields = resolveApplicationPreviewEditableFields(preview)
|
||||
if (!editableFields) {
|
||||
return true
|
||||
}
|
||||
return editableFields.has(rowKey)
|
||||
}
|
||||
|
||||
export function applyApplicationBusinessTimeContext(preview = {}, businessTimeContext = null) {
|
||||
if (!businessTimeContext || typeof businessTimeContext !== 'object') {
|
||||
return normalizeApplicationPreview(preview)
|
||||
@@ -394,7 +437,7 @@ export function buildApplicationPreviewRows(preview = {}) {
|
||||
...item,
|
||||
label: '出发时间',
|
||||
value: tripDates.startDate || '待补充',
|
||||
editable: item.editable !== false,
|
||||
editable: isApplicationPreviewFieldEditable(normalized, item, 'time'),
|
||||
highlight: Boolean(item.highlight),
|
||||
missing
|
||||
},
|
||||
@@ -402,7 +445,7 @@ export function buildApplicationPreviewRows(preview = {}) {
|
||||
key: 'time_return',
|
||||
label: '返回时间',
|
||||
value: tripDates.endDate || '待补充',
|
||||
editable: item.editable !== false,
|
||||
editable: isApplicationPreviewFieldEditable(normalized, item, 'time_return'),
|
||||
highlight: Boolean(item.highlight),
|
||||
missing
|
||||
}
|
||||
@@ -415,7 +458,7 @@ export function buildApplicationPreviewRows(preview = {}) {
|
||||
...item,
|
||||
label: resolveApplicationFieldLabel(item, fields),
|
||||
value,
|
||||
editable: item.editable !== false,
|
||||
editable: isApplicationPreviewFieldEditable(normalized, item, item.key),
|
||||
highlight: Boolean(item.highlight),
|
||||
missing: item.required !== false && !isApplicationPreviewValueProvided(rawValue)
|
||||
}]
|
||||
@@ -484,6 +527,10 @@ export function buildApplicationTemplatePreview(currentUser = {}) {
|
||||
export function buildLocalApplicationPreviewMessage(preview) {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const modelReviewStatus = String(normalized.modelReviewStatus || '').trim()
|
||||
const editMode = Boolean(normalized.applicationEditMode || normalized.application_edit_mode)
|
||||
if (editMode) {
|
||||
return '我已载入原申请信息。请只修改事由、时间、地点和出行方式;职级、负责人、标准和费用会按规则带入或重新测算。'
|
||||
}
|
||||
return [
|
||||
modelReviewStatus === 'completed'
|
||||
? '我已完成模型复核,并整理成下方表格。请核查识别结果;点击对应行即可直接编辑。'
|
||||
|
||||
@@ -95,9 +95,62 @@ async function testSaveDraftActionUsesFastPreviewEndpoint() {
|
||||
assert.equal(body.context_json.application_stage, 'expense_application')
|
||||
}
|
||||
|
||||
async function testEditDraftActionCarriesClaimAndEditableFields() {
|
||||
let capturedOptions = null
|
||||
|
||||
global.fetch = async (_url, options) => {
|
||||
capturedOptions = options
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return {
|
||||
status: 'succeeded',
|
||||
result: {
|
||||
draft_payload: {
|
||||
claim_id: 'claim-edit-application',
|
||||
claim_no: 'AP-20260620-EDIT',
|
||||
status: 'draft',
|
||||
approval_stage: '待提交'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await runAiApplicationPreviewAction({
|
||||
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||||
applicationPreview: {
|
||||
applicationEditMode: true,
|
||||
editableFields: ['reason', 'time', 'location', 'transportMode'],
|
||||
fields: {
|
||||
applicationType: '差旅费用申请',
|
||||
time: '2026-07-01 至 2026-07-03',
|
||||
location: '北京',
|
||||
reason: '项目实施',
|
||||
days: '3天',
|
||||
transportMode: '火车',
|
||||
amount: '1000元'
|
||||
}
|
||||
},
|
||||
currentUser: { username: 'zhangsan@example.com', name: '张三' },
|
||||
draftPayload: {
|
||||
claim_id: 'claim-edit-application',
|
||||
claim_no: 'AP-20260620-EDIT',
|
||||
status: 'returned'
|
||||
}
|
||||
})
|
||||
|
||||
const body = JSON.parse(capturedOptions.body)
|
||||
assert.equal(body.context_json.application_edit_claim_id, 'claim-edit-application')
|
||||
assert.equal(body.context_json.application_edit_mode, true)
|
||||
assert.deepEqual(body.context_json.application_editable_fields, ['reason', 'time', 'location', 'transportMode'])
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await testSubmitActionUsesFastPreviewEndpoint()
|
||||
await testSaveDraftActionUsesFastPreviewEndpoint()
|
||||
await testEditDraftActionCarriesClaimAndEditableFields()
|
||||
console.log('ai-application-preview-actions tests passed')
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from 'node:test'
|
||||
|
||||
import {
|
||||
applyAiExpenseAnswer,
|
||||
buildAiExpenseDraftPrefillValues,
|
||||
buildAiExpenseStepPrompt,
|
||||
buildAiExpenseSummary,
|
||||
createAiExpenseDraft,
|
||||
@@ -71,3 +72,41 @@ test('summary lists every filled field and the linked application', () => {
|
||||
assert.match(summary, /AP-202606-001/)
|
||||
assert.match(summary, /85元/)
|
||||
})
|
||||
|
||||
test('buildAiExpenseDraftPrefillValues maps task ontology fields onto draft fields', () => {
|
||||
const values = buildAiExpenseDraftPrefillValues({
|
||||
expense_type: 'meal',
|
||||
amount: '2000元',
|
||||
time_range: '昨天',
|
||||
reason: '客户招待',
|
||||
location: '上海',
|
||||
unrelated_field: 'ignore me'
|
||||
})
|
||||
assert.equal(values.amount, '2000元')
|
||||
assert.equal(values.time_range, '昨天')
|
||||
assert.equal(values.reason, '客户招待')
|
||||
assert.equal(values.location, '上海')
|
||||
assert.equal(values.unrelated_field, undefined)
|
||||
})
|
||||
|
||||
test('createAiExpenseDraft with prefillValues skips already filled steps', () => {
|
||||
const draft = createAiExpenseDraft('meal', '业务招待费', {
|
||||
amount: '2000元',
|
||||
reason: '客户招待'
|
||||
})
|
||||
// reason 已填,跳到下一个未填字段 time_range
|
||||
assert.equal(draft.values.amount, '2000元')
|
||||
assert.equal(draft.values.reason, '客户招待')
|
||||
assert.equal(draft.stepKey, 'time_range')
|
||||
})
|
||||
|
||||
test('createAiExpenseDraft with all prefillValues lands on summary', () => {
|
||||
const draft = createAiExpenseDraft('meal', '业务招待费', {
|
||||
reason: '客户招待',
|
||||
time_range: '昨天',
|
||||
location: '上海',
|
||||
amount: '2000元',
|
||||
attachments: '稍后上传'
|
||||
})
|
||||
assert.ok(isAiExpenseDraftComplete(draft))
|
||||
})
|
||||
|
||||
@@ -67,3 +67,37 @@ test('AI workbench conversation store persists scoped history for sidebar sessio
|
||||
assert.equal(nextHistory.length, 1)
|
||||
assert.equal(nextHistory[0].id, 'conv-first')
|
||||
})
|
||||
|
||||
test('AI workbench conversation store preserves stewardRemainingTasks on messages', () => {
|
||||
installLocalStorageMock()
|
||||
const user = { username: 'caoxiaozhu' }
|
||||
const remainingTasks = [
|
||||
{ task_id: 't2', task_type: 'reimbursement', ontology_fields: { expense_type: 'meal' } }
|
||||
]
|
||||
|
||||
saveAiWorkbenchConversation(user, {
|
||||
id: 'conv-multi-task',
|
||||
title: '出差+招待费',
|
||||
updatedAt: Date.now(),
|
||||
messages: [
|
||||
{ id: 'u1', role: 'user', content: '出差+报销招待费' },
|
||||
{
|
||||
id: 'a1',
|
||||
role: 'assistant',
|
||||
content: '申请草稿已保存',
|
||||
stewardRemainingTasks: remainingTasks
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const history = loadAiWorkbenchConversationHistory(user)
|
||||
assert.equal(history.length, 1)
|
||||
// 历史摘要不要求保留 stewardRemainingTasks,但加载完整会话时消息上应保留。
|
||||
// 这里通过 saveAiWorkbenchConversation 的往返确认 normalizeMessage 不会丢弃该字段。
|
||||
const stored = JSON.parse(globalThis.window.localStorage.getItem(
|
||||
'x-financial:workbench-ai-conversations:caoxiaozhu'
|
||||
))
|
||||
const conversation = stored.find((item) => item.id === 'conv-multi-task')
|
||||
const persistedMessage = conversation.messages.find((m) => m.id === 'a1')
|
||||
assert.deepEqual(persistedMessage.stewardRemainingTasks, remainingTasks)
|
||||
})
|
||||
|
||||
@@ -362,6 +362,40 @@ test('travel application submit can continue with conversational planning recomm
|
||||
assert.match(recommendation, /AP-202606030001-ABCDE123/)
|
||||
})
|
||||
|
||||
test('application edit preview only allows reason time location and transport changes', () => {
|
||||
const preview = normalizeApplicationPreview({
|
||||
sourceText: '修改申请',
|
||||
applicationEditMode: true,
|
||||
editableFields: ['reason', 'time', 'location', 'transportMode'],
|
||||
fields: {
|
||||
applicationType: '差旅费用申请',
|
||||
applicant: '李文静',
|
||||
grade: 'P5',
|
||||
department: '财务部',
|
||||
position: '财务分析师',
|
||||
managerName: '王强',
|
||||
time: '2026-05-25 至 2026-05-28',
|
||||
location: '上海',
|
||||
reason: '客户现场项目支持',
|
||||
days: '4天',
|
||||
transportMode: '火车',
|
||||
lodgingDailyCap: '450元/天',
|
||||
subsidyDailyCap: '100元/天',
|
||||
transportPolicy: '按规则测算',
|
||||
policyEstimate: '交通 300元 + 住宿 1800元 + 补贴 400元 = 2500元',
|
||||
amount: '2500元'
|
||||
}
|
||||
})
|
||||
|
||||
const rows = buildApplicationPreviewRows(preview)
|
||||
const editableKeys = rows.filter((row) => row.editable).map((row) => row.key)
|
||||
assert.deepEqual(editableKeys, ['time', 'time_return', 'location', 'reason', 'transportMode'])
|
||||
assert.equal(rows.find((row) => row.key === 'applicationType')?.editable, false)
|
||||
assert.equal(rows.find((row) => row.key === 'days')?.editable, false)
|
||||
assert.equal(rows.find((row) => row.key === 'amount')?.editable, false)
|
||||
assert.match(buildLocalApplicationPreviewMessage(preview), /只修改事由、时间、地点和出行方式/)
|
||||
})
|
||||
|
||||
test('application preview renders ordered editable rows and submit text uses edited values', () => {
|
||||
const preview = buildLocalApplicationPreview('申请 2026-05-25 至 2026-05-28 去新疆,伊犁出差,服务美团业务部署,火车,预计费用1800元', {
|
||||
name: '李文静',
|
||||
|
||||
@@ -93,6 +93,65 @@ test('workbench steward application confirmation opens inline application previe
|
||||
assert.equal(preview.fields.transportMode, '')
|
||||
})
|
||||
|
||||
test('workbench low-confidence application confirmation forwards remaining tasks', () => {
|
||||
let previewCall = null
|
||||
const remainingTasks = [{
|
||||
task_id: 'task-reimbursement-2',
|
||||
task_type: 'reimbursement',
|
||||
assigned_agent: 'reimbursement_assistant',
|
||||
ontology_fields: {
|
||||
expense_type: 'entertainment',
|
||||
expense_type_label: '业务招待费',
|
||||
amount: '2000元',
|
||||
time_range: '2026-06-25',
|
||||
reason: '业务招待'
|
||||
}
|
||||
}]
|
||||
const router = useWorkbenchAiActionRouter({
|
||||
aiExpenseDraft: { value: null },
|
||||
applicationFlow: {
|
||||
isInlineSuggestedActionDisabled: () => false,
|
||||
executeInlineApplicationPreviewAction: () => {},
|
||||
startAiApplicationPreview: (...args) => {
|
||||
previewCall = args
|
||||
}
|
||||
},
|
||||
assistantDraft: { value: '' },
|
||||
attachmentFlow: {
|
||||
confirmAiAttachmentAssociation: () => {}
|
||||
},
|
||||
emit: () => {},
|
||||
expenseFlow: {
|
||||
linkAiExpenseApplication: () => {},
|
||||
pushInlineExpenseSceneSelectionPrompt: () => {},
|
||||
startAiApplicationPreviewFromAction: () => {},
|
||||
startAiExpenseDraft: () => {}
|
||||
},
|
||||
focusAiModeInput: () => {},
|
||||
hasInlineAttachmentOcrDetails: () => false,
|
||||
resolveLatestInlineUserPrompt: () => '',
|
||||
selectedFiles: { value: [] },
|
||||
startInlineConversation: () => {},
|
||||
toast: () => {},
|
||||
toggleInlineAttachmentOcrDetails: () => {}
|
||||
})
|
||||
|
||||
router.handleInlineSuggestedAction({
|
||||
label: '确认发起出差申请',
|
||||
action_type: 'ai_application_confirm_intent',
|
||||
payload: {
|
||||
sourceText: '2月20-23日去上海出差3天,服务国网服务器部署,并且报销昨天的业务招待费2000元',
|
||||
ontologyFields: { location: '上海', reason: '服务国网服务器部署' },
|
||||
stewardRemainingTasks: remainingTasks
|
||||
}
|
||||
})
|
||||
|
||||
assert.ok(previewCall, 'startAiApplicationPreview 应被调用')
|
||||
assert.deepEqual(previewCall[3].stewardRemainingTasks, remainingTasks)
|
||||
assert.equal(typeof previewCall[3].onPreviewReadyForNextTask, 'function')
|
||||
assert.equal(typeof previewCall[3].onApplicationActionCompleted, 'function')
|
||||
})
|
||||
|
||||
test('workbench reimbursement skip link action opens new reimbursement flow', () => {
|
||||
let sceneSelectionPayload = null
|
||||
let fallbackConversationStarted = false
|
||||
@@ -389,3 +448,70 @@ test('workbench steward executable submit action runs precheck before submit and
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
})
|
||||
|
||||
test('workbench steward continue-next-task reimbursement prefills ontology and forwards remaining tasks', () => {
|
||||
let expenseDraftCall = null
|
||||
const router = useWorkbenchAiActionRouter({
|
||||
aiExpenseDraft: { value: null },
|
||||
applicationFlow: {
|
||||
isInlineSuggestedActionDisabled: () => false,
|
||||
executeInlineApplicationPreviewAction: () => {}
|
||||
},
|
||||
assistantDraft: { value: '' },
|
||||
attachmentFlow: {
|
||||
confirmAiAttachmentAssociation: () => {}
|
||||
},
|
||||
emit: () => {},
|
||||
expenseFlow: {
|
||||
linkAiExpenseApplication: () => {},
|
||||
promptAiReimbursementDraftContinuation: () => {},
|
||||
promptStandaloneReimbursementDraftCreation: () => {},
|
||||
pushInlineExpenseSceneSelectionPrompt: () => {},
|
||||
startAiApplicationPreviewFromAction: () => {},
|
||||
startAiExpenseDraft: (expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement, options) => {
|
||||
expenseDraftCall = { expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement, options }
|
||||
},
|
||||
startAiReimbursementAssociationGate: () => {}
|
||||
},
|
||||
focusAiModeInput: () => {},
|
||||
hasInlineAttachmentOcrDetails: () => false,
|
||||
resolveLatestInlineUserPrompt: () => '',
|
||||
selectedFiles: { value: [] },
|
||||
startInlineConversation: () => {},
|
||||
toast: () => {},
|
||||
toggleInlineAttachmentOcrDetails: () => {}
|
||||
})
|
||||
|
||||
router.handleInlineSuggestedAction({
|
||||
label: '继续处理费用报销',
|
||||
action_type: 'steward_continue_next_task',
|
||||
payload: {
|
||||
steward_confirm_flow: true,
|
||||
flow_id: 'travel_reimbursement',
|
||||
steward_current_task: {
|
||||
task_id: 'task-meal-1',
|
||||
task_type: 'reimbursement',
|
||||
title: '业务招待费报销',
|
||||
summary: '报销昨天业务招待费2000元',
|
||||
ontology_fields: {
|
||||
expense_type: 'meal',
|
||||
expense_type_label: '业务招待费',
|
||||
amount: '2000元',
|
||||
time_range: '昨天',
|
||||
reason: '客户招待'
|
||||
}
|
||||
},
|
||||
steward_remaining_tasks: []
|
||||
}
|
||||
})
|
||||
|
||||
// task2(招待费报销)启动时:费用类型正确、语义预填到草稿、remaining tasks 透传
|
||||
assert.ok(expenseDraftCall, 'startAiExpenseDraft 应被调用')
|
||||
assert.equal(expenseDraftCall.expenseType, 'meal')
|
||||
assert.equal(expenseDraftCall.expenseTypeLabel, '业务招待费')
|
||||
assert.equal(expenseDraftCall.requiresApplicationBeforeReimbursement, true)
|
||||
assert.equal(expenseDraftCall.options.prefillValues.amount, '2000元')
|
||||
assert.equal(expenseDraftCall.options.prefillValues.reason, '客户招待')
|
||||
assert.equal(expenseDraftCall.options.prefillValues.time_range, '昨天')
|
||||
assert.deepEqual(expenseDraftCall.options.stewardRemainingTasks, [])
|
||||
})
|
||||
|
||||
@@ -4,8 +4,10 @@ import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import {
|
||||
buildWorkbenchDocumentCommandFollowupGuidance,
|
||||
buildWorkbenchDraftDeletionGuidance,
|
||||
isWorkbenchDraftDeletionIntent,
|
||||
resolveLatestWorkbenchDocumentCommandContext,
|
||||
resolveLatestWorkbenchDraftPayload
|
||||
} from '../src/composables/workbenchAiMode/workbenchAiCommandIntentModel.js'
|
||||
|
||||
@@ -87,8 +89,45 @@ test('workbench draft deletion guidance opens detail instead of deleting directl
|
||||
assert.equal(guidance.suggestedActions[0].payload.claim_no, 'ALATEST1')
|
||||
})
|
||||
|
||||
test('workbench command intent reuses previous approval candidates for follow-up approval command', () => {
|
||||
const context = resolveLatestWorkbenchDocumentCommandContext([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
'### 已查询到相关单据',
|
||||
'',
|
||||
'<article class="ai-document-card ai-document-card--application ai-document-card--approval-task is-pending">',
|
||||
'<a class="ai-document-card__action" href="#ai-open-document-detail:claim_id%3Dapproval-1%26claim_no%3DAP-APPROVAL-001">查看详情</a>',
|
||||
'</article>',
|
||||
'<article class="ai-document-card ai-document-card--reimbursement ai-document-card--approval-task is-pending">',
|
||||
'<a class="ai-document-card__action" href="#ai-open-document-detail:claim_id%3Dapproval-2%26claim_no%3DRE-APPROVAL-002">查看详情</a>',
|
||||
'</article>'
|
||||
].join('\n')
|
||||
}
|
||||
], { action: 'approve', safetyLevel: 'confirm_required' })
|
||||
|
||||
assert.equal(context?.candidates.length, 2)
|
||||
assert.deepEqual(context.candidates[0], {
|
||||
claimId: 'approval-1',
|
||||
claimNo: 'AP-APPROVAL-001',
|
||||
documentType: 'application',
|
||||
actionLabel: '查看详情'
|
||||
})
|
||||
|
||||
const guidance = buildWorkbenchDocumentCommandFollowupGuidance(context, { action: 'approve' })
|
||||
assert.match(guidance.content, /已接上刚才查询到的待审单据/)
|
||||
assert.match(guidance.content, /AP-APPROVAL-001/)
|
||||
assert.match(guidance.content, /RE-APPROVAL-002/)
|
||||
assert.equal(guidance.suggestedActions.length, 2)
|
||||
assert.equal(guidance.suggestedActions[0].action_type, 'open_application_detail')
|
||||
assert.equal(guidance.suggestedActions[0].payload.claim_id, 'approval-1')
|
||||
assert.equal(guidance.suggestedActions[0].payload.command_action, 'approve')
|
||||
})
|
||||
|
||||
test('workbench draft deletion intent is wired before draft slot continuation', () => {
|
||||
assert.match(commandIntentsScript, /isWorkbenchDraftDeletionIntent/)
|
||||
assert.match(commandIntentsScript, /resolveLatestWorkbenchDocumentCommandContext/)
|
||||
assert.match(commandIntentsScript, /buildWorkbenchDocumentCommandFollowupGuidance/)
|
||||
assert.match(commandIntentsScript, /function handleInlineDraftDeletionIntent\(cleanPrompt, entry = \{\}\)/)
|
||||
assert.match(commandIntentsScript, /resolveLatestWorkbenchDraftPayload\(conversationMessages\.value\)/)
|
||||
assert.match(commandIntentsScript, /buildWorkbenchDraftDeletionGuidance\(draftPayload\)/)
|
||||
|
||||
@@ -79,6 +79,48 @@ test('workbench AI intent planner normalizes model travel application submit pla
|
||||
})
|
||||
})
|
||||
|
||||
test('workbench AI intent planner keeps reimbursement task after first application task', () => {
|
||||
const reimbursementTask = {
|
||||
task_id: 'task-reimbursement-2',
|
||||
task_type: 'reimbursement',
|
||||
assigned_agent: 'reimbursement_assistant',
|
||||
title: '业务招待费报销',
|
||||
summary: '报销昨天的业务招待费 2000 元',
|
||||
requested_action: 'preview',
|
||||
confidence: 0.9,
|
||||
ontology_fields: {
|
||||
expense_type: 'entertainment',
|
||||
expense_type_label: '业务招待费',
|
||||
time_range: '2026-06-25',
|
||||
amount: '2000元',
|
||||
reason: '业务招待'
|
||||
},
|
||||
missing_fields: []
|
||||
}
|
||||
const plan = normalizeWorkbenchAiIntentPlan({
|
||||
planning_source: 'llm_function_call',
|
||||
tasks: [{
|
||||
task_id: 'task-application-1',
|
||||
task_type: 'expense_application',
|
||||
assigned_agent: 'application_assistant',
|
||||
requested_action: 'preview',
|
||||
confidence: 0.93,
|
||||
ontology_fields: {
|
||||
expense_type: 'travel',
|
||||
time_range: '2026-02-20 至 2026-02-23',
|
||||
location: '上海',
|
||||
reason: '服务国网服务器部署'
|
||||
},
|
||||
missing_fields: ['transport_mode']
|
||||
}, reimbursementTask]
|
||||
}, {
|
||||
prompt: '2月20-23日去上海出差3天,服务国网服务器部署,并且报销昨天的业务招待费2000元'
|
||||
})
|
||||
|
||||
assert.deepEqual(plan.stewardRemainingTasks, [reimbursementTask])
|
||||
assert.deepEqual(resolveExecutableTravelApplicationPlan(plan).stewardRemainingTasks, [reimbursementTask])
|
||||
})
|
||||
|
||||
test('workbench AI intent planner prefers server action steps when present', () => {
|
||||
const plan = normalizeWorkbenchAiIntentPlan({
|
||||
planning_source: 'llm_function_call',
|
||||
@@ -304,7 +346,12 @@ test('workbench AI mode asks steward model plan before fallback execution', () =
|
||||
assert.match(personalWorkbenchAiModeScript, /requestedSubmit:\s*travelApplicationRequest\.requestedSubmit/)
|
||||
assert.match(personalWorkbenchAiModeScript, /submitRequiresConfirmation:\s*travelApplicationRequest\.submitRequiresConfirmation/)
|
||||
assert.match(personalWorkbenchAiModeScript, /ontologyFields:\s*travelApplicationRequest\.ontologyFields/)
|
||||
assert.match(personalWorkbenchAiModeScript, /stewardRemainingTasks:\s*travelApplicationRequest\.stewardRemainingTasks/)
|
||||
assert.match(personalWorkbenchAiModeScript, /onPreviewReadyForNextTask:\s*startModelPlannedNextTask/)
|
||||
assert.match(personalWorkbenchAiModeScript, /onApplicationActionCompleted:\s*startModelPlannedNextTask/)
|
||||
assert.match(applicationPreviewFlowScript, /options\.autoSaveDraft/)
|
||||
assert.match(applicationPreviewFlowScript, /options\.onPreviewReadyForNextTask/)
|
||||
assert.match(applicationPreviewFlowScript, /onApplicationActionCompleted\(\s*targetMessage\.stewardRemainingTasks/)
|
||||
assert.doesNotMatch(applicationPreviewFlowScript, /options\.autoSubmit && normalizeApplicationPreview\(preview\)\.readyToSubmit/)
|
||||
assert.match(applicationPreviewFlowScript, /ontologyFields:\s*options\.ontologyFields/)
|
||||
assert.match(applicationPreviewFlowScript, /executeInlineApplicationPreviewAction\(AI_APPLICATION_ACTION_SAVE_DRAFT/)
|
||||
|
||||
Reference in New Issue
Block a user