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/client.js
CHANGED
|
@@ -1,156 +1,578 @@
|
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
/** Format a one-click health report into a send-to-conversation text block. */
|
|
59
|
+
function formatReportBlock(report) {
|
|
60
|
+
if (!report || typeof report !== 'object') return '(无体检报告数据)'
|
|
61
|
+
const lines = []
|
|
62
|
+
const health = report.health
|
|
63
|
+
const d = report.device
|
|
64
|
+
if (d) {
|
|
65
|
+
const parts = [d.model, d.manufacturer, `Android ${d.release ?? '?'}`].filter(Boolean)
|
|
66
|
+
if (d.sdk) parts.push(`API ${d.sdk}`)
|
|
67
|
+
if (d.resolution) parts.push(d.resolution)
|
|
68
|
+
if (d.memTotalKb) parts.push(`内存 ${Math.round(d.memTotalKb / 1024)}MB`)
|
|
69
|
+
lines.push(`设备:${parts.join(' · ') || report.serial}`)
|
|
70
|
+
} else {
|
|
71
|
+
lines.push(`设备:${report.serial || '未知'}`)
|
|
72
|
+
}
|
|
73
|
+
if (health) {
|
|
74
|
+
lines.push(`体检结论:${health.verdict === 'attention' ? '需关注' : '正常'}`)
|
|
75
|
+
for (const line of health.lines) lines.push(`- ${line}`)
|
|
76
|
+
if (health.issues && health.issues.length > 0) {
|
|
77
|
+
lines.push('关注项:')
|
|
78
|
+
for (const issue of health.issues) lines.push(`- ⚠ ${issue}`)
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const cb = report.crashBuffer
|
|
82
|
+
const lg = report.logcat
|
|
83
|
+
lines.push(`崩溃缓冲:${cb ? cb.total : 0} 条`)
|
|
84
|
+
lines.push(`W/E/F 日志:${lg ? lg.total : 0} 条`)
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(report.errors) && report.errors.length > 0) {
|
|
87
|
+
lines.push(`采集失败:${report.errors.map((e) => `${e.section}: ${e.message}`).join('; ')}`)
|
|
88
|
+
}
|
|
89
|
+
lines.push(`采集时间:${report.collectedAt ?? '未知'}`)
|
|
90
|
+
// Evidence: only real crash chains, not the whole (mostly boot-marker) buffer.
|
|
91
|
+
if (report.crashBuffer && report.crashBuffer.chains && report.crashBuffer.chains.length > 0) {
|
|
92
|
+
lines.push('', '真实崩溃堆栈:')
|
|
93
|
+
for (const chain of report.crashBuffer.chains) {
|
|
94
|
+
lines.push('```log',
|
|
95
|
+
`${chain.signature.time} ${chain.signature.pid} ${chain.signature.tid} ${chain.signature.level} ${chain.signature.tag}: ${chain.signature.message}`,
|
|
96
|
+
...(chain.following || []).map((e) => `${e.time} ${e.pid} ${e.tid} ${e.level} ${e.tag}: ${e.message}`),
|
|
97
|
+
'```')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Evidence: top logcat noise sources with one sample each.
|
|
101
|
+
if (report.logcat && report.logcat.byTag && report.logcat.byTag.length > 0) {
|
|
102
|
+
lines.push('', 'W/E/F 主要来源(样本):')
|
|
103
|
+
for (const agg of report.logcat.byTag.slice(0, 5)) {
|
|
104
|
+
const s = agg.sample
|
|
105
|
+
lines.push(`- ${agg.tag}(${agg.level}) ×${agg.count}:${s.time} ${s.pid} ${s.tid} ${s.tag}: ${s.message}`)
|
|
47
106
|
}
|
|
107
|
+
}
|
|
108
|
+
return ['以下是从设备面板生成的一键体检报告,请结合 dsh-adb-crash-analysis 技能分析设备健康状态:', ...lines].join('\n')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Panel dictionary: zh is the key-set source of truth, en mirrors it. */
|
|
112
|
+
const DICTIONARY = {
|
|
113
|
+
zh: {
|
|
114
|
+
'panel.title': 'ADB 设备',
|
|
115
|
+
'refresh': '刷新',
|
|
116
|
+
'noDevices': '未连接设备',
|
|
117
|
+
'deviceInfo': '设备信息',
|
|
118
|
+
'package': '包名',
|
|
119
|
+
'packagePlaceholder': '输入或选择包名',
|
|
120
|
+
'snapshot': '性能快照',
|
|
121
|
+
'sendToChat': '发送到对话',
|
|
122
|
+
'processes': '进程({n})— 点击按 pid 过滤 logcat',
|
|
123
|
+
'logcat': 'logcat',
|
|
124
|
+
'keywordFilter': '关键字过滤',
|
|
125
|
+
'packageFilter': '包名过滤(按进程)',
|
|
126
|
+
'resume': '继续',
|
|
127
|
+
'pause': '暂停',
|
|
128
|
+
'clear': '清空',
|
|
129
|
+
'autoScroll': '自动滚动',
|
|
130
|
+
'shownCount': '已显示 {n} 条',
|
|
131
|
+
'refreshing': '每 1.5s 增量刷新',
|
|
132
|
+
'paused': '已暂停',
|
|
133
|
+
'waitingLog': '(等待日志…)',
|
|
134
|
+
'agentActivity': 'agent 的 adb 操作',
|
|
135
|
+
'noData': '(无数据)',
|
|
136
|
+
'model': '型号', 'manufacturer': '厂商', 'android': 'Android', 'api': 'API',
|
|
137
|
+
'resolution': '分辨率', 'memTotal': '内存总量',
|
|
138
|
+
'memPss': '内存 PSS (KB)', 'memRss': '内存 RSS (KB)', 'javaHeap': 'Java Heap (KB)', 'nativeHeap': 'Native Heap (KB)',
|
|
139
|
+
'frames': '总帧数', 'janky': '卡顿帧 / %', 'p50p90': 'P50/P90 (ms)', 'p95p99': 'P95/P99 (ms)',
|
|
140
|
+
'battery': '电量', 'temp': '温度 (°C)', 'pkg': '包=', 'pid': 'pid=',
|
|
141
|
+
'report': '一键体检', 'reportRunning': '采集中…', 'reportDevice': '设备',
|
|
142
|
+
'reportCrash': '崩溃缓冲', 'reportLogcat': 'W/E/F 日志', 'reportProcesses': 'Top 进程',
|
|
143
|
+
'reportErrors': '采集失败', 'reportTime': '采集时间', 'reportSaved': '已保存',
|
|
144
|
+
'reportVerdict': '体检结论', 'reportVerdictOk': '正常', 'reportVerdictAttention': '需关注',
|
|
145
|
+
'reportSignals': '关注项',
|
|
146
|
+
},
|
|
147
|
+
en: {
|
|
148
|
+
'panel.title': 'ADB Devices',
|
|
149
|
+
'refresh': 'Refresh',
|
|
150
|
+
'noDevices': 'No device connected',
|
|
151
|
+
'deviceInfo': 'Device Info',
|
|
152
|
+
'package': 'Package',
|
|
153
|
+
'packagePlaceholder': 'Type or pick a package',
|
|
154
|
+
'snapshot': 'Perf Snapshot',
|
|
155
|
+
'sendToChat': 'Send to chat',
|
|
156
|
+
'processes': 'Processes ({n}) — click to filter logcat by pid',
|
|
157
|
+
'logcat': 'logcat',
|
|
158
|
+
'keywordFilter': 'Keyword filter',
|
|
159
|
+
'packageFilter': 'Package filter (by process)',
|
|
160
|
+
'resume': 'Resume',
|
|
161
|
+
'pause': 'Pause',
|
|
162
|
+
'clear': 'Clear',
|
|
163
|
+
'autoScroll': 'Auto-scroll',
|
|
164
|
+
'shownCount': '{n} entries shown',
|
|
165
|
+
'refreshing': 'incremental 1.5s refresh',
|
|
166
|
+
'paused': 'paused',
|
|
167
|
+
'waitingLog': '(waiting for logs…)',
|
|
168
|
+
'agentActivity': "Agent's adb activity",
|
|
169
|
+
'noData': '(no data)',
|
|
170
|
+
'model': 'Model', 'manufacturer': 'Manufacturer', 'android': 'Android', 'api': 'API',
|
|
171
|
+
'resolution': 'Resolution', 'memTotal': 'Total memory',
|
|
172
|
+
'memPss': 'PSS (KB)', 'memRss': 'RSS (KB)', 'javaHeap': 'Java Heap (KB)', 'nativeHeap': 'Native Heap (KB)',
|
|
173
|
+
'frames': 'Total frames', 'janky': 'Janky / %', 'p50p90': 'P50/P90 (ms)', 'p95p99': 'P95/P99 (ms)',
|
|
174
|
+
'battery': 'Battery', 'temp': 'Temp (°C)', 'pkg': 'pkg=', 'pid': 'pid=',
|
|
175
|
+
'report': 'Health report', 'reportRunning': 'collecting…', 'reportDevice': 'Device',
|
|
176
|
+
'reportCrash': 'Crash buffer', 'reportLogcat': 'W/E/F logs', 'reportProcesses': 'Top processes',
|
|
177
|
+
'reportErrors': 'Collection errors', 'reportTime': 'Collected at', 'reportSaved': 'Saved',
|
|
178
|
+
'reportVerdict': 'Health', 'reportVerdictOk': 'OK', 'reportVerdictAttention': 'needs attention',
|
|
179
|
+
'reportSignals': 'Concerns',
|
|
180
|
+
},
|
|
181
|
+
}
|
|
48
182
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.
|
|
183
|
+
// ---- Browser entry ----
|
|
184
|
+
|
|
185
|
+
if (typeof window !== 'undefined' && typeof window.__ModuleLoader__ === 'object') {
|
|
186
|
+
window.__ModuleLoader__.load({
|
|
187
|
+
id: 'dsh-adb',
|
|
188
|
+
factory: (require) => {
|
|
189
|
+
const module = { exports: {} }
|
|
190
|
+
|
|
191
|
+
const React = require('react')
|
|
192
|
+
const { defineStore } = require('@deepseek-ai/dsh-client-runtime/client')
|
|
193
|
+
const CHANNEL = '/dsh-adb'
|
|
194
|
+
|
|
195
|
+
function unwrap(value) {
|
|
196
|
+
if (typeof value !== 'object' || value === null || !('ok' in value)) {
|
|
197
|
+
throw new Error('dsh-adb host returned an invalid response.')
|
|
198
|
+
}
|
|
199
|
+
if (value.ok === true && 'value' in value) return value.value
|
|
200
|
+
if (value.ok === false && value.error) {
|
|
201
|
+
throw new Error(value.error.message ?? 'dsh-adb request failed.')
|
|
202
|
+
}
|
|
203
|
+
throw new Error('dsh-adb host returned an invalid response.')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function createRuntime(rpc) {
|
|
207
|
+
const call = (endpoint, payload) => rpc.call(CHANNEL, endpoint, payload).then(unwrap)
|
|
208
|
+
return {
|
|
209
|
+
listDevices: () => call('listDevices', {}),
|
|
210
|
+
listPackages: (payload) => call('listPackages', payload),
|
|
211
|
+
deviceInfo: (payload) => call('deviceInfo', payload),
|
|
212
|
+
processList: (payload) => call('processList', payload),
|
|
213
|
+
logcatDelta: (payload) => call('logcatDelta', payload),
|
|
214
|
+
perfSnapshot: (payload) => call('perfSnapshot', payload),
|
|
215
|
+
deviceReport: (payload) => call('deviceReport', payload),
|
|
216
|
+
}
|
|
68
217
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
218
|
+
|
|
219
|
+
const h = React.createElement
|
|
220
|
+
const ROW = { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', flexWrap: 'wrap' }
|
|
221
|
+
const BTN = { padding: '3px 10px', cursor: 'pointer' }
|
|
222
|
+
const INPUT = { padding: '3px 6px' }
|
|
223
|
+
const SECTION = { marginTop: 14, borderTop: '1px solid var(--dsh-border, #ddd)', paddingTop: 10 }
|
|
224
|
+
const DROPDOWN = {
|
|
225
|
+
position: 'absolute', top: '100%', left: 0, right: 0, maxHeight: 180, overflowY: 'auto',
|
|
226
|
+
background: 'var(--dsh-bg, #ffffff)', color: 'inherit',
|
|
227
|
+
border: '1px solid var(--dsh-border, #ccc)', borderRadius: 4, zIndex: 20, boxShadow: '0 2px 8px rgba(0,0,0,.2)',
|
|
78
228
|
}
|
|
229
|
+
const DROPDOWN_ITEM = { padding: '4px 8px', cursor: 'pointer', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
|
|
79
230
|
|
|
80
|
-
|
|
81
|
-
if (!
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
231
|
+
function MetricRows({ rows }) {
|
|
232
|
+
if (!rows || rows.length === 0) return h('div', null, '(无数据)')
|
|
233
|
+
return h('table', { style: { borderCollapse: 'collapse' } },
|
|
234
|
+
rows.map((row) => h('tr', { key: row[0] },
|
|
235
|
+
h('td', { style: { padding: '2px 12px 2px 0', color: 'var(--dsh-text-secondary, #888)' } }, row[0]),
|
|
236
|
+
h('td', { style: { padding: '2px 0' } }, String(row[1])),
|
|
237
|
+
)),
|
|
238
|
+
)
|
|
88
239
|
}
|
|
89
240
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
241
|
+
function PackageCombobox({ packages, value, placeholder, onChange }) {
|
|
242
|
+
const [query, setQuery] = React.useState('')
|
|
243
|
+
const [open, setOpen] = React.useState(false)
|
|
244
|
+
const shown = open
|
|
245
|
+
? packages.filter((name) => query === '' || name.toLowerCase().includes(query.toLowerCase())).slice(0, 60)
|
|
246
|
+
: []
|
|
247
|
+
return h('div', { style: { position: 'relative', flex: 1, minWidth: 220 } },
|
|
248
|
+
h('input', {
|
|
249
|
+
style: { ...INPUT, width: '100%', boxSizing: 'border-box' },
|
|
250
|
+
value: query,
|
|
251
|
+
placeholder: value || placeholder || '',
|
|
252
|
+
onChange: (e) => { setQuery(e.target.value); setOpen(true) },
|
|
253
|
+
onFocus: () => setOpen(true),
|
|
254
|
+
onBlur: () => setTimeout(() => { setOpen(false); setQuery('') }, 150),
|
|
255
|
+
onKeyDown: (e) => {
|
|
256
|
+
if (e.key === 'Enter' && shown.length > 0) { onChange(shown[0]); setQuery(''); setOpen(false) }
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
open && shown.length > 0 && h('div', { style: DROPDOWN },
|
|
260
|
+
shown.map((name) => h('div', {
|
|
261
|
+
key: name,
|
|
262
|
+
onMouseDown: () => { onChange(name); setQuery(''); setOpen(false) },
|
|
263
|
+
onMouseEnter: (e) => { e.currentTarget.style.background = 'var(--dsh-accent-soft, rgba(66,133,244,.12))' },
|
|
264
|
+
onMouseLeave: (e) => { e.currentTarget.style.background = 'transparent' },
|
|
265
|
+
style: DROPDOWN_ITEM,
|
|
266
|
+
}, name))),
|
|
267
|
+
)
|
|
96
268
|
}
|
|
97
269
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
270
|
+
function DeviceView(props) {
|
|
271
|
+
const runtime = props.runtime
|
|
272
|
+
const t = props.t || ((key) => DICTIONARY.zh[key] ?? key)
|
|
273
|
+
const st = props.useStore((s) => s)
|
|
274
|
+
const actions = props.actions
|
|
275
|
+
const [busy, setBusy] = React.useState(false)
|
|
276
|
+
const logRef = React.useRef(null)
|
|
277
|
+
const stateRef = React.useRef(st)
|
|
278
|
+
stateRef.current = st
|
|
279
|
+
|
|
280
|
+
const adbActivity = props.useSession
|
|
281
|
+
? props.useSession((snap) => extractAdbActivity(nodeArrayOf(snap)))
|
|
282
|
+
: []
|
|
283
|
+
const sendToChat = (text) => {
|
|
284
|
+
if (props.inputActions && typeof props.inputActions.setDraft === 'function') {
|
|
285
|
+
props.inputActions.setDraft(text)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const fail = (e) => actions.setError(String((e && e.message) || e))
|
|
290
|
+
|
|
291
|
+
const refresh = () => {
|
|
292
|
+
setBusy(true); actions.setError(null)
|
|
293
|
+
runtime.listDevices()
|
|
294
|
+
.then((value) => actions.setDevices(value.devices ?? []))
|
|
295
|
+
.catch(fail)
|
|
296
|
+
.finally(() => setBusy(false))
|
|
297
|
+
}
|
|
298
|
+
React.useEffect(refresh, [])
|
|
299
|
+
|
|
300
|
+
const selectDevice = (device) => {
|
|
301
|
+
actions.setSelected(device)
|
|
302
|
+
actions.setInfo(null); actions.setSnapshot(null); actions.setReport(null); actions.setProcesses([])
|
|
303
|
+
actions.clearLog()
|
|
304
|
+
setBusy(true); actions.setError(null)
|
|
305
|
+
Promise.all([
|
|
306
|
+
runtime.deviceInfo({ serial: device.serial }).then(actions.setInfo),
|
|
307
|
+
runtime.listPackages({ serial: device.serial }).then((v) => actions.setPackages(v.packages ?? [])),
|
|
308
|
+
]).catch(fail).finally(() => setBusy(false))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const loadProcesses = (pkgName) => {
|
|
312
|
+
const device = stateRef.current.selected
|
|
313
|
+
if (!device) return
|
|
314
|
+
runtime.processList({ serial: device.serial, package: pkgName })
|
|
315
|
+
.then((v) => actions.setProcesses(v.processes ?? []))
|
|
316
|
+
.catch(fail)
|
|
317
|
+
}
|
|
318
|
+
React.useEffect(() => { if (st.selected) loadProcesses(st.pkg) }, [st.pkg]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
319
|
+
|
|
320
|
+
const runSnapshot = () => {
|
|
321
|
+
const device = stateRef.current.selected
|
|
322
|
+
if (!device) return
|
|
323
|
+
setBusy(true); actions.setError(null)
|
|
324
|
+
runtime.perfSnapshot({ serial: device.serial, package: st.pkg })
|
|
325
|
+
.then(actions.setSnapshot)
|
|
326
|
+
.catch(fail)
|
|
327
|
+
.finally(() => setBusy(false))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const runReport = () => {
|
|
331
|
+
const device = stateRef.current.selected
|
|
332
|
+
if (!device) return
|
|
333
|
+
setBusy(true); actions.setError(null)
|
|
334
|
+
runtime.deviceReport({ serial: device.serial })
|
|
335
|
+
.then(actions.setReport)
|
|
336
|
+
.catch(fail)
|
|
337
|
+
.finally(() => setBusy(false))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const applyPackageFilter = (name) => {
|
|
341
|
+
actions.setLogPkg(name)
|
|
342
|
+
actions.clearLog()
|
|
343
|
+
if (name === '') { actions.setLogPids([]); return }
|
|
344
|
+
const device = stateRef.current.selected
|
|
345
|
+
if (!device) return
|
|
346
|
+
runtime.processList({ serial: device.serial, package: name })
|
|
347
|
+
.then((v) => actions.setLogPids((v.processes ?? []).map((p) => p.pid)))
|
|
348
|
+
.catch(fail)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const applyLogFilters = (level, keyword, pids) => {
|
|
352
|
+
actions.setLogLevel(level)
|
|
353
|
+
actions.setLogKeyword(keyword)
|
|
354
|
+
actions.setLogPids(pids)
|
|
355
|
+
actions.clearLog()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
React.useEffect(() => {
|
|
359
|
+
if (!st.selected || st.logPaused) return
|
|
360
|
+
const timer = setInterval(() => {
|
|
361
|
+
const s = stateRef.current
|
|
362
|
+
runtime.logcatDelta({
|
|
363
|
+
serial: s.selected.serial,
|
|
364
|
+
since: s.logSince,
|
|
365
|
+
level: s.logLevel,
|
|
366
|
+
keyword: s.logKeyword || undefined,
|
|
367
|
+
tail: 200,
|
|
368
|
+
}).then((value) => {
|
|
369
|
+
let entries = value.entries ?? []
|
|
370
|
+
if (s.logPids.length > 0) entries = entries.filter((e) => s.logPids.includes(e.pid))
|
|
371
|
+
if (entries.length > 0) {
|
|
372
|
+
actions.appendLog(entries)
|
|
373
|
+
actions.setLogSince(entries[entries.length - 1].time)
|
|
374
|
+
}
|
|
375
|
+
}).catch(() => { /* transient poll errors are ignored */ })
|
|
376
|
+
}, 1500)
|
|
377
|
+
return () => clearInterval(timer)
|
|
378
|
+
}, [st.selected, st.logPaused]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
379
|
+
|
|
380
|
+
React.useEffect(() => {
|
|
381
|
+
const el = logRef.current
|
|
382
|
+
if (el && st.logAuto) el.scrollTop = el.scrollHeight
|
|
383
|
+
}, [st.logEntries, st.logAuto])
|
|
384
|
+
|
|
385
|
+
const snapshotRows = []
|
|
386
|
+
if (st.snapshot) {
|
|
387
|
+
const m = st.snapshot.meminfo; const g = st.snapshot.gfxinfo; const b = st.snapshot.battery
|
|
388
|
+
if (m) snapshotRows.push([t('memPss'), m.totalPssKb], [t('memRss'), m.totalRssKb], [t('javaHeap'), m.javaHeapKb], [t('nativeHeap'), m.nativeHeapKb])
|
|
389
|
+
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}`])
|
|
390
|
+
if (b) snapshotRows.push([t('battery'), `${b.levelPercent}%`], [t('temp'), b.temperatureC])
|
|
391
|
+
}
|
|
392
|
+
const infoRows = st.info
|
|
393
|
+
? [[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)
|
|
394
|
+
: []
|
|
395
|
+
|
|
396
|
+
const reportRows = []
|
|
397
|
+
if (st.report) {
|
|
398
|
+
const r = st.report
|
|
399
|
+
if (r.health) {
|
|
400
|
+
reportRows.push([t('reportVerdict'), r.health.verdict === 'attention' ? t('reportVerdictAttention') : t('reportVerdictOk')])
|
|
401
|
+
if (r.health.issues && r.health.issues.length > 0) {
|
|
402
|
+
reportRows.push([t('reportSignals'), r.health.issues.length])
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (r.device) {
|
|
406
|
+
const parts = [r.device.model, r.device.release, r.device.sdk ? `API ${r.device.sdk}` : r.device.sdk].filter(Boolean)
|
|
407
|
+
reportRows.push([t('reportDevice'), parts.join(' · ')])
|
|
408
|
+
}
|
|
409
|
+
if (r.crashBuffer) {
|
|
410
|
+
const cb = r.crashBuffer
|
|
411
|
+
reportRows.push([t('reportCrash'), `${cb.realCrashCount} 真实${cb.bootMarkerCount > 0 ? ` + ${cb.bootMarkerCount} 启动标记` : ''} / ${cb.total} 条`])
|
|
412
|
+
}
|
|
413
|
+
if (r.logcat && r.logcat.byTag && r.logcat.byTag.length > 0) {
|
|
414
|
+
const top = r.logcat.byTag[0]
|
|
415
|
+
reportRows.push([t('reportLogcat'), `${r.logcat.total} 条 · ${top.tag} ×${top.count}`])
|
|
416
|
+
}
|
|
417
|
+
if (Array.isArray(r.errors) && r.errors.length > 0) {
|
|
418
|
+
reportRows.push([t('reportErrors'), r.errors.length])
|
|
419
|
+
}
|
|
420
|
+
if (r.savedTo) reportRows.push([t('reportSaved'), r.savedTo])
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const pidsLabel = st.logPids.length > 0 ? ` · ${t('pid')}${st.logPids.join(',')}` : ''
|
|
424
|
+
const statusText = `${t('shownCount').replace('{n}', String(st.logEntries.length))}${st.logPkg ? ` · ${t('pkg')}${st.logPkg}` : ''}${pidsLabel}${st.logPaused ? ` · ${t('paused')}` : ` · ${t('refreshing')}`}`
|
|
425
|
+
|
|
426
|
+
return h('div', { style: { padding: 12, fontFamily: 'inherit', fontSize: 13 } },
|
|
427
|
+
h('div', { style: { ...ROW, justifyContent: 'space-between' } },
|
|
428
|
+
h('strong', null, t('panel.title')),
|
|
429
|
+
h('button', { style: BTN, onClick: refresh, disabled: busy }, busy ? '…' : t('refresh')),
|
|
120
430
|
),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
h('
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
431
|
+
st.error !== null && h('div', { style: { color: '#e5484d', margin: '6px 0', wordBreak: 'break-all' } }, String(st.error)),
|
|
432
|
+
st.devices.length === 0
|
|
433
|
+
? h('div', { style: { color: 'var(--dsh-text-secondary, #888)', margin: '8px 0' } }, t('noDevices'))
|
|
434
|
+
: h('div', null, st.devices.map((d) =>
|
|
435
|
+
h('button', {
|
|
436
|
+
key: d.serial,
|
|
437
|
+
onClick: () => selectDevice(d),
|
|
438
|
+
style: { ...BTN, display: 'block', width: '100%', textAlign: 'left', margin: '2px 0',
|
|
439
|
+
background: st.selected && st.selected.serial === d.serial ? 'var(--dsh-accent-soft, rgba(66,133,244,.15))' : 'transparent' },
|
|
440
|
+
}, `${d.serial} · ${d.state}${d.model ? ' · ' + d.model : ''}`),
|
|
441
|
+
)),
|
|
442
|
+
|
|
443
|
+
adbActivity.length > 0 && h('div', { style: SECTION },
|
|
444
|
+
h('strong', null, t('agentActivity')),
|
|
445
|
+
h('div', { style: { marginTop: 4, fontSize: 12, color: 'var(--dsh-text-secondary, #888)' } },
|
|
446
|
+
adbActivity.map((a, i) => h('div', { key: `${a.name}-${i}` }, `• ${a.name}${a.time ? ' @ ' + a.time : ''}`))),
|
|
130
447
|
),
|
|
131
|
-
|
|
132
|
-
h('div', { style: {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
448
|
+
|
|
449
|
+
st.selected !== null && h('div', { style: { marginTop: 14 } },
|
|
450
|
+
st.info !== null && infoRows.length > 0 && h('div', { style: SECTION },
|
|
451
|
+
h('strong', null, t('deviceInfo')),
|
|
452
|
+
h(MetricRows, { rows: infoRows }),
|
|
453
|
+
),
|
|
454
|
+
|
|
455
|
+
h('div', { style: SECTION },
|
|
456
|
+
h('div', { style: ROW },
|
|
457
|
+
h('button', { style: BTN, onClick: runReport, disabled: busy }, busy ? t('reportRunning') : t('report')),
|
|
458
|
+
st.report && h('button', { style: BTN, onClick: () => sendToChat(formatReportBlock(st.report)) }, t('sendToChat')),
|
|
459
|
+
),
|
|
460
|
+
st.report && h('div', { style: { marginTop: 8 } }, h(MetricRows, { rows: reportRows })),
|
|
461
|
+
),
|
|
462
|
+
|
|
463
|
+
h('div', { style: SECTION },
|
|
464
|
+
h('div', { style: ROW },
|
|
465
|
+
h('label', null, t('package')),
|
|
466
|
+
h(PackageCombobox, { packages: st.packages, value: st.pkg, placeholder: t('packagePlaceholder'), onChange: (name) => actions.setPkg(name) }),
|
|
467
|
+
h('button', { style: BTN, onClick: runSnapshot, disabled: busy }, t('snapshot')),
|
|
468
|
+
st.snapshot && h('button', { style: BTN, onClick: () => sendToChat(formatSnapshotBlock(st.snapshot)) }, t('sendToChat')),
|
|
469
|
+
),
|
|
470
|
+
st.snapshot && h('div', { style: { marginTop: 8 } }, h(MetricRows, { rows: snapshotRows })),
|
|
471
|
+
|
|
472
|
+
st.processes.length > 0 && h('div', { style: { marginTop: 8 } },
|
|
473
|
+
h('div', { style: { color: 'var(--dsh-text-secondary, #888)' } }, t('processes').replace('{n}', String(st.processes.length))),
|
|
474
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 } },
|
|
475
|
+
st.processes.map((p) => {
|
|
476
|
+
const active = st.logPids.length === 1 && st.logPids[0] === p.pid
|
|
477
|
+
return h('button', {
|
|
478
|
+
key: p.pid,
|
|
479
|
+
onClick: () => { const next = active ? [] : [p.pid]; applyLogFilters(st.logLevel, st.logKeyword, next) },
|
|
480
|
+
style: { ...BTN, fontSize: 11, background: active ? 'var(--dsh-accent-soft, rgba(66,133,244,.15))' : 'transparent' },
|
|
481
|
+
}, `${p.pid} ${p.name}`)
|
|
482
|
+
})),
|
|
483
|
+
),
|
|
484
|
+
),
|
|
485
|
+
|
|
486
|
+
h('div', { style: SECTION },
|
|
487
|
+
h('div', { style: ROW },
|
|
488
|
+
h('label', null, t('logcat')),
|
|
489
|
+
h('select', { style: INPUT, value: st.logLevel, onChange: (e) => applyLogFilters(e.target.value, st.logKeyword, st.logPids) },
|
|
490
|
+
['V', 'D', 'I', 'W', 'E', 'F'].map((lv) => h('option', { key: lv, value: lv }, lv))),
|
|
491
|
+
h('input', { style: { ...INPUT, minWidth: 130 }, placeholder: t('keywordFilter'), value: st.logKeyword, onChange: (e) => applyLogFilters(st.logLevel, e.target.value, st.logPids) }),
|
|
492
|
+
h('div', { style: { flex: 1, minWidth: 180 } },
|
|
493
|
+
h(PackageCombobox, { packages: st.packages, value: st.logPkg, placeholder: st.logPkg || t('packageFilter'), onChange: applyPackageFilter })),
|
|
494
|
+
h('button', { style: BTN, onClick: () => actions.setLogPaused(!st.logPaused) }, st.logPaused ? t('resume') : t('pause')),
|
|
495
|
+
h('button', { style: BTN, onClick: () => actions.clearLog() }, t('clear')),
|
|
496
|
+
h('button', { style: BTN, onClick: () => sendToChat(formatLogcatBlock(st.logEntries)), disabled: st.logEntries.length === 0 }, t('sendToChat')),
|
|
497
|
+
h('label', { style: { fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 } },
|
|
498
|
+
h('input', { type: 'checkbox', checked: st.logAuto, onChange: (e) => actions.setLogAuto(e.target.checked) }), ' ', t('autoScroll')),
|
|
499
|
+
),
|
|
500
|
+
h('div', { style: { color: 'var(--dsh-text-secondary, #888)', margin: '4px 0', fontSize: 12 } }, statusText),
|
|
501
|
+
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 } },
|
|
502
|
+
st.logEntries.length === 0
|
|
503
|
+
? h('div', { style: { color: 'var(--dsh-text-secondary, #888)' } }, t('waitingLog'))
|
|
504
|
+
: st.logEntries.map((e) => h('div', { key: `${e.time}-${e.pid}-${e.tid}-${e.message}` },
|
|
505
|
+
`${e.time} ${e.pid} ${e.tid} ${e.level} ${e.tag}: ${e.message}`))),
|
|
506
|
+
),
|
|
137
507
|
),
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
}
|
|
508
|
+
)
|
|
509
|
+
}
|
|
141
510
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
slots.inject('conversation.view', () => slots.register(
|
|
148
|
-
{ name: 'conversation.view', id: 'devices', order: 30, label: '设备' },
|
|
149
|
-
(props) => h(DeviceView, { ...props, runtime }),
|
|
150
|
-
))
|
|
151
|
-
}
|
|
511
|
+
function apply(ctx) {
|
|
512
|
+
const slots = ctx.get('slots')
|
|
513
|
+
const connection = ctx.get('connection')
|
|
514
|
+
if (slots === undefined || connection === undefined || connection.rpc === undefined) return
|
|
515
|
+
const runtime = createRuntime(connection.rpc)
|
|
152
516
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
517
|
+
// i18n: register the panel dictionary under the harness locale system.
|
|
518
|
+
const locale = ctx.get('locale')
|
|
519
|
+
if (locale && typeof locale.register === 'function') {
|
|
520
|
+
ctx.effect(() => locale.register('dsh-adb', DICTIONARY))
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Store: declared at register, so state survives view tab switches.
|
|
524
|
+
const panelStore = defineStore({
|
|
525
|
+
init: () => ({
|
|
526
|
+
devices: [], selected: null, info: null, packages: [], pkg: 'com.android.systemui',
|
|
527
|
+
snapshot: null, report: null, processes: [], logEntries: [], logSince: '',
|
|
528
|
+
logLevel: 'V', logKeyword: '', logPkg: '', logPids: [], logPaused: false, logAuto: true, error: null,
|
|
529
|
+
}),
|
|
530
|
+
actions: {
|
|
531
|
+
setDevices: (d, v) => { d.devices = v },
|
|
532
|
+
setSelected: (d, v) => { d.selected = v },
|
|
533
|
+
setInfo: (d, v) => { d.info = v },
|
|
534
|
+
setPackages: (d, v) => { d.packages = v },
|
|
535
|
+
setPkg: (d, v) => { d.pkg = v },
|
|
536
|
+
setSnapshot: (d, v) => { d.snapshot = v },
|
|
537
|
+
setReport: (d, v) => { d.report = v },
|
|
538
|
+
setProcesses: (d, v) => { d.processes = v },
|
|
539
|
+
setError: (d, v) => { d.error = v },
|
|
540
|
+
appendLog: (d, entries) => {
|
|
541
|
+
const seen = new Set(d.logEntries.map((e) => `${e.time}:${e.pid}:${e.message}`))
|
|
542
|
+
const fresh = entries.filter((e) => !seen.has(`${e.time}:${e.pid}:${e.message}`))
|
|
543
|
+
d.logEntries = [...d.logEntries, ...fresh].slice(-500)
|
|
544
|
+
},
|
|
545
|
+
setLogSince: (d, v) => { d.logSince = v },
|
|
546
|
+
setLogLevel: (d, v) => { d.logLevel = v },
|
|
547
|
+
setLogKeyword: (d, v) => { d.logKeyword = v },
|
|
548
|
+
setLogPkg: (d, v) => { d.logPkg = v },
|
|
549
|
+
setLogPids: (d, v) => { d.logPids = v },
|
|
550
|
+
setLogPaused: (d, v) => { d.logPaused = v },
|
|
551
|
+
setLogAuto: (d, v) => { d.logAuto = v },
|
|
552
|
+
clearLog: (d) => { d.logEntries = []; d.logSince = '' },
|
|
553
|
+
},
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
slots.inject('conversation.view', () => slots.register(
|
|
557
|
+
{
|
|
558
|
+
name: 'conversation.view',
|
|
559
|
+
id: 'devices',
|
|
560
|
+
order: 30,
|
|
561
|
+
label: () => {
|
|
562
|
+
const loc = ctx.get('locale')
|
|
563
|
+
return loc && typeof loc.getLocale === 'function' && loc.getLocale().active === 'en' ? 'Devices' : '设备'
|
|
564
|
+
},
|
|
565
|
+
store: panelStore,
|
|
566
|
+
locale: 'dsh-adb',
|
|
567
|
+
},
|
|
568
|
+
(props) => h(DeviceView, { ...props, runtime }),
|
|
569
|
+
))
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
module.exports = { apply }
|
|
573
|
+
return module.exports
|
|
574
|
+
},
|
|
575
|
+
})
|
|
576
|
+
} else if (typeof module !== 'undefined' && module.exports) {
|
|
577
|
+
module.exports = { formatLogcatBlock, formatSnapshotBlock, formatReportBlock, extractAdbActivity, nodeArrayOf, DICTIONARY }
|
|
578
|
+
}
|