@windypro-rourou/dsh-logcat 0.2.2 → 0.2.4

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,13 +7,22 @@ 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
14
14
  - 窗口化渲染 + 自动滚动(滚动手动上翻时自动停用)
15
15
  - 未授权设备提示「请在手机上点击允许 USB 调试」
16
- - **Agent 工具**:`logcat_recent`(读取某设备最近 N 条日志,支持级别/关键词过滤)。
16
+ - **Agent 工具**:
17
+ - `logcat_devices`:列出已连接设备(serial / model / state),判断能否实机调试。
18
+ - `adb_exec`:在指定设备执行 `adb shell` 命令(安装 APK、启动 Activity、查进程、截图、dump UI 等),
19
+ 破坏性操作(卸载 / 重启 / 清数据)需先确认。
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
+ 闭环真机调试。
17
26
  - **附加能力**:`POST /api/dsh-logcat/exec` 可对设备执行 `adb shell` 命令(UI 后续版本可扩展)。
18
27
 
19
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; }
@@ -528,16 +530,59 @@ window.__ModuleLoader__.load({
528
530
  function mountPanel(controller) {
529
531
  let root;
530
532
  let container;
533
+ let handle;
534
+ let drag = null;
535
+
536
+ const MIN_WIDTH = 320;
537
+ const DEFAULT_WIDTH = 620;
538
+ const readWidth = () => {
539
+ try {
540
+ const w = Number(localStorage.getItem("dsh-logcat-width"));
541
+ if (Number.isFinite(w) && w >= MIN_WIDTH && w <= window.innerWidth - 40) return w;
542
+ } catch { /* private mode */ }
543
+ return Math.min(DEFAULT_WIDTH, Math.max(MIN_WIDTH, window.innerWidth - 80));
544
+ };
545
+
546
+ const onMove = (e) => {
547
+ if (drag === null || container === undefined) return;
548
+ const width = Math.max(MIN_WIDTH, Math.min(window.innerWidth - 40, drag.startWidth + (drag.startX - e.clientX)));
549
+ container.style.width = width + "px";
550
+ };
551
+ const onUp = () => {
552
+ if (drag === null) return;
553
+ handle?.removeAttribute("data-drag");
554
+ try {
555
+ const w = container.style.width.replace("px", "");
556
+ localStorage.setItem("dsh-logcat-width", w);
557
+ } catch { /* private mode */ }
558
+ window.removeEventListener("mousemove", onMove);
559
+ window.removeEventListener("mouseup", onUp);
560
+ drag = null;
561
+ };
562
+ const onDown = (e) => {
563
+ e.preventDefault();
564
+ drag = { startX: e.clientX, startWidth: container.getBoundingClientRect().width };
565
+ handle?.setAttribute("data-drag", "");
566
+ window.addEventListener("mousemove", onMove);
567
+ window.addEventListener("mouseup", onUp);
568
+ };
531
569
 
532
570
  const ensure = () => {
533
571
  if (container !== undefined && container.isConnected) return;
534
572
  root?.unmount();
535
573
  root = undefined;
536
574
  container?.remove();
575
+ handle = undefined;
537
576
  container = document.createElement("div");
538
577
  container.dataset.dshLogcatView = "";
539
578
  container.className = "dsh-logcat-view";
579
+ container.style.width = readWidth() + "px";
540
580
  container.hidden = true; // side-drawer: hidden until the sidebar entry is clicked
581
+ handle = document.createElement("div");
582
+ handle.className = "dsh-logcat-resize";
583
+ handle.title = "拖拽调整宽度";
584
+ handle.addEventListener("mousedown", onDown);
585
+ container.appendChild(handle);
541
586
  document.body.appendChild(container);
542
587
  root = createRoot(container);
543
588
  root.render(h(LogcatPanel, { controller }));
@@ -553,11 +598,14 @@ window.__ModuleLoader__.load({
553
598
  applyOpen();
554
599
 
555
600
  return () => {
601
+ window.removeEventListener("mousemove", onMove);
602
+ window.removeEventListener("mouseup", onUp);
556
603
  unsubscribe();
557
604
  root?.unmount();
558
605
  root = undefined;
559
606
  container?.remove();
560
607
  container = undefined;
608
+ handle = undefined;
561
609
  };
562
610
  }
563
611
  //#endregion
package/lib/index.js CHANGED
@@ -36,7 +36,22 @@ const SECTION_ORDER = 152
36
36
 
37
37
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
38
38
  export const LOGCAT_GUIDANCE =
39
- '本机已安装 dsh-logcat 插件(DSH Web GUI 的安卓实机调试面板):侧边栏「Logcat」入口;自动探测本机 adb(ANDROID_HOME / 默认 SDK 路径),对处于调试模式的已连接设备自动附加 logcat 流(threadtime 格式,每设备保留最近 2000 行环形缓冲);Web 面板支持设备切换、级别/关键词过滤、暂停/清空/导出;agent 可用 logcat_recent 工具读取最近日志。限制:需设备开启 USB 调试并授权本机;logcat 输出可能含敏感信息;执行 adb 命令消耗真实设备资源,先确认再操作。用户提到「Logcat / 安卓日志 / 实机调试 / adb 日志」时即指本插件,请据此协作。'
39
+ '本机已安装 dsh-logcat 插件(DSH Web GUI 的安卓实机调试面板):侧边栏「Logcat」入口;自动探测本机 adb,对已连接的调试设备自动附加 logcat 流(threadtime,每设备 2000 行环形缓冲);Web 面板支持设备切换、级别/关键词过滤、暂停/清空/导出;agent 工具:logcat_devices(列出已连接设备)、adb_exec(在设备上执行 adb shell:安装/启动/查看日志/截图等)、logcat_recent(读取最近日志)。构建安卓应用时若检测到已连接设备,可先向用户确认后用 adb_exec / logcat_recent 直接实机调试。限制:需设备开启 USB 调试并授权本机;logcat 输出可能含敏感信息;adb 命令消耗真实设备资源,破坏性操作(卸载/重启/清数据)先确认再执行。用户提到「Logcat / 安卓日志 / 实机调试 / adb 日志」时即指本插件,请据此协作。'
40
+
41
+ /** Dynamic model-facing announcement: base guidance plus currently attached devices. */
42
+ function logcatGuidance(engine) {
43
+ return () => {
44
+ const devices = engine.deviceList()
45
+ const live = devices.filter((d) => d.state === 'device')
46
+ const deviceLine = live.length > 0
47
+ ? `当前已连接安卓设备:${live.map((d) => `${d.serial}${d.model !== '' ? `(${d.model})` : ''}`).join('、')}(已授权,可实机调试:安装 APK / 启动应用 / logcat / 截图等)。`
48
+ : '当前无已连接的安卓设备(插上设备并开启 USB 调试后会自动识别)。'
49
+ const pkgLine = engine.currentPackage !== ''
50
+ ? `当前测试应用包名:${engine.currentPackage}(logcat_recent 默认按它过滤日志)。`
51
+ : ''
52
+ return `${LOGCAT_GUIDANCE}\n${deviceLine}${pkgLine !== '' ? `\n${pkgLine}` : ''}`
53
+ }
54
+ }
40
55
 
41
56
  /** ---------------------------------------------------------------- adb */
42
57
 
@@ -195,6 +210,7 @@ class AdbEngine {
195
210
  this.timer = null
196
211
  this.polling = false
197
212
  this.BUFFER_CAP = 2000
213
+ this.currentPackage = '' // app package currently under test (agent-set)
198
214
  }
199
215
 
200
216
  /** Probe and warm the adb server. Returns true when usable. */
@@ -328,6 +344,14 @@ class AdbEngine {
328
344
  return picked.slice(-lines)
329
345
  }
330
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
+
331
355
  /** Broadcast one frame to every open panel socket. */
332
356
  broadcast(frame) {
333
357
  const payload = JSON.stringify(frame)
@@ -465,6 +489,7 @@ function logcatRecentTool(engine) {
465
489
  lines: { type: 'integer', description: 'Max entries to return (default 200, max 2000).' },
466
490
  level: { type: 'string', enum: ['V', 'D', 'I', 'W', 'E', 'F'], description: 'Minimum severity filter (V=verbose … F=fatal).' },
467
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.' },
468
493
  },
469
494
  output: {
470
495
  schema: {
@@ -487,6 +512,7 @@ function logcatRecentTool(engine) {
487
512
  },
488
513
  },
489
514
  },
515
+ note: { type: 'string' },
490
516
  },
491
517
  },
492
518
  render: (_args, value) => {
@@ -509,7 +535,135 @@ function logcatRecentTool(engine) {
509
535
  const lines = Math.min(Math.max(Number(args.lines ?? 200) || 200, 1), 2000)
510
536
  const level = typeof args.level === 'string' ? args.level : ''
511
537
  const filter = typeof args.filter === 'string' ? args.filter : ''
512
- 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
+ return { package: pkg }
577
+ },
578
+ })
579
+ }
580
+
581
+ /** The logcat_devices agent tool: list attached adb devices for on-device debugging. */
582
+ function logcatDevicesTool(engine) {
583
+ return defineTool({
584
+ name: 'logcat_devices',
585
+ description: 'List currently attached Android devices (serial, model, state) for on-device debugging. ' +
586
+ 'Triggers: check adb devices, see which phones are connected, decide whether to debug a build on a real device.',
587
+ parameters: {},
588
+ output: {
589
+ schema: {
590
+ type: 'object',
591
+ additionalProperties: false,
592
+ properties: {
593
+ devices: {
594
+ type: 'array',
595
+ required: true,
596
+ items: {
597
+ type: 'object',
598
+ additionalProperties: false,
599
+ properties: {
600
+ serial: { type: 'string', required: true },
601
+ model: { type: 'string', required: true },
602
+ state: { type: 'string', required: true },
603
+ },
604
+ },
605
+ },
606
+ },
607
+ },
608
+ render: (_args, value) => {
609
+ const devices = value?.devices ?? []
610
+ if (devices.length === 0) return [{ type: 'text', text: '(no devices attached)' }]
611
+ const lines = devices.map((d) => `${d.state.padEnd(11)} ${(d.model || '?').padEnd(22)} ${d.serial}`)
612
+ return [{ type: 'text', text: lines.join('\n') }]
613
+ },
614
+ },
615
+ async execute() {
616
+ return { devices: engine.deviceList().map(({ serial, model, state }) => ({ serial, model, state })) }
617
+ },
618
+ })
619
+ }
620
+
621
+ /** The adb_exec agent tool: run one adb shell command on a device. */
622
+ function adbExecTool(engine) {
623
+ return defineTool({
624
+ name: 'adb_exec',
625
+ description: 'Run one adb shell command on an attached Android device — e.g. install an APK (pm install -r <path>), ' +
626
+ 'start an activity (am start -n pkg/.Activity), list processes, take a screenshot (screencap -p /sdcard/x.png), ' +
627
+ 'or dump UI (uiautomator dump). NOTE: executes real commands on the user\'s device and consumes device resources — ' +
628
+ 'confirm intent before destructive actions (uninstall, reboot, clear data, factory reset).',
629
+ parameters: {
630
+ serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
631
+ command: { type: 'string', description: 'The shell command to run without the "adb shell" prefix, e.g. "pm list packages | grep com.example" or "am start -n com.example/.MainActivity".' },
632
+ timeoutMs: { type: 'integer', description: 'Command timeout in ms (default 15000, max 120000).' },
633
+ },
634
+ output: {
635
+ schema: {
636
+ type: 'object',
637
+ additionalProperties: false,
638
+ properties: {
639
+ ok: { type: 'boolean', required: true },
640
+ code: { type: 'integer' },
641
+ stdout: { type: 'string', required: true },
642
+ stderr: { type: 'string', required: true },
643
+ },
644
+ },
645
+ render: (_args, value) => {
646
+ const out = value?.stdout ?? ''
647
+ const err = value?.stderr ?? ''
648
+ const parts = []
649
+ if (out !== '') parts.push(out.trimEnd())
650
+ if (err !== '') parts.push('stderr: ' + err.trimEnd())
651
+ if (parts.length === 0) parts.push(`(exit ${value?.code ?? '?'}, no output)`)
652
+ return [{ type: 'text', text: parts.join('\n') }]
653
+ },
654
+ },
655
+ async execute(args) {
656
+ const devices = engine.deviceList()
657
+ const serial = typeof args.serial === 'string' && args.serial !== ''
658
+ ? args.serial
659
+ : devices.find((d) => d.state === 'device')?.serial ?? ''
660
+ const command = typeof args.command === 'string' ? args.command.trim() : ''
661
+ if (engine.adb === null) return { ok: false, code: null, stdout: '', stderr: 'adb not found on this host' }
662
+ if (serial === '') return { ok: false, code: null, stdout: '', stderr: 'no attached device; connect and authorize one first' }
663
+ if (command === '') return { ok: false, code: null, stdout: '', stderr: 'command is required' }
664
+ const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 15000) || 15000, 1000), 120000)
665
+ const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', command], timeoutMs)
666
+ return { ok: result.ok, code: result.code, stdout: result.stdout, stderr: result.stderr }
513
667
  },
514
668
  })
