Files
Agent-System/web/src/api/projectDocuments.ts
T
WeiCHEN 681ca1b95e feat(web): unify API client with auth, errors, and limited retry
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.
2026-09-22 21:26:44 +08:00

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();
}