@nsnanocat/util 2.6.3 → 2.6.7
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 +21 -8
- package/getStorage.mjs +1 -1
- package/index.cjs +2 -0
- package/index.js +1 -1
- package/package.json +5 -1
- package/polyfill/Storage.cjs +220 -0
- package/polyfill/{Storage.js → Storage.mjs} +3 -3
- package/polyfill/fetch.mjs +129 -7
- package/polyfill/index.d.ts +1 -1
- package/polyfill/index.js +1 -1
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` 使用 |
|
|
@@ -434,7 +435,11 @@ Worker / Node.js 使用说明:
|
|
|
434
435
|
|
|
435
436
|
### `polyfill/Storage.mjs`
|
|
436
437
|
|
|
437
|
-
`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`)设计:
|
|
438
443
|
- 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
|
439
444
|
- 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Storage
|
|
440
445
|
- 目标:统一 VPN App 脚本环境中的持久化读写接口,并尽量贴近 Web Storage 行为
|
|
@@ -452,23 +457,31 @@ Worker / Node.js 使用说明:
|
|
|
452
457
|
- Quantumult X:可用(`$prefs.removeValueForKey`)。
|
|
453
458
|
- Surge:通过 `$persistentStore.write(null, keyName)` 删除。
|
|
454
459
|
- Worker:可用(仅删除内存缓存中的对应 key,不持久化)。
|
|
455
|
-
- Node.js
|
|
460
|
+
- Node.js:ESM 路径不支持,会提示改用 CJS 入口。
|
|
456
461
|
- Loon / Stash / Egern / Shadowrocket:返回 `false`。
|
|
457
462
|
|
|
458
463
|
#### `Storage.clear()`
|
|
459
464
|
- Quantumult X:可用(`$prefs.removeAllValues`)。
|
|
460
465
|
- Worker:可用(仅清空内存缓存,不持久化)。
|
|
461
|
-
- Node.js
|
|
466
|
+
- Node.js:ESM 路径不支持,会提示改用 CJS 入口。
|
|
462
467
|
- 其他平台:返回 `false`。
|
|
463
468
|
|
|
464
|
-
#### Worker
|
|
469
|
+
#### Worker 特性(ESM)
|
|
465
470
|
- Worker:使用进程内内存缓存,不写文件。
|
|
471
|
+
|
|
472
|
+
#### Node.js 特性(CJS)
|
|
466
473
|
- 数据文件默认:`box.dat`。
|
|
467
474
|
- 读取路径优先级:当前目录 -> `process.cwd()`。
|
|
468
475
|
|
|
476
|
+
Node.js 使用说明:
|
|
477
|
+
- 请通过 CJS 入口调用:`require("@nsnanocat/util").Storage` 或 `require("@nsnanocat/util/polyfill/Storage").Storage`
|
|
478
|
+
- CJS 的 `Storage` 分支支持 `box.dat` 读写与落盘
|
|
479
|
+
|
|
469
480
|
与 Web Storage 的行为差异:
|
|
470
481
|
- 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。
|
|
471
|
-
- `removeItem/clear`
|
|
482
|
+
- `removeItem/clear` 仅部分平台可用:
|
|
483
|
+
- ESM 路径:Quantumult X、Worker,以及 Surge 的 `removeItem`
|
|
484
|
+
- CJS 路径:Worker、Node.js
|
|
472
485
|
- `getItem` 会尝试 `JSON.parse`,`setItem` 写入对象会 `JSON.stringify`。
|
|
473
486
|
|
|
474
487
|
平台后端映射:
|
|
@@ -477,8 +490,8 @@ Worker / Node.js 使用说明:
|
|
|
477
490
|
| --- | --- |
|
|
478
491
|
| Surge / Loon / Stash / Egern / Shadowrocket | `$persistentStore.read/write` |
|
|
479
492
|
| Quantumult X | `$prefs.valueForKey/setValueForKey` |
|
|
480
|
-
| Worker | 进程内内存缓存 |
|
|
481
|
-
| Node.js | 本地 `box.dat` |
|
|
493
|
+
| Worker(ESM/CJS) | 进程内内存缓存 |
|
|
494
|
+
| Node.js(仅 CJS) | 本地 `box.dat` |
|
|
482
495
|
|
|
483
496
|
### `polyfill/Console.mjs`
|
|
484
497
|
|
package/getStorage.mjs
CHANGED
package/index.cjs
CHANGED
package/index.js
CHANGED
|
@@ -9,7 +9,7 @@ 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
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"import": "./polyfill/fetch.mjs",
|
|
24
24
|
"require": "./polyfill/fetch.cjs"
|
|
25
25
|
},
|
|
26
|
+
"./polyfill/Storage": {
|
|
27
|
+
"import": "./polyfill/Storage.mjs",
|
|
28
|
+
"require": "./polyfill/Storage.cjs"
|
|
29
|
+
},
|
|
26
30
|
"./*": "./*"
|
|
27
31
|
},
|
|
28
32
|
"types": "types/nsnanocat-util.d.ts",
|
|
@@ -59,5 +63,5 @@
|
|
|
59
63
|
"registry": "https://registry.npmjs.org/",
|
|
60
64
|
"access": "public"
|
|
61
65
|
},
|
|
62
|
-
"version": "2.6.
|
|
66
|
+
"version": "2.6.7"
|
|
63
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:
|
|
@@ -111,7 +111,7 @@ export class Storage {
|
|
|
111
111
|
}
|
|
112
112
|
try {
|
|
113
113
|
keyValue = JSON.parse(keyValue);
|
|
114
|
-
} catch
|
|
114
|
+
} catch {
|
|
115
115
|
// do nothing
|
|
116
116
|
}
|
|
117
117
|
break;
|
|
@@ -200,7 +200,7 @@ export class Storage {
|
|
|
200
200
|
keyName = key;
|
|
201
201
|
let value = Storage.getItem(keyName);
|
|
202
202
|
if (typeof value !== "object") value = {};
|
|
203
|
-
|
|
203
|
+
_.unset(value, path);
|
|
204
204
|
result = Storage.setItem(keyName, value);
|
|
205
205
|
break;
|
|
206
206
|
}
|
package/polyfill/fetch.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
16
16
|
* @property {string} [policy] 指定策略 / Preferred policy.
|
|
17
17
|
* @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
|
|
18
18
|
* @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag.
|
|
19
|
+
* @property {boolean|number|string} ["auto-cookie"] Worker / Node.js Cookie 开关 / Worker / Node.js Cookie toggle.
|
|
19
20
|
* @property {Record<string, any>} [opts] 平台扩展字段 / Platform extension fields.
|
|
20
21
|
*/
|
|
21
22
|
|
|
@@ -34,8 +35,35 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
*
|
|
38
|
-
* `fetch` adapter
|
|
38
|
+
* 跨平台 `fetch` 适配层。
|
|
39
|
+
* Cross-platform `fetch` adapter.
|
|
40
|
+
*
|
|
41
|
+
* 设计目标:
|
|
42
|
+
* Design goal:
|
|
43
|
+
* - 仿照 Web API `fetch`(`Window.fetch`)接口设计
|
|
44
|
+
* - Modeled after Web API `fetch` (`Window.fetch`)
|
|
45
|
+
* - 统一 VPN App、Worker 与 Node.js 环境中的请求调用
|
|
46
|
+
* - Unify request calls across VPN apps, Worker, and Node.js
|
|
47
|
+
*
|
|
48
|
+
* 功能:
|
|
49
|
+
* Features:
|
|
50
|
+
* - 统一 Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js 请求接口
|
|
51
|
+
* - Normalize request APIs across Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js
|
|
52
|
+
* - 统一返回体字段(`ok/status/statusText/body/bodyBytes`)
|
|
53
|
+
* - Normalize response fields (`ok/status/statusText/body/bodyBytes`)
|
|
54
|
+
*
|
|
55
|
+
* 与 Web `fetch` 的已知差异:
|
|
56
|
+
* Known differences from Web `fetch`:
|
|
57
|
+
* - 支持 `policy`、`auto-redirect` 等平台扩展字段
|
|
58
|
+
* - Supports platform extension fields like `policy` and `auto-redirect`
|
|
59
|
+
* - Worker / Node.js 共享基于 `fetch` 的请求分支
|
|
60
|
+
* - Worker / Node.js share the `fetch`-based request branch
|
|
61
|
+
* - `auto-cookie` 在 Worker / Node.js 共享分支中识别
|
|
62
|
+
* - `auto-cookie` is recognized by the shared Worker / Node.js branch
|
|
63
|
+
* - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现
|
|
64
|
+
* - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine
|
|
65
|
+
* - 返回结构包含 `statusCode/bodyBytes` 等兼容字段
|
|
66
|
+
* - Response includes compatibility fields like `statusCode/bodyBytes`
|
|
39
67
|
*
|
|
40
68
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
|
41
69
|
* @link https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
|
|
@@ -45,6 +73,8 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
45
73
|
* @returns {Promise<FetchResponse>}
|
|
46
74
|
*/
|
|
47
75
|
export async function fetch(resource, options = {}) {
|
|
76
|
+
// 初始化参数。
|
|
77
|
+
// Initialize request input.
|
|
48
78
|
switch (typeof resource) {
|
|
49
79
|
case "object":
|
|
50
80
|
resource = { ...options, ...resource };
|
|
@@ -56,39 +86,58 @@ export async function fetch(resource, options = {}) {
|
|
|
56
86
|
default:
|
|
57
87
|
throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
|
|
58
88
|
}
|
|
59
|
-
|
|
89
|
+
// 自动判断请求方法。
|
|
90
|
+
// Infer the HTTP method automatically.
|
|
60
91
|
if (!resource.method) {
|
|
61
92
|
resource.method = "GET";
|
|
62
93
|
if (resource.body ?? resource.bodyBytes) resource.method = "POST";
|
|
63
94
|
}
|
|
95
|
+
// 移除需要由底层实现自动生成的请求头。
|
|
96
|
+
// Remove headers that should be generated by the underlying runtime.
|
|
64
97
|
delete resource.headers?.Host;
|
|
65
98
|
delete resource.headers?.[":authority"];
|
|
66
99
|
delete resource.headers?.["Content-Length"];
|
|
67
100
|
delete resource.headers?.["content-length"];
|
|
101
|
+
// 统一请求方法为小写,方便后续索引平台 API。
|
|
102
|
+
// Normalize the method to lowercase for platform API lookups.
|
|
68
103
|
const method = resource.method.toLocaleLowerCase();
|
|
69
|
-
|
|
104
|
+
// 默认请求超时时间为 5 秒。
|
|
105
|
+
// Default request timeout to 5 seconds.
|
|
70
106
|
if (!resource.timeout) resource.timeout = 5;
|
|
71
107
|
if (resource.timeout) {
|
|
72
108
|
resource.timeout = Number.parseInt(resource.timeout, 10);
|
|
109
|
+
// 统一先转换为秒,大于 500 视为毫秒输入。
|
|
110
|
+
// Convert to seconds first and treat values above 500 as milliseconds.
|
|
73
111
|
if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
|
|
74
112
|
}
|
|
75
113
|
if (resource.timeout) {
|
|
76
114
|
switch ($app) {
|
|
77
115
|
case "Loon":
|
|
78
116
|
case "Quantumult X":
|
|
117
|
+
case "Worker":
|
|
118
|
+
case "Node.js":
|
|
119
|
+
// 这些平台要求毫秒,因此把秒重新换算为毫秒。
|
|
120
|
+
// These platforms expect milliseconds, so convert seconds back to milliseconds.
|
|
79
121
|
resource.timeout = resource.timeout * 1000;
|
|
80
122
|
break;
|
|
123
|
+
case "Shadowrocket":
|
|
124
|
+
case "Stash":
|
|
125
|
+
case "Egern":
|
|
126
|
+
case "Surge":
|
|
81
127
|
default:
|
|
82
128
|
break;
|
|
83
129
|
}
|
|
84
130
|
}
|
|
85
|
-
|
|
131
|
+
// 根据当前平台选择请求实现。
|
|
132
|
+
// Select the request engine for the current platform.
|
|
86
133
|
switch ($app) {
|
|
87
134
|
case "Loon":
|
|
88
135
|
case "Surge":
|
|
89
136
|
case "Stash":
|
|
90
137
|
case "Egern":
|
|
91
138
|
case "Shadowrocket":
|
|
139
|
+
// 转换通用请求参数到 `$httpClient` 语义。
|
|
140
|
+
// Map shared request fields to `$httpClient` semantics.
|
|
92
141
|
if (resource.policy) {
|
|
93
142
|
switch ($app) {
|
|
94
143
|
case "Loon":
|
|
@@ -103,21 +152,28 @@ export async function fetch(resource, options = {}) {
|
|
|
103
152
|
}
|
|
104
153
|
}
|
|
105
154
|
if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection;
|
|
155
|
+
// 优先把 `bodyBytes` 映射回 `$httpClient` 能接受的 `body`。
|
|
156
|
+
// Prefer mapping `bodyBytes` back to the `body` field expected by `$httpClient`.
|
|
106
157
|
if (resource.bodyBytes && !resource.body) {
|
|
107
158
|
resource.body = resource.bodyBytes;
|
|
108
159
|
resource.bodyBytes = undefined;
|
|
109
160
|
}
|
|
161
|
+
// 根据 `Accept` 推断是否需要二进制响应体。
|
|
162
|
+
// Infer whether the response should be treated as binary from `Accept`.
|
|
110
163
|
switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) {
|
|
111
164
|
case "application/protobuf":
|
|
112
165
|
case "application/x-protobuf":
|
|
113
166
|
case "application/vnd.google.protobuf":
|
|
114
167
|
case "application/vnd.apple.flatbuffer":
|
|
115
168
|
case "application/grpc":
|
|
169
|
+
case "application/grpc-web":
|
|
116
170
|
case "application/grpc+proto":
|
|
117
171
|
case "application/octet-stream":
|
|
118
172
|
resource["binary-mode"] = true;
|
|
119
173
|
break;
|
|
120
174
|
}
|
|
175
|
+
// 发送 `$httpClient` 请求并归一化返回结构。
|
|
176
|
+
// Send the `$httpClient` request and normalize the response payload.
|
|
121
177
|
return new Promise((resolve, reject) => {
|
|
122
178
|
globalThis.$httpClient[method](resource, (error, response, body) => {
|
|
123
179
|
if (error) reject(error);
|
|
@@ -134,8 +190,12 @@ export async function fetch(resource, options = {}) {
|
|
|
134
190
|
});
|
|
135
191
|
});
|
|
136
192
|
case "Quantumult X":
|
|
193
|
+
// 转换 Quantumult X 专有请求参数。
|
|
194
|
+
// Map request fields to Quantumult X specific options.
|
|
137
195
|
if (resource.policy) _.set(resource, "opts.policy", resource.policy);
|
|
138
196
|
if (typeof resource["auto-redirect"] === "boolean") _.set(resource, "opts.redirection", resource["auto-redirect"]);
|
|
197
|
+
// Quantumult X 使用 `bodyBytes` 传输二进制请求体。
|
|
198
|
+
// Quantumult X uses `bodyBytes` for binary request payloads.
|
|
139
199
|
if (resource.body instanceof ArrayBuffer) {
|
|
140
200
|
resource.bodyBytes = resource.body;
|
|
141
201
|
resource.body = undefined;
|
|
@@ -143,6 +203,8 @@ export async function fetch(resource, options = {}) {
|
|
|
143
203
|
resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset);
|
|
144
204
|
resource.body = undefined;
|
|
145
205
|
} else if (resource.body) resource.bodyBytes = undefined;
|
|
206
|
+
// 发送请求,并用 `Promise.race` 提供统一超时保护。
|
|
207
|
+
// Send the request and enforce timeout with `Promise.race`.
|
|
146
208
|
return Promise.race([
|
|
147
209
|
globalThis.$task.fetch(resource).then(
|
|
148
210
|
response => {
|
|
@@ -155,6 +217,7 @@ export async function fetch(resource, options = {}) {
|
|
|
155
217
|
case "application/vnd.google.protobuf":
|
|
156
218
|
case "application/vnd.apple.flatbuffer":
|
|
157
219
|
case "application/grpc":
|
|
220
|
+
case "application/grpc-web":
|
|
158
221
|
case "application/grpc+proto":
|
|
159
222
|
case "application/octet-stream":
|
|
160
223
|
response.body = response.bodyBytes;
|
|
@@ -175,8 +238,67 @@ export async function fetch(resource, options = {}) {
|
|
|
175
238
|
}),
|
|
176
239
|
]);
|
|
177
240
|
case "Worker":
|
|
178
|
-
case "Node.js":
|
|
179
|
-
|
|
241
|
+
case "Node.js": {
|
|
242
|
+
// Worker 复用宿主 `fetch`;Node.js 优先复用原生 `fetch`,缺失时再回退到 `node-fetch`。
|
|
243
|
+
// Worker reuses host `fetch`; Node.js reuses native `fetch` first and falls back to `node-fetch`.
|
|
244
|
+
if (!globalThis.fetch) globalThis.fetch = require("node-fetch");
|
|
245
|
+
switch (resource["auto-cookie"]) {
|
|
246
|
+
case undefined:
|
|
247
|
+
case "true":
|
|
248
|
+
case true:
|
|
249
|
+
case "1":
|
|
250
|
+
case 1:
|
|
251
|
+
default:
|
|
252
|
+
// 仅在尚未包裹 CookieJar 时注入 `fetch-cookie`,避免重复包装。
|
|
253
|
+
// Inject `fetch-cookie` only once when a cookie jar is not already attached.
|
|
254
|
+
if (!globalThis.fetch?.cookieJar) globalThis.fetch = require("fetch-cookie").default(globalThis.fetch);
|
|
255
|
+
break;
|
|
256
|
+
case "false":
|
|
257
|
+
case false:
|
|
258
|
+
case "0":
|
|
259
|
+
case 0:
|
|
260
|
+
case "-1":
|
|
261
|
+
case -1:
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
// 将通用字段映射到 Worker / Node.js Fetch 语义。
|
|
265
|
+
// Map shared fields to Worker / Node.js Fetch semantics.
|
|
266
|
+
resource.redirect = resource.redirection ? "follow" : "manual";
|
|
267
|
+
const { url, ...options } = resource;
|
|
268
|
+
// 发起请求并归一化响应头、文本与二进制响应体。
|
|
269
|
+
// Send the request and normalize headers, text, and binary response data.
|
|
270
|
+
return Promise.race([
|
|
271
|
+
globalThis
|
|
272
|
+
.fetch(url, options)
|
|
273
|
+
.then(async response => {
|
|
274
|
+
const bodyBytes = await response.arrayBuffer();
|
|
275
|
+
let headers;
|
|
276
|
+
try {
|
|
277
|
+
headers = response.headers.raw();
|
|
278
|
+
} catch {
|
|
279
|
+
headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => {
|
|
280
|
+
acc[key] = acc[key] ? [...acc[key], value] : [value];
|
|
281
|
+
return acc;
|
|
282
|
+
}, {});
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
ok: response.ok ?? /^2\d\d$/.test(response.status),
|
|
286
|
+
status: response.status,
|
|
287
|
+
statusCode: response.status,
|
|
288
|
+
statusText: response.statusText,
|
|
289
|
+
body: new TextDecoder("utf-8").decode(bodyBytes),
|
|
290
|
+
bodyBytes: bodyBytes,
|
|
291
|
+
headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])),
|
|
292
|
+
};
|
|
293
|
+
})
|
|
294
|
+
.catch(error => Promise.reject(error.message)),
|
|
295
|
+
new Promise((resolve, reject) => {
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
|
|
298
|
+
}, resource.timeout);
|
|
299
|
+
}),
|
|
300
|
+
]);
|
|
301
|
+
}
|
|
180
302
|
default:
|
|
181
303
|
throw new Error(`${Function.name}: 当前平台不支持`);
|
|
182
304
|
}
|
package/polyfill/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ 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