---
name: exe-service-app-shell
description: >-
  通用「本机 exe 服务端 + Web/安卓客户端壳」开发模式。桌面托盘 exe（Python + PyInstaller）
  守护/托管本机任意 Web 应用（相册、游戏、工具、纯静态或嵌套 HTML，或任意外部程序），
  经 frp 反向隧道暴露成 https://<prefix><code>.<domain>/「地址即钥匙」；浏览器直连，
  安卓用通用 WebView 多页签壳；配内容哈希自动升版 + 打包/部署/push 一键发布流水线。
  Use when 用户要按 opencode-app / web-cmd 的模式起一个新项目，或提到「exe 服务端」
  「app 客户端」「托盘程序 / tray」「frp 隧道」「把本机服务/网页暴露到公网」「连接码」
  「WebView 壳」「地址即钥匙」「打包 exe 发布」「一键发布流水线」「把网页/相册/小游戏做成本机应用」
  等关键词时触发。
  不用于普通桌面/安卓 App、不涉及本地服务或网页 + 公网隧道 + 发布流水线的任务。
triggers:
  - exe 服务端
  - app 客户端
  - 托盘程序
  - tray app
  - frp 隧道
  - 本机服务暴露公网
  - 把网页做成应用
  - WebView 壳
  - 连接码
  - 地址即钥匙
  - opencode-app 模式
  - web-cmd 模式
  - 打包 exe
  - 一键发布
  - 发布流水线
  - 远程构建 APK
  - 无 Android SDK 打包
  - 构建主机
  - json-db
  - online-html
  - 无 exe 托管
  - 联网数据存储
---

# exe-service-app-shell —— 本机 exe 服务端 + 客户端壳 开发模式

把一台电脑上的**本地 Web 应用**包装成「双击即用的托盘 exe」，经 **frp 反向隧道**暴露到公网，
浏览器/安卓 App 输入**连接码**即可远程访问。核心取舍：**地址即钥匙，无密码**。

**内容无关**：服务端可以是任何东西——相册、Web 小游戏、工具面板、纯静态/嵌套 HTML，
或任意已存在的本地 HTTP 程序。客户端只是通用外壳，不绑定具体业务。

原型参考实现：`C:\Users\15680\projects\opencode-app`（守护 `opencode serve` + frp + 安卓 WebView 壳）。

## 什么时候用这套模式

- 要把**跑在某台私人电脑上**的网页/服务（本地 AI、相册、Web 游戏、内网工具、静态站点）
  变成「手机/浏览器远程可访问」。
- 不想搞服务器、账号体系、认证；能接受「地址泄露=控制权泄露」。
- 需要 Windows 免安装托盘程序 + 手机端 App，且要有一键发布流水线。

不适合：多租户/需鉴权/需长期在线的生产服务。纯隧道选型（SaaS/frp/自研）见 `intranet-penetration` skill。

## 服务端两种模式（核心开关 `{{SERVE_MODE}}`）

| 模式 | `SERVE_MODE` | 谁在服务 HTTP | 适用 |
|------|--------------|---------------|------|
| 外部程序 | `external`（默认） | 任意外部可执行（`{{SERVICE_NAME}}` + `{{SERVICE_ARGS}}`） | 已有服务：本地 AI、CLI 的 Web UI、`node`/`python`/`go` 程序 |
| 内置站点 | `builtin` | 托盘 exe 自己（stdlib `http.server` 服务打包的 `webapp/`） | 相册、Web 小游戏、静态/嵌套 HTML、纯前端应用 |

内置模式无需外部程序：把网页放进 `webapp/`，打包时随 exe 一起带上（`build.ps1` 已加
`--add-data "webapp;webapp"`），`app/webserver.py` 提供与外部模式一致的接口。

## 模式四件套

| 层 | 技术 | 职责 |
|----|------|------|
| 托盘 exe | Python + pystray + PyInstaller | 托管内置站点 或 定位/启动/守护外部程序；开隧道；托盘菜单；单实例接管 |
| 隧道 | frpc | 本地端口 → `https://<prefix><code>.<domain>/`；原生支持 WS/SSE |
| 客户端 | 浏览器直连 / 安卓 WebView 壳 | 输入连接码；安卓多页签各带独立会话，可指向**任意**内容 |
| 流水线 | Python + PowerShell + ssh/scp | 内容哈希升版 → 打包 → 部署 → 产品页同步 → git push |

## 快速开始

### 方式 A：用脚手架生成（推荐）

