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