/** * User-visible personal-report failure copy, derived only from allowlisted * error codes and section row status. Never include prompts, model text, * bundle contents, or user profile fields. */ export type PersonalReportSectionFailureRow = Readonly<{ status: string; lastErrorCode: string | null; }>; export type PersonalReportFailureSummary = Readonly<{ summary: string; innerReason: string | null; lastErrorCodes: readonly string[]; appendixLastErrorCode: string | null; }>; const SECTION_ERROR_LABELS: Readonly> = { section_output_truncated: "输出被截断", section_refs_mismatch: "证据引用未对齐", section_identity_mismatch: "章节身份未对齐", section_evidence_insufficient: "证据不足", section_output_invalid: "输出未通过校验", }; const APPENDIX_ERROR_LABELS: Readonly> = { upstream_unavailable: "计算引擎暂时不可用,请稍后重试", upstream_busy: "计算引擎繁忙,请稍后重试", empty_markdown: "计算引擎返回了空文,请稍后重试", generation_failed: "报告生成超时或中断,请稍后重试", appendix_persist_failed: "报告正文未能保存,请稍后重试", longform_appendix_persist_failed: "报告正文未能保存,请稍后重试", }; function uniqueLabels(codes: readonly string[]): string[] { const labels: string[] = []; for (const code of codes) { const label = SECTION_ERROR_LABELS[code] ?? "输出未通过校验"; if (!labels.includes(label)) labels.push(label); } return labels; } export function summarizePersonalReportFailure(input: Readonly<{ themeCount?: number; sections: readonly PersonalReportSectionFailureRow[]; failureCode?: string | null; appendixLastErrorCode?: string | null; }>): PersonalReportFailureSummary { const sections = input.sections; const blocked = sections.filter((row) => row.status === "blocked"); const pending = sections.filter((row) => row.status === "pending"); const ready = sections.filter((row) => row.status === "ready"); const lastErrorCodes = blocked .map((row) => row.lastErrorCode) .filter((code): code is string => typeof code === "string" && code.length > 0); const appendixLastErrorCode = textCode(input.appendixLastErrorCode); const total = input.themeCount && input.themeCount > 0 ? input.themeCount : sections.length; const labels = uniqueLabels(lastErrorCodes); const labelText = labels.length > 0 ? labels.join("、") : input.failureCode === "report_schema_invalid" ? "结构校验失败" : "生成失败"; let summary: string; if (total > 0 && blocked.length > 0 && pending.length > 0) { summary = `${total} 个主题中 ${blocked.length} 个写作失败:${labelText},${pending.length} 个未开始`; } else if (total > 0 && blocked.length > 0) { summary = `${total} 个主题中 ${blocked.length} 个写作失败:${labelText}`; } else if (ready.length > 0 && input.failureCode === "report_schema_invalid") { summary = `${ready.length} 个主题已写成,整份报告未完成装配`; } else if (input.failureCode === "report_schema_invalid") { summary = "报告未通过结构校验"; } else if (appendixLastErrorCode && APPENDIX_ERROR_LABELS[appendixLastErrorCode]) { summary = APPENDIX_ERROR_LABELS[appendixLastErrorCode]; } else { summary = "本次生成没有产出可用报告"; } let innerReason: string | null = null; if (blocked.length > 0 && pending.length === 0 && (total === 0 || blocked.length === total)) { innerReason = "all_sections_blocked"; } else if ((blocked.length > 0 && pending.length > 0) || (ready.length > 0 && blocked.length + pending.length < total)) { innerReason = "section_generation_incomplete"; } return { summary, innerReason, lastErrorCodes, appendixLastErrorCode }; } function textCode(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; }