@42ailab/42plugin 0.1.0-beta.0 → 0.1.2
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 +211 -68
- package/package.json +13 -8
- package/src/api.ts +447 -0
- package/src/cli.ts +33 -16
- package/src/commands/auth.ts +83 -69
- package/src/commands/check.ts +118 -0
- package/src/commands/completion.ts +210 -0
- package/src/commands/index.ts +13 -0
- package/src/commands/install-helper.ts +71 -0
- package/src/commands/install.ts +219 -300
- package/src/commands/list.ts +42 -66
- package/src/commands/publish.ts +121 -0
- package/src/commands/search.ts +89 -72
- package/src/commands/setup.ts +158 -0
- package/src/commands/uninstall.ts +53 -44
- package/src/config.ts +27 -36
- package/src/db.ts +593 -0
- package/src/errors.ts +40 -0
- package/src/index.ts +4 -31
- package/src/services/packager.ts +177 -0
- package/src/services/publisher.ts +237 -0
- package/src/services/upload.ts +52 -0
- package/src/services/version-manager.ts +65 -0
- package/src/types.ts +396 -0
- package/src/utils.ts +128 -0
- package/src/validators/plugin-validator.ts +635 -0
- package/src/commands/version.ts +0 -20
- package/src/db/client.ts +0 -180
- package/src/services/api.ts +0 -128
- package/src/services/auth.ts +0 -46
- package/src/services/cache.ts +0 -101
- package/src/services/download.ts +0 -148
- package/src/services/link.ts +0 -86
- package/src/services/project.ts +0 -179
- package/src/types/api.ts +0 -115
- package/src/types/db.ts +0 -31
- package/src/utils/errors.ts +0 -40
- package/src/utils/platform.ts +0 -6
- package/src/utils/target.ts +0 -114
package/src/api.ts
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 客户端
|
|
3
|
+
*
|
|
4
|
+
* 单文件封装所有 API 调用
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { config } from './config';
|
|
8
|
+
import type {
|
|
9
|
+
PluginDownload,
|
|
10
|
+
KitDownload,
|
|
11
|
+
KitApiResponse,
|
|
12
|
+
SearchResponse,
|
|
13
|
+
SearchApiResponse,
|
|
14
|
+
DeviceCodeResponse,
|
|
15
|
+
DeviceTokenResponse,
|
|
16
|
+
Session,
|
|
17
|
+
PluginStatusResponse,
|
|
18
|
+
UploadUrlResponse,
|
|
19
|
+
PublishConfirmRequest,
|
|
20
|
+
PublishConfirmResponse,
|
|
21
|
+
PluginType,
|
|
22
|
+
} from './types';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// 错误类型
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export class ApiError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
public statusCode: number,
|
|
31
|
+
public endpoint: string,
|
|
32
|
+
message: string,
|
|
33
|
+
public code?: string
|
|
34
|
+
) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'ApiError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class QuotaExceededError extends ApiError {
|
|
41
|
+
constructor(
|
|
42
|
+
public quotaType: string,
|
|
43
|
+
public limit: number,
|
|
44
|
+
public role: string,
|
|
45
|
+
message: string
|
|
46
|
+
) {
|
|
47
|
+
super(429, 'quota', message, 'QUOTA_EXCEEDED');
|
|
48
|
+
this.name = 'QuotaExceededError';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
isGuestQuota(): boolean {
|
|
52
|
+
return this.role === 'guest';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// API 客户端
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
class ApiClient {
|
|
61
|
+
private baseUrl: string;
|
|
62
|
+
private sessionToken: string | null = null;
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
this.baseUrl = config.apiBaseUrl;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setSessionToken(token: string | null): void {
|
|
69
|
+
this.sessionToken = token;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isAuthenticated(): boolean {
|
|
73
|
+
return !!this.sessionToken;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async request<T>(
|
|
77
|
+
path: string,
|
|
78
|
+
options: {
|
|
79
|
+
method?: string;
|
|
80
|
+
body?: unknown;
|
|
81
|
+
auth?: boolean;
|
|
82
|
+
} = {}
|
|
83
|
+
): Promise<T> {
|
|
84
|
+
const { method = 'GET', body, auth = true } = options;
|
|
85
|
+
|
|
86
|
+
const headers: Record<string, string> = {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
'User-Agent': '42plugin-cli/1.0.0',
|
|
89
|
+
'X-Download-Source': 'cli',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (auth && this.sessionToken) {
|
|
93
|
+
headers['Authorization'] = `Bearer ${this.sessionToken}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
97
|
+
method,
|
|
98
|
+
headers,
|
|
99
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const data = await response.json().catch(() => ({}));
|
|
103
|
+
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const error = (data as { error?: { code?: string; message?: string; quotaType?: string; limit?: number; role?: string } }).error;
|
|
106
|
+
const message = error?.message || `HTTP ${response.status}`;
|
|
107
|
+
|
|
108
|
+
if (response.status === 429 && error?.code === 'QUOTA_EXCEEDED') {
|
|
109
|
+
throw new QuotaExceededError(
|
|
110
|
+
error.quotaType || 'unknown',
|
|
111
|
+
error.limit || 0,
|
|
112
|
+
error.role || 'guest',
|
|
113
|
+
message
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw new ApiError(response.status, path, message, error?.code);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return ((data as { data?: T }).data ?? data) as T;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ==========================================================================
|
|
124
|
+
// 插件 API
|
|
125
|
+
// ==========================================================================
|
|
126
|
+
|
|
127
|
+
async getPluginDownload(author: string, name: string): Promise<PluginDownload> {
|
|
128
|
+
return this.request<PluginDownload>(`/v1/plugins/${author}/${name}/download`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ==========================================================================
|
|
132
|
+
// 套包 API
|
|
133
|
+
// ==========================================================================
|
|
134
|
+
|
|
135
|
+
async getKitDownload(username: string, slugOrId: string): Promise<KitDownload> {
|
|
136
|
+
const raw = await this.request<KitApiResponse['data']>(
|
|
137
|
+
`/v1/kits/${username}/kit/${slugOrId}/download`
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// 转换 snake_case -> camelCase
|
|
141
|
+
return {
|
|
142
|
+
kit: {
|
|
143
|
+
fullName: raw.kit.full_name,
|
|
144
|
+
name: raw.kit.name,
|
|
145
|
+
description: raw.kit.description,
|
|
146
|
+
priceTier: raw.kit.price_tier,
|
|
147
|
+
effectivePriceTier: raw.kit.effective_price_tier,
|
|
148
|
+
},
|
|
149
|
+
plugins: raw.plugins.map((p) => ({
|
|
150
|
+
plugin: {
|
|
151
|
+
fullName: p.plugin.full_name,
|
|
152
|
+
name: p.plugin.name,
|
|
153
|
+
title: p.plugin.title,
|
|
154
|
+
author: p.plugin.author,
|
|
155
|
+
type: p.plugin.type,
|
|
156
|
+
},
|
|
157
|
+
download: {
|
|
158
|
+
fullName: p.download.full_name,
|
|
159
|
+
name: p.download.name,
|
|
160
|
+
type: p.download.type,
|
|
161
|
+
version: p.download.version,
|
|
162
|
+
downloadUrl: p.download.download_url,
|
|
163
|
+
checksum: p.download.checksum,
|
|
164
|
+
sizeBytes: p.download.size_bytes,
|
|
165
|
+
installPath: p.download.install_path,
|
|
166
|
+
},
|
|
167
|
+
required: p.required,
|
|
168
|
+
reason: p.reason,
|
|
169
|
+
})),
|
|
170
|
+
summary: {
|
|
171
|
+
totalPlugins: raw.summary.total_plugins,
|
|
172
|
+
requiredPlugins: raw.summary.required_plugins,
|
|
173
|
+
optionalPlugins: raw.summary.optional_plugins,
|
|
174
|
+
totalSizeBytes: raw.summary.total_size_bytes,
|
|
175
|
+
expiresAt: raw.summary.expires_at,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
// 搜索 API
|
|
182
|
+
// ==========================================================================
|
|
183
|
+
|
|
184
|
+
async search(params: {
|
|
185
|
+
q: string;
|
|
186
|
+
pluginType?: string;
|
|
187
|
+
page?: number;
|
|
188
|
+
perPage?: number;
|
|
189
|
+
}): Promise<SearchResponse> {
|
|
190
|
+
const searchParams = new URLSearchParams();
|
|
191
|
+
searchParams.set('q', params.q);
|
|
192
|
+
if (params.pluginType) searchParams.set('plugin_type', params.pluginType);
|
|
193
|
+
if (params.page) searchParams.set('page', String(params.page));
|
|
194
|
+
if (params.perPage) searchParams.set('per_page', String(params.perPage));
|
|
195
|
+
|
|
196
|
+
// 搜索 API 直接返回 {data, pagination},不需要解包
|
|
197
|
+
const headers: Record<string, string> = {
|
|
198
|
+
'Content-Type': 'application/json',
|
|
199
|
+
'User-Agent': '42plugin-cli/1.0.0',
|
|
200
|
+
'X-Download-Source': 'cli',
|
|
201
|
+
};
|
|
202
|
+
if (this.sessionToken) {
|
|
203
|
+
headers['Authorization'] = `Bearer ${this.sessionToken}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const response = await fetch(`${this.baseUrl}/v1/search?${searchParams}`, {
|
|
207
|
+
method: 'GET',
|
|
208
|
+
headers,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const raw = await response.json();
|
|
212
|
+
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
const error = raw?.error;
|
|
215
|
+
const message = error?.message || `HTTP ${response.status}`;
|
|
216
|
+
throw new ApiError(response.status, '/v1/search', message, error?.code);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 转换 API 响应格式为内部格式 (snake_case -> camelCase)
|
|
220
|
+
const apiData = raw as SearchApiResponse;
|
|
221
|
+
const plugins = apiData.data?.plugins || [];
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
data: plugins.map((p) => ({
|
|
225
|
+
fullName: p.full_name,
|
|
226
|
+
name: p.name,
|
|
227
|
+
title: p.title,
|
|
228
|
+
slogan: p.slogan || null,
|
|
229
|
+
description: p.description,
|
|
230
|
+
type: p.type,
|
|
231
|
+
version: p.version || '0.0.0',
|
|
232
|
+
author: p.author,
|
|
233
|
+
tags: p.tags || [],
|
|
234
|
+
downloads: p.downloads,
|
|
235
|
+
icon: p.icon || null,
|
|
236
|
+
})),
|
|
237
|
+
pagination: {
|
|
238
|
+
page: apiData.pagination?.page || 1,
|
|
239
|
+
perPage: apiData.pagination?.per_page || 20,
|
|
240
|
+
total: apiData.pagination?.total || 0,
|
|
241
|
+
totalPages: Math.ceil((apiData.pagination?.total || 0) / (apiData.pagination?.per_page || 20)),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ==========================================================================
|
|
247
|
+
// 认证 API
|
|
248
|
+
// ==========================================================================
|
|
249
|
+
|
|
250
|
+
async requestDeviceCode(): Promise<DeviceCodeResponse> {
|
|
251
|
+
const response = await fetch(`${this.baseUrl}/auth/device/code`, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: {
|
|
254
|
+
'Content-Type': 'application/json',
|
|
255
|
+
'User-Agent': '42plugin-cli/1.0.0',
|
|
256
|
+
'X-Download-Source': 'cli',
|
|
257
|
+
},
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
client_id: '42plugin-cli',
|
|
260
|
+
scope: 'openid profile email',
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
deviceCode: data.device_code,
|
|
268
|
+
userCode: data.user_code,
|
|
269
|
+
verificationUri: data.verification_uri,
|
|
270
|
+
verificationUriComplete: data.verification_uri_complete,
|
|
271
|
+
expiresIn: data.expires_in,
|
|
272
|
+
interval: data.interval,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async pollDeviceToken(deviceCode: string): Promise<DeviceTokenResponse> {
|
|
277
|
+
const response = await fetch(`${this.baseUrl}/auth/device/token`, {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: {
|
|
280
|
+
'Content-Type': 'application/json',
|
|
281
|
+
'User-Agent': '42plugin-cli/1.0.0',
|
|
282
|
+
'X-Download-Source': 'cli',
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
client_id: '42plugin-cli',
|
|
286
|
+
device_code: deviceCode,
|
|
287
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
288
|
+
}),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const data = await response.json();
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
accessToken: data.access_token,
|
|
295
|
+
user: data.user
|
|
296
|
+
? {
|
|
297
|
+
id: data.user.id,
|
|
298
|
+
name: data.user.name,
|
|
299
|
+
username: data.user.username,
|
|
300
|
+
email: data.user.email,
|
|
301
|
+
}
|
|
302
|
+
: undefined,
|
|
303
|
+
error: data.error,
|
|
304
|
+
errorDescription: data.error_description,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async getSession(): Promise<Session> {
|
|
309
|
+
// 开发模式跳过认证 - 返回 mock session
|
|
310
|
+
if (config.devSkipAuth) {
|
|
311
|
+
return {
|
|
312
|
+
user: {
|
|
313
|
+
id: 'dev-user-id',
|
|
314
|
+
name: 'DevUser',
|
|
315
|
+
username: 'devuser',
|
|
316
|
+
email: 'dev@localhost',
|
|
317
|
+
},
|
|
318
|
+
session: {
|
|
319
|
+
id: 'dev-session-id',
|
|
320
|
+
expiresAt: new Date(Date.now() + 86400000).toISOString(),
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return this.request<Session>('/session');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ==========================================================================
|
|
328
|
+
// 用户 API
|
|
329
|
+
// ==========================================================================
|
|
330
|
+
|
|
331
|
+
async recordInstall(pluginFullName: string): Promise<void> {
|
|
332
|
+
await this.request('/user/installed', {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
body: { plugin_full_name: pluginFullName },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async removeInstallRecord(author: string, name: string): Promise<void> {
|
|
339
|
+
await this.request(`/user/installed/${author}/${name}`, {
|
|
340
|
+
method: 'DELETE',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ==========================================================================
|
|
345
|
+
// 发布 API
|
|
346
|
+
// ==========================================================================
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 获取插件状态(用于版本决策)
|
|
350
|
+
*/
|
|
351
|
+
async getPluginStatus(name: string): Promise<PluginStatusResponse> {
|
|
352
|
+
try {
|
|
353
|
+
const result = await this.request<{
|
|
354
|
+
exists: boolean;
|
|
355
|
+
version?: string;
|
|
356
|
+
content_hash?: string;
|
|
357
|
+
visibility?: 'self' | 'public';
|
|
358
|
+
}>(`/v1/plugins/me/${name}/status`);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
exists: result.exists,
|
|
362
|
+
version: result.version,
|
|
363
|
+
contentHash: result.content_hash,
|
|
364
|
+
visibility: result.visibility,
|
|
365
|
+
};
|
|
366
|
+
} catch (error) {
|
|
367
|
+
// 404 表示插件不存在
|
|
368
|
+
if (error instanceof ApiError && error.statusCode === 404) {
|
|
369
|
+
return { exists: false };
|
|
370
|
+
}
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 获取上传签名 URL
|
|
377
|
+
*/
|
|
378
|
+
async getUploadUrl(params: {
|
|
379
|
+
name: string;
|
|
380
|
+
type: PluginType;
|
|
381
|
+
version: string;
|
|
382
|
+
contentHash: string;
|
|
383
|
+
sizeBytes: number;
|
|
384
|
+
}): Promise<UploadUrlResponse> {
|
|
385
|
+
const result = await this.request<{
|
|
386
|
+
upload_url: string;
|
|
387
|
+
storage_key: string;
|
|
388
|
+
expires_at: string;
|
|
389
|
+
}>('/v1/plugins/upload-url', {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
body: {
|
|
392
|
+
name: params.name,
|
|
393
|
+
type: params.type,
|
|
394
|
+
version: params.version,
|
|
395
|
+
content_hash: params.contentHash,
|
|
396
|
+
size_bytes: params.sizeBytes,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
uploadUrl: result.upload_url,
|
|
402
|
+
storageKey: result.storage_key,
|
|
403
|
+
expiresAt: result.expires_at,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 确认发布
|
|
409
|
+
*/
|
|
410
|
+
async confirmPublish(params: PublishConfirmRequest): Promise<PublishConfirmResponse> {
|
|
411
|
+
const result = await this.request<{
|
|
412
|
+
plugin: {
|
|
413
|
+
id: string;
|
|
414
|
+
full_name: string;
|
|
415
|
+
version: string;
|
|
416
|
+
};
|
|
417
|
+
action: 'created' | 'updated';
|
|
418
|
+
}>('/v1/plugins', {
|
|
419
|
+
method: 'POST',
|
|
420
|
+
body: {
|
|
421
|
+
name: params.name,
|
|
422
|
+
type: params.type,
|
|
423
|
+
version: params.version,
|
|
424
|
+
content_hash: params.contentHash,
|
|
425
|
+
package_hash: params.packageHash,
|
|
426
|
+
storage_key: params.storageKey,
|
|
427
|
+
size_bytes: params.sizeBytes,
|
|
428
|
+
title: params.title,
|
|
429
|
+
description: params.description,
|
|
430
|
+
tags: params.tags,
|
|
431
|
+
visibility: params.visibility,
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
plugin: {
|
|
437
|
+
id: result.plugin.id,
|
|
438
|
+
fullName: result.plugin.full_name,
|
|
439
|
+
version: result.plugin.version,
|
|
440
|
+
},
|
|
441
|
+
action: result.action,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 单例导出
|
|
447
|
+
export const api = new ApiClient();
|
package/src/cli.ts
CHANGED
|
@@ -1,27 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 入口
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
authCommand,
|
|
8
|
+
installCommand,
|
|
9
|
+
searchCommand,
|
|
10
|
+
listCommand,
|
|
11
|
+
uninstallCommand,
|
|
12
|
+
completionCommand,
|
|
13
|
+
setupCommand,
|
|
14
|
+
publishCommand,
|
|
15
|
+
checkCommand,
|
|
16
|
+
} from './commands';
|
|
8
17
|
|
|
9
|
-
|
|
18
|
+
const program = new Command();
|
|
10
19
|
|
|
11
20
|
program
|
|
12
21
|
.name('42plugin')
|
|
13
|
-
.description('
|
|
14
|
-
.version('0.1.0'
|
|
15
|
-
.option('--verbose', '详细输出')
|
|
16
|
-
.option('--no-color', '禁用颜色输出');
|
|
22
|
+
.description('活水插件 - AI 插件管理工具')
|
|
23
|
+
.version('0.1.0');
|
|
17
24
|
|
|
18
|
-
//
|
|
25
|
+
// 注册命令
|
|
19
26
|
program.addCommand(authCommand);
|
|
20
|
-
program.addCommand(installCommand);
|
|
21
27
|
program.addCommand(searchCommand);
|
|
28
|
+
program.addCommand(installCommand);
|
|
22
29
|
program.addCommand(listCommand);
|
|
23
30
|
program.addCommand(uninstallCommand);
|
|
24
|
-
program.addCommand(
|
|
31
|
+
program.addCommand(publishCommand);
|
|
32
|
+
program.addCommand(checkCommand);
|
|
33
|
+
program.addCommand(completionCommand);
|
|
34
|
+
program.addCommand(setupCommand);
|
|
35
|
+
|
|
36
|
+
// 版本命令
|
|
37
|
+
program
|
|
38
|
+
.command('version')
|
|
39
|
+
.description('显示版本信息')
|
|
40
|
+
.action(() => {
|
|
41
|
+
console.log('42plugin v0.1.0');
|
|
42
|
+
});
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
program.showHelpAfterError();
|
|
44
|
+
export { program };
|
package/src/commands/auth.ts
CHANGED
|
@@ -1,114 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth 命令 - 认证管理
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { Command } from 'commander';
|
|
2
|
-
import open from 'open';
|
|
3
|
-
import ora from 'ora';
|
|
4
6
|
import chalk from 'chalk';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import open from 'open';
|
|
9
|
+
import { api } from '../api';
|
|
10
|
+
import { getSessionToken, saveSessionToken, clearSessionToken } from '../db';
|
|
7
11
|
|
|
8
12
|
export const authCommand = new Command('auth')
|
|
9
|
-
.description('
|
|
10
|
-
.option('--status', '
|
|
11
|
-
.option('--logout', '
|
|
13
|
+
.description('登录/登出 42plugin')
|
|
14
|
+
.option('--status', '查看登录状态')
|
|
15
|
+
.option('--logout', '登出')
|
|
12
16
|
.action(async (options) => {
|
|
13
|
-
if (options.status) {
|
|
14
|
-
await showStatus();
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
17
|
if (options.logout) {
|
|
19
18
|
await logout();
|
|
20
|
-
|
|
19
|
+
} else if (options.status) {
|
|
20
|
+
await status();
|
|
21
|
+
} else {
|
|
22
|
+
await login();
|
|
21
23
|
}
|
|
22
|
-
|
|
23
|
-
await login();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
async function login() {
|
|
27
|
-
|
|
26
|
+
async function login(): Promise<void> {
|
|
27
|
+
// 检查是否已登录
|
|
28
|
+
const existingToken = await getSessionToken();
|
|
29
|
+
if (existingToken) {
|
|
30
|
+
api.setSessionToken(existingToken);
|
|
31
|
+
try {
|
|
32
|
+
const session = await api.getSession();
|
|
33
|
+
console.log(chalk.yellow(`已登录为 ${session.user.name || session.user.email}`));
|
|
34
|
+
console.log(chalk.gray('如需切换账号,请先执行 42plugin auth --logout'));
|
|
35
|
+
return;
|
|
36
|
+
} catch {
|
|
37
|
+
// Token 无效,继续登录流程
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const spinner = ora('正在请求设备授权码...').start();
|
|
28
42
|
|
|
29
43
|
try {
|
|
30
|
-
|
|
31
|
-
const { code, auth_url, expires_at } = await api.startAuth();
|
|
44
|
+
const deviceCode = await api.requestDeviceCode();
|
|
32
45
|
spinner.stop();
|
|
33
46
|
|
|
34
47
|
console.log();
|
|
35
|
-
console.log(chalk.cyan('
|
|
48
|
+
console.log(chalk.cyan('请在浏览器中完成登录:'));
|
|
36
49
|
console.log();
|
|
37
|
-
console.log('
|
|
38
|
-
console.log(chalk.
|
|
50
|
+
console.log(` ${chalk.bold('验证码')}: ${chalk.yellow.bold(deviceCode.userCode)}`);
|
|
51
|
+
console.log(` ${chalk.bold('链接')}: ${deviceCode.verificationUri}`);
|
|
39
52
|
console.log();
|
|
40
53
|
|
|
41
|
-
//
|
|
42
|
-
|
|
54
|
+
// 尝试打开浏览器
|
|
55
|
+
if (deviceCode.verificationUriComplete) {
|
|
56
|
+
await open(deviceCode.verificationUriComplete).catch(() => {});
|
|
57
|
+
}
|
|
43
58
|
|
|
44
|
-
//
|
|
45
|
-
|
|
59
|
+
// 轮询等待授权
|
|
60
|
+
const pollSpinner = ora('等待授权...').start();
|
|
61
|
+
const interval = (deviceCode.interval || 5) * 1000;
|
|
62
|
+
const expiresAt = Date.now() + deviceCode.expiresIn * 1000;
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
while (Date.now() < expiresAt) {
|
|
65
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
const result = await api.pollAuth(code);
|
|
67
|
+
const tokenResponse = await api.pollDeviceToken(deviceCode.deviceCode);
|
|
52
68
|
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
await saveSecrets({
|
|
56
|
-
access_token: result.access_token!,
|
|
57
|
-
refresh_token: result.refresh_token!,
|
|
58
|
-
created_at: new Date().toISOString(),
|
|
59
|
-
});
|
|
69
|
+
if (tokenResponse.accessToken && tokenResponse.user) {
|
|
70
|
+
pollSpinner.succeed('授权成功!');
|
|
60
71
|
|
|
61
|
-
//
|
|
62
|
-
|
|
72
|
+
// 保存 token
|
|
73
|
+
await saveSessionToken(tokenResponse.accessToken);
|
|
74
|
+
api.setSessionToken(tokenResponse.accessToken);
|
|
63
75
|
|
|
64
|
-
spinner.succeed(chalk.green('登录成功!'));
|
|
65
76
|
console.log();
|
|
66
|
-
console.log(
|
|
77
|
+
console.log(chalk.green(`欢迎,${tokenResponse.user.name || tokenResponse.user.username || tokenResponse.user.email}!`));
|
|
67
78
|
return;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
|
|
81
|
+
if (tokenResponse.error === 'expired_token') {
|
|
82
|
+
pollSpinner.fail('授权码已过期,请重新执行 42plugin auth');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (tokenResponse.error === 'access_denied') {
|
|
87
|
+
pollSpinner.fail('授权被拒绝');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// authorization_pending 继续等待
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
|
|
94
|
+
pollSpinner.fail('授权超时,请重试');
|
|
95
|
+
process.exit(1);
|
|
74
96
|
} catch (error) {
|
|
75
|
-
spinner.fail(
|
|
97
|
+
spinner.fail('登录失败');
|
|
98
|
+
console.error(chalk.red((error as Error).message));
|
|
76
99
|
process.exit(1);
|
|
77
100
|
}
|
|
78
101
|
}
|
|
79
102
|
|
|
80
|
-
async function
|
|
81
|
-
|
|
103
|
+
async function logout(): Promise<void> {
|
|
104
|
+
await clearSessionToken();
|
|
105
|
+
api.setSessionToken(null);
|
|
106
|
+
console.log(chalk.green('已登出'));
|
|
107
|
+
}
|
|
82
108
|
|
|
83
|
-
|
|
109
|
+
async function status(): Promise<void> {
|
|
110
|
+
const token = await getSessionToken();
|
|
111
|
+
|
|
112
|
+
if (!token) {
|
|
84
113
|
console.log(chalk.yellow('未登录'));
|
|
85
|
-
console.log('
|
|
114
|
+
console.log(chalk.gray('执行 42plugin auth 登录'));
|
|
86
115
|
return;
|
|
87
116
|
}
|
|
88
117
|
|
|
118
|
+
api.setSessionToken(token);
|
|
119
|
+
|
|
89
120
|
try {
|
|
90
|
-
const
|
|
121
|
+
const session = await api.getSession();
|
|
91
122
|
console.log(chalk.green('已登录'));
|
|
92
|
-
console.log();
|
|
93
|
-
console.log(`用户名: ${chalk.cyan(user.username)}`);
|
|
94
|
-
console.log(`显示名: ${user.display_name || '-'}`);
|
|
95
|
-
console.log(`角色: ${user.role}`);
|
|
96
|
-
|
|
97
|
-
if (user.role === 'vip' && user.vip_expires_at) {
|
|
98
|
-
console.log(`VIP 到期: ${new Date(user.vip_expires_at).toLocaleDateString()}`);
|
|
99
|
-
}
|
|
123
|
+
console.log(` 用户: ${session.user.name || session.user.email}`);
|
|
100
124
|
} catch {
|
|
101
|
-
console.log(chalk.yellow('
|
|
102
|
-
console.log('
|
|
125
|
+
console.log(chalk.yellow('登录已过期'));
|
|
126
|
+
console.log(chalk.gray('请执行 42plugin auth 重新登录'));
|
|
103
127
|
}
|
|
104
128
|
}
|
|
105
|
-
|
|
106
|
-
async function logout() {
|
|
107
|
-
await clearSecrets();
|
|
108
|
-
api.resetToken();
|
|
109
|
-
console.log(chalk.green('已登出'));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function sleep(ms: number) {
|
|
113
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
114
|
-
}
|