dsh-enhancement 1.0.1 → 1.1.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.
package/README.en.md CHANGED
@@ -17,6 +17,7 @@ Set a password, open the gateway URL, and log in to DSH without carrying around
17
17
  | PWA | Install DSH on the desktop or phone home screen, with custom app name, theme color, and icons |
18
18
  | Desktop notifications | System notifications for task completion, questions, approvals, and background jobs |
19
19
  | Mobile push | PWA Web Push notifications on your phone; tap to return to the matching session |
20
+ | Session header | Send a DSH session-id request header on third-party API routes too, so a gateway can correlate requests by session |
20
21
 
21
22
  ## Password gateway
22
23
 
@@ -51,6 +52,29 @@ The certificate must cover the hostname or IP you actually use (SAN), otherwise
51
52
  - Chromium / Edge show an install prompt; iPhone / iPad use Safari's “Share → Add to Home Screen”.
52
53
  - Installed apps open without browser chrome; HTTPS is required for installation, Service Worker, and push.
53
54
 
55
+ ## Session header
56
+
57
+ DSH only sends `x-deepseek-harness-session-id` when talking to the official DeepSeek API; third-party routes through OpenAI-compatible gateways (`llm-pi-ai`) carry no session identifier at all. With this feature on, the plugin injects the current session id as a request header so your gateway can correlate requests by session.
58
+
59
+ - **Off by default**; the default header name matches the official DeepSeek one: `x-deepseek-harness-session-id`.
60
+ - The header name is configurable for gateways with their own convention; the value is always DSH's raw session id.
61
+ - Applies only to third-party API adapters (`llm-pi-ai`); official DeepSeek routes already send it and are untouched.
62
+ - Takes effect on the next model request after saving — no restart needed.
63
+ - An existing header of the same name set upstream is never overwritten.
64
+
65
+ > Implementation note: injection happens on the third-party adapter's `streamWithSnapshot` prototype method, because DSH rebuilds the pi-ai options inside it — the `llm/stream` layer cannot change request headers. The feature therefore depends on host internals; if the host shape changes, it bypasses itself and records the reason instead of blocking requests.
66
+
67
+ ### Why it says "not mounted yet"
68
+
69
+ `dsh-base` mounts `llm-pi-ai` in a **dormant** state: with no provider routes configured it never registers an adapter (`routes.length === 0` returns early), so there is nothing to inject into. That is a normal state, not a failure. The settings page distinguishes the causes:
70
+
71
+ | Message | Meaning | What to do |
72
+ | --- | --- | --- |
73
+ | Mounted on N third-party API adapters | Working | — |
74
+ | No third-party model routes registered | No OpenAI-compatible provider configured yet | Add one under Settings → Models, or declare `providers` for `llm-pi-ai` in this profile's `cordis.patch.yml` |
75
+ | N adapters registered, none is a third-party API adapter | Only official DeepSeek routes | Same as above: add a third-party provider |
76
+ | Host llm service unavailable or reshaped | Version drift | Plugin has bypassed itself; requests are unaffected |
77
+
54
78
  ## Notifications
55
79
 
56
80
  Supported events, all enabled by default and individually selectable:
package/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  | PWA | 把 DSH 添加到桌面/手机主屏,支持自定义应用名、主题色和图标 |
18
18
  | 桌面通知 | 任务完成、等待回答、等待审批、后台任务结束等事件弹出系统通知 |
19
19
  | 手机推送 | 通过 PWA Web Push 把通知推到手机,点击直接回到对应会话 |
20
+ | 会话请求头 | 让第三方 API 路由也携带 DSH 的 session id 请求头,便于上游网关按会话关联请求 |
20
21
 
21
22
  ## 独立密码网关
22
23
 
