@windypro-rourou/dsh-logcat 0.5.0-preview.2 → 0.5.0-preview.3

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/lib/client.js CHANGED
@@ -35,50 +35,62 @@ 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); z-index: 9999; }
37
37
  .dsh-logcat-view[hidden] { display: none !important; }
38
- .dsh-logcat-panel { position: absolute; inset: 0; 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); }
38
+ .dsh-logcat-panel {
39
+ position: absolute; inset: 0; display: flex; flex-direction: column;
40
+ background: light-dark(#ffffff, #171a1f);
41
+ color: light-dark(#1a1d23, #dfe3ea);
42
+ border-left: 1px solid light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14));
43
+ box-shadow: -10px 0 28px rgba(0,0,0,.22);
44
+ }
39
45
  .dsh-logcat-resize { position: absolute; left: -6px; top: 0; bottom: 0; width: 12px; cursor: col-resize; z-index: 2; display: flex; align-items: center; justify-content: center; }
40
- .dsh-logcat-resize::before { content: ""; width: 3px; height: 48px; border-radius: 2px; background: rgba(128,128,128,.35); transition: background .15s, height .15s; }
41
- .dsh-logcat-resize:hover::before, .dsh-logcat-resize[data-drag]::before { background: rgba(128,128,128,.75); height: 72px; }
42
- .dsh-logcat-resize:hover, .dsh-logcat-resize[data-drag] { background: rgba(128,128,128,.16); }
46
+ .dsh-logcat-resize::before { content: ""; width: 3px; height: 48px; border-radius: 2px; background: light-dark(rgba(0,0,0,.3), rgba(255,255,255,.35)); transition: background .15s, height .15s; }
47
+ .dsh-logcat-resize:hover::before, .dsh-logcat-resize[data-drag]::before { background: light-dark(rgba(0,0,0,.6), rgba(255,255,255,.75)); height: 72px; }
48
+ .dsh-logcat-resize:hover, .dsh-logcat-resize[data-drag] { background: light-dark(rgba(0,0,0,.08), rgba(255,255,255,.14)); }
43
49
  .lc-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
44
- .lc-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid rgba(128,128,128,.25); flex: none; }
50
+ .lc-header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; }
45
51
  .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; }
46
- .lc-back:hover { background: rgba(128,128,128,.14); }
47
- .lc-title { font-size: 15px; font-weight: 600; margin: 0; }
52
+ .lc-back:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
53
+ .lc-title { font-size: 15px; font-weight: 700; margin: 0; letter-spacing: .2px; }
48
54
  .lc-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
