From bf796946f9adb8e3b002ff70dc78dd884482d748 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 12 Jul 2026 13:35:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90=20Parent=20Group=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E5=B1=95=E7=A4=BA=E4=B8=8E=E5=A4=8D=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationTaskDetailPanel.vue | 292 +++++++++++++++++- .../reservation/ReservationTaskQueue.vue | 4 +- client/src/i18n/locales/en-US.ts | 13 + client/src/i18n/locales/th-TH.ts | 13 + client/src/i18n/locales/zh-CN.ts | 13 + .../tests/reservationTaskDetailPanel.spec.ts | 162 ++++++++++ client/src/tests/reservationViews.spec.ts | 31 ++ client/src/types/reservation.ts | 2 + client/src/utils/reservationDisplay.ts | 43 ++- ...servationSourceMessageConversationView.vue | 4 +- .../reservation/ReservationTaskListView.vue | 4 +- .../backend-to-frontend-notes.md | 3 +- .../frontend-to-backend-api-requests.md | 4 +- 13 files changed, 575 insertions(+), 13 deletions(-) diff --git a/client/src/components/reservation/ReservationTaskDetailPanel.vue b/client/src/components/reservation/ReservationTaskDetailPanel.vue index 8edddae..49d3b3b 100644 --- a/client/src/components/reservation/ReservationTaskDetailPanel.vue +++ b/client/src/components/reservation/ReservationTaskDetailPanel.vue @@ -88,7 +88,7 @@
{{ t('task.taskCardType') }}
- {{ formatReservationTaskCard(t, detail.task_card_type) }} + {{ formatReservationTaskCardLabel(t, detail) }} {{ detail.task_card_type }}
@@ -191,6 +191,79 @@
{{ formatJson(manualReviewRecord) }}
+
+
+

{{ t('task.manualReviewEvidence') }}

+

{{ manualReviewEvidence.visibleReason || t('task.manualReviewEvidenceFallback') }}

+
+
+
+
{{ t('task.manualResolution.reasonCode') }}
+
+ {{ manualReviewEvidence.reasonCode || '-' }} +
+
+
+
{{ t('task.manualReviewMissingFields') }}
+
{{ formatStringList(manualReviewEvidence.missingFields) }}
+
+
+
{{ t('task.manualReviewBlockingPoints') }}
+
{{ formatStringList(manualReviewEvidence.blockingPoints) }}
+
+
+
{{ t('task.manualReviewConflictingPoints') }}
+
{{ formatStringList(manualReviewEvidence.conflictingPoints) }}
+
+
+
{{ t('task.manualReviewSuggestedActions') }}
+
{{ formatStringList(manualReviewEvidence.suggestedHumanActions) }}
+
+
+
{{ t('task.manualReviewEvidenceToCheck') }}
+
{{ formatStringList(manualReviewEvidence.evidenceToCheck) }}
+
+
+
+

{{ t('task.parentIdentityCandidates') }}

+
+
+ {{ candidate.title || t('task.parentIdentityCandidate') }} + {{ candidate.summary || '-' }} + {{ candidate.sourceSummary || '-' }} +
+
+
{{ attribute.key }}
+
{{ attribute.value }}
+
+
+
+
+

+ {{ t('task.noParentIdentityCandidates') }} +

+
+
+
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(' / ') : '-' +}