From 62d2c576d81806bd0b78dd5261cce14d49df715f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 11:23:55 +0800 Subject: [PATCH 01/72] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=80=92?= =?UTF-8?q?=E5=BD=92=E5=90=88=E5=B9=B6=E5=8A=9F=E8=83=BD=E5=92=8C=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E6=99=AE=E9=80=9A=E5=AF=B9=E8=B1=A1=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Lodash.mjs --- polyfill/Lodash.mjs | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/polyfill/Lodash.mjs b/polyfill/Lodash.mjs index 1e2f71e..dd9b6ad 100644 --- a/polyfill/Lodash.mjs +++ b/polyfill/Lodash.mjs @@ -22,6 +22,68 @@ export class Lodash { return result === undefined ? defaultValue : result; } + /** + * 递归合并源对象的自身可枚举属性到目标对象 + * @description 简化版 lodash.merge,用于合并配置对象 + * + * 适用情况: + * - 合并嵌套的配置/设置对象 + * - 需要深度合并而非浅层覆盖的场景 + * - 多个源对象依次合并到目标对象 + * + * 限制: + * - 仅处理普通对象 (Plain Object),不处理 Map/Set/Date/RegExp 等特殊对象 + * - 数组会被直接覆盖,不会合并数组元素 + * - 不处理循环引用,可能导致栈溢出 + * - 不复制 Symbol 属性和不可枚举属性 + * - 不保留原型链,仅处理自身属性 + * - 会修改原始目标对象 (mutates target) + * + * @param {object} object - 目标对象 + * @param {...object} sources - 源对象(可多个) + * @returns {object} 返回合并后的目标对象 + * @example + * const target = { a: { b: 1 }, c: 2 }; + * const source = { a: { d: 3 }, e: 4 }; + * Lodash.merge(target, source); + * // => { a: { b: 1, d: 3 }, c: 2, e: 4 } + */ + static merge(object, ...sources) { + if (object === null || object === undefined) return object; + + for (const source of sources) { + if (source === null || source === undefined) continue; + + for (const key of Object.keys(source)) { + const sourceValue = source[key]; + const targetValue = object[key]; + + switch (true) { + case Lodash.#isPlainObject(sourceValue) && Lodash.#isPlainObject(targetValue): + // 递归合并对象 + object[key] = Lodash.merge(targetValue, sourceValue); + break; + case sourceValue !== undefined: + object[key] = sourceValue; + break; + } + } + } + + return object; + } + + /** + * 判断值是否为普通对象 (Plain Object) + * @param {*} value - 要检查的值 + * @returns {boolean} 如果是普通对象返回 true + */ + static #isPlainObject(value) { + if (value === null || typeof value !== "object") return false; + const proto = Object.getPrototypeOf(value); + return proto === null || proto === Object.prototype; + } + static omit(object = {}, paths = []) { if (!Array.isArray(paths)) paths = [paths.toString()]; paths.forEach(path => Lodash.unset(object, path)); From 5f4a7f246ef232ac90276f6f8b221e509e679ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 11:23:59 +0800 Subject: [PATCH 02/72] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=20lodash=20?= =?UTF-8?q?=E7=9A=84=20merge=20=E6=96=B9=E6=B3=95=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- getStorage.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index f2f1528..8aa6bc5 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -19,8 +19,8 @@ export function getStorage(key, names, database) { //Console.debug("Default", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** Database *****************/ names.forEach(name => { - Store.Settings = { ...Store.Settings, ...database?.[name]?.Settings }; - Store.Configs = { ...Store.Configs, ...database?.[name]?.Configs }; + _.merge(Store.Settings, database?.[name]?.Settings); + _.merge(Store.Configs, database?.[name]?.Configs); }); //Console.debug("Database", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** Argument *****************/ From c06ad64a37da699c956017ca5104cbe1542562b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 11:29:41 +0800 Subject: [PATCH 03/72] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=84=9A=E6=9C=AC=E5=B9=B6=E6=B7=BB=E5=8A=A0=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E5=8A=9F=E8=83=BD=E7=9A=84=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- test/merge.test.js | 160 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 test/merge.test.js diff --git a/package.json b/package.json index 61c084c..33f595c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "type": "module", "scripts": { "tsc:build": "npx tsc", - "test": "exit 0" + "test": "node --test test/*.test.js", + "test:merge": "node --test test/merge.test.js" }, "repository": { "type": "git", diff --git a/test/merge.test.js b/test/merge.test.js new file mode 100644 index 0000000..0c429ab --- /dev/null +++ b/test/merge.test.js @@ -0,0 +1,160 @@ +import { Lodash as _ } from "../polyfill/Lodash.mjs"; +import assert from "node:assert"; +import { describe, it } from "node:test"; + +describe("Lodash.merge", () => { + describe("基础合并", () => { + it("应该合并两个简单对象", () => { + const target = { a: 1, b: 2 }; + const source = { c: 3 }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 }); + }); + + it("应该用源对象覆盖目标对象的同名属性", () => { + const target = { a: 1, b: 2 }; + const source = { b: 3 }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: 1, b: 3 }); + }); + + it("应该返回修改后的目标对象(mutates target)", () => { + const target = { a: 1 }; + const source = { b: 2 }; + const result = _.merge(target, source); + assert.strictEqual(result, target); + }); + }); + + describe("深度合并", () => { + it("应该递归合并嵌套对象", () => { + const target = { a: { b: 1, c: 2 } }; + const source = { a: { d: 3 } }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: { b: 1, c: 2, d: 3 } }); + }); + + it("应该递归合并多层嵌套对象", () => { + const target = { a: { b: { c: 1 } } }; + const source = { a: { b: { d: 2 }, e: 3 } }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: { b: { c: 1, d: 2 }, e: 3 } }); + }); + + it("嵌套对象中的同名属性应该被覆盖", () => { + const target = { a: { b: 1, c: 2 } }; + const source = { a: { b: 10 } }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: { b: 10, c: 2 } }); + }); + }); + + describe("多个源对象", () => { + it("应该依次合并多个源对象", () => { + const target = { a: 1 }; + const source1 = { b: 2 }; + const source2 = { c: 3 }; + const result = _.merge(target, source1, source2); + assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 }); + }); + + it("后面的源对象应该覆盖前面的", () => { + const target = { a: 1 }; + const source1 = { a: 2, b: 2 }; + const source2 = { a: 3 }; + const result = _.merge(target, source1, source2); + assert.deepStrictEqual(result, { a: 3, b: 2 }); + }); + }); + + describe("数组处理", () => { + it("数组应该被直接覆盖而不是合并", () => { + const target = { a: [1, 2, 3] }; + const source = { a: [4, 5] }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: [4, 5] }); + }); + + it("对象覆盖数组", () => { + const target = { a: [1, 2, 3] }; + const source = { a: { b: 1 } }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: { b: 1 } }); + }); + + it("数组覆盖对象", () => { + const target = { a: { b: 1 } }; + const source = { a: [1, 2, 3] }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: [1, 2, 3] }); + }); + }); + + describe("特殊值处理", () => { + it("目标对象为 null 时返回 null", () => { + const result = _.merge(null, { a: 1 }); + assert.strictEqual(result, null); + }); + + it("目标对象为 undefined 时返回 undefined", () => { + const result = _.merge(undefined, { a: 1 }); + assert.strictEqual(result, undefined); + }); + + it("源对象为 null 时跳过", () => { + const target = { a: 1 }; + const result = _.merge(target, null); + assert.deepStrictEqual(result, { a: 1 }); + }); + + it("源对象为 undefined 时跳过", () => { + const target = { a: 1 }; + const result = _.merge(target, undefined); + assert.deepStrictEqual(result, { a: 1 }); + }); + + it("源属性值为 undefined 时不覆盖目标属性", () => { + const target = { a: 1, b: 2 }; + const source = { a: undefined, c: 3 }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 }); + }); + + it("源属性值为 null 时覆盖目标属性", () => { + const target = { a: 1 }; + const source = { a: null }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: null }); + }); + }); + + describe("配置对象合并场景", () => { + it("应该正确合并典型的配置对象", () => { + const defaultSettings = { + theme: "light", + language: "en", + notifications: { + email: true, + push: true, + sms: false, + }, + }; + const userSettings = { + theme: "dark", + notifications: { + push: false, + }, + }; + const result = _.merge(defaultSettings, userSettings); + assert.deepStrictEqual(result, { + theme: "dark", + language: "en", + notifications: { + email: true, + push: false, + sms: false, + }, + }); + }); + }); +}); From 4d10660d6d778b44168dbbb1c9bd442a057023ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 11:45:11 +0800 Subject: [PATCH 04/72] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=A9=BA?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E4=B8=8D=E8=A6=86=E7=9B=96=E5=B7=B2=E6=9C=89?= =?UTF-8?q?=E5=80=BC=E5=B9=B6=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- polyfill/Lodash.mjs | 3 +++ test/merge.test.js | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/polyfill/Lodash.mjs b/polyfill/Lodash.mjs index dd9b6ad..c7f407f 100644 --- a/polyfill/Lodash.mjs +++ b/polyfill/Lodash.mjs @@ -63,6 +63,9 @@ export class Lodash { // 递归合并对象 object[key] = Lodash.merge(targetValue, sourceValue); break; + case Array.isArray(sourceValue) && sourceValue.length === 0 && targetValue !== undefined: + // 空数组不覆盖已有值 + break; case sourceValue !== undefined: object[key] = sourceValue; break; diff --git a/test/merge.test.js b/test/merge.test.js index 0c429ab..bc5561e 100644 --- a/test/merge.test.js +++ b/test/merge.test.js @@ -88,6 +88,28 @@ describe("Lodash.merge", () => { const result = _.merge(target, source); assert.deepStrictEqual(result, { a: [1, 2, 3] }); }); + + it("空数组不覆盖已有值", () => { + const target = { a: [1, 2, 3], b: { c: 1 }, d: "hello" }; + const source = { a: [], b: [], d: [] }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: [1, 2, 3], b: { c: 1 }, d: "hello" }); + }); + + it("空数组可以赋值给 undefined 的目标属性", () => { + const target = { a: 1 }; + const source = { b: [] }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: 1, b: [] }); + }); + + it("空数组不覆盖已有的空数组", () => { + const target = { a: [] }; + const source = { a: [] }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: [] }); + assert.strictEqual(result.a, target.a); // 保持原引用 + }); }); describe("特殊值处理", () => { From 7ac356a90992e5e39224543c43f51a0c65595986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 11:48:17 +0800 Subject: [PATCH 05/72] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9=20Map?= =?UTF-8?q?=20=E5=92=8C=20Set=20=E7=9A=84=E5=90=88=E5=B9=B6=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=A9=BA?= =?UTF-8?q?=20Map/Set=20=E4=B8=8D=E8=A6=86=E7=9B=96=E5=B7=B2=E6=9C=89?= =?UTF-8?q?=E5=80=BC=E5=B9=B6=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- polyfill/Lodash.mjs | 23 ++++++++++++- test/merge.test.js | 82 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/polyfill/Lodash.mjs b/polyfill/Lodash.mjs index c7f407f..0f42f41 100644 --- a/polyfill/Lodash.mjs +++ b/polyfill/Lodash.mjs @@ -32,7 +32,8 @@ export class Lodash { * - 多个源对象依次合并到目标对象 * * 限制: - * - 仅处理普通对象 (Plain Object),不处理 Map/Set/Date/RegExp 等特殊对象 + * - 仅处理普通对象 (Plain Object),不处理 Date/RegExp 等特殊对象 + * - Map/Set 仅支持同类型合并,不递归内部值 * - 数组会被直接覆盖,不会合并数组元素 * - 不处理循环引用,可能导致栈溢出 * - 不复制 Symbol 属性和不可枚举属性 @@ -63,9 +64,29 @@ export class Lodash { // 递归合并对象 object[key] = Lodash.merge(targetValue, sourceValue); break; + case sourceValue instanceof Map && targetValue instanceof Map: + // 合并 Map(空 Map 跳过) + if (sourceValue.size > 0) { + for (const [k, v] of sourceValue) { + targetValue.set(k, v); + } + } + break; + case sourceValue instanceof Set && targetValue instanceof Set: + // 合并 Set(空 Set 跳过) + if (sourceValue.size > 0) { + for (const v of sourceValue) { + targetValue.add(v); + } + } + break; case Array.isArray(sourceValue) && sourceValue.length === 0 && targetValue !== undefined: // 空数组不覆盖已有值 break; + case (sourceValue instanceof Map && sourceValue.size === 0 && targetValue !== undefined): + case (sourceValue instanceof Set && sourceValue.size === 0 && targetValue !== undefined): + // 空 Map/Set 不覆盖已有值 + break; case sourceValue !== undefined: object[key] = sourceValue; break; diff --git a/test/merge.test.js b/test/merge.test.js index bc5561e..f860582 100644 --- a/test/merge.test.js +++ b/test/merge.test.js @@ -112,6 +112,88 @@ describe("Lodash.merge", () => { }); }); + describe("Map 处理", () => { + it("应该合并两个 Map", () => { + const target = { a: new Map([["x", 1], ["y", 2]]) }; + const source = { a: new Map([["y", 3], ["z", 4]]) }; + const result = _.merge(target, source); + assert.deepStrictEqual(result.a, new Map([["x", 1], ["y", 3], ["z", 4]])); + }); + + it("空 Map 不覆盖已有 Map", () => { + const targetMap = new Map([["x", 1]]); + const target = { a: targetMap }; + const source = { a: new Map() }; + const result = _.merge(target, source); + assert.strictEqual(result.a, targetMap); + assert.deepStrictEqual(result.a, new Map([["x", 1]])); + }); + + it("空 Map 不覆盖已有非 Map 值", () => { + const target = { a: { b: 1 } }; + const source = { a: new Map() }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: { b: 1 } }); + }); + + it("空 Map 可以赋值给 undefined 的目标属性", () => { + const target = { a: 1 }; + const source = { b: new Map() }; + const result = _.merge(target, source); + assert.ok(result.b instanceof Map); + assert.strictEqual(result.b.size, 0); + }); + + it("非空 Map 覆盖非 Map 值", () => { + const target = { a: { b: 1 } }; + const source = { a: new Map([["x", 1]]) }; + const result = _.merge(target, source); + assert.ok(result.a instanceof Map); + assert.deepStrictEqual(result.a, new Map([["x", 1]])); + }); + }); + + describe("Set 处理", () => { + it("应该合并两个 Set(取并集)", () => { + const target = { a: new Set([1, 2, 3]) }; + const source = { a: new Set([3, 4, 5]) }; + const result = _.merge(target, source); + assert.deepStrictEqual(result.a, new Set([1, 2, 3, 4, 5])); + }); + + it("空 Set 不覆盖已有 Set", () => { + const targetSet = new Set([1, 2]); + const target = { a: targetSet }; + const source = { a: new Set() }; + const result = _.merge(target, source); + assert.strictEqual(result.a, targetSet); + assert.deepStrictEqual(result.a, new Set([1, 2])); + }); + + it("空 Set 不覆盖已有非 Set 值", () => { + const target = { a: [1, 2, 3] }; + const source = { a: new Set() }; + const result = _.merge(target, source); + assert.deepStrictEqual(result, { a: [1, 2, 3] }); + }); + + it("空 Set 可以赋值给 undefined 的目标属性", () => { + const target = { a: 1 }; + const source = { b: new Set() }; + const result = _.merge(target, source); + assert.ok(result.b instanceof Set); + assert.strictEqual(result.b.size, 0); + }); + + it("非空 Set 覆盖非 Set 值", () => { + const target = { a: [1, 2, 3] }; + const source = { a: new Set([4, 5]) }; + const result = _.merge(target, source); + assert.ok(result.a instanceof Set); + assert.deepStrictEqual(result.a, new Set([4, 5])); + }); + }); + describe("特殊值处理", () => { it("目标对象为 null 时返回 null", () => { const result = _.merge(null, { a: 1 }); From 130c4d6a7083f20a5d16b9d77e281f698efc58b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 11:48:49 +0800 Subject: [PATCH 06/72] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20Lodash.merge=20=E7=9A=84=E5=AE=8C=E6=95=B4=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- test/{merge.test.js => Lodash.merge.test.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/{merge.test.js => Lodash.merge.test.js} (100%) diff --git a/package.json b/package.json index 33f595c..c612157 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "scripts": { "tsc:build": "npx tsc", "test": "node --test test/*.test.js", - "test:merge": "node --test test/merge.test.js" + "test:merge": "node --test test/Lodash.merge.test.js" }, "repository": { "type": "git", diff --git a/test/merge.test.js b/test/Lodash.merge.test.js similarity index 100% rename from test/merge.test.js rename to test/Lodash.merge.test.js From 89584911a746c908b3dbaea862a8de6cec707beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 12:17:55 +0800 Subject: [PATCH 07/72] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20NPM=20?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E5=B7=A5=E4=BD=9C=E6=B5=81=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=20Node.js=20=E7=89=88=E6=9C=AC=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=9E=84=E5=BB=BA=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-package-to-npm.yml | 28 +++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release-package-to-npm.yml b/.github/workflows/release-package-to-npm.yml index fdb3557..ad9dda8 100644 --- a/.github/workflows/release-package-to-npm.yml +++ b/.github/workflows/release-package-to-npm.yml @@ -6,23 +6,13 @@ on: tags: - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 16 - - run: npm ci - - run: npm test +permissions: + id-token: write # Required for OIDC + contents: read - publish-gpr: - needs: build +jobs: + publish: runs-on: ubuntu-latest - permissions: - packages: write - contents: read steps: - uses: actions/checkout@v4 - name: Update local package.json version from release tag @@ -33,9 +23,9 @@ jobs: ignore-semver-check: "false" # If set to "true", will not check if the version number is a valid semver version. - uses: actions/setup-node@v4 with: - node-version: 16 - registry-url: 'https://registry.npmjs.org' + node-version: "24" + registry-url: "https://registry.npmjs.org" - run: npm ci + - run: npm run build --if-present + - run: npm test - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From ec16fe75536d5ffa36620b212de29183171f33d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 12:32:23 +0800 Subject: [PATCH 08/72] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20getStorage?= =?UTF-8?q?=20=E5=87=BD=E6=95=B0=EF=BC=8C=E4=BD=BF=E7=94=A8=20=5F.merge=20?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=E5=AF=B9=E8=B1=A1=E5=B1=95=E5=BC=80=E4=BB=A5?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E8=AE=BE=E7=BD=AE=E5=92=8C=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- getStorage.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index 8aa6bc5..356403d 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -32,7 +32,7 @@ export function getStorage(key, names, database) { const argument = {}; Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); //Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); - Store.Settings = { ...Store.Settings, ...argument }; + _.merge(Store.Settings, argument); break; } case "undefined": @@ -44,14 +44,14 @@ export function getStorage(key, names, database) { // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 const BoxJs = Storage.getItem(key); if (BoxJs) { - //Console.debug("BoxJs", `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs || {})}`); + Console.debug("BoxJs", `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs || {})}`); names.forEach(name => { switch (typeof BoxJs?.[name]?.Settings) { // biome-ignore lint/suspicious/noFallthroughSwitchClause: case "string": BoxJs[name].Settings = JSON.parse(BoxJs[name].Settings || "{}"); case "object": - Store.Settings = { ...Store.Settings, ...BoxJs[name].Settings }; + _.merge(Store.Settings, BoxJs[name].Settings); break; case "undefined": break; @@ -61,13 +61,13 @@ export function getStorage(key, names, database) { case "string": BoxJs[name].Caches = JSON.parse(BoxJs[name].Caches || "{}"); case "object": - Store.Caches = { ...Store.Caches, ...BoxJs[name].Caches }; + _.merge(Store.Caches, BoxJs[name].Caches); break; case "undefined": break; } }); - //Console.debug("BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + Console.debug("BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); } /***************** traverseObject *****************/ traverseObject(Store.Settings, (key, value) => { @@ -81,7 +81,7 @@ export function getStorage(key, names, database) { } return value; }); - //Console.debug("✅ traverseObject", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + Console.debug("✅ traverseObject", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); return Store; } From 10b38fca08acb26b9c0602b246999fdef283aeef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 12:51:32 +0800 Subject: [PATCH 09/72] =?UTF-8?q?feat:=20=E7=A7=BB=E5=8A=A8=20Argument=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E5=88=B0=20getStorage=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E7=9A=84=E6=9C=AB=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- getStorage.mjs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index 356403d..e52668f 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -23,22 +23,6 @@ export function getStorage(key, names, database) { _.merge(Store.Configs, database?.[name]?.Configs); }); //Console.debug("Database", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - /***************** Argument *****************/ - switch (typeof $argument) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: - case "string": - $argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - case "object": { - const argument = {}; - Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); - //Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); - _.merge(Store.Settings, argument); - break; - } - case "undefined": - break; - } - //Console.debug("$argument", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** BoxJs *****************/ // 包装为局部变量,用完释放内存 // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 @@ -69,6 +53,22 @@ export function getStorage(key, names, database) { }); Console.debug("BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); } + /***************** Argument *****************/ + switch (typeof $argument) { + // biome-ignore lint/suspicious/noFallthroughSwitchClause: + case "string": + $argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + case "object": { + const argument = {}; + Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); + //Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); + _.merge(Store.Settings, argument); + break; + } + case "undefined": + break; + } + //Console.debug("$argument", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** traverseObject *****************/ traverseObject(Store.Settings, (key, value) => { //Console.debug("☑️ traverseObject", `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`); From c3232b141c7cf58f664edb775776963168444861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 13:03:07 +0800 Subject: [PATCH 10/72] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20getStorage?= =?UTF-8?q?=20=E5=87=BD=E6=95=B0=EF=BC=8C=E9=87=8D=E6=9E=84=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=A4=84=E7=90=86=E5=92=8C=E5=90=88=E5=B9=B6=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BB=A3=E7=A0=81=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update getStorage.mjs --- getStorage.mjs | 79 +++++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index e52668f..791bbb1 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -12,66 +12,52 @@ import { Storage } from "./polyfill/Storage.mjs"; * @return {object} { Settings, Caches, Configs } */ export function getStorage(key, names, database) { - names = [names].flat(Number.POSITIVE_INFINITY); - //Console.log("☑️ getStorage"); + Console.debug("☑️ getStorage"); /***************** Default *****************/ const Store = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; - //Console.debug("Default", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - /***************** Database *****************/ - names.forEach(name => { - _.merge(Store.Settings, database?.[name]?.Settings); - _.merge(Store.Configs, database?.[name]?.Configs); - }); - //Console.debug("Database", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - /***************** BoxJs *****************/ - // 包装为局部变量,用完释放内存 - // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 - const BoxJs = Storage.getItem(key); - if (BoxJs) { - Console.debug("BoxJs", `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs || {})}`); - names.forEach(name => { - switch (typeof BoxJs?.[name]?.Settings) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: - case "string": - BoxJs[name].Settings = JSON.parse(BoxJs[name].Settings || "{}"); - case "object": - _.merge(Store.Settings, BoxJs[name].Settings); - break; - case "undefined": - break; - } - switch (typeof BoxJs?.[name]?.Caches) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: - case "string": - BoxJs[name].Caches = JSON.parse(BoxJs[name].Caches || "{}"); - case "object": - _.merge(Store.Caches, BoxJs[name].Caches); - break; - case "undefined": - break; - } - }); - Console.debug("BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - } + Console.debug("Default", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** Argument *****************/ + Console.debug(`☑️ $argument`); + const argument = {}; switch (typeof $argument) { // biome-ignore lint/suspicious/noFallthroughSwitchClause: case "string": $argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - case "object": { - const argument = {}; + case "object": Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); - //Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); - _.merge(Store.Settings, argument); break; - } case "undefined": break; } - //Console.debug("$argument", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); + /***************** BoxJs *****************/ + // 包装为局部变量,用完释放内存 + // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 + const BoxJs = Storage.getItem(key); + if (BoxJs) { + Console.debug("☑️ BoxJs", `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs || {})}`); + names.forEach(name => { + if (typeof BoxJs?.[name]?.Settings === "string") { + BoxJs[name].Settings = JSON.parse(BoxJs[name].Settings || "{}"); + } + if (typeof BoxJs?.[name]?.Caches === "string") { + BoxJs[name].Caches = JSON.parse(BoxJs[name].Caches || "{}"); + } + }); + Console.debug("✅ BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + } + /***************** Merge *****************/ + names = [names].flat(Number.POSITIVE_INFINITY); + names.forEach(name => { + _.merge(Store.Settings, database?.[name]?.Settings, BoxJs?.[name]?.Settings); + _.merge(Store.Configs, database?.[name]?.Configs); + _.merge(Store.Caches, BoxJs?.[name]?.Caches); + }); + _.merge(Store.Settings, argument); + Console.debug("✅ Merge", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** traverseObject *****************/ traverseObject(Store.Settings, (key, value) => { - //Console.debug("☑️ traverseObject", `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`); + Console.debug("☑️ traverseObject", `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`); if (value === "true" || value === "false") value = JSON.parse(value); // 字符串转Boolean else if (typeof value === "string") { @@ -82,6 +68,7 @@ export function getStorage(key, names, database) { return value; }); Console.debug("✅ traverseObject", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + Console.debug("✅ getStorage"); return Store; } From 096204ef722977c37fc2ea7f3a1f59ad3d0cc833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 13:08:38 +0800 Subject: [PATCH 11/72] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20LogLevel=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8C=E5=85=81=E8=AE=B8=E4=BB=8E=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=92=8C=20BoxJs=20=E4=B8=AD=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- getStorage.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/getStorage.mjs b/getStorage.mjs index 791bbb1..86c461a 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -29,6 +29,7 @@ export function getStorage(key, names, database) { case "undefined": break; } + if (argument.LogLevel) Console.logLevel = argument.LogLevel; Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); /***************** BoxJs *****************/ // 包装为局部变量,用完释放内存 @@ -44,6 +45,7 @@ export function getStorage(key, names, database) { BoxJs[name].Caches = JSON.parse(BoxJs[name].Caches || "{}"); } }); + if (BoxJs.LogLevel) Console.logLevel = BoxJs.LogLevel; Console.debug("✅ BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); } /***************** Merge *****************/ @@ -54,6 +56,7 @@ export function getStorage(key, names, database) { _.merge(Store.Caches, BoxJs?.[name]?.Caches); }); _.merge(Store.Settings, argument); + if (Store.Settings.LogLevel) Console.logLevel = Store.Settings.LogLevel; Console.debug("✅ Merge", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** traverseObject *****************/ traverseObject(Store.Settings, (key, value) => { From c358f1e9ca227efa618486bf3c9fefd46bdda7a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 13:17:42 +0800 Subject: [PATCH 12/72] =?UTF-8?q?fiX:=20=E4=BF=AE=E5=A4=8D=20getStorage=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E7=9A=84=20names=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- getStorage.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/getStorage.mjs b/getStorage.mjs index 86c461a..673a66e 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -13,6 +13,7 @@ import { Storage } from "./polyfill/Storage.mjs"; */ export function getStorage(key, names, database) { Console.debug("☑️ getStorage"); + names = [names].flat(Number.POSITIVE_INFINITY); /***************** Default *****************/ const Store = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; Console.debug("Default", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); @@ -49,7 +50,6 @@ export function getStorage(key, names, database) { Console.debug("✅ BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); } /***************** Merge *****************/ - names = [names].flat(Number.POSITIVE_INFINITY); names.forEach(name => { _.merge(Store.Settings, database?.[name]?.Settings, BoxJs?.[name]?.Settings); _.merge(Store.Configs, database?.[name]?.Configs); From 70123a36efa8eafe27fa2932939c2ca6276f06ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sun, 1 Feb 2026 13:21:26 +0800 Subject: [PATCH 13/72] =?UTF-8?q?feat:=20=E5=9C=A8=20getStorage=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=B8=AD=E6=B7=BB=E5=8A=A0=E5=AF=B9=20LogLev?= =?UTF-8?q?el=20=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB=E6=AD=A3=E7=A1=AE=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update getStorage.mjs --- getStorage.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/getStorage.mjs b/getStorage.mjs index 673a66e..823738c 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -12,6 +12,7 @@ import { Storage } from "./polyfill/Storage.mjs"; * @return {object} { Settings, Caches, Configs } */ export function getStorage(key, names, database) { + if (database?.Default?.Settings?.LogLevel) Console.logLevel = database.Default.Settings.LogLevel; Console.debug("☑️ getStorage"); names = [names].flat(Number.POSITIVE_INFINITY); /***************** Default *****************/ From ec7b4b8e116490c44667516dee2655a47d60bf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Mon, 2 Feb 2026 16:54:30 +0800 Subject: [PATCH 14/72] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20package.json?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=20deprecate=20=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E6=AD=A3=20repository=20URL=20=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c612157..c29a04a 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,12 @@ "scripts": { "tsc:build": "npx tsc", "test": "node --test test/*.test.js", - "test:merge": "node --test test/Lodash.merge.test.js" + "test:merge": "node --test test/Lodash.merge.test.js", + "deprecate": "npm deprecate -f '@nsnanocat/util@0.0.0-preview' \"this package has been deprecated\"" }, "repository": { "type": "git", - "url": "https://github.com/NSNanoCat/util.git" + "url": "git+https://github.com/NSNanoCat/util.git" }, "files": [ "index.js", @@ -32,5 +33,9 @@ ], "devDependencies": { "typescript": "^5.6.3" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" } } From 3d23ec0b561b9f78b4d37c8ba8fda6f3795208ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:46:22 +0000 Subject: [PATCH 15/72] chore: plan argument module extraction Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- package-lock.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7860e7..5def421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,19 +6,10 @@ "": { "name": "@nsnanocat/util", "license": "Apache-2.0", - "dependencies": { - "pako": "^2.1.0" - }, "devDependencies": { "typescript": "^5.6.3" } }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "license": "(MIT AND Zlib)" - }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", From 538ba6893c8bba095d47f0dee34fef02cf2056ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:51:03 +0000 Subject: [PATCH 16/72] refactor: extract argument handler Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- getStorage.mjs | 16 ++-------------- index.js | 1 + lib/argument.mjs | 24 ++++++++++++++++++++++++ lib/index.js | 1 + package-lock.json | 9 +++++++++ test/argument.test.js | 15 +++++++++++++++ 6 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 lib/argument.mjs create mode 100644 test/argument.test.js diff --git a/getStorage.mjs b/getStorage.mjs index 823738c..ef982d7 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -1,3 +1,4 @@ +import { argument as parseArgument } from "./lib/argument.mjs"; import { Console } from "./polyfill/Console.mjs"; import { Lodash as _ } from "./polyfill/Lodash.mjs"; import { Storage } from "./polyfill/Storage.mjs"; @@ -19,20 +20,7 @@ export function getStorage(key, names, database) { const Store = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; Console.debug("Default", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** Argument *****************/ - Console.debug(`☑️ $argument`); - const argument = {}; - switch (typeof $argument) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: - case "string": - $argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - case "object": - Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); - break; - case "undefined": - break; - } - if (argument.LogLevel) Console.logLevel = argument.LogLevel; - Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); + const argument = parseArgument(); /***************** BoxJs *****************/ // 包装为局部变量,用完释放内存 // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 diff --git a/index.js b/index.js index 84c68c3..1aaa932 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ export * from "./lib/app.mjs"; +export * from "./lib/argument.mjs"; export * from "./lib/done.mjs"; export * from "./lib/notification.mjs"; export * from "./lib/time.mjs"; diff --git a/lib/argument.mjs b/lib/argument.mjs new file mode 100644 index 0000000..c0e11dd --- /dev/null +++ b/lib/argument.mjs @@ -0,0 +1,24 @@ +import { Console } from "../polyfill/Console.mjs"; +import { Lodash as _ } from "../polyfill/Lodash.mjs"; + +export function argument(value) { + Console.debug(`☑️ $argument`); + const argument = {}; + let target = value; + const useGlobalArgument = typeof target === "undefined" && typeof $argument !== "undefined"; + if (useGlobalArgument) target = $argument; + switch (typeof target) { + // biome-ignore lint/suspicious/noFallthroughSwitchClause: + case "string": + target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + if (useGlobalArgument) $argument = target; + case "object": + Object.keys(target).forEach(key => _.set(argument, key, target[key])); + break; + case "undefined": + break; + } + if (argument.LogLevel) Console.logLevel = argument.LogLevel; + Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); + return argument; +} diff --git a/lib/index.js b/lib/index.js index c73a06f..acdf370 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,4 +1,5 @@ export * from "./app.mjs"; +export * from "./argument.mjs"; export * from "./done.mjs"; export * from "./notification.mjs"; export * from "./time.mjs"; diff --git a/package-lock.json b/package-lock.json index 5def421..c7860e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,19 @@ "": { "name": "@nsnanocat/util", "license": "Apache-2.0", + "dependencies": { + "pako": "^2.1.0" + }, "devDependencies": { "typescript": "^5.6.3" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", diff --git a/test/argument.test.js b/test/argument.test.js new file mode 100644 index 0000000..f9e1ebe --- /dev/null +++ b/test/argument.test.js @@ -0,0 +1,15 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { argument } from "../index.js"; + +describe("argument", () => { + it("应该解析字符串参数", () => { + const result = argument("foo=bar&count=1"); + assert.deepStrictEqual(result, { foo: "bar", count: "1" }); + }); + + it("应该处理对象参数", () => { + const result = argument({ "nested.value": "ok" }); + assert.deepStrictEqual(result, { nested: { value: "ok" } }); + }); +}); From 96f6dd182152f70453a1bce9fab541dc40ed0c14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:52:48 +0000 Subject: [PATCH 17/72] test: cover argument edge cases Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- test/argument.test.js | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index c0e11dd..f4b207d 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -8,7 +8,7 @@ export function argument(value) { const useGlobalArgument = typeof target === "undefined" && typeof $argument !== "undefined"; if (useGlobalArgument) target = $argument; switch (typeof target) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: + // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects. case "string": target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); if (useGlobalArgument) $argument = target; diff --git a/test/argument.test.js b/test/argument.test.js index f9e1ebe..28612b5 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -12,4 +12,17 @@ describe("argument", () => { const result = argument({ "nested.value": "ok" }); assert.deepStrictEqual(result, { nested: { value: "ok" } }); }); + + it("应该处理未定义参数", () => { + const result = argument(); + assert.deepStrictEqual(result, {}); + }); + + it("应该支持全局 $argument", () => { + globalThis.$argument = "mode=on"; + const result = argument(); + assert.deepStrictEqual(result, { mode: "on" }); + assert.deepStrictEqual(globalThis.$argument, { mode: "on" }); + delete globalThis.$argument; + }); }); From fa54b090f769940d9b8807bfdc526e5ec503b66f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:10:05 +0000 Subject: [PATCH 18/72] refactor: make argument a computed property Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- getStorage.mjs | 4 +--- lib/argument.mjs | 26 +++++++++++--------------- test/argument.test.js | 28 ++++++++++++++++++---------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index ef982d7..ce61264 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -1,4 +1,4 @@ -import { argument as parseArgument } from "./lib/argument.mjs"; +import { argument } from "./lib/argument.mjs"; import { Console } from "./polyfill/Console.mjs"; import { Lodash as _ } from "./polyfill/Lodash.mjs"; import { Storage } from "./polyfill/Storage.mjs"; @@ -19,8 +19,6 @@ export function getStorage(key, names, database) { /***************** Default *****************/ const Store = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; Console.debug("Default", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - /***************** Argument *****************/ - const argument = parseArgument(); /***************** BoxJs *****************/ // 包装为局部变量,用完释放内存 // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 diff --git a/lib/argument.mjs b/lib/argument.mjs index f4b207d..e3cbc9c 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -1,24 +1,20 @@ import { Console } from "../polyfill/Console.mjs"; import { Lodash as _ } from "../polyfill/Lodash.mjs"; -export function argument(value) { +const parseArgument = () => { Console.debug(`☑️ $argument`); const argument = {}; - let target = value; - const useGlobalArgument = typeof target === "undefined" && typeof $argument !== "undefined"; - if (useGlobalArgument) target = $argument; - switch (typeof target) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects. - case "string": - target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - if (useGlobalArgument) $argument = target; - case "object": - Object.keys(target).forEach(key => _.set(argument, key, target[key])); - break; - case "undefined": - break; + let target = typeof globalThis.$argument === "undefined" ? undefined : globalThis.$argument; + if (typeof target === "string") { + target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + globalThis.$argument = target; + } + if (typeof target === "object") { + Object.keys(target).forEach(key => _.set(argument, key, target[key])); } if (argument.LogLevel) Console.logLevel = argument.LogLevel; Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); return argument; -} +}; + +export const argument = parseArgument(); diff --git a/test/argument.test.js b/test/argument.test.js index 28612b5..1eecef9 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -1,26 +1,34 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { argument } from "../index.js"; + +let importSeed = 0; +const loadArgument = async value => { + if (typeof value === "undefined") delete globalThis.$argument; + else globalThis.$argument = value; + importSeed += 1; + const { argument } = await import(`../lib/argument.mjs?test=${importSeed}`); + return argument; +}; describe("argument", () => { - it("应该解析字符串参数", () => { - const result = argument("foo=bar&count=1"); + it("应该解析字符串参数", async () => { + const result = await loadArgument("foo=bar&count=1"); assert.deepStrictEqual(result, { foo: "bar", count: "1" }); + assert.deepStrictEqual(globalThis.$argument, { foo: "bar", count: "1" }); }); - it("应该处理对象参数", () => { - const result = argument({ "nested.value": "ok" }); + it("应该处理对象参数", async () => { + const result = await loadArgument({ "nested.value": "ok" }); assert.deepStrictEqual(result, { nested: { value: "ok" } }); }); - it("应该处理未定义参数", () => { - const result = argument(); + it("应该处理未定义参数", async () => { + const result = await loadArgument(); assert.deepStrictEqual(result, {}); }); - it("应该支持全局 $argument", () => { - globalThis.$argument = "mode=on"; - const result = argument(); + it("应该支持全局 $argument", async () => { + const result = await loadArgument("mode=on"); assert.deepStrictEqual(result, { mode: "on" }); assert.deepStrictEqual(globalThis.$argument, { mode: "on" }); delete globalThis.$argument; From 15ee911bb9ee21fa8d2cef2bb586c6501aa37107 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:10:48 +0000 Subject: [PATCH 19/72] test: stabilize argument parsing Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- test/argument.test.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index e3cbc9c..a57ee00 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -9,7 +9,7 @@ const parseArgument = () => { target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); globalThis.$argument = target; } - if (typeof target === "object") { + if (target && typeof target === "object") { Object.keys(target).forEach(key => _.set(argument, key, target[key])); } if (argument.LogLevel) Console.logLevel = argument.LogLevel; diff --git a/test/argument.test.js b/test/argument.test.js index 1eecef9..82d6ae3 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -1,5 +1,5 @@ import assert from "node:assert"; -import { describe, it } from "node:test"; +import { afterEach, describe, it } from "node:test"; let importSeed = 0; const loadArgument = async value => { @@ -11,6 +11,10 @@ const loadArgument = async value => { }; describe("argument", () => { + afterEach(() => { + delete globalThis.$argument; + }); + it("应该解析字符串参数", async () => { const result = await loadArgument("foo=bar&count=1"); assert.deepStrictEqual(result, { foo: "bar", count: "1" }); @@ -31,6 +35,5 @@ describe("argument", () => { const result = await loadArgument("mode=on"); assert.deepStrictEqual(result, { mode: "on" }); assert.deepStrictEqual(globalThis.$argument, { mode: "on" }); - delete globalThis.$argument; }); }); From 76b5abc05c4837b7f859e24061250ab8934b1dd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:11:38 +0000 Subject: [PATCH 20/72] chore: simplify argument parsing Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- test/argument.test.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index a57ee00..b3698d6 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,7 +4,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; const parseArgument = () => { Console.debug(`☑️ $argument`); const argument = {}; - let target = typeof globalThis.$argument === "undefined" ? undefined : globalThis.$argument; + let target = globalThis.$argument; if (typeof target === "string") { target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); globalThis.$argument = target; diff --git a/test/argument.test.js b/test/argument.test.js index 82d6ae3..d280fdc 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -18,7 +18,6 @@ describe("argument", () => { it("应该解析字符串参数", async () => { const result = await loadArgument("foo=bar&count=1"); assert.deepStrictEqual(result, { foo: "bar", count: "1" }); - assert.deepStrictEqual(globalThis.$argument, { foo: "bar", count: "1" }); }); it("应该处理对象参数", async () => { @@ -34,6 +33,5 @@ describe("argument", () => { it("应该支持全局 $argument", async () => { const result = await loadArgument("mode=on"); assert.deepStrictEqual(result, { mode: "on" }); - assert.deepStrictEqual(globalThis.$argument, { mode: "on" }); }); }); From 17848c7dba5a8b3e481e0efb3a1a22986a4a910e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:12:17 +0000 Subject: [PATCH 21/72] test: harden argument imports Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- test/argument.test.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index b3698d6..4aa6a81 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,7 +4,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; const parseArgument = () => { Console.debug(`☑️ $argument`); const argument = {}; - let target = globalThis.$argument; + let target = globalThis?.$argument; if (typeof target === "string") { target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); globalThis.$argument = target; diff --git a/test/argument.test.js b/test/argument.test.js index d280fdc..2515328 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -2,11 +2,12 @@ import assert from "node:assert"; import { afterEach, describe, it } from "node:test"; let importSeed = 0; +const argumentModule = new URL("../lib/argument.mjs", import.meta.url); const loadArgument = async value => { if (typeof value === "undefined") delete globalThis.$argument; else globalThis.$argument = value; importSeed += 1; - const { argument } = await import(`../lib/argument.mjs?test=${importSeed}`); + const { argument } = await import(`${argumentModule}?test=${importSeed}`); return argument; }; From 6e6da14d921a93f58766a3f9063689ab527f2ebb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:13:07 +0000 Subject: [PATCH 22/72] test: clarify argument helper Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- test/argument.test.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 4aa6a81..7a24606 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,7 +4,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; const parseArgument = () => { Console.debug(`☑️ $argument`); const argument = {}; - let target = globalThis?.$argument; +let target = globalThis.$argument; if (typeof target === "string") { target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); globalThis.$argument = target; diff --git a/test/argument.test.js b/test/argument.test.js index 2515328..2c30126 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -3,7 +3,7 @@ import { afterEach, describe, it } from "node:test"; let importSeed = 0; const argumentModule = new URL("../lib/argument.mjs", import.meta.url); -const loadArgument = async value => { +const importArgumentWithGlobal = async value => { if (typeof value === "undefined") delete globalThis.$argument; else globalThis.$argument = value; importSeed += 1; @@ -17,22 +17,22 @@ describe("argument", () => { }); it("应该解析字符串参数", async () => { - const result = await loadArgument("foo=bar&count=1"); + const result = await importArgumentWithGlobal("foo=bar&count=1"); assert.deepStrictEqual(result, { foo: "bar", count: "1" }); }); it("应该处理对象参数", async () => { - const result = await loadArgument({ "nested.value": "ok" }); + const result = await importArgumentWithGlobal({ "nested.value": "ok" }); assert.deepStrictEqual(result, { nested: { value: "ok" } }); }); it("应该处理未定义参数", async () => { - const result = await loadArgument(); + const result = await importArgumentWithGlobal(); assert.deepStrictEqual(result, {}); }); it("应该支持全局 $argument", async () => { - const result = await loadArgument("mode=on"); + const result = await importArgumentWithGlobal("mode=on"); assert.deepStrictEqual(result, { mode: "on" }); }); }); From 925b12db63a54a4f50fa810dd884136f256d0a9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:13:55 +0000 Subject: [PATCH 23/72] style: fix argument indentation Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 7a24606..b3698d6 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,7 +4,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; const parseArgument = () => { Console.debug(`☑️ $argument`); const argument = {}; -let target = globalThis.$argument; + let target = globalThis.$argument; if (typeof target === "string") { target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); globalThis.$argument = target; From 66298d492288e3619117fc965fbc4b969d97e4a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:14:49 +0000 Subject: [PATCH 24/72] test: assert argument side effects Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 14 ++++++++------ test/argument.test.js | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index b3698d6..fa7500e 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -5,12 +5,14 @@ const parseArgument = () => { Console.debug(`☑️ $argument`); const argument = {}; let target = globalThis.$argument; - if (typeof target === "string") { - target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - globalThis.$argument = target; - } - if (target && typeof target === "object") { - Object.keys(target).forEach(key => _.set(argument, key, target[key])); + if (typeof target !== "undefined") { + if (typeof target === "string") { + target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + globalThis.$argument = target; + } + if (target && typeof target === "object") { + Object.keys(target).forEach(key => _.set(argument, key, target[key])); + } } if (argument.LogLevel) Console.logLevel = argument.LogLevel; Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); diff --git a/test/argument.test.js b/test/argument.test.js index 2c30126..9360110 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -34,5 +34,6 @@ describe("argument", () => { it("应该支持全局 $argument", async () => { const result = await importArgumentWithGlobal("mode=on"); assert.deepStrictEqual(result, { mode: "on" }); + assert.deepStrictEqual(globalThis.$argument, { mode: "on" }); }); }); From 75f576865317121d44e89a6ab08a804f944d163d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:15:46 +0000 Subject: [PATCH 25/72] test: clarify argument handling Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 1 + test/argument.test.js | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index fa7500e..2898464 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -11,6 +11,7 @@ const parseArgument = () => { globalThis.$argument = target; } if (target && typeof target === "object") { + // 数组按对象处理以支持索引键。 Object.keys(target).forEach(key => _.set(argument, key, target[key])); } } diff --git a/test/argument.test.js b/test/argument.test.js index 9360110..cffd7b0 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -3,7 +3,7 @@ import { afterEach, describe, it } from "node:test"; let importSeed = 0; const argumentModule = new URL("../lib/argument.mjs", import.meta.url); -const importArgumentWithGlobal = async value => { +const importWithArgument = async value => { if (typeof value === "undefined") delete globalThis.$argument; else globalThis.$argument = value; importSeed += 1; @@ -17,22 +17,22 @@ describe("argument", () => { }); it("应该解析字符串参数", async () => { - const result = await importArgumentWithGlobal("foo=bar&count=1"); + const result = await importWithArgument("foo=bar&count=1"); assert.deepStrictEqual(result, { foo: "bar", count: "1" }); }); it("应该处理对象参数", async () => { - const result = await importArgumentWithGlobal({ "nested.value": "ok" }); + const result = await importWithArgument({ "nested.value": "ok" }); assert.deepStrictEqual(result, { nested: { value: "ok" } }); }); it("应该处理未定义参数", async () => { - const result = await importArgumentWithGlobal(); + const result = await importWithArgument(); assert.deepStrictEqual(result, {}); }); it("应该支持全局 $argument", async () => { - const result = await importArgumentWithGlobal("mode=on"); + const result = await importWithArgument("mode=on"); assert.deepStrictEqual(result, { mode: "on" }); assert.deepStrictEqual(globalThis.$argument, { mode: "on" }); }); From cabaff8a096d3ef5163e74b7f15c42b19abc1460 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:16:39 +0000 Subject: [PATCH 26/72] test: cover object argument path Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- test/argument.test.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 2898464..df1b0cb 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -10,7 +10,7 @@ const parseArgument = () => { target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); globalThis.$argument = target; } - if (target && typeof target === "object") { + if (target !== null && typeof target === "object") { // 数组按对象处理以支持索引键。 Object.keys(target).forEach(key => _.set(argument, key, target[key])); } diff --git a/test/argument.test.js b/test/argument.test.js index cffd7b0..37715bd 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -24,6 +24,7 @@ describe("argument", () => { it("应该处理对象参数", async () => { const result = await importWithArgument({ "nested.value": "ok" }); assert.deepStrictEqual(result, { nested: { value: "ok" } }); + assert.deepStrictEqual(globalThis.$argument, { "nested.value": "ok" }); }); it("应该处理未定义参数", async () => { From ca52bda9ae0b1fbb635666d378d16e69398461ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:31:49 +0000 Subject: [PATCH 27/72] test: cover quoted argument values Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- test/argument.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/argument.test.js b/test/argument.test.js index 37715bd..77367da 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -21,6 +21,16 @@ describe("argument", () => { assert.deepStrictEqual(result, { foo: "bar", count: "1" }); }); + it("应该解析点号路径参数", async () => { + const result = await importWithArgument("a.b.c=123&a.d=456"); + assert.deepStrictEqual(result, { a: { b: { c: "123" }, d: "456" } }); + }); + + it("应该解析带双引号的参数值", async () => { + const result = await importWithArgument('a.b.c="[1,2,3]"&a.d="456"'); + assert.deepStrictEqual(result, { a: { b: { c: "[1,2,3]" }, d: "456" } }); + }); + it("应该处理对象参数", async () => { const result = await importWithArgument({ "nested.value": "ok" }); assert.deepStrictEqual(result, { nested: { value: "ok" } }); From 241f653d083fa17ef12609664402d71946c772f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:46:10 +0000 Subject: [PATCH 28/72] refactor: mutate $argument in parser Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- getStorage.mjs | 4 ++-- lib/argument.mjs | 15 ++++++++------- test/argument.test.js | 12 ++++++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index ce61264..e35c172 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -1,4 +1,4 @@ -import { argument } from "./lib/argument.mjs"; +import { $argument } from "./lib/argument.mjs"; import { Console } from "./polyfill/Console.mjs"; import { Lodash as _ } from "./polyfill/Lodash.mjs"; import { Storage } from "./polyfill/Storage.mjs"; @@ -42,7 +42,7 @@ export function getStorage(key, names, database) { _.merge(Store.Configs, database?.[name]?.Configs); _.merge(Store.Caches, BoxJs?.[name]?.Caches); }); - _.merge(Store.Settings, argument); + _.merge(Store.Settings, $argument); if (Store.Settings.LogLevel) Console.logLevel = Store.Settings.LogLevel; Console.debug("✅ Merge", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** traverseObject *****************/ diff --git a/lib/argument.mjs b/lib/argument.mjs index df1b0cb..426f872 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -3,21 +3,22 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; const parseArgument = () => { Console.debug(`☑️ $argument`); - const argument = {}; let target = globalThis.$argument; if (typeof target !== "undefined") { if (typeof target === "string") { target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - globalThis.$argument = target; } if (target !== null && typeof target === "object") { + const parsed = {}; // 数组按对象处理以支持索引键。 - Object.keys(target).forEach(key => _.set(argument, key, target[key])); + Object.keys(target).forEach(key => _.set(parsed, key, target[key])); + target = parsed; } + globalThis.$argument = target; } - if (argument.LogLevel) Console.logLevel = argument.LogLevel; - Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); - return argument; + if (target && typeof target === "object" && target.LogLevel) Console.logLevel = target.LogLevel; + Console.debug(`✅ $argument`, `argument: ${JSON.stringify(target || {})}`); + return target; }; -export const argument = parseArgument(); +export const $argument = parseArgument(); diff --git a/test/argument.test.js b/test/argument.test.js index 77367da..f324e45 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -7,8 +7,8 @@ const importWithArgument = async value => { if (typeof value === "undefined") delete globalThis.$argument; else globalThis.$argument = value; importSeed += 1; - const { argument } = await import(`${argumentModule}?test=${importSeed}`); - return argument; + const { $argument } = await import(`${argumentModule}?test=${importSeed}`); + return $argument; }; describe("argument", () => { @@ -19,27 +19,31 @@ describe("argument", () => { it("应该解析字符串参数", async () => { const result = await importWithArgument("foo=bar&count=1"); assert.deepStrictEqual(result, { foo: "bar", count: "1" }); + assert.deepStrictEqual(globalThis.$argument, { foo: "bar", count: "1" }); }); it("应该解析点号路径参数", async () => { const result = await importWithArgument("a.b.c=123&a.d=456"); assert.deepStrictEqual(result, { a: { b: { c: "123" }, d: "456" } }); + assert.deepStrictEqual(globalThis.$argument, { a: { b: { c: "123" }, d: "456" } }); }); it("应该解析带双引号的参数值", async () => { const result = await importWithArgument('a.b.c="[1,2,3]"&a.d="456"'); assert.deepStrictEqual(result, { a: { b: { c: "[1,2,3]" }, d: "456" } }); + assert.deepStrictEqual(globalThis.$argument, { a: { b: { c: "[1,2,3]" }, d: "456" } }); }); it("应该处理对象参数", async () => { const result = await importWithArgument({ "nested.value": "ok" }); assert.deepStrictEqual(result, { nested: { value: "ok" } }); - assert.deepStrictEqual(globalThis.$argument, { "nested.value": "ok" }); + assert.deepStrictEqual(globalThis.$argument, { nested: { value: "ok" } }); }); it("应该处理未定义参数", async () => { const result = await importWithArgument(); - assert.deepStrictEqual(result, {}); + assert.strictEqual(result, undefined); + assert.strictEqual(globalThis.$argument, undefined); }); it("应该支持全局 $argument", async () => { From f859d29509809ee1d9c37b1cf29bd6b52c1276eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:46:52 +0000 Subject: [PATCH 29/72] chore: align $argument logging Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 426f872..9c05ac0 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -16,8 +16,8 @@ const parseArgument = () => { } globalThis.$argument = target; } - if (target && typeof target === "object" && target.LogLevel) Console.logLevel = target.LogLevel; - Console.debug(`✅ $argument`, `argument: ${JSON.stringify(target || {})}`); + if (target !== null && typeof target === "object" && target.LogLevel) Console.logLevel = target.LogLevel; + Console.debug(`✅ $argument`, `$argument: ${JSON.stringify(target || {})}`); return target; }; From 48429a515dd9ba2b476b665569fed917bbeb3e50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:47:36 +0000 Subject: [PATCH 30/72] chore: simplify $argument logging Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 9c05ac0..8ac78ff 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -16,8 +16,8 @@ const parseArgument = () => { } globalThis.$argument = target; } - if (target !== null && typeof target === "object" && target.LogLevel) Console.logLevel = target.LogLevel; - Console.debug(`✅ $argument`, `$argument: ${JSON.stringify(target || {})}`); + if (target?.LogLevel) Console.logLevel = target.LogLevel; + Console.debug(`✅ $argument`, `$argument: ${JSON.stringify(target)}`); return target; }; From 200441e796ccb33c274ebfb963c64c4600cf19bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:16:30 +0000 Subject: [PATCH 31/72] refactor: auto-run argument parsing Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- getStorage.mjs | 4 ++-- lib/argument.mjs | 9 +++------ test/argument.test.js | 8 ++++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index e35c172..ba64e9d 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -1,4 +1,4 @@ -import { $argument } from "./lib/argument.mjs"; +import "./lib/argument.mjs"; import { Console } from "./polyfill/Console.mjs"; import { Lodash as _ } from "./polyfill/Lodash.mjs"; import { Storage } from "./polyfill/Storage.mjs"; @@ -42,7 +42,7 @@ export function getStorage(key, names, database) { _.merge(Store.Configs, database?.[name]?.Configs); _.merge(Store.Caches, BoxJs?.[name]?.Caches); }); - _.merge(Store.Settings, $argument); + _.merge(Store.Settings, globalThis.$argument); if (Store.Settings.LogLevel) Console.logLevel = Store.Settings.LogLevel; Console.debug("✅ Merge", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** traverseObject *****************/ diff --git a/lib/argument.mjs b/lib/argument.mjs index 8ac78ff..520a3c2 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -1,7 +1,7 @@ import { Console } from "../polyfill/Console.mjs"; import { Lodash as _ } from "../polyfill/Lodash.mjs"; -const parseArgument = () => { +(() => { Console.debug(`☑️ $argument`); let target = globalThis.$argument; if (typeof target !== "undefined") { @@ -14,11 +14,8 @@ const parseArgument = () => { Object.keys(target).forEach(key => _.set(parsed, key, target[key])); target = parsed; } - globalThis.$argument = target; } + globalThis.$argument = target; if (target?.LogLevel) Console.logLevel = target.LogLevel; Console.debug(`✅ $argument`, `$argument: ${JSON.stringify(target)}`); - return target; -}; - -export const $argument = parseArgument(); +})(); diff --git a/test/argument.test.js b/test/argument.test.js index f324e45..348bd92 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -4,16 +4,16 @@ import { afterEach, describe, it } from "node:test"; let importSeed = 0; const argumentModule = new URL("../lib/argument.mjs", import.meta.url); const importWithArgument = async value => { - if (typeof value === "undefined") delete globalThis.$argument; + if (typeof value === "undefined") globalThis.$argument = undefined; else globalThis.$argument = value; importSeed += 1; - const { $argument } = await import(`${argumentModule}?test=${importSeed}`); - return $argument; + await import(`${argumentModule}?test=${importSeed}`); + return globalThis.$argument; }; describe("argument", () => { afterEach(() => { - delete globalThis.$argument; + globalThis.$argument = undefined; }); it("应该解析字符串参数", async () => { From 28f6c61597c5611d17012bb1c866fceb2b3e2f28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:10:21 +0000 Subject: [PATCH 32/72] refactor: restore switch parsing for $argument Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 520a3c2..7fbcd0b 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,16 +4,20 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; (() => { Console.debug(`☑️ $argument`); let target = globalThis.$argument; - if (typeof target !== "undefined") { - if (typeof target === "string") { + switch (typeof target) { + // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects. + case "string": target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - } - if (target !== null && typeof target === "object") { - const parsed = {}; - // 数组按对象处理以支持索引键。 - Object.keys(target).forEach(key => _.set(parsed, key, target[key])); - target = parsed; - } + case "object": + if (target !== null) { + const parsed = {}; + // 数组按对象处理以支持索引键。 + Object.keys(target).forEach(key => _.set(parsed, key, target[key])); + target = parsed; + } + break; + case "undefined": + break; } globalThis.$argument = target; if (target?.LogLevel) Console.logLevel = target.LogLevel; From 99ec58b5e50d15b786cb658bd00056b5abeed175 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:11:07 +0000 Subject: [PATCH 33/72] style: clarify switch fallthrough Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 7fbcd0b..58cccd5 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -8,6 +8,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects. case "string": target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + // fallthrough case "object": if (target !== null) { const parsed = {}; @@ -16,8 +17,6 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; target = parsed; } break; - case "undefined": - break; } globalThis.$argument = target; if (target?.LogLevel) Console.logLevel = target.LogLevel; From b6574bce2fac2e12aaf68b33d7de355cfa26823c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:11:45 +0000 Subject: [PATCH 34/72] style: streamline switch comments Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 58cccd5..11b28bb 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -8,7 +8,6 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects. case "string": target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - // fallthrough case "object": if (target !== null) { const parsed = {}; @@ -17,6 +16,8 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; target = parsed; } break; + default: + break; } globalThis.$argument = target; if (target?.LogLevel) Console.logLevel = target.LogLevel; From 31b2135bd514972b9b5ffe1d09438fa9266747c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:12:41 +0000 Subject: [PATCH 35/72] style: clarify switch fallthrough intent Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 11b28bb..063d339 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -5,7 +5,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; Console.debug(`☑️ $argument`); let target = globalThis.$argument; switch (typeof target) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects. + // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fallthrough. case "string": target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); case "object": @@ -16,8 +16,6 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; target = parsed; } break; - default: - break; } globalThis.$argument = target; if (target?.LogLevel) Console.logLevel = target.LogLevel; From 83d757c16fa97f06efec6f079f2619e1827c4a08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:13:21 +0000 Subject: [PATCH 36/72] style: restore undefined case in switch Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 063d339..45b5187 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -5,7 +5,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; Console.debug(`☑️ $argument`); let target = globalThis.$argument; switch (typeof target) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fallthrough. + // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fall-through. case "string": target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); case "object": @@ -16,6 +16,8 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; target = parsed; } break; + case "undefined": + break; } globalThis.$argument = target; if (target?.LogLevel) Console.logLevel = target.LogLevel; From e7e2c036477c7a5efe484147b43243064e818040 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:07:26 +0000 Subject: [PATCH 37/72] refactor: align switch with argument object Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 18 ++++++++---------- test/argument.test.js | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 45b5187..6e6c69d 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -3,23 +3,21 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; (() => { Console.debug(`☑️ $argument`); - let target = globalThis.$argument; - switch (typeof target) { + let argument = {}; + switch (typeof $argument) { // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fall-through. case "string": - target = Object.fromEntries(target.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + $argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); case "object": - if (target !== null) { - const parsed = {}; + if ($argument !== null) { // 数组按对象处理以支持索引键。 - Object.keys(target).forEach(key => _.set(parsed, key, target[key])); - target = parsed; + Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); } break; case "undefined": break; } - globalThis.$argument = target; - if (target?.LogLevel) Console.logLevel = target.LogLevel; - Console.debug(`✅ $argument`, `$argument: ${JSON.stringify(target)}`); + globalThis.$argument = argument; + if (argument.LogLevel) Console.logLevel = argument.LogLevel; + Console.debug(`✅ $argument`, `$argument: ${JSON.stringify(argument)}`); })(); diff --git a/test/argument.test.js b/test/argument.test.js index 348bd92..e84c82c 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -42,8 +42,8 @@ describe("argument", () => { it("应该处理未定义参数", async () => { const result = await importWithArgument(); - assert.strictEqual(result, undefined); - assert.strictEqual(globalThis.$argument, undefined); + assert.deepStrictEqual(result, {}); + assert.deepStrictEqual(globalThis.$argument, {}); }); it("应该支持全局 $argument", async () => { From 407741f291a57d5c547ae2884241ab5bcad2fe31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:08:57 +0000 Subject: [PATCH 38/72] fix: build argument from source object Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 6e6c69d..3587542 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,14 +4,16 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; (() => { Console.debug(`☑️ $argument`); let argument = {}; + let source; switch (typeof $argument) { // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fall-through. case "string": - $argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + source = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); case "object": if ($argument !== null) { + const input = typeof $argument === "string" ? source : $argument; // 数组按对象处理以支持索引键。 - Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); + Object.keys(input).forEach(key => _.set(argument, key, input[key])); } break; case "undefined": From 87c68c57e43cd4ef71e4867fff53a7b72279f863 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:11:04 +0000 Subject: [PATCH 39/72] refactor: build argument via switch flow Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 3587542..e39307a 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,16 +4,17 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; (() => { Console.debug(`☑️ $argument`); let argument = {}; - let source; switch (typeof $argument) { // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fall-through. case "string": - source = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + $argument = argument; case "object": if ($argument !== null) { - const input = typeof $argument === "string" ? source : $argument; + const parsed = {}; // 数组按对象处理以支持索引键。 - Object.keys(input).forEach(key => _.set(argument, key, input[key])); + Object.keys($argument).forEach(key => _.set(parsed, key, $argument[key])); + argument = parsed; } break; case "undefined": From f75a6720cdf5df1e146bdd2254b1a64491f844b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:12:01 +0000 Subject: [PATCH 40/72] fix: avoid mutating $argument in string case Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index e39307a..5fa04e2 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -8,12 +8,12 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fall-through. case "string": argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - $argument = argument; case "object": if ($argument !== null) { const parsed = {}; // 数组按对象处理以支持索引键。 - Object.keys($argument).forEach(key => _.set(parsed, key, $argument[key])); + const source = typeof $argument === "string" ? argument : $argument; + Object.keys(source).forEach(key => _.set(parsed, key, source[key])); argument = parsed; } break; From fb93ead1238dd17bf3a3b101e94b2a3a924343e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:13:05 +0000 Subject: [PATCH 41/72] chore: guard argument log level Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 5fa04e2..6b77830 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -21,6 +21,6 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; break; } globalThis.$argument = argument; - if (argument.LogLevel) Console.logLevel = argument.LogLevel; + if (argument?.LogLevel) Console.logLevel = argument.LogLevel; Console.debug(`✅ $argument`, `$argument: ${JSON.stringify(argument)}`); })(); From 4cc019fe6672ed1ea71c3e90b37f1c23cae97b45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:13:44 +0000 Subject: [PATCH 42/72] fix: use parsed source for argument build Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 6b77830..41661d7 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -9,12 +9,14 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; case "string": argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); case "object": - if ($argument !== null) { - const parsed = {}; - // 数组按对象处理以支持索引键。 + { const source = typeof $argument === "string" ? argument : $argument; - Object.keys(source).forEach(key => _.set(parsed, key, source[key])); - argument = parsed; + if (source !== null) { + const parsed = {}; + // 数组按对象处理以支持索引键。 + Object.keys(source).forEach(key => _.set(parsed, key, source[key])); + argument = parsed; + } } break; case "undefined": From 30056d596873781e9e865cf8fed7ca0651642840 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:14:19 +0000 Subject: [PATCH 43/72] style: make undefined handling explicit Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/argument.mjs b/lib/argument.mjs index 41661d7..8d09d71 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -20,6 +20,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; } break; case "undefined": + argument = {}; break; } globalThis.$argument = argument; From fd05869e8f08064c39fa8668884cf85be2acc9cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:15:04 +0000 Subject: [PATCH 44/72] refactor: derive source from argument Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 8d09d71..add92a0 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -10,7 +10,8 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); case "object": { - const source = typeof $argument === "string" ? argument : $argument; + let source = argument; + if (typeof $argument === "object") source = $argument; if (source !== null) { const parsed = {}; // 数组按对象处理以支持索引键。 From a492b182afc4d05f1280cffe7c82ceddeec52643 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:16:01 +0000 Subject: [PATCH 45/72] refactor: simplify argument source selection Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index add92a0..8d09d71 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -10,8 +10,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); case "object": { - let source = argument; - if (typeof $argument === "object") source = $argument; + const source = typeof $argument === "string" ? argument : $argument; if (source !== null) { const parsed = {}; // 数组按对象处理以支持索引键。 From 98d0ef760321214c56b08e3799775d1dbaaf9337 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:16:48 +0000 Subject: [PATCH 46/72] refactor: track string argument fallthrough Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 8d09d71..a1893c5 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,13 +4,15 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; (() => { Console.debug(`☑️ $argument`); let argument = {}; + let isStringArgument = false; switch (typeof $argument) { // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fall-through. case "string": argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + isStringArgument = true; case "object": { - const source = typeof $argument === "string" ? argument : $argument; + const source = isStringArgument ? argument : $argument; if (source !== null) { const parsed = {}; // 数组按对象处理以支持索引键。 From 678234192f60e2c42c5f7909ed47c6f48a0010e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:21:59 +0000 Subject: [PATCH 47/72] refactor: remove string flag from argument parsing Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index a1893c5..9ae0fc6 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,15 +4,14 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; (() => { Console.debug(`☑️ $argument`); let argument = {}; - let isStringArgument = false; switch (typeof $argument) { // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fall-through. case "string": argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - isStringArgument = true; + $argument = argument; case "object": { - const source = isStringArgument ? argument : $argument; + const source = $argument; if (source !== null) { const parsed = {}; // 数组按对象处理以支持索引键。 From ea9971a1cdc4b9ef402e05bbbf68f7844ff8490f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:26:40 +0000 Subject: [PATCH 48/72] style: match switch comment format Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index 9ae0fc6..ca7c2fb 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -5,7 +5,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; Console.debug(`☑️ $argument`); let argument = {}; switch (typeof $argument) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: String arguments are parsed then handled as objects via fall-through. + // biome-ignore lint/suspicious/noFallthroughSwitchClause: case "string": argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); $argument = argument; From 37c4780a9fa96cab6b8c4c8ee8bb1f7d5e6ed2de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:34:39 +0000 Subject: [PATCH 49/72] refactor: match switch snippet behavior Co-authored-by: VirgilClyne <2111377+VirgilClyne@users.noreply.github.com> --- lib/argument.mjs | 34 ++++++++++++++-------------------- test/argument.test.js | 8 ++++---- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index ca7c2fb..41472d3 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -2,29 +2,23 @@ import { Console } from "../polyfill/Console.mjs"; import { Lodash as _ } from "../polyfill/Lodash.mjs"; (() => { - Console.debug(`☑️ $argument`); - let argument = {}; + Console.debug("☑️ $argument"); switch (typeof $argument) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: - case "string": - argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - $argument = argument; - case "object": - { - const source = $argument; - if (source !== null) { - const parsed = {}; - // 数组按对象处理以支持索引键。 - Object.keys(source).forEach(key => _.set(parsed, key, source[key])); - argument = parsed; - } - } + // biome-ignore lint/suspicious/noFallthroughSwitchClause: + case "string": { + const argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + $argument = {}; + Object.keys(argument).forEach(key => _.set($argument, key, argument[key])); break; + } + case "object": { + const argument = $argument; + Object.keys(argument ?? $argument).forEach(key => _.set(argument, key, $argument[key])); + break; + } case "undefined": - argument = {}; break; } - globalThis.$argument = argument; - if (argument?.LogLevel) Console.logLevel = argument.LogLevel; - Console.debug(`✅ $argument`, `$argument: ${JSON.stringify(argument)}`); + if ($argument.LogLevel) Console.logLevel = $argument.LogLevel; + Console.debug("✅ $argument", `$argument: ${JSON.stringify($argument)}`); })(); diff --git a/test/argument.test.js b/test/argument.test.js index e84c82c..12602a1 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -4,7 +4,7 @@ import { afterEach, describe, it } from "node:test"; let importSeed = 0; const argumentModule = new URL("../lib/argument.mjs", import.meta.url); const importWithArgument = async value => { - if (typeof value === "undefined") globalThis.$argument = undefined; + if (typeof value === "undefined") globalThis.$argument = {}; else globalThis.$argument = value; importSeed += 1; await import(`${argumentModule}?test=${importSeed}`); @@ -13,7 +13,7 @@ const importWithArgument = async value => { describe("argument", () => { afterEach(() => { - globalThis.$argument = undefined; + globalThis.$argument = {}; }); it("应该解析字符串参数", async () => { @@ -36,8 +36,8 @@ describe("argument", () => { it("应该处理对象参数", async () => { const result = await importWithArgument({ "nested.value": "ok" }); - assert.deepStrictEqual(result, { nested: { value: "ok" } }); - assert.deepStrictEqual(globalThis.$argument, { nested: { value: "ok" } }); + assert.deepStrictEqual(result, { "nested.value": "ok", nested: { value: "ok" } }); + assert.deepStrictEqual(globalThis.$argument, { "nested.value": "ok", nested: { value: "ok" } }); }); it("应该处理未定义参数", async () => { From 89c9cfc28ee3654376f1c4d9ede24edc9a3e9df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 13 Feb 2026 15:25:12 +0800 Subject: [PATCH 50/72] =?UTF-8?q?fix(argument):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E5=8F=82=E6=95=B0=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 case object 分支中未定义变量 argument 的问题 - 更新测试期望值,只保留转换后的嵌套结构 - 添加 test:argument 脚本 --- getStorage.mjs | 2 +- lib/argument.mjs | 6 +++--- package.json | 1 + test/argument.test.js | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index ba64e9d..9c4c858 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -42,7 +42,7 @@ export function getStorage(key, names, database) { _.merge(Store.Configs, database?.[name]?.Configs); _.merge(Store.Caches, BoxJs?.[name]?.Caches); }); - _.merge(Store.Settings, globalThis.$argument); + _.merge(Store.Settings, $argument); if (Store.Settings.LogLevel) Console.logLevel = Store.Settings.LogLevel; Console.debug("✅ Merge", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** traverseObject *****************/ diff --git a/lib/argument.mjs b/lib/argument.mjs index 41472d3..1a5ede4 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -4,7 +4,6 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; (() => { Console.debug("☑️ $argument"); switch (typeof $argument) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: case "string": { const argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); $argument = {}; @@ -12,8 +11,9 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; break; } case "object": { - const argument = $argument; - Object.keys(argument ?? $argument).forEach(key => _.set(argument, key, $argument[key])); + const argument = {}; + Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); + $argument = argument; break; } case "undefined": diff --git a/package.json b/package.json index c29a04a..954ae03 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "tsc:build": "npx tsc", "test": "node --test test/*.test.js", "test:merge": "node --test test/Lodash.merge.test.js", + "test:argument": "node --test test/argument.test.js", "deprecate": "npm deprecate -f '@nsnanocat/util@0.0.0-preview' \"this package has been deprecated\"" }, "repository": { diff --git a/test/argument.test.js b/test/argument.test.js index 12602a1..af790e6 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -36,8 +36,8 @@ describe("argument", () => { it("应该处理对象参数", async () => { const result = await importWithArgument({ "nested.value": "ok" }); - assert.deepStrictEqual(result, { "nested.value": "ok", nested: { value: "ok" } }); - assert.deepStrictEqual(globalThis.$argument, { "nested.value": "ok", nested: { value: "ok" } }); + assert.deepStrictEqual(result, { nested: { value: "ok" } }); + assert.deepStrictEqual(globalThis.$argument, { nested: { value: "ok" } }); }); it("应该处理未定义参数", async () => { From 5c5f1f37d12fd0489d3c9c78c01ed0e3116807ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 13 Feb 2026 19:03:47 +0800 Subject: [PATCH 51/72] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=20README=20?= =?UTF-8?q?=E4=B8=8E=20JSDoc=20=E6=B3=A8=E9=87=8A=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 529 ++++++++++++++++++++++++++++++++++++++- getStorage.mjs | 45 +++- lib/app.mjs | 15 +- lib/argument.mjs | 18 ++ lib/done.mjs | 26 +- lib/environment.mjs | 22 ++ lib/notification.mjs | 51 +++- lib/runScript.mjs | 21 ++ lib/time.mjs | 18 +- lib/wait.mjs | 8 +- polyfill/Console.mjs | 112 +++++++++ polyfill/Lodash.mjs | 75 ++++++ polyfill/StatusTexts.mjs | 6 + polyfill/Storage.mjs | 78 +++--- polyfill/fetch.mjs | 48 +++- 15 files changed, 1004 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index d581ac4..07e5d8d 100644 --- a/README.md +++ b/README.md @@ -1 +1,528 @@ -# utils \ No newline at end of file +# @nsnanocat/util + +用于统一 Quantumult X / Loon / Shadowrocket / Node.js / Egern / Surge / Stash 脚本接口的通用工具库。 + +核心目标: +- 统一不同平台的 HTTP、通知、持久化、结束脚本等调用方式。 +- 在一个脚本里尽量少写平台分支。 +- 提供一组可直接复用的 polyfill(`fetch` / `Storage` / `Console` / `Lodash`)。 + +## 目录 +- [安装与导入](#安装与导入) +- [导出清单](#导出清单) +- [模块依赖关系](#模块依赖关系) +- [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) + +如果你从 GitHub Packages 安装,需要先配置 GitHub 认证(PAT Token)。 + +```bash +npm i @nsnanocat/util +``` + +```bash +# GitHub Packages 示例(需先配置认证) +npm config set @nsnanocat:registry https://npm.pkg.github.com +npm i @nsnanocat/util +``` + +```js +import { + $app, + done, + fetch, + notification, + time, + wait, + Console, + Lodash, + Storage, + getStorage, +} 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/StatusTexts.mjs` +- `polyfill/Storage.mjs` +- `getStorage.mjs` + +### 仓库中存在但未从主入口导出 +- `lib/environment.mjs` +- `lib/runScript.mjs` + +## 模块依赖关系 + +说明: +- 下表只描述“模块之间”的依赖关系、调用到的函数/常量、以及依赖原因。 +- 你在业务脚本中通常只需要调用对外 API;底层跨平台差异已在这些依赖链里处理。 + +| 模块 | 依赖模块 | 使用的函数/常量 | 为什么依赖 | +| --- | --- | --- | --- | +| `lib/app.mjs` | 无 | 无 | 核心平台识别源头,供其他差异模块分流 | +| `lib/environment.mjs` | `lib/app.mjs` | `$app` | 按平台生成统一 `$environment`(尤其补齐 `app` 字段) | +| `lib/argument.mjs` | `polyfill/Console.mjs`, `polyfill/Lodash.mjs` | `Console.debug`, `Console.logLevel`, `Lodash.set` | 统一 `$argument` 结构并支持深路径写入 | +| `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` | 日志在 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/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" | "Node.js" | "Egern" | "Surge" | "Stash" | 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. 存在 `module` -> `Node.js` +5. 存在 `Egern` -> `Egern` +6. 存在 `$environment` 且有 `surge-version` -> `Surge` +7. 存在 `$environment` 且有 `stash-version` -> `Stash` + +#### `$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" }` | +| Node.js | 读取 `process.env` 并写入 `process.env.app` | `{ ..., app: "Node.js" }` | +| 其他 | 无 | `{}` | + +### `lib/argument.mjs`(`$argument` 参数标准化模块) + +此文件无显式导出;`import` 后立即执行。这是为了统一各平台 `$argument` 的输入差异。 + +#### 行为 +- 单独使用时可直接格式化全局 `$argument`: + - `await import("@nsnanocat/util/lib/argument.mjs")` +- 通过包入口导入(`import ... from "@nsnanocat/util"`)时也会执行本模块: + - 读取到的 `$argument` 会按 URL Params 样式格式化为对象,并支持深路径。 +- 平台输入差异说明: + - 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` 为 `undefined`:不处理。 +- 若 `$argument.LogLevel` 存在:同步到 `Console.logLevel`。 + +#### 用法 +```js +globalThis.$argument = "mode=on&a.b=1"; +await import("@nsnanocat/util/lib/argument.mjs"); +console.log(globalThis.$argument); // { mode: "on", a: { b: "1" } } +``` + +### `lib/done.mjs` + +#### `done(object = {})` +- 签名:`done(object?: object): void` +- 作用:统一不同平台的脚本结束接口(`$done` / 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)` | +| Node.js | 不适用 | 不适用 | 不适用 | `process.exit(1)` | + +不可用/差异点: +- `policy` 在 Egern / Shadowrocket 分支不做映射。 +- Quantumult X 会丢弃未在白名单内的字段。 +- Node.js 不调用 `$done`,而是直接退出进程,且退出码固定为 `1`。 + +### `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` | +| Node.js | 不发送通知(非 iOS App 环境) | 无 | 无 | + +不可用/差异点: +- `copy/update-pasteboard` 在 Loon 分支不会生效。 +- Loon / Quantumult X 对 `media` 仅接受网络 URL;Base64 媒体不会自动映射。 +- 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` +- 用法: + +```js +await wait(500); +``` + +### `lib/runScript.mjs`(未主入口导出) + +#### `runScript(script, runOpts)` +- 签名:`runScript(script: string, runOpts?: { timeout?: number }): Promise` +- 作用:通过 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` + +#### `getStorage(key, names, database)` +- 签名: + - `key: string`(持久化主键) + - `names: string | string[]`(平台名/配置组名,可嵌套数组) + - `database: object`(默认数据库) +- 返回:`{ Settings, Configs, Caches }` + +合并顺序: +1. `database.Default` -> 初始 `Store` +2. 持久化中的 BoxJS 值(`Storage.getItem(key)`) +3. 按 `names` 合并 `database[name]` + `BoxJs[name]` +4. 最后合并 `$argument` + +自动类型转换(`Store.Settings`): +- 字符串 `"true"/"false"` -> `boolean` +- 纯数字字符串 -> `number` +- 含逗号字符串 -> `array`,并尝试逐项转数字 + +示例: +```js +const store = getStorage("@my_box", ["YouTube", "Global"], database); +``` + +### `polyfill/fetch.mjs` + +#### `fetch(resource, options = {})` +- 签名:`fetch(resource: object | string, options?: object): Promise` +- 参数合并: + - `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` + +说明:下表是各 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` | +| Node.js | `fetch` + `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) | + +返回对象(统一后)常见字段: +- `ok` +- `status` +- `statusCode` +- `statusText` +- `headers` +- `body` +- `bodyBytes` + +不可用/差异点: +- `policy` 在 Surge / Egern / Node.js 分支没有额外适配逻辑。 +- `redirection` 在部分平台会映射为 `auto-redirect` 或 `opts.redirection`。 +- Node.js 分支依赖 `globalThis.fetch` / `globalThis.fetchCookie` 或 `node-fetch` + `fetch-cookie`。 + +### `polyfill/Storage.mjs` + +#### `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 / Loon / Stash / Egern / Shadowrocket / Node.js:返回 `false`。 + +#### `Storage.clear()` +- Quantumult X:可用(`$prefs.removeAllValues`)。 +- 其他平台:返回 `false`。 + +#### Node.js 特性 +- 数据文件默认:`box.dat`。 +- 读取路径优先级:当前目录 -> `process.cwd()`。 + +平台后端映射: + +| 平台 | 读写接口 | +| --- | --- | +| Surge / Loon / Stash / Egern / Shadowrocket | `$persistentStore.read/write` | +| Quantumult X | `$prefs.valueForKey/setValueForKey` | +| Node.js | 本地 `box.dat` | + +### `polyfill/Console.mjs` + +`Console` 是统一日志工具(静态类)。 + +#### 日志级别 +- `Console.logLevel` 可读写。 +- 支持:`OFF(0)` / `ERROR(1)` / `WARN(2)` / `INFO(3)` / `DEBUG(4)` / `ALL(5)`。 + +#### 方法 +- `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` | 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` 及以上 | + +平台差异: +- Node.js 下 `error` 会优先打印 `Error.stack`。 +- 其他平台统一加前缀符号输出(`❌/⚠️/ℹ️/🅱️`)。 + +### `polyfill/Lodash.mjs` + +`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` | 挑选 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/StatusTexts.mjs` + +#### `StatusTexts` +- 类型:`Record` +- 内容:HTTP 状态码到状态文本映射(100~511 的常见码)。 + +## 平台差异总览 + +说明:本节展示的是各平台原生脚本接口差异。实际在本库中,这些差异已由 `done`、`fetch`、`notification`、`Storage` 等模块做了统一适配。 + +| 能力 | Quantumult X | Loon | Surge | Stash | Egern | Shadowrocket | Node.js | +| --- | --- | --- | --- | --- | --- | --- | --- | +| HTTP 请求 | `$task.fetch` | `$httpClient` | `$httpClient` | `$httpClient` | `$httpClient` | `$httpClient` | `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` | 可用 | 不可用 | 不可用 | 不可用 | 不可用 | 不可用 | 不可用 | +| `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` 在 Node.js 固定 `process.exit(1)`。 +- `polyfill/fetch.mjs` 的超时保护使用了 `Promise.race`,但当前实现里请求 Promise 先被 `await`,可能导致超时行为与预期不完全一致。 +- `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) + +### Node.js +- [Node.js Globals - fetch](https://nodejs.org/api/globals.html#fetch) + +### Egern / Shadowrocket +- [Egern Docs - Scriptings 配置](https://egernapp.com/docs/configuration/scriptings) +- [Shadowrocket 官方站点](https://www.shadowlaunch.com/) + +> 说明:Egern 与 Shadowrocket 暂未检索到等价于 Surge/Loon/Stash 的完整公开脚本 API 页面;相关差异说明以本库实际代码分支行为为准。 diff --git a/getStorage.mjs b/getStorage.mjs index 9c4c858..49a130b 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -4,13 +4,32 @@ import { Lodash as _ } from "./polyfill/Lodash.mjs"; import { Storage } from "./polyfill/Storage.mjs"; /** - * Get Storage Variables + * 存储配置读取与合并结果。 + * Merged storage result object. + * + * @typedef {object} StorageProfile + * @property {Record} Settings 运行设置 / Runtime settings. + * @property {Record} Configs 静态配置 / Static configs. + * @property {Record} Caches 缓存数据 / Runtime caches. + */ + +/** + * 读取并合并默认配置、持久化配置与 `$argument`。 + * Read and merge default config, persisted config and `$argument`. + * + * 合并顺序: + * Merge order: + * 1) `database.Default` + * 2) BoxJS persisted value + * 3) `database[name]` + `BoxJs[name]` + * 4) `$argument` + * * @link https://github.com/NanoCat-Me/utils/blob/main/getStorage.mjs * @author VirgilClyne - * @param {string} key - Persistent Store Key - * @param {array | string} names - Platform Names - * @param {object} database - Default Database - * @return {object} { Settings, Caches, Configs } + * @param {string} key 持久化主键 / Persistent store key. + * @param {string|string[]|Array} names 目标配置名 / Target profile names. + * @param {Record} database 默认数据库 / Default database object. + * @returns {StorageProfile} */ export function getStorage(key, names, database) { if (database?.Default?.Settings?.LogLevel) Console.logLevel = database.Default.Settings.LogLevel; @@ -62,6 +81,14 @@ export function getStorage(key, names, database) { return Store; } +/** + * 深度遍历对象并用回调替换叶子值。 + * Deep-walk an object and replace leaf values using callback. + * + * @param {Record} o 目标对象 / Target object. + * @param {(key: string, value: any) => any} c 处理回调 / Transformer callback. + * @returns {Record} + */ function traverseObject(o, c) { for (const t in o) { const n = o[t]; @@ -69,6 +96,14 @@ function traverseObject(o, c) { } return o; } + +/** + * 将纯数字字符串转换为数字。 + * Convert integer-like string into number. + * + * @param {string} string 输入字符串 / Input string. + * @returns {string|number} + */ function string2number(string) { if (/^\d+$/.test(string)) string = Number.parseInt(string, 10); return string; diff --git a/lib/app.mjs b/lib/app.mjs index d74211d..99110f1 100644 --- a/lib/app.mjs +++ b/lib/app.mjs @@ -1,7 +1,18 @@ /** - * Current app name + * 当前运行平台名称。 + * Current runtime platform name. * - * @type {("Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash")} + * 识别顺序: + * Detection order: + * 1) `$task` -> Quantumult X + * 2) `$loon` -> Loon + * 3) `$rocket` -> Shadowrocket + * 4) `module` -> Node.js + * 5) `Egern` -> Egern + * 6) `$environment["surge-version"]` -> Surge + * 7) `$environment["stash-version"]` -> Stash + * + * @type {("Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash" | undefined)} */ export const $app = (() => { const keys = Object.keys(globalThis); diff --git a/lib/argument.mjs b/lib/argument.mjs index 1a5ede4..d66f925 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -1,6 +1,24 @@ import { Console } from "../polyfill/Console.mjs"; import { Lodash as _ } from "../polyfill/Lodash.mjs"; +/** + * 统一 `$argument` 输入格式并展开深路径。 + * Normalize `$argument` input format and expand deep paths. + * + * 平台差异: + * Platform differences: + * - Surge / Stash / Egern 常见为字符串参数: `a=1&b=2` + * - Surge / Stash / Egern usually pass string args: `a=1&b=2` + * - Loon 支持字符串和对象两种形态 + * - Loon supports both string and object forms + * - Quantumult X / Shadowrocket 一般不提供 `$argument` + * - Quantumult X / Shadowrocket usually do not expose `$argument` + * + * 执行时机: + * Execution timing: + * - 该模块为即时执行模块,`import` 时立即处理全局 `$argument` + * - This module executes immediately and mutates global `$argument` on import + */ (() => { Console.debug("☑️ $argument"); switch (typeof $argument) { diff --git a/lib/done.mjs b/lib/done.mjs index 1bb5098..6d38453 100644 --- a/lib/done.mjs +++ b/lib/done.mjs @@ -4,10 +4,30 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; import { StatusTexts } from "../polyfill/StatusTexts.mjs"; /** - * Complete the script execution + * `done` 的统一入参结构。 + * Unified `done` input payload. * - * @export - * @param {object} object + * @typedef {object} DonePayload + * @property {number|string} [status] 响应状态码或状态行 / Response status code or status line. + * @property {string} [url] 响应 URL / Response URL. + * @property {Record} [headers] 响应头 / Response headers. + * @property {string|ArrayBuffer|ArrayBufferView} [body] 响应体 / Response body. + * @property {ArrayBuffer} [bodyBytes] 二进制响应体 / Binary response body. + * @property {string} [policy] 指定策略名 / Preferred policy name. + */ + +/** + * 结束脚本执行并按平台转换参数。 + * Complete script execution with platform-specific parameter mapping. + * + * 说明: + * Notes: + * - 这是调用入口,平台原生 `$done` 差异在内部处理 + * - This is the call entry and native `$done` differences are handled internally + * - Node.js 不调用 `$done`,而是直接退出进程 + * - Node.js does not call `$done`; it exits the process directly + * + * @param {DonePayload} [object={}] 统一响应对象 / Unified response object. * @returns {void} */ export function done(object = {}) { diff --git a/lib/environment.mjs b/lib/environment.mjs index 5b70418..931e4f8 100644 --- a/lib/environment.mjs +++ b/lib/environment.mjs @@ -1,6 +1,28 @@ import { $app } from "./app.mjs"; +/** + * 标准化后的运行环境对象。 + * Normalized runtime environment object. + * + * - Surge/Stash/Egern: 基于全局 `$environment` 并补充 `app` + * - Surge/Stash/Egern: based on global `$environment` with `app` patched + * - Loon: 解析 `$loon` 得到设备与版本信息 + * - Loon: parse `$loon` into device/version fields + * - Quantumult X: 仅返回 `{ app: "Quantumult X" }` + * - Quantumult X: returns `{ app: "Quantumult X" }` only + * - Node.js: 复用 `process.env` 并写入 `process.env.app` + * - Node.js: reuses `process.env` and writes `process.env.app` + * + * @type {Record} + */ export const $environment = environment(); + +/** + * 获取标准化环境对象。 + * Build and return the normalized environment object. + * + * @returns {Record} + */ export function environment() { switch ($app) { case "Surge": diff --git a/lib/notification.mjs b/lib/notification.mjs index 940caad..5dd8e06 100644 --- a/lib/notification.mjs +++ b/lib/notification.mjs @@ -2,20 +2,41 @@ import { $app } from "./app.mjs"; import { Console } from "../polyfill/Console.mjs"; /** - * 系统通知 + * 通知内容扩展参数。 + * Extended notification content options. * - * > 通知参数: 同时支持 QuanX 和 Loon 两种格式, EnvJs根据运行环境自动转换, Surge 环境不支持多媒体通知 - * - * 示例: - * $.msg(title, subtitle, body, "twitter://") - * $.msg(title, subtitle, body, { "open-url": "twitter://", "media-url": "https://github.githubassets.com/images/modules/open_graph/github-mark.png" }) - * $.msg(title, subtitle, body, { "open-url": "https://bing.com", "media-url": "https://github.githubassets.com/images/modules/open_graph/github-mark.png" }) + * @typedef {object|string|number|boolean} NotificationContent + * @property {string} [open] 打开链接 / Open URL. + * @property {string} ["open-url"] 打开链接 (QuanX) / Open URL (QuanX). + * @property {string} [url] 打开链接 / Open URL. + * @property {string} [openUrl] 打开链接 (Loon/Shadowrocket) / Open URL (Loon/Shadowrocket). + * @property {string} [copy] 复制文本 / Copy text. + * @property {string} ["update-pasteboard"] 复制文本 (QuanX) / Copy text (QuanX). + * @property {string} [updatePasteboard] 复制文本 / Copy text. + * @property {string} [media] 媒体 URL 或 Base64 / Media URL or Base64. + * @property {string} ["media-url"] 媒体 URL / Media URL. + * @property {string} [mediaUrl] 媒体 URL / Media URL. + * @property {string} [mime] Base64 媒体 MIME / MIME type for Base64 media. + * @property {number} ["auto-dismiss"] 自动消失秒数 / Auto dismiss seconds. + * @property {string} [sound] 提示音 / Notification sound. + */ + +/** + * 发送系统通知并按平台适配参数格式。 + * Send system notification with platform-specific payload mapping. * - * @param {string} title 标题 - * @param {string} subtitle 副标题 - * @param {string} body 内容 - * @param {*} mutableContent 通知扩展字段 + * 说明: + * Notes: + * - iOS App 平台调用 `$notification.post` 或 `$notify` + * - iOS app platforms call `$notification.post` or `$notify` + * - Node.js 不支持 iOS 通知接口,仅输出日志 + * - Node.js does not support iOS notification APIs; it logs only * + * @param {string} [title=`ℹ️ ${$app} 通知`] 标题 / Title. + * @param {string} [subtitle=""] 副标题 / Subtitle. + * @param {string} [body=""] 内容 / Message body. + * @param {NotificationContent} [content={}] 扩展参数 / Extended content options. + * @returns {void} */ export function notification(title = `ℹ️ ${$app} 通知`, subtitle = "", body = "", content = {}) { const mutableContent = MutableContent(content); @@ -39,6 +60,14 @@ export function notification(title = `ℹ️ ${$app} 通知`, subtitle = "", bod Console.groupEnd(); } +/** + * 将统一通知参数转换为平台可识别字段。 + * Convert normalized content into platform-recognized fields. + * + * @private + * @param {NotificationContent} content 通知扩展参数 / Extended content options. + * @returns {Record} + */ const MutableContent = content => { const mutableContent = {}; switch (typeof content) { diff --git a/lib/runScript.mjs b/lib/runScript.mjs index ec7aeeb..720bd5d 100644 --- a/lib/runScript.mjs +++ b/lib/runScript.mjs @@ -3,6 +3,27 @@ import { fetch } from "../polyfill/fetch.mjs"; import { Lodash as _ } from "../polyfill/Lodash.mjs"; import { Storage } from "../polyfill/Storage.mjs"; +/** + * 远程脚本执行选项。 + * Remote script execution options. + * + * @typedef {object} RunScriptOptions + * @property {number} [timeout] 执行超时秒数 / Timeout in seconds. + */ + +/** + * 通过 BoxJS HTTP API 触发脚本执行。 + * Trigger script execution through BoxJS HTTP API. + * + * 依赖键: + * Required keys: + * - `@chavy_boxjs_userCfgs.httpapi` (`password@host:port`) + * - `@chavy_boxjs_userCfgs.httpapi_timeout` + * + * @param {string} script 脚本文本 / Script source text. + * @param {RunScriptOptions} [runOpts] 运行选项 / Runtime options. + * @returns {Promise} + */ export async function runScript(script, runOpts) { let httpapi = Storage.getItem("@chavy_boxjs_userCfgs.httpapi"); httpapi = httpapi?.replace?.(/\n/g, "")?.trim(); diff --git a/lib/time.mjs b/lib/time.mjs index 9272670..c4824d8 100644 --- a/lib/time.mjs +++ b/lib/time.mjs @@ -1,14 +1,14 @@ /** - * time - * 时间格式化 - * [version of ISO8601]{@link https://262.ecma-international.org/5.1/#sec-15.9.1.15} - * 示例:time("yyyy-MM-dd qq HH:mm:ss.S") YYYY-MM-DDTHH:mm:ss.sssZ - * :time("yyyyMMddHHmmssS") - * YY:年 MM:月 dd:日 S:季 HH:时 m:分 ss:秒 sss:毫秒 Z:时区 - * 其中y可选0-4位占位符、S可选0-1位占位符,其余可选0-2位占位符 - * @param {string} format 格式化参数 - * @param {number} ts 可选: 根据指定时间戳返回格式化日期 + * 按模板格式化时间字符串。 + * Format date/time into a template string. * + * 支持占位符: + * Supported tokens: + * - `YY`, `yyyy`, `MM`, `dd`, `HH`, `mm`, `ss`, `sss`, `S` + * + * @param {string} format 格式模板 / Format template. + * @param {number} [ts] 可选时间戳 / Optional timestamp. + * @returns {string} */ export function time(format, ts) { const date = ts ? new Date(ts) : new Date(); diff --git a/lib/wait.mjs b/lib/wait.mjs index 260e674..5a1a85e 100644 --- a/lib/wait.mjs +++ b/lib/wait.mjs @@ -1,9 +1,9 @@ /** - * wait + * 延时等待指定毫秒后继续执行。 + * Wait for the given milliseconds before continuing. * - * @export - * @param {number} [delay=1000] - * @returns {Promise} + * @param {number} [delay=1000] 延迟毫秒 / Delay in milliseconds. + * @returns {Promise} */ export function wait(delay = 1000) { return new Promise(resolve => setTimeout(resolve, delay)); diff --git a/polyfill/Console.mjs b/polyfill/Console.mjs index 9affeb6..e500604 100644 --- a/polyfill/Console.mjs +++ b/polyfill/Console.mjs @@ -1,12 +1,29 @@ import { $app } from "../lib/app.mjs"; +/** + * 统一日志工具,兼容各脚本平台与 Node.js。 + * Unified logger compatible with script platforms and Node.js. + */ export class Console { static #counts = new Map([]); static #groups = []; static #times = new Map([]); + /** + * 清空控制台(当前为空实现)。 + * Clear console (currently a no-op). + * + * @returns {void} + */ static clear = () => {}; + /** + * 增加计数器并打印当前值。 + * Increment counter and print the current value. + * + * @param {string} [label="default"] 计数器名称 / Counter label. + * @returns {void} + */ static count = (label = "default") => { switch (Console.#counts.has(label)) { case true: @@ -19,6 +36,13 @@ export class Console { Console.log(`${label}: ${Console.#counts.get(label)}`); }; + /** + * 重置计数器。 + * Reset a counter. + * + * @param {string} [label="default"] 计数器名称 / Counter label. + * @returns {void} + */ static countReset = (label = "default") => { switch (Console.#counts.has(label)) { case true: @@ -31,12 +55,26 @@ export class Console { } }; + /** + * 输出调试日志。 + * Print debug logs. + * + * @param {...any} msg 日志内容 / Log messages. + * @returns {void} + */ static debug = (...msg) => { if (Console.#level < 4) return; msg = msg.map(m => `🅱️ ${m}`); Console.log(...msg); }; + /** + * 输出错误日志。 + * Print error logs. + * + * @param {...any} msg 日志内容 / Log messages. + * @returns {void} + */ static error(...msg) { if (Console.#level < 1) return; switch ($app) { @@ -56,12 +94,39 @@ export class Console { Console.log(...msg); } + /** + * `error` 的别名。 + * Alias of `error`. + * + * @param {...any} msg 日志内容 / Log messages. + * @returns {void} + */ static exception = (...msg) => Console.error(...msg); + /** + * 进入日志分组。 + * Enter a log group. + * + * @param {string} label 分组名 / Group label. + * @returns {number} + */ static group = label => Console.#groups.unshift(label); + /** + * 退出日志分组。 + * Exit the latest log group. + * + * @returns {*} + */ static groupEnd = () => Console.#groups.shift(); + /** + * 输出信息日志。 + * Print info logs. + * + * @param {...any} msg 日志内容 / Log messages. + * @returns {void} + */ static info(...msg) { if (Console.#level < 3) return; msg = msg.map(m => `ℹ️ ${m}`); @@ -70,6 +135,12 @@ export class Console { static #level = 3; + /** + * 获取日志级别文本。 + * Get current log level text. + * + * @returns {"OFF"|"ERROR"|"WARN"|"INFO"|"DEBUG"|"ALL"} + */ static get logLevel() { switch (Console.#level) { case 0: @@ -88,6 +159,12 @@ export class Console { } } + /** + * 设置日志级别。 + * Set current log level. + * + * @param {number|string} level 级别值 / Level value. + */ static set logLevel(level) { switch (typeof level) { case "string": @@ -130,6 +207,13 @@ export class Console { } } + /** + * 输出通用日志。 + * Print generic logs. + * + * @param {...any} msg 日志内容 / Log messages. + * @returns {void} + */ static log = (...msg) => { if (Console.#level === 0) return; msg = msg.map(log => { @@ -157,16 +241,44 @@ export class Console { console.log(msg.join("\n")); }; + /** + * 开始计时。 + * Start timer. + * + * @param {string} [label="default"] 计时器名称 / Timer label. + * @returns {Map} + */ static time = (label = "default") => Console.#times.set(label, Date.now()); + /** + * 结束计时并移除计时器。 + * End timer and remove it. + * + * @param {string} [label="default"] 计时器名称 / Timer label. + * @returns {boolean} + */ static timeEnd = (label = "default") => Console.#times.delete(label); + /** + * 输出当前计时器耗时。 + * Print elapsed time for a timer. + * + * @param {string} [label="default"] 计时器名称 / Timer label. + * @returns {void} + */ static timeLog = (label = "default") => { const time = Console.#times.get(label); if (time) Console.log(`${label}: ${Date.now() - time}ms`); else Console.warn(`Timer "${label}" doesn’t exist`); }; + /** + * 输出警告日志。 + * Print warning logs. + * + * @param {...any} msg 日志内容 / Log messages. + * @returns {void} + */ static warn(...msg) { if (Console.#level < 2) return; msg = msg.map(m => `⚠️ ${m}`); diff --git a/polyfill/Lodash.mjs b/polyfill/Lodash.mjs index 0f42f41..38782b3 100644 --- a/polyfill/Lodash.mjs +++ b/polyfill/Lodash.mjs @@ -1,5 +1,16 @@ /* https://www.lodashjs.com */ +/** + * 轻量 Lodash 工具集。 + * Lightweight Lodash-like utilities. + */ export class Lodash { + /** + * HTML 特殊字符转义。 + * Escape HTML special characters. + * + * @param {string} string 输入文本 / Input text. + * @returns {string} + */ static escape(string) { const map = { "&": "&", @@ -11,6 +22,15 @@ export class Lodash { return string.replace(/[&<>"']/g, m => map[m]); } + /** + * 按路径读取对象值。 + * Get object value by path. + * + * @param {object} [object={}] 目标对象 / Target object. + * @param {string|string[]} [path=""] 路径 / Path. + * @param {*} [defaultValue=undefined] 默认值 / Default value. + * @returns {*} + */ static get(object = {}, path = "", defaultValue = undefined) { // translate array case to dot case, then split with . // a[0].b -> a.0.b -> ['a', '0', 'b'] @@ -24,7 +44,9 @@ export class Lodash { /** * 递归合并源对象的自身可枚举属性到目标对象 + * Recursively merge source enumerable properties into target object. * @description 简化版 lodash.merge,用于合并配置对象 + * @description A simplified lodash.merge for config merging. * * 适用情况: * - 合并嵌套的配置/设置对象 @@ -41,8 +63,11 @@ export class Lodash { * - 会修改原始目标对象 (mutates target) * * @param {object} object - 目标对象 + * @param {object} object - Target object. * @param {...object} sources - 源对象(可多个) + * @param {...object} sources - Source objects. * @returns {object} 返回合并后的目标对象 + * @returns {object} Merged target object. * @example * const target = { a: { b: 1 }, c: 2 }; * const source = { a: { d: 3 }, e: 4 }; @@ -99,8 +124,11 @@ export class Lodash { /** * 判断值是否为普通对象 (Plain Object) + * Check whether a value is a plain object. * @param {*} value - 要检查的值 + * @param {*} value - Value to check. * @returns {boolean} 如果是普通对象返回 true + * @returns {boolean} Returns true when value is a plain object. */ static #isPlainObject(value) { if (value === null || typeof value !== "object") return false; @@ -108,24 +136,56 @@ export class Lodash { return proto === null || proto === Object.prototype; } + /** + * 删除对象指定路径并返回对象。 + * Omit paths from object and return the same object. + * + * @param {object} [object={}] 目标对象 / Target object. + * @param {string|string[]} [paths=[]] 要删除的路径 / Paths to remove. + * @returns {object} + */ static omit(object = {}, paths = []) { if (!Array.isArray(paths)) paths = [paths.toString()]; paths.forEach(path => Lodash.unset(object, path)); return object; } + /** + * 仅保留对象指定键(第一层)。 + * Pick selected keys from object (top level only). + * + * @param {object} [object={}] 目标对象 / Target object. + * @param {string|string[]} [paths=[]] 需要保留的键 / Keys to keep. + * @returns {object} + */ static pick(object = {}, paths = []) { if (!Array.isArray(paths)) paths = [paths.toString()]; const filteredEntries = Object.entries(object).filter(([key, value]) => paths.includes(key)); return Object.fromEntries(filteredEntries); } + /** + * 按路径写入对象值。 + * Set object value by path. + * + * @param {object} object 目标对象 / Target object. + * @param {string|string[]} path 路径 / Path. + * @param {*} value 写入值 / Value. + * @returns {object} + */ static set(object, path, value) { if (!Array.isArray(path)) path = Lodash.toPath(path); path.slice(0, -1).reduce((previousValue, currentValue, currentIndex) => (Object(previousValue[currentValue]) === previousValue[currentValue] ? previousValue[currentValue] : (previousValue[currentValue] = /^\d+$/.test(path[currentIndex + 1]) ? [] : {})), object)[path[path.length - 1]] = value; return object; } + /** + * 将点路径或数组下标路径转换为数组。 + * Convert dot/array-index path string into path segments. + * + * @param {string} value 路径字符串 / Path string. + * @returns {string[]} + */ static toPath(value) { return value .replace(/\[(\d+)\]/g, ".$1") @@ -133,6 +193,13 @@ export class Lodash { .filter(Boolean); } + /** + * HTML 实体反转义。 + * Unescape HTML entities. + * + * @param {string} string 输入文本 / Input text. + * @returns {string} + */ static unescape(string) { const map = { "&": "&", @@ -144,6 +211,14 @@ export class Lodash { return string.replace(/&|<|>|"|'/g, m => map[m]); } + /** + * 删除对象路径对应的值。 + * Remove value by object path. + * + * @param {object} [object={}] 目标对象 / Target object. + * @param {string|string[]} [path=""] 路径 / Path. + * @returns {boolean} + */ static unset(object = {}, path = "") { if (!Array.isArray(path)) path = Lodash.toPath(path); const result = path.reduce((previousValue, currentValue, currentIndex) => { diff --git a/polyfill/StatusTexts.mjs b/polyfill/StatusTexts.mjs index 26a3d71..dddfa83 100644 --- a/polyfill/StatusTexts.mjs +++ b/polyfill/StatusTexts.mjs @@ -1,3 +1,9 @@ +/** + * HTTP 状态码文本映射表。 + * HTTP status code to status text map. + * + * @type {Record} + */ export const StatusTexts = { 100: "Continue", 101: "Switching Protocols", diff --git a/polyfill/Storage.mjs b/polyfill/Storage.mjs index 116d70a..79ee126 100644 --- a/polyfill/Storage.mjs +++ b/polyfill/Storage.mjs @@ -2,36 +2,53 @@ import { $app } from "../lib/app.mjs"; import { Lodash as _ } from "./Lodash.mjs"; /** - * Storage + * 跨平台持久化存储适配器。 + * Cross-platform persistent storage adapter. + * + * 支持后端: + * Supported backends: + * - Surge/Loon/Stash/Egern/Shadowrocket: `$persistentStore` + * - Quantumult X: `$prefs` + * - Node.js: 本地 `box.dat` + * - Node.js: local `box.dat` + * + * 支持路径键: + * Supports path key: + * - `@root.path.to.value` * * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Storage/setItem - * @export - * @class Storage - * @typedef {Storage} */ export class Storage { /** - * data + * Node.js 环境下的内存数据缓存。 + * In-memory data cache for Node.js runtime. * - * @static - * @type {file} + * @type {Record|null} */ static data = null; + + /** + * Node.js 持久化文件名。 + * Data file name used in Node.js. + * + * @type {string} + */ static dataFile = "box.dat"; + /** - * nameRegex + * `@key.path` 解析正则。 + * Regex for `@key.path` parsing. * - * @static - * @type {regexp} + * @type {RegExp} */ static #nameRegex = /^@(?[^.]+)(?:\.(?.*))?$/; /** - * getItem + * 读取存储值。 + * Read value from persistent storage. * - * @static - * @param {string} keyName - * @param {*} [defaultValue] + * @param {string} keyName 键名或路径键 / Key or path key. + * @param {*} [defaultValue=null] 默认值 / Default value when key is missing. * @returns {*} */ static getItem(keyName, defaultValue = null) { @@ -80,11 +97,11 @@ export class Storage { } /** - * setItem + * 写入存储值。 + * Write value into persistent storage. * - * @static - * @param {string} keyName - * @param {*} keyValue + * @param {string} keyName 键名或路径键 / Key or path key. + * @param {*} keyValue 写入值 / Value to store. * @returns {boolean} */ static setItem(keyName = new String(), keyValue = new String()) { @@ -135,10 +152,10 @@ export class Storage { } /** - * removeItem + * 删除存储值。 + * Remove value from persistent storage. * - * @static - * @param {string} keyName + * @param {string} keyName 键名或路径键 / Key or path key. * @returns {boolean} */ static removeItem(keyName) { @@ -178,9 +195,9 @@ export class Storage { } /** - * clear + * 清空存储(仅 Quantumult X 支持)。 + * Clear storage (supported by Quantumult X only). * - * @static * @returns {boolean} */ static clear() { @@ -207,10 +224,12 @@ export class Storage { } /** - * #loaddata + * 从 Node.js 数据文件加载 JSON。 + * Load JSON data from Node.js data file. * - * @param {string} dataFile - * @returns {*} + * @private + * @param {string} dataFile 数据文件名 / Data file name. + * @returns {Record} */ static #loaddata = dataFile => { if ($app === "Node.js") { @@ -232,9 +251,12 @@ export class Storage { }; /** - * #writedata + * 将内存数据写入 Node.js 数据文件。 + * Persist in-memory data to Node.js data file. * - * @param {string} [dataFile=this.dataFile] + * @private + * @param {string} [dataFile=this.dataFile] 数据文件名 / Data file name. + * @returns {void} */ static #writedata = (dataFile = this.dataFile) => { if ($app === "Node.js") { diff --git a/polyfill/fetch.mjs b/polyfill/fetch.mjs index 57abf36..553624b 100644 --- a/polyfill/fetch.mjs +++ b/polyfill/fetch.mjs @@ -4,14 +4,52 @@ import { Lodash as _ } from "./Lodash.mjs"; import { StatusTexts } from "./StatusTexts.mjs"; /** - * fetch + * 统一请求参数。 + * Unified request payload. + * + * @typedef {object} FetchRequest + * @property {string} url 请求地址 / Request URL. + * @property {string} [method] 请求方法 / HTTP method. + * @property {Record} [headers] 请求头 / Request headers. + * @property {string|ArrayBuffer|ArrayBufferView|object} [body] 请求体 / Request body. + * @property {ArrayBuffer} [bodyBytes] 二进制请求体 / Binary request body. + * @property {number|string} [timeout] 超时(秒或毫秒)/ Timeout (seconds or milliseconds). + * @property {string} [policy] 指定策略 / Preferred policy. + * @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects. + * @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag. + * @property {Record} [opts] 平台扩展字段 / Platform extension fields. + */ + +/** + * 统一响应结构。 + * Unified response payload. + * + * @typedef {object} FetchResponse + * @property {boolean} ok 请求是否成功 / Whether request is successful. + * @property {number} status 状态码 / HTTP status code. + * @property {number} [statusCode] 状态码别名 / Status code alias. + * @property {string} [statusText] 状态文本 / HTTP status text. + * @property {Record} [headers] 响应头 / Response headers. + * @property {string|ArrayBuffer} [body] 响应体 / Response body. + * @property {ArrayBuffer} [bodyBytes] 二进制响应体 / Binary response body. + */ + +/** + * 跨平台 `fetch` 适配层。 + * Cross-platform `fetch` adapter. + * + * 功能: + * Features: + * - 统一 Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Node.js 请求接口 + * - Normalize request APIs across Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Node.js + * - 统一返回体字段(`ok/status/statusText/body/bodyBytes`) + * - Normalize response fields (`ok/status/statusText/body/bodyBytes`) * * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API - * @export * @async - * @param {object|string} resource - * @param {object} [options] - * @returns {Promise} + * @param {FetchRequest|string} resource 请求对象或 URL / Request object or URL string. + * @param {Partial} [options={}] 追加参数 / Extra options. + * @returns {Promise} */ export async function fetch(resource, options = {}) { // 初始化参数 From b817c0731b4a2da615448f9d0c6479305670a26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 13 Feb 2026 22:06:57 +0800 Subject: [PATCH 52/72] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=20polyfill=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E8=AF=B4=E6=98=8E=E4=B8=8E=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 49 +++++++++++++++++++++++++++++++++++++++- polyfill/Console.mjs | 12 ++++++++++ polyfill/Lodash.mjs | 12 ++++++++++ polyfill/StatusTexts.mjs | 11 +++++++++ polyfill/Storage.mjs | 19 +++++++++++++++- polyfill/fetch.mjs | 19 +++++++++++++++- 6 files changed, 119 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 07e5d8d..86491d6 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ console.log(globalThis.$argument); // { mode: "on", a: { b: "1" } } 不可用/差异点: - `policy` 在 Egern / Shadowrocket 分支不做映射。 - Quantumult X 会丢弃未在白名单内的字段。 +- Quantumult X 的 `status` 在部分场景要求完整状态行(如 `HTTP/1.1 200 OK`),本库会在传入数字状态码时自动拼接(依赖 `StatusTexts`)。 - Node.js 不调用 `$done`,而是直接退出进程,且退出码固定为 `1`。 ### `lib/notification.mjs` @@ -308,6 +309,11 @@ 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` - 参数合并: @@ -356,9 +362,15 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); - `policy` 在 Surge / Egern / Node.js 分支没有额外适配逻辑。 - `redirection` 在部分平台会映射为 `auto-redirect` 或 `opts.redirection`。 - Node.js 分支依赖 `globalThis.fetch` / `globalThis.fetchCookie` 或 `node-fetch` + `fetch-cookie`。 +- 返回结构是统一兼容结构,不等同于浏览器 `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`。 @@ -380,6 +392,11 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); - 数据文件默认:`box.dat`。 - 读取路径优先级:当前目录 -> `process.cwd()`。 +与 Web Storage 的行为差异: +- 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。 +- `removeItem/clear` 仅部分平台可用(目前主要是 Quantumult X)。 +- `getItem` 会尝试 `JSON.parse`,`setItem` 写入对象会 `JSON.stringify`。 + 平台后端映射: | 平台 | 读写接口 | @@ -396,6 +413,21 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); - `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")` @@ -437,7 +469,11 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); ### `polyfill/Lodash.mjs` -`Lodash` 为轻量实现,包含: +`Lodash` 为“部分方法的简化实现”,不是完整 Lodash。各方法语义可参考: +- https://www.lodashjs.com +- https://lodash.com + +当前实现包含: - `escape(string)` - `unescape(string)` - `toPath(value)` @@ -474,6 +510,8 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); #### `StatusTexts` - 类型:`Record` - 内容: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 ## 平台差异总览 @@ -517,10 +555,19 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); - [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) ### 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/) diff --git a/polyfill/Console.mjs b/polyfill/Console.mjs index e500604..5e3b9f4 100644 --- a/polyfill/Console.mjs +++ b/polyfill/Console.mjs @@ -3,6 +3,18 @@ import { $app } from "../lib/app.mjs"; /** * 统一日志工具,兼容各脚本平台与 Node.js。 * Unified logger compatible with script platforms and Node.js. + * + * logLevel 用法: + * logLevel usage: + * - 可读: `Console.logLevel` 返回 `OFF|ERROR|WARN|INFO|DEBUG|ALL` + * - Read: `Console.logLevel` returns `OFF|ERROR|WARN|INFO|DEBUG|ALL` + * - 可写: 数字 `0~5` 或字符串 `off/error/warn/info/debug/all` + * - Write: number `0~5` or string `off/error/warn/info/debug/all` + * + * @example + * Console.logLevel = "debug"; + * Console.debug("only shown when level >= DEBUG"); + * Console.logLevel = 2; // WARN */ export class Console { static #counts = new Map([]); diff --git a/polyfill/Lodash.mjs b/polyfill/Lodash.mjs index 38782b3..5d90728 100644 --- a/polyfill/Lodash.mjs +++ b/polyfill/Lodash.mjs @@ -2,6 +2,18 @@ /** * 轻量 Lodash 工具集。 * Lightweight Lodash-like utilities. + * + * 说明: + * Notes: + * - 这是 Lodash 的“部分方法”简化实现,不等价于完整 Lodash + * - This is a simplified subset, not a full Lodash implementation + * - 各方法语义可参考 Lodash 官方文档 + * - Method semantics can be referenced from official Lodash docs + * + * 参考: + * Reference: + * - https://www.lodashjs.com + * - https://lodash.com */ export class Lodash { /** diff --git a/polyfill/StatusTexts.mjs b/polyfill/StatusTexts.mjs index dddfa83..40fd8fa 100644 --- a/polyfill/StatusTexts.mjs +++ b/polyfill/StatusTexts.mjs @@ -2,6 +2,17 @@ * HTTP 状态码文本映射表。 * HTTP status code to status text map. * + * 主要用途: + * Primary usage: + * - 为 Quantumult X 的 `$done` 状态行拼接提供状态文本 + * - Provide status text for Quantumult X `$done` status-line composition + * - QX 在部分场景要求 `status` 为完整状态行(如 `HTTP/1.1 200 OK`) + * - QX may require full status line (e.g. `HTTP/1.1 200 OK`) in some cases + * + * 参考: + * Reference: + * - https://github.com/crossutility/Quantumult-X/raw/refs/heads/master/sample-rewrite-response-header.js + * * @type {Record} */ export const StatusTexts = { diff --git a/polyfill/Storage.mjs b/polyfill/Storage.mjs index 79ee126..b567383 100644 --- a/polyfill/Storage.mjs +++ b/polyfill/Storage.mjs @@ -5,6 +5,13 @@ import { Lodash as _ } from "./Lodash.mjs"; * 跨平台持久化存储适配器。 * Cross-platform persistent storage adapter. * + * 设计目标: + * Design goal: + * - 仿照 Web Storage (`Storage`) 接口设计 + * - Modeled after Web Storage (`Storage`) interface + * - 统一 VPN App 脚本环境中的持久化读写接口 + * - Unify persistence APIs across VPN app script environments + * * 支持后端: * Supported backends: * - Surge/Loon/Stash/Egern/Shadowrocket: `$persistentStore` @@ -16,7 +23,17 @@ import { Lodash as _ } from "./Lodash.mjs"; * Supports path key: * - `@root.path.to.value` * - * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Storage/setItem + * 与 Web Storage 的已知差异: + * Known differences from Web Storage: + * - 支持 `@key.path` 深路径读写(Web Storage 原生不支持) + * - Supports `@key.path` deep-path access (not native in Web Storage) + * - `removeItem/clear` 并非所有平台都可用 + * - `removeItem/clear` are not available on every platform + * - 读取时会尝试 `JSON.parse`,写入对象会 `JSON.stringify` + * - Reads try `JSON.parse`, writes stringify objects + * + * @link https://developer.mozilla.org/en-US/docs/Web/API/Storage + * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Storage */ export class Storage { /** diff --git a/polyfill/fetch.mjs b/polyfill/fetch.mjs index 553624b..ec96343 100644 --- a/polyfill/fetch.mjs +++ b/polyfill/fetch.mjs @@ -38,6 +38,13 @@ import { StatusTexts } from "./StatusTexts.mjs"; * 跨平台 `fetch` 适配层。 * Cross-platform `fetch` adapter. * + * 设计目标: + * Design goal: + * - 仿照 Web API `fetch`(`Window.fetch`)接口设计 + * - Modeled after Web API `fetch` (`Window.fetch`) + * - 统一 VPN App 与 Node.js 环境中的请求调用 + * - Unify request calls across VPN apps and Node.js + * * 功能: * Features: * - 统一 Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Node.js 请求接口 @@ -45,7 +52,17 @@ import { StatusTexts } from "./StatusTexts.mjs"; * - 统一返回体字段(`ok/status/statusText/body/bodyBytes`) * - Normalize response fields (`ok/status/statusText/body/bodyBytes`) * - * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API + * 与 Web `fetch` 的已知差异: + * Known differences from Web `fetch`: + * - 支持 `policy`、`auto-redirect` 等平台扩展字段 + * - Supports platform extension fields like `policy` and `auto-redirect` + * - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现 + * - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine + * - 返回结构包含 `statusCode/bodyBytes` 等兼容字段 + * - Response includes compatibility fields like `statusCode/bodyBytes` + * + * @link https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch + * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch * @async * @param {FetchRequest|string} resource 请求对象或 URL / Request object or URL string. * @param {Partial} [options={}] 追加参数 / Extra options. From ce7c81a2e5c0b47b99b6f22884e8c106ccaa4270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 13 Feb 2026 22:11:35 +0800 Subject: [PATCH 53/72] =?UTF-8?q?docs:=20=E4=BC=98=E5=8C=96=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E4=B8=8E=E6=9B=B4=E6=96=B0=E6=8C=87=E5=BC=95=E9=9D=A2?= =?UTF-8?q?=E5=90=91=E6=96=B0=E6=89=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 86491d6..4565095 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,36 @@ - 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 -# GitHub Packages 示例(需先配置认证) +# 把 @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 From b0a1bd9ac8ef629a8b3de7a53bcbaec95dea3cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 13 Feb 2026 22:36:19 +0800 Subject: [PATCH 54/72] =?UTF-8?q?feat:=20=E4=BB=8E=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=20=20=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=8E=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++++++++++-------- index.js | 12 ++++++++++++ lib/index.js | 12 ++++++++++++ test/argument.test.js | 10 ++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4565095..33bac7a 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ npm i @nsnanocat/util@latest ```js import { $app, + $argument, // 已标准化的 $argument 快照(由包入口自动处理) done, fetch, notification, @@ -73,7 +74,7 @@ import { ### 包主入口(`index.js`)已导出 - `lib/app.mjs` -- `lib/argument.mjs`(`$argument` 参数标准化模块,无导出) +- `lib/argument.mjs`(`$argument` 参数标准化模块,导入时自动执行) - `lib/done.mjs` - `lib/notification.mjs` - `lib/time.mjs` @@ -163,10 +164,10 @@ console.log(environment()); // 当前环境对象 此文件无显式导出;`import` 后立即执行。这是为了统一各平台 `$argument` 的输入差异。 #### 行为 -- 单独使用时可直接格式化全局 `$argument`: - - `await import("@nsnanocat/util/lib/argument.mjs")` -- 通过包入口导入(`import ... from "@nsnanocat/util"`)时也会执行本模块: - - 读取到的 `$argument` 会按 URL Params 样式格式化为对象,并支持深路径。 +- 通过包入口导入(`import ... from "@nsnanocat/util"`)时会自动执行本模块。 +- JSCore 环境不支持 `await import`,请使用静态导入或直接走包入口导入。 +- 读取到的 `$argument` 会按 URL Params 样式格式化为对象,并支持深路径。 +- 你也可以通过 `import { $argument } from "@nsnanocat/util"` 读取当前已标准化的 `$argument` 快照。 - 平台输入差异说明: - Surge / Stash / Egern:脚本参数通常以字符串形式传入(如 `a=1&b=2`)。 - Loon:支持字符串和对象两种 `$argument` 形式。 @@ -182,9 +183,10 @@ console.log(environment()); // 当前环境对象 #### 用法 ```js -globalThis.$argument = "mode=on&a.b=1"; -await import("@nsnanocat/util/lib/argument.mjs"); -console.log(globalThis.$argument); // { mode: "on", a: { b: "1" } } +import { $argument } from "@nsnanocat/util"; + +// $argument = "mode=on&a.b=1"; // 示例入参,实际由模块参数注入 +console.log($argument); // { mode: "on", a: { b: "1" } } ``` ### `lib/done.mjs` diff --git a/index.js b/index.js index 1aaa932..904222a 100644 --- a/index.js +++ b/index.js @@ -10,3 +10,15 @@ export * from "./polyfill/Lodash.mjs"; export * from "./polyfill/StatusTexts.mjs"; export * from "./polyfill/Storage.mjs"; export * from "./getStorage.mjs"; + +/** + * 已标准化的 `$argument` 快照。 + * Normalized `$argument` snapshot. + */ +export const $argument = globalThis.$argument ?? {}; + +/** + * 兼容别名(建议优先使用 `$argument`)。 + * Backward-compatible alias (prefer `$argument`). + */ +export const argument = $argument; diff --git a/lib/index.js b/lib/index.js index acdf370..9c44600 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,3 +4,15 @@ export * from "./done.mjs"; export * from "./notification.mjs"; export * from "./time.mjs"; export * from "./wait.mjs"; + +/** + * 已标准化的 `$argument` 快照。 + * Normalized `$argument` snapshot. + */ +export const $argument = globalThis.$argument ?? {}; + +/** + * 兼容别名(建议优先使用 `$argument`)。 + * Backward-compatible alias (prefer `$argument`). + */ +export const argument = $argument; diff --git a/test/argument.test.js b/test/argument.test.js index af790e6..72d502d 100644 --- a/test/argument.test.js +++ b/test/argument.test.js @@ -3,6 +3,7 @@ import { afterEach, describe, it } from "node:test"; let importSeed = 0; const argumentModule = new URL("../lib/argument.mjs", import.meta.url); +const packageEntryModule = new URL("../index.js", import.meta.url); const importWithArgument = async value => { if (typeof value === "undefined") globalThis.$argument = {}; else globalThis.$argument = value; @@ -51,4 +52,13 @@ describe("argument", () => { assert.deepStrictEqual(result, { mode: "on" }); assert.deepStrictEqual(globalThis.$argument, { mode: "on" }); }); + + it("应该从包入口导出 $argument 快照", async () => { + globalThis.$argument = "a.b=1"; + importSeed += 1; + const mod = await import(`${packageEntryModule}?test=${importSeed}`); + assert.deepStrictEqual(mod.$argument, { a: { b: "1" } }); + assert.deepStrictEqual(mod.argument, { a: { b: "1" } }); + assert.deepStrictEqual(globalThis.$argument, { a: { b: "1" } }); + }); }); From 4105cf2e5d68e5fd8bfa130a86f1df7a1044235d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 13 Feb 2026 23:08:33 +0800 Subject: [PATCH 55/72] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=20getStora?= =?UTF-8?q?ge=20=E9=BB=98=E8=AE=A4=E5=AF=BC=E5=87=BA=E5=B9=B6=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E4=BD=BF=E7=94=A8=E8=BE=B9=E7=95=8C=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 28 ++++++++++++++++------------ index.js | 1 - 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 33bac7a..6ce1578 100644 --- a/README.md +++ b/README.md @@ -56,17 +56,16 @@ npm i @nsnanocat/util@latest ```js import { - $app, - $argument, // 已标准化的 $argument 快照(由包入口自动处理) - done, - fetch, - notification, - time, - wait, - Console, - Lodash, - Storage, - getStorage, + $app, // 当前平台名(如 "Surge" / "Loon" / "Quantumult X" / "Node.js") + $argument, // 已标准化的模块参数对象(导入包时自动处理字符串 -> 对象) + done, // 统一结束脚本函数(内部自动适配各平台 $done 差异) + fetch, // 统一 HTTP 请求函数(内部自动适配 $httpClient / $task / Node fetch) + notification, // 统一通知函数(内部自动适配 $notify / $notification.post) + time, // 时间格式化工具 + wait, // 延时等待工具(Promise) + Console, // 统一日志工具(支持 logLevel) + Lodash, // 内置的 Lodash 部分方法实现 + Storage, // 统一持久化存储接口(适配 $prefs / $persistentStore / 文件) } from "@nsnanocat/util"; ``` @@ -84,11 +83,11 @@ import { - `polyfill/Lodash.mjs` - `polyfill/StatusTexts.mjs` - `polyfill/Storage.mjs` -- `getStorage.mjs` ### 仓库中存在但未从主入口导出 - `lib/environment.mjs` - `lib/runScript.mjs` +- `getStorage.mjs`(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用) ## 模块依赖关系 @@ -306,6 +305,9 @@ await runScript("$done({})", { timeout: 20 }); ### `getStorage.mjs` +⚠️ 注意:该模块主要为薯条项目的存储结构设计,不作为通用默认 API。 +仅当你的持久化结构与薯条项目一致时才建议使用。 + #### `getStorage(key, names, database)` - 签名: - `key: string`(持久化主键) @@ -326,6 +328,8 @@ await runScript("$done({})", { timeout: 20 }); 示例: ```js +import { getStorage } from "@nsnanocat/util/getStorage.mjs"; + const store = getStorage("@my_box", ["YouTube", "Global"], database); ``` diff --git a/index.js b/index.js index 904222a..3649ba6 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,6 @@ export * from "./polyfill/fetch.mjs"; export * from "./polyfill/Lodash.mjs"; export * from "./polyfill/StatusTexts.mjs"; export * from "./polyfill/Storage.mjs"; -export * from "./getStorage.mjs"; /** * 已标准化的 `$argument` 快照。 From 0ee3081260348f5112880db30b6db532225d2b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Sat, 14 Feb 2026 10:44:33 +0800 Subject: [PATCH 56/72] =?UTF-8?q?docs(lodash):=20=E8=A1=A5=E5=85=85=20READ?= =?UTF-8?q?ME=20=E4=B8=8E=20JSDoc=20=E7=9A=84=E4=B8=AD=E8=8B=B1=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E9=93=BE=E6=8E=A5=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++- polyfill/Lodash.mjs | 22 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ce1578..0d0af51 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ import { time, // 时间格式化工具 wait, // 延时等待工具(Promise) Console, // 统一日志工具(支持 logLevel) - Lodash, // 内置的 Lodash 部分方法实现 + Lodash as _, // Lodash 建议按官方示例惯例使用 `_` 作为工具对象别名 Storage, // 统一持久化存储接口(适配 $prefs / $persistentStore / 文件) } from "@nsnanocat/util"; ``` @@ -499,6 +499,28 @@ console.log(Console.logLevel); // "WARN" - 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)` @@ -510,6 +532,35 @@ console.log(Console.logLevel); // "WARN" - `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 + 参数与返回值: | 方法 | 参数 | 返回值 | 说明 | diff --git a/polyfill/Lodash.mjs b/polyfill/Lodash.mjs index 5d90728..1733af6 100644 --- a/polyfill/Lodash.mjs +++ b/polyfill/Lodash.mjs @@ -9,6 +9,8 @@ * - This is a simplified subset, not a full Lodash implementation * - 各方法语义可参考 Lodash 官方文档 * - Method semantics can be referenced from official Lodash docs + * - 导入时建议使用 `Lodash as _`,遵循 lodash 官方示例惯例 + * - Use `Lodash as _` when importing, following official lodash example convention * * 参考: * Reference: @@ -22,6 +24,8 @@ export class Lodash { * * @param {string} string 输入文本 / Input text. * @returns {string} + * @see {@link https://lodash.com/docs/#escape lodash.escape} + * @see {@link https://www.lodashjs.com/docs/lodash.escape lodash.escape (中文)} */ static escape(string) { const map = { @@ -42,6 +46,8 @@ export class Lodash { * @param {string|string[]} [path=""] 路径 / Path. * @param {*} [defaultValue=undefined] 默认值 / Default value. * @returns {*} + * @see {@link https://lodash.com/docs/#get lodash.get} + * @see {@link https://www.lodashjs.com/docs/lodash.get lodash.get (中文)} */ static get(object = {}, path = "", defaultValue = undefined) { // translate array case to dot case, then split with . @@ -80,6 +86,8 @@ export class Lodash { * @param {...object} sources - Source objects. * @returns {object} 返回合并后的目标对象 * @returns {object} Merged target object. + * @see {@link https://lodash.com/docs/#merge lodash.merge} + * @see {@link https://www.lodashjs.com/docs/lodash.merge lodash.merge (中文)} * @example * const target = { a: { b: 1 }, c: 2 }; * const source = { a: { d: 3 }, e: 4 }; @@ -141,6 +149,8 @@ export class Lodash { * @param {*} value - Value to check. * @returns {boolean} 如果是普通对象返回 true * @returns {boolean} Returns true when value is a plain object. + * @see {@link https://lodash.com/docs/#isPlainObject lodash.isPlainObject} + * @see {@link https://www.lodashjs.com/docs/lodash.isPlainObject lodash.isPlainObject (中文)} */ static #isPlainObject(value) { if (value === null || typeof value !== "object") return false; @@ -155,6 +165,8 @@ export class Lodash { * @param {object} [object={}] 目标对象 / Target object. * @param {string|string[]} [paths=[]] 要删除的路径 / Paths to remove. * @returns {object} + * @see {@link https://lodash.com/docs/#omit lodash.omit} + * @see {@link https://www.lodashjs.com/docs/lodash.omit lodash.omit (中文)} */ static omit(object = {}, paths = []) { if (!Array.isArray(paths)) paths = [paths.toString()]; @@ -169,6 +181,8 @@ export class Lodash { * @param {object} [object={}] 目标对象 / Target object. * @param {string|string[]} [paths=[]] 需要保留的键 / Keys to keep. * @returns {object} + * @see {@link https://lodash.com/docs/#pick lodash.pick} + * @see {@link https://www.lodashjs.com/docs/lodash.pick lodash.pick (中文)} */ static pick(object = {}, paths = []) { if (!Array.isArray(paths)) paths = [paths.toString()]; @@ -184,6 +198,8 @@ export class Lodash { * @param {string|string[]} path 路径 / Path. * @param {*} value 写入值 / Value. * @returns {object} + * @see {@link https://lodash.com/docs/#set lodash.set} + * @see {@link https://www.lodashjs.com/docs/lodash.set lodash.set (中文)} */ static set(object, path, value) { if (!Array.isArray(path)) path = Lodash.toPath(path); @@ -197,6 +213,8 @@ export class Lodash { * * @param {string} value 路径字符串 / Path string. * @returns {string[]} + * @see {@link https://lodash.com/docs/#toPath lodash.toPath} + * @see {@link https://www.lodashjs.com/docs/lodash.toPath lodash.toPath (中文)} */ static toPath(value) { return value @@ -211,6 +229,8 @@ export class Lodash { * * @param {string} string 输入文本 / Input text. * @returns {string} + * @see {@link https://lodash.com/docs/#unescape lodash.unescape} + * @see {@link https://www.lodashjs.com/docs/lodash.unescape lodash.unescape (中文)} */ static unescape(string) { const map = { @@ -230,6 +250,8 @@ export class Lodash { * @param {object} [object={}] 目标对象 / Target object. * @param {string|string[]} [path=""] 路径 / Path. * @returns {boolean} + * @see {@link https://lodash.com/docs/#unset lodash.unset} + * @see {@link https://www.lodashjs.com/docs/lodash.unset lodash.unset (中文)} */ static unset(object = {}, path = "") { if (!Array.isArray(path)) path = Lodash.toPath(path); From c475e76c5506a401da9050464d34c35aa34061d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 00:41:22 +0800 Subject: [PATCH 57/72] fix(argument): normalize globalThis.$argument and guard null Update argument.mjs --- lib/argument.mjs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/argument.mjs b/lib/argument.mjs index d66f925..deb3a9c 100644 --- a/lib/argument.mjs +++ b/lib/argument.mjs @@ -18,25 +18,37 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs"; * Execution timing: * - 该模块为即时执行模块,`import` 时立即处理全局 `$argument` * - This module executes immediately and mutates global `$argument` on import + * + * 归一化规则补充: + * Normalization details: + * - 使用 `globalThis.$argument` 读写,避免运行环境下未声明变量引用问题 + * - Read/write via `globalThis.$argument` to avoid undeclared variable access + * - 当 `$argument` 为 `null` 或 `undefined` 时,会重置为 `{}` + * - When `$argument` is `null` or `undefined`, it is normalized to `{}` */ (() => { Console.debug("☑️ $argument"); - switch (typeof $argument) { + switch (typeof globalThis.$argument) { case "string": { - const argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - $argument = {}; - Object.keys(argument).forEach(key => _.set($argument, key, argument[key])); + const argument = Object.fromEntries(globalThis.$argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); + globalThis.$argument = {}; + Object.keys(argument).forEach(key => _.set(globalThis.$argument, key, argument[key])); break; } case "object": { + if (globalThis.$argument === null) { + globalThis.$argument = {}; + break; + } const argument = {}; - Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); - $argument = argument; + Object.keys(globalThis.$argument).forEach(key => _.set(argument, key, globalThis.$argument[key])); + globalThis.$argument = argument; break; } case "undefined": + globalThis.$argument = {}; break; } - if ($argument.LogLevel) Console.logLevel = $argument.LogLevel; - Console.debug("✅ $argument", `$argument: ${JSON.stringify($argument)}`); + if (globalThis.$argument.LogLevel) Console.logLevel = globalThis.$argument.LogLevel; + Console.debug("✅ $argument", `$argument: ${JSON.stringify(globalThis.$argument)}`); })(); From 3a1c8bbda718f98131431ca5b3b370140ee01695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 00:51:34 +0800 Subject: [PATCH 58/72] =?UTF-8?q?fix(getStorage):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E9=80=BB=E8=BE=91=E4=BB=A5=E5=8C=85=E5=90=AB?= =?UTF-8?q?=20$argument=20=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- getStorage.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index 49a130b..743e5f4 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -57,11 +57,11 @@ export function getStorage(key, names, database) { } /***************** Merge *****************/ names.forEach(name => { - _.merge(Store.Settings, database?.[name]?.Settings, BoxJs?.[name]?.Settings); + _.merge(Store.Settings, database?.[name]?.Settings, $argument, BoxJs?.[name]?.Settings); _.merge(Store.Configs, database?.[name]?.Configs); _.merge(Store.Caches, BoxJs?.[name]?.Caches); }); - _.merge(Store.Settings, $argument); + if ($argument.Storage === "$argument") _.merge(Store.Settings, $argument); if (Store.Settings.LogLevel) Console.logLevel = Store.Settings.LogLevel; Console.debug("✅ Merge", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); /***************** traverseObject *****************/ From 570a75c0618ed4c66a68418c652feda583a1eab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 00:52:47 +0800 Subject: [PATCH 59/72] =?UTF-8?q?refactor(getStorage):=20=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E5=8F=98=E9=87=8F=20Store=20=E4=B8=BA=20Root?= =?UTF-8?q?=20=E5=B9=B6=E6=9B=B4=E6=96=B0=E8=B0=83=E8=AF=95=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- getStorage.mjs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index 743e5f4..b9fc06e 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -36,8 +36,8 @@ export function getStorage(key, names, database) { Console.debug("☑️ getStorage"); names = [names].flat(Number.POSITIVE_INFINITY); /***************** Default *****************/ - const Store = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; - Console.debug("Default", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + const Root = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; + Console.debug("Default", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); /***************** BoxJs *****************/ // 包装为局部变量,用完释放内存 // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 @@ -53,19 +53,19 @@ export function getStorage(key, names, database) { } }); if (BoxJs.LogLevel) Console.logLevel = BoxJs.LogLevel; - Console.debug("✅ BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + Console.debug("✅ BoxJs", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); } /***************** Merge *****************/ names.forEach(name => { - _.merge(Store.Settings, database?.[name]?.Settings, $argument, BoxJs?.[name]?.Settings); - _.merge(Store.Configs, database?.[name]?.Configs); - _.merge(Store.Caches, BoxJs?.[name]?.Caches); + _.merge(Root.Settings, database?.[name]?.Settings, $argument, BoxJs?.[name]?.Settings); + _.merge(Root.Configs, database?.[name]?.Configs); + _.merge(Root.Caches, BoxJs?.[name]?.Caches); }); - if ($argument.Storage === "$argument") _.merge(Store.Settings, $argument); - if (Store.Settings.LogLevel) Console.logLevel = Store.Settings.LogLevel; - Console.debug("✅ Merge", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + if ($argument.Storage === "$argument") _.merge(Root.Settings, $argument); + if (Root.Settings.LogLevel) Console.logLevel = Root.Settings.LogLevel; + Console.debug("✅ Merge", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); /***************** traverseObject *****************/ - traverseObject(Store.Settings, (key, value) => { + traverseObject(Root.Settings, (key, value) => { Console.debug("☑️ traverseObject", `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`); if (value === "true" || value === "false") value = JSON.parse(value); // 字符串转Boolean @@ -76,9 +76,9 @@ export function getStorage(key, names, database) { } return value; }); - Console.debug("✅ traverseObject", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); + Console.debug("✅ traverseObject", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); Console.debug("✅ getStorage"); - return Store; + return Root; } /** From 8a5989238cb5ca3a36528874c9a15b0fbec65ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 00:55:27 +0800 Subject: [PATCH 60/72] =?UTF-8?q?fix(getStorage):=20=E6=A0=B9=E6=8D=AE=20$?= =?UTF-8?q?argument.Storage=20=E7=9A=84=E5=80=BC=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E9=80=BB=E8=BE=91=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=E7=9A=84=E9=85=8D=E7=BD=AE=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- getStorage.mjs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index b9fc06e..a70e056 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -57,11 +57,24 @@ export function getStorage(key, names, database) { } /***************** Merge *****************/ names.forEach(name => { - _.merge(Root.Settings, database?.[name]?.Settings, $argument, BoxJs?.[name]?.Settings); + switch ($argument.Storage) { + case "$argument": + _.merge(Root.Settings, database?.[name]?.Settings, BoxJs?.[name]?.Settings, $argument); + break; + case "BoxJs": + _.merge(Root.Settings, database?.[name]?.Settings, BoxJs?.[name]?.Settings); + break; + case "database": + _.merge(Root.Settings, database?.[name]?.Settings); + break; + default: + case undefined: + _.merge(Root.Settings, database?.[name]?.Settings, $argument, BoxJs?.[name]?.Settings); + break; + } _.merge(Root.Configs, database?.[name]?.Configs); _.merge(Root.Caches, BoxJs?.[name]?.Caches); }); - if ($argument.Storage === "$argument") _.merge(Root.Settings, $argument); if (Root.Settings.LogLevel) Console.logLevel = Root.Settings.LogLevel; Console.debug("✅ Merge", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); /***************** traverseObject *****************/ From 5fa69e4fbb63b290a0a66e709a21f7e7fd99e2e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 00:58:12 +0800 Subject: [PATCH 61/72] =?UTF-8?q?fix(getStorage):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E9=80=BB=E8=BE=91=E4=BB=A5=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=20PersistentStore=20=E6=9B=BF=E4=BB=A3=20BoxJs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update getStorage.mjs --- getStorage.mjs | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/getStorage.mjs b/getStorage.mjs index a70e056..1517ba0 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -17,12 +17,12 @@ import { Storage } from "./polyfill/Storage.mjs"; * 读取并合并默认配置、持久化配置与 `$argument`。 * Read and merge default config, persisted config and `$argument`. * - * 合并顺序: - * Merge order: - * 1) `database.Default` - * 2) BoxJS persisted value - * 3) `database[name]` + `BoxJs[name]` - * 4) `$argument` + * 合并来源与顺序由 `$argument.Storage` 控制: + * Merge source order is controlled by `$argument.Storage`: + * - `undefined`(默认): `database[name]` -> `$argument` -> `PersistentStore[name]` + * - `$argument`: `database[name]` -> `PersistentStore[name]` -> `$argument` + * - `PersistentStore` / `BoxJs`: `database[name]` -> `PersistentStore[name]` + * - `database`: 仅 `database[name]` * * @link https://github.com/NanoCat-Me/utils/blob/main/getStorage.mjs * @author VirgilClyne @@ -38,42 +38,43 @@ export function getStorage(key, names, database) { /***************** Default *****************/ const Root = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; Console.debug("Default", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); - /***************** BoxJs *****************/ + /***************** PersistentStore *****************/ // 包装为局部变量,用完释放内存 - // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 - const BoxJs = Storage.getItem(key); - if (BoxJs) { - Console.debug("☑️ BoxJs", `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs || {})}`); + // BoxJs 的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 + const PersistentStore = Storage.getItem(key, {}); + if (PersistentStore) { + Console.debug("☑️ PersistentStore", `PersistentStore类型: ${typeof PersistentStore}`, `PersistentStore内容: ${JSON.stringify(PersistentStore || {})}`); names.forEach(name => { - if (typeof BoxJs?.[name]?.Settings === "string") { - BoxJs[name].Settings = JSON.parse(BoxJs[name].Settings || "{}"); + if (typeof PersistentStore?.[name]?.Settings === "string") { + PersistentStore[name].Settings = JSON.parse(PersistentStore[name].Settings || "{}"); } - if (typeof BoxJs?.[name]?.Caches === "string") { - BoxJs[name].Caches = JSON.parse(BoxJs[name].Caches || "{}"); + if (typeof PersistentStore?.[name]?.Caches === "string") { + PersistentStore[name].Caches = JSON.parse(PersistentStore[name].Caches || "{}"); } }); - if (BoxJs.LogLevel) Console.logLevel = BoxJs.LogLevel; - Console.debug("✅ BoxJs", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); + if (PersistentStore.LogLevel) Console.logLevel = PersistentStore.LogLevel; + Console.debug("✅ PersistentStore", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); } /***************** Merge *****************/ names.forEach(name => { switch ($argument.Storage) { case "$argument": - _.merge(Root.Settings, database?.[name]?.Settings, BoxJs?.[name]?.Settings, $argument); + _.merge(Root.Settings, database?.[name]?.Settings, PersistentStore?.[name]?.Settings, $argument); break; case "BoxJs": - _.merge(Root.Settings, database?.[name]?.Settings, BoxJs?.[name]?.Settings); + case "PersistentStore": + _.merge(Root.Settings, database?.[name]?.Settings, PersistentStore?.[name]?.Settings); break; case "database": _.merge(Root.Settings, database?.[name]?.Settings); break; default: case undefined: - _.merge(Root.Settings, database?.[name]?.Settings, $argument, BoxJs?.[name]?.Settings); + _.merge(Root.Settings, database?.[name]?.Settings, $argument, PersistentStore?.[name]?.Settings); break; } _.merge(Root.Configs, database?.[name]?.Configs); - _.merge(Root.Caches, BoxJs?.[name]?.Caches); + _.merge(Root.Caches, PersistentStore?.[name]?.Caches); }); if (Root.Settings.LogLevel) Console.logLevel = Root.Settings.LogLevel; Console.debug("✅ Merge", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); From 23ebecb26dfb7e32e456260c7e07c42d8a2a0dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 01:02:09 +0800 Subject: [PATCH 62/72] =?UTF-8?q?fix(Storage):=20=E5=9C=A8=20Surge=20?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E4=B8=AD=E6=B7=BB=E5=8A=A0=E5=AF=B9=20null?= =?UTF-8?q?=20=E5=80=BC=E7=9A=84=E5=86=99=E5=85=A5=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Storage.mjs --- polyfill/Storage.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/polyfill/Storage.mjs b/polyfill/Storage.mjs index b567383..f716538 100644 --- a/polyfill/Storage.mjs +++ b/polyfill/Storage.mjs @@ -172,6 +172,12 @@ export class Storage { * 删除存储值。 * Remove value from persistent storage. * + * 平台说明: + * Platform notes: + * - Quantumult X: `$prefs.removeValueForKey` + * - Surge: 通过 `$persistentStore.write(null, keyName)` 删除 + * - 其余平台当前返回 `false` + * * @param {string} keyName 键名或路径键 / Key or path key. * @returns {boolean} */ @@ -190,6 +196,8 @@ export class Storage { default: switch ($app) { case "Surge": + result = $persistentStore.write(null, keyName); + break; case "Loon": case "Stash": case "Egern": From 76a3dc17d4d7162b304262bbe199eb7e63412b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 01:13:45 +0800 Subject: [PATCH 63/72] docs(changelog): add bilingual changelog for v2.1.2 Update README.md --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ README.md | 21 +++++++++++---------- 2 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab32549 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +变更日志 + +All notable changes to this project will be documented in this file. + +项目中的所有重要变更都会记录在此文件中。 + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +格式参考 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)。 + +## [2.1.2] - 2026-02-20 + +### Fixed / 修复 +- `fix(argument)`: Normalize `globalThis.$argument` and guard `null`; 标准化 `globalThis.$argument` 并处理 `null` 场景(`c475e76`)。 +- `fix(getStorage)`: Include `$argument` in merge flow with conditional handling; 修复合并流程以包含 `$argument` 并增加条件控制(`3a1c8bb`)。 +- `fix(getStorage)`: Add merge source control by `$argument.Storage`; 支持通过 `$argument.Storage` 控制合并来源(`8a59892`)。 +- `fix(getStorage)`: Replace `BoxJs` merge source naming/usage with `PersistentStore`; 将合并来源命名/实现统一为 `PersistentStore`(`5fa69e4`)。 +- `fix(Storage)`: Add Surge `removeItem` support via `$persistentStore.write(null, keyName)`; 为 Surge 增加 `removeItem` 删除支持(`23ebecb`)。 + +### Changed / 变更 +- `refactor(getStorage)`: Rename `Store` to `Root` and align debug output; 重命名 `Store` 为 `Root` 并同步调试输出字段(`570a75c`)。 + +### Docs / 文档 +- Sync README/JSDoc with recent behavior changes for `argument` / `getStorage` / `Storage`; 同步 `argument` / `getStorage` / `Storage` 的 README 与 JSDoc 说明(`2b13601`)。 + +[2.1.2]: https://github.com/NSNanoCat/util/compare/main...dev diff --git a/README.md b/README.md index 0d0af51..c9e6f00 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ console.log(environment()); // 当前环境对象 - 使用点路径展开对象(`a.b=1 -> { a: { b: "1" } }`)。 - 当全局 `$argument` 为 `object` 时: - 将 key 当路径写回新对象(`{"a.b":"1"}` -> `{a:{b:"1"}}`)。 -- 当 `$argument` 为 `undefined`:不处理。 +- 当 `$argument` 为 `null` 或 `undefined`:会归一化为 `{}`。 - 若 `$argument.LogLevel` 存在:同步到 `Console.logLevel`。 #### 用法 @@ -315,13 +315,13 @@ await runScript("$done({})", { timeout: 20 }); - `database: object`(默认数据库) - 返回:`{ Settings, Configs, Caches }` -合并顺序: -1. `database.Default` -> 初始 `Store` -2. 持久化中的 BoxJS 值(`Storage.getItem(key)`) -3. 按 `names` 合并 `database[name]` + `BoxJs[name]` -4. 最后合并 `$argument` +合并顺序由 `$argument.Storage` 控制(持久化读取统一使用 `PersistentStore = Storage.getItem(key, {})`): +1. 默认(`undefined`):`database[name]` -> `$argument` -> `PersistentStore[name]` +2. `$argument`:`database[name]` -> `PersistentStore[name]` -> `$argument` +3. `PersistentStore` / `BoxJs`:`database[name]` -> `PersistentStore[name]` +4. `database`:仅 `database[name]` -自动类型转换(`Store.Settings`): +自动类型转换(`Root.Settings`): - 字符串 `"true"/"false"` -> `boolean` - 纯数字字符串 -> `number` - 含逗号字符串 -> `array`,并尝试逐项转数字 @@ -408,7 +408,8 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); #### `Storage.removeItem(keyName)` - Quantumult X:可用(`$prefs.removeValueForKey`)。 -- Surge / Loon / Stash / Egern / Shadowrocket / Node.js:返回 `false`。 +- Surge:通过 `$persistentStore.write(null, keyName)` 删除。 +- Loon / Stash / Egern / Shadowrocket / Node.js:返回 `false`。 #### `Storage.clear()` - Quantumult X:可用(`$prefs.removeAllValues`)。 @@ -420,7 +421,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); 与 Web Storage 的行为差异: - 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。 -- `removeItem/clear` 仅部分平台可用(目前主要是 Quantumult X)。 +- `removeItem/clear` 仅部分平台可用(目前为 Quantumult X,以及 Surge 的 `removeItem`)。 - `getItem` 会尝试 `JSON.parse`,`setItem` 写入对象会 `JSON.stringify`。 平台后端映射: @@ -600,7 +601,7 @@ console.log(value); // 1 | 通知 | `$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` | 可用 | 不可用 | `removeItem` 可用 / `clear` 不可用 | 不可用 | 不可用 | 不可用 | 不可用 | | `policy` 注入(`fetch/done`) | `opts.policy` | `node` | `X-Surge-Policy`(done) | `X-Stash-Selected-Proxy` | 无专门映射 | `X-Surge-Proxy`(fetch) | 无 | ## 已知限制与注意事项 From 6bccb000e67e0c8718810e45b2e600de25011f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 01:48:04 +0800 Subject: [PATCH 64/72] docs(getStorage): clarify Settings/Configs/Caches merge; update JSDoc/README/CHANGELOG Update CHANGELOG.md --- CHANGELOG.md | 3 ++- README.md | 2 ++ getStorage.mjs | 49 +++++++++++++++++++++++++++++++++---------------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab32549..73c6b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 格式参考 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)。 -## [2.1.2] - 2026-02-20 +## [2.1.3] - 2026-02-20 ### Fixed / 修复 - `fix(argument)`: Normalize `globalThis.$argument` and guard `null`; 标准化 `globalThis.$argument` 并处理 `null` 场景(`c475e76`)。 @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed / 变更 - `refactor(getStorage)`: Rename `Store` to `Root` and align debug output; 重命名 `Store` 为 `Root` 并同步调试输出字段(`570a75c`)。 +- `refactor(getStorage)`: Centralize `Settings` merge controlled by `$argument.Storage`; ensure `Configs`/`Caches` are merged per-profile (`names`)(`17747ae`)。 ### Docs / 文档 - Sync README/JSDoc with recent behavior changes for `argument` / `getStorage` / `Storage`; 同步 `argument` / `getStorage` / `Storage` 的 README 与 JSDoc 说明(`2b13601`)。 diff --git a/README.md b/README.md index c9e6f00..562cf2e 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,8 @@ await runScript("$done({})", { timeout: 20 }); 3. `PersistentStore` / `BoxJs`:`database[name]` -> `PersistentStore[name]` 4. `database`:仅 `database[name]` +注意:`Configs` 与 `Caches` 始终按每个 `name` 合并(与 `$argument.Storage` 无关)。 + 自动类型转换(`Root.Settings`): - 字符串 `"true"/"false"` -> `boolean` - 纯数字字符串 -> `number` diff --git a/getStorage.mjs b/getStorage.mjs index 1517ba0..bd6fd52 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -17,6 +17,9 @@ import { Storage } from "./polyfill/Storage.mjs"; * 读取并合并默认配置、持久化配置与 `$argument`。 * Read and merge default config, persisted config and `$argument`. * + * 注意:`Configs` 与 `Caches` 始终按每个 profile(`names`)合并;`Settings` 的合并顺序由 `$argument.Storage` 控制。 + * Note: `Configs` and `Caches` are always merged per-profile (`names`); the merge order for `Settings` is controlled by `$argument.Storage`. + * * 合并来源与顺序由 `$argument.Storage` 控制: * Merge source order is controlled by `$argument.Storage`: * - `undefined`(默认): `database[name]` -> `$argument` -> `PersistentStore[name]` @@ -24,6 +27,7 @@ import { Storage } from "./polyfill/Storage.mjs"; * - `PersistentStore` / `BoxJs`: `database[name]` -> `PersistentStore[name]` * - `database`: 仅 `database[name]` * + * @since 2.1.2 * @link https://github.com/NanoCat-Me/utils/blob/main/getStorage.mjs * @author VirgilClyne * @param {string} key 持久化主键 / Persistent store key. @@ -57,25 +61,38 @@ export function getStorage(key, names, database) { } /***************** Merge *****************/ names.forEach(name => { - switch ($argument.Storage) { - case "$argument": - _.merge(Root.Settings, database?.[name]?.Settings, PersistentStore?.[name]?.Settings, $argument); - break; - case "BoxJs": - case "PersistentStore": - _.merge(Root.Settings, database?.[name]?.Settings, PersistentStore?.[name]?.Settings); - break; - case "database": - _.merge(Root.Settings, database?.[name]?.Settings); - break; - default: - case undefined: - _.merge(Root.Settings, database?.[name]?.Settings, $argument, PersistentStore?.[name]?.Settings); - break; - } _.merge(Root.Configs, database?.[name]?.Configs); _.merge(Root.Caches, PersistentStore?.[name]?.Caches); }); + switch ($argument.Storage) { + case "$argument": + names.forEach(name => { + _.merge(Root.Settings, database?.[name]?.Settings, PersistentStore?.[name]?.Settings); + }); + _.merge(Root.Settings, $argument); + break; + case "BoxJs": + case "PersistentStore": + names.forEach(name => { + _.merge(Root.Settings, database?.[name]?.Settings, PersistentStore?.[name]?.Settings); + }); + break; + case "database": + names.forEach(name => { + _.merge(Root.Settings, database?.[name]?.Settings); + }); + break; + default: + case undefined: + names.forEach(name => { + _.merge(Root.Settings, database?.[name]?.Settings); + }); + _.merge(Root.Settings, $argument); + names.forEach(name => { + _.merge(Root.Settings, PersistentStore?.[name]?.Settings); + }); + break; + } if (Root.Settings.LogLevel) Console.logLevel = Root.Settings.LogLevel; Console.debug("✅ Merge", `Root.Settings类型: ${typeof Root.Settings}`, `Root.Settings: ${JSON.stringify(Root.Settings)}`); /***************** traverseObject *****************/ From 99869a0294425bdc748ae8892a0503deae3b1406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 03:34:36 +0800 Subject: [PATCH 65/72] fix(getStorage): treat undefined .Storage as PersistentStore; update JSDoc/README --- CHANGELOG.md | 2 ++ README.md | 4 ++-- getStorage.mjs | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c6b38..c58ae7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,4 +26,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Docs / 文档 - Sync README/JSDoc with recent behavior changes for `argument` / `getStorage` / `Storage`; 同步 `argument` / `getStorage` / `Storage` 的 README 与 JSDoc 说明(`2b13601`)。 +[2.1.3]: https://github.com/NSNanoCat/util/compare/main...dev + [2.1.2]: https://github.com/NSNanoCat/util/compare/main...dev diff --git a/README.md b/README.md index 562cf2e..de3b3c7 100644 --- a/README.md +++ b/README.md @@ -316,9 +316,9 @@ await runScript("$done({})", { timeout: 20 }); - 返回:`{ Settings, Configs, Caches }` 合并顺序由 `$argument.Storage` 控制(持久化读取统一使用 `PersistentStore = Storage.getItem(key, {})`): -1. 默认(`undefined`):`database[name]` -> `$argument` -> `PersistentStore[name]` +1. `undefined`:`database[name]` -> `PersistentStore[name]`(不再自动合并 `$argument`) 2. `$argument`:`database[name]` -> `PersistentStore[name]` -> `$argument` -3. `PersistentStore` / `BoxJs`:`database[name]` -> `PersistentStore[name]` +3. 默认`PersistentStore` / `BoxJs`:`database[name]` -> `PersistentStore[name]` 4. `database`:仅 `database[name]` 注意:`Configs` 与 `Caches` 始终按每个 `name` 合并(与 `$argument.Storage` 无关)。 diff --git a/getStorage.mjs b/getStorage.mjs index bd6fd52..5bd1a5f 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -22,12 +22,12 @@ import { Storage } from "./polyfill/Storage.mjs"; * * 合并来源与顺序由 `$argument.Storage` 控制: * Merge source order is controlled by `$argument.Storage`: - * - `undefined`(默认): `database[name]` -> `$argument` -> `PersistentStore[name]` + * - `undefined`: `database[name]` -> `$argument` -> `PersistentStore[name]` * - `$argument`: `database[name]` -> `PersistentStore[name]` -> `$argument` - * - `PersistentStore` / `BoxJs`: `database[name]` -> `PersistentStore[name]` + * - `PersistentStore` / `BoxJs`(默认): `database[name]` -> `PersistentStore[name]` * - `database`: 仅 `database[name]` * - * @since 2.1.2 + * @since 2.1.3 * @link https://github.com/NanoCat-Me/utils/blob/main/getStorage.mjs * @author VirgilClyne * @param {string} key 持久化主键 / Persistent store key. @@ -71,6 +71,7 @@ export function getStorage(key, names, database) { }); _.merge(Root.Settings, $argument); break; + default: case "BoxJs": case "PersistentStore": names.forEach(name => { @@ -82,7 +83,6 @@ export function getStorage(key, names, database) { _.merge(Root.Settings, database?.[name]?.Settings); }); break; - default: case undefined: names.forEach(name => { _.merge(Root.Settings, database?.[name]?.Settings); From 45f5cd8c397e2426b7fefdeaac128ef8c92344f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 13:02:13 +0800 Subject: [PATCH 66/72] fix(getStorage): accept lowercase 'boxjs' alias; docs --- CHANGELOG.md | 2 ++ README.md | 9 +++++---- getStorage.mjs | 17 +++++++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c58ae7e..ae0d026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `fix(getStorage)`: Add merge source control by `$argument.Storage`; 支持通过 `$argument.Storage` 控制合并来源(`8a59892`)。 - `fix(getStorage)`: Replace `BoxJs` merge source naming/usage with `PersistentStore`; 将合并来源命名/实现统一为 `PersistentStore`(`5fa69e4`)。 - `fix(Storage)`: Add Surge `removeItem` support via `$persistentStore.write(null, keyName)`; 为 Surge 增加 `removeItem` 删除支持(`23ebecb`)。 +- `fix(getStorage)`: Accept lowercase `boxjs` as alias for `BoxJs`; 支持小写别名 `boxjs`。 ### Changed / 变更 - `refactor(getStorage)`: Rename `Store` to `Root` and align debug output; 重命名 `Store` 为 `Root` 并同步调试输出字段(`570a75c`)。 @@ -25,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Docs / 文档 - Sync README/JSDoc with recent behavior changes for `argument` / `getStorage` / `Storage`; 同步 `argument` / `getStorage` / `Storage` 的 README 与 JSDoc 说明(`2b13601`)。 +- `docs(getStorage)`: Document aliases for `$argument.Storage` (`Argument` / `$argument`, `PersistentStore` / `BoxJs` / `$persistentStore`) and correct merge-order in README/JSDoc; 为 `$argument.Storage` 增加别名说明并修正 README 中的合并顺序说明。 [2.1.3]: https://github.com/NSNanoCat/util/compare/main...dev diff --git a/README.md b/README.md index de3b3c7..fef5848 100644 --- a/README.md +++ b/README.md @@ -315,10 +315,11 @@ await runScript("$done({})", { timeout: 20 }); - `database: object`(默认数据库) - 返回:`{ Settings, Configs, Caches }` -合并顺序由 `$argument.Storage` 控制(持久化读取统一使用 `PersistentStore = Storage.getItem(key, {})`): -1. `undefined`:`database[name]` -> `PersistentStore[name]`(不再自动合并 `$argument`) -2. `$argument`:`database[name]` -> `PersistentStore[name]` -> `$argument` -3. 默认`PersistentStore` / `BoxJs`:`database[name]` -> `PersistentStore[name]` +合并顺序由 `$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` 无关)。 diff --git a/getStorage.mjs b/getStorage.mjs index 5bd1a5f..0257058 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -17,16 +17,18 @@ import { Storage } from "./polyfill/Storage.mjs"; * 读取并合并默认配置、持久化配置与 `$argument`。 * Read and merge default config, persisted config and `$argument`. * - * 注意:`Configs` 与 `Caches` 始终按每个 profile(`names`)合并;`Settings` 的合并顺序由 `$argument.Storage` 控制。 - * Note: `Configs` and `Caches` are always merged per-profile (`names`); the merge order for `Settings` is controlled by `$argument.Storage`. + * 注意:`Configs` 与 `Caches` 始终按每个 profile(`names`)合并;`Settings` 的合并顺序由 `$argument.Storage` 控制(支持别名)。 + * Note: `Configs` and `Caches` are always merged per-profile (`names`); the merge order for `Settings` is controlled by `$argument.Storage` (aliases supported). * - * 合并来源与顺序由 `$argument.Storage` 控制: - * Merge source order is controlled by `$argument.Storage`: + * 合并来源与顺序由 `$argument.Storage` 控制(支持以下值 / 别名): + * Merge source order is controlled by `$argument.Storage` (accepted values / aliases): * - `undefined`: `database[name]` -> `$argument` -> `PersistentStore[name]` - * - `$argument`: `database[name]` -> `PersistentStore[name]` -> `$argument` - * - `PersistentStore` / `BoxJs`(默认): `database[name]` -> `PersistentStore[name]` + * - `Argument` / `$argument`: `database[name]` -> `PersistentStore[name]` -> `$argument` + * - `PersistentStore` / `BoxJs` / `boxjs` / `$persistentStore`(默认):`database[name]` -> `PersistentStore[name]` * - `database`: 仅 `database[name]` * + * 注意:字符串比较为精确匹配(区分大小写)。 + * * @since 2.1.3 * @link https://github.com/NanoCat-Me/utils/blob/main/getStorage.mjs * @author VirgilClyne @@ -65,6 +67,7 @@ export function getStorage(key, names, database) { _.merge(Root.Caches, PersistentStore?.[name]?.Caches); }); switch ($argument.Storage) { + case "Argument": case "$argument": names.forEach(name => { _.merge(Root.Settings, database?.[name]?.Settings, PersistentStore?.[name]?.Settings); @@ -73,7 +76,9 @@ export function getStorage(key, names, database) { break; default: case "BoxJs": + case "boxjs": case "PersistentStore": + case "$persistentStore": names.forEach(name => { _.merge(Root.Settings, database?.[name]?.Settings, PersistentStore?.[name]?.Settings); }); From 5a5994ae6776b3a87a2c544b97b25441d39bfd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 23:38:29 +0800 Subject: [PATCH 67/72] docs: update changelog order and reflect export change --- CHANGELOG.md | 38 +++++++++++++++++++++++++++++++++++--- README.md | 2 +- getStorage.mjs | 5 ++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0d026..202d698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,24 +10,56 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 格式参考 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)。 +## [2.1.4] - 2026-02-20 + +### Fixed / 修复 +- `fix(getStorage)`: Accept lowercase `boxjs` as alias for `BoxJs`; 支持小写别名 `boxjs`。(`45f5cd8`) +- `fix(getStorage)`: Treat undefined `$argument.Storage` the same as `PersistentStore`; 将未定义的 `.Storage` 视为 `PersistentStore` 并更新文档。(`99869a0`) + +### Docs / 文档 +- `docs(getStorage)`: Update README/JSDoc to mention undefined `.Storage` defaulting and adjust import example; 在 README/JSDoc 中说明当 `$argument.Storage` 未定义时视为 `PersistentStore`,并修正导入示例。 + ## [2.1.3] - 2026-02-20 +### Docs / 文档 +- `docs(getStorage)`: clarify Settings/Configs/Caches merge; update JSDoc/README/CHANGELOG (commit 6bccb00)。 + +## [2.1.2] - 2026-02-20 + ### Fixed / 修复 - `fix(argument)`: Normalize `globalThis.$argument` and guard `null`; 标准化 `globalThis.$argument` 并处理 `null` 场景(`c475e76`)。 - `fix(getStorage)`: Include `$argument` in merge flow with conditional handling; 修复合并流程以包含 `$argument` 并增加条件控制(`3a1c8bb`)。 - `fix(getStorage)`: Add merge source control by `$argument.Storage`; 支持通过 `$argument.Storage` 控制合并来源(`8a59892`)。 - `fix(getStorage)`: Replace `BoxJs` merge source naming/usage with `PersistentStore`; 将合并来源命名/实现统一为 `PersistentStore`(`5fa69e4`)。 - `fix(Storage)`: Add Surge `removeItem` support via `$persistentStore.write(null, keyName)`; 为 Surge 增加 `removeItem` 删除支持(`23ebecb`)。 -- `fix(getStorage)`: Accept lowercase `boxjs` as alias for `BoxJs`; 支持小写别名 `boxjs`。 ### Changed / 变更 - `refactor(getStorage)`: Rename `Store` to `Root` and align debug output; 重命名 `Store` 为 `Root` 并同步调试输出字段(`570a75c`)。 - `refactor(getStorage)`: Centralize `Settings` merge controlled by `$argument.Storage`; ensure `Configs`/`Caches` are merged per-profile (`names`)(`17747ae`)。 +- `refactor(getStorage)`: switch to a default export instead of named; 改用默认导出(`export default`)。 ### Docs / 文档 - Sync README/JSDoc with recent behavior changes for `argument` / `getStorage` / `Storage`; 同步 `argument` / `getStorage` / `Storage` 的 README 与 JSDoc 说明(`2b13601`)。 - `docs(getStorage)`: Document aliases for `$argument.Storage` (`Argument` / `$argument`, `PersistentStore` / `BoxJs` / `$persistentStore`) and correct merge-order in README/JSDoc; 为 `$argument.Storage` 增加别名说明并修正 README 中的合并顺序说明。 +- `docs(getStorage)`: Update import example and JSDoc to reflect default export; 更新导入示例及 JSDoc 以反映默认导出变更。 + +## [2.1.1] - 2026-02-20 + +### Changed / 变更 +- `refactor(getStorage)`: remove default export and clarify usage boundaries; 改为无默认导出并补充使用边界说明(`4105cf2`)。 +- `feat(index)`: re-export `getStorage` from entry point and update docs/tests; 从入口导出并更新文档/测试(`b0a1bd9`)。 + +### Docs / 文档 +- `docs`: improve installation/update guidance for newcomers; 优化安装与更新指引面向新手(`ce7c81a`)。 +- `docs`: enhance polyfill descriptions and links; 完善 polyfill 文档说明与引用链接(`b817c07`)。 +- `docs`: fill out README and JSDoc comments; 补充 README 与 JSDoc 注释说明(`5c5f1f3`)。 + + + + -[2.1.3]: https://github.com/NSNanoCat/util/compare/main...dev +[2.1.4]: https://github.com/NSNanoCat/util/compare/v2.1.3...v2.1.4 +[2.1.3]: https://github.com/NSNanoCat/util/compare/v2.1.2...v2.1.3 +[2.1.2]: https://github.com/NSNanoCat/util/compare/v2.1.1...v2.1.2 +[2.1.1]: https://github.com/NSNanoCat/util/compare/v2.1.0...v2.1.1 -[2.1.2]: https://github.com/NSNanoCat/util/compare/main...dev diff --git a/README.md b/README.md index fef5848..e909160 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,7 @@ await runScript("$done({})", { timeout: 20 }); 示例: ```js -import { getStorage } from "@nsnanocat/util/getStorage.mjs"; +import getStorage from "@nsnanocat/util/getStorage.mjs"; const store = getStorage("@my_box", ["YouTube", "Global"], database); ``` diff --git a/getStorage.mjs b/getStorage.mjs index 0257058..e6a7ed6 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -36,8 +36,11 @@ import { Storage } from "./polyfill/Storage.mjs"; * @param {string|string[]|Array} names 目标配置名 / Target profile names. * @param {Record} database 默认数据库 / Default database object. * @returns {StorageProfile} + * + * @module getStorage + * @default */ -export function getStorage(key, names, database) { +export default function getStorage(key, names, database) { if (database?.Default?.Settings?.LogLevel) Console.logLevel = database.Default.Settings.LogLevel; Console.debug("☑️ getStorage"); names = [names].flat(Number.POSITIVE_INFINITY); From d0c2997b6bd912e0de5fff0cf64dd9d76c975949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Fri, 20 Feb 2026 23:51:21 +0800 Subject: [PATCH 68/72] chore(types): add local declarations for @nsnanocat/util --- CHANGELOG.md | 9 +++ package.json | 4 +- tsconfig.json | 1 + types/nsnanocat-util.d.ts | 134 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 types/nsnanocat-util.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 202d698..51800e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 格式参考 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)。 +## [2.1.5] - 2026-02-21 + +### Changed / 变更 +- `chore(types)`: add local declarations for `@nsnanocat/util`; 新增本地类型声明并发布类型入口(`3071c12`)。 + +### Docs / 文档 +- `docs`: update changelog order and reflect export change; 更新 changelog 顺序并反映导出变更(`5a5994a`)。 + ## [2.1.4] - 2026-02-20 ### Fixed / 修复 @@ -58,6 +66,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +[2.1.5]: https://github.com/NSNanoCat/util/compare/v2.1.4...v2.1.5 [2.1.4]: https://github.com/NSNanoCat/util/compare/v2.1.3...v2.1.4 [2.1.3]: https://github.com/NSNanoCat/util/compare/v2.1.2...v2.1.3 [2.1.2]: https://github.com/NSNanoCat/util/compare/v2.1.1...v2.1.2 diff --git a/package.json b/package.json index 954ae03..4a0e5c3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "license": "Apache-2.0", "bugs": "https://github.com/NSNanoCat/util/issues", "main": "index.js", + "types": "types/nsnanocat-util.d.ts", "type": "module", "scripts": { "tsc:build": "npx tsc", @@ -30,7 +31,8 @@ "index.js", "lib", "polyfill", - "getStorage.mjs" + "getStorage.mjs", + "types" ], "devDependencies": { "typescript": "^5.6.3" diff --git a/tsconfig.json b/tsconfig.json index ca246d1..a1ade11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "include": [ "polyfill/URL.mts", "polyfill/URLSearchParams.mts", + "types/**/*.d.ts", ], "compilerOptions": { // Tells TypeScript to read JS files, as diff --git a/types/nsnanocat-util.d.ts b/types/nsnanocat-util.d.ts new file mode 100644 index 0000000..62b3944 --- /dev/null +++ b/types/nsnanocat-util.d.ts @@ -0,0 +1,134 @@ +declare module "@nsnanocat/util" { + export type AppName = "Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash"; + + export const $app: AppName | undefined; + export const $argument: Record; + export const argument: typeof $argument; + + export interface DonePayload { + status?: number | string; + url?: string; + headers?: Record; + body?: string | ArrayBuffer | ArrayBufferView; + bodyBytes?: ArrayBuffer; + policy?: string; + } + + export function done(object?: DonePayload): void; + + export interface NotificationContentObject { + open?: string; + "open-url"?: string; + url?: string; + openUrl?: string; + copy?: string; + "update-pasteboard"?: string; + updatePasteboard?: string; + media?: string; + "media-url"?: string; + mediaUrl?: string; + mime?: string; + "auto-dismiss"?: number; + sound?: string; + } + + export type NotificationContent = NotificationContentObject | string | number | boolean; + + export function notification( + title?: string, + subtitle?: string, + body?: string, + content?: NotificationContent, + ): void; + + export function time(format: string, ts?: number): string; + export function wait(delay?: number): Promise; + + export interface FetchRequest { + url: string; + method?: string; + headers?: Record; + body?: string | ArrayBuffer | ArrayBufferView | object; + bodyBytes?: ArrayBuffer; + timeout?: number | string; + policy?: string; + redirection?: boolean; + "auto-redirect"?: boolean; + opts?: Record; + [key: string]: unknown; + } + + export interface FetchResponse { + ok: boolean; + status: number; + statusCode?: number; + statusText?: string; + headers?: Record; + body?: string | ArrayBuffer; + bodyBytes?: ArrayBuffer; + [key: string]: unknown; + } + + export function fetch(resource: FetchRequest | string, options?: Partial): Promise; + + export class Console { + static clear(): void; + static count(label?: string): void; + static countReset(label?: string): void; + static debug(...msg: unknown[]): void; + static error(...msg: unknown[]): void; + static exception(...msg: unknown[]): void; + static group(label: string): number; + static groupEnd(): string | undefined; + static info(...msg: unknown[]): void; + static get logLevel(): "OFF" | "ERROR" | "WARN" | "INFO" | "DEBUG" | "ALL"; + static set logLevel(level: number | string); + static log(...msg: unknown[]): void; + static time(label?: string): Map; + static timeEnd(label?: string): boolean; + static timeLog(label?: string): void; + static warn(...msg: unknown[]): void; + } + + export class Lodash { + static escape(string: string): string; + static get(object?: Record, path?: string | string[], defaultValue?: D): T | D; + static merge>(object: T, ...sources: Array | null | undefined>): T; + static omit>(object?: T, paths?: string | string[]): T; + static pick, K extends keyof T>(object?: T, paths?: K | K[]): Pick; + static set>(object: T, path: string | string[], value: unknown): T; + static toPath(value: string): string[]; + static unescape(string: string): string; + static unset(object?: Record, path?: string | string[]): boolean; + } + + export const StatusTexts: Record; + + export class Storage { + static data: Record | null; + static dataFile: string; + static getItem(keyName: string, defaultValue?: T): T; + static setItem(keyName: string, keyValue: unknown): boolean; + static removeItem(keyName: string): boolean; + static clear(): boolean; + } +} + +declare module "@nsnanocat/util/getStorage.mjs" { + export interface StorageProfile { + Settings: Record; + Configs: Record; + Caches: Record; + } + + export default function getStorage( + key: string, + names: string | string[] | Array, + database: Record, + ): StorageProfile; +} + +declare module "@nsnanocat/util/lib/environment.mjs" { + export const $environment: Record; + export function environment(): Record; +} From 47de721b1fbcf89b34f1d05cee2ac583b16959c1 Mon Sep 17 00:00:00 2001 From: ONZ3V Date: Tue, 24 Feb 2026 14:37:16 +0800 Subject: [PATCH 69/72] =?UTF-8?q?fix(Storage):=20=E5=AE=8C=E5=96=84=20Node?= =?UTF-8?q?.js=20=E7=8E=AF=E5=A2=83=E4=B8=8B=20=0DemoveItem=20=E5=92=8C=20?= =?UTF-8?q?clear=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- polyfill/Storage.mjs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/polyfill/Storage.mjs b/polyfill/Storage.mjs index f716538..238b1a4 100644 --- a/polyfill/Storage.mjs +++ b/polyfill/Storage.mjs @@ -208,7 +208,11 @@ export class Storage { result = $prefs.removeValueForKey(keyName); break; case "Node.js": - result = false; + // result = false; + Storage.data = Storage.#loaddata(Storage.dataFile); + delete Storage.data[keyName]; + Storage.#writedata(Storage.dataFile); + result = true; break; default: result = false; @@ -239,7 +243,11 @@ export class Storage { result = $prefs.removeAllValues(); break; case "Node.js": - result = false; + // result = false; + Storage.data = Storage.#loaddata(Storage.dataFile); + Storage.data = {}; + Storage.#writedata(Storage.dataFile); + result = true; break; default: result = false; From ccd91f3b25e0b4b9188ce33e7e895166f74556d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Tue, 24 Feb 2026 15:06:57 +0800 Subject: [PATCH 70/72] feat(getStorage): export helper helpers with types and docs --- README.md | 20 +++++++++++++++++++- getStorage.mjs | 16 ++++++++++++++-- types/nsnanocat-util.d.ts | 9 +++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e909160..51559ba 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ import { ### 仓库中存在但未从主入口导出 - `lib/environment.mjs` - `lib/runScript.mjs` -- `getStorage.mjs`(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用) +- `getStorage.mjs`(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用;请通过子路径 `@nsnanocat/util/getStorage.mjs` 导入) ## 模块依赖关系 @@ -336,6 +336,24 @@ import getStorage from "@nsnanocat/util/getStorage.mjs"; const store = getStorage("@my_box", ["YouTube", "Global"], database); ``` +#### 命名导出(辅助函数) + +`getStorage.mjs` 同时导出以下辅助函数: +- `traverseObject(o, c)`:深度遍历对象并替换叶子值 +- `string2number(string)`:将纯数字字符串转换为数字 +- `string2array(string)`:按逗号拆分字符串为数组 + +示例: +```js +import getStorage, { + traverseObject, + string2number, + string2array, +} from "@nsnanocat/util/getStorage.mjs"; + +const store = getStorage("@my_box", ["YouTube", "Global"], database); +``` + ### `polyfill/fetch.mjs` `fetch` 是仿照 Web API `Window.fetch` 设计的跨平台适配实现: diff --git a/getStorage.mjs b/getStorage.mjs index e6a7ed6..b426ef3 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -128,7 +128,7 @@ export default function getStorage(key, names, database) { * @param {(key: string, value: any) => any} c 处理回调 / Transformer callback. * @returns {Record} */ -function traverseObject(o, c) { +export function traverseObject(o, c) { for (const t in o) { const n = o[t]; o[t] = "object" === typeof n && null !== n ? traverseObject(n, c) : c(t, n); @@ -143,7 +143,19 @@ function traverseObject(o, c) { * @param {string} string 输入字符串 / Input string. * @returns {string|number} */ -function string2number(string) { +export function string2number(string) { if (/^\d+$/.test(string)) string = Number.parseInt(string, 10); return string; } + +/** + * 将字符串包装为数组。 + * Split comma-separated string into array. + * + * @param {string|string[]|null|undefined} string 输入值 / Input value. + * @returns {string[]} + */ +export function string2array(string) { + if (Array.isArray(string)) return string; + return string?.split(",") ?? []; +} diff --git a/types/nsnanocat-util.d.ts b/types/nsnanocat-util.d.ts index 62b3944..98f41fc 100644 --- a/types/nsnanocat-util.d.ts +++ b/types/nsnanocat-util.d.ts @@ -121,6 +121,15 @@ declare module "@nsnanocat/util/getStorage.mjs" { Caches: Record; } + export function traverseObject( + o: Record, + c: (key: string, value: unknown) => unknown, + ): Record; + + export function string2number(string: string): string | number; + + export function string2array(string: string | string[] | null | undefined): string[]; + export default function getStorage( key: string, names: string | string[] | Array, From 462892097ddd27b15635c7885dde07dfd92cd910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Tue, 24 Feb 2026 15:30:37 +0800 Subject: [PATCH 71/72] refactor(getStorage): rename value2array and switch-based parsing --- README.md | 4 ++-- getStorage.mjs | 41 ++++++++++++++++++++++++++------------- types/nsnanocat-util.d.ts | 2 +- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 51559ba..1f3e2ed 100644 --- a/README.md +++ b/README.md @@ -341,14 +341,14 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); `getStorage.mjs` 同时导出以下辅助函数: - `traverseObject(o, c)`:深度遍历对象并替换叶子值 - `string2number(string)`:将纯数字字符串转换为数字 -- `string2array(string)`:按逗号拆分字符串为数组 +- `value2array(value)`:字符串按逗号拆分;数字/布尔值会被包装为单元素数组 示例: ```js import getStorage, { traverseObject, string2number, - string2array, + value2array, } from "@nsnanocat/util/getStorage.mjs"; const store = getStorage("@my_box", ["YouTube", "Global"], database); diff --git a/getStorage.mjs b/getStorage.mjs index b426ef3..2b1c921 100644 --- a/getStorage.mjs +++ b/getStorage.mjs @@ -106,12 +106,20 @@ export default function getStorage(key, names, database) { /***************** traverseObject *****************/ traverseObject(Root.Settings, (key, value) => { Console.debug("☑️ traverseObject", `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`); - if (value === "true" || value === "false") - value = JSON.parse(value); // 字符串转Boolean - else if (typeof value === "string") { - if (value.includes(",")) - value = value.split(",").map(item => string2number(item)); // 字符串转数组转数字 - else value = string2number(value); // 字符串转数字 + switch (typeof value) { + case "string": + switch (value) { + case "true": + case "false": + case "[]": + value = JSON.parse(value); // 字符串转Boolean/空数组 + break; + default: + if (value.includes(",")) + value = value2array(value).map(item => string2number(item)); // 字符串转数组转数字 + else value = string2number(value); // 字符串转数字 + } + break; } return value; }); @@ -149,13 +157,20 @@ export function string2number(string) { } /** - * 将字符串包装为数组。 - * Split comma-separated string into array. + * 将值包装为数组。 + * Split value into array. * - * @param {string|string[]|null|undefined} string 输入值 / Input value. - * @returns {string[]} + * @param {string|number|boolean|string[]|null|undefined} value 输入值 / Input value. + * @returns {(string|number|boolean)[]} */ -export function string2array(string) { - if (Array.isArray(string)) return string; - return string?.split(",") ?? []; +export function value2array(value) { + switch (typeof value) { + case "string": + return value.split(","); + case "number": + case "boolean": + return [value]; + default: + return value || []; + } } diff --git a/types/nsnanocat-util.d.ts b/types/nsnanocat-util.d.ts index 98f41fc..d878c77 100644 --- a/types/nsnanocat-util.d.ts +++ b/types/nsnanocat-util.d.ts @@ -128,7 +128,7 @@ declare module "@nsnanocat/util/getStorage.mjs" { export function string2number(string: string): string | number; - export function string2array(string: string | string[] | null | undefined): string[]; + export function value2array(value: string | number | boolean | string[] | null | undefined): Array; export default function getStorage( key: string, From 0a865d7feefd06729ba028f0636171f65a085f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=86=E8=96=AF=E9=A5=BC?= Date: Tue, 24 Feb 2026 15:34:51 +0800 Subject: [PATCH 72/72] docs(changelog): add 2.1.6 release notes Update README.md --- CHANGELOG.md | 12 ++++++++++++ README.md | 8 +++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51800e8..b3bab72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 格式参考 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)。 +## [2.1.6] - 2026-02-24 + +### Fixed / 修复 +- `fix(Storage)`: improve `removeItem` / `clear` behavior in Node.js environment; 完善 Node.js 环境下 `removeItem` / `clear` 方法(`47de721`)。 + +### Added / 新增 +- `feat(getStorage)`: export helper functions with type definitions and docs updates; 导出 `getStorage` 辅助函数并同步类型定义与文档(`ccd91f3`)。 + +### Changed / 变更 +- `refactor(getStorage)`: rename `string2array` to `value2array` and switch parsing logic to `switch`; 重命名 `string2array` 为 `value2array`,并将解析逻辑调整为 `switch` 语法(`4628920`)。 + ## [2.1.5] - 2026-02-21 ### Changed / 变更 @@ -66,6 +77,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +[2.1.6]: https://github.com/NSNanoCat/util/compare/v2.1.5...v2.1.6 [2.1.5]: https://github.com/NSNanoCat/util/compare/v2.1.4...v2.1.5 [2.1.4]: https://github.com/NSNanoCat/util/compare/v2.1.3...v2.1.4 [2.1.3]: https://github.com/NSNanoCat/util/compare/v2.1.2...v2.1.3 diff --git a/README.md b/README.md index 1f3e2ed..4cc3632 100644 --- a/README.md +++ b/README.md @@ -430,10 +430,12 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); #### `Storage.removeItem(keyName)` - Quantumult X:可用(`$prefs.removeValueForKey`)。 - Surge:通过 `$persistentStore.write(null, keyName)` 删除。 -- Loon / Stash / Egern / Shadowrocket / Node.js:返回 `false`。 +- Node.js:可用(删除 `box.dat` 中对应 key 并落盘)。 +- Loon / Stash / Egern / Shadowrocket:返回 `false`。 #### `Storage.clear()` - Quantumult X:可用(`$prefs.removeAllValues`)。 +- Node.js:可用(清空 `box.dat` 并落盘)。 - 其他平台:返回 `false`。 #### Node.js 特性 @@ -442,7 +444,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database); 与 Web Storage 的行为差异: - 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。 -- `removeItem/clear` 仅部分平台可用(目前为 Quantumult X,以及 Surge 的 `removeItem`)。 +- `removeItem/clear` 仅部分平台可用(目前为 Quantumult X、Node.js,以及 Surge 的 `removeItem`)。 - `getItem` 会尝试 `JSON.parse`,`setItem` 写入对象会 `JSON.stringify`。 平台后端映射: @@ -622,7 +624,7 @@ console.log(value); // 1 | 通知 | `$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` 不可用 | 不可用 | 不可用 | 不可用 | 不可用 | +| `removeItem/clear` | 可用 | 不可用 | `removeItem` 可用 / `clear` 不可用 | 不可用 | 不可用 | 不可用 | 可用 | | `policy` 注入(`fetch/done`) | `opts.policy` | `node` | `X-Surge-Policy`(done) | `X-Stash-Selected-Proxy` | 无专门映射 | `X-Surge-Proxy`(fetch) | 无 | ## 已知限制与注意事项