/** * 自由文档 (Freeform Document) 前端 API 客户端 * 提供单条回复保存、私密列表、详情获取、编辑保存、共享流转及回收站接口 */ import { getBearerAuthHeaders } from './authHeaders'; import { projectSettingsErrorFromResponse } from './projectSettingsError'; import { apiFetch } from './client'; export interface FreeformDocSummary { doc_id: string; project_id: number; owner_user_id: number; title: string; space: 'my_private' | 'project_shared' | 'trash'; current_version_id: number; current_sha256: string; /** 上传附件注册时持久化的源文件元数据;非上传文档缺省。 */ source_filename?: string; source_mime_type?: string; source_size_bytes?: number; source_asset_id?: number; created_at: string; updated_at: string; } export interface FreeformDocDetail { document: { doc_id: string; project_id: number; owner_user_id: number; source_message_id?: number; source_content_sha256: string; title: string; governance_mode: 'freeform'; space: 'my_private' | 'project_shared' | 'trash'; current_version_id: number; current_sha256: string; /** 上传附件注册时持久化的源文件元数据;非上传文档缺省。 */ source_filename?: string; source_mime_type?: string; source_size_bytes?: number; source_asset_id?: number; is_deleted: boolean; created_at: string; updated_at: string; }; content_markdown: string; } export interface CreateFromMessageResponse { code: number; message: string; is_existing: boolean; data: FreeformDocDetail; } /** * 直接创建自由文档(上传附件即时注册服务端事实源) */ export async function createFreeformDoc(payload: { projectId: number; title: string; contentMarkdown: string; space?: 'my_private' | 'project_shared'; clientKey?: string; sourceFilename?: string; sourceMimeType?: string; sourceSizeBytes?: number; sourceAssetId?: number; }): Promise { const res = await apiFetch('/api/v1/freeform-documents', { method: 'POST', headers: getBearerAuthHeaders('application/json'), body: JSON.stringify({ project_id: payload.projectId, title: payload.title, content_markdown: payload.contentMarkdown, space: payload.space || 'my_private', client_key: payload.clientKey || undefined, source_filename: payload.sourceFilename || undefined, source_mime_type: payload.sourceMimeType || undefined, source_size_bytes: payload.sourceSizeBytes || undefined, source_asset_id: payload.sourceAssetId || undefined, }), }); if (!res.ok) { const errData = await res.json().catch(() => ({})); const msg = errData.message || errData.error_code || `创建失败 (${res.status})`; throw new Error(typeof msg === 'string' ? msg : `创建失败 (${res.status})`); } const body = await res.json(); return body.data; } /** * 从单条智能体回复保存为我的私密文档(幂等) */ export async function saveMessageToPrivate( messageId: number, projectId: number = 1, title?: string, expectedSha256?: string, ): Promise { const res = await apiFetch('/api/v1/freeform-documents/from-message', { method: 'POST', headers: getBearerAuthHeaders('application/json'), body: JSON.stringify({ message_id: messageId, project_id: projectId, title: title || undefined, expected_sha256: expectedSha256 || undefined, }), }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.message || `保存失败 (${res.status})`); } return res.json(); } /** * 按空间获取自由文档概要列表 */ export async function listFreeformDocs( space: 'my_private' | 'project_shared' | 'trash' = 'my_private', projectId?: number, ): Promise { let url = `/api/v1/freeform-documents?space=${encodeURIComponent(space)}`; if (projectId) { url += `&project_id=${projectId}`; } const res = await apiFetch(url, { headers: getBearerAuthHeaders() }); if (!res.ok) { throw new Error(`获取列表失败 (${res.status})`); } const body = await res.json(); return body.data || []; } /** * 获取自由文档详情与 Markdown 正文 */ export async function fetchFreeformDoc(docId: string): Promise { const res = await apiFetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}`, { headers: getBearerAuthHeaders(), }); if (!res.ok) { if (res.status === 403) { throw new Error('该文档为他人私密文档,严格禁止跨用户访问'); } if (res.status === 404) { throw new Error('文档不存在或已被删除'); } throw new Error(`读取文档失败 (${res.status})`); } const body = await res.json(); return body.data; } /** * 更新自由文档正文或标题(支持 If-Match 乐观锁) */ export async function updateFreeformDoc( docId: string, payload: { title?: string; contentMarkdown?: string; changeSummary?: string; expectedVersion?: number; expectedSha256?: string; } ): Promise { const headers = getBearerAuthHeaders('application/json'); if (payload.expectedVersion !== undefined) { headers['If-Match'] = `"${payload.expectedVersion}"`; } else if (payload.expectedSha256) { headers['If-Match'] = `"${payload.expectedSha256}"`; } const res = await apiFetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}`, { method: 'PUT', headers, body: JSON.stringify({ title: payload.title, content_markdown: payload.contentMarkdown, change_summary: payload.changeSummary, expected_version: payload.expectedVersion, expected_sha256: payload.expectedSha256, }), }); if (!res.ok) { const text = await res.text(); throw projectSettingsErrorFromResponse(res.status, text, `保存失败 (${res.status})`); } const body = await res.json(); return body.data; } /** * 一键共享至项目参考库 */ export async function shareFreeformDoc(docId: string): Promise { const res = await apiFetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}/share`, { method: 'POST', headers: getBearerAuthHeaders(), }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.message || `共享失败 (${res.status})`); } } /** * 撤回共享,恢复为个人私密 */ export async function unshareFreeformDoc(docId: string): Promise { const res = await apiFetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}/unshare`, { method: 'POST', headers: getBearerAuthHeaders(), }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.message || `撤回失败 (${res.status})`); } } /** * 软删除文档移至回收站 */ export async function deleteFreeformDoc(docId: string): Promise { const res = await apiFetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}`, { method: 'DELETE', headers: getBearerAuthHeaders(), }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.message || `删除失败 (${res.status})`); } }