@nsnanocat/preference-panes 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @nsnanocat/preference-panes
2
2
 
3
- PreferencePanes 提供一个由 BoxJS JSON 驱动的通用设置前端,以及一个独立的代理配置与持久化 API。业务模块只发布自己的 `/configs/{module}`;通用 `/settings/**` 前端和 `/api/**` 后端分别只需要安装一次。
3
+ PreferencePanes 提供一个由 BoxJS JSON 驱动的通用设置前端,以及一个独立的代理持久化 API。业务模块直接将自己的 BoxJS JSON Mock 到 `/api/{module}`;通用 `/settings/**` 前端和固定存储 API 分别只需要安装一次。
4
4
 
5
5
  在 Biliverse 中,Enhanced 是唯一安装 `web.js` 的模块。Global、Redirect、ADBlock 不携带 `web.js`,它们的设置页仍由同一份通用前端读取各自 BoxJS 后渲染。
6
6
 
@@ -41,15 +41,18 @@ preferences.destroy();
41
41
 
42
42
  模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,通过同源 `/api/{module}` 读取原始 BoxJS,然后调用 `mount(boxjs)`。页面不接受 JSON/CSS 查询参数、私有请求头或兼容资源别名,也不直接访问 `/configs/**`。
43
43
 
44
+ 业务模块模板直接处理:
45
+
46
+ - `HEAD /api/{module}`:返回空正文和 `X-PreferencePanes-Version`。
47
+ - `GET /api/{module}`:返回同版原始 BoxJS JSON。
48
+
44
49
  `api.js` 只处理:
45
50
 
46
- - `HEAD /api/{module}`:探测同源 `/configs/{module}`,透传状态与 `X-PreferencePanes-Version`。
47
- - `GET /api/{module}`:原样返回同源 `/configs/{module}` 的 BoxJS JSON 与版本头。
48
51
  - `POST /api/get`:读取完整 `@root.path`。
49
52
  - `POST /api/set`:写入完整 `@root.path`。
50
53
  - `POST /api/delete`:删除完整 `@root.path` 或子树。
51
54
 
52
- 模块 API 只转发业务配置;固定存储 API 不下载或解析 BoxJSBoxJS 字段、控件、选项、默认值与写入值都由浏览器校验。
55
+ 模块名 `get`、`set` 和 `delete` 为固定存储端点保留。模块 API 与固定存储 API 都不解析 BoxJSBoxJS 字段、控件、选项、默认值与写入值全部由浏览器校验。
53
56
 
