dsh-plugin-android-emulator
把 ZCode 官方内置插件 android-emulator(MIT,© Z.ai)移植到 DeepSeek Harness。
提供 23 个 Android 原生工具:Gradle 构建、模拟器/真机生命周期、APK 安装与启动、截图、日志、ADB 与 UI Automator 自动化;并附带 android-dev 技能、/android 斜杠命令与原生设置面板。
本插件是集成层:不修改上游一行代码。上游产物以 MIT 许可逐字节 vendor 在
vendor/zcode-android-emulator/,来源、版本与逐文件 SHA-256 见 PROVENANCE.md。
1. 移植可行性结论
可以移植,而且属于「低风险高保真」移植。
原因:ZCode 的插件模型与 DSH 在关键处天然对齐 —— 上游插件本身就是一个
自包含的 stdio MCP 服务器(esbuild 打包,只 import node:* 内置模块),
而 DSH 有官方 MCP 桥接器 @deepseek-ai/dsh-mcp-client。两者对接后:
- 工具名完全一致:上游
.mcp.json里服务器名是android-emulator,ZCode 会规范化成android_emulator,模型看到mcp__android_emulator__<tool>;本插件直接把serverName设为android_emulator,产出的工具名与 ZCode 逐字节相同, 因此上游技能文本无需改写工具名。 - 上游的 23 个工具、12 项 preflight 诊断、Gradle/ADB/UI Automator 全部逻辑 零改动复用,只是换了个宿主。
映射表
| ZCode 机制 | DSH 对应实现 |
|---|---|
.mcp.json 里的 ${ZCODE_PLUGIN_ROOT} |
本包内 vendor/zcode-android-emulator/ 路径 |
${ZCODE_PROJECT_DIR}(子进程 cwd) |
projectDir 设置,默认 DSH 进程工作目录 |
${ZCODE_PLUGIN_DATA} |
<DSH_HOME>/android-emulator |
userConfig.*(7 项) |
ctx.settings.register('android-emulator', ...) 原生设置面板 |
skills/android-dev/SKILL.md |
ctx.skills.registerProvider()(source: bundled, rank: 600) |
commands/android-dev.md → /android-dev <goal> |
DSH 原生技能手势:输入框打 /android-dev <goal>,技能正文作为 instructions 注入、你的话作为任务(已实测,见 §7) |
| (本移植新增) | ctx.tools.register() → android_set_project_dir:让模型自己把 MCP 工程根目录对齐到当前会话(ZCode 靠动态 cwd 自动完成,DSH 的 cwd 是静态的,必须补这一环) |
| (本移植新增) | ctx.commands.register() → /android:给人用的状态查看 / 切换工程根目录 |
| 插件被 ZCode 发现并注入系统提示 | ctx.systemPrompt.section() 注入工作流段落 |
| 官方 MCP 服务器托管 | @deepseek-ai/dsh-mcp-client stdio 传输 + 自动重连 |
ZCode 的
/android-dev命令在 DSH 里不需要用ctx.commands复刻:dsh-commands的 handler 只能返回给用户看的文本,没有「把任务派给模型」的通道。而 DSH 的技能手势 (/(^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/)对userInvocable技能做的事, 恰好等价于 ZCode 命令的skills:frontmatter +$ARGUMENTS:技能正文进 instructions, 用户原话作为任务。所以这条是宿主原生覆盖,不是缺口。
与 ZCode 的差异
| 项 | ZCode | 本移植 |
|---|---|---|
| 工具名 | mcp__android_emulator__* |
完全相同 |
| 工具超时 | 60 s | 默认 900 s(Gradle 首次构建很慢) |
| 工程根目录 | 随会话工作区 | 进程级固定,用 /android dir <path> 切换(见「已知限制」) |
| 技能资源 | skills/android-dev/INSTALL_ENVIRONMENT.md |
同样随包提供,resourceBase 指向 assets/android-dev/ |
| 宿主运行时 | ZCode 内嵌 Node 24 | DSH 的 Electron-as-Node 24.18,自动加 ELECTRON_RUN_AS_NODE=1 |
2. 工具清单(23 个)
全部以 mcp__android_emulator__ 为前缀,例如 mcp__android_emulator__android_preflight。
| 分组 | 工具 |
|---|---|
| 诊断 | android_preflight |
| 工程 | android_discover_project、android_create_app、android_build_app、android_build_and_run |
| 设备 | android_list_devices、android_list_avds、android_start_emulator、android_stop_emulator、android_create_avd |
| 应用 | android_install_app、android_launch_app、android_terminate_app、android_open_url |
| 观测 | android_screenshot、android_logs |
| UI 自动化 | android_ui_status、android_ui_describe、android_ui_resolve、android_ui_tap、android_ui_swipe、android_ui_type_text、android_ui_keyevent |
android_preflight 会实际探测 12 项:Host OS、Android SDK root、插件默认值、adb、emulator、
sdkmanager、avdmanager、Java、Gradle、模拟器加速、AVD 列表、已连接设备。
3. 环境要求
- DSH ≥ 0.1.5-rc.1(本插件在 0.1.7-rc.2 上验证)
- Node ≥ 24(上游产物以
--target=node24构建)。DSH 桌面版自带 Node 24.18,无需额外安装; 若宿主 Node 低于 24,插件会自动优先使用 PATH 上满足要求的node。 - macOS 或 Windows(上游 P0 明确不支持 Linux,
android_preflight会如实报告) - Android SDK:
platform-tools(adb)、emulator、cmdline-tools(sdkmanager/avdmanager) - 模拟器工作流:至少一个 AVD;真机工作流:开启 USB 调试的设备
- 可选:
gradle在 PATH 上(用于给缺 wrapper 的工程生成 Gradle wrapper)
4. 安装
方式一:官方插件安装器
dsh plugin --profile desktop add dsh-plugin-android-emulator
方式二:本地开发安装(推荐先 dry-run)
cd dsh-plugin-android-emulator
# 1. 还原上游产物(首次 clone 后必须执行;从本机 ZCode 安装目录复制并校验哈希)
node tools/vendor-upstream.mjs
# 2. 构建宿主产物
npm run build
# 3. 看看将要做什么改动
node tools/install-into-profile.mjs --profile desktop --dry-run
# 4. 真正安装(自动备份 profile package.json,并用 dsh --dump-config 复核合成树)
node tools/install-into-profile.mjs --profile desktop
安装后下次启动 DSH 生效(正在运行的 Host 不会热加载 profile bundles)。
卸载:node tools/install-into-profile.mjs --profile desktop --uninstall。
方式三:只做一次试验(不改任何 profile)
dsh --profile desktop --patch ./cordis.patch.yml headless "调用 android_preflight 看看环境"
5. 设置项(DSH 设置页 →「Android 模拟器」)
| 设置 | 默认 | 说明 |
|---|---|---|
enabled |
true |
是否启用 |
projectDir |
'' |
MCP 工程根目录;留空 = DSH 进程工作目录 |
nodePath |
'' |
运行 MCP 服务器的 Node;留空自动选择 |
sdkPath |
'' |
Android SDK 根目录;留空自动探测 ANDROID_HOME / ANDROID_SDK_ROOT / 常见路径 |
defaultAvd |
medium_phone |
首选 AVD |
apiLevel |
35 |
建工程 / 装 SDK 包 / 选系统镜像用的 API level |
buildToolsVersion |
35.0.0 |
build-tools 版本(用于安装指引) |
systemImageVariant |
default |
default / google_apis / google_apis_playstore |
systemImageAbi |
'' |
留空时 ARM64 主机用 arm64-v8a,其余 x86_64 |
jdkMajor |
17 |
preflight 与安装指引使用的 JDK 主版本 |
toolCallTimeoutMs |
900000 |
单次工具调用超时(15 分钟) |
failOnStartupError |
false |
首次连接失败是否让插件加载失败 |
registerSkill |
true |
是否注册 android-dev 技能 |
promptSection |
true |
是否注入工作流系统提示段落 |
以上 7 项 Android 参数会以 ANDROID_PLUGIN_* 环境变量传给 MCP 服务器,
与上游 plugin.json 的 userConfig 一一对应。
6. 使用
斜杠命令(给人用)
/android # 查看状态:服务器路径、工程根目录、数据目录、Node 运行时、ANDROID_PLUGIN_* 环境
/android dir <path> # 切换工程根目录(自动重启 MCP 服务器,23 个工具重新注册)
原生工具 android_set_project_dir(给模型用)
ZCode 的工程根目录跟着会话自动变(ZCODE_PROJECT_DIR = context.workingDirectory,每次连接动态解析);
DSH 官方 MCP 桥接器的 cwd 只接受静态字符串,所以本移植补了一个原生工具让模型自己对齐:
android_set_project_dir # 不传参数 = 用当前会话的工作目录
android_set_project_dir { path } # 显式指定工程根目录
它做三件事:校验目录 → 按新根目录重建 MCP 桥接 → 在本地探测 settings.gradle(.kts) /
gradlew / gradle.properties 等标记并直接返回,所以模型一次调用就能确认对不对,
不用再多跑一次 android_discover_project。返回形如:
{
"ok": true, "changed": true,
"previousProjectDir": "...", "projectDir": "C:\\…\\<你的安卓工程>",
"source": "argument",
"looksLikeAndroidProject": true,
"markers": ["settings.gradle.kts", "gradlew", "gradlew.bat", "gradle.properties", "local.properties"]
}
系统提示词里也写了「若 android_discover_project 找不到 Gradle 根,就调 android_set_project_dir」,
所以这一步是自愈的,不需要人插手。isConcurrencySafe() 返回 false —— 重挂是进程级操作,不能并发。
技能
android-dev 技能会出现在会话技能目录里,模型可直接加载;其工作流为:
android_preflight → android_discover_project(或 android_create_app)→
android_build_and_run → android_screenshot → 按需 android_logs / UI 自动化。
在输入框直接打 /android-dev <你的目标> 即可启动开发循环 —— 这是 DSH 的原生技能手势,
等价于 ZCode 的 /android-dev 命令(技能正文进 instructions,你的话作为任务):
/android-dev 帮我做一个计数器 App,跑起来截图给我看
技能还随包提供上游的 INSTALL_ENVIRONMENT.md(macOS / Windows 的 JDK、Gradle、
cmdline-tools、SDK 包、AVD 创建、模拟器加速逐步指引)。
本机已有 DSH 自带的 Android 技能(
adaptive、agp-9-upgrade、edge-to-edge、navigation-3、camerax、testing-setup、r8-analyzer等)。那些覆盖怎么写, 本插件覆盖怎么跑起来并验证,两者互补。
7. 验证
npm run verify # 构建 + 全部测试
npm run vendor:check # 校验 vendor 目录与上游逐文件哈希一致
测试金字塔(45 个用例,全部通过):
| 层 | 文件 | 覆盖内容 |
|---|---|---|
| L1 契约 | test/contract.test.mjs |
files 含 cordis.patch.yml/vendor/assets;patch 只有一个 insert;无 export default;无运行时依赖;产物未内联 @deepseek-ai/* |
| L1 完整性 | test/vendor-integrity.test.mjs |
PROVENANCE 清单与 41 个 vendored 文件 SHA-256 逐一比对;bundle 只依赖 node:* |
| L2 生命周期 | test/mock-lifecycle.test.mjs |
用真实 schemastery 与真实 MCP 桥接模块 + 假 ctx,断言桥接配置、技能 provider、提示词段落、/android 行为、effect 作用域回收、缺服务降级 |
| L3 端到端 | test/mcp-server.test.mjs |
真实 spawn 上游 MCP 服务器:握手、23 个工具与入参 schema、真实 android_preflight 报告、ANDROID_PLUGIN_* 覆盖生效、cwd 决定工程根 |
本机实测记录(Windows + 完整 Android SDK)
① 用 DSH 自己的运行时(Electron 44 / Node 24.18.1,ELECTRON_RUN_AS_NODE=1)
拉起上游 MCP 服务器:
initialize: {"name":"android-emulator","version":"0.1.0"} protocol 2024-11-05
tools: 23
names: mcp__android_emulator__android_preflight, ... , mcp__android_emulator__android_build_app
preflight ok: false | checks: 12
PASS Host OS / Android SDK root / Android plugin defaults
PASS adb / emulator / sdkmanager / avdmanager / Java
PASS Emulator acceleration / Android Virtual Devices
FAIL Gradle <- 本机 PATH 无 gradle 且工程无 wrapper,符合预期
FAIL ADB devices <- 当前没有运行中的设备,符合预期
② 真实 DSH 宿主端到端验收(隔离 profile,dsh --profile android-e2e "…"):
1. Yes — `android-dev` is in my skill catalog.
2. 23 `mcp__android_emulator__` tools.
3. android_preflight: passed 11, failed 2 — failed checks: Gradle (not found), ADB devices (0 devices).
即:技能注册、23 个工具注册、真实工具调用在真实 DSH 宿主里全部跑通, preflight 的 11/2 结果与直接探测一致(两项 FAIL 是真实环境状态,不是移植缺陷)。
③ 原生技能手势等价于 ZCode 的 /android-dev 命令(同 profile,直接以 /android-dev 开头):
$ dsh --profile android-e2e "/android-dev 我想做一个计数器 App,先别动手,只告诉我:按照技能里的
默认工作流,你的第一步和第二步分别要调用哪个工具?"
按 `android-dev` 技能里的 Default Workflow,前两步是:
| 第 1 步 | mcp__android_emulator__android_preflight | 纯诊断…若环境缺失,先按 INSTALL_ENVIRONMENT.md 处理,
不能替你接受 SDK 许可、输密码、清模拟器数据或删 AVD |
| 第 2 步 | mcp__android_emulator__android_discover_project | …并读取 warnings(缺 gradle.properties /
local.properties / wrapper 要先修)|
补充:第 2 步里内含一个分支:如果发现没有 Android 工程,才在该步调用 android_create_app…
另外注意 path 解析:这两个工具的相对路径都相对插件的 projectDir(MCP project root),不是当前 shell 目录。
模型复述的「这两个工具的相对路径都相对插件的 projectDir(MCP project root),
不是当前 shell 目录」只存在于本移植改写后的技能正文里(上游 SKILL.md 里没有
projectDir 也没有 ambient shell directory 这句话)—— 证明注入的确实是这个技能。
/android-dev 手势完整可用,不需要再用 ctx.commands 复刻一条。
(注:同一次回答里提到的「不能替你接受 SDK 许可、清模拟器数据」是上游原文,
不能作为版本证据;projectDir 那句才是。)
④ 真实工程端到端(2026-09-25,本地一个真实工程
AiDocHelper v4.131 —— Kotlin + Compose + Room + OpenCV + MLKit,含 ABI splits):
| 步骤 | 结果 |
|---|---|
android_preflight |
ok=true,12 项检查,仅 ADB devices 未就绪(尚无设备) |
android_discover_project |
root=""(= cwd)、modules=[:app]、appIds=[com.aidoc.helper]、4 个 APK |
android_start_emulator |
复用既有 AVD dj,5.5s 启动,serial=emulator-5554 |
android_build_app |
:app:assembleDebug BUILD SUCCESSFUL in 22s(38 tasks up-to-date) |
android_install_app |
显式传 x86_64 APK,Success |
android_launch_app |
launched=true |
android_screenshot |
1080×2400 PNG,142,629 bytes,App 主界面渲染正常 |
android_logs |
logcat 拿到 libopencv_java4.so ... ok / Library opencv_java4 loaded |
android_stop_emulator |
stopped=true |
全程 85 秒(首次 12:23:54 → 12:25:19),未创建任何 AVD、未安装任何 SDK 包、
未修改工程任何源文件(git status 干净)。
两个实测细节值得记下:
- 截图要等冷启动。 首轮在
launch后 12 秒截图,拿到的是闪屏(39 KB); 该 App 冷启动要加载 OpenCV 原生库,45 秒后才是主界面(142 KB)。 - ARM 转译让错配 APK 也能装上,详见 §8.5 —— 预测的
INSTALL_FAILED_NO_MATCHING_ABIS没有发生,如实记录。
⑤ 在运行中的 DSH 会话里用插件自己的工具跑完整条链(2026-09-25 21:00,
projectDir 指向同一工程,全部通过 MCP 工具调用,无脚本):
| 工具 | 结果 |
|---|---|
android_discover_project |
root=""、modules=[:app]、appIds=[com.aidoc.helper]、4 APK、warnings=[] |
android_create_avd |
首次不传 device → 96M/320×640 废 AVD(见 §8.6);传 medium_phone 后 1536M/1080×2400 正常 |
android_start_emulator |
serial=emulator-5554 |
android_build_app |
BUILD SUCCESSFUL in 12s,38 tasks up-to-date;apkPath 又是 arm64(§8.5) |
android_install_app |
显式 x86_64 APK → Success |
android_launch_app |
launched=true,topResumedActivity=com.aidoc.helper/.MainActivity,crash buffer 为空 |
android_screenshot |
1080×2400 PNG,141,507 bytes,主界面正常 |
android_stop_emulator |
stopped=true |
清理核对:临时 AVD dsh_tmp_test 已删、AVD 目录回到空、无残留 emulator/qemu-system
进程、无 adb 设备、工程 git status 0 改动。
这轮真实宿主验收抓到了一个只有实机启动才会暴露的 bug:Cordis 对未在
inject声明的服务读取即抛错(cannot get property "skills" without inject), 所以ctx.skills?.xxx这种「防御式探测」根本防不住 ——?.之前就炸了。 正确做法是ctx.inject(['skills'], inner => …)(服务可用时才执行) 与ctx.get('skills')(不抛错的读取,仅用于诊断)。test/mock-lifecycle.test.mjs现在用「直接读服务就抛错」的假 ctx 复现该契约, 防止回归。
8. 已知限制
工程根目录是进程级的(已缓解)。 DSH 官方 MCP 桥接器的
cwd只接受静态字符串 (z.string(),传函数会校验失败),而 MCP 服务器进程长驻,所以process.cwd()在启动时就固定了。影响面仅限两个以 cwd 为基准的调用:android_discover_project(无参数)与android_create_app的相对dir; 其余工具都接受显式projectDir/ 路径参数。三条改法,按推荐顺序:
android_set_project_dir(模型自己调) —— 系统提示词已写明「找不到 Gradle 根就调它」, 所以是自愈的;不带参数即对齐当前会话工作目录。/android dir <path>—— 给人用的等价命令。projectDir设置 —— 想固定成一个路径时用。
三者都是「销毁并按新根目录重建桥接器」(约 1 秒,23 个工具重新注册), 不是真正的 per-session 路由:同一时刻只有一个工程根目录。 彻底解决:用
@deepseek-ai/dsh-scope的 per-agent scope 为每个会话懒启动一个 cwd 正确的子进程、按会话路由调用。代价是 23 个工具要自己注册与转发, 放弃官方桥接器 —— 在有真实多工作区并发需求之前不值得。重连预算耗尽后工具会消失。 这是官方桥接器的既定行为(连续 10 次失败后注销工具, 只能靠重载插件恢复)。排查顺序:
/android看 Node 运行时与路径 →android_preflight看环境。Linux 不受支持(上游 P0 决策),
android_preflight会报告为不支持的宿主。上游 TypeScript 源码未公开。 插件在 ZCode 仓库里以
apps/zcode-cli/packages/android-emulator-plugin出现在pnpm-lock.yaml, 但不在开源快照中,zai-org 名下也没有对应仓库。因此本插件 vendor 的是 ZCode 随包分发的编译产物 —— 它是唯一忠实的来源。dist/providers/*.js是未混淆的 esbuild 输出,随包保留以便审计。ABI 拆分工程:
android_build_app不按 ABI 选 APK。 上游pickApk()只按 variant 目录匹配,取第一个命中项。工程若开了splits.abi(例如同时出 arm64-v8a 与 x86_64),android_build_app/android_build_and_run会返回arm64 那个, 在 x86_64 模拟器上属于错配。实测结论(2026-09-25,Windows +
djAVD = android-36 google_apis x86_64):android_build_and_run确实选了app-arm64-v8a-debug.apk,但adb install仍返回Success—— 因为该 x86_64 镜像带 ARM 二进制转译,arm64 包能装也能跑, 只是走转译层。所以这不是一个必然失败,而是潜在脆弱点:换成不带转译的镜像、 或真机 arm64 配只出 x86_64 的包时,就会以INSTALL_FAILED_NO_MATCHING_ABIS失败。规避:显式调用
android_install_app并传正确的apkPath(用android_discover_project的apks列表挑选),不要依赖build_and_run的自动选择。android-dev技能里也写了这一条。android_create_avd不传device会建出废 AVD。 上游把device做成可选参数, 不传时avdmanager落到最小默认硬件档案 —— 实测hw.ramSize = 96M、hw.lcd 320×640@160dpi。这种 AVD 能启动、能出现在 adb 里,几秒后自己死掉, 症状很像「工具坏了」。必须显式传device(如medium_phone); 建完核对config.ini里hw.ramSize >= 1024M。 实测对比:不传 → 96M/320×640;传medium_phone→ 1536M/1080×2400/4 核,正常。死掉的模拟器进程仍占着 AVD 锁。 若
android_start_emulator返回了 serial 但设备随即消失,通常是上一个emulator.exe/qemu-system-*还挂着。上游的runningAvd()守卫是问 adb 的,而 adb 已经看不到这具尸体,于是它又启了第二个实例, 两个互踩。处理:杀掉残留的emulator.exe/qemu-system-*,adb kill-server重启 adb,再重试 —— 不要反复调android_start_emulator。
9. 来源与许可
本插件采用 MIT,并对上游做双重署名(代码注释 + 本文档)。
| 内容 | 来源 | 许可 |
|---|---|---|
vendor/zcode-android-emulator/** |
ZCode 官方 android-emulator@zcode-plugins-official v0.1.0,© Z.ai,逐字节复制 |
MIT(见其 package.json / .zcode-plugin/plugin.json) |
assets/android-dev/INSTALL_ENVIRONMENT.md |
同上,逐字节复制 | MIT |
assets/android-dev/SKILL.md |
上游技能文档的衍生作品:保留全部工作流,仅改写宿主集成章节 | MIT |
src/、build.mjs、test/、tools/ |
本移植新增 | MIT |
上游 dist/mcp/server.js 内嵌 @modelcontextprotocol/server 与 zod;上游以
legalComments: "none" 构建,bundle 内不留第三方声明,故在此说明。
ZCode 仓库本体为 Apache-2.0(LICENSE / NOTICE.md / THIRD-PARTY-NOTICES.md)。
vendor/ 里的原始副本永不被手工编辑 —— test/vendor-integrity.test.mjs 会校验哈希,
npm run vendor:check 可与上游目录直接比对。
10. 开发
node tools/vendor-upstream.mjs --from "<ZCode android-emulator-plugin 目录>" # 重新 vendor
node tools/vendor-upstream.mjs --check # 与上游比对
node tools/link-dev-deps.mjs # 从 DSH profile 链接 @deepseek-ai/*(测试用)
npm run build # esbuild → lib/index.js
npm test # 仅测试
npm run verify # 构建 + 测试
node_modules/、lib/ 均不入库;插件没有运行时依赖(@deepseek-ai/* 全部是
optional peer,由 DSH 宿主提供)。
No comments yet. Be the first to write one.