Files
Agent-System/web/src/api/freeformDoc.ts
T
weichen 88e33282db fix(freedoc): persist uploaded file metadata and text body server-side
上传文档跨端不可访问修复(ISSUE-20260927-upload-doc-cross-device):
- 迁移 00076:freeform_documents 增加 source_filename/mime/size_bytes/asset_id
- freedoc:Create/Get/List 读写源文件元数据,负值拒绝
- 前端:注册前全部附件先登记 CAS 并预读文本正文;文本正文嵌入注册
  markdown;跨端恢复 sourceType/fileMeta 路由回文件预览器
- 大小显示 formatFileSize(<512B 不再显示 0 KB)
- E2E 新增 FS-UPLOAD-02 跨浏览器上下文可访问性用例

回归:go vet/arch-check/go test -short(隔离 PG)+ web npm test/build 全绿;
存量无元数据旧文档不可恢复(重新上传即可)。
2026-09-27 07:45:15 +00:00

245 lines
7.2 KiB
TypeScript

/**
* 自由文档 (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<FreeformDocDetail> {
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<CreateFromMessageResponse> {
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<FreeformDocSummary[]> {
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<FreeformDocDetail> {
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<FreeformDocDetail> {
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<void> {
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<void> {
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<void> {
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})`);
}
}