```bash
# 内置站点模式：把相册/游戏/HTML 放进 webapp/ 就能跑
python scripts/scaffold.py \
  --app-name myalbum \
  --serve-mode builtin \
  --domain-suffix frp.example.com \
  --android-pkg com.example.myalbum \
  --deploy-host root@1.2.3.4 \
  --out C:\Users\me\projects\myalbum

# 外部程序模式：守护一个已存在的本机服务
python scripts/scaffold.py \
  --app-name mytool-app \
  --serve-mode external \
  --service-exe mytool \
  --service-args "serve --port {port} --hostname {host}" \
  --health-path /global/health \
  --default-port 4096 \
  --domain-suffix frp.example.com \
  --android-pkg com.example.mytool \
  --deploy-host root@1.2.3.4 \
  --out C:\Users\me\projects\mytool-app
```

生成后：`pip install -r requirements.txt` → 放 `payload/frpc.exe` 与 `assets/icon.ico`
→ `python app/main.py` 本地跑通 → `powershell -File build.ps1` 打包。

也可交互式：`python scripts/scaffold.py --interactive`，或先导出配置模板
`python scripts/scaffold.py --write-config mytool.json` 再 `--config mytool.json`。

### 方式 B：手动复制

1. 复制 `templates/project/` 到你项目目录。
2. 全局替换下表 `{{TOKEN}}`（共 17 个可配置项，另加自动派生的 `{{APP_ENV}}`；不要漏 `.ps1`；含中文的 `.ps1` 存 UTF-8 **BOM**）。
3. 补 `assets/icon.ico`、`payload/frpc.exe`、`%APPDATA%\<app>\frp.json`（frps 地址/token）。

## 远程构建 APK（换机 / 本地无安卓工具链）

本地不想装 SDK/JDK/Gradle 时，把源码交给一台已装好工具链的**构建主机**出 APK：

- 客户端 `tools\remote_build.ps1`（`{{DEPLOY_HOST}}` 即构建主机）：
  - `powershell -File tools\remote_build.ps1` → 生成缺失图标 → 打包源码 → scp → 触发构建 → 打印 `APK_URL`/SHA
  - `-Download` 顺带取回 APK 到 `dist\`（源码一律 tar 上传，git 模式已移除）
  - `-Http <url>` 改走**无认证 HTTP API**（`https://<站点>/k/<key>/build`）：全程不碰 ssh/scp，
    开发机**无需持有构建主机密钥**，随机长路径即全部凭证（地址即钥匙）。契约见 `references/build-api.md`
- 主机侧 `templates/build-host/`：
  - `setup_build_host.sh` 幂等安装 JDK17 + Gradle 8.13 + Android SDK + swap + 构建目录 + **HTTP 触发器** + cron 清理
  - `env.sh` 工具链路径；`build_agent.sh` 解包 →（缺失时）生成图标、生成签名密钥 → `gradle assembleRelease`
    → 只留 APK 到站点目录并输出 `APK_FILE/APK_SHA256/APK_URL`，**结束即删源码**
  - 每个 app 首建自动生成并复用签名密钥 `/opt/appbuild/keys/<app>.jks`，保证可覆盖升级
  - `trigger.py` + `appbuild-trigger.service` 起 `127.0.0.1:8791` 触发器（**只听 loopback**）；
    `patch_nginx_buildapi.sh <站点conf> <key>` 把 `/k/<key>/build` 精确路径打进 nginx，钥匙即对外凭证

要点：构建约需 1G 堆 + 单 worker（脚本内置低内存 `gradle.properties` 覆盖与 `flock` 串行）；
APK 落到 `{{REMOTE_BASE}}/download/builds/`，需 nginx 已服务该路径（本模式复用产品页域名）。

## 外部数据与托管（可选）

两个已部署的公共后端，与隧道方案互补（都开 CORS）：

| 服务 | 地址 | 场景 |
|------|------|------|
| json-db | `https://jsondb.fangna.tech` | **联网数据**：`GET/POST/DELETE /<scope>` 读写 JSON（不存在返回 `{}`），前端直连 |
| online-html | `https://html.fangna.tech` | **无本地 exe**：把静态页存成命名空间 URL，免 exe + 隧道 |

- 前端直连数据：`webapp/jdb.js` → `jdb.get/set/del(scope)`；命令行 `tools/json_db.ps1 get|set|del <scope>`。
- 无 exe 托管静态页：`tools/publish_html.ps1 <scope> webapp\index.html` → 打印 `https://html.fangna.tech/<scope>`。
- 取舍：二者**无鉴权**，scope 名即钥匙，**别放敏感数据**；有隐私/离线需求仍用本机 exe + frp。
- 详见 `references/hosting-options.md`（API、组合用法、注意事项）。

## 产品页设计（务必遵循 cluade-frontend-design）

`web/index.html`（产品页 / 下载页）是这个项目的门面。脚手架生成的样式只是**能跑的占位实现，
不是最终设计**——发布前必须按 `cluade-frontend-design` skill 的流程做成有主张、非套路的界面：

