@windypro-rourou/dsh-logcat 0.2.7 → 0.2.9

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
@@ -4,8 +4,10 @@ DSH Web GUI 的安卓实机调试面板(类似 Android Studio 的 Logcat 视
4
4
 
5
5
  ## 功能
6
6
 
7
- - **自动连接**:探测本机 adb(`ANDROID_HOME` / `ANDROID_SDK_ROOT` / 默认 `%LOCALAPPDATA%\Android\Sdk` / PATH),
8
- 2 秒轮询 `adb devices -l`;检测到处于调试模式的设备**自动附加 logcat 流**(`-v threadtime`),无需打开面板。
7
+ - **自动连接**:探测本机 adb(`ANDROID_HOME` / `ANDROID_SDK_ROOT` / 默认 `%LOCALAPPDATA%\Android\Sdk` / PATH /
8
+ `~/.dsh/adb`),每 2 秒轮询 `adb devices -l`;检测到处于调试模式的设备**自动附加 logcat 流**(`-v threadtime`),无需打开面板。
9
+ - **一键安装 adb**:未找到 adb 时,面板状态栏显示「一键安装 adb」按钮 —— 从 USTC 镜像(Google 官方兜底)
10
+ 下载 platform-tools 解压到 `~/.dsh/adb` 并自动接入,开箱即用(约 10MB)。
9
11
  - **实时日志**:WebSocket 推送,每设备保留最近 2000 行环形缓冲;断线自动重连(指数退避)。
10
12
  - **Logcat 面板**(侧边栏「Logcat」入口,右侧抽屉,**宽度可拖拽调整并记忆**):
11
13
  - 设备下拉(显示型号/序列号/状态,记住上次选择)
package/lib/client.js CHANGED
@@ -110,6 +110,7 @@ window.__ModuleLoader__.load({
110
110
  const [paused, setPaused] = useState(false);
111
111
  const [currentPackage, setCurrentPackage] = useState("");
112
112
  const [pkgInput, setPkgInput] = useState("");
113
+ const [installingAdb, setInstallingAdb] = useState(false);
113
114
  const [level, setLevel] = useState("");
114
115
  const [keyword, setKeyword] = useState("");
115
116
  const [autoScroll, setAutoScroll] = useState(true);
@@ -329,6 +330,30 @@ window.__ModuleLoader__.load({
329
330
  .catch(() => { /* device offline etc. */ });
330
331
  };
331
332
 
333
+ const installAdb = () => {
334
+ setInstallingAdb(true);
335
+ fetch(API_BASE + "/install-adb", { method: "POST" })
336
+ .then((res) => res.json())
337
+ .then((body) => {
338
+ if (body?.ok !== true) {
339
+ window.alert("adb 安装失败:" + (body?.error ?? "未知错误"));
340
+ return;
341
+ }
342
+ // Refresh status so the panel picks up the freshly installed adb.
343
+ return fetch(API_BASE + "/status")
344
+ .then((res) => res.json())
345
+ .then((b) => {
346
+ setAdbPath(b.adbPath ?? "");
347
+ setAdbReady(b.ready === true);
348
+ setDevices(b.devices ?? []);
349
+ setStreaming(b.streaming ?? []);
350
+ setCurrentPackage(b.currentPackage ?? "");
351
+ });
352
+ })
353
+ .catch(() => { window.alert("adb 安装失败:网络错误"); })
354
+ .finally(() => setInstallingAdb(false));
355
+ };
356
+
332
357
  return h("div", { className: "lc-panel" },
333
358
  h("div", { className: "lc-header" },
334
359
  h("button", { type: "button", className: "lc-back", onClick: () => controller.close() },
@@ -407,7 +432,18 @@ window.__ModuleLoader__.load({
407
432
  : h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef }),
408
433
  ),
409
434
  h("div", { className: "lc-status" },
410
- h("span", null, h("b", null, adbReady ? "adb 就绪" : "未找到 adb"), " · " + (adbPath || "—")),
435
+ h("span", null,
436
+ h("b", null, adbReady ? "adb 就绪" : "未找到 adb"),
437
+ " · " + (adbPath || "—"),
438
+ !adbReady
439
+ ? h("button", {
440
+ type: "button",
441
+ className: "lc-btn",
442
+ style: { marginLeft: 8, padding: "2px 8px" },
443
+ title: "从 Google 官方下载 platform-tools 到 ~/.dsh/adb 并接入(约 10MB)",
444
+ onClick: installAdb,
445
+ }, installingAdb ? "安装中…" : "一键安装 adb")
446
+ : null),
411
447
  h("span", null, "设备 " + devices.length + " · 在线 " + devices.filter((d) => d.state === "device").length),
412
448
  h("span", null, "显示 " + filtered.length + " / 缓冲 " + entries.length + " 行"),
413
449
  currentPackage !== ""
package/lib/index.js CHANGED
@@ -55,7 +55,12 @@ function logcatGuidance(engine) {
55
55
 
56
56
  /** ---------------------------------------------------------------- adb */
57
57
 
58
- /** Candidate adb.exe locations, in probe order. */
58
+ /** Where the plugin can self-install adb platform-tools (~/.dsh/adb). */
59
+ function bundledAdbRoot() {
60
+ return join(homedir(), '.dsh', 'adb')
61
+ }
62
+
63
+ /** Candidate adb(.exe) locations, in probe order. */
59
64
  function adbCandidates() {
60
65
  const list = []
61
66
  const envs = [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]
@@ -65,9 +70,69 @@ function adbCandidates() {
65
70
  const sdk = join(homedir(), 'AppData', 'Local', 'Android', 'Sdk')
66
71
  list.push(join(sdk, 'platform-tools', 'adb.exe'))
67
72
  list.push(join(sdk, 'platform-tools', 'adb'))
73
+ // Plugin-managed install location (one-click setup on first run).
74
+ list.push(join(bundledAdbRoot(), 'platform-tools', 'adb.exe'))
75
+ list.push(join(bundledAdbRoot(), 'platform-tools', 'adb'))
68
76
  return list
69
77
  }
70
78
 
79
+ /** Official platform-tools zip mirrors for this host, in preference order. */
80
+ function platformToolsUrls() {
81
+ const file = {
82
+ win32: 'platform-tools-latest-windows.zip',
83
+ darwin: 'platform-tools-latest-darwin.zip',
84
+ }[process.platform] ?? 'platform-tools-latest-linux.zip'
85
+ return [
86
+ 'https://mirrors.ustc.edu.cn/android/repository/' + file,
87
+ 'https://dl.google.com/android/repository/' + file,
88
+ ]
89
+ }
90
+
91
+ /** Download and unpack platform-tools into ~/.dsh/adb. Resolves the adb path. */
92
+ export async function installPlatformTools() {
93
+ const { mkdir, rm, writeFile } = await import('node:fs/promises')
94
+ const { tmpdir } = await import('node:os')
95
+ const dest = bundledAdbRoot()
96
+ await mkdir(dest, { recursive: true })
97
+ const zipPath = join(tmpdir(), `platform-tools-${Date.now()}.zip`)
98
+ let downloaded = false
99
+ let lastError = null
100
+ for (const url of platformToolsUrls()) {
101
+ try {
102
+ const response = await fetch(url, { signal: AbortSignal.timeout(180000) })
103
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
104
+ await writeFile(zipPath, Buffer.from(await response.arrayBuffer()))
105
+ downloaded = true
106
+ break
107
+ } catch (error) {
108
+ lastError = error
109
+ }
110
+ }
111
+ if (!downloaded) throw new Error(`download failed from all mirrors: ${lastError?.message ?? 'unknown'}`)
112
+ const exe = process.platform === 'win32' ? 'adb.exe' : 'adb'
113
+ try {
114
+ if (process.platform === 'win32') {
115
+ // PowerShell ships with Windows; Expand-Archive unzips natively.
116
+ await new Promise((resolve, reject) => {
117
+ execFile('powershell', ['-NoProfile', '-Command', `Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${dest}'`], { timeout: 180000, windowsHide: true }, (error) => {
118
+ if (error) reject(new Error('unzip failed: ' + (error.message ?? String(error))))
119
+ else resolve()
120
+ })
121
+ })
122
+ } else {
123
+ await new Promise((resolve, reject) => {
124
+ execFile('unzip', ['-o', zipPath, '-d', dest], { timeout: 180000 }, (error) => {
125
+ if (error) reject(new Error('unzip failed: ' + (error.message ?? String(error))))
126
+ else resolve()
127
+ })
128
+ })
129
+ }
130
+ } finally {
131
+ await rm(zipPath, { force: true })
132
+ }
133
+ return join(dest, 'platform-tools', exe)
134
+ }
135
+
71
136
  /** Run one short adb command, returning stdout (or null on failure). */
72
137
  function runAdb(adb, args, timeoutMs = 8000) {
73
138
  return new Promise((resolve) => {
@@ -415,9 +480,28 @@ function makeRoutes(engine) {
415
480
  ready: engine.adb !== null,
416
481
  devices: engine.deviceList(),
417
482
  streaming: [...engine.streams.keys()],
483
+ currentPackage: engine.currentPackage,
418
484
  })
419
485
  },
420
486
  },
487
+ {
488
+ kind: 'exact',
489
+ path: API_BASE + '/install-adb',
490
+ handler: async (req, res) => {
491
+ if (!isLoopbackRequest(req)) { writeJson(res, 403, { error: 'forbidden: loopback-only' }); return }
492
+ if ((req.method ?? 'GET') !== 'POST') { writeJson(res, 405, { error: 'method not allowed' }); return }
493
+ if (engine.adb !== null) { writeJson(res, 200, { ok: true, already: true, adbPath: engine.adb }); return }
494
+ try {
495
+ const adbPath = await installPlatformTools()
496
+ engine.adb = adbPath
497
+ engine.adbVersion = (await runAdb(adbPath, ['version']))?.split(/\r?\n/)[0] ?? ''
498
+ engine.startPolling()
499
+ writeJson(res, 200, { ok: true, already: false, adbPath })
500
+ } catch (error) {
501
+ writeJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) })
502
+ }
503
+ },
504
+ },
421
505
  {
422
506
  kind: 'exact',
423
507
  path: API_BASE + '/exec',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@windypro-rourou/dsh-logcat",
3
3
  "description": "Android Logcat viewer for the dsh web GUI: auto-connects to any adb device in debug mode, live logcat stream with level/keyword filters, pause/clear/export, plus agent tools (logcat_recent). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules copy, no dsh source changes.",
4
- "version": "0.2.7",
4
+ "version": "0.2.9",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {