dsh-adb 0.1.5 → 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/README.md CHANGED
@@ -1,42 +1,50 @@
1
1
  # dsh-adb
2
2
 
3
- > ADB 设备·台架运维工具集 for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
3
+ > ADB device & bench operations for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
4
4
 
5
- DSH agent 直接操作 Android 设备 / 车机台架:设备发现、结构化 logcat、apk 安装、文件 pull/push、性能快照。面向实车与台架联调场景,业务内通用(不限 Unity、不限具体车机协议)。
5
+ Give DSH agents direct control over Android devices and automotive bench rigs: device discovery, structured logcat, APK install, file pull/push, and performance snapshots. Built for on-vehicle and bench debugging workflows — generic within the domain (no Unity, no vendor protocol lock-in).
6
6
 
7
- ## 安装
7
+ **English** | [简体中文](README.zh-CN.md)
8
+
9
+ ## Install
8
10
 
9
11
  ```sh
10
12
  dsh plugin --profile web add dsh-adb
11
13
  ```
12
14
 
13
- 或从 GitHub 直装:`dsh plugin --profile web add github:SamXiaBing/dsh-adb`
15
+ Or install directly from GitHub: `dsh plugin --profile web add github:SamXiaBing/dsh-adb`
16
+
17
+ ## Web device panel (v1.0.0)
14
18
 
15
- ## 生态收录
19
+ A "设备" tab in the conversation view ring (next to chat / trajectory / automation): device list with status, package-scoped performance snapshot (memory / frame stats / battery), and filtered logcat tail. Data flows over the package RPC channel; requires the plugin installed in a web profile and a GUI restart.
16
20
 
17
- - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb@0.1.0` 已发布(2026-08-14)
18
- - ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **已合并**
19
- - ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **已合并**
20
- - ⏳ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — 待合并
21
+ ## Ecosystem
21
22
 
22
- Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
23
+ - ✅ [npm](https://www.npmjs.com/package/dsh-adb) `dsh-adb` published (latest: 1.0.0)
24
+ - ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **merged**
25
+ - ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **merged**
26
+ - ✅ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — **merged**
23
27
 
24
- ## 工具
28
+ Topics: `dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
25
29
 
26
- | 工具 | 说明 |
30
+ ## Tools
31
+
32
+ | Tool | Description |
27
33
  | --- | --- |
28
- | `adb_devices` | 列出设备(serial/state/product/model),先发现再操作 |
29
- | `adb_connect` / `adb_disconnect` | 无线台架连接(host:port,默认 5555 |
30
- | `adb_logcat` | 过滤读取(tag/级别/关键字/时间窗/tail);`run_in_background` 后台连续采集,job_output 读增量、job_kill 停止 |
31
- | `adb_install` | 安装 apk(-r/-d/-g 选项),校验本地文件存在 |
32
- | `adb_file` | pull / push / ls / rm,设备隔离 |
33
- | `adb_perf_snapshot` | `dumpsys meminfo / gfxinfo / battery` 结构化快照(PSS/帧率百分位/卡顿率/电量) |
34
+ | `adb_devices` | List devices (serial/state/product/model); run first to discover serials |
35
+ | `adb_connect` / `adb_disconnect` | Wireless bench connection (host:port, default 5555) |
36
+ | `adb_logcat` | Filtered read (tag/level/keyword/time-window/tail); `run_in_background` streams continuously as a job — read deltas with `job_output`, stop with `job_kill` |
37
+ | `adb_install` | Install APKs (`-r`/`-d`/`-g` options); validates the local file exists |
38
+ | `adb_file` | pull / push / ls / rm, per-device isolation |
39
+ | `adb_perf_snapshot` | Structured `dumpsys meminfo / gfxinfo / battery` snapshots (PSS, frame percentiles, jank rate, battery) |
40
+ | `adb_perf_baseline` | Perf regression: save a snapshot as a baseline (label/tags), compare current state and get a numeric diff (PSS, janky %, percentiles), list/delete baselines (stored locally under `baselineDir`) |
41
+ | `adb_crash_report` | One-call crash scene: parsed logcat crash buffer + dropbox excerpt + process state + memory summary |
34
42
 
35
- 错误码:`ADB_NOT_FOUND`、`ADB_UNAVAILABLE`、`DEVICE_NOT_FOUND`、`NO_DEVICES`、`CONNECT_FAILED`、`INSTALL_FAILED`、`ADB_EXIT_<code>` 等,均为结构化 `AdbError`。
43
+ Errors are structured `AdbError` with stable codes: `ADB_NOT_FOUND`, `ADB_UNAVAILABLE`, `DEVICE_NOT_FOUND`, `NO_DEVICES`, `CONNECT_FAILED`, `INSTALL_FAILED`, `ADB_EXIT_<code>`, etc.
36
44
 
37
- ## 配置
45
+ ## Configuration
38
46
 
39
- `cordis.patch.yml` `config` 块(或 profile patch):
47
+ Set the `config` block in `cordis.patch.yml` (or a profile patch):
40
48
 
41
49
  ```yaml
42
50
  - id: dsh-adb
@@ -47,36 +55,35 @@ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
47
55
  timeoutMs: 30000
48
56
  ```
49
57
 
50
- | | 说明 | 默认 |
58
+ | Key | Description | Default |
51
59
  | --- | --- | --- |
52
- | `adbPath` | adb 可执行文件绝对路径 | 自动探测 PATH / ANDROID_HOME / ANDROID_SDK_ROOT/platform-tools |
53
- | `defaultSerial` | 默认设备 serial | |
54
- | `timeoutMs` | 命令超时 | 30000 |
60
+ | `adbPath` | Absolute path to the adb executable | Auto-detect PATH / ANDROID_HOME / ANDROID_SDK_ROOT/platform-tools |
61
+ | `defaultSerial` | Default target device serial | none |
62
+ | `timeoutMs` | Per-command timeout | 30000 |
63
+ | `baselineDir` | Directory for `adb_perf_baseline` storage | `~/.dsh/storages/dsh-adb` |
55
64
 
56
- ## 开发
65
+ ## Development
57
66
 
58
67
  ```sh
59
- npm install # 本机 NODE_ENV=production 时加 --include=dev
68
+ npm install # add --include=dev when NODE_ENV=production
60
69
  npm run build # tsc → lib/
61
- npm test # 解析器/错误分类单测(node --test
62
- npm pack --dry-run # 校验发布包内容(lib/ + cordis.patch.yml
70
+ npm test # parser/classification unit tests (node --test)
71
+ npm pack --dry-run # verify publish contents (lib/ + cordis.patch.yml)
63
72
  ```
64
73
 
65
- 注意:本机若设了 `NODE_ENV=production`,npm 会跳过 devDependencies,安装时用 `npm install --include=dev`。
66
-
67
- ## 测试与验证
74
+ ## Testing & Verification
68
75
 
69
- - 原则:**提交即测** —— 全部已提交功能均有实测覆盖(单元 + headless 端到端 + 车机台架/真机)。
70
- - 验证设备:Android 13 车机台架 + Android 13 真机。
71
- - 版本化变更与每版验证记录见 [CHANGELOG.md](CHANGELOG.md);测试方法与覆盖现状见 [docs/TESTING.md](docs/TESTING.md)
76
+ - Principle: **ship only what is tested** every committed feature has unit and/or end-to-end coverage.
77
+ - Verified on: Android 13 automotive bench + Android 13 phone.
78
+ - Per-version changes and verification: [CHANGELOG.md](CHANGELOG.md); test methodology and coverage: [docs/TESTING.md](docs/TESTING.md) (Chinese).
72
79
 
73
- ## 项目文档(供 AI 对话/协作者参考)
80
+ ## Project Docs (bilingual; for AI agents & contributors)
74
81
 
75
- - [docs/AGENTS.md](docs/AGENTS.md) — 进项目先读:定位、铁律、命令、环境事实、文档地图
76
- - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) — 目的/范围/非目标/验收标准
77
- - [docs/TESTING.md](docs/TESTING.md) — 测试哲学(提交即测)、三层测试方法、E2E 步骤、回归清单
78
- - [docs/DEVELOPMENT-LOG.md](docs/DEVELOPMENT-LOG.md) — 进度时间线、4 个已修复 bug 教训、环境/生态经验
79
- - [PLAN.md](PLAN.md) — 里程碑与待办
82
+ - [docs/AGENTS.md](docs/AGENTS.md) / [docs/AGENTS.en.md](docs/AGENTS.en.md) read first: purpose, rules, commands, environment facts, doc map
83
+ - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.en.md](docs/REQUIREMENTS.en.md) purpose / scope / non-goals / acceptance criteria
84
+ - [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.en.md](docs/TESTING.en.md) testing philosophy, three test layers, E2E steps, regression checklist
85
+ - [docs/DEVELOPMENT-LOG.md](docs/DEVELOPMENT-LOG.md) / [docs/DEVELOPMENT-LOG.en.md](docs/DEVELOPMENT-LOG.en.md) timeline, fixed-bug lessons, environment & ecosystem notes
86
+ - [PLAN.md](PLAN.md) / [PLAN.en.md](PLAN.en.md) milestones & backlog
80
87
 
