@webmcp-bridge/adapter-x 0.5.0 → 0.5.2
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/adapter.d.ts +1 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +3456 -479
- package/dist/adapter.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
package/dist/adapter.js
CHANGED
|
@@ -3,21 +3,24 @@
|
|
|
3
3
|
* It depends on Playwright page evaluation and shared adapter contracts to execute browser-side tool actions.
|
|
4
4
|
*/
|
|
5
5
|
import { buildRequestCaptureInitScript, captureRoutedResponseText, collectTextByTag, joinTextParts, parseNdjsonLines, TemplateCache, } from "@webmcp-bridge/adapter-utils";
|
|
6
|
-
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
|
-
import { basename, extname, isAbsolute, join } from "node:path";
|
|
8
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
9
9
|
const DEFAULT_TIMELINE_LIMIT = 10;
|
|
10
10
|
const MAX_TIMELINE_LIMIT = 20;
|
|
11
11
|
const MAX_READ_PAGE_CACHE_SIZE = 8;
|
|
12
12
|
const DEFAULT_COMPOSE_CONFIRM_TIMEOUT_MS = 10_000;
|
|
13
13
|
const DEFAULT_GROK_RESPONSE_TIMEOUT_MS = 90_000;
|
|
14
|
+
const DEFAULT_ARTICLE_PUBLISH_TIMEOUT_MS = 30_000;
|
|
14
15
|
const DEFAULT_MAX_POST_LENGTH = 280;
|
|
15
16
|
const AUTH_STABILIZE_ATTEMPTS = 6;
|
|
16
17
|
const AUTH_STABILIZE_DELAY_MS = 750;
|
|
17
18
|
const AUTH_WARMUP_TIMEOUT_MS = 12_000;
|
|
18
19
|
const GROK_ARTIFACT_DIR_PREFIX = "webmcp-bridge-grok-";
|
|
19
20
|
const TWEET_MEDIA_ARTIFACT_DIR_PREFIX = "webmcp-bridge-x-media-";
|
|
21
|
+
const ARTICLE_INLINE_IMAGE_MARKER_PREFIX = "[[WEBMCP_INLINE_IMAGE_";
|
|
20
22
|
const ALLOWED_TWEET_MEDIA_HOSTS = new Set(["pbs.twimg.com", "video.twimg.com"]);
|
|
23
|
+
const ALLOWED_X_HOSTS = new Set(["x.com", "www.x.com", "twitter.com", "www.twitter.com"]);
|
|
21
24
|
const CAPTURE_INJECT_SCRIPT = buildRequestCaptureInitScript({
|
|
22
25
|
globalKey: "__WEBMCP_X_CAPTURE__",
|
|
23
26
|
shouldCaptureSource: String.raw `((url) => {
|
|
@@ -32,7 +35,10 @@ const CAPTURE_INJECT_SCRIPT = buildRequestCaptureInitScript({
|
|
|
32
35
|
url.includes("/UserTweets") ||
|
|
33
36
|
url.includes("/UserMedia") ||
|
|
34
37
|
url.includes("/UserTweetsAndReplies") ||
|
|
35
|
-
url.includes("/SearchTimeline")
|
|
38
|
+
url.includes("/SearchTimeline") ||
|
|
39
|
+
url.includes("/ArticleEntitiesSlice") ||
|
|
40
|
+
url.includes("/ArticleRedirectScreenQuery") ||
|
|
41
|
+
url.includes("/UserArticlesTweets")
|
|
36
42
|
)
|
|
37
43
|
);
|
|
38
44
|
})`,
|
|
@@ -47,6 +53,9 @@ const CAPTURE_INJECT_SCRIPT = buildRequestCaptureInitScript({
|
|
|
47
53
|
else if (url.includes("/UserMedia")) op = "UserMedia";
|
|
48
54
|
else if (url.includes("/UserTweets")) op = "UserTweets";
|
|
49
55
|
else if (url.includes("/SearchTimeline")) op = "SearchTimeline";
|
|
56
|
+
else if (url.includes("/ArticleEntitiesSlice")) op = "ArticleEntitiesSlice";
|
|
57
|
+
else if (url.includes("/ArticleRedirectScreenQuery")) op = "ArticleRedirectScreenQuery";
|
|
58
|
+
else if (url.includes("/UserArticlesTweets")) op = "UserArticlesTweets";
|
|
50
59
|
return { ...entry, op };
|
|
51
60
|
})`,
|
|
52
61
|
maxEntries: 80,
|
|
@@ -390,6 +399,26 @@ const TOOL_DEFINITIONS = [
|
|
|
390
399
|
additionalProperties: false,
|
|
391
400
|
},
|
|
392
401
|
},
|
|
402
|
+
{
|
|
403
|
+
name: "tweet.delete",
|
|
404
|
+
description: "Delete one tweet by url or id",
|
|
405
|
+
annotations: {
|
|
406
|
+
readOnlyHint: false,
|
|
407
|
+
},
|
|
408
|
+
inputSchema: {
|
|
409
|
+
type: "object",
|
|
410
|
+
description: "Open a tweet detail page and delete one tweet owned by the authenticated account.",
|
|
411
|
+
properties: {
|
|
412
|
+
url: { type: "string", description: "Tweet URL, for example https://x.com/<user>/status/<id>." },
|
|
413
|
+
id: { type: "string", description: "Tweet id. Used when url is not provided." },
|
|
414
|
+
dryRun: {
|
|
415
|
+
type: "boolean",
|
|
416
|
+
description: "When true, validate delete controls without confirming the destructive action.",
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
additionalProperties: false,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
393
422
|
{
|
|
394
423
|
name: "grok.chat",
|
|
395
424
|
description: "Send one prompt to Grok from the authenticated X session",
|
|
@@ -422,6 +451,280 @@ const TOOL_DEFINITIONS = [
|
|
|
422
451
|
additionalProperties: false,
|
|
423
452
|
},
|
|
424
453
|
},
|
|
454
|
+
{
|
|
455
|
+
name: "article.get",
|
|
456
|
+
description: "Read one X article by public url, edit url, or id",
|
|
457
|
+
inputSchema: {
|
|
458
|
+
type: "object",
|
|
459
|
+
description: "Fetch one X article using public article URL, edit URL, or article id.",
|
|
460
|
+
properties: {
|
|
461
|
+
url: {
|
|
462
|
+
type: "string",
|
|
463
|
+
description: "Public article URL or edit URL.",
|
|
464
|
+
minLength: 1,
|
|
465
|
+
},
|
|
466
|
+
id: {
|
|
467
|
+
type: "string",
|
|
468
|
+
description: "Article id. Used when url is not provided.",
|
|
469
|
+
minLength: 1,
|
|
470
|
+
},
|
|
471
|
+
authorHandle: {
|
|
472
|
+
type: "string",
|
|
473
|
+
description: "Optional article author handle, for example jolestar or @jolestar. Used for profile articles fallback when public article route is unavailable.",
|
|
474
|
+
minLength: 1,
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
additionalProperties: false,
|
|
478
|
+
},
|
|
479
|
+
annotations: {
|
|
480
|
+
readOnlyHint: true,
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: "article.listDrafts",
|
|
485
|
+
description: "List existing X article drafts from the authenticated account",
|
|
486
|
+
inputSchema: {
|
|
487
|
+
type: "object",
|
|
488
|
+
description: "List article drafts owned by the authenticated X account.",
|
|
489
|
+
properties: {},
|
|
490
|
+
additionalProperties: false,
|
|
491
|
+
},
|
|
492
|
+
annotations: {
|
|
493
|
+
readOnlyHint: true,
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "article.getDraft",
|
|
498
|
+
description: "Read one X article draft by id, preview url, or edit url",
|
|
499
|
+
inputSchema: {
|
|
500
|
+
type: "object",
|
|
501
|
+
description: "Fetch one draft from the X article editor using article id, preview URL, or edit URL.",
|
|
502
|
+
properties: {
|
|
503
|
+
url: {
|
|
504
|
+
type: "string",
|
|
505
|
+
description: "Draft preview URL or edit URL.",
|
|
506
|
+
minLength: 1,
|
|
507
|
+
},
|
|
508
|
+
id: {
|
|
509
|
+
type: "string",
|
|
510
|
+
description: "Draft article id. Used when url is not provided.",
|
|
511
|
+
minLength: 1,
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
additionalProperties: false,
|
|
515
|
+
},
|
|
516
|
+
annotations: {
|
|
517
|
+
readOnlyHint: true,
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: "article.draftMarkdown",
|
|
522
|
+
description: "Create one X article draft from a local markdown file",
|
|
523
|
+
inputSchema: {
|
|
524
|
+
type: "object",
|
|
525
|
+
description: "Create one X article draft from a local markdown file. The adapter derives the title from the first markdown heading when title is omitted.",
|
|
526
|
+
properties: {
|
|
527
|
+
markdownPath: {
|
|
528
|
+
type: "string",
|
|
529
|
+
description: "Absolute local file path to the markdown file to draft.",
|
|
530
|
+
minLength: 1,
|
|
531
|
+
"x-uxc-kind": "file-path",
|
|
532
|
+
},
|
|
533
|
+
title: {
|
|
534
|
+
type: "string",
|
|
535
|
+
description: "Optional title override. When omitted, the first markdown heading becomes the article title.",
|
|
536
|
+
minLength: 1,
|
|
537
|
+
},
|
|
538
|
+
coverImagePath: {
|
|
539
|
+
type: "string",
|
|
540
|
+
description: "Optional absolute local image path for the article cover image.",
|
|
541
|
+
minLength: 1,
|
|
542
|
+
"x-uxc-kind": "file-path",
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
required: ["markdownPath"],
|
|
546
|
+
additionalProperties: false,
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "article.upsertDraftMarkdown",
|
|
551
|
+
description: "Create or update one X article draft from a local markdown file",
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: "object",
|
|
554
|
+
description: "Create a new draft when id/url is omitted, or update the existing draft identified by id, preview URL, or edit URL.",
|
|
555
|
+
properties: {
|
|
556
|
+
url: {
|
|
557
|
+
type: "string",
|
|
558
|
+
description: "Draft preview URL or edit URL.",
|
|
559
|
+
minLength: 1,
|
|
560
|
+
},
|
|
561
|
+
id: {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "Draft article id. Used when url is not provided.",
|
|
564
|
+
minLength: 1,
|
|
565
|
+
},
|
|
566
|
+
markdownPath: {
|
|
567
|
+
type: "string",
|
|
568
|
+
description: "Absolute local file path to the markdown file to apply.",
|
|
569
|
+
minLength: 1,
|
|
570
|
+
"x-uxc-kind": "file-path",
|
|
571
|
+
},
|
|
572
|
+
title: {
|
|
573
|
+
type: "string",
|
|
574
|
+
description: "Optional title override. When omitted, the first markdown heading becomes the article title.",
|
|
575
|
+
minLength: 1,
|
|
576
|
+
},
|
|
577
|
+
coverImagePath: {
|
|
578
|
+
type: "string",
|
|
579
|
+
description: "Optional absolute local image path for the article cover image when creating a new draft.",
|
|
580
|
+
minLength: 1,
|
|
581
|
+
"x-uxc-kind": "file-path",
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
required: ["markdownPath"],
|
|
585
|
+
additionalProperties: false,
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: "article.publishMarkdown",
|
|
590
|
+
description: "Publish one X article from a local markdown file",
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: "object",
|
|
593
|
+
description: "Create and publish one X article from a local markdown file. The adapter derives the title from the first markdown heading when title is omitted.",
|
|
594
|
+
properties: {
|
|
595
|
+
markdownPath: {
|
|
596
|
+
type: "string",
|
|
597
|
+
description: "Absolute local file path to the markdown file to publish.",
|
|
598
|
+
minLength: 1,
|
|
599
|
+
"x-uxc-kind": "file-path",
|
|
600
|
+
},
|
|
601
|
+
title: {
|
|
602
|
+
type: "string",
|
|
603
|
+
description: "Optional title override. When omitted, the first markdown heading becomes the article title.",
|
|
604
|
+
minLength: 1,
|
|
605
|
+
},
|
|
606
|
+
coverImagePath: {
|
|
607
|
+
type: "string",
|
|
608
|
+
description: "Optional absolute local image path for the article cover image.",
|
|
609
|
+
minLength: 1,
|
|
610
|
+
"x-uxc-kind": "file-path",
|
|
611
|
+
},
|
|
612
|
+
dryRun: {
|
|
613
|
+
type: "boolean",
|
|
614
|
+
description: "When true, validate article creation and editor population without publishing.",
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
required: ["markdownPath"],
|
|
618
|
+
additionalProperties: false,
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: "article.publish",
|
|
623
|
+
description: "Publish one existing X article draft by edit url, public url, or id",
|
|
624
|
+
inputSchema: {
|
|
625
|
+
type: "object",
|
|
626
|
+
description: "Open one article editor page and publish the current draft.",
|
|
627
|
+
properties: {
|
|
628
|
+
url: {
|
|
629
|
+
type: "string",
|
|
630
|
+
description: "Article edit URL or public article URL.",
|
|
631
|
+
minLength: 1,
|
|
632
|
+
},
|
|
633
|
+
id: {
|
|
634
|
+
type: "string",
|
|
635
|
+
description: "Article id. Used when url is not provided.",
|
|
636
|
+
minLength: 1,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
additionalProperties: false,
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "article.setCoverImage",
|
|
644
|
+
description: "Set or replace the cover image for one existing X article draft",
|
|
645
|
+
inputSchema: {
|
|
646
|
+
type: "object",
|
|
647
|
+
description: "Open one article editor page and set the cover image for the current draft.",
|
|
648
|
+
properties: {
|
|
649
|
+
url: {
|
|
650
|
+
type: "string",
|
|
651
|
+
description: "Article edit URL or public article URL.",
|
|
652
|
+
minLength: 1,
|
|
653
|
+
},
|
|
654
|
+
id: {
|
|
655
|
+
type: "string",
|
|
656
|
+
description: "Article id. Used when url is not provided.",
|
|
657
|
+
minLength: 1,
|
|
658
|
+
},
|
|
659
|
+
coverImagePath: {
|
|
660
|
+
type: "string",
|
|
661
|
+
description: "Absolute local image path for the article cover image.",
|
|
662
|
+
minLength: 1,
|
|
663
|
+
"x-uxc-kind": "file-path",
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
required: ["coverImagePath"],
|
|
667
|
+
additionalProperties: false,
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "article.updateMarkdown",
|
|
672
|
+
description: "Replace the title and body of one existing X article draft from a local markdown file",
|
|
673
|
+
inputSchema: {
|
|
674
|
+
type: "object",
|
|
675
|
+
description: "Open one article editor page, replace the current title and body from a local markdown file, and upload any local inline images referenced by markdown image syntax.",
|
|
676
|
+
properties: {
|
|
677
|
+
url: {
|
|
678
|
+
type: "string",
|
|
679
|
+
description: "Article edit URL or public article URL.",
|
|
680
|
+
minLength: 1,
|
|
681
|
+
},
|
|
682
|
+
id: {
|
|
683
|
+
type: "string",
|
|
684
|
+
description: "Article id. Used when url is not provided.",
|
|
685
|
+
minLength: 1,
|
|
686
|
+
},
|
|
687
|
+
markdownPath: {
|
|
688
|
+
type: "string",
|
|
689
|
+
description: "Absolute local file path to the markdown file to apply.",
|
|
690
|
+
minLength: 1,
|
|
691
|
+
"x-uxc-kind": "file-path",
|
|
692
|
+
},
|
|
693
|
+
title: {
|
|
694
|
+
type: "string",
|
|
695
|
+
description: "Optional title override. When omitted, the first markdown heading becomes the article title.",
|
|
696
|
+
minLength: 1,
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
required: ["markdownPath"],
|
|
700
|
+
additionalProperties: false,
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
name: "article.delete",
|
|
705
|
+
description: "Delete one X article draft or published article by edit url, public url, or id",
|
|
706
|
+
inputSchema: {
|
|
707
|
+
type: "object",
|
|
708
|
+
description: "Open one article editor page and delete the article after confirmation.",
|
|
709
|
+
properties: {
|
|
710
|
+
url: {
|
|
711
|
+
type: "string",
|
|
712
|
+
description: "Article edit URL or public article URL.",
|
|
713
|
+
minLength: 1,
|
|
714
|
+
},
|
|
715
|
+
id: {
|
|
716
|
+
type: "string",
|
|
717
|
+
description: "Article id. Used when url is not provided.",
|
|
718
|
+
minLength: 1,
|
|
719
|
+
},
|
|
720
|
+
dryRun: {
|
|
721
|
+
type: "boolean",
|
|
722
|
+
description: "When true, validate delete controls without confirming the destructive action.",
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
additionalProperties: false,
|
|
726
|
+
},
|
|
727
|
+
},
|
|
425
728
|
];
|
|
426
729
|
function toRecord(value) {
|
|
427
730
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
@@ -520,65 +823,6 @@ function parseDataUri(uri) {
|
|
|
520
823
|
return undefined;
|
|
521
824
|
}
|
|
522
825
|
}
|
|
523
|
-
function toTweetMediaArray(value) {
|
|
524
|
-
if (!Array.isArray(value)) {
|
|
525
|
-
return [];
|
|
526
|
-
}
|
|
527
|
-
const output = [];
|
|
528
|
-
for (const rawEntry of value) {
|
|
529
|
-
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
const entry = rawEntry;
|
|
533
|
-
const type = entry.type;
|
|
534
|
-
const url = entry.url;
|
|
535
|
-
if ((type !== "photo" && type !== "video" && type !== "animated_gif")
|
|
536
|
-
|| typeof url !== "string"
|
|
537
|
-
|| url.trim().length === 0) {
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
const media = {
|
|
541
|
-
type,
|
|
542
|
-
url,
|
|
543
|
-
};
|
|
544
|
-
if (typeof entry.previewUrl === "string" && entry.previewUrl.trim()) {
|
|
545
|
-
media.previewUrl = entry.previewUrl;
|
|
546
|
-
}
|
|
547
|
-
if (typeof entry.width === "number" && Number.isFinite(entry.width)) {
|
|
548
|
-
media.width = entry.width;
|
|
549
|
-
}
|
|
550
|
-
if (typeof entry.height === "number" && Number.isFinite(entry.height)) {
|
|
551
|
-
media.height = entry.height;
|
|
552
|
-
}
|
|
553
|
-
if (typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs)) {
|
|
554
|
-
media.durationMs = entry.durationMs;
|
|
555
|
-
}
|
|
556
|
-
if (Array.isArray(entry.variants)) {
|
|
557
|
-
const variants = entry.variants.flatMap((rawVariant) => {
|
|
558
|
-
if (!rawVariant || typeof rawVariant !== "object" || Array.isArray(rawVariant)) {
|
|
559
|
-
return [];
|
|
560
|
-
}
|
|
561
|
-
const variantRecord = rawVariant;
|
|
562
|
-
if (typeof variantRecord.url !== "string" || !variantRecord.url.trim()) {
|
|
563
|
-
return [];
|
|
564
|
-
}
|
|
565
|
-
const variant = { url: variantRecord.url };
|
|
566
|
-
if (typeof variantRecord.contentType === "string" && variantRecord.contentType.trim()) {
|
|
567
|
-
variant.contentType = variantRecord.contentType;
|
|
568
|
-
}
|
|
569
|
-
if (typeof variantRecord.bitrate === "number" && Number.isFinite(variantRecord.bitrate)) {
|
|
570
|
-
variant.bitrate = variantRecord.bitrate;
|
|
571
|
-
}
|
|
572
|
-
return [variant];
|
|
573
|
-
});
|
|
574
|
-
if (variants.length > 0) {
|
|
575
|
-
media.variants = variants;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
output.push(media);
|
|
579
|
-
}
|
|
580
|
-
return output;
|
|
581
|
-
}
|
|
582
826
|
function normalizeTweetMediaForDownload(media) {
|
|
583
827
|
if (media.type !== "photo") {
|
|
584
828
|
return media;
|
|
@@ -776,27 +1020,322 @@ async function resolveGrokAttachments(input) {
|
|
|
776
1020
|
}
|
|
777
1021
|
return { ok: true, attachments };
|
|
778
1022
|
}
|
|
779
|
-
function
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return DEFAULT_TIMELINE_LIMIT;
|
|
1023
|
+
async function resolveArticleAttachment(value, fieldName) {
|
|
1024
|
+
if (value === undefined) {
|
|
1025
|
+
return { ok: true };
|
|
783
1026
|
}
|
|
784
|
-
|
|
1027
|
+
const path = typeof value === "string" ? value.trim() : "";
|
|
1028
|
+
if (!path) {
|
|
1029
|
+
return {
|
|
1030
|
+
ok: false,
|
|
1031
|
+
result: errorResult("VALIDATION_ERROR", `${fieldName} must be a non-empty string`),
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
if (!isAbsolute(path)) {
|
|
1035
|
+
return {
|
|
1036
|
+
ok: false,
|
|
1037
|
+
result: errorResult("VALIDATION_ERROR", `${fieldName} must be an absolute file path`),
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
const fileStat = await stat(path);
|
|
1042
|
+
if (!fileStat.isFile()) {
|
|
1043
|
+
return {
|
|
1044
|
+
ok: false,
|
|
1045
|
+
result: errorResult("VALIDATION_ERROR", `${fieldName} must point to a file`),
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
return {
|
|
1051
|
+
ok: false,
|
|
1052
|
+
result: errorResult("VALIDATION_ERROR", `${fieldName} was not found`),
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
ok: true,
|
|
1057
|
+
attachment: {
|
|
1058
|
+
path,
|
|
1059
|
+
name: basename(path),
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
785
1062
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1063
|
+
function stripMarkdownImageDestination(rawDestination) {
|
|
1064
|
+
const trimmed = rawDestination.trim();
|
|
1065
|
+
const withoutAngle = trimmed.startsWith("<") && trimmed.endsWith(">") ? trimmed.slice(1, -1) : trimmed;
|
|
1066
|
+
const titleMatch = withoutAngle.match(/^(.+?)(?:\s+["'(].*)?$/);
|
|
1067
|
+
return (titleMatch?.[1] ?? withoutAngle).trim();
|
|
789
1068
|
}
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1069
|
+
function extractArticleTitle(markdown, markdownPath, explicitTitle) {
|
|
1070
|
+
const title = typeof explicitTitle === "string" ? explicitTitle.trim() : "";
|
|
1071
|
+
if (title) {
|
|
1072
|
+
return title;
|
|
1073
|
+
}
|
|
1074
|
+
const headingMatch = markdown.match(/^\s*#\s+(.+?)\s*$/m);
|
|
1075
|
+
if (headingMatch?.[1]) {
|
|
1076
|
+
return headingMatch[1].trim();
|
|
1077
|
+
}
|
|
1078
|
+
return basename(markdownPath, extname(markdownPath)).trim() || "Untitled";
|
|
1079
|
+
}
|
|
1080
|
+
function normalizeArticleTitleForComparison(value) {
|
|
1081
|
+
return value
|
|
1082
|
+
.normalize("NFKC")
|
|
1083
|
+
.toLowerCase()
|
|
1084
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
1085
|
+
.trim();
|
|
1086
|
+
}
|
|
1087
|
+
function normalizeArticleMarkdown(markdown, markdownPath, explicitTitle) {
|
|
1088
|
+
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1089
|
+
const lines = normalized.split("\n");
|
|
1090
|
+
const explicit = typeof explicitTitle === "string" ? explicitTitle.trim() : "";
|
|
1091
|
+
const derivedTitle = extractArticleTitle(normalized, markdownPath, explicitTitle);
|
|
1092
|
+
const comparisonTitle = normalizeArticleTitleForComparison(derivedTitle);
|
|
1093
|
+
const output = [];
|
|
1094
|
+
let insideFence = false;
|
|
1095
|
+
let firstH1Handled = false;
|
|
1096
|
+
for (const line of lines) {
|
|
1097
|
+
if (/^\s*```/.test(line)) {
|
|
1098
|
+
insideFence = !insideFence;
|
|
1099
|
+
output.push(line);
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
if (insideFence) {
|
|
1103
|
+
output.push(line);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const match = line.match(/^(\s*)#\s+(.+?)\s*$/);
|
|
1107
|
+
if (!match) {
|
|
1108
|
+
output.push(line);
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const headingText = (match[2] ?? "").trim();
|
|
1112
|
+
if (!firstH1Handled) {
|
|
1113
|
+
firstH1Handled = true;
|
|
1114
|
+
if (!explicit || normalizeArticleTitleForComparison(headingText) === comparisonTitle) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
output.push(`${match[1]}## ${headingText}`);
|
|
1119
|
+
}
|
|
1120
|
+
const bodyMarkdown = output.join("\n").replace(/^\s*\n/, "").trim();
|
|
1121
|
+
return {
|
|
1122
|
+
title: derivedTitle,
|
|
1123
|
+
bodyMarkdown,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function escapeHtml(value) {
|
|
1127
|
+
return value
|
|
1128
|
+
.replaceAll("&", "&")
|
|
1129
|
+
.replaceAll("<", "<")
|
|
1130
|
+
.replaceAll(">", ">")
|
|
1131
|
+
.replaceAll('"', """)
|
|
1132
|
+
.replaceAll("'", "'");
|
|
1133
|
+
}
|
|
1134
|
+
function convertMarkdownInlineToHtml(value) {
|
|
1135
|
+
const placeholders = [];
|
|
1136
|
+
const reserve = (html) => {
|
|
1137
|
+
const token = `__WEBMCP_HTML_${placeholders.length}__`;
|
|
1138
|
+
placeholders.push(html);
|
|
1139
|
+
return token;
|
|
1140
|
+
};
|
|
1141
|
+
let rendered = value
|
|
1142
|
+
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, altRaw, destinationRaw) => {
|
|
1143
|
+
const alt = escapeHtml(altRaw.trim());
|
|
1144
|
+
const destination = escapeHtml(stripMarkdownImageDestination(destinationRaw));
|
|
1145
|
+
return reserve(`<img src="${destination}" alt="${alt}">`);
|
|
1146
|
+
})
|
|
1147
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, textRaw, hrefRaw) => {
|
|
1148
|
+
const text = escapeHtml(textRaw.trim() || hrefRaw.trim());
|
|
1149
|
+
const href = escapeHtml(hrefRaw.trim());
|
|
1150
|
+
return reserve(`<a href="${href}">${text}</a>`);
|
|
1151
|
+
})
|
|
1152
|
+
.replace(/`([^`]+)`/g, (_match, codeRaw) => reserve(`<code>${escapeHtml(codeRaw)}</code>`))
|
|
1153
|
+
.replace(/\*\*([^*]+)\*\*/g, (_match, textRaw) => reserve(`<strong>${escapeHtml(textRaw)}</strong>`))
|
|
1154
|
+
.replace(/\*([^*]+)\*/g, (_match, textRaw) => reserve(`<em>${escapeHtml(textRaw)}</em>`));
|
|
1155
|
+
rendered = escapeHtml(rendered);
|
|
1156
|
+
return rendered.replace(/__WEBMCP_HTML_(\d+)__/g, (_match, indexRaw) => {
|
|
1157
|
+
const index = Number.parseInt(indexRaw, 10);
|
|
1158
|
+
return placeholders[index] ?? "";
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
function convertMarkdownToHtml(markdown) {
|
|
1162
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
1163
|
+
const blocks = [];
|
|
1164
|
+
let index = 0;
|
|
1165
|
+
const flushParagraph = (paragraphLines) => {
|
|
1166
|
+
if (paragraphLines.length === 0) {
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const text = paragraphLines.join(" ").trim();
|
|
1170
|
+
if (!text) {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
blocks.push(`<p>${convertMarkdownInlineToHtml(text)}</p>`);
|
|
1174
|
+
};
|
|
1175
|
+
while (index < lines.length) {
|
|
1176
|
+
const line = lines[index] ?? "";
|
|
1177
|
+
const trimmed = line.trim();
|
|
1178
|
+
if (!trimmed) {
|
|
1179
|
+
index += 1;
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
const fenceMatch = trimmed.match(/^```([^`]*)$/);
|
|
1183
|
+
if (fenceMatch) {
|
|
1184
|
+
const language = escapeHtml(fenceMatch[1]?.trim() ?? "");
|
|
1185
|
+
const codeLines = [];
|
|
1186
|
+
index += 1;
|
|
1187
|
+
while (index < lines.length && !lines[index].trim().startsWith("```")) {
|
|
1188
|
+
codeLines.push(lines[index]);
|
|
1189
|
+
index += 1;
|
|
1190
|
+
}
|
|
1191
|
+
if (index < lines.length) {
|
|
1192
|
+
index += 1;
|
|
1193
|
+
}
|
|
1194
|
+
const code = escapeHtml(codeLines.join("\n"));
|
|
1195
|
+
blocks.push(language ? `<pre><code class="language-${language}">${code}</code></pre>` : `<pre><code>${code}</code></pre>`);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
1199
|
+
if (headingMatch) {
|
|
1200
|
+
const level = Math.min(6, headingMatch[1].length);
|
|
1201
|
+
const content = convertMarkdownInlineToHtml(headingMatch[2].trim());
|
|
1202
|
+
blocks.push(`<h${level}>${content}</h${level}>`);
|
|
1203
|
+
index += 1;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const bulletMatch = trimmed.match(/^[-*+]\s+(.+)$/);
|
|
1207
|
+
if (bulletMatch) {
|
|
1208
|
+
const items = [];
|
|
1209
|
+
while (index < lines.length) {
|
|
1210
|
+
const itemLine = lines[index].trim();
|
|
1211
|
+
const match = itemLine.match(/^[-*+]\s+(.+)$/);
|
|
1212
|
+
if (!match) {
|
|
1213
|
+
break;
|
|
1214
|
+
}
|
|
1215
|
+
items.push(`<li>${convertMarkdownInlineToHtml(match[1].trim())}</li>`);
|
|
1216
|
+
index += 1;
|
|
1217
|
+
}
|
|
1218
|
+
blocks.push(`<ul>${items.join("")}</ul>`);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
const orderedMatch = trimmed.match(/^\d+\.\s+(.+)$/);
|
|
1222
|
+
if (orderedMatch) {
|
|
1223
|
+
const items = [];
|
|
1224
|
+
while (index < lines.length) {
|
|
1225
|
+
const itemLine = lines[index].trim();
|
|
1226
|
+
const match = itemLine.match(/^\d+\.\s+(.+)$/);
|
|
1227
|
+
if (!match) {
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
items.push(`<li>${convertMarkdownInlineToHtml(match[1].trim())}</li>`);
|
|
1231
|
+
index += 1;
|
|
1232
|
+
}
|
|
1233
|
+
blocks.push(`<ol>${items.join("")}</ol>`);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
const paragraphLines = [];
|
|
1237
|
+
while (index < lines.length) {
|
|
1238
|
+
const paragraphLine = lines[index] ?? "";
|
|
1239
|
+
const paragraphTrimmed = paragraphLine.trim();
|
|
1240
|
+
if (!paragraphTrimmed) {
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
if (/^```/.test(paragraphTrimmed) || /^(#{1,6})\s+/.test(paragraphTrimmed) || /^[-*+]\s+/.test(paragraphTrimmed) || /^\d+\.\s+/.test(paragraphTrimmed)) {
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
paragraphLines.push(paragraphLine.trim());
|
|
1247
|
+
index += 1;
|
|
1248
|
+
}
|
|
1249
|
+
flushParagraph(paragraphLines);
|
|
1250
|
+
}
|
|
1251
|
+
return blocks.join("\n");
|
|
1252
|
+
}
|
|
1253
|
+
function convertMarkdownLineToPlainText(line) {
|
|
1254
|
+
return line
|
|
1255
|
+
.replace(/^#{1,6}\s+/, "")
|
|
1256
|
+
.replace(/^[-*+]\s+/, "")
|
|
1257
|
+
.replace(/^\d+\.\s+/, "")
|
|
1258
|
+
.replace(/^>\s+/, "")
|
|
1259
|
+
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1")
|
|
1260
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1")
|
|
1261
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
1262
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
1263
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
1264
|
+
.replace(/\s+/g, " ")
|
|
1265
|
+
.trim();
|
|
1266
|
+
}
|
|
1267
|
+
function extractArticleConfirmationSnippets(markdown) {
|
|
1268
|
+
const snippets = [];
|
|
1269
|
+
let insideFence = false;
|
|
1270
|
+
for (const rawLine of markdown.replace(/\r\n/g, "\n").split("\n")) {
|
|
1271
|
+
const trimmed = rawLine.trim();
|
|
1272
|
+
if (!trimmed) {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
if (/^```/.test(trimmed)) {
|
|
1276
|
+
insideFence = !insideFence;
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const normalized = insideFence ? trimmed : convertMarkdownLineToPlainText(trimmed);
|
|
1280
|
+
if (normalized) {
|
|
1281
|
+
snippets.push(normalized);
|
|
1282
|
+
}
|
|
1283
|
+
if (snippets.length >= 3) {
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return snippets;
|
|
1288
|
+
}
|
|
1289
|
+
function prepareArticleMarkdown(markdown, markdownPath) {
|
|
1290
|
+
const inlineImages = [];
|
|
1291
|
+
let nextIndex = 1;
|
|
1292
|
+
const prepared = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, altRaw, destinationRaw) => {
|
|
1293
|
+
const destination = stripMarkdownImageDestination(destinationRaw);
|
|
1294
|
+
if (/^(?:https?:|data:)/i.test(destination)) {
|
|
1295
|
+
return _match;
|
|
1296
|
+
}
|
|
1297
|
+
const resolvedPath = isAbsolute(destination) ? destination : resolve(dirname(markdownPath), destination);
|
|
1298
|
+
const marker = `${ARTICLE_INLINE_IMAGE_MARKER_PREFIX}${nextIndex}]]`;
|
|
1299
|
+
nextIndex += 1;
|
|
1300
|
+
const image = {
|
|
1301
|
+
marker,
|
|
1302
|
+
path: resolvedPath,
|
|
1303
|
+
name: basename(resolvedPath),
|
|
1304
|
+
};
|
|
1305
|
+
const alt = altRaw.trim();
|
|
1306
|
+
if (alt) {
|
|
1307
|
+
image.alt = alt;
|
|
1308
|
+
}
|
|
1309
|
+
inlineImages.push(image);
|
|
1310
|
+
return `\n\n${marker}\n\n`;
|
|
1311
|
+
});
|
|
1312
|
+
return {
|
|
1313
|
+
markdown: prepared,
|
|
1314
|
+
html: convertMarkdownToHtml(prepared),
|
|
1315
|
+
inlineImages,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeTimelineLimit(input) {
|
|
1319
|
+
const rawLimit = input.limit;
|
|
1320
|
+
if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
|
|
1321
|
+
return DEFAULT_TIMELINE_LIMIT;
|
|
1322
|
+
}
|
|
1323
|
+
return Math.max(1, Math.min(MAX_TIMELINE_LIMIT, Math.floor(rawLimit)));
|
|
1324
|
+
}
|
|
1325
|
+
async function ensureNetworkCaptureInstalled(page) {
|
|
1326
|
+
await page.addInitScript(CAPTURE_INJECT_SCRIPT);
|
|
1327
|
+
await page.evaluate(CAPTURE_INJECT_SCRIPT);
|
|
1328
|
+
}
|
|
1329
|
+
async function hasCapturedTemplate(page, mode) {
|
|
1330
|
+
const result = await page.evaluate(({ targetMode }) => {
|
|
1331
|
+
const globalAny = window;
|
|
1332
|
+
const entries = Array.isArray(globalAny.__WEBMCP_X_CAPTURE__?.entries)
|
|
1333
|
+
? globalAny.__WEBMCP_X_CAPTURE__?.entries ?? []
|
|
1334
|
+
: [];
|
|
1335
|
+
const ops = targetMode === "home"
|
|
1336
|
+
? ["HomeTimeline", "TweetDetail"]
|
|
1337
|
+
: targetMode === "bookmarks"
|
|
1338
|
+
? ["BookmarksAll", "Bookmarks"]
|
|
800
1339
|
: targetMode === "tweet"
|
|
801
1340
|
? ["TweetDetail"]
|
|
802
1341
|
: targetMode === "user_timeline"
|
|
@@ -885,22 +1424,48 @@ async function detectAuth(page) {
|
|
|
885
1424
|
return { state: "auth_required", signals };
|
|
886
1425
|
}, { op: "detect_auth" });
|
|
887
1426
|
}
|
|
1427
|
+
function isTransientExecutionContextError(error) {
|
|
1428
|
+
if (!(error instanceof Error)) {
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
const message = error.message.toLowerCase();
|
|
1432
|
+
return (message.includes("execution context was destroyed") ||
|
|
1433
|
+
message.includes("cannot find context with specified id") ||
|
|
1434
|
+
message.includes("target closed"));
|
|
1435
|
+
}
|
|
1436
|
+
async function detectAuthWithRetry(page) {
|
|
1437
|
+
for (let attempt = 0; attempt < AUTH_STABILIZE_ATTEMPTS; attempt += 1) {
|
|
1438
|
+
try {
|
|
1439
|
+
return await detectAuth(page);
|
|
1440
|
+
}
|
|
1441
|
+
catch (error) {
|
|
1442
|
+
if (!isTransientExecutionContextError(error) || attempt === AUTH_STABILIZE_ATTEMPTS - 1) {
|
|
1443
|
+
throw error;
|
|
1444
|
+
}
|
|
1445
|
+
await page.waitForLoadState("domcontentloaded").catch(() => {
|
|
1446
|
+
// The page may still be mid-navigation; let the retry loop continue.
|
|
1447
|
+
});
|
|
1448
|
+
await page.waitForTimeout(AUTH_STABILIZE_DELAY_MS);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return { state: "auth_required", signals: ["auth_unknown"] };
|
|
1452
|
+
}
|
|
888
1453
|
async function detectAuthStable(page) {
|
|
889
|
-
let auth = await
|
|
1454
|
+
let auth = await detectAuthWithRetry(page);
|
|
890
1455
|
for (let attempt = 1; attempt < AUTH_STABILIZE_ATTEMPTS; attempt += 1) {
|
|
891
1456
|
const shouldRetry = auth.state === "auth_required" && auth.signals.includes("auth_unknown");
|
|
892
1457
|
if (!shouldRetry) {
|
|
893
1458
|
return auth;
|
|
894
1459
|
}
|
|
895
1460
|
await page.waitForTimeout(AUTH_STABILIZE_DELAY_MS);
|
|
896
|
-
auth = await
|
|
1461
|
+
auth = await detectAuthWithRetry(page);
|
|
897
1462
|
}
|
|
898
1463
|
return auth;
|
|
899
1464
|
}
|
|
900
1465
|
async function warmupAuthProbe(page) {
|
|
901
1466
|
const deadline = Date.now() + AUTH_WARMUP_TIMEOUT_MS;
|
|
902
1467
|
for (;;) {
|
|
903
|
-
const auth = await
|
|
1468
|
+
const auth = await detectAuthWithRetry(page);
|
|
904
1469
|
const stable = !(auth.state === "auth_required" && auth.signals.includes("auth_unknown"));
|
|
905
1470
|
if (stable || Date.now() >= deadline) {
|
|
906
1471
|
return;
|
|
@@ -939,6 +1504,33 @@ function canonicalizeStatusUrl(input, fallbackId) {
|
|
|
939
1504
|
return input;
|
|
940
1505
|
}
|
|
941
1506
|
}
|
|
1507
|
+
function canonicalizeArticleUrl(input, fallbackId) {
|
|
1508
|
+
if (!input) {
|
|
1509
|
+
return fallbackId ? `https://x.com/i/article/${fallbackId}` : undefined;
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
const url = new URL(input);
|
|
1513
|
+
if (!ALLOWED_X_HOSTS.has(url.hostname.toLowerCase())) {
|
|
1514
|
+
return input;
|
|
1515
|
+
}
|
|
1516
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
1517
|
+
const articleIndex = segments.findIndex((segment) => segment === "article" || segment === "articles");
|
|
1518
|
+
if (articleIndex < 0) {
|
|
1519
|
+
return input;
|
|
1520
|
+
}
|
|
1521
|
+
const articleId = segments[articleIndex + 1] ?? fallbackId;
|
|
1522
|
+
if (!articleId) {
|
|
1523
|
+
return input;
|
|
1524
|
+
}
|
|
1525
|
+
if (articleIndex > 0 && segments[0] !== "i") {
|
|
1526
|
+
return `${url.origin}/${segments[0]}/article/${articleId}`;
|
|
1527
|
+
}
|
|
1528
|
+
return `${url.origin}/i/article/${articleId}`;
|
|
1529
|
+
}
|
|
1530
|
+
catch {
|
|
1531
|
+
return input;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
942
1534
|
function enrichNotificationItem(item) {
|
|
943
1535
|
const text = normalizeInlineText(item.text);
|
|
944
1536
|
const summary = item.summary ? normalizeInlineText(item.summary) : undefined;
|
|
@@ -998,6 +1590,7 @@ function enrichNotificationItem(item) {
|
|
|
998
1590
|
return next;
|
|
999
1591
|
}
|
|
1000
1592
|
const READ_PAGE_CACHE = new WeakMap();
|
|
1593
|
+
const ARTICLE_DRAFT_PAGE_CACHE = new WeakMap();
|
|
1001
1594
|
const PROCESS_TEMPLATE_CACHE = new TemplateCache();
|
|
1002
1595
|
async function readTimelineViaNetwork(page, options) {
|
|
1003
1596
|
const fallbackTemplate = PROCESS_TEMPLATE_CACHE.get(options.mode);
|
|
@@ -1202,7 +1795,20 @@ async function readTimelineViaNetwork(page, options) {
|
|
|
1202
1795
|
item.createdAt = createdAt;
|
|
1203
1796
|
}
|
|
1204
1797
|
if (media.length > 0) {
|
|
1205
|
-
item.media =
|
|
1798
|
+
item.media = media;
|
|
1799
|
+
}
|
|
1800
|
+
const articleResult = tweet?.article?.article_results
|
|
1801
|
+
?.result ?? undefined;
|
|
1802
|
+
const articleRestId = typeof articleResult?.rest_id === "string" ? articleResult.rest_id : "";
|
|
1803
|
+
if (articleRestId) {
|
|
1804
|
+
const screenName = typeof userLegacy.screen_name === "string" ? userLegacy.screen_name : "";
|
|
1805
|
+
const articleUrl = screenName
|
|
1806
|
+
? `https://x.com/${screenName.replace(/^@+/, "")}/article/${articleRestId}`
|
|
1807
|
+
: `https://x.com/i/article/${articleRestId}`;
|
|
1808
|
+
item.article = {
|
|
1809
|
+
id: articleRestId,
|
|
1810
|
+
url: articleUrl,
|
|
1811
|
+
};
|
|
1206
1812
|
}
|
|
1207
1813
|
outputItems.push(item);
|
|
1208
1814
|
}
|
|
@@ -1468,7 +2074,15 @@ async function extractTweetCards(page, limit) {
|
|
|
1468
2074
|
}
|
|
1469
2075
|
const media = collectDomMedia(article);
|
|
1470
2076
|
if (media.length > 0) {
|
|
1471
|
-
item.media =
|
|
2077
|
+
item.media = media;
|
|
2078
|
+
}
|
|
2079
|
+
const articleAnchor = article.querySelector("a[href*='/article/'], a[href*='/i/article/'], a[href*='/articles/']");
|
|
2080
|
+
const articleUrl = canonicalizeArticleUrl(articleAnchor?.href);
|
|
2081
|
+
const articleId = articleUrl ? articleUrl.match(/\/article(?:s)?\/(\d+)/)?.[1] : undefined;
|
|
2082
|
+
if (articleId) {
|
|
2083
|
+
item.article = articleUrl
|
|
2084
|
+
? { id: articleId, url: articleUrl }
|
|
2085
|
+
: { id: articleId };
|
|
1472
2086
|
}
|
|
1473
2087
|
pushItem(item);
|
|
1474
2088
|
if (items.length >= maxItems) {
|
|
@@ -1496,7 +2110,15 @@ async function extractTweetCards(page, limit) {
|
|
|
1496
2110
|
}
|
|
1497
2111
|
const media = collectDomMedia(cell);
|
|
1498
2112
|
if (media.length > 0) {
|
|
1499
|
-
item.media =
|
|
2113
|
+
item.media = media;
|
|
2114
|
+
}
|
|
2115
|
+
const articleAnchor = cell.querySelector("a[href*='/article/'], a[href*='/i/article/'], a[href*='/articles/']");
|
|
2116
|
+
const articleUrl = canonicalizeArticleUrl(articleAnchor?.href);
|
|
2117
|
+
const articleId = articleUrl ? articleUrl.match(/\/article(?:s)?\/(\d+)/)?.[1] : undefined;
|
|
2118
|
+
if (articleId) {
|
|
2119
|
+
item.article = articleUrl
|
|
2120
|
+
? { id: articleId, url: articleUrl }
|
|
2121
|
+
: { id: articleId };
|
|
1500
2122
|
}
|
|
1501
2123
|
pushItem(item);
|
|
1502
2124
|
}
|
|
@@ -1791,6 +2413,51 @@ async function closeCachedReadPages(ownerPage) {
|
|
|
1791
2413
|
}
|
|
1792
2414
|
}
|
|
1793
2415
|
}
|
|
2416
|
+
function getArticleDraftPageCache(ownerPage) {
|
|
2417
|
+
let cache = ARTICLE_DRAFT_PAGE_CACHE.get(ownerPage);
|
|
2418
|
+
if (!cache) {
|
|
2419
|
+
cache = new Map();
|
|
2420
|
+
ARTICLE_DRAFT_PAGE_CACHE.set(ownerPage, cache);
|
|
2421
|
+
}
|
|
2422
|
+
return cache;
|
|
2423
|
+
}
|
|
2424
|
+
async function cacheArticleDraftPage(ownerPage, articleId, articlePage) {
|
|
2425
|
+
const cache = getArticleDraftPageCache(ownerPage);
|
|
2426
|
+
const existing = cache.get(articleId);
|
|
2427
|
+
if (existing && existing !== articlePage && !existing.isClosed()) {
|
|
2428
|
+
await existing.close().catch(() => { });
|
|
2429
|
+
}
|
|
2430
|
+
cache.set(articleId, articlePage);
|
|
2431
|
+
}
|
|
2432
|
+
function getCachedArticleDraftPage(ownerPage, articleId) {
|
|
2433
|
+
const cache = ARTICLE_DRAFT_PAGE_CACHE.get(ownerPage);
|
|
2434
|
+
const page = cache?.get(articleId);
|
|
2435
|
+
if (!page || page.isClosed()) {
|
|
2436
|
+
cache?.delete(articleId);
|
|
2437
|
+
return undefined;
|
|
2438
|
+
}
|
|
2439
|
+
return page;
|
|
2440
|
+
}
|
|
2441
|
+
async function removeCachedArticleDraftPage(ownerPage, articleId) {
|
|
2442
|
+
const cache = ARTICLE_DRAFT_PAGE_CACHE.get(ownerPage);
|
|
2443
|
+
const page = cache?.get(articleId);
|
|
2444
|
+
cache?.delete(articleId);
|
|
2445
|
+
if (page && !page.isClosed()) {
|
|
2446
|
+
await page.close().catch(() => { });
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
async function closeCachedArticleDraftPages(ownerPage) {
|
|
2450
|
+
const cache = ARTICLE_DRAFT_PAGE_CACHE.get(ownerPage);
|
|
2451
|
+
ARTICLE_DRAFT_PAGE_CACHE.delete(ownerPage);
|
|
2452
|
+
if (!cache) {
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
for (const articlePage of cache.values()) {
|
|
2456
|
+
if (!articlePage.isClosed()) {
|
|
2457
|
+
await articlePage.close().catch(() => { });
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
1794
2461
|
async function waitForTweetSurface(page) {
|
|
1795
2462
|
await page
|
|
1796
2463
|
.waitForFunction(() => {
|
|
@@ -1814,6 +2481,9 @@ function mapTweetCards(items) {
|
|
|
1814
2481
|
if (item.media && item.media.length > 0) {
|
|
1815
2482
|
mapped.media = item.media;
|
|
1816
2483
|
}
|
|
2484
|
+
if (item.article) {
|
|
2485
|
+
mapped.article = item.article;
|
|
2486
|
+
}
|
|
1817
2487
|
return mapped;
|
|
1818
2488
|
});
|
|
1819
2489
|
}
|
|
@@ -2576,62 +3246,2344 @@ async function waitForReplyConfirmation(page, targetUrl, text, timeoutMs) {
|
|
|
2576
3246
|
const secondPassTimeoutMs = Math.max(2_500, timeoutMs - firstPassTimeoutMs);
|
|
2577
3247
|
return await waitForComposeConfirmation(page, text, secondPassTimeoutMs);
|
|
2578
3248
|
}
|
|
2579
|
-
async function
|
|
3249
|
+
async function deleteTweetDetail(page, targetUrl, dryRun) {
|
|
3250
|
+
const matchId = targetUrl.match(/status\/(\d+)/)?.[1];
|
|
3251
|
+
return await withEphemeralPage(page, targetUrl, async (detailPage) => {
|
|
3252
|
+
await waitForTweetSurface(detailPage);
|
|
3253
|
+
const menuOpened = await detailPage.evaluate(({ op }) => {
|
|
3254
|
+
if (op !== "tweet_open_delete_menu") {
|
|
3255
|
+
return false;
|
|
3256
|
+
}
|
|
3257
|
+
const menuButton = Array.from(document.querySelectorAll("button, div[role='button']")).find((element) => {
|
|
3258
|
+
const testId = element.getAttribute("data-testid") || "";
|
|
3259
|
+
const aria = (element.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim();
|
|
3260
|
+
return testId === "caret" || aria === "More";
|
|
3261
|
+
});
|
|
3262
|
+
if (!menuButton) {
|
|
3263
|
+
return false;
|
|
3264
|
+
}
|
|
3265
|
+
menuButton.click();
|
|
3266
|
+
return true;
|
|
3267
|
+
}, { op: "tweet_open_delete_menu" }).catch(() => false);
|
|
3268
|
+
if (!menuOpened) {
|
|
3269
|
+
return errorResult("UPSTREAM_CHANGED", "tweet delete controls not found", {
|
|
3270
|
+
reason: "more_button_not_found",
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
const deleteReady = await detailPage.waitForFunction(() => {
|
|
3274
|
+
return Array.from(document.querySelectorAll("[role='menuitem'], button, div[role='button']")).some((element) => {
|
|
3275
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
3276
|
+
return text === "Delete";
|
|
3277
|
+
});
|
|
3278
|
+
}, undefined, { timeout: 5_000 }).then(() => true).catch(() => false);
|
|
3279
|
+
if (!deleteReady) {
|
|
3280
|
+
return errorResult("UPSTREAM_CHANGED", "tweet delete controls not found", {
|
|
3281
|
+
reason: "delete_menu_item_not_found",
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
if (dryRun) {
|
|
3285
|
+
const output = {
|
|
3286
|
+
ok: true,
|
|
3287
|
+
dryRun: true,
|
|
3288
|
+
deleteVisible: true,
|
|
3289
|
+
};
|
|
3290
|
+
if (matchId) {
|
|
3291
|
+
output.tweetId = matchId;
|
|
3292
|
+
}
|
|
3293
|
+
output.url = targetUrl;
|
|
3294
|
+
return output;
|
|
3295
|
+
}
|
|
3296
|
+
const firstDelete = await detailPage.evaluate(({ op }) => {
|
|
3297
|
+
if (op !== "tweet_click_delete_menu_item") {
|
|
3298
|
+
return false;
|
|
3299
|
+
}
|
|
3300
|
+
const item = Array.from(document.querySelectorAll("[role='menuitem'], button, div[role='button']")).find((element) => {
|
|
3301
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
3302
|
+
return text === "Delete";
|
|
3303
|
+
});
|
|
3304
|
+
if (!item) {
|
|
3305
|
+
return false;
|
|
3306
|
+
}
|
|
3307
|
+
item.click();
|
|
3308
|
+
return true;
|
|
3309
|
+
}, { op: "tweet_click_delete_menu_item" }).catch(() => false);
|
|
3310
|
+
if (!firstDelete) {
|
|
3311
|
+
return errorResult("UPSTREAM_CHANGED", "tweet delete controls not found", {
|
|
3312
|
+
reason: "delete_menu_click_failed",
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
await detailPage.waitForTimeout(700);
|
|
3316
|
+
await detailPage.evaluate(({ op }) => {
|
|
3317
|
+
if (op !== "tweet_confirm_delete") {
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
const dialog = document.querySelector("[role='dialog'], [data-testid='confirmationSheetDialog']");
|
|
3321
|
+
const dialogButtons = dialog
|
|
3322
|
+
? Array.from(dialog.querySelectorAll("button, div[role='button']"))
|
|
3323
|
+
: [];
|
|
3324
|
+
const dialogConfirm = dialogButtons.find((element) => {
|
|
3325
|
+
const testId = element.getAttribute("data-testid") || "";
|
|
3326
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
3327
|
+
return testId === "confirmationSheetConfirm" || text === "Delete";
|
|
3328
|
+
});
|
|
3329
|
+
if (dialogConfirm) {
|
|
3330
|
+
dialogConfirm.click();
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
const fallback = Array.from(document.querySelectorAll("button, div[role='button']")).filter((element) => {
|
|
3334
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
3335
|
+
return text === "Delete";
|
|
3336
|
+
});
|
|
3337
|
+
fallback[fallback.length - 1]?.click();
|
|
3338
|
+
}, { op: "tweet_confirm_delete" }).catch(() => { });
|
|
3339
|
+
const deleted = await detailPage
|
|
3340
|
+
.waitForFunction(({ tweetId }) => {
|
|
3341
|
+
const bodyText = document.body?.innerText || "";
|
|
3342
|
+
if (bodyText.includes("This Post was deleted") || bodyText.includes("This Tweet was deleted")) {
|
|
3343
|
+
return true;
|
|
3344
|
+
}
|
|
3345
|
+
if (tweetId) {
|
|
3346
|
+
const currentUrl = window.location.href;
|
|
3347
|
+
return !currentUrl.includes(`/status/${tweetId}`);
|
|
3348
|
+
}
|
|
3349
|
+
return false;
|
|
3350
|
+
}, { tweetId: matchId }, { timeout: 15_000 })
|
|
3351
|
+
.then(() => true)
|
|
3352
|
+
.catch(() => false);
|
|
3353
|
+
if (!deleted) {
|
|
3354
|
+
return errorResult("ACTION_UNCONFIRMED", "tweet delete was not confirmed");
|
|
3355
|
+
}
|
|
3356
|
+
const output = {
|
|
3357
|
+
ok: true,
|
|
3358
|
+
confirmed: true,
|
|
3359
|
+
url: targetUrl,
|
|
3360
|
+
};
|
|
3361
|
+
if (matchId) {
|
|
3362
|
+
output.tweetId = matchId;
|
|
3363
|
+
}
|
|
3364
|
+
return output;
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
async function waitForArticleEditorSurface(page) {
|
|
2580
3368
|
await page
|
|
2581
3369
|
.waitForFunction(() => {
|
|
2582
|
-
const
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
document.querySelector("article");
|
|
2588
|
-
return composer !== null || messages !== null;
|
|
2589
|
-
}, undefined, { timeout: 12_000 })
|
|
3370
|
+
const title = document.querySelector("textarea[placeholder='Add a title']");
|
|
3371
|
+
const composer = document.querySelector("[data-testid='composer'][role='textbox']");
|
|
3372
|
+
const publishButton = Array.from(document.querySelectorAll("button")).find((button) => (button.textContent || "").replace(/\s+/g, " ").trim() === "Publish");
|
|
3373
|
+
return title !== null && composer !== null && publishButton !== undefined;
|
|
3374
|
+
}, undefined, { timeout: 20_000 })
|
|
2590
3375
|
.catch(() => { });
|
|
2591
3376
|
await page.waitForTimeout(800);
|
|
2592
3377
|
}
|
|
2593
|
-
async function
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
3378
|
+
async function ensureArticleDraftLoaded(page, articleId) {
|
|
3379
|
+
if (!articleId) {
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
const hasContent = async () => {
|
|
3383
|
+
return await page.evaluate(() => {
|
|
3384
|
+
const title = document.querySelector("textarea[placeholder='Add a title']");
|
|
3385
|
+
const composer = document.querySelector("[data-testid='composer'][role='textbox']");
|
|
3386
|
+
const titleValue = title instanceof HTMLTextAreaElement ? title.value.trim() : "";
|
|
3387
|
+
const composerText = composer instanceof HTMLElement ? (composer.textContent || "").trim() : "";
|
|
3388
|
+
return titleValue.length > 0 || composerText.length > 0;
|
|
3389
|
+
}).catch(() => false);
|
|
3390
|
+
};
|
|
3391
|
+
if (await hasContent()) {
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
await page.goto("https://x.com/compose/articles", { waitUntil: "domcontentloaded", timeout: 60_000 }).catch(() => { });
|
|
3395
|
+
await page.waitForTimeout(1_200);
|
|
3396
|
+
await page.evaluate(({ targetId }) => {
|
|
3397
|
+
const anchors = Array.from(document.querySelectorAll("a[href]"));
|
|
3398
|
+
const draftAnchor = anchors.find((anchor) => anchor.href.includes(`/compose/articles/edit/${targetId}`));
|
|
3399
|
+
draftAnchor?.click();
|
|
3400
|
+
}, { targetId: articleId }).catch(() => { });
|
|
3401
|
+
await page.waitForTimeout(1_200);
|
|
3402
|
+
if (await hasContent()) {
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
await page.goto(`https://x.com/compose/articles/edit/${articleId}`, { waitUntil: "domcontentloaded", timeout: 60_000 }).catch(() => { });
|
|
3406
|
+
await waitForArticleEditorSurface(page);
|
|
3407
|
+
await page.waitForTimeout(1_000);
|
|
3408
|
+
}
|
|
3409
|
+
async function waitForArticleDraftPersisted(page, articleId, title) {
|
|
3410
|
+
await page
|
|
3411
|
+
.evaluate(() => {
|
|
3412
|
+
const active = document.activeElement;
|
|
3413
|
+
if (active instanceof HTMLElement) {
|
|
3414
|
+
active.blur();
|
|
3415
|
+
}
|
|
3416
|
+
})
|
|
3417
|
+
.catch(() => { });
|
|
3418
|
+
await page.keyboard.press("Escape").catch(() => { });
|
|
3419
|
+
return await page
|
|
3420
|
+
.waitForFunction(({ targetId, expectedTitle }) => {
|
|
3421
|
+
const anchors = Array.from(document.querySelectorAll("a[href]"));
|
|
3422
|
+
const draftAnchor = anchors.find((anchor) => anchor.href.includes(`/compose/articles/edit/${targetId}`));
|
|
3423
|
+
const containerText = draftAnchor?.closest("article, li, div")?.textContent || draftAnchor?.textContent || "";
|
|
3424
|
+
const normalized = containerText.replace(/\s+/g, " ").trim();
|
|
3425
|
+
return normalized.includes(expectedTitle) && !normalized.includes("(Needs title)");
|
|
3426
|
+
}, { targetId: articleId, expectedTitle: title.trim() }, { timeout: 20_000 })
|
|
3427
|
+
.then(() => true)
|
|
3428
|
+
.catch(() => false);
|
|
3429
|
+
}
|
|
3430
|
+
async function openNewArticleEditor(page) {
|
|
3431
|
+
await page.goto("https://x.com/compose/articles", { waitUntil: "domcontentloaded", timeout: 60_000 }).catch(() => { });
|
|
3432
|
+
await page.waitForTimeout(2_500);
|
|
3433
|
+
let clicked = false;
|
|
3434
|
+
const createSelectors = [
|
|
3435
|
+
"a[data-testid='empty_state_button_text']",
|
|
3436
|
+
"button[aria-label='create']",
|
|
3437
|
+
"a:has-text('Write')",
|
|
2605
3438
|
];
|
|
2606
|
-
const
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
3439
|
+
for (const selector of createSelectors) {
|
|
3440
|
+
try {
|
|
3441
|
+
await page.click(selector, { timeout: 4_000 });
|
|
3442
|
+
clicked = true;
|
|
3443
|
+
break;
|
|
3444
|
+
}
|
|
3445
|
+
catch {
|
|
3446
|
+
// Try the next known article entry point.
|
|
2612
3447
|
}
|
|
2613
|
-
await handle.dispose().catch(() => { });
|
|
2614
|
-
composerSelector = selector;
|
|
2615
|
-
break;
|
|
2616
3448
|
}
|
|
2617
|
-
if (!
|
|
2618
|
-
|
|
3449
|
+
if (!clicked) {
|
|
3450
|
+
const openedExistingDraft = await page
|
|
3451
|
+
.evaluate(() => {
|
|
3452
|
+
const draftAnchor = Array.from(document.querySelectorAll("a[href]")).find((anchor) => {
|
|
3453
|
+
return anchor.href.includes("/compose/articles/edit/");
|
|
3454
|
+
});
|
|
3455
|
+
if (!draftAnchor) {
|
|
3456
|
+
return false;
|
|
3457
|
+
}
|
|
3458
|
+
draftAnchor.click();
|
|
3459
|
+
return true;
|
|
3460
|
+
})
|
|
3461
|
+
.catch(() => false);
|
|
3462
|
+
if (openedExistingDraft) {
|
|
3463
|
+
try {
|
|
3464
|
+
await page.waitForFunction(() => window.location.pathname.includes("/compose/articles/edit/"), undefined, { timeout: 20_000 });
|
|
3465
|
+
await waitForArticleEditorSurface(page);
|
|
3466
|
+
return { ok: true, editUrl: page.url() };
|
|
3467
|
+
}
|
|
3468
|
+
catch {
|
|
3469
|
+
// Fall through to existing edit-url fallback below.
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
if (page.url().includes("/compose/articles/edit/")) {
|
|
3473
|
+
await waitForArticleEditorSurface(page);
|
|
3474
|
+
return { ok: true, editUrl: page.url() };
|
|
3475
|
+
}
|
|
3476
|
+
return { ok: false, reason: "create_button_not_found" };
|
|
2619
3477
|
}
|
|
2620
3478
|
try {
|
|
2621
|
-
await page.
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
await page.type(composerSelector, prompt, { delay: 12 });
|
|
3479
|
+
await page.waitForFunction(({ op }) => op === "article_wait_editor" && window.location.pathname.includes("/compose/articles/edit/"), { op: "article_wait_editor" }, {
|
|
3480
|
+
timeout: 20_000,
|
|
3481
|
+
});
|
|
2625
3482
|
}
|
|
2626
3483
|
catch {
|
|
2627
|
-
return { ok: false, reason: "
|
|
3484
|
+
return { ok: false, reason: "edit_url_not_reached" };
|
|
2628
3485
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
3486
|
+
await waitForArticleEditorSurface(page);
|
|
3487
|
+
const editUrl = page.url();
|
|
3488
|
+
return { ok: true, editUrl };
|
|
3489
|
+
}
|
|
3490
|
+
async function setArticleTitle(page, title) {
|
|
3491
|
+
const trimmed = title.trim();
|
|
3492
|
+
if (!trimmed) {
|
|
3493
|
+
return false;
|
|
3494
|
+
}
|
|
3495
|
+
let interacted = false;
|
|
3496
|
+
if (typeof page.locator === "function") {
|
|
3497
|
+
const titleLocator = page.locator("textarea[placeholder='Add a title']").first();
|
|
3498
|
+
interacted = await titleLocator.click().then(() => true).catch(() => false);
|
|
3499
|
+
if (interacted) {
|
|
3500
|
+
await page.keyboard.press("Meta+A").catch(() => { });
|
|
3501
|
+
await page.keyboard.press("Backspace").catch(() => { });
|
|
3502
|
+
const keyboard = page.keyboard;
|
|
3503
|
+
if (typeof keyboard.insertText === "function") {
|
|
3504
|
+
interacted = await keyboard.insertText(trimmed).then(() => true).catch(() => false);
|
|
3505
|
+
}
|
|
3506
|
+
else if (typeof keyboard.type === "function") {
|
|
3507
|
+
interacted = await keyboard.type(trimmed).then(() => true).catch(() => false);
|
|
3508
|
+
}
|
|
3509
|
+
else {
|
|
3510
|
+
interacted = await titleLocator.fill(trimmed).then(() => true).catch(() => false);
|
|
3511
|
+
}
|
|
3512
|
+
await titleLocator.blur().catch(() => { });
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
const injected = interacted
|
|
3516
|
+
? true
|
|
3517
|
+
: await page.evaluate(({ op, value }) => {
|
|
3518
|
+
if (op !== "article_set_title") {
|
|
3519
|
+
return false;
|
|
3520
|
+
}
|
|
3521
|
+
const input = document.querySelector("textarea[placeholder='Add a title']");
|
|
3522
|
+
if (!(input instanceof HTMLTextAreaElement)) {
|
|
3523
|
+
return false;
|
|
3524
|
+
}
|
|
3525
|
+
input.focus();
|
|
3526
|
+
input.value = "";
|
|
3527
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3528
|
+
input.value = value;
|
|
3529
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3530
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3531
|
+
return true;
|
|
3532
|
+
}, { op: "article_set_title", value: trimmed }).catch(() => false);
|
|
3533
|
+
if (!injected) {
|
|
3534
|
+
return false;
|
|
3535
|
+
}
|
|
3536
|
+
return await page
|
|
3537
|
+
.waitForFunction(({ expectedTitle }) => {
|
|
3538
|
+
const input = document.querySelector("textarea[placeholder='Add a title']");
|
|
3539
|
+
return input instanceof HTMLTextAreaElement && input.value.trim() === expectedTitle;
|
|
3540
|
+
}, { expectedTitle: trimmed }, { timeout: 3_000 })
|
|
3541
|
+
.then(() => true)
|
|
3542
|
+
.catch(() => false);
|
|
3543
|
+
}
|
|
3544
|
+
async function pasteArticleMarkdown(page, markdown, html) {
|
|
3545
|
+
let pasted = false;
|
|
3546
|
+
if (typeof page.locator === "function") {
|
|
3547
|
+
const composerLocator = page.locator("[data-testid='composer'][role='textbox']").first();
|
|
3548
|
+
const clicked = await composerLocator.click().then(() => true).catch(() => false);
|
|
3549
|
+
if (clicked) {
|
|
3550
|
+
const wroteClipboard = await page.evaluate(async ({ plainText, htmlText }) => {
|
|
3551
|
+
try {
|
|
3552
|
+
const ClipboardItemCtor = window.ClipboardItem;
|
|
3553
|
+
if (typeof navigator.clipboard?.write === "function" && ClipboardItemCtor) {
|
|
3554
|
+
const items = {
|
|
3555
|
+
"text/plain": new Blob([plainText], { type: "text/plain" }),
|
|
3556
|
+
};
|
|
3557
|
+
if (typeof htmlText === "string" && htmlText.trim().length > 0) {
|
|
3558
|
+
items["text/html"] = new Blob([htmlText], { type: "text/html" });
|
|
3559
|
+
}
|
|
3560
|
+
await navigator.clipboard.write([new ClipboardItemCtor(items)]);
|
|
3561
|
+
}
|
|
3562
|
+
else if (typeof navigator.clipboard?.writeText === "function") {
|
|
3563
|
+
await navigator.clipboard.writeText(plainText);
|
|
3564
|
+
}
|
|
3565
|
+
else {
|
|
3566
|
+
return false;
|
|
3567
|
+
}
|
|
3568
|
+
return true;
|
|
3569
|
+
}
|
|
3570
|
+
catch {
|
|
3571
|
+
return false;
|
|
3572
|
+
}
|
|
3573
|
+
}, { plainText: markdown, htmlText: html }).catch(() => false);
|
|
3574
|
+
if (wroteClipboard) {
|
|
3575
|
+
const pasteShortcut = process.platform === "darwin" ? "Meta+V" : "Control+V";
|
|
3576
|
+
pasted = await page.keyboard.press(pasteShortcut).then(() => true).catch(() => false);
|
|
3577
|
+
}
|
|
3578
|
+
if (!pasted) {
|
|
3579
|
+
const keyboard = page.keyboard;
|
|
3580
|
+
if (typeof keyboard.insertText === "function") {
|
|
3581
|
+
pasted = await keyboard.insertText(markdown).then(() => true).catch(() => false);
|
|
3582
|
+
}
|
|
3583
|
+
else if (typeof keyboard.type === "function") {
|
|
3584
|
+
pasted = await keyboard.type(markdown).then(() => true).catch(() => false);
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
if (!pasted) {
|
|
3590
|
+
pasted = await page.evaluate(({ op, markdownText, htmlText }) => {
|
|
3591
|
+
if (op !== "article_paste_markdown") {
|
|
3592
|
+
return false;
|
|
3593
|
+
}
|
|
3594
|
+
const composer = document.querySelector("[data-testid='composer'][role='textbox']");
|
|
3595
|
+
if (!(composer instanceof HTMLElement)) {
|
|
3596
|
+
return false;
|
|
3597
|
+
}
|
|
3598
|
+
composer.focus();
|
|
3599
|
+
const data = new DataTransfer();
|
|
3600
|
+
data.setData("text/plain", markdownText);
|
|
3601
|
+
data.setData("text/markdown", markdownText);
|
|
3602
|
+
if (typeof htmlText === "string" && htmlText.trim().length > 0) {
|
|
3603
|
+
data.setData("text/html", htmlText);
|
|
3604
|
+
}
|
|
3605
|
+
const event = new ClipboardEvent("paste", {
|
|
3606
|
+
bubbles: true,
|
|
3607
|
+
cancelable: true,
|
|
3608
|
+
clipboardData: data,
|
|
3609
|
+
});
|
|
3610
|
+
composer.dispatchEvent(event);
|
|
3611
|
+
return true;
|
|
3612
|
+
}, { op: "article_paste_markdown", markdownText: markdown, htmlText: html }).catch(() => false);
|
|
3613
|
+
}
|
|
3614
|
+
if (!pasted) {
|
|
3615
|
+
return false;
|
|
3616
|
+
}
|
|
3617
|
+
const requiredSnippets = extractArticleConfirmationSnippets(markdown);
|
|
3618
|
+
if (requiredSnippets.length === 0) {
|
|
3619
|
+
return true;
|
|
3620
|
+
}
|
|
3621
|
+
try {
|
|
3622
|
+
await page.waitForFunction(({ snippets }) => {
|
|
3623
|
+
const bodyText = document.body?.innerText ?? "";
|
|
3624
|
+
return snippets.every((snippet) => bodyText.includes(snippet));
|
|
3625
|
+
}, { snippets: requiredSnippets }, { timeout: 10_000 });
|
|
3626
|
+
return true;
|
|
3627
|
+
}
|
|
3628
|
+
catch {
|
|
3629
|
+
return false;
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
async function triggerArticleCoverUpload(page) {
|
|
3633
|
+
return ((await page.evaluate(({ op }) => {
|
|
3634
|
+
if (op !== "article_trigger_cover_upload") {
|
|
3635
|
+
return false;
|
|
3636
|
+
}
|
|
3637
|
+
const hint = Array.from(document.querySelectorAll("button, div[role='button'], label, div")).find((element) => {
|
|
3638
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
3639
|
+
return text.includes("5:2 aspect ratio");
|
|
3640
|
+
});
|
|
3641
|
+
if (hint) {
|
|
3642
|
+
hint.click();
|
|
3643
|
+
return true;
|
|
3644
|
+
}
|
|
3645
|
+
const button = Array.from(document.querySelectorAll("button")).find((element) => {
|
|
3646
|
+
const aria = (element.getAttribute("aria-label") || "").toLowerCase();
|
|
3647
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
3648
|
+
return aria.includes("cover") || text.includes("cover");
|
|
3649
|
+
});
|
|
3650
|
+
if (button) {
|
|
3651
|
+
button.click();
|
|
3652
|
+
return true;
|
|
3653
|
+
}
|
|
3654
|
+
return document.querySelector("input[data-testid='fileInput']") !== null;
|
|
3655
|
+
}, { op: "article_trigger_cover_upload" }).catch(() => false)) === true);
|
|
3656
|
+
}
|
|
3657
|
+
async function triggerArticleInlineImageUpload(page) {
|
|
3658
|
+
return ((await page.evaluate(({ op }) => {
|
|
3659
|
+
if (op !== "article_trigger_inline_upload") {
|
|
3660
|
+
return false;
|
|
3661
|
+
}
|
|
3662
|
+
const candidates = [
|
|
3663
|
+
"button[aria-label='Add Media']",
|
|
3664
|
+
"button[aria-label='Add photos or video']",
|
|
3665
|
+
];
|
|
3666
|
+
for (const selector of candidates) {
|
|
3667
|
+
const button = document.querySelector(selector);
|
|
3668
|
+
if (!button) {
|
|
3669
|
+
continue;
|
|
3670
|
+
}
|
|
3671
|
+
button.click();
|
|
3672
|
+
return true;
|
|
3673
|
+
}
|
|
3674
|
+
return document.querySelector("input[data-testid='fileInput']") !== null;
|
|
3675
|
+
}, { op: "article_trigger_inline_upload" }).catch(() => false)) === true);
|
|
3676
|
+
}
|
|
3677
|
+
async function uploadArticleFile(page, filePath) {
|
|
3678
|
+
try {
|
|
3679
|
+
await page.setInputFiles("input[data-testid='fileInput']", filePath);
|
|
3680
|
+
const applyReady = await page
|
|
3681
|
+
.waitForFunction(() => {
|
|
3682
|
+
const buttons = Array.from(document.querySelectorAll("button"));
|
|
3683
|
+
const apply = buttons.find((button) => (button.textContent || "").replace(/\s+/g, " ").trim() === "Apply");
|
|
3684
|
+
if (!apply) {
|
|
3685
|
+
return false;
|
|
3686
|
+
}
|
|
3687
|
+
const ariaDisabled = (apply.getAttribute("aria-disabled") || "").toLowerCase();
|
|
3688
|
+
return !apply.disabled && ariaDisabled !== "true";
|
|
3689
|
+
}, undefined, { timeout: 8_000 })
|
|
3690
|
+
.then(() => true)
|
|
3691
|
+
.catch(() => false);
|
|
3692
|
+
if (applyReady) {
|
|
3693
|
+
if (typeof page.locator === "function") {
|
|
3694
|
+
const applyLocator = page.locator("button:has-text('Apply')").last();
|
|
3695
|
+
await applyLocator.click({ timeout: 2_000, force: true }).catch(() => { });
|
|
3696
|
+
}
|
|
3697
|
+
else {
|
|
3698
|
+
await page.click("button:has-text('Apply')", { timeout: 2_000 }).catch(() => { });
|
|
3699
|
+
}
|
|
3700
|
+
await page
|
|
3701
|
+
.evaluate(() => {
|
|
3702
|
+
const buttons = Array.from(document.querySelectorAll("button, div[role='button']")).filter((element) => {
|
|
3703
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
3704
|
+
return text === "Apply";
|
|
3705
|
+
});
|
|
3706
|
+
const button = buttons[buttons.length - 1];
|
|
3707
|
+
button?.click();
|
|
3708
|
+
})
|
|
3709
|
+
.catch(() => { });
|
|
3710
|
+
await page
|
|
3711
|
+
.waitForFunction(() => {
|
|
3712
|
+
const buttons = Array.from(document.querySelectorAll("button"));
|
|
3713
|
+
return !buttons.some((button) => (button.textContent || "").replace(/\s+/g, " ").trim() === "Apply");
|
|
3714
|
+
}, undefined, { timeout: 8_000 })
|
|
3715
|
+
.catch(() => { });
|
|
3716
|
+
}
|
|
3717
|
+
else {
|
|
3718
|
+
await page.waitForTimeout(1_500);
|
|
3719
|
+
}
|
|
3720
|
+
return true;
|
|
3721
|
+
}
|
|
3722
|
+
catch {
|
|
3723
|
+
return false;
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
async function waitForArticleCoverApplied(page) {
|
|
3727
|
+
return await page
|
|
3728
|
+
.waitForFunction(() => {
|
|
3729
|
+
const hasRemoveControl = Array.from(document.querySelectorAll("button, div[role='button']")).some((element) => {
|
|
3730
|
+
const aria = (element.getAttribute("aria-label") || "").toLowerCase();
|
|
3731
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
3732
|
+
return aria.includes("remove photo") || text === "remove photo";
|
|
3733
|
+
});
|
|
3734
|
+
const coverImages = Array.from((document.querySelector("main") ?? document.body).querySelectorAll("img[src]"))
|
|
3735
|
+
.filter((img) => !img.closest("nav, header, aside"))
|
|
3736
|
+
.map((img) => ({
|
|
3737
|
+
url: img.currentSrc || img.src,
|
|
3738
|
+
width: img.naturalWidth || 0,
|
|
3739
|
+
height: img.naturalHeight || 0,
|
|
3740
|
+
}))
|
|
3741
|
+
.filter((item) => /^https?:\/\//.test(item.url))
|
|
3742
|
+
.filter((item) => !/\/profile_images\/|\/emoji\/|\/hashflags\//.test(item.url))
|
|
3743
|
+
.filter((item) => item.width >= 200 || item.height >= 120);
|
|
3744
|
+
return hasRemoveControl && coverImages.length > 0;
|
|
3745
|
+
}, undefined, { timeout: 15_000 })
|
|
3746
|
+
.then(() => true)
|
|
3747
|
+
.catch(() => false);
|
|
3748
|
+
}
|
|
3749
|
+
async function placeArticleCursorAtMarker(page, marker) {
|
|
3750
|
+
return ((await page.evaluate(({ op, markerText }) => {
|
|
3751
|
+
if (op !== "article_place_marker") {
|
|
3752
|
+
return false;
|
|
3753
|
+
}
|
|
3754
|
+
const composer = document.querySelector("[data-testid='composer'][role='textbox']");
|
|
3755
|
+
if (!(composer instanceof HTMLElement)) {
|
|
3756
|
+
return false;
|
|
3757
|
+
}
|
|
3758
|
+
const walker = document.createTreeWalker(composer, NodeFilter.SHOW_TEXT);
|
|
3759
|
+
let current = walker.nextNode();
|
|
3760
|
+
while (current) {
|
|
3761
|
+
const textNode = current;
|
|
3762
|
+
const content = textNode.textContent ?? "";
|
|
3763
|
+
const index = content.indexOf(markerText);
|
|
3764
|
+
if (index >= 0) {
|
|
3765
|
+
const selection = window.getSelection();
|
|
3766
|
+
if (!selection) {
|
|
3767
|
+
return false;
|
|
3768
|
+
}
|
|
3769
|
+
const range = document.createRange();
|
|
3770
|
+
range.setStart(textNode, index);
|
|
3771
|
+
range.setEnd(textNode, index + markerText.length);
|
|
3772
|
+
selection.removeAllRanges();
|
|
3773
|
+
selection.addRange(range);
|
|
3774
|
+
return true;
|
|
3775
|
+
}
|
|
3776
|
+
current = walker.nextNode();
|
|
3777
|
+
}
|
|
3778
|
+
return false;
|
|
3779
|
+
}, { op: "article_place_marker", markerText: marker }).catch(() => false)) === true);
|
|
3780
|
+
}
|
|
3781
|
+
async function deleteArticleSelectedMarker(page) {
|
|
3782
|
+
await page.keyboard.press("Backspace").catch(() => { });
|
|
3783
|
+
await page.waitForTimeout(200);
|
|
3784
|
+
}
|
|
3785
|
+
async function uploadArticleInlineImages(page, images) {
|
|
3786
|
+
for (const image of images) {
|
|
3787
|
+
const resolved = await resolveArticleAttachment(image.path, image.marker);
|
|
3788
|
+
if (!resolved.ok || !resolved.attachment) {
|
|
3789
|
+
return { ok: false, reason: "inline_image_missing" };
|
|
3790
|
+
}
|
|
3791
|
+
const positioned = await placeArticleCursorAtMarker(page, image.marker);
|
|
3792
|
+
if (!positioned) {
|
|
3793
|
+
return { ok: false, reason: "inline_marker_not_found" };
|
|
3794
|
+
}
|
|
3795
|
+
await deleteArticleSelectedMarker(page);
|
|
3796
|
+
const triggered = await triggerArticleInlineImageUpload(page);
|
|
3797
|
+
if (!triggered) {
|
|
3798
|
+
return { ok: false, reason: "inline_upload_trigger_not_found" };
|
|
3799
|
+
}
|
|
3800
|
+
const uploaded = await uploadArticleFile(page, resolved.attachment.path);
|
|
3801
|
+
if (!uploaded) {
|
|
3802
|
+
return { ok: false, reason: "inline_upload_failed" };
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
return { ok: true };
|
|
3806
|
+
}
|
|
3807
|
+
async function clearArticleBody(page) {
|
|
3808
|
+
let cleared = false;
|
|
3809
|
+
if (typeof page.locator === "function") {
|
|
3810
|
+
const composerLocator = page.locator("[data-testid='composer'][role='textbox']").first();
|
|
3811
|
+
cleared = await composerLocator.click().then(() => true).catch(() => false);
|
|
3812
|
+
if (cleared) {
|
|
3813
|
+
await page.keyboard.press("Meta+A").catch(() => { });
|
|
3814
|
+
await page.keyboard.press("Backspace").catch(() => { });
|
|
3815
|
+
await page.waitForTimeout(200);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
if (cleared) {
|
|
3819
|
+
return true;
|
|
3820
|
+
}
|
|
3821
|
+
return await page.evaluate(({ op }) => {
|
|
3822
|
+
if (op !== "article_clear_body") {
|
|
3823
|
+
return false;
|
|
3824
|
+
}
|
|
3825
|
+
const composer = document.querySelector("[data-testid='composer'][role='textbox']");
|
|
3826
|
+
if (!(composer instanceof HTMLElement)) {
|
|
3827
|
+
return false;
|
|
3828
|
+
}
|
|
3829
|
+
composer.focus();
|
|
3830
|
+
composer.textContent = "";
|
|
3831
|
+
composer.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3832
|
+
return true;
|
|
3833
|
+
}, { op: "article_clear_body" }).catch(() => false);
|
|
3834
|
+
}
|
|
3835
|
+
function parseArticleIdFromUrl(url) {
|
|
3836
|
+
let parsed;
|
|
3837
|
+
try {
|
|
3838
|
+
parsed = new URL(url, "https://x.com");
|
|
3839
|
+
}
|
|
3840
|
+
catch {
|
|
3841
|
+
return undefined;
|
|
3842
|
+
}
|
|
3843
|
+
if (!ALLOWED_X_HOSTS.has(parsed.hostname.toLowerCase())) {
|
|
3844
|
+
return undefined;
|
|
3845
|
+
}
|
|
3846
|
+
const match = parsed.pathname.match(/^\/(?:compose\/articles\/edit|i\/article|i\/articles|[^/]+\/article|articles)\/(\d+)(?:\/preview)?(?:\/|$)/);
|
|
3847
|
+
return match?.[1];
|
|
3848
|
+
}
|
|
3849
|
+
function buildArticleEditUrl(articleId) {
|
|
3850
|
+
return `https://x.com/compose/articles/edit/${encodeURIComponent(articleId)}`;
|
|
3851
|
+
}
|
|
3852
|
+
function buildArticlePreviewUrl(articleId) {
|
|
3853
|
+
return `https://x.com/i/articles/${encodeURIComponent(articleId)}/preview`;
|
|
3854
|
+
}
|
|
3855
|
+
function isArticlePreviewUrl(url) {
|
|
3856
|
+
try {
|
|
3857
|
+
const parsed = new URL(url, "https://x.com");
|
|
3858
|
+
return /\/preview(?:\/|$)/.test(parsed.pathname) && parseArticleIdFromUrl(parsed.toString()) !== undefined;
|
|
3859
|
+
}
|
|
3860
|
+
catch {
|
|
3861
|
+
return false;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
async function waitForCapturedOperation(page, op, timeoutMs = 10_000) {
|
|
3865
|
+
await page.waitForFunction(({ targetOp }) => {
|
|
3866
|
+
const globalAny = window;
|
|
3867
|
+
const entries = Array.isArray(globalAny.__WEBMCP_X_CAPTURE__?.entries)
|
|
3868
|
+
? globalAny.__WEBMCP_X_CAPTURE__.entries
|
|
3869
|
+
: [];
|
|
3870
|
+
return entries.some((entry) => entry && entry.op === targetOp && !!entry.url && !!entry.method);
|
|
3871
|
+
}, { targetOp: op }, { timeout: timeoutMs }).catch(() => { });
|
|
3872
|
+
}
|
|
3873
|
+
function normalizeArticleUrl(url, articleId) {
|
|
3874
|
+
const trimmed = typeof url === "string" ? url.trim() : "";
|
|
3875
|
+
if (trimmed) {
|
|
3876
|
+
return trimmed;
|
|
3877
|
+
}
|
|
3878
|
+
return articleId ? `https://x.com/compose/articles/edit/${encodeURIComponent(articleId)}` : undefined;
|
|
3879
|
+
}
|
|
3880
|
+
function sanitizeArticleText(text) {
|
|
3881
|
+
return text
|
|
3882
|
+
.replace(/\u00a0/g, " ")
|
|
3883
|
+
.replace(/\s+\n/g, "\n")
|
|
3884
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
3885
|
+
.trim();
|
|
3886
|
+
}
|
|
3887
|
+
async function waitForArticleReadSurface(page) {
|
|
3888
|
+
await page
|
|
3889
|
+
.waitForFunction(() => {
|
|
3890
|
+
const title = document.querySelector("h1, textarea");
|
|
3891
|
+
const composer = document.querySelector("[data-testid='composer'][role='textbox']");
|
|
3892
|
+
const articleNodes = document.querySelectorAll("article, main img[src], script[type='application/ld+json']").length;
|
|
3893
|
+
return title !== null || composer !== null || articleNodes > 0;
|
|
3894
|
+
}, undefined, { timeout: 12_000 })
|
|
3895
|
+
.catch(() => { });
|
|
3896
|
+
await page.waitForTimeout(600);
|
|
3897
|
+
}
|
|
3898
|
+
async function readArticleFromEditorPage(page, articleId, sessionScoped) {
|
|
3899
|
+
const article = await page.evaluate(({ op }) => {
|
|
3900
|
+
if (op !== "article_collect_editor") {
|
|
3901
|
+
return undefined;
|
|
3902
|
+
}
|
|
3903
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
3904
|
+
const title = document.querySelector("textarea")?.value?.trim() ||
|
|
3905
|
+
normalize(document.querySelector("h1")?.innerText || "");
|
|
3906
|
+
const composer = document.querySelector("[data-testid='composer'][role='textbox']");
|
|
3907
|
+
const rawText = (composer?.innerText || composer?.textContent || "").trim();
|
|
3908
|
+
const editorRoot = document.querySelector("main") ?? document.body;
|
|
3909
|
+
const images = Array.from(editorRoot.querySelectorAll("img[src]"))
|
|
3910
|
+
.filter((img) => !img.closest("nav, header, aside, [data-testid='SideNav_AccountSwitcher_Button']"))
|
|
3911
|
+
.map((img) => ({
|
|
3912
|
+
url: img.currentSrc || img.src,
|
|
3913
|
+
alt: normalize(img.alt || ""),
|
|
3914
|
+
width: img.naturalWidth || 0,
|
|
3915
|
+
height: img.naturalHeight || 0,
|
|
3916
|
+
}))
|
|
3917
|
+
.filter((item) => /^https?:\/\//.test(item.url))
|
|
3918
|
+
.filter((item) => item.width > 64 || item.height > 64)
|
|
3919
|
+
.filter((item) => !/\/profile_images\/|\/emoji\/|\/hashflags\//.test(item.url));
|
|
3920
|
+
const deduped = new Map();
|
|
3921
|
+
for (const image of images) {
|
|
3922
|
+
if (!deduped.has(image.url)) {
|
|
3923
|
+
deduped.set(image.url, image.alt ? { url: image.url, alt: image.alt } : { url: image.url });
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
return {
|
|
3927
|
+
title,
|
|
3928
|
+
text: rawText,
|
|
3929
|
+
images: Array.from(deduped.values()),
|
|
3930
|
+
editUrl: window.location.href,
|
|
3931
|
+
};
|
|
3932
|
+
}, { op: "article_collect_editor" }).catch(() => undefined);
|
|
3933
|
+
if (!article || typeof article !== "object") {
|
|
3934
|
+
return errorResult("UPSTREAM_CHANGED", "article editor content not found");
|
|
3935
|
+
}
|
|
3936
|
+
const title = typeof article.title === "string" ? article.title : "";
|
|
3937
|
+
const rawText = typeof article.text === "string" ? article.text : "";
|
|
3938
|
+
const normalizedTitleHeading = title ? `# ${title}` : "";
|
|
3939
|
+
let text = sanitizeArticleText(rawText);
|
|
3940
|
+
if (normalizedTitleHeading && text.startsWith(normalizedTitleHeading)) {
|
|
3941
|
+
text = sanitizeArticleText(text.slice(normalizedTitleHeading.length));
|
|
3942
|
+
}
|
|
3943
|
+
const images = Array.isArray(article.images) ? article.images : [];
|
|
3944
|
+
const coverImage = images[0];
|
|
3945
|
+
const inlineImages = coverImage ? images.slice(1) : images;
|
|
3946
|
+
const output = {
|
|
3947
|
+
article: {
|
|
3948
|
+
...(articleId ? { id: articleId } : {}),
|
|
3949
|
+
title,
|
|
3950
|
+
text,
|
|
3951
|
+
editUrl: typeof article.editUrl === "string" ? article.editUrl : page.url(),
|
|
3952
|
+
images: inlineImages,
|
|
3953
|
+
source: "editor",
|
|
3954
|
+
published: false,
|
|
3955
|
+
},
|
|
3956
|
+
};
|
|
3957
|
+
if (articleId) {
|
|
3958
|
+
output.article.previewUrl = buildArticlePreviewUrl(articleId);
|
|
3959
|
+
}
|
|
3960
|
+
if (coverImage && typeof coverImage === "object" && coverImage !== null && "url" in coverImage) {
|
|
3961
|
+
output.article.coverImageUrl = coverImage.url;
|
|
3962
|
+
output.article.hasCoverImage = true;
|
|
3963
|
+
}
|
|
3964
|
+
else {
|
|
3965
|
+
output.article.hasCoverImage = false;
|
|
3966
|
+
}
|
|
3967
|
+
if (articleId) {
|
|
3968
|
+
output.article.sessionScoped = sessionScoped === true;
|
|
3969
|
+
}
|
|
3970
|
+
return output;
|
|
3971
|
+
}
|
|
3972
|
+
async function listArticleDrafts(page) {
|
|
3973
|
+
return await withEphemeralPage(page, "https://x.com/compose/articles", async (articlePage) => {
|
|
3974
|
+
await articlePage.waitForTimeout(1_000);
|
|
3975
|
+
await waitForCapturedOperation(articlePage, "ArticleEntitiesSlice", 12_000);
|
|
3976
|
+
const result = await articlePage.evaluate(async ({ op }) => {
|
|
3977
|
+
if (op !== "article_list_drafts") {
|
|
3978
|
+
return undefined;
|
|
3979
|
+
}
|
|
3980
|
+
const normalizeInline = (value) => value.replace(/\s+/g, " ").trim();
|
|
3981
|
+
const parseJsonSafely = (value) => {
|
|
3982
|
+
if (!value) {
|
|
3983
|
+
return {};
|
|
3984
|
+
}
|
|
3985
|
+
try {
|
|
3986
|
+
const parsed = JSON.parse(value);
|
|
3987
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
3988
|
+
}
|
|
3989
|
+
catch {
|
|
3990
|
+
return {};
|
|
3991
|
+
}
|
|
3992
|
+
};
|
|
3993
|
+
const toHttpsImage = (value) => typeof value === "string" && /^https?:\/\//.test(value) ? value : undefined;
|
|
3994
|
+
const sanitizeHeaders = (headers) => {
|
|
3995
|
+
const blockedPrefixes = ["sec-", ":"];
|
|
3996
|
+
const blockedExact = new Set(["host", "content-length", "cookie", "origin", "referer", "connection"]);
|
|
3997
|
+
const output = {};
|
|
3998
|
+
if (!headers) {
|
|
3999
|
+
return output;
|
|
4000
|
+
}
|
|
4001
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4002
|
+
const normalized = key.toLowerCase();
|
|
4003
|
+
if (blockedExact.has(normalized) || blockedPrefixes.some((prefix) => normalized.startsWith(prefix))) {
|
|
4004
|
+
continue;
|
|
4005
|
+
}
|
|
4006
|
+
output[normalized] = value;
|
|
4007
|
+
}
|
|
4008
|
+
return output;
|
|
4009
|
+
};
|
|
4010
|
+
const extractUpdatedAt = (record) => {
|
|
4011
|
+
const metadata = record.metadata ?? {};
|
|
4012
|
+
const candidates = [
|
|
4013
|
+
metadata.updated_at_secs,
|
|
4014
|
+
metadata.last_edited_at_secs,
|
|
4015
|
+
metadata.last_updated_at_secs,
|
|
4016
|
+
metadata.created_at_secs,
|
|
4017
|
+
metadata.first_published_at_secs,
|
|
4018
|
+
];
|
|
4019
|
+
for (const candidate of candidates) {
|
|
4020
|
+
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
|
|
4021
|
+
return new Date(candidate * 1000).toISOString();
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
return undefined;
|
|
4025
|
+
};
|
|
4026
|
+
const extractNextCursor = (sliceInfo) => {
|
|
4027
|
+
if (!sliceInfo || typeof sliceInfo !== "object" || Array.isArray(sliceInfo)) {
|
|
4028
|
+
return undefined;
|
|
4029
|
+
}
|
|
4030
|
+
const record = sliceInfo;
|
|
4031
|
+
const keys = ["next_cursor", "nextCursor", "cursor", "bottom_cursor"];
|
|
4032
|
+
for (const key of keys) {
|
|
4033
|
+
if (typeof record[key] === "string" && record[key]) {
|
|
4034
|
+
return record[key];
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
return undefined;
|
|
4038
|
+
};
|
|
4039
|
+
const globalAny = window;
|
|
4040
|
+
const entries = Array.isArray(globalAny.__WEBMCP_X_CAPTURE__?.entries)
|
|
4041
|
+
? globalAny.__WEBMCP_X_CAPTURE__?.entries ?? []
|
|
4042
|
+
: [];
|
|
4043
|
+
const template = (() => {
|
|
4044
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
4045
|
+
const entry = entries[i];
|
|
4046
|
+
if (!entry || entry.op !== "ArticleEntitiesSlice" || !entry.url || !entry.method) {
|
|
4047
|
+
continue;
|
|
4048
|
+
}
|
|
4049
|
+
return {
|
|
4050
|
+
url: entry.url,
|
|
4051
|
+
method: entry.method,
|
|
4052
|
+
headers: entry.headers ?? {},
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
return null;
|
|
4056
|
+
})();
|
|
4057
|
+
if (!template) {
|
|
4058
|
+
return { drafts: [] };
|
|
4059
|
+
}
|
|
4060
|
+
const templateUrl = new URL(template.url, location.origin);
|
|
4061
|
+
const templateVariables = parseJsonSafely(templateUrl.searchParams.get("variables"));
|
|
4062
|
+
const templateFeatures = parseJsonSafely(templateUrl.searchParams.get("features"));
|
|
4063
|
+
const headers = sanitizeHeaders(template.headers);
|
|
4064
|
+
const userId = typeof templateVariables.userId === "string" ? templateVariables.userId : "";
|
|
4065
|
+
if (!userId) {
|
|
4066
|
+
return { drafts: [] };
|
|
4067
|
+
}
|
|
4068
|
+
const fetchSlice = async (cursor) => {
|
|
4069
|
+
const vars = {
|
|
4070
|
+
...templateVariables,
|
|
4071
|
+
userId,
|
|
4072
|
+
lifecycle: "Draft",
|
|
4073
|
+
count: 20,
|
|
4074
|
+
};
|
|
4075
|
+
if (cursor) {
|
|
4076
|
+
vars.cursor = cursor;
|
|
4077
|
+
}
|
|
4078
|
+
else {
|
|
4079
|
+
delete vars.cursor;
|
|
4080
|
+
}
|
|
4081
|
+
const requestUrl = new URL(template.url, location.origin);
|
|
4082
|
+
requestUrl.searchParams.set("variables", JSON.stringify(vars));
|
|
4083
|
+
if (Object.keys(templateFeatures).length > 0) {
|
|
4084
|
+
requestUrl.searchParams.set("features", JSON.stringify(templateFeatures));
|
|
4085
|
+
}
|
|
4086
|
+
const response = await fetch(requestUrl.toString(), {
|
|
4087
|
+
method: template.method,
|
|
4088
|
+
headers,
|
|
4089
|
+
credentials: "include",
|
|
4090
|
+
});
|
|
4091
|
+
if (!response.ok) {
|
|
4092
|
+
throw new Error(`http_${response.status}`);
|
|
4093
|
+
}
|
|
4094
|
+
return await response.json();
|
|
4095
|
+
};
|
|
4096
|
+
const drafts = [];
|
|
4097
|
+
let cursor;
|
|
4098
|
+
for (let pageIndex = 0; pageIndex < 8; pageIndex += 1) {
|
|
4099
|
+
let responseJson;
|
|
4100
|
+
try {
|
|
4101
|
+
responseJson = await fetchSlice(cursor);
|
|
4102
|
+
}
|
|
4103
|
+
catch {
|
|
4104
|
+
break;
|
|
4105
|
+
}
|
|
4106
|
+
const slice = responseJson?.data?.user;
|
|
4107
|
+
const result = slice?.result?.articles_article_mixer_slice;
|
|
4108
|
+
const items = Array.isArray(result?.items) ? result.items : [];
|
|
4109
|
+
for (const rawItem of items) {
|
|
4110
|
+
if (!rawItem || typeof rawItem !== "object" || Array.isArray(rawItem)) {
|
|
4111
|
+
continue;
|
|
4112
|
+
}
|
|
4113
|
+
const item = rawItem;
|
|
4114
|
+
const articleResult = item.article_entity_results?.result;
|
|
4115
|
+
if (!articleResult) {
|
|
4116
|
+
continue;
|
|
4117
|
+
}
|
|
4118
|
+
const restId = typeof articleResult.rest_id === "string" ? articleResult.rest_id : "";
|
|
4119
|
+
if (!restId) {
|
|
4120
|
+
continue;
|
|
4121
|
+
}
|
|
4122
|
+
const coverMedia = articleResult.cover_media;
|
|
4123
|
+
const hasCoverImage = toHttpsImage(coverMedia?.original_img_url) ??
|
|
4124
|
+
toHttpsImage(coverMedia?.media_url_https) ??
|
|
4125
|
+
toHttpsImage(coverMedia?.media_url);
|
|
4126
|
+
drafts.push({
|
|
4127
|
+
id: restId,
|
|
4128
|
+
title: typeof articleResult.title === "string" ? normalizeInline(articleResult.title) : "",
|
|
4129
|
+
updatedAt: extractUpdatedAt(articleResult),
|
|
4130
|
+
hasCoverImage: Boolean(hasCoverImage),
|
|
4131
|
+
editUrl: buildArticleEditUrl(restId),
|
|
4132
|
+
previewUrl: buildArticlePreviewUrl(restId),
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
cursor = extractNextCursor(result?.slice_info);
|
|
4136
|
+
if (!cursor) {
|
|
4137
|
+
break;
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
return { drafts };
|
|
4141
|
+
}, { op: "article_list_drafts" }).catch(() => undefined);
|
|
4142
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
4143
|
+
return errorResult("UPSTREAM_CHANGED", "article drafts could not be listed");
|
|
4144
|
+
}
|
|
4145
|
+
const drafts = Array.isArray(result.drafts)
|
|
4146
|
+
? result.drafts
|
|
4147
|
+
: [];
|
|
4148
|
+
const normalizedDrafts = drafts
|
|
4149
|
+
.map((draft) => {
|
|
4150
|
+
if (!draft || typeof draft !== "object" || Array.isArray(draft)) {
|
|
4151
|
+
return undefined;
|
|
4152
|
+
}
|
|
4153
|
+
const entry = draft;
|
|
4154
|
+
if (typeof entry.id !== "string" ||
|
|
4155
|
+
typeof entry.editUrl !== "string" ||
|
|
4156
|
+
typeof entry.previewUrl !== "string") {
|
|
4157
|
+
return undefined;
|
|
4158
|
+
}
|
|
4159
|
+
return {
|
|
4160
|
+
id: entry.id,
|
|
4161
|
+
editUrl: entry.editUrl,
|
|
4162
|
+
previewUrl: entry.previewUrl,
|
|
4163
|
+
title: typeof entry.title === "string" ? entry.title : "",
|
|
4164
|
+
hasCoverImage: entry.hasCoverImage === true,
|
|
4165
|
+
...(typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {}),
|
|
4166
|
+
};
|
|
4167
|
+
})
|
|
4168
|
+
.filter((draft) => draft !== undefined);
|
|
4169
|
+
for (const draft of normalizedDrafts) {
|
|
4170
|
+
if (draft.hasCoverImage) {
|
|
4171
|
+
continue;
|
|
4172
|
+
}
|
|
4173
|
+
const cachedPage = getCachedArticleDraftPage(page, draft.id);
|
|
4174
|
+
if (cachedPage) {
|
|
4175
|
+
const cachedRead = await readArticleFromEditorPage(cachedPage, draft.id, true);
|
|
4176
|
+
if (cachedRead &&
|
|
4177
|
+
typeof cachedRead === "object" &&
|
|
4178
|
+
!Array.isArray(cachedRead) &&
|
|
4179
|
+
"article" in cachedRead &&
|
|
4180
|
+
cachedRead.article &&
|
|
4181
|
+
typeof cachedRead.article === "object" &&
|
|
4182
|
+
!Array.isArray(cachedRead.article) &&
|
|
4183
|
+
cachedRead.article.hasCoverImage === true) {
|
|
4184
|
+
draft.hasCoverImage = true;
|
|
4185
|
+
continue;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
const liveRead = await withEphemeralPage(page, buildArticleEditUrl(draft.id), async (draftPage) => {
|
|
4189
|
+
await waitForArticleEditorSurface(draftPage);
|
|
4190
|
+
await ensureArticleDraftLoaded(draftPage, draft.id);
|
|
4191
|
+
return await readArticleFromEditorPage(draftPage, draft.id, false);
|
|
4192
|
+
}).catch(() => undefined);
|
|
4193
|
+
if (liveRead &&
|
|
4194
|
+
typeof liveRead === "object" &&
|
|
4195
|
+
!Array.isArray(liveRead) &&
|
|
4196
|
+
"article" in liveRead &&
|
|
4197
|
+
liveRead.article &&
|
|
4198
|
+
typeof liveRead.article === "object" &&
|
|
4199
|
+
!Array.isArray(liveRead.article) &&
|
|
4200
|
+
liveRead.article.hasCoverImage === true) {
|
|
4201
|
+
draft.hasCoverImage = true;
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
return {
|
|
4205
|
+
drafts: normalizedDrafts,
|
|
4206
|
+
};
|
|
4207
|
+
});
|
|
4208
|
+
}
|
|
4209
|
+
async function getArticleDraft(page, targetUrl) {
|
|
4210
|
+
const articleId = parseArticleIdFromUrl(targetUrl);
|
|
4211
|
+
if (!articleId) {
|
|
4212
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
4213
|
+
}
|
|
4214
|
+
return await withArticleDraftPage(page, buildArticleEditUrl(articleId), async (articlePage, resolvedId, sessionScoped) => {
|
|
4215
|
+
return await readArticleFromEditorPage(articlePage, resolvedId ?? articleId, sessionScoped);
|
|
4216
|
+
});
|
|
4217
|
+
}
|
|
4218
|
+
function parseArticleReadErrorCode(value) {
|
|
4219
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4220
|
+
return undefined;
|
|
4221
|
+
}
|
|
4222
|
+
const error = "error" in value ? value.error : undefined;
|
|
4223
|
+
if (!error || typeof error !== "object" || Array.isArray(error)) {
|
|
4224
|
+
return undefined;
|
|
4225
|
+
}
|
|
4226
|
+
return typeof error.code === "string"
|
|
4227
|
+
? error.code
|
|
4228
|
+
: undefined;
|
|
4229
|
+
}
|
|
4230
|
+
async function readArticleFromOwnedSlices(page, articleId) {
|
|
4231
|
+
return await withEphemeralPage(page, "https://x.com/compose/articles", async (articlePage) => {
|
|
4232
|
+
await waitForCapturedOperation(articlePage, "ArticleEntitiesSlice", 12_000);
|
|
4233
|
+
const article = await articlePage.evaluate(async ({ op, articleId: targetArticleId }) => {
|
|
4234
|
+
if (op !== "article_collect_owned") {
|
|
4235
|
+
return undefined;
|
|
4236
|
+
}
|
|
4237
|
+
const globalAny = window;
|
|
4238
|
+
const capture = globalAny.__WEBMCP_X_CAPTURE__;
|
|
4239
|
+
const entries = Array.isArray(capture?.entries) ? capture.entries : [];
|
|
4240
|
+
const pickTemplate = () => {
|
|
4241
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
4242
|
+
const entry = entries[i];
|
|
4243
|
+
if (!entry || entry.op !== "ArticleEntitiesSlice" || !entry.url || !entry.method) {
|
|
4244
|
+
continue;
|
|
4245
|
+
}
|
|
4246
|
+
return {
|
|
4247
|
+
url: entry.url,
|
|
4248
|
+
method: entry.method,
|
|
4249
|
+
headers: entry.headers ?? {},
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
return null;
|
|
4253
|
+
};
|
|
4254
|
+
const template = pickTemplate();
|
|
4255
|
+
if (!template) {
|
|
4256
|
+
return { error: "no_template" };
|
|
4257
|
+
}
|
|
4258
|
+
const sanitizeHeaders = (headers) => {
|
|
4259
|
+
const blockedPrefixes = ["sec-", ":"];
|
|
4260
|
+
const blockedExact = new Set(["host", "content-length", "cookie", "origin", "referer", "connection"]);
|
|
4261
|
+
const output = {};
|
|
4262
|
+
if (!headers) {
|
|
4263
|
+
return output;
|
|
4264
|
+
}
|
|
4265
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4266
|
+
const normalized = key.toLowerCase();
|
|
4267
|
+
if (blockedExact.has(normalized) || blockedPrefixes.some((prefix) => normalized.startsWith(prefix))) {
|
|
4268
|
+
continue;
|
|
4269
|
+
}
|
|
4270
|
+
output[normalized] = value;
|
|
4271
|
+
}
|
|
4272
|
+
return output;
|
|
4273
|
+
};
|
|
4274
|
+
const normalizeText = (value) => value.replace(/\u00a0/g, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
4275
|
+
const normalizeInline = (value) => value.replace(/\s+/g, " ").trim();
|
|
4276
|
+
const parseJsonSafely = (value) => {
|
|
4277
|
+
if (!value) {
|
|
4278
|
+
return {};
|
|
4279
|
+
}
|
|
4280
|
+
try {
|
|
4281
|
+
const parsed = JSON.parse(value);
|
|
4282
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
4283
|
+
}
|
|
4284
|
+
catch {
|
|
4285
|
+
return {};
|
|
4286
|
+
}
|
|
4287
|
+
};
|
|
4288
|
+
const toHttpsImage = (value) => typeof value === "string" && /^https?:\/\//.test(value) ? value : undefined;
|
|
4289
|
+
const parseMediaImage = (value) => {
|
|
4290
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4291
|
+
return [];
|
|
4292
|
+
}
|
|
4293
|
+
const record = value;
|
|
4294
|
+
const url = toHttpsImage(record.original_img_url) ??
|
|
4295
|
+
toHttpsImage(record.media_url_https) ??
|
|
4296
|
+
toHttpsImage(record.media_url) ??
|
|
4297
|
+
toHttpsImage(record.image_info?.original_img_url);
|
|
4298
|
+
if (!url) {
|
|
4299
|
+
return [];
|
|
4300
|
+
}
|
|
4301
|
+
const alt = typeof record.alt_text === "string" ? normalizeInline(record.alt_text) : "";
|
|
4302
|
+
return [alt ? { url, alt } : { url }];
|
|
4303
|
+
};
|
|
4304
|
+
const parseBlocksText = (value) => {
|
|
4305
|
+
if (!Array.isArray(value)) {
|
|
4306
|
+
return "";
|
|
4307
|
+
}
|
|
4308
|
+
const lines = [];
|
|
4309
|
+
for (const rawBlock of value) {
|
|
4310
|
+
if (!rawBlock || typeof rawBlock !== "object" || Array.isArray(rawBlock)) {
|
|
4311
|
+
continue;
|
|
4312
|
+
}
|
|
4313
|
+
const block = rawBlock;
|
|
4314
|
+
const rawText = typeof block.text === "string" ? block.text : "";
|
|
4315
|
+
const text = normalizeText(rawText);
|
|
4316
|
+
if (!text) {
|
|
4317
|
+
lines.push("");
|
|
4318
|
+
continue;
|
|
4319
|
+
}
|
|
4320
|
+
const type = typeof block.type === "string" ? block.type : "";
|
|
4321
|
+
if (type === "unordered-list-item") {
|
|
4322
|
+
lines.push(`- ${text}`);
|
|
4323
|
+
}
|
|
4324
|
+
else if (type === "ordered-list-item") {
|
|
4325
|
+
lines.push(`1. ${text}`);
|
|
4326
|
+
}
|
|
4327
|
+
else {
|
|
4328
|
+
lines.push(text);
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
return normalizeText(lines.join("\n"));
|
|
4332
|
+
};
|
|
4333
|
+
const extractNextCursor = (sliceInfo) => {
|
|
4334
|
+
if (!sliceInfo || typeof sliceInfo !== "object" || Array.isArray(sliceInfo)) {
|
|
4335
|
+
return undefined;
|
|
4336
|
+
}
|
|
4337
|
+
const record = sliceInfo;
|
|
4338
|
+
const keys = ["next_cursor", "nextCursor", "cursor", "bottom_cursor"];
|
|
4339
|
+
for (const key of keys) {
|
|
4340
|
+
if (typeof record[key] === "string" && record[key]) {
|
|
4341
|
+
return record[key];
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
return undefined;
|
|
4345
|
+
};
|
|
4346
|
+
const templateUrl = new URL(template.url, location.origin);
|
|
4347
|
+
const templateVariables = parseJsonSafely(templateUrl.searchParams.get("variables"));
|
|
4348
|
+
const templateFeatures = parseJsonSafely(templateUrl.searchParams.get("features"));
|
|
4349
|
+
const headers = sanitizeHeaders(template.headers);
|
|
4350
|
+
const userId = typeof templateVariables.userId === "string" ? templateVariables.userId : "";
|
|
4351
|
+
if (!userId) {
|
|
4352
|
+
return { error: "missing_user_id" };
|
|
4353
|
+
}
|
|
4354
|
+
const fetchSlice = async (lifecycle, cursor) => {
|
|
4355
|
+
const vars = {
|
|
4356
|
+
...templateVariables,
|
|
4357
|
+
userId,
|
|
4358
|
+
lifecycle,
|
|
4359
|
+
count: 20,
|
|
4360
|
+
};
|
|
4361
|
+
if (cursor) {
|
|
4362
|
+
vars.cursor = cursor;
|
|
4363
|
+
}
|
|
4364
|
+
else {
|
|
4365
|
+
delete vars.cursor;
|
|
4366
|
+
}
|
|
4367
|
+
const requestUrl = new URL(template.url, location.origin);
|
|
4368
|
+
requestUrl.searchParams.set("variables", JSON.stringify(vars));
|
|
4369
|
+
if (Object.keys(templateFeatures).length > 0) {
|
|
4370
|
+
requestUrl.searchParams.set("features", JSON.stringify(templateFeatures));
|
|
4371
|
+
}
|
|
4372
|
+
const response = await fetch(requestUrl.toString(), {
|
|
4373
|
+
method: template.method,
|
|
4374
|
+
headers,
|
|
4375
|
+
credentials: "include",
|
|
4376
|
+
});
|
|
4377
|
+
if (!response.ok) {
|
|
4378
|
+
throw new Error(`http_${response.status}`);
|
|
4379
|
+
}
|
|
4380
|
+
return await response.json();
|
|
4381
|
+
};
|
|
4382
|
+
for (const lifecycle of ["Draft", "Published"]) {
|
|
4383
|
+
let cursor;
|
|
4384
|
+
for (let pageIndex = 0; pageIndex < 8; pageIndex += 1) {
|
|
4385
|
+
let responseJson;
|
|
4386
|
+
try {
|
|
4387
|
+
responseJson = await fetchSlice(lifecycle, cursor);
|
|
4388
|
+
}
|
|
4389
|
+
catch (error) {
|
|
4390
|
+
return { error: String(error) };
|
|
4391
|
+
}
|
|
4392
|
+
const slice = responseJson?.data?.user;
|
|
4393
|
+
const result = slice?.result?.articles_article_mixer_slice;
|
|
4394
|
+
const items = Array.isArray(result?.items) ? result.items : [];
|
|
4395
|
+
for (const rawItem of items) {
|
|
4396
|
+
if (!rawItem || typeof rawItem !== "object" || Array.isArray(rawItem)) {
|
|
4397
|
+
continue;
|
|
4398
|
+
}
|
|
4399
|
+
const item = rawItem;
|
|
4400
|
+
const articleResult = item.article_entity_results?.result;
|
|
4401
|
+
if (!articleResult) {
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
const restId = typeof articleResult.rest_id === "string" ? articleResult.rest_id : "";
|
|
4405
|
+
if (restId !== targetArticleId) {
|
|
4406
|
+
continue;
|
|
4407
|
+
}
|
|
4408
|
+
const title = typeof articleResult.title === "string" ? normalizeInline(articleResult.title) : "";
|
|
4409
|
+
const text = parseBlocksText(articleResult.content_state?.blocks);
|
|
4410
|
+
const metadata = articleResult.metadata ?? {};
|
|
4411
|
+
const authorResult = (metadata.author_results?.result ??
|
|
4412
|
+
{});
|
|
4413
|
+
const authorCore = authorResult.core ?? {};
|
|
4414
|
+
const authorName = typeof authorCore.name === "string" ? normalizeInline(authorCore.name) : "";
|
|
4415
|
+
const authorHandleRaw = typeof authorCore.screen_name === "string" ? authorCore.screen_name : "";
|
|
4416
|
+
const authorHandle = authorHandleRaw ? `@${authorHandleRaw.replace(/^@+/, "")}` : "";
|
|
4417
|
+
const coverMedia = articleResult.cover_media;
|
|
4418
|
+
const mediaEntities = Array.isArray(articleResult.media_entities) ? articleResult.media_entities : [];
|
|
4419
|
+
const coverImage = parseMediaImage(coverMedia)[0];
|
|
4420
|
+
const inlineImages = mediaEntities.flatMap((entry) => parseMediaImage(entry));
|
|
4421
|
+
const dedupedImages = [];
|
|
4422
|
+
const seenImageUrls = new Set();
|
|
4423
|
+
for (const image of inlineImages) {
|
|
4424
|
+
if (!seenImageUrls.has(image.url) && image.url !== coverImage?.url) {
|
|
4425
|
+
seenImageUrls.add(image.url);
|
|
4426
|
+
dedupedImages.push(image);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
const output = {
|
|
4430
|
+
id: restId,
|
|
4431
|
+
title,
|
|
4432
|
+
text,
|
|
4433
|
+
url: `https://x.com/i/article/${restId}`,
|
|
4434
|
+
images: dedupedImages,
|
|
4435
|
+
source: "owner_slice",
|
|
4436
|
+
published: lifecycle === "Published",
|
|
4437
|
+
};
|
|
4438
|
+
if (coverImage?.url) {
|
|
4439
|
+
output.coverImageUrl = coverImage.url;
|
|
4440
|
+
}
|
|
4441
|
+
if (authorName) {
|
|
4442
|
+
output.authorName = authorName;
|
|
4443
|
+
}
|
|
4444
|
+
if (authorHandle) {
|
|
4445
|
+
output.authorHandle = authorHandle;
|
|
4446
|
+
}
|
|
4447
|
+
return output;
|
|
4448
|
+
}
|
|
4449
|
+
cursor = extractNextCursor(result?.slice_info);
|
|
4450
|
+
if (!cursor) {
|
|
4451
|
+
break;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
return { error: "not_found" };
|
|
4456
|
+
}, { op: "article_collect_owned", articleId }).catch(() => undefined);
|
|
4457
|
+
if (!article || typeof article !== "object") {
|
|
4458
|
+
return errorResult("UPSTREAM_CHANGED", "article owner fallback failed");
|
|
4459
|
+
}
|
|
4460
|
+
if ("error" in article) {
|
|
4461
|
+
return errorResult("UPSTREAM_CHANGED", "article owner fallback failed", article);
|
|
4462
|
+
}
|
|
4463
|
+
const outputArticle = {
|
|
4464
|
+
id: articleId,
|
|
4465
|
+
title: typeof article.title === "string" ? article.title : "",
|
|
4466
|
+
text: sanitizeArticleText(typeof article.text === "string" ? article.text : ""),
|
|
4467
|
+
url: typeof article.url === "string" ? article.url : `https://x.com/i/article/${articleId}`,
|
|
4468
|
+
images: Array.isArray(article.images) ? article.images : [],
|
|
4469
|
+
source: typeof article.source === "string" ? article.source : "owner_slice",
|
|
4470
|
+
published: article.published === true,
|
|
4471
|
+
};
|
|
4472
|
+
if (typeof article.coverImageUrl === "string" && article.coverImageUrl) {
|
|
4473
|
+
outputArticle.coverImageUrl = article.coverImageUrl;
|
|
4474
|
+
}
|
|
4475
|
+
if (typeof article.authorName === "string" && article.authorName) {
|
|
4476
|
+
outputArticle.authorName = article.authorName;
|
|
4477
|
+
}
|
|
4478
|
+
if (typeof article.authorHandle === "string" && article.authorHandle) {
|
|
4479
|
+
outputArticle.authorHandle = article.authorHandle;
|
|
4480
|
+
}
|
|
4481
|
+
return { article: outputArticle };
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
async function readArticleFromProfileArticles(page, articleId, authorHandle) {
|
|
4485
|
+
const normalizedHandle = authorHandle.replace(/^@+/, "").trim();
|
|
4486
|
+
if (!normalizedHandle) {
|
|
4487
|
+
return errorResult("VALIDATION_ERROR", "authorHandle must be a non-empty string");
|
|
4488
|
+
}
|
|
4489
|
+
return await withEphemeralPage(page, `https://x.com/${encodeURIComponent(normalizedHandle)}/articles`, async (articlePage) => {
|
|
4490
|
+
await waitForCapturedOperation(articlePage, "UserArticlesTweets", 15_000);
|
|
4491
|
+
const article = await articlePage.evaluate(async ({ op, articleId: targetArticleId }) => {
|
|
4492
|
+
if (op !== "article_collect_profile") {
|
|
4493
|
+
return undefined;
|
|
4494
|
+
}
|
|
4495
|
+
const globalAny = window;
|
|
4496
|
+
const capture = globalAny.__WEBMCP_X_CAPTURE__;
|
|
4497
|
+
const entries = Array.isArray(capture?.entries) ? capture.entries : [];
|
|
4498
|
+
const template = (() => {
|
|
4499
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
4500
|
+
const entry = entries[i];
|
|
4501
|
+
if (!entry || entry.op !== "UserArticlesTweets" || !entry.url || !entry.method) {
|
|
4502
|
+
continue;
|
|
4503
|
+
}
|
|
4504
|
+
return {
|
|
4505
|
+
url: entry.url,
|
|
4506
|
+
method: entry.method,
|
|
4507
|
+
headers: entry.headers ?? {},
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4510
|
+
return null;
|
|
4511
|
+
})();
|
|
4512
|
+
if (!template) {
|
|
4513
|
+
return { error: "no_template" };
|
|
4514
|
+
}
|
|
4515
|
+
const sanitizeHeaders = (headers) => {
|
|
4516
|
+
const blockedPrefixes = ["sec-", ":"];
|
|
4517
|
+
const blockedExact = new Set(["host", "content-length", "cookie", "origin", "referer", "connection"]);
|
|
4518
|
+
const output = {};
|
|
4519
|
+
if (!headers) {
|
|
4520
|
+
return output;
|
|
4521
|
+
}
|
|
4522
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4523
|
+
const normalized = key.toLowerCase();
|
|
4524
|
+
if (blockedExact.has(normalized) || blockedPrefixes.some((prefix) => normalized.startsWith(prefix))) {
|
|
4525
|
+
continue;
|
|
4526
|
+
}
|
|
4527
|
+
output[normalized] = value;
|
|
4528
|
+
}
|
|
4529
|
+
return output;
|
|
4530
|
+
};
|
|
4531
|
+
const normalizeText = (value) => value.replace(/\u00a0/g, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
4532
|
+
const normalizeInline = (value) => value.replace(/\s+/g, " ").trim();
|
|
4533
|
+
const parseJsonSafely = (value) => {
|
|
4534
|
+
if (!value) {
|
|
4535
|
+
return {};
|
|
4536
|
+
}
|
|
4537
|
+
try {
|
|
4538
|
+
const parsed = JSON.parse(value);
|
|
4539
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
4540
|
+
}
|
|
4541
|
+
catch {
|
|
4542
|
+
return {};
|
|
4543
|
+
}
|
|
4544
|
+
};
|
|
4545
|
+
const toHttpsImage = (value) => typeof value === "string" && /^https?:\/\//.test(value) ? value : undefined;
|
|
4546
|
+
const parseMediaImage = (value) => {
|
|
4547
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4548
|
+
return [];
|
|
4549
|
+
}
|
|
4550
|
+
const record = value;
|
|
4551
|
+
const url = toHttpsImage(record.original_img_url) ??
|
|
4552
|
+
toHttpsImage(record.media_url_https) ??
|
|
4553
|
+
toHttpsImage(record.media_url) ??
|
|
4554
|
+
toHttpsImage(record.image_info?.original_img_url);
|
|
4555
|
+
if (!url) {
|
|
4556
|
+
return [];
|
|
4557
|
+
}
|
|
4558
|
+
const alt = typeof record.alt_text === "string" ? normalizeInline(record.alt_text) : "";
|
|
4559
|
+
return [alt ? { url, alt } : { url }];
|
|
4560
|
+
};
|
|
4561
|
+
const parseBlocksText = (value) => {
|
|
4562
|
+
if (!Array.isArray(value)) {
|
|
4563
|
+
return "";
|
|
4564
|
+
}
|
|
4565
|
+
const lines = [];
|
|
4566
|
+
for (const rawBlock of value) {
|
|
4567
|
+
if (!rawBlock || typeof rawBlock !== "object" || Array.isArray(rawBlock)) {
|
|
4568
|
+
continue;
|
|
4569
|
+
}
|
|
4570
|
+
const block = rawBlock;
|
|
4571
|
+
const rawText = typeof block.text === "string" ? block.text : "";
|
|
4572
|
+
const text = normalizeText(rawText);
|
|
4573
|
+
if (!text) {
|
|
4574
|
+
lines.push("");
|
|
4575
|
+
continue;
|
|
4576
|
+
}
|
|
4577
|
+
const type = typeof block.type === "string" ? block.type : "";
|
|
4578
|
+
if (type === "unordered-list-item") {
|
|
4579
|
+
lines.push(`- ${text}`);
|
|
4580
|
+
}
|
|
4581
|
+
else if (type === "ordered-list-item") {
|
|
4582
|
+
lines.push(`1. ${text}`);
|
|
4583
|
+
}
|
|
4584
|
+
else {
|
|
4585
|
+
lines.push(text);
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
return normalizeText(lines.join("\n"));
|
|
4589
|
+
};
|
|
4590
|
+
const extractNextCursor = (instructions) => {
|
|
4591
|
+
for (const instruction of instructions) {
|
|
4592
|
+
if (!instruction || typeof instruction !== "object" || Array.isArray(instruction)) {
|
|
4593
|
+
continue;
|
|
4594
|
+
}
|
|
4595
|
+
const entries = Array.isArray(instruction.entries)
|
|
4596
|
+
? instruction.entries
|
|
4597
|
+
: [];
|
|
4598
|
+
for (const rawEntry of entries) {
|
|
4599
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
4600
|
+
continue;
|
|
4601
|
+
}
|
|
4602
|
+
const entry = rawEntry;
|
|
4603
|
+
const content = entry.content ?? {};
|
|
4604
|
+
const cursorType = typeof content.cursorType === "string" ? content.cursorType : "";
|
|
4605
|
+
const value = typeof content.value === "string" ? content.value : "";
|
|
4606
|
+
if (cursorType === "Bottom" && value) {
|
|
4607
|
+
return value;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
return undefined;
|
|
4612
|
+
};
|
|
4613
|
+
const templateUrl = new URL(template.url, location.origin);
|
|
4614
|
+
const templateVariables = parseJsonSafely(templateUrl.searchParams.get("variables"));
|
|
4615
|
+
const templateFeatures = parseJsonSafely(templateUrl.searchParams.get("features"));
|
|
4616
|
+
const templateFieldToggles = parseJsonSafely(templateUrl.searchParams.get("fieldToggles"));
|
|
4617
|
+
const headers = sanitizeHeaders(template.headers);
|
|
4618
|
+
templateFieldToggles.withArticlePlainText = true;
|
|
4619
|
+
const fetchTimeline = async (cursor) => {
|
|
4620
|
+
const vars = {
|
|
4621
|
+
...templateVariables,
|
|
4622
|
+
count: 20,
|
|
4623
|
+
};
|
|
4624
|
+
if (cursor) {
|
|
4625
|
+
vars.cursor = cursor;
|
|
4626
|
+
}
|
|
4627
|
+
else {
|
|
4628
|
+
delete vars.cursor;
|
|
4629
|
+
}
|
|
4630
|
+
const requestUrl = new URL(template.url, location.origin);
|
|
4631
|
+
requestUrl.searchParams.set("variables", JSON.stringify(vars));
|
|
4632
|
+
if (Object.keys(templateFeatures).length > 0) {
|
|
4633
|
+
requestUrl.searchParams.set("features", JSON.stringify(templateFeatures));
|
|
4634
|
+
}
|
|
4635
|
+
if (Object.keys(templateFieldToggles).length > 0) {
|
|
4636
|
+
requestUrl.searchParams.set("fieldToggles", JSON.stringify(templateFieldToggles));
|
|
4637
|
+
}
|
|
4638
|
+
const response = await fetch(requestUrl.toString(), {
|
|
4639
|
+
method: template.method,
|
|
4640
|
+
headers,
|
|
4641
|
+
credentials: "include",
|
|
4642
|
+
});
|
|
4643
|
+
if (!response.ok) {
|
|
4644
|
+
throw new Error(`http_${response.status}`);
|
|
4645
|
+
}
|
|
4646
|
+
return await response.json();
|
|
4647
|
+
};
|
|
4648
|
+
let cursor;
|
|
4649
|
+
for (let pageIndex = 0; pageIndex < 8; pageIndex += 1) {
|
|
4650
|
+
let responseJson;
|
|
4651
|
+
try {
|
|
4652
|
+
responseJson = await fetchTimeline(cursor);
|
|
4653
|
+
}
|
|
4654
|
+
catch (error) {
|
|
4655
|
+
return { error: String(error) };
|
|
4656
|
+
}
|
|
4657
|
+
const dataRecord = responseJson?.data;
|
|
4658
|
+
const userRecord = dataRecord?.user;
|
|
4659
|
+
const resultRecord = userRecord?.result;
|
|
4660
|
+
const timelineRecord = resultRecord?.timeline;
|
|
4661
|
+
const timelineInnerRecord = timelineRecord?.timeline;
|
|
4662
|
+
const instructions = timelineInnerRecord?.instructions;
|
|
4663
|
+
const entries = Array.isArray(instructions) ? instructions : [];
|
|
4664
|
+
for (const instruction of entries) {
|
|
4665
|
+
if (!instruction || typeof instruction !== "object" || Array.isArray(instruction)) {
|
|
4666
|
+
continue;
|
|
4667
|
+
}
|
|
4668
|
+
const timelineEntries = Array.isArray(instruction.entries)
|
|
4669
|
+
? instruction.entries
|
|
4670
|
+
: [];
|
|
4671
|
+
for (const rawEntry of timelineEntries) {
|
|
4672
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
4673
|
+
continue;
|
|
4674
|
+
}
|
|
4675
|
+
const entry = rawEntry;
|
|
4676
|
+
const tweetResult = entry.content?.itemContent
|
|
4677
|
+
?.tweet_results?.result;
|
|
4678
|
+
const articleResult = tweetResult?.article?.article_results
|
|
4679
|
+
?.result ?? undefined;
|
|
4680
|
+
if (!articleResult) {
|
|
4681
|
+
continue;
|
|
4682
|
+
}
|
|
4683
|
+
const restId = typeof articleResult.rest_id === "string" ? articleResult.rest_id : "";
|
|
4684
|
+
if (restId !== targetArticleId) {
|
|
4685
|
+
continue;
|
|
4686
|
+
}
|
|
4687
|
+
const metadata = articleResult.metadata ?? {};
|
|
4688
|
+
const title = typeof articleResult.title === "string" ? normalizeInline(articleResult.title) : "";
|
|
4689
|
+
const previewText = typeof articleResult.preview_text === "string" ? normalizeText(articleResult.preview_text) : "";
|
|
4690
|
+
const plainText = typeof articleResult.plain_text === "string" ? normalizeText(articleResult.plain_text) : "";
|
|
4691
|
+
const text = plainText || parseBlocksText(articleResult.content_state?.blocks) || previewText;
|
|
4692
|
+
const coverImage = parseMediaImage(articleResult.cover_media)[0];
|
|
4693
|
+
const mediaEntities = Array.isArray(articleResult.media_entities) ? articleResult.media_entities : [];
|
|
4694
|
+
const inlineImages = mediaEntities.flatMap((entry) => parseMediaImage(entry));
|
|
4695
|
+
const dedupedImages = [];
|
|
4696
|
+
const seenImageUrls = new Set();
|
|
4697
|
+
for (const image of inlineImages) {
|
|
4698
|
+
if (!seenImageUrls.has(image.url) && image.url !== coverImage?.url) {
|
|
4699
|
+
seenImageUrls.add(image.url);
|
|
4700
|
+
dedupedImages.push(image);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
const output = {
|
|
4704
|
+
id: restId,
|
|
4705
|
+
title,
|
|
4706
|
+
text,
|
|
4707
|
+
url: `https://x.com/i/article/${restId}`,
|
|
4708
|
+
images: dedupedImages,
|
|
4709
|
+
source: "profile_articles",
|
|
4710
|
+
published: true,
|
|
4711
|
+
};
|
|
4712
|
+
if (coverImage?.url) {
|
|
4713
|
+
output.coverImageUrl = coverImage.url;
|
|
4714
|
+
}
|
|
4715
|
+
if (typeof metadata.first_published_at_secs === "number") {
|
|
4716
|
+
output.firstPublishedAtSecs = metadata.first_published_at_secs;
|
|
4717
|
+
}
|
|
4718
|
+
return output;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
cursor = extractNextCursor(entries);
|
|
4722
|
+
if (!cursor) {
|
|
4723
|
+
break;
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
return { error: "not_found" };
|
|
4727
|
+
}, { op: "article_collect_profile", articleId }).catch(() => undefined);
|
|
4728
|
+
if (!article || typeof article !== "object") {
|
|
4729
|
+
return errorResult("UPSTREAM_CHANGED", "article profile fallback failed");
|
|
4730
|
+
}
|
|
4731
|
+
if ("error" in article) {
|
|
4732
|
+
return errorResult("UPSTREAM_CHANGED", "article profile fallback failed", article);
|
|
4733
|
+
}
|
|
4734
|
+
const outputArticle = {
|
|
4735
|
+
id: articleId,
|
|
4736
|
+
title: typeof article.title === "string" ? article.title : "",
|
|
4737
|
+
text: sanitizeArticleText(typeof article.text === "string" ? article.text : ""),
|
|
4738
|
+
url: typeof article.url === "string" ? article.url : `https://x.com/i/article/${articleId}`,
|
|
4739
|
+
images: Array.isArray(article.images) ? article.images : [],
|
|
4740
|
+
source: typeof article.source === "string" ? article.source : "profile_articles",
|
|
4741
|
+
published: true,
|
|
4742
|
+
authorHandle: `@${normalizedHandle}`,
|
|
4743
|
+
};
|
|
4744
|
+
if (typeof article.coverImageUrl === "string" && article.coverImageUrl) {
|
|
4745
|
+
outputArticle.coverImageUrl = article.coverImageUrl;
|
|
4746
|
+
}
|
|
4747
|
+
return { article: outputArticle };
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
async function readArticleFromPublicPage(page, targetUrl) {
|
|
4751
|
+
return await withEphemeralPage(page, targetUrl, async (readPage) => {
|
|
4752
|
+
await waitForArticleReadSurface(readPage);
|
|
4753
|
+
const article = await readPage.evaluate(({ op }) => {
|
|
4754
|
+
if (op !== "article_collect_public") {
|
|
4755
|
+
return undefined;
|
|
4756
|
+
}
|
|
4757
|
+
const normalizeInline = (value) => value.replace(/\s+/g, " ").trim();
|
|
4758
|
+
const normalizeBlock = (value) => value.replace(/\u00a0/g, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
4759
|
+
const asArray = (value) => (Array.isArray(value) ? value : [value]);
|
|
4760
|
+
const readJsonLd = () => {
|
|
4761
|
+
const scripts = Array.from(document.querySelectorAll("script[type='application/ld+json']"));
|
|
4762
|
+
for (const script of scripts) {
|
|
4763
|
+
const raw = script.textContent?.trim();
|
|
4764
|
+
if (!raw) {
|
|
4765
|
+
continue;
|
|
4766
|
+
}
|
|
4767
|
+
try {
|
|
4768
|
+
const parsed = JSON.parse(raw);
|
|
4769
|
+
const queue = asArray(parsed);
|
|
4770
|
+
while (queue.length > 0) {
|
|
4771
|
+
const next = queue.shift();
|
|
4772
|
+
if (!next || typeof next !== "object") {
|
|
4773
|
+
continue;
|
|
4774
|
+
}
|
|
4775
|
+
const record = next;
|
|
4776
|
+
const typeValue = record["@type"];
|
|
4777
|
+
const types = asArray(typeValue).filter((item) => typeof item === "string");
|
|
4778
|
+
if (types.some((item) => item.toLowerCase().includes("article"))) {
|
|
4779
|
+
return record;
|
|
4780
|
+
}
|
|
4781
|
+
const graph = record["@graph"];
|
|
4782
|
+
if (graph) {
|
|
4783
|
+
queue.push(...asArray(graph));
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
catch {
|
|
4788
|
+
continue;
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
return undefined;
|
|
4792
|
+
};
|
|
4793
|
+
const ld = readJsonLd();
|
|
4794
|
+
const ldHeadline = typeof ld?.headline === "string" ? normalizeInline(ld.headline) : "";
|
|
4795
|
+
const ldBody = typeof ld?.articleBody === "string" ? normalizeBlock(ld.articleBody) : "";
|
|
4796
|
+
const ldImage = typeof ld?.image === "string"
|
|
4797
|
+
? ld.image
|
|
4798
|
+
: Array.isArray(ld?.image)
|
|
4799
|
+
? ld.image.find((item) => typeof item === "string")
|
|
4800
|
+
: undefined;
|
|
4801
|
+
const ldAuthor = (() => {
|
|
4802
|
+
const author = ld?.author;
|
|
4803
|
+
if (!author || typeof author !== "object" || Array.isArray(author)) {
|
|
4804
|
+
return undefined;
|
|
4805
|
+
}
|
|
4806
|
+
const authorRecord = author;
|
|
4807
|
+
const name = typeof authorRecord.name === "string" ? normalizeInline(authorRecord.name) : "";
|
|
4808
|
+
const url = typeof authorRecord.url === "string" ? authorRecord.url : "";
|
|
4809
|
+
const handleMatch = url.match(/x\.com\/([^/?#]+)/i);
|
|
4810
|
+
return {
|
|
4811
|
+
name,
|
|
4812
|
+
handle: handleMatch?.[1] ? `@${handleMatch[1].replace(/^@+/, "")}` : undefined,
|
|
4813
|
+
};
|
|
4814
|
+
})();
|
|
4815
|
+
const title = ldHeadline ||
|
|
4816
|
+
normalizeInline(document.querySelector("h1")?.innerText || "") ||
|
|
4817
|
+
normalizeInline(document.querySelector("meta[property='og:title']")?.getAttribute("content") || "");
|
|
4818
|
+
const articleTextCandidates = [
|
|
4819
|
+
...Array.from(document.querySelectorAll("main article [dir='auto'], main article div[lang], main article p")),
|
|
4820
|
+
...Array.from(document.querySelectorAll("article [dir='auto'], article div[lang], article p")),
|
|
4821
|
+
]
|
|
4822
|
+
.map((node) => normalizeBlock(node.innerText || node.textContent || ""))
|
|
4823
|
+
.filter((value) => value.length > 0);
|
|
4824
|
+
const text = ldBody || articleTextCandidates.join("\n\n");
|
|
4825
|
+
const canonicalUrl = document.querySelector("link[rel='canonical']")?.href ||
|
|
4826
|
+
document.querySelector("meta[property='og:url']")?.getAttribute("content") ||
|
|
4827
|
+
window.location.href;
|
|
4828
|
+
const articleIdMatch = canonicalUrl.match(/\/articles\/(\d+)(?:[/?#]|$)/) || window.location.href.match(/\/articles\/(\d+)(?:[/?#]|$)/);
|
|
4829
|
+
const allImages = Array.from(document.querySelectorAll("img[src]"))
|
|
4830
|
+
.map((img) => ({
|
|
4831
|
+
url: img.currentSrc || img.src,
|
|
4832
|
+
alt: normalizeInline(img.alt || ""),
|
|
4833
|
+
width: img.naturalWidth || 0,
|
|
4834
|
+
height: img.naturalHeight || 0,
|
|
4835
|
+
}))
|
|
4836
|
+
.filter((item) => /^https?:\/\//.test(item.url))
|
|
4837
|
+
.filter((item) => item.width > 100 || item.height > 100);
|
|
4838
|
+
const imageByUrl = new Map();
|
|
4839
|
+
for (const image of allImages) {
|
|
4840
|
+
if (!imageByUrl.has(image.url)) {
|
|
4841
|
+
imageByUrl.set(image.url, image.alt ? { url: image.url, alt: image.alt } : { url: image.url });
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
const coverImageUrl = (typeof ldImage === "string" && /^https?:\/\//.test(ldImage) ? ldImage : undefined) ||
|
|
4845
|
+
Array.from(imageByUrl.keys())[0];
|
|
4846
|
+
const images = Array.from(imageByUrl.values()).filter((item) => item.url !== coverImageUrl);
|
|
4847
|
+
const bodyText = normalizeBlock(document.body?.innerText || "");
|
|
4848
|
+
return {
|
|
4849
|
+
id: articleIdMatch?.[1],
|
|
4850
|
+
url: canonicalUrl,
|
|
4851
|
+
title,
|
|
4852
|
+
text,
|
|
4853
|
+
coverImageUrl,
|
|
4854
|
+
images,
|
|
4855
|
+
authorName: ldAuthor?.name,
|
|
4856
|
+
authorHandle: ldAuthor?.handle,
|
|
4857
|
+
unsupported: bodyText.includes("This page is not supported."),
|
|
4858
|
+
};
|
|
4859
|
+
}, { op: "article_collect_public" }).catch(() => undefined);
|
|
4860
|
+
if (!article || typeof article !== "object") {
|
|
4861
|
+
return errorResult("UPSTREAM_CHANGED", "article content not found");
|
|
4862
|
+
}
|
|
4863
|
+
if (article.unsupported === true) {
|
|
4864
|
+
return errorResult("UPSTREAM_CHANGED", "article unavailable on public article route", {
|
|
4865
|
+
reason: "public_article_unsupported",
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4868
|
+
const title = typeof article.title === "string" ? article.title : "";
|
|
4869
|
+
const text = sanitizeArticleText(typeof article.text === "string" ? article.text : "");
|
|
4870
|
+
const images = Array.isArray(article.images) ? article.images : [];
|
|
4871
|
+
const coverImageUrl = typeof article.coverImageUrl === "string" ? article.coverImageUrl : "";
|
|
4872
|
+
if ((!title || title === "X") && !text && images.length === 0 && !coverImageUrl) {
|
|
4873
|
+
return errorResult("UPSTREAM_CHANGED", "article content not found on public article route", {
|
|
4874
|
+
reason: "public_article_empty",
|
|
4875
|
+
});
|
|
4876
|
+
}
|
|
4877
|
+
const outputArticle = {
|
|
4878
|
+
source: "public",
|
|
4879
|
+
published: true,
|
|
4880
|
+
title,
|
|
4881
|
+
text,
|
|
4882
|
+
url: typeof article.url === "string" ? article.url : targetUrl,
|
|
4883
|
+
images,
|
|
4884
|
+
};
|
|
4885
|
+
if (typeof article.id === "string" && article.id) {
|
|
4886
|
+
outputArticle.id = article.id;
|
|
4887
|
+
}
|
|
4888
|
+
if (typeof article.coverImageUrl === "string" && article.coverImageUrl) {
|
|
4889
|
+
outputArticle.coverImageUrl = article.coverImageUrl;
|
|
4890
|
+
}
|
|
4891
|
+
if (typeof article.authorName === "string" && article.authorName) {
|
|
4892
|
+
outputArticle.authorName = article.authorName;
|
|
4893
|
+
}
|
|
4894
|
+
if (typeof article.authorHandle === "string" && article.authorHandle) {
|
|
4895
|
+
outputArticle.authorHandle = article.authorHandle;
|
|
4896
|
+
}
|
|
4897
|
+
return { article: outputArticle };
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
async function readArticleByUrl(page, targetUrl, authorHandle) {
|
|
4901
|
+
let articleId = parseArticleIdFromUrl(targetUrl);
|
|
4902
|
+
if (!articleId && /\/status\/\d+(?:[/?#]|$)/.test(targetUrl)) {
|
|
4903
|
+
const tweetResult = await readTweetByUrl(page, targetUrl);
|
|
4904
|
+
if (tweetResult &&
|
|
4905
|
+
typeof tweetResult === "object" &&
|
|
4906
|
+
"tweet" in tweetResult &&
|
|
4907
|
+
tweetResult.tweet &&
|
|
4908
|
+
typeof tweetResult.tweet === "object" &&
|
|
4909
|
+
!Array.isArray(tweetResult.tweet)) {
|
|
4910
|
+
const tweetRecord = tweetResult.tweet;
|
|
4911
|
+
const article = tweetRecord.article;
|
|
4912
|
+
if (article && typeof article === "object" && !Array.isArray(article)) {
|
|
4913
|
+
const articleRecord = article;
|
|
4914
|
+
const nestedUrl = typeof articleRecord.url === "string" ? articleRecord.url : "";
|
|
4915
|
+
const nestedId = typeof articleRecord.id === "string" ? articleRecord.id : "";
|
|
4916
|
+
if (nestedUrl) {
|
|
4917
|
+
targetUrl = nestedUrl;
|
|
4918
|
+
}
|
|
4919
|
+
articleId = nestedId || parseArticleIdFromUrl(nestedUrl);
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
const useEditor = targetUrl.includes("/compose/articles/edit/") || isArticlePreviewUrl(targetUrl);
|
|
4924
|
+
if (articleId) {
|
|
4925
|
+
const cachedPage = getCachedArticleDraftPage(page, articleId);
|
|
4926
|
+
if (cachedPage) {
|
|
4927
|
+
await waitForArticleEditorSurface(cachedPage);
|
|
4928
|
+
await ensureArticleDraftLoaded(cachedPage, articleId);
|
|
4929
|
+
return await readArticleFromEditorPage(cachedPage, articleId, true);
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
if (useEditor) {
|
|
4933
|
+
return await withEphemeralPage(page, articleId ? buildArticleEditUrl(articleId) : targetUrl, async (articlePage) => {
|
|
4934
|
+
await waitForArticleEditorSurface(articlePage);
|
|
4935
|
+
await ensureArticleDraftLoaded(articlePage, articleId);
|
|
4936
|
+
return await readArticleFromEditorPage(articlePage, articleId, false);
|
|
4937
|
+
});
|
|
4938
|
+
}
|
|
4939
|
+
const publicResult = await readArticleFromPublicPage(page, targetUrl);
|
|
4940
|
+
if (!articleId) {
|
|
4941
|
+
return publicResult;
|
|
4942
|
+
}
|
|
4943
|
+
if (!parseArticleReadErrorCode(publicResult)) {
|
|
4944
|
+
return publicResult;
|
|
4945
|
+
}
|
|
4946
|
+
if (typeof authorHandle === "string" && authorHandle.trim()) {
|
|
4947
|
+
const profileResult = await readArticleFromProfileArticles(page, articleId, authorHandle);
|
|
4948
|
+
if (!parseArticleReadErrorCode(profileResult)) {
|
|
4949
|
+
return profileResult;
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
const ownerResult = await readArticleFromOwnedSlices(page, articleId);
|
|
4953
|
+
return parseArticleReadErrorCode(ownerResult) ? publicResult : ownerResult;
|
|
4954
|
+
}
|
|
4955
|
+
async function publishArticleEditor(page, timeoutMs) {
|
|
4956
|
+
const editUrl = page.url();
|
|
4957
|
+
await page
|
|
4958
|
+
.evaluate(() => {
|
|
4959
|
+
const active = document.activeElement;
|
|
4960
|
+
if (active instanceof HTMLElement) {
|
|
4961
|
+
active.blur();
|
|
4962
|
+
}
|
|
4963
|
+
})
|
|
4964
|
+
.catch(() => { });
|
|
4965
|
+
await page.keyboard.press("Escape").catch(() => { });
|
|
4966
|
+
await page
|
|
4967
|
+
.waitForFunction(() => {
|
|
4968
|
+
const buttons = Array.from(document.querySelectorAll("button"));
|
|
4969
|
+
return buttons.some((button) => {
|
|
4970
|
+
const label = (button.textContent || "").replace(/\s+/g, " ").trim();
|
|
4971
|
+
const ariaDisabled = (button.getAttribute("aria-disabled") || "").toLowerCase();
|
|
4972
|
+
return label === "Publish" && !button.disabled && ariaDisabled !== "true";
|
|
4973
|
+
});
|
|
4974
|
+
}, undefined, { timeout: Math.min(timeoutMs, 15_000) })
|
|
4975
|
+
.catch(() => { });
|
|
4976
|
+
const clickPrimaryPublish = async () => {
|
|
4977
|
+
return ((await page.evaluate(({ op }) => {
|
|
4978
|
+
if (op !== "article_click_publish") {
|
|
4979
|
+
return false;
|
|
4980
|
+
}
|
|
4981
|
+
const buttons = Array.from(document.querySelectorAll("button")).filter((button) => {
|
|
4982
|
+
const label = (button.textContent || "").replace(/\s+/g, " ").trim();
|
|
4983
|
+
const ariaDisabled = (button.getAttribute("aria-disabled") || "").toLowerCase();
|
|
4984
|
+
return label === "Publish" && !button.disabled && ariaDisabled !== "true";
|
|
4985
|
+
});
|
|
4986
|
+
const button = buttons[buttons.length - 1];
|
|
4987
|
+
if (!button) {
|
|
4988
|
+
return false;
|
|
4989
|
+
}
|
|
4990
|
+
button.click();
|
|
4991
|
+
return true;
|
|
4992
|
+
}, { op: "article_click_publish" }).catch(() => false)) === true);
|
|
4993
|
+
};
|
|
4994
|
+
if (!(await clickPrimaryPublish())) {
|
|
4995
|
+
const details = await page
|
|
4996
|
+
.evaluate(() => {
|
|
4997
|
+
const titleAreas = Array.from(document.querySelectorAll("textarea")).map((input) => ({
|
|
4998
|
+
placeholder: input.getAttribute("placeholder") || "",
|
|
4999
|
+
value: input instanceof HTMLTextAreaElement ? input.value : "",
|
|
5000
|
+
}));
|
|
5001
|
+
const composers = Array.from(document.querySelectorAll("[data-testid='composer'][role='textbox']")).map((node) => ({
|
|
5002
|
+
text: (node.textContent || "").slice(0, 500),
|
|
5003
|
+
}));
|
|
5004
|
+
const buttonLabels = Array.from(document.querySelectorAll("button"))
|
|
5005
|
+
.map((button) => ({
|
|
5006
|
+
text: (button.textContent || "").replace(/\s+/g, " ").trim(),
|
|
5007
|
+
aria: button.getAttribute("aria-label") || "",
|
|
5008
|
+
disabled: button.disabled || (button.getAttribute("aria-disabled") || "").toLowerCase() === "true",
|
|
5009
|
+
}))
|
|
5010
|
+
.filter((item) => item.text || item.aria)
|
|
5011
|
+
.slice(0, 30);
|
|
5012
|
+
const bodyLines = (document.body?.innerText || "")
|
|
5013
|
+
.split(/\n+/)
|
|
5014
|
+
.map((line) => line.trim())
|
|
5015
|
+
.filter((line) => line.length > 0)
|
|
5016
|
+
.slice(0, 40);
|
|
5017
|
+
return {
|
|
5018
|
+
currentUrl: window.location.href,
|
|
5019
|
+
titleAreas,
|
|
5020
|
+
composers,
|
|
5021
|
+
buttonLabels,
|
|
5022
|
+
bodyLines,
|
|
5023
|
+
};
|
|
5024
|
+
})
|
|
5025
|
+
.catch(() => undefined);
|
|
5026
|
+
return details
|
|
5027
|
+
? { ok: false, reason: "publish_button_not_found", details }
|
|
5028
|
+
: { ok: false, reason: "publish_button_not_found" };
|
|
5029
|
+
}
|
|
5030
|
+
await page.waitForTimeout(1_000);
|
|
5031
|
+
await clickPrimaryPublish().catch(() => false);
|
|
5032
|
+
try {
|
|
5033
|
+
await page.waitForFunction(({ previousUrl }) => {
|
|
5034
|
+
const currentUrl = window.location.href;
|
|
5035
|
+
if (!currentUrl.includes("/compose/articles/edit/")) {
|
|
5036
|
+
return true;
|
|
5037
|
+
}
|
|
5038
|
+
if (currentUrl !== previousUrl && !currentUrl.includes("/preview")) {
|
|
5039
|
+
return true;
|
|
5040
|
+
}
|
|
5041
|
+
return (document.body?.innerText || "").includes("Published");
|
|
5042
|
+
}, { previousUrl: editUrl }, { timeout: timeoutMs });
|
|
5043
|
+
}
|
|
5044
|
+
catch {
|
|
5045
|
+
return { ok: false, reason: "publish_not_confirmed" };
|
|
5046
|
+
}
|
|
5047
|
+
const details = await page.evaluate(({ op }) => {
|
|
5048
|
+
if (op !== "article_collect_publish_details") {
|
|
5049
|
+
return {
|
|
5050
|
+
currentUrl: window.location.href,
|
|
5051
|
+
editUrl: undefined,
|
|
5052
|
+
publicUrl: undefined,
|
|
5053
|
+
};
|
|
5054
|
+
}
|
|
5055
|
+
const editAnchor = Array.from(document.querySelectorAll("a[href]")).find((anchor) => anchor.href.includes("/compose/articles/edit/"));
|
|
5056
|
+
const publicAnchor = Array.from(document.querySelectorAll("a[href]")).find((anchor) => {
|
|
5057
|
+
return anchor.href.includes("/articles/") && !anchor.href.includes("/compose/articles/edit/");
|
|
5058
|
+
});
|
|
5059
|
+
return {
|
|
5060
|
+
currentUrl: window.location.href,
|
|
5061
|
+
editUrl: editAnchor?.href,
|
|
5062
|
+
publicUrl: publicAnchor?.href,
|
|
5063
|
+
};
|
|
5064
|
+
}, { op: "article_collect_publish_details" }).catch(() => ({ currentUrl: page.url(), editUrl: undefined, publicUrl: undefined }));
|
|
5065
|
+
const articleId = parseArticleIdFromUrl(details.currentUrl) ??
|
|
5066
|
+
(typeof details.editUrl === "string" ? parseArticleIdFromUrl(details.editUrl) : undefined) ??
|
|
5067
|
+
undefined;
|
|
5068
|
+
const articleUrl = typeof details.publicUrl === "string" && details.publicUrl.length > 0
|
|
5069
|
+
? details.publicUrl
|
|
5070
|
+
: !details.currentUrl.includes("/compose/articles/edit/")
|
|
5071
|
+
? details.currentUrl
|
|
5072
|
+
: undefined;
|
|
5073
|
+
const output = {
|
|
5074
|
+
ok: true,
|
|
5075
|
+
editUrl: typeof details.editUrl === "string" && details.editUrl.length > 0 ? details.editUrl : editUrl,
|
|
5076
|
+
};
|
|
5077
|
+
if (articleId) {
|
|
5078
|
+
output.articleId = articleId;
|
|
5079
|
+
}
|
|
5080
|
+
if (articleUrl) {
|
|
5081
|
+
output.articleUrl = articleUrl;
|
|
5082
|
+
}
|
|
5083
|
+
return output;
|
|
5084
|
+
}
|
|
5085
|
+
async function deleteArticleEditor(page, dryRun) {
|
|
5086
|
+
const menuOpened = await page.evaluate(({ op }) => {
|
|
5087
|
+
if (op !== "article_open_delete_menu") {
|
|
5088
|
+
return false;
|
|
5089
|
+
}
|
|
5090
|
+
const buttons = Array.from(document.querySelectorAll("button[aria-label='More']"));
|
|
5091
|
+
const button = buttons[buttons.length - 1];
|
|
5092
|
+
if (!button) {
|
|
5093
|
+
return false;
|
|
5094
|
+
}
|
|
5095
|
+
button.click();
|
|
5096
|
+
return true;
|
|
5097
|
+
}, { op: "article_open_delete_menu" }).catch(() => false);
|
|
5098
|
+
if (!menuOpened) {
|
|
5099
|
+
return errorResult("UPSTREAM_CHANGED", "article delete controls not found", {
|
|
5100
|
+
reason: "more_button_not_found",
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
const deleteReady = await page.waitForFunction(() => {
|
|
5104
|
+
return Array.from(document.querySelectorAll("[role='menuitem'], button, div[role='button']")).some((element) => {
|
|
5105
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
5106
|
+
return text === "Delete Article";
|
|
5107
|
+
});
|
|
5108
|
+
}, undefined, { timeout: 5_000 }).then(() => true).catch(() => false);
|
|
5109
|
+
if (!deleteReady) {
|
|
5110
|
+
return errorResult("UPSTREAM_CHANGED", "article delete controls not found", {
|
|
5111
|
+
reason: "delete_menu_item_not_found",
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
5114
|
+
if (dryRun) {
|
|
5115
|
+
return {
|
|
5116
|
+
ok: true,
|
|
5117
|
+
dryRun: true,
|
|
5118
|
+
deleteVisible: true,
|
|
5119
|
+
};
|
|
5120
|
+
}
|
|
5121
|
+
const firstDelete = await page.evaluate(({ op }) => {
|
|
5122
|
+
if (op !== "article_click_delete_menu_item") {
|
|
5123
|
+
return false;
|
|
5124
|
+
}
|
|
5125
|
+
const item = Array.from(document.querySelectorAll("[role='menuitem'], button, div[role='button']")).find((element) => {
|
|
5126
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
5127
|
+
return text === "Delete Article";
|
|
5128
|
+
});
|
|
5129
|
+
if (!item) {
|
|
5130
|
+
return false;
|
|
5131
|
+
}
|
|
5132
|
+
item.click();
|
|
5133
|
+
return true;
|
|
5134
|
+
}, { op: "article_click_delete_menu_item" }).catch(() => false);
|
|
5135
|
+
if (!firstDelete) {
|
|
5136
|
+
return errorResult("UPSTREAM_CHANGED", "article delete controls not found", {
|
|
5137
|
+
reason: "delete_menu_click_failed",
|
|
5138
|
+
});
|
|
5139
|
+
}
|
|
5140
|
+
await page.waitForTimeout(700);
|
|
5141
|
+
await page.evaluate(({ op }) => {
|
|
5142
|
+
if (op !== "article_confirm_delete") {
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
const dialog = document.querySelector("[role='dialog'], [data-testid='confirmationSheetDialog']");
|
|
5146
|
+
const dialogButtons = dialog
|
|
5147
|
+
? Array.from(dialog.querySelectorAll("button, div[role='button']"))
|
|
5148
|
+
: [];
|
|
5149
|
+
const dialogConfirm = dialogButtons.find((element) => {
|
|
5150
|
+
const testId = element.getAttribute("data-testid") || "";
|
|
5151
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
5152
|
+
return testId === "confirmationSheetConfirm" || text === "Delete Article";
|
|
5153
|
+
});
|
|
5154
|
+
if (dialogConfirm) {
|
|
5155
|
+
dialogConfirm.click();
|
|
5156
|
+
return;
|
|
5157
|
+
}
|
|
5158
|
+
const buttons = Array.from(document.querySelectorAll("button, div[role='button']")).filter((element) => {
|
|
5159
|
+
const text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
|
5160
|
+
return text === "Delete Article";
|
|
5161
|
+
});
|
|
5162
|
+
const button = buttons[buttons.length - 1];
|
|
5163
|
+
button?.click();
|
|
5164
|
+
}, { op: "article_confirm_delete" }).catch(() => { });
|
|
5165
|
+
const deleted = await page
|
|
5166
|
+
.waitForFunction(() => {
|
|
5167
|
+
const bodyText = document.body?.innerText || "";
|
|
5168
|
+
return window.location.pathname === "/compose/articles" || bodyText.includes("Continue a draft or create a new Article");
|
|
5169
|
+
}, undefined, { timeout: 15_000 })
|
|
5170
|
+
.then(() => true)
|
|
5171
|
+
.catch(() => false);
|
|
5172
|
+
if (!deleted) {
|
|
5173
|
+
const details = await page.evaluate(() => {
|
|
5174
|
+
const buttonLabels = Array.from(document.querySelectorAll("button, div[role='button']"))
|
|
5175
|
+
.map((element) => ({
|
|
5176
|
+
text: (element.textContent || "").replace(/\s+/g, " ").trim(),
|
|
5177
|
+
aria: element.getAttribute("aria-label") || "",
|
|
5178
|
+
testId: element.getAttribute("data-testid") || "",
|
|
5179
|
+
}))
|
|
5180
|
+
.filter((item) => item.text || item.aria || item.testId)
|
|
5181
|
+
.slice(0, 40);
|
|
5182
|
+
const bodyLines = (document.body?.innerText || "")
|
|
5183
|
+
.split(/\n+/)
|
|
5184
|
+
.map((line) => line.trim())
|
|
5185
|
+
.filter((line) => line.length > 0)
|
|
5186
|
+
.slice(0, 40);
|
|
5187
|
+
return {
|
|
5188
|
+
currentUrl: window.location.href,
|
|
5189
|
+
dialogOpen: document.querySelector("[role='dialog'], [data-testid='confirmationSheetDialog']") !== null,
|
|
5190
|
+
buttonLabels,
|
|
5191
|
+
bodyLines,
|
|
5192
|
+
};
|
|
5193
|
+
}).catch(() => undefined);
|
|
5194
|
+
return errorResult("ACTION_UNCONFIRMED", "article delete was not confirmed", details);
|
|
5195
|
+
}
|
|
5196
|
+
return {
|
|
5197
|
+
ok: true,
|
|
5198
|
+
confirmed: true,
|
|
5199
|
+
};
|
|
5200
|
+
}
|
|
5201
|
+
async function draftArticleMarkdown(page, markdownPath, explicitTitle, coverImagePath) {
|
|
5202
|
+
const resolvedMarkdown = await resolveArticleAttachment(markdownPath, "markdownPath");
|
|
5203
|
+
if (!resolvedMarkdown.ok || !resolvedMarkdown.attachment) {
|
|
5204
|
+
return resolvedMarkdown.ok ? errorResult("VALIDATION_ERROR", "markdownPath was not found") : resolvedMarkdown.result;
|
|
5205
|
+
}
|
|
5206
|
+
const markdown = await readFile(resolvedMarkdown.attachment.path, "utf8").catch(() => undefined);
|
|
5207
|
+
if (markdown === undefined) {
|
|
5208
|
+
return errorResult("VALIDATION_ERROR", `markdownPath was not found: ${markdownPath}`);
|
|
5209
|
+
}
|
|
5210
|
+
const normalized = normalizeArticleMarkdown(markdown, resolvedMarkdown.attachment.path, explicitTitle);
|
|
5211
|
+
const title = normalized.title;
|
|
5212
|
+
const draftAssets = prepareArticleMarkdown(normalized.bodyMarkdown, resolvedMarkdown.attachment.path);
|
|
5213
|
+
const resolvedInlineImages = [];
|
|
5214
|
+
for (const image of draftAssets.inlineImages) {
|
|
5215
|
+
const resolved = await resolveArticleAttachment(image.path, image.marker);
|
|
5216
|
+
if (!resolved.ok || !resolved.attachment) {
|
|
5217
|
+
return resolved.ok
|
|
5218
|
+
? errorResult("VALIDATION_ERROR", `${image.marker} was not found`)
|
|
5219
|
+
: resolved.result;
|
|
5220
|
+
}
|
|
5221
|
+
resolvedInlineImages.push({
|
|
5222
|
+
...image,
|
|
5223
|
+
path: resolved.attachment.path,
|
|
5224
|
+
name: resolved.attachment.name,
|
|
5225
|
+
});
|
|
5226
|
+
}
|
|
5227
|
+
const articlePage = await page.context().newPage();
|
|
5228
|
+
let shouldClose = true;
|
|
5229
|
+
try {
|
|
5230
|
+
await ensureNetworkCaptureInstalled(articlePage);
|
|
5231
|
+
await articlePage.goto("https://x.com/compose/articles", { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
5232
|
+
const started = await openNewArticleEditor(articlePage);
|
|
5233
|
+
if (!started.ok) {
|
|
5234
|
+
return errorResult("UPSTREAM_CHANGED", "article editor could not be opened", {
|
|
5235
|
+
reason: started.reason,
|
|
5236
|
+
});
|
|
5237
|
+
}
|
|
5238
|
+
const titleSet = await setArticleTitle(articlePage, title);
|
|
5239
|
+
if (!titleSet) {
|
|
5240
|
+
return errorResult("UPSTREAM_CHANGED", "article title controls not found");
|
|
5241
|
+
}
|
|
5242
|
+
if (coverImagePath) {
|
|
5243
|
+
const resolvedCover = await resolveArticleAttachment(coverImagePath, "coverImagePath");
|
|
5244
|
+
if (!resolvedCover.ok || !resolvedCover.attachment) {
|
|
5245
|
+
return resolvedCover.ok ? errorResult("VALIDATION_ERROR", "coverImagePath was not found") : resolvedCover.result;
|
|
5246
|
+
}
|
|
5247
|
+
const coverTriggered = await triggerArticleCoverUpload(articlePage);
|
|
5248
|
+
if (!coverTriggered) {
|
|
5249
|
+
return errorResult("UPSTREAM_CHANGED", "article cover upload controls not found");
|
|
5250
|
+
}
|
|
5251
|
+
const coverUploaded = await uploadArticleFile(articlePage, resolvedCover.attachment.path);
|
|
5252
|
+
if (!coverUploaded) {
|
|
5253
|
+
return errorResult("UPSTREAM_CHANGED", "article cover upload failed");
|
|
5254
|
+
}
|
|
5255
|
+
const coverApplied = await waitForArticleCoverApplied(articlePage);
|
|
5256
|
+
if (!coverApplied) {
|
|
5257
|
+
return errorResult("ACTION_UNCONFIRMED", "article cover upload was not confirmed");
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
const pasted = await pasteArticleMarkdown(articlePage, draftAssets.markdown, draftAssets.html);
|
|
5261
|
+
if (!pasted) {
|
|
5262
|
+
return errorResult("UPSTREAM_CHANGED", "article markdown paste failed");
|
|
5263
|
+
}
|
|
5264
|
+
const inlineUploadResult = await uploadArticleInlineImages(articlePage, resolvedInlineImages);
|
|
5265
|
+
if (!inlineUploadResult.ok) {
|
|
5266
|
+
return errorResult("UPSTREAM_CHANGED", "article inline image upload failed", {
|
|
5267
|
+
reason: inlineUploadResult.reason,
|
|
5268
|
+
});
|
|
5269
|
+
}
|
|
5270
|
+
const editUrl = started.editUrl;
|
|
5271
|
+
const articleId = parseArticleIdFromUrl(editUrl);
|
|
5272
|
+
const output = {
|
|
5273
|
+
ok: true,
|
|
5274
|
+
editUrl,
|
|
5275
|
+
title,
|
|
5276
|
+
inlineImageCount: resolvedInlineImages.length,
|
|
5277
|
+
hasCoverImage: typeof coverImagePath === "string" && coverImagePath.trim().length > 0,
|
|
5278
|
+
};
|
|
5279
|
+
if (articleId) {
|
|
5280
|
+
output.articleId = articleId;
|
|
5281
|
+
output.draftId = articleId;
|
|
5282
|
+
output.previewUrl = buildArticlePreviewUrl(articleId);
|
|
5283
|
+
const persisted = await waitForArticleDraftPersisted(articlePage, articleId, title);
|
|
5284
|
+
output.persisted = persisted;
|
|
5285
|
+
output.sessionScoped = !persisted;
|
|
5286
|
+
await cacheArticleDraftPage(page, articleId, articlePage);
|
|
5287
|
+
shouldClose = false;
|
|
5288
|
+
}
|
|
5289
|
+
return output;
|
|
5290
|
+
}
|
|
5291
|
+
finally {
|
|
5292
|
+
if (shouldClose) {
|
|
5293
|
+
await articlePage.close().catch(() => { });
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
async function publishArticleMarkdown(page, markdownPath, explicitTitle, coverImagePath, dryRun, timeoutMs) {
|
|
5298
|
+
const drafted = await draftArticleMarkdown(page, markdownPath, explicitTitle, coverImagePath);
|
|
5299
|
+
if (dryRun) {
|
|
5300
|
+
if (drafted && typeof drafted === "object" && !Array.isArray(drafted) && !("error" in drafted)) {
|
|
5301
|
+
return { ...drafted, dryRun: true };
|
|
5302
|
+
}
|
|
5303
|
+
return drafted;
|
|
5304
|
+
}
|
|
5305
|
+
if (!drafted || typeof drafted !== "object" || Array.isArray(drafted) || ("error" in drafted)) {
|
|
5306
|
+
return drafted;
|
|
5307
|
+
}
|
|
5308
|
+
const editUrl = typeof drafted.editUrl === "string" ? drafted.editUrl : "";
|
|
5309
|
+
if (!editUrl) {
|
|
5310
|
+
return errorResult("UPSTREAM_CHANGED", "article draft edit url not found");
|
|
5311
|
+
}
|
|
5312
|
+
return await withEphemeralPage(page, editUrl, async (articlePage) => {
|
|
5313
|
+
await waitForArticleEditorSurface(articlePage);
|
|
5314
|
+
const published = await publishArticleEditor(articlePage, timeoutMs);
|
|
5315
|
+
if (!published.ok) {
|
|
5316
|
+
const details = {
|
|
5317
|
+
reason: published.reason,
|
|
5318
|
+
};
|
|
5319
|
+
if (published.details) {
|
|
5320
|
+
details.debug = published.details;
|
|
5321
|
+
}
|
|
5322
|
+
return errorResult("ACTION_UNCONFIRMED", "article publish was not confirmed", {
|
|
5323
|
+
...details,
|
|
5324
|
+
});
|
|
5325
|
+
}
|
|
5326
|
+
const output = {
|
|
5327
|
+
...drafted,
|
|
5328
|
+
confirmed: true,
|
|
5329
|
+
editUrl: published.editUrl,
|
|
5330
|
+
};
|
|
5331
|
+
if (published.articleId) {
|
|
5332
|
+
output.articleId = published.articleId;
|
|
5333
|
+
}
|
|
5334
|
+
if (published.articleUrl) {
|
|
5335
|
+
output.articleUrl = published.articleUrl;
|
|
5336
|
+
}
|
|
5337
|
+
return output;
|
|
5338
|
+
});
|
|
5339
|
+
}
|
|
5340
|
+
async function publishExistingArticle(page, targetUrl, timeoutMs) {
|
|
5341
|
+
const articleId = parseArticleIdFromUrl(targetUrl);
|
|
5342
|
+
const cachedPage = articleId ? getCachedArticleDraftPage(page, articleId) : undefined;
|
|
5343
|
+
const runPublish = async (articlePage) => {
|
|
5344
|
+
await waitForArticleEditorSurface(articlePage);
|
|
5345
|
+
await ensureArticleDraftLoaded(articlePage, articleId);
|
|
5346
|
+
const published = await publishArticleEditor(articlePage, timeoutMs);
|
|
5347
|
+
if (!published.ok) {
|
|
5348
|
+
const details = {
|
|
5349
|
+
reason: published.reason,
|
|
5350
|
+
};
|
|
5351
|
+
if (published.details) {
|
|
5352
|
+
details.debug = published.details;
|
|
5353
|
+
}
|
|
5354
|
+
return errorResult("ACTION_UNCONFIRMED", "article publish was not confirmed", details);
|
|
5355
|
+
}
|
|
5356
|
+
const output = {
|
|
5357
|
+
ok: true,
|
|
5358
|
+
confirmed: true,
|
|
5359
|
+
editUrl: published.editUrl,
|
|
5360
|
+
};
|
|
5361
|
+
if (published.articleId) {
|
|
5362
|
+
output.articleId = published.articleId;
|
|
5363
|
+
}
|
|
5364
|
+
if (published.articleUrl) {
|
|
5365
|
+
output.articleUrl = published.articleUrl;
|
|
5366
|
+
}
|
|
5367
|
+
return output;
|
|
5368
|
+
};
|
|
5369
|
+
if (cachedPage) {
|
|
5370
|
+
const result = await runPublish(cachedPage);
|
|
5371
|
+
if (articleId && (!result || typeof result !== "object" || Array.isArray(result) || !("error" in result))) {
|
|
5372
|
+
await removeCachedArticleDraftPage(page, articleId);
|
|
5373
|
+
}
|
|
5374
|
+
return result;
|
|
5375
|
+
}
|
|
5376
|
+
return await withEphemeralPage(page, targetUrl, runPublish);
|
|
5377
|
+
}
|
|
5378
|
+
async function withArticleDraftPage(ownerPage, targetUrl, run) {
|
|
5379
|
+
const articleId = parseArticleIdFromUrl(targetUrl);
|
|
5380
|
+
const cachedPage = articleId ? getCachedArticleDraftPage(ownerPage, articleId) : undefined;
|
|
5381
|
+
if (cachedPage) {
|
|
5382
|
+
const cachedUrl = cachedPage.url();
|
|
5383
|
+
const cachedArticleId = parseArticleIdFromUrl(cachedUrl);
|
|
5384
|
+
if (!articleId || cachedArticleId === articleId) {
|
|
5385
|
+
await waitForArticleEditorSurface(cachedPage);
|
|
5386
|
+
await ensureArticleDraftLoaded(cachedPage, articleId);
|
|
5387
|
+
return await run(cachedPage, articleId, true);
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
return await withEphemeralPage(ownerPage, targetUrl, async (articlePage) => {
|
|
5391
|
+
await waitForArticleEditorSurface(articlePage);
|
|
5392
|
+
await ensureArticleDraftLoaded(articlePage, articleId);
|
|
5393
|
+
return await run(articlePage, articleId, false);
|
|
5394
|
+
});
|
|
5395
|
+
}
|
|
5396
|
+
async function setArticleCoverImage(page, targetUrl, coverImagePath) {
|
|
5397
|
+
const resolvedCover = await resolveArticleAttachment(coverImagePath, "coverImagePath");
|
|
5398
|
+
if (!resolvedCover.ok || !resolvedCover.attachment) {
|
|
5399
|
+
return resolvedCover.ok ? errorResult("VALIDATION_ERROR", "coverImagePath was not found") : resolvedCover.result;
|
|
5400
|
+
}
|
|
5401
|
+
const coverAttachment = resolvedCover.attachment;
|
|
5402
|
+
return await withArticleDraftPage(page, targetUrl, async (articlePage, articleId, sessionScoped) => {
|
|
5403
|
+
const coverTriggered = await triggerArticleCoverUpload(articlePage);
|
|
5404
|
+
if (!coverTriggered) {
|
|
5405
|
+
return errorResult("UPSTREAM_CHANGED", "article cover upload controls not found");
|
|
5406
|
+
}
|
|
5407
|
+
const coverUploaded = await uploadArticleFile(articlePage, coverAttachment.path);
|
|
5408
|
+
if (!coverUploaded) {
|
|
5409
|
+
return errorResult("UPSTREAM_CHANGED", "article cover upload failed");
|
|
5410
|
+
}
|
|
5411
|
+
const coverApplied = await waitForArticleCoverApplied(articlePage);
|
|
5412
|
+
if (!coverApplied) {
|
|
5413
|
+
return errorResult("ACTION_UNCONFIRMED", "article cover upload was not confirmed");
|
|
5414
|
+
}
|
|
5415
|
+
const output = {
|
|
5416
|
+
ok: true,
|
|
5417
|
+
editUrl: articlePage.url(),
|
|
5418
|
+
hasCoverImage: true,
|
|
5419
|
+
};
|
|
5420
|
+
if (articleId) {
|
|
5421
|
+
output.articleId = articleId;
|
|
5422
|
+
output.sessionScoped = sessionScoped === true;
|
|
5423
|
+
}
|
|
5424
|
+
return output;
|
|
5425
|
+
});
|
|
5426
|
+
}
|
|
5427
|
+
async function updateArticleMarkdown(page, targetUrl, markdownPath, explicitTitle) {
|
|
5428
|
+
const resolvedMarkdown = await resolveArticleAttachment(markdownPath, "markdownPath");
|
|
5429
|
+
if (!resolvedMarkdown.ok || !resolvedMarkdown.attachment) {
|
|
5430
|
+
return resolvedMarkdown.ok ? errorResult("VALIDATION_ERROR", "markdownPath was not found") : resolvedMarkdown.result;
|
|
5431
|
+
}
|
|
5432
|
+
const markdown = await readFile(resolvedMarkdown.attachment.path, "utf8").catch(() => undefined);
|
|
5433
|
+
if (markdown === undefined) {
|
|
5434
|
+
return errorResult("VALIDATION_ERROR", `markdownPath was not found: ${markdownPath}`);
|
|
5435
|
+
}
|
|
5436
|
+
const normalized = normalizeArticleMarkdown(markdown, resolvedMarkdown.attachment.path, explicitTitle);
|
|
5437
|
+
const title = normalized.title;
|
|
5438
|
+
const draftAssets = prepareArticleMarkdown(normalized.bodyMarkdown, resolvedMarkdown.attachment.path);
|
|
5439
|
+
const resolvedInlineImages = [];
|
|
5440
|
+
for (const image of draftAssets.inlineImages) {
|
|
5441
|
+
const resolved = await resolveArticleAttachment(image.path, image.marker);
|
|
5442
|
+
if (!resolved.ok || !resolved.attachment) {
|
|
5443
|
+
return resolved.ok
|
|
5444
|
+
? errorResult("VALIDATION_ERROR", `${image.marker} was not found`)
|
|
5445
|
+
: resolved.result;
|
|
5446
|
+
}
|
|
5447
|
+
resolvedInlineImages.push({
|
|
5448
|
+
...image,
|
|
5449
|
+
path: resolved.attachment.path,
|
|
5450
|
+
name: resolved.attachment.name,
|
|
5451
|
+
});
|
|
5452
|
+
}
|
|
5453
|
+
return await withArticleDraftPage(page, targetUrl, async (articlePage, articleId, sessionScoped) => {
|
|
5454
|
+
const titleSet = await setArticleTitle(articlePage, title);
|
|
5455
|
+
if (!titleSet) {
|
|
5456
|
+
return errorResult("UPSTREAM_CHANGED", "article title controls not found");
|
|
5457
|
+
}
|
|
5458
|
+
const cleared = await clearArticleBody(articlePage);
|
|
5459
|
+
if (!cleared) {
|
|
5460
|
+
return errorResult("UPSTREAM_CHANGED", "article body controls not found");
|
|
5461
|
+
}
|
|
5462
|
+
const pasted = await pasteArticleMarkdown(articlePage, draftAssets.markdown, draftAssets.html);
|
|
5463
|
+
if (!pasted) {
|
|
5464
|
+
return errorResult("UPSTREAM_CHANGED", "article markdown paste failed");
|
|
5465
|
+
}
|
|
5466
|
+
const inlineUploadResult = await uploadArticleInlineImages(articlePage, resolvedInlineImages);
|
|
5467
|
+
if (!inlineUploadResult.ok) {
|
|
5468
|
+
return errorResult("UPSTREAM_CHANGED", "article inline image upload failed", {
|
|
5469
|
+
reason: inlineUploadResult.reason,
|
|
5470
|
+
});
|
|
5471
|
+
}
|
|
5472
|
+
const output = {
|
|
5473
|
+
ok: true,
|
|
5474
|
+
title,
|
|
5475
|
+
editUrl: articlePage.url(),
|
|
5476
|
+
inlineImageCount: resolvedInlineImages.length,
|
|
5477
|
+
};
|
|
5478
|
+
if (articleId) {
|
|
5479
|
+
output.articleId = articleId;
|
|
5480
|
+
output.draftId = articleId;
|
|
5481
|
+
output.previewUrl = buildArticlePreviewUrl(articleId);
|
|
5482
|
+
const persisted = await waitForArticleDraftPersisted(articlePage, articleId, title);
|
|
5483
|
+
output.persisted = persisted;
|
|
5484
|
+
output.sessionScoped = sessionScoped === true || !persisted;
|
|
5485
|
+
}
|
|
5486
|
+
return output;
|
|
5487
|
+
});
|
|
5488
|
+
}
|
|
5489
|
+
async function upsertArticleDraftMarkdown(page, targetUrl, markdownPath, explicitTitle, coverImagePath) {
|
|
5490
|
+
if (targetUrl) {
|
|
5491
|
+
return await updateArticleMarkdown(page, targetUrl, markdownPath, explicitTitle);
|
|
5492
|
+
}
|
|
5493
|
+
return await draftArticleMarkdown(page, markdownPath, explicitTitle, coverImagePath);
|
|
5494
|
+
}
|
|
5495
|
+
async function waitForGrokSurface(page) {
|
|
5496
|
+
await page
|
|
5497
|
+
.waitForFunction(() => {
|
|
5498
|
+
const composer = document.querySelector("textarea") ||
|
|
5499
|
+
document.querySelector("[contenteditable='true'][role='textbox']") ||
|
|
5500
|
+
document.querySelector("[role='textbox'][contenteditable='true']");
|
|
5501
|
+
const messages = document.querySelector("[data-message-author-role='assistant']") ||
|
|
5502
|
+
document.querySelector("[data-testid*='assistant']") ||
|
|
5503
|
+
document.querySelector("article");
|
|
5504
|
+
return composer !== null || messages !== null;
|
|
5505
|
+
}, undefined, { timeout: 12_000 })
|
|
5506
|
+
.catch(() => { });
|
|
5507
|
+
await page.waitForTimeout(800);
|
|
5508
|
+
}
|
|
5509
|
+
async function submitGrokPrompt(page, prompt) {
|
|
5510
|
+
const composerSelectors = [
|
|
5511
|
+
"textarea",
|
|
5512
|
+
"[contenteditable='true'][role='textbox']",
|
|
5513
|
+
"[role='textbox'][contenteditable='true']",
|
|
5514
|
+
];
|
|
5515
|
+
const submitSelectors = [
|
|
5516
|
+
"button[aria-label*='Grok something']",
|
|
5517
|
+
"button[aria-label*='Send']",
|
|
5518
|
+
"button[aria-label*='send']",
|
|
5519
|
+
"button[data-testid*='send']",
|
|
5520
|
+
"button[type='submit']",
|
|
5521
|
+
];
|
|
5522
|
+
const selectAllShortcut = process.platform === "darwin" ? "Meta+A" : "Control+A";
|
|
5523
|
+
let composerSelector;
|
|
5524
|
+
for (const selector of composerSelectors) {
|
|
5525
|
+
const handle = await page.waitForSelector(selector, { timeout: 1_200 }).catch(() => null);
|
|
5526
|
+
if (!handle) {
|
|
5527
|
+
continue;
|
|
5528
|
+
}
|
|
5529
|
+
await handle.dispose().catch(() => { });
|
|
5530
|
+
composerSelector = selector;
|
|
5531
|
+
break;
|
|
5532
|
+
}
|
|
5533
|
+
if (!composerSelector) {
|
|
5534
|
+
return { ok: false, reason: "composer_not_found" };
|
|
5535
|
+
}
|
|
5536
|
+
try {
|
|
5537
|
+
await page.click(composerSelector);
|
|
5538
|
+
await page.keyboard.press(selectAllShortcut).catch(() => { });
|
|
5539
|
+
await page.keyboard.press("Backspace").catch(() => { });
|
|
5540
|
+
const setPromptInOneShot = await page.evaluate(({ selector, value }) => {
|
|
5541
|
+
const element = document.querySelector(selector);
|
|
5542
|
+
if (!element) {
|
|
5543
|
+
return false;
|
|
5544
|
+
}
|
|
5545
|
+
if (element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement) {
|
|
5546
|
+
element.focus();
|
|
5547
|
+
element.value = value;
|
|
5548
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
|
|
5549
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
5550
|
+
return true;
|
|
5551
|
+
}
|
|
5552
|
+
if (element instanceof HTMLElement && element.isContentEditable) {
|
|
5553
|
+
element.focus();
|
|
5554
|
+
try {
|
|
5555
|
+
const selection = window.getSelection();
|
|
5556
|
+
const range = document.createRange();
|
|
5557
|
+
range.selectNodeContents(element);
|
|
5558
|
+
selection?.removeAllRanges();
|
|
5559
|
+
selection?.addRange(range);
|
|
5560
|
+
document.execCommand("insertText", false, value);
|
|
5561
|
+
}
|
|
5562
|
+
catch {
|
|
5563
|
+
// Ignore and fallback to direct assignment below.
|
|
5564
|
+
}
|
|
5565
|
+
if ((element.textContent ?? "").trim() !== value) {
|
|
5566
|
+
element.textContent = value;
|
|
5567
|
+
}
|
|
5568
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
|
|
5569
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
5570
|
+
return true;
|
|
5571
|
+
}
|
|
5572
|
+
return false;
|
|
5573
|
+
}, { selector: composerSelector, value: prompt }).catch(() => false);
|
|
5574
|
+
if (!setPromptInOneShot) {
|
|
5575
|
+
await page.type(composerSelector, prompt, { delay: 12 });
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
catch {
|
|
5579
|
+
return { ok: false, reason: "compose_input_failed" };
|
|
5580
|
+
}
|
|
5581
|
+
const submitSelector = await page.evaluate((selectors) => {
|
|
5582
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
5583
|
+
for (const selector of selectors) {
|
|
5584
|
+
const element = document.querySelector(selector);
|
|
5585
|
+
if (!element) {
|
|
5586
|
+
continue;
|
|
2635
5587
|
}
|
|
2636
5588
|
if (element instanceof HTMLButtonElement && element.disabled) {
|
|
2637
5589
|
continue;
|
|
@@ -2709,314 +5661,95 @@ async function prepareGrokSession(page, conversationId) {
|
|
|
2709
5661
|
async function uploadGrokAttachments(page, attachments) {
|
|
2710
5662
|
if (attachments.length === 0) {
|
|
2711
5663
|
return { ok: true };
|
|
2712
|
-
}
|
|
2713
|
-
const uploadSelectors = [
|
|
2714
|
-
"input[type='file'][accept*='application/pdf']",
|
|
2715
|
-
"input[type='file'][accept*='text/csv']",
|
|
2716
|
-
"input[type='file'][accept*='text/plain']",
|
|
2717
|
-
"input[type='file']",
|
|
2718
|
-
];
|
|
2719
|
-
let uploadSelector;
|
|
2720
|
-
for (const selector of uploadSelectors) {
|
|
2721
|
-
const handle = await page.waitForSelector(selector, { timeout: 1_200 }).catch(() => null);
|
|
2722
|
-
if (!handle) {
|
|
2723
|
-
continue;
|
|
2724
|
-
}
|
|
2725
|
-
await handle.dispose().catch(() => { });
|
|
2726
|
-
uploadSelector = selector;
|
|
2727
|
-
break;
|
|
2728
|
-
}
|
|
2729
|
-
if (!uploadSelector) {
|
|
2730
|
-
return { ok: false, reason: "attachment_input_not_found" };
|
|
2731
|
-
}
|
|
2732
|
-
try {
|
|
2733
|
-
await page.setInputFiles(uploadSelector, attachments.map((attachment) => attachment.path));
|
|
2734
|
-
}
|
|
2735
|
-
catch {
|
|
2736
|
-
return { ok: false, reason: "attachment_upload_failed" };
|
|
2737
|
-
}
|
|
2738
|
-
const attachmentNames = attachments.map((attachment) => attachment.name);
|
|
2739
|
-
await page
|
|
2740
|
-
.waitForFunction(({ names }) => {
|
|
2741
|
-
const bodyText = document.body?.innerText ?? "";
|
|
2742
|
-
return names.every((name) => bodyText.includes(name));
|
|
2743
|
-
}, { names: attachmentNames }, { timeout: 10_000 })
|
|
2744
|
-
.catch(() => { });
|
|
2745
|
-
await page.waitForTimeout(600);
|
|
2746
|
-
return { ok: true };
|
|
2747
|
-
}
|
|
2748
|
-
async function askGrokViaNetwork(page, prompt, timeoutMs) {
|
|
2749
|
-
const captured = await captureRoutedResponseText(page, "https://grok.x.com/2/grok/add_response.json*", async () => {
|
|
2750
|
-
const submitResult = await submitGrokPrompt(page, prompt);
|
|
2751
|
-
return submitResult.ok;
|
|
2752
|
-
}, {
|
|
2753
|
-
timeoutMs,
|
|
2754
|
-
});
|
|
2755
|
-
if (!captured || captured.status < 200 || captured.status >= 300) {
|
|
2756
|
-
return undefined;
|
|
2757
|
-
}
|
|
2758
|
-
const responseText = captured.text;
|
|
2759
|
-
const entries = parseNdjsonLines(responseText);
|
|
2760
|
-
const finalParts = collectTextByTag(entries.map((entry) => {
|
|
2761
|
-
const output = {};
|
|
2762
|
-
if (typeof entry.result?.message === "string") {
|
|
2763
|
-
output.message = entry.result.message;
|
|
2764
|
-
}
|
|
2765
|
-
if (typeof entry.result?.messageTag === "string") {
|
|
2766
|
-
output.messageTag = entry.result.messageTag;
|
|
2767
|
-
}
|
|
2768
|
-
return output;
|
|
2769
|
-
}), "final");
|
|
2770
|
-
let conversationId;
|
|
2771
|
-
for (const entry of entries) {
|
|
2772
|
-
if (!conversationId && typeof entry.conversationId === "string") {
|
|
2773
|
-
conversationId = entry.conversationId;
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
const finalResponse = joinTextParts(finalParts).trim();
|
|
2777
|
-
if (!finalResponse) {
|
|
2778
|
-
return undefined;
|
|
2779
|
-
}
|
|
2780
|
-
const artifactResult = await materializeGrokArtifacts(finalResponse);
|
|
2781
|
-
const output = {
|
|
2782
|
-
ok: true,
|
|
2783
|
-
response: artifactResult.response,
|
|
2784
|
-
url: typeof conversationId === "string" ? `https://x.com/i/grok?conversation=${conversationId}` : page.url(),
|
|
2785
|
-
};
|
|
2786
|
-
if (typeof conversationId === "string") {
|
|
2787
|
-
output.conversationId = conversationId;
|
|
2788
|
-
}
|
|
2789
|
-
if (artifactResult.artifacts) {
|
|
2790
|
-
output.artifacts = artifactResult.artifacts;
|
|
2791
|
-
}
|
|
2792
|
-
return output;
|
|
2793
|
-
}
|
|
2794
|
-
async function waitForGrokResponse(page, previousResponse, prompt, timeoutMs) {
|
|
2795
|
-
try {
|
|
2796
|
-
await page.waitForFunction(({ op, previous, promptText }) => {
|
|
2797
|
-
if (op !== "grok_wait") {
|
|
2798
|
-
return false;
|
|
2799
|
-
}
|
|
2800
|
-
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
2801
|
-
const previousText = normalize(previous);
|
|
2802
|
-
const normalizedPrompt = normalize(promptText);
|
|
2803
|
-
const isIgnoredResponse = (value) => {
|
|
2804
|
-
const lower = value.toLowerCase();
|
|
2805
|
-
return (lower.length < 3 ||
|
|
2806
|
-
lower.startsWith("see new posts") ||
|
|
2807
|
-
lower.startsWith("thought for ") ||
|
|
2808
|
-
lower.startsWith("agents thinking") ||
|
|
2809
|
-
lower.startsWith("ask anything") ||
|
|
2810
|
-
lower === "agents" ||
|
|
2811
|
-
lower === "thinking" ||
|
|
2812
|
-
lower === "expert" ||
|
|
2813
|
-
lower.startsWith("grok") ||
|
|
2814
|
-
lower.includes("explore ") ||
|
|
2815
|
-
lower.includes("discuss ") ||
|
|
2816
|
-
lower.includes("create images") ||
|
|
2817
|
-
lower.includes("edit image") ||
|
|
2818
|
-
lower.includes("latest news") ||
|
|
2819
|
-
lower === normalizedPrompt.toLowerCase() ||
|
|
2820
|
-
lower === previousText.toLowerCase());
|
|
2821
|
-
};
|
|
2822
|
-
const scope = document.querySelector("div[aria-label='Grok']") ??
|
|
2823
|
-
document.querySelector("main");
|
|
2824
|
-
if (!scope) {
|
|
2825
|
-
return false;
|
|
2826
|
-
}
|
|
2827
|
-
const lines = (scope.innerText || scope.textContent || "")
|
|
2828
|
-
.split(/\n+/)
|
|
2829
|
-
.map((line) => normalize(line))
|
|
2830
|
-
.filter((line) => line.length > 0);
|
|
2831
|
-
const linePromptIndex = lines.lastIndexOf(normalizedPrompt);
|
|
2832
|
-
if (linePromptIndex >= 0) {
|
|
2833
|
-
const hasStopControl = Array.from(document.querySelectorAll("button")).some((button) => {
|
|
2834
|
-
const label = normalize(button.getAttribute("aria-label") ?? button.textContent ?? "").toLowerCase();
|
|
2835
|
-
return label.includes("stop");
|
|
2836
|
-
});
|
|
2837
|
-
let hasLineCandidate = false;
|
|
2838
|
-
for (let index = linePromptIndex + 1; index < lines.length; index += 1) {
|
|
2839
|
-
const candidate = lines[index];
|
|
2840
|
-
if (!candidate || isIgnoredResponse(candidate)) {
|
|
2841
|
-
continue;
|
|
2842
|
-
}
|
|
2843
|
-
hasLineCandidate = true;
|
|
2844
|
-
}
|
|
2845
|
-
if (!hasStopControl && hasLineCandidate) {
|
|
2846
|
-
return true;
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
const entries = [];
|
|
2850
|
-
for (const node of Array.from(scope.querySelectorAll("div, span, p"))) {
|
|
2851
|
-
if (node.closest("button, a, textarea, nav")) {
|
|
2852
|
-
continue;
|
|
2853
|
-
}
|
|
2854
|
-
const text = normalize(node.innerText || node.textContent || "");
|
|
2855
|
-
if (!text) {
|
|
2856
|
-
continue;
|
|
2857
|
-
}
|
|
2858
|
-
const childWithSameText = Array.from(node.children).some((child) => {
|
|
2859
|
-
if (!(child instanceof HTMLElement)) {
|
|
2860
|
-
return false;
|
|
2861
|
-
}
|
|
2862
|
-
return normalize(child.innerText || child.textContent || "") === text;
|
|
2863
|
-
});
|
|
2864
|
-
if (childWithSameText) {
|
|
2865
|
-
continue;
|
|
2866
|
-
}
|
|
2867
|
-
if (entries[entries.length - 1] !== text) {
|
|
2868
|
-
entries.push(text);
|
|
2869
|
-
}
|
|
2870
|
-
}
|
|
2871
|
-
const hasStopControl = Array.from(document.querySelectorAll("button")).some((button) => {
|
|
2872
|
-
const label = normalize(button.getAttribute("aria-label") ?? button.textContent ?? "").toLowerCase();
|
|
2873
|
-
return label.includes("stop");
|
|
2874
|
-
});
|
|
2875
|
-
const promptIndex = entries.lastIndexOf(normalizedPrompt);
|
|
2876
|
-
if (promptIndex < 0) {
|
|
2877
|
-
return false;
|
|
2878
|
-
}
|
|
2879
|
-
let hasCandidate = false;
|
|
2880
|
-
for (let index = promptIndex + 1; index < entries.length; index += 1) {
|
|
2881
|
-
const candidate = entries[index];
|
|
2882
|
-
if (!candidate || isIgnoredResponse(candidate)) {
|
|
2883
|
-
continue;
|
|
2884
|
-
}
|
|
2885
|
-
hasCandidate = true;
|
|
2886
|
-
}
|
|
2887
|
-
return !hasStopControl && hasCandidate;
|
|
2888
|
-
}, {
|
|
2889
|
-
op: "grok_wait",
|
|
2890
|
-
previous: (previousResponse ?? "").replace(/\s+/g, " ").trim(),
|
|
2891
|
-
promptText: prompt,
|
|
2892
|
-
}, { timeout: timeoutMs });
|
|
2893
|
-
}
|
|
2894
|
-
catch {
|
|
2895
|
-
return { confirmed: false };
|
|
2896
|
-
}
|
|
2897
|
-
const state = await page.evaluate(({ op, promptText, previousText }) => {
|
|
2898
|
-
if (op !== "grok_extract_state") {
|
|
2899
|
-
return undefined;
|
|
2900
|
-
}
|
|
2901
|
-
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
2902
|
-
const normalizedPrompt = normalize(promptText);
|
|
2903
|
-
const normalizedPrevious = normalize(previousText);
|
|
2904
|
-
const isIgnoredResponse = (value) => {
|
|
2905
|
-
const lower = value.toLowerCase();
|
|
2906
|
-
return (lower.length < 3 ||
|
|
2907
|
-
lower.startsWith("see new posts") ||
|
|
2908
|
-
lower.startsWith("thought for ") ||
|
|
2909
|
-
lower.startsWith("agents thinking") ||
|
|
2910
|
-
lower.startsWith("ask anything") ||
|
|
2911
|
-
lower === "agents" ||
|
|
2912
|
-
lower === "thinking" ||
|
|
2913
|
-
lower === "expert" ||
|
|
2914
|
-
lower.startsWith("grok") ||
|
|
2915
|
-
lower.includes("explore ") ||
|
|
2916
|
-
lower.includes("discuss ") ||
|
|
2917
|
-
lower.includes("create images") ||
|
|
2918
|
-
lower.includes("edit image") ||
|
|
2919
|
-
lower.includes("latest news") ||
|
|
2920
|
-
lower === normalizedPrompt.toLowerCase() ||
|
|
2921
|
-
lower === normalizedPrevious.toLowerCase());
|
|
2922
|
-
};
|
|
2923
|
-
const scope = document.querySelector("div[aria-label='Grok']") ??
|
|
2924
|
-
document.querySelector("main");
|
|
2925
|
-
if (!scope) {
|
|
2926
|
-
return undefined;
|
|
2927
|
-
}
|
|
2928
|
-
const lines = (scope.innerText || scope.textContent || "")
|
|
2929
|
-
.split(/\n+/)
|
|
2930
|
-
.map((line) => normalize(line))
|
|
2931
|
-
.filter((line) => line.length > 0);
|
|
2932
|
-
const lineResponseCandidates = [];
|
|
2933
|
-
const linePromptIndex = lines.lastIndexOf(normalizedPrompt);
|
|
2934
|
-
if (linePromptIndex >= 0) {
|
|
2935
|
-
for (let index = linePromptIndex + 1; index < lines.length; index += 1) {
|
|
2936
|
-
const candidate = lines[index];
|
|
2937
|
-
if (!candidate || isIgnoredResponse(candidate)) {
|
|
2938
|
-
continue;
|
|
2939
|
-
}
|
|
2940
|
-
lineResponseCandidates.push(candidate);
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
let responseForPrompt;
|
|
2944
|
-
if (lineResponseCandidates.length > 0) {
|
|
2945
|
-
responseForPrompt = lineResponseCandidates.sort((left, right) => right.length - left.length)[0];
|
|
2946
|
-
}
|
|
2947
|
-
if (responseForPrompt) {
|
|
2948
|
-
let latestResponse = responseForPrompt;
|
|
2949
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
2950
|
-
const candidate = lines[index];
|
|
2951
|
-
if (!candidate || isIgnoredResponse(candidate)) {
|
|
2952
|
-
continue;
|
|
2953
|
-
}
|
|
2954
|
-
latestResponse = candidate;
|
|
2955
|
-
break;
|
|
2956
|
-
}
|
|
2957
|
-
return {
|
|
2958
|
-
responseForPrompt,
|
|
2959
|
-
latestResponse,
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
const entries = [];
|
|
2963
|
-
for (const node of Array.from(scope.querySelectorAll("div, span, p"))) {
|
|
2964
|
-
if (node.closest("button, a, textarea, nav")) {
|
|
2965
|
-
continue;
|
|
2966
|
-
}
|
|
2967
|
-
const text = normalize(node.innerText || node.textContent || "");
|
|
2968
|
-
if (!text) {
|
|
2969
|
-
continue;
|
|
2970
|
-
}
|
|
2971
|
-
const childWithSameText = Array.from(node.children).some((child) => {
|
|
2972
|
-
if (!(child instanceof HTMLElement)) {
|
|
2973
|
-
return false;
|
|
2974
|
-
}
|
|
2975
|
-
return normalize(child.innerText || child.textContent || "") === text;
|
|
2976
|
-
});
|
|
2977
|
-
if (childWithSameText) {
|
|
2978
|
-
continue;
|
|
2979
|
-
}
|
|
2980
|
-
if (entries[entries.length - 1] !== text) {
|
|
2981
|
-
entries.push(text);
|
|
2982
|
-
}
|
|
5664
|
+
}
|
|
5665
|
+
const uploadSelectors = [
|
|
5666
|
+
"input[type='file'][accept*='application/pdf']",
|
|
5667
|
+
"input[type='file'][accept*='text/csv']",
|
|
5668
|
+
"input[type='file'][accept*='text/plain']",
|
|
5669
|
+
"input[type='file']",
|
|
5670
|
+
];
|
|
5671
|
+
let uploadSelector;
|
|
5672
|
+
for (const selector of uploadSelectors) {
|
|
5673
|
+
const handle = await page.waitForSelector(selector, { timeout: 1_200 }).catch(() => null);
|
|
5674
|
+
if (!handle) {
|
|
5675
|
+
continue;
|
|
2983
5676
|
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
5677
|
+
await handle.dispose().catch(() => { });
|
|
5678
|
+
uploadSelector = selector;
|
|
5679
|
+
break;
|
|
5680
|
+
}
|
|
5681
|
+
if (!uploadSelector) {
|
|
5682
|
+
return { ok: false, reason: "attachment_input_not_found" };
|
|
5683
|
+
}
|
|
5684
|
+
try {
|
|
5685
|
+
await page.setInputFiles(uploadSelector, attachments.map((attachment) => attachment.path));
|
|
5686
|
+
}
|
|
5687
|
+
catch {
|
|
5688
|
+
return { ok: false, reason: "attachment_upload_failed" };
|
|
5689
|
+
}
|
|
5690
|
+
const attachmentNames = attachments.map((attachment) => attachment.name);
|
|
5691
|
+
await page
|
|
5692
|
+
.waitForFunction(({ names }) => {
|
|
5693
|
+
const bodyText = document.body?.innerText ?? "";
|
|
5694
|
+
return names.every((name) => bodyText.includes(name));
|
|
5695
|
+
}, { names: attachmentNames }, { timeout: 10_000 })
|
|
5696
|
+
.catch(() => { });
|
|
5697
|
+
await page.waitForTimeout(600);
|
|
5698
|
+
return { ok: true };
|
|
5699
|
+
}
|
|
5700
|
+
async function askGrokViaNetwork(page, prompt, timeoutMs) {
|
|
5701
|
+
const captured = await captureRoutedResponseText(page, "https://grok.x.com/2/grok/add_response.json*", async () => {
|
|
5702
|
+
const submitResult = await submitGrokPrompt(page, prompt);
|
|
5703
|
+
return submitResult.ok;
|
|
5704
|
+
}, {
|
|
5705
|
+
timeoutMs,
|
|
5706
|
+
});
|
|
5707
|
+
if (!captured || captured.status < 200 || captured.status >= 300) {
|
|
5708
|
+
return undefined;
|
|
5709
|
+
}
|
|
5710
|
+
const responseText = captured.text;
|
|
5711
|
+
const entries = parseNdjsonLines(responseText);
|
|
5712
|
+
const finalParts = collectTextByTag(entries.map((entry) => {
|
|
5713
|
+
const output = {};
|
|
5714
|
+
if (typeof entry.result?.message === "string") {
|
|
5715
|
+
output.message = entry.result.message;
|
|
2994
5716
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
responseForPrompt = responseCandidates.sort((left, right) => right.length - left.length)[0];
|
|
5717
|
+
if (typeof entry.result?.messageTag === "string") {
|
|
5718
|
+
output.messageTag = entry.result.messageTag;
|
|
2998
5719
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
latestResponse = candidate;
|
|
3006
|
-
break;
|
|
5720
|
+
return output;
|
|
5721
|
+
}), "final");
|
|
5722
|
+
let conversationId;
|
|
5723
|
+
for (const entry of entries) {
|
|
5724
|
+
if (!conversationId && typeof entry.conversationId === "string") {
|
|
5725
|
+
conversationId = entry.conversationId;
|
|
3007
5726
|
}
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
}
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
5727
|
+
}
|
|
5728
|
+
const finalResponse = joinTextParts(finalParts).trim();
|
|
5729
|
+
if (!finalResponse) {
|
|
5730
|
+
return undefined;
|
|
5731
|
+
}
|
|
5732
|
+
const artifactResult = await materializeGrokArtifacts(finalResponse);
|
|
5733
|
+
const output = {
|
|
5734
|
+
ok: true,
|
|
5735
|
+
response: artifactResult.response,
|
|
5736
|
+
url: typeof conversationId === "string" ? `https://x.com/i/grok?conversation=${conversationId}` : page.url(),
|
|
5737
|
+
};
|
|
5738
|
+
if (typeof conversationId === "string") {
|
|
5739
|
+
output.conversationId = conversationId;
|
|
5740
|
+
}
|
|
5741
|
+
if (artifactResult.artifacts) {
|
|
5742
|
+
output.artifacts = artifactResult.artifacts;
|
|
5743
|
+
}
|
|
5744
|
+
return output;
|
|
5745
|
+
}
|
|
5746
|
+
async function waitForGrokResponse(page, previousResponse, prompt, timeoutMs) {
|
|
5747
|
+
const startedAt = Date.now();
|
|
5748
|
+
const hardDeadline = startedAt + Math.max(timeoutMs * 3, timeoutMs + 120_000);
|
|
5749
|
+
let idleDeadline = startedAt + timeoutMs;
|
|
5750
|
+
let previousSignature = "";
|
|
5751
|
+
while (Date.now() < hardDeadline && Date.now() < idleDeadline) {
|
|
5752
|
+
const state = await page.evaluate(({ op, promptText, previousText }) => {
|
|
3020
5753
|
if (op !== "grok_extract_state") {
|
|
3021
5754
|
return undefined;
|
|
3022
5755
|
}
|
|
@@ -3042,10 +5775,19 @@ async function waitForGrokResponse(page, previousResponse, prompt, timeoutMs) {
|
|
|
3042
5775
|
lower === normalizedPrompt.toLowerCase() ||
|
|
3043
5776
|
lower === normalizedPrevious.toLowerCase());
|
|
3044
5777
|
};
|
|
5778
|
+
const hasStopControl = Array.from(document.querySelectorAll("button")).some((button) => {
|
|
5779
|
+
const label = normalize(button.getAttribute("aria-label") ?? button.textContent ?? "").toLowerCase();
|
|
5780
|
+
return label.includes("stop");
|
|
5781
|
+
});
|
|
3045
5782
|
const scope = document.querySelector("div[aria-label='Grok']") ??
|
|
3046
5783
|
document.querySelector("main");
|
|
3047
5784
|
if (!scope) {
|
|
3048
|
-
return
|
|
5785
|
+
return {
|
|
5786
|
+
hasStopControl,
|
|
5787
|
+
responseForPrompt: undefined,
|
|
5788
|
+
latestResponse: undefined,
|
|
5789
|
+
signature: `stop:${String(hasStopControl)}`,
|
|
5790
|
+
};
|
|
3049
5791
|
}
|
|
3050
5792
|
const lines = (scope.innerText || scope.textContent || "")
|
|
3051
5793
|
.split(/\n+/)
|
|
@@ -3062,11 +5804,9 @@ async function waitForGrokResponse(page, previousResponse, prompt, timeoutMs) {
|
|
|
3062
5804
|
lineResponseCandidates.push(candidate);
|
|
3063
5805
|
}
|
|
3064
5806
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
5807
|
+
let responseForPrompt = lineResponseCandidates.length > 0
|
|
5808
|
+
? lineResponseCandidates.sort((left, right) => right.length - left.length)[0]
|
|
5809
|
+
: undefined;
|
|
3070
5810
|
const entries = [];
|
|
3071
5811
|
for (const node of Array.from(scope.querySelectorAll("div, span, p"))) {
|
|
3072
5812
|
if (node.closest("button, a, textarea, nav")) {
|
|
@@ -3089,44 +5829,69 @@ async function waitForGrokResponse(page, previousResponse, prompt, timeoutMs) {
|
|
|
3089
5829
|
entries.push(text);
|
|
3090
5830
|
}
|
|
3091
5831
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
5832
|
+
if (!responseForPrompt) {
|
|
5833
|
+
const promptIndex = entries.lastIndexOf(normalizedPrompt);
|
|
5834
|
+
if (promptIndex >= 0) {
|
|
5835
|
+
const responseCandidates = [];
|
|
5836
|
+
for (let index = promptIndex + 1; index < entries.length; index += 1) {
|
|
5837
|
+
const candidate = entries[index];
|
|
5838
|
+
if (!candidate || isIgnoredResponse(candidate)) {
|
|
5839
|
+
continue;
|
|
5840
|
+
}
|
|
5841
|
+
responseCandidates.push(candidate);
|
|
5842
|
+
}
|
|
5843
|
+
if (responseCandidates.length > 0) {
|
|
5844
|
+
responseForPrompt = responseCandidates.sort((left, right) => right.length - left.length)[0];
|
|
3099
5845
|
}
|
|
3100
|
-
responseCandidates.push(candidate);
|
|
3101
5846
|
}
|
|
3102
5847
|
}
|
|
3103
|
-
let
|
|
3104
|
-
|
|
3105
|
-
|
|
5848
|
+
let latestResponse;
|
|
5849
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
5850
|
+
const candidate = entries[index];
|
|
5851
|
+
if (!candidate || isIgnoredResponse(candidate)) {
|
|
5852
|
+
continue;
|
|
5853
|
+
}
|
|
5854
|
+
latestResponse = candidate;
|
|
5855
|
+
break;
|
|
3106
5856
|
}
|
|
3107
5857
|
return {
|
|
5858
|
+
hasStopControl,
|
|
3108
5859
|
responseForPrompt,
|
|
5860
|
+
latestResponse,
|
|
5861
|
+
signature: JSON.stringify({
|
|
5862
|
+
hasStopControl,
|
|
5863
|
+
responseForPrompt: responseForPrompt ?? "",
|
|
5864
|
+
latestResponse: latestResponse ?? "",
|
|
5865
|
+
}),
|
|
3109
5866
|
};
|
|
3110
5867
|
}, {
|
|
3111
5868
|
op: "grok_extract_state",
|
|
3112
5869
|
promptText: prompt,
|
|
3113
5870
|
previousText: previousResponse ?? "",
|
|
3114
|
-
});
|
|
3115
|
-
if (
|
|
3116
|
-
typeof
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
5871
|
+
}).catch(() => undefined);
|
|
5872
|
+
if (state && typeof state === "object") {
|
|
5873
|
+
const signature = typeof state.signature === "string" ? state.signature : "";
|
|
5874
|
+
if (signature && signature !== previousSignature) {
|
|
5875
|
+
previousSignature = signature;
|
|
5876
|
+
idleDeadline = Date.now() + timeoutMs;
|
|
5877
|
+
}
|
|
5878
|
+
if (state.hasStopControl === true) {
|
|
5879
|
+
idleDeadline = Date.now() + timeoutMs;
|
|
5880
|
+
}
|
|
5881
|
+
if (state.hasStopControl !== true &&
|
|
5882
|
+
typeof state.responseForPrompt === "string" &&
|
|
5883
|
+
state.responseForPrompt.length > 0) {
|
|
5884
|
+
return {
|
|
5885
|
+
confirmed: true,
|
|
5886
|
+
response: state.responseForPrompt,
|
|
5887
|
+
};
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
const remainingIdleMs = idleDeadline - Date.now();
|
|
5891
|
+
const sleepMs = Math.min(1_000, Math.max(0, remainingIdleMs));
|
|
5892
|
+
if (sleepMs > 0) {
|
|
5893
|
+
await page.waitForTimeout(sleepMs);
|
|
3123
5894
|
}
|
|
3124
|
-
}
|
|
3125
|
-
if (state && typeof state === "object" && typeof state.responseForPrompt === "string" && state.responseForPrompt.length > 0) {
|
|
3126
|
-
return {
|
|
3127
|
-
confirmed: true,
|
|
3128
|
-
response: state.responseForPrompt,
|
|
3129
|
-
};
|
|
3130
5895
|
}
|
|
3131
5896
|
return { confirmed: false };
|
|
3132
5897
|
}
|
|
@@ -3147,7 +5912,11 @@ async function readLatestGrokResponse(page) {
|
|
|
3147
5912
|
lower === "thinking" ||
|
|
3148
5913
|
lower === "expert" ||
|
|
3149
5914
|
lower.startsWith("grok") ||
|
|
3150
|
-
lower.includes("explore ")
|
|
5915
|
+
lower.includes("explore ") ||
|
|
5916
|
+
lower.includes("discuss ") ||
|
|
5917
|
+
lower.includes("create images") ||
|
|
5918
|
+
lower.includes("edit image") ||
|
|
5919
|
+
lower.includes("latest news"));
|
|
3151
5920
|
};
|
|
3152
5921
|
const scope = document.querySelector("div[aria-label='Grok']") ??
|
|
3153
5922
|
document.querySelector("main");
|
|
@@ -3158,20 +5927,13 @@ async function readLatestGrokResponse(page) {
|
|
|
3158
5927
|
.split(/\n+/)
|
|
3159
5928
|
.map((line) => normalize(line))
|
|
3160
5929
|
.filter((line) => line.length > 0);
|
|
3161
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
3162
|
-
const candidate = lines[index];
|
|
3163
|
-
if (!candidate || isIgnoredResponse(candidate)) {
|
|
3164
|
-
continue;
|
|
3165
|
-
}
|
|
3166
|
-
return { latestResponse: candidate };
|
|
3167
|
-
}
|
|
3168
5930
|
const entries = [];
|
|
3169
5931
|
for (const node of Array.from(scope.querySelectorAll("div, span, p"))) {
|
|
3170
5932
|
if (node.closest("button, a, textarea, nav")) {
|
|
3171
5933
|
continue;
|
|
3172
5934
|
}
|
|
3173
5935
|
const text = normalize(node.innerText || node.textContent || "");
|
|
3174
|
-
if (!text
|
|
5936
|
+
if (!text) {
|
|
3175
5937
|
continue;
|
|
3176
5938
|
}
|
|
3177
5939
|
const childWithSameText = Array.from(node.children).some((child) => {
|
|
@@ -3188,15 +5950,18 @@ async function readLatestGrokResponse(page) {
|
|
|
3188
5950
|
}
|
|
3189
5951
|
}
|
|
3190
5952
|
let latestResponse;
|
|
3191
|
-
|
|
3192
|
-
|
|
5953
|
+
const responseCandidates = entries.length > 0 ? entries : lines;
|
|
5954
|
+
for (let index = responseCandidates.length - 1; index >= 0; index -= 1) {
|
|
5955
|
+
const candidate = responseCandidates[index];
|
|
3193
5956
|
if (!candidate || isIgnoredResponse(candidate)) {
|
|
3194
5957
|
continue;
|
|
3195
5958
|
}
|
|
3196
5959
|
latestResponse = candidate;
|
|
3197
5960
|
break;
|
|
3198
5961
|
}
|
|
3199
|
-
return {
|
|
5962
|
+
return {
|
|
5963
|
+
latestResponse,
|
|
5964
|
+
};
|
|
3200
5965
|
}, { op: "grok_extract_state" });
|
|
3201
5966
|
if (state && typeof state === "object" && typeof state.latestResponse === "string") {
|
|
3202
5967
|
return state.latestResponse;
|
|
@@ -3259,8 +6024,6 @@ async function askGrok(page, prompt, timeoutMs, attachments, conversationId) {
|
|
|
3259
6024
|
}
|
|
3260
6025
|
return output;
|
|
3261
6026
|
}
|
|
3262
|
-
await grokPage.goto("https://x.com/i/grok", { waitUntil: "domcontentloaded", timeout: 60_000 }).catch(() => { });
|
|
3263
|
-
await waitForGrokSurface(grokPage);
|
|
3264
6027
|
const previousResponse = await readLatestGrokResponse(grokPage);
|
|
3265
6028
|
logGrokPhase("previous_response_read", {
|
|
3266
6029
|
hasPreviousResponse: previousResponse !== undefined,
|
|
@@ -3386,6 +6149,7 @@ async function requireAuthenticated(page) {
|
|
|
3386
6149
|
export function createXAdapter(options) {
|
|
3387
6150
|
const composeConfirmTimeoutMs = options?.composeConfirmTimeoutMs ?? DEFAULT_COMPOSE_CONFIRM_TIMEOUT_MS;
|
|
3388
6151
|
const grokResponseTimeoutMs = options?.grokResponseTimeoutMs ?? DEFAULT_GROK_RESPONSE_TIMEOUT_MS;
|
|
6152
|
+
const articlePublishTimeoutMs = options?.articlePublishTimeoutMs ?? DEFAULT_ARTICLE_PUBLISH_TIMEOUT_MS;
|
|
3389
6153
|
const maxPostLength = options?.maxPostLength ?? DEFAULT_MAX_POST_LENGTH;
|
|
3390
6154
|
return {
|
|
3391
6155
|
name: "adapter-x",
|
|
@@ -3642,6 +6406,20 @@ export function createXAdapter(options) {
|
|
|
3642
6406
|
const dryRun = args.dryRun === true;
|
|
3643
6407
|
return await replyToTweet(page, targetUrl, text, dryRun, composeConfirmTimeoutMs);
|
|
3644
6408
|
}
|
|
6409
|
+
if (name === "tweet.delete") {
|
|
6410
|
+
const authCheck = await requireAuthenticated(page);
|
|
6411
|
+
if (!authCheck.ok) {
|
|
6412
|
+
return authCheck.result;
|
|
6413
|
+
}
|
|
6414
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
6415
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6416
|
+
if (!url && !id) {
|
|
6417
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6418
|
+
}
|
|
6419
|
+
const targetUrl = url || `https://x.com/i/web/status/${encodeURIComponent(id)}`;
|
|
6420
|
+
const dryRun = args.dryRun === true;
|
|
6421
|
+
return await deleteTweetDetail(page, targetUrl, dryRun);
|
|
6422
|
+
}
|
|
3645
6423
|
if (name === "grok.chat") {
|
|
3646
6424
|
const authCheck = await requireAuthenticated(page);
|
|
3647
6425
|
if (!authCheck.ok) {
|
|
@@ -3658,10 +6436,209 @@ export function createXAdapter(options) {
|
|
|
3658
6436
|
const conversationId = typeof args.conversationId === "string" ? args.conversationId.trim() : "";
|
|
3659
6437
|
return await askGrok(page, prompt, grokResponseTimeoutMs, resolvedAttachments.attachments, conversationId || undefined);
|
|
3660
6438
|
}
|
|
6439
|
+
if (name === "article.get") {
|
|
6440
|
+
const authCheck = await requireAuthenticated(page);
|
|
6441
|
+
if (!authCheck.ok) {
|
|
6442
|
+
return authCheck.result;
|
|
6443
|
+
}
|
|
6444
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
6445
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6446
|
+
const authorHandle = typeof args.authorHandle === "string" ? args.authorHandle.trim() : "";
|
|
6447
|
+
const targetUrl = normalizeArticleUrl(url, id);
|
|
6448
|
+
if (!targetUrl) {
|
|
6449
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6450
|
+
}
|
|
6451
|
+
return await readArticleByUrl(page, targetUrl, authorHandle || undefined);
|
|
6452
|
+
}
|
|
6453
|
+
if (name === "article.listDrafts") {
|
|
6454
|
+
const authCheck = await requireAuthenticated(page);
|
|
6455
|
+
if (!authCheck.ok) {
|
|
6456
|
+
return authCheck.result;
|
|
6457
|
+
}
|
|
6458
|
+
return await listArticleDrafts(page);
|
|
6459
|
+
}
|
|
6460
|
+
if (name === "article.getDraft") {
|
|
6461
|
+
const authCheck = await requireAuthenticated(page);
|
|
6462
|
+
if (!authCheck.ok) {
|
|
6463
|
+
return authCheck.result;
|
|
6464
|
+
}
|
|
6465
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
6466
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6467
|
+
if (!id && !url) {
|
|
6468
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6469
|
+
}
|
|
6470
|
+
let articleId = id || undefined;
|
|
6471
|
+
if (!articleId && url) {
|
|
6472
|
+
articleId = parseArticleIdFromUrl(url);
|
|
6473
|
+
if (!articleId) {
|
|
6474
|
+
return errorResult("VALIDATION_ERROR", "could not parse article id from url");
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
if (!articleId) {
|
|
6478
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6479
|
+
}
|
|
6480
|
+
return await getArticleDraft(page, buildArticleEditUrl(articleId));
|
|
6481
|
+
}
|
|
6482
|
+
if (name === "article.draftMarkdown") {
|
|
6483
|
+
const authCheck = await requireAuthenticated(page);
|
|
6484
|
+
if (!authCheck.ok) {
|
|
6485
|
+
return authCheck.result;
|
|
6486
|
+
}
|
|
6487
|
+
const markdownPath = typeof args.markdownPath === "string" ? args.markdownPath.trim() : "";
|
|
6488
|
+
if (!markdownPath) {
|
|
6489
|
+
return errorResult("VALIDATION_ERROR", "markdownPath is required");
|
|
6490
|
+
}
|
|
6491
|
+
const explicitTitle = typeof args.title === "string" ? args.title.trim() : "";
|
|
6492
|
+
const coverImagePath = typeof args.coverImagePath === "string" ? args.coverImagePath.trim() : "";
|
|
6493
|
+
return await draftArticleMarkdown(page, markdownPath, explicitTitle || undefined, coverImagePath || undefined);
|
|
6494
|
+
}
|
|
6495
|
+
if (name === "article.upsertDraftMarkdown") {
|
|
6496
|
+
const authCheck = await requireAuthenticated(page);
|
|
6497
|
+
if (!authCheck.ok) {
|
|
6498
|
+
return authCheck.result;
|
|
6499
|
+
}
|
|
6500
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
6501
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6502
|
+
const markdownPath = typeof args.markdownPath === "string" ? args.markdownPath.trim() : "";
|
|
6503
|
+
if (!markdownPath) {
|
|
6504
|
+
return errorResult("VALIDATION_ERROR", "markdownPath is required");
|
|
6505
|
+
}
|
|
6506
|
+
const explicitTitle = typeof args.title === "string" ? args.title.trim() : "";
|
|
6507
|
+
const coverImagePath = typeof args.coverImagePath === "string" ? args.coverImagePath.trim() : "";
|
|
6508
|
+
let articleId = id || undefined;
|
|
6509
|
+
if (!articleId && url) {
|
|
6510
|
+
articleId = parseArticleIdFromUrl(url);
|
|
6511
|
+
if (!articleId) {
|
|
6512
|
+
return errorResult("VALIDATION_ERROR", "url is invalid or unsupported");
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
6515
|
+
const targetUrl = articleId ? buildArticleEditUrl(articleId) : undefined;
|
|
6516
|
+
return await upsertArticleDraftMarkdown(page, targetUrl, markdownPath, explicitTitle || undefined, coverImagePath || undefined);
|
|
6517
|
+
}
|
|
6518
|
+
if (name === "article.publishMarkdown") {
|
|
6519
|
+
const authCheck = await requireAuthenticated(page);
|
|
6520
|
+
if (!authCheck.ok) {
|
|
6521
|
+
return authCheck.result;
|
|
6522
|
+
}
|
|
6523
|
+
const markdownPath = typeof args.markdownPath === "string" ? args.markdownPath.trim() : "";
|
|
6524
|
+
if (!markdownPath) {
|
|
6525
|
+
return errorResult("VALIDATION_ERROR", "markdownPath is required");
|
|
6526
|
+
}
|
|
6527
|
+
const explicitTitle = typeof args.title === "string" ? args.title.trim() : "";
|
|
6528
|
+
const coverImagePath = typeof args.coverImagePath === "string" ? args.coverImagePath.trim() : "";
|
|
6529
|
+
const dryRun = args.dryRun === true;
|
|
6530
|
+
return await publishArticleMarkdown(page, markdownPath, explicitTitle || undefined, coverImagePath || undefined, dryRun, articlePublishTimeoutMs);
|
|
6531
|
+
}
|
|
6532
|
+
if (name === "article.publish") {
|
|
6533
|
+
const authCheck = await requireAuthenticated(page);
|
|
6534
|
+
if (!authCheck.ok) {
|
|
6535
|
+
return authCheck.result;
|
|
6536
|
+
}
|
|
6537
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
6538
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6539
|
+
const articleId = id || (url ? parseArticleIdFromUrl(url) : undefined);
|
|
6540
|
+
if (!articleId && !url) {
|
|
6541
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6542
|
+
}
|
|
6543
|
+
const targetUrl = url && url.includes("/compose/articles/edit/")
|
|
6544
|
+
? url
|
|
6545
|
+
: articleId
|
|
6546
|
+
? `https://x.com/compose/articles/edit/${articleId}`
|
|
6547
|
+
: url;
|
|
6548
|
+
if (!targetUrl) {
|
|
6549
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6550
|
+
}
|
|
6551
|
+
return await publishExistingArticle(page, targetUrl, articlePublishTimeoutMs);
|
|
6552
|
+
}
|
|
6553
|
+
if (name === "article.setCoverImage") {
|
|
6554
|
+
const authCheck = await requireAuthenticated(page);
|
|
6555
|
+
if (!authCheck.ok) {
|
|
6556
|
+
return authCheck.result;
|
|
6557
|
+
}
|
|
6558
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
6559
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6560
|
+
const articleId = id || (url ? parseArticleIdFromUrl(url) : undefined);
|
|
6561
|
+
if (!articleId && !url) {
|
|
6562
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6563
|
+
}
|
|
6564
|
+
const coverImagePath = typeof args.coverImagePath === "string" ? args.coverImagePath.trim() : "";
|
|
6565
|
+
if (!coverImagePath) {
|
|
6566
|
+
return errorResult("VALIDATION_ERROR", "coverImagePath is required");
|
|
6567
|
+
}
|
|
6568
|
+
const targetUrl = url && url.includes("/compose/articles/edit/")
|
|
6569
|
+
? url
|
|
6570
|
+
: articleId
|
|
6571
|
+
? `https://x.com/compose/articles/edit/${articleId}`
|
|
6572
|
+
: url;
|
|
6573
|
+
if (!targetUrl) {
|
|
6574
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6575
|
+
}
|
|
6576
|
+
return await setArticleCoverImage(page, targetUrl, coverImagePath);
|
|
6577
|
+
}
|
|
6578
|
+
if (name === "article.updateMarkdown") {
|
|
6579
|
+
const authCheck = await requireAuthenticated(page);
|
|
6580
|
+
if (!authCheck.ok) {
|
|
6581
|
+
return authCheck.result;
|
|
6582
|
+
}
|
|
6583
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
6584
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6585
|
+
const articleId = id || (url ? parseArticleIdFromUrl(url) : undefined);
|
|
6586
|
+
if (!articleId && !url) {
|
|
6587
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6588
|
+
}
|
|
6589
|
+
const markdownPath = typeof args.markdownPath === "string" ? args.markdownPath.trim() : "";
|
|
6590
|
+
if (!markdownPath) {
|
|
6591
|
+
return errorResult("VALIDATION_ERROR", "markdownPath is required");
|
|
6592
|
+
}
|
|
6593
|
+
const explicitTitle = typeof args.title === "string" ? args.title.trim() : "";
|
|
6594
|
+
const targetUrl = url && url.includes("/compose/articles/edit/")
|
|
6595
|
+
? url
|
|
6596
|
+
: articleId
|
|
6597
|
+
? `https://x.com/compose/articles/edit/${articleId}`
|
|
6598
|
+
: url;
|
|
6599
|
+
if (!targetUrl) {
|
|
6600
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6601
|
+
}
|
|
6602
|
+
return await updateArticleMarkdown(page, targetUrl, markdownPath, explicitTitle || undefined);
|
|
6603
|
+
}
|
|
6604
|
+
if (name === "article.delete") {
|
|
6605
|
+
const authCheck = await requireAuthenticated(page);
|
|
6606
|
+
if (!authCheck.ok) {
|
|
6607
|
+
return authCheck.result;
|
|
6608
|
+
}
|
|
6609
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
6610
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6611
|
+
const dryRun = args.dryRun === true;
|
|
6612
|
+
const articleId = id || (url ? parseArticleIdFromUrl(url) : undefined);
|
|
6613
|
+
if (!articleId && !url) {
|
|
6614
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6615
|
+
}
|
|
6616
|
+
const targetUrl = url && url.includes("/compose/articles/edit/")
|
|
6617
|
+
? url
|
|
6618
|
+
: articleId
|
|
6619
|
+
? `https://x.com/compose/articles/edit/${articleId}`
|
|
6620
|
+
: url;
|
|
6621
|
+
if (!targetUrl) {
|
|
6622
|
+
return errorResult("VALIDATION_ERROR", "url or id is required");
|
|
6623
|
+
}
|
|
6624
|
+
const cachedPage = articleId ? getCachedArticleDraftPage(page, articleId) : undefined;
|
|
6625
|
+
if (cachedPage) {
|
|
6626
|
+
const result = await deleteArticleEditor(cachedPage, dryRun);
|
|
6627
|
+
if (!dryRun && articleId && (!result || typeof result !== "object" || Array.isArray(result) || !("error" in result))) {
|
|
6628
|
+
await removeCachedArticleDraftPage(page, articleId);
|
|
6629
|
+
}
|
|
6630
|
+
return result;
|
|
6631
|
+
}
|
|
6632
|
+
return await withEphemeralPage(page, targetUrl, async (articlePage) => {
|
|
6633
|
+
await waitForArticleEditorSurface(articlePage);
|
|
6634
|
+
return await deleteArticleEditor(articlePage, dryRun);
|
|
6635
|
+
});
|
|
6636
|
+
}
|
|
3661
6637
|
return errorResult("TOOL_NOT_FOUND", `unknown tool: ${name}`);
|
|
3662
6638
|
},
|
|
3663
6639
|
stop: async ({ page }) => {
|
|
3664
6640
|
await closeCachedReadPages(page);
|
|
6641
|
+
await closeCachedArticleDraftPages(page);
|
|
3665
6642
|
},
|
|
3666
6643
|
};
|
|
3667
6644
|
}
|