54
57
  ```js
55
58
  await fetch("/api/set", {
package/dist/api.js CHANGED
@@ -960,201 +960,6 @@
960
960
  }
961
961
  }
962
962
 
963
- /* https://github.com/ljharb/qs */
964
- /**
965
- * 轻量 `qs` 查询字符串工具。
966
- * Lightweight `qs` query-string utilities.
967
- *
968
- * 说明:
969
- * Notes:
970
- * - 参考 `qs` 的 `parse` / `stringify` 接口设计
971
- * - Modeled after the `qs` `parse` / `stringify` API
972
- * - `parse` 保持当前项目原有 `$argument` 字符串解析语义
973
- * - `parse` preserves the existing `$argument` string parsing semantics
974
- * - `stringify` 基于项目内 `Lodash` 路径能力展开对象
975
- * - `stringify` expands objects via the in-project `Lodash` path helpers
976
- *
977
- * 参考:
978
- * Reference:
979
- * - https://github.com/ljharb/qs
980
- * - https://www.npmjs.com/package/qs
981
- */
982
- class qs {
983
- /**
984
- * 将查询字符串解析为对象。
985
- * Parse a query string into an object.
986
- *
987
- * @param {string | Record<string, unknown> | null | undefined} [query=""] 查询字符串或对象 / Query string or object.
988
- * @returns {Record<string, unknown>}
989
- */
990
- static parse(query) {
991
- let result = {};
992
- switch (typeof query) {
993
- case "string": {
994
- const source = query.replace(/^\?/, "");
995
- if (!source) break;
996
- const obj = Object.fromEntries(
997
- source
998
- .split("&")
999
- .filter(Boolean)
1000
- .map(item => {
1001
- const [rawKey = "", rawValue = ""] = item.split("=", 2);
1002
- const key = qs.#decode(rawKey).replace(/\[([^\[\]]+)\]/g, ".$1");
1003
- return [key, qs.#decode(rawValue).replace(/\"/g, "")];
1004
- }),
1005
- );
1006
- Object.keys(obj).forEach(key => Lodash.set(result, key, obj[key]));
1007
- break;
1008
- }
1009
- case "object": {
1010
- switch (query) {
1011
- case null:
1012
- break;
1013
- default: {
1014
- const obj = {};
1015
- Object.keys(query).forEach(key => Lodash.set(obj, key, query[key]));
1016
- result = obj;
1017
- break;
1018
- }
1019
- }
1020
- break;
1021
- }
1022
- case "undefined":
1023
- result = {};
1024
- break;
1025
- }
1026
- return result;
1027
- }
1028
-
1029
- /**
1030
- * 将对象序列化为查询字符串。
1031
- * Serialize an object into a query string.
1032
- *
1033
- * @param {Record<string, unknown>} [object={}] 输入对象 / Input object.
1034
- * @returns {string}
1035
- */
1036
- static stringify(object = {}) {
1037
- if (!object || typeof object !== "object") return "";
1038
-
1039
- const entries = [];
1040
- Object.keys(object).forEach(key => qs.#collect(object, key, entries));
1041
-
1042
- if (entries.length === 0) return "";
1043
- return entries
1044
- .map(([key, value]) => `${qs.#encode(qs.#formatPath(key))}=${qs.#encode(value)}`)
1045
- .join("&");
1046
- }
1047
-
1048
- /**
1049
- * 收集待序列化的键值对。
1050
- * Collect key-value pairs for stringification.
1051
- *
1052
- * @param {Record<string, unknown>} object 输入对象 / Input object.
1053
- * @param {string} path 当前路径 / Current path.
1054
- * @param {[string, string][]} entries 输出数组 / Output entries.
1055
- * @returns {void}
1056
- */
1057
- static #collect(object, path, entries) {
1058
- const value = Lodash.get(object, path);
1059
- if (value === undefined) return;
1060
- if (value === null) {
1061
- entries.push([path, ""]);
1062
- return;
1063
- }
1064
- if (Array.isArray(value)) {
1065
- value.forEach((item, index) => {
1066
- if (item === undefined) return;
1067
- qs.#collect(object, `${path}[${index}]`, entries);
1068
- });
1069
- return;
1070
- }
1071
- if (qs.#isPlainObject(value)) {
1072
- Object.keys(value).forEach(key => qs.#collect(object, `${path}.${key}`, entries));
1073
- return;
1074
- }
1075
- entries.push([path, String(value)]);
1076
- }
1077
-
1078
- /**
1079
- * 使用 `Lodash.toPath` 规范化输出路径。
1080
- * Normalize output path via `Lodash.toPath`.
1081
- *
1082
- * @param {string} path 原始路径 / Raw path.
1083
- * @returns {string}
1084
- */
1085
- static #formatPath(path) {
1086
- const [head, ...tail] = Lodash.toPath(path);
1087
- return tail.reduce((result, segment) => (/^\d+$/.test(segment) ? `${result}[${segment}]` : `${result}.${segment}`), head);
1088
- }
1089
-
1090
- /**
1091
- * 判断值是否为普通对象。
1092
- * Check whether a value is a plain object.
1093
- *
1094
- * @param {unknown} value 输入值 / Input value.
1095
- * @returns {boolean}
1096
- */
1097
- static #isPlainObject(value) {
1098
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1099
- const proto = Object.getPrototypeOf(value);
1100
- return proto === null || proto === Object.prototype;
1101
- }
1102
-
1103
- /**
1104
- * 编码查询字符串片段。
1105
- * Encode a query-string fragment.
1106
- *
1107
- * @param {string} value 原始值 / Raw value.
1108
- * @returns {string}
1109
- */
1110
- static #encode(value) {
1111
- return encodeURIComponent(value);
1112
- }
1113
-
1114
- /**
1115
- * 解码查询字符串片段。
1116
- * Decode a query-string fragment.
1117
- *
1118
- * @param {string} value 编码值 / Encoded value.
1119
- * @returns {string}
1120
- */
1121
- static #decode(value) {
1122
- return decodeURIComponent(value.replace(/\+/g, " "));
1123
- }
1124
- }
1125
-
1126
- /**
1127
- * 统一 `$argument` 输入格式并展开深路径。
1128
- * Normalize `$argument` input format and expand deep paths.
1129
- *
1130
- * 平台差异:
1131
- * Platform differences:
1132
- * - Surge / Stash / Egern 常见为字符串参数: `a=1&b=2`
1133
- * - Surge / Stash / Egern usually pass string args: `a=1&b=2`
1134
- * - Loon 支持字符串和对象两种形态
1135
- * - Loon supports both string and object forms
1136
- * - Quantumult X / Shadowrocket 一般不提供 `$argument`
1137
- * - Quantumult X / Shadowrocket usually do not expose `$argument`
1138
- *
1139
- * 执行时机:
1140
- * Execution timing:
1141
- * - 该模块为即时执行模块,`import` 时立即处理全局 `$argument`
1142
- * - This module executes immediately and mutates global `$argument` on import
1143
- *
1144
- * 归一化规则补充:
1145
- * Normalization details:
1146
- * - 使用 `globalThis.$argument` 读写,避免运行环境下未声明变量引用问题
1147
- * - Read/write via `globalThis.$argument` to avoid undeclared variable access
1148
- * - 当 `$argument` 为 `null` 或 `undefined` 时,会重置为 `{}`
1149
- * - When `$argument` is `null` or `undefined`, it is normalized to `{}`
1150
- */
1151
- (() => {
1152
- Console.debug("☑️ $argument");
1153
- globalThis.$argument = qs.parse(globalThis.$argument);
1154
- if (globalThis.$argument.LogLevel) Console.logLevel = globalThis.$argument.LogLevel;
1155
- Console.debug("✅ $argument", `$argument: ${JSON.stringify(globalThis.$argument)}`);
1156
- })();
1157
-
1158
963
  /**
1159
964
  * HTTP 状态码文本映射表。
1160
965
  * HTTP status code to status text map.
@@ -1327,304 +1132,6 @@
1327
1132
  }
1328
1133
  }
1329
1134
 
1330
- /**
1331
- * 统一请求参数。
1332
- * Unified request payload.
1333
- *
1334
- * @typedef {object} FetchRequest
1335
- * @property {string} url 请求地址 / Request URL.
1336
- * @property {string} [method] 请求方法 / HTTP method.
1337
- * @property {Record<string, any>} [headers] 请求头 / Request headers.
1338
- * @property {string|ArrayBuffer|ArrayBufferView|object} [body] 请求体 / Request body.
1339
- * @property {ArrayBuffer} [bodyBytes] 二进制请求体 / Binary request body.
1340
- * @property {number|string} [timeout] 超时(秒或毫秒)/ Timeout (seconds or milliseconds).
1341
- * @property {string} [policy] 指定策略 / Preferred policy.
1342
- * @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
1343
- * @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag.
1344
- * @property {boolean|number|string} ["auto-cookie"] Worker / Node.js Cookie 开关 / Worker / Node.js Cookie toggle.
1345
- * @property {Record<string, any>} [opts] 平台扩展字段 / Platform extension fields.
1346
- */
1347
-
1348
- /**
1349
- * 统一响应结构。
1350
- * Unified response payload.
1351
- *
1352
- * @typedef {object} FetchResponse
1353
- * @property {boolean} ok 请求是否成功 / Whether request is successful.
1354
- * @property {number} status 状态码 / HTTP status code.
1355
- * @property {number} [statusCode] 状态码别名 / Status code alias.
1356
- * @property {string} [statusText] 状态文本 / HTTP status text.
1357
- * @property {Record<string, any>} [headers] 响应头 / Response headers.
1358
- * @property {string|ArrayBuffer} [body] 响应体 / Response body.
1359
- * @property {ArrayBuffer} [bodyBytes] 二进制响应体 / Binary response body.
1360
- */
1361
-
1362
- /**
1363
- * 跨平台 `fetch` 适配层。
1364
- * Cross-platform `fetch` adapter.
1365
- *
1366
- * 设计目标:
1367
- * Design goal:
1368
- * - 仿照 Web API `fetch`(`Window.fetch`)接口设计
1369
- * - Modeled after Web API `fetch` (`Window.fetch`)
1370
- * - 统一 VPN App、Worker 与 Node.js 环境中的请求调用
1371
- * - Unify request calls across VPN apps, Worker, and Node.js
1372
- *
1373
- * 功能:
1374
- * Features:
1375
- * - 统一 Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js 请求接口
1376
- * - Normalize request APIs across Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js
1377
- * - 统一返回体字段(`ok/status/statusText/body/bodyBytes`)
1378
- * - Normalize response fields (`ok/status/statusText/body/bodyBytes`)
1379
- *
1380
- * 与 Web `fetch` 的已知差异:
1381
- * Known differences from Web `fetch`:
1382
- * - 支持 `policy`、`auto-redirect` 等平台扩展字段
1383
- * - Supports platform extension fields like `policy` and `auto-redirect`
1384
- * - Worker / Node.js 共享基于 `fetch` 的请求分支
1385
- * - Worker / Node.js share the `fetch`-based request branch
1386
- * - Node.js ESM 的 `auto-cookie` 由 `fetch.node.mjs` 处理,本文件只使用宿主 `fetch`
1387
- * - Node.js ESM `auto-cookie` is handled by `fetch.node.mjs`; this module only uses the host `fetch`
1388
- * - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现
1389
- * - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine
1390
- * - 返回结构包含 `statusCode/bodyBytes` 等兼容字段
1391
- * - Response includes compatibility fields like `statusCode/bodyBytes`
1392
- *
1393
- * @link https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
1394
- * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
1395
- * @async
1396
- * @param {FetchRequest|string} resource 请求对象或 URL / Request object or URL string.
1397
- * @param {Partial<FetchRequest>} [options={}] 追加参数 / Extra options.
1398
- * @returns {Promise<FetchResponse>}
1399
- */
1400
- async function fetch(resource, options = {}) {
1401
- // 初始化参数。
1402
- // Initialize request input.
1403
- switch (typeof resource) {
1404
- case "object":
1405
- resource = { ...options, ...resource };
1406
- break;
1407
- case "string":
1408
- resource = { ...options, url: resource };
1409
- break;
1410
- case "undefined":
1411
- default:
1412
- throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
1413
- }
1414
- // 自动判断请求方法。
1415
- // Infer the HTTP method automatically.
1416
- if (!resource.method) {
1417
- resource.method = "GET";
1418
- if (resource.body ?? resource.bodyBytes) resource.method = "POST";
1419
- }
1420
- // 移除需要由底层实现自动生成的请求头。
1421
- // Remove headers that should be generated by the underlying runtime.
1422
- delete resource.headers?.Host;
1423
- delete resource.headers?.[":authority"];
1424
- delete resource.headers?.["Content-Length"];
1425
- delete resource.headers?.["content-length"];
1426
- // 统一请求方法为小写,方便后续索引平台 API。
1427
- // Normalize the method to lowercase for platform API lookups.
1428
- const method = resource.method.toLocaleLowerCase();
1429
- // 默认请求超时时间为 5 秒。
1430
- // Default request timeout to 5 seconds.
1431
- if (!resource.timeout) resource.timeout = 5;
1432
- if (resource.timeout) {
1433
- resource.timeout = Number.parseInt(resource.timeout, 10);
1434
- // 统一先转换为秒,大于 500 视为毫秒输入。
1435
- // Convert to seconds first and treat values above 500 as milliseconds.
1436
- if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
1437
- }
1438
- if (resource.timeout) {
1439
- switch ($app) {
1440
- case "Loon":
1441
- case "Quantumult X":
1442
- case "Worker":
1443
- case "Node.js":
1444
- // 这些平台要求毫秒,因此把秒重新换算为毫秒。
1445
- // These platforms expect milliseconds, so convert seconds back to milliseconds.
1446
- resource.timeout = resource.timeout * 1000;
1447
- break;
1448
- }
1449
- }
1450
- // 根据当前平台选择请求实现。
1451
- // Select the request engine for the current platform.
1452
- switch ($app) {
1453
- case "Loon":
1454
- case "Surge":
1455
- case "Stash":
1456
- case "Egern":
1457
- case "Shadowrocket":
1458
- // 转换通用请求参数到 `$httpClient` 语义。
1459
- // Map shared request fields to `$httpClient` semantics.
1460
- if (resource.policy) {
1461
- switch ($app) {
1462
- case "Loon":
1463
- resource.node = resource.policy;
1464
- break;
1465
- case "Stash":
1466
- Lodash.set(resource, "headers.X-Stash-Selected-Proxy", encodeURI(resource.policy));
1467
- break;
1468
- case "Shadowrocket":
1469
- Lodash.set(resource, "headers.X-Surge-Proxy", resource.policy);
1470
- break;
1471
- }
1472
- }
1473
- if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection;
1474
- // 优先把 `bodyBytes` 映射回 `$httpClient` 能接受的 `body`。
1475
- // Prefer mapping `bodyBytes` back to the `body` field expected by `$httpClient`.
1476
- if (resource.bodyBytes && !resource.body) {
1477
- resource.body = resource.bodyBytes;
1478
- resource.bodyBytes = undefined;
1479
- }
1480
- // 根据 `Accept` 推断是否需要二进制响应体。
1481
- // Infer whether the response should be treated as binary from `Accept`.
1482
- switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) {
1483
- case "application/protobuf":
1484
- case "application/x-protobuf":
1485
- case "application/vnd.google.protobuf":
1486
- case "application/vnd.apple.flatbuffer":
1487
- case "application/grpc":
1488
- case "application/grpc-web":
1489
- case "application/grpc+proto":
1490
- case "application/octet-stream":
1491
- resource["binary-mode"] = true;
1492
- break;
1493
- }
1494
- // 发送 `$httpClient` 请求并归一化返回结构。
1495
- // Send the `$httpClient` request and normalize the response payload.
1496
- return new Promise((resolve, reject) => {
1497
- globalThis.$httpClient[method](resource, (error, response, body) => {
1498
- if (error) reject(error);
1499
- else {
1500
- response.ok = /^2\d\d$/.test(response.status);
1501
- response.statusCode = response.status;
1502
- response.statusText = StatusTexts[response.status];
1503
- if (body) {
1504
- response.body = body;
1505
- if (resource["binary-mode"] == true) response.bodyBytes = body;
1506
- }
1507
- resolve(response);
1508
- }
1509
- });
1510
- });
1511
- case "Quantumult X":
1512
- // 转换 Quantumult X 专有请求参数。
1513
- // Map request fields to Quantumult X specific options.
1514
- if (resource.policy) Lodash.set(resource, "opts.policy", resource.policy);
1515
- if (typeof resource["auto-redirect"] === "boolean") Lodash.set(resource, "opts.redirection", resource["auto-redirect"]);
1516
- // Quantumult X 使用 `bodyBytes` 传输二进制请求体。
1517
- // Quantumult X uses `bodyBytes` for binary request payloads.
1518
- if (resource.body instanceof ArrayBuffer) {
1519
- resource.bodyBytes = resource.body;
1520
- resource.body = undefined;
1521
- } else if (ArrayBuffer.isView(resource.body)) {
1522
- resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset);
1523
- resource.body = undefined;
1524
- } else if (resource.body) resource.bodyBytes = undefined;
1525
- // 发送请求,并用 `Promise.race` 提供统一超时保护。
1526
- // Send the request and enforce timeout with `Promise.race`.
1527
- return Promise.race([
1528
- globalThis.$task.fetch(resource).then(
1529
- response => {
1530
- response.ok = /^2\d\d$/.test(response.statusCode);
1531
- response.status = response.statusCode;
1532
- response.statusText = StatusTexts[response.status];
1533
- switch ((response.headers?.["Content-Type"] ?? response.headers?.["content-type"])?.split(";")?.[0]) {
1534
- case "application/protobuf":
1535
- case "application/x-protobuf":
1536
- case "application/vnd.google.protobuf":
1537
- case "application/vnd.apple.flatbuffer":
1538
- case "application/grpc":
1539
- case "application/grpc-web":
1540
- case "application/grpc+proto":
1541
- case "application/octet-stream":
1542
- response.body = response.bodyBytes;
1543
- break;
1544
- }
1545
- response.bodyBytes = undefined;
1546
- return response;
1547
- },
1548
- reason => Promise.reject(reason.error),
1549
- ),
1550
- new Promise((resolve, reject) => {
1551
- setTimeout(() => {
1552
- reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
1553
- }, resource.timeout);
1554
- }),
1555
- ]);
1556
- case "Worker":
1557
- case "Node.js":
1558
- default: {
1559
- let request;
1560
- let timeout;
1561
- let shouldWrapError = false;
1562
- switch ($app) {
1563
- case "Worker":
1564
- case "Node.js":
1565
- switch (typeof globalThis.fetch) {
1566
- case "function":
1567
- break;
1568
- default:
1569
- throw new Error(`${Function.name}: 当前运行环境不支持 Fetch API`);
1570
- }
1571
- // 将通用字段映射到 Worker / Node.js Fetch 语义。
1572
- // Map shared fields to Worker / Node.js Fetch semantics.
1573
- resource.redirect = resource.redirection ? "follow" : "manual";
1574
- request = resource;
1575
- timeout = resource.timeout;
1576
- shouldWrapError = true;
1577
- break;
1578
- default: {
1579
- // 未识别宿主也可使用完整标准 Fetch API;不将能力推断为宿主类型。
1580
- // An unrecognized host may still use the complete standard Fetch API; capability does not imply a host type.
1581
- if (typeof globalThis.fetch !== "function" || typeof globalThis.Headers !== "function" || typeof globalThis.Request !== "function" || typeof globalThis.Response !== "function") {
1582
- throw new Error(`${Function.name}: 当前运行环境不支持 Fetch API`);
1583
- }
1584
- const { url, bodyBytes, redirection, timeout: _timeout, policy: _policy, "auto-redirect": _autoRedirect, "auto-cookie": _autoCookie, opts: _opts, ...fetchOptions } = resource;
1585
- if (bodyBytes !== undefined && fetchOptions.body === undefined) fetchOptions.body = bodyBytes;
1586
- fetchOptions.redirect = redirection ? "follow" : "manual";
1587
- request = { url, ...fetchOptions };
1588
- timeout = resource.timeout * 1000;
1589
- break;
1590
- }
1591
- }
1592
- const { url, ...options } = request;
1593
- // 发起请求并归一化响应头、文本与二进制响应体。
1594
- // Send the request and normalize headers, text, and binary response data.
1595
- const responsePromise = globalThis.fetch(url, options).then(async response => {
1596
- const bodyBytes = await response.arrayBuffer();
1597
- let headers;
1598
- try {
1599
- headers = response.headers.raw();
1600
- } catch {
1601
- headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => {
1602
- acc[key] = acc[key] ? [...acc[key], value] : [value];
1603
- return acc;
1604
- }, {});
1605
- }
1606
- return {
1607
- ok: response.ok ?? /^2\d\d$/.test(response.status),
1608
- status: response.status,
1609
- statusCode: response.status,
1610
- statusText: response.statusText,
1611
- body: new TextDecoder("utf-8").decode(bodyBytes),
1612
- bodyBytes: bodyBytes,
1613
- headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])),
1614
- };
1615
- });
1616
- return Promise.race([
1617
- shouldWrapError ? responsePromise.catch(error => Promise.reject(error.message)) : responsePromise,
1618
- new Promise((_resolve, reject) => {
1619
- setTimeout(() => {
1620
- reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
1621
- }, timeout);
1622
- }),
1623
- ]);
1624
- }
1625
- }
1626
- }
1627
-
1628
1135
  /**
1629
1136
  * 跨平台持久化存储适配器。
1630
1137
  * Cross-platform persistent storage adapter.
@@ -1922,8 +1429,8 @@
1922
1429
  }
1923
1430
 
1924
1431
  /**
1925
- * PreferencePanes 后端 API,只转发模块配置并提供通用持久化操作。
1926
- * PreferencePanes backend API only relaying module configurations and providing generic persistence operations.
1432
+ * PreferencePanes 后端 API,只提供通用持久化操作。
1433
+ * PreferencePanes backend API providing generic persistence operations only.
1927
1434
  */
