把自托管网页装进手机桌面,像原生应用一样全屏运行
30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/21 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Lian-peiji/dsh-android-shell该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:需留意
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
把自托管 Web 应用装进手机桌面的零依赖安卓 WebView 外壳:边到边 / 系统栏常驻 / 键盘让位 / 安全区去重(零 Gradle 构建)
README
由 DeepSeek 最新模型翻译生成DSH Android Shell
把自托管的 Web 应用,变成手机上「像原生一样」的 App。
一个单 Activity 的 WebView 外壳:零第三方依赖、零 Gradle、约 540 行 Java(含完整中文注释)。
专门解决浏览器「加到桌面」做不到的那部分 —— 边到边、系统栏常驻、键盘让位、拍平页面与原生窗口之间重复的安全区留白。
典型用法:家里 PC 上跑着 DSH(DeepSeek Harness),用 dsh-pocket 的 Cloudflare 命名隧道把它发布成一个固定 https 域名,再用这个外壳把它装进手机桌面。任何自托管 Web 应用同样适用。
⚠️ 前置依赖(先读这段)
本壳不提供手机访问入口,也不改变页面在窄屏下的布局——这两件事由服务端的 dsh-pocket 插件负责(反向代理 + 密码校验 + 移动端样式注入)。
换句话说:这是一个「窗口」,不是一个「应用」。使用前你的服务端需要满足:
1. DSH 所在 profile 已安装并启用 dsh-pocket,已开好手机可访问的入口(局域网地址或隧道域名);
2. 把该入口地址填进 START_URL,再按「快速开始」重新编译——仓库里的 YOUR-DOMAIN 是占位符,直接编译出来的包打不开任何真实页面。
展开完整文档(README 剩余 1 节)
DSH Android Shell
把自托管的 Web 应用,变成手机上「像原生一样」的 App。
一个单 Activity 的 WebView 外壳:零第三方依赖、零 Gradle、约 540 行 Java(含完整中文注释)。
专门解决浏览器「加到桌面」做不到的那部分 —— 边到边、系统栏常驻、键盘让位、拍平页面与原生窗口之间重复的安全区留白。
典型用法:家里 PC 上跑着 DSH(DeepSeek Harness),用 dsh-pocket 的 Cloudflare 命名隧道把它发布成一个固定 https 域名,再用这个外壳把它装进手机桌面。任何自托管 Web 应用同样适用。
⚠️ 前置依赖(先读这段)
本壳不提供手机访问入口,也不改变页面在窄屏下的布局——这两件事由服务端的 dsh-pocket 插件负责(反向代理 + 密码校验 + 移动端样式注入)。
换句话说:这是一个「窗口」,不是一个「应用」。使用前你的服务端需要满足:
1. DSH 所在 profile 已安装并启用 dsh-pocket,已开好手机可访问的入口(局域网地址或隧道域名);
2. 把该入口地址填进 START_URL,再按「快速开始」重新编译——仓库里的 YOUR-DOMAIN 是占位符,直接编译出来的包打不开任何真实页面。
只装这个 App、不装 dsh-pocket:能启动,但连不上(或只有桌面版排版)。两者是互补关系,不是二选一。
📖 想搞懂每一层在干什么:见 docs/术语与原理.md —— 网络 / 视口 / PWA / 安卓原生 / 构建链 / 插件机制,全篇「大白话 + 专业名词 + 在本项目里的位置」。
背景:为什么不能直接用浏览器的「安装应用」
在没有 Google Play 服务的安卓机上,浏览器的「安装到桌面」是骗人的:
- 把网页「安装」成真 App(WebAPK)需要经 Google 服务器铸造。缺 GMS 时浏览器只是静默降级成一个快捷方式,但弹窗照样写着「安装应用」。
- 怎么识别:长按桌面图标只有「移除」、没有「应用信息」;系统应用列表里也找不到它。
- 后果:窗口由浏览器自己的 Activity 决定,manifest、display: standalone/fullscreen 一律无效,于是出现这一串经典症状:
- 顶部一块死黑(状态栏被藏起来了);
- 底部一条白线/黑线(根本没做边到边);
- 呼出系统导航栏时整页抖(窗口高度变化 → 页面重排);
- env(safe-area-inset-) 全是 0。
只有原生窗口能拿到系统的 WindowInsets,也只有它能决定「系统栏常驻」。 所以正确做法是自己写一个极薄的壳 —— 就是这个项目。
它做了什么
按重要性排,这也是整个项目真正的价值所在:
1)边到边 + 系统栏常驻,把 insets 换算成「容器 padding」 targetSdk 35 + setDecorFitsSystemWindows(false),再把 systemBars() | displayCutout() 的 insets 换算成外层容器的 padding。
三条通道兜底:① 建视图时先读系统资源 status_bar_height 垫底;② 容器的 OnApplyWindowInsetsListener(正常路径);③ 布局完成后 root.post() 里再读一次 getRootWindowInsets()(有的机型不派发监听器)。
顶部只在这三条都拿不到正值时才用兜底值,绝不把 padding 归零。窗口尺寸恒定 → 呼出导航栏不会抖。
2)底部不留白:容器的底部 padding 恒为 0 让 WebView 一直铺到物理底边,底部那点抬升交给网页自己做。
如果由容器留底边距,屏幕最底下会露出窗口背景色 —— 一条黑边,输入框看起来「和屏幕底部不贴合」。
3)键盘让位:自己按 IME 高度给容器 padding
setDecorFitsSystemWindows(false) 之后系统不再替窗口让位,必须自己处理:键盘弹出时取
insets.getInsets(WindowInsets.Type.ime()).bottom 作为容器底部 padding,平时为 0。
否则键盘只是「盖」在页面上,输入框被死死埋在键盘底下 —— 打了什么字都看不见。
(代码里还判断了引擎是否已经自己缩过视口,避免缩两次。)
4)往页面注入三条 CSS(onPageFinished 时注入一个 ,幂等)
/ a. 让输入框躲开手势条:用 max() 取大,不要相加 —— 页面自己也抬过一次 /
[data-composer-seat]{ padding-bottom: max(env(safe-area-inset-bottom, 0px), <系统栏高度>px) !important }
/ b. 键盘态把根容器钉平:压过某些移动端皮肤用 visualViewport 推算的 fixed + top:var(--vv-top) / html body [id=root][id=root][id=root][id=root]{ position: relative !important; top: 0 !important; height: 100% !important; max-height: 100% !important } / c. 掐掉文档级滚动与右侧滚动条(SPA 自己管内部滚动) */ html, body { overflow: hidden !important; overscroll-behavior: none !important; height: 100% !important }
[data-conversation-scroll] { scrollbar-width: none !important }
::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important }
权重技巧:两条规则都带 !important 时,按选择器 specificity 决胜、与注入先后无关。
这里用重复属性名把权重顶高([id=root][id=root] 是 (0,4,0)),因此不需要改第三方皮肤或网页的源码,对方升级也不会把补丁覆盖掉。
5)修「浏览器能开、App 里 401」服务端每次重启端口都会变,之前签发的 cookie 会烂在 WebView 里;而反代只在请求还没带这个 cookie 时才补发 launch token → 上游对不上就 401。
做法:启动时先 CookieManager.removeAllCookies() + flush() 再 loadUrl(...);onPause() 里 flush() 让登录 cookie 落盘。
6)开屏遮白首屏 + 密码握手要几百毫秒到几秒。先盖一层浅色开屏(插画 + 标题),onPageFinished 后淡出;下限 700 ms(防一闪而过)、上限 12 s(防卡在插画上退不出去)。
7)杂项:附件选择 onShowFileChooser、返回键 goBack()、onPause flush cookie、系统栏图标走浅色。
快速开始
0. 前置
- JDK 17(javac / keytool)
- Android SDK:build-tools/35.0.0(aapt2、d8、zipalign、apksigner)+ platforms/android-35/android.jar
- python3(只用来把 classes.dex 追加进 APK,十几行脚本)
1. 准备你的入口地址如果你也是用 DSH + dsh-pocket:打开 设置 → 手机访问,选命名隧道,填自己的域名,设一个自定义访问密码(这样它不会每次开启都轮换)。记下 域名 和 密码。
链路:手机 → Cloudflare 边缘 → cloudflared(PC 主动拨出)→ 127.0.0.1:3081(本地反代验密码)→ 应用本体全程走出站连接,不需要公网 IP、不需要开放任何入站端口。
2. 改 3 处
| 位置 | 改什么 |
| proj/src/com/example/dshshell/MainActivity.java | package 换成你自己的包名;START_URL 换成 https://你的域名/?token=你的密码 |
| proj/AndroidManifest.xml | package 与上面一致;android:label(桌面显示名);versionCode / versionName |
| proj/res/ | 替换占位图标:mipmap-xxxhdpi/ic_launcher.png(192×192)、mipmap-xxxhdpi/ic_launcher_foreground.png(432×432)、drawable/splash_art.png(开屏插画);底色改 values/colors.xml |
3. 构建 bash bash build.sh # 产物:./DSH.apk
build.sh 顶部的 JDK / SDK / KS_PASS 支持用环境变量覆盖:JDK_HOME、ANDROID_SDK_ROOT、DSH_KS_PASS。
4. 安装与更新把 APK 传到手机点装(HyperOS 要给安装来源开「安装未知应用」)。
签名一致才能覆盖安装 —— 首次构建会生成 dsh.keystore,请务必保管好;丢了就只能换签名重打包,而手机上必须先卸载(本地数据清空)。
目录结构
build.sh 零 Gradle 构建脚本 proj/ ├── AndroidManifest.xml 权限、Activity、主题、configChanges ├── src/com/example/dshshell/MainActivity.java 全部逻辑(约 540 行,中文注释,建议通读) └── res/ ├── values/styles.xml 状态栏/导航栏/窗口底色 ├── values/colors.xml 自适应图标底色 ├── mipmap-anydpi-v26/ic_launcher.xml 自适应图标描述 ├── mipmap-xxxhdpi/ 占位图标(换成你自己的) └── drawable/splash_art.png 开屏插画
构建原理
不使用 Gradle / Android Studio,一条流水线跑完:
aapt2 compile 资源 → aapt2 link 打基座 APK(--java 生成 R.java) → javac 编 MainActivity.java → d8 生成 classes.dex → python zipfile 把 classes.dex 追加进 APK → zipalign -f -p 4 → keytool 生成密钥(首次)+ apksigner 签名
踩过的坑(都写在脚本注释里)
- javac 必须带 -encoding UTF-8:中文 Windows 默认 GBK,源码里有中文注释就直接报错。
- 不要写 R.drawable.xxx:本项目只编译 MainActivity.java,R.java 不参与编译 —— 一律改用
getResources().getIdentifier("名字", "drawable"|"mipmap"|"dimen", getPackageName())。
- d8 / apksigner 的 .bat 靠 JAVA_HOME 找 java,脚本里要先导出。
- aapt2 link 的 --version-code/--version-name 会被 manifest 里的同名属性盖掉,改版本要改 manifest。
- 只给一张老式方形 PNG 当图标,系统会套上一个白方块 —— 要走 mipmap-anydpi-v26/ 这套自适应图标。
兼容性
| 项 | 值 |
| 最低版本 | Android 8.0(API 26) |
| 目标版本 | Android 15(API 35,强制边到边) |
| 编译版本 | API 35 / Java 8 字节码 |
| 权限 | 仅 INTERNET、ACCESS_NETWORK_STATE |
| 第三方依赖 | 无 |
| 实测环境 | HyperOS(无 Google Play 服务的国产安卓机) |
已知限制
- 页面里的选择器([data-composer-seat]、[data-mobile-nav="frame"]、[data-conversation-scroll] 等)来自 DSH Web 本身与常见的移动端皮肤插件。换皮肤或应用升级后如果又冒出顶部空带 / 底部黑边,按第 1~4 条的原则重新对齐即可。
- 入口地址与密码写在源码里(换取「打开就来、不用每次输密码」)。别把你改好的源码公开出去 —— 要公开就保留占位符。
- 只处理窗口与视口,不碰应用本身的任何功能。
一句话总结
浏览器(或 WebView)负责画页面,原生外壳负责窗口。
视口和安全区只能由一方留白 —— 留两次,就一定会多出一条黑边或空带。
License
MIT预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
Lian-peiji/dsh-android-shell
把自托管网页装进手机桌面,像原生应用一样全屏运行
相关插件 · UI / 主题
破甲增强版JeffKeeping/dsh-purge-plus
dsh-purge 的非官方分支fork:适配 DeepSeek Harness 0.2.1-rc.2、破甲内核…
★ 387已汉化DeepSeek 客户端
71.9
无代码业务系统平台nocobase/nocobase
NocoBase 是什么
★ 24,804已汉化DSH
71.8
1Panel 服务器面板1Panel-dev/1Panel
全球 250 万+ 自托管用户共同喜爱。
★ 37,362已汉化DSH
71.4
终端编码代理esengine/DeepSeek-Reasonix
· 指南 · ACP · 扩展开发 · 规格 · 官方网站 · Discord · QQ
★ 36,076已汉化DSH
71.2
工作状态行动态ccch1mneyyy/working-activity
作者:chimney@ccch1mneyyy。社区项目,非 DeepSeek 官方项目。
★ 664已汉化DSH
71.2
多模型配置管家farion1231/cc-switch
跨平台桌面一体化助手,支持 Claude Code、Codex、OpenCode、DeepSeek Harness 等工具,唯一官网 ccswitch.io
★ 151,814已汉化DSH
71.1
这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。