515
669
  }
@@ -537,6 +691,7 @@ export function apply(ctx, config) {
537
691
  devices: engine.deviceList(),
538
692
  streaming: [...engine.streams.keys()],
539
693
  bufferSizes: Object.fromEntries([...engine.buffers.entries()].map(([s, b]) => [s, b.length])),
694
+ currentPackage: engine.currentPackage,
540
695
  }),
541
696
  }
542
697
  if (typeof ctx.provide === 'function') ctx.provide('logcat', logcatHandle)
@@ -565,7 +720,7 @@ export function apply(ctx, config) {
565
720
  disposeSection = ctx.systemPrompt.section({
566
721
  name: 'plugin:dsh-logcat',
567
722
  order: SECTION_ORDER,
568
- text: LOGCAT_GUIDANCE,
723
+ text: logcatGuidance(engine),
569
724
  })
570
725
  }
571
726
  disposeRoutes = ctx.effect(() => {
@@ -577,7 +732,12 @@ export function apply(ctx, config) {
577
732
  }
578
733
  }, 'dsh-logcat: routes')
579
734
  disposeTools = ctx.effect(() => {
580
- const disposers = [logcatRecentTool(engine)].map((tool) => ctx.tools.register(tool))
735
+ const disposers = [
736
+ logcatRecentTool(engine),
737
+ logcatDevicesTool(engine),
738
+ adbExecTool(engine),
739
+ logcatSetPackageTool(engine),
740
+ ].map((tool) => ctx.tools.register(tool))
581
741
  return () => { for (const dispose of disposers) dispose() }
582
742
  }, 'dsh-logcat: tools')
583
743
  initOnce()
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.2",
4
+ "version": "0.2.4",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {