@qcplay/cli 1.0.14 → 1.0.15
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 +200 -9
- package/bin/qcplay.js +552 -77
- package/lib/content-rules.js +807 -0
- package/lib/lark-article.js +1 -1
- package/lib/platform-publish.js +2414 -270
- package/lib/wechat-article.js +400 -25
- package/package.json +4 -1
- package/templates/skills/qcplay-publish-article/SKILL.md +38 -4
- package/templates/skills/qcplay-publish-article/article/article.md +3 -1
- package/templates/skills/qcplay-publish-article/article/west-article.md +22 -0
package/bin/qcplay.js
CHANGED
|
@@ -16,11 +16,20 @@ import {
|
|
|
16
16
|
sanitizeArticleRichHtml
|
|
17
17
|
} from "../lib/wechat-article.js";
|
|
18
18
|
import { importLarkArticle, parseLarkDocumentUrl } from "../lib/lark-article.js";
|
|
19
|
+
import {
|
|
20
|
+
CONTENT_RULES_TEMPLATE,
|
|
21
|
+
DEFAULT_CONTENT_RULES_FILE,
|
|
22
|
+
applyContentRules,
|
|
23
|
+
loadContentRules,
|
|
24
|
+
matchingContentRules
|
|
25
|
+
} from "../lib/content-rules.js";
|
|
19
26
|
import {
|
|
20
27
|
availableProjects,
|
|
21
28
|
bilibiliPublishingPlan,
|
|
22
29
|
entriesForProject,
|
|
23
30
|
loadPlatformEntries,
|
|
31
|
+
openPublishPage,
|
|
32
|
+
prepareArticleForPlatform,
|
|
24
33
|
promptForPlatformEntries,
|
|
25
34
|
publicEntrySummary,
|
|
26
35
|
publishPlatformEntry,
|
|
@@ -52,6 +61,48 @@ function resolveAuthPagePath() {
|
|
|
52
61
|
return path.resolve(__dirname, "../web/auth.html");
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
const GAME_CATALOG = {
|
|
65
|
+
"39": {
|
|
66
|
+
names: ["39", "最强蜗牛"],
|
|
67
|
+
defaultCategory: "2",
|
|
68
|
+
categories: { "2": "综合", "3": "活动", "7": "游戏攻略", "8": "视频中心", "9": "萌新入门", "25": "萌新入门-攻略专区", "26": "高手进阶", "27": "活动攻略", "29": "视频攻略" }
|
|
69
|
+
},
|
|
70
|
+
"41": {
|
|
71
|
+
names: ["41", "阿瑞斯病毒2", "阿瑞斯病毒"],
|
|
72
|
+
defaultCategory: "2",
|
|
73
|
+
categories: { "2": "综合", "3": "活动", "7": "游戏攻略", "8": "视频中心", "9": "萌新入门", "25": "萌新入门-攻略专区", "26": "高手进阶", "27": "活动攻略", "29": "视频攻略" }
|
|
74
|
+
},
|
|
75
|
+
"69": {
|
|
76
|
+
names: ["69", "新仙剑", "新仙剑奇侠传之挥剑问情手游"],
|
|
77
|
+
defaultCategory: "61",
|
|
78
|
+
categories: { "61": "全部", "62": "活动", "63": "公告" }
|
|
79
|
+
},
|
|
80
|
+
"33": {
|
|
81
|
+
names: ["33", "提灯与地下城"],
|
|
82
|
+
defaultCategory: "40",
|
|
83
|
+
categories: { "40": "新闻", "41": "公告", "42": "活动" }
|
|
84
|
+
},
|
|
85
|
+
"74": {
|
|
86
|
+
names: ["74", "魔卡少女樱", "魔卡少女樱(情报站)", "魔卡少女樱情报站", "魔卡少女樱:回忆钥匙"],
|
|
87
|
+
defaultCategory: "71",
|
|
88
|
+
categories: { "71": "最新", "72": "公告", "73": "活动" }
|
|
89
|
+
},
|
|
90
|
+
"50": {
|
|
91
|
+
names: ["50", "迷途之光"],
|
|
92
|
+
defaultCategory: "109",
|
|
93
|
+
categories: { "109": "全部", "110": "公告", "111": "活动" }
|
|
94
|
+
},
|
|
95
|
+
"64": {
|
|
96
|
+
names: ["64", "最强蜗牛欧美", "最强蜗牛(欧美)", "最强蜗牛国际服"],
|
|
97
|
+
defaultCategory: "2",
|
|
98
|
+
categories: { "2": "综合", "3": "活动", "7": "游戏攻略", "8": "视频中心", "9": "萌新入门", "25": "萌新入门-攻略专区", "26": "高手进阶", "27": "活动攻略", "29": "视频攻略" }
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const GAME_MAP = Object.fromEntries(
|
|
103
|
+
Object.entries(GAME_CATALOG).flatMap(([gameId, config]) => config.names.map(name => [name, gameId]))
|
|
104
|
+
);
|
|
105
|
+
|
|
55
106
|
const CATEGORY_MAP = {
|
|
56
107
|
"2": "2",
|
|
57
108
|
"3": "3",
|
|
@@ -73,6 +124,13 @@ const CATEGORY_MAP = {
|
|
|
73
124
|
"视频攻略": "29"
|
|
74
125
|
};
|
|
75
126
|
|
|
127
|
+
for (const config of Object.values(GAME_CATALOG)) {
|
|
128
|
+
for (const [id, name] of Object.entries(config.categories)) {
|
|
129
|
+
CATEGORY_MAP[id] = id;
|
|
130
|
+
if (!CATEGORY_MAP[name]) CATEGORY_MAP[name] = id;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
76
134
|
const AREA_MAP = {
|
|
77
135
|
"1": "1",
|
|
78
136
|
"3": "3",
|
|
@@ -83,11 +141,6 @@ const AREA_MAP = {
|
|
|
83
141
|
"公益网站": "4"
|
|
84
142
|
};
|
|
85
143
|
|
|
86
|
-
const GAME_MAP = {
|
|
87
|
-
"39": "39",
|
|
88
|
-
"最强蜗牛": "39"
|
|
89
|
-
};
|
|
90
|
-
|
|
91
144
|
const STATUS_MAP = {
|
|
92
145
|
"0": "0",
|
|
93
146
|
"1": "1",
|
|
@@ -114,8 +167,16 @@ const AGENTS_DIR = path.join(os.homedir(), ".agents");
|
|
|
114
167
|
const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
|
|
115
168
|
const AUTH_FILE = path.join(QCPLAY_DIR, "auth.json");
|
|
116
169
|
const ARTICLE_STATE_FILE = path.join(QCPLAY_DIR, "article-state.json");
|
|
170
|
+
const CONTENT_RULES_FILE = DEFAULT_CONTENT_RULES_FILE;
|
|
117
171
|
const ARTICLE_PREVIEW_BASE_URL = "https://snail.qingcigame.com/official/news-details.html";
|
|
118
172
|
const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
173
|
+
const WEST_ARTICLE_LANGUAGES = new Set(["portuguese", "german", "spanish", "italian", "french", "america_en"]);
|
|
174
|
+
const ARTICLE_PREVIEW_URLS = {
|
|
175
|
+
"50": "https://mtzg.qingcijoy.com/news-details.html",
|
|
176
|
+
"69": "https://hjwq.qingcijoy.com/news-details.html",
|
|
177
|
+
"74": "https://sakura.qingcijoy.com/news-details.html",
|
|
178
|
+
"64": "https://snail-na.qcplay.com/official/news-details-mobile.html"
|
|
179
|
+
};
|
|
119
180
|
|
|
120
181
|
function readPackageMetadata() {
|
|
121
182
|
try {
|
|
@@ -163,15 +224,20 @@ Usage:
|
|
|
163
224
|
qcplay-cli auth permissions [--local] [--key <key>] [--json]
|
|
164
225
|
qcplay-cli article
|
|
165
226
|
qcplay-cli article init [file]
|
|
166
|
-
qcplay-cli article
|
|
167
|
-
qcplay-cli article
|
|
227
|
+
qcplay-cli article west-init [file]
|
|
228
|
+
qcplay-cli article import <wechat-or-feishu-url> [file] [--rules-file <json|txt|docx>] [--project <项目>] [--platform <平台>]
|
|
229
|
+
qcplay-cli article publish <file> [--title <标题>] [--cover <图片>] [--project <项目>] [--region <区域>] [--rules-file <json>] [--dry-run]
|
|
168
230
|
qcplay-cli article update [id] [--local] [--backend <url>] [--dry-run]
|
|
169
231
|
qcplay-cli article delete [id] [--local] [--backend <url>] [--dry-run]
|
|
170
|
-
qcplay-cli publish [file] [--project <项目>] [--region <区域>] [--platform <平台>]
|
|
232
|
+
qcplay-cli publish [file] [--title <标题>] [--cover <图片>] [--weibo-column <专栏>] [--project <项目>] [--region <区域>] [--platform <平台>]
|
|
171
233
|
qcplay-cli publish --project <项目> --list-platforms
|
|
172
|
-
qcplay-cli
|
|
234
|
+
qcplay-cli rules [init|validate|list] [--rules-file <json>]
|
|
235
|
+
qcplay-cli www-article-list.store <file> [--title <标题>] [--cover <图片>] [--project <项目>] [--region <区域>] [--rules-file <json>] [--dry-run]
|
|
173
236
|
qcplay-cli www-article-list.update [id] [--local] [--backend <url>] [--dry-run]
|
|
174
237
|
qcplay-cli www-article-list.delete [id] [--local] [--backend <url>] [--dry-run]
|
|
238
|
+
qcplay-cli west-article-list.store <file> [--title <标题>] [--cover <图片>] [--project <项目>] [--region <区域>] [--rules-file <json>] [--dry-run]
|
|
239
|
+
qcplay-cli west-article-list.update [id] [--local] [--backend <url>] [--dry-run]
|
|
240
|
+
qcplay-cli west-article-list.delete [id] [--local] [--backend <url>] [--dry-run]
|
|
175
241
|
qcplay-cli features
|
|
176
242
|
qcplay-cli where
|
|
177
243
|
|
|
@@ -196,32 +262,64 @@ function printArticleHelp() {
|
|
|
196
262
|
console.log(`Usage:
|
|
197
263
|
qcplay-cli article
|
|
198
264
|
qcplay-cli article init [file]
|
|
199
|
-
qcplay-cli article
|
|
200
|
-
qcplay-cli article
|
|
265
|
+
qcplay-cli article west-init [file]
|
|
266
|
+
qcplay-cli article import <wechat-or-feishu-url> [file] [--rules-file <json>] [--project <项目>]
|
|
267
|
+
qcplay-cli article publish <file> [--title <标题>] [--cover <图片>] [--project <项目>] [--region <区域>] [--rules-file <json>] [--dry-run]
|
|
201
268
|
qcplay-cli article update [id] [--local] [--backend <url>] [--dry-run]
|
|
202
269
|
qcplay-cli article delete [id] [--local] [--backend <url>] [--dry-run]`);
|
|
203
270
|
}
|
|
204
271
|
|
|
272
|
+
function parseArticleImportOptions(args) {
|
|
273
|
+
const positional = [];
|
|
274
|
+
const options = { rulesFile: "", project: "", platform: "" };
|
|
275
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
276
|
+
const current = args[index];
|
|
277
|
+
if (current === "--rules-file" || current === "--project" || current === "--platform") {
|
|
278
|
+
const next = args[index + 1];
|
|
279
|
+
if (!next || next.startsWith("--")) throw new Error(`${current} 缺少参数值`);
|
|
280
|
+
options[current === "--rules-file" ? "rulesFile" : current === "--project" ? "project" : "platform"] = next;
|
|
281
|
+
index += 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (current.startsWith("--")) throw new Error(`未知参数: ${current}`);
|
|
285
|
+
positional.push(current);
|
|
286
|
+
}
|
|
287
|
+
if (positional.length > 2) throw new Error(`未知参数: ${positional.slice(2).join(" ")}`);
|
|
288
|
+
return { sourceUrl: positional[0], file: positional[1] || "article.md", options };
|
|
289
|
+
}
|
|
290
|
+
|
|
205
291
|
function printPublishHelp() {
|
|
206
292
|
console.log(`Usage:
|
|
207
|
-
qcplay-cli publish <file> --project <项目> [--region <区域>] [--platform <平台> ...]
|
|
293
|
+
qcplay-cli publish <file> --project <项目> [--title <标题>] [--cover <图片>] [--weibo-column <专栏>] [--region <区域>] [--platform <平台> ...]
|
|
208
294
|
qcplay-cli publish --project <项目> [--region <区域>] --list-platforms
|
|
295
|
+
qcplay-cli publish --project <项目> --platform <平台> --open-page
|
|
209
296
|
|
|
210
297
|
Options:
|
|
211
298
|
--project <项目> 项目名称;game_id=39 时默认使用“最强蜗牛”
|
|
212
299
|
--region <区域> 国内、日本、欧美等区域
|
|
300
|
+
--title <标题> 本次发布临时替换标题,不改动 Markdown 文件
|
|
301
|
+
--cover <图片> 本次发布临时替换封面;浏览器平台支持本地路径或 URL,官网使用图片 URL
|
|
302
|
+
--weibo-column <专栏> 仅微博:设置头条文章专栏;不填写则不设置专栏
|
|
213
303
|
-p, --platform <平台> 发布平台,可重复使用或用逗号分隔
|
|
214
304
|
--list-platforms 列出匹配的平台,不发布
|
|
215
305
|
--config-file <json> 使用本地平台配置(主要用于测试)
|
|
306
|
+
--rules-file <json> 使用指定内容规则;默认 ~/.qcplay/platform-rules.json
|
|
216
307
|
--browser-channel <name> Playwright 浏览器通道,Windows 默认 msedge
|
|
217
308
|
--review-draft, --review 官网外平台先准备草稿,预览后在 CMD 确认发布
|
|
309
|
+
--open-page 只打开真实浏览器发布页,供登录和页面流程测试,不读取或提交文章
|
|
310
|
+
--switch-account 先打开发布页切换账号,确认后继续当前发布流程
|
|
218
311
|
--keep-open 浏览器提交后等待用户确认再关闭
|
|
219
312
|
--dry-run 只展示发布计划,不登录或发布
|
|
220
313
|
--local 官网发布使用本地后端
|
|
221
314
|
--backend <url> 官网发布后端地址
|
|
222
315
|
|
|
316
|
+
TapTap command inputs:
|
|
317
|
+
--taptap-secret-code <密令> 飞书“特殊要求”要求的本次福利密令
|
|
318
|
+
--taptap-secret-period <有效期> 本次密令生效时间或有效期
|
|
319
|
+
--taptap-official-group <群号> 仅当飞书群号为动态占位符时提供
|
|
320
|
+
|
|
223
321
|
TapTap Front Matter:
|
|
224
|
-
taptap_type long-post
|
|
322
|
+
taptap_type 留空时跟随飞书页面 URL,也可指定 long-post、image-text 或 video
|
|
225
323
|
taptap_forum 默认使用当前项目同名论坛;填 0 时不添加论坛
|
|
226
324
|
taptap_scheduled 填 1 时勾选定时发布
|
|
227
325
|
taptap_draft 填 1 时保存草稿而非发布
|
|
@@ -238,7 +336,78 @@ B站 Front Matter:
|
|
|
238
336
|
bilibili_topic 投稿话题;填写后搜索并选择精确同名话题
|
|
239
337
|
|
|
240
338
|
微博 Front Matter:
|
|
241
|
-
weibo_intro 头条文章导语,最多 44 个字符;默认使用 article_excerpt
|
|
339
|
+
weibo_intro 头条文章导语,最多 44 个字符;默认使用 article_excerpt
|
|
340
|
+
weibo_super_topic 可选;自定义超话名称
|
|
341
|
+
weibo_cover 头条文章封面;可填写正文图片或本地路径/URL
|
|
342
|
+
weibo_column 可选;微博头条文章专栏,留空则不设置
|
|
343
|
+
|
|
344
|
+
小红书 Front Matter:
|
|
345
|
+
xiaohongshu_images 可选;本地路径或 URL,多个以逗号分隔;留空时使用正文第 1 张图片
|
|
346
|
+
xiaohongshu_collection 可选;精确合集名称,留空则不加入合集`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function printRulesHelp() {
|
|
350
|
+
console.log(`Usage:
|
|
351
|
+
qcplay-cli rules init [--rules-file <json>]
|
|
352
|
+
qcplay-cli rules validate [--rules-file <json|txt|docx>]
|
|
353
|
+
qcplay-cli rules list [--rules-file <json|txt|docx>]
|
|
354
|
+
|
|
355
|
+
默认规则文件: ${CONTENT_RULES_FILE}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function parseRulesOptions(args) {
|
|
359
|
+
const options = { rulesFile: "" };
|
|
360
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
361
|
+
const current = args[index];
|
|
362
|
+
if (current === "--rules-file") {
|
|
363
|
+
const next = args[index + 1];
|
|
364
|
+
if (!next || next.startsWith("--")) throw new Error("--rules-file 缺少参数值");
|
|
365
|
+
options.rulesFile = next;
|
|
366
|
+
index += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
throw new Error(`未知参数: ${current}`);
|
|
370
|
+
}
|
|
371
|
+
return options;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function contentRulesCommand(action, args = []) {
|
|
375
|
+
const options = parseRulesOptions(args);
|
|
376
|
+
const file = path.resolve(options.rulesFile || CONTENT_RULES_FILE);
|
|
377
|
+
if (action === "init") {
|
|
378
|
+
if (await pathExists(file)) throw new Error(`内容规则文件已存在: ${file}`);
|
|
379
|
+
await ensureDir(path.dirname(file));
|
|
380
|
+
await fs.promises.writeFile(file, `${JSON.stringify(CONTENT_RULES_TEMPLATE, null, 2)}\n`, {
|
|
381
|
+
encoding: "utf8",
|
|
382
|
+
mode: 0o600
|
|
383
|
+
});
|
|
384
|
+
console.log(chalk.green("内容规则模板已创建"));
|
|
385
|
+
console.log(file);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const config = await loadContentRules(file);
|
|
389
|
+
if (action === "validate") {
|
|
390
|
+
console.log(chalk.green(`内容规则校验通过,共 ${config.rules.length} 条`));
|
|
391
|
+
console.log(file);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (action === "list") {
|
|
395
|
+
console.log(JSON.stringify({
|
|
396
|
+
file,
|
|
397
|
+
version: config.version,
|
|
398
|
+
rules: config.rules.map(rule => ({
|
|
399
|
+
name: rule.name,
|
|
400
|
+
enabled: rule.enabled !== false,
|
|
401
|
+
source: rule.source || "*",
|
|
402
|
+
project: rule.project || "*",
|
|
403
|
+
region: rule.region || "*",
|
|
404
|
+
platform: rule.platform || "*",
|
|
405
|
+
actions: rule.actions.map(action => action.type)
|
|
406
|
+
}))
|
|
407
|
+
}, null, 2));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
throw new Error(`未知 rules 命令: ${action || "空"}`);
|
|
242
411
|
}
|
|
243
412
|
|
|
244
413
|
function printFeatures() {
|
|
@@ -331,6 +500,16 @@ async function ensureLocalDirs() {
|
|
|
331
500
|
await ensureDir(SKILLS_DIR);
|
|
332
501
|
}
|
|
333
502
|
|
|
503
|
+
async function ensureContentRulesFile() {
|
|
504
|
+
if (await pathExists(CONTENT_RULES_FILE)) return false;
|
|
505
|
+
await ensureDir(path.dirname(CONTENT_RULES_FILE));
|
|
506
|
+
await fs.promises.writeFile(CONTENT_RULES_FILE, `${JSON.stringify(CONTENT_RULES_TEMPLATE, null, 2)}\n`, {
|
|
507
|
+
encoding: "utf8",
|
|
508
|
+
mode: 0o600
|
|
509
|
+
});
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
334
513
|
async function filesAreEqual(sourcePath, targetPath) {
|
|
335
514
|
try {
|
|
336
515
|
const [sourceContent, targetContent] = await Promise.all([
|
|
@@ -440,6 +619,28 @@ function normalizeMappedValue(value, map) {
|
|
|
440
619
|
return map[text] ?? map[text.toLowerCase?.() || ""] ?? text;
|
|
441
620
|
}
|
|
442
621
|
|
|
622
|
+
function gameCatalogEntry(gameId) {
|
|
623
|
+
return GAME_CATALOG[gameId] || null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function normalizeGameAndCategory(meta) {
|
|
627
|
+
const rawGame = normalizeText(meta.game_id);
|
|
628
|
+
const gameId = normalizeMappedValue(rawGame, GAME_MAP) || "39";
|
|
629
|
+
const game = gameCatalogEntry(gameId);
|
|
630
|
+
if (!game) {
|
|
631
|
+
throw new Error(`未知 game_id: ${gameId}`);
|
|
632
|
+
}
|
|
633
|
+
const rawCategory = normalizeText(meta.cate_id);
|
|
634
|
+
const category = rawCategory
|
|
635
|
+
? Object.entries(game.categories).find(([, name]) => name === rawCategory)?.[0] || normalizeMappedValue(rawCategory, CATEGORY_MAP)
|
|
636
|
+
: game.defaultCategory;
|
|
637
|
+
if (!game.categories[category]) {
|
|
638
|
+
const allowed = Object.entries(game.categories).map(([id, name]) => `${id}(${name})`).join("、");
|
|
639
|
+
throw new Error(`项目“${game.names.at(-1)}”的 cate_id 不合法:当前允许 ${allowed}`);
|
|
640
|
+
}
|
|
641
|
+
return { gameId, category };
|
|
642
|
+
}
|
|
643
|
+
|
|
443
644
|
function normalizeBoolLike(value, defaultValue = "0") {
|
|
444
645
|
const text = normalizeText(value);
|
|
445
646
|
if (!text) {
|
|
@@ -531,7 +732,12 @@ function parsePublishOptions(args) {
|
|
|
531
732
|
const options = {
|
|
532
733
|
backend: undefined,
|
|
533
734
|
local: false,
|
|
534
|
-
dryRun: false
|
|
735
|
+
dryRun: false,
|
|
736
|
+
project: "",
|
|
737
|
+
region: "",
|
|
738
|
+
rulesFile: "",
|
|
739
|
+
title: "",
|
|
740
|
+
cover: ""
|
|
535
741
|
};
|
|
536
742
|
const positional = [];
|
|
537
743
|
|
|
@@ -543,13 +749,21 @@ function parsePublishOptions(args) {
|
|
|
543
749
|
continue;
|
|
544
750
|
}
|
|
545
751
|
|
|
546
|
-
if (
|
|
752
|
+
if (["--backend", "--project", "--region", "--rules-file", "--title", "--cover"].includes(current)) {
|
|
547
753
|
const next = args[index + 1];
|
|
548
754
|
if (!next || next.startsWith("--")) {
|
|
549
|
-
throw new Error(
|
|
755
|
+
throw new Error(`${current} 缺少参数值`);
|
|
550
756
|
}
|
|
551
757
|
|
|
552
|
-
|
|
758
|
+
const key = {
|
|
759
|
+
"--backend": "backend",
|
|
760
|
+
"--project": "project",
|
|
761
|
+
"--region": "region",
|
|
762
|
+
"--rules-file": "rulesFile",
|
|
763
|
+
"--title": "title",
|
|
764
|
+
"--cover": "cover"
|
|
765
|
+
}[current];
|
|
766
|
+
options[key] = next;
|
|
553
767
|
index += 1;
|
|
554
768
|
continue;
|
|
555
769
|
}
|
|
@@ -586,9 +800,18 @@ function parseDistributionOptions(args) {
|
|
|
586
800
|
platforms: [],
|
|
587
801
|
listPlatforms: false,
|
|
588
802
|
configFile: "",
|
|
803
|
+
rulesFile: "",
|
|
804
|
+
title: "",
|
|
805
|
+
cover: "",
|
|
806
|
+
weiboColumn: "",
|
|
589
807
|
browserChannel: "",
|
|
590
808
|
reviewDraft: false,
|
|
591
|
-
keepOpen: false
|
|
809
|
+
keepOpen: false,
|
|
810
|
+
openPage: false,
|
|
811
|
+
switchAccount: false,
|
|
812
|
+
taptapSecretCode: "",
|
|
813
|
+
taptapSecretPeriod: "",
|
|
814
|
+
taptapOfficialGroup: ""
|
|
592
815
|
};
|
|
593
816
|
const positional = [];
|
|
594
817
|
|
|
@@ -610,6 +833,14 @@ function parseDistributionOptions(args) {
|
|
|
610
833
|
options.keepOpen = true;
|
|
611
834
|
continue;
|
|
612
835
|
}
|
|
836
|
+
if (current === "--open-page") {
|
|
837
|
+
options.openPage = true;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (current === "--switch-account") {
|
|
841
|
+
options.switchAccount = true;
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
613
844
|
if (current === "--review-draft" || current === "--review") {
|
|
614
845
|
options.reviewDraft = true;
|
|
615
846
|
continue;
|
|
@@ -620,7 +851,14 @@ function parseDistributionOptions(args) {
|
|
|
620
851
|
["--project", "project"],
|
|
621
852
|
["--region", "region"],
|
|
622
853
|
["--config-file", "configFile"],
|
|
623
|
-
["--
|
|
854
|
+
["--rules-file", "rulesFile"],
|
|
855
|
+
["--title", "title"],
|
|
856
|
+
["--cover", "cover"],
|
|
857
|
+
["--weibo-column", "weiboColumn"],
|
|
858
|
+
["--browser-channel", "browserChannel"],
|
|
859
|
+
["--taptap-secret-code", "taptapSecretCode"],
|
|
860
|
+
["--taptap-secret-period", "taptapSecretPeriod"],
|
|
861
|
+
["--taptap-official-group", "taptapOfficialGroup"]
|
|
624
862
|
]);
|
|
625
863
|
if (valueFlags.has(current)) {
|
|
626
864
|
const next = args[index + 1];
|
|
@@ -654,6 +892,34 @@ function parseDistributionOptions(args) {
|
|
|
654
892
|
return { file: positional[0], options };
|
|
655
893
|
}
|
|
656
894
|
|
|
895
|
+
function applyPublishOverrides(parsedArticle, options) {
|
|
896
|
+
const title = normalizeText(options.title);
|
|
897
|
+
const cover = normalizeText(options.cover);
|
|
898
|
+
const weiboColumn = normalizeText(options.weiboColumn);
|
|
899
|
+
if (!title && !cover && !weiboColumn) {
|
|
900
|
+
return parsedArticle;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const payload = {
|
|
904
|
+
...parsedArticle.payload,
|
|
905
|
+
...(title ? { article_title: title } : {}),
|
|
906
|
+
...(cover && /^https?:\/\//i.test(cover) ? { thumbnail: cover, move_thumbnail: cover } : {})
|
|
907
|
+
};
|
|
908
|
+
const meta = {
|
|
909
|
+
...parsedArticle.meta,
|
|
910
|
+
...(cover
|
|
911
|
+
? {
|
|
912
|
+
weibo_cover: cover,
|
|
913
|
+
taptap_cover: cover,
|
|
914
|
+
bilibili_cover: cover,
|
|
915
|
+
xiaohongshu_images: cover
|
|
916
|
+
}
|
|
917
|
+
: {}),
|
|
918
|
+
...(weiboColumn ? { weibo_column: weiboColumn } : {})
|
|
919
|
+
};
|
|
920
|
+
return { ...parsedArticle, payload, meta };
|
|
921
|
+
}
|
|
922
|
+
|
|
657
923
|
function parseArticleMutationOptions(args) {
|
|
658
924
|
const parsed = parsePublishOptions(args);
|
|
659
925
|
return {
|
|
@@ -1048,9 +1314,13 @@ function normalizeArticleId(value) {
|
|
|
1048
1314
|
return articleId;
|
|
1049
1315
|
}
|
|
1050
1316
|
|
|
1051
|
-
function articlePreviewUrl(articleId) {
|
|
1052
|
-
const
|
|
1317
|
+
function articlePreviewUrl(articleId, payload = {}) {
|
|
1318
|
+
const gameId = normalizeText(payload.game_id);
|
|
1319
|
+
const url = new URL(ARTICLE_PREVIEW_URLS[gameId] || (gameId === "33" ? "https://tideng.qingcigame.com/official/news.html" : ARTICLE_PREVIEW_BASE_URL));
|
|
1053
1320
|
url.searchParams.set("id", String(articleId));
|
|
1321
|
+
if (gameId === "33") {
|
|
1322
|
+
url.searchParams.set("cate_id", normalizeText(payload.cate_id) || "40");
|
|
1323
|
+
}
|
|
1054
1324
|
return url.toString();
|
|
1055
1325
|
}
|
|
1056
1326
|
|
|
@@ -1062,12 +1332,13 @@ function backendOriginsMatch(left, right) {
|
|
|
1062
1332
|
}
|
|
1063
1333
|
}
|
|
1064
1334
|
|
|
1065
|
-
async function saveRecentArticleState(articleId, status, backendUrl, previewUrl = "") {
|
|
1335
|
+
async function saveRecentArticleState(articleId, status, backendUrl, previewUrl = "", target = "domestic") {
|
|
1066
1336
|
const authState = await loadAuthState();
|
|
1067
1337
|
await writeJson(ARTICLE_STATE_FILE, {
|
|
1068
1338
|
article_id: articleId,
|
|
1069
1339
|
status: String(status),
|
|
1070
1340
|
preview_url: previewUrl,
|
|
1341
|
+
target,
|
|
1071
1342
|
backend_url: backendUrl,
|
|
1072
1343
|
administrator_id: Number(authState.id || 0),
|
|
1073
1344
|
account: normalizeText(authState.account),
|
|
@@ -1075,16 +1346,16 @@ async function saveRecentArticleState(articleId, status, backendUrl, previewUrl
|
|
|
1075
1346
|
});
|
|
1076
1347
|
}
|
|
1077
1348
|
|
|
1078
|
-
async function trySaveRecentArticleState(articleId, status, backendUrl, previewUrl = "") {
|
|
1349
|
+
async function trySaveRecentArticleState(articleId, status, backendUrl, previewUrl = "", target = "domestic") {
|
|
1079
1350
|
try {
|
|
1080
|
-
await saveRecentArticleState(articleId, status, backendUrl, previewUrl);
|
|
1351
|
+
await saveRecentArticleState(articleId, status, backendUrl, previewUrl, target);
|
|
1081
1352
|
return "";
|
|
1082
1353
|
} catch (error) {
|
|
1083
1354
|
return normalizeText(error?.message) || String(error);
|
|
1084
1355
|
}
|
|
1085
1356
|
}
|
|
1086
1357
|
|
|
1087
|
-
async function resolveArticleId(value, backendUrl) {
|
|
1358
|
+
async function resolveArticleId(value, backendUrl, target = "domestic") {
|
|
1088
1359
|
if (normalizeText(value)) {
|
|
1089
1360
|
return normalizeArticleId(value);
|
|
1090
1361
|
}
|
|
@@ -1096,6 +1367,9 @@ async function resolveArticleId(value, backendUrl) {
|
|
|
1096
1367
|
if (!backendOriginsMatch(recentArticle.backend_url, backendUrl)) {
|
|
1097
1368
|
throw new Error("最近文章属于其他后端,请手动输入文章 ID");
|
|
1098
1369
|
}
|
|
1370
|
+
if ((recentArticle.target || "domestic") !== target) {
|
|
1371
|
+
throw new Error("最近文章属于其他站点,请手动输入文章 ID");
|
|
1372
|
+
}
|
|
1099
1373
|
if (String(recentArticle.status) !== "0") {
|
|
1100
1374
|
throw new Error("最近文章不是未上线状态,请手动输入文章 ID");
|
|
1101
1375
|
}
|
|
@@ -1206,6 +1480,9 @@ async function installCommand(options) {
|
|
|
1206
1480
|
|
|
1207
1481
|
await ensureLocalDirs();
|
|
1208
1482
|
console.log(chalk.green("✔ 本地目录已创建"));
|
|
1483
|
+
const rulesCreated = await ensureContentRulesFile();
|
|
1484
|
+
console.log(chalk.green(rulesCreated ? "✔ 用户内容规则已创建" : "✔ 用户内容规则已保留"));
|
|
1485
|
+
console.log(`内容规则: ${chalk.gray(CONTENT_RULES_FILE)}`);
|
|
1209
1486
|
|
|
1210
1487
|
const skillsInstalled = await installSkills();
|
|
1211
1488
|
if (skillsInstalled.available) {
|
|
@@ -1258,6 +1535,7 @@ async function updateCommand() {
|
|
|
1258
1535
|
console.log("");
|
|
1259
1536
|
|
|
1260
1537
|
await ensureLocalDirs();
|
|
1538
|
+
const rulesCreated = await ensureContentRulesFile();
|
|
1261
1539
|
await runCommand(getNpmCommand(), ["install", "-g", `${PACKAGE_NAME}@latest`]);
|
|
1262
1540
|
|
|
1263
1541
|
const latestPackage = readPackageMetadata();
|
|
@@ -1272,6 +1550,8 @@ async function updateCommand() {
|
|
|
1272
1550
|
console.log(chalk.gray(`版本: ${latestPackage.version}`));
|
|
1273
1551
|
}
|
|
1274
1552
|
printSkillsSyncResult(skillsInstalled);
|
|
1553
|
+
console.log(chalk.green(rulesCreated ? "✔ 用户内容规则已创建" : "✔ 用户内容规则未覆盖"));
|
|
1554
|
+
console.log(`内容规则: ${chalk.gray(CONTENT_RULES_FILE)}`);
|
|
1275
1555
|
console.log("");
|
|
1276
1556
|
}
|
|
1277
1557
|
|
|
@@ -1715,29 +1995,38 @@ function parseFrontMatter(raw) {
|
|
|
1715
1995
|
}
|
|
1716
1996
|
|
|
1717
1997
|
function normalizeArticlePayload(meta, content) {
|
|
1998
|
+
const articleContent = markdownToHtml(content);
|
|
1999
|
+
const { gameId, category } = normalizeGameAndCategory(meta);
|
|
1718
2000
|
const payload = {
|
|
1719
2001
|
article_title: normalizeText(meta.article_title),
|
|
1720
2002
|
thumbnail: normalizeText(meta.thumbnail),
|
|
1721
2003
|
move_thumbnail: normalizeText(meta.move_thumbnail),
|
|
1722
|
-
article_content:
|
|
2004
|
+
article_content: articleContent,
|
|
1723
2005
|
article_excerpt: normalizeText(meta.article_excerpt),
|
|
1724
2006
|
article_url: normalizeText(meta.article_url),
|
|
1725
2007
|
origin: normalizeText(meta.origin),
|
|
1726
2008
|
status: normalizeMappedValue(meta.status, STATUS_MAP) || "0",
|
|
1727
|
-
cate_id:
|
|
2009
|
+
cate_id: category,
|
|
1728
2010
|
video_link: normalizeText(meta.video_link),
|
|
1729
2011
|
is_hot: normalizeBoolLike(meta.is_hot, "0"),
|
|
1730
2012
|
is_index: normalizeBoolLike(meta.is_index, "0"),
|
|
1731
2013
|
release_time: normalizeText(meta.release_time),
|
|
1732
2014
|
area: normalizeMappedValue(meta.area, AREA_MAP) || "1",
|
|
1733
2015
|
sort: normalizeText(meta.sort) || "1",
|
|
1734
|
-
game_id:
|
|
2016
|
+
game_id: gameId,
|
|
1735
2017
|
is_index2: "0",
|
|
1736
2018
|
index_pc_img: normalizeText(meta.index_pc_img),
|
|
1737
2019
|
index_move_img: normalizeText(meta.index_move_img),
|
|
1738
2020
|
type: "1"
|
|
1739
2021
|
};
|
|
1740
2022
|
|
|
2023
|
+
if (gameId === "64") {
|
|
2024
|
+
payload.language = normalizeText(meta.language).toLowerCase();
|
|
2025
|
+
if (!WEST_ARTICLE_LANGUAGES.has(payload.language)) {
|
|
2026
|
+
throw new Error("欧美蜗牛文章的 language 必须是 portuguese、german、spanish、italian、french 或 america_en");
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1741
2030
|
if (!payload.article_title) {
|
|
1742
2031
|
throw new Error("article_title 不能为空");
|
|
1743
2032
|
}
|
|
@@ -1753,6 +2042,19 @@ function normalizeArticlePayload(meta, content) {
|
|
|
1753
2042
|
return payload;
|
|
1754
2043
|
}
|
|
1755
2044
|
|
|
2045
|
+
function articleTarget(payload) {
|
|
2046
|
+
return payload.game_id === "64" ? "west" : "domestic";
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function articleApiPath(target, action) {
|
|
2050
|
+
const prefix = target === "west" ? "/api/west-articles" : "/api/articles";
|
|
2051
|
+
return action === "publish" ? `${prefix}/publish` : `${prefix}/status`;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function articleMutationCommand(target) {
|
|
2055
|
+
return target === "west" ? "west-article-list.update" : "article update";
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1756
2058
|
async function parseArticleFile(file) {
|
|
1757
2059
|
const articleFile = path.resolve(process.cwd(), file);
|
|
1758
2060
|
if (!(await pathExists(articleFile))) {
|
|
@@ -1769,8 +2071,8 @@ async function parseArticleFile(file) {
|
|
|
1769
2071
|
};
|
|
1770
2072
|
}
|
|
1771
2073
|
|
|
1772
|
-
async function initArticleTemplate(file = "article.md") {
|
|
1773
|
-
const sourceFile = path.resolve(__dirname,
|
|
2074
|
+
async function initArticleTemplate(file = "article.md", template = "article.md") {
|
|
2075
|
+
const sourceFile = path.resolve(__dirname, `../templates/skills/qcplay-publish-article/article/${template}`);
|
|
1774
2076
|
const targetFile = path.resolve(process.cwd(), file);
|
|
1775
2077
|
|
|
1776
2078
|
if (!(await pathExists(sourceFile))) {
|
|
@@ -1788,11 +2090,11 @@ async function initArticleTemplate(file = "article.md") {
|
|
|
1788
2090
|
console.log("");
|
|
1789
2091
|
console.log("请编辑该文件后执行:");
|
|
1790
2092
|
console.log("");
|
|
1791
|
-
console.log(` qcplay-cli www-article-list.store ${file}`);
|
|
2093
|
+
console.log(` qcplay-cli ${template === "west-article.md" ? "west-article-list.store" : "www-article-list.store"} ${file}`);
|
|
1792
2094
|
console.log("");
|
|
1793
2095
|
}
|
|
1794
2096
|
|
|
1795
|
-
async function importArticleCommand(sourceUrl, file = "article.md") {
|
|
2097
|
+
async function importArticleCommand(sourceUrl, file = "article.md", options = {}) {
|
|
1796
2098
|
if (!sourceUrl) {
|
|
1797
2099
|
throw new Error("缺少文章来源链接,请提供微信公众号文章或飞书 Docx/Wiki 文档链接");
|
|
1798
2100
|
}
|
|
@@ -1816,9 +2118,19 @@ async function importArticleCommand(sourceUrl, file = "article.md") {
|
|
|
1816
2118
|
}
|
|
1817
2119
|
sourceLabel = "微信文章";
|
|
1818
2120
|
console.log(chalk.cyan("正在获取微信文章..."));
|
|
2121
|
+
const contentRules = await loadContentRules(options.rulesFile || CONTENT_RULES_FILE, {
|
|
2122
|
+
optional: !options.rulesFile
|
|
2123
|
+
});
|
|
2124
|
+
if (options.rulesFile) {
|
|
2125
|
+
console.log(chalk.gray(`已加载内容规则: ${contentRules.file}(${contentRules.rules.length} 条)`));
|
|
2126
|
+
}
|
|
1819
2127
|
article = await importWechatArticle(sourceUrl, {
|
|
1820
|
-
|
|
1821
|
-
|
|
2128
|
+
contentRules,
|
|
2129
|
+
project: options.project,
|
|
2130
|
+
platform: options.platform,
|
|
2131
|
+
onProgress: ({ mediaType = "image", current, total }) => {
|
|
2132
|
+
const label = mediaType === "video" ? "正文视频" : "正文图片";
|
|
2133
|
+
console.log(chalk.gray(`正在转存${label} ${current}/${total}`));
|
|
1822
2134
|
}
|
|
1823
2135
|
});
|
|
1824
2136
|
}
|
|
@@ -1834,7 +2146,22 @@ async function importArticleCommand(sourceUrl, file = "article.md") {
|
|
|
1834
2146
|
console.log(chalk.gray(`已保留图片: ${article.imageCount} 张,表格: ${article.tableCount} 个`));
|
|
1835
2147
|
console.log(chalk.gray("已保留标题、列表、引用、颜色、高亮、对齐、列宽及合并单元格"));
|
|
1836
2148
|
} else {
|
|
2149
|
+
if (article.matchedContentRules?.length) {
|
|
2150
|
+
console.log(chalk.gray(`导入匹配规则: ${article.matchedContentRules.join("、")}`));
|
|
2151
|
+
console.log(
|
|
2152
|
+
chalk.gray(
|
|
2153
|
+
article.appliedContentRules?.length
|
|
2154
|
+
? `已应用规则: ${article.appliedContentRules.join("、")}`
|
|
2155
|
+
: "匹配规则未改变文章内容"
|
|
2156
|
+
)
|
|
2157
|
+
);
|
|
2158
|
+
} else if (options.rulesFile) {
|
|
2159
|
+
console.log(chalk.yellow("未匹配任何导入规则,请检查规则的项目、平台、来源和阶段范围"));
|
|
2160
|
+
}
|
|
1837
2161
|
console.log(chalk.gray(`已转存图片: ${article.imageCount} 张`));
|
|
2162
|
+
if (article.videoCount > 0) {
|
|
2163
|
+
console.log(chalk.gray(`已转存视频: ${article.videoCount} 个`));
|
|
2164
|
+
}
|
|
1838
2165
|
console.log(chalk.gray(`已提取正文: ${article.textSegmentCount} 段`));
|
|
1839
2166
|
console.log(chalk.gray(`已保留文字颜色: ${article.colorCount} 处`));
|
|
1840
2167
|
console.log(
|
|
@@ -1877,9 +2204,10 @@ release_time: 2026-07-16
|
|
|
1877
2204
|
area: pc
|
|
1878
2205
|
sort: 100
|
|
1879
2206
|
game_id: 最强蜗牛
|
|
2207
|
+
distribution_project: ""
|
|
1880
2208
|
index_pc_img: ""
|
|
1881
2209
|
index_move_img: ""
|
|
1882
|
-
taptap_type:
|
|
2210
|
+
taptap_type: ""
|
|
1883
2211
|
taptap_forum: ""
|
|
1884
2212
|
taptap_scheduled: "0"
|
|
1885
2213
|
taptap_draft: "0"
|
|
@@ -1893,6 +2221,8 @@ bilibili_category: ""
|
|
|
1893
2221
|
bilibili_tags: ""
|
|
1894
2222
|
bilibili_topic: ""
|
|
1895
2223
|
weibo_intro: ""
|
|
2224
|
+
weibo_super_topic: ""
|
|
2225
|
+
weibo_cover: ""
|
|
1896
2226
|
---
|
|
1897
2227
|
|
|
1898
2228
|
# 文章标题
|
|
@@ -1907,13 +2237,23 @@ ${chalk.cyan("发布命令:")}
|
|
|
1907
2237
|
${chalk.cyan("从微信推文生成官网草稿:")}
|
|
1908
2238
|
|
|
1909
2239
|
qcplay-cli article import "https://mp.weixin.qq.com/s/..." article.md
|
|
2240
|
+
|
|
2241
|
+
${chalk.cyan("项目默认分类:")}
|
|
2242
|
+
|
|
2243
|
+
新仙剑(game_id=69):全部(cate_id=61)
|
|
2244
|
+
提灯与地下城(game_id=33):新闻(cate_id=40)
|
|
2245
|
+
魔卡少女樱(game_id=74):最新(cate_id=71)
|
|
2246
|
+
迷途之光(game_id=50):全部(cate_id=109)
|
|
1910
2247
|
`);
|
|
1911
2248
|
}
|
|
1912
2249
|
|
|
1913
|
-
function inferDistributionProject(payload, explicitProject) {
|
|
2250
|
+
function inferDistributionProject(payload, explicitProject, meta = {}) {
|
|
1914
2251
|
if (normalizeText(explicitProject)) {
|
|
1915
2252
|
return normalizeText(explicitProject);
|
|
1916
2253
|
}
|
|
2254
|
+
if (normalizeText(meta.distribution_project)) {
|
|
2255
|
+
return normalizeText(meta.distribution_project);
|
|
2256
|
+
}
|
|
1917
2257
|
if (payload?.game_id === "39") {
|
|
1918
2258
|
return "最强蜗牛";
|
|
1919
2259
|
}
|
|
@@ -1927,6 +2267,12 @@ function printPlatformEntries(entries) {
|
|
|
1927
2267
|
entries.forEach(entry => {
|
|
1928
2268
|
const status = entry.publisher === "blocked" || entry.publisher === "unavailable" ? "不可自动发布" : entry.publisher;
|
|
1929
2269
|
console.log(` ${entry.project} / ${entry.region} / ${entry.platform} [${status}; ${entry.authMode}]`);
|
|
2270
|
+
const requiredInputs = publicEntrySummary(entry).contentRequirements?.requiredUserInputs || [];
|
|
2271
|
+
if (requiredInputs.length > 0) {
|
|
2272
|
+
console.log(
|
|
2273
|
+
chalk.yellow(` 飞书特殊要求需用户提供: ${requiredInputs.map(item => `${item.label} (${item.field})`).join("、")}`)
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
1930
2276
|
});
|
|
1931
2277
|
console.log("");
|
|
1932
2278
|
}
|
|
@@ -1935,10 +2281,11 @@ async function publishPlatformsCommand(file, options) {
|
|
|
1935
2281
|
let parsedArticle;
|
|
1936
2282
|
if (file) {
|
|
1937
2283
|
parsedArticle = await parseArticleFile(file);
|
|
2284
|
+
parsedArticle = applyPublishOverrides(parsedArticle, options);
|
|
1938
2285
|
}
|
|
1939
2286
|
const allEntries = await loadPlatformEntries({ configFile: options.configFile });
|
|
1940
2287
|
|
|
1941
|
-
const project = inferDistributionProject(parsedArticle?.payload, options.project);
|
|
2288
|
+
const project = inferDistributionProject(parsedArticle?.payload, options.project, parsedArticle?.meta);
|
|
1942
2289
|
if (!project) {
|
|
1943
2290
|
if (options.listPlatforms) {
|
|
1944
2291
|
printPlatformEntries(allEntries);
|
|
@@ -1956,24 +2303,68 @@ async function publishPlatformsCommand(file, options) {
|
|
|
1956
2303
|
printPlatformEntries(candidates);
|
|
1957
2304
|
return;
|
|
1958
2305
|
}
|
|
1959
|
-
if (!parsedArticle) {
|
|
2306
|
+
if (!parsedArticle && !options.openPage) {
|
|
1960
2307
|
throw new Error("缺少文章文件,例如:qcplay-cli publish article.md --platform TapTap");
|
|
1961
2308
|
}
|
|
1962
2309
|
|
|
1963
|
-
const
|
|
2310
|
+
const selectedEntries = options.platforms.length
|
|
1964
2311
|
? selectRequestedEntries(candidates, options.platforms)
|
|
1965
2312
|
: await promptForPlatformEntries(candidates);
|
|
2313
|
+
if (options.openPage) {
|
|
2314
|
+
if (selectedEntries.some(entry => entry.publisher !== "browser")) {
|
|
2315
|
+
throw new Error("--open-page 仅支持有真实浏览器发布页的平台,请使用 --platform 指定浏览器平台");
|
|
2316
|
+
}
|
|
2317
|
+
for (const entry of selectedEntries) {
|
|
2318
|
+
console.log("");
|
|
2319
|
+
console.log(chalk.cyan(`正在打开 ${entry.platform}(${entry.region})真实发布页面...`));
|
|
2320
|
+
await openPublishPage(entry, {
|
|
2321
|
+
browserChannel: options.browserChannel,
|
|
2322
|
+
onProgress: message => console.log(chalk.gray(` ${message}`))
|
|
2323
|
+
});
|
|
2324
|
+
console.log(chalk.green(`${entry.platform} 页面流程测试结束`));
|
|
2325
|
+
}
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
if (options.switchAccount) {
|
|
2329
|
+
if (selectedEntries.some(entry => entry.publisher !== "browser")) {
|
|
2330
|
+
throw new Error("--switch-account 仅支持有真实浏览器发布页的平台,请使用 --platform 指定浏览器平台");
|
|
2331
|
+
}
|
|
2332
|
+
for (const entry of selectedEntries) {
|
|
2333
|
+
console.log("");
|
|
2334
|
+
console.log(chalk.cyan(`正在打开 ${entry.platform}(${entry.region})页面切换账号...`));
|
|
2335
|
+
await openPublishPage(entry, {
|
|
2336
|
+
browserChannel: options.browserChannel,
|
|
2337
|
+
accountSwitch: true,
|
|
2338
|
+
onProgress: message => console.log(chalk.gray(` ${message}`))
|
|
2339
|
+
});
|
|
2340
|
+
console.log(chalk.green(`${entry.platform} 账号切换已确认,继续发布流程`));
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
const contentRules = await loadContentRules(options.rulesFile || CONTENT_RULES_FILE, {
|
|
2344
|
+
optional: !options.rulesFile
|
|
2345
|
+
});
|
|
2346
|
+
const selected = selectedEntries.map(entry => ({ ...entry, contentRules }));
|
|
2347
|
+
const platformInputs = {
|
|
2348
|
+
taptap_secret_code: options.taptapSecretCode,
|
|
2349
|
+
taptap_secret_period: options.taptapSecretPeriod,
|
|
2350
|
+
taptap_official_group: options.taptapOfficialGroup
|
|
2351
|
+
};
|
|
1966
2352
|
const planningArticle = {
|
|
1967
2353
|
...parsedArticle,
|
|
1968
2354
|
title: parsedArticle.payload.article_title,
|
|
1969
|
-
html: parsedArticle.payload.article_content
|
|
2355
|
+
html: parsedArticle.payload.article_content,
|
|
2356
|
+
platformInputs
|
|
1970
2357
|
};
|
|
1971
|
-
const plan = selected.map(entry =>
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2358
|
+
const plan = selected.map(entry => {
|
|
2359
|
+
const matchedRules = matchingContentRules(contentRules, planningArticle, entry).map(rule => rule.name);
|
|
2360
|
+
return {
|
|
2361
|
+
...publicEntrySummary(entry),
|
|
2362
|
+
...(matchedRules.length > 0 ? { contentRules: matchedRules } : {}),
|
|
2363
|
+
...(entry.platformKey === "taptap" ? { taptap: tapTapPublishingPlan(planningArticle, entry) } : {}),
|
|
2364
|
+
...(entry.platformKey === "bilibili" ? { bilibili: bilibiliPublishingPlan(planningArticle, entry) } : {}),
|
|
2365
|
+
...(entry.platformKey === "weibo" ? { weibo: weiboPublishingPlan(planningArticle, entry) } : {})
|
|
2366
|
+
};
|
|
2367
|
+
});
|
|
1977
2368
|
if (options.dryRun) {
|
|
1978
2369
|
console.log(JSON.stringify({ article: parsedArticle.articleFile, project, platforms: plan }, null, 2));
|
|
1979
2370
|
return;
|
|
@@ -1985,14 +2376,15 @@ async function publishPlatformsCommand(file, options) {
|
|
|
1985
2376
|
markdown: parsedArticle.markdown,
|
|
1986
2377
|
html: parsedArticle.payload.article_content,
|
|
1987
2378
|
payload: parsedArticle.payload,
|
|
1988
|
-
meta: parsedArticle.meta
|
|
2379
|
+
meta: parsedArticle.meta,
|
|
2380
|
+
platformInputs
|
|
1989
2381
|
};
|
|
1990
2382
|
const { successes, failures, skipped } = await runPlatformPublishSequence(selected, async entry => {
|
|
1991
2383
|
console.log("");
|
|
1992
2384
|
console.log(chalk.cyan(`正在发布到 ${entry.platform}(${entry.region})...`));
|
|
1993
2385
|
let result;
|
|
1994
2386
|
if (entry.publisher === "official-api") {
|
|
1995
|
-
await publishArticleCommand(file, options);
|
|
2387
|
+
await publishArticleCommand(file, options, entry);
|
|
1996
2388
|
} else {
|
|
1997
2389
|
result = await publishPlatformEntry(entry, article, {
|
|
1998
2390
|
browserChannel: options.browserChannel,
|
|
@@ -2032,13 +2424,58 @@ async function publishPlatformsCommand(file, options) {
|
|
|
2032
2424
|
}
|
|
2033
2425
|
}
|
|
2034
2426
|
|
|
2035
|
-
async function publishArticleCommand(file, options) {
|
|
2427
|
+
async function publishArticleCommand(file, options, platformEntry = null, expectedTarget = "") {
|
|
2036
2428
|
if (!file) {
|
|
2037
2429
|
throw new Error("缺少文章文件,例如:qcplay-cli www-article-list.store article.md");
|
|
2038
2430
|
}
|
|
2039
2431
|
|
|
2040
|
-
const
|
|
2432
|
+
const parsedArticle = await parseArticleFile(file);
|
|
2433
|
+
const { articleFile } = parsedArticle;
|
|
2434
|
+
let payload = parsedArticle.payload;
|
|
2435
|
+
if (normalizeText(options.title)) {
|
|
2436
|
+
payload = { ...payload, article_title: normalizeText(options.title) };
|
|
2437
|
+
}
|
|
2438
|
+
if (normalizeText(options.cover)) {
|
|
2439
|
+
const cover = normalizeText(options.cover);
|
|
2440
|
+
if (!/^https?:\/\//i.test(cover)) {
|
|
2441
|
+
throw new Error("官网封面必须使用 http 或 https 图片地址");
|
|
2442
|
+
}
|
|
2443
|
+
payload = { ...payload, thumbnail: cover, move_thumbnail: cover };
|
|
2444
|
+
}
|
|
2445
|
+
let effectiveEntry = platformEntry;
|
|
2446
|
+
let prepareArticle = prepareArticleForPlatform;
|
|
2447
|
+
if (!effectiveEntry) {
|
|
2448
|
+
const contentRules = await loadContentRules(options.rulesFile || CONTENT_RULES_FILE, {
|
|
2449
|
+
optional: !options.rulesFile
|
|
2450
|
+
});
|
|
2451
|
+
effectiveEntry = {
|
|
2452
|
+
project: inferDistributionProject(payload, options.project, parsedArticle.meta),
|
|
2453
|
+
region: options.region || "",
|
|
2454
|
+
platform: "官网",
|
|
2455
|
+
platformKey: "website",
|
|
2456
|
+
contentRules
|
|
2457
|
+
};
|
|
2458
|
+
prepareArticle = (article, entry) => applyContentRules(article, entry);
|
|
2459
|
+
}
|
|
2460
|
+
if (effectiveEntry) {
|
|
2461
|
+
const preparedArticle = prepareArticle(
|
|
2462
|
+
{
|
|
2463
|
+
articleFile,
|
|
2464
|
+
title: payload.article_title,
|
|
2465
|
+
markdown: parsedArticle.markdown,
|
|
2466
|
+
html: payload.article_content,
|
|
2467
|
+
payload,
|
|
2468
|
+
meta: parsedArticle.meta
|
|
2469
|
+
},
|
|
2470
|
+
effectiveEntry
|
|
2471
|
+
);
|
|
2472
|
+
payload = { ...payload, article_content: preparedArticle.html };
|
|
2473
|
+
}
|
|
2041
2474
|
payload.status = "0";
|
|
2475
|
+
const target = articleTarget(payload);
|
|
2476
|
+
if (expectedTarget && target !== expectedTarget) {
|
|
2477
|
+
throw new Error("欧美蜗牛官网文章必须使用 game_id: 64 和有效的 language");
|
|
2478
|
+
}
|
|
2042
2479
|
const config = await loadConfig();
|
|
2043
2480
|
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
2044
2481
|
|
|
@@ -2048,40 +2485,46 @@ async function publishArticleCommand(file, options) {
|
|
|
2048
2485
|
}
|
|
2049
2486
|
|
|
2050
2487
|
console.log("");
|
|
2051
|
-
console.log(chalk.cyan("正在保存官网文章为未上线状态..."));
|
|
2488
|
+
console.log(chalk.cyan(target === "west" ? "正在保存欧美蜗牛官网文章为未上线状态..." : "正在保存官网文章为未上线状态..."));
|
|
2052
2489
|
console.log(chalk.gray(`文章文件: ${articleFile}`));
|
|
2053
2490
|
console.log("");
|
|
2054
2491
|
|
|
2055
|
-
const response = await authenticatedRequest("POST", backendUrl, "
|
|
2492
|
+
const response = await authenticatedRequest("POST", backendUrl, articleApiPath(target, "publish"), payload);
|
|
2056
2493
|
const rawArticleId = response.data?.id ?? response.data?.article_id ?? response.data?.articleId;
|
|
2057
2494
|
const articleId = normalizeArticleId(rawArticleId);
|
|
2058
|
-
const previewUrl = normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId);
|
|
2059
|
-
const stateWarning = await trySaveRecentArticleState(articleId, "0", backendUrl, previewUrl);
|
|
2495
|
+
const previewUrl = normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId, payload);
|
|
2496
|
+
const stateWarning = await trySaveRecentArticleState(articleId, "0", backendUrl, previewUrl, target);
|
|
2060
2497
|
|
|
2061
2498
|
console.log(chalk.green(response.message || "文章已保存为未上线状态"));
|
|
2062
2499
|
console.log(`文章 ID: ${articleId}`);
|
|
2063
2500
|
console.log("文章状态: 未上线");
|
|
2064
|
-
|
|
2501
|
+
if (previewUrl) {
|
|
2502
|
+
console.log(`预览地址: ${previewUrl}`);
|
|
2503
|
+
} else {
|
|
2504
|
+
console.log(chalk.yellow("欧美官网预览地址尚未配置,请在后台确认草稿内容和排版。"));
|
|
2505
|
+
}
|
|
2065
2506
|
if (stateWarning) {
|
|
2066
2507
|
console.log(chalk.yellow(`本地未能记录最近文章 ID,后续操作请手动输入 ${articleId}: ${stateWarning}`));
|
|
2067
2508
|
}
|
|
2068
2509
|
console.log("");
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2510
|
+
if (previewUrl) {
|
|
2511
|
+
try {
|
|
2512
|
+
await openBrowser(previewUrl);
|
|
2513
|
+
console.log(chalk.cyan("已打开文章预览页面,请由用户检查文章内容和排版。"));
|
|
2514
|
+
} catch {
|
|
2515
|
+
console.log(chalk.yellow("自动打开预览页面失败,请手动打开上面的预览地址。"));
|
|
2516
|
+
}
|
|
2074
2517
|
}
|
|
2075
2518
|
console.log("用户确认无误后再执行:");
|
|
2076
2519
|
console.log("");
|
|
2077
|
-
console.log(` qcplay-cli
|
|
2520
|
+
console.log(` qcplay-cli ${articleMutationCommand(target)} ${articleId}`);
|
|
2078
2521
|
console.log("");
|
|
2079
2522
|
}
|
|
2080
2523
|
|
|
2081
|
-
async function updateArticleStatusCommand(articleIdValue, targetStatus, options) {
|
|
2524
|
+
async function updateArticleStatusCommand(articleIdValue, targetStatus, options, target = "domestic") {
|
|
2082
2525
|
const config = await loadConfig();
|
|
2083
2526
|
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
2084
|
-
const articleId = await resolveArticleId(articleIdValue, backendUrl);
|
|
2527
|
+
const articleId = await resolveArticleId(articleIdValue, backendUrl, target);
|
|
2085
2528
|
const payload = {
|
|
2086
2529
|
id: articleId,
|
|
2087
2530
|
status: targetStatus
|
|
@@ -2094,17 +2537,17 @@ async function updateArticleStatusCommand(articleIdValue, targetStatus, options)
|
|
|
2094
2537
|
|
|
2095
2538
|
const action = targetStatus === "2" ? "删除" : "上线";
|
|
2096
2539
|
console.log("");
|
|
2097
|
-
console.log(chalk.cyan(`正在${action}官网文章...`));
|
|
2540
|
+
console.log(chalk.cyan(`正在${action}${target === "west" ? "欧美蜗牛" : ""}官网文章...`));
|
|
2098
2541
|
console.log(chalk.gray(`文章 ID: ${articleId}`));
|
|
2099
2542
|
console.log("");
|
|
2100
2543
|
|
|
2101
|
-
const response = await authenticatedRequest("PATCH", backendUrl, "
|
|
2544
|
+
const response = await authenticatedRequest("PATCH", backendUrl, articleApiPath(target, "status"), payload);
|
|
2102
2545
|
const responseStatus = normalizeText(response.data?.status) || targetStatus;
|
|
2103
2546
|
const previewUrl =
|
|
2104
2547
|
responseStatus === "1"
|
|
2105
|
-
? normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId)
|
|
2548
|
+
? normalizeText(response.data?.preview_url) || (target === "domestic" ? articlePreviewUrl(articleId) : "")
|
|
2106
2549
|
: "";
|
|
2107
|
-
const stateWarning = await trySaveRecentArticleState(articleId, responseStatus, backendUrl, previewUrl);
|
|
2550
|
+
const stateWarning = await trySaveRecentArticleState(articleId, responseStatus, backendUrl, previewUrl, target);
|
|
2108
2551
|
|
|
2109
2552
|
console.log(chalk.green(response.message || (responseStatus === "2" ? "文章已删除" : "文章已上线")));
|
|
2110
2553
|
console.log(`文章 ID: ${articleId}`);
|
|
@@ -2178,6 +2621,21 @@ async function main() {
|
|
|
2178
2621
|
return;
|
|
2179
2622
|
}
|
|
2180
2623
|
|
|
2624
|
+
if (command === "rules") {
|
|
2625
|
+
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
2626
|
+
printRulesHelp();
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
if (!new Set(["init", "validate", "list"]).has(subcommand)) {
|
|
2630
|
+
await runWithErrorBanner("内容规则失败", async () => {
|
|
2631
|
+
throw new Error(`未知 rules 命令: ${subcommand}`);
|
|
2632
|
+
});
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
await runWithErrorBanner("内容规则失败", () => contentRulesCommand(subcommand, rest));
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2181
2639
|
if (command === "article") {
|
|
2182
2640
|
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
2183
2641
|
if (!subcommand) {
|
|
@@ -2194,16 +2652,17 @@ async function main() {
|
|
|
2194
2652
|
return;
|
|
2195
2653
|
}
|
|
2196
2654
|
|
|
2655
|
+
if (subcommand === "west-init") {
|
|
2656
|
+
const file = rest[0] || "west-article.md";
|
|
2657
|
+
await runWithErrorBanner("创建模板失败", () => initArticleTemplate(file, "west-article.md"));
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2197
2661
|
if (subcommand === "import") {
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
throw new Error(`未知参数: ${rest.slice(2).join(" ")}`);
|
|
2203
|
-
});
|
|
2204
|
-
return;
|
|
2205
|
-
}
|
|
2206
|
-
await runWithErrorBanner("转换失败", () => importArticleCommand(sourceUrl, file));
|
|
2662
|
+
const parsed = parseArticleImportOptions(rest);
|
|
2663
|
+
await runWithErrorBanner("转换失败", () =>
|
|
2664
|
+
importArticleCommand(parsed.sourceUrl, parsed.file, parsed.options)
|
|
2665
|
+
);
|
|
2207
2666
|
return;
|
|
2208
2667
|
}
|
|
2209
2668
|
|
|
@@ -2243,6 +2702,22 @@ async function main() {
|
|
|
2243
2702
|
return;
|
|
2244
2703
|
}
|
|
2245
2704
|
|
|
2705
|
+
if (command === "west-article-list.store") {
|
|
2706
|
+
const parsed = parsePublishOptions([subcommand, ...rest].filter(Boolean));
|
|
2707
|
+
await runWithErrorBanner("发布失败", () => publishArticleCommand(parsed.file, parsed.options, null, "west"));
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
if (command === "west-article-list.update" || command === "west-article-list.delete") {
|
|
2712
|
+
const parsed = parseArticleMutationOptions([subcommand, ...rest].filter(Boolean));
|
|
2713
|
+
const targetStatus = command === "west-article-list.delete" ? "2" : "1";
|
|
2714
|
+
const errorTitle = targetStatus === "2" ? "删除失败" : "上线失败";
|
|
2715
|
+
await runWithErrorBanner(errorTitle, () =>
|
|
2716
|
+
updateArticleStatusCommand(parsed.articleId, targetStatus, parsed.options, "west")
|
|
2717
|
+
);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2246
2721
|
if (command === "features") {
|
|
2247
2722
|
printFeatures();
|
|
2248
2723
|
return;
|