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>
128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
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 };
|
||
}
|