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; const h = React.createElement; const { useEffect } = React; const useState: any = React.useState; function requestJson(path: string, options: AnyRecord = {}): Promise { 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 { 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 { const transfers = await requestJson(baiduApi(apiBaseUrl, "/api/transfers?limit=80")); setState((prev: any) => ({ ...prev, transfers })); } async function load(): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 文本通道传输大字节流")), ), ), ), ); }