dsh-adb 1.1.0 → 1.2.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
@@ -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, and performance snapshot — plus harness synergy: **send any logcat/snapshot 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).
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.0.0)
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,7 @@ 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` |
42
43
 
43
44
  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
45
 
@@ -61,6 +62,7 @@ Set the `config` block in `cordis.patch.yml` (or a profile patch):
61
62
  | `defaultSerial` | Default target device serial | none |
62
63
  | `timeoutMs` | Per-command timeout | 30000 |
63
64
  | `baselineDir` | Directory for `adb_perf_baseline` storage | `~/.dsh/storages/dsh-adb` |
65
+ | `reportDir` | Directory for `adb_device_report` storage | `<baselineDir>/reports` |
64
66
 
65
67
  ## Development
66
68
 
@@ -83,6 +85,7 @@ npm pack --dry-run # verify publish contents (lib/ + cordis.patch.yml)
83
85
  - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.en.md](docs/REQUIREMENTS.en.md) — purpose / scope / non-goals / acceptance criteria
84
86
  - [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.en.md](docs/TESTING.en.md) — testing philosophy, three test layers, E2E steps, regression checklist
85
87
  - [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
88
+ - [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
89
  - [PLAN.md](PLAN.md) / [PLAN.en.md](PLAN.en.md) — milestones & backlog
87
90
 
88
91
  ## 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、性能快照。面向实车与台架联调场景,业务内通用(不限 Unity、不限具体车机协议)。
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.0.0)
17
+ ## Web 设备面板(v1.2.0)
18
18
 
19
- 会话视图页签「设备」(与 chat/轨迹/任务管理并列):设备列表/状态、按包名的性能快照(内存/帧率/电量)、过滤 logcat 尾部。数据走 Package RPC;需装入 web profile 并重启 GUI 生效。
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.0.0)
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,7 @@ 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` |
42
43
 
43
44
  错误码:`ADB_NOT_FOUND`、`ADB_UNAVAILABLE`、`DEVICE_NOT_FOUND`、`NO_DEVICES`、`CONNECT_FAILED`、`INSTALL_FAILED`、`ADB_EXIT_<code>` 等,均为结构化 `AdbError`。
44
45
 
@@ -61,6 +62,7 @@ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
61
62
  | `defaultSerial` | 默认设备 serial | 无 |
62
63
  | `timeoutMs` | 命令超时 | 30000 |
63
64
  | `baselineDir` | `adb_perf_baseline` 基线存储目录 | `~/.dsh/storages/dsh-adb` |
65
+ | `reportDir` | `adb_device_report` 体检报告存储目录 | `<baselineDir>/reports` |
64
66
 
65
67
  ## 开发
66
68
 
@@ -85,6 +87,7 @@ npm pack --dry-run # 校验发布包内容(lib/ + cordis.patch.yml)
85
87
  - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.en.md](docs/REQUIREMENTS.en.md) — 目的/范围/非目标/验收标准
86
88
  - [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.en.md](docs/TESTING.en.md) — 测试哲学(提交即测)、三层测试方法、E2E 步骤、回归清单
87
89
  - [docs/DEVELOPMENT-LOG.md](docs/DEVELOPMENT-LOG.md) / [docs/DEVELOPMENT-LOG.en.md](docs/DEVELOPMENT-LOG.en.md) — 进度时间线、4 个已修复 bug 教训、环境/生态经验
90
+ - [docs/ROADMAP.md](docs/ROADMAP.md) — harness×adb 协同功能路线图(诊断报告/崩溃归因/截图视觉/台架自动化测试/等待原语/审批/多设备对比/定时巡检/回滚台账)
88
91
  - [PLAN.md](PLAN.md) / [PLAN.en.md](PLAN.en.md) — 里程碑与待办
89
92
 
90
93
  ## 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,6 +2,7 @@ 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';
@@ -17,9 +18,11 @@ export const Config = Schema.object({
17
18
  defaultSerial: Schema.string().description('默认目标设备 serial'),
18
19
  timeoutMs: Schema.number().default(30000).description('adb 命令超时(毫秒)'),
19
20
  baselineDir: Schema.string().description('adb_perf_baseline 基线存储目录;缺省 ~/.dsh/storages/dsh-adb'),
21
+ reportDir: Schema.string().description('adb_device_report 报告存储目录;缺省 ~/.dsh/storages/dsh-adb/reports'),
20
22
  });
21
23
  export function apply(ctx, config) {
22
24
  const cfg = config;
25
+ const reportDir = config.reportDir ?? `${(config.baselineDir ?? DEFAULT_BASELINE_DIR).replace(/\\/g, '/')}/reports`;
23
26
  registerDeviceTools(ctx, cfg);
24
27
  registerInstallTool(ctx, cfg);
25
28
  registerFileTool(ctx, cfg);
@@ -27,10 +30,11 @@ export function apply(ctx, config) {
27
30
  registerPerfTool(ctx, cfg);
28
31
  registerPerfBaselineTool(ctx, cfg, config.baselineDir ?? DEFAULT_BASELINE_DIR);
29
32
  registerCrashReportTool(ctx, cfg);
33
+ registerDeviceReportTool(ctx, cfg, reportDir);
30
34
  registerSkills(ctx);
31
35
  // The RPC channel needs the client connection, which mounts after this
32
36
  // plugin starts in web compositions; register lazily so headless profiles
33
37
  // (no connection) stay unaffected.
34
- ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg));
35
- ctx.logger.info('[dsh-adb] loaded: 9 tools + web device panel rpc');
38
+ ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg, reportDir));
39
+ ctx.logger.info('[dsh-adb] loaded: 10 tools + web device panel rpc');
36
40
  }
@@ -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[];
@@ -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
- entries.push({ pid, name });
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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-adb",
3
- "version": "1.1.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 (autocomplete, live logcat, profiler)",
3
+ "version": "1.2.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, web device panel (autocomplete, live logcat, profiler)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {