@qcplay/cli 1.0.21 → 1.0.22
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/bin/qcplay.js +4 -0
- package/lib/lark-article.js +2 -1
- package/lib/lark-cli-command.js +16 -0
- package/lib/platform-publish.js +270 -41
- package/package.json +2 -1
package/bin/qcplay.js
CHANGED
|
@@ -3111,6 +3111,10 @@ async function runWithErrorBanner(title, action) {
|
|
|
3111
3111
|
try {
|
|
3112
3112
|
await action();
|
|
3113
3113
|
} catch (err) {
|
|
3114
|
+
if (err.authorization?.action === "platform_authorization_required") {
|
|
3115
|
+
console.log(JSON.stringify(err.authorization));
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3114
3118
|
console.error("");
|
|
3115
3119
|
console.error(chalk.red(`${title}:`));
|
|
3116
3120
|
console.error(err.message || err);
|
package/lib/lark-article.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "child_process";
|
|
|
2
2
|
|
|
3
3
|
import * as cheerio from "cheerio";
|
|
4
4
|
|
|
5
|
+
import { larkCliExecutable } from "./lark-cli-command.js";
|
|
5
6
|
import { normalizeArticleColor, sanitizeArticleRichHtml } from "./wechat-article.js";
|
|
6
7
|
|
|
7
8
|
const LARK_DOCUMENT_PATH = /^\/(?:wiki|docx)\/([A-Za-z0-9_-]+)\/?$/;
|
|
@@ -59,7 +60,7 @@ function larkError(output, fallback) {
|
|
|
59
60
|
|
|
60
61
|
function runLarkJson(args) {
|
|
61
62
|
return new Promise((resolve, reject) => {
|
|
62
|
-
const executable =
|
|
63
|
+
const executable = larkCliExecutable();
|
|
63
64
|
const child = spawn(executable, args, {
|
|
64
65
|
windowsHide: true,
|
|
65
66
|
shell: process.platform === "win32",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export function larkCliExecutable() {
|
|
8
|
+
const configured = String(process.env.QCPLAY_LARK_CLI || "").trim();
|
|
9
|
+
if (configured) {
|
|
10
|
+
return configured;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const executableName = process.platform === "win32" ? "lark-cli.cmd" : "lark-cli";
|
|
14
|
+
const localExecutable = path.resolve(moduleDirectory, "..", "node_modules", ".bin", executableName);
|
|
15
|
+
return fs.existsSync(localExecutable) ? localExecutable : executableName;
|
|
16
|
+
}
|
package/lib/platform-publish.js
CHANGED
|
@@ -9,6 +9,7 @@ import readline from "readline";
|
|
|
9
9
|
|
|
10
10
|
import * as cheerio from "cheerio";
|
|
11
11
|
import { applyContentRules } from "./content-rules.js";
|
|
12
|
+
import { larkCliExecutable } from "./lark-cli-command.js";
|
|
12
13
|
import { stripWechatGuidanceHtml } from "./wechat-article.js";
|
|
13
14
|
|
|
14
15
|
const PLATFORM_CONFIG_URL = "https://t4blw8ys5w.feishu.cn/wiki/OqH7wkx9PiBaTYkKeWNc35SonGf";
|
|
@@ -484,7 +485,11 @@ export function normalizePlatformRows(rows) {
|
|
|
484
485
|
|
|
485
486
|
function runJson(command, args) {
|
|
486
487
|
return new Promise((resolve, reject) => {
|
|
487
|
-
const executable =
|
|
488
|
+
const executable = command === "lark-cli"
|
|
489
|
+
? larkCliExecutable()
|
|
490
|
+
: process.platform === "win32"
|
|
491
|
+
? `${command}.cmd`
|
|
492
|
+
: command;
|
|
488
493
|
const child = spawn(executable, args, {
|
|
489
494
|
windowsHide: true,
|
|
490
495
|
shell: process.platform === "win32",
|
|
@@ -498,19 +503,95 @@ function runJson(command, args) {
|
|
|
498
503
|
child.once("close", code => {
|
|
499
504
|
const output = Buffer.concat(stdout).toString("utf8").trim();
|
|
500
505
|
const diagnostics = Buffer.concat(stderr).toString("utf8").trim();
|
|
506
|
+
const parsed = parseJsonEnvelope(output) || parseJsonEnvelope(diagnostics);
|
|
501
507
|
if (code !== 0) {
|
|
502
|
-
|
|
508
|
+
const error = new Error(diagnostics || output || `${command} 执行失败 (${code})`);
|
|
509
|
+
error.exitCode = code;
|
|
510
|
+
error.payload = parsed;
|
|
511
|
+
reject(error);
|
|
503
512
|
return;
|
|
504
513
|
}
|
|
505
|
-
|
|
506
|
-
resolve(
|
|
507
|
-
}
|
|
514
|
+
if (parsed) {
|
|
515
|
+
resolve(parsed);
|
|
516
|
+
} else {
|
|
508
517
|
reject(new Error(`${command} 没有返回有效 JSON`));
|
|
509
518
|
}
|
|
510
519
|
});
|
|
511
520
|
});
|
|
512
521
|
}
|
|
513
522
|
|
|
523
|
+
function parseJsonEnvelope(value) {
|
|
524
|
+
if (!value) return null;
|
|
525
|
+
try {
|
|
526
|
+
return JSON.parse(value);
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function isAuthFailure(error) {
|
|
533
|
+
const payload = error?.payload;
|
|
534
|
+
const serialized = JSON.stringify(payload || {}) + "\n" + String(error?.message || "");
|
|
535
|
+
return /token_invalid|invalid access token|authentication/i.test(serialized);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function beginPlatformAuthorization(commandRunner = runJson) {
|
|
539
|
+
try {
|
|
540
|
+
const result = await commandRunner("lark-cli", ["auth", "login", "--recommend", "--no-wait", "--json"]);
|
|
541
|
+
const verificationUrl = normalizeText(result?.verification_url || result?.verification_uri_complete);
|
|
542
|
+
if (verificationUrl) {
|
|
543
|
+
return { verificationUrl, deviceCode: normalizeText(result?.device_code) };
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
platformManaged: result?.action === "platform_authorization_required",
|
|
547
|
+
message: normalizeText(result?.message)
|
|
548
|
+
};
|
|
549
|
+
} catch (error) {
|
|
550
|
+
const verificationUrl = normalizeText(error?.payload?.verification_url || error?.payload?.verification_uri_complete);
|
|
551
|
+
return {
|
|
552
|
+
verificationUrl,
|
|
553
|
+
deviceCode: normalizeText(error?.payload?.device_code),
|
|
554
|
+
platformManaged: error?.payload?.action === "platform_authorization_required",
|
|
555
|
+
message: normalizeText(error?.payload?.message || error?.message)
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function throwPlatformAuthorizationError(error, commandRunner = runJson) {
|
|
561
|
+
if (!isAuthFailure(error)) {
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
const authorization = await beginPlatformAuthorization(commandRunner);
|
|
565
|
+
if (authorization.verificationUrl) {
|
|
566
|
+
const authorizationError = new Error(
|
|
567
|
+
`飞书授权需要完成,请打开以下地址并完成授权:\n${authorization.verificationUrl}`
|
|
568
|
+
);
|
|
569
|
+
authorizationError.authorization = {
|
|
570
|
+
ok: true,
|
|
571
|
+
action: "platform_authorization_required",
|
|
572
|
+
provider: "lark",
|
|
573
|
+
verification_url: authorization.verificationUrl,
|
|
574
|
+
...(authorization.deviceCode ? { device_code: authorization.deviceCode } : {}),
|
|
575
|
+
message: "请完成飞书授权后重试原命令"
|
|
576
|
+
};
|
|
577
|
+
throw authorizationError;
|
|
578
|
+
}
|
|
579
|
+
if (authorization.platformManaged) {
|
|
580
|
+
const authorizationError = new Error(
|
|
581
|
+
"飞书访问令牌已失效,但当前 lark-cli 由平台托管凭证,无法生成本地授权地址。请在当前平台的飞书/Lark 连接器中重新授权后重试。"
|
|
582
|
+
);
|
|
583
|
+
authorizationError.authorization = {
|
|
584
|
+
ok: true,
|
|
585
|
+
action: "platform_authorization_required",
|
|
586
|
+
provider: "lark",
|
|
587
|
+
managed_by_platform: true,
|
|
588
|
+
message: "飞书访问令牌已失效,请在当前平台重新授权飞书/Lark 连接器后重试"
|
|
589
|
+
};
|
|
590
|
+
throw authorizationError;
|
|
591
|
+
}
|
|
592
|
+
throw new Error(`飞书访问令牌已失效,授权启动失败:${authorization.message || "未知错误"}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
514
595
|
function domesticPlatformRows() {
|
|
515
596
|
return normalizePlatformRows([...DOMESTIC_PLATFORM_ROWS, ...DOMESTIC_STANDARD_PLATFORM_ROWS]);
|
|
516
597
|
}
|
|
@@ -530,41 +611,52 @@ export async function loadPlatformEntries(options = {}) {
|
|
|
530
611
|
}
|
|
531
612
|
|
|
532
613
|
const domesticEntries = domesticPlatformRows();
|
|
614
|
+
const commandRunner = options.runJson || runJson;
|
|
533
615
|
const requestedRegion = normalizeKey(options.region || "");
|
|
534
616
|
const requestedProject = normalizeProjectKey(options.project || "");
|
|
535
617
|
if ((!requestedRegion || requestedRegion === normalizeKey(DOMESTIC_REGION)) && (!requestedProject || entriesForProject(domesticEntries, options.project).length > 0)) {
|
|
536
618
|
return domesticEntries;
|
|
537
619
|
}
|
|
538
620
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
"
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
621
|
+
let workbook;
|
|
622
|
+
try {
|
|
623
|
+
workbook = await commandRunner("lark-cli", [
|
|
624
|
+
"sheets",
|
|
625
|
+
"+workbook-info",
|
|
626
|
+
"--url",
|
|
627
|
+
PLATFORM_CONFIG_URL,
|
|
628
|
+
"--as",
|
|
629
|
+
"user",
|
|
630
|
+
"--format",
|
|
631
|
+
"json"
|
|
632
|
+
]);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
await throwPlatformAuthorizationError(error, commandRunner);
|
|
635
|
+
}
|
|
549
636
|
const sheet = workbook?.data?.sheets?.find(candidate => !candidate.is_hidden) || workbook?.data?.sheets?.[0];
|
|
550
637
|
if (!sheet?.sheet_id || !Number.isInteger(sheet.row_count) || sheet.row_count < 1) {
|
|
551
638
|
throw new Error("飞书平台配置没有可读取的工作表");
|
|
552
639
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
"
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
640
|
+
let value;
|
|
641
|
+
try {
|
|
642
|
+
value = await commandRunner("lark-cli", [
|
|
643
|
+
"sheets",
|
|
644
|
+
"+csv-get",
|
|
645
|
+
"--url",
|
|
646
|
+
PLATFORM_CONFIG_URL,
|
|
647
|
+
"--sheet-id",
|
|
648
|
+
sheet.sheet_id,
|
|
649
|
+
"--range",
|
|
650
|
+
`A1:I${sheet.row_count}`,
|
|
651
|
+
"--include-row-prefix=false",
|
|
652
|
+
"--as",
|
|
653
|
+
"user",
|
|
654
|
+
"--format",
|
|
655
|
+
"json"
|
|
656
|
+
]);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
await throwPlatformAuthorizationError(error, commandRunner);
|
|
659
|
+
}
|
|
568
660
|
return normalizePlatformRows(rowsFromCsvEnvelope(value));
|
|
569
661
|
}
|
|
570
662
|
|
|
@@ -904,7 +996,7 @@ function plainTextForPlatform(article, platformKey) {
|
|
|
904
996
|
$("img").each((_, element) => {
|
|
905
997
|
const image = $(element);
|
|
906
998
|
const source = image.attr("src") || image.attr("data-qcplay-src") || image.attr("data-src") || image.attr("data-original");
|
|
907
|
-
image.replaceWith(source ? `\n${source}\n` : "");
|
|
999
|
+
image.replaceWith(platformKey === "meta" ? "\n" : source ? `\n${source}\n` : "");
|
|
908
1000
|
});
|
|
909
1001
|
const markdown = normalizeText($.root().text() || article.markdown)
|
|
910
1002
|
.replace(/[ \t]+\n/g, "\n")
|
|
@@ -2105,7 +2197,7 @@ export function browserPlatformPageUrl(entry, type = "topic") {
|
|
|
2105
2197
|
case "x":
|
|
2106
2198
|
return "https://x.com/compose/post";
|
|
2107
2199
|
case "meta":
|
|
2108
|
-
return (
|
|
2200
|
+
return metaComposerUrl(
|
|
2109
2201
|
configuredUrl(entry.url, url => url.hostname.toLowerCase() === "business.facebook.com") ||
|
|
2110
2202
|
configuredUrl(entry.url)
|
|
2111
2203
|
);
|
|
@@ -2133,6 +2225,10 @@ function imageSourcesFromArticle(article) {
|
|
|
2133
2225
|
.filter(Boolean);
|
|
2134
2226
|
}
|
|
2135
2227
|
|
|
2228
|
+
export function resolveMetaPublishingOptions(article = {}) {
|
|
2229
|
+
return { images: imageSourcesFromArticle(article) };
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2136
2232
|
function splitImageSources(value) {
|
|
2137
2233
|
return normalizeText(value)
|
|
2138
2234
|
.split(/[,,\r\n]+/)
|
|
@@ -6229,6 +6325,114 @@ async function clearWeiboArticleBody(page, bodyInput) {
|
|
|
6229
6325
|
}
|
|
6230
6326
|
}
|
|
6231
6327
|
|
|
6328
|
+
function metaComposerUrl(value) {
|
|
6329
|
+
const configured = normalizeText(value);
|
|
6330
|
+
if (!configured) return "";
|
|
6331
|
+
try {
|
|
6332
|
+
const source = new URL(configured);
|
|
6333
|
+
if (source.hostname.toLowerCase() !== "business.facebook.com" || !/\/latest\/composer\/?$/i.test(source.pathname)) {
|
|
6334
|
+
return source.toString();
|
|
6335
|
+
}
|
|
6336
|
+
const target = new URL("/latest/composer/", source.origin);
|
|
6337
|
+
for (const name of ["asset_id", "business_id"]) {
|
|
6338
|
+
const parameter = source.searchParams.get(name);
|
|
6339
|
+
if (parameter) target.searchParams.set(name, parameter);
|
|
6340
|
+
}
|
|
6341
|
+
return target.toString();
|
|
6342
|
+
} catch {
|
|
6343
|
+
return configured;
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
|
|
6347
|
+
function genericMetaComposerUrl(value) {
|
|
6348
|
+
try {
|
|
6349
|
+
const url = new URL(value);
|
|
6350
|
+
return new URL("/latest/composer/", url.origin).toString();
|
|
6351
|
+
} catch {
|
|
6352
|
+
return "https://business.facebook.com/latest/composer/";
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
|
|
6356
|
+
export function isMetaUnavailableContent(value) {
|
|
6357
|
+
return /很抱歉.{0,24}(?:无法显示内容|内容不可用)|链接可能已过期|(?:sorry,?\s*)?(?:this|the) content isn['’]t available|content isn['’]t available right now|page may only be visible to an audience/i.test(
|
|
6358
|
+
normalizeText(value)
|
|
6359
|
+
);
|
|
6360
|
+
}
|
|
6361
|
+
|
|
6362
|
+
async function metaUnavailablePage(page) {
|
|
6363
|
+
if (typeof page?.locator !== "function") return false;
|
|
6364
|
+
const text = await page.locator("body").innerText().catch(() => "");
|
|
6365
|
+
return isMetaUnavailableContent(text);
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
async function metaUploadedPhotoCount(page) {
|
|
6369
|
+
if (typeof page?.getByText !== "function") return 0;
|
|
6370
|
+
return retryAcrossNavigation(page, () =>
|
|
6371
|
+
page.getByText(/^(?:移除|删除)(?:照片|图片)$|^Remove photo$|^Delete photo$/i, { exact: true }).count()
|
|
6372
|
+
);
|
|
6373
|
+
}
|
|
6374
|
+
|
|
6375
|
+
async function waitForMetaPhotoUploads(page, expectedCount, timeoutMs = 180000) {
|
|
6376
|
+
const deadline = Date.now() + timeoutMs;
|
|
6377
|
+
let stableChecks = 0;
|
|
6378
|
+
do {
|
|
6379
|
+
const text = await page.locator("body").innerText().catch(() => "");
|
|
6380
|
+
if (/上传失败|无法上传|照片.{0,12}(?:不受支持|格式错误)|处理失败|upload failed|couldn['’]t upload|unsupported/i.test(text)) {
|
|
6381
|
+
throw new Error("Facebook 图片上传失败,请检查页面提示或图片格式");
|
|
6382
|
+
}
|
|
6383
|
+
const uploaded = await metaUploadedPhotoCount(page);
|
|
6384
|
+
const pending = await pendingUploadCount(page);
|
|
6385
|
+
if (uploaded >= expectedCount && pending === 0) {
|
|
6386
|
+
stableChecks += 1;
|
|
6387
|
+
if (stableChecks >= 3) return uploaded;
|
|
6388
|
+
} else {
|
|
6389
|
+
stableChecks = 0;
|
|
6390
|
+
}
|
|
6391
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
6392
|
+
} while (Date.now() < deadline);
|
|
6393
|
+
const uploaded = await metaUploadedPhotoCount(page).catch(() => 0);
|
|
6394
|
+
throw new Error(`Facebook 图片上传未完成,预期 ${expectedCount} 张,页面确认 ${uploaded} 张`);
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
export async function uploadMetaImages(page, article, options = {}) {
|
|
6398
|
+
const publishing = resolveMetaPublishingOptions(article);
|
|
6399
|
+
if (publishing.images.length === 0) return { images: 0 };
|
|
6400
|
+
if (typeof page?.waitForEvent !== "function") {
|
|
6401
|
+
throw new Error("Facebook 页面不支持文件选择器事件,无法上传正文图片");
|
|
6402
|
+
}
|
|
6403
|
+
|
|
6404
|
+
const payloads = [];
|
|
6405
|
+
for (let index = 0; index < publishing.images.length; index += 1) {
|
|
6406
|
+
payloads.push(await imageUploadPayload(publishing.images[index], article.articleFile, index, "Facebook"));
|
|
6407
|
+
if ((index + 1) % 5 === 0 || index + 1 === publishing.images.length) {
|
|
6408
|
+
options.onProgress?.(`已准备 Facebook 图片 ${index + 1}/${publishing.images.length}`);
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
|
|
6412
|
+
const trigger =
|
|
6413
|
+
(await waitForVisibleButton(page, [/添加照片\s*\/\s*视频/i, /Add photos?\s*\/\s*videos?/i], 8000)) ||
|
|
6414
|
+
(await waitForVisibleTextAcrossFrames(page, /添加照片\s*\/\s*视频|Add photos?\s*\/\s*videos?/i, 3000, false));
|
|
6415
|
+
if (!trigger) {
|
|
6416
|
+
throw new Error("Facebook 发布页未找到“添加照片/视频”控件");
|
|
6417
|
+
}
|
|
6418
|
+
|
|
6419
|
+
const chooserPromise = page.waitForEvent("filechooser", { timeout: 10000 }).catch(() => null);
|
|
6420
|
+
await trigger.click();
|
|
6421
|
+
const chooser = await chooserPromise;
|
|
6422
|
+
if (!chooser) {
|
|
6423
|
+
throw new Error("Facebook 点击“添加照片/视频”后未打开文件选择器");
|
|
6424
|
+
}
|
|
6425
|
+
if (payloads.length > 1 && typeof chooser.isMultiple === "function" && !chooser.isMultiple()) {
|
|
6426
|
+
throw new Error(`Facebook 文件选择器不支持多选,无法一次上传 ${payloads.length} 张图片`);
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
options.onProgress?.(`正在上传 Facebook 图片 ${payloads.length} 张`);
|
|
6430
|
+
await chooser.setFiles(payloads);
|
|
6431
|
+
const uploaded = await waitForMetaPhotoUploads(page, payloads.length, options.uploadTimeoutMs);
|
|
6432
|
+
options.onProgress?.(`Facebook 图片上传完成,共 ${uploaded} 张`);
|
|
6433
|
+
return { images: uploaded };
|
|
6434
|
+
}
|
|
6435
|
+
|
|
6232
6436
|
export async function insertWeiboArticleRichContent(page, bodyInput, article, spec, fallbackText, options = {}) {
|
|
6233
6437
|
if (typeof bodyInput.evaluate !== "function" || typeof bodyInput.locator !== "function") {
|
|
6234
6438
|
await fillLocator(bodyInput, fallbackText);
|
|
@@ -7780,14 +7984,14 @@ async function launchBrowserContext(entry, browserChannel, options = {}) {
|
|
|
7780
7984
|
const profileDir = browserProfileDir(entry);
|
|
7781
7985
|
await fs.promises.mkdir(profileDir, { recursive: true, mode: 0o700 });
|
|
7782
7986
|
const channel = browserChannel || process.env.QCPLAY_BROWSER_CHANNEL || (process.platform === "win32" ? "msedge" : "chrome");
|
|
7783
|
-
if (options.awaitAIAction) {
|
|
7784
|
-
return launchDetachedBrowserContext(profileDir, channel);
|
|
7785
|
-
}
|
|
7786
7987
|
const detachedContext = await reconnectDetachedBrowserContext(profileDir, chromium);
|
|
7787
7988
|
if (detachedContext) {
|
|
7788
7989
|
options.onProgress?.(`已复用当前 ${entry.platform} 授权编辑页,继续执行已确认的动作`);
|
|
7789
7990
|
return detachedContext;
|
|
7790
7991
|
}
|
|
7992
|
+
if (options.awaitAIAction) {
|
|
7993
|
+
return launchDetachedBrowserContext(profileDir, channel);
|
|
7994
|
+
}
|
|
7791
7995
|
return chromium.launchPersistentContext(profileDir, {
|
|
7792
7996
|
channel,
|
|
7793
7997
|
headless: false,
|
|
@@ -7971,7 +8175,11 @@ export async function publishWithBrowser(entry, article, options = {}) {
|
|
|
7971
8175
|
const ownsContext = !options.context;
|
|
7972
8176
|
const context = options.context || (await launchBrowserContext(entry, options.browserChannel, options));
|
|
7973
8177
|
const pages = context.pages();
|
|
7974
|
-
|
|
8178
|
+
// A detached Edge session may briefly restore and then close its first tab.
|
|
8179
|
+
// Use a fresh page for AI-action flows so navigation keeps a stable target.
|
|
8180
|
+
let page = options.awaitAIAction && typeof context.newPage === "function"
|
|
8181
|
+
? await context.newPage()
|
|
8182
|
+
: pages[0] || (await context.newPage());
|
|
7975
8183
|
const promptUser = options.waitForUser || (message => waitForUser(message, options.streams));
|
|
7976
8184
|
let haoyouEditorOpened = false;
|
|
7977
8185
|
let preserveEditorOnFailure = false;
|
|
@@ -7991,13 +8199,29 @@ export async function publishWithBrowser(entry, article, options = {}) {
|
|
|
7991
8199
|
if (entry.platformKey === "xiaohongshu") {
|
|
7992
8200
|
return await publishXiaohongshu(page, entry, preparedArticle, spec, options, promptUser);
|
|
7993
8201
|
}
|
|
7994
|
-
|
|
8202
|
+
let editorUrl = browserPlatformPageUrl(entry);
|
|
8203
|
+
let metaFallbackTried = false;
|
|
7995
8204
|
let editorReady = false;
|
|
7996
8205
|
let loginHandled = false;
|
|
7997
|
-
const attempts = entry.platformKey === "haoyou" ? 3 : 2;
|
|
8206
|
+
const attempts = entry.platformKey === "haoyou" || entry.platformKey === "meta" ? 3 : 2;
|
|
7998
8207
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
8208
|
+
if (typeof page.isClosed === "function" && page.isClosed()) {
|
|
8209
|
+
const livePages = context.pages().filter(candidate => !(typeof candidate.isClosed === "function" && candidate.isClosed()));
|
|
8210
|
+
page = livePages.at(-1) || (await context.newPage());
|
|
8211
|
+
}
|
|
7999
8212
|
await page.goto(editorUrl, { waitUntil: "domcontentloaded", timeout: 60000 });
|
|
8000
8213
|
await afterNavigation(page);
|
|
8214
|
+
if (entry.platformKey === "meta" && (await metaUnavailablePage(page))) {
|
|
8215
|
+
if (!metaFallbackTried) {
|
|
8216
|
+
metaFallbackTried = true;
|
|
8217
|
+
editorUrl = genericMetaComposerUrl(editorUrl);
|
|
8218
|
+
options.onProgress?.(`${entry.platform} 资产专用链接不可用,正在尝试 Meta 通用发布入口`);
|
|
8219
|
+
continue;
|
|
8220
|
+
}
|
|
8221
|
+
throw new Error(
|
|
8222
|
+
`${entry.platform} 当前账号无法访问配置中的 Meta 资产,请确认该账号已加入 business_id 对应的商务管理平台并拥有 asset_id 对应主页的内容发布权限`
|
|
8223
|
+
);
|
|
8224
|
+
}
|
|
8001
8225
|
const readyTimeout = attempt === 0 && spec.loginPage ? 3000 : 15000;
|
|
8002
8226
|
const ready = entry.platformKey === "haoyou"
|
|
8003
8227
|
? await waitForHaoyouTargetEditor(page, editorUrl, spec, readyTimeout)
|
|
@@ -8039,6 +8263,7 @@ export async function publishWithBrowser(entry, article, options = {}) {
|
|
|
8039
8263
|
throw new Error(`${entry.platform} 页面中未找到正文编辑器,页面结构可能已更新`);
|
|
8040
8264
|
}
|
|
8041
8265
|
let richContent = null;
|
|
8266
|
+
let platformMedia = null;
|
|
8042
8267
|
if (entry.platformKey === "haoyou") {
|
|
8043
8268
|
if (options.previewTextOnly) {
|
|
8044
8269
|
await fillStableHaoyouValue(page, bodyInput, body, "正文");
|
|
@@ -8055,6 +8280,9 @@ export async function publishWithBrowser(entry, article, options = {}) {
|
|
|
8055
8280
|
} else {
|
|
8056
8281
|
await fillLocator(bodyInput, body);
|
|
8057
8282
|
}
|
|
8283
|
+
if (entry.platformKey === "meta") {
|
|
8284
|
+
platformMedia = await uploadMetaImages(page, preparedArticle, options);
|
|
8285
|
+
}
|
|
8058
8286
|
|
|
8059
8287
|
let submitButton = await waitForVisible(page, spec.submitSelectors, 5000);
|
|
8060
8288
|
submitButton ||= await waitForVisibleButton(page, spec.submit, 5000);
|
|
@@ -8066,7 +8294,7 @@ export async function publishWithBrowser(entry, article, options = {}) {
|
|
|
8066
8294
|
}
|
|
8067
8295
|
const review = await reviewPreparedDraft(page, entry, options);
|
|
8068
8296
|
if (review) {
|
|
8069
|
-
return { ...review, ...richContent };
|
|
8297
|
+
return { ...review, ...richContent, ...platformMedia };
|
|
8070
8298
|
}
|
|
8071
8299
|
if (options.draftOnly) {
|
|
8072
8300
|
return {
|
|
@@ -8075,7 +8303,8 @@ export async function publishWithBrowser(entry, article, options = {}) {
|
|
|
8075
8303
|
draftOnly: true,
|
|
8076
8304
|
approvalRequired: true,
|
|
8077
8305
|
approvalActions: ["publish", "cancel"],
|
|
8078
|
-
...richContent
|
|
8306
|
+
...richContent,
|
|
8307
|
+
...platformMedia
|
|
8079
8308
|
};
|
|
8080
8309
|
}
|
|
8081
8310
|
if (entry.platformKey === "haoyou") {
|
|
@@ -8096,7 +8325,7 @@ export async function publishWithBrowser(entry, article, options = {}) {
|
|
|
8096
8325
|
if (options.keepOpen) {
|
|
8097
8326
|
await promptUser(`${entry.platform} 已提交,浏览器保持打开供检查`);
|
|
8098
8327
|
}
|
|
8099
|
-
return { url: page.url() };
|
|
8328
|
+
return { url: page.url(), ...platformMedia };
|
|
8100
8329
|
} catch (error) {
|
|
8101
8330
|
if ((entry.platformKey === "haoyou" && haoyouEditorOpened) || entry.platformKey === "xiaohongshu") {
|
|
8102
8331
|
if (!ownsContext) throw error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qcplay/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "QCPlay CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"@larksuite/cli": "1.0.96",
|
|
41
42
|
"cheerio": "^1.0.0-rc.12",
|
|
42
43
|
"playwright-core": "1.40.1"
|
|
43
44
|
}
|