dsh-adb 0.2.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 CHANGED
@@ -14,9 +14,13 @@ 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.1.0)
18
+
19
+ A "设备" tab in the conversation view ring (next to chat / trajectory / automation): device list with status, package autocomplete (fuzzy search), live streaming logcat window (level/keyword/package/pid filters, pause/clear/auto-scroll), device info card, process list, and performance snapshot — plus harness synergy: **send any logcat/snapshot to the conversation** (the agent analyzes it), a live strip of the agent's adb operations, and a registered **crash-analysis skill** (`dsh-adb-crash-analysis`) for automation pipelines. Data flows over the package RPC channel; install into a web profile and restart the GUI (see `scripts/restart-web.ps1` for a one-click restart).
20
+
17
21
  ## Ecosystem
18
22
 
19
- - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` published (latest: 0.1.5)
23
+ - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` published (latest: 1.0.0)
20
24
  - ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **merged**
21
25
  - ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **merged**
22
26
  - ✅ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — **merged**
package/README.zh-CN.md CHANGED
@@ -14,9 +14,13 @@ dsh plugin --profile web add dsh-adb
14
14
 
15
15
  或从 GitHub 直装:`dsh plugin --profile web add github:SamXiaBing/dsh-adb`
16
16
 
17
+ ## Web 设备面板(v1.0.0)
18
+
19
+ 会话视图页签「设备」(与 chat/轨迹/任务管理并列):设备列表/状态、按包名的性能快照(内存/帧率/电量)、过滤 logcat 尾部。数据走 Package RPC;需装入 web profile 并重启 GUI 生效。
20
+
17
21
  ## 生态收录
18
22
 
19
- - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 0.1.5
23
+ - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 1.0.0
20
24
  - ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **已合并**
21
25
  - ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **已合并**
22
26
  - ✅ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — **已合并**
