@qcplay/cli 1.0.5 → 1.0.7

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/bin/qcplay.js +372 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,6 +7,9 @@
7
7
  - 认证默认请求线上接口:`https://cli.qcg.ink`
8
8
  - 发布默认请求线上接口:`https://cli.qcg.ink`
9
9
  - 登录默认打开线上页面:`https://cli.qcg.ink/auth.html`
10
+ - 登录凭证保存在当前用户本机的 `~/.qcplay/auth.json`,不同客户端之间不共享账号
11
+ - 状态、权限和发布请求通过 Bearer Token 识别当前账号
12
+ - `qcplay-cli update` 会更新 npm 全局包,并同步 `~/.agents/skills`
10
13
  - 本地调试时可直接使用 `--local`
11
14
  - `--local` 默认指向 `http://127.0.0.1:8787`
12
15
  - `qcplay-cli auth --local` 会打开仓库里的本地页面 `web/auth.html`
package/bin/qcplay.js CHANGED
@@ -91,18 +91,27 @@ const BOOLEAN_TEXT_MAP = {
91
91
  const QCPLAY_DIR = path.join(os.homedir(), ".qcplay");
92
92
  const AGENTS_DIR = path.join(os.homedir(), ".agents");
93
93
  const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
94
+ const AUTH_FILE = path.join(QCPLAY_DIR, "auth.json");
94
95
  const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
95
96
 
96
- function readPackageVersion() {
97
+ function readPackageMetadata() {
97
98
  try {
98
99
  const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
99
- return packageJson.version || "0.0.0";
100
+ return {
101
+ name: packageJson.name || "@qcplay/cli",
102
+ version: packageJson.version || "0.0.0"
103
+ };
100
104
  } catch {
101
- return "0.0.0";
105
+ return {
106
+ name: "@qcplay/cli",
107
+ version: "0.0.0"
108
+ };
102
109
  }
103
110
  }
104
111
 
105
- const PACKAGE_VERSION = readPackageVersion();
112
+ const PACKAGE_METADATA = readPackageMetadata();
113
+ const PACKAGE_NAME = PACKAGE_METADATA.name;
114
+ const PACKAGE_VERSION = PACKAGE_METADATA.version;
106
115
  const colorsEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
107
116
 
108
117
  const chalk = {
@@ -126,6 +135,7 @@ function printRootHelp() {
126
135
 
127
136
  Usage:
128
137
  qcplay-cli install [--local]
138
+ qcplay-cli update
129
139
  qcplay-cli auth [login|status|logout] [--local] [--backend <url>] [--auth-page <url>]
130
140
  qcplay-cli auth permissions [--local] [--key <key>] [--json]
131
141
  qcplay-cli article
@@ -141,6 +151,11 @@ Options:
141
151
  `);
142
152
  }
143
153
 
154
+ function printUpdateHelp() {
155
+ console.log("Usage:");
156
+ console.log(" qcplay-cli update");
157
+ }
158
+
144
159
  function printAuthHelp() {
145
160
  console.log("Usage:");
146
161
  console.log(" qcplay-cli auth [login|status|logout] [--local] [--backend <url>] [--auth-page <url>]");
@@ -220,6 +235,7 @@ async function writeJson(filePath, value) {
220
235
  encoding: "utf8",
221
236
  mode: 0o600
222
237
  });
238
+ await fs.promises.chmod(filePath, 0o600);
223
239
  }
224
240
 
225
241
  async function readJson(filePath) {
@@ -236,15 +252,70 @@ async function ensureLocalDirs() {
236
252
  await ensureDir(SKILLS_DIR);
237
253
  }
238
254
 
255
+ async function filesAreEqual(sourcePath, targetPath) {
256
+ try {
257
+ const [sourceContent, targetContent] = await Promise.all([
258
+ fs.promises.readFile(sourcePath),
259
+ fs.promises.readFile(targetPath)
260
+ ]);
261
+ return sourceContent.equals(targetContent);
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
266
+
267
+ async function syncDirectoryChanges(sourceDir, targetDir, rootDir = sourceDir, changes = { added: [], updated: [] }) {
268
+ await ensureDir(targetDir);
269
+ const entries = await fs.promises.readdir(sourceDir, { withFileTypes: true });
270
+
271
+ for (const entry of entries) {
272
+ const sourcePath = path.join(sourceDir, entry.name);
273
+ const targetPath = path.join(targetDir, entry.name);
274
+
275
+ if (entry.isDirectory()) {
276
+ await syncDirectoryChanges(sourcePath, targetPath, rootDir, changes);
277
+ continue;
278
+ }
279
+
280
+ if (!entry.isFile()) {
281
+ continue;
282
+ }
283
+
284
+ const relativePath = path.relative(rootDir, sourcePath);
285
+ if (!(await pathExists(targetPath))) {
286
+ await ensureDir(path.dirname(targetPath));
287
+ await fs.promises.copyFile(sourcePath, targetPath);
288
+ changes.added.push(relativePath);
289
+ continue;
290
+ }
291
+
292
+ if (await filesAreEqual(sourcePath, targetPath)) {
293
+ continue;
294
+ }
295
+
296
+ await fs.promises.copyFile(sourcePath, targetPath);
297
+ changes.updated.push(relativePath);
298
+ }
299
+
300
+ return changes;
301
+ }
302
+
239
303
  async function installSkills() {
240
304
  const sourceDir = path.resolve(__dirname, "../templates/skills");
241
305
 
242
306
  if (!(await pathExists(sourceDir))) {
243
- return false;
307
+ return {
308
+ available: false,
309
+ added: [],
310
+ updated: []
311
+ };
244
312
  }
245
313
 
246
- await copyRecursive(sourceDir, SKILLS_DIR);
247
- return true;
314
+ const changes = await syncDirectoryChanges(sourceDir, SKILLS_DIR);
315
+ return {
316
+ available: true,
317
+ ...changes
318
+ };
248
319
  }
249
320
 
250
321
  async function saveConfig(authBackendUrl, publishBackendUrl, authPageUrl, localMode = false) {
@@ -264,6 +335,19 @@ async function loadConfig() {
264
335
  return readJson(CONFIG_FILE);
265
336
  }
266
337
 
338
+ async function updateStoredConfigVersion(version) {
339
+ const config = await loadConfig();
340
+ if (Object.keys(config).length === 0) {
341
+ return;
342
+ }
343
+
344
+ await writeJson(CONFIG_FILE, {
345
+ ...config,
346
+ version,
347
+ updated_at: Date.now()
348
+ });
349
+ }
350
+
267
351
  function normalizeText(value) {
268
352
  return String(value ?? "").trim();
269
353
  }
@@ -409,7 +493,7 @@ function parsePublishOptions(args) {
409
493
  };
410
494
  }
411
495
 
412
- function requestJson(method, baseUrl, pathname, payload) {
496
+ function requestJson(method, baseUrl, pathname, payload, headers = {}) {
413
497
  return new Promise((resolve, reject) => {
414
498
  const url = new URL(pathname, baseUrl);
415
499
  const client = url.protocol === "https:" ? https : http;
@@ -423,6 +507,7 @@ function requestJson(method, baseUrl, pathname, payload) {
423
507
  Accept: "application/json",
424
508
  "Content-Type": "application/json",
425
509
  "User-Agent": `qcplay-cli/${PACKAGE_VERSION}`,
510
+ ...headers,
426
511
  ...(body ? { "Content-Length": Buffer.byteLength(body) } : {})
427
512
  }
428
513
  },
@@ -441,7 +526,9 @@ function requestJson(method, baseUrl, pathname, payload) {
441
526
  }
442
527
 
443
528
  if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) {
444
- reject(new Error(json.message || `接口请求失败 (${res.statusCode})`));
529
+ const error = new Error(json.message || `接口请求失败 (${res.statusCode})`);
530
+ error.statusCode = res.statusCode || 0;
531
+ reject(error);
445
532
  return;
446
533
  }
447
534
 
@@ -489,6 +576,28 @@ function openBrowser(targetUrl) {
489
576
  });
490
577
  }
491
578
 
579
+ function getNpmCommand() {
580
+ return process.platform === "win32" ? "npm.cmd" : "npm";
581
+ }
582
+
583
+ function runCommand(command, args) {
584
+ return new Promise((resolve, reject) => {
585
+ const child = spawn(command, args, {
586
+ stdio: "inherit"
587
+ });
588
+
589
+ child.on("error", reject);
590
+ child.on("close", code => {
591
+ if (code === 0) {
592
+ resolve();
593
+ return;
594
+ }
595
+
596
+ reject(new Error(`命令执行失败 (${code}): ${command} ${args.join(" ")}`));
597
+ });
598
+ });
599
+ }
600
+
492
601
  function delay(ms) {
493
602
  return new Promise(resolve => {
494
603
  setTimeout(resolve, ms);
@@ -557,9 +666,18 @@ function resolvePublishBackendUrl(options, config = {}) {
557
666
  return ensureAbsoluteUrl(backendUrl, "--backend");
558
667
  }
559
668
 
560
- async function fetchLoginSuccessSequence(baseUrl) {
561
- const response = await requestJson("GET", baseUrl, "/api/auth/login-success");
562
- return Number(response.data?.sequence || 0);
669
+ async function createLoginRequest(baseUrl) {
670
+ const response = await requestJson("POST", baseUrl, "/api/auth/login-request", {});
671
+ const requestId = normalizeText(response.data?.request_id);
672
+ const pollToken = normalizeText(response.data?.poll_token);
673
+ if (!requestId || !pollToken) {
674
+ throw new Error("认证后端没有返回有效的登录请求凭证");
675
+ }
676
+ return {
677
+ requestId,
678
+ pollToken,
679
+ expiresIn: Number(response.data?.expires_in || 600)
680
+ };
563
681
  }
564
682
 
565
683
  function isNonJsonResponseError(error) {
@@ -569,22 +687,30 @@ function isNonJsonResponseError(error) {
569
687
  function buildAuthBackendError(baseUrl, error) {
570
688
  if (isNonJsonResponseError(error)) {
571
689
  return new Error(
572
- `认证接口 ${baseUrl} 返回了 HTML 页面,不是 JSON。请确认 ${baseUrl}/api/auth/login、${baseUrl}/api/auth/login-success、${baseUrl}/api/auth/status 已正确部署,而不是被站点首页或登录页接管。`
690
+ `认证接口 ${baseUrl} 返回了 HTML 页面,不是 JSON。请确认 ${baseUrl}/api/auth/login-request、${baseUrl}/api/auth/login、${baseUrl}/api/auth/login-success、${baseUrl}/api/auth/status 已正确部署,而不是被站点首页或登录页接管。`
573
691
  );
574
692
  }
575
693
 
576
694
  return error;
577
695
  }
578
696
 
579
- async function waitForLoginSuccessSequence(baseUrl, previousSequence, intervalMs = 1000, timeoutMs = 60000) {
697
+ async function waitForLoginResult(baseUrl, loginRequest, intervalMs = 1000) {
580
698
  const startedAt = Date.now();
699
+ const timeoutMs = Math.max(1, loginRequest.expiresIn) * 1000;
581
700
  let lastError;
701
+ const query = new URLSearchParams({ request_id: loginRequest.requestId });
582
702
 
583
703
  while (true) {
584
704
  try {
585
- const nextSequence = await fetchLoginSuccessSequence(baseUrl);
586
- if (nextSequence > previousSequence) {
587
- return nextSequence;
705
+ const response = await requestJson("GET", baseUrl, `/api/auth/login-success?${query}`, undefined, {
706
+ "X-QCPlay-Poll-Token": loginRequest.pollToken
707
+ });
708
+ if (response.data?.complete) {
709
+ const result = response.data?.result;
710
+ if (!result?.access_token || !result?.refresh_token) {
711
+ throw new Error("认证后端返回的登录结果不完整");
712
+ }
713
+ return result;
588
714
  }
589
715
  } catch (error) {
590
716
  lastError = error;
@@ -602,13 +728,15 @@ async function waitForLoginSuccessSequence(baseUrl, previousSequence, intervalMs
602
728
  }
603
729
  }
604
730
 
605
- function buildAuthPageUrl(backendUrl, authPageUrl = "") {
731
+ function buildAuthPageUrl(backendUrl, authPageUrl = "", requestId = "") {
606
732
  const targetUrl = authPageUrl ? new URL(authPageUrl) : pathToFileURL(AUTH_PAGE);
607
733
 
608
734
  if (shouldAttachBackendQuery(targetUrl, backendUrl)) {
609
735
  targetUrl.searchParams.set("backend", backendUrl);
610
736
  }
611
737
 
738
+ targetUrl.searchParams.set("request_id", requestId);
739
+
612
740
  return targetUrl.toString();
613
741
  }
614
742
 
@@ -625,6 +753,117 @@ function shouldAttachBackendQuery(targetUrl, backendUrl) {
625
753
  return normalizedBackendUrl.origin !== targetUrl.origin;
626
754
  }
627
755
 
756
+ function normalizeTimestamp(value) {
757
+ const timestamp = Number(value || 0);
758
+ return timestamp > 0 && timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp;
759
+ }
760
+
761
+ function tokenIsValid(token, expiresAt) {
762
+ return Boolean(normalizeText(token)) && Date.now() < normalizeTimestamp(expiresAt);
763
+ }
764
+
765
+ function authRequiredError(message) {
766
+ const error = new Error(message);
767
+ error.code = "AUTH_REQUIRED";
768
+ return error;
769
+ }
770
+
771
+ async function loadAuthState() {
772
+ const state = await readJson(AUTH_FILE);
773
+ return normalizeText(state.access_token) ? state : {};
774
+ }
775
+
776
+ async function saveAuthState(result, backendUrl) {
777
+ if (!normalizeText(result?.access_token) || !normalizeText(result?.refresh_token)) {
778
+ throw new Error("认证后端返回的登录结果不完整");
779
+ }
780
+
781
+ await writeJson(AUTH_FILE, {
782
+ backend_url: backendUrl,
783
+ api_base: result.api_base || "",
784
+ access_token: result.access_token,
785
+ refresh_token: result.refresh_token,
786
+ token_type: result.token_type || "Bearer",
787
+ expires_at: Number(result.expires_at || 0),
788
+ refresh_expires_at: Number(result.refresh_expires_at || 0),
789
+ created_at: Number(result.created_at || Date.now()),
790
+ id: Number(result.id || 0),
791
+ account: result.account || "",
792
+ name: result.name || "",
793
+ status: result.status || "",
794
+ type: result.type || ""
795
+ });
796
+ }
797
+
798
+ async function removeAuthState() {
799
+ try {
800
+ await fs.promises.unlink(AUTH_FILE);
801
+ } catch (error) {
802
+ if (error.code !== "ENOENT") {
803
+ throw error;
804
+ }
805
+ }
806
+ }
807
+
808
+ async function refreshAuthState(state, fallbackBackendUrl) {
809
+ if (!tokenIsValid(state.refresh_token, state.refresh_expires_at)) {
810
+ throw authRequiredError("登录状态已过期,请重新登录");
811
+ }
812
+
813
+ const authBackendUrl = normalizeOptionalUrl(state.backend_url) || fallbackBackendUrl;
814
+ const response = await requestJson("POST", authBackendUrl, "/api/auth/login", {
815
+ refresh_token: state.refresh_token
816
+ });
817
+ const result = response.data?.result;
818
+ await saveAuthState(result, authBackendUrl);
819
+ return loadAuthState();
820
+ }
821
+
822
+ async function requireAuthState(backendUrl) {
823
+ let state = await loadAuthState();
824
+ if (!normalizeText(state.access_token)) {
825
+ throw authRequiredError("未检测到登录状态,请先执行 qcplay-cli auth");
826
+ }
827
+
828
+ if (!tokenIsValid(state.access_token, state.expires_at)) {
829
+ try {
830
+ state = await refreshAuthState(state, backendUrl);
831
+ } catch (error) {
832
+ if (error.code === "AUTH_REQUIRED" || error.statusCode === 401) {
833
+ await removeAuthState();
834
+ }
835
+ throw error;
836
+ }
837
+ }
838
+ return state;
839
+ }
840
+
841
+ async function authenticatedRequest(method, baseUrl, pathname, payload) {
842
+ let state = await requireAuthState(baseUrl);
843
+ try {
844
+ return await requestJson(method, baseUrl, pathname, payload, {
845
+ Authorization: `Bearer ${state.access_token}`
846
+ });
847
+ } catch (error) {
848
+ if (error.statusCode !== 401) {
849
+ throw error;
850
+ }
851
+ }
852
+
853
+ try {
854
+ state = await refreshAuthState(state, baseUrl);
855
+ } catch (error) {
856
+ if (error.code === "AUTH_REQUIRED" || error.statusCode === 401) {
857
+ await removeAuthState();
858
+ }
859
+ throw error;
860
+ }
861
+
862
+ return requestJson(method, baseUrl, pathname, payload, {
863
+ Authorization: `Bearer ${state.access_token}`
864
+ });
865
+ }
866
+
628
867
  function flattenPermissionTree(items, bucket = []) {
629
868
  for (const item of items) {
630
869
  bucket.push(item);
@@ -675,6 +914,33 @@ function printPermissionTree(items) {
675
914
  }
676
915
  }
677
916
 
917
+ function formatAccountSummary(data = {}) {
918
+ const id = Number(data.id || 0);
919
+ const name = normalizeText(data.name) || "未知账号";
920
+ const account = normalizeText(data.account);
921
+ const type = normalizeText(data.type);
922
+ const status = normalizeText(data.status);
923
+ const parts = [name];
924
+
925
+ if (account && account !== name) {
926
+ parts.push(`account=${account}`);
927
+ }
928
+
929
+ if (id > 0) {
930
+ parts.push(`id=${id}`);
931
+ }
932
+
933
+ if (type) {
934
+ parts.push(`type=${type}`);
935
+ }
936
+
937
+ if (status) {
938
+ parts.push(`status=${status}`);
939
+ }
940
+
941
+ return parts.join(" | ");
942
+ }
943
+
678
944
  async function installCommand(options) {
679
945
  const config = await loadConfig();
680
946
  const localMode = isLocalMode(options, config);
@@ -690,7 +956,7 @@ async function installCommand(options) {
690
956
  console.log(chalk.green("✔ 本地目录已创建"));
691
957
 
692
958
  const skillsInstalled = await installSkills();
693
- if (skillsInstalled) {
959
+ if (skillsInstalled.available) {
694
960
  console.log(chalk.green("✔ Skills 已安装"));
695
961
  console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
696
962
  } else {
@@ -708,6 +974,56 @@ async function installCommand(options) {
708
974
  console.log("");
709
975
  }
710
976
 
977
+ function printSkillsSyncResult(result) {
978
+ if (!result.available) {
979
+ console.log(chalk.yellow("! 未找到 Skills 模板,已跳过"));
980
+ return;
981
+ }
982
+
983
+ const addedCount = result.added.length;
984
+ const updatedCount = result.updated.length;
985
+ if (addedCount === 0 && updatedCount === 0) {
986
+ console.log(chalk.green("✔ Skills 已检查,无新增或变更"));
987
+ console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
988
+ return;
989
+ }
990
+
991
+ const parts = [];
992
+ if (addedCount > 0) {
993
+ parts.push(`新增 ${addedCount} 个文件`);
994
+ }
995
+ if (updatedCount > 0) {
996
+ parts.push(`更新 ${updatedCount} 个文件`);
997
+ }
998
+
999
+ console.log(chalk.green(`✔ Skills 已同步(${parts.join(",")})`));
1000
+ console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
1001
+ }
1002
+
1003
+ async function updateCommand() {
1004
+ console.log("");
1005
+ console.log(chalk.cyan("正在更新 QCPlay CLI..."));
1006
+ console.log(chalk.gray(`当前版本: ${PACKAGE_VERSION}`));
1007
+ console.log("");
1008
+
1009
+ await ensureLocalDirs();
1010
+ await runCommand(getNpmCommand(), ["install", "-g", `${PACKAGE_NAME}@latest`]);
1011
+
1012
+ const latestPackage = readPackageMetadata();
1013
+ await updateStoredConfigVersion(latestPackage.version);
1014
+ const skillsInstalled = await installSkills();
1015
+
1016
+ console.log("");
1017
+ console.log(chalk.green("CLI 更新完成"));
1018
+ if (latestPackage.version !== PACKAGE_VERSION) {
1019
+ console.log(chalk.gray(`版本: ${PACKAGE_VERSION} -> ${latestPackage.version}`));
1020
+ } else {
1021
+ console.log(chalk.gray(`版本: ${latestPackage.version}`));
1022
+ }
1023
+ printSkillsSyncResult(skillsInstalled);
1024
+ console.log("");
1025
+ }
1026
+
711
1027
  async function authCommand(rawArgs = []) {
712
1028
  const parsed = parseBackendOptions(rawArgs);
713
1029
  const options = parsed.options;
@@ -718,8 +1034,8 @@ async function authCommand(rawArgs = []) {
718
1034
  const authPageUrl = resolveAuthPageUrl(options, config);
719
1035
 
720
1036
  if (!action || action === "login") {
721
- const loginPageUrl = buildAuthPageUrl(backendUrl, authPageUrl);
722
- let previousSequence = 0;
1037
+ const loginRequest = await createLoginRequest(backendUrl);
1038
+ const loginPageUrl = buildAuthPageUrl(backendUrl, authPageUrl, loginRequest.requestId);
723
1039
 
724
1040
  console.log(chalk.cyan("正在登录中"));
725
1041
  try {
@@ -729,35 +1045,50 @@ async function authCommand(rawArgs = []) {
729
1045
  console.log(chalk.gray(loginPageUrl));
730
1046
  }
731
1047
 
732
- try {
733
- previousSequence = await fetchLoginSuccessSequence(backendUrl);
734
- } catch {}
735
-
736
- await waitForLoginSuccessSequence(backendUrl, previousSequence);
1048
+ const result = await waitForLoginResult(backendUrl, loginRequest);
1049
+ await saveAuthState(result, backendUrl);
1050
+ const statusResponse = await authenticatedRequest("GET", backendUrl, "/api/auth/status");
1051
+ if (!statusResponse.data?.logged_in) {
1052
+ await removeAuthState();
1053
+ throw new Error("登录结果校验失败,请重新登录");
1054
+ }
737
1055
  console.log(chalk.green("登录成功"));
1056
+ console.log(chalk.gray(`当前账号: ${formatAccountSummary(statusResponse.data)}`));
738
1057
  return;
739
1058
  }
740
1059
 
741
1060
  if (action === "status") {
742
- const response = await requestJson("GET", backendUrl, "/api/auth/status");
1061
+ let response;
1062
+ try {
1063
+ response = await authenticatedRequest("GET", backendUrl, "/api/auth/status");
1064
+ } catch (error) {
1065
+ if (error.code === "AUTH_REQUIRED" || error.statusCode === 401 || !(await pathExists(AUTH_FILE))) {
1066
+ await removeAuthState();
1067
+ console.log(chalk.yellow("未登录"));
1068
+ return;
1069
+ }
1070
+ throw error;
1071
+ }
743
1072
  if (response.data?.logged_in) {
744
1073
  console.log(chalk.green("已登录"));
1074
+ console.log(chalk.gray(`当前账号: ${formatAccountSummary(response.data)}`));
745
1075
  return;
746
1076
  }
747
1077
 
1078
+ await removeAuthState();
748
1079
  console.log(chalk.yellow("未登录"));
749
1080
  return;
750
1081
  }
751
1082
 
752
1083
  if (action === "logout") {
753
- const response = await requestJson("POST", backendUrl, "/api/auth/logout", {});
754
- console.log(chalk.green(response.message || "已退出登录"));
1084
+ await removeAuthState();
1085
+ console.log(chalk.green("已退出登录"));
755
1086
  return;
756
1087
  }
757
1088
 
758
1089
  if (action === "permissions") {
759
1090
  const flags = parsePermissionsOptions(args.slice(1));
760
- const response = await requestJson("GET", backendUrl, "/api/permissions");
1091
+ const response = await authenticatedRequest("GET", backendUrl, "/api/permissions");
761
1092
  let permissions = response.data || [];
762
1093
 
763
1094
  if (flags.key) {
@@ -1150,7 +1481,7 @@ async function publishArticleCommand(file, options) {
1150
1481
  console.log(chalk.gray(`文章文件: ${articleFile}`));
1151
1482
  console.log("");
1152
1483
 
1153
- const response = await requestJson("POST", backendUrl, "/api/articles/publish", payload);
1484
+ const response = await authenticatedRequest("POST", backendUrl, "/api/articles/publish", payload);
1154
1485
  console.log(chalk.green(response.message || "发布成功"));
1155
1486
  const articleId = response.data?.id ?? response.data?.article_id ?? response.data?.articleId;
1156
1487
  if (articleId !== undefined) {
@@ -1189,6 +1520,16 @@ async function main() {
1189
1520
  return;
1190
1521
  }
1191
1522
 
1523
+ if (command === "update") {
1524
+ if (subcommand === "-h" || subcommand === "--help") {
1525
+ printUpdateHelp();
1526
+ return;
1527
+ }
1528
+
1529
+ await runWithErrorBanner("更新失败", () => updateCommand());
1530
+ return;
1531
+ }
1532
+
1192
1533
  if (command === "auth") {
1193
1534
  if (subcommand === "-h" || subcommand === "--help") {
1194
1535
  printAuthHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qcplay/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "QCPlay CLI",
5
5
  "type": "module",
6
6
  "bin": {