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/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 (beside chat/trajectory/automation)
3
- * and talks to the Host half over the package RPC channel /dsh-adb.
4
- * Uses only client builtins: React, ctx, styles, console.
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
- window.__ModuleLoader__.load({
7
- id: 'dsh-adb',
8
- factory: (require) => {
9
- 'use strict'
10
- const module = { exports: {} }
9
+ 'use strict'
11
10
 
12
- const React = require('react')
13
- const CHANNEL = '/dsh-adb'
11
+ // ---- Pure helpers (shared by the panel and the unit tests) ----
14
12
 
15
- function unwrap(value) {
16
- if (typeof value !== 'object' || value === null || !('ok' in value)) {
17
- throw new Error('dsh-adb host returned an invalid response.')
18
- }
19
- if (value.ok === true && 'value' in value) return value.value
20
- if (value.ok === false && value.error) {
21
- throw new Error(value.error.message ?? 'dsh-adb request failed.')
22
- }
23
- throw new Error('dsh-adb host returned an invalid response.')
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
- function createRuntime(rpc) {
27
- return {
28
- listDevices: () => rpc.call(CHANNEL, 'listDevices', {}).then(unwrap),
29
- perfSnapshot: (payload) => rpc.call(CHANNEL, 'perfSnapshot', payload).then(unwrap),
30
- logcatTail: (payload) => rpc.call(CHANNEL, 'logcatTail', payload).then(unwrap),
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
- 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
- )
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
- 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))
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
- React.useEffect(refresh, [])
70
-
71
- const runSnapshot = () => {
72
- if (!selected) return
73
- setBusy(true); setError(null)
74
- runtime.perfSnapshot({ serial: selected.serial, package: pkg })
75
- .then(setSnapshot)
76
- .catch((e) => setError(String(e.message ?? e)))
77
- .finally(() => setBusy(false))
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
- const runLogcat = () => {
81
- if (!selected) return
82
- setBusy(true); setError(null)
83
- const n = parseInt(tail, 10)
84
- runtime.logcatTail({ serial: selected.serial, level, tail: Number.isFinite(n) && n > 0 ? n : 30 })
85
- .then((value) => { setLogEntries(value.entries ?? []); setLogTotal(value.total ?? 0) })
86
- .catch((e) => setError(String(e.message ?? e)))
87
- .finally(() => setBusy(false))
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
- const snapshotRows = []
91
- if (snapshot) {
92
- const m = snapshot.meminfo; const g = snapshot.gfxinfo; const b = snapshot.battery
93
- if (m) snapshotRows.push(['内存 PSS (KB)', m.totalPssKb], ['内存 RSS (KB)', m.totalRssKb], ['Java Heap (KB)', m.javaHeapKb], ['Native Heap (KB)', m.nativeHeapKb])
94
- if (g) snapshotRows.push(['总帧数', g.totalFrames], ['卡顿帧 / %', `${g.jankyFrames} / ${g.jankyPercent}%`], ['P50/P90 (ms)', `${g.percentile50Ms} / ${g.percentile90Ms}`], ['P95/P99 (ms)', `${g.percentile95Ms} / ${g.percentile99Ms}`], ['Missed Vsync', g.missedVsync])
95
- if (b) snapshotRows.push(['电量', `${b.levelPercent}%`], ['温度 (°C)', b.temperatureC])
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
- return h('div', { style: { padding: 12, fontFamily: 'inherit', fontSize: 13 } },
99
- h('div', { style: { ...ROW, justifyContent: 'space-between' } },
100
- h('strong', null, 'ADB 设备'),
101
- h('button', { style: BTN, onClick: refresh, disabled: busy }, busy ? '…' : '刷新'),
102
- ),
103
- error !== null && h('div', { style: { color: '#e5484d', margin: '6px 0' } }, String(error)),
104
- devices.length === 0
105
- ? h('div', { style: { color: 'var(--dsh-text-secondary, #888)', margin: '8px 0' } }, '未连接设备')
106
- : h('div', null, devices.map((d) =>
107
- h('button', {
108
- key: d.serial,
109
- onClick: () => setSelected(d),
110
- style: { ...BTN, display: 'block', width: '100%', textAlign: 'left', margin: '2px 0',
111
- background: selected && selected.serial === d.serial ? 'var(--dsh-accent-soft, rgba(66,133,244,.15))' : 'transparent' },
112
- }, `${d.serial} · ${d.state}${d.model ? ' · ' + d.model : ''}`),
113
- )),
114
-
115
- selected !== null && h('div', { style: { marginTop: 14, borderTop: '1px solid var(--dsh-border, #333)', paddingTop: 10 } },
116
- h('div', { style: ROW },
117
- h('label', null, '包名'),
118
- h('input', { style: INPUT, value: pkg, onChange: (e) => setPkg(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') runSnapshot() } }),
119
- h('button', { style: BTN, onClick: runSnapshot, disabled: busy }, '性能快照'),
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
- snapshot && h('div', { style: { marginTop: 8 } }, h(MetricRows, { rows: snapshotRows })),
122
-
123
- h('div', { style: { ...ROW, marginTop: 12 } },
124
- h('label', null, '级别'),
125
- h('select', { style: INPUT, value: level, onChange: (e) => setLevel(e.target.value) },
126
- ['V', 'D', 'I', 'W', 'E', 'F'].map((lv) => h('option', { key: lv, value: lv }, lv))),
127
- h('label', null, '条数'),
128
- h('input', { style: { ...INPUT, width: 56 }, value: tail, onChange: (e) => setTail(e.target.value) }),
129
- h('button', { style: BTN, onClick: runLogcat, disabled: busy }, '获取日志'),
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
- h('div', { style: { color: 'var(--dsh-text-secondary, #888)', margin: '4px 0' } }, `logcat 共 ${logTotal} 条(显示 ${logEntries.length})`),
132
- h('div', { style: { maxHeight: 260, overflowY: 'auto', fontFamily: 'monospace', fontSize: 12, whiteSpace: 'pre-wrap' } },
133
- logEntries.length === 0
134
- ? h('div', null, '(暂无)')
135
- : logEntries.map((e) => h('div', { key: `${e.time}-${e.pid}-${e.tid}-${e.message}` },
136
- `${e.time} ${e.pid} ${e.tid} ${e.level} ${e.tag}: ${e.message}`)),
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
- function apply(ctx) {
143
- const slots = ctx.get('slots')
144
- const connection = ctx.get('connection')
145
- if (slots === undefined || connection === undefined || connection.rpc === undefined) return
146
- const runtime = createRuntime(connection.rpc)
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
- module.exports = { apply }
154
- return module.exports
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
+ }