@guowenzhang/dsh-pocket 0.0.0-stage → 2.12.2

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,117 @@
1
+ // 抽屉点击规则的判定常量与纯函数 —— 抽成独立 ESM 模块,好让 node 测试直接
2
+ // 引入(client 侧是 esbuild 打进 bundle 的,测试里没有 DOM 可用)。
3
+
4
+ /**
5
+ * 抽屉本体(宿主自带抽屉的版本,DSH 0.2.2 起):AppFrame 内部结构变了——
6
+ * 宽 < 768 时宿主自己把侧边栏列拉成抽屉,并在第一个网格位置插入一个
7
+ * `drawerMask`(点它关闭抽屉的全屏遮罩)。所以「第一个子元素」不再等于侧边栏,
8
+ * 只能按列名找:CSS Module 的哈希前缀每次构建都变,后缀 `_sidebarCol` 稳定。
9
+ * @type {string}
10
+ */
11
+ export const DRAWER_COLUMN_SELECTOR = '[data-mobile-nav="frame"] > [class$="_sidebarCol"]'
12
+
13
+ /**
14
+ * 抽屉本体的旧口径:没有自带抽屉的宿主里,第一个网格子元素就是侧边栏列。
15
+ * @type {string}
16
+ */
17
+ export const LEGACY_DRAWER_SELECTOR = '[data-mobile-nav="frame"] > :first-child'
18
+
19
+ /**
20
+ * 当前页面的抽屉根节点:优先按列名找(新宿主),找不到再退回第一个子元素
21
+ * (老宿主)。两代结构都在用,不能只留一条选择器。
22
+ * @param {Document | Element | null} [root] - 查询根,默认当前 document。
23
+ * @returns {Element | null} 抽屉根节点。
24
+ */
25
+ export function findDrawer(root) {
26
+ const scope = root ?? (typeof document === 'undefined' ? null : document)
27
+ if (scope === null || typeof scope.querySelector !== 'function') return null
28
+ return scope.querySelector(DRAWER_COLUMN_SELECTOR) ?? scope.querySelector(LEGACY_DRAWER_SELECTOR)
29
+ }
30
+
31
+ /**
32
+ * 宿主是否自带移动端抽屉(0.2.2 起)。自带时网格改写、抽屉定位、点外部关闭
33
+ * 全部交给宿主,插件再插手就是两套抽屉打架。
34
+ * @param {Element | null} frame - AppFrame 元素。
35
+ * @returns {boolean}
36
+ */
37
+ export function hasNativeDrawer(frame) {
38
+ if (frame == null || typeof frame.hasAttribute !== 'function') return false
39
+ return frame.hasAttribute('data-sidebar-drawer')
40
+ }
41
+
42
+ /** 抽屉开关(会话头部那颗按钮)——点它不该被"点抽屉外就关"二次触发。 */
43
+ export const TOGGLE_SELECTOR = '[data-mobile-nav="toggle"]'
44
+
45
+ /**
46
+ * 命中即视为导航的行:点它们要把屏幕让给刚打开的内容,所以顺带关抽屉。
47
+ *
48
+ * 类名现状(2026-08,侧边栏 qDHVXG_* / YDXeBa_* 这一代):
49
+ * - `YDXeBa_sessionRow` 会话行 —— 命中 [class*="sessionRow"]
50
+ * - `YDXeBa_projectRow` 工作区行 —— **故意不收**:实测它是折叠/展开开关
51
+ * (aria-expanded 来回翻,会话列表显隐),不是导航。收进来会让每次展开
52
+ * 工作区都把抽屉关掉(issue #72)。
53
+ * 旧一代侧边栏的 `_searchResultWorkspace_` / `_searchResultRow_` 保留,兼容
54
+ * 尚未升级的宿主。
55
+ *
56
+ * @type {string}
57
+ */
58
+ export const NAV_TARGETS = [
59
+ 'button[data-dsh-taskboard-entry]',
60
+ 'button[data-dsh-ssh-entry]',
61
+ // 抽屉底部的 "文件" 入口打开的是 dsh-web-ui 的 explorer 面板,它的 z-index
62
+ // (55) 低于展开的抽屉 (600),且在抽屉 DOM 之外:抽屉不关就会盖住面板,点
63
+ // 面板里的行又会被"点抽屉外就关"吃掉。所以按导航处理,一起关掉。
64
+ '[data-mobile-nav="files"]',
65
+ '[class*="sessionRow"]',
66
+ '[class*="newSession"]',
67
+ '[class*="searchResultWorkspace"]',
68
+ '[class*="searchResultRow"]',
69
+ ].join(', ')
70
+
71
+ /**
72
+ * 行内操作按钮(会话行的 kebab:重命名 / 删除)——点开的是菜单不是内容,
73
+ * 不能按导航处理,否则抽屉一关,刚弹出的菜单跟着没了。
74
+ * @type {string}
75
+ */
76
+ export const NAV_EXCLUDE = '[class*="sessionRow"] button'
77
+
78
+ /**
79
+ * 浮层(portal 到 body 的下拉菜单 / 弹窗)。
80
+ *
81
+ * 抽屉里的控件(工作区区块的"视图选项"、"添加工作区",会话行的 kebab…)
82
+ * 弹出的菜单在 DOM 上属于 body,不在抽屉里。"点抽屉外就关"因此会把菜单项
83
+ * 的第一下点击吃掉:抽屉先滑走 → 菜单随侧边栏卸载 → 菜单项的 onClick 永远
84
+ * 不执行。手机上表现就是工作区相关菜单"点了没反应"(issue #72)。
85
+ *
86
+ * 落在浮层内的点击一律豁免,交给浮层自己收尾。
87
+ *
88
+ * @type {string}
89
+ */
90
+ export const OVERLAY_SELECTOR = [
91
+ '[role="menu"]',
92
+ '[role="listbox"]',
93
+ '[role="dialog"]',
94
+ '[role="tooltip"]',
95
+ '[data-radix-popper-content-wrapper]',
96
+ ].join(', ')
97
+
98
+ /**
99
+ * 抽屉内一次点击是否算导航(应关抽屉)。命中 NAV_EXCLUDE 的行内按钮不算。
100
+ * @param {Element | null | undefined} target - 点击目标。
101
+ * @returns {Element | null} 命中的导航行,未命中返回 null。
102
+ */
103
+ export function navTargetFor(target) {
104
+ if (target == null || typeof target.closest !== 'function') return null
105
+ if (target.closest(NAV_EXCLUDE) !== null) return null
106
+ return target.closest(NAV_TARGETS)
107
+ }
108
+
109
+ /**
110
+ * 一次点击是否落在浮层内(浮层自己会关闭,不该由抽屉规则接管)。
111
+ * @param {Element | null | undefined} target - 点击目标。
112
+ * @returns {boolean}
113
+ */
114
+ export function isOverlayTap(target) {
115
+ if (target == null || typeof target.closest !== 'function') return false
116
+ return target.closest(OVERLAY_SELECTOR) !== null
117
+ }
@@ -0,0 +1,165 @@
1
+ // dsh-pocket 设置页签 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
+ 'restarted': '🔄 已重启',
11
+ 'ok': '知道了',
12
+ 'bgHint': '进程在后台运行(不挂终端)。如需停止:{cmd}',
13
+ 'wanAccess': '公网访问',
14
+ 'pinLabel': '访问密码',
15
+ 'modeLabel': '地址模式',
16
+ 'advAddress': '高级 · 手动选地址',
17
+ 'wanOffHint': '开启后可从任何网络访问(每次开启需确认免责声明)',
18
+ 'resetFactory': '🧹 恢复出厂设置',
19
+ 'resetGo': '恢复',
20
+ 'resetIntro': '设置搞出问题时的临时兜底:清空本机配置并重设随机密码(DSH 的会话、模型、插件配置不受影响)',
21
+ 'resetTitle': '⚠️ 确认恢复出厂设置?',
22
+ 'resetBody': '将清空并恢复默认:\n① 开关:局域网访问=开、访问密码=开、手机端右边栏=开、局域网地址=自动\n② 公网:模式回到随机域名,清空 Tunnel Token 与固定域名,并关闭正在运行的公网\n③ 密码:公网和局域网都换成新的随机 8 位密码(旧密码立即作废,手机需重新输入)\n\nDSH 自身的会话、模型、插件配置不受影响;此操作不可撤销。',
23
+ 'resetConfirm': '确认恢复',
24
+ 'resetDone': '✅ 已恢复出厂设置:设置已清空,密码已换新(手机需重新输入)',
25
+ 'resetFailed': '❌ 恢复失败,请重试',
26
+ 'lanTitle': '📶 局域网(同一 WiFi)',
27
+ 'lanHint': '手机连接同一 WiFi 后扫码即可打开',
28
+ 'lanAccess': '局域网访问',
29
+ 'lanDisabledHint': '🔒 局域网访问已关闭:手机扫码/链接均不可用(公网不受影响)。点「开」恢复。',
30
+ 'lanToggleTitleOff': '关闭局域网访问',
31
+ 'lanToggleBodyOff': '关闭后,同一 WiFi 下的手机将无法扫码访问(局域网二维码/链接立即失效)。公网访问不受影响。确定关闭?',
32
+ 'lanToggleTitleOn': '开启局域网访问',
33
+ 'lanToggleBodyOn': '开启后,同一 WiFi 的手机扫码即可访问(默认需输入局域网密码)。确定开启?',
34
+ 'confirm': '确定',
35
+ 'lanAddress': '局域网地址',
36
+ 'lanAddressAuto': '自动(推荐)',
37
+ 'lanPin': '局域网访问密码',
38
+ 'on': '开',
39
+ 'off': '关',
40
+ 'lanPinValue': '🔐 访问密码:{pin}(手机打开需输入;与公网密码分开)',
41
+ 'lanPinCustomValue': '🔐 访问密码:{pin}(自定义;手机打开需输入)',
42
+ 'refresh': '刷新',
43
+ 'customize': '自定义',
44
+ 'customizing': '新密码(8–64 位,英文字母或数字):',
45
+ 'save': '保存',
46
+ 'cancel': '取消',
47
+ 'pinInvalid': '密码必须是 8–64 位英文字母或数字',
48
+ 'pinCustomHint': '自定义后开启公网不再自动换新',
49
+ 'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)',
50
+ 'lanStarting': '代理未就绪…',
51
+ 'mobileRightbar': '手机端右边栏',
52
+ 'mobileRightbarHint': '显示原生右边栏入口;普通手机可按需关闭,折叠屏展开后使用更方便',
53
+ 'wanTitle': '🌐 公网(人在外面)',
54
+ 'wanHint': '任何网络扫码即用(URL 每次重启自动换新)',
55
+ 'wanPin': '🔐 访问密码:{pin}(每次开启公网变新;手机打开链接需输入此密码)',
56
+ 'wanPinCustom': '🔐 访问密码:{pin}(自定义,开启公网不再自动换新)',
57
+ 'wanEphemeralWarn': '⚠️ 公网链接仅在本次开启期间有效:关闭或重启后失效,并可能被他人复用为陌生网站。请勿收藏,每次从本页扫「当前」二维码。需要固定不变的地址请用下方「固定域名」。',
58
+ 'stopTunnel': '关闭公网',
59
+ 'enable': '开启公网访问',
60
+ 'opening': '开启中…',
61
+ 'tunnelMode': '公网模式:',
62
+ 'modeQuick': '随机域名(默认)',
63
+ 'modeNamed': '固定域名',
64
+ 'namedSummary': '固定域名:{host} · Token {token}',
65
+ 'namedTokenSet': '已配置',
66
+ 'namedTokenMissing': '未配置',
67
+ 'namedEdit': '修改',
68
+ 'namedHostnameLabel': '固定域名:',
69
+ 'namedTokenLabel': 'Tunnel Token(留空 = 保持不变):',
70
+ 'namedHow': '在 Cloudflare Zero Trust → Networks → Tunnels 创建隧道并复制 Token;把域名的 Service 指向 http://127.0.0.1:3081(代理端口)。地址固定,重启不再变化。',
71
+ 'namedSecurity': '固定域名长期暴露在公网、更易被扫描,建议同时设置自定义强密码(本模式公网密码默认不随重启轮换)。',
72
+ 'namedNeedCfg': '请先填写固定域名与 Tunnel Token',
73
+ 'namedRunningHint': '固定域名(Cloudflare 命名隧道)——地址不随重启变化',
74
+ 'namedTakeEffect': '已保存固定域名配置——需关闭并重新开启公网访问后生效',
75
+ 'disclaimerTitle': '⚠️ 安全免责声明',
76
+ 'disclaimerBody': '开启公网 = 把本机 DSH(能执行代码)暴露到互联网。任何人拿到公网链接和密码,都能访问甚至操作你的电脑。请确认:① 使用自定义强密码或妥善保管自动密码;② 用完立即「关闭公网」;③ 公司/涉密网络请先确认合规。',
77
+ 'disclaimerAgree': '我已知情,同意开启',
78
+ 'disclaimerHint': '请勾选「我已知情」后再开启公网',
79
+ 'downloading': '⏳ 下载 cloudflared(首次约 20-50MB,通常 1-2 分钟;之后秒开)· 已等待 {s} 秒',
80
+ 'connecting': '⏳ 连接 Cloudflare 边缘(通常 5-30 秒)· 已等待 {s} 秒{suffix}',
81
+ 'slowHint': ' — 有点久?检查是否开着代理/VPN(Clash TUN 等)',
82
+ 'error': '❌ 开启失败:{detail}(可重试;若是代理/VPN 问题见 README 排障)',
83
+ 'unknownError': '未知错误',
84
+ }
85
+
86
+ /** English dictionary, key-identical to the Chinese source of truth. */
87
+ export const en = {
88
+ 'section': 'Phone access',
89
+ 'title': '📱 Phone access',
90
+ 'subtitle': 'The phone shows this exact screen, live',
91
+ 'restarted': '🔄 Restarted',
92
+ 'ok': 'Got it',
93
+ 'bgHint': 'Running in the background (not attached to a terminal). To stop: {cmd}',
94
+ 'wanAccess': 'Public access',
95
+ 'pinLabel': 'Access PIN',
96
+ 'modeLabel': 'Address mode',
97
+ 'advAddress': 'Advanced · Pick address',
98
+ 'wanOffHint': 'Reachable from any network once enabled (a disclaimer is confirmed on each enable)',
99
+ 'resetFactory': '🧹 Factory reset',
100
+ 'resetGo': 'Reset',
101
+ 'resetIntro': 'Temporary fallback when settings break: clear local config and re-roll random PINs (DSH sessions, models and plugin config are untouched)',
102
+ 'resetTitle': '⚠️ Confirm factory reset?',
103
+ 'resetBody': 'This clears and restores defaults:\n① Switches: LAN access on, access PIN on, mobile right sidebar 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.',
104
+ 'resetConfirm': 'Reset',
105
+ 'resetDone': '✅ Factory reset done: settings cleared and PINs re-rolled (re-enter the PIN on your phone)',
106
+ 'resetFailed': '❌ Reset failed — please retry',
107
+ 'lanTitle': '📶 LAN (same Wi-Fi)',
108
+ 'lanHint': 'Scan to open once your phone is on the same Wi-Fi',
109
+ 'lanAccess': 'LAN access',
110
+ 'lanDisabledHint': '🔒 LAN access is off — the QR code and link are unavailable (public access is unaffected). Tap "On" to restore.',
111
+ 'lanToggleTitleOff': 'Turn off LAN access',
112
+ '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?',
113
+ 'lanToggleTitleOn': 'Turn on LAN access',
114
+ 'lanToggleBodyOn': 'Once on, phones on the same Wi-Fi can scan to connect (a LAN PIN is required by default). Turn it on?',
115
+ 'confirm': 'Confirm',
116
+ 'lanAddress': 'LAN address',
117
+ 'lanAddressAuto': 'Auto (recommended)',
118
+ 'lanPin': 'LAN access PIN',
119
+ 'on': 'On',
120
+ 'off': 'Off',
121
+ 'lanPinValue': '🔐 PIN: {pin} (required on the phone; separate from the public PIN)',
122
+ 'lanPinCustomValue': '🔐 PIN: {pin} (custom; required on the phone)',
123
+ 'refresh': 'Refresh',
124
+ 'customize': 'Customize',
125
+ 'customizing': 'New PIN (8–64 chars, letters/digits): ',
126
+ 'save': 'Save',
127
+ 'cancel': 'Cancel',
128
+ 'pinInvalid': 'PIN must be 8–64 characters (letters and digits only)',
129
+ 'pinCustomHint': 'custom PINs are not rotated on tunnel start',
130
+ 'lanPinOff': '🔓 PIN off — scan & go, no PIN (LAN devices only; public still requires PIN)',
131
+ 'lanStarting': 'Proxy starting…',
132
+ 'mobileRightbar': 'Mobile right sidebar',
133
+ 'mobileRightbarHint': 'Show the native right-sidebar entry; disable it for a compact phone header or keep it on for an unfolded display',
134
+ 'wanTitle': '🌐 Anywhere (public)',
135
+ 'wanHint': 'Scan from any network (the URL changes on every restart)',
136
+ 'wanPin': '🔐 PIN: {pin} (changes each time the tunnel is enabled; required on the phone)',
137
+ 'wanPinCustom': '🔐 PIN: {pin} (custom — not rotated on tunnel start)',
138
+ '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.',
139
+ 'stopTunnel': 'Stop',
140
+ 'enable': 'Enable anywhere',
141
+ 'opening': 'Enabling…',
142
+ 'tunnelMode': 'Mode:',
143
+ 'modeQuick': 'Random URL (default)',
144
+ 'modeNamed': 'Fixed domain',
145
+ 'namedSummary': 'Fixed domain: {host} · Token {token}',
146
+ 'namedTokenSet': 'configured',
147
+ 'namedTokenMissing': 'not set',
148
+ 'namedEdit': 'Edit',
149
+ 'namedHostnameLabel': 'Fixed domain:',
150
+ 'namedTokenLabel': 'Tunnel Token (blank = keep current):',
151
+ '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.',
152
+ '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).',
153
+ 'namedNeedCfg': 'Set the fixed domain and Tunnel Token first',
154
+ 'namedRunningHint': 'Fixed domain (Cloudflare named tunnel) — the URL no longer changes on restart',
155
+ 'namedTakeEffect': 'Fixed-domain config saved — turn public access off and on again to take effect',
156
+ 'disclaimerTitle': '⚠️ Security disclaimer',
157
+ '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.',
158
+ 'disclaimerAgree': 'I understand and agree',
159
+ 'disclaimerHint': 'Check "I understand" before enabling public access',
160
+ 'downloading': '⏳ Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) · {s}s elapsed',
161
+ 'connecting': '⏳ Connecting to Cloudflare edge (usually 5-30s) · {s}s elapsed{suffix}',
162
+ 'slowHint': ' — taking long? Check for a proxy/VPN (e.g., Clash TUN)',
163
+ 'error': '❌ Failed to enable: {detail} (you can retry; for proxy/VPN issues see the README)',
164
+ 'unknownError': 'unknown error',
165
+ }
@@ -0,0 +1,5 @@
1
+ # dsh-pocket bundle patch(dsh.bundle manifest 要求的加载配置)
2
+ # 单包单插件:装一次,设置页「手机访问」全部功能到位。
3
+ - insert:
4
+ - id: dsh-pocket
5
+ name: '@guowenzhang/dsh-pocket'