@nsnanocat/preference-panes 0.4.1 → 0.5.0

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.
@@ -1295,6 +1295,304 @@
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
+
1298
1596
  /**
1299
1597
  * 跨平台持久化存储适配器。
1300
1598
  * Cross-platform persistent storage adapter.
@@ -1746,15 +2044,63 @@
1746
2044
  }
1747
2045
 
1748
2046
  /**
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.
2047
+ * 统一处理存储 API 和无原生 Mock 平台的静态资源请求。
2048
+ * Handle storage APIs and static resources on hosts without native Mock support.
1752
2049
  */
1753
- (async () => {
2050
+ class PreferencesHandler extends SettingsHandler {
2051
+ #origin;
2052
+ #resources;
2053
+ /**
2054
+ * 根据安装 JSON 配置资源映射,不执行项目自定义代码。
2055
+ * Configure resource mappings from installation JSON without project-specific code.
2056
+ * @param {import("./index.js").PreferencesHandlerOptions} options 安装映射 / Installation mapping.
2057
+ */
2058
+ constructor(options) {
2059
+ super(options);
2060
+ this.#origin = new URL(options.origin).origin;
2061
+ this.#resources = options.resources.map(({ pattern, source, contentType }) => {
2062
+ const url = new URL(source);
2063
+ if (url.protocol !== "https:") throw new TypeError("Resource sources must use HTTPS");
2064
+ if (typeof contentType !== "string" || /[\r\n]/.test(contentType)) throw new TypeError("Invalid resource content type");
2065
+ return { pattern: new RegExp(pattern), source: url.href, contentType };
2066
+ });
2067
+ }
2068
+ /**
2069
+ * API 先交给存储桥接,只有资源路由才发出下载请求。
2070
+ * Dispatch APIs to storage first; only resource routes perform downloads.
2071
+ * @param {import("./index.js").SettingsRequest} request 宿主请求 / Host request.
2072
+ * @returns {Promise<import("./index.js").SettingsResponse | undefined>} 响应或非接管请求 / Response or unhandled request.
2073
+ */
2074
+ async handle(request) {
2075
+ const api = await super.handle(request);
2076
+ if (api) return api;
2077
+ const url = new URL(request.url);
2078
+ if (url.origin !== this.#origin) return;
2079
+ const resource = this.#resources.find(resource => resource.pattern.test(url.pathname));
2080
+ if (!resource) return;
2081
+ switch (request.method) {
2082
+ case "GET":
2083
+ case "HEAD": {
2084
+ const response = await fetch({ url: resource.source, method: "GET", headers: { "Cache-Control": "no-cache" }, timeout: 5000 });
2085
+ return { status: response.status, headers: { "Content-Type": `${resource.contentType}; charset=utf-8`, "Cache-Control": "no-store" }, body: request.method === "HEAD" ? "" : response.body };
2086
+ }
2087
+ default:
2088
+ return { status: 405, headers: { Allow: "HEAD, GET" }, body: "" };
2089
+ }
2090
+ }
2091
+ }
2092
+
2093
+ /**
2094
+ * 执行独立代理脚本;安装映射由托管站点生成,或由宿主参数提供。
2095
+ * Run a standalone proxy script with a site-generated installation mapping or host arguments.
2096
+ * @param {import("../index.js").PreferencesHandlerOptions} [options] 安装映射 / Installation mapping.
2097
+ * @returns {Promise<void>} 已交给宿主的响应 / Response delivered to the proxy host.
2098
+ */
2099
+ async function runPreferences(options) {
1754
2100
  let response;
1755
2101
  try {
1756
- const { origin, storageKey, module } = qs.parse(globalThis.$argument);
1757
- const handler = new SettingsHandler({ origin, storageKey, module });
2102
+ const config = options ?? qs.parse(globalThis.$argument);
2103
+ const handler = new PreferencesHandler({ ...config, resources: config.resources ?? [] });
1758
2104
  response = await handler.handle(globalThis.$request);
1759
2105
  } catch (error) {
1760
2106
  console.error(`PreferencePanes: ${error.message}`);
@@ -1769,6 +2115,10 @@
1769
2115
  return;
1770
2116
  }
1771
2117
  done($app === "Quantumult X" ? response : { response });
1772
- })();
2118
+ }
2119
+
2120
+ // 直接安装时读取宿主参数;站点生成的脚本使用同一执行入口并传入安装映射。
2121
+ // Direct installs read host arguments; site-generated scripts call the same entry with a mapping.
2122
+ runPreferences();
1773
2123
 
1774
2124
  })();
@@ -5,11 +5,11 @@
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
6
  <meta name="color-scheme" content="light dark">
