Route web/src/api modules through shared apiFetch so Bearer headers, typed error envelopes, and GET/HEAD transient retries live in one place without swallowing domain failures used by existing error cards.
138 lines
3.7 KiB
TypeScript
138 lines
3.7 KiB
TypeScript
/** 项目工作流受控文档 API */
|
|
|
|
import { getBearerAuthHeaders } from './authHeaders';
|
|
import { apiFetch } from './client';
|
|
|
|
export type WorkflowDocumentSummary = {
|
|
doc_id: string;
|
|
project_id: number;
|
|
task_key: string;
|
|
title: string;
|
|
workflow_key?: string;
|
|
lifecycle_state: string;
|
|
is_template_only: boolean;
|
|
content_sha256?: string;
|
|
work_dir?: string;
|
|
version?: number;
|
|
updated_at?: string;
|
|
lease_holder_type?: 'squad' | 'human' | '';
|
|
lease_holder_id?: string;
|
|
lease_expires_at?: string;
|
|
lease_heartbeat_at?: string;
|
|
author_id?: number;
|
|
author_name?: string;
|
|
reviewer_id?: number;
|
|
reviewer_name?: string;
|
|
approver_id?: number;
|
|
approver_name?: string;
|
|
duty_policy?: string;
|
|
};
|
|
|
|
export type ProjectWorkflowDocumentsResponse = {
|
|
documents: WorkflowDocumentSummary[];
|
|
active_workflow_key?: string;
|
|
workflow_scope_mode?: 'strict' | 'weighted';
|
|
};
|
|
|
|
export type ControlledDocConsistency =
|
|
| 'aligned'
|
|
| 'missing'
|
|
| 'drift'
|
|
| 'empty'
|
|
| 'ledger_missing';
|
|
|
|
export type ControlledDocumentContent = {
|
|
doc_id: string;
|
|
version: number;
|
|
content_sha256: string;
|
|
markdown: string;
|
|
is_template_only: boolean;
|
|
consistency?: ControlledDocConsistency;
|
|
record_sha256?: string;
|
|
file_sha256?: string;
|
|
};
|
|
|
|
export async function listProjectWorkflowDocuments(
|
|
projectId: number,
|
|
): Promise<WorkflowDocumentSummary[]> {
|
|
const resp = await apiFetch(
|
|
`/api/v1/projects/${projectId}/documents?category=workflow`,
|
|
{ headers: getBearerAuthHeaders() },
|
|
);
|
|
if (!resp.ok) {
|
|
throw new Error(`list project workflow documents failed: ${resp.status}`);
|
|
}
|
|
const data = await resp.json();
|
|
// 兼容旧数组响应与新 { documents, active_workflow_key } 包装。
|
|
if (Array.isArray(data)) return data;
|
|
if (data && Array.isArray(data.documents)) return data.documents;
|
|
return [];
|
|
}
|
|
|
|
export async function listProjectWorkflowDocumentsDetailed(
|
|
projectId: number,
|
|
): Promise<ProjectWorkflowDocumentsResponse> {
|
|
const resp = await apiFetch(
|
|
`/api/v1/projects/${projectId}/documents?category=workflow`,
|
|
{ headers: getBearerAuthHeaders() },
|
|
);
|
|
if (!resp.ok) {
|
|
throw new Error(`list project workflow documents failed: ${resp.status}`);
|
|
}
|
|
const data = await resp.json();
|
|
if (Array.isArray(data)) {
|
|
return { documents: data };
|
|
}
|
|
return {
|
|
documents: Array.isArray(data?.documents) ? data.documents : [],
|
|
active_workflow_key: typeof data?.active_workflow_key === 'string' ? data.active_workflow_key : undefined,
|
|
workflow_scope_mode: data?.workflow_scope_mode === 'weighted' ? 'weighted' : 'strict',
|
|
};
|
|
}
|
|
|
|
export class ControlledContentRequestError extends Error {
|
|
status: number;
|
|
code?: string;
|
|
retryable?: boolean;
|
|
needsAdmin?: boolean;
|
|
correlationId?: string;
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message);
|
|
this.name = 'ControlledContentRequestError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
export async function fetchControlledDocumentContent(
|
|
docId: string,
|
|
): Promise<ControlledDocumentContent> {
|
|
const resp = await apiFetch(`/api/v1/docgov/documents/${encodeURIComponent(docId)}/content`, {
|
|
headers: getBearerAuthHeaders(),
|
|
});
|
|
if (!resp.ok) {
|
|
let payload: {
|
|
error_code?: string;
|
|
message?: string;
|
|
retryable?: boolean;
|
|
needs_admin?: boolean;
|
|
correlation_id?: string;
|
|
} = {};
|
|
try {
|
|
payload = await resp.json();
|
|
} catch {
|
|
payload = {};
|
|
}
|
|
const err = new ControlledContentRequestError(
|
|
payload.message || `fetch controlled document content failed: ${resp.status}`,
|
|
resp.status,
|
|
);
|
|
err.code = payload.error_code;
|
|
err.retryable = payload.retryable;
|
|
err.needsAdmin = payload.needs_admin;
|
|
err.correlationId = payload.correlation_id;
|
|
throw err;
|
|
}
|
|
return resp.json();
|
|
}
|