@ceki/n8n-nodes-ceki 0.2.29 → 0.2.30

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.
@@ -23,6 +23,7 @@ __export(BrowserCeki_node_exports, {
23
23
  BrowserCeki: () => BrowserCeki
24
24
  });
25
25
  module.exports = __toCommonJS(BrowserCeki_node_exports);
26
+ var import_n8n_workflow = require("n8n-workflow");
26
27
 
27
28
  // lib/ceki-client.ts
28
29
  function jsonParse(raw) {
@@ -370,9 +371,9 @@ var CekiBrowser = class {
370
371
  async navigate(url, timeoutMs) {
371
372
  await this.send("Page.navigate", { url }, timeoutMs);
372
373
  }
373
- async click(x2, y2) {
374
- await this.send("Input.dispatchMouseEvent", { type: "mousePressed", x: x2, y: y2, button: "left", clickCount: 1 });
375
- await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: x2, y: y2, button: "left", clickCount: 1 });
374
+ async click(x, y) {
375
+ await this.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
376
+ await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
376
377
  }
377
378
  async type(text) {
378
379
  await this.send("Ceki.typeText", { text });
@@ -435,7 +436,180 @@ var CekiBrowser = class {
435
436
  }
436
437
  };
437
438
 
439
+ // lib/contract-client.ts
440
+ function cleanArgs(o) {
441
+ const out = { ...o };
442
+ for (const k of Object.keys(out)) {
443
+ if (out[k] === void 0 || out[k] === null) delete out[k];
444
+ }
445
+ return out;
446
+ }
447
+ function parseBenefitable(value) {
448
+ if (!value) return null;
449
+ const m = /^(agent|user):(\d+)$/.exec(value);
450
+ if (!m) return null;
451
+ return { type: m[1], value: Number(m[2]) };
452
+ }
453
+ function parseParticipant(value, roleId) {
454
+ const b = parseBenefitable(value);
455
+ if (!b) return null;
456
+ return { participable_id: b.value, type: b.type, role_id: roleId };
457
+ }
458
+ function deriveLabel(desc) {
459
+ if (!desc) return "";
460
+ const line = desc.split("\n")[0].trim();
461
+ return line.length > 60 ? line.slice(0, 57) + "..." : line;
462
+ }
463
+ var ROLE_REVIEWER = 5;
464
+ var ROLE_QA = 6;
465
+ var ContractClient = class {
466
+ constructor(token, endpoint, apiBase) {
467
+ this._endpoint = (endpoint ?? "https://api.ceki.me/mcp").replace(/\/+$/, "");
468
+ this._apiBase = (apiBase ?? "https://api.ceki.me").replace(/\/+$/, "");
469
+ this._token = token;
470
+ }
471
+ _headers() {
472
+ return {
473
+ "Content-Type": "application/json",
474
+ Accept: "application/json",
475
+ Authorization: `Bearer ${this._token}`
476
+ };
477
+ }
478
+ async _rpc(method, params) {
479
+ const body = JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params });
480
+ const resp = await fetch(this._endpoint, {
481
+ method: "POST",
482
+ headers: this._headers(),
483
+ body
484
+ });
485
+ if (!resp.ok) {
486
+ const text = await resp.text().catch(() => "");
487
+ throw new Error(`HTTP ${resp.status}: ${text.slice(0, 400)}`);
488
+ }
489
+ return resp.json();
490
+ }
491
+ async _call(tool, args) {
492
+ const body = await this._rpc("tools/call", { name: tool, arguments: args ?? {} });
493
+ if (body.error) throw new Error(`${tool} \u2192 ${JSON.stringify(body.error).slice(0, 400)}`);
494
+ const result = body.result ?? {};
495
+ const content = result.content;
496
+ if (Array.isArray(content)) {
497
+ const texts = content.filter((c) => c.type === "text").map((c) => String(c.text ?? ""));
498
+ const joined = texts.join("\n");
499
+ try {
500
+ return JSON.parse(joined);
501
+ } catch {
502
+ return joined;
503
+ }
504
+ }
505
+ if (result.structuredContent !== void 0) return result.structuredContent;
506
+ return result;
507
+ }
508
+ // ── domain methods ─────────────────────────────────────────
509
+ async listContracts() {
510
+ return this._call("list-contracts");
511
+ }
512
+ async members(contractId) {
513
+ return this._call("contract-members", { contract_id: contractId });
514
+ }
515
+ async tasks(contractId) {
516
+ return this._call("contract-tasks", { contract_id: contractId });
517
+ }
518
+ async myEvents() {
519
+ return this._call("get-my-events");
520
+ }
521
+ async task(eventId) {
522
+ return this._call("get-event", { event_id: eventId });
523
+ }
524
+ async create(contractId, opts) {
525
+ const args = cleanArgs({
526
+ contract_id: contractId,
527
+ label: opts.label,
528
+ type_id: opts.type,
529
+ status_id: opts.status,
530
+ description: opts.description,
531
+ benefitable: opts.benefitable ? parseBenefitable(opts.benefitable) : void 0
532
+ });
533
+ const users = [];
534
+ const rev = parseParticipant(opts.reviewer, ROLE_REVIEWER);
535
+ if (rev) users.push(rev);
536
+ const qa = parseParticipant(opts.qa, ROLE_QA);
537
+ if (qa) users.push(qa);
538
+ if (users.length) args.users = users;
539
+ return this._call("create-contract-event", args);
540
+ }
541
+ async propose(eventId, opts) {
542
+ return this._call("propose-correction", cleanArgs({
543
+ event_id: eventId,
544
+ status_id: opts.status,
545
+ label: opts.label,
546
+ description: opts.description,
547
+ benefitable: opts.benefitable ? parseBenefitable(opts.benefitable) : void 0
548
+ }));
549
+ }
550
+ async comment(eventId, opts) {
551
+ return this._call("comment", cleanArgs({
552
+ event_id: eventId,
553
+ label: opts?.label,
554
+ description: opts?.description
555
+ }));
556
+ }
557
+ async progress(eventId, opts) {
558
+ let statusResult = null;
559
+ if (opts.status != null) {
560
+ statusResult = await this.propose(eventId, { status: opts.status });
561
+ }
562
+ const commentResult = await this.comment(eventId, { label: deriveLabel(opts.desc), description: opts.desc });
563
+ return { status_correction: statusResult, comment: commentResult };
564
+ }
565
+ async callHuman(eventId, kind, desc) {
566
+ if (!["input", "review", "stuck"].includes(kind)) throw new Error(`kind must be input|review|stuck, got ${kind}`);
567
+ return this._call("call-human", { event_id: eventId, kind, desc });
568
+ }
569
+ /** GET /agent/polling. Returns [] on 429. */
570
+ async poll() {
571
+ const resp = await fetch(`${this._apiBase}/agent/polling`, {
572
+ headers: { Accept: "application/json", Authorization: `Bearer ${this._token}` }
573
+ });
574
+ if (resp.status === 429) return [];
575
+ if (!resp.ok) {
576
+ const text = await resp.text().catch(() => "");
577
+ throw new Error(`poll HTTP ${resp.status}: ${text.slice(0, 300)}`);
578
+ }
579
+ const body = await resp.json();
580
+ if (Array.isArray(body)) return body;
581
+ if (body && typeof body === "object") {
582
+ for (const k of ["notifications", "data", "items"]) {
583
+ if (Array.isArray(body[k])) return body[k];
584
+ }
585
+ }
586
+ return [];
587
+ }
588
+ };
589
+
438
590
  // nodes/BrowserCeki/BrowserCeki.node.ts
