fix: make rectification follow-up actionable
This commit is contained in:
@@ -458,6 +458,21 @@ function boundaryDistance(range: { readonly startTime: string; readonly endTime:
|
||||
return Math.max(0, Math.min(value - start, end - value));
|
||||
}
|
||||
|
||||
async function rectificationPacketStage<Value>(
|
||||
stage: "score_events" | "scan" | "merge_scans" | "candidate_differences" | "time_links" | "technical_packet",
|
||||
operation: () => Value | Promise<Value>,
|
||||
): Promise<Value> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
const errorKind = error instanceof Error ? error.name : typeof error;
|
||||
const status = error !== null && typeof error === "object" && "status" in error
|
||||
&& typeof error.status === "number" ? error.status : null;
|
||||
console.error(`[birth-time-conversation-packet] stage=${stage} error=${errorKind}${status === null ? "" : ` status=${status}`}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildProductionConversationalRectificationPacket(
|
||||
engine: BirthTimeJourneyEngine,
|
||||
input: ConversationalRectificationPacketBuildInput,
|
||||
@@ -466,21 +481,23 @@ export async function buildProductionConversationalRectificationPacket(
|
||||
if (place.latitude === undefined || place.longitude === undefined) {
|
||||
throw new ConversationalRectificationError("profile_incomplete");
|
||||
}
|
||||
const latitude = place.latitude;
|
||||
const longitude = place.longitude;
|
||||
const baseRange = currentRange(input);
|
||||
const events = scoreableLifeEvents(
|
||||
input.evidence as readonly LifeEventEvidence[],
|
||||
input.declaredBirthInput.birthDate,
|
||||
);
|
||||
const eventScore: CandidateResult | null = events.length >= 3
|
||||
? await engine.scoreEvents({
|
||||
? await rectificationPacketStage("score_events", () => engine.scoreEvents({
|
||||
birthDate: input.declaredBirthInput.birthDate,
|
||||
startTime: baseRange.startTime,
|
||||
endTime: baseRange.endTime,
|
||||
lat: place.latitude,
|
||||
lon: place.longitude,
|
||||
lat: latitude,
|
||||
lon: longitude,
|
||||
tz: place.timezoneOffset,
|
||||
events,
|
||||
})
|
||||
}))
|
||||
: null;
|
||||
const selectedRange = !input.preserveCandidateRange && eventScore?.winningSegment
|
||||
? { startTime: eventScore.winningSegment.startTime, endTime: eventScore.winningSegment.endTime }
|
||||
@@ -488,25 +505,28 @@ export async function buildProductionConversationalRectificationPacket(
|
||||
const questionnaires: RectificationQuestionnaire[] = [];
|
||||
for (const scanRange of boundedScanRanges(selectedRange)) {
|
||||
const scanPoint = scanCoordinates(scanRange);
|
||||
const { questionnaire } = await engine.scan({
|
||||
const { questionnaire } = await rectificationPacketStage("scan", () => engine.scan({
|
||||
birthTime: `${input.declaredBirthInput.birthDate} ${scanPoint.centerTime}`,
|
||||
uncertaintyMinutes: scanPoint.uncertaintyMinutes,
|
||||
lat: place.latitude,
|
||||
lon: place.longitude,
|
||||
lat: latitude,
|
||||
lon: longitude,
|
||||
tz: place.timezoneOffset,
|
||||
ayanamsa: "lahiri",
|
||||
});
|
||||
}));
|
||||
questionnaires.push(questionnaire);
|
||||
}
|
||||
const questionnaire = mergeQuestionnaireScans(questionnaires, selectedRange);
|
||||
const candidateDifferences = await engine.buildDifferencePacket({
|
||||
const questionnaire = await rectificationPacketStage(
|
||||
"merge_scans",
|
||||
() => mergeQuestionnaireScans(questionnaires, selectedRange),
|
||||
);
|
||||
const candidateDifferences = await rectificationPacketStage("candidate_differences", () => engine.buildDifferencePacket({
|
||||
caseId: input.caseId,
|
||||
asOfDate: input.asOfDate,
|
||||
birthDate: input.declaredBirthInput.birthDate,
|
||||
startTime: selectedRange.startTime,
|
||||
endTime: selectedRange.endTime,
|
||||
lat: place.latitude,
|
||||
lon: place.longitude,
|
||||
lat: latitude,
|
||||
lon: longitude,
|
||||
tz: place.timezoneOffset,
|
||||
evidence: [],
|
||||
events,
|
||||
@@ -515,18 +535,22 @@ export async function buildProductionConversationalRectificationPacket(
|
||||
partitionFingerprints: [],
|
||||
recentRanges: [],
|
||||
candidateModel: null,
|
||||
});
|
||||
}));
|
||||
const calculationVersion = eventScore
|
||||
? `${candidateDifferences.packet.scoringVersion}+${eventScore.algorithmVersion}`
|
||||
: candidateDifferences.packet.scoringVersion;
|
||||
const metadata = layerMetadata(questionnaire, calculationVersion);
|
||||
const timeLinkedScanSamples = await rectificationPacketStage(
|
||||
"time_links",
|
||||
() => sampleTimes(questionnaire),
|
||||
);
|
||||
const representative = eventScore?.winningSegment?.representativeTime
|
||||
?? scanCoordinates(selectedRange).centerTime;
|
||||
const { buildRectificationTechnicalPacket } = await import(
|
||||
"../../../lib/conversational-rectification/technical-packet.ts"
|
||||
);
|
||||
return {
|
||||
packet: buildRectificationTechnicalPacket({
|
||||
packet: await rectificationPacketStage("technical_packet", () => buildRectificationTechnicalPacket({
|
||||
scan: questionnaire,
|
||||
candidateDifferences,
|
||||
eventScore: input.preserveCandidateRange && eventScore
|
||||
@@ -537,11 +561,11 @@ export async function buildProductionConversationalRectificationPacket(
|
||||
calculationVersion,
|
||||
availableLayers: metadata.availableLayers,
|
||||
layerReferences: metadata.layerReferences,
|
||||
timeLinkedScanSamples: sampleTimes(questionnaire),
|
||||
timeLinkedScanSamples,
|
||||
boundaryDistanceMinutes: boundaryDistance(selectedRange, representative),
|
||||
futureWindows: [],
|
||||
},
|
||||
}),
|
||||
})),
|
||||
resultId: eventScore?.resultId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -459,8 +459,17 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.conversational-domain-picker { display: grid; gap: var(--space-3); margin: 0; padding: 0; border: 0; }
|
||||
.conversational-domain-picker .conversational-domain-question { margin-bottom: var(--space-2); color: var(--color-ink); font-size: var(--type-body-md); font-weight: 600; line-height: 1.5; }
|
||||
.conversational-domain-picker > div { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--space-2); }
|
||||
.conversational-domain-picker button { padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; text-align: left; }
|
||||
.conversational-domain-picker button { display: grid; gap: var(--space-1); padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; text-align: left; }
|
||||
.conversational-domain-picker button > span { font-size: var(--type-body-sm); font-weight: 600; }
|
||||
.conversational-domain-picker button > small { color: var(--color-ink-secondary); font-size: var(--type-overline); font-weight: 400; }
|
||||
.conversational-domain-picker button[aria-pressed="true"] { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-action); }
|
||||
.conversational-domain-picker button[aria-label$="下一步建议"] { border-color: color-mix(in srgb, var(--color-action) 52%, var(--color-border)); background: var(--color-action-soft); }
|
||||
.conversational-answer-pending { min-width: 0; display: grid; grid-template-columns: 40px minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid color-mix(in srgb, var(--color-action) 24%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-action-soft); }
|
||||
.conversational-answer-pending .app-loading-symbol { width: 40px; height: 40px; }
|
||||
.conversational-answer-pending .app-loading-mark { width: 22px; height: 22px; }
|
||||
.conversational-answer-pending > div:last-child { min-width: 0; display: grid; gap: var(--space-1); }
|
||||
.conversational-answer-pending strong { color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; }
|
||||
.conversational-answer-pending span { color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.5; }
|
||||
.conversational-composer { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); }
|
||||
.conversational-composer label { display: grid; gap: var(--space-1); font-size: var(--type-body-sm); font-weight: 600; }
|
||||
.conversational-composer label span { color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 400; line-height: 1.5; }
|
||||
@@ -486,6 +495,9 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.conversational-candidate dt { color: var(--color-ink-tertiary); font-size: var(--type-overline); }
|
||||
.conversational-candidate dd { min-width: 0; margin: 0; color: var(--color-ink); font-size: var(--type-body-sm); overflow-wrap: anywhere; }
|
||||
.conversational-candidate time { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
||||
.conversational-candidate-progress { min-width: 0; display: grid; gap: var(--space-1); padding-top: var(--space-3); border-top: 1px solid var(--color-border); }
|
||||
.conversational-candidate-progress strong { color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; }
|
||||
.conversational-candidate-progress span { color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.55; text-wrap: pretty; }
|
||||
.conversational-confirmation { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); background: var(--color-action-soft); }
|
||||
.conversational-status { min-width: 0; min-height: 1px; }
|
||||
.conversational-original-question { background: var(--color-success-muted); }
|
||||
|
||||
@@ -1953,12 +1953,16 @@ export default function Home() {
|
||||
&& session.rectificationCaseId === null)
|
||||
?? null
|
||||
: null;
|
||||
const rectificationSession = sourceSession.sessionType === "birth_time_rectification"
|
||||
const canReuseSourceRectificationSession = action === "resume"
|
||||
&& sourceSession.sessionType === "birth_time_rectification"
|
||||
&& account.rectificationCase !== null
|
||||
&& (sourceSession.rectificationCaseId === account.rectificationCase.caseId
|
||||
|| sourceSession.rectificationCaseId === null);
|
||||
const rectificationSession = canReuseSourceRectificationSession
|
||||
? sourceSession
|
||||
: resumableSession ?? createSession(modelCatalog.defaultModelId, "birth_time_rectification");
|
||||
const reusingRectificationSession = rectificationSession !== sourceSession
|
||||
? resumableSession !== null
|
||||
: sourceSession.sessionType === "birth_time_rectification";
|
||||
const reusingRectificationSession = canReuseSourceRectificationSession
|
||||
|| resumableSession !== null;
|
||||
const requestedQuestion = pendingConsultationQuestion
|
||||
?? (reusingRectificationSession
|
||||
? null
|
||||
|
||||
@@ -26,6 +26,42 @@ const domainLabels = {
|
||||
other: "其他关键经历",
|
||||
} as const satisfies Readonly<Record<EvidenceDomain, string>>;
|
||||
|
||||
function orderedEvidenceDomains(turn: ConversationalRectificationTurn): EvidenceDomain[] {
|
||||
const provided = new Set(turn.evidenceRecap.flatMap((item) => item.domain ? [item.domain] : []));
|
||||
return [...(turn.evidenceRequest?.domains ?? [])].sort((left, right) =>
|
||||
Number(provided.has(left)) - Number(provided.has(right)));
|
||||
}
|
||||
|
||||
function visibleTurnNarrative(turn: ConversationalRectificationTurn): string {
|
||||
if (
|
||||
turn.status !== "active"
|
||||
|| turn.evidenceRecap.length === 0
|
||||
|| /(?:已记录|已修订):/.test(turn.narrative)
|
||||
) {
|
||||
return turn.narrative;
|
||||
}
|
||||
|
||||
const latestEvidence = turn.evidenceRecap.at(-1)!;
|
||||
const candidate = turn.candidate;
|
||||
const candidateRange = candidate.rangeStart && candidate.rangeEnd
|
||||
? `${candidate.rangeStart}–${candidate.rangeEnd}`
|
||||
: candidate.representativeTime ?? "当前候选范围";
|
||||
const nextDomains = orderedEvidenceDomains(turn)
|
||||
.slice(0, 2)
|
||||
.map((domain) => domainLabels[domain]);
|
||||
const nextStep = candidate.status === "ready_for_confirmation"
|
||||
? "当前证据已形成候选总结;请先核对上方候选时间,再决定是否确认或继续补充经历。"
|
||||
: nextDomains.length > 0
|
||||
? `下一步:请优先补充一件${nextDomains.join("或")}领域已经发生的事件,并选择发生年月。`
|
||||
: "下一步:请继续补充一件已经发生并带有年月的真实经历。";
|
||||
|
||||
return [
|
||||
`${latestEvidence.isCorrection ? "已修订" : "已记录"}:${latestEvidence.dateLabel} · ${latestEvidence.summary}。`,
|
||||
`候选范围当前为 ${candidateRange};范围暂未变化不代表提交失败,系统会结合后续经历继续比较相邻分钟。`,
|
||||
nextStep,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
type SurfaceProps = Readonly<{
|
||||
controller: ConversationalRectificationController;
|
||||
pendingConsultationQuestion?: string | null;
|
||||
@@ -47,6 +83,10 @@ function CandidateSummary({ turn }: { readonly turn: ConversationalRectification
|
||||
: candidate.status === "ready_for_confirmation"
|
||||
? "待确认 · 未验证"
|
||||
: "待验证 · 未确认";
|
||||
const recordedCount = turn.evidenceRecap.length;
|
||||
const nextDomains = orderedEvidenceDomains(turn)
|
||||
.slice(0, 2)
|
||||
.map((domain) => domainLabels[domain]);
|
||||
|
||||
return (
|
||||
<section className="conversational-candidate" aria-labelledby="conversational-candidate-title">
|
||||
@@ -69,6 +109,15 @@ function CandidateSummary({ turn }: { readonly turn: ConversationalRectification
|
||||
</dl>
|
||||
) : <p>尚未形成可供确认的具体时间。</p>}
|
||||
{!confirmed && <p>候选仍待真实经历验证,未经你的明确确认不会成为当前排盘时间。</p>}
|
||||
{!confirmed && recordedCount > 0 && turn.status !== "completed" && (
|
||||
<div className="conversational-candidate-progress" role="status">
|
||||
<strong>已记录 {recordedCount} 条经历</strong>
|
||||
<span>系统至少需要 3 条时间明确、可评分的经历后才会可靠缩小范围;范围不变不代表提交失败。</span>
|
||||
{nextDomains.length > 0 && (
|
||||
<span>下一步优先补充:{nextDomains.join("或")}领域已经发生的事件。</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -98,7 +147,13 @@ export function ConversationalRectificationSurface({
|
||||
}
|
||||
|
||||
const canAnswer = turn.actions.includes("answer") && turn.status !== "abandoned" && turn.status !== "completed";
|
||||
const requestedDomains = turn.evidenceRequest?.domains ?? [];
|
||||
const requestedDomains = orderedEvidenceDomains(turn);
|
||||
const providedDomains = new Set(
|
||||
turn.evidenceRecap.flatMap((item) => item.domain ? [item.domain] : []),
|
||||
);
|
||||
const priorityDomain = requestedDomains.find((domain) => !providedDomains.has(domain))
|
||||
?? requestedDomains[0]
|
||||
?? null;
|
||||
const submit = async () => {
|
||||
if (!canAnswer || !controller.draft.trim() || controller.pending) return;
|
||||
const dateLabel = eventYear
|
||||
@@ -125,7 +180,7 @@ export function ConversationalRectificationSurface({
|
||||
|
||||
<article className="conversational-narrative" aria-label="校正分析">
|
||||
<div className="conversational-narrative-body">
|
||||
<ChatMessageContent text={turn.narrative} />
|
||||
<ChatMessageContent text={visibleTurnNarrative(turn)} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -135,6 +190,9 @@ export function ConversationalRectificationSurface({
|
||||
<div>
|
||||
{requestedDomains.map((domain) => (
|
||||
<button
|
||||
aria-label={`${domainLabels[domain]},${domain === priorityDomain
|
||||
? "下一步建议"
|
||||
: providedDomains.has(domain) ? "已提供,可继续补充" : "可补充"}`}
|
||||
aria-pressed={controller.selectedDomain === domain}
|
||||
data-evidence-domain={domain}
|
||||
disabled={controller.pending || !canAnswer}
|
||||
@@ -145,13 +203,35 @@ export function ConversationalRectificationSurface({
|
||||
focusComposer();
|
||||
}}
|
||||
>
|
||||
{domainLabels[domain]}
|
||||
<span>{domainLabels[domain]}</span>
|
||||
<small>{domain === priorityDomain
|
||||
? "下一步建议"
|
||||
: providedDomains.has(domain) ? "已提供,可继续补充" : "可补充"}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{controller.pending && canAnswer && (
|
||||
<div
|
||||
aria-atomic="true"
|
||||
aria-label="正在核对经历"
|
||||
aria-live="polite"
|
||||
className="conversational-answer-pending"
|
||||
role="status"
|
||||
>
|
||||
<div className="app-loading-symbol" aria-hidden="true">
|
||||
<span className="app-loading-orbit" />
|
||||
<span className="app-loading-mark" />
|
||||
</div>
|
||||
<div>
|
||||
<strong>正在核对这段经历</strong>
|
||||
<span>正在比较相邻候选分钟;完成后会继续提问或更新候选范围。</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canAnswer && <form
|
||||
className="conversational-composer"
|
||||
onSubmit={(event) => {
|
||||
|
||||
@@ -83,6 +83,8 @@ const evidenceRecapEntrySchema = boundedJson(z.object({
|
||||
id: z.string().uuid(),
|
||||
summary: boundedNonblankText(1_000),
|
||||
dateLabel: boundedNonblankText(80),
|
||||
// Optional so turns written before domain-aware follow-up ordering still resume safely.
|
||||
domain: evidenceDomainSchema.optional(),
|
||||
// Optional so turns written before correction lineage was introduced still resume safely.
|
||||
isCorrection: z.boolean().optional(),
|
||||
}).strict(), 4_096);
|
||||
|
||||
@@ -69,6 +69,7 @@ function eventSummary(fragment: string): string {
|
||||
const withoutDates = fragment
|
||||
.replace(chineseDatePattern, "")
|
||||
.replace(isoDatePattern, "")
|
||||
.replace(/(?:发生时间|事件详情)\s*[::]\s*/g, "")
|
||||
.replace(/^\s*(?:更正|纠正|修正)\s*[::]?\s*/, "")
|
||||
.replace(leadingRelativeTimePattern, "")
|
||||
.replace(/^\s*(?:同时|又)\s*/, "")
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "./technical-packet.ts";
|
||||
import {
|
||||
convergenceNotes,
|
||||
MINIMUM_SCOREABLE_EVENTS,
|
||||
nextPlateauCount,
|
||||
rangeCompletionCopy,
|
||||
rangeCompletionReason,
|
||||
@@ -182,10 +183,27 @@ function commandFingerprint(command: MutableCommand): string {
|
||||
return createHash("sha256").update(JSON.stringify(identity), "utf8").digest("hex");
|
||||
}
|
||||
|
||||
function visibleEvidenceSummary(value: string): string {
|
||||
const cleaned = value.replace(/(?:发生时间|事件详情)\s*[::]\s*/g, "").trim();
|
||||
return cleaned || value;
|
||||
}
|
||||
|
||||
function publicTurn(value: StoredConversationalRectificationCase): ConversationalRectificationTurn {
|
||||
const parsed = conversationalRectificationTurnSchema.safeParse(value.latestTurn);
|
||||
if (!parsed.success) throw new ConversationalRectificationError("store_unavailable");
|
||||
return parsed.data;
|
||||
const evidenceDomains = new Map(
|
||||
effectiveLifeEventEvidence(value.eventEvidence ?? []).map((item) => [item.id, item.domain]),
|
||||
);
|
||||
return {
|
||||
...parsed.data,
|
||||
evidenceRecap: parsed.data.evidenceRecap.map((item) => ({
|
||||
...item,
|
||||
summary: visibleEvidenceSummary(item.summary),
|
||||
...(item.domain ? {} : evidenceDomains.get(item.id)
|
||||
? { domain: evidenceDomains.get(item.id) }
|
||||
: {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function transitionReceipt(modelId = "deterministic-rectification-transition"): ValidationReceipt {
|
||||
@@ -222,16 +240,65 @@ export function effectiveLifeEventEvidence<
|
||||
function evidenceRecap(evidence: ReadonlyArray<LifeEventEvidenceInput>) {
|
||||
return effectiveLifeEventEvidence(evidence).slice(-20).map((item) => ({
|
||||
id: item.id,
|
||||
summary: item.eventSummary,
|
||||
summary: visibleEvidenceSummary(item.eventSummary),
|
||||
dateLabel: item.dateValue
|
||||
? item.scoreable === false && item.extractionStatus !== "needs_clarification"
|
||||
? `${item.dateValue}(未来,仅作背景)`
|
||||
: item.dateValue
|
||||
: "日期待补充",
|
||||
domain: item.domain,
|
||||
...((item.correctsEvidenceIds?.length ?? 0) > 0 ? { isCorrection: true } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
const progressDomainLabels = {
|
||||
career: "事业",
|
||||
education: "学业",
|
||||
finance: "财务",
|
||||
relocation: "搬迁",
|
||||
relationship: "重要关系",
|
||||
family: "家庭",
|
||||
other: "其他关键经历",
|
||||
} as const satisfies Readonly<Record<RectificationEvidenceDomain, string>>;
|
||||
|
||||
function evidenceProgressNarrative(input: Readonly<{
|
||||
previousCandidate: PrivateCandidateInput;
|
||||
packet: RectificationTechnicalPacket;
|
||||
newEvidence: ReadonlyArray<LifeEventEvidenceInput>;
|
||||
scoreableEventCount: number;
|
||||
willContinue: boolean;
|
||||
}>): string {
|
||||
const recorded = evidenceRecap(input.newEvidence);
|
||||
const acknowledgement = recorded.length === 0
|
||||
? "这段经历已经保存。"
|
||||
: `已记录:${recorded.map((item) => `${item.dateLabel} · ${item.summary}`).join(";")}。`;
|
||||
const previousStart = input.previousCandidate.rangeStart;
|
||||
const previousEnd = input.previousCandidate.rangeEnd;
|
||||
const nextStart = input.packet.candidate.range.startTime;
|
||||
const nextEnd = input.packet.candidate.range.endTime;
|
||||
const rangeChanged = previousStart !== nextStart || previousEnd !== nextEnd;
|
||||
const progress = input.scoreableEventCount < MINIMUM_SCOREABLE_EVENTS
|
||||
? `当前累计 ${input.scoreableEventCount} 条可评分经历;系统至少需要 ${MINIMUM_SCOREABLE_EVENTS} 条时间明确的经历才开始事件排序,所以本轮候选范围暂时保持 ${nextStart}–${nextEnd}。`
|
||||
: rangeChanged
|
||||
? `候选范围已从 ${previousStart ?? "原范围"}–${previousEnd ?? "原范围"} 更新为 ${nextStart}–${nextEnd}。`
|
||||
: `本轮已纳入 ${input.scoreableEventCount} 条可评分经历,但候选范围暂未稳定缩小;这不是提交失败。`;
|
||||
const suggested = input.packet.suggestedDomains
|
||||
.slice(0, 2)
|
||||
.map((item) => progressDomainLabels[item.domain]);
|
||||
const nextStep = input.willContinue && suggested.length > 0
|
||||
? `下一步:请优先补充一件${suggested.join("或")}领域已经发生的事件,并选择大致年月。`
|
||||
: "";
|
||||
const differenceBasis = input.packet.suggestedDomains.length > 0
|
||||
? `本轮区分重点:${input.packet.suggestedDomains.slice(0, 2)
|
||||
.map((item) => `${progressDomainLabels[item.domain]}事件用于比较 ${item.layer}`)
|
||||
.join(";")}。`
|
||||
: "";
|
||||
return [acknowledgement, progress, nextStep, differenceBasis]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.slice(0, 12_000);
|
||||
}
|
||||
|
||||
function exactTechnicalReceipt(packet: RectificationTechnicalPacket) {
|
||||
const projected = projectRectificationTechnicalPacket(packet);
|
||||
return {
|
||||
@@ -1060,6 +1127,17 @@ export function createConversationalRectificationService(
|
||||
scoreableEventCount: allScoreable.length,
|
||||
plateauCount,
|
||||
});
|
||||
const narrativeWithProgress = {
|
||||
...narrative,
|
||||
narrative: evidenceProgressNarrative({
|
||||
previousCandidate: current.privateCandidate,
|
||||
packet: computed.packet,
|
||||
newEvidence: evidence,
|
||||
scoreableEventCount: allScoreable.length,
|
||||
willContinue: completionReason === null
|
||||
&& computed.packet.candidate.status !== "ready_for_confirmation",
|
||||
}),
|
||||
} satisfies RectificationNarrativeResult;
|
||||
const privateCandidate = privateCandidateFromPacket({
|
||||
packet: computed.packet,
|
||||
resultId: computed.resultId,
|
||||
@@ -1071,7 +1149,7 @@ export function createConversationalRectificationService(
|
||||
turnVersion: command.turnVersion + 1,
|
||||
pendingConsultationQuestion: current.pendingConsultationQuestion,
|
||||
packet: computed.packet,
|
||||
narrative,
|
||||
narrative: narrativeWithProgress,
|
||||
evidence: [...current.eventEvidence, ...evidence],
|
||||
});
|
||||
const turn = completionReason
|
||||
|
||||
Reference in New Issue
Block a user