dsh-adb 1.0.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 +6 -3
- package/README.zh-CN.md +7 -4
- package/client.js +559 -137
- package/lib/index.d.ts +2 -0
- package/lib/index.js +8 -2
- package/lib/parsers/sysinfo.d.ts +18 -0
- package/lib/parsers/sysinfo.js +56 -0
- package/lib/report-store.d.ts +18 -0
- package/lib/report-store.js +55 -0
- package/lib/report.d.ts +103 -0
- package/lib/report.js +192 -0
- package/lib/rpc.d.ts +2 -7
- package/lib/rpc.js +114 -27
- package/lib/skill.d.ts +9 -0
- package/lib/skill.js +40 -0
- package/lib/tools/device-report.d.ts +10 -0
- package/lib/tools/device-report.js +52 -0
- package/package.json +2 -2
package/lib/rpc.js
CHANGED
|
@@ -1,47 +1,134 @@
|
|
|
1
|
+
import { readFileSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { classifyFailure, runAdb } from './adb.js';
|
|
2
5
|
import { parseDevices } from './parsers/devices.js';
|
|
3
|
-
import { matchesLevel, parseLogcat } from './parsers/logcat.js';
|
|
6
|
+
import { matchesKeyword, matchesLevel, parseLogcat } from './parsers/logcat.js';
|
|
7
|
+
import { parseGetprop, parseMemTotal, parsePackageList, parseProcessList, parseWmSize, } from './parsers/sysinfo.js';
|
|
4
8
|
import { capturePerfSnapshot } from './tools/perf.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { collectDeviceReport, REPORT_SECTIONS } from './report.js';
|
|
10
|
+
import { saveReport } from './report-store.js';
|
|
11
|
+
async function requireDevice(ctx, cfg, argv, options) {
|
|
12
|
+
const result = await runAdb(ctx, cfg, argv, { signal: options.signal, serial: options.serial, maxBytes: options.maxBytes });
|
|
13
|
+
if (result.exitCode !== 0)
|
|
14
|
+
throw classifyFailure(result);
|
|
15
|
+
return result.stdout;
|
|
16
|
+
}
|
|
17
|
+
function serialOf(payload) {
|
|
18
|
+
return typeof payload.serial === 'string' ? payload.serial : undefined;
|
|
19
|
+
}
|
|
20
|
+
export async function handleRpcEndpoint(ctx, cfg, reportDir, endpoint, raw, signal) {
|
|
11
21
|
try {
|
|
12
22
|
const payload = (raw ?? {});
|
|
13
23
|
switch (endpoint) {
|
|
14
24
|
case 'listDevices': {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
throw classifyFailure(result);
|
|
18
|
-
return { ok: true, value: { server: 'ok', devices: parseDevices(result.stdout) } };
|
|
25
|
+
const stdout = await requireDevice(ctx, cfg, ['devices', '-l'], { signal });
|
|
26
|
+
return { ok: true, value: { server: 'ok', devices: parseDevices(stdout) } };
|
|
19
27
|
}
|
|
20
|
-
case '
|
|
28
|
+
case 'listPackages': {
|
|
29
|
+
const stdout = await requireDevice(ctx, cfg, ['shell', 'pm', 'list', 'packages'], {
|
|
30
|
+
signal,
|
|
31
|
+
serial: serialOf(payload),
|
|
32
|
+
maxBytes: 4 * 1024 * 1024,
|
|
33
|
+
});
|
|
34
|
+
return { ok: true, value: { packages: parsePackageList(stdout) } };
|
|
35
|
+
}
|
|
36
|
+
case 'deviceInfo': {
|
|
37
|
+
const serial = serialOf(payload);
|
|
38
|
+
const getprop = parseGetprop(await requireDevice(ctx, cfg, ['shell', 'getprop'], { signal, serial, maxBytes: 2 * 1024 * 1024 }));
|
|
39
|
+
const size = parseWmSize(await requireDevice(ctx, cfg, ['shell', 'wm', 'size'], { signal, serial }));
|
|
40
|
+
const memTotalKb = parseMemTotal(await requireDevice(ctx, cfg, ['shell', 'cat', '/proc/meminfo'], { signal, serial }));
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
value: {
|
|
44
|
+
model: getprop['ro.product.model'],
|
|
45
|
+
manufacturer: getprop['ro.product.manufacturer'],
|
|
46
|
+
release: getprop['ro.build.version.release'],
|
|
47
|
+
sdk: getprop['ro.build.version.sdk'],
|
|
48
|
+
resolution: size === undefined ? undefined : `${size.width}x${size.height}`,
|
|
49
|
+
memTotalKb,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
case 'processList': {
|
|
54
|
+
const stdout = await requireDevice(ctx, cfg, ['shell', 'ps', '-A'], {
|
|
55
|
+
signal,
|
|
56
|
+
serial: serialOf(payload),
|
|
57
|
+
maxBytes: 2 * 1024 * 1024,
|
|
58
|
+
});
|
|
59
|
+
let processes = parseProcessList(stdout);
|
|
21
60
|
const pkg = typeof payload.package === 'string' ? payload.package : undefined;
|
|
22
|
-
if (pkg
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return { ok: true, value: snapshot };
|
|
61
|
+
if (pkg !== undefined)
|
|
62
|
+
processes = processes.filter((entry) => entry.name.includes(pkg));
|
|
63
|
+
processes = processes.slice(0, 100);
|
|
64
|
+
return { ok: true, value: { total: processes.length, processes } };
|
|
27
65
|
}
|
|
28
|
-
case 'logcatTail':
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const output = await runAdb(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], {
|
|
66
|
+
case 'logcatTail':
|
|
67
|
+
case 'logcatDelta': {
|
|
68
|
+
const serial = serialOf(payload);
|
|
69
|
+
const stdout = await requireDevice(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], {
|
|
33
70
|
signal,
|
|
34
71
|
serial,
|
|
35
72
|
maxBytes: 8 * 1024 * 1024,
|
|
36
73
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
const since = typeof payload.since === 'string' ? payload.since : undefined;
|
|
75
|
+
const level = typeof payload.level === 'string' ? payload.level : undefined;
|
|
76
|
+
const keyword = typeof payload.keyword === 'string' ? payload.keyword : undefined;
|
|
77
|
+
const pid = typeof payload.pid === 'string' ? payload.pid : undefined;
|
|
78
|
+
const tail = typeof payload.tail === 'number' && payload.tail > 0 ? Math.floor(payload.tail) : 200;
|
|
79
|
+
let entries = parseLogcat(stdout);
|
|
80
|
+
// threadtime timestamps ("MM-DD HH:MM:SS.mmm") sort lexicographically.
|
|
81
|
+
if (since !== undefined)
|
|
82
|
+
entries = entries.filter((entry) => entry.time > since);
|
|
40
83
|
if (level !== undefined)
|
|
41
84
|
entries = entries.filter((entry) => matchesLevel(entry, level));
|
|
85
|
+
if (keyword !== undefined)
|
|
86
|
+
entries = entries.filter((entry) => matchesKeyword(entry, keyword));
|
|
87
|
+
if (pid !== undefined)
|
|
88
|
+
entries = entries.filter((entry) => entry.pid === pid);
|
|
42
89
|
const capped = entries.slice(-tail);
|
|
43
90
|
return { ok: true, value: { total: entries.length, truncated: entries.length > tail, entries: capped } };
|
|
44
91
|
}
|
|
92
|
+
case 'perfSnapshot': {
|
|
93
|
+
const pkg = typeof payload.package === 'string' ? payload.package : undefined;
|
|
94
|
+
if (pkg === undefined)
|
|
95
|
+
throw new Error('perfSnapshot requires a string "package"');
|
|
96
|
+
const snapshot = await capturePerfSnapshot(ctx, cfg, signal, { package: pkg, serial: serialOf(payload) });
|
|
97
|
+
return { ok: true, value: snapshot };
|
|
98
|
+
}
|
|
99
|
+
case 'screenshot': {
|
|
100
|
+
const serial = serialOf(payload);
|
|
101
|
+
const devicePath = `/data/local/tmp/dsh-shot-${Date.now()}.png`;
|
|
102
|
+
const localPath = join(tmpdir(), `dsh-shot-${Date.now()}.png`);
|
|
103
|
+
try {
|
|
104
|
+
await requireDevice(ctx, cfg, ['shell', 'screencap', '-p', devicePath], { signal, serial });
|
|
105
|
+
await requireDevice(ctx, cfg, ['pull', devicePath, localPath], { signal, serial });
|
|
106
|
+
const bytes = readFileSync(localPath);
|
|
107
|
+
return { ok: true, value: { mime: 'image/png', bytes: bytes.length, dataUrl: `data:image/png;base64,${bytes.toString('base64')}` } };
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
try {
|
|
111
|
+
unlinkSync(localPath);
|
|
112
|
+
}
|
|
113
|
+
catch { /* temp cleanup is best-effort */ }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
case 'perfSample': {
|
|
117
|
+
const pkg = typeof payload.package === 'string' ? payload.package : undefined;
|
|
118
|
+
if (pkg === undefined)
|
|
119
|
+
throw new Error('perfSample requires a string "package"');
|
|
120
|
+
const snapshot = await capturePerfSnapshot(ctx, cfg, signal, { package: pkg, serial: serialOf(payload), metrics: ['meminfo', 'battery'] });
|
|
121
|
+
return { ok: true, value: { package: pkg, meminfo: snapshot.meminfo, battery: snapshot.battery } };
|
|
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
|
+
}
|
|
45
132
|
default:
|
|
46
133
|
return { ok: false, error: { message: `unknown endpoint: ${endpoint}` } };
|
|
47
134
|
}
|
|
@@ -55,12 +142,12 @@ export async function handleRpcEndpoint(ctx, cfg, endpoint, raw, signal) {
|
|
|
55
142
|
* (conversation.view tab "设备"). Called lazily once the client connection
|
|
56
143
|
* mounts; headless compositions (no connection) stay unaffected.
|
|
57
144
|
*/
|
|
58
|
-
export function registerRpc(ctx, cfg) {
|
|
145
|
+
export function registerRpc(ctx, cfg, reportDir) {
|
|
59
146
|
const connection = ctx.get('connection');
|
|
60
147
|
const rpc = connection?.rpc;
|
|
61
148
|
if (rpc === undefined)
|
|
62
149
|
return;
|
|
63
|
-
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),
|
|
64
151
|
// Browser-only channel: accept requests from the loopback web GUI.
|
|
65
152
|
{ authority: 'loopback' }));
|
|
66
153
|
}
|
package/lib/skill.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
/** Runtime skill: automated crash-scene collection + analysis for automotive/Android devices. */
|
|
3
|
+
export declare const CRASH_ANALYSIS_SKILL: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
whenToUse: string;
|
|
7
|
+
content: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerSkills(ctx: Context): void;
|
package/lib/skill.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Runtime skill: automated crash-scene collection + analysis for automotive/Android devices. */
|
|
2
|
+
export const CRASH_ANALYSIS_SKILL = {
|
|
3
|
+
name: 'dsh-adb-crash-analysis',
|
|
4
|
+
description: '采集车机/安卓设备崩溃现场(crash buffer / dropbox / 进程 / 内存)并输出结构化分析报告',
|
|
5
|
+
whenToUse: '遇到车机应用崩溃、ANR、无响应,或需要系统性采集崩溃上下文做稳定性分析时',
|
|
6
|
+
content: `# 车机崩溃现场分析
|
|
7
|
+
|
|
8
|
+
用 dsh-adb 采集崩溃现场并给出结构化结论。适合实车/台架联调与稳定性回归。
|
|
9
|
+
|
|
10
|
+
## 流程
|
|
11
|
+
|
|
12
|
+
1. **确认设备**:调用 \`adb_devices\` 拿到设备 serial;多设备时用 \`-s <serial>\` 显式指定。
|
|
13
|
+
2. **采集崩溃现场**:调用 \`adb_crash_report\`(package 为目标应用,如 com.example.hmi),一次拿到:
|
|
14
|
+
- crashBuffer:logcat crash 缓冲区的结构化条目(AndroidRuntime 堆栈、native crash)
|
|
15
|
+
- dropbox:系统 dropbox 的崩溃条目摘录
|
|
16
|
+
- processes:当前进程状态摘录
|
|
17
|
+
- meminfo:目标应用内存摘要(PSS/堆)
|
|
18
|
+
3. **定位主崩溃**:从 crashBuffer 里找 \`FATAL EXCEPTION\` 或 \`AndroidRuntime\` E 级条目;提取崩溃线程、异常类型(如 NullPointerException / RuntimeException / SIGSEGV)、首次出现时间与最后出现时间。
|
|
19
|
+
4. **交叉验证**:
|
|
20
|
+
- 有堆栈 → 结合进程状态判断是否仍在运行/重启循环
|
|
21
|
+
- 内存异常(PSS 过高/持续增长)→ 用 \`adb_perf_snapshot\` 补充 meminfo/gfxinfo
|
|
22
|
+
- dropbox 有 \`SYSTEM_TOMBSTONE\`/ANR 条目 → 一并纳入
|
|
23
|
+
5. **输出报告**(结构化):
|
|
24
|
+
- 崩溃进程与包名、时间线(首现/末现)、异常类型与关键堆栈(截断到可读)
|
|
25
|
+
- 根因线索(代码位置、资源、并发、内存)与置信度
|
|
26
|
+
- 建议动作(修代码 / 加防护 / 复测方案)
|
|
27
|
+
|
|
28
|
+
## 与定时任务组合(dsh-automation)
|
|
29
|
+
|
|
30
|
+
在 dsh-automation 中建每日/每版本任务,prompt 指向本流程:
|
|
31
|
+
「按 dsh-adb-crash-analysis 流程,对 <设备> 上 <包名> 采集崩溃现场并输出分析报告,保留可审计历史。」
|
|
32
|
+
配合 \`adb_crash_report\` 的 since 参数可只采集指定时间窗口之后的崩溃。
|
|
33
|
+
`,
|
|
34
|
+
};
|
|
35
|
+
export function registerSkills(ctx) {
|
|
36
|
+
const skills = ctx.get('skills');
|
|
37
|
+
if (skills === undefined)
|
|
38
|
+
return;
|
|
39
|
+
ctx.effect(() => skills.register(CRASH_ANALYSIS_SKILL));
|
|
40
|
+
}
|
|
@@ -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.
|
|
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",
|
|
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": {
|