dsh-adb 1.0.0 → 1.1.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 +2 -2
- package/client.js +453 -141
- package/lib/index.js +2 -0
- package/lib/parsers/sysinfo.d.ts +16 -0
- package/lib/parsers/sysinfo.js +54 -0
- package/lib/rpc.d.ts +0 -5
- package/lib/rpc.js +100 -24
- package/lib/skill.d.ts +9 -0
- package/lib/skill.js +40 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@ dsh plugin --profile web add dsh-adb
|
|
|
14
14
|
|
|
15
15
|
Or install directly from GitHub: `dsh plugin --profile web add github:SamXiaBing/dsh-adb`
|
|
16
16
|
|
|
17
|
-
## Web device panel (v1.
|
|
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-
|
|
19
|
+
A "设备" tab in the conversation view ring (next to chat / trajectory / automation): device list with status, package autocomplete (fuzzy search), live streaming logcat window (level/keyword/package/pid filters, pause/clear/auto-scroll), device info card, process list, and performance snapshot — plus harness synergy: **send any logcat/snapshot to the conversation** (the agent analyzes it), a live strip of the agent's adb operations, and a registered **crash-analysis skill** (`dsh-adb-crash-analysis`) for automation pipelines. Data flows over the package RPC channel; install into a web profile and restart the GUI (see `scripts/restart-web.ps1` for a one-click restart).
|
|
20
20
|
|
|
21
21
|
## Ecosystem
|
|
22
22
|
|
package/client.js
CHANGED
|
@@ -1,156 +1,468 @@
|
|
|
1
|
-
/* dsh-adb Web device panel (client half) — plain JS, no build step.
|
|
2
|
-
* Registers a "设备" tab in conversation.view
|
|
3
|
-
*
|
|
4
|
-
*
|
|
1
|
+
/* dsh-adb Web device panel (client half, v1.1.3) — plain JS, no build step.
|
|
2
|
+
* Registers a "设备" tab in conversation.view and talks to the Host half over
|
|
3
|
+
* the package RPC channel /dsh-adb. Uses only client builtins: React, ctx.
|
|
4
|
+
* v1.1.3: i18n (follows harness locale via the slots `locale:` seat) + state
|
|
5
|
+
* survives tab switches (defineStore declared at register).
|
|
6
|
+
* Dual-mode: browser registers via __ModuleLoader__; Node (tests via node:vm
|
|
7
|
+
* with a fake `module`) exports the pure helpers + dictionary.
|
|
5
8
|
*/
|
|
6
|
-
|
|
7
|
-
id: 'dsh-adb',
|
|
8
|
-
factory: (require) => {
|
|
9
|
-
'use strict'
|
|
10
|
-
const module = { exports: {} }
|
|
9
|
+
'use strict'
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const CHANNEL = '/dsh-adb'
|
|
11
|
+
// ---- Pure helpers (shared by the panel and the unit tests) ----
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
function nodeArrayOf(snapshot) {
|
|
14
|
+
try {
|
|
15
|
+
const nodes = snapshot && snapshot.chat && snapshot.chat.nodes
|
|
16
|
+
if (!nodes) return []
|
|
17
|
+
if (typeof nodes.values === 'function') return Array.from(nodes.values())
|
|
18
|
+
if (Array.isArray(nodes)) return nodes
|
|
19
|
+
return []
|
|
20
|
+
} catch {
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Extract recent adb_* tool calls from conversation nodes: [{name, time}]. */
|
|
26
|
+
function extractAdbActivity(nodes) {
|
|
27
|
+
const out = []
|
|
28
|
+
const list = Array.isArray(nodes) ? nodes : []
|
|
29
|
+
for (const node of list) {
|
|
30
|
+
if (!node || node.kind !== 'tool-call') continue
|
|
31
|
+
const root = node.root
|
|
32
|
+
const name = root && typeof root.name === 'string' ? root.name : ''
|
|
33
|
+
if (!name.startsWith('adb_')) continue
|
|
34
|
+
out.push({ name, time: root.time })
|
|
35
|
+
}
|
|
36
|
+
return out.slice(-8).reverse()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Format a logcat entry list into a send-to-conversation text block. */
|
|
40
|
+
function formatLogcatBlock(entries) {
|
|
41
|
+
const lines = (Array.isArray(entries) ? entries : [])
|
|
42
|
+
.map((e) => `${e.time} ${e.pid} ${e.tid} ${e.level} ${e.tag}: ${e.message}`)
|
|
43
|
+
return [`以下是从设备面板抓取的 logcat 片段(${lines.length} 条),请分析:`, '```log', ...lines, '```'].join('\n')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Format a perf snapshot into a send-to-conversation text block. */
|
|
47
|
+
function formatSnapshotBlock(snapshot) {
|
|
48
|
+
const rows = []
|
|
49
|
+
const m = snapshot && snapshot.meminfo
|
|
50
|
+
const g = snapshot && snapshot.gfxinfo
|
|
51
|
+
const b = snapshot && snapshot.battery
|
|
52
|
+
if (m) rows.push(`内存 PSS=${m.totalPssKb}KB RSS=${m.totalRssKb}KB JavaHeap=${m.javaHeapKb}KB NativeHeap=${m.nativeHeapKb}KB`)
|
|
53
|
+
if (g) rows.push(`帧=${g.totalFrames} 卡顿=${g.jankyFrames}(${g.jankyPercent}%) P50=${g.percentile50Ms}ms P90=${g.percentile90Ms}ms P95=${g.percentile95Ms}ms P99=${g.percentile99Ms}ms MissedVsync=${g.missedVsync}`)
|
|
54
|
+
if (b) rows.push(`电量=${b.levelPercent}% 温度=${b.temperatureC}°C`)
|
|
55
|
+
return ['以下是从设备面板抓取的性能快照,请分析:', ...rows.map((r) => '- ' + r)].join('\n')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Panel dictionary: zh is the key-set source of truth, en mirrors it. */
|
|
59
|
+
const DICTIONARY = {
|
|
60
|
+
zh: {
|
|
61
|
+
'panel.title': 'ADB 设备',
|
|
62
|
+
'refresh': '刷新',
|
|
63
|
+
'noDevices': '未连接设备',
|
|
64
|
+
'deviceInfo': '设备信息',
|
|
65
|
+
'package': '包名',
|
|
66
|
+
'packagePlaceholder': '输入或选择包名',
|
|
67
|
+
'snapshot': '性能快照',
|
|
68
|
+
'sendToChat': '发送到对话',
|
|
69
|
+
'processes': '进程({n})— 点击按 pid 过滤 logcat',
|
|
70
|
+
'logcat': 'logcat',
|
|
71
|
+
'keywordFilter': '关键字过滤',
|
|
72
|
+
'packageFilter': '包名过滤(按进程)',
|
|
73
|
+
'resume': '继续',
|
|
74
|
+
'pause': '暂停',
|
|
75
|
+
'clear': '清空',
|
|
76
|
+
'autoScroll': '自动滚动',
|
|
77
|
+
'shownCount': '已显示 {n} 条',
|
|
78
|
+
'refreshing': '每 1.5s 增量刷新',
|
|
79
|
+
'paused': '已暂停',
|
|
80
|
+
'waitingLog': '(等待日志…)',
|
|
81
|
+
'agentActivity': 'agent 的 adb 操作',
|
|
82
|
+
'noData': '(无数据)',
|
|
83
|
+
'model': '型号', 'manufacturer': '厂商', 'android': 'Android', 'api': 'API',
|
|
84
|
+
'resolution': '分辨率', 'memTotal': '内存总量',
|
|
85
|
+
'memPss': '内存 PSS (KB)', 'memRss': '内存 RSS (KB)', 'javaHeap': 'Java Heap (KB)', 'nativeHeap': 'Native Heap (KB)',
|
|
86
|
+
'frames': '总帧数', 'janky': '卡顿帧 / %', 'p50p90': 'P50/P90 (ms)', 'p95p99': 'P95/P99 (ms)',
|
|
87
|
+
'battery': '电量', 'temp': '温度 (°C)', 'pkg': '包=', 'pid': 'pid=',
|
|
88
|
+
},
|
|
89
|
+
en: {
|
|
90
|
+
'panel.title': 'ADB Devices',
|
|
91
|
+
'refresh': 'Refresh',
|
|
92
|
+
'noDevices': 'No device connected',
|
|
93
|
+
'deviceInfo': 'Device Info',
|
|
94
|
+
'package': 'Package',
|
|
95
|
+
'packagePlaceholder': 'Type or pick a package',
|
|
96
|
+
'snapshot': 'Perf Snapshot',
|
|
97
|
+
'sendToChat': 'Send to chat',
|
|
98
|
+
'processes': 'Processes ({n}) — click to filter logcat by pid',
|
|
99
|
+
'logcat': 'logcat',
|
|
100
|
+
'keywordFilter': 'Keyword filter',
|
|
101
|
+
'packageFilter': 'Package filter (by process)',
|
|
102
|
+
'resume': 'Resume',
|
|
103
|
+
'pause': 'Pause',
|
|
104
|
+
'clear': 'Clear',
|
|
105
|
+
'autoScroll': 'Auto-scroll',
|
|
106
|
+
'shownCount': '{n} entries shown',
|
|
107
|
+
'refreshing': 'incremental 1.5s refresh',
|
|
108
|
+
'paused': 'paused',
|
|
109
|
+
'waitingLog': '(waiting for logs…)',
|
|
110
|
+
'agentActivity': "Agent's adb activity",
|
|
111
|
+
'noData': '(no data)',
|
|
112
|
+
'model': 'Model', 'manufacturer': 'Manufacturer', 'android': 'Android', 'api': 'API',
|
|
113
|
+
'resolution': 'Resolution', 'memTotal': 'Total memory',
|
|
114
|
+
'memPss': 'PSS (KB)', 'memRss': 'RSS (KB)', 'javaHeap': 'Java Heap (KB)', 'nativeHeap': 'Native Heap (KB)',
|
|
115
|
+
'frames': 'Total frames', 'janky': 'Janky / %', 'p50p90': 'P50/P90 (ms)', 'p95p99': 'P95/P99 (ms)',
|
|
116
|
+
'battery': 'Battery', 'temp': 'Temp (°C)', 'pkg': 'pkg=', 'pid': 'pid=',
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- Browser entry ----
|
|
121
|
+
|
|
122
|
+
if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object') {
|
|
123
|
+
window.__ModuleLoader__.load({
|
|
124
|
+
id: 'dsh-adb',
|
|
125
|
+
factory: (require) => {
|
|
126
|
+
const module = { exports: {} }
|
|
127
|
+
|
|
128
|
+
const React = require('react')
|
|
129
|
+
const { defineStore } = require('@deepseek-ai/dsh-client-runtime/client')
|
|
130
|
+
const CHANNEL = '/dsh-adb'
|
|
131
|
+
|
|
132
|
+
function unwrap(value) {
|
|
133
|
+
if (typeof value !== 'object' || value === null || !('ok' in value)) {
|
|
134
|
+
throw new Error('dsh-adb host returned an invalid response.')
|
|
135
|
+
}
|
|
136
|
+
if (value.ok === true && 'value' in value) return value.value
|
|
137
|
+
if (value.ok === false && value.error) {
|
|
138
|
+
throw new Error(value.error.message ?? 'dsh-adb request failed.')
|
|
139
|
+
}
|
|
17
140
|
throw new Error('dsh-adb host returned an invalid response.')
|
|
18
141
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
logcatTail: (payload) => rpc.call(CHANNEL, 'logcatTail', payload).then(unwrap),
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const h = React.createElement
|
|
35
|
-
const ROW = { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0' }
|
|
36
|
-
const BTN = { padding: '3px 10px', cursor: 'pointer' }
|
|
37
|
-
const INPUT = { padding: '3px 6px' }
|
|
38
|
-
|
|
39
|
-
function MetricRows({ rows }) {
|
|
40
|
-
if (!rows || rows.length === 0) return h('div', null, '(无数据)')
|
|
41
|
-
return h('table', { style: { borderCollapse: 'collapse' } },
|
|
42
|
-
rows.map((row) => h('tr', { key: row[0] },
|
|
43
|
-
h('td', { style: { padding: '2px 12px 2px 0', color: 'var(--dsh-text-secondary, #888)' } }, row[0]),
|
|
44
|
-
h('td', { style: { padding: '2px 0' } }, String(row[1])),
|
|
45
|
-
)),
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function DeviceView(props) {
|
|
50
|
-
const runtime = props.runtime
|
|
51
|
-
const [devices, setDevices] = React.useState([])
|
|
52
|
-
const [selected, setSelected] = React.useState(null)
|
|
53
|
-
const [pkg, setPkg] = React.useState('com.android.systemui')
|
|
54
|
-
const [snapshot, setSnapshot] = React.useState(null)
|
|
55
|
-
const [logEntries, setLogEntries] = React.useState([])
|
|
56
|
-
const [logTotal, setLogTotal] = React.useState(0)
|
|
57
|
-
const [level, setLevel] = React.useState('E')
|
|
58
|
-
const [tail, setTail] = React.useState('30')
|
|
59
|
-
const [error, setError] = React.useState(null)
|
|
60
|
-
const [busy, setBusy] = React.useState(false)
|
|
61
|
-
|
|
62
|
-
const refresh = () => {
|
|
63
|
-
setBusy(true); setError(null)
|
|
64
|
-
runtime.listDevices()
|
|
65
|
-
.then((value) => setDevices(value.devices ?? []))
|
|
66
|
-
.catch((e) => setError(String(e.message ?? e)))
|
|
67
|
-
.finally(() => setBusy(false))
|
|
142
|
+
|
|
143
|
+
function createRuntime(rpc) {
|
|
144
|
+
const call = (endpoint, payload) => rpc.call(CHANNEL, endpoint, payload).then(unwrap)
|
|
145
|
+
return {
|
|
146
|
+
listDevices: () => call('listDevices', {}),
|
|
147
|
+
listPackages: (payload) => call('listPackages', payload),
|
|
148
|
+
deviceInfo: (payload) => call('deviceInfo', payload),
|
|
149
|
+
processList: (payload) => call('processList', payload),
|
|
150
|
+
logcatDelta: (payload) => call('logcatDelta', payload),
|
|
151
|
+
perfSnapshot: (payload) => call('perfSnapshot', payload),
|
|
152
|
+
}
|
|
68
153
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
154
|
+
|
|
155
|
+
const h = React.createElement
|
|
156
|
+
const ROW = { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', flexWrap: 'wrap' }
|
|
157
|
+
const BTN = { padding: '3px 10px', cursor: 'pointer' }
|
|
158
|
+
const INPUT = { padding: '3px 6px' }
|
|
159
|
+
const SECTION = { marginTop: 14, borderTop: '1px solid var(--dsh-border, #ddd)', paddingTop: 10 }
|
|
160
|
+
const DROPDOWN = {
|
|
161
|
+
position: 'absolute', top: '100%', left: 0, right: 0, maxHeight: 180, overflowY: 'auto',
|
|
162
|
+
background: 'var(--dsh-bg, #ffffff)', color: 'inherit',
|
|
163
|
+
border: '1px solid var(--dsh-border, #ccc)', borderRadius: 4, zIndex: 20, boxShadow: '0 2px 8px rgba(0,0,0,.2)',
|
|
78
164
|
}
|
|
165
|
+
const DROPDOWN_ITEM = { padding: '4px 8px', cursor: 'pointer', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
|
|
79
166
|
|
|
80
|
-
|
|
81
|
-
if (!
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
167
|
+
function MetricRows({ rows }) {
|
|
168
|
+
if (!rows || rows.length === 0) return h('div', null, '(无数据)')
|
|
169
|
+
return h('table', { style: { borderCollapse: 'collapse' } },
|
|
170
|
+
rows.map((row) => h('tr', { key: row[0] },
|
|
171
|
+
h('td', { style: { padding: '2px 12px 2px 0', color: 'var(--dsh-text-secondary, #888)' } }, row[0]),
|
|
172
|
+
h('td', { style: { padding: '2px 0' } }, String(row[1])),
|
|
173
|
+
)),
|
|
174
|
+
)
|
|
88
175
|
}
|
|
89
176
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
177
|
+
function PackageCombobox({ packages, value, placeholder, onChange }) {
|
|
178
|
+
const [query, setQuery] = React.useState('')
|
|
179
|
+
const [open, setOpen] = React.useState(false)
|
|
180
|
+
const shown = open
|
|
181
|
+
? packages.filter((name) => query === '' || name.toLowerCase().includes(query.toLowerCase())).slice(0, 60)
|
|
182
|
+
: []
|
|
183
|
+
return h('div', { style: { position: 'relative', flex: 1, minWidth: 220 } },
|
|
184
|
+
h('input', {
|
|
185
|
+
style: { ...INPUT, width: '100%', boxSizing: 'border-box' },
|
|
186
|
+
value: query,
|
|
187
|
+
placeholder: value || placeholder || '',
|
|
188
|
+
onChange: (e) => { setQuery(e.target.value); setOpen(true) },
|
|
189
|
+
onFocus: () => setOpen(true),
|
|
190
|
+
onBlur: () => setTimeout(() => { setOpen(false); setQuery('') }, 150),
|
|
191
|
+
onKeyDown: (e) => {
|
|
192
|
+
if (e.key === 'Enter' && shown.length > 0) { onChange(shown[0]); setQuery(''); setOpen(false) }
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
open && shown.length > 0 && h('div', { style: DROPDOWN },
|
|
196
|
+
shown.map((name) => h('div', {
|
|
197
|
+
key: name,
|
|
198
|
+
onMouseDown: () => { onChange(name); setQuery(''); setOpen(false) },
|
|
199
|
+
onMouseEnter: (e) => { e.currentTarget.style.background = 'var(--dsh-accent-soft, rgba(66,133,244,.12))' },
|
|
200
|
+
onMouseLeave: (e) => { e.currentTarget.style.background = 'transparent' },
|
|
201
|
+
style: DROPDOWN_ITEM,
|
|
202
|
+
}, name))),
|
|
203
|
+
)
|
|
96
204
|
}
|
|
97
205
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
206
|
+
function DeviceView(props) {
|
|
207
|
+
const runtime = props.runtime
|
|
208
|
+
const t = props.t || ((key) => DICTIONARY.zh[key] ?? key)
|
|
209
|
+
const st = props.useStore((s) => s)
|
|
210
|
+
const actions = props.actions
|
|
211
|
+
const [busy, setBusy] = React.useState(false)
|
|
212
|
+
const logRef = React.useRef(null)
|
|
213
|
+
const stateRef = React.useRef(st)
|
|
214
|
+
stateRef.current = st
|
|
215
|
+
|
|
216
|
+
const adbActivity = props.useSession
|
|
217
|
+
? props.useSession((snap) => extractAdbActivity(nodeArrayOf(snap)))
|
|
218
|
+
: []
|
|
219
|
+
const sendToChat = (text) => {
|
|
220
|
+
if (props.inputActions && typeof props.inputActions.setDraft === 'function') {
|
|
221
|
+
props.inputActions.setDraft(text)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const fail = (e) => actions.setError(String((e && e.message) || e))
|
|
226
|
+
|
|
227
|
+
const refresh = () => {
|
|
228
|
+
setBusy(true); actions.setError(null)
|
|
229
|
+
runtime.listDevices()
|
|
230
|
+
.then((value) => actions.setDevices(value.devices ?? []))
|
|
231
|
+
.catch(fail)
|
|
232
|
+
.finally(() => setBusy(false))
|
|
233
|
+
}
|
|
234
|
+
React.useEffect(refresh, [])
|
|
235
|
+
|
|
236
|
+
const selectDevice = (device) => {
|
|
237
|
+
actions.setSelected(device)
|
|
238
|
+
actions.setInfo(null); actions.setSnapshot(null); actions.setProcesses([])
|
|
239
|
+
actions.clearLog()
|
|
240
|
+
setBusy(true); actions.setError(null)
|
|
241
|
+
Promise.all([
|
|
242
|
+
runtime.deviceInfo({ serial: device.serial }).then(actions.setInfo),
|
|
243
|
+
runtime.listPackages({ serial: device.serial }).then((v) => actions.setPackages(v.packages ?? [])),
|
|
244
|
+
]).catch(fail).finally(() => setBusy(false))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const loadProcesses = (pkgName) => {
|
|
248
|
+
const device = stateRef.current.selected
|
|
249
|
+
if (!device) return
|
|
250
|
+
runtime.processList({ serial: device.serial, package: pkgName })
|
|
251
|
+
.then((v) => actions.setProcesses(v.processes ?? []))
|
|
252
|
+
.catch(fail)
|
|
253
|
+
}
|
|
254
|
+
React.useEffect(() => { if (st.selected) loadProcesses(st.pkg) }, [st.pkg]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
255
|
+
|
|
256
|
+
const runSnapshot = () => {
|
|
257
|
+
const device = stateRef.current.selected
|
|
258
|
+
if (!device) return
|
|
259
|
+
setBusy(true); actions.setError(null)
|
|
260
|
+
runtime.perfSnapshot({ serial: device.serial, package: st.pkg })
|
|
261
|
+
.then(actions.setSnapshot)
|
|
262
|
+
.catch(fail)
|
|
263
|
+
.finally(() => setBusy(false))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const applyPackageFilter = (name) => {
|
|
267
|
+
actions.setLogPkg(name)
|
|
268
|
+
actions.clearLog()
|
|
269
|
+
if (name === '') { actions.setLogPids([]); return }
|
|
270
|
+
const device = stateRef.current.selected
|
|
271
|
+
if (!device) return
|
|
272
|
+
runtime.processList({ serial: device.serial, package: name })
|
|
273
|
+
.then((v) => actions.setLogPids((v.processes ?? []).map((p) => p.pid)))
|
|
274
|
+
.catch(fail)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const applyLogFilters = (level, keyword, pids) => {
|
|
278
|
+
actions.setLogLevel(level)
|
|
279
|
+
actions.setLogKeyword(keyword)
|
|
280
|
+
actions.setLogPids(pids)
|
|
281
|
+
actions.clearLog()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
React.useEffect(() => {
|
|
285
|
+
if (!st.selected || st.logPaused) return
|
|
286
|
+
const timer = setInterval(() => {
|
|
287
|
+
const s = stateRef.current
|
|
288
|
+
runtime.logcatDelta({
|
|
289
|
+
serial: s.selected.serial,
|
|
290
|
+
since: s.logSince,
|
|
291
|
+
level: s.logLevel,
|
|
292
|
+
keyword: s.logKeyword || undefined,
|
|
293
|
+
tail: 200,
|
|
294
|
+
}).then((value) => {
|
|
295
|
+
let entries = value.entries ?? []
|
|
296
|
+
if (s.logPids.length > 0) entries = entries.filter((e) => s.logPids.includes(e.pid))
|
|
297
|
+
if (entries.length > 0) {
|
|
298
|
+
actions.appendLog(entries)
|
|
299
|
+
actions.setLogSince(entries[entries.length - 1].time)
|
|
300
|
+
}
|
|
301
|
+
}).catch(() => { /* transient poll errors are ignored */ })
|
|
302
|
+
}, 1500)
|
|
303
|
+
return () => clearInterval(timer)
|
|
304
|
+
}, [st.selected, st.logPaused]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
305
|
+
|
|
306
|
+
React.useEffect(() => {
|
|
307
|
+
const el = logRef.current
|
|
308
|
+
if (el && st.logAuto) el.scrollTop = el.scrollHeight
|
|
309
|
+
}, [st.logEntries, st.logAuto])
|
|
310
|
+
|
|
311
|
+
const snapshotRows = []
|
|
312
|
+
if (st.snapshot) {
|
|
313
|
+
const m = st.snapshot.meminfo; const g = st.snapshot.gfxinfo; const b = st.snapshot.battery
|
|
314
|
+
if (m) snapshotRows.push([t('memPss'), m.totalPssKb], [t('memRss'), m.totalRssKb], [t('javaHeap'), m.javaHeapKb], [t('nativeHeap'), m.nativeHeapKb])
|
|
315
|
+
if (g) snapshotRows.push([t('frames'), g.totalFrames], [t('janky'), `${g.jankyFrames} / ${g.jankyPercent}%`], [t('p50p90'), `${g.percentile50Ms} / ${g.percentile90Ms}`], [t('p95p99'), `${g.percentile95Ms} / ${g.percentile99Ms}`])
|
|
316
|
+
if (b) snapshotRows.push([t('battery'), `${b.levelPercent}%`], [t('temp'), b.temperatureC])
|
|
317
|
+
}
|
|
318
|
+
const infoRows = st.info
|
|
319
|
+
? [[t('model'), st.info.model], [t('manufacturer'), st.info.manufacturer], [t('android'), st.info.release], [t('api'), st.info.sdk], [t('resolution'), st.info.resolution], [t('memTotal'), st.info.memTotalKb ? `${Math.round(st.info.memTotalKb / 1024)} MB` : undefined]].filter((r) => r[1] !== undefined && r[1] !== null)
|
|
320
|
+
: []
|
|
321
|
+
|
|
322
|
+
const pidsLabel = st.logPids.length > 0 ? ` · ${t('pid')}${st.logPids.join(',')}` : ''
|
|
323
|
+
const statusText = `${t('shownCount').replace('{n}', String(st.logEntries.length))}${st.logPkg ? ` · ${t('pkg')}${st.logPkg}` : ''}${pidsLabel}${st.logPaused ? ` · ${t('paused')}` : ` · ${t('refreshing')}`}`
|
|
324
|
+
|
|
325
|
+
return h('div', { style: { padding: 12, fontFamily: 'inherit', fontSize: 13 } },
|
|
326
|
+
h('div', { style: { ...ROW, justifyContent: 'space-between' } },
|
|
327
|
+
h('strong', null, t('panel.title')),
|
|
328
|
+
h('button', { style: BTN, onClick: refresh, disabled: busy }, busy ? '…' : t('refresh')),
|
|
120
329
|
),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
h('
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
330
|
+
st.error !== null && h('div', { style: { color: '#e5484d', margin: '6px 0', wordBreak: 'break-all' } }, String(st.error)),
|
|
331
|
+
st.devices.length === 0
|
|
332
|
+
? h('div', { style: { color: 'var(--dsh-text-secondary, #888)', margin: '8px 0' } }, t('noDevices'))
|
|
333
|
+
: h('div', null, st.devices.map((d) =>
|
|
334
|
+
h('button', {
|
|
335
|
+
key: d.serial,
|
|
336
|
+
onClick: () => selectDevice(d),
|
|
337
|
+
style: { ...BTN, display: 'block', width: '100%', textAlign: 'left', margin: '2px 0',
|
|
338
|
+
background: st.selected && st.selected.serial === d.serial ? 'var(--dsh-accent-soft, rgba(66,133,244,.15))' : 'transparent' },
|
|
339
|
+
}, `${d.serial} · ${d.state}${d.model ? ' · ' + d.model : ''}`),
|
|
340
|
+
)),
|
|
341
|
+
|
|
342
|
+
adbActivity.length > 0 && h('div', { style: SECTION },
|
|
343
|
+
h('strong', null, t('agentActivity')),
|
|
344
|
+
h('div', { style: { marginTop: 4, fontSize: 12, color: 'var(--dsh-text-secondary, #888)' } },
|
|
345
|
+
adbActivity.map((a, i) => h('div', { key: `${a.name}-${i}` }, `• ${a.name}${a.time ? ' @ ' + a.time : ''}`))),
|
|
130
346
|
),
|
|
131
|
-
|
|
132
|
-
h('div', { style: {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
347
|
+
|
|
348
|
+
st.selected !== null && h('div', { style: { marginTop: 14 } },
|
|
349
|
+
st.info !== null && infoRows.length > 0 && h('div', { style: SECTION },
|
|
350
|
+
h('strong', null, t('deviceInfo')),
|
|
351
|
+
h(MetricRows, { rows: infoRows }),
|
|
352
|
+
),
|
|
353
|
+
|
|
354
|
+
h('div', { style: SECTION },
|
|
355
|
+
h('div', { style: ROW },
|
|
356
|
+
h('label', null, t('package')),
|
|
357
|
+
h(PackageCombobox, { packages: st.packages, value: st.pkg, placeholder: t('packagePlaceholder'), onChange: (name) => actions.setPkg(name) }),
|
|
358
|
+
h('button', { style: BTN, onClick: runSnapshot, disabled: busy }, t('snapshot')),
|
|
359
|
+
st.snapshot && h('button', { style: BTN, onClick: () => sendToChat(formatSnapshotBlock(st.snapshot)) }, t('sendToChat')),
|
|
360
|
+
),
|
|
361
|
+
st.snapshot && h('div', { style: { marginTop: 8 } }, h(MetricRows, { rows: snapshotRows })),
|
|
362
|
+
|
|
363
|
+
st.processes.length > 0 && h('div', { style: { marginTop: 8 } },
|
|
364
|
+
h('div', { style: { color: 'var(--dsh-text-secondary, #888)' } }, t('processes').replace('{n}', String(st.processes.length))),
|
|
365
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 } },
|
|
366
|
+
st.processes.map((p) => {
|
|
367
|
+
const active = st.logPids.length === 1 && st.logPids[0] === p.pid
|
|
368
|
+
return h('button', {
|
|
369
|
+
key: p.pid,
|
|
370
|
+
onClick: () => { const next = active ? [] : [p.pid]; applyLogFilters(st.logLevel, st.logKeyword, next) },
|
|
371
|
+
style: { ...BTN, fontSize: 11, background: active ? 'var(--dsh-accent-soft, rgba(66,133,244,.15))' : 'transparent' },
|
|
372
|
+
}, `${p.pid} ${p.name}`)
|
|
373
|
+
})),
|
|
374
|
+
),
|
|
375
|
+
),
|
|
376
|
+
|
|
377
|
+
h('div', { style: SECTION },
|
|
378
|
+
h('div', { style: ROW },
|
|
379
|
+
h('label', null, t('logcat')),
|
|
380
|
+
h('select', { style: INPUT, value: st.logLevel, onChange: (e) => applyLogFilters(e.target.value, st.logKeyword, st.logPids) },
|
|
381
|
+
['V', 'D', 'I', 'W', 'E', 'F'].map((lv) => h('option', { key: lv, value: lv }, lv))),
|
|
382
|
+
h('input', { style: { ...INPUT, minWidth: 130 }, placeholder: t('keywordFilter'), value: st.logKeyword, onChange: (e) => applyLogFilters(st.logLevel, e.target.value, st.logPids) }),
|
|
383
|
+
h('div', { style: { flex: 1, minWidth: 180 } },
|
|
384
|
+
h(PackageCombobox, { packages: st.packages, value: st.logPkg, placeholder: st.logPkg || t('packageFilter'), onChange: applyPackageFilter })),
|
|
385
|
+
h('button', { style: BTN, onClick: () => actions.setLogPaused(!st.logPaused) }, st.logPaused ? t('resume') : t('pause')),
|
|
386
|
+
h('button', { style: BTN, onClick: () => actions.clearLog() }, t('clear')),
|
|
387
|
+
h('button', { style: BTN, onClick: () => sendToChat(formatLogcatBlock(st.logEntries)), disabled: st.logEntries.length === 0 }, t('sendToChat')),
|
|
388
|
+
h('label', { style: { fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 } },
|
|
389
|
+
h('input', { type: 'checkbox', checked: st.logAuto, onChange: (e) => actions.setLogAuto(e.target.checked) }), ' ', t('autoScroll')),
|
|
390
|
+
),
|
|
391
|
+
h('div', { style: { color: 'var(--dsh-text-secondary, #888)', margin: '4px 0', fontSize: 12 } }, statusText),
|
|
392
|
+
h('div', { ref: logRef, style: { maxHeight: 300, overflowY: 'auto', fontFamily: 'monospace', fontSize: 12, whiteSpace: 'pre-wrap', border: '1px solid var(--dsh-border, #ccc)', padding: 6 } },
|
|
393
|
+
st.logEntries.length === 0
|
|
394
|
+
? h('div', { style: { color: 'var(--dsh-text-secondary, #888)' } }, t('waitingLog'))
|
|
395
|
+
: st.logEntries.map((e) => h('div', { key: `${e.time}-${e.pid}-${e.tid}-${e.message}` },
|
|
396
|
+
`${e.time} ${e.pid} ${e.tid} ${e.level} ${e.tag}: ${e.message}`))),
|
|
397
|
+
),
|
|
137
398
|
),
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function apply(ctx) {
|
|
403
|
+
const slots = ctx.get('slots')
|
|
404
|
+
const connection = ctx.get('connection')
|
|
405
|
+
if (slots === undefined || connection === undefined || connection.rpc === undefined) return
|
|
406
|
+
const runtime = createRuntime(connection.rpc)
|
|
407
|
+
|
|
408
|
+
// i18n: register the panel dictionary under the harness locale system.
|
|
409
|
+
const locale = ctx.get('locale')
|
|
410
|
+
if (locale && typeof locale.register === 'function') {
|
|
411
|
+
ctx.effect(() => locale.register('dsh-adb', DICTIONARY))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Store: declared at register, so state survives view tab switches.
|
|
415
|
+
const panelStore = defineStore({
|
|
416
|
+
init: () => ({
|
|
417
|
+
devices: [], selected: null, info: null, packages: [], pkg: 'com.android.systemui',
|
|
418
|
+
snapshot: null, processes: [], logEntries: [], logSince: '',
|
|
419
|
+
logLevel: 'V', logKeyword: '', logPkg: '', logPids: [], logPaused: false, logAuto: true, error: null,
|
|
420
|
+
}),
|
|
421
|
+
actions: {
|
|
422
|
+
setDevices: (d, v) => { d.devices = v },
|
|
423
|
+
setSelected: (d, v) => { d.selected = v },
|
|
424
|
+
setInfo: (d, v) => { d.info = v },
|
|
425
|
+
setPackages: (d, v) => { d.packages = v },
|
|
426
|
+
setPkg: (d, v) => { d.pkg = v },
|
|
427
|
+
setSnapshot: (d, v) => { d.snapshot = v },
|
|
428
|
+
setProcesses: (d, v) => { d.processes = v },
|
|
429
|
+
setError: (d, v) => { d.error = v },
|
|
430
|
+
appendLog: (d, entries) => {
|
|
431
|
+
const seen = new Set(d.logEntries.map((e) => `${e.time}:${e.pid}:${e.message}`))
|
|
432
|
+
const fresh = entries.filter((e) => !seen.has(`${e.time}:${e.pid}:${e.message}`))
|
|
433
|
+
d.logEntries = [...d.logEntries, ...fresh].slice(-500)
|
|
434
|
+
},
|
|
435
|
+
setLogSince: (d, v) => { d.logSince = v },
|
|
436
|
+
setLogLevel: (d, v) => { d.logLevel = v },
|
|
437
|
+
setLogKeyword: (d, v) => { d.logKeyword = v },
|
|
438
|
+
setLogPkg: (d, v) => { d.logPkg = v },
|
|
439
|
+
setLogPids: (d, v) => { d.logPids = v },
|
|
440
|
+
setLogPaused: (d, v) => { d.logPaused = v },
|
|
441
|
+
setLogAuto: (d, v) => { d.logAuto = v },
|
|
442
|
+
clearLog: (d) => { d.logEntries = []; d.logSince = '' },
|
|
443
|
+
},
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
slots.inject('conversation.view', () => slots.register(
|
|
447
|
+
{
|
|
448
|
+
name: 'conversation.view',
|
|
449
|
+
id: 'devices',
|
|
450
|
+
order: 30,
|
|
451
|
+
label: () => {
|
|
452
|
+
const loc = ctx.get('locale')
|
|
453
|
+
return loc && typeof loc.getLocale === 'function' && loc.getLocale().active === 'en' ? 'Devices' : '设备'
|
|
454
|
+
},
|
|
455
|
+
store: panelStore,
|
|
456
|
+
locale: 'dsh-adb',
|
|
457
|
+
},
|
|
458
|
+
(props) => h(DeviceView, { ...props, runtime }),
|
|
459
|
+
))
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
module.exports = { apply }
|
|
463
|
+
return module.exports
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
} else if (typeof module !== 'undefined' && module.exports) {
|
|
467
|
+
module.exports = { formatLogcatBlock, formatSnapshotBlock, extractAdbActivity, nodeArrayOf, DICTIONARY }
|
|
468
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerLogcatTool } from './tools/logcat.js';
|
|
|
8
8
|
import { registerPerfTool } from './tools/perf.js';
|
|
9
9
|
import { registerPerfBaselineTool } from './tools/perf-baseline.js';
|
|
10
10
|
import { registerRpc } from './rpc.js';
|
|
11
|
+
import { registerSkills } from './skill.js';
|
|
11
12
|
export const name = 'dsh-adb';
|
|
12
13
|
/** The tool registry is a hard dependency: every tool registers through it. */
|
|
13
14
|
export const inject = ['tools'];
|
|
@@ -26,6 +27,7 @@ export function apply(ctx, config) {
|
|
|
26
27
|
registerPerfTool(ctx, cfg);
|
|
27
28
|
registerPerfBaselineTool(ctx, cfg, config.baselineDir ?? DEFAULT_BASELINE_DIR);
|
|
28
29
|
registerCrashReportTool(ctx, cfg);
|
|
30
|
+
registerSkills(ctx);
|
|
29
31
|
// The RPC channel needs the client connection, which mounts after this
|
|
30
32
|
// plugin starts in web compositions; register lazily so headless profiles
|
|
31
33
|
// (no connection) stay unaffected.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Pure parsers for device-system info (packages, getprop, processes, meminfo, wm). */
|
|
2
|
+
export declare function parsePackageList(text: string): string[];
|
|
3
|
+
export declare function parseGetprop(text: string): Record<string, string>;
|
|
4
|
+
export interface ProcessEntry {
|
|
5
|
+
pid: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
/** Parse `ps -A` output (Android toybox: USER PID PPID VSZ RSS WCHAN ADDR S NAME). */
|
|
9
|
+
export declare function parseProcessList(text: string): ProcessEntry[];
|
|
10
|
+
/** Parse /proc/meminfo "MemTotal: 123456 kB" into KB. */
|
|
11
|
+
export declare function parseMemTotal(text: string): number | undefined;
|
|
12
|
+
/** Parse `wm size` -> "Physical size: 1080x2400". */
|
|
13
|
+
export declare function parseWmSize(text: string): {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
} | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Pure parsers for device-system info (packages, getprop, processes, meminfo, wm). */
|
|
2
|
+
export function parsePackageList(text) {
|
|
3
|
+
const names = [];
|
|
4
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
5
|
+
const line = rawLine.trim();
|
|
6
|
+
if (!line.startsWith('package:'))
|
|
7
|
+
continue;
|
|
8
|
+
const name = line.slice('package:'.length);
|
|
9
|
+
if (name !== '')
|
|
10
|
+
names.push(name);
|
|
11
|
+
}
|
|
12
|
+
return names;
|
|
13
|
+
}
|
|
14
|
+
export function parseGetprop(text) {
|
|
15
|
+
const props = {};
|
|
16
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
17
|
+
const match = /^\[([^\]]+)\]:\s*\[([^\]]*)\]$/.exec(rawLine.trim());
|
|
18
|
+
if (match === null)
|
|
19
|
+
continue;
|
|
20
|
+
props[match[1]] = match[2];
|
|
21
|
+
}
|
|
22
|
+
return props;
|
|
23
|
+
}
|
|
24
|
+
/** Parse `ps -A` output (Android toybox: USER PID PPID VSZ RSS WCHAN ADDR S NAME). */
|
|
25
|
+
export function parseProcessList(text) {
|
|
26
|
+
const entries = [];
|
|
27
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
28
|
+
const line = rawLine.trim();
|
|
29
|
+
if (line === '' || /^USER\s+PID/.test(line))
|
|
30
|
+
continue;
|
|
31
|
+
const parts = line.split(/\s+/);
|
|
32
|
+
// Android ps: index 1 = PID, last = NAME (comm may be in brackets)
|
|
33
|
+
if (parts.length < 2)
|
|
34
|
+
continue;
|
|
35
|
+
const pid = parts[1];
|
|
36
|
+
const name = parts[parts.length - 1];
|
|
37
|
+
if (pid !== undefined && name !== undefined && /^\d+$/.test(pid)) {
|
|
38
|
+
entries.push({ pid, name });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
|
43
|
+
/** Parse /proc/meminfo "MemTotal: 123456 kB" into KB. */
|
|
44
|
+
export function parseMemTotal(text) {
|
|
45
|
+
const match = /^MemTotal:\s+(\d+)\s*kB/im.exec(text);
|
|
46
|
+
return match === null ? undefined : Number(match[1]);
|
|
47
|
+
}
|
|
48
|
+
/** Parse `wm size` -> "Physical size: 1080x2400". */
|
|
49
|
+
export function parseWmSize(text) {
|
|
50
|
+
const match = /Physical size:\s+(\d+)x(\d+)/.exec(text);
|
|
51
|
+
if (match === null)
|
|
52
|
+
return undefined;
|
|
53
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
54
|
+
}
|
package/lib/rpc.d.ts
CHANGED
|
@@ -9,11 +9,6 @@ export type RpcEndpointResult = {
|
|
|
9
9
|
message: string;
|
|
10
10
|
};
|
|
11
11
|
};
|
|
12
|
-
/**
|
|
13
|
-
* Package-private Client↔Host RPC endpoint dispatch for the Web device panel.
|
|
14
|
-
* Extracted from the connection handler so the endpoints are unit-testable
|
|
15
|
-
* against a fake adb backend.
|
|
16
|
-
*/
|
|
17
12
|
export declare function handleRpcEndpoint(ctx: Context, cfg: AdbConfig, endpoint: string, raw: unknown, signal: AbortSignal): Promise<RpcEndpointResult>;
|
|
18
13
|
/**
|
|
19
14
|
* Register the Package-private Client↔Host RPC for the Web device panel
|
package/lib/rpc.js
CHANGED
|
@@ -1,47 +1,123 @@
|
|
|
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
|
-
|
|
9
|
+
async function requireDevice(ctx, cfg, argv, options) {
|
|
10
|
+
const result = await runAdb(ctx, cfg, argv, { signal: options.signal, serial: options.serial, maxBytes: options.maxBytes });
|
|
11
|
+
if (result.exitCode !== 0)
|
|
12
|
+
throw classifyFailure(result);
|
|
13
|
+
return result.stdout;
|
|
14
|
+
}
|
|
15
|
+
function serialOf(payload) {
|
|
16
|
+
return typeof payload.serial === 'string' ? payload.serial : undefined;
|
|
17
|
+
}
|
|
10
18
|
export async function handleRpcEndpoint(ctx, cfg, endpoint, raw, signal) {
|
|
11
19
|
try {
|
|
12
20
|
const payload = (raw ?? {});
|
|
13
21
|
switch (endpoint) {
|
|
14
22
|
case 'listDevices': {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
throw classifyFailure(result);
|
|
18
|
-
return { ok: true, value: { server: 'ok', devices: parseDevices(result.stdout) } };
|
|
23
|
+
const stdout = await requireDevice(ctx, cfg, ['devices', '-l'], { signal });
|
|
24
|
+
return { ok: true, value: { server: 'ok', devices: parseDevices(stdout) } };
|
|
19
25
|
}
|
|
20
|
-
case '
|
|
26
|
+
case 'listPackages': {
|
|
27
|
+
const stdout = await requireDevice(ctx, cfg, ['shell', 'pm', 'list', 'packages'], {
|
|
28
|
+
signal,
|
|
29
|
+
serial: serialOf(payload),
|
|
30
|
+
maxBytes: 4 * 1024 * 1024,
|
|
31
|
+
});
|
|
32
|
+
return { ok: true, value: { packages: parsePackageList(stdout) } };
|
|
33
|
+
}
|
|
34
|
+
case 'deviceInfo': {
|
|
35
|
+
const serial = serialOf(payload);
|
|
36
|
+
const getprop = parseGetprop(await requireDevice(ctx, cfg, ['shell', 'getprop'], { signal, serial, maxBytes: 2 * 1024 * 1024 }));
|
|
37
|
+
const size = parseWmSize(await requireDevice(ctx, cfg, ['shell', 'wm', 'size'], { signal, serial }));
|
|
38
|
+
const memTotalKb = parseMemTotal(await requireDevice(ctx, cfg, ['shell', 'cat', '/proc/meminfo'], { signal, serial }));
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
value: {
|
|
42
|
+
model: getprop['ro.product.model'],
|
|
43
|
+
manufacturer: getprop['ro.product.manufacturer'],
|
|
44
|
+
release: getprop['ro.build.version.release'],
|
|
45
|
+
sdk: getprop['ro.build.version.sdk'],
|
|
46
|
+
resolution: size === undefined ? undefined : `${size.width}x${size.height}`,
|
|
47
|
+
memTotalKb,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
case 'processList': {
|
|
52
|
+
const stdout = await requireDevice(ctx, cfg, ['shell', 'ps', '-A'], {
|
|
53
|
+
signal,
|
|
54
|
+
serial: serialOf(payload),
|
|
55
|
+
maxBytes: 2 * 1024 * 1024,
|
|
56
|
+
});
|
|
57
|
+
let processes = parseProcessList(stdout);
|
|
21
58
|
const pkg = typeof payload.package === 'string' ? payload.package : undefined;
|
|
22
|
-
if (pkg
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return { ok: true, value: snapshot };
|
|
59
|
+
if (pkg !== undefined)
|
|
60
|
+
processes = processes.filter((entry) => entry.name.includes(pkg));
|
|
61
|
+
processes = processes.slice(0, 100);
|
|
62
|
+
return { ok: true, value: { total: processes.length, processes } };
|
|
27
63
|
}
|
|
28
|
-
case 'logcatTail':
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const output = await runAdb(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], {
|
|
64
|
+
case 'logcatTail':
|
|
65
|
+
case 'logcatDelta': {
|
|
66
|
+
const serial = serialOf(payload);
|
|
67
|
+
const stdout = await requireDevice(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], {
|
|
33
68
|
signal,
|
|
34
69
|
serial,
|
|
35
70
|
maxBytes: 8 * 1024 * 1024,
|
|
36
71
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
72
|
+
const since = typeof payload.since === 'string' ? payload.since : undefined;
|
|
73
|
+
const level = typeof payload.level === 'string' ? payload.level : undefined;
|
|
74
|
+
const keyword = typeof payload.keyword === 'string' ? payload.keyword : undefined;
|
|
75
|
+
const pid = typeof payload.pid === 'string' ? payload.pid : undefined;
|
|
76
|
+
const tail = typeof payload.tail === 'number' && payload.tail > 0 ? Math.floor(payload.tail) : 200;
|
|
77
|
+
let entries = parseLogcat(stdout);
|
|
78
|
+
// threadtime timestamps ("MM-DD HH:MM:SS.mmm") sort lexicographically.
|
|
79
|
+
if (since !== undefined)
|
|
80
|
+
entries = entries.filter((entry) => entry.time > since);
|
|
40
81
|
if (level !== undefined)
|
|
41
82
|
entries = entries.filter((entry) => matchesLevel(entry, level));
|
|
83
|
+
if (keyword !== undefined)
|
|
84
|
+
entries = entries.filter((entry) => matchesKeyword(entry, keyword));
|
|
85
|
+
if (pid !== undefined)
|
|
86
|
+
entries = entries.filter((entry) => entry.pid === pid);
|
|
42
87
|
const capped = entries.slice(-tail);
|
|
43
88
|
return { ok: true, value: { total: entries.length, truncated: entries.length > tail, entries: capped } };
|
|
44
89
|
}
|
|
90
|
+
case 'perfSnapshot': {
|
|
91
|
+
const pkg = typeof payload.package === 'string' ? payload.package : undefined;
|
|
92
|
+
if (pkg === undefined)
|
|
93
|
+
throw new Error('perfSnapshot requires a string "package"');
|
|
94
|
+
const snapshot = await capturePerfSnapshot(ctx, cfg, signal, { package: pkg, serial: serialOf(payload) });
|
|
95
|
+
return { ok: true, value: snapshot };
|
|
96
|
+
}
|
|
97
|
+
case 'screenshot': {
|
|
98
|
+
const serial = serialOf(payload);
|
|
99
|
+
const devicePath = `/data/local/tmp/dsh-shot-${Date.now()}.png`;
|
|
100
|
+
const localPath = join(tmpdir(), `dsh-shot-${Date.now()}.png`);
|
|
101
|
+
try {
|
|
102
|
+
await requireDevice(ctx, cfg, ['shell', 'screencap', '-p', devicePath], { signal, serial });
|
|
103
|
+
await requireDevice(ctx, cfg, ['pull', devicePath, localPath], { signal, serial });
|
|
104
|
+
const bytes = readFileSync(localPath);
|
|
105
|
+
return { ok: true, value: { mime: 'image/png', bytes: bytes.length, dataUrl: `data:image/png;base64,${bytes.toString('base64')}` } };
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
try {
|
|
109
|
+
unlinkSync(localPath);
|
|
110
|
+
}
|
|
111
|
+
catch { /* temp cleanup is best-effort */ }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
case 'perfSample': {
|
|
115
|
+
const pkg = typeof payload.package === 'string' ? payload.package : undefined;
|
|
116
|
+
if (pkg === undefined)
|
|
117
|
+
throw new Error('perfSample requires a string "package"');
|
|
118
|
+
const snapshot = await capturePerfSnapshot(ctx, cfg, signal, { package: pkg, serial: serialOf(payload), metrics: ['meminfo', 'battery'] });
|
|
119
|
+
return { ok: true, value: { package: pkg, meminfo: snapshot.meminfo, battery: snapshot.battery } };
|
|
120
|
+
}
|
|
45
121
|
default:
|
|
46
122
|
return { ok: false, error: { message: `unknown endpoint: ${endpoint}` } };
|
|
47
123
|
}
|
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
|
+
}
|
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.1.0",
|
|
4
|
+
"description": "ADB device & bench operations for DeepSeek Harness: device discovery, structured logcat, apk install, file pull/push, performance snapshots, perf baselines, crash reports, web device panel (autocomplete, live logcat, profiler)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|