@qcplay/cli 1.0.13 → 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 +309 -3
- package/bin/qcplay.js +852 -74
- package/lib/content-rules.js +807 -0
- package/lib/lark-article.js +294 -0
- package/lib/platform-publish.js +6617 -0
- package/lib/wechat-article.js +414 -28
- package/package.json +8 -2
- package/templates/skills/qcplay-publish-article/SKILL.md +144 -2
- package/templates/skills/qcplay-publish-article/article/article.md +17 -0
- package/templates/skills/qcplay-publish-article/article/west-article.md +22 -0
package/bin/qcplay.js
CHANGED
|
@@ -15,6 +15,29 @@ import {
|
|
|
15
15
|
normalizeArticleColor,
|
|
16
16
|
sanitizeArticleRichHtml
|
|
17
17
|
} from "../lib/wechat-article.js";
|
|
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";
|
|
26
|
+
import {
|
|
27
|
+
availableProjects,
|
|
28
|
+
bilibiliPublishingPlan,
|
|
29
|
+
entriesForProject,
|
|
30
|
+
loadPlatformEntries,
|
|
31
|
+
openPublishPage,
|
|
32
|
+
prepareArticleForPlatform,
|
|
33
|
+
promptForPlatformEntries,
|
|
34
|
+
publicEntrySummary,
|
|
35
|
+
publishPlatformEntry,
|
|
36
|
+
runPlatformPublishSequence,
|
|
37
|
+
selectRequestedEntries,
|
|
38
|
+
tapTapPublishingPlan,
|
|
39
|
+
weiboPublishingPlan
|
|
40
|
+
} from "../lib/platform-publish.js";
|
|
18
41
|
|
|
19
42
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
43
|
const __dirname = path.dirname(__filename);
|
|
@@ -38,6 +61,48 @@ function resolveAuthPagePath() {
|
|
|
38
61
|
return path.resolve(__dirname, "../web/auth.html");
|
|
39
62
|
}
|
|
40
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
|
+
|
|
41
106
|
const CATEGORY_MAP = {
|
|
42
107
|
"2": "2",
|
|
43
108
|
"3": "3",
|
|
@@ -59,6 +124,13 @@ const CATEGORY_MAP = {
|
|
|
59
124
|
"视频攻略": "29"
|
|
60
125
|
};
|
|
61
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
|
+
|
|
62
134
|
const AREA_MAP = {
|
|
63
135
|
"1": "1",
|
|
64
136
|
"3": "3",
|
|
@@ -69,11 +141,6 @@ const AREA_MAP = {
|
|
|
69
141
|
"公益网站": "4"
|
|
70
142
|
};
|
|
71
143
|
|
|
72
|
-
const GAME_MAP = {
|
|
73
|
-
"39": "39",
|
|
74
|
-
"最强蜗牛": "39"
|
|
75
|
-
};
|
|
76
|
-
|
|
77
144
|
const STATUS_MAP = {
|
|
78
145
|
"0": "0",
|
|
79
146
|
"1": "1",
|
|
@@ -100,8 +167,16 @@ const AGENTS_DIR = path.join(os.homedir(), ".agents");
|
|
|
100
167
|
const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
|
|
101
168
|
const AUTH_FILE = path.join(QCPLAY_DIR, "auth.json");
|
|
102
169
|
const ARTICLE_STATE_FILE = path.join(QCPLAY_DIR, "article-state.json");
|
|
170
|
+
const CONTENT_RULES_FILE = DEFAULT_CONTENT_RULES_FILE;
|
|
103
171
|
const ARTICLE_PREVIEW_BASE_URL = "https://snail.qingcigame.com/official/news-details.html";
|
|
104
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
|
+
};
|
|
105
180
|
|
|
106
181
|
function readPackageMetadata() {
|
|
107
182
|
try {
|
|
@@ -149,13 +224,20 @@ Usage:
|
|
|
149
224
|
qcplay-cli auth permissions [--local] [--key <key>] [--json]
|
|
150
225
|
qcplay-cli article
|
|
151
226
|
qcplay-cli article init [file]
|
|
152
|
-
qcplay-cli article
|
|
153
|
-
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]
|
|
154
230
|
qcplay-cli article update [id] [--local] [--backend <url>] [--dry-run]
|
|
155
231
|
qcplay-cli article delete [id] [--local] [--backend <url>] [--dry-run]
|
|
156
|
-
qcplay-cli
|
|
232
|
+
qcplay-cli publish [file] [--title <标题>] [--cover <图片>] [--weibo-column <专栏>] [--project <项目>] [--region <区域>] [--platform <平台>]
|
|
233
|
+
qcplay-cli publish --project <项目> --list-platforms
|
|
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]
|
|
157
236
|
qcplay-cli www-article-list.update [id] [--local] [--backend <url>] [--dry-run]
|
|
158
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]
|
|
159
241
|
qcplay-cli features
|
|
160
242
|
qcplay-cli where
|
|
161
243
|
|
|
@@ -180,12 +262,154 @@ function printArticleHelp() {
|
|
|
180
262
|
console.log(`Usage:
|
|
181
263
|
qcplay-cli article
|
|
182
264
|
qcplay-cli article init [file]
|
|
183
|
-
qcplay-cli article
|
|
184
|
-
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]
|
|
185
268
|
qcplay-cli article update [id] [--local] [--backend <url>] [--dry-run]
|
|
186
269
|
qcplay-cli article delete [id] [--local] [--backend <url>] [--dry-run]`);
|
|
187
270
|
}
|
|
188
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
|
+
|
|
291
|
+
function printPublishHelp() {
|
|
292
|
+
console.log(`Usage:
|
|
293
|
+
qcplay-cli publish <file> --project <项目> [--title <标题>] [--cover <图片>] [--weibo-column <专栏>] [--region <区域>] [--platform <平台> ...]
|
|
294
|
+
qcplay-cli publish --project <项目> [--region <区域>] --list-platforms
|
|
295
|
+
qcplay-cli publish --project <项目> --platform <平台> --open-page
|
|
296
|
+
|
|
297
|
+
Options:
|
|
298
|
+
--project <项目> 项目名称;game_id=39 时默认使用“最强蜗牛”
|
|
299
|
+
--region <区域> 国内、日本、欧美等区域
|
|
300
|
+
--title <标题> 本次发布临时替换标题,不改动 Markdown 文件
|
|
301
|
+
--cover <图片> 本次发布临时替换封面;浏览器平台支持本地路径或 URL,官网使用图片 URL
|
|
302
|
+
--weibo-column <专栏> 仅微博:设置头条文章专栏;不填写则不设置专栏
|
|
303
|
+
-p, --platform <平台> 发布平台,可重复使用或用逗号分隔
|
|
304
|
+
--list-platforms 列出匹配的平台,不发布
|
|
305
|
+
--config-file <json> 使用本地平台配置(主要用于测试)
|
|
306
|
+
--rules-file <json> 使用指定内容规则;默认 ~/.qcplay/platform-rules.json
|
|
307
|
+
--browser-channel <name> Playwright 浏览器通道,Windows 默认 msedge
|
|
308
|
+
--review-draft, --review 官网外平台先准备草稿,预览后在 CMD 确认发布
|
|
309
|
+
--open-page 只打开真实浏览器发布页,供登录和页面流程测试,不读取或提交文章
|
|
310
|
+
--switch-account 先打开发布页切换账号,确认后继续当前发布流程
|
|
311
|
+
--keep-open 浏览器提交后等待用户确认再关闭
|
|
312
|
+
--dry-run 只展示发布计划,不登录或发布
|
|
313
|
+
--local 官网发布使用本地后端
|
|
314
|
+
--backend <url> 官网发布后端地址
|
|
315
|
+
|
|
316
|
+
TapTap command inputs:
|
|
317
|
+
--taptap-secret-code <密令> 飞书“特殊要求”要求的本次福利密令
|
|
318
|
+
--taptap-secret-period <有效期> 本次密令生效时间或有效期
|
|
319
|
+
--taptap-official-group <群号> 仅当飞书群号为动态占位符时提供
|
|
320
|
+
|
|
321
|
+
TapTap Front Matter:
|
|
322
|
+
taptap_type 留空时跟随飞书页面 URL,也可指定 long-post、image-text 或 video
|
|
323
|
+
taptap_forum 默认使用当前项目同名论坛;填 0 时不添加论坛
|
|
324
|
+
taptap_scheduled 填 1 时勾选定时发布
|
|
325
|
+
taptap_draft 填 1 时保存草稿而非发布
|
|
326
|
+
taptap_images 图文图片路径或 URL,多个用逗号分隔
|
|
327
|
+
taptap_video 视频模式使用的本地视频路径
|
|
328
|
+
taptap_cover 长帖封面路径或 URL
|
|
329
|
+
|
|
330
|
+
B站 Front Matter:
|
|
331
|
+
bilibili_type article(默认)或 video
|
|
332
|
+
bilibili_video 视频投稿使用的本地视频路径
|
|
333
|
+
bilibili_cover 投稿封面本地路径或 URL
|
|
334
|
+
bilibili_category 投稿分区,可用“一级/二级”表示层级
|
|
335
|
+
bilibili_tags 投稿标签,多个用逗号分隔
|
|
336
|
+
bilibili_topic 投稿话题;填写后搜索并选择精确同名话题
|
|
337
|
+
|
|
338
|
+
微博 Front Matter:
|
|
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 || "空"}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
189
413
|
function printFeatures() {
|
|
190
414
|
console.log("");
|
|
191
415
|
console.log(chalk.green("你现在可以使用以下功能:"));
|
|
@@ -199,12 +423,16 @@ function printFeatures() {
|
|
|
199
423
|
console.log(" qcplay-cli auth permissions --key www-article-list.store");
|
|
200
424
|
console.log("");
|
|
201
425
|
console.log(chalk.cyan("3. 发布官网文章"));
|
|
202
|
-
console.log(" qcplay-cli article import
|
|
426
|
+
console.log(" qcplay-cli article import <微信文章或飞书文档链接> article.md");
|
|
203
427
|
console.log(" qcplay-cli www-article-list.store article.md");
|
|
204
428
|
console.log(" qcplay-cli article update [id]");
|
|
205
429
|
console.log(" qcplay-cli article delete [id]");
|
|
206
430
|
console.log("");
|
|
207
|
-
console.log(chalk.cyan("4.
|
|
431
|
+
console.log(chalk.cyan("4. 多平台分发"));
|
|
432
|
+
console.log(" qcplay-cli publish article.md --project 最强蜗牛");
|
|
433
|
+
console.log(" qcplay-cli publish --project 最强蜗牛 --list-platforms");
|
|
434
|
+
console.log("");
|
|
435
|
+
console.log(chalk.cyan("5. 查看本地目录"));
|
|
208
436
|
console.log(" qcplay-cli where");
|
|
209
437
|
console.log("");
|
|
210
438
|
}
|
|
@@ -272,6 +500,16 @@ async function ensureLocalDirs() {
|
|
|
272
500
|
await ensureDir(SKILLS_DIR);
|
|
273
501
|
}
|
|
274
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
|
+
|
|
275
513
|
async function filesAreEqual(sourcePath, targetPath) {
|
|
276
514
|
try {
|
|
277
515
|
const [sourceContent, targetContent] = await Promise.all([
|
|
@@ -381,6 +619,28 @@ function normalizeMappedValue(value, map) {
|
|
|
381
619
|
return map[text] ?? map[text.toLowerCase?.() || ""] ?? text;
|
|
382
620
|
}
|
|
383
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
|
+
|
|
384
644
|
function normalizeBoolLike(value, defaultValue = "0") {
|
|
385
645
|
const text = normalizeText(value);
|
|
386
646
|
if (!text) {
|
|
@@ -472,7 +732,12 @@ function parsePublishOptions(args) {
|
|
|
472
732
|
const options = {
|
|
473
733
|
backend: undefined,
|
|
474
734
|
local: false,
|
|
475
|
-
dryRun: false
|
|
735
|
+
dryRun: false,
|
|
736
|
+
project: "",
|
|
737
|
+
region: "",
|
|
738
|
+
rulesFile: "",
|
|
739
|
+
title: "",
|
|
740
|
+
cover: ""
|
|
476
741
|
};
|
|
477
742
|
const positional = [];
|
|
478
743
|
|
|
@@ -484,13 +749,21 @@ function parsePublishOptions(args) {
|
|
|
484
749
|
continue;
|
|
485
750
|
}
|
|
486
751
|
|
|
487
|
-
if (
|
|
752
|
+
if (["--backend", "--project", "--region", "--rules-file", "--title", "--cover"].includes(current)) {
|
|
488
753
|
const next = args[index + 1];
|
|
489
754
|
if (!next || next.startsWith("--")) {
|
|
490
|
-
throw new Error(
|
|
755
|
+
throw new Error(`${current} 缺少参数值`);
|
|
491
756
|
}
|
|
492
757
|
|
|
493
|
-
|
|
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;
|
|
494
767
|
index += 1;
|
|
495
768
|
continue;
|
|
496
769
|
}
|
|
@@ -517,6 +790,136 @@ function parsePublishOptions(args) {
|
|
|
517
790
|
};
|
|
518
791
|
}
|
|
519
792
|
|
|
793
|
+
function parseDistributionOptions(args) {
|
|
794
|
+
const options = {
|
|
795
|
+
backend: undefined,
|
|
796
|
+
local: false,
|
|
797
|
+
dryRun: false,
|
|
798
|
+
project: "",
|
|
799
|
+
region: "",
|
|
800
|
+
platforms: [],
|
|
801
|
+
listPlatforms: false,
|
|
802
|
+
configFile: "",
|
|
803
|
+
rulesFile: "",
|
|
804
|
+
title: "",
|
|
805
|
+
cover: "",
|
|
806
|
+
weiboColumn: "",
|
|
807
|
+
browserChannel: "",
|
|
808
|
+
reviewDraft: false,
|
|
809
|
+
keepOpen: false,
|
|
810
|
+
openPage: false,
|
|
811
|
+
switchAccount: false,
|
|
812
|
+
taptapSecretCode: "",
|
|
813
|
+
taptapSecretPeriod: "",
|
|
814
|
+
taptapOfficialGroup: ""
|
|
815
|
+
};
|
|
816
|
+
const positional = [];
|
|
817
|
+
|
|
818
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
819
|
+
const current = args[index];
|
|
820
|
+
if (current === "--dry-run") {
|
|
821
|
+
options.dryRun = true;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (current === "--local") {
|
|
825
|
+
options.local = true;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (current === "--list-platforms") {
|
|
829
|
+
options.listPlatforms = true;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (current === "--keep-open") {
|
|
833
|
+
options.keepOpen = true;
|
|
834
|
+
continue;
|
|
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
|
+
}
|
|
844
|
+
if (current === "--review-draft" || current === "--review") {
|
|
845
|
+
options.reviewDraft = true;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const valueFlags = new Map([
|
|
850
|
+
["--backend", "backend"],
|
|
851
|
+
["--project", "project"],
|
|
852
|
+
["--region", "region"],
|
|
853
|
+
["--config-file", "configFile"],
|
|
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"]
|
|
862
|
+
]);
|
|
863
|
+
if (valueFlags.has(current)) {
|
|
864
|
+
const next = args[index + 1];
|
|
865
|
+
if (!next || next.startsWith("--")) {
|
|
866
|
+
throw new Error(`${current} 缺少参数值`);
|
|
867
|
+
}
|
|
868
|
+
options[valueFlags.get(current)] = next;
|
|
869
|
+
index += 1;
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (current === "--platform" || current === "-p") {
|
|
874
|
+
const next = args[index + 1];
|
|
875
|
+
if (!next || next.startsWith("--")) {
|
|
876
|
+
throw new Error(`${current} 缺少参数值`);
|
|
877
|
+
}
|
|
878
|
+
options.platforms.push(...next.split(/[,,]/).map(value => value.trim()).filter(Boolean));
|
|
879
|
+
index += 1;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (current.startsWith("--")) {
|
|
884
|
+
throw new Error(`未知参数: ${current}`);
|
|
885
|
+
}
|
|
886
|
+
positional.push(current);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (positional.length > 1) {
|
|
890
|
+
throw new Error(`未知参数: ${positional.slice(1).join(" ")}`);
|
|
891
|
+
}
|
|
892
|
+
return { file: positional[0], options };
|
|
893
|
+
}
|
|
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
|
+
|
|
520
923
|
function parseArticleMutationOptions(args) {
|
|
521
924
|
const parsed = parsePublishOptions(args);
|
|
522
925
|
return {
|
|
@@ -911,9 +1314,13 @@ function normalizeArticleId(value) {
|
|
|
911
1314
|
return articleId;
|
|
912
1315
|
}
|
|
913
1316
|
|
|
914
|
-
function articlePreviewUrl(articleId) {
|
|
915
|
-
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));
|
|
916
1320
|
url.searchParams.set("id", String(articleId));
|
|
1321
|
+
if (gameId === "33") {
|
|
1322
|
+
url.searchParams.set("cate_id", normalizeText(payload.cate_id) || "40");
|
|
1323
|
+
}
|
|
917
1324
|
return url.toString();
|
|
918
1325
|
}
|
|
919
1326
|
|
|
@@ -925,12 +1332,13 @@ function backendOriginsMatch(left, right) {
|
|
|
925
1332
|
}
|
|
926
1333
|
}
|
|
927
1334
|
|
|
928
|
-
async function saveRecentArticleState(articleId, status, backendUrl, previewUrl = "") {
|
|
1335
|
+
async function saveRecentArticleState(articleId, status, backendUrl, previewUrl = "", target = "domestic") {
|
|
929
1336
|
const authState = await loadAuthState();
|
|
930
1337
|
await writeJson(ARTICLE_STATE_FILE, {
|
|
931
1338
|
article_id: articleId,
|
|
932
1339
|
status: String(status),
|
|
933
1340
|
preview_url: previewUrl,
|
|
1341
|
+
target,
|
|
934
1342
|
backend_url: backendUrl,
|
|
935
1343
|
administrator_id: Number(authState.id || 0),
|
|
936
1344
|
account: normalizeText(authState.account),
|
|
@@ -938,16 +1346,16 @@ async function saveRecentArticleState(articleId, status, backendUrl, previewUrl
|
|
|
938
1346
|
});
|
|
939
1347
|
}
|
|
940
1348
|
|
|
941
|
-
async function trySaveRecentArticleState(articleId, status, backendUrl, previewUrl = "") {
|
|
1349
|
+
async function trySaveRecentArticleState(articleId, status, backendUrl, previewUrl = "", target = "domestic") {
|
|
942
1350
|
try {
|
|
943
|
-
await saveRecentArticleState(articleId, status, backendUrl, previewUrl);
|
|
1351
|
+
await saveRecentArticleState(articleId, status, backendUrl, previewUrl, target);
|
|
944
1352
|
return "";
|
|
945
1353
|
} catch (error) {
|
|
946
1354
|
return normalizeText(error?.message) || String(error);
|
|
947
1355
|
}
|
|
948
1356
|
}
|
|
949
1357
|
|
|
950
|
-
async function resolveArticleId(value, backendUrl) {
|
|
1358
|
+
async function resolveArticleId(value, backendUrl, target = "domestic") {
|
|
951
1359
|
if (normalizeText(value)) {
|
|
952
1360
|
return normalizeArticleId(value);
|
|
953
1361
|
}
|
|
@@ -959,6 +1367,9 @@ async function resolveArticleId(value, backendUrl) {
|
|
|
959
1367
|
if (!backendOriginsMatch(recentArticle.backend_url, backendUrl)) {
|
|
960
1368
|
throw new Error("最近文章属于其他后端,请手动输入文章 ID");
|
|
961
1369
|
}
|
|
1370
|
+
if ((recentArticle.target || "domestic") !== target) {
|
|
1371
|
+
throw new Error("最近文章属于其他站点,请手动输入文章 ID");
|
|
1372
|
+
}
|
|
962
1373
|
if (String(recentArticle.status) !== "0") {
|
|
963
1374
|
throw new Error("最近文章不是未上线状态,请手动输入文章 ID");
|
|
964
1375
|
}
|
|
@@ -1069,6 +1480,9 @@ async function installCommand(options) {
|
|
|
1069
1480
|
|
|
1070
1481
|
await ensureLocalDirs();
|
|
1071
1482
|
console.log(chalk.green("✔ 本地目录已创建"));
|
|
1483
|
+
const rulesCreated = await ensureContentRulesFile();
|
|
1484
|
+
console.log(chalk.green(rulesCreated ? "✔ 用户内容规则已创建" : "✔ 用户内容规则已保留"));
|
|
1485
|
+
console.log(`内容规则: ${chalk.gray(CONTENT_RULES_FILE)}`);
|
|
1072
1486
|
|
|
1073
1487
|
const skillsInstalled = await installSkills();
|
|
1074
1488
|
if (skillsInstalled.available) {
|
|
@@ -1121,6 +1535,7 @@ async function updateCommand() {
|
|
|
1121
1535
|
console.log("");
|
|
1122
1536
|
|
|
1123
1537
|
await ensureLocalDirs();
|
|
1538
|
+
const rulesCreated = await ensureContentRulesFile();
|
|
1124
1539
|
await runCommand(getNpmCommand(), ["install", "-g", `${PACKAGE_NAME}@latest`]);
|
|
1125
1540
|
|
|
1126
1541
|
const latestPackage = readPackageMetadata();
|
|
@@ -1135,6 +1550,8 @@ async function updateCommand() {
|
|
|
1135
1550
|
console.log(chalk.gray(`版本: ${latestPackage.version}`));
|
|
1136
1551
|
}
|
|
1137
1552
|
printSkillsSyncResult(skillsInstalled);
|
|
1553
|
+
console.log(chalk.green(rulesCreated ? "✔ 用户内容规则已创建" : "✔ 用户内容规则未覆盖"));
|
|
1554
|
+
console.log(`内容规则: ${chalk.gray(CONTENT_RULES_FILE)}`);
|
|
1138
1555
|
console.log("");
|
|
1139
1556
|
}
|
|
1140
1557
|
|
|
@@ -1578,29 +1995,38 @@ function parseFrontMatter(raw) {
|
|
|
1578
1995
|
}
|
|
1579
1996
|
|
|
1580
1997
|
function normalizeArticlePayload(meta, content) {
|
|
1998
|
+
const articleContent = markdownToHtml(content);
|
|
1999
|
+
const { gameId, category } = normalizeGameAndCategory(meta);
|
|
1581
2000
|
const payload = {
|
|
1582
2001
|
article_title: normalizeText(meta.article_title),
|
|
1583
2002
|
thumbnail: normalizeText(meta.thumbnail),
|
|
1584
2003
|
move_thumbnail: normalizeText(meta.move_thumbnail),
|
|
1585
|
-
article_content:
|
|
2004
|
+
article_content: articleContent,
|
|
1586
2005
|
article_excerpt: normalizeText(meta.article_excerpt),
|
|
1587
2006
|
article_url: normalizeText(meta.article_url),
|
|
1588
2007
|
origin: normalizeText(meta.origin),
|
|
1589
2008
|
status: normalizeMappedValue(meta.status, STATUS_MAP) || "0",
|
|
1590
|
-
cate_id:
|
|
2009
|
+
cate_id: category,
|
|
1591
2010
|
video_link: normalizeText(meta.video_link),
|
|
1592
2011
|
is_hot: normalizeBoolLike(meta.is_hot, "0"),
|
|
1593
2012
|
is_index: normalizeBoolLike(meta.is_index, "0"),
|
|
1594
2013
|
release_time: normalizeText(meta.release_time),
|
|
1595
2014
|
area: normalizeMappedValue(meta.area, AREA_MAP) || "1",
|
|
1596
2015
|
sort: normalizeText(meta.sort) || "1",
|
|
1597
|
-
game_id:
|
|
2016
|
+
game_id: gameId,
|
|
1598
2017
|
is_index2: "0",
|
|
1599
2018
|
index_pc_img: normalizeText(meta.index_pc_img),
|
|
1600
2019
|
index_move_img: normalizeText(meta.index_move_img),
|
|
1601
2020
|
type: "1"
|
|
1602
2021
|
};
|
|
1603
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
|
+
|
|
1604
2030
|
if (!payload.article_title) {
|
|
1605
2031
|
throw new Error("article_title 不能为空");
|
|
1606
2032
|
}
|
|
@@ -1616,6 +2042,19 @@ function normalizeArticlePayload(meta, content) {
|
|
|
1616
2042
|
return payload;
|
|
1617
2043
|
}
|
|
1618
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
|
+
|
|
1619
2058
|
async function parseArticleFile(file) {
|
|
1620
2059
|
const articleFile = path.resolve(process.cwd(), file);
|
|
1621
2060
|
if (!(await pathExists(articleFile))) {
|
|
@@ -1626,12 +2065,14 @@ async function parseArticleFile(file) {
|
|
|
1626
2065
|
const parsed = parseFrontMatter(raw);
|
|
1627
2066
|
return {
|
|
1628
2067
|
articleFile,
|
|
1629
|
-
payload: normalizeArticlePayload(parsed.meta, parsed.content)
|
|
2068
|
+
payload: normalizeArticlePayload(parsed.meta, parsed.content),
|
|
2069
|
+
meta: parsed.meta,
|
|
2070
|
+
markdown: parsed.content
|
|
1630
2071
|
};
|
|
1631
2072
|
}
|
|
1632
2073
|
|
|
1633
|
-
async function initArticleTemplate(file = "article.md") {
|
|
1634
|
-
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}`);
|
|
1635
2076
|
const targetFile = path.resolve(process.cwd(), file);
|
|
1636
2077
|
|
|
1637
2078
|
if (!(await pathExists(sourceFile))) {
|
|
@@ -1649,13 +2090,13 @@ async function initArticleTemplate(file = "article.md") {
|
|
|
1649
2090
|
console.log("");
|
|
1650
2091
|
console.log("请编辑该文件后执行:");
|
|
1651
2092
|
console.log("");
|
|
1652
|
-
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}`);
|
|
1653
2094
|
console.log("");
|
|
1654
2095
|
}
|
|
1655
2096
|
|
|
1656
|
-
async function
|
|
2097
|
+
async function importArticleCommand(sourceUrl, file = "article.md", options = {}) {
|
|
1657
2098
|
if (!sourceUrl) {
|
|
1658
|
-
throw new Error("
|
|
2099
|
+
throw new Error("缺少文章来源链接,请提供微信公众号文章或飞书 Docx/Wiki 文档链接");
|
|
1659
2100
|
}
|
|
1660
2101
|
|
|
1661
2102
|
const targetFile = path.resolve(process.cwd(), file);
|
|
@@ -1664,30 +2105,73 @@ async function importWechatArticleCommand(sourceUrl, file = "article.md") {
|
|
|
1664
2105
|
}
|
|
1665
2106
|
|
|
1666
2107
|
console.log("");
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
2108
|
+
let article;
|
|
2109
|
+
let sourceLabel;
|
|
2110
|
+
try {
|
|
2111
|
+
parseLarkDocumentUrl(sourceUrl);
|
|
2112
|
+
sourceLabel = "飞书文档";
|
|
2113
|
+
console.log(chalk.cyan("正在获取飞书文档完整格式..."));
|
|
2114
|
+
article = await importLarkArticle(sourceUrl);
|
|
2115
|
+
} catch (larkError) {
|
|
2116
|
+
if (!/飞书文档链接无效|仅支持飞书 Docx 或 Wiki 文档链接/.test(larkError.message)) {
|
|
2117
|
+
throw larkError;
|
|
1671
2118
|
}
|
|
1672
|
-
|
|
2119
|
+
sourceLabel = "微信文章";
|
|
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
|
+
}
|
|
2127
|
+
article = await importWechatArticle(sourceUrl, {
|
|
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}`));
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
1673
2137
|
const output = buildOfficialArticleMarkdown(article);
|
|
1674
2138
|
await ensureDir(path.dirname(targetFile));
|
|
1675
2139
|
await fs.promises.writeFile(targetFile, output, { encoding: "utf8", flag: "wx" });
|
|
1676
2140
|
|
|
1677
2141
|
console.log("");
|
|
1678
|
-
console.log(chalk.green(
|
|
2142
|
+
console.log(chalk.green(`${sourceLabel}已转换为官网文章草稿:`));
|
|
1679
2143
|
console.log(targetFile);
|
|
1680
2144
|
console.log(chalk.gray(`标题: ${article.title}`));
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2145
|
+
if (sourceLabel === "飞书文档") {
|
|
2146
|
+
console.log(chalk.gray(`已保留图片: ${article.imageCount} 张,表格: ${article.tableCount} 个`));
|
|
2147
|
+
console.log(chalk.gray("已保留标题、列表、引用、颜色、高亮、对齐、列宽及合并单元格"));
|
|
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
|
+
}
|
|
2161
|
+
console.log(chalk.gray(`已转存图片: ${article.imageCount} 张`));
|
|
2162
|
+
if (article.videoCount > 0) {
|
|
2163
|
+
console.log(chalk.gray(`已转存视频: ${article.videoCount} 个`));
|
|
2164
|
+
}
|
|
2165
|
+
console.log(chalk.gray(`已提取正文: ${article.textSegmentCount} 段`));
|
|
2166
|
+
console.log(chalk.gray(`已保留文字颜色: ${article.colorCount} 处`));
|
|
2167
|
+
console.log(
|
|
2168
|
+
chalk.gray(
|
|
2169
|
+
`特殊内容: ${article.specialElementCount} 处,媒体: ${article.mediaCount} 处,表格: ${article.tableCount} 个`
|
|
2170
|
+
)
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
1689
2173
|
console.log("");
|
|
1690
|
-
console.log("请补充 cate_id 等业务字段,确认内容后执行:");
|
|
2174
|
+
console.log(sourceLabel === "飞书文档" ? "请补充 thumbnail、cate_id 等业务字段,确认内容后执行:" : "请补充 cate_id 等业务字段,确认内容后执行:");
|
|
1691
2175
|
console.log("");
|
|
1692
2176
|
console.log(` qcplay-cli www-article-list.store ${file}`);
|
|
1693
2177
|
console.log("");
|
|
@@ -1720,8 +2204,25 @@ release_time: 2026-07-16
|
|
|
1720
2204
|
area: pc
|
|
1721
2205
|
sort: 100
|
|
1722
2206
|
game_id: 最强蜗牛
|
|
2207
|
+
distribution_project: ""
|
|
1723
2208
|
index_pc_img: ""
|
|
1724
2209
|
index_move_img: ""
|
|
2210
|
+
taptap_type: ""
|
|
2211
|
+
taptap_forum: ""
|
|
2212
|
+
taptap_scheduled: "0"
|
|
2213
|
+
taptap_draft: "0"
|
|
2214
|
+
taptap_images: ""
|
|
2215
|
+
taptap_video: ""
|
|
2216
|
+
taptap_cover: ""
|
|
2217
|
+
bilibili_type: article
|
|
2218
|
+
bilibili_video: ""
|
|
2219
|
+
bilibili_cover: ""
|
|
2220
|
+
bilibili_category: ""
|
|
2221
|
+
bilibili_tags: ""
|
|
2222
|
+
bilibili_topic: ""
|
|
2223
|
+
weibo_intro: ""
|
|
2224
|
+
weibo_super_topic: ""
|
|
2225
|
+
weibo_cover: ""
|
|
1725
2226
|
---
|
|
1726
2227
|
|
|
1727
2228
|
# 文章标题
|
|
@@ -1736,16 +2237,245 @@ ${chalk.cyan("发布命令:")}
|
|
|
1736
2237
|
${chalk.cyan("从微信推文生成官网草稿:")}
|
|
1737
2238
|
|
|
1738
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)
|
|
1739
2247
|
`);
|
|
1740
2248
|
}
|
|
1741
2249
|
|
|
1742
|
-
|
|
2250
|
+
function inferDistributionProject(payload, explicitProject, meta = {}) {
|
|
2251
|
+
if (normalizeText(explicitProject)) {
|
|
2252
|
+
return normalizeText(explicitProject);
|
|
2253
|
+
}
|
|
2254
|
+
if (normalizeText(meta.distribution_project)) {
|
|
2255
|
+
return normalizeText(meta.distribution_project);
|
|
2256
|
+
}
|
|
2257
|
+
if (payload?.game_id === "39") {
|
|
2258
|
+
return "最强蜗牛";
|
|
2259
|
+
}
|
|
2260
|
+
return "";
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function printPlatformEntries(entries) {
|
|
2264
|
+
console.log("");
|
|
2265
|
+
console.log(chalk.cyan("可用发布平台:"));
|
|
2266
|
+
console.log("");
|
|
2267
|
+
entries.forEach(entry => {
|
|
2268
|
+
const status = entry.publisher === "blocked" || entry.publisher === "unavailable" ? "不可自动发布" : entry.publisher;
|
|
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
|
+
}
|
|
2276
|
+
});
|
|
2277
|
+
console.log("");
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
async function publishPlatformsCommand(file, options) {
|
|
2281
|
+
let parsedArticle;
|
|
2282
|
+
if (file) {
|
|
2283
|
+
parsedArticle = await parseArticleFile(file);
|
|
2284
|
+
parsedArticle = applyPublishOverrides(parsedArticle, options);
|
|
2285
|
+
}
|
|
2286
|
+
const allEntries = await loadPlatformEntries({ configFile: options.configFile });
|
|
2287
|
+
|
|
2288
|
+
const project = inferDistributionProject(parsedArticle?.payload, options.project, parsedArticle?.meta);
|
|
2289
|
+
if (!project) {
|
|
2290
|
+
if (options.listPlatforms) {
|
|
2291
|
+
printPlatformEntries(allEntries);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
throw new Error(`缺少项目名称。可用项目: ${availableProjects(allEntries).join("、")}。请使用 --project <项目>`);
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
const candidates = entriesForProject(allEntries, project, options.region);
|
|
2298
|
+
if (candidates.length === 0) {
|
|
2299
|
+
const regionHint = options.region ? ` / ${options.region}` : "";
|
|
2300
|
+
throw new Error(`平台配置中没有找到项目 ${project}${regionHint}`);
|
|
2301
|
+
}
|
|
2302
|
+
if (options.listPlatforms) {
|
|
2303
|
+
printPlatformEntries(candidates);
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
if (!parsedArticle && !options.openPage) {
|
|
2307
|
+
throw new Error("缺少文章文件,例如:qcplay-cli publish article.md --platform TapTap");
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
const selectedEntries = options.platforms.length
|
|
2311
|
+
? selectRequestedEntries(candidates, options.platforms)
|
|
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
|
+
};
|
|
2352
|
+
const planningArticle = {
|
|
2353
|
+
...parsedArticle,
|
|
2354
|
+
title: parsedArticle.payload.article_title,
|
|
2355
|
+
html: parsedArticle.payload.article_content,
|
|
2356
|
+
platformInputs
|
|
2357
|
+
};
|
|
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
|
+
});
|
|
2368
|
+
if (options.dryRun) {
|
|
2369
|
+
console.log(JSON.stringify({ article: parsedArticle.articleFile, project, platforms: plan }, null, 2));
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
const article = {
|
|
2374
|
+
articleFile: parsedArticle.articleFile,
|
|
2375
|
+
title: parsedArticle.payload.article_title,
|
|
2376
|
+
markdown: parsedArticle.markdown,
|
|
2377
|
+
html: parsedArticle.payload.article_content,
|
|
2378
|
+
payload: parsedArticle.payload,
|
|
2379
|
+
meta: parsedArticle.meta,
|
|
2380
|
+
platformInputs
|
|
2381
|
+
};
|
|
2382
|
+
const { successes, failures, skipped } = await runPlatformPublishSequence(selected, async entry => {
|
|
2383
|
+
console.log("");
|
|
2384
|
+
console.log(chalk.cyan(`正在发布到 ${entry.platform}(${entry.region})...`));
|
|
2385
|
+
let result;
|
|
2386
|
+
if (entry.publisher === "official-api") {
|
|
2387
|
+
await publishArticleCommand(file, options, entry);
|
|
2388
|
+
} else {
|
|
2389
|
+
result = await publishPlatformEntry(entry, article, {
|
|
2390
|
+
browserChannel: options.browserChannel,
|
|
2391
|
+
reviewDraft: options.reviewDraft,
|
|
2392
|
+
keepOpen: options.keepOpen,
|
|
2393
|
+
onProgress: message => console.log(chalk.gray(` ${message}`))
|
|
2394
|
+
});
|
|
2395
|
+
if (entry.platformKey === "taptap" && result?.status === "draft") {
|
|
2396
|
+
console.log(chalk.green("TapTap 草稿已保存"));
|
|
2397
|
+
return result;
|
|
2398
|
+
}
|
|
2399
|
+
if (entry.platformKey === "taptap" && result?.status === "scheduled") {
|
|
2400
|
+
console.log(chalk.green("TapTap 定时发布已提交"));
|
|
2401
|
+
return result;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
console.log(chalk.green(`${entry.platform} 发布流程完成`));
|
|
2405
|
+
return result;
|
|
2406
|
+
});
|
|
2407
|
+
failures.forEach(({ entry, error }) => {
|
|
2408
|
+
console.error(chalk.red(`${entry.platform} 发布失败: ${error.message || error}`));
|
|
2409
|
+
});
|
|
2410
|
+
if (skipped.length > 0) {
|
|
2411
|
+
console.error(chalk.yellow("已停止后续平台发布,请先处理当前浏览器平台的登录或页面问题。"));
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
console.log("");
|
|
2415
|
+
console.log(`发布完成: ${successes.length} 个成功,${failures.length} 个失败,${skipped.length} 个未执行`);
|
|
2416
|
+
failures.forEach(({ entry, error }) => {
|
|
2417
|
+
console.log(` - ${entry.platform}(${entry.region}): ${error.message || error}`);
|
|
2418
|
+
});
|
|
2419
|
+
skipped.forEach(entry => {
|
|
2420
|
+
console.log(` - ${entry.platform}(${entry.region}): 未执行`);
|
|
2421
|
+
});
|
|
2422
|
+
if (failures.length > 0) {
|
|
2423
|
+
throw new Error("部分平台发布失败,请根据上面的错误处理后重试失败平台");
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
async function publishArticleCommand(file, options, platformEntry = null, expectedTarget = "") {
|
|
1743
2428
|
if (!file) {
|
|
1744
2429
|
throw new Error("缺少文章文件,例如:qcplay-cli www-article-list.store article.md");
|
|
1745
2430
|
}
|
|
1746
2431
|
|
|
1747
|
-
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
|
+
}
|
|
1748
2474
|
payload.status = "0";
|
|
2475
|
+
const target = articleTarget(payload);
|
|
2476
|
+
if (expectedTarget && target !== expectedTarget) {
|
|
2477
|
+
throw new Error("欧美蜗牛官网文章必须使用 game_id: 64 和有效的 language");
|
|
2478
|
+
}
|
|
1749
2479
|
const config = await loadConfig();
|
|
1750
2480
|
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
1751
2481
|
|
|
@@ -1755,40 +2485,46 @@ async function publishArticleCommand(file, options) {
|
|
|
1755
2485
|
}
|
|
1756
2486
|
|
|
1757
2487
|
console.log("");
|
|
1758
|
-
console.log(chalk.cyan("正在保存官网文章为未上线状态..."));
|
|
2488
|
+
console.log(chalk.cyan(target === "west" ? "正在保存欧美蜗牛官网文章为未上线状态..." : "正在保存官网文章为未上线状态..."));
|
|
1759
2489
|
console.log(chalk.gray(`文章文件: ${articleFile}`));
|
|
1760
2490
|
console.log("");
|
|
1761
2491
|
|
|
1762
|
-
const response = await authenticatedRequest("POST", backendUrl, "
|
|
2492
|
+
const response = await authenticatedRequest("POST", backendUrl, articleApiPath(target, "publish"), payload);
|
|
1763
2493
|
const rawArticleId = response.data?.id ?? response.data?.article_id ?? response.data?.articleId;
|
|
1764
2494
|
const articleId = normalizeArticleId(rawArticleId);
|
|
1765
|
-
const previewUrl = normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId);
|
|
1766
|
-
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);
|
|
1767
2497
|
|
|
1768
2498
|
console.log(chalk.green(response.message || "文章已保存为未上线状态"));
|
|
1769
2499
|
console.log(`文章 ID: ${articleId}`);
|
|
1770
2500
|
console.log("文章状态: 未上线");
|
|
1771
|
-
|
|
2501
|
+
if (previewUrl) {
|
|
2502
|
+
console.log(`预览地址: ${previewUrl}`);
|
|
2503
|
+
} else {
|
|
2504
|
+
console.log(chalk.yellow("欧美官网预览地址尚未配置,请在后台确认草稿内容和排版。"));
|
|
2505
|
+
}
|
|
1772
2506
|
if (stateWarning) {
|
|
1773
2507
|
console.log(chalk.yellow(`本地未能记录最近文章 ID,后续操作请手动输入 ${articleId}: ${stateWarning}`));
|
|
1774
2508
|
}
|
|
1775
2509
|
console.log("");
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
2510
|
+
if (previewUrl) {
|
|
2511
|
+
try {
|
|
2512
|
+
await openBrowser(previewUrl);
|
|
2513
|
+
console.log(chalk.cyan("已打开文章预览页面,请由用户检查文章内容和排版。"));
|
|
2514
|
+
} catch {
|
|
2515
|
+
console.log(chalk.yellow("自动打开预览页面失败,请手动打开上面的预览地址。"));
|
|
2516
|
+
}
|
|
1781
2517
|
}
|
|
1782
2518
|
console.log("用户确认无误后再执行:");
|
|
1783
2519
|
console.log("");
|
|
1784
|
-
console.log(` qcplay-cli
|
|
2520
|
+
console.log(` qcplay-cli ${articleMutationCommand(target)} ${articleId}`);
|
|
1785
2521
|
console.log("");
|
|
1786
2522
|
}
|
|
1787
2523
|
|
|
1788
|
-
async function updateArticleStatusCommand(articleIdValue, targetStatus, options) {
|
|
2524
|
+
async function updateArticleStatusCommand(articleIdValue, targetStatus, options, target = "domestic") {
|
|
1789
2525
|
const config = await loadConfig();
|
|
1790
2526
|
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
1791
|
-
const articleId = await resolveArticleId(articleIdValue, backendUrl);
|
|
2527
|
+
const articleId = await resolveArticleId(articleIdValue, backendUrl, target);
|
|
1792
2528
|
const payload = {
|
|
1793
2529
|
id: articleId,
|
|
1794
2530
|
status: targetStatus
|
|
@@ -1801,17 +2537,17 @@ async function updateArticleStatusCommand(articleIdValue, targetStatus, options)
|
|
|
1801
2537
|
|
|
1802
2538
|
const action = targetStatus === "2" ? "删除" : "上线";
|
|
1803
2539
|
console.log("");
|
|
1804
|
-
console.log(chalk.cyan(`正在${action}官网文章...`));
|
|
2540
|
+
console.log(chalk.cyan(`正在${action}${target === "west" ? "欧美蜗牛" : ""}官网文章...`));
|
|
1805
2541
|
console.log(chalk.gray(`文章 ID: ${articleId}`));
|
|
1806
2542
|
console.log("");
|
|
1807
2543
|
|
|
1808
|
-
const response = await authenticatedRequest("PATCH", backendUrl, "
|
|
2544
|
+
const response = await authenticatedRequest("PATCH", backendUrl, articleApiPath(target, "status"), payload);
|
|
1809
2545
|
const responseStatus = normalizeText(response.data?.status) || targetStatus;
|
|
1810
2546
|
const previewUrl =
|
|
1811
2547
|
responseStatus === "1"
|
|
1812
|
-
? normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId)
|
|
2548
|
+
? normalizeText(response.data?.preview_url) || (target === "domestic" ? articlePreviewUrl(articleId) : "")
|
|
1813
2549
|
: "";
|
|
1814
|
-
const stateWarning = await trySaveRecentArticleState(articleId, responseStatus, backendUrl, previewUrl);
|
|
2550
|
+
const stateWarning = await trySaveRecentArticleState(articleId, responseStatus, backendUrl, previewUrl, target);
|
|
1815
2551
|
|
|
1816
2552
|
console.log(chalk.green(response.message || (responseStatus === "2" ? "文章已删除" : "文章已上线")));
|
|
1817
2553
|
console.log(`文章 ID: ${articleId}`);
|
|
@@ -1875,6 +2611,31 @@ async function main() {
|
|
|
1875
2611
|
return;
|
|
1876
2612
|
}
|
|
1877
2613
|
|
|
2614
|
+
if (command === "publish") {
|
|
2615
|
+
if (subcommand === "-h" || subcommand === "--help") {
|
|
2616
|
+
printPublishHelp();
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
const parsed = parseDistributionOptions([subcommand, ...rest].filter(Boolean));
|
|
2620
|
+
await runWithErrorBanner("多平台发布失败", () => publishPlatformsCommand(parsed.file, parsed.options));
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
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
|
+
|
|
1878
2639
|
if (command === "article") {
|
|
1879
2640
|
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
1880
2641
|
if (!subcommand) {
|
|
@@ -1891,16 +2652,17 @@ async function main() {
|
|
|
1891
2652
|
return;
|
|
1892
2653
|
}
|
|
1893
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
|
+
|
|
1894
2661
|
if (subcommand === "import") {
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
throw new Error(`未知参数: ${rest.slice(2).join(" ")}`);
|
|
1900
|
-
});
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
await runWithErrorBanner("转换失败", () => importWechatArticleCommand(sourceUrl, file));
|
|
2662
|
+
const parsed = parseArticleImportOptions(rest);
|
|
2663
|
+
await runWithErrorBanner("转换失败", () =>
|
|
2664
|
+
importArticleCommand(parsed.sourceUrl, parsed.file, parsed.options)
|
|
2665
|
+
);
|
|
1904
2666
|
return;
|
|
1905
2667
|
}
|
|
1906
2668
|
|
|
@@ -1940,6 +2702,22 @@ async function main() {
|
|
|
1940
2702
|
return;
|
|
1941
2703
|
}
|
|
1942
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
|
+
|
|
1943
2721
|
if (command === "features") {
|
|
1944
2722
|
printFeatures();
|
|
1945
2723
|
return;
|