81
88
  ## License
82
89
 
@@ -0,0 +1,92 @@
1
+ # dsh-adb
2
+
3
+ > ADB 设备·台架运维工具集 for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
4
+
5
+ [English](README.md) | 简体中文
6
+
7
+ 让 DSH agent 直接操作 Android 设备 / 车机台架:设备发现、结构化 logcat、apk 安装、文件 pull/push、性能快照。面向实车与台架联调场景,业务内通用(不限 Unity、不限具体车机协议)。
8
+
9
+ ## 安装
10
+
11
+ ```sh
12
+ dsh plugin --profile web add dsh-adb
13
+ ```
14
+
15
+ 或从 GitHub 直装:`dsh plugin --profile web add github:SamXiaBing/dsh-adb`
16
+
17
+ ## Web 设备面板(v1.0.0)
18
+
19
+ 会话视图页签「设备」(与 chat/轨迹/任务管理并列):设备列表/状态、按包名的性能快照(内存/帧率/电量)、过滤 logcat 尾部。数据走 Package RPC;需装入 web profile 并重启 GUI 生效。
20
+
21
+ ## 生态收录
22
+
23
+ - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 1.0.0)
24
+ - ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **已合并**
25
+ - ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **已合并**
26
+ - ✅ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — **已合并**
27
+
28
+ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
29
+
30
+ ## 工具
31
+
32
+ | 工具 | 说明 |
33
+ | --- | --- |
34
+ | `adb_devices` | 列出设备(serial/state/product/model),先发现再操作 |
35
+ | `adb_connect` / `adb_disconnect` | 无线台架连接(host:port,默认 5555) |
36
+ | `adb_logcat` | 过滤读取(tag/级别/关键字/时间窗/tail);`run_in_background` 后台连续采集,job_output 读增量、job_kill 停止 |
37
+ | `adb_install` | 安装 apk(-r/-d/-g 选项),校验本地文件存在 |
38
+ | `adb_file` | pull / push / ls / rm,设备隔离 |
39
+ | `adb_perf_snapshot` | `dumpsys meminfo / gfxinfo / battery` 结构化快照(PSS/帧率百分位/卡顿率/电量) |
40
+ | `adb_perf_baseline` | 性能回归:快照存基线(label/tags)、与当前状态数值对比(PSS/卡顿率/百分位)、list/delete(本地存储,`baselineDir`) |
41
+ | `adb_crash_report` | 崩溃现场一键采集:crash buffer 解析 + dropbox 摘录 + 进程状态 + 内存摘要 |
42
+
43
+ 错误码:`ADB_NOT_FOUND`、`ADB_UNAVAILABLE`、`DEVICE_NOT_FOUND`、`NO_DEVICES`、`CONNECT_FAILED`、`INSTALL_FAILED`、`ADB_EXIT_<code>` 等,均为结构化 `AdbError`。
44
+
45
+ ## 配置
46
+
47
+ `cordis.patch.yml` 的 `config` 块(或 profile patch):
48
+
49
+ ```yaml
50
+ - id: dsh-adb
51
+ name: dsh-adb
52
+ config:
53
+ adbPath: C:\Users\me\AppData\Local\Android\Sdk\platform-tools\adb.exe
54
+ defaultSerial: emulator-5554
55
+ timeoutMs: 30000
56
+ ```
57
+
58
+ | 键 | 说明 | 默认 |
59
+ | --- | --- | --- |
60
+ | `adbPath` | adb 可执行文件绝对路径 | 自动探测 PATH / ANDROID_HOME / ANDROID_SDK_ROOT/platform-tools |
61
+ | `defaultSerial` | 默认设备 serial | 无 |
62
+ | `timeoutMs` | 命令超时 | 30000 |
63
+ | `baselineDir` | `adb_perf_baseline` 基线存储目录 | `~/.dsh/storages/dsh-adb` |
64
+
65
+ ## 开发
66
+
67
+ ```sh
68
+ npm install # 本机 NODE_ENV=production 时加 --include=dev
69
+ npm run build # tsc → lib/
70
+ npm test # 解析器/错误分类单测(node --test)
71
+ npm pack --dry-run # 校验发布包内容(lib/ + cordis.patch.yml)
72
+ ```
73
+
74
+ 注意:本机若设了 `NODE_ENV=production`,npm 会跳过 devDependencies,安装时用 `npm install --include=dev`。
75
+
76
+ ## 测试与验证
77
+
78
+ - 原则:**提交即测** —— 全部已提交功能均有实测覆盖(单元 + headless 端到端 + 车机台架/真机)。
79
+ - 验证设备:Android 13 车机台架 + Android 13 真机。
80
+ - 版本化变更与每版验证记录见 [CHANGELOG.md](CHANGELOG.md);测试方法与覆盖现状见 [docs/TESTING.md](docs/TESTING.md)。
81
+
82
+ ## 项目文档(双语,供 AI 对话/协作者参考)
83
+
84
+ - [docs/AGENTS.md](docs/AGENTS.md) / [docs/AGENTS.en.md](docs/AGENTS.en.md) — 进项目先读:定位、铁律、命令、环境事实、文档地图
85
+ - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.en.md](docs/REQUIREMENTS.en.md) — 目的/范围/非目标/验收标准
86
+ - [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.en.md](docs/TESTING.en.md) — 测试哲学(提交即测)、三层测试方法、E2E 步骤、回归清单
87
+ - [docs/DEVELOPMENT-LOG.md](docs/DEVELOPMENT-LOG.md) / [docs/DEVELOPMENT-LOG.en.md](docs/DEVELOPMENT-LOG.en.md) — 进度时间线、4 个已修复 bug 教训、环境/生态经验
88
+ - [PLAN.md](PLAN.md) / [PLAN.en.md](PLAN.en.md) — 里程碑与待办
89
+
90
+ ## License
91
+
92
+ MIT
package/client.js ADDED
@@ -0,0 +1,156 @@
1
+ /* dsh-adb Web device panel (client half) — plain JS, no build step.
2
+ * Registers a "设备" tab in conversation.view (beside chat/trajectory/automation)
3
+ * and talks to the Host half over the package RPC channel /dsh-adb.
4
+ * Uses only client builtins: React, ctx, styles, console.
5
+ */
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-adb',
8
+ factory: (require) => {
9
+ 'use strict'
10
+ const module = { exports: {} }
11
+
12
+ const React = require('react')
13
+ const CHANNEL = '/dsh-adb'
14
+
15
+ function unwrap(value) {
16
+ if (typeof value !== 'object' || value === null || !('ok' in value)) {
17
+ throw new Error('dsh-adb host returned an invalid response.')
18
+ }
19
+ if (value.ok === true && 'value' in value) return value.value
20
+ if (value.ok === false && value.error) {
21
+ throw new Error(value.error.message ?? 'dsh-adb request failed.')
22
+ }
23
+ throw new Error('dsh-adb host returned an invalid response.')
24
+ }
25
+
26
+ function createRuntime(rpc) {
27
+ return {
28
+ listDevices: () => rpc.call(CHANNEL, 'listDevices', {}).then(unwrap),
29
+ perfSnapshot: (payload) => rpc.call(CHANNEL, 'perfSnapshot', payload).then(unwrap),
30
+ logcatTail: (payload) => rpc.call(CHANNEL, 'logcatTail', payload).then(unwrap),
31
+ }
32
+ }
33
+
34
+ const h = React.createElement
35
+ const ROW = { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0' }
36
+ const BTN = { padding: '3px 10px', cursor: 'pointer' }
37
+ const INPUT = { padding: '3px 6px' }
38
+
39
+ function MetricRows({ rows }) {
40
+ if (!rows || rows.length === 0) return h('div', null, '(无数据)')
41
+ return h('table', { style: { borderCollapse: 'collapse' } },
42
+ rows.map((row) => h('tr', { key: row[0] },
43
+ h('td', { style: { padding: '2px 12px 2px 0', color: 'var(--dsh-text-secondary, #888)' } }, row[0]),
44
+ h('td', { style: { padding: '2px 0' } }, String(row[1])),
45
+ )),
46
+ )
47
+ }
48
+
49
+ function DeviceView(props) {
50
+ const runtime = props.runtime
51
+ const [devices, setDevices] = React.useState([])
52
+ const [selected, setSelected] = React.useState(null)
53
+ const [pkg, setPkg] = React.useState('com.android.systemui')
54
+ const [snapshot, setSnapshot] = React.useState(null)
55
+ const [logEntries, setLogEntries] = React.useState([])
56
+ const [logTotal, setLogTotal] = React.useState(0)
57
+ const [level, setLevel] = React.useState('E')
58
+ const [tail, setTail] = React.useState('30')
59
+ const [error, setError] = React.useState(null)
60
+ const [busy, setBusy] = React.useState(false)
61
+
62
+ const refresh = () => {
63
+ setBusy(true); setError(null)
64
+ runtime.listDevices()
65
+ .then((value) => setDevices(value.devices ?? []))
66
+ .catch((e) => setError(String(e.message ?? e)))
67
+ .finally(() => setBusy(false))
68
+ }
69
+ React.useEffect(refresh, [])
70
+
71
+ const runSnapshot = () => {
72
+ if (!selected) return
73
+ setBusy(true); setError(null)
74
+ runtime.perfSnapshot({ serial: selected.serial, package: pkg })
75
+ .then(setSnapshot)
76
+ .catch((e) => setError(String(e.message ?? e)))
77
+ .finally(() => setBusy(false))
78
+ }
79
+
80
+ const runLogcat = () => {
81
+ if (!selected) return
82
+ setBusy(true); setError(null)
83
+ const n = parseInt(tail, 10)
84
+ runtime.logcatTail({ serial: selected.serial, level, tail: Number.isFinite(n) && n > 0 ? n : 30 })
85
+ .then((value) => { setLogEntries(value.entries ?? []); setLogTotal(value.total ?? 0) })
86
+ .catch((e) => setError(String(e.message ?? e)))
87
+ .finally(() => setBusy(false))
88
+ }
89
+
90
+ const snapshotRows = []
91
+ if (snapshot) {
92
+ const m = snapshot.meminfo; const g = snapshot.gfxinfo; const b = snapshot.battery
93
+ if (m) snapshotRows.push(['内存 PSS (KB)', m.totalPssKb], ['内存 RSS (KB)', m.totalRssKb], ['Java Heap (KB)', m.javaHeapKb], ['Native Heap (KB)', m.nativeHeapKb])
94
+ if (g) snapshotRows.push(['总帧数', g.totalFrames], ['卡顿帧 / %', `${g.jankyFrames} / ${g.jankyPercent}%`], ['P50/P90 (ms)', `${g.percentile50Ms} / ${g.percentile90Ms}`], ['P95/P99 (ms)', `${g.percentile95Ms} / ${g.percentile99Ms}`], ['Missed Vsync', g.missedVsync])
95
+ if (b) snapshotRows.push(['电量', `${b.levelPercent}%`], ['温度 (°C)', b.temperatureC])
96
+ }
97
+
98
+ return h('div', { style: { padding: 12, fontFamily: 'inherit', fontSize: 13 } },
99
+ h('div', { style: { ...ROW, justifyContent: 'space-between' } },
100
+ h('strong', null, 'ADB 设备'),
101
+ h('button', { style: BTN, onClick: refresh, disabled: busy }, busy ? '…' : '刷新'),
102
+ ),
103
+ error !== null && h('div', { style: { color: '#e5484d', margin: '6px 0' } }, String(error)),
104
+ devices.length === 0
105
+ ? h('div', { style: { color: 'var(--dsh-text-secondary, #888)', margin: '8px 0' } }, '未连接设备')
106
+ : h('div', null, devices.map((d) =>
107
+ h('button', {
108
+ key: d.serial,
109
+ onClick: () => setSelected(d),
110
+ style: { ...BTN, display: 'block', width: '100%', textAlign: 'left', margin: '2px 0',
111
+ background: selected && selected.serial === d.serial ? 'var(--dsh-accent-soft, rgba(66,133,244,.15))' : 'transparent' },
112
+ }, `${d.serial} · ${d.state}${d.model ? ' · ' + d.model : ''}`),
113
+ )),
114
+
115
+ selected !== null && h('div', { style: { marginTop: 14, borderTop: '1px solid var(--dsh-border, #333)', paddingTop: 10 } },
116
+ h('div', { style: ROW },
117
+ h('label', null, '包名'),
118
+ h('input', { style: INPUT, value: pkg, onChange: (e) => setPkg(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') runSnapshot() } }),
119
+ h('button', { style: BTN, onClick: runSnapshot, disabled: busy }, '性能快照'),
120
+ ),
121
+ snapshot && h('div', { style: { marginTop: 8 } }, h(MetricRows, { rows: snapshotRows })),
122
+
123
+ h('div', { style: { ...ROW, marginTop: 12 } },
124
+ h('label', null, '级别'),
125
+ h('select', { style: INPUT, value: level, onChange: (e) => setLevel(e.target.value) },
126
+ ['V', 'D', 'I', 'W', 'E', 'F'].map((lv) => h('option', { key: lv, value: lv }, lv))),
127
+ h('label', null, '条数'),
128
+ h('input', { style: { ...INPUT, width: 56 }, value: tail, onChange: (e) => setTail(e.target.value) }),
129
+ h('button', { style: BTN, onClick: runLogcat, disabled: busy }, '获取日志'),
130
+ ),
131
+ h('div', { style: { color: 'var(--dsh-text-secondary, #888)', margin: '4px 0' } }, `logcat 共 ${logTotal} 条(显示 ${logEntries.length})`),
132
+ h('div', { style: { maxHeight: 260, overflowY: 'auto', fontFamily: 'monospace', fontSize: 12, whiteSpace: 'pre-wrap' } },
133
+ logEntries.length === 0
134
+ ? h('div', null, '(暂无)')
135
+ : logEntries.map((e) => h('div', { key: `${e.time}-${e.pid}-${e.tid}-${e.message}` },
136
+ `${e.time} ${e.pid} ${e.tid} ${e.level} ${e.tag}: ${e.message}`)),
137
+ ),
138
+ ),
139
+ )
140
+ }
141
+
142
+ function apply(ctx) {
143
+ const slots = ctx.get('slots')
144
+ const connection = ctx.get('connection')
145
+ if (slots === undefined || connection === undefined || connection.rpc === undefined) return
146
+ const runtime = createRuntime(connection.rpc)
147
+ slots.inject('conversation.view', () => slots.register(
148
+ { name: 'conversation.view', id: 'devices', order: 30, label: '设备' },
149
+ (props) => h(DeviceView, { ...props, runtime }),
150
+ ))
151
+ }
152
+
153
+ module.exports = { apply }
154
+ return module.exports
155
+ },
156
+ })
package/lib/adb.js CHANGED
@@ -117,7 +117,7 @@ export async function runAdb(ctx, cfg, argv, options = {}) {
117
117
  export function classifyFailure(result) {
118
118
  const stderr = result.stderr;
119
119
  const stdout = result.stdout;
120
- if (/(?:error:\s*)?device ['"]?[^'\r\n]*['"]? not found/i.test(stderr)) {
120
+ if (/(?:error:\s*)?device ['"]?[^'\r\n]*['"]? not found|waiting for device/i.test(stderr)) {
121
121
  return new AdbError('DEVICE_NOT_FOUND', excerpt(stderr));
122
122
  }
123
123
  if (/no devices\/emulators found/i.test(stderr)) {
@@ -0,0 +1,43 @@
1
+ import type { PerfSnapshot } from './tools/perf.js';
2
+ /**
3
+ * Perf baseline store + numeric diff computation. Storage is a single JSON
4
+ * file under a configurable directory; diff logic is pure and unit-tested.
5
+ */
6
+ export interface BaselineMeta {
7
+ id: string;
8
+ label: string;
9
+ createdAt: string;
10
+ device: string;
11
+ package: string;
12
+ tags?: string[];
13
+ }
14
+ export interface BaselineEntry extends BaselineMeta {
15
+ snapshot: PerfSnapshot;
16
+ }
17
+ export interface BaselineStoreData {
18
+ version: 1;
19
+ baselines: BaselineEntry[];
20
+ }
21
+ export interface BaselineStore {
22
+ list(): BaselineEntry[];
23
+ get(id: string): BaselineEntry | undefined;
24
+ latest(device: string, pkg: string): BaselineEntry | undefined;
25
+ save(entry: Omit<BaselineEntry, 'id' | 'createdAt'>): BaselineEntry;
26
+ delete(id: string): boolean;
27
+ }
28
+ export declare const DEFAULT_BASELINE_DIR: string;
29
+ export declare function loadBaselines(dir: string): BaselineStoreData;
30
+ export declare function saveBaselines(dir: string, data: BaselineStoreData): void;
31
+ export declare function createStore(dir: string): BaselineStore;
32
+ /** Human-readable names for diff fields. */
33
+ export declare const FIELD_LABELS: Record<string, string>;
34
+ export interface FieldDiff {
35
+ field: string;
36
+ label: string;
37
+ from?: number;
38
+ to?: number;
39
+ delta?: number;
40
+ deltaPercent?: number;
41
+ }
42
+ /** Numeric diff between two snapshots; only fields present in both are reported. */
43
+ export declare function diffSnapshots(from: PerfSnapshot, to: PerfSnapshot): FieldDiff[];
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ export const DEFAULT_BASELINE_DIR = joinDefault();
5
+ function joinDefault() {
6
+ return `${homedir().replace(/\\/g, '/')}/.dsh/storages/dsh-adb`;
7
+ }
8
+ function filePath(dir) {
9
+ return `${dir.replace(/\\/g, '/')}/baselines.json`;
10
+ }
11
+ function emptyData() {
12
+ return { version: 1, baselines: [] };
13
+ }
14
+ export function loadBaselines(dir) {
15
+ const file = filePath(dir);
16
+ if (!existsSync(file))
17
+ return emptyData();
18
+ let raw;
19
+ try {
20
+ raw = readFileSync(file, 'utf8');
21
+ }
22
+ catch (error) {
23
+ throw new Error(`baseline store unreadable at ${file}: ${describe(error)}`);
24
+ }
25
+ try {
26
+ const data = JSON.parse(raw);
27
+ if (data?.version !== 1 || !Array.isArray(data.baselines)) {
28
+ throw new Error('unexpected shape');
29
+ }
30
+ return data;
31
+ }
32
+ catch {
33
+ throw new Error(`baseline store corrupted at ${file}: not valid version-1 JSON`);
34
+ }
35
+ }
36
+ export function saveBaselines(dir, data) {
37
+ const file = filePath(dir);
38
+ try {
39
+ mkdirSync(dirname(file), { recursive: true });
40
+ writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
41
+ }
42
+ catch (error) {
43
+ throw new Error(`baseline store unwritable at ${file}: ${describe(error)}`);
44
+ }
45
+ }
46
+ export function createStore(dir) {
47
+ return {
48
+ list() {
49
+ return loadBaselines(dir).baselines;
50
+ },
51
+ get(id) {
52
+ return loadBaselines(dir).baselines.find((entry) => entry.id === id);
53
+ },
54
+ latest(device, pkg) {
55
+ const data = loadBaselines(dir);
56
+ const matches = data.baselines.filter((entry) => entry.device === device && entry.package === pkg);
57
+ if (matches.length === 0)
58
+ return undefined;
59
+ return matches.reduce((a, b) => (a.createdAt > b.createdAt ? a : b));
60
+ },
61
+ save(entry) {
62
+ const data = loadBaselines(dir);
63
+ const stored = {
64
+ ...entry,
65
+ id: `bl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
66
+ createdAt: new Date().toISOString(),
67
+ };
68
+ data.baselines.push(stored);
69
+ saveBaselines(dir, data);
70
+ return stored;
71
+ },
72
+ delete(id) {
73
+ const data = loadBaselines(dir);
74
+ const before = data.baselines.length;
75
+ data.baselines = data.baselines.filter((entry) => entry.id !== id);
76
+ if (data.baselines.length === before)
77
+ return false;
78
+ saveBaselines(dir, data);
79
+ return true;
80
+ },
81
+ };
82
+ }
83
+ /** Human-readable names for diff fields. */
84
+ export const FIELD_LABELS = {
85
+ 'meminfo.totalPssKb': 'Total PSS (KB)',
86
+ 'meminfo.totalRssKb': 'Total RSS (KB)',
87
+ 'meminfo.javaHeapKb': 'Java Heap (KB)',
88
+ 'meminfo.nativeHeapKb': 'Native Heap (KB)',
89
+ 'meminfo.graphicsKb': 'Graphics (KB)',
90
+ 'gfxinfo.totalFrames': 'Total frames',
91
+ 'gfxinfo.jankyFrames': 'Janky frames',
92
+ 'gfxinfo.jankyPercent': 'Janky %',
93
+ 'gfxinfo.percentile50Ms': '50th percentile (ms)',
94
+ 'gfxinfo.percentile90Ms': '90th percentile (ms)',
95
+ 'gfxinfo.percentile95Ms': '95th percentile (ms)',
96
+ 'gfxinfo.percentile99Ms': '99th percentile (ms)',
97
+ 'gfxinfo.missedVsync': 'Missed vsync',
98
+ 'battery.levelPercent': 'Battery level (%)',
99
+ 'battery.temperatureC': 'Temperature (°C)',
100
+ };
101
+ const DIFF_FIELDS = [
102
+ ['meminfo', 'totalPssKb'],
103
+ ['meminfo', 'totalRssKb'],
104
+ ['meminfo', 'javaHeapKb'],
105
+ ['meminfo', 'nativeHeapKb'],
106
+ ['meminfo', 'graphicsKb'],
107
+ ['gfxinfo', 'totalFrames'],
108
+ ['gfxinfo', 'jankyFrames'],
109
+ ['gfxinfo', 'jankyPercent'],
110
+ ['gfxinfo', 'percentile50Ms'],
111
+ ['gfxinfo', 'percentile90Ms'],
112
+ ['gfxinfo', 'percentile95Ms'],
113
+ ['gfxinfo', 'percentile99Ms'],
114
+ ['gfxinfo', 'missedVsync'],
115
+ ['battery', 'levelPercent'],
116
+ ['battery', 'temperatureC'],
117
+ ];
118
+ /** Numeric diff between two snapshots; only fields present in both are reported. */
119
+ export function diffSnapshots(from, to) {
120
+ const diffs = [];
121
+ for (const [group, field] of DIFF_FIELDS) {
122
+ const fromGroup = from;
123
+ const toGroup = to;
124
+ const fromValue = fromGroup[group]?.[field];
125
+ const toValue = toGroup[group]?.[field];
126
+ if (fromValue === undefined || toValue === undefined)
127
+ continue;
128
+ const key = `${group}.${field}`;
129
+ const delta = toValue - fromValue;
130
+ diffs.push({
131
+ field: key,
132
+ label: FIELD_LABELS[key] ?? key,
133
+ from: fromValue,
134
+ to: toValue,
135
+ delta,
136
+ deltaPercent: fromValue === 0 ? undefined : (delta / fromValue) * 100,
137
+ });
138
+ }
139
+ return diffs;
140
+ }
141
+ function describe(error) {
142
+ return error instanceof Error ? error.message : String(error);
143
+ }
package/lib/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface Config {
11
11
  defaultSerial?: string;
12
12
  /** Per-command timeout in milliseconds. */
13
13
  timeoutMs?: number;
14
+ /** Directory for adb_perf_baseline storage. */
15
+ baselineDir?: string;
14
16
  }
15
17
  export declare const Config: Schema<Config>;
16
18
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import Schema from 'schemastery';
2
+ import { DEFAULT_BASELINE_DIR } from './baseline.js';
2
3
  import { registerDeviceTools } from './tools/devices.js';
4
+ import { registerCrashReportTool } from './tools/crash-report.js';
3
5
  import { registerFileTool } from './tools/file.js';
4
6
  import { registerInstallTool } from './tools/install.js';
5
7
  import { registerLogcatTool } from './tools/logcat.js';
6
8
  import { registerPerfTool } from './tools/perf.js';
9
+ import { registerPerfBaselineTool } from './tools/perf-baseline.js';
10
+ import { registerRpc } from './rpc.js';
7
11
  export const name = 'dsh-adb';
8
12
  /** The tool registry is a hard dependency: every tool registers through it. */
9
13
  export const inject = ['tools'];
@@ -11,6 +15,7 @@ export const Config = Schema.object({
11
15
  adbPath: Schema.string().description('adb 可执行文件绝对路径;缺省自动探测 PATH / ANDROID_HOME / ANDROID_SDK_ROOT / platform-tools'),
12
16
  defaultSerial: Schema.string().description('默认目标设备 serial'),
13
17
  timeoutMs: Schema.number().default(30000).description('adb 命令超时(毫秒)'),
18
+ baselineDir: Schema.string().description('adb_perf_baseline 基线存储目录;缺省 ~/.dsh/storages/dsh-adb'),
14
19
  });
15
20
  export function apply(ctx, config) {
16
21
  const cfg = config;
@@ -19,5 +24,11 @@ export function apply(ctx, config) {
19
24
  registerFileTool(ctx, cfg);
20
25
  registerLogcatTool(ctx, cfg);
21
26
  registerPerfTool(ctx, cfg);
22
- ctx.logger.info('[dsh-adb] loaded: adb_devices / adb_connect / adb_disconnect / adb_logcat / adb_install / adb_file / adb_perf_snapshot');
27
+ registerPerfBaselineTool(ctx, cfg, config.baselineDir ?? DEFAULT_BASELINE_DIR);
28
+ registerCrashReportTool(ctx, cfg);
29
+ // The RPC channel needs the client connection, which mounts after this
30
+ // plugin starts in web compositions; register lazily so headless profiles
31
+ // (no connection) stay unaffected.
32
+ ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg));
33
+ ctx.logger.info('[dsh-adb] loaded: 9 tools + web device panel rpc');
23
34
  }
package/lib/rpc.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { AdbConfig } from './adb.js';
3
+ export type RpcEndpointResult = {
4
+ ok: true;
5
+ value: unknown;
6
+ } | {
7
+ ok: false;
8
+ error: {
9
+ message: string;
10
+ };
11
+ };
12
+ /**
13
+ * Package-private Client↔Host RPC endpoint dispatch for the Web device panel.
14
+ * Extracted from the connection handler so the endpoints are unit-testable
15
+ * against a fake adb backend.
16
+ */
17
+ export declare function handleRpcEndpoint(ctx: Context, cfg: AdbConfig, endpoint: string, raw: unknown, signal: AbortSignal): Promise<RpcEndpointResult>;
18
+ /**
19
+ * Register the Package-private Client↔Host RPC for the Web device panel
20
+ * (conversation.view tab "设备"). Called lazily once the client connection
21
+ * mounts; headless compositions (no connection) stay unaffected.
22
+ */
23
+ export declare function registerRpc(ctx: Context, cfg: AdbConfig): void;
package/lib/rpc.js ADDED
@@ -0,0 +1,66 @@
1
+ import { classifyFailure, runAdb } from './adb.js';
2
+ import { parseDevices } from './parsers/devices.js';
3
+ import { matchesLevel, parseLogcat } from './parsers/logcat.js';
4
+ import { capturePerfSnapshot } from './tools/perf.js';
5
+ /**
6
+ * Package-private Client↔Host RPC endpoint dispatch for the Web device panel.
7
+ * Extracted from the connection handler so the endpoints are unit-testable
8
+ * against a fake adb backend.
9
+ */
10
+ export async function handleRpcEndpoint(ctx, cfg, endpoint, raw, signal) {
11
+ try {
12
+ const payload = (raw ?? {});
13
+ switch (endpoint) {
14
+ case 'listDevices': {
15
+ const result = await runAdb(ctx, cfg, ['devices', '-l'], { signal });
16
+ if (result.exitCode !== 0)
17
+ throw classifyFailure(result);
18
+ return { ok: true, value: { server: 'ok', devices: parseDevices(result.stdout) } };
19
+ }
20
+ case 'perfSnapshot': {
21
+ const pkg = typeof payload.package === 'string' ? payload.package : undefined;
22
+ if (pkg === undefined)
23
+ throw new Error('perfSnapshot requires a string "package"');
24
+ const serial = typeof payload.serial === 'string' ? payload.serial : undefined;
25
+ const snapshot = await capturePerfSnapshot(ctx, cfg, signal, { package: pkg, serial });
26
+ return { ok: true, value: snapshot };
27
+ }
28
+ case 'logcatTail': {
29
+ const serial = typeof payload.serial === 'string' ? payload.serial : undefined;
30
+ const level = typeof payload.level === 'string' ? payload.level : undefined;
31
+ const tail = typeof payload.tail === 'number' && payload.tail > 0 ? Math.floor(payload.tail) : 30;
32
+ const output = await runAdb(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], {
33
+ signal,
34
+ serial,
35
+ maxBytes: 8 * 1024 * 1024,
36
+ });
37
+ if (output.exitCode !== 0)
38
+ throw classifyFailure(output);
39
+ let entries = parseLogcat(output.stdout);
40
+ if (level !== undefined)
41
+ entries = entries.filter((entry) => matchesLevel(entry, level));
42
+ const capped = entries.slice(-tail);
43
+ return { ok: true, value: { total: entries.length, truncated: entries.length > tail, entries: capped } };
44
+ }
45
+ default:
46
+ return { ok: false, error: { message: `unknown endpoint: ${endpoint}` } };
47
+ }
48
+ }
49
+ catch (error) {
50
+ return { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
51
+ }
52
+ }
53
+ /**
54
+ * Register the Package-private Client↔Host RPC for the Web device panel
55
+ * (conversation.view tab "设备"). Called lazily once the client connection
56
+ * mounts; headless compositions (no connection) stay unaffected.
57
+ */
58
+ export function registerRpc(ctx, cfg) {
59
+ const connection = ctx.get('connection');
60
+ const rpc = connection?.rpc;
61
+ if (rpc === undefined)
62
+ return;
63
+ ctx.effect(() => rpc.handle('/dsh-adb', (endpoint, raw, signal) => handleRpcEndpoint(ctx, cfg, endpoint, raw, signal),
64
+ // Browser-only channel: accept requests from the loopback web GUI.
65
+ { authority: 'loopback' }));
66
+ }
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ /** adb_crash_report: collect a crash scene from an Android device. */
4
+ export declare function registerCrashReportTool(ctx: Context, cfg: AdbConfig): void;
@@ -0,0 +1,109 @@
1
+ import { AdbError, classifyFailure, jsonOutput, runAdb } from '../adb.js';
2
+ import { parseLogcat } from '../parsers/logcat.js';
3
+ import { capturePerfSnapshot } from './perf.js';
4
+ const DEFAULT_INCLUDE = ['crash-buffer', 'dropbox', 'processes'];
5
+ function excerpt(text, maxLines) {
6
+ const lines = text.split(/\r?\n/).filter((line) => line !== '');
7
+ return { lines: lines.length, excerpt: lines.slice(-maxLines).join('\n') };
8
+ }
9
+ /** adb_crash_report: collect a crash scene from an Android device. */
10
+ export function registerCrashReportTool(ctx, cfg) {
11
+ ctx.tools.register({
12
+ name: 'adb_crash_report',
13
+ description: 'Collect a crash scene from an Android device into one structured report: the logcat crash buffer (parsed entries), dropbox crash entries excerpt, process state excerpt, and (when a package is given) memory summary. Use it to gather crash context in one call instead of hunting through buffers.',
14
+ parameters: {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ properties: {
18
+ package: { type: 'string', description: 'App package id; enables the meminfo section and helps narrow the crash buffer.' },
19
+ serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
20
+ since: { type: 'string', description: 'Only crash-buffer entries at/after this time, e.g. "08-15 10:00:00.000".' },
21
+ include: {
22
+ type: 'array',
23
+ items: { type: 'string', enum: ['crash-buffer', 'dropbox', 'processes', 'meminfo'] },
24
+ description: 'Which sections to collect; defaults to crash-buffer, dropbox, processes (plus meminfo when package is given).',
25
+ },
26
+ tail: { type: 'integer', description: 'Cap for parsed crash-buffer entries and excerpt lines; defaults to 100.' },
27
+ },
28
+ },
29
+ output: jsonOutput(),
30
+ async execute(args, exec) {
31
+ const { since, tail } = args;
32
+ const tailCount = tail ?? 100;
33
+ const include = args.include ?? [
34
+ ...DEFAULT_INCLUDE,
35
+ ...(args.package !== undefined ? (['meminfo']) : []),
36
+ ];
37
+ const result = { collectedAt: new Date().toISOString() };
38
+ if (args.package !== undefined)
39
+ result.package = args.package;
40
+ if (include.includes('crash-buffer')) {
41
+ const output = await runAdb(ctx, cfg, ['logcat', '-b', 'crash', '-v', 'threadtime', '-d'], {
42
+ signal: exec.signal,
43
+ serial: args.serial,
44
+ maxBytes: 8 * 1024 * 1024,
45
+ });
46
+ if (output.exitCode !== 0)
47
+ throw classifyFailure(output);
48
+ let entries = parseLogcat(output.stdout);
49
+ if (since !== undefined)
50
+ entries = entries.filter((entry) => entry.time >= since);
51
+ const truncated = entries.length > tailCount;
52
+ result.crashBuffer = {
53
+ total: entries.length,
54
+ truncated,
55
+ entries: (truncated ? entries.slice(-tailCount) : entries).map((entry) => ({
56
+ time: entry.time,
57
+ pid: entry.pid,
58
+ tid: entry.tid,
59
+ level: entry.level,
60
+ tag: entry.tag,
61
+ message: entry.message,
62
+ })),
63
+ };
64
+ }
65
+ if (include.includes('dropbox')) {
66
+ const output = await runAdb(ctx, cfg, ['shell', 'dumpsys', 'dropbox', '--print'], {
67
+ signal: exec.signal,
68
+ serial: args.serial,
69
+ maxBytes: 4 * 1024 * 1024,
70
+ });
71
+ if (output.exitCode !== 0)
72
+ throw classifyFailure(output);
73
+ const capped = excerpt(output.stdout, tailCount);
74
+ result.dropbox = {
75
+ lines: capped.lines,
76
+ truncated: capped.lines > tailCount,
77
+ excerpt: capped.excerpt,
78
+ };
79
+ }
80
+ if (include.includes('processes')) {
81
+ const output = await runAdb(ctx, cfg, ['shell', 'dumpsys', 'activity', 'processes'], {
82
+ signal: exec.signal,
83
+ serial: args.serial,
84
+ maxBytes: 2 * 1024 * 1024,
85
+ });
86
+ if (output.exitCode !== 0)
87
+ throw classifyFailure(output);
88
+ const capped = excerpt(output.stdout, tailCount);
89
+ result.processes = {
90
+ lines: capped.lines,
91
+ truncated: capped.lines > tailCount,
92
+ excerpt: capped.excerpt,
93
+ };
94
+ }
95
+ if (include.includes('meminfo')) {
96
+ if (args.package === undefined) {
97
+ throw new AdbError('ARGS_INVALID', 'meminfo section requires a package');
98
+ }
99
+ const snapshot = await capturePerfSnapshot(ctx, cfg, exec.signal, {
100
+ package: args.package,
101
+ serial: args.serial,
102
+ metrics: ['meminfo'],
103
+ });
104
+ result.meminfo = snapshot.meminfo;
105
+ }
106
+ return result;
107
+ },
108
+ });
109
+ }
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ /** adb_perf_baseline: save/compare/list/delete perf snapshots as baselines. */
4
+ export declare function registerPerfBaselineTool(ctx: Context, cfg: AdbConfig, baselineDir: string): void;
@@ -0,0 +1,100 @@
1
+ import { AdbError, jsonOutput } from '../adb.js';
2
+ import { createStore, diffSnapshots } from '../baseline.js';
3
+ import { capturePerfSnapshot } from './perf.js';
4
+ function publicMeta(entry) {
5
+ return {
6
+ id: entry.id,
7
+ label: entry.label,
8
+ createdAt: entry.createdAt,
9
+ device: entry.device,
10
+ package: entry.package,
11
+ tags: entry.tags ?? [],
12
+ };
13
+ }
14
+ /** adb_perf_baseline: save/compare/list/delete perf snapshots as baselines. */
15
+ export function registerPerfBaselineTool(ctx, cfg, baselineDir) {
16
+ ctx.tools.register({
17
+ name: 'adb_perf_baseline',
18
+ description: 'Manage performance baselines for regression checks: save a snapshot as a baseline (label + optional tags), compare the current device state against a baseline and get a numeric diff report (PSS, janky %, frame percentiles), list stored baselines, or delete one. Baselines are stored locally on the host under the configured baselineDir.',
19
+ parameters: {
20
+ type: 'object',
21
+ additionalProperties: false,
22
+ required: ['command'],
23
+ properties: {
24
+ command: {
25
+ type: 'string',
26
+ enum: ['save', 'compare', 'list', 'delete'],
27
+ description: 'save: capture + store a baseline; compare: diff current state against a baseline (id, or latest for device+package); list: stored baselines; delete: remove one by id.',
28
+ },
29
+ package: { type: 'string', description: 'App package id; required for save and compare.' },
30
+ serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
31
+ metrics: {
32
+ type: 'array',
33
+ items: { type: 'string', enum: ['meminfo', 'gfxinfo', 'battery'] },
34
+ description: 'Which metrics to capture; defaults to all three.',
35
+ },
36
+ label: { type: 'string', description: 'Baseline label for save, e.g. the build/version under test.' },
37
+ tags: {
38
+ type: 'array',
39
+ items: { type: 'string' },
40
+ description: 'Optional tags for save, e.g. ["release", "before-optimization"].',
41
+ },
42
+ id: { type: 'string', description: 'Baseline id; required for delete, optional for compare (defaults to the latest for device+package).' },
43
+ },
44
+ },
45
+ output: jsonOutput(),
46
+ async execute(args, exec) {
47
+ const store = createStore(baselineDir);
48
+ const device = args.serial ?? cfg.defaultSerial ?? 'default';
49
+ if (args.command === 'list') {
50
+ const entries = store.list().map(publicMeta);
51
+ return { command: 'list', count: entries.length, baselines: entries };
52
+ }
53
+ if (args.command === 'delete') {
54
+ if (args.id === undefined)
55
+ throw new AdbError('ARGS_INVALID', 'delete requires a baseline id');
56
+ const deleted = store.delete(args.id);
57
+ if (!deleted)
58
+ throw new AdbError('BASELINE_NOT_FOUND', `no baseline with id ${args.id}`);
59
+ return { command: 'delete', deleted: true, id: args.id };
60
+ }
61
+ if (args.package === undefined) {
62
+ throw new AdbError('ARGS_INVALID', `${args.command} requires a package`);
63
+ }
64
+ if (args.command === 'save') {
65
+ const snapshot = await capturePerfSnapshot(ctx, cfg, exec.signal, {
66
+ package: args.package,
67
+ serial: args.serial,
68
+ metrics: args.metrics,
69
+ });
70
+ const stored = store.save({
71
+ label: args.label ?? `baseline-${new Date().toISOString().slice(0, 10)}`,
72
+ device,
73
+ package: args.package,
74
+ ...(args.tags !== undefined && args.tags.length > 0 ? { tags: args.tags } : {}),
75
+ snapshot,
76
+ });
77
+ return { command: 'save', saved: publicMeta(stored) };
78
+ }
79
+ // compare
80
+ const baseline = args.id !== undefined
81
+ ? store.get(args.id)
82
+ : store.latest(device, args.package);
83
+ if (baseline === undefined) {
84
+ throw new AdbError('BASELINE_NOT_FOUND', `no baseline for device ${device} package ${args.package}${args.id !== undefined ? ` (id ${args.id})` : ''}; save one with command=save first`);
85
+ }
86
+ const current = await capturePerfSnapshot(ctx, cfg, exec.signal, {
87
+ package: args.package,
88
+ serial: args.serial,
89
+ metrics: args.metrics,
90
+ });
91
+ const diffs = diffSnapshots(baseline.snapshot, current);
92
+ return {
93
+ command: 'compare',
94
+ baseline: publicMeta(baseline),
95
+ current: { package: current.package, metrics: current.metrics },
96
+ diffs,
97
+ };
98
+ },
99
+ });
100
+ }
@@ -1,4 +1,19 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import { type AdbConfig } from '../adb.js';
3
+ import { parseBattery, parseGfxinfo, parseMeminfo } from '../parsers/perf.js';
4
+ export type PerfMetric = 'meminfo' | 'gfxinfo' | 'battery';
5
+ export interface PerfSnapshot {
6
+ package: string;
7
+ metrics: PerfMetric[];
8
+ meminfo?: ReturnType<typeof parseMeminfo>;
9
+ gfxinfo?: ReturnType<typeof parseGfxinfo>;
10
+ battery?: ReturnType<typeof parseBattery>;
11
+ }
12
+ /** Shared capture: dumpsys meminfo / gfxinfo / battery for one app. */
13
+ export declare function capturePerfSnapshot(ctx: Context, cfg: AdbConfig, signal: AbortSignal, args: {
14
+ package: string;
15
+ serial?: string;
16
+ metrics?: PerfMetric[];
17
+ }): Promise<PerfSnapshot>;
3
18
  /** adb_perf_snapshot: dumpsys meminfo / gfxinfo / battery for one app. */
4
19
  export declare function registerPerfTool(ctx: Context, cfg: AdbConfig): void;
package/lib/tools/perf.js CHANGED
@@ -1,5 +1,30 @@
1
1
  import { classifyFailure, jsonOutput, runAdb } from '../adb.js';
2
2
  import { parseBattery, parseGfxinfo, parseMeminfo } from '../parsers/perf.js';
3
+ /** Shared capture: dumpsys meminfo / gfxinfo / battery for one app. */
4
+ export async function capturePerfSnapshot(ctx, cfg, signal, args) {
5
+ const metrics = args.metrics ?? ['meminfo', 'gfxinfo', 'battery'];
6
+ const result = { package: args.package, metrics: [...metrics] };
7
+ for (const metric of metrics) {
8
+ // dumpsys battery is device-global and takes no package argument.
9
+ const argv = metric === 'battery'
10
+ ? ['shell', 'dumpsys', 'battery']
11
+ : ['shell', 'dumpsys', metric, args.package];
12
+ const output = await runAdb(ctx, cfg, argv, {
13
+ signal,
14
+ serial: args.serial,
15
+ maxBytes: 4 * 1024 * 1024,
16
+ });
17
+ if (output.exitCode !== 0)
18
+ throw classifyFailure(output);
19
+ if (metric === 'meminfo')
20
+ result.meminfo = parseMeminfo(output.stdout);
21
+ else if (metric === 'gfxinfo')
22
+ result.gfxinfo = parseGfxinfo(output.stdout);
23
+ else
24
+ result.battery = parseBattery(output.stdout);
25
+ }
26
+ return result;
27
+ }
3
28
  /** adb_perf_snapshot: dumpsys meminfo / gfxinfo / battery for one app. */
4
29
  export function registerPerfTool(ctx, cfg) {
5
30
  ctx.tools.register({
@@ -21,28 +46,7 @@ export function registerPerfTool(ctx, cfg) {
21
46
  },
22
47
  output: jsonOutput(),
23
48
  async execute(args, exec) {
24
- const metrics = args.metrics ?? ['meminfo', 'gfxinfo', 'battery'];
25
- const result = { package: args.package, metrics: [...metrics] };
26
- for (const metric of metrics) {
27
- // dumpsys battery is device-global and takes no package argument.
28
- const argv = metric === 'battery'
29
- ? ['shell', 'dumpsys', 'battery']
30
- : ['shell', 'dumpsys', metric, args.package];
31
- const output = await runAdb(ctx, cfg, argv, {
32
- signal: exec.signal,
33
- serial: args.serial,
34
- maxBytes: 4 * 1024 * 1024,
35
- });
36
- if (output.exitCode !== 0)
37
- throw classifyFailure(output);
38
- if (metric === 'meminfo')
39
- result.meminfo = parseMeminfo(output.stdout);
40
- else if (metric === 'gfxinfo')
41
- result.gfxinfo = parseGfxinfo(output.stdout);
42
- else
43
- result.battery = parseBattery(output.stdout);
44
- }
45
- return result;
49
+ return capturePerfSnapshot(ctx, cfg, exec.signal, args);
46
50
  },
47
51
  });
48
52
  }
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "dsh-adb",
3
- "version": "0.1.5",
4
- "description": "ADB device & bench operations for DeepSeek Harness: device discovery, structured logcat, apk install, file pull/push, performance snapshots",
3
+ "version": "1.0.0",
4
+ "description": "ADB device & bench operations for DeepSeek Harness: device discovery, structured logcat, apk install, file pull/push, performance snapshots, perf baselines, crash reports, web device panel",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
7
12
  "files": [
8
13
  "lib",
14
+ "client.js",
9
15
  "cordis.patch.yml"
10
16
  ],
11
17
  "keywords": [
@@ -21,6 +27,9 @@
21
27
  "dsh": {
22
28
  "bundle": {
23
29
  "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "platform": "web"
24
33
  }
25
34
  },
26
35
  "scripts": {