@nsnanocat/util 2.6.2 → 2.6.4
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/README.md +32 -14
- package/getStorage.mjs +1 -1
- package/index.cjs +9 -0
- package/index.js +2 -2
- package/package.json +23 -1
- package/polyfill/Storage.cjs +220 -0
- package/polyfill/{Storage.js → Storage.mjs} +7 -76
- package/polyfill/fetch.cjs +128 -0
- package/polyfill/{fetch.js → fetch.mjs} +9 -133
- package/polyfill/fetch.mts +1 -1
- package/polyfill/index.d.ts +3 -3
- package/polyfill/index.js +2 -2
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
- `lib/environment.mjs`
|
|
91
91
|
- `lib/runScript.mjs`
|
|
92
92
|
- `getStorage.mjs`(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用;请通过子路径 `@nsnanocat/util/getStorage.mjs` 导入)
|
|
93
|
+
- `polyfill/Storage.cjs`(Node.js / Worker CJS 入口;请通过子路径 `@nsnanocat/util/polyfill/Storage` 导入)
|
|
93
94
|
|
|
94
95
|
## 模块依赖关系
|
|
95
96
|
|
|
@@ -108,7 +109,7 @@ import {
|
|
|
108
109
|
| `getStorage.mjs` | `lib/argument.mjs`, `polyfill/Console.mjs`, `polyfill/Lodash.mjs`, `polyfill/Storage.mjs` | `Console.debug`, `Console.logLevel`, `Lodash.merge`, `Storage.getItem` | 先标准化 `$argument`,再合并默认配置/持久化配置/运行参数 |
|
|
109
110
|
| `polyfill/Console.mjs` | `lib/app.mjs` | `$app` | 日志在 Worker / Node.js 与 iOS 脚本环境使用不同错误输出策略 |
|
|
110
111
|
| `polyfill/fetch.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs`, `polyfill/StatusTexts.mjs`, `polyfill/Console.mjs` | `$app`, `Lodash.set`, `StatusTexts`(`Console` 当前版本未实际调用) | 按平台选请求引擎并做参数映射、响应结构统一 |
|
|
111
|
-
| `polyfill/Storage.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs` | `$app`, `Lodash.get`, `Lodash.set`, `Lodash.unset` |
|
|
112
|
+
| `polyfill/Storage.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs` | `$app`, `Lodash.get`, `Lodash.set`, `Lodash.unset` | ESM 路径下按平台选持久化后端并支持 `@key.path` 读写(Node.js 请走 `Storage.cjs`) |
|
|
112
113
|
| `polyfill/Lodash.mjs` | 无 | 无 | 提供路径/合并等基础能力,被多个模块复用 |
|
|
113
114
|
| `polyfill/qs.mjs` | `polyfill/Lodash.mjs` | `Lodash.get`, `Lodash.set`, `Lodash.toPath` | 提供查询字符串与对象之间的解析/序列化能力 |
|
|
114
115
|
| `polyfill/StatusTexts.mjs` | 无 | 无 | 提供 HTTP 状态文案,供 `fetch/done` 使用 |
|
|
@@ -369,7 +370,11 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
369
370
|
|
|
370
371
|
### `polyfill/fetch.mjs`
|
|
371
372
|
|
|
372
|
-
`fetch`
|
|
373
|
+
`fetch` 现已拆分为 ESM / CJS 两条运行路径:
|
|
374
|
+
- `polyfill/fetch.mjs`:仅用于 iOS 脚本平台(Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket)
|
|
375
|
+
- `polyfill/fetch.cjs`:用于 Worker / Node.js
|
|
376
|
+
|
|
377
|
+
`polyfill/fetch.mjs` 仍仿照 Web API `Window.fetch` 设计:
|
|
373
378
|
- 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
|
374
379
|
- 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
|
|
375
380
|
- 目标:尽量保持 Web `fetch` 调用习惯,同时补齐各平台扩展参数映射
|
|
@@ -394,7 +399,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
394
399
|
- `timeout`
|
|
395
400
|
- `policy`
|
|
396
401
|
- `redirection` / `auto-redirect`
|
|
397
|
-
- `auto-cookie
|
|
402
|
+
- `auto-cookie`(仅 CJS 的 Worker / Node.js 分支识别;默认启用,传入 `false` / `0` / `-1` 可关闭)
|
|
398
403
|
|
|
399
404
|
说明:下表是各 App 原生 HTTP 接口的差异补充,以及本库 `fetch` 的内部映射方式。调用方使用统一入参即可。
|
|
400
405
|
|
|
@@ -408,8 +413,6 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
408
413
|
| Egern | `$httpClient[method]` | 秒 | 无专门映射 | `auto-redirect` | 同上 |
|
|
409
414
|
| Shadowrocket | `$httpClient[method]` | 秒 | `headers.X-Surge-Proxy` | `auto-redirect` | 同上 |
|
|
410
415
|
| Quantumult X | `$task.fetch` | 毫秒(内部乘 1000) | `opts.policy` | `opts.redirection` | `body(ArrayBuffer/TypedArray)` 转 `bodyBytes`;响应按 `Content-Type` 恢复到 `body` |
|
|
411
|
-
| Worker | `globalThis.fetch`(不存在时回退 `node-fetch`);共享 `auto-cookie` 处理 | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
|
|
412
|
-
| Node.js | `globalThis.fetch`(不存在时回退 `node-fetch`);默认按需包裹 `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
|
|
413
416
|
|
|
414
417
|
返回对象(统一后)常见字段:
|
|
415
418
|
- `ok`
|
|
@@ -421,15 +424,22 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
421
424
|
- `bodyBytes`
|
|
422
425
|
|
|
423
426
|
不可用/差异点:
|
|
424
|
-
- `policy` 在 Surge / Egern
|
|
427
|
+
- `policy` 在 Surge / Egern 分支没有额外适配逻辑。
|
|
425
428
|
- `redirection` 在部分平台会映射为 `auto-redirect` 或 `opts.redirection`。
|
|
426
|
-
- Worker / Node.js 共享基于 `fetch` 的请求分支;若 `globalThis.fetch` 不存在则回退到 `node-fetch`,并在 `auto-cookie` 未关闭时按需包裹 `fetch-cookie`。
|
|
427
429
|
- 传入 `timeout` 时,`5` 和 `5000` 都会被接受;库会先将用户输入归一化,再按平台要求转换为秒或毫秒。
|
|
428
430
|
- 返回结构是统一兼容结构,不等同于浏览器 `Response` 对象。
|
|
429
431
|
|
|
432
|
+
Worker / Node.js 使用说明:
|
|
433
|
+
- 请通过 CJS 入口调用:`require("@nsnanocat/util").fetch` 或 `require("@nsnanocat/util/polyfill/fetch").fetch`
|
|
434
|
+
- CJS 分支会处理 `auto-cookie`,并将响应归一化为 `ok/status/statusText/body/bodyBytes`
|
|
435
|
+
|
|
430
436
|
### `polyfill/Storage.mjs`
|
|
431
437
|
|
|
432
|
-
`Storage`
|
|
438
|
+
`Storage` 已拆分为 ESM / CJS 两条运行路径:
|
|
439
|
+
- `polyfill/Storage.mjs`:用于 iOS 脚本平台 + Worker
|
|
440
|
+
- `polyfill/Storage.cjs`:用于 Worker / Node.js(含 `box.dat` 文件读写)
|
|
441
|
+
|
|
442
|
+
`polyfill/Storage.mjs` 仍仿照 Web Storage 接口(`Storage`)设计:
|
|
433
443
|
- 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
|
434
444
|
- 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Storage
|
|
435
445
|
- 目标:统一 VPN App 脚本环境中的持久化读写接口,并尽量贴近 Web Storage 行为
|
|
@@ -447,23 +457,31 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
447
457
|
- Quantumult X:可用(`$prefs.removeValueForKey`)。
|
|
448
458
|
- Surge:通过 `$persistentStore.write(null, keyName)` 删除。
|
|
449
459
|
- Worker:可用(仅删除内存缓存中的对应 key,不持久化)。
|
|
450
|
-
- Node.js
|
|
460
|
+
- Node.js:ESM 路径不支持,会提示改用 CJS 入口。
|
|
451
461
|
- Loon / Stash / Egern / Shadowrocket:返回 `false`。
|
|
452
462
|
|
|
453
463
|
#### `Storage.clear()`
|
|
454
464
|
- Quantumult X:可用(`$prefs.removeAllValues`)。
|
|
455
465
|
- Worker:可用(仅清空内存缓存,不持久化)。
|
|
456
|
-
- Node.js
|
|
466
|
+
- Node.js:ESM 路径不支持,会提示改用 CJS 入口。
|
|
457
467
|
- 其他平台:返回 `false`。
|
|
458
468
|
|
|
459
|
-
#### Worker
|
|
469
|
+
#### Worker 特性(ESM)
|
|
460
470
|
- Worker:使用进程内内存缓存,不写文件。
|
|
471
|
+
|
|
472
|
+
#### Node.js 特性(CJS)
|
|
461
473
|
- 数据文件默认:`box.dat`。
|
|
462
474
|
- 读取路径优先级:当前目录 -> `process.cwd()`。
|
|
463
475
|
|
|
476
|
+
Node.js 使用说明:
|
|
477
|
+
- 请通过 CJS 入口调用:`require("@nsnanocat/util").Storage` 或 `require("@nsnanocat/util/polyfill/Storage").Storage`
|
|
478
|
+
- CJS 的 `Storage` 分支支持 `box.dat` 读写与落盘
|
|
479
|
+
|
|
464
480
|
与 Web Storage 的行为差异:
|
|
465
481
|
- 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。
|
|
466
|
-
- `removeItem/clear`
|
|
482
|
+
- `removeItem/clear` 仅部分平台可用:
|
|
483
|
+
- ESM 路径:Quantumult X、Worker,以及 Surge 的 `removeItem`
|
|
484
|
+
- CJS 路径:Worker、Node.js
|
|
467
485
|
- `getItem` 会尝试 `JSON.parse`,`setItem` 写入对象会 `JSON.stringify`。
|
|
468
486
|
|
|
469
487
|
平台后端映射:
|
|
@@ -472,8 +490,8 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
472
490
|
| --- | --- |
|
|
473
491
|
| Surge / Loon / Stash / Egern / Shadowrocket | `$persistentStore.read/write` |
|
|
474
492
|
| Quantumult X | `$prefs.valueForKey/setValueForKey` |
|
|
475
|
-
| Worker | 进程内内存缓存 |
|
|
476
|
-
| Node.js | 本地 `box.dat` |
|
|
493
|
+
| Worker(ESM/CJS) | 进程内内存缓存 |
|
|
494
|
+
| Node.js(仅 CJS) | 本地 `box.dat` |
|
|
477
495
|
|
|
478
496
|
### `polyfill/Console.mjs`
|
|
479
497
|
|
package/getStorage.mjs
CHANGED
package/index.cjs
ADDED
package/index.js
CHANGED
|
@@ -5,11 +5,11 @@ export * from "./lib/notification.mjs";
|
|
|
5
5
|
export * from "./lib/time.mjs";
|
|
6
6
|
export * from "./lib/wait.mjs";
|
|
7
7
|
export * from "./polyfill/Console.mjs";
|
|
8
|
-
export * from "./polyfill/fetch.
|
|
8
|
+
export * from "./polyfill/fetch.mjs";
|
|
9
9
|
export * from "./polyfill/Lodash.mjs";
|
|
10
10
|
export * from "./polyfill/qs.mjs";
|
|
11
11
|
export * from "./polyfill/StatusTexts.mjs";
|
|
12
|
-
export * from "./polyfill/Storage.
|
|
12
|
+
export * from "./polyfill/Storage.mjs";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 已标准化的 `$argument` 快照。
|
package/package.json
CHANGED
|
@@ -14,10 +14,27 @@
|
|
|
14
14
|
"license": "Apache-2.0",
|
|
15
15
|
"bugs": "https://github.com/NSNanoCat/util/issues",
|
|
16
16
|
"main": "index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": "./index.js",
|
|
20
|
+
"require": "./index.cjs"
|
|
21
|
+
},
|
|
22
|
+
"./polyfill/fetch": {
|
|
23
|
+
"import": "./polyfill/fetch.mjs",
|
|
24
|
+
"require": "./polyfill/fetch.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./polyfill/Storage": {
|
|
27
|
+
"import": "./polyfill/Storage.mjs",
|
|
28
|
+
"require": "./polyfill/Storage.cjs"
|
|
29
|
+
},
|
|
30
|
+
"./*": "./*"
|
|
31
|
+
},
|
|
17
32
|
"types": "types/nsnanocat-util.d.ts",
|
|
18
33
|
"scripts": {
|
|
19
34
|
"tsc:build": "npx tsc",
|
|
20
35
|
"test": "node --test test/*.test.js",
|
|
36
|
+
"test:fetch": "node --test test/fetch.test.js",
|
|
37
|
+
"test:fetch:cjs": "node --test test/fetch.test.cjs",
|
|
21
38
|
"test:merge": "node --test test/Lodash.merge.test.js",
|
|
22
39
|
"test:argument": "node --test test/argument.test.js",
|
|
23
40
|
"deprecate": "npm deprecate -f '@nsnanocat/util@0.0.0-preview' \"this package has been deprecated\""
|
|
@@ -27,12 +44,17 @@
|
|
|
27
44
|
"url": "git+https://github.com/NSNanoCat/util.git"
|
|
28
45
|
},
|
|
29
46
|
"files": [
|
|
47
|
+
"index.cjs",
|
|
30
48
|
"index.js",
|
|
31
49
|
"lib",
|
|
32
50
|
"polyfill",
|
|
33
51
|
"getStorage.mjs",
|
|
34
52
|
"types"
|
|
35
53
|
],
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"fetch-cookie": "^3.2.0",
|
|
56
|
+
"node-fetch": "^3.3.2"
|
|
57
|
+
},
|
|
36
58
|
"devDependencies": {
|
|
37
59
|
"@biomejs/biome": "2.4.6",
|
|
38
60
|
"typescript": "^5.9.3"
|
|
@@ -41,5 +63,5 @@
|
|
|
41
63
|
"registry": "https://registry.npmjs.org/",
|
|
42
64
|
"access": "public"
|
|
43
65
|
},
|
|
44
|
-
"version": "2.6.
|
|
66
|
+
"version": "2.6.4"
|
|
45
67
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { Lodash: _ } = require("./Lodash.mjs");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 仅面向 Worker / Node.js 的持久化存储适配器(CJS 版本)。
|
|
7
|
+
* Persistent storage adapter for Worker / Node.js only (CJS version).
|
|
8
|
+
*/
|
|
9
|
+
class Storage {
|
|
10
|
+
/**
|
|
11
|
+
* Worker / Node.js 环境下的内存数据缓存。
|
|
12
|
+
* In-memory data cache for Worker / Node.js runtime.
|
|
13
|
+
*
|
|
14
|
+
* @type {Record<string, any>|null}
|
|
15
|
+
*/
|
|
16
|
+
static data = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Node.js 持久化文件名。
|
|
20
|
+
* Data file name used in Node.js.
|
|
21
|
+
*
|
|
22
|
+
* @type {string}
|
|
23
|
+
*/
|
|
24
|
+
static dataFile = "box.dat";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `@key.path` 解析正则。
|
|
28
|
+
* Regex for `@key.path` parsing.
|
|
29
|
+
*
|
|
30
|
+
* @type {RegExp}
|
|
31
|
+
*/
|
|
32
|
+
static #nameRegex = /^@(?<key>[^.]+)(?:\.(?<path>.*))?$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 读取存储值。
|
|
36
|
+
* Read value from persistent storage.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} keyName 键名或路径键 / Key or path key.
|
|
39
|
+
* @param {*} [defaultValue=null] 默认值 / Default value when key is missing.
|
|
40
|
+
* @returns {*}
|
|
41
|
+
*/
|
|
42
|
+
static getItem(keyName, defaultValue = null) {
|
|
43
|
+
let keyValue = defaultValue;
|
|
44
|
+
switch (keyName.startsWith("@")) {
|
|
45
|
+
case true: {
|
|
46
|
+
const { key, path } = keyName.match(Storage.#nameRegex)?.groups;
|
|
47
|
+
keyName = key;
|
|
48
|
+
let value = Storage.getItem(keyName, {});
|
|
49
|
+
if (typeof value !== "object") value = {};
|
|
50
|
+
keyValue = _.get(value, path);
|
|
51
|
+
try {
|
|
52
|
+
keyValue = JSON.parse(keyValue);
|
|
53
|
+
} catch {}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
58
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
59
|
+
keyValue = Storage.data?.[keyName];
|
|
60
|
+
} else {
|
|
61
|
+
Storage.data = Storage.data ?? {};
|
|
62
|
+
keyValue = Storage.data[keyName];
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
keyValue = JSON.parse(keyValue);
|
|
66
|
+
} catch {}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
return keyValue ?? defaultValue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 写入存储值。
|
|
74
|
+
* Write value into persistent storage.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} keyName 键名或路径键 / Key or path key.
|
|
77
|
+
* @param {*} keyValue 写入值 / Value to store.
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
static setItem(keyName = new String(), keyValue = new String()) {
|
|
81
|
+
let result = false;
|
|
82
|
+
switch (typeof keyValue) {
|
|
83
|
+
case "object":
|
|
84
|
+
keyValue = JSON.stringify(keyValue);
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
keyValue = String(keyValue);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
switch (keyName.startsWith("@")) {
|
|
91
|
+
case true: {
|
|
92
|
+
const { key, path } = keyName.match(Storage.#nameRegex)?.groups;
|
|
93
|
+
keyName = key;
|
|
94
|
+
let value = Storage.getItem(keyName, {});
|
|
95
|
+
if (typeof value !== "object") value = {};
|
|
96
|
+
_.set(value, path, keyValue);
|
|
97
|
+
result = Storage.setItem(keyName, value);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
102
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
103
|
+
Storage.data[keyName] = keyValue;
|
|
104
|
+
Storage.#writedata(Storage.dataFile);
|
|
105
|
+
result = true;
|
|
106
|
+
} else {
|
|
107
|
+
Storage.data = Storage.data ?? {};
|
|
108
|
+
Storage.data[keyName] = keyValue;
|
|
109
|
+
result = true;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 删除存储值。
|
|
118
|
+
* Remove value from persistent storage.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} keyName 键名或路径键 / Key or path key.
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
123
|
+
static removeItem(keyName) {
|
|
124
|
+
let result = false;
|
|
125
|
+
switch (keyName.startsWith("@")) {
|
|
126
|
+
case true: {
|
|
127
|
+
const { key, path } = keyName.match(Storage.#nameRegex)?.groups;
|
|
128
|
+
keyName = key;
|
|
129
|
+
let value = Storage.getItem(keyName);
|
|
130
|
+
if (typeof value !== "object") value = {};
|
|
131
|
+
_.unset(value, path);
|
|
132
|
+
result = Storage.setItem(keyName, value);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
137
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
138
|
+
delete Storage.data[keyName];
|
|
139
|
+
Storage.#writedata(Storage.dataFile);
|
|
140
|
+
result = true;
|
|
141
|
+
} else {
|
|
142
|
+
Storage.data = Storage.data ?? {};
|
|
143
|
+
delete Storage.data[keyName];
|
|
144
|
+
result = true;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 清空存储。
|
|
153
|
+
* Clear storage.
|
|
154
|
+
*
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
static clear() {
|
|
158
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
159
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
160
|
+
Storage.data = {};
|
|
161
|
+
Storage.#writedata(Storage.dataFile);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
Storage.data = {};
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 从 Node.js 数据文件加载 JSON。
|
|
170
|
+
* Load JSON data from Node.js data file.
|
|
171
|
+
*
|
|
172
|
+
* @private
|
|
173
|
+
* @param {string} dataFile 数据文件名 / Data file name.
|
|
174
|
+
* @returns {Record<string, any>}
|
|
175
|
+
*/
|
|
176
|
+
static #loaddata = dataFile => {
|
|
177
|
+
const fs = require("node:fs");
|
|
178
|
+
const path = require("node:path");
|
|
179
|
+
const curDirDataFilePath = path.resolve(dataFile);
|
|
180
|
+
const rootDirDataFilePath = path.resolve(process.cwd(), dataFile);
|
|
181
|
+
const isCurDirDataFile = fs.existsSync(curDirDataFilePath);
|
|
182
|
+
const isRootDirDataFile = !isCurDirDataFile && fs.existsSync(rootDirDataFilePath);
|
|
183
|
+
if (isCurDirDataFile || isRootDirDataFile) {
|
|
184
|
+
const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath;
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(fs.readFileSync(datPath));
|
|
187
|
+
} catch {
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {};
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 将内存数据写入 Node.js 数据文件。
|
|
196
|
+
* Persist in-memory data to Node.js data file.
|
|
197
|
+
*
|
|
198
|
+
* @private
|
|
199
|
+
* @param {string} [dataFile=this.dataFile] 数据文件名 / Data file name.
|
|
200
|
+
* @returns {void}
|
|
201
|
+
*/
|
|
202
|
+
static #writedata = (dataFile = this.dataFile) => {
|
|
203
|
+
const fs = require("node:fs");
|
|
204
|
+
const path = require("node:path");
|
|
205
|
+
const curDirDataFilePath = path.resolve(dataFile);
|
|
206
|
+
const rootDirDataFilePath = path.resolve(process.cwd(), dataFile);
|
|
207
|
+
const isCurDirDataFile = fs.existsSync(curDirDataFilePath);
|
|
208
|
+
const isRootDirDataFile = !isCurDirDataFile && fs.existsSync(rootDirDataFilePath);
|
|
209
|
+
const jsondata = JSON.stringify(this.data);
|
|
210
|
+
if (isCurDirDataFile) {
|
|
211
|
+
fs.writeFileSync(curDirDataFilePath, jsondata);
|
|
212
|
+
} else if (isRootDirDataFile) {
|
|
213
|
+
fs.writeFileSync(rootDirDataFilePath, jsondata);
|
|
214
|
+
} else {
|
|
215
|
+
fs.writeFileSync(curDirDataFilePath, jsondata);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { Storage };
|
|
@@ -82,7 +82,7 @@ export class Storage {
|
|
|
82
82
|
keyValue = _.get(value, path);
|
|
83
83
|
try {
|
|
84
84
|
keyValue = JSON.parse(keyValue);
|
|
85
|
-
} catch
|
|
85
|
+
} catch {}
|
|
86
86
|
break;
|
|
87
87
|
}
|
|
88
88
|
default:
|
|
@@ -102,16 +102,14 @@ export class Storage {
|
|
|
102
102
|
keyValue = Storage.data[keyName];
|
|
103
103
|
break;
|
|
104
104
|
case "Node.js":
|
|
105
|
-
|
|
106
|
-
keyValue = Storage.data?.[keyName];
|
|
107
|
-
break;
|
|
105
|
+
throw new Error(`${Storage.name}.getItem: ESM 版本不支持 Node.js,请改用 CJS 入口`);
|
|
108
106
|
default:
|
|
109
107
|
keyValue = Storage.data?.[keyName] || null;
|
|
110
108
|
break;
|
|
111
109
|
}
|
|
112
110
|
try {
|
|
113
111
|
keyValue = JSON.parse(keyValue);
|
|
114
|
-
} catch
|
|
112
|
+
} catch {
|
|
115
113
|
// do nothing
|
|
116
114
|
}
|
|
117
115
|
break;
|
|
@@ -165,11 +163,7 @@ export class Storage {
|
|
|
165
163
|
result = true;
|
|
166
164
|
break;
|
|
167
165
|
case "Node.js":
|
|
168
|
-
|
|
169
|
-
Storage.data[keyName] = keyValue;
|
|
170
|
-
Storage.#writedata(Storage.dataFile);
|
|
171
|
-
result = true;
|
|
172
|
-
break;
|
|
166
|
+
throw new Error(`${Storage.name}.setItem: ESM 版本不支持 Node.js,请改用 CJS 入口`);
|
|
173
167
|
default:
|
|
174
168
|
result = Storage.data?.[keyName] || null;
|
|
175
169
|
break;
|
|
@@ -200,7 +194,7 @@ export class Storage {
|
|
|
200
194
|
keyName = key;
|
|
201
195
|
let value = Storage.getItem(keyName);
|
|
202
196
|
if (typeof value !== "object") value = {};
|
|
203
|
-
|
|
197
|
+
_.unset(value, path);
|
|
204
198
|
result = Storage.setItem(keyName, value);
|
|
205
199
|
break;
|
|
206
200
|
}
|
|
@@ -224,12 +218,7 @@ export class Storage {
|
|
|
224
218
|
result = true;
|
|
225
219
|
break;
|
|
226
220
|
case "Node.js":
|
|
227
|
-
|
|
228
|
-
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
229
|
-
delete Storage.data[keyName];
|
|
230
|
-
Storage.#writedata(Storage.dataFile);
|
|
231
|
-
result = true;
|
|
232
|
-
break;
|
|
221
|
+
throw new Error(`${Storage.name}.removeItem: ESM 版本不支持 Node.js,请改用 CJS 入口`);
|
|
233
222
|
default:
|
|
234
223
|
result = false;
|
|
235
224
|
break;
|
|
@@ -263,12 +252,7 @@ export class Storage {
|
|
|
263
252
|
result = true;
|
|
264
253
|
break;
|
|
265
254
|
case "Node.js":
|
|
266
|
-
|
|
267
|
-
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
268
|
-
Storage.data = {};
|
|
269
|
-
Storage.#writedata(Storage.dataFile);
|
|
270
|
-
result = true;
|
|
271
|
-
break;
|
|
255
|
+
throw new Error(`${Storage.name}.clear: ESM 版本不支持 Node.js,请改用 CJS 入口`);
|
|
272
256
|
default:
|
|
273
257
|
result = false;
|
|
274
258
|
break;
|
|
@@ -276,57 +260,4 @@ export class Storage {
|
|
|
276
260
|
return result;
|
|
277
261
|
}
|
|
278
262
|
|
|
279
|
-
/**
|
|
280
|
-
* 从 Node.js 数据文件加载 JSON。
|
|
281
|
-
* Load JSON data from Node.js data file.
|
|
282
|
-
*
|
|
283
|
-
* @private
|
|
284
|
-
* @param {string} dataFile 数据文件名 / Data file name.
|
|
285
|
-
* @returns {Record<string, any>}
|
|
286
|
-
*/
|
|
287
|
-
static #loaddata = dataFile => {
|
|
288
|
-
if ($app === "Node.js") {
|
|
289
|
-
this.fs = this.fs ? this.fs : require("fs");
|
|
290
|
-
this.path = this.path ? this.path : require("path");
|
|
291
|
-
const curDirDataFilePath = this.path.resolve(dataFile);
|
|
292
|
-
const rootDirDataFilePath = this.path.resolve(process.cwd(), dataFile);
|
|
293
|
-
const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath);
|
|
294
|
-
const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath);
|
|
295
|
-
if (isCurDirDataFile || isRootDirDataFile) {
|
|
296
|
-
const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath;
|
|
297
|
-
try {
|
|
298
|
-
return JSON.parse(this.fs.readFileSync(datPath));
|
|
299
|
-
} catch (e) {
|
|
300
|
-
return {};
|
|
301
|
-
}
|
|
302
|
-
} else return {};
|
|
303
|
-
} else return {};
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* 将内存数据写入 Node.js 数据文件。
|
|
308
|
-
* Persist in-memory data to Node.js data file.
|
|
309
|
-
*
|
|
310
|
-
* @private
|
|
311
|
-
* @param {string} [dataFile=this.dataFile] 数据文件名 / Data file name.
|
|
312
|
-
* @returns {void}
|
|
313
|
-
*/
|
|
314
|
-
static #writedata = (dataFile = this.dataFile) => {
|
|
315
|
-
if ($app === "Node.js") {
|
|
316
|
-
this.fs = this.fs ? this.fs : require("fs");
|
|
317
|
-
this.path = this.path ? this.path : require("path");
|
|
318
|
-
const curDirDataFilePath = this.path.resolve(dataFile);
|
|
319
|
-
const rootDirDataFilePath = this.path.resolve(process.cwd(), dataFile);
|
|
320
|
-
const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath);
|
|
321
|
-
const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath);
|
|
322
|
-
const jsondata = JSON.stringify(this.data);
|
|
323
|
-
if (isCurDirDataFile) {
|
|
324
|
-
this.fs.writeFileSync(curDirDataFilePath, jsondata);
|
|
325
|
-
} else if (isRootDirDataFile) {
|
|
326
|
-
this.fs.writeFileSync(rootDirDataFilePath, jsondata);
|
|
327
|
-
} else {
|
|
328
|
-
this.fs.writeFileSync(curDirDataFilePath, jsondata);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
263
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 统一请求参数。
|
|
5
|
+
* Unified request payload.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {object} FetchRequest
|
|
8
|
+
* @property {string} url 请求地址 / Request URL.
|
|
9
|
+
* @property {string} [method] 请求方法 / HTTP method.
|
|
10
|
+
* @property {Record<string, any>} [headers] 请求头 / Request headers.
|
|
11
|
+
* @property {string|ArrayBuffer|ArrayBufferView|object} [body] 请求体 / Request body.
|
|
12
|
+
* @property {ArrayBuffer} [bodyBytes] 二进制请求体 / Binary request body.
|
|
13
|
+
* @property {number|string} [timeout] 超时(秒或毫秒)/ Timeout (seconds or milliseconds).
|
|
14
|
+
* @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
|
|
15
|
+
* @property {boolean|number|string} ["auto-cookie"] Worker / Node.js Cookie 开关 / Worker / Node.js Cookie toggle.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 统一响应结构。
|
|
20
|
+
* Unified response payload.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {object} FetchResponse
|
|
23
|
+
* @property {boolean} ok 请求是否成功 / Whether request is successful.
|
|
24
|
+
* @property {number} status 状态码 / HTTP status code.
|
|
25
|
+
* @property {number} [statusCode] 状态码别名 / Status code alias.
|
|
26
|
+
* @property {string} [statusText] 状态文本 / HTTP status text.
|
|
27
|
+
* @property {Record<string, any>} [headers] 响应头 / Response headers.
|
|
28
|
+
* @property {string|ArrayBuffer} [body] 响应体 / Response body.
|
|
29
|
+
* @property {ArrayBuffer} [bodyBytes] 二进制响应体 / Binary response body.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 仅面向 Worker / Node.js 的 `fetch` 适配层(CJS 版本)。
|
|
34
|
+
* `fetch` adapter for Worker / Node.js only (CJS version).
|
|
35
|
+
*
|
|
36
|
+
* @async
|
|
37
|
+
* @param {FetchRequest|string} resource 请求对象或 URL / Request object or URL string.
|
|
38
|
+
* @param {Partial<FetchRequest>} [options={}] 追加参数 / Extra options.
|
|
39
|
+
* @returns {Promise<FetchResponse>}
|
|
40
|
+
*/
|
|
41
|
+
async function fetch(resource, options = {}) {
|
|
42
|
+
switch (typeof resource) {
|
|
43
|
+
case "object":
|
|
44
|
+
resource = { ...options, ...resource };
|
|
45
|
+
break;
|
|
46
|
+
case "string":
|
|
47
|
+
resource = { ...options, url: resource };
|
|
48
|
+
break;
|
|
49
|
+
case "undefined":
|
|
50
|
+
default:
|
|
51
|
+
throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!resource.method) {
|
|
55
|
+
resource.method = "GET";
|
|
56
|
+
if (resource.body ?? resource.bodyBytes) resource.method = "POST";
|
|
57
|
+
}
|
|
58
|
+
delete resource.headers?.Host;
|
|
59
|
+
delete resource.headers?.[":authority"];
|
|
60
|
+
delete resource.headers?.["Content-Length"];
|
|
61
|
+
delete resource.headers?.["content-length"];
|
|
62
|
+
|
|
63
|
+
if (!resource.timeout) resource.timeout = 5;
|
|
64
|
+
if (resource.timeout) {
|
|
65
|
+
resource.timeout = Number.parseInt(resource.timeout, 10);
|
|
66
|
+
if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
|
|
67
|
+
resource.timeout = resource.timeout * 1000;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!globalThis.fetch) {
|
|
71
|
+
throw new Error(`${Function.name}: 当前 Node.js 运行时缺少全局 fetch,请升级 Node.js 版本`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
switch (resource["auto-cookie"]) {
|
|
75
|
+
case undefined:
|
|
76
|
+
case "true":
|
|
77
|
+
case true:
|
|
78
|
+
case "1":
|
|
79
|
+
case 1:
|
|
80
|
+
default:
|
|
81
|
+
if (!globalThis.fetch?.cookieJar) globalThis.fetch = require("fetch-cookie").default(globalThis.fetch);
|
|
82
|
+
break;
|
|
83
|
+
case "false":
|
|
84
|
+
case false:
|
|
85
|
+
case "0":
|
|
86
|
+
case 0:
|
|
87
|
+
case "-1":
|
|
88
|
+
case -1:
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
resource.redirect = resource.redirection ? "follow" : "manual";
|
|
93
|
+
const { url, ...fetchOptions } = resource;
|
|
94
|
+
|
|
95
|
+
return Promise.race([
|
|
96
|
+
globalThis
|
|
97
|
+
.fetch(url, fetchOptions)
|
|
98
|
+
.then(async response => {
|
|
99
|
+
const bodyBytes = await response.arrayBuffer();
|
|
100
|
+
let headers;
|
|
101
|
+
try {
|
|
102
|
+
headers = response.headers.raw();
|
|
103
|
+
} catch {
|
|
104
|
+
headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => {
|
|
105
|
+
acc[key] = acc[key] ? [...acc[key], value] : [value];
|
|
106
|
+
return acc;
|
|
107
|
+
}, {});
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
ok: response.ok ?? /^2\d\d$/.test(response.status),
|
|
111
|
+
status: response.status,
|
|
112
|
+
statusCode: response.status,
|
|
113
|
+
statusText: response.statusText,
|
|
114
|
+
body: new TextDecoder("utf-8").decode(bodyBytes),
|
|
115
|
+
bodyBytes,
|
|
116
|
+
headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])),
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
.catch(error => Promise.reject(error.message)),
|
|
120
|
+
new Promise((resolve, reject) => {
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
|
|
123
|
+
}, resource.timeout);
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { fetch };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { $app } from "../lib/app.mjs";
|
|
2
|
-
import { Console } from "./Console.mjs";
|
|
3
2
|
import { Lodash as _ } from "./Lodash.mjs";
|
|
4
3
|
import { StatusTexts } from "./StatusTexts.mjs";
|
|
5
4
|
|
|
@@ -17,7 +16,6 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
17
16
|
* @property {string} [policy] 指定策略 / Preferred policy.
|
|
18
17
|
* @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
|
|
19
18
|
* @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag.
|
|
20
|
-
* @property {boolean|number|string} ["auto-cookie"] Worker / Node.js Cookie 开关 / Worker / Node.js Cookie toggle.
|
|
21
19
|
* @property {Record<string, any>} [opts] 平台扩展字段 / Platform extension fields.
|
|
22
20
|
*/
|
|
23
21
|
|
|
@@ -36,35 +34,8 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
36
34
|
*/
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* 设计目标:
|
|
43
|
-
* Design goal:
|
|
44
|
-
* - 仿照 Web API `fetch`(`Window.fetch`)接口设计
|
|
45
|
-
* - Modeled after Web API `fetch` (`Window.fetch`)
|
|
46
|
-
* - 统一 VPN App、Worker 与 Node.js 环境中的请求调用
|
|
47
|
-
* - Unify request calls across VPN apps, Worker, and Node.js
|
|
48
|
-
*
|
|
49
|
-
* 功能:
|
|
50
|
-
* Features:
|
|
51
|
-
* - 统一 Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js 请求接口
|
|
52
|
-
* - Normalize request APIs across Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js
|
|
53
|
-
* - 统一返回体字段(`ok/status/statusText/body/bodyBytes`)
|
|
54
|
-
* - Normalize response fields (`ok/status/statusText/body/bodyBytes`)
|
|
55
|
-
*
|
|
56
|
-
* 与 Web `fetch` 的已知差异:
|
|
57
|
-
* Known differences from Web `fetch`:
|
|
58
|
-
* - 支持 `policy`、`auto-redirect` 等平台扩展字段
|
|
59
|
-
* - Supports platform extension fields like `policy` and `auto-redirect`
|
|
60
|
-
* - Worker / Node.js 共享基于 `fetch` 的请求分支
|
|
61
|
-
* - Worker / Node.js share the `fetch`-based request branch
|
|
62
|
-
* - `auto-cookie` 在 Worker / Node.js 共享分支中识别
|
|
63
|
-
* - `auto-cookie` is recognized by the shared Worker / Node.js branch
|
|
64
|
-
* - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现
|
|
65
|
-
* - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine
|
|
66
|
-
* - 返回结构包含 `statusCode/bodyBytes` 等兼容字段
|
|
67
|
-
* - Response includes compatibility fields like `statusCode/bodyBytes`
|
|
37
|
+
* 仅面向 iOS 脚本平台的 `fetch` 适配层(ESM 版本)。
|
|
38
|
+
* `fetch` adapter for iOS script platforms only (ESM version).
|
|
68
39
|
*
|
|
69
40
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
|
70
41
|
* @link https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
|
|
@@ -74,8 +45,6 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
74
45
|
* @returns {Promise<FetchResponse>}
|
|
75
46
|
*/
|
|
76
47
|
export async function fetch(resource, options = {}) {
|
|
77
|
-
// 初始化参数。
|
|
78
|
-
// Initialize request input.
|
|
79
48
|
switch (typeof resource) {
|
|
80
49
|
case "object":
|
|
81
50
|
resource = { ...options, ...resource };
|
|
@@ -87,63 +56,39 @@ export async function fetch(resource, options = {}) {
|
|
|
87
56
|
default:
|
|
88
57
|
throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
|
|
89
58
|
}
|
|
90
|
-
|
|
91
|
-
// Infer the HTTP method automatically.
|
|
59
|
+
|
|
92
60
|
if (!resource.method) {
|
|
93
61
|
resource.method = "GET";
|
|
94
62
|
if (resource.body ?? resource.bodyBytes) resource.method = "POST";
|
|
95
63
|
}
|
|
96
|
-
// 移除需要由底层实现自动生成的请求头。
|
|
97
|
-
// Remove headers that should be generated by the underlying runtime.
|
|
98
64
|
delete resource.headers?.Host;
|
|
99
65
|
delete resource.headers?.[":authority"];
|
|
100
66
|
delete resource.headers?.["Content-Length"];
|
|
101
67
|
delete resource.headers?.["content-length"];
|
|
102
|
-
// 统一请求方法为小写,方便后续索引平台 API。
|
|
103
|
-
// Normalize the method to lowercase for platform API lookups.
|
|
104
68
|
const method = resource.method.toLocaleLowerCase();
|
|
105
|
-
|
|
106
|
-
// Default request timeout to 5 seconds.
|
|
69
|
+
|
|
107
70
|
if (!resource.timeout) resource.timeout = 5;
|
|
108
|
-
// 智能矫正请求超时时间,兼容用户输入的秒或毫秒。
|
|
109
|
-
// Normalize timeout input so both seconds and milliseconds are accepted.
|
|
110
71
|
if (resource.timeout) {
|
|
111
72
|
resource.timeout = Number.parseInt(resource.timeout, 10);
|
|
112
|
-
// 统一先转换为秒,大于 500 视为毫秒输入。
|
|
113
|
-
// Convert to seconds first and treat values above 500 as milliseconds.
|
|
114
73
|
if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
|
|
115
74
|
}
|
|
116
|
-
// 某些平台要求毫秒级超时,进行二次换算。
|
|
117
|
-
// Some platforms expect timeout in milliseconds, so convert again.
|
|
118
75
|
if (resource.timeout) {
|
|
119
76
|
switch ($app) {
|
|
120
77
|
case "Loon":
|
|
121
78
|
case "Quantumult X":
|
|
122
|
-
case "Worker":
|
|
123
|
-
case "Node.js":
|
|
124
|
-
// 这些平台要求毫秒,因此把秒重新换算为毫秒。
|
|
125
|
-
// These platforms expect milliseconds, so convert seconds back to milliseconds.
|
|
126
79
|
resource.timeout = resource.timeout * 1000;
|
|
127
80
|
break;
|
|
128
|
-
case "Shadowrocket":
|
|
129
|
-
case "Stash":
|
|
130
|
-
case "Egern":
|
|
131
|
-
case "Surge":
|
|
132
81
|
default:
|
|
133
82
|
break;
|
|
134
83
|
}
|
|
135
84
|
}
|
|
136
|
-
|
|
137
|
-
// Select the request engine for the current platform.
|
|
85
|
+
|
|
138
86
|
switch ($app) {
|
|
139
87
|
case "Loon":
|
|
140
88
|
case "Surge":
|
|
141
89
|
case "Stash":
|
|
142
90
|
case "Egern":
|
|
143
91
|
case "Shadowrocket":
|
|
144
|
-
default:
|
|
145
|
-
// 转换通用请求参数到 `$httpClient` 语义。
|
|
146
|
-
// Map shared request fields to `$httpClient` semantics.
|
|
147
92
|
if (resource.policy) {
|
|
148
93
|
switch ($app) {
|
|
149
94
|
case "Loon":
|
|
@@ -158,14 +103,10 @@ export async function fetch(resource, options = {}) {
|
|
|
158
103
|
}
|
|
159
104
|
}
|
|
160
105
|
if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection;
|
|
161
|
-
// 优先把 `bodyBytes` 映射回 `$httpClient` 能接受的 `body`。
|
|
162
|
-
// Prefer mapping `bodyBytes` back to the `body` field expected by `$httpClient`.
|
|
163
106
|
if (resource.bodyBytes && !resource.body) {
|
|
164
107
|
resource.body = resource.bodyBytes;
|
|
165
108
|
resource.bodyBytes = undefined;
|
|
166
109
|
}
|
|
167
|
-
// 根据 `Accept` 推断是否需要二进制响应体。
|
|
168
|
-
// Infer whether the response should be treated as binary from `Accept`.
|
|
169
110
|
switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) {
|
|
170
111
|
case "application/protobuf":
|
|
171
112
|
case "application/x-protobuf":
|
|
@@ -177,8 +118,6 @@ export async function fetch(resource, options = {}) {
|
|
|
177
118
|
resource["binary-mode"] = true;
|
|
178
119
|
break;
|
|
179
120
|
}
|
|
180
|
-
// 发送 `$httpClient` 请求并归一化返回结构。
|
|
181
|
-
// Send the `$httpClient` request and normalize the response payload.
|
|
182
121
|
return new Promise((resolve, reject) => {
|
|
183
122
|
globalThis.$httpClient[method](resource, (error, response, body) => {
|
|
184
123
|
if (error) reject(error);
|
|
@@ -195,12 +134,8 @@ export async function fetch(resource, options = {}) {
|
|
|
195
134
|
});
|
|
196
135
|
});
|
|
197
136
|
case "Quantumult X":
|
|
198
|
-
// 转换 Quantumult X 专有请求参数。
|
|
199
|
-
// Map request fields to Quantumult X specific options.
|
|
200
137
|
if (resource.policy) _.set(resource, "opts.policy", resource.policy);
|
|
201
138
|
if (typeof resource["auto-redirect"] === "boolean") _.set(resource, "opts.redirection", resource["auto-redirect"]);
|
|
202
|
-
// Quantumult X 使用 `bodyBytes` 传输二进制请求体。
|
|
203
|
-
// Quantumult X uses `bodyBytes` for binary request payloads.
|
|
204
139
|
if (resource.body instanceof ArrayBuffer) {
|
|
205
140
|
resource.bodyBytes = resource.body;
|
|
206
141
|
resource.body = undefined;
|
|
@@ -208,8 +143,6 @@ export async function fetch(resource, options = {}) {
|
|
|
208
143
|
resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset);
|
|
209
144
|
resource.body = undefined;
|
|
210
145
|
} else if (resource.body) resource.bodyBytes = undefined;
|
|
211
|
-
// 发送请求,并用 `Promise.race` 提供统一超时保护。
|
|
212
|
-
// Send the request and enforce timeout with `Promise.race`.
|
|
213
146
|
return Promise.race([
|
|
214
147
|
globalThis.$task.fetch(resource).then(
|
|
215
148
|
response => {
|
|
@@ -242,66 +175,9 @@ export async function fetch(resource, options = {}) {
|
|
|
242
175
|
}),
|
|
243
176
|
]);
|
|
244
177
|
case "Worker":
|
|
245
|
-
case "Node.js":
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
switch (resource["auto-cookie"]) {
|
|
250
|
-
case undefined:
|
|
251
|
-
case "true":
|
|
252
|
-
case true:
|
|
253
|
-
case "1":
|
|
254
|
-
case 1:
|
|
255
|
-
default:
|
|
256
|
-
// 仅在尚未包裹 CookieJar 时注入 `fetch-cookie`,避免重复包装。
|
|
257
|
-
// Inject `fetch-cookie` only once when a cookie jar is not already attached.
|
|
258
|
-
if (!globalThis.fetch?.cookieJar) globalThis.fetch = require("fetch-cookie").default(globalThis.fetch);
|
|
259
|
-
break;
|
|
260
|
-
case "false":
|
|
261
|
-
case false:
|
|
262
|
-
case "0":
|
|
263
|
-
case 0:
|
|
264
|
-
case "-1":
|
|
265
|
-
case -1:
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
// 将通用字段映射到 Worker / Node.js Fetch 语义。
|
|
269
|
-
// Map shared fields to Worker / Node.js Fetch semantics.
|
|
270
|
-
resource.redirect = resource.redirection ? "follow" : "manual";
|
|
271
|
-
const { url, ...options } = resource;
|
|
272
|
-
// 发起请求并归一化响应头、文本与二进制响应体。
|
|
273
|
-
// Send the request and normalize headers, text, and binary response data.
|
|
274
|
-
return Promise.race([
|
|
275
|
-
globalThis
|
|
276
|
-
.fetch(url, options)
|
|
277
|
-
.then(async response => {
|
|
278
|
-
const bodyBytes = await response.arrayBuffer();
|
|
279
|
-
let headers;
|
|
280
|
-
try {
|
|
281
|
-
headers = response.headers.raw();
|
|
282
|
-
} catch {
|
|
283
|
-
headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => {
|
|
284
|
-
acc[key] = acc[key] ? [...acc[key], value] : [value];
|
|
285
|
-
return acc;
|
|
286
|
-
}, {});
|
|
287
|
-
}
|
|
288
|
-
return {
|
|
289
|
-
ok: response.ok ?? /^2\d\d$/.test(response.status),
|
|
290
|
-
status: response.status,
|
|
291
|
-
statusCode: response.status,
|
|
292
|
-
statusText: response.statusText,
|
|
293
|
-
body: new TextDecoder("utf-8").decode(bodyBytes),
|
|
294
|
-
bodyBytes: bodyBytes,
|
|
295
|
-
headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])),
|
|
296
|
-
};
|
|
297
|
-
})
|
|
298
|
-
.catch(error => Promise.reject(error.message)),
|
|
299
|
-
new Promise((resolve, reject) => {
|
|
300
|
-
setTimeout(() => {
|
|
301
|
-
reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
|
|
302
|
-
}, resource.timeout);
|
|
303
|
-
}),
|
|
304
|
-
]);
|
|
305
|
-
}
|
|
178
|
+
case "Node.js":
|
|
179
|
+
throw new Error(`${Function.name}: ESM 版本不支持 Worker/Node.js,请改用 CJS 入口`);
|
|
180
|
+
default:
|
|
181
|
+
throw new Error(`${Function.name}: 当前平台不支持`);
|
|
306
182
|
}
|
|
307
183
|
}
|
package/polyfill/fetch.mts
CHANGED
package/polyfill/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Aggregated exports for polyfill modules.
|
|
4
4
|
*/
|
|
5
5
|
export { Console } from "./Console.mjs";
|
|
6
|
-
export { fetch } from "./fetch.
|
|
7
|
-
export type { Fetch, FetchRequest, FetchResponse } from "./fetch.
|
|
6
|
+
export { fetch } from "./fetch.mjs";
|
|
7
|
+
export type { Fetch, FetchRequest, FetchResponse } from "./fetch.d.ts";
|
|
8
8
|
export { Lodash } from "./Lodash.mjs";
|
|
9
9
|
export { qs } from "./qs.mjs";
|
|
10
10
|
export { StatusTexts } from "./StatusTexts.mjs";
|
|
11
|
-
export { Storage } from "./Storage.
|
|
11
|
+
export { Storage } from "./Storage.mjs";
|
package/polyfill/index.js
CHANGED