7
7
  <title>Preferences</title>
8
- <link rel="stylesheet" href="/settings/assets/panel.css?v=0.4.1">
9
- <link rel="stylesheet" href="/settings/assets/home.css?v=0.4.1">
8
+ <link rel="stylesheet" href="/settings/assets/panel.css?v=0.5.0">
9
+ <link rel="stylesheet" href="/settings/assets/home.css?v=0.5.0">
10
10
  </head>
11
11
  <body>
12
12
  <main id="preferences"></main>
13
- <script type="module" src="/settings/assets/app.mjs?v=0.4.1"></script>
13
+ <script type="module" src="/settings/assets/app.mjs?v=0.5.0"></script>
14
14
  </body>
15
15
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsnanocat/preference-panes",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Shared settings API runtime for JavaScript proxy modules",
5
5
  "author": "VirgilClyne <Virgil@nanocat.me>",
6
6
  "homepage": "https://NSNanoCat.github.io/preference-panes",
@@ -29,6 +29,7 @@
29
29
  "./browser/panel.css": "./src/browser/panel.css",
30
30
  "./dist/preference-panes.mjs": "./dist/preference-panes.mjs",
31
31
  "./dist/preference-panes.request.js": "./dist/preference-panes.request.js",
32
+ "./dist/preference-panes.proxy.js": "./dist/preference-panes.proxy.js",
32
33
  "./dist/settings/*": "./dist/settings/*"
33
34
  },
34
35
  "types": "src/index.d.ts",
@@ -0,0 +1,31 @@
1
+ import { $app } from "@nsnanocat/util/lib/app.mjs";
2
+ import { done } from "@nsnanocat/util/lib/done.mjs";
3
+ import { qs } from "@nsnanocat/util/polyfill/qs.mjs";
4
+ import { PreferencesHandler } from "../PreferencesHandler.mjs";
5
+
6
+ /**
7
+ * 执行独立代理脚本;安装映射由托管站点生成,或由宿主参数提供。
8
+ * Run a standalone proxy script with a site-generated installation mapping or host arguments.
9
+ * @param {import("../index.js").PreferencesHandlerOptions} [options] 安装映射 / Installation mapping.
10
+ * @returns {Promise<void>} 已交给宿主的响应 / Response delivered to the proxy host.
11
+ */
12
+ export async function runPreferences(options) {
13
+ let response;
14
+ try {
15
+ const config = options ?? qs.parse(globalThis.$argument);
16
+ const handler = new PreferencesHandler({ ...config, resources: config.resources ?? [] });
17
+ response = await handler.handle(globalThis.$request);
18
+ } catch (error) {
19
+ console.error(`PreferencePanes: ${error.message}`);
20
+ response = {
21
+ status: 500,
22
+ headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" },
23
+ body: globalThis.$request.method === "HEAD" ? "" : JSON.stringify({ error: "Settings execution failed" }),
24
+ };
25
+ }
26
+ if (!response) {
27
+ done({});
28
+ return;
29
+ }
30
+ done($app === "Quantumult X" ? response : { response });
31
+ }
@@ -1,30 +1,5 @@
1
- import { $app } from "@nsnanocat/util/lib/app.mjs";
2
- import { done } from "@nsnanocat/util/lib/done.mjs";
3
- import { qs } from "@nsnanocat/util/polyfill/qs.mjs";
4
- import { SettingsHandler } from "../SettingsHandler.mjs";
1
+ import { runPreferences } from "./handler.mjs";
5
2
 
6
- /**
7
- * 读取代理参数、执行处理器并将响应交给宿主 done。
8
- * Read proxy arguments, execute the handler and pass the response to the host's done function.
9
- * @returns {Promise<void>} 代理脚本执行结束 / Proxy script execution completion.
10
- */
11
- (async () => {
12
- let response;
13
- try {
14
- const { origin, storageKey, module } = qs.parse(globalThis.$argument);
15
- const handler = new SettingsHandler({ origin, storageKey, module });
16
- response = await handler.handle(globalThis.$request);
17
- } catch (error) {
18
- console.error(`PreferencePanes: ${error.message}`);
19
- response = {
20
- status: 500,
21
- headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" },
22
- body: globalThis.$request.method === "HEAD" ? "" : JSON.stringify({ error: "Settings execution failed" }),
23
- };
24
- }
25
- if (!response) {
26
- done({});
27
- return;
28
- }
29
- done($app === "Quantumult X" ? response : { response });
30
- })();
3
+ // 直接安装时读取宿主参数;站点生成的脚本使用同一执行入口并传入安装映射。
4
+ // Direct installs read host arguments; site-generated scripts call the same entry with a mapping.
5
+ runPreferences();