@nsnanocat/preference-panes 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -47
- package/dist/api.js +114 -124
- package/dist/module/index.html +1 -1
- package/dist/module/index.mjs +188 -218
- package/dist/module/navigation.mjs +15 -32
- package/dist/preference-panes.mjs +83 -72
- package/dist/web.js +2 -23
- package/package.json +2 -2
- package/src/api.mjs +117 -124
- package/src/browser/ModuleFrame.mjs +13 -13
- package/src/browser/ModuleStatus.mjs +2 -3
- package/src/browser/Navigation.d.mts +2 -4
- package/src/browser/client.d.mts +5 -5
- package/src/browser/client.mjs +59 -30
- package/src/browser/index.d.ts +5 -21
- package/src/browser/index.mjs +16 -48
- package/src/browser/mount.mjs +16 -32
- package/src/browser/panel.mjs +9 -9
- package/src/index.d.ts +2 -24
- package/src/index.mjs +3 -3
- package/src/web.mjs +1 -5
- package/src/build.mjs +0 -20
- package/src/lib/page-inputs.mjs +0 -17
package/README.md
CHANGED
|
@@ -1,76 +1,92 @@
|
|
|
1
1
|
# @nsnanocat/preference-panes
|
|
2
2
|
|
|
3
|
-
PreferencePanes
|
|
3
|
+
PreferencePanes 提供一个由 BoxJS JSON 驱动的通用设置前端,以及一个独立的代理配置与持久化 API。业务模块只发布自己的 `/configs/{module}`;通用 `/settings/**` 前端和 `/api/**` 后端分别只需要安装一次。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
在 Biliverse 中,Enhanced 是唯一安装 `web.js` 的模块。Global、Redirect、ADBlock 不携带 `web.js`,它们的设置页仍由同一份通用前端读取各自 BoxJS 后渲染。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 浏览器入口
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
浏览器包只公开一个入口:
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
```ts
|
|
12
|
+
function mount(boxjs: BoxJSInput): MountedPreferences;
|
|
13
|
+
interface MountedPreferences { destroy(): void }
|
|
14
|
+
```
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
```js
|
|
17
|
+
import { mount } from "@nsnanocat/preference-panes/browser";
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
const boxjs = await fetch("/api/Module", {
|
|
20
|
+
cache: "no-store",
|
|
21
|
+
credentials: "omit",
|
|
22
|
+
}).then(response => response.json());
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
method: "POST",
|
|
22
|
-
headers: {
|
|
23
|
-
"Content-Type": "application/json",
|
|
24
|
-
"X-PreferencePanes-JSON": "/configs/Enhanced",
|
|
25
|
-
},
|
|
26
|
-
body: JSON.stringify({ key: "Enhanced.Settings.Home.Top_left", value: "mine" }),
|
|
27
|
-
});
|
|
24
|
+
const preferences = mount(boxjs);
|
|
25
|
+
preferences.destroy();
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
`mount()` 同步建立页面生命周期。BoxJS 规范化、默认值、已存值校验、加载状态和失败重试都由内部流程管理;不公开 API Model、视图 class、构建器或 CSS 第二参数。
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
前端支持 BoxJS 字段数组、单个 app 和 apps 订阅,并要求输入恰好包含一个模块。模块名、存储根和 Settings 路径都从 `@Root.Module.Settings.key` 字段 ID 推导,不从 app 名称或调用参数补充。
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
默认样式随包内置。嵌入页面只跟随宿主根元素的 `data-theme` 和 `--pp-keyboard-height`,不识别客户端 User-Agent,也不加载项目 CSS 或客户端 SDK。
|
|
33
|
+
|
|
34
|
+
## 页面与 API
|
|
35
|
+
|
|
36
|
+
`web.js` 只返回三类通用资源:
|
|
37
|
+
|
|
38
|
+
- `GET /settings/{module}`
|
|
39
|
+
- `GET /settings/assets/index.mjs`
|
|
40
|
+
- `GET /settings/assets/navigation.mjs`
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,通过同源 `/api/{module}` 读取原始 BoxJS,然后调用 `mount(boxjs)`。页面不接受 JSON/CSS 查询参数、私有请求头或兼容资源别名,也不直接访问 `/configs/**`。
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
`api.js` 只处理:
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
- `HEAD /api/{module}`:探测同源 `/configs/{module}`,透传状态与 `X-PreferencePanes-Version`。
|
|
47
|
+
- `GET /api/{module}`:原样返回同源 `/configs/{module}` 的 BoxJS JSON 与版本头。
|
|
48
|
+
- `POST /api/get`:读取完整 `@root.path`。
|
|
49
|
+
- `POST /api/set`:写入完整 `@root.path`。
|
|
50
|
+
- `POST /api/delete`:删除完整 `@root.path` 或子树。
|
|
51
|
+
|
|
52
|
+
模块 API 只转发业务配置;固定存储 API 不下载或解析 BoxJS。BoxJS 字段、控件、选项、默认值与写入值都由浏览器校验。
|
|
48
53
|
|
|
49
54
|
```js
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
headers: { "
|
|
53
|
-
|
|
55
|
+
await fetch("/api/set", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
58
|
+
body: new URLSearchParams([["@BiliBili.Enhanced.Settings.Home.Top_left", JSON.stringify("mine")]]),
|
|
54
59
|
});
|
|
55
|
-
container.append(frame.element);
|
|
56
|
-
await frame.load();
|
|
57
|
-
frame.addEventListener("change", () => { title.textContent = frame.state.title; });
|
|
58
|
-
back.onclick = () => frame.back();
|
|
59
|
-
frame.destroy();
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
页面初始化时只执行一次 `POST /api/get` 读取 Settings 子树。写入成功后仅更新当前页面快照;查看 Settings/Caches 时按需读取,清空和重置通过 `/api/delete` 完成。
|
|
63
|
+
|
|
64
|
+
## 宿主集成
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import { ModuleFrame, ModuleStatus } from "@nsnanocat/preference-panes/navigation";
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
const status = new ModuleStatus(statusElement);
|
|
70
|
+
await status.check("/api/Module");
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
const frame = new ModuleFrame("/settings/Module", { signal });
|
|
73
|
+
container.append(frame.element);
|
|
74
|
+
await frame.load();
|
|
75
|
+
```
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
`ModuleFrame` 只携带模块身份和取消信号。宿主通过 `change`、`confirm`、`notice` 事件同步标题、操作菜单、确认框和提示,不读取或改写 iframe 内部 DOM。项目主页、Bilibili JSBridge、原生导航和视觉样式仍由宿主负责。
|
|
69
78
|
|
|
70
79
|
## 构建与验证
|
|
71
80
|
|
|
72
|
-
|
|
81
|
+
```sh
|
|
82
|
+
npm run build
|
|
83
|
+
npm run check
|
|
84
|
+
npm run apifox:check
|
|
85
|
+
npm pack --dry-run
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
构建生成 `dist/api.js`、`dist/web.js`、`dist/preference-panes.mjs` 和 `dist/module/` 通用页面资源。包不再提供按模块生成 HTML/CSS 的公开 `build()`。
|
|
73
89
|
|
|
74
|
-
`npm run preview`
|
|
90
|
+
`npm run preview` 启动 JSON 文件导入测试台,使用同一通用页面、同源配置和内存存储验证模块行为。
|
|
75
91
|
|
|
76
|
-
[接口规范](apifox/Specification.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
|
|
92
|
+
[接口规范](apifox/Specification.md) · [宿主集成](apifox/HostIntegration.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
|
package/dist/api.js
CHANGED
|
@@ -1921,11 +1921,9 @@
|
|
|
1921
1921
|
return parts;
|
|
1922
1922
|
}
|
|
1923
1923
|
|
|
1924
|
-
const MISSING = Symbol("missing");
|
|
1925
|
-
|
|
1926
1924
|
/**
|
|
1927
|
-
* PreferencePanes 后端 API
|
|
1928
|
-
* PreferencePanes backend API
|
|
1925
|
+
* PreferencePanes 后端 API,只转发模块配置并提供通用持久化操作。
|
|
1926
|
+
* PreferencePanes backend API only relaying module configurations and providing generic persistence operations.
|
|
1929
1927
|
*/
|
|
1930
1928
|
class API {
|
|
1931
1929
|
/**
|
|
@@ -1947,153 +1945,107 @@
|
|
|
1947
1945
|
}
|
|
1948
1946
|
|
|
1949
1947
|
/**
|
|
1950
|
-
*
|
|
1951
|
-
* Handle
|
|
1948
|
+
* 处理模块配置 API 与固定存储动作,不接管页面或静态资源。
|
|
1949
|
+
* Handle module configuration APIs and fixed storage actions without intercepting pages or static assets.
|
|
1952
1950
|
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
1953
1951
|
* @returns {Promise<import("./index.js").SettingsResponse | undefined>} API 响应或未接管 / API response or pass-through.
|
|
1954
1952
|
*/
|
|
1955
1953
|
async handle(request) {
|
|
1956
1954
|
const url = new URL(request.url);
|
|
1957
|
-
const
|
|
1958
|
-
if (
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
return this.#probe(request, configURL);
|
|
1964
|
-
case !action && request.method === "GET":
|
|
1965
|
-
return this.#model(request, module, configURL);
|
|
1966
|
-
case Boolean(action) && request.method === "POST":
|
|
1967
|
-
return this.#action(request, module, action, configURL);
|
|
1968
|
-
default:
|
|
1969
|
-
return this.#response(request, 405, { error: "Use GET or HEAD for module reads, POST for module actions" });
|
|
1970
|
-
}
|
|
1955
|
+
const action = /^\/api\/(get|set|delete)\/?$/.exec(url.pathname)?.[1];
|
|
1956
|
+
if (action) return this.#store(request, action);
|
|
1957
|
+
const module = /^\/api\/([a-zA-Z0-9_-]+)\/?$/.exec(url.pathname)?.[1];
|
|
1958
|
+
if (!module) return;
|
|
1959
|
+
if (!["HEAD", "GET"].includes(request.method)) return this.#response(request, 405, { error: "Use GET or HEAD for module configuration" });
|
|
1960
|
+
return this.#configuration(request, `${url.origin}/configs/${module}`);
|
|
1971
1961
|
}
|
|
1972
1962
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
async #probe(request, configURL) {
|
|
1963
|
+
/**
|
|
1964
|
+
* 从同源业务配置响应模块探测或原始 BoxJS JSON。
|
|
1965
|
+
* Respond to a module probe or raw BoxJS JSON from the same-origin business configuration.
|
|
1966
|
+
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
1967
|
+
* @param {string} configuration 同源配置地址 / Same-origin configuration URL.
|
|
1968
|
+
* @returns {Promise<import("./index.js").SettingsResponse>} 配置响应 / Configuration response.
|
|
1969
|
+
*/
|
|
1970
|
+
async #configuration(request, configuration) {
|
|
1982
1971
|
let result;
|
|
1983
1972
|
try {
|
|
1984
|
-
result = await fetch({ url:
|
|
1973
|
+
result = await fetch({ url: configuration, method: request.method, timeout: 5000, headers: { Accept: "application/json" } });
|
|
1985
1974
|
} catch (error) {
|
|
1986
1975
|
return this.#response(request, 502, { error: error.message });
|
|
1987
1976
|
}
|
|
1988
1977
|
const version = this.#header(result.headers, "x-preferencepanes-version");
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
for (const entry of loaded.entries) {
|
|
1996
|
-
const value = Storage.getItem(entry.id, MISSING);
|
|
1997
|
-
if (value !== MISSING) values[entry.id.slice(loaded.storageKey.length + 2)] = value;
|
|
1998
|
-
}
|
|
1999
|
-
return this.#response(request, 200, { module, boxjs: loaded.boxjs, values, configURL }, loaded.version ? { "X-PreferencePanes-Version": loaded.version } : {});
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
async #action(request, module, action, configURL) {
|
|
2003
|
-
const payload = this.#jsonBody(request);
|
|
2004
|
-
const target = await this.#load(module, configURL);
|
|
2005
|
-
switch (action) {
|
|
2006
|
-
case "get": {
|
|
2007
|
-
const value = Storage.getItem(payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key), MISSING);
|
|
2008
|
-
return value === MISSING ? this.#response(request, 404, { error: "Stored path does not exist" }) : this.#response(request, 200, value);
|
|
2009
|
-
}
|
|
2010
|
-
case "set":
|
|
2011
|
-
if (!Object.hasOwn(payload ?? {}, "value")) throw Object.assign(new TypeError("A value is required"), { status: 400 });
|
|
2012
|
-
if (!Storage.setItem(this.#storagePath(target, payload.key), payload.value)) throw new Error("Storage write failed");
|
|
2013
|
-
return this.#response(request, 200, { saved: true });
|
|
2014
|
-
case "delete": {
|
|
2015
|
-
const path = payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key);
|
|
2016
|
-
if (!Storage.removeItem(path)) throw new Error("Storage write failed");
|
|
2017
|
-
return this.#response(request, 200, { deleted: true });
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
1978
|
+
const contentType = this.#header(result.headers, "content-type") ?? "application/json; charset=utf-8";
|
|
1979
|
+
return {
|
|
1980
|
+
status: result.statusCode ?? result.status,
|
|
1981
|
+
headers: { "Content-Type": contentType, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...(version ? { "X-PreferencePanes-Version": version } : {}) },
|
|
1982
|
+
body: request.method === "HEAD" ? "" : typeof result.body === "string" ? result.body : new TextDecoder().decode(result.body),
|
|
1983
|
+
};
|
|
2020
1984
|
}
|
|
2021
1985
|
|
|
2022
|
-
|
|
2023
|
-
|
|
1986
|
+
/**
|
|
1987
|
+
* 使用唯一 form 字段中的完整 @root.path 执行存储操作。
|
|
1988
|
+
* Execute a storage operation using the complete @root.path from the sole form field.
|
|
1989
|
+
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
1990
|
+
* @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
|
|
1991
|
+
* @returns {import("./index.js").SettingsResponse} 操作响应 / Operation response.
|
|
1992
|
+
*/
|
|
1993
|
+
#store(request, action) {
|
|
1994
|
+
const reply = (status, data) => this.#response(request, status, data);
|
|
1995
|
+
if (request.method !== "POST") return reply(405, { error: "Use POST with a form body" });
|
|
1996
|
+
const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
1997
|
+
if (headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/x-www-form-urlencoded") return reply(415, { error: "Expected application/x-www-form-urlencoded" });
|
|
1998
|
+
if (typeof request.body !== "string" || request.body.length > 65536) return reply(400, { error: "Expected a form body up to 65536 characters" });
|
|
1999
|
+
let parts, value;
|
|
2024
2000
|
try {
|
|
2025
|
-
|
|
2001
|
+
const fields = request.body.split("&");
|
|
2002
|
+
if (fields.length !== 1) throw new TypeError("Send exactly one storage key");
|
|
2003
|
+
const separator = fields[0].indexOf("=");
|
|
2004
|
+
if (separator < 0) throw new TypeError("Expected @root.path=value");
|
|
2005
|
+
const key = decodeURIComponent(fields[0].slice(0, separator).replace(/\+/g, " "));
|
|
2006
|
+
value = decodeURIComponent(fields[0].slice(separator + 1).replace(/\+/g, " "));
|
|
2007
|
+
if (!key.startsWith("@")) throw new TypeError("Storage keys must start with @");
|
|
2008
|
+
parts = validatePathParts(key.slice(1).split("."));
|
|
2009
|
+
if (parts.length < 2) throw new TypeError("Specify a storage root and child path");
|
|
2026
2010
|
} catch (error) {
|
|
2027
|
-
|
|
2011
|
+
return reply(400, { error: error.message });
|
|
2028
2012
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
const apps = Array.isArray(boxjs) ? [{ settings: boxjs }] : (boxjs.apps ?? [boxjs]);
|
|
2035
|
-
if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
|
|
2036
|
-
const entries = [];
|
|
2037
|
-
let storageKey;
|
|
2038
|
-
for (const app of apps) {
|
|
2039
|
-
if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
|
|
2040
|
-
for (const entry of app.settings) {
|
|
2041
|
-
if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
|
|
2042
|
-
if (!entry.id.startsWith("@")) {
|
|
2043
|
-
if (Array.isArray(boxjs)) throw new TypeError("BoxJS settings require @root.path IDs");
|
|
2044
|
-
continue;
|
|
2045
|
-
}
|
|
2046
|
-
const [root, ...parts] = entry.id.slice(1).split(".");
|
|
2047
|
-
if (!root || root.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
|
|
2048
|
-
validatePathParts(parts);
|
|
2049
|
-
if (parts[0] !== module) continue;
|
|
2050
|
-
if (storageKey && storageKey !== root) throw new TypeError(`A module must use one storage root: ${module}`);
|
|
2051
|
-
storageKey = root;
|
|
2052
|
-
entries.push(entry);
|
|
2053
|
-
}
|
|
2013
|
+
if (action === "set") {
|
|
2014
|
+
try {
|
|
2015
|
+
value = JSON.parse(value);
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
2054
2018
|
}
|
|
2055
|
-
if (!entries.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
2056
|
-
return { boxjs, entries, module, storageKey, version: this.#header(result.headers, "x-preferencepanes-version") };
|
|
2057
|
-
} catch (error) {
|
|
2058
|
-
throw Object.assign(new Error(`Invalid BoxJS: ${error.message}`), { status: 422 });
|
|
2059
2019
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
#jsonBody(request) {
|
|
2063
|
-
const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
2064
|
-
if (headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/json") throw Object.assign(new TypeError("Expected application/json"), { status: 415 });
|
|
2065
|
-
if (typeof request.body !== "string" || request.body.length > 65536) throw Object.assign(new TypeError("Expected a JSON body up to 65536 characters"), { status: 400 });
|
|
2020
|
+
const [storageKey, ...path] = parts;
|
|
2066
2021
|
try {
|
|
2067
|
-
|
|
2022
|
+
const root = Storage.getItem(storageKey, {});
|
|
2023
|
+
if (!isRecord(root)) throw new TypeError("Stored root must be an object");
|
|
2024
|
+
const parent = storageParent(root, path, action === "set");
|
|
2025
|
+
const key = path.at(-1);
|
|
2026
|
+
switch (action) {
|
|
2027
|
+
case "get": {
|
|
2028
|
+
const result = parent ? Lodash.get(parent, [key]) : undefined;
|
|
2029
|
+
return result === undefined ? reply(404, { error: "Stored path does not exist" }) : reply(200, result);
|
|
2030
|
+
}
|
|
2031
|
+
case "set":
|
|
2032
|
+
Lodash.set(parent, [key], value);
|
|
2033
|
+
break;
|
|
2034
|
+
case "delete":
|
|
2035
|
+
if (parent) Lodash.unset(parent, [key]);
|
|
2036
|
+
break;
|
|
2037
|
+
}
|
|
2038
|
+
if (!Storage.setItem(storageKey, root)) throw new Error("Storage write failed");
|
|
2039
|
+
return reply(200, action === "set" ? { saved: true } : { deleted: true });
|
|
2068
2040
|
} catch (error) {
|
|
2069
|
-
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
#storagePath(target, key) {
|
|
2074
|
-
if (typeof key !== "string") throw Object.assign(new TypeError("A BoxJS field path is required"), { status: 400 });
|
|
2075
|
-
const path = `@${target.storageKey}.${key}`;
|
|
2076
|
-
if (!target.entries.some(entry => entry.id === path)) throw Object.assign(new TypeError(`Unknown BoxJS field: ${key}`), { status: 400 });
|
|
2077
|
-
return path;
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
#scopePath(target, scope) {
|
|
2081
|
-
switch (scope) {
|
|
2082
|
-
case "settings":
|
|
2083
|
-
return `@${target.storageKey}.${target.module}.Settings`;
|
|
2084
|
-
case "caches":
|
|
2085
|
-
return `@${target.storageKey}.${target.module}.Caches`;
|
|
2086
|
-
case "module":
|
|
2087
|
-
return `@${target.storageKey}.${target.module}`;
|
|
2088
|
-
default:
|
|
2089
|
-
throw Object.assign(new TypeError("Scope must be settings, caches or module"), { status: 400 });
|
|
2041
|
+
return reply(500, { error: error.message });
|
|
2090
2042
|
}
|
|
2091
2043
|
}
|
|
2092
2044
|
|
|
2093
|
-
#response(request, status, body
|
|
2045
|
+
#response(request, status, body) {
|
|
2094
2046
|
return {
|
|
2095
2047
|
status,
|
|
2096
|
-
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"
|
|
2048
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" },
|
|
2097
2049
|
body: request.method === "HEAD" ? "" : JSON.stringify(body),
|
|
2098
2050
|
};
|
|
2099
2051
|
}
|
|
@@ -2104,6 +2056,44 @@
|
|
|
2104
2056
|
}
|
|
2105
2057
|
}
|
|
2106
2058
|
|
|
2059
|
+
/**
|
|
2060
|
+
* 判断存储根是否为普通对象。
|
|
2061
|
+
* Determine whether a storage root is a plain object.
|
|
2062
|
+
* @param {unknown} value 待检查值 / Value to inspect.
|
|
2063
|
+
* @returns {boolean} 是否为普通对象 / Whether this is a plain object.
|
|
2064
|
+
*/
|
|
2065
|
+
function isRecord(value) {
|
|
2066
|
+
return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
/**
|
|
2070
|
+
* 遍历父路径,并解码旧存储中的 JSON 字符串中间节点。
|
|
2071
|
+
* Traverse parent paths and decode legacy intermediate nodes stored as JSON strings.
|
|
2072
|
+
* @param {Record<string, unknown>} root 存储根 / Storage root.
|
|
2073
|
+
* @param {string[]} parts 完整路径 / Complete path.
|
|
2074
|
+
* @param {boolean} create 是否创建缺失节点 / Whether to create missing parents.
|
|
2075
|
+
* @returns {object | undefined} 父节点或 undefined / Parent node or undefined.
|
|
2076
|
+
*/
|
|
2077
|
+
function storageParent(root, parts, create) {
|
|
2078
|
+
let parent = root;
|
|
2079
|
+
for (const part of parts.slice(0, -1)) {
|
|
2080
|
+
let next = Lodash.get(parent, [part]);
|
|
2081
|
+
switch (typeof next) {
|
|
2082
|
+
case "undefined":
|
|
2083
|
+
if (!create) return;
|
|
2084
|
+
next = {};
|
|
2085
|
+
break;
|
|
2086
|
+
case "string":
|
|
2087
|
+
next = JSON.parse(next);
|
|
2088
|
+
break;
|
|
2089
|
+
}
|
|
2090
|
+
if (!isRecord(next) && !Array.isArray(next)) throw new TypeError("Stored parent is not an object or array");
|
|
2091
|
+
Lodash.set(parent, [part], next);
|
|
2092
|
+
parent = next;
|
|
2093
|
+
}
|
|
2094
|
+
return parent;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2107
2097
|
new API().run();
|
|
2108
2098
|
|
|
2109
2099
|
})();
|
package/dist/module/index.html
CHANGED