From 03ef154d85e83c34eb0feb6316fbde2ab31a4f10 Mon Sep 17 00:00:00 2001 From: Jan Eickholt Date: Thu, 17 Sep 2026 15:17:17 +0200 Subject: [PATCH 1/4] fix(proxy): restore HTML injection over gzip and make remote settings opt-in (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题(issue #58 + 本地排查): 1. 注入对真实浏览器从未生效:代理只注入未压缩 HTML(isCompressed 跳过), 而 dsh web 对浏览器导航返回 gzip 压缩文档 → polyfill 与 transport shim 全部落空。此前历次注入类修复(#3/#53/#96)实际都是死代码。 2. 远程设置不可用(issue #58):dsh-client-connection 以 location.hostname 判 isLoopback;经代理访问时是局域网 IP/隧道域名 → settings mirror 降级 memory 只读 → 插件配置/模型管理报 settings unavailable。 #87 的 location 伪装方案因 #105 回退;作者等待的「可注入 loopback 标记」 其实已存在:transport?.ownsHost === true。 修复: 1. HTML 导航请求向上游要求 accept-encoding: identity,注入分支恢复生效; 压缩透传行为不变(上游忽略 identity 时仍原样透传,无损坏风险)。 2. 新增 TRUSTED_TRANSPORT_SHIM(仅当 __DSH_TRANSPORT__ 缺失时创建 { ownsHost: true },桌面端宿主自带的 transport 不受影响),默认关闭, 由设置页「远程设置」开关(trustProxiedClients)控制;开启后远程页面的 插件配置/模型管理恢复 host 持久化。安全边界回落到访问密码(公网强制)。 测试:test/proxy.test.js 新增 2 例(identity 注入恢复 + 信任开关默认值/ 运行时切换);既有「压缩 HTML 不注入」用例不变全绿。 --- client/api.js | 1 + client/client.js | 24 ++++++++++++++ client/index.jsx | 14 ++++++++ client/pocket-locales.js | 6 ++++ lib/index.js | 6 +++- lib/proxy.mjs | 29 ++++++++++++++-- lib/service.mjs | 4 +++ lib/settings.mjs | 21 ++++++++++++ lib/web-rpc.js | 10 +++++- test/proxy.test.js | 71 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 181 insertions(+), 5 deletions(-) diff --git a/client/api.js b/client/api.js index 648a56d..a1de863 100644 --- a/client/api.js +++ b/client/api.js @@ -13,6 +13,7 @@ export const POCKET_ENDPOINTS = Object.freeze({ restart: 'pocket.restart', lanTokenRefresh: 'token.lanRefresh', lanAuthSetEnabled: 'lanAuth.setEnabled', + trustSetEnabled: 'trust.setEnabled', lanSetOverride: 'lan.setOverride', lanSetEnabled: 'lan.setEnabled', mobileRightbarSetEnabled: 'mobile.rightbar.setEnabled', diff --git a/client/client.js b/client/client.js index d614db0..c8190c0 100644 --- a/client/client.js +++ b/client/client.js @@ -53,6 +53,7 @@ var POCKET_ENDPOINTS = Object.freeze({ restart: "pocket.restart", lanTokenRefresh: "token.lanRefresh", lanAuthSetEnabled: "lanAuth.setEnabled", + trustSetEnabled: "trust.setEnabled", lanSetOverride: "lan.setOverride", lanSetEnabled: "lan.setEnabled", mobileRightbarSetEnabled: "mobile.rightbar.setEnabled", @@ -1943,6 +1944,9 @@ var zh2 = { "pinInvalid": "\u5BC6\u7801\u5FC5\u987B\u662F 8\u201364 \u4F4D\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57", "pinCustomHint": "\u81EA\u5B9A\u4E49\u540E\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0", "lanPinOff": "\u{1F513} \u5BC6\u7801\u5DF2\u5173\u95ED\uFF1A\u626B\u7801\u76F4\u8FDE\uFF0C\u65E0\u9700\u5BC6\u7801\uFF08\u4EC5\u540C\u4E00\u5C40\u57DF\u7F51\u8BBE\u5907\u53EF\u8BBF\u95EE\uFF1B\u516C\u7F51\u4ECD\u8981\u5BC6\u7801\uFF09", + "trustClients": "\u8FDC\u7A0B\u8BBE\u7F6E\uFF08\u63D2\u4EF6\u914D\u7F6E / \u6A21\u578B\u7BA1\u7406\uFF09", + "trustClientsOn": "\u2705 \u624B\u673A/\u8FDC\u7A0B\u9875\u9762\u53EF\u7F16\u8F91\u8BBE\u7F6E\uFF08\u63D2\u4EF6\u914D\u7F6E\u3001\u6A21\u578B\u7BA1\u7406\u7B49\uFF09\u3002\u5B89\u5168\u8FB9\u754C\u4E3A\u8BBF\u95EE\u5BC6\u7801\u2014\u2014\u516C\u7F51\u5F3A\u5236\u3001\u5C40\u57DF\u7F51\u5EFA\u8BAE\u5F00\u542F\u3002", + "trustClientsOff": "\u{1F512} \u8FDC\u7A0B\u9875\u9762\u53EA\u8BFB\uFF1A\u8BBE\u7F6E\u8BF7\u5728\u7535\u8111\u672C\u673A\uFF08127.0.0.1\uFF09\u4FEE\u6539\u3002", "lanStarting": "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026", "mobileRightbar": "\u624B\u673A\u7AEF\u53F3\u8FB9\u680F", "mobileRightbarHint": "\u663E\u793A\u539F\u751F\u53F3\u8FB9\u680F\u5165\u53E3\uFF1B\u666E\u901A\u624B\u673A\u53EF\u6309\u9700\u5173\u95ED\uFF0C\u6298\u53E0\u5C4F\u5C55\u5F00\u540E\u4F7F\u7528\u66F4\u65B9\u4FBF", @@ -2041,6 +2045,9 @@ var en2 = { "pinInvalid": "PIN must be 8\u201364 characters (letters and digits only)", "pinCustomHint": "custom PINs are not rotated on tunnel start", "lanPinOff": "\u{1F513} PIN off \u2014 scan & go, no PIN (LAN devices only; public still requires PIN)", + "trustClients": "Remote settings (plugin config / models)", + "trustClientsOn": "\u2705 Phone/remote pages can edit settings (plugin config, models, etc.). The access PIN is the trust boundary \u2014 mandatory for public, recommended for LAN.", + "trustClientsOff": "\u{1F512} Remote pages are read-only: change settings on the computer itself (127.0.0.1).", "lanStarting": "Proxy starting\u2026", "mobileRightbar": "Mobile right sidebar", "mobileRightbarHint": "Show the native right-sidebar entry; disable it for a compact phone header or keep it on for an unfolded display", @@ -2305,6 +2312,13 @@ function PocketSettingsTab({ rpcCall, t }) { } catch { } }; + const setTrust = async (on) => { + try { + const r = await call(POCKET_ENDPOINTS.trustSetEnabled, { on }); + setStatus((s) => ({ ...s, trustProxiedClients: r.trustProxiedClients })); + } catch { + } + }; const setMobileRightbar = async (on) => { try { const r = await call(POCKET_ENDPOINTS.mobileRightbarSetEnabled, { on }); @@ -2515,6 +2529,16 @@ function PocketSettingsTab({ rpcCall, t }) { status?.lanPinCustom ? (0, import_react2.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null ) ), + // 远程设置开关(issue #58):信任经代理客户端 → 设置页/模型管理远程可编辑 + row( + t("trustClients"), + Switch(status?.trustProxiedClients === true, () => setTrust(status?.trustProxiedClients !== true)), + (0, import_react2.createElement)( + "div", + { style: { ...styles.muted, marginTop: 6 } }, + status?.trustProxiedClients === true ? t("trustClientsOn") : t("trustClientsOff") + ) + ), // 高级:手动选地址(默认收起) row( t("advAddress"), diff --git a/client/index.jsx b/client/index.jsx index 1227511..5bdb18b 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -266,6 +266,15 @@ function PocketSettingsTab({ rpcCall, t }) { } catch { /* 忽略 */ } }; + // 远程设置开关(issue #58):开启后手机/远程页面的插件配置、模型管理恢复可编辑; + // 关闭回到只读(设置仅本机可改)。安全边界为访问密码。 + const setTrust = async (on) => { + try { + const r = await call(POCKET_ENDPOINTS.trustSetEnabled, { on }); + setStatus((s) => ({ ...s, trustProxiedClients: r.trustProxiedClients })); + } catch { /* 忽略 */ } + }; + const setMobileRightbar = async (on) => { try { const r = await call(POCKET_ENDPOINTS.mobileRightbarSetEnabled, { on }); @@ -467,6 +476,11 @@ function PocketSettingsTab({ rpcCall, t }) { customBtn('lan'), status?.lanPinCustom ? h('span', { style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)' } }, t('pinCustomHint')) : null, ))), + // 远程设置开关(issue #58):信任经代理客户端 → 设置页/模型管理远程可编辑 + row(t('trustClients'), + Switch(status?.trustProxiedClients === true, () => setTrust(status?.trustProxiedClients !== true)), + h('div', { style: { ...styles.muted, marginTop: 6 } }, + status?.trustProxiedClients === true ? t('trustClientsOn') : t('trustClientsOff'))), // 高级:手动选地址(默认收起) row(t('advAddress'), h('button', { style: { border: 'none', background: 'none', font: 'inherit', cursor: 'pointer', fontSize: 12, color: 'var(--dsw-alias-label-tertiary,#8b93a1)', padding: 0 }, onClick: () => setAdvOpen((v) => !v) }, diff --git a/client/pocket-locales.js b/client/pocket-locales.js index effee90..9a3bf26 100644 --- a/client/pocket-locales.js +++ b/client/pocket-locales.js @@ -65,6 +65,9 @@ export const zh = { 'pinInvalid': '密码必须是 8–64 位英文字母或数字', 'pinCustomHint': '自定义后开启公网不再自动换新', 'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)', + 'trustClients': '远程设置(插件配置 / 模型管理)', + 'trustClientsOn': '✅ 手机/远程页面可编辑设置(插件配置、模型管理等)。安全边界为访问密码——公网强制、局域网建议开启。', + 'trustClientsOff': '🔒 远程页面只读:设置请在电脑本机(127.0.0.1)修改。', 'lanStarting': '代理未就绪…', 'mobileRightbar': '手机端右边栏', 'mobileRightbarHint': '显示原生右边栏入口;普通手机可按需关闭,折叠屏展开后使用更方便', @@ -165,6 +168,9 @@ export const en = { 'pinInvalid': 'PIN must be 8–64 characters (letters and digits only)', 'pinCustomHint': 'custom PINs are not rotated on tunnel start', 'lanPinOff': '🔓 PIN off — scan & go, no PIN (LAN devices only; public still requires PIN)', + 'trustClients': 'Remote settings (plugin config / models)', + 'trustClientsOn': '✅ Phone/remote pages can edit settings (plugin config, models, etc.). The access PIN is the trust boundary — mandatory for public, recommended for LAN.', + 'trustClientsOff': '🔒 Remote pages are read-only: change settings on the computer itself (127.0.0.1).', 'lanStarting': 'Proxy starting…', 'mobileRightbar': 'Mobile right sidebar', 'mobileRightbarHint': 'Show the native right-sidebar entry; disable it for a compact phone header or keep it on for an unfolded display', diff --git a/lib/index.js b/lib/index.js index bcfe433..c90924c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -21,7 +21,7 @@ import { createPocketService } from './service.mjs'; import { installPocketRpc } from './web-rpc.js'; import { restartHost } from './restart.js'; import { advancedNoticeScript, DEFAULT_INJECT, classifyHost } from './proxy.mjs'; -import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, mobileRightbarEnabled, setMobileRightbarEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings, proxyPort, cloudflaredPath } from './settings.mjs'; +import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, mobileRightbarEnabled, setMobileRightbarEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings, proxyPort, cloudflaredPath, trustProxiedClients, setTrustProxiedClients } from './settings.mjs'; const name = 'dsh-pocket'; const inject = ['connection', 'webServer']; @@ -291,6 +291,8 @@ export function apply(ctx, config = {}, internals = {}) { internals, getLanIpOverride: () => lanIpOverride(), getLanEnabled: () => lanEnabled(), + // 经代理客户端信任开关(issue #58):代理每次请求实时读,切换立即生效 + trustProxiedClients: () => trustProxiedClients(), // 桌面端**不再**注入 dsh-desktop-* 标记(issue #76): // - 旧版 dsh-plugin-desktop 缺 mode/platform 会抛错,当初正是为此加了补丁(issue #3/#4); // - 但 2.0.3 起,mode 与 platform **同时缺失**时 parseDesktopClientEnvironment 直接返回 @@ -355,6 +357,8 @@ export function apply(ctx, config = {}, internals = {}) { refreshLanToken: () => refreshLanToken(), getLanAuthEnabled: () => lanAuthEnabled(), setLanAuthEnabled: (on) => setLanAuthEnabled(on), + getTrustEnabled: () => trustProxiedClients(), + setTrustEnabled: (on) => setTrustProxiedClients(on), getLanEnabled: () => lanEnabled(), setLanEnabled: (on) => setLanEnabled(on), getMobileRightbarEnabled: () => mobileRightbarEnabled(), diff --git a/lib/proxy.mjs b/lib/proxy.mjs index 8e29df1..3155d82 100644 --- a/lib/proxy.mjs +++ b/lib/proxy.mjs @@ -51,6 +51,19 @@ export const RANDOM_UUID_POLYFILL = ``; +/** + * 信任版 transport shim(issue #58):web shell 从不创建 __DSH_TRANSPORT__,这里在 + * 缺失时创建 { ownsHost: true } —— dsh-client-connection 以 transport?.ownsHost===true + * 视作 loopback(client.js 的 isLoopback 计算),使经代理访问时 settings/plugin-config + * 恢复 host 持久化(issue #58 作者等待的「可注入 loopback 标记」其实已存在)。 + * 仅在 __DSH_TRANSPORT__ 缺失时创建(桌面端宿主自己会提供 transport,不受影响—— + * 这也是 #87 方案在 #105 场景出问题、而本 shim 不会的原因)。 + * 默认关闭,由设置页「远程设置」开关(trustProxiedClients)控制。开启后经本代理的 + * 所有客户端都被客户端栅栏视为受信;服务器侧栅栏又因 Host/Origin 改写而放行 + * → 安全边界回落到 pocket 访问密码(公网强制;局域网按开关)。 + */ +export const TRUSTED_TRANSPORT_SHIM = ``; + const INJECT_MARK = 'data-dsh-pocket-polyfill="1"'; /** @@ -708,7 +721,7 @@ function attachWebSocketHeartbeat(socket, { intervalMs = 30_000, missLimit = 2 } * @param {() => boolean} [opts.lanAccessEnabled] 局域网访问是否开启(默认开启)。关闭时拦截经局域网 Host 的请求(公网/loopback 不受影响)。 * @returns {Promise<{server:import('node:http').Server, close:()=>Promise}>} */ -export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null, rateLimit = null, heartbeat = {}, lanAccessEnabled = () => true, launchToken = () => '', handshakeLimit } = {}) { +export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null, rateLimit = null, heartbeat = {}, lanAccessEnabled = () => true, launchToken = () => '', handshakeLimit, trustProxiedClients = () => false } = {}) { const limiter = auth ? createRateLimiter(rateLimit ?? {}) : null; // 会话握手重试计数(issue #91):Safari 在 http://IP 源上丢 3xx 的 cookie → 死循环 const handshake = createHandshakeTracker( @@ -841,6 +854,13 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE } } const headers = loopbackAuthority({ ...req.headers }, upstream); + // HTML 文档请求强制上游返回未压缩体:注入分支只能改写未压缩 HTML,而浏览器导航 + // 默认带 accept-encoding: gzip,上游(dsh web)会把文档压缩 → isCompressed 命中 + // → 注入被整体跳过,polyfill 与 transport shim 对真实浏览器等于没生效。 + // 文档本身很小,identity 无带宽损失;JS/CSS 等子资源不注入,仍透传压缩。 + if (injectHtml && isHtmlRequest(req)) { + headers['accept-encoding'] = 'identity'; + } // dsh web 浏览器会话 token(issue #77):首屏根路径补一次,换回绑定 authority 的 cookie const launchTok = (typeof launchToken === 'function' ? launchToken() : '') || ''; // 先清掉历史遗留的 dsh-desktop-* 参数(issue #75),再补 launch token @@ -940,14 +960,17 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE return; } // 只给**未压缩**的 HTML 文档注入(SSE/WS/JS/CSS 原样透传;压缩流注入会损坏页面); - // 注入后修正 Content-Length + // 注入后修正 Content-Length。信任开关开启时追加 TRUSTED_TRANSPORT_SHIM + // (每次请求实时读设置,开关切换立即生效,无需重启代理)。 if (injectHtml && contentType.includes('text/html') && !isCompressed(proxyRes.headers)) { + const trusted = trustProxiedClients?.() === true; + const injectNow = trusted ? injectHtml + TRUSTED_TRANSPORT_SHIM : injectHtml; const chunks = []; proxyRes.on('data', (c) => chunks.push(c)); proxyRes.on('end', () => { let html = Buffer.concat(chunks).toString('utf8'); if (!html.includes(INJECT_MARK)) { - html = html.replace(/]*>/i, (m) => `${m}${injectHtml}`); + html = html.replace(/]*>/i, (m) => `${m}${injectNow}`); } const out = Buffer.from(html, 'utf8'); const outHeaders = { ...proxyRes.headers }; diff --git a/lib/service.mjs b/lib/service.mjs index 4312153..02d8dd5 100644 --- a/lib/service.mjs +++ b/lib/service.mjs @@ -179,6 +179,8 @@ export function createPocketService({ getLanEnabled = () => true, /** 代理注入 HTML 的内容(桌面端补丁等由 lib/index.js 传入;默认 randomUUID polyfill) */ injectHtml, + /** 经代理客户端信任开关(issue #58):() => boolean;默认关闭。开启时注入 ownsHost shim */ + trustProxiedClients = () => false, /** 访问令牌认证配置(issue #13):{ getToken, isProtected },传给代理 */ auth, /** @type {() => string} dsh web 浏览器会话启动 token(issue #77;老版本返回空字符串) */ @@ -276,6 +278,8 @@ export function createPocketService({ ...(auth ? { auth } : {}), // 每次请求实时读开关:设置页切换后立即生效,无需重启代理 lanAccessEnabled: () => getLanEnabled(), + // 经代理客户端信任开关(issue #58):每次请求实时读,切换立即生效 + trustProxiedClients, // dsh web 浏览器会话启动 token(issue #77):实时取,新版 dsh 才有 ...(launchToken ? { launchToken } : {}), }); diff --git a/lib/settings.mjs b/lib/settings.mjs index 5b71531..690acd5 100644 --- a/lib/settings.mjs +++ b/lib/settings.mjs @@ -4,6 +4,7 @@ // - lanEnabled 局域网访问总开关(默认开启):关闭后局域网扫码/链接直接失效(代理拒绝局域网 Host) // - lanAuthEnabled 局域网访问密码开关(issue #24),默认开启 // - mobileRightbarEnabled 手机端右边栏入口(默认开启) +// - trustProxiedClients 经代理客户端信任开关(issue #58),默认关闭 // - publicPinCustom 公网密码是否用户自定义(issue #33),自定义后不自动轮换 // - lanPinCustom 局域网密码是否用户自定义(issue #33) // - tunnelMode 公网隧道模式(issue #66):'quick'(默认,随机 trycloudflare.com)| 'named'(固定域名) @@ -65,6 +66,26 @@ export function setLanAuthEnabled(on) { return s.lanAuthEnabled; } +/** + * 经代理客户端信任开关(issue #58):默认**关闭**(文件缺失/损坏都视为关闭)。 + * 开启后代理在注入 HTML 时附 TRUSTED_TRANSPORT_SHIM:web shell 从不创建 + * __DSH_TRANSPORT__,缺一个 { ownsHost: true }——dsh-client-connection 以 + * transport?.ownsHost===true 视作 loopback,于是手机/远程页面的设置(插件配置、 + * 模型管理)恢复 host 持久化,而不是降级 memory 只读。开启后经代理的所有客户端 + * 都被客户端栅栏视为受信,安全边界回落到访问密码(公网强制;局域网按开关)。 + */ +export function trustProxiedClients() { + return readSettings().trustProxiedClients === true; +} + +/** 设置经代理客户端信任开关,返回新状态(持久化)。 */ +export function setTrustProxiedClients(on) { + const s = readSettings(); + s.trustProxiedClients = !!on; + writeSettings(s); + return s.trustProxiedClients; +} + /** 手机端右边栏入口:默认开启,可按需关闭以保持更紧凑的标题栏。 */ export function mobileRightbarEnabled() { return readSettings().mobileRightbarEnabled !== false; diff --git a/lib/web-rpc.js b/lib/web-rpc.js index 42132a8..a4badd8 100644 --- a/lib/web-rpc.js +++ b/lib/web-rpc.js @@ -267,7 +267,7 @@ export function killHint(port) { * 优先直接挂到本插件 inject 的 webServer 上(dsh v0.1.5-alpha.1+ 兼容路径), * 不可用时回退 ctx.connection.rpc.handle(旧版 dsh 兼容路径)。 * wire 协议两条路径完全一致:client-request → handler → server-response。 */ -export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null, getLanAuthEnabled = null, setLanAuthEnabled = null, getLanEnabled = null, setLanEnabled = null, getMobileRightbarEnabled = null, setMobileRightbarEnabled = null, getLanIpOverride = null, setLanIpOverride = null, getPinCustom = null, setCustomPin = null, getTunnelConfig = null, setTunnelConfig = null, resetPocket = null }) { +export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null, getLanAuthEnabled = null, setLanAuthEnabled = null, getTrustEnabled = null, setTrustEnabled = null, getLanEnabled = null, setLanEnabled = null, getMobileRightbarEnabled = null, setMobileRightbarEnabled = null, getLanIpOverride = null, setLanIpOverride = null, getPinCustom = null, setCustomPin = null, getTunnelConfig = null, setTunnelConfig = null, resetPocket = null }) { const pocketRpcHandler = async (endpoint, payload = {}, signal) => { if (signal?.aborted) return fail('cancelled', 'The request was cancelled.'); @@ -284,6 +284,7 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false, accessToken: getToken?.() ?? null, lanToken: getLanToken?.() ?? null, lanAuthEnabled: getLanAuthEnabled?.() ?? true, + trustProxiedClients: getTrustEnabled?.() ?? false, lanEnabled: getLanEnabled?.() ?? true, mobileRightbarEnabled: getMobileRightbarEnabled?.() ?? true, publicPinCustom: getPinCustom?.('public') ?? false, @@ -306,6 +307,13 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false, if (enabled === undefined) return fail('bad-request', '局域网密码开关不可用 | LAN PIN switch unavailable'); return ok({ lanAuthEnabled: enabled }); } + if (endpoint === POCKET_ENDPOINTS.trustSetEnabled) { + // 经代理客户端信任开关(issue #58):开启后设置页/模型管理等远程编辑恢复可用, + // 安全边界回落到访问密码;关闭回到原行为(远程只读,仅本机可改)。 + const enabled = setTrustEnabled?.(payload?.on === true); + if (enabled === undefined) return fail('bad-request', '信任开关不可用 | trust switch unavailable'); + return ok({ trustProxiedClients: enabled }); + } if (endpoint === POCKET_ENDPOINTS.lanSetEnabled) { const enabled = setLanEnabled?.(payload?.on === true); if (enabled === undefined) return fail('bad-request', '局域网访问开关不可用 | LAN access switch unavailable'); diff --git a/test/proxy.test.js b/test/proxy.test.js index 77fe8c3..385ff42 100644 --- a/test/proxy.test.js +++ b/test/proxy.test.js @@ -422,6 +422,77 @@ test('压缩 HTML(gzip)不注入 polyfill——防止损坏压缩流', async } }); +test('HTML 导航请求向上游要求 identity,压缩文档也能注入(上游按 accept-encoding 压缩的场景)', async () => { + const zlib = await import('node:zlib'); + const up = createServer((req, res) => { + if (req.url === '/') { + // 模拟 dsh web:客户端带 gzip 时返回压缩文档 + if (String(req.headers['accept-encoding'] ?? '').includes('gzip')) { + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'content-encoding': 'gzip' }); + res.end(zlib.gzipSync('gz')); + } else { + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); + res.end('identity'); + } + } else { + res.writeHead(200, { 'content-type': 'application/javascript' }); + res.end('console.log("asset");'); + } + }); + await new Promise((r) => up.listen(0, '127.0.0.1', r)); + const proxy = await createPocketProxy({ port: 0, host: '127.0.0.1', upstream: { host: '127.0.0.1', port: up.address().port } }); + try { + // 用原始 http.request(不带 undici 自动解压)拿真实字节 + const raw = await new Promise((resolve, reject) => { + const req = httpRequest({ host: '127.0.0.1', port: proxy.port, path: '/', headers: { accept: 'text/html', 'accept-encoding': 'gzip' } }, (res) => { + const chunks = []; + res.on('data', (c) => chunks.push(c)); + res.on('end', () => resolve({ headers: res.headers, body: Buffer.concat(chunks) })); + }); + req.on('error', reject); + req.end(); + }); + assert.equal(raw.headers['content-encoding'], undefined, '导航响应未压缩(可注入)'); + assert.ok(raw.body.toString('utf8').includes('randomUUID'), '浏览器导航也能拿到注入'); + assert.ok(raw.body.toString('utf8').includes('identity'), '注入的是上游未压缩文档'); + } finally { + await proxy.close(); + await new Promise((r) => up.close(r)); + } +}); + +test('TRUSTED_TRANSPORT_SHIM(issue #58):默认关闭,开启后注入 ownsHost 标记,运行时切换立即生效', async () => { + const up = createServer((req, res) => { + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); + res.end('xapp'); + }); + await new Promise((r) => up.listen(0, '127.0.0.1', r)); + const flag = { v: false }; + let on = null; + try { + const off = await createPocketProxy({ port: 0, host: '127.0.0.1', upstream: { host: '127.0.0.1', port: up.address().port } }); + const htmlOff = await (await fetch(`http://127.0.0.1:${off.port}/`)).text(); + assert.ok(!htmlOff.includes('ownsHost'), '默认(关闭)不注入 ownsHost'); + assert.ok(htmlOff.includes('data-dsh-pocket-transport-shim'), '基础 transport shim 仍在'); + await off.close(); + + on = await createPocketProxy({ + port: 0, host: '127.0.0.1', upstream: { host: '127.0.0.1', port: up.address().port }, + trustProxiedClients: () => flag.v, + }); + flag.v = true; + const htmlOn = await (await fetch(`http://127.0.0.1:${on.port}/`)).text(); + assert.ok(htmlOn.includes('ownsHost:true'), '开启后注入 ownsHost:true'); + assert.ok(htmlOn.indexOf('ownsHost:true') < htmlOn.indexOf(''), '注入在 head 内、bundle 之前'); + flag.v = false; + const htmlOff2 = await (await fetch(`http://127.0.0.1:${on.port}/`)).text(); + assert.ok(!htmlOff2.includes('ownsHost'), '关闭开关后立即停止注入(无需重启代理)'); + } finally { + if (on) await on.close(); + await new Promise((r) => up.close(r)); + } +}); + test('活动 WS 连接存在时 close 不挂起(closeAllConnections)', async () => { const up = await fakeUpstream(); const proxy = await createPocketProxy({ port: 0, host: '127.0.0.1', upstream: { host: '127.0.0.1', port: up.port } }); From 6e616ea12b229831479e6bce32f06484a392444c Mon Sep 17 00:00:00 2001 From: Jan Eickholt Date: Thu, 17 Sep 2026 15:54:05 +0200 Subject: [PATCH 2/4] =?UTF-8?q?docs(readme):=20document=20the=20=E8=BF=9C?= =?UTF-8?q?=E7=A8=8B=E8=AE=BE=E7=BD=AE/Remote=20settings=20switch;=20test(?= =?UTF-8?q?proxy):=20close=20the=20default-off=20proxy=20in=20finally?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md / README.en.md:功能表新增一行 + 局域网区块补充说明(默认关闭, 开启后远程可编辑插件配置/模型管理,信任边界为访问密码) - test:信任开关用例里默认关闭的代理改在 finally 中关闭,断言失败不再悬挂 测试进程 --- README.en.md | 3 +++ README.md | 3 +++ test/proxy.test.js | 5 +++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.en.md b/README.en.md index c5a23f8..9c07273 100644 --- a/README.en.md +++ b/README.en.md @@ -53,6 +53,7 @@ What it looks like — the phone shows the exact same UI as your computer, live: | ⚡ Real-time sync | Streaming output passes through WebSocket untouched — what the computer renders, the phone renders live; fully interactive both ways; built-in WS heartbeat keep-alive (defeats silent NAT/battery link drops with auto-reconnect) | | 📱 Mobile-adaptive layout | Narrow screens get a drawer layout automatically (ported from dsh-web-mobile, MIT): sidebar drawer, full-width conversation, safe-area insets, touch optimizations | | 🧭 Optional right sidebar | Shows the native right-sidebar entry on mobile; disable it for a compact phone header or keep it available alongside the terminal dock on an unfolded display | +| 🛠️ Remote settings | Phone/remote pages can **edit settings** (plugin config, models, etc., issue #58). **Off by default** (remote pages are read-only; change settings on the computer itself). When on, the access PIN is the trust boundary — mandatory for public, recommended for LAN. | | 📁 File browser | The mobile "Files" entries need a host-side explorer panel (a dsh-web-ui component); on stock DSH without it the entries are auto-hidden instead of doing nothing | | 🗜️ Transfer compression | Large JSON responses are gzip/brotli'd on the fly (17MB session history → ~1MB; brotli quality 6: fast and bandwidth-friendly) — faster loads, less mobile data | | 🔁 Tunnel auto-restore | After a DSH restart the previously-running public tunnel comes back automatically | @@ -89,6 +90,8 @@ Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LA > > The LAN PIN is **on by default** (security-first). If you're the only user and find typing it every time annoying, flip "LAN access PIN" to **Off** in the LAN block — LAN scans then connect directly with no PIN (LAN-only devices; the **public tunnel always requires a PIN**, unaffected). > +> "**Remote settings**" is **off by default**: DSH settings (plugin config, models, etc.) are read-only on phone/remote pages — change them on the computer itself (127.0.0.1). Flip it **On** in the LAN block and the phone can edit them directly; the access PIN is then the trust boundary (public always requires it; keep the LAN PIN on on shared networks). +> > After logging in once, the phone **won't ask again**: as long as the computer's dsh web keeps running, reopening the phone needs no PIN (**a dsh web restart/update asks for it once more**). > > Advanced option: auto-detection may not pick a reachable address for Tailscale/VPN setups. You can select a detected IP from the "LAN address" dropdown; normally no change is needed. diff --git a/README.md b/README.md index 470dc21..ca7383f 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看 | ⚡ 实时同步 | 流式输出走 WebSocket 全透传——**电脑上在输出,手机上同步在滚**,可双向操作;内置心跳保活(防路由器 NAT/省电机制静默断链,断线自动重连) | | 📱 移动端适配 | 窄屏自动变抽屉布局(移植 dsh-web-mobile,MIT):侧栏抽屉、会话全宽、状态栏安全区、触控优化 | | 🧭 可选右边栏 | 手机端显示原生右边栏入口;普通手机可在设置中关闭以保持紧凑,折叠屏展开后可更方便地同时使用终端底栏和右边栏 | +| 🛠️ 远程设置 | 手机/远程页面**可编辑设置**(插件配置、模型管理等,issue #58)。默认**关闭**(远程只读,设置仅本机可改);开启后安全边界为访问密码——公网强制、局域网建议开启 | | 📁 文件浏览 | 移动端「文件浏览」入口需要宿主提供 explorer 面板(dsh-web-ui 组件);官方 DSH 未内置时入口自动隐藏,不会出现"点了没反应" | | 🗜️ 传输压缩 | 大 JSON 响应自动 gzip/brotli(长会话 17MB → ~1MB,brotli 质量 6:快且省流量),手机加载更快、更省流量 | | 🔁 隧道自动恢复 | DSH 重启后自动重新拉起之前开着的公网隧道,无需手动重开 | @@ -89,6 +90,8 @@ npx @deepseek-ai/dsh web > > 局域网密码**默认开启**(安全优先)。如果只有自己用、嫌每次输密码麻烦,可在设置页局域网区块把「局域网访问密码」切到**关**——之后局域网扫码直连、无需密码(仅同一局域网设备可访问;**公网始终要密码**,不受影响)。 > +> 「**远程设置**」默认**关**:远程页面(手机)里的 DSH 设置(插件配置、模型管理等)为只读,改动请在电脑本机(127.0.0.1)操作。在设置页局域网区块把它切到**开**,手机即可直接编辑这些设置——信任边界为访问密码:公网始终要密码,局域网建议把「局域网访问密码」保持开启。 +> > 手机登录一次后**长期免输**:只要电脑上的 dsh web 不重启,再次打开手机不用再输入(**dsh web 重启/更新后需重新输入一次**)。 > > 高级选项:自动识别在 Tailscale/VPN 等场景下可能选不到可达地址。可在「局域网地址」下拉框手动选择已检测到的 IP;一般不需要修改。 diff --git a/test/proxy.test.js b/test/proxy.test.js index 385ff42..52889e7 100644 --- a/test/proxy.test.js +++ b/test/proxy.test.js @@ -468,13 +468,13 @@ test('TRUSTED_TRANSPORT_SHIM(issue #58):默认关闭,开启后注入 own }); await new Promise((r) => up.listen(0, '127.0.0.1', r)); const flag = { v: false }; + let off = null; let on = null; try { - const off = await createPocketProxy({ port: 0, host: '127.0.0.1', upstream: { host: '127.0.0.1', port: up.address().port } }); + off = await createPocketProxy({ port: 0, host: '127.0.0.1', upstream: { host: '127.0.0.1', port: up.address().port } }); const htmlOff = await (await fetch(`http://127.0.0.1:${off.port}/`)).text(); assert.ok(!htmlOff.includes('ownsHost'), '默认(关闭)不注入 ownsHost'); assert.ok(htmlOff.includes('data-dsh-pocket-transport-shim'), '基础 transport shim 仍在'); - await off.close(); on = await createPocketProxy({ port: 0, host: '127.0.0.1', upstream: { host: '127.0.0.1', port: up.address().port }, @@ -488,6 +488,7 @@ test('TRUSTED_TRANSPORT_SHIM(issue #58):默认关闭,开启后注入 own const htmlOff2 = await (await fetch(`http://127.0.0.1:${on.port}/`)).text(); assert.ok(!htmlOff2.includes('ownsHost'), '关闭开关后立即停止注入(无需重启代理)'); } finally { + if (off) await off.close(); if (on) await on.close(); await new Promise((r) => up.close(r)); } From c4fa432aa0a2c82be5b4075be6fbf19895756e02 Mon Sep 17 00:00:00 2001 From: Jan Eickholt Date: Thu, 17 Sep 2026 18:24:30 +0200 Subject: [PATCH 3/4] i18n: drop prescriptive wording from the remote-settings switch description Leave the decision to the user: state what the switch does and that the access PIN is the only safeguard, instead of 'mandatory for public, recommended for LAN'. --- client/client.js | 32 ++++++++++++++++---------------- client/pocket-locales.js | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/client/client.js b/client/client.js index c8190c0..c2c2864 100644 --- a/client/client.js +++ b/client/client.js @@ -28,7 +28,7 @@ var __copyProps = (to, from, except, desc) => { }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); -// client/index.jsx +// index.jsx var index_exports = {}; __export(index_exports, { apply: () => apply, @@ -39,7 +39,7 @@ __export(index_exports, { module.exports = __toCommonJS(index_exports); var import_react2 = require("react"); -// client/api.js +// api.js var POCKET_RPC_CHANNEL = "/dsh-pocket"; var MOBILE_RIGHTBAR_ATTRIBUTE = "data-dsh-pocket-mobile-rightbar"; var MOBILE_RIGHTBAR_EVENT = "dsh-pocket:mobile-rightbar"; @@ -109,7 +109,7 @@ function redactStatus(s) { }; } -// client/mobile/MobileNavToggle.tsx +// mobile/MobileNavToggle.tsx var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); function MobileNavToggle({ toggleSidebar, t }) { const toggleExplorer = () => { @@ -144,11 +144,11 @@ function MobileNavToggle({ toggleSidebar, t }) { )); } -// client/mobile/MobileNavOverlay.tsx +// mobile/MobileNavOverlay.tsx var import_react = require("react"); var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); -// client/mobile/nav-targets.mjs +// mobile/nav-targets.mjs var DRAWER_SELECTOR = '[data-mobile-nav="frame"] > :first-child'; var TOGGLE_SELECTOR = '[data-mobile-nav="toggle"]'; var NAV_TARGETS = [ @@ -181,7 +181,7 @@ function isOverlayTap(target) { return target.closest(OVERLAY_SELECTOR) !== null; } -// client/mobile/MobileNavOverlay.tsx +// mobile/MobileNavOverlay.tsx var MOBILE_QUERY = "(max-width: 1023px)"; function useMobile() { const [mobile, setMobile] = (0, import_react.useState)(() => window.matchMedia(MOBILE_QUERY).matches); @@ -372,7 +372,7 @@ function MobileNavOverlay({ toggleSidebar, t }) { )); } -// client/mobile/MobileDrawerFooter.tsx +// mobile/MobileDrawerFooter.tsx var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t }) { const sessionId = useSessions((state) => state.current); @@ -408,7 +408,7 @@ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t )); } -// client/mobile/fileGuard.ts +// mobile/fileGuard.ts var GUARD_MSG = "\u624B\u673A\u4E0A\u65E0\u6CD5\u76F4\u63A5\u6253\u5F00\u7535\u8111\u4E0A\u7684\u6587\u4EF6"; var WS_LABELS = ["\u6DFB\u52A0\u5DE5\u4F5C\u533A", "\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026", "Add workspace", "Add workspace\u2026"]; var COPY_LABEL = "\u590D\u5236"; @@ -571,7 +571,7 @@ function startFileGuard(readFile) { }; } -// client/mobile/mobile.css.ts +// mobile/mobile.css.ts var MOBILE_CSS = ` /* ---------- base control styles (rendered at any width, hidden where unused) ---------- */ @@ -1562,7 +1562,7 @@ var MOBILE_CSS = ` `; -// client/mobile/locales.ts +// mobile/locales.ts var NS = "mobileNav"; var zh = { "open": "\u6253\u5F00\u76EE\u5F55", @@ -1579,7 +1579,7 @@ var en = { "files": "Files" }; -// client/mobile/layout-mode.mjs +// mobile/layout-mode.mjs function resolveLayout({ urlValue, stored, narrowMatch }) { const url = String(urlValue ?? "").trim(); if (url === "desktop") return "desktop"; @@ -1603,7 +1603,7 @@ function persistLayoutFromUrl(urlValue) { } } -// client/mobile/mobile-apply.tsx +// mobile/mobile-apply.tsx function mobileApply(ctx) { const urlValue = new URL(window.location.href).searchParams.get("dsh-layout") ?? ""; const narrowMQ = window.matchMedia("(max-width: 1023px)"); @@ -1880,7 +1880,7 @@ function mobileApply(ctx) { }, MobileDrawerFooter)); } -// client/pocket-locales.js +// pocket-locales.js var NS2 = "pocket"; var zh2 = { "section": "\u624B\u673A\u8BBF\u95EE", @@ -1945,7 +1945,7 @@ var zh2 = { "pinCustomHint": "\u81EA\u5B9A\u4E49\u540E\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0", "lanPinOff": "\u{1F513} \u5BC6\u7801\u5DF2\u5173\u95ED\uFF1A\u626B\u7801\u76F4\u8FDE\uFF0C\u65E0\u9700\u5BC6\u7801\uFF08\u4EC5\u540C\u4E00\u5C40\u57DF\u7F51\u8BBE\u5907\u53EF\u8BBF\u95EE\uFF1B\u516C\u7F51\u4ECD\u8981\u5BC6\u7801\uFF09", "trustClients": "\u8FDC\u7A0B\u8BBE\u7F6E\uFF08\u63D2\u4EF6\u914D\u7F6E / \u6A21\u578B\u7BA1\u7406\uFF09", - "trustClientsOn": "\u2705 \u624B\u673A/\u8FDC\u7A0B\u9875\u9762\u53EF\u7F16\u8F91\u8BBE\u7F6E\uFF08\u63D2\u4EF6\u914D\u7F6E\u3001\u6A21\u578B\u7BA1\u7406\u7B49\uFF09\u3002\u5B89\u5168\u8FB9\u754C\u4E3A\u8BBF\u95EE\u5BC6\u7801\u2014\u2014\u516C\u7F51\u5F3A\u5236\u3001\u5C40\u57DF\u7F51\u5EFA\u8BAE\u5F00\u542F\u3002", + "trustClientsOn": "\u2705 \u624B\u673A/\u8FDC\u7A0B\u9875\u9762\u53EF\u7F16\u8F91\u8BBE\u7F6E\uFF08\u63D2\u4EF6\u914D\u7F6E\u3001\u6A21\u578B\u7BA1\u7406\u7B49\uFF09\u3002\u662F\u5426\u5F00\u542F\u7531\u4F60\u51B3\u5B9A\u2014\u2014\u8BBF\u95EE\u5BC6\u7801\u662F\u552F\u4E00\u9632\u7EBF\u3002", "trustClientsOff": "\u{1F512} \u8FDC\u7A0B\u9875\u9762\u53EA\u8BFB\uFF1A\u8BBE\u7F6E\u8BF7\u5728\u7535\u8111\u672C\u673A\uFF08127.0.0.1\uFF09\u4FEE\u6539\u3002", "lanStarting": "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026", "mobileRightbar": "\u624B\u673A\u7AEF\u53F3\u8FB9\u680F", @@ -2046,7 +2046,7 @@ var en2 = { "pinCustomHint": "custom PINs are not rotated on tunnel start", "lanPinOff": "\u{1F513} PIN off \u2014 scan & go, no PIN (LAN devices only; public still requires PIN)", "trustClients": "Remote settings (plugin config / models)", - "trustClientsOn": "\u2705 Phone/remote pages can edit settings (plugin config, models, etc.). The access PIN is the trust boundary \u2014 mandatory for public, recommended for LAN.", + "trustClientsOn": "\u2705 Phone/remote pages can edit settings (plugin config, models, etc.). Your call on any network \u2014 the access PIN is the only safeguard.", "trustClientsOff": "\u{1F512} Remote pages are read-only: change settings on the computer itself (127.0.0.1).", "lanStarting": "Proxy starting\u2026", "mobileRightbar": "Mobile right sidebar", @@ -2085,7 +2085,7 @@ var en2 = { "feedback": "\u{1F64F} Questions? Open an issue on GitHub" }; -// client/index.jsx +// index.jsx var name = "dsh-pocket"; var inject = ["slots", "connection", "layout", "locale", "sessionLogDownload"]; function fmt(t, key, vars) { diff --git a/client/pocket-locales.js b/client/pocket-locales.js index 9a3bf26..1a25038 100644 --- a/client/pocket-locales.js +++ b/client/pocket-locales.js @@ -66,7 +66,7 @@ export const zh = { 'pinCustomHint': '自定义后开启公网不再自动换新', 'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)', 'trustClients': '远程设置(插件配置 / 模型管理)', - 'trustClientsOn': '✅ 手机/远程页面可编辑设置(插件配置、模型管理等)。安全边界为访问密码——公网强制、局域网建议开启。', + 'trustClientsOn': '✅ 手机/远程页面可编辑设置(插件配置、模型管理等)。是否开启由你决定——访问密码是唯一防线。', 'trustClientsOff': '🔒 远程页面只读:设置请在电脑本机(127.0.0.1)修改。', 'lanStarting': '代理未就绪…', 'mobileRightbar': '手机端右边栏', @@ -169,7 +169,7 @@ export const en = { 'pinCustomHint': 'custom PINs are not rotated on tunnel start', 'lanPinOff': '🔓 PIN off — scan & go, no PIN (LAN devices only; public still requires PIN)', 'trustClients': 'Remote settings (plugin config / models)', - 'trustClientsOn': '✅ Phone/remote pages can edit settings (plugin config, models, etc.). The access PIN is the trust boundary — mandatory for public, recommended for LAN.', + 'trustClientsOn': '✅ Phone/remote pages can edit settings (plugin config, models, etc.). Your call on any network — the access PIN is the only safeguard.', 'trustClientsOff': '🔒 Remote pages are read-only: change settings on the computer itself (127.0.0.1).', 'lanStarting': 'Proxy starting…', 'mobileRightbar': 'Mobile right sidebar', From 1afbebcd0e4ace12a7af771c3c073c8a23369552 Mon Sep 17 00:00:00 2001 From: Jan Eickholt Date: Thu, 17 Sep 2026 18:26:08 +0200 Subject: [PATCH 4/4] docs+ui: remote-settings switch is just a toggle for plugin config / models Strip the PIN trust-boundary wording from the switch description and the READMEs; the toggle's job is one sentence: allow remote edits or not. --- README.en.md | 4 ++-- README.md | 2 +- client/client.js | 4 ++-- client/pocket-locales.js | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.en.md b/README.en.md index 9c07273..6c97d63 100644 --- a/README.en.md +++ b/README.en.md @@ -53,7 +53,7 @@ What it looks like — the phone shows the exact same UI as your computer, live: | ⚡ Real-time sync | Streaming output passes through WebSocket untouched — what the computer renders, the phone renders live; fully interactive both ways; built-in WS heartbeat keep-alive (defeats silent NAT/battery link drops with auto-reconnect) | | 📱 Mobile-adaptive layout | Narrow screens get a drawer layout automatically (ported from dsh-web-mobile, MIT): sidebar drawer, full-width conversation, safe-area insets, touch optimizations | | 🧭 Optional right sidebar | Shows the native right-sidebar entry on mobile; disable it for a compact phone header or keep it available alongside the terminal dock on an unfolded display | -| 🛠️ Remote settings | Phone/remote pages can **edit settings** (plugin config, models, etc., issue #58). **Off by default** (remote pages are read-only; change settings on the computer itself). When on, the access PIN is the trust boundary — mandatory for public, recommended for LAN. | +| 🛠️ Remote settings | Phone/remote pages can **edit plugin config and models** (issue #58). **Off by default** (remote pages are read-only; change settings on the computer itself). | | 📁 File browser | The mobile "Files" entries need a host-side explorer panel (a dsh-web-ui component); on stock DSH without it the entries are auto-hidden instead of doing nothing | | 🗜️ Transfer compression | Large JSON responses are gzip/brotli'd on the fly (17MB session history → ~1MB; brotli quality 6: fast and bandwidth-friendly) — faster loads, less mobile data | | 🔁 Tunnel auto-restore | After a DSH restart the previously-running public tunnel comes back automatically | @@ -90,7 +90,7 @@ Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LA > > The LAN PIN is **on by default** (security-first). If you're the only user and find typing it every time annoying, flip "LAN access PIN" to **Off** in the LAN block — LAN scans then connect directly with no PIN (LAN-only devices; the **public tunnel always requires a PIN**, unaffected). > -> "**Remote settings**" is **off by default**: DSH settings (plugin config, models, etc.) are read-only on phone/remote pages — change them on the computer itself (127.0.0.1). Flip it **On** in the LAN block and the phone can edit them directly; the access PIN is then the trust boundary (public always requires it; keep the LAN PIN on on shared networks). +> "**Remote settings**" is **off by default**: plugin config and models are read-only on phone/remote pages — change them on the computer itself (127.0.0.1). Flip it **On** in the LAN block and the phone can edit plugin config and models directly. > > After logging in once, the phone **won't ask again**: as long as the computer's dsh web keeps running, reopening the phone needs no PIN (**a dsh web restart/update asks for it once more**). > diff --git a/README.md b/README.md index ca7383f..20355ac 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ npx @deepseek-ai/dsh web > > 局域网密码**默认开启**(安全优先)。如果只有自己用、嫌每次输密码麻烦,可在设置页局域网区块把「局域网访问密码」切到**关**——之后局域网扫码直连、无需密码(仅同一局域网设备可访问;**公网始终要密码**,不受影响)。 > -> 「**远程设置**」默认**关**:远程页面(手机)里的 DSH 设置(插件配置、模型管理等)为只读,改动请在电脑本机(127.0.0.1)操作。在设置页局域网区块把它切到**开**,手机即可直接编辑这些设置——信任边界为访问密码:公网始终要密码,局域网建议把「局域网访问密码」保持开启。 +> 「**远程设置**」默认**关**:远程页面(手机)里的插件配置、模型管理为只读,改动请在电脑本机(127.0.0.1)操作。在设置页局域网区块把它切到**开**,手机即可直接编辑插件配置与模型管理。 > > 手机登录一次后**长期免输**:只要电脑上的 dsh web 不重启,再次打开手机不用再输入(**dsh web 重启/更新后需重新输入一次**)。 > diff --git a/client/client.js b/client/client.js index c2c2864..f2d0288 100644 --- a/client/client.js +++ b/client/client.js @@ -1945,7 +1945,7 @@ var zh2 = { "pinCustomHint": "\u81EA\u5B9A\u4E49\u540E\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0", "lanPinOff": "\u{1F513} \u5BC6\u7801\u5DF2\u5173\u95ED\uFF1A\u626B\u7801\u76F4\u8FDE\uFF0C\u65E0\u9700\u5BC6\u7801\uFF08\u4EC5\u540C\u4E00\u5C40\u57DF\u7F51\u8BBE\u5907\u53EF\u8BBF\u95EE\uFF1B\u516C\u7F51\u4ECD\u8981\u5BC6\u7801\uFF09", "trustClients": "\u8FDC\u7A0B\u8BBE\u7F6E\uFF08\u63D2\u4EF6\u914D\u7F6E / \u6A21\u578B\u7BA1\u7406\uFF09", - "trustClientsOn": "\u2705 \u624B\u673A/\u8FDC\u7A0B\u9875\u9762\u53EF\u7F16\u8F91\u8BBE\u7F6E\uFF08\u63D2\u4EF6\u914D\u7F6E\u3001\u6A21\u578B\u7BA1\u7406\u7B49\uFF09\u3002\u662F\u5426\u5F00\u542F\u7531\u4F60\u51B3\u5B9A\u2014\u2014\u8BBF\u95EE\u5BC6\u7801\u662F\u552F\u4E00\u9632\u7EBF\u3002", + "trustClientsOn": "\u2705 \u624B\u673A/\u8FDC\u7A0B\u9875\u9762\u53EF\u7F16\u8F91\u63D2\u4EF6\u914D\u7F6E\u4E0E\u6A21\u578B\u7BA1\u7406\u3002", "trustClientsOff": "\u{1F512} \u8FDC\u7A0B\u9875\u9762\u53EA\u8BFB\uFF1A\u8BBE\u7F6E\u8BF7\u5728\u7535\u8111\u672C\u673A\uFF08127.0.0.1\uFF09\u4FEE\u6539\u3002", "lanStarting": "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026", "mobileRightbar": "\u624B\u673A\u7AEF\u53F3\u8FB9\u680F", @@ -2046,7 +2046,7 @@ var en2 = { "pinCustomHint": "custom PINs are not rotated on tunnel start", "lanPinOff": "\u{1F513} PIN off \u2014 scan & go, no PIN (LAN devices only; public still requires PIN)", "trustClients": "Remote settings (plugin config / models)", - "trustClientsOn": "\u2705 Phone/remote pages can edit settings (plugin config, models, etc.). Your call on any network \u2014 the access PIN is the only safeguard.", + "trustClientsOn": "\u2705 Phone/remote pages can edit plugin config and models.", "trustClientsOff": "\u{1F512} Remote pages are read-only: change settings on the computer itself (127.0.0.1).", "lanStarting": "Proxy starting\u2026", "mobileRightbar": "Mobile right sidebar", diff --git a/client/pocket-locales.js b/client/pocket-locales.js index 1a25038..c71032f 100644 --- a/client/pocket-locales.js +++ b/client/pocket-locales.js @@ -66,7 +66,7 @@ export const zh = { 'pinCustomHint': '自定义后开启公网不再自动换新', 'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)', 'trustClients': '远程设置(插件配置 / 模型管理)', - 'trustClientsOn': '✅ 手机/远程页面可编辑设置(插件配置、模型管理等)。是否开启由你决定——访问密码是唯一防线。', + 'trustClientsOn': '✅ 手机/远程页面可编辑插件配置与模型管理。', 'trustClientsOff': '🔒 远程页面只读:设置请在电脑本机(127.0.0.1)修改。', 'lanStarting': '代理未就绪…', 'mobileRightbar': '手机端右边栏', @@ -169,7 +169,7 @@ export const en = { 'pinCustomHint': 'custom PINs are not rotated on tunnel start', 'lanPinOff': '🔓 PIN off — scan & go, no PIN (LAN devices only; public still requires PIN)', 'trustClients': 'Remote settings (plugin config / models)', - 'trustClientsOn': '✅ Phone/remote pages can edit settings (plugin config, models, etc.). Your call on any network — the access PIN is the only safeguard.', + 'trustClientsOn': '✅ Phone/remote pages can edit plugin config and models.', 'trustClientsOff': '🔒 Remote pages are read-only: change settings on the computer itself (127.0.0.1).', 'lanStarting': 'Proxy starting…', 'mobileRightbar': 'Mobile right sidebar',