Skip to content

Commit d10a612

Browse files
committed
fix(sniffer): 修复加载挂起超时测试与 Prettier 自动格式化
1 parent 3e25223 commit d10a612

21 files changed

Lines changed: 234 additions & 167 deletions

src/core/resolvers/pixiv.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ export class PixivResolver implements IUrlResolver {
1111
const isNovelCover = /\/(sci|ci)\d+_[a-zA-Z0-9]+/.test(url);
1212

1313
// 1. 小说封面等带有 sci/ci 前缀的图片原图存放在 /novel-cover-original/ 目录下,其他常规图片存放在 /img-original/ 目录下
14-
const originalDir = isNovelCover ? "/novel-cover-original/" : "/img-original/";
14+
const originalDir = isNovelCover
15+
? "/novel-cover-original/"
16+
: "/img-original/";
1517

1618
let resolved = url
1719
.replace(/\/c\/[^/]+\/[^/]+/, "") // 匹配双层缩略路径
18-
.replace(/\/c\/[^/]+/, "") // 匹配单层缩略路径
20+
.replace(/\/c\/[^/]+/, "") // 匹配单层缩略路径
1921
.replace(/\/(img-master|custom-thumb)\//, originalDir)
20-
.replace(/i\.pximg\.net\/img\/(\d{4})\//, `i.pximg.net${originalDir}img/$1/`); // 适配小说/插画封面路径,锚定域名防递归
22+
.replace(
23+
/i\.pximg\.net\/img\/(\d{4})\//,
24+
`i.pximg.net${originalDir}img/$1/`,
25+
); // 适配小说/插画封面路径,锚定域名防递归
2126

2227
// 2. 剥离文件名中的尺寸后缀,如 _p0_master1200, _p0_custom1200, _p0_square1200, _master1200
2328
resolved = resolved.replace(/_(master|custom|square)\d+/, "");

src/core/resolvers/weibo.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@ export class WeiboResolver implements IUrlResolver {
1010
resolve(url: string): string {
1111
const resolved = url.replace(
1212
/\/(mw\d+|thumbnail|orj\d+|square|bmiddle|woriginal|small|thumb\d+|wap\d+|crop\.\d+\.\d+\.\d+\.\d+\.\d+)\//,
13-
"/large/"
13+
"/large/",
1414
);
1515
return resolved.split("?")[0];
1616
}
1717

18-
static parseDimensions(url: string): { width: number; height: number } | null {
18+
static parseDimensions(
19+
url: string,
20+
): { width: number; height: number } | null {
1921
try {
20-
const match = url.match(/\/([a-zA-Z0-9]{8,22})(ly1|gy1|gy3|ly3|my1|j6|j2|j3|j|g|mw\d+|orj\d+)?([a-zA-Z0-9]{3})([a-zA-Z0-9]{3})([a-zA-Z0-9]{2,4})\.(jpg|jpeg|png|webp|gif)$/i);
22+
const match = url.match(
23+
/\/([a-zA-Z0-9]{8,22})(ly1|gy1|gy3|ly3|my1|j6|j2|j3|j|g|mw\d+|orj\d+)?([a-zA-Z0-9]{3})([a-zA-Z0-9]{3})([a-zA-Z0-9]{2,4})\.(jpg|jpeg|png|webp|gif)$/i,
24+
);
2125
if (!match) return null;
2226

2327
const widthStr = match[3];

src/core/sniffer.ts

Lines changed: 112 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -559,25 +559,29 @@ export class Sniffer {
559559
} else {
560560
const results: ImageItem[] = response.results || [];
561561
const updatedResults = [...results];
562-
562+
563563
// 在侧边栏环境重新获取防盗链域名的真实宽高(DNR 规则已生效,可正常加载)
564-
await runConcurrent(updatedResults, METADATA_CONCURRENCY, async (item, index) => {
565-
const needUpdate =
566-
item.url.includes("sinaimg.cn") ||
567-
item.url.includes("weibo.com");
568-
if (needUpdate) {
569-
const meta = await this.getImageMetadata(item.url);
570-
if (meta) {
571-
updatedResults[index] = {
572-
...item,
573-
width: meta.width,
574-
height: meta.height,
575-
sizeKB: meta.sizeKB > 0 ? meta.sizeKB : item.sizeKB,
576-
};
564+
await runConcurrent(
565+
updatedResults,
566+
METADATA_CONCURRENCY,
567+
async (item, index) => {
568+
const needUpdate =
569+
item.url.includes("sinaimg.cn") ||
570+
item.url.includes("weibo.com");
571+
if (needUpdate) {
572+
const meta = await this.getImageMetadata(item.url);
573+
if (meta) {
574+
updatedResults[index] = {
575+
...item,
576+
width: meta.width,
577+
height: meta.height,
578+
sizeKB: meta.sizeKB > 0 ? meta.sizeKB : item.sizeKB,
579+
};
580+
}
577581
}
578-
}
579-
});
580-
582+
},
583+
);
584+
581585
resolve(updatedResults);
582586
}
583587
},
@@ -612,7 +616,8 @@ export class Sniffer {
612616
settings?.interfaceBehavior?.identifyBackgroundImages ?? true;
613617
const isTelegramHost = window.location.host.includes("telegram");
614618
const identifyBlob =
615-
(settings?.interfaceBehavior?.identifyBlobImages ?? false) || isTelegramHost;
619+
(settings?.interfaceBehavior?.identifyBlobImages ?? false) ||
620+
isTelegramHost;
616621

617622
const isPixiv = window.location.href.includes("pixiv.net");
618623
const [treeUrls, perfUrls, svgUrls] = await Promise.all([
@@ -629,11 +634,18 @@ export class Sniffer {
629634
) {
630635
const resolved = UrlResolver.transformSiteSpecificUrl(url);
631636
// 过滤掉 weibo.com 的网页链接(如 /u/false 或 /status/ 等非真实图片)
632-
if (resolved.includes("weibo.com") && !resolved.match(/\.(jpg|jpeg|png|gif|webp|svg)/i)) {
637+
if (
638+
resolved.includes("weibo.com") &&
639+
!resolved.match(/\.(jpg|jpeg|png|gif|webp|svg)/i)
640+
) {
633641
return;
634642
}
635643
// 如果在 Pixiv 网站,过滤掉所有的 SVG 资源,防止 UI 背景渐变/图标等乱入
636-
if (isPixiv && (resolved.includes("image/svg+xml") || resolved.toLowerCase().includes(".svg"))) {
644+
if (
645+
isPixiv &&
646+
(resolved.includes("image/svg+xml") ||
647+
resolved.toLowerCase().includes(".svg"))
648+
) {
637649
return;
638650
}
639651
urls.add(resolved);
@@ -669,20 +681,6 @@ export class Sniffer {
669681
pageTitle: document.title,
670682
pageUrl: window.location.href,
671683
});
672-
} else {
673-
// 兜底保留:如果测量超时或失败,不直接丢弃图片,而是生成兜底的 ImageItem!
674-
items.push({
675-
url,
676-
width: 0,
677-
height: 0,
678-
sizeKB: 0,
679-
format: ImageTypeDetector.getFormatFromUrl(url),
680-
filename: url.split("/").pop()?.split(/[?#]/)[0] || "image",
681-
id,
682-
isSelected: false,
683-
pageTitle: document.title,
684-
pageUrl: window.location.href,
685-
});
686684
}
687685
});
688686

@@ -717,82 +715,91 @@ export class Sniffer {
717715
filename: url.split("/").pop()?.split(/[?#]/)[0] || "image",
718716
};
719717
}
720-
721718
try {
722-
const dimensions = await new Promise<{ width: number; height: number }>(
723-
(resolve, reject) => {
724-
const img = new Image();
725-
const timeoutId = window.setTimeout(() => {
726-
cleanup();
727-
reject(
728-
new Error(
719+
let dimensions: { width: number; height: number };
720+
try {
721+
dimensions = await new Promise<{ width: number; height: number }>(
722+
(resolve, reject) => {
723+
const img = new Image();
724+
const timeoutId = window.setTimeout(() => {
725+
cleanup();
726+
const err = new Error(
729727
`Timed out loading image metadata: ${String(url).slice(0, 100)}`,
730-
),
731-
);
732-
}, IMAGE_METADATA_TIMEOUT_MS);
733-
const cleanup = () => {
734-
window.clearTimeout(timeoutId);
735-
img.onload = null;
736-
img.onerror = null;
737-
try {
738-
img.src = "";
739-
} catch {
740-
// 忽略清理失败
741-
}
742-
};
728+
);
729+
(err as Error & { isTimeout?: boolean }).isTimeout = true;
730+
reject(err);
731+
}, IMAGE_METADATA_TIMEOUT_MS);
732+
const cleanup = () => {
733+
window.clearTimeout(timeoutId);
734+
img.onload = null;
735+
img.onerror = null;
736+
try {
737+
img.src = "";
738+
} catch {
739+
// 忽略清理失败
740+
}
741+
};
743742

744-
img.onload = () => {
745-
const size = {
746-
width: img.naturalWidth,
747-
height: img.naturalHeight,
743+
img.onload = () => {
744+
const size = {
745+
width: img.naturalWidth,
746+
height: img.naturalHeight,
747+
};
748+
cleanup();
749+
resolve(size);
748750
};
749-
cleanup();
750-
resolve(size);
751-
};
752-
img.onerror = () => {
753-
cleanup();
754-
reject(
755-
new Error(`Failed to load image: ${String(url).slice(0, 100)}`),
756-
);
757-
};
758-
// 仅侧边栏页面(chrome-extension:// 协议)才需要走后台 Blob 代理。
759-
// Content script 运行在目标页上下文,可直接加载且受 DNR 规则保护,无需代理。
760-
// 对于本地内存中的 blob: 图片,后台 Service Worker 无法跨越沙盒拉取,直接不走代理。
761-
const isSidePanelPage =
762-
typeof window !== "undefined" &&
763-
window.location.protocol === "chrome-extension:" &&
764-
typeof chrome !== "undefined" &&
765-
!!chrome.runtime?.sendMessage;
766-
767-
const isBlobUrl = url.startsWith("blob:");
768-
769-
if (isSidePanelPage && !isBlobUrl) {
770-
// 根据域名传入对应的防盗链 Referer,Service Worker fetch 不受 DNR 规则覆盖
771-
let referer: string | undefined;
772-
if (url.includes("sinaimg.cn") || url.includes("weibo.com")) {
773-
referer = "https://weibo.com/";
774-
} else if (url.includes("pximg.net")) {
775-
referer = "https://www.pixiv.net/";
751+
img.onerror = () => {
752+
cleanup();
753+
reject(
754+
new Error(`Failed to load image: ${String(url).slice(0, 100)}`),
755+
);
756+
};
757+
// 仅侧边栏页面(chrome-extension:// 协议)才需要走后台 Blob 代理。
758+
// Content script 运行在目标页上下文,可直接加载且受 DNR 规则保护,无需代理。
759+
// 对于本地内存中的 blob: 图片,后台 Service Worker 无法跨越沙盒拉取,直接不走代理。
760+
const isSidePanelPage =
761+
typeof window !== "undefined" &&
762+
window.location.protocol === "chrome-extension:" &&
763+
typeof chrome !== "undefined" &&
764+
!!chrome.runtime?.sendMessage;
765+
766+
const isBlobUrl = url.startsWith("blob:");
767+
768+
if (isSidePanelPage && !isBlobUrl) {
769+
// 根据域名传入对应的防盗链 Referer,Service Worker fetch 不受 DNR 规则覆盖
770+
let referer: string | undefined;
771+
if (url.includes("sinaimg.cn") || url.includes("weibo.com")) {
772+
referer = "https://weibo.com/";
773+
} else if (url.includes("pximg.net")) {
774+
referer = "https://www.pixiv.net/";
775+
}
776+
chrome.runtime.sendMessage(
777+
{
778+
type: "FETCH_BLOB",
779+
payload: { url, referer },
780+
},
781+
(response) => {
782+
if (response?.success && response.arrayBuffer) {
783+
const mimeType = response.mimeType || "image/jpeg";
784+
img.src = `data:${mimeType};base64,${response.arrayBuffer}`;
785+
} else {
786+
img.src = url;
787+
}
788+
},
789+
);
790+
} else {
791+
img.src = url;
776792
}
777-
chrome.runtime.sendMessage(
778-
{
779-
type: "FETCH_BLOB",
780-
payload: { url, referer },
781-
},
782-
(response) => {
783-
if (response?.success && response.arrayBuffer) {
784-
const mimeType = response.mimeType || "image/jpeg";
785-
img.src = `data:${mimeType};base64,${response.arrayBuffer}`;
786-
} else {
787-
img.src = url;
788-
}
789-
},
790-
);
791-
} else {
792-
img.src = url;
793-
}
794-
},
795-
);
793+
},
794+
);
795+
} catch (err) {
796+
const error = err as Error & { isTimeout?: boolean };
797+
if (error?.isTimeout) {
798+
throw error; // 扔给最外层 try...catch,直接返回 null 丢弃以符合测试对 stalled 图片的处理
799+
}
800+
// 网络加载失败(如防盗链拦截),设宽高为 0 以保留该项,便于后续代理下载
801+
dimensions = { width: 0, height: 0 };
802+
}
796803

797804
let sizeKB = 0;
798805
let format: ImageFormat = "UNKNOWN";

src/core/utils/loaded-image-candidates.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,18 @@ export function collectLoadedImageItems({
6363
if (!url.startsWith("http") && !allowBlob) continue;
6464

6565
const isPixiv = window.location.href.includes("pixiv.net");
66-
if (isPixiv && (url.includes("image/svg+xml") || url.toLowerCase().includes(".svg"))) {
66+
if (
67+
isPixiv &&
68+
(url.includes("image/svg+xml") || url.toLowerCase().includes(".svg"))
69+
) {
6770
continue;
6871
}
6972

7073
// 过滤掉 weibo.com 的网页链接(如 /u/false 或 /status/ 等非真实图片)
71-
if (url.includes("weibo.com") && !url.match(/\.(jpg|jpeg|png|gif|webp|svg)/i)) {
74+
if (
75+
url.includes("weibo.com") &&
76+
!url.match(/\.(jpg|jpeg|png|gif|webp|svg)/i)
77+
) {
7278
continue;
7379
}
7480

src/entry/background.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
7777

7878
if (message.type === "FETCH_BLOB") {
7979
const { url, referer } = message.payload;
80-
80+
8181
// 规范澄清:Chrome 扩展 Service Worker 无法在普通 fetch 中真正自定义 Referer (属于 Forbidden Headers)。
8282
// 此处能成功绕过防盗链主要是因为 SW 处于插件特权域,其发起的 fetch 请求不会被浏览器强制标记
8383
// 跨域沙盒特征(如 Sec-Fetch-Site: cross-site),使得 CDN 防火墙放行。
@@ -90,12 +90,14 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
9090
// 增加安全防御:限制超大图或大媒体文件(50MB 阈值),防止 Base64 通信信道堆栈溢出或 SW 内存崩溃
9191
const contentLength = res.headers.get("content-length");
9292
if (contentLength && parseInt(contentLength, 10) > 50 * 1024 * 1024) {
93-
throw new Error("Target resource size exceeds proxy safe limits (50MB)");
93+
throw new Error(
94+
"Target resource size exceeds proxy safe limits (50MB)",
95+
);
9496
}
9597

9698
const mimeType = res.headers.get("content-type") || "";
9799
const blob = await res.blob();
98-
100+
99101
if (blob.size > 50 * 1024 * 1024) {
100102
throw new Error("Blob payload size exceeds proxy safe limits (50MB)");
101103
}
@@ -106,7 +108,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
106108
const chunkSize = 8192;
107109
let binary = "";
108110
for (let i = 0; i < bytes.length; i += chunkSize) {
109-
binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunkSize)));
111+
binary += String.fromCharCode.apply(
112+
null,
113+
Array.from(bytes.subarray(i, i + chunkSize)),
114+
);
110115
}
111116
const base64 = btoa(binary);
112117

@@ -298,7 +303,10 @@ async function setupDeclarativeNetRequestRules() {
298303
});
299304
console.log("[Background] DeclarativeNetRequest rules set successfully.");
300305
} catch (err) {
301-
console.error("[Background] Failed to update declarativeNetRequest rules:", err);
306+
console.error(
307+
"[Background] Failed to update declarativeNetRequest rules:",
308+
err,
309+
);
302310
}
303311
}
304312

0 commit comments

Comments
 (0)