dsh-pocket-relay 1.0.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.
@@ -0,0 +1,85 @@
1
+ // 抽屉点击规则的判定常量与纯函数 —— 抽成独立 ESM 模块,好让 node 测试直接
2
+ // 引入(client 侧是 esbuild 打进 bundle 的,测试里没有 DOM 可用)。
3
+
4
+ /**
5
+ * 抽屉本体:AppFrame 的第一个网格子元素,即被拉成抽屉的侧边栏列。
6
+ * @type {string}
7
+ */
8
+ export const DRAWER_SELECTOR = '[data-mobile-nav="frame"] > :first-child'
9
+
10
+ /** 抽屉开关(会话头部那颗按钮)——点它不该被"点抽屉外就关"二次触发。 */
11
+ export const TOGGLE_SELECTOR = '[data-mobile-nav="toggle"]'
12
+
13
+ /**
14
+ * 命中即视为导航的行:点它们要把屏幕让给刚打开的内容,所以顺带关抽屉。
15
+ *
16
+ * 类名现状(2026-08,侧边栏 qDHVXG_* / YDXeBa_* 这一代):
17
+ * - `YDXeBa_sessionRow` 会话行 —— 命中 [class*="sessionRow"]
18
+ * - `YDXeBa_projectRow` 工作区行 —— **故意不收**:实测它是折叠/展开开关
19
+ * (aria-expanded 来回翻,会话列表显隐),不是导航。收进来会让每次展开
20
+ * 工作区都把抽屉关掉(issue #72)。
21
+ * 旧一代侧边栏的 `_searchResultWorkspace_` / `_searchResultRow_` 保留,兼容
22
+ * 尚未升级的宿主。
23
+ *
24
+ * @type {string}
25
+ */
26
+ export const NAV_TARGETS = [
27
+ 'button[data-dsh-taskboard-entry]',
28
+ 'button[data-dsh-ssh-entry]',
29
+ // 抽屉底部的 "文件" 入口打开的是 dsh-web-ui 的 explorer 面板,它的 z-index
30
+ // (55) 低于展开的抽屉 (600),且在抽屉 DOM 之外:抽屉不关就会盖住面板,点
31
+ // 面板里的行又会被"点抽屉外就关"吃掉。所以按导航处理,一起关掉。
32
+ '[data-mobile-nav="files"]',
33
+ '[class*="sessionRow"]',
34
+ '[class*="newSession"]',
35
+ '[class*="searchResultWorkspace"]',
36
+ '[class*="searchResultRow"]',
37
+ ].join(', ')
38
+
39
+ /**
40
+ * 行内操作按钮(会话行的 kebab:重命名 / 删除)——点开的是菜单不是内容,
41
+ * 不能按导航处理,否则抽屉一关,刚弹出的菜单跟着没了。
42
+ * @type {string}
43
+ */
44
+ export const NAV_EXCLUDE = '[class*="sessionRow"] button'
45
+
46
+ /**
47
+ * 浮层(portal 到 body 的下拉菜单 / 弹窗)。
48
+ *
49
+ * 抽屉里的控件(工作区区块的"视图选项"、"添加工作区",会话行的 kebab…)
50
+ * 弹出的菜单在 DOM 上属于 body,不在抽屉里。"点抽屉外就关"因此会把菜单项
51
+ * 的第一下点击吃掉:抽屉先滑走 → 菜单随侧边栏卸载 → 菜单项的 onClick 永远
52
+ * 不执行。手机上表现就是工作区相关菜单"点了没反应"(issue #72)。
53
+ *
54
+ * 落在浮层内的点击一律豁免,交给浮层自己收尾。
55
+ *
56
+ * @type {string}
57
+ */
58
+ export const OVERLAY_SELECTOR = [
59
+ '[role="menu"]',
60
+ '[role="listbox"]',
61
+ '[role="dialog"]',
62
+ '[role="tooltip"]',
63
+ '[data-radix-popper-content-wrapper]',
64
+ ].join(', ')
65
+
66
+ /**
67
+ * 抽屉内一次点击是否算导航(应关抽屉)。命中 NAV_EXCLUDE 的行内按钮不算。
68
+ * @param {Element | null | undefined} target - 点击目标。
69
+ * @returns {Element | null} 命中的导航行,未命中返回 null。
70
+ */
71
+ export function navTargetFor(target) {
72
+ if (target == null || typeof target.closest !== 'function') return null
73
+ if (target.closest(NAV_EXCLUDE) !== null) return null
74
+ return target.closest(NAV_TARGETS)
75
+ }
76
+
77
+ /**
78
+ * 一次点击是否落在浮层内(浮层自己会关闭,不该由抽屉规则接管)。
79
+ * @param {Element | null | undefined} target - 点击目标。
80
+ * @returns {boolean}
81
+ */
82
+ export function isOverlayTap(target) {
83
+ if (target == null || typeof target.closest !== 'function') return false
84
+ return target.closest(OVERLAY_SELECTOR) !== null
85
+ }
@@ -0,0 +1,331 @@
1
+ // dsh-pocket-relay 设置页签 i18n 词典(与 client/mobile/locales.ts 同款:zh 为源真,en 为译文)。
2
+ // 需要新增字符串时:先加到 zh(key-set 唯一来源),再补 en(key 必须与 zh 完全一致)。
3
+ export const NS = 'pocket'
4
+
5
+ /** Simplified Chinese dictionary (the key-set source of truth). */
6
+ export const zh = {
7
+ 'section': '手机访问',
8
+ 'title': '📱 手机访问',
9
+ 'subtitle': '手机扫码打开的就是电脑上的这个界面,实时同步',
10
+ 'developer': '基于 dsh-pocket 二次开发(GPL-2.0)',
11
+ 'starAsk': '⭐ 顺手留颗 Star,作者能高兴一整天',
12
+ 'starCta': '行,给你一颗 Star',
13
+ 'restarted': '🔄 已重启',
14
+ 'ok': '知道了',
15
+ 'bgHint': '进程在后台运行(不挂终端)。如需停止:{cmd}',
16
+ 'updatedRestart': '✅ 已更新 v{ver},重启生效',
17
+ 'updateAutoRestarting': '✅ 已更新 v{ver},正在自动重启…',
18
+ 'updatedOk': '✅ 已更新 v{ver}',
19
+ 'updateAvailable': '📦 新版本 v{ver}',
20
+ 'updating': '更新中…',
21
+ 'updateTo': '更新到 v{ver}',
22
+ 'restartingNow': '正在重启生效…',
23
+ 'restarting': '重启中…',
24
+ 'restartNow': '🔄 重启 dsh web 生效',
25
+ 'updatingDetail': '⏳ 更新中(通常 1-2 分钟)· 已等待 {s} 秒',
26
+ 'restartingDetail': '⏳ 正在重启生效(通常 10-30 秒)· 已等待 {s} 秒',
27
+ 'updatedAutoDetail': '✅ 已更新,正在自动重启生效,请稍候刷新',
28
+ 'updatedRestartDetail': '✅ 已更新,重启 dsh web 生效',
29
+ 'updateFailed': '❌ 失败:{err}(手动更新:dsh plugin --profile web update dsh-pocket-relay --latest -w)',
30
+ 'versionRange': '当前 v{cur} → 最新 v{latest}',
31
+ 'wanAccess': '公网访问',
32
+ 'pinLabel': '访问密码',
33
+ 'modeLabel': '地址模式',
34
+ 'advAddress': '高级 · 手动选地址',
35
+ 'wanOffHint': '开启后可从任何网络访问(每次开启需确认免责声明)',
36
+ 'resetFactory': '🧹 恢复出厂设置',
37
+ 'resetGo': '恢复',
38
+ 'resetIntro': '设置搞出问题时的临时兜底:清空本机配置并重设随机密码(DSH 的会话、模型、插件配置不受影响)',
39
+ 'resetTitle': '⚠️ 确认恢复出厂设置?',
40
+ 'resetBody': '将清空并恢复默认:\n① 开关:局域网访问=开、访问密码=开、局域网地址=自动\n② 公网:模式回到随机域名,清空 Tunnel Token 与固定域名,并关闭正在运行的公网\n③ 密码:公网和局域网都换成新的随机 8 位密码(旧密码立即作废,手机需重新输入)\n\nDSH 自身的会话、模型、插件配置不受影响;此操作不可撤销。',
41
+ 'resetConfirm': '确认恢复',
42
+ 'resetDone': '✅ 已恢复出厂设置:设置已清空,密码已换新(手机需重新输入)',
43
+ 'resetFailed': '❌ 恢复失败,请重试',
44
+ 'lanTitle': '📶 局域网(同一 WiFi)',
45
+ 'lanHint': '手机连接同一 WiFi 后扫码即可打开',
46
+ 'lanAccess': '局域网访问',
47
+ 'lanDisabledHint': '🔒 局域网访问已关闭:手机扫码/链接均不可用(公网不受影响)。点「开」恢复。',
48
+ 'lanToggleTitleOff': '关闭局域网访问',
49
+ 'lanToggleBodyOff': '关闭后,同一 WiFi 下的手机将无法扫码访问(局域网二维码/链接立即失效)。公网访问不受影响。确定关闭?',
50
+ 'lanToggleTitleOn': '开启局域网访问',
51
+ 'lanToggleBodyOn': '开启后,同一 WiFi 的手机扫码即可访问(默认需输入局域网密码)。确定开启?',
52
+ 'confirm': '确定',
53
+ 'lanAddress': '局域网地址',
54
+ 'lanAddressAuto': '自动(推荐)',
55
+ 'lanPin': '局域网访问密码',
56
+ 'on': '开',
57
+ 'off': '关',
58
+ 'lanPinValue': '🔐 访问密码:{pin}(手机打开需输入;与公网密码分开)',
59
+ 'lanPinCustomValue': '🔐 访问密码:{pin}(自定义;手机打开需输入)',
60
+ 'refresh': '刷新',
61
+ 'customize': '自定义',
62
+ 'customizing': '新密码(8 位,英文字母或数字):',
63
+ 'save': '保存',
64
+ 'cancel': '取消',
65
+ 'pinInvalid': '密码必须是 8 位英文字母或数字',
66
+ 'pinCustomHint': '自定义后开启公网不再自动换新',
67
+ 'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)',
68
+ 'lanStarting': '代理未就绪…',
69
+ 'wanTitle': '🌐 公网(人在外面)',
70
+ 'wanHint': '任何网络扫码即用(URL 每次重启自动换新)',
71
+ 'wanPin': '🔐 访问密码:{pin}(每次开启公网变新;手机打开链接需输入此密码)',
72
+ 'wanPinCustom': '🔐 访问密码:{pin}(自定义,开启公网不再自动换新)',
73
+ 'wanEphemeralWarn': '⚠️ 公网链接仅在本次开启期间有效:关闭或重启后失效,并可能被他人复用为陌生网站。请勿收藏,每次从本页扫「当前」二维码。需要固定不变的地址请用下方「固定域名」。',
74
+ 'stopTunnel': '关闭公网',
75
+ 'enable': '开启公网访问',
76
+ 'opening': '开启中…',
77
+ 'tunnelMode': '公网模式:',
78
+ 'modeQuick': '随机域名(默认)',
79
+ 'modeNamed': '固定域名',
80
+ 'namedSummary': '固定域名:{host} · Token {token}',
81
+ 'namedTokenSet': '已配置',
82
+ 'namedTokenMissing': '未配置',
83
+ 'namedEdit': '修改',
84
+ 'namedHostnameLabel': '固定域名:',
85
+ 'namedTokenLabel': 'Tunnel Token(留空 = 保持不变):',
86
+ 'namedHow': '在 Cloudflare Zero Trust → Networks → Tunnels 创建隧道并复制 Token;把域名的 Service 指向 http://127.0.0.1:3081(代理端口)。地址固定,重启不再变化。',
87
+ 'namedSecurity': '固定域名长期暴露在公网、更易被扫描,建议同时设置自定义强密码(本模式公网密码默认不随重启轮换)。',
88
+ 'namedNeedCfg': '请先填写固定域名与 Tunnel Token',
89
+ 'namedRunningHint': '固定域名(Cloudflare 命名隧道)——地址不随重启变化',
90
+ 'namedTakeEffect': '已保存固定域名配置——需关闭并重新开启公网访问后生效',
91
+ 'disclaimerTitle': '⚠️ 安全免责声明',
92
+ 'disclaimerBody': '开启公网 = 把本机 DSH(能执行代码)暴露到互联网。任何人拿到公网链接和密码,都能访问甚至操作你的电脑。请确认:① 使用自定义强密码或妥善保管自动密码;② 用完立即「关闭公网」;③ 公司/涉密网络请先确认合规。',
93
+ 'disclaimerAgree': '我已知情,同意开启',
94
+ 'disclaimerHint': '请勾选「我已知情」后再开启公网',
95
+ 'downloading': '⏳ 下载 cloudflared(首次约 20-50MB,通常 1-2 分钟;之后秒开)· 已等待 {s} 秒',
96
+ 'connecting': '⏳ 连接 Cloudflare 边缘(通常 5-30 秒)· 已等待 {s} 秒{suffix}',
97
+ 'slowHint': ' — 有点久?检查是否开着代理/VPN(Clash TUN 等)',
98
+ 'error': '❌ 开启失败:{detail}(可重试;若是代理/VPN 问题见 README 排障)',
99
+ 'unknownError': '未知错误',
100
+ 'feedback': '有问题?欢迎到 GitHub Issues 反馈 🙏',
101
+
102
+ // ---------- 中继(自建服务器) ----------
103
+ 'relayTitle': '🛰 中继(自建服务器)',
104
+ 'relayIntro': '用自己的服务器中转,地址固定、不依赖第三方。配置好即常开:dsh 一起来就在线,不用每次手动开。',
105
+ 'relayStateOnline': '🟢 已连接',
106
+ 'relayStateConnecting': '🟠 连接中…',
107
+ 'relayStateError': '🔴 未连接',
108
+ 'relayStateStopped': '⚪ 已暂停',
109
+ 'relayStateIdle': '⚪ 未启用',
110
+ 'relayHostLabel': '服务器地址',
111
+ 'relayHostPh': 'relay.example.com',
112
+ 'relayPortLabel': 'agent 端口',
113
+ 'relayPublicUrlLabel': '对外访问地址',
114
+ 'relayPublicUrlPh': 'https://relay.example.com:8443',
115
+ 'relayPublicUrlHint': '手机扫码和收藏用这个地址。relay 自己终结 TLS,所以填 https:// 加端口。',
116
+ 'relayTokenLabel': 'Token(留空 = 保持不变)',
117
+ 'relayTokenSet': '已配置',
118
+ 'relayTokenMissing': '未配置',
119
+ 'relayAgentIdLabel': '本机名称',
120
+ 'relayAgentIdPh': '留空 = 用电脑主机名',
121
+ 'relayAgentIdHint': '同一台中继上挂多台电脑时靠它区分(只允许字母、数字、- _ .)。留空就用主机名——同一台中继上多台电脑不能同名,否则会互相顶下线。',
122
+ 'relayThisPc': '本机名称:{id}',
123
+ 'relayTlsLabel': 'agent 通道使用 TLS',
124
+ 'relayTlsHint': 'agent 通道承载你和电脑之间的全部明文流量,除非走 Tailscale/WireGuard 内网,否则建议开启。',
125
+ 'relayInsecureLabel': '允许自签名证书',
126
+ 'relayInsecureHint': '⚠️ 等于放弃中间人防护,仅在完全可信的网络里用。',
127
+ 'relaySave': '保存并连接',
128
+ 'relaySaving': '保存中…',
129
+ 'relaySaved': '✅ 中继配置已保存',
130
+ 'relaySaveFailed': '❌ 保存失败:{err}',
131
+ 'relayNeedCfg': '请先填写服务器地址、agent 端口、对外访问地址和 Token',
132
+ 'relayPause': '暂停远程',
133
+ 'relayResume': '恢复远程',
134
+ 'relayReconnects': '已重连 {n} 次',
135
+ 'relayStreams': '进行中的连接:{n}',
136
+ 'relayConsentTitle': '⚠️ 常开远程访问的安全声明',
137
+ 'relayConsentBody': '开启后,只要 dsh 在运行,这台电脑就会**长期**挂在中继服务器上,任何人在公网上都能尝试连接。\n\n① 请确认已设置访问密码或完成设备配对;\n② 中继通道使用设备认证:每台设备一个独立密码,可单独撤销;\n③ 手机丢失请在电脑上「撤销」那台设备(不是改密码);\n④ 公司/涉密网络请先确认合规。',
138
+ 'relayConsentAgree': '我已知情,同意常开',
139
+ 'relayConsentHint': '请先勾选「我已知情」',
140
+ 'relayQrHint': '手机用这个地址访问(需先完成配对)',
141
+
142
+ // ---------- 设备管理 ----------
143
+ 'deviceTitle': '已配对设备',
144
+ 'deviceIntro': '中继通道用设备认证:每台手机一个独立密码,丢了一台只撤一台。',
145
+ 'deviceAdd': '添加设备',
146
+ 'deviceAdding': '生成中…',
147
+ 'deviceNone': '还没有已配对的设备',
148
+ 'devicePendingTitle': '待批准',
149
+ 'devicePendingNone': '没有待批准的设备',
150
+ 'deviceApprove': '批准',
151
+ 'deviceReject': '拒绝',
152
+ 'deviceRevoke': '撤销',
153
+ 'deviceName': '名称',
154
+ 'deviceCreatedAt': '添加时间',
155
+ 'deviceLastLogin': '最后登录',
156
+ 'deviceNever': '从未登录',
157
+ 'devicePairTitle': '📱 配对二维码',
158
+ 'devicePairHint': '手机扫码后设置自己的设备密码,再回来点「批准」。二维码 5 分钟内有效,且只能用一次。',
159
+ 'devicePairExpire': '过期时间:{time}',
160
+ 'devicePairCancel': '取消配对',
161
+ 'deviceRevokeConfirm': '撤销后该设备需重新配对才能访问,确定继续?',
162
+ 'deviceRevokeDone': '✅ 已撤销该设备',
163
+ 'deviceApprovedDone': '✅ 已批准该设备',
164
+ 'deviceRejectedDone': '✅ 已拒绝该设备',
165
+ 'deviceNamePh': '如:我的 iPhone',
166
+ 'deviceCount': '已配对 {n} 台',
167
+ }
168
+
169
+ /** English dictionary, key-identical to the Chinese source of truth. */
170
+ export const en = {
171
+ 'section': 'Phone access',
172
+ 'title': '📱 Phone access',
173
+ 'subtitle': 'The phone shows this exact screen, live',
174
+ 'developer': 'Built on dsh-pocket (GPL-2.0)',
175
+ 'starAsk': '⭐ Drop a Star if it helped — it makes the author’s day',
176
+ 'starCta': '★ Give a Star',
177
+ 'restarted': '🔄 Restarted',
178
+ 'ok': 'Got it',
179
+ 'bgHint': 'Running in the background (not attached to a terminal). To stop: {cmd}',
180
+ 'updatedRestart': '✅ Updated to v{ver} — restart to apply',
181
+ 'updateAutoRestarting': '✅ Updated to v{ver} — auto-restarting…',
182
+ 'updatedOk': '✅ Updated to v{ver}',
183
+ 'updateAvailable': '📦 Update available: v{ver}',
184
+ 'updating': 'Updating…',
185
+ 'updateTo': 'Update to v{ver}',
186
+ 'restartingNow': 'Restarting to apply…',
187
+ 'restarting': 'Restarting…',
188
+ 'restartNow': '🔄 Restart dsh web now',
189
+ 'updatingDetail': '⏳ Updating (usually 1-2 min) · {s}s elapsed',
190
+ 'restartingDetail': '⏳ Restarting to apply (usually 10-30s) · {s}s elapsed',
191
+ 'updatedAutoDetail': '✅ Updated — auto-restarting in progress, refresh shortly',
192
+ 'updatedRestartDetail': '✅ Updated — restart dsh web to apply',
193
+ 'updateFailed': '❌ Failed: {err} (manual update: dsh plugin --profile web update dsh-pocket-relay --latest -w)',
194
+ 'versionRange': 'Current v{cur} → latest v{latest}',
195
+ 'wanAccess': 'Public access',
196
+ 'pinLabel': 'Access PIN',
197
+ 'modeLabel': 'Address mode',
198
+ 'advAddress': 'Advanced · Pick address',
199
+ 'wanOffHint': 'Reachable from any network once enabled (a disclaimer is confirmed on each enable)',
200
+ 'resetFactory': '🧹 Factory reset',
201
+ 'resetGo': 'Reset',
202
+ 'resetIntro': 'Temporary fallback when settings break: clear local config and re-roll random PINs (DSH sessions, models and plugin config are untouched)',
203
+ 'resetTitle': '⚠️ Confirm factory reset?',
204
+ 'resetBody': 'This clears and restores defaults:\n① Switches: LAN access on, access PIN on, LAN address auto\n② Public: mode back to random URL, Tunnel Token and fixed domain cleared, and any running tunnel is stopped\n③ PINs: both public and LAN become new random 8-character PINs (old ones stop working; the phone must re-enter)\n\nYour DSH sessions, models and plugin config are untouched. This cannot be undone.',
205
+ 'resetConfirm': 'Reset',
206
+ 'resetDone': '✅ Factory reset done: settings cleared and PINs re-rolled (re-enter the PIN on your phone)',
207
+ 'resetFailed': '❌ Reset failed — please retry',
208
+ 'lanTitle': '📶 LAN (same Wi-Fi)',
209
+ 'lanHint': 'Scan to open once your phone is on the same Wi-Fi',
210
+ 'lanAccess': 'LAN access',
211
+ 'lanDisabledHint': '🔒 LAN access is off — the QR code and link are unavailable (public access is unaffected). Tap "On" to restore.',
212
+ 'lanToggleTitleOff': 'Turn off LAN access',
213
+ 'lanToggleBodyOff': 'Once off, phones on the same Wi-Fi can no longer scan to connect (the LAN QR code and link stop working immediately). Public access is unaffected. Turn it off?',
214
+ 'lanToggleTitleOn': 'Turn on LAN access',
215
+ 'lanToggleBodyOn': 'Once on, phones on the same Wi-Fi can scan to connect (a LAN PIN is required by default). Turn it on?',
216
+ 'confirm': 'Confirm',
217
+ 'lanAddress': 'LAN address',
218
+ 'lanAddressAuto': 'Auto (recommended)',
219
+ 'lanPin': 'LAN access PIN',
220
+ 'on': 'On',
221
+ 'off': 'Off',
222
+ 'lanPinValue': '🔐 PIN: {pin} (required on the phone; separate from the public PIN)',
223
+ 'lanPinCustomValue': '🔐 PIN: {pin} (custom; required on the phone)',
224
+ 'refresh': 'Refresh',
225
+ 'customize': 'Customize',
226
+ 'customizing': 'New PIN (8 chars, letters/digits): ',
227
+ 'save': 'Save',
228
+ 'cancel': 'Cancel',
229
+ 'pinInvalid': 'PIN must be exactly 8 characters (letters and digits only)',
230
+ 'pinCustomHint': 'custom PINs are not rotated on tunnel start',
231
+ 'lanPinOff': '🔓 PIN off — scan & go, no PIN (LAN devices only; public still requires PIN)',
232
+ 'lanStarting': 'Proxy starting…',
233
+ 'wanTitle': '🌐 Anywhere (public)',
234
+ 'wanHint': 'Scan from any network (the URL changes on every restart)',
235
+ 'wanPin': '🔐 PIN: {pin} (changes each time the tunnel is enabled; required on the phone)',
236
+ 'wanPinCustom': '🔐 PIN: {pin} (custom — not rotated on tunnel start)',
237
+ 'wanEphemeralWarn': '⚠️ The public link is valid only for this session: it stops working after you close or restart, and may be reused by someone else for an unrelated site. Do not bookmark it — scan the CURRENT QR code from this page each time. For a permanent address use "Fixed domain" below.',
238
+ 'stopTunnel': 'Stop',
239
+ 'enable': 'Enable anywhere',
240
+ 'opening': 'Enabling…',
241
+ 'tunnelMode': 'Mode:',
242
+ 'modeQuick': 'Random URL (default)',
243
+ 'modeNamed': 'Fixed domain',
244
+ 'namedSummary': 'Fixed domain: {host} · Token {token}',
245
+ 'namedTokenSet': 'configured',
246
+ 'namedTokenMissing': 'not set',
247
+ 'namedEdit': 'Edit',
248
+ 'namedHostnameLabel': 'Fixed domain:',
249
+ 'namedTokenLabel': 'Tunnel Token (blank = keep current):',
250
+ 'namedHow': 'Create a tunnel in Cloudflare Zero Trust → Networks → Tunnels and copy the token; point the hostname\'s Service at http://127.0.0.1:3081 (the proxy port). The URL stays fixed across restarts.',
251
+ 'namedSecurity': 'A fixed domain is long-lived and easier to scan — set a strong custom PIN too (the public PIN is not rotated on restart in this mode).',
252
+ 'namedNeedCfg': 'Set the fixed domain and Tunnel Token first',
253
+ 'namedRunningHint': 'Fixed domain (Cloudflare named tunnel) — the URL no longer changes on restart',
254
+ 'namedTakeEffect': 'Fixed-domain config saved — turn public access off and on again to take effect',
255
+ 'disclaimerTitle': '⚠️ Security disclaimer',
256
+ 'disclaimerBody': 'Enabling public access exposes this computer\u2019s DSH (which can execute code) to the internet. Anyone with the public link and PIN can reach — and operate — your computer. Please confirm: ① use a strong custom PIN or keep the auto-generated one safe; ② turn public access OFF as soon as you\u2019re done; ③ on a corporate/classified network, confirm compliance first.',
257
+ 'disclaimerAgree': 'I understand and agree',
258
+ 'disclaimerHint': 'Check "I understand" before enabling public access',
259
+ 'downloading': '⏳ Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) · {s}s elapsed',
260
+ 'connecting': '⏳ Connecting to Cloudflare edge (usually 5-30s) · {s}s elapsed{suffix}',
261
+ 'slowHint': ' — taking long? Check for a proxy/VPN (e.g., Clash TUN)',
262
+ 'error': '❌ Failed to enable: {detail} (you can retry; for proxy/VPN issues see the README)',
263
+ 'unknownError': 'unknown error',
264
+ 'feedback': '🙏 Questions? Open an issue on GitHub',
265
+
266
+ // ---------- Relay (self-hosted server) ----------
267
+ 'relayTitle': '🛰 Relay (self-hosted)',
268
+ 'relayIntro': 'Route through your own server: fixed address, no third party. Once configured it stays on — as long as dsh runs, you are reachable.',
269
+ 'relayStateOnline': '🟢 Connected',
270
+ 'relayStateConnecting': '🟠 Connecting…',
271
+ 'relayStateError': '🔴 Not connected',
272
+ 'relayStateStopped': '⚪ Paused',
273
+ 'relayStateIdle': '⚪ Not enabled',
274
+ 'relayHostLabel': 'Server address',
275
+ 'relayHostPh': 'relay.example.com',
276
+ 'relayPortLabel': 'Agent port',
277
+ 'relayPublicUrlLabel': 'Public URL',
278
+ 'relayPublicUrlPh': 'https://relay.example.com:8443',
279
+ 'relayPublicUrlHint': 'The phone scans/bookmarks this. The relay terminates TLS itself, so use https:// plus the port.',
280
+ 'relayTokenLabel': 'Token (blank = keep current)',
281
+ 'relayTokenSet': 'configured',
282
+ 'relayTokenMissing': 'not set',
283
+ 'relayAgentIdLabel': 'This PC name',
284
+ 'relayAgentIdPh': 'blank = computer hostname',
285
+ 'relayAgentIdHint': 'Distinguishes multiple PCs behind one relay (letters, digits, - _ . only). Blank uses the hostname — two PCs on one relay must not share a name, or they will keep kicking each other off.',
286
+ 'relayThisPc': 'This PC: {id}',
287
+ 'relayTlsLabel': 'Use TLS for the agent channel',
288
+ 'relayTlsHint': 'The agent channel carries all plaintext traffic between you and the PC. Keep it on unless you are on a Tailscale/WireGuard network.',
289
+ 'relayInsecureLabel': 'Allow self-signed certificate',
290
+ 'relayInsecureHint': '⚠️ Gives up man-in-the-middle protection. Only on a fully trusted network.',
291
+ 'relaySave': 'Save & connect',
292
+ 'relaySaving': 'Saving…',
293
+ 'relaySaved': '✅ Relay settings saved',
294
+ 'relaySaveFailed': '❌ Save failed: {err}',
295
+ 'relayNeedCfg': 'Fill in the server address, agent port, public URL and token first',
296
+ 'relayPause': 'Pause remote',
297
+ 'relayResume': 'Resume remote',
298
+ 'relayReconnects': 'Reconnects: {n}',
299
+ 'relayStreams': 'Active connections: {n}',
300
+ 'relayConsentTitle': '⚠️ Always-on remote access',
301
+ 'relayConsentBody': 'Once enabled, this computer stays reachable through the relay server for as long as dsh is running, and anyone on the internet can attempt to connect.\n\n① Make sure an access PIN is set or device pairing is complete;\n② The relay channel uses device auth: each device has its own password and can be revoked individually;\n③ If a phone is lost, revoke THAT device on the computer (do not just change a password);\n④ On a corporate/classified network, confirm compliance first.',
302
+ 'relayConsentAgree': 'I understand, enable always-on',
303
+ 'relayConsentHint': 'Check "I understand" first',
304
+ 'relayQrHint': 'Open this address on your phone (pairing required first)',
305
+
306
+ // ---------- Device management ----------
307
+ 'deviceTitle': 'Paired devices',
308
+ 'deviceIntro': 'The relay channel uses device auth: each phone has its own password, so losing one only affects that one.',
309
+ 'deviceAdd': 'Add device',
310
+ 'deviceAdding': 'Generating…',
311
+ 'deviceNone': 'No paired devices yet',
312
+ 'devicePendingTitle': 'Pending approval',
313
+ 'devicePendingNone': 'Nothing waiting for approval',
314
+ 'deviceApprove': 'Approve',
315
+ 'deviceReject': 'Reject',
316
+ 'deviceRevoke': 'Revoke',
317
+ 'deviceName': 'Name',
318
+ 'deviceCreatedAt': 'Added',
319
+ 'deviceLastLogin': 'Last login',
320
+ 'deviceNever': 'never',
321
+ 'devicePairTitle': '📱 Pairing QR code',
322
+ 'devicePairHint': 'Scan on the phone, set its own device password, then come back and tap Approve. The code is valid for 5 minutes and single-use.',
323
+ 'devicePairExpire': 'Expires: {time}',
324
+ 'devicePairCancel': 'Cancel pairing',
325
+ 'deviceRevokeConfirm': 'The device will need to pair again to regain access. Continue?',
326
+ 'deviceRevokeDone': '✅ Device revoked',
327
+ 'deviceApprovedDone': '✅ Device approved',
328
+ 'deviceRejectedDone': '✅ Device rejected',
329
+ 'deviceNamePh': 'e.g. My iPhone',
330
+ 'deviceCount': '{n} paired',
331
+ }
@@ -0,0 +1,11 @@
1
+ # dsh-pocket-relay bundle patch(dsh.bundle manifest 要求的加载配置)
2
+ # 单包单插件:装一次,设置页「手机访问」全部功能到位。
3
+ #
4
+ # 注意 id 与 name 的分工(别随手改成一样的):
5
+ # - name:loader 据此解析 npm 包名,必须与 package.json 的 name 一致;
6
+ # - id:市场「热禁用 / 启用」开关写 profile 补丁时认的就是它。刻意保留
7
+ # dsh-pocket 这个历史 id,这样从旧包名升级上来的用户,原有的启用/停用
8
+ # 状态不会失配。
9
+ - insert:
10
+ - id: dsh-pocket
11
+ name: dsh-pocket-relay