/** * **对话区按任务内序号排,不按墙上时钟**(2026-08-24 柴浩:「上一个步骤的消息跑到这一个 * 步骤的后面」)。 * * 对话流 = 时间线(AI 发言 / 系统事件,来自任务记忆)+ 对话卡族(录入卡 / 决策卡), * 从前合并时按 `at` 字符串排序。同一台后端上它们出自同一只时钟,没问题; * 可两台后端连同一个库交替推同一张单时(本机 + 局域网另一台,当天实测的部署形态), * 各自的时钟差上几秒,上一拍的话就排到了下一拍的后面 —— 「有时候乱」正是这个形状。 * * 任务记忆的 `seq` 是任务内单调的(取 max+1,与时钟无关):每条对话项都能找到它在记忆里 * 的落点 —— AI/系统项本身就是记忆条目;卡对应 `card-sent` 那条(detail.id); * 人的自由输入对应 `human-input` 那条(detail.id)。找不到落点的(老数据)退回按 * 「它的时刻之前最后一条记忆的序号」插位 —— 仍是一个总序,只是精度回到时钟。 */ export type OrderEntry = { readonly id: string; readonly seq: number; readonly kind: string; readonly at: string; readonly detail?: Readonly> | undefined; }; export function orderChat( items: readonly T[], history: readonly OrderEntry[], ): T[] { const byEntryId = new Map(); const byLinkedId = new Map(); for (const e of history) { byEntryId.set(e.id, e.seq); const linked = e.detail?.['id']; if (typeof linked === 'string' && (e.kind === 'card-sent' || e.kind === 'human-input')) { if (!byLinkedId.has(linked)) byLinkedId.set(linked, e.seq); } } const sortedByAt = [...history].sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0)); const seqBefore = (at: string): number => { let seq = 0; for (const e of sortedByAt) { if (e.at <= at) seq = Math.max(seq, e.seq); else break; } return seq; }; const keyed = items.map((item, index) => ({ item, index, seq: byEntryId.get(item.id) ?? byLinkedId.get(item.id) ?? seqBefore(item.at), })); keyed.sort((a, b) => a.seq - b.seq || (a.item.at < b.item.at ? -1 : a.item.at > b.item.at ? 1 : 0) || a.index - b.index); return keyed.map((k) => k.item); }