@@ -52,6 +53,29 @@ PWA 安装、Service Worker 和手机推送都要求 HTTPS(本机 `localhost`
52
53
  - 安装后从主屏启动,不显示浏览器地址栏,使用体验接近原生应用。
53
54
  - 只有 HTTPS 下才能安装、注册 Service Worker 和接收推送。
54
55
 
56
+ ## 会话请求头
57
+
58
+ DSH 只在直连官方 DeepSeek API 时发送 `x-deepseek-harness-session-id`;走 OpenAI 兼容网关(`llm-pi-ai`)的第三方路由不会携带任何会话标识。启用本功能后,插件会把当前会话的 session id 注入成请求头,让上游网关也能按会话关联请求。
59
+
60
+ - 默认**关闭**,默认头名与 DeepSeek 官方一致:`x-deepseek-harness-session-id`。
61
+ - 头名可改成上游网关约定的其它名称;值始终是 DSH 的原始 session id。
62
+ - 只作用于第三方 API adapter(`llm-pi-ai`);官方 DeepSeek 路由本来就带该头,不受影响。
63
+ - 保存后下一次模型请求即生效,不需要重启 DSH。
64
+ - 上游若已存在同名头,插件的注入不会覆盖它。
65
+
66
+ > 实现说明:注入点在第三方 adapter 的 `streamWithSnapshot` 原型方法上(DSH 在该方法内重建 pi-ai 的 options,`llm/stream` 层改不动请求头)。因此本功能依赖宿主内部结构,宿主版本结构变化时会自动旁路并记录原因,不会阻断请求。
67
+
68
+ ### 为什么显示「尚未挂载」
69
+
70
+ `dsh-base` 以**休眠态**挂载 `llm-pi-ai`:没有配置任何 provider 路由时它不会注册 adapter(`routes.length === 0` 直接返回),此时插件无处注入。这是正常状态,不是故障。设置页按原因区分:
71
+
72
+ | 提示 | 含义 | 处理 |
73
+ | --- | --- | --- |
74
+ | 已挂载到 N 个第三方 API adapter | 正常工作 | — |
75
+ | 当前没有已注册的第三方模型路由 | 尚未配置 OpenAI 兼容 provider | 到「设置 → 模型」添加,或在本 profile 的 `cordis.patch.yml` 写 `llm-pi-ai` 的 `providers` |
76
+ | 已注册 N 个 adapter,但没有一个是第三方 API adapter | 只有官方 DeepSeek 路由 | 同上:需要添加第三方 provider |
77
+ | 宿主 llm 服务不可用或结构已变化 | 版本漂移 | 插件已旁路,不影响正常请求 |
78
+
55
79
  ## 通知
56
80
 
57
81
  支持这些事件,默认全部开启,可以按需勾选:
package/lib/client.js CHANGED
@@ -223,6 +223,8 @@ window.__ModuleLoader__.load({
223
223
  const [pushCount, setPushCount] = (0, react_1.useState)(0);
224
224
  const [deviceSubscribed, setDeviceSubscribed] = (0, react_1.useState)(false);
225
225
  const [customIcons, setCustomIcons] = (0, react_1.useState)([]);
226
+ const [sessionHeaderEnabled, setSessionHeaderEnabled] = (0, react_1.useState)(false);
227
+ const [sessionHeaderName, setSessionHeaderName] = (0, react_1.useState)('x-deepseek-harness-session-id');
226
228
  const applyStatus = (0, react_1.useCallback)((next) => {
227
229
  setStatus(next);
228
230
  setEnabled(next.enabled);
@@ -252,6 +254,8 @@ window.__ModuleLoader__.load({
252
254
  setPushPublicKey(next.push?.publicKey ?? null);
253
255
  setPushCount(next.push?.subscriptions ?? 0);
254
256
  setCustomIcons(next.pwa?.customIcons ?? []);
257
+ setSessionHeaderEnabled(next.sessionHeader?.enabled ?? false);
258
+ setSessionHeaderName(next.sessionHeader?.headerName ?? 'x-deepseek-harness-session-id');
255
259
  setHttpsChoice(next.https?.mode === 'uploaded' ? 'uploaded' : next.https?.mode === 'off' ? 'off' : 'selfsigned');
256
260
  setCaSource(next.https?.mode === 'ca' ? 'uploaded' : 'generated');
257
261
  }, []);
@@ -323,6 +327,13 @@ window.__ModuleLoader__.load({
323
327
  const next = await api('/config', { method: 'POST', body: JSON.stringify(fields) });
324
328
  applyStatus(next);
325
329
  }, success);
330
+ const saveSessionHeader = () => runBlock('sessionHeader', async () => {
331
+ const next = await api('/config', {
332
+ method: 'POST',
333
+ body: JSON.stringify({ sessionHeaderEnabled, sessionHeaderName }),
334
+ });
335
+ applyStatus(next);
336
+ }, '会话请求头配置已保存,下一次模型请求即生效。');
326
337
  const saveGateway = () => runBlock('gateway', async () => {
327
338
  let next = await api('/config', {
328
339
  method: 'POST',
@@ -586,8 +597,29 @@ window.__ModuleLoader__.load({
586
597
  notifyJobs !== status.notify.jobs ||
587
598
  pushEnabled !== status.push.enabled ||
588
599
  pushSubject !== status.push.subject;
600
+ const sessionHeaderDirty = sessionHeaderEnabled !== status.sessionHeader.enabled ||
601
+ sessionHeaderName !== status.sessionHeader.headerName;
589
602
  const accessDirty = newPassword.length > 0 || confirmPassword.length > 0;
590
603
  const iconUrl = (kind) => `/pwa/enhancement/${kind}?v=${iconVersion}`;
604
+ /**
605
+ * 会话请求头的挂载说明。
606
+ * 「没配第三方模型」是正常状态而非故障,必须与真正的结构异常区分开。
607
+ */
608
+ const sessionHeaderSummary = (errorStyle = false) => {
609
+ const header = status.sessionHeader;
610
+ if (header.patched > 0) {
611
+ return `已挂载到 ${header.patched} 个第三方 API adapter;改动即时生效,无需重启。`;
612
+ }
613
+ if (header.reason === 'no-adapters') {
614
+ return '当前没有已注册的第三方模型路由,因此暂时无处注入。在「设置 → 模型」添加一个 OpenAI 兼容 provider(或在本 profile 的 cordis.patch.yml 配置 llm-pi-ai 路由)后会自动挂载;官方 DeepSeek 路由本来就自带该头,不需要它。';
615
+ }
616
+ if (header.reason === 'no-pi-ai-adapter') {
617
+ return `已注册 ${header.adapterCount} 个 adapter,但没有一个是第三方 API adapter(llm-pi-ai),因此未挂载。`;
618
+ }
619
+ if (errorStyle)
620
+ return '宿主 llm 服务不可用或结构已变化,注入已旁路,不会影响正常请求。';
621
+ return '宿主 llm 服务不可用或结构已变化,注入暂时旁路。';
622
+ };
591
623
  return ((0, jsx_runtime_1.jsxs)("div", { className: "enh-root", style: styles.wrap, children: [(0, jsx_runtime_1.jsx)("style", { children: CARD_CSS }), (0, jsx_runtime_1.jsxs)("div", { style: styles.header, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { style: styles.title, children: "\u63D2\u4EF6\u72B6\u6001" }), (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "DSH \u589E\u5F3A\u7684\u7F51\u5173\u3001PWA\u3001\u901A\u77E5\u4E0E\u63A8\u9001\u914D\u7F6E\u3002" })] }), (0, jsx_runtime_1.jsxs)("span", { style: styles.pill, children: [(0, jsx_runtime_1.jsx)("span", { style: styles.dot }), "\u8FD0\u884C\u4E2D"] })] }), error ? (0, jsx_runtime_1.jsx)("div", { style: styles.error, children: error }) : null, notice ? (0, jsx_runtime_1.jsx)("div", { style: styles.success, children: notice }) : null, (0, jsx_runtime_1.jsxs)(Block, { order: 2, title: "\u7F51\u5173\u914D\u7F6E", hint: "\u5BC6\u7801\u7F51\u5173\u7684\u76D1\u542C\u3001\u8BBF\u95EE\u4E0E\u4F1A\u8BDD\u8BBE\u7F6E\uFF1BHTTPS \u914D\u7F6E\u4E5F\u5728\u672C\u5757\u5185\uFF0C\u4FDD\u5B58\u65F6\u6309\u987A\u5E8F\u5E94\u7528\u3002", dirty: gatewayDirty, busy: busy, saveLabel: "\u4FDD\u5B58\u5E76\u5E94\u7528", onSave: saveGateway, status: blockStatus.gateway, extra: (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy || !status.passwordSet, onClick: restart, children: "\u91CD\u542F\u7F51\u5173" }), children: [(0, jsx_runtime_1.jsxs)(InlineRow, { label: "DSH \u4E0A\u6E38", children: [(0, jsx_runtime_1.jsx)("code", { style: { ...styles.code, flex: '1 1 260px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: status.dshAccessUrl || status.dshOrigin }), (0, jsx_runtime_1.jsx)("button", { style: styles.iconButton, title: "\u590D\u5236", "aria-label": "\u590D\u5236 DSH \u4E0A\u6E38\u5730\u5740\uFF08\u542B token\uFF09", onClick: () => copyToClipboard(status.dshAccessUrl || status.dshOrigin), children: (0, jsx_runtime_1.jsx)(CopyIcon, {}) }), (0, jsx_runtime_1.jsx)("a", { style: styles.iconButton, title: "\u6253\u5F00", "aria-label": "\u6253\u5F00 DSH \u4E0A\u6E38\uFF08\u542B token\uFF09", href: status.dshAccessUrl || status.dshOrigin, target: "_blank", rel: "noreferrer", children: (0, jsx_runtime_1.jsx)(OpenIcon, {}) })] }), (0, jsx_runtime_1.jsx)(InlineRow, { label: "\u7F51\u5173\u5730\u5740", children: status.url ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("code", { style: { ...styles.code, flex: '1 1 260px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: status.url }), (0, jsx_runtime_1.jsx)("button", { style: styles.iconButton, title: "\u590D\u5236", "aria-label": "\u590D\u5236\u7F51\u5173\u5730\u5740", onClick: () => copyToClipboard(status.url), children: (0, jsx_runtime_1.jsx)(CopyIcon, {}) }), (0, jsx_runtime_1.jsx)("a", { style: styles.iconButton, title: "\u6253\u5F00", "aria-label": "\u6253\u5F00\u7F51\u5173", href: status.url, target: "_blank", rel: "noreferrer", children: (0, jsx_runtime_1.jsx)(OpenIcon, {}) })] })) : (0, jsx_runtime_1.jsx)("span", { style: styles.muted, children: "\u672A\u8FD0\u884C\uFF1B\u8BBE\u7F6E\u5BC6\u7801\u5E76\u542F\u7528\u540E\u751F\u6210" }) }), !status.dshSecretReady ? (0, jsx_runtime_1.jsx)("div", { style: styles.error, children: "\u7F51\u5173\u4F1A\u8BDD\u6CE8\u5165\u4E0D\u53EF\u7528\uFF1A\u65E0\u6CD5\u8BFB\u53D6 DSH \u6D4F\u89C8\u5668\u7B7E\u540D\u5BC6\u94A5\uFF0C\u7F51\u5173\u4EE3\u7406\u4F1A\u8FD4\u56DE 401\u3002" }) : null, status.error ? (0, jsx_runtime_1.jsxs)("div", { style: styles.error, children: ["\u6700\u8FD1\u9519\u8BEF\uFF1A", status.error] }) : null, (0, jsx_runtime_1.jsx)(Row, { label: "\u542F\u7528\u7F51\u5173", children: (0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 8 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: enabled, disabled: !status.passwordSet, onChange: (event) => setEnabled(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: !status.passwordSet ? '请先设置密码' : enabled ? '启用' : '停用' })] }) }), enabled ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "enh-grid3", children: [(0, jsx_runtime_1.jsxs)(Row, { label: "\u76D1\u542C\u5730\u5740", children: [(0, jsx_runtime_1.jsx)("select", { style: styles.select, value: bindHost, onChange: (event) => setBindHost(event.target.value), children: interfaceOptions.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option.value, children: option.label }, option.value))) }), currentInterface ? (0, jsx_runtime_1.jsxs)("div", { style: styles.muted, children: ["\u5F53\u524D\uFF1A", currentInterface.address, "\uFF08", currentInterface.name, "\uFF09"] }) : null] }), (0, jsx_runtime_1.jsx)(Row, { label: "\u7AEF\u53E3", children: (0, jsx_runtime_1.jsx)("input", { style: styles.input, value: port, inputMode: "numeric", onChange: (event) => setPort(event.target.value) }) }), (0, jsx_runtime_1.jsxs)(Row, { label: "\u4F1A\u8BDD\u6709\u6548\u671F", children: [(0, jsx_runtime_1.jsx)("input", { style: styles.input, value: sessionDays, inputMode: "numeric", onChange: (event) => setSessionDays(event.target.value) }), (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5929\uFF1B\u5BC6\u7801\u4FEE\u6539\u540E\u5168\u90E8\u4F1A\u8BDD\u5931\u6548\u3002" })] })] }), (0, jsx_runtime_1.jsx)(Row, { label: "\u767B\u5F55\u5931\u8D25\u8282\u6D41", children: (0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 8 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: loginThrottle, onChange: (event) => setLoginThrottle(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: loginThrottle ? '开启(5 次 / 10 分钟 → 锁 1 分钟)' : '关闭(不推荐)' })] }) }), (0, jsx_runtime_1.jsx)(Row, { label: "HTTPS \u6A21\u5F0F", children: (0, jsx_runtime_1.jsxs)("select", { style: styles.select, value: httpsChoice, onChange: (event) => setHttpsChoice(event.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "off", children: "\u5173\u95ED HTTPS" }), (0, jsx_runtime_1.jsx)("option", { value: "selfsigned", children: "\u4F7F\u7528 CA \u8BC1\u4E66\u81EA\u7B7E\u53D1" }), (0, jsx_runtime_1.jsx)("option", { value: "uploaded", children: "\u4F7F\u7528\u5DF2\u6709\u8BC1\u4E66" })] }) }), httpsChoice === 'off' ? ((0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5173\u95ED\u540E\u6D4F\u89C8\u5668\u4F1A\u63D0\u793A\u4E0D\u5B89\u5168\uFF0CPWA \u5B89\u88C5\u3001Service Worker \u4E0E Web Push \u90FD\u4E0D\u53EF\u7528\u3002\u4FDD\u5B58\u540E\u751F\u6548\u3002" })) : null, httpsChoice === 'selfsigned' ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(Row, { label: "CA \u6765\u6E90", children: (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 16 }, children: [(0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 6 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: "ca-source", checked: caSource === 'generated', onChange: () => setCaSource('generated') }), (0, jsx_runtime_1.jsx)("span", { children: "\u81EA\u52A8\u751F\u6210 CA" })] }), (0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 6 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: "ca-source", checked: caSource === 'uploaded', onChange: () => setCaSource('uploaded') }), (0, jsx_runtime_1.jsx)("span", { children: "\u4F7F\u7528\u5DF2\u6709 CA" })] })] }) }), caSource === 'uploaded' ? ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)(Row, { label: "\u5F53\u524D CA", children: [status.https.uploadedCaInfo ? ((0, jsx_runtime_1.jsxs)("div", { style: styles.muted, children: [status.https.uploadedCaInfo.subject, "\uFF0C\u6709\u6548\u671F\u81F3 ", status.https.uploadedCaInfo.validTo, "\uFF1B\u6587\u4EF6\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u76F4\u63A5\u4F7F\u7528\uFF0C\u65E0\u9700\u91CD\u65B0\u4E0A\u4F20\u3002"] })) : status.https.caInfo ? ((0, jsx_runtime_1.jsxs)("div", { style: styles.muted, children: [status.https.caInfo.subject, "\uFF0C\u6709\u6548\u671F\u81F3 ", status.https.caInfo.validTo, "\uFF1B\u6587\u4EF6\u4FDD\u5B58\u5728\u672C\u673A TLS \u76EE\u5F55\uFF0C\u53EF\u7EE7\u7EED\u7B7E\u53D1 leaf\u3002"] })) : (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5C1A\u672A\u4E0A\u4F20 CA\u3002" }), (0, jsx_runtime_1.jsx)("input", { id: "enh-ca-cert-file", type: "file", accept: ".pem,.crt,.cer", style: { display: 'none' }, onChange: (event) => setCaCertFile(event.target.files?.[0] ?? null) }), (0, jsx_runtime_1.jsx)("input", { id: "enh-ca-key-file", type: "file", accept: ".pem,.key", style: { display: 'none' }, onChange: (event) => setCaKeyFile(event.target.files?.[0] ?? null) }), (0, jsx_runtime_1.jsxs)("div", { style: styles.actions, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "enh-ca-cert-file", style: { ...styles.button, display: 'inline-flex', alignItems: 'center' }, children: caCertFile ? `已选择:${caCertFile.name}` : status.https.uploadedCaInfo ? '更换 CA 证书' : '选择 CA 证书' }), (0, jsx_runtime_1.jsx)("label", { htmlFor: "enh-ca-key-file", style: { ...styles.button, display: 'inline-flex', alignItems: 'center' }, children: caKeyFile ? `已选择:${caKeyFile.name}` : status.https.uploadedCaInfo ? '更换 CA 私钥' : '选择 CA 私钥' })] }), caCertFile || caKeyFile ? (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u4FDD\u5B58\u65F6\u624D\u4F1A\u5E94\u7528\uFF1B\u53EA\u4FDD\u7559\u6700\u65B0\u4E00\u4EFD\u6587\u4EF6\u3002" }) : null] }) })) : ((0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u4FDD\u5B58\u65F6\u4F1A\u5728\u63D2\u4EF6 TLS \u76EE\u5F55\u751F\u6210\u672C\u5730 CA\uFF0C\u5E76\u7528\u5B83\u7B7E\u53D1\u5F53\u524D\u76D1\u542C\u5730\u5740 + \u989D\u5916\u4E3B\u673A\u540D\u7684\u8BC1\u4E66\u3002" })), (0, jsx_runtime_1.jsxs)(Row, { label: "\u989D\u5916\u4E3B\u673A\u540D", children: [(0, jsx_runtime_1.jsx)("input", { style: styles.input, placeholder: "\u4F8B\u5982 dsh.lan, myhost.local\uFF08\u9017\u53F7\u5206\u9694\uFF09", value: httpsHostnames, onChange: (event) => setHttpsHostnames(event.target.value) }), (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u76D1\u542C IP\u3001localhost \u4F1A\u81EA\u52A8\u5305\u542B\uFF1B\u8FD9\u91CC\u8865\u5145 DNS \u4E3B\u673A\u540D\u3002" })] }), status.https.caAvailable ? ((0, jsx_runtime_1.jsx)("div", { style: styles.actions, children: (0, jsx_runtime_1.jsx)("a", { style: { ...styles.button, textDecoration: 'none', display: 'inline-flex', alignItems: 'center' }, href: `${API}/tls/ca`, download: "dsh-enhancement-ca.crt", children: "\u4E0B\u8F7D CA \u8BC1\u4E66" }) })) : null, status.https.mode !== 'off' && status.https.cert ? ((0, jsx_runtime_1.jsxs)("div", { style: styles.muted, children: ["\u5F53\u524D\u8BC1\u4E66\uFF1A", status.https.cert.subject, "\uFF0C\u6709\u6548\u671F\u81F3 ", status.https.cert.validTo] })) : null, (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "iOS \u5B89\u88C5 CA\uFF1ASafari \u6253\u5F00\u4E0B\u8F7D\u6587\u4EF6 \u2192 \u8BBE\u7F6E \u2192 \u901A\u7528 \u2192 VPN \u4E0E\u8BBE\u5907\u7BA1\u7406 \u2192 \u5B89\u88C5\u63CF\u8FF0\u6587\u4EF6 \u2192 \u8BBE\u7F6E \u2192 \u901A\u7528 \u2192 \u5173\u4E8E\u672C\u673A \u2192 \u8BC1\u4E66\u4FE1\u4EFB\u8BBE\u7F6E \u2192 \u5BF9 \u201CDSH Enhancement Local CA\u201D \u5F00\u542F\u5B8C\u5168\u4FE1\u4EFB\u3002" })] })) : null, httpsChoice === 'uploaded' ? ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsxs)(Row, { label: "\u5F53\u524D\u8BC1\u4E66", children: [status.https.cert && status.https.mode === 'uploaded' ? ((0, jsx_runtime_1.jsxs)("div", { style: styles.muted, children: [status.https.cert.subject, "\uFF0C\u6709\u6548\u671F\u81F3 ", status.https.cert.validTo, "\uFF1BSAN\uFF1A", status.https.cert.subjectAltName || '未提供', "\u3002\u8BC1\u4E66\u548C\u79C1\u94A5\u4FDD\u5B58\u5728\u672C\u673A TLS \u76EE\u5F55\u3002"] })) : (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5C1A\u672A\u4E0A\u4F20\u8BC1\u4E66\u3002" }), (0, jsx_runtime_1.jsx)("input", { id: "enh-cert-file", type: "file", accept: ".pem,.crt,.cer", style: { display: 'none' }, onChange: (event) => setCertFile(event.target.files?.[0] ?? null) }), (0, jsx_runtime_1.jsx)("input", { id: "enh-key-file", type: "file", accept: ".pem,.key", style: { display: 'none' }, onChange: (event) => setKeyFile(event.target.files?.[0] ?? null) }), (0, jsx_runtime_1.jsxs)("div", { style: styles.actions, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "enh-cert-file", style: { ...styles.button, display: 'inline-flex', alignItems: 'center' }, children: certFile ? `已选择:${certFile.name}` : status.https.mode === 'uploaded' ? '更换证书' : '选择证书' }), (0, jsx_runtime_1.jsx)("label", { htmlFor: "enh-key-file", style: { ...styles.button, display: 'inline-flex', alignItems: 'center' }, children: keyFile ? `已选择:${keyFile.name}` : status.https.mode === 'uploaded' ? '更换私钥' : '选择私钥' })] }), certFile || keyFile ? (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u4FDD\u5B58\u65F6\u624D\u4F1A\u5E94\u7528\uFF1B\u8BC1\u4E66 SAN \u5FC5\u987B\u8986\u76D6\u8BBF\u95EE\u5730\u5740/IP\uFF0C\u53EA\u4FDD\u7559\u6700\u65B0\u4E00\u4EFD\u6587\u4EF6\u3002" }) : null] }) })) : null] })) : (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u542F\u7528\u7F51\u5173\u540E\u663E\u793A\u76D1\u542C\u5730\u5740\u3001\u7AEF\u53E3\u3001\u4F1A\u8BDD\u548C HTTPS \u8BBE\u7F6E\u3002" })] }), (0, jsx_runtime_1.jsxs)(Block, { order: 1, title: "\u8BBF\u95EE\u63A7\u5236", hint: status.passwordSet ? '已设置访问密码;修改后全部网关会话失效。' : '尚未设置密码;设置后才能启用网关。', dirty: accessDirty, busy: busy, saveLabel: "\u8BBE\u7F6E\u5BC6\u7801", onSave: savePassword, status: blockStatus.access, extra: (0, jsx_runtime_1.jsx)("button", { style: { ...styles.button, ...styles.danger }, disabled: busy || !status.passwordSet, onClick: logoutAll, children: "\u9000\u51FA\u5168\u90E8\u7F51\u5173\u4F1A\u8BDD" }), children: [(0, jsx_runtime_1.jsxs)("div", { className: "enh-grid2", children: [(0, jsx_runtime_1.jsx)(Row, { label: "\u65B0\u5BC6\u7801", children: (0, jsx_runtime_1.jsx)(PasswordInput, { value: newPassword, onChange: setNewPassword, autoComplete: "new-password", show: showNewPassword, onToggle: () => setShowNewPassword((value) => !value) }) }), (0, jsx_runtime_1.jsx)(Row, { label: "\u786E\u8BA4\u65B0\u5BC6\u7801", children: (0, jsx_runtime_1.jsx)(PasswordInput, { value: confirmPassword, onChange: setConfirmPassword, autoComplete: "new-password", show: showConfirmPassword, onToggle: () => setShowConfirmPassword((value) => !value) }) })] }), newPassword.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { style: { ...styles.error, marginTop: -4 }, children: newPassword.length < 6 ? '新密码至少 6 个字符' : confirmPassword.length > 0 && newPassword !== confirmPassword ? '两次输入的新密码不一致' : '' })) : null, (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u81F3\u5C11 6 \u4E2A\u5B57\u7B26\uFF1B\u5BC6\u7801\u7B49\u4EF7\u4E8E\u672C\u673A DSH \u7684\u8BBF\u95EE\u6743\u9650\uFF0C\u8BF7\u4F7F\u7528\u5F3A\u5BC6\u7801\u3002" })] }), (0, jsx_runtime_1.jsxs)(Block, { order: 3, title: "PWA\uFF08\u6DFB\u52A0\u5230\u684C\u9762 / \u4E3B\u5C4F\uFF09", hint: "\u542F\u7528\u540E\u63A5\u7BA1 /manifest.webmanifest\u3001\u56FE\u6807\u3001/sw.js \u4E0E\u5B89\u88C5\u63D0\u793A\uFF1BHTTP \u4E0B\u5B89\u88C5\u5165\u53E3\u548C Service Worker \u4E0D\u751F\u6548\u3002", dirty: pwaDirty, busy: busy, saveLabel: "\u4FDD\u5B58 PWA \u914D\u7F6E", onSave: savePwa, status: blockStatus.pwa, extra: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("a", { style: { ...styles.button, textDecoration: 'none', display: 'inline-flex', alignItems: 'center' }, href: "/manifest.webmanifest", target: "_blank", rel: "noreferrer", children: "\u67E5\u770B manifest" }), (0, jsx_runtime_1.jsx)("a", { style: { ...styles.button, textDecoration: 'none', display: 'inline-flex', alignItems: 'center' }, href: "/sw.js", target: "_blank", rel: "noreferrer", children: "\u67E5\u770B Service Worker" })] }), children: [(0, jsx_runtime_1.jsx)(Row, { label: "\u542F\u7528 PWA", children: (0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 8 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: pwaEnabled, onChange: (event) => setPwaEnabled(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: pwaEnabled ? '启用' : '停用' })] }) }), status.https.mode === 'off' ? (0, jsx_runtime_1.jsx)("div", { style: styles.error, children: "\u5F53\u524D\u662F HTTP\uFF1A\u5B89\u88C5\u63D0\u793A\u3001Service Worker\u3001Web Push \u90FD\u4E0D\u4F1A\u751F\u6548\u3002\u8BF7\u5148\u5728\u7F51\u5173\u914D\u7F6E\u542F\u7528 HTTPS\u3002" }) : null, status.pwa.error ? (0, jsx_runtime_1.jsxs)("div", { style: styles.error, children: ["PWA \u6CE8\u518C\u5931\u8D25\uFF1A", status.pwa.error] }) : null, pwaEnabled ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "enh-grid2", children: [(0, jsx_runtime_1.jsx)(Row, { label: "\u5E94\u7528\u540D", children: (0, jsx_runtime_1.jsx)("input", { style: styles.input, value: pwaName, onChange: (event) => setPwaName(event.target.value) }) }), (0, jsx_runtime_1.jsx)(Row, { label: "\u77ED\u540D\u79F0", children: (0, jsx_runtime_1.jsx)("input", { style: styles.input, value: pwaShortName, onChange: (event) => setPwaShortName(event.target.value) }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "enh-grid3", children: [(0, jsx_runtime_1.jsx)(Row, { label: "\u663E\u793A\u6A21\u5F0F", children: (0, jsx_runtime_1.jsxs)("select", { style: styles.select, value: pwaDisplay, onChange: (event) => setPwaDisplay(event.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "standalone", children: "standalone" }), (0, jsx_runtime_1.jsx)("option", { value: "fullscreen", children: "fullscreen" }), (0, jsx_runtime_1.jsx)("option", { value: "minimal-ui", children: "minimal-ui" })] }) }), (0, jsx_runtime_1.jsx)(Row, { label: "\u4E3B\u9898\u8272", children: (0, jsx_runtime_1.jsx)("input", { style: styles.input, value: pwaThemeColor, onChange: (event) => setPwaThemeColor(event.target.value) }) }), (0, jsx_runtime_1.jsx)(Row, { label: "\u80CC\u666F\u8272", children: (0, jsx_runtime_1.jsx)("input", { style: styles.input, value: pwaBackgroundColor, onChange: (event) => setPwaBackgroundColor(event.target.value) }) })] }), (0, jsx_runtime_1.jsx)("div", { style: { ...styles.label, marginTop: 8 }, children: "\u5206\u5E73\u53F0\u56FE\u6807" }), (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u4E0A\u4F20\u540E\u7ACB\u5373\u751F\u6548\uFF1BAndroid \u81EA\u9002\u5E94\u56FE\u6807\u8BF7\u5355\u72EC\u4E0A\u4F20 maskable \u56FE\u3002\u5EFA\u8BAE 512 \u56FE\u6807\u5185\u5BB9\u7559\u5728\u4E2D\u5FC3 80% \u5B89\u5168\u533A\u3002" }), (0, jsx_runtime_1.jsx)("div", { className: "enh-icon-grid", children: [
592
624
  ['icon-192.png', '通用 192 × 192', 'any'],
593
625
  ['icon-512.png', '通用 512 × 512', 'any'],
@@ -598,7 +630,7 @@ window.__ModuleLoader__.load({
598
630
  event.target.value = '';
599
631
  if (file)
600
632
  void uploadIcon(kind, file);
601
- } }), customIcons.includes(kind) ? (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy, onClick: () => resetIcon(kind), children: "\u6062\u590D\u9ED8\u8BA4" }) : null] })] })] }, kind))) }), (0, jsx_runtime_1.jsx)("div", { style: styles.actions, children: (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy || customIcons.length === 0, onClick: () => resetIcon(), children: "\u5168\u90E8\u6062\u590D\u9ED8\u8BA4" }) })] })) : (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u542F\u7528 PWA \u540E\u624D\u663E\u793A\u5E94\u7528\u540D\u3001\u989C\u8272\u4E0E\u56FE\u6807\u8BBE\u7F6E\u3002" })] }), (0, jsx_runtime_1.jsxs)(Block, { order: 4, title: "\u901A\u77E5", hint: "\u4E8B\u4EF6\u52FE\u9009\u540C\u65F6\u63A7\u5236\u684C\u9762\u901A\u77E5\u548C PWA \u63A8\u9001\uFF1B\u4E24\u4E2A\u6E20\u9053\u53EF\u4EE5\u72EC\u7ACB\u5F00\u5173\uFF0C\u5173\u95ED\u67D0\u4E2A\u6E20\u9053\u4E0D\u4F1A\u4E22\u4E8B\u4EF6\u9009\u62E9\u3002", dirty: notifyDirty, busy: busy, saveLabel: "\u4FDD\u5B58\u901A\u77E5\u914D\u7F6E", onSave: saveNotify, status: blockStatus.notify, extra: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy, onClick: testNotify, children: "\u6D4B\u8BD5\u684C\u9762\u901A\u77E5" }), pushUsable ? (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy || pushCount === 0, onClick: testPush, children: "\u6D4B\u8BD5 PWA \u63A8\u9001" }) : null] }), children: [(0, jsx_runtime_1.jsx)(Row, { label: "\u901A\u77E5\u4E8B\u4EF6", children: (0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '8px 18px' }, children: [['notifyTask', '任务完成', notifyTask, setNotifyTask], ['notifyAsk', '等待回答', notifyAsk, setNotifyAsk], ['notifyApproval', '等待审批', notifyApproval, setNotifyApproval], ['notifyDenied', '审批被拒绝', notifyDenied, setNotifyDenied], ['notifySubagent', '子任务结束', notifySubagent, setNotifySubagent], ['notifyGoal', '目标完成/受阻', notifyGoal, setNotifyGoal], ['notifyJobs', '后台任务结束', notifyJobs, setNotifyJobs]].map(([key, label, value, setter]) => ((0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap' }, children: [(0, jsx_runtime_1.jsx)("input", { className: "enh-event-check", type: "checkbox", checked: value, onChange: (event) => setter(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: label })] }, key))) }) }), (0, jsx_runtime_1.jsx)(Row, { label: "\u684C\u9762\u901A\u77E5", children: (0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 8 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: notifyEnabled, onChange: (event) => setNotifyEnabled(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: notifyEnabled ? '启用' : '停用' })] }) }), (0, jsx_runtime_1.jsxs)(Row, { label: "PWA \u63A8\u9001", children: [(0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 8, opacity: pushUsable ? 1 : 0.5 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: pushUsable && pushEnabled, disabled: !pushUsable, onChange: (event) => setPushEnabled(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: pushUsable ? (pushEnabled ? '启用' : '停用') : '需要 HTTPS' })] }), !pushUsable ? (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5F53\u524D\u9875\u9762\u4E0D\u662F HTTPS/\u5B89\u5168\u4E0A\u4E0B\u6587\uFF0CPWA \u63A8\u9001\u5F00\u5173\u7F6E\u7070\uFF1B\u8BF7\u5148\u542F\u7528 HTTPS\u3002" }) : null] }), pushUsable && pushEnabled ? ((0, jsx_runtime_1.jsxs)("div", { style: { paddingLeft: 12, borderLeft: '2px solid var(--dsw-alias-border-l4, rgba(0,0,0,.08))' }, children: [(0, jsx_runtime_1.jsxs)("div", { style: styles.muted, children: ["\u901A\u77E5\u6743\u9650\uFF1A", typeof Notification !== 'undefined' ? Notification.permission : '不可用', "\uFF1B\u5DF2\u8BA2\u9605\u8BBE\u5907\uFF1A", pushCount, "\u3002"] }), (0, jsx_runtime_1.jsx)("div", { style: styles.actions, children: (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy, onClick: deviceSubscribed ? disablePush : enablePush, children: deviceSubscribed ? '取消本设备订阅' : '订阅本设备' }) }), (0, jsx_runtime_1.jsxs)("details", { className: "enh-advanced", children: [(0, jsx_runtime_1.jsx)("summary", { children: "\u9AD8\u7EA7\uFF1AVAPID \u8054\u7CFB\u5730\u5740" }), (0, jsx_runtime_1.jsxs)(Row, { label: "VAPID subject", children: [(0, jsx_runtime_1.jsx)("input", { style: styles.input, value: pushSubject, onChange: (event) => setPushSubject(event.target.value), placeholder: "mailto:dsh-enhancement@localhost" }), (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "Web Push \u534F\u8BAE\u8981\u6C42\u7684\u8054\u7CFB\u65B9\u5F0F\uFF0C\u4E0D\u662F\u767B\u5F55\u90AE\u7BB1\uFF0C\u4E5F\u4E0D\u9700\u8981\u9A8C\u8BC1\uFF1B\u4E2A\u4EBA\u4F7F\u7528\u4FDD\u7559\u9ED8\u8BA4\u5373\u53EF\u3002" })] })] })] })) : pushUsable ? (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5F00\u542F PWA \u63A8\u9001\u540E\u663E\u793A\u8BA2\u9605\u3001\u6D4B\u8BD5\u548C VAPID \u8BBE\u7F6E\u3002" }) : null] }), (0, jsx_runtime_1.jsxs)("div", { style: { ...styles.card, order: 5 }, children: [(0, jsx_runtime_1.jsx)("div", { style: styles.label, children: "\u72B6\u6001\u6587\u4EF6" }), (0, jsx_runtime_1.jsx)("div", { style: styles.code, children: status.statePath }), (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5BC6\u7801\u53EA\u5B58 scrypt \u54C8\u5E0C\uFF0CDSH \u7B7E\u540D\u5BC6\u94A5\u4E0D\u4F1A\u5199\u5165\u8FD9\u91CC\u3002" })] })] }));
633
+ } }), customIcons.includes(kind) ? (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy, onClick: () => resetIcon(kind), children: "\u6062\u590D\u9ED8\u8BA4" }) : null] })] })] }, kind))) }), (0, jsx_runtime_1.jsx)("div", { style: styles.actions, children: (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy || customIcons.length === 0, onClick: () => resetIcon(), children: "\u5168\u90E8\u6062\u590D\u9ED8\u8BA4" }) })] })) : (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u542F\u7528 PWA \u540E\u624D\u663E\u793A\u5E94\u7528\u540D\u3001\u989C\u8272\u4E0E\u56FE\u6807\u8BBE\u7F6E\u3002" })] }), (0, jsx_runtime_1.jsxs)(Block, { order: 4, title: "\u901A\u77E5", hint: "\u4E8B\u4EF6\u52FE\u9009\u540C\u65F6\u63A7\u5236\u684C\u9762\u901A\u77E5\u548C PWA \u63A8\u9001\uFF1B\u4E24\u4E2A\u6E20\u9053\u53EF\u4EE5\u72EC\u7ACB\u5F00\u5173\uFF0C\u5173\u95ED\u67D0\u4E2A\u6E20\u9053\u4E0D\u4F1A\u4E22\u4E8B\u4EF6\u9009\u62E9\u3002", dirty: notifyDirty, busy: busy, saveLabel: "\u4FDD\u5B58\u901A\u77E5\u914D\u7F6E", onSave: saveNotify, status: blockStatus.notify, extra: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy, onClick: testNotify, children: "\u6D4B\u8BD5\u684C\u9762\u901A\u77E5" }), pushUsable ? (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy || pushCount === 0, onClick: testPush, children: "\u6D4B\u8BD5 PWA \u63A8\u9001" }) : null] }), children: [(0, jsx_runtime_1.jsx)(Row, { label: "\u901A\u77E5\u4E8B\u4EF6", children: (0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '8px 18px' }, children: [['notifyTask', '任务完成', notifyTask, setNotifyTask], ['notifyAsk', '等待回答', notifyAsk, setNotifyAsk], ['notifyApproval', '等待审批', notifyApproval, setNotifyApproval], ['notifyDenied', '审批被拒绝', notifyDenied, setNotifyDenied], ['notifySubagent', '子任务结束', notifySubagent, setNotifySubagent], ['notifyGoal', '目标完成/受阻', notifyGoal, setNotifyGoal], ['notifyJobs', '后台任务结束', notifyJobs, setNotifyJobs]].map(([key, label, value, setter]) => ((0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap' }, children: [(0, jsx_runtime_1.jsx)("input", { className: "enh-event-check", type: "checkbox", checked: value, onChange: (event) => setter(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: label })] }, key))) }) }), (0, jsx_runtime_1.jsx)(Row, { label: "\u684C\u9762\u901A\u77E5", children: (0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 8 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: notifyEnabled, onChange: (event) => setNotifyEnabled(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: notifyEnabled ? '启用' : '停用' })] }) }), (0, jsx_runtime_1.jsxs)(Row, { label: "PWA \u63A8\u9001", children: [(0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 8, opacity: pushUsable ? 1 : 0.5 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: pushUsable && pushEnabled, disabled: !pushUsable, onChange: (event) => setPushEnabled(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: pushUsable ? (pushEnabled ? '启用' : '停用') : '需要 HTTPS' })] }), !pushUsable ? (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5F53\u524D\u9875\u9762\u4E0D\u662F HTTPS/\u5B89\u5168\u4E0A\u4E0B\u6587\uFF0CPWA \u63A8\u9001\u5F00\u5173\u7F6E\u7070\uFF1B\u8BF7\u5148\u542F\u7528 HTTPS\u3002" }) : null] }), pushUsable && pushEnabled ? ((0, jsx_runtime_1.jsxs)("div", { style: { paddingLeft: 12, borderLeft: '2px solid var(--dsw-alias-border-l4, rgba(0,0,0,.08))' }, children: [(0, jsx_runtime_1.jsxs)("div", { style: styles.muted, children: ["\u901A\u77E5\u6743\u9650\uFF1A", typeof Notification !== 'undefined' ? Notification.permission : '不可用', "\uFF1B\u5DF2\u8BA2\u9605\u8BBE\u5907\uFF1A", pushCount, "\u3002"] }), (0, jsx_runtime_1.jsx)("div", { style: styles.actions, children: (0, jsx_runtime_1.jsx)("button", { style: styles.button, disabled: busy, onClick: deviceSubscribed ? disablePush : enablePush, children: deviceSubscribed ? '取消本设备订阅' : '订阅本设备' }) }), (0, jsx_runtime_1.jsxs)("details", { className: "enh-advanced", children: [(0, jsx_runtime_1.jsx)("summary", { children: "\u9AD8\u7EA7\uFF1AVAPID \u8054\u7CFB\u5730\u5740" }), (0, jsx_runtime_1.jsxs)(Row, { label: "VAPID subject", children: [(0, jsx_runtime_1.jsx)("input", { style: styles.input, value: pushSubject, onChange: (event) => setPushSubject(event.target.value), placeholder: "mailto:dsh-enhancement@localhost" }), (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "Web Push \u534F\u8BAE\u8981\u6C42\u7684\u8054\u7CFB\u65B9\u5F0F\uFF0C\u4E0D\u662F\u767B\u5F55\u90AE\u7BB1\uFF0C\u4E5F\u4E0D\u9700\u8981\u9A8C\u8BC1\uFF1B\u4E2A\u4EBA\u4F7F\u7528\u4FDD\u7559\u9ED8\u8BA4\u5373\u53EF\u3002" })] })] })] })) : pushUsable ? (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5F00\u542F PWA \u63A8\u9001\u540E\u663E\u793A\u8BA2\u9605\u3001\u6D4B\u8BD5\u548C VAPID \u8BBE\u7F6E\u3002" }) : null] }), (0, jsx_runtime_1.jsxs)("div", { style: { ...styles.card, order: 5 }, children: [(0, jsx_runtime_1.jsx)("div", { style: styles.label, children: "\u72B6\u6001\u6587\u4EF6" }), (0, jsx_runtime_1.jsx)("div", { style: styles.code, children: status.statePath }), (0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: "\u5BC6\u7801\u53EA\u5B58 scrypt \u54C8\u5E0C\uFF0CDSH \u7B7E\u540D\u5BC6\u94A5\u4E0D\u4F1A\u5199\u5165\u8FD9\u91CC\u3002" })] }), (0, jsx_runtime_1.jsxs)(Block, { order: 5, title: "\u4F1A\u8BDD\u8BF7\u6C42\u5934", hint: "\u8BA9\u975E\u5B98\u65B9 API \u8DEF\u7531\uFF08\u5982 OpenAI \u517C\u5BB9\u7F51\u5173\uFF09\u4E5F\u643A\u5E26 DSH \u7684 session id\u3002\u5B98\u65B9 DeepSeek API \u672C\u6765\u5C31\u5E26\uFF0C\u4E0D\u53D7\u5F71\u54CD\u3002", dirty: sessionHeaderDirty, busy: busy, saveLabel: "\u4FDD\u5B58\u4F1A\u8BDD\u8BF7\u6C42\u5934", onSave: saveSessionHeader, status: blockStatus.sessionHeader, children: [(0, jsx_runtime_1.jsx)(Row, { label: "\u542F\u7528\u6CE8\u5165", children: (0, jsx_runtime_1.jsxs)("label", { style: { display: 'inline-flex', alignItems: 'center', gap: 8 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: sessionHeaderEnabled, onChange: (event) => setSessionHeaderEnabled(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: sessionHeaderEnabled ? '启用' : '停用' })] }) }), (0, jsx_runtime_1.jsx)(Row, { label: "\u8BF7\u6C42\u5934\u540D\u79F0", children: (0, jsx_runtime_1.jsx)("input", { value: sessionHeaderName, onChange: (event) => setSessionHeaderName(event.target.value), placeholder: "x-deepseek-harness-session-id", style: { ...styles.input, width: '100%', maxWidth: 360 }, spellCheck: false, autoCapitalize: "off", autoCorrect: "off" }) }), (0, jsx_runtime_1.jsxs)("div", { style: styles.muted, children: ["\u9ED8\u8BA4\u503C\u4E0E DeepSeek \u5B98\u65B9\u53D1\u9001\u7684\u5934\u540D\u4E00\u81F4\uFF08", (0, jsx_runtime_1.jsx)("code", { children: "x-deepseek-harness-session-id" }), "\uFF09\uFF0C\u4E5F\u53EF\u6539\u6210\u4E0A\u6E38\u7F51\u5173\u7EA6\u5B9A\u7684\u5176\u5B83\u540D\u79F0\u3002"] }), status.sessionHeader.reason === 'llm-unavailable' ? ((0, jsx_runtime_1.jsx)("div", { style: styles.error, children: sessionHeaderSummary(true) })) : ((0, jsx_runtime_1.jsx)("div", { style: styles.muted, children: sessionHeaderSummary() }))] })] }));
602
634
  }