49
- .lc-dot.on { background: #4caf50; }
55
+ .lc-dot.on { background: #4caf50; box-shadow: 0 0 6px rgba(76,175,80,.6); }
50
56
  .lc-dot.off { background: #9e9e9e; }
51
57
  .lc-dot.warn { background: #ff9800; }
52
- .lc-toolbar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid rgba(128,128,128,.25); flex: none; flex-wrap: wrap; }
53
- .lc-select, .lc-search { background: rgba(128,128,128,.08); border: 1px solid rgba(128,128,128,.3); border-radius: 6px; color: inherit; font: inherit; font-size: 12px; padding: 4px 8px; }
54
- .lc-select:focus, .lc-search:focus { outline: 1px solid rgba(128,128,128,.5); }
58
+ .lc-stats { display: flex; align-items: center; gap: 6px; padding: 6px 14px; border-bottom: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); flex: none; flex-wrap: wrap; font-size: 11px; }
59
+ .lc-chip { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 999px; background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); color: light-dark(#4a4f58, #b9bec7); white-space: nowrap; }
60
+ .lc-chip b { color: inherit; font-weight: 600; }
61
+ .lc-mem-bar { display: inline-block; width: 44px; height: 5px; border-radius: 3px; background: light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14)); overflow: hidden; vertical-align: middle; }
62
+ .lc-mem-fill { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg, #4caf50, #fbc02d); }
63
+ .lc-mem-fill.hot { background: linear-gradient(90deg, #fbc02d, #ef5350); }
64
+ .lc-toolbar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; flex-wrap: wrap; }
65
+ .lc-select, .lc-search { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 7px; color: inherit; font: inherit; font-size: 12px; padding: 5px 9px; }
66
+ .lc-select:focus, .lc-search:focus { outline: 2px solid rgba(66,133,244,.35); border-color: rgba(66,133,244,.5); }
55
67
  .lc-search { flex: 1; min-width: 120px; max-width: 320px; }
56
- .lc-levels { display: flex; gap: 2px; }
57
- .lc-level { border: 1px solid transparent; background: transparent; color: inherit; font: inherit; font-size: 12px; font-weight: 600; width: 24px; height: 24px; border-radius: 5px; cursor: pointer; }
58
- .lc-level:hover { background: rgba(128,128,128,.12); }
59
- .lc-level[data-on] { background: rgba(128,128,128,.22); border-color: rgba(128,128,128,.4); }
68
+ .lc-levels { display: flex; gap: 2px; background: light-dark(rgba(0,0,0,.04), rgba(255,255,255,.06)); padding: 2px; border-radius: 7px; }
69
+ .lc-level { border: 0; background: transparent; color: inherit; font: inherit; font-size: 12px; font-weight: 600; width: 24px; height: 22px; border-radius: 5px; cursor: pointer; }
70
+ .lc-level:hover { background: light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); }
71
+ .lc-level[data-on] { background: light-dark(#fff, #2a2f37); box-shadow: 0 1px 3px rgba(0,0,0,.2); }
60
72
  .lc-level.v { color: #9e9e9e; } .lc-level.d { color: #4fc3f7; } .lc-level.i { color: #4caf50; }
61
73
  .lc-level.w { color: #fbc02d; } .lc-level.e { color: #ef5350; } .lc-level.f { color: #ab47bc; }
62
- .lc-btn { border: 1px solid rgba(128,128,128,.3); background: transparent; color: inherit; font: inherit; font-size: 12px; padding: 4px 10px; border-radius: 6px; cursor: pointer; }
63
- .lc-btn:hover { background: rgba(128,128,128,.12); }
64
- .lc-btn[data-on] { background: rgba(128,128,128,.22); }
74
+ .lc-btn { border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); background: transparent; color: inherit; font: inherit; font-size: 12px; padding: 5px 10px; border-radius: 7px; cursor: pointer; transition: background .12s; }
75
+ .lc-btn:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
76
+ .lc-btn[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); border-color: rgba(66,133,244,.45); color: #4285f4; }
65
77
  .lc-body { flex: 1; min-height: 0; position: relative; overflow: hidden; }
66
78
  .lc-log { position: absolute; inset: 0; overflow: auto; font-family: Consolas, "Cascadia Mono", "Courier New", monospace; font-size: 12px; line-height: 20px; }
67
79
  .lc-log-inner { position: relative; }
68
80
  .lc-line { position: absolute; left: 0; right: 0; padding: 0 12px; white-space: pre; overflow: hidden; text-overflow: ellipsis; cursor: default; }
69
- .lc-line:hover { background: rgba(128,128,128,.12); }
70
- .lc-line .ts { color: var(--lc-dim, #9e9e9e); margin-right: 8px; }
71
- .lc-line .pid { color: var(--lc-dim, #9e9e9e); margin-right: 6px; }
81
+ .lc-line:hover { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.07)); }
82
+ .lc-line .ts { color: light-dark(#9aa0a8, #7c838d); margin-right: 8px; }
83
+ .lc-line .pid { color: light-dark(#9aa0a8, #7c838d); margin-right: 6px; }
72
84
  .lc-line .lv { display: inline-block; width: 14px; text-align: center; font-weight: 700; margin-right: 6px; }
73
85
  .lc-line .lv.V { color: #9e9e9e; } .lc-line .lv.D { color: #4fc3f7; } .lc-line .lv.I { color: #4caf50; }
74
86
  .lc-line .lv.W { color: #fbc02d; } .lc-line .lv.E { color: #ef5350; } .lc-line .lv.F { color: #ab47bc; }
75
87
  .lc-line .tag { color: #29b6f6; margin-right: 8px; }
76
- .lc-line.cont .msg { padding-left: 44px; color: var(--lc-dim, #9e9e9e); }
88
+ .lc-line.cont .msg { padding-left: 44px; color: light-dark(#9aa0a8, #7c838d); }
77
89
  .lc-line.crash { background: rgba(239,83,80,.14); box-shadow: inset 2px 0 0 #ef5350; }
78
90
  .lc-line.crash .tag { color: #ef5350; font-weight: 700; }
79
91
  .lc-line.crash .msg { color: #ef5350; }
80
- .lc-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--lc-dim, #9e9e9e); font-size: 13px; }
81
- .lc-status { display: flex; align-items: center; gap: 14px; padding: 4px 12px; border-top: 1px solid rgba(128,128,128,.25); flex: none; font-size: 11px; color: var(--lc-dim, #9e9e9e); }
92
+ .lc-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: light-dark(#9aa0a8, #7c838d); font-size: 13px; }
93
+ .lc-status { display: flex; align-items: center; gap: 14px; padding: 5px 14px; border-top: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; font-size: 11px; color: light-dark(#6b7078, #9aa0a8); }
82
94
  .lc-status b { font-weight: 600; color: inherit; }
83
95
  `;
84
96
  //#endregion
@@ -122,6 +134,7 @@ window.__ModuleLoader__.load({
122
134
  const [updateAvailable, setUpdateAvailable] = useState(false);
123
135
  const [updateKind, setUpdateKind] = useState("");
124
136
  const [updateHint, setUpdateHint] = useState("");
137
+ const [stats, setStats] = useState(null);
125
138
  const [level, setLevel] = useState("");
126
139
  const [keyword, setKeyword] = useState("");
127
140
  const [autoScroll, setAutoScroll] = useState(true);
@@ -271,6 +284,34 @@ window.__ModuleLoader__.load({
271
284
  .catch(() => { /* host not up yet */ });
272
285
  }, []);
273
286
 
287
+ // Live device stats (model / memory / cpu / battery) — refresh every 5s.
288
+ useEffect(() => {
289
+ let alive = true;
290
+ let timer = null;
291
+ const load = () => {
292
+ const target = serialRef.current;
293
+ if (target === "") { setStats(null); return; }
294
+ fetch(API_BASE + "/stats?serial=" + encodeURIComponent(target))
295
+ .then((res) => res.json())
296
+ .then((body) => { if (alive) setStats(body?.ready === true ? body : null); })
297
+ .catch(() => { /* device offline */ });
298
+ };
299
+ load();
300
+ timer = setInterval(load, 5000);
301
+ return () => { alive = false; if (timer !== null) clearInterval(timer); };
302
+ }, [serial]);
303
+
304
+ const parseCpu = (line) => {
305
+ if (!line) return undefined;
306
+ const m = /([\d.]+)%cpu\s+.*?\s+([\d.]+)%idle/.exec(line);
307
+ if (m === null) return undefined;
308
+ const total = Number(m[1]);
309
+ if (total <= 0) return undefined;
310
+ return Math.round(((total - Number(m[2])) / total) * 100);
311
+ };
312
+ const BATTERY_STATUS = { 1: "未知", 2: "充电中", 3: "放电", 4: "未充电", 5: "已满" };
313
+ const mb = (kb) => kb !== undefined ? (kb / 1024 / 1024).toFixed(1) : undefined;
314
+
274
315
  const filtered = useMemo(() => {
275
316
  const needle = keyword.trim().toLowerCase();
276
317
  return entries.filter((e) => {
@@ -392,6 +433,33 @@ window.__ModuleLoader__.load({
392
433
  h("option", { key: d.serial, value: d.serial },
393
434
  (d.model !== "" ? d.model + " · " : "") + d.serial + " [" + d.state + "]"))),
394
435
  ),
436
+ h("div", { className: "lc-stats" },
437
+ stats?.model
438
+ ? h("span", { className: "lc-chip", title: "设备型号 / 系统版本" },
439
+ stats.model + (stats.androidVersion ? " · Android " + stats.androidVersion : ""))
440
+ : null,
441
+ stats?.resolution
442
+ ? h("span", { className: "lc-chip", title: "屏幕分辨率" }, stats.resolution)
443
+ : null,
444
+ stats?.memPct !== undefined
445
+ ? h("span", { className: "lc-chip", title: "内存 " + (mb(stats.usedMemKb) ?? "?") + "G / " + (mb(stats.totalMemKb) ?? "?") + "G" },
446
+ "内存 ",
447
+ h("span", { className: "lc-mem-bar" },
448
+ h("span", {
449
+ className: "lc-mem-fill" + (stats.memPct > 85 ? " hot" : ""),
450
+ style: { width: stats.memPct + "%" },
451
+ })),
452
+ " " + stats.memPct + "%")
453
+ : null,
454
+ parseCpu(stats?.cpu) !== undefined
455
+ ? h("span", { className: "lc-chip", title: "CPU 使用率(采样)" }, "CPU " + parseCpu(stats.cpu) + "%")
456
+ : null,
457
+ stats?.batteryLevel !== undefined
458
+ ? h("span", { className: "lc-chip", title: "电量" },
459
+ "电量 " + stats.batteryLevel + "%" + (BATTERY_STATUS[stats.batteryStatus] !== undefined ? " · " + BATTERY_STATUS[stats.batteryStatus] : ""))
460
+ : null,
461
+ stats === null && serial !== "" ? h("span", { className: "lc-chip" }, "正在采集设备状态…") : null,
462
+ ),
395
463
  h("div", { className: "lc-toolbar" },
396
464
  h("div", { className: "lc-levels", role: "group", "aria-label": "日志级别" },
397
465
  h("button", {
package/lib/index.js CHANGED
@@ -543,6 +543,36 @@ function writeJson(res, status, body) {
543
543
  /** The one WebSocket server for logcat fan-out. */
544
544
  const streamWss = new WebSocketServer({ noServer: true })
545
545
 
546
+ /** One-shot device info + resource stats for the panel (single adb round-trip). */
547
+ async function collectStats(engine, serial) {
548
+ const sh = 'getprop ro.product.model; getprop ro.product.brand; getprop ro.build.version.release; getprop ro.build.version.sdk; getprop ro.product.cpu.abi; wm size; grep -E "MemTotal|MemFree" /proc/meminfo; dumpsys battery | grep -E "level:|status:"; top -b -n 1 | head -3'
549
+ const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', sh], 20000)
550
+ const lines = (result.stdout ?? '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
551
+ const get = (i) => lines[i] ?? ''
552
+ const field = (prefix) => { const l = lines.find((x) => x.startsWith(prefix)); return l ? l.slice(prefix.length).trim() : undefined }
553
+ const kb = (prefix) => { const v = field(prefix); return v ? Number.parseInt(v, 10) || undefined : undefined }
554
+ const totalMemKb = kb('MemTotal') ?? 0
555
+ const freeMemKb = kb('MemFree') ?? 0
556
+ const usedMemKb = totalMemKb > 0 ? totalMemKb - freeMemKb : 0
557
+ return {
558
+ ready: true,
559
+ serial,
560
+ model: get(0) || undefined,
561
+ brand: get(1) || undefined,
562
+ androidVersion: get(2) || undefined,
563
+ sdk: get(3) || undefined,
564
+ abi: get(4) || undefined,
565
+ resolution: (lines.find((l) => l.startsWith('Physical size')) ?? '').replace('Physical size: ', '') || undefined,
566
+ totalMemKb,
567
+ freeMemKb,
568
+ usedMemKb,
569
+ memPct: totalMemKb > 0 ? Math.round((usedMemKb / totalMemKb) * 100) : undefined,
570
+ batteryLevel: kb('level'),
571
+ batteryStatus: field('status'),
572
+ cpu: lines.find((l) => /%cpu|Cpu\(s\)|CPU:/i.test(l)) ?? undefined,
573
+ }
574
+ }
575
+
546
576
  /** Build every /api/dsh-logcat route plus the stream upgrade. */
547
577
  function makeRoutes(engine) {
548
578
  const routes = [
@@ -636,6 +666,26 @@ function makeRoutes(engine) {
636
666
  writeJson(res, 200, { package: pkg })
637
667
  },
638
668
  },
669
+ {
670
+ kind: 'exact',
671
+ path: API_BASE + '/stats',
672
+ handler: async (req, res) => {
673
+ if (!isLoopbackRequest(req)) { writeJson(res, 403, { error: 'forbidden: loopback-only' }); return }
674
+ if ((req.method ?? 'GET') !== 'GET') { writeJson(res, 405, { error: 'method not allowed' }); return }
675
+ if (engine.adb === null) { writeJson(res, 200, { ready: false }); return }
676
+ const url = new URL(req.url ?? '/', 'http://localhost')
677
+ const requested = url.searchParams.get('serial') ?? ''
678
+ const serial = requested !== '' && engine.devices.has(requested)
679
+ ? requested
680
+ : [...engine.devices.keys()].find((s) => engine.devices.get(s).state === 'device') ?? ''
681
+ if (serial === '') { writeJson(res, 200, { ready: false }); return }
682
+ try {
683
+ writeJson(res, 200, await collectStats(engine, serial))
684
+ } catch (error) {
685
+ writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
686
+ }
687
+ },
688
+ },
639
689
  {
640
690
  kind: 'exact',
641
691
  path: API_BASE + '/screenshot',
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.5.0-preview.2",
4
+ "version": "0.5.0-preview.3",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {