Files
Jyotisha/frontend/src/lib/personal-report-failure-summary.ts
T
Jesse_ChenandCursor e87c58d6e4 fix(report): persist longform appendix on self-hosted upsert (BUG-576)
Engine calls already returned markdown; the worker failed because local postgres upsert required onConflict and the appendix write omitted it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 14:19:21 +08:00

97 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<Record<string, string>> = {
section_output_truncated: "输出被截断",
section_refs_mismatch: "证据引用未对齐",
section_identity_mismatch: "章节身份未对齐",
section_evidence_insufficient: "证据不足",
section_output_invalid: "输出未通过校验",
};
const APPENDIX_ERROR_LABELS: Readonly<Record<string, string>> = {
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;
}