From 29c949c351a42e0064d2a89007cf8c30ce945dd4 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:42:06 +0900 Subject: [PATCH 01/30] =?UTF-8?q?=E4=B8=8D=E4=BE=9D=E8=B5=96=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E7=BD=91=E7=AB=99=E8=AE=BF=E9=97=AE=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/cache.ts | 13 + src/app/service/service_worker/script.ts | 51 +- src/locales/ach-UG/translation.json | 5 +- src/locales/de-DE/translation.json | 5 +- src/locales/en-US/translation.json | 5 +- src/locales/ja-JP/translation.json | 5 +- src/locales/ru-RU/translation.json | 5 +- src/locales/vi-VN/translation.json | 5 +- src/locales/zh-CN/translation.json | 5 +- src/locales/zh-TW/translation.json | 5 +- src/manifest.json | 11 +- src/pages/components/CodeEditor/index.tsx | 2 + src/pages/install/App.tsx | 588 ++++++++++++++++------ src/pages/install/index.css | 89 +++- src/pages/install/main.tsx | 10 +- 15 files changed, 597 insertions(+), 207 deletions(-) diff --git a/src/app/cache.ts b/src/app/cache.ts index 4d304442f..9ac047288 100644 --- a/src/app/cache.ts +++ b/src/app/cache.ts @@ -81,6 +81,19 @@ class ExtCache implements CacheStorage { }); } + dels(keys: string[]): Promise { + return new Promise((resolve) => { + chrome.storage.session.remove(keys, () => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.storage.session.remove:", lastError); + // 无视storage API错误,继续执行 + } + resolve(); + }); + }); + } + clear(): Promise { return new Promise((resolve) => { chrome.storage.session.clear(() => { diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index e946e4804..ec4931f40 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -28,9 +28,7 @@ import { type ResourceService } from "./resource"; import { type ValueService } from "./value"; import { compileScriptCode, isEarlyStartScript } from "../content/utils"; import { type SystemConfig } from "@App/pkg/config/config"; -import { localePath } from "@App/locales/locales"; import { arrayMove } from "@dnd-kit/sortable"; -import { DocumentationSite } from "@App/app/const"; import type { TScriptRunStatus, TDeleteScript, @@ -81,12 +79,13 @@ export class ScriptService { listenerScriptInstall() { // 初始化脚本安装监听 - chrome.webRequest.onBeforeRequest.addListener( - (req: chrome.webRequest.OnBeforeRequestDetails) => { - // 处理url, 实现安装脚本 - if (req.method !== "GET") { - return undefined; + chrome.webNavigation.onBeforeNavigate.addListener( + (req: chrome.webNavigation.WebNavigationParentedCallbackDetails) => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error(lastError.message); } + // 处理url, 实现安装脚本 let targetUrl: string | null = null; // 判断是否为 file:///*/*.user.js if (req.url.startsWith("file://") && req.url.endsWith(".user.js")) { @@ -150,13 +149,12 @@ export class ScriptService { }); }, { - urls: [ - `${DocumentationSite}/docs/script_installation/*`, - `${DocumentationSite}/en/docs/script_installation/*`, - "https://www.tampermonkey.net/script_installation.php*", - "file:///*/*.user.js*", + url: [ + { schemes: ["http", "https"], hostEquals: "docs.scriptcat.org", pathPrefix: "/docs/script_installation/" }, + { schemes: ["http", "https"], hostEquals: "docs.scriptcat.org", pathPrefix: "/en/docs/script_installation/" }, + { schemes: ["http", "https"], hostEquals: "www.tampermonkey.net", pathPrefix: "/script_installation.php" }, + { schemes: ["file"], pathSuffix: ".user.js" }, ], - types: ["main_frame"], } ); // 兼容 chrome 内核 < 128 处理 @@ -187,7 +185,7 @@ export class ScriptService { action: { type: "redirect" as chrome.declarativeNetRequest.RuleActionType, redirect: { - regexSubstitution: `${DocumentationSite}${localePath}/docs/script_installation/#url=\\0`, + regexSubstitution: `chrome-extension://${chrome.runtime.id}/src/install.html?url=\\0`, }, }, condition: condition, @@ -206,18 +204,10 @@ export class ScriptService { } public async openInstallPageByUrl(url: string, source: InstallSource): Promise<{ success: boolean; msg: string }> { - const uuid = uuidv4(); try { - await this.openUpdateOrInstallPage(uuid, url, source, false); - timeoutExecution( - `${cIdKey}_cleanup_${uuid}`, - () => { - // 清理缓存 - cacheInstance.del(`${CACHE_KEY_SCRIPT_INFO}${uuid}`); - }, - 30 * 1000 - ); - await openInCurrentTab(`/src/install.html?uuid=${uuid}`); + const installPageUrl = await this.getInstallPageUrl(url, source); + if (!installPageUrl) throw new Error("getInstallPageUrl failed"); + await openInCurrentTab(installPageUrl); return { success: true, msg: "" }; } catch (err: any) { console.error(err); @@ -225,6 +215,17 @@ export class ScriptService { } } + public async getInstallPageUrl(url: string, source: InstallSource): Promise { + const uuid = uuidv4(); + try { + await this.openUpdateOrInstallPage(uuid, url, source, false); + return `/src/install.html?uuid=${uuid}`; + } catch (err: any) { + console.error(err); + return ""; + } + } + // 直接通过url静默安装脚本 async installByUrl(url: string, source: InstallSource, subscribeUrl?: string) { const uuid = uuidv4(); diff --git a/src/locales/ach-UG/translation.json b/src/locales/ach-UG/translation.json index 6f172224c..843111e9f 100644 --- a/src/locales/ach-UG/translation.json +++ b/src/locales/ach-UG/translation.json @@ -282,7 +282,8 @@ "script_requires": "crwdns8440:0crwdne8440:0", "cookie_warning": "crwdns8442:0crwdne8442:0", "scheduled_script_description_title": "crwdns8706:0crwdne8706:0", - "scheduled_script_description_description": "crwdns8708:0{{expression}}crwdnd8708:0{{time}}crwdne8708:0", + "scheduled_script_description_description_expr": "crwdns8708:0", + "scheduled_script_description_description_next": "crwdns8708:0", "background_script_description": "crwdns8444:0crwdne8444:0", "install_success": "crwdns8446:0crwdne8446:0", "install": { @@ -322,6 +323,8 @@ "status_autoclose": "crwdns12778:0crwdne12778:0", "header_other_update": "crwdns12784:0crwdne12784:0" }, + "install_page_loading": "Installation page loading", + "invalid_page": "Invalid page", "background_script_tag": "crwdns8460:0crwdne8460:0", "scheduled_script_tag": "crwdns8462:0crwdne8462:0", "background_script": "crwdns8464:0crwdne8464:0", diff --git a/src/locales/de-DE/translation.json b/src/locales/de-DE/translation.json index 439d29c0c..9f38a61f2 100644 --- a/src/locales/de-DE/translation.json +++ b/src/locales/de-DE/translation.json @@ -282,7 +282,8 @@ "script_requires": "Skript referenziert die folgenden externen Ressourcen", "cookie_warning": "Achtung: Dieses Skript beantragt Cookie-Operationsberechtigung. Dies ist eine gefährliche Berechtigung, bitte stellen Sie die Sicherheit des Skripts sicher.", "scheduled_script_description_title": "Dies ist ein geplantes Skript. Wenn aktiviert, wird es zu bestimmten Zeiten automatisch ausgeführt und kann im Panel manuell gesteuert werden.", - "scheduled_script_description_description": "Geplante Aufgaben-Ausdruck: {{expression}}, letzte Ausführungszeit: {{time}}", + "scheduled_script_description_description_expr": "Geplante Aufgaben-Ausdruck", + "scheduled_script_description_description_next": "Letzte Ausführungszeit", "background_script_description": "Dies ist ein Hintergrundskript. Wenn aktiviert, wird es automatisch einmal ausgeführt, wenn der Browser geöffnet wird, und kann im Panel manuell gesteuert werden.", "install_success": "Installation erfolgreich", "install": { @@ -322,6 +323,8 @@ "status_autoclose": "Automatisches Schließen in $0 Sekunden", "header_other_update": "Andere verfügbare Updates" }, + "install_page_loading": "Installationsseite wird geladen", + "invalid_page": "Ungültige Seite", "background_script_tag": "Dies ist ein Hintergrundskript", "scheduled_script_tag": "Dies ist ein geplantes Skript", "background_script": "Hintergrundskript", diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 7d15d9e30..c7d1a28be 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -282,7 +282,8 @@ "script_requires": "Script requires the following external resources", "cookie_warning": "Please note, this script requests access to Cookie permissions, which is a dangerous permission. Please verify the security of the script.", "scheduled_script_description_title": "This is a scheduled script, which will automatically run at a specific time once enabled and can be manually controlled in the panel.", - "scheduled_script_description_description": "Scheduled task expression: {{expression}}, most recent run time: {{time}}", + "scheduled_script_description_description_expr": "Scheduled task expression:", + "scheduled_script_description_description_next": "Most recent run time:", "background_script_description": "This is a background script, which will automatically run once when the browser opens once enabled, and can be manually controlled in the panel.", "install_success": "Install Successful", "install": { @@ -322,6 +323,8 @@ "status_autoclose": "Auto-closing in $0 seconds", "header_other_update": "Other available updates" }, + "install_page_loading": "Installation page loading", + "invalid_page": "Invalid page", "background_script_tag": "This is a Background Script", "scheduled_script_tag": "This is a Scheduled Script", "background_script": "Background Script", diff --git a/src/locales/ja-JP/translation.json b/src/locales/ja-JP/translation.json index 0d2bd7b12..a254eb088 100644 --- a/src/locales/ja-JP/translation.json +++ b/src/locales/ja-JP/translation.json @@ -282,7 +282,8 @@ "script_requires": "スクリプトは以下の外部リソースを参照しています", "cookie_warning": "注意:このスクリプトはCookieの操作権限を申請します。これは危険な権限ですので、スクリプトの安全性を確認してください。", "scheduled_script_description_title": "これはスケジュールスクリプトです。有効にすると特定の時間に自動実行され、手動操作も可能です。", - "scheduled_script_description_description": "スケジュールタスク式:{{expression}}、最近の実行時間:{{time}}", + "scheduled_script_description_description_expr": "スケジュールタスク表現:", + "scheduled_script_description_description_next": "最近の実行時間:", "background_script_description": "これはバックグラウンドスクリプトです。有効にするとブラウザを開いたときに自動的に一度実行され、パネルで手動制御できます。", "install_success": "インストールに成功しました", "install": { @@ -322,6 +323,8 @@ "status_autoclose": "$0 秒後に自動的に閉じます", "header_other_update": "その他の利用可能な更新" }, + "install_page_loading": "インストールページを読み込み中", + "invalid_page": "無効なページ", "background_script_tag": "これはバックグラウンドスクリプトです", "scheduled_script_tag": "これはスケジュールスクリプトです", "background_script": "バックグラウンドスクリプト", diff --git a/src/locales/ru-RU/translation.json b/src/locales/ru-RU/translation.json index b79ffbc09..6d3b3cb7a 100644 --- a/src/locales/ru-RU/translation.json +++ b/src/locales/ru-RU/translation.json @@ -282,7 +282,8 @@ "script_requires": "Скрипт ссылается на следующие внешние ресурсы", "cookie_warning": "Обратите внимание, что этот скрипт запрашивает разрешения на операции с Cookie. Это опасное разрешение, пожалуйста, убедитесь в безопасности скрипта.", "scheduled_script_description_title": "Это запланированный скрипт. После включения он будет автоматически выполняться в определенное время и может управляться вручную с панели.", - "scheduled_script_description_description": "Выражение планировщика: {{expression}}, последнее время выполнения: {{time}}", + "scheduled_script_description_description_expr": "Выражение планировщика", + "scheduled_script_description_description_next": "Последнее время выполнения:", "background_script_description": "Это фоновый скрипт. После включения он будет автоматически выполняться один раз при открытии браузера и может управляться вручную с панели.", "install_success": "Установка успешна", "install": { @@ -322,6 +323,8 @@ "status_autoclose": "Автоматическое закрытие через $0 сек.", "header_other_update": "Другие доступные обновления" }, + "install_page_loading": "Загрузка страницы установки", + "invalid_page": "Недействительная страница", "background_script_tag": "Это фоновый скрипт", "scheduled_script_tag": "Это запланированный скрипт", "background_script": "Фоновый скрипт", diff --git a/src/locales/vi-VN/translation.json b/src/locales/vi-VN/translation.json index 1728d6bea..b9c93e3ae 100644 --- a/src/locales/vi-VN/translation.json +++ b/src/locales/vi-VN/translation.json @@ -282,7 +282,8 @@ "script_requires": "Script yêu cầu các tài nguyên bên ngoài sau", "cookie_warning": "Xin lưu ý, script này yêu cầu quyền truy cập cookie, đây là một quyền nguy hiểm. Vui lòng xác minh tính bảo mật của script.", "scheduled_script_description_title": "Đây là script hẹn giờ, sẽ tự động chạy vào một thời điểm cụ thể sau khi được bật và có thể được điều khiển thủ công trong bảng điều khiển.", - "scheduled_script_description_description": "Biểu thức tác vụ hẹn giờ: {{expression}}, thời gian chạy gần nhất: {{time}}", + "scheduled_script_description_description_expr": "Biểu thức tác vụ hẹn giờ:", + "scheduled_script_description_description_next": "Thời gian chạy gần nhất:", "background_script_description": "Đây là script nền, sẽ tự động chạy một lần khi trình duyệt mở sau khi được bật và có thể được điều khiển thủ công trong bảng điều khiển.", "install_success": "Cài đặt thành công", "install": { @@ -322,6 +323,8 @@ "status_autoclose": "Tự đóng trong $0 giây", "header_other_update": "Các bản cập nhật khác" }, + "install_page_loading": "Đang tải trang cài đặt", + "invalid_page": "Trang không hợp lệ", "background_script_tag": "Đây là một script nền", "scheduled_script_tag": "Đây là một script hẹn giờ", "background_script": "Script nền", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 9ed06a218..b6b318ee4 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -282,7 +282,8 @@ "script_requires": "脚本引用了下列外部资源", "cookie_warning": "请注意,本脚本会申请 Cookie 的操作权限,这是一个危险的权限,请确认脚本的安全性。", "scheduled_script_description_title": "这是一个定时脚本,启用后将在特定时间自动运行,并可在面板中手动控制。", - "scheduled_script_description_description": "定时任务表达式:{{expression}},最近一次运行时间:{{time}}", + "scheduled_script_description_description_expr": "定时任务表达式:", + "scheduled_script_description_description_next": "最近一次运行时间:", "background_script_description": "这是一个后台脚本,启用后将在浏览器打开时自动运行一次,并可在面板中手动控制。", "install_success": "安装成功", "install": { @@ -322,6 +323,8 @@ "status_autoclose": "$0 秒后自动关闭", "header_other_update": "其他可用更新" }, + "install_page_loading": "安装页面加载中", + "invalid_page": "无效页面", "background_script_tag": "这是一个后台脚本", "scheduled_script_tag": "这是一个定时脚本", "background_script": "后台脚本", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index 027f029ac..c99f93919 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -282,7 +282,8 @@ "script_requires": "腳本引用了以下外部資源", "cookie_warning": "請注意,此腳本會要求 Cookie 的操作權限,這是一項危險的權限,請確認腳本的安全性。", "scheduled_script_description_title": "這是一個排程腳本,啟用後將在特定時間自動執行,並可在控制面板中手動控制。", - "scheduled_script_description_description": "排程任務表達式:{{expression}},最近一次執行時間:{{time}}", + "scheduled_script_description_description_expr": "排程任務表達式:", + "scheduled_script_description_description_next": "最近一次執行時間:", "background_script_description": "這是一個背景腳本,啟用後將在瀏覽器開啟時自動執行一次,並可在控制面板中手動控制。", "install_success": "安裝成功", "install": { @@ -322,6 +323,8 @@ "status_autoclose": "$0 秒後自動關閉", "header_other_update": "其他可用更新" }, + "install_page_loading": "安裝頁載入中", + "invalid_page": "無效頁面", "background_script_tag": "這是一個背景腳本", "scheduled_script_tag": "這是一個排程腳本", "background_script": "背景腳本", diff --git a/src/manifest.json b/src/manifest.json index b8e6e72e4..a36d60e9c 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -42,7 +42,8 @@ "notifications", "clipboardWrite", "unlimitedStorage", - "declarativeNetRequest" + "declarativeNetRequest", + "webNavigation" ], "optional_permissions": [ "userScripts" @@ -54,5 +55,11 @@ "pages": [ "src/sandbox.html" ] - } + }, + "web_accessible_resources": [ + { + "resources": ["/src/install.html"], + "matches": [""] + } + ] } \ No newline at end of file diff --git a/src/pages/components/CodeEditor/index.tsx b/src/pages/components/CodeEditor/index.tsx index 4fa13d33e..879f87c37 100644 --- a/src/pages/components/CodeEditor/index.tsx +++ b/src/pages/components/CodeEditor/index.tsx @@ -53,6 +53,7 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod folding: true, foldingStrategy: "indentation", automaticLayout: true, + scrollbar: { alwaysConsumeMouseWheel: false }, overviewRulerBorder: false, scrollBeyondLastLine: false, readOnly: true, @@ -73,6 +74,7 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod folding: true, foldingStrategy: "indentation", automaticLayout: true, + scrollbar: { alwaysConsumeMouseWheel: false }, overviewRulerBorder: false, scrollBeyondLastLine: false, readOnly: !editable, diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 95f62360a..0c5ae6a77 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -2,7 +2,6 @@ import { Avatar, Button, Dropdown, - Grid, Message, Menu, Space, @@ -29,6 +28,10 @@ import { type FTInfo, startFileTrack, unmountFileTrack } from "@App/pkg/utils/fi import { cleanupOldHandles, loadHandle, saveHandle } from "@App/pkg/utils/filehandle-db"; import { dayFormat } from "@App/pkg/utils/day_format"; import { intervalExecution, timeoutExecution } from "@App/pkg/utils/timer"; +import { useSearchParams } from "react-router-dom"; +import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; +import { cacheInstance } from "@App/app/cache"; +import Paragraph from "@arco-design/web-react/es/Typography/paragraph"; type ScriptOrSubscribe = Script | Subscribe; @@ -41,8 +44,150 @@ interface PermissionItem { type Permission = PermissionItem[]; -const closeWindow = () => { - window.close(); +const closeWindow = (doBackwards: boolean) => { + if (doBackwards) { + history.go(-1); + } else { + window.close(); + } +}; + +/** + * 將字節數轉換為人類可讀的格式(B, KB, MB, GB 等)。 + * @param bytes - 要轉換的字節數(number)。 + * @param decimals - 小數位數,默認為 2。 + * @returns 格式化的字符串,例如 "1.23 MB"。 + */ +const formatBytes = (bytes: number, decimals: number = 2): string => { + if (bytes === 0) return "0 B"; + + const k = 1024; + const units = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + const value = bytes / Math.pow(k, i); + + return `${value.toFixed(decimals)} ${units[i]}`; +}; + +const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any }) => { + const response = await fetch(url, { + headers: { + "Cache-Control": "no-cache", + // 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", + }, + }); + + if (!response.ok) { + throw new Error(`Fetch failed with status ${response.status}`); + } + + 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 contentLength = +(response.headers.get("Content-Length") || 0); + if (contentLength < 30) { + throw new Error(`Content-Length ${contentLength} is too small for a valid UserScript`); + } + + // 檢查 Content-Type 中的 charset + const contentType = response.headers.get("content-type") || ""; + const charsetMatch = contentType.match(/charset=([^;]+)/i); + const charset = charsetMatch ? charsetMatch[1].toLowerCase() : "utf-8"; + + const reader = response.body.getReader(); + + // Step 2: 合計の長さを取得します + + // Step 3: データを読み込みます + let receivedLength = 0; // その時点の長さ + const chunks = []; // 受信したバイナリチャンクの配列(本文を構成します) + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + chunks.push(value); + receivedLength += value.length; + onProgress?.({ receivedLength, contentLength }); + } + + if (response.status !== 200) { + throw new Error("fetch script info failed"); + } + + // 合併 chunks + const chunksAll = new Uint8Array(receivedLength); // (4.1) + let position = 0; + for (const chunk of chunks) { + chunksAll.set(chunk, position); // (4.2) + position += chunk.length; + } + // 使用檢測到的 charset 解碼 + + let code; + try { + code = new TextDecoder(charset).decode(chunksAll); + } catch (e: any) { + throw new Error(`Failed to decode response with charset ${charset}: ${e.message}`); + } + + const metadata = parseMetadata(code); + if (!metadata) { + throw new Error("parse script info failed"); + } + + return { code, metadata }; +}; + +const cleanupStaleInstallInfo = (uuid: string) => { + // 頁面打開時不清除當前uuid,每30秒更新一次記錄 + const f = () => { + cacheInstance.tx(`scriptInfoKeeps`, (val: Record | undefined, tx) => { + val = val || {}; + val[uuid] = Date.now(); + tx.set(val); + }); + }; + f(); + setInterval(f, 30_000); + + // 頁面打開後清除舊記錄 + const delay = Math.floor(5000 * Math.random()) + 10000; // 使用乱数时间避免瀏览器重啟时大量Tabs同时执行清除 + timeoutExecution( + `${cIdKey}cleanupStaleInstallInfo`, + () => { + cacheInstance + .tx(`scriptInfoKeeps`, (val: Record | undefined, tx) => { + const now = Date.now(); + const keeps = new Set(); + const out: Record = {}; + for (const [k, ts] of Object.entries(val ?? {})) { + if (ts > 0 && now - ts < 60_000) { + keeps.add(`${CACHE_KEY_SCRIPT_INFO}${k}`); + out[k] = ts; + } + } + tx.set(out); + return keeps; + }) + .then(async (keeps) => { + const list = await cacheInstance.list(); + const filtered = list.filter((key) => key.startsWith(CACHE_KEY_SCRIPT_INFO) && !keeps.has(key)); + if (filtered.length) { + // 清理缓存 + cacheInstance.dels(filtered); + } + }); + }, + delay + ); }; const cIdKey = `(cid_${Math.random()})`; @@ -58,6 +203,9 @@ function App() { const [isUpdate, setIsUpdate] = useState(false); const [localFileHandle, setLocalFileHandle] = useState(null); const { t } = useTranslation(); + const [searchParams, setSearchParams] = useSearchParams(); + const [loaded, setLoaded] = useState(false); + const [doBackwards, setDoBackwards] = useState(false); const installOrUpdateScript = async (newScript: Script, code: string) => { if (newScript.ignoreVersion) newScript.ignoreVersion = ""; @@ -84,12 +232,24 @@ function App() { const initAsync = async () => { try { - const locationUrl = new URL(window.location.href); - const uuid = locationUrl.searchParams.get("uuid"); + const uuid = searchParams.get("uuid"); + const fid = searchParams.get("file"); let info: ScriptInfo | undefined; let isKnownUpdate: boolean = false; + + // 如果没有 uuid 和 file,跳过初始化逻辑 + if (!uuid && !fid) { + return; + } + + if (window.history.length > 1) { + setDoBackwards(true); + } + setLoaded(true); + if (uuid) { const cachedInfo = await scriptClient.getInstallInfo(uuid); + cleanupStaleInstallInfo(uuid); if (cachedInfo?.[0]) isKnownUpdate = true; info = cachedInfo?.[1] || undefined; if (!info) { @@ -97,7 +257,6 @@ function App() { } } else { // 检查是不是本地文件安装 - const fid = locationUrl.searchParams.get("file"); if (!fid) { throw new Error("url param - local file id is not found"); } @@ -171,8 +330,8 @@ function App() { }; useEffect(() => { - initAsync(); - }, []); + !loaded && initAsync(); + }, [searchParams, loaded]); const [watchFile, setWatchFile] = useState(false); const metadataLive = useMemo(() => (scriptInfo?.metadata || {}) as SCMetadata, [scriptInfo]); @@ -209,14 +368,14 @@ function App() { return permissions; }, [scriptInfo, metadataLive]); - const description = useMemo(() => { - const description: JSX.Element[] = []; + const descriptionParagraph = useMemo(() => { + const ret: JSX.Element[] = []; - if (!scriptInfo) return description; + if (!scriptInfo) return ret; const isCookie = metadataLive.grant?.some((val) => val === "GM_cookie"); if (isCookie) { - description.push( + ret.push( {t("cookie_warning")} @@ -224,20 +383,20 @@ function App() { } if (metadataLive.crontab) { - description.push({t("scheduled_script_description_title")}); - description.push( - - {t("scheduled_script_description_description", { - expression: metadataLive.crontab[0], - time: nextTime(metadataLive.crontab[0]), - })} - + ret.push({t("scheduled_script_description_title")}); + ret.push( +
+ {t("scheduled_script_description_description_expr")} + {metadataLive.crontab[0]} + {t("scheduled_script_description_description_next")} + {nextTime(metadataLive.crontab[0])} +
); } else if (metadataLive.background) { - description.push({t("background_script_description")}); + ret.push({t("background_script_description")}); } - return description; + return ret; }, [scriptInfo, metadataLive]); const antifeatures: { [key: string]: { color: string; title: string; description: string } } = { @@ -337,7 +496,7 @@ function App() { if (shouldClose) { setTimeout(() => { - closeWindow(); + closeWindow(doBackwards); }, 500); } } catch (e) { @@ -351,7 +510,7 @@ function App() { if (noMoreUpdates && scriptInfo && !scriptInfo.userSubscribe) { scriptClient.setCheckUpdateUrl(scriptInfo.uuid, false); } - closeWindow(); + closeWindow(doBackwards); }; const { @@ -464,123 +623,146 @@ function App() { }; }, [memoWatchFile]); + // 处理没有 uuid 和 file 的情况 + // const handleCreateNewScript = () => { + // const newUuid = uuidv4(); + // setSearchParams({ uuid: newUuid }, { replace: true }); + // }; + + // 检查是否有 uuid 或 file + const hasUUIDorFile = useMemo(() => { + return !!(searchParams.get("uuid") || searchParams.get("file")); + }, [searchParams]); + + const urlHref = useMemo(() => { + try { + if (!hasUUIDorFile) { + const url = searchParams.get("url"); + if (url) { + const urlObject = new URL(url); + if (urlObject && urlObject.protocol && urlObject.hostname && urlObject.pathname) { + return urlObject.href; + } + } + } + } catch { + // ignored + } + return ""; + }, [hasUUIDorFile, searchParams]); + + const [fetchingState, setFetchingState] = useState({ + loadingStatus: "", + errorStatus: "", + }); + + const loadURLAsync = async (urlHref: string) => { + try { + const { code, metadata } = await fetchScriptBody(urlHref, { + onProgress: (info: { receivedLength: number; contentLength: number }) => { + setFetchingState((prev) => ({ + ...prev, + loadingStatus: `Downloading. Received ${formatBytes(info.receivedLength)}.`, + })); + }, + }); + const update = false; + const uuid = uuidv4(); + const url = urlHref; + const upsertBy = "user"; + + const si = [update, createScriptInfo(uuid, code, url, upsertBy, metadata)]; + await cacheInstance.set(`${CACHE_KEY_SCRIPT_INFO}${uuid}`, si); + setSearchParams( + (prev) => { + prev.delete("url"); + prev.set("uuid", uuid); + return prev; + }, + { replace: true } + ); + } catch (err: any) { + const errMessage = `${err.message || err}`; + setFetchingState((prev) => ({ + ...prev, + loadingStatus: "", + errorStatus: errMessage, + })); + } + }; + + useEffect(() => { + if (!urlHref) return; + loadURLAsync(urlHref); + }, [urlHref]); + + if (!hasUUIDorFile) { + return urlHref ? ( +
+ + {t("install_page_loading")} + {fetchingState.loadingStatus && ( +
+ {fetchingState.loadingStatus} +
+
+ )} + {fetchingState.errorStatus &&
{fetchingState.errorStatus}
} +
+
+ ) : ( +
+ + {t("invalid_page")} + +
+ ); + } + return ( -
- - - -
- {upsertScript?.metadata.icon && ( - - {upsertScript.name} - +
+
+
+
+ {upsertScript?.metadata.icon && ( + + {upsertScript.name} + + )} + {upsertScript && ( + + + {i18nName(upsertScript)} + + + )} + + + +
+
+
+ {oldScriptVersion && ( + + {oldScriptVersion} + )} - - {upsertScript && i18nName(upsertScript)} - - + {metadataLive.version && metadataLive.version[0] !== oldScriptVersion && ( + + + {metadataLive.version[0]} + - -
-
- {upsertScript && i18nDescription(upsertScript!)} -
-
- {`${t("author")}: ${metadataLive.author}`} -
-
- - {`${t("source")}: ${scriptInfo?.url}`} - -
-
- - - - - - {isUpdate ? t("update_script_no_close") : t("install_script_no_close")} - - {!scriptInfo?.userSubscribe && ( - - {isUpdate ? t("update_script_no_more_update") : t("install_script_no_more_update")} - - )} - - } - position="bottom" - disabled={watchFile} - > - - - )} - {isUpdate ? ( - - - - {!scriptInfo?.userSubscribe && ( - - {t("close_update_script_no_more_update")} - - )} - - } - position="bottom" - > - - )} - + )}
- - - - -
- - {oldScriptVersion && ( - - {oldScriptVersion} - - )} - {metadataLive.version && metadataLive.version[0] !== oldScriptVersion && ( - - - {metadataLive.version[0]} - - - )} +
+
+
+
+
+
+
+
{(metadataLive.background || metadataLive.crontab) && ( @@ -595,7 +777,7 @@ function App() { )} - {metadataLive.antifeature && + {metadataLive.antifeature?.length && metadataLive.antifeature.map((antifeature) => { const item = antifeature.split(" ")[0]; return ( @@ -608,20 +790,110 @@ function App() { ) ); })} - +
+
+
+ {upsertScript && i18nDescription(upsertScript!)} +
+
+ {`${t("author")}: ${metadataLive.author}`} +
+
+ + {`${t("source")}: ${scriptInfo?.url}`} + +
+
+
+
+
+ {t("install_from_legitimate_sources_warning")} +
+
+ + + + + + {isUpdate ? t("update_script_no_close") : t("install_script_no_close")} + + {!scriptInfo?.userSubscribe && ( + + {isUpdate ? t("update_script_no_more_update") : t("install_script_no_more_update")} + + )} + + } + position="bottom" + disabled={watchFile} + > + + + )} + {isUpdate ? ( + + + + {!scriptInfo?.userSubscribe && ( + + {t("close_update_script_no_more_update")} + + )} + + } + position="bottom" + > + + )} + +
- {description && description} -
- {t("install_from_legitimate_sources_warning")} +
+ {descriptionParagraph?.length ? ( +
+ + + {descriptionParagraph} + +
- - - - + ) : ( + <> + )} +
{permissions.map((item) => ( - {v}
))} -
+
))} - - - -
- +
+
+
+ +
); diff --git a/src/pages/install/index.css b/src/pages/install/index.css index 5b90b58fd..96a59b331 100644 --- a/src/pages/install/index.css +++ b/src/pages/install/index.css @@ -2,15 +2,6 @@ background: var(--vscode-editorGutter-background); } -:root { - --show-code-height: 100vh; - --show-code-height: min(calc(100vh), calc(40vh + 360px)); /* 可設為 400px */ /* 可設為 100vh */ -} - -.install-main-layout { - min-height: max-content; -} - #install-app-container { display: flex; flex-direction: column; @@ -20,15 +11,15 @@ #show-code-container { display: block; - height: var(--show-code-height); - padding: 16px 0px; + height: 100%; + padding: 2px 2px; position: relative; box-sizing: border-box; margin: 0; border: 0; flex-grow: 1; - flex-shrink: 0; - contain: strict; + flex-shrink: 0; + contain: strict; } #show-code { @@ -43,3 +34,75 @@ position: relative; background: #071119; } + +.downloading { + display: flex; + flex-direction: row; + flex-wrap: wrap; + column-gap: 4px; + align-items: center; +} + +.error-message { + color: red; +} + +.error-message:empty { + display: none; +} + +.error-message::before { + content: "ERROR: "; +} + +/* https://css-loaders.com/dots/ */ +.loader { + width: 60px; + aspect-ratio: 2; + --_g: no-repeat radial-gradient(circle closest-side, currentColor 90%, rgba(0,0,0,0)); + background: var(--_g) 0% 50%, var(--_g) 50% 50%, var(--_g) 100% 50%; + background-size: calc(100%/3) 50%; + animation: l3 1s infinite linear; + transform: scale(0.5); +} + +@keyframes l3 { + 20% { + background-position: 0% 0%, 50% 50%, 100% 50% + } + + 40% { + background-position: 0% 100%, 50% 0%, 100% 50% + } + + 60% { + background-position: 0% 50%, 50% 100%, 100% 0% + } + + 80% { + background-position: 0% 50%, 50% 50%, 100% 100% + } + +} + +#script-title-container { + position: sticky; + top: 0px; + background-color: var(--color-bg-2); +} + +#script-title { + background-color: var(--color-fill-2); +} + +.tag-container { + inline-size: min-content; + align-content: flex-start; + justify-items: flex-end; + justify-content: flex-end; +} + +.tag-container .arco-tag { + flex-grow: 1; + text-align: center; +} diff --git a/src/pages/install/main.tsx b/src/pages/install/main.tsx index d20b8a28e..19c64ef27 100644 --- a/src/pages/install/main.tsx +++ b/src/pages/install/main.tsx @@ -11,6 +11,7 @@ import "@App/locales/locales"; import "@App/index.css"; import "./index.css"; import registerEditor from "@App/pkg/utils/monaco-editor"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; registerEditor(); @@ -22,13 +23,20 @@ const loggerCore = new LoggerCore({ loggerCore.logger().debug("install page start"); -const Root = ( +const MyApp = () => ( ); +const Root = ( + + + } /> + + +); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( process.env.NODE_ENV === "development" ? {Root} : Root From d5f6310ad51a67ab88ef3e5e585e778ac5b1bed9 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:29:02 +0900 Subject: [PATCH 02/30] Update script.ts --- src/app/service/service_worker/script.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index ec4931f40..a4fac87ec 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -37,7 +37,6 @@ import type { TSortedScript, TInstallScriptParams, } from "../queue"; -import { timeoutExecution } from "@App/pkg/utils/timer"; import { buildScriptRunResourceBasic, selfMetadataUpdate } from "./utils"; import { BatchUpdateListActionCode, @@ -51,8 +50,6 @@ import { LocalStorageDAO } from "@App/app/repo/localStorage"; import { CompiledResourceDAO } from "@App/app/repo/resource"; // import { gzip as pakoGzip } from "pako"; -const cIdKey = `(cid_${Math.random()})`; - export type TCheckScriptUpdateOption = Partial< { checkType: "user"; noUpdateCheck?: number } | ({ checkType: "system" } & Record) >; From 404daa715aa3febccfe871b59b5849c18c812ed4 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:33:20 +0900 Subject: [PATCH 03/30] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20fetchScriptBody?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/install/App.tsx | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 0c5ae6a77..95a5dad4d 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -73,6 +73,7 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any const response = await fetch(url, { headers: { "Cache-Control": "no-cache", + // 参考:加权 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", }, @@ -89,23 +90,11 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any throw new Error("Response is text/html, not a valid UserScript"); } - const contentLength = +(response.headers.get("Content-Length") || 0); - if (contentLength < 30) { - throw new Error(`Content-Length ${contentLength} is too small for a valid UserScript`); - } - - // 檢查 Content-Type 中的 charset - const contentType = response.headers.get("content-type") || ""; - const charsetMatch = contentType.match(/charset=([^;]+)/i); - const charset = charsetMatch ? charsetMatch[1].toLowerCase() : "utf-8"; - const reader = response.body.getReader(); - // Step 2: 合計の長さを取得します - - // Step 3: データを読み込みます - let receivedLength = 0; // その時点の長さ - const chunks = []; // 受信したバイナリチャンクの配列(本文を構成します) + // 读取数据 + let receivedLength = 0; // 当前已接收的长度 + const chunks = []; // 已接收的二进制分片数组(用于组装正文) while (true) { const { done, value } = await reader.read(); @@ -115,22 +104,27 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any chunks.push(value); receivedLength += value.length; - onProgress?.({ receivedLength, contentLength }); + onProgress?.({ receivedLength }); } + // 检查 Content-Type 中的 charset + const contentType = response.headers.get("content-type") || ""; + const charsetMatch = contentType.match(/charset=([^;]+)/i); + const charset = charsetMatch ? charsetMatch[1].toLowerCase() : "utf-8"; + if (response.status !== 200) { throw new Error("fetch script info failed"); } - // 合併 chunks + // 合并分片(chunks) const chunksAll = new Uint8Array(receivedLength); // (4.1) let position = 0; for (const chunk of chunks) { chunksAll.set(chunk, position); // (4.2) position += chunk.length; } - // 使用檢測到的 charset 解碼 + // 使用检测到的 charset 解码 let code; try { code = new TextDecoder(charset).decode(chunksAll); @@ -659,7 +653,7 @@ function App() { const loadURLAsync = async (urlHref: string) => { try { const { code, metadata } = await fetchScriptBody(urlHref, { - onProgress: (info: { receivedLength: number; contentLength: number }) => { + onProgress: (info: { receivedLength: number }) => { setFetchingState((prev) => ({ ...prev, loadingStatus: `Downloading. Received ${formatBytes(info.receivedLength)}.`, From 49ccdb4d5b0d534a87a4e8987df53bd9ef59f99d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:48:25 +0900 Subject: [PATCH 04/30] css --- src/pages/install/App.tsx | 64 ++++++++++++++++++------------------- src/pages/install/index.css | 10 ------ 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 95a5dad4d..9d0cc5ae2 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -714,45 +714,43 @@ function App() { } return ( -
-
-
-
- {upsertScript?.metadata.icon && ( - - {upsertScript.name} - +
+
+
+ {upsertScript?.metadata.icon && ( + + {upsertScript.name} + + )} + {upsertScript && ( + + + {i18nName(upsertScript)} + + + )} + + + +
+
+
+ {oldScriptVersion && ( + + {oldScriptVersion} + )} - {upsertScript && ( - - - {i18nName(upsertScript)} - + {metadataLive.version && metadataLive.version[0] !== oldScriptVersion && ( + + + {metadataLive.version[0]} + )} - - - -
-
-
- {oldScriptVersion && ( - - {oldScriptVersion} - - )} - {metadataLive.version && metadataLive.version[0] !== oldScriptVersion && ( - - - {metadataLive.version[0]} - - - )} -
-
+
diff --git a/src/pages/install/index.css b/src/pages/install/index.css index 96a59b331..ba5d31ae4 100644 --- a/src/pages/install/index.css +++ b/src/pages/install/index.css @@ -85,16 +85,6 @@ } -#script-title-container { - position: sticky; - top: 0px; - background-color: var(--color-bg-2); -} - -#script-title { - background-color: var(--color-fill-2); -} - .tag-container { inline-size: min-content; align-content: flex-start; From 8e00da0f3390cfb1801a41f977f213d34c0f94bb Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:50:12 +0900 Subject: [PATCH 05/30] css --- src/pages/install/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/install/main.tsx b/src/pages/install/main.tsx index 19c64ef27..8953bb657 100644 --- a/src/pages/install/main.tsx +++ b/src/pages/install/main.tsx @@ -25,7 +25,7 @@ loggerCore.logger().debug("install page start"); const MyApp = () => ( - + From d5dd55f1835641920bb93ec7ae7b96018ae6ea47 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 20:00:15 +0900 Subject: [PATCH 06/30] css --- src/pages/install/App.tsx | 130 ++++++++++++++++++------------------ src/pages/install/index.css | 2 +- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 9d0cc5ae2..95a6ac151 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -750,7 +750,7 @@ function App() {
-
+
@@ -806,70 +806,6 @@ function App() {
-
-
- {t("install_from_legitimate_sources_warning")} -
-
- - - - - - {isUpdate ? t("update_script_no_close") : t("install_script_no_close")} - - {!scriptInfo?.userSubscribe && ( - - {isUpdate ? t("update_script_no_more_update") : t("install_script_no_more_update")} - - )} - - } - position="bottom" - disabled={watchFile} - > - - - )} - {isUpdate ? ( - - - - {!scriptInfo?.userSubscribe && ( - - {t("close_update_script_no_more_update")} - - )} - - } - position="bottom" - > - - )} - -
-
{descriptionParagraph?.length ? (
@@ -906,6 +842,70 @@ function App() { ))}
+
+
+ {t("install_from_legitimate_sources_warning")} +
+
+ + + + + + {isUpdate ? t("update_script_no_close") : t("install_script_no_close")} + + {!scriptInfo?.userSubscribe && ( + + {isUpdate ? t("update_script_no_more_update") : t("install_script_no_more_update")} + + )} + + } + position="bottom" + disabled={watchFile} + > + + + )} + {isUpdate ? ( + + + + {!scriptInfo?.userSubscribe && ( + + {t("close_update_script_no_more_update")} + + )} + + } + position="bottom" + > + + )} + +
+
Date: Sat, 18 Oct 2025 20:05:58 +0900 Subject: [PATCH 07/30] i18n t --- src/pages/install/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 95a6ac151..6a1a2f842 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -360,7 +360,7 @@ function App() { } return permissions; - }, [scriptInfo, metadataLive]); + }, [scriptInfo, metadataLive, t]); const descriptionParagraph = useMemo(() => { const ret: JSX.Element[] = []; @@ -391,7 +391,7 @@ function App() { } return ret; - }, [scriptInfo, metadataLive]); + }, [scriptInfo, metadataLive, t]); const antifeatures: { [key: string]: { color: string; title: string; description: string } } = { "referral-link": { @@ -436,7 +436,7 @@ function App() { if (upsertScript) { document.title = `${!isUpdate ? t("install_script") : t("update_script")} - ${i18nName(upsertScript!)} - ScriptCat`; } - }, [isUpdate, scriptInfo, upsertScript]); + }, [isUpdate, scriptInfo, upsertScript, t]); // 设置脚本状态 useEffect(() => { From 59ba0674a0a87075b2ce15e786ab40ed96e070f5 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 20:06:58 +0900 Subject: [PATCH 08/30] css --- src/pages/install/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 6a1a2f842..173b0582d 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -733,7 +733,7 @@ function App() {
-
+
{oldScriptVersion && ( From 031d412bf4b5b7a5390fa120f0402c751fb43aee Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 20:10:00 +0900 Subject: [PATCH 09/30] css --- src/pages/install/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 173b0582d..1c349b6f9 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -751,7 +751,7 @@ function App() {
-
+
From d95d5b463155d2be5bcae76d73518ce1f333ee3c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 22:12:45 +0900 Subject: [PATCH 10/30] =?UTF-8?q?=E6=98=BE=E7=A4=BA=207.5=20=E4=B8=AA?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=9D=A5=E6=8F=90=E9=86=92=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E8=80=85=E4=BB=A5=E6=BB=9A=E5=8A=A8=E6=B5=8F=E8=A7=88=E6=9B=B4?= =?UTF-8?q?=E5=A4=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/install/App.tsx | 42 +++++++++++++++++++++---------------- src/pages/install/index.css | 12 +++++++++++ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 1c349b6f9..cf014ba8e 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -820,24 +820,30 @@ function App() { )}
{permissions.map((item) => ( -
- - {item.label} - - {item.value.map((v) => ( -
- {v} -
- ))} +
+ {item.value?.length > 0 ? ( + <> + + {item.label} + +
+ {item.value.map((v) => ( +
+ {v} +
+ ))} +
+ + ) : ( + <> + )}
))}
diff --git a/src/pages/install/index.css b/src/pages/install/index.css index 2d33181c2..58a8e6797 100644 --- a/src/pages/install/index.css +++ b/src/pages/install/index.css @@ -96,3 +96,15 @@ flex-grow: 1; text-align: center; } + +div.permission-entry span.arco-typography { + line-height: 1rem; + padding: 0; + margin: 0; +} + +div.permission-entry { + line-height: 1.2rem; + padding: 0; + margin: 0; +} From f6ebfb99421e480f3f5819610723d0bca9bad3d9 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Oct 2025 22:16:52 +0900 Subject: [PATCH 11/30] =?UTF-8?q?=E7=A9=BA=E7=99=BD=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/install/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index cf014ba8e..983a31aa3 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -820,7 +820,7 @@ function App() { )}
{permissions.map((item) => ( -
+
{item.value?.length > 0 ? ( <> From d8ff8fd1879ce54be6814eb58e89527c243ff64c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:36:08 +0900 Subject: [PATCH 12/30] . --- src/pages/install/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 983a31aa3..59634d9ba 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -117,10 +117,10 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any } // 合并分片(chunks) - const chunksAll = new Uint8Array(receivedLength); // (4.1) + const chunksAll = new Uint8Array(receivedLength); let position = 0; for (const chunk of chunks) { - chunksAll.set(chunk, position); // (4.2) + chunksAll.set(chunk, position); position += chunk.length; } From 694177b1227b2498529dfa7552444661d4435544 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:03:14 +0900 Subject: [PATCH 13/30] =?UTF-8?q?=E5=85=BC=E5=AE=B9=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/script.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index a4fac87ec..ef65c3a98 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -171,6 +171,7 @@ export class ScriptService { } else { condition.excludedRequestDomains = ["github.com"]; } + const installPageURL = chrome.runtime.getURL("src/install.html"); // 重定向到脚本安装页 chrome.declarativeNetRequest.updateDynamicRules( { @@ -182,7 +183,7 @@ export class ScriptService { action: { type: "redirect" as chrome.declarativeNetRequest.RuleActionType, redirect: { - regexSubstitution: `chrome-extension://${chrome.runtime.id}/src/install.html?url=\\0`, + regexSubstitution: `${installPageURL}?url=\\0`, }, }, condition: condition, From 1d5db72252aeda57abe7ecd8ffa4f3b6ded47454 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:31:43 +0900 Subject: [PATCH 14/30] #876 --- src/app/service/service_worker/script.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index ef65c3a98..6311abd72 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -156,21 +156,21 @@ export class ScriptService { ); // 兼容 chrome 内核 < 128 处理 const condition: chrome.declarativeNetRequest.RuleCondition = { - regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)", + regexFilter: "^[^#]+\\.user(\\.bg|\\.sub)?\\.js(\\?.*?)?$", resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], }; - const browserType = getBrowserType(); - if (browserType.chrome && browserType.chromeVersion >= 128) { - condition.excludedResponseHeaders = [ - { - header: "Content-Type", - values: ["text/html"], - }, - ]; - } else { - condition.excludedRequestDomains = ["github.com"]; - } + // const browserType = getBrowserType(); + // if (browserType.chrome && browserType.chromeVersion >= 128) { + // condition.excludedResponseHeaders = [ + // { + // header: "Content-Type", + // values: ["text/html"], + // }, + // ]; + // } else { + // condition.excludedRequestDomains = ["github.com"]; + // } const installPageURL = chrome.runtime.getURL("src/install.html"); // 重定向到脚本安装页 chrome.declarativeNetRequest.updateDynamicRules( From cd8fae07113b7c233193fe64f4da0f2a9193d783 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:58:12 +0900 Subject: [PATCH 15/30] lint --- src/app/service/service_worker/script.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 6311abd72..0286eaf2a 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -5,13 +5,7 @@ import Logger from "@App/app/logger/logger"; import LoggerCore from "@App/app/logger/core"; import { cacheInstance } from "@App/app/cache"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; -import { - checkSilenceUpdate, - getBrowserType, - getStorageName, - openInCurrentTab, - stringMatching, -} from "@App/pkg/utils/utils"; +import { checkSilenceUpdate, getStorageName, openInCurrentTab, stringMatching } from "@App/pkg/utils/utils"; import { ltever } from "@App/pkg/utils/semver"; import type { SCMetadata, From ebd6a4f76d99e17c3f10c58178739c9e7ed75023 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 24 Oct 2025 21:03:33 +0900 Subject: [PATCH 16/30] i18n --- src/locales/ach-UG/translation.json | 1 + src/locales/de-DE/translation.json | 1 + src/locales/en-US/translation.json | 1 + src/locales/ja-JP/translation.json | 1 + src/locales/ru-RU/translation.json | 1 + src/locales/vi-VN/translation.json | 1 + src/locales/zh-CN/translation.json | 1 + src/locales/zh-TW/translation.json | 1 + src/pages/install/App.tsx | 2 +- 9 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/locales/ach-UG/translation.json b/src/locales/ach-UG/translation.json index 679a5e158..016aacc99 100644 --- a/src/locales/ach-UG/translation.json +++ b/src/locales/ach-UG/translation.json @@ -323,6 +323,7 @@ "status_autoclose": "crwdns12778:0crwdne12778:0", "header_other_update": "crwdns12784:0crwdne12784:0" }, + "downloading_status_text": "Downloading. Received {{bytes}}.", "install_page_loading": "Installation page loading", "invalid_page": "Invalid page", "background_script_tag": "crwdns8460:0crwdne8460:0", diff --git a/src/locales/de-DE/translation.json b/src/locales/de-DE/translation.json index 5611a2a45..33ef3edce 100644 --- a/src/locales/de-DE/translation.json +++ b/src/locales/de-DE/translation.json @@ -323,6 +323,7 @@ "status_autoclose": "Automatisches Schließen in $0 Sekunden", "header_other_update": "Andere verfügbare Updates" }, + "downloading_status_text": "Wird heruntergeladen. {{bytes}} empfangen.", "install_page_loading": "Installationsseite wird geladen", "invalid_page": "Ungültige Seite", "background_script_tag": "Dies ist ein Hintergrundskript", diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 9cb59cc97..91ca1335d 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -323,6 +323,7 @@ "status_autoclose": "Auto-closing in $0 seconds", "header_other_update": "Other available updates" }, + "downloading_status_text": "Downloading. Received {{bytes}}.", "install_page_loading": "Installation page loading", "invalid_page": "Invalid page", "background_script_tag": "This is a Background Script", diff --git a/src/locales/ja-JP/translation.json b/src/locales/ja-JP/translation.json index 577ef7469..170c70a62 100644 --- a/src/locales/ja-JP/translation.json +++ b/src/locales/ja-JP/translation.json @@ -323,6 +323,7 @@ "status_autoclose": "$0 秒後に自動的に閉じます", "header_other_update": "その他の利用可能な更新" }, + "downloading_status_text": "ダウンロード中。{{bytes}} を受信しました。", "install_page_loading": "インストールページを読み込み中", "invalid_page": "無効なページ", "background_script_tag": "これはバックグラウンドスクリプトです", diff --git a/src/locales/ru-RU/translation.json b/src/locales/ru-RU/translation.json index e58310ae1..77292536c 100644 --- a/src/locales/ru-RU/translation.json +++ b/src/locales/ru-RU/translation.json @@ -323,6 +323,7 @@ "status_autoclose": "Автоматическое закрытие через $0 сек.", "header_other_update": "Другие доступные обновления" }, + "downloading_status_text": "Загрузка. Получено {{bytes}}.", "install_page_loading": "Загрузка страницы установки", "invalid_page": "Недействительная страница", "background_script_tag": "Это фоновый скрипт", diff --git a/src/locales/vi-VN/translation.json b/src/locales/vi-VN/translation.json index d94bac83d..2e78dbdbd 100644 --- a/src/locales/vi-VN/translation.json +++ b/src/locales/vi-VN/translation.json @@ -323,6 +323,7 @@ "status_autoclose": "Tự đóng trong $0 giây", "header_other_update": "Các bản cập nhật khác" }, + "downloading_status_text": "Đang tải xuống. Đã nhận {{bytes}}.", "install_page_loading": "Đang tải trang cài đặt", "invalid_page": "Trang không hợp lệ", "background_script_tag": "Đây là một script nền", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 4b0f5f88f..20f34b911 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -323,6 +323,7 @@ "status_autoclose": "$0 秒后自动关闭", "header_other_update": "其他可用更新" }, + "downloading_status_text": "正在下载。已接收 {{bytes}}。", "install_page_loading": "安装页面加载中", "invalid_page": "无效页面", "background_script_tag": "这是一个后台脚本", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index c06ae233b..9cdaa4473 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -323,6 +323,7 @@ "status_autoclose": "$0 秒後自動關閉", "header_other_update": "其他可用更新" }, + "downloading_status_text": "正在下載。已接收 {{bytes}}。", "install_page_loading": "安裝頁載入中", "invalid_page": "無效頁面", "background_script_tag": "這是一個背景腳本", diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 59634d9ba..d55e2c2b3 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -656,7 +656,7 @@ function App() { onProgress: (info: { receivedLength: number }) => { setFetchingState((prev) => ({ ...prev, - loadingStatus: `Downloading. Received ${formatBytes(info.receivedLength)}.`, + loadingStatus: t("downloading_status_text", { bytes: `${formatBytes(info.receivedLength)}` }), })); }, }); From 9eca6c46e2bdd91bb7cffc769ba12d1b5353e305 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 24 Oct 2025 21:25:42 +0900 Subject: [PATCH 17/30] #877 --- src/app/service/service_worker/script.ts | 131 +++++++++++++++++------ 1 file changed, 98 insertions(+), 33 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 0286eaf2a..2695d2f87 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -5,7 +5,13 @@ import Logger from "@App/app/logger/logger"; import LoggerCore from "@App/app/logger/core"; import { cacheInstance } from "@App/app/cache"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; -import { checkSilenceUpdate, getStorageName, openInCurrentTab, stringMatching } from "@App/pkg/utils/utils"; +import { + checkSilenceUpdate, + getBrowserType, + getStorageName, + openInCurrentTab, + stringMatching, +} from "@App/pkg/utils/utils"; import { ltever } from "@App/pkg/utils/semver"; import type { SCMetadata, @@ -88,11 +94,12 @@ export class ScriptService { return undefined; } // 判断是否有url参数 - if (!reqUrl.hash.includes("url=")) { + const idx = reqUrl.hash.indexOf("url="); + if (idx < 0) { return undefined; } // 获取url参数 - targetUrl = reqUrl.hash.split("url=")[1]; + targetUrl = reqUrl.hash.substring(idx + 4); } // 读取脚本url内容, 进行安装 const logger = this.logger.with({ url: targetUrl }); @@ -148,41 +155,99 @@ export class ScriptService { ], } ); + // 兼容 chrome 内核 < 128 处理 - const condition: chrome.declarativeNetRequest.RuleCondition = { - regexFilter: "^[^#]+\\.user(\\.bg|\\.sub)?\\.js(\\?.*?)?$", - resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], - requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], - }; - // const browserType = getBrowserType(); - // if (browserType.chrome && browserType.chromeVersion >= 128) { - // condition.excludedResponseHeaders = [ - // { - // header: "Content-Type", - // values: ["text/html"], - // }, - // ]; - // } else { - // condition.excludedRequestDomains = ["github.com"]; - // } + const browserType = getBrowserType(); + const addResponseHeaders = browserType.chrome && browserType.chromeVersion >= 128; + const conditions: chrome.declarativeNetRequest.RuleCondition[] = [ + { + regexFilter: "^[^?#]+\\.user(\\.bg|\\.sub)?\\.js([?#][^./\\s#?]*?)*?$", + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ + isUrlFilterCaseSensitive: false, + excludedRequestDomains: ["github.com", "gitlab.com", "gitea.com", "bitbucket.org"], + }, + { + regexFilter: + "^https?://github.com/[^\\s/?#]+/[^\\s/?#]+/releases/([^\\s.?#]+/|)[^.?#]+.user(\\.bg|\\.sub)?.js([?#][^./\\s#?]*?)*?$", + // https://github.com///releases/latest/download/file.user.js + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ + isUrlFilterCaseSensitive: false, + }, + { + regexFilter: + "^https?://github.com/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z]+/([^\\s.?#]+/|)[^.?#]+.user(\\.bg|\\.sub)?.js([?#][^./\\s#?]*?)*?$", + // https://github.com///raw/refs/heads/main/.../file.user.js + // https://github.com///raw//.../file.user.js + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ + isUrlFilterCaseSensitive: false, + }, + { + regexFilter: + "^https?://gitlab\\.com/[^\\s/?#]+/[^\\s/?#]+/-/raw/[a-z0-9_/.-]+/([^\\s.?#]+/|)[^.?#]+\\.user(\\.bg|\\.sub)?\\.js([?#][^./\\s#?]*?)*?$", + // https://gitlab.com///-/raw//.../file.user.js + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], + isUrlFilterCaseSensitive: false, + }, + { + regexFilter: + "^https?://gitea\\.com/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z0-9_/.-]+/([^\\s.?#]+/|)[^.?#]+\\.user(\\.bg|\\.sub)?\\.js([?#][^./\\s#?]*?)*?$", + // https://gitea.com///raw//.../file.user.js + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], + isUrlFilterCaseSensitive: false, + }, + { + regexFilter: + "^https?://bitbucket\\.org/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z0-9_/.-]+/([^\\s.?#]+/|)[^.?#]+\\.user(\\.bg|\\.sub)?\\.js([?#][^./\\s#?]*?)*?$", + // https://bitbucket.org///raw//.../file.user.js + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], + isUrlFilterCaseSensitive: false, + }, + ]; const installPageURL = chrome.runtime.getURL("src/install.html"); + const rules = conditions.map((condition, idx) => { + Object.assign(condition, { + excludedTabIds: [chrome.tabs.TAB_ID_NONE], + }); + if (addResponseHeaders) { + Object.assign(condition, { + responseHeaders: [ + { + header: "Content-Type", + values: [ + "text/javascript*", + "application/javascript*", + "text/html*", + "text/plain*", + "application/octet-stream*", + "application/force-download*", + ], + }, + ], + }); + } + return { + id: 1000 + idx, + priority: 1, + action: { + type: "redirect" as chrome.declarativeNetRequest.RuleActionType, + redirect: { + regexSubstitution: `${installPageURL}?url=\\0`, + }, + }, + condition: condition, + } as chrome.declarativeNetRequest.Rule; + }); // 重定向到脚本安装页 chrome.declarativeNetRequest.updateDynamicRules( { - removeRuleIds: [1, 2], - addRules: [ - { - id: 1, - priority: 1, - action: { - type: "redirect" as chrome.declarativeNetRequest.RuleActionType, - redirect: { - regexSubstitution: `${installPageURL}?url=\\0`, - }, - }, - condition: condition, - }, - ], + removeRuleIds: [1, ...rules.map((rule) => rule.id)], + addRules: rules, }, () => { if (chrome.runtime.lastError) { From 9fe2303c17debf751be58b19be1bde3b9e2116c2 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 5 Nov 2025 21:29:59 +0900 Subject: [PATCH 18/30] =?UTF-8?q?#877=20=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/script.ts | 76 ++++++++++++++++++++---- src/pages/install/App.tsx | 9 +++ 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 2695d2f87..243b2e2ee 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -156,57 +156,100 @@ export class ScriptService { } ); + // chrome.webRequest.onHeadersReceived.addListener( + // (details) => { + // const lastError = chrome.runtime.lastError; + // if (lastError) { + // console.error(lastError.message); + // } + // console.log("onHeadersReceived inspect", details); + // return undefined; + // }, + // { + // urls: ["*://*/*.user.js", "*://*/*.user.bg.js", "*://*/*.user.sub.js"], + // }, + // ["responseHeaders"] + // ); + // 兼容 chrome 内核 < 128 处理 const browserType = getBrowserType(); const addResponseHeaders = browserType.chrome && browserType.chromeVersion >= 128; + // Chrome 84+ const conditions: chrome.declarativeNetRequest.RuleCondition[] = [ { - regexFilter: "^[^?#]+\\.user(\\.bg|\\.sub)?\\.js([?#][^./\\s#?]*?)*?$", + regexFilter: "^([^?#]+?\\.user(\\.bg|\\.sub)?\\.js)", // Chrome 84+ + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], // Chrome 84+ + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ + isUrlFilterCaseSensitive: false, // Chrome 84+ + excludedRequestDomains: ["github.com", "gitlab.com", "gitea.com", "bitbucket.org"], // Chrome 101+ + }, + { + regexFilter: "^(.+?\\.user(\\.bg|\\.sub)?\\.js&response-content-type=application%2Foctet-stream)", resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ isUrlFilterCaseSensitive: false, - excludedRequestDomains: ["github.com", "gitlab.com", "gitea.com", "bitbucket.org"], + requestDomains: ["githubusercontent.com"], // Chrome 101+ }, { regexFilter: - "^https?://github.com/[^\\s/?#]+/[^\\s/?#]+/releases/([^\\s.?#]+/|)[^.?#]+.user(\\.bg|\\.sub)?.js([?#][^./\\s#?]*?)*?$", + "^(https?:\\/\\/github.com\\/[^\\s/?#]+\\/[^\\s/?#]+\\/releases/[^\\s/?#]+/download/[^?#]+?\\.user(\\.bg|\\.sub)?\\.js)", // https://github.com///releases/latest/download/file.user.js resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ isUrlFilterCaseSensitive: false, + requestDomains: ["github.com"], // Chrome 101+ }, { regexFilter: - "^https?://github.com/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z]+/([^\\s.?#]+/|)[^.?#]+.user(\\.bg|\\.sub)?.js([?#][^./\\s#?]*?)*?$", + "^(https?:\\/\\/gitlab\\.com\\/[^\\s/?#]+\\/[^\\s/?#]+\\/-\\/raw\\/[a-z0-9_/.-]+\\/[^?#]+?\\.user(\\.bg|\\.sub)?\\.js)", + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ + isUrlFilterCaseSensitive: false, + requestDomains: ["gitlab.com"], // Chrome 101+ + }, + { + regexFilter: "^(https?:\\/\\/github\\.com\\/[^\\/]+\\/[^\\/]+\\/releases\\/[^?#]+?\\.user(\\.bg|\\.sub)?\\.js)", + // https://github.com///releases/latest/download/file.user.js + resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], + requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ + isUrlFilterCaseSensitive: false, + requestDomains: ["github.com"], // Chrome 101+ + }, + { + regexFilter: "^(https?://github.com/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z]+/[^?#]+?.user(\\.bg|\\.sub)?.js)", // https://github.com///raw/refs/heads/main/.../file.user.js // https://github.com///raw//.../file.user.js resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], // Chrome 91+ isUrlFilterCaseSensitive: false, + requestDomains: ["github.com"], // Chrome 101+ }, { regexFilter: - "^https?://gitlab\\.com/[^\\s/?#]+/[^\\s/?#]+/-/raw/[a-z0-9_/.-]+/([^\\s.?#]+/|)[^.?#]+\\.user(\\.bg|\\.sub)?\\.js([?#][^./\\s#?]*?)*?$", + "^(https?://gitlab\\.com/[^\\s/?#]+/[^\\s/?#]+/-/raw/[a-z0-9_/.-]+/[^?#]+?\\.user(\\.bg|\\.sub)?\\.js)", // https://gitlab.com///-/raw//.../file.user.js resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], isUrlFilterCaseSensitive: false, + requestDomains: ["gitlab.com"], // Chrome 101+ }, { regexFilter: - "^https?://gitea\\.com/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z0-9_/.-]+/([^\\s.?#]+/|)[^.?#]+\\.user(\\.bg|\\.sub)?\\.js([?#][^./\\s#?]*?)*?$", + "^(https?://gitea\\.com/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z0-9_/.-]+/[^?#]+?\\.user(\\.bg|\\.sub)?\\.js)", // https://gitea.com///raw//.../file.user.js resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], isUrlFilterCaseSensitive: false, + requestDomains: ["gitea.com"], // Chrome 101+ }, { regexFilter: - "^https?://bitbucket\\.org/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z0-9_/.-]+/([^\\s.?#]+/|)[^.?#]+\\.user(\\.bg|\\.sub)?\\.js([?#][^./\\s#?]*?)*?$", + "^(https?://bitbucket\\.org/[^\\s/?#]+/[^\\s/?#]+/raw/[a-z0-9_/.-]+/[^?#]+?\\.user(\\.bg|\\.sub)?\\.js)", // https://bitbucket.org///raw//.../file.user.js resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], isUrlFilterCaseSensitive: false, + requestDomains: ["bitbucket.org"], // Chrome 101+ }, ]; const installPageURL = chrome.runtime.getURL("src/install.html"); @@ -237,7 +280,7 @@ export class ScriptService { action: { type: "redirect" as chrome.declarativeNetRequest.RuleActionType, redirect: { - regexSubstitution: `${installPageURL}?url=\\0`, + regexSubstitution: `${installPageURL}?url=\\1`, }, }, condition: condition, @@ -246,8 +289,7 @@ export class ScriptService { // 重定向到脚本安装页 chrome.declarativeNetRequest.updateDynamicRules( { - removeRuleIds: [1, ...rules.map((rule) => rule.id)], - addRules: rules, + removeRuleIds: [1], }, () => { if (chrome.runtime.lastError) { @@ -258,6 +300,20 @@ export class ScriptService { } } ); + chrome.declarativeNetRequest.updateSessionRules( + { + removeRuleIds: [...rules.map((rule) => rule.id)], + addRules: rules, + }, + () => { + if (chrome.runtime.lastError) { + console.error( + "chrome.runtime.lastError in chrome.declarativeNetRequest.updateSessionRules:", + chrome.runtime.lastError + ); + } + } + ); } public async openInstallPageByUrl(url: string, source: InstallSource): Promise<{ success: boolean; msg: string }> { diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index d55e2c2b3..736b1d411 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -70,13 +70,22 @@ const formatBytes = (bytes: number, decimals: number = 2): string => { }; const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any }) => { + let origin; + try { + origin = new URL(url).origin; + } catch { + throw new Error(`Invalid url: ${url}`); + } 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", + Origin: origin, }, + referrer: origin + "/", }); if (!response.ok) { From e0375ebc3d579170c14e0bceb01df77691af8a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 18 Nov 2025 10:57:59 +0800 Subject: [PATCH 19/30] =?UTF-8?q?=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/script.ts | 31 +++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index d19aa6c2b..b37fc0584 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -107,7 +107,7 @@ export class ScriptService { // 读取脚本url内容, 进行安装 const logger = this.logger.with({ url: targetUrl }); logger.debug("install script"); - this.openInstallPageByUrl(targetUrl, "user") + this.openInstallPageByUrl(targetUrl, { source: "user", byWebRequest: true }) .catch((e) => { logger.error("install script error", Logger.E(e)); // 不再重定向当前url @@ -319,9 +319,12 @@ export class ScriptService { ); } - public async openInstallPageByUrl(url: string, source: InstallSource): Promise<{ success: boolean; msg: string }> { + public async openInstallPageByUrl( + url: string, + options: { source: InstallSource; byWebRequest?: boolean } + ): Promise<{ success: boolean; msg: string }> { try { - const installPageUrl = await this.getInstallPageUrl(url, source); + const installPageUrl = await this.getInstallPageUrl(url, options); if (!installPageUrl) throw new Error("getInstallPageUrl failed"); await openInCurrentTab(installPageUrl); return { success: true, msg: "" }; @@ -331,10 +334,13 @@ export class ScriptService { } } - public async getInstallPageUrl(url: string, source: InstallSource): Promise { + public async getInstallPageUrl( + url: string, + options: { source: InstallSource; byWebRequest?: boolean } + ): Promise { const uuid = uuidv4(); try { - await this.openUpdateOrInstallPage(uuid, url, source, false); + await this.openUpdateOrInstallPage(uuid, url, options, false); return `/src/install.html?uuid=${uuid}`; } catch (err: any) { console.error(err); @@ -838,7 +844,14 @@ export class ScriptService { return script; } - async openUpdateOrInstallPage(uuid: string, url: string, upsertBy: InstallSource, update: boolean, logger?: Logger) { + async openUpdateOrInstallPage( + uuid: string, + url: string, + options: { source: InstallSource; byWebRequest?: boolean }, + update: boolean, + logger?: Logger + ) { + const upsertBy = options.source; const code = await fetchScriptBody(url); if (update && (await this.systemConfig.getSilenceUpdateScript())) { try { @@ -862,7 +875,7 @@ export class ScriptService { if (!metadata) { throw new Error("parse script info failed"); } - const si = [update, createScriptInfo(uuid, code, url, upsertBy, metadata)]; + const si = [update, createScriptInfo(uuid, code, url, upsertBy, metadata), options]; await cacheInstance.set(`${CACHE_KEY_SCRIPT_INFO}${uuid}`, si); return 1; } @@ -878,7 +891,7 @@ export class ScriptService { }); const url = downloadUrl || checkUpdateUrl!; try { - const ret = await this.openUpdateOrInstallPage(uuid, url, source, true, logger); + const ret = await this.openUpdateOrInstallPage(uuid, url, { source }, true, logger); if (ret === 2) return; // slience update // 打开安装页面 openInCurrentTab(`/src/install.html?uuid=${uuid}`); @@ -1269,7 +1282,7 @@ export class ScriptService { } importByUrl(url: string) { - return this.openInstallPageByUrl(url, "user"); + return this.openInstallPageByUrl(url, { source: "user" }); } setCheckUpdateUrl({ From 55cf73fbdabf0493e6551eecc444b1ef1eeb075d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:42:18 +0900 Subject: [PATCH 20/30] typescript fix --- src/app/service/service_worker/script.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index b37fc0584..73dc2ad9f 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -80,13 +80,13 @@ export class ScriptService { listenerScriptInstall() { // 初始化脚本安装监听 chrome.webNavigation.onBeforeNavigate.addListener( - (req: chrome.webNavigation.WebNavigationParentedCallbackDetails) => { + (req: chrome.webNavigation.WebNavigationBaseCallbackDetails) => { const lastError = chrome.runtime.lastError; if (lastError) { console.error(lastError.message); } // 处理url, 实现安装脚本 - let targetUrl: string | null = null; + let targetUrl: string; // 判断是否为 file:///*/*.user.js if (req.url.startsWith("file://") && req.url.endsWith(".user.js")) { targetUrl = req.url; From 5b8b1fb4aabb42c62b7d462f629d400ddb8d9345 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:57:20 +0900 Subject: [PATCH 21/30] Update src/locales/de-DE/translation.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/locales/de-DE/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/de-DE/translation.json b/src/locales/de-DE/translation.json index 96964c732..116925ff4 100644 --- a/src/locales/de-DE/translation.json +++ b/src/locales/de-DE/translation.json @@ -282,7 +282,7 @@ "cookie_warning": "Achtung: Dieses Skript beantragt Cookie-Operationsberechtigung. Dies ist eine gefährliche Berechtigung, bitte stellen Sie die Sicherheit des Skripts sicher.", "scheduled_script_description_title": "Dies ist ein geplantes Skript. Wenn aktiviert, wird es zu bestimmten Zeiten automatisch ausgeführt und kann im Panel manuell gesteuert werden.", "scheduled_script_description_description_expr": "Geplante Aufgaben-Ausdruck", - "scheduled_script_description_description_next": "Letzte Ausführungszeit", + "scheduled_script_description_description_next": "Letzte Ausführungszeit:", "background_script_description": "Dies ist ein Hintergrundskript. Wenn aktiviert, wird es automatisch einmal ausgeführt, wenn der Browser geöffnet wird, und kann im Panel manuell gesteuert werden.", "install_success": "Installation erfolgreich", "install": { From 350b592828a03c5a0398ff785a9d1391b4c6c567 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:57:58 +0900 Subject: [PATCH 22/30] Update src/pages/install/App.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/pages/install/App.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index a209817db..8453a55a7 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -121,9 +121,6 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any const charsetMatch = contentType.match(/charset=([^;]+)/i); const charset = charsetMatch ? charsetMatch[1].toLowerCase() : "utf-8"; - if (response.status !== 200) { - throw new Error("fetch script info failed"); - } // 合并分片(chunks) const chunksAll = new Uint8Array(receivedLength); From d0751437256463cc76389497ae425bae849538ee Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:58:18 +0900 Subject: [PATCH 23/30] Update src/pages/install/App.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/pages/install/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 8453a55a7..d598dbb90 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -147,7 +147,7 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any }; const cleanupStaleInstallInfo = (uuid: string) => { - // 頁面打開時不清除當前uuid,每30秒更新一次記錄 + // 页面打开时不清除当前uuid,每30秒更新一次记录 const f = () => { cacheInstance.tx(`scriptInfoKeeps`, (val: Record | undefined, tx) => { val = val || {}; From b8631e2c731ff6dbc27040cf6b25da5f4a7dc0f1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:02:21 +0900 Subject: [PATCH 24/30] chrome.runtime.lastError --- src/app/service/service_worker/script.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 73dc2ad9f..080301d10 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -83,7 +83,8 @@ export class ScriptService { (req: chrome.webNavigation.WebNavigationBaseCallbackDetails) => { const lastError = chrome.runtime.lastError; if (lastError) { - console.error(lastError.message); + console.error("chrome.runtime.lastError in chrome.webNavigation.onBeforeNavigate:", lastError); + return; } // 处理url, 实现安装脚本 let targetUrl: string; From 4e31818348a98066b52de480c878b28379d8a4cd Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:03:47 +0900 Subject: [PATCH 25/30] fix --- src/pages/install/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index d598dbb90..cbf935e03 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -642,7 +642,7 @@ function App() { const url = searchParams.get("url"); if (url) { const urlObject = new URL(url); - if (urlObject && urlObject.protocol && urlObject.hostname && urlObject.pathname) { + if (urlObject.protocol && urlObject.hostname && urlObject.pathname) { return urlObject.href; } } From 121e7371cd03d592b8df223e8428dc7115049f8e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:08:31 +0900 Subject: [PATCH 26/30] =?UTF-8?q?=E4=B8=AD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/install/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index cbf935e03..d60db6e2c 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -158,8 +158,8 @@ const cleanupStaleInstallInfo = (uuid: string) => { f(); setInterval(f, 30_000); - // 頁面打開後清除舊記錄 - const delay = Math.floor(5000 * Math.random()) + 10000; // 使用乱数时间避免瀏览器重啟时大量Tabs同时执行清除 + // 页面打开后清除旧记录 + const delay = Math.floor(5000 * Math.random()) + 10000; // 使用随机时间避免浏览器重启时大量Tabs同时执行清除 timeoutExecution( `${cIdKey}cleanupStaleInstallInfo`, () => { From a22e03cfa0ba50882416786e211928033b9dc515 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:11:48 +0900 Subject: [PATCH 27/30] formatBytes --- src/pages/install/App.tsx | 24 +++--------------------- src/pkg/utils/utils.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index d60db6e2c..4886e5111 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -31,7 +31,7 @@ import { intervalExecution, timeoutExecution } from "@App/pkg/utils/timer"; import { useSearchParams } from "react-router-dom"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; import { cacheInstance } from "@App/app/cache"; -import Paragraph from "@arco-design/web-react/es/Typography/paragraph"; +import { formatBytes } from "@App/pkg/utils/utils"; type ScriptOrSubscribe = Script | Subscribe; @@ -52,23 +52,6 @@ const closeWindow = (doBackwards: boolean) => { } }; -/** - * 將字節數轉換為人類可讀的格式(B, KB, MB, GB 等)。 - * @param bytes - 要轉換的字節數(number)。 - * @param decimals - 小數位數,默認為 2。 - * @returns 格式化的字符串,例如 "1.23 MB"。 - */ -const formatBytes = (bytes: number, decimals: number = 2): string => { - if (bytes === 0) return "0 B"; - - const k = 1024; - const units = ["B", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - const value = bytes / Math.pow(k, i); - - return `${value.toFixed(decimals)} ${units[i]}`; -}; - const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any }) => { let origin; try { @@ -121,7 +104,6 @@ const fetchScriptBody = async (url: string, { onProgress }: { [key: string]: any const charsetMatch = contentType.match(/charset=([^;]+)/i); const charset = charsetMatch ? charsetMatch[1].toLowerCase() : "utf-8"; - // 合并分片(chunks) const chunksAll = new Uint8Array(receivedLength); let position = 0; @@ -818,9 +800,9 @@ function App() { {descriptionParagraph?.length ? (
- + {descriptionParagraph} - +
) : ( diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index f24ff5547..cac004526 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -417,3 +417,20 @@ export const stringMatching = (main: string, sub: string): boolean => { return false; } }; + +/** + * 将字节数转换为人类可读的格式(B, KB, MB, GB 等)。 + * @param bytes - 要转换的字节数(number)。 + * @param decimals - 小数位数,默认为 2。 + * @returns 格式化的字符串,例如 "1.23 MB"。 + */ +export const formatBytes = (bytes: number, decimals: number = 2): string => { + if (bytes === 0) return "0 B"; + + const k = 1024; + const units = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + const value = bytes / Math.pow(k, i); + + return `${value.toFixed(decimals)} ${units[i]}`; +}; From b844d566eae6382971220c35d01f0ba36b8c59f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 20 Nov 2025 14:58:45 +0800 Subject: [PATCH 28/30] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=92=8C=E6=B7=BB=E5=8A=A0=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/cache.test.ts | 198 +++++++++++++++++++++++ src/app/service/service_worker/script.ts | 17 +- src/pages/install/App.tsx | 6 - src/pkg/utils/utils.test.ts | 27 +++- 4 files changed, 225 insertions(+), 23 deletions(-) create mode 100644 src/app/cache.test.ts diff --git a/src/app/cache.test.ts b/src/app/cache.test.ts new file mode 100644 index 000000000..520f2c52b --- /dev/null +++ b/src/app/cache.test.ts @@ -0,0 +1,198 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { cacheInstance } from "./cache"; + +describe("Cache", () => { + beforeEach(async () => { + // 每个测试前清空缓存 + await cacheInstance.clear(); + }); + + describe("基本操作", () => { + it("应该能够设置和获取不同类型的值", async () => { + await cacheInstance.set("string", "hello"); + expect(await cacheInstance.get("string")).toBe("hello"); + + await cacheInstance.set("number", 42); + expect(await cacheInstance.get("number")).toBe(42); + + const obj = { name: "test", count: 1 }; + await cacheInstance.set("object", obj); + expect(await cacheInstance.get("object")).toEqual(obj); + + expect(await cacheInstance.get("non-existent")).toBeUndefined(); + }); + + it("应该能够批量设置值", async () => { + await cacheInstance.batchSet({ + key1: "value1", + key2: "value2", + key3: "value3", + }); + + expect(await cacheInstance.get("key1")).toBe("value1"); + expect(await cacheInstance.get("key2")).toBe("value2"); + expect(await cacheInstance.get("key3")).toBe("value3"); + }); + }); + + describe("has/del/clear/list 方法", () => { + it("应该正确检查键是否存在", async () => { + await cacheInstance.set("existing-key", "value"); + expect(await cacheInstance.has("existing-key")).toBe(true); + expect(await cacheInstance.has("non-existing-key")).toBe(false); + + await cacheInstance.set("undefined-key", undefined); + expect(await cacheInstance.has("undefined-key")).toBe(false); + }); + + it("应该能够删除键和清空缓存", async () => { + await cacheInstance.batchSet({ key1: "v1", key2: "v2" }); + + await cacheInstance.del("key1"); + expect(await cacheInstance.has("key1")).toBe(false); + expect(await cacheInstance.has("key2")).toBe(true); + + await cacheInstance.clear(); + expect(await cacheInstance.has("key2")).toBe(false); + }); + + it("应该返回所有键的列表", async () => { + await cacheInstance.batchSet({ key1: "v1", key2: "v2", key3: "v3" }); + const keys = await cacheInstance.list(); + expect(keys).toContain("key1"); + expect(keys).toContain("key2"); + expect(keys).toContain("key3"); + }); + }); + + describe("getOrSet 方法", () => { + it("应该懒加载和缓存值", async () => { + const setFn = vi.fn(() => "computed-value"); + const value1 = await cacheInstance.getOrSet("new-key", setFn); + expect(setFn).toHaveBeenCalledTimes(1); + expect(value1).toBe("computed-value"); + + const setFn2 = vi.fn(() => "new-value"); + const value2 = await cacheInstance.getOrSet("new-key", setFn2); + expect(setFn2).not.toHaveBeenCalled(); + expect(value2).toBe("computed-value"); + }); + }); + + describe("tx 方法(事务)", () => { + it("应该能够读取、修改和删除值", async () => { + // 设置值 + await cacheInstance.tx("tx-key", (val, tx) => { + tx.set("new-value"); + }); + expect(await cacheInstance.get("tx-key")).toBe("new-value"); + + // 基于现有值修改 + await cacheInstance.tx("tx-key", (val: string | undefined, tx) => { + tx.set((val || "") + "-updated"); + }); + expect(await cacheInstance.get("tx-key")).toBe("new-value-updated"); + + // 删除值 + await cacheInstance.tx("tx-key", (val, tx) => { + tx.del(); + }); + expect(await cacheInstance.has("tx-key")).toBe(false); + }); + }); + + describe("incr 方法", () => { + it("应该能够增加和减少数值", async () => { + expect(await cacheInstance.incr("counter", 5)).toBe(5); + expect(await cacheInstance.incr("counter", 3)).toBe(8); + expect(await cacheInstance.incr("counter", -2)).toBe(6); + expect(await cacheInstance.get("counter")).toBe(6); + }); + }); + + describe("并发操作", () => { + it("同一个键的事务应该串行化执行", async () => { + await cacheInstance.set("tx-counter", 0); + const executionOrder: number[] = []; + + const promises = [ + cacheInstance.tx("tx-counter", async (val: number | undefined, tx) => { + executionOrder.push(1); + await new Promise((resolve) => setTimeout(resolve, 30)); + tx.set((val || 0) + 1); + return 1; + }), + cacheInstance.tx("tx-counter", async (val: number | undefined, tx) => { + executionOrder.push(2); + await new Promise((resolve) => setTimeout(resolve, 20)); + tx.set((val || 0) + 1); + return 2; + }), + cacheInstance.tx("tx-counter", async (val: number | undefined, tx) => { + executionOrder.push(3); + await new Promise((resolve) => setTimeout(resolve, 10)); + tx.set((val || 0) + 1); + return 3; + }), + ]; + + const results = await Promise.all(promises); + + // 验证事务按顺序执行,每个事务都返回正确的递增值 + expect(executionOrder).toEqual([1, 2, 3]); + expect(results).toEqual([1, 2, 3]); + expect(await cacheInstance.get("tx-counter")).toBe(3); + }); + + it("不同键的事务可以并发执行", async () => { + const startTime = Date.now(); + + await Promise.all([ + cacheInstance.tx("key-a", async (val, tx) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + tx.set("value-a"); + }), + cacheInstance.tx("key-b", async (val, tx) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + tx.set("value-b"); + }), + cacheInstance.tx("key-c", async (val, tx) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + tx.set("value-c"); + }), + ]); + + const duration = Date.now() - startTime; + + // 如果并发执行,总时间应该接近单个操作的时间(约50ms) + // 如果串行执行,总时间会接近 150ms + expect(duration).toBeLessThan(100); + }); + + it("并发 incr 操作应该正确累加", async () => { + await cacheInstance.set("incr-concurrent", 0); + + const promises = Array.from({ length: 10 }, () => cacheInstance.incr("incr-concurrent", 1)); + const results = await Promise.all(promises); + + // 每个操作都应该返回唯一的递增值 + const sortedResults = [...results].sort((a, b) => a - b); + expect(sortedResults).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(await cacheInstance.get("incr-concurrent")).toBe(10); + }); + }); + + describe("边界情况", () => { + it("应该能够处理特殊值", async () => { + await cacheInstance.set("empty-value", ""); + expect(await cacheInstance.get("empty-value")).toBe(""); + + await cacheInstance.set("null-value", null); + expect(await cacheInstance.get("null-value")).toBe(null); + + await cacheInstance.set("overwrite", "original"); + await cacheInstance.set("overwrite", "updated"); + expect(await cacheInstance.get("overwrite")).toBe("updated"); + }); + }); +}); diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 080301d10..5d0d55e2d 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -80,7 +80,7 @@ export class ScriptService { listenerScriptInstall() { // 初始化脚本安装监听 chrome.webNavigation.onBeforeNavigate.addListener( - (req: chrome.webNavigation.WebNavigationBaseCallbackDetails) => { + (req: chrome.webNavigation.WebNavigationParentedCallbackDetails) => { const lastError = chrome.runtime.lastError; if (lastError) { console.error("chrome.runtime.lastError in chrome.webNavigation.onBeforeNavigate:", lastError); @@ -160,21 +160,6 @@ export class ScriptService { } ); - // chrome.webRequest.onHeadersReceived.addListener( - // (details) => { - // const lastError = chrome.runtime.lastError; - // if (lastError) { - // console.error(lastError.message); - // } - // console.log("onHeadersReceived inspect", details); - // return undefined; - // }, - // { - // urls: ["*://*/*.user.js", "*://*/*.user.bg.js", "*://*/*.user.sub.js"], - // }, - // ["responseHeaders"] - // ); - // 兼容 chrome 内核 < 128 处理 const browserType = getBrowserType(); const addResponseHeaders = browserType.chrome && browserType.chromeVersion >= 128; diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 4886e5111..ee282d963 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -607,12 +607,6 @@ function App() { }; }, [memoWatchFile]); - // 处理没有 uuid 和 file 的情况 - // const handleCreateNewScript = () => { - // const newUuid = uuidv4(); - // setSearchParams({ uuid: newUuid }, { replace: true }); - // }; - // 检查是否有 uuid 或 file const hasUUIDorFile = useMemo(() => { return !!(searchParams.get("uuid") || searchParams.get("file")); diff --git a/src/pkg/utils/utils.test.ts b/src/pkg/utils/utils.test.ts index 4f60ad633..77613a1e7 100644 --- a/src/pkg/utils/utils.test.ts +++ b/src/pkg/utils/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, beforeAll } from "vitest"; -import { aNow, checkSilenceUpdate, cleanFileName, stringMatching, toCamelCase } from "./utils"; +import { aNow, checkSilenceUpdate, cleanFileName, formatBytes, stringMatching, toCamelCase } from "./utils"; import { ltever, versionCompare } from "@App/pkg/utils/semver"; import { nextTime } from "./cron"; import dayjs from "dayjs"; @@ -373,3 +373,28 @@ describe.concurrent("toCamelCase", () => { expect(toCamelCase("script_list_column_width")).toBe("ScriptListColumnWidth"); }); }); + +describe.concurrent("formatBytes", () => { + it.concurrent("应当正确格式化字节大小", () => { + // 0 字节 + expect(formatBytes(0)).toBe("0 B"); + + // 字节单位 + expect(formatBytes(100)).toBe("100.00 B"); + expect(formatBytes(512)).toBe("512.00 B"); + + // KB 单位 + expect(formatBytes(1024)).toBe("1.00 KB"); + expect(formatBytes(2048)).toBe("2.00 KB"); + expect(formatBytes(1536)).toBe("1.50 KB"); + + // MB 单位 + expect(formatBytes(1048576)).toBe("1.00 MB"); + expect(formatBytes(2097152)).toBe("2.00 MB"); + expect(formatBytes(1234567)).toBe("1.18 MB"); + + // 自定义小数位数 + expect(formatBytes(1536, 0)).toBe("2 KB"); + expect(formatBytes(1536, 1)).toBe("1.5 KB"); + }); +}); From d18b9cb1e11fc8f2a4cb31b3a88bc692bdcc60d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 20 Nov 2025 15:05:26 +0800 Subject: [PATCH 29/30] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtypecheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/script.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 5d0d55e2d..3b68a51c4 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -80,7 +80,7 @@ export class ScriptService { listenerScriptInstall() { // 初始化脚本安装监听 chrome.webNavigation.onBeforeNavigate.addListener( - (req: chrome.webNavigation.WebNavigationParentedCallbackDetails) => { + (req: chrome.webNavigation.WebNavigationBaseCallbackDetails) => { const lastError = chrome.runtime.lastError; if (lastError) { console.error("chrome.runtime.lastError in chrome.webNavigation.onBeforeNavigate:", lastError); From 33383ec0472bacba95fa59584d5bdbc74ad12f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 20 Nov 2025 15:10:55 +0800 Subject: [PATCH 30/30] =?UTF-8?q?=E6=B7=BB=E5=8A=A0dels=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/cache.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/cache.test.ts b/src/app/cache.test.ts index 520f2c52b..c45c1cdaa 100644 --- a/src/app/cache.test.ts +++ b/src/app/cache.test.ts @@ -56,6 +56,16 @@ describe("Cache", () => { expect(await cacheInstance.has("key2")).toBe(false); }); + it("应该能够批量删除键", async () => { + await cacheInstance.batchSet({ key1: "v1", key2: "v2", key3: "v3", key4: "v4" }); + + await (cacheInstance as any).dels(["key1", "key2", "key3"]); + expect(await cacheInstance.has("key1")).toBe(false); + expect(await cacheInstance.has("key2")).toBe(false); + expect(await cacheInstance.has("key3")).toBe(false); + expect(await cacheInstance.has("key4")).toBe(true); + }); + it("应该返回所有键的列表", async () => { await cacheInstance.batchSet({ key1: "v1", key2: "v2", key3: "v3" }); const keys = await cacheInstance.list();