@qcplay/cli 1.0.17 → 1.0.19
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/README.md +58 -329
- package/bin/qcplay.js +416 -54
- package/lib/platform-publish.js +1173 -177
- package/lib/rich-article-upload.js +116 -0
- package/lib/wechat-article.js +126 -29
- package/lib/wechat-data.js +155 -0
- package/package.json +2 -1
- package/templates/skills/qcplay-publish-article/SKILL.md +46 -18
- package/templates/skills/qcplay-wechat-data/SKILL.md +101 -0
package/bin/qcplay.js
CHANGED
|
@@ -10,11 +10,15 @@ import readline from "readline";
|
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
+
analyzeArticleContentSafety,
|
|
14
|
+
buildImportedArticleHtml,
|
|
13
15
|
buildOfficialArticleMarkdown,
|
|
14
16
|
importWechatArticle,
|
|
15
17
|
normalizeArticleColor,
|
|
16
|
-
sanitizeArticleRichHtml
|
|
18
|
+
sanitizeArticleRichHtml,
|
|
19
|
+
uploadMediaToBackend
|
|
17
20
|
} from "../lib/wechat-article.js";
|
|
21
|
+
import { buildRichArticleUploadPayload, parseRichArticleUploadFile } from "../lib/rich-article-upload.js";
|
|
18
22
|
import { importLarkArticle, parseLarkDocumentUrl } from "../lib/lark-article.js";
|
|
19
23
|
import {
|
|
20
24
|
CONTENT_RULES_TEMPLATE,
|
|
@@ -23,6 +27,15 @@ import {
|
|
|
23
27
|
loadContentRules,
|
|
24
28
|
matchingContentRules
|
|
25
29
|
} from "../lib/content-rules.js";
|
|
30
|
+
import {
|
|
31
|
+
WECHAT_GAME_CATALOG,
|
|
32
|
+
buildWechatArticleJson,
|
|
33
|
+
normalizeWechatGameId,
|
|
34
|
+
monthDateRange,
|
|
35
|
+
renderWechatArticleMarkdown,
|
|
36
|
+
splitWechatDateRange,
|
|
37
|
+
todayInShanghai
|
|
38
|
+
} from "../lib/wechat-data.js";
|
|
26
39
|
import {
|
|
27
40
|
availableProjects,
|
|
28
41
|
bilibiliPublishingPlan,
|
|
@@ -36,7 +49,8 @@ import {
|
|
|
36
49
|
runPlatformPublishSequence,
|
|
37
50
|
selectRequestedEntries,
|
|
38
51
|
tapTapPublishingPlan,
|
|
39
|
-
weiboPublishingPlan
|
|
52
|
+
weiboPublishingPlan,
|
|
53
|
+
writePlatformPublishUrls
|
|
40
54
|
} from "../lib/platform-publish.js";
|
|
41
55
|
|
|
42
56
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -172,6 +186,7 @@ const ARTICLE_PREVIEW_BASE_URL = "https://snail.qingcigame.com/official/news-det
|
|
|
172
186
|
const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
173
187
|
const WEST_ARTICLE_LANGUAGES = new Set(["portuguese", "german", "spanish", "italian", "french", "america_en"]);
|
|
174
188
|
const ARTICLE_PREVIEW_URLS = {
|
|
189
|
+
"33": "https://tideng.qingcigame.com/official/news.html",
|
|
175
190
|
"50": "https://mtzg.qingcijoy.com/news-details.html",
|
|
176
191
|
"69": "https://hjwq.qingcijoy.com/news-details.html",
|
|
177
192
|
"74": "https://sakura.qingcijoy.com/news-details.html",
|
|
@@ -225,10 +240,11 @@ Usage:
|
|
|
225
240
|
qcplay-cli article
|
|
226
241
|
qcplay-cli article init [file]
|
|
227
242
|
qcplay-cli article west-init [file]
|
|
228
|
-
qcplay-cli article import <wechat-or-feishu-url> [file] [--rules-file <json|txt|docx>] [--project <项目>] [--platform <平台>]
|
|
243
|
+
qcplay-cli article import <wechat-or-feishu-url> [file] [--format html|markdown] [--rules-file <json|txt|docx>] [--project <项目>] [--platform <平台>] [--backend <url>] [--local]
|
|
229
244
|
qcplay-cli article publish <file> [--title <标题>] [--cover <图片>] [--project <项目>] [--region <区域>] [--rules-file <json>] [--dry-run]
|
|
230
245
|
qcplay-cli article update [id] [--local] [--backend <url>] [--dry-run]
|
|
231
246
|
qcplay-cli article delete [id] [--local] [--backend <url>] [--dry-run]
|
|
247
|
+
qcplay-cli wechat article --game-id <id> [--game-id <id> ...] (--month <YYYY-MM> | --begin-date <YYYY-MM-DD> --end-date <YYYY-MM-DD>) [--output <file>] [--format markdown|json] [--local] [--backend <url>]
|
|
232
248
|
qcplay-cli publish [file] [--title <标题>] [--cover <图片>] [--weibo-column <专栏>] [--project <项目>] [--region <区域>] [--platform <平台>]
|
|
233
249
|
qcplay-cli publish --project <项目> --list-platforms
|
|
234
250
|
qcplay-cli rules [init|validate|list] [--rules-file <json>]
|
|
@@ -263,29 +279,124 @@ function printArticleHelp() {
|
|
|
263
279
|
qcplay-cli article
|
|
264
280
|
qcplay-cli article init [file]
|
|
265
281
|
qcplay-cli article west-init [file]
|
|
266
|
-
qcplay-cli article import <wechat-or-feishu-url> [file] [--rules-file <json>] [--project <项目>]
|
|
282
|
+
qcplay-cli article import <wechat-or-feishu-url> [file] [--format html|markdown] [--rules-file <json>] [--project <项目>] [--backend <url>] [--local]
|
|
267
283
|
qcplay-cli article publish <file> [--title <标题>] [--cover <图片>] [--project <项目>] [--region <区域>] [--rules-file <json>] [--dry-run]
|
|
268
284
|
qcplay-cli article update [id] [--local] [--backend <url>] [--dry-run]
|
|
269
285
|
qcplay-cli article delete [id] [--local] [--backend <url>] [--dry-run]`);
|
|
270
286
|
}
|
|
271
287
|
|
|
288
|
+
function printWechatHelp() {
|
|
289
|
+
console.log(`Usage:
|
|
290
|
+
qcplay-cli wechat article --game-id <id> [--game-id <id> ...] (--month <YYYY-MM> | --begin-date <YYYY-MM-DD> --end-date <YYYY-MM-DD>) [--output <file>] [--format markdown|json] [--local] [--backend <url>]
|
|
291
|
+
|
|
292
|
+
只读取微信公众号接口返回的 day_total,并按游戏和月份生成表格。
|
|
293
|
+
支持的 game_id: ${Object.entries(WECHAT_GAME_CATALOG).map(([id, name]) => `${id}(${name})`).join("、")}
|
|
294
|
+
当前日期作为结束日期时,必须填写到昨天;超过 30 天的范围会按月份和 30 天窗口自动拆分请求。`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseWechatArticleOptions(args) {
|
|
298
|
+
const options = {
|
|
299
|
+
gameIds: [],
|
|
300
|
+
beginDate: "",
|
|
301
|
+
endDate: "",
|
|
302
|
+
month: "",
|
|
303
|
+
output: "",
|
|
304
|
+
format: "markdown",
|
|
305
|
+
backend: undefined,
|
|
306
|
+
local: false
|
|
307
|
+
};
|
|
308
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
309
|
+
const current = args[index];
|
|
310
|
+
if (["--game-id", "--game", "--begin-date", "--start", "--end-date", "--end", "--month", "--output", "--format", "--backend"].includes(current)) {
|
|
311
|
+
const next = args[index + 1];
|
|
312
|
+
if (!next || next.startsWith("--")) throw new Error(`${current} 缺少参数值`);
|
|
313
|
+
if (current === "--game-id" || current === "--game") {
|
|
314
|
+
options.gameIds.push(...next.split(",").map(normalizeWechatGameId));
|
|
315
|
+
} else if (current === "--begin-date" || current === "--start") {
|
|
316
|
+
options.beginDate = next;
|
|
317
|
+
} else if (current === "--end-date" || current === "--end") {
|
|
318
|
+
options.endDate = next;
|
|
319
|
+
} else if (current === "--month") {
|
|
320
|
+
options.month = next;
|
|
321
|
+
} else if (current === "--output") {
|
|
322
|
+
options.output = next;
|
|
323
|
+
} else if (current === "--format") {
|
|
324
|
+
options.format = next.toLowerCase();
|
|
325
|
+
} else {
|
|
326
|
+
options.backend = next;
|
|
327
|
+
}
|
|
328
|
+
index += 1;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (current === "--local") {
|
|
332
|
+
options.local = true;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (current === "--all-games") {
|
|
336
|
+
options.gameIds = Object.keys(WECHAT_GAME_CATALOG);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`未知参数: ${current}`);
|
|
340
|
+
}
|
|
341
|
+
options.gameIds = [...new Set(options.gameIds)];
|
|
342
|
+
if (options.gameIds.length === 0) throw new Error("请至少指定一个 --game-id");
|
|
343
|
+
if (options.month && (options.beginDate || options.endDate)) throw new Error("--month 不能与 --begin-date/--end-date 同时使用");
|
|
344
|
+
if (options.month) {
|
|
345
|
+
const range = monthDateRange(options.month, todayInShanghai());
|
|
346
|
+
options.beginDate = range.begin.toISOString().slice(0, 10);
|
|
347
|
+
options.endDate = range.end.toISOString().slice(0, 10);
|
|
348
|
+
}
|
|
349
|
+
if (!options.beginDate || !options.endDate) throw new Error("请指定 --month,或同时指定 --begin-date 和 --end-date");
|
|
350
|
+
if (!new Set(["markdown", "json"]).has(options.format)) throw new Error("--format 只能是 markdown 或 json");
|
|
351
|
+
return options;
|
|
352
|
+
}
|
|
353
|
+
|
|
272
354
|
function parseArticleImportOptions(args) {
|
|
273
355
|
const positional = [];
|
|
274
|
-
const options = { rulesFile: "", project: "", platform: "" };
|
|
356
|
+
const options = { rulesFile: "", project: "", platform: "", format: "html", backend: undefined, local: false };
|
|
275
357
|
for (let index = 0; index < args.length; index += 1) {
|
|
276
358
|
const current = args[index];
|
|
277
|
-
if (
|
|
359
|
+
if (
|
|
360
|
+
current === "--rules-file" ||
|
|
361
|
+
current === "--project" ||
|
|
362
|
+
current === "--platform" ||
|
|
363
|
+
current === "--format" ||
|
|
364
|
+
current === "--backend"
|
|
365
|
+
) {
|
|
278
366
|
const next = args[index + 1];
|
|
279
367
|
if (!next || next.startsWith("--")) throw new Error(`${current} 缺少参数值`);
|
|
280
|
-
|
|
368
|
+
if (current === "--format") {
|
|
369
|
+
options.format = next.toLowerCase();
|
|
370
|
+
} else {
|
|
371
|
+
const key =
|
|
372
|
+
current === "--rules-file"
|
|
373
|
+
? "rulesFile"
|
|
374
|
+
: current === "--project"
|
|
375
|
+
? "project"
|
|
376
|
+
: current === "--platform"
|
|
377
|
+
? "platform"
|
|
378
|
+
: "backend";
|
|
379
|
+
options[key] = next;
|
|
380
|
+
}
|
|
281
381
|
index += 1;
|
|
282
382
|
continue;
|
|
283
383
|
}
|
|
384
|
+
if (current === "--local") {
|
|
385
|
+
options.local = true;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
284
388
|
if (current.startsWith("--")) throw new Error(`未知参数: ${current}`);
|
|
285
389
|
positional.push(current);
|
|
286
390
|
}
|
|
287
391
|
if (positional.length > 2) throw new Error(`未知参数: ${positional.slice(2).join(" ")}`);
|
|
288
|
-
|
|
392
|
+
if (!new Set(["markdown", "html"]).has(options.format)) {
|
|
393
|
+
throw new Error("--format 只能是 markdown 或 html");
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
sourceUrl: positional[0],
|
|
397
|
+
file: positional[1] || (options.format === "markdown" ? "article.md" : "article.html"),
|
|
398
|
+
options
|
|
399
|
+
};
|
|
289
400
|
}
|
|
290
401
|
|
|
291
402
|
function printPublishHelp() {
|
|
@@ -305,7 +416,10 @@ Options:
|
|
|
305
416
|
--config-file <json> 使用本地平台配置(主要用于测试)
|
|
306
417
|
--rules-file <json> 使用指定内容规则;默认 ~/.qcplay/platform-rules.json
|
|
307
418
|
--browser-channel <name> Playwright 浏览器通道,Windows 默认 msedge
|
|
308
|
-
--review-draft, --review
|
|
419
|
+
--review-draft, --review 官网外平台先准备草稿,预览后由当前 AI 对话确认发布
|
|
420
|
+
--direct, --no-review 需配合 AI 对话确认标记后才可跳过发布前预览
|
|
421
|
+
--publish-draft 仅 TapTap:发布标题匹配的已有草稿,不重新写入内容
|
|
422
|
+
--draft-only, --draft 仅保存或停留在官网外平台草稿,不执行正式发布
|
|
309
423
|
--open-page 只打开真实浏览器发布页,供登录和页面流程测试,不读取或提交文章
|
|
310
424
|
--switch-account 先打开发布页切换账号,确认后继续当前发布流程
|
|
311
425
|
--keep-open 浏览器提交后等待用户确认再关闭
|
|
@@ -371,6 +485,56 @@ function parseRulesOptions(args) {
|
|
|
371
485
|
return options;
|
|
372
486
|
}
|
|
373
487
|
|
|
488
|
+
async function importWechatDataCommand(options) {
|
|
489
|
+
const config = await loadConfig();
|
|
490
|
+
const backendUrl = resolveAuthBackendUrl(options, config);
|
|
491
|
+
const reports = [];
|
|
492
|
+
for (const gameId of options.gameIds) {
|
|
493
|
+
const ranges = splitWechatDateRange(options.beginDate, options.endDate, todayInShanghai());
|
|
494
|
+
const dayTotal = new Map();
|
|
495
|
+
for (const range of ranges) {
|
|
496
|
+
const query = new URLSearchParams({
|
|
497
|
+
game_id: gameId,
|
|
498
|
+
begin_date: range.beginDate,
|
|
499
|
+
end_date: range.endDate
|
|
500
|
+
});
|
|
501
|
+
console.log(chalk.gray(`正在拉取${WECHAT_GAME_CATALOG[gameId]} ${range.beginDate} 至 ${range.endDate}`));
|
|
502
|
+
const response = await authenticatedRequest("GET", backendUrl, `/api/wechat/article?${query}`);
|
|
503
|
+
if (Number(response.code) !== 200) {
|
|
504
|
+
throw new Error(`${WECHAT_GAME_CATALOG[gameId]} ${range.beginDate} 至 ${range.endDate}:${response.message || `接口返回 code=${response.code ?? "未知"}`}`);
|
|
505
|
+
}
|
|
506
|
+
const items = Array.isArray(response.data?.day_total) ? response.data.day_total : [];
|
|
507
|
+
for (const item of items) {
|
|
508
|
+
const key = `${item.ref_date || ""}\u0000${item.msgid || ""}`;
|
|
509
|
+
dayTotal.set(key, item);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
reports.push({
|
|
513
|
+
gameId,
|
|
514
|
+
gameName: WECHAT_GAME_CATALOG[gameId],
|
|
515
|
+
beginDate: options.beginDate,
|
|
516
|
+
endDate: options.endDate,
|
|
517
|
+
ranges,
|
|
518
|
+
dayTotal: [...dayTotal.values()].sort((left, right) => {
|
|
519
|
+
const byDate = String(left.ref_date || "").localeCompare(String(right.ref_date || ""));
|
|
520
|
+
return byDate || String(left.msgid || "").localeCompare(String(right.msgid || ""));
|
|
521
|
+
})
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const output = options.format === "json" ? buildWechatArticleJson(reports) : renderWechatArticleMarkdown(reports);
|
|
526
|
+
if (options.output) {
|
|
527
|
+
const target = path.resolve(options.output);
|
|
528
|
+
if (await pathExists(target)) throw new Error(`输出文件已存在: ${target}`);
|
|
529
|
+
await ensureDir(path.dirname(target));
|
|
530
|
+
await fs.promises.writeFile(target, output, { encoding: "utf8", mode: 0o600 });
|
|
531
|
+
console.log(chalk.green(`微信公众号数据已生成: ${target}`));
|
|
532
|
+
console.log(`文章数据: ${reports.reduce((sum, report) => sum + report.dayTotal.length, 0)} 篇`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
process.stdout.write(output);
|
|
536
|
+
}
|
|
537
|
+
|
|
374
538
|
async function contentRulesCommand(action, args = []) {
|
|
375
539
|
const options = parseRulesOptions(args);
|
|
376
540
|
const file = path.resolve(options.rulesFile || CONTENT_RULES_FILE);
|
|
@@ -423,7 +587,7 @@ function printFeatures() {
|
|
|
423
587
|
console.log(" qcplay-cli auth permissions --key www-article-list.store");
|
|
424
588
|
console.log("");
|
|
425
589
|
console.log(chalk.cyan("3. 发布官网文章"));
|
|
426
|
-
console.log(" qcplay-cli article import <微信文章或飞书文档链接> article.
|
|
590
|
+
console.log(" qcplay-cli article import <微信文章或飞书文档链接> article.html");
|
|
427
591
|
console.log(" qcplay-cli www-article-list.store article.md");
|
|
428
592
|
console.log(" qcplay-cli article update [id]");
|
|
429
593
|
console.log(" qcplay-cli article delete [id]");
|
|
@@ -435,6 +599,9 @@ function printFeatures() {
|
|
|
435
599
|
console.log(chalk.cyan("5. 查看本地目录"));
|
|
436
600
|
console.log(" qcplay-cli where");
|
|
437
601
|
console.log("");
|
|
602
|
+
console.log(chalk.cyan("6. 拉取微信公众号数据"));
|
|
603
|
+
console.log(" qcplay-cli wechat article --game-id 39 --month 2026-08 --output wechat-2026-08.md");
|
|
604
|
+
console.log("");
|
|
438
605
|
}
|
|
439
606
|
|
|
440
607
|
function printWhere() {
|
|
@@ -806,6 +973,10 @@ function parseDistributionOptions(args) {
|
|
|
806
973
|
weiboColumn: "",
|
|
807
974
|
browserChannel: "",
|
|
808
975
|
reviewDraft: false,
|
|
976
|
+
directPublish: false,
|
|
977
|
+
confirmed: false,
|
|
978
|
+
draftOnly: false,
|
|
979
|
+
publishDraft: false,
|
|
809
980
|
keepOpen: false,
|
|
810
981
|
openPage: false,
|
|
811
982
|
switchAccount: false,
|
|
@@ -845,6 +1016,22 @@ function parseDistributionOptions(args) {
|
|
|
845
1016
|
options.reviewDraft = true;
|
|
846
1017
|
continue;
|
|
847
1018
|
}
|
|
1019
|
+
if (current === "--direct" || current === "--no-review") {
|
|
1020
|
+
options.directPublish = true;
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (current === "--confirmed") {
|
|
1024
|
+
options.confirmed = true;
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (current === "--draft-only" || current === "--draft") {
|
|
1028
|
+
options.draftOnly = true;
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (current === "--publish-draft") {
|
|
1032
|
+
options.publishDraft = true;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
848
1035
|
|
|
849
1036
|
const valueFlags = new Map([
|
|
850
1037
|
["--backend", "backend"],
|
|
@@ -909,11 +1096,11 @@ function applyPublishOverrides(parsedArticle, options) {
|
|
|
909
1096
|
...parsedArticle.meta,
|
|
910
1097
|
...(cover
|
|
911
1098
|
? {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1099
|
+
weibo_cover: cover,
|
|
1100
|
+
taptap_cover: cover,
|
|
1101
|
+
bilibili_cover: cover,
|
|
1102
|
+
xiaohongshu_images: cover
|
|
1103
|
+
}
|
|
917
1104
|
: {}),
|
|
918
1105
|
...(weiboColumn ? { weibo_column: weiboColumn } : {})
|
|
919
1106
|
};
|
|
@@ -991,7 +1178,9 @@ function openBrowser(targetUrl) {
|
|
|
991
1178
|
|
|
992
1179
|
if (process.platform === "win32") {
|
|
993
1180
|
command = "cmd";
|
|
994
|
-
|
|
1181
|
+
// 关键修复:将 URL 中的 & 替换为 ^&
|
|
1182
|
+
const safeUrl = targetUrl.replace(/&/g, '^&');
|
|
1183
|
+
args = ["/c", "start", "", safeUrl];
|
|
995
1184
|
} else if (process.platform === "darwin") {
|
|
996
1185
|
command = "open";
|
|
997
1186
|
args = [targetUrl];
|
|
@@ -1332,7 +1521,7 @@ function backendOriginsMatch(left, right) {
|
|
|
1332
1521
|
}
|
|
1333
1522
|
}
|
|
1334
1523
|
|
|
1335
|
-
async function saveRecentArticleState(articleId, status, backendUrl, previewUrl = "", target = "domestic") {
|
|
1524
|
+
async function saveRecentArticleState(articleId, status, backendUrl, previewUrl = "", target = "domestic", metadata = {}) {
|
|
1336
1525
|
const authState = await loadAuthState();
|
|
1337
1526
|
await writeJson(ARTICLE_STATE_FILE, {
|
|
1338
1527
|
article_id: articleId,
|
|
@@ -1342,13 +1531,15 @@ async function saveRecentArticleState(articleId, status, backendUrl, previewUrl
|
|
|
1342
1531
|
backend_url: backendUrl,
|
|
1343
1532
|
administrator_id: Number(authState.id || 0),
|
|
1344
1533
|
account: normalizeText(authState.account),
|
|
1534
|
+
game_id: normalizeText(metadata.game_id),
|
|
1535
|
+
cate_id: normalizeText(metadata.cate_id),
|
|
1345
1536
|
updated_at: Date.now()
|
|
1346
1537
|
});
|
|
1347
1538
|
}
|
|
1348
1539
|
|
|
1349
|
-
async function trySaveRecentArticleState(articleId, status, backendUrl, previewUrl = "", target = "domestic") {
|
|
1540
|
+
async function trySaveRecentArticleState(articleId, status, backendUrl, previewUrl = "", target = "domestic", metadata = {}) {
|
|
1350
1541
|
try {
|
|
1351
|
-
await saveRecentArticleState(articleId, status, backendUrl, previewUrl, target);
|
|
1542
|
+
await saveRecentArticleState(articleId, status, backendUrl, previewUrl, target, metadata);
|
|
1352
1543
|
return "";
|
|
1353
1544
|
} catch (error) {
|
|
1354
1545
|
return normalizeText(error?.message) || String(error);
|
|
@@ -1689,14 +1880,14 @@ function applyInlineMarkdown(text) {
|
|
|
1689
1880
|
output = output.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) => {
|
|
1690
1881
|
return storeToken(
|
|
1691
1882
|
`<img src="${escapeHtml(url.trim())}" alt="${escapeHtml(alt.trim())}" ` +
|
|
1692
|
-
|
|
1883
|
+
'style="display:block;box-sizing:border-box;width:auto;max-width:100%;height:auto;object-fit:contain;margin:24px auto;" />'
|
|
1693
1884
|
);
|
|
1694
1885
|
});
|
|
1695
1886
|
|
|
1696
1887
|
output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
|
|
1697
1888
|
return storeToken(
|
|
1698
1889
|
`<a href="${escapeHtml(url.trim())}" target="_blank" rel="noreferrer" ` +
|
|
1699
|
-
|
|
1890
|
+
`style="color:#576b95;text-decoration:none;overflow-wrap:anywhere;word-break:break-all;">${escapeHtml(label.trim())}</a>`
|
|
1700
1891
|
);
|
|
1701
1892
|
});
|
|
1702
1893
|
|
|
@@ -1742,7 +1933,7 @@ function markdownToHtml(markdown) {
|
|
|
1742
1933
|
|
|
1743
1934
|
html.push(
|
|
1744
1935
|
`<p style="margin:0 0 20px;font-size:16px;line-height:1.9;color:#3f3f3f;text-align:justify;overflow-wrap:anywhere;word-break:break-word;">` +
|
|
1745
|
-
|
|
1936
|
+
`${paragraph.map(line => applyInlineMarkdown(line)).join("<br />")}</p>`
|
|
1746
1937
|
);
|
|
1747
1938
|
paragraph = [];
|
|
1748
1939
|
}
|
|
@@ -1754,9 +1945,9 @@ function markdownToHtml(markdown) {
|
|
|
1754
1945
|
|
|
1755
1946
|
html.push(
|
|
1756
1947
|
'<blockquote style="margin:24px 0;padding:14px 18px;border-left:4px solid #17b9c2;background:#f5f7f8;color:#57606a;">' +
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1948
|
+
`<p style="margin:0;font-size:15px;line-height:1.8;">${quote
|
|
1949
|
+
.map(line => applyInlineMarkdown(line))
|
|
1950
|
+
.join("<br />")}</p></blockquote>`
|
|
1760
1951
|
);
|
|
1761
1952
|
quote = [];
|
|
1762
1953
|
}
|
|
@@ -1770,7 +1961,7 @@ function markdownToHtml(markdown) {
|
|
|
1770
1961
|
|
|
1771
1962
|
html.push(
|
|
1772
1963
|
`<${listType} style="margin:0 0 20px;padding-left:1.6em;font-size:16px;line-height:1.9;color:#3f3f3f;">` +
|
|
1773
|
-
|
|
1964
|
+
`${listItems.map(item => `<li style="margin:6px 0;">${applyInlineMarkdown(item)}</li>`).join("")}</${listType}>`
|
|
1774
1965
|
);
|
|
1775
1966
|
listType = "";
|
|
1776
1967
|
listItems = [];
|
|
@@ -1783,7 +1974,7 @@ function markdownToHtml(markdown) {
|
|
|
1783
1974
|
|
|
1784
1975
|
html.push(
|
|
1785
1976
|
'<pre style="margin:24px 0;padding:16px;overflow:auto;border-radius:4px;background:#f5f7f8;">' +
|
|
1786
|
-
|
|
1977
|
+
`<code>${escapeHtml(codeLines.join("\n"))}</code></pre>`
|
|
1787
1978
|
);
|
|
1788
1979
|
inCodeBlock = false;
|
|
1789
1980
|
codeLines = [];
|
|
@@ -1822,18 +2013,18 @@ function markdownToHtml(markdown) {
|
|
|
1822
2013
|
"font-size:15px;line-height:1.7;overflow-wrap:anywhere;word-break:break-word;";
|
|
1823
2014
|
html.push(
|
|
1824
2015
|
'<div style="width:100%;margin:24px 0;overflow-x:auto;-webkit-overflow-scrolling:touch;">' +
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2016
|
+
'<table style="width:100%;min-width:560px;border-collapse:collapse;table-layout:auto;color:#3f3f3f;">' +
|
|
2017
|
+
`<thead><tr>${header
|
|
2018
|
+
.map(cell => `<th style="${cellStyle}background:#f5f7f8;font-weight:600;">${applyInlineMarkdown(cell)}</th>`)
|
|
2019
|
+
.join("")}</tr></thead>` +
|
|
2020
|
+
`<tbody>${body
|
|
2021
|
+
.map(
|
|
2022
|
+
row =>
|
|
2023
|
+
`<tr>${row
|
|
2024
|
+
.map(cell => `<td style="${cellStyle}">${applyInlineMarkdown(cell)}</td>`)
|
|
2025
|
+
.join("")}</tr>`
|
|
2026
|
+
)
|
|
2027
|
+
.join("")}</tbody></table></div>`
|
|
1837
2028
|
);
|
|
1838
2029
|
tableLines = [];
|
|
1839
2030
|
}
|
|
@@ -1896,7 +2087,7 @@ function markdownToHtml(markdown) {
|
|
|
1896
2087
|
const headingSize = level === 1 ? 24 : level === 2 ? 20 : 18;
|
|
1897
2088
|
html.push(
|
|
1898
2089
|
`<h${level} style="margin:32px 0 16px;font-size:${headingSize}px;line-height:1.5;font-weight:700;color:#24292f;overflow-wrap:anywhere;word-break:break-word;">` +
|
|
1899
|
-
|
|
2090
|
+
`${applyInlineMarkdown(headingMatch[2].trim())}</h${level}>`
|
|
1900
2091
|
);
|
|
1901
2092
|
continue;
|
|
1902
2093
|
}
|
|
@@ -2062,6 +2253,23 @@ async function parseArticleFile(file) {
|
|
|
2062
2253
|
}
|
|
2063
2254
|
|
|
2064
2255
|
const raw = await fs.promises.readFile(articleFile, "utf8");
|
|
2256
|
+
if (path.extname(articleFile).toLowerCase() === ".html") {
|
|
2257
|
+
const { meta, richHtml } = parseRichArticleUploadFile(raw);
|
|
2258
|
+
return {
|
|
2259
|
+
articleFile,
|
|
2260
|
+
payload: buildRichArticleUploadPayload(meta, richHtml, {
|
|
2261
|
+
normalizeText,
|
|
2262
|
+
normalizeMappedValue,
|
|
2263
|
+
normalizeGameAndCategory,
|
|
2264
|
+
normalizeBoolLike,
|
|
2265
|
+
statusMap: STATUS_MAP,
|
|
2266
|
+
areaMap: AREA_MAP,
|
|
2267
|
+
westLanguages: WEST_ARTICLE_LANGUAGES
|
|
2268
|
+
}),
|
|
2269
|
+
meta,
|
|
2270
|
+
markdown: ""
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2065
2273
|
const parsed = parseFrontMatter(raw);
|
|
2066
2274
|
return {
|
|
2067
2275
|
articleFile,
|
|
@@ -2094,7 +2302,7 @@ async function initArticleTemplate(file = "article.md", template = "article.md")
|
|
|
2094
2302
|
console.log("");
|
|
2095
2303
|
}
|
|
2096
2304
|
|
|
2097
|
-
async function importArticleCommand(sourceUrl, file = "article.
|
|
2305
|
+
async function importArticleCommand(sourceUrl, file = "article.html", options = {}) {
|
|
2098
2306
|
if (!sourceUrl) {
|
|
2099
2307
|
throw new Error("缺少文章来源链接,请提供微信公众号文章或飞书 Docx/Wiki 文档链接");
|
|
2100
2308
|
}
|
|
@@ -2124,24 +2332,37 @@ async function importArticleCommand(sourceUrl, file = "article.md", options = {}
|
|
|
2124
2332
|
if (options.rulesFile) {
|
|
2125
2333
|
console.log(chalk.gray(`已加载内容规则: ${contentRules.file}(${contentRules.rules.length} 条)`));
|
|
2126
2334
|
}
|
|
2335
|
+
const config = await loadConfig();
|
|
2336
|
+
const backendUrl = resolveAuthBackendUrl(options, config);
|
|
2337
|
+
const authState = await requireAuthState(backendUrl);
|
|
2127
2338
|
article = await importWechatArticle(sourceUrl, {
|
|
2128
2339
|
contentRules,
|
|
2129
2340
|
project: options.project,
|
|
2130
2341
|
platform: options.platform,
|
|
2342
|
+
uploadImage: image => uploadMediaToBackend(image, "image", backendUrl, authState.access_token),
|
|
2343
|
+
uploadVideo: video => uploadMediaToBackend(video, "video", backendUrl, authState.access_token),
|
|
2131
2344
|
onProgress: ({ mediaType = "image", current, total }) => {
|
|
2132
2345
|
const label = mediaType === "video" ? "正文视频" : "正文图片";
|
|
2133
2346
|
console.log(chalk.gray(`正在转存${label} ${current}/${total}`));
|
|
2134
2347
|
}
|
|
2135
2348
|
});
|
|
2136
2349
|
}
|
|
2137
|
-
const
|
|
2350
|
+
const projectMetadata = importProjectMetadata(options.project);
|
|
2351
|
+
if (projectMetadata) {
|
|
2352
|
+
article = { ...article, ...projectMetadata };
|
|
2353
|
+
}
|
|
2354
|
+
const output = options.format === "html" ? buildImportedArticleHtml(article) : buildOfficialArticleMarkdown(article);
|
|
2138
2355
|
await ensureDir(path.dirname(targetFile));
|
|
2139
2356
|
await fs.promises.writeFile(targetFile, output, { encoding: "utf8", flag: "wx" });
|
|
2140
2357
|
|
|
2141
2358
|
console.log("");
|
|
2142
|
-
console.log(chalk.green(`${sourceLabel}
|
|
2359
|
+
console.log(chalk.green(`${sourceLabel}已转换为${options.format === "html" ? "HTML 富文本文章" : "官网文章草稿"}:`));
|
|
2143
2360
|
console.log(targetFile);
|
|
2144
2361
|
console.log(chalk.gray(`标题: ${article.title}`));
|
|
2362
|
+
const contentSafety = article.contentSafety || analyzeArticleContentSafety(article);
|
|
2363
|
+
for (const warning of contentSafety.warnings) {
|
|
2364
|
+
console.log(chalk.yellow(`内容安全提示: ${warning}`));
|
|
2365
|
+
}
|
|
2145
2366
|
if (sourceLabel === "飞书文档") {
|
|
2146
2367
|
console.log(chalk.gray(`已保留图片: ${article.imageCount} 张,表格: ${article.tableCount} 个`));
|
|
2147
2368
|
console.log(chalk.gray("已保留标题、列表、引用、颜色、高亮、对齐、列宽及合并单元格"));
|
|
@@ -2171,7 +2392,13 @@ async function importArticleCommand(sourceUrl, file = "article.md", options = {}
|
|
|
2171
2392
|
);
|
|
2172
2393
|
}
|
|
2173
2394
|
console.log("");
|
|
2174
|
-
console.log(
|
|
2395
|
+
console.log(
|
|
2396
|
+
options.format === "html"
|
|
2397
|
+
? "可直接用于官网或各分发平台,平台规则会在发布时按原有流程处理:"
|
|
2398
|
+
: sourceLabel === "飞书文档"
|
|
2399
|
+
? "请补充 thumbnail、cate_id 等业务字段,确认内容后执行:"
|
|
2400
|
+
: "请补充 cate_id 等业务字段,确认内容后执行:"
|
|
2401
|
+
);
|
|
2175
2402
|
console.log("");
|
|
2176
2403
|
console.log(` qcplay-cli www-article-list.store ${file}`);
|
|
2177
2404
|
console.log("");
|
|
@@ -2234,9 +2461,9 @@ ${chalk.cyan("发布命令:")}
|
|
|
2234
2461
|
|
|
2235
2462
|
qcplay-cli www-article-list.store article.md
|
|
2236
2463
|
|
|
2237
|
-
${chalk.cyan("
|
|
2464
|
+
${chalk.cyan("从微信推文拉取 HTML 富文本:")}
|
|
2238
2465
|
|
|
2239
|
-
qcplay-cli article import "https://mp.weixin.qq.com/s/..." article.
|
|
2466
|
+
qcplay-cli article import "https://mp.weixin.qq.com/s/..." article.html
|
|
2240
2467
|
|
|
2241
2468
|
${chalk.cyan("项目默认分类:")}
|
|
2242
2469
|
|
|
@@ -2260,6 +2487,21 @@ function inferDistributionProject(payload, explicitProject, meta = {}) {
|
|
|
2260
2487
|
return "";
|
|
2261
2488
|
}
|
|
2262
2489
|
|
|
2490
|
+
function importProjectMetadata(project) {
|
|
2491
|
+
const normalizedProject = normalizeText(project);
|
|
2492
|
+
if (!normalizedProject) return null;
|
|
2493
|
+
const gameId = normalizeMappedValue(normalizedProject, GAME_MAP);
|
|
2494
|
+
const game = gameCatalogEntry(gameId);
|
|
2495
|
+
if (!game) {
|
|
2496
|
+
throw new Error(`未知项目: ${normalizedProject}`);
|
|
2497
|
+
}
|
|
2498
|
+
return {
|
|
2499
|
+
gameId,
|
|
2500
|
+
categoryId: game.defaultCategory,
|
|
2501
|
+
distributionProject: normalizedProject
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2263
2505
|
function printPlatformEntries(entries) {
|
|
2264
2506
|
console.log("");
|
|
2265
2507
|
console.log(chalk.cyan("可用发布平台:"));
|
|
@@ -2307,9 +2549,25 @@ async function publishPlatformsCommand(file, options) {
|
|
|
2307
2549
|
throw new Error("缺少文章文件,例如:qcplay-cli publish article.md --platform TapTap");
|
|
2308
2550
|
}
|
|
2309
2551
|
|
|
2310
|
-
|
|
2552
|
+
let selectedEntries = options.platforms.length
|
|
2311
2553
|
? selectRequestedEntries(candidates, options.platforms)
|
|
2312
2554
|
: await promptForPlatformEntries(candidates);
|
|
2555
|
+
if (options.draftOnly) {
|
|
2556
|
+
selectedEntries = selectedEntries.filter(entry => entry.platformKey !== "website");
|
|
2557
|
+
if (selectedEntries.length === 0) {
|
|
2558
|
+
throw new Error("--draft-only 仅适用于官网之外的游戏平台");
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (options.draftOnly && options.publishDraft) {
|
|
2562
|
+
throw new Error("--draft-only 与 --publish-draft 不能同时使用");
|
|
2563
|
+
}
|
|
2564
|
+
const needsActionSelection =
|
|
2565
|
+
!options.dryRun &&
|
|
2566
|
+
!options.openPage &&
|
|
2567
|
+
!options.switchAccount &&
|
|
2568
|
+
!options.draftOnly &&
|
|
2569
|
+
!(options.publishDraft && options.confirmed) &&
|
|
2570
|
+
!(options.directPublish && options.confirmed);
|
|
2313
2571
|
if (options.openPage) {
|
|
2314
2572
|
if (selectedEntries.some(entry => entry.publisher !== "browser")) {
|
|
2315
2573
|
throw new Error("--open-page 仅支持有真实浏览器发布页的平台,请使用 --platform 指定浏览器平台");
|
|
@@ -2359,8 +2617,11 @@ async function publishPlatformsCommand(file, options) {
|
|
|
2359
2617
|
const matchedRules = matchingContentRules(contentRules, planningArticle, entry).map(rule => rule.name);
|
|
2360
2618
|
return {
|
|
2361
2619
|
...publicEntrySummary(entry),
|
|
2620
|
+
...(options.draftOnly ? { mode: "draft-only" } : {}),
|
|
2362
2621
|
...(matchedRules.length > 0 ? { contentRules: matchedRules } : {}),
|
|
2363
|
-
...(entry.platformKey === "taptap"
|
|
2622
|
+
...(entry.platformKey === "taptap"
|
|
2623
|
+
? { taptap: { ...tapTapPublishingPlan(planningArticle, entry), ...(options.draftOnly ? { draft: true } : {}) } }
|
|
2624
|
+
: {}),
|
|
2364
2625
|
...(entry.platformKey === "bilibili" ? { bilibili: bilibiliPublishingPlan(planningArticle, entry) } : {}),
|
|
2365
2626
|
...(entry.platformKey === "weibo" ? { weibo: weiboPublishingPlan(planningArticle, entry) } : {})
|
|
2366
2627
|
};
|
|
@@ -2379,7 +2640,17 @@ async function publishPlatformsCommand(file, options) {
|
|
|
2379
2640
|
meta: parsedArticle.meta,
|
|
2380
2641
|
platformInputs
|
|
2381
2642
|
};
|
|
2382
|
-
const
|
|
2643
|
+
const contentSafety = analyzeArticleContentSafety(article);
|
|
2644
|
+
for (const warning of contentSafety.warnings) {
|
|
2645
|
+
console.log(chalk.yellow(`内容安全提示: ${warning}`));
|
|
2646
|
+
}
|
|
2647
|
+
const browserEntries = selected.filter(entry => entry.publisher === "browser");
|
|
2648
|
+
// Each browser platform needs its own explicit AI-conversation authorisation.
|
|
2649
|
+
// Preparing every selected editor before asking keeps the user waiting behind
|
|
2650
|
+
// long image uploads on later platforms and leaves no safe action boundary.
|
|
2651
|
+
const executionEntries = needsActionSelection ? browserEntries.slice(0, 1) : selected;
|
|
2652
|
+
const pendingApprovalEntries = needsActionSelection ? browserEntries.slice(1) : [];
|
|
2653
|
+
const { successes, failures, skipped } = await runPlatformPublishSequence(executionEntries, async entry => {
|
|
2383
2654
|
console.log("");
|
|
2384
2655
|
console.log(chalk.cyan(`正在发布到 ${entry.platform}(${entry.region})...`));
|
|
2385
2656
|
let result;
|
|
@@ -2388,12 +2659,34 @@ async function publishPlatformsCommand(file, options) {
|
|
|
2388
2659
|
} else {
|
|
2389
2660
|
result = await publishPlatformEntry(entry, article, {
|
|
2390
2661
|
browserChannel: options.browserChannel,
|
|
2391
|
-
reviewDraft: options.reviewDraft,
|
|
2662
|
+
reviewDraft: needsActionSelection || options.reviewDraft,
|
|
2663
|
+
// A review is always approved in the current AI conversation. Never
|
|
2664
|
+
// fall back to the legacy terminal confirmation path.
|
|
2665
|
+
awaitAIAction: needsActionSelection || options.reviewDraft,
|
|
2666
|
+
// The initial AI-authorisation preview prepares the complete native
|
|
2667
|
+
// editor draft, including body media. It still never clicks save or
|
|
2668
|
+
// publish; only the later explicit action may submit it. A platform
|
|
2669
|
+
// must not reach the action selection while any image is still
|
|
2670
|
+
// uploading, including editors that expose their fields after upload.
|
|
2671
|
+
previewTextOnly: false,
|
|
2672
|
+
directPublish: options.directPublish && options.confirmed,
|
|
2673
|
+
draftOnly: options.draftOnly,
|
|
2674
|
+
publishDraft: options.publishDraft,
|
|
2392
2675
|
keepOpen: options.keepOpen,
|
|
2393
2676
|
onProgress: message => console.log(chalk.gray(` ${message}`))
|
|
2394
2677
|
});
|
|
2395
|
-
if (
|
|
2396
|
-
console.log(chalk.green(
|
|
2678
|
+
if (result?.status === "draft") {
|
|
2679
|
+
console.log(chalk.green(`${entry.platform} 草稿已保存,浏览器保持打开`));
|
|
2680
|
+
if (result.approvalRequired) {
|
|
2681
|
+
console.log(
|
|
2682
|
+
JSON.stringify({
|
|
2683
|
+
event: "qcplay.draft-approval-required",
|
|
2684
|
+
platform: entry.platform,
|
|
2685
|
+
title: article.title,
|
|
2686
|
+
actions: result.approvalActions || ["publish", "cancel"]
|
|
2687
|
+
})
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2397
2690
|
return result;
|
|
2398
2691
|
}
|
|
2399
2692
|
if (entry.platformKey === "taptap" && result?.status === "scheduled") {
|
|
@@ -2407,6 +2700,42 @@ async function publishPlatformsCommand(file, options) {
|
|
|
2407
2700
|
failures.forEach(({ entry, error }) => {
|
|
2408
2701
|
console.error(chalk.red(`${entry.platform} 发布失败: ${error.message || error}`));
|
|
2409
2702
|
});
|
|
2703
|
+
|
|
2704
|
+
if (needsActionSelection) {
|
|
2705
|
+
if (failures.length > 0) {
|
|
2706
|
+
throw new Error("待授权预览准备失败,请根据上面的错误处理后重试失败平台");
|
|
2707
|
+
}
|
|
2708
|
+
const pages = successes
|
|
2709
|
+
.map(({ entry, result }) => ({ platform: entry.platform, url: result?.url || "" }))
|
|
2710
|
+
.filter(item => item.url);
|
|
2711
|
+
const previewResults = successes.map(({ result }) => result || {});
|
|
2712
|
+
const imagesPending = previewResults.reduce((sum, result) => sum + Number(result.imagesPending || 0), 0);
|
|
2713
|
+
const requiresMediaBeforeText = previewResults.some(result => result.requiresMediaBeforeText);
|
|
2714
|
+
const contentPrepared = previewResults.every(result => result.contentPrepared !== false) && imagesPending === 0;
|
|
2715
|
+
if (!contentPrepared || requiresMediaBeforeText) {
|
|
2716
|
+
throw new Error("编辑器尚未完成正文或图片回读校验,暂不提供发布授权选项");
|
|
2717
|
+
}
|
|
2718
|
+
console.log(
|
|
2719
|
+
JSON.stringify({
|
|
2720
|
+
event: "qcplay.publish-action-required",
|
|
2721
|
+
title: previewResults.find(result => normalizeText(result.title))?.title || article.title,
|
|
2722
|
+
platforms: executionEntries.map(entry => entry.platform),
|
|
2723
|
+
...(pendingApprovalEntries.length > 0
|
|
2724
|
+
? { pendingPlatforms: pendingApprovalEntries.map(entry => entry.platform) }
|
|
2725
|
+
: {}),
|
|
2726
|
+
pages,
|
|
2727
|
+
contentPrepared,
|
|
2728
|
+
imagesPending,
|
|
2729
|
+
actions: ["publish", "save_draft", "cancel"]
|
|
2730
|
+
})
|
|
2731
|
+
);
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
const urlsFile = await writePlatformPublishUrls(file, successes);
|
|
2736
|
+
if (urlsFile) {
|
|
2737
|
+
console.log(chalk.green(`平台地址已写入: ${urlsFile}`));
|
|
2738
|
+
}
|
|
2410
2739
|
if (skipped.length > 0) {
|
|
2411
2740
|
console.error(chalk.yellow("已停止后续平台发布,请先处理当前浏览器平台的登录或页面问题。"));
|
|
2412
2741
|
}
|
|
@@ -2543,11 +2872,20 @@ async function updateArticleStatusCommand(articleIdValue, targetStatus, options,
|
|
|
2543
2872
|
|
|
2544
2873
|
const response = await authenticatedRequest("PATCH", backendUrl, articleApiPath(target, "status"), payload);
|
|
2545
2874
|
const responseStatus = normalizeText(response.data?.status) || targetStatus;
|
|
2875
|
+
const recentState = await readJson(ARTICLE_STATE_FILE);
|
|
2876
|
+
const recentStateMatches =
|
|
2877
|
+
String(recentState.article_id || "") === String(articleId) &&
|
|
2878
|
+
(recentState.target || "domestic") === target;
|
|
2879
|
+
const previewPayload = recentStateMatches
|
|
2880
|
+
? { game_id: recentState.game_id, cate_id: recentState.cate_id }
|
|
2881
|
+
: {};
|
|
2546
2882
|
const previewUrl =
|
|
2547
2883
|
responseStatus === "1"
|
|
2548
|
-
? normalizeText(response.data?.preview_url) ||
|
|
2884
|
+
? normalizeText(response.data?.preview_url) ||
|
|
2885
|
+
(recentStateMatches ? normalizeText(recentState.preview_url) : "") ||
|
|
2886
|
+
(target === "domestic" ? articlePreviewUrl(articleId, previewPayload) : "")
|
|
2549
2887
|
: "";
|
|
2550
|
-
const stateWarning = await trySaveRecentArticleState(articleId, responseStatus, backendUrl, previewUrl, target);
|
|
2888
|
+
const stateWarning = await trySaveRecentArticleState(articleId, responseStatus, backendUrl, previewUrl, target, previewPayload);
|
|
2551
2889
|
|
|
2552
2890
|
console.log(chalk.green(response.message || (responseStatus === "2" ? "文章已删除" : "文章已上线")));
|
|
2553
2891
|
console.log(`文章 ID: ${articleId}`);
|
|
@@ -2636,6 +2974,26 @@ async function main() {
|
|
|
2636
2974
|
return;
|
|
2637
2975
|
}
|
|
2638
2976
|
|
|
2977
|
+
if (command === "wechat") {
|
|
2978
|
+
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
2979
|
+
printWechatHelp();
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
if (subcommand !== "article") {
|
|
2983
|
+
await runWithErrorBanner("微信公众号数据失败", async () => {
|
|
2984
|
+
throw new Error(`未知 wechat 命令: ${subcommand}`);
|
|
2985
|
+
});
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
if (rest.includes("-h") || rest.includes("--help")) {
|
|
2989
|
+
printWechatHelp();
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
const parsed = parseWechatArticleOptions(rest);
|
|
2993
|
+
await runWithErrorBanner("微信公众号数据失败", () => importWechatDataCommand(parsed));
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2639
2997
|
if (command === "article") {
|
|
2640
2998
|
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
2641
2999
|
if (!subcommand) {
|
|
@@ -2659,6 +3017,10 @@ async function main() {
|
|
|
2659
3017
|
}
|
|
2660
3018
|
|
|
2661
3019
|
if (subcommand === "import") {
|
|
3020
|
+
if (rest.length === 1 && ["--help", "-h"].includes(rest[0])) {
|
|
3021
|
+
printArticleHelp();
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
2662
3024
|
const parsed = parseArticleImportOptions(rest);
|
|
2663
3025
|
await runWithErrorBanner("转换失败", () =>
|
|
2664
3026
|
importArticleCommand(parsed.sourceUrl, parsed.file, parsed.options)
|