@huanlin/dsh-plugin-android-use 0.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/README.md +144 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +326 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +2178 -0
- package/package.json +112 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://dshfind.com/zh/plugins/huanlinoto/dsh-plugin-android-use"><img src="https://dshfind.com/api/card/huanlinoto/dsh-plugin-android-use?lang=zh" alt="dsh-plugin-android-use card"></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# dsh-android-use
|
|
6
|
+
|
|
7
|
+
DSH 插件:让 AI 通过 adb 操作安卓手机。提供 10 个工具覆盖设备发现、屏幕感知、输入操作和应用管理,让模型能够自主操控 Android 设备。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
# 从本地 checkout 开发安装:
|
|
13
|
+
dsh plugin --profile web add link:D:\Projects\deepseek-harness\dsh-plugin-android-use
|
|
14
|
+
|
|
15
|
+
# 从 npm 安装(发布后):
|
|
16
|
+
dsh plugin --profile web add @huanlin/dsh-plugin-android-use
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
预构建策略:`lib/` 入库,无 `prepare` 脚本,npm 安装开箱即用,无需 `allowBuilds`。
|
|
20
|
+
|
|
21
|
+
**前提条件**:本机已安装 adb 并在 PATH 上(或通过 `adbPath` 配置指定路径),已通过 `adb connect <ip:port>` 或 USB 连接 Android 设备。
|
|
22
|
+
|
|
23
|
+
## 配置
|
|
24
|
+
|
|
25
|
+
在 DSH GUI 设置页或 `cordis.patch.yml` 中配置:
|
|
26
|
+
|
|
27
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
28
|
+
|------|------|--------|------|
|
|
29
|
+
| `adbPath` | string | `'adb'` | adb 可执行路径。默认从 PATH 查找。 |
|
|
30
|
+
| `defaultSerial` | string? | - | 默认设备 serial。省略则单设备自动选择;多设备时模型需传 `serial` 参数。 |
|
|
31
|
+
| `inputTextMode` | `'input' \| 'adbkeyboard'` | `'input'` | 文本输入模式。`input` 仅支持 ASCII(用 `adb shell input text`);`adbkeyboard` 支持 Unicode(需设备安装 ADBKeyboard IME)。 |
|
|
32
|
+
|
|
33
|
+
## 工具
|
|
34
|
+
|
|
35
|
+
### 设备发现
|
|
36
|
+
|
|
37
|
+
| 工具 | 参数 | 说明 |
|
|
38
|
+
|------|------|------|
|
|
39
|
+
| `android_list_devices` | 无 | 列出所有已连接的安卓设备(serial、state、product、model)。 |
|
|
40
|
+
| `android_device_info` | `serial?` | 获取设备详细信息:型号、品牌、Android 版本、SDK、屏幕分辨率、密度、屏幕状态。 |
|
|
41
|
+
|
|
42
|
+
### 屏幕感知
|
|
43
|
+
|
|
44
|
+
| 工具 | 参数 | 说明 |
|
|
45
|
+
|------|------|------|
|
|
46
|
+
| `android_screenshot` | `serial?` | 截屏并保存到 attachment store。仅当当前模型路由支持 image 输入时向模型发图;否则仅返回元数据(感知走 `android_ui_dump`)。 |
|
|
47
|
+
| `android_ui_dump` | `serial?` | 转储 accessibility 树(UI 层级),返回节点列表(文本、bounds、center 坐标、交互标志)。**主要屏幕感知方式**——DeepSeek 无视觉能力时靠它理解屏幕。 |
|
|
48
|
+
|
|
49
|
+
### 输入操作
|
|
50
|
+
|
|
51
|
+
| 工具 | 参数 | 说明 |
|
|
52
|
+
|------|------|------|
|
|
53
|
+
| `android_tap` | `x, y` (必填), `duration_ms?`, `times?` | 点击屏幕坐标。`duration_ms > 0` 为长按。 |
|
|
54
|
+
| `android_swipe` | `x1, y1, x2, y2` (必填), `duration_ms?` | 从一点滑动到另一点。 |
|
|
55
|
+
| `android_press_key` | `key` (必填, 命名或整数), `times?` | 按键。命名键:`home`/`back`/`app_switch`/`power`/`enter`/`volume_up` 等,或裸 keycode 整数。 |
|
|
56
|
+
| `android_input_text` | `text` (必填), `submit?` | 在聚焦输入框中输入文本。`submit: true` 输入后按回车。 |
|
|
57
|
+
|
|
58
|
+
### 应用管理
|
|
59
|
+
|
|
60
|
+
| 工具 | 参数 | 说明 |
|
|
61
|
+
|------|------|------|
|
|
62
|
+
| `android_open_app` | `package` (必填), `activity?` | 打开应用。有 activity 用 `am start`;无 activity 用 `monkey` 启动默认 Activity。 |
|
|
63
|
+
| `android_foreground_app` | `serial?` | 获取当前前台应用和 Activity(通过 `dumpsys window`)。 |
|
|
64
|
+
|
|
65
|
+
## 开发
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
pnpm install # 安装开发依赖(schemastery、typescript、vitest、tsdown)
|
|
69
|
+
pnpm run typecheck # tsc --noEmit 类型检查
|
|
70
|
+
pnpm test # vitest run 单元测试(132 用例)
|
|
71
|
+
pnpm run build # tsc + tsdown → lib/(index.js + client.js 双产物)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
类型检查需要 junction 到本地 DSH 源码树(私有 peer 依赖不在 npm 上;junction 必须在 `pnpm install` 之后创建):
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
# 在 node_modules/@deepseek-ai/ 下创建 junction(指向本地 DSH 源码 checkout)
|
|
78
|
+
$nm = "node_modules/@deepseek-ai"
|
|
79
|
+
$src = "C:\Users\Administrator\.dsh\source\current"
|
|
80
|
+
@{
|
|
81
|
+
"cordis" = "$src\vendor\cordis"
|
|
82
|
+
"dsh-tools" = "$src\packages\core\tools"
|
|
83
|
+
"dsh-attachment" = "$src\packages\attachment\attachment"
|
|
84
|
+
"dsh-llm" = "$src\packages\llm\llm"
|
|
85
|
+
"dsh-client-ui-slots" = "$src\packages\client\ui-slots"
|
|
86
|
+
"dsh-client-ui-tool" = "$src\packages\client\ui-tool"
|
|
87
|
+
"dsh-client-ui-session" = "$src\packages\client\ui-session"
|
|
88
|
+
"dsh-client-ui-conversation" = "$src\packages\client\ui-conversation"
|
|
89
|
+
"dsh-client-ui-renderer" = "$src\packages\client\ui-renderer"
|
|
90
|
+
}.GetEnumerator() | ForEach-Object {
|
|
91
|
+
New-Item -ItemType Junction -Path (Join-Path $nm $_.Key) -Target $_.Value | Out-Null
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 检查
|
|
96
|
+
|
|
97
|
+
合规自检(参见 `plugin-development-guide.md` §10):
|
|
98
|
+
|
|
99
|
+
- [x] **零源码 patch**:未修改 DSH checkout 任何文件
|
|
100
|
+
- [x] B1: `package.json` 声明 `dsh.bundle.patch`
|
|
101
|
+
- [x] B2: 插件自带 `cordis.patch.yml`(insert 行 id/name/config 齐全)
|
|
102
|
+
- [x] B3: patch 行 `name` 用包名(Loader 从 profile node_modules 解析)
|
|
103
|
+
- [x] F1: `files` 含 `lib/` + `cordis.patch.yml`
|
|
104
|
+
- [x] F2: `peerDependencies` 含 `@deepseek-ai/cordis` + 用到的 `@deepseek-ai/*`(不用 devDependencies 冒充)
|
|
105
|
+
- [x] F3: typecheck/test/build script 齐全
|
|
106
|
+
- [x] A6: 不导出 default
|
|
107
|
+
- [x] C4: 工具返回规范 JSON 值 + render 投影分离
|
|
108
|
+
- [x] C5: adb 缺失/设备离线等基础设施失败 throw;取消不 throw
|
|
109
|
+
- [x] C6: 尊重 `exec.signal` 取消在途 adb 子进程
|
|
110
|
+
- [x] G: Unit 测试(`tests/*.spec.ts`,132 用例,含真实 launcher dump fixture)
|
|
111
|
+
|
|
112
|
+
## 目录结构
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
dsh-plugin-android-use/
|
|
116
|
+
├── src/
|
|
117
|
+
│ ├── index.ts # 入口:name、inject、Config(Schemastery)、apply
|
|
118
|
+
│ ├── adb.ts # AdbClient: spawn 封装 + serial 解析 + 设备列表解析
|
|
119
|
+
│ ├── xml.ts # uiautomator XML 解析(零依赖)
|
|
120
|
+
│ ├── keys.ts # 命名按键表(home/back/app_switch/... → keycode)
|
|
121
|
+
│ ├── registry.ts # registerTools(ctx, deps) — 依赖注入入口
|
|
122
|
+
│ ├── client/ # 浏览器半:tool.call.toolview 卡片(tap / screenshot)
|
|
123
|
+
│ │ ├── index.ts # client 插件:slots.inject 注册两张卡片
|
|
124
|
+
│ │ ├── TapCard.tsx # android_tap 卡片(前后截图对照)
|
|
125
|
+
│ │ └── ScreenshotCard.tsx # android_screenshot 卡片
|
|
126
|
+
│ └── tools/
|
|
127
|
+
│ ├── device.ts # list_devices / device_info
|
|
128
|
+
│ ├── screen.ts # screenshot / ui_dump
|
|
129
|
+
│ ├── input.ts # tap / swipe / press_key / input_text
|
|
130
|
+
│ └── apps.ts # open_app / foreground_app
|
|
131
|
+
├── tests/
|
|
132
|
+
│ ├── adb.spec.ts # 设备列表解析、serial 解析序
|
|
133
|
+
│ ├── xml.spec.ts # XML 解析(真实 launcher dump fixture)
|
|
134
|
+
│ ├── keys.spec.ts # 命名按键 → keycode 映射
|
|
135
|
+
│ ├── input.spec.ts # 文本转义、非 ASCII 报错、adbkeyboard 编码
|
|
136
|
+
│ ├── tools.spec.ts # fake AdbClient + 全工具 schema/execute/render 验证
|
|
137
|
+
│ └── fixtures/
|
|
138
|
+
│ └── launcher_dump.xml # 真实 PJF110 设备 launcher dump
|
|
139
|
+
├── cordis.patch.yml # bundle 层:插入 dsh-android-use 插件行
|
|
140
|
+
├── package.json # dsh.bundle.patch 声明 + peerDeps
|
|
141
|
+
├── tsconfig.json # NodeNext、ES2022、strict
|
|
142
|
+
├── tsdown.config.ts # 双 entry:index.js(node)+ client.js(DSSH 模块加载器包裹)
|
|
143
|
+
└── vitest.config.ts # vitest forks pool
|
|
144
|
+
```
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# dsh-android-use bundle layer: inserts the dsh-android-use plugin row.
|
|
2
|
+
# Loaded by `dsh plugin --profile <name> add @huanlin/dsh-plugin-android-use`.
|
|
3
|
+
# The `name` is the package entry; Loader resolves it from profile node_modules.
|
|
4
|
+
# Defaults: adb from PATH, input mode 'input' (ASCII), no default serial.
|
|
5
|
+
- insert:
|
|
6
|
+
- id: dsh-android-use
|
|
7
|
+
name: '@huanlin/dsh-plugin-android-use'
|
|
8
|
+
config:
|
|
9
|
+
adbPath: adb
|
|
10
|
+
inputTextMode: input
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@huanlin/dsh-plugin-android-use",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region \0dsh-css:D:\Projects\deepseek-harness\dsh-plugin-android-use\src\client\TapCard.module.css.mjs
|
|
10
|
+
const css$1 = "/* TapCard toolview: a compact card showing the annotated pre-tap screenshot\n * with tap coordinates and action metadata.\n *\n * Every color resolves through a --dsw-alias-* token (no literal colors). */\n\n.d8shy4r_card {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* ---- Header: title + action badge ---- */\n\n.d8shy4r_header {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.d8shy4r_stateDot {\n flex: none;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--dsw-alias-label-caption);\n transition: background 0.15s ease;\n}\n\n.d8shy4r_card[data-state='running'] .d8shy4r_stateDot {\n background: var(--dsw-alias-brand-primary);\n animation: dsh-android-tap-pulse 1.4s ease-in-out infinite;\n}\n\n.d8shy4r_card[data-state='completed'] .d8shy4r_stateDot {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.d8shy4r_card[data-state='error'] .d8shy4r_stateDot {\n background: var(--dsw-alias-state-error-primary);\n}\n\n@keyframes dsh-android-tap-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n.d8shy4r_title {\n flex: 1 1 auto;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.d8shy4r_badge {\n flex: none;\n font-size: 11px;\n line-height: 18px;\n padding: 0 6px;\n border-radius: 4px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n/* ---- Screenshot images (pre-tap + post-tap grid) ---- */\n\n.d8shy4r_imageGrid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n}\n\n.d8shy4r_imageSlot {\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n\n.d8shy4r_imageLabel {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n font-weight: 500;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.d8shy4r_imageWrap {\n position: relative;\n border-radius: 6px;\n overflow: hidden;\n background: var(--dsw-alias-bg-layer-2);\n max-height: 280px;\n display: flex;\n justify-content: center;\n}\n\n.d8shy4r_image {\n display: block;\n max-width: 100%;\n max-height: 280px;\n object-fit: contain;\n}\n\n.d8shy4r_placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 80px;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Footer: coordinates + metadata ---- */\n\n.d8shy4r_meta {\n display: flex;\n flex-wrap: wrap;\n gap: 8px 16px;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n.d8shy4r_metaItem {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .d8shy4r_card[data-state='running'] .d8shy4r_stateDot {\n animation: none;\n }\n}\n";
|
|
11
|
+
const classMap$1 = {
|
|
12
|
+
"card": "d8shy4r_card",
|
|
13
|
+
"header": "d8shy4r_header",
|
|
14
|
+
"stateDot": "d8shy4r_stateDot",
|
|
15
|
+
"title": "d8shy4r_title",
|
|
16
|
+
"badge": "d8shy4r_badge",
|
|
17
|
+
"imageGrid": "d8shy4r_imageGrid",
|
|
18
|
+
"imageSlot": "d8shy4r_imageSlot",
|
|
19
|
+
"imageLabel": "d8shy4r_imageLabel",
|
|
20
|
+
"imageWrap": "d8shy4r_imageWrap",
|
|
21
|
+
"image": "d8shy4r_image",
|
|
22
|
+
"placeholder": "d8shy4r_placeholder",
|
|
23
|
+
"meta": "d8shy4r_meta",
|
|
24
|
+
"metaItem": "d8shy4r_metaItem"
|
|
25
|
+
};
|
|
26
|
+
const tagId$1 = "@huanlin/dsh-plugin-android-use/TapCard.module.css";
|
|
27
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
28
|
+
const tag = document.createElement("style");
|
|
29
|
+
tag.dataset.plugin = "@huanlin/dsh-plugin-android-use";
|
|
30
|
+
tag.dataset.pluginCss = tagId$1;
|
|
31
|
+
tag.textContent = css$1;
|
|
32
|
+
document.head.appendChild(tag);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/client/TapCard.tsx
|
|
36
|
+
/**
|
|
37
|
+
* TapCard — the toolcall card for the `android_tap` tool.
|
|
38
|
+
*
|
|
39
|
+
* Renders both the pre-tap screenshot (annotated with the tap marker) and
|
|
40
|
+
* the post-tap screenshot (showing the result after tap) inline, along with
|
|
41
|
+
* coordinates and action metadata. Images are loaded through the
|
|
42
|
+
* `uiConversation.imageUrl` service, which converts the ImageAttachmentRef
|
|
43
|
+
* into a session-authorized blob URL.
|
|
44
|
+
*
|
|
45
|
+
* @module @huanlin/dsh-plugin-android-use/client/TapCard
|
|
46
|
+
*/
|
|
47
|
+
/** Extract all image blocks from a settled tool result, in order. */
|
|
48
|
+
function findImages(block) {
|
|
49
|
+
return block.content.filter((c) => c.type === "image");
|
|
50
|
+
}
|
|
51
|
+
/** Extract the first text block (summary) from a settled tool result. */
|
|
52
|
+
function findText$1(block) {
|
|
53
|
+
return block.content.find((c) => c.type === "text")?.text;
|
|
54
|
+
}
|
|
55
|
+
/** Parse tap coordinates from the running block's argsRaw. */
|
|
56
|
+
function parseTapArgs(argsRaw) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(argsRaw);
|
|
59
|
+
return {
|
|
60
|
+
x: typeof parsed.x === "number" ? parsed.x : void 0,
|
|
61
|
+
y: typeof parsed.y === "number" ? parsed.y : void 0
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve an attachmentId to a browser URL through the uiConversation service.
|
|
69
|
+
* Caches by attachmentId so re-renders don't re-fetch.
|
|
70
|
+
*/
|
|
71
|
+
function useImageUrl$1(uiConversation, sessionId, attachment) {
|
|
72
|
+
const [url, setUrl] = (0, react.useState)(void 0);
|
|
73
|
+
(0, react.useEffect)(() => {
|
|
74
|
+
if (attachment === void 0 || sessionId === void 0) {
|
|
75
|
+
setUrl(void 0);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let cancelled = false;
|
|
79
|
+
uiConversation.imageUrl(sessionId, attachment).then((resolved) => {
|
|
80
|
+
if (!cancelled) setUrl(resolved);
|
|
81
|
+
}).catch(() => {
|
|
82
|
+
if (!cancelled) setUrl(void 0);
|
|
83
|
+
});
|
|
84
|
+
return () => {
|
|
85
|
+
cancelled = true;
|
|
86
|
+
};
|
|
87
|
+
}, [
|
|
88
|
+
uiConversation,
|
|
89
|
+
sessionId,
|
|
90
|
+
attachment
|
|
91
|
+
]);
|
|
92
|
+
return url;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Render one `android_tap` tool call as a card with pre-tap and post-tap screenshots.
|
|
96
|
+
*/
|
|
97
|
+
function TapCard({ block, toolName, sessionId, uiConversation }) {
|
|
98
|
+
const settled = "kind" in block ? block : void 0;
|
|
99
|
+
const running = "kind" in block ? void 0 : block;
|
|
100
|
+
const images = settled !== void 0 ? findImages(settled) : [];
|
|
101
|
+
const text = settled !== void 0 ? findText$1(settled) : void 0;
|
|
102
|
+
const runArgs = running !== void 0 ? parseTapArgs(running.argsRaw) : void 0;
|
|
103
|
+
const settledArgs = settled?.call != null ? parseTapArgs(settled.call.argsRaw) : void 0;
|
|
104
|
+
const coords = runArgs ?? settledArgs ?? {
|
|
105
|
+
x: void 0,
|
|
106
|
+
y: void 0
|
|
107
|
+
};
|
|
108
|
+
const preImage = images[0];
|
|
109
|
+
const postImage = images[1];
|
|
110
|
+
const preUrl = useImageUrl$1(uiConversation, sessionId, preImage?.attachment);
|
|
111
|
+
const postUrl = useImageUrl$1(uiConversation, sessionId, postImage?.attachment);
|
|
112
|
+
const state = settled === void 0 ? "running" : settled.isError ? "error" : "completed";
|
|
113
|
+
const title = coords.x !== void 0 && coords.y !== void 0 ? `Tap (${coords.x}, ${coords.y})` : toolName;
|
|
114
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
115
|
+
className: classMap$1.card,
|
|
116
|
+
"data-tool": toolName,
|
|
117
|
+
"data-state": state,
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
120
|
+
className: classMap$1.header,
|
|
121
|
+
children: [
|
|
122
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
123
|
+
className: classMap$1.stateDot,
|
|
124
|
+
"aria-hidden": true
|
|
125
|
+
}),
|
|
126
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
127
|
+
className: classMap$1.title,
|
|
128
|
+
children: title
|
|
129
|
+
}),
|
|
130
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
131
|
+
className: classMap$1.badge,
|
|
132
|
+
children: state
|
|
133
|
+
})
|
|
134
|
+
]
|
|
135
|
+
}),
|
|
136
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
137
|
+
className: classMap$1.imageGrid,
|
|
138
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
139
|
+
className: classMap$1.imageSlot,
|
|
140
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
141
|
+
className: classMap$1.imageLabel,
|
|
142
|
+
children: "Pre-tap (annotated)"
|
|
143
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
144
|
+
className: classMap$1.imageWrap,
|
|
145
|
+
children: preUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
146
|
+
className: classMap$1.image,
|
|
147
|
+
src: preUrl,
|
|
148
|
+
alt: `Pre-tap screenshot at (${coords.x ?? "?"}, ${coords.y ?? "?"})`
|
|
149
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
150
|
+
className: classMap$1.placeholder,
|
|
151
|
+
children: settled === void 0 ? "Capturing…" : preImage !== void 0 ? "Loading…" : "No screenshot"
|
|
152
|
+
})
|
|
153
|
+
})]
|
|
154
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
155
|
+
className: classMap$1.imageSlot,
|
|
156
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
157
|
+
className: classMap$1.imageLabel,
|
|
158
|
+
children: "Post-tap (result)"
|
|
159
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
160
|
+
className: classMap$1.imageWrap,
|
|
161
|
+
children: postUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
162
|
+
className: classMap$1.image,
|
|
163
|
+
src: postUrl,
|
|
164
|
+
alt: "Post-tap screenshot showing the result"
|
|
165
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
166
|
+
className: classMap$1.placeholder,
|
|
167
|
+
children: settled === void 0 ? "Capturing…" : postImage !== void 0 ? "Loading…" : "No screenshot"
|
|
168
|
+
})
|
|
169
|
+
})]
|
|
170
|
+
})]
|
|
171
|
+
}),
|
|
172
|
+
text !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
173
|
+
className: classMap$1.meta,
|
|
174
|
+
children: text.split("\n").map((line, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
175
|
+
className: classMap$1.metaItem,
|
|
176
|
+
children: line
|
|
177
|
+
}, i))
|
|
178
|
+
})
|
|
179
|
+
]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region \0dsh-css:D:\Projects\deepseek-harness\dsh-plugin-android-use\src\client\ScreenshotCard.module.css.mjs
|
|
184
|
+
const css = "/* ScreenshotCard toolview: a compact card showing the captured screenshot.\n *\n * Every color resolves through a --dsw-alias-* token (no literal colors). */\n\n.d8godkw_card {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* ---- Header: title + state badge ---- */\n\n.d8godkw_header {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.d8godkw_stateDot {\n flex: none;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--dsw-alias-label-caption);\n transition: background 0.15s ease;\n}\n\n.d8godkw_card[data-state='running'] .d8godkw_stateDot {\n background: var(--dsw-alias-brand-primary);\n animation: dsh-android-screenshot-pulse 1.4s ease-in-out infinite;\n}\n\n.d8godkw_card[data-state='completed'] .d8godkw_stateDot {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.d8godkw_card[data-state='error'] .d8godkw_stateDot {\n background: var(--dsw-alias-state-error-primary);\n}\n\n@keyframes dsh-android-screenshot-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n.d8godkw_title {\n flex: 1 1 auto;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.d8godkw_badge {\n flex: none;\n font-size: 11px;\n line-height: 18px;\n padding: 0 6px;\n border-radius: 4px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n/* ---- Screenshot image ---- */\n\n.d8godkw_imageWrap {\n position: relative;\n border-radius: 6px;\n overflow: hidden;\n background: var(--dsw-alias-bg-layer-2);\n max-height: 320px;\n display: flex;\n justify-content: center;\n}\n\n.d8godkw_image {\n display: block;\n max-width: 100%;\n max-height: 320px;\n object-fit: contain;\n}\n\n.d8godkw_placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 80px;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Footer: metadata ---- */\n\n.d8godkw_meta {\n display: flex;\n flex-wrap: wrap;\n gap: 8px 16px;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n.d8godkw_metaItem {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .d8godkw_card[data-state='running'] .d8godkw_stateDot {\n animation: none;\n }\n}\n";
|
|
185
|
+
const classMap = {
|
|
186
|
+
"card": "d8godkw_card",
|
|
187
|
+
"header": "d8godkw_header",
|
|
188
|
+
"stateDot": "d8godkw_stateDot",
|
|
189
|
+
"title": "d8godkw_title",
|
|
190
|
+
"badge": "d8godkw_badge",
|
|
191
|
+
"imageWrap": "d8godkw_imageWrap",
|
|
192
|
+
"image": "d8godkw_image",
|
|
193
|
+
"placeholder": "d8godkw_placeholder",
|
|
194
|
+
"meta": "d8godkw_meta",
|
|
195
|
+
"metaItem": "d8godkw_metaItem"
|
|
196
|
+
};
|
|
197
|
+
const tagId = "@huanlin/dsh-plugin-android-use/ScreenshotCard.module.css";
|
|
198
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
199
|
+
const tag = document.createElement("style");
|
|
200
|
+
tag.dataset.plugin = "@huanlin/dsh-plugin-android-use";
|
|
201
|
+
tag.dataset.pluginCss = tagId;
|
|
202
|
+
tag.textContent = css;
|
|
203
|
+
document.head.appendChild(tag);
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/client/ScreenshotCard.tsx
|
|
207
|
+
/**
|
|
208
|
+
* ScreenshotCard — the toolcall card for the `android_screenshot` tool.
|
|
209
|
+
*
|
|
210
|
+
* Renders the captured screenshot inline. The image is loaded through
|
|
211
|
+
* the `uiConversation.imageUrl` service, which converts the ImageAttachmentRef
|
|
212
|
+
* into a session-authorized blob URL.
|
|
213
|
+
*
|
|
214
|
+
* @module @huanlin/dsh-plugin-android-use/client/ScreenshotCard
|
|
215
|
+
*/
|
|
216
|
+
function findImage(block) {
|
|
217
|
+
return block.content.find((c) => c.type === "image");
|
|
218
|
+
}
|
|
219
|
+
function findText(block) {
|
|
220
|
+
return block.content.find((c) => c.type === "text")?.text;
|
|
221
|
+
}
|
|
222
|
+
function useImageUrl(uiConversation, sessionId, attachment) {
|
|
223
|
+
const [url, setUrl] = (0, react.useState)(void 0);
|
|
224
|
+
(0, react.useEffect)(() => {
|
|
225
|
+
if (attachment === void 0 || sessionId === void 0) {
|
|
226
|
+
setUrl(void 0);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let cancelled = false;
|
|
230
|
+
uiConversation.imageUrl(sessionId, attachment).then((resolved) => {
|
|
231
|
+
if (!cancelled) setUrl(resolved);
|
|
232
|
+
}).catch(() => {
|
|
233
|
+
if (!cancelled) setUrl(void 0);
|
|
234
|
+
});
|
|
235
|
+
return () => {
|
|
236
|
+
cancelled = true;
|
|
237
|
+
};
|
|
238
|
+
}, [
|
|
239
|
+
uiConversation,
|
|
240
|
+
sessionId,
|
|
241
|
+
attachment
|
|
242
|
+
]);
|
|
243
|
+
return url;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Render one `android_screenshot` tool call as a card with the screenshot.
|
|
247
|
+
*/
|
|
248
|
+
function ScreenshotCard({ block, toolName, sessionId, uiConversation }) {
|
|
249
|
+
const settled = "kind" in block ? block : void 0;
|
|
250
|
+
const image = settled !== void 0 ? findImage(settled) : void 0;
|
|
251
|
+
const text = settled !== void 0 ? findText(settled) : void 0;
|
|
252
|
+
const imageUrl = useImageUrl(uiConversation, sessionId, image?.attachment);
|
|
253
|
+
const state = settled === void 0 ? "running" : settled.isError ? "error" : "completed";
|
|
254
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
255
|
+
className: classMap.card,
|
|
256
|
+
"data-tool": toolName,
|
|
257
|
+
"data-state": state,
|
|
258
|
+
children: [
|
|
259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
260
|
+
className: classMap.header,
|
|
261
|
+
children: [
|
|
262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
263
|
+
className: classMap.stateDot,
|
|
264
|
+
"aria-hidden": true
|
|
265
|
+
}),
|
|
266
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
267
|
+
className: classMap.title,
|
|
268
|
+
children: toolName
|
|
269
|
+
}),
|
|
270
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
271
|
+
className: classMap.badge,
|
|
272
|
+
children: state
|
|
273
|
+
})
|
|
274
|
+
]
|
|
275
|
+
}),
|
|
276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
277
|
+
className: classMap.imageWrap,
|
|
278
|
+
children: imageUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
279
|
+
className: classMap.image,
|
|
280
|
+
src: imageUrl,
|
|
281
|
+
alt: "Android screenshot"
|
|
282
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
283
|
+
className: classMap.placeholder,
|
|
284
|
+
children: settled === void 0 ? "Capturing…" : image !== void 0 ? "Loading…" : "No screenshot"
|
|
285
|
+
})
|
|
286
|
+
}),
|
|
287
|
+
text !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
288
|
+
className: classMap.meta,
|
|
289
|
+
children: text.split("\n").map((line, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
290
|
+
className: classMap.metaItem,
|
|
291
|
+
children: line
|
|
292
|
+
}, i))
|
|
293
|
+
})
|
|
294
|
+
]
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/client/index.ts
|
|
299
|
+
/** Required services: slot registry + Conversation image cache. */
|
|
300
|
+
const inject = ["slots", "uiConversation"];
|
|
301
|
+
/**
|
|
302
|
+
* Client plugin body: register the `android_tap` and `android_screenshot` toolview slots.
|
|
303
|
+
* @param ctx - client root context.
|
|
304
|
+
*/
|
|
305
|
+
function apply(ctx) {
|
|
306
|
+
const tapInjected = () => ({ uiConversation: ctx.uiConversation });
|
|
307
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
308
|
+
name: "tool.call.toolview",
|
|
309
|
+
key: "android_tap",
|
|
310
|
+
inject: tapInjected
|
|
311
|
+
}, TapCard));
|
|
312
|
+
const screenshotInjected = () => ({ uiConversation: ctx.uiConversation });
|
|
313
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
314
|
+
name: "tool.call.toolview",
|
|
315
|
+
key: "android_screenshot",
|
|
316
|
+
inject: screenshotInjected
|
|
317
|
+
}, ScreenshotCard));
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
exports.apply = apply;
|
|
321
|
+
exports.inject = inject;
|
|
322
|
+
return module.exports;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["findText","useImageUrl","useState","css","useState","css"],"sources":["../src/client/TapCard.tsx","../src/client/ScreenshotCard.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * TapCard — the toolcall card for the `android_tap` tool.\n *\n * Renders both the pre-tap screenshot (annotated with the tap marker) and\n * the post-tap screenshot (showing the result after tap) inline, along with\n * coordinates and action metadata. Images are loaded through the\n * `uiConversation.imageUrl` service, which converts the ImageAttachmentRef\n * into a session-authorized blob URL.\n *\n * @module @huanlin/dsh-plugin-android-use/client/TapCard\n */\n\nimport { useEffect, useState } from 'react'\nimport type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'\nimport type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ToolResultNode, UiConversation } from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type { ContentBlock, ImageBlock } from '@deepseek-ai/dsh-llm'\nimport type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'\nimport css from './TapCard.module.css'\n\n/** Inject face: the Conversation image cache for durable URL resolution. */\nexport type TapCardInjected = {\n uiConversation: UiConversation\n}\n\ntype TapCardProps = ToolCallViewProps & InjectFace<TapCardInjected>\n\n/** Extract all image blocks from a settled tool result, in order. */\nfunction findImages(block: ToolResultNode): ImageBlock[] {\n return block.content.filter((c): c is ImageBlock => c.type === 'image')\n}\n\n/** Extract the first text block (summary) from a settled tool result. */\nfunction findText(block: ToolResultNode): string | undefined {\n const text = block.content.find((c): c is Extract<ContentBlock, { type: 'text' }> => c.type === 'text')\n return text?.text\n}\n\n/** Parse tap coordinates from the running block's argsRaw. */\nfunction parseTapArgs(argsRaw: string): { x?: number; y?: number } {\n try {\n const parsed = JSON.parse(argsRaw) as { x?: unknown; y?: unknown }\n return {\n x: typeof parsed.x === 'number' ? parsed.x : undefined,\n y: typeof parsed.y === 'number' ? parsed.y : undefined,\n }\n } catch {\n return {}\n }\n}\n\n/**\n * Resolve an attachmentId to a browser URL through the uiConversation service.\n * Caches by attachmentId so re-renders don't re-fetch.\n */\nfunction useImageUrl(\n uiConversation: UiConversation,\n sessionId: TapCardProps['sessionId'] | undefined,\n attachment: ImageAttachmentRef | undefined,\n): string | undefined {\n const [url, setUrl] = useState<string | undefined>(undefined)\n useEffect(() => {\n if (attachment === undefined || sessionId === undefined) {\n setUrl(undefined)\n return\n }\n let cancelled = false\n uiConversation.imageUrl(sessionId, attachment)\n .then((resolved) => { if (!cancelled) setUrl(resolved) })\n .catch(() => { if (!cancelled) setUrl(undefined) })\n return () => { cancelled = true }\n }, [uiConversation, sessionId, attachment])\n return url\n}\n\n/**\n * Render one `android_tap` tool call as a card with pre-tap and post-tap screenshots.\n */\nexport function TapCard({ block, toolName, sessionId, uiConversation }: TapCardProps) {\n const settled = 'kind' in block ? block : undefined\n const running = 'kind' in block ? undefined : block\n\n const images = settled !== undefined ? findImages(settled) : []\n const text = settled !== undefined ? findText(settled) : undefined\n const runArgs = running !== undefined ? parseTapArgs(running.argsRaw) : undefined\n const settledArgs = settled?.call != null ? parseTapArgs(settled.call.argsRaw) : undefined\n const coords = runArgs ?? settledArgs ?? { x: undefined, y: undefined }\n\n const preImage = images[0]\n const postImage = images[1]\n const preUrl = useImageUrl(uiConversation, sessionId, preImage?.attachment)\n const postUrl = useImageUrl(uiConversation, sessionId, postImage?.attachment)\n\n const state = settled === undefined ? 'running' : (settled.isError ? 'error' : 'completed')\n\n const title = coords.x !== undefined && coords.y !== undefined\n ? `Tap (${coords.x}, ${coords.y})`\n : toolName\n\n return (\n <div className={css.card} data-tool={toolName} data-state={state}>\n <div className={css.header}>\n <span className={css.stateDot} aria-hidden />\n <span className={css.title}>{title}</span>\n <span className={css.badge}>{state}</span>\n </div>\n <div className={css.imageGrid}>\n <div className={css.imageSlot}>\n <span className={css.imageLabel}>Pre-tap (annotated)</span>\n <div className={css.imageWrap}>\n {preUrl !== undefined ? (\n <img className={css.image} src={preUrl} alt={`Pre-tap screenshot at (${coords.x ?? '?'}, ${coords.y ?? '?'})`} />\n ) : (\n <span className={css.placeholder}>\n {settled === undefined ? 'Capturing…' : preImage !== undefined ? 'Loading…' : 'No screenshot'}\n </span>\n )}\n </div>\n </div>\n <div className={css.imageSlot}>\n <span className={css.imageLabel}>Post-tap (result)</span>\n <div className={css.imageWrap}>\n {postUrl !== undefined ? (\n <img className={css.image} src={postUrl} alt=\"Post-tap screenshot showing the result\" />\n ) : (\n <span className={css.placeholder}>\n {settled === undefined ? 'Capturing…' : postImage !== undefined ? 'Loading…' : 'No screenshot'}\n </span>\n )}\n </div>\n </div>\n </div>\n {text !== undefined && (\n <div className={css.meta}>\n {text.split('\\n').map((line, i) => (\n <span key={i} className={css.metaItem}>{line}</span>\n ))}\n </div>\n )}\n </div>\n )\n}\n","/**\n * ScreenshotCard — the toolcall card for the `android_screenshot` tool.\n *\n * Renders the captured screenshot inline. The image is loaded through\n * the `uiConversation.imageUrl` service, which converts the ImageAttachmentRef\n * into a session-authorized blob URL.\n *\n * @module @huanlin/dsh-plugin-android-use/client/ScreenshotCard\n */\n\nimport { useEffect, useState } from 'react'\nimport type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'\nimport type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ToolResultNode, UiConversation } from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type { ContentBlock, ImageBlock } from '@deepseek-ai/dsh-llm'\nimport type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'\nimport css from './ScreenshotCard.module.css'\n\n/** Inject face: the Conversation image cache for durable URL resolution. */\nexport type ScreenshotCardInjected = {\n uiConversation: UiConversation\n}\n\ntype ScreenshotCardProps = ToolCallViewProps & InjectFace<ScreenshotCardInjected>\n\nfunction findImage(block: ToolResultNode): ImageBlock | undefined {\n return block.content.find((c): c is ImageBlock => c.type === 'image')\n}\n\nfunction findText(block: ToolResultNode): string | undefined {\n const text = block.content.find((c): c is Extract<ContentBlock, { type: 'text' }> => c.type === 'text')\n return text?.text\n}\n\nfunction useImageUrl(\n uiConversation: UiConversation,\n sessionId: ScreenshotCardProps['sessionId'] | undefined,\n attachment: ImageAttachmentRef | undefined,\n): string | undefined {\n const [url, setUrl] = useState<string | undefined>(undefined)\n useEffect(() => {\n if (attachment === undefined || sessionId === undefined) {\n setUrl(undefined)\n return\n }\n let cancelled = false\n uiConversation.imageUrl(sessionId, attachment)\n .then((resolved) => { if (!cancelled) setUrl(resolved) })\n .catch(() => { if (!cancelled) setUrl(undefined) })\n return () => { cancelled = true }\n }, [uiConversation, sessionId, attachment])\n return url\n}\n\n/**\n * Render one `android_screenshot` tool call as a card with the screenshot.\n */\nexport function ScreenshotCard({ block, toolName, sessionId, uiConversation }: ScreenshotCardProps) {\n const settled = 'kind' in block ? block : undefined\n\n const image = settled !== undefined ? findImage(settled) : undefined\n const text = settled !== undefined ? findText(settled) : undefined\n const imageUrl = useImageUrl(uiConversation, sessionId, image?.attachment)\n\n const state = settled === undefined ? 'running' : (settled.isError ? 'error' : 'completed')\n\n return (\n <div className={css.card} data-tool={toolName} data-state={state}>\n <div className={css.header}>\n <span className={css.stateDot} aria-hidden />\n <span className={css.title}>{toolName}</span>\n <span className={css.badge}>{state}</span>\n </div>\n <div className={css.imageWrap}>\n {imageUrl !== undefined ? (\n <img className={css.image} src={imageUrl} alt=\"Android screenshot\" />\n ) : (\n <span className={css.placeholder}>\n {settled === undefined ? 'Capturing…' : image !== undefined ? 'Loading…' : 'No screenshot'}\n </span>\n )}\n </div>\n {text !== undefined && (\n <div className={css.meta}>\n {text.split('\\n').map((line, i) => (\n <span key={i} className={css.metaItem}>{line}</span>\n ))}\n </div>\n )}\n </div>\n )\n}\n","/**\n * dsh-plugin-android-use — browser half.\n *\n * Single bundle, dual entry: this is the client half (exports `./client`).\n * Host half ships via `.` (see `src/index.ts`).\n *\n * Registers two `tool.call.toolview` keyed slots:\n * - key `android_tap` → TapCard (annotated pre-tap screenshot)\n * - key `android_screenshot` → ScreenshotCard (captured screenshot)\n *\n * `tool.call.toolview` is declared by `@deepseek-ai/dsh-client-ui-tool`, so\n * both registrations go through `ctx.slots.inject` (waits on the declaration,\n * leaves with this plugin's fiber). Durable image URLs resolve through the\n * `uiConversation` service (v0.1.2: `conversation.resolveImage` is gone).\n *\n * @module @huanlin/dsh-plugin-android-use/client\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only imports: pull the Context/SlotMap/StandardProps declaration\n// merges this plugin's compiled props depend on. Erased in the bundle.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-tool/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-session/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { TapCard, type TapCardInjected } from './TapCard.tsx'\nimport { ScreenshotCard, type ScreenshotCardInjected } from './ScreenshotCard.tsx'\n\n/** Required services: slot registry + Conversation image cache. */\nexport const inject = ['slots', 'uiConversation']\n\n/**\n * Client plugin body: register the `android_tap` and `android_screenshot` toolview slots.\n * @param ctx - client root context.\n */\nexport function apply(ctx: Context): void {\n const tapInjected = (): TapCardInjected => ({ uiConversation: ctx.uiConversation })\n ctx.slots.inject('tool.call.toolview', () =>\n ctx.slots.register({\n name: 'tool.call.toolview',\n key: 'android_tap',\n inject: tapInjected,\n }, TapCard))\n\n const screenshotInjected = (): ScreenshotCardInjected => ({ uiConversation: ctx.uiConversation })\n ctx.slots.inject('tool.call.toolview', () =>\n ctx.slots.register({\n name: 'tool.call.toolview',\n key: 'android_screenshot',\n inject: screenshotInjected,\n }, ScreenshotCard))\n}\n"],"mappings":"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"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import z from "schemastery";
|
|
2
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
//#region src/registry.d.ts
|
|
4
|
+
/** Validated plugin config consumed by the tools at execution time. */
|
|
5
|
+
interface ResolvedConfig {
|
|
6
|
+
defaultSerial: string | undefined;
|
|
7
|
+
inputTextMode: 'input' | 'adbkeyboard';
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/index.d.ts
|
|
11
|
+
declare const name = "dsh-android-use";
|
|
12
|
+
declare const inject: string[];
|
|
13
|
+
interface Config {
|
|
14
|
+
adbPath?: string;
|
|
15
|
+
defaultSerial?: string;
|
|
16
|
+
inputTextMode?: 'input' | 'adbkeyboard';
|
|
17
|
+
}
|
|
18
|
+
declare const Config: z<Config>;
|
|
19
|
+
declare function resolveConfig(config: Config): {
|
|
20
|
+
adbPath: string;
|
|
21
|
+
} & ResolvedConfig;
|
|
22
|
+
declare function apply(ctx: Context, config?: Config): void;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { Config, apply, inject, name, resolveConfig };
|