@khorsheed/dsh-mobile 0.1.0 → 0.1.1

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.
@@ -1,4 +1,4 @@
1
- export const ORDINARY_TITLE = '[data-slot="main.conversation"] header:has([data-conversation-header-corner]) nav:has(> span:only-child > button:disabled + [data-slot="conversation.session.header.lineage"]:empty)';
1
+ export const ORDINARY_TITLE = '[data-slot="main.conversation"] header:has([data-conversation-header-corner]) nav:has(> span:only-child > :is(button:disabled,span:not([data-slot])) + [data-slot="conversation.session.header.lineage"]:empty)';
2
2
  /** Local overrides target official slot anchors; frame attributes are verified before activation. */
3
3
  export const MOBILE_CSS = `
4
4
  html[data-dsh-mobile] { --mobile-bar: 56px; --mobile-accent: #4d6bfe; }
@@ -69,6 +69,10 @@ html[data-dsh-mobile] [data-slot="conversation.input.right"] { max-width: 100%;
69
69
  [data-mobile-dialog] button[aria-pressed="true"] { color: #4d6bfe; background: #4d6bfe12; }
70
70
  [data-mobile-dialog] [data-mobile-done] { display: block; width: 100%; margin-top: 24px; background: #4d6bfe; color: white; border-radius: 100px; }
71
71
 
72
+ /* The host trigger menu is translucent; mobile overlays need an opaque face. */
73
+ html[data-dsh-mobile] [data-trigger-menu] { background:var(--mobile-face,var(--dsw-alias-bg-base))!important; --dsw-specific-menu:var(--mobile-face,var(--dsw-alias-bg-base)); opacity:1; backdrop-filter:none; -webkit-backdrop-filter:none; border:1px solid var(--mobile-edge); padding:8px; border-radius:22px; box-shadow:var(--mobile-shadow); }
74
+ html[data-dsh-mobile] [data-trigger-menu] [role=option] { min-height:48px; padding:12px; font-size:16px; line-height:1.45; }
75
+ html[data-dsh-mobile] [data-trigger-menu] [role=listbox] { overscroll-behavior:contain; }
72
76
  /* Hide an empty ordinary breadcrumb; populated catalogs keep their Host trigger. */
73
77
  html[data-dsh-mobile]:has([data-mobile-session-title]) ${ORDINARY_TITLE} { display:none; }
74
78
  html[data-dsh-mobile] [data-mobile-lineage-count] { flex-shrink:0; white-space:nowrap; }
@@ -207,17 +211,17 @@ html[data-dsh-mobile]:has([data-mobile-session-title]) [data-mobile-header-hidde
207
211
  [data-mobile-folded-action] { display:none!important; }
208
212
  [data-mobile-tools-seat] { display:contents; }
209
213
  html[data-dsh-mobile] [data-mobile-tools-open] { display:grid; place-items:center; width:44px; height:44px; min-width:44px; padding:0; border:0; border-radius:50%; corner-shape:round; color:var(--dsw-alias-label-primary); background:var(--mobile-soft); }
210
- [data-mobile-tools-dialog] { box-sizing:border-box; margin:auto auto 0; width:min(100%,520px); max-width:100%; max-height:75dvh; overflow-y:auto; padding:16px 20px max(20px,env(safe-area-inset-bottom)); border:1px solid var(--mobile-line); border-bottom:0; border-radius:26px 26px 0 0; background:var(--mobile-face); color:var(--dsw-alias-label-primary); box-shadow:0 -8px 40px #0002; }
214
+ [data-mobile-tools-dialog] { box-sizing:border-box; margin:auto auto 0; width:min(100%,520px); max-width:100%; max-height:75dvh; overscroll-behavior:contain; overflow-y:auto; padding:16px 20px max(20px,env(safe-area-inset-bottom)); border:1px solid var(--mobile-line); border-bottom:0; border-radius:26px 26px 0 0; background:var(--mobile-face); color:var(--dsw-alias-label-primary); box-shadow:0 -8px 40px #0002; }
211
215
  [data-mobile-tools-dialog]::backdrop { background:#0005; }
212
- [data-mobile-tools-dialog] [data-mobile-tools-handle] { width:34px; height:4px; margin:0 auto 12px; border-radius:4px; background:var(--dsw-alias-label-tertiary); opacity:.5; }
213
- [data-mobile-tools-dialog] header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
214
- [data-mobile-tools-dialog] strong { font-size:16px; font-weight:500; }
216
+ [data-mobile-tools-dialog] [data-mobile-tools-handle] { width:34px; height:4px; margin:0 auto 12px; touch-action:pan-x; border-radius:4px; background:var(--dsw-alias-label-tertiary); opacity:.5; }
217
+ [data-mobile-tools-dialog] header { touch-action:pan-x; display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
218
+ [data-mobile-tools-dialog] strong { font-size:20px; font-weight:600; }
215
219
  [data-mobile-tools-dialog] button { font:inherit; color:inherit; cursor:pointer; border:0; background:transparent; -webkit-tap-highlight-color:transparent; }
216
220
  [data-mobile-tools-dialog] button:disabled { opacity:.45; cursor:default; }
217
221
  [data-mobile-tools-dialog] header button { width:44px; height:44px; display:grid; place-items:center; border-radius:50%; padding:0; }
218
222
  [data-mobile-tools-grid] { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
219
- [data-mobile-tools-grid] button { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; min-height:92px; padding:12px; border-radius:20px; background:var(--mobile-soft); font-size:14px; }
220
- [data-mobile-tools-permission] { display:flex; align-items:center; width:100%; gap:10px; margin-top:12px; min-height:64px; padding:12px 0; text-align:left; font-size:14px!important; }
223
+ [data-mobile-tools-grid] button { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; min-height:96px; padding:16px; border-radius:18px; background:var(--mobile-soft); font-size:14px; }
224
+ [data-mobile-tools-permission] { display:flex; align-items:center; width:100%; gap:10px; margin-top:12px; border-top:1px solid var(--mobile-line)!important; min-height:64px; padding:12px 0; text-align:left; font-size:14px!important; }
221
225
  [data-mobile-tools-permission] > span { flex-shrink:0; }
222
226
  [data-mobile-tools-permission] > small { margin-left:auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; color:var(--dsw-alias-label-secondary); }
223
227
  [data-mobile-tools-dialog] svg { flex:none; }
@@ -326,6 +330,12 @@ html[data-dsh-mobile] [data-mobile-room-form] :is(input,textarea,select) { box-s
326
330
  html[data-dsh-mobile] [data-mobile-room-form] textarea { min-height:94px; resize:vertical; }
327
331
  html[data-dsh-mobile] [data-mobile-room-form] button { min-height:44px; min-width:44px; font-size:16px; border-radius:14px; }
328
332
  html[data-dsh-mobile] [data-mobile-room-form] summary { min-height:44px; padding:10px 0; font-size:16px; color:var(--dsw-alias-label-primary); }
333
+ /* Harness model controls are expandable summaries, not native selects. Keep the
334
+ advanced-settings disclosure plain while aligning picker faces with form fields. */
335
+ html[data-dsh-mobile] [data-mobile-room-form] details > summary[aria-expanded] { display:flex; align-items:center; box-sizing:border-box; width:100%; max-width:none; min-width:0; min-height:48px; gap:12px; padding:11px 12px; border:1px solid var(--mobile-edge); border-radius:14px; background:var(--mobile-soft); font-size:17px; line-height:1.5; color:var(--dsw-alias-label-primary); }
336
+ html[data-dsh-mobile] [data-mobile-room-form] details > summary[aria-expanded] > span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
337
+ html[data-dsh-mobile] [data-mobile-room-form] details > summary[aria-expanded]::after { flex-shrink:0; margin-inline-start:auto; margin-inline-end:2px; }
338
+
329
339
  html[data-dsh-mobile] [data-mobile-room-form] > div:last-child { position:sticky; bottom:-16px; background:var(--mobile-face); padding:12px 0 0; margin-top:0; display:grid; grid-template-columns:1fr 1fr; gap:12px; }
330
340
  html[data-dsh-mobile] [data-mobile-room-form] [role=alert] { font-size:15px; line-height:1.5; }
331
341
  html[data-dsh-mobile] [data-member] { font-size:16px; }
@@ -386,6 +396,15 @@ html[data-dsh-mobile] [data-mobile-picker] { position:fixed!important; inset:aut
386
396
  html[data-dsh-mobile] [data-mobile-picker] > [role=presentation] { max-height:none; overflow:visible; }
387
397
  html[data-dsh-mobile] [data-mobile-picker] [role=menuitem] { min-height:52px; padding:12px; border-radius:14px; font-size:17px; }
388
398
  html[data-dsh-mobile] [data-mobile-picker] [role=menuitem][hidden] { display:none; }
399
+ /* Keep the workspace list inside its flex viewport: visible overflow used to
400
+ * paint the last folder over the pinned add-action divider on short screens. */
401
+ html[data-dsh-mobile] [data-mobile-picker=workspace] { overflow:hidden; }
402
+ html[data-dsh-mobile] [data-mobile-picker=workspace] > [data-mobile-picker-chrome] { flex:none; }
403
+ html[data-dsh-mobile] [data-mobile-picker=workspace] > [role=presentation]:nth-child(2) { flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain; }
404
+ html[data-dsh-mobile] [data-mobile-picker=workspace] > [role=presentation] > * { flex:none; }
405
+ html[data-dsh-mobile] [data-mobile-picker=workspace] > [role=presentation]:nth-child(3):last-child { flex:none; margin-top:8px; padding-top:8px; border-top:1px solid var(--mobile-edge); }
406
+ html[data-dsh-mobile] [data-mobile-picker=workspace] [role=menuitem] { box-sizing:border-box; min-height:52px; padding:12px; line-height:28px; }
407
+
389
408
  html[data-dsh-mobile] [data-mobile-picker=preset] [role=menuitem] span > span:last-child { font-size:14px; line-height:1.55; color:var(--dsw-alias-label-secondary); }
390
409
 
391
410
  html[data-mobile-gesture] [data-mobile-library-pane][hidden] { display:block; }
@@ -25,7 +25,7 @@ export class MobileSurface {
25
25
  this.clearMarks();
26
26
  const root = frame?.querySelector('[data-slot="main.conversation"] > [data-phase]');
27
27
  const row = root?.querySelector('header [data-conversation-header-corner]')?.parentElement;
28
- const ordinary = row?.querySelector('nav > span:only-child > button:disabled + [data-slot="conversation.session.header.lineage"]:empty');
28
+ const ordinary = row?.querySelector('nav > span:only-child > :is(button:disabled,span:not([data-slot])) + [data-slot="conversation.session.header.lineage"]:empty');
29
29
  let preset = '';
30
30
  if (row) {
31
31
  for (const button of row.querySelectorAll('[data-slot="conversation.session.header.actions"] button[aria-label]')) {
@@ -36,7 +36,7 @@ export class MobileSurface {
36
36
  if (root?.dataset.phase === 'active' && row) {
37
37
  // The ordinary current title duplicates the mobile title. Compact only the
38
38
  // checked count dropdown; preserve ancestor links and subagent switchers.
39
- for (const title of row.querySelectorAll('nav > span > button:disabled')) {
39
+ for (const title of row.querySelectorAll('nav > span > :is(button:disabled,span:not([data-slot]))')) {
40
40
  const slot = title.nextElementSibling;
41
41
  if (slot?.getAttribute('data-slot') !== 'conversation.session.header.lineage')
42
42
  continue;
@@ -48,11 +48,14 @@ export class MobileSurface {
48
48
  this.mark(separator, 'data-mobile-header-hidden');
49
49
  this.mark(count, 'data-mobile-lineage-count');
50
50
  }
51
- const label = row.querySelector('[data-slot="conversation.session.header.actions"] > span[title]:has(svg mask[id^="mask0_agent_preset"])');
51
+ const label = row.querySelector('[data-slot="conversation.session.header.actions"] > span[title]:has(svg :is(mask[id^="mask0_agent_preset"],path[d^="M6.51867 12.3282"]))');
52
52
  if (label) {
53
53
  preset = label.textContent?.trim() ?? '';
54
54
  this.mark(label, 'data-mobile-header-hidden');
55
55
  }
56
+ // 0.1.7 publishes an explicit target; icons may now be data/blob URLs.
57
+ for (const target of row.querySelectorAll('[data-slot="conversation.session.header.utilities"] [data-open-target="directory"]'))
58
+ this.mark(target, 'data-mobile-header-hidden');
56
59
  for (const img of row.querySelectorAll('img[src^="/open-in-app/icon/"]')) {
57
60
  const split = img.closest('button')?.parentElement;
58
61
  if (split && split.querySelectorAll('button').length === 2)
@@ -62,7 +65,7 @@ export class MobileSurface {
62
65
  if (button.hasAttribute('data-sidebar-right-expand') || /^(更多操作|More actions)$/i.test(button.getAttribute('aria-label') ?? ''))
63
66
  this.mark(button, 'data-mobile-header-hidden');
64
67
  }
65
- if (this.doc.querySelector('[data-mobile-members-open], [data-mobile-tools-open]')) {
68
+ if (this.doc.querySelector('[data-mobile-members-open]:not([disabled])')) {
66
69
  for (const button of row.querySelectorAll('[data-slot="conversation.session.header.actions"] button')) {
67
70
  if (/^(+|\+)?\s*(邀请 agent|Invite agent)$/i.test(button.textContent?.trim() ?? ''))
68
71
  this.mark(button, 'data-mobile-header-hidden');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khorsheed/dsh-mobile",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Removable mobile presentation and iOS bridge for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,8 +33,9 @@
33
33
  "locale/*.json",
34
34
  "examples",
35
35
  "README.md",
36
- "README.zh.md",
37
- "LICENSE"
36
+ "README.en.md",
37
+ "LICENSE",
38
+ "CHANGELOG.md"
38
39
  ],
39
40
  "dsh": {
40
41
  "bundle": {
package/README.zh.md DELETED
@@ -1,132 +0,0 @@
1
- # DSH Mobile
2
-
3
- [English](README.md) | 中文
4
-
5
- 可独立卸载的 DeepSeek Harness 移动适配插件。Mac 继续持有会话、模型、skill、tool 和插件执行;可选的 [iOS 薄壳](../../apps/ios/README.zh.md) 嵌入同一套官方 Web 客户端。
6
-
7
- ## 功能
8
-
9
- - 会话首页支持时间/工作区分组、工作区折叠和底部搜索。分组偏好仅保存在当前浏览器;导航覆盖期间仍挂载官方聊天与草稿。
10
- - 当前客户端独立的移动布局、全宽右栏详情、移动设置,以及可换行、扩大触控区域的输入工具栏。
11
- - 保留官方正文、输入框、模型/权限选择、附件及流式传输。
12
- - 通过移动端面板选择**电脑目录**:已有工作区、逐级浏览、返回上级、显示隐藏目录及校验完整路径。工作区接入和 Room 成员目录选择共用此面板;桌面客户端保留原生选择器。
13
- - 受认证的 `GET /api/mobile/handshake`;带版本的原生桥接提供展示状态与可选原生设置/扫码入口。回前台通过官方连接服务恢复,不重放发送命令。短暂切换且连接正常、初次正在连接时不重复重连;后台停留至少 5 秒仍刷新流式连接代际。
14
- - 无社区插件强依赖。message-tools、成员、预览保留各自所有者;移动组合仍需验收。
15
-
16
- 当前是开发基线,尚未发布或通过真机资格验收。相机/文件选择、分享/下载、后台恢复和社区组合尚未完成验收。iOS 扫码接收已有官方 HTTPS 登录链接,并要求确认主机;相机实测验收仍待完成。不提供一次性设备配对、逐设备凭据撤销或 APNs。
17
-
18
- ## 安装与卸载
19
-
20
- 先在仓库 worktree 构建、打包:
21
-
22
- ```sh
23
- pnpm --filter @khorsheed/dsh-mobile build
24
- pnpm --filter @khorsheed/dsh-mobile test
25
- pnpm exec tsx scripts/pack-dist.ts --package packages/mobile --scope @khorsheed --version 0.1.0 --out packages/mobile/.dev/dist
26
- ```
27
-
28
- 用 `dsh plugin --profile web add /path/to/package.tgz` 安装产物;用 `dsh plugin --profile web remove @khorsheed/dsh-mobile` 移除。插件自挂载,不手改 profile YAML。实测 rc1 实例的依赖变化需要受控重启 Host 并刷新客户端,未通过热卸载验收。开发使用独立 `DSH_HOME` 与端口;生产 3080 仍走仓库部署门禁。
29
-
30
- 窄屏触控设备或原生壳自动启用移动布局。`?mobile=1` 显式启用,`?mobile=0` 停用。移动设置中的选择仅保存在当前浏览器的 `dsh.mobile.display`,首页分组使用 `dsh.mobile.grouping`。切到桌面布局后,可用 `?mobile=1` 恢复设置入口。官方登录交换会跳转 `/`,需要时在认证后再加参数。
31
-
32
- 卸载移除自有样式、布局标记、observer、监听、路由和槽贡献,不删除会话、取消 Host 任务、关闭共享连接、撤销官方 Cookie 或卸载 App/VPN。偏好可保留用于重装。浏览器热卸载与 tarball 验收分开记录于下方证据。
33
-
34
- ## 连接与认证
35
-
36
- App 接受配置的 HTTPS 主机地址或官方启动 token 登录链接,只保存无凭据的 origin;WebKit 保存官方浏览器会话 Cookie。桥接仅接受同 origin 主框架、bridge version 1 的消息,支持 `ready`、`unloaded`、`chrome`、`settings` 和 `scan`;新增原生动作需壳声明能力,旧壳保留回退控件。不授予文件或命令权限。
37
-
38
- 跨物理网络需要独立配置的 HTTPS/WSS 入口转发到 loopback Host 端口(生产为 3080)、有效官方认证、保持唤醒和联网的 Mac,以及网络可达的手机。仅启动 3080 不等于远程可用。App 不自动配置网络或凭据。Debug 模拟器构建仅允许 loopback HTTP,Release 要求 HTTPS。清除 App 本地数据与服务端设备撤销是不同操作。
39
-
40
- ## 从 Web 设置连接手机
41
-
42
- 已登录的 Web 客户端通过公开 `settings.section` 槽位提供 **设置 → 连接手机**;桌面无需开启移动布局。将手机可达的**无凭据 HTTPS origin** 配置为 mobile 插件的 `publicOrigin` 选项,或在 **Host 进程**上设置 `DSH_MOBILE_PUBLIC_ORIGIN`;插件选项优先。同一域名需加入 Host 的 `--trusted-host`,ingress 的 `PUBLIC_ORIGIN` 也必须一致。这些属于部署设置,不是手机偏好。
43
-
44
- ```sh
45
- DSH_MOBILE_PUBLIC_ORIGIN=https://YOUR-HOST.trycloudflare.com dsh web --no-open --port 3181 --trusted-host YOUR-HOST.trycloudflare.com
46
- ```
47
-
48
- 示例使用隔离 profile;生产启动配置通过守护进程的受保护切换流程更新。地址缺失或无效、宿主能力不支持、目标域名未受信任时,只显示配置提示,不生成登录链接。
49
-
50
- 1. 打开 Web「设置 → 连接手机」,核对展示的主机。
51
- 2. 点击**显示登录二维码**。
52
- 3. 在 iOS App 进入**连接设置 → 扫码连接电脑**,扫描后确认主机。
53
-
54
- `GET /api/mobile/connect` 仅返回配置状态。已认证、同源的 JSON `POST` 调用官方 `connection.authenticatedUrl()`,为部署者配置的 origin 获取登录链接;浏览器输入不能覆盖目标地址。两个接口都通过官方 Connection 注册,保留 Cookie 和 Host/Origin 校验,响应为 no-store。二维码在浏览器本地编码,不调用第三方二维码服务、不记录凭据日志、不写浏览器存储。离开设置分区、页面隐藏或展示两分钟后会隐藏二维码,并取消未完成的请求。
55
-
56
- **隐藏不等于凭据过期。** 这里使用宿主进程的官方登录 token,在该进程重启前有效,并非短时或一次性配对码。已有 Cookie 的有效期仍由宿主控制。隐藏或重新生成二维码不会撤销已复制的链接,也不会登出手机。逐设备凭据、撤销、Keychain 配对和推送仍未实现。
57
-
58
- 入口不可达时,应先恢复 HTTPS 隧道。隧道进程仍在但活跃连接数为零时,仍无法承载请求。临时域名改变后,同步更新 ingress origin、Host 信任域名和 mobile 公网 origin,再生成新二维码;旧 Cookie 绑定旧 authority。本机 3080 响应正常不等于手机能连通。日常使用建议使用固定 HTTPS 入口;扫码也不会续签过期的 iOS 开发签名。
59
-
60
- ## 可选的 Quick Tunnel 预览
61
-
62
- 网络接入属于部署配置,不是插件依赖。社区用户可以选择临时预览用的 Quick Tunnel、自建 HTTPS 反向隧道/服务器或私网连接。插件不嵌入个人域名、服务器或 Cloudflare 账号。稳定的托管中继属于独立服务,会有自己的运营成本。
63
-
64
- [Cloudflare Quick Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/) 无需 Cloudflare 账号或自有域名即可生成临时 HTTPS 地址。它用于测试,不保证在线时间,允许 200 个并发请求,不支持 SSE。实测 rc1 的会话传输使用 WebSocket;这不代表需要 SSE 的其他插件已通过验收。
65
-
66
- 使用已安装 mobile tarball 的隔离 Host profile。从官方渠道安装 `cloudflared`,使用 Node 22+。在仓库根目录先启动隧道(分配地址时本地入口可以尚未启动):
67
-
68
- ```sh
69
- cloudflared tunnel --url http://127.0.0.1:3182 --protocol quic --no-autoupdate
70
- ```
71
-
72
- 协议选择取决于本地网络。已注册的 QUIC 隧道若在实际请求上卡住,可以尝试 `--protocol http2`;其他网络上的 HTTP/2 也可能失败,两者都不是普遍适用的默认选择。仅看到隧道注册成功或本地入口健康还不够,分享预览前需验证公网登录、会话列表和完整 WebSocket 回复。新建 Quick Tunnel 会分配另一个域名:通过受保护的启动切换同步更新 ingress origin 与 Host trusted-host,再让 App 重新连接。测试进程应独立运行,避免依赖短生命周期的自动化会话。
73
-
74
- 将生成的主机名替换下方的 `YOUR-HOST.trycloudflare.com`,在第二个终端启动独立示例:
75
-
76
- ```sh
77
- PUBLIC_ORIGIN=https://YOUR-HOST.trycloudflare.com HOST_PORT=3181 INGRESS_PORT=3182 node packages/mobile/examples/https-ingress.mjs
78
- ```
79
-
80
- 然后使用独立的 `DSH_HOME`、工作区和已配置模型启动隔离 Host,通过官方参数加入精确的公网 authority:
81
-
82
- ```sh
83
- dsh web --no-open --port 3181 --trusted-host YOUR-HOST.trycloudflare.com
84
- ```
85
-
86
- 示例也随 tarball 的 `examples/` 分发。它仅绑定 loopback,保留公网 Host 与 Origin 交给官方认证,要求转发协议为 HTTPS,为上游 Cookie 补充 Secure,并透传 HTTP 流和 WebSocket 升级。它不读取 Host 私有密钥,不增加配对或逐设备撤销系统。仅在 HTTPS 隧道之后运行;本地请求头不构成用户已认证的证明,授权仍由 Host 负责。
87
-
88
- 手机登录时,仅将 Host 启动链接的 origin 替换为隧道 HTTPS origin,保留 `?token=...`,在 Safari 打开或粘贴到 iOS 壳。带凭据链接应私下保存。登录会跳转到干净的 `/`;如果设备没有自动启用移动布局,认证后再加 `?mobile=1`。
89
-
90
- Mac 保持唤醒,三个服务持续运行。停止隧道进程可关闭这条公网访问路径;仅卸载 mobile 不会停止隧道或撤销官方会话。重新分配域名后,需要同步更新 `PUBLIC_ORIGIN`、`--trusted-host` 并重新登录。这次预览不代表长期蜂窝网络可达性已验证。参见 [Quick Tunnel 验收记录](../../docs/acceptance/mobile-quick-tunnel-2026-09-11.md)。
91
-
92
- 桌面模式在重载后仍保留“返回移动布局”按钮。移动端进入会话时阻止输入框自动聚焦,等待明确的输入手势;rc1 DOM 锚点变化时回退官方聚焦行为。普通会话标题去重,保留祖先/lineage 导航。这些 Web 层修复无需重新安装原生 App。
93
-
94
- 新版移动界面保留输入区统计和官方消息动作,隐藏未选中的轨迹入口,通过公开品牌槽位提供欢迎内容。rc1 已有会话工作区/preset 为只读标签,新建会话才提供官方选择器。原生设置通过校验后的 `dsh-mobile-display` 事件调整布局。未知标题区结构保留原始标题及祖先导航。
95
-
96
- 输入框加号菜单归并已有附件、指令和权限入口,当前权限名称与可选项仍由官方提供。校验后的 rc1 按钮锚点保留原回调和确认流程,结构未知时恢复官方控件。工作区/模式显示在移动标题下方;隐藏已识别的桌面标题控件,保留未知插件贡献。
97
-
98
- ## 可选协作界面
99
-
100
- message-tools、TaskPilot、Room 和 Local Agent 家族独立安装。移动端读取公开能力,不把它们作为启动依赖。长按已识别的用户消息,调用原有复制/编辑/撤回;助手消息使用自己的回合动作。确认、禁用状态及修改语义仍由原组件负责。无法识别的渲染器保留原控件。
101
-
102
- 加号面板打开 **Room 原有邀请表单**,仅调整为移动端底部面板样式。成员快捷面板读取名册、打开已有子会话;设置按钮进入 Room 原成员页与编辑表单。代理发现、认证、随机命名、首个任务派发、目录接入、仅修改字段的补丁、移除确认及业务错误都由 Room 负责,不再维护第二套移动邀请/编辑 API。移动端在当前浏览器内临时适配公开的 `uiWorkspace.pickDirectory` 方法,由面板返回所选路径,Room 原表单继续消费该结果。卸载时恢复方法;未知的只读服务外观保留原行为。首个任务留空表示待命;填写后沿用 Room 的派发行为。无法识别快捷入口时保留原成员页作为回退。
103
-
104
- Room 与 Local Agent 保留各自输入框。Room 排队面板使用官方会话 API 和 `row.text` 可编辑判据,按请求 ID 去重已接收/待确认消息,并在消息消失或不可修改时退出编辑。目标、任务与 TaskPilot 保留原组件所有权。Room 在两端都会用自己的输入框替换普通 dock,因此移动端不承诺 Room 内展示全部普通会话 TaskPilot 胶囊。
105
-
106
- 移动阅读正文最低 17px,主要控件 16–17px,辅助信息约 14px;从宿主字体变量派生,不写入宿主字号偏好。邀请/编辑控件点击高度至少 44px,面板跟随键盘打开后的可见视口。
107
-
108
- 会话列表统一保留工作区,并显示群聊和普通会话的成员数。仅为可见行查询可选 Room 元数据;失败保留未知状态,不显示错误的成员数。当前 Room 版本首次异步填充缓存时可能未刷新输入框选择;移动端通过始终让出的 chain 条目刷新公开槽位选择,不替换胜出的组件。待处理交互仍优先。
109
-
110
- 本机消息确认提交后,收起同一个空白编辑器的键盘;提交失败和后续草稿保留焦点。上下文圆环留在发送按钮旁。玻璃质感表面跟随宿主浅暗色,文字保持不透明,并提供降低透明度时的回退。参见[协作验收](../../docs/acceptance/mobile-collaboration-2026-09-12.md)。
111
-
112
-
113
- ## Safari 流式同步兼容层
114
-
115
- 宿主 `0.1.5-rc.1` 的 JSON 校验会在 Safari 还原进行中回复时误拒绝普通对象。针对该已知缺陷,随包分发的 HTTPS ingress 支持 `MOBILE_SAFARI_COMPAT=1`。使用此环境变量重启 ingress,兼容层生效后重新加载客户端。已经停止的正文订阅不能仅靠重新连接 socket 恢复。
116
-
117
- 这是默认关闭的交付层适配,不修改宿主源码:只检查 WebKit 对 `/plugins/` JavaScript 的 GET 请求。完整已知校验函数的 SHA-256 指纹决定是否替换;未知代码和已经修好的代码保持原样。补丁使用当前引擎的原生构造函数格式比较,不修改 `Function.prototype.toString`。认证、RPC 请求体、WebSocket 帧和宿主执行保持不变。直接连接局域网或使用其他 ingress 不会获得此兼容层。
118
-
119
- 有界 JavaScript 完成检查后,向支持 gzip 的客户端压缩输出,包括未修改的 bundle,避免多兆字节的未压缩脚本拖慢手机隧道加载。响应保持 private/no-store;修改后的脚本重算字节长度并移除旧 ETag/digest。超过 16 MiB、非 JS、认证失败、上游忽略 identity 编码要求而返回压缩内容的响应原样透传。升级宿主时检查 `x-dsh-mobile-compat` 与 ingress 不含请求内容的兼容日志,不自动扩大指纹范围。上游修复通过真实 WebKit 前后台和重连验收后移除开关。正式部署从已安装 tarball 的 `examples/` 启动 ingress,避免使用开发检出。
120
-
121
- 只读配套接口 `GET /api/mobile/directories` 复用官方 Connection 认证和 Host/Origin 检查。在已认证操作者现有文件系统权限内返回目录名称与路径,包括可进入的符号链接;不读取文件内容,不创建目录。每次最多返回 1,000 个目录、扫描 10,000 个条目,并提示截断。原生选择器宿主无需切换共享后端即可支持手机浏览。接口缺失或连接中断时显示错误和重试;此功能要求使用提供 HTTP 服务的连接方式。
122
-
123
- ## Compatibility
124
-
125
- | 宿主线 | 结论 |
126
- |---|---|
127
- | 官方 `0.1.5-rc.1`、`183f08e9c6` | 本地浏览器/Host 验证,iOS 模拟器构建通过;完整真机/网络验收待完成。 |
128
- | 其他 npm RC / Harness master | 未验证,采用前审计。 |
129
-
130
- `dsh.compat.minHost` 为 `0.1.5-rc.1`,发布矩阵未完成前不声明 `verifiedHost`。会话首页使用可选的官方 sessions/workspaces/uiWorkspace 服务,过滤已归档会话和子代理,保留普通分叉会话;缺少服务时回退基础官方侧栏。这里搜索的是标题和工作区路径,不是消息正文。启用布局前检查公开 frame/slot DOM 锚点;未知结构保留官方页面。结构变化可能降低移动可用性,但不改变 Host 执行。原生桥接版本变化需要判断 App 兼容性;兼容的 Web 更新不自动要求重发 IPA。
131
-
132
- 参见[导航/扫码验收](../../docs/acceptance/mobile-navigation-2026-09-11.md)、[验收证据](../../docs/acceptance/mobile-rc1-2026-09-11.md)与[提案](../../proposals/active/2026-08-19-mobile-access.md)。无需修改官方或兄弟插件源码。