From a7d3fc7584e8042fdd7fc9d4695d7f1de7227421 Mon Sep 17 00:00:00 2001 From: lazy Date: Sun, 9 Aug 2026 23:44:55 +0800 Subject: [PATCH] Fix mobile AI controls and split model routing --- README.md | 29 ++--- README.zh-CN.md | 23 ++-- apps/mobile-lab-control/AndroidManifest.xml | 20 ++++ apps/mobile-lab-control/README.md | 23 ++-- apps/mobile-lab-control/README.zh-CN.md | 17 ++- .../res/layout/activity_main.xml | 36 +++++++ .../res/layout/widget_model.xml | 2 + .../res/layout/widget_oc_web.xml | 15 +++ .../res/values-zh-rCN/strings.xml | 29 +++-- .../mobile-lab-control/res/values/strings.xml | 29 +++-- apps/mobile-lab-control/res/values/styles.xml | 7 ++ .../res/xml/widget_oc_model_info.xml | 6 ++ .../res/xml/widget_oc_web_info.xml | 6 ++ .../CommandResultReceiver.java | 10 ++ .../mobilelabcontrol/ControlActivity.java | 86 +++++++++++++++ .../mobilelabcontrol/ControlContract.java | 21 ++++ .../mobilelabcontrol/HappyWidgetProvider.java | 29 ----- .../mobilelabcontrol/LlamaWidgetProvider.java | 24 ----- .../mobilelabcontrol/MainActivity.java | 18 ++++ .../mobilelabcontrol/ModelWidgetProvider.java | 28 ----- .../OpenCodeModelWidgetProvider.java | 11 ++ .../OpenCodeWebWidgetProvider.java | 11 ++ .../mobilelabcontrol/TermuxRunner.java | 17 +++ .../mobilelabcontrol/WidgetState.java | 10 +- .../mobilelabcontrol/WidgetUpdater.java | 74 ++++++++++--- config/widget-profiles.example | 4 +- docs/architecture.md | 20 ++-- docs/phone-quickstart.en.md | 25 +++-- docs/phone-quickstart.zh-CN.md | 26 +++-- scripts/verify.sh | 12 +++ skills/deploy-android-ai-stack/SKILL.md | 24 +++-- .../references/control-widgets.md | 31 ++++-- .../references/control-widgets.zh-CN.md | 25 +++-- .../references/provider-profiles.md | 6 +- .../deploy-android-ai-stack/scripts/ai-model | 70 ++++-------- .../scripts/ai-model-profile.sh | 16 +-- .../scripts/install-runtime.sh | 5 +- .../scripts/mobile-lab-widget-action | 95 +++++++++++++--- skills/deploy-android-ai-stack/scripts/oc | 2 +- .../deploy-android-ai-stack/scripts/oc-model | 101 ++++++++++++++++++ .../scripts/opencode-model-profile.sh | 32 ++++++ skills/deploy-android-ai-stack/scripts/stack | 5 +- .../scripts/ai-stack-check | 20 +++- tests/control-bridge.sh | 34 +++++- tests/profile-switch.sh | 54 +++++++--- 45 files changed, 889 insertions(+), 299 deletions(-) create mode 100644 apps/mobile-lab-control/res/layout/widget_oc_web.xml create mode 100644 apps/mobile-lab-control/res/xml/widget_oc_model_info.xml create mode 100644 apps/mobile-lab-control/res/xml/widget_oc_web_info.xml create mode 100644 apps/mobile-lab-control/src/io/github/toolazytoname/mobilelabcontrol/ControlActivity.java create mode 100644 apps/mobile-lab-control/src/io/github/toolazytoname/mobilelabcontrol/OpenCodeModelWidgetProvider.java create mode 100644 apps/mobile-lab-control/src/io/github/toolazytoname/mobilelabcontrol/OpenCodeWebWidgetProvider.java create mode 100644 skills/deploy-android-ai-stack/scripts/oc-model create mode 100644 skills/deploy-android-ai-stack/scripts/opencode-model-profile.sh diff --git a/README.md b/README.md index 98f2a65..e809606 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ or model weights. ```text Happy app -> 127.0.0.1 Happy server -> local Happy daemon -> local Claude Code - | -OpenCode TUI/Web ---------------------+-> provider profile -> model API - (cloud/relay/local) + | + Claude/Happy profile -> model API + +OpenCode TUI/Web -> independent OpenCode profile -> model API (cloud/relay/local) ``` - The control plane is self-contained on the phone. @@ -29,11 +30,12 @@ OpenCode TUI/Web ---------------------+-> provider profile -> model API if you intentionally need one. - Local inference is optional. A small `llama.cpp` server can expose a loopback OpenAI-compatible API for lightweight offline tasks. -- Switching model profiles is explicit, keeps secrets outside committed - configuration, and never changes service lifecycle implicitly. -- The offline `Mobile Lab Control` app exposes three independent native widgets - for Happy, llama.cpp, and model profiles. Termux:Widget remains an opt-in - terminal fallback. +- OpenCode and Claude/Happy have separate selectors and active files. Switching + either route is explicit and never changes the other client or a service + lifecycle. +- The offline `Mobile Lab Control` app exposes five independent native widgets: + Happy, llama.cpp, Claude/Happy models, OpenCode Web, and OpenCode models. + Termux:Widget remains an opt-in terminal fallback. ## Repository layout @@ -82,12 +84,13 @@ fallback have both been exercised. Exact versions and evidence are recorded in | Phone-local Happy | exercised end to end | lifecycle wrapper included | | Provider switching | exercised on the private device config | redacted generic profiles included | | llama.cpp local model | Qwen3 0.6B Q8, API and `local-lite` passed | authenticated loopback lifecycle and bounded checker included | -| Native control widgets | three independent widgets exercised with zero model calls | source, allow-list bridge, bilingual UI, and release APK included | +| Native control widgets | five controls exercised on Android 14; lifecycle and model routes stayed independent | source, allow-list bridge, bilingual UI, and release APK included | -The final phone acceptance also exercised the control split: profile changes -left running services untouched, Happy reported `STALE` when its startup -profile differed, and both services stopped cleanly without changing the -selected profile. +The 2026-08-09 phone acceptance also exercised real inference: OpenCode and +Claude CLI each returned an exact nonce through the OpenAI-labelled relay, and +Happy returned an exact nonce from the mobile UI. The Android 14 widget path was +fixed to use a user-initiated activity, and both local services stopped cleanly +afterward. “Included” does not mean automatically installed. Upstream binaries and model weights remain explicit, pinned operator choices. diff --git a/README.zh-CN.md b/README.zh-CN.md index 897d293..9ecfac6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -17,9 +17,10 @@ GitHub Release 资产发布,不进入 Git 历史。 ```text Happy App -> 127.0.0.1 Happy server -> 本地 Happy daemon -> 本地 Claude Code - | -OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API - 云 / 中转 / 本地 + | + Claude/Happy profile -> 模型 API + +OpenCode TUI/Web -> 独立 OpenCode profile -> 模型 API(云 / 中转 / 本地) ``` - 手机默认是自洽的控制面,不依赖远端 Happy relay。 @@ -28,9 +29,10 @@ OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API 负责。 - 本地推理也是可选扩展:小型 llama.cpp server 只监听 loopback,适合离线 摘要、分类、脱敏和断网兜底。 -- 模型 profile 只保存路由信息;凭据始终位于独立的 mode `0600` 文件。 -- 离线 `Mobile Lab Control` App 分别提供 Happy、llama.cpp、模型配置三个 - 原生微件;Termux:Widget 只保留为可选终端回退。 +- OpenCode 与 Claude/Happy 分别使用独立选择器和 active 文件,切换一边绝不 + 改写另一边,也不会隐式启停服务;凭据仍位于独立的 mode `0600` 文件。 +- 离线 `Mobile Lab Control` App 提供五个原生微件:Happy、llama.cpp、 + Claude/Happy 模型、OpenCode Web、OpenCode 模型。 ## 仓库内容 @@ -66,11 +68,12 @@ OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API | 手机本地 Happy | 已做端到端验证 | 提供显式启停 wrapper | | Provider 切换 | 私有设备配置已验证 | 提供脱敏通用 profile | | llama.cpp 本地模型 | Qwen3 0.6B Q8 的 API 与 `local-lite` 已通过 | 提供带认证的 loopback wrapper 与限次 checker | -| 原生控制微件 | 三个独立微件已完成零模型调用真机验收 | 提供源码、白名单桥接、双语 UI 与 Release APK | +| 原生控制微件 | 五项控制已在 Android 14 真机验收,服务与两条模型路由互不联动 | 提供源码、白名单桥接、双语 UI 与 Release APK | -最终手机验收也覆盖了控制面解耦:切换 profile 不改变正在运行的服务,Happy -启动时 profile 与当前配置不一致时明确显示 `STALE`,两个服务都能在不改变 -模型选择的情况下独立关闭。 +2026-08-09 的手机验收不仅检查配置:OpenCode、Claude CLI 均通过标为 OpenAI +的中转返回精确 nonce,Happy 也从手机 App 得到精确回复。Android 14 微件改用 +用户触发的短生命周期 Activity 后不再报后台启动限制;验收结束后所有本地服务 +端口均已关闭。 “仓库包含 wrapper”不等于“上游组件已经自动安装”。二进制、服务端源码和 GGUF 都必须由操作者明确选择、固定来源并验证 SHA-256。 diff --git a/apps/mobile-lab-control/AndroidManifest.xml b/apps/mobile-lab-control/AndroidManifest.xml index 5ee8f7d..3ac75c7 100644 --- a/apps/mobile-lab-control/AndroidManifest.xml +++ b/apps/mobile-lab-control/AndroidManifest.xml @@ -24,6 +24,16 @@ + + + @@ -60,5 +70,15 @@ android:name="android.appwidget.provider" android:resource="@xml/widget_model_info" /> + + + + + + + + + + diff --git a/apps/mobile-lab-control/README.md b/apps/mobile-lab-control/README.md index 07a30c2..7315ee4 100644 --- a/apps/mobile-lab-control/README.md +++ b/apps/mobile-lab-control/README.md @@ -2,13 +2,16 @@ [English](README.md) · [简体中文](README.zh-CN.md) -An offline Android control surface for the Android AI Stack. It exposes three +An offline Android control surface for the Android AI Stack. It exposes five independent home-screen widgets: - **Happy** — status, start/stop, restart, and refresh; - **llama.cpp** — status, start/stop, memory release, and refresh; -- **model profiles** — current readable profile plus one-tap configuration - switching without changing either service lifecycle. +- **Claude Code + Happy models** — readable cloud profile selection for that + client pair only; +- **OpenCode Web** — status, loopback-only start/stop, and browser handoff; +- **OpenCode models** — an independent cloud/relay/local selector that never + rewrites Claude Code or Happy. The app has no Internet permission, reads no token, holds no root privilege, and accepts no free-form command. It can invoke only @@ -42,12 +45,18 @@ Verify future APKs against this identity as well as the attached file checksum. `install-control-bridge --enable-external-apps`. 3. Install the APK and grant `com.termux.permission.RUN_COMMAND` when Android prompts. -4. Open the app and add Happy, llama.cpp, and model-profile widgets separately. +4. Open the app and add the five controls separately. OpenCode Web may also be + started and stopped from the app page. Copy [`config/widget-profiles.example`](../../config/widget-profiles.example) to -`~/.config/android-ai-stack/widget-profiles`, keep it mode `0600`, and replace -only the redacted profile names and labels. The bridge never reads provider -tokens or CC Switch databases. +`~/.config/android-ai-stack/widget-profiles`, or split it into mode-`0600` +`claude-widget-profiles` and `opencode-widget-profiles`. The two selectors use +separate active files and commands (`ai-model` and `oc-model`). The bridge never +reads provider tokens or CC Switch databases. + +Android 14 blocks an AppWidget broadcast receiver from starting Termux's +service directly. Widget taps therefore enter a short-lived, user-initiated +activity before invoking the fixed allow-list bridge. ## Upgrade identity diff --git a/apps/mobile-lab-control/README.zh-CN.md b/apps/mobile-lab-control/README.zh-CN.md index 99cfaab..7e4a3f0 100644 --- a/apps/mobile-lab-control/README.zh-CN.md +++ b/apps/mobile-lab-control/README.zh-CN.md @@ -2,11 +2,13 @@ [English](README.md) · [简体中文](README.zh-CN.md) -这是 Android AI Stack 的离线原生控制面,提供三个彼此独立的桌面微件: +这是 Android AI Stack 的离线原生控制面,提供五个彼此独立的桌面微件: - **Happy**:状态、启动/关闭、重启与刷新; - **llama.cpp**:状态、启动/关闭、释放模型内存与刷新; -- **模型配置**:显示当前可读名称,一键切换配置,但不改变两个服务的启停状态。 +- **Claude Code + Happy 模型**:只切换这两个客户端使用的云端配置; +- **OpenCode Web**:显示状态、仅 loopback 启停,并在启动后打开浏览器; +- **OpenCode 模型**:独立切换云端、中转或本地模型,绝不改写 Claude/Happy。 App 不申请联网权限、不读取 Token、不持有 root,也没有任意命令输入。它只通过 Termux 官方 [`RUN_COMMAND` 接口](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent) @@ -35,11 +37,16 @@ Release 发布,不提交进源码历史。 1. 在 Termux 安装 Android AI Stack 操作脚本; 2. 理解外部命令边界后执行 `install-control-bridge --enable-external-apps`; 3. 安装 APK,并在 Android 提示时授予 `com.termux.permission.RUN_COMMAND`; -4. 打开 App,分别添加 Happy、llama.cpp、模型配置三个微件。 +4. 打开 App,分别添加五个控制微件;OpenCode Web 也可以直接在 App 页面启停。 把 [`config/widget-profiles.example`](../../config/widget-profiles.example) 复制到 -`~/.config/android-ai-stack/widget-profiles`,保持 mode `0600`,只填写脱敏的 -Profile 名称和展示名。桥接层不会读取 Provider Token 或 CC Switch 数据库。 +`~/.config/android-ai-stack/widget-profiles`,或拆成 mode `0600` 的 +`claude-widget-profiles` 与 `opencode-widget-profiles`。两个选择器分别使用 +`ai-model`、`oc-model` 和独立 active 文件。桥接层不会读取 Provider Token 或 +CC Switch 数据库。 + +Android 14 不允许 AppWidget 广播接收器直接启动 Termux 服务,因此微件点击会先 +进入一个短生命周期、由用户明确触发的 Activity,再调用固定白名单桥接。 ## 升级签名说明 diff --git a/apps/mobile-lab-control/res/layout/activity_main.xml b/apps/mobile-lab-control/res/layout/activity_main.xml index d41023b..e6f6661 100644 --- a/apps/mobile-lab-control/res/layout/activity_main.xml +++ b/apps/mobile-lab-control/res/layout/activity_main.xml @@ -56,6 +56,24 @@ android:text="@string/refresh_all" android:textColor="@color/widget_text" /> +