package/client.js ADDED
@@ -0,0 +1,468 @@
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.
8
+ */
9
+ 'use strict'
10
+
11
+ // ---- Pure helpers (shared by the panel and the unit tests) ----
12
+
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
+ }
140
+ throw new Error('dsh-adb host returned an invalid response.')
141
+ }
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
+ }
153
+ }
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)',
164
+ }
165
+ const DROPDOWN_ITEM = { padding: '4px 8px', cursor: 'pointer', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
166
+
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
+ )
175
+ }
176
+
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
+ )
204
+ }
205
+
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')),
329
+ ),
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 : ''}`))),
346
+ ),
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
+ ),
398
+ ),
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
@@ -7,6 +7,8 @@ import { registerInstallTool } from './tools/install.js';
7
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
+ import { registerRpc } from './rpc.js';
11
+ import { registerSkills } from './skill.js';
10
12
  export const name = 'dsh-adb';
11
13
  /** The tool registry is a hard dependency: every tool registers through it. */
12
14
  export const inject = ['tools'];
@@ -25,5 +27,10 @@ export function apply(ctx, config) {
25
27
  registerPerfTool(ctx, cfg);
26
28
  registerPerfBaselineTool(ctx, cfg, config.baselineDir ?? DEFAULT_BASELINE_DIR);
27
29
  registerCrashReportTool(ctx, cfg);
28
- ctx.logger.info('[dsh-adb] loaded: adb_devices / adb_connect / adb_disconnect / adb_logcat / adb_install / adb_file / adb_perf_snapshot / adb_perf_baseline / adb_crash_report');
30
+ registerSkills(ctx);
31
+ // The RPC channel needs the client connection, which mounts after this
32
+ // plugin starts in web compositions; register lazily so headless profiles
33
+ // (no connection) stay unaffected.
34
+ ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg));
35
+ ctx.logger.info('[dsh-adb] loaded: 9 tools + web device panel rpc');
29
36
  }
@@ -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 ADDED
@@ -0,0 +1,18 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { AdbConfig } from './adb.js';
3
+ export type RpcEndpointResult = {
4
+ ok: true;
5
+ value: unknown;
6
+ } | {
7
+ ok: false;
8
+ error: {
9
+ message: string;
10
+ };
11
+ };
12
+ export declare function handleRpcEndpoint(ctx: Context, cfg: AdbConfig, endpoint: string, raw: unknown, signal: AbortSignal): Promise<RpcEndpointResult>;
13
+ /**
14
+ * Register the Package-private Client↔Host RPC for the Web device panel
15
+ * (conversation.view tab "设备"). Called lazily once the client connection
16
+ * mounts; headless compositions (no connection) stay unaffected.
17
+ */
18
+ export declare function registerRpc(ctx: Context, cfg: AdbConfig): void;
package/lib/rpc.js ADDED
@@ -0,0 +1,142 @@
1
+ import { readFileSync, unlinkSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { classifyFailure, runAdb } from './adb.js';
5
+ import { parseDevices } from './parsers/devices.js';
6
+ import { matchesKeyword, matchesLevel, parseLogcat } from './parsers/logcat.js';
7
+ import { parseGetprop, parseMemTotal, parsePackageList, parseProcessList, parseWmSize, } from './parsers/sysinfo.js';
8
+ import { capturePerfSnapshot } from './tools/perf.js';
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
+ }
18
+ export async function handleRpcEndpoint(ctx, cfg, endpoint, raw, signal) {
19
+ try {
20
+ const payload = (raw ?? {});
21
+ switch (endpoint) {
22
+ case 'listDevices': {
23
+ const stdout = await requireDevice(ctx, cfg, ['devices', '-l'], { signal });
24
+ return { ok: true, value: { server: 'ok', devices: parseDevices(stdout) } };
25
+ }
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);
58
+ const pkg = typeof payload.package === 'string' ? payload.package : undefined;
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 } };
63
+ }
64
+ case 'logcatTail':
65
+ case 'logcatDelta': {
66
+ const serial = serialOf(payload);
67
+ const stdout = await requireDevice(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], {
68
+ signal,
69
+ serial,
70
+ maxBytes: 8 * 1024 * 1024,
71
+ });
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);
81
+ if (level !== undefined)
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);
87
+ const capped = entries.slice(-tail);
88
+ return { ok: true, value: { total: entries.length, truncated: entries.length > tail, entries: capped } };
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
+ }
121
+ default:
122
+ return { ok: false, error: { message: `unknown endpoint: ${endpoint}` } };
123
+ }
124
+ }
125
+ catch (error) {
126
+ return { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
127
+ }
128
+ }
129
+ /**
130
+ * Register the Package-private Client↔Host RPC for the Web device panel
131
+ * (conversation.view tab "设备"). Called lazily once the client connection
132
+ * mounts; headless compositions (no connection) stay unaffected.
133
+ */
134
+ export function registerRpc(ctx, cfg) {
135
+ const connection = ctx.get('connection');
136
+ const rpc = connection?.rpc;
137
+ if (rpc === undefined)
138
+ return;
139
+ ctx.effect(() => rpc.handle('/dsh-adb', (endpoint, raw, signal) => handleRpcEndpoint(ctx, cfg, endpoint, raw, signal),
140
+ // Browser-only channel: accept requests from the loopback web GUI.
141
+ { authority: 'loopback' }));
142
+ }
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
+ }
@@ -96,7 +96,7 @@ export function registerCrashReportTool(ctx, cfg) {
96
96
  if (args.package === undefined) {
97
97
  throw new AdbError('ARGS_INVALID', 'meminfo section requires a package');
98
98
  }
99
- const snapshot = await capturePerfSnapshot(ctx, cfg, exec, {
99
+ const snapshot = await capturePerfSnapshot(ctx, cfg, exec.signal, {
100
100
  package: args.package,
101
101
  serial: args.serial,
102
102
  metrics: ['meminfo'],
@@ -62,7 +62,7 @@ export function registerPerfBaselineTool(ctx, cfg, baselineDir) {
62
62
  throw new AdbError('ARGS_INVALID', `${args.command} requires a package`);
63
63
  }
64
64
  if (args.command === 'save') {
65
- const snapshot = await capturePerfSnapshot(ctx, cfg, exec, {
65
+ const snapshot = await capturePerfSnapshot(ctx, cfg, exec.signal, {
66
66
  package: args.package,
67
67
  serial: args.serial,
68
68
  metrics: args.metrics,
@@ -83,7 +83,7 @@ export function registerPerfBaselineTool(ctx, cfg, baselineDir) {
83
83
  if (baseline === undefined) {
84
84
  throw new AdbError('BASELINE_NOT_FOUND', `no baseline for device ${device} package ${args.package}${args.id !== undefined ? ` (id ${args.id})` : ''}; save one with command=save first`);
85
85
  }
86
- const current = await capturePerfSnapshot(ctx, cfg, exec, {
86
+ const current = await capturePerfSnapshot(ctx, cfg, exec.signal, {
87
87
  package: args.package,
88
88
  serial: args.serial,
89
89
  metrics: args.metrics,
@@ -1,5 +1,4 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- import type { ToolExecution } from '@deepseek-ai/dsh-tools';
3
2
  import { type AdbConfig } from '../adb.js';
4
3
  import { parseBattery, parseGfxinfo, parseMeminfo } from '../parsers/perf.js';
5
4
  export type PerfMetric = 'meminfo' | 'gfxinfo' | 'battery';
@@ -11,7 +10,7 @@ export interface PerfSnapshot {
11
10
  battery?: ReturnType<typeof parseBattery>;
12
11
  }
13
12
  /** Shared capture: dumpsys meminfo / gfxinfo / battery for one app. */
14
- export declare function capturePerfSnapshot(ctx: Context, cfg: AdbConfig, exec: ToolExecution, args: {
13
+ export declare function capturePerfSnapshot(ctx: Context, cfg: AdbConfig, signal: AbortSignal, args: {
15
14
  package: string;
16
15
  serial?: string;
17
16
  metrics?: PerfMetric[];
package/lib/tools/perf.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { classifyFailure, jsonOutput, runAdb } from '../adb.js';
2
2
  import { parseBattery, parseGfxinfo, parseMeminfo } from '../parsers/perf.js';
3
3
  /** Shared capture: dumpsys meminfo / gfxinfo / battery for one app. */
4
- export async function capturePerfSnapshot(ctx, cfg, exec, args) {
4
+ export async function capturePerfSnapshot(ctx, cfg, signal, args) {
5
5
  const metrics = args.metrics ?? ['meminfo', 'gfxinfo', 'battery'];
6
6
  const result = { package: args.package, metrics: [...metrics] };
7
7
  for (const metric of metrics) {
@@ -10,7 +10,7 @@ export async function capturePerfSnapshot(ctx, cfg, exec, args) {
10
10
  ? ['shell', 'dumpsys', 'battery']
11
11
  : ['shell', 'dumpsys', metric, args.package];
12
12
  const output = await runAdb(ctx, cfg, argv, {
13
- signal: exec.signal,
13
+ signal,
14
14
  serial: args.serial,
15
15
  maxBytes: 4 * 1024 * 1024,
16
16
  });
@@ -46,7 +46,7 @@ export function registerPerfTool(ctx, cfg) {
46
46
  },
47
47
  output: jsonOutput(),
48
48
  async execute(args, exec) {
49
- return capturePerfSnapshot(ctx, cfg, exec, args);
49
+ return capturePerfSnapshot(ctx, cfg, exec.signal, args);
50
50
  },
51
51
  });
52
52
  }
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "dsh-adb",
3
- "version": "0.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",
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
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
7
12
  "files": [
8
13
  "lib",
14
+ "client.js",
9
15
  "cordis.patch.yml"
10
16
  ],
11
17
  "keywords": [
@@ -21,6 +27,9 @@
21
27
  "dsh": {
22
28
  "bundle": {
23
29
  "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "platform": "web"
24
33
  }
25
34
  },
26
35
  "scripts": {