Fix server file browse flow
This commit is contained in:
@@ -563,14 +563,14 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
const [panelResult, setPanelResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null);
|
||||
const [uploadBusy, setUploadBusy] = useState(false);
|
||||
const [editor, setEditor] = useState<ServerFileEditorState>({ entry: null, key: "", draft: "", loading: false, saving: false });
|
||||
const initialRuntimeLoadRef = useRef(false);
|
||||
const browseRequestRef = useRef<{ key: string; idempotencyKey: string } | null>(null);
|
||||
|
||||
const activeDirectory = workspace.status === "ready" ? workspace.data.directories.find((item) => item.key === directoryKey) : undefined;
|
||||
const canUpload = workspace.status === "ready" && Boolean(activeDirectory) && !uploadBusy;
|
||||
const entries = list.status === "ready" ? list.data.entries : [];
|
||||
|
||||
const loadWorkspace = useCallback(async () => {
|
||||
initialRuntimeLoadRef.current = false;
|
||||
browseRequestRef.current = null;
|
||||
setWorkspace({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.getServerFileWorkspace(instance.id);
|
||||
@@ -584,56 +584,45 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
}
|
||||
}, [instance.id]);
|
||||
|
||||
const loadList = useCallback(async (options: { silent?: boolean } = {}) => {
|
||||
if (!directoryKey) return;
|
||||
const browseList = useCallback(async (options: { silent?: boolean; forceNew?: boolean; manual?: boolean } = {}): Promise<ServerFileListResponse | undefined> => {
|
||||
if (!directoryKey) return undefined;
|
||||
if (!options.silent) setList({ status: "loading" });
|
||||
const browseKey = `${directoryKey}:${relativePath}:${searchQuery}:${recursive}`;
|
||||
if (options.forceNew || browseRequestRef.current?.key !== browseKey) {
|
||||
browseRequestRef.current = { key: browseKey, idempotencyKey: serverFileIdempotency("browse", instance.id, `${browseKey}:${Date.now()}`) };
|
||||
}
|
||||
const operationId = options.manual ? operations.begin({ intent: "读取文件目录", targetKind: "server", targetId: instance.id, requester: session.displayName }) : "";
|
||||
if (!options.silent) setPanelResult({ status: "pending", label: "正在读取目录…" });
|
||||
try {
|
||||
const response = await platformApiClient.listServerFiles(instance.id, { directoryKey, path: relativePath || undefined, query: searchQuery || undefined, recursive });
|
||||
const response = await platformApiClient.browseServerFiles(instance.id, { directoryKey, path: relativePath || undefined, query: searchQuery || undefined, recursive, idempotencyKey: browseRequestRef.current?.idempotencyKey });
|
||||
setList({ status: "ready", data: response });
|
||||
if (options.silent && response.state === "ready") setPanelResult(null);
|
||||
if (options.silent && response.state === "pending") setPanelResult({ status: "pending", label: serverFileListPendingLabel(response) });
|
||||
if (options.silent && response.state === "failed") setPanelResult({ status: "failed", label: response.reason ?? "目录刷新失败" });
|
||||
if (operationId) operations.succeed(operationId, response.state === "ready" ? "目录读取完成" : "正在读取目录", response.job);
|
||||
if (response.state === "ready") setPanelResult(null);
|
||||
else if (response.state === "failed") setPanelResult({ status: "failed", label: response.reason ?? "目录读取失败" });
|
||||
else setPanelResult({ status: "pending", label: serverFileListPendingLabel(response) });
|
||||
return response;
|
||||
} catch (error) {
|
||||
setList({ status: "error", reason: error instanceof Error ? error.message : "文件列表加载失败" });
|
||||
if (operationId) operations.fail(operationId, error instanceof Error ? error.message : "目录读取失败", operationId);
|
||||
setPanelResult({ status: "failed", label: error instanceof Error ? error.message : "目录读取失败" });
|
||||
return undefined;
|
||||
}
|
||||
}, [directoryKey, instance.id, recursive, relativePath, searchQuery]);
|
||||
}, [directoryKey, instance.id, operations, recursive, relativePath, searchQuery, session.displayName]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadWorkspace();
|
||||
}, [loadWorkspace]);
|
||||
|
||||
const refreshRuntimeList = useCallback(async (options: { silent?: boolean } = {}) => {
|
||||
if (!directoryKey) return;
|
||||
const operationId = options.silent ? "" : operations.begin({ intent: "刷新文件目录", targetKind: "server", targetId: instance.id, requester: session.displayName });
|
||||
if (!options.silent) setPanelResult({ status: "pending", label: "正在向 Run 请求实时目录…" });
|
||||
try {
|
||||
const response = await platformApiClient.refreshServerFiles(instance.id, { directoryKey, path: relativePath || undefined, query: searchQuery || undefined, recursive, idempotencyKey: serverFileIdempotency("list", instance.id, directoryKey) });
|
||||
setList({ status: "ready", data: response });
|
||||
if (operationId) operations.succeed(operationId, `目录刷新任务 ${response.job?.id ?? "已派发"}`, response.job);
|
||||
if (response.state === "ready") setPanelResult(null);
|
||||
else setPanelResult({ status: serverFileListResultStatus(response), label: serverFileListResultLabel(response) });
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : "目录刷新失败";
|
||||
if (operationId) operations.fail(operationId, reason, operationId);
|
||||
setPanelResult({ status: "failed", label: reason });
|
||||
}
|
||||
}, [directoryKey, instance.id, operations, relativePath, recursive, searchQuery, session.displayName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace.status !== "ready" || !directoryKey) return;
|
||||
if (!initialRuntimeLoadRef.current) {
|
||||
initialRuntimeLoadRef.current = true;
|
||||
void refreshRuntimeList({ silent: true });
|
||||
return;
|
||||
}
|
||||
void loadList();
|
||||
}, [directoryKey, loadList, refreshRuntimeList, workspace.status]);
|
||||
void browseList({ forceNew: true });
|
||||
}, [browseList, directoryKey, relativePath, recursive, searchQuery, workspace.status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace.status !== "ready" || !directoryKey || list.status !== "ready" || list.data.state !== "pending") return;
|
||||
const timer = window.setInterval(() => void loadList({ silent: true }), 300);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [directoryKey, list, loadList, workspace.status]);
|
||||
const timer = window.setTimeout(() => void browseList({ silent: true }), 500);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [browseList, directoryKey, list, workspace.status]);
|
||||
|
||||
async function openEntry(entry: ServerFileEntryResponse) {
|
||||
if (entry.kind === "directory") {
|
||||
@@ -662,7 +651,7 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
const dispatch = await platformApiClient.readServerFile(instance.id, { key, idempotencyKey: serverFileIdempotency("read", instance.id, key) });
|
||||
operations.succeed(operationId, `读取任务 ${dispatch.job.id} 已派发`, dispatch.job);
|
||||
setEditor({ entry, key, draft: "", snapshot, loading: false, saving: false, message: snapshot.reason ?? "读取任务已派发;Run 返回后再次打开即可编辑。" });
|
||||
setPanelResult({ status: "pending", label: `读取任务已派发:${dispatch.job.id}` });
|
||||
setPanelResult({ status: "pending", label: "正在读取文件…" });
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : "文件读取失败";
|
||||
setEditor({ entry, key, draft: "", loading: false, saving: false, error: reason });
|
||||
@@ -678,8 +667,8 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
const dispatch = await platformApiClient.writeServerFile(instance.id, { key: editor.key, content: editor.draft, expectedVersion: editor.snapshot?.version, expectedChecksum: editor.snapshot?.checksum, idempotencyKey: serverFileIdempotency("write", instance.id, editor.key) });
|
||||
operations.succeed(operationId, `写入任务 ${dispatch.job.id} 已派发`, dispatch.job);
|
||||
setEditor((current) => ({ ...current, saving: false, message: "保存任务已派发;Run 会在工作区内原子写入。" }));
|
||||
setPanelResult({ status: "pending", label: `写入任务已派发:${dispatch.job.id}` });
|
||||
await loadList();
|
||||
setPanelResult({ status: "pending", label: "正在保存文件…" });
|
||||
await browseList({ forceNew: true });
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : "文件保存失败";
|
||||
operations.fail(operationId, reason, operationId);
|
||||
@@ -692,12 +681,19 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
const key = serverFileEntryKey(entry);
|
||||
if (!key) return;
|
||||
const operationId = operations.begin({ intent: "下载文件", targetKind: "server", targetId: instance.id, requester: session.displayName });
|
||||
const idempotencyKey = serverFileIdempotency("download", instance.id, key);
|
||||
setPanelResult({ status: "pending", label: "正在准备文件下载…" });
|
||||
try {
|
||||
const result = await platformApiClient.prepareServerFileDownload(instance.id, { key, idempotencyKey: serverFileIdempotency("download", instance.id, key) });
|
||||
let result = await platformApiClient.prepareServerFileDownload(instance.id, { key, idempotencyKey });
|
||||
for (let attempt = 0; result.status === "pending" && attempt < 240; attempt += 1) {
|
||||
setPanelResult({ status: "pending", label: `正在准备下载… ${Math.min(99, Math.max(1, attempt))}%` });
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 500));
|
||||
result = await platformApiClient.prepareServerFileDownload(instance.id, { key, idempotencyKey });
|
||||
}
|
||||
if (result.status !== "ready") throw new Error(result.reason ?? "文件下载准备超时");
|
||||
const message = await downloadServerFileResult(platformApiClient, result);
|
||||
operations.succeed(operationId, message, result.job);
|
||||
setPanelResult({ status: result.status === "ready" ? "succeeded" : "pending", label: message });
|
||||
setPanelResult({ status: "succeeded", label: message });
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : "文件下载失败";
|
||||
operations.fail(operationId, reason, operationId);
|
||||
@@ -719,8 +715,8 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
try {
|
||||
const response = await platformApiClient.uploadServerFile(instance.id, { directoryKey, relativePath: relativePath || undefined, file, idempotencyKey: serverFileIdempotency("upload", instance.id, file.name) });
|
||||
operations.succeed(operationId, `上传已暂存,写入任务 ${response.job.id} 已派发`, response.job);
|
||||
setPanelResult({ status: "pending", label: `上传已走独立文件通道排队:${response.relativePath}` });
|
||||
await loadList();
|
||||
setPanelResult({ status: "pending", label: `上传已提交:${response.relativePath}` });
|
||||
await browseList({ forceNew: true });
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : "文件上传失败";
|
||||
operations.fail(operationId, reason, operationId);
|
||||
@@ -777,7 +773,7 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
<button type="submit" className="icon-command">搜索</button>
|
||||
</form>
|
||||
<div className="action-strip server-file-actions">
|
||||
<button type="button" className="icon-command" onClick={() => void refreshRuntimeList()} disabled={!directoryKey}><RefreshCw size={14} /><span>刷新目录</span></button>
|
||||
<button type="button" className="icon-command" onClick={() => void browseList({ forceNew: true, manual: true })} disabled={!directoryKey}><RefreshCw size={14} /><span>刷新</span></button>
|
||||
<label className={cx("server-file-upload-control", !canUpload && "server-file-upload-disabled")} title={canUpload ? "上传到当前目录" : "当前目录不可上传或正在上传"}>
|
||||
<Upload size={14} /><span>{uploadBusy ? "上传中…" : "上传"}</span><input type="file" disabled={!canUpload} onChange={(event) => void uploadFile(event)} />
|
||||
</label>
|
||||
@@ -785,7 +781,7 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
</div>
|
||||
{panelResult && <ResultBadge status={panelResult.status} label={panelResult.label} />}
|
||||
{list.status === "loading" && <LoadingState label="正在加载文件列表…" compact />}
|
||||
{list.status === "error" && <ErrorState title="文件列表不可用" reason={list.reason} diagnosticId={`server-file-list:${instance.id}:${directoryKey}`} onRetry={() => void loadList()} compact />}
|
||||
{list.status === "error" && <ErrorState title="文件列表不可用" reason={list.reason} diagnosticId={`server-file-list:${instance.id}:${directoryKey}`} onRetry={() => void browseList({ forceNew: true })} compact />}
|
||||
{list.status === "ready" && (
|
||||
<div className="resource-table-wrap server-file-table-wrap">
|
||||
<table className="resource-table server-file-table">
|
||||
@@ -848,30 +844,14 @@ function serverFileEntryRowKey(entry: ServerFileEntryResponse): string {
|
||||
return `${entry.kind}:${entry.directoryKey}:${entry.relativePath ?? ""}:${entry.logicalKey ?? ""}:${entry.name}`;
|
||||
}
|
||||
|
||||
function serverFileListResultStatus(response: ServerFileListResponse): "pending" | "succeeded" | "failed" {
|
||||
if (response.state === "ready") return "succeeded";
|
||||
if (response.state === "failed") return "failed";
|
||||
return "pending";
|
||||
}
|
||||
|
||||
function serverFileListResultLabel(response: ServerFileListResponse): string {
|
||||
if (response.state === "failed") return response.reason ?? "目录刷新失败";
|
||||
return serverFileListPendingLabel(response);
|
||||
}
|
||||
|
||||
function serverFileListEmptyLabel(response: ServerFileListResponse): string {
|
||||
if (response.state === "pending") return serverFileListPendingLabel(response);
|
||||
if (response.state === "failed") return response.reason ?? "目录刷新失败";
|
||||
if (response.state === "failed") return response.reason ?? "目录读取失败";
|
||||
return "当前目录暂无文件。";
|
||||
}
|
||||
|
||||
function serverFileListPendingLabel(response: ServerFileListResponse): string {
|
||||
const job = response.job;
|
||||
if (!job) return response.reason ?? "正在读取当前目录,Run 返回后会自动更新。";
|
||||
const progress = job.progress?.message?.trim();
|
||||
const attempt = job.attempt > 0 ? ` · 第 ${job.attempt} 次尝试` : "";
|
||||
const nextAttempt = job.state === "retrying" && job.nextAttemptAt ? ` · 下次 ${formatDateTime(job.nextAttemptAt)}` : "";
|
||||
return `文件刷新任务 ${serverFileJobStateLabel(job.state)}${attempt}${nextAttempt}${progress ? ` · ${progress}` : ""}`;
|
||||
return "正在读取当前目录…";
|
||||
}
|
||||
|
||||
function serverFileJobStateLabel(state: JobResponse["state"]): string {
|
||||
|
||||
Reference in New Issue
Block a user