dsh-adb 1.6.0 → 1.7.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, 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).
19
+ A **Devices** tab (UI label 设备) 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** (UI label 设备体检 / Device Checkup: 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.1.0)
23
+ - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` published (latest: 1.6.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**
@@ -44,9 +44,57 @@ Topics: `dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
44
44
  | `adb_operation_ledger` | Append-only device operation ledger (record/list/rollback): record installs/pushes/etc., list history, or roll an app back to its last known-good APK (`adb install -r`); persisted as `operations.json` — the trust base for agent-driven device modification |
45
45
  | `adb_screenshot` | Capture the device screen into a local PNG (screencap → pull), returning the saved path, byte size, and pixel dimensions — durable evidence for crash scenes / UI states / test frames |
46
46
  | `adb_watch_crash` | Watch the crash buffer for NEW real crashes (foreground poll or background job; boot markers ignored; `matched:false` on timeout) — the first link of the monitor→capture→attribute chain |
47
+ | `adb_patrol_check` | One-click patrol: crash scan (real vs. boot markers) + perf vs. the latest stored baseline (regressions beyond a threshold) + battery/temperature/storage → compact verdict (`ok`/`attention`) with concrete issues; report persisted under `<reportDir>/patrol`. `compareToLast:true` attaches a delta vs. the previous stored patrol (new/gone crashes, worsening regressions, verdict transition). Fail-closed: a section that cannot be collected is itself an issue. No baseline → comparison skipped with a note; omit `package` → the perf section is skipped. Schedule it unattended with dsh-automation — see [docs/SCHEDULED-PATROL.md](docs/SCHEDULED-PATROL.md) |
47
48
 
48
49
  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.
49
50
 
51
+ ## Usage: adb_watch_crash (Crash Watchdog)
52
+
53
+ Watch the device's crash buffer for **new** real crashes. On start, it reads the current buffer and remembers every existing crash signature (seed), so only crashes that appear *after* the watch begins are reported. Boot markers (`mtk-brm-*`) are not crashes and are ignored.
54
+
55
+ **Foreground mode** (default) — blocks until a new crash appears or the budget expires:
56
+
57
+ ```json
58
+ // Agent calls:
59
+ { "name": "adb_watch_crash", "args": { "timeoutMs": 60000 } }
60
+
61
+ // New crash detected:
62
+ { "matched": true, "waitedMs": 3200, "crashes": [{ "time": "08-26 06:22:56.714", "pid": "2947", "tag": "AndroidRuntime", "message": "FATAL EXCEPTION: WM.task-1", "stack": ["AndroidRuntime: Process: com.miui.weather2, PID: 2947", "..."] }] }
63
+
64
+ // No new crash within budget (not an error):
65
+ { "matched": false, "waitedMs": 60000, "crashes": [], "reason": "no new crash within 60000ms" }
66
+ ```
67
+
68
+ **Background mode** (`run_in_background`) — returns a job id immediately, keeps polling:
69
+
70
+ ```json
71
+ // Start watching in the background:
72
+ { "name": "adb_watch_crash", "args": { "run_in_background": true, "timeoutMs": 300000 } }
73
+ // → { "kind": "background", "jobId": "adb-watch-crash-1" }
74
+
75
+ // Read latest detections with job_output:
76
+ // [adb_watch_crash] detected 1 new crash(es) after 5400ms
77
+ // - 08-26 06:22:56.714 pid=2947 AndroidRuntime: FATAL EXCEPTION: WM.task-1
78
+ // AndroidRuntime: Process: com.miui.weather2, PID: 2947
79
+ // ...
80
+ ```
81
+
82
+ **Chain with crash capture** — when a crash is detected, pair with `adb_crash_report` and `adb_screenshot` to capture the scene:
83
+
84
+ ```
85
+ 1. adb_watch_crash (watch for new crash)
86
+ 2. → crash detected → adb_crash_report (full crash scene: buffer + dropbox + process + memory)
87
+ 3. → adb_screenshot (screen state at crash time)
88
+ ```
89
+
90
+ | Parameter | Type | Default | Description |
91
+ | --- | --- | --- | --- |
92
+ | `serial` | string | defaultSerial | Target device serial |
93
+ | `timeoutMs` | integer | 60000 | Watch budget (max 600000 = 10 min) |
94
+ | `intervalMs` | integer | 1000 | Poll interval (min 250) |
95
+ | `withStacks` | boolean | true | Include same-pid stack lines per crash |
96
+ | `run_in_background` | boolean | false | Run as a background job; read via `job_output` |
97
+
50
98
  ## Configuration
51
99
 
52
100
  Set the `config` block in `cordis.patch.yml` (or a profile patch):
@@ -81,17 +129,17 @@ npm pack --dry-run # verify publish contents (lib/ + cordis.patch.yml)
81
129
  ## Testing & Verification
82
130
 
83
131
  - Principle: **ship only what is tested** — every committed feature has unit and/or end-to-end coverage.
84
- - Verified on: Android 13 automotive bench + Android 13 phone.
132
+ - Verified on: Android 13 automotive bench + Android 14 phone (Redmi K50 Pro) + emulator.
85
133
  - Per-version changes and verification: [CHANGELOG.md](CHANGELOG.md); test methodology and coverage: [docs/TESTING.md](docs/TESTING.md) (Chinese).
86
134
 
87
135
  ## Project Docs (bilingual; for AI agents & contributors)
88
136
 