- **先定方向**：用几句话锁定 Tone（极简 / 编辑杂志 / 复古未来 / 工业实用 / 极繁…），
  想清它解决什么问题、给谁看、那个「一眼记住」的记忆点是什么，再动笔。
- **排版**：显示字体要有性格 + 正文克制，成对使用；**避开 Inter / Roboto / Arial / 系统默认**；
  中文配思源宋/黑，等宽配 IBM Plex Mono 之类；字号/行高/字距刻意经营。
- **配色**：一套 CSS 变量贯穿，主色 + 尖锐点缀色，别用「紫渐变白底」等陈词滥调；
  `color-mix()` 前留 `rgba()` 兜底；每个项目**换一套**，不要千篇一律。
- **动效**：以 CSS 为主，一次编排好的错峰入场胜过满地微交互；尊重 `prefers-reduced-motion`。
- **空间与质感**：敢不对称/重叠/破格；用网格纹理 / 噪点 / 分层透明 / 投影做氛围，别纯色平铺。
- **没有一个产品的页面应该一样**：复杂度匹配愿景，极繁写足效果，极简靠留白与精度取胜。

**硬约束（改样式不得破坏，否则发布流水线会坏）**：必须保留 JS 里的
`META{gui,android}` / `CHANGELOG` 数据契约、`tools/update_page.py` 依赖的字段与 `label` 格式、
页面脚本依赖的元素 id，且不留 `{{...}}` 残留。完整清单见
`references/product-page-design.md`。

## 配套资源：设计与音频（fangna.tech 公共产品）

项目里需要**配图 / 生图 / 视觉素材**时用 **Qwen Image**；需要**人声 / TTS / 背景音乐**时用
**Phonate**（都经隧道直连、开放 API、无鉴权）：

| 需求 | 产品 | 地址 |
|------|------|------|
| 设计 / 生图 / 图标 / 背景 / 视觉素材 | Qwen Image | `http://qwen-image.frp.fangna.tech/` |
| 语音合成 / 配音 / 背景音乐 | Phonate | `http://phonate.fangna.tech/` |

- 生成结果**下载到本地**（产品页 → `web/`，内置站点 → `webapp/`）用相对路径引用，
  运行时不要依赖外部服务；不可用时要有本地兜底（占位图 / 不加载音频）。
- Phonate 开放 TTS：`POST /api/tts`，body 形如
  `{"text":"你好","gender":"female","age":24,"pitch":0.5,"emotion":0.8}`，返回可播放的音频 URL；
  默认锁 seed 可复现。用法与取舍详见 `references/companion-services.md`。
- 数据存储（json-db）与无 exe 静态托管（online-html）仍见上一节，别与素材服务混用。

## 占位符表（脚手架与模板共用）

| Token | 示例 | 含义 / 落点 |
|-------|------|-------------|
| `{{APP_NAME}}` | `myalbum` | 标识符：exe/zip 名、`%APPDATA%\<app>` 目录、注册表项、日志名 |
| `{{APP_LABEL}}` | `myalbum` | 展示名：托盘标题、产品页 |
| `{{APP_ENV}}` | `MYALBUM_` | 环境变量前缀（**由 app-name 自动派生**，含结尾 `_`，模板里拼 `{{APP_ENV}}PORT` 等） |
| `{{SERVE_MODE}}` | `external` / `builtin` | 服务模式：守护外部程序 / 内置站点 |
| `{{SERVICE_NAME}}` | `mytool` | 被守护的本地服务可执行名（builtin 模式为空） |
| `{{SERVICE_ARGS}}` | `serve --port {port} --hostname {host}` | 外部程序启动参数（`{port}`/`{host}` 运行时替换） |
| `{{HEALTH_PATH}}` | 空 或 `/global/health` | 健康检查路径；**空=只做 TCP 连通性检查** |
| `{{DEFAULT_PORT}}` | `4096` | 首选端口（占用则向后找空闲） |
| `{{CODE_PREFIX}}` | `myalbum-` | 隧道子域前缀（含结尾 `-`） |
| `{{CODE_LEN}}` | `16` | 随机连接码长度 |
| `{{DOMAIN_SUFFIX}}` | `frp.example.com` | 隧道域名后缀 |
| `{{FRP_SERVER_PORT}}` | `7000` | frps 端口 |
| `{{ANDROID_PKG}}` | `com.example.myalbum` | 安卓包名 / applicationId / java package |
| `{{PRODUCT_DOMAIN}}` | `webcmd.example.com` | 产品页与下载域名 |
| `{{DEPLOY_HOST}}` | `root@1.2.3.4` | 部署目标（免密 SSH） |
| `{{REMOTE_BASE}}` | `/var/www/webcmd` | 远端站点根 |
| `{{PROJECT_SLUG}}` | `myalbum` | 产品页 URL 路径 `.../<slug>/` |
| `{{TRAY_GLYPH}}` | `>_` | 托盘兜底图标上绘制的文字 |

