@qcplay/cli 1.0.4 → 1.0.6
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/bin/qcplay.js
CHANGED
|
@@ -1,32 +1,116 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn } from "child_process";
|
|
3
4
|
import fs from "fs";
|
|
5
|
+
import http from "http";
|
|
6
|
+
import https from "https";
|
|
4
7
|
import os from "os";
|
|
5
8
|
import path from "path";
|
|
6
|
-
import
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
9
|
+
import readline from "readline";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
8
11
|
|
|
9
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
13
|
const __dirname = path.dirname(__filename);
|
|
11
14
|
const PACKAGE_JSON = path.resolve(__dirname, "../package.json");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const AUTH_PAGE = resolveAuthPagePath();
|
|
16
|
+
const DEFAULT_AUTH_BACKEND_URL = "https://cli.qcg.ink";
|
|
17
|
+
const DEFAULT_PUBLISH_BACKEND_URL = "https://cli.qcg.ink";
|
|
18
|
+
const DEFAULT_AUTH_PAGE_URL = "https://cli.qcg.ink/auth.html";
|
|
19
|
+
const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:8787";
|
|
20
|
+
const ENV_AUTH_BACKEND_URL = process.env.QCPLAY_BACKEND_URL || process.env.QCPLAY_AUTH_BACKEND_URL;
|
|
21
|
+
const ENV_PUBLISH_BACKEND_URL = process.env.QCPLAY_PUBLISH_BACKEND_URL;
|
|
22
|
+
const ENV_AUTH_PAGE_URL = process.env.QCPLAY_AUTH_PAGE_URL;
|
|
23
|
+
const ENV_LOCAL_BASE_URL = process.env.QCPLAY_LOCAL_BASE_URL;
|
|
24
|
+
|
|
25
|
+
function resolveAuthPagePath() {
|
|
26
|
+
const repoAuthPage = path.resolve(__dirname, "../../web/auth.html");
|
|
27
|
+
if (fs.existsSync(repoAuthPage)) {
|
|
28
|
+
return repoAuthPage;
|
|
19
29
|
}
|
|
30
|
+
|
|
31
|
+
return path.resolve(__dirname, "../web/auth.html");
|
|
20
32
|
}
|
|
21
33
|
|
|
22
|
-
const
|
|
34
|
+
const CATEGORY_MAP = {
|
|
35
|
+
"2": "2",
|
|
36
|
+
"3": "3",
|
|
37
|
+
"7": "7",
|
|
38
|
+
"8": "8",
|
|
39
|
+
"9": "9",
|
|
40
|
+
"25": "25",
|
|
41
|
+
"26": "26",
|
|
42
|
+
"27": "27",
|
|
43
|
+
"29": "29",
|
|
44
|
+
"综合": "2",
|
|
45
|
+
"活动": "3",
|
|
46
|
+
"游戏攻略": "7",
|
|
47
|
+
"视频中心": "8",
|
|
48
|
+
"萌新入门": "9",
|
|
49
|
+
"萌新入门-攻略专区": "25",
|
|
50
|
+
"高手进阶": "26",
|
|
51
|
+
"活动攻略": "27",
|
|
52
|
+
"视频攻略": "29"
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const AREA_MAP = {
|
|
56
|
+
"1": "1",
|
|
57
|
+
"3": "3",
|
|
58
|
+
"4": "4",
|
|
59
|
+
pc: "1",
|
|
60
|
+
PC: "1",
|
|
61
|
+
"资料站": "3",
|
|
62
|
+
"公益网站": "4"
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const GAME_MAP = {
|
|
66
|
+
"39": "39",
|
|
67
|
+
"最强蜗牛": "39"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const STATUS_MAP = {
|
|
71
|
+
"0": "0",
|
|
72
|
+
"1": "1",
|
|
73
|
+
"未发布": "0",
|
|
74
|
+
"已发布": "1",
|
|
75
|
+
"发布": "1"
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const BOOLEAN_TEXT_MAP = {
|
|
79
|
+
"0": "0",
|
|
80
|
+
"1": "1",
|
|
81
|
+
false: "0",
|
|
82
|
+
true: "1",
|
|
83
|
+
no: "0",
|
|
84
|
+
yes: "1",
|
|
85
|
+
"非热门": "0",
|
|
86
|
+
"热门": "1",
|
|
87
|
+
"不推荐": "0",
|
|
88
|
+
"推荐": "1"
|
|
89
|
+
};
|
|
23
90
|
|
|
24
91
|
const QCPLAY_DIR = path.join(os.homedir(), ".qcplay");
|
|
25
92
|
const AGENTS_DIR = path.join(os.homedir(), ".agents");
|
|
26
|
-
const AUTH_FILE = path.join(QCPLAY_DIR, "auth.json");
|
|
27
93
|
const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
|
|
28
94
|
const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
29
95
|
|
|
96
|
+
function readPackageMetadata() {
|
|
97
|
+
try {
|
|
98
|
+
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
|
|
99
|
+
return {
|
|
100
|
+
name: packageJson.name || "@qcplay/cli",
|
|
101
|
+
version: packageJson.version || "0.0.0"
|
|
102
|
+
};
|
|
103
|
+
} catch {
|
|
104
|
+
return {
|
|
105
|
+
name: "@qcplay/cli",
|
|
106
|
+
version: "0.0.0"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PACKAGE_METADATA = readPackageMetadata();
|
|
112
|
+
const PACKAGE_NAME = PACKAGE_METADATA.name;
|
|
113
|
+
const PACKAGE_VERSION = PACKAGE_METADATA.version;
|
|
30
114
|
const colorsEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
31
115
|
|
|
32
116
|
const chalk = {
|
|
@@ -49,13 +133,14 @@ function printRootHelp() {
|
|
|
49
133
|
console.log(`QCPlay CLI
|
|
50
134
|
|
|
51
135
|
Usage:
|
|
52
|
-
qcplay-cli install
|
|
53
|
-
qcplay-cli
|
|
54
|
-
qcplay-cli auth
|
|
136
|
+
qcplay-cli install [--local]
|
|
137
|
+
qcplay-cli update
|
|
138
|
+
qcplay-cli auth [login|status|logout] [--local] [--backend <url>] [--auth-page <url>]
|
|
139
|
+
qcplay-cli auth permissions [--local] [--key <key>] [--json]
|
|
55
140
|
qcplay-cli article
|
|
56
141
|
qcplay-cli article init [file]
|
|
57
|
-
qcplay-cli article publish <file> [--
|
|
58
|
-
qcplay-cli www-article-list.store <file> [--
|
|
142
|
+
qcplay-cli article publish <file> [--local] [--backend <url>] [--dry-run]
|
|
143
|
+
qcplay-cli www-article-list.store <file> [--local] [--backend <url>] [--dry-run]
|
|
59
144
|
qcplay-cli features
|
|
60
145
|
qcplay-cli where
|
|
61
146
|
|
|
@@ -65,109 +150,55 @@ Options:
|
|
|
65
150
|
`);
|
|
66
151
|
}
|
|
67
152
|
|
|
153
|
+
function printUpdateHelp() {
|
|
154
|
+
console.log("Usage:");
|
|
155
|
+
console.log(" qcplay-cli update");
|
|
156
|
+
}
|
|
157
|
+
|
|
68
158
|
function printAuthHelp() {
|
|
69
159
|
console.log("Usage:");
|
|
70
|
-
console.log(" qcplay-cli auth");
|
|
71
|
-
console.log(" qcplay-cli auth
|
|
72
|
-
console.log(" qcplay-cli auth logout");
|
|
73
|
-
console.log(" qcplay-cli auth permissions [--key <key>] [--json]");
|
|
160
|
+
console.log(" qcplay-cli auth [login|status|logout] [--local] [--backend <url>] [--auth-page <url>]");
|
|
161
|
+
console.log(" qcplay-cli auth permissions [--local] [--backend <url>] [--key <key>] [--json]");
|
|
74
162
|
}
|
|
75
163
|
|
|
76
164
|
function printArticleHelp() {
|
|
77
165
|
console.log(`Usage:
|
|
78
166
|
qcplay-cli article
|
|
79
167
|
qcplay-cli article init [file]
|
|
80
|
-
qcplay-cli article publish <file> [--
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function getVendorDir() {
|
|
84
|
-
if (process.platform === "win32") {
|
|
85
|
-
return path.resolve(__dirname, "../vendor/win32");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (process.platform === "darwin") {
|
|
89
|
-
return path.resolve(__dirname, "../vendor/darwin");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (process.platform === "linux") {
|
|
93
|
-
return path.resolve(__dirname, "../vendor/linux");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
168
|
+
qcplay-cli article publish <file> [--local] [--backend <url>] [--dry-run]`);
|
|
97
169
|
}
|
|
98
170
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
171
|
+
function printFeatures() {
|
|
172
|
+
console.log("");
|
|
173
|
+
console.log(chalk.green("你现在可以使用以下功能:"));
|
|
174
|
+
console.log("");
|
|
175
|
+
console.log(chalk.cyan("1. 登录认证"));
|
|
176
|
+
console.log(" qcplay-cli auth");
|
|
177
|
+
console.log(chalk.gray(" 会唤起网页登录页,不再在控制台输入密码。"));
|
|
178
|
+
console.log("");
|
|
179
|
+
console.log(chalk.cyan("2. 查看权限"));
|
|
180
|
+
console.log(" qcplay-cli auth permissions");
|
|
181
|
+
console.log(" qcplay-cli auth permissions --key www-article-list.store");
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log(chalk.cyan("3. 发布官网文章"));
|
|
184
|
+
console.log(" qcplay-cli www-article-list.store article.md");
|
|
185
|
+
console.log("");
|
|
186
|
+
console.log(chalk.cyan("4. 查看本地目录"));
|
|
187
|
+
console.log(" qcplay-cli where");
|
|
188
|
+
console.log("");
|
|
113
189
|
}
|
|
114
190
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
const candidates = getExeCandidates(name);
|
|
118
|
-
|
|
119
|
-
for (const candidate of candidates) {
|
|
120
|
-
const exePath = path.join(vendorDir, candidate);
|
|
121
|
-
if (fs.existsSync(exePath)) {
|
|
122
|
-
return exePath;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return path.join(vendorDir, candidates[0]);
|
|
191
|
+
function printWhere() {
|
|
192
|
+
console.log(QCPLAY_DIR);
|
|
127
193
|
}
|
|
128
194
|
|
|
129
|
-
function
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!fs.existsSync(exePath)) {
|
|
134
|
-
reject(new Error(`执行程序不存在: ${exePath}`));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const child = spawn(exePath, args, {
|
|
139
|
-
stdio: "inherit",
|
|
140
|
-
shell: false
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
child.on("error", err => {
|
|
144
|
-
reject(err);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
child.on("exit", code => {
|
|
148
|
-
if (code === 0) {
|
|
149
|
-
resolve();
|
|
150
|
-
} else {
|
|
151
|
-
reject(new Error(`${name} 执行失败,退出码: ${code}`));
|
|
152
|
-
}
|
|
153
|
-
});
|
|
195
|
+
function createReadline() {
|
|
196
|
+
return readline.createInterface({
|
|
197
|
+
input: process.stdin,
|
|
198
|
+
output: process.stdout
|
|
154
199
|
});
|
|
155
200
|
}
|
|
156
201
|
|
|
157
|
-
async function runAuthLogin() {
|
|
158
|
-
try {
|
|
159
|
-
await runNative("qcplay-auth", []);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
const message = String(err?.message || err);
|
|
162
|
-
|
|
163
|
-
if (!message.includes("退出码")) {
|
|
164
|
-
throw err;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
await runNative("qcplay-auth", ["login"]);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
202
|
async function pathExists(targetPath) {
|
|
172
203
|
try {
|
|
173
204
|
await fs.promises.access(targetPath);
|
|
@@ -199,16 +230,19 @@ async function copyRecursive(sourcePath, targetPath) {
|
|
|
199
230
|
|
|
200
231
|
async function writeJson(filePath, value) {
|
|
201
232
|
await ensureDir(path.dirname(filePath));
|
|
202
|
-
await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`,
|
|
233
|
+
await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
234
|
+
encoding: "utf8",
|
|
235
|
+
mode: 0o600
|
|
236
|
+
});
|
|
203
237
|
}
|
|
204
238
|
|
|
205
239
|
async function readJson(filePath) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
240
|
+
try {
|
|
241
|
+
const raw = await fs.promises.readFile(filePath, "utf8");
|
|
242
|
+
return JSON.parse(raw);
|
|
243
|
+
} catch {
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
212
246
|
}
|
|
213
247
|
|
|
214
248
|
async function ensureLocalDirs() {
|
|
@@ -216,147 +250,564 @@ async function ensureLocalDirs() {
|
|
|
216
250
|
await ensureDir(SKILLS_DIR);
|
|
217
251
|
}
|
|
218
252
|
|
|
253
|
+
async function filesAreEqual(sourcePath, targetPath) {
|
|
254
|
+
try {
|
|
255
|
+
const [sourceContent, targetContent] = await Promise.all([
|
|
256
|
+
fs.promises.readFile(sourcePath),
|
|
257
|
+
fs.promises.readFile(targetPath)
|
|
258
|
+
]);
|
|
259
|
+
return sourceContent.equals(targetContent);
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function syncDirectoryChanges(sourceDir, targetDir, rootDir = sourceDir, changes = { added: [], updated: [] }) {
|
|
266
|
+
await ensureDir(targetDir);
|
|
267
|
+
const entries = await fs.promises.readdir(sourceDir, { withFileTypes: true });
|
|
268
|
+
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
271
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
272
|
+
|
|
273
|
+
if (entry.isDirectory()) {
|
|
274
|
+
await syncDirectoryChanges(sourcePath, targetPath, rootDir, changes);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!entry.isFile()) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const relativePath = path.relative(rootDir, sourcePath);
|
|
283
|
+
if (!(await pathExists(targetPath))) {
|
|
284
|
+
await ensureDir(path.dirname(targetPath));
|
|
285
|
+
await fs.promises.copyFile(sourcePath, targetPath);
|
|
286
|
+
changes.added.push(relativePath);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (await filesAreEqual(sourcePath, targetPath)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await fs.promises.copyFile(sourcePath, targetPath);
|
|
295
|
+
changes.updated.push(relativePath);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return changes;
|
|
299
|
+
}
|
|
300
|
+
|
|
219
301
|
async function installSkills() {
|
|
220
302
|
const sourceDir = path.resolve(__dirname, "../templates/skills");
|
|
221
303
|
|
|
222
304
|
if (!(await pathExists(sourceDir))) {
|
|
223
|
-
return
|
|
305
|
+
return {
|
|
306
|
+
available: false,
|
|
307
|
+
added: [],
|
|
308
|
+
updated: []
|
|
309
|
+
};
|
|
224
310
|
}
|
|
225
311
|
|
|
226
|
-
await
|
|
227
|
-
return
|
|
312
|
+
const changes = await syncDirectoryChanges(sourceDir, SKILLS_DIR);
|
|
313
|
+
return {
|
|
314
|
+
available: true,
|
|
315
|
+
...changes
|
|
316
|
+
};
|
|
228
317
|
}
|
|
229
318
|
|
|
230
|
-
async function saveConfig() {
|
|
319
|
+
async function saveConfig(authBackendUrl, publishBackendUrl, authPageUrl, localMode = false) {
|
|
231
320
|
await writeJson(CONFIG_FILE, {
|
|
232
321
|
cli: "qcplay-cli",
|
|
233
322
|
version: PACKAGE_VERSION,
|
|
234
|
-
|
|
323
|
+
auth_backend_url: authBackendUrl,
|
|
324
|
+
publish_backend_url: publishBackendUrl,
|
|
325
|
+
auth_page_url: authPageUrl || "",
|
|
326
|
+
local_mode: localMode,
|
|
235
327
|
skills_dir: SKILLS_DIR,
|
|
236
328
|
configured_at: Date.now()
|
|
237
329
|
});
|
|
238
330
|
}
|
|
239
331
|
|
|
240
|
-
async function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
332
|
+
async function loadConfig() {
|
|
333
|
+
return readJson(CONFIG_FILE);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function updateStoredConfigVersion(version) {
|
|
337
|
+
const config = await loadConfig();
|
|
338
|
+
if (Object.keys(config).length === 0) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
await writeJson(CONFIG_FILE, {
|
|
343
|
+
...config,
|
|
344
|
+
version,
|
|
345
|
+
updated_at: Date.now()
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalizeText(value) {
|
|
350
|
+
return String(value ?? "").trim();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function normalizeMappedValue(value, map) {
|
|
354
|
+
const text = normalizeText(value);
|
|
355
|
+
if (!text) {
|
|
356
|
+
return "";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return map[text] ?? map[text.toLowerCase?.() || ""] ?? text;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function normalizeBoolLike(value, defaultValue = "0") {
|
|
363
|
+
const text = normalizeText(value);
|
|
364
|
+
if (!text) {
|
|
365
|
+
return defaultValue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return BOOLEAN_TEXT_MAP[text] ?? BOOLEAN_TEXT_MAP[text.toLowerCase()] ?? text;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function parseBackendOptions(args) {
|
|
372
|
+
const options = {
|
|
373
|
+
backend: undefined,
|
|
374
|
+
authPage: undefined,
|
|
375
|
+
local: false
|
|
376
|
+
};
|
|
377
|
+
const passthrough = [];
|
|
378
|
+
|
|
379
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
380
|
+
const current = args[index];
|
|
381
|
+
|
|
382
|
+
if (current === "--backend") {
|
|
383
|
+
const next = args[index + 1];
|
|
384
|
+
if (!next || next.startsWith("--")) {
|
|
385
|
+
throw new Error("--backend 缺少参数值");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
options.backend = next;
|
|
389
|
+
index += 1;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (current === "--auth-page") {
|
|
394
|
+
const next = args[index + 1];
|
|
395
|
+
if (!next || next.startsWith("--")) {
|
|
396
|
+
throw new Error("--auth-page 缺少参数值");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
options.authPage = next;
|
|
400
|
+
index += 1;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (current === "--local") {
|
|
405
|
+
options.local = true;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
passthrough.push(current);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
options,
|
|
414
|
+
args: passthrough
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function parsePermissionsOptions(args) {
|
|
419
|
+
const options = {
|
|
420
|
+
key: undefined,
|
|
421
|
+
json: false
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
425
|
+
const current = args[index];
|
|
426
|
+
|
|
427
|
+
if (current === "--json") {
|
|
428
|
+
options.json = true;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (current === "--key") {
|
|
433
|
+
const next = args[index + 1];
|
|
434
|
+
if (!next || next.startsWith("--")) {
|
|
435
|
+
throw new Error("--key 缺少参数值");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
options.key = next;
|
|
439
|
+
index += 1;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
throw new Error(`未知参数: ${current}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return options;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function parsePublishOptions(args) {
|
|
450
|
+
const options = {
|
|
451
|
+
backend: undefined,
|
|
452
|
+
local: false,
|
|
453
|
+
dryRun: false
|
|
454
|
+
};
|
|
455
|
+
const positional = [];
|
|
456
|
+
|
|
457
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
458
|
+
const current = args[index];
|
|
459
|
+
|
|
460
|
+
if (current === "--dry-run") {
|
|
461
|
+
options.dryRun = true;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (current === "--backend") {
|
|
466
|
+
const next = args[index + 1];
|
|
467
|
+
if (!next || next.startsWith("--")) {
|
|
468
|
+
throw new Error("--backend 缺少参数值");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
options.backend = next;
|
|
472
|
+
index += 1;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (current === "--local") {
|
|
477
|
+
options.local = true;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (current.startsWith("--")) {
|
|
482
|
+
throw new Error(`未知参数: ${current}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
positional.push(current);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
file: positional[0],
|
|
490
|
+
options
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function requestJson(method, baseUrl, pathname, payload) {
|
|
495
|
+
return new Promise((resolve, reject) => {
|
|
496
|
+
const url = new URL(pathname, baseUrl);
|
|
497
|
+
const client = url.protocol === "https:" ? https : http;
|
|
498
|
+
const body = payload === undefined ? undefined : JSON.stringify(payload);
|
|
499
|
+
|
|
500
|
+
const req = client.request(
|
|
501
|
+
url,
|
|
502
|
+
{
|
|
503
|
+
method,
|
|
504
|
+
headers: {
|
|
505
|
+
Accept: "application/json",
|
|
506
|
+
"Content-Type": "application/json",
|
|
507
|
+
"User-Agent": `qcplay-cli/${PACKAGE_VERSION}`,
|
|
508
|
+
...(body ? { "Content-Length": Buffer.byteLength(body) } : {})
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
res => {
|
|
512
|
+
const chunks = [];
|
|
513
|
+
res.on("data", chunk => chunks.push(chunk));
|
|
514
|
+
res.on("end", () => {
|
|
515
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
516
|
+
let json = {};
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
json = text ? JSON.parse(text) : {};
|
|
520
|
+
} catch {
|
|
521
|
+
reject(new Error(`后端返回了非 JSON 内容: ${text.slice(0, 160)}`));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) {
|
|
526
|
+
reject(new Error(json.message || `接口请求失败 (${res.statusCode})`));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
resolve(json);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
req.on("error", err => {
|
|
536
|
+
reject(new Error(`无法连接后端服务 ${baseUrl},请检查服务地址或网络连接。原始错误: ${err.message}`));
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
if (body) {
|
|
540
|
+
req.write(body);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
req.end();
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function openBrowser(targetUrl) {
|
|
548
|
+
return new Promise((resolve, reject) => {
|
|
549
|
+
let command;
|
|
550
|
+
let args;
|
|
551
|
+
|
|
552
|
+
if (process.platform === "win32") {
|
|
553
|
+
command = "cmd";
|
|
554
|
+
args = ["/c", "start", "", targetUrl];
|
|
555
|
+
} else if (process.platform === "darwin") {
|
|
556
|
+
command = "open";
|
|
557
|
+
args = [targetUrl];
|
|
558
|
+
} else {
|
|
559
|
+
command = "xdg-open";
|
|
560
|
+
args = [targetUrl];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const child = spawn(command, args, {
|
|
564
|
+
detached: true,
|
|
565
|
+
stdio: "ignore"
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
child.on("error", reject);
|
|
569
|
+
child.unref();
|
|
570
|
+
resolve();
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function getNpmCommand() {
|
|
575
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function runCommand(command, args) {
|
|
579
|
+
return new Promise((resolve, reject) => {
|
|
580
|
+
const child = spawn(command, args, {
|
|
581
|
+
stdio: "inherit"
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
child.on("error", reject);
|
|
585
|
+
child.on("close", code => {
|
|
586
|
+
if (code === 0) {
|
|
587
|
+
resolve();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
reject(new Error(`命令执行失败 (${code}): ${command} ${args.join(" ")}`));
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function delay(ms) {
|
|
597
|
+
return new Promise(resolve => {
|
|
598
|
+
setTimeout(resolve, ms);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function normalizeOptionalUrl(value) {
|
|
603
|
+
const text = normalizeText(value);
|
|
604
|
+
return text || "";
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function ensureAbsoluteUrl(value, flagName) {
|
|
608
|
+
try {
|
|
609
|
+
return new URL(value).toString();
|
|
610
|
+
} catch {
|
|
611
|
+
throw new Error(`${flagName} 必须是完整 URL,例如 https://example.com/auth.html`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function resolveLocalBaseUrl() {
|
|
616
|
+
const localBaseUrl = normalizeOptionalUrl(ENV_LOCAL_BASE_URL) || DEFAULT_LOCAL_BASE_URL;
|
|
617
|
+
return ensureAbsoluteUrl(localBaseUrl, "QCPLAY_LOCAL_BASE_URL");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function isLocalMode(options, config = {}) {
|
|
621
|
+
return Boolean(options.local || config.local_mode);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function resolveAuthBackendUrl(options, config = {}) {
|
|
625
|
+
if (!normalizeOptionalUrl(options.backend) && isLocalMode(options, config)) {
|
|
626
|
+
return resolveLocalBaseUrl();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const backendUrl =
|
|
630
|
+
normalizeOptionalUrl(options.backend) ||
|
|
631
|
+
normalizeOptionalUrl(ENV_AUTH_BACKEND_URL) ||
|
|
632
|
+
normalizeOptionalUrl(config.auth_backend_url) ||
|
|
633
|
+
normalizeOptionalUrl(config.backend_url) ||
|
|
634
|
+
DEFAULT_AUTH_BACKEND_URL;
|
|
635
|
+
return ensureAbsoluteUrl(backendUrl, "--backend");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function resolveAuthPageUrl(options, config = {}) {
|
|
639
|
+
if (!normalizeOptionalUrl(options.authPage) && isLocalMode(options, config)) {
|
|
640
|
+
return "";
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const authPageUrl =
|
|
644
|
+
normalizeOptionalUrl(options.authPage) ||
|
|
645
|
+
normalizeOptionalUrl(ENV_AUTH_PAGE_URL) ||
|
|
646
|
+
normalizeOptionalUrl(config.auth_page_url) ||
|
|
647
|
+
DEFAULT_AUTH_PAGE_URL;
|
|
648
|
+
return authPageUrl ? ensureAbsoluteUrl(authPageUrl, "--auth-page") : "";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function resolvePublishBackendUrl(options, config = {}) {
|
|
652
|
+
if (!normalizeOptionalUrl(options.backend) && isLocalMode(options, config)) {
|
|
653
|
+
return resolveLocalBaseUrl();
|
|
246
654
|
}
|
|
247
655
|
|
|
248
|
-
|
|
656
|
+
const backendUrl =
|
|
657
|
+
normalizeOptionalUrl(options.backend) ||
|
|
658
|
+
normalizeOptionalUrl(ENV_PUBLISH_BACKEND_URL) ||
|
|
659
|
+
normalizeOptionalUrl(config.publish_backend_url) ||
|
|
660
|
+
DEFAULT_PUBLISH_BACKEND_URL;
|
|
661
|
+
return ensureAbsoluteUrl(backendUrl, "--backend");
|
|
662
|
+
}
|
|
249
663
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
ok: false,
|
|
255
|
-
reason: `认证文件不是合法 JSON: ${err.message}`
|
|
256
|
-
};
|
|
257
|
-
}
|
|
664
|
+
async function fetchLoginSuccessSequence(baseUrl) {
|
|
665
|
+
const response = await requestJson("GET", baseUrl, "/api/auth/login-success");
|
|
666
|
+
return Number(response.data?.sequence || 0);
|
|
667
|
+
}
|
|
258
668
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
auth.token ||
|
|
263
|
-
auth.data?.access_token ||
|
|
264
|
-
auth.data?.accessToken;
|
|
669
|
+
function isNonJsonResponseError(error) {
|
|
670
|
+
return String(error?.message || "").includes("后端返回了非 JSON 内容");
|
|
671
|
+
}
|
|
265
672
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
673
|
+
function buildAuthBackendError(baseUrl, error) {
|
|
674
|
+
if (isNonJsonResponseError(error)) {
|
|
675
|
+
return new Error(
|
|
676
|
+
`认证接口 ${baseUrl} 返回了 HTML 页面,不是 JSON。请确认 ${baseUrl}/api/auth/login、${baseUrl}/api/auth/login-success、${baseUrl}/api/auth/status 已正确部署,而不是被站点首页或登录页接管。`
|
|
677
|
+
);
|
|
271
678
|
}
|
|
272
679
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
auth.expiresAt ||
|
|
276
|
-
auth.data?.expires_at ||
|
|
277
|
-
auth.data?.expiresAt;
|
|
680
|
+
return error;
|
|
681
|
+
}
|
|
278
682
|
|
|
279
|
-
|
|
280
|
-
|
|
683
|
+
async function waitForLoginSuccessSequence(baseUrl, previousSequence, intervalMs = 1000, timeoutMs = 60000) {
|
|
684
|
+
const startedAt = Date.now();
|
|
685
|
+
let lastError;
|
|
281
686
|
|
|
282
|
-
|
|
283
|
-
|
|
687
|
+
while (true) {
|
|
688
|
+
try {
|
|
689
|
+
const nextSequence = await fetchLoginSuccessSequence(baseUrl);
|
|
690
|
+
if (nextSequence > previousSequence) {
|
|
691
|
+
return nextSequence;
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
lastError = error;
|
|
284
695
|
}
|
|
285
696
|
|
|
286
|
-
if (Date.now() >=
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
697
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
698
|
+
if (lastError) {
|
|
699
|
+
throw buildAuthBackendError(baseUrl, lastError);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
throw new Error(`等待登录结果超时,请确认认证接口 ${baseUrl} 可用`);
|
|
291
703
|
}
|
|
292
|
-
}
|
|
293
704
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
auth
|
|
297
|
-
};
|
|
705
|
+
await delay(intervalMs);
|
|
706
|
+
}
|
|
298
707
|
}
|
|
299
708
|
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
console.log(chalk.green("你现在可以使用以下功能:"));
|
|
303
|
-
console.log("");
|
|
709
|
+
function buildAuthPageUrl(backendUrl, authPageUrl = "") {
|
|
710
|
+
const targetUrl = authPageUrl ? new URL(authPageUrl) : pathToFileURL(AUTH_PAGE);
|
|
304
711
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
712
|
+
if (shouldAttachBackendQuery(targetUrl, backendUrl)) {
|
|
713
|
+
targetUrl.searchParams.set("backend", backendUrl);
|
|
714
|
+
}
|
|
308
715
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
console.log(" qcplay-cli auth permissions --key www-article-list.store");
|
|
312
|
-
console.log("");
|
|
716
|
+
return targetUrl.toString();
|
|
717
|
+
}
|
|
313
718
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
719
|
+
function shouldAttachBackendQuery(targetUrl, backendUrl) {
|
|
720
|
+
if (!backendUrl) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
318
723
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
724
|
+
const normalizedBackendUrl = new URL(backendUrl);
|
|
725
|
+
if (targetUrl.protocol === "file:") {
|
|
726
|
+
return normalizedBackendUrl.origin !== new URL(DEFAULT_LOCAL_BASE_URL).origin;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return normalizedBackendUrl.origin !== targetUrl.origin;
|
|
322
730
|
}
|
|
323
731
|
|
|
324
|
-
function
|
|
325
|
-
|
|
732
|
+
function flattenPermissionTree(items, bucket = []) {
|
|
733
|
+
for (const item of items) {
|
|
734
|
+
bucket.push(item);
|
|
735
|
+
if (Array.isArray(item.children) && item.children.length > 0) {
|
|
736
|
+
flattenPermissionTree(item.children, bucket);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return bucket;
|
|
326
741
|
}
|
|
327
742
|
|
|
328
|
-
function
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
743
|
+
function printPermissionTree(items) {
|
|
744
|
+
const flatItems = flattenPermissionTree(items);
|
|
745
|
+
const indexed = new Map();
|
|
746
|
+
const children = new Map();
|
|
747
|
+
const roots = [];
|
|
333
748
|
|
|
334
|
-
for (
|
|
335
|
-
const
|
|
749
|
+
for (const item of flatItems) {
|
|
750
|
+
const id = Number(item.id ?? 0);
|
|
751
|
+
indexed.set(id || item.key, item);
|
|
752
|
+
}
|
|
336
753
|
|
|
337
|
-
|
|
338
|
-
|
|
754
|
+
for (const item of flatItems) {
|
|
755
|
+
const parentId = Number(item.menu_id ?? item.parent_id ?? 0);
|
|
756
|
+
if (parentId && indexed.has(parentId)) {
|
|
757
|
+
if (!children.has(parentId)) {
|
|
758
|
+
children.set(parentId, []);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
children.get(parentId).push(item);
|
|
339
762
|
continue;
|
|
340
763
|
}
|
|
341
764
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!next || next.startsWith("--")) {
|
|
345
|
-
throw new Error("--key 缺少参数值");
|
|
346
|
-
}
|
|
765
|
+
roots.push(item);
|
|
766
|
+
}
|
|
347
767
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
768
|
+
if (roots.length === 0) {
|
|
769
|
+
console.log(chalk.yellow("未返回任何权限"));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
for (const item of roots) {
|
|
774
|
+
console.log(item.key);
|
|
775
|
+
const nested = children.get(Number(item.id ?? 0)) || [];
|
|
776
|
+
for (const child of nested) {
|
|
777
|
+
console.log(` - ${child.key}`);
|
|
351
778
|
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
352
781
|
|
|
353
|
-
|
|
782
|
+
function formatAccountSummary(data = {}) {
|
|
783
|
+
const id = Number(data.id || 0);
|
|
784
|
+
const name = normalizeText(data.name) || "未知账号";
|
|
785
|
+
const type = normalizeText(data.type);
|
|
786
|
+
const status = normalizeText(data.status);
|
|
787
|
+
const parts = [name];
|
|
788
|
+
|
|
789
|
+
if (id > 0) {
|
|
790
|
+
parts.push(`id=${id}`);
|
|
354
791
|
}
|
|
355
792
|
|
|
356
|
-
|
|
793
|
+
if (type) {
|
|
794
|
+
parts.push(`type=${type}`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (status) {
|
|
798
|
+
parts.push(`status=${status}`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return parts.join(" | ");
|
|
357
802
|
}
|
|
358
803
|
|
|
359
|
-
async function installCommand() {
|
|
804
|
+
async function installCommand(options) {
|
|
805
|
+
const config = await loadConfig();
|
|
806
|
+
const localMode = isLocalMode(options, config);
|
|
807
|
+
const authBackendUrl = resolveAuthBackendUrl(options, config);
|
|
808
|
+
const publishBackendUrl = resolvePublishBackendUrl(options, config);
|
|
809
|
+
const authPageUrl = resolveAuthPageUrl(options, config);
|
|
810
|
+
|
|
360
811
|
console.log("");
|
|
361
812
|
console.log(chalk.cyan("正在安装 QCPlay CLI..."));
|
|
362
813
|
console.log("");
|
|
@@ -365,177 +816,435 @@ async function installCommand() {
|
|
|
365
816
|
console.log(chalk.green("✔ 本地目录已创建"));
|
|
366
817
|
|
|
367
818
|
const skillsInstalled = await installSkills();
|
|
368
|
-
if (skillsInstalled) {
|
|
819
|
+
if (skillsInstalled.available) {
|
|
369
820
|
console.log(chalk.green("✔ Skills 已安装"));
|
|
370
821
|
console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
|
|
371
822
|
} else {
|
|
372
823
|
console.log(chalk.yellow("! 未找到 Skills 模板,已跳过"));
|
|
373
824
|
}
|
|
374
825
|
|
|
375
|
-
await saveConfig();
|
|
826
|
+
await saveConfig(authBackendUrl, publishBackendUrl, authPageUrl, localMode);
|
|
376
827
|
console.log(chalk.green("✔ 配置文件已生成"));
|
|
377
|
-
|
|
828
|
+
console.log(chalk.gray(`认证后端: ${authBackendUrl}`));
|
|
829
|
+
console.log(chalk.gray(`发布后端: ${publishBackendUrl}`));
|
|
830
|
+
console.log(chalk.gray(`登录页地址: ${authPageUrl || "本地内置页面"}`));
|
|
378
831
|
console.log("");
|
|
379
|
-
console.log(
|
|
380
|
-
console.log(
|
|
832
|
+
console.log("安装完成后可直接执行:");
|
|
833
|
+
console.log(" qcplay-cli auth");
|
|
381
834
|
console.log("");
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function printSkillsSyncResult(result) {
|
|
838
|
+
if (!result.available) {
|
|
839
|
+
console.log(chalk.yellow("! 未找到 Skills 模板,已跳过"));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
382
842
|
|
|
383
|
-
|
|
843
|
+
const addedCount = result.added.length;
|
|
844
|
+
const updatedCount = result.updated.length;
|
|
845
|
+
if (addedCount === 0 && updatedCount === 0) {
|
|
846
|
+
console.log(chalk.green("✔ Skills 已检查,无新增或变更"));
|
|
847
|
+
console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
384
850
|
|
|
385
|
-
const
|
|
386
|
-
if (
|
|
387
|
-
|
|
851
|
+
const parts = [];
|
|
852
|
+
if (addedCount > 0) {
|
|
853
|
+
parts.push(`新增 ${addedCount} 个文件`);
|
|
854
|
+
}
|
|
855
|
+
if (updatedCount > 0) {
|
|
856
|
+
parts.push(`更新 ${updatedCount} 个文件`);
|
|
388
857
|
}
|
|
389
858
|
|
|
859
|
+
console.log(chalk.green(`✔ Skills 已同步(${parts.join(",")})`));
|
|
860
|
+
console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function updateCommand() {
|
|
390
864
|
console.log("");
|
|
391
|
-
console.log(chalk.
|
|
865
|
+
console.log(chalk.cyan("正在更新 QCPlay CLI..."));
|
|
866
|
+
console.log(chalk.gray(`当前版本: ${PACKAGE_VERSION}`));
|
|
867
|
+
console.log("");
|
|
868
|
+
|
|
869
|
+
await ensureLocalDirs();
|
|
870
|
+
await runCommand(getNpmCommand(), ["install", "-g", `${PACKAGE_NAME}@latest`]);
|
|
871
|
+
|
|
872
|
+
const latestPackage = readPackageMetadata();
|
|
873
|
+
await updateStoredConfigVersion(latestPackage.version);
|
|
874
|
+
const skillsInstalled = await installSkills();
|
|
392
875
|
|
|
393
|
-
|
|
876
|
+
console.log("");
|
|
877
|
+
console.log(chalk.green("CLI 更新完成"));
|
|
878
|
+
if (latestPackage.version !== PACKAGE_VERSION) {
|
|
879
|
+
console.log(chalk.gray(`版本: ${PACKAGE_VERSION} -> ${latestPackage.version}`));
|
|
880
|
+
} else {
|
|
881
|
+
console.log(chalk.gray(`版本: ${latestPackage.version}`));
|
|
882
|
+
}
|
|
883
|
+
printSkillsSyncResult(skillsInstalled);
|
|
884
|
+
console.log("");
|
|
394
885
|
}
|
|
395
886
|
|
|
396
|
-
async function authCommand(
|
|
887
|
+
async function authCommand(rawArgs = []) {
|
|
888
|
+
const parsed = parseBackendOptions(rawArgs);
|
|
889
|
+
const options = parsed.options;
|
|
890
|
+
const args = parsed.args;
|
|
891
|
+
const action = args[0];
|
|
892
|
+
const config = await loadConfig();
|
|
893
|
+
const backendUrl = resolveAuthBackendUrl(options, config);
|
|
894
|
+
const authPageUrl = resolveAuthPageUrl(options, config);
|
|
895
|
+
|
|
397
896
|
if (!action || action === "login") {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
897
|
+
const loginPageUrl = buildAuthPageUrl(backendUrl, authPageUrl);
|
|
898
|
+
let previousSequence = 0;
|
|
899
|
+
|
|
900
|
+
console.log(chalk.cyan("正在登录中"));
|
|
901
|
+
try {
|
|
902
|
+
await openBrowser(loginPageUrl);
|
|
903
|
+
} catch {
|
|
904
|
+
console.log(chalk.yellow("自动打开浏览器失败,请手动打开下面地址:"));
|
|
905
|
+
console.log(chalk.gray(loginPageUrl));
|
|
906
|
+
}
|
|
401
907
|
|
|
402
|
-
|
|
908
|
+
try {
|
|
909
|
+
previousSequence = await fetchLoginSuccessSequence(backendUrl);
|
|
910
|
+
} catch {}
|
|
403
911
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
912
|
+
await waitForLoginSuccessSequence(backendUrl, previousSequence);
|
|
913
|
+
const statusResponse = await requestJson("GET", backendUrl, "/api/auth/status");
|
|
914
|
+
console.log(chalk.green("登录成功"));
|
|
915
|
+
if (statusResponse.data?.logged_in) {
|
|
916
|
+
console.log(chalk.gray(`当前账号: ${formatAccountSummary(statusResponse.data)}`));
|
|
407
917
|
}
|
|
408
|
-
|
|
409
918
|
return;
|
|
410
919
|
}
|
|
411
920
|
|
|
412
921
|
if (action === "status") {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
console.log(chalk.
|
|
417
|
-
console.log(`原因: ${loginStatus.reason}`);
|
|
418
|
-
console.log("");
|
|
419
|
-
console.log("请执行:");
|
|
420
|
-
console.log(" qcplay-cli auth");
|
|
922
|
+
const response = await requestJson("GET", backendUrl, "/api/auth/status");
|
|
923
|
+
if (response.data?.logged_in) {
|
|
924
|
+
console.log(chalk.green("已登录"));
|
|
925
|
+
console.log(chalk.gray(`当前账号: ${formatAccountSummary(response.data)}`));
|
|
421
926
|
return;
|
|
422
927
|
}
|
|
423
928
|
|
|
424
|
-
console.log(chalk.
|
|
929
|
+
console.log(chalk.yellow("未登录"));
|
|
425
930
|
return;
|
|
426
931
|
}
|
|
427
932
|
|
|
428
933
|
if (action === "logout") {
|
|
429
|
-
await
|
|
430
|
-
console.log(chalk.green("已退出登录"));
|
|
934
|
+
const response = await requestJson("POST", backendUrl, "/api/auth/logout", {});
|
|
935
|
+
console.log(chalk.green(response.message || "已退出登录"));
|
|
431
936
|
return;
|
|
432
937
|
}
|
|
433
938
|
|
|
434
939
|
if (action === "permissions") {
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
throw new Error(`未检测到有效登录状态。\n原因: ${loginStatus.reason}\n请先执行:qcplay-cli auth`);
|
|
439
|
-
}
|
|
940
|
+
const flags = parsePermissionsOptions(args.slice(1));
|
|
941
|
+
const response = await requestJson("GET", backendUrl, "/api/permissions");
|
|
942
|
+
let permissions = response.data || [];
|
|
440
943
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (options.key) {
|
|
445
|
-
nativeArgs.push("--key", options.key);
|
|
944
|
+
if (flags.key) {
|
|
945
|
+
permissions = permissions.filter(item => item.key === flags.key || item.key.startsWith(`${flags.key}.`));
|
|
446
946
|
}
|
|
447
947
|
|
|
448
|
-
if (
|
|
449
|
-
|
|
948
|
+
if (flags.json) {
|
|
949
|
+
console.log(JSON.stringify(permissions, null, 2));
|
|
950
|
+
return;
|
|
450
951
|
}
|
|
451
952
|
|
|
452
|
-
|
|
953
|
+
printPermissionTree(permissions);
|
|
453
954
|
return;
|
|
454
955
|
}
|
|
455
956
|
|
|
456
957
|
printAuthHelp();
|
|
457
958
|
}
|
|
458
959
|
|
|
459
|
-
function
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
960
|
+
function unquoteValue(input) {
|
|
961
|
+
const trimmed = String(input).trim();
|
|
962
|
+
if (
|
|
963
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
964
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
965
|
+
) {
|
|
966
|
+
return trimmed.slice(1, -1);
|
|
967
|
+
}
|
|
466
968
|
|
|
467
|
-
|
|
468
|
-
|
|
969
|
+
return trimmed;
|
|
970
|
+
}
|
|
469
971
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
972
|
+
function stripBom(input) {
|
|
973
|
+
return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function escapeHtml(value) {
|
|
977
|
+
return String(value)
|
|
978
|
+
.replaceAll("&", "&")
|
|
979
|
+
.replaceAll("<", "<")
|
|
980
|
+
.replaceAll(">", ">")
|
|
981
|
+
.replaceAll('"', """)
|
|
982
|
+
.replaceAll("'", "'");
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function applyInlineMarkdown(text) {
|
|
986
|
+
const tokens = [];
|
|
987
|
+
let output = String(text);
|
|
988
|
+
|
|
989
|
+
function storeToken(value) {
|
|
990
|
+
const token = `@@MDTOKEN${tokens.length}@@`;
|
|
991
|
+
tokens.push(value);
|
|
992
|
+
return token;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
output = output.replace(/`([^`]+)`/g, (_, code) => {
|
|
996
|
+
return storeToken(`<code>${escapeHtml(code)}</code>`);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
output = output.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) => {
|
|
1000
|
+
return storeToken(`<img src="${escapeHtml(url.trim())}" alt="${escapeHtml(alt.trim())}" />`);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
|
|
1004
|
+
return storeToken(`<a href="${escapeHtml(url.trim())}" target="_blank" rel="noreferrer">${escapeHtml(label.trim())}</a>`);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
output = escapeHtml(output);
|
|
1008
|
+
|
|
1009
|
+
output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
1010
|
+
output = output.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
1011
|
+
output = output.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>");
|
|
1012
|
+
output = output.replace(/(?<!_)_([^_]+)_(?!_)/g, "<em>$1</em>");
|
|
1013
|
+
|
|
1014
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1015
|
+
output = output.replace(`@@MDTOKEN${index}@@`, tokens[index]);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
return output;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function markdownToHtml(markdown) {
|
|
1022
|
+
const lines = stripBom(markdown).replace(/\r\n/g, "\n").split("\n");
|
|
1023
|
+
const html = [];
|
|
1024
|
+
let paragraph = [];
|
|
1025
|
+
let quote = [];
|
|
1026
|
+
let listType = "";
|
|
1027
|
+
let listItems = [];
|
|
1028
|
+
let inCodeBlock = false;
|
|
1029
|
+
let codeLines = [];
|
|
1030
|
+
|
|
1031
|
+
function flushParagraph() {
|
|
1032
|
+
if (paragraph.length === 0) {
|
|
1033
|
+
return;
|
|
473
1034
|
}
|
|
474
1035
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1036
|
+
html.push(`<p>${paragraph.map(line => applyInlineMarkdown(line)).join("<br />")}</p>`);
|
|
1037
|
+
paragraph = [];
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function flushQuote() {
|
|
1041
|
+
if (quote.length === 0) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
html.push(`<blockquote><p>${quote.map(line => applyInlineMarkdown(line)).join("<br />")}</p></blockquote>`);
|
|
1046
|
+
quote = [];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function flushList() {
|
|
1050
|
+
if (!listType || listItems.length === 0) {
|
|
1051
|
+
listType = "";
|
|
1052
|
+
listItems = [];
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
html.push(`<${listType}>${listItems.map(item => `<li>${applyInlineMarkdown(item)}</li>`).join("")}</${listType}>`);
|
|
1057
|
+
listType = "";
|
|
1058
|
+
listItems = [];
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function flushCodeBlock() {
|
|
1062
|
+
if (!inCodeBlock) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
html.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
|
1067
|
+
inCodeBlock = false;
|
|
1068
|
+
codeLines = [];
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function flushAll() {
|
|
1072
|
+
flushParagraph();
|
|
1073
|
+
flushQuote();
|
|
1074
|
+
flushList();
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
for (const line of lines) {
|
|
1078
|
+
const trimmed = line.trim();
|
|
480
1079
|
|
|
481
|
-
|
|
482
|
-
|
|
1080
|
+
if (trimmed.startsWith("```")) {
|
|
1081
|
+
if (inCodeBlock) {
|
|
1082
|
+
flushCodeBlock();
|
|
483
1083
|
} else {
|
|
484
|
-
|
|
1084
|
+
flushAll();
|
|
1085
|
+
inCodeBlock = true;
|
|
1086
|
+
codeLines = [];
|
|
485
1087
|
}
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
486
1090
|
|
|
487
|
-
|
|
1091
|
+
if (inCodeBlock) {
|
|
1092
|
+
codeLines.push(line);
|
|
488
1093
|
continue;
|
|
489
1094
|
}
|
|
490
1095
|
|
|
491
|
-
if (
|
|
492
|
-
|
|
1096
|
+
if (!trimmed) {
|
|
1097
|
+
flushAll();
|
|
1098
|
+
continue;
|
|
493
1099
|
}
|
|
494
1100
|
|
|
495
|
-
|
|
1101
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
1102
|
+
if (headingMatch) {
|
|
1103
|
+
flushAll();
|
|
1104
|
+
const level = headingMatch[1].length;
|
|
1105
|
+
html.push(`<h${level}>${applyInlineMarkdown(headingMatch[2].trim())}</h${level}>`);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
1110
|
+
flushAll();
|
|
1111
|
+
html.push("<hr />");
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const quoteMatch = line.match(/^\s*>\s?(.*)$/);
|
|
1116
|
+
if (quoteMatch) {
|
|
1117
|
+
flushParagraph();
|
|
1118
|
+
flushList();
|
|
1119
|
+
quote.push(quoteMatch[1]);
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const unorderedMatch = line.match(/^\s*[-*+]\s+(.+)$/);
|
|
1124
|
+
if (unorderedMatch) {
|
|
1125
|
+
flushParagraph();
|
|
1126
|
+
flushQuote();
|
|
1127
|
+
if (listType && listType !== "ul") {
|
|
1128
|
+
flushList();
|
|
1129
|
+
}
|
|
1130
|
+
listType = "ul";
|
|
1131
|
+
listItems.push(unorderedMatch[1].trim());
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const orderedMatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
1136
|
+
if (orderedMatch) {
|
|
1137
|
+
flushParagraph();
|
|
1138
|
+
flushQuote();
|
|
1139
|
+
if (listType && listType !== "ol") {
|
|
1140
|
+
flushList();
|
|
1141
|
+
}
|
|
1142
|
+
listType = "ol";
|
|
1143
|
+
listItems.push(orderedMatch[1].trim());
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
flushQuote();
|
|
1148
|
+
flushList();
|
|
1149
|
+
paragraph.push(trimmed);
|
|
496
1150
|
}
|
|
497
1151
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
};
|
|
1152
|
+
flushCodeBlock();
|
|
1153
|
+
flushAll();
|
|
1154
|
+
return html.join("\n");
|
|
502
1155
|
}
|
|
503
1156
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1157
|
+
function parseFrontMatter(raw) {
|
|
1158
|
+
const normalized = stripBom(raw).replace(/\r\n/g, "\n");
|
|
1159
|
+
const lines = normalized.split("\n");
|
|
1160
|
+
|
|
1161
|
+
if (lines[0]?.trim() !== "---") {
|
|
1162
|
+
throw new Error("文章文件缺少 Front Matter,首行必须是 ---");
|
|
507
1163
|
}
|
|
508
1164
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1165
|
+
let dividerIndex = -1;
|
|
1166
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
1167
|
+
if (lines[index].trim() === "---") {
|
|
1168
|
+
dividerIndex = index;
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
512
1171
|
}
|
|
513
1172
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
throw new Error(`未检测到有效登录状态。\n原因: ${loginStatus.reason}\n请先执行:qcplay-cli auth`);
|
|
1173
|
+
if (dividerIndex === -1) {
|
|
1174
|
+
throw new Error("文章文件缺少 Front Matter 结束标记 ---");
|
|
517
1175
|
}
|
|
518
1176
|
|
|
519
|
-
const
|
|
1177
|
+
const meta = {};
|
|
1178
|
+
for (const line of lines.slice(1, dividerIndex)) {
|
|
1179
|
+
if (!line.trim()) {
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const separatorIndex = line.indexOf(":");
|
|
1184
|
+
if (separatorIndex === -1) {
|
|
1185
|
+
throw new Error(`Front Matter 格式错误: ${line}`);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
meta[line.slice(0, separatorIndex).trim()] = unquoteValue(line.slice(separatorIndex + 1));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return {
|
|
1192
|
+
meta,
|
|
1193
|
+
content: lines.slice(dividerIndex + 1).join("\n").trim()
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function normalizeArticlePayload(meta, content) {
|
|
1198
|
+
const payload = {
|
|
1199
|
+
article_title: normalizeText(meta.article_title),
|
|
1200
|
+
thumbnail: normalizeText(meta.thumbnail),
|
|
1201
|
+
move_thumbnail: normalizeText(meta.move_thumbnail),
|
|
1202
|
+
article_content: markdownToHtml(content),
|
|
1203
|
+
article_excerpt: normalizeText(meta.article_excerpt),
|
|
1204
|
+
article_url: normalizeText(meta.article_url),
|
|
1205
|
+
origin: normalizeText(meta.origin),
|
|
1206
|
+
status: normalizeMappedValue(meta.status, STATUS_MAP) || "0",
|
|
1207
|
+
cate_id: normalizeMappedValue(meta.cate_id, CATEGORY_MAP),
|
|
1208
|
+
video_link: normalizeText(meta.video_link),
|
|
1209
|
+
is_hot: normalizeBoolLike(meta.is_hot, "0"),
|
|
1210
|
+
is_index: normalizeBoolLike(meta.is_index, "0"),
|
|
1211
|
+
release_time: normalizeText(meta.release_time),
|
|
1212
|
+
area: normalizeMappedValue(meta.area, AREA_MAP) || "1",
|
|
1213
|
+
sort: normalizeText(meta.sort) || "1",
|
|
1214
|
+
game_id: normalizeMappedValue(meta.game_id, GAME_MAP) || "39",
|
|
1215
|
+
is_index2: "0",
|
|
1216
|
+
index_pc_img: normalizeText(meta.index_pc_img),
|
|
1217
|
+
index_move_img: normalizeText(meta.index_move_img),
|
|
1218
|
+
type: "1"
|
|
1219
|
+
};
|
|
520
1220
|
|
|
521
|
-
if (
|
|
522
|
-
|
|
1221
|
+
if (!payload.article_title) {
|
|
1222
|
+
throw new Error("article_title 不能为空");
|
|
523
1223
|
}
|
|
524
1224
|
|
|
525
|
-
if (
|
|
526
|
-
|
|
1225
|
+
if (!payload.thumbnail) {
|
|
1226
|
+
throw new Error("thumbnail 不能为空");
|
|
527
1227
|
}
|
|
528
1228
|
|
|
529
|
-
if (
|
|
530
|
-
|
|
1229
|
+
if (!payload.article_content) {
|
|
1230
|
+
throw new Error("文章正文不能为空");
|
|
531
1231
|
}
|
|
532
1232
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1233
|
+
return payload;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async function parseArticleFile(file) {
|
|
1237
|
+
const articleFile = path.resolve(process.cwd(), file);
|
|
1238
|
+
if (!(await pathExists(articleFile))) {
|
|
1239
|
+
throw new Error(`文章文件不存在: ${articleFile}`);
|
|
1240
|
+
}
|
|
537
1241
|
|
|
538
|
-
await
|
|
1242
|
+
const raw = await fs.promises.readFile(articleFile, "utf8");
|
|
1243
|
+
const parsed = parseFrontMatter(raw);
|
|
1244
|
+
return {
|
|
1245
|
+
articleFile,
|
|
1246
|
+
payload: normalizeArticlePayload(parsed.meta, parsed.content)
|
|
1247
|
+
};
|
|
539
1248
|
}
|
|
540
1249
|
|
|
541
1250
|
async function initArticleTemplate(file = "article.md") {
|
|
@@ -579,15 +1288,15 @@ move_thumbnail: https://example.com/mobile-thumbnail.png
|
|
|
579
1288
|
article_excerpt: 文章描述,可以为空
|
|
580
1289
|
article_url: article-url-slug
|
|
581
1290
|
origin: QCPlay
|
|
582
|
-
status:
|
|
583
|
-
cate_id:
|
|
1291
|
+
status: 未发布
|
|
1292
|
+
cate_id: 综合
|
|
584
1293
|
video_link: ""
|
|
585
|
-
is_hot:
|
|
586
|
-
is_index:
|
|
587
|
-
release_time:
|
|
588
|
-
area:
|
|
589
|
-
sort:
|
|
590
|
-
game_id:
|
|
1294
|
+
is_hot: 非热门
|
|
1295
|
+
is_index: 不推荐
|
|
1296
|
+
release_time: 2026-07-16
|
|
1297
|
+
area: pc
|
|
1298
|
+
sort: 100
|
|
1299
|
+
game_id: 最强蜗牛
|
|
591
1300
|
index_pc_img: ""
|
|
592
1301
|
index_move_img: ""
|
|
593
1302
|
---
|
|
@@ -595,77 +1304,39 @@ index_move_img: ""
|
|
|
595
1304
|
# 文章标题
|
|
596
1305
|
|
|
597
1306
|
这里填写文章正文内容。
|
|
598
|
-
|
|
599
|
-
## 一、文章小标题
|
|
600
|
-
|
|
601
|
-
这里填写正文内容。
|
|
602
|
-
|
|
603
|
-
## 二、内容说明
|
|
604
|
-
|
|
605
|
-
这里继续填写正文。
|
|
606
|
-
|
|
607
|
-
## 三、总结
|
|
608
|
-
|
|
609
|
-
这里填写文章结尾内容。
|
|
610
1307
|
${chalk.gray("--------------------------------------------------")}
|
|
611
1308
|
|
|
612
1309
|
${chalk.cyan("发布命令:")}
|
|
613
1310
|
|
|
614
1311
|
qcplay-cli www-article-list.store article.md
|
|
1312
|
+
`);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function publishArticleCommand(file, options) {
|
|
1316
|
+
if (!file) {
|
|
1317
|
+
throw new Error("缺少文章文件,例如:qcplay-cli www-article-list.store article.md");
|
|
1318
|
+
}
|
|
615
1319
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
thumbnail 缩略图,必填
|
|
620
|
-
move_thumbnail 移动端缩略图,可为空
|
|
621
|
-
article_excerpt 文章描述,可为空
|
|
622
|
-
article_url 文章链接,建议英文 slug
|
|
623
|
-
origin 作者来源
|
|
624
|
-
status 1 发布,0 未发布
|
|
625
|
-
cate_id 分类 ID
|
|
626
|
-
video_link 视频链接,可为空
|
|
627
|
-
is_hot 1 热门,0 非热门
|
|
628
|
-
is_index 1 推荐,0 不推荐
|
|
629
|
-
release_time 发布时间,格式:年-月-日
|
|
630
|
-
area 所属区域
|
|
631
|
-
sort 排序,数字
|
|
632
|
-
game_id 游戏 ID,最强蜗牛是 39
|
|
633
|
-
index_pc_img 推荐 PC 端缩略图,可为空
|
|
634
|
-
index_move_img 推荐移动端缩略图,可为空
|
|
635
|
-
|
|
636
|
-
${chalk.cyan("分类 ID:")}
|
|
637
|
-
|
|
638
|
-
综合 2
|
|
639
|
-
视频中心 8
|
|
640
|
-
活动 3
|
|
641
|
-
游戏攻略 7
|
|
642
|
-
萌新入门 9
|
|
643
|
-
萌新入门-攻略专区 25
|
|
644
|
-
高手进阶 26
|
|
645
|
-
活动攻略 27
|
|
646
|
-
视频攻略 29
|
|
647
|
-
|
|
648
|
-
${chalk.cyan("区域 ID:")}
|
|
649
|
-
|
|
650
|
-
PC 1
|
|
651
|
-
资料站 3
|
|
652
|
-
公益网站 4
|
|
653
|
-
|
|
654
|
-
${chalk.cyan("游戏 ID:")}
|
|
655
|
-
|
|
656
|
-
最强蜗牛 39
|
|
657
|
-
|
|
658
|
-
${chalk.yellow("注意:")}
|
|
659
|
-
|
|
660
|
-
1. article_content 不需要写在 Front Matter 中。
|
|
661
|
-
2. Markdown 正文会自动作为 article_content。
|
|
662
|
-
3. is_index2 默认 0,不需要填写。
|
|
663
|
-
4. type 默认 1,不需要填写。
|
|
664
|
-
5. 发布前请先登录:
|
|
665
|
-
|
|
666
|
-
qcplay-cli auth
|
|
1320
|
+
const { articleFile, payload } = await parseArticleFile(file);
|
|
1321
|
+
const config = await loadConfig();
|
|
1322
|
+
const backendUrl = resolvePublishBackendUrl(options, config);
|
|
667
1323
|
|
|
668
|
-
|
|
1324
|
+
if (options.dryRun) {
|
|
1325
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
console.log("");
|
|
1330
|
+
console.log(chalk.cyan("正在发布官网文章..."));
|
|
1331
|
+
console.log(chalk.gray(`文章文件: ${articleFile}`));
|
|
1332
|
+
console.log("");
|
|
1333
|
+
|
|
1334
|
+
const response = await requestJson("POST", backendUrl, "/api/articles/publish", payload);
|
|
1335
|
+
console.log(chalk.green(response.message || "发布成功"));
|
|
1336
|
+
const articleId = response.data?.id ?? response.data?.article_id ?? response.data?.articleId;
|
|
1337
|
+
if (articleId !== undefined) {
|
|
1338
|
+
console.log(`文章 ID: ${articleId}`);
|
|
1339
|
+
}
|
|
669
1340
|
}
|
|
670
1341
|
|
|
671
1342
|
async function runWithErrorBanner(title, action) {
|
|
@@ -694,7 +1365,18 @@ async function main() {
|
|
|
694
1365
|
}
|
|
695
1366
|
|
|
696
1367
|
if (command === "install") {
|
|
697
|
-
|
|
1368
|
+
const parsed = parseBackendOptions([subcommand, ...rest].filter(Boolean));
|
|
1369
|
+
await runWithErrorBanner("安装失败", () => installCommand(parsed.options));
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (command === "update") {
|
|
1374
|
+
if (subcommand === "-h" || subcommand === "--help") {
|
|
1375
|
+
printUpdateHelp();
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
await runWithErrorBanner("更新失败", () => updateCommand());
|
|
698
1380
|
return;
|
|
699
1381
|
}
|
|
700
1382
|
|
|
@@ -704,7 +1386,7 @@ async function main() {
|
|
|
704
1386
|
return;
|
|
705
1387
|
}
|
|
706
1388
|
|
|
707
|
-
await runWithErrorBanner("认证失败", () => authCommand(subcommand, rest));
|
|
1389
|
+
await runWithErrorBanner("认证失败", () => authCommand([subcommand, ...rest].filter(Boolean)));
|
|
708
1390
|
return;
|
|
709
1391
|
}
|
|
710
1392
|
|
|
@@ -725,8 +1407,8 @@ async function main() {
|
|
|
725
1407
|
}
|
|
726
1408
|
|
|
727
1409
|
if (subcommand === "publish") {
|
|
728
|
-
const
|
|
729
|
-
await runWithErrorBanner("发布失败", () => publishArticleCommand(file, options));
|
|
1410
|
+
const parsed = parsePublishOptions(rest);
|
|
1411
|
+
await runWithErrorBanner("发布失败", () => publishArticleCommand(parsed.file, parsed.options));
|
|
730
1412
|
return;
|
|
731
1413
|
}
|
|
732
1414
|
|