89
- - [docs/AGENTS.md](docs/AGENTS.md) / [docs/AGENTS.en.md](docs/AGENTS.en.md) — read first: purpose, rules, commands, environment facts, doc map
90
- - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.en.md](docs/REQUIREMENTS.en.md) — purpose / scope / non-goals / acceptance criteria
91
- - [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.en.md](docs/TESTING.en.md) — testing philosophy, three test layers, E2E steps, regression checklist
92
- - [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
93
- - [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)
94
- - [PLAN.md](PLAN.md) / [PLAN.en.md](PLAN.en.md) — milestones & backlog
137
+ - [docs/AGENTS.md](docs/AGENTS.md) / [docs/AGENTS.zh-CN.md](docs/AGENTS.zh-CN.md) — read first: purpose, rules, commands, environment facts, doc map
138
+ - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.zh-CN.md](docs/REQUIREMENTS.zh-CN.md) — purpose / scope / non-goals / acceptance criteria
139
+ - [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.zh-CN.md](docs/TESTING.zh-CN.md) — testing philosophy, three test layers, E2E steps, regression checklist
140
+ - [docs/ROADMAP.md](docs/ROADMAP.md) / [docs/ROADMAP.zh-CN.md](docs/ROADMAP.zh-CN.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)
141
+ - [docs/SCHEDULED-PATROL.md](docs/SCHEDULED-PATROL.md) / [docs/SCHEDULED-PATROL.zh-CN.md](docs/SCHEDULED-PATROL.zh-CN.md) run the patrol unattended on a schedule (dsh-automation prompt template + gotchas)
142
+ - [PLAN.md](PLAN.md) / [PLAN.zh-CN.md](PLAN.zh-CN.md) — milestones & backlog
95
143
 
96
144
  ## License
97
145
 
package/README.zh-CN.md CHANGED
@@ -20,7 +20,7 @@ dsh plugin --profile web add dsh-adb
20
20
 
21
21
  ## 生态收录
22
22
 
23
- - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 1.1.0)
23
+ - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 1.6.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) — **已合并**
@@ -44,9 +44,57 @@ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
44
44
  | `adb_operation_ledger` | 操作回滚台账(record/list/rollback):追加记录安装/推送等操作,查询历史,或回滚到最近一次成功安装的 APK(`adb install -r`);落盘 `operations.json` —— agent 自主改设备的信任基础 |
45
45
  | `adb_screenshot` | 截图落盘 PNG(screencap → pull),返回路径/字节数/像素尺寸 —— 崩溃现场、UI 状态、测试前后对比的持久证据 |
46
46
  | `adb_watch_crash` | 崩溃看门狗:监控 crash buffer 的**新**真实崩溃(前台轮询或后台 job;启动标记不算;超时返回 `matched:false`)—— 「盯崩溃→采集→归因」链第一环 |
47
+ | `adb_patrol_check` | 一键巡检:崩溃扫描(真实 vs 启动标记)+ 性能 vs 最近基线(超阈值的回归)+ 电池/温度/存储 → 紧凑结论(`ok`/`attention`)+ 具体问题列表;报告落盘到 `<reportDir>/patrol`。`compareToLast:true` 附上与上次巡检的 delta(新增/消失崩溃、回归恶化、结论变化)。fail-closed:采不到的分区本身就是问题。无基线 → 跳过对比并提示;不传 `package` → 跳过性能整节。配合 dsh-automation 无人值守定时跑——见 [docs/SCHEDULED-PATROL.zh-CN.md](docs/SCHEDULED-PATROL.zh-CN.md) |
47
48
 
48
49
  错误码:`ADB_NOT_FOUND`、`ADB_UNAVAILABLE`、`DEVICE_NOT_FOUND`、`NO_DEVICES`、`CONNECT_FAILED`、`INSTALL_FAILED`、`ADB_EXIT_<code>` 等,均为结构化 `AdbError`。
49
50
 
51
+ ## 用法:adb_watch_crash(崩溃看门狗)
52
+
53
+ 监控设备 crash buffer 里的**新**真实崩溃。启动时先读一遍当前 buffer,记住所有已有崩溃签名(seed),所以只报 watch 开始**之后**新出现的崩溃。启动标记(`mtk-brm-*`)不算崩溃,自动忽略。
54
+
55
+ **前台模式**(默认)——阻塞到新崩溃出现或预算耗尽:
56
+
57
+ ```json
58
+ // Agent 调用:
59
+ { "name": "adb_watch_crash", "args": { "timeoutMs": 60000 } }
60
+
61
+ // 检测到新崩溃:
62
+ { "matched": true, "waitedMs": 3200, "crashes": [{ "time": "08-26 06:22:56.714", "pid": "2947", "tag": "AndroidRuntime", "message": "FATAL EXCEPTION: WM.task-1", "stack": ["AndroidRuntime: Process: com.miui.weather2, PID: 2947", "..."] }] }
63
+
64
+ // 预算内无新崩溃(不是错误):
65
+ { "matched": false, "waitedMs": 60000, "crashes": [], "reason": "no new crash within 60000ms" }
66
+ ```
67
+
68
+ **后台模式**(`run_in_background`)——立即返回 job id,持续轮询:
69
+
70
+ ```json
71
+ // 启动后台监控:
72
+ { "name": "adb_watch_crash", "args": { "run_in_background": true, "timeoutMs": 300000 } }
73
+ // → { "kind": "background", "jobId": "adb-watch-crash-1" }
74
+
75
+ // 用 job_output 读最新检测结果:
76
+ // [adb_watch_crash] detected 1 new crash(es) after 5400ms
77
+ // - 08-26 06:22:56.714 pid=2947 AndroidRuntime: FATAL EXCEPTION: WM.task-1
78
+ // AndroidRuntime: Process: com.miui.weather2, PID: 2947
79
+ // ...
80
+ ```
81
+
82
+ **与现场采集衔接**——检测到崩溃后,配 `adb_crash_report` 和 `adb_screenshot` 抓现场:
83
+
84
+ ```
85
+ 1. adb_watch_crash(盯新崩溃)
86
+ 2. → 崩溃出现 → adb_crash_report(采集完整现场:buffer + dropbox + 进程 + 内存)
87
+ 3. → adb_screenshot(截取崩溃时的屏幕状态)
88
+ ```
89
+
90
+ | 参数 | 类型 | 默认 | 说明 |
91
+ | --- | --- | --- | --- |
92
+ | `serial` | string | defaultSerial | 目标设备 serial |
93
+ | `timeoutMs` | integer | 60000 | 监控预算(上限 600000 = 10 分钟) |
94
+ | `intervalMs` | integer | 1000 | 轮询间隔(最小 250) |
95
+ | `withStacks` | boolean | true | 是否包含同 pid 堆栈行 |
96
+ | `run_in_background` | boolean | false | 后台 job 模式,用 `job_output` 读结果 |
97
+
50
98
  ## 配置
51
99
 
52
100
  `cordis.patch.yml` 的 `config` 块(或 profile patch):
