@qcplay/cli 1.0.3 → 1.0.5

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,14 +1,97 @@
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 { spawn } from "child_process";
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");
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;
29
+ }
30
+
31
+ return path.resolve(__dirname, "../web/auth.html");
32
+ }
33
+
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
+ };
90
+
91
+ const QCPLAY_DIR = path.join(os.homedir(), ".qcplay");
92
+ const AGENTS_DIR = path.join(os.homedir(), ".agents");
93
+ const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
94
+ const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
12
95
 
13
96
  function readPackageVersion() {
14
97
  try {
@@ -20,13 +103,6 @@ function readPackageVersion() {
20
103
  }
21
104
 
22
105
  const PACKAGE_VERSION = readPackageVersion();
23
-
24
- const QCPLAY_DIR = path.join(os.homedir(), ".qcplay");
25
- const AGENTS_DIR = path.join(os.homedir(), ".agents");
26
- const AUTH_FILE = path.join(QCPLAY_DIR, "auth.json");
27
- const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
28
- const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
29
-
30
106
  const colorsEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
31
107
 
32
108
  const chalk = {
@@ -49,13 +125,13 @@ function printRootHelp() {
49
125
  console.log(`QCPlay CLI
50
126
 
51
127
  Usage:
52
- qcplay-cli install
53
- qcplay-cli auth [login|status|logout]
54
- qcplay-cli auth permissions [--key <key>] [--json]
128
+ qcplay-cli install [--local]
129
+ qcplay-cli auth [login|status|logout] [--local] [--backend <url>] [--auth-page <url>]
130
+ qcplay-cli auth permissions [--local] [--key <key>] [--json]
55
131
  qcplay-cli article
56
132
  qcplay-cli article init [file]
57
- qcplay-cli article publish <file> [--env <env>] [--url <url>] [--dry-run]
58
- qcplay-cli www-article-list.store <file> [--env <env>] [--url <url>] [--dry-run]
133
+ qcplay-cli article publish <file> [--local] [--backend <url>] [--dry-run]
134
+ qcplay-cli www-article-list.store <file> [--local] [--backend <url>] [--dry-run]
59
135
  qcplay-cli features
60
136
  qcplay-cli where
61
137
 
@@ -67,107 +143,48 @@ Options:
67
143
 
68
144
  function printAuthHelp() {
69
145
  console.log("Usage:");
70
- console.log(" qcplay-cli auth");
71
- console.log(" qcplay-cli auth status");
72
- console.log(" qcplay-cli auth logout");
73
- console.log(" qcplay-cli auth permissions [--key <key>] [--json]");
146
+ console.log(" qcplay-cli auth [login|status|logout] [--local] [--backend <url>] [--auth-page <url>]");
147
+ console.log(" qcplay-cli auth permissions [--local] [--backend <url>] [--key <key>] [--json]");
74
148
  }
75
149
 
76
150
  function printArticleHelp() {
77
151
  console.log(`Usage:
78
152
  qcplay-cli article
79
153
  qcplay-cli article init [file]
80
- qcplay-cli article publish <file> [--env <env>] [--url <url>] [--dry-run]`);
154
+ qcplay-cli article publish <file> [--local] [--backend <url>] [--dry-run]`);
81
155
  }
82
156
 
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}`);
97
- }
98
-
99
- function getExeCandidates(name) {
100
- if (process.platform === "win32") {
101
- return [`${name}.exe`, name];
102
- }
103
-
104
- if (process.platform === "darwin") {
105
- return [`${name}-darwin-${process.arch}`, name];
106
- }
107
-
108
- if (process.platform === "linux") {
109
- return [`${name}-linux-${process.arch}`, name];
110
- }
111
-
112
- return [name];
157
+ function printFeatures() {
158
+ console.log("");
159
+ console.log(chalk.green("你现在可以使用以下功能:"));
160
+ console.log("");
161
+ console.log(chalk.cyan("1. 登录认证"));
162
+ console.log(" qcplay-cli auth");
163
+ console.log(chalk.gray(" 会唤起网页登录页,不再在控制台输入密码。"));
164
+ console.log("");
165
+ console.log(chalk.cyan("2. 查看权限"));
166
+ console.log(" qcplay-cli auth permissions");
167
+ console.log(" qcplay-cli auth permissions --key www-article-list.store");
168
+ console.log("");
169
+ console.log(chalk.cyan("3. 发布官网文章"));
170
+ console.log(" qcplay-cli www-article-list.store article.md");
171
+ console.log("");
172
+ console.log(chalk.cyan("4. 查看本地目录"));
173
+ console.log(" qcplay-cli where");
174
+ console.log("");
113
175
  }
114
176
 
115
- function getExePath(name) {
116
- const vendorDir = getVendorDir();
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]);
177
+ function printWhere() {
178
+ console.log(QCPLAY_DIR);
127
179
  }
128
180
 
129
- function runNative(name, args = []) {
130
- return new Promise((resolve, reject) => {
131
- const exePath = getExePath(name);
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
- });
181
+ function createReadline() {
182
+ return readline.createInterface({
183
+ input: process.stdin,
184
+ output: process.stdout
154
185
  });
155
186
  }
156
187
 
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
188
  async function pathExists(targetPath) {
172
189
  try {
173
190
  await fs.promises.access(targetPath);
@@ -199,16 +216,19 @@ async function copyRecursive(sourcePath, targetPath) {
199
216
 
200
217
  async function writeJson(filePath, value) {
201
218
  await ensureDir(path.dirname(filePath));
202
- await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
219
+ await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, {
220
+ encoding: "utf8",
221
+ mode: 0o600
222
+ });
203
223
  }
204
224
 
205
225
  async function readJson(filePath) {
206
- const raw = await fs.promises.readFile(filePath, "utf8");
207
- return JSON.parse(raw);
208
- }
209
-
210
- async function removeFile(filePath) {
211
- await fs.promises.rm(filePath, { force: true });
226
+ try {
227
+ const raw = await fs.promises.readFile(filePath, "utf8");
228
+ return JSON.parse(raw);
229
+ } catch {
230
+ return {};
231
+ }
212
232
  }
213
233
 
214
234
  async function ensureLocalDirs() {
@@ -227,102 +247,90 @@ async function installSkills() {
227
247
  return true;
228
248
  }
229
249
 
230
- async function saveConfig() {
250
+ async function saveConfig(authBackendUrl, publishBackendUrl, authPageUrl, localMode = false) {
231
251
  await writeJson(CONFIG_FILE, {
232
252
  cli: "qcplay-cli",
233
253
  version: PACKAGE_VERSION,
234
- auth_file: AUTH_FILE,
254
+ auth_backend_url: authBackendUrl,
255
+ publish_backend_url: publishBackendUrl,
256
+ auth_page_url: authPageUrl || "",
257
+ local_mode: localMode,
235
258
  skills_dir: SKILLS_DIR,
236
259
  configured_at: Date.now()
237
260
  });
238
261
  }
239
262
 
240
- async function getLoginStatus() {
241
- if (!(await pathExists(AUTH_FILE))) {
242
- return {
243
- ok: false,
244
- reason: `认证文件不存在: ${AUTH_FILE}`
245
- };
246
- }
263
+ async function loadConfig() {
264
+ return readJson(CONFIG_FILE);
265
+ }
247
266
 
248
- let auth;
267
+ function normalizeText(value) {
268
+ return String(value ?? "").trim();
269
+ }
249
270
 
250
- try {
251
- auth = await readJson(AUTH_FILE);
252
- } catch (err) {
253
- return {
254
- ok: false,
255
- reason: `认证文件不是合法 JSON: ${err.message}`
256
- };
271
+ function normalizeMappedValue(value, map) {
272
+ const text = normalizeText(value);
273
+ if (!text) {
274
+ return "";
257
275
  }
258
276
 
259
- const accessToken =
260
- auth.access_token ||
261
- auth.accessToken ||
262
- auth.token ||
263
- auth.data?.access_token ||
264
- auth.data?.accessToken;
277
+ return map[text] ?? map[text.toLowerCase?.() || ""] ?? text;
278
+ }
265
279
 
266
- if (!accessToken) {
267
- return {
268
- ok: false,
269
- reason: "认证文件中没有找到 access_token"
270
- };
280
+ function normalizeBoolLike(value, defaultValue = "0") {
281
+ const text = normalizeText(value);
282
+ if (!text) {
283
+ return defaultValue;
271
284
  }
272
285
 
273
- let expiresAt =
274
- auth.expires_at ||
275
- auth.expiresAt ||
276
- auth.data?.expires_at ||
277
- auth.data?.expiresAt;
278
-
279
- if (expiresAt) {
280
- expiresAt = Number(expiresAt);
286
+ return BOOLEAN_TEXT_MAP[text] ?? BOOLEAN_TEXT_MAP[text.toLowerCase()] ?? text;
287
+ }
281
288
 
282
- if (expiresAt > 0 && expiresAt < 1000000000000) {
283
- expiresAt = expiresAt * 1000;
284
- }
289
+ function parseBackendOptions(args) {
290
+ const options = {
291
+ backend: undefined,
292
+ authPage: undefined,
293
+ local: false
294
+ };
295
+ const passthrough = [];
285
296
 
286
- if (Date.now() >= expiresAt) {
287
- return {
288
- ok: false,
289
- reason: `登录状态已过期: ${new Date(expiresAt).toLocaleString()}`
290
- };
291
- }
292
- }
297
+ for (let index = 0; index < args.length; index += 1) {
298
+ const current = args[index];
293
299
 
294
- return {
295
- ok: true,
296
- auth
297
- };
298
- }
300
+ if (current === "--backend") {
301
+ const next = args[index + 1];
302
+ if (!next || next.startsWith("--")) {
303
+ throw new Error("--backend 缺少参数值");
304
+ }
299
305
 
300
- function printFeatures() {
301
- console.log("");
302
- console.log(chalk.green("你现在可以使用以下功能:"));
303
- console.log("");
306
+ options.backend = next;
307
+ index += 1;
308
+ continue;
309
+ }
304
310
 
305
- console.log(chalk.cyan("1. 登录认证"));
306
- console.log(" qcplay-cli auth");
307
- console.log("");
311
+ if (current === "--auth-page") {
312
+ const next = args[index + 1];
313
+ if (!next || next.startsWith("--")) {
314
+ throw new Error("--auth-page 缺少参数值");
315
+ }
308
316
 
309
- console.log(chalk.cyan("2. 查看权限"));
310
- console.log(" qcplay-cli auth permissions");
311
- console.log(" qcplay-cli auth permissions --key www-article-list.store");
312
- console.log("");
317
+ options.authPage = next;
318
+ index += 1;
319
+ continue;
320
+ }
313
321
 
314
- console.log(chalk.cyan("4. 发布官网文章"));
315
- console.log(" qcplay-cli www-article-list.store article.md");
316
- console.log(chalk.gray(" 文章标题、分类、缩略图、标签等信息写在 article.md 顶部 Front Matter 中。"));
317
- console.log("");
322
+ if (current === "--local") {
323
+ options.local = true;
324
+ continue;
325
+ }
318
326
 
319
- console.log(chalk.cyan("5. 查看本地配置目录"));
320
- console.log(" qcplay-cli where");
321
- console.log("");
322
- }
327
+ passthrough.push(current);
328
+ }
323
329
 
324
- function printWhere() {
325
- console.log(QCPLAY_DIR);
330
+ return {
331
+ options,
332
+ args: passthrough
333
+ };
326
334
  }
327
335
 
328
336
  function parsePermissionsOptions(args) {
@@ -356,7 +364,324 @@ function parsePermissionsOptions(args) {
356
364
  return options;
357
365
  }
358
366
 
359
- async function installCommand() {
367
+ function parsePublishOptions(args) {
368
+ const options = {
369
+ backend: undefined,
370
+ local: false,
371
+ dryRun: false
372
+ };
373
+ const positional = [];
374
+
375
+ for (let index = 0; index < args.length; index += 1) {
376
+ const current = args[index];
377
+
378
+ if (current === "--dry-run") {
379
+ options.dryRun = true;
380
+ continue;
381
+ }
382
+
383
+ if (current === "--backend") {
384
+ const next = args[index + 1];
385
+ if (!next || next.startsWith("--")) {
386
+ throw new Error("--backend 缺少参数值");
387
+ }
388
+
389
+ options.backend = next;
390
+ index += 1;
391
+ continue;
392
+ }
393
+
394
+ if (current === "--local") {
395
+ options.local = true;
396
+ continue;
397
+ }
398
+
399
+ if (current.startsWith("--")) {
400
+ throw new Error(`未知参数: ${current}`);
401
+ }
402
+
403
+ positional.push(current);
404
+ }
405
+
406
+ return {
407
+ file: positional[0],
408
+ options
409
+ };
410
+ }
411
+
412
+ function requestJson(method, baseUrl, pathname, payload) {
413
+ return new Promise((resolve, reject) => {
414
+ const url = new URL(pathname, baseUrl);
415
+ const client = url.protocol === "https:" ? https : http;
416
+ const body = payload === undefined ? undefined : JSON.stringify(payload);
417
+
418
+ const req = client.request(
419
+ url,
420
+ {
421
+ method,
422
+ headers: {
423
+ Accept: "application/json",
424
+ "Content-Type": "application/json",
425
+ "User-Agent": `qcplay-cli/${PACKAGE_VERSION}`,
426
+ ...(body ? { "Content-Length": Buffer.byteLength(body) } : {})
427
+ }
428
+ },
429
+ res => {
430
+ const chunks = [];
431
+ res.on("data", chunk => chunks.push(chunk));
432
+ res.on("end", () => {
433
+ const text = Buffer.concat(chunks).toString("utf8");
434
+ let json = {};
435
+
436
+ try {
437
+ json = text ? JSON.parse(text) : {};
438
+ } catch {
439
+ reject(new Error(`后端返回了非 JSON 内容: ${text.slice(0, 160)}`));
440
+ return;
441
+ }
442
+
443
+ if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) {
444
+ reject(new Error(json.message || `接口请求失败 (${res.statusCode})`));
445
+ return;
446
+ }
447
+
448
+ resolve(json);
449
+ });
450
+ }
451
+ );
452
+
453
+ req.on("error", err => {
454
+ reject(new Error(`无法连接后端服务 ${baseUrl},请检查服务地址或网络连接。原始错误: ${err.message}`));
455
+ });
456
+
457
+ if (body) {
458
+ req.write(body);
459
+ }
460
+
461
+ req.end();
462
+ });
463
+ }
464
+
465
+ function openBrowser(targetUrl) {
466
+ return new Promise((resolve, reject) => {
467
+ let command;
468
+ let args;
469
+
470
+ if (process.platform === "win32") {
471
+ command = "cmd";
472
+ args = ["/c", "start", "", targetUrl];
473
+ } else if (process.platform === "darwin") {
474
+ command = "open";
475
+ args = [targetUrl];
476
+ } else {
477
+ command = "xdg-open";
478
+ args = [targetUrl];
479
+ }
480
+
481
+ const child = spawn(command, args, {
482
+ detached: true,
483
+ stdio: "ignore"
484
+ });
485
+
486
+ child.on("error", reject);
487
+ child.unref();
488
+ resolve();
489
+ });
490
+ }
491
+
492
+ function delay(ms) {
493
+ return new Promise(resolve => {
494
+ setTimeout(resolve, ms);
495
+ });
496
+ }
497
+
498
+ function normalizeOptionalUrl(value) {
499
+ const text = normalizeText(value);
500
+ return text || "";
501
+ }
502
+
503
+ function ensureAbsoluteUrl(value, flagName) {
504
+ try {
505
+ return new URL(value).toString();
506
+ } catch {
507
+ throw new Error(`${flagName} 必须是完整 URL,例如 https://example.com/auth.html`);
508
+ }
509
+ }
510
+
511
+ function resolveLocalBaseUrl() {
512
+ const localBaseUrl = normalizeOptionalUrl(ENV_LOCAL_BASE_URL) || DEFAULT_LOCAL_BASE_URL;
513
+ return ensureAbsoluteUrl(localBaseUrl, "QCPLAY_LOCAL_BASE_URL");
514
+ }
515
+
516
+ function isLocalMode(options, config = {}) {
517
+ return Boolean(options.local || config.local_mode);
518
+ }
519
+
520
+ function resolveAuthBackendUrl(options, config = {}) {
521
+ if (!normalizeOptionalUrl(options.backend) && isLocalMode(options, config)) {
522
+ return resolveLocalBaseUrl();
523
+ }
524
+
525
+ const backendUrl =
526
+ normalizeOptionalUrl(options.backend) ||
527
+ normalizeOptionalUrl(ENV_AUTH_BACKEND_URL) ||
528
+ normalizeOptionalUrl(config.auth_backend_url) ||
529
+ normalizeOptionalUrl(config.backend_url) ||
530
+ DEFAULT_AUTH_BACKEND_URL;
531
+ return ensureAbsoluteUrl(backendUrl, "--backend");
532
+ }
533
+
534
+ function resolveAuthPageUrl(options, config = {}) {
535
+ if (!normalizeOptionalUrl(options.authPage) && isLocalMode(options, config)) {
536
+ return "";
537
+ }
538
+
539
+ const authPageUrl =
540
+ normalizeOptionalUrl(options.authPage) ||
541
+ normalizeOptionalUrl(ENV_AUTH_PAGE_URL) ||
542
+ normalizeOptionalUrl(config.auth_page_url) ||
543
+ DEFAULT_AUTH_PAGE_URL;
544
+ return authPageUrl ? ensureAbsoluteUrl(authPageUrl, "--auth-page") : "";
545
+ }
546
+
547
+ function resolvePublishBackendUrl(options, config = {}) {
548
+ if (!normalizeOptionalUrl(options.backend) && isLocalMode(options, config)) {
549
+ return resolveLocalBaseUrl();
550
+ }
551
+
552
+ const backendUrl =
553
+ normalizeOptionalUrl(options.backend) ||
554
+ normalizeOptionalUrl(ENV_PUBLISH_BACKEND_URL) ||
555
+ normalizeOptionalUrl(config.publish_backend_url) ||
556
+ DEFAULT_PUBLISH_BACKEND_URL;
557
+ return ensureAbsoluteUrl(backendUrl, "--backend");
558
+ }
559
+
560
+ async function fetchLoginSuccessSequence(baseUrl) {
561
+ const response = await requestJson("GET", baseUrl, "/api/auth/login-success");
562
+ return Number(response.data?.sequence || 0);
563
+ }
564
+
565
+ function isNonJsonResponseError(error) {
566
+ return String(error?.message || "").includes("后端返回了非 JSON 内容");
567
+ }
568
+
569
+ function buildAuthBackendError(baseUrl, error) {
570
+ if (isNonJsonResponseError(error)) {
571
+ return new Error(
572
+ `认证接口 ${baseUrl} 返回了 HTML 页面,不是 JSON。请确认 ${baseUrl}/api/auth/login、${baseUrl}/api/auth/login-success、${baseUrl}/api/auth/status 已正确部署,而不是被站点首页或登录页接管。`
573
+ );
574
+ }
575
+
576
+ return error;
577
+ }
578
+
579
+ async function waitForLoginSuccessSequence(baseUrl, previousSequence, intervalMs = 1000, timeoutMs = 60000) {
580
+ const startedAt = Date.now();
581
+ let lastError;
582
+
583
+ while (true) {
584
+ try {
585
+ const nextSequence = await fetchLoginSuccessSequence(baseUrl);
586
+ if (nextSequence > previousSequence) {
587
+ return nextSequence;
588
+ }
589
+ } catch (error) {
590
+ lastError = error;
591
+ }
592
+
593
+ if (Date.now() - startedAt >= timeoutMs) {
594
+ if (lastError) {
595
+ throw buildAuthBackendError(baseUrl, lastError);
596
+ }
597
+
598
+ throw new Error(`等待登录结果超时,请确认认证接口 ${baseUrl} 可用`);
599
+ }
600
+
601
+ await delay(intervalMs);
602
+ }
603
+ }
604
+
605
+ function buildAuthPageUrl(backendUrl, authPageUrl = "") {
606
+ const targetUrl = authPageUrl ? new URL(authPageUrl) : pathToFileURL(AUTH_PAGE);
607
+
608
+ if (shouldAttachBackendQuery(targetUrl, backendUrl)) {
609
+ targetUrl.searchParams.set("backend", backendUrl);
610
+ }
611
+
612
+ return targetUrl.toString();
613
+ }
614
+
615
+ function shouldAttachBackendQuery(targetUrl, backendUrl) {
616
+ if (!backendUrl) {
617
+ return false;
618
+ }
619
+
620
+ const normalizedBackendUrl = new URL(backendUrl);
621
+ if (targetUrl.protocol === "file:") {
622
+ return normalizedBackendUrl.origin !== new URL(DEFAULT_LOCAL_BASE_URL).origin;
623
+ }
624
+
625
+ return normalizedBackendUrl.origin !== targetUrl.origin;
626
+ }
627
+
628
+ function flattenPermissionTree(items, bucket = []) {
629
+ for (const item of items) {
630
+ bucket.push(item);
631
+ if (Array.isArray(item.children) && item.children.length > 0) {
632
+ flattenPermissionTree(item.children, bucket);
633
+ }
634
+ }
635
+
636
+ return bucket;
637
+ }
638
+
639
+ function printPermissionTree(items) {
640
+ const flatItems = flattenPermissionTree(items);
641
+ const indexed = new Map();
642
+ const children = new Map();
643
+ const roots = [];
644
+
645
+ for (const item of flatItems) {
646
+ const id = Number(item.id ?? 0);
647
+ indexed.set(id || item.key, item);
648
+ }
649
+
650
+ for (const item of flatItems) {
651
+ const parentId = Number(item.menu_id ?? item.parent_id ?? 0);
652
+ if (parentId && indexed.has(parentId)) {
653
+ if (!children.has(parentId)) {
654
+ children.set(parentId, []);
655
+ }
656
+
657
+ children.get(parentId).push(item);
658
+ continue;
659
+ }
660
+
661
+ roots.push(item);
662
+ }
663
+
664
+ if (roots.length === 0) {
665
+ console.log(chalk.yellow("未返回任何权限"));
666
+ return;
667
+ }
668
+
669
+ for (const item of roots) {
670
+ console.log(item.key);
671
+ const nested = children.get(Number(item.id ?? 0)) || [];
672
+ for (const child of nested) {
673
+ console.log(` - ${child.key}`);
674
+ }
675
+ }
676
+ }
677
+
678
+ async function installCommand(options) {
679
+ const config = await loadConfig();
680
+ const localMode = isLocalMode(options, config);
681
+ const authBackendUrl = resolveAuthBackendUrl(options, config);
682
+ const publishBackendUrl = resolvePublishBackendUrl(options, config);
683
+ const authPageUrl = resolveAuthPageUrl(options, config);
684
+
360
685
  console.log("");
361
686
  console.log(chalk.cyan("正在安装 QCPlay CLI..."));
362
687
  console.log("");
@@ -372,170 +697,373 @@ async function installCommand() {
372
697
  console.log(chalk.yellow("! 未找到 Skills 模板,已跳过"));
373
698
  }
374
699
 
375
- await saveConfig();
700
+ await saveConfig(authBackendUrl, publishBackendUrl, authPageUrl, localMode);
376
701
  console.log(chalk.green("✔ 配置文件已生成"));
377
-
378
- console.log("");
379
- console.log(chalk.cyan("正在打开登录窗口..."));
380
- console.log(chalk.gray("请在浏览器中完成账号密码登录。"));
702
+ console.log(chalk.gray(`认证后端: ${authBackendUrl}`));
703
+ console.log(chalk.gray(`发布后端: ${publishBackendUrl}`));
704
+ console.log(chalk.gray(`登录页地址: ${authPageUrl || "本地内置页面"}`));
381
705
  console.log("");
382
-
383
- await runAuthLogin();
384
-
385
- const loginStatus = await getLoginStatus();
386
- if (!loginStatus.ok) {
387
- throw new Error(`登录流程结束,但未检测到有效认证文件。\n原因: ${loginStatus.reason}`);
388
- }
389
-
706
+ console.log("安装完成后可直接执行:");
707
+ console.log(" qcplay-cli auth");
390
708
  console.log("");
391
- console.log(chalk.green("✔ QCPlay CLI 安装完成"));
392
-
393
- printFeatures();
394
709
  }
395
710
 
396
- async function authCommand(action, args = []) {
397
- if (!action || action === "login") {
398
- console.log("");
399
- console.log(chalk.cyan("正在打开 QCPlay 登录窗口..."));
400
- console.log("");
401
-
402
- await runAuthLogin();
711
+ async function authCommand(rawArgs = []) {
712
+ const parsed = parseBackendOptions(rawArgs);
713
+ const options = parsed.options;
714
+ const args = parsed.args;
715
+ const action = args[0];
716
+ const config = await loadConfig();
717
+ const backendUrl = resolveAuthBackendUrl(options, config);
718
+ const authPageUrl = resolveAuthPageUrl(options, config);
403
719
 
404
- const loginStatus = await getLoginStatus();
405
- if (!loginStatus.ok) {
406
- throw new Error(`登录失败。\n原因: ${loginStatus.reason}`);
720
+ if (!action || action === "login") {
721
+ const loginPageUrl = buildAuthPageUrl(backendUrl, authPageUrl);
722
+ let previousSequence = 0;
723
+
724
+ console.log(chalk.cyan("正在登录中"));
725
+ try {
726
+ await openBrowser(loginPageUrl);
727
+ } catch {
728
+ console.log(chalk.yellow("自动打开浏览器失败,请手动打开下面地址:"));
729
+ console.log(chalk.gray(loginPageUrl));
407
730
  }
408
731
 
732
+ try {
733
+ previousSequence = await fetchLoginSuccessSequence(backendUrl);
734
+ } catch {}
735
+
736
+ await waitForLoginSuccessSequence(backendUrl, previousSequence);
737
+ console.log(chalk.green("登录成功"));
409
738
  return;
410
739
  }
411
740
 
412
741
  if (action === "status") {
413
- const loginStatus = await getLoginStatus();
414
-
415
- if (!loginStatus.ok) {
416
- console.log(chalk.yellow("未登录或登录无效"));
417
- console.log(`原因: ${loginStatus.reason}`);
418
- console.log("");
419
- console.log("请执行:");
420
- console.log(" qcplay-cli auth");
742
+ const response = await requestJson("GET", backendUrl, "/api/auth/status");
743
+ if (response.data?.logged_in) {
744
+ console.log(chalk.green("已登录"));
421
745
  return;
422
746
  }
423
747
 
424
- console.log(chalk.green("已登录"));
748
+ console.log(chalk.yellow("未登录"));
425
749
  return;
426
750
  }
427
751
 
428
752
  if (action === "logout") {
429
- await removeFile(AUTH_FILE);
430
- console.log(chalk.green("已退出登录"));
753
+ const response = await requestJson("POST", backendUrl, "/api/auth/logout", {});
754
+ console.log(chalk.green(response.message || "已退出登录"));
431
755
  return;
432
756
  }
433
757
 
434
758
  if (action === "permissions") {
435
- const loginStatus = await getLoginStatus();
759
+ const flags = parsePermissionsOptions(args.slice(1));
760
+ const response = await requestJson("GET", backendUrl, "/api/permissions");
761
+ let permissions = response.data || [];
436
762
 
437
- if (!loginStatus.ok) {
438
- throw new Error(`未检测到有效登录状态。\n原因: ${loginStatus.reason}\n请先执行:qcplay-cli auth`);
763
+ if (flags.key) {
764
+ permissions = permissions.filter(item => item.key === flags.key || item.key.startsWith(`${flags.key}.`));
439
765
  }
440
766
 
441
- const options = parsePermissionsOptions(args);
442
- const nativeArgs = ["permissions"];
443
-
444
- if (options.key) {
445
- nativeArgs.push("--key", options.key);
446
- }
447
-
448
- if (options.json) {
449
- nativeArgs.push("--json");
767
+ if (flags.json) {
768
+ console.log(JSON.stringify(permissions, null, 2));
769
+ return;
450
770
  }
451
771
 
452
- await runNative("qcplay-auth", nativeArgs);
772
+ printPermissionTree(permissions);
453
773
  return;
454
774
  }
455
775
 
456
776
  printAuthHelp();
457
777
  }
458
778
 
459
- function parsePublishOptions(args) {
460
- const options = {
461
- env: undefined,
462
- url: undefined,
463
- dryRun: false
464
- };
465
- const positional = [];
779
+ function unquoteValue(input) {
780
+ const trimmed = String(input).trim();
781
+ if (
782
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
783
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
784
+ ) {
785
+ return trimmed.slice(1, -1);
786
+ }
466
787
 
467
- for (let index = 0; index < args.length; index += 1) {
468
- const current = args[index];
788
+ return trimmed;
789
+ }
469
790
 
470
- if (current === "--dry-run") {
471
- options.dryRun = true;
472
- continue;
791
+ function stripBom(input) {
792
+ return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input;
793
+ }
794
+
795
+ function escapeHtml(value) {
796
+ return String(value)
797
+ .replaceAll("&", "&amp;")
798
+ .replaceAll("<", "&lt;")
799
+ .replaceAll(">", "&gt;")
800
+ .replaceAll('"', "&quot;")
801
+ .replaceAll("'", "&#39;");
802
+ }
803
+
804
+ function applyInlineMarkdown(text) {
805
+ const tokens = [];
806
+ let output = String(text);
807
+
808
+ function storeToken(value) {
809
+ const token = `@@MDTOKEN${tokens.length}@@`;
810
+ tokens.push(value);
811
+ return token;
812
+ }
813
+
814
+ output = output.replace(/`([^`]+)`/g, (_, code) => {
815
+ return storeToken(`<code>${escapeHtml(code)}</code>`);
816
+ });
817
+
818
+ output = output.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) => {
819
+ return storeToken(`<img src="${escapeHtml(url.trim())}" alt="${escapeHtml(alt.trim())}" />`);
820
+ });
821
+
822
+ output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
823
+ return storeToken(`<a href="${escapeHtml(url.trim())}" target="_blank" rel="noreferrer">${escapeHtml(label.trim())}</a>`);
824
+ });
825
+
826
+ output = escapeHtml(output);
827
+
828
+ output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
829
+ output = output.replace(/__([^_]+)__/g, "<strong>$1</strong>");
830
+ output = output.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>");
831
+ output = output.replace(/(?<!_)_([^_]+)_(?!_)/g, "<em>$1</em>");
832
+
833
+ for (let index = 0; index < tokens.length; index += 1) {
834
+ output = output.replace(`@@MDTOKEN${index}@@`, tokens[index]);
835
+ }
836
+
837
+ return output;
838
+ }
839
+
840
+ function markdownToHtml(markdown) {
841
+ const lines = stripBom(markdown).replace(/\r\n/g, "\n").split("\n");
842
+ const html = [];
843
+ let paragraph = [];
844
+ let quote = [];
845
+ let listType = "";
846
+ let listItems = [];
847
+ let inCodeBlock = false;
848
+ let codeLines = [];
849
+
850
+ function flushParagraph() {
851
+ if (paragraph.length === 0) {
852
+ return;
473
853
  }
474
854
 
475
- if (current === "--env" || current === "--url") {
476
- const next = args[index + 1];
477
- if (!next || next.startsWith("--")) {
478
- throw new Error(`${current} 缺少参数值`);
479
- }
855
+ html.push(`<p>${paragraph.map(line => applyInlineMarkdown(line)).join("<br />")}</p>`);
856
+ paragraph = [];
857
+ }
858
+
859
+ function flushQuote() {
860
+ if (quote.length === 0) {
861
+ return;
862
+ }
863
+
864
+ html.push(`<blockquote><p>${quote.map(line => applyInlineMarkdown(line)).join("<br />")}</p></blockquote>`);
865
+ quote = [];
866
+ }
480
867
 
481
- if (current === "--env") {
482
- options.env = next;
868
+ function flushList() {
869
+ if (!listType || listItems.length === 0) {
870
+ listType = "";
871
+ listItems = [];
872
+ return;
873
+ }
874
+
875
+ html.push(`<${listType}>${listItems.map(item => `<li>${applyInlineMarkdown(item)}</li>`).join("")}</${listType}>`);
876
+ listType = "";
877
+ listItems = [];
878
+ }
879
+
880
+ function flushCodeBlock() {
881
+ if (!inCodeBlock) {
882
+ return;
883
+ }
884
+
885
+ html.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
886
+ inCodeBlock = false;
887
+ codeLines = [];
888
+ }
889
+
890
+ function flushAll() {
891
+ flushParagraph();
892
+ flushQuote();
893
+ flushList();
894
+ }
895
+
896
+ for (const line of lines) {
897
+ const trimmed = line.trim();
898
+
899
+ if (trimmed.startsWith("```")) {
900
+ if (inCodeBlock) {
901
+ flushCodeBlock();
483
902
  } else {
484
- options.url = next;
903
+ flushAll();
904
+ inCodeBlock = true;
905
+ codeLines = [];
485
906
  }
907
+ continue;
908
+ }
486
909
 
487
- index += 1;
910
+ if (inCodeBlock) {
911
+ codeLines.push(line);
488
912
  continue;
489
913
  }
490
914
 
491
- if (current.startsWith("--")) {
492
- throw new Error(`未知参数: ${current}`);
915
+ if (!trimmed) {
916
+ flushAll();
917
+ continue;
493
918
  }
494
919
 
495
- positional.push(current);
920
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
921
+ if (headingMatch) {
922
+ flushAll();
923
+ const level = headingMatch[1].length;
924
+ html.push(`<h${level}>${applyInlineMarkdown(headingMatch[2].trim())}</h${level}>`);
925
+ continue;
926
+ }
927
+
928
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
929
+ flushAll();
930
+ html.push("<hr />");
931
+ continue;
932
+ }
933
+
934
+ const quoteMatch = line.match(/^\s*>\s?(.*)$/);
935
+ if (quoteMatch) {
936
+ flushParagraph();
937
+ flushList();
938
+ quote.push(quoteMatch[1]);
939
+ continue;
940
+ }
941
+
942
+ const unorderedMatch = line.match(/^\s*[-*+]\s+(.+)$/);
943
+ if (unorderedMatch) {
944
+ flushParagraph();
945
+ flushQuote();
946
+ if (listType && listType !== "ul") {
947
+ flushList();
948
+ }
949
+ listType = "ul";
950
+ listItems.push(unorderedMatch[1].trim());
951
+ continue;
952
+ }
953
+
954
+ const orderedMatch = line.match(/^\s*\d+\.\s+(.+)$/);
955
+ if (orderedMatch) {
956
+ flushParagraph();
957
+ flushQuote();
958
+ if (listType && listType !== "ol") {
959
+ flushList();
960
+ }
961
+ listType = "ol";
962
+ listItems.push(orderedMatch[1].trim());
963
+ continue;
964
+ }
965
+
966
+ flushQuote();
967
+ flushList();
968
+ paragraph.push(trimmed);
496
969
  }
497
970
 
498
- return {
499
- file: positional[0],
500
- options
501
- };
971
+ flushCodeBlock();
972
+ flushAll();
973
+ return html.join("\n");
502
974
  }
503
975
 
504
- async function publishArticleCommand(file, options) {
505
- if (!file) {
506
- throw new Error("缺少文章文件,例如:qcplay-cli www-article-list.store article.md");
976
+ function parseFrontMatter(raw) {
977
+ const normalized = stripBom(raw).replace(/\r\n/g, "\n");
978
+ const lines = normalized.split("\n");
979
+
980
+ if (lines[0]?.trim() !== "---") {
981
+ throw new Error("文章文件缺少 Front Matter,首行必须是 ---");
507
982
  }
508
983
 
509
- const articleFile = path.resolve(process.cwd(), file);
510
- if (!(await pathExists(articleFile))) {
511
- throw new Error(`文章文件不存在: ${articleFile}`);
984
+ let dividerIndex = -1;
985
+ for (let index = 1; index < lines.length; index += 1) {
986
+ if (lines[index].trim() === "---") {
987
+ dividerIndex = index;
988
+ break;
989
+ }
512
990
  }
513
991
 
514
- const loginStatus = await getLoginStatus();
515
- if (!loginStatus.ok) {
516
- throw new Error(`未检测到有效登录状态。\n原因: ${loginStatus.reason}\n请先执行:qcplay-cli auth`);
992
+ if (dividerIndex === -1) {
993
+ throw new Error("文章文件缺少 Front Matter 结束标记 ---");
517
994
  }
518
995
 
519
- const args = ["--content-file", articleFile];
996
+ const meta = {};
997
+ for (const line of lines.slice(1, dividerIndex)) {
998
+ if (!line.trim()) {
999
+ continue;
1000
+ }
1001
+
1002
+ const separatorIndex = line.indexOf(":");
1003
+ if (separatorIndex === -1) {
1004
+ throw new Error(`Front Matter 格式错误: ${line}`);
1005
+ }
520
1006
 
521
- if (options.env) {
522
- args.push("--env", options.env);
1007
+ meta[line.slice(0, separatorIndex).trim()] = unquoteValue(line.slice(separatorIndex + 1));
523
1008
  }
524
1009
 
525
- if (options.url) {
526
- args.push("--url", options.url);
1010
+ return {
1011
+ meta,
1012
+ content: lines.slice(dividerIndex + 1).join("\n").trim()
1013
+ };
1014
+ }
1015
+
1016
+ function normalizeArticlePayload(meta, content) {
1017
+ const payload = {
1018
+ article_title: normalizeText(meta.article_title),
1019
+ thumbnail: normalizeText(meta.thumbnail),
1020
+ move_thumbnail: normalizeText(meta.move_thumbnail),
1021
+ article_content: markdownToHtml(content),
1022
+ article_excerpt: normalizeText(meta.article_excerpt),
1023
+ article_url: normalizeText(meta.article_url),
1024
+ origin: normalizeText(meta.origin),
1025
+ status: normalizeMappedValue(meta.status, STATUS_MAP) || "0",
1026
+ cate_id: normalizeMappedValue(meta.cate_id, CATEGORY_MAP),
1027
+ video_link: normalizeText(meta.video_link),
1028
+ is_hot: normalizeBoolLike(meta.is_hot, "0"),
1029
+ is_index: normalizeBoolLike(meta.is_index, "0"),
1030
+ release_time: normalizeText(meta.release_time),
1031
+ area: normalizeMappedValue(meta.area, AREA_MAP) || "1",
1032
+ sort: normalizeText(meta.sort) || "1",
1033
+ game_id: normalizeMappedValue(meta.game_id, GAME_MAP) || "39",
1034
+ is_index2: "0",
1035
+ index_pc_img: normalizeText(meta.index_pc_img),
1036
+ index_move_img: normalizeText(meta.index_move_img),
1037
+ type: "1"
1038
+ };
1039
+
1040
+ if (!payload.article_title) {
1041
+ throw new Error("article_title 不能为空");
527
1042
  }
528
1043
 
529
- if (options.dryRun) {
530
- args.push("--dry-run");
1044
+ if (!payload.thumbnail) {
1045
+ throw new Error("thumbnail 不能为空");
531
1046
  }
532
1047
 
533
- console.log("");
534
- console.log(chalk.cyan("正在发布官网文章..."));
535
- console.log(chalk.gray(`文章文件: ${articleFile}`));
536
- console.log("");
1048
+ if (!payload.article_content) {
1049
+ throw new Error("文章正文不能为空");
1050
+ }
1051
+
1052
+ return payload;
1053
+ }
1054
+
1055
+ async function parseArticleFile(file) {
1056
+ const articleFile = path.resolve(process.cwd(), file);
1057
+ if (!(await pathExists(articleFile))) {
1058
+ throw new Error(`文章文件不存在: ${articleFile}`);
1059
+ }
537
1060
 
538
- await runNative("publish-article", args);
1061
+ const raw = await fs.promises.readFile(articleFile, "utf8");
1062
+ const parsed = parseFrontMatter(raw);
1063
+ return {
1064
+ articleFile,
1065
+ payload: normalizeArticlePayload(parsed.meta, parsed.content)
1066
+ };
539
1067
  }
540
1068
 
541
1069
  async function initArticleTemplate(file = "article.md") {
@@ -579,15 +1107,15 @@ move_thumbnail: https://example.com/mobile-thumbnail.png
579
1107
  article_excerpt: 文章描述,可以为空
580
1108
  article_url: article-url-slug
581
1109
  origin: QCPlay
582
- status: "1"
583
- cate_id: "2"
1110
+ status: 未发布
1111
+ cate_id: 综合
584
1112
  video_link: ""
585
- is_hot: "0"
586
- is_index: "0"
587
- release_time: "2026-07-09"
588
- area: "1"
589
- sort: "100"
590
- game_id: "39"
1113
+ is_hot: 非热门
1114
+ is_index: 不推荐
1115
+ release_time: 2026-07-16
1116
+ area: pc
1117
+ sort: 100
1118
+ game_id: 最强蜗牛
591
1119
  index_pc_img: ""
592
1120
  index_move_img: ""
593
1121
  ---
@@ -595,77 +1123,39 @@ index_move_img: ""
595
1123
  # 文章标题
596
1124
 
597
1125
  这里填写文章正文内容。
598
-
599
- ## 一、文章小标题
600
-
601
- 这里填写正文内容。
602
-
603
- ## 二、内容说明
604
-
605
- 这里继续填写正文。
606
-
607
- ## 三、总结
608
-
609
- 这里填写文章结尾内容。
610
1126
  ${chalk.gray("--------------------------------------------------")}
611
1127
 
612
1128
  ${chalk.cyan("发布命令:")}
613
1129
 
614
1130
  qcplay-cli www-article-list.store article.md
1131
+ `);
1132
+ }
615
1133
 
616
- ${chalk.cyan("参数说明:")}
617
-
618
- article_title 文章标题,必填
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
1134
+ async function publishArticleCommand(file, options) {
1135
+ if (!file) {
1136
+ throw new Error("缺少文章文件,例如:qcplay-cli www-article-list.store article.md");
1137
+ }
667
1138
 
668
- `);
1139
+ const { articleFile, payload } = await parseArticleFile(file);
1140
+ const config = await loadConfig();
1141
+ const backendUrl = resolvePublishBackendUrl(options, config);
1142
+
1143
+ if (options.dryRun) {
1144
+ console.log(JSON.stringify(payload, null, 2));
1145
+ return;
1146
+ }
1147
+
1148
+ console.log("");
1149
+ console.log(chalk.cyan("正在发布官网文章..."));
1150
+ console.log(chalk.gray(`文章文件: ${articleFile}`));
1151
+ console.log("");
1152
+
1153
+ const response = await requestJson("POST", backendUrl, "/api/articles/publish", payload);
1154
+ console.log(chalk.green(response.message || "发布成功"));
1155
+ const articleId = response.data?.id ?? response.data?.article_id ?? response.data?.articleId;
1156
+ if (articleId !== undefined) {
1157
+ console.log(`文章 ID: ${articleId}`);
1158
+ }
669
1159
  }
670
1160
 
671
1161
  async function runWithErrorBanner(title, action) {
@@ -694,7 +1184,8 @@ async function main() {
694
1184
  }
695
1185
 
696
1186
  if (command === "install") {
697
- await runWithErrorBanner("安装失败", installCommand);
1187
+ const parsed = parseBackendOptions([subcommand, ...rest].filter(Boolean));
1188
+ await runWithErrorBanner("安装失败", () => installCommand(parsed.options));
698
1189
  return;
699
1190
  }
700
1191
 
@@ -704,7 +1195,7 @@ async function main() {
704
1195
  return;
705
1196
  }
706
1197
 
707
- await runWithErrorBanner("认证失败", () => authCommand(subcommand, rest));
1198
+ await runWithErrorBanner("认证失败", () => authCommand([subcommand, ...rest].filter(Boolean)));
708
1199
  return;
709
1200
  }
710
1201
 
@@ -725,8 +1216,8 @@ async function main() {
725
1216
  }
726
1217
 
727
1218
  if (subcommand === "publish") {
728
- const { file, options } = parsePublishOptions(rest);
729
- await runWithErrorBanner("发布失败", () => publishArticleCommand(file, options));
1219
+ const parsed = parsePublishOptions(rest);
1220
+ await runWithErrorBanner("发布失败", () => publishArticleCommand(parsed.file, parsed.options));
730
1221
  return;
731
1222
  }
732
1223