用于统一 Quantumult X / Loon / Shadowrocket / Worker / Node.js / Egern / Surge / Stash 脚本接口的通用工具库。
核心目标:
fetch / Storage / Console / Lodash / qs)。发布源:
如果你不确定该选哪个,直接用 npm 源即可。 如果你从 GitHub Packages 安装,需要先配置 GitHub 认证(PAT Token)。
# 首次安装:拉取并安装这个包
npm i @nsnanocat/util
# 更新到最新版本:升级已安装的 util
npm i @nsnanocat/util@latest
# 你也可以使用 update(效果类似)
# npm update @nsnanocat/util
# 把 @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
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";
package.json 会根据运行时与模块系统选择明确入口:
index.node.mjsindex.mjsindex.cjsESM 主入口已导出:
lib/app.mjslib/argument.mjs($argument 参数标准化模块,导入时自动执行)lib/done.mjslib/notification.mjslib/time.mjslib/wait.mjspolyfill/Console.mjspolyfill/fetch.mjspolyfill/Lodash.mjspolyfill/qs.mjspolyfill/StatusTexts.mjspolyfill/Storage.mjslib/environment.mjslib/runScript.mjsgetStorage.mjs(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用;请通过子路径 @nsnanocat/util/getStorage.mjs 导入)polyfill/Storage.cjs(Node.js / Worker CJS 入口;请通过子路径 @nsnanocat/util/polyfill/Storage 导入)说明:
| 模块 | 依赖模块 | 使用的函数/常量 | 为什么依赖 |
|---|---|---|---|
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/fetch.node.mjs | polyfill/fetch.mjs, node-fetch, fetch-cookie | fetch, CookieJar | 优先使用 Node.js 原生 Fetch,并通过静态 ESM 导入提供回退与 CookieJar |
polyfill/Storage.mjs | lib/app.mjs, polyfill/Lodash.mjs | $app, Lodash.get, Lodash.set, Lodash.unset | ESM 路径下按平台选持久化后端并支持 @key.path 读写 |
polyfill/Storage.node.mjs | polyfill/Storage.mjs, node:fs, node:path | Storage, 文件系统后端 | 为 Node.js ESM 注入本地文件与 Vercel /tmp 后端 |
polyfill/Lodash.mjs | 无 | 无 | 提供路径/合并等基础能力,被多个模块复用 |
polyfill/qs.mjs | polyfill/Lodash.mjs | Lodash.get, Lodash.set, Lodash.toPath | 提供查询字符串与对象之间的解析/序列化能力 |
polyfill/StatusTexts.mjs | 无 | 无 | 提供 HTTP 状态文案,供 fetch/done 使用 |
index.mjs / lib/index.js / polyfill/index.js | 多个模块 | export * | 聚合导出,不含业务逻辑 |
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)。import { $app } from "@nsnanocat/util";
const appName = $app; // 读取 $app,返回平台字符串
console.log(appName);
lib/app.mjs):$task -> Quantumult X$loon -> Loon$rocket -> ShadowrocketEgern -> Egern$environment 且有 surge-version -> Surge$environment 且有 stash-version -> StashCloudflare -> Workerprocess.versions.node -> Node.jsundefined'key' in globalThis 检测平台标记,避免 Object.keys(globalThis) 漏掉不可枚举全局变量;当前 Worker 识别以 Cloudflare 全局标记为准。$environment / environment()lib/environment.mjs(未从包主入口导出)environment(): objectimport { $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")时会自动执行本模块。await import,请使用静态导入或直接走包入口导入。$argument 会按 URL Params 样式格式化为对象,并支持深路径。qs.parse(globalThis.$argument)。import { $argument } from "@nsnanocat/util" 读取当前已标准化的 $argument 快照。a=1&b=2)。$argument 形式。$argument。$argument 为 string(如 "a.b=1&x=2")时:
& / = 切分。a.b=1 -> { a: { b: "1" } })。$argument 为 object 时:
{"a.b":"1"} -> {a:{b:"1"}})。$argument 为 null 或 undefined:会归一化为 {}。$argument.LogLevel 存在:同步到 Console.logLevel。import { $argument } from "@nsnanocat/util";
// $argument = "mode=on&a.b=1"; // 示例入参,实际由模块参数注入
console.log($argument); // { mode: "on", a: { b: "1" } }
lib/done.mjsdone(object = {})done(object?: object): void$done / Worker 日志结束 / Node 退出)。说明:下表描述的是各 App 原生接口差异与本库内部映射逻辑。调用方只需要按 done 的统一参数传值即可,不需要自己再写平台分支。
支持字段(输入):
status: number | stringurl: stringheaders: objectbody: string | ArrayBuffer | TypedArraybodyBytes: ArrayBufferpolicy: 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 分支不做映射。status 在部分场景要求完整状态行(如 HTTP/1.1 200 OK),本库会在传入数字状态码时自动拼接(依赖 StatusTexts)。$done,仅记录结束日志。$done,而是直接退出进程,且退出码固定为 1。$app === undefined)只记录结束日志,不会尝试调用 $done 或退出进程。lib/notification.mjsnotification(title, subtitle, body, content)title?: stringsubtitle?: stringbody?: stringcontent?: string | number | boolean | objecttitle = "ℹ️ ${$app} 通知"notify/notification 参数格式并发送通知。content 可用 key(对象形式):
open / open-url / url / openUrlcopy / update-pasteboard / updatePasteboardmedia / media-url / mediaUrlauto-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 分支不会生效。media 仅接受网络 URL;Base64 媒体不会自动映射。lib/time.mjstime(format, ts)time(format: string, ts?: number): stringts:可选时间戳(传给 new Date(ts))。YY、yyyy、MM、dd、HH、mm、ss、sss、S(季度)。time("yyyy-MM-dd HH:mm:ss.sss");
time("yyyyMMddHHmmss", Date.now());
注意:当前实现对每个 token 只替换一次(String.replace 非全局)。
lib/wait.mjswait(delay = 1000)wait(delay?: number): Promise<void>await wait(500);
lib/runScript.mjs(未主入口导出)runScript(script, runOpts)runScript(script: string, runOpts?: { timeout?: number }): Promise<void>httpapi 调用本地脚本执行接口:/v1/scripting/evaluate。@chavy_boxjs_userCfgs.httpapi@chavy_boxjs_userCfgs.httpapi_timeoutscript_textmock_type: "cron"timeout示例:
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 | databaseundefined:database[name] -> $argument -> PersistentStore[name]Argument / $argument:database[name] -> PersistentStore[name] -> $argumentPersistentStore / BoxJs / $persistentStore(默认):database[name] -> PersistentStore[name]database:仅 database[name]注意:Configs 与 Caches 始终按每个 name 合并(与 $argument.Storage 无关)。
自动类型转换(Root.Settings):
"true"/"false" -> booleannumberarray,并尝试逐项转数字示例:
import getStorage from "@nsnanocat/util/getStorage.mjs";
const store = getStorage("@my_box", ["YouTube", "Global"], database);
getStorage.mjs 同时导出以下辅助函数:
traverseObject(o, c):深度遍历对象并替换叶子值string2number(string):将纯数字字符串转换为数字value2array(value):字符串按逗号拆分;数字/布尔值会被包装为单元素数组示例:
import getStorage, {
traverseObject,
string2number,
value2array,
} from "@nsnanocat/util/getStorage.mjs";
const store = getStorage("@my_box", ["YouTube", "Global"], database);
polyfill/fetch.mjsfetch 使用三条明确运行路径:
polyfill/fetch.mjs:用于通用 ESM、JavaScriptCore 与 iOS 脚本平台;只使用宿主请求 API,不导入 Node.js 模块polyfill/fetch.node.mjs:用于 Node.js/Vercel ESM;优先使用原生 fetch,通过静态 import 加载 node-fetch 与 fetch-cookiepolyfill/fetch.cjs:用于 CommonJS;通过 require 加载 CommonJS 依赖polyfill/fetch.mjs 仍仿照 Web API Window.fetch 设计:
fetch 调用习惯,同时补齐各平台扩展参数映射fetch(resource, options = {})fetch(resource: object | string, options?: object): Promise<object>resource 为对象:{ ...options, ...resource }resource 为字符串:{ ...options, url: resource }method 时,若有 body/bodyBytes -> POST,否则 GETHost、:authority、Content-Length/content-lengthtimeout 规则:
5(秒)> 500 视为毫秒并转为秒通用请求字段:
urlmethodheadersbodybodyBytestimeoutpolicyredirection / auto-redirectauto-cookie(Node.js 的 ESM/CJS 分支识别,默认启用,传入 false / 0 / -1 可关闭;通用 ESM / JavaScriptCore 使用宿主 fetch)说明:下表是各 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 |
返回对象(统一后)常见字段:
okstatusstatusCodestatusTextheadersbodybodyBytes不可用/差异点:
policy 在 Surge / Egern 分支没有额外适配逻辑。redirection 在部分平台会映射为 auto-redirect 或 opts.redirection。timeout 时,5 和 5000 都会被接受;库会先将用户输入归一化,再按平台要求转换为秒或毫秒。Response 对象。fetch、Headers、Request 与 Response 时,ESM 路径直接调用宿主 fetch;bodyBytes 映射为 body,redirection 映射为 redirect,并移除 timeout、policy、auto-cookie、opts 等非标准字段。Worker / Node.js 使用说明:
import { fetch } from "@nsnanocat/util" 自动选择 fetch.node.mjs。require("@nsnanocat/util").fetch 自动选择 fetch.cjs。fetch.mjs,不会解析 node-fetch、fetch-cookie 或任何 Node.js 模块。ok/status/statusText/body/bodyBytes。polyfill/Storage.mjsStorage 使用三条明确运行路径:
polyfill/Storage.mjs:用于通用 ESM、JavaScriptCore、Worker 与 iOS 脚本平台,不加载 Node.js 模块polyfill/Storage.node.mjs:用于 Node.js/Vercel ESM,静态导入 node:fs 与 node:pathpolyfill/Storage.cjs:用于 CommonJS,不加载 ESM 文件polyfill/Storage.mjs 仍仿照 Web Storage 接口(Storage)设计:
Storage.getItem(keyName, defaultValue = null)@root.path.to.key。Storage.setItem(keyName, keyValue)@root.path 写入嵌套对象。keyValue 为对象时自动 JSON.stringify。Storage.removeItem(keyName)$prefs.removeValueForKey)。$persistentStore.write(null, keyName) 删除。false。Storage.clear()$prefs.removeAllValues)。false。box.dat。process.cwd()。/tmp/box.dat;该文件只作为实例级临时缓存,不保证跨实例持久化。process.getBuiltinModule();CommonJS 不再 require .mjs 文件。Node.js 使用说明:
import { Storage } from "@nsnanocat/util" 调用。require("@nsnanocat/util").Storage 或 require("@nsnanocat/util/polyfill/Storage").Storage 调用。Storage 分支支持 box.dat 读写与落盘与 Web Storage 的行为差异:
@key.path 深路径读写(Web Storage 原生不支持)。removeItem/clear 仅部分平台可用:
removeItemgetItem 会尝试 JSON.parse,setItem 写入对象会 JSON.stringify。平台后端映射:
| 平台 | 读写接口 |
|---|---|
| Surge / Loon / Stash / Egern / Shadowrocket | $persistentStore.read/write |
| Quantumult X | $prefs.valueForKey/setValueForKey |
| Worker(ESM/CJS) | 进程内内存缓存 |
| Node.js(ESM/CJS) | 本地 box.dat;Vercel ESM 使用 /tmp/box.dat |
polyfill/Console.mjsConsole 是统一日志工具(静态类)。
Console.logLevel 可读写。OFF(0) / ERROR(1) / WARN(2) / INFO(3) / DEBUG(4) / ALL(5)。logLevel 用法示例:
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 及以上 |
平台差异:
error 会优先打印 Error.stack。❌/⚠️/ℹ️/🅱️)。polyfill/Lodash.mjsLodash 为“部分方法的简化实现”,不是完整 Lodash。各方法语义可参考:
导入约定(建议):
_ 作为工具对象别名。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)
get(object, path, defaultValue)
当前实现包含:
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)
unescape(string)
toPath(value)
get(object, path, defaultValue)
set(object, path, value)
unset(object, path)
pick(object, paths)
omit(object, paths)
merge(object, ...sources)
参数与返回值:
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
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 |
omit | `object?: object, paths?: string\ | string[]` | object |
merge | object: object, ...sources: object[] | object | 深合并(非完整 lodash 行为) |
merge 行为(与 lodash 官方有差异):
undefined 不覆盖,null 会覆盖。polyfill/qs.mjs当前实现为项目内使用的轻量子集,不追求与官方 qs 完全一致。
qs.parse(query)qs.parse(query?: string | object | null): objectLodash.set 展开路径。当前行为:
query 为 string 时:
?(会先去掉)。& / = 切分。decodeURIComponent,并将 + 视为空格)。a[b])展开对象。query 为 object 时:
{"a.b":"1"} -> { a: { b: "1" } })。query 为 null 或 undefined 时:
{}。import { qs } from "@nsnanocat/util";
console.log(qs.parse("mode=on&a.b=1"));
// { mode: "on", a: { b: "1" } }
console.log(qs.parse("a%5Bb%5D=c%20d"));
// { a: { b: "c d" } }
console.log(qs.parse({ "list[0]": "x", "list[1]": "y" }));
// { list: ["x", "y"] }
qs.stringify(object)qs.stringify(object?: object): stringLodash.get 与 Lodash.toPath 读取并格式化路径。当前行为:
a.b=1)。list[0]=x)。encodeURIComponent 编码。null 会序列化为空值(key=),undefined 会跳过。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.mjsStatusTextsRecord<number, string>$done 状态行补全文本(如 HTTP/1.1 200 OK)。说明:本节展示的是各平台原生脚本接口差异。实际在本库中,这些差异已由 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 的“平台差异”优先以本仓库实现为准。
Cloudflare 全局标记识别 Worker 运行时。说明:Egern 与 Shadowrocket 暂未检索到等价于 Surge/Loon/Stash 的完整公开脚本 API 页面;相关差异说明以本库实际代码分支行为为准。
JavaScript
99.8%
用于统一 Quantumult X / Loon / Shadowrocket / Worker / Node.js / Egern / Surge / Stash 脚本接口的通用工具库。
核心目标:
fetch / Storage / Console / Lodash / qs)。发布源:
如果你不确定该选哪个,直接用 npm 源即可。 如果你从 GitHub Packages 安装,需要先配置 GitHub 认证(PAT Token)。
# 首次安装:拉取并安装这个包
npm i @nsnanocat/util
# 更新到最新版本:升级已安装的 util
npm i @nsnanocat/util@latest
# 你也可以使用 update(效果类似)
# npm update @nsnanocat/util
# 把 @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
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";
package.json 会根据运行时与模块系统选择明确入口:
index.node.mjsindex.mjsindex.cjsESM 主入口已导出:
lib/app.mjslib/argument.mjs($argument 参数标准化模块,导入时自动执行)lib/done.mjslib/notification.mjslib/time.mjslib/wait.mjspolyfill/Console.mjspolyfill/fetch.mjspolyfill/Lodash.mjspolyfill/qs.mjspolyfill/StatusTexts.mjspolyfill/Storage.mjslib/environment.mjslib/runScript.mjsgetStorage.mjs(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用;请通过子路径 @nsnanocat/util/getStorage.mjs 导入)polyfill/Storage.cjs(Node.js / Worker CJS 入口;请通过子路径 @nsnanocat/util/polyfill/Storage 导入)说明:
| 模块 | 依赖模块 | 使用的函数/常量 | 为什么依赖 |
|---|---|---|---|
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/fetch.node.mjs | polyfill/fetch.mjs, node-fetch, fetch-cookie | fetch, CookieJar | 优先使用 Node.js 原生 Fetch,并通过静态 ESM 导入提供回退与 CookieJar |
polyfill/Storage.mjs | lib/app.mjs, polyfill/Lodash.mjs | $app, Lodash.get, Lodash.set, Lodash.unset | ESM 路径下按平台选持久化后端并支持 @key.path 读写 |
polyfill/Storage.node.mjs | polyfill/Storage.mjs, node:fs, node:path | Storage, 文件系统后端 | 为 Node.js ESM 注入本地文件与 Vercel /tmp 后端 |
polyfill/Lodash.mjs | 无 | 无 | 提供路径/合并等基础能力,被多个模块复用 |
polyfill/qs.mjs | polyfill/Lodash.mjs | Lodash.get, Lodash.set, Lodash.toPath | 提供查询字符串与对象之间的解析/序列化能力 |
polyfill/StatusTexts.mjs | 无 | 无 | 提供 HTTP 状态文案,供 fetch/done 使用 |
index.mjs / lib/index.js / polyfill/index.js | 多个模块 | export * | 聚合导出,不含业务逻辑 |
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)。import { $app } from "@nsnanocat/util";
const appName = $app; // 读取 $app,返回平台字符串
console.log(appName);
lib/app.mjs):$task -> Quantumult X$loon -> Loon$rocket -> ShadowrocketEgern -> Egern$environment 且有 surge-version -> Surge$environment 且有 stash-version -> StashCloudflare -> Workerprocess.versions.node -> Node.jsundefined'key' in globalThis 检测平台标记,避免 Object.keys(globalThis) 漏掉不可枚举全局变量;当前 Worker 识别以 Cloudflare 全局标记为准。$environment / environment()lib/environment.mjs(未从包主入口导出)environment(): objectimport { $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")时会自动执行本模块。await import,请使用静态导入或直接走包入口导入。$argument 会按 URL Params 样式格式化为对象,并支持深路径。qs.parse(globalThis.$argument)。import { $argument } from "@nsnanocat/util" 读取当前已标准化的 $argument 快照。a=1&b=2)。$argument 形式。$argument。$argument 为 string(如 "a.b=1&x=2")时:
& / = 切分。a.b=1 -> { a: { b: "1" } })。$argument 为 object 时:
{"a.b":"1"} -> {a:{b:"1"}})。$argument 为 null 或 undefined:会归一化为 {}。$argument.LogLevel 存在:同步到 Console.logLevel。import { $argument } from "@nsnanocat/util";
// $argument = "mode=on&a.b=1"; // 示例入参,实际由模块参数注入
console.log($argument); // { mode: "on", a: { b: "1" } }
lib/done.mjsdone(object = {})done(object?: object): void$done / Worker 日志结束 / Node 退出)。说明:下表描述的是各 App 原生接口差异与本库内部映射逻辑。调用方只需要按 done 的统一参数传值即可,不需要自己再写平台分支。
支持字段(输入):
status: number | stringurl: stringheaders: objectbody: string | ArrayBuffer | TypedArraybodyBytes: ArrayBufferpolicy: 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 分支不做映射。status 在部分场景要求完整状态行(如 HTTP/1.1 200 OK),本库会在传入数字状态码时自动拼接(依赖 StatusTexts)。$done,仅记录结束日志。$done,而是直接退出进程,且退出码固定为 1。$app === undefined)只记录结束日志,不会尝试调用 $done 或退出进程。lib/notification.mjsnotification(title, subtitle, body, content)title?: stringsubtitle?: stringbody?: stringcontent?: string | number | boolean | objecttitle = "ℹ️ ${$app} 通知"notify/notification 参数格式并发送通知。content 可用 key(对象形式):
open / open-url / url / openUrlcopy / update-pasteboard / updatePasteboardmedia / media-url / mediaUrlauto-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 分支不会生效。media 仅接受网络 URL;Base64 媒体不会自动映射。lib/time.mjstime(format, ts)time(format: string, ts?: number): stringts:可选时间戳(传给 new Date(ts))。YY、yyyy、MM、dd、HH、mm、ss、sss、S(季度)。time("yyyy-MM-dd HH:mm:ss.sss");
time("yyyyMMddHHmmss", Date.now());
注意:当前实现对每个 token 只替换一次(String.replace 非全局)。
lib/wait.mjswait(delay = 1000)wait(delay?: number): Promise<void>await wait(500);
lib/runScript.mjs(未主入口导出)runScript(script, runOpts)runScript(script: string, runOpts?: { timeout?: number }): Promise<void>httpapi 调用本地脚本执行接口:/v1/scripting/evaluate。@chavy_boxjs_userCfgs.httpapi@chavy_boxjs_userCfgs.httpapi_timeoutscript_textmock_type: "cron"timeout示例:
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 | databaseundefined:database[name] -> $argument -> PersistentStore[name]Argument / $argument:database[name] -> PersistentStore[name] -> $argumentPersistentStore / BoxJs / $persistentStore(默认):database[name] -> PersistentStore[name]database:仅 database[name]注意:Configs 与 Caches 始终按每个 name 合并(与 $argument.Storage 无关)。
自动类型转换(Root.Settings):
"true"/"false" -> booleannumberarray,并尝试逐项转数字示例:
import getStorage from "@nsnanocat/util/getStorage.mjs";
const store = getStorage("@my_box", ["YouTube", "Global"], database);
getStorage.mjs 同时导出以下辅助函数:
traverseObject(o, c):深度遍历对象并替换叶子值string2number(string):将纯数字字符串转换为数字value2array(value):字符串按逗号拆分;数字/布尔值会被包装为单元素数组示例:
import getStorage, {
traverseObject,
string2number,
value2array,
} from "@nsnanocat/util/getStorage.mjs";
const store = getStorage("@my_box", ["YouTube", "Global"], database);
polyfill/fetch.mjsfetch 使用三条明确运行路径:
polyfill/fetch.mjs:用于通用 ESM、JavaScriptCore 与 iOS 脚本平台;只使用宿主请求 API,不导入 Node.js 模块polyfill/fetch.node.mjs:用于 Node.js/Vercel ESM;优先使用原生 fetch,通过静态 import 加载 node-fetch 与 fetch-cookiepolyfill/fetch.cjs:用于 CommonJS;通过 require 加载 CommonJS 依赖polyfill/fetch.mjs 仍仿照 Web API Window.fetch 设计:
fetch 调用习惯,同时补齐各平台扩展参数映射fetch(resource, options = {})fetch(resource: object | string, options?: object): Promise<object>resource 为对象:{ ...options, ...resource }resource 为字符串:{ ...options, url: resource }method 时,若有 body/bodyBytes -> POST,否则 GETHost、:authority、Content-Length/content-lengthtimeout 规则:
5(秒)> 500 视为毫秒并转为秒通用请求字段:
urlmethodheadersbodybodyBytestimeoutpolicyredirection / auto-redirectauto-cookie(Node.js 的 ESM/CJS 分支识别,默认启用,传入 false / 0 / -1 可关闭;通用 ESM / JavaScriptCore 使用宿主 fetch)说明:下表是各 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 |
返回对象(统一后)常见字段:
okstatusstatusCodestatusTextheadersbodybodyBytes不可用/差异点:
policy 在 Surge / Egern 分支没有额外适配逻辑。redirection 在部分平台会映射为 auto-redirect 或 opts.redirection。timeout 时,5 和 5000 都会被接受;库会先将用户输入归一化,再按平台要求转换为秒或毫秒。Response 对象。fetch、Headers、Request 与 Response 时,ESM 路径直接调用宿主 fetch;bodyBytes 映射为 body,redirection 映射为 redirect,并移除 timeout、policy、auto-cookie、opts 等非标准字段。Worker / Node.js 使用说明:
import { fetch } from "@nsnanocat/util" 自动选择 fetch.node.mjs。require("@nsnanocat/util").fetch 自动选择 fetch.cjs。fetch.mjs,不会解析 node-fetch、fetch-cookie 或任何 Node.js 模块。ok/status/statusText/body/bodyBytes。polyfill/Storage.mjsStorage 使用三条明确运行路径:
polyfill/Storage.mjs:用于通用 ESM、JavaScriptCore、Worker 与 iOS 脚本平台,不加载 Node.js 模块polyfill/Storage.node.mjs:用于 Node.js/Vercel ESM,静态导入 node:fs 与 node:pathpolyfill/Storage.cjs:用于 CommonJS,不加载 ESM 文件polyfill/Storage.mjs 仍仿照 Web Storage 接口(Storage)设计:
Storage.getItem(keyName, defaultValue = null)@root.path.to.key。Storage.setItem(keyName, keyValue)@root.path 写入嵌套对象。keyValue 为对象时自动 JSON.stringify。Storage.removeItem(keyName)$prefs.removeValueForKey)。$persistentStore.write(null, keyName) 删除。false。Storage.clear()$prefs.removeAllValues)。false。box.dat。process.cwd()。/tmp/box.dat;该文件只作为实例级临时缓存,不保证跨实例持久化。process.getBuiltinModule();CommonJS 不再 require .mjs 文件。Node.js 使用说明:
import { Storage } from "@nsnanocat/util" 调用。require("@nsnanocat/util").Storage 或 require("@nsnanocat/util/polyfill/Storage").Storage 调用。Storage 分支支持 box.dat 读写与落盘与 Web Storage 的行为差异:
@key.path 深路径读写(Web Storage 原生不支持)。removeItem/clear 仅部分平台可用:
removeItemgetItem 会尝试 JSON.parse,setItem 写入对象会 JSON.stringify。平台后端映射:
| 平台 | 读写接口 |
|---|---|
| Surge / Loon / Stash / Egern / Shadowrocket | $persistentStore.read/write |
| Quantumult X | $prefs.valueForKey/setValueForKey |
| Worker(ESM/CJS) | 进程内内存缓存 |
| Node.js(ESM/CJS) | 本地 box.dat;Vercel ESM 使用 /tmp/box.dat |
polyfill/Console.mjsConsole 是统一日志工具(静态类)。
Console.logLevel 可读写。OFF(0) / ERROR(1) / WARN(2) / INFO(3) / DEBUG(4) / ALL(5)。logLevel 用法示例:
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 及以上 |
平台差异:
error 会优先打印 Error.stack。❌/⚠️/ℹ️/🅱️)。polyfill/Lodash.mjsLodash 为“部分方法的简化实现”,不是完整 Lodash。各方法语义可参考:
导入约定(建议):
_ 作为工具对象别名。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)
get(object, path, defaultValue)
当前实现包含:
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)
unescape(string)
toPath(value)
get(object, path, defaultValue)
set(object, path, value)
unset(object, path)
pick(object, paths)
omit(object, paths)
merge(object, ...sources)
参数与返回值:
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
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 |
omit | `object?: object, paths?: string\ | string[]` | object |
merge | object: object, ...sources: object[] | object | 深合并(非完整 lodash 行为) |
merge 行为(与 lodash 官方有差异):
undefined 不覆盖,null 会覆盖。polyfill/qs.mjs当前实现为项目内使用的轻量子集,不追求与官方 qs 完全一致。
qs.parse(query)qs.parse(query?: string | object | null): objectLodash.set 展开路径。当前行为:
query 为 string 时:
?(会先去掉)。& / = 切分。decodeURIComponent,并将 + 视为空格)。a[b])展开对象。query 为 object 时:
{"a.b":"1"} -> { a: { b: "1" } })。query 为 null 或 undefined 时:
{}。import { qs } from "@nsnanocat/util";
console.log(qs.parse("mode=on&a.b=1"));
// { mode: "on", a: { b: "1" } }
console.log(qs.parse("a%5Bb%5D=c%20d"));
// { a: { b: "c d" } }
console.log(qs.parse({ "list[0]": "x", "list[1]": "y" }));
// { list: ["x", "y"] }
qs.stringify(object)qs.stringify(object?: object): stringLodash.get 与 Lodash.toPath 读取并格式化路径。当前行为:
a.b=1)。list[0]=x)。encodeURIComponent 编码。null 会序列化为空值(key=),undefined 会跳过。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.mjsStatusTextsRecord<number, string>$done 状态行补全文本(如 HTTP/1.1 200 OK)。说明:本节展示的是各平台原生脚本接口差异。实际在本库中,这些差异已由 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 的“平台差异”优先以本仓库实现为准。
Cloudflare 全局标记识别 Worker 运行时。说明:Egern 与 Shadowrocket 暂未检索到等价于 Surge/Loon/Stash 的完整公开脚本 API 页面;相关差异说明以本库实际代码分支行为为准。
JavaScript
99.8%