@huakirax/dsh-plugin-pocket 0.0.0-stage → 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.
Files changed (44) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +237 -2
  3. package/bin/pocket.js +31 -0
  4. package/cordis.patch.yml +6 -0
  5. package/index.js +360 -0
  6. package/lib/client.js +2453 -0
  7. package/package.json +91 -4
  8. package/src/advertise-host.js +69 -0
  9. package/src/approval-hook.js +50 -0
  10. package/src/client/ConfirmDialog.tsx +80 -0
  11. package/src/client/HostUiLauncher.tsx +101 -0
  12. package/src/client/ManagePanel.tsx +218 -0
  13. package/src/client/PhoneIcon.tsx +48 -0
  14. package/src/client/PocketSettings.tsx +13 -0
  15. package/src/client/api.ts +97 -0
  16. package/src/client/completion-sync.js +289 -0
  17. package/src/client/host-ui-styles.ts +62 -0
  18. package/src/client/index.ts +47 -0
  19. package/src/client/locales.ts +75 -0
  20. package/src/client/nav-icon.ts +31 -0
  21. package/src/client/qr.ts +15 -0
  22. package/src/client/styles.ts +76 -0
  23. package/src/completion-route.js +87 -0
  24. package/src/completion-tracker.js +95 -0
  25. package/src/devices.js +60 -0
  26. package/src/extra-ops.js +604 -0
  27. package/src/host-ui.js +466 -0
  28. package/src/instance-id.js +33 -0
  29. package/src/iroh.js +260 -0
  30. package/src/manage.js +122 -0
  31. package/src/mdns-advertise.js +75 -0
  32. package/src/office-preview.js +129 -0
  33. package/src/pairing-uri.js +42 -0
  34. package/src/pairing.js +34 -0
  35. package/src/parity-ops.js +250 -0
  36. package/src/server.js +761 -0
  37. package/src/settings-ops.js +387 -0
  38. package/src/task-events.js +77 -0
  39. package/src/task-fallback.js +177 -0
  40. package/src/token.js +18 -0
  41. package/src/transport-pref.js +47 -0
  42. package/src/trust.js +25 -0
  43. package/src/user-question-hook.js +93 -0
  44. package/src/web-route.js +116 -0
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Huakira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,238 @@
1
- # Temporary Holding Version
1
+ # dsh-plugin-pocket
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > 个人自用插件,代码和文档由 AI 参与编写,不承诺维护。使用前请先备份 DSH 配置。
4
+
5
+ 本仓库是从 [minglu6/dsh-android](https://github.com/minglu6/dsh-android) 的 `plugin/` 目录抽出独立维护的自用改版(MIT,感谢原作者);上游的协议与功能说明仍然适用,下文指向原仓库的链接未改写。手机端配套 App 见 [Huakira/dsh-ios](https://github.com/Huakira/dsh-ios) 的 `local-patches` 分支。
6
+
7
+ [dsh-android](https://github.com/minglu6/dsh-android/blob/main/README.zh-CN.md) 的 Host 桥接插件。在电脑端 dsh 进程中调用官方会话、配置、工作区等服务,通过带鉴权的 JSON/WebSocket 协议供原生 Android 客户端使用。手机不运行另一套任务数据库、包管理器或模型执行引擎。
8
+
9
+ ## 安装与升级
10
+
11
+ ### 运行前提
12
+
13
+ - Node.js:`^22.19.0 || >=24.0.0`,不包含 23.x。
14
+ - dsh:当前 CI 使用 `@deepseek-ai/dsh@0.2.1-alpha.2`;session-controller peer 范围为 `>=0.2.1-alpha.2 <0.2.2-0`。该范围是依赖声明,不代表每个版本都经过测试。
15
+ - `dsh plugin` 需要 PATH 中的 pnpm;CI 使用 `pnpm@10`。
16
+ - 手机与电脑可通过局域网或 Tailscale 互访,防火墙允许桥接端口(默认 TCP `7690`)。
17
+
18
+ ### 从源码安装
19
+
20
+ 从仓库根目录执行;尚未安装 dsh/pnpm 时,可先执行根 README 中的 [环境安装命令](https://github.com/minglu6/dsh-android/blob/main/README.zh-CN.md#安装桥接)。不依赖该插件已发布到 npm。
21
+
22
+ ```bash
23
+ cd plugin
24
+ npm install
25
+ npm run build:client
26
+ dsh plugin --profile web add "$PWD"
27
+ dsh web
28
+ ```
29
+
30
+ 桌面 App 使用 `desktop` profile,需在该 profile 安装,而不是只装到 `web`:
31
+
32
+ ```bash
33
+ dsh plugin --profile desktop add "$PWD"
34
+ ```
35
+
36
+ 随后重启相应 Host。管理 UI 通过官方 client half(`dsh.client` 与 `exports["./client"]`)注册到 **设置 → 手机连接**;不修改官方账号菜单,也不使用 HTML 注入替换桌面入口。
37
+
38
+ 更新源码后重新安装依赖、执行 `npm run build:client`,按相同 profile 安装流程更新并重启 Host。尤其是修改 `src/client/**` 后必须重建 `lib/client.js`。**升级 APK 不会自动更新或重载电脑端桥接。**
39
+
40
+ 本轮文件改动通过 `workspaceChanges.summary {sessionId, seq}` 返回 Host 的完整摘要,通过 `workspaceChanges.diff {sessionId, seq, index}` 返回该轮指定文件的快照差异。`index` 是摘要文件列表的原始下标;只公布 Host 实际提供的方法,并传递请求取消信号。摘要/差异已释放时返回 `null`,不改读当前工作区文件。旧桥接需重启才能启用新增的差异接口。
41
+
42
+ 点赞/点踩通过 `feedback.get`、`feedback.set` 调用 Host `messageFeedback` 的单层业务 Result。成功只返回业务值;失败完整传递 `error.code` 与 `error.details`,版本冲突的 `details.current` 保留权威条目或显式 `null`,供手机更新选中态而不自动重试。使用新版反馈冲突提示时,须同步更新并重启此桥接;仅升级 APK 不足以修复旧桥接丢失冲突详情的问题。
43
+
44
+ ## 配置与网络
45
+
46
+ 以 Web profile 为例,在 `$DSH_HOME/profiles/web/cordis.patch.yml` 配置(默认 `DSH_HOME` 为 `~/.dsh`);桌面使用对应的 `desktop` profile。所有配置项均可省略:
47
+
48
+ ```yaml
49
+ - id: dsh-pocket
50
+ config:
51
+ # host: 192.168.1.10 # 指定监听接口;不写则绑定 0.0.0.0
52
+ # port: 7690
53
+ # dataDir: /path/to/pocket # 覆盖默认 $DSH_HOME/pocket 数据目录
54
+ # token: <owner-token> # 可选固定 owner token,通常使用自动生成值
55
+ # pair: true # 启动时打开配对窗口
56
+ # pluginUiPaths: ['/dsh-purge', '/redteam'] # 显式授权可信插件的私有 HTTP 根命名空间
57
+ # iroh: true # 仅用于遗留桌面 sidecar 实验
58
+ # hostBinary: /path/to/pocket-host
59
+ ```
60
+
61
+ 手机链路仅为 **host:port WebSocket**。内部的 `tailscale` 传输标识不代表必须安装 Tailscale:
62
+
63
+ - 未指定 `host` 时绑定 `0.0.0.0`,默认公布局域网 IPv4,没有局域网地址时使用可用的 Tailscale 地址。
64
+ - 两类地址均可用时,「手机连接」可切换 **局域网 / Tailscale**;地址、二维码与复制链接随之变化,不重启服务或打断已配对连接。
65
+ - 指定具体 `host` 会限制监听与公布的接口;希望两类网络均可连接时,应省略该项或使用 `0.0.0.0`。
66
+ - 刷新重新发现网卡;明确选择的网络不可用时会报错,不静默切换到另一网络。没有可用地址时禁用配对。
67
+ - 每类网络使用首个可用 IPv4,不支持 IPv6-only 配对。局域网需要关闭客户端隔离;Tailscale 需要双方处于可互访的 tailnet。
68
+
69
+ 默认全接口监听不是公网部署方案。请用防火墙限制访问,跨网络使用 Tailscale 或已有可信加密链路;`ws://` 与 LAN HTTP 本身没有 TLS,配对不能防止未加密链路上的窃听。
70
+
71
+ WebSocket 协议错误(例如客户端在线路上发送保留关闭码 `1006`)按连接隔离:记录错误、关闭该连接并释放其订阅、设备在线状态和网页授权,不影响其他连接或 Host 进程。修正客户端帧后可重新连接。
72
+
73
+ 插件的 `cordis.patch.yml` 还会禁用默认 `directory-picker` 自动选择条目,并加载官方 `browse` 后端和客户端。电脑与手机因此使用应用内目录浏览,电脑不再弹出 macOS 系统目录选择器。
74
+
75
+ ## 配对与设备
76
+
77
+ 首次启动且没有已配对设备时会自动打开配对窗口,并在日志中打印 6 位码。配对码 **10 分钟有效、最多尝试 3 次、成功后单次作废**;请勿公开启动日志或配对二维码。
78
+
79
+ 管理 UI 支持选择网络、复制地址、打开/关闭配对窗口、显示 QR、设备重命名与撤销。配对 URI:
80
+
81
+ ```text
82
+ dshpocket://pair?h=<host:port>&c=<6位码>
83
+ ```
84
+
85
+ ### 本机 CLI
86
+
87
+ 在 `plugin/` 目录执行:
88
+
89
+ ```bash
90
+ node bin/pocket.js pair
91
+ node bin/pocket.js devices
92
+ node bin/pocket.js revoke <device-id>
93
+ ```
94
+
95
+ 默认使用 `ws://127.0.0.1:7690` 和 `$DSH_HOME/pocket/token`。自定义端口或数据目录时可指定 `--url ws://127.0.0.1:<port>`、`--token-file /path/to/token`。CLI 使用本机 owner token,手机应使用配对所得设备 token,不应共享 owner token。
96
+
97
+ ### 凭据与撤销
98
+
99
+ 手机发送 `pair {proto:1, code, name}` 获得 `{deviceId, token}`,之后用 `hello` 携带设备 token 登录。默认存储如下;设置 `dataDir` 后位置随之变化:
100
+
101
+ | 文件 | 内容与保护 |
102
+ |---|---|
103
+ | `$DSH_HOME/pocket/token` | 自动生成的 owner token,权限 `0600`;配置固定 `token` 时不生成该文件 |
104
+ | `$DSH_HOME/pocket/devices.json` | 设备记录与设备 token 的 SHA-256,权限 `0600`,不保存设备 token 明文 |
105
+
106
+ WebSocket 管理操作要求 owner 身份。管理 UI 通过受本机信任边界保护的 HTTP 路由操作;已配对手机不因此获得开配对窗口的 owner 凭据。撤销设备会断开其已认证连接。配对设备仍具有广泛的 Host 控制能力,包括会话、终端与配置,请按主机管理权限保护它。
107
+
108
+ ### 本机管理路由
109
+
110
+ 以下路由在 **dsh 网页端口**,不是普通手机 RPC;要求 Host 为回环 authority,并拒绝 `Sec-Fetch-Site: cross-site` 与跨站 Origin:
111
+
112
+ | 路由 | 用途 |
113
+ |---|---|
114
+ | `GET /pocket/status[?network=lan|tailscale]` | 状态、网络候选、选中连接类型、地址、版本与设备 |
115
+ | `POST /pocket/pair[?network=lan|tailscale]` | 打开配对窗口 |
116
+ | `GET /pocket/pair[?network=lan|tailscale]` | 查询窗口与对应网络的 URI |
117
+ | `DELETE /pocket/pair` | 关闭窗口 |
118
+ | `GET /pocket/devices` | 设备列表 |
119
+ | `POST /pocket/devices`,body `{id}` | 撤销设备 |
120
+ | `PATCH /pocket/devices`,body `{id,name}` | 重命名设备 |
121
+
122
+ `network` 缺省时优先 LAN;未知值返回 400,不可用网络返回 503,均不打开配对窗口。
123
+
124
+ ## Host 能力与操作结果
125
+
126
+ 客户端以 `hello.ops` 及后续能力变更为准,不把 Host 上存在某个方法视为必定可用的产品入口。
127
+
128
+ | 领域 | 必须保留的契约 |
129
+ |---|---|
130
+ | 工作区 | 使用官方实体、成员关系和归档状态;有活动任务时保留官方拒绝详情,由用户确认停止后再提交。 |
131
+ | 目录选择 | `workspace.directoryList` 仅在官方 `browse` 模式公布;后端支持创建时才公布 `workspace.directoryCreate {path,name}`。返回官方绝对路径,保留空格及业务错误,不另写文件系统实现。仅有 `native` 时手机可输入完整路径,不弹电脑系统窗口。 |
132
+ | 文件目录订阅 | `files.follow {sessionId,path,subId}` 复用官方 `workspaceFiles.changes`,推送 `files` 的 `ready/change` 帧及规范化 `frame.path`;`unfollow` 取消。手机先等 ready 再读取,避免初始竞态;路径变更、断连和退出均取消旧订阅。 |
133
+ | Office 预览 | 可选 `office.render {sessionId,path,subId}` 调用官方 `officeToPdf.render`,返回 `{handle,absolutePath,version,generation,bytes,missingFonts,chunkBytes}`;`office.read {sessionId,handle,version,generation,offset,length}` 返回最多 128 KiB 的 base64 块;`office.release {sessionId,handle}` 或 `unfollow {subId}` 取消/释放,包括尚未返回的转换。仅支持 DOC/DOCX、PPT/PPTX,按 live/persisted session header 解析工作区,不激活 Agent。句柄绑定连接,读取前核对源版本;默认最多 8 个句柄、200 MiB 总预留、100 MiB 单输出,60 秒闲置过期,取消/断线/卸载清理。 |
134
+ | 指令与目标 | 指令来自 `commands.list`:有 `sessionId` 时读该 live Agent;无 `sessionId` 时用默认 preset 的 standing scope(桌面新会话)。技能目录有 `sessionId` 时转发 Host `sessionSkillCatalog.list`(桌面 `skills/list`),无 `sessionId` 时用默认 workspace cwd + 默认 preset scope 读 `ctx.skills.list`。两者都返回 `{ commands\|skills: [...] }`;目标更新使用官方 `id/revision`,不能从文本猜版本。 |
135
+ | `@` 引用 | 两类查询通过官方 `sessionController.resolveAgent(sessionId)` 加载或恢复 Agent;Host 重载后的历史会话也可直接查询,不以先发送消息为前提。`references.files` 调用 Host `fileReferences.list(agent, query, signal)`,返回 `{items:[{path,kind}]}`;`references.sessions` 调用 `sessionReferenceResolver.remoteExportCandidates(agent, query, signal)`(`candidates` 只是 Remote 名称),保留官方 canonical `mention`,将 `displayTitle ?? label` 映射为 `title`。`kind=subagent` 仅用于 `sessionQuery` 中 `origin=subagent` 且 `parentSession` 为当前会话的直接子智能体,其他候选为 `session`;普通 fork 不算子智能体。 |
136
+ | 后台任务 | `jobs.follow` 推送 `job` / `job.end` / `job.error`,共享 `unfollow` 只取消订阅,不终止任务。 |
137
+ | 页面更新 | `workspace.changed`、`schedule.changed`、`plugins.changed` 驱动对应页面;预设目录和默认值来自 Host。 |
138
+ | 共享设置 | `plugins.settingsDescribe/settingsMutate` 复用官方配置;写入携带 `expectedRevision`,冲突重新读取,不静默覆盖。`settings.changed {ns,revision}` 通知手机更新镜像。 |
139
+ | 模型与凭据 | 订阅 `settings.changed`、`credentials.changed`、`models.changed`、`plugins.changed`。`credentials.describe {ref}` 仅返回状态元数据,不回传密钥;删除提供商先完成配置 CAS,再清理该页面管理的密钥。 |
140
+ | 插件目录 | `plugins.inventory` 独立返回全局 `entries` 和 `agentPresets[].rows`,不依赖包管理,也不将会话预设声明转成全局开关。 |
141
+ | 插件管理 | `plugins.list/setEnabled` 以 `entryId` 关联组件;检查、安装、取消、等待与卸载按 Host 可用方法公布。来源支持 npm、Git、tarball 和主机路径,不是手机本地目录。 |
142
+ | 自动化 | `schedule.catalog/list/create/update/delete/history` 保留官方记录、时区与业务结果;编辑带完整 `expected` 记录。手机新建入口进入会话,投递成功不表示模型执行完成。 |
143
+
144
+ `ok: true` 只表示 RPC 返回。客户端还需检查插件 `application`(如 `restart-required/overridden/failed/cancelled`)与任务 `updated/deleted/code`。安装等待最长 30 分钟,不沿用普通 RPC 的 60 秒超时;不确定的安装保留 `requestId` 供核对,不能自动重复提交。
145
+
146
+ 完整原生入口与限制见 [功能对齐清单](https://github.com/minglu6/dsh-android/blob/main/docs/feature-parity.md) 和 [Android 使用说明](https://github.com/minglu6/dsh-android/blob/main/app/README.md)。
147
+
148
+ ## 会话流、审批与恢复
149
+
150
+ - `follow` 支持 `afterSeq`,断线重连携带上次 cursor 续订,Host 按序补帧。手机的排队消息以 Host 收件箱为准;`queue` 等待后续轮次,`steer` 引导当前轮次,详见 [发送语义](https://github.com/minglu6/dsh-android/blob/main/app/README.md#排队与插话)。
151
+ - `sessions.list` 的精简摘要保留 `projections.values.title` 与 `agentPreset` 这两个轻量页头字段,仍剔除 usage、turnOutline、contextBreakdown 等大投影。预设缺失表示未知,显式 `null` 原样保留;Android 可据同一会话的真实预设初始化首帧,再由历史快照校准,不能填入默认模式。此修复需要同时更新 App 与运行中的桥接。
152
+ - 会话顶部投影:`sessions.projections {sessionId,parentSessionId?,subagentMode?}` 仅在 Host 有 `sessionController.projections` 时公布;原样返回 `null`、`{kind:"sequenced",asOfSeq,values}` 或 `{kind:"migration-required",values}`。缺失键表示未知,不等于空团队或空目录。`agentTeam`/`subagentCatalog`/`todos` 增量随 `follow` 的 `projection` 帧发送,包含 `todos: null` 的权威清空;初始快照先于增量,快照已覆盖的缓冲增量丢弃,不能跨会话投递。
153
+ - 全日志轮次大纲 `turnOutline` 保持官方数组结构,通过快照/续订基线和同一会话的增量投影发送;Android 结合已加载行构建完整导航刻度,未加载轮次按真实起始 `seq` 使用既有 `history.page` 补页,不把历史游标与投影水位混用。新增实时广播白名单需要重新加载运行中的桥接;只更新 APK 不会热替换旧实例。
154
+ - 子会话地址:`follow`、`history.page`、`sessions.projections`、`prompt`、`cancel` 均可携带成对的 `parentSessionId` + `subagentMode`(`one-shot` / `continuable` / `unknown`);不能是同一会话,`external` 无本地会话不可打开。历史及续订补页使用官方 `{kind:"subagent",parentSessionId,childSessionId,mode}`,投影读取先通过官方空前缀历史读校验父子归属。省略两字段时保留原 root 路由,不把 child ID 冒充 root。
155
+ - 子会话控制仅接受 `continuable`:`prompt.mode` 仍是 `queue` / `steer`,经官方 `subagents.prompt` 映射为 `delivery`,返回原始 `{messageId}`;只接受文字及图片,不接受文件。`cancel` 经 `subagents.interruptByParent(childId,parentId,"continuable")` 返回 `{accepted:true}`。缺少对应官方服务时报 `subagent/control-unavailable`,不会回退至 root 控制器。安装新手机应用不更新 Host;这些能力需构建并更新桌面运行中的 pocket 插件后才生效,本文变更本身不执行部署或重启。
156
+ - Host 发起 `approval/request` 时向在线设备推送 `approval`。`approval.answer` 接受 `allowed-once` 或 `rejected`,手机与网页谁先答谁生效;另一端收到关闭/取消通知。工具取消对应 `cancelled`,新连接补发待审批项,也可用 `approval.pending` 查询。
157
+ - `user-questions/request` 同时保留手机与桌面的应答通道;`user-question` 推送与重连重放使用 `sessionId` + `questionId/callId` 标识。`userQuestions.answer` 提交完整 `answers` 批次,每题恰好一项;同一实时请求只接受一次,另一端结算、取消或超时会推送带 `sessionId` 的 `user-question.closed`。
158
+ - 提问状态与已排队回复分别由 `userQuestions`、`inbox` 投影同步。已继续的问题通过官方 `userQuestions.answer` 提交,必要时由 `sessionController.resolveAgent` 恢复会话;不伪造普通聊天消息。手机连接对限时等待使用官方 `attachWait` 剩余时长,断线释放接手,不将限时问题无限挂起。
159
+ - 用量与权限来自 `sessionStats`、`tokenUsage`、`contextPressure`、`contextBreakdown`、`permissions` 等 Host 投影,仅推送给已认证且订阅该会话的客户端。
160
+ - 首个 snapshot/resume 前的投影变更按 key 暂存,先发基线再发较新投影。**投影 `seq` 是投影水位,不用于推进历史 cursor。**
161
+ - `permissions.catalog` 返回 `options/defaultOptions/defaultPreset`;自动审查只在 Host 注册 `auto` 时可选,且不能作为默认权限。`permissions.get/set` 保持 `{current:"…"}` 格式,目录变更通过 `permission-presets/catalog-changed` 通知客户端重新读取。
162
+
163
+ 上述实时通知需要运行相应版本桥接;只更新 App 无法让旧 Host 产生新事件。
164
+
165
+ ### 双端完成未读状态
166
+
167
+ - Host 桥接维护 `{epoch, revision, unreadIds}`,运行开始清除旧提醒,运行结束且没有查看端时产生提醒;历史空闲会话不会在初始化时全部标为未读。
168
+ - 手机通过 `completion.state` 读取快照、`completion` 事件接收更新,使用 `completion.view {sessionId:string|null}` 声明当前实际可见会话。`follow` 不等于查看;连接断开自动释放查看状态,未读保留至查看或 Host/桥接重启。
169
+ - 桌面通过同源 `GET /pocket/completion/events` SSE 接收 `{clientId,...snapshot}`,以 `POST /pocket/completion/view {clientId,sessionId}` 回写查看状态。路由沿用本机 Host/Origin 信任边界;SSE 断开后旧 `clientId` 失效。
170
+ - 桌面优先使用 `uiSession.setCompletionUnreadSource`(若实际挂载的 client 提供)。官方 `ui-session@0.2.1-alpha.2` 没有该钩子:改为叠加同一对象上的 `sessionStatus`(`completionUnread` 以 Host 快照为准,running/pending 仍来自官方),并通知官方 `statusListeners`。缺少 `sessionStatus` 或监听集合则抛错,不 warn 后假装已同步。
171
+ - 叠加后的状态快照仅在官方状态或 Host 未读快照变化时重建;重复读取保持同一对象,避免 React 外部状态订阅重复更新。
172
+ - 页面失焦、隐藏、全局面板/设置覆盖或内容未加载时不确认已读。手机插件设置 WebView 不代理上述路由,由原生会话界面管理查看状态。客户端按连接代际和快照版本拒绝过期响应,防止绿点被旧数据恢复。
173
+
174
+ ## 插件自定义 Web 界面
175
+
176
+ 仅当 Host 提供 `webServer` 与 `connection` 时公布 `ui.open({section?})` / `ui.close({ticket?})`,供已认证客户端按需使用。Android 模型页、内置插件目录和插件管理仍保留原生界面;通用 Host Web 界面入口由 Android APK 发布单元提供,**只更新此 Host 插件不会为旧 APK 增加入口**。已有 Qoder CN 原生卡片继续复用 `models.settingsMutate` 和写入型 `credentials.*`,不增加认证实现或新协议。
177
+
178
+ 1. `ui.open` 返回 `{ticket,path:"/pocket-ui/open"}`;原生以表单 POST 兑换 `dsh-pocket-web` HttpOnly / SameSite=Strict cookie,并跳转至不带票据的 303 地址。
179
+ 2. 单次票据有效 60 秒,网页会话 15 分钟,绑定设备、当前 WS 连接与 authority;断连、撤销或卸载结束相关 HTTP/SSE/WS 会话。
180
+ 3. 默认仅根页面、官方 assets/plugins/API 及 `/api/remote.mux` 代理到固定本机 Host listener;第三方私有 HTTP 路由默认拒绝。启动令牌只在服务端兑换内部 cookie,不向手机页面回传 Host 启动令牌、Host cookie 或敏感重定向。
181
+ 4. 拒绝 `/pocket/*`、`/open-in-app/*`、任意上游 URL、路径绕过和跨站请求。页面请求与 WS 由 CSP 限定为当前手机端口 origin;不提供第三方脚本到 Android 的 JavaScript bridge、Electron API、设备文件选择或下载。
182
+
183
+ 管理员可在上面的 Host `config` 中设置 `pluginUiPaths: ['/dsh-purge', '/redteam']`,允许已认证网页访问这两个 namespace 自身及其 `/` 子路径,例如 `/dsh-purge/status`、`/redteam/api`,不会放行 `/redteams` 等相似前缀。配置只接受 `/` 后一个小写 ASCII 字母/数字开头、后续由小写字母、数字、`_`、`-` 组成的单段 namespace;不能带尾斜杠、子路径、查询、片段、编码或完整 URL。`pocket`、`pocket-ui`、`open-in-app`、`api`、`assets`、`plugins`、`discovery` 为保留 namespace,配置时直接报错;不改变原有官方路径白名单。
184
+
185
+ **这不是只读资源授权。** 放行 namespace 等价于允许已配对设备在有效网页会话内调用该插件的 HTTP 管理接口,包括同源 POST、查询和请求体;请仅授权明确可信的插件及其独占根路径。设备不能通过 RPC 指定代理地址或自行扩充授权。鉴权、同源写入限制、响应脱敏、CSP 和断连撤销规则继续生效;插件专用 WebSocket 路由不会因此开放,仍只支持官方 `/api/remote.mux`。
186
+
187
+ 不指定 `section` 时打开完整 Host Web shell,兼容的已启用 Web 插件可显示其启动入口、覆盖层及自定义页面,无需逐插件新增 RPC 或修改插件源码;插件私有 HTTP 接口需要另行配置上述授权。`/?pocket-ui=1&section=models` 打开官方模型设置及第三方卡片;设置深链等待 session baseline,使用官方设置导航,不增加等待说明、引导按钮或替换根组件。正常桌面窗口不受影响。
188
+
189
+ 已认证页面在官方客户端启动前注入独立的 `__DSH_TRANSPORT__={ownsHost:true}`,使 LAN/Tailscale 页面具备与原生已配对连接相同的 Host 管理声明。上游 transport/native/recovery 声明被剥离,不继承其函数、`streamBaseUrl`、进程令牌或 Host cookie;未认证请求无法取得该页面。修改这条链路时必须验证非回环地址,不能只测 USB `127.0.0.1`。
190
+
191
+ HTML 在脱敏后按 `Accept-Encoding` 协商 gzip,JavaScript/CSS/JSON 同样可压缩;SSE、Range/206、HEAD 与上游已压缩响应不重编码。以上措施**不增加 LAN HTTP 的传输加密**,也不保证任意第三方插件兼容或可信。Android WebView 的 cookie 存储仍为应用级共享;服务端仅接受唯一有效且匹配当前 authority 的会话。
192
+
193
+ 手机启动保留全部官方模块与依赖,但把 application combo 拆为逐模块请求,移除原大包预加载。账户模块中的大型 onboarding PNG/WebP 导入改为同源鉴权图片 URL;原图字节与 MIME 类型不变,只有页面实际使用图片时才读取,避免上游切换图片格式后再次把引导图嵌入启动脚本。资源按内容寻址、在服务作用域内有界保留,退出或撤销设备后不能继续读取;不裁剪插件功能或关闭 WebView 安全检查。
194
+
195
+ ## 开发与测试
196
+
197
+ 在 `plugin/` 目录执行:
198
+
199
+ ```bash
200
+ npm install
201
+ npm run build:client
202
+ npm run lint
203
+ npm test
204
+ ```
205
+
206
+ 仅运行本地 Host UI 消费者行为回归(不触发 `pretest` 或客户端构建):`node --test test/host-ui.test.js`。该 fixture 覆盖默认拒绝、显式 namespace 授权后的 GET/POST 透传、路径与配置边界、跨站写入拒绝,以及设备断连后的私有路由失效;不连接现用 Host 或读取其凭据。
207
+
208
+ `npm test` 的 `pretest` 会重建客户端。若需覆盖遗留 sidecar 相关测试,先准备 Rust 工具链并执行 `npm run build:iroh`;CI 会先构建 sidecar,再将二进制提供给插件测试。
209
+ 遗留 sidecar 的握手/配对端到端用例使用真实 QUIC 与 Node pipe bridge,但通过显式回环地址隔离公网发现和中继可用性;不会退回 WebSocket,也不跳过认证。测试模式与默认中继行为见 [`iroh/README.md`](iroh/README.md#local-transport-tests)。
210
+
211
+ 真实 Host 集成测试需要 PATH 中的 dsh 与 pnpm:
212
+
213
+ ```bash
214
+ npm run test:integration
215
+ ```
216
+
217
+ 只验证真实 Host 的 `@` 引用契约(文件候选、标题映射、canonical mention、自身排除、Host 重启后旧会话的并发引用查询及不存在会话的真实错误),可执行 `node --test --test-name-pattern='file and session reference contracts' test/integration/dsh.test.js`。测试使用临时 `DSH_HOME` 与回环桥接端口,复用临时目录验证真实进程重启,不重启当前桌面 Host;它调用真实服务,不用同名 mock 模拟 Host 方法。PATH 中没有 `dsh` 时会跳过,不能把跳过视为通过。
218
+
219
+ Android 协议联调还需 JDK 17 与 Android 开发环境:
220
+
221
+ ```bash
222
+ node test/integration/kotlin-live.mjs
223
+ ```
224
+
225
+ 该脚本启动临时真实 `dsh web` 并执行 Kotlin 协议测试。真实模型测试需要额外凭据,会调用模型与工具,仅供显式本地执行,见 [Android 测试说明](https://github.com/minglu6/dsh-android/blob/main/app/README.md#构建与测试入口)。
226
+
227
+ ### 协议与源码导航
228
+
229
+ - [设计文档第 4 节](https://github.com/minglu6/dsh-android/blob/main/docs/design.md#4-protocol-spec-v1-json-over-websocket-text-frames):协议 envelope 与早期核心操作;该文档含历史规划,不是全部当前 RPC 的清单。
230
+ - [上游 API 笔记](https://github.com/minglu6/dsh-android/blob/main/docs/api-notes.md):dsh Remote RPC、会话事件及权限模型研究。
231
+ - [`src/server.js`](src/server.js):鉴权、操作分发与会话订阅;[`src/parity-ops.js`](src/parity-ops.js)、[`src/settings-ops.js`](src/settings-ops.js)、[`src/extra-ops.js`](src/extra-ops.js):扩展操作。
232
+ - [`src/host-ui.js`](src/host-ui.js):插件自定义页面的 Host 代理;[`src/manage.js`](src/manage.js):本机管理路由。
233
+ - [Android `PocketClient.kt`](https://github.com/minglu6/dsh-android/blob/main/app/core/protocol/src/main/kotlin/dev/dsh/pocket/protocol/PocketClient.kt):当前客户端协议调用入口。
234
+ - [CI 配置](https://github.com/minglu6/dsh-android/blob/main/.github/workflows/ci.yml):固定 dsh 版本、插件检查与 Android 联调流程;[第三方声明](https://github.com/minglu6/dsh-android/blob/main/docs/THIRD_PARTY_NOTICES.md):上游参考与资产归属。
235
+
236
+ ## 遗留桌面 sidecar
237
+
238
+ `iroh/` 中的 `pocket-host` 仅在 `config.iroh: true` 时启动,供桌面侧实验,不参与手机配对。Android APK 不包含 `libpocket_iroh.so`/JNI。普通 Android 使用者无需构建或安装它;CI 仍构建 Linux x64 与 macOS arm64 二进制供相关测试和可选发布产物使用。
package/bin/pocket.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // CLI: node bin/pocket.js pair|devices|revoke <id> [--url ws://127.0.0.1:7690] [--token-file ~/.dsh/pocket/token]
3
+ import { readFileSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import WebSocket from 'ws'
7
+
8
+ const argv = process.argv.slice(2)
9
+ const flag = (n, d) => { const i = argv.indexOf(n); return i >= 0 ? argv.splice(i, 2)[1] : d }
10
+ const url = flag('--url', 'ws://127.0.0.1:7690')
11
+ const tokenFile = flag('--token-file', join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'pocket', 'token'))
12
+ const [cmd, arg] = argv
13
+ const token = readFileSync(tokenFile, 'utf8').trim()
14
+ const ws = new WebSocket(url)
15
+ let id = 0; const pend = new Map()
16
+ ws.on('message', (d) => { const m = JSON.parse(d); pend.get(m.id)?.(m) })
17
+ const call = (op, args = {}) => new Promise((r) => { const i = ++id; pend.set(i, r); ws.send(JSON.stringify({ id: i, op, args: { proto: 1, ...args } })) })
18
+ ws.on('error', (e) => { console.error(e.message); process.exit(1) })
19
+ ws.on('open', async () => {
20
+ const h = await call('hello', { token })
21
+ if (!h.ok) { console.error(h.error); process.exit(1) }
22
+ let r
23
+ if (cmd === 'pair') r = await call('pairing.open')
24
+ else if (cmd === 'devices') r = await call('devices.list')
25
+ else if (cmd === 'revoke') r = await call('devices.revoke', { deviceId: arg })
26
+ else { console.error('usage: pocket pair | devices | revoke <deviceId>'); process.exit(2) }
27
+ if (!r.ok) { console.error(r.error); process.exit(1) }
28
+ if (cmd === 'pair') console.log(`配对码 / pairing code: ${r.result.code} (${Math.round(r.result.expiresInSec / 60)} min)`)
29
+ else console.log(JSON.stringify(r.result, null, 2))
30
+ ws.close()
31
+ })
@@ -0,0 +1,6 @@
1
+ - insert:
2
+ - id: dsh-pocket
3
+ name: "@huakirax/dsh-plugin-pocket"
4
+ config:
5
+ host: 127.0.0.1
6
+ port: 7690