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>
97 lines
4.0 KiB
TypeScript
97 lines
4.0 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|