Files
pikasTech-unidesk/src/components/frontend/src/baidu-netdisk.tsx
T
Codex a242e3e3ec feat: expand scheduling, notifications, and queue runtime
- add scheduled task plumbing across backend core, CLI, and frontend surfaces

- add frontend notification UI and keep service pages using the repaired shared stylesheet

- refactor code queue runtime and update baidu netdisk/service integration docs
2026-05-13 08:43:43 +00:00

567 lines
33 KiB
TypeScript

import React from "react";
import { fmtClock, fmtDate } from "./time";
import { LoadingTitle } from "./loading-indicator";
import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error";
import { UniDeskErrorBanner } from "./unidesk-error-banner";
import { useNotification } from "./notification-context";
type AnyRecord = Record<string, any>;
const h = React.createElement;
const { useEffect } = React;
const useState: any = React.useState;
function requestJson(path: string, options: AnyRecord = {}): Promise<any> {
return requestUniDeskJson(path, { failureFields: ["ok", "success"], ...options });
}
function baiduApi(apiBaseUrl: string, path: string): string {
return `${apiBaseUrl}/microservices/baidu-netdisk/proxy${path}`;
}
function numberText(value: any): string {
const number = Number(value);
return Number.isFinite(number) ? number.toLocaleString("zh-CN") : "--";
}
function fmtBytes(value: any): string {
const bytes = Number(value);
if (!Number.isFinite(bytes) || bytes <= 0) return "--";
const units = ["B", "KB", "MB", "GB", "TB"];
let current = bytes;
let index = 0;
while (current >= 1024 && index < units.length - 1) {
current /= 1024;
index += 1;
}
return `${current.toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
}
function StatusBadge({ status, children }: AnyRecord) {
const normalized = String(status || "unknown").toLowerCase();
return h("span", { className: `status-badge ${normalized}` }, children || status || "unknown");
}
function MetricCard({ label, value, hint, tone }: AnyRecord) {
return h("article", { className: `metric-card ${tone || ""}` },
h("div", { className: "metric-label" }, label),
h("div", { className: "metric-value" }, value),
h("div", { className: "metric-hint" }, hint),
);
}
function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) {
return h("section", { className: `panel ${className || ""}` },
h("div", { className: "panel-head" },
h("div", null,
eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null,
h(LoadingTitle, { title, loading }),
),
actions ? h("div", { className: "panel-actions" }, actions) : null,
),
h("div", { className: "panel-body" }, children),
);
}
function RawButton({ title, data, onOpen, testId }: AnyRecord) {
return h("button", {
type: "button",
className: "ghost-btn",
"data-testid": testId,
onClick: (event: any) => {
event?.stopPropagation?.();
onOpen(title, data);
},
}, "查看原始JSON");
}
function EmptyState({ title, text }: AnyRecord) {
return h("div", { className: "empty-state" }, h("strong", null, title), h("span", null, text));
}
function DocLinkCard({ title, text, href, badge, testId }: AnyRecord) {
return h("a", {
className: "doc-link-card",
href,
target: "_blank",
rel: "noreferrer",
"data-testid": testId,
},
h("span", null, badge || "DOC"),
h("strong", null, title),
h("p", null, text),
h("code", null, href),
);
}
function microserviceRuntime(service: any): AnyRecord {
return service?.runtime && typeof service.runtime === "object" && !Array.isArray(service.runtime) ? service.runtime : {};
}
function microserviceBackend(service: any): AnyRecord {
return service?.backend && typeof service.backend === "object" && !Array.isArray(service.backend) ? service.backend : {};
}
function microserviceRepository(service: any): AnyRecord {
return service?.repository && typeof service.repository === "object" && !Array.isArray(service.repository) ? service.repository : {};
}
function filesFromPayload(payload: any): any[] {
return Array.isArray(payload?.files) ? payload.files : [];
}
function jobsFromPayload(payload: any): any[] {
return Array.isArray(payload?.jobs) ? payload.jobs : [];
}
function pathParent(path: string, root: string): string {
if (!path || path === root) return root;
const withoutSlash = path.replace(/\/+$/u, "");
const parent = withoutSlash.slice(0, withoutSlash.lastIndexOf("/")) || root;
return parent.length < root.length ? root : parent;
}
function pathJoin(base: string, name: string): string {
const safeName = String(name || "").replace(/^\/+|\/+$/gu, "");
return `${base.replace(/\/+$/u, "")}/${safeName}`;
}
function rootDisplayName(root: string): string {
return root === "/" ? "/" : root.split("/").filter(Boolean).pop() || root;
}
function ProgressBar({ percent }: AnyRecord) {
const value = Math.max(0, Math.min(100, Number(percent) || 0));
return h("div", { className: "baidu-progress" }, h("span", { style: { width: `${value}%` } }), h("em", null, `${value.toFixed(1)}%`));
}
export function BaiduNetdiskPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRecord) {
const service = microservices.find((item: any) => item.id === "baidu-netdisk") || null;
const [state, setState] = useState({ loading: false, actionLoading: false, error: "", message: "", health: null, account: null, files: null, transfers: null, logs: null, selfTest: null, refreshedAt: null });
const [currentDir, setCurrentDir] = useState("/");
const [deviceSession, setDeviceSession] = useState(null);
const [folderName, setFolderName] = useState("");
const [uploadForm, setUploadForm] = useState({ localPath: "sample.txt", remotePath: "/sample.txt" });
const [downloadForm, setDownloadForm] = useState({ fsId: "", localPath: "downloads/" });
const { addNotification } = useNotification();
const appRoot = state.health?.baidu?.appRoot || state.account?.rootPath || "/";
useEffect(() => {
setUploadForm((prev: any) => {
const legacyDefaults = new Set(["/sample.txt", "/apps/UniDeskBaiduNetdisk/sample.txt"]);
if (prev.remotePath && !legacyDefaults.has(prev.remotePath)) return prev;
const remotePath = pathJoin(appRoot, "sample.txt");
return prev.remotePath === remotePath ? prev : { ...prev, remotePath };
});
}, [appRoot]);
async function loadFiles(dir = currentDir): Promise<void> {
const targetDir = dir || appRoot;
const files = await requestJson(baiduApi(apiBaseUrl, `/api/files?dir=${encodeURIComponent(targetDir)}&limit=100`));
setState((prev: any) => ({ ...prev, files }));
}
async function loadTransfers(): Promise<void> {
const transfers = await requestJson(baiduApi(apiBaseUrl, "/api/transfers?limit=80"));
setState((prev: any) => ({ ...prev, transfers }));
}
async function load(): Promise<void> {
if (!service) return;
setState((prev: any) => ({ ...prev, loading: true, error: "", message: "" }));
try {
const health = await requestJson(`${apiBaseUrl}/microservices/baidu-netdisk/health`);
const root = health?.baidu?.appRoot || appRoot;
let account: any = null;
let files: any = null;
if (health?.auth?.loggedIn) {
account = await requestJson(baiduApi(apiBaseUrl, "/api/account?refresh=1"));
const dir = currentDir && currentDir.startsWith(root) ? currentDir : root;
setCurrentDir(dir);
files = await requestJson(baiduApi(apiBaseUrl, `/api/files?dir=${encodeURIComponent(dir)}&limit=100`));
} else {
setCurrentDir(root);
}
const transfers = await requestJson(baiduApi(apiBaseUrl, "/api/transfers?limit=80"));
const logs = await requestJson(baiduApi(apiBaseUrl, "/logs?limit=60"));
setState((prev: any) => ({ ...prev, loading: false, health, account: account?.account || null, files, transfers, logs, refreshedAt: new Date() }));
} catch (err) {
setState((prev: any) => ({ ...prev, loading: false, error: errorMessage(err, "百度网盘服务加载失败") }));
}
}
async function startLogin(): Promise<void> {
setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" }));
try {
const result = await requestJson(baiduApi(apiBaseUrl, "/api/auth/device/start"), { method: "POST", body: {} });
setDeviceSession(result.session || null);
setState((prev: any) => ({ ...prev, actionLoading: false, message: "设备码已生成,请扫码授权" }));
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "创建设备码失败") }));
}
}
async function pollLogin(manual = false): Promise<void> {
if (!deviceSession?.id) return;
if (manual) setState((prev: any) => ({ ...prev, actionLoading: true, error: "" }));
try {
const result = await requestJson(baiduApi(apiBaseUrl, `/api/auth/device/status?sessionId=${encodeURIComponent(deviceSession.id)}`));
setDeviceSession(result.session || null);
if (result.session?.status === "succeeded") {
setState((prev: any) => ({ ...prev, actionLoading: false, message: "授权成功,正在刷新账号与文件列表" }));
await load();
} else if (manual) {
setState((prev: any) => ({ ...prev, actionLoading: false }));
}
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "轮询登录状态失败") }));
}
}
async function logout(): Promise<void> {
setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" }));
try {
await requestJson(baiduApi(apiBaseUrl, "/api/auth/logout"), { method: "POST", body: {} });
setDeviceSession(null);
setState((prev: any) => ({ ...prev, actionLoading: false, account: null, files: null, message: "本地 token 已清除" }));
await load();
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "退出登录失败") }));
}
}
async function createFolder(event: any): Promise<void> {
event.preventDefault();
const name = folderName.trim();
if (!name) return;
setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" }));
try {
await requestJson(baiduApi(apiBaseUrl, "/api/folders"), { method: "POST", body: { path: pathJoin(currentDir, name) } });
setFolderName("");
setState((prev: any) => ({ ...prev, actionLoading: false, message: "文件夹已创建" }));
await loadFiles(currentDir);
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "创建文件夹失败") }));
}
}
async function deleteRemote(path: string): Promise<void> {
if (!path) return;
setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" }));
try {
await requestJson(baiduApi(apiBaseUrl, "/api/files/manage"), { method: "POST", body: { opera: "delete", filelist: [{ path }], async: 1 } });
setState((prev: any) => ({ ...prev, actionLoading: false, message: "删除任务已提交" }));
await loadFiles(currentDir);
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "删除失败") }));
}
}
async function submitUpload(event: any): Promise<void> {
event.preventDefault();
setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" }));
try {
await requestJson(baiduApi(apiBaseUrl, "/api/transfers/upload-from-path"), { method: "POST", body: uploadForm });
setState((prev: any) => ({ ...prev, actionLoading: false, message: "上传任务已入队" }));
await loadTransfers();
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "上传任务创建失败") }));
}
}
async function submitDownload(event: any): Promise<void> {
event.preventDefault();
setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" }));
try {
await requestJson(baiduApi(apiBaseUrl, "/api/transfers/download-to-path"), { method: "POST", body: downloadForm });
setState((prev: any) => ({ ...prev, actionLoading: false, message: "下载任务已入队" }));
await loadTransfers();
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "下载任务创建失败") }));
}
}
async function transferAction(id: string, action: "cancel" | "retry"): Promise<void> {
setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" }));
try {
await requestJson(baiduApi(apiBaseUrl, `/api/transfers/${encodeURIComponent(id)}/${action}`), { method: "POST", body: {} });
setState((prev: any) => ({ ...prev, actionLoading: false, message: action === "cancel" ? "已请求取消任务" : "任务已重新入队" }));
await loadTransfers();
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "任务操作失败") }));
}
}
async function runSelfTest(): Promise<void> {
setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "正在运行上传/下载自测..." }));
try {
const selfTest = await requestJson(baiduApi(apiBaseUrl, "/api/self-test"), { method: "POST", body: {} });
setState((prev: any) => ({ ...prev, actionLoading: false, selfTest, message: `上传/下载自测通过:${selfTest.remotePath || ""}` }));
await loadFiles(currentDir);
await loadTransfers();
} catch (err) {
setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "上传/下载自测失败") }));
}
}
useEffect(() => {
if (!service) return undefined;
load();
return undefined;
}, [service?.id, service?.runtime?.providerStatus]);
useEffect(() => {
if (!deviceSession?.id || deviceSession.status !== "pending") return undefined;
const timer = window.setInterval(() => void pollLogin(false), Math.max(5000, Number(deviceSession.pollIntervalSeconds || 5) * 1000));
return () => window.clearInterval(timer);
}, [deviceSession?.id, deviceSession?.status, deviceSession?.pollIntervalSeconds]);
useEffect(() => {
if (!service) return undefined;
const timer = window.setInterval(() => void loadTransfers(), 5000);
return () => window.clearInterval(timer);
}, [service?.id]);
if (!service) return h(EmptyState, { title: "Baidu Netdisk 未登记", text: "请在 config.json 的 microservices 中登记用户服务 id=baidu-netdisk" });
const runtime = microserviceRuntime(service);
const repository = microserviceRepository(service);
const backend = microserviceBackend(service);
const health = state.health || {};
const account = state.account || health.auth?.account || null;
const auth = health.auth || {};
const files = filesFromPayload(state.files);
const jobs = jobsFromPayload(state.transfers);
const quota = account?.quota || {};
const loggedIn = Boolean(auth.loggedIn || account);
const configured = Boolean(auth.configured);
return h("div", { className: "baidu-netdisk-page", "data-testid": "baidu-netdisk-page" },
h(Panel, {
title: "Baidu Netdisk 工作台",
eyebrow: "Containerized Storage Gateway",
loading: state.loading,
actions: h("div", { className: "panel-actions" },
h("a", { className: "ghost-btn", href: "/docs/issue/baidu-netdisk-env-setup.md", target: "_blank", rel: "noreferrer", "data-testid": "baidu-netdisk-config-doc-link" }, "配置文档"),
h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "baidu-netdisk-refresh" }, state.loading ? "刷新中" : "刷新"),
h(RawButton, { title: "Baidu Netdisk 用户服务", data: service, onOpen: onRaw, testId: "raw-baidu-netdisk-service" }),
),
},
h("div", { className: "baidu-netdisk-hero" },
h("div", null,
h("div", { className: "node-version-line" },
h(StatusBadge, { status: runtime.providerStatus === "online" ? "online" : "warn" }, runtime.providerStatus || "unknown"),
h("span", null, service.providerId),
h(StatusBadge, { status: backend.public ? "warn" : "private" }, backend.public ? "公网暴露" : "仅 UniDesk frontend 代理访问"),
),
h("p", { className: "muted paragraph" }, service.description),
),
h("div", { className: "microservice-ref-card" },
h("span", null, "Repo"),
h("strong", null, repository.url || "--"),
h("code", null, repository.commitId || "--"),
),
h("div", { className: "microservice-ref-card" },
h("span", null, "Private Backend"),
h("strong", null, `${backend.nodeBindHost || "--"}:${backend.nodePort || "--"}`),
h("code", null, `${repository.composeFile || "--"} / ${repository.composeService || "--"}`),
),
),
h(UniDeskErrorBanner, { error: state.error, wide: true }),
),
h("div", { className: "metric-grid" },
h(MetricCard, { label: "Health", value: health.ok ? "OK" : "--", hint: health.storage?.postgres || "postgres", tone: health.ok ? "ok" : "warn" }),
h(MetricCard, { label: "OAuth", value: configured ? "已配置" : "待配置", hint: configured ? "client + secret + token key" : "需要设置 UNIDESK_BAIDU_NETDISK_*", tone: configured ? "ok" : "warn" }),
h(MetricCard, { label: "Login", value: loggedIn ? "已登录" : "未登录", hint: account?.username || "Device Code QR", tone: loggedIn ? "ok" : "warn" }),
h(MetricCard, { label: "Work Root", value: rootDisplayName(appRoot), hint: appRoot }),
h(MetricCard, { label: "Quota", value: fmtBytes(quota.used), hint: quota.total ? `${quota.usedPercent || 0}% / ${fmtBytes(quota.total)}` : "授权后刷新" }),
h(MetricCard, { label: "Transfers", value: numberText(jobs.length), hint: `running ${state.transfers?.counts?.running || 0} / failed ${state.transfers?.counts?.failed || 0}` }),
),
h("div", { className: "baidu-netdisk-grid" },
h(Panel, {
title: "配置与文档",
eyebrow: "Deployment References",
className: "baidu-docs-panel",
actions: h("div", { className: "panel-actions inline-actions" },
h("a", { className: "ghost-btn", href: "/docs/issue/baidu-netdisk-env-setup.md", target: "_blank", rel: "noreferrer" }, "打开环境配置"),
h("a", { className: "ghost-btn", href: "/docs/issue/baidu-netdisk-user-service.md", target: "_blank", rel: "noreferrer" }, "打开服务方案"),
),
},
h("p", { className: "muted paragraph" }, configured
? "OAuth 运行时变量已配置;如需轮换密钥、迁移部署或排查代理边界,可直接打开下面的项目内文档。"
: "首次使用请先按环境变量配置文档填入百度应用 client id / secret,然后重建 baidu-netdisk 服务并刷新本页。"),
h("div", { className: "baidu-doc-grid", "data-testid": "baidu-netdisk-doc-links" },
h(DocLinkCard, {
title: "环境变量配置",
text: "填写 UNIDESK_BAIDU_NETDISK_CLIENT_ID、CLIENT_SECRET、TOKEN_KEY,并执行重建与健康检查。",
href: "/docs/issue/baidu-netdisk-env-setup.md",
badge: "SETUP",
testId: "baidu-netdisk-env-doc-card",
}),
h(DocLinkCard, {
title: "服务方案与 API",
text: "说明 OAuth Device Code、根目录工作区、staging 上传下载任务和后端 API 设计。",
href: "/docs/issue/baidu-netdisk-user-service.md",
badge: "DESIGN",
}),
h(DocLinkCard, {
title: "用户服务安全边界",
text: "查看 UniDesk microservice 私有代理、允许路径、frontendOnly 和密钥边界规则。",
href: "/docs/reference/microservices.md",
badge: "REF",
}),
h(DocLinkCard, {
title: "部署与重建流程",
text: "查看 server rebuild、Compose 编排、健康检查和交付验证的长期规则。",
href: "/docs/reference/deployment.md",
badge: "DEPLOY",
}),
h(DocLinkCard, {
title: "CLI 验证命令",
text: "查看 microservice health/proxy、server rebuild、job status 等命令入口。",
href: "/docs/reference/cli.md",
badge: "CLI",
}),
h(DocLinkCard, {
title: "百度设备码模式",
text: "打开百度官方 OAuth Device Code 文档,对照扫码登录和轮询参数。",
href: "https://pan.baidu.com/union/doc/fl1x114ti",
badge: "OFFICIAL",
}),
),
),
h(Panel, {
title: "设备码登录",
eyebrow: "OAuth Device Code",
className: "baidu-login-panel",
loading: state.actionLoading,
actions: h("div", { className: "panel-actions inline-actions" },
h("button", { type: "button", className: "primary-btn", onClick: startLogin, disabled: state.actionLoading || !configured, "data-testid": "baidu-netdisk-start-login" }, "生成二维码"),
deviceSession?.id ? h("button", { type: "button", className: "ghost-btn", onClick: () => pollLogin(true), disabled: state.actionLoading }, "检查状态") : null,
loggedIn ? h("button", { type: "button", className: "ghost-btn", onClick: logout, disabled: state.actionLoading }, "清除本地登录") : null,
h(RawButton, { title: "Baidu Device Session", data: deviceSession || auth.latestSession, onOpen: onRaw, testId: "raw-baidu-device-session" }),
),
},
h("div", { className: "baidu-login-card", "data-testid": "baidu-netdisk-login-card" },
h("div", { className: "baidu-qr-frame" },
deviceSession?.qrcodeUrl
? h("img", { src: deviceSession.qrcodeUrl, alt: "百度网盘设备码授权二维码", "data-testid": "baidu-netdisk-qrcode" })
: h(EmptyState, { title: configured ? "等待二维码" : "OAuth 未配置", text: configured ? "点击生成二维码后使用百度网盘或百度 App 扫码" : "设置 client id、secret 和 token key 后重建服务" }),
),
h("div", { className: "claudeqq-login-copy" },
h("div", { className: "node-version-line" },
h(StatusBadge, { status: loggedIn ? "online" : deviceSession?.status === "pending" ? "warn" : "unknown" }, loggedIn ? "已登录" : deviceSession?.status || "未开始"),
h("span", null, deviceSession?.secondsRemaining !== undefined ? `${deviceSession.secondsRemaining}s` : "--"),
h("span", null, "scope basic,netdisk"),
),
h("p", { className: "muted paragraph" }, loggedIn
? "access token / refresh token 已加密保存到 PostgreSQL;前端只看到脱敏登录态。"
: "后端使用百度 OAuth Device Code 轮询换取 token;二维码过期后重新生成即可。"),
h("div", { className: "microservice-ref-card" }, h("span", null, "User Code"), h("strong", null, deviceSession?.userCode || "--"), h("code", null, deviceSession?.verificationUrl || "https://openapi.baidu.com/device")),
h("div", { className: "microservice-ref-card" }, h("span", null, "Expires"), h("strong", null, deviceSession?.expiresAt ? fmtDate(deviceSession.expiresAt) : "--"), h("code", null, deviceSession?.error || "no token exposed")),
),
),
),
h(Panel, { title: "账号与容量", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Account", loading: state.loading,
actions: h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "Baidu Account", data: account, onOpen: onRaw, testId: "raw-baidu-account" })),
},
account ? h("div", { className: "baidu-account-card" },
h("div", { className: "node-version-line" }, h(StatusBadge, { status: "online" }, "connected"), h("span", null, account.baiduUid || "--"), h("span", null, `VIP ${account.vipType ?? "--"}`)),
h("h3", null, account.username || "Baidu Netdisk"),
h("p", { className: "muted paragraph" }, `工作目录固定在 ${account.rootPath || appRoot};v1 上传/下载只读写容器 staging 目录,不把大文件字节流穿过 UniDesk proxy。`),
h("div", { className: "quota-bar" }, h("span", { style: { width: `${Math.max(0, Math.min(100, Number(quota.usedPercent || 0)))}%` } })),
h("div", { className: "microservice-ref-card" }, h("span", null, "Quota"), h("strong", null, `${fmtBytes(quota.used)} / ${fmtBytes(quota.total)}`), h("code", null, `${quota.usedPercent || 0}% used`)),
) : h(EmptyState, { title: "尚未登录", text: "扫码授权后这里会显示账号、UID、会员状态和容量" }),
),
h(Panel, { title: "文件浏览器", eyebrow: currentDir, className: "baidu-files-panel", loading: state.loading,
actions: h("div", { className: "panel-actions inline-actions" },
h("button", { type: "button", className: "ghost-btn", onClick: () => { const parent = pathParent(currentDir, appRoot); setCurrentDir(parent); void loadFiles(parent); }, disabled: !loggedIn || currentDir === appRoot }, "上级"),
h("button", { type: "button", className: "ghost-btn", onClick: () => loadFiles(currentDir), disabled: !loggedIn }, "刷新文件"),
h(RawButton, { title: "Baidu Files", data: state.files, onOpen: onRaw, testId: "raw-baidu-files" }),
),
},
h("form", { className: "baidu-pathbar", onSubmit: (event: any) => { event.preventDefault(); void loadFiles(currentDir); } },
h("input", { value: currentDir, onChange: (event: any) => setCurrentDir(event.target.value), disabled: !loggedIn }),
h("button", { type: "submit", className: "ghost-btn", disabled: !loggedIn }, "打开路径"),
),
h("form", { className: "baidu-pathbar", onSubmit: createFolder },
h("input", { value: folderName, onChange: (event: any) => setFolderName(event.target.value), placeholder: "新文件夹名称", disabled: !loggedIn }),
h("button", { type: "submit", className: "primary-btn", disabled: !loggedIn || !folderName.trim() }, "新建文件夹"),
),
!loggedIn ? h(EmptyState, { title: "等待授权", text: "登录后通过 /api/files 读取工作目录文件列表" }) : files.length === 0 ? h(EmptyState, { title: "目录为空", text: "可以从 staging 目录上传文件或新建文件夹" }) :
h("div", { className: "table-wrap", "data-testid": "baidu-netdisk-file-table" }, h("table", null,
h("thead", null, h("tr", null, h("th", null, "名称"), h("th", null, "类型"), h("th", null, "大小"), h("th", null, "修改时间"), h("th", null, "fs_id"), h("th", null, "操作"))),
h("tbody", null, files.map((file: any) => h("tr", { key: file.fsId || file.path },
h("td", null, h("strong", null, file.serverFilename || file.path), h("code", null, file.path || "--")),
h("td", null, h(StatusBadge, { status: file.isDir ? "queued" : "private" }, file.isDir ? "DIR" : "FILE")),
h("td", null, file.isDir ? "--" : fmtBytes(file.size)),
h("td", null, file.serverMtime ? fmtDate(file.serverMtime * 1000) : "--"),
h("td", null, h("code", null, file.fsId || "--")),
h("td", null, h("div", { className: "inline-actions" },
file.isDir ? h("button", { type: "button", className: "ghost-btn", onClick: () => { setCurrentDir(file.path); void loadFiles(file.path); } }, "打开") :
h("button", { type: "button", className: "ghost-btn", onClick: () => setDownloadForm((prev: any) => ({ ...prev, fsId: file.fsId })) }, "填入下载"),
h("button", { type: "button", className: "ghost-btn", onClick: () => deleteRemote(file.path), disabled: state.actionLoading }, "删除"),
)),
))),
)),
),
h(Panel, { title: "传输任务", eyebrow: "staging path jobs", className: "baidu-transfers-panel", loading: state.actionLoading,
actions: h("div", { className: "panel-actions inline-actions" },
h("button", { type: "button", className: "primary-btn", onClick: runSelfTest, disabled: !loggedIn || state.actionLoading, "data-testid": "baidu-netdisk-self-test" }, "运行自测"),
h("button", { type: "button", className: "ghost-btn", onClick: loadTransfers }, "刷新任务"),
h(RawButton, { title: "Baidu Transfers", data: state.transfers, onOpen: onRaw, testId: "raw-baidu-transfers" }),
),
},
h("div", { className: "baidu-transfer-forms" },
h("form", { className: "stack-form", onSubmit: submitUpload, "data-testid": "baidu-upload-form" },
h("label", null, "容器 staging 文件", h("input", { value: uploadForm.localPath, onChange: (event: any) => setUploadForm((prev: any) => ({ ...prev, localPath: event.target.value })), placeholder: "sample.txt" })),
h("label", null, "百度网盘目标路径", h("input", { value: uploadForm.remotePath, onChange: (event: any) => setUploadForm((prev: any) => ({ ...prev, remotePath: event.target.value })), placeholder: pathJoin(appRoot, "sample.txt") })),
h("button", { type: "submit", className: "primary-btn", disabled: !loggedIn || state.actionLoading }, "上传 staging 文件"),
),
h("form", { className: "stack-form", onSubmit: submitDownload, "data-testid": "baidu-download-form" },
h("label", null, "文件 fs_id", h("input", { value: downloadForm.fsId, onChange: (event: any) => setDownloadForm((prev: any) => ({ ...prev, fsId: event.target.value })), placeholder: "从文件表填入" })),
h("label", null, "保存到 staging 路径", h("input", { value: downloadForm.localPath, onChange: (event: any) => setDownloadForm((prev: any) => ({ ...prev, localPath: event.target.value })), placeholder: "downloads/" })),
h("button", { type: "submit", className: "primary-btn", disabled: !loggedIn || !downloadForm.fsId || state.actionLoading }, "下载到 staging"),
),
),
state.selfTest ? h("div", { className: "baidu-account-card", "data-testid": "baidu-netdisk-self-test-result" },
h("div", { className: "node-version-line" },
h(StatusBadge, { status: state.selfTest.ok ? "online" : "warn" }, state.selfTest.ok ? "self-test ok" : "self-test"),
h("span", null, fmtBytes(state.selfTest.sizeBytes)),
),
h("h3", null, state.selfTest.remotePath || "Baidu self-test"),
h("div", { className: "microservice-ref-card" }, h("span", null, "fs_id"), h("strong", null, state.selfTest.fsId || "--"), h("code", null, state.selfTest.downloadedPath || "--")),
h("div", { className: "microservice-ref-card" }, h("span", null, "MD5"), h("strong", null, state.selfTest.downloadedMd5 || "--"), h("code", null, state.selfTest.expectedMd5 || "--")),
h(RawButton, { title: "Baidu Self Test", data: state.selfTest, onOpen: onRaw, testId: "raw-baidu-self-test" }),
) : null,
jobs.length === 0 ? h(EmptyState, { title: "暂无传输任务", text: "上传/下载任务会在后端容器内执行,避免大文件穿过 UniDesk proxy" }) :
h("div", { className: "table-wrap", "data-testid": "baidu-transfer-table" }, h("table", null,
h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "方向"), h("th", null, "路径"), h("th", null, "进度"), h("th", null, "时间"), h("th", null, "操作"))),
h("tbody", null, jobs.map((job: any) => h("tr", { key: job.id },
h("td", null, h(StatusBadge, { status: job.status }, job.status)),
h("td", null, job.direction),
h("td", null, h("strong", null, job.remotePath || job.fsId || "--"), h("code", null, job.localPath || "--"), job.error ? h("span", { className: "form-error" }, job.error) : null),
h("td", null, h(ProgressBar, { percent: job.progressPercent }), h("span", { className: "muted" }, `${fmtBytes(job.bytesDone)} / ${fmtBytes(job.sizeBytes)}`)),
h("td", null, fmtDate(job.updatedAt)),
h("td", null, h("div", { className: "inline-actions" },
["queued", "running"].includes(job.status) ? h("button", { type: "button", className: "ghost-btn", onClick: () => transferAction(job.id, "cancel") }, "取消") : null,
["failed", "canceled"].includes(job.status) ? h("button", { type: "button", className: "ghost-btn", onClick: () => transferAction(job.id, "retry") }, "重试") : null,
h(RawButton, { title: `Transfer ${job.id}`, data: job, onOpen: onRaw }),
)),
))),
)),
),
h(Panel, { title: "安全与日志", eyebrow: "redacted diagnostics", className: "baidu-wide-panel", loading: state.loading,
actions: h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "Baidu Health", data: health, onOpen: onRaw, testId: "raw-baidu-health" }), h(RawButton, { title: "Baidu Logs", data: state.logs, onOpen: onRaw, testId: "raw-baidu-logs" })),
},
h("div", { className: "policy-grid" },
h("article", null, h("b", null, "私有后端"), h("span", null, "4244 只在 Compose 网络 expose,浏览器经 UniDesk 同源代理访问")),
h("article", null, h("b", null, "Token 加密"), h("span", null, "access/refresh token 使用 BAIDU_NETDISK_TOKEN_KEY 加密后写入 PostgreSQL")),
h("article", null, h("b", null, "无浏览器大文件流"), h("span", null, "上传/下载以容器 staging 目录为边界,避免 proxy 文本通道传输大字节流")),
),
),
),
);
}