对齐 Parent Group 前端展示与复核
This commit is contained in:
@@ -88,7 +88,7 @@
|
||||
<div>
|
||||
<dt>{{ t('task.taskCardType') }}</dt>
|
||||
<dd>
|
||||
{{ formatReservationTaskCard(t, detail.task_card_type) }}
|
||||
{{ formatReservationTaskCardLabel(t, detail) }}
|
||||
<small class="th-code">{{ detail.task_card_type }}</small>
|
||||
</dd>
|
||||
</div>
|
||||
@@ -191,6 +191,79 @@
|
||||
<pre>{{ formatJson(manualReviewRecord) }}</pre>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="manualReviewEvidence"
|
||||
class="manual-review-evidence"
|
||||
>
|
||||
<header>
|
||||
<h2>{{ t('task.manualReviewEvidence') }}</h2>
|
||||
<p>{{ manualReviewEvidence.visibleReason || t('task.manualReviewEvidenceFallback') }}</p>
|
||||
</header>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>{{ t('task.manualResolution.reasonCode') }}</dt>
|
||||
<dd class="th-code">
|
||||
{{ manualReviewEvidence.reasonCode || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.manualReviewMissingFields') }}</dt>
|
||||
<dd>{{ formatStringList(manualReviewEvidence.missingFields) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.manualReviewBlockingPoints') }}</dt>
|
||||
<dd>{{ formatStringList(manualReviewEvidence.blockingPoints) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.manualReviewConflictingPoints') }}</dt>
|
||||
<dd>{{ formatStringList(manualReviewEvidence.conflictingPoints) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.manualReviewSuggestedActions') }}</dt>
|
||||
<dd>{{ formatStringList(manualReviewEvidence.suggestedHumanActions) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.manualReviewEvidenceToCheck') }}</dt>
|
||||
<dd>{{ formatStringList(manualReviewEvidence.evidenceToCheck) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="manual-review-evidence__candidates">
|
||||
<h3>{{ t('task.parentIdentityCandidates') }}</h3>
|
||||
<div
|
||||
v-if="manualReviewEvidence.parentIdentityCandidates.length"
|
||||
class="candidate-grid"
|
||||
>
|
||||
<article
|
||||
v-for="(candidate, index) in manualReviewEvidence.parentIdentityCandidates"
|
||||
:key="`${candidate.title}-${candidate.summary}-${index}`"
|
||||
class="candidate-item"
|
||||
>
|
||||
<strong>{{ candidate.title || t('task.parentIdentityCandidate') }}</strong>
|
||||
<span class="th-code">{{ candidate.summary || '-' }}</span>
|
||||
<small>{{ candidate.sourceSummary || '-' }}</small>
|
||||
<dl
|
||||
v-if="candidate.attributes.length"
|
||||
class="candidate-item__attributes"
|
||||
>
|
||||
<div
|
||||
v-for="attribute in candidate.attributes"
|
||||
:key="attribute.key"
|
||||
>
|
||||
<dt>{{ attribute.key }}</dt>
|
||||
<dd>{{ attribute.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
<p
|
||||
v-else
|
||||
class="manual-review-evidence__empty"
|
||||
>
|
||||
{{ t('task.noParentIdentityCandidates') }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="adapterContractErrors.length"
|
||||
class="task-ai-diagnostics task-ai-diagnostics--error"
|
||||
@@ -366,6 +439,7 @@ import {
|
||||
formatReservationMaybeStableReason,
|
||||
formatReservationSystemProcessCategory,
|
||||
formatReservationTaskCard,
|
||||
formatReservationTaskCardLabel,
|
||||
formatReservationTaskRouteSummary,
|
||||
isReservationFallbackManualReviewTask,
|
||||
isReservationReadonlyDiagnosticTask,
|
||||
@@ -404,6 +478,7 @@ const isPendingTypeKnownManualReviewDetail = computed(
|
||||
const isFallbackManualReviewDetail = computed(() => isReservationFallbackManualReviewTask(detail.value))
|
||||
const sourceMessageOnlyResult = computed(() => detail.value?.source_message_only_result ?? null)
|
||||
const manualReviewRecord = computed(() => detail.value?.manual_review ?? sourceMessageOnlyResult.value?.manual_review ?? null)
|
||||
const manualReviewEvidence = computed(() => buildManualReviewEvidence(detail.value, manualReviewRecord.value))
|
||||
const adapterContractErrors = computed(() => detail.value?.adapter_contract_errors ?? [])
|
||||
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
|
||||
const isFieldRendererReadOnly = computed(
|
||||
@@ -659,6 +734,123 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
function formatJson(value: unknown): string {
|
||||
return JSON.stringify(value ?? {}, null, 2)
|
||||
}
|
||||
|
||||
interface ManualReviewEvidenceCandidate {
|
||||
title: string | null
|
||||
summary: string | null
|
||||
sourceSummary: string | null
|
||||
attributes: Array<{
|
||||
key: string
|
||||
value: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface ManualReviewEvidence {
|
||||
reasonCode: string | null
|
||||
visibleReason: string | null
|
||||
missingFields: string[]
|
||||
blockingPoints: string[]
|
||||
conflictingPoints: string[]
|
||||
suggestedHumanActions: string[]
|
||||
evidenceToCheck: string[]
|
||||
parentIdentityCandidates: ManualReviewEvidenceCandidate[]
|
||||
}
|
||||
|
||||
function buildManualReviewEvidence(
|
||||
taskDetail: ReservationTaskDetailResult | null,
|
||||
manualReview: ReservationRecord | null | undefined,
|
||||
): ManualReviewEvidence | null {
|
||||
if (!manualReview || normalizeStableCode(readString(manualReview.reason_code)) !== 'TARGET_OBJECT_UNCLEAR') {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
reasonCode: readString(manualReview.reason_code),
|
||||
visibleReason: readString(manualReview.visible_reason),
|
||||
missingFields: readStringArray(manualReview.missing_fields),
|
||||
blockingPoints: readStringArray(manualReview.blocking_points),
|
||||
conflictingPoints: readStringArray(manualReview.conflicting_points),
|
||||
suggestedHumanActions: readStringArray(manualReview.suggested_human_actions),
|
||||
evidenceToCheck: readStringArray(manualReview.evidence_to_check),
|
||||
parentIdentityCandidates: readParentIdentityCandidates(taskDetail, manualReview),
|
||||
}
|
||||
}
|
||||
|
||||
function readParentIdentityCandidates(
|
||||
taskDetail: ReservationTaskDetailResult | null,
|
||||
manualReview: ReservationRecord,
|
||||
): ManualReviewEvidenceCandidate[] {
|
||||
const contextCandidates = [
|
||||
readRecord(taskDetail?.context_used)?.parent_identity_candidates,
|
||||
readRecord(manualReview.context_used)?.parent_identity_candidates,
|
||||
manualReview.parent_identity_candidates,
|
||||
]
|
||||
const candidateList = contextCandidates.find((item) => Array.isArray(item))
|
||||
if (!Array.isArray(candidateList)) {
|
||||
return []
|
||||
}
|
||||
return candidateList
|
||||
.map((item) => readRecord(item))
|
||||
.filter((item): item is ReservationRecord => item !== null)
|
||||
.map((item) => {
|
||||
const attributes = Object.entries(item)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value: stringifyCandidateValue(value),
|
||||
}))
|
||||
.filter((attribute) => attribute.value !== '')
|
||||
return {
|
||||
title: readFirstString(item, ['field', 'candidate_field', 'key', 'field_path']),
|
||||
summary: readFirstString(item, ['value', 'raw_value', 'normalized_value', 'candidate_value']),
|
||||
sourceSummary: readFirstString(item, ['evidence_source', 'source', 'evidence', 'origin']),
|
||||
attributes,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function readRecord(value: unknown): ReservationRecord | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null
|
||||
}
|
||||
return value as ReservationRecord
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.trim() ? value : null
|
||||
}
|
||||
|
||||
function readFirstString(record: ReservationRecord, keys: string[]): string | null {
|
||||
for (const key of keys) {
|
||||
const value = readString(record[key])
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function readStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
return value.filter((item): item is string => typeof item === 'string' && item.trim() !== '')
|
||||
}
|
||||
|
||||
function stringifyCandidateValue(value: unknown): string {
|
||||
if (value === undefined || value === null) {
|
||||
return ''
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value)
|
||||
}
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
function formatStringList(items: string[]): string {
|
||||
return items.length ? items.join(' / ') : '-'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -751,6 +943,15 @@ function formatJson(value: unknown): string {
|
||||
border-color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.manual-review-evidence {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
border: 1px solid var(--th-color-warning);
|
||||
border-radius: var(--th-radius-md);
|
||||
background: color-mix(in srgb, var(--th-color-warning) 8%, var(--th-color-white));
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.source-only-result {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -766,6 +967,10 @@ function formatJson(value: unknown): string {
|
||||
.task-route-info dl,
|
||||
.task-ai-diagnostics h2,
|
||||
.task-ai-diagnostics pre,
|
||||
.manual-review-evidence h2,
|
||||
.manual-review-evidence h3,
|
||||
.manual-review-evidence p,
|
||||
.manual-review-evidence dl,
|
||||
.source-only-result h2,
|
||||
.source-only-result p,
|
||||
.source-only-result dl {
|
||||
@@ -775,11 +980,24 @@ function formatJson(value: unknown): string {
|
||||
.task-evidence h2,
|
||||
.task-route-info h2,
|
||||
.task-ai-diagnostics h2,
|
||||
.manual-review-evidence h2,
|
||||
.source-only-result h2 {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.manual-review-evidence header {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.manual-review-evidence header p,
|
||||
.manual-review-evidence__empty {
|
||||
color: var(--th-color-slate-600);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.source-only-result header {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -793,7 +1011,8 @@ function formatJson(value: unknown): string {
|
||||
}
|
||||
|
||||
.task-evidence dl,
|
||||
.task-route-info dl {
|
||||
.task-route-info dl,
|
||||
.manual-review-evidence > dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
@@ -806,7 +1025,8 @@ function formatJson(value: unknown): string {
|
||||
}
|
||||
|
||||
.task-evidence dt,
|
||||
.task-route-info dt {
|
||||
.task-route-info dt,
|
||||
.manual-review-evidence > dl dt {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
@@ -814,6 +1034,7 @@ function formatJson(value: unknown): string {
|
||||
|
||||
.task-evidence dd,
|
||||
.task-route-info dd,
|
||||
.manual-review-evidence > dl dd,
|
||||
.source-only-result dd {
|
||||
margin: 5px 0 0;
|
||||
color: var(--th-color-slate-900);
|
||||
@@ -821,6 +1042,65 @@ function formatJson(value: unknown): string {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.manual-review-evidence__candidates {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.manual-review-evidence__candidates h3 {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.candidate-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.candidate-item {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.candidate-item strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.candidate-item small {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.candidate-item__attributes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.candidate-item__attributes div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(90px, 0.35fr) minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.candidate-item__attributes dt,
|
||||
.candidate-item__attributes dd {
|
||||
margin: 0;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.candidate-item__attributes dd {
|
||||
color: var(--th-color-slate-700);
|
||||
}
|
||||
|
||||
.source-only-result__raw {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -934,7 +1214,9 @@ function formatJson(value: unknown): string {
|
||||
}
|
||||
|
||||
.task-evidence dl,
|
||||
.task-route-info dl {
|
||||
.task-route-info dl,
|
||||
.manual-review-evidence > dl,
|
||||
.candidate-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -942,6 +1224,8 @@ function formatJson(value: unknown): string {
|
||||
@media (max-width: 640px) {
|
||||
.task-evidence dl,
|
||||
.task-route-info dl,
|
||||
.manual-review-evidence > dl,
|
||||
.candidate-grid,
|
||||
.source-only-result dl {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
>
|
||||
<span class="task-row__index">{{ task.queue_sequence ?? '-' }}</span>
|
||||
<span class="task-row__main">
|
||||
<strong>{{ formatReservationTaskCard(t, task.task_type, task.card_name) }}</strong>
|
||||
<strong>{{ formatReservationTaskCardLabel(t, task) }}</strong>
|
||||
<small>{{ formatReservationTaskRouteSummary(t, task) }}</small>
|
||||
<em :class="{ 'task-row__reason--blocked': !task.can_process }">
|
||||
{{ task.can_process ? t('taskList.canProcess') : t('taskList.cannotProcess') }}
|
||||
@@ -51,7 +51,7 @@ import ReservationStatusBadge from '@/components/reservation/ReservationStatusBa
|
||||
import type { ReservationOrderTaskTimelineItem } from '@/types/reservation'
|
||||
import {
|
||||
formatReservationReadonlyReason,
|
||||
formatReservationTaskCard,
|
||||
formatReservationTaskCardLabel,
|
||||
formatReservationTaskRouteSummary,
|
||||
} from '@/utils/reservationDisplay'
|
||||
|
||||
|
||||
@@ -309,6 +309,7 @@ export default {
|
||||
},
|
||||
viewOrder: 'View order',
|
||||
viewConversation: 'View email conversation',
|
||||
parentGroup: 'Parent Group',
|
||||
sourceMessageId: 'Source message ID',
|
||||
sourceSubject: 'Source subject',
|
||||
sourceSender: 'Sender',
|
||||
@@ -334,6 +335,16 @@ export default {
|
||||
agentAssessment: 'AI raw fields',
|
||||
sourceNotification: 'Source message notification',
|
||||
manualReviewData: 'Manual review data',
|
||||
manualReviewEvidence: 'Manual review evidence',
|
||||
manualReviewEvidenceFallback: 'Confirm the Parent Group identity from the source email and candidates.',
|
||||
manualReviewMissingFields: 'Missing fields',
|
||||
manualReviewBlockingPoints: 'Blocking points',
|
||||
manualReviewConflictingPoints: 'Conflicting points',
|
||||
manualReviewSuggestedActions: 'Suggested actions',
|
||||
manualReviewEvidenceToCheck: 'Evidence to check',
|
||||
parentIdentityCandidates: 'Parent Group candidates',
|
||||
parentIdentityCandidate: 'Candidate',
|
||||
noParentIdentityCandidates: 'No Parent Group candidates yet. Backend should expose context_used.parent_identity_candidates.',
|
||||
adapterContractErrors: 'Adapter contract errors',
|
||||
unhandledIntents: 'Unhandled intents',
|
||||
noSourceOnlyResult: 'No source message entry result is available.',
|
||||
@@ -459,6 +470,7 @@ export default {
|
||||
NEW_BOOKING: 'New booking',
|
||||
UPDATE_BOOKING: 'Update booking',
|
||||
CANCEL_BOOKING: 'Cancel booking',
|
||||
CANCEL_ALLOTMENT: 'Cancel Parent Group allotment',
|
||||
VOUCHER: 'Voucher',
|
||||
ROOMING_LIST: 'Rooming list',
|
||||
AMEND_GROUP_CODE: 'Amend group code',
|
||||
@@ -479,6 +491,7 @@ export default {
|
||||
UPDATE_STAY_DATES: 'Update stay dates',
|
||||
UPDATE_ROOM_TYPE: 'Update room type',
|
||||
UPDATE_FIX_CHARGE: 'Update fixed charge',
|
||||
CANCEL_ALLOTMENT_CONTROL_BLOCK: 'Cancel Parent Group control block',
|
||||
NEW_GROUP_BLOCK: 'New group block',
|
||||
NEW_FIT_RESERVATION: 'New FIT reservation',
|
||||
MANUAL_REVIEW: 'Manual review',
|
||||
|
||||
@@ -309,6 +309,7 @@ export default {
|
||||
},
|
||||
viewOrder: 'ดูออเดอร์',
|
||||
viewConversation: 'ดูเธรดอีเมล',
|
||||
parentGroup: 'Parent Group',
|
||||
sourceMessageId: 'รหัสข้อความต้นทาง (ID)',
|
||||
sourceSubject: 'หัวข้อต้นทาง',
|
||||
sourceSender: 'ผู้ส่ง',
|
||||
@@ -334,6 +335,16 @@ export default {
|
||||
agentAssessment: 'ข้อมูลดิบจาก AI',
|
||||
sourceNotification: 'การแจ้งเตือนข้อความต้นทาง',
|
||||
manualReviewData: 'ข้อมูลตรวจสอบด้วยคน',
|
||||
manualReviewEvidence: 'หลักฐานการตรวจสอบด้วยคน',
|
||||
manualReviewEvidenceFallback: 'ยืนยันตัวตน Parent Group จากอีเมลต้นทางและรายการตัวเลือก',
|
||||
manualReviewMissingFields: 'ข้อมูลที่ขาด',
|
||||
manualReviewBlockingPoints: 'จุดที่บล็อก',
|
||||
manualReviewConflictingPoints: 'จุดที่ขัดแย้ง',
|
||||
manualReviewSuggestedActions: 'การดำเนินการที่แนะนำ',
|
||||
manualReviewEvidenceToCheck: 'หลักฐานที่ควรตรวจ',
|
||||
parentIdentityCandidates: 'ตัวเลือก Parent Group',
|
||||
parentIdentityCandidate: 'ตัวเลือก',
|
||||
noParentIdentityCandidates: 'ยังไม่มีตัวเลือก Parent Group ต้องให้หลังบ้านส่ง context_used.parent_identity_candidates',
|
||||
adapterContractErrors: 'ข้อผิดพลาดสัญญา Adapter',
|
||||
unhandledIntents: 'เจตนาที่ยังไม่รองรับ',
|
||||
noSourceOnlyResult: 'ยังไม่มีผลลัพธ์ทางเข้าของข้อความต้นทาง',
|
||||
@@ -459,6 +470,7 @@ export default {
|
||||
NEW_BOOKING: 'จองใหม่',
|
||||
UPDATE_BOOKING: 'แก้ไขการจอง',
|
||||
CANCEL_BOOKING: 'ยกเลิกการจอง',
|
||||
CANCEL_ALLOTMENT: 'ยกเลิกโควตา Parent Group',
|
||||
VOUCHER: 'วอเชอร์',
|
||||
ROOMING_LIST: 'รายชื่อห้องพัก',
|
||||
AMEND_GROUP_CODE: 'แก้ไขเลขกรุ๊ป',
|
||||
@@ -479,6 +491,7 @@ export default {
|
||||
UPDATE_STAY_DATES: 'แก้ไขวันเข้าพัก',
|
||||
UPDATE_ROOM_TYPE: 'แก้ไขประเภทห้อง',
|
||||
UPDATE_FIX_CHARGE: 'แก้ไขค่าบริการคงที่',
|
||||
CANCEL_ALLOTMENT_CONTROL_BLOCK: 'ยกเลิกบล็อก Parent Group ทั้งก้อน',
|
||||
NEW_GROUP_BLOCK: 'บล็อกกรุ๊ปใหม่',
|
||||
NEW_FIT_RESERVATION: 'จองรายบุคคลใหม่',
|
||||
MANUAL_REVIEW: 'ตรวจสอบด้วยคน',
|
||||
|
||||
@@ -309,6 +309,7 @@ export default {
|
||||
},
|
||||
viewOrder: '查看订单',
|
||||
viewConversation: '查看邮件会话',
|
||||
parentGroup: 'Parent Group',
|
||||
sourceMessageId: '来源消息编号(ID)',
|
||||
sourceSubject: '来源主题',
|
||||
sourceSender: '发件人',
|
||||
@@ -334,6 +335,16 @@ export default {
|
||||
agentAssessment: 'AI 原始字段',
|
||||
sourceNotification: '来源消息通知',
|
||||
manualReviewData: '人工复核信息',
|
||||
manualReviewEvidence: '人工复核证据',
|
||||
manualReviewEvidenceFallback: '请根据来源邮件和候选信息确认 Parent Group 身份。',
|
||||
manualReviewMissingFields: '缺失字段',
|
||||
manualReviewBlockingPoints: '阻塞点',
|
||||
manualReviewConflictingPoints: '冲突点',
|
||||
manualReviewSuggestedActions: '建议动作',
|
||||
manualReviewEvidenceToCheck: '建议核对证据',
|
||||
parentIdentityCandidates: 'Parent Group 候选',
|
||||
parentIdentityCandidate: '候选项',
|
||||
noParentIdentityCandidates: '暂无 Parent Group 候选;需要后端透出 context_used.parent_identity_candidates。',
|
||||
adapterContractErrors: '适配契约异常',
|
||||
unhandledIntents: '未处理意图',
|
||||
noSourceOnlyResult: '暂无来源消息入口结果。',
|
||||
@@ -459,6 +470,7 @@ export default {
|
||||
NEW_BOOKING: '新预订',
|
||||
UPDATE_BOOKING: '修改预订',
|
||||
CANCEL_BOOKING: '取消预订',
|
||||
CANCEL_ALLOTMENT: '取消 Parent Group 配额',
|
||||
VOUCHER: '凭证',
|
||||
ROOMING_LIST: '房表',
|
||||
AMEND_GROUP_CODE: '修改团队号',
|
||||
@@ -479,6 +491,7 @@ export default {
|
||||
UPDATE_STAY_DATES: '修改入住日期',
|
||||
UPDATE_ROOM_TYPE: '修改房型',
|
||||
UPDATE_FIX_CHARGE: '修改固定收费',
|
||||
CANCEL_ALLOTMENT_CONTROL_BLOCK: '取消 Parent Group 整块配额',
|
||||
NEW_GROUP_BLOCK: '新团队预留',
|
||||
NEW_FIT_RESERVATION: '新散客预订',
|
||||
MANUAL_REVIEW: '人工复核',
|
||||
|
||||
@@ -587,6 +587,168 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
expect(wrapper.find('button[title="重试"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders R08 parent group manual review with identity candidates on the same task card', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail)
|
||||
.mockResolvedValueOnce(
|
||||
createTaskDetail({
|
||||
result_type: 'manual_review',
|
||||
ai_task_type: 'Cancel Allotment',
|
||||
task_subtype: 'cancel_allotment_control_block',
|
||||
route_code: 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW',
|
||||
system_process_category: 'BUSINESS_TASK',
|
||||
system_task_type: 'CANCEL_BOOKING',
|
||||
task_card_type: 'CANCEL_ALLOTMENT',
|
||||
task_status: 'BLOCKED',
|
||||
review_status: 'PENDING',
|
||||
manual_review: {
|
||||
reason_code: 'target_object_unclear',
|
||||
visible_reason: 'Parent Group 的 group_code 与 block_code 原始候选冲突,请人工确认目标。',
|
||||
missing_fields: ['/case_keys/group_code', '/case_keys/block_code'],
|
||||
blocking_points: ['Parent Group identity cannot be safely normalized.'],
|
||||
conflicting_points: ['PARENT-2608-CONFLICT-A', 'PARENT-2608-CONFLICT-B'],
|
||||
suggested_human_actions: ['confirm_parent_group_identity'],
|
||||
evidence_to_check: ['parent_identity_candidates'],
|
||||
},
|
||||
context_used: {
|
||||
parent_identity_candidates: [
|
||||
{
|
||||
field: 'group_code',
|
||||
value: 'PARENT-2608-CONFLICT-A',
|
||||
evidence_source: 'subject',
|
||||
},
|
||||
{
|
||||
field: 'block_code',
|
||||
value: 'PARENT-2608-CONFLICT-B',
|
||||
evidence_source: 'attachment',
|
||||
normalized_value: 'PARENT-2608-CONFLICT-B',
|
||||
},
|
||||
{
|
||||
candidate_field: 'parent_group_code',
|
||||
raw_value: 'PARENT-2608-CONFLICT-C',
|
||||
origin: 'email_body',
|
||||
},
|
||||
],
|
||||
},
|
||||
availability: {
|
||||
blocked: true,
|
||||
read_only: true,
|
||||
editable: false,
|
||||
confirmable: false,
|
||||
executable: false,
|
||||
blocked_by_task_id: null,
|
||||
blocked_reason: 'MANUAL_REVIEW_REQUIRED',
|
||||
},
|
||||
fields: [
|
||||
createRequiredField({
|
||||
field_path: 'case_keys.group_code',
|
||||
field_pointer: '/case_keys/group_code',
|
||||
display_name: 'Parent Group 团队号',
|
||||
value: null,
|
||||
}),
|
||||
createRequiredField({
|
||||
field_path: 'case_keys.block_code',
|
||||
field_pointer: '/case_keys/block_code',
|
||||
display_name: 'Parent Group Block Code',
|
||||
value: null,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
createTaskDetail({
|
||||
result_type: 'manual_review',
|
||||
ai_task_type: 'Cancel Allotment',
|
||||
task_subtype: 'cancel_allotment_control_block',
|
||||
route_code: 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW',
|
||||
system_process_category: 'BUSINESS_TASK',
|
||||
system_task_type: 'CANCEL_BOOKING',
|
||||
task_card_type: 'CANCEL_ALLOTMENT',
|
||||
task_status: 'READY',
|
||||
review_status: 'RESOLVED',
|
||||
review_resolution: {
|
||||
confirmed_order_id: '20001',
|
||||
field_overrides: [
|
||||
{
|
||||
field_pointer: '/case_keys/group_code',
|
||||
field_path: 'case_keys.group_code',
|
||||
value: 'PARENT-2608-CONFLICT-A',
|
||||
},
|
||||
{
|
||||
field_pointer: '/case_keys/block_code',
|
||||
field_path: 'case_keys.block_code',
|
||||
value: 'PARENT-2608-CONFLICT-A',
|
||||
},
|
||||
],
|
||||
},
|
||||
availability: {
|
||||
blocked: false,
|
||||
read_only: false,
|
||||
editable: false,
|
||||
confirmable: false,
|
||||
executable: true,
|
||||
blocked_by_task_id: null,
|
||||
blocked_reason: null,
|
||||
},
|
||||
opera_operations: [createOperaOperation()],
|
||||
}),
|
||||
)
|
||||
vi.mocked(service.fetchReservationTaskAudits)
|
||||
.mockResolvedValueOnce({
|
||||
task_id: '10001',
|
||||
items: [createAudit('audit-1', 'TASK_LOADED')],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
task_id: '10001',
|
||||
items: [createAudit('audit-2', 'MANUAL_REVIEW_RESOLVED')],
|
||||
})
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
|
||||
expect(wrapper.text()).toContain('Parent Group')
|
||||
expect(wrapper.text()).toContain('取消 Parent Group 配额')
|
||||
expect(wrapper.text()).toContain('取消 Parent Group 整块配额')
|
||||
expect(wrapper.text()).toContain('R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW')
|
||||
expect(wrapper.text()).toContain('人工复核证据')
|
||||
expect(wrapper.text()).toContain('target_object_unclear')
|
||||
expect(wrapper.text()).toContain('Parent Group 的 group_code 与 block_code 原始候选冲突')
|
||||
expect(wrapper.text()).toContain('PARENT-2608-CONFLICT-A')
|
||||
expect(wrapper.text()).toContain('PARENT-2608-CONFLICT-B')
|
||||
expect(wrapper.text()).toContain('parent_group_code')
|
||||
expect(wrapper.text()).toContain('PARENT-2608-CONFLICT-C')
|
||||
expect(wrapper.text()).toContain('subject')
|
||||
expect(wrapper.text()).toContain('attachment')
|
||||
expect(wrapper.text()).toContain('email_body')
|
||||
expect(wrapper.text()).toContain('normalized_value')
|
||||
expect(wrapper.text()).not.toContain('适配契约异常')
|
||||
expect(wrapper.text()).not.toContain('人工转换')
|
||||
expect(wrapper.text()).not.toContain('保存草稿')
|
||||
expect(wrapper.text()).not.toContain('确认任务')
|
||||
|
||||
await wrapper.find('input[name="manual_resolution_field_override_0"]').setValue('PARENT-2608-CONFLICT-A')
|
||||
await wrapper.find('input[name="manual_resolution_field_override_1"]').setValue('PARENT-2608-CONFLICT-A')
|
||||
await wrapper.find('textarea[name="manual_resolution_reason"]').setValue('确认 Parent Group identity')
|
||||
await findButton(wrapper, '提交复核结果').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.resolveReservationManualReviewTask).toHaveBeenCalledWith('10001', {
|
||||
confirmed_order_id: '20001',
|
||||
reason: '确认 Parent Group identity',
|
||||
field_overrides: [
|
||||
{
|
||||
field_pointer: '/case_keys/group_code',
|
||||
field_path: 'case_keys.group_code',
|
||||
value: 'PARENT-2608-CONFLICT-A',
|
||||
},
|
||||
{
|
||||
field_pointer: '/case_keys/block_code',
|
||||
field_path: 'case_keys.block_code',
|
||||
value: 'PARENT-2608-CONFLICT-A',
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(wrapper.text()).toContain('人工复核已解阻')
|
||||
})
|
||||
|
||||
it('resolves type-known manual review on the same task card', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail)
|
||||
.mockResolvedValueOnce(
|
||||
|
||||
@@ -132,6 +132,21 @@ function createSourceMessageOnlyTaskListResult(taskSubtype: 'S10' | 'S99' = 'S10
|
||||
}
|
||||
}
|
||||
|
||||
function createParentGroupCancelAllotmentTaskListResult(): ReservationTaskListResult {
|
||||
const result = createTaskListResult('PARENT-2608-BLOCK', 'Parent split confirmation')
|
||||
result.items[0] = {
|
||||
...result.items[0]!,
|
||||
task_type: 'CANCEL_BOOKING',
|
||||
task_subtype: 'cancel_allotment_control_block',
|
||||
result_type: 'normal_task',
|
||||
ai_task_type: 'Cancel Allotment',
|
||||
route_code: 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL',
|
||||
system_process_category: 'BUSINESS_TASK',
|
||||
card_name: 'CANCEL_ALLOTMENT',
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function createOrderDetailResult(displayName = 'GRP-001') {
|
||||
return {
|
||||
order: {
|
||||
@@ -516,6 +531,7 @@ describe('reservation P0 views', () => {
|
||||
])
|
||||
expect(taskSubtypeOptions).not.toContain('S000')
|
||||
expect(taskSubtypeOptions).not.toContain('S999')
|
||||
expect(taskSubtypeOptions).not.toContain('linked_parent_release_after_child_split')
|
||||
expect(wrapper.find('select[name="task_type"]').text()).toContain('来源消息只读')
|
||||
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('纯信息类邮件(S10)')
|
||||
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('无法形成业务素材包(S99)')
|
||||
@@ -550,6 +566,21 @@ describe('reservation P0 views', () => {
|
||||
expect(lastTaskRequest).not.toHaveProperty('system_process_category')
|
||||
})
|
||||
|
||||
it('renders R08 parent group cancel allotment as a business card', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createParentGroupCancelAllotmentTaskListResult())
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(wrapper.text()).toContain('PARENT-2608-BLOCK')
|
||||
expect(wrapper.text()).toContain('Parent Group')
|
||||
expect(wrapper.text()).toContain('取消 Parent Group 配额')
|
||||
expect(wrapper.text()).toContain('取消 Parent Group 整块配额')
|
||||
expect(wrapper.text()).toContain('查看订单')
|
||||
expect(wrapper.text()).not.toContain('适配契约异常')
|
||||
expect(wrapper.text()).not.toContain('linked_parent_release_after_child_split')
|
||||
})
|
||||
|
||||
it('renders source-message-only task rows without a visible order action', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10'))
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export type ReservationTaskCardType =
|
||||
| 'NEW_BOOKING'
|
||||
| 'UPDATE_BOOKING'
|
||||
| 'CANCEL_BOOKING'
|
||||
| 'CANCEL_ALLOTMENT'
|
||||
| 'VOUCHER'
|
||||
| 'ROOMING_LIST'
|
||||
| 'AMEND_GROUP_CODE'
|
||||
@@ -365,6 +366,7 @@ export interface ReservationTaskDetailResult {
|
||||
review_status?: string | null
|
||||
review_resolution?: ReservationRecord | null
|
||||
manual_review?: ReservationRecord | null
|
||||
context_used?: ReservationRecord | null
|
||||
availability: ReservationTaskAvailabilityResult
|
||||
fields: ReservationTaskFieldResult[]
|
||||
opera_operations: ReservationOperaOperationResult[]
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ReservationTaskRouteLike {
|
||||
task_type?: string | null
|
||||
system_task_type?: string | null
|
||||
task_card_type?: string | null
|
||||
card_name?: string | null
|
||||
task_subtype?: string | null
|
||||
result_type?: string | null
|
||||
ai_task_type?: string | null
|
||||
@@ -19,6 +20,27 @@ export function formatReservationTaskCard(
|
||||
return translateStableCode(t, 'cardType', taskType, 'common.unknownTaskType', fallbackLabel)
|
||||
}
|
||||
|
||||
export function formatReservationTaskCardLabel(
|
||||
t: ReservationTranslator,
|
||||
input: ReservationTaskRouteLike,
|
||||
fallbackLabel?: string | null,
|
||||
): string {
|
||||
if (isReservationAdapterContractErrorTask(input)) {
|
||||
return t('resultType.ADAPTER_CONTRACT_ERROR')
|
||||
}
|
||||
if (isReservationUnhandledIntentTask(input)) {
|
||||
return t('resultType.UNHANDLED_CURRENT_INTENT')
|
||||
}
|
||||
if (isReservationParentGroupCancelAllotmentTask(input)) {
|
||||
return formatReservationTaskCard(t, 'CANCEL_ALLOTMENT')
|
||||
}
|
||||
return formatReservationTaskCard(
|
||||
t,
|
||||
input.task_card_type ?? input.task_type ?? input.system_task_type,
|
||||
fallbackLabel ?? input.card_name,
|
||||
)
|
||||
}
|
||||
|
||||
export function formatReservationTaskTypeSummary(
|
||||
t: ReservationTranslator,
|
||||
taskType: string | null | undefined,
|
||||
@@ -86,6 +108,18 @@ export function isReservationReadonlyDiagnosticTask(input: ReservationTaskRouteL
|
||||
)
|
||||
}
|
||||
|
||||
export function isReservationParentGroupCancelAllotmentTask(input: ReservationTaskRouteLike | null | undefined): boolean {
|
||||
const routeCode = normalizeStableCode(input?.route_code)
|
||||
const taskSubtype = normalizeStableCode(input?.task_subtype)
|
||||
const taskCardType = normalizeStableCode(input?.task_card_type ?? input?.card_name)
|
||||
return (
|
||||
routeCode === 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL' ||
|
||||
routeCode === 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW' ||
|
||||
taskSubtype === 'CANCEL_ALLOTMENT_CONTROL_BLOCK' ||
|
||||
taskCardType === 'CANCEL_ALLOTMENT'
|
||||
)
|
||||
}
|
||||
|
||||
export function isReservationTypeKnownManualReviewTask(input: ReservationTaskRouteLike | null | undefined): boolean {
|
||||
if (normalizeStableCode(input?.result_type) !== 'MANUAL_REVIEW') {
|
||||
return false
|
||||
@@ -121,6 +155,13 @@ export function formatReservationTaskRouteSummary(
|
||||
if (isReservationUnhandledIntentTask(input)) {
|
||||
return t('resultType.UNHANDLED_CURRENT_INTENT')
|
||||
}
|
||||
if (isReservationParentGroupCancelAllotmentTask(input)) {
|
||||
return [
|
||||
t('task.parentGroup'),
|
||||
formatReservationTaskCard(t, 'CANCEL_ALLOTMENT'),
|
||||
formatReservationRouteCode(t, 'cancel_allotment_control_block'),
|
||||
].join(' / ')
|
||||
}
|
||||
return formatReservationTaskTypeSummary(
|
||||
t,
|
||||
input.task_type ?? input.system_task_type ?? input.task_card_type,
|
||||
@@ -144,7 +185,7 @@ export function formatReservationRouteCode(t: ReservationTranslator, routeCode:
|
||||
if (['S10', 'S99', 'S000', 'S999'].includes(normalizedCode)) {
|
||||
return translateStableCode(t, 'taskSubtype', routeCode, 'common.unknownTaskSubtype')
|
||||
}
|
||||
return routeCode || t('common.unknownTaskSubtype')
|
||||
return translateStableCode(t, 'taskSubtype', routeCode, 'common.unknownTaskSubtype', routeCode)
|
||||
}
|
||||
|
||||
export function formatReservationReadonlyReason(
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
:key="task.task_id"
|
||||
>
|
||||
<RouterLink :to="`/reservation/tasks/${task.task_id}`">
|
||||
{{ formatReservationTaskCard(t, task.task_type, task.card_name) }}
|
||||
{{ formatReservationTaskCardLabel(t, task) }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -154,7 +154,7 @@ import type {
|
||||
SourceMessageConversationResult,
|
||||
SourceMessageOriginalMedia,
|
||||
} from '@/types/reservation'
|
||||
import { formatReservationTaskCard } from '@/utils/reservationDisplay'
|
||||
import { formatReservationTaskCardLabel } from '@/utils/reservationDisplay'
|
||||
import { formatReservationDateTime } from '@/utils/reservationFormat'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ formatReservationTaskCard(t, task.task_type, task.card_name) }}</span>
|
||||
<span>{{ formatReservationTaskCardLabel(t, task) }}</span>
|
||||
<small v-if="task.result_type">{{ formatReservationResultType(t, task.result_type) }}</small>
|
||||
</td>
|
||||
<td><ReservationStatusBadge :status="task.task_status" /></td>
|
||||
@@ -275,7 +275,7 @@ import type { ReservationPageResult, ReservationTaskListFilters, ReservationTask
|
||||
import {
|
||||
formatReservationReadonlyReason,
|
||||
formatReservationResultType,
|
||||
formatReservationTaskCard,
|
||||
formatReservationTaskCardLabel,
|
||||
formatReservationTaskRouteSummary,
|
||||
isReservationReadonlyDiagnosticTask,
|
||||
} from '@/utils/reservationDisplay'
|
||||
|
||||
@@ -146,7 +146,8 @@ POST /api/auth/logout
|
||||
- 任务列表里旧 `task_type=SOURCE_MESSAGE_ONLY`、`task_subtype=S000/S999` 或新 `task_subtype=S10/S99` 的记录只展示邮件来源和 SuperAgent 入口结果,不展示处理按钮。
|
||||
- 任务详情里 `source_message_only_result` 仅对 `SOURCE_MESSAGE_ONLY` 返回,包含 `entry_result_code`、`entry_result_meaning`、`entry_result_description`、`entry_result_source_message_id`、`result_type`、`route_code`、`agent_assessment`、`notification`、`manual_review` 和 `raw_answer`;普通业务任务该字段为空。
|
||||
- 任务列表、订单任务时间线和任务详情顶层已透出 `result_type`、`ai_task_type`、`route_code`、`system_process_category`。前端展示任务卡标题和标签时优先用这些稳定 code,不要只靠旧 `task_type` 判断。
|
||||
- P0.1 后,Parent split 父事件不再是独立 Parent Cancel Booking 卡;前端应展示为 `Cancel Allotment / cancel_allotment_control_block`。`linked_parent_release_after_child_split` 只作为关系字段或详情信息,不作为任务 subtype 筛选项。
|
||||
- P0.1 后,Parent split 父事件不再是独立 Parent Cancel Booking 卡;前端应展示为 `Parent Group / Cancel Allotment / cancel_allotment_control_block`。`route_code=R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL` 是普通业务卡,`route_code=R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW` 是同卡人工复核业务卡,不应展示成 `adapter_contract_error`。`linked_parent_release_after_child_split` 只作为关系字段或详情信息,不作为任务 subtype 筛选项。
|
||||
- `manual_review.reason_code=target_object_unclear` 时,前端需要在任务详情展示 `manual_review.visible_reason`、`missing_fields`、`blocking_points`、`conflicting_points`、`suggested_human_actions`、`evidence_to_check`,并展示 `context_used.parent_identity_candidates[]` 辅助确认 Parent Group identity。当前前端已兼容顶层 `context_used.parent_identity_candidates[]` 或 `manual_review.context_used.parent_identity_candidates[]`;若后端 DTO 不透出 candidates,页面会显示候选空态。
|
||||
- P0.1 的“40 条路由”表示当前合法 route definition 数量;`route_code` 保持历史稳定且不连续重编号,因此 `R41_FALLBACK_BUSINESS_EVENT_REVIEW` 和 `R42_UNHANDLED_CURRENT_INTENT` 仍是合法展示 code。
|
||||
- `adapter_contract_errors[]` 和 `unhandled_intents[]` 只在任务详情返回,表示同一 SuperAgent 入站批次中没有生成业务任务的诊断块;前端只读展示并提供来源邮件入口,不显示保存、确认、执行或重试按钮。
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ POST /api/system/reservation/demo-data
|
||||
| 旧 `S000/S999` 兼容映射 | 任务列表、任务详情 | 已完成第一版 | 旧数据继续可见;前端可按 `S000→S10`、`S999→S99` 展示统一文案。 |
|
||||
| 40 条 P0.1 路由元数据 | 任务列表筛选、订单任务时间线、任务详情标题、字段展示 | 已完成第一版 | 后端保存并返回 AI 原始 `result_type/ai_task_type/task_subtype`、`route_code` 和系统处理分类;前端不要只依赖系统主任务类型判断卡片。 |
|
||||
| P0.1 稳定 route_code | 任务列表筛选、订单任务时间线、任务详情标题 | 已完成第一版 | `route_code` 保持历史稳定,不因路由总数变 40 而连续重编号;前端仍可能看到 `R41_FALLBACK_BUSINESS_EVENT_REVIEW` 和 `R42_UNHANDLED_CURRENT_INTENT`。 |
|
||||
| Parent split 父事件卡型 | 任务列表、订单任务时间线、任务详情标题 | 已完成第一版 | 0712 P0.1 后,Parent split 父事件展示为 `Cancel Allotment / cancel_allotment_control_block`;`linked_parent_release_after_child_split` 仅作为关系字段,不作为任务 subtype 筛选项。 |
|
||||
| Parent split 父事件卡型 | 任务列表、订单任务时间线、任务详情标题 | 已完成第一版 | 0712 P0.1 后,Parent split 父事件展示为 `Parent Group / Cancel Allotment / cancel_allotment_control_block`;`route_code=R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL` 展示为普通业务卡,`route_code=R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW` 展示为同卡人工复核业务卡;`linked_parent_release_after_child_split` 仅作为关系字段,不作为任务 subtype 筛选项。 |
|
||||
| `unhandled_current_intents[]` 展示块 | 任务详情 | 已完成第一版 | 后端保存并在任务详情 `unhandled_intents[]` 返回未覆盖业务意图,只用于展示和源邮件查看,不自动建业务任务卡。 |
|
||||
| `adapter_contract_error` | 任务详情、错误提示 | 已完成第一版 | 命中 P1/P2 未闭合或路由冲突时,任务详情 `adapter_contract_errors[]` 返回稳定错误 code 和原始片段,不转成 Fallback。 |
|
||||
| type-known manual review 同卡解阻 | 任务详情复核 | 已完成第一版 | `manual_review` 不再全部等同 Fallback;已知业务卡型返回原业务卡信息、`review_status`、`review_resolution` 和可编辑 pointer 字段,解阻后进入 `READY`。 |
|
||||
@@ -340,6 +340,7 @@ POST /api/system/reservation/demo-data
|
||||
|
||||
- 任务列表已按 `result_type`、`route_code`、`system_process_category` 识别 `source_message_review_notification`、`adapter_contract_error`、`unhandled_current_intent` 只读诊断任务;S10/S99 和旧 S000/S999 都不展示订单入口。
|
||||
- 任务详情已展示 `result_type`、`ai_task_type`、`task_subtype`、`route_code`、`system_process_category`、`review_status`、来源邮件入口、`source_message_only_result`、`manual_review`、`adapter_contract_errors[]` 和 `unhandled_intents[]`。
|
||||
- Parent Group P0.1 前端已按 `route_code=R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL/REVIEW` 展示 `Parent Group / Cancel Allotment / cancel_allotment_control_block`;review 场景继续走同卡人工复核解阻,不展示为 `adapter_contract_error`。
|
||||
- type-known `result_type=manual_review` 已在原业务任务卡展示复核状态和缺失字段,并调用 `POST /api/reservation/tasks/{taskId}/manual-review-resolutions` 解阻,不再创建第二张人工复核任务卡。
|
||||
- 第一版解阻 UI 已改为优先使用任务详情 `fields[].field_pointer`,并同时提交 P0 主 `fields[].field_path`;旧扁平 `legacy_field_path` / `legacy_field_values` 仅用于 `field_contract_version=code-v1` 历史任务过渡回显,不作为新前端主动提交路径。
|
||||
- 前端只读规则已收口:S10/S99、适配契约异常、未处理意图、前置任务阻塞和同卡人工复核待解阻状态都不显示保存草稿、确认任务、人工转换或 OPERA 执行 / 重试入口。
|
||||
@@ -350,6 +351,7 @@ POST /api/system/reservation/demo-data
|
||||
- `GET /api/reservation/tasks` 是否会在结构化 S10/S99 行中稳定返回 `task_type=SOURCE_MESSAGE_ONLY`,或允许返回 `MESSAGE_NOTIFICATION` 并只依赖 `result_type/route_code/system_process_category`;前端当前两种都兼容。
|
||||
- `adapter_contract_error` / `unhandled_current_intent` 如果未来也作为独立列表行返回,请保持 `source_message_id` 可用,便于前端继续提供邮件会话入口。
|
||||
- 同卡人工复核解阻成功后是否一定返回 `opera_operations[]`。当前文档写“两条 OPERA 模拟操作”,前端实现按实际返回刷新,不假设固定数量。
|
||||
- Parent Group `manual_review.reason_code=target_object_unclear` 场景需要任务详情稳定透出 `context_used.parent_identity_candidates[]`。当前 `ReservationTaskDetailResult` 后端 DTO 仅透出 `manual_review`,前端已兼容顶层 `context_used.parent_identity_candidates[]` 或 `manual_review.context_used.parent_identity_candidates[]`,但若后端不透出 candidates,页面只能显示空态提示。
|
||||
|
||||
前端注意:不要把访问口令写入前端仓库、浏览器环境变量或构建产物;该接口只能由本地联调人员手动调用或由受控测试脚本调用。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user