# dsh-companion
**Repository Path**: westanke/dsh-companion
## Basic Information
- **Project Name**: dsh-companion
- **Description**: DSH 的 Android 伴侣端(fork 自 Hakunm/dsh-android-app):一台电脑多地址、配置导入取代扫码、旧数据自动迁移;会话里的 dsh-ui 交互卡片原生渲染,文件按类型预览(Markdown / HTML)。
- **Primary Language**: Unknown
- **License**: AGPL-3.0
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-10-08
- **Last Updated**: 2026-10-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# dsh-companion
把你自己的 DeepSeek Harness 带到 Android 手机上。
中文 · English
## 关于本仓库
本仓库是 [Hakunm/dsh-android-app](https://github.com/Hakunm/dsh-android-app) 的**衍生版本(fork)**,
在上游 `v1.0.0` 的基础上继续开发。它保留上游的完整提交历史,因此归属关系可逐条追溯。
- **基于什么**:上游 [Hakunm/dsh-android-app](https://github.com/Hakunm/dsh-android-app)(Android 原生客户端,Jetpack Compose + Material 3)。
- **为什么有它**:上游的连接模型只支持「一台电脑 = 一个地址」,在真实移动场景下不够用。具体见下一节。
- **许可证**:上游以 **AGPL-3.0** 发布,本仓库沿用同一许可证,见 [LICENSE](./LICENSE)。
上游的版权声明、[NOTICE](./NOTICE) 与 [THIRD_PARTY_LICENSES](./THIRD_PARTY_LICENSES) 全部保留未改动。
- **我们改了什么、为什么、测了什么**:见 [开发日志](./docs/project/CHANGELOG-DEV.md)。
## 这个版本解决什么问题
手机端日常使用暴露出四个问题,它们看起来互不相干,实际上指向**同一个根因**:
旧实现把「一台电脑」和「一个地址」当成了同一个东西。它的数据模型只有:
```kotlin
data class StoredConnection(val endpoint: String, val token: String)
class DshClient(endpoint: String, token: String?)
```
`endpoint` 是**单个字符串**。于是:
| 实际遇到的问题 | 真正的根因 |
|---|---|
| 只能添加一条路径 | 模型里只有一个 `endpoint: String`,装不下第二个地址 |
| 出门在外没法配对 | 配对码必须在电脑上生成,而那时人已经不在电脑旁 |
| 局域网 / 虚拟网 / 公网不能同时保存 | 同上:只有一个地址位 |
| 会话里提到的文件看不了 | 会话事件中的绝对路径没有与授权根做匹配 |
### 关于「出门在外」这个场景
这一条值得单独说,因为它推翻了上游的一个前提。上游的连接方式是**一次性配对码**:
在电脑的 WebUI 上生成,拿到手机上输入。这个流程默认「人和电脑在一起」。
但真实场景恰恰相反:**人已经出门了,电脑留在家里,屏幕根本看不到**。此时
「去电脑上生成一个配对码」不是难用,而是**做不到**。扫码同理 —— 对着看不到的屏幕扫不了。
所以本版本把「配置导入」作为主路径:
```
电脑上生成一行文本 → 发给自己(微信 / 邮件 / 私密笔记)→ 到了外面粘贴进 App
```
全程不需要电脑在旁边,也不需要摄像头。配对码被保留为**次要入口**(你正坐在电脑前时它更快),
并在界面上明确标注了这个前提条件。
## 本仓库相对上游的改动
### 1. 一台电脑,多个地址
连接层重新建模(`app/src/main/java/io/github/hakunm/deepseekharness/data/ConnectionModels.kt`):
```
Host 一台电脑:名字、凭据、若干地址、上次成功用的地址
├─ Endpoint 一个可达地址:标签、baseUrl、类型(局域网/虚拟网/公网)、探活历史
└─ Credential 凭据:挂在电脑上,而不是挂在地址上
```
**关键点:这个改动不需要服务端做任何修改。** DSH 的 `devices` 表结构是
`id, name, token_hash, scopes_json, created_at, last_seen_at, revoked_at` ——
**根本没有地址列**。设备令牌天然是主机级的,同一个令牌在任何地址上都有效。
也就是说「多地址」一直是客户端本来就能做到、只是过去没做的事。
启动时的行为:并发探测这台电脑的**全部**地址(每个地址独立超时,实测 1.5 秒),
按「延迟优先、局域网近似即优先」选路,失败自动回退到下一个可达地址。
另外,**启动永远先落在连接页**:自动重连成功也不会直接进应用,要由用户的一个主动动作放行
(点「进入应用」/ 导入配置 / 配对)。
### 2. 配置导入取代扫码
`ConnectionShare.kt` 定义了线格式 `DSH1:`,解码端对真实粘贴环境做了容错:
微信插入的换行与零宽字符、前后带标签文字(`Pixel 9 的配置:DSH1:xxx`)、缺失的 `=` 填充、
标准与 URL-safe 两种 Base64 字母表、结尾被粘上的标点 —— 全部能正确还原。
生成端是 `tools/emit-config.mjs`。
### 3. 地址诊断
全部地址都连不上时,不再只说一句「连接失败」,而是逐条列出每个地址的结果:
```
局域网 192.168.1.126:3090 Unreachable — timeout after 1500ms
虚拟网 100.64.250.1:3090 Reachable · 38 ms
```
过去用户只能靠猜「我该填哪个地址」,现在他看得到。
### 4. 断开不再清除凭据
上游把「断开连接」和「删除令牌」绑成同一个按钮。于是网络抖一下、手点错一次,
用户就得重新配对 —— 而重新配对又要求他人在电脑旁。
现在断开只断连(`disconnect`),删除电脑是独立动作(`removeHost`)。
### 5. 旧数据自动迁移
从旧版升级时,已保存的地址与设备令牌会自动迁移成新的 `Host` 结构,无需重新配对。
迁移是幂等的,且**旧格式的令牌会用旧的 Keystore 别名解密**后搬过来。
### 6. 插件清单页
手机上多了一个「插件」页签,能看到这台机器装了哪些插件、**实际跑的是哪个版本**、以及有没有异常状态。
这一条**必须动服务端**:`dsh-remote-bridge`(当时名为 `dsh-workspace`)的 `/api/v1` 原有 10 个端点
(`healthz` / `pairings/exchange` / `devices/self` / `roots` / `trash` / `chat/sessions` /
`chat/workspaces` / `chat/agent-presets` / `settings/models` / `settings/providers`),
**没有任何一个与插件相关**,所以先在插件侧加了 `GET /api/v1/settings/plugins`。
它区分四种状态,其中两种只有对比两个数据源才能得出(`package.json` 的 `dependencies` =
装了哪些包,`dsh.profile.bundles` = 实际加载了哪些包):
| 状态 | 含义 |
|---|---|
| `loaded` | 声明加载且已落盘 |
| `runtime-provided` | 官方包随 DSH 运行时自带(不在 profile 的 `node_modules` 里)—— **正常状态** |
| `installed-not-loaded` | 装了但没启用 |
| `declared-missing` | 非官方包声明加载却缺失,通常意味着启动会出问题 |
「装了哪些」与「跑着哪些」的差别正是它的价值所在 —— 例如本机实测发现
`dsh-hyperframes` 与 `dsh-remotion` 装了但未启用。
> **DSH 0.2.x 兼容性**:本分支同样包含上游修复的枚举契约问题 —— 0.2.x 的 `AgentPreset`
> 不再返回 `trust` 字段,而客户端严格解码要求该字段必填,导致 Agent 预设选择器报
> `Field 'trust' is required ... missing at path: $.items[0]` 并整体打不开。
> 修复方式是给可选字段补默认值,使**单个字段缺失不再拖垮整个列表**。
### 7. 运行时的发送行为可配置:排队 / 插话(v1.3.0)
运行中按发送键之后会发生什么,此前是写死的。现在设置页有一组单选项,并且运行中会在发送键旁
显示当前默认(`回车=排队` / `回车=插话`),不必回设置页确认。
同一版还修掉了一个更要命的问题:**智能体运行时根本发不出消息**。旧实现用一个全局 `busy`
标志禁用发送键,而这个标志会被**任何一次后台刷新**置位 —— 于是「运行中不能发」实际变成了
「随时不能发」。会话中提到的文件也在这一版可以直接打开。
### 8. 会话图片可见 + 消息可复制 + 链接可点 + 发图 / 发文件(v1.4.0)
「会话里的图片看不见」是**两个问题叠加**,只修任何一个都不生效:
1. 只发图、不打字的消息被判成**空消息**,整条丢弃;
2. 历史图片在事件里只是一个 `attachmentId` **引用**,字节需要另发一次请求去取。
此外消息可以复制、链接可以点、输入区可以发图片和文件(图片内联 base64,文件先换取 receipt)。
附件体积在**发送前**拦截(图片 8 MiB / 文件 24 MiB),给的是能照做的动作(「先压缩再试」),
而不是一句「文件过大」。
### 9. dsh-ui 交互卡片原生渲染(v1.5.0)
`dsh-ui` 围栏里的 JSON 在网页版会渲染成真正的组件。App 原来把围栏当代码块画,用户看到的是一坨
**裸 JSON** —— 最直接的后果是**助手在网页版弹给用户的选择题,手机上没法回答**。
现在 22 种组件(卡片 / 表格 / 列表 / 按钮 / 选项 / 进度 / 时间线 …)原生渲染,卡片里的选择按钮
等价于用户打字发送,走同一条消息通道。
### 10. 会话产生的文件列在消息流末尾(v1.6.0)
对齐网页版形态:本轮回复结束后,会话产生的文件自动列在消息流末尾(显示条数、可折叠、点开看内容)。
顶栏的文件夹图标**保留**,两者不冲突。
刻意**不**解析 `bash` 命令里的路径 —— 那里面大多是搜索范围、变量或 `echo` 出来的文本,
**误报比少列更糟**(点一个打不开的路径,用户会以为功能坏了)。
### 11. dsh-ui 诊断开关(v1.7.0)
设置页「调试」组新增一个开关(默认关)。打开后每条助手消息下方显示一行灰字,说明这张卡片为什么
没渲染:**围栏识别 → JSON 解析 → 节点分发**三段各报一次结果,点击展开、长按复制。
「围栏没识别」「JSON 坏了」「节点类型不认识」这三种故障原本在屏幕上长得一模一样,
在没有可用设备的环境里只能靠猜。
### 12. 发送失败不吞字 + 取图失败说清原因(v1.8.0)
- **发送失败不再吞字**:原文自动放回输入框。此前失败只还回附件,文字直接丢 ——
打了一长串字、发送失败、输入框空了,一个字都找不回来。
- **取图失败显示真实原因**:401 授权过期 / 403 缺 `files.read` / 404 附件真没了分开提示,
不再一律显示「图片已不可用(可能已被清理)」。授权类失败还允许重试 ——
此前失败一次就永久记住,重新配对也不能自愈。
### 13. 长按选中片段 + 文件按类型预览(v1.9.0)
**手势分工**:长按留给「选中片段」,整条复制改用**双击**。此前长按被「复制整条」消费掉,
内层选择手柄根本弹不出来,用户只能整段复制。消息流顶部有一次性的可关闭提示说明这个改动。
**文件预览按类型分流**:`.md` 走 Markdown 渲染,`.html` 走结构化抽取
(标题 / 段落 / 列表 / 引用 / 代码 / 链接,**丢弃 `script` 与 `style`**)。
刻意**不用 WebView** —— 那等于让会话里出现的任意文件在手机上拿到执行权。
### 14. 2.0–2.2 版本线:WebView 聊天页,以及与原生对齐
2.0.0 起,会话详情页多了一条 **WebView 实现**(`assets/chat.html`,把 Markdown 与卡片渲染交给浏览器),
与原生 Compose 版并存、由设置里的「聊天页实现」开关切换(**默认仍走原生** —— 原生版是走过真机验证的资产)。
2.2.15 起,把网页版那套呈现方式反向移植回原生。这条线到 2.2.20 的主要变化:
- **连接页是入口,不是兜底**:启动永远先落连接页(进程级门闩 `connectGate`,只由用户的主动动作放行);
冷启动自动重连成功也仍然停在连接页。**导入配置**与**手动配对**两种方式默认都收起、点哪个开哪个,
且互斥展开 —— 屏幕上不会同时铺两份表单。
- **输入区照网页版重做**:药丸压成一行(`+` · 输入框 · 发送),模型 / 智能体 / 权限模式 / 插话收进顶栏
`⋯` 菜单,附件并入 `+`;**输入框高度随内容自适应**(44–120dp;此前写死 34dp,敲到第二行就被裁)。
- **思考条在输入区上方一行**:流式期间只显示「当前思考」,取尾部、过长省略号截断;没有思考时整条不渲染
(不占高度)。历史里的思考过程不再显示。
- **工具显示改官方口径的动作短语**(「运行了代码,修改了文件」),且**连续的工具调用合并成一行**;
此前一次调用一行,跑几十次就是一屏。
- **上下文注入默认隐藏**:系统每轮塞进来的运行上下文 / 时间上下文 / 后台任务通知默认不显示,
需要排查时在设置里打开。
- **模型供应商配置独立成「模型」页**(导航里的「模型」),不再是设置页里的一块;设置页因此从 697 行瘦到 297 行。
- **设置页开关统一形态、点一下立刻生效**:三个开关(用网页版聊天页 / 显示上下文注入 / dsh-ui 卡片诊断)
统一走同一套 `SwitchRow`,状态进 `HarnessState`。此前直接读 `SharedPreferences`,那不是 Compose
状态,于是表现为「点了开关不动、得再点别的才跳」。
- **会话交付物直读内核事件**:会话底部与标题栏的文件夹弹层都改读内核的 `deliverables/presented`
(`present` 工具触发,带绝对路径与人写的说明)。此前只认三种会话事件,agent 交付的报告与安装包
整类看不到;两处共用同一个 `sessionFileList()`,不会再出现「底部是交付物、弹层是一堆嗅出来的路径」。
这一段的逐版取舍与踩坑记录在 `app/build.gradle.kts` 的版本注释与 `docs/project/CHANGELOG-DEV.md`;
「移植未实现项」清单在 `docs/project/webview-port-todo.md`。
## 构建
```bash
scripts/build.sh # 默认 assembleDebug
scripts/build.sh assembleRelease # 发布包
scripts/build.sh :app:testDebugUnitTest # 单元测试
```
`scripts/build.sh` 的存在是有原因的,不是多余的包装:
1. **绕开 Gradle wrapper 的静默挂起。** `gradle-wrapper.properties` 里的
`distributionUrl` 指向 `services.gradle.org`,该地址在国内网络下不可达,而 wrapper
下载失败时**不报错退出,而是挂起**(实测:CPU 0.3%、零网络连接、无 daemon 日志,十几分钟无输出)。
脚本直接调用本地已缓存的 Gradle 二进制。
2. **依赖镜像。** 本机无法访问 `maven.google.com`,需要经阿里云镜像重定向。
注意用的是**本项目自带**的 `scripts/init-mirrors.gradle`:工具链里那份会往 project 级
仓库追加,与本项目 `settings.gradle.kts` 的 `RepositoriesMode.FAIL_ON_PROJECT_REPOS`
冲突并直接构建失败。
3. **串行化。** Gradle 对同一项目目录是互斥的,脚本用 `flock` 保证并发调用不会互相抢锁。
## 下载与安装
从本仓库的 Releases 下载(两站内容相同,任选其一):
- GitHub:
- Gitee(国内访问更快):
```text
DeepSeek-Harness-companion-v2.2.32.apk
```
安装要求:
- Android 8.0(API 26)或更高版本
- 已运行的 DeepSeek Harness WebUI
- DSH WebUI 已安装 `dsh-remote-bridge` **≥ 2.3.3**(旧名 `dsh-workspace`;2.0.0 / 2.0.1 有严重缺陷:
图片读取全部失败、插件无法激活)
- 插件中至少添加了一个授权根,并已启用远程访问
- 手机可以访问插件配置的 IP、域名和端口
**签名现状(重要):`v2.2.25` 起使用重新生成的正式证书(`CN=DSH Pocket Client`,主题与旧证书一致)。**
项目原来那把发布证书的**密码已丢失**(本机 / 仓库 / CI / 记忆库全查过),所以现在这把是重新生成的:
主题刻意保持一致,但它**不是旧钥匙**。几个直接后果:
- **`v1.9.0` 及更早的包盖不上**:签名不同,必须先卸载再装(卸载会清除已保存的连接,
装好后重新配对,或直接粘贴一份配置文本)。
- **`v2.2.25` 及以后可以互相覆盖升级**(同一把证书)。
- 与上游原版(`CN=Hakunm`)的证书同样不同,从上游版本切过来也要先卸载。
`applicationId` 与上游保持一致(`io.github.hakunm.deepseekharness`)。
发布历史:`v1.1.0` / `v1.2.0` / `v1.4.0` / `v1.9.0` 各有独立 tag;`v1.5.0`–`v1.8.0` 没有单独打 tag,
这四个版本的功能随 `v1.9.0` 一并发布。`v2.0.0`–`v2.2.19` 之间没有 tag;`v2.2.20` 与 `v2.2.23` 已打 tag 并双站发布,
产物见仓库 `artifacts/` 与 Releases 页。
## 界面预览
## 连接你的 DSH
### 服务器端
1. 在 DSH WebUI 打开 `dsh-remote-bridge`(旧名 `dsh-workspace`)的“工作区设置”。
2. 添加至少一个授权根目录。
3. 在“远程访问”页填写绑定 IP 和端口,并保存监听设置。
4. 点击“启用并创建配对”,取得十分钟内有效的一次性配对码。
### 手机端
打开 App 会先落在**连接页**,上面有已保存的电脑,以及两个默认**收起**的入口(点哪个展开哪个,一次只开一个):
1. **从电脑导入配置**(主路径):把配置文本粘贴进来即可 —— 人已经出门、看不到电脑屏幕时,只有这条走得通。
2. **手动配对**:填服务器地址(例如 `http://192.168.1.20:3090` 或 `https://dsh.example.com`)、配对码和设备名称,
再点“配对并连接”。人正坐在电脑前时它更快。
> **地址格式:千万不要带 `/dsh-workspace-api` 前缀。**
> 这一点很容易搞错,而且错了必然连不上。实测:
>
> | 请求 | 结果 |
> |---|---|
> | `http://192.168.1.126:3090/api/v1/healthz` | 200 ✅ |
> | `http://192.168.1.126:3090/dsh-workspace-api/api/v1/healthz` | 401 ❌ |
> | `http://127.0.0.1:3080/dsh-workspace-api/api/v1/healthz` | 200(但只绑 loopback,手机到不了) |
>
> 原因:`3090` 是**插件的远程监听端口**,路由直接挂在根上、**不带前缀**;而
> `/dsh-workspace-api` 是 DSH Web 在 `3080` 上的挂载面,只绑 `127.0.0.1`。
> 客户端 `DshClient.normalizeEndpoint()` 会自动补 `/api/v1`,所以如果你填了
> `http://ip:3090/dsh-workspace-api`,实际会请求 `/dsh-workspace-api/api/v1/...`,
> 而该路由在 3090 上并不存在。
>
> 插件虽然已经改名 `dsh-remote-bridge`,但 Web 挂载前缀仍是历史名 `/dsh-workspace-api`
> (插件源码里硬编码,没有跟着改)—— 这不是笔误,也别写成 `/dsh-remote-bridge-api`。
设备令牌由 Android Keystore 支持的加密存储保存。App 重启后会恢复连接。
> **令牌不会自动过期。** 服务端的 `devices` 表没有过期列,所以一段配置文本一旦发出就长期有效,
> 只能通过吊销设备使其失效。因此:配置文本只发给自己,一旦怀疑泄露,请立刻在电脑上吊销对应设备
> (`curl -X DELETE http://127.0.0.1:3090/manage/devices/`),而不是指望它“看不懂”。
## 聊天不只是收发消息
- 查看设备有权访问的 DSH 会话和实时运行状态。
- 以流式增量显示助手正文;思考显示在输入区上方那一行,结束后与服务器历史校准。
- 正确排版 Markdown 标题、列表、引用、代码块和表格。
- 工具调用显示成动作短语(「运行了代码,修改了文件」),连续的一串合并成一行、点开看每次调用。
- 原生渲染助手的 `dsh-ui` 交互卡片(卡片 / 表格 / 列表 / 按钮 / 选项 / 进度 / 时间线 …),
卡片里的选项点一下等于把它发送出去。
- 发送普通消息,在运行中追加引导(插话),或取消当前任务。
- 发送图片和文件(输入区 `+` 菜单);运行中的发送行为(排队 / 插话)在 `⋯` 菜单或设置页选。
- 查看 DSH TODO 模块、完成进度和正在处理的事项。
- 输入 `/` 浏览并执行服务器提供的 host 斜杠命令;**不是命令的 `/技能名` 按官方语义原样发给模型**,
由 Host 注入技能正文(早先版本把任何 `/` 开头都当命令,技能因此一个都调不动)。
- **回答 agent 的提问**:它中途问你时卡片直接出现 —— 题干、补充说明、每个选项的说明分开显示,
`multiSelect` 决定单选还是多选,没有选项时给自由文本输入。答完卡片转「已回答」。
电脑端官方界面同时也能答,**先答者胜**。需要电脑端插件 `dsh-remote-bridge ≥ 2.2.2`。
- 在 `⋯` 菜单切换「只读」「工作区写入」「完整访问」。完整访问会再次提示风险。
- 查看待审批工具调用的脱敏信息,选择「允许一次」或「拒绝」。
- **展开工具调用看细节**:完整输入、输出、成败与耗时。手机上开不了终端,这是事后排查的唯一手段。
- **预览图片与 PDF**(文件页与会话附件都支持):双指缩放、拖动,PDF 可逐页翻。
- 在 `⋯` 菜单切换模型和思考强度。空白会话还可以在首条消息前选择 Agent。
- 输入框高度随内容自适应(44–120dp,一行到六行左右)。
- 双击复制整条消息,长按选中片段,点开链接。
- agent 交付的文件(`present` 工具)列在消息流末尾,点开即看;标题栏的文件夹按钮列出同一份清单。
## 工作区与会话
新建会话时,可以选择 DSH WebUI 已登记的工作区,也可以从设备获准访问的根目录中挑选一个现有目录,将它登记为新的 DSH 工作区。
工作区管理支持:
- 查看设备可访问的 DSH 工作区。
- 重命名工作区。
- 移除工作区登记。
移除登记不会删除服务器目录、文件或会话日志。
每个会话的菜单支持:
- **重命名**:修改 DSH WebUI 中显示的标题。
- **分叉**:继承已有聊天历史、工作目录和模型配置,进入新的子会话。
- **归档**:从默认列表隐藏会话,同时保留服务器日志。
App 只显示工作目录位于设备授权根内的工作区和会话,接口不会向手机暴露服务器绝对路径。
## 文件管理与编辑
- 懒加载浏览授权根和子目录。
- 新建文件或文件夹,上传、下载和替换文件。
- 重命名、移动,以及将内容移入插件回收站。
- 查看回收站并恢复项目,发生路径冲突时不会覆盖现有文件。
- 使用带同步行号的 UTF-8 文本编辑器。
- 在 Markdown 源码与渲染预览之间切换。
- 使用按钮或双指手势将源码和预览缩放到 `75%-250%`。
- 使用 ETag 检测外部修改,拒绝静默覆盖较新的文件。
- 对二进制文件只提供元数据、下载和替换。
App 不能添加服务器授权根,也不能永久清空插件回收站。这些高权限操作只在服务器本机 DSH WebUI 中提供。
## 模型供应商
拥有 `settings.read` 权限时,App 可以查看 DSH 的有效供应商配置、模型列表以及凭据是否已配置。服务器不会回传 API 密钥正文。
拥有 `settings.write` 权限时,还可以:
- 编辑供应商名称、Base URL、协议、模型和思考设置。
- 写入或清除供应商凭据。
- 从兼容服务发现模型。
- 创建 DSH 支持的完全自定义供应商和模型路由。
## 权限如何生效
App 会根据服务器授予的 scope 自动显示或禁用功能:
| Scope | App 中允许的操作 |
| --- | --- |
| `chat.read` | 查看工作区、会话、消息、TODO、命令和审批 |
| `chat.write` | 管理工作区与会话、发消息、切换配置、执行命令和处理审批 |
| `files.read` | 浏览、预览、下载和查看回收站 |
| `files.write` | 新建、编辑、上传、移动和重命名 |
| `files.delete` | 将文件或目录移入插件回收站 |
| `settings.read` | 查看供应商和模型配置 |
| `settings.write` | 修改或创建供应商,写入凭据 |
每台设备还需要单独的根目录授权。服务器管理员可以在 `dsh-remote-bridge`(旧名 `dsh-workspace`)
的“设备”页随时修改权限或撤销设备。
## HTTP 安全提示
App 支持 `http://` 和 `https://`,不会强制 HTTPS。HTTP 适合可信局域网、VPN 或临时测试,但会明文传输设备令牌、聊天内容和文件内容。
跨公网或不可信网络使用时,建议在插件前配置 Caddy/Nginx HTTPS,或通过 Tailscale/WireGuard、其他 VPN 或可信隧道连接。不要把配对码、设备令牌或 APK 签名材料分享给其他人。
## 隐私
- App 不包含广告或第三方分析 SDK。
- 设备令牌保存在 Android Keystore 支持的加密存储中。
- API 密钥只会写入 DSH 服务器凭据库,不会由 App 读取回显。
- 聊天和文件数据只发送到你配置的 DSH 地址。
## 常见问题
**新建会话时没有可选工作区**
切换到“新建工作区”,从授权根中选择目录;也可以先在 DSH WebUI 中登记工作区。
**某些按钮不可用**
当前设备缺少对应 scope。请在插件的“设备”页调整权限。
**无法连接服务器**
确认远程访问已经启用,服务器防火墙和云安全组允许该端口,手机填写的地址也不是服务器自己的 `127.0.0.1`。
**会话长时间没有继续**
查看聊天页是否出现审批面板。需要授权的操作不会在后台自动批准。
**保存文件时提示冲突**
服务器文件已经被其他程序修改。重新载入并合并内容后再保存。
## 已知限制
**尚未做一次成体系的真机 UI 验收。** 单元测试与构建产物层面已确认,但下面这些还没有逐项在真机上验收:
- 长按选择与双击复制的边界 —— 双击与滚动手势的冲突(滚动中误触)需要真机才能判断;
- `dsh-ui` 卡片的排版观感与按钮点击热区;
- HTML 结构化预览的排版观感(样式被整体丢弃,只保留结构);
- 输入区 `⋯` 菜单在窄屏与各种输入法下弹出的位置与可达性。
需要说明的是:2.0 之后的多轮修复**是由真机反馈与截图驱动的**(逐版记录见 `app/build.gradle.kts`
的版本注释),但那是「发现问题就修」,不等于一次系统性的真机验收。
接口层(配对、连接、会话、文件、插件清单、会话交付物)在开发过程中对着本机 DSH WebUI 做过端到端实测。
## 从源码构建
需要 JDK 17 和 Android SDK 36:
```sh
./gradlew testDebugUnitTest lintDebug assembleRelease
```
Release 构建必须提供独立签名配置:
```properties
storeFile=/absolute/path/to/release-signing.p12
storePassword=...
keyAlias=...
keyPassword=...
```
通过 `-PdshSigningProperties=/path/to/signing.properties` 指定配置。签名文件和密码不应提交到仓库。
## 项目信息
- 当前版本:`v2.2.32`(`versionCode` 10057)
- 包名:`io.github.hakunm.deepseekharness`
- 单元测试:25 个测试类 / 271 用例(最近一次运行 0 失败)。数量会随代码变化,要最新值就跑一次
`scripts/build.sh :app:testDebugUnitTest`,或数一数 `app/src/test/` 里的类与 `@Test`
- 当前 APK:`DeepSeek-Harness-companion-v2.2.32.apk`,2,879,742 字节,
SHA-256 `596c16210d9dfc89289ebeafde0fcbcc9d748ed1136a2834e29b33f9ee0ba3ad`(`artifacts/SHA256SUMS.txt` 同步)
- 签名:`CN=DSH Pocket Client`(正式证书;v2.2.25 起使用,v2.2.31 → v2.2.32 可直接覆盖升级)
- 作者:上游 [Hakunm](https://github.com/Hakunm),本 fork 维护 [westanke](https://github.com/westanke)
- 许可证:[GNU Affero General Public License v3.0](./LICENSE)
- 服务端插件:dsh-remote-bridge([GitHub](https://github.com/westanke/dsh-remote-bridge) · [Gitee](https://gitee.com/westanke/dsh-remote-bridge))**≥ 2.3.1**(「加载更早消息」需要 2.3.1+)
(旧名 [`dsh-workspace`](https://github.com/Hakunm/dsh-workspace))