dsh-adb 1.6.1 → 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 +8 -7
- package/README.zh-CN.md +7 -6
- package/lib/index.js +3 -1
- package/lib/patrol-store.d.ts +19 -0
- package/lib/patrol-store.js +59 -0
- package/lib/patrol.d.ts +169 -0
- package/lib/patrol.js +297 -0
- package/lib/tools/patrol.d.ts +9 -0
- package/lib/tools/patrol.js +68 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ 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
|
|
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
|
|
|
@@ -44,6 +44,7 @@ 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
|
|
|
@@ -133,12 +134,12 @@ npm pack --dry-run # verify publish contents (lib/ + cordis.patch.yml)
|
|
|
133
134
|
|
|
134
135
|
## Project Docs (bilingual; for AI agents & contributors)
|
|
135
136
|
|
|
136
|
-
- [docs/AGENTS.md](docs/AGENTS.md) / [docs/AGENTS.
|
|
137
|
-
- [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.
|
|
138
|
-
- [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.
|
|
139
|
-
- [docs/
|
|
140
|
-
- [docs/
|
|
141
|
-
- [PLAN.md](PLAN.md) / [PLAN.
|
|
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
|
|
142
143
|
|
|
143
144
|
## License
|
|
144
145
|
|
package/README.zh-CN.md
CHANGED
|
@@ -44,6 +44,7 @@ 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
|
|
|
@@ -135,12 +136,12 @@ npm pack --dry-run # 校验发布包内容(lib/ + cordis.patch.yml)
|
|
|
135
136
|
|
|
136
137
|
## 项目文档(双语,供 AI 对话/协作者参考)
|
|
137
138
|
|
|
138
|
-
- [docs/AGENTS.md](docs/AGENTS.md) / [docs/AGENTS.
|
|
139
|
-
- [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) / [docs/REQUIREMENTS.
|
|
140
|
-
- [docs/TESTING.md](docs/TESTING.md) / [docs/TESTING.
|
|
141
|
-
- [docs/
|
|
142
|
-
- [docs/
|
|
143
|
-
- [PLAN.md](PLAN.md) / [PLAN.
|
|
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) — 里程碑与待办
|
|
144
145
|
|
|
145
146
|
## License
|
|
146
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:
|
|
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
|
+
}
|
package/lib/patrol.d.ts
ADDED
|
@@ -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.
|
|
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": {
|