@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 +7 -4
- package/lib/client.js +55 -0
- package/lib/index.js +58 -2
- package/package.json +1 -1
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
|
-
- `
|
|
21
|
-
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|