## 目录约定

```
app/            托盘程序（main.py 打包入口）+ 各单一职责模块
  service_server.py  外部程序守护（端口/探活/重连）
  webserver.py       内置站点服务（stdlib http.server 服务 webapp/）
tools/          发布流水线（升版/打包/部署/推送/安卓/产品页）
  gen_icons.py      缺图标时生成占位 ico/png/mipmap
  check_env.ps1     环境自检（桌面/安卓/发布依赖，-Fix 顺带补图标）
  remote_build.ps1  远程构建（ssh 上传 或 `-Http` 走无认证 API → 主机出 APK → 取回）
  json_db.ps1       json-db 客户端（命令行读写 https://jsondb.fangna.tech）
  publish_html.ps1  发布静态页到 online-html（https://html.fangna.tech，免 exe/隧道）
webapp/         内置站点模式的前端内容（相册/游戏/嵌套 HTML，随 exe 打包）
  jdb.js            json-db 浏览器客户端（<script src="jdb.js"> 即用）
android/        安卓通用 WebView 壳（gradle 工程）
web/            产品页 index.html（须按 cluade-frontend-design 重设计）+ 更新清单 <slug>/android.json
payload/        frpc.exe（不入库）
assets/         icon.ico / icon.png
build.ps1       PyInstaller 打包
.version-lock.json  内容哈希 + 当前版本（必须提交）
```

## 关键约定（务必遵守）

- **`.ps1` 含中文必须 UTF-8 BOM**，否则 PowerShell 5.1 按 ANSI 读会乱码/报错。
- **升版与打包分离**：先 `bump_version.py` 再 `pack_release.ps1`；`pack_release.ps1` 不升版。
- **读用户 JSON/收据用 `utf-8-sig`**，字节写回版本文件，避免 CRLF/BOM 造成「内容变了」误升版。
- **子域前缀必须同步进 nginx 通配正则**，否则自定义 ID 会 TLS 证书不匹配。
- 打包前**停掉运行中的 exe**（占 `dist\<app>\`）。
- **构建自举**：缺图标时 `build.ps1`/`build_android.ps1` 自动跑 `gen_icons.py`；缺 Gradle 时
  `build_android.ps1` 按 wrapper 的 `distributionUrl` 自动下载。先跑 `tools\check_env.ps1` 自检。

## 深入阅读

- `references/architecture.md` —— 各模块职责、外部/内置两种服务模式、进程守护/重连、单实例、隧道与 Defender。
- `references/release-pipeline.md` —— 升版算法、打包/部署/push 细节、产品页与安卓更新清单。
- `references/pitfalls.md` —— 踩坑清单（BOM、CRLF、PID 复用、SSE 缓冲、证书）。
- `references/scaffolding.md` —— 脚手架用法与从零手搭步骤。
- `references/hosting-options.md` —— json-db（联网数据）与 online-html（无 exe 静态托管）的 API 与组合用法。
- `references/product-page-design.md` —— 产品页设计流程（cluade-frontend-design）与不可破坏的数据契约。
- `references/companion-services.md` —— Qwen Image（生图）与 Phonate（TTS/音频）的用法与取舍。
- `references/build-api.md` —— 无认证 HTTP 构建 API：契约、主机侧部署、钥匙与安全取舍、实测记录。

## 步骤清单（新项目上线）

1. `scaffold.py` 生成骨架（选 `--serve-mode external/builtin`）→ 全局搜 `{{` 确认无残留。
2. `pip install -r requirements.txt` → `powershell -File tools\check_env.ps1 -Fix` 自检并补占位图标。
3. builtin：把前端放进 `webapp/`；external：确认目标程序可被定位/启动。
4. 按 `cluade-frontend-design` 重设计 `web/index.html` 产品页；需素材时用 Qwen Image（生图）
   / Phonate（语音/背景音乐），生成后落到本地再引用（见 `companion-services.md`）。
5. 放 `payload/frpc.exe`；写 `%APPDATA%\<app>\frp.json`（图标缺失会自动生成占位图）。
6. `python app/main.py` 本地验证：服务起来、托盘可用、`http://127.0.0.1:<port>` 可访问。
7. 服务端：nginx 通配块加 `{{CODE_PREFIX}}` 正则 + `proxy_buffering off` + WS upgrade。
8. `bump_version.py --target app` → `pack_release.ps1` 出 zip 并部署。
9. 安卓：装 SDK + JDK 17（Gradle 自动下载）→ `release_android.ps1` 出签名 APK 并发布；
   本地无工具链时用 `tools\remote_build.ps1` 走构建主机（见上节）。
10. 用模拟器/浏览器验证公网地址与安卓多页签。
