@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.
- package/README.md +3 -0
- package/bin/qcplay.js +372 -31
- 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
|
|
97
|
+
function readPackageMetadata() {
|
|
97
98
|
try {
|
|
98
99
|
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
|
|
99
|
-
return
|
|
100
|
+
return {
|
|
101
|
+
name: packageJson.name || "@qcplay/cli",
|
|
102
|
+
version: packageJson.version || "0.0.0"
|
|
103
|
+
};
|
|
100
104
|
} catch {
|
|
101
|
-
return
|
|
105
|
+
return {
|
|
106
|
+
name: "@qcplay/cli",
|
|
107
|
+
version: "0.0.0"
|
|
108
|
+
};
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
111
|
|
|
105
|
-
const
|
|
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
|
|
307
|
+
return {
|
|
308
|
+
available: false,
|
|
309
|
+
added: [],
|
|
310
|
+
updated: []
|
|
311
|
+
};
|
|
244
312
|
}
|
|
245
313
|
|
|
246
|
-
await
|
|
247
|
-
return
|
|
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
|
-
|
|
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
|
|
561
|
-
const response = await requestJson("
|
|
562
|
-
|
|
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
|
|
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
|
|
586
|
-
|
|
587
|
-
|
|
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
|
|
722
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
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
|
-
|
|
754
|
-
console.log(chalk.green(
|
|
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
|
|
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
|
|
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();
|