@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 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" | "Node.js" | "Egern" | "Surge" | "Stash" | undefined`
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. 存在 `module` -> `Node.js`
134
- 5. 存在 `Egern` -> `Egern`
135
- 6. 存在 `$environment` 且有 `surge-version` -> `Surge`
136
- 7. 存在 `$environment` 且有 `stash-version` -> `Stash`
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` + `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
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 分支依赖 `globalThis.fetch` / `globalThis.fetchCookie` 或 `node-fetch` + `fetch-cookie`。
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
- - Loon / Stash / Egern / Shadowrocket / Node.js:返回 `false`。
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
- if (value === "true" || value === "false")
110
- value = JSON.parse(value); // 字符串转Boolean
111
- else if (typeof value === "string") {
112
- if (value.includes(","))
113
- value = value.split(",").map(item => string2number(item)); // 字符串转数组转数字
114
- else value = string2number(value); // 字符串转数字
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) `module` -> Node.js
11
- * 5) `Egern` -> Egern
12
- * 6) `$environment["surge-version"]` -> Surge
13
- * 7) `$environment["stash-version"]` -> Stash
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
- * @type {("Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash" | undefined)}
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 keys = Object.keys(globalThis);
25
+ const has = (key) => key in globalThis;
19
26
  switch (true) {
20
- case keys.includes("$task"):
27
+ case has("$task"):
21
28
  return "Quantumult X";
22
- case keys.includes("$loon"):
29
+ case has("$loon"):
23
30
  return "Loon";
24
- case keys.includes("$rocket"):
31
+ case has("$rocket"):
25
32
  return "Shadowrocket";
26
- case typeof module !== "undefined":
27
- return "Node.js";
28
- case keys.includes("Egern"):
33
+ case has("Egern"):
29
34
  return "Egern";
30
- case keys.includes("$environment"):
31
- if ($environment["surge-version"]) return "Surge";
32
- if ($environment["stash-version"]) return "Stash";
33
- return undefined;
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
@@ -41,5 +41,5 @@
41
41
  "registry": "https://registry.npmjs.org/",
42
42
  "access": "public"
43
43
  },
44
- "version": "2.1.5"
44
+ "version": "2.2.0"
45
45
  }
@@ -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;
@@ -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
- // 转换为秒,大于500视为毫秒,小于等于500视为秒
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
- return await new Promise((resolve, reject) => {
158
- $httpClient[method](resource, (error, response, body) => {
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
- resource.timeout = resource.timeout * 1000;
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
- await $task.fetch(resource).then(
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
- const nodeFetch = globalThis.fetch ? globalThis.fetch : require("node-fetch");
219
- const fetchCookie = globalThis.fetchCookie ? globalThis.fetchCookie : require("fetch-cookie").default;
220
- const fetch = fetchCookie(nodeFetch);
221
- // 转换请求参数
222
- resource.timeout = resource.timeout * 1000;
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
- await fetch(url, options)
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 = "Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash";
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[]>,