@qcplay/cli 1.0.11 → 1.0.13
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 +22 -2
- package/bin/qcplay.js +192 -9
- package/package.json +1 -1
- package/templates/skills/qcplay-publish-article/SKILL.md +136 -147
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`
|
|
@@ -26,12 +26,32 @@ qcplay-cli article import "https://mp.weixin.qq.com/s/..." article.md
|
|
|
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
|
+
文章内容和排版只由用户在打开的预览页面中检查,CLI 或 AI 不自动抓取、分析或判断预览内容。用户明确确认无误后再上线。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
|
@@ -99,6 +99,8 @@ const QCPLAY_DIR = path.join(os.homedir(), ".qcplay");
|
|
|
99
99
|
const AGENTS_DIR = path.join(os.homedir(), ".agents");
|
|
100
100
|
const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
|
|
101
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";
|
|
102
104
|
const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
103
105
|
|
|
104
106
|
function readPackageMetadata() {
|
|
@@ -149,7 +151,11 @@ Usage:
|
|
|
149
151
|
qcplay-cli article init [file]
|
|
150
152
|
qcplay-cli article import <wechat-url> [file]
|
|
151
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]
|
|
152
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]
|
|
153
159
|
qcplay-cli features
|
|
154
160
|
qcplay-cli where
|
|
155
161
|
|
|
@@ -175,7 +181,9 @@ function printArticleHelp() {
|
|
|
175
181
|
qcplay-cli article
|
|
176
182
|
qcplay-cli article init [file]
|
|
177
183
|
qcplay-cli article import <wechat-url> [file]
|
|
178
|
-
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]`);
|
|
179
187
|
}
|
|
180
188
|
|
|
181
189
|
function printFeatures() {
|
|
@@ -193,6 +201,8 @@ function printFeatures() {
|
|
|
193
201
|
console.log(chalk.cyan("3. 发布官网文章"));
|
|
194
202
|
console.log(" qcplay-cli article import <微信文章链接> article.md");
|
|
195
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]");
|
|
196
206
|
console.log("");
|
|
197
207
|
console.log(chalk.cyan("4. 查看本地目录"));
|
|
198
208
|
console.log(" qcplay-cli where");
|
|
@@ -497,12 +507,24 @@ function parsePublishOptions(args) {
|
|
|
497
507
|
positional.push(current);
|
|
498
508
|
}
|
|
499
509
|
|
|
510
|
+
if (positional.length > 1) {
|
|
511
|
+
throw new Error(`未知参数: ${positional.slice(1).join(" ")}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
500
514
|
return {
|
|
501
515
|
file: positional[0],
|
|
502
516
|
options
|
|
503
517
|
};
|
|
504
518
|
}
|
|
505
519
|
|
|
520
|
+
function parseArticleMutationOptions(args) {
|
|
521
|
+
const parsed = parsePublishOptions(args);
|
|
522
|
+
return {
|
|
523
|
+
articleId: parsed.file,
|
|
524
|
+
options: parsed.options
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
506
528
|
function requestJson(method, baseUrl, pathname, payload, headers = {}) {
|
|
507
529
|
return new Promise((resolve, reject) => {
|
|
508
530
|
const url = new URL(pathname, baseUrl);
|
|
@@ -580,9 +602,11 @@ function openBrowser(targetUrl) {
|
|
|
580
602
|
stdio: "ignore"
|
|
581
603
|
});
|
|
582
604
|
|
|
583
|
-
child.
|
|
584
|
-
child.
|
|
585
|
-
|
|
605
|
+
child.once("error", reject);
|
|
606
|
+
child.once("spawn", () => {
|
|
607
|
+
child.unref();
|
|
608
|
+
resolve();
|
|
609
|
+
});
|
|
586
610
|
});
|
|
587
611
|
}
|
|
588
612
|
|
|
@@ -874,6 +898,87 @@ async function authenticatedRequest(method, baseUrl, pathname, payload) {
|
|
|
874
898
|
});
|
|
875
899
|
}
|
|
876
900
|
|
|
901
|
+
function normalizeArticleId(value) {
|
|
902
|
+
const text = normalizeText(value);
|
|
903
|
+
if (!/^\d+$/.test(text)) {
|
|
904
|
+
throw new Error("文章 ID 必须是正整数");
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const articleId = Number(text);
|
|
908
|
+
if (!Number.isSafeInteger(articleId) || articleId <= 0) {
|
|
909
|
+
throw new Error("文章 ID 必须是正整数");
|
|
910
|
+
}
|
|
911
|
+
return articleId;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function articlePreviewUrl(articleId) {
|
|
915
|
+
const url = new URL(ARTICLE_PREVIEW_BASE_URL);
|
|
916
|
+
url.searchParams.set("id", String(articleId));
|
|
917
|
+
return url.toString();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function backendOriginsMatch(left, right) {
|
|
921
|
+
try {
|
|
922
|
+
return new URL(left).origin === new URL(right).origin;
|
|
923
|
+
} catch {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function saveRecentArticleState(articleId, status, backendUrl, previewUrl = "") {
|
|
929
|
+
const authState = await loadAuthState();
|
|
930
|
+
await writeJson(ARTICLE_STATE_FILE, {
|
|
931
|
+
article_id: articleId,
|
|
932
|
+
status: String(status),
|
|
933
|
+
preview_url: previewUrl,
|
|
934
|
+
backend_url: backendUrl,
|
|
935
|
+
administrator_id: Number(authState.id || 0),
|
|
936
|
+
account: normalizeText(authState.account),
|
|
937
|
+
updated_at: Date.now()
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function trySaveRecentArticleState(articleId, status, backendUrl, previewUrl = "") {
|
|
942
|
+
try {
|
|
943
|
+
await saveRecentArticleState(articleId, status, backendUrl, previewUrl);
|
|
944
|
+
return "";
|
|
945
|
+
} catch (error) {
|
|
946
|
+
return normalizeText(error?.message) || String(error);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function resolveArticleId(value, backendUrl) {
|
|
951
|
+
if (normalizeText(value)) {
|
|
952
|
+
return normalizeArticleId(value);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const [recentArticle, authState] = await Promise.all([readJson(ARTICLE_STATE_FILE), loadAuthState()]);
|
|
956
|
+
if (!recentArticle.article_id) {
|
|
957
|
+
throw new Error("没有找到最近发布的未上线文章,请手动输入文章 ID");
|
|
958
|
+
}
|
|
959
|
+
if (!backendOriginsMatch(recentArticle.backend_url, backendUrl)) {
|
|
960
|
+
throw new Error("最近文章属于其他后端,请手动输入文章 ID");
|
|
961
|
+
}
|
|
962
|
+
if (String(recentArticle.status) !== "0") {
|
|
963
|
+
throw new Error("最近文章不是未上线状态,请手动输入文章 ID");
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const recentAdministratorId = Number(recentArticle.administrator_id || 0);
|
|
967
|
+
const currentAdministratorId = Number(authState.id || 0);
|
|
968
|
+
if (recentAdministratorId && recentAdministratorId !== currentAdministratorId) {
|
|
969
|
+
throw new Error("最近文章属于其他登录账号,请手动输入文章 ID");
|
|
970
|
+
}
|
|
971
|
+
if (
|
|
972
|
+
!recentAdministratorId &&
|
|
973
|
+
normalizeText(recentArticle.account) &&
|
|
974
|
+
normalizeText(recentArticle.account) !== normalizeText(authState.account)
|
|
975
|
+
) {
|
|
976
|
+
throw new Error("最近文章属于其他登录账号,请手动输入文章 ID");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return normalizeArticleId(recentArticle.article_id);
|
|
980
|
+
}
|
|
981
|
+
|
|
877
982
|
function flattenPermissionTree(items, bucket = []) {
|
|
878
983
|
for (const item of items) {
|
|
879
984
|
bucket.push(item);
|
|
@@ -1640,6 +1745,7 @@ async function publishArticleCommand(file, options) {
|
|
|
1640
1745
|
}
|
|
1641
1746
|
|
|
1642
1747
|
const { articleFile, payload } = await parseArticleFile(file);
|
|
1748
|
+
payload.status = "0";
|
|
1643
1749
|
const config = await loadConfig();
|
|
1644
1750
|
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
1645
1751
|
|
|
@@ -1649,15 +1755,72 @@ async function publishArticleCommand(file, options) {
|
|
|
1649
1755
|
}
|
|
1650
1756
|
|
|
1651
1757
|
console.log("");
|
|
1652
|
-
console.log(chalk.cyan("
|
|
1758
|
+
console.log(chalk.cyan("正在保存官网文章为未上线状态..."));
|
|
1653
1759
|
console.log(chalk.gray(`文章文件: ${articleFile}`));
|
|
1654
1760
|
console.log("");
|
|
1655
1761
|
|
|
1656
1762
|
const response = await authenticatedRequest("POST", backendUrl, "/api/articles/publish", payload);
|
|
1657
|
-
|
|
1658
|
-
const articleId =
|
|
1659
|
-
|
|
1660
|
-
|
|
1763
|
+
const rawArticleId = response.data?.id ?? response.data?.article_id ?? response.data?.articleId;
|
|
1764
|
+
const articleId = normalizeArticleId(rawArticleId);
|
|
1765
|
+
const previewUrl = normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId);
|
|
1766
|
+
const stateWarning = await trySaveRecentArticleState(articleId, "0", backendUrl, previewUrl);
|
|
1767
|
+
|
|
1768
|
+
console.log(chalk.green(response.message || "文章已保存为未上线状态"));
|
|
1769
|
+
console.log(`文章 ID: ${articleId}`);
|
|
1770
|
+
console.log("文章状态: 未上线");
|
|
1771
|
+
console.log(`预览地址: ${previewUrl}`);
|
|
1772
|
+
if (stateWarning) {
|
|
1773
|
+
console.log(chalk.yellow(`本地未能记录最近文章 ID,后续操作请手动输入 ${articleId}: ${stateWarning}`));
|
|
1774
|
+
}
|
|
1775
|
+
console.log("");
|
|
1776
|
+
try {
|
|
1777
|
+
await openBrowser(previewUrl);
|
|
1778
|
+
console.log(chalk.cyan("已打开文章预览页面,请由用户检查文章内容和排版。"));
|
|
1779
|
+
} catch {
|
|
1780
|
+
console.log(chalk.yellow("自动打开预览页面失败,请手动打开上面的预览地址。"));
|
|
1781
|
+
}
|
|
1782
|
+
console.log("用户确认无误后再执行:");
|
|
1783
|
+
console.log("");
|
|
1784
|
+
console.log(` qcplay-cli article update ${articleId}`);
|
|
1785
|
+
console.log("");
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
async function updateArticleStatusCommand(articleIdValue, targetStatus, options) {
|
|
1789
|
+
const config = await loadConfig();
|
|
1790
|
+
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
1791
|
+
const articleId = await resolveArticleId(articleIdValue, backendUrl);
|
|
1792
|
+
const payload = {
|
|
1793
|
+
id: articleId,
|
|
1794
|
+
status: targetStatus
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
if (options.dryRun) {
|
|
1798
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const action = targetStatus === "2" ? "删除" : "上线";
|
|
1803
|
+
console.log("");
|
|
1804
|
+
console.log(chalk.cyan(`正在${action}官网文章...`));
|
|
1805
|
+
console.log(chalk.gray(`文章 ID: ${articleId}`));
|
|
1806
|
+
console.log("");
|
|
1807
|
+
|
|
1808
|
+
const response = await authenticatedRequest("PATCH", backendUrl, "/api/articles/status", payload);
|
|
1809
|
+
const responseStatus = normalizeText(response.data?.status) || targetStatus;
|
|
1810
|
+
const previewUrl =
|
|
1811
|
+
responseStatus === "1"
|
|
1812
|
+
? normalizeText(response.data?.preview_url) || articlePreviewUrl(articleId)
|
|
1813
|
+
: "";
|
|
1814
|
+
const stateWarning = await trySaveRecentArticleState(articleId, responseStatus, backendUrl, previewUrl);
|
|
1815
|
+
|
|
1816
|
+
console.log(chalk.green(response.message || (responseStatus === "2" ? "文章已删除" : "文章已上线")));
|
|
1817
|
+
console.log(`文章 ID: ${articleId}`);
|
|
1818
|
+
console.log(`文章状态: ${responseStatus === "2" ? "已删除" : "已上线"}`);
|
|
1819
|
+
if (previewUrl) {
|
|
1820
|
+
console.log(`文章地址: ${previewUrl}`);
|
|
1821
|
+
}
|
|
1822
|
+
if (stateWarning) {
|
|
1823
|
+
console.log(chalk.yellow(`本地未能记录文章状态,后续操作请手动输入 ${articleId}: ${stateWarning}`));
|
|
1661
1824
|
}
|
|
1662
1825
|
}
|
|
1663
1826
|
|
|
@@ -1747,6 +1910,16 @@ async function main() {
|
|
|
1747
1910
|
return;
|
|
1748
1911
|
}
|
|
1749
1912
|
|
|
1913
|
+
if (subcommand === "update" || subcommand === "delete") {
|
|
1914
|
+
const parsed = parseArticleMutationOptions(rest);
|
|
1915
|
+
const targetStatus = subcommand === "delete" ? "2" : "1";
|
|
1916
|
+
const errorTitle = subcommand === "delete" ? "删除失败" : "上线失败";
|
|
1917
|
+
await runWithErrorBanner(errorTitle, () =>
|
|
1918
|
+
updateArticleStatusCommand(parsed.articleId, targetStatus, parsed.options)
|
|
1919
|
+
);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1750
1923
|
printArticleHelp();
|
|
1751
1924
|
process.exit(1);
|
|
1752
1925
|
}
|
|
@@ -1757,6 +1930,16 @@ async function main() {
|
|
|
1757
1930
|
return;
|
|
1758
1931
|
}
|
|
1759
1932
|
|
|
1933
|
+
if (command === "www-article-list.update" || command === "www-article-list.delete") {
|
|
1934
|
+
const parsed = parseArticleMutationOptions([subcommand, ...rest].filter(Boolean));
|
|
1935
|
+
const targetStatus = command === "www-article-list.delete" ? "2" : "1";
|
|
1936
|
+
const errorTitle = targetStatus === "2" ? "删除失败" : "上线失败";
|
|
1937
|
+
await runWithErrorBanner(errorTitle, () =>
|
|
1938
|
+
updateArticleStatusCommand(parsed.articleId, targetStatus, parsed.options)
|
|
1939
|
+
);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1760
1943
|
if (command === "features") {
|
|
1761
1944
|
printFeatures();
|
|
1762
1945
|
return;
|
package/package.json
CHANGED
|
@@ -5,24 +5,25 @@ description: "QCPlay 官网文章上传:Use when user mentions 发布文章、
|
|
|
5
5
|
metadata:
|
|
6
6
|
requires:
|
|
7
7
|
bins: ["qcplay", "qcplay-cli"]
|
|
8
|
-
native_bins: ["publish-article.exe", "qcplay-auth.exe"]
|
|
9
8
|
cliHelp: "qcplay article --help"
|
|
10
9
|
authFile: "~/.qcplay/auth.json"
|
|
11
10
|
templateCommand: "qcplay article"
|
|
12
11
|
initCommand: "qcplay article init"
|
|
13
12
|
importCommand: "qcplay article import <wechat-url> [file]"
|
|
14
13
|
publishCommand: "qcplay www-article-list.store article.md"
|
|
14
|
+
updateCommand: "qcplay article update [id]"
|
|
15
|
+
deleteCommand: "qcplay article delete [id]"
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
# qcplay www-article-list.store
|
|
18
19
|
|
|
19
20
|
## CRITICAL — 文章上传工作流,必须遵循
|
|
20
21
|
|
|
21
|
-
本 Skill 用于通过 QCPlay CLI
|
|
22
|
+
本 Skill 用于通过 QCPlay CLI 保存、预览、上线和删除官网文章。
|
|
22
23
|
|
|
23
24
|
文章参数必须写在 Markdown 文件顶部的 Front Matter 中,正文写在 Front Matter 之后。
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
用户提交文章时,先执行:
|
|
26
27
|
|
|
27
28
|
```bash
|
|
28
29
|
qcplay www-article-list.store article.md
|
|
@@ -42,7 +43,21 @@ qcplay-cli www-article-list.store article.md
|
|
|
42
43
|
qcplay article import "https://mp.weixin.qq.com/s/..." article.md
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
该命令只生成本地官网文章草稿,不会提交到官网。确认草稿正文和 Front Matter 后,再执行保存命令。
|
|
47
|
+
|
|
48
|
+
保存命令始终以 `status: "0"` 创建未上线文章,并输出文章 ID 和可点击预览地址。保存成功后必须打开该预览网页,只让用户本人检查文章内容和排版。AI 不得自动访问、抓取、截图、分析、比较或判断预览内容。只有用户明确确认上线后,才能执行:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
qcplay article update [id]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
用户要求删除时执行软删除,将状态更新为 `2`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
qcplay article delete [id]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`id` 省略时,只能使用当前账号最近一次保存的未上线文章;找不到时必须要求手动输入 ID。
|
|
46
61
|
|
|
47
62
|
正确方式是:
|
|
48
63
|
|
|
@@ -53,7 +68,7 @@ Front Matter 解析文章参数
|
|
|
53
68
|
↓
|
|
54
69
|
Markdown 正文作为 article_content
|
|
55
70
|
↓
|
|
56
|
-
|
|
71
|
+
QCPlay CLI 调用认证后端保存文章
|
|
57
72
|
```
|
|
58
73
|
|
|
59
74
|
---
|
|
@@ -63,9 +78,7 @@ publish-article.exe 执行上传
|
|
|
63
78
|
- **文章文件(Article File)**:一个 Markdown 文件,例如 `article.md`,顶部包含 YAML Front Matter,下面是正文内容。
|
|
64
79
|
- **Front Matter**:`---` 包裹的 YAML 参数区,用来填写文章标题、缩略图、分类、标签、发布时间等。
|
|
65
80
|
- **文章正文(article_content)**:Front Matter 后面的 Markdown 正文内容,程序自动作为 `article_content` 上传。
|
|
66
|
-
-
|
|
67
|
-
- **登录程序(qcplay-auth.exe)**:Go 编译的本地登录程序,负责浏览器登录并生成 `~/.qcplay/auth.json`。
|
|
68
|
-
- **统一 CLI(qcplay / qcplay-cli)**:npm 包提供的统一入口,根据子命令调用不同 exe。
|
|
81
|
+
- **统一 CLI(qcplay / qcplay-cli)**:npm 包提供的统一入口,负责解析文章、读取登录凭证并调用认证后端。
|
|
69
82
|
- **认证文件(auth.json)**:登录成功后保存的本地认证文件,路径为 `~/.qcplay/auth.json`。
|
|
70
83
|
- **文章模板**:通过 `qcplay article` 查看模板说明,或通过 `qcplay article init` 生成 `article.md`。
|
|
71
84
|
- **微信文章导入**:通过 `qcplay article import <wechat-url> [file]` 提取推文正文,将图片转存到官网图片服务并生成 Markdown 草稿。
|
|
@@ -109,12 +122,12 @@ publish-article.exe 执行上传
|
|
|
109
122
|
6. **文章正文不能为空**
|
|
110
123
|
Front Matter 后面的 Markdown 正文为空时不得上传。
|
|
111
124
|
|
|
112
|
-
7.
|
|
113
|
-
`
|
|
114
|
-
如果用户没有明确要求上线,建议使用:
|
|
125
|
+
7. **首次保存固定为未上线**
|
|
126
|
+
`www-article-list.store` 必须忽略 Front Matter 中的上线值并强制使用:
|
|
115
127
|
```yaml
|
|
116
128
|
status: "0"
|
|
117
129
|
```
|
|
130
|
+
CLI 打开预览网页后,由用户本人检查。AI 不检查预览内容,也不代替用户判断。用户明确确认后,使用 `article update [id]` 将状态更新为 `1`。不得通过首次保存直接上线。
|
|
118
131
|
|
|
119
132
|
8. **不得伪造发布结果**
|
|
120
133
|
如果接口返回失败,必须原样提示失败原因,不得编造文章 URL 或成功状态。
|
|
@@ -318,22 +331,41 @@ qcplay article import "https://mp.weixin.qq.com/s/..." article.md
|
|
|
318
331
|
|
|
319
332
|
用户在 `article.md` 顶部 Front Matter 填写文章参数,在下方填写正文。
|
|
320
333
|
|
|
321
|
-
6.
|
|
334
|
+
6. **保存为未上线文章**
|
|
322
335
|
|
|
323
336
|
```bash
|
|
324
337
|
qcplay www-article-list.store article.md
|
|
325
338
|
```
|
|
326
339
|
|
|
327
|
-
7.
|
|
340
|
+
7. **检查预览**
|
|
328
341
|
|
|
329
|
-
|
|
342
|
+
保存成功后必须输出:
|
|
330
343
|
|
|
331
344
|
```txt
|
|
332
|
-
|
|
345
|
+
文章已保存为未上线状态
|
|
346
|
+
文章 ID: 123
|
|
347
|
+
文章状态: 未上线
|
|
348
|
+
预览地址: https://snail.qingcigame.com/official/news-details.html?id=123
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
CLI 必须自动打开预览地址,然后停止流程并让用户本人检查。AI 不读取或分析预览页面,不得自行确认上线。用户明确确认上线后执行:
|
|
333
352
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
353
|
+
```bash
|
|
354
|
+
qcplay article update 123
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
也可以省略 ID,使用当前账号最近一次保存的未上线文章:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
qcplay article update
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
8. **删除文章**
|
|
364
|
+
|
|
365
|
+
用户明确要求删除时,将文章状态软删除为 `2`:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
qcplay article delete 123
|
|
337
369
|
```
|
|
338
370
|
|
|
339
371
|
---
|
|
@@ -395,50 +427,54 @@ qcplay article init --force
|
|
|
395
427
|
|
|
396
428
|
---
|
|
397
429
|
|
|
398
|
-
###
|
|
430
|
+
### 保存未上线文章
|
|
399
431
|
|
|
400
432
|
```bash
|
|
401
433
|
qcplay www-article-list.store article.md
|
|
402
434
|
```
|
|
403
435
|
|
|
404
|
-
|
|
436
|
+
首次保存始终将 `status` 强制为 `0`。成功后记录最近文章 ID,并输出预览地址。
|
|
437
|
+
|
|
438
|
+
### 确认上线
|
|
405
439
|
|
|
406
440
|
```bash
|
|
407
|
-
|
|
441
|
+
qcplay article update [id]
|
|
408
442
|
```
|
|
409
443
|
|
|
410
|
-
|
|
444
|
+
等价命令:
|
|
411
445
|
|
|
412
446
|
```bash
|
|
413
|
-
|
|
447
|
+
qcplay www-article-list.update [id]
|
|
414
448
|
```
|
|
415
449
|
|
|
416
|
-
|
|
450
|
+
该命令通过更新操作将 `status` 修改为 `1`。
|
|
417
451
|
|
|
418
|
-
###
|
|
452
|
+
### 删除文章
|
|
419
453
|
|
|
420
|
-
|
|
454
|
+
```bash
|
|
455
|
+
qcplay article delete [id]
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
等价命令:
|
|
421
459
|
|
|
422
460
|
```bash
|
|
423
|
-
qcplay www-article-list.
|
|
461
|
+
qcplay www-article-list.delete [id]
|
|
424
462
|
```
|
|
425
463
|
|
|
426
|
-
|
|
464
|
+
该命令通过更新操作将 `status` 修改为 `2`,不得执行物理删除。
|
|
427
465
|
|
|
428
466
|
---
|
|
429
467
|
|
|
430
|
-
###
|
|
431
|
-
|
|
432
|
-
```bash
|
|
433
|
-
qcplay www-article-list.store article.md --env prod
|
|
434
|
-
```
|
|
468
|
+
### Dry Run,可选
|
|
435
469
|
|
|
436
|
-
|
|
470
|
+
如果支持:
|
|
437
471
|
|
|
438
472
|
```bash
|
|
439
|
-
|
|
473
|
+
qcplay www-article-list.store article.md --dry-run
|
|
440
474
|
```
|
|
441
475
|
|
|
476
|
+
则只解析、校验并展示 payload,不真正调用发布接口。
|
|
477
|
+
|
|
442
478
|
---
|
|
443
479
|
|
|
444
480
|
## article.md 文件格式
|
|
@@ -455,7 +491,7 @@ move_thumbnail: https://example.com/mobile-thumbnail.png
|
|
|
455
491
|
article_excerpt: 文章描述,可以为空
|
|
456
492
|
article_url: article-url-slug
|
|
457
493
|
origin: QCPlay
|
|
458
|
-
status: "
|
|
494
|
+
status: "0"
|
|
459
495
|
cate_id: "2"
|
|
460
496
|
video_link: ""
|
|
461
497
|
is_hot: "0"
|
|
@@ -497,7 +533,7 @@ index_move_img: ""
|
|
|
497
533
|
| `article_excerpt` | 否 | 文章描述,可以为空 |
|
|
498
534
|
| `article_url` | 建议 | 文章链接,建议使用英文 slug |
|
|
499
535
|
| `origin` | 否 | 作者来源 |
|
|
500
|
-
| `status` | 是 | `
|
|
536
|
+
| `status` | 是 | 模板填写 `0`;首次保存会强制为 `0` |
|
|
501
537
|
| `cate_id` | 是 | 分类 ID |
|
|
502
538
|
| `video_link` | 否 | 视频链接 |
|
|
503
539
|
| `is_hot` | 是 | `1` 热门,`0` 非热门 |
|
|
@@ -586,20 +622,17 @@ Front Matter.index_move_img -> index_move_img
|
|
|
586
622
|
|
|
587
623
|
| 状态 | status |
|
|
588
624
|
|---|---:|
|
|
589
|
-
|
|
|
590
|
-
|
|
|
625
|
+
| 未上线 | 0 |
|
|
626
|
+
| 已上线 | 1 |
|
|
627
|
+
| 已删除(软删除) | 2 |
|
|
591
628
|
|
|
592
|
-
|
|
629
|
+
首次保存固定使用:
|
|
593
630
|
|
|
594
631
|
```yaml
|
|
595
632
|
status: "0"
|
|
596
633
|
```
|
|
597
634
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
```yaml
|
|
601
|
-
status: "1"
|
|
602
|
-
```
|
|
635
|
+
用户明确确认预览排版无误后,不修改 Front Matter,执行 `qcplay article update [id]` 上线。
|
|
603
636
|
|
|
604
637
|
---
|
|
605
638
|
|
|
@@ -818,7 +851,7 @@ index_move_img: ""
|
|
|
818
851
|
1. `article_title` 不能为空;
|
|
819
852
|
2. `thumbnail` 不能为空;
|
|
820
853
|
3. `cate_id` 不能为空;
|
|
821
|
-
4. `status` 只能是 `"0"` 或 `"1"`;
|
|
854
|
+
4. Front Matter 的 `status` 只能是 `"0"` 或 `"1"`,首次保存请求固定覆盖为 `"0"`;
|
|
822
855
|
5. `is_hot` 只能是 `"0"` 或 `"1"`;
|
|
823
856
|
6. `is_index` 只能是 `"0"` 或 `"1"`;
|
|
824
857
|
7. `release_time` 格式必须是 `YYYY-MM-DD`,例如:
|
|
@@ -880,7 +913,7 @@ qcplay article import "https://mp.weixin.qq.com/s/..." article.md
|
|
|
880
913
|
- 输出状态固定为未发布,发布前必须检查分类、缩略图、发布日期和正文;
|
|
881
914
|
- 不允许覆盖已经存在的文件。
|
|
882
915
|
|
|
883
|
-
###
|
|
916
|
+
### 保存未上线文章
|
|
884
917
|
|
|
885
918
|
```bash
|
|
886
919
|
qcplay www-article-list.store article.md
|
|
@@ -894,88 +927,21 @@ qcplay www-article-list.store ./articles/news.md
|
|
|
894
927
|
|
|
895
928
|
---
|
|
896
929
|
|
|
897
|
-
##
|
|
930
|
+
## CLI 与后端约定
|
|
898
931
|
|
|
899
|
-
Node CLI
|
|
932
|
+
Node CLI 负责解析 Markdown、构建请求、读取本地认证状态和输出结果;Go 后端负责认证、权限校验和数据库更新。
|
|
900
933
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
Node CLI 内部应调用:
|
|
908
|
-
|
|
909
|
-
```bash
|
|
910
|
-
publish-article.exe --content-file <article.md绝对路径>
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
不要把 Front Matter 中的字段拆成命令行参数传递。
|
|
914
|
-
|
|
915
|
-
正确:
|
|
916
|
-
|
|
917
|
-
```bash
|
|
918
|
-
publish-article.exe --content-file E:\go-admin\article.md
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
不推荐:
|
|
922
|
-
|
|
923
|
-
```bash
|
|
924
|
-
publish-article.exe --article-title "标题" --cate-id 2 --thumbnail xxx
|
|
925
|
-
```
|
|
926
|
-
|
|
927
|
-
---
|
|
928
|
-
|
|
929
|
-
## Go 程序实现要求
|
|
930
|
-
|
|
931
|
-
`publish-article.exe` 必须完成:
|
|
932
|
-
|
|
933
|
-
1. 读取 `--content-file`;
|
|
934
|
-
2. 解析 Markdown Front Matter;
|
|
935
|
-
3. 将正文作为 `article_content`;
|
|
936
|
-
4. 从 `~/.qcplay/auth.json` 读取 `access_token`;
|
|
937
|
-
5. 校验 token 是否存在;
|
|
938
|
-
6. 校验 token 是否过期;
|
|
939
|
-
7. 补充默认参数:
|
|
940
|
-
```txt
|
|
941
|
-
is_index2 = 0
|
|
942
|
-
type = 1
|
|
943
|
-
```
|
|
944
|
-
8. 构建发布请求;
|
|
945
|
-
9. 调用官网文章发布接口;
|
|
946
|
-
10. 输出发布结果。
|
|
947
|
-
|
|
948
|
-
---
|
|
949
|
-
|
|
950
|
-
## Go 字段结构参考
|
|
951
|
-
|
|
952
|
-
```go
|
|
953
|
-
type ArticleMeta struct {
|
|
954
|
-
ArticleTitle string `yaml:"article_title"`
|
|
955
|
-
Thumbnail string `yaml:"thumbnail"`
|
|
956
|
-
MoveThumbnail string `yaml:"move_thumbnail"`
|
|
957
|
-
ArticleExcerpt string `yaml:"article_excerpt"`
|
|
958
|
-
ArticleURL string `yaml:"article_url"`
|
|
959
|
-
Origin string `yaml:"origin"`
|
|
960
|
-
Status string `yaml:"status"`
|
|
961
|
-
CateID string `yaml:"cate_id"`
|
|
962
|
-
VideoLink string `yaml:"video_link"`
|
|
963
|
-
IsHot string `yaml:"is_hot"`
|
|
964
|
-
IsIndex string `yaml:"is_index"`
|
|
965
|
-
ReleaseTime string `yaml:"release_time"`
|
|
966
|
-
Area string `yaml:"area"`
|
|
967
|
-
Sort string `yaml:"sort"`
|
|
968
|
-
GameID string `yaml:"game_id"`
|
|
969
|
-
IndexPCImg string `yaml:"index_pc_img"`
|
|
970
|
-
IndexMoveImg string `yaml:"index_move_img"`
|
|
971
|
-
}
|
|
972
|
-
```
|
|
934
|
+
- 首次保存:`POST /api/articles/publish`,后端再次强制 `status=0`;
|
|
935
|
+
- 确认上线:`PATCH /api/articles/status`,请求 `{ "id": 123, "status": "1" }`;
|
|
936
|
+
- 删除文章:`PATCH /api/articles/status`,请求 `{ "id": 123, "status": "2" }`;
|
|
937
|
+
- 上线和删除都要求 `www-article-list.update` 权限;
|
|
938
|
+
- 删除是状态更新,不得执行物理删除。
|
|
973
939
|
|
|
974
940
|
---
|
|
975
941
|
|
|
976
942
|
## Front Matter 解析要求
|
|
977
943
|
|
|
978
|
-
|
|
944
|
+
CLI 必须兼容:
|
|
979
945
|
|
|
980
946
|
1. UTF-8 BOM;
|
|
981
947
|
2. Windows 换行 `\r\n`;
|
|
@@ -1000,9 +966,9 @@ Go 程序必须兼容:
|
|
|
1000
966
|
|
|
1001
967
|
---
|
|
1002
968
|
|
|
1003
|
-
##
|
|
969
|
+
## 首次保存请求字段
|
|
1004
970
|
|
|
1005
|
-
|
|
971
|
+
首次保存接口最终应收到:
|
|
1006
972
|
|
|
1007
973
|
```json
|
|
1008
974
|
{
|
|
@@ -1013,7 +979,7 @@ Go 程序必须兼容:
|
|
|
1013
979
|
"article_excerpt": "文章描述,可以为空",
|
|
1014
980
|
"article_url": "article-url-slug",
|
|
1015
981
|
"origin": "QCPlay",
|
|
1016
|
-
"status": "
|
|
982
|
+
"status": "0",
|
|
1017
983
|
"cate_id": "2",
|
|
1018
984
|
"video_link": "",
|
|
1019
985
|
"is_hot": "0",
|
|
@@ -1033,23 +999,33 @@ Go 程序必须兼容:
|
|
|
1033
999
|
|
|
1034
1000
|
## 成功输出
|
|
1035
1001
|
|
|
1036
|
-
|
|
1002
|
+
首次保存成功后输出:
|
|
1037
1003
|
|
|
1038
1004
|
```txt
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1005
|
+
文章已保存为未上线状态
|
|
1006
|
+
文章 ID: 123
|
|
1007
|
+
文章状态: 未上线
|
|
1008
|
+
预览地址: https://snail.qingcigame.com/official/news-details.html?id=123
|
|
1009
|
+
已打开文章预览页面,请由用户检查文章内容和排版。
|
|
1044
1010
|
```
|
|
1045
1011
|
|
|
1046
|
-
|
|
1012
|
+
输出后必须等待用户明确答复。不能因为接口成功、页面可以打开或 AI 自行检查通过而自动上线。
|
|
1013
|
+
|
|
1014
|
+
确认上线成功后输出:
|
|
1047
1015
|
|
|
1048
1016
|
```txt
|
|
1049
|
-
|
|
1017
|
+
文章已上线
|
|
1018
|
+
文章 ID: 123
|
|
1019
|
+
文章状态: 已上线
|
|
1020
|
+
文章地址: https://snail.qingcigame.com/official/news-details.html?id=123
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
删除成功后输出:
|
|
1050
1024
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1025
|
+
```txt
|
|
1026
|
+
文章已删除
|
|
1027
|
+
文章 ID: 123
|
|
1028
|
+
文章状态: 已删除
|
|
1053
1029
|
```
|
|
1054
1030
|
|
|
1055
1031
|
---
|
|
@@ -1142,13 +1118,14 @@ release_time 格式错误,请使用 YYYY-MM-DD,例如 2026-07-09
|
|
|
1142
1118
|
4. 不要在命令中拼接全部文章字段;
|
|
1143
1119
|
5. 文章字段从 `article.md` Front Matter 读取;
|
|
1144
1120
|
6. Markdown 正文作为 `article_content`;
|
|
1145
|
-
7. Node CLI
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1121
|
+
7. Node CLI 负责解析、校验、读取 auth 并调用后端接口;
|
|
1122
|
+
8. Go 后端负责权限校验、强制草稿状态和数据库更新;
|
|
1123
|
+
9. 保存、上线或删除的成功和失败都在当前终端输出;
|
|
1124
|
+
10. 不得打印完整 token;
|
|
1125
|
+
11. 未得到用户确认,不得执行上线;
|
|
1126
|
+
12. 删除只能更新 `status=2`,不得物理删除数据;
|
|
1127
|
+
13. 保存成功后只负责打开预览网页,由用户本人检查;
|
|
1128
|
+
14. AI 不得访问、抓取、截图、分析、比较或判断预览内容。
|
|
1152
1129
|
|
|
1153
1130
|
---
|
|
1154
1131
|
|
|
@@ -1165,7 +1142,9 @@ release_time 格式错误,请使用 YYYY-MM-DD,例如 2026-07-09
|
|
|
1165
1142
|
7. 将 token 写入 article.md;
|
|
1166
1143
|
8. 未校验正文直接发布;
|
|
1167
1144
|
9. 将 `is_index2` 和 `type` 暴露给普通用户填写;
|
|
1168
|
-
10.
|
|
1145
|
+
10. 发布失败时伪造成功结果;
|
|
1146
|
+
11. AI 自动检查预览页面并代替用户决定上线;
|
|
1147
|
+
12. 未收到用户明确确认就把文章更新为 `status=1`。
|
|
1169
1148
|
|
|
1170
1149
|
---
|
|
1171
1150
|
|
|
@@ -1189,14 +1168,21 @@ qcplay article init
|
|
|
1189
1168
|
qcplay www-article-list.store article.md
|
|
1190
1169
|
```
|
|
1191
1170
|
|
|
1192
|
-
读取 `article.md`
|
|
1171
|
+
读取 `article.md` 并保存为未上线文章。
|
|
1172
|
+
|
|
1173
|
+
```bash
|
|
1174
|
+
qcplay article update [id]
|
|
1175
|
+
qcplay article delete [id]
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
分别将文章状态更新为已上线和已删除。
|
|
1193
1179
|
|
|
1194
1180
|
并满足:
|
|
1195
1181
|
|
|
1196
1182
|
- 能解析 Front Matter;
|
|
1197
1183
|
- 能读取 Markdown 正文;
|
|
1198
1184
|
- 能读取 `~/.qcplay/auth.json`;
|
|
1199
|
-
-
|
|
1185
|
+
- 能调用认证后端并处理接口错误;
|
|
1200
1186
|
- 能处理无权限错误;
|
|
1201
1187
|
- 能处理字段缺失错误;
|
|
1202
1188
|
- 不要求用户在命令行传标题;
|
|
@@ -1211,8 +1197,11 @@ qcplay www-article-list.store article.md
|
|
|
1211
1197
|
命令只传文章文件。
|
|
1212
1198
|
正文自动作为 article_content。
|
|
1213
1199
|
登录状态读取 auth.json。
|
|
1214
|
-
Node
|
|
1215
|
-
Go
|
|
1200
|
+
Node 解析文章并调用后端。
|
|
1201
|
+
Go 后端负责权限和数据库更新。
|
|
1216
1202
|
默认参数程序内部补齐。
|
|
1203
|
+
首次保存固定 status=0。
|
|
1204
|
+
确认后更新 status=1。
|
|
1205
|
+
删除更新 status=2。
|
|
1217
1206
|
发布失败不得伪造成成功。
|
|
1218
1207
|
```
|