UNPKG

@nsnanocat/util

Version:

Pure JS's util module for well-known iOS network tools

747 lines (603 loc) 34.1 kB
# @nsnanocat/util 用于统一 Quantumult X / Loon / Shadowrocket / Worker / Node.js / Egern / Surge / Stash 脚本接口的通用工具库。 核心目标: - 统一不同平台的 HTTP、通知、持久化、结束脚本等调用方式。 - 在一个脚本里尽量少写平台分支。 - 提供一组可直接复用的 polyfill(`fetch` / `Storage` / `Console` / `Lodash` / `qs`)。 ## 目录 - [安装与导入](#安装与导入) - [导出清单](#导出清单) - [模块依赖关系](#模块依赖关系) - [API 参考(按 mjs 文件)](#api-参考按-mjs-文件) - [平台差异总览](#平台差异总览) - [已知限制与注意事项](#已知限制与注意事项) - [参考资料](#参考资料) ## 安装与导入 发布源: - npm(推荐):[https://www.npmjs.com/package/@nsnanocat/util](https://www.npmjs.com/package/@nsnanocat/util) - GitHub Packages(同步发布):[https://github.com/NSNanoCat/util/pkgs/npm/util](https://github.com/NSNanoCat/util/pkgs/npm/util) 如果你不确定该选哪个,直接用 npm 源即可。 如果你从 GitHub Packages 安装,需要先配置 GitHub 认证(PAT Token)。 ### 1) 使用 npm 源(推荐,最省事) ```bash # 首次安装:拉取并安装这个包 npm i @nsnanocat/util # 更新到最新版本:升级已安装的 util npm i @nsnanocat/util@latest # 你也可以使用 update(效果类似) # npm update @nsnanocat/util ``` ### 2) 使用 GitHub Packages 源(同步源,需要 GitHub 鉴权) ```bash # 把 @nsnanocat 作用域的包下载源切到 GitHub Packages npm config set @nsnanocat:registry https://npm.pkg.github.com # 配置 GitHub Token(用于下载 GitHub Packages) # 建议把 YOUR_GITHUB_PAT 换成你的真实 Token,再执行 # echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT" >> ~/.npmrc # 首次安装:从 GitHub Packages 安装 util npm i @nsnanocat/util # 更新到最新版本:从 GitHub Packages 拉取最新 util npm i @nsnanocat/util@latest ``` ```js import { $app, // 当前平台名(如 "Surge" / "Loon" / "Quantumult X" / "Worker" / "Node.js") $argument, // 已标准化的模块参数对象(导入包时自动处理字符串 -> 对象) done, // 统一结束脚本函数(内部自动适配各平台 $done 差异) fetch, // 统一 HTTP 请求函数(内部自动适配 $httpClient / $task / fetch) notification, // 统一通知函数(内部自动适配 $notify / $notification.post) time, // 时间格式化工具 wait, // 延时等待工具(Promise) Console, // 统一日志工具(支持 logLevel) Lodash as _, // Lodash 建议按官方示例惯例使用 `_` 作为工具对象别名 qs, // 查询字符串工具(parse / stringify) Storage, // 统一持久化存储接口(适配 $prefs / $persistentStore / 内存 / 文件) } from "@nsnanocat/util"; ``` ## 导出清单 ### 包主入口(`index.js`)已导出 - `lib/app.mjs` - `lib/argument.mjs``$argument` 参数标准化模块,导入时自动执行) - `lib/done.mjs` - `lib/notification.mjs` - `lib/time.mjs` - `lib/wait.mjs` - `polyfill/Console.mjs` - `polyfill/fetch.mjs` - `polyfill/Lodash.mjs` - `polyfill/qs.mjs` - `polyfill/StatusTexts.mjs` - `polyfill/Storage.mjs` ### 仓库中存在但未从主入口导出 - `lib/environment.mjs` - `lib/runScript.mjs` - `getStorage.mjs`(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用;请通过子路径 `@nsnanocat/util/getStorage.mjs` 导入) ## 模块依赖关系 说明: - 下表只描述“模块之间”的依赖关系、调用到的函数/常量、以及依赖原因。 - 你在业务脚本中通常只需要调用对外 API;底层跨平台差异已在这些依赖链里处理。 | 模块 | 依赖模块 | 使用的函数/常量 | 为什么依赖 | | --- | --- | --- | --- | | `lib/app.mjs` | 无 | 无 | 核心平台识别源头,供其他差异模块分流 | | `lib/environment.mjs` | `lib/app.mjs` | `$app` | 按平台生成统一 `$environment`(尤其补齐 `app` 字段) | | `lib/argument.mjs` | `polyfill/Console.mjs`, `polyfill/qs.mjs` | `Console.debug`, `Console.logLevel`, `qs.parse` | 统一 `$argument` 结构,并委托 `qs.parse` 处理字符串/对象/空值输入 | | `lib/done.mjs` | `lib/app.mjs`, `polyfill/Console.mjs`, `polyfill/Lodash.mjs`, `polyfill/StatusTexts.mjs` | `$app`, `Console.log`, `Lodash.set`, `Lodash.pick`, `StatusTexts` | 将各平台 `$done` 参数格式拉平并兼容状态码/策略字段 | | `lib/notification.mjs` | `lib/app.mjs`, `polyfill/Console.mjs` | `$app`, `Console.group`, `Console.log`, `Console.groupEnd`, `Console.error` | 将通知参数映射到各平台通知接口并统一日志输出 | | `lib/runScript.mjs` | `polyfill/Console.mjs`, `polyfill/fetch.mjs`, `polyfill/Storage.mjs`, `polyfill/Lodash.mjs` | `Console.error`, `fetch`, `Storage.getItem``Lodash` 当前版本未实际调用) | 读取 BoxJS 配置并发起统一 HTTP 调用执行脚本 | | `getStorage.mjs` | `lib/argument.mjs`, `polyfill/Console.mjs`, `polyfill/Lodash.mjs`, `polyfill/Storage.mjs` | `Console.debug`, `Console.logLevel`, `Lodash.merge`, `Storage.getItem` | 先标准化 `$argument`,再合并默认配置/持久化配置/运行参数 | | `polyfill/Console.mjs` | `lib/app.mjs` | `$app` | 日志在 Worker / Node.js 与 iOS 脚本环境使用不同错误输出策略 | | `polyfill/fetch.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs`, `polyfill/StatusTexts.mjs`, `polyfill/Console.mjs` | `$app`, `Lodash.set`, `StatusTexts``Console` 当前版本未实际调用) | 按平台选请求引擎并做参数映射、响应结构统一 | | `polyfill/Storage.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs` | `$app`, `Lodash.get`, `Lodash.set`, `Lodash.unset` | 按平台选持久化后端并支持 `@key.path` 读写 | | `polyfill/Lodash.mjs` | 无 | 无 | 提供路径/合并等基础能力,被多个模块复用 | | `polyfill/qs.mjs` | `polyfill/Lodash.mjs` | `Lodash.get`, `Lodash.set`, `Lodash.toPath` | 提供查询字符串与对象之间的解析/序列化能力 | | `polyfill/StatusTexts.mjs` | 无 | 无 | 提供 HTTP 状态文案,供 `fetch/done` 使用 | | `index.js` / `lib/index.js` / `polyfill/index.js` | 多个模块 | `export *` | 聚合导出,不含业务逻辑 | ## API 参考(按 mjs 文件) ### `lib/app.mjs` 与 `lib/environment.mjs`(平台识别与环境) #### `$app` - 类型:`"Quantumult X" | "Loon" | "Shadowrocket" | "Egern" | "Surge" | "Stash" | "Worker" | "Node.js" | undefined` - 角色:核心模块。库内所有存在平台行为差异的模块都会先读取 `$app` 再分流(如 `done``notification``fetch``Storage``Console``environment`)。 - 读取方式: ```js import { $app } from "@nsnanocat/util"; const appName = $app; // 读取 $app,返回平台字符串 console.log(appName); ``` - 识别顺序(`lib/app.mjs`): 1. 存在 `$task` -> `Quantumult X` 2. 存在 `$loon` -> `Loon` 3. 存在 `$rocket` -> `Shadowrocket` 4. 存在 `Egern` -> `Egern` 5. 存在 `$environment` 且有 `surge-version` -> `Surge` 6. 存在 `$environment` 且有 `stash-version` -> `Stash` 7. 存在 `Cloudflare` -> `Worker` 8. 存在 `process.versions.node` -> `Node.js` 9. 默认回落 -> `undefined` - 实现细节:内部使用 `'key' in globalThis` 检测平台标记,避免 `Object.keys(globalThis)` 漏掉不可枚举全局变量;当前 Worker 识别以 `Cloudflare` 全局标记为准。 #### `$environment` / `environment()` - 路径:`lib/environment.mjs`(未从包主入口导出) - 签名:`environment(): object` - 调用方式: ```js import { $environment, environment } from "@nsnanocat/util/lib/environment.mjs"; console.log($environment.app); // 统一平台名 console.log(environment()); // 当前环境对象 ``` - 规则:会为已识别平台统一生成 `$environment.app = "平台名称"`。 | 平台 | 调用路径(读取来源) | 读取结果示例 | | --- | --- | --- | | Surge | 读取全局 `$environment`,再写入 `app` | `{ ..., "surge-version": "x", app: "Surge" }` | | Stash | 读取全局 `$environment`,再写入 `app` | `{ ..., "stash-version": "x", app: "Stash" }` | | Egern | 读取全局 `$environment`,再写入 `app` | `{ ..., app: "Egern" }` | | Loon | 读取全局 `$loon` 字符串并拆分 | `{ device, ios, "loon-version", app: "Loon" }` | | Quantumult X | 不读取额外环境字段,直接构造对象 | `{ app: "Quantumult X" }` | | Worker | 直接构造对象 | `{ app: "Worker" }` | | Node.js | 读取 `process.env` 并写入 `process.env.app` | `{ ..., app: "Node.js" }` | | 其他 | 无 | `{}` | ### `lib/argument.mjs`(`$argument` 参数标准化模块) 此文件无显式导出;`import` 后立即执行。这是为了统一各平台 `$argument` 的输入差异。 #### 行为 - 通过包入口导入(`import ... from "@nsnanocat/util"`)时会自动执行本模块。 - JSCore 环境不支持 `await import`,请使用静态导入或直接走包入口导入。 - 读取到的 `$argument` 会按 URL Params 样式格式化为对象,并支持深路径。 - 内部实现统一委托给 `qs.parse(globalThis.$argument)`- 你也可以通过 `import { $argument } from "@nsnanocat/util"` 读取当前已标准化的 `$argument` 快照。 - 平台输入差异说明: - Surge / Stash / Egern:脚本参数通常以字符串形式传入(如 `a=1&b=2`)。 - Loon:支持字符串和对象两种 `$argument` 形式。 - Quantumult X / Shadowrocket:不提供 `$argument`- 当全局 `$argument``string`(如 `"a.b=1&x=2"`)时: -`&` / `=` 切分。 - 去掉值中的双引号。 - 使用点路径展开对象(`a.b=1 -> { a: { b: "1" } }`)。 - 当全局 `$argument``object` 时: - 将 key 当路径写回新对象(`{"a.b":"1"}` -> `{a:{b:"1"}}`)。 -`$argument``null``undefined`:会归一化为 `{}`-`$argument.LogLevel` 存在:同步到 `Console.logLevel`#### 用法 ```js import { $argument } from "@nsnanocat/util"; // $argument = "mode=on&a.b=1"; // 示例入参,实际由模块参数注入 console.log($argument); // { mode: "on", a: { b: "1" } } ``` ### `lib/done.mjs` #### `done(object = {})` - 签名:`done(object?: object): void` - 作用:统一不同平台的脚本结束接口(`$done` / Worker 日志结束 / Node 退出)。 说明:下表描述的是各 App 原生接口差异与本库内部映射逻辑。调用方只需要按 `done` 的统一参数传值即可,不需要自己再写平台分支。 支持字段(输入): - `status`: `number | string` - `url`: `string` - `headers`: `object` - `body`: `string | ArrayBuffer | TypedArray` - `bodyBytes`: `ArrayBuffer` - `policy`: `string` 平台行为差异: | 平台 | `policy` 处理 | `status` 处理 | `body/bodyBytes` 处理 | 最终行为 | | --- | --- | --- | --- | --- | | Surge | 写入 `headers.X-Surge-Policy` | 透传 | 透传 | `$done(object)` | | Loon | `object.node = policy` | 透传 | 透传 | `$done(object)` | | Stash | 写入 `headers.X-Stash-Selected-Proxy`(URL 编码) | 透传 | 透传 | `$done(object)` | | Egern | 不转换 | 透传 | 透传 | `$done(object)` | | Shadowrocket | 不转换 | 透传 | 透传 | `$done(object)` | | Quantumult X | 写入 `opts.policy` | `number` 会转 `HTTP/1.1 200 OK` 字符串 | 仅保留 `status/url/headers/body/bodyBytes``ArrayBuffer/TypedArray``bodyBytes` | `$done(object)` | | Worker | 不适用 | 不适用 | 不适用 | 仅记录结束日志 | | Node.js | 不适用 | 不适用 | 不适用 | `process.exit(1)` | 不可用/差异点: - `policy` 在 Egern / Shadowrocket 分支不做映射。 - Quantumult X 会丢弃未在白名单内的字段。 - Quantumult X 的 `status` 在部分场景要求完整状态行(如 `HTTP/1.1 200 OK`),本库会在传入数字状态码时自动拼接(依赖 `StatusTexts`)。 - Worker 不调用 `$done`,仅记录结束日志。 - Node.js 不调用 `$done`,而是直接退出进程,且退出码固定为 `1`- 未识别平台(`$app === undefined`)只记录结束日志,不会尝试调用 `$done` 或退出进程。 ### `lib/notification.mjs` #### `notification(title, subtitle, body, content)` - 签名: - `title?: string` - `subtitle?: string` - `body?: string` - `content?: string | number | boolean | object` - 默认值:`title = "ℹ️ ${$app} 通知"` - 作用:统一 `notify/notification` 参数格式并发送通知。 `content` 可用 key(对象形式): - 跳转:`open` / `open-url` / `url` / `openUrl` - 复制:`copy` / `update-pasteboard` / `updatePasteboard` - 媒体:`media` / `media-url` / `mediaUrl` - 其他:`auto-dismiss``sound``mime` 平台映射: | 平台 | 调用接口 | 字符串 `content` 行为 | 对象字段支持 | | --- | --- | --- | --- | | Surge | `$notification.post` | `{ url: content }` | `open-url`/`clipboard` 动作、`media-url``media-base64``auto-dismiss``sound` | | Stash | `$notification.post` | `{ url: content }` | 同 Surge 分支(是否全部展示取决于 Stash 支持) | | Egern | `$notification.post` | `{ url: content }` | 同 Surge 分支(是否全部展示取决于 Egern 支持) | | Shadowrocket | `$notification.post` | `{ openUrl: content }` | 走 Surge 分支的 action/url/text/media 字段 | | Loon | `$notification.post` | `{ openUrl: content }` | `openUrl``mediaUrl`(仅 http/https) | | Quantumult X | `$notify` | `{ "open-url": content }` | `open-url``media-url`(仅 http/https)、`update-pasteboard` | | Worker | 不发送通知(非 iOS App 环境) | 无 | 无 | | Node.js | 不发送通知(非 iOS App 环境) | 无 | 无 | 不可用/差异点: - `copy/update-pasteboard` 在 Loon 分支不会生效。 - Loon / Quantumult X 对 `media` 仅接受网络 URL;Base64 媒体不会自动映射。 - Worker 不是 iOS App 脚本环境,不支持 iOS 通知行为;当前分支仅日志输出。 - Node.js 不是 iOS App 脚本环境,不支持 iOS 通知行为;当前分支仅日志输出。 ### `lib/time.mjs` #### `time(format, ts)` - 签名:`time(format: string, ts?: number): string` - `ts`:可选时间戳(传给 `new Date(ts)`)。 - 支持占位符:`YY``yyyy``MM``dd``HH``mm``ss``sss``S`(季度)。 ```js time("yyyy-MM-dd HH:mm:ss.sss"); time("yyyyMMddHHmmss", Date.now()); ``` 注意:当前实现对每个 token 只替换一次(`String.replace` 非全局)。 ### `lib/wait.mjs` #### `wait(delay = 1000)` - 签名:`wait(delay?: number): Promise<void>` - 用法: ```js await wait(500); ``` ### `lib/runScript.mjs`(未主入口导出) #### `runScript(script, runOpts)` - 签名:`runScript(script: string, runOpts?: { timeout?: number }): Promise<void>` - 作用:通过 BoxJS `httpapi` 调用本地脚本执行接口:`/v1/scripting/evaluate`- 读取存储键: - `@chavy_boxjs_userCfgs.httpapi` - `@chavy_boxjs_userCfgs.httpapi_timeout` - 请求体: - `script_text` - `mock_type: "cron"` - `timeout` 示例: ```js import { runScript } from "./lib/runScript.mjs"; await runScript("$done({})", { timeout: 20 }); ``` 注意: - 依赖你本地已正确配置 `httpapi``password@host:port`)。 - 函数不返回接口响应,仅在失败时 `Console.error`### `getStorage.mjs` ⚠️ 注意:该模块主要为薯条项目的存储结构设计,不作为通用默认 API。 仅当你的持久化结构与薯条项目一致时才建议使用。 #### `getStorage(key, names, database)` - 签名: - `key: string`(持久化主键) - `names: string | string[]`(平台名/配置组名,可嵌套数组) - `database: object`(默认数据库) - 返回:`{ Settings, Configs, Caches }` 合并顺序由 `$argument.Storage` 控制(持久化读取统一使用 `PersistentStore = Storage.getItem(key, {})`;支持别名): - 可用值(大小写敏感):`undefined` | `Argument` | `$argument` | `PersistentStore` | `BoxJs` | `boxjs` | `$persistentStore` | `database` 1. `undefined``database[name]` -> `$argument` -> `PersistentStore[name]` 2. `Argument` / `$argument``database[name]` -> `PersistentStore[name]` -> `$argument` 3. `PersistentStore` / `BoxJs` / `$persistentStore`(默认):`database[name]` -> `PersistentStore[name]` 4. `database`:仅 `database[name]` 注意:`Configs``Caches` 始终按每个 `name` 合并(与 `$argument.Storage` 无关)。 自动类型转换(`Root.Settings`): - 字符串 `"true"/"false"` -> `boolean` - 纯数字字符串 -> `number` - 含逗号字符串 -> `array`,并尝试逐项转数字 示例: ```js import getStorage from "@nsnanocat/util/getStorage.mjs"; const store = getStorage("@my_box", ["YouTube", "Global"], database); ``` #### 命名导出(辅助函数) `getStorage.mjs` 同时导出以下辅助函数: - `traverseObject(o, c)`:深度遍历对象并替换叶子值 - `string2number(string)`:将纯数字字符串转换为数字 - `value2array(value)`:字符串按逗号拆分;数字/布尔值会被包装为单元素数组 示例: ```js import getStorage, { traverseObject, string2number, value2array, } from "@nsnanocat/util/getStorage.mjs"; const store = getStorage("@my_box", ["YouTube", "Global"], database); ``` ### `polyfill/fetch.mjs` `fetch` 是仿照 Web API `Window.fetch` 设计的跨平台适配实现: - 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch - 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch - 目标:尽量保持 Web `fetch` 调用习惯,同时补齐各平台扩展参数映射 #### `fetch(resource, options = {})` - 签名:`fetch(resource: object | string, options?: object): Promise<object>` - 参数合并: - `resource` 为对象:`{ ...options, ...resource }` - `resource` 为字符串:`{ ...options, url: resource }` - 默认方法:无 `method` 时,若有 `body/bodyBytes` -> `POST`,否则 `GET` - 会删除 headers:`Host``:authority``Content-Length/content-length` - `timeout` 规则: - 缺省 -> `5`(秒) - `> 500` 视为毫秒并转为秒 通用请求字段: - `url` - `method` - `headers` - `body` - `bodyBytes` - `timeout` - `policy` - `redirection` / `auto-redirect` - `auto-cookie`(Worker / Node.js 共享分支识别;默认启用,传入 `false` / `0` / `-1` 可关闭) 说明:下表是各 App 原生 HTTP 接口的差异补充,以及本库 `fetch` 的内部映射方式。调用方使用统一入参即可。 平台行为差异: | 平台 | 请求发送接口 | `timeout` 单位 | `policy` 映射 | 重定向字段 | 二进制处理 | | --- | --- | --- | --- | --- | --- | | Surge | `$httpClient[method]` | 秒 | 无专门映射 | `auto-redirect` | `Accept` 命中二进制类型时设置 `binary-mode` | | Loon | `$httpClient[method]` | 毫秒(内部乘 1000) | `node = policy` | `auto-redirect` | 同上 | | Stash | `$httpClient[method]` | 秒 | `headers.X-Stash-Selected-Proxy` | `auto-redirect` | 同上 | | Egern | `$httpClient[method]` | 秒 | 无专门映射 | `auto-redirect` | 同上 | | Shadowrocket | `$httpClient[method]` | 秒 | `headers.X-Surge-Proxy` | `auto-redirect` | 同上 | | Quantumult X | `$task.fetch` | 毫秒(内部乘 1000) | `opts.policy` | `opts.redirection` | `body(ArrayBuffer/TypedArray)``bodyBytes`;响应按 `Content-Type` 恢复到 `body` | | Worker | `globalThis.fetch`(不存在时回退 `node-fetch`);共享 `auto-cookie` 处理 | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) | | Node.js | `globalThis.fetch`(不存在时回退 `node-fetch`);默认按需包裹 `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) | 返回对象(统一后)常见字段: - `ok` - `status` - `statusCode` - `statusText` - `headers` - `body` - `bodyBytes` 不可用/差异点: - `policy` 在 Surge / Egern / Worker / Node.js 分支没有额外适配逻辑。 - `redirection` 在部分平台会映射为 `auto-redirect``opts.redirection`- Worker / Node.js 共享基于 `fetch` 的请求分支;若 `globalThis.fetch` 不存在则回退到 `node-fetch`,并在 `auto-cookie` 未关闭时按需包裹 `fetch-cookie`- 传入 `timeout` 时,`5``5000` 都会被接受;库会先将用户输入归一化,再按平台要求转换为秒或毫秒。 - 返回结构是统一兼容结构,不等同于浏览器 `Response` 对象。 ### `polyfill/Storage.mjs` `Storage` 是仿照 Web Storage 接口(`Storage`)设计的跨平台持久化适配器: - 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Storage - 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Storage - 目标:统一 VPN App 脚本环境中的持久化读写接口,并尽量贴近 Web Storage 行为 #### `Storage.getItem(keyName, defaultValue = null)` - 支持普通 key:按平台读持久化。 - 支持路径 key:`@root.path.to.key`#### `Storage.setItem(keyName, keyValue)` - 普通 key:按平台写持久化。 - 路径 key:`@root.path` 写入嵌套对象。 - `keyValue` 为对象时自动 `JSON.stringify`#### `Storage.removeItem(keyName)` - Quantumult X:可用(`$prefs.removeValueForKey`)。 - Surge:通过 `$persistentStore.write(null, keyName)` 删除。 - Worker:可用(仅删除内存缓存中的对应 key,不持久化)。 - Node.js:可用(删除 `box.dat` 中对应 key 并落盘)。 - Loon / Stash / Egern / Shadowrocket:返回 `false`#### `Storage.clear()` - Quantumult X:可用(`$prefs.removeAllValues`)。 - Worker:可用(仅清空内存缓存,不持久化)。 - Node.js:可用(清空 `box.dat` 并落盘)。 - 其他平台:返回 `false`#### Worker / Node.js 特性 - Worker:使用进程内内存缓存,不写文件。 - 数据文件默认:`box.dat`- 读取路径优先级:当前目录 -> `process.cwd()`。 与 Web Storage 的行为差异: - 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。 - `removeItem/clear` 仅部分平台可用(目前为 Quantumult X、Worker、Node.js,以及 Surge 的 `removeItem`)。 - `getItem` 会尝试 `JSON.parse``setItem` 写入对象会 `JSON.stringify`。 平台后端映射: | 平台 | 读写接口 | | --- | --- | | Surge / Loon / Stash / Egern / Shadowrocket | `$persistentStore.read/write` | | Quantumult X | `$prefs.valueForKey/setValueForKey` | | Worker | 进程内内存缓存 | | Node.js | 本地 `box.dat` | ### `polyfill/Console.mjs` `Console` 是统一日志工具(静态类)。 #### 日志级别 - `Console.logLevel` 可读写。 - 支持:`OFF(0)` / `ERROR(1)` / `WARN(2)` / `INFO(3)` / `DEBUG(4)` / `ALL(5)``logLevel` 用法示例: ```js import { Console } from "@nsnanocat/util"; Console.logLevel = "debug"; // 或 4 Console.debug("debug message"); Console.logLevel = 2; // WARN Console.info("won't print at WARN level"); Console.warn("will print"); console.log(Console.logLevel); // "WARN" ``` #### 方法 - `clear()` - `count(label = "default")` - `countReset(label = "default")` - `debug(...msg)` - `error(...msg)` - `exception(...msg)` - `group(label)` - `groupEnd()` - `info(...msg)` - `log(...msg)` - `time(label = "default")` - `timeLog(label = "default")` - `timeEnd(label = "default")` - `warn(...msg)` 参数与返回值: | 方法 | 参数 | 返回值 | 说明 | | --- | --- | --- | --- | | `clear()` | 无 | `void` | 当前实现为空函数 | | `count(label)` | `label?: string` | `void` | 计数并输出 | | `countReset(label)` | `label?: string` | `void` | 重置计数器 | | `debug(...msg)` | `...msg: any[]` | `void` | 仅 `DEBUG/ALL` 级别输出 | | `error(...msg)` | `...msg: any[]` | `void` | Worker / Node.js 优先输出 `stack` | | `exception(...msg)` | `...msg: any[]` | `void` | `error` 别名 | | `group(label)` | `label: string` | `void` | 压栈分组 | | `groupEnd()` | 无 | `void` | 出栈分组 | | `info(...msg)` | `...msg: any[]` | `void` | `INFO` 及以上 | | `log(...msg)` | `...msg: any[]` | `void` | 通用日志 | | `time(label)` | `label?: string` | `void` | 记录起始时间 | | `timeLog(label)` | `label?: string` | `void` | 输出耗时 | | `timeEnd(label)` | `label?: string` | `void` | 清除计时器 | | `warn(...msg)` | `...msg: any[]` | `void` | `WARN` 及以上 | 平台差异: - Worker / Node.js 下 `error` 会优先打印 `Error.stack`- 其他平台统一加前缀符号输出(`❌/⚠️/ℹ️/🅱️`)。 ### `polyfill/Lodash.mjs` `Lodash` 为“部分方法的简化实现”,不是完整 Lodash。各方法语义可参考: - https://www.lodashjs.com - https://lodash.com 导入约定(建议): - 这是 lodash 官方示例中常见的惯例写法:使用 `_` 作为工具对象别名。 ```js import { Lodash as _ } from "@nsnanocat/util"; const data = {}; _.set(data, "a.b", 1); console.log(data); // { a: { b: 1 } } const value = _.get(data, "a.b", 0); console.log(value); // 1 ``` 示例对应的 lodash 官方文档页面: - `set(object, path, value)` - 官方文档:https://lodash.com/docs/#set - 中文文档:https://www.lodashjs.com/docs/lodash.set - `get(object, path, defaultValue)` - 官方文档:https://lodash.com/docs/#get - 中文文档:https://www.lodashjs.com/docs/lodash.get 当前实现包含: - `escape(string)` - `unescape(string)` - `toPath(value)` - `get(object, path, defaultValue)` - `set(object, path, value)` - `unset(object, path)` - `pick(object, paths)` - `omit(object, paths)` - `merge(object, ...sources)` 对应 lodash 官方文档页面: - `escape(string)` - 官方文档:https://lodash.com/docs/#escape - 中文文档:https://www.lodashjs.com/docs/lodash.escape - `unescape(string)` - 官方文档:https://lodash.com/docs/#unescape - 中文文档:https://www.lodashjs.com/docs/lodash.unescape - `toPath(value)` - 官方文档:https://lodash.com/docs/#toPath - 中文文档:https://www.lodashjs.com/docs/lodash.toPath - `get(object, path, defaultValue)` - 官方文档:https://lodash.com/docs/#get - 中文文档:https://www.lodashjs.com/docs/lodash.get - `set(object, path, value)` - 官方文档:https://lodash.com/docs/#set - 中文文档:https://www.lodashjs.com/docs/lodash.set - `unset(object, path)` - 官方文档:https://lodash.com/docs/#unset - 中文文档:https://www.lodashjs.com/docs/lodash.unset - `pick(object, paths)` - 官方文档:https://lodash.com/docs/#pick - 中文文档:https://www.lodashjs.com/docs/lodash.pick - `omit(object, paths)` - 官方文档:https://lodash.com/docs/#omit - 中文文档:https://www.lodashjs.com/docs/lodash.omit - `merge(object, ...sources)` - 官方文档:https://lodash.com/docs/#merge - 中文文档:https://www.lodashjs.com/docs/lodash.merge 参数与返回值: | 方法 | 参数 | 返回值 | 说明 | | --- | --- | --- | --- | | `escape` | `string: string` | `string` | HTML 转义 | | `unescape` | `string: string` | `string` | HTML 反转义 | | `toPath` | `value: string` | `string[]` | `a[0].b` -> `['a','0','b']` | | `get` | `object?: object, path?: string\\|string[], defaultValue?: any` | `any` | 路径读取 | | `set` | `object: object, path: string\\|string[], value: any` | `object` | 路径写入(会创建中间层) | | `unset` | `object?: object, path?: string\\|string[]` | `boolean` | 删除路径并返回结果 | | `pick` | `object?: object, paths?: string\\|string[]` | `object` | 挑选 key(仅第一层) | | `omit` | `object?: object, paths?: string\\|string[]` | `object` | 删除 key(会修改原对象) | | `merge` | `object: object, ...sources: object[]` | `object` | 深合并(非完整 lodash 行为) | `merge` 行为(与 lodash 官方有差异): - 深度合并 Plain Object。 - Array 直接覆盖;空数组不覆盖已存在值。 - Map/Set 支持同类型合并;空 Map/Set 不覆盖已存在值。 - `undefined` 不覆盖,`null` 会覆盖。 - 直接修改目标对象(mutates target)。 ### `polyfill/qs.mjs` 当前实现为项目内使用的轻量子集,不追求与官方 `qs` 完全一致。 #### `qs.parse(query)` - 签名:`qs.parse(query?: string | object | null): object` - 作用:将查询字符串或对象输入归一化为支持深路径的对象。 - 依赖:内部使用 `Lodash.set` 展开路径。 当前行为: -`query``string` 时: -`&` / `=` 切分。 - 去掉值中的双引号。 - 使用点路径或数组下标路径展开对象。 -`query``object` 时: - 将 key 当路径写入新对象(`{"a.b":"1"}` -> `{ a: { b: "1" } }`)。 -`query``null``undefined` 时: - 返回 `{}````js import { qs } from "@nsnanocat/util"; console.log(qs.parse("mode=on&a.b=1")); // { mode: "on", a: { b: "1" } } console.log(qs.parse({ "list[0]": "x", "list[1]": "y" })); // { list: ["x", "y"] } ``` #### `qs.stringify(object)` - 签名:`qs.stringify(object?: object): string` - 作用:将对象按深路径展开为查询字符串。 - 依赖:内部使用 `Lodash.get``Lodash.toPath` 读取并格式化路径。 当前行为: - 普通对象输出为点路径(`a.b=1`)。 - 数组输出为索引路径(`list[0]=x`)。 - 值始终执行 `encodeURIComponent` 编码。 - `null` 会序列化为空值(`key=`),`undefined` 会跳过。 ```js import { qs } from "@nsnanocat/util"; console.log(qs.stringify({ a: { b: "1" }, list: ["x", "y"] })); // a.b=1&list%5B0%5D=x&list%5B1%5D=y ``` ### `polyfill/StatusTexts.mjs` #### `StatusTexts` - 类型:`Record<number, string>` - 内容:HTTP 状态码到状态文本映射(100~511 的常见码)。 - 主要用途:给 Quantumult X 的 `$done` 状态行补全文本(如 `HTTP/1.1 200 OK`)。 - 参考示例:https://github.com/crossutility/Quantumult-X/raw/refs/heads/master/sample-rewrite-response-header.js ## 平台差异总览 说明:本节展示的是各平台原生脚本接口差异。实际在本库中,这些差异已由 `done``fetch``notification``Storage` 等模块做了统一适配。 | 能力 | Quantumult X | Loon | Surge | Stash | Egern | Shadowrocket | Worker | Node.js | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | HTTP 请求 | `$task.fetch` | `$httpClient` | `$httpClient` | `$httpClient` | `$httpClient` | `$httpClient` | `fetch` | `fetch` | | 通知 | `$notify` | `$notification.post` | `$notification.post` | `$notification.post` | `$notification.post` | `$notification.post` | 无 | 无 | | 持久化 | `$prefs` | `$persistentStore` | `$persistentStore` | `$persistentStore` | `$persistentStore` | `$persistentStore` | 内存缓存 | `box.dat` | | 结束脚本 | `$done` | `$done` | `$done` | `$done` | `$done` | `$done` | 仅日志 | `process.exit(1)` | | `removeItem/clear` | 可用 | 不可用 | `removeItem` 可用 / `clear` 不可用 | 不可用 | 不可用 | 不可用 | 可用 | 可用 | | `policy` 注入(`fetch/done`) | `opts.policy` | `node` | `X-Surge-Policy`(done) | `X-Stash-Selected-Proxy` | 无专门映射 | `X-Surge-Proxy`(fetch) | 无 | 无 | ## 已知限制与注意事项 - `lib/argument.mjs``$argument` 标准化模块,`import` 时会按规则重写全局 `$argument`- `lib/done.mjs` 在 Worker 仅记录结束日志。 - `lib/done.mjs` 在 Node.js 固定 `process.exit(1)`- `Storage.removeItem("@a.b")` 分支存在未声明变量写入风险;如要大量使用路径删除,建议先本地验证。 - `lib/runScript.mjs` 未从包主入口导出,需要按文件路径直接导入。 ## 参考资料 以下资料用于对齐不同平台 `$` API 语义;README 的“平台差异”优先以本仓库实现为准。 ### Surge - [Surge Manual - Scripting API](https://manual.nssurge.com/scripting/common.html) - [Surge Manual - HTTP Client API](https://manual.nssurge.com/scripting/http-client.html) ### Stash - [Stash Docs - Scripting Overview](https://stash.wiki/scripting/overview/) - [Stash Docs - API](https://stash.wiki/scripting/apis/) - [Stash Docs - Rewrite Script](https://stash.wiki/scripting/rewrite-script/) ### Loon - [Loon Script](https://nsloon.app/Loon-Script) - [Loon API](https://nsloon.app/Loon-API) ### Quantumult X - [crossutility/Quantumult-X - sample-task.js](https://raw.githubusercontent.com/crossutility/Quantumult-X/master/sample-task.js) - [crossutility/Quantumult-X - sample-rewrite-with-script.js](https://raw.githubusercontent.com/crossutility/Quantumult-X/master/sample-rewrite-with-script.js) - [crossutility/Quantumult-X - sample-fetch-opts-policy.js](https://raw.githubusercontent.com/crossutility/Quantumult-X/master/sample-fetch-opts-policy.js) - [crossutility/Quantumult-X - sample-rewrite-response-header.js](https://github.com/crossutility/Quantumult-X/raw/refs/heads/master/sample-rewrite-response-header.js) ### Worker -`Cloudflare` 全局标记识别 Worker 运行时。 ### Node.js - [Node.js Globals - fetch](https://nodejs.org/api/globals.html#fetch) ### Web API / Lodash - [MDN - Window.fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) - [MDN(中文)- Window.fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch) - [MDN - Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) - [MDN(中文)- Storage](https://developer.mozilla.org/zh-CN/docs/Web/API/Storage) - [Lodash Docs](https://www.lodashjs.com) - [lodash.com](https://lodash.com) ### Egern / Shadowrocket - [Egern Docs - Scriptings 配置](https://egernapp.com/docs/configuration/scriptings) - [Shadowrocket 官方站点](https://www.shadowlaunch.com/) > 说明:Egern 与 Shadowrocket 暂未检索到等价于 Surge/Loon/Stash 的完整公开脚本 API 页面;相关差异说明以本库实际代码分支行为为准。