@nsnanocat/util 2.1.5 → 2.2.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/README.md +36 -12
- package/getStorage.mjs +35 -8
- package/lib/app.mjs +25 -18
- package/lib/done.mjs +5 -1
- package/package.json +1 -1
- package/polyfill/Storage.mjs +10 -2
- package/polyfill/fetch.mjs +68 -25
- package/types/nsnanocat-util.d.ts +18 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
### 仓库中存在但未从主入口导出
|
|
88
88
|
- `lib/environment.mjs`
|
|
89
89
|
- `lib/runScript.mjs`
|
|
90
|
-
- `getStorage.mjs
|
|
90
|
+
- `getStorage.mjs`(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用;请通过子路径 `@nsnanocat/util/getStorage.mjs` 导入)
|
|
91
91
|
|
|
92
92
|
## 模块依赖关系
|
|
93
93
|
|
|
@@ -116,7 +116,7 @@ import {
|
|
|
116
116
|
### `lib/app.mjs` 与 `lib/environment.mjs`(平台识别与环境)
|
|
117
117
|
|
|
118
118
|
#### `$app`
|
|
119
|
-
- 类型:`"Quantumult X" | "Loon" | "Shadowrocket" | "
|
|
119
|
+
- 类型:`"Quantumult X" | "Loon" | "Shadowrocket" | "Egern" | "Surge" | "Stash" | "Node.js" | undefined`
|
|
120
120
|
- 角色:核心模块。库内所有存在平台行为差异的模块都会先读取 `$app` 再分流(如 `done`、`notification`、`fetch`、`Storage`、`Console`、`environment`)。
|
|
121
121
|
- 读取方式:
|
|
122
122
|
|
|
@@ -130,10 +130,12 @@ console.log(appName);
|
|
|
130
130
|
1. 存在 `$task` -> `Quantumult X`
|
|
131
131
|
2. 存在 `$loon` -> `Loon`
|
|
132
132
|
3. 存在 `$rocket` -> `Shadowrocket`
|
|
133
|
-
4. 存在 `
|
|
134
|
-
5. 存在 `
|
|
135
|
-
6. 存在 `$environment` 且有 `
|
|
136
|
-
7. 存在
|
|
133
|
+
4. 存在 `Egern` -> `Egern`
|
|
134
|
+
5. 存在 `$environment` 且有 `surge-version` -> `Surge`
|
|
135
|
+
6. 存在 `$environment` 且有 `stash-version` -> `Stash`
|
|
136
|
+
7. 存在 `process.versions.node` -> `Node.js`
|
|
137
|
+
8. 默认回落 -> `undefined`
|
|
138
|
+
- 实现细节:内部使用 `'key' in globalThis` 检测平台标记,避免 `Object.keys(globalThis)` 漏掉不可枚举全局变量;因此在 Workers / Vercel 风格全局对象存在时,只要 `process.versions.node` 可用,仍会识别为 `Node.js`。
|
|
137
139
|
|
|
138
140
|
#### `$environment` / `environment()`
|
|
139
141
|
- 路径:`lib/environment.mjs`(未从包主入口导出)
|
|
@@ -221,6 +223,7 @@ console.log($argument); // { mode: "on", a: { b: "1" } }
|
|
|
221
223
|
- Quantumult X 会丢弃未在白名单内的字段。
|
|
222
224
|
- Quantumult X 的 `status` 在部分场景要求完整状态行(如 `HTTP/1.1 200 OK`),本库会在传入数字状态码时自动拼接(依赖 `StatusTexts`)。
|
|
223
225
|
- Node.js 不调用 `$done`,而是直接退出进程,且退出码固定为 `1`。
|
|
226
|
+
- 未识别平台(`$app === undefined`)只记录结束日志,不会尝试调用 `$done` 或退出进程。
|
|
224
227
|
|
|
225
228
|
### `lib/notification.mjs`
|
|
226
229
|
|
|
@@ -336,6 +339,24 @@ import getStorage from "@nsnanocat/util/getStorage.mjs";
|
|
|
336
339
|
const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
337
340
|
```
|
|
338
341
|
|
|
342
|
+
#### 命名导出(辅助函数)
|
|
343
|
+
|
|
344
|
+
`getStorage.mjs` 同时导出以下辅助函数:
|
|
345
|
+
- `traverseObject(o, c)`:深度遍历对象并替换叶子值
|
|
346
|
+
- `string2number(string)`:将纯数字字符串转换为数字
|
|
347
|
+
- `value2array(value)`:字符串按逗号拆分;数字/布尔值会被包装为单元素数组
|
|
348
|
+
|
|
349
|
+
示例:
|
|
350
|
+
```js
|
|
351
|
+
import getStorage, {
|
|
352
|
+
traverseObject,
|
|
353
|
+
string2number,
|
|
354
|
+
value2array,
|
|
355
|
+
} from "@nsnanocat/util/getStorage.mjs";
|
|
356
|
+
|
|
357
|
+
const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
358
|
+
```
|
|
359
|
+
|
|
339
360
|
### `polyfill/fetch.mjs`
|
|
340
361
|
|
|
341
362
|
`fetch` 是仿照 Web API `Window.fetch` 设计的跨平台适配实现:
|
|
@@ -363,6 +384,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
363
384
|
- `timeout`
|
|
364
385
|
- `policy`
|
|
365
386
|
- `redirection` / `auto-redirect`
|
|
387
|
+
- `auto-cookie`(仅 Node.js 分支识别;默认启用,传入 `false` / `0` / `-1` 可关闭)
|
|
366
388
|
|
|
367
389
|
说明:下表是各 App 原生 HTTP 接口的差异补充,以及本库 `fetch` 的内部映射方式。调用方使用统一入参即可。
|
|
368
390
|
|
|
@@ -376,7 +398,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
376
398
|
| Egern | `$httpClient[method]` | 秒 | 无专门映射 | `auto-redirect` | 同上 |
|
|
377
399
|
| Shadowrocket | `$httpClient[method]` | 秒 | `headers.X-Surge-Proxy` | `auto-redirect` | 同上 |
|
|
378
400
|
| Quantumult X | `$task.fetch` | 毫秒(内部乘 1000) | `opts.policy` | `opts.redirection` | `body(ArrayBuffer/TypedArray)` 转 `bodyBytes`;响应按 `Content-Type` 恢复到 `body` |
|
|
379
|
-
| Node.js | `fetch`
|
|
401
|
+
| Node.js | `globalThis.fetch`(不存在时回退 `node-fetch`);默认按需包裹 `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
|
|
380
402
|
|
|
381
403
|
返回对象(统一后)常见字段:
|
|
382
404
|
- `ok`
|
|
@@ -390,7 +412,8 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
390
412
|
不可用/差异点:
|
|
391
413
|
- `policy` 在 Surge / Egern / Node.js 分支没有额外适配逻辑。
|
|
392
414
|
- `redirection` 在部分平台会映射为 `auto-redirect` 或 `opts.redirection`。
|
|
393
|
-
- Node.js
|
|
415
|
+
- Node.js 分支优先复用 `globalThis.fetch`;若不存在则回退到 `node-fetch`,并在 `auto-cookie` 未关闭时按需包裹 `fetch-cookie`。
|
|
416
|
+
- 传入 `timeout` 时,`5` 和 `5000` 都会被接受;库会先将用户输入归一化,再按平台要求转换为秒或毫秒。
|
|
394
417
|
- 返回结构是统一兼容结构,不等同于浏览器 `Response` 对象。
|
|
395
418
|
|
|
396
419
|
### `polyfill/Storage.mjs`
|
|
@@ -412,10 +435,12 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
412
435
|
#### `Storage.removeItem(keyName)`
|
|
413
436
|
- Quantumult X:可用(`$prefs.removeValueForKey`)。
|
|
414
437
|
- Surge:通过 `$persistentStore.write(null, keyName)` 删除。
|
|
415
|
-
-
|
|
438
|
+
- Node.js:可用(删除 `box.dat` 中对应 key 并落盘)。
|
|
439
|
+
- Loon / Stash / Egern / Shadowrocket:返回 `false`。
|
|
416
440
|
|
|
417
441
|
#### `Storage.clear()`
|
|
418
442
|
- Quantumult X:可用(`$prefs.removeAllValues`)。
|
|
443
|
+
- Node.js:可用(清空 `box.dat` 并落盘)。
|
|
419
444
|
- 其他平台:返回 `false`。
|
|
420
445
|
|
|
421
446
|
#### Node.js 特性
|
|
@@ -424,7 +449,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
|
|
|
424
449
|
|
|
425
450
|
与 Web Storage 的行为差异:
|
|
426
451
|
- 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。
|
|
427
|
-
- `removeItem/clear` 仅部分平台可用(目前为 Quantumult X,以及 Surge 的 `removeItem`)。
|
|
452
|
+
- `removeItem/clear` 仅部分平台可用(目前为 Quantumult X、Node.js,以及 Surge 的 `removeItem`)。
|
|
428
453
|
- `getItem` 会尝试 `JSON.parse`,`setItem` 写入对象会 `JSON.stringify`。
|
|
429
454
|
|
|
430
455
|
平台后端映射:
|
|
@@ -604,14 +629,13 @@ console.log(value); // 1
|
|
|
604
629
|
| 通知 | `$notify` | `$notification.post` | `$notification.post` | `$notification.post` | `$notification.post` | `$notification.post` | 无 |
|
|
605
630
|
| 持久化 | `$prefs` | `$persistentStore` | `$persistentStore` | `$persistentStore` | `$persistentStore` | `$persistentStore` | `box.dat` |
|
|
606
631
|
| 结束脚本 | `$done` | `$done` | `$done` | `$done` | `$done` | `$done` | `process.exit(1)` |
|
|
607
|
-
| `removeItem/clear` | 可用 | 不可用 | `removeItem` 可用 / `clear` 不可用 | 不可用 | 不可用 | 不可用 |
|
|
632
|
+
| `removeItem/clear` | 可用 | 不可用 | `removeItem` 可用 / `clear` 不可用 | 不可用 | 不可用 | 不可用 | 可用 |
|
|
608
633
|
| `policy` 注入(`fetch/done`) | `opts.policy` | `node` | `X-Surge-Policy`(done) | `X-Stash-Selected-Proxy` | 无专门映射 | `X-Surge-Proxy`(fetch) | 无 |
|
|
609
634
|
|
|
610
635
|
## 已知限制与注意事项
|
|
611
636
|
|
|
612
637
|
- `lib/argument.mjs` 为 `$argument` 标准化模块,`import` 时会按规则重写全局 `$argument`。
|
|
613
638
|
- `lib/done.mjs` 在 Node.js 固定 `process.exit(1)`。
|
|
614
|
-
- `polyfill/fetch.mjs` 的超时保护使用了 `Promise.race`,但当前实现里请求 Promise 先被 `await`,可能导致超时行为与预期不完全一致。
|
|
615
639
|
- `Storage.removeItem("@a.b")` 分支存在未声明变量写入风险;如要大量使用路径删除,建议先本地验证。
|
|
616
640
|
- `lib/runScript.mjs` 未从包主入口导出,需要按文件路径直接导入。
|
|
617
641
|
|
package/getStorage.mjs
CHANGED
|
@@ -106,12 +106,20 @@ export default function getStorage(key, names, database) {
|
|
|
106
106
|
/***************** traverseObject *****************/
|
|
107
107
|
traverseObject(Root.Settings, (key, value) => {
|
|
108
108
|
Console.debug("☑️ traverseObject", `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
switch (typeof value) {
|
|
110
|
+
case "string":
|
|
111
|
+
switch (value) {
|
|
112
|
+
case "true":
|
|
113
|
+
case "false":
|
|
114
|
+
case "[]":
|
|
115
|
+
value = JSON.parse(value); // 字符串转Boolean/空数组
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
if (value.includes(","))
|
|
119
|
+
value = value2array(value).map(item => string2number(item)); // 字符串转数组转数字
|
|
120
|
+
else value = string2number(value); // 字符串转数字
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
115
123
|
}
|
|
116
124
|
return value;
|
|
117
125
|
});
|
|
@@ -128,7 +136,7 @@ export default function getStorage(key, names, database) {
|
|
|
128
136
|
* @param {(key: string, value: any) => any} c 处理回调 / Transformer callback.
|
|
129
137
|
* @returns {Record<string, any>}
|
|
130
138
|
*/
|
|
131
|
-
function traverseObject(o, c) {
|
|
139
|
+
export function traverseObject(o, c) {
|
|
132
140
|
for (const t in o) {
|
|
133
141
|
const n = o[t];
|
|
134
142
|
o[t] = "object" === typeof n && null !== n ? traverseObject(n, c) : c(t, n);
|
|
@@ -143,7 +151,26 @@ function traverseObject(o, c) {
|
|
|
143
151
|
* @param {string} string 输入字符串 / Input string.
|
|
144
152
|
* @returns {string|number}
|
|
145
153
|
*/
|
|
146
|
-
function string2number(string) {
|
|
154
|
+
export function string2number(string) {
|
|
147
155
|
if (/^\d+$/.test(string)) string = Number.parseInt(string, 10);
|
|
148
156
|
return string;
|
|
149
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 将值包装为数组。
|
|
161
|
+
* Split value into array.
|
|
162
|
+
*
|
|
163
|
+
* @param {string|number|boolean|string[]|null|undefined} value 输入值 / Input value.
|
|
164
|
+
* @returns {(string|number|boolean)[]}
|
|
165
|
+
*/
|
|
166
|
+
export function value2array(value) {
|
|
167
|
+
switch (typeof value) {
|
|
168
|
+
case "string":
|
|
169
|
+
return value.split(",");
|
|
170
|
+
case "number":
|
|
171
|
+
case "boolean":
|
|
172
|
+
return [value];
|
|
173
|
+
default:
|
|
174
|
+
return value || [];
|
|
175
|
+
}
|
|
176
|
+
}
|
package/lib/app.mjs
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Current runtime platform name.
|
|
2
|
+
* 当前运行平台名称(脚本平台优先,模块系统次之)。
|
|
3
|
+
* Current runtime platform name (script platform first, module system second).
|
|
4
4
|
*
|
|
5
5
|
* 识别顺序:
|
|
6
6
|
* Detection order:
|
|
7
7
|
* 1) `$task` -> Quantumult X
|
|
8
8
|
* 2) `$loon` -> Loon
|
|
9
9
|
* 3) `$rocket` -> Shadowrocket
|
|
10
|
-
* 4) `
|
|
11
|
-
* 5) `
|
|
12
|
-
* 6) `$environment["
|
|
13
|
-
* 7)
|
|
10
|
+
* 4) `Egern` -> Egern
|
|
11
|
+
* 5) `$environment["surge-version"]` -> Surge
|
|
12
|
+
* 6) `$environment["stash-version"]` -> Stash
|
|
13
|
+
* 7) `process.versions.node` -> Node.js
|
|
14
|
+
* 8) 默认回落 -> undefined
|
|
15
|
+
* default fallback -> undefined
|
|
14
16
|
*
|
|
15
|
-
*
|
|
17
|
+
* 说明:
|
|
18
|
+
* Notes:
|
|
19
|
+
* - 使用 `'key' in globalThis`,避免 `Object.keys` 对不可枚举全局变量漏检。
|
|
20
|
+
* - Use `'key' in globalThis` to avoid missing non-enumerable globals with `Object.keys`.
|
|
21
|
+
*
|
|
22
|
+
* @type {("Quantumult X" | "Loon" | "Shadowrocket" | "Egern" | "Surge" | "Stash" | "Node.js" | undefined)}
|
|
16
23
|
*/
|
|
17
24
|
export const $app = (() => {
|
|
18
|
-
const
|
|
25
|
+
const has = (key) => key in globalThis;
|
|
19
26
|
switch (true) {
|
|
20
|
-
case
|
|
27
|
+
case has("$task"):
|
|
21
28
|
return "Quantumult X";
|
|
22
|
-
case
|
|
29
|
+
case has("$loon"):
|
|
23
30
|
return "Loon";
|
|
24
|
-
case
|
|
31
|
+
case has("$rocket"):
|
|
25
32
|
return "Shadowrocket";
|
|
26
|
-
case
|
|
27
|
-
return "Node.js";
|
|
28
|
-
case keys.includes("Egern"):
|
|
33
|
+
case has("Egern"):
|
|
29
34
|
return "Egern";
|
|
30
|
-
case
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
35
|
+
case Boolean(globalThis.$environment?.["surge-version"]):
|
|
36
|
+
return "Surge";
|
|
37
|
+
case Boolean(globalThis.$environment?.["stash-version"]):
|
|
38
|
+
return "Stash";
|
|
39
|
+
case Boolean(globalThis.process?.versions?.node):
|
|
40
|
+
return "Node.js";
|
|
34
41
|
default:
|
|
35
42
|
return undefined;
|
|
36
43
|
}
|
package/lib/done.mjs
CHANGED
|
@@ -26,6 +26,8 @@ import { StatusTexts } from "../polyfill/StatusTexts.mjs";
|
|
|
26
26
|
* - This is the call entry and native `$done` differences are handled internally
|
|
27
27
|
* - Node.js 不调用 `$done`,而是直接退出进程
|
|
28
28
|
* - Node.js does not call `$done`; it exits the process directly
|
|
29
|
+
* - 未识别平台仅记录结束日志,不会强制退出
|
|
30
|
+
* - Unknown runtimes only log completion and do not force an exit
|
|
29
31
|
*
|
|
30
32
|
* @param {DonePayload} [object={}] 统一响应对象 / Unified response object.
|
|
31
33
|
* @returns {void}
|
|
@@ -79,9 +81,11 @@ export function done(object = {}) {
|
|
|
79
81
|
$done(object);
|
|
80
82
|
break;
|
|
81
83
|
case "Node.js":
|
|
82
|
-
default:
|
|
83
84
|
Console.log("🚩 执行结束!");
|
|
84
85
|
process.exit(1);
|
|
85
86
|
break;
|
|
87
|
+
default:
|
|
88
|
+
Console.log("🚩 执行结束!");
|
|
89
|
+
break;
|
|
86
90
|
}
|
|
87
91
|
}
|
package/package.json
CHANGED
package/polyfill/Storage.mjs
CHANGED
|
@@ -208,7 +208,11 @@ export class Storage {
|
|
|
208
208
|
result = $prefs.removeValueForKey(keyName);
|
|
209
209
|
break;
|
|
210
210
|
case "Node.js":
|
|
211
|
-
result = false;
|
|
211
|
+
// result = false;
|
|
212
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
213
|
+
delete Storage.data[keyName];
|
|
214
|
+
Storage.#writedata(Storage.dataFile);
|
|
215
|
+
result = true;
|
|
212
216
|
break;
|
|
213
217
|
default:
|
|
214
218
|
result = false;
|
|
@@ -239,7 +243,11 @@ export class Storage {
|
|
|
239
243
|
result = $prefs.removeAllValues();
|
|
240
244
|
break;
|
|
241
245
|
case "Node.js":
|
|
242
|
-
result = false;
|
|
246
|
+
// result = false;
|
|
247
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
248
|
+
Storage.data = {};
|
|
249
|
+
Storage.#writedata(Storage.dataFile);
|
|
250
|
+
result = true;
|
|
243
251
|
break;
|
|
244
252
|
default:
|
|
245
253
|
result = false;
|
package/polyfill/fetch.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
17
17
|
* @property {string} [policy] 指定策略 / Preferred policy.
|
|
18
18
|
* @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
|
|
19
19
|
* @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag.
|
|
20
|
+
* @property {boolean|number|string} ["auto-cookie"] Node.js Cookie 开关 / Node.js Cookie toggle.
|
|
20
21
|
* @property {Record<string, any>} [opts] 平台扩展字段 / Platform extension fields.
|
|
21
22
|
*/
|
|
22
23
|
|
|
@@ -56,6 +57,8 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
56
57
|
* Known differences from Web `fetch`:
|
|
57
58
|
* - 支持 `policy`、`auto-redirect` 等平台扩展字段
|
|
58
59
|
* - Supports platform extension fields like `policy` and `auto-redirect`
|
|
60
|
+
* - Node.js 分支默认启用 Cookie 透传,可通过 `auto-cookie` 关闭
|
|
61
|
+
* - Node.js enables Cookie forwarding by default and can disable it via `auto-cookie`
|
|
59
62
|
* - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现
|
|
60
63
|
* - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine
|
|
61
64
|
* - 返回结构包含 `statusCode/bodyBytes` 等兼容字段
|
|
@@ -69,7 +72,8 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
69
72
|
* @returns {Promise<FetchResponse>}
|
|
70
73
|
*/
|
|
71
74
|
export async function fetch(resource, options = {}) {
|
|
72
|
-
//
|
|
75
|
+
// 初始化参数。
|
|
76
|
+
// Initialize request input.
|
|
73
77
|
switch (typeof resource) {
|
|
74
78
|
case "object":
|
|
75
79
|
resource = { ...options, ...resource };
|
|
@@ -81,26 +85,34 @@ export async function fetch(resource, options = {}) {
|
|
|
81
85
|
default:
|
|
82
86
|
throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
|
|
83
87
|
}
|
|
84
|
-
//
|
|
88
|
+
// 自动判断请求方法。
|
|
89
|
+
// Infer the HTTP method automatically.
|
|
85
90
|
if (!resource.method) {
|
|
86
91
|
resource.method = "GET";
|
|
87
92
|
if (resource.body ?? resource.bodyBytes) resource.method = "POST";
|
|
88
93
|
}
|
|
89
|
-
//
|
|
94
|
+
// 移除需要由底层实现自动生成的请求头。
|
|
95
|
+
// Remove headers that should be generated by the underlying runtime.
|
|
90
96
|
delete resource.headers?.Host;
|
|
91
97
|
delete resource.headers?.[":authority"];
|
|
92
98
|
delete resource.headers?.["Content-Length"];
|
|
93
99
|
delete resource.headers?.["content-length"];
|
|
94
|
-
//
|
|
100
|
+
// 统一请求方法为小写,方便后续索引平台 API。
|
|
101
|
+
// Normalize the method to lowercase for platform API lookups.
|
|
95
102
|
const method = resource.method.toLocaleLowerCase();
|
|
96
|
-
//
|
|
103
|
+
// 默认请求超时时间为 5 秒。
|
|
104
|
+
// Default request timeout to 5 seconds.
|
|
97
105
|
if (!resource.timeout) resource.timeout = 5;
|
|
106
|
+
// 智能矫正请求超时时间,兼容用户输入的秒或毫秒。
|
|
107
|
+
// Normalize timeout input so both seconds and milliseconds are accepted.
|
|
98
108
|
if (resource.timeout) {
|
|
99
109
|
resource.timeout = Number.parseInt(resource.timeout, 10);
|
|
100
|
-
//
|
|
110
|
+
// 统一先转换为秒,大于 500 视为毫秒输入。
|
|
111
|
+
// Convert to seconds first and treat values above 500 as milliseconds.
|
|
101
112
|
if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
|
|
102
113
|
}
|
|
103
|
-
//
|
|
114
|
+
// 根据当前平台选择请求实现。
|
|
115
|
+
// Select the request engine for the current platform.
|
|
104
116
|
switch ($app) {
|
|
105
117
|
case "Loon":
|
|
106
118
|
case "Surge":
|
|
@@ -108,10 +120,15 @@ export async function fetch(resource, options = {}) {
|
|
|
108
120
|
case "Egern":
|
|
109
121
|
case "Shadowrocket":
|
|
110
122
|
default:
|
|
111
|
-
//
|
|
123
|
+
// 转换通用请求参数到 `$httpClient` 语义。
|
|
124
|
+
// Map shared request fields to `$httpClient` semantics.
|
|
112
125
|
if (resource.timeout) {
|
|
113
126
|
switch ($app) {
|
|
114
127
|
case "Loon":
|
|
128
|
+
case "Quantumult X":
|
|
129
|
+
case "Node.js":
|
|
130
|
+
// 这些平台要求毫秒,因此把秒重新换算为毫秒。
|
|
131
|
+
// These platforms expect milliseconds, so convert seconds back to milliseconds.
|
|
115
132
|
resource.timeout = resource.timeout * 1000;
|
|
116
133
|
break;
|
|
117
134
|
case "Shadowrocket":
|
|
@@ -136,12 +153,14 @@ export async function fetch(resource, options = {}) {
|
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection;
|
|
139
|
-
//
|
|
156
|
+
// 优先把 `bodyBytes` 映射回 `$httpClient` 能接受的 `body`。
|
|
157
|
+
// Prefer mapping `bodyBytes` back to the `body` field expected by `$httpClient`.
|
|
140
158
|
if (resource.bodyBytes && !resource.body) {
|
|
141
159
|
resource.body = resource.bodyBytes;
|
|
142
160
|
resource.bodyBytes = undefined;
|
|
143
161
|
}
|
|
144
|
-
//
|
|
162
|
+
// 根据 `Accept` 推断是否需要二进制响应体。
|
|
163
|
+
// Infer whether the response should be treated as binary from `Accept`.
|
|
145
164
|
switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) {
|
|
146
165
|
case "application/protobuf":
|
|
147
166
|
case "application/x-protobuf":
|
|
@@ -153,9 +172,10 @@ export async function fetch(resource, options = {}) {
|
|
|
153
172
|
resource["binary-mode"] = true;
|
|
154
173
|
break;
|
|
155
174
|
}
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
// 发送 `$httpClient` 请求并归一化返回结构。
|
|
176
|
+
// Send the `$httpClient` request and normalize the response payload.
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
globalThis.$httpClient[method](resource, (error, response, body) => {
|
|
159
179
|
if (error) reject(error);
|
|
160
180
|
else {
|
|
161
181
|
response.ok = /^2\d\d$/.test(response.status);
|
|
@@ -170,11 +190,12 @@ export async function fetch(resource, options = {}) {
|
|
|
170
190
|
});
|
|
171
191
|
});
|
|
172
192
|
case "Quantumult X":
|
|
173
|
-
//
|
|
174
|
-
|
|
193
|
+
// 转换 Quantumult X 专有请求参数。
|
|
194
|
+
// Map request fields to Quantumult X specific options.
|
|
175
195
|
if (resource.policy) _.set(resource, "opts.policy", resource.policy);
|
|
176
196
|
if (typeof resource["auto-redirect"] === "boolean") _.set(resource, "opts.redirection", resource["auto-redirect"]);
|
|
177
|
-
//
|
|
197
|
+
// Quantumult X 使用 `bodyBytes` 传输二进制请求体。
|
|
198
|
+
// Quantumult X uses `bodyBytes` for binary request payloads.
|
|
178
199
|
if (resource.body instanceof ArrayBuffer) {
|
|
179
200
|
resource.bodyBytes = resource.body;
|
|
180
201
|
resource.body = undefined;
|
|
@@ -182,9 +203,10 @@ export async function fetch(resource, options = {}) {
|
|
|
182
203
|
resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset);
|
|
183
204
|
resource.body = undefined;
|
|
184
205
|
} else if (resource.body) resource.bodyBytes = undefined;
|
|
185
|
-
//
|
|
206
|
+
// 发送请求,并用 `Promise.race` 提供统一超时保护。
|
|
207
|
+
// Send the request and enforce timeout with `Promise.race`.
|
|
186
208
|
return Promise.race([
|
|
187
|
-
|
|
209
|
+
globalThis.$task.fetch(resource).then(
|
|
188
210
|
response => {
|
|
189
211
|
response.ok = /^2\d\d$/.test(response.statusCode);
|
|
190
212
|
response.status = response.statusCode;
|
|
@@ -215,16 +237,37 @@ export async function fetch(resource, options = {}) {
|
|
|
215
237
|
}),
|
|
216
238
|
]);
|
|
217
239
|
case "Node.js": {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
// Node.js 优先复用原生/宿主 `fetch`,缺失时再回退到 `node-fetch`。
|
|
241
|
+
// Reuse host `fetch` in Node.js when available and fall back to `node-fetch` otherwise.
|
|
242
|
+
if (!globalThis.fetch) globalThis.fetch = require("node-fetch");
|
|
243
|
+
switch (resource["auto-cookie"]) {
|
|
244
|
+
case undefined:
|
|
245
|
+
case "true":
|
|
246
|
+
case true:
|
|
247
|
+
case "1":
|
|
248
|
+
case 1:
|
|
249
|
+
default:
|
|
250
|
+
// 仅在尚未包裹 CookieJar 时注入 `fetch-cookie`,避免重复包装。
|
|
251
|
+
// Inject `fetch-cookie` only once when a cookie jar is not already attached.
|
|
252
|
+
if (!globalThis.fetch?.cookieJar) globalThis.fetch = require("fetch-cookie").default(globalThis.fetch);
|
|
253
|
+
break;
|
|
254
|
+
case "false":
|
|
255
|
+
case false:
|
|
256
|
+
case "0":
|
|
257
|
+
case 0:
|
|
258
|
+
case "-1":
|
|
259
|
+
case -1:
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
// 将通用字段映射到 Node.js Fetch 语义。
|
|
263
|
+
// Map shared fields to Node.js Fetch semantics.
|
|
223
264
|
resource.redirect = resource.redirection ? "follow" : "manual";
|
|
224
265
|
const { url, ...options } = resource;
|
|
225
|
-
//
|
|
266
|
+
// 发起请求并归一化响应头、文本与二进制响应体。
|
|
267
|
+
// Send the request and normalize headers, text, and binary response data.
|
|
226
268
|
return Promise.race([
|
|
227
|
-
|
|
269
|
+
globalThis
|
|
270
|
+
.fetch(url, options)
|
|
228
271
|
.then(async response => {
|
|
229
272
|
const bodyBytes = await response.arrayBuffer();
|
|
230
273
|
let headers;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
declare module "@nsnanocat/util" {
|
|
2
|
-
export type AppName =
|
|
2
|
+
export type AppName =
|
|
3
|
+
| "Quantumult X"
|
|
4
|
+
| "Loon"
|
|
5
|
+
| "Shadowrocket"
|
|
6
|
+
| "Egern"
|
|
7
|
+
| "Surge"
|
|
8
|
+
| "Stash"
|
|
9
|
+
| "Node.js";
|
|
3
10
|
|
|
4
11
|
export const $app: AppName | undefined;
|
|
5
12
|
export const $argument: Record<string, unknown>;
|
|
@@ -54,6 +61,7 @@ declare module "@nsnanocat/util" {
|
|
|
54
61
|
policy?: string;
|
|
55
62
|
redirection?: boolean;
|
|
56
63
|
"auto-redirect"?: boolean;
|
|
64
|
+
"auto-cookie"?: boolean | number | string;
|
|
57
65
|
opts?: Record<string, unknown>;
|
|
58
66
|
[key: string]: unknown;
|
|
59
67
|
}
|
|
@@ -121,6 +129,15 @@ declare module "@nsnanocat/util/getStorage.mjs" {
|
|
|
121
129
|
Caches: Record<string, unknown>;
|
|
122
130
|
}
|
|
123
131
|
|
|
132
|
+
export function traverseObject(
|
|
133
|
+
o: Record<string, unknown>,
|
|
134
|
+
c: (key: string, value: unknown) => unknown,
|
|
135
|
+
): Record<string, unknown>;
|
|
136
|
+
|
|
137
|
+
export function string2number(string: string): string | number;
|
|
138
|
+
|
|
139
|
+
export function value2array(value: string | number | boolean | string[] | null | undefined): Array<string | number | boolean>;
|
|
140
|
+
|
|
124
141
|
export default function getStorage(
|
|
125
142
|
key: string,
|
|
126
143
|
names: string | string[] | Array<string | string[]>,
|