@@ -83,17 +131,17 @@ npm pack --dry-run # 校验发布包内容(lib/ + cordis.patch.yml)
83
131
  ## 测试与验证
84
132
 
85
133
  - 原则:**提交即测** —— 全部已提交功能均有实测覆盖(单元 + headless 端到端 + 车机台架/真机)。
86
- - 验证设备:Android 13 车机台架 + Android 13 真机。
134
+ - 验证设备:Android 13 车机台架 + Android 14 真机(Redmi K50 Pro)+ 模拟器。
87
135
  - 版本化变更与每版验证记录见 [CHANGELOG.md](CHANGELOG.md);测试方法与覆盖现状见 [docs/TESTING.md](docs/TESTING.md)。
88
136
 
89
137
  ## 项目文档(双语,供 AI 对话/协作者参考)
90
138
 
91
- - [docs/AGENTS.md](docs/AGENTS.md) / [docs/AGENTS.en.md](docs/AGENTS.en.md) — 进项目先读:定位、铁律、命令、环境事实、文档地图
92
- - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.en.md](docs/REQUIREMENTS.en.md) — 目的/范围/非目标/验收标准
93
- - [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.en.md](docs/TESTING.en.md) — 测试哲学(提交即测)、三层测试方法、E2E 步骤、回归清单
94
- - [docs/DEVELOPMENT-LOG.md](docs/DEVELOPMENT-LOG.md) / [docs/DEVELOPMENT-LOG.en.md](docs/DEVELOPMENT-LOG.en.md) — 进度时间线、4 个已修复 bug 教训、环境/生态经验
95
- - [docs/ROADMAP.md](docs/ROADMAP.md) — harness×adb 协同功能路线图(诊断报告/崩溃归因/截图视觉/台架自动化测试/等待原语/审批/多设备对比/定时巡检/回滚台账)
96
- - [PLAN.md](PLAN.md) / [PLAN.en.md](PLAN.en.md) — 里程碑与待办
139
+ - [docs/AGENTS.md](docs/AGENTS.md) / [docs/AGENTS.zh-CN.md](docs/AGENTS.zh-CN.md) — 进项目先读:定位、铁律、命令、环境事实、文档地图
140
+ - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.zh-CN.md](docs/REQUIREMENTS.zh-CN.md) — 目的/范围/非目标/验收标准
141
+ - [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.zh-CN.md](docs/TESTING.zh-CN.md) — 测试哲学(提交即测)、三层测试方法、E2E 步骤、回归清单
142
+ - [docs/ROADMAP.md](docs/ROADMAP.md) / [docs/ROADMAP.zh-CN.md](docs/ROADMAP.zh-CN.md) — harness×adb 协同功能路线图(诊断报告/崩溃归因/截图视觉/台架自动化测试/等待原语/审批/多设备对比/定时巡检/回滚台账)
143
+ - [docs/SCHEDULED-PATROL.md](docs/SCHEDULED-PATROL.md) / [docs/SCHEDULED-PATROL.zh-CN.md](docs/SCHEDULED-PATROL.zh-CN.md) dsh-automation 定时无人值守巡检(prompt 模板 + 运维注意)
144
+ - [PLAN.md](PLAN.md) / [PLAN.zh-CN.md](PLAN.zh-CN.md) — 里程碑与待办
97
145
 
98
146
  ## License
99
147
 
package/lib/index.js CHANGED
@@ -12,6 +12,7 @@ import { registerWaitTool } from './tools/wait.js';
12
12
  import { registerOperationLedgerTool } from './tools/operation-ledger.js';
13
13
  import { registerScreenshotTool } from './tools/screenshot.js';
14
14
  import { registerWatchCrashTool } from './tools/watch-crash.js';
15
+ import { registerPatrolTool } from './tools/patrol.js';
15
16
  import { registerRpc } from './rpc.js';
16
17
  import { registerSkills } from './skill.js';
17
18
  export const name = 'dsh-adb';
@@ -42,10 +43,11 @@ export function apply(ctx, config) {
42
43
  registerOperationLedgerTool(ctx, cfg, baselineDir);
43
44
  registerScreenshotTool(ctx, cfg, screenshotDir);
44
45
  registerWatchCrashTool(ctx, cfg);
46
+ registerPatrolTool(ctx, cfg, `${reportDir.replace(/\\/g, '/')}/patrol`, baselineDir);
45
47
  registerSkills(ctx);
46
48
  // The RPC channel needs the client connection, which mounts after this
47
49
  // plugin starts in web compositions; register lazily so headless profiles
48
50
  // (no connection) stay unaffected.
49
51
  ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg, reportDir));
50
- ctx.logger.info('[dsh-adb] loaded: 14 tools + web device panel rpc');
52
+ ctx.logger.info('[dsh-adb] loaded: 15 tools + web device panel rpc');
51
53
  }