591
+ async function waitForSelector(browser, selector, timeoutMs, intervalMs = 500) {
592
+ const expr = `!!document.querySelector(${JSON.stringify(selector)})`;
593
+ const deadline = Date.now() + timeoutMs;
594
+ let lastErr = null;
595
+ while (Date.now() < deadline) {
596
+ try {
597
+ const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
598
+ if (res?.result?.value === true) return true;
599
+ } catch (e) {
600
+ lastErr = e;
601
+ }
602
+ await sleep(intervalMs);
603
+ }
604
+ throw new Error(
605
+ `waitForSelector("${selector}") timed out after ${timeoutMs}ms${lastErr ? ": " + lastErr.message : ""}`
606
+ );
607
+ }
608
+ async function extractHtml(browser, selector) {
609
+ const expr = selector.trim() === "" || selector === "body" ? "document.body ? document.body.outerHTML : ''" : "(function(){ var el = document.querySelector(" + JSON.stringify(selector) + "); return el ? el.outerHTML : ''; })()";
610
+ const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
611
+ return res?.result?.value ?? "";
612
+ }
439
613
  var sleep = (ms) => new Promise((resolve) => {
440
614
  AbortSignal.timeout(ms).addEventListener("abort", () => resolve(), { once: true });
441
615
  });
