dsh-adb 1.1.0 → 1.3.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 +6 -2
- package/README.zh-CN.md +8 -4
- package/client.js +113 -3
- package/lib/index.d.ts +2 -0
- package/lib/index.js +8 -2
- package/lib/parsers/sysinfo.d.ts +2 -0
- package/lib/parsers/sysinfo.js +4 -2
- package/lib/report-store.d.ts +18 -0
- package/lib/report-store.js +55 -0
- package/lib/report.d.ts +103 -0
- package/lib/report.js +192 -0
- package/lib/rpc.d.ts +2 -2
- package/lib/rpc.js +14 -3
- package/lib/tools/device-report.d.ts +10 -0
- package/lib/tools/device-report.js +52 -0
- package/lib/tools/wait.d.ts +42 -0
- package/lib/tools/wait.js +144 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,11 +16,11 @@ Or install directly from GitHub: `dsh plugin --profile web add github:SamXiaBing
|
|
|
16
16
|
|
|
17
17
|
## Web device panel (v1.1.0)
|
|
18
18
|
|
|
19
|
-
A "设备" tab in the conversation view ring (next to chat / trajectory / automation): device list with status, package autocomplete (fuzzy search), live streaming logcat window (level/keyword/package/pid filters, pause/clear/auto-scroll), device info card, process list,
|
|
19
|
+
A "设备" tab in the conversation view ring (next to chat / trajectory / automation): device list with status, package autocomplete (fuzzy search), live streaming logcat window (level/keyword/package/pid filters, pause/clear/auto-scroll), device info card, process list, performance snapshot, and a **one-click health report** (设备体检: device identity, top-RSS processes, crash buffer, W/E/F logcat window, storage — persisted under `reportDir`, sendable to the conversation for diagnosis). The report turns raw evidence into signal before it reaches the model: crash-buffer entries are classified into **real crashes (with stack chains) vs. MediaTek boot markers**, repetitive logcat is **aggregated by tag** ("AOSP-MdnsDiscoveryManag ×3264" instead of 3k identical lines), and a compact **health summary** (verdict + issues) is attached — so the agent reasons from conclusions, not 17k raw lines. Plus harness synergy: **send any logcat/snapshot/report to the conversation** (the agent analyzes it), a live strip of the agent's adb operations, and a registered **crash-analysis skill** (`dsh-adb-crash-analysis`) for automation pipelines. Data flows over the package RPC channel; install into a web profile and restart the GUI (see `scripts/restart-web.ps1` for a one-click restart).
|
|
20
20
|
|
|
21
21
|
## Ecosystem
|
|
22
22
|
|
|
23
|
-
- ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` published (latest: 1.
|
|
23
|
+
- ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` published (latest: 1.1.0)
|
|
24
24
|
- ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **merged**
|
|
25
25
|
- ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **merged**
|
|
26
26
|
- ✅ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — **merged**
|
|
@@ -39,6 +39,8 @@ Topics: `dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
|
|
|
39
39
|
| `adb_perf_snapshot` | Structured `dumpsys meminfo / gfxinfo / battery` snapshots (PSS, frame percentiles, jank rate, battery) |
|
|
40
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
41
|
| `adb_crash_report` | One-call crash scene: parsed logcat crash buffer + dropbox excerpt + process state + memory summary |
|
|
42
|
+
| `adb_device_report` | One-click health report: device identity + top-RSS processes + crash buffer (real crashes w/ stacks vs. boot markers) + W/E/F logcat aggregated by tag + storage + health verdict; each section degrades independently; persisted under `reportDir` |
|
|
43
|
+
| `adb_wait_for` | Wait until a device condition holds — device-online / boot-complete / process appeared / logcat keyword — polling up to a budget, instead of sleeping a fixed number of seconds; returns `matched:false` on timeout |
|
|
42
44
|
|
|
43
45
|
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.
|
|
44
46
|
|
|
@@ -61,6 +63,7 @@ Set the `config` block in `cordis.patch.yml` (or a profile patch):
|
|
|
61
63
|
| `defaultSerial` | Default target device serial | none |
|
|
62
64
|
| `timeoutMs` | Per-command timeout | 30000 |
|
|
63
65
|
| `baselineDir` | Directory for `adb_perf_baseline` storage | `~/.dsh/storages/dsh-adb` |
|
|
66
|
+
| `reportDir` | Directory for `adb_device_report` storage | `<baselineDir>/reports` |
|
|
64
67
|
|
|
65
68
|
## Development
|
|
66
69
|
|
|
@@ -83,6 +86,7 @@ npm pack --dry-run # verify publish contents (lib/ + cordis.patch.yml)
|
|
|
83
86
|
- [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.en.md](docs/REQUIREMENTS.en.md) — purpose / scope / non-goals / acceptance criteria
|
|
84
87
|
- [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.en.md](docs/TESTING.en.md) — testing philosophy, three test layers, E2E steps, regression checklist
|
|
85
88
|
- [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
|
|
89
|
+
- [docs/ROADMAP.md](docs/ROADMAP.md) — harness×adb synergy feature roadmap (diagnosis report, crash attribution, screenshot vision, bench automation tests, wait primitives, approvals, multi-device compare, scheduled monitoring, rollback ledger)
|
|
86
90
|
- [PLAN.md](PLAN.md) / [PLAN.en.md](PLAN.en.md) — milestones & backlog
|
|
87
91
|
|
|
88
92
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.md) | 简体中文
|
|
6
6
|
|
|
7
|
-
让 DSH agent 直接操作 Android 设备 / 车机台架:设备发现、结构化 logcat、apk 安装、文件 pull/push
|
|
7
|
+
让 DSH agent 直接操作 Android 设备 / 车机台架:设备发现、结构化 logcat、apk 安装、文件 pull/push、性能快照、一键体检。面向实车与台架联调场景,业务内通用(不限 Unity、不限具体车机协议)。
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
@@ -14,13 +14,13 @@ dsh plugin --profile web add dsh-adb
|
|
|
14
14
|
|
|
15
15
|
或从 GitHub 直装:`dsh plugin --profile web add github:SamXiaBing/dsh-adb`
|
|
16
16
|
|
|
17
|
-
## Web 设备面板(v1.
|
|
17
|
+
## Web 设备面板(v1.2.0)
|
|
18
18
|
|
|
19
|
-
会话视图页签「设备」(与 chat
|
|
19
|
+
会话视图页签「设备」(与 chat/轨迹/任务管理并列):设备列表/状态、包名下拉自动补全、实时 logcat 窗口(级别/关键字/包名/pid 过滤、暂停/清空/自动滚动)、设备信息卡、进程列表、性能快照、**一键体检**(设备信息 + Top RSS 进程 + 崩溃缓冲 + W/E/F 日志 + 存储用量,落盘到 `reportDir`,可一键发送到对话诊断)。体检报告先做**证据→信号**提炼再交给 agent:崩溃按签名分类(真实崩溃+堆栈链 / MediaTek 启动标记 / 其他)、W/E/F 按 tag 聚合(计数+样本行)、插件自产健康摘要(verdict + issues)——agent 从结论出发而非从 17k 行原始日志出发。harness 协同:logcat/快照/体检报告**一键发送到对话**、面板顶部实时显示 agent 的 adb 操作、注册 **crash-analysis 技能**(`dsh-adb-crash-analysis`)供自动化流水线使用。数据走 Package RPC;需装入 web profile 并重启 GUI 生效(一键重启见 `scripts/restart-web.ps1`)。
|
|
20
20
|
|
|
21
21
|
## 生态收录
|
|
22
22
|
|
|
23
|
-
- ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 1.
|
|
23
|
+
- ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 1.1.0)
|
|
24
24
|
- ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **已合并**
|
|
25
25
|
- ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **已合并**
|
|
26
26
|
- ✅ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — **已合并**
|
|
@@ -39,6 +39,8 @@ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
|
|
|
39
39
|
| `adb_perf_snapshot` | `dumpsys meminfo / gfxinfo / battery` 结构化快照(PSS/帧率百分位/卡顿率/电量) |
|
|
40
40
|
| `adb_perf_baseline` | 性能回归:快照存基线(label/tags)、与当前状态数值对比(PSS/卡顿率/百分位)、list/delete(本地存储,`baselineDir`) |
|
|
41
41
|
| `adb_crash_report` | 崩溃现场一键采集:crash buffer 解析 + dropbox 摘录 + 进程状态 + 内存摘要 |
|
|
42
|
+
| `adb_device_report` | 一键体检:设备信息 + Top RSS 进程 + 崩溃缓冲(真实崩溃带堆栈/启动标记分类)+ W/E/F 日志按 tag 聚合 + 存储用量 + 健康结论;每节独立降级;落盘到 `reportDir` |
|
|
43
|
+
| `adb_wait_for` | 等待原语:等设备上线 / 启动完成 / 进程出现 / logcat 出现关键字,轮询到预算上限,替代盲目 sleep;超时返回 `matched:false` |
|
|
42
44
|
|
|
43
45
|
错误码:`ADB_NOT_FOUND`、`ADB_UNAVAILABLE`、`DEVICE_NOT_FOUND`、`NO_DEVICES`、`CONNECT_FAILED`、`INSTALL_FAILED`、`ADB_EXIT_<code>` 等,均为结构化 `AdbError`。
|
|
44
46
|
|
|
@@ -61,6 +63,7 @@ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
|
|
|
61
63
|
| `defaultSerial` | 默认设备 serial | 无 |
|
|
62
64
|
| `timeoutMs` | 命令超时 | 30000 |
|
|
63
65
|
| `baselineDir` | `adb_perf_baseline` 基线存储目录 | `~/.dsh/storages/dsh-adb` |
|
|
66
|
+
| `reportDir` | `adb_device_report` 体检报告存储目录 | `<baselineDir>/reports` |
|
|
64
67
|
|
|
65
68
|
## 开发
|
|
66
69
|
|
|
@@ -85,6 +88,7 @@ npm pack --dry-run # 校验发布包内容(lib/ + cordis.patch.yml)
|
|
|
85
88
|
- [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.en.md](docs/REQUIREMENTS.en.md) — 目的/范围/非目标/验收标准
|
|
86
89
|
- [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.en.md](docs/TESTING.en.md) — 测试哲学(提交即测)、三层测试方法、E2E 步骤、回归清单
|
|
87
90
|
- [docs/DEVELOPMENT-LOG.md](docs/DEVELOPMENT-LOG.md) / [docs/DEVELOPMENT-LOG.en.md](docs/DEVELOPMENT-LOG.en.md) — 进度时间线、4 个已修复 bug 教训、环境/生态经验
|
|
91
|
+
- [docs/ROADMAP.md](docs/ROADMAP.md) — harness×adb 协同功能路线图(诊断报告/崩溃归因/截图视觉/台架自动化测试/等待原语/审批/多设备对比/定时巡检/回滚台账)
|
|
88
92
|
- [PLAN.md](PLAN.md) / [PLAN.en.md](PLAN.en.md) — 里程碑与待办
|
|
89
93
|
|
|
90
94
|
## License
|
package/client.js
CHANGED
|
@@ -55,6 +55,59 @@ function formatSnapshotBlock(snapshot) {
|
|
|
55
55
|
return ['以下是从设备面板抓取的性能快照,请分析:', ...rows.map((r) => '- ' + r)].join('\n')
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** Format a one-click health report into a send-to-conversation text block. */
|
|
59
|
+
function formatReportBlock(report) {
|
|
60
|
+
if (!report || typeof report !== 'object') return '(无体检报告数据)'
|
|
61
|
+
const lines = []
|
|
62
|
+
const health = report.health
|
|
63
|
+
const d = report.device
|
|
64
|
+
if (d) {
|
|
65
|
+
const parts = [d.model, d.manufacturer, `Android ${d.release ?? '?'}`].filter(Boolean)
|
|
66
|
+
if (d.sdk) parts.push(`API ${d.sdk}`)
|
|
67
|
+
if (d.resolution) parts.push(d.resolution)
|
|
68
|
+
if (d.memTotalKb) parts.push(`内存 ${Math.round(d.memTotalKb / 1024)}MB`)
|
|
69
|
+
lines.push(`设备:${parts.join(' · ') || report.serial}`)
|
|
70
|
+
} else {
|
|
71
|
+
lines.push(`设备:${report.serial || '未知'}`)
|
|
72
|
+
}
|
|
73
|
+
if (health) {
|
|
74
|
+
lines.push(`体检结论:${health.verdict === 'attention' ? '需关注' : '正常'}`)
|
|
75
|
+
for (const line of health.lines) lines.push(`- ${line}`)
|
|
76
|
+
if (health.issues && health.issues.length > 0) {
|
|
77
|
+
lines.push('关注项:')
|
|
78
|
+
for (const issue of health.issues) lines.push(`- ⚠ ${issue}`)
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const cb = report.crashBuffer
|
|
82
|
+
const lg = report.logcat
|
|
83
|
+
lines.push(`崩溃缓冲:${cb ? cb.total : 0} 条`)
|
|
84
|
+
lines.push(`W/E/F 日志:${lg ? lg.total : 0} 条`)
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(report.errors) && report.errors.length > 0) {
|
|
87
|
+
lines.push(`采集失败:${report.errors.map((e) => `${e.section}: ${e.message}`).join('; ')}`)
|
|
88
|
+
}
|
|
89
|
+
lines.push(`采集时间:${report.collectedAt ?? '未知'}`)
|
|
90
|
+
// Evidence: only real crash chains, not the whole (mostly boot-marker) buffer.
|
|
91
|
+
if (report.crashBuffer && report.crashBuffer.chains && report.crashBuffer.chains.length > 0) {
|
|
92
|
+
lines.push('', '真实崩溃堆栈:')
|
|
93
|
+
for (const chain of report.crashBuffer.chains) {
|
|
94
|
+
lines.push('```log',
|
|
95
|
+
`${chain.signature.time} ${chain.signature.pid} ${chain.signature.tid} ${chain.signature.level} ${chain.signature.tag}: ${chain.signature.message}`,
|
|
96
|
+
...(chain.following || []).map((e) => `${e.time} ${e.pid} ${e.tid} ${e.level} ${e.tag}: ${e.message}`),
|
|
97
|
+
'```')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Evidence: top logcat noise sources with one sample each.
|
|
101
|
+
if (report.logcat && report.logcat.byTag && report.logcat.byTag.length > 0) {
|
|
102
|
+
lines.push('', 'W/E/F 主要来源(样本):')
|
|
103
|
+
for (const agg of report.logcat.byTag.slice(0, 5)) {
|
|
104
|
+
const s = agg.sample
|
|
105
|
+
lines.push(`- ${agg.tag}(${agg.level}) ×${agg.count}:${s.time} ${s.pid} ${s.tid} ${s.tag}: ${s.message}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return ['以下是从设备面板生成的一键体检报告,请结合 dsh-adb-crash-analysis 技能分析设备健康状态:', ...lines].join('\n')
|
|
109
|
+
}
|
|
110
|
+
|
|
58
111
|
/** Panel dictionary: zh is the key-set source of truth, en mirrors it. */
|
|
59
112
|
const DICTIONARY = {
|
|
60
113
|
zh: {
|
|
@@ -85,6 +138,11 @@ const DICTIONARY = {
|
|
|
85
138
|
'memPss': '内存 PSS (KB)', 'memRss': '内存 RSS (KB)', 'javaHeap': 'Java Heap (KB)', 'nativeHeap': 'Native Heap (KB)',
|
|
86
139
|
'frames': '总帧数', 'janky': '卡顿帧 / %', 'p50p90': 'P50/P90 (ms)', 'p95p99': 'P95/P99 (ms)',
|
|
87
140
|
'battery': '电量', 'temp': '温度 (°C)', 'pkg': '包=', 'pid': 'pid=',
|
|
141
|
+
'report': '一键体检', 'reportRunning': '采集中…', 'reportDevice': '设备',
|
|
142
|
+
'reportCrash': '崩溃缓冲', 'reportLogcat': 'W/E/F 日志', 'reportProcesses': 'Top 进程',
|
|
143
|
+
'reportErrors': '采集失败', 'reportTime': '采集时间', 'reportSaved': '已保存',
|
|
144
|
+
'reportVerdict': '体检结论', 'reportVerdictOk': '正常', 'reportVerdictAttention': '需关注',
|
|
145
|
+
'reportSignals': '关注项',
|
|
88
146
|
},
|
|
89
147
|
en: {
|
|
90
148
|
'panel.title': 'ADB Devices',
|
|
@@ -114,6 +172,11 @@ const DICTIONARY = {
|
|
|
114
172
|
'memPss': 'PSS (KB)', 'memRss': 'RSS (KB)', 'javaHeap': 'Java Heap (KB)', 'nativeHeap': 'Native Heap (KB)',
|
|
115
173
|
'frames': 'Total frames', 'janky': 'Janky / %', 'p50p90': 'P50/P90 (ms)', 'p95p99': 'P95/P99 (ms)',
|
|
116
174
|
'battery': 'Battery', 'temp': 'Temp (°C)', 'pkg': 'pkg=', 'pid': 'pid=',
|
|
175
|
+
'report': 'Health report', 'reportRunning': 'collecting…', 'reportDevice': 'Device',
|
|
176
|
+
'reportCrash': 'Crash buffer', 'reportLogcat': 'W/E/F logs', 'reportProcesses': 'Top processes',
|
|
177
|
+
'reportErrors': 'Collection errors', 'reportTime': 'Collected at', 'reportSaved': 'Saved',
|
|
178
|
+
'reportVerdict': 'Health', 'reportVerdictOk': 'OK', 'reportVerdictAttention': 'needs attention',
|
|
179
|
+
'reportSignals': 'Concerns',
|
|
117
180
|
},
|
|
118
181
|
}
|
|
119
182
|
|
|
@@ -149,6 +212,7 @@ if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object'
|
|
|
149
212
|
processList: (payload) => call('processList', payload),
|
|
150
213
|
logcatDelta: (payload) => call('logcatDelta', payload),
|
|
151
214
|
perfSnapshot: (payload) => call('perfSnapshot', payload),
|
|
215
|
+
deviceReport: (payload) => call('deviceReport', payload),
|
|
152
216
|
}
|
|
153
217
|
}
|
|
154
218
|
|
|
@@ -235,7 +299,7 @@ if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object'
|
|
|
235
299
|
|
|
236
300
|
const selectDevice = (device) => {
|
|
237
301
|
actions.setSelected(device)
|
|
238
|
-
actions.setInfo(null); actions.setSnapshot(null); actions.setProcesses([])
|
|
302
|
+
actions.setInfo(null); actions.setSnapshot(null); actions.setReport(null); actions.setProcesses([])
|
|
239
303
|
actions.clearLog()
|
|
240
304
|
setBusy(true); actions.setError(null)
|
|
241
305
|
Promise.all([
|
|
@@ -263,6 +327,16 @@ if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object'
|
|
|
263
327
|
.finally(() => setBusy(false))
|
|
264
328
|
}
|
|
265
329
|
|
|
330
|
+
const runReport = () => {
|
|
331
|
+
const device = stateRef.current.selected
|
|
332
|
+
if (!device) return
|
|
333
|
+
setBusy(true); actions.setError(null)
|
|
334
|
+
runtime.deviceReport({ serial: device.serial })
|
|
335
|
+
.then(actions.setReport)
|
|
336
|
+
.catch(fail)
|
|
337
|
+
.finally(() => setBusy(false))
|
|
338
|
+
}
|
|
339
|
+
|
|
266
340
|
const applyPackageFilter = (name) => {
|
|
267
341
|
actions.setLogPkg(name)
|
|
268
342
|
actions.clearLog()
|
|
@@ -319,6 +393,33 @@ if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object'
|
|
|
319
393
|
? [[t('model'), st.info.model], [t('manufacturer'), st.info.manufacturer], [t('android'), st.info.release], [t('api'), st.info.sdk], [t('resolution'), st.info.resolution], [t('memTotal'), st.info.memTotalKb ? `${Math.round(st.info.memTotalKb / 1024)} MB` : undefined]].filter((r) => r[1] !== undefined && r[1] !== null)
|
|
320
394
|
: []
|
|
321
395
|
|
|
396
|
+
const reportRows = []
|
|
397
|
+
if (st.report) {
|
|
398
|
+
const r = st.report
|
|
399
|
+
if (r.health) {
|
|
400
|
+
reportRows.push([t('reportVerdict'), r.health.verdict === 'attention' ? t('reportVerdictAttention') : t('reportVerdictOk')])
|
|
401
|
+
if (r.health.issues && r.health.issues.length > 0) {
|
|
402
|
+
reportRows.push([t('reportSignals'), r.health.issues.length])
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (r.device) {
|
|
406
|
+
const parts = [r.device.model, r.device.release, r.device.sdk ? `API ${r.device.sdk}` : r.device.sdk].filter(Boolean)
|
|
407
|
+
reportRows.push([t('reportDevice'), parts.join(' · ')])
|
|
408
|
+
}
|
|
409
|
+
if (r.crashBuffer) {
|
|
410
|
+
const cb = r.crashBuffer
|
|
411
|
+
reportRows.push([t('reportCrash'), `${cb.realCrashCount} 真实${cb.bootMarkerCount > 0 ? ` + ${cb.bootMarkerCount} 启动标记` : ''} / ${cb.total} 条`])
|
|
412
|
+
}
|
|
413
|
+
if (r.logcat && r.logcat.byTag && r.logcat.byTag.length > 0) {
|
|
414
|
+
const top = r.logcat.byTag[0]
|
|
415
|
+
reportRows.push([t('reportLogcat'), `${r.logcat.total} 条 · ${top.tag} ×${top.count}`])
|
|
416
|
+
}
|
|
417
|
+
if (Array.isArray(r.errors) && r.errors.length > 0) {
|
|
418
|
+
reportRows.push([t('reportErrors'), r.errors.length])
|
|
419
|
+
}
|
|
420
|
+
if (r.savedTo) reportRows.push([t('reportSaved'), r.savedTo])
|
|
421
|
+
}
|
|
422
|
+
|
|
322
423
|
const pidsLabel = st.logPids.length > 0 ? ` · ${t('pid')}${st.logPids.join(',')}` : ''
|
|
323
424
|
const statusText = `${t('shownCount').replace('{n}', String(st.logEntries.length))}${st.logPkg ? ` · ${t('pkg')}${st.logPkg}` : ''}${pidsLabel}${st.logPaused ? ` · ${t('paused')}` : ` · ${t('refreshing')}`}`
|
|
324
425
|
|
|
@@ -351,6 +452,14 @@ if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object'
|
|
|
351
452
|
h(MetricRows, { rows: infoRows }),
|
|
352
453
|
),
|
|
353
454
|
|
|
455
|
+
h('div', { style: SECTION },
|
|
456
|
+
h('div', { style: ROW },
|
|
457
|
+
h('button', { style: BTN, onClick: runReport, disabled: busy }, busy ? t('reportRunning') : t('report')),
|
|
458
|
+
st.report && h('button', { style: BTN, onClick: () => sendToChat(formatReportBlock(st.report)) }, t('sendToChat')),
|
|
459
|
+
),
|
|
460
|
+
st.report && h('div', { style: { marginTop: 8 } }, h(MetricRows, { rows: reportRows })),
|
|
461
|
+
),
|
|
462
|
+
|
|
354
463
|
h('div', { style: SECTION },
|
|
355
464
|
h('div', { style: ROW },
|
|
356
465
|
h('label', null, t('package')),
|
|
@@ -415,7 +524,7 @@ if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object'
|
|
|
415
524
|
const panelStore = defineStore({
|
|
416
525
|
init: () => ({
|
|
417
526
|
devices: [], selected: null, info: null, packages: [], pkg: 'com.android.systemui',
|
|
418
|
-
snapshot: null, processes: [], logEntries: [], logSince: '',
|
|
527
|
+
snapshot: null, report: null, processes: [], logEntries: [], logSince: '',
|
|
419
528
|
logLevel: 'V', logKeyword: '', logPkg: '', logPids: [], logPaused: false, logAuto: true, error: null,
|
|
420
529
|
}),
|
|
421
530
|
actions: {
|
|
@@ -425,6 +534,7 @@ if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object'
|
|
|
425
534
|
setPackages: (d, v) => { d.packages = v },
|
|
426
535
|
setPkg: (d, v) => { d.pkg = v },
|
|
427
536
|
setSnapshot: (d, v) => { d.snapshot = v },
|
|
537
|
+
setReport: (d, v) => { d.report = v },
|
|
428
538
|
setProcesses: (d, v) => { d.processes = v },
|
|
429
539
|
setError: (d, v) => { d.error = v },
|
|
430
540
|
appendLog: (d, entries) => {
|
|
@@ -464,5 +574,5 @@ if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object'
|
|
|
464
574
|
},
|
|
465
575
|
})
|
|
466
576
|
} else if (typeof module !== 'undefined' && module.exports) {
|
|
467
|
-
module.exports = { formatLogcatBlock, formatSnapshotBlock, extractAdbActivity, nodeArrayOf, DICTIONARY }
|
|
577
|
+
module.exports = { formatLogcatBlock, formatSnapshotBlock, formatReportBlock, extractAdbActivity, nodeArrayOf, DICTIONARY }
|
|
468
578
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface Config {
|
|
|
13
13
|
timeoutMs?: number;
|
|
14
14
|
/** Directory for adb_perf_baseline storage. */
|
|
15
15
|
baselineDir?: string;
|
|
16
|
+
/** Directory for adb_device_report storage; defaults to <baselineDir>/reports. */
|
|
17
|
+
reportDir?: string;
|
|
16
18
|
}
|
|
17
19
|
export declare const Config: Schema<Config>;
|
|
18
20
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -2,11 +2,13 @@ import Schema from 'schemastery';
|
|
|
2
2
|
import { DEFAULT_BASELINE_DIR } from './baseline.js';
|
|
3
3
|
import { registerDeviceTools } from './tools/devices.js';
|
|
4
4
|
import { registerCrashReportTool } from './tools/crash-report.js';
|
|
5
|
+
import { registerDeviceReportTool } from './tools/device-report.js';
|
|
5
6
|
import { registerFileTool } from './tools/file.js';
|
|
6
7
|
import { registerInstallTool } from './tools/install.js';
|
|
7
8
|
import { registerLogcatTool } from './tools/logcat.js';
|
|
8
9
|
import { registerPerfTool } from './tools/perf.js';
|
|
9
10
|
import { registerPerfBaselineTool } from './tools/perf-baseline.js';
|
|
11
|
+
import { registerWaitTool } from './tools/wait.js';
|
|
10
12
|
import { registerRpc } from './rpc.js';
|
|
11
13
|
import { registerSkills } from './skill.js';
|
|
12
14
|
export const name = 'dsh-adb';
|
|
@@ -17,9 +19,11 @@ export const Config = Schema.object({
|
|
|
17
19
|
defaultSerial: Schema.string().description('默认目标设备 serial'),
|
|
18
20
|
timeoutMs: Schema.number().default(30000).description('adb 命令超时(毫秒)'),
|
|
19
21
|
baselineDir: Schema.string().description('adb_perf_baseline 基线存储目录;缺省 ~/.dsh/storages/dsh-adb'),
|
|
22
|
+
reportDir: Schema.string().description('adb_device_report 报告存储目录;缺省 ~/.dsh/storages/dsh-adb/reports'),
|
|
20
23
|
});
|
|
21
24
|
export function apply(ctx, config) {
|
|
22
25
|
const cfg = config;
|
|
26
|
+
const reportDir = config.reportDir ?? `${(config.baselineDir ?? DEFAULT_BASELINE_DIR).replace(/\\/g, '/')}/reports`;
|
|
23
27
|
registerDeviceTools(ctx, cfg);
|
|
24
28
|
registerInstallTool(ctx, cfg);
|
|
25
29
|
registerFileTool(ctx, cfg);
|
|
@@ -27,10 +31,12 @@ export function apply(ctx, config) {
|
|
|
27
31
|
registerPerfTool(ctx, cfg);
|
|
28
32
|
registerPerfBaselineTool(ctx, cfg, config.baselineDir ?? DEFAULT_BASELINE_DIR);
|
|
29
33
|
registerCrashReportTool(ctx, cfg);
|
|
34
|
+
registerDeviceReportTool(ctx, cfg, reportDir);
|
|
35
|
+
registerWaitTool(ctx, cfg);
|
|
30
36
|
registerSkills(ctx);
|
|
31
37
|
// The RPC channel needs the client connection, which mounts after this
|
|
32
38
|
// plugin starts in web compositions; register lazily so headless profiles
|
|
33
39
|
// (no connection) stay unaffected.
|
|
34
|
-
ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg));
|
|
35
|
-
ctx.logger.info('[dsh-adb] loaded:
|
|
40
|
+
ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg, reportDir));
|
|
41
|
+
ctx.logger.info('[dsh-adb] loaded: 11 tools + web device panel rpc');
|
|
36
42
|
}
|
package/lib/parsers/sysinfo.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare function parseGetprop(text: string): Record<string, string>;
|
|
|
4
4
|
export interface ProcessEntry {
|
|
5
5
|
pid: string;
|
|
6
6
|
name: string;
|
|
7
|
+
/** Resident set size in KB (Android `ps -A` column 4), when numeric. */
|
|
8
|
+
rss?: number;
|
|
7
9
|
}
|
|
8
10
|
/** Parse `ps -A` output (Android toybox: USER PID PPID VSZ RSS WCHAN ADDR S NAME). */
|
|
9
11
|
export declare function parseProcessList(text: string): ProcessEntry[];
|
package/lib/parsers/sysinfo.js
CHANGED
|
@@ -29,13 +29,15 @@ export function parseProcessList(text) {
|
|
|
29
29
|
if (line === '' || /^USER\s+PID/.test(line))
|
|
30
30
|
continue;
|
|
31
31
|
const parts = line.split(/\s+/);
|
|
32
|
-
// Android ps: index 1 = PID, last = NAME (comm may be in brackets)
|
|
32
|
+
// Android ps: index 1 = PID, index 4 = RSS, last = NAME (comm may be in brackets)
|
|
33
33
|
if (parts.length < 2)
|
|
34
34
|
continue;
|
|
35
35
|
const pid = parts[1];
|
|
36
36
|
const name = parts[parts.length - 1];
|
|
37
37
|
if (pid !== undefined && name !== undefined && /^\d+$/.test(pid)) {
|
|
38
|
-
|
|
38
|
+
const rssRaw = parts[4];
|
|
39
|
+
const rss = rssRaw !== undefined && /^\d+$/.test(rssRaw) ? Number(rssRaw) : undefined;
|
|
40
|
+
entries.push({ pid, name, ...(rss !== undefined ? { rss } : {}) });
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
return entries;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DeviceReport } from './report.js';
|
|
2
|
+
/**
|
|
3
|
+
* Device health report store: one JSON file per report under a configurable
|
|
4
|
+
* directory (`<baselineDir>/reports` by default). Read/write helpers are pure
|
|
5
|
+
* fs functions so the store is unit-testable like the baseline store.
|
|
6
|
+
* Filename: `<serial>--<epoch-ms>.json`; the id is the epoch prefix.
|
|
7
|
+
*/
|
|
8
|
+
export interface StoredReportMeta {
|
|
9
|
+
id: string;
|
|
10
|
+
collectedAt: string;
|
|
11
|
+
serial: string;
|
|
12
|
+
file: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const DEFAULT_REPORT_DIR: string;
|
|
15
|
+
export declare function reportFileFor(serial: string): string;
|
|
16
|
+
export declare function listReports(dir: string): StoredReportMeta[];
|
|
17
|
+
export declare function saveReport(dir: string, report: DeviceReport): StoredReportMeta;
|
|
18
|
+
export declare function loadReport(dir: string, file: string): DeviceReport;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
export const DEFAULT_REPORT_DIR = `${homedir().replace(/\\/g, '/')}/.dsh/storages/dsh-adb/reports`;
|
|
5
|
+
const FILE_PATTERN = /^(.+)--(\d+)\.json$/;
|
|
6
|
+
/** Parse a report filename back into its meta; undefined for foreign files. */
|
|
7
|
+
function parseMeta(file) {
|
|
8
|
+
const match = FILE_PATTERN.exec(file);
|
|
9
|
+
if (match === null)
|
|
10
|
+
return undefined;
|
|
11
|
+
return { id: match[2], collectedAt: '', serial: match[1], file };
|
|
12
|
+
}
|
|
13
|
+
export function reportFileFor(serial) {
|
|
14
|
+
const safeSerial = serial.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
15
|
+
return `${safeSerial}--${Date.now()}.json`;
|
|
16
|
+
}
|
|
17
|
+
export function listReports(dir) {
|
|
18
|
+
if (!existsSync(dir))
|
|
19
|
+
return [];
|
|
20
|
+
const metas = [];
|
|
21
|
+
for (const name of readdirSync(dir)) {
|
|
22
|
+
const meta = parseMeta(name);
|
|
23
|
+
if (meta !== undefined)
|
|
24
|
+
metas.push(meta);
|
|
25
|
+
}
|
|
26
|
+
// Newest first by epoch id.
|
|
27
|
+
return metas.sort((a, b) => (Number(a.id) < Number(b.id) ? 1 : -1));
|
|
28
|
+
}
|
|
29
|
+
export function saveReport(dir, report) {
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
const file = reportFileFor(report.serial);
|
|
33
|
+
writeFileSync(join(dir, file), JSON.stringify(report, null, 2), 'utf8');
|
|
34
|
+
return { id: String(Date.now()), collectedAt: report.collectedAt, serial: report.serial, file };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new Error(`report store unwritable at ${dir}: ${describe(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function loadReport(dir, file) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = readFileSync(join(dir, file), 'utf8');
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
if (parsed === null || typeof parsed !== 'object' || !Array.isArray(parsed.errors) || typeof parsed.collectedAt !== 'string') {
|
|
45
|
+
throw new Error('unexpected shape');
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new Error(`report store unreadable at ${join(dir, file)}: ${describe(error)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function describe(error) {
|
|
54
|
+
return error instanceof Error ? error.message : String(error);
|
|
55
|
+
}
|
package/lib/report.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type AdbConfig } from './adb.js';
|
|
3
|
+
import { type LogcatEntry, type LogLevel } from './parsers/logcat.js';
|
|
4
|
+
import { type ProcessEntry } from './parsers/sysinfo.js';
|
|
5
|
+
/**
|
|
6
|
+
* One-click device health report ("体检"): collect device identity, top
|
|
7
|
+
* processes, crash buffer, the worrying logcat window (W/E/F), and storage
|
|
8
|
+
* usage into one structured snapshot. Every section degrades independently —
|
|
9
|
+
* a failing section lands in `errors` instead of killing the whole report,
|
|
10
|
+
* so a half-alive device still yields whatever could be read.
|
|
11
|
+
*
|
|
12
|
+
* Evidence → signal: raw crash/logcat counts are noisy (boot markers, one
|
|
13
|
+
* mDNS error repeating thousands of times), so the report classifies crashes
|
|
14
|
+
* into real crashes vs. startup markers, aggregates logcat by tag, and emits a
|
|
15
|
+
* compact health summary the agent can reason from instead of 17k raw lines.
|
|
16
|
+
*/
|
|
17
|
+
export type ReportSection = 'device' | 'processes' | 'crash' | 'logcat' | 'storage';
|
|
18
|
+
export declare const REPORT_SECTIONS: readonly ReportSection[];
|
|
19
|
+
export interface ReportDeviceInfo {
|
|
20
|
+
model?: string;
|
|
21
|
+
manufacturer?: string;
|
|
22
|
+
release?: string;
|
|
23
|
+
sdk?: string;
|
|
24
|
+
fingerprint?: string;
|
|
25
|
+
resolution?: string;
|
|
26
|
+
memTotalKb?: number;
|
|
27
|
+
}
|
|
28
|
+
/** A real crash with the following same-pid lines (its stack trace) attached. */
|
|
29
|
+
export interface CrashChain {
|
|
30
|
+
signature: LogcatEntry;
|
|
31
|
+
following: LogcatEntry[];
|
|
32
|
+
}
|
|
33
|
+
export interface CrashSummary {
|
|
34
|
+
total: number;
|
|
35
|
+
realCrashCount: number;
|
|
36
|
+
bootMarkerCount: number;
|
|
37
|
+
otherCount: number;
|
|
38
|
+
/** Real crashes, each with its contiguous same-pid tail. */
|
|
39
|
+
chains: CrashChain[];
|
|
40
|
+
}
|
|
41
|
+
export interface TagAggregate {
|
|
42
|
+
tag: string;
|
|
43
|
+
level: LogLevel;
|
|
44
|
+
count: number;
|
|
45
|
+
sample: LogcatEntry;
|
|
46
|
+
}
|
|
47
|
+
export interface LogcatSummary {
|
|
48
|
+
total: number;
|
|
49
|
+
byTag: TagAggregate[];
|
|
50
|
+
}
|
|
51
|
+
export type HealthVerdict = 'ok' | 'attention';
|
|
52
|
+
export interface HealthSummary {
|
|
53
|
+
verdict: HealthVerdict;
|
|
54
|
+
/** Human/agent-readable lines: device, memory hogs, notable signals. */
|
|
55
|
+
lines: string[];
|
|
56
|
+
/** Concrete issues that drove the verdict (empty when ok). */
|
|
57
|
+
issues: string[];
|
|
58
|
+
}
|
|
59
|
+
export interface DeviceReport {
|
|
60
|
+
collectedAt: string;
|
|
61
|
+
serial: string;
|
|
62
|
+
device?: ReportDeviceInfo;
|
|
63
|
+
processes?: ProcessEntry[];
|
|
64
|
+
crashBuffer?: CrashSummary;
|
|
65
|
+
logcat?: LogcatSummary;
|
|
66
|
+
storage?: {
|
|
67
|
+
lines: number;
|
|
68
|
+
truncated: boolean;
|
|
69
|
+
excerpt: string;
|
|
70
|
+
};
|
|
71
|
+
health?: HealthSummary;
|
|
72
|
+
errors: Array<{
|
|
73
|
+
section: string;
|
|
74
|
+
message: string;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
export interface CollectDeviceReportArgs {
|
|
78
|
+
serial?: string;
|
|
79
|
+
include?: ReportSection[];
|
|
80
|
+
/** Cap for crash chains, logcat tag aggregates, and the process/df lists; defaults to 10. */
|
|
81
|
+
tail?: number;
|
|
82
|
+
}
|
|
83
|
+
export declare function isRealCrash(entry: LogcatEntry): boolean;
|
|
84
|
+
export declare function isBootMarker(entry: LogcatEntry): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Classify crash-buffer entries into real crashes vs. MediaTek boot markers
|
|
87
|
+
* vs. everything else, grouping each real crash with the contiguous same-pid
|
|
88
|
+
* lines that follow it (the stack trace).
|
|
89
|
+
*/
|
|
90
|
+
export declare function classifyCrashBuffer(entries: LogcatEntry[]): CrashSummary;
|
|
91
|
+
/**
|
|
92
|
+
* Aggregate logcat entries by tag+level, keeping one sample line per group and
|
|
93
|
+
* sorting by count descending. Turns "16987 lines" into "AOSP-MdnsDiscovery ×16200".
|
|
94
|
+
*/
|
|
95
|
+
export declare function aggregateByTag(entries: LogcatEntry[], topN?: number): LogcatSummary;
|
|
96
|
+
/**
|
|
97
|
+
* Compact health summary: verdict + a few lines the agent (or user) can reason
|
|
98
|
+
* from directly, instead of raw counts. Signals are intentionally coarse —
|
|
99
|
+
* the report is a triage surface, not a root-cause tool.
|
|
100
|
+
*/
|
|
101
|
+
export declare function buildHealthSummary(report: Pick<DeviceReport, 'device' | 'crashBuffer' | 'logcat' | 'processes'>): HealthSummary;
|
|
102
|
+
/** Collect the one-click device health report with per-section degradation. */
|
|
103
|
+
export declare function collectDeviceReport(ctx: Context, cfg: AdbConfig, signal: AbortSignal, args?: CollectDeviceReportArgs): Promise<DeviceReport>;
|
package/lib/report.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { classifyFailure, runAdb } from './adb.js';
|
|
2
|
+
import { matchesLevel, parseLogcat } from './parsers/logcat.js';
|
|
3
|
+
import { parseGetprop, parseMemTotal, parseProcessList, parseWmSize } from './parsers/sysinfo.js';
|
|
4
|
+
export const REPORT_SECTIONS = ['device', 'processes', 'crash', 'logcat', 'storage'];
|
|
5
|
+
function excerpt(text, maxLines) {
|
|
6
|
+
const lines = text.split(/\r?\n/).filter((line) => line !== '');
|
|
7
|
+
return {
|
|
8
|
+
lines: lines.length,
|
|
9
|
+
truncated: lines.length > maxLines,
|
|
10
|
+
excerpt: lines.slice(-maxLines).join('\n'),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
// ---- Evidence → signal pure helpers (unit-tested) ----
|
|
14
|
+
const REAL_CRASH = /FATAL EXCEPTION|Fatal signal|beginning of crash|SIGSEGV|SIGABRT|SIGBUS|SIGFPE|SIGILL|SIGTRAP/i;
|
|
15
|
+
const BOOT_MARKER = /mtk-brm-(?:commit|change|merge)-id|libimsma_adapt|SmartRatSwitch.*mtk-brm/i;
|
|
16
|
+
export function isRealCrash(entry) {
|
|
17
|
+
return REAL_CRASH.test(entry.message) || REAL_CRASH.test(entry.tag);
|
|
18
|
+
}
|
|
19
|
+
export function isBootMarker(entry) {
|
|
20
|
+
return BOOT_MARKER.test(entry.message) || BOOT_MARKER.test(entry.tag);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Classify crash-buffer entries into real crashes vs. MediaTek boot markers
|
|
24
|
+
* vs. everything else, grouping each real crash with the contiguous same-pid
|
|
25
|
+
* lines that follow it (the stack trace).
|
|
26
|
+
*/
|
|
27
|
+
export function classifyCrashBuffer(entries) {
|
|
28
|
+
const chains = [];
|
|
29
|
+
for (let i = 0; i < entries.length; i++) {
|
|
30
|
+
if (!isRealCrash(entries[i]))
|
|
31
|
+
continue;
|
|
32
|
+
const following = [];
|
|
33
|
+
const pid = entries[i].pid;
|
|
34
|
+
for (let j = i + 1; j < entries.length && entries[j].pid === pid; j++) {
|
|
35
|
+
following.push(entries[j]);
|
|
36
|
+
}
|
|
37
|
+
chains.push({ signature: entries[i], following });
|
|
38
|
+
}
|
|
39
|
+
const bootMarkerCount = entries.filter(isBootMarker).length;
|
|
40
|
+
return {
|
|
41
|
+
total: entries.length,
|
|
42
|
+
realCrashCount: chains.length,
|
|
43
|
+
bootMarkerCount,
|
|
44
|
+
otherCount: entries.length - chains.length - bootMarkerCount,
|
|
45
|
+
chains,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Aggregate logcat entries by tag+level, keeping one sample line per group and
|
|
50
|
+
* sorting by count descending. Turns "16987 lines" into "AOSP-MdnsDiscovery ×16200".
|
|
51
|
+
*/
|
|
52
|
+
export function aggregateByTag(entries, topN = 10) {
|
|
53
|
+
const counts = new Map();
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const key = `${entry.tag}\u0000${entry.level}`;
|
|
56
|
+
const existing = counts.get(key);
|
|
57
|
+
if (existing !== undefined) {
|
|
58
|
+
existing.count++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
counts.set(key, { tag: entry.tag, level: entry.level, count: 1, sample: entry });
|
|
62
|
+
}
|
|
63
|
+
const byTag = [...counts.values()].sort((a, b) => b.count - a.count).slice(0, topN);
|
|
64
|
+
return { total: entries.length, byTag };
|
|
65
|
+
}
|
|
66
|
+
const NETWORK_SIGNAL = /WifiHAL|NETWORK_ABNORMAL|MdnsDiscovery|multicast mDNS|sendto failed: EPERM/i;
|
|
67
|
+
const THERMAL_NOISE = /PowerKeeper\.Thermal/i;
|
|
68
|
+
/**
|
|
69
|
+
* Compact health summary: verdict + a few lines the agent (or user) can reason
|
|
70
|
+
* from directly, instead of raw counts. Signals are intentionally coarse —
|
|
71
|
+
* the report is a triage surface, not a root-cause tool.
|
|
72
|
+
*/
|
|
73
|
+
export function buildHealthSummary(report) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
const issues = [];
|
|
76
|
+
const device = report.device;
|
|
77
|
+
if (device) {
|
|
78
|
+
const parts = [device.model, `Android ${device.release ?? '?'}`].filter(Boolean);
|
|
79
|
+
if (device.memTotalKb !== undefined)
|
|
80
|
+
parts.push(`内存 ${Math.round(device.memTotalKb / 1024)}MB`);
|
|
81
|
+
lines.push(`设备:${parts.join(' · ')}`);
|
|
82
|
+
}
|
|
83
|
+
const crash = report.crashBuffer;
|
|
84
|
+
if (crash && crash.total > 0) {
|
|
85
|
+
if (crash.realCrashCount > 0) {
|
|
86
|
+
const tags = [...new Set(crash.chains.map((chain) => chain.signature.tag))].join(', ');
|
|
87
|
+
issues.push(`真实崩溃 ${crash.realCrashCount} 起(${tags})`);
|
|
88
|
+
lines.push(`崩溃:${crash.realCrashCount} 真实崩溃 + ${crash.bootMarkerCount} 启动标记 + ${crash.otherCount} 其他(共 ${crash.total})`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
lines.push(`崩溃:无真实崩溃(${crash.bootMarkerCount} 启动标记 + ${crash.otherCount} 其他,共 ${crash.total})`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const logcat = report.logcat;
|
|
95
|
+
if (logcat && logcat.total > 0) {
|
|
96
|
+
const top = logcat.byTag[0];
|
|
97
|
+
if (top)
|
|
98
|
+
lines.push(`W/E/F 日志:共 ${logcat.total} 条,主要来源 ${top.tag}(${top.level}) ×${top.count}${logcat.byTag.length > 1 ? ` 等 ${logcat.byTag.length} 个来源` : ''}`);
|
|
99
|
+
const net = logcat.byTag.find((agg) => NETWORK_SIGNAL.test(agg.tag) || NETWORK_SIGNAL.test(agg.sample.message));
|
|
100
|
+
if (net)
|
|
101
|
+
issues.push(`网络异常信号(${net.tag}: ${net.sample.message.slice(0, 80)})`);
|
|
102
|
+
const thermal = logcat.byTag.find((agg) => THERMAL_NOISE.test(agg.tag));
|
|
103
|
+
if (thermal)
|
|
104
|
+
lines.push('注:PowerKeeper.Thermal 为 MIUI 解析噪音,可忽略');
|
|
105
|
+
}
|
|
106
|
+
if (report.processes && report.processes.length > 0) {
|
|
107
|
+
const top = report.processes.slice(0, 3);
|
|
108
|
+
lines.push(`内存大户:${top.map((p) => `${p.name}(${p.rss}KB)`).join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
const verdict = issues.length > 0 ? 'attention' : 'ok';
|
|
111
|
+
return { verdict, lines, issues };
|
|
112
|
+
}
|
|
113
|
+
// ---- Section collectors ----
|
|
114
|
+
/** Device identity block: getprop + wm size + /proc/meminfo. */
|
|
115
|
+
async function collectDevice(ctx, cfg, signal, serial) {
|
|
116
|
+
const [getpropOut, sizeOut, memOut] = await Promise.all([
|
|
117
|
+
runAdb(ctx, cfg, ['shell', 'getprop'], { signal, serial, maxBytes: 2 * 1024 * 1024 }),
|
|
118
|
+
runAdb(ctx, cfg, ['shell', 'wm', 'size'], { signal, serial }),
|
|
119
|
+
runAdb(ctx, cfg, ['shell', 'cat', '/proc/meminfo'], { signal, serial }),
|
|
120
|
+
]);
|
|
121
|
+
for (const result of [getpropOut, sizeOut, memOut]) {
|
|
122
|
+
if (result.exitCode !== 0)
|
|
123
|
+
throw classifyFailure(result);
|
|
124
|
+
}
|
|
125
|
+
const props = parseGetprop(getpropOut.stdout);
|
|
126
|
+
const size = parseWmSize(sizeOut.stdout);
|
|
127
|
+
return {
|
|
128
|
+
model: props['ro.product.model'],
|
|
129
|
+
manufacturer: props['ro.product.manufacturer'],
|
|
130
|
+
release: props['ro.build.version.release'],
|
|
131
|
+
sdk: props['ro.build.version.sdk'],
|
|
132
|
+
fingerprint: props['ro.build.fingerprint'],
|
|
133
|
+
resolution: size === undefined ? undefined : `${size.width}x${size.height}`,
|
|
134
|
+
memTotalKb: parseMemTotal(memOut.stdout),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Top processes by RSS (memory hogs first). */
|
|
138
|
+
async function collectProcesses(ctx, cfg, signal, serial, tail) {
|
|
139
|
+
const result = await runAdb(ctx, cfg, ['shell', 'ps', '-A'], { signal, serial, maxBytes: 2 * 1024 * 1024 });
|
|
140
|
+
if (result.exitCode !== 0)
|
|
141
|
+
throw classifyFailure(result);
|
|
142
|
+
return parseProcessList(result.stdout)
|
|
143
|
+
.filter((entry) => entry.rss !== undefined)
|
|
144
|
+
.sort((a, b) => (b.rss ?? 0) - (a.rss ?? 0))
|
|
145
|
+
.slice(0, tail);
|
|
146
|
+
}
|
|
147
|
+
/** Crash buffer classified into real crashes vs. boot markers. */
|
|
148
|
+
async function collectCrash(ctx, cfg, signal, serial) {
|
|
149
|
+
const result = await runAdb(ctx, cfg, ['logcat', '-b', 'crash', '-v', 'threadtime', '-d'], { signal, serial, maxBytes: 8 * 1024 * 1024 });
|
|
150
|
+
if (result.exitCode !== 0)
|
|
151
|
+
throw classifyFailure(result);
|
|
152
|
+
return classifyCrashBuffer(parseLogcat(result.stdout));
|
|
153
|
+
}
|
|
154
|
+
/** The worrying logcat window: W/E/F entries from the main buffer, aggregated by tag. */
|
|
155
|
+
async function collectLogcat(ctx, cfg, signal, serial, tail) {
|
|
156
|
+
const result = await runAdb(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], { signal, serial, maxBytes: 8 * 1024 * 1024 });
|
|
157
|
+
if (result.exitCode !== 0)
|
|
158
|
+
throw classifyFailure(result);
|
|
159
|
+
const entries = parseLogcat(result.stdout).filter((entry) => matchesLevel(entry, 'W'));
|
|
160
|
+
return aggregateByTag(entries, tail);
|
|
161
|
+
}
|
|
162
|
+
/** Storage usage excerpt (`df`). */
|
|
163
|
+
async function collectStorage(ctx, cfg, signal, serial, tail) {
|
|
164
|
+
const result = await runAdb(ctx, cfg, ['shell', 'df'], { signal, serial, maxBytes: 1024 * 1024 });
|
|
165
|
+
if (result.exitCode !== 0)
|
|
166
|
+
throw classifyFailure(result);
|
|
167
|
+
return excerpt(result.stdout, tail);
|
|
168
|
+
}
|
|
169
|
+
/** Collect the one-click device health report with per-section degradation. */
|
|
170
|
+
export async function collectDeviceReport(ctx, cfg, signal, args = {}) {
|
|
171
|
+
const serial = args.serial ?? cfg.defaultSerial;
|
|
172
|
+
const tail = args.tail !== undefined && args.tail > 0 ? Math.floor(args.tail) : 10;
|
|
173
|
+
const include = args.include ?? [...REPORT_SECTIONS];
|
|
174
|
+
const report = { collectedAt: new Date().toISOString(), serial: serial ?? 'default', errors: [] };
|
|
175
|
+
const guard = async (section, collect, assign) => {
|
|
176
|
+
try {
|
|
177
|
+
assign(await collect());
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
report.errors.push({ section, message: error instanceof Error ? error.message : String(error) });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
await Promise.all([
|
|
184
|
+
include.includes('device') && guard('device', () => collectDevice(ctx, cfg, signal, serial), (v) => { report.device = v; }),
|
|
185
|
+
include.includes('processes') && guard('processes', () => collectProcesses(ctx, cfg, signal, serial, tail), (v) => { report.processes = v; }),
|
|
186
|
+
include.includes('crash') && guard('crash', () => collectCrash(ctx, cfg, signal, serial), (v) => { report.crashBuffer = v; }),
|
|
187
|
+
include.includes('logcat') && guard('logcat', () => collectLogcat(ctx, cfg, signal, serial, tail), (v) => { report.logcat = v; }),
|
|
188
|
+
include.includes('storage') && guard('storage', () => collectStorage(ctx, cfg, signal, serial, tail), (v) => { report.storage = v; }),
|
|
189
|
+
]);
|
|
190
|
+
report.health = buildHealthSummary(report);
|
|
191
|
+
return report;
|
|
192
|
+
}
|
package/lib/rpc.d.ts
CHANGED
|
@@ -9,10 +9,10 @@ export type RpcEndpointResult = {
|
|
|
9
9
|
message: string;
|
|
10
10
|
};
|
|
11
11
|
};
|
|
12
|
-
export declare function handleRpcEndpoint(ctx: Context, cfg: AdbConfig, endpoint: string, raw: unknown, signal: AbortSignal): Promise<RpcEndpointResult>;
|
|
12
|
+
export declare function handleRpcEndpoint(ctx: Context, cfg: AdbConfig, reportDir: string, endpoint: string, raw: unknown, signal: AbortSignal): Promise<RpcEndpointResult>;
|
|
13
13
|
/**
|
|
14
14
|
* Register the Package-private Client↔Host RPC for the Web device panel
|
|
15
15
|
* (conversation.view tab "设备"). Called lazily once the client connection
|
|
16
16
|
* mounts; headless compositions (no connection) stay unaffected.
|
|
17
17
|
*/
|
|
18
|
-
export declare function registerRpc(ctx: Context, cfg: AdbConfig): void;
|
|
18
|
+
export declare function registerRpc(ctx: Context, cfg: AdbConfig, reportDir: string): void;
|
package/lib/rpc.js
CHANGED
|
@@ -6,6 +6,8 @@ import { parseDevices } from './parsers/devices.js';
|
|
|
6
6
|
import { matchesKeyword, matchesLevel, parseLogcat } from './parsers/logcat.js';
|
|
7
7
|
import { parseGetprop, parseMemTotal, parsePackageList, parseProcessList, parseWmSize, } from './parsers/sysinfo.js';
|
|
8
8
|
import { capturePerfSnapshot } from './tools/perf.js';
|
|
9
|
+
import { collectDeviceReport, REPORT_SECTIONS } from './report.js';
|
|
10
|
+
import { saveReport } from './report-store.js';
|
|
9
11
|
async function requireDevice(ctx, cfg, argv, options) {
|
|
10
12
|
const result = await runAdb(ctx, cfg, argv, { signal: options.signal, serial: options.serial, maxBytes: options.maxBytes });
|
|
11
13
|
if (result.exitCode !== 0)
|
|
@@ -15,7 +17,7 @@ async function requireDevice(ctx, cfg, argv, options) {
|
|
|
15
17
|
function serialOf(payload) {
|
|
16
18
|
return typeof payload.serial === 'string' ? payload.serial : undefined;
|
|
17
19
|
}
|
|
18
|
-
export async function handleRpcEndpoint(ctx, cfg, endpoint, raw, signal) {
|
|
20
|
+
export async function handleRpcEndpoint(ctx, cfg, reportDir, endpoint, raw, signal) {
|
|
19
21
|
try {
|
|
20
22
|
const payload = (raw ?? {});
|
|
21
23
|
switch (endpoint) {
|
|
@@ -118,6 +120,15 @@ export async function handleRpcEndpoint(ctx, cfg, endpoint, raw, signal) {
|
|
|
118
120
|
const snapshot = await capturePerfSnapshot(ctx, cfg, signal, { package: pkg, serial: serialOf(payload), metrics: ['meminfo', 'battery'] });
|
|
119
121
|
return { ok: true, value: { package: pkg, meminfo: snapshot.meminfo, battery: snapshot.battery } };
|
|
120
122
|
}
|
|
123
|
+
case 'deviceReport': {
|
|
124
|
+
const include = Array.isArray(payload.include)
|
|
125
|
+
? payload.include.filter((item) => typeof item === 'string' && REPORT_SECTIONS.includes(item))
|
|
126
|
+
: undefined;
|
|
127
|
+
const tail = typeof payload.tail === 'number' ? payload.tail : undefined;
|
|
128
|
+
const report = await collectDeviceReport(ctx, cfg, signal, { serial: serialOf(payload), include, tail });
|
|
129
|
+
const saved = saveReport(reportDir, report);
|
|
130
|
+
return { ok: true, value: { ...report, savedTo: saved.file } };
|
|
131
|
+
}
|
|
121
132
|
default:
|
|
122
133
|
return { ok: false, error: { message: `unknown endpoint: ${endpoint}` } };
|
|
123
134
|
}
|
|
@@ -131,12 +142,12 @@ export async function handleRpcEndpoint(ctx, cfg, endpoint, raw, signal) {
|
|
|
131
142
|
* (conversation.view tab "设备"). Called lazily once the client connection
|
|
132
143
|
* mounts; headless compositions (no connection) stay unaffected.
|
|
133
144
|
*/
|
|
134
|
-
export function registerRpc(ctx, cfg) {
|
|
145
|
+
export function registerRpc(ctx, cfg, reportDir) {
|
|
135
146
|
const connection = ctx.get('connection');
|
|
136
147
|
const rpc = connection?.rpc;
|
|
137
148
|
if (rpc === undefined)
|
|
138
149
|
return;
|
|
139
|
-
ctx.effect(() => rpc.handle('/dsh-adb', (endpoint, raw, signal) => handleRpcEndpoint(ctx, cfg, endpoint, raw, signal),
|
|
150
|
+
ctx.effect(() => rpc.handle('/dsh-adb', (endpoint, raw, signal) => handleRpcEndpoint(ctx, cfg, reportDir, endpoint, raw, signal),
|
|
140
151
|
// Browser-only channel: accept requests from the loopback web GUI.
|
|
141
152
|
{ authority: 'loopback' }));
|
|
142
153
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type AdbConfig } from '../adb.js';
|
|
3
|
+
/**
|
|
4
|
+
* adb_device_report: one-click device health check ("体检"). Collects device
|
|
5
|
+
* identity, top RSS processes, crash buffer, the W/E/F logcat window, and
|
|
6
|
+
* storage usage into one structured report, persists it under the report
|
|
7
|
+
* store, and returns it for the agent to diagnose (pair with the
|
|
8
|
+
* dsh-adb-crash-analysis skill).
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerDeviceReportTool(ctx: Context, cfg: AdbConfig, reportDir: string): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsonOutput } from '../adb.js';
|
|
2
|
+
import { collectDeviceReport, REPORT_SECTIONS } from '../report.js';
|
|
3
|
+
import { saveReport } from '../report-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* adb_device_report: one-click device health check ("体检"). Collects device
|
|
6
|
+
* identity, top RSS processes, crash buffer, the W/E/F logcat window, and
|
|
7
|
+
* storage usage into one structured report, persists it under the report
|
|
8
|
+
* store, and returns it for the agent to diagnose (pair with the
|
|
9
|
+
* dsh-adb-crash-analysis skill).
|
|
10
|
+
*/
|
|
11
|
+
export function registerDeviceReportTool(ctx, cfg, reportDir) {
|
|
12
|
+
ctx.tools.register({
|
|
13
|
+
name: 'adb_device_report',
|
|
14
|
+
description: 'One-click device health report: collect device identity, top memory processes, the crash buffer (classified into real crashes vs. boot markers, with stack chains), the warning/error logcat window (aggregated by tag), storage usage, and a compact health summary (verdict + issues) into one structured snapshot, persist it to the report store, and return it. Each section degrades independently — a failing section lands in errors instead of failing the whole report. Pair the result with the dsh-adb-crash-analysis skill to diagnose the device state.',
|
|
15
|
+
parameters: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
properties: {
|
|
19
|
+
serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
|
|
20
|
+
include: {
|
|
21
|
+
type: 'array',
|
|
22
|
+
items: { type: 'string', enum: [...REPORT_SECTIONS] },
|
|
23
|
+
description: 'Which sections to collect; defaults to all five (device, processes, crash, logcat, storage).',
|
|
24
|
+
},
|
|
25
|
+
tail: { type: 'integer', description: 'Cap for crash/logcat entries and the process/storage lists; defaults to 100.' },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
output: jsonOutput(),
|
|
29
|
+
async execute(args, exec) {
|
|
30
|
+
const report = await collectDeviceReport(ctx, cfg, exec.signal, args);
|
|
31
|
+
let saved;
|
|
32
|
+
try {
|
|
33
|
+
saved = saveReport(reportDir, report);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Persistence is best-effort: the report is still returned to the agent.
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
collectedAt: report.collectedAt,
|
|
40
|
+
serial: report.serial,
|
|
41
|
+
...(report.device !== undefined ? { device: report.device } : {}),
|
|
42
|
+
...(report.processes !== undefined ? { topProcesses: report.processes } : {}),
|
|
43
|
+
...(report.crashBuffer !== undefined ? { crashBuffer: report.crashBuffer } : {}),
|
|
44
|
+
...(report.logcat !== undefined ? { logcat: report.logcat } : {}),
|
|
45
|
+
...(report.storage !== undefined ? { storage: report.storage } : {}),
|
|
46
|
+
...(report.health !== undefined ? { health: report.health } : {}),
|
|
47
|
+
errors: report.errors,
|
|
48
|
+
...(saved !== undefined ? { savedTo: saved.file } : {}),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type AdbConfig } from '../adb.js';
|
|
3
|
+
import { type AdbDevice } from '../parsers/devices.js';
|
|
4
|
+
import { type LogcatEntry } from '../parsers/logcat.js';
|
|
5
|
+
import { type ProcessEntry } from '../parsers/sysinfo.js';
|
|
6
|
+
/**
|
|
7
|
+
* adb_wait_for: wait until a device reaches a condition, instead of sleeping a
|
|
8
|
+
* fixed number of seconds. Conditions: device-online, boot-complete, process
|
|
9
|
+
* (a process appears), logcat-pattern (a keyword appears). Polls at
|
|
10
|
+
* `intervalMs` until `timeoutMs`, then returns `matched: false` (not an error)
|
|
11
|
+
* so the agent can react to the timeout instead of guessing.
|
|
12
|
+
*/
|
|
13
|
+
export type WaitCondition = 'device-online' | 'boot-complete' | 'process' | 'logcat-pattern';
|
|
14
|
+
export interface WaitArgs {
|
|
15
|
+
serial?: string;
|
|
16
|
+
condition: WaitCondition;
|
|
17
|
+
/** Substring matched against process names (condition=process) or logcat tag/message (condition=logcat-pattern). */
|
|
18
|
+
pattern?: string;
|
|
19
|
+
/** Overall wait budget in milliseconds; defaults to 30000, capped at 300000. */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Poll interval in milliseconds; defaults to 1000. */
|
|
22
|
+
intervalMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface WaitResult {
|
|
25
|
+
condition: WaitCondition;
|
|
26
|
+
matched: boolean;
|
|
27
|
+
waitedMs: number;
|
|
28
|
+
attempts: number;
|
|
29
|
+
reason?: string;
|
|
30
|
+
}
|
|
31
|
+
/** device-online: the serial is present in `adb devices -l` with state `device`. */
|
|
32
|
+
export declare function checkDeviceOnline(devices: AdbDevice[], serial?: string): boolean;
|
|
33
|
+
/** boot-complete: `getprop sys.boot_completed` output is exactly "1". */
|
|
34
|
+
export declare function checkBootComplete(getpropOut: string): boolean;
|
|
35
|
+
/** process: any process name contains the pattern. */
|
|
36
|
+
export declare function checkProcessPresent(processes: ProcessEntry[], pattern: string): boolean;
|
|
37
|
+
/** logcat-pattern: any entry's tag or message contains the keyword. */
|
|
38
|
+
export declare function checkLogcatKeyword(entries: LogcatEntry[], keyword: string): boolean;
|
|
39
|
+
/** Wait until the condition holds or the budget expires (returns matched:false on timeout). */
|
|
40
|
+
export declare function waitForCondition(ctx: Context, cfg: AdbConfig, signal: AbortSignal, args: WaitArgs): Promise<WaitResult>;
|
|
41
|
+
/** adb_wait_for: wait until a device condition holds (online / boot complete / process / logcat keyword). */
|
|
42
|
+
export declare function registerWaitTool(ctx: Context, cfg: AdbConfig): void;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { classifyFailure, jsonOutput, runAdb } from '../adb.js';
|
|
2
|
+
import { parseDevices } from '../parsers/devices.js';
|
|
3
|
+
import { parseLogcat } from '../parsers/logcat.js';
|
|
4
|
+
import { parseProcessList } from '../parsers/sysinfo.js';
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
|
+
const MAX_TIMEOUT_MS = 300_000;
|
|
7
|
+
const DEFAULT_INTERVAL_MS = 1_000;
|
|
8
|
+
// ---- Pure condition checks (unit-tested) ----
|
|
9
|
+
/** device-online: the serial is present in `adb devices -l` with state `device`. */
|
|
10
|
+
export function checkDeviceOnline(devices, serial) {
|
|
11
|
+
return devices.some((device) => {
|
|
12
|
+
if (serial !== undefined && device.serial !== serial)
|
|
13
|
+
return false;
|
|
14
|
+
return device.state === 'device';
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** boot-complete: `getprop sys.boot_completed` output is exactly "1". */
|
|
18
|
+
export function checkBootComplete(getpropOut) {
|
|
19
|
+
return getpropOut.trim() === '1';
|
|
20
|
+
}
|
|
21
|
+
/** process: any process name contains the pattern. */
|
|
22
|
+
export function checkProcessPresent(processes, pattern) {
|
|
23
|
+
return processes.some((entry) => entry.name.includes(pattern));
|
|
24
|
+
}
|
|
25
|
+
/** logcat-pattern: any entry's tag or message contains the keyword. */
|
|
26
|
+
export function checkLogcatKeyword(entries, keyword) {
|
|
27
|
+
return entries.some((entry) => entry.tag.includes(keyword) || entry.message.includes(keyword));
|
|
28
|
+
}
|
|
29
|
+
// ---- Poll loop ----
|
|
30
|
+
function sleep(ms, signal) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
if (signal?.aborted) {
|
|
33
|
+
reject(new Error('tool call aborted'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const timer = setTimeout(resolve, ms);
|
|
37
|
+
signal?.addEventListener('abort', () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
reject(new Error('tool call aborted'));
|
|
40
|
+
}, { once: true });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Run one adb probe for the condition; returns true when satisfied. */
|
|
44
|
+
async function probe(ctx, cfg, args, signal) {
|
|
45
|
+
const serial = args.serial;
|
|
46
|
+
switch (args.condition) {
|
|
47
|
+
case 'device-online': {
|
|
48
|
+
const result = await runAdb(ctx, cfg, ['devices', '-l'], { signal, maxBytes: 1024 * 1024 });
|
|
49
|
+
if (result.exitCode !== 0)
|
|
50
|
+
throw classifyFailure(result);
|
|
51
|
+
return checkDeviceOnline(parseDevices(result.stdout), args.serial);
|
|
52
|
+
}
|
|
53
|
+
case 'boot-complete': {
|
|
54
|
+
const result = await runAdb(ctx, cfg, ['shell', 'getprop', 'sys.boot_completed'], { signal, serial });
|
|
55
|
+
if (result.exitCode !== 0)
|
|
56
|
+
throw classifyFailure(result);
|
|
57
|
+
return checkBootComplete(result.stdout);
|
|
58
|
+
}
|
|
59
|
+
case 'process': {
|
|
60
|
+
const result = await runAdb(ctx, cfg, ['shell', 'ps', '-A'], { signal, serial, maxBytes: 2 * 1024 * 1024 });
|
|
61
|
+
if (result.exitCode !== 0)
|
|
62
|
+
throw classifyFailure(result);
|
|
63
|
+
return checkProcessPresent(parseProcessList(result.stdout), args.pattern);
|
|
64
|
+
}
|
|
65
|
+
case 'logcat-pattern': {
|
|
66
|
+
const result = await runAdb(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], { signal, serial, maxBytes: 8 * 1024 * 1024 });
|
|
67
|
+
if (result.exitCode !== 0)
|
|
68
|
+
throw classifyFailure(result);
|
|
69
|
+
return checkLogcatKeyword(parseLogcat(result.stdout), args.pattern);
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`unknown condition: ${String(args.condition)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Validate args that fail fast — before the poll loop so they surface as errors, not timeouts. */
|
|
76
|
+
function validateArgs(args) {
|
|
77
|
+
if (!['device-online', 'boot-complete', 'process', 'logcat-pattern'].includes(args.condition)) {
|
|
78
|
+
throw new Error(`unknown condition: ${String(args.condition)}`);
|
|
79
|
+
}
|
|
80
|
+
if ((args.condition === 'process' || args.condition === 'logcat-pattern') && (args.pattern === undefined || args.pattern === '')) {
|
|
81
|
+
throw new Error(`condition "${args.condition}" requires a non-empty "pattern"`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Wait until the condition holds or the budget expires (returns matched:false on timeout). */
|
|
85
|
+
export async function waitForCondition(ctx, cfg, signal, args) {
|
|
86
|
+
validateArgs(args);
|
|
87
|
+
const timeoutMs = Math.min(Math.floor(args.timeoutMs ?? DEFAULT_TIMEOUT_MS), MAX_TIMEOUT_MS);
|
|
88
|
+
const intervalMs = Math.max(Math.floor(args.intervalMs ?? DEFAULT_INTERVAL_MS), 250);
|
|
89
|
+
const start = Date.now();
|
|
90
|
+
const deadline = start + timeoutMs;
|
|
91
|
+
let attempts = 0;
|
|
92
|
+
let lastProbe;
|
|
93
|
+
while (true) {
|
|
94
|
+
attempts++;
|
|
95
|
+
try {
|
|
96
|
+
const satisfied = await probe(ctx, cfg, args, signal);
|
|
97
|
+
if (satisfied) {
|
|
98
|
+
return { condition: args.condition, matched: true, waitedMs: Date.now() - start, attempts };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// A transient probe failure (device offline mid-wait) is not terminal:
|
|
103
|
+
// keep polling until the budget, but record the reason.
|
|
104
|
+
lastProbe = error instanceof Error ? error.message : String(error);
|
|
105
|
+
}
|
|
106
|
+
if (Date.now() >= deadline) {
|
|
107
|
+
return {
|
|
108
|
+
condition: args.condition,
|
|
109
|
+
matched: false,
|
|
110
|
+
waitedMs: timeoutMs,
|
|
111
|
+
attempts,
|
|
112
|
+
...(lastProbe !== undefined ? { reason: lastProbe } : { reason: `condition not met within ${timeoutMs}ms` }),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
await sleep(Math.min(intervalMs, deadline - Date.now()), signal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** adb_wait_for: wait until a device condition holds (online / boot complete / process / logcat keyword). */
|
|
119
|
+
export function registerWaitTool(ctx, cfg) {
|
|
120
|
+
ctx.tools.register({
|
|
121
|
+
name: 'adb_wait_for',
|
|
122
|
+
description: 'Wait until a device reaches a condition, then return — instead of sleeping a fixed number of seconds. Conditions: device-online (device is in `adb devices` with state `device`), boot-complete (sys.boot_completed=1), process (a process whose name contains `pattern` appears in ps), logcat-pattern (a keyword appears in logcat tag/message). Polls every `intervalMs` up to `timeoutMs`; on timeout returns matched:false (not an error) so you can react. Use it to sequence multi-step device flows: install → wait for process → snapshot.',
|
|
123
|
+
parameters: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
required: ['condition'],
|
|
127
|
+
properties: {
|
|
128
|
+
condition: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
enum: ['device-online', 'boot-complete', 'process', 'logcat-pattern'],
|
|
131
|
+
description: 'Which condition to wait for.',
|
|
132
|
+
},
|
|
133
|
+
serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial. For device-online, omit serial to wait for ANY device to come online.' },
|
|
134
|
+
pattern: { type: 'string', description: 'Required for process (process-name substring) and logcat-pattern (keyword).' },
|
|
135
|
+
timeoutMs: { type: 'integer', description: 'Overall wait budget in milliseconds; defaults to 30000, capped at 300000.' },
|
|
136
|
+
intervalMs: { type: 'integer', description: 'Poll interval in milliseconds; defaults to 1000, minimum 250.' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
output: jsonOutput(),
|
|
140
|
+
async execute(args, exec) {
|
|
141
|
+
return waitForCondition(ctx, cfg, exec.signal, args);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-adb",
|
|
3
|
-
"version": "1.
|
|
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 (autocomplete, live logcat, profiler)",
|
|
3
|
+
"version": "1.3.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, one-click device health reports, condition waits, web device panel (autocomplete, live logcat, profiler)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|