lx.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. #!/usr/bin/env node
  2. /**
  3. * lx.js — 布局紧凑渲染器(Layout eXtract)
  4. *
  5. * 把 MCP `device_get_layout` 的 JSON 结果渲染成一行一个控件的紧凑列表,
  6. * 避免把 25KB 的原始 JSON 直接塞进上下文。
  7. *
  8. * 用法:
  9. * node _probe/lx.js <layout.json> [文案过滤词]
  10. * node _probe/lx.js <layout.json> --all # 含 dropped / scrollables 统计
  11. *
  12. * 输入既可以是工具返回的原样 JSON,也可以是 {"content":[{"text":"<json>"}]} 形态的 MCP 响应。
  13. */
  14. 'use strict';
  15. const fs = require('fs');
  16. function readPayload(file) {
  17. const raw = fs.readFileSync(file, 'utf8');
  18. const parsed = JSON.parse(raw);
  19. if (Array.isArray(parsed.content) && parsed.content[0] && parsed.content[0].text) {
  20. return readPayloadValue(JSON.parse(parsed.content[0].text));
  21. }
  22. return readPayloadValue(parsed);
  23. }
  24. /** 工具返回可能把布局挂在 tree 字段下,这里统一成 {nodes, dropped, scrollables} */
  25. function readPayloadValue(value) {
  26. if (value && value.tree && Array.isArray(value.tree.nodes)) {
  27. return { device: value.device, ...value.tree };
  28. }
  29. return value;
  30. }
  31. function shorten(value, max = 60) {
  32. const text = String(value == null ? '' : value).replace(/\s+/g, ' ');
  33. return text.length > max ? `${text.slice(0, max - 1)}…` : text;
  34. }
  35. function renderNode(node) {
  36. const flags = [
  37. node.clk ? 'clk' : '-',
  38. node.sc ? 'scr' : '-',
  39. node.ed ? 'ed' : '-',
  40. node.opaque ? 'shell' : '-',
  41. ].join(' ');
  42. const label = shorten(node.text || node.desc || '');
  43. const parent = node.pt ? ` <-${shorten(node.pt, 18)}` : '';
  44. return [
  45. String(node.ref).padEnd(12),
  46. String(node.cls || '').padEnd(16),
  47. flags,
  48. `(${node.cx},${node.cy})`.padEnd(12),
  49. `[${(node.b || []).join(',')}]`.padEnd(26),
  50. label + parent,
  51. ].join(' ');
  52. }
  53. function main() {
  54. const [file, filter] = process.argv.slice(2);
  55. const layout = readPayload(file);
  56. const nodes = layout.nodes || [];
  57. const shown = filter && filter !== '--all'
  58. ? nodes.filter((n) => (String(n.text || '') + String(n.desc || '')).includes(filter))
  59. : nodes;
  60. console.log(`# 节点 ${nodes.length} → 显示 ${shown.length}${layout.truncated ? ' (truncated)' : ''}`);
  61. shown.forEach((node) => console.log(renderNode(node)));
  62. if (layout.scrollables && layout.scrollables.length) {
  63. console.log('\n# scrollables');
  64. layout.scrollables.forEach((s) => {
  65. console.log(` ${s.ref} dir=${s.dir} [${(s.b || []).join(',')}]`);
  66. });
  67. }
  68. if (layout.dropped && (filter === '--all' || !filter)) {
  69. console.log(`\n# dropped ${JSON.stringify(layout.dropped)}`);
  70. }
  71. }
  72. main();