@nsnanocat/preference-panes 0.2.0 → 0.3.1
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 +60 -103
- package/dist/preference-panes.mjs +849 -1100
- package/dist/preference-panes.request.js +168 -571
- package/package.json +64 -64
- package/src/SettingsHandler.mjs +127 -123
- package/src/browser/client.mjs +218 -102
- package/src/browser/index.d.ts +143 -20
- package/src/browser/index.mjs +5 -0
- package/src/browser/panel.css +293 -94
- package/src/browser/panel.mjs +510 -272
- package/src/index.d.ts +143 -54
- package/src/index.mjs +5 -0
- package/src/lib/boxjs.mjs +117 -104
- package/src/lib/settings-path.mjs +31 -11
- package/src/proxy/request.mjs +23 -14
|
@@ -1295,304 +1295,6 @@
|
|
|
1295
1295
|
});
|
|
1296
1296
|
}
|
|
1297
1297
|
|
|
1298
|
-
/**
|
|
1299
|
-
* 统一请求参数。
|
|
1300
|
-
* Unified request payload.
|
|
1301
|
-
*
|
|
1302
|
-
* @typedef {object} FetchRequest
|
|
1303
|
-
* @property {string} url 请求地址 / Request URL.
|
|
1304
|
-
* @property {string} [method] 请求方法 / HTTP method.
|
|
1305
|
-
* @property {Record<string, any>} [headers] 请求头 / Request headers.
|
|
1306
|
-
* @property {string|ArrayBuffer|ArrayBufferView|object} [body] 请求体 / Request body.
|
|
1307
|
-
* @property {ArrayBuffer} [bodyBytes] 二进制请求体 / Binary request body.
|
|
1308
|
-
* @property {number|string} [timeout] 超时(秒或毫秒)/ Timeout (seconds or milliseconds).
|
|
1309
|
-
* @property {string} [policy] 指定策略 / Preferred policy.
|
|
1310
|
-
* @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
|
|
1311
|
-
* @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag.
|
|
1312
|
-
* @property {boolean|number|string} ["auto-cookie"] Worker / Node.js Cookie 开关 / Worker / Node.js Cookie toggle.
|
|
1313
|
-
* @property {Record<string, any>} [opts] 平台扩展字段 / Platform extension fields.
|
|
1314
|
-
*/
|
|
1315
|
-
|
|
1316
|
-
/**
|
|
1317
|
-
* 统一响应结构。
|
|
1318
|
-
* Unified response payload.
|
|
1319
|
-
*
|
|
1320
|
-
* @typedef {object} FetchResponse
|
|
1321
|
-
* @property {boolean} ok 请求是否成功 / Whether request is successful.
|
|
1322
|
-
* @property {number} status 状态码 / HTTP status code.
|
|
1323
|
-
* @property {number} [statusCode] 状态码别名 / Status code alias.
|
|
1324
|
-
* @property {string} [statusText] 状态文本 / HTTP status text.
|
|
1325
|
-
* @property {Record<string, any>} [headers] 响应头 / Response headers.
|
|
1326
|
-
* @property {string|ArrayBuffer} [body] 响应体 / Response body.
|
|
1327
|
-
* @property {ArrayBuffer} [bodyBytes] 二进制响应体 / Binary response body.
|
|
1328
|
-
*/
|
|
1329
|
-
|
|
1330
|
-
/**
|
|
1331
|
-
* 跨平台 `fetch` 适配层。
|
|
1332
|
-
* Cross-platform `fetch` adapter.
|
|
1333
|
-
*
|
|
1334
|
-
* 设计目标:
|
|
1335
|
-
* Design goal:
|
|
1336
|
-
* - 仿照 Web API `fetch`(`Window.fetch`)接口设计
|
|
1337
|
-
* - Modeled after Web API `fetch` (`Window.fetch`)
|
|
1338
|
-
* - 统一 VPN App、Worker 与 Node.js 环境中的请求调用
|
|
1339
|
-
* - Unify request calls across VPN apps, Worker, and Node.js
|
|
1340
|
-
*
|
|
1341
|
-
* 功能:
|
|
1342
|
-
* Features:
|
|
1343
|
-
* - 统一 Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js 请求接口
|
|
1344
|
-
* - Normalize request APIs across Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js
|
|
1345
|
-
* - 统一返回体字段(`ok/status/statusText/body/bodyBytes`)
|
|
1346
|
-
* - Normalize response fields (`ok/status/statusText/body/bodyBytes`)
|
|
1347
|
-
*
|
|
1348
|
-
* 与 Web `fetch` 的已知差异:
|
|
1349
|
-
* Known differences from Web `fetch`:
|
|
1350
|
-
* - 支持 `policy`、`auto-redirect` 等平台扩展字段
|
|
1351
|
-
* - Supports platform extension fields like `policy` and `auto-redirect`
|
|
1352
|
-
* - Worker / Node.js 共享基于 `fetch` 的请求分支
|
|
1353
|
-
* - Worker / Node.js share the `fetch`-based request branch
|
|
1354
|
-
* - Node.js ESM 的 `auto-cookie` 由 `fetch.node.mjs` 处理,本文件只使用宿主 `fetch`
|
|
1355
|
-
* - Node.js ESM `auto-cookie` is handled by `fetch.node.mjs`; this module only uses the host `fetch`
|
|
1356
|
-
* - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现
|
|
1357
|
-
* - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine
|
|
1358
|
-
* - 返回结构包含 `statusCode/bodyBytes` 等兼容字段
|
|
1359
|
-
* - Response includes compatibility fields like `statusCode/bodyBytes`
|
|
1360
|
-
*
|
|
1361
|
-
* @link https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
|
1362
|
-
* @link https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
|
|
1363
|
-
* @async
|
|
1364
|
-
* @param {FetchRequest|string} resource 请求对象或 URL / Request object or URL string.
|
|
1365
|
-
* @param {Partial<FetchRequest>} [options={}] 追加参数 / Extra options.
|
|
1366
|
-
* @returns {Promise<FetchResponse>}
|
|
1367
|
-
*/
|
|
1368
|
-
async function fetch(resource, options = {}) {
|
|
1369
|
-
// 初始化参数。
|
|
1370
|
-
// Initialize request input.
|
|
1371
|
-
switch (typeof resource) {
|
|
1372
|
-
case "object":
|
|
1373
|
-
resource = { ...options, ...resource };
|
|
1374
|
-
break;
|
|
1375
|
-
case "string":
|
|
1376
|
-
resource = { ...options, url: resource };
|
|
1377
|
-
break;
|
|
1378
|
-
case "undefined":
|
|
1379
|
-
default:
|
|
1380
|
-
throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
|
|
1381
|
-
}
|
|
1382
|
-
// 自动判断请求方法。
|
|
1383
|
-
// Infer the HTTP method automatically.
|
|
1384
|
-
if (!resource.method) {
|
|
1385
|
-
resource.method = "GET";
|
|
1386
|
-
if (resource.body ?? resource.bodyBytes) resource.method = "POST";
|
|
1387
|
-
}
|
|
1388
|
-
// 移除需要由底层实现自动生成的请求头。
|
|
1389
|
-
// Remove headers that should be generated by the underlying runtime.
|
|
1390
|
-
delete resource.headers?.Host;
|
|
1391
|
-
delete resource.headers?.[":authority"];
|
|
1392
|
-
delete resource.headers?.["Content-Length"];
|
|
1393
|
-
delete resource.headers?.["content-length"];
|
|
1394
|
-
// 统一请求方法为小写,方便后续索引平台 API。
|
|
1395
|
-
// Normalize the method to lowercase for platform API lookups.
|
|
1396
|
-
const method = resource.method.toLocaleLowerCase();
|
|
1397
|
-
// 默认请求超时时间为 5 秒。
|
|
1398
|
-
// Default request timeout to 5 seconds.
|
|
1399
|
-
if (!resource.timeout) resource.timeout = 5;
|
|
1400
|
-
if (resource.timeout) {
|
|
1401
|
-
resource.timeout = Number.parseInt(resource.timeout, 10);
|
|
1402
|
-
// 统一先转换为秒,大于 500 视为毫秒输入。
|
|
1403
|
-
// Convert to seconds first and treat values above 500 as milliseconds.
|
|
1404
|
-
if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
|
|
1405
|
-
}
|
|
1406
|
-
if (resource.timeout) {
|
|
1407
|
-
switch ($app) {
|
|
1408
|
-
case "Loon":
|
|
1409
|
-
case "Quantumult X":
|
|
1410
|
-
case "Worker":
|
|
1411
|
-
case "Node.js":
|
|
1412
|
-
// 这些平台要求毫秒,因此把秒重新换算为毫秒。
|
|
1413
|
-
// These platforms expect milliseconds, so convert seconds back to milliseconds.
|
|
1414
|
-
resource.timeout = resource.timeout * 1000;
|
|
1415
|
-
break;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
// 根据当前平台选择请求实现。
|
|
1419
|
-
// Select the request engine for the current platform.
|
|
1420
|
-
switch ($app) {
|
|
1421
|
-
case "Loon":
|
|
1422
|
-
case "Surge":
|
|
1423
|
-
case "Stash":
|
|
1424
|
-
case "Egern":
|
|
1425
|
-
case "Shadowrocket":
|
|
1426
|
-
// 转换通用请求参数到 `$httpClient` 语义。
|
|
1427
|
-
// Map shared request fields to `$httpClient` semantics.
|
|
1428
|
-
if (resource.policy) {
|
|
1429
|
-
switch ($app) {
|
|
1430
|
-
case "Loon":
|
|
1431
|
-
resource.node = resource.policy;
|
|
1432
|
-
break;
|
|
1433
|
-
case "Stash":
|
|
1434
|
-
Lodash.set(resource, "headers.X-Stash-Selected-Proxy", encodeURI(resource.policy));
|
|
1435
|
-
break;
|
|
1436
|
-
case "Shadowrocket":
|
|
1437
|
-
Lodash.set(resource, "headers.X-Surge-Proxy", resource.policy);
|
|
1438
|
-
break;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection;
|
|
1442
|
-
// 优先把 `bodyBytes` 映射回 `$httpClient` 能接受的 `body`。
|
|
1443
|
-
// Prefer mapping `bodyBytes` back to the `body` field expected by `$httpClient`.
|
|
1444
|
-
if (resource.bodyBytes && !resource.body) {
|
|
1445
|
-
resource.body = resource.bodyBytes;
|
|
1446
|
-
resource.bodyBytes = undefined;
|
|
1447
|
-
}
|
|
1448
|
-
// 根据 `Accept` 推断是否需要二进制响应体。
|
|
1449
|
-
// Infer whether the response should be treated as binary from `Accept`.
|
|
1450
|
-
switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) {
|
|
1451
|
-
case "application/protobuf":
|
|
1452
|
-
case "application/x-protobuf":
|
|
1453
|
-
case "application/vnd.google.protobuf":
|
|
1454
|
-
case "application/vnd.apple.flatbuffer":
|
|
1455
|
-
case "application/grpc":
|
|
1456
|
-
case "application/grpc-web":
|
|
1457
|
-
case "application/grpc+proto":
|
|
1458
|
-
case "application/octet-stream":
|
|
1459
|
-
resource["binary-mode"] = true;
|
|
1460
|
-
break;
|
|
1461
|
-
}
|
|
1462
|
-
// 发送 `$httpClient` 请求并归一化返回结构。
|
|
1463
|
-
// Send the `$httpClient` request and normalize the response payload.
|
|
1464
|
-
return new Promise((resolve, reject) => {
|
|
1465
|
-
globalThis.$httpClient[method](resource, (error, response, body) => {
|
|
1466
|
-
if (error) reject(error);
|
|
1467
|
-
else {
|
|
1468
|
-
response.ok = /^2\d\d$/.test(response.status);
|
|
1469
|
-
response.statusCode = response.status;
|
|
1470
|
-
response.statusText = StatusTexts[response.status];
|
|
1471
|
-
if (body) {
|
|
1472
|
-
response.body = body;
|
|
1473
|
-
if (resource["binary-mode"] == true) response.bodyBytes = body;
|
|
1474
|
-
}
|
|
1475
|
-
resolve(response);
|
|
1476
|
-
}
|
|
1477
|
-
});
|
|
1478
|
-
});
|
|
1479
|
-
case "Quantumult X":
|
|
1480
|
-
// 转换 Quantumult X 专有请求参数。
|
|
1481
|
-
// Map request fields to Quantumult X specific options.
|
|
1482
|
-
if (resource.policy) Lodash.set(resource, "opts.policy", resource.policy);
|
|
1483
|
-
if (typeof resource["auto-redirect"] === "boolean") Lodash.set(resource, "opts.redirection", resource["auto-redirect"]);
|
|
1484
|
-
// Quantumult X 使用 `bodyBytes` 传输二进制请求体。
|
|
1485
|
-
// Quantumult X uses `bodyBytes` for binary request payloads.
|
|
1486
|
-
if (resource.body instanceof ArrayBuffer) {
|
|
1487
|
-
resource.bodyBytes = resource.body;
|
|
1488
|
-
resource.body = undefined;
|
|
1489
|
-
} else if (ArrayBuffer.isView(resource.body)) {
|
|
1490
|
-
resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset);
|
|
1491
|
-
resource.body = undefined;
|
|
1492
|
-
} else if (resource.body) resource.bodyBytes = undefined;
|
|
1493
|
-
// 发送请求,并用 `Promise.race` 提供统一超时保护。
|
|
1494
|
-
// Send the request and enforce timeout with `Promise.race`.
|
|
1495
|
-
return Promise.race([
|
|
1496
|
-
globalThis.$task.fetch(resource).then(
|
|
1497
|
-
response => {
|
|
1498
|
-
response.ok = /^2\d\d$/.test(response.statusCode);
|
|
1499
|
-
response.status = response.statusCode;
|
|
1500
|
-
response.statusText = StatusTexts[response.status];
|
|
1501
|
-
switch ((response.headers?.["Content-Type"] ?? response.headers?.["content-type"])?.split(";")?.[0]) {
|
|
1502
|
-
case "application/protobuf":
|
|
1503
|
-
case "application/x-protobuf":
|
|
1504
|
-
case "application/vnd.google.protobuf":
|
|
1505
|
-
case "application/vnd.apple.flatbuffer":
|
|
1506
|
-
case "application/grpc":
|
|
1507
|
-
case "application/grpc-web":
|
|
1508
|
-
case "application/grpc+proto":
|
|
1509
|
-
case "application/octet-stream":
|
|
1510
|
-
response.body = response.bodyBytes;
|
|
1511
|
-
break;
|
|
1512
|
-
}
|
|
1513
|
-
response.bodyBytes = undefined;
|
|
1514
|
-
return response;
|
|
1515
|
-
},
|
|
1516
|
-
reason => Promise.reject(reason.error),
|
|
1517
|
-
),
|
|
1518
|
-
new Promise((resolve, reject) => {
|
|
1519
|
-
setTimeout(() => {
|
|
1520
|
-
reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
|
|
1521
|
-
}, resource.timeout);
|
|
1522
|
-
}),
|
|
1523
|
-
]);
|
|
1524
|
-
case "Worker":
|
|
1525
|
-
case "Node.js":
|
|
1526
|
-
default: {
|
|
1527
|
-
let request;
|
|
1528
|
-
let timeout;
|
|
1529
|
-
let shouldWrapError = false;
|
|
1530
|
-
switch ($app) {
|
|
1531
|
-
case "Worker":
|
|
1532
|
-
case "Node.js":
|
|
1533
|
-
switch (typeof globalThis.fetch) {
|
|
1534
|
-
case "function":
|
|
1535
|
-
break;
|
|
1536
|
-
default:
|
|
1537
|
-
throw new Error(`${Function.name}: 当前运行环境不支持 Fetch API`);
|
|
1538
|
-
}
|
|
1539
|
-
// 将通用字段映射到 Worker / Node.js Fetch 语义。
|
|
1540
|
-
// Map shared fields to Worker / Node.js Fetch semantics.
|
|
1541
|
-
resource.redirect = resource.redirection ? "follow" : "manual";
|
|
1542
|
-
request = resource;
|
|
1543
|
-
timeout = resource.timeout;
|
|
1544
|
-
shouldWrapError = true;
|
|
1545
|
-
break;
|
|
1546
|
-
default: {
|
|
1547
|
-
// 未识别宿主也可使用完整标准 Fetch API;不将能力推断为宿主类型。
|
|
1548
|
-
// An unrecognized host may still use the complete standard Fetch API; capability does not imply a host type.
|
|
1549
|
-
if (typeof globalThis.fetch !== "function" || typeof globalThis.Headers !== "function" || typeof globalThis.Request !== "function" || typeof globalThis.Response !== "function") {
|
|
1550
|
-
throw new Error(`${Function.name}: 当前运行环境不支持 Fetch API`);
|
|
1551
|
-
}
|
|
1552
|
-
const { url, bodyBytes, redirection, timeout: _timeout, policy: _policy, "auto-redirect": _autoRedirect, "auto-cookie": _autoCookie, opts: _opts, ...fetchOptions } = resource;
|
|
1553
|
-
if (bodyBytes !== undefined && fetchOptions.body === undefined) fetchOptions.body = bodyBytes;
|
|
1554
|
-
fetchOptions.redirect = redirection ? "follow" : "manual";
|
|
1555
|
-
request = { url, ...fetchOptions };
|
|
1556
|
-
timeout = resource.timeout * 1000;
|
|
1557
|
-
break;
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
const { url, ...options } = request;
|
|
1561
|
-
// 发起请求并归一化响应头、文本与二进制响应体。
|
|
1562
|
-
// Send the request and normalize headers, text, and binary response data.
|
|
1563
|
-
const responsePromise = globalThis.fetch(url, options).then(async response => {
|
|
1564
|
-
const bodyBytes = await response.arrayBuffer();
|
|
1565
|
-
let headers;
|
|
1566
|
-
try {
|
|
1567
|
-
headers = response.headers.raw();
|
|
1568
|
-
} catch {
|
|
1569
|
-
headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => {
|
|
1570
|
-
acc[key] = acc[key] ? [...acc[key], value] : [value];
|
|
1571
|
-
return acc;
|
|
1572
|
-
}, {});
|
|
1573
|
-
}
|
|
1574
|
-
return {
|
|
1575
|
-
ok: response.ok ?? /^2\d\d$/.test(response.status),
|
|
1576
|
-
status: response.status,
|
|
1577
|
-
statusCode: response.status,
|
|
1578
|
-
statusText: response.statusText,
|
|
1579
|
-
body: new TextDecoder("utf-8").decode(bodyBytes),
|
|
1580
|
-
bodyBytes: bodyBytes,
|
|
1581
|
-
headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])),
|
|
1582
|
-
};
|
|
1583
|
-
});
|
|
1584
|
-
return Promise.race([
|
|
1585
|
-
shouldWrapError ? responsePromise.catch(error => Promise.reject(error.message)) : responsePromise,
|
|
1586
|
-
new Promise((_resolve, reject) => {
|
|
1587
|
-
setTimeout(() => {
|
|
1588
|
-
reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
|
|
1589
|
-
}, timeout);
|
|
1590
|
-
}),
|
|
1591
|
-
]);
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
1298
|
/**
|
|
1597
1299
|
* 跨平台持久化存储适配器。
|
|
1598
1300
|
* Cross-platform persistent storage adapter.
|
|
@@ -1878,300 +1580,195 @@
|
|
|
1878
1580
|
}
|
|
1879
1581
|
|
|
1880
1582
|
/**
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
* @param {string}
|
|
1884
|
-
* @returns {string[] | undefined}
|
|
1885
|
-
* @throws {TypeError}
|
|
1583
|
+
* 解析已经取得的 pathname,避免重复构造 URL。
|
|
1584
|
+
* Parse an existing pathname without constructing another URL.
|
|
1585
|
+
* @param {string} pathname 以 / 开头的 URL pathname / URL pathname beginning with /.
|
|
1586
|
+
* @returns {string[] | undefined} 解码后的路径,非 API 路径不处理 / Decoded path, or undefined outside /api/.
|
|
1587
|
+
* @throws {TypeError} 转义编码或路径片段非法 / Invalid percent encoding or path segments.
|
|
1886
1588
|
*/
|
|
1887
|
-
function
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
if (!parts.every((part) => /^[a-zA-Z0-9_-]+$/.test(part) && !["__proto__", "prototype", "constructor"].includes(part)))
|
|
1897
|
-
throw new TypeError("Invalid key path");
|
|
1898
|
-
return parts;
|
|
1589
|
+
function parseSettingsPathname(pathname) {
|
|
1590
|
+
if (!pathname.startsWith("/api/")) return;
|
|
1591
|
+
let parts;
|
|
1592
|
+
try {
|
|
1593
|
+
parts = pathname.slice(5).replace(/\/$/, "").split("/").map(decodeURIComponent);
|
|
1594
|
+
} catch {
|
|
1595
|
+
throw new TypeError("Invalid encoded key path");
|
|
1596
|
+
}
|
|
1597
|
+
return validatePathParts(parts);
|
|
1899
1598
|
}
|
|
1900
1599
|
|
|
1901
1600
|
/**
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1904
|
-
* @param {
|
|
1905
|
-
* @
|
|
1906
|
-
* @
|
|
1601
|
+
* 校验原始路径片段,不进行 URL 编码转换。
|
|
1602
|
+
* Validate raw path segments without URL encoding conversion.
|
|
1603
|
+
* @param {string[]} parts 原始路径片段 / Raw path segments.
|
|
1604
|
+
* @returns {string[]} 同一数组,不复制或修改 / The same array without copying or mutation.
|
|
1605
|
+
* @throws {TypeError} 空片段、非法字符或原型属性名 / Empty segments, invalid characters or prototype property names.
|
|
1907
1606
|
*/
|
|
1908
|
-
function
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
|
|
1912
|
-
for (const candidate of apps) {
|
|
1913
|
-
if (!candidate || typeof candidate !== "object") throw new TypeError("Expected BoxJS app object");
|
|
1914
|
-
if (candidate.settings !== undefined && !Array.isArray(candidate.settings)) throw new TypeError("Expected BoxJS settings array");
|
|
1915
|
-
}
|
|
1916
|
-
const owners = apps.filter((candidate) =>
|
|
1917
|
-
candidate.settings?.some(
|
|
1918
|
-
(entry) => typeof entry.id === "string" && entry.id.startsWith("@") && entry.id.slice(1).split(".")[1] === module,
|
|
1919
|
-
),
|
|
1920
|
-
);
|
|
1921
|
-
const entries = Array.isArray(config) ? config : owners.flatMap((candidate) => candidate.settings);
|
|
1922
|
-
const app = owners.length === 1 ? owners[0] : undefined;
|
|
1923
|
-
if (!Array.isArray(entries)) throw new TypeError("Expected BoxJS settings array, app or subscription");
|
|
1924
|
-
let storageKey;
|
|
1925
|
-
const fields = [];
|
|
1926
|
-
for (const entry of entries) {
|
|
1927
|
-
if (typeof entry.id !== "string" || !entry.id.startsWith("@")) throw new TypeError("BoxJS settings require @root.path IDs");
|
|
1928
|
-
const [root, ...parts] = entry.id.slice(1).split(".");
|
|
1929
|
-
if (parts[0] !== module) continue;
|
|
1930
|
-
if (parts.length < 2) throw new TypeError("A BoxJS setting must be below the module root");
|
|
1931
|
-
parseSettingsPath(`https://example.invalid/api/${parts.map(encodeURIComponent).join("/")}`);
|
|
1932
|
-
if (!root || (storageKey && root !== storageKey)) throw new TypeError("A module must use one storage root");
|
|
1933
|
-
storageKey = root;
|
|
1934
|
-
const type = { boolean: "boolean", checkboxes: "array", selects: "select", text: "string", textarea: "string", number: "number" }[
|
|
1935
|
-
entry.type
|
|
1936
|
-
];
|
|
1937
|
-
if (!type) throw new TypeError(`Unsupported BoxJS control: ${entry.type}`);
|
|
1938
|
-
const field = {
|
|
1939
|
-
key: parts.join("."),
|
|
1940
|
-
name: entry.name,
|
|
1941
|
-
type: type === "select" ? typeof entry.val : type,
|
|
1942
|
-
description: entry.desc ?? "",
|
|
1943
|
-
control: entry.type,
|
|
1944
|
-
...(entry.placeholder === undefined ? {} : { placeholder: entry.placeholder }),
|
|
1945
|
-
...(entry.rows === undefined ? {} : { rows: entry.rows }),
|
|
1946
|
-
...(entry.autoGrow === undefined ? {} : { autoGrow: entry.autoGrow }),
|
|
1947
|
-
};
|
|
1948
|
-
if (type === "select" && !["string", "number", "boolean"].includes(field.type))
|
|
1949
|
-
throw new TypeError(`Select requires a scalar val: ${entry.id}`);
|
|
1950
|
-
if (entry.items) field.options = entry.items.map((item) => ({ key: item.key, label: item.label }));
|
|
1951
|
-
if (Object.hasOwn(entry, "val")) field.defaultValue = normalizeStoredValue(field, entry.val);
|
|
1952
|
-
if (
|
|
1953
|
-
typeof field.name !== "string" ||
|
|
1954
|
-
(field.placeholder !== undefined && typeof field.placeholder !== "string") ||
|
|
1955
|
-
(field.rows !== undefined && (!Number.isInteger(field.rows) || field.rows < 1)) ||
|
|
1956
|
-
(field.autoGrow !== undefined && typeof field.autoGrow !== "boolean") ||
|
|
1957
|
-
fields.some((other) => other.key === field.key || other.key.startsWith(`${field.key}.`) || field.key.startsWith(`${other.key}.`))
|
|
1958
|
-
)
|
|
1959
|
-
throw new TypeError(`Invalid or overlapping BoxJS field: ${entry.id}`);
|
|
1960
|
-
if (
|
|
1961
|
-
field.options &&
|
|
1962
|
-
(new Set(field.options.map((item) => item.key)).size !== field.options.length ||
|
|
1963
|
-
field.options.some((item) => !scalar(item.key) || typeof item.label !== "string"))
|
|
1964
|
-
)
|
|
1965
|
-
throw new TypeError(`Invalid options: ${entry.id}`);
|
|
1966
|
-
if (Object.hasOwn(field, "defaultValue") && !validValue(field, field.defaultValue))
|
|
1967
|
-
throw new TypeError(`Invalid BoxJS val: ${entry.id}`);
|
|
1968
|
-
fields.push(field);
|
|
1969
|
-
}
|
|
1970
|
-
if (!fields.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
1971
|
-
const common = fields[0].key.split(".").slice(0, -1);
|
|
1972
|
-
for (const field of fields) while (!field.key.startsWith(`${common.join(".")}.`)) common.pop();
|
|
1973
|
-
const metadata = {};
|
|
1974
|
-
if (app) {
|
|
1975
|
-
for (const key of ["id", "name", "author", "repo", "script", "icon", "description", "desc"]) {
|
|
1976
|
-
if (app[key] === undefined) continue;
|
|
1977
|
-
if (typeof app[key] !== "string") throw new TypeError(`Invalid BoxJS app ${key}`);
|
|
1978
|
-
metadata[key] = app[key];
|
|
1979
|
-
}
|
|
1980
|
-
for (const key of ["icons", "descs"]) {
|
|
1981
|
-
if (app[key] === undefined) continue;
|
|
1982
|
-
if (!Array.isArray(app[key]) || app[key].some((item) => typeof item !== "string")) throw new TypeError(`Invalid BoxJS app ${key}`);
|
|
1983
|
-
metadata[key] = [...app[key]];
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
return {
|
|
1987
|
-
module,
|
|
1988
|
-
storageKey,
|
|
1989
|
-
fields,
|
|
1990
|
-
settingsPath: common,
|
|
1991
|
-
...(Object.keys(metadata).length ? { metadata } : {}),
|
|
1992
|
-
};
|
|
1607
|
+
function validatePathParts(parts) {
|
|
1608
|
+
if (!parts.every(part => typeof part === "string" && /^[a-zA-Z0-9_-]+$/.test(part) && !["__proto__", "prototype", "constructor"].includes(part))) throw new TypeError("Invalid key path");
|
|
1609
|
+
return parts;
|
|
1993
1610
|
}
|
|
1994
1611
|
|
|
1995
1612
|
/**
|
|
1996
|
-
*
|
|
1997
|
-
*
|
|
1998
|
-
* @param {import("../index.js").SettingsField} field 字段 / Field.
|
|
1999
|
-
* @param {unknown} value 存储值 / Stored value.
|
|
2000
|
-
* @returns {unknown} 控件值 / Control value.
|
|
1613
|
+
* 按插件声明的根和模块桥接持久化存储,不下载或解析 BoxJS。
|
|
1614
|
+
* Bridge persistence within the installed root and module without downloading or parsing BoxJS.
|
|
2001
1615
|
*/
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
}
|
|
1616
|
+
class SettingsHandler {
|
|
1617
|
+
/** @type {string} 接管来源 / Handled origin. */
|
|
1618
|
+
#origin;
|
|
1619
|
+
/** @type {string} 安装配置中的存储根 / Storage root from installation config. */
|
|
1620
|
+
#storageKey;
|
|
1621
|
+
/** @type {string} 安装配置中的模块 / Module from installation config. */
|
|
1622
|
+
#module;
|
|
1623
|
+
/** @type {string} 页面标记头 / Page marker header. */
|
|
1624
|
+
#requestHeader;
|
|
2012
1625
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
1626
|
+
/**
|
|
1627
|
+
* 固定来源、存储根和模块,构造时不访问网络或存储。
|
|
1628
|
+
* Fix the origin, storage root and module without network or storage access at construction.
|
|
1629
|
+
* @param {import("./index.js").SettingsHandlerOptions} options 插件安装配置 / Plugin installation config.
|
|
1630
|
+
* @throws {TypeError} 安装配置无效 / Invalid installation config.
|
|
1631
|
+
*/
|
|
1632
|
+
constructor({ origin, storageKey, module, requestHeader = "X-Settings-Client" }) {
|
|
1633
|
+
const target = new URL(origin);
|
|
1634
|
+
if (target.protocol !== "https:" || target.pathname !== "/" || target.search || target.hash || target.username || target.password) throw new TypeError("origin must be an HTTPS origin");
|
|
1635
|
+
if (typeof storageKey !== "string" || !storageKey || storageKey.startsWith("@")) throw new TypeError("storageKey must be a literal root key");
|
|
1636
|
+
validatePathParts([module]);
|
|
1637
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(requestHeader)) throw new TypeError("Invalid requestHeader");
|
|
1638
|
+
this.#origin = target.origin;
|
|
1639
|
+
this.#storageKey = storageKey;
|
|
1640
|
+
this.#module = module;
|
|
1641
|
+
this.#requestHeader = requestHeader;
|
|
1642
|
+
}
|
|
2020
1643
|
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
1644
|
+
/**
|
|
1645
|
+
* GET 返回指定值,POST 替换指定值,DELETE 删除指定键或整个模块。
|
|
1646
|
+
* GET returns a value, POST replaces it, and DELETE removes a key or the entire module.
|
|
1647
|
+
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
1648
|
+
* @returns {Promise<import("./index.js").SettingsResponse | undefined>} 响应或非接管请求 / Response, or undefined for an unhandled request.
|
|
1649
|
+
*/
|
|
1650
|
+
async handle(request) {
|
|
1651
|
+
const url = new URL(request.url);
|
|
1652
|
+
if (url.origin !== this.#origin || !url.pathname.startsWith("/api/")) return;
|
|
1653
|
+
const headers = { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" };
|
|
1654
|
+
const reply = (status, data) => ({ status, headers, body: request.method === "HEAD" ? "" : JSON.stringify(data) });
|
|
1655
|
+
let parts;
|
|
1656
|
+
try {
|
|
1657
|
+
parts = parseSettingsPathname(url.pathname);
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
return reply(400, { error: error.message });
|
|
1660
|
+
}
|
|
1661
|
+
if (parts[0] !== this.#module) return reply(404, { error: "Module is not handled" });
|
|
1662
|
+
const requestHeaders = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
1663
|
+
if (requestHeaders[this.#requestHeader.toLowerCase()] !== "1" || (requestHeaders.origin && requestHeaders.origin !== this.#origin)) return reply(403, { error: "Forbidden settings client" });
|
|
1664
|
+
let value;
|
|
1665
|
+
switch (request.method) {
|
|
1666
|
+
case "HEAD":
|
|
1667
|
+
return reply(200, undefined);
|
|
1668
|
+
case "GET":
|
|
1669
|
+
case "DELETE":
|
|
1670
|
+
break;
|
|
1671
|
+
case "POST":
|
|
1672
|
+
if (requestHeaders["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/json") return reply(415, { error: "Expected application/json" });
|
|
1673
|
+
if (typeof request.body !== "string") return reply(400, { error: "Expected a JSON string body" });
|
|
1674
|
+
if (request.body.length > 65536) return reply(413, { error: "Body exceeds 65536 UTF-16 code units" });
|
|
1675
|
+
try {
|
|
1676
|
+
value = JSON.parse(request.body);
|
|
1677
|
+
} catch {
|
|
1678
|
+
return reply(400, { error: "Invalid JSON" });
|
|
1679
|
+
}
|
|
1680
|
+
break;
|
|
1681
|
+
default:
|
|
1682
|
+
return { ...reply(405, { error: "Method not allowed" }), headers: { ...headers, Allow: "HEAD, GET, POST, DELETE" } };
|
|
1683
|
+
}
|
|
1684
|
+
try {
|
|
1685
|
+
const root = Storage.getItem(this.#storageKey, {});
|
|
1686
|
+
if (!isRecord(root)) throw new TypeError("stored root must be an object");
|
|
1687
|
+
const parent = storageParent(root, parts, request.method === "POST");
|
|
1688
|
+
const key = parts.at(-1);
|
|
1689
|
+
switch (request.method) {
|
|
1690
|
+
case "GET": {
|
|
1691
|
+
const result = parent ? Lodash.get(parent, [key]) : undefined;
|
|
1692
|
+
return result === undefined ? reply(404, { error: "Stored path does not exist" }) : reply(200, result);
|
|
1693
|
+
}
|
|
1694
|
+
case "POST":
|
|
1695
|
+
Lodash.set(parent, [key], value);
|
|
1696
|
+
break;
|
|
1697
|
+
case "DELETE":
|
|
1698
|
+
if (parent) Lodash.unset(parent, [key]);
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
1701
|
+
if (!Storage.setItem(this.#storageKey, root)) throw new Error("Storage write failed");
|
|
1702
|
+
return reply(200, request.method === "POST" ? { saved: true } : { deleted: true });
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
return reply(500, { error: error.message });
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
2026
1707
|
}
|
|
2027
1708
|
|
|
2028
1709
|
/**
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
1710
|
+
* 判断根节点是否为普通对象。
|
|
1711
|
+
* Determine whether a root node is a plain object.
|
|
1712
|
+
* @param {unknown} value 待检查值 / Value to inspect.
|
|
1713
|
+
* @returns {boolean} 是否为普通对象 / Whether this is a plain object.
|
|
2031
1714
|
*/
|
|
2032
|
-
class SettingsHandler {
|
|
2033
|
-
#origin;
|
|
2034
|
-
#configURL;
|
|
2035
|
-
#requestHeader;
|
|
2036
|
-
#resolveSettings;
|
|
2037
|
-
|
|
2038
|
-
/** @param {import("./index.js").SettingsHandlerOptions} options 来源、配置地址与 GET 解析器 / Origin, config source and GET resolver. */
|
|
2039
|
-
constructor({ origin, configURL, requestHeader = "X-Settings-Client", resolveSettings }) {
|
|
2040
|
-
const target = new URL(origin);
|
|
2041
|
-
if (target.protocol !== "https:" || target.pathname !== "/" || target.search || target.hash || target.username || target.password)
|
|
2042
|
-
throw new TypeError("origin must be an HTTPS origin");
|
|
2043
|
-
const source = new URL(configURL);
|
|
2044
|
-
if (source.protocol !== "https:" || source.username || source.password || source.hash)
|
|
2045
|
-
throw new TypeError("configURL must be an HTTPS URL without credentials or fragment");
|
|
2046
|
-
if (!/^[a-z][a-z0-9-]*$/i.test(requestHeader)) throw new TypeError("Invalid requestHeader");
|
|
2047
|
-
this.#origin = target.origin;
|
|
2048
|
-
this.#configURL = source.href;
|
|
2049
|
-
this.#requestHeader = requestHeader;
|
|
2050
|
-
this.#resolveSettings = resolveSettings;
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
async #loadConfig(module) {
|
|
2054
|
-
const response = await fetch({ url: this.#configURL, method: "GET", headers: { "Cache-Control": "no-cache" }, timeout: 5000 });
|
|
2055
|
-
if (response.status !== 200) throw new Error(`BoxJS source HTTP ${response.status}`);
|
|
2056
|
-
return normalizeBoxJs(JSON.parse(response.body), module);
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
/**
|
|
2060
|
-
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
2061
|
-
* @returns {Promise<import("./index.js").SettingsResponse | undefined>} API 响应,非本来源 API 则不处理 / API response, or undefined outside the configured API origin.
|
|
2062
|
-
*/
|
|
2063
|
-
async handle(request) {
|
|
2064
|
-
const url = new URL(request.url);
|
|
2065
|
-
if (url.origin !== this.#origin || !url.pathname.startsWith("/api/")) return;
|
|
2066
|
-
const headers = { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" };
|
|
2067
|
-
const reply = (status, data) => ({ status, headers, body: request.method === "HEAD" ? "" : JSON.stringify(data) });
|
|
2068
|
-
let parts;
|
|
2069
|
-
try {
|
|
2070
|
-
parts = parseSettingsPath(request.url);
|
|
2071
|
-
} catch (error) {
|
|
2072
|
-
return reply(400, { error: error.message });
|
|
2073
|
-
}
|
|
2074
|
-
const requestHeaders = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
2075
|
-
if (requestHeaders[this.#requestHeader.toLowerCase()] !== "1" || (requestHeaders.origin && requestHeaders.origin !== this.#origin))
|
|
2076
|
-
return reply(403, { error: "Forbidden settings client" });
|
|
2077
|
-
if (!["HEAD", "GET", "POST", "DELETE"].includes(request.method))
|
|
2078
|
-
return { ...reply(405, { error: "Method not allowed" }), headers: { ...headers, Allow: "HEAD, GET, POST, DELETE" } };
|
|
2079
|
-
let definition;
|
|
2080
|
-
try {
|
|
2081
|
-
definition = await this.#loadConfig(parts[0]);
|
|
2082
|
-
} catch (error) {
|
|
2083
|
-
return reply(502, { error: `Module configuration unavailable: ${error.message}` });
|
|
2084
|
-
}
|
|
2085
|
-
const key = parts.join(".");
|
|
2086
|
-
const field = definition.fields.find((field) => field.key === key);
|
|
2087
|
-
const descendants = definition.fields.filter((field) => field.key.startsWith(`${key}.`));
|
|
2088
|
-
if (!field && !descendants.length) return reply(404, { error: `Unknown setting path: ${key}` });
|
|
2089
|
-
if (request.method === "HEAD") return reply(200, undefined);
|
|
2090
|
-
if (request.method === "GET") {
|
|
2091
|
-
const stored = Storage.getItem(definition.storageKey, {});
|
|
2092
|
-
const effective = this.#resolveSettings ? this.#resolveSettings(stored, definition) : stored;
|
|
2093
|
-
if (!isRecord(effective)) throw new TypeError("resolved settings must be a synchronous object");
|
|
2094
|
-
if (field) {
|
|
2095
|
-
const value = pathValue(effective, parts);
|
|
2096
|
-
return value === undefined ? reply(404, { error: `Setting has no stored value: ${key}` }) : reply(200, value);
|
|
2097
|
-
}
|
|
2098
|
-
const subtree = {};
|
|
2099
|
-
// 只返回配置文件公开的字段;默认值由浏览器用 BoxJS val 生成。
|
|
2100
|
-
// Expose only declared fields; the browser renders defaults from BoxJS val.
|
|
2101
|
-
for (const descendant of descendants) {
|
|
2102
|
-
const fullPath = descendant.key.split(".");
|
|
2103
|
-
const value = pathValue(effective, fullPath);
|
|
2104
|
-
if (value !== undefined) Lodash.set(subtree, fullPath.slice(parts.length), value);
|
|
2105
|
-
}
|
|
2106
|
-
return reply(200, subtree);
|
|
2107
|
-
}
|
|
2108
|
-
if (!field) return reply(405, { error: "Only individual declared keys can be modified" });
|
|
2109
|
-
let value;
|
|
2110
|
-
if (request.method === "POST") {
|
|
2111
|
-
if (requestHeaders["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/json")
|
|
2112
|
-
return reply(415, { error: "Expected application/json" });
|
|
2113
|
-
if (typeof request.body !== "string") return reply(400, { error: "Expected a JSON string body" });
|
|
2114
|
-
if (request.body.length > 65536) return reply(413, { error: "Body exceeds 65536 UTF-16 code units" });
|
|
2115
|
-
try {
|
|
2116
|
-
value = JSON.parse(request.body);
|
|
2117
|
-
} catch {
|
|
2118
|
-
return reply(400, { error: "Invalid JSON" });
|
|
2119
|
-
}
|
|
2120
|
-
if (!validValue(field, value)) return reply(400, { error: `Invalid setting value: ${key}` });
|
|
2121
|
-
}
|
|
2122
|
-
const saved = Storage.getItem(definition.storageKey, {});
|
|
2123
|
-
if (!isRecord(saved)) throw new TypeError("stored settings must be an object");
|
|
2124
|
-
const parent = settingsParent(saved, parts, request.method === "POST");
|
|
2125
|
-
if (parent) {
|
|
2126
|
-
if (request.method === "DELETE") Lodash.unset(parent, [parts.at(-1)]);
|
|
2127
|
-
else Lodash.set(parent, [parts.at(-1)], value);
|
|
2128
|
-
}
|
|
2129
|
-
if (!Storage.setItem(definition.storageKey, saved)) return reply(500, { error: "Settings storage write failed" });
|
|
2130
|
-
return reply(200, request.method === "DELETE" ? { deleted: true } : { saved: true });
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
1715
|
function isRecord(value) {
|
|
2135
|
-
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
function pathValue(root, parts) {
|
|
2139
|
-
const parent = settingsParent(root, parts, false);
|
|
2140
|
-
return parent ? Lodash.get(parent, [parts.at(-1)]) : undefined;
|
|
1716
|
+
return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
2141
1717
|
}
|
|
2142
1718
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
1719
|
+
/**
|
|
1720
|
+
* 遍历父路径,兼容旧存储中 JSON 字符串形式的中间节点。
|
|
1721
|
+
* Traverse parents, supporting legacy intermediate nodes serialized as JSON strings.
|
|
1722
|
+
* @param {Record<string, unknown>} root 存储根 / Storage root.
|
|
1723
|
+
* @param {string[]} parts 完整路径 / Complete path.
|
|
1724
|
+
* @param {boolean} create 是否创建缺失节点 / Whether to create missing parents.
|
|
1725
|
+
* @returns {object | undefined} 父节点,缺失且不创建时为 undefined / Parent, or undefined when absent and not creating.
|
|
1726
|
+
* @throws {TypeError} 无法继续遍历标量节点 / A scalar node cannot be traversed.
|
|
1727
|
+
*/
|
|
1728
|
+
function storageParent(root, parts, create) {
|
|
1729
|
+
let parent = root;
|
|
1730
|
+
for (const part of parts.slice(0, -1)) {
|
|
1731
|
+
let next = Lodash.get(parent, [part]);
|
|
1732
|
+
switch (typeof next) {
|
|
1733
|
+
case "undefined":
|
|
1734
|
+
if (!create) return;
|
|
1735
|
+
next = {};
|
|
1736
|
+
break;
|
|
1737
|
+
case "string":
|
|
1738
|
+
next = JSON.parse(next);
|
|
1739
|
+
break;
|
|
1740
|
+
}
|
|
1741
|
+
if (!isRecord(next) && !Array.isArray(next)) throw new TypeError("Stored parent is not an object or array");
|
|
1742
|
+
Lodash.set(parent, [part], next);
|
|
1743
|
+
parent = next;
|
|
1744
|
+
}
|
|
1745
|
+
return parent;
|
|
2158
1746
|
}
|
|
2159
1747
|
|
|
1748
|
+
/**
|
|
1749
|
+
* 读取代理参数、执行处理器并将响应交给宿主 done。
|
|
1750
|
+
* Read proxy arguments, execute the handler and pass the response to the host's done function.
|
|
1751
|
+
* @returns {Promise<void>} 代理脚本执行结束 / Proxy script execution completion.
|
|
1752
|
+
*/
|
|
2160
1753
|
(async () => {
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
1754
|
+
let response;
|
|
1755
|
+
try {
|
|
1756
|
+
const { origin, storageKey, module } = qs.parse(globalThis.$argument);
|
|
1757
|
+
const handler = new SettingsHandler({ origin, storageKey, module });
|
|
1758
|
+
response = await handler.handle(globalThis.$request);
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
console.error(`PreferencePanes: ${error.message}`);
|
|
1761
|
+
response = {
|
|
1762
|
+
status: 500,
|
|
1763
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" },
|
|
1764
|
+
body: globalThis.$request.method === "HEAD" ? "" : JSON.stringify({ error: "Settings execution failed" }),
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
if (!response) {
|
|
1768
|
+
done({});
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
done($app === "Quantumult X" ? response : { response });
|
|
2175
1772
|
})();
|
|
2176
1773
|
|
|
2177
1774
|
})();
|