@webmcp-bridge/adapter-x 0.5.1 → 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.map +1 -1
- package/dist/adapter.js +699 -34
- package/dist/adapter.js.map +1 -1
- package/package.json +4 -4
package/dist/adapter.js
CHANGED
|
@@ -480,6 +480,43 @@ const TOOL_DEFINITIONS = [
|
|
|
480
480
|
readOnlyHint: true,
|
|
481
481
|
},
|
|
482
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
|
+
},
|
|
483
520
|
{
|
|
484
521
|
name: "article.draftMarkdown",
|
|
485
522
|
description: "Create one X article draft from a local markdown file",
|
|
@@ -509,6 +546,45 @@ const TOOL_DEFINITIONS = [
|
|
|
509
546
|
additionalProperties: false,
|
|
510
547
|
},
|
|
511
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
|
+
},
|
|
512
588
|
{
|
|
513
589
|
name: "article.publishMarkdown",
|
|
514
590
|
description: "Publish one X article from a local markdown file",
|
|
@@ -1001,6 +1077,215 @@ function extractArticleTitle(markdown, markdownPath, explicitTitle) {
|
|
|
1001
1077
|
}
|
|
1002
1078
|
return basename(markdownPath, extname(markdownPath)).trim() || "Untitled";
|
|
1003
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
|
+
}
|
|
1004
1289
|
function prepareArticleMarkdown(markdown, markdownPath) {
|
|
1005
1290
|
const inlineImages = [];
|
|
1006
1291
|
let nextIndex = 1;
|
|
@@ -1026,6 +1311,7 @@ function prepareArticleMarkdown(markdown, markdownPath) {
|
|
|
1026
1311
|
});
|
|
1027
1312
|
return {
|
|
1028
1313
|
markdown: prepared,
|
|
1314
|
+
html: convertMarkdownToHtml(prepared),
|
|
1029
1315
|
inlineImages,
|
|
1030
1316
|
};
|
|
1031
1317
|
}
|
|
@@ -3255,37 +3541,53 @@ async function setArticleTitle(page, title) {
|
|
|
3255
3541
|
.then(() => true)
|
|
3256
3542
|
.catch(() => false);
|
|
3257
3543
|
}
|
|
3258
|
-
async function pasteArticleMarkdown(page, markdown) {
|
|
3259
|
-
let
|
|
3544
|
+
async function pasteArticleMarkdown(page, markdown, html) {
|
|
3545
|
+
let pasted = false;
|
|
3260
3546
|
if (typeof page.locator === "function") {
|
|
3261
3547
|
const composerLocator = page.locator("[data-testid='composer'][role='textbox']").first();
|
|
3262
|
-
|
|
3263
|
-
if (
|
|
3264
|
-
const wroteClipboard = await page.evaluate(async ({
|
|
3548
|
+
const clicked = await composerLocator.click().then(() => true).catch(() => false);
|
|
3549
|
+
if (clicked) {
|
|
3550
|
+
const wroteClipboard = await page.evaluate(async ({ plainText, htmlText }) => {
|
|
3265
3551
|
try {
|
|
3266
|
-
|
|
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
|
+
}
|
|
3267
3568
|
return true;
|
|
3268
3569
|
}
|
|
3269
3570
|
catch {
|
|
3270
3571
|
return false;
|
|
3271
3572
|
}
|
|
3272
|
-
}, {
|
|
3573
|
+
}, { plainText: markdown, htmlText: html }).catch(() => false);
|
|
3273
3574
|
if (wroteClipboard) {
|
|
3274
|
-
|
|
3575
|
+
const pasteShortcut = process.platform === "darwin" ? "Meta+V" : "Control+V";
|
|
3576
|
+
pasted = await page.keyboard.press(pasteShortcut).then(() => true).catch(() => false);
|
|
3275
3577
|
}
|
|
3276
|
-
if (!
|
|
3578
|
+
if (!pasted) {
|
|
3277
3579
|
const keyboard = page.keyboard;
|
|
3278
3580
|
if (typeof keyboard.insertText === "function") {
|
|
3279
|
-
|
|
3581
|
+
pasted = await keyboard.insertText(markdown).then(() => true).catch(() => false);
|
|
3280
3582
|
}
|
|
3281
3583
|
else if (typeof keyboard.type === "function") {
|
|
3282
|
-
|
|
3584
|
+
pasted = await keyboard.type(markdown).then(() => true).catch(() => false);
|
|
3283
3585
|
}
|
|
3284
3586
|
}
|
|
3285
3587
|
}
|
|
3286
3588
|
}
|
|
3287
|
-
if (!
|
|
3288
|
-
|
|
3589
|
+
if (!pasted) {
|
|
3590
|
+
pasted = await page.evaluate(({ op, markdownText, htmlText }) => {
|
|
3289
3591
|
if (op !== "article_paste_markdown") {
|
|
3290
3592
|
return false;
|
|
3291
3593
|
}
|
|
@@ -3297,6 +3599,9 @@ async function pasteArticleMarkdown(page, markdown) {
|
|
|
3297
3599
|
const data = new DataTransfer();
|
|
3298
3600
|
data.setData("text/plain", markdownText);
|
|
3299
3601
|
data.setData("text/markdown", markdownText);
|
|
3602
|
+
if (typeof htmlText === "string" && htmlText.trim().length > 0) {
|
|
3603
|
+
data.setData("text/html", htmlText);
|
|
3604
|
+
}
|
|
3300
3605
|
const event = new ClipboardEvent("paste", {
|
|
3301
3606
|
bubbles: true,
|
|
3302
3607
|
cancelable: true,
|
|
@@ -3304,16 +3609,12 @@ async function pasteArticleMarkdown(page, markdown) {
|
|
|
3304
3609
|
});
|
|
3305
3610
|
composer.dispatchEvent(event);
|
|
3306
3611
|
return true;
|
|
3307
|
-
}, { op: "article_paste_markdown", markdownText: markdown }).catch(() => false);
|
|
3612
|
+
}, { op: "article_paste_markdown", markdownText: markdown, htmlText: html }).catch(() => false);
|
|
3308
3613
|
}
|
|
3309
|
-
if (!
|
|
3614
|
+
if (!pasted) {
|
|
3310
3615
|
return false;
|
|
3311
3616
|
}
|
|
3312
|
-
const requiredSnippets = markdown
|
|
3313
|
-
.split(/\n+/)
|
|
3314
|
-
.map((line) => line.trim())
|
|
3315
|
-
.filter((line) => line.length > 0)
|
|
3316
|
-
.slice(0, 3);
|
|
3617
|
+
const requiredSnippets = extractArticleConfirmationSnippets(markdown);
|
|
3317
3618
|
if (requiredSnippets.length === 0) {
|
|
3318
3619
|
return true;
|
|
3319
3620
|
}
|
|
@@ -3422,6 +3723,29 @@ async function uploadArticleFile(page, filePath) {
|
|
|
3422
3723
|
return false;
|
|
3423
3724
|
}
|
|
3424
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
|
+
}
|
|
3425
3749
|
async function placeArticleCursorAtMarker(page, marker) {
|
|
3426
3750
|
return ((await page.evaluate(({ op, markerText }) => {
|
|
3427
3751
|
if (op !== "article_place_marker") {
|
|
@@ -3519,9 +3843,24 @@ function parseArticleIdFromUrl(url) {
|
|
|
3519
3843
|
if (!ALLOWED_X_HOSTS.has(parsed.hostname.toLowerCase())) {
|
|
3520
3844
|
return undefined;
|
|
3521
3845
|
}
|
|
3522
|
-
const match = parsed.pathname.match(/^\/(?:compose\/articles\/edit|i\/article|i\/articles|[^/]+\/article|articles)\/(\d+)(?:\/|$)/);
|
|
3846
|
+
const match = parsed.pathname.match(/^\/(?:compose\/articles\/edit|i\/article|i\/articles|[^/]+\/article|articles)\/(\d+)(?:\/preview)?(?:\/|$)/);
|
|
3523
3847
|
return match?.[1];
|
|
3524
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
|
+
}
|
|
3525
3864
|
async function waitForCapturedOperation(page, op, timeoutMs = 10_000) {
|
|
3526
3865
|
await page.waitForFunction(({ targetOp }) => {
|
|
3527
3866
|
const globalAny = window;
|
|
@@ -3566,7 +3905,9 @@ async function readArticleFromEditorPage(page, articleId, sessionScoped) {
|
|
|
3566
3905
|
normalize(document.querySelector("h1")?.innerText || "");
|
|
3567
3906
|
const composer = document.querySelector("[data-testid='composer'][role='textbox']");
|
|
3568
3907
|
const rawText = (composer?.innerText || composer?.textContent || "").trim();
|
|
3569
|
-
const
|
|
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']"))
|
|
3570
3911
|
.map((img) => ({
|
|
3571
3912
|
url: img.currentSrc || img.src,
|
|
3572
3913
|
alt: normalize(img.alt || ""),
|
|
@@ -3574,7 +3915,8 @@ async function readArticleFromEditorPage(page, articleId, sessionScoped) {
|
|
|
3574
3915
|
height: img.naturalHeight || 0,
|
|
3575
3916
|
}))
|
|
3576
3917
|
.filter((item) => /^https?:\/\//.test(item.url))
|
|
3577
|
-
.filter((item) => item.width > 64 || item.height > 64)
|
|
3918
|
+
.filter((item) => item.width > 64 || item.height > 64)
|
|
3919
|
+
.filter((item) => !/\/profile_images\/|\/emoji\/|\/hashflags\//.test(item.url));
|
|
3578
3920
|
const deduped = new Map();
|
|
3579
3921
|
for (const image of images) {
|
|
3580
3922
|
if (!deduped.has(image.url)) {
|
|
@@ -3609,19 +3951,270 @@ async function readArticleFromEditorPage(page, articleId, sessionScoped) {
|
|
|
3609
3951
|
editUrl: typeof article.editUrl === "string" ? article.editUrl : page.url(),
|
|
3610
3952
|
images: inlineImages,
|
|
3611
3953
|
source: "editor",
|
|
3954
|
+
published: false,
|
|
3612
3955
|
},
|
|
3613
3956
|
};
|
|
3957
|
+
if (articleId) {
|
|
3958
|
+
output.article.previewUrl = buildArticlePreviewUrl(articleId);
|
|
3959
|
+
}
|
|
3614
3960
|
if (coverImage && typeof coverImage === "object" && coverImage !== null && "url" in coverImage) {
|
|
3615
3961
|
output.article.coverImageUrl = coverImage.url;
|
|
3962
|
+
output.article.hasCoverImage = true;
|
|
3963
|
+
}
|
|
3964
|
+
else {
|
|
3965
|
+
output.article.hasCoverImage = false;
|
|
3616
3966
|
}
|
|
3617
3967
|
if (articleId) {
|
|
3618
3968
|
output.article.sessionScoped = sessionScoped === true;
|
|
3619
3969
|
}
|
|
3620
|
-
if (sessionScoped === true) {
|
|
3621
|
-
output.article.published = false;
|
|
3622
|
-
}
|
|
3623
3970
|
return output;
|
|
3624
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
|
+
}
|
|
3625
4218
|
function parseArticleReadErrorCode(value) {
|
|
3626
4219
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3627
4220
|
return undefined;
|
|
@@ -4327,7 +4920,7 @@ async function readArticleByUrl(page, targetUrl, authorHandle) {
|
|
|
4327
4920
|
}
|
|
4328
4921
|
}
|
|
4329
4922
|
}
|
|
4330
|
-
const useEditor = targetUrl.includes("/compose/articles/edit/");
|
|
4923
|
+
const useEditor = targetUrl.includes("/compose/articles/edit/") || isArticlePreviewUrl(targetUrl);
|
|
4331
4924
|
if (articleId) {
|
|
4332
4925
|
const cachedPage = getCachedArticleDraftPage(page, articleId);
|
|
4333
4926
|
if (cachedPage) {
|
|
@@ -4337,7 +4930,7 @@ async function readArticleByUrl(page, targetUrl, authorHandle) {
|
|
|
4337
4930
|
}
|
|
4338
4931
|
}
|
|
4339
4932
|
if (useEditor) {
|
|
4340
|
-
return await withEphemeralPage(page, targetUrl, async (articlePage) => {
|
|
4933
|
+
return await withEphemeralPage(page, articleId ? buildArticleEditUrl(articleId) : targetUrl, async (articlePage) => {
|
|
4341
4934
|
await waitForArticleEditorSurface(articlePage);
|
|
4342
4935
|
await ensureArticleDraftLoaded(articlePage, articleId);
|
|
4343
4936
|
return await readArticleFromEditorPage(articlePage, articleId, false);
|
|
@@ -4614,8 +5207,9 @@ async function draftArticleMarkdown(page, markdownPath, explicitTitle, coverImag
|
|
|
4614
5207
|
if (markdown === undefined) {
|
|
4615
5208
|
return errorResult("VALIDATION_ERROR", `markdownPath was not found: ${markdownPath}`);
|
|
4616
5209
|
}
|
|
4617
|
-
const
|
|
4618
|
-
const
|
|
5210
|
+
const normalized = normalizeArticleMarkdown(markdown, resolvedMarkdown.attachment.path, explicitTitle);
|
|
5211
|
+
const title = normalized.title;
|
|
5212
|
+
const draftAssets = prepareArticleMarkdown(normalized.bodyMarkdown, resolvedMarkdown.attachment.path);
|
|
4619
5213
|
const resolvedInlineImages = [];
|
|
4620
5214
|
for (const image of draftAssets.inlineImages) {
|
|
4621
5215
|
const resolved = await resolveArticleAttachment(image.path, image.marker);
|
|
@@ -4658,8 +5252,12 @@ async function draftArticleMarkdown(page, markdownPath, explicitTitle, coverImag
|
|
|
4658
5252
|
if (!coverUploaded) {
|
|
4659
5253
|
return errorResult("UPSTREAM_CHANGED", "article cover upload failed");
|
|
4660
5254
|
}
|
|
5255
|
+
const coverApplied = await waitForArticleCoverApplied(articlePage);
|
|
5256
|
+
if (!coverApplied) {
|
|
5257
|
+
return errorResult("ACTION_UNCONFIRMED", "article cover upload was not confirmed");
|
|
5258
|
+
}
|
|
4661
5259
|
}
|
|
4662
|
-
const pasted = await pasteArticleMarkdown(articlePage, draftAssets.markdown);
|
|
5260
|
+
const pasted = await pasteArticleMarkdown(articlePage, draftAssets.markdown, draftAssets.html);
|
|
4663
5261
|
if (!pasted) {
|
|
4664
5262
|
return errorResult("UPSTREAM_CHANGED", "article markdown paste failed");
|
|
4665
5263
|
}
|
|
@@ -4680,6 +5278,8 @@ async function draftArticleMarkdown(page, markdownPath, explicitTitle, coverImag
|
|
|
4680
5278
|
};
|
|
4681
5279
|
if (articleId) {
|
|
4682
5280
|
output.articleId = articleId;
|
|
5281
|
+
output.draftId = articleId;
|
|
5282
|
+
output.previewUrl = buildArticlePreviewUrl(articleId);
|
|
4683
5283
|
const persisted = await waitForArticleDraftPersisted(articlePage, articleId, title);
|
|
4684
5284
|
output.persisted = persisted;
|
|
4685
5285
|
output.sessionScoped = !persisted;
|
|
@@ -4808,6 +5408,10 @@ async function setArticleCoverImage(page, targetUrl, coverImagePath) {
|
|
|
4808
5408
|
if (!coverUploaded) {
|
|
4809
5409
|
return errorResult("UPSTREAM_CHANGED", "article cover upload failed");
|
|
4810
5410
|
}
|
|
5411
|
+
const coverApplied = await waitForArticleCoverApplied(articlePage);
|
|
5412
|
+
if (!coverApplied) {
|
|
5413
|
+
return errorResult("ACTION_UNCONFIRMED", "article cover upload was not confirmed");
|
|
5414
|
+
}
|
|
4811
5415
|
const output = {
|
|
4812
5416
|
ok: true,
|
|
4813
5417
|
editUrl: articlePage.url(),
|
|
@@ -4829,8 +5433,9 @@ async function updateArticleMarkdown(page, targetUrl, markdownPath, explicitTitl
|
|
|
4829
5433
|
if (markdown === undefined) {
|
|
4830
5434
|
return errorResult("VALIDATION_ERROR", `markdownPath was not found: ${markdownPath}`);
|
|
4831
5435
|
}
|
|
4832
|
-
const
|
|
4833
|
-
const
|
|
5436
|
+
const normalized = normalizeArticleMarkdown(markdown, resolvedMarkdown.attachment.path, explicitTitle);
|
|
5437
|
+
const title = normalized.title;
|
|
5438
|
+
const draftAssets = prepareArticleMarkdown(normalized.bodyMarkdown, resolvedMarkdown.attachment.path);
|
|
4834
5439
|
const resolvedInlineImages = [];
|
|
4835
5440
|
for (const image of draftAssets.inlineImages) {
|
|
4836
5441
|
const resolved = await resolveArticleAttachment(image.path, image.marker);
|
|
@@ -4854,7 +5459,7 @@ async function updateArticleMarkdown(page, targetUrl, markdownPath, explicitTitl
|
|
|
4854
5459
|
if (!cleared) {
|
|
4855
5460
|
return errorResult("UPSTREAM_CHANGED", "article body controls not found");
|
|
4856
5461
|
}
|
|
4857
|
-
const pasted = await pasteArticleMarkdown(articlePage, draftAssets.markdown);
|
|
5462
|
+
const pasted = await pasteArticleMarkdown(articlePage, draftAssets.markdown, draftAssets.html);
|
|
4858
5463
|
if (!pasted) {
|
|
4859
5464
|
return errorResult("UPSTREAM_CHANGED", "article markdown paste failed");
|
|
4860
5465
|
}
|
|
@@ -4872,6 +5477,8 @@ async function updateArticleMarkdown(page, targetUrl, markdownPath, explicitTitl
|
|
|
4872
5477
|
};
|
|
4873
5478
|
if (articleId) {
|
|
4874
5479
|
output.articleId = articleId;
|
|
5480
|
+
output.draftId = articleId;
|
|
5481
|
+
output.previewUrl = buildArticlePreviewUrl(articleId);
|
|
4875
5482
|
const persisted = await waitForArticleDraftPersisted(articlePage, articleId, title);
|
|
4876
5483
|
output.persisted = persisted;
|
|
4877
5484
|
output.sessionScoped = sessionScoped === true || !persisted;
|
|
@@ -4879,6 +5486,12 @@ async function updateArticleMarkdown(page, targetUrl, markdownPath, explicitTitl
|
|
|
4879
5486
|
return output;
|
|
4880
5487
|
});
|
|
4881
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
|
+
}
|
|
4882
5495
|
async function waitForGrokSurface(page) {
|
|
4883
5496
|
await page
|
|
4884
5497
|
.waitForFunction(() => {
|
|
@@ -5837,6 +6450,35 @@ export function createXAdapter(options) {
|
|
|
5837
6450
|
}
|
|
5838
6451
|
return await readArticleByUrl(page, targetUrl, authorHandle || undefined);
|
|
5839
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
|
+
}
|
|
5840
6482
|
if (name === "article.draftMarkdown") {
|
|
5841
6483
|
const authCheck = await requireAuthenticated(page);
|
|
5842
6484
|
if (!authCheck.ok) {
|
|
@@ -5850,6 +6492,29 @@ export function createXAdapter(options) {
|
|
|
5850
6492
|
const coverImagePath = typeof args.coverImagePath === "string" ? args.coverImagePath.trim() : "";
|
|
5851
6493
|
return await draftArticleMarkdown(page, markdownPath, explicitTitle || undefined, coverImagePath || undefined);
|
|
5852
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
|
+
}
|
|
5853
6518
|
if (name === "article.publishMarkdown") {
|
|
5854
6519
|
const authCheck = await requireAuthenticated(page);
|
|
5855
6520
|
if (!authCheck.ok) {
|