From 28ffa93e9c1546a77c6f144ebe675a6e748fe7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 11 Mar 2026 21:23:54 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20=E6=94=AF=E6=8C=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8C=96=20Favicon=20=E6=9C=8D=E5=8A=A1=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E8=80=85=20#1268?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增图标服务设置项,支持 ScriptCat / 本地获取 / Google 三种 provider, 默认使用 ScriptCat API。切换 provider 时自动清除 favicon 缓存。 同时增加了对接口返回异常状态码(>=300)的处理。 --- 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/pages/options/routes/ScriptList/hooks.tsx | 4 ++- src/pages/options/routes/Setting.tsx | 36 +++++++++++++++++++ src/pages/store/favicons.ts | 32 +++++++++++++---- src/pkg/config/config.ts | 10 ++++++ 11 files changed, 110 insertions(+), 7 deletions(-) diff --git a/src/locales/de-DE/translation.json b/src/locales/de-DE/translation.json index 9df67cfd8..b01f555b4 100644 --- a/src/locales/de-DE/translation.json +++ b/src/locales/de-DE/translation.json @@ -581,6 +581,11 @@ "maybe_later": "Vielleicht später", "settings_hint": "Sie können diese Option jederzeit in den Einstellungen ändern." }, + "favicon_service": "Favicon-Dienst", + "favicon_service_desc": "Dienst zum Abrufen von Website-Symbolen auswählen", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_local": "Lokal abrufen", + "favicon_service_google": "Google", "editor": { "show_script_list": "Skriptliste anzeigen", "hide_script_list": "Skriptliste ausblenden" diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 9769ef7f7..186228e50 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -581,6 +581,11 @@ "maybe_later": "Maybe Later", "settings_hint": "You can change this option in settings at any time." }, + "favicon_service": "Favicon Service", + "favicon_service_desc": "Choose the service for fetching website icons", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_local": "Local Fetch", + "favicon_service_google": "Google", "editor": { "show_script_list": "Show Script List", "hide_script_list": "Hide Script List" diff --git a/src/locales/ja-JP/translation.json b/src/locales/ja-JP/translation.json index 823949c52..3b02d866c 100644 --- a/src/locales/ja-JP/translation.json +++ b/src/locales/ja-JP/translation.json @@ -581,6 +581,11 @@ "maybe_later": "後で", "settings_hint": "設定ページでいつでも変更できます。" }, + "favicon_service": "Favicon サービス", + "favicon_service_desc": "ウェブサイトアイコンの取得サービスを選択", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_local": "ローカル取得", + "favicon_service_google": "Google", "editor": { "show_script_list": "スクリプトリストを表示", "hide_script_list": "スクリプトリストを非表示" diff --git a/src/locales/ru-RU/translation.json b/src/locales/ru-RU/translation.json index 1b6b72973..e7ba825bb 100644 --- a/src/locales/ru-RU/translation.json +++ b/src/locales/ru-RU/translation.json @@ -581,6 +581,11 @@ "maybe_later": "Может быть позже", "settings_hint": "Вы можете изменить эту опцию в настройках в любое время." }, + "favicon_service": "Сервис Favicon", + "favicon_service_desc": "Выберите сервис для получения значков сайтов", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_local": "Локальное получение", + "favicon_service_google": "Google", "editor": { "show_script_list": "Показать список скриптов", "hide_script_list": "Скрыть список скриптов" diff --git a/src/locales/vi-VN/translation.json b/src/locales/vi-VN/translation.json index c69cb10be..96788df47 100644 --- a/src/locales/vi-VN/translation.json +++ b/src/locales/vi-VN/translation.json @@ -581,6 +581,11 @@ "maybe_later": "Để sau", "settings_hint": "Bạn có thể thay đổi tùy chọn này trong cài đặt bất kỳ lúc nào." }, + "favicon_service": "Dịch vụ Favicon", + "favicon_service_desc": "Chọn dịch vụ để lấy biểu tượng trang web", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_local": "Lấy cục bộ", + "favicon_service_google": "Google", "editor": { "show_script_list": "Hiển thị danh sách script", "hide_script_list": "Ẩn danh sách script" diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 1267d3472..105fc3855 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -581,6 +581,11 @@ "maybe_later": "暂不启用", "settings_hint": "你可以随时在设置中修改此选项。" }, + "favicon_service": "图标服务", + "favicon_service_desc": "选择获取网站图标的服务", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_local": "本地获取", + "favicon_service_google": "Google", "editor": { "show_script_list": "显示脚本列表", "hide_script_list": "隐藏脚本列表" diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index a56ddd57e..e86a5b42a 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -581,6 +581,11 @@ "maybe_later": "暫不啟用", "settings_hint": "你可以隨時在設定中修改此選項。" }, + "favicon_service": "圖示服務", + "favicon_service_desc": "選擇取得網站圖示的服務", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_local": "本地取得", + "favicon_service_google": "Google", "editor": { "show_script_list": "顯示腳本列表", "hide_script_list": "隱藏腳本列表" diff --git a/src/pages/options/routes/ScriptList/hooks.tsx b/src/pages/options/routes/ScriptList/hooks.tsx index 425082748..6cb0909ee 100644 --- a/src/pages/options/routes/ScriptList/hooks.tsx +++ b/src/pages/options/routes/ScriptList/hooks.tsx @@ -10,6 +10,7 @@ import { } from "@App/app/repo/scripts"; import { fetchScript, fetchScriptList } from "@App/pages/store/features/script"; import { loadScriptFavicons } from "@App/pages/store/favicons"; +import { systemConfig } from "@App/pages/store/global"; import { parseTags } from "@App/app/repo/metadata"; import { getCombinedMeta } from "@App/app/service/service_worker/utils"; import { cacheInstance } from "@App/app/cache"; @@ -76,7 +77,8 @@ export function useScriptDataManagement() { setLoadingList(false); cacheInstance.tx("faviconOPFSControl", async () => { if (!mounted) return; - for await (const { chunkResults } of loadScriptFavicons(list)) { + const faviconService = await systemConfig.getFaviconService(); + for await (const { chunkResults } of loadScriptFavicons(list, faviconService)) { if (!mounted) return; setScriptList((prev) => { const favMap = new Map(chunkResults.map((r) => [r.uuid, r])); diff --git a/src/pages/options/routes/Setting.tsx b/src/pages/options/routes/Setting.tsx index afb779499..9f22a9ded 100644 --- a/src/pages/options/routes/Setting.tsx +++ b/src/pages/options/routes/Setting.tsx @@ -15,6 +15,7 @@ import CustomTrans from "@App/pages/components/CustomTrans"; import { useSystemConfig } from "./utils"; import { subscribeMessage } from "@App/pages/store/global"; import { SystemConfigChange, type SystemConfigKey } from "@App/pkg/config/config"; +import { FaviconDAO } from "@App/app/repo/favicon"; import { type TKeyValue } from "@Packages/message/message_queue"; import { useEffect, useMemo } from "react"; import { systemConfig } from "@App/pages/store/global"; @@ -41,6 +42,7 @@ function Setting() { const [badgeTextColor, setBadgeTextColor, submitBadgeTextColor] = useSystemConfig("badge_text_color"); const [scriptMenuDisplayType, setScriptMenuDisplayType, submitScriptMenuDisplayType] = useSystemConfig("script_menu_display_type"); + const [faviconService, setFaviconService, submitFaviconService] = useSystemConfig("favicon_service"); const [editorTypeDefinition, setEditorTypeDefinition, submitEditorTypeDefinition] = useSystemConfig("editor_type_definition"); @@ -81,6 +83,7 @@ function Setting() { badge_background_color: setBadgeBackgroundColor, badge_text_color: setBadgeTextColor, script_menu_display_type: setScriptMenuDisplayType, + favicon_service: setFaviconService, editor_type_definition: setEditorTypeDefinition, } as const; const hookMgr = new HookManager(); @@ -306,6 +309,39 @@ function Setting() { + + {/* Favicon 服务 */} +
+
{t("favicon_service")}
+
+
+ +
+ {t("favicon_service_desc")} +
+
diff --git a/src/pages/store/favicons.ts b/src/pages/store/favicons.ts index 0ed61ef6f..1ba9d7da0 100644 --- a/src/pages/store/favicons.ts +++ b/src/pages/store/favicons.ts @@ -3,6 +3,7 @@ import { FaviconDAO, type FaviconFile, type FaviconRecord } from "@App/app/repo/ import { v5 as uuidv5 } from "uuid"; import { getFaviconRootFolder } from "@App/app/service/service_worker/utils"; import { readBlobContent } from "@App/pkg/utils/encoding"; +import type { FaviconService } from "@App/pkg/config/config"; let scriptDAO: ScriptDAO | null = null; let faviconDAO: FaviconDAO | null = null; @@ -179,8 +180,22 @@ export async function fetchIconByDomain(domain: string): Promise { return urls.filter((url) => !!url) as string[]; } +/** + * 根据服务类型获取favicon URL列表 + */ +export async function fetchIconByService(domain: string, service: FaviconService): Promise { + switch (service) { + case "scriptcat": + return [`https://ext.scriptcat.org/api/v1/open/favicons?domain=${encodeURIComponent(domain)}&sz=64`]; + case "google": + return [`https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`]; + case "local": + return fetchIconByDomain(domain); + } +} + // 获取脚本的favicon -export const getScriptFavicon = async (uuid: string): Promise => { +export const getScriptFavicon = async (uuid: string, service: FaviconService = "scriptcat"): Promise => { scriptDAO ||= new ScriptDAO(); faviconDAO ||= new FaviconDAO(); const script = await scriptDAO.get(uuid); @@ -199,7 +214,7 @@ export const getScriptFavicon = async (uuid: string): Promise = domains.map(async (domain) => { try { if (domain.domain) { - const icons = await fetchIconByDomain(domain.domain); + const icons = await fetchIconByService(domain.domain, service); const icon = icons.length > 0 ? icons[0] : ""; return { match: domain.match, website: "http://" + domain.domain, icon }; } @@ -233,6 +248,11 @@ export const loadFavicon = async (iconUrl: string): Promise => { // 文件不存在,下载并保存 const newFileHandle = await directoryHandle.getFileHandle(filename, { create: true }); const response = await fetch(iconUrl); + if (response.status >= 300) { + // 状态码异常,删除创建的空文件并抛出错误 + await directoryHandle.removeEntry(filename).catch(() => {}); + throw new Error(`Favicon fetch failed with status ${response.status}`); + } const blob = await response.blob(); const writable = await newFileHandle.createWritable(); await writable.write(blob); @@ -256,9 +276,9 @@ const getFileFromOPFS = async (opfsRet: FaviconFile): Promise => { }; // 处理单个脚本的favicon -const processScriptFavicon = async (script: Script) => { +const processScriptFavicon = async (script: Script, service: FaviconService = "scriptcat") => { const favFnAsync = async () => { - const icons = await getScriptFavicon(script.uuid); // 恒久。不会因SW重启而失效 + const icons = await getScriptFavicon(script.uuid, service); // 恒久。不会因SW重启而失效 if (icons.length === 0) return []; const newIcons = await Promise.all( icons.map(async (icon) => { @@ -305,7 +325,7 @@ type FavIconResult = { type TFaviconStack = { chunkResults: FavIconResult[]; pendingCount: number }; // 处理favicon加载,以批次方式处理 -export const loadScriptFavicons = async function* (scripts: Script[]) { +export const loadScriptFavicons = async function* (scripts: Script[], service: FaviconService = "scriptcat") { const stack: TFaviconStack[] = []; const asyncWaiter: { promise?: any; resolve?: any } = {}; const createPromise = () => { @@ -319,7 +339,7 @@ export const loadScriptFavicons = async function* (scripts: Script[]) { const results: FavIconResult[] = []; let waiting = false; for (const script of scripts) { - processScriptFavicon(script).then((result: FavIconResult) => { + processScriptFavicon(script, service).then((result: FavIconResult) => { results.push(result); // 下一个 MacroTask 执行。 // 使用 requestAnimationFrame 而非setTimeout 是因为前台才要显示。而且网页绘画中时会延后这个 diff --git a/src/pkg/config/config.ts b/src/pkg/config/config.ts index 4215fb516..1f98fcebc 100644 --- a/src/pkg/config/config.ts +++ b/src/pkg/config/config.ts @@ -19,6 +19,8 @@ export type CloudSyncConfig = { params: { [key: string]: any }; }; +export type FaviconService = "scriptcat" | "local" | "google"; + export type CATFileStorage = { filesystem: FileSystemType; params: { [key: string]: any }; @@ -474,6 +476,14 @@ export class SystemConfig { getScriptMenuDisplayType(): Promise<"no_browser" | "all"> { return this._get("script_menu_display_type", "all"); } + + getFaviconService() { + return this._get("favicon_service", "scriptcat"); + } + + setFaviconService(val: FaviconService) { + this._set("favicon_service", val); + } } let lazyScriptNamePrefix: string = ""; From 7bfe3b11498bfdb350e3a005ff9d1313ad15b895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 11 Mar 2026 21:25:33 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E6=9C=8D=E5=8A=A1=E8=AE=BE=E7=BD=AE=E9=A1=B9=E5=B8=83?= =?UTF-8?q?=E5=B1=80=EF=BC=8C=E6=A0=87=E9=A2=98=E4=B8=8E=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E6=A1=86=E6=94=BE=E5=9C=A8=E5=90=8C=E4=B8=80=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/options/routes/Setting.tsx | 56 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/pages/options/routes/Setting.tsx b/src/pages/options/routes/Setting.tsx index 9f22a9ded..41ff6e6a3 100644 --- a/src/pages/options/routes/Setting.tsx +++ b/src/pages/options/routes/Setting.tsx @@ -311,36 +311,34 @@ function Setting() { {/* Favicon 服务 */} -
-
{t("favicon_service")}
-
-
- -
- {t("favicon_service_desc")} +
+
+ {t("favicon_service")} +
+ {t("favicon_service_desc")}