@@ -0,0 +1,19 @@
1
+ import type { PatrolReport } from './patrol.js';
2
+ /**
3
+ * Patrol report store: one JSON per patrol under `<reportDir>/patrol`.
4
+ * Kept in a subdirectory of the device-report dir so the existing device
5
+ * report listing (one readdir, non-recursive) never mixes the two report
6
+ * kinds. Filename: `patrol-<safeSerial>--<epoch-ms>.json`.
7
+ */
8
+ export interface StoredPatrolMeta {
9
+ id: string;
10
+ collectedAt: string;
11
+ serial: string;
12
+ label?: string;
13
+ verdict: string;
14
+ file: string;
15
+ }
16
+ export declare function patrolFileFor(serial: string): string;
17
+ export declare function listPatrolReports(dir: string): StoredPatrolMeta[];
18
+ export declare function savePatrolReport(dir: string, report: PatrolReport): StoredPatrolMeta;
19
+ export declare function loadPatrolReport(dir: string, file: string): PatrolReport;
@@ -0,0 +1,59 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const FILE_PATTERN = /^patrol-(.+)--(\d+)\.json$/;
4
+ function parseMeta(file) {
5
+ const match = FILE_PATTERN.exec(file);
6
+ if (match === null)
7
+ return undefined;
8
+ return { id: match[2], collectedAt: '', serial: match[1], file, verdict: '' };
9
+ }
10
+ export function patrolFileFor(serial) {
11
+ const safeSerial = serial.replace(/[^A-Za-z0-9._-]/g, '_');
12
+ return `patrol-${safeSerial}--${Date.now()}.json`;
13
+ }
14
+ export function listPatrolReports(dir) {
15
+ if (!existsSync(dir))
16
+ return [];
17
+ const metas = [];
18
+ for (const name of readdirSync(dir)) {
19
+ const meta = parseMeta(name);
20
+ if (meta !== undefined)
21
+ metas.push(meta);
22
+ }
23
+ // Newest first by epoch id.
24
+ return metas.sort((a, b) => (Number(a.id) < Number(b.id) ? 1 : -1));
25
+ }
26
+ export function savePatrolReport(dir, report) {
27
+ try {
28
+ mkdirSync(dir, { recursive: true });
29
+ const file = patrolFileFor(report.serial);
30
+ writeFileSync(join(dir, file), JSON.stringify(report, null, 2), 'utf8');
31
+ return {
32
+ id: String(Date.now()),
33
+ collectedAt: report.collectedAt,
34
+ serial: report.serial,
35
+ ...(report.label !== undefined ? { label: report.label } : {}),
36
+ verdict: report.verdict,
37
+ file,
38
+ };
39
+ }
40
+ catch (error) {
41
+ throw new Error(`patrol store unwritable at ${dir}: ${describe(error)}`);
42
+ }
43
+ }
44
+ export function loadPatrolReport(dir, file) {
45
+ try {
46
+ const raw = readFileSync(join(dir, file), 'utf8');
47
+ const parsed = JSON.parse(raw);
48
+ if (parsed === null || typeof parsed !== 'object' || parsed.kind !== 'patrol' || !Array.isArray(parsed.errors) || typeof parsed.collectedAt !== 'string') {
49
+ throw new Error('unexpected shape');
50
+ }
51
+ return parsed;
52
+ }
53
+ catch (error) {
54
+ throw new Error(`patrol store unreadable at ${join(dir, file)}: ${describe(error)}`);
55
+ }
56
+ }
57
+ function describe(error) {
58
+ return error instanceof Error ? error.message : String(error);
59
+ }
@@ -0,0 +1,169 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from './adb.js';
3
+ import { type BaselineEntry, type FieldDiff } from './baseline.js';
4
+ import { type PerfSnapshot } from './tools/perf.js';
5
+ import { parseBattery } from './parsers/perf.js';
6
+ /**
7
+ * One-click device patrol ("巡检"): crash scan + perf baseline comparison +
8
+ * device essentials in one structured report the agent (or a scheduled task)
9
+ * can reason from. Evidence → signal again: crash counts are classified (real
10
+ * crashes vs. boot markers), perf diffs split into regressions / improvements
11
+ * / stable, and a compact verdict drives attention.
12
+ *
13
+ * LLM-free by design: the patrol report is the input a future attribution
14
+ * step (ROADMAP ②) will consume, but nothing here needs a model.
15
+ */
16
+ export type PatrolVerdict = 'ok' | 'attention';
17
+ export interface PatrolCrashSection {
18
+ total: number;
19
+ realCrashCount: number;
20
+ bootMarkerCount: number;
21
+ otherCount: number;
22
+ /** Newest real crashes first (time/pid/tag/message only, no stacks). */
23
+ recent: Array<{
24
+ time: string;
25
+ pid: string;
26
+ tag: string;
27
+ message: string;
28
+ }>;
29
+ }
30
+ export interface PatrolPerfSection {
31
+ package: string;
32
+ /** Baseline metadata, or null when the device has no stored baseline yet. */
33
+ baseline: {
34
+ id: string;
35
+ label: string;
36
+ createdAt: string;
37
+ tags: string[];
38
+ } | null;
39
+ note?: string;
40
+ regressions: FieldDiff[];
41
+ improvements: FieldDiff[];
42
+ stableCount: number;
43
+ }
44
+ export interface PatrolDeviceSection {
45
+ battery?: ReturnType<typeof parseBattery>;
46
+ storage?: {
47
+ lines: number;
48
+ truncated: boolean;
49
+ excerpt: string;
50
+ };
51
+ }
52
+ export interface PatrolReport {
53
+ kind: 'patrol';
54
+ collectedAt: string;
55
+ serial: string;
56
+ label?: string;
57
+ verdict: PatrolVerdict;
58
+ lines: string[];
59
+ issues: string[];
60
+ crash?: PatrolCrashSection;
61
+ perf?: PatrolPerfSection;
62
+ device?: PatrolDeviceSection;
63
+ /** Delta vs. the previous stored patrol report; present when compareToLast is set and a previous report exists. */
64
+ delta?: PatrolDelta;
65
+ errors: Array<{
66
+ section: string;
67
+ message: string;
68
+ }>;
69
+ }
70
+ /** Change block vs. the previous patrol report (pure data, unit-tested). */
71
+ export interface PatrolDelta {
72
+ /** Previous report the comparison ran against. */
73
+ comparedTo: {
74
+ collectedAt: string;
75
+ verdict: string;
76
+ label?: string;
77
+ };
78
+ /** Verdict transition; 'same' when unchanged. */
79
+ verdictChange: 'same' | 'worse' | 'better';
80
+ /** Crash signatures (time|pid|tag|message) seen now but not last time. */
81
+ newCrashes: Array<{
82
+ time: string;
83
+ pid: string;
84
+ tag: string;
85
+ message: string;
86
+ }>;
87
+ /** Crash signatures seen last time but not now (resolved/repaired). */
88
+ goneCrashes: Array<{
89
+ time: string;
90
+ pid: string;
91
+ tag: string;
92
+ message: string;
93
+ }>;
94
+ /** Regression fields that persisted and got worse (percent moved further up). */
95
+ worseningRegressions: Array<{
96
+ field: string;
97
+ label: string;
98
+ from: number;
99
+ to: number;
100
+ }>;
101
+ /** Regression fields seen last time that are gone now. */
102
+ resolvedRegressions: string[];
103
+ /** Human/agent-readable one-liners summarizing the delta. */
104
+ lines: string[];
105
+ }
106
+ export interface CollectPatrolArgs {
107
+ serial?: string;
108
+ /** Enables the perf comparison; omitted → the perf section is skipped entirely. */
109
+ package?: string;
110
+ label?: string;
111
+ /** Relative threshold (%) beyond which a diff counts as a regression/improvement; default 5. */
112
+ thresholdPercent?: number;
113
+ /** Max recent real crashes listed; default 5. */
114
+ crashTail?: number;
115
+ /**
116
+ * Compare against the most recent stored patrol report for this device
117
+ * (excluding the one being written now) and attach the delta block.
118
+ */
119
+ compareToLast?: boolean;
120
+ }
121
+ export interface ClassifiedDiffs {
122
+ regressions: FieldDiff[];
123
+ improvements: FieldDiff[];
124
+ stable: FieldDiff[];
125
+ }
126
+ /**
127
+ * Split a numeric diff into regressions / improvements / stable. A field with
128
+ * no percentage (from === 0) falls back to its absolute delta: any increase
129
+ * from zero on a higher-is-worse field is a regression (e.g. janky 0 → 5).
130
+ */
131
+ export declare function classifyDiffs(diffs: FieldDiff[], thresholdPercent?: number): ClassifiedDiffs;
132
+ /**
133
+ * Compact patrol verdict: crash counts + perf regressions + battery
134
+ * essentials → verdict + human/agent-readable lines + concrete issues.
135
+ */
136
+ export declare function buildPatrolSummary(parts: {
137
+ crash?: PatrolCrashSection;
138
+ perf?: PatrolPerfSection;
139
+ device?: PatrolDeviceSection;
140
+ errors: Array<{
141
+ section: string;
142
+ message: string;
143
+ }>;
144
+ }): {
145
+ verdict: PatrolVerdict;
146
+ lines: string[];
147
+ issues: string[];
148
+ };
149
+ /**
150
+ * Compute the delta block against the previous stored patrol report. Pure:
151
+ * both reports are inputs, so the store wiring stays in the tool layer.
152
+ */
153
+ export declare function diffAgainstPrevious(current: PatrolReport, previous: PatrolReport): PatrolDelta;
154
+ /** Compose the perf section from a baseline entry and the current snapshot. */
155
+ export declare function buildPerfSection(packageName: string, input: {
156
+ baseline: BaselineEntry | undefined;
157
+ current: PerfSnapshot;
158
+ } | undefined, thresholdPercent: number): PatrolPerfSection | undefined;
159
+ export interface PatrolBaselineHooks {
160
+ /** Look up the latest baseline for device+package (wired to the baseline store). */
161
+ latestBaseline: (device: string, pkg: string) => BaselineEntry | undefined;
162
+ /**
163
+ * Most recent stored patrol report for this device, excluding the one being
164
+ * collected now (the store wires this to listPatrolReports/loadPatrolReport).
165
+ */
166
+ previousPatrol?: (device: string) => PatrolReport | undefined;
167
+ }
168
+ /** Collect the patrol report with per-section degradation. */
169
+ export declare function collectPatrol(ctx: Context, cfg: AdbConfig, signal: AbortSignal, args: CollectPatrolArgs, hooks: PatrolBaselineHooks): Promise<PatrolReport>;
package/lib/patrol.js ADDED
@@ -0,0 +1,297 @@
1
+ import { classifyFailure, runAdb } from './adb.js';
2
+ import { classifyCrashBuffer } from './report.js';
3
+ import { diffSnapshots } from './baseline.js';
4
+ import { capturePerfSnapshot } from './tools/perf.js';
5
+ import { parseBattery } from './parsers/perf.js';
6
+ import { parseLogcat } from './parsers/logcat.js';
7
+ // ---- Pure helpers (unit-tested) ----
8
+ /** Which direction is "worse" per diff field (mirrors baseline.ts DIFF_FIELDS). */
9
+ const HIGHER_IS_WORSE = new Set([
10
+ 'meminfo.totalPssKb',
11
+ 'meminfo.totalRssKb',
12
+ 'meminfo.javaHeapKb',
13
+ 'meminfo.nativeHeapKb',
14
+ 'meminfo.graphicsKb',
15
+ 'gfxinfo.totalFrames',
16
+ 'gfxinfo.jankyFrames',
17
+ 'gfxinfo.jankyPercent',
18
+ 'gfxinfo.percentile50Ms',
19
+ 'gfxinfo.percentile90Ms',
20
+ 'gfxinfo.percentile95Ms',
21
+ 'gfxinfo.percentile99Ms',
22
+ 'gfxinfo.missedVsync',
23
+ 'battery.temperatureC',
24
+ ]);
25
+ const LOW_BATTERY_PERCENT = 15;
26
+ const HIGH_BATTERY_TEMP_C = 45;
27
+ /**
28
+ * Split a numeric diff into regressions / improvements / stable. A field with
29
+ * no percentage (from === 0) falls back to its absolute delta: any increase
30
+ * from zero on a higher-is-worse field is a regression (e.g. janky 0 → 5).
31
+ */
32
+ export function classifyDiffs(diffs, thresholdPercent = 5) {
33
+ const regressions = [];
34
+ const improvements = [];
35
+ const stable = [];
36
+ for (const diff of diffs) {
37
+ const higherWorse = HIGHER_IS_WORSE.has(diff.field);
38
+ const percent = diff.deltaPercent;
39
+ if (percent === undefined) {
40
+ // from === 0: judge by the absolute delta alone.
41
+ if (diff.delta === undefined || diff.delta === 0) {
42
+ stable.push(diff);
43
+ }
44
+ else if (higherWorse) {
45
+ ;
46
+ (diff.delta > 0 ? regressions : improvements).push(diff);
47
+ }
48
+ else {
49
+ ;
50
+ (diff.delta < 0 ? regressions : improvements).push(diff);
51
+ }
52
+ continue;
53
+ }
54
+ if (percent > thresholdPercent)
55
+ (higherWorse ? regressions : improvements).push(diff);
56
+ else if (percent < -thresholdPercent)
57
+ (higherWorse ? improvements : regressions).push(diff);
58
+ else
59
+ stable.push(diff);
60
+ }
61
+ return { regressions, improvements, stable };
62
+ }
63
+ function crashSectionOf(summary, tail) {
64
+ return {
65
+ total: summary.total,
66
+ realCrashCount: summary.realCrashCount,
67
+ bootMarkerCount: summary.bootMarkerCount,
68
+ otherCount: summary.otherCount,
69
+ recent: summary.chains.slice(-tail).reverse().map((chain) => ({
70
+ time: chain.signature.time,
71
+ pid: chain.signature.pid,
72
+ tag: chain.signature.tag,
73
+ message: chain.signature.message,
74
+ })),
75
+ };
76
+ }
77
+ /**
78
+ * Compact patrol verdict: crash counts + perf regressions + battery
79
+ * essentials → verdict + human/agent-readable lines + concrete issues.
80
+ */
81
+ export function buildPatrolSummary(parts) {
82
+ const lines = [];
83
+ const issues = [];
84
+ const crash = parts.crash;
85
+ if (crash) {
86
+ if (crash.realCrashCount > 0) {
87
+ const newest = crash.recent[0];
88
+ issues.push(`真实崩溃 ${crash.realCrashCount} 起${newest !== undefined ? `(最新 ${newest.time} ${newest.tag}: ${newest.message.slice(0, 60)})` : ''}`);
89
+ lines.push(`崩溃:${crash.realCrashCount} 真实 + ${crash.bootMarkerCount} 启动标记 + ${crash.otherCount} 其他(共 ${crash.total})`);
90
+ }
91
+ else {
92
+ lines.push(`崩溃:无真实崩溃(${crash.bootMarkerCount} 启动标记 + ${crash.otherCount} 其他,共 ${crash.total})`);
93
+ }
94
+ }
95
+ const perf = parts.perf;
96
+ if (perf) {
97
+ if (perf.baseline === null) {
98
+ lines.push(`性能:无 ${perf.package} 基线,跳过对比(用 adb_perf_baseline save 先存一次)`);
99
+ }
100
+ else {
101
+ const reg = perf.regressions.map((d) => `${d.label} ${d.deltaPercent !== undefined ? `${d.deltaPercent > 0 ? '+' : ''}${d.deltaPercent.toFixed(1)}%` : `${d.delta ?? ''}`}`);
102
+ const imp = perf.improvements.length;
103
+ lines.push(`性能 vs ${perf.baseline.label}:${reg.length} 回归 / ${imp} 改善 / ${perf.stableCount} 持平${reg.length > 0 ? ` — ${reg.join(', ')}` : ''}`);
104
+ if (reg.length > 0)
105
+ issues.push(`性能回归(vs ${perf.baseline.label}):${reg.join(', ')}`);
106
+ }
107
+ }
108
+ const battery = parts.device?.battery;
109
+ if (battery) {
110
+ const bits = [];
111
+ if (battery.levelPercent !== undefined)
112
+ bits.push(`${battery.levelPercent}%`);
113
+ if (battery.temperatureC !== undefined)
114
+ bits.push(`${battery.temperatureC}°C`);
115
+ lines.push(`电池:${bits.join(' ') || '无数据'}`);
116
+ if (battery.levelPercent !== undefined && battery.levelPercent <= LOW_BATTERY_PERCENT)
117
+ issues.push(`电量低(${battery.levelPercent}%)`);
118
+ if (battery.temperatureC !== undefined && battery.temperatureC >= HIGH_BATTERY_TEMP_C)
119
+ issues.push(`电池高温(${battery.temperatureC}°C)`);
120
+ }
121
+ // Fail-closed: a section we could not collect is itself a patrol finding —
122
+ // "couldn't check the crash buffer" must not read as "no crashes".
123
+ for (const error of parts.errors) {
124
+ lines.push(`⚠ ${error.section} 采集失败:${error.message.slice(0, 80)}`);
125
+ issues.push(`${error.section} 采集失败(${error.message.slice(0, 60)})`);
126
+ }
127
+ const verdict = issues.length > 0 ? 'attention' : 'ok';
128
+ return { verdict, lines, issues };
129
+ }
130
+ // ---- Delta vs. the previous patrol report (pure, unit-tested) ----
131
+ function crashSignatureKey(entry) {
132
+ return `${entry.time}|${entry.pid}|${entry.tag}|${entry.message}`;
133
+ }
134
+ const VERDICT_RANK = { ok: 0, attention: 1 };
135
+ /**
136
+ * Compute the delta block against the previous stored patrol report. Pure:
137
+ * both reports are inputs, so the store wiring stays in the tool layer.
138
+ */
139
+ export function diffAgainstPrevious(current, previous) {
140
+ const prevCrashes = new Map();
141
+ for (const entry of previous.crash?.recent ?? [])
142
+ prevCrashes.set(crashSignatureKey(entry), entry);
143
+ const nowCrashes = new Map();
144
+ for (const entry of current.crash?.recent ?? [])
145
+ nowCrashes.set(crashSignatureKey(entry), entry);
146
+ const newCrashes = [...nowCrashes.entries()].filter(([key]) => !prevCrashes.has(key)).map(([, entry]) => entry);
147
+ const goneCrashes = [...prevCrashes.entries()].filter(([key]) => !nowCrashes.has(key)).map(([, entry]) => entry);
148
+ const prevRegressions = new Map();
149
+ for (const diff of previous.perf?.regressions ?? []) {
150
+ if (diff.deltaPercent !== undefined)
151
+ prevRegressions.set(diff.field, diff.deltaPercent);
152
+ }
153
+ const nowRegressions = new Map();
154
+ for (const diff of current.perf?.regressions ?? []) {
155
+ if (diff.deltaPercent !== undefined)
156
+ nowRegressions.set(diff.field, diff.deltaPercent);
157
+ }
158
+ const worseningRegressions = [];
159
+ for (const [field, nowPercent] of nowRegressions) {
160
+ const prevPercent = prevRegressions.get(field);
161
+ if (prevPercent !== undefined && nowPercent > prevPercent) {
162
+ worseningRegressions.push({ field, label: current.perf?.regressions.find((d) => d.field === field)?.label ?? field, from: prevPercent, to: nowPercent });
163
+ }
164
+ }
165
+ const resolvedRegressions = [...prevRegressions.keys()].filter((field) => !nowRegressions.has(field));
166
+ const rankDelta = VERDICT_RANK[current.verdict] - VERDICT_RANK[previous.verdict];
167
+ const verdictChange = rankDelta > 0 ? 'worse' : rankDelta < 0 ? 'better' : 'same';
168
+ const lines = [];
169
+ if (verdictChange !== 'same')
170
+ lines.push(`结论变化:${previous.verdict} → ${current.verdict}(${verdictChange === 'worse' ? '恶化' : '好转'})`);
171
+ if (newCrashes.length > 0)
172
+ lines.push(`新增真实崩溃 ${newCrashes.length} 起(最新 ${newCrashes[0].time} ${newCrashes[0].tag})`);
173
+ if (goneCrashes.length > 0)
174
+ lines.push(`${goneCrashes.length} 起上次崩溃已不在缓冲中`);
175
+ if (worseningRegressions.length > 0) {
176
+ lines.push(`回归恶化:${worseningRegressions.map((r) => `${r.label} ${r.from.toFixed(1)}%→${r.to.toFixed(1)}%`).join(', ')}`);
177
+ }
178
+ if (resolvedRegressions.length > 0)
179
+ lines.push(`已缓解回归:${resolvedRegressions.length} 项`);
180
+ if (lines.length === 0)
181
+ lines.push('与上次巡检相比无显著变化');
182
+ return {
183
+ comparedTo: { collectedAt: previous.collectedAt, verdict: previous.verdict, ...(previous.label !== undefined ? { label: previous.label } : {}) },
184
+ verdictChange,
185
+ newCrashes,
186
+ goneCrashes,
187
+ worseningRegressions,
188
+ resolvedRegressions,
189
+ lines,
190
+ };
191
+ }
192
+ /** Compose the perf section from a baseline entry and the current snapshot. */
193
+ export function buildPerfSection(packageName, input, thresholdPercent) {
194
+ if (input === undefined)
195
+ return undefined;
196
+ if (input.baseline === undefined) {
197
+ return {
198
+ package: packageName,
199
+ baseline: null,
200
+ note: 'no baseline for this device+package yet; save one with adb_perf_baseline save',
201
+ regressions: [],
202
+ improvements: [],
203
+ stableCount: 0,
204
+ };
205
+ }
206
+ const classified = classifyDiffs(diffSnapshots(input.baseline.snapshot, input.current), thresholdPercent);
207
+ return {
208
+ package: packageName,
209
+ baseline: {
210
+ id: input.baseline.id,
211
+ label: input.baseline.label,
212
+ createdAt: input.baseline.createdAt,
213
+ tags: input.baseline.tags ?? [],
214
+ },
215
+ regressions: classified.regressions,
216
+ improvements: classified.improvements,
217
+ stableCount: classified.stable.length,
218
+ };
219
+ }
220
+ // ---- Collectors ----
221
+ async function collectCrashScan(ctx, cfg, signal, serial, tail) {
222
+ const result = await runAdb(ctx, cfg, ['logcat', '-b', 'crash', '-v', 'threadtime', '-d'], { signal, serial, maxBytes: 8 * 1024 * 1024 });
223
+ if (result.exitCode !== 0)
224
+ throw classifyFailure(result);
225
+ return crashSectionOf(classifyCrashBuffer(parseLogcat(result.stdout)), tail);
226
+ }
227
+ async function collectBattery(ctx, cfg, signal, serial) {
228
+ // dumpsys battery is device-global and takes no package argument.
229
+ const result = await runAdb(ctx, cfg, ['shell', 'dumpsys', 'battery'], { signal, serial });
230
+ if (result.exitCode !== 0)
231
+ throw classifyFailure(result);
232
+ return parseBattery(result.stdout);
233
+ }
234
+ function storageExcerpt(text, maxLines) {
235
+ const lines = text.split(/\r?\n/).filter((line) => line !== '');
236
+ return { lines: lines.length, truncated: lines.length > maxLines, excerpt: lines.slice(-maxLines).join('\n') };
237
+ }
238
+ async function collectStorage(ctx, cfg, signal, serial) {
239
+ const result = await runAdb(ctx, cfg, ['shell', 'df'], { signal, serial, maxBytes: 1024 * 1024 });
240
+ if (result.exitCode !== 0)
241
+ throw classifyFailure(result);
242
+ return storageExcerpt(result.stdout, 12);
243
+ }
244
+ /** Collect the patrol report with per-section degradation. */
245
+ export async function collectPatrol(ctx, cfg, signal, args, hooks) {
246
+ const serial = args.serial ?? cfg.defaultSerial;
247
+ const resolved = serial ?? 'default';
248
+ const crashTail = args.crashTail !== undefined && args.crashTail > 0 ? Math.floor(args.crashTail) : 5;
249
+ const threshold = args.thresholdPercent !== undefined && args.thresholdPercent > 0 ? args.thresholdPercent : 5;
250
+ const report = { kind: 'patrol', collectedAt: new Date().toISOString(), serial: resolved, verdict: 'ok', lines: [], issues: [], errors: [] };
251
+ if (args.label !== undefined)
252
+ report.label = args.label;
253
+ const guard = async (section, collect, assign) => {
254
+ try {
255
+ assign(await collect());
256
+ }
257
+ catch (error) {
258
+ report.errors.push({ section, message: error instanceof Error ? error.message : String(error) });
259
+ }
260
+ };
261
+ const perfTask = (async () => {
262
+ if (args.package === undefined)
263
+ return undefined;
264
+ try {
265
+ const baseline = hooks.latestBaseline(resolved, args.package);
266
+ const current = await capturePerfSnapshot(ctx, cfg, signal, { package: args.package, serial: args.serial });
267
+ return buildPerfSection(args.package, { baseline, current }, threshold);
268
+ }
269
+ catch (error) {
270
+ report.errors.push({ section: 'perf', message: error instanceof Error ? error.message : String(error) });
271
+ return undefined;
272
+ }
273
+ })();
274
+ await Promise.all([
275
+ guard('crash', () => collectCrashScan(ctx, cfg, signal, serial, crashTail), (v) => { report.crash = v; }),
276
+ guard('device', () => collectBattery(ctx, cfg, signal, serial), (v) => { report.device = { ...report.device, battery: v }; }),
277
+ guard('storage', () => collectStorage(ctx, cfg, signal, serial), (v) => { report.device = { ...report.device, storage: v }; }),
278
+ ]);
279
+ report.perf = await perfTask;
280
+ const summary = buildPatrolSummary({ crash: report.crash, perf: report.perf, device: report.device, errors: report.errors });
281
+ report.verdict = summary.verdict;
282
+ report.lines = summary.lines;
283
+ report.issues = summary.issues;
284
+ if (args.compareToLast === true && hooks.previousPatrol !== undefined) {
285
+ try {
286
+ const previous = hooks.previousPatrol(resolved);
287
+ if (previous !== undefined) {
288
+ report.delta = diffAgainstPrevious(report, previous);
289
+ report.lines.push(...report.delta.lines.map((line) => `Δ ${line}`));
290
+ }
291
+ }
292
+ catch (error) {
293
+ report.errors.push({ section: 'delta', message: error instanceof Error ? error.message : String(error) });
294
+ }
295
+ }
296
+ return report;
297
+ }
@@ -0,0 +1,9 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ /**
4
+ * adb_patrol_check: one-click patrol ("巡检"). Crash scan + perf baseline
5
+ * comparison + battery/storage essentials in one pass, classified into a
6
+ * compact verdict (ok/attention) with concrete issues — the report a nightly
7
+ * scheduled task or a morning "巡检一下" prompt consumes. LLM-free.
8
+ */
9
+ export declare function registerPatrolTool(ctx: Context, cfg: AdbConfig, patrolDir: string, baselineDir: string): void;
@@ -0,0 +1,68 @@
1
+ import { jsonOutput } from '../adb.js';
2
+ import { collectPatrol } from '../patrol.js';
3
+ import { savePatrolReport, listPatrolReports, loadPatrolReport } from '../patrol-store.js';
4
+ import { createStore } from '../baseline.js';
5
+ function publicReport(report, saved) {
6
+ return {
7
+ kind: report.kind,
8
+ collectedAt: report.collectedAt,
9
+ serial: report.serial,
10
+ ...(report.label !== undefined ? { label: report.label } : {}),
11
+ verdict: report.verdict,
12
+ lines: report.lines,
13
+ issues: report.issues,
14
+ ...(report.crash !== undefined ? { crash: report.crash } : {}),
15
+ ...(report.perf !== undefined ? { perf: report.perf } : {}),
16
+ ...(report.device !== undefined ? { device: report.device } : {}),
17
+ ...(report.delta !== undefined ? { delta: report.delta } : {}),
18
+ errors: report.errors,
19
+ ...(saved !== undefined ? { savedTo: saved.file } : {}),
20
+ };
21
+ }
22
+ /**
23
+ * adb_patrol_check: one-click patrol ("巡检"). Crash scan + perf baseline
24
+ * comparison + battery/storage essentials in one pass, classified into a
25
+ * compact verdict (ok/attention) with concrete issues — the report a nightly
26
+ * scheduled task or a morning "巡检一下" prompt consumes. LLM-free.
27
+ */
28
+ export function registerPatrolTool(ctx, cfg, patrolDir, baselineDir) {
29
+ ctx.tools.register({
30
+ name: 'adb_patrol_check',
31
+ description: 'One-click device patrol: scan the crash buffer (classified into real crashes vs. boot markers), compare current perf against the latest stored baseline for a package (regressions/improvements beyond a threshold), check battery/temperature/storage, and persist a structured patrol report. Returns a compact verdict (ok/attention) with concrete issues — reason from it, then drill in with adb_crash_report / adb_perf_baseline compare / adb_device_report as needed. Omit `package` to skip the perf comparison; with no stored baseline the comparison is skipped with a note, not an error. With `compareToLast:true` a delta block is attached: new/gone crashes, worsening regressions, and the verdict transition vs. the previous stored patrol for this device.',
32
+ parameters: {
33
+ type: 'object',
34
+ additionalProperties: false,
35
+ properties: {
36
+ serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
37
+ package: { type: 'string', description: 'App package id; when given, the current perf is captured and diffed against the latest stored baseline for this device+package.' },
38
+ label: { type: 'string', description: 'Optional patrol label, e.g. soak-night-0828.' },
39
+ thresholdPercent: { type: 'number', description: 'Relative change (%) beyond which a perf diff counts as a regression/improvement; defaults to 5.' },
40
+ crashTail: { type: 'integer', description: 'Max recent real crashes listed; defaults to 5.' },
41
+ compareToLast: { type: 'boolean', description: 'Attach a delta vs. the previous stored patrol report for this device (new/gone crashes, worsening regressions, verdict transition); defaults to false.' },
42
+ },
43
+ },
44
+ output: jsonOutput(),
45
+ async execute(args, exec) {
46
+ const store = createStore(baselineDir);
47
+ const device = args.serial ?? cfg.defaultSerial ?? 'default';
48
+ const report = await collectPatrol(ctx, cfg, exec.signal, args, {
49
+ latestBaseline: (dev, pkg) => store.latest(dev, pkg),
50
+ previousPatrol: (dev) => {
51
+ const metas = listPatrolReports(patrolDir).filter((meta) => meta.serial === dev.replace(/[^A-Za-z0-9._-]/g, '_'));
52
+ if (metas.length === 0)
53
+ return undefined;
54
+ return loadPatrolReport(patrolDir, metas[0].file);
55
+ },
56
+ });
57
+ let saved;
58
+ try {
59
+ saved = savePatrolReport(patrolDir, report);
60
+ }
61
+ catch {
62
+ // Persistence is best-effort: the report is still returned to the agent.
63
+ }
64
+ void device;
65
+ return publicReport(report, saved);
66
+ },
67
+ });
68
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-adb",
3
- "version": "1.6.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, operation ledger w/ rollback, screenshots, crash watchdog, web device panel (autocomplete, live logcat, profiler)",
3
+ "version": "1.7.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, operation ledger w/ rollback, screenshots, crash watchdog, one-click device patrol (scheduled-watchdog ready), web device panel (autocomplete, live logcat, profiler)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {