@thinkingos/vsl-sdk 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1912 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ACTION_TOOL_DESCRIPTION: () => ACTION_TOOL_DESCRIPTION,
24
+ ACTION_TOOL_NAME: () => ACTION_TOOL_NAME,
25
+ ACTION_TOOL_SCHEMA: () => ACTION_TOOL_SCHEMA,
26
+ ARIA_ROLE_TYPE_MAP: () => ARIA_ROLE_TYPE_MAP,
27
+ ActionExecutionError: () => ActionExecutionError,
28
+ AlibabaAdapter: () => AlibabaAdapter,
29
+ AnthropicAdapter: () => AnthropicAdapter,
30
+ LEVEL1_TAG_MAP: () => LEVEL1_TAG_MAP,
31
+ LlmError: () => LlmError,
32
+ LlmValidationError: () => LlmValidationError,
33
+ OpenAIAdapter: () => OpenAIAdapter,
34
+ VALID_ACTIONS: () => VALID_ACTIONS,
35
+ VSL_SDK_VERSION: () => VSL_SDK_VERSION,
36
+ VSL_VERSION: () => VSL_VERSION,
37
+ VslSnapshotSession: () => VslSnapshotSession,
38
+ attachMutationObserver: () => attachMutationObserver,
39
+ buildSystemPrompt: () => buildSystemPrompt,
40
+ buildUserPrompt: () => buildUserPrompt,
41
+ buildVslDocument: () => buildVslDocument,
42
+ collectIds: () => collectIds,
43
+ computeContentHash: () => computeContentHash,
44
+ computeCoordHash: () => computeCoordHash,
45
+ createCacheStore: () => createCacheStore,
46
+ diffVslDocuments: () => diffVslDocuments,
47
+ executeAction: () => executeAction,
48
+ extractDomTree: () => extractDomTree,
49
+ isAriaHidden: () => isAriaHidden,
50
+ isVslDiff: () => isVslDiff,
51
+ ownText: () => ownText,
52
+ resolveAriaRoleType: () => resolveAriaRoleType,
53
+ resolveLevel1Type: () => resolveLevel1Type,
54
+ resolveSt: () => resolveSt,
55
+ resolveTarget: () => resolveTarget,
56
+ retryWithBackoff: () => retryWithBackoff,
57
+ segmentTree: () => segmentTree,
58
+ validateAction: () => validateAction
59
+ });
60
+ module.exports = __toCommonJS(index_exports);
61
+
62
+ // src/capture/domExtractor.ts
63
+ var NON_RENDERABLE_TAGS = /* @__PURE__ */ new Set([
64
+ "script",
65
+ "style",
66
+ "link",
67
+ "meta",
68
+ "noscript",
69
+ "template",
70
+ "head",
71
+ "title",
72
+ "base"
73
+ ]);
74
+ function viewOf(el) {
75
+ return el.ownerDocument.defaultView;
76
+ }
77
+ function isDisplayNone(el) {
78
+ return viewOf(el)?.getComputedStyle(el).display === "none";
79
+ }
80
+ function isVisibilityHidden(el) {
81
+ return viewOf(el)?.getComputedStyle(el).visibility === "hidden";
82
+ }
83
+ var TEXT_NODE_TYPE = 3;
84
+ function ownText(el) {
85
+ let raw = "";
86
+ for (const node of Array.from(el.childNodes)) {
87
+ if (node.nodeType === TEXT_NODE_TYPE) {
88
+ raw += node.textContent ?? "";
89
+ }
90
+ }
91
+ return raw.replace(/\s+/g, " ").trim();
92
+ }
93
+ function extractAttributes(el) {
94
+ const attributes = {};
95
+ for (const attr of Array.from(el.attributes)) {
96
+ attributes[attr.name] = attr.value;
97
+ }
98
+ return attributes;
99
+ }
100
+ var CSS_PROPERTIES = [
101
+ ["cursor", "cursor"],
102
+ ["position", "position"],
103
+ ["top", "top"],
104
+ ["bottom", "bottom"],
105
+ ["display", "display"],
106
+ ["gap", "gap"],
107
+ ["fontWeight", "font-weight"],
108
+ ["fontSize", "font-size"],
109
+ ["opacity", "opacity"],
110
+ ["pointerEvents", "pointer-events"],
111
+ ["overflow", "overflow"],
112
+ ["height", "height"]
113
+ ];
114
+ function captureCss(el) {
115
+ const view = viewOf(el);
116
+ if (view === null) return {};
117
+ const style = view.getComputedStyle(el);
118
+ const css = {};
119
+ for (const [key, property] of CSS_PROPERTIES) {
120
+ const value = style.getPropertyValue(property);
121
+ if (value !== "") css[key] = value;
122
+ }
123
+ return css;
124
+ }
125
+ function toRect(rect) {
126
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
127
+ }
128
+ function hasZeroBox(rect) {
129
+ return rect.width <= 0 || rect.height <= 0;
130
+ }
131
+ function collectVisibleChildren(parent, parentPath) {
132
+ const result = [];
133
+ Array.from(parent.children).forEach((child, index) => {
134
+ const path = [...parentPath, index];
135
+ const tag = child.tagName.toLowerCase();
136
+ if (NON_RENDERABLE_TAGS.has(tag) || isDisplayNone(child)) {
137
+ return;
138
+ }
139
+ const rect = toRect(child.getBoundingClientRect());
140
+ if (isVisibilityHidden(child) || hasZeroBox(rect)) {
141
+ result.push(...collectVisibleChildren(child, path));
142
+ return;
143
+ }
144
+ result.push({
145
+ tag,
146
+ indexPath: path,
147
+ rect,
148
+ text: ownText(child) || null,
149
+ attributes: extractAttributes(child),
150
+ css: captureCss(child),
151
+ children: collectVisibleChildren(child, path)
152
+ });
153
+ });
154
+ return result;
155
+ }
156
+ function defaultRoot() {
157
+ const body = document.body;
158
+ if (!body) {
159
+ throw new Error("extractDomTree: document.body is null \u2014 call after the DOM is loaded");
160
+ }
161
+ return body;
162
+ }
163
+ function extractDomTree(root = defaultRoot()) {
164
+ return collectVisibleChildren(root, []);
165
+ }
166
+
167
+ // src/segmentation/level1.ts
168
+ var LEVEL1_TAG_MAP = {
169
+ button: "button",
170
+ input: "input",
171
+ a: "link",
172
+ nav: "nav",
173
+ header: "header",
174
+ main: "main",
175
+ section: "container",
176
+ img: "image",
177
+ select: "select",
178
+ textarea: "textarea"
179
+ };
180
+ function resolveLevel1Type(tag) {
181
+ return LEVEL1_TAG_MAP[tag.toLowerCase()] ?? null;
182
+ }
183
+
184
+ // src/segmentation/level2.ts
185
+ var ARIA_ROLE_TYPE_MAP = {
186
+ button: "button",
187
+ dialog: "modal",
188
+ tab: "tab",
189
+ tabpanel: "container"
190
+ };
191
+ function resolveAriaRoleType(role) {
192
+ if (!role) return null;
193
+ return ARIA_ROLE_TYPE_MAP[role.toLowerCase()] ?? null;
194
+ }
195
+ var TRUE = "true";
196
+ function resolveSt(attributes) {
197
+ if (attributes["aria-pressed"] === TRUE) return "checked";
198
+ if (attributes["aria-expanded"] === TRUE) return "expanded";
199
+ if (attributes["aria-disabled"] === TRUE) return "disabled";
200
+ return null;
201
+ }
202
+
203
+ // src/segmentation/level3.ts
204
+ function px(value) {
205
+ return value === void 0 ? Number.NaN : Number.parseFloat(value);
206
+ }
207
+ function resolveLevel3Type(css, attributes) {
208
+ if (css === void 0) return null;
209
+ if (css.cursor === "pointer" && attributes["onclick"] !== void 0) return "button";
210
+ if (css.display === "flex" && px(css.gap) > 0) return "container";
211
+ const isBold = css.fontWeight === "bold" || px(css.fontWeight) >= 700;
212
+ if (isBold && px(css.fontSize) > 20) return "heading";
213
+ if (css.position === "fixed" && px(css.top) === 0) return "header";
214
+ if (css.position === "fixed" && px(css.bottom) === 0) return "footer";
215
+ if (css.overflow === "hidden" && px(css.height) > 200) return "scrollable_container";
216
+ return null;
217
+ }
218
+ function isPointerInvisible(css) {
219
+ if (css === void 0) return false;
220
+ return px(css.opacity) === 0 && css.pointerEvents === "none";
221
+ }
222
+
223
+ // src/segmentation/level4.ts
224
+ var EPS = 2;
225
+ var bottomOf = (r) => r.y + r.height;
226
+ var rightOf = (r) => r.x + r.width;
227
+ function sameRow(a, b) {
228
+ const overlap = Math.min(bottomOf(a), bottomOf(b)) - Math.max(a.y, b.y);
229
+ return overlap > 0.5 * Math.min(a.height, b.height);
230
+ }
231
+ function sameColumn(a, b) {
232
+ const overlap = Math.min(rightOf(a), rightOf(b)) - Math.max(a.x, b.x);
233
+ return overlap > 0.5 * Math.min(a.width, b.width);
234
+ }
235
+ var eq = (a, b) => Math.abs(a - b) <= EPS;
236
+ var sameHeight = (a, b) => eq(a.height, b.height);
237
+ var sameWidth = (a, b) => eq(a.width, b.width);
238
+ function isToolbarRow(children) {
239
+ const buttons = children.filter((c) => c.t === "button");
240
+ const first = buttons[0];
241
+ if (buttons.length < 3 || first === void 0) return false;
242
+ return buttons.slice(1).every((b) => sameRow(first.rect, b.rect) && sameHeight(first.rect, b.rect));
243
+ }
244
+ function verticallyStacked(children) {
245
+ const sorted = [...children].sort((a, b) => a.rect.y - b.rect.y);
246
+ for (let i = 1; i < sorted.length; i += 1) {
247
+ if (sorted[i].rect.y < bottomOf(sorted[i - 1].rect) - EPS) return false;
248
+ }
249
+ return true;
250
+ }
251
+ function isVerticalList(children) {
252
+ const first = children[0];
253
+ if (children.length < 3 || first === void 0) return false;
254
+ return children.slice(1).every((c) => sameWidth(first.rect, c.rect) && sameColumn(first.rect, c.rect)) && verticallyStacked(children);
255
+ }
256
+ function bandCount(values, eps) {
257
+ const sorted = [...values].sort((a, b) => a - b);
258
+ let count = 1;
259
+ let anchor = sorted[0];
260
+ for (const value of sorted.slice(1)) {
261
+ if (value - anchor > eps) {
262
+ count += 1;
263
+ anchor = value;
264
+ }
265
+ }
266
+ return count;
267
+ }
268
+ function isGrid(children) {
269
+ const first = children[0];
270
+ if (children.length < 4 || first === void 0) return false;
271
+ const sameSize = children.slice(1).every((c) => sameWidth(first.rect, c.rect) && sameHeight(first.rect, c.rect));
272
+ if (!sameSize) return false;
273
+ return bandCount(children.map((c) => c.rect.y), first.rect.height + EPS) >= 2 && bandCount(children.map((c) => c.rect.x), first.rect.width + EPS) >= 2;
274
+ }
275
+ function adjacent(a, b) {
276
+ const gapX = Math.max(a.x, b.x) - Math.min(rightOf(a), rightOf(b));
277
+ const gapY = Math.max(a.y, b.y) - Math.min(bottomOf(a), bottomOf(b));
278
+ return sameRow(a, b) && gapX <= 8 * EPS || sameColumn(a, b) && gapY <= 8 * EPS;
279
+ }
280
+ function hasLabelNextToInput(children) {
281
+ return children.some((input) => {
282
+ if (input.t !== "input") return false;
283
+ return children.some((label) => {
284
+ if (label === input) return false;
285
+ if ((label.txt ?? "").length === 0) return false;
286
+ return adjacent(label.rect, input.rect);
287
+ });
288
+ });
289
+ }
290
+ function isTabBarRow(children) {
291
+ const tabs = children.filter((c) => c.attributes?.["role"] === "tab");
292
+ const first = tabs[0];
293
+ if (tabs.length < 2 || first === void 0) return false;
294
+ return tabs.slice(1).every((t) => sameRow(first.rect, t.rect));
295
+ }
296
+ function isSidebarSplit(children, parent) {
297
+ if (children.length !== 2) return false;
298
+ const sorted = [...children].sort((a, b) => a.rect.x - b.rect.x);
299
+ const left = sorted[0];
300
+ const right = sorted[1];
301
+ if (left === void 0 || right === void 0) return false;
302
+ return left.rect.width < parent.width / 3 && right.rect.width > parent.width / 2 && rightOf(left.rect) <= right.rect.x + EPS && sameRow(left.rect, right.rect);
303
+ }
304
+ function resolveLevel4Type(el) {
305
+ const children = el.ch ?? [];
306
+ if (children.length === 0) return null;
307
+ if (isToolbarRow(children)) return "toolbar";
308
+ if (isVerticalList(children)) return "list";
309
+ if (isGrid(children)) return "grid";
310
+ if (hasLabelNextToInput(children)) return "form_field";
311
+ if (isTabBarRow(children)) return "tab_bar";
312
+ if (isSidebarSplit(children, el.rect)) return "layout";
313
+ return null;
314
+ }
315
+
316
+ // src/segmentation/segmenter.ts
317
+ function isAriaHidden(attributes) {
318
+ return attributes["aria-hidden"] === "true";
319
+ }
320
+ function segmentOne(el) {
321
+ const { attributes } = el;
322
+ return {
323
+ tag: el.tag,
324
+ indexPath: el.indexPath,
325
+ rect: el.rect,
326
+ attributes,
327
+ t: resolveLevel1Type(el.tag) ?? resolveAriaRoleType(attributes["role"]) ?? resolveLevel3Type(el.css, attributes),
328
+ txt: attributes["aria-label"] ?? el.text,
329
+ st: resolveSt(attributes),
330
+ ch: []
331
+ };
332
+ }
333
+ function segmentTree(elements) {
334
+ const result = [];
335
+ for (const el of elements) {
336
+ if (isAriaHidden(el.attributes)) continue;
337
+ if (isPointerInvisible(el.css)) continue;
338
+ const segmented = segmentOne(el);
339
+ segmented.ch = segmentTree(el.children);
340
+ if (segmented.t === null) {
341
+ segmented.t = resolveLevel4Type(segmented);
342
+ }
343
+ result.push(segmented);
344
+ }
345
+ return result;
346
+ }
347
+
348
+ // src/types/vsl.ts
349
+ var VSL_VERSION = "1.0.0";
350
+
351
+ // src/builder/vslBuilder.ts
352
+ var LAZY_TEXT_THRESHOLD = 200;
353
+ var LAZY_TEXT_PREVIEW_LENGTH = 50;
354
+ var INTERACTIVE_ATTRS = [
355
+ "onclick",
356
+ "onmousedown",
357
+ "ontouchstart",
358
+ "tabindex",
359
+ "contenteditable"
360
+ ];
361
+ function relative(value, dimension) {
362
+ if (dimension <= 0) return 0;
363
+ return Math.round(value / dimension * 1e4) / 1e4;
364
+ }
365
+ function defaultActions(t, st, attributes) {
366
+ if (st === "disabled") return void 0;
367
+ switch (t) {
368
+ case "button":
369
+ case "link":
370
+ case "tab":
371
+ return ["click"];
372
+ case "modal":
373
+ return ["close"];
374
+ case "input": {
375
+ const type = attributes["type"];
376
+ return type === "checkbox" || type === "radio" ? ["click", "check", "uncheck"] : ["click", "type", "clear"];
377
+ }
378
+ case "select":
379
+ return ["click", "select"];
380
+ case "textarea":
381
+ return ["click", "type", "clear"];
382
+ default:
383
+ return void 0;
384
+ }
385
+ }
386
+ function baseSemanticScore(el) {
387
+ let score = 0;
388
+ if (el.attributes["role"] !== void 0) score += 3;
389
+ if (el.attributes["aria-label"] !== void 0) score += 2;
390
+ if (INTERACTIVE_ATTRS.some((attr) => el.attributes[attr] !== void 0)) score += 2;
391
+ if (el.txt) score += 1;
392
+ return score;
393
+ }
394
+ function toVslObject(el, viewport, includedChildren, textBlocks, counter) {
395
+ const t = el.t ?? "container";
396
+ const object = {
397
+ id: `${el.tag}_${el.indexPath.join("_")}`,
398
+ t,
399
+ p: [relative(el.rect.x, viewport.width), relative(el.rect.y, viewport.height)],
400
+ s: [Math.round(el.rect.width), Math.round(el.rect.height)]
401
+ };
402
+ const role = el.attributes["role"];
403
+ if (role !== void 0) object.r = role;
404
+ if (el.st) object.st = el.st;
405
+ if (el.txt) {
406
+ if (el.txt.length > LAZY_TEXT_THRESHOLD) {
407
+ object.txt_preview = el.txt.slice(0, LAZY_TEXT_PREVIEW_LENGTH) + "\u2026";
408
+ const refId = `tb_${String(counter.value++).padStart(3, "0")}`;
409
+ object.txt_ref = refId;
410
+ textBlocks.set(refId, el.txt);
411
+ } else {
412
+ object.txt = el.txt;
413
+ }
414
+ }
415
+ const act = defaultActions(t, el.st, el.attributes);
416
+ if (act !== void 0) object.act = act;
417
+ if (includedChildren.length > 0) object.ch = includedChildren;
418
+ if (el.vf !== void 0) object.vf = el.vf;
419
+ if (el.vf_meta !== void 0) object.vf_meta = el.vf_meta;
420
+ return object;
421
+ }
422
+ function convert(el, viewport, textBlocks, counter) {
423
+ const includedChildren = el.ch.map((child) => convert(child, viewport, textBlocks, counter)).filter((obj) => obj !== null);
424
+ if (el.t !== null) return toVslObject(el, viewport, includedChildren, textBlocks, counter);
425
+ const score = baseSemanticScore(el) + (includedChildren.length > 0 ? 1 : 0);
426
+ if (score >= 3 || includedChildren.length > 0) {
427
+ return toVslObject(el, viewport, includedChildren, textBlocks, counter);
428
+ }
429
+ return null;
430
+ }
431
+ function isIncludedInVsl(el) {
432
+ if (el.t !== null) return true;
433
+ const includedChildren = el.ch.filter(isIncludedInVsl).length;
434
+ const score = baseSemanticScore(el) + (includedChildren > 0 ? 1 : 0);
435
+ return score >= 3 || includedChildren > 0;
436
+ }
437
+ function buildCanvas(options, viewport) {
438
+ const url = options.url ?? window.location.href;
439
+ const title = options.title ?? document.title;
440
+ return {
441
+ viewport: { ...viewport, unit: "px" },
442
+ background: options.background ?? "#ffffff",
443
+ scale: 1,
444
+ orientation: viewport.width >= viewport.height ? "landscape" : "portrait",
445
+ timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
446
+ ...url ? { url } : {},
447
+ ...title ? { title } : {}
448
+ };
449
+ }
450
+ function collectVisualFragments(objects) {
451
+ const map = {};
452
+ const visit = (list) => {
453
+ for (const object of list) {
454
+ if (object.vf !== void 0 && object.vf_meta !== void 0) {
455
+ map[object.vf] = {
456
+ type: object.vf_meta.type,
457
+ format: object.vf_meta.format ?? "webp",
458
+ size: object.vf_meta.size ?? [0, 0],
459
+ hash: object.vf_meta.hash,
460
+ cached_at: object.vf_meta.cached_at
461
+ };
462
+ }
463
+ visit(object.ch ?? []);
464
+ }
465
+ };
466
+ visit(objects);
467
+ return Object.keys(map).length > 0 ? map : void 0;
468
+ }
469
+ function buildVslDocument(elements, options = {}) {
470
+ const viewport = {
471
+ width: options.viewport?.width ?? window.innerWidth,
472
+ height: options.viewport?.height ?? window.innerHeight
473
+ };
474
+ const textBlocks = /* @__PURE__ */ new Map();
475
+ const counter = { value: 0 };
476
+ const objects = elements.map((el) => convert(el, viewport, textBlocks, counter)).filter((obj) => obj !== null);
477
+ const vslDocument = {
478
+ vsl_version: VSL_VERSION,
479
+ canvas: buildCanvas(options, viewport),
480
+ objects
481
+ };
482
+ const visualFragments = collectVisualFragments(objects);
483
+ if (visualFragments !== void 0) vslDocument.visual_fragments = visualFragments;
484
+ if (textBlocks.size > 0) vslDocument.text_blocks = Object.fromEntries(textBlocks);
485
+ return vslDocument;
486
+ }
487
+
488
+ // src/cache/cacheStore.ts
489
+ var import_node_crypto = require("crypto");
490
+ function sha256(payload) {
491
+ return (0, import_node_crypto.createHash)("sha256").update(payload).digest("hex");
492
+ }
493
+ function computeContentHash(object) {
494
+ const payload = { t: object.t };
495
+ if (object.r !== void 0) payload.r = object.r;
496
+ if (object.st !== void 0) payload.st = object.st;
497
+ if (object.txt !== void 0) payload.txt = object.txt;
498
+ if (object.act !== void 0) payload.act = object.act;
499
+ return sha256(JSON.stringify(payload));
500
+ }
501
+ function computeCoordHash(object) {
502
+ return sha256(JSON.stringify({ p: object.p, s: object.s }));
503
+ }
504
+ function createCacheStore() {
505
+ const entries = /* @__PURE__ */ new Map();
506
+ return {
507
+ set(id, object) {
508
+ entries.set(id, {
509
+ object,
510
+ contentHash: computeContentHash(object),
511
+ coordHash: computeCoordHash(object)
512
+ });
513
+ },
514
+ get(id) {
515
+ return entries.get(id);
516
+ },
517
+ has(id) {
518
+ return entries.has(id);
519
+ },
520
+ invalidate(id) {
521
+ entries.delete(id);
522
+ },
523
+ invalidateBySelector(selector) {
524
+ if (selector === "*") {
525
+ entries.clear();
526
+ return;
527
+ }
528
+ if (entries.has(selector)) {
529
+ entries.delete(selector);
530
+ return;
531
+ }
532
+ const prefix = `${selector}_`;
533
+ for (const id of entries.keys()) {
534
+ if (id.startsWith(prefix)) entries.delete(id);
535
+ }
536
+ },
537
+ invalidateCoordinates() {
538
+ for (const [id, entry] of entries) {
539
+ entries.set(id, { ...entry, coordHash: "" });
540
+ }
541
+ },
542
+ clear() {
543
+ entries.clear();
544
+ },
545
+ keys() {
546
+ return [...entries.keys()];
547
+ },
548
+ get size() {
549
+ return entries.size;
550
+ }
551
+ };
552
+ }
553
+
554
+ // src/cache/invalidation.ts
555
+ function elementId(el, root) {
556
+ const indexPath = [];
557
+ let current = el;
558
+ while (current !== null && current !== root) {
559
+ const parent = current.parentElement;
560
+ if (parent === null) return null;
561
+ indexPath.unshift(Array.from(parent.children).indexOf(current));
562
+ current = parent;
563
+ }
564
+ if (current !== root || indexPath.length === 0) return null;
565
+ return `${el.tagName.toLowerCase()}_${indexPath.join("_")}`;
566
+ }
567
+ function invalidateMutationTarget(node, root, store) {
568
+ const start = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
569
+ if (start === null || start === root || !root.contains(start)) return;
570
+ let current = start;
571
+ while (current !== null && current !== root) {
572
+ const id = elementId(current, root);
573
+ if (id !== null && store.has(id)) {
574
+ store.invalidate(id);
575
+ return;
576
+ }
577
+ current = current.parentElement;
578
+ }
579
+ }
580
+ function attachMutationObserver(root, store) {
581
+ const observer = new MutationObserver((records) => {
582
+ for (const record of records) {
583
+ invalidateMutationTarget(record.target, root, store);
584
+ }
585
+ });
586
+ observer.observe(root, {
587
+ childList: true,
588
+ attributes: true,
589
+ characterData: true,
590
+ subtree: true
591
+ });
592
+ return { observer, disconnect: () => observer.disconnect() };
593
+ }
594
+
595
+ // src/diff/diffEngine.ts
596
+ function flatten(objects, map) {
597
+ for (const object of objects) {
598
+ map.set(object.id, object);
599
+ flatten(object.ch ?? [], map);
600
+ }
601
+ }
602
+ function objectsEqual(a, b) {
603
+ return computeContentHash(a) === computeContentHash(b) && computeCoordHash(a) === computeCoordHash(b);
604
+ }
605
+ function tuple2Equal(a, b) {
606
+ return a[0] === b[0] && a[1] === b[1];
607
+ }
608
+ function stringArrayEqual(a, b) {
609
+ if (a === void 0 || b === void 0) return a === b;
610
+ return a.length === b.length && a.every((value, i) => value === b[i]);
611
+ }
612
+ function buildModified(prev, next) {
613
+ const result = { id: next.id };
614
+ if (prev.t !== next.t) result.t = next.t;
615
+ if (prev.r !== next.r) result.r = next.r ?? null;
616
+ if (!tuple2Equal(prev.p, next.p)) result.p = next.p;
617
+ if (!tuple2Equal(prev.s, next.s)) result.s = next.s;
618
+ if (prev.st !== next.st) result.st = next.st ?? null;
619
+ if (prev.txt !== next.txt) result.txt = next.txt ?? null;
620
+ if (!stringArrayEqual(prev.act, next.act)) result.act = next.act ?? null;
621
+ return result;
622
+ }
623
+ function diffVslDocuments(prev, next, options) {
624
+ const prevFlat = /* @__PURE__ */ new Map();
625
+ const nextFlat = /* @__PURE__ */ new Map();
626
+ flatten(prev.objects, prevFlat);
627
+ flatten(next.objects, nextFlat);
628
+ const added = [];
629
+ const modified = [];
630
+ const unchangedRefs = [];
631
+ const walk = (objects, insideAdded) => {
632
+ for (const nextObject of objects) {
633
+ if (insideAdded) {
634
+ walk(nextObject.ch ?? [], true);
635
+ continue;
636
+ }
637
+ const prevObject = prevFlat.get(nextObject.id);
638
+ if (prevObject === void 0) {
639
+ added.push(nextObject);
640
+ walk(nextObject.ch ?? [], true);
641
+ } else if (objectsEqual(prevObject, nextObject)) {
642
+ unchangedRefs.push(nextObject.id);
643
+ walk(nextObject.ch ?? [], false);
644
+ } else {
645
+ modified.push(buildModified(prevObject, nextObject));
646
+ walk(nextObject.ch ?? [], false);
647
+ }
648
+ }
649
+ };
650
+ walk(next.objects, false);
651
+ const removed = [];
652
+ for (const id of prevFlat.keys()) {
653
+ if (!nextFlat.has(id)) removed.push({ id });
654
+ }
655
+ return {
656
+ diff_version: options.diffVersion,
657
+ base_version: options.baseVersion,
658
+ timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
659
+ changes: { added, modified, removed, unchanged_refs: unchangedRefs }
660
+ };
661
+ }
662
+
663
+ // src/vision/enrichWithVision.ts
664
+ function collectCandidates(elements) {
665
+ const candidates = [];
666
+ const visit = (element) => {
667
+ if (element.t === null && !isIncludedInVsl(element)) candidates.push(element);
668
+ for (const child of element.ch) {
669
+ visit(child);
670
+ }
671
+ };
672
+ for (const element of elements) visit(element);
673
+ return candidates;
674
+ }
675
+ function buildHint(element) {
676
+ return element.txt === null ? `tag=${element.tag}` : `tag=${element.tag}; text: ${element.txt}`;
677
+ }
678
+ async function enrichWithVision(elements, deps) {
679
+ const fragments = /* @__PURE__ */ new Map();
680
+ let enrichedCount = 0;
681
+ let failedCount = 0;
682
+ for (const element of collectCandidates(elements)) {
683
+ let extraction;
684
+ try {
685
+ extraction = await deps.extractor.extract(element.rect);
686
+ } catch {
687
+ failedCount += 1;
688
+ continue;
689
+ }
690
+ if (extraction === null) continue;
691
+ const { entry } = extraction;
692
+ let classification = entry.classification;
693
+ try {
694
+ if (classification === void 0) {
695
+ classification = await deps.classifier.classify(entry.data, buildHint(element));
696
+ entry.classification = classification;
697
+ }
698
+ } catch {
699
+ failedCount += 1;
700
+ continue;
701
+ }
702
+ element.t = classification.type;
703
+ element.vf = entry.vfId;
704
+ element.vf_meta = {
705
+ type: classification.type,
706
+ format: entry.meta.format,
707
+ size: entry.meta.size,
708
+ hash: entry.meta.hash,
709
+ cached_at: entry.meta.cached_at
710
+ };
711
+ fragments.set(entry.vfId, entry.data);
712
+ enrichedCount += 1;
713
+ }
714
+ return { enrichedCount, failedCount, fragments };
715
+ }
716
+
717
+ // src/session/snapshotSession.ts
718
+ function isVslDiff(result) {
719
+ return "changes" in result;
720
+ }
721
+ function flattenObjects(objects, map) {
722
+ for (const object of objects) {
723
+ map.set(object.id, object);
724
+ flattenObjects(object.ch ?? [], map);
725
+ }
726
+ }
727
+ function storeTree(store, objects) {
728
+ for (const object of objects) {
729
+ store.set(object.id, object);
730
+ storeTree(store, object.ch ?? []);
731
+ }
732
+ }
733
+ var VslSnapshotSession = class {
734
+ store;
735
+ lastUrl = null;
736
+ lastViewport = null;
737
+ lastDocument = null;
738
+ version = 0;
739
+ constructor(store = createCacheStore()) {
740
+ this.store = store;
741
+ }
742
+ /**
743
+ * Строит VSL из DOM поддерева root (extractDomTree → segmentTree →
744
+ * buildVslDocument) и возвращает полный документ либо дифф (см. шапку).
745
+ */
746
+ snapshot(root, input) {
747
+ const elements = segmentTree(extractDomTree(root));
748
+ return this.commit(elements, this.buildOptions(root, input), input);
749
+ }
750
+ /**
751
+ * Vision-вариант конвейера (T1.5.5): segmentTree → enrichWithVision (Level 5
752
+ * fallback — аннотация t + vf/vf_meta на элементах ДО сборки) → buildVslDocument
753
+ * → cache/diff — та же логика состояния, что snapshot(). Синхронный snapshot()
754
+ * сохранён (существующие контракты/тесты не ломаются); данные фрагментов
755
+ * (base64) возвращаются ОТДЕЛЬНО — в документе только метаданные (DEC-015).
756
+ */
757
+ async snapshotWithVision(root, input, vision) {
758
+ const elements = segmentTree(extractDomTree(root));
759
+ const visionResult = await enrichWithVision(elements, vision);
760
+ const snapshot = this.commit(elements, this.buildOptions(root, input), input);
761
+ return { snapshot, fragments: visionResult.fragments };
762
+ }
763
+ /** BuildOptions из input + портативный title (логика бывшего snapshot()). */
764
+ buildOptions(root, input) {
765
+ const options = {
766
+ viewport: { width: input.viewport.width, height: input.viewport.height },
767
+ url: input.url
768
+ };
769
+ options.title = input.title ?? root.ownerDocument.title;
770
+ if (input.timestamp !== void 0) options.timestamp = input.timestamp;
771
+ if (input.background !== void 0) options.background = input.background;
772
+ return options;
773
+ }
774
+ /** Сборка документа + кэш/дифф (логика бывшего snapshot(), см. шапку модуля). */
775
+ commit(elements, options, input) {
776
+ const nextDocument = buildVslDocument(elements, options);
777
+ const firstCall = this.lastDocument === null;
778
+ const urlChanged = !firstCall && input.url !== this.lastUrl;
779
+ if (firstCall || urlChanged) {
780
+ this.store.clear();
781
+ storeTree(this.store, nextDocument.objects);
782
+ this.version = 1;
783
+ this.lastUrl = input.url;
784
+ this.lastViewport = { ...input.viewport };
785
+ this.lastDocument = nextDocument;
786
+ return nextDocument;
787
+ }
788
+ const viewportChanged = this.lastViewport === null || input.viewport.width !== this.lastViewport.width || input.viewport.height !== this.lastViewport.height;
789
+ if (viewportChanged) this.store.invalidateCoordinates();
790
+ const diffOptions = {
791
+ diffVersion: this.version + 1,
792
+ baseVersion: this.version
793
+ };
794
+ if (input.timestamp !== void 0) diffOptions.timestamp = input.timestamp;
795
+ const diff = diffVslDocuments(this.lastDocument, nextDocument, diffOptions);
796
+ const fresh = /* @__PURE__ */ new Map();
797
+ flattenObjects(nextDocument.objects, fresh);
798
+ for (const removed of diff.changes.removed) this.store.invalidate(removed.id);
799
+ for (const modified of diff.changes.modified) {
800
+ const object = fresh.get(modified.id);
801
+ if (object !== void 0) this.store.set(object.id, object);
802
+ }
803
+ for (const added of diff.changes.added) storeTree(this.store, [added]);
804
+ this.version = diff.diff_version;
805
+ this.lastUrl = input.url;
806
+ this.lastViewport = { ...input.viewport };
807
+ this.lastDocument = nextDocument;
808
+ return diff;
809
+ }
810
+ };
811
+
812
+ // src/llm/types.ts
813
+ var LlmError = class extends Error {
814
+ /** HTTP-статус, если ошибка пришла от API (для классификации retry). */
815
+ status;
816
+ constructor(message, status) {
817
+ super(message);
818
+ this.name = "LlmError";
819
+ this.status = status;
820
+ }
821
+ };
822
+ var LlmValidationError = class extends LlmError {
823
+ constructor(message) {
824
+ super(message);
825
+ this.name = "LlmValidationError";
826
+ }
827
+ };
828
+
829
+ // src/llm/actions.ts
830
+ var VALID_ACTIONS = [
831
+ // §7.1 Базовые
832
+ "click",
833
+ "type",
834
+ "clear",
835
+ "scroll",
836
+ "hover",
837
+ "focus",
838
+ "blur",
839
+ "select",
840
+ "check",
841
+ "uncheck",
842
+ // §7.2 Расширенные
843
+ "drag",
844
+ "drop",
845
+ "submit",
846
+ "reset",
847
+ "open",
848
+ "close",
849
+ "expand",
850
+ "collapse",
851
+ "wait",
852
+ // §7.3 Навигационные
853
+ "navigate",
854
+ "go_back",
855
+ "go_forward",
856
+ "refresh",
857
+ // §7.4 Загрузка файлов
858
+ "download"
859
+ ];
860
+ var TARGET_ACTIONS = /* @__PURE__ */ new Set([
861
+ "click",
862
+ "type",
863
+ "clear",
864
+ "hover",
865
+ "focus",
866
+ "blur",
867
+ "select",
868
+ "check",
869
+ "uncheck",
870
+ "drag",
871
+ "drop",
872
+ "submit",
873
+ "reset",
874
+ "open",
875
+ "close",
876
+ "expand",
877
+ "collapse",
878
+ "download"
879
+ ]);
880
+ var collectFromObjects = (objects, ids) => {
881
+ for (const object of objects) {
882
+ ids.add(object.id);
883
+ collectFromObjects(object.ch ?? [], ids);
884
+ }
885
+ };
886
+ function collectIds(vslJson) {
887
+ const ids = /* @__PURE__ */ new Set();
888
+ if (isVslDiff(vslJson)) {
889
+ collectFromObjects(vslJson.changes.added, ids);
890
+ for (const modified of vslJson.changes.modified) ids.add(modified.id);
891
+ for (const ref of vslJson.changes.unchanged_refs) ids.add(ref);
892
+ } else {
893
+ collectFromObjects(vslJson.objects, ids);
894
+ }
895
+ return ids;
896
+ }
897
+ function validateAction(raw, validIds) {
898
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
899
+ throw new LlmValidationError("Model response: action must be a JSON object");
900
+ }
901
+ const record = raw;
902
+ const action = record.action;
903
+ if (typeof action !== "string" || !VALID_ACTIONS.includes(action)) {
904
+ const shown = typeof action === "string" ? action : JSON.stringify(action) ?? "undefined";
905
+ throw new LlmValidationError(`Unknown action: ${shown}`);
906
+ }
907
+ const result = { action };
908
+ if (TARGET_ACTIONS.has(action)) {
909
+ const targetId = record.target_id;
910
+ if (typeof targetId !== "string") {
911
+ throw new LlmValidationError(`Action "${action}" requires a string target_id`);
912
+ }
913
+ if (!validIds.has(targetId)) {
914
+ throw new LlmValidationError(`target_id "${targetId}" not found in VSL JSON`);
915
+ }
916
+ result.target_id = targetId;
917
+ }
918
+ const value = record.value;
919
+ if (typeof value === "string" || value === null) result.value = value;
920
+ const reasoning = record.reasoning;
921
+ if (typeof reasoning === "string") result.reasoning = reasoning;
922
+ return result;
923
+ }
924
+
925
+ // src/llm/prompt.ts
926
+ var FEW_SHOT_EXAMPLES = [
927
+ {
928
+ title: "Example 1 \u2014 click a button (full document)",
929
+ vsl: '{"vsl_version":"1.0.0","objects":[{"id":"header_0","t":"header","ch":[{"id":"link_0_0","t":"link","txt":"\u0412\u043E\u0439\u0442\u0438","act":["click"]},{"id":"link_0_1","t":"link","txt":"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F","act":["click"]}]}]}',
930
+ goal: "\u0412\u043E\u0439\u0442\u0438 \u0432 \u0430\u043A\u043A\u0430\u0443\u043D\u0442",
931
+ action: '{"action":"click","target_id":"link_0_0","value":null,"reasoning":"\u0421\u0441\u044B\u043B\u043A\u0430 \xAB\u0412\u043E\u0439\u0442\u0438\xBB \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u0444\u043E\u0440\u043C\u0443 \u0432\u0445\u043E\u0434\u0430"}'
932
+ },
933
+ {
934
+ title: "Example 2 \u2014 fill a form (diff input)",
935
+ vsl: '{"diff_version":2,"base_version":1,"changes":{"added":[{"id":"form_1_0","t":"input","txt":"\u041F\u043E\u0438\u0441\u043A","act":["click","type"]}],"modified":[],"removed":[],"unchanged_refs":["button_1_1"]}}',
936
+ goal: "\u041D\u0430\u0439\u0442\u0438 \xABvsl json\xBB",
937
+ action: '{"action":"type","target_id":"form_1_0","value":"vsl json","reasoning":"\u0412\u0432\u043E\u0436\u0443 \u0437\u0430\u043F\u0440\u043E\u0441 \u0432 \u043D\u043E\u0432\u043E\u0435 \u043F\u043E\u043B\u0435 \u043F\u043E\u0438\u0441\u043A\u0430 \u0438\u0437 \u0434\u0438\u0444\u0444\u0430; \u0437\u0430\u0442\u0435\u043C \u043D\u0430\u0436\u043C\u0443 button_1_1 \u0438\u0437 unchanged_refs"}'
938
+ },
939
+ {
940
+ title: "Example 3 \u2014 navigation",
941
+ vsl: '{"vsl_version":"1.0.0","objects":[{"id":"main_0","t":"main","ch":[{"id":"header_0_0","t":"header","txt":"\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438"}]}]}',
942
+ goal: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043A \u043F\u043E \u043F\u0440\u044F\u043C\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435",
943
+ action: '{"action":"navigate","value":"https://example.com/settings","reasoning":"\u0426\u0435\u043B\u0435\u0432\u043E\u0439 URL \u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D \u2014 \u043F\u0440\u044F\u043C\u0430\u044F \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F \u0431\u044B\u0441\u0442\u0440\u0435\u0435"}'
944
+ }
945
+ ];
946
+ var PARAMETER_CONVENTIONS = [
947
+ "- click, hover, focus, blur, clear, check, uncheck, open, close, expand, collapse \u2014 target_id;",
948
+ "- type \u2014 target_id + value (text to type);",
949
+ "- select \u2014 target_id + value (option);",
950
+ '- scroll \u2014 value: "up" | "down" (optional amount: "down:300");',
951
+ "- drag \u2014 target_id: source element, value: destination element id;",
952
+ "- submit, reset \u2014 target_id: form id;",
953
+ '- wait \u2014 value: condition (e.g. a selector or "idle");',
954
+ "- navigate \u2014 value: URL;",
955
+ "- download \u2014 target_id (element to click) OR value (URL to download directly);",
956
+ "- go_back, go_forward, refresh \u2014 no parameters."
957
+ ].join("\n");
958
+ function buildSystemPrompt() {
959
+ const actions = VALID_ACTIONS.join(" | ");
960
+ const examples = FEW_SHOT_EXAMPLES.map(
961
+ (example) => `${example.title}
962
+ VSL JSON: ${example.vsl}
963
+ User goal: ${example.goal}
964
+ Action: ${example.action}`
965
+ ).join("\n\n");
966
+ return [
967
+ "You are a screen understanding agent. You receive a VSL JSON representation of the current screen. Analyze the structure and decide what action to take to accomplish the user's goal.",
968
+ "",
969
+ "## VSL JSON format",
970
+ "",
971
+ 'Elements form a tree via "ch" (children). Element keys:',
972
+ '- "id" \u2014 deterministic unique element id; the ONLY valid value for target_id;',
973
+ '- "t" \u2014 semantic type: button | input | link | nav | header | main | container | image | select | textarea | modal | tab;',
974
+ '- "r" \u2014 ARIA role (clarifies semantics);',
975
+ '- "p" \u2014 position [x, y], relative 0.0\u20131.0 of the viewport;',
976
+ '- "s" \u2014 size [width, height] in pixels;',
977
+ '- "st" \u2014 state: checked | expanded | disabled;',
978
+ '- "txt" \u2014 visible text or accessible label;',
979
+ '- "act" \u2014 actions the element supports (e.g. ["click"]);',
980
+ '- "vf" / "vf_meta" \u2014 optional visual fragment reference; matching image content, if provided, is attached as separate image blocks.',
981
+ "",
982
+ "The input is either a full document:",
983
+ '{"vsl_version": "...", "canvas": {...}, "objects": [...]}',
984
+ "or a diff (diff-first mode):",
985
+ '{"diff_version": N, "base_version": M, "changes": {"added": [...], "modified": [...], "removed": [...], "unchanged_refs": [...]}}',
986
+ "",
987
+ "Diff semantics:",
988
+ '- "added" \u2014 new subtrees, fully described;',
989
+ '- "modified" \u2014 objects carrying ONLY the changed fields;',
990
+ '- "removed" \u2014 ids that no longer exist: NEVER target them;',
991
+ '- "unchanged_refs" \u2014 known unchanged elements: valid targets.',
992
+ "",
993
+ "## Lazy text loading (M1.7)",
994
+ "",
995
+ "When an element text exceeds 200 characters, the VSL JSON contains only:",
996
+ '- "txt_preview" \u2014 first ~50 characters of the text (with ellipsis);',
997
+ '- "txt_ref" \u2014 reference to the full text block (format: tb_xxx, e.g. tb_001).',
998
+ "",
999
+ 'The full text is stored in "text_blocks" at the document root. To retrieve it, call the MCP tool vsl_get_text_block with {block_id: "<txt_ref value>"}.',
1000
+ "",
1001
+ 'When to fetch full text: only when the user goal REQUIRES reading the content of a long text (e.g. "find the paragraph about X", "click the link in the article about Y"). For navigation by headings/titles \u2014 txt_preview is sufficient.',
1002
+ "",
1003
+ "## Actions",
1004
+ "",
1005
+ "Respond with exactly ONE next action. Allowed actions:",
1006
+ actions + ".",
1007
+ "",
1008
+ "Unified parameter format {action, target_id, value, reasoning}:",
1009
+ PARAMETER_CONVENTIONS,
1010
+ "",
1011
+ "## Response rules",
1012
+ "",
1013
+ "1. Respond ONLY by calling the execute_action tool with one object:",
1014
+ ' {"action": "...", "target_id": "...", "value": null, "reasoning": "..."}',
1015
+ "2. When required, target_id MUST be an id present in the provided VSL JSON (document or diff, including unchanged_refs).",
1016
+ "3. Never target removed ids; never invent ids or actions outside the allowed list.",
1017
+ '4. Prefer the smallest action that moves toward the goal; explain briefly in "reasoning".',
1018
+ "",
1019
+ "## Examples",
1020
+ "",
1021
+ examples
1022
+ ].join("\n");
1023
+ }
1024
+ function buildUserPrompt(vslJson, goal) {
1025
+ return `Current screen (VSL JSON):
1026
+ ${JSON.stringify(vslJson)}
1027
+
1028
+ User goal: ${goal}`;
1029
+ }
1030
+ function collectFragmentData(vslJson, store) {
1031
+ if (store === void 0 || store.size === 0) return [];
1032
+ const refs = /* @__PURE__ */ new Set();
1033
+ const visit = (objects) => {
1034
+ for (const object of objects) {
1035
+ const vf = object.vf;
1036
+ if (typeof vf === "string") refs.add(vf);
1037
+ visit(object.ch ?? []);
1038
+ }
1039
+ };
1040
+ if (isVslDiff(vslJson)) {
1041
+ visit(vslJson.changes.added);
1042
+ for (const modified of vslJson.changes.modified) {
1043
+ const vf = modified.vf;
1044
+ if (typeof vf === "string") refs.add(vf);
1045
+ }
1046
+ } else {
1047
+ visit(vslJson.objects);
1048
+ }
1049
+ const fragments = [];
1050
+ for (const ref of refs) {
1051
+ const data = store.get(ref);
1052
+ if (data !== void 0) fragments.push(data);
1053
+ }
1054
+ return fragments;
1055
+ }
1056
+
1057
+ // src/llm/schema.ts
1058
+ var ACTION_TOOL_NAME = "execute_action";
1059
+ var ACTION_TOOL_DESCRIPTION = "Execute exactly one next browser action toward the user goal.";
1060
+ var ACTION_TOOL_SCHEMA = {
1061
+ type: "object",
1062
+ properties: {
1063
+ action: {
1064
+ type: "string",
1065
+ description: "Action name from the Action Model (ARCHITECTURE.md \xA77.1\u20137.3).",
1066
+ enum: [...VALID_ACTIONS]
1067
+ },
1068
+ target_id: {
1069
+ type: "string",
1070
+ description: "Element id from the provided VSL JSON (required for target actions)."
1071
+ },
1072
+ value: {
1073
+ type: ["string", "null"],
1074
+ description: "Value for parameterized actions: type/select/scroll/navigate/wait/drag."
1075
+ },
1076
+ reasoning: {
1077
+ type: "string",
1078
+ description: "Brief explanation of the chosen action."
1079
+ }
1080
+ },
1081
+ required: ["action"],
1082
+ additionalProperties: false
1083
+ };
1084
+
1085
+ // src/llm/transport.ts
1086
+ var defaultTransport = (url, init) => fetch(url, init);
1087
+ var defaultSleep = (ms) => new Promise((resolve) => {
1088
+ setTimeout(resolve, ms);
1089
+ });
1090
+ var backoffDelayMs = (attempt) => 2 ** attempt * 1e3;
1091
+ var isRetryable = (error) => {
1092
+ if (error instanceof LlmError) {
1093
+ return error.status === void 0 || error.status === 429 || error.status >= 500;
1094
+ }
1095
+ return true;
1096
+ };
1097
+ async function retryWithBackoff(operation, options = {}) {
1098
+ const maxRetries = Math.max(1, options.maxRetries ?? 3);
1099
+ const sleep2 = options.sleep ?? defaultSleep;
1100
+ for (let attempt = 0; ; attempt += 1) {
1101
+ try {
1102
+ return await operation();
1103
+ } catch (error) {
1104
+ if (!isRetryable(error) || attempt >= maxRetries - 1) throw error;
1105
+ await sleep2(backoffDelayMs(attempt));
1106
+ }
1107
+ }
1108
+ }
1109
+ async function executeJsonWithRetry(transport, url, init, providerLabel, options = {}) {
1110
+ const response = await retryWithBackoff(async () => {
1111
+ let res;
1112
+ try {
1113
+ res = await transport(url, init);
1114
+ } catch (error) {
1115
+ const message = error instanceof Error ? error.message : String(error);
1116
+ throw new LlmError(`${providerLabel}: network error \u2014 ${message}`);
1117
+ }
1118
+ if (!res.ok) {
1119
+ throw new LlmError(`${providerLabel}: HTTP ${res.status} ${res.statusText}`, res.status);
1120
+ }
1121
+ return res;
1122
+ }, options);
1123
+ try {
1124
+ return await response.json();
1125
+ } catch {
1126
+ throw new LlmError(`${providerLabel}: response is not valid JSON`);
1127
+ }
1128
+ }
1129
+
1130
+ // src/llm/openai.ts
1131
+ var DEFAULT_MODEL = "gpt-4o";
1132
+ var DEFAULT_BASE_URL = "https://api.openai.com/v1";
1133
+ var OpenAIAdapter = class {
1134
+ apiKey;
1135
+ model;
1136
+ baseUrl;
1137
+ maxRetries;
1138
+ transport;
1139
+ sleep;
1140
+ constructor(config) {
1141
+ if (config.apiKey.length === 0) {
1142
+ throw new LlmError("OpenAIAdapter: apiKey is required");
1143
+ }
1144
+ this.apiKey = config.apiKey;
1145
+ this.model = config.model ?? DEFAULT_MODEL;
1146
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1147
+ this.maxRetries = config.maxRetries ?? 3;
1148
+ this.transport = config.transport ?? defaultTransport;
1149
+ this.sleep = config.sleep;
1150
+ }
1151
+ async sendPrompt(vslJson, task, options) {
1152
+ const userContent = [
1153
+ { type: "text", text: buildUserPrompt(vslJson, task) },
1154
+ ...collectFragmentData(vslJson, options?.visualFragments).map((fragment) => ({
1155
+ type: "image",
1156
+ mediaType: fragment.mediaType,
1157
+ data: fragment.data
1158
+ }))
1159
+ ];
1160
+ return this.sendRaw(buildSystemPrompt(), userContent, {
1161
+ name: ACTION_TOOL_NAME,
1162
+ description: ACTION_TOOL_DESCRIPTION,
1163
+ schema: ACTION_TOOL_SCHEMA
1164
+ });
1165
+ }
1166
+ /**
1167
+ * Низкоуровневый вызов (T1.5.4, вариант C): провайдер-независимые аргументы
1168
+ * → Chat Completions → LlmResponse. Потребители: sendPrompt (tool
1169
+ * «execute_action») и LlmVisionClassifier (tool «classify_fragment»).
1170
+ * Retry (§10.3) и парсинг tool_calls живут здесь в одном месте.
1171
+ */
1172
+ async sendRaw(system, userContent, tool) {
1173
+ const content = userContent.map(
1174
+ (part) => part.type === "text" ? { type: "text", text: part.text } : {
1175
+ type: "image_url",
1176
+ image_url: { url: `data:${part.mediaType};base64,${part.data}` }
1177
+ }
1178
+ );
1179
+ const body = {
1180
+ model: this.model,
1181
+ messages: [
1182
+ { role: "system", content: system },
1183
+ { role: "user", content }
1184
+ ],
1185
+ tools: [
1186
+ {
1187
+ type: "function",
1188
+ function: {
1189
+ name: tool.name,
1190
+ description: tool.description,
1191
+ parameters: tool.schema
1192
+ }
1193
+ }
1194
+ ],
1195
+ tool_choice: { type: "function", function: { name: tool.name } }
1196
+ };
1197
+ const data = await executeJsonWithRetry(
1198
+ this.transport,
1199
+ `${this.baseUrl}/chat/completions`,
1200
+ {
1201
+ method: "POST",
1202
+ headers: {
1203
+ "content-type": "application/json",
1204
+ authorization: `Bearer ${this.apiKey}`
1205
+ },
1206
+ body: JSON.stringify(body)
1207
+ },
1208
+ "OpenAI",
1209
+ { maxRetries: this.maxRetries, sleep: this.sleep }
1210
+ );
1211
+ const message = data.choices?.[0]?.message;
1212
+ const toolCall = message?.tool_calls?.[0];
1213
+ let rawAction;
1214
+ const args = toolCall?.function?.arguments;
1215
+ if (args !== void 0) {
1216
+ try {
1217
+ rawAction = JSON.parse(args);
1218
+ } catch {
1219
+ throw new LlmError("OpenAI: tool_call.arguments is not valid JSON");
1220
+ }
1221
+ }
1222
+ return {
1223
+ text: message?.content ?? null,
1224
+ action: rawAction,
1225
+ model: data.model ?? this.model,
1226
+ usage: {
1227
+ inputTokens: data.usage?.prompt_tokens,
1228
+ outputTokens: data.usage?.completion_tokens
1229
+ }
1230
+ };
1231
+ }
1232
+ /** Convenience §9.4: решение для цели + валидация (whitelist + target_id). */
1233
+ async decide(input) {
1234
+ const response = await this.sendPrompt(input.vslJson, input.goal, {
1235
+ visualFragments: input.visualFragments
1236
+ });
1237
+ return validateAction(response.action, collectIds(input.vslJson));
1238
+ }
1239
+ };
1240
+
1241
+ // src/llm/anthropic.ts
1242
+ var DEFAULT_MODEL2 = "claude-sonnet-4-20250514";
1243
+ var DEFAULT_BASE_URL2 = "https://api.anthropic.com";
1244
+ var ANTHROPIC_VERSION = "2023-06-01";
1245
+ var DEFAULT_MAX_TOKENS = 1024;
1246
+ var AnthropicAdapter = class {
1247
+ apiKey;
1248
+ model;
1249
+ baseUrl;
1250
+ maxTokens;
1251
+ maxRetries;
1252
+ transport;
1253
+ sleep;
1254
+ constructor(config) {
1255
+ if (config.apiKey.length === 0) {
1256
+ throw new LlmError("AnthropicAdapter: apiKey is required");
1257
+ }
1258
+ this.apiKey = config.apiKey;
1259
+ this.model = config.model ?? DEFAULT_MODEL2;
1260
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
1261
+ this.maxTokens = DEFAULT_MAX_TOKENS;
1262
+ this.maxRetries = config.maxRetries ?? 3;
1263
+ this.transport = config.transport ?? defaultTransport;
1264
+ this.sleep = config.sleep;
1265
+ }
1266
+ async sendPrompt(vslJson, task, options) {
1267
+ const userContent = [
1268
+ { type: "text", text: buildUserPrompt(vslJson, task) },
1269
+ ...collectFragmentData(vslJson, options?.visualFragments).map((fragment) => ({
1270
+ type: "image",
1271
+ mediaType: fragment.mediaType,
1272
+ data: fragment.data
1273
+ }))
1274
+ ];
1275
+ return this.sendRaw(buildSystemPrompt(), userContent, {
1276
+ name: ACTION_TOOL_NAME,
1277
+ description: ACTION_TOOL_DESCRIPTION,
1278
+ schema: ACTION_TOOL_SCHEMA
1279
+ });
1280
+ }
1281
+ /**
1282
+ * Низкоуровневый вызов (T1.5.4, вариант C): провайдер-независимые аргументы
1283
+ * → Messages API → LlmResponse. Потребители: sendPrompt (tool
1284
+ * «execute_action») и LlmVisionClassifier (tool «classify_fragment»).
1285
+ * tool_use-блоки фильтруются по tool.name (ранее — константа ACTION_TOOL_NAME):
1286
+ * параметризация безопасна, sendPrompt передаёт ACTION_TOOL_NAME.
1287
+ */
1288
+ async sendRaw(system, userContent, tool) {
1289
+ const content = userContent.map(
1290
+ (part) => part.type === "text" ? { type: "text", text: part.text } : {
1291
+ type: "image",
1292
+ source: {
1293
+ type: "base64",
1294
+ media_type: part.mediaType,
1295
+ data: part.data
1296
+ }
1297
+ }
1298
+ );
1299
+ const body = {
1300
+ model: this.model,
1301
+ max_tokens: this.maxTokens,
1302
+ system,
1303
+ messages: [{ role: "user", content }],
1304
+ tools: [
1305
+ {
1306
+ name: tool.name,
1307
+ description: tool.description,
1308
+ input_schema: tool.schema
1309
+ }
1310
+ ],
1311
+ tool_choice: { type: "tool", name: tool.name }
1312
+ };
1313
+ const data = await executeJsonWithRetry(
1314
+ this.transport,
1315
+ `${this.baseUrl}/v1/messages`,
1316
+ {
1317
+ method: "POST",
1318
+ headers: {
1319
+ "content-type": "application/json",
1320
+ "x-api-key": this.apiKey,
1321
+ "anthropic-version": ANTHROPIC_VERSION
1322
+ },
1323
+ body: JSON.stringify(body)
1324
+ },
1325
+ "Anthropic",
1326
+ { maxRetries: this.maxRetries, sleep: this.sleep }
1327
+ );
1328
+ let text = null;
1329
+ let rawAction;
1330
+ for (const block of data.content ?? []) {
1331
+ if (block.type === "text" && typeof block.text === "string") {
1332
+ text = text === null ? block.text : `${text}
1333
+ ${block.text}`;
1334
+ } else if (block.type === "tool_use" && block.name === tool.name) {
1335
+ rawAction = block.input;
1336
+ }
1337
+ }
1338
+ return {
1339
+ text,
1340
+ action: rawAction,
1341
+ model: data.model ?? this.model,
1342
+ usage: {
1343
+ inputTokens: data.usage?.input_tokens,
1344
+ outputTokens: data.usage?.output_tokens
1345
+ }
1346
+ };
1347
+ }
1348
+ /** Convenience §9.4: решение для цели + валидация (whitelist + target_id). */
1349
+ async decide(input) {
1350
+ const response = await this.sendPrompt(input.vslJson, input.goal, {
1351
+ visualFragments: input.visualFragments
1352
+ });
1353
+ return validateAction(response.action, collectIds(input.vslJson));
1354
+ }
1355
+ };
1356
+
1357
+ // src/llm/alibaba.ts
1358
+ var DEFAULT_MODEL3 = "qwen3.8-flash";
1359
+ var DEFAULT_BASE_URL3 = "https://dashscope.aliyuncs.com/compatible-mode/v1";
1360
+ var AlibabaAdapter = class {
1361
+ apiKey;
1362
+ model;
1363
+ baseUrl;
1364
+ maxRetries;
1365
+ transport;
1366
+ sleep;
1367
+ constructor(config) {
1368
+ if (config.apiKey.length === 0) {
1369
+ throw new LlmError("AlibabaAdapter: apiKey is required");
1370
+ }
1371
+ this.apiKey = config.apiKey;
1372
+ this.model = config.model ?? DEFAULT_MODEL3;
1373
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL3).replace(/\/+$/, "");
1374
+ this.maxRetries = config.maxRetries ?? 3;
1375
+ this.transport = config.transport ?? defaultTransport;
1376
+ this.sleep = config.sleep;
1377
+ }
1378
+ async sendPrompt(vslJson, task, options) {
1379
+ const userContent = [
1380
+ { type: "text", text: buildUserPrompt(vslJson, task) },
1381
+ ...collectFragmentData(vslJson, options?.visualFragments).map((fragment) => ({
1382
+ type: "image",
1383
+ mediaType: fragment.mediaType,
1384
+ data: fragment.data
1385
+ }))
1386
+ ];
1387
+ return this.sendRaw(buildSystemPrompt(), userContent, {
1388
+ name: ACTION_TOOL_NAME,
1389
+ description: ACTION_TOOL_DESCRIPTION,
1390
+ schema: ACTION_TOOL_SCHEMA
1391
+ });
1392
+ }
1393
+ /**
1394
+ * Низкоуровневый вызов (T1.5.4, вариант C): провайдер-независимые аргументы
1395
+ * → DashScope Chat Completions → LlmResponse. Потребители: sendPrompt (tool
1396
+ * «execute_action») и LlmVisionClassifier (tool «classify_fragment»).
1397
+ * Retry (§10.3) и парсинг tool_calls живут здесь в одном месте.
1398
+ */
1399
+ async sendRaw(system, userContent, tool) {
1400
+ const content = userContent.map(
1401
+ (part) => part.type === "text" ? { type: "text", text: part.text } : {
1402
+ type: "image_url",
1403
+ image_url: { url: `data:${part.mediaType};base64,${part.data}` }
1404
+ }
1405
+ );
1406
+ const body = {
1407
+ model: this.model,
1408
+ messages: [
1409
+ { role: "system", content: system },
1410
+ { role: "user", content }
1411
+ ],
1412
+ tools: [
1413
+ {
1414
+ type: "function",
1415
+ function: {
1416
+ name: tool.name,
1417
+ description: tool.description,
1418
+ parameters: tool.schema
1419
+ }
1420
+ }
1421
+ ],
1422
+ tool_choice: { type: "function", function: { name: tool.name } }
1423
+ };
1424
+ const data = await executeJsonWithRetry(
1425
+ this.transport,
1426
+ `${this.baseUrl}/chat/completions`,
1427
+ {
1428
+ method: "POST",
1429
+ headers: {
1430
+ "content-type": "application/json",
1431
+ authorization: `Bearer ${this.apiKey}`
1432
+ },
1433
+ body: JSON.stringify(body)
1434
+ },
1435
+ "Alibaba/Qwen",
1436
+ { maxRetries: this.maxRetries, sleep: this.sleep }
1437
+ );
1438
+ const message = data.choices?.[0]?.message;
1439
+ const toolCall = message?.tool_calls?.[0];
1440
+ let rawAction;
1441
+ const args = toolCall?.function?.arguments;
1442
+ if (args !== void 0) {
1443
+ try {
1444
+ rawAction = JSON.parse(args);
1445
+ } catch {
1446
+ throw new LlmError("Alibaba/Qwen: tool_call.arguments is not valid JSON");
1447
+ }
1448
+ }
1449
+ return {
1450
+ text: message?.content ?? null,
1451
+ action: rawAction,
1452
+ model: data.model ?? this.model,
1453
+ usage: {
1454
+ inputTokens: data.usage?.prompt_tokens,
1455
+ outputTokens: data.usage?.completion_tokens
1456
+ }
1457
+ };
1458
+ }
1459
+ /** Convenience §9.4: решение для цели + валидация (whitelist + target_id). */
1460
+ async decide(input) {
1461
+ const response = await this.sendPrompt(input.vslJson, input.goal, {
1462
+ visualFragments: input.visualFragments
1463
+ });
1464
+ return validateAction(response.action, collectIds(input.vslJson));
1465
+ }
1466
+ };
1467
+
1468
+ // src/executor/types.ts
1469
+ var ActionExecutionError = class extends Error {
1470
+ constructor(message) {
1471
+ super(message);
1472
+ this.name = "ActionExecutionError";
1473
+ }
1474
+ };
1475
+
1476
+ // src/executor/resolveTarget.ts
1477
+ var ID_SEPARATOR = "_";
1478
+ var INDEX_SEGMENT_RE = /^\d+$/;
1479
+ function defaultRoot2() {
1480
+ const body = document.body;
1481
+ if (body === null) {
1482
+ throw new ActionExecutionError(
1483
+ "resolveTarget: document.body is not available \u2014 pass the snapshot root via ExecutorOptions.root"
1484
+ );
1485
+ }
1486
+ return body;
1487
+ }
1488
+ function parseTargetId(targetId) {
1489
+ const segments = targetId.split(ID_SEPARATOR);
1490
+ const tag = segments[0];
1491
+ if (tag === void 0 || tag.length === 0) {
1492
+ throw new ActionExecutionError(
1493
+ `Invalid target_id "${targetId}": missing tag \u2014 expected format "tag_index1_index2_..."`
1494
+ );
1495
+ }
1496
+ const indexSegments = segments.slice(1);
1497
+ if (indexSegments.length === 0) {
1498
+ throw new ActionExecutionError(
1499
+ `Invalid target_id "${targetId}": missing index path \u2014 expected format "tag_index1_index2_..."`
1500
+ );
1501
+ }
1502
+ const indexPath = [];
1503
+ for (const segment of indexSegments) {
1504
+ if (!INDEX_SEGMENT_RE.test(segment)) {
1505
+ throw new ActionExecutionError(
1506
+ `Invalid target_id "${targetId}": index segment "${segment}" is not a non-negative integer`
1507
+ );
1508
+ }
1509
+ indexPath.push(Number(segment));
1510
+ }
1511
+ return { tag, indexPath };
1512
+ }
1513
+ function resolveTarget(targetId, options = {}) {
1514
+ const root = options.root ?? defaultRoot2();
1515
+ const { tag, indexPath } = parseTargetId(targetId);
1516
+ let current = root;
1517
+ let depth = 0;
1518
+ for (const index of indexPath) {
1519
+ depth += 1;
1520
+ const next = current.children[index];
1521
+ if (next === void 0) {
1522
+ throw new ActionExecutionError(
1523
+ `Failed to resolve target_id "${targetId}": no element child at index ${index} on depth ${depth} \u2014 DOM may have changed since the snapshot`
1524
+ );
1525
+ }
1526
+ current = next;
1527
+ }
1528
+ const actualTag = current.tagName.toLowerCase();
1529
+ if (actualTag !== tag) {
1530
+ throw new ActionExecutionError(
1531
+ `Failed to resolve target_id "${targetId}": found <${actualTag}>, expected tag "${tag}" \u2014 DOM may have changed since the snapshot`
1532
+ );
1533
+ }
1534
+ return current;
1535
+ }
1536
+
1537
+ // src/executor/actionExecutor.ts
1538
+ var DEFAULT_SCROLL_AMOUNT = 300;
1539
+ var SCROLL_DIRECTIONS = /* @__PURE__ */ new Set(["up", "down", "left", "right"]);
1540
+ var WAIT_DEFAULT_TIMEOUT_MS = 5e3;
1541
+ var WAIT_POLL_INTERVAL_MS = 50;
1542
+ function resolveTargetOf(action, options) {
1543
+ const targetId = action.target_id;
1544
+ if (typeof targetId !== "string") {
1545
+ throw new ActionExecutionError(`Action "${action.action}" requires a string target_id`);
1546
+ }
1547
+ return resolveTarget(targetId, options);
1548
+ }
1549
+ function requireValue(action) {
1550
+ const value = action.value;
1551
+ if (typeof value !== "string") {
1552
+ throw new ActionExecutionError(`Action "${action.action}" requires a string value`);
1553
+ }
1554
+ return value;
1555
+ }
1556
+ function dispatchValueEvents(el) {
1557
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1558
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1559
+ }
1560
+ function requireTextTarget(action, el) {
1561
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1562
+ return el;
1563
+ }
1564
+ throw new ActionExecutionError(
1565
+ `Action "${action.action}" requires an <input> or <textarea> target, got <${el.tagName.toLowerCase()}>`
1566
+ );
1567
+ }
1568
+ function typeInto(el, text) {
1569
+ el.value = text;
1570
+ dispatchValueEvents(el);
1571
+ }
1572
+ function clickElement(el) {
1573
+ if (typeof el.click === "function") {
1574
+ el.click();
1575
+ return;
1576
+ }
1577
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1578
+ }
1579
+ function hoverElement(el) {
1580
+ el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, cancelable: true }));
1581
+ el.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false }));
1582
+ }
1583
+ function focusableOrThrow(action, el) {
1584
+ const htmlEl = el;
1585
+ if (typeof htmlEl.focus !== "function" || typeof htmlEl.blur !== "function") {
1586
+ throw new ActionExecutionError(
1587
+ `Action "${action.action}" requires a focusable element, got <${el.tagName.toLowerCase()}>`
1588
+ );
1589
+ }
1590
+ return htmlEl;
1591
+ }
1592
+ function selectOption(el, raw) {
1593
+ if (!(el instanceof HTMLSelectElement)) {
1594
+ throw new ActionExecutionError(
1595
+ `Action "select" requires a <select> target, got <${el.tagName.toLowerCase()}>`
1596
+ );
1597
+ }
1598
+ const wanted = raw.trim();
1599
+ for (const option of Array.from(el.options)) {
1600
+ if (option.value === raw) {
1601
+ el.value = option.value;
1602
+ dispatchValueEvents(el);
1603
+ return;
1604
+ }
1605
+ }
1606
+ for (const option of Array.from(el.options)) {
1607
+ if (option.text.trim() === wanted) {
1608
+ el.value = option.value;
1609
+ dispatchValueEvents(el);
1610
+ return;
1611
+ }
1612
+ }
1613
+ throw new ActionExecutionError(
1614
+ `Action "select": option "${raw}" not found in <select> (neither by value nor by visible text)`
1615
+ );
1616
+ }
1617
+ function setChecked(el, action, checked) {
1618
+ if (!(el instanceof HTMLInputElement) || el.type !== "checkbox" && el.type !== "radio") {
1619
+ throw new ActionExecutionError(
1620
+ `Action "${action.action}" requires an <input type="checkbox"> or <input type="radio"> target, got <${el.tagName.toLowerCase()}>`
1621
+ );
1622
+ }
1623
+ el.checked = checked;
1624
+ dispatchValueEvents(el);
1625
+ }
1626
+ function parseScrollValue(value, defaultAmount) {
1627
+ const parts = value.trim().split(":");
1628
+ const dir = parts[0] ?? "";
1629
+ const amountPart = parts[1];
1630
+ if (parts.length > 2 || !SCROLL_DIRECTIONS.has(dir)) {
1631
+ throw new ActionExecutionError(
1632
+ `Invalid scroll value "${value}" \u2014 expected "up", "down", "left", "right" or "dir:amount" (e.g. "down:300")`
1633
+ );
1634
+ }
1635
+ let amount = defaultAmount;
1636
+ if (amountPart !== void 0) {
1637
+ if (!/^\d+(?:\.\d+)?$/.test(amountPart)) {
1638
+ throw new ActionExecutionError(
1639
+ `Invalid scroll amount in "${value}" \u2014 expected a positive number, e.g. "down:300"`
1640
+ );
1641
+ }
1642
+ amount = Number(amountPart);
1643
+ }
1644
+ const dx = dir === "left" ? -amount : dir === "right" ? amount : 0;
1645
+ const dy = dir === "up" ? -amount : dir === "down" ? amount : 0;
1646
+ return { dx, dy };
1647
+ }
1648
+ function dispatchDragEvent(target, type) {
1649
+ if (typeof DragEvent === "function") {
1650
+ target.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true }));
1651
+ } else {
1652
+ target.dispatchEvent(new Event(type, { bubbles: true, cancelable: true }));
1653
+ }
1654
+ }
1655
+ function requireForm(action, el) {
1656
+ if (el instanceof HTMLFormElement) {
1657
+ return el;
1658
+ }
1659
+ throw new ActionExecutionError(
1660
+ `Action "${action.action}" requires a <form> target, got <${el.tagName.toLowerCase()}>`
1661
+ );
1662
+ }
1663
+ function setOpenState(action, el, open) {
1664
+ if (typeof HTMLDialogElement !== "undefined" && el instanceof HTMLDialogElement) {
1665
+ if (open) {
1666
+ el.showModal();
1667
+ } else {
1668
+ el.close();
1669
+ }
1670
+ return;
1671
+ }
1672
+ if (el instanceof HTMLDetailsElement) {
1673
+ el.open = open;
1674
+ return;
1675
+ }
1676
+ throw new ActionExecutionError(
1677
+ `Action "${action.action}" requires a <dialog> or <details> target, got <${el.tagName.toLowerCase()}>`
1678
+ );
1679
+ }
1680
+ function setExpanded(action, el, expanded) {
1681
+ if (el instanceof HTMLDetailsElement) {
1682
+ el.open = expanded;
1683
+ return;
1684
+ }
1685
+ if (el.hasAttribute("aria-expanded")) {
1686
+ el.setAttribute("aria-expanded", expanded ? "true" : "false");
1687
+ return;
1688
+ }
1689
+ throw new ActionExecutionError(
1690
+ `Action "${action.action}" requires a <details> or an element with aria-expanded, got <${el.tagName.toLowerCase()}>`
1691
+ );
1692
+ }
1693
+ function sleep(ms) {
1694
+ return new Promise((resolve) => {
1695
+ setTimeout(resolve, ms);
1696
+ });
1697
+ }
1698
+ function isWaitSatisfied(value) {
1699
+ if (value.trim() === "idle") {
1700
+ return document.readyState === "complete";
1701
+ }
1702
+ try {
1703
+ return document.querySelector(value) !== null;
1704
+ } catch {
1705
+ throw new ActionExecutionError(`wait: invalid selector "${value}"`);
1706
+ }
1707
+ }
1708
+ async function waitForValue(value, options) {
1709
+ const timeoutMs = options.waitTimeout ?? WAIT_DEFAULT_TIMEOUT_MS;
1710
+ const deadline = Date.now() + timeoutMs;
1711
+ for (; ; ) {
1712
+ if (isWaitSatisfied(value)) {
1713
+ return;
1714
+ }
1715
+ if (Date.now() >= deadline) {
1716
+ throw new ActionExecutionError(
1717
+ `wait: condition "${value}" is not met within ${timeoutMs}ms`
1718
+ );
1719
+ }
1720
+ await sleep(WAIT_POLL_INTERVAL_MS);
1721
+ }
1722
+ }
1723
+ async function executeAction(action, options = {}) {
1724
+ const targetId = action.target_id;
1725
+ const echo = {
1726
+ success: false,
1727
+ action: action.action,
1728
+ ...typeof targetId === "string" ? { targetId } : {}
1729
+ };
1730
+ try {
1731
+ switch (action.action) {
1732
+ // ——— §7.1 Базовые (T1.4.1) ———
1733
+ case "click":
1734
+ clickElement(resolveTargetOf(action, options));
1735
+ break;
1736
+ case "type":
1737
+ typeInto(
1738
+ requireTextTarget(action, resolveTargetOf(action, options)),
1739
+ requireValue(action)
1740
+ );
1741
+ break;
1742
+ case "clear": {
1743
+ const field = requireTextTarget(action, resolveTargetOf(action, options));
1744
+ typeInto(field, "");
1745
+ break;
1746
+ }
1747
+ case "scroll": {
1748
+ const amount = options.defaultScrollAmount ?? DEFAULT_SCROLL_AMOUNT;
1749
+ const { dx, dy } = parseScrollValue(requireValue(action), amount);
1750
+ window.scrollBy(dx, dy);
1751
+ break;
1752
+ }
1753
+ case "hover":
1754
+ hoverElement(resolveTargetOf(action, options));
1755
+ break;
1756
+ case "focus":
1757
+ focusableOrThrow(action, resolveTargetOf(action, options)).focus();
1758
+ break;
1759
+ case "blur":
1760
+ focusableOrThrow(action, resolveTargetOf(action, options)).blur();
1761
+ break;
1762
+ case "select":
1763
+ selectOption(resolveTargetOf(action, options), requireValue(action));
1764
+ break;
1765
+ case "check":
1766
+ setChecked(resolveTargetOf(action, options), action, true);
1767
+ break;
1768
+ case "uncheck":
1769
+ setChecked(resolveTargetOf(action, options), action, false);
1770
+ break;
1771
+ // ——— §7.2 Расширенные (T1.4.2) ———
1772
+ case "drag": {
1773
+ const destinationId = requireValue(action);
1774
+ const source = resolveTargetOf(action, options);
1775
+ const destination = resolveTarget(destinationId, options);
1776
+ dispatchDragEvent(source, "dragstart");
1777
+ dispatchDragEvent(destination, "dragenter");
1778
+ dispatchDragEvent(destination, "dragover");
1779
+ dispatchDragEvent(destination, "drop");
1780
+ dispatchDragEvent(source, "dragend");
1781
+ break;
1782
+ }
1783
+ case "drop": {
1784
+ const dropTarget = resolveTargetOf(action, options);
1785
+ dispatchDragEvent(dropTarget, "dragover");
1786
+ dispatchDragEvent(dropTarget, "drop");
1787
+ break;
1788
+ }
1789
+ case "submit": {
1790
+ const form = requireForm(action, resolveTargetOf(action, options));
1791
+ form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
1792
+ break;
1793
+ }
1794
+ case "reset":
1795
+ requireForm(action, resolveTargetOf(action, options)).reset();
1796
+ break;
1797
+ case "open":
1798
+ setOpenState(action, resolveTargetOf(action, options), true);
1799
+ break;
1800
+ case "close":
1801
+ setOpenState(action, resolveTargetOf(action, options), false);
1802
+ break;
1803
+ case "expand":
1804
+ setExpanded(action, resolveTargetOf(action, options), true);
1805
+ break;
1806
+ case "collapse":
1807
+ setExpanded(action, resolveTargetOf(action, options), false);
1808
+ break;
1809
+ case "wait":
1810
+ await waitForValue(requireValue(action), options);
1811
+ break;
1812
+ // ——— §7.3 Навигационные (T1.4.3) ———
1813
+ case "navigate":
1814
+ window.location.assign(requireValue(action));
1815
+ break;
1816
+ case "go_back":
1817
+ window.history.back();
1818
+ break;
1819
+ case "go_forward":
1820
+ window.history.forward();
1821
+ break;
1822
+ case "refresh":
1823
+ window.location.reload();
1824
+ break;
1825
+ case "download": {
1826
+ const downloadTargetId = action.target_id;
1827
+ const downloadValue = action.value;
1828
+ if (typeof downloadTargetId === "string") {
1829
+ const el = resolveTarget(downloadTargetId, options);
1830
+ clickElement(el);
1831
+ break;
1832
+ }
1833
+ if (typeof downloadValue === "string") {
1834
+ const url = downloadValue;
1835
+ const a = document.createElement("a");
1836
+ a.href = url;
1837
+ a.download = "";
1838
+ document.body.appendChild(a);
1839
+ a.click();
1840
+ document.body.removeChild(a);
1841
+ const downloadId = `download_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1842
+ const filename = url.split("/").pop() || "download";
1843
+ const downloadInfo = {
1844
+ downloadId,
1845
+ filename,
1846
+ url,
1847
+ status: "pending"
1848
+ };
1849
+ return {
1850
+ ...echo,
1851
+ success: true,
1852
+ download: downloadInfo
1853
+ };
1854
+ }
1855
+ throw new ActionExecutionError(
1856
+ 'Action "download" requires either target_id (element to click) or value (URL to download)'
1857
+ );
1858
+ }
1859
+ default:
1860
+ throw new ActionExecutionError(
1861
+ `Unknown action: "${action.action}" \u2014 must be one of VALID_ACTIONS (24 actions)`
1862
+ );
1863
+ }
1864
+ return { ...echo, success: true };
1865
+ } catch (error) {
1866
+ const message = error instanceof Error ? error.message : String(error);
1867
+ return { ...echo, success: false, error: message };
1868
+ }
1869
+ }
1870
+
1871
+ // src/index.ts
1872
+ var VSL_SDK_VERSION = "0.1.0";
1873
+ // Annotate the CommonJS export names for ESM import in node:
1874
+ 0 && (module.exports = {
1875
+ ACTION_TOOL_DESCRIPTION,
1876
+ ACTION_TOOL_NAME,
1877
+ ACTION_TOOL_SCHEMA,
1878
+ ARIA_ROLE_TYPE_MAP,
1879
+ ActionExecutionError,
1880
+ AlibabaAdapter,
1881
+ AnthropicAdapter,
1882
+ LEVEL1_TAG_MAP,
1883
+ LlmError,
1884
+ LlmValidationError,
1885
+ OpenAIAdapter,
1886
+ VALID_ACTIONS,
1887
+ VSL_SDK_VERSION,
1888
+ VSL_VERSION,
1889
+ VslSnapshotSession,
1890
+ attachMutationObserver,
1891
+ buildSystemPrompt,
1892
+ buildUserPrompt,
1893
+ buildVslDocument,
1894
+ collectIds,
1895
+ computeContentHash,
1896
+ computeCoordHash,
1897
+ createCacheStore,
1898
+ diffVslDocuments,
1899
+ executeAction,
1900
+ extractDomTree,
1901
+ isAriaHidden,
1902
+ isVslDiff,
1903
+ ownText,
1904
+ resolveAriaRoleType,
1905
+ resolveLevel1Type,
1906
+ resolveSt,
1907
+ resolveTarget,
1908
+ retryWithBackoff,
1909
+ segmentTree,
1910
+ validateAction
1911
+ });
1912
+ //# sourceMappingURL=index.js.map