@@ -444,14 +618,18 @@ var BrowserCeki = class {
444
618
  this.description = {
445
619
  displayName: "Browser Ceki",
446
620
  name: "browserCeki",
447
- icon: "file:ceki.png",
621
+ icon: {
622
+ light: "file:ceki-light.svg",
623
+ dark: "file:ceki-dark.svg"
624
+ },
448
625
  group: ["transform"],
449
626
  version: 1,
627
+ usableAsTool: true,
450
628
  subtitle: '={{ "Ceki: " + $parameter.operation }}',
451
629
  description: "Rent a real human browser and control it: rent, navigate, click, type, screenshot, solve captchas, and more",
452
630
  defaults: { name: "Browser Ceki" },
453
- inputs: ["main"],
454
- outputs: ["main"],
631
+ inputs: [import_n8n_workflow.NodeConnectionTypes.Main],
632
+ outputs: [import_n8n_workflow.NodeConnectionTypes.Main],
455
633
  credentials: [{ name: "cekiApi", required: true }],
456
634
  properties: [
457
635
  {
@@ -459,22 +637,38 @@ var BrowserCeki = class {
459
637
  name: "operation",
460
638
  type: "options",
461
639
  default: "search",
640
+ noDataExpression: true,
462
641
  options: [
463
- { name: "Search", value: "search" },
464
- { name: "Rent", value: "rent" },
465
- { name: "Navigate", value: "navigate" },
642
+ // Browser operations
643
+ { name: "Captcha-Protected Scrape", value: "captchaScrape" },
466
644
  { name: "Click", value: "click" },
467
- { name: "Type", value: "type" },
468
- { name: "Scroll", value: "scroll" },
645
+ { name: "Close", value: "close" },
646
+ { name: "Full: Rent \u2192 Navigate \u2192 Screenshot", value: "full" },
647
+ { name: "My Sessions", value: "my_sessions" },
648
+ { name: "Navigate", value: "navigate" },
649
+ { name: "Rent", value: "rent" },
469
650
  { name: "Screenshot", value: "screenshot" },
651
+ { name: "Screenshot in Geo", value: "screenshotGeo" },
652
+ { name: "Scroll", value: "scroll" },
653
+ { name: "Search", value: "search" },
470
654
  { name: "Snapshot", value: "snapshot" },
655
+ { name: "Stop Session", value: "stop_session" },
656
+ { name: "Type", value: "type" },
657
+ { name: "Upload", value: "upload" },
471
658
  { name: "Wait", value: "wait" },
472
659
  { name: "Wait for Selector", value: "waitForSelector" },
473
- { name: "Upload", value: "upload" },
474
- { name: "Close", value: "close" },
475
- { name: "Full: Rent \u2192 Navigate \u2192 Screenshot", value: "full" },
476
- { name: "My Sessions", value: "my_sessions" },
477
- { name: "Stop Session", value: "stop_session" }
660
+ // Contract operations
661
+ { name: "Contract: Assign Executor", value: "contract_assign" },
662
+ { name: "Contract: Call Human", value: "contract_callHuman" },
663
+ { name: "Contract: Comment", value: "contract_comment" },
664
+ { name: "Contract: Create Task", value: "contract_create" },
665
+ { name: "Contract: Get Task", value: "contract_get" },
666
+ { name: "Contract: List My Contracts", value: "contract_list" },
667
+ { name: "Contract: List Tasks in Contract", value: "contract_tasks" },
668
+ { name: "Contract: My Assigned Events", value: "contract_myEvents" },
669
+ { name: "Contract: Poll Notifications", value: "contract_poll" },
670
+ { name: "Contract: Progress Report", value: "contract_progress" },
671
+ { name: "Contract: Update Status", value: "contract_setStatus" }
478
672
  ]
479
673
  },
480
674
  // === Rent: rental parameters ===
@@ -492,7 +686,7 @@ var BrowserCeki = class {
492
686
  type: "string",
493
687
  default: "",
494
688
  placeholder: "RU, EE, US\u2026",
495
- displayOptions: { show: { operation: ["search"] } }
689
+ displayOptions: { show: { operation: ["search", "captchaScrape", "screenshotGeo"] } }
496
690
  },
497
691
  {
498
692
  displayName: "Max $/min",
@@ -500,17 +694,17 @@ var BrowserCeki = class {
500
694
  type: "number",
501
695
  typeOptions: { numberPrecision: 4 },
502
696
  default: 0.02,
503
- displayOptions: { show: { operation: ["search"] } }
697
+ displayOptions: { show: { operation: ["search", "captchaScrape", "screenshotGeo"] } }
504
698
  },
505
699
  {
506
- displayName: "Min rating",
700
+ displayName: "Min Rating",
507
701
  name: "minRating",
508
702
  type: "number",
509
703
  default: 0,
510
- displayOptions: { show: { operation: ["search"] } }
704
+ displayOptions: { show: { operation: ["search", "captchaScrape", "screenshotGeo"] } }
511
705
  },
512
706
  {
513
- displayName: "Profile mode",
707
+ displayName: "Profile Mode",
514
708
  name: "mode",
515
709
  type: "options",
516
710
  default: "incognito",
@@ -529,13 +723,211 @@ var BrowserCeki = class {
529
723
  displayOptions: { show: { operation: ["navigate", "full"] } }
530
724
  },
531
725
  {
532
- displayName: "Demo mode (no browser needed)",
726
+ displayName: "Demo Mode (No Browser Needed)",
533
727
  name: "demoMode",
534
728
  type: "boolean",
535
729
  default: true,
536
- description: "Skip actual browser rent and generate demo output",
730
+ description: "Whether to skip actual browser rent and generate demo output",
537
731
  displayOptions: { show: { operation: ["full"] } }
538
732
  },
733
+ // === Captcha-protected Scrape params ===
734
+ {
735
+ displayName: "Captcha URL",
736
+ name: "url",
737
+ type: "string",
738
+ default: "https://example.com",
739
+ required: true,
740
+ description: "Page protected by anti-bot / captcha",
741
+ displayOptions: { show: { operation: ["captchaScrape"] } }
742
+ },
743
+ {
744
+ displayName: "Geo",
745
+ name: "geo",
746
+ type: "string",
747
+ default: "RU",
748
+ placeholder: "RU, EE, US\u2026",
749
+ displayOptions: { show: { operation: ["captchaScrape"] } }
750
+ },
751
+ {
752
+ displayName: "Max $/min",
753
+ name: "maxPrice",
754
+ type: "number",
755
+ typeOptions: { numberPrecision: 4 },
756
+ default: 0.02,
757
+ displayOptions: { show: { operation: ["captchaScrape"] } }
758
+ },
759
+ {
760
+ displayName: "Wait for Selector",
761
+ name: "waitSelector",
762
+ type: "string",
763
+ default: "",
764
+ placeholder: "CSS selector (optional)",
765
+ description: "Wait until this selector appears in the DOM",
766
+ displayOptions: { show: { operation: ["captchaScrape"] } }
767
+ },
768
+ {
769
+ displayName: "Wait Timeout (ms)",
770
+ name: "waitTimeout",
771
+ type: "number",
772
+ default: 3e4,
773
+ displayOptions: { show: { operation: ["captchaScrape"] } }
774
+ },
775
+ {
776
+ displayName: "Extract HTML",
777
+ name: "extractHtml",
778
+ type: "boolean",
779
+ default: true,
780
+ displayOptions: { show: { operation: ["captchaScrape"] } }
781
+ },
782
+ {
783
+ displayName: "HTML Selector",
784
+ name: "htmlSelector",
785
+ type: "string",
786
+ default: "body",
787
+ placeholder: 'CSS selector or "body"',
788
+ description: "OuterHTML of this selector is returned as html",
789
+ displayOptions: { show: { operation: ["captchaScrape"] } }
790
+ },
791
+ {
792
+ displayName: "Full Page Screenshot",
793
+ name: "fullPage",
794
+ type: "boolean",
795
+ default: false,
796
+ displayOptions: { show: { operation: ["captchaScrape"] } }
797
+ },
798
+ // === Screenshot in Geo params ===
799
+ {
800
+ displayName: "Geo URL",
801
+ name: "url",
802
+ type: "string",
803
+ default: "https://ifconfig.me",
804
+ required: true,
805
+ displayOptions: { show: { operation: ["screenshotGeo"] } }
806
+ },
807
+ {
808
+ displayName: "Geo",
809
+ name: "geo",
810
+ type: "string",
811
+ default: "RU",
812
+ placeholder: "RU, EE, US\u2026",
813
+ displayOptions: { show: { operation: ["screenshotGeo"] } }
814
+ },
815
+ {
816
+ displayName: "Full Page Screenshot",
817
+ name: "fullPage",
818
+ type: "boolean",
819
+ default: false,
820
+ displayOptions: { show: { operation: ["screenshotGeo"] } }
821
+ },
822
+ {
823
+ displayName: "Max $/min",
824
+ name: "maxPrice",
825
+ type: "number",
826
+ default: 0.02,
827
+ displayOptions: { show: { operation: ["screenshotGeo"] } }
828
+ },
829
+ // === Contract params ===
830
+ {
831
+ displayName: "Contract ID",
832
+ name: "contractId",
833
+ type: "number",
834
+ default: 0,
835
+ description: "Ceki contract ID",
836
+ displayOptions: { show: { operation: ["contract_tasks", "contract_create"] } }
837
+ },
838
+ {
839
+ displayName: "Event ID",
840
+ name: "eventId",
841
+ type: "number",
842
+ default: 0,
843
+ description: "Task / event ID (KalEvent)",
844
+ displayOptions: {
845
+ show: { operation: ["contract_get", "contract_assign", "contract_setStatus", "contract_comment", "contract_progress", "contract_callHuman"] }
846
+ }
847
+ },
848
+ {
849
+ displayName: "Label",
850
+ name: "label",
851
+ type: "string",
852
+ default: "",
853
+ required: true,
854
+ displayOptions: { show: { operation: ["contract_create"] } }
855
+ },
856
+ {
857
+ displayName: "Description",
858
+ name: "description",
859
+ type: "string",
860
+ typeOptions: { rows: 4 },
861
+ default: "",
862
+ displayOptions: { show: { operation: ["contract_create", "contract_comment"] } }
863
+ },
864
+ {
865
+ displayName: "Executor Type",
866
+ name: "benefitableType",
867
+ type: "options",
868
+ default: "agent",
869
+ options: [
870
+ { name: "Agent", value: "agent" },
871
+ { name: "User (Human)", value: "user" }
872
+ ],
873
+ displayOptions: { show: { operation: ["contract_create", "contract_assign"] } }
874
+ },
875
+ {
876
+ displayName: "Executor ID",
877
+ name: "benefitableValue",
878
+ type: "number",
879
+ default: 0,
880
+ description: "Agent ID or user ID of the executor",
881
+ displayOptions: { show: { operation: ["contract_create", "contract_assign"] } }
882
+ },
883
+ {
884
+ displayName: "Status",
885
+ name: "status",
886
+ type: "options",
887
+ options: [
888
+ { name: "100 \xB7 Backlog", value: 100 },
889
+ { name: "200 \xB7 Hand (Assigned)", value: 200 },
890
+ { name: "222 \xB7 Hand Done", value: 222 },
891
+ { name: "300 \xB7 QA", value: 300 },
892
+ { name: "350 \xB7 QA Done", value: 350 },
893
+ { name: "499 \xB7 Reviewer", value: 499 }
894
+ ],
895
+ default: 200,
896
+ displayOptions: { show: { operation: ["contract_create", "contract_setStatus", "contract_progress"] } }
897
+ },
898
+ {
899
+ displayName: "Progress Description",
900
+ name: "progressDesc",
901
+ type: "string",
902
+ typeOptions: { rows: 4 },
903
+ default: "",
904
+ required: true,
905
+ description: "Body of the progress comment (does not overwrite the task spec)",
906
+ displayOptions: { show: { operation: ["contract_progress"] } }
907
+ },
908
+ {
909
+ displayName: "Call Kind",
910
+ name: "callKind",
911
+ type: "options",
912
+ default: "review",
913
+ options: [
914
+ { name: "Input (Need Clarification)", value: "input" },
915
+ { name: "Review (Done, Take a Look)", value: "review" },
916
+ { name: "Stuck (Blocked)", value: "stuck" }
917
+ ],
918
+ description: "Type of escalation to a human (the call-human action)",
919
+ displayOptions: { show: { operation: ["contract_callHuman"] } }
920
+ },
921
+ {
922
+ displayName: "Message",
923
+ name: "callDesc",
924
+ type: "string",
925
+ typeOptions: { rows: 4 },
926
+ default: "",
927
+ required: true,
928
+ description: "What to tell the human \u2014 context, question, or what was done",
929
+ displayOptions: { show: { operation: ["contract_callHuman"] } }
930
+ },
539
931
  // === Operations: session_id ===
540
932
  {
541
933
  displayName: "Session ID",
@@ -653,6 +1045,39 @@ var BrowserCeki = class {
653
1045
  const out = [];
654
1046
  const creds = await this.getCredentials("cekiApi");
655
1047
  const token = creds.token;
1048
+ const touchedSessions = /* @__PURE__ */ new Set();
1049
+ const _stopSession = async (sid) => {
1050
+ await new Promise((resolve) => {
1051
+ const stopWs = new WebSocket("wss://browser.ceki.me/ws/agent", ["bearer." + token]);
1052
+ const _t = AbortSignal.timeout(1e4);
1053
+ const _done = () => {
1054
+ try {
1055
+ stopWs.close();
1056
+ } catch {
1057
+ }
1058
+ resolve();
1059
+ };
1060
+ _t.addEventListener("abort", _done, { once: true });
1061
+ stopWs.onopen = () => {
1062
+ try {
1063
+ stopWs.send(JSON.stringify({ type: "stop", session_id: sid, reason: "n8n error" }));
1064
+ } catch {
1065
+ }
1066
+ };
1067
+ stopWs.onmessage = (ev) => {
1068
+ try {
1069
+ const m = JSON.parse(ev.data);
1070
+ if (m.type === "session_ended") {
1071
+ _t.removeEventListener("abort", _done);
1072
+ _done();
1073
+ }
1074
+ } catch {
1075
+ }
1076
+ };
1077
+ stopWs.onerror = _done;
1078
+ stopWs.onclose = _done;
1079
+ });
1080
+ };
656
1081
  const resolveSid = async (i, client) => {
657
1082
  const scheduleId = this.getNodeParameter("scheduleId", i);
658
1083
  if (scheduleId) return scheduleId;
@@ -662,335 +1087,531 @@ var BrowserCeki = class {
662
1087
  geo: geo || void 0,
663
1088
  max_price_per_min: maxPrice
664
1089
  });
665
- if (!list.length) throw new Error("No browsers found by filters");
1090
+ if (!list.length) throw new import_n8n_workflow.NodeOperationError(this.getNode(), "No browsers found by filters");
666
1091
  return list[0].schedule_id;
667
1092
  };
668
- for (let i = 0; i < items.length; i++) {
669
- const op = this.getNodeParameter("operation", i);
670
- const client = new CekiClient(token);
671
- let browser;
672
- let needFullClose = false;
673
- try {
674
- await client.connect();
675
- if (op === "rent") {
676
- try {
677
- const sid2 = await resolveSid(i, client);
678
- const mode = this.getNodeParameter("mode", i);
679
- browser = await client.rent(sid2, { mode });
680
- out.push({
681
- json: { session_id: browser.sessionId, schedule_id: sid2, mode }
682
- });
683
- } catch (e) {
684
- throw new Error(`Rent failed: ${e.message}`);
685
- } finally {
686
- await client.disconnect();
1093
+ let _execErr = null;
1094
+ try {
1095
+ for (let i = 0; i < items.length; i++) {
1096
+ const op = this.getNodeParameter("operation", i);
1097
+ const client = new CekiClient(token);
1098
+ let browser;
1099
+ let needFullClose = false;
1100
+ try {
1101
+ await client.connect();
1102
+ if (op === "rent") {
1103
+ try {
1104
+ const sid2 = await resolveSid(i, client);
1105
+ const mode = this.getNodeParameter("mode", i);
1106
+ browser = await client.rent(sid2, { mode });
1107
+ out.push({
1108
+ json: { session_id: browser.sessionId, schedule_id: sid2, mode },
1109
+ pairedItem: { item: i }
1110
+ });
1111
+ } catch (e) {
1112
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Rent failed" });
1113
+ } finally {
1114
+ await client.disconnect();
1115
+ }
1116
+ continue;
687
1117
  }
688
- continue;
689
- }
690
- if (op === "search") {
691
- try {
692
- const geo = this.getNodeParameter("geo", i);
693
- const maxPrice = this.getNodeParameter("maxPrice", i);
694
- const list = await client.search({
695
- geo: geo || void 0,
696
- max_price_per_min: maxPrice
697
- });
698
- out.push({ json: { browsers: list, count: list.length } });
699
- } catch (e) {
700
- throw new Error(`Search failed: ${e.message}`);
701
- } finally {
702
- await client.disconnect();
1118
+ if (op === "search") {
1119
+ try {
1120
+ const geo = this.getNodeParameter("geo", i);
1121
+ const maxPrice = this.getNodeParameter("maxPrice", i);
1122
+ const list = await client.search({
1123
+ geo: geo || void 0,
1124
+ max_price_per_min: maxPrice
1125
+ });
1126
+ out.push({ json: { browsers: list, count: list.length }, pairedItem: { item: i } });
1127
+ } catch (e) {
1128
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Search failed" });
1129
+ } finally {
1130
+ await client.disconnect();
1131
+ }
1132
+ continue;
703
1133
  }
704
- continue;
705
- }
706
- if (op === "my_sessions") {
707
- try {
708
- const resp = await fetch("https://api.ceki.me/api/agent/sessions", {
709
- headers: { Authorization: `Bearer ${token}` }
710
- });
711
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
712
- const body = await resp.json();
713
- const sessions = body.data ?? [];
714
- out.push({ json: { sessions, count: sessions.length } });
715
- } catch (e) {
716
- throw new Error(`My sessions failed: ${e.message}`);
1134
+ if (op === "my_sessions") {
1135
+ try {
1136
+ const resp = await fetch("https://api.ceki.me/api/agent/sessions", {
1137
+ headers: { Authorization: `Bearer ${token}` }
1138
+ });
1139
+ if (!resp.ok) throw new import_n8n_workflow.NodeApiError(this.getNode(), void 0, { message: `HTTP ${resp.status}`, httpCode: `${resp.status}` });
1140
+ const body = await resp.json();
1141
+ const sessions = body.data ?? [];
1142
+ out.push({ json: { sessions, count: sessions.length }, pairedItem: { item: i } });
1143
+ } catch (e) {
1144
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "My sessions failed" });
1145
+ }
1146
+ continue;
717
1147
  }
718
- continue;
719
- }
720
- if (op === "stop_session") {
721
- const sessionId2 = this.getNodeParameter("sessionId", i);
722
- try {
723
- await new Promise((resolve, reject) => {
724
- const ws = new WebSocket("wss://browser.ceki.me/ws/agent", [`bearer.${token}`]);
725
- const t = AbortSignal.timeout(15e3);
726
- t.addEventListener("abort", () => {
727
- try {
728
- ws.close();
729
- } catch {
730
- }
731
- reject(new Error("Stop session timed out"));
732
- }, { once: true });
733
- ws.onopen = () => {
734
- ws.send(JSON.stringify({ type: "stop", session_id: sessionId2, reason: "n8n stop_session" }));
735
- };
736
- ws.onmessage = (ev) => {
737
- const msg = JSON.parse(ev.data);
738
- if (msg.type === "session_ended" || msg.type === "pong") {
1148
+ if (op === "stop_session") {
1149
+ const sessionId2 = this.getNodeParameter("sessionId", i);
1150
+ try {
1151
+ await new Promise((resolve, reject) => {
1152
+ const ws = new WebSocket("wss://browser.ceki.me/ws/agent", [`bearer.${token}`]);
1153
+ const t = AbortSignal.timeout(15e3);
1154
+ t.addEventListener("abort", () => {
739
1155
  try {
740
1156
  ws.close();
741
1157
  } catch {
742
1158
  }
743
- resolve();
744
- }
745
- };
746
- ws.onerror = () => {
747
- reject(new Error("WebSocket connection failed"));
748
- };
749
- });
750
- out.push({ json: { stopped: true, session_id: sessionId2 } });
751
- } catch (e) {
752
- throw new Error(`Stop session failed: ${e.message}`);
753
- }
754
- continue;
755
- }
756
- if (op === "full") {
757
- const demoMode = this.getNodeParameter("demoMode", i);
758
- let sid2;
759
- if (demoMode) {
760
- sid2 = 99999;
761
- } else {
762
- try {
763
- sid2 = await resolveSid(i, client);
1159
+ reject(new import_n8n_workflow.NodeApiError(this.getNode(), void 0, { message: "Stop session timed out" }));
1160
+ }, { once: true });
1161
+ ws.onopen = () => {
1162
+ ws.send(JSON.stringify({ type: "stop", session_id: sessionId2, reason: "n8n stop_session" }));
1163
+ };
1164
+ ws.onmessage = (ev) => {
1165
+ const msg = JSON.parse(ev.data);
1166
+ if (msg.type === "session_ended" || msg.type === "pong") {
1167
+ try {
1168
+ ws.close();
1169
+ } catch {
1170
+ }
1171
+ resolve();
1172
+ }
1173
+ };
1174
+ ws.onerror = () => {
1175
+ reject(new import_n8n_workflow.NodeApiError(this.getNode(), void 0, { message: "WebSocket connection failed" }));
1176
+ };
1177
+ });
1178
+ out.push({ json: { stopped: true, session_id: sessionId2 }, pairedItem: { item: i } });
764
1179
  } catch (e) {
765
- throw new Error(`Full: resolve schedule failed: ${e.message}`);
1180
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Stop session failed" });
766
1181
  }
1182
+ continue;
767
1183
  }
768
- const mode = this.getNodeParameter("mode", i);
769
- const url = this.getNodeParameter("url", i);
770
- const ms = this.getNodeParameter("ms", i);
771
- let session_id;
772
- let binary;
773
- try {
1184
+ if (op === "full") {
1185
+ const demoMode = this.getNodeParameter("demoMode", i);
1186
+ let sid2;
774
1187
  if (demoMode) {
775
- session_id = "demo-" + Date.now();
776
- const demoPng = Buffer.from("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", "base64");
777
- binary = await this.helpers.prepareBinaryData(demoPng, "screenshot.png", "image/png");
1188
+ sid2 = 99999;
778
1189
  } else {
779
1190
  try {
780
- browser = await client.rent(sid2, { mode });
781
- session_id = browser.sessionId;
782
- await browser.navigate(url);
783
- await sleep(ms);
784
- const shot = await browser.screenshot({ format: "png", fullPage: true });
785
- const data = shot.toString("base64");
786
- binary = await this.helpers.prepareBinaryData(Buffer.from(data, "base64"), "screenshot.png", "image/png");
787
- await browser.close();
1191
+ sid2 = await resolveSid(i, client);
788
1192
  } catch (e) {
789
- throw new Error(`Full op failed at step "${browser?.sessionId ? "navigate/screenshot" : "rent"}": ${e.message}`);
1193
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Full: resolve schedule failed" });
790
1194
  }
791
1195
  }
792
- out.push({
793
- json: {
794
- session_id,
795
- schedule_id: sid2,
796
- mode,
797
- url,
798
- waited: ms
799
- },
800
- binary: { data: binary }
801
- });
802
- } finally {
803
- await client.close().catch(() => {
804
- });
805
- }
806
- continue;
807
- }
808
- const sessionId = this.getNodeParameter("sessionId", i);
809
- try {
810
- browser = await client.resume(sessionId);
811
- } catch (e) {
812
- throw new Error(`Resume session "${sessionId}" failed: ${e.message}. Session may have expired or still be in grace.`);
813
- }
814
- const sid = browser.sessionId;
815
- switch (op) {
816
- case "navigate": {
817
- try {
818
- const url = this.getNodeParameter("url", i);
819
- await browser.navigate(url);
820
- out.push({ json: { session_id: sid, url } });
821
- } catch (e) {
822
- throw new Error(`Navigate failed: ${e.message}`);
823
- }
824
- break;
825
- }
826
- case "click": {
827
- try {
828
- const x2 = this.getNodeParameter("x", i);
829
- const y2 = this.getNodeParameter("y", i);
830
- await browser.click(x2, y2);
831
- out.push({ json: { session_id: sid, clicked: [x2, y2] } });
832
- } catch (e) {
833
- throw new Error(`Click at (${x},${y}) failed: ${e.message}`);
834
- }
835
- break;
836
- }
837
- case "type": {
1196
+ const mode = this.getNodeParameter("mode", i);
1197
+ const url = this.getNodeParameter("url", i);
1198
+ const ms = this.getNodeParameter("ms", i);
1199
+ let session_id;
1200
+ let binary;
838
1201
  try {
839
- const text = this.getNodeParameter("text", i);
840
- await browser.type(text);
841
- out.push({ json: { session_id: sid, typed: text } });
842
- } catch (e) {
843
- throw new Error(`Type failed: ${e.message}`);
1202
+ if (demoMode) {
1203
+ session_id = "demo-" + Date.now();
1204
+ const demoPng = Buffer.from("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", "base64");
1205
+ binary = await this.helpers.prepareBinaryData(demoPng, "screenshot.png", "image/png");
1206
+ } else {
1207
+ try {
1208
+ browser = await client.rent(sid2, { mode });
1209
+ session_id = browser.sessionId;
1210
+ await browser.navigate(url);
1211
+ await sleep(ms);
1212
+ const shot = await browser.screenshot({ format: "png", fullPage: true });
1213
+ const data = shot.toString("base64");
1214
+ binary = await this.helpers.prepareBinaryData(Buffer.from(data, "base64"), "screenshot.png", "image/png");
1215
+ await browser.close();
1216
+ } catch (e) {
1217
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Full op failed" });
1218
+ }
1219
+ }
1220
+ out.push({
1221
+ json: {
1222
+ session_id,
1223
+ schedule_id: sid2,
1224
+ mode,
1225
+ url,
1226
+ waited: ms
1227
+ },
1228
+ binary: { data: binary },
1229
+ pairedItem: { item: i }
1230
+ });
1231
+ } finally {
1232
+ await client.close().catch(() => {
1233
+ });
844
1234
  }
845
- break;
1235
+ continue;
846
1236
  }
847
- case "scroll": {
1237
+ if (op === "captchaScrape") {
1238
+ const url = this.getNodeParameter("url", i);
1239
+ const geo = this.getNodeParameter("geo", i);
1240
+ const maxPrice = this.getNodeParameter("maxPrice", i);
1241
+ const waitSelector = this.getNodeParameter("waitSelector", i) || "";
1242
+ const waitTimeout = this.getNodeParameter("waitTimeout", i);
1243
+ const extractHtmlFlag = this.getNodeParameter("extractHtml", i);
1244
+ const htmlSelector = this.getNodeParameter("htmlSelector", i) || "body";
1245
+ const fullPage = this.getNodeParameter("fullPage", i);
1246
+ let scheduleId = 0;
848
1247
  try {
849
- const deltaY = this.getNodeParameter("deltaY", i);
850
- await browser.scroll(deltaY);
851
- out.push({ json: { session_id: sid, scrolled: deltaY } });
1248
+ const list = await client.search({ geo: geo || void 0, max_price_per_min: maxPrice });
1249
+ if (!list.length) throw new import_n8n_workflow.NodeOperationError(this.getNode(), "No browsers in geo " + (geo || "*"));
1250
+ scheduleId = list[0].schedule_id;
1251
+ browser = await client.rent(scheduleId);
1252
+ touchedSessions.add(browser.sessionId);
1253
+ await browser.navigate(url);
1254
+ if (waitSelector) {
1255
+ await waitForSelector(browser, waitSelector, waitTimeout);
1256
+ }
1257
+ const shot = await browser.screenshot({ format: "base64", fullPage });
1258
+ const data = shot.data ?? (shot instanceof Buffer ? shot.toString("base64") : "");
1259
+ const binary = await this.helpers.prepareBinaryData(
1260
+ Buffer.from(data, "base64"),
1261
+ "captcha-scrape.png",
1262
+ "image/png"
1263
+ );
1264
+ const json = {
1265
+ url,
1266
+ geo,
1267
+ schedule_id: scheduleId
1268
+ };
1269
+ if (extractHtmlFlag) {
1270
+ json.html = await extractHtml(browser, htmlSelector);
1271
+ }
1272
+ out.push({ json, binary: { data: binary }, pairedItem: { item: i } });
852
1273
  } catch (e) {
853
- throw new Error(`Scroll failed: ${e.message}`);
1274
+ if (e instanceof import_n8n_workflow.NodeOperationError || e instanceof import_n8n_workflow.NodeApiError) throw e;
1275
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Captcha-protected scrape failed" });
1276
+ } finally {
1277
+ if (browser) {
1278
+ try {
1279
+ await browser.close();
1280
+ } catch {
1281
+ }
1282
+ }
1283
+ try {
1284
+ await client.close();
1285
+ } catch {
1286
+ }
854
1287
  }
855
- break;
1288
+ continue;
856
1289
  }
857
- case "screenshot": {
1290
+ if (op === "screenshotGeo") {
1291
+ const url = this.getNodeParameter("url", i);
1292
+ const geo = this.getNodeParameter("geo", i);
1293
+ const fullPage = this.getNodeParameter("fullPage", i);
1294
+ const maxPrice = this.getNodeParameter("maxPrice", i);
858
1295
  try {
859
- const format = this.getNodeParameter("format", i);
860
- const fullPage = this.getNodeParameter("fullPage", i);
861
- const shot = await browser.screenshot({ format, fullPage });
862
- const data = format === "base64" ? shot.data : shot.toString("base64");
1296
+ const list = await client.search({ geo, max_price_per_min: maxPrice });
1297
+ if (!list.length) throw new import_n8n_workflow.NodeOperationError(this.getNode(), "No browsers in geo " + geo);
1298
+ browser = await client.rent(list[0].schedule_id);
1299
+ touchedSessions.add(browser.sessionId);
1300
+ await browser.navigate(url);
1301
+ const shot = await browser.screenshot({ format: "base64", fullPage });
863
1302
  const binary = await this.helpers.prepareBinaryData(
864
- Buffer.from(data, "base64"),
1303
+ Buffer.from(shot.data, "base64"),
865
1304
  "screenshot.png",
866
1305
  "image/png"
867
1306
  );
868
- out.push({ json: { session_id: sid }, binary: { data: binary } });
1307
+ out.push({
1308
+ json: { url, geo, schedule_id: list[0].schedule_id },
1309
+ binary: { data: binary },
1310
+ pairedItem: { item: i }
1311
+ });
869
1312
  } catch (e) {
870
- throw new Error(`Screenshot failed: ${e.message}`);
1313
+ if (e instanceof import_n8n_workflow.NodeOperationError || e instanceof import_n8n_workflow.NodeApiError) throw e;
1314
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Screenshot in geo " + geo + " failed" });
1315
+ } finally {
1316
+ if (browser) {
1317
+ try {
1318
+ await browser.close();
1319
+ } catch {
1320
+ }
1321
+ }
1322
+ try {
1323
+ await client.close();
1324
+ } catch {
1325
+ }
871
1326
  }
872
- break;
1327
+ continue;
873
1328
  }
874
- case "snapshot": {
1329
+ if (op.startsWith("contract_")) {
1330
+ const contractClient = new ContractClient(token);
1331
+ const pop = op.replace("contract_", "");
1332
+ let result;
875
1333
  try {
876
- const snap = await browser.snapshot();
1334
+ switch (pop) {
1335
+ case "list":
1336
+ result = await contractClient.listContracts();
1337
+ break;
1338
+ case "tasks": {
1339
+ const contractId = this.getNodeParameter("contractId", i);
1340
+ result = await contractClient.tasks(contractId);
1341
+ break;
1342
+ }
1343
+ case "get": {
1344
+ const eventId = this.getNodeParameter("eventId", i);
1345
+ result = await contractClient.task(eventId);
1346
+ break;
1347
+ }
1348
+ case "myEvents":
1349
+ result = await contractClient.myEvents();
1350
+ break;
1351
+ case "create": {
1352
+ const contractId = this.getNodeParameter("contractId", i);
1353
+ const label = this.getNodeParameter("label", i);
1354
+ const description = this.getNodeParameter("description", i) || "";
1355
+ const status = this.getNodeParameter("status", i);
1356
+ const bType = this.getNodeParameter("benefitableType", i);
1357
+ const bValue = this.getNodeParameter("benefitableValue", i);
1358
+ result = await contractClient.create(contractId, {
1359
+ label,
1360
+ description: description || void 0,
1361
+ status,
1362
+ benefitable: bValue ? bType + ":" + bValue : void 0
1363
+ });
1364
+ break;
1365
+ }
1366
+ case "assign": {
1367
+ const eventId = this.getNodeParameter("eventId", i);
1368
+ const bType = this.getNodeParameter("benefitableType", i);
1369
+ const bValue = this.getNodeParameter("benefitableValue", i);
1370
+ result = await contractClient.assign(eventId, bType + ":" + bValue);
1371
+ break;
1372
+ }
1373
+ case "setStatus": {
1374
+ const eventId = this.getNodeParameter("eventId", i);
1375
+ const status = this.getNodeParameter("status", i);
1376
+ result = await contractClient.setStatus(eventId, status);
1377
+ break;
1378
+ }
1379
+ case "comment": {
1380
+ const eventId = this.getNodeParameter("eventId", i);
1381
+ const text = this.getNodeParameter("description", i) || "";
1382
+ result = await contractClient.comment(eventId, text);
1383
+ break;
1384
+ }
1385
+ case "progress": {
1386
+ const eventId = this.getNodeParameter("eventId", i);
1387
+ const status = this.getNodeParameter("status", i);
1388
+ const desc = this.getNodeParameter("progressDesc", i);
1389
+ result = await contractClient.progress(eventId, status, desc);
1390
+ break;
1391
+ }
1392
+ case "callHuman": {
1393
+ const eventId = this.getNodeParameter("eventId", i);
1394
+ const kind = this.getNodeParameter("callKind", i);
1395
+ const msg = this.getNodeParameter("callDesc", i);
1396
+ result = await contractClient.callHuman(eventId, kind, msg);
1397
+ break;
1398
+ }
1399
+ case "poll":
1400
+ result = await contractClient.poll();
1401
+ break;
1402
+ default:
1403
+ throw new import_n8n_workflow.NodeOperationError(this.getNode(), "Unknown contract operation: " + pop);
1404
+ }
877
1405
  out.push({
878
- json: { session_id: sid, screenshot: snap.screenshot }
1406
+ json: result,
1407
+ pairedItem: { item: i }
879
1408
  });
880
1409
  } catch (e) {
881
- throw new Error(`Snapshot failed: ${e.message}`);
1410
+ if (e instanceof import_n8n_workflow.NodeOperationError || e instanceof import_n8n_workflow.NodeApiError) throw e;
1411
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Contract " + pop + " failed" });
882
1412
  }
883
- break;
1413
+ continue;
884
1414
  }
885
- case "wait": {
886
- const ms = this.getNodeParameter("ms", i);
887
- await sleep(ms);
888
- out.push({ json: { session_id: sid, waited: ms } });
889
- break;
1415
+ const sessionId = this.getNodeParameter("sessionId", i);
1416
+ try {
1417
+ browser = await client.resume(sessionId);
1418
+ } catch (e) {
1419
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: `Resume session "${sessionId}" failed. Session may have expired or still be in grace.` });
890
1420
  }
891
- case "waitForSelector": {
892
- const selector = this.getNodeParameter("waitSelector", i);
893
- const timeout = this.getNodeParameter("waitTimeout", i);
894
- const expr = `!!document.querySelector(${JSON.stringify(selector)})`;
895
- const deadline = Date.now() + timeout;
896
- let ok = false;
897
- let lastErr = null;
898
- while (Date.now() < deadline) {
1421
+ const sid = browser.sessionId;
1422
+ switch (op) {
1423
+ case "navigate": {
899
1424
  try {
900
- const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
901
- if (res?.result?.value === true) {
902
- ok = true;
903
- break;
904
- }
1425
+ const url = this.getNodeParameter("url", i);
1426
+ await browser.navigate(url);
1427
+ out.push({ json: { session_id: sid, url }, pairedItem: { item: i } });
905
1428
  } catch (e) {
906
- lastErr = e;
1429
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Navigate failed" });
907
1430
  }
908
- await sleep(500);
1431
+ break;
909
1432
  }
910
- if (!ok) {
911
- throw new Error(
912
- `waitForSelector("${selector}") timed out after ${timeout}ms${lastErr ? `: ${lastErr.message}` : ""}`
913
- );
1433
+ case "click": {
1434
+ try {
1435
+ const x = this.getNodeParameter("x", i);
1436
+ const y = this.getNodeParameter("y", i);
1437
+ await browser.click(x, y);
1438
+ out.push({ json: { session_id: sid, clicked: [x, y] }, pairedItem: { item: i } });
1439
+ } catch (e) {
1440
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Click failed" });
1441
+ }
1442
+ break;
914
1443
  }
915
- out.push({ json: { session_id: sid, selector, found: true } });
916
- break;
917
- }
918
- case "upload": {
919
- try {
920
- const selector = this.getNodeParameter("selector", i);
921
- const bpn = this.getNodeParameter("binaryPropertyName", i);
922
- const bin = items[i].binary?.[bpn];
923
- if (!bin) throw new Error(`Binary property "${bpn}" not found on input`);
924
- const stream = await this.helpers.getBinaryStream(bin.id);
925
- const chunks = [];
926
- for await (const c of stream) chunks.push(c);
927
- const buf = Buffer.concat(chunks);
928
- const res = await browser.upload(selector, buf);
929
- out.push({ json: { session_id: sid, uploaded: res } });
930
- } catch (e) {
931
- throw new Error(`Upload failed: ${e.message}`);
1444
+ case "type": {
1445
+ try {
1446
+ const text = this.getNodeParameter("text", i);
1447
+ await browser.type(text);
1448
+ out.push({ json: { session_id: sid, typed: text }, pairedItem: { item: i } });
1449
+ } catch (e) {
1450
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Type failed" });
1451
+ }
1452
+ break;
932
1453
  }
933
- break;
934
- }
935
- case "close": {
936
- try {
937
- await browser.close().catch(() => {
938
- });
939
- } catch {
1454
+ case "scroll": {
1455
+ try {
1456
+ const deltaY = this.getNodeParameter("deltaY", i);
1457
+ await browser.scroll(deltaY);
1458
+ out.push({ json: { session_id: sid, scrolled: deltaY }, pairedItem: { item: i } });
1459
+ } catch (e) {
1460
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Scroll failed" });
1461
+ }
1462
+ break;
1463
+ }
1464
+ case "screenshot": {
1465
+ try {
1466
+ const format = this.getNodeParameter("format", i);
1467
+ const fullPage = this.getNodeParameter("fullPage", i);
1468
+ const shot = await browser.screenshot({ format, fullPage });
1469
+ const data = format === "base64" ? shot.data : shot.toString("base64");
1470
+ const binary = await this.helpers.prepareBinaryData(
1471
+ Buffer.from(data, "base64"),
1472
+ "screenshot.png",
1473
+ "image/png"
1474
+ );
1475
+ out.push({ json: { session_id: sid }, binary: { data: binary }, pairedItem: { item: i } });
1476
+ } catch (e) {
1477
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Screenshot failed" });
1478
+ }
1479
+ break;
1480
+ }
1481
+ case "snapshot": {
1482
+ try {
1483
+ const snap = await browser.snapshot();
1484
+ out.push({
1485
+ json: { session_id: sid, screenshot: snap.screenshot },
1486
+ pairedItem: { item: i }
1487
+ });
1488
+ } catch (e) {
1489
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Snapshot failed" });
1490
+ }
1491
+ break;
940
1492
  }
941
- await new Promise((resolve) => {
942
- const stopWs = new WebSocket("wss://browser.ceki.me/ws/agent", [`bearer.${token}`]);
943
- const abortTimer = AbortSignal.timeout(1e4);
944
- const onTimer = () => {
1493
+ case "wait": {
1494
+ const ms = this.getNodeParameter("ms", i);
1495
+ await sleep(ms);
1496
+ out.push({ json: { session_id: sid, waited: ms }, pairedItem: { item: i } });
1497
+ break;
1498
+ }
1499
+ case "waitForSelector": {
1500
+ const selector = this.getNodeParameter("waitSelector", i);
1501
+ const timeout = this.getNodeParameter("waitTimeout", i);
1502
+ const expr = `!!document.querySelector(${JSON.stringify(selector)})`;
1503
+ const deadline = Date.now() + timeout;
1504
+ let ok = false;
1505
+ let lastErr = null;
1506
+ while (Date.now() < deadline) {
945
1507
  try {
946
- stopWs.close();
947
- } catch {
1508
+ const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
1509
+ if (res?.result?.value === true) {
1510
+ ok = true;
1511
+ break;
1512
+ }
1513
+ } catch (e) {
1514
+ lastErr = e;
948
1515
  }
949
- resolve();
950
- };
951
- abortTimer.addEventListener("abort", onTimer, { once: true });
952
- stopWs.onopen = () => {
953
- stopWs.send(JSON.stringify({ type: "stop", session_id: sessionId, reason: "n8n close" }));
954
- };
955
- stopWs.onmessage = (ev) => {
956
- try {
957
- const msg = JSON.parse(ev.data);
958
- if (msg.type === "session_ended") {
959
- abortTimer.removeEventListener("abort", onTimer);
960
- try {
961
- stopWs.close();
962
- } catch {
1516
+ await sleep(500);
1517
+ }
1518
+ if (!ok) {
1519
+ throw new import_n8n_workflow.NodeOperationError(
1520
+ this.getNode(),
1521
+ `waitForSelector("${selector}") timed out after ${timeout}ms${lastErr ? `: ${lastErr.message}` : ""}`
1522
+ );
1523
+ }
1524
+ out.push({ json: { session_id: sid, selector, found: true }, pairedItem: { item: i } });
1525
+ break;
1526
+ }
1527
+ case "upload": {
1528
+ try {
1529
+ const selector = this.getNodeParameter("selector", i);
1530
+ const bpn = this.getNodeParameter("binaryPropertyName", i);
1531
+ const bin = items[i].binary?.[bpn];
1532
+ if (!bin) throw new import_n8n_workflow.NodeOperationError(this.getNode(), `Binary property "${bpn}" not found on input`);
1533
+ const stream = await this.helpers.getBinaryStream(bin.id);
1534
+ const chunks = [];
1535
+ for await (const c of stream) chunks.push(c);
1536
+ const buf = Buffer.concat(chunks);
1537
+ const res = await browser.upload(selector, buf);
1538
+ out.push({ json: { session_id: sid, uploaded: res }, pairedItem: { item: i } });
1539
+ } catch (e) {
1540
+ throw new import_n8n_workflow.NodeApiError(this.getNode(), e, { message: "Upload failed" });
1541
+ }
1542
+ break;
1543
+ }
1544
+ case "close": {
1545
+ try {
1546
+ await browser.close().catch(() => {
1547
+ });
1548
+ } catch {
1549
+ }
1550
+ await new Promise((resolve) => {
1551
+ const stopWs = new WebSocket("wss://browser.ceki.me/ws/agent", [`bearer.${token}`]);
1552
+ const abortTimer = AbortSignal.timeout(1e4);
1553
+ const onTimer = () => {
1554
+ try {
1555
+ stopWs.close();
1556
+ } catch {
1557
+ }
1558
+ resolve();
1559
+ };
1560
+ abortTimer.addEventListener("abort", onTimer, { once: true });
1561
+ stopWs.onopen = () => {
1562
+ stopWs.send(JSON.stringify({ type: "stop", session_id: sessionId, reason: "n8n close" }));
1563
+ };
1564
+ stopWs.onmessage = (ev) => {
1565
+ try {
1566
+ const msg = JSON.parse(ev.data);
1567
+ if (msg.type === "session_ended") {
1568
+ abortTimer.removeEventListener("abort", onTimer);
1569
+ try {
1570
+ stopWs.close();
1571
+ } catch {
1572
+ }
1573
+ resolve();
963
1574
  }
964
- resolve();
1575
+ } catch {
965
1576
  }
966
- } catch {
967
- }
968
- };
969
- stopWs.onerror = () => {
970
- abortTimer.removeEventListener("abort", onTimer);
971
- resolve();
972
- };
973
- stopWs.onclose = () => {
974
- abortTimer.removeEventListener("abort", onTimer);
975
- resolve();
976
- };
977
- });
978
- out.push({ json: { closed: true, session_id: sessionId } });
979
- break;
1577
+ };
1578
+ stopWs.onerror = () => {
1579
+ abortTimer.removeEventListener("abort", onTimer);
1580
+ resolve();
1581
+ };
1582
+ stopWs.onclose = () => {
1583
+ abortTimer.removeEventListener("abort", onTimer);
1584
+ resolve();
1585
+ };
1586
+ });
1587
+ out.push({ json: { closed: true, session_id: sessionId }, pairedItem: { item: i } });
1588
+ break;
1589
+ }
1590
+ }
1591
+ needFullClose = op === "close";
1592
+ } finally {
1593
+ try {
1594
+ if (needFullClose) {
1595
+ await client.close();
1596
+ } else {
1597
+ await client.disconnect();
1598
+ }
1599
+ } catch {
980
1600
  }
981
1601
  }
982
- needFullClose = op === "close";
983
- } finally {
1602
+ }
1603
+ } catch (e) {
1604
+ _execErr = e;
1605
+ }
1606
+ if (_execErr && touchedSessions.size) {
1607
+ for (const sid of touchedSessions) {
984
1608
  try {
985
- if (needFullClose) {
986
- await client.close();
987
- } else {
988
- await client.disconnect();
989
- }
1609
+ await _stopSession(sid);
990
1610
  } catch {
991
1611
  }
992
1612
  }
993
1613
  }
1614
+ if (_execErr) throw _execErr;
994
1615
  return [out];
995
1616
  }
996
1617
  };