diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 849884b54..db659cc56 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,15 +1,13 @@ -## 概述 Descriptions +## Checklist / 检查清单 - - - +- [ ] Fixes mentioned issues / 修复已提及的问题 +- [ ] Code reviewed by human / 代码通过人工检查 +- [ ] Changes tested / 已完成测试 -## 变更内容 Changes +## Description / 描述 - - + -### 截图 Screenshots +## Screenshots / 截图 - - + diff --git a/.gitignore b/.gitignore index bfb41a63c..143763ebb 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ CLAUDE.md test-results playwright-report + +superpowers diff --git a/packages/filesystem/factory.ts b/packages/filesystem/factory.ts index bdeb26e16..99dfa5d0e 100644 --- a/packages/filesystem/factory.ts +++ b/packages/filesystem/factory.ts @@ -8,6 +8,7 @@ import ZipFileSystem from "./zip/zip"; import S3FileSystem from "./s3/s3"; import { t } from "@App/locales/locales"; import LimiterFileSystem from "./limiter"; +import type { WebDAVClientOptions, OAuthToken } from "webdav"; export type FileSystemType = "zip" | "webdav" | "baidu-netdsik" | "onedrive" | "googledrive" | "dropbox" | "s3"; @@ -16,18 +17,47 @@ export type FileSystemParams = { title: string; type?: "select" | "authorize" | "password"; options?: string[]; + visibilityFor?: string[]; + minWidth?: string; }; }; export default class FileSystemFactory { static create(type: FileSystemType, params: any): Promise { let fs: FileSystem; + let options; switch (type) { case "zip": fs = new ZipFileSystem(params); break; case "webdav": - fs = new WebDAVFileSystem(params.authType, params.url, params.username, params.password); + /* + Auto = "auto", + Digest = "digest", // 需要避免密码直接传输 + None = "none", // 公开资源 / 自定义认证 + Password = "password", // 普通 WebDAV 服务,需要确保 HTTPS / Nextcloud 生产环境 + Token = "token" // OAuth2 / 现代云服务 / Nextcloud 生产环境 + */ + if (params.authType === "none") { + options = { + authType: params.authType, + } satisfies WebDAVClientOptions; + } else if (params.authType === "token") { + options = { + authType: params.authType, + token: { + token_type: "Bearer", + access_token: params.accessToken, + } satisfies OAuthToken, + } satisfies WebDAVClientOptions; + } else { + options = { + authType: params.authType || "auto", // UI 问题,有undefined机会。undefined等价于 password, 但此处用 webdav 本身的 auto 侦测算了 + username: params.username, + password: params.password, + } satisfies WebDAVClientOptions; + } + fs = WebDAVFileSystem.fromCredentials(params.url, options); break; case "baidu-netdsik": fs = new BaiduFileSystem(); @@ -64,10 +94,12 @@ export default class FileSystemFactory { title: t("auth_type"), type: "select", options: ["password", "digest", "none", "token"], + minWidth: "140px", }, url: { title: t("url") }, - username: { title: t("username") }, - password: { title: t("password"), type: "password" }, + username: { title: t("username"), visibilityFor: ["password", "digest"] }, + password: { title: t("password"), type: "password", visibilityFor: ["password", "digest"] }, + accessToken: { title: t("access_token_bearer"), visibilityFor: ["token"] }, }, "baidu-netdsik": {}, onedrive: {}, diff --git a/packages/filesystem/webdav/webdav.test.ts b/packages/filesystem/webdav/webdav.test.ts new file mode 100644 index 000000000..cf389c0d0 --- /dev/null +++ b/packages/filesystem/webdav/webdav.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { WebDAVClient } from "webdav"; +import { getPatcher } from "webdav"; +import WebDAVFileSystem from "./webdav"; +import { WarpTokenError } from "../error"; + +/** 创建 mock WebDAVClient */ +function createMockClient(overrides?: Partial): WebDAVClient { + return { + getQuota: vi.fn().mockResolvedValue({}), + getDirectoryContents: vi.fn().mockResolvedValue([]), + getFileContents: vi.fn().mockResolvedValue("content"), + putFileContents: vi.fn().mockResolvedValue(true), + createDirectory: vi.fn().mockResolvedValue(undefined), + deleteFile: vi.fn().mockResolvedValue(undefined), + ...overrides, + } as unknown as WebDAVClient; +} + +/** 创建可测试的 WebDAVFileSystem 实例(替换 client 为 mock) */ +function createTestFS(mockClient: WebDAVClient, url = "https://dav.example.com"): WebDAVFileSystem { + const fs = WebDAVFileSystem.fromCredentials(url, {}); + fs.client = mockClient; + return fs; +} + +describe("WebDAVFileSystem", () => { + let mockClient: WebDAVClient; + + beforeEach(() => { + vi.clearAllMocks(); + mockClient = createMockClient(); + }); + + describe("initWebDAVPatch", () => { + it("应当通过 getPatcher 注册 fetch patch,设置 credentials 为 omit", () => { + // fromCredentials 内部调用 initWebDAVPatch,验证 patcher 已注册 fetch + WebDAVFileSystem.fromCredentials("https://dav.example.com", {}); + + const patcher = getPatcher(); + // 验证 fetch 已被 patch(patcher 内部有 fetch 注册) + expect(patcher.isPatched("fetch")).toBe(true); + }); + }); + + describe("fromCredentials", () => { + it("应当创建 WebDAVFileSystem 实例并设置 url 和 basePath", () => { + const fs = WebDAVFileSystem.fromCredentials("https://dav.example.com", { + authType: "password" as any, + username: "user", + password: "pass", + }); + + expect(fs).toBeInstanceOf(WebDAVFileSystem); + expect(fs.url).toBe("https://dav.example.com"); + expect(fs.basePath).toBe("/"); + }); + }); + + describe("fromSameClient", () => { + it("应当复用已有 client 并设置新 basePath", () => { + const fs = createTestFS(mockClient); + const subFs = WebDAVFileSystem.fromSameClient(fs, "/subdir"); + + expect(subFs).toBeInstanceOf(WebDAVFileSystem); + expect(subFs.url).toBe("https://dav.example.com"); + expect(subFs.basePath).toBe("/subdir"); + expect(subFs.client).toBe(mockClient); + }); + }); + + describe("verify", () => { + it("应当成功验证", async () => { + const fs = createTestFS(mockClient); + + await expect(fs.verify()).resolves.toBeUndefined(); + expect(mockClient.getQuota).toHaveBeenCalled(); + }); + + it("应当在 401 时抛出 WarpTokenError", async () => { + (mockClient.getQuota as ReturnType).mockRejectedValue({ + response: { status: 401 }, + message: "Unauthorized", + }); + const fs = createTestFS(mockClient); + + await expect(fs.verify()).rejects.toBeInstanceOf(WarpTokenError); + }); + + it("应当在其他错误时抛出包含原始信息的 Error", async () => { + (mockClient.getQuota as ReturnType).mockRejectedValue({ + message: "Network error", + }); + const fs = createTestFS(mockClient); + + await expect(fs.verify()).rejects.toThrow("WebDAV verify failed: Network error"); + }); + }); + + describe("openDir", () => { + it("应当返回新实例并拼接路径", async () => { + const fs = createTestFS(mockClient); + const subFs = (await fs.openDir("docs")) as WebDAVFileSystem; + + expect(subFs).toBeInstanceOf(WebDAVFileSystem); + expect(subFs.basePath).toBe("/docs"); + expect(subFs.client).toBe(mockClient); + }); + + it("应当支持嵌套 openDir", async () => { + const fs = createTestFS(mockClient); + const sub1 = (await fs.openDir("a")) as WebDAVFileSystem; + const sub2 = (await sub1.openDir("b")) as WebDAVFileSystem; + + expect(sub2.basePath).toBe("/a/b"); + }); + }); + + describe("createDir", () => { + it("应当调用 createDirectory", async () => { + const fs = createTestFS(mockClient); + + await fs.createDir("new-folder"); + + expect(mockClient.createDirectory).toHaveBeenCalledWith("/new-folder"); + }); + + it("应当在 405 错误时静默成功(目录已存在)", async () => { + (mockClient.createDirectory as ReturnType).mockRejectedValue({ + response: { status: 405 }, + message: "405 Method Not Allowed", + }); + const fs = createTestFS(mockClient); + + await expect(fs.createDir("existing")).resolves.toBeUndefined(); + }); + + it("应当在 message 包含 405 时也静默成功", async () => { + (mockClient.createDirectory as ReturnType).mockRejectedValue({ + message: "Request failed with status code 405", + }); + const fs = createTestFS(mockClient); + + await expect(fs.createDir("existing")).resolves.toBeUndefined(); + }); + + it("应当在其他错误时抛出异常", async () => { + const err = new Error("Forbidden"); + (mockClient.createDirectory as ReturnType).mockRejectedValue(err); + const fs = createTestFS(mockClient); + + await expect(fs.createDir("denied")).rejects.toThrow("Forbidden"); + }); + }); + + describe("delete", () => { + it("应当调用 deleteFile", async () => { + const fs = createTestFS(mockClient); + + await fs.delete("test.txt"); + + expect(mockClient.deleteFile).toHaveBeenCalledWith("/test.txt"); + }); + }); + + describe("list", () => { + it("应当列出文件并过滤目录", async () => { + (mockClient.getDirectoryContents as ReturnType).mockResolvedValue([ + { + type: "file", + basename: "test.txt", + lastmod: "2024-01-01T00:00:00Z", + etag: '"abc"', + size: 1024, + }, + { + type: "directory", + basename: "subdir", + lastmod: "2024-01-01T00:00:00Z", + etag: "", + size: 0, + }, + ]); + const fs = createTestFS(mockClient); + + const files = await fs.list(); + + expect(files).toHaveLength(1); + expect(files[0]).toMatchObject({ + name: "test.txt", + path: "/", + digest: '"abc"', + size: 1024, + }); + }); + + it("应当在 404 时返回空数组", async () => { + (mockClient.getDirectoryContents as ReturnType).mockRejectedValue({ + response: { status: 404 }, + }); + const fs = createTestFS(mockClient); + + const files = await fs.list(); + expect(files).toHaveLength(0); + }); + + it("应当在其他错误时抛出异常", async () => { + const err = new Error("Server Error"); + (err as any).response = { status: 500 }; + (mockClient.getDirectoryContents as ReturnType).mockRejectedValue(err); + const fs = createTestFS(mockClient); + + await expect(fs.list()).rejects.toThrow("Server Error"); + }); + }); + + describe("getDirUrl", () => { + it("应当返回 url + basePath", async () => { + const fs = createTestFS(mockClient); + const subFs = (await fs.openDir("docs")) as WebDAVFileSystem; + + expect(await subFs.getDirUrl()).toBe("https://dav.example.com/docs"); + }); + + it("根路径应返回 url + /", async () => { + const fs = createTestFS(mockClient); + + expect(await fs.getDirUrl()).toBe("https://dav.example.com/"); + }); + }); +}); diff --git a/packages/filesystem/webdav/webdav.ts b/packages/filesystem/webdav/webdav.ts index a3dde3e86..10ea10b21 100644 --- a/packages/filesystem/webdav/webdav.ts +++ b/packages/filesystem/webdav/webdav.ts @@ -1,11 +1,28 @@ -import type { AuthType, FileStat, WebDAVClient } from "webdav"; -import { createClient } from "webdav"; +import type { FileStat, WebDAVClient, WebDAVClientOptions } from "webdav"; +import { createClient, getPatcher } from "webdav"; import type FileSystem from "../filesystem"; import type { FileInfo, FileCreateOptions, FileReader, FileWriter } from "../filesystem"; import { joinPath } from "../utils"; import { WebDAVFileReader, WebDAVFileWriter } from "./rw"; import { WarpTokenError } from "../error"; +// 禁止 WebDAV 请求携带浏览器 cookies,只通过账号密码认证 (#1297) +// 全局单次注册 +let patchInited = false; +const initWebDAVPatch = () => { + if (patchInited) return; + patchInited = true; + return getPatcher().patch("fetch", (...args: unknown[]) => { + const options = (args[1] as RequestInit) || {}; + const headers = new Headers((options.headers as HeadersInit) || {}); + return fetch(args[0] as RequestInfo | URL, { + ...options, + headers, + credentials: "omit", + }); + }); +}; + export default class WebDAVFileSystem implements FileSystem { client: WebDAVClient; @@ -13,29 +30,36 @@ export default class WebDAVFileSystem implements FileSystem { basePath: string = "/"; - constructor(authType: AuthType | WebDAVClient, url?: string, username?: string, password?: string) { - if (typeof authType === "object") { - this.client = authType; - this.basePath = joinPath(url || ""); - this.url = username!; - } else { - this.url = url!; - this.client = createClient(url!, { - authType, - username, - password, - }); - } + static fromCredentials(url: string, options: WebDAVClientOptions) { + initWebDAVPatch(); + options = { + ...options, + headers: { + "X-Requested-With": "XMLHttpRequest", // Nextcloud 等需要 + // "requesttoken": csrfToken, // 按账号各自传入 + }, + }; + return new WebDAVFileSystem(createClient(url, options), url, "/"); + } + + static fromSameClient(fs: WebDAVFileSystem, basePath: string) { + return new WebDAVFileSystem(fs.client, fs.url, basePath); + } + + private constructor(client: WebDAVClient, url: string, basePath: string) { + this.client = client; + this.url = url; + this.basePath = basePath; } async verify(): Promise { try { await this.client.getQuota(); } catch (e: any) { - if (e.response && e.response.status === 401) { + if (e.response?.status === 401) { throw new WarpTokenError(e); } - throw new Error("verify failed"); + throw new Error(`WebDAV verify failed: ${e.message}`); // 保留原始信息 } } @@ -44,7 +68,7 @@ export default class WebDAVFileSystem implements FileSystem { } async openDir(path: string): Promise { - return new WebDAVFileSystem(this.client, joinPath(this.basePath, path), this.url); + return WebDAVFileSystem.fromSameClient(this, joinPath(this.basePath, path)); } async create(path: string, _opts?: FileCreateOptions): Promise { @@ -56,7 +80,7 @@ export default class WebDAVFileSystem implements FileSystem { await this.client.createDirectory(joinPath(this.basePath, path)); } catch (e: any) { // 如果是405错误,则忽略 - if (e.message.includes("405")) { + if (e.response?.status === 405 || e.message?.includes("405")) { return; } throw e; @@ -68,7 +92,13 @@ export default class WebDAVFileSystem implements FileSystem { } async list(): Promise { - const dir = (await this.client.getDirectoryContents(this.basePath)) as FileStat[]; + let dir: FileStat[]; + try { + dir = (await this.client.getDirectoryContents(this.basePath)) as FileStat[]; + } catch (e: any) { + if (e.response?.status === 404) return [] as FileInfo[]; // 目录不存在视为空 + throw e; + } const ret: FileInfo[] = []; for (const item of dir) { if (item.type !== "file") { diff --git a/packages/message/server.ts b/packages/message/server.ts index e69e7f0e7..f46347709 100644 --- a/packages/message/server.ts +++ b/packages/message/server.ts @@ -104,6 +104,19 @@ type ApiFunction = (params: any, con: IGetSender) => Promise | any | void; type ApiFunctionSync = (params: any, con: IGetSender) => any; type MiddlewareFunction = (params: any, con: IGetSender, next: () => Promise | any) => Promise | any; +const formatErrorToClient = (e: any) => { + if (!e) return `${e}`; + if (typeof e?.message === "string") return e.message; + if (typeof e === "object") { + try { + return JSON.stringify(e); + } catch { + // ignored + } + } + return e.toString(); +}; + export class Server { private apiFunctionMap: Map = new Map(); @@ -159,7 +172,7 @@ export class Server { data && con.sendMessage({ code: 0, data }); }) .catch((e: Error) => { - con.sendMessage({ code: -1, message: e.message || e.toString() }); + con.sendMessage({ code: -1, message: formatErrorToClient(e) }); this.logger.error("connectHandle error", Logger.E(e)); }); return true; @@ -191,7 +204,7 @@ export class Server { } }) .catch((e: Error) => { - sendResponse({ code: -1, message: e.message || e.toString() }); + sendResponse({ code: -1, message: formatErrorToClient(e) }); this.logger.error("messageHandle error", Logger.E(e)); }); return true; @@ -199,7 +212,7 @@ export class Server { sendResponse({ code: 0, data: ret }); } } catch (e: any) { - sendResponse({ code: -1, message: e.message || e.toString() }); + sendResponse({ code: -1, message: formatErrorToClient(e) }); this.logger.error("messageHandle error", Logger.E(e)); } } else { diff --git a/src/locales/ach-UG/translation.json b/src/locales/ach-UG/translation.json index e6f3850b5..e72aa1844 100644 --- a/src/locales/ach-UG/translation.json +++ b/src/locales/ach-UG/translation.json @@ -73,7 +73,7 @@ "deleting": "crwdns8020:0crwdne8020:0", "backup_strategy": "crwdns8022:0crwdne8022:0", "under_construction": "crwdns8024:0crwdne8024:0", - "development_debugging": "crwdns8026:0crwdne8026:0", + "development_tool": "crwdns8026:0crwdne8026:0", "vscode_url": "crwdns8028:0crwdne8028:0", "auto_connect_vscode_service": "crwdns8030:0crwdne8030:0", "connect": "crwdns8032:0crwdne8032:0", @@ -378,6 +378,7 @@ "url": "crwdns8544:0crwdne8544:0", "username": "crwdns8546:0crwdne8546:0", "password": "crwdns8548:0crwdne8548:0", + "access_token_bearer": "Access Token (Bearer)", "s3_bucket_name": "Bucket Name", "s3_region": "Region", "s3_access_key_id": "Access Key ID", diff --git a/src/locales/de-DE/translation.json b/src/locales/de-DE/translation.json index 9df67cfd8..396f65f73 100644 --- a/src/locales/de-DE/translation.json +++ b/src/locales/de-DE/translation.json @@ -74,7 +74,7 @@ "deleting": "Wird gelöscht", "backup_strategy": "Sicherungsstrategie", "under_construction": "Im Aufbau", - "development_debugging": "Entwicklungsdebugging", + "development_tool": "Entwicklungstool", "vscode_url": "VSCode-Adresse", "auto_connect_vscode_service": "Automatisch mit VSCode-Service verbinden", "connect": "Verbinden", @@ -387,6 +387,7 @@ "url": "URL", "username": "Benutzername", "password": "Passwort", + "access_token_bearer": "Zugriffstoken (Bearer)", "s3_bucket_name": "Bucket-Name", "s3_region": "Region", "s3_access_key_id": "Zugriffs-Schlüssel-ID", diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 9769ef7f7..4068a909e 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -74,7 +74,7 @@ "deleting": "Deleting", "backup_strategy": "Backup Strategy", "under_construction": "Under Construction", - "development_debugging": "Development Debugging", + "development_tool": "Development Tool", "vscode_url": "VSCode URL", "auto_connect_vscode_service": "Auto Connect VSCode Service", "connect": "Connect", @@ -387,6 +387,7 @@ "url": "URL", "username": "Username", "password": "Password", + "access_token_bearer": "Access Token (Bearer)", "s3_bucket_name": "Bucket Name", "s3_region": "Region", "s3_access_key_id": "Access Key ID", diff --git a/src/locales/ja-JP/translation.json b/src/locales/ja-JP/translation.json index 823949c52..379ecf612 100644 --- a/src/locales/ja-JP/translation.json +++ b/src/locales/ja-JP/translation.json @@ -74,7 +74,7 @@ "deleting": "削除中", "backup_strategy": "バックアップ戦略", "under_construction": "建設中", - "development_debugging": "開発デバッグ", + "development_tool": "開発ツール", "vscode_url": "VSCodeアドレス", "auto_connect_vscode_service": "VSCodeサービスに自動接続", "connect": "接続", @@ -387,6 +387,7 @@ "url": "URL", "username": "ユーザー名", "password": "パスワード", + "access_token_bearer": "アクセストークン(Bearer)", "s3_bucket_name": "バケット名", "s3_region": "リージョン", "s3_access_key_id": "アクセスキーID", diff --git a/src/locales/ru-RU/translation.json b/src/locales/ru-RU/translation.json index 1b6b72973..aaf8c51dc 100644 --- a/src/locales/ru-RU/translation.json +++ b/src/locales/ru-RU/translation.json @@ -74,7 +74,7 @@ "deleting": "Удаление", "backup_strategy": "Стратегия резервного копирования", "under_construction": "В разработке", - "development_debugging": "Отладка разработки", + "development_tool": "Инструмент разработки", "vscode_url": "Адрес VSCode", "auto_connect_vscode_service": "Автоматически подключаться к службе VSCode", "connect": "Подключить", @@ -387,6 +387,7 @@ "url": "URL", "username": "Имя пользователя", "password": "Пароль", + "access_token_bearer": "Токен доступа (Bearer)", "s3_bucket_name": "Имя корзины", "s3_region": "Регион", "s3_access_key_id": "Идентификатор ключа доступа", diff --git a/src/locales/vi-VN/translation.json b/src/locales/vi-VN/translation.json index c69cb10be..ebbcd8626 100644 --- a/src/locales/vi-VN/translation.json +++ b/src/locales/vi-VN/translation.json @@ -74,7 +74,7 @@ "deleting": "Đang xóa", "backup_strategy": "Chiến lược sao lưu", "under_construction": "Đang xây dựng", - "development_debugging": "Gỡ lỗi phát triển", + "development_tool": "Công cụ phát triển", "vscode_url": "Url vscode", "auto_connect_vscode_service": "Tự động kết nối dịch vụ vscode", "connect": "Kết nối", @@ -387,6 +387,7 @@ "url": "Url", "username": "Tên người dùng", "password": "Mật khẩu", + "access_token_bearer": "Mã truy cập (Bearer)", "s3_bucket_name": "Tên Bucket", "s3_region": "Vùng", "s3_access_key_id": "ID Khóa Truy Cập", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 1267d3472..99377ea67 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -74,7 +74,7 @@ "deleting": "删除中", "backup_strategy": "备份策略", "under_construction": "建设中", - "development_debugging": "开发调试", + "development_tool": "开发工具", "vscode_url": "VSCode地址", "auto_connect_vscode_service": "自动连接vscode服务", "connect": "连接", @@ -387,6 +387,7 @@ "url": "URL", "username": "用户名", "password": "密码", + "access_token_bearer": "访问令牌(Bearer)", "s3_bucket_name": "存储桶名称", "s3_region": "区域", "s3_access_key_id": "访问密钥 ID", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index a56ddd57e..65e6af433 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -74,7 +74,7 @@ "deleting": "刪除中", "backup_strategy": "備份策略", "under_construction": "建設中", - "development_debugging": "開發除錯", + "development_tool": "開發工具", "vscode_url": "VSCode網址", "auto_connect_vscode_service": "自動連接VSCode服務", "connect": "連接", @@ -387,6 +387,7 @@ "url": "網址", "username": "使用者名稱", "password": "密碼", + "access_token_bearer": "存取權杖(Bearer)", "s3_bucket_name": "儲存貯體名稱", "s3_region": "區域", "s3_access_key_id": "存取金鑰 ID", diff --git a/src/pages/components/FileSystemParams/index.tsx b/src/pages/components/FileSystemParams/index.tsx index ebf74473a..2cec8cfb9 100644 --- a/src/pages/components/FileSystemParams/index.tsx +++ b/src/pages/components/FileSystemParams/index.tsx @@ -65,6 +65,7 @@ const FileSystemParams: React.FC<{ ]; const netDiskName = netDiskType ? fileSystemList.find((item) => item.key === fileSystemType)?.name : null; + const fsParam = fsParams[fileSystemType]; return ( <> @@ -110,58 +111,66 @@ const FileSystemParams: React.FC<{ marginTop: 4, }} > - {Object.keys(fsParams[fileSystemType]).map((key) => ( -
- {fsParams[fileSystemType][key].type === "select" && ( - <> - {fsParams[fileSystemType][key].title} - - - )} - {fsParams[fileSystemType][key].type === "password" && ( - <> - {fsParams[fileSystemType][key].title} - { - onChangeFileSystemParams({ - ...fileSystemParams, - [key]: value, - }); - }} - /> - - )} - {!fsParams[fileSystemType][key].type && ( - <> - {fsParams[fileSystemType][key].title} - { - onChangeFileSystemParams({ - ...fileSystemParams, - [key]: value, - }); - }} - /> - - )} -
- ))} + {Object.keys(fsParam).map((key) => { + const props = fsParam[key]; + const selectAuth = fsParam?.authType?.options?.[0]; // webDAV + if (selectAuth && props?.visibilityFor?.includes(fileSystemParams?.authType || selectAuth) === false) { + return null; + } + return ( +
+ {props.type === "select" && ( + <> + {props.title} + + + )} + {props.type === "password" && ( + <> + {props.title} + { + onChangeFileSystemParams({ + ...fileSystemParams, + [key]: value, + }); + }} + /> + + )} + {!props.type && ( + <> + {props.title} + { + onChangeFileSystemParams({ + ...fileSystemParams, + [key]: value, + }); + }} + /> + + )} +
+ ); + })} ); diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 6bbb48d51..efea71d29 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -67,7 +67,6 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any const response = await fetch(url, { headers: { "Cache-Control": "no-cache", - Accept: "text/javascript,application/javascript,text/plain,application/octet-stream,application/force-download", // 参考:加权 Accept-Encoding 值说明 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding#weighted_accept-encoding_values "Accept-Encoding": "br;q=1.0, gzip;q=0.8, *;q=0.1", @@ -83,10 +82,6 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any if (!response.body || !response.headers) { throw new Error("No response body or headers"); } - if (response.headers.get("content-type")?.includes("text/html")) { - throw new Error("Response is text/html, not a valid UserScript"); - } - const reader = response.body.getReader(); // 读取数据 diff --git a/src/pages/options/routes/Tools.tsx b/src/pages/options/routes/Tools.tsx index aa844e504..b55d0265a 100644 --- a/src/pages/options/routes/Tools.tsx +++ b/src/pages/options/routes/Tools.tsx @@ -286,7 +286,7 @@ function Tools() { - {t("development_debugging")} + {t("development_tool")}