@windypro-rourou/dsh-logcat 0.2.3 → 0.2.5

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
@@ -7,7 +7,7 @@ DSH Web GUI 的安卓实机调试面板(类似 Android Studio 的 Logcat 视
7
7
  - **自动连接**:探测本机 adb(`ANDROID_HOME` / `ANDROID_SDK_ROOT` / 默认 `%LOCALAPPDATA%\Android\Sdk` / PATH),
8
8
  每 2 秒轮询 `adb devices -l`;检测到处于调试模式的设备**自动附加 logcat 流**(`-v threadtime`),无需打开面板。
9
9
  - **实时日志**:WebSocket 推送,每设备保留最近 2000 行环形缓冲;断线自动重连(指数退避)。
10
- - **Logcat 面板**(侧边栏「Logcat」入口):
10
+ - **Logcat 面板**(侧边栏「Logcat」入口,右侧抽屉,**宽度可拖拽调整并记忆**):
11
11
  - 设备下拉(显示型号/序列号/状态,记住上次选择)
12
12
  - 级别过滤(V/D/I/W/E/F 单选,颜色与 Android Studio 一致)
13
13
  - 关键词过滤、暂停/继续(暂停时缓冲,恢复自动回放)、清空、复制、导出 .txt
@@ -17,9 +17,12 @@ DSH Web GUI 的安卓实机调试面板(类似 Android Studio 的 Logcat 视
17
17
  - `logcat_devices`:列出已连接设备(serial / model / state),判断能否实机调试。
18
18
  - `adb_exec`:在指定设备执行 `adb shell` 命令(安装 APK、启动 Activity、查进程、截图、dump UI 等),
19
19
  破坏性操作(卸载 / 重启 / 清数据)需先确认。
20
- - `logcat_recent`:读取某设备最近 N 条日志,支持级别 / 关键词过滤。
21
- - **实机调试工作流**:构建安卓应用时,agent 的通告会动态列出当前已连接设备(serial + 型号),
22
- 可先向用户确认后用 `adb_exec` 安装 / 启动、`logcat_recent` 查看崩溃日志,闭环真机调试。
20
+ - `logcat_set_package`:设置 / 清除当前测试的 app 包名(安装 / 启动应用后调用)。
21
+ - `logcat_recent`:读取某设备最近 N 条日志,支持级别 / 关键词过滤;
22
+ 设置了测试包名(或显式传 `package`)时自动按该 app pid 过滤日志。
23
+ - **实机调试工作流**:构建安卓应用时,agent 的通告会动态列出当前已连接设备(serial + 型号)与当前测试包名,
24
+ 可先向用户确认后用 `adb_exec` 安装 / 启动、`logcat_set_package` 锁定目标 app、`logcat_recent` 按包名查看崩溃日志,
25
+ 闭环真机调试。
23
26
  - **附加能力**:`POST /api/dsh-logcat/exec` 可对设备执行 `adb shell` 命令(UI 后续版本可扩展)。
24
27
 
25
28
  ## 安装
package/lib/client.js CHANGED
@@ -35,6 +35,8 @@ window.__ModuleLoader__.load({
35
35
  [data-dsh-logcat-entry] .lc-entry-label { font-size: 13px; line-height: 1.2; opacity: .92; }
36
36
  .dsh-logcat-view { position: fixed; top: 0; right: 0; bottom: 0; width: min(620px, 94vw); display: flex; flex-direction: column; background: var(--lc-bg, #ffffff); border-left: 1px solid rgba(128,128,128,.3); box-shadow: -10px 0 28px rgba(0,0,0,.18); z-index: 9999; }
37
37
  .dsh-logcat-view[hidden] { display: none !important; }
38
+ .dsh-logcat-resize { position: absolute; left: -5px; top: 0; bottom: 0; width: 10px; cursor: col-resize; z-index: 2; }
39
+ .dsh-logcat-resize:hover, .dsh-logcat-resize[data-drag] { background: rgba(128,128,128,.28); }
38
40
  .lc-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
39
41
  .lc-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid rgba(128,128,128,.25); flex: none; }
40
42
  .lc-back { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; display: flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 6px; font-size: 13px; }
@@ -106,6 +108,7 @@ window.__ModuleLoader__.load({
106
108
  const [entries, setEntries] = useState([]);
107
109
  const [connected, setConnected] = useState(false);
108
110
  const [paused, setPaused] = useState(false);
111
+ const [currentPackage, setCurrentPackage] = useState("");
109
112
  const [level, setLevel] = useState("");
110
113
  const [keyword, setKeyword] = useState("");
111
114
  const [autoScroll, setAutoScroll] = useState(true);
@@ -188,6 +191,8 @@ window.__ModuleLoader__.load({
188
191
  appendEntries([frame.entry]);
189
192
  } else if (frame.type === "device-state") {
190
193
  setDevices(devicesRef.current.map((d) => d.serial === frame.serial ? { ...d, state: frame.state } : d));
194
+ } else if (frame.type === "package") {
195
+ setCurrentPackage(frame.package ?? "");
191
196
  }
192
197
  };
193
198
  applyFrameRef.current = applyFrame;
@@ -238,6 +243,7 @@ window.__ModuleLoader__.load({
238
243
  setAdbReady(body.ready === true);
239
244
  setDevices(body.devices ?? []);
240
245
  setStreaming(body.streaming ?? []);
246
+ setCurrentPackage(body.currentPackage ?? "");
241
247
  devicesRef.current = body.devices ?? [];
242
248
  if (serialRef.current === "" || !(body.devices ?? []).some((d) => d.serial === serialRef.current)) {
243
249
  const first = (body.devices ?? []).find((d) => d.state === "device");
@@ -363,6 +369,9 @@ window.__ModuleLoader__.load({
363
369
  h("span", null, h("b", null, adbReady ? "adb 就绪" : "未找到 adb"), " · " + (adbPath || "—")),
364
370
  h("span", null, "设备 " + devices.length + " · 在线 " + devices.filter((d) => d.state === "device").length),
365
371
  h("span", null, "显示 " + filtered.length + " / 缓冲 " + entries.length + " 行"),
372
+ currentPackage !== ""
373
+ ? h("span", { title: "当前测试应用包名(agent 通过 logcat_set_package 设置)" }, "测试: " + currentPackage)
374
+ : null,
366
375
  deviceState === "unauthorized"
367
376
  ? h("span", { style: { color: "#ef5350" } }, "⚠ 设备未授权 — 请在手机上点击“允许 USB 调试”")
368
377
  : null,
@@ -528,16 +537,59 @@ window.__ModuleLoader__.load({
528
537
  function mountPanel(controller) {
529
538
  let root;
530
539
  let container;
540
+ let handle;
541
+ let drag = null;
542
+
543
+ const MIN_WIDTH = 320;
544
+ const DEFAULT_WIDTH = 620;
545
+ const readWidth = () => {
546
+ try {
547
+ const w = Number(localStorage.getItem("dsh-logcat-width"));
548
+ if (Number.isFinite(w) && w >= MIN_WIDTH && w <= window.innerWidth - 40) return w;
549
+ } catch { /* private mode */ }
550
+ return Math.min(DEFAULT_WIDTH, Math.max(MIN_WIDTH, window.innerWidth - 80));
551
+ };
552
+
553
+ const onMove = (e) => {
554
+ if (drag === null || container === undefined) return;
555
+ const width = Math.max(MIN_WIDTH, Math.min(window.innerWidth - 40, drag.startWidth + (drag.startX - e.clientX)));
556
+ container.style.width = width + "px";
557
+ };
558
+ const onUp = () => {
559
+ if (drag === null) return;
560
+ handle?.removeAttribute("data-drag");
561
+ try {
562
+ const w = container.style.width.replace("px", "");
563
+ localStorage.setItem("dsh-logcat-width", w);
564
+ } catch { /* private mode */ }
565
+ window.removeEventListener("mousemove", onMove);
566
+ window.removeEventListener("mouseup", onUp);
567
+ drag = null;
568
+ };
569
+ const onDown = (e) => {
570
+ e.preventDefault();
571
+ drag = { startX: e.clientX, startWidth: container.getBoundingClientRect().width };
572
+ handle?.setAttribute("data-drag", "");
573
+ window.addEventListener("mousemove", onMove);
574
+ window.addEventListener("mouseup", onUp);
575
+ };
531
576
 
532
577
  const ensure = () => {
533
578
  if (container !== undefined && container.isConnected) return;
534
579
  root?.unmount();
535
580
  root = undefined;
536
581
  container?.remove();
582
+ handle = undefined;
537
583
  container = document.createElement("div");
538
584
  container.dataset.dshLogcatView = "";
539
585
  container.className = "dsh-logcat-view";
586
+ container.style.width = readWidth() + "px";
540
587
  container.hidden = true; // side-drawer: hidden until the sidebar entry is clicked
588
+ handle = document.createElement("div");
589
+ handle.className = "dsh-logcat-resize";
590
+ handle.title = "拖拽调整宽度";
591
+ handle.addEventListener("mousedown", onDown);
592
+ container.appendChild(handle);
541
593
  document.body.appendChild(container);
542
594
  root = createRoot(container);
543
595
  root.render(h(LogcatPanel, { controller }));
@@ -553,11 +605,14 @@ window.__ModuleLoader__.load({
553
605
  applyOpen();
554
606
 
555
607
  return () => {
608
+ window.removeEventListener("mousemove", onMove);
609
+ window.removeEventListener("mouseup", onUp);
556
610
  unsubscribe();
557
611
  root?.unmount();
558
612
  root = undefined;
559
613
  container?.remove();
560
614
  container = undefined;
615
+ handle = undefined;
561
616
  };
562
617
  }
563
618
  //#endregion
package/lib/index.js CHANGED
@@ -46,7 +46,10 @@ function logcatGuidance(engine) {
46
46
  const deviceLine = live.length > 0
47
47
  ? `当前已连接安卓设备:${live.map((d) => `${d.serial}${d.model !== '' ? `(${d.model})` : ''}`).join('、')}(已授权,可实机调试:安装 APK / 启动应用 / logcat / 截图等)。`
48
48
  : '当前无已连接的安卓设备(插上设备并开启 USB 调试后会自动识别)。'
49
- return `${LOGCAT_GUIDANCE}\n${deviceLine}`
49
+ const pkgLine = engine.currentPackage !== ''
50
+ ? `当前测试应用包名:${engine.currentPackage}(logcat_recent 默认按它过滤日志)。`
51
+ : ''
52
+ return `${LOGCAT_GUIDANCE}\n${deviceLine}${pkgLine !== '' ? `\n${pkgLine}` : ''}`
50
53
  }
51
54
  }
52
55
 
@@ -207,6 +210,7 @@ class AdbEngine {
207
210
  this.timer = null
208
211
  this.polling = false
209
212
  this.BUFFER_CAP = 2000
213
+ this.currentPackage = '' // app package currently under test (agent-set)
210
214
  }
211
215
 
212
216
  /** Probe and warm the adb server. Returns true when usable. */
@@ -340,6 +344,14 @@ class AdbEngine {
340
344
  return picked.slice(-lines)
341
345
  }
342
346
 
347
+ /** Map a package name to its running pids on a device ([] when not running). */
348
+ async pidsOfPackage(serial, packageName) {
349
+ if (this.adb === null || packageName === '') return []
350
+ const output = await runAdb(this.adb, ['-s', serial, 'shell', 'pidof', packageName], 8000)
351
+ if (output === null) return []
352
+ return output.split(/\s+/).map((s) => Number.parseInt(s, 10)).filter((n) => Number.isInteger(n) && n > 0)
353
+ }
354
+
343
355
  /** Broadcast one frame to every open panel socket. */
344
356
  broadcast(frame) {
345
357
  const payload = JSON.stringify(frame)
@@ -477,6 +489,7 @@ function logcatRecentTool(engine) {
477
489
  lines: { type: 'integer', description: 'Max entries to return (default 200, max 2000).' },
478
490
  level: { type: 'string', enum: ['V', 'D', 'I', 'W', 'E', 'F'], description: 'Minimum severity filter (V=verbose … F=fatal).' },
479
491
  filter: { type: 'string', description: 'Substring to filter the raw line (case-insensitive).' },
492
+ package: { type: 'string', description: 'App package to filter by (pid of the running app); falls back to the package set via logcat_set_package.' },
480
493
  },
481
494
  output: {
482
495
  schema: {
@@ -499,6 +512,7 @@ function logcatRecentTool(engine) {
499
512
  },
500
513
  },
501
514
  },
515
+ note: { type: 'string' },
502
516
  },
503
517
  },
504
518
  render: (_args, value) => {
@@ -521,7 +535,47 @@ function logcatRecentTool(engine) {
521
535
  const lines = Math.min(Math.max(Number(args.lines ?? 200) || 200, 1), 2000)
522
536
  const level = typeof args.level === 'string' ? args.level : ''
523
537
  const filter = typeof args.filter === 'string' ? args.filter : ''
524
- return { entries: engine.recent(serial, lines, level, filter) }
538
+ const pkg = (typeof args.package === 'string' ? args.package.trim() : '') || engine.currentPackage
539
+ const picked = engine.recent(serial, lines, level, filter)
540
+ if (pkg === '') return { entries: picked }
541
+ const pids = await engine.pidsOfPackage(serial, pkg)
542
+ if (pids.length === 0) {
543
+ return { entries: [], note: `package ${pkg} is not running on ${serial} — start it first (adb_exec: am start -n ...) or pass another package` }
544
+ }
545
+ const byPid = picked.filter((e) => e.pid > 0 && pids.includes(e.pid))
546
+ return byPid.length > 0
547
+ ? { entries: byPid }
548
+ : { entries: [], note: `no buffered logcat lines for package ${pkg} (pids ${pids.join(', ')}); the app may not have logged yet` }
549
+ },
550
+ })
551
+ }
552
+
553
+ /** The logcat_set_package agent tool: set/clear the app package currently under test. */
554
+ function logcatSetPackageTool(engine) {
555
+ return defineTool({
556
+ name: 'logcat_set_package',
557
+ description: 'Set (or clear) the Android app package currently under test, so logcat_recent filters by it. ' +
558
+ 'Call after installing/starting the app you are debugging (e.g. via adb_exec). ' +
559
+ 'Pass an empty string to clear the filter. Triggers: focus logs on one app, filter logcat by package.',
560
+ parameters: {
561
+ package: { type: 'string', description: 'The app package to track, e.g. "com.example.app"; an empty string clears it.' },
562
+ },
563
+ output: {
564
+ schema: {
565
+ type: 'object',
566
+ additionalProperties: false,
567
+ properties: {
568
+ package: { type: 'string', required: true },
569
+ },
570
+ },
571
+ render: (_args, value) => [{ type: 'text', text: value?.package ? `tracking package: ${value.package}` : 'package filter cleared' }],
572
+ },
573
+ async execute(args) {
574
+ const pkg = typeof args.package === 'string' ? args.package.trim() : ''
575
+ engine.currentPackage = pkg
576
+ // Let open panels reflect the current test package immediately.
577
+ engine.broadcast({ type: 'package', package: pkg })
578
+ return { package: pkg }
525
579
  },
526
580
  })
527
581
  }
@@ -639,6 +693,7 @@ export function apply(ctx, config) {
639
693
  devices: engine.deviceList(),
640
694
  streaming: [...engine.streams.keys()],
641
695
  bufferSizes: Object.fromEntries([...engine.buffers.entries()].map(([s, b]) => [s, b.length])),
696
+ currentPackage: engine.currentPackage,
642
697
  }),
643
698
  }
644
699
  if (typeof ctx.provide === 'function') ctx.provide('logcat', logcatHandle)
@@ -683,6 +738,7 @@ export function apply(ctx, config) {
683
738
  logcatRecentTool(engine),
684
739
  logcatDevicesTool(engine),
685
740
  adbExecTool(engine),
741
+ logcatSetPackageTool(engine),
686
742
  ].map((tool) => ctx.tools.register(tool))
687
743
  return () => { for (const dispose of disposers) dispose() }
688
744
  }, 'dsh-logcat: tools')
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.3",
4
+ "version": "0.2.5",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {