对齐 Parent Group 前端展示与复核
This commit is contained in:
@@ -88,7 +88,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<dt>{{ t('task.taskCardType') }}</dt>
|
<dt>{{ t('task.taskCardType') }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ formatReservationTaskCard(t, detail.task_card_type) }}
|
{{ formatReservationTaskCardLabel(t, detail) }}
|
||||||
<small class="th-code">{{ detail.task_card_type }}</small>
|
<small class="th-code">{{ detail.task_card_type }}</small>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,6 +191,79 @@
|
|||||||
<pre>{{ formatJson(manualReviewRecord) }}</pre>
|
<pre>{{ formatJson(manualReviewRecord) }}</pre>
|
||||||
</section>
|
</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
|
<section
|
||||||
v-if="adapterContractErrors.length"
|
v-if="adapterContractErrors.length"
|
||||||
class="task-ai-diagnostics task-ai-diagnostics--error"
|
class="task-ai-diagnostics task-ai-diagnostics--error"
|
||||||
@@ -366,6 +439,7 @@ import {
|
|||||||
formatReservationMaybeStableReason,
|
formatReservationMaybeStableReason,
|
||||||
formatReservationSystemProcessCategory,
|
formatReservationSystemProcessCategory,
|
||||||
formatReservationTaskCard,
|
formatReservationTaskCard,
|
||||||
|
formatReservationTaskCardLabel,
|
||||||
formatReservationTaskRouteSummary,
|
formatReservationTaskRouteSummary,
|
||||||
isReservationFallbackManualReviewTask,
|
isReservationFallbackManualReviewTask,
|
||||||
isReservationReadonlyDiagnosticTask,
|
isReservationReadonlyDiagnosticTask,
|
||||||
@@ -404,6 +478,7 @@ const isPendingTypeKnownManualReviewDetail = computed(
|
|||||||
const isFallbackManualReviewDetail = computed(() => isReservationFallbackManualReviewTask(detail.value))
|
const isFallbackManualReviewDetail = computed(() => isReservationFallbackManualReviewTask(detail.value))
|
||||||
const sourceMessageOnlyResult = computed(() => detail.value?.source_message_only_result ?? null)
|
const sourceMessageOnlyResult = computed(() => detail.value?.source_message_only_result ?? null)
|
||||||
const manualReviewRecord = computed(() => detail.value?.manual_review ?? sourceMessageOnlyResult.value?.manual_review ?? 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 adapterContractErrors = computed(() => detail.value?.adapter_contract_errors ?? [])
|
||||||
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
|
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
|
||||||
const isFieldRendererReadOnly = computed(
|
const isFieldRendererReadOnly = computed(
|
||||||
@@ -659,6 +734,123 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
|||||||
function formatJson(value: unknown): string {
|
function formatJson(value: unknown): string {
|
||||||
return JSON.stringify(value ?? {}, null, 2)
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -751,6 +943,15 @@ function formatJson(value: unknown): string {
|
|||||||
border-color: var(--th-color-danger);
|
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 {
|
.source-only-result {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -766,6 +967,10 @@ function formatJson(value: unknown): string {
|
|||||||
.task-route-info dl,
|
.task-route-info dl,
|
||||||
.task-ai-diagnostics h2,
|
.task-ai-diagnostics h2,
|
||||||
.task-ai-diagnostics pre,
|
.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 h2,
|
||||||
.source-only-result p,
|
.source-only-result p,
|
||||||
.source-only-result dl {
|
.source-only-result dl {
|
||||||
@@ -775,11 +980,24 @@ function formatJson(value: unknown): string {
|
|||||||
.task-evidence h2,
|
.task-evidence h2,
|
||||||
.task-route-info h2,
|
.task-route-info h2,
|
||||||
.task-ai-diagnostics h2,
|
.task-ai-diagnostics h2,
|
||||||
|
.manual-review-evidence h2,
|
||||||
.source-only-result h2 {
|
.source-only-result h2 {
|
||||||
color: var(--th-color-slate-900);
|
color: var(--th-color-slate-900);
|
||||||
font-size: 15px;
|
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 {
|
.source-only-result header {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -793,7 +1011,8 @@ function formatJson(value: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-evidence dl,
|
.task-evidence dl,
|
||||||
.task-route-info dl {
|
.task-route-info dl,
|
||||||
|
.manual-review-evidence > dl {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -806,7 +1025,8 @@ function formatJson(value: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-evidence dt,
|
.task-evidence dt,
|
||||||
.task-route-info dt {
|
.task-route-info dt,
|
||||||
|
.manual-review-evidence > dl dt {
|
||||||
color: var(--th-color-slate-500);
|
color: var(--th-color-slate-500);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -814,6 +1034,7 @@ function formatJson(value: unknown): string {
|
|||||||
|
|
||||||
.task-evidence dd,
|
.task-evidence dd,
|
||||||
.task-route-info dd,
|
.task-route-info dd,
|
||||||
|
.manual-review-evidence > dl dd,
|
||||||
.source-only-result dd {
|
.source-only-result dd {
|
||||||
margin: 5px 0 0;
|
margin: 5px 0 0;
|
||||||
color: var(--th-color-slate-900);
|
color: var(--th-color-slate-900);
|
||||||
@@ -821,6 +1042,65 @@ function formatJson(value: unknown): string {
|
|||||||
overflow-wrap: anywhere;
|
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 {
|
.source-only-result__raw {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
@@ -934,7 +1214,9 @@ function formatJson(value: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-evidence dl,
|
.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));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -942,6 +1224,8 @@ function formatJson(value: unknown): string {
|
|||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.task-evidence dl,
|
.task-evidence dl,
|
||||||
.task-route-info dl,
|
.task-route-info dl,
|
||||||
|
.manual-review-evidence > dl,
|
||||||
|
.candidate-grid,
|
||||||
.source-only-result dl {
|
.source-only-result dl {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
>
|
>
|
||||||
<span class="task-row__index">{{ task.queue_sequence ?? '-' }}</span>
|
<span class="task-row__index">{{ task.queue_sequence ?? '-' }}</span>
|
||||||
<span class="task-row__main">
|
<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>
|
<small>{{ formatReservationTaskRouteSummary(t, task) }}</small>
|
||||||
<em :class="{ 'task-row__reason--blocked': !task.can_process }">
|
<em :class="{ 'task-row__reason--blocked': !task.can_process }">
|
||||||
{{ task.can_process ? t('taskList.canProcess') : t('taskList.cannotProcess') }}
|
{{ 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 type { ReservationOrderTaskTimelineItem } from '@/types/reservation'
|
||||||
import {
|
import {
|
||||||
formatReservationReadonlyReason,
|
formatReservationReadonlyReason,
|
||||||
formatReservationTaskCard,
|
formatReservationTaskCardLabel,
|
||||||
formatReservationTaskRouteSummary,
|
formatReservationTaskRouteSummary,
|
||||||
} from '@/utils/reservationDisplay'
|
} from '@/utils/reservationDisplay'
|
||||||
|
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export default {
|
|||||||
},
|
},
|
||||||
viewOrder: 'View order',
|
viewOrder: 'View order',
|
||||||
viewConversation: 'View email conversation',
|
viewConversation: 'View email conversation',
|
||||||
|
parentGroup: 'Parent Group',
|
||||||
sourceMessageId: 'Source message ID',
|
sourceMessageId: 'Source message ID',
|
||||||
sourceSubject: 'Source subject',
|
sourceSubject: 'Source subject',
|
||||||
sourceSender: 'Sender',
|
sourceSender: 'Sender',
|
||||||
@@ -334,6 +335,16 @@ export default {
|
|||||||
agentAssessment: 'AI raw fields',
|
agentAssessment: 'AI raw fields',
|
||||||
sourceNotification: 'Source message notification',
|
sourceNotification: 'Source message notification',
|
||||||
manualReviewData: 'Manual review data',
|
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',
|
adapterContractErrors: 'Adapter contract errors',
|
||||||
unhandledIntents: 'Unhandled intents',
|
unhandledIntents: 'Unhandled intents',
|
||||||
noSourceOnlyResult: 'No source message entry result is available.',
|
noSourceOnlyResult: 'No source message entry result is available.',
|
||||||
@@ -459,6 +470,7 @@ export default {
|
|||||||
NEW_BOOKING: 'New booking',
|
NEW_BOOKING: 'New booking',
|
||||||
UPDATE_BOOKING: 'Update booking',
|
UPDATE_BOOKING: 'Update booking',
|
||||||
CANCEL_BOOKING: 'Cancel booking',
|
CANCEL_BOOKING: 'Cancel booking',
|
||||||
|
CANCEL_ALLOTMENT: 'Cancel Parent Group allotment',
|
||||||
VOUCHER: 'Voucher',
|
VOUCHER: 'Voucher',
|
||||||
ROOMING_LIST: 'Rooming list',
|
ROOMING_LIST: 'Rooming list',
|
||||||
AMEND_GROUP_CODE: 'Amend group code',
|
AMEND_GROUP_CODE: 'Amend group code',
|
||||||
@@ -479,6 +491,7 @@ export default {
|
|||||||
UPDATE_STAY_DATES: 'Update stay dates',
|
UPDATE_STAY_DATES: 'Update stay dates',
|
||||||
UPDATE_ROOM_TYPE: 'Update room type',
|
UPDATE_ROOM_TYPE: 'Update room type',
|
||||||
UPDATE_FIX_CHARGE: 'Update fixed charge',
|
UPDATE_FIX_CHARGE: 'Update fixed charge',
|
||||||
|
CANCEL_ALLOTMENT_CONTROL_BLOCK: 'Cancel Parent Group control block',
|
||||||
NEW_GROUP_BLOCK: 'New group block',
|
NEW_GROUP_BLOCK: 'New group block',
|
||||||
NEW_FIT_RESERVATION: 'New FIT reservation',
|
NEW_FIT_RESERVATION: 'New FIT reservation',
|
||||||
MANUAL_REVIEW: 'Manual review',
|
MANUAL_REVIEW: 'Manual review',
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export default {
|
|||||||
},
|
},
|
||||||
viewOrder: 'ดูออเดอร์',
|
viewOrder: 'ดูออเดอร์',
|
||||||
viewConversation: 'ดูเธรดอีเมล',
|
viewConversation: 'ดูเธรดอีเมล',
|
||||||
|
parentGroup: 'Parent Group',
|
||||||
sourceMessageId: 'รหัสข้อความต้นทาง (ID)',
|
sourceMessageId: 'รหัสข้อความต้นทาง (ID)',
|
||||||
sourceSubject: 'หัวข้อต้นทาง',
|
sourceSubject: 'หัวข้อต้นทาง',
|
||||||
sourceSender: 'ผู้ส่ง',
|
sourceSender: 'ผู้ส่ง',
|
||||||
@@ -334,6 +335,16 @@ export default {
|
|||||||
agentAssessment: 'ข้อมูลดิบจาก AI',
|
agentAssessment: 'ข้อมูลดิบจาก AI',
|
||||||
sourceNotification: 'การแจ้งเตือนข้อความต้นทาง',
|
sourceNotification: 'การแจ้งเตือนข้อความต้นทาง',
|
||||||
manualReviewData: 'ข้อมูลตรวจสอบด้วยคน',
|
manualReviewData: 'ข้อมูลตรวจสอบด้วยคน',
|
||||||
|
manualReviewEvidence: 'หลักฐานการตรวจสอบด้วยคน',
|
||||||
|
manualReviewEvidenceFallback: 'ยืนยันตัวตน Parent Group จากอีเมลต้นทางและรายการตัวเลือก',
|
||||||
|
manualReviewMissingFields: 'ข้อมูลที่ขาด',
|
||||||
|
manualReviewBlockingPoints: 'จุดที่บล็อก',
|
||||||
|
manualReviewConflictingPoints: 'จุดที่ขัดแย้ง',
|
||||||
|
manualReviewSuggestedActions: 'การดำเนินการที่แนะนำ',
|
||||||
|
manualReviewEvidenceToCheck: 'หลักฐานที่ควรตรวจ',
|
||||||
|
parentIdentityCandidates: 'ตัวเลือก Parent Group',
|
||||||
|
parentIdentityCandidate: 'ตัวเลือก',
|
||||||
|
noParentIdentityCandidates: 'ยังไม่มีตัวเลือก Parent Group ต้องให้หลังบ้านส่ง context_used.parent_identity_candidates',
|
||||||
adapterContractErrors: 'ข้อผิดพลาดสัญญา Adapter',
|
adapterContractErrors: 'ข้อผิดพลาดสัญญา Adapter',
|
||||||
unhandledIntents: 'เจตนาที่ยังไม่รองรับ',
|
unhandledIntents: 'เจตนาที่ยังไม่รองรับ',
|
||||||
noSourceOnlyResult: 'ยังไม่มีผลลัพธ์ทางเข้าของข้อความต้นทาง',
|
noSourceOnlyResult: 'ยังไม่มีผลลัพธ์ทางเข้าของข้อความต้นทาง',
|
||||||
@@ -459,6 +470,7 @@ export default {
|
|||||||
NEW_BOOKING: 'จองใหม่',
|
NEW_BOOKING: 'จองใหม่',
|
||||||
UPDATE_BOOKING: 'แก้ไขการจอง',
|
UPDATE_BOOKING: 'แก้ไขการจอง',
|
||||||
CANCEL_BOOKING: 'ยกเลิกการจอง',
|
CANCEL_BOOKING: 'ยกเลิกการจอง',
|
||||||
|
CANCEL_ALLOTMENT: 'ยกเลิกโควตา Parent Group',
|
||||||
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_FIX_CHARGE: 'แก้ไขค่าบริการคงที่',
|
||||||
|
CANCEL_ALLOTMENT_CONTROL_BLOCK: 'ยกเลิกบล็อก Parent Group ทั้งก้อน',
|
||||||
NEW_GROUP_BLOCK: 'บล็อกกรุ๊ปใหม่',
|
NEW_GROUP_BLOCK: 'บล็อกกรุ๊ปใหม่',
|
||||||
NEW_FIT_RESERVATION: 'จองรายบุคคลใหม่',
|
NEW_FIT_RESERVATION: 'จองรายบุคคลใหม่',
|
||||||
MANUAL_REVIEW: 'ตรวจสอบด้วยคน',
|
MANUAL_REVIEW: 'ตรวจสอบด้วยคน',
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export default {
|
|||||||
},
|
},
|
||||||
viewOrder: '查看订单',
|
viewOrder: '查看订单',
|
||||||
viewConversation: '查看邮件会话',
|
viewConversation: '查看邮件会话',
|
||||||
|
parentGroup: 'Parent Group',
|
||||||
sourceMessageId: '来源消息编号(ID)',
|
sourceMessageId: '来源消息编号(ID)',
|
||||||
sourceSubject: '来源主题',
|
sourceSubject: '来源主题',
|
||||||
sourceSender: '发件人',
|
sourceSender: '发件人',
|
||||||
@@ -334,6 +335,16 @@ export default {
|
|||||||
agentAssessment: 'AI 原始字段',
|
agentAssessment: 'AI 原始字段',
|
||||||
sourceNotification: '来源消息通知',
|
sourceNotification: '来源消息通知',
|
||||||
manualReviewData: '人工复核信息',
|
manualReviewData: '人工复核信息',
|
||||||
|
manualReviewEvidence: '人工复核证据',
|
||||||
|
manualReviewEvidenceFallback: '请根据来源邮件和候选信息确认 Parent Group 身份。',
|
||||||
|
manualReviewMissingFields: '缺失字段',
|
||||||
|
manualReviewBlockingPoints: '阻塞点',
|
||||||
|
manualReviewConflictingPoints: '冲突点',
|
||||||
|
manualReviewSuggestedActions: '建议动作',
|
||||||
|
manualReviewEvidenceToCheck: '建议核对证据',
|
||||||
|
parentIdentityCandidates: 'Parent Group 候选',
|
||||||
|
parentIdentityCandidate: '候选项',
|
||||||
|
noParentIdentityCandidates: '暂无 Parent Group 候选;需要后端透出 context_used.parent_identity_candidates。',
|
||||||
adapterContractErrors: '适配契约异常',
|
adapterContractErrors: '适配契约异常',
|
||||||
unhandledIntents: '未处理意图',
|
unhandledIntents: '未处理意图',
|
||||||
noSourceOnlyResult: '暂无来源消息入口结果。',
|
noSourceOnlyResult: '暂无来源消息入口结果。',
|
||||||
@@ -459,6 +470,7 @@ export default {
|
|||||||
NEW_BOOKING: '新预订',
|
NEW_BOOKING: '新预订',
|
||||||
UPDATE_BOOKING: '修改预订',
|
UPDATE_BOOKING: '修改预订',
|
||||||
CANCEL_BOOKING: '取消预订',
|
CANCEL_BOOKING: '取消预订',
|
||||||
|
CANCEL_ALLOTMENT: '取消 Parent Group 配额',
|
||||||
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_FIX_CHARGE: '修改固定收费',
|
||||||
|
CANCEL_ALLOTMENT_CONTROL_BLOCK: '取消 Parent Group 整块配额',
|
||||||
NEW_GROUP_BLOCK: '新团队预留',
|
NEW_GROUP_BLOCK: '新团队预留',
|
||||||
NEW_FIT_RESERVATION: '新散客预订',
|
NEW_FIT_RESERVATION: '新散客预订',
|
||||||
MANUAL_REVIEW: '人工复核',
|
MANUAL_REVIEW: '人工复核',
|
||||||
|
|||||||
@@ -587,6 +587,168 @@ describe('ReservationTaskDetailPanel', () => {
|
|||||||
expect(wrapper.find('button[title="重试"]').exists()).toBe(false)
|
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 () => {
|
it('resolves type-known manual review on the same task card', async () => {
|
||||||
vi.mocked(service.fetchReservationTaskDetail)
|
vi.mocked(service.fetchReservationTaskDetail)
|
||||||
.mockResolvedValueOnce(
|
.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') {
|
function createOrderDetailResult(displayName = 'GRP-001') {
|
||||||
return {
|
return {
|
||||||
order: {
|
order: {
|
||||||
@@ -516,6 +531,7 @@ describe('reservation P0 views', () => {
|
|||||||
])
|
])
|
||||||
expect(taskSubtypeOptions).not.toContain('S000')
|
expect(taskSubtypeOptions).not.toContain('S000')
|
||||||
expect(taskSubtypeOptions).not.toContain('S999')
|
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_type"]').text()).toContain('来源消息只读')
|
||||||
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('纯信息类邮件(S10)')
|
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('纯信息类邮件(S10)')
|
||||||
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('无法形成业务素材包(S99)')
|
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')
|
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 () => {
|
it('renders source-message-only task rows without a visible order action', async () => {
|
||||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10'))
|
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10'))
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type ReservationTaskCardType =
|
|||||||
| 'NEW_BOOKING'
|
| 'NEW_BOOKING'
|
||||||
| 'UPDATE_BOOKING'
|
| 'UPDATE_BOOKING'
|
||||||
| 'CANCEL_BOOKING'
|
| 'CANCEL_BOOKING'
|
||||||
|
| 'CANCEL_ALLOTMENT'
|
||||||
| 'VOUCHER'
|
| 'VOUCHER'
|
||||||
| 'ROOMING_LIST'
|
| 'ROOMING_LIST'
|
||||||
| 'AMEND_GROUP_CODE'
|
| 'AMEND_GROUP_CODE'
|
||||||
@@ -365,6 +366,7 @@ export interface ReservationTaskDetailResult {
|
|||||||
review_status?: string | null
|
review_status?: string | null
|
||||||
review_resolution?: ReservationRecord | null
|
review_resolution?: ReservationRecord | null
|
||||||
manual_review?: ReservationRecord | null
|
manual_review?: ReservationRecord | null
|
||||||
|
context_used?: ReservationRecord | null
|
||||||
availability: ReservationTaskAvailabilityResult
|
availability: ReservationTaskAvailabilityResult
|
||||||
fields: ReservationTaskFieldResult[]
|
fields: ReservationTaskFieldResult[]
|
||||||
opera_operations: ReservationOperaOperationResult[]
|
opera_operations: ReservationOperaOperationResult[]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface ReservationTaskRouteLike {
|
|||||||
task_type?: string | null
|
task_type?: string | null
|
||||||
system_task_type?: string | null
|
system_task_type?: string | null
|
||||||
task_card_type?: string | null
|
task_card_type?: string | null
|
||||||
|
card_name?: string | null
|
||||||
task_subtype?: string | null
|
task_subtype?: string | null
|
||||||
result_type?: string | null
|
result_type?: string | null
|
||||||
ai_task_type?: string | null
|
ai_task_type?: string | null
|
||||||
@@ -19,6 +20,27 @@ export function formatReservationTaskCard(
|
|||||||
return translateStableCode(t, 'cardType', taskType, 'common.unknownTaskType', fallbackLabel)
|
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(
|
export function formatReservationTaskTypeSummary(
|
||||||
t: ReservationTranslator,
|
t: ReservationTranslator,
|
||||||
taskType: string | null | undefined,
|
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 {
|
export function isReservationTypeKnownManualReviewTask(input: ReservationTaskRouteLike | null | undefined): boolean {
|
||||||
if (normalizeStableCode(input?.result_type) !== 'MANUAL_REVIEW') {
|
if (normalizeStableCode(input?.result_type) !== 'MANUAL_REVIEW') {
|
||||||
return false
|
return false
|
||||||
@@ -121,6 +155,13 @@ export function formatReservationTaskRouteSummary(
|
|||||||
if (isReservationUnhandledIntentTask(input)) {
|
if (isReservationUnhandledIntentTask(input)) {
|
||||||
return t('resultType.UNHANDLED_CURRENT_INTENT')
|
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(
|
return formatReservationTaskTypeSummary(
|
||||||
t,
|
t,
|
||||||
input.task_type ?? input.system_task_type ?? input.task_card_type,
|
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)) {
|
if (['S10', 'S99', 'S000', 'S999'].includes(normalizedCode)) {
|
||||||
return translateStableCode(t, 'taskSubtype', routeCode, 'common.unknownTaskSubtype')
|
return translateStableCode(t, 'taskSubtype', routeCode, 'common.unknownTaskSubtype')
|
||||||
}
|
}
|
||||||
return routeCode || t('common.unknownTaskSubtype')
|
return translateStableCode(t, 'taskSubtype', routeCode, 'common.unknownTaskSubtype', routeCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatReservationReadonlyReason(
|
export function formatReservationReadonlyReason(
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
:key="task.task_id"
|
:key="task.task_id"
|
||||||
>
|
>
|
||||||
<RouterLink :to="`/reservation/tasks/${task.task_id}`">
|
<RouterLink :to="`/reservation/tasks/${task.task_id}`">
|
||||||
{{ formatReservationTaskCard(t, task.task_type, task.card_name) }}
|
{{ formatReservationTaskCardLabel(t, task) }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -154,7 +154,7 @@ import type {
|
|||||||
SourceMessageConversationResult,
|
SourceMessageConversationResult,
|
||||||
SourceMessageOriginalMedia,
|
SourceMessageOriginalMedia,
|
||||||
} from '@/types/reservation'
|
} from '@/types/reservation'
|
||||||
import { formatReservationTaskCard } from '@/utils/reservationDisplay'
|
import { formatReservationTaskCardLabel } from '@/utils/reservationDisplay'
|
||||||
import { formatReservationDateTime } from '@/utils/reservationFormat'
|
import { formatReservationDateTime } from '@/utils/reservationFormat'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
<small v-if="task.result_type">{{ formatReservationResultType(t, task.result_type) }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td><ReservationStatusBadge :status="task.task_status" /></td>
|
<td><ReservationStatusBadge :status="task.task_status" /></td>
|
||||||
@@ -275,7 +275,7 @@ import type { ReservationPageResult, ReservationTaskListFilters, ReservationTask
|
|||||||
import {
|
import {
|
||||||
formatReservationReadonlyReason,
|
formatReservationReadonlyReason,
|
||||||
formatReservationResultType,
|
formatReservationResultType,
|
||||||
formatReservationTaskCard,
|
formatReservationTaskCardLabel,
|
||||||
formatReservationTaskRouteSummary,
|
formatReservationTaskRouteSummary,
|
||||||
isReservationReadonlyDiagnosticTask,
|
isReservationReadonlyDiagnosticTask,
|
||||||
} from '@/utils/reservationDisplay'
|
} 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 入口结果,不展示处理按钮。
|
- 任务列表里旧 `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`;普通业务任务该字段为空。
|
- 任务详情里 `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` 判断。
|
- 任务列表、订单任务时间线和任务详情顶层已透出 `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。
|
- P0.1 的“40 条路由”表示当前合法 route definition 数量;`route_code` 保持历史稳定且不连续重编号,因此 `R41_FALLBACK_BUSINESS_EVENT_REVIEW` 和 `R42_UNHANDLED_CURRENT_INTENT` 仍是合法展示 code。
|
||||||
- `adapter_contract_errors[]` 和 `unhandled_intents[]` 只在任务详情返回,表示同一 SuperAgent 入站批次中没有生成业务任务的诊断块;前端只读展示并提供来源邮件入口,不显示保存、确认、执行或重试按钮。
|
- `adapter_contract_errors[]` 和 `unhandled_intents[]` 只在任务详情返回,表示同一 SuperAgent 入站批次中没有生成业务任务的诊断块;前端只读展示并提供来源邮件入口,不显示保存、确认、执行或重试按钮。
|
||||||
|
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ POST /api/system/reservation/demo-data
|
|||||||
| 旧 `S000/S999` 兼容映射 | 任务列表、任务详情 | 已完成第一版 | 旧数据继续可见;前端可按 `S000→S10`、`S999→S99` 展示统一文案。 |
|
| 旧 `S000/S999` 兼容映射 | 任务列表、任务详情 | 已完成第一版 | 旧数据继续可见;前端可按 `S000→S10`、`S999→S99` 展示统一文案。 |
|
||||||
| 40 条 P0.1 路由元数据 | 任务列表筛选、订单任务时间线、任务详情标题、字段展示 | 已完成第一版 | 后端保存并返回 AI 原始 `result_type/ai_task_type/task_subtype`、`route_code` 和系统处理分类;前端不要只依赖系统主任务类型判断卡片。 |
|
| 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`。 |
|
| 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[]` 返回未覆盖业务意图,只用于展示和源邮件查看,不自动建业务任务卡。 |
|
| `unhandled_current_intents[]` 展示块 | 任务详情 | 已完成第一版 | 后端保存并在任务详情 `unhandled_intents[]` 返回未覆盖业务意图,只用于展示和源邮件查看,不自动建业务任务卡。 |
|
||||||
| `adapter_contract_error` | 任务详情、错误提示 | 已完成第一版 | 命中 P1/P2 未闭合或路由冲突时,任务详情 `adapter_contract_errors[]` 返回稳定错误 code 和原始片段,不转成 Fallback。 |
|
| `adapter_contract_error` | 任务详情、错误提示 | 已完成第一版 | 命中 P1/P2 未闭合或路由冲突时,任务详情 `adapter_contract_errors[]` 返回稳定错误 code 和原始片段,不转成 Fallback。 |
|
||||||
| type-known manual review 同卡解阻 | 任务详情复核 | 已完成第一版 | `manual_review` 不再全部等同 Fallback;已知业务卡型返回原业务卡信息、`review_status`、`review_resolution` 和可编辑 pointer 字段,解阻后进入 `READY`。 |
|
| 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`、`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[]`。
|
- 任务详情已展示 `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` 解阻,不再创建第二张人工复核任务卡。
|
- 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` 历史任务过渡回显,不作为新前端主动提交路径。
|
- 第一版解阻 UI 已改为优先使用任务详情 `fields[].field_pointer`,并同时提交 P0 主 `fields[].field_path`;旧扁平 `legacy_field_path` / `legacy_field_values` 仅用于 `field_contract_version=code-v1` 历史任务过渡回显,不作为新前端主动提交路径。
|
||||||
- 前端只读规则已收口:S10/S99、适配契约异常、未处理意图、前置任务阻塞和同卡人工复核待解阻状态都不显示保存草稿、确认任务、人工转换或 OPERA 执行 / 重试入口。
|
- 前端只读规则已收口: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`;前端当前两种都兼容。
|
- `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` 可用,便于前端继续提供邮件会话入口。
|
- `adapter_contract_error` / `unhandled_current_intent` 如果未来也作为独立列表行返回,请保持 `source_message_id` 可用,便于前端继续提供邮件会话入口。
|
||||||
- 同卡人工复核解阻成功后是否一定返回 `opera_operations[]`。当前文档写“两条 OPERA 模拟操作”,前端实现按实际返回刷新,不假设固定数量。
|
- 同卡人工复核解阻成功后是否一定返回 `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