@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 CHANGED
@@ -9,7 +9,7 @@
9
9
  - 登录默认打开线上页面:`https://cli.qcg.ink/auth.html`
10
10
  - `qcplay-cli install` 初始化完成后会自动打开登录页并等待登录完成
11
11
  - 登录凭证保存在当前用户本机的 `~/.qcplay/auth.json`,不同客户端之间不共享账号
12
- - 状态、权限和发布请求通过 Bearer Token 识别当前账号
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
- 转换流程会读取微信文章标题、来源、发布日期和正文,将正文及封面图片逐张上传到官网图片服务,再生成带 Front Matter Markdown 草稿。输出固定为 `status: "0"`,并保留空的 `cate_id` 供发布前确认;任何图片下载或上传失败都会终止转换且不生成文章文件。
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 { buildOfficialArticleMarkdown, importWechatArticle, normalizeArticleColor } from "../lib/wechat-article.js";
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 lines = stripBom(markdown).replace(/\r\n/g, "\n").split("\n");
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:677px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,' +
1343
- 'PingFang SC,Hiragino Sans GB,Microsoft YaHei,sans-serif;letter-spacing:0;overflow-wrap:anywhere;">\n' +
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
- console.log(chalk.green(response.message || "发布成功"));
1565
- const articleId = response.data?.id ?? response.data?.article_id ?? response.data?.articleId;
1566
- if (articleId !== undefined) {
1567
- console.log(`文章 ID: ${articleId}`);
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;