603
635
  const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,200}$/;
604
636
  function openSession(ctx, sessionId) {
package/lib/control.d.ts CHANGED
@@ -4,4 +4,5 @@ import type { Pwa } from './pwa.js';
4
4
  import type { NotificationEngine } from './notify.js';
5
5
  import type { PushService } from './push.js';
6
6
  import type { HostContextLike } from './types.js';
7
- export declare function registerControl(ctx: HostContextLike, gateway: Gateway, store: StateStore, pwa: Pwa, notify: NotificationEngine, push: PushService): void;
7
+ import type { SessionHeaderMount } from './session-header.js';
8
+ export declare function registerControl(ctx: HostContextLike, gateway: Gateway, store: StateStore, pwa: Pwa, notify: NotificationEngine, push: PushService, sessionHeader?: SessionHeaderMount): void;
package/lib/control.js CHANGED
@@ -86,7 +86,7 @@ function allowedHost(address) {
86
86
  }
87
87
  return address === '127.0.0.1' || address === '::1' || address === 'localhost';
88
88
  }
89
- export function registerControl(ctx, gateway, store, pwa, notify, push) {
89
+ export function registerControl(ctx, gateway, store, pwa, notify, push, sessionHeader) {
90
90
  const disposers = [];
91
91
  let restartTimer;
92
92
  const scheduleRestart = () => {
@@ -130,6 +130,8 @@ export function registerControl(ctx, gateway, store, pwa, notify, push) {
130
130
  value.push = push.status();
131
131
  value.pwa.customIcons = pwa.customIcons();
132
132
  value.focusClients = focusHub.size;
133
+ if (sessionHeader !== undefined)
134
+ value.sessionHeader = sessionHeader.status();
133
135
  return value;
134
136
  };
135
137
  const add = (kind, path, handler) => {
@@ -199,6 +201,15 @@ export function registerControl(ctx, gateway, store, pwa, notify, push) {
199
201
  patch.pushEnabled = body.pushEnabled;
200
202
  if (typeof body.pushSubject === 'string' && /^(mailto:|https:\/\/)/i.test(body.pushSubject.trim()))
201
203
  patch.pushSubject = body.pushSubject.trim();
204
+ if (typeof body.sessionHeaderEnabled === 'boolean')
205
+ patch.sessionHeaderEnabled = body.sessionHeaderEnabled;
206
+ if (typeof body.sessionHeaderName === 'string') {
207
+ const name = body.sessionHeaderName.trim();
208
+ // 头名必须是合法的 HTTP token,否则请求会被上游直接拒绝。
209
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name))
210
+ return json(res, 400, { error: '请求头名称不合法' });
211
+ patch.sessionHeaderName = name;
212
+ }
202
213
  if (Array.isArray(body.httpsHostnames)) {
203
214
  patch.httpsHostnames = body.httpsHostnames
204
215
  .filter((item) => typeof item === 'string' && item.trim() !== '')
package/lib/gateway.js CHANGED
@@ -329,6 +329,15 @@ export class Gateway {
329
329
  goal: config.notifyGoal,
330
330
  jobs: config.notifyJobs,
331
331
  },
332
+ // 实际注入状态由 control 层用 session-header 的运行时读数覆盖。
333
+ sessionHeader: {
334
+ enabled: config.sessionHeaderEnabled,
335
+ headerName: config.sessionHeaderName,
336
+ patched: 0,
337
+ adapterCount: 0,
338
+ reason: 'no-adapters',
339
+ lastError: null,
340
+ },
332
341
  };
333
342
  }
334
343
  // -------------------------------------------------------------------------
package/lib/index.js CHANGED
@@ -13,6 +13,7 @@ import { NotificationEngine } from './notify.js';
13
13
  import { PushService } from './push.js';
14
14
  import { STATE_DIR, StateStore, configureStateDir } from './state.js';
15
15
  import { focusHub } from './focus.js';
16
+ import { mountSessionHeader, normalizeHeaderName } from './session-header.js';
16
17
  export const name = 'dsh-enhancement';
17
18
  export const inject = ['webServer'];
18
19
  /**
@@ -64,7 +65,13 @@ export function apply(ctx, config) {
64
65
  const push = new PushService(ctx, store);
65
66
  const notify = new NotificationEngine(ctx, store, gateway, push);
66
67
  notify.start();
67
- registerControl(ctx, gateway, store, pwa, notify, push);
68
+ // 会话请求头注入:让非官方 API 路由(dsh-llm-pi-ai)也能携带 session id。
69
+ // 每次请求读当前设置,因此在设置页改动即时生效、无需重启。
70
+ const sessionHeader = mountSessionHeader(ctx, () => ({
71
+ enabled: store.config.sessionHeaderEnabled,
72
+ headerName: normalizeHeaderName(store.config.sessionHeaderName),
73
+ }));
74
+ registerControl(ctx, gateway, store, pwa, notify, push, sessionHeader);
68
75
  pwa.sync();
69
76
  ctx.effect?.(() => {
70
77
  void gateway.start().catch((error) => {
@@ -74,6 +81,7 @@ export function apply(ctx, config) {
74
81
  notify.stop();
75
82
  pwa.dispose();
76
83
  focusHub.stop();
84
+ sessionHeader.dispose();
77
85
  void gateway.stop();
78
86
  scheduleUninstallPurge(profileDir, STATE_DIR);
79
87
  };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * dsh-enhancement · 会话请求头注入(宿主半侧)。
3
+ *
4
+ * 背景:DSH 只在直连官方 DeepSeek API(`dsh-llm-deepseek`)时发送
5
+ * `x-deepseek-harness-session-id`;走 `dsh-llm-pi-ai` 的第三方 OpenAI 兼容网关
6
+ * 不会携带任何会话标识。本模块把 DSH 的 sessionId 补成 HTTP 请求头,
7
+ * 让上游网关也能按会话关联请求。
8
+ *
9
+ * 注入点为什么是 `streamWithSnapshot`:
10
+ * - `llm/stream` waterfall 的 `options` 是深冻结的,且 `dsh-llm-pi-ai` 在
11
+ * `streamWithSnapshot` 内用白名单(`profileOptions()` + `headers:
12
+ * requestHeaders(profile.headers)`)重新构造 pi-ai 的 options,所以 dsh 层的
13
+ * 任何字段都到不了 HTTP 层;
14
+ * - `snapshot.models.streamSimple(model, context, options)` 是该 adapter 唯一
15
+ * 把 options 交给 pi-ai 的调用点,`options.headers` 会被 pi-ai 直接用作请求头。
16
+ *
17
+ * 目标 adapter 的识别用结构特征而非硬编码包名/路由名:只有 pi-ai adapter
18
+ * 的原型上有 `streamWithSnapshot`;官方 DeepSeek adapter 没有该方法,因此天然
19
+ * 不会被补丁,官方路由的既有行为完全不受影响。
20
+ *
21
+ * 版本漂移防御:宿主内部形态不符(`llm.adapters` 不是 Map、原型上没有
22
+ * `streamWithSnapshot`、`snapshot.models.streamSimple` 缺失)时一律旁路,
23
+ * 绝不阻断原始请求。
24
+ */
25
+ import type { HostContextLike } from './types.js';
26
+ /** 与官方 DeepSeek adapter 一致的头名,便于上游网关统一识别。 */
27
+ export declare const DEFAULT_SESSION_HEADER_NAME = "x-deepseek-harness-session-id";
28
+ export interface SessionHeaderSettings {
29
+ /** 总开关;关闭时纯透传,不触碰任何请求。 */
30
+ enabled: boolean;
31
+ /** 注入的头名。 */
32
+ headerName: string;
33
+ }
34
+ /**
35
+ * 未挂载的具体原因,供设置页给出可操作的提示。
36
+ *
37
+ * - `ok`:已挂载到至少一个第三方 adapter。
38
+ * - `no-adapters`:宿主没有任何已注册 adapter。`dsh-base` 以休眠态挂载
39
+ * `llm-pi-ai`,只有配置了 provider 路由它才会 `registerAdapter`;所以这通常
40
+ * 是「还没添加第三方模型」,不是故障。
41
+ * - `no-pi-ai-adapter`:有 adapter 但没有一个是 pi-ai 形态(结构漂移或只有官方路由)。
42
+ * - `llm-unavailable`:宿主 `llm` 服务不可用或形态变化。
43
+ */
44
+ export type SessionHeaderReason = 'ok' | 'no-adapters' | 'no-pi-ai-adapter' | 'llm-unavailable';
45
+ export interface SessionHeaderStatus {
46
+ enabled: boolean;
47
+ headerName: string;
48
+ /** 已完成注入补丁的 adapter 数。 */
49
+ patched: number;
50
+ /** 宿主已注册的 adapter 总数,用于区分「没配路由」与「结构漂移」。 */
51
+ adapterCount: number;
52
+ reason: SessionHeaderReason;
53
+ /** 最近一次旁路/失败原因(仅异常时非 null),便于排障。 */
54
+ lastError: string | null;
55
+ }
56
+ /** 读取当前设置的入口;由插件在每次请求时调用,因此设置改动即时生效。 */
57
+ export type SettingsReader = () => SessionHeaderSettings;
58
+ /** pi-ai 的 GenerateOptions 形态(只声明本模块用到的部分)。 */
59
+ interface GenerateOptionsLike {
60
+ sessionId?: unknown;
61
+ headers?: Record<string, string>;
62
+ }
63
+ interface AdapterLike {
64
+ constructor?: unknown;
65
+ }
66
+ /** `ctx.llm` 服务中本模块依赖的内部形态。 */
67
+ interface LlmLike {
68
+ adapters?: unknown;
69
+ }
70
+ /** 校验并规范化头名;非法时回退默认值,避免写入无法发送的请求头。 */
71
+ export declare function normalizeHeaderName(value: unknown): string;
72
+ /**
73
+ * 给一份 pi-ai GenerateOptions 补上会话请求头。
74
+ *
75
+ * 不可变构造:入参可能是深冻结对象,直接赋值会抛。已存在同名头时保持原值,
76
+ * 不覆盖用户的静态配置。
77
+ *
78
+ * @param options - pi-ai 的 GenerateOptions。
79
+ * @param settings - 当前设置快照。
80
+ * @returns 补过头的新 options;无需注入时原样返回。
81
+ */
82
+ export declare function injectSessionHeader<T extends GenerateOptionsLike>(options: T, settings: SessionHeaderSettings): T;
83
+ /**
84
+ * 在一个 adapter 的类原型上打 `streamWithSnapshot` 补丁。
85
+ *
86
+ * 同一个原型只包装一次;多个 adapter 实例共享原型时按 owner 隔离,只有被
87
+ * 登记授权的实例会注入。
88
+ *
89
+ * @returns 补丁是否就绪(原型方法存在且可写)。
90
+ */
91
+ export declare function patchAdapterPrototype(adapter: AdapterLike, read: SettingsReader, isActive: () => boolean): boolean;
92
+ /**
93
+ * 扫描已注册 adapter,给 pi-ai 型 adapter 打补丁。
94
+ *
95
+ * @param llm - 宿主 `llm` 服务。
96
+ * @returns 本轮确保覆盖的 adapter 数。
97
+ */
98
+ export declare function sweepAdapters(llm: LlmLike, read: SettingsReader, isActive: () => boolean): number;
99
+ /** 还原全部已打补丁的原型(插件 dispose 时调用)。 */
100
+ export declare function restorePrototypes(): void;
101
+ /**
102
+ * 挂载会话请求头注入。
103
+ *
104
+ * @param ctx - 宿主插件上下文。
105
+ * @param read - 当前设置读取器(每次请求求值,设置改动即时生效)。
106
+ * @returns 挂载结果与状态读取器。
107
+ */
108
+ export interface SessionHeaderMount {
109
+ status: () => SessionHeaderStatus;
110
+ dispose: () => void;
111
+ }
112
+ export declare function mountSessionHeader(ctx: HostContextLike, read: SettingsReader): SessionHeaderMount;
113
+ export {};