aiphone-mcp 1.0.0

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.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * UIAutomator XML hierarchy parser.
3
+ * Mirrors the Dart UiAutomatorParser — produces a flat list of UiElements.
4
+ */
5
+ import { XMLParser } from 'fast-xml-parser';
6
+
7
+ const xmlParser = new XMLParser({
8
+ ignoreAttributes: false,
9
+ attributeNamePrefix: '@_',
10
+ parseAttributeValue: false,
11
+ allowBooleanAttributes: true,
12
+ });
13
+
14
+ /**
15
+ * @typedef {Object} UiElement
16
+ * @property {string} id - "el_N"
17
+ * @property {string} text
18
+ * @property {string} contentDesc
19
+ * @property {number[]} bounds - [x1, y1, x2, y2]
20
+ * @property {boolean} clickable
21
+ * @property {boolean} enabled
22
+ * @property {string|null} resourceId
23
+ * @property {string|null} className
24
+ */
25
+
26
+ /**
27
+ * Parses a UIAutomator XML string into a flat array of UiElement objects.
28
+ * @param {string} xmlString
29
+ * @returns {UiElement[]}
30
+ */
31
+ export function parseUiXml(xmlString) {
32
+ if (!xmlString || !xmlString.trim()) return [];
33
+
34
+ let doc;
35
+ try {
36
+ doc = xmlParser.parse(xmlString);
37
+ } catch {
38
+ return [];
39
+ }
40
+
41
+ const elements = [];
42
+ let idCounter = 0;
43
+
44
+ function visit(node) {
45
+ if (typeof node !== 'object' || node === null) return;
46
+
47
+ // Handle array of same-named child nodes
48
+ if (Array.isArray(node)) {
49
+ for (const child of node) visit(child);
50
+ return;
51
+ }
52
+
53
+ // Extract node attributes
54
+ const boundsStr = node['@_bounds'];
55
+ if (boundsStr) {
56
+ const bounds = parseBounds(boundsStr);
57
+ if (bounds) {
58
+ elements.push({
59
+ id: `el_${idCounter++}`,
60
+ text: node['@_text'] ?? '',
61
+ contentDesc: node['@_content-desc'] ?? '',
62
+ bounds,
63
+ clickable: node['@_clickable'] === 'true',
64
+ enabled: node['@_enabled'] !== 'false',
65
+ resourceId: node['@_resource-id'] || null,
66
+ className: node['@_class'] || null,
67
+ });
68
+ }
69
+ }
70
+
71
+ // Recurse into child nodes (any key that doesn't start with @_)
72
+ for (const key of Object.keys(node)) {
73
+ if (key.startsWith('@_')) continue;
74
+ visit(node[key]);
75
+ }
76
+ }
77
+
78
+ // The root element of a UIAutomator dump is <hierarchy>
79
+ const hierarchy = doc['hierarchy'] ?? doc;
80
+ visit(hierarchy);
81
+
82
+ return elements;
83
+ }
84
+
85
+ function parseBounds(raw) {
86
+ const m = raw.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
87
+ if (!m) return null;
88
+ return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10), parseInt(m[4], 10)];
89
+ }
90
+
91
+ /**
92
+ * Serialises elements to a compact JSON-friendly array (mirrors PromptBuilder).
93
+ * @param {UiElement[]} elements
94
+ * @param {number} limit
95
+ * @returns {object[]}
96
+ */
97
+ export function compactElements(elements, limit = 25) {
98
+ const prioritized = [...elements].sort((a, b) => {
99
+ const aScore = (a.clickable ? 2 : 0) + (a.text || a.contentDesc ? 1 : 0);
100
+ const bScore = (b.clickable ? 2 : 0) + (b.text || b.contentDesc ? 1 : 0);
101
+ return bScore - aScore;
102
+ });
103
+
104
+ return prioritized.slice(0, limit).map((e) => ({
105
+ id: e.id,
106
+ text: e.text,
107
+ content_desc: e.contentDesc,
108
+ clickable: e.clickable,
109
+ bounds: e.bounds,
110
+ ...(e.resourceId ? { resource_id: e.resourceId } : {}),
111
+ ...(e.className ? { class: e.className } : {}),
112
+ }));
113
+ }
114
+
115
+ /**
116
+ * Finds the best matching UiElement for a selector object.
117
+ *
118
+ * Selector fields (all optional, evaluated with AND logic):
119
+ * resourceId – exact match against element.resourceId
120
+ * text – case-insensitive substring match against element.text
121
+ * contentDesc – case-insensitive substring match against element.contentDesc
122
+ * className – exact match against element.className
123
+ * clickableOnly – if true, skip non-clickable elements
124
+ *
125
+ * Priority order when multiple elements match: resourceId first, then text, etc.
126
+ *
127
+ * @param {UiElement[]} elements
128
+ * @param {object} selector
129
+ * @returns {UiElement|null}
130
+ */
131
+ export function findElement(elements, selector = {}) {
132
+ const { resourceId, text, contentDesc, className, clickableOnly } = selector;
133
+ for (const el of elements) {
134
+ if (clickableOnly && !el.clickable) continue;
135
+ if (resourceId !== undefined && el.resourceId !== resourceId) continue;
136
+ if (text !== undefined && !el.text.toLowerCase().includes(text.toLowerCase())) continue;
137
+ if (contentDesc !== undefined && !el.contentDesc.toLowerCase().includes(contentDesc.toLowerCase())) continue;
138
+ if (className !== undefined && el.className !== className) continue;
139
+ return el;
140
+ }
141
+ return null;
142
+ }