@zhz8888/dsh-android-emulator-helper 0.0.0-stage → 1.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zhz8888
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This package is a DeepSeek Harness port of the ZCode official
26
+ `android-emulator` plugin, which is distributed under the MIT License by Z.ai.
27
+ Its provider logic and Kotlin/Jetpack Compose project template are ported from
28
+ that plugin, and the template files are kept verbatim. See the "Differences
29
+ from the original" section of README.md for the full account of what changed.
package/README.md CHANGED
@@ -1,3 +1,294 @@
1
- # Temporary Holding Version
1
+ # dsh-android-emulator-helper
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
+ **English** | [简体中文](README.zh-CN.md)
4
+
5
+ The package is named `@zhz8888/dsh-android-emulator-helper`, and the plugin page
6
+ shows its localized name — **Android Emulator** in English, **Android 模拟器** in
7
+ Chinese — read from `locale/en.json` and `locale/zh.json`, which carry the
8
+ one-line description too.
9
+
10
+ A DSH (DeepSeek Harness) port of the official ZCode `android-emulator@0.1.0`
11
+ plugin. All 23 tools keep their original names and argument names, and the
12
+ plugin adds a full **settings page** (Settings → Android emulator) so the SDK,
13
+ emulator, system-image, build and output parameters are configured in the app
14
+ instead of through environment variables.
15
+
16
+ ## Contents
17
+
18
+ - [Install](#install)
19
+ - [Settings page](#settings-page)
20
+ - [Tools](#tools)
21
+ - [Skill and command](#skill-and-command)
22
+ - [Differences from the original](#differences-from-the-original)
23
+ - [Architecture](#architecture)
24
+ - [Development](#development)
25
+ - [Known limitations](#known-limitations)
26
+ - [Releases](#releases)
27
+ - [License](#license)
28
+
29
+ ## Install
30
+
31
+ ### 1. Install dependencies
32
+
33
+ ```bash
34
+ cd /path/to/dsh-android-emulator-helper
35
+ pnpm install # the @deepseek-ai/* peers + react for the tests
36
+ ```
37
+
38
+ Every `@deepseek-ai/*` package this plugin imports — `cordis`, `dsh-tools` and
39
+ `schemastery` — is a **peer dependency the host provides**, not a dependency of
40
+ its own: the profile already carries them, and resolving them to the host's copy
41
+ keeps one `schemastery` instance behind the settings form. The local `pnpm
42
+ install` above installs them for development and tests only.
43
+
44
+ DSH's compatibility gate compares exactly the `@deepseek-ai/dsh*` peers against
45
+ the runtime version, so a mismatch refuses the install loudly. The ranges are
46
+ therefore lower-bound only (`>=0.2.0-rc.2`): an upper bound would have to be
47
+ widened by a new release on every harness bump, and a `^` range silently drops
48
+ the prereleases of the next version tuple.
49
+
50
+ ### 2. Mount into a profile
51
+
52
+ **DSH Desktop**: the `desktop` profile is managed exclusively by the Electron
53
+ app — `dsh plugin --profile desktop` is refused by the CLI. Install through the
54
+ in-app plugin manager instead.
55
+
56
+ **CLI / any other profile**:
57
+
58
+ ```bash
59
+ dsh plugin --profile <profile> add /absolute/path/to/dsh-android-emulator-helper
60
+ ```
61
+
62
+ `dsh.bundle.patch` is declared, so DSH reconciles the package into
63
+ `dsh.profile.bundles` and merges `cordis.patch.yml` at boot. Do **not** also add
64
+ the same row to the profile's own `cordis.patch.yml`: a double mount registers
65
+ every tool name twice and fails the load.
66
+
67
+ ### 3. Open the settings page
68
+
69
+ After a restart or hot reload, open **Settings → Android emulator**. The page
70
+ appears only while the host half actually serves the settings namespace, and
71
+ leaves no trace when it does not.
72
+
73
+ ## Settings page
74
+
75
+ The settings namespace is the profile patch row id, `android-emulator-helper`.
76
+ The page reads and writes through `ctx.configForms`, so staged edits, dirty
77
+ detection, inline validation, optimistic-concurrency revisions and
78
+ "overridden / reset to default" all come from the harness. It draws its own
79
+ title and one card per group — the card outline is what separates the
80
+ categories — and the save lives in a sticky action bar at the bottom that
81
+ appears only while there is something to act on: a staged edit, a write in
82
+ flight, or a failure worth retrying.
83
+
84
+ | Group | Field | Kind | Default |
85
+ |---|---|---|---|
86
+ | SDK and toolchain | `sdkPath` | text | empty (auto-detect) |
87
+ | | `jdkMajor` | text, suggestions from this machine | `17` |
88
+ | | `buildToolsVersion` | text, suggestions | `35.0.0` |
89
+ | Emulator and devices | `defaultAvd` | text, suggestions | `medium_phone` |
90
+ | | `defaultSerial` | text, suggestions | empty |
91
+ | | `autoStartEmulator` | switch | on |
92
+ | | `emulatorArgs` | multiline, one argument per line | empty |
93
+ | | `bootTimeoutSeconds` | integer | `180` |
94
+ | API and system image | `apiLevel` | text, suggestions | `35` |
95
+ | | `systemImageVariant` | select | `default` |
96
+ | | `systemImageAbi` | select (empty = host architecture) | auto |
97
+ | | `minSdk` | integer | `23` |
98
+ | Build | `buildTimeoutSeconds` | integer | `600` |
99
+ | | `gradleExtraArgs` | multiline | empty |
100
+ | Output and logs | `dataDir` | text | `$DSH_HOME/android-emulator-helper` |
101
+ | | `screenshotDir` | text | `<data dir>/screenshots` |
102
+ | | `logTailLines` | integer | `500` |
103
+
104
+ Two buttons back the page with exact Fetch routes on the host half:
105
+
106
+ - **Run diagnostic** → `GET /api/android-emulator-helper/status`, the same
107
+ checks as `android_preflight`, each with its detail and fix hint.
108
+ - **Refresh devices and AVDs** → `GET /api/android-emulator-helper/inventory`,
109
+ detecting the SDK root, AVDs, connected devices, installed JDKs, system
110
+ images, platforms and build-tools to populate the fields' suggestions.
111
+
112
+ The seven fields the original plugin exposed still fall back to their
113
+ `ANDROID_PLUGIN_*` environment variables, so an existing ZCode or CI setup keeps
114
+ working: the resolution order is settings page → environment variable →
115
+ declared default.
116
+
117
+ ## Tools
118
+
119
+ `android_preflight`, `android_discover_project`, `android_create_app`,
120
+ `android_build_app`, `android_build_and_run`, `android_list_devices`,
121
+ `android_list_avds`, `android_start_emulator`, `android_stop_emulator`,
122
+ `android_create_avd`, `android_install_app`, `android_launch_app`,
123
+ `android_terminate_app`, `android_open_url`, `android_screenshot`,
124
+ `android_logs`, `android_ui_status`, `android_ui_describe`, `android_ui_resolve`,
125
+ `android_ui_tap`, `android_ui_swipe`, `android_ui_type_text`,
126
+ `android_ui_keyevent` — identical names and arguments to the original MCP
127
+ server.
128
+
129
+ `android_screenshot` always writes the PNG to disk and additionally submits it
130
+ to the host attachment store for a canonical image block. When the current model
131
+ does not declare image input, or the deployment stores no attachments, the
132
+ result degrades to text plus the file path rather than failing the call.
133
+
134
+ ## Skill and command
135
+
136
+ - The bundled `android-dev` skill is registered through `ctx.skills.register()`
137
+ with the skill directory as its `resourceBase`, so `INSTALL_ENVIRONMENT.md`
138
+ keeps resolving. Its text is rewritten for DSH: no `mcp__…` prefixes,
139
+ environment setup points at the settings page.
140
+ - `/android-dev [goal]` runs the diagnostic and reports the verdict, failures
141
+ and fixes. DSH user commands deliberately create no model message, so the
142
+ command does the one useful thing that needs no model.
143
+
144
+ ## Differences from the original
145
+
146
+ | | Original (ZCode) | This port (DSH) |
147
+ |---|---|---|
148
+ | Tool exposure | stdio MCP server, `mcp__android_emulator__*` | native `ctx.tools.register()`, `android_*` |
149
+ | Configuration | `plugin.json` `userConfig` via `ANDROID_PLUGIN_*` | settings page + volatile `Config`; env vars still fall back |
150
+ | Working directory | `process.cwd()` | `exec.agent.session.header.cwd` (session workspace) |
151
+ | Data directory | `ANDROID_PLUGIN_DATA` / temp | `$DSH_HOME/android-emulator-helper`, configurable |
152
+ | Screenshot | MCP image content (base64) | host attachment reference; text fallback |
153
+ | Slash command | injects a model prompt | runs the diagnostic directly |
154
+ | Process execution | direct `node:child_process` | unchanged |
155
+
156
+ The direct `spawn` is a deliberate trade-off: `gradlew`, `adb` and `emulator`
157
+ behave byte-for-byte as they did in the original, and `adb exec-out` needs no
158
+ binary-stream adapter. The cost is bypassing DSH's subprocess seam — no managed
159
+ process ranges, env scrubbing, output spill or teardown termination. Every call
160
+ here has an explicit timeout and `SIGTERM`, and the plugin reads and forwards no
161
+ credential-bearing environment variables.
162
+
163
+ ## Architecture
164
+
165
+ ```
166
+ dsh-android-emulator-helper/
167
+ ├── package.json # dsh.bundle.patch, dsh.client, peer dependencies (the compatibility gate)
168
+ ├── cordis.patch.yml # - insert: [{ id: android-emulator-helper, name: '@zhz8888/dsh-android-emulator-helper' }]
169
+ ├── icon.svg # plugin icon: the Android robot head, brand green, readable in both themes
170
+ ├── locale/en.json # plugin-page name and description (English; its presence is what enables every other language)
171
+ ├── locale/zh.json # the Simplified Chinese pair
172
+ ├── lib/
173
+ │ ├── index.js # host half: tools, skill, command and the two Fetch routes
174
+ │ ├── client.js # browser half: a hand-written __ModuleLoader__ bundle, the settings page
175
+ │ ├── settings.js # field declarations, the schemastery Config (every field .volatile()), live reads
176
+ │ ├── runtime.js # one runtime per call: session cwd + live settings → data directory
177
+ │ ├── tools/index.js # the 23 defineTool definitions, argument/result schemas, image admission
178
+ │ ├── providers/ # ported from the original plugin file by file, now taking an explicit runtime
179
+ │ └── util/ # process execution, paths, content blocks
180
+ ├── skills/android-dev/ # SKILL.md + INSTALL_ENVIRONMENT.md
181
+ ├── scripts/ # check-bundle.mjs (consistency gate), smoke.mjs (real-machine), verify-mount.mjs
182
+ └── test/ # node:test unit and integration suites
183
+ ```
184
+
185
+ Three constraints shape everything above:
186
+
187
+ 1. **The settings namespace has exactly one source of truth**: the profile patch
188
+ row id. The `id` in `cordis.patch.yml`, `SETTINGS_NAMESPACE` in
189
+ `lib/settings.js` and the literal in `lib/client.js` must agree;
190
+ `scripts/check-bundle.mjs` verifies them, and also that the host and client
191
+ field tables have not drifted apart.
192
+ 2. **A browser bundle cannot import a host module** — the client purity gate
193
+ forbids cross-plugin value imports — so the field table is written twice and
194
+ held together by that gate. The bundle may `require` only the shell's nine
195
+ static modules (React, Cordis, `dsh-client-store`, `dsh-client-ui-slots`,
196
+ `dsh-client-ui-primitives`, `dsh-client-ui-dockkit`, …).
197
+ 3. **The plugin-page name is a package resource, not a manifest field**: DSH
198
+ resolves `<package>/locale/en.json` through the `exports` map, so
199
+ `./locale/*.json` has to stay exported and `locale` has to stay in `files`;
200
+ either omission degrades silently to showing the raw package name. `en.json`
201
+ is also the switch that enables every other language file, so it cannot be
202
+ dropped in favour of `zh.json`. `test/manifest.test.js` pins all three
203
+ conditions.
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ pnpm install # the @deepseek-ai/* peers plus react/react-dom for the tests
209
+ pnpm test # 145 tests, no Android SDK needed
210
+ pnpm run check # tests + host/client gate + real-Cordis mount
211
+ pnpm run verify:mount # real Cordis Context and real ToolRuntime
212
+ pnpm run smoke # read-only real-machine probes
213
+ pnpm run smoke:emulator # also boot an AVD, screenshot, logs, stop
214
+ pnpm run link:dsh-deps # offline dev: symlink a local DSH's @deepseek-ai packages
215
+ ```
216
+
217
+ The package manager is pnpm: `packageManager` pins `pnpm@11.8.0`, and
218
+ `pnpm-lock.yaml` is the only lockfile (npm's `package-lock.json` is gone).
219
+ [`pnpm-workspace.yaml`](pnpm-workspace.yaml) does not open a workspace — this is a
220
+ single-package repository — it only carries the install-script policy:
221
+ `strictDepBuilds: true`. pnpm v10+ skips a dependency's build scripts *silently*
222
+ by default, which is the wrong failure mode here, so the strict mode makes
223
+ `pnpm install` fail immediately with `ERR_PNPM_IGNORED_BUILDS` and forces an
224
+ explicit per-package decision. The current tree (27 packages) was scanned and
225
+ contains no install-time build scripts, so no `allowBuilds` entry is needed.
226
+
227
+ Four layers of verification:
228
+
229
+ | Layer | Command | What it proves |
230
+ |---|---|---|
231
+ | Unit and parsers | `pnpm test` | Bundle load contract, manifest/patch contract, settings projection (live volatile reads, env fallback, out-of-range fallback), every ported parser, and the lossless-JSON projection |
232
+ | Settings page render | `pnpm test` | The whole page rendered with real React and `react-dom/server`: every group, field and dictionary key present, controlled inputs holding live values, switch and select state correct, override badge and reset, both route buttons, and no action bar while nothing is staged |
233
+ | Consistency gate | `scripts/check-bundle.mjs` | Bundle id equals the package name, namespace equals the patch row id, the two field tables and the two group tables match key-for-key and in order, every heading, hint, placeholder and option matches the host **word for word in both languages**, both routes agree on both sides, `locale/*.json` carries the same keys in both languages, and the bundle is still a classic script |
234
+ | Route integration | `test/routes-integration.test.js` | The two settings routes mounted on a **real** Cordis `Context`: each handler invoked and its `Response` read, all 17 fields arriving as live volatile references, a saved setting visible to the very next call, a handler error answered as a `500` body rather than a crash, an absent optional service tolerated, and disposal removing both routes |
235
+ | Real host mount | `scripts/verify-mount.mjs` | Mounted on a **real** Cordis `Context` with the **real** `ToolRuntime`: 23 tools registered, **all 23 dispatched through the registry** and validated against their argument and result schemas, malformed arguments refused, unknown names unresolved, 17 volatile fields, full unregistration on dispose |
236
+ | Real device | `pnpm run smoke` … | What only a machine with an SDK, an emulator and Gradle can prove: discovery, boot and teardown, screenshots, logcat, UI automation, and a real `android_create_app` → build → install → launch cycle |
237
+
238
+ ### Registry constraints that shape the tools
239
+
240
+ Two constraints come from the real tool registry and cannot be seen through a
241
+ hand-written test double:
242
+
243
+ - A result must match the tool's declared `output.schema`, so the two listing
244
+ tools return `{ devices: [...] }` and `{ avds: [...] }` rather than a bare
245
+ array, which the registry rejects as invalid output.
246
+ - A result must be lossless JSON, and the registry's snapshot rejects
247
+ `undefined` outright. Every ported provider models an absent field as
248
+ `undefined` — a device without a `model`, a UI node without `bounds`, a
249
+ discovery report with no Gradle root — so `toLosslessJson()` normalizes the
250
+ value on the single path every tool result travels, instead of 23 providers
251
+ each handling it.
252
+
253
+ ## Known limitations
254
+
255
+ - Linux is unsupported, exactly as in the original; `android_preflight` reports
256
+ it.
257
+ - `android_start_emulator` always starts a new emulator and never reuses one.
258
+ Pass `serial` to the other tools to reuse a target.
259
+ - `android_create_avd` always answers `no` to `avdmanager`'s confirmation, so it
260
+ can never silently trigger a multi-gigabyte image download.
261
+ - The settings page's installed-image list comes from scanning the SDK
262
+ directory, not from `sdkmanager --list_installed`, which fails in restricted
263
+ environments and prefixes its output with deprecation notices.
264
+ - The settings page's two routes are served by the plugin and are mounted in a
265
+ real profile, but their HTTP responses cannot be exercised outside the GUI:
266
+ the desktop app authenticates every request, so a bare `curl` to
267
+ `/api/android-emulator-helper/status` answers `403 forbidden` before the
268
+ handler runs. `test/routes-integration.test.js` therefore invokes the handlers
269
+ directly on a real Cordis context instead. The page's spacing, dark theme and
270
+ narrow-width wrapping still deserve a human look in the GUI.
271
+ - `@deepseek-ai/dsh-client-ui-primitives` publishes only `0.0.1-rc.1` on npm,
272
+ which does not match the runtime API, so the render test uses a faithful double
273
+ for the primitives while React and `react-dom/server` stay real.
274
+
275
+ ## Releases
276
+
277
+ | Version | Highlights |
278
+ |---|---|
279
+ | **1.1.0** | Settings page rebuilt around cards with a sticky action bar; both timeouts asked for in seconds; eight defects fixed, among them a generated project that did not compile, `android_ui_type_text` corrupting every `%` into `%25`, `android_ui_describe` serving the previous screen after a failed dump, and `android_start_emulator` silently starting a different AVD than the one requested. All 23 tools were exercised on a real emulator. |
280
+ | 1.0.0 | First release: the 23 native `android_*` tools, the embedded `android-dev` skill and the `/android-dev` command, and the settings page. |
281
+
282
+ **Restart the DSH host process after upgrading.** The package is mounted as a
283
+ `link:` symlink and Node caches ESM modules for the life of the process, so a
284
+ change under `lib/` does not take effect until the host restarts — until then the
285
+ tools keep serving the old code.
286
+
287
+ ## License
288
+
289
+ [MIT](LICENSE), copyright (c) 2026 zhz8888.
290
+
291
+ Tools and provider logic are ported from the ZCode official `android-emulator`
292
+ plugin (also MIT, copyright Z.ai); the Kotlin/Compose project template files are
293
+ kept verbatim. See the note at the end of [LICENSE](LICENSE) for the full
294
+ provenance.
@@ -0,0 +1,232 @@
1
+ # dsh-android-emulator-helper
2
+
3
+ [English](README.md) | **简体中文**
4
+
5
+ 包名为 `@zhz8888/dsh-android-emulator-helper`,插件页显示的是本地化名称——英文 **Android Emulator**、中文 **Android 模拟器**——由 `locale/en.json` 与 `locale/zh.json` 提供,一句话描述也在其中。
6
+
7
+ 把 ZCode 官方 `android-emulator@0.1.0` 插件移植为 **DSH(DeepSeek Harness)原生插件**:原有 23 个 MCP 工具全部保留为 DSH 原生工具,并新增一个完整的 **设置页面**(设置 → Android 模拟器),让 SDK、模拟器、系统镜像、构建与输出目录等参数都能在应用里配置,而不是靠环境变量。
8
+
9
+ - 工具名、参数名与原插件完全一致,为原 MCP 服务器写的工作流可直接迁移。
10
+ - 设置项 = 原插件 `userConfig` 的 7 个字段 + 10 个实用扩展。
11
+ - 设置页在「设置」侧栏新增一个独立分区,含分组表单、下拉与建议列表、内联校验、「检测环境」与「刷新设备与 AVD」按钮。
12
+
13
+ ## 目录
14
+
15
+ - [安装](#安装)
16
+ - [设置页面](#设置页面)
17
+ - [工具清单](#工具清单)
18
+ - [Skill 与斜杠命令](#skill-与斜杠命令)
19
+ - [与原插件的差异](#与原插件的差异)
20
+ - [架构](#架构)
21
+ - [开发与验证](#开发与验证)
22
+ - [已知限制](#已知限制)
23
+ - [版本](#版本)
24
+ - [许可](#许可)
25
+
26
+ ## 安装
27
+
28
+ ### 1. 先安装依赖
29
+
30
+ 包本身只发布源码与手写的客户端 bundle,运行依赖由 DSH profile 提供。
31
+
32
+ ```bash
33
+ cd /path/to/dsh-android-emulator-helper
34
+ pnpm install
35
+ ```
36
+
37
+ 本插件 import 的官方包 `@deepseek-ai/cordis`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/schemastery` **全部声明在 `peerDependencies`**,由宿主提供:profile 里本来就带着它们,解析到宿主那一份还能保证设置表单背后只有一个 `schemastery` 实例。上面的 `pnpm install` 只为开发与测试把它们装到本仓库。
38
+
39
+ DSH 的兼容性闸门正是按 `peerDependencies` 里 `@deepseek-ai/dsh*` 的版本区间与运行时版本比对,不匹配会明确拒装。区间因此**只写下界**(`>=0.2.0-rc.2`):写上界会在 DSH 每次升版时逼着插件重发一次放宽范围,而 `^` 区间会静默漏掉下一个版本元组的预发布版。
40
+
41
+ ### 2. 装进 profile
42
+
43
+ **DSH Desktop(本机 GUI)**:`desktop` profile 由 Electron 应用独占管理,`dsh plugin --profile desktop` 会被 CLI 主动拒绝。请通过应用内的插件管理器安装:把本目录加入本机连接器/插件源,或使用插件管理器的「安装」入口指向本地目录。
44
+
45
+ **CLI / 其他 profile**:
46
+
47
+ ```bash
48
+ dsh plugin --profile <profile> add /absolute/path/to/dsh-android-emulator-helper
49
+ ```
50
+
51
+ `dsh.bundle.patch` 已声明,因此安装后 DSH 的 reconcile 会自动把本包追加到 `dsh.profile.bundles`,profile 启动时合并 `cordis.patch.yml`,挂载唯一一行 host 入口。**不要**再手动往 profile 自己的 `cordis.patch.yml` 里加同一行,重复挂载会让同名工具二次注册并在加载期报错。
52
+
53
+ ### 3. 打开设置页
54
+
55
+ 重启/热加载后,打开 **设置 → Android 模拟器**。页面在 host 半侧真正提供了该设置命名空间时才会出现;反之不会留下任何痕迹。
56
+
57
+ ## 设置页面
58
+
59
+ 设置命名空间 = profile patch 行 id = `android-emulator-helper`(同时是 `dsh.name`)。页面通过 `ctx.configForms` 读写,因此具备暂存编辑、脏值检测、内联校验、乐观并发(revision)与「已覆盖/恢复默认」语义。页面自绘标题、每个分组一张卡片(卡片边框就是分组之间的分隔),保存按钮位于底部悬浮胶囊操作栏,只在有可操作内容时出现:有暂存改动、正在写入,或一次失败值得重试。
60
+
61
+ | 分组 | 设置项 | 类型 | 默认值 | 说明 |
62
+ |---|---|---|---|---|
63
+ | SDK 与工具链 | `sdkPath` | 文本 | 空 | Android SDK 根目录;留空时探测 `ANDROID_HOME`、`ANDROID_SDK_ROOT` 与常见路径 |
64
+ | | `jdkMajor` | 文本(建议来自本机 JDK) | `17` | JDK 主版本,用于预检与安装指引 |
65
+ | | `buildToolsVersion` | 文本(建议来自已装 build-tools) | `35.0.0` | 安装指引引用的 build-tools 版本 |
66
+ | 模拟器与设备 | `defaultAvd` | 文本(建议来自本机 AVD) | `medium_phone` | 需要启动模拟器时优先使用 |
67
+ | | `defaultSerial` | 文本(建议来自已连接设备) | 空 | 优先使用的设备序列号 |
68
+ | | `autoStartEmulator` | 开关 | 开 | 关闭后无可用设备时直接报错,不启动模拟器 |
69
+ | | `emulatorArgs` | 多行文本 | 空 | 额外 `emulator` 参数,每行一个,`#` 为注释 |
70
+ | | `bootTimeoutSeconds` | 整数 | `180` | 模拟器启动超时(秒) |
71
+ | API 与系统镜像 | `apiLevel` | 文本(建议来自已装 platform/镜像) | `35` | `compileSdk` 与系统镜像包名使用的 API 级别 |
72
+ | | `systemImageVariant` | 下拉 | `default` | `default` / `google_apis` / `google_apis_playstore` |
73
+ | | `systemImageAbi` | 下拉 | 自动 | 留空按宿主架构,Apple Silicon 用 `arm64-v8a` |
74
+ | | `minSdk` | 整数 | `23` | `android_create_app` 生成项目的默认 minSdk |
75
+ | 构建 | `buildTimeoutSeconds` | 整数 | `600` | 单次 assemble 的最长运行时间(秒) |
76
+ | | `gradleExtraArgs` | 多行文本 | 空 | 额外 Gradle 参数,每行一个 |
77
+ | 输出目录与日志 | `dataDir` | 文本 | 空 | 截图与构建日志根目录;留空用 `$DSH_HOME/android-emulator-helper` |
78
+ | | `screenshotDir` | 文本 | 空 | 截图目录;留空用 `<数据目录>/screenshots` |
79
+ | | `logTailLines` | 整数 | `500` | `android_logs` 未指定行数时的默认行数 |
80
+
81
+ 页面底部两个按钮调用 host 半侧的两个精确 Fetch 路由:
82
+
83
+ - **检测环境** → `GET /api/android-emulator-helper/status`,返回与 `android_preflight` 相同的检查结果,逐条显示状态、详情与修复建议。
84
+ - **刷新设备与 AVD** → `GET /api/android-emulator-helper/inventory`,探测 SDK 根目录、AVD、已连接设备、已安装 JDK、已装系统镜像/platform/build-tools,用于填充上方字段的建议列表。
85
+
86
+ ### 兼容旧配置
87
+
88
+ 原插件的 7 个字段仍会回落到同名环境变量(`ANDROID_PLUGIN_SDK_PATH`、`ANDROID_PLUGIN_DEFAULT_AVD`、`ANDROID_PLUGIN_API_LEVEL`、`ANDROID_PLUGIN_BUILD_TOOLS_VERSION`、`ANDROID_PLUGIN_SYSTEM_IMAGE_VARIANT`、`ANDROID_PLUGIN_SYSTEM_IMAGE_ABI`、`ANDROID_PLUGIN_JDK_MAJOR`)。解析顺序为「设置页 → 环境变量 → 内置默认值」,所以已有 ZCode 或 CI 环境无需改动即可继续工作。
89
+
90
+ ## 工具清单
91
+
92
+ 23 个工具,名称与参数与原插件一致。
93
+
94
+ | 工具 | 作用 |
95
+ |---|---|
96
+ | `android_preflight` | 检查 SDK、adb、emulator、AVD、Java、Gradle 项目状态与 UI 自动化后端,并回显当前设置 |
97
+ | `android_discover_project` | 发现 Gradle 根、模块、variant、applicationId、manifest 与 APK 产物 |
98
+ | `android_create_app` | 生成最小 Kotlin + Jetpack Compose 工程 |
99
+ | `android_build_app` | 构建指定模块/variant |
100
+ | `android_build_and_run` | 构建 → 复用或启动设备 → 安装 → 启动 |
101
+ | `android_list_devices` | 列出 adb 可见设备 |
102
+ | `android_list_avds` | 列出 AVD |
103
+ | `android_start_emulator` | 启动新的 GUI 模拟器(从不复用已有设备) |
104
+ | `android_stop_emulator` | 按序列号停止模拟器 |
105
+ | `android_create_avd` | 用 `avdmanager` 创建 AVD |
106
+ | `android_install_app` | 安装 APK |
107
+ | `android_launch_app` | 按 applicationId 启动 |
108
+ | `android_terminate_app` | 强制停止应用 |
109
+ | `android_open_url` | 在设备上打开 URL |
110
+ | `android_screenshot` | 截图并落盘;模型支持图像输入时同时返回图像 |
111
+ | `android_logs` | 读取 logcat 尾部,可按 applicationId 过滤 |
112
+ | `android_ui_status` | 报告 UI 自动化后端可用性 |
113
+ | `android_ui_describe` | 返回 UI Automator 元素树 |
114
+ | `android_ui_resolve` | 按文本/描述/资源 id/类名解析到坐标 |
115
+ | `android_ui_tap` | 坐标点击 |
116
+ | `android_ui_swipe` | 坐标滑动 |
117
+ | `android_ui_type_text` | 向聚焦控件输入文本 |
118
+ | `android_ui_keyevent` | 发送 `BACK`/`HOME`/`ENTER`/`APP_SWITCH`/`MENU`/`SEARCH` |
119
+
120
+ `android_screenshot` 的行为值得单独说明:PNG 总会写到磁盘(默认在数据目录的 `screenshots/` 下),同时尝试把图像提交给宿主的附件服务以获得规范的图像块。当前模型未声明图像输入、或部署没有附件服务时,结果会退化为纯文本并附上文件路径,而不会让整次调用失败。
121
+
122
+ ## Skill 与斜杠命令
123
+
124
+ - **`android-dev` skill**:随包提供(`skills/android-dev/`),host 半侧通过 `ctx.skills.register()` 注册为嵌入式 skill,并把 skill 目录作为 `resourceBase`,因此 `INSTALL_ENVIRONMENT.md` 的相对引用照旧可用。内容已按 DSH 改写:不再有 `mcp__...` 前缀,环境配置指向设置页而非环境变量,并补充了设置页使用说明。
125
+ - **`/android-dev [目标]` 命令**:通过 `ctx.commands.register()` 注册。DSH 的用户命令刻意不产生模型消息,因此该命令做一件不依赖模型的事——跑一遍环境诊断,输出结论、失败项与修复建议,并给出下一步提示。
126
+
127
+ ## 与原插件的差异
128
+
129
+ | 方面 | 原插件(ZCode) | 本插件(DSH) |
130
+ |---|---|---|
131
+ | 工具暴露 | stdio MCP 服务器 `android-emulator`,模型看到 `mcp__android_emulator__*` | 原生 `ctx.tools.register()`,模型看到 `android_*` |
132
+ | 用户配置 | `plugin.json` 的 `userConfig`,经 `ANDROID_PLUGIN_*` 注入 | 设置页 + schemastery `Config`,字段全部 `.volatile()` 实时生效;环境变量仍作回落 |
133
+ | 工作目录 | `process.cwd()` | `exec.agent.session.header.cwd`(会话工作区) |
134
+ | 数据目录 | `ANDROID_PLUGIN_DATA` / 临时目录 | `$DSH_HOME/android-emulator-helper`,可配置 |
135
+ | 截图返回 | MCP image content(base64) | host 附件引用生成的图像块,失败时退化为文本 |
136
+ | 斜杠命令 | 注入一段提示词给模型 | 直接运行诊断并输出结论 |
137
+ | 进程调用 | `node:child_process` 直接 spawn | 保持一致 |
138
+
139
+ 保留直接 spawn 是有意的取舍:这样 `gradlew`、`adb`、`emulator` 的行为与原插件逐字节一致,且不必为 `adb exec-out` 的二进制流另写一层适配。代价是绕过了 DSH 的 subprocess seam——没有统一的过程管理、环境变量清洗、输出 spill 与卸载期终止。本插件的每个外部调用都有显式 timeout 与 `SIGTERM`,且不读写任何凭据环境变量。
140
+
141
+ ## 架构
142
+
143
+ ```
144
+ dsh-android-emulator-helper/
145
+ ├── package.json # dsh.bundle.patch、dsh.client、peer 依赖(兼容性闸门)
146
+ ├── cordis.patch.yml # - insert: [{ id: android-emulator-helper, name: '@zhz8888/dsh-android-emulator-helper' }]
147
+ ├── icon.svg # 插件图标:Android 机器人头部,品牌绿,明暗主题下都可辨认
148
+ ├── locale/en.json # 插件页的显示名与描述(英文;它的存在才是其它语言文件生效的开关)
149
+ ├── locale/zh.json # 简体中文那一份
150
+ ├── lib/
151
+ │ ├── index.js # host 半侧:注册工具 / skill / 命令 / 两个 Fetch 路由
152
+ │ ├── client.js # browser 半侧:__ModuleLoader__ 手写 bundle,设置页
153
+ │ ├── settings.js # 字段声明、schemastery Config(全部 .volatile())、实时读取
154
+ │ ├── runtime.js # 每次调用构造 runtime(会话 cwd + 实时设置 → 数据目录)
155
+ │ ├── tools/index.js # 23 个 defineTool 定义、参数/结果 schema、图像准入
156
+ │ ├── providers/ # 从原插件逐文件移植,改为显式接收 runtime
157
+ │ └── util/ # 进程执行、路径、内容块
158
+ ├── skills/android-dev/ # SKILL.md + INSTALL_ENVIRONMENT.md
159
+ ├── scripts/ # check-bundle.mjs(一致性闸门)、smoke.mjs(真机集成)
160
+ └── test/ # node:test 单元与集成测试
161
+ ```
162
+
163
+ 三个关键约束决定了上面的形状:
164
+
165
+ 1. **设置命名空间只有一个真相来源**:profile patch 行 id。`cordis.patch.yml` 的 `id`、`lib/settings.js` 的 `SETTINGS_NAMESPACE`、`lib/client.js` 的字面量必须一致;`scripts/check-bundle.mjs` 会校验它们,也会校验 host 与 client 两份字段表无漂移。
166
+ 2. **browser bundle 不能 import host 模块**(客户端纯度闸门禁止跨插件值导入),因此字段表被写了两遍,靠闸门保持一致;bundle 只能 `require` 宿主的 9 个静态模块(React、Cordis、`dsh-client-store`、`dsh-client-ui-slots`、`dsh-client-ui-primitives`、`dsh-client-ui-dockkit` 等)。
167
+ 3. **插件页的名字是包内资源,不是 manifest 字段**:DSH 通过 `exports` 解析 `<包名>/locale/en.json`,所以 `./locale/*.json` 必须保持导出、`locale` 必须留在 `files` 里;任一处缺失都会静默退化成显示原始包名。`en.json` 同时是其它语言文件的开关,因此不能只留 `zh.json`。这三条都由 `test/manifest.test.js` 钉住。
168
+
169
+ ## 开发与验证
170
+
171
+ ```bash
172
+ pnpm install # 装官方 peer 包与 react/react-dom(开发与测试用)
173
+ pnpm test # 145 个测试(无需 Android SDK)
174
+ pnpm run check # 测试 + host/client 一致性闸门 + 真实 Cordis 挂载验证
175
+ pnpm run verify:mount # 真实 Cordis Context + 真实 ToolRuntime 注册表
176
+ pnpm run smoke # 只读真机探测:preflight / AVD / 设备 / 项目发现 / inventory
177
+ pnpm run smoke:emulator # 额外:启动模拟器 → 截图 → 读日志 → 停止
178
+ pnpm run link:dsh-deps # 离线开发:把本机 DSH 的 @deepseek-ai 包软链进 node_modules
179
+ ```
180
+
181
+ ### 四层验证
182
+
183
+ | 层 | 命令 | 证明了什么 |
184
+ |---|---|---|
185
+ | 单元 / 解析器 | `pnpm test` | 客户端 bundle 加载契约与静态模块约束、manifest 与 patch 契约、设置投影(volatile 实时读取、环境变量回落、越界回落)、移植过来的解析器(`adb devices`、`emulator -list-avds`、UI Automator XML、`input text` 转义、APK 选择、JDK 主版本归一、无损 JSON 归一) |
186
+ | 设置页渲染 | `pnpm test`(`client-render.test.js`) | 用真实 React + `react-dom/server` 渲染整个设置页:分组/字段/字典键完整、受控输入带真实值、开关与下拉状态正确、「已覆盖/恢复默认」、两个按钮齐全、无未解析字典键,且没有暂存改动时不出现操作栏 |
187
+ | 一致性闸门 | `scripts/check-bundle.mjs` | bundle id = 包名、命名空间 = patch 行 id、host 与 client 两份字段表与分组表逐键逐序一致、每个标题/提示/占位符/选项在**两种语言下都逐字一致**、两个路由两侧一致、`locale/*.json` 两种语言键集相同、bundle 仍是经典脚本 |
188
+ | 路由集成 | `test/routes-integration.test.js` | 在**真实** Cordis `Context` 上挂载两条设置路由:真正调用 handler 并读取其 `Response`、17 个字段都以 volatile 引用形式到达、保存的设置对下一次调用立即可见、handler 出错时返回 `500` JSON 体而不是抛出、缺少可选服务时仍能挂载、dispose 后两条路由都被移除 |
189
+ | 真实宿主挂载 | `scripts/verify-mount.mjs` | 在**真实** Cordis `Context` + **真实** `ToolRuntime` 上挂载:23 个工具注册成功、**23 个工具全部经注册表实际派发**并通过参数/结果 schema 校验、错误参数被注册表拒绝、未知工具名不解析、17 个设置字段全部 volatile、dispose 后工具全部注销 |
190
+ | 真机 | `pnpm run smoke`… | 只有带 SDK、模拟器与 Gradle 的机器才能证明的部分:发现、启动与停止、截图、logcat、UI 自动化,以及真实的 `android_create_app` → 构建 → 安装 → 启动闭环 |
191
+
192
+ `pnpm run smoke` 覆盖只有真机才能证明的部分:SDK 发现、`adb`/`emulator` 实际调用、环境拼装、设置投影真正喂给 provider。
193
+ `pnpm run smoke:emulator` 进一步覆盖启动模拟器 → 截图 → 读日志 → 停止的完整链路。
194
+
195
+ ### 注册表给工具层带来的两条约束
196
+
197
+ 这两条约束来自真实工具注册表,手写的测试替身看不到:
198
+
199
+ - 结果必须匹配工具声明的 `output.schema`,因此两个列表工具返回
200
+ `{ devices: [...] }` 与 `{ avds: [...] }`,而不是裸数组——注册表会以
201
+ invalid output 拒绝裸数组。
202
+ - 结果必须是无损 JSON,注册表的快照会直接拒绝 `undefined`。而移植过来的
203
+ provider 一律用 `undefined` 表示缺失字段(设备没有 `model`、UI 节点没有
204
+ `bounds`、发现结果没有 Gradle 根),因此在所有工具结果唯一的必经路径上用
205
+ `toLosslessJson()` 统一归一,而不是在 23 个 provider 里各处理一遍。
206
+
207
+ ## 已知限制
208
+
209
+ - **Linux 不支持**,与原插件一致;`android_preflight` 会将其标记为不支持的宿主。
210
+ - `android_start_emulator` 永远启动新模拟器,从不复用已有目标(原插件语义)。复用请把 `serial` 传给其他工具。
211
+ - `android_create_avd` 对 `avdmanager` 的确认提示固定回答 `no`,绝不静默触发数 GB 的镜像下载;需要先自行 `sdkmanager --install`。
212
+ - UI 自动化依赖设备端 `uiautomator`;失败时 `android_ui_status` 会报告不可用,构建/运行/截图链路不受影响。
213
+ - 设置页的「已安装系统镜像」列表来自 SDK 目录扫描,而不是 `sdkmanager --list_installed`(后者在受限环境下会因创建 bin 目录失败,且输出含大量弃用提示)。
214
+ - 设置页的两条路由由插件提供、并在真实 profile 中挂载,但它们的 HTTP 响应无法在 GUI 之外验证:桌面端对每个请求做鉴权,未带浏览器会话的 `curl` 访问 `/api/android-emulator-helper/status` 会在进入 handler 之前就返回 `403 forbidden`。因此 `test/routes-integration.test.js` 改为在真实 Cordis context 上直接调用 handler。设置页的间距、暗色主题与窄屏换行仍建议在 GUI 中人工核对一次。
215
+ - `@deepseek-ai/dsh-client-ui-primitives` 在 npm 上只有 `0.0.1-rc.1`,与运行时 API 不一致;因此设置页渲染测试对 primitives 使用了忠实替身,真实 React 与 `react-dom/server` 仍是真件。
216
+ - 插件不触碰凭据:不读取、不转发任何含 key/token/secret 的环境变量。
217
+
218
+ ## 版本
219
+
220
+ | 版本 | 要点 |
221
+ |---|---|
222
+ | **1.1.0** | 设置页按卡片重做并加底部粘性操作栏;两处超时改为按秒填写;修复 8 个缺陷,其中包含生成的新工程编译不过、`android_ui_type_text` 把每个 `%` 变成 `%25`、`android_ui_describe` 在 dump 失败时返回上一屏、`android_start_emulator` 传入不存在的 AVD 名却静默启动另一个模拟器。23 个工具全部在真机上实测过。 |
223
+ | 1.0.0 | 首个版本:23 个原生 `android_*` 工具、内置 `android-dev` skill 与 `/android-dev` 命令,以及设置页。 |
224
+
225
+ **升级后请重启 DSH 宿主进程。** 本包以 `link:` 软链挂载,Node 对 ESM 做进程级缓存,`lib/` 的改动在宿主重启前不会生效——否则工具仍会跑旧代码。
226
+
227
+ ## 许可
228
+
229
+ [MIT](LICENSE),版权所有 (c) 2026 zhz8888。
230
+
231
+ 工具与 provider 逻辑移植自 ZCode 官方 `android-emulator` 插件(同样以 MIT 分发,
232
+ 版权归 Z.ai),Kotlin/Compose 工程模板逐字保留;完整的来源说明见 [LICENSE](LICENSE) 末尾。
@@ -0,0 +1,23 @@
1
+ # dsh bundle patch for @zhz8888/dsh-android-emulator-helper.
2
+ #
3
+ # This file is the `dsh.bundle.patch` layer of the package. Installing the
4
+ # plugin through the official CLI —
5
+ #
6
+ # dsh plugin --profile <name> add @zhz8888/dsh-android-emulator-helper
7
+ #
8
+ # — reconciles `dsh.profile.bundles` against the installed packages and, seeing
9
+ # this declaration, appends this package to the bundle stack. Profile boot then
10
+ # merges THIS patch, which mounts exactly one host row.
11
+ #
12
+ # The row id doubles as the settings namespace: `@deepseek-ai/dsh-settings`
13
+ # keys every editable form by the profile entry id, and the browser half
14
+ # watches that same id through `ctx.configForms.whileServed([...])`. Renaming
15
+ # the id here therefore requires renaming `SETTINGS_NAMESPACE` in
16
+ # `lib/settings.js` and `lib/client.js` together.
17
+ #
18
+ # Mounting this package twice (for example by adding the row to the profile's
19
+ # own cordis.patch.yml while the bundle channel is also active) registers the
20
+ # same tool names twice and fails loudly at the loader. Use one channel only.
21
+ - insert:
22
+ - id: android-emulator-helper
23
+ name: '@zhz8888/dsh-android-emulator-helper'
package/icon.svg ADDED
@@ -0,0 +1,33 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="20 199 985 627" width="985" height="627" role="img">
2
+ <title>Android emulator helper</title>
3
+ <!--
4
+ The Android robot head, from the artwork supplied as `android.svg`, at its
5
+ original `#2c2c2c`.
6
+
7
+ DSH renders `package.json.icon` as an <img> whose src is a base64 data URL, so
8
+ the SVG is an isolated document: it cannot see the host page's
9
+ `body[data-ds-dark-theme]`, and `currentColor` would resolve to black. It can,
10
+ however, see `prefers-color-scheme`, and that is the exact lever we need — DSH
11
+ Desktop pushes the user's theme preference into Electron as
12
+ `nativeTheme.themeSource = spec.readThemeSource()` (electron-runtime), and
13
+ `themeSource` is precisely what drives `prefers-color-scheme` in every renderer
14
+ frame. Light / dark / system therefore all agree between the app chrome and
15
+ this image, and the fill below inverts with the theme.
16
+
17
+ `#d3d3d3` is the exact RGB inversion of `#2c2c2c` (255 - 44 = 211), which also
18
+ happens to balance the pair: 13.97:1 against the light background `#ffffff`
19
+ and 11.98:1 against the dark one `#151517`.
20
+
21
+ The fill also sits on the element as a presentation attribute, so a renderer
22
+ without CSS support (an SVG-to-PNG pipeline, a strict validator) still draws
23
+ the light-theme colour instead of nothing. Presentation attributes lose to any
24
+ CSS rule, which is what lets the media query override it.
25
+ -->
26
+ <style>
27
+ path { fill: #2c2c2c; }
28
+ @media (prefers-color-scheme: dark) {
29
+ path { fill: #d3d3d3; }
30
+ }
31
+ </style>
32
+ <path fill="#2c2c2c" d="M805.664 239.21c15.194 8.676 20.578 27.92 12.181 43.195l-0.258 0.462-70.477 123.435c121.382 72.426 206.947 199.48 221.734 348.54C970.712 773.67 955.922 790 937 790H88c-18.922 0-33.712-16.33-31.844-35.16 14.14-142.538 93.003-264.954 206.014-338.722L186.09 282.867c-8.763-15.348-3.425-34.893 11.923-43.656 15.194-8.676 34.502-3.53 43.39 11.465l0.266 0.457 76.406 133.822c59.16-27.753 125.102-43.216 194.427-43.216 62.75 0 122.73 12.67 177.416 35.655l72.09-126.26c8.764-15.348 28.31-20.686 43.657-11.923zM512.5 405.74c-191.04 0-352.265 136.52-387.48 319.779l-0.089 0.481h775.135l-0.088-0.481c-34.84-181.31-193.023-316.87-381.391-319.734zM320.53 565.87c23.56 0 42.66 19.115 42.66 42.695 0 23.58-19.1 42.696-42.66 42.696-23.561 0-42.66-19.116-42.66-42.696s19.099-42.695 42.66-42.695z m383.946 0c23.56 0 42.66 19.115 42.66 42.695 0 23.58-19.1 42.696-42.66 42.696-23.56 0-42.66-19.116-42.66-42.696s19.1-42.695 42.66-42.695z"/>
33
+ </svg>