@qcplay/cli 1.0.10 → 1.0.12
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 +23 -3
- package/bin/qcplay.js +282 -14
- package/lib/wechat-article.js +638 -20
- package/package.json +1 -1
- package/templates/skills/qcplay-publish-article/SKILL.md +134 -149
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 登录默认打开线上页面:`https://cli.qcg.ink/auth.html`
|
|
10
10
|
- `qcplay-cli install` 初始化完成后会自动打开登录页并等待登录完成
|
|
11
11
|
- 登录凭证保存在当前用户本机的 `~/.qcplay/auth.json`,不同客户端之间不共享账号
|
|
12
|
-
-
|
|
12
|
+
- 状态、权限和文章请求通过 Bearer Token 识别当前账号
|
|
13
13
|
- `qcplay-cli article import <微信文章链接> [文件]` 会提取正文、转存图片并生成未发布的官网文章草稿
|
|
14
14
|
- `qcplay-cli update` 会更新 npm 全局包,并同步 `~/.agents/skills`
|
|
15
15
|
- 本地调试时可直接使用 `--local`
|
|
@@ -24,14 +24,34 @@
|
|
|
24
24
|
qcplay-cli article import "https://mp.weixin.qq.com/s/..." article.md
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
转换流程会读取微信文章标题、来源、发布日期和完整正文,经过安全清理后保留微信原文的 HTML 层级、内联样式、字号、颜色、对齐、间距、媒体链接和表格,并转存正文图片、CSS 背景图及封面图。背景图片仍保留在原容器中,不会额外生成一张普通图片。正文保持微信的 `677px` 阅读宽度并兼容窄屏;独占一行的图片会使用块级布局,纯图片标题容器会转换为普通区块,避免官网 PC 标题样式造成相邻图片重叠。任何正文节点遗漏或图片处理失败都会终止转换,不生成不完整草稿。输出固定为 `status: "0"`,并保留空的 `cate_id` 供发布前确认。
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
确认草稿内容和业务字段后保存为未上线文章:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
qcplay-cli www-article-list.store article.md
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
保存成功后,CLI 会输出文章 ID、`未上线` 状态和可点击的排版预览地址:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
https://snail.qingcigame.com/official/news-details.html?id=<文章ID>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
检查 PC 和移动端排版无误后再上线。ID 可以省略,此时只使用当前账号最近一次保存的未上线文章:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
qcplay-cli article update [id]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
删除使用软删除,将同一文章的 `status` 更新为 `2`:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
qcplay-cli article delete [id]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
兼容权限名形式的命令:`www-article-list.update [id]` 和 `www-article-list.delete [id]`。首次保存始终强制使用 `status: "0"`,不会因为 Front Matter 中误填 `1` 而直接上线。
|
|
54
|
+
|
|
35
55
|
仓库整体需求说明仍在根目录:
|
|
36
56
|
|
|
37
57
|
- `../docs/requirement.md`
|
package/bin/qcplay.js
CHANGED
|
@@ -9,7 +9,12 @@ import path from "path";
|
|
|
9
9
|
import readline from "readline";
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
buildOfficialArticleMarkdown,
|
|
14
|
+
importWechatArticle,
|
|
15
|
+
normalizeArticleColor,
|
|
16
|
+
sanitizeArticleRichHtml
|
|
17
|
+
} from "../lib/wechat-article.js";
|
|
13
18
|
|
|
14
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
20
|
const __dirname = path.dirname(__filename);
|
|
@@ -94,6 +99,8 @@ const QCPLAY_DIR = path.join(os.homedir(), ".qcplay");
|
|
|
94
99
|
const AGENTS_DIR = path.join(os.homedir(), ".agents");
|
|
95
100
|
const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
|
|
96
101
|
const AUTH_FILE = path.join(QCPLAY_DIR, "auth.json");
|
|
102
|
+
const ARTICLE_STATE_FILE = path.join(QCPLAY_DIR, "article-state.json");
|
|
103
|
+
const ARTICLE_PREVIEW_BASE_URL = "https://snail.qingcigame.com/official/news-details.html";
|
|
97
104
|
const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
98
105
|
|
|
99
106
|
function readPackageMetadata() {
|
|
@@ -144,7 +151,11 @@ Usage:
|
|
|
144
151
|
qcplay-cli article init [file]
|
|
145
152
|
qcplay-cli article import <wechat-url> [file]
|
|
146
153
|
qcplay-cli article publish <file> [--local] [--backend <url>] [--dry-run]
|
|
154
|
+
qcplay-cli article update [id] [--local] [--backend <url>] [--dry-run]
|
|
155
|
+
qcplay-cli article delete [id] [--local] [--backend <url>] [--dry-run]
|
|
147
156
|
qcplay-cli www-article-list.store <file> [--local] [--backend <url>] [--dry-run]
|
|
157
|
+
qcplay-cli www-article-list.update [id] [--local] [--backend <url>] [--dry-run]
|
|
158
|
+
qcplay-cli www-article-list.delete [id] [--local] [--backend <url>] [--dry-run]
|
|
148
159
|
qcplay-cli features
|
|
149
160
|
qcplay-cli where
|
|
150
161
|
|
|
@@ -170,7 +181,9 @@ function printArticleHelp() {
|
|
|
170
181
|
qcplay-cli article
|
|
171
182
|
qcplay-cli article init [file]
|
|
172
183
|
qcplay-cli article import <wechat-url> [file]
|
|
173
|
-
qcplay-cli article publish <file> [--local] [--backend <url>] [--dry-run]
|
|
184
|
+
qcplay-cli article publish <file> [--local] [--backend <url>] [--dry-run]
|
|
185
|
+
qcplay-cli article update [id] [--local] [--backend <url>] [--dry-run]
|
|
186
|
+
qcplay-cli article delete [id] [--local] [--backend <url>] [--dry-run]`);
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
function printFeatures() {
|
|
@@ -188,6 +201,8 @@ function printFeatures() {
|
|
|
188
201
|
console.log(chalk.cyan("3. 发布官网文章"));
|
|
189
202
|
console.log(" qcplay-cli article import <微信文章链接> article.md");
|
|
190
203
|
console.log(" qcplay-cli www-article-list.store article.md");
|
|
204
|
+
console.log(" qcplay-cli article update [id]");
|
|
205
|
+
console.log(" qcplay-cli article delete [id]");
|
|
191
206
|
console.log("");
|
|
192
207
|
console.log(chalk.cyan("4. 查看本地目录"));
|
|
193
208
|
console.log(" qcplay-cli where");
|
|
@@ -492,12 +507,24 @@ function parsePublishOptions(args) {
|
|
|
492
507
|
positional.push(current);
|
|
493
508
|
}
|
|
494
509
|
|
|
510
|
+
if (positional.length > 1) {
|
|
511
|
+
throw new Error(`未知参数: ${positional.slice(1).join(" ")}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
495
514
|
return {
|
|
496
515
|
file: positional[0],
|
|
497
516
|
options
|
|
498
517
|
};
|
|
499
518
|
}
|
|
500
519
|
|
|
520
|
+
function parseArticleMutationOptions(args) {
|
|
521
|
+
const parsed = parsePublishOptions(args);
|
|
522
|
+
return {
|
|
523
|
+
articleId: parsed.file,
|
|
524
|
+
options: parsed.options
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
501
528
|
function requestJson(method, baseUrl, pathname, payload, headers = {}) {
|
|
502
529
|
return new Promise((resolve, reject) => {
|
|
503
530
|
const url = new URL(pathname, baseUrl);
|
|
@@ -869,6 +896,87 @@ async function authenticatedRequest(method, baseUrl, pathname, payload) {
|
|
|
869
896
|
});
|
|
870
897
|
}
|
|
871
898
|
|
|
899
|
+
function normalizeArticleId(value) {
|
|
900
|
+
const text = normalizeText(value);
|
|
901
|
+
if (!/^\d+$/.test(text)) {
|
|
902
|
+
throw new Error("文章 ID 必须是正整数");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const articleId = Number(text);
|
|
906
|
+
if (!Number.isSafeInteger(articleId) || articleId <= 0) {
|
|
907
|
+
throw new Error("文章 ID 必须是正整数");
|
|
908
|
+
}
|
|
909
|
+
return articleId;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function articlePreviewUrl(articleId) {
|
|
913
|
+
const url = new URL(ARTICLE_PREVIEW_BASE_URL);
|
|
914
|
+
url.searchParams.set("id", String(articleId));
|
|
915
|
+
return url.toString();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function backendOriginsMatch(left, right) {
|
|
919
|
+
try {
|
|
920
|
+
return new URL(left).origin === new URL(right).origin;
|
|
921
|
+
} catch {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function saveRecentArticleState(articleId, status, backendUrl, previewUrl = "") {
|
|
927
|
+
const authState = await loadAuthState();
|
|
928
|
+
await writeJson(ARTICLE_STATE_FILE, {
|
|
929
|
+
article_id: articleId,
|
|
930
|
+
status: String(status),
|
|
931
|
+
preview_url: previewUrl,
|
|
932
|
+
backend_url: backendUrl,
|
|
933
|
+
administrator_id: Number(authState.id || 0),
|
|
934
|
+
account: normalizeText(authState.account),
|
|
935
|
+
updated_at: Date.now()
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async function trySaveRecentArticleState(articleId, status, backendUrl, previewUrl = "") {
|
|
940
|
+
try {
|
|
941
|
+
await saveRecentArticleState(articleId, status, backendUrl, previewUrl);
|
|
942
|
+
return "";
|
|
943
|
+
} catch (error) {
|
|
944
|
+
return normalizeText(error?.message) || String(error);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function resolveArticleId(value, backendUrl) {
|
|
949
|
+
if (normalizeText(value)) {
|
|
950
|
+
return normalizeArticleId(value);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const [recentArticle, authState] = await Promise.all([readJson(ARTICLE_STATE_FILE), loadAuthState()]);
|
|
954
|
+
if (!recentArticle.article_id) {
|
|
955
|
+
throw new Error("没有找到最近发布的未上线文章,请手动输入文章 ID");
|
|
956
|
+
}
|
|
957
|
+
if (!backendOriginsMatch(recentArticle.backend_url, backendUrl)) {
|
|
958
|
+
throw new Error("最近文章属于其他后端,请手动输入文章 ID");
|
|
959
|
+
}
|
|
960
|
+
if (String(recentArticle.status) !== "0") {
|
|
961
|
+
throw new Error("最近文章不是未上线状态,请手动输入文章 ID");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const recentAdministratorId = Number(recentArticle.administrator_id || 0);
|
|
965
|
+
const currentAdministratorId = Number(authState.id || 0);
|
|
966
|
+
if (recentAdministratorId && recentAdministratorId !== currentAdministratorId) {
|
|
967
|
+
throw new Error("最近文章属于其他登录账号,请手动输入文章 ID");
|
|
968
|
+
}
|
|
969
|
+
if (
|
|
970
|
+
!recentAdministratorId &&
|
|
971
|
+
normalizeText(recentArticle.account) &&
|
|
972
|
+
normalizeText(recentArticle.account) !== normalizeText(authState.account)
|
|
973
|
+
) {
|
|
974
|
+
throw new Error("最近文章属于其他登录账号,请手动输入文章 ID");
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return normalizeArticleId(recentArticle.article_id);
|
|
978
|
+
}
|
|
979
|
+
|
|
872
980
|
function flattenPermissionTree(items, bucket = []) {
|
|
873
981
|
for (const item of items) {
|
|
874
982
|
bucket.push(item);
|
|
@@ -1162,14 +1270,14 @@ function applyInlineMarkdown(text) {
|
|
|
1162
1270
|
output = output.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) => {
|
|
1163
1271
|
return storeToken(
|
|
1164
1272
|
`<img src="${escapeHtml(url.trim())}" alt="${escapeHtml(alt.trim())}" ` +
|
|
1165
|
-
'style="display:block;width:auto;max-width:100%;height:auto;margin:24px auto;" />'
|
|
1273
|
+
'style="display:block;box-sizing:border-box;width:auto;max-width:100%;height:auto;object-fit:contain;margin:24px auto;" />'
|
|
1166
1274
|
);
|
|
1167
1275
|
});
|
|
1168
1276
|
|
|
1169
1277
|
output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
|
|
1170
1278
|
return storeToken(
|
|
1171
1279
|
`<a href="${escapeHtml(url.trim())}" target="_blank" rel="noreferrer" ` +
|
|
1172
|
-
`style="color:#576b95;text-decoration:none;">${escapeHtml(label.trim())}</a>`
|
|
1280
|
+
`style="color:#576b95;text-decoration:none;overflow-wrap:anywhere;word-break:break-all;">${escapeHtml(label.trim())}</a>`
|
|
1173
1281
|
);
|
|
1174
1282
|
});
|
|
1175
1283
|
|
|
@@ -1188,12 +1296,23 @@ function applyInlineMarkdown(text) {
|
|
|
1188
1296
|
}
|
|
1189
1297
|
|
|
1190
1298
|
function markdownToHtml(markdown) {
|
|
1191
|
-
const
|
|
1299
|
+
const richHtmlBlocks = [];
|
|
1300
|
+
const normalizedMarkdown = stripBom(markdown)
|
|
1301
|
+
.replace(/\r\n/g, "\n")
|
|
1302
|
+
.replace(
|
|
1303
|
+
/<!--\s*qcplay-rich-html:start\s*-->([\s\S]*?)<!--\s*qcplay-rich-html:end\s*-->/gi,
|
|
1304
|
+
(_, richHtml) => {
|
|
1305
|
+
const index = richHtmlBlocks.push(richHtml) - 1;
|
|
1306
|
+
return `\n@@QCRICHHTML${index}@@\n`;
|
|
1307
|
+
}
|
|
1308
|
+
);
|
|
1309
|
+
const lines = normalizedMarkdown.split("\n");
|
|
1192
1310
|
const html = [];
|
|
1193
1311
|
let paragraph = [];
|
|
1194
1312
|
let quote = [];
|
|
1195
1313
|
let listType = "";
|
|
1196
1314
|
let listItems = [];
|
|
1315
|
+
let tableLines = [];
|
|
1197
1316
|
let inCodeBlock = false;
|
|
1198
1317
|
let codeLines = [];
|
|
1199
1318
|
|
|
@@ -1203,7 +1322,7 @@ function markdownToHtml(markdown) {
|
|
|
1203
1322
|
}
|
|
1204
1323
|
|
|
1205
1324
|
html.push(
|
|
1206
|
-
`<p style="margin:0 0 20px;font-size:16px;line-height:1.9;color:#3f3f3f;text-align:justify;">` +
|
|
1325
|
+
`<p style="margin:0 0 20px;font-size:16px;line-height:1.9;color:#3f3f3f;text-align:justify;overflow-wrap:anywhere;word-break:break-word;">` +
|
|
1207
1326
|
`${paragraph.map(line => applyInlineMarkdown(line)).join("<br />")}</p>`
|
|
1208
1327
|
);
|
|
1209
1328
|
paragraph = [];
|
|
@@ -1251,7 +1370,57 @@ function markdownToHtml(markdown) {
|
|
|
1251
1370
|
codeLines = [];
|
|
1252
1371
|
}
|
|
1253
1372
|
|
|
1373
|
+
function parseTableLine(line) {
|
|
1374
|
+
return line
|
|
1375
|
+
.trim()
|
|
1376
|
+
.replace(/^\|/, "")
|
|
1377
|
+
.replace(/\|$/, "")
|
|
1378
|
+
.split(/(?<!\\)\|/)
|
|
1379
|
+
.map(cell => cell.trim().replace(/\\\|/g, "|"));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function flushTable() {
|
|
1383
|
+
if (tableLines.length === 0) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const rows = tableLines.map(parseTableLine);
|
|
1388
|
+
const separator = rows[1];
|
|
1389
|
+
const isMarkdownTable =
|
|
1390
|
+
rows.length >= 2 && separator.every(cell => /^:?-{3,}:?$/.test(cell.replace(/\s/g, "")));
|
|
1391
|
+
if (!isMarkdownTable) {
|
|
1392
|
+
paragraph.push(...tableLines);
|
|
1393
|
+
tableLines = [];
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const columnCount = Math.max(...rows.map(row => row.length));
|
|
1398
|
+
const normalizeRow = row => [...row, ...Array(columnCount - row.length).fill("")];
|
|
1399
|
+
const header = normalizeRow(rows[0]);
|
|
1400
|
+
const body = rows.slice(2).map(normalizeRow);
|
|
1401
|
+
const cellStyle =
|
|
1402
|
+
"padding:10px 12px;border:1px solid #dfe3e8;text-align:left;vertical-align:top;" +
|
|
1403
|
+
"font-size:15px;line-height:1.7;overflow-wrap:anywhere;word-break:break-word;";
|
|
1404
|
+
html.push(
|
|
1405
|
+
'<div style="width:100%;margin:24px 0;overflow-x:auto;-webkit-overflow-scrolling:touch;">' +
|
|
1406
|
+
'<table style="width:100%;min-width:560px;border-collapse:collapse;table-layout:auto;color:#3f3f3f;">' +
|
|
1407
|
+
`<thead><tr>${header
|
|
1408
|
+
.map(cell => `<th style="${cellStyle}background:#f5f7f8;font-weight:600;">${applyInlineMarkdown(cell)}</th>`)
|
|
1409
|
+
.join("")}</tr></thead>` +
|
|
1410
|
+
`<tbody>${body
|
|
1411
|
+
.map(
|
|
1412
|
+
row =>
|
|
1413
|
+
`<tr>${row
|
|
1414
|
+
.map(cell => `<td style="${cellStyle}">${applyInlineMarkdown(cell)}</td>`)
|
|
1415
|
+
.join("")}</tr>`
|
|
1416
|
+
)
|
|
1417
|
+
.join("")}</tbody></table></div>`
|
|
1418
|
+
);
|
|
1419
|
+
tableLines = [];
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1254
1422
|
function flushAll() {
|
|
1423
|
+
flushTable();
|
|
1255
1424
|
flushParagraph();
|
|
1256
1425
|
flushQuote();
|
|
1257
1426
|
flushList();
|
|
@@ -1281,13 +1450,33 @@ function markdownToHtml(markdown) {
|
|
|
1281
1450
|
continue;
|
|
1282
1451
|
}
|
|
1283
1452
|
|
|
1453
|
+
const richHtmlMatch = trimmed.match(/^@@QCRICHHTML(\d+)@@$/);
|
|
1454
|
+
if (richHtmlMatch) {
|
|
1455
|
+
flushAll();
|
|
1456
|
+
const sanitizedHtml = sanitizeArticleRichHtml(richHtmlBlocks[Number(richHtmlMatch[1])] || "");
|
|
1457
|
+
if (sanitizedHtml) {
|
|
1458
|
+
html.push(sanitizedHtml);
|
|
1459
|
+
}
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (/^\|.*\|$/.test(trimmed)) {
|
|
1464
|
+
flushParagraph();
|
|
1465
|
+
flushQuote();
|
|
1466
|
+
flushList();
|
|
1467
|
+
tableLines.push(trimmed);
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
flushTable();
|
|
1472
|
+
|
|
1284
1473
|
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
1285
1474
|
if (headingMatch) {
|
|
1286
1475
|
flushAll();
|
|
1287
1476
|
const level = headingMatch[1].length;
|
|
1288
1477
|
const headingSize = level === 1 ? 24 : level === 2 ? 20 : 18;
|
|
1289
1478
|
html.push(
|
|
1290
|
-
`<h${level} style="margin:32px 0 16px;font-size:${headingSize}px;line-height:1.5;font-weight:700;color:#24292f;">` +
|
|
1479
|
+
`<h${level} style="margin:32px 0 16px;font-size:${headingSize}px;line-height:1.5;font-weight:700;color:#24292f;overflow-wrap:anywhere;word-break:break-word;">` +
|
|
1291
1480
|
`${applyInlineMarkdown(headingMatch[2].trim())}</h${level}>`
|
|
1292
1481
|
);
|
|
1293
1482
|
continue;
|
|
@@ -1339,8 +1528,9 @@ function markdownToHtml(markdown) {
|
|
|
1339
1528
|
flushCodeBlock();
|
|
1340
1529
|
flushAll();
|
|
1341
1530
|
return (
|
|
1342
|
-
'<section style="max-width:
|
|
1343
|
-
'PingFang SC,Hiragino Sans GB,Microsoft YaHei,
|
|
1531
|
+
'<section style="box-sizing:border-box;width:100%;max-width:960px;min-width:0;margin:0 auto;padding:0 16px;' +
|
|
1532
|
+
'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,' +
|
|
1533
|
+
'sans-serif;letter-spacing:0;overflow-wrap:anywhere;word-break:break-word;">\n' +
|
|
1344
1534
|
`${html.join("\n")}\n</section>`
|
|
1345
1535
|
);
|
|
1346
1536
|
}
|
|
@@ -1487,7 +1677,13 @@ async function importWechatArticleCommand(sourceUrl, file = "article.md") {
|
|
|
1487
1677
|
console.log(targetFile);
|
|
1488
1678
|
console.log(chalk.gray(`标题: ${article.title}`));
|
|
1489
1679
|
console.log(chalk.gray(`已转存图片: ${article.imageCount} 张`));
|
|
1680
|
+
console.log(chalk.gray(`已提取正文: ${article.textSegmentCount} 段`));
|
|
1490
1681
|
console.log(chalk.gray(`已保留文字颜色: ${article.colorCount} 处`));
|
|
1682
|
+
console.log(
|
|
1683
|
+
chalk.gray(
|
|
1684
|
+
`特殊内容: ${article.specialElementCount} 处,媒体: ${article.mediaCount} 处,表格: ${article.tableCount} 个`
|
|
1685
|
+
)
|
|
1686
|
+
);
|
|
1491
1687
|
console.log("");
|
|
1492
1688
|
console.log("请补充 cate_id 等业务字段,确认内容后执行:");
|
|
1493
1689
|
console.log("");
|
|
@@ -1547,6 +1743,7 @@ async function publishArticleCommand(file, options) {
|
|
|
1547
1743
|
}
|
|
1548
1744
|
|
|
1549
1745
|
const { articleFile, payload } = await parseArticleFile(file);
|
|
1746
|
+
payload.status = "0";
|
|
1550
1747
|
const config = await loadConfig();
|
|
1551
1748
|
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
1552
1749
|
|
|
@@ -1556,15 +1753,66 @@ async function publishArticleCommand(file, options) {
|
|
|
1556
1753
|
}
|
|
1557
1754
|
|
|
1558
1755
|
console.log("");
|
|
1559
|
-
console.log(chalk.cyan("
|
|
1756
|
+
console.log(chalk.cyan("正在保存官网文章为未上线状态..."));
|
|
1560
1757
|
console.log(chalk.gray(`文章文件: ${articleFile}`));
|
|
1561
1758
|
console.log("");
|
|
1562
1759
|
|
|
1563
1760
|
const response = await authenticatedRequest("POST", backendUrl, "/api/articles/publish", payload);
|
|
1564
|
-
|
|
1565
|
-
const articleId =
|
|
1566
|
-
|
|
1567
|
-
|
|
1761
|
+
const rawArticleId = response.data?.id ?? response.data?.article_id ?? response.data?.articleId;
|
|
1762
|
+
const articleId = normalizeArticleId(rawArticleId);
|
|
1763
|
+
const previewUrl = normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId);
|
|
1764
|
+
const stateWarning = await trySaveRecentArticleState(articleId, "0", backendUrl, previewUrl);
|
|
1765
|
+
|
|
1766
|
+
console.log(chalk.green(response.message || "文章已保存为未上线状态"));
|
|
1767
|
+
console.log(`文章 ID: ${articleId}`);
|
|
1768
|
+
console.log("文章状态: 未上线");
|
|
1769
|
+
console.log(`预览地址: ${previewUrl}`);
|
|
1770
|
+
if (stateWarning) {
|
|
1771
|
+
console.log(chalk.yellow(`本地未能记录最近文章 ID,后续操作请手动输入 ${articleId}: ${stateWarning}`));
|
|
1772
|
+
}
|
|
1773
|
+
console.log("");
|
|
1774
|
+
console.log("请打开预览地址检查 PC 和移动端排版。确认无误后执行:");
|
|
1775
|
+
console.log("");
|
|
1776
|
+
console.log(` qcplay-cli article update ${articleId}`);
|
|
1777
|
+
console.log("");
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async function updateArticleStatusCommand(articleIdValue, targetStatus, options) {
|
|
1781
|
+
const config = await loadConfig();
|
|
1782
|
+
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
1783
|
+
const articleId = await resolveArticleId(articleIdValue, backendUrl);
|
|
1784
|
+
const payload = {
|
|
1785
|
+
id: articleId,
|
|
1786
|
+
status: targetStatus
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
if (options.dryRun) {
|
|
1790
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const action = targetStatus === "2" ? "删除" : "上线";
|
|
1795
|
+
console.log("");
|
|
1796
|
+
console.log(chalk.cyan(`正在${action}官网文章...`));
|
|
1797
|
+
console.log(chalk.gray(`文章 ID: ${articleId}`));
|
|
1798
|
+
console.log("");
|
|
1799
|
+
|
|
1800
|
+
const response = await authenticatedRequest("PATCH", backendUrl, "/api/articles/status", payload);
|
|
1801
|
+
const responseStatus = normalizeText(response.data?.status) || targetStatus;
|
|
1802
|
+
const previewUrl =
|
|
1803
|
+
responseStatus === "1"
|
|
1804
|
+
? normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId)
|
|
1805
|
+
: "";
|
|
1806
|
+
const stateWarning = await trySaveRecentArticleState(articleId, responseStatus, backendUrl, previewUrl);
|
|
1807
|
+
|
|
1808
|
+
console.log(chalk.green(response.message || (responseStatus === "2" ? "文章已删除" : "文章已上线")));
|
|
1809
|
+
console.log(`文章 ID: ${articleId}`);
|
|
1810
|
+
console.log(`文章状态: ${responseStatus === "2" ? "已删除" : "已上线"}`);
|
|
1811
|
+
if (previewUrl) {
|
|
1812
|
+
console.log(`文章地址: ${previewUrl}`);
|
|
1813
|
+
}
|
|
1814
|
+
if (stateWarning) {
|
|
1815
|
+
console.log(chalk.yellow(`本地未能记录文章状态,后续操作请手动输入 ${articleId}: ${stateWarning}`));
|
|
1568
1816
|
}
|
|
1569
1817
|
}
|
|
1570
1818
|
|
|
@@ -1654,6 +1902,16 @@ async function main() {
|
|
|
1654
1902
|
return;
|
|
1655
1903
|
}
|
|
1656
1904
|
|
|
1905
|
+
if (subcommand === "update" || subcommand === "delete") {
|
|
1906
|
+
const parsed = parseArticleMutationOptions(rest);
|
|
1907
|
+
const targetStatus = subcommand === "delete" ? "2" : "1";
|
|
1908
|
+
const errorTitle = subcommand === "delete" ? "删除失败" : "上线失败";
|
|
1909
|
+
await runWithErrorBanner(errorTitle, () =>
|
|
1910
|
+
updateArticleStatusCommand(parsed.articleId, targetStatus, parsed.options)
|
|
1911
|
+
);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1657
1915
|
printArticleHelp();
|
|
1658
1916
|
process.exit(1);
|
|
1659
1917
|
}
|
|
@@ -1664,6 +1922,16 @@ async function main() {
|
|
|
1664
1922
|
return;
|
|
1665
1923
|
}
|
|
1666
1924
|
|
|
1925
|
+
if (command === "www-article-list.update" || command === "www-article-list.delete") {
|
|
1926
|
+
const parsed = parseArticleMutationOptions([subcommand, ...rest].filter(Boolean));
|
|
1927
|
+
const targetStatus = command === "www-article-list.delete" ? "2" : "1";
|
|
1928
|
+
const errorTitle = targetStatus === "2" ? "删除失败" : "上线失败";
|
|
1929
|
+
await runWithErrorBanner(errorTitle, () =>
|
|
1930
|
+
updateArticleStatusCommand(parsed.articleId, targetStatus, parsed.options)
|
|
1931
|
+
);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1667
1935
|
if (command === "features") {
|
|
1668
1936
|
printFeatures();
|
|
1669
1937
|
return;
|