1928
1435
  class API {
1929
1436
  /**
@@ -1945,42 +1452,16 @@
1945
1452
  }
1946
1453
 
1947
1454
  /**
1948
- * 处理模块配置 API 与固定存储动作,不接管页面或静态资源。
1949
- * Handle module configuration APIs and fixed storage actions without intercepting pages or static assets.
1455
+ * 处理固定存储动作,不接管模块配置、页面或静态资源。
1456
+ * Handle fixed storage actions without intercepting module configurations, pages, or static assets.
1950
1457
  * @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
1951
1458
  * @returns {Promise<import("./index.js").SettingsResponse | undefined>} API 响应或未接管 / API response or pass-through.
1952
1459
  */
1953
1460
  async handle(request) {
1954
1461
  const url = new URL(request.url);
1955
- const action = /^\/api\/(get|set|delete)\/?$/.exec(url.pathname)?.[1];
1462
+ const action = /^\/api\/(get|set|delete)$/.exec(url.pathname)?.[1];
1956
1463
  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}`);
1961
- }
1962
-
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) {
1971
- let result;
1972
- try {
1973
- result = await fetch({ url: configuration, method: request.method, timeout: 5000, headers: { Accept: "application/json" } });
1974
- } catch (error) {
1975
- return this.#response(request, 502, { error: error.message });
1976
- }
1977
- const version = this.#header(result.headers, "x-preferencepanes-version");
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
- };
1464
+ return;
1984
1465
  }
1985
1466
 
1986
1467
  /**
@@ -2049,11 +1530,6 @@
2049
1530
  body: request.method === "HEAD" ? "" : JSON.stringify(body),
2050
1531
  };
2051
1532
  }
2052
-
2053
- #header(headers, name) {
2054
- const entry = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === name);
2055
- return entry?.[1] === undefined ? undefined : String(entry[1]).trim();
2056
- }
2057
1533
  }
2058
1534
 
2059
1535
  /**
@@ -9,6 +9,6 @@
9
9
  </head>
10
10
  <body>
11
11
  <main id="preferences"></main>
12
- <script type="module" src="/settings/assets/index.mjs?v=1.1.2"></script>
12
+ <script type="module" src="/settings/assets/index.mjs?v=1.1.3"></script>
13
13
  </body>
14
14
  </html>
@@ -35,6 +35,7 @@ function normalizeBoxJs(config, module) {
35
35
  if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
36
36
  validatePathParts(parts);
37
37
  const name = parts[0];
38
+ if (["get", "set", "delete"].includes(name)) throw new TypeError(`Reserved API module name: ${name}`);
38
39
  let target = modules.get(name);
39
40
  if (!target) {
40
41
  target = { module: name, storageKey, entries: [], owners: new Set() };
@@ -331,7 +331,7 @@ class ModuleStatus extends EventTarget {
331
331
  const response = await probeModule(url, { ...options, signal: controller.signal });
332
332
  if (controller !== this.#controller) return response;
333
333
  const version = response.status === 200 ? response.headers.get("X-PreferencePanes-Version")?.trim() || null : null;
334
- this.#render(response.status === 200 ? "installed" : "missing", version);
334
+ this.#render(version ? "installed" : "missing", version);
335
335
  return response;
336
336
  } catch (error) {
337
337
  if (controller !== this.#controller) return;
@@ -35,6 +35,7 @@ function normalizeBoxJs(config, module) {
35
35
  if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
36
36
  validatePathParts(parts);
37
37
  const name = parts[0];
38
+ if (["get", "set", "delete"].includes(name)) throw new TypeError(`Reserved API module name: ${name}`);
38
39
  let target = modules.get(name);
39
40
  if (!target) {
40
41
  target = { module: name, storageKey, entries: [], owners: new Set() };