dsh-adb 0.2.0 → 1.0.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.0.0)
18
+
19
+ A "设备" tab in the conversation view ring (next to chat / trajectory / automation): device list with status, package-scoped performance snapshot (memory / frame stats / battery), and filtered logcat tail. Data flows over the package RPC channel; requires the plugin installed in a web profile and a GUI 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,156 @@
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.
5
+ */
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-adb',
8
+ factory: (require) => {
9
+ 'use strict'
10
+ const module = { exports: {} }
11
+
12
+ const React = require('react')
13
+ const CHANNEL = '/dsh-adb'
14
+
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
+ }
25
+
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
+ }
33
+
34
+ const h = React.createElement
35
+ const ROW = { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0' }
36
+ const BTN = { padding: '3px 10px', cursor: 'pointer' }
37
+ const INPUT = { padding: '3px 6px' }
38
+
39
+ function MetricRows({ rows }) {
40
+ if (!rows || rows.length === 0) return h('div', null, '(无数据)')
41
+ return h('table', { style: { borderCollapse: 'collapse' } },
42
+ rows.map((row) => h('tr', { key: row[0] },
43
+ h('td', { style: { padding: '2px 12px 2px 0', color: 'var(--dsh-text-secondary, #888)' } }, row[0]),
44
+ h('td', { style: { padding: '2px 0' } }, String(row[1])),
45
+ )),
46
+ )
47
+ }
48
+
49
+ function DeviceView(props) {
50
+ const runtime = props.runtime
51
+ const [devices, setDevices] = React.useState([])
52
+ const [selected, setSelected] = React.useState(null)
53
+ const [pkg, setPkg] = React.useState('com.android.systemui')
54
+ const [snapshot, setSnapshot] = React.useState(null)
55
+ const [logEntries, setLogEntries] = React.useState([])
56
+ const [logTotal, setLogTotal] = React.useState(0)
57
+ const [level, setLevel] = React.useState('E')
58
+ const [tail, setTail] = React.useState('30')
59
+ const [error, setError] = React.useState(null)
60
+ const [busy, setBusy] = React.useState(false)
61
+
62
+ const refresh = () => {
63
+ setBusy(true); setError(null)
64
+ runtime.listDevices()
65
+ .then((value) => setDevices(value.devices ?? []))
66
+ .catch((e) => setError(String(e.message ?? e)))
67
+ .finally(() => setBusy(false))
68
+ }
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))
78
+ }
79
+
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))
88
+ }
89
+
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])
96
+ }
97
+
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 }, '性能快照'),
120
+ ),
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 }, '获取日志'),
130
+ ),
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}`)),
137
+ ),
138
+ ),
139
+ )
140
+ }
141
+
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
+ }
152
+
153
+ module.exports = { apply }
154
+ return module.exports
155
+ },
156
+ })
package/lib/index.js CHANGED
@@ -7,6 +7,7 @@ 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';
10
11
  export const name = 'dsh-adb';
11
12
  /** The tool registry is a hard dependency: every tool registers through it. */
12
13
  export const inject = ['tools'];
@@ -25,5 +26,9 @@ export function apply(ctx, config) {
25
26
  registerPerfTool(ctx, cfg);
26
27
  registerPerfBaselineTool(ctx, cfg, config.baselineDir ?? DEFAULT_BASELINE_DIR);
27
28
  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');
29
+ // The RPC channel needs the client connection, which mounts after this
30
+ // plugin starts in web compositions; register lazily so headless profiles
31
+ // (no connection) stay unaffected.
32
+ ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg));
33
+ ctx.logger.info('[dsh-adb] loaded: 9 tools + web device panel rpc');
29
34
  }
package/lib/rpc.d.ts ADDED
@@ -0,0 +1,23 @@
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
+ /**
13
+ * Package-private Client↔Host RPC endpoint dispatch for the Web device panel.
14
+ * Extracted from the connection handler so the endpoints are unit-testable
15
+ * against a fake adb backend.
16
+ */
17
+ export declare function handleRpcEndpoint(ctx: Context, cfg: AdbConfig, endpoint: string, raw: unknown, signal: AbortSignal): Promise<RpcEndpointResult>;
18
+ /**
19
+ * Register the Package-private Client↔Host RPC for the Web device panel
20
+ * (conversation.view tab "设备"). Called lazily once the client connection
21
+ * mounts; headless compositions (no connection) stay unaffected.
22
+ */
23
+ export declare function registerRpc(ctx: Context, cfg: AdbConfig): void;
package/lib/rpc.js ADDED
@@ -0,0 +1,66 @@
1
+ import { classifyFailure, runAdb } from './adb.js';
2
+ import { parseDevices } from './parsers/devices.js';
3
+ import { matchesLevel, parseLogcat } from './parsers/logcat.js';
4
+ import { capturePerfSnapshot } from './tools/perf.js';
5
+ /**
6
+ * Package-private Client↔Host RPC endpoint dispatch for the Web device panel.
7
+ * Extracted from the connection handler so the endpoints are unit-testable
8
+ * against a fake adb backend.
9
+ */
10
+ export async function handleRpcEndpoint(ctx, cfg, endpoint, raw, signal) {
11
+ try {
12
+ const payload = (raw ?? {});
13
+ switch (endpoint) {
14
+ case 'listDevices': {
15
+ const result = await runAdb(ctx, cfg, ['devices', '-l'], { signal });
16
+ if (result.exitCode !== 0)
17
+ throw classifyFailure(result);
18
+ return { ok: true, value: { server: 'ok', devices: parseDevices(result.stdout) } };
19
+ }
20
+ case 'perfSnapshot': {
21
+ const pkg = typeof payload.package === 'string' ? payload.package : undefined;
22
+ if (pkg === undefined)
23
+ throw new Error('perfSnapshot requires a string "package"');
24
+ const serial = typeof payload.serial === 'string' ? payload.serial : undefined;
25
+ const snapshot = await capturePerfSnapshot(ctx, cfg, signal, { package: pkg, serial });
26
+ return { ok: true, value: snapshot };
27
+ }
28
+ case 'logcatTail': {
29
+ const serial = typeof payload.serial === 'string' ? payload.serial : undefined;
30
+ const level = typeof payload.level === 'string' ? payload.level : undefined;
31
+ const tail = typeof payload.tail === 'number' && payload.tail > 0 ? Math.floor(payload.tail) : 30;
32
+ const output = await runAdb(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], {
33
+ signal,
34
+ serial,
35
+ maxBytes: 8 * 1024 * 1024,
36
+ });
37
+ if (output.exitCode !== 0)
38
+ throw classifyFailure(output);
39
+ let entries = parseLogcat(output.stdout);
40
+ if (level !== undefined)
41
+ entries = entries.filter((entry) => matchesLevel(entry, level));
42
+ const capped = entries.slice(-tail);
43
+ return { ok: true, value: { total: entries.length, truncated: entries.length > tail, entries: capped } };
44
+ }
45
+ default:
46
+ return { ok: false, error: { message: `unknown endpoint: ${endpoint}` } };
47
+ }
48
+ }
49
+ catch (error) {
50
+ return { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
51
+ }
52
+ }
53
+ /**
54
+ * Register the Package-private Client↔Host RPC for the Web device panel
55
+ * (conversation.view tab "设备"). Called lazily once the client connection
56
+ * mounts; headless compositions (no connection) stay unaffected.
57
+ */
58
+ export function registerRpc(ctx, cfg) {
59
+ const connection = ctx.get('connection');
60
+ const rpc = connection?.rpc;
61
+ if (rpc === undefined)
62
+ return;
63
+ ctx.effect(() => rpc.handle('/dsh-adb', (endpoint, raw, signal) => handleRpcEndpoint(ctx, cfg, endpoint, raw, signal),
64
+ // Browser-only channel: accept requests from the loopback web GUI.
65
+ { authority: 'loopback' }));
66
+ }
@@ -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.0.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",
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": {