@nsnanocat/util 2.0.0 → 2.1.1

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
@@ -1 +1,601 @@
1
- # utils
1
+ # @nsnanocat/util
2
+
3
+ 用于统一 Quantumult X / Loon / Shadowrocket / Node.js / Egern / Surge / Stash 脚本接口的通用工具库。
4
+
5
+ 核心目标:
6
+ - 统一不同平台的 HTTP、通知、持久化、结束脚本等调用方式。
7
+ - 在一个脚本里尽量少写平台分支。
8
+ - 提供一组可直接复用的 polyfill(`fetch` / `Storage` / `Console` / `Lodash`)。
9
+
10
+ ## 目录
11
+ - [安装与导入](#安装与导入)
12
+ - [导出清单](#导出清单)
13
+ - [模块依赖关系](#模块依赖关系)
14
+ - [API 参考(按 mjs 文件)](#api-参考按-mjs-文件)
15
+ - [平台差异总览](#平台差异总览)
16
+ - [已知限制与注意事项](#已知限制与注意事项)
17
+ - [参考资料](#参考资料)
18
+
19
+ ## 安装与导入
20
+
21
+ 发布源:
22
+ - npm(推荐):[https://www.npmjs.com/package/@nsnanocat/util](https://www.npmjs.com/package/@nsnanocat/util)
23
+ - GitHub Packages(同步发布):[https://github.com/NSNanoCat/util/pkgs/npm/util](https://github.com/NSNanoCat/util/pkgs/npm/util)
24
+
25
+ 如果你不确定该选哪个,直接用 npm 源即可。
26
+ 如果你从 GitHub Packages 安装,需要先配置 GitHub 认证(PAT Token)。
27
+
28
+ ### 1) 使用 npm 源(推荐,最省事)
29
+
30
+ ```bash
31
+ # 首次安装:拉取并安装这个包
32
+ npm i @nsnanocat/util
33
+
34
+ # 更新到最新版本:升级已安装的 util
35
+ npm i @nsnanocat/util@latest
36
+ # 你也可以使用 update(效果类似)
37
+ # npm update @nsnanocat/util
38
+ ```
39
+
40
+ ### 2) 使用 GitHub Packages 源(同步源,需要 GitHub 鉴权)
41
+
42
+ ```bash
43
+ # 把 @nsnanocat 作用域的包下载源切到 GitHub Packages
44
+ npm config set @nsnanocat:registry https://npm.pkg.github.com
45
+
46
+ # 配置 GitHub Token(用于下载 GitHub Packages)
47
+ # 建议把 YOUR_GITHUB_PAT 换成你的真实 Token,再执行
48
+ # echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT" >> ~/.npmrc
49
+
50
+ # 首次安装:从 GitHub Packages 安装 util
51
+ npm i @nsnanocat/util
52
+
53
+ # 更新到最新版本:从 GitHub Packages 拉取最新 util
54
+ npm i @nsnanocat/util@latest
55
+ ```
56
+
57
+ ```js
58
+ import {
59
+ $app, // 当前平台名(如 "Surge" / "Loon" / "Quantumult X" / "Node.js")
60
+ $argument, // 已标准化的模块参数对象(导入包时自动处理字符串 -> 对象)
61
+ done, // 统一结束脚本函数(内部自动适配各平台 $done 差异)
62
+ fetch, // 统一 HTTP 请求函数(内部自动适配 $httpClient / $task / Node fetch)
63
+ notification, // 统一通知函数(内部自动适配 $notify / $notification.post)
64
+ time, // 时间格式化工具
65
+ wait, // 延时等待工具(Promise)
66
+ Console, // 统一日志工具(支持 logLevel)
67
+ Lodash, // 内置的 Lodash 部分方法实现
68
+ Storage, // 统一持久化存储接口(适配 $prefs / $persistentStore / 文件)
69
+ } from "@nsnanocat/util";
70
+ ```
71
+
72
+ ## 导出清单
73
+
74
+ ### 包主入口(`index.js`)已导出
75
+ - `lib/app.mjs`
76
+ - `lib/argument.mjs`(`$argument` 参数标准化模块,导入时自动执行)
77
+ - `lib/done.mjs`
78
+ - `lib/notification.mjs`
79
+ - `lib/time.mjs`
80
+ - `lib/wait.mjs`
81
+ - `polyfill/Console.mjs`
82
+ - `polyfill/fetch.mjs`
83
+ - `polyfill/Lodash.mjs`
84
+ - `polyfill/StatusTexts.mjs`
85
+ - `polyfill/Storage.mjs`
86
+
87
+ ### 仓库中存在但未从主入口导出
88
+ - `lib/environment.mjs`
89
+ - `lib/runScript.mjs`
90
+ - `getStorage.mjs`(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用)
91
+
92
+ ## 模块依赖关系
93
+
94
+ 说明:
95
+ - 下表只描述“模块之间”的依赖关系、调用到的函数/常量、以及依赖原因。
96
+ - 你在业务脚本中通常只需要调用对外 API;底层跨平台差异已在这些依赖链里处理。
97
+
98
+ | 模块 | 依赖模块 | 使用的函数/常量 | 为什么依赖 |
99
+ | --- | --- | --- | --- |
100
+ | `lib/app.mjs` | 无 | 无 | 核心平台识别源头,供其他差异模块分流 |
101
+ | `lib/environment.mjs` | `lib/app.mjs` | `$app` | 按平台生成统一 `$environment`(尤其补齐 `app` 字段) |
102
+ | `lib/argument.mjs` | `polyfill/Console.mjs`, `polyfill/Lodash.mjs` | `Console.debug`, `Console.logLevel`, `Lodash.set` | 统一 `$argument` 结构并支持深路径写入 |
103
+ | `lib/done.mjs` | `lib/app.mjs`, `polyfill/Console.mjs`, `polyfill/Lodash.mjs`, `polyfill/StatusTexts.mjs` | `$app`, `Console.log`, `Lodash.set`, `Lodash.pick`, `StatusTexts` | 将各平台 `$done` 参数格式拉平并兼容状态码/策略字段 |
104
+ | `lib/notification.mjs` | `lib/app.mjs`, `polyfill/Console.mjs` | `$app`, `Console.group`, `Console.log`, `Console.groupEnd`, `Console.error` | 将通知参数映射到各平台通知接口并统一日志输出 |
105
+ | `lib/runScript.mjs` | `polyfill/Console.mjs`, `polyfill/fetch.mjs`, `polyfill/Storage.mjs`, `polyfill/Lodash.mjs` | `Console.error`, `fetch`, `Storage.getItem`(`Lodash` 当前版本未实际调用) | 读取 BoxJS 配置并发起统一 HTTP 调用执行脚本 |
106
+ | `getStorage.mjs` | `lib/argument.mjs`, `polyfill/Console.mjs`, `polyfill/Lodash.mjs`, `polyfill/Storage.mjs` | `Console.debug`, `Console.logLevel`, `Lodash.merge`, `Storage.getItem` | 先标准化 `$argument`,再合并默认配置/持久化配置/运行参数 |
107
+ | `polyfill/Console.mjs` | `lib/app.mjs` | `$app` | 日志在 Node.js 与 iOS 脚本环境使用不同错误输出策略 |
108
+ | `polyfill/fetch.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs`, `polyfill/StatusTexts.mjs`, `polyfill/Console.mjs` | `$app`, `Lodash.set`, `StatusTexts`(`Console` 当前版本未实际调用) | 按平台选请求引擎并做参数映射、响应结构统一 |
109
+ | `polyfill/Storage.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs` | `$app`, `Lodash.get`, `Lodash.set`, `Lodash.unset` | 按平台选持久化后端并支持 `@key.path` 读写 |
110
+ | `polyfill/Lodash.mjs` | 无 | 无 | 提供路径/合并等基础能力,被多个模块复用 |
111
+ | `polyfill/StatusTexts.mjs` | 无 | 无 | 提供 HTTP 状态文案,供 `fetch/done` 使用 |
112
+ | `index.js` / `lib/index.js` / `polyfill/index.js` | 多个模块 | `export *` | 聚合导出,不含业务逻辑 |
113
+
114
+ ## API 参考(按 mjs 文件)
115
+
116
+ ### `lib/app.mjs` 与 `lib/environment.mjs`(平台识别与环境)
117
+
118
+ #### `$app`
119
+ - 类型:`"Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash" | undefined`
120
+ - 角色:核心模块。库内所有存在平台行为差异的模块都会先读取 `$app` 再分流(如 `done`、`notification`、`fetch`、`Storage`、`Console`、`environment`)。
121
+ - 读取方式:
122
+
123
+ ```js
124
+ import { $app } from "@nsnanocat/util";
125
+ const appName = $app; // 读取 $app,返回平台字符串
126
+ console.log(appName);
127
+ ```
128
+
129
+ - 识别顺序(`lib/app.mjs`):
130
+ 1. 存在 `$task` -> `Quantumult X`
131
+ 2. 存在 `$loon` -> `Loon`
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`
137
+
138
+ #### `$environment` / `environment()`
139
+ - 路径:`lib/environment.mjs`(未从包主入口导出)
140
+ - 签名:`environment(): object`
141
+ - 调用方式:
142
+
143
+ ```js
144
+ import { $environment, environment } from "@nsnanocat/util/lib/environment.mjs";
145
+ console.log($environment.app); // 统一平台名
146
+ console.log(environment()); // 当前环境对象
147
+ ```
148
+
149
+ - 规则:会为已识别平台统一生成 `$environment.app = "平台名称"`。
150
+
151
+ | 平台 | 调用路径(读取来源) | 读取结果示例 |
152
+ | --- | --- | --- |
153
+ | Surge | 读取全局 `$environment`,再写入 `app` | `{ ..., "surge-version": "x", app: "Surge" }` |
154
+ | Stash | 读取全局 `$environment`,再写入 `app` | `{ ..., "stash-version": "x", app: "Stash" }` |
155
+ | Egern | 读取全局 `$environment`,再写入 `app` | `{ ..., app: "Egern" }` |
156
+ | Loon | 读取全局 `$loon` 字符串并拆分 | `{ device, ios, "loon-version", app: "Loon" }` |
157
+ | Quantumult X | 不读取额外环境字段,直接构造对象 | `{ app: "Quantumult X" }` |
158
+ | Node.js | 读取 `process.env` 并写入 `process.env.app` | `{ ..., app: "Node.js" }` |
159
+ | 其他 | 无 | `{}` |
160
+
161
+ ### `lib/argument.mjs`(`$argument` 参数标准化模块)
162
+
163
+ 此文件无显式导出;`import` 后立即执行。这是为了统一各平台 `$argument` 的输入差异。
164
+
165
+ #### 行为
166
+ - 通过包入口导入(`import ... from "@nsnanocat/util"`)时会自动执行本模块。
167
+ - JSCore 环境不支持 `await import`,请使用静态导入或直接走包入口导入。
168
+ - 读取到的 `$argument` 会按 URL Params 样式格式化为对象,并支持深路径。
169
+ - 你也可以通过 `import { $argument } from "@nsnanocat/util"` 读取当前已标准化的 `$argument` 快照。
170
+ - 平台输入差异说明:
171
+ - Surge / Stash / Egern:脚本参数通常以字符串形式传入(如 `a=1&b=2`)。
172
+ - Loon:支持字符串和对象两种 `$argument` 形式。
173
+ - Quantumult X / Shadowrocket:不提供 `$argument`。
174
+ - 当全局 `$argument` 为 `string`(如 `"a.b=1&x=2"`)时:
175
+ - 按 `&` / `=` 切分。
176
+ - 去掉值中的双引号。
177
+ - 使用点路径展开对象(`a.b=1 -> { a: { b: "1" } }`)。
178
+ - 当全局 `$argument` 为 `object` 时:
179
+ - 将 key 当路径写回新对象(`{"a.b":"1"}` -> `{a:{b:"1"}}`)。
180
+ - 当 `$argument` 为 `undefined`:不处理。
181
+ - 若 `$argument.LogLevel` 存在:同步到 `Console.logLevel`。
182
+
183
+ #### 用法
184
+ ```js
185
+ import { $argument } from "@nsnanocat/util";
186
+
187
+ // $argument = "mode=on&a.b=1"; // 示例入参,实际由模块参数注入
188
+ console.log($argument); // { mode: "on", a: { b: "1" } }
189
+ ```
190
+
191
+ ### `lib/done.mjs`
192
+
193
+ #### `done(object = {})`
194
+ - 签名:`done(object?: object): void`
195
+ - 作用:统一不同平台的脚本结束接口(`$done` / Node 退出)。
196
+
197
+ 说明:下表描述的是各 App 原生接口差异与本库内部映射逻辑。调用方只需要按 `done` 的统一参数传值即可,不需要自己再写平台分支。
198
+
199
+ 支持字段(输入):
200
+ - `status`: `number | string`
201
+ - `url`: `string`
202
+ - `headers`: `object`
203
+ - `body`: `string | ArrayBuffer | TypedArray`
204
+ - `bodyBytes`: `ArrayBuffer`
205
+ - `policy`: `string`
206
+
207
+ 平台行为差异:
208
+
209
+ | 平台 | `policy` 处理 | `status` 处理 | `body/bodyBytes` 处理 | 最终行为 |
210
+ | --- | --- | --- | --- | --- |
211
+ | Surge | 写入 `headers.X-Surge-Policy` | 透传 | 透传 | `$done(object)` |
212
+ | Loon | `object.node = policy` | 透传 | 透传 | `$done(object)` |
213
+ | Stash | 写入 `headers.X-Stash-Selected-Proxy`(URL 编码) | 透传 | 透传 | `$done(object)` |
214
+ | Egern | 不转换 | 透传 | 透传 | `$done(object)` |
215
+ | Shadowrocket | 不转换 | 透传 | 透传 | `$done(object)` |
216
+ | Quantumult X | 写入 `opts.policy` | `number` 会转 `HTTP/1.1 200 OK` 字符串 | 仅保留 `status/url/headers/body/bodyBytes`;`ArrayBuffer/TypedArray` 转 `bodyBytes` | `$done(object)` |
217
+ | Node.js | 不适用 | 不适用 | 不适用 | `process.exit(1)` |
218
+
219
+ 不可用/差异点:
220
+ - `policy` 在 Egern / Shadowrocket 分支不做映射。
221
+ - Quantumult X 会丢弃未在白名单内的字段。
222
+ - Quantumult X 的 `status` 在部分场景要求完整状态行(如 `HTTP/1.1 200 OK`),本库会在传入数字状态码时自动拼接(依赖 `StatusTexts`)。
223
+ - Node.js 不调用 `$done`,而是直接退出进程,且退出码固定为 `1`。
224
+
225
+ ### `lib/notification.mjs`
226
+
227
+ #### `notification(title, subtitle, body, content)`
228
+ - 签名:
229
+ - `title?: string`
230
+ - `subtitle?: string`
231
+ - `body?: string`
232
+ - `content?: string | number | boolean | object`
233
+ - 默认值:`title = "ℹ️ ${$app} 通知"`
234
+ - 作用:统一 `notify/notification` 参数格式并发送通知。
235
+
236
+ `content` 可用 key(对象形式):
237
+ - 跳转:`open` / `open-url` / `url` / `openUrl`
238
+ - 复制:`copy` / `update-pasteboard` / `updatePasteboard`
239
+ - 媒体:`media` / `media-url` / `mediaUrl`
240
+ - 其他:`auto-dismiss`、`sound`、`mime`
241
+
242
+ 平台映射:
243
+
244
+ | 平台 | 调用接口 | 字符串 `content` 行为 | 对象字段支持 |
245
+ | --- | --- | --- | --- |
246
+ | Surge | `$notification.post` | `{ url: content }` | `open-url`/`clipboard` 动作、`media-url`、`media-base64`、`auto-dismiss`、`sound` |
247
+ | Stash | `$notification.post` | `{ url: content }` | 同 Surge 分支(是否全部展示取决于 Stash 支持) |
248
+ | Egern | `$notification.post` | `{ url: content }` | 同 Surge 分支(是否全部展示取决于 Egern 支持) |
249
+ | Shadowrocket | `$notification.post` | `{ openUrl: content }` | 走 Surge 分支的 action/url/text/media 字段 |
250
+ | Loon | `$notification.post` | `{ openUrl: content }` | `openUrl`、`mediaUrl`(仅 http/https) |
251
+ | Quantumult X | `$notify` | `{ "open-url": content }` | `open-url`、`media-url`(仅 http/https)、`update-pasteboard` |
252
+ | Node.js | 不发送通知(非 iOS App 环境) | 无 | 无 |
253
+
254
+ 不可用/差异点:
255
+ - `copy/update-pasteboard` 在 Loon 分支不会生效。
256
+ - Loon / Quantumult X 对 `media` 仅接受网络 URL;Base64 媒体不会自动映射。
257
+ - Node.js 不是 iOS App 脚本环境,不支持 iOS 通知行为;当前分支仅日志输出。
258
+
259
+ ### `lib/time.mjs`
260
+
261
+ #### `time(format, ts)`
262
+ - 签名:`time(format: string, ts?: number): string`
263
+ - `ts`:可选时间戳(传给 `new Date(ts)`)。
264
+ - 支持占位符:`YY`、`yyyy`、`MM`、`dd`、`HH`、`mm`、`ss`、`sss`、`S`(季度)。
265
+
266
+ ```js
267
+ time("yyyy-MM-dd HH:mm:ss.sss");
268
+ time("yyyyMMddHHmmss", Date.now());
269
+ ```
270
+
271
+ 注意:当前实现对每个 token 只替换一次(`String.replace` 非全局)。
272
+
273
+ ### `lib/wait.mjs`
274
+
275
+ #### `wait(delay = 1000)`
276
+ - 签名:`wait(delay?: number): Promise<void>`
277
+ - 用法:
278
+
279
+ ```js
280
+ await wait(500);
281
+ ```
282
+
283
+ ### `lib/runScript.mjs`(未主入口导出)
284
+
285
+ #### `runScript(script, runOpts)`
286
+ - 签名:`runScript(script: string, runOpts?: { timeout?: number }): Promise<void>`
287
+ - 作用:通过 BoxJS `httpapi` 调用本地脚本执行接口:`/v1/scripting/evaluate`。
288
+ - 读取存储键:
289
+ - `@chavy_boxjs_userCfgs.httpapi`
290
+ - `@chavy_boxjs_userCfgs.httpapi_timeout`
291
+ - 请求体:
292
+ - `script_text`
293
+ - `mock_type: "cron"`
294
+ - `timeout`
295
+
296
+ 示例:
297
+ ```js
298
+ import { runScript } from "./lib/runScript.mjs";
299
+ await runScript("$done({})", { timeout: 20 });
300
+ ```
301
+
302
+ 注意:
303
+ - 依赖你本地已正确配置 `httpapi`(`password@host:port`)。
304
+ - 函数不返回接口响应,仅在失败时 `Console.error`。
305
+
306
+ ### `getStorage.mjs`
307
+
308
+ ⚠️ 注意:该模块主要为薯条项目的存储结构设计,不作为通用默认 API。
309
+ 仅当你的持久化结构与薯条项目一致时才建议使用。
310
+
311
+ #### `getStorage(key, names, database)`
312
+ - 签名:
313
+ - `key: string`(持久化主键)
314
+ - `names: string | string[]`(平台名/配置组名,可嵌套数组)
315
+ - `database: object`(默认数据库)
316
+ - 返回:`{ Settings, Configs, Caches }`
317
+
318
+ 合并顺序:
319
+ 1. `database.Default` -> 初始 `Store`
320
+ 2. 持久化中的 BoxJS 值(`Storage.getItem(key)`)
321
+ 3. 按 `names` 合并 `database[name]` + `BoxJs[name]`
322
+ 4. 最后合并 `$argument`
323
+
324
+ 自动类型转换(`Store.Settings`):
325
+ - 字符串 `"true"/"false"` -> `boolean`
326
+ - 纯数字字符串 -> `number`
327
+ - 含逗号字符串 -> `array`,并尝试逐项转数字
328
+
329
+ 示例:
330
+ ```js
331
+ import { getStorage } from "@nsnanocat/util/getStorage.mjs";
332
+
333
+ const store = getStorage("@my_box", ["YouTube", "Global"], database);
334
+ ```
335
+
336
+ ### `polyfill/fetch.mjs`
337
+
338
+ `fetch` 是仿照 Web API `Window.fetch` 设计的跨平台适配实现:
339
+ - 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
340
+ - 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
341
+ - 目标:尽量保持 Web `fetch` 调用习惯,同时补齐各平台扩展参数映射
342
+
343
+ #### `fetch(resource, options = {})`
344
+ - 签名:`fetch(resource: object | string, options?: object): Promise<object>`
345
+ - 参数合并:
346
+ - `resource` 为对象:`{ ...options, ...resource }`
347
+ - `resource` 为字符串:`{ ...options, url: resource }`
348
+ - 默认方法:无 `method` 时,若有 `body/bodyBytes` -> `POST`,否则 `GET`
349
+ - 会删除 headers:`Host`、`:authority`、`Content-Length/content-length`
350
+ - `timeout` 规则:
351
+ - 缺省 -> `5`(秒)
352
+ - `> 500` 视为毫秒并转为秒
353
+
354
+ 通用请求字段:
355
+ - `url`
356
+ - `method`
357
+ - `headers`
358
+ - `body`
359
+ - `bodyBytes`
360
+ - `timeout`
361
+ - `policy`
362
+ - `redirection` / `auto-redirect`
363
+
364
+ 说明:下表是各 App 原生 HTTP 接口的差异补充,以及本库 `fetch` 的内部映射方式。调用方使用统一入参即可。
365
+
366
+ 平台行为差异:
367
+
368
+ | 平台 | 请求发送接口 | `timeout` 单位 | `policy` 映射 | 重定向字段 | 二进制处理 |
369
+ | --- | --- | --- | --- | --- | --- |
370
+ | Surge | `$httpClient[method]` | 秒 | 无专门映射 | `auto-redirect` | `Accept` 命中二进制类型时设置 `binary-mode` |
371
+ | Loon | `$httpClient[method]` | 毫秒(内部乘 1000) | `node = policy` | `auto-redirect` | 同上 |
372
+ | Stash | `$httpClient[method]` | 秒 | `headers.X-Stash-Selected-Proxy` | `auto-redirect` | 同上 |
373
+ | Egern | `$httpClient[method]` | 秒 | 无专门映射 | `auto-redirect` | 同上 |
374
+ | Shadowrocket | `$httpClient[method]` | 秒 | `headers.X-Surge-Proxy` | `auto-redirect` | 同上 |
375
+ | Quantumult X | `$task.fetch` | 毫秒(内部乘 1000) | `opts.policy` | `opts.redirection` | `body(ArrayBuffer/TypedArray)` 转 `bodyBytes`;响应按 `Content-Type` 恢复到 `body` |
376
+ | Node.js | `fetch` + `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
377
+
378
+ 返回对象(统一后)常见字段:
379
+ - `ok`
380
+ - `status`
381
+ - `statusCode`
382
+ - `statusText`
383
+ - `headers`
384
+ - `body`
385
+ - `bodyBytes`
386
+
387
+ 不可用/差异点:
388
+ - `policy` 在 Surge / Egern / Node.js 分支没有额外适配逻辑。
389
+ - `redirection` 在部分平台会映射为 `auto-redirect` 或 `opts.redirection`。
390
+ - Node.js 分支依赖 `globalThis.fetch` / `globalThis.fetchCookie` 或 `node-fetch` + `fetch-cookie`。
391
+ - 返回结构是统一兼容结构,不等同于浏览器 `Response` 对象。
392
+
393
+ ### `polyfill/Storage.mjs`
394
+
395
+ `Storage` 是仿照 Web Storage 接口(`Storage`)设计的跨平台持久化适配器:
396
+ - 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Storage
397
+ - 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Storage
398
+ - 目标:统一 VPN App 脚本环境中的持久化读写接口,并尽量贴近 Web Storage 行为
399
+
400
+ #### `Storage.getItem(keyName, defaultValue = null)`
401
+ - 支持普通 key:按平台读持久化。
402
+ - 支持路径 key:`@root.path.to.key`。
403
+
404
+ #### `Storage.setItem(keyName, keyValue)`
405
+ - 普通 key:按平台写持久化。
406
+ - 路径 key:`@root.path` 写入嵌套对象。
407
+ - `keyValue` 为对象时自动 `JSON.stringify`。
408
+
409
+ #### `Storage.removeItem(keyName)`
410
+ - Quantumult X:可用(`$prefs.removeValueForKey`)。
411
+ - Surge / Loon / Stash / Egern / Shadowrocket / Node.js:返回 `false`。
412
+
413
+ #### `Storage.clear()`
414
+ - Quantumult X:可用(`$prefs.removeAllValues`)。
415
+ - 其他平台:返回 `false`。
416
+
417
+ #### Node.js 特性
418
+ - 数据文件默认:`box.dat`。
419
+ - 读取路径优先级:当前目录 -> `process.cwd()`。
420
+
421
+ 与 Web Storage 的行为差异:
422
+ - 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。
423
+ - `removeItem/clear` 仅部分平台可用(目前主要是 Quantumult X)。
424
+ - `getItem` 会尝试 `JSON.parse`,`setItem` 写入对象会 `JSON.stringify`。
425
+
426
+ 平台后端映射:
427
+
428
+ | 平台 | 读写接口 |
429
+ | --- | --- |
430
+ | Surge / Loon / Stash / Egern / Shadowrocket | `$persistentStore.read/write` |
431
+ | Quantumult X | `$prefs.valueForKey/setValueForKey` |
432
+ | Node.js | 本地 `box.dat` |
433
+
434
+ ### `polyfill/Console.mjs`
435
+
436
+ `Console` 是统一日志工具(静态类)。
437
+
438
+ #### 日志级别
439
+ - `Console.logLevel` 可读写。
440
+ - 支持:`OFF(0)` / `ERROR(1)` / `WARN(2)` / `INFO(3)` / `DEBUG(4)` / `ALL(5)`。
441
+
442
+ `logLevel` 用法示例:
443
+
444
+ ```js
445
+ import { Console } from "@nsnanocat/util";
446
+
447
+ Console.logLevel = "debug"; // 或 4
448
+ Console.debug("debug message");
449
+
450
+ Console.logLevel = 2; // WARN
451
+ Console.info("won't print at WARN level");
452
+ Console.warn("will print");
453
+
454
+ console.log(Console.logLevel); // "WARN"
455
+ ```
456
+
457
+ #### 方法
458
+ - `clear()`
459
+ - `count(label = "default")`
460
+ - `countReset(label = "default")`
461
+ - `debug(...msg)`
462
+ - `error(...msg)`
463
+ - `exception(...msg)`
464
+ - `group(label)`
465
+ - `groupEnd()`
466
+ - `info(...msg)`
467
+ - `log(...msg)`
468
+ - `time(label = "default")`
469
+ - `timeLog(label = "default")`
470
+ - `timeEnd(label = "default")`
471
+ - `warn(...msg)`
472
+
473
+ 参数与返回值:
474
+
475
+ | 方法 | 参数 | 返回值 | 说明 |
476
+ | --- | --- | --- | --- |
477
+ | `clear()` | 无 | `void` | 当前实现为空函数 |
478
+ | `count(label)` | `label?: string` | `void` | 计数并输出 |
479
+ | `countReset(label)` | `label?: string` | `void` | 重置计数器 |
480
+ | `debug(...msg)` | `...msg: any[]` | `void` | 仅 `DEBUG/ALL` 级别输出 |
481
+ | `error(...msg)` | `...msg: any[]` | `void` | Node.js 优先输出 `stack` |
482
+ | `exception(...msg)` | `...msg: any[]` | `void` | `error` 别名 |
483
+ | `group(label)` | `label: string` | `void` | 压栈分组 |
484
+ | `groupEnd()` | 无 | `void` | 出栈分组 |
485
+ | `info(...msg)` | `...msg: any[]` | `void` | `INFO` 及以上 |
486
+ | `log(...msg)` | `...msg: any[]` | `void` | 通用日志 |
487
+ | `time(label)` | `label?: string` | `void` | 记录起始时间 |
488
+ | `timeLog(label)` | `label?: string` | `void` | 输出耗时 |
489
+ | `timeEnd(label)` | `label?: string` | `void` | 清除计时器 |
490
+ | `warn(...msg)` | `...msg: any[]` | `void` | `WARN` 及以上 |
491
+
492
+ 平台差异:
493
+ - Node.js 下 `error` 会优先打印 `Error.stack`。
494
+ - 其他平台统一加前缀符号输出(`❌/⚠️/ℹ️/🅱️`)。
495
+
496
+ ### `polyfill/Lodash.mjs`
497
+
498
+ `Lodash` 为“部分方法的简化实现”,不是完整 Lodash。各方法语义可参考:
499
+ - https://www.lodashjs.com
500
+ - https://lodash.com
501
+
502
+ 当前实现包含:
503
+ - `escape(string)`
504
+ - `unescape(string)`
505
+ - `toPath(value)`
506
+ - `get(object, path, defaultValue)`
507
+ - `set(object, path, value)`
508
+ - `unset(object, path)`
509
+ - `pick(object, paths)`
510
+ - `omit(object, paths)`
511
+ - `merge(object, ...sources)`
512
+
513
+ 参数与返回值:
514
+
515
+ | 方法 | 参数 | 返回值 | 说明 |
516
+ | --- | --- | --- | --- |
517
+ | `escape` | `string: string` | `string` | HTML 转义 |
518
+ | `unescape` | `string: string` | `string` | HTML 反转义 |
519
+ | `toPath` | `value: string` | `string[]` | `a[0].b` -> `['a','0','b']` |
520
+ | `get` | `object?: object, path?: string\\|string[], defaultValue?: any` | `any` | 路径读取 |
521
+ | `set` | `object: object, path: string\\|string[], value: any` | `object` | 路径写入(会创建中间层) |
522
+ | `unset` | `object?: object, path?: string\\|string[]` | `boolean` | 删除路径并返回结果 |
523
+ | `pick` | `object?: object, paths?: string\\|string[]` | `object` | 挑选 key(仅第一层) |
524
+ | `omit` | `object?: object, paths?: string\\|string[]` | `object` | 删除 key(会修改原对象) |
525
+ | `merge` | `object: object, ...sources: object[]` | `object` | 深合并(非完整 lodash 行为) |
526
+
527
+ `merge` 行为(与 lodash 官方有差异):
528
+ - 深度合并 Plain Object。
529
+ - Array 直接覆盖;空数组不覆盖已存在值。
530
+ - Map/Set 支持同类型合并;空 Map/Set 不覆盖已存在值。
531
+ - `undefined` 不覆盖,`null` 会覆盖。
532
+ - 直接修改目标对象(mutates target)。
533
+
534
+ ### `polyfill/StatusTexts.mjs`
535
+
536
+ #### `StatusTexts`
537
+ - 类型:`Record<number, string>`
538
+ - 内容:HTTP 状态码到状态文本映射(100~511 的常见码)。
539
+ - 主要用途:给 Quantumult X 的 `$done` 状态行补全文本(如 `HTTP/1.1 200 OK`)。
540
+ - 参考示例:https://github.com/crossutility/Quantumult-X/raw/refs/heads/master/sample-rewrite-response-header.js
541
+
542
+ ## 平台差异总览
543
+
544
+ 说明:本节展示的是各平台原生脚本接口差异。实际在本库中,这些差异已由 `done`、`fetch`、`notification`、`Storage` 等模块做了统一适配。
545
+
546
+ | 能力 | Quantumult X | Loon | Surge | Stash | Egern | Shadowrocket | Node.js |
547
+ | --- | --- | --- | --- | --- | --- | --- | --- |
548
+ | HTTP 请求 | `$task.fetch` | `$httpClient` | `$httpClient` | `$httpClient` | `$httpClient` | `$httpClient` | `fetch` |
549
+ | 通知 | `$notify` | `$notification.post` | `$notification.post` | `$notification.post` | `$notification.post` | `$notification.post` | 无 |
550
+ | 持久化 | `$prefs` | `$persistentStore` | `$persistentStore` | `$persistentStore` | `$persistentStore` | `$persistentStore` | `box.dat` |
551
+ | 结束脚本 | `$done` | `$done` | `$done` | `$done` | `$done` | `$done` | `process.exit(1)` |
552
+ | `removeItem/clear` | 可用 | 不可用 | 不可用 | 不可用 | 不可用 | 不可用 | 不可用 |
553
+ | `policy` 注入(`fetch/done`) | `opts.policy` | `node` | `X-Surge-Policy`(done) | `X-Stash-Selected-Proxy` | 无专门映射 | `X-Surge-Proxy`(fetch) | 无 |
554
+
555
+ ## 已知限制与注意事项
556
+
557
+ - `lib/argument.mjs` 为 `$argument` 标准化模块,`import` 时会按规则重写全局 `$argument`。
558
+ - `lib/done.mjs` 在 Node.js 固定 `process.exit(1)`。
559
+ - `polyfill/fetch.mjs` 的超时保护使用了 `Promise.race`,但当前实现里请求 Promise 先被 `await`,可能导致超时行为与预期不完全一致。
560
+ - `Storage.removeItem("@a.b")` 分支存在未声明变量写入风险;如要大量使用路径删除,建议先本地验证。
561
+ - `lib/runScript.mjs` 未从包主入口导出,需要按文件路径直接导入。
562
+
563
+ ## 参考资料
564
+
565
+ 以下资料用于对齐不同平台 `$` API 语义;README 的“平台差异”优先以本仓库实现为准。
566
+
567
+ ### Surge
568
+ - [Surge Manual - Scripting API](https://manual.nssurge.com/scripting/common.html)
569
+ - [Surge Manual - HTTP Client API](https://manual.nssurge.com/scripting/http-client.html)
570
+
571
+ ### Stash
572
+ - [Stash Docs - Scripting Overview](https://stash.wiki/scripting/overview/)
573
+ - [Stash Docs - API](https://stash.wiki/scripting/apis/)
574
+ - [Stash Docs - Rewrite Script](https://stash.wiki/scripting/rewrite-script/)
575
+
576
+ ### Loon
577
+ - [Loon Script](https://nsloon.app/Loon-Script)
578
+ - [Loon API](https://nsloon.app/Loon-API)
579
+
580
+ ### Quantumult X
581
+ - [crossutility/Quantumult-X - sample-task.js](https://raw.githubusercontent.com/crossutility/Quantumult-X/master/sample-task.js)
582
+ - [crossutility/Quantumult-X - sample-rewrite-with-script.js](https://raw.githubusercontent.com/crossutility/Quantumult-X/master/sample-rewrite-with-script.js)
583
+ - [crossutility/Quantumult-X - sample-fetch-opts-policy.js](https://raw.githubusercontent.com/crossutility/Quantumult-X/master/sample-fetch-opts-policy.js)
584
+ - [crossutility/Quantumult-X - sample-rewrite-response-header.js](https://github.com/crossutility/Quantumult-X/raw/refs/heads/master/sample-rewrite-response-header.js)
585
+
586
+ ### Node.js
587
+ - [Node.js Globals - fetch](https://nodejs.org/api/globals.html#fetch)
588
+
589
+ ### Web API / Lodash
590
+ - [MDN - Window.fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch)
591
+ - [MDN(中文)- Window.fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch)
592
+ - [MDN - Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage)
593
+ - [MDN(中文)- Storage](https://developer.mozilla.org/zh-CN/docs/Web/API/Storage)
594
+ - [Lodash Docs](https://www.lodashjs.com)
595
+ - [lodash.com](https://lodash.com)
596
+
597
+ ### Egern / Shadowrocket
598
+ - [Egern Docs - Scriptings 配置](https://egernapp.com/docs/configuration/scriptings)
599
+ - [Shadowrocket 官方站点](https://www.shadowlaunch.com/)
600
+
601
+ > 说明:Egern 与 Shadowrocket 暂未检索到等价于 Surge/Loon/Stash 的完整公开脚本 API 页面;相关差异说明以本库实际代码分支行为为准。