feat(workspace): 新增工作空间文件管理器(针对 workDir)

后端 server/workspace.go(全部在 requireAuth 之后,路径限定在 workDir):
- list/read/write/mkdir/delete/download/upload 七个端点
- wsResolve 用 Clean("/"+rel)+前缀校验防 .. 遍历;上传经 filepath.Base
  再二次校验;读文本 2MB 上限(超限/二进制仅下载);上传 512MB 上限;
  下载文件名过滤 + filename* UTF-8 编码

前端:
- 侧边栏「工作空间」入口(/function/workspace)
- api/types:workspaceList/read/write/mkdir/delete + upload(FormData)
  + download(blob 带 token)
- page:面包屑导航 + 目录表格 + 上传/新建文件夹/刷新 + 每行下载/删除
  + 右侧 Sheet 文本编辑器(二进制/超大仅下载)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Autumn-27andClaude Opus 4.8 committed 2026-08-09 01:01:19 -04:00
1 parent 5f0fac33aa
commit 6bffd491ee
6 files changed
+726

No files matched your search

+41
View File
@@ -12,6 +12,7 @@ import type {
Tool, Conversation, AgentTrigger,
InterceptRule, InterceptPending, InterceptApprovalRow,
TaskAssetView, SSProject, SSTask, ConvTokenSummary, CoverageGraphData, CoverageAssetRefs,
WorkspaceListing, WorkspaceFile,
CommandRecord, LLMRecordItem, LLMRecordDetail,
} from "@/lib/types";
@@ -131,6 +132,46 @@ export const api = {
taskAssetRefs: (id: string, assetId: number) =>
get<CoverageAssetRefs>(`/tasks/${id}/asset-refs?asset_id=${assetId}`),
// ---- workspace file manager (workDir) ----
workspaceList: (path = "") =>
get<WorkspaceListing>(`/workspace/list?path=${encodeURIComponent(path)}`),
workspaceRead: (path: string) =>
get<WorkspaceFile>(`/workspace/read?path=${encodeURIComponent(path)}`),
workspaceWrite: (path: string, content: string) =>
post<{ ok: boolean; path: string }>(`/workspace/write`, { path, content }),
workspaceMkdir: (path: string) =>
post<{ ok: boolean; path: string }>(`/workspace/mkdir`, { path }),
workspaceDelete: (path: string) =>
del<{ ok: boolean }>(`/workspace/delete?path=${encodeURIComponent(path)}`),
workspaceUpload: async (dir: string, files: File[]) => {
const fd = new FormData();
for (const f of files) fd.append("file", f);
const token = getToken();
const r = await fetch(`/api/workspace/upload?path=${encodeURIComponent(dir)}`, {
method: "POST",
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
body: fd,
});
if (!r.ok) throw new Error(`上传失败: ${r.status}`);
return r.json() as Promise<{ uploaded: number }>;
},
workspaceDownload: async (path: string) => {
const token = getToken();
const r = await fetch(`/api/workspace/download?path=${encodeURIComponent(path)}`, {
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
});
if (!r.ok) throw new Error(`下载失败: ${r.status}`);
const blob = await r.blob();
const objUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = objUrl;
a.download = path.split("/").pop() || "download";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(objUrl);
},
// ---- assets ----
// Server-side paginated: pass limit/offset, get back the page + full match total.
assets: (type = "", limit = 50, offset = 0) =>
+20
View File
@@ -178,6 +178,26 @@ export interface CoverageAssetRefs {
findings: CoverageAssetRef[];
}
// ---- Workspace file manager (workDir) ----
export interface WorkspaceEntry {
name: string;
path: string; // workspace-relative, forward slashes
dir: boolean;
size: number;
mtime: number; // unix millis
}
export interface WorkspaceListing {
path: string;
entries: WorkspaceEntry[];
}
export interface WorkspaceFile {
path: string;
size: number;
binary: boolean;
too_large?: boolean;
content?: string;
}
// 公司资产范围规则的一条(归属唯一真值来源)。
export interface ScopeRow {
id: number;