@zucker-framework/ext-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +22 -0
- package/THIRD_PARTY_NOTICES +4 -0
- package/assets/offscreen.html +5 -0
- package/assets/offscreen.js +32 -0
- package/assets/package.mjs +18 -0
- package/dist/ext-kit-popup.js +525 -0
- package/dist/ext-kit.js +955 -0
- package/dist/index.d.mts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +1818 -0
- package/dist/index.mjs +1781 -0
- package/package.json +52 -0
package/dist/ext-kit.js
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// src/util.ts
|
|
4
|
+
function delay(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
function clamp(value, min, max) {
|
|
8
|
+
return Math.min(Math.max(value, min), max);
|
|
9
|
+
}
|
|
10
|
+
function normalizeError(error) {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
function storageGet(keys) {
|
|
14
|
+
return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
|
|
15
|
+
}
|
|
16
|
+
function storageSet(value) {
|
|
17
|
+
return new Promise((resolve) => chrome.storage.local.set(value, () => resolve()));
|
|
18
|
+
}
|
|
19
|
+
function storageRemove(keys) {
|
|
20
|
+
return new Promise((resolve) => chrome.storage.local.remove(keys, () => resolve()));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/naming.ts
|
|
24
|
+
function sanitize(value) {
|
|
25
|
+
return String(value || "").replace(/[\\/:*?"<>|#&=%]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
26
|
+
}
|
|
27
|
+
function hostOf(url) {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(url).host;
|
|
30
|
+
} catch {
|
|
31
|
+
return String(url || "");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function normalizeUrls(urls) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const seen = /* @__PURE__ */ new Set();
|
|
37
|
+
for (const raw of urls || []) {
|
|
38
|
+
const value = String(raw || "").trim();
|
|
39
|
+
if (!/^https?:\/\//i.test(value)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let key = value;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(value);
|
|
45
|
+
parsed.hash = "";
|
|
46
|
+
key = parsed.toString();
|
|
47
|
+
} catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!seen.has(key)) {
|
|
51
|
+
seen.add(key);
|
|
52
|
+
out.push(key);
|
|
53
|
+
}
|
|
54
|
+
if (out.length >= 500) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function sessionStamp() {
|
|
61
|
+
const d = /* @__PURE__ */ new Date();
|
|
62
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
63
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`;
|
|
64
|
+
}
|
|
65
|
+
function fileStamp() {
|
|
66
|
+
const d = /* @__PURE__ */ new Date();
|
|
67
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
68
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
69
|
+
}
|
|
70
|
+
function todayString() {
|
|
71
|
+
const d = /* @__PURE__ */ new Date();
|
|
72
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
73
|
+
}
|
|
74
|
+
function folderNameFor(urls) {
|
|
75
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
76
|
+
for (const url of urls) {
|
|
77
|
+
try {
|
|
78
|
+
hosts.add(new URL(url).hostname.replace(/^www\./, ""));
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const stamp = sessionStamp();
|
|
83
|
+
if (hosts.size === 1) {
|
|
84
|
+
const host = sanitize([...hosts][0]).slice(0, 60);
|
|
85
|
+
return host ? `${host}-${stamp}` : stamp;
|
|
86
|
+
}
|
|
87
|
+
return stamp;
|
|
88
|
+
}
|
|
89
|
+
function urlBaseName(url, maxLength, includeQuery) {
|
|
90
|
+
let base = "page";
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
const host = parsed.hostname.replace(/^www\./, "");
|
|
94
|
+
let rest = (parsed.pathname + (includeQuery ? parsed.search : "")).replace(/\/+$/, "");
|
|
95
|
+
try {
|
|
96
|
+
rest = decodeURIComponent(rest);
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
base = host + rest;
|
|
100
|
+
} catch {
|
|
101
|
+
base = url;
|
|
102
|
+
}
|
|
103
|
+
return sanitize(base).slice(0, maxLength) || "page";
|
|
104
|
+
}
|
|
105
|
+
function buildBatchFileName(folder, item, extension) {
|
|
106
|
+
const base = urlBaseName(item.url, 120, true);
|
|
107
|
+
const index = String(item.index).padStart(3, "0");
|
|
108
|
+
return `${folder}/${index}_${base}.${extension}`;
|
|
109
|
+
}
|
|
110
|
+
function buildQuickFileName(brand, quickFolder, pageUrl, extension) {
|
|
111
|
+
const base = urlBaseName(pageUrl, 110, false);
|
|
112
|
+
return `${brand}/${quickFolder}/${base}-${fileStamp()}.${extension}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/licensing.ts
|
|
116
|
+
var CACHE_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
117
|
+
function unwrap(payload) {
|
|
118
|
+
const obj = payload;
|
|
119
|
+
return obj && typeof obj === "object" && "result" in obj ? obj.result : obj || {};
|
|
120
|
+
}
|
|
121
|
+
function createLicensing(product, brand, config) {
|
|
122
|
+
const TOKEN_KEY = `${product}_license_token`;
|
|
123
|
+
const ENTITLEMENT_KEY = `${product}_entitlement`;
|
|
124
|
+
const DEVICE_KEY = `${product}_device_id`;
|
|
125
|
+
async function getDeviceId() {
|
|
126
|
+
const data = await storageGet(DEVICE_KEY);
|
|
127
|
+
if (data[DEVICE_KEY]) {
|
|
128
|
+
return data[DEVICE_KEY];
|
|
129
|
+
}
|
|
130
|
+
const bytes = new Uint8Array(16);
|
|
131
|
+
crypto.getRandomValues(bytes);
|
|
132
|
+
const id = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
133
|
+
await storageSet({ [DEVICE_KEY]: id });
|
|
134
|
+
return id;
|
|
135
|
+
}
|
|
136
|
+
async function activate(code) {
|
|
137
|
+
const deviceId = await getDeviceId();
|
|
138
|
+
let response;
|
|
139
|
+
try {
|
|
140
|
+
const platform = navigator.userAgentData?.platform || "Chrome";
|
|
141
|
+
response = await fetch(`${config.apiBase}/extension/licenses/activate`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
code: String(code || "").trim().toUpperCase().replace(/[^A-Z0-9]/g, ""),
|
|
146
|
+
deviceId,
|
|
147
|
+
deviceName: `${brand} \xB7 ${platform}`
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
} catch {
|
|
151
|
+
throw new Error("License service unreachable. Check your connection and retry.");
|
|
152
|
+
}
|
|
153
|
+
const payload = await response.json().catch(() => ({}));
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
if (response.status === 403) {
|
|
156
|
+
throw new Error("Invalid code, or its device limit is reached.");
|
|
157
|
+
}
|
|
158
|
+
throw new Error("Activation failed. Please retry later.");
|
|
159
|
+
}
|
|
160
|
+
const data = unwrap(payload);
|
|
161
|
+
await storageSet({
|
|
162
|
+
[TOKEN_KEY]: data.token || "",
|
|
163
|
+
[ENTITLEMENT_KEY]: { ...data.entitlement || {}, checkedAt: Date.now() }
|
|
164
|
+
});
|
|
165
|
+
return data.entitlement;
|
|
166
|
+
}
|
|
167
|
+
function normalize(value) {
|
|
168
|
+
const v = value;
|
|
169
|
+
if (!v) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const expired = v.expiresAt && new Date(v.expiresAt).getTime() <= Date.now();
|
|
173
|
+
return expired ? null : { plan: v.plan || "pro", expiresAt: v.expiresAt || "" };
|
|
174
|
+
}
|
|
175
|
+
async function getEntitlement() {
|
|
176
|
+
const data = await storageGet([TOKEN_KEY, ENTITLEMENT_KEY]);
|
|
177
|
+
const token = data[TOKEN_KEY];
|
|
178
|
+
const cached = data[ENTITLEMENT_KEY];
|
|
179
|
+
if (!token) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (cached?.checkedAt && Date.now() - cached.checkedAt < CACHE_TTL_MS) {
|
|
183
|
+
return normalize(cached);
|
|
184
|
+
}
|
|
185
|
+
let response;
|
|
186
|
+
try {
|
|
187
|
+
response = await fetch(`${config.apiBase}/extension/licenses/me`, {
|
|
188
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
189
|
+
});
|
|
190
|
+
} catch {
|
|
191
|
+
return cached ? normalize(cached) : null;
|
|
192
|
+
}
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
if (response.status === 401 || response.status === 403) {
|
|
195
|
+
await clear();
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return cached ? normalize(cached) : null;
|
|
199
|
+
}
|
|
200
|
+
const payload = await response.json().catch(() => ({}));
|
|
201
|
+
const entitlement = unwrap(payload)?.entitlement;
|
|
202
|
+
await storageSet({
|
|
203
|
+
[ENTITLEMENT_KEY]: { ...entitlement || {}, checkedAt: Date.now() }
|
|
204
|
+
});
|
|
205
|
+
return normalize(entitlement);
|
|
206
|
+
}
|
|
207
|
+
async function isPro() {
|
|
208
|
+
return Boolean(await getEntitlement());
|
|
209
|
+
}
|
|
210
|
+
async function clear() {
|
|
211
|
+
await storageRemove([TOKEN_KEY, ENTITLEMENT_KEY]);
|
|
212
|
+
}
|
|
213
|
+
return { activate, getEntitlement, isPro, clear, purchaseUrl: config.purchaseUrl };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/download.ts
|
|
217
|
+
function createDownloader(justification) {
|
|
218
|
+
const pendingDownloadNames = /* @__PURE__ */ new Map();
|
|
219
|
+
let offscreenSetup = null;
|
|
220
|
+
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
|
221
|
+
const fileName = pendingDownloadNames.get(item.url) || pendingDownloadNames.get(item.finalUrl || "");
|
|
222
|
+
if (!fileName) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
suggest({ filename: fileName, conflictAction: "uniquify" });
|
|
226
|
+
return true;
|
|
227
|
+
});
|
|
228
|
+
async function ensureOffscreen() {
|
|
229
|
+
if (await chrome.offscreen.hasDocument()) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!offscreenSetup) {
|
|
233
|
+
offscreenSetup = chrome.offscreen.createDocument({
|
|
234
|
+
url: "offscreen.html",
|
|
235
|
+
reasons: [chrome.offscreen.Reason.BLOBS],
|
|
236
|
+
justification
|
|
237
|
+
}).catch((error) => {
|
|
238
|
+
if (!String(error?.message || "").includes("Only a single")) {
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}).finally(() => {
|
|
242
|
+
offscreenSetup = null;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return offscreenSetup;
|
|
246
|
+
}
|
|
247
|
+
async function downloadViaBlob(base64, mime, filename) {
|
|
248
|
+
await ensureOffscreen();
|
|
249
|
+
const created = await chrome.runtime.sendMessage({
|
|
250
|
+
target: "offscreen",
|
|
251
|
+
type: "CREATE_BLOB_URL",
|
|
252
|
+
base64,
|
|
253
|
+
mime
|
|
254
|
+
});
|
|
255
|
+
if (!created?.ok || !created.url) {
|
|
256
|
+
throw new Error(created?.error || "Failed to prepare download");
|
|
257
|
+
}
|
|
258
|
+
const url = created.url;
|
|
259
|
+
pendingDownloadNames.set(url, filename);
|
|
260
|
+
const cleanup = () => {
|
|
261
|
+
pendingDownloadNames.delete(url);
|
|
262
|
+
chrome.runtime.sendMessage({ target: "offscreen", type: "REVOKE_BLOB_URL", url }).catch(() => {
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
try {
|
|
266
|
+
const downloadId = await new Promise((resolve, reject) => {
|
|
267
|
+
chrome.downloads.download(
|
|
268
|
+
{ url, filename, conflictAction: "uniquify", saveAs: false },
|
|
269
|
+
(id) => {
|
|
270
|
+
const error = chrome.runtime.lastError;
|
|
271
|
+
error ? reject(new Error(error.message)) : resolve(id);
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
setTimeout(cleanup, 6e4);
|
|
276
|
+
return downloadId;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
cleanup();
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { downloadViaBlob };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/session.ts
|
|
286
|
+
function debuggerAttach(target, version) {
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
288
|
+
chrome.debugger.attach(target, version, () => {
|
|
289
|
+
const error = chrome.runtime.lastError;
|
|
290
|
+
error ? reject(new Error(error.message)) : resolve();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function debuggerDetach(target) {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
chrome.debugger.detach(target, () => {
|
|
297
|
+
const error = chrome.runtime.lastError;
|
|
298
|
+
error ? reject(new Error(error.message)) : resolve();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function sendCommand(target, method, params) {
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
chrome.debugger.sendCommand(target, method, params || {}, (result) => {
|
|
305
|
+
const error = chrome.runtime.lastError;
|
|
306
|
+
error ? reject(new Error(error.message)) : resolve(result || {});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function waitForLoad(tabId, timeoutMs) {
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
let settled = false;
|
|
313
|
+
const finish = (fn, value) => {
|
|
314
|
+
if (!settled) {
|
|
315
|
+
settled = true;
|
|
316
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
fn(value);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const listener = (updatedTabId, changeInfo) => {
|
|
322
|
+
if (updatedTabId === tabId && changeInfo.status === "complete") {
|
|
323
|
+
finish(resolve);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const timer = setTimeout(() => finish(reject, new Error("Page load timeout")), timeoutMs);
|
|
327
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
328
|
+
chrome.tabs.get(tabId).then((tab) => {
|
|
329
|
+
if (tab.status === "complete") {
|
|
330
|
+
finish(resolve);
|
|
331
|
+
}
|
|
332
|
+
}).catch(() => {
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function challengeError(name) {
|
|
337
|
+
const error = new Error(`Blocked by ${name} verification`);
|
|
338
|
+
error.isChallenge = true;
|
|
339
|
+
return error;
|
|
340
|
+
}
|
|
341
|
+
function isChallengeError(error) {
|
|
342
|
+
return Boolean(error?.isChallenge);
|
|
343
|
+
}
|
|
344
|
+
var CHALLENGE_TITLE_RE = /just a moment|checking your browser|attention required|verifying|security checkpoint|正在验证|安全检查点|verify you are human|are you a robot|请完成验证|人机验证|captcha/;
|
|
345
|
+
async function probePage(target) {
|
|
346
|
+
const r = await sendCommand(target, "Runtime.evaluate", {
|
|
347
|
+
returnByValue: true,
|
|
348
|
+
expression: `(function(){
|
|
349
|
+
var uri=document.documentURI||"";
|
|
350
|
+
if(uri.indexOf("chrome-error://")===0) return {error:"Page failed to load (DNS or network error)",challenge:""};
|
|
351
|
+
var t=(document.title||"").toLowerCase();
|
|
352
|
+
var b=(document.body?document.body.innerText:"").slice(0,500).toLowerCase();
|
|
353
|
+
var u=(location.href||"").toLowerCase();
|
|
354
|
+
var s=t+" "+b+" "+u;
|
|
355
|
+
var c="";
|
|
356
|
+
if(/just a moment|checking your browser|attention required|cf-browser-verification|challenges\\.cloudflare/.test(s)) c="Cloudflare";
|
|
357
|
+
else if(/verifying your browser|\u6B63\u5728\u9A8C\u8BC1\u60A8\u7684\u6D4F\u89C8\u5668|security checkpoint|\u5B89\u5168\u68C0\u67E5\u70B9/.test(s)) c="Vercel";
|
|
358
|
+
else if(/verify you are human|are you a robot|\u8BF7\u5B8C\u6210\u9A8C\u8BC1|\u4EBA\u673A\u9A8C\u8BC1|captcha/.test(s)) c="bot-check";
|
|
359
|
+
return {error:"",challenge:c};
|
|
360
|
+
})()`
|
|
361
|
+
});
|
|
362
|
+
const value = r.result?.value;
|
|
363
|
+
return value || { error: "", challenge: "" };
|
|
364
|
+
}
|
|
365
|
+
async function waitForVerification(tabId, shouldStop, timeoutMs = 18e4) {
|
|
366
|
+
const start = Date.now();
|
|
367
|
+
while (Date.now() - start < timeoutMs) {
|
|
368
|
+
if (shouldStop()) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
await delay(2500);
|
|
372
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
373
|
+
if (!tab) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
const title = (tab.title || "").toLowerCase();
|
|
377
|
+
const url = (tab.url || "").toLowerCase();
|
|
378
|
+
if (url.startsWith("chrome-error")) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (title && !CHALLENGE_TITLE_RE.test(title)) {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
async function focusTab(tabId) {
|
|
388
|
+
await chrome.tabs.update(tabId, { active: true }).catch(() => {
|
|
389
|
+
});
|
|
390
|
+
try {
|
|
391
|
+
const tab = await chrome.tabs.get(tabId);
|
|
392
|
+
if (tab.windowId != null) {
|
|
393
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function autoScroll(target) {
|
|
399
|
+
return sendCommand(target, "Runtime.evaluate", {
|
|
400
|
+
awaitPromise: true,
|
|
401
|
+
expression: `new Promise(function(resolve){
|
|
402
|
+
var distance=600, scrolled=0, guard=0;
|
|
403
|
+
var maxHeight=Math.min(document.body?document.body.scrollHeight:0, 20000);
|
|
404
|
+
var timer=setInterval(function(){
|
|
405
|
+
window.scrollBy(0, distance);
|
|
406
|
+
scrolled+=distance; guard+=1;
|
|
407
|
+
var reached=scrolled>=Math.max(maxHeight, document.body?document.body.scrollHeight:0);
|
|
408
|
+
if(reached || guard>60){
|
|
409
|
+
clearInterval(timer);
|
|
410
|
+
setTimeout(function(){ window.scrollTo(0,0); setTimeout(resolve, 400); }, 300);
|
|
411
|
+
}
|
|
412
|
+
}, 120);
|
|
413
|
+
})`
|
|
414
|
+
}).catch(() => void 0);
|
|
415
|
+
}
|
|
416
|
+
function revealAnimations(target, reversible = false) {
|
|
417
|
+
return sendCommand(target, "Runtime.evaluate", {
|
|
418
|
+
expression: `(function(){
|
|
419
|
+
try{
|
|
420
|
+
var reversible=${reversible ? "true" : "false"};
|
|
421
|
+
var state=reversible?{style:null,aos:[],inline:[]}:null;
|
|
422
|
+
var css=document.createElement('style');
|
|
423
|
+
css.textContent=[
|
|
424
|
+
'[data-aos]{opacity:1!important;transform:none!important;transition:none!important;}',
|
|
425
|
+
'.wow{visibility:visible!important;opacity:1!important;animation-name:none!important;}',
|
|
426
|
+
'.animate__animated,.animated{opacity:1!important;}',
|
|
427
|
+
'[data-scroll]{opacity:1!important;transform:none!important;}',
|
|
428
|
+
'.reveal,[data-sr],[data-sr-id],.sr-reveal{opacity:1!important;transform:none!important;visibility:visible!important;}'
|
|
429
|
+
].join('');
|
|
430
|
+
(document.head||document.documentElement).appendChild(css);
|
|
431
|
+
if(state) state.style=css;
|
|
432
|
+
document.querySelectorAll('[data-aos]').forEach(function(el){
|
|
433
|
+
if(state&&!el.classList.contains('aos-animate')) state.aos.push(el);
|
|
434
|
+
el.classList.add('aos-animate');
|
|
435
|
+
});
|
|
436
|
+
// \u901A\u7528\u515C\u5E95\uFF1A\u88AB\u52A8\u753B\u85CF\u8D77\u6765\u7684\u5185\u5BB9\u5757\uFF08opacity\u22480 \u4E14\u5E26 transition/animation\uFF09\u5F3A\u5236\u663E\u793A\u3002
|
|
437
|
+
// \u53EA\u5904\u7406 static/relative\uFF08\u666E\u901A\u5185\u5BB9\u6D41\uFF09\uFF0C\u8DF3\u8FC7 fixed/absolute\uFF08\u591A\u4E3A\u5F39\u7A97/\u83DC\u5355/\u8F6E\u64AD\uFF09\uFF0C\u907F\u514D\u8BEF\u4F24
|
|
438
|
+
var all=document.body?document.body.querySelectorAll('*'):[];
|
|
439
|
+
for(var i=0;i<all.length;i++){
|
|
440
|
+
var el=all[i], s=getComputedStyle(el);
|
|
441
|
+
if(parseFloat(s.opacity)<0.05 && (s.transitionDuration!=='0s'||s.animationName!=='none')){
|
|
442
|
+
var p=s.position;
|
|
443
|
+
if(p==='static'||p==='relative'){
|
|
444
|
+
if(state) state.inline.push({
|
|
445
|
+
el:el,
|
|
446
|
+
opacity:el.style.getPropertyValue('opacity'),
|
|
447
|
+
opacityPriority:el.style.getPropertyPriority('opacity'),
|
|
448
|
+
transform:el.style.getPropertyValue('transform'),
|
|
449
|
+
transformPriority:el.style.getPropertyPriority('transform')
|
|
450
|
+
});
|
|
451
|
+
el.style.setProperty('opacity','1','important');
|
|
452
|
+
el.style.setProperty('transform','none','important');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if(state) window.__zuckerExtRevealState=state;
|
|
457
|
+
}catch(e){}
|
|
458
|
+
})()`
|
|
459
|
+
}).catch(() => void 0);
|
|
460
|
+
}
|
|
461
|
+
async function getScrollPosition(target) {
|
|
462
|
+
const result = await sendCommand(target, "Runtime.evaluate", {
|
|
463
|
+
returnByValue: true,
|
|
464
|
+
expression: `({ x: window.scrollX || 0, y: window.scrollY || 0 })`
|
|
465
|
+
});
|
|
466
|
+
const value = result.result?.value;
|
|
467
|
+
return value || { x: 0, y: 0 };
|
|
468
|
+
}
|
|
469
|
+
function restorePageAfterQuick(target, position) {
|
|
470
|
+
const x = Number(position?.x) || 0;
|
|
471
|
+
const y = Number(position?.y) || 0;
|
|
472
|
+
return sendCommand(target, "Runtime.evaluate", {
|
|
473
|
+
expression: `(function(){
|
|
474
|
+
try{
|
|
475
|
+
var state=window.__zuckerExtRevealState;
|
|
476
|
+
if(state){
|
|
477
|
+
if(state.style&&state.style.remove) state.style.remove();
|
|
478
|
+
(state.aos||[]).forEach(function(el){ el.classList.remove('aos-animate'); });
|
|
479
|
+
(state.inline||[]).forEach(function(saved){
|
|
480
|
+
if(saved.opacity) saved.el.style.setProperty('opacity',saved.opacity,saved.opacityPriority||'');
|
|
481
|
+
else saved.el.style.removeProperty('opacity');
|
|
482
|
+
if(saved.transform) saved.el.style.setProperty('transform',saved.transform,saved.transformPriority||'');
|
|
483
|
+
else saved.el.style.removeProperty('transform');
|
|
484
|
+
});
|
|
485
|
+
delete window.__zuckerExtRevealState;
|
|
486
|
+
}
|
|
487
|
+
}catch(e){}
|
|
488
|
+
window.scrollTo(${x}, ${y});
|
|
489
|
+
})()`
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/engine.ts
|
|
494
|
+
var LOAD_TIMEOUT_MS = 3e4;
|
|
495
|
+
var SAME_HOST_MIN_GAP_MS = 1800;
|
|
496
|
+
var DEFAULT_TEXTS = {
|
|
497
|
+
alreadyRunning: "Another capture is already running",
|
|
498
|
+
cannotQuick: "This page cannot be captured. Open an http(s) website first.",
|
|
499
|
+
challengeBeforeQuick: (challenge) => `Complete the ${challenge} verification before capturing this page.`,
|
|
500
|
+
quickFolder: "Quick Captures"
|
|
501
|
+
};
|
|
502
|
+
function initExtension(config) {
|
|
503
|
+
const texts = { ...DEFAULT_TEXTS, ...config.texts || {} };
|
|
504
|
+
const freeDailyLimit = config.freeDailyLimit ?? 30;
|
|
505
|
+
const USAGE_KEY = `${config.product}_free_usage`;
|
|
506
|
+
const BATCH_KEY = `${config.product}_batch_state`;
|
|
507
|
+
const prefix = config.messagePrefix;
|
|
508
|
+
const licensing = createLicensing(config.product, config.brand, config.licensing);
|
|
509
|
+
const downloader = createDownloader(
|
|
510
|
+
`Convert large ${config.brand} output into a downloadable blob URL.`
|
|
511
|
+
);
|
|
512
|
+
let batch = idleBatch();
|
|
513
|
+
let restorePromise = restoreBatch();
|
|
514
|
+
let quickRunning = false;
|
|
515
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
516
|
+
const previousRestore = restorePromise;
|
|
517
|
+
restorePromise = (async () => {
|
|
518
|
+
await previousRestore.catch(() => {
|
|
519
|
+
});
|
|
520
|
+
batch = idleBatch();
|
|
521
|
+
await storageRemove(BATCH_KEY);
|
|
522
|
+
setBadge("");
|
|
523
|
+
})();
|
|
524
|
+
});
|
|
525
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
526
|
+
if (message?.target === "offscreen") {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
handleMessage(message).then(sendResponse).catch((error) => sendResponse({ ok: false, error: normalizeError(error) }));
|
|
530
|
+
return true;
|
|
531
|
+
});
|
|
532
|
+
function idleBatch() {
|
|
533
|
+
return {
|
|
534
|
+
running: false,
|
|
535
|
+
cancelRequested: false,
|
|
536
|
+
startedAt: null,
|
|
537
|
+
folder: "",
|
|
538
|
+
total: 0,
|
|
539
|
+
done: 0,
|
|
540
|
+
success: 0,
|
|
541
|
+
failed: 0,
|
|
542
|
+
current: "",
|
|
543
|
+
freeLimited: false,
|
|
544
|
+
needVerify: null,
|
|
545
|
+
skipVerify: false,
|
|
546
|
+
settings: { ...config.defaultSettings },
|
|
547
|
+
pro: false,
|
|
548
|
+
items: []
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async function handleMessage(message) {
|
|
552
|
+
await restorePromise;
|
|
553
|
+
switch (message.type) {
|
|
554
|
+
case `${prefix}_START`:
|
|
555
|
+
return startBatch(message.urls, message.settings);
|
|
556
|
+
// 两个后缀都接受:CAPTURE_CURRENT(截图类)/ CONVERT_CURRENT(转换类),兼容既有 popup
|
|
557
|
+
case `${prefix}_CAPTURE_CURRENT`:
|
|
558
|
+
case `${prefix}_CONVERT_CURRENT`:
|
|
559
|
+
return quickCapture(message.tabId, message.settings);
|
|
560
|
+
case `${prefix}_CANCEL`:
|
|
561
|
+
return cancelBatch();
|
|
562
|
+
case `${prefix}_CLEAR_BATCH`:
|
|
563
|
+
return clearBatch();
|
|
564
|
+
case `${prefix}_SKIP_VERIFY`:
|
|
565
|
+
batch.skipVerify = true;
|
|
566
|
+
await persistBatch();
|
|
567
|
+
return { ok: true };
|
|
568
|
+
case `${prefix}_STATE`:
|
|
569
|
+
return { ok: true, batch: publicBatch(), quota: await getQuota() };
|
|
570
|
+
case `${prefix}_ACTIVATE`:
|
|
571
|
+
await licensing.activate(message.code);
|
|
572
|
+
return { ok: true, quota: await getQuota() };
|
|
573
|
+
case `${prefix}_CLEAR_LICENSE`:
|
|
574
|
+
await licensing.clear();
|
|
575
|
+
return { ok: true, quota: await getQuota() };
|
|
576
|
+
default:
|
|
577
|
+
return { ok: false, error: "Unknown message" };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function getQuota() {
|
|
581
|
+
const entitlement = await licensing.getEntitlement().catch(() => null);
|
|
582
|
+
if (entitlement) {
|
|
583
|
+
return { pro: true, expiresAt: entitlement.expiresAt, limit: null, used: 0, remaining: 999999 };
|
|
584
|
+
}
|
|
585
|
+
const data = await storageGet(USAGE_KEY);
|
|
586
|
+
const usage = data[USAGE_KEY];
|
|
587
|
+
const today = todayString();
|
|
588
|
+
const used = usage && usage.date === today ? usage.used || 0 : 0;
|
|
589
|
+
return { pro: false, limit: freeDailyLimit, used, remaining: Math.max(0, freeDailyLimit - used) };
|
|
590
|
+
}
|
|
591
|
+
async function recordUsage(count) {
|
|
592
|
+
const data = await storageGet(USAGE_KEY);
|
|
593
|
+
const today = todayString();
|
|
594
|
+
const usage = data[USAGE_KEY];
|
|
595
|
+
const used = usage && usage.date === today ? usage.used || 0 : 0;
|
|
596
|
+
await storageSet({ [USAGE_KEY]: { date: today, used: used + count } });
|
|
597
|
+
}
|
|
598
|
+
async function startBatch(urls, settings) {
|
|
599
|
+
if (batch.running || quickRunning) {
|
|
600
|
+
return { ok: false, error: texts.alreadyRunning };
|
|
601
|
+
}
|
|
602
|
+
const normalized = normalizeUrls(urls);
|
|
603
|
+
if (!normalized.length) {
|
|
604
|
+
return { ok: false, error: "No valid http(s) URLs" };
|
|
605
|
+
}
|
|
606
|
+
const quota = await getQuota();
|
|
607
|
+
if (!quota.pro && quota.remaining <= 0) {
|
|
608
|
+
return {
|
|
609
|
+
ok: false,
|
|
610
|
+
error: `Daily free limit reached (${quota.limit} pages/day). Go Pro for unlimited pages.`
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const freeLimited = !quota.pro && normalized.length > quota.remaining;
|
|
614
|
+
const accepted = quota.pro ? normalized : normalized.slice(0, quota.remaining);
|
|
615
|
+
const options = { ...config.defaultSettings, ...settings || {} };
|
|
616
|
+
batch = {
|
|
617
|
+
...idleBatch(),
|
|
618
|
+
running: true,
|
|
619
|
+
startedAt: Date.now(),
|
|
620
|
+
folder: `${config.brand}/${folderNameFor(accepted)}`,
|
|
621
|
+
total: accepted.length,
|
|
622
|
+
freeLimited,
|
|
623
|
+
settings: options,
|
|
624
|
+
pro: quota.pro,
|
|
625
|
+
items: accepted.map((url, index) => ({
|
|
626
|
+
url,
|
|
627
|
+
index: index + 1,
|
|
628
|
+
status: "pending",
|
|
629
|
+
fileName: "",
|
|
630
|
+
error: "",
|
|
631
|
+
warning: "",
|
|
632
|
+
usageRecorded: false
|
|
633
|
+
}))
|
|
634
|
+
};
|
|
635
|
+
await persistBatch();
|
|
636
|
+
runBatch(options).catch((error) => {
|
|
637
|
+
batch.running = false;
|
|
638
|
+
batch.current = normalizeError(error);
|
|
639
|
+
persistBatch();
|
|
640
|
+
});
|
|
641
|
+
return { ok: true, batch: publicBatch(), quota: await getQuota() };
|
|
642
|
+
}
|
|
643
|
+
async function quickCapture(tabId, settings) {
|
|
644
|
+
if (batch.running || quickRunning) {
|
|
645
|
+
return { ok: false, error: texts.alreadyRunning };
|
|
646
|
+
}
|
|
647
|
+
if (!Number.isInteger(tabId)) {
|
|
648
|
+
return { ok: false, error: "No active web page" };
|
|
649
|
+
}
|
|
650
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
651
|
+
if (!tab?.url || !/^https?:\/\//i.test(tab.url)) {
|
|
652
|
+
return { ok: false, error: texts.cannotQuick };
|
|
653
|
+
}
|
|
654
|
+
const quota = await getQuota();
|
|
655
|
+
if (!quota.pro && quota.remaining <= 0) {
|
|
656
|
+
return {
|
|
657
|
+
ok: false,
|
|
658
|
+
error: `Daily free limit reached (${quota.limit} pages/day). Go Pro for unlimited pages.`
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
quickRunning = true;
|
|
662
|
+
setBadge("1");
|
|
663
|
+
try {
|
|
664
|
+
const options = { ...config.defaultSettings, ...settings || {} };
|
|
665
|
+
const result = await captureExistingTab(tabId, tab.url, options);
|
|
666
|
+
if (!quota.pro) {
|
|
667
|
+
await recordUsage(1);
|
|
668
|
+
}
|
|
669
|
+
setBadge("OK");
|
|
670
|
+
setTimeout(() => setBadge(""), 4e3);
|
|
671
|
+
return { ok: true, result, quota: await getQuota() };
|
|
672
|
+
} catch (error) {
|
|
673
|
+
setBadge("ERR");
|
|
674
|
+
setTimeout(() => setBadge(""), 4e3);
|
|
675
|
+
return { ok: false, error: normalizeError(error), quota: await getQuota() };
|
|
676
|
+
} finally {
|
|
677
|
+
quickRunning = false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function captureExistingTab(tabId, pageUrl, options) {
|
|
681
|
+
const target = { tabId };
|
|
682
|
+
let attached = false;
|
|
683
|
+
let scrollPosition = { x: 0, y: 0 };
|
|
684
|
+
try {
|
|
685
|
+
await debuggerAttach(target, "1.3");
|
|
686
|
+
attached = true;
|
|
687
|
+
await sendCommand(target, "Page.enable");
|
|
688
|
+
await sendCommand(target, "Runtime.enable");
|
|
689
|
+
const probe = await probePage(target);
|
|
690
|
+
if (probe.error) {
|
|
691
|
+
throw new Error(probe.error);
|
|
692
|
+
}
|
|
693
|
+
if (probe.challenge) {
|
|
694
|
+
throw new Error(texts.challengeBeforeQuick(probe.challenge));
|
|
695
|
+
}
|
|
696
|
+
scrollPosition = await getScrollPosition(target);
|
|
697
|
+
await delay(clamp(Number(options.delayMs) || Number(config.defaultSettings.delayMs) || 1500, 0, 1e4));
|
|
698
|
+
await autoScroll(target);
|
|
699
|
+
await revealAnimations(target, true);
|
|
700
|
+
await delay(400);
|
|
701
|
+
const output = await config.capture(makeContext("quick", tabId, pageUrl, options, target));
|
|
702
|
+
const fileName = buildQuickFileName(config.brand, texts.quickFolder, pageUrl, output.extension);
|
|
703
|
+
await downloader.downloadViaBlob(output.base64, output.mime, fileName);
|
|
704
|
+
return { fileName, warning: output.warning || "" };
|
|
705
|
+
} finally {
|
|
706
|
+
if (attached) {
|
|
707
|
+
await restorePageAfterQuick(target, scrollPosition).catch(() => {
|
|
708
|
+
});
|
|
709
|
+
await debuggerDetach(target).catch(() => {
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async function cancelBatch() {
|
|
715
|
+
if (batch.running) {
|
|
716
|
+
batch.cancelRequested = true;
|
|
717
|
+
batch.current = "Stopping\u2026";
|
|
718
|
+
await persistBatch();
|
|
719
|
+
}
|
|
720
|
+
return { ok: true, batch: publicBatch() };
|
|
721
|
+
}
|
|
722
|
+
async function clearBatch() {
|
|
723
|
+
if (batch.running) {
|
|
724
|
+
return { ok: false, error: "Stop the running batch before clearing it" };
|
|
725
|
+
}
|
|
726
|
+
batch = idleBatch();
|
|
727
|
+
await storageRemove(BATCH_KEY);
|
|
728
|
+
setBadge("");
|
|
729
|
+
return { ok: true, batch: publicBatch() };
|
|
730
|
+
}
|
|
731
|
+
async function runBatch(options) {
|
|
732
|
+
const lastHostAt = {};
|
|
733
|
+
for (const item of batch.items) {
|
|
734
|
+
if (item.status !== "pending") {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (batch.cancelRequested) {
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
const host = hostOf(item.url);
|
|
741
|
+
const since = Date.now() - (lastHostAt[host] || 0);
|
|
742
|
+
if (lastHostAt[host] && since < SAME_HOST_MIN_GAP_MS) {
|
|
743
|
+
batch.current = "Pacing to avoid rate-limits\u2026";
|
|
744
|
+
await delay(SAME_HOST_MIN_GAP_MS - since);
|
|
745
|
+
if (batch.cancelRequested) {
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
lastHostAt[host] = Date.now();
|
|
750
|
+
item.status = "running";
|
|
751
|
+
batch.current = item.url;
|
|
752
|
+
setBadge(`${batch.done + 1}/${batch.total}`);
|
|
753
|
+
await persistBatch();
|
|
754
|
+
let shouldCharge = false;
|
|
755
|
+
try {
|
|
756
|
+
const result = await captureUrl(item, options);
|
|
757
|
+
item.fileName = result.fileName;
|
|
758
|
+
item.warning = result.warning;
|
|
759
|
+
item.status = "success";
|
|
760
|
+
batch.success += 1;
|
|
761
|
+
if (!batch.pro && !item.usageRecorded) {
|
|
762
|
+
item.usageRecorded = true;
|
|
763
|
+
shouldCharge = true;
|
|
764
|
+
}
|
|
765
|
+
} catch (error) {
|
|
766
|
+
item.status = "failed";
|
|
767
|
+
item.error = normalizeError(error);
|
|
768
|
+
batch.failed += 1;
|
|
769
|
+
}
|
|
770
|
+
batch.done = batch.success + batch.failed;
|
|
771
|
+
await persistBatch();
|
|
772
|
+
if (shouldCharge) {
|
|
773
|
+
await recordUsage(1);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
batch.running = false;
|
|
777
|
+
batch.current = batch.cancelRequested ? "Stopped" : "Done";
|
|
778
|
+
batch.needVerify = null;
|
|
779
|
+
await persistBatch();
|
|
780
|
+
setBadge(batch.failed ? "ERR" : "OK");
|
|
781
|
+
setTimeout(() => setBadge(""), 4e3);
|
|
782
|
+
}
|
|
783
|
+
async function captureUrl(item, options) {
|
|
784
|
+
const maxChallengeRetry = 2;
|
|
785
|
+
let lastError;
|
|
786
|
+
for (let attempt = 0; attempt <= maxChallengeRetry; attempt += 1) {
|
|
787
|
+
if (batch.cancelRequested) {
|
|
788
|
+
throw new Error("Cancelled");
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
return await captureOnce(item, options, false);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
lastError = error;
|
|
794
|
+
if (!isChallengeError(error)) {
|
|
795
|
+
throw error;
|
|
796
|
+
}
|
|
797
|
+
if (attempt < maxChallengeRetry) {
|
|
798
|
+
const backoff = 8e3 * (attempt + 1);
|
|
799
|
+
batch.current = `Rate-limited \u2014 cooling down ${backoff / 1e3}s (retry ${attempt + 1}/${maxChallengeRetry})`;
|
|
800
|
+
await delay(backoff);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (isChallengeError(lastError) && !batch.cancelRequested) {
|
|
805
|
+
return await captureOnce(item, options, true);
|
|
806
|
+
}
|
|
807
|
+
throw lastError;
|
|
808
|
+
}
|
|
809
|
+
async function captureOnce(item, options, interactive) {
|
|
810
|
+
const tab = await chrome.tabs.create({ url: item.url, active: Boolean(interactive) });
|
|
811
|
+
const tabId = tab.id;
|
|
812
|
+
if (!tabId) {
|
|
813
|
+
throw new Error("Could not open tab");
|
|
814
|
+
}
|
|
815
|
+
const target = { tabId };
|
|
816
|
+
let attached = false;
|
|
817
|
+
try {
|
|
818
|
+
await waitForLoad(tabId, LOAD_TIMEOUT_MS);
|
|
819
|
+
await delay(Number(options.delayMs) || 0);
|
|
820
|
+
if (batch.cancelRequested) {
|
|
821
|
+
throw new Error("Cancelled");
|
|
822
|
+
}
|
|
823
|
+
await debuggerAttach(target, "1.3");
|
|
824
|
+
attached = true;
|
|
825
|
+
await sendCommand(target, "Page.enable");
|
|
826
|
+
await sendCommand(target, "Runtime.enable");
|
|
827
|
+
const probe = await probePage(target);
|
|
828
|
+
if (probe.error) {
|
|
829
|
+
throw new Error(probe.error);
|
|
830
|
+
}
|
|
831
|
+
if (probe.challenge) {
|
|
832
|
+
if (!interactive) {
|
|
833
|
+
throw challengeError(probe.challenge);
|
|
834
|
+
}
|
|
835
|
+
await debuggerDetach(target).catch(() => {
|
|
836
|
+
});
|
|
837
|
+
attached = false;
|
|
838
|
+
await focusTab(tabId);
|
|
839
|
+
batch.needVerify = { url: item.url, challenge: probe.challenge };
|
|
840
|
+
batch.current = `Complete ${probe.challenge} verification in the opened tab\u2026`;
|
|
841
|
+
await persistBatch();
|
|
842
|
+
batch.skipVerify = false;
|
|
843
|
+
const passed = await waitForVerification(tabId, () => batch.cancelRequested || batch.skipVerify);
|
|
844
|
+
batch.needVerify = null;
|
|
845
|
+
batch.current = item.url;
|
|
846
|
+
await persistBatch();
|
|
847
|
+
if (!passed) {
|
|
848
|
+
throw challengeError(`${probe.challenge} verification not completed`);
|
|
849
|
+
}
|
|
850
|
+
await delay(800);
|
|
851
|
+
await debuggerAttach(target, "1.3");
|
|
852
|
+
attached = true;
|
|
853
|
+
await sendCommand(target, "Page.enable");
|
|
854
|
+
await sendCommand(target, "Runtime.enable");
|
|
855
|
+
}
|
|
856
|
+
const ctx = makeContext("batch", tabId, item.url, options, target);
|
|
857
|
+
if (config.beforeScroll) {
|
|
858
|
+
await config.beforeScroll(ctx);
|
|
859
|
+
}
|
|
860
|
+
await autoScroll(target);
|
|
861
|
+
await revealAnimations(target);
|
|
862
|
+
await delay(400);
|
|
863
|
+
const output = await config.capture(ctx);
|
|
864
|
+
const fileName = buildBatchFileName(batch.folder, item, output.extension);
|
|
865
|
+
await downloader.downloadViaBlob(output.base64, output.mime, fileName);
|
|
866
|
+
return { fileName, warning: output.warning || "" };
|
|
867
|
+
} finally {
|
|
868
|
+
if (attached) {
|
|
869
|
+
await debuggerDetach(target).catch(() => {
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
await chrome.tabs.remove(tabId).catch(() => {
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function makeContext(mode, tabId, url, options, target) {
|
|
877
|
+
return {
|
|
878
|
+
mode,
|
|
879
|
+
tabId,
|
|
880
|
+
url,
|
|
881
|
+
options,
|
|
882
|
+
send: (method, params) => sendCommand(target, method, params)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function publicBatch() {
|
|
886
|
+
return {
|
|
887
|
+
running: batch.running,
|
|
888
|
+
total: batch.total,
|
|
889
|
+
done: batch.done,
|
|
890
|
+
success: batch.success,
|
|
891
|
+
failed: batch.failed,
|
|
892
|
+
current: batch.current,
|
|
893
|
+
folder: batch.folder,
|
|
894
|
+
freeLimited: batch.freeLimited,
|
|
895
|
+
needVerify: batch.needVerify,
|
|
896
|
+
items: batch.items.map((item) => ({
|
|
897
|
+
url: item.url,
|
|
898
|
+
index: item.index,
|
|
899
|
+
status: item.status,
|
|
900
|
+
fileName: item.fileName,
|
|
901
|
+
error: item.error,
|
|
902
|
+
warning: item.warning
|
|
903
|
+
}))
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
async function persistBatch() {
|
|
907
|
+
await storageSet({ [BATCH_KEY]: batch });
|
|
908
|
+
}
|
|
909
|
+
async function restoreBatch() {
|
|
910
|
+
const data = await storageGet(BATCH_KEY);
|
|
911
|
+
const saved = data[BATCH_KEY];
|
|
912
|
+
if (!saved?.items?.length) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
batch = { ...idleBatch(), ...saved };
|
|
916
|
+
batch.settings = { ...config.defaultSettings, ...saved.settings || {} };
|
|
917
|
+
batch.items = saved.items.map((item) => ({
|
|
918
|
+
url: item.url || "",
|
|
919
|
+
index: item.index || 0,
|
|
920
|
+
status: item.status === "running" ? "pending" : item.status || "pending",
|
|
921
|
+
fileName: item.fileName || "",
|
|
922
|
+
error: item.error || "",
|
|
923
|
+
warning: item.warning || "",
|
|
924
|
+
usageRecorded: item.usageRecorded || false
|
|
925
|
+
}));
|
|
926
|
+
batch.done = batch.items.filter((item) => item.status === "success" || item.status === "failed").length;
|
|
927
|
+
batch.success = batch.items.filter((item) => item.status === "success").length;
|
|
928
|
+
batch.failed = batch.items.filter((item) => item.status === "failed").length;
|
|
929
|
+
batch.needVerify = null;
|
|
930
|
+
batch.skipVerify = false;
|
|
931
|
+
if (saved.running && batch.items.some((item) => item.status === "pending")) {
|
|
932
|
+
batch.running = true;
|
|
933
|
+
batch.cancelRequested = false;
|
|
934
|
+
batch.current = "Resuming interrupted batch\u2026";
|
|
935
|
+
await persistBatch();
|
|
936
|
+
runBatch(batch.settings).catch(async (error) => {
|
|
937
|
+
batch.running = false;
|
|
938
|
+
batch.current = normalizeError(error);
|
|
939
|
+
await persistBatch();
|
|
940
|
+
});
|
|
941
|
+
} else if (saved.running) {
|
|
942
|
+
batch.running = false;
|
|
943
|
+
batch.current = "Done";
|
|
944
|
+
await persistBatch();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function setBadge(text) {
|
|
948
|
+
chrome.action.setBadgeText({ text });
|
|
949
|
+
chrome.action.setBadgeBackgroundColor({ color: config.badgeColor });
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/background.entry.ts
|
|
954
|
+
globalThis.ZuckerExt = { init: initExtension };
|
|
955
|
+
})();
|