Recover verified applied receipts against a fresh canonical base

This commit is contained in:
2026-11-08 02:09:36 +08:00
parent 89b6f4a253
commit f9cb1ae120
13 changed files with 293 additions and 32 deletions
@@ -10,9 +10,12 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle } f
function message(error:unknown){
const code=(error as {code?:string;message?:string})?.code??(error as Error)?.message;
if(code==='recovery_waiting_for_receipts')return '还有请求正在确认。请关闭此窗口,重试同步后再处理。';
if(code==='recovery_read_behind_receipt')return '服务端读取尚未包含已确认的修改,请稍后重新检查。';
if(code?.endsWith('receipt_interpreter_unavailable')||code==='recovery_receipt_missing')return '暂时无法验证原成功回执,修改和原请求仍已保留。请稍后重试。';
if(code==='recovery_in_progress')return '上一次处理尚未结束。可先恢复编辑,再重新检查。';
if(code==='recovery_choices_incomplete')return '请为每一项修改选择处理方式。';
if(code==='stale_incarnation')return '服务端已删除或替换了这个元素。可以放弃这项本地修改。';
if(code==='operation_context_changed')return '元素所属分区已改变,请重新预览这项修改。';
if(code==='ownership_conflict')return '元素已移入其它分区,当前坐标需要重新确认,暂时无法直接保留。';
if(code?.startsWith('recovery_history')||code==='recovery_structure_not_supported')return '这批修改含结构或撤销操作,暂时无法自动处理。原内容和历史均已保留。';
if(code==='recovery_action_requires_new_command')return '生成任务需要重新手动发起。可以在这里放弃尚未执行的请求。';
@@ -31,7 +34,7 @@ export function HeadlessRecoveryDialog({engine,close}:{engine:ProjectEngine;clos
const [choices,setChoices]=useState(new Map<Id,RecoveryChoice>()),[plan,setPlan]=useState<ConflictPlan>(),[error,setError]=useState(''),[busy,setBusy]=useState(true),[frozen,setFrozen]=useState(false);
useEffect(()=>{
let live=true;
Promise.resolve().then(()=>live?engine.prepareRecovery():undefined).then(async session=>{if(!session)return;if(!live){await session.cancel();return;}owner.current=session;setSession(session);setRecords([...engine.getSnapshot().runtime.ledger.operations.values()].filter(record=>record.phase!=='resolved'&&record.intent.operationType!=='asset.upload').sort((a,b)=>a.intent.sequence-b.intent.sequence).map(record=>record.intent));}).catch(error=>{if(live)setError(message(error));}).finally(()=>{if(live)setBusy(false);});
Promise.resolve().then(()=>live?engine.prepareRecovery():undefined).then(async session=>{if(!session)return;if(!live){await session.cancel();return;}owner.current=session;setSession(session);setRecords([...engine.getSnapshot().runtime.ledger.operations.values()].filter(record=>session.pendingOperationIds.includes(record.intent.id)&&record.intent.operationType!=='asset.upload').sort((a,b)=>a.intent.sequence-b.intent.sequence).map(record=>record.intent));}).catch(error=>{if(live)setError(message(error));}).finally(()=>{if(live)setBusy(false);});
return()=>{live=false;void owner.current?.cancel().catch(()=>{/* Engine retains the failed durable barrier; retry/cancelRecovery can finish it. */});};
},[engine]);
async function cancel(){if(busy&&session)return;setBusy(true);try{await engine.cancelRecovery();close();}catch(error){setError(message(error));}finally{setBusy(false);}}
@@ -41,6 +44,7 @@ export function HeadlessRecoveryDialog({engine,close}:{engine:ProjectEngine;clos
<DialogTitle>处理同步修改</DialogTitle>
<DialogDescription>检查期间暂停编辑。逐项选择要保留的修改,预览后再应用;原内容和撤销记录会保留备份。</DialogDescription>
{busy&&!session&&<p role="status">正在等待同步并读取最新内容…</p>}
{!!session?.acknowledgedRequestIds.length&&<p>已找回 {session.acknowledgedRequestIds.length} 个成功回执,仅处理后续尚未同步的修改。</p>}
{session&&records.length===0&&<p>没有待处理的本地修改,可以采用服务端最新内容。</p>}
{session&&records.map((intent,index)=><article key={intent.id} style={{borderBottom:'1px solid #ddd5ca',paddingBottom:12}}>
<strong>修改 {index+1}{intent.operationType==='run.start'?' · 尚未受理的生成':intent.operationType==='run.cancel'?' · 尚未确认的取消':''}</strong>