Files
Agent-System/web/src/lib/postTaskDeliverableSync.ts
T
weichenandCursor 7466a15319 fix(docgov): harden UI-03 sync poll, reconnectRunId, and E2E status attrs
Use deliverablesRef + limited poll for post-task server docs; pass
reconnectRunId across auto-retry; GetRun reads idempotency_key;
StartClaimedRun fails closed on UpdateRun; MessageBubble/E2E scope
this-turn assistant; correct audit retry/schema19 wording.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 13:54:49 +00:00

128 lines
4.1 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.
import type { DeliverableItem } from '../types/workbench';
import type { WorkflowDocumentSummary } from '../api/projectDocuments';
/**
* 任务完成后:在已 merge 的 deliverables 中定位同 taskKey 的服务端受控文档槽,
* 用于把当前模板页切换到 doc_proj*(toolbar V/r 可挂载)。
*/
export function resolvePostTaskActiveDocId(params: {
deliverables: DeliverableItem[];
projectId: number;
taskKey: string;
preferredDocId?: string | null;
}): string | null {
const { deliverables, projectId, taskKey, preferredDocId } = params;
if (!taskKey || projectId <= 0) return null;
if (preferredDocId) {
const preferred = deliverables.find(
(d) =>
d.docId === preferredDocId &&
d.projectId === projectId &&
d.taskKey === taskKey &&
d.governanceMode !== 'freeform' &&
!d.isTemplateOnly,
);
if (preferred) return preferred.docId;
}
const serverSlot = deliverables.find(
(d) =>
d.projectId === projectId &&
d.taskKey === taskKey &&
d.governanceMode !== 'freeform' &&
d.isFixedWorkflow !== false &&
!d.isTemplateOnly &&
/^doc_proj\d+_/.test(d.docId),
);
if (serverSlot) return serverSlot.docId;
const anyNonTemplate = deliverables.find(
(d) =>
d.projectId === projectId &&
d.taskKey === taskKey &&
d.governanceMode !== 'freeform' &&
!d.isTemplateOnly,
);
return anyNonTemplate?.docId ?? null;
}
/** 若当前 active 仍是同 taskKey 的模板槽,则应切换到 serverDocId。 */
export function shouldSwitchTemplateSlotToServer(params: {
activeDocId: string | null | undefined;
deliverables: DeliverableItem[];
projectId: number;
taskKey: string;
serverDocId: string;
}): boolean {
const { activeDocId, deliverables, projectId, taskKey, serverDocId } = params;
if (!serverDocId || !taskKey) return false;
if (!activeDocId) return true;
if (activeDocId === serverDocId) return false;
const active = deliverables.find((d) => d.docId === activeDocId);
if (!active) return true;
if (active.projectId !== projectId || active.taskKey !== taskKey) return false;
return Boolean(active.isTemplateOnly) || active.docId.startsWith('doc-p');
}
export type PollUntilPostTaskServerDocParams = {
listDocs: () => Promise<WorkflowDocumentSummary[]>;
/** 每次 attempt 读取当前本地快照(应用侧用 ref.current,禁止依赖 setState updater 回写)。 */
getDeliverables: () => DeliverableItem[];
merge: (prev: DeliverableItem[], docs: WorkflowDocumentSummary[]) => DeliverableItem[];
projectId: number;
taskKey: string;
preferredDocId?: string | null;
maxAttempts?: number;
delayMs?: number;
sleep?: (ms: number) => Promise<void>;
/** 每轮 merge 后回调(应用侧同步写 ref + setDeliverables(merged))。 */
onMerged?: (merged: DeliverableItem[]) => void;
};
/**
* Finalize 后文档可能尚未立即可见:有限重试 list+merge,直到同 taskKey 出现非模板 server doc。
* 返回最后一次 merged 快照与解析到的 serverDocId(未出现则为 null)。
*/
export async function pollUntilPostTaskServerDoc(
params: PollUntilPostTaskServerDocParams,
): Promise<{ merged: DeliverableItem[]; serverDocId: string | null; attempts: number }> {
const {
listDocs,
getDeliverables,
merge,
projectId,
taskKey,
preferredDocId,
maxAttempts = 10,
delayMs = 800,
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
onMerged,
} = params;
let merged = getDeliverables();
let serverDocId: string | null = null;
let attempts = 0;
for (let i = 0; i < maxAttempts; i++) {
attempts = i + 1;
const docs = await listDocs();
merged = merge(getDeliverables(), docs);
onMerged?.(merged);
serverDocId = resolvePostTaskActiveDocId({
deliverables: merged,
projectId,
taskKey,
preferredDocId,
});
if (serverDocId) {
return { merged, serverDocId, attempts };
}
if (i + 1 < maxAttempts) {
await sleep(delayMs);
}
}
return { merged, serverDocId: null, attempts };
}