import { fetchControlledDocumentContent } from '../api/projectDocuments'; import { getRevisionState } from '../api/documentRevision'; import { markdownToBlockAST } from '../engine/markdownToBlock'; import type { DocumentAST } from '../types/block'; import type { DeliverableItem } from '../types/workbench'; import { revisionEtagFromHydrate } from './revisionEtag'; export function isControlledWorkflowDeliverable(doc: DeliverableItem): boolean { return ( doc.isFixedWorkflow !== false && doc.governanceMode !== 'freeform' && !!doc.taskKey && doc.taskKey !== 'freeform_doc' ); } /** 服务端已有正文、应拉取 output.md 而非展示本地模板 */ export function shouldLoadControlledContent(doc: DeliverableItem): boolean { return isControlledWorkflowDeliverable(doc) && !doc.isTemplateOnly; } export function buildControlledLoadErrorSkeleton(doc: DeliverableItem): DocumentAST { const skeleton = `# ${doc.title}\n\n> 加载失败,请点击重试或稍后再试。\n`; const ast = markdownToBlockAST(skeleton, { docId: doc.docId, title: doc.title, taskKey: doc.taskKey, version: doc.stage, category: doc.category, creatorId: doc.creatorId, creatorName: doc.creatorName, lifecycleState: doc.lifecycleState, isClaimed: doc.isClaimed, author: doc.author, authorId: doc.authorId, reviewer: doc.reviewer, reviewerId: doc.reviewerId, approver: doc.approver, approverId: doc.approverId, dutiesAuthoritative: doc.dutiesAuthoritative ?? true, }); ast.governanceMode = 'controlled'; ast.editLease = doc.editLease; return ast; } export function buildControlledLoadingSkeleton(doc: DeliverableItem): DocumentAST { const skeleton = `# ${doc.title}\n\n> 正在从服务端加载文档正文…\n`; const ast = markdownToBlockAST(skeleton, { docId: doc.docId, title: doc.title, taskKey: doc.taskKey, version: doc.stage, category: doc.category, creatorId: doc.creatorId, creatorName: doc.creatorName, lifecycleState: doc.lifecycleState, isClaimed: doc.isClaimed, author: doc.author, authorId: doc.authorId, reviewer: doc.reviewer, reviewerId: doc.reviewerId, approver: doc.approver, approverId: doc.approverId, dutiesAuthoritative: doc.dutiesAuthoritative ?? true, }); ast.governanceMode = 'controlled'; ast.editLease = doc.editLease; return ast; } export function buildControlledDocumentAST( markdown: string, doc: DeliverableItem, docId: string, version?: number, ): DocumentAST { const fullAST = markdownToBlockAST(markdown, { docId, title: doc.title, taskKey: doc.taskKey, version: `${version ?? doc.stage}`, category: doc.category, creatorId: doc.creatorId, creatorName: doc.creatorName, lifecycleState: doc.lifecycleState, isClaimed: doc.isClaimed, author: doc.author, authorId: doc.authorId, reviewer: doc.reviewer, reviewerId: doc.reviewerId, approver: doc.approver, approverId: doc.approverId, dutiesAuthoritative: doc.dutiesAuthoritative ?? true, }); fullAST.governanceMode = 'controlled'; fullAST.editLease = doc.editLease; fullAST.dutiesAuthoritative = doc.dutiesAuthoritative ?? true; return fullAST; } export async function hydrateControlledDocumentContent( docId: string, doc: DeliverableItem, deps?: { fetchContent?: typeof fetchControlledDocumentContent; fetchRevisionState?: (id: string) => Promise<{ etag: string }>; }, ): Promise { const loadContent = deps?.fetchContent ?? fetchControlledDocumentContent; const loadRevision = deps?.fetchRevisionState ?? getRevisionState; const [detail, rev] = await Promise.all([ loadContent(docId), loadRevision(docId).then( (st) => ({ ok: true as const, st }), (err: unknown) => ({ ok: false as const, err }), ), ]); if (!detail?.markdown) return null; if (!rev.ok) { throw rev.err instanceof Error ? rev.err : new Error('revision-state hydrate failed'); } const etag = revisionEtagFromHydrate(rev.st); if (!etag) { throw new Error('受控文档打开时未取得修订基线,请重试'); } const ast = buildControlledDocumentAST(detail.markdown, doc, detail.doc_id, detail.version); ast.revisionEtag = etag; ast.dutiesAuthoritative = doc.dutiesAuthoritative ?? true; return ast; } const LEGACY_DOC_P = /^doc-p(\d+)-(.+)$/; const LEGACY_DOC_PROJ = /^doc-proj(\d+)-(.+)$/; function isLegacyLocalDocId(docId: string): boolean { return LEGACY_DOC_P.test(docId) || LEGACY_DOC_PROJ.test(docId); } function legacyDocIdToCanonical(docId: string): string | null { const legacyP = docId.match(LEGACY_DOC_P); if (legacyP) return `doc_proj${legacyP[1]}_${legacyP[2]}`; const legacyProj = docId.match(LEGACY_DOC_PROJ); if (legacyProj) return `doc_proj${legacyProj[1]}_${legacyProj[2]}`; return null; } /** 将 legacy doc-p{projectId}-{taskKey} 映射到服务端 doc_id */ export function resolveCanonicalDocId( docId: string, deliverables: DeliverableItem[], ): string { const direct = deliverables.find((d) => d.docId === docId); if (direct) return docId; const legacyCanonical = legacyDocIdToCanonical(docId); if (!legacyCanonical) return docId; const projectId = Number(legacyCanonical.match(/^doc_proj(\d+)_/)?.[1] ?? 0); const taskKey = legacyCanonical.replace(/^doc_proj\d+_/, ''); const slot = deliverables.find( (d) => d.projectId === projectId && d.taskKey === taskKey, ); if (slot && !isLegacyLocalDocId(slot.docId)) { return slot.docId; } return legacyCanonical; } /** 任务请求使用的权威 doc_id:绝不透传 legacy doc-p* 本地 ID */ export function canonicalDocIdForTask( projectId: number | undefined, taskKey: string | undefined, deliverables?: DeliverableItem[], rawDocId?: string, ): string | undefined { if (!projectId || projectId <= 0 || !taskKey) return undefined; const slot = deliverables?.find( (d) => d.projectId === projectId && d.taskKey === taskKey, ); if (slot && !isLegacyLocalDocId(slot.docId)) { return slot.docId; } if (rawDocId?.trim()) { const resolved = deliverables?.length ? resolveCanonicalDocId(rawDocId, deliverables) : rawDocId.trim(); if (!isLegacyLocalDocId(resolved)) { return resolved; } const fromRaw = legacyDocIdToCanonical(rawDocId.trim()); if (fromRaw) return fromRaw; } return `doc_proj${projectId}_${taskKey}`; }