@zhz8888/dsh-android-emulator-helper 0.0.0-stage → 1.0.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,278 @@
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
+ - [License](#license)
27
+
28
+ ## Install
29
+
30
+ ### 1. Install dependencies
31
+
32
+ ```bash
33
+ cd /path/to/dsh-android-emulator-helper
34
+ pnpm install # the @deepseek-ai/* peers + react for the tests
35
+ ```
36
+
37
+ Every `@deepseek-ai/*` package this plugin imports — `cordis`, `dsh-tools` and
38
+ `schemastery` — is a **peer dependency the host provides**, not a dependency of
39
+ its own: the profile already carries them, and resolving them to the host's copy
40
+ keeps one `schemastery` instance behind the settings form. The local `pnpm
41
+ install` above installs them for development and tests only.
42
+
43
+ DSH's compatibility gate compares exactly the `@deepseek-ai/dsh*` peers against
44
+ the runtime version, so a mismatch refuses the install loudly. The ranges are
45
+ therefore lower-bound only (`>=0.2.0-rc.2`): an upper bound would have to be
46
+ widened by a new release on every harness bump, and a `^` range silently drops
47
+ the prereleases of the next version tuple.
48
+
49
+ ### 2. Mount into a profile
50
+
51
+ **DSH Desktop**: the `desktop` profile is managed exclusively by the Electron
52
+ app — `dsh plugin --profile desktop` is refused by the CLI. Install through the
53
+ in-app plugin manager instead.
54
+
55
+ **CLI / any other profile**:
56
+
57
+ ```bash
58
+ dsh plugin --profile <profile> add /absolute/path/to/dsh-android-emulator-helper
59
+ ```
60
+
61
+ `dsh.bundle.patch` is declared, so DSH reconciles the package into
62
+ `dsh.profile.bundles` and merges `cordis.patch.yml` at boot. Do **not** also add
63
+ the same row to the profile's own `cordis.patch.yml`: a double mount registers
64
+ every tool name twice and fails the load.
65
+
66
+ ### 3. Open the settings page
67
+
68
+ After a restart or hot reload, open **Settings → Android emulator**. The page
69
+ appears only while the host half actually serves the settings namespace, and
70
+ leaves no trace when it does not.
71
+
72
+ ## Settings page
73
+
74
+ The settings namespace is the profile patch row id, `android-emulator-helper`.
75
+ The page reads and writes through `ctx.configForms`, so staged edits, dirty
76
+ detection, inline validation, optimistic-concurrency revisions and
77
+ "overridden / reset to default" all come from the harness.
78
+
79
+ | Group | Field | Kind | Default |
80
+ |---|---|---|---|
81
+ | SDK and toolchain | `sdkPath` | text | empty (auto-detect) |
82
+ | | `jdkMajor` | text, suggestions from this machine | `17` |
83
+ | | `buildToolsVersion` | text, suggestions | `35.0.0` |
84
+ | Emulator and devices | `defaultAvd` | text, suggestions | `medium_phone` |
85
+ | | `defaultSerial` | text, suggestions | empty |
86
+ | | `autoStartEmulator` | switch | on |
87
+ | | `emulatorArgs` | multiline, one argument per line | empty |
88
+ | | `bootTimeoutMs` | integer | `180000` |
89
+ | API and system image | `apiLevel` | text, suggestions | `35` |
90
+ | | `systemImageVariant` | select | `default` |
91
+ | | `systemImageAbi` | select (empty = host architecture) | auto |
92
+ | | `minSdk` | integer | `23` |
93
+ | Build | `buildTimeoutMs` | integer | `600000` |
94
+ | | `gradleExtraArgs` | multiline | empty |
95
+ | Output and logs | `dataDir` | text | `$DSH_HOME/android-emulator-helper` |
96
+ | | `screenshotDir` | text | `<data dir>/screenshots` |
97
+ | | `logTailLines` | integer | `500` |
98
+
99
+ Two buttons back the page with exact Fetch routes on the host half:
100
+
101
+ - **Run diagnostic** → `GET /api/android-emulator-helper/status`, the same
102
+ checks as `android_preflight`, each with its detail and fix hint.
103
+ - **Refresh devices and AVDs** → `GET /api/android-emulator-helper/inventory`,
104
+ detecting the SDK root, AVDs, connected devices, installed JDKs, system
105
+ images, platforms and build-tools to populate the fields' suggestions.
106
+
107
+ The seven fields the original plugin exposed still fall back to their
108
+ `ANDROID_PLUGIN_*` environment variables, so an existing ZCode or CI setup keeps
109
+ working: the resolution order is settings page → environment variable →
110
+ declared default.
111
+
112
+ ## Tools
113
+
114
+ `android_preflight`, `android_discover_project`, `android_create_app`,
115
+ `android_build_app`, `android_build_and_run`, `android_list_devices`,
116
+ `android_list_avds`, `android_start_emulator`, `android_stop_emulator`,
117
+ `android_create_avd`, `android_install_app`, `android_launch_app`,
118
+ `android_terminate_app`, `android_open_url`, `android_screenshot`,
119
+ `android_logs`, `android_ui_status`, `android_ui_describe`, `android_ui_resolve`,
120
+ `android_ui_tap`, `android_ui_swipe`, `android_ui_type_text`,
121
+ `android_ui_keyevent` — identical names and arguments to the original MCP
122
+ server.
123
+
124
+ `android_screenshot` always writes the PNG to disk and additionally submits it
125
+ to the host attachment store for a canonical image block. When the current model
126
+ does not declare image input, or the deployment stores no attachments, the
127
+ result degrades to text plus the file path rather than failing the call.
128
+
129
+ ## Skill and command
130
+
131
+ - The bundled `android-dev` skill is registered through `ctx.skills.register()`
132
+ with the skill directory as its `resourceBase`, so `INSTALL_ENVIRONMENT.md`
133
+ keeps resolving. Its text is rewritten for DSH: no `mcp__…` prefixes,
134
+ environment setup points at the settings page.
135
+ - `/android-dev [goal]` runs the diagnostic and reports the verdict, failures
136
+ and fixes. DSH user commands deliberately create no model message, so the
137
+ command does the one useful thing that needs no model.
138
+
139
+ ## Differences from the original
140
+
141
+ | | Original (ZCode) | This port (DSH) |
142
+ |---|---|---|
143
+ | Tool exposure | stdio MCP server, `mcp__android_emulator__*` | native `ctx.tools.register()`, `android_*` |
144
+ | Configuration | `plugin.json` `userConfig` via `ANDROID_PLUGIN_*` | settings page + volatile `Config`; env vars still fall back |
145
+ | Working directory | `process.cwd()` | `exec.agent.session.header.cwd` (session workspace) |
146
+ | Data directory | `ANDROID_PLUGIN_DATA` / temp | `$DSH_HOME/android-emulator-helper`, configurable |
147
+ | Screenshot | MCP image content (base64) | host attachment reference; text fallback |
148
+ | Slash command | injects a model prompt | runs the diagnostic directly |
149
+ | Process execution | direct `node:child_process` | unchanged |
150
+
151
+ The direct `spawn` is a deliberate trade-off: `gradlew`, `adb` and `emulator`
152
+ behave byte-for-byte as they did in the original, and `adb exec-out` needs no
153
+ binary-stream adapter. The cost is bypassing DSH's subprocess seam — no managed
154
+ process ranges, env scrubbing, output spill or teardown termination. Every call
155
+ here has an explicit timeout and `SIGTERM`, and the plugin reads and forwards no
156
+ credential-bearing environment variables.
157
+
158
+ ## Architecture
159
+
160
+ ```
161
+ dsh-android-emulator-helper/
162
+ ├── package.json # dsh.bundle.patch, dsh.client, peer dependencies (the compatibility gate)
163
+ ├── cordis.patch.yml # - insert: [{ id: android-emulator-helper, name: '@zhz8888/dsh-android-emulator-helper' }]
164
+ ├── icon.svg # plugin icon: the Android robot head, brand green, readable in both themes
165
+ ├── locale/en.json # plugin-page name and description (English; its presence is what enables every other language)
166
+ ├── locale/zh.json # the Simplified Chinese pair
167
+ ├── lib/
168
+ │ ├── index.js # host half: tools, skill, command and the two Fetch routes
169
+ │ ├── client.js # browser half: a hand-written __ModuleLoader__ bundle, the settings page
170
+ │ ├── settings.js # field declarations, the schemastery Config (every field .volatile()), live reads
171
+ │ ├── runtime.js # one runtime per call: session cwd + live settings → data directory
172
+ │ ├── tools/index.js # the 23 defineTool definitions, argument/result schemas, image admission
173
+ │ ├── providers/ # ported from the original plugin file by file, now taking an explicit runtime
174
+ │ └── util/ # process execution, paths, content blocks
175
+ ├── skills/android-dev/ # SKILL.md + INSTALL_ENVIRONMENT.md
176
+ ├── scripts/ # check-bundle.mjs (consistency gate), smoke.mjs (real-machine), verify-mount.mjs
177
+ └── test/ # node:test unit and integration suites
178
+ ```
179
+
180
+ Three constraints shape everything above:
181
+
182
+ 1. **The settings namespace has exactly one source of truth**: the profile patch
183
+ row id. The `id` in `cordis.patch.yml`, `SETTINGS_NAMESPACE` in
184
+ `lib/settings.js` and the literal in `lib/client.js` must agree;
185
+ `scripts/check-bundle.mjs` verifies them, and also that the host and client
186
+ field tables have not drifted apart.
187
+ 2. **A browser bundle cannot import a host module** — the client purity gate
188
+ forbids cross-plugin value imports — so the field table is written twice and
189
+ held together by that gate. The bundle may `require` only the shell's nine
190
+ static modules (React, Cordis, `dsh-client-store`, `dsh-client-ui-slots`,
191
+ `dsh-client-ui-primitives`, `dsh-client-ui-dockkit`, …).
192
+ 3. **The plugin-page name is a package resource, not a manifest field**: DSH
193
+ resolves `<package>/locale/en.json` through the `exports` map, so
194
+ `./locale/*.json` has to stay exported and `locale` has to stay in `files`;
195
+ either omission degrades silently to showing the raw package name. `en.json`
196
+ is also the switch that enables every other language file, so it cannot be
197
+ dropped in favour of `zh.json`. `test/manifest.test.js` pins all three
198
+ conditions.
199
+
200
+ ## Development
201
+
202
+ ```bash
203
+ pnpm install # the @deepseek-ai/* peers plus react/react-dom for the tests
204
+ pnpm test # 63 tests, no Android SDK needed
205
+ pnpm run check # tests + host/client gate + real-Cordis mount
206
+ pnpm run verify:mount # real Cordis Context and real ToolRuntime
207
+ pnpm run smoke # read-only real-machine probes
208
+ pnpm run smoke:emulator # also boot an AVD, screenshot, logs, stop
209
+ pnpm run link:dsh-deps # offline dev: symlink a local DSH's @deepseek-ai packages
210
+ ```
211
+
212
+ The package manager is pnpm: `packageManager` pins `pnpm@11.8.0`, and
213
+ `pnpm-lock.yaml` is the only lockfile (npm's `package-lock.json` is gone).
214
+ [`pnpm-workspace.yaml`](pnpm-workspace.yaml) does not open a workspace — this is a
215
+ single-package repository — it only carries the install-script policy:
216
+ `strictDepBuilds: true`. pnpm v10+ skips a dependency's build scripts *silently*
217
+ by default, which is the wrong failure mode here, so the strict mode makes
218
+ `pnpm install` fail immediately with `ERR_PNPM_IGNORED_BUILDS` and forces an
219
+ explicit per-package decision. The current tree (27 packages) was scanned and
220
+ contains no install-time build scripts, so no `allowBuilds` entry is needed.
221
+
222
+ Four layers of verification:
223
+
224
+ | Layer | Command | What it proves |
225
+ |---|---|---|
226
+ | 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 |
227
+ | 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 the save control |
228
+ | Consistency gate | `scripts/check-bundle.mjs` | Bundle id equals the package name, namespace equals the patch row id, the two field tables match key-for-key, both routes agree on both sides, and the bundle is still a classic script |
229
+ | Real host mount | `scripts/verify-mount.mjs` | Mounted on a **real** Cordis `Context` with the **real** `ToolRuntime`: 23 tools registered, four read-only tools 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 |
230
+
231
+ ### Registry constraints that shape the tools
232
+
233
+ Two constraints come from the real tool registry and cannot be seen through a
234
+ hand-written test double:
235
+
236
+ - A result must match the tool's declared `output.schema`, so the two listing
237
+ tools return `{ devices: [...] }` and `{ avds: [...] }` rather than a bare
238
+ array, which the registry rejects as invalid output.
239
+ - A result must be lossless JSON, and the registry's snapshot rejects
240
+ `undefined` outright. Every ported provider models an absent field as
241
+ `undefined` — a device without a `model`, a UI node without `bounds`, a
242
+ discovery report with no Gradle root — so `toLosslessJson()` normalizes the
243
+ value on the single path every tool result travels, instead of 23 providers
244
+ each handling it.
245
+
246
+ `docs/research/` documents the DSH plugin contracts this port depends on, with
247
+ line-level citations into the shipped packages. It is the closest thing to a
248
+ "DSH plugin development spec" that exists, because the application bundle ships
249
+ no developer documentation.
250
+
251
+ ## Known limitations
252
+
253
+ - Linux is unsupported, exactly as in the original; `android_preflight` reports
254
+ it.
255
+ - `android_start_emulator` always starts a new emulator and never reuses one.
256
+ Pass `serial` to the other tools to reuse a target.
257
+ - `android_create_avd` always answers `no` to `avdmanager`'s confirmation, so it
258
+ can never silently trigger a multi-gigabyte image download.
259
+ - The settings page's installed-image list comes from scanning the SDK
260
+ directory, not from `sdkmanager --list_installed`, which fails in restricted
261
+ environments and prefixes its output with deprecation notices.
262
+ - **The settings page has not been visually confirmed in the real GUI**: the
263
+ delivery choice was source-only, so it was never installed into the `desktop`
264
+ profile. Its render layer is covered by the server-render test above, but
265
+ spacing, dark theme and narrow-width wrapping should be checked after
266
+ installing.
267
+ - `@deepseek-ai/dsh-client-ui-primitives` publishes only `0.0.1-rc.1` on npm,
268
+ which does not match the runtime API, so the render test uses a faithful double
269
+ for the primitives while React and `react-dom/server` stay real.
270
+
271
+ ## License
272
+
273
+ [MIT](LICENSE), copyright (c) 2026 zhz8888.
274
+
275
+ Tools and provider logic are ported from the ZCode official `android-emulator`
276
+ plugin (also MIT, copyright Z.ai); the Kotlin/Compose project template files are
277
+ kept verbatim. See the note at the end of [LICENSE](LICENSE) for the full
278
+ provenance.
@@ -0,0 +1,222 @@
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
+ ### 1. 先安装依赖
27
+
28
+ 包本身只发布源码与手写的客户端 bundle,运行依赖由 DSH profile 提供。
29
+
30
+ ```bash
31
+ cd /path/to/dsh-android-emulator-helper
32
+ pnpm install
33
+ ```
34
+
35
+ 本插件 import 的官方包 `@deepseek-ai/cordis`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/schemastery` **全部声明在 `peerDependencies`**,由宿主提供:profile 里本来就带着它们,解析到宿主那一份还能保证设置表单背后只有一个 `schemastery` 实例。上面的 `pnpm install` 只为开发与测试把它们装到本仓库。
36
+
37
+ DSH 的兼容性闸门正是按 `peerDependencies` 里 `@deepseek-ai/dsh*` 的版本区间与运行时版本比对,不匹配会明确拒装。区间因此**只写下界**(`>=0.2.0-rc.2`):写上界会在 DSH 每次升版时逼着插件重发一次放宽范围,而 `^` 区间会静默漏掉下一个版本元组的预发布版。
38
+
39
+ ### 2. 装进 profile
40
+
41
+ **DSH Desktop(本机 GUI)**:`desktop` profile 由 Electron 应用独占管理,`dsh plugin --profile desktop` 会被 CLI 主动拒绝。请通过应用内的插件管理器安装:把本目录加入本机连接器/插件源,或使用插件管理器的「安装」入口指向本地目录。
42
+
43
+ **CLI / 其他 profile**:
44
+
45
+ ```bash
46
+ dsh plugin --profile <profile> add /absolute/path/to/dsh-android-emulator-helper
47
+ ```
48
+
49
+ `dsh.bundle.patch` 已声明,因此安装后 DSH 的 reconcile 会自动把本包追加到 `dsh.profile.bundles`,profile 启动时合并 `cordis.patch.yml`,挂载唯一一行 host 入口。**不要**再手动往 profile 自己的 `cordis.patch.yml` 里加同一行,重复挂载会让同名工具二次注册并在加载期报错。
50
+
51
+ ### 3. 打开设置页
52
+
53
+ 重启/热加载后,打开 **设置 → Android 模拟器**。页面在 host 半侧真正提供了该设置命名空间时才会出现;反之不会留下任何痕迹。
54
+
55
+ ## 设置页面
56
+
57
+ 设置命名空间 = profile patch 行 id = `android-emulator-helper`(同时是 `dsh.name`)。页面通过 `ctx.configForms` 读写,因此具备暂存编辑、脏值检测、内联校验、乐观并发(revision)与「已覆盖/恢复默认」语义。
58
+
59
+ | 分组 | 设置项 | 类型 | 默认值 | 说明 |
60
+ |---|---|---|---|---|
61
+ | SDK 与工具链 | `sdkPath` | 文本 | 空 | Android SDK 根目录;留空时探测 `ANDROID_HOME`、`ANDROID_SDK_ROOT` 与常见路径 |
62
+ | | `jdkMajor` | 文本(建议来自本机 JDK) | `17` | JDK 主版本,用于预检与安装指引 |
63
+ | | `buildToolsVersion` | 文本(建议来自已装 build-tools) | `35.0.0` | 安装指引引用的 build-tools 版本 |
64
+ | 模拟器与设备 | `defaultAvd` | 文本(建议来自本机 AVD) | `medium_phone` | 需要启动模拟器时优先使用 |
65
+ | | `defaultSerial` | 文本(建议来自已连接设备) | 空 | 优先使用的设备序列号 |
66
+ | | `autoStartEmulator` | 开关 | 开 | 关闭后无可用设备时直接报错,不启动模拟器 |
67
+ | | `emulatorArgs` | 多行文本 | 空 | 额外 `emulator` 参数,每行一个,`#` 为注释 |
68
+ | | `bootTimeoutMs` | 整数 | `180000` | 模拟器启动超时 |
69
+ | API 与系统镜像 | `apiLevel` | 文本(建议来自已装 platform/镜像) | `35` | `compileSdk` 与系统镜像包名使用的 API 级别 |
70
+ | | `systemImageVariant` | 下拉 | `default` | `default` / `google_apis` / `google_apis_playstore` |
71
+ | | `systemImageAbi` | 下拉 | 自动 | 留空按宿主架构,Apple Silicon 用 `arm64-v8a` |
72
+ | | `minSdk` | 整数 | `23` | `android_create_app` 生成项目的默认 minSdk |
73
+ | 构建 | `buildTimeoutMs` | 整数 | `600000` | 单次 assemble 的最长运行时间 |
74
+ | | `gradleExtraArgs` | 多行文本 | 空 | 额外 Gradle 参数,每行一个 |
75
+ | 输出目录与日志 | `dataDir` | 文本 | 空 | 截图与构建日志根目录;留空用 `$DSH_HOME/android-emulator-helper` |
76
+ | | `screenshotDir` | 文本 | 空 | 截图目录;留空用 `<数据目录>/screenshots` |
77
+ | | `logTailLines` | 整数 | `500` | `android_logs` 未指定行数时的默认行数 |
78
+
79
+ 页面底部两个按钮调用 host 半侧的两个精确 Fetch 路由:
80
+
81
+ - **检测环境** → `GET /api/android-emulator-helper/status`,返回与 `android_preflight` 相同的检查结果,逐条显示状态、详情与修复建议。
82
+ - **刷新设备与 AVD** → `GET /api/android-emulator-helper/inventory`,探测 SDK 根目录、AVD、已连接设备、已安装 JDK、已装系统镜像/platform/build-tools,用于填充上方字段的建议列表。
83
+
84
+ ### 兼容旧配置
85
+
86
+ 原插件的 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 环境无需改动即可继续工作。
87
+
88
+ ## 工具清单
89
+
90
+ 23 个工具,名称与参数与原插件一致。
91
+
92
+ | 工具 | 作用 |
93
+ |---|---|
94
+ | `android_preflight` | 检查 SDK、adb、emulator、AVD、Java、Gradle 项目状态与 UI 自动化后端,并回显当前设置 |
95
+ | `android_discover_project` | 发现 Gradle 根、模块、variant、applicationId、manifest 与 APK 产物 |
96
+ | `android_create_app` | 生成最小 Kotlin + Jetpack Compose 工程 |
97
+ | `android_build_app` | 构建指定模块/variant |
98
+ | `android_build_and_run` | 构建 → 复用或启动设备 → 安装 → 启动 |
99
+ | `android_list_devices` | 列出 adb 可见设备 |
100
+ | `android_list_avds` | 列出 AVD |
101
+ | `android_start_emulator` | 启动新的 GUI 模拟器(从不复用已有设备) |
102
+ | `android_stop_emulator` | 按序列号停止模拟器 |
103
+ | `android_create_avd` | 用 `avdmanager` 创建 AVD |
104
+ | `android_install_app` | 安装 APK |
105
+ | `android_launch_app` | 按 applicationId 启动 |
106
+ | `android_terminate_app` | 强制停止应用 |
107
+ | `android_open_url` | 在设备上打开 URL |
108
+ | `android_screenshot` | 截图并落盘;模型支持图像输入时同时返回图像 |
109
+ | `android_logs` | 读取 logcat 尾部,可按 applicationId 过滤 |
110
+ | `android_ui_status` | 报告 UI 自动化后端可用性 |
111
+ | `android_ui_describe` | 返回 UI Automator 元素树 |
112
+ | `android_ui_resolve` | 按文本/描述/资源 id/类名解析到坐标 |
113
+ | `android_ui_tap` | 坐标点击 |
114
+ | `android_ui_swipe` | 坐标滑动 |
115
+ | `android_ui_type_text` | 向聚焦控件输入文本 |
116
+ | `android_ui_keyevent` | 发送 `BACK`/`HOME`/`ENTER`/`APP_SWITCH`/`MENU`/`SEARCH` |
117
+
118
+ `android_screenshot` 的行为值得单独说明:PNG 总会写到磁盘(默认在数据目录的 `screenshots/` 下),同时尝试把图像提交给宿主的附件服务以获得规范的图像块。当前模型未声明图像输入、或部署没有附件服务时,结果会退化为纯文本并附上文件路径,而不会让整次调用失败。
119
+
120
+ ## Skill 与斜杠命令
121
+
122
+ - **`android-dev` skill**:随包提供(`skills/android-dev/`),host 半侧通过 `ctx.skills.register()` 注册为嵌入式 skill,并把 skill 目录作为 `resourceBase`,因此 `INSTALL_ENVIRONMENT.md` 的相对引用照旧可用。内容已按 DSH 改写:不再有 `mcp__...` 前缀,环境配置指向设置页而非环境变量,并补充了设置页使用说明。
123
+ - **`/android-dev [目标]` 命令**:通过 `ctx.commands.register()` 注册。DSH 的用户命令刻意不产生模型消息,因此该命令做一件不依赖模型的事——跑一遍环境诊断,输出结论、失败项与修复建议,并给出下一步提示。
124
+
125
+ ## 与原插件的差异
126
+
127
+ | 方面 | 原插件(ZCode) | 本插件(DSH) |
128
+ |---|---|---|
129
+ | 工具暴露 | stdio MCP 服务器 `android-emulator`,模型看到 `mcp__android_emulator__*` | 原生 `ctx.tools.register()`,模型看到 `android_*` |
130
+ | 用户配置 | `plugin.json` 的 `userConfig`,经 `ANDROID_PLUGIN_*` 注入 | 设置页 + schemastery `Config`,字段全部 `.volatile()` 实时生效;环境变量仍作回落 |
131
+ | 工作目录 | `process.cwd()` | `exec.agent.session.header.cwd`(会话工作区) |
132
+ | 数据目录 | `ANDROID_PLUGIN_DATA` / 临时目录 | `$DSH_HOME/android-emulator-helper`,可配置 |
133
+ | 截图返回 | MCP image content(base64) | host 附件引用生成的图像块,失败时退化为文本 |
134
+ | 斜杠命令 | 注入一段提示词给模型 | 直接运行诊断并输出结论 |
135
+ | 进程调用 | `node:child_process` 直接 spawn | 保持一致 |
136
+
137
+ 保留直接 spawn 是有意的取舍:这样 `gradlew`、`adb`、`emulator` 的行为与原插件逐字节一致,且不必为 `adb exec-out` 的二进制流另写一层适配。代价是绕过了 DSH 的 subprocess seam——没有统一的过程管理、环境变量清洗、输出 spill 与卸载期终止。本插件的每个外部调用都有显式 timeout 与 `SIGTERM`,且不读写任何凭据环境变量。
138
+
139
+ ## 架构
140
+
141
+ ```
142
+ dsh-android-emulator-helper/
143
+ ├── package.json # dsh.bundle.patch、dsh.client、peer 依赖(兼容性闸门)
144
+ ├── cordis.patch.yml # - insert: [{ id: android-emulator-helper, name: '@zhz8888/dsh-android-emulator-helper' }]
145
+ ├── icon.svg # 插件图标:Android 机器人头部,品牌绿,明暗主题下都可辨认
146
+ ├── locale/en.json # 插件页的显示名与描述(英文;它的存在才是其它语言文件生效的开关)
147
+ ├── locale/zh.json # 简体中文那一份
148
+ ├── lib/
149
+ │ ├── index.js # host 半侧:注册工具 / skill / 命令 / 两个 Fetch 路由
150
+ │ ├── client.js # browser 半侧:__ModuleLoader__ 手写 bundle,设置页
151
+ │ ├── settings.js # 字段声明、schemastery Config(全部 .volatile())、实时读取
152
+ │ ├── runtime.js # 每次调用构造 runtime(会话 cwd + 实时设置 → 数据目录)
153
+ │ ├── tools/index.js # 23 个 defineTool 定义、参数/结果 schema、图像准入
154
+ │ ├── providers/ # 从原插件逐文件移植,改为显式接收 runtime
155
+ │ └── util/ # 进程执行、路径、内容块
156
+ ├── skills/android-dev/ # SKILL.md + INSTALL_ENVIRONMENT.md
157
+ ├── scripts/ # check-bundle.mjs(一致性闸门)、smoke.mjs(真机集成)
158
+ └── test/ # node:test 单元与集成测试
159
+ ```
160
+
161
+ 三个关键约束决定了上面的形状:
162
+
163
+ 1. **设置命名空间只有一个真相来源**:profile patch 行 id。`cordis.patch.yml` 的 `id`、`lib/settings.js` 的 `SETTINGS_NAMESPACE`、`lib/client.js` 的字面量必须一致;`scripts/check-bundle.mjs` 会校验它们,也会校验 host 与 client 两份字段表无漂移。
164
+ 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` 等)。
165
+ 3. **插件页的名字是包内资源,不是 manifest 字段**:DSH 通过 `exports` 解析 `<包名>/locale/en.json`,所以 `./locale/*.json` 必须保持导出、`locale` 必须留在 `files` 里;任一处缺失都会静默退化成显示原始包名。`en.json` 同时是其它语言文件的开关,因此不能只留 `zh.json`。这三条都由 `test/manifest.test.js` 钉住。
166
+
167
+ ## 开发与验证
168
+
169
+ ```bash
170
+ pnpm install # 装官方 peer 包与 react/react-dom(开发与测试用)
171
+ pnpm test # 63 个测试(无需 Android SDK)
172
+ pnpm run check # 测试 + host/client 一致性闸门 + 真实 Cordis 挂载验证
173
+ pnpm run verify:mount # 真实 Cordis Context + 真实 ToolRuntime 注册表
174
+ pnpm run smoke # 只读真机探测:preflight / AVD / 设备 / 项目发现 / inventory
175
+ pnpm run smoke:emulator # 额外:启动模拟器 → 截图 → 读日志 → 停止
176
+ pnpm run link:dsh-deps # 离线开发:把本机 DSH 的 @deepseek-ai 包软链进 node_modules
177
+ ```
178
+
179
+ ### 四层验证
180
+
181
+ | 层 | 命令 | 证明了什么 |
182
+ |---|---|---|
183
+ | 单元 / 解析器 | `pnpm test` | 客户端 bundle 加载契约与静态模块约束、manifest 与 patch 契约、设置投影(volatile 实时读取、环境变量回落、越界回落)、移植过来的解析器(`adb devices`、`emulator -list-avds`、UI Automator XML、`input text` 转义、APK 选择、JDK 主版本归一、无损 JSON 归一) |
184
+ | 设置页渲染 | `pnpm test`(`client-render.test.js`) | 用真实 React + `react-dom/server` 渲染整个设置页:分组/字段/字典键完整、受控输入带真实值、开关与下拉状态正确、「已覆盖/恢复默认」、两个按钮与保存按钮齐全、无未解析字典键 |
185
+ | 一致性闸门 | `scripts/check-bundle.mjs` | bundle id = 包名、命名空间 = patch 行 id、host 与 client 两份字段表逐键逐选项一致、两个路由两侧一致、bundle 仍是经典脚本 |
186
+ | 真实宿主挂载 | `scripts/verify-mount.mjs` | 在**真实** Cordis `Context` + **真实** `ToolRuntime` 上挂载:23 个工具注册成功、四个只读工具经注册表实际派发并通过参数/结果 schema 校验、错误参数被注册表拒绝、未知工具名不解析、17 个设置字段全部 volatile、dispose 后工具全部注销 |
187
+
188
+ `pnpm run smoke` 覆盖只有真机才能证明的部分:SDK 发现、`adb`/`emulator` 实际调用、环境拼装、设置投影真正喂给 provider。
189
+
190
+ ### 注册表给工具层带来的两条约束
191
+
192
+ 这两条约束来自真实工具注册表,手写的测试替身看不到:
193
+
194
+ - 结果必须匹配工具声明的 `output.schema`,因此两个列表工具返回
195
+ `{ devices: [...] }` 与 `{ avds: [...] }`,而不是裸数组——注册表会以
196
+ invalid output 拒绝裸数组。
197
+ - 结果必须是无损 JSON,注册表的快照会直接拒绝 `undefined`。而移植过来的
198
+ provider 一律用 `undefined` 表示缺失字段(设备没有 `model`、UI 节点没有
199
+ `bounds`、发现结果没有 Gradle 根),因此在所有工具结果唯一的必经路径上用
200
+ `toLosslessJson()` 统一归一,而不是在 23 个 provider 里各处理一遍。
201
+
202
+ `docs/research/` 记录了本次移植所依赖的 DSH 插件契约,全部带指向实际包源码的
203
+ 逐行引用;DSH 应用包不提供开发者文档,这是目前最接近「DSH 插件开发规范」的
204
+ 可核对材料。
205
+
206
+ ## 已知限制
207
+
208
+ - **Linux 不支持**,与原插件一致;`android_preflight` 会将其标记为不支持的宿主。
209
+ - `android_start_emulator` 永远启动新模拟器,从不复用已有目标(原插件语义)。复用请把 `serial` 传给其他工具。
210
+ - `android_create_avd` 对 `avdmanager` 的确认提示固定回答 `no`,绝不静默触发数 GB 的镜像下载;需要先自行 `sdkmanager --install`。
211
+ - UI 自动化依赖设备端 `uiautomator`;失败时 `android_ui_status` 会报告不可用,构建/运行/截图链路不受影响。
212
+ - 设置页的「已安装系统镜像」列表来自 SDK 目录扫描,而不是 `sdkmanager --list_installed`(后者在受限环境下会因创建 bin 目录失败,且输出含大量弃用提示)。
213
+ - **设置页尚未在真实 GUI 中人工目视确认**:交付方式选择了「只交付源码包」,未安装进 `desktop` profile。渲染层已由真实 React 的服务端渲染测试覆盖(见上表),但视觉细节(间距、暗色主题、窄屏换行)需要装上后自行核对。
214
+ - `@deepseek-ai/dsh-client-ui-primitives` 在 npm 上只有 `0.0.1-rc.1`,与运行时 API 不一致;因此设置页渲染测试对 primitives 使用了忠实替身,真实 React 与 `react-dom/server` 仍是真件。
215
+ - 插件不触碰凭据:不读取、不转发任何含 key/token/secret 的环境变量。
216
+
217
+ ## 许可
218
+
219
+ [MIT](LICENSE),版权所有 (c) 2026 zhz8888。
220
+
221
+ 工具与 provider 逻辑移植自 ZCode 官方 `android-emulator` 插件(同样以 MIT 分发,
222
+ 版权归 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>