ccjk 2.0.7 → 2.0.9
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.ja.md +6 -6
- package/README.ko.md +5 -5
- package/README.md +55 -9
- package/README.zh-CN.md +9 -9
- package/dist/chunks/claude-code-config-manager.mjs +8 -8
- package/dist/chunks/claude-code-incremental-manager.mjs +2 -2
- package/dist/chunks/codex-config-switch.mjs +4 -4
- package/dist/chunks/codex-provider-manager.mjs +2 -2
- package/dist/chunks/codex-uninstaller.mjs +3 -3
- package/dist/chunks/commands.mjs +2 -2
- package/dist/chunks/features.mjs +11 -11
- package/dist/chunks/plugin-recommendation.mjs +555 -0
- package/dist/chunks/simple-config.mjs +231 -89
- package/dist/chunks/skills-sync.mjs +854 -0
- package/dist/cli.mjs +8187 -1317
- package/dist/i18n/locales/en/agents.json +135 -0
- package/dist/i18n/locales/en/claude-md.json +73 -0
- package/dist/i18n/locales/en/cloudPlugins.json +118 -0
- package/dist/i18n/locales/en/common.json +2 -1
- package/dist/i18n/locales/en/hooksSync.json +111 -0
- package/dist/i18n/locales/en/mcp.json +31 -6
- package/dist/i18n/locales/en/menu.json +60 -1
- package/dist/i18n/locales/en/notification.json +306 -0
- package/dist/i18n/locales/en/plugins.json +133 -0
- package/dist/i18n/locales/en/skillsSync.json +74 -0
- package/dist/i18n/locales/zh-CN/agents.json +135 -0
- package/dist/i18n/locales/zh-CN/claude-md.json +73 -0
- package/dist/i18n/locales/zh-CN/cloudPlugins.json +118 -0
- package/dist/i18n/locales/zh-CN/common.json +2 -1
- package/dist/i18n/locales/zh-CN/hooksSync.json +111 -0
- package/dist/i18n/locales/zh-CN/mcp.json +31 -6
- package/dist/i18n/locales/zh-CN/menu.json +60 -1
- package/dist/i18n/locales/zh-CN/notification.json +306 -0
- package/dist/i18n/locales/zh-CN/plugins.json +133 -0
- package/dist/i18n/locales/zh-CN/skillsSync.json +74 -0
- package/dist/index.d.mts +18 -18
- package/dist/index.d.ts +18 -18
- package/dist/index.mjs +190 -188
- package/package.json +52 -48
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { existsSync, unlinkSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { platform, homedir } from 'node:os';
|
|
3
|
+
import { join } from 'pathe';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT$1 = 3e4;
|
|
6
|
+
const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
7
|
+
const DEFAULT_RETRY_DELAY = 1e3;
|
|
8
|
+
class CloudApiClient {
|
|
9
|
+
baseUrl;
|
|
10
|
+
timeout;
|
|
11
|
+
authToken;
|
|
12
|
+
userAgent;
|
|
13
|
+
retry;
|
|
14
|
+
/**
|
|
15
|
+
* Create a new CloudApiClient instance
|
|
16
|
+
*
|
|
17
|
+
* @param config - Client configuration
|
|
18
|
+
*/
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
21
|
+
this.timeout = config.timeout || DEFAULT_TIMEOUT$1;
|
|
22
|
+
this.authToken = config.authToken;
|
|
23
|
+
this.userAgent = config.userAgent || "CCJK-Client/1.0";
|
|
24
|
+
this.retry = config.retry || {
|
|
25
|
+
maxAttempts: DEFAULT_RETRY_ATTEMPTS,
|
|
26
|
+
delay: DEFAULT_RETRY_DELAY
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// ==========================================================================
|
|
30
|
+
// Configuration Methods
|
|
31
|
+
// ==========================================================================
|
|
32
|
+
/**
|
|
33
|
+
* Set authentication token
|
|
34
|
+
*
|
|
35
|
+
* @param token - Authentication token
|
|
36
|
+
*/
|
|
37
|
+
setAuthToken(token) {
|
|
38
|
+
this.authToken = token;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Clear authentication token
|
|
42
|
+
*/
|
|
43
|
+
clearAuthToken() {
|
|
44
|
+
this.authToken = void 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get current base URL
|
|
48
|
+
*/
|
|
49
|
+
getBaseUrl() {
|
|
50
|
+
return this.baseUrl;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Update base URL
|
|
54
|
+
*
|
|
55
|
+
* @param url - New base URL
|
|
56
|
+
*/
|
|
57
|
+
setBaseUrl(url) {
|
|
58
|
+
this.baseUrl = url.replace(/\/$/, "");
|
|
59
|
+
}
|
|
60
|
+
// ==========================================================================
|
|
61
|
+
// Request Methods
|
|
62
|
+
// ==========================================================================
|
|
63
|
+
/**
|
|
64
|
+
* Make an HTTP request to the cloud service
|
|
65
|
+
*
|
|
66
|
+
* @param path - API endpoint path (e.g., '/plugins/recommend')
|
|
67
|
+
* @param options - Request options
|
|
68
|
+
* @returns API response
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const response = await client.request<{ data: string }>('/api/endpoint', {
|
|
73
|
+
* method: 'POST',
|
|
74
|
+
* body: { key: 'value' },
|
|
75
|
+
* timeout: 5000
|
|
76
|
+
* })
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
async request(path, options) {
|
|
80
|
+
const maxAttempts = options.retry?.maxAttempts || this.retry.maxAttempts;
|
|
81
|
+
const retryDelay = options.retry?.delay || this.retry.delay;
|
|
82
|
+
let lastError = null;
|
|
83
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
return await this.executeRequest(path, options);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
88
|
+
if (attempt === maxAttempts || this.isClientError(lastError)) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
await this.sleep(retryDelay * attempt);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
error: lastError?.message || "Request failed",
|
|
97
|
+
code: "REQUEST_FAILED"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Execute a single HTTP request
|
|
102
|
+
*
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
async executeRequest(path, options) {
|
|
106
|
+
const url = this.buildUrl(path, options.query);
|
|
107
|
+
const timeout = options.timeout || this.timeout;
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
110
|
+
try {
|
|
111
|
+
const headers = this.buildHeaders(options);
|
|
112
|
+
const response = await fetch(url, {
|
|
113
|
+
method: options.method,
|
|
114
|
+
headers,
|
|
115
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
116
|
+
signal: controller.signal
|
|
117
|
+
});
|
|
118
|
+
clearTimeout(timeoutId);
|
|
119
|
+
const data = await response.json();
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
error: data.error || `HTTP ${response.status}: ${response.statusText}`,
|
|
124
|
+
code: data.code || `HTTP_${response.status}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...data,
|
|
129
|
+
success: true
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
clearTimeout(timeoutId);
|
|
133
|
+
if (error instanceof Error) {
|
|
134
|
+
if (error.name === "AbortError") {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: "Request timeout",
|
|
138
|
+
code: "TIMEOUT"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
error: error.message,
|
|
144
|
+
code: "NETWORK_ERROR"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: String(error),
|
|
150
|
+
code: "UNKNOWN_ERROR"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ==========================================================================
|
|
155
|
+
// Convenience Methods
|
|
156
|
+
// ==========================================================================
|
|
157
|
+
/**
|
|
158
|
+
* Make a GET request
|
|
159
|
+
*
|
|
160
|
+
* @param path - API endpoint path
|
|
161
|
+
* @param query - Query parameters
|
|
162
|
+
* @param options - Additional request options
|
|
163
|
+
* @returns API response
|
|
164
|
+
*/
|
|
165
|
+
async get(path, query, options) {
|
|
166
|
+
return this.request(path, {
|
|
167
|
+
method: "GET",
|
|
168
|
+
query,
|
|
169
|
+
...options
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Make a POST request
|
|
174
|
+
*
|
|
175
|
+
* @param path - API endpoint path
|
|
176
|
+
* @param body - Request body
|
|
177
|
+
* @param options - Additional request options
|
|
178
|
+
* @returns API response
|
|
179
|
+
*/
|
|
180
|
+
async post(path, body, options) {
|
|
181
|
+
return this.request(path, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
body,
|
|
184
|
+
...options
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Make a PUT request
|
|
189
|
+
*
|
|
190
|
+
* @param path - API endpoint path
|
|
191
|
+
* @param body - Request body
|
|
192
|
+
* @param options - Additional request options
|
|
193
|
+
* @returns API response
|
|
194
|
+
*/
|
|
195
|
+
async put(path, body, options) {
|
|
196
|
+
return this.request(path, {
|
|
197
|
+
method: "PUT",
|
|
198
|
+
body,
|
|
199
|
+
...options
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Make a DELETE request
|
|
204
|
+
*
|
|
205
|
+
* @param path - API endpoint path
|
|
206
|
+
* @param options - Additional request options
|
|
207
|
+
* @returns API response
|
|
208
|
+
*/
|
|
209
|
+
async delete(path, options) {
|
|
210
|
+
return this.request(path, {
|
|
211
|
+
method: "DELETE",
|
|
212
|
+
...options
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// ==========================================================================
|
|
216
|
+
// Helper Methods
|
|
217
|
+
// ==========================================================================
|
|
218
|
+
/**
|
|
219
|
+
* Build full URL with query parameters
|
|
220
|
+
*
|
|
221
|
+
* @private
|
|
222
|
+
*/
|
|
223
|
+
buildUrl(path, query) {
|
|
224
|
+
const url = `${this.baseUrl}${path}`;
|
|
225
|
+
if (!query || Object.keys(query).length === 0) {
|
|
226
|
+
return url;
|
|
227
|
+
}
|
|
228
|
+
const params = new URLSearchParams();
|
|
229
|
+
for (const [key, value] of Object.entries(query)) {
|
|
230
|
+
params.append(key, String(value));
|
|
231
|
+
}
|
|
232
|
+
return `${url}?${params.toString()}`;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Build request headers
|
|
236
|
+
*
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
buildHeaders(options) {
|
|
240
|
+
const headers = {
|
|
241
|
+
"Content-Type": "application/json",
|
|
242
|
+
"User-Agent": this.userAgent,
|
|
243
|
+
...options.headers
|
|
244
|
+
};
|
|
245
|
+
const token = options.authToken || this.authToken;
|
|
246
|
+
if (token) {
|
|
247
|
+
headers.Authorization = `Bearer ${token}`;
|
|
248
|
+
}
|
|
249
|
+
return headers;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Check if error is a client error (4xx)
|
|
253
|
+
*
|
|
254
|
+
* @private
|
|
255
|
+
*/
|
|
256
|
+
isClientError(error) {
|
|
257
|
+
return error.message.includes("HTTP 4");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Sleep for specified milliseconds
|
|
261
|
+
*
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
sleep(ms) {
|
|
265
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const CLOUD_API_BASE_URL = "https://api.claudehome.cn";
|
|
270
|
+
const CACHE_DIR = join(homedir(), ".ccjk", "cache");
|
|
271
|
+
const CACHE_FILE = join(CACHE_DIR, "plugin-recommendations.json");
|
|
272
|
+
const CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
273
|
+
const DEFAULT_TIMEOUT = 1e4;
|
|
274
|
+
class PluginRecommendationService {
|
|
275
|
+
apiClient;
|
|
276
|
+
fallbackData;
|
|
277
|
+
/**
|
|
278
|
+
* Create a new PluginRecommendationService instance
|
|
279
|
+
*
|
|
280
|
+
* @param baseUrl - Cloud API base URL (default: https://api.claudehome.cn)
|
|
281
|
+
* @param fallbackData - Fallback data for offline mode
|
|
282
|
+
*/
|
|
283
|
+
constructor(baseUrl = CLOUD_API_BASE_URL, fallbackData = []) {
|
|
284
|
+
this.apiClient = new CloudApiClient({
|
|
285
|
+
baseUrl,
|
|
286
|
+
timeout: DEFAULT_TIMEOUT,
|
|
287
|
+
userAgent: "CCJK-PluginRecommendation/1.0"
|
|
288
|
+
});
|
|
289
|
+
this.fallbackData = fallbackData;
|
|
290
|
+
}
|
|
291
|
+
// ==========================================================================
|
|
292
|
+
// Public Methods
|
|
293
|
+
// ==========================================================================
|
|
294
|
+
/**
|
|
295
|
+
* Get plugin recommendations
|
|
296
|
+
*
|
|
297
|
+
* Fetches recommendations from cache if available and valid,
|
|
298
|
+
* otherwise requests from cloud API with fallback to local data.
|
|
299
|
+
*
|
|
300
|
+
* @param request - Recommendation request parameters
|
|
301
|
+
* @returns Recommendation response
|
|
302
|
+
*/
|
|
303
|
+
async getRecommendations(request) {
|
|
304
|
+
const cached = this.getCachedRecommendations(request);
|
|
305
|
+
if (cached) {
|
|
306
|
+
return {
|
|
307
|
+
recommendations: cached.data,
|
|
308
|
+
total: cached.data.length,
|
|
309
|
+
fromCache: true,
|
|
310
|
+
cacheTimestamp: cached.timestamp
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const response = await this.fetchFromCloud(request);
|
|
315
|
+
if (response.success && response.data) {
|
|
316
|
+
this.cacheRecommendations(request, response.data.recommendations);
|
|
317
|
+
return {
|
|
318
|
+
recommendations: response.data.recommendations,
|
|
319
|
+
total: response.data.total || response.data.recommendations.length,
|
|
320
|
+
fromCache: false,
|
|
321
|
+
algorithmVersion: response.data.algorithmVersion
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.warn("Cloud API request failed:", error);
|
|
326
|
+
}
|
|
327
|
+
const localRecommendations = this.getLocalRecommendations(request);
|
|
328
|
+
this.cacheRecommendations(request, localRecommendations);
|
|
329
|
+
return {
|
|
330
|
+
recommendations: localRecommendations,
|
|
331
|
+
total: localRecommendations.length,
|
|
332
|
+
fromCache: false
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Clear recommendation cache
|
|
337
|
+
*
|
|
338
|
+
* Removes all cached recommendations.
|
|
339
|
+
*/
|
|
340
|
+
clearCache() {
|
|
341
|
+
try {
|
|
342
|
+
if (existsSync(CACHE_FILE)) {
|
|
343
|
+
unlinkSync(CACHE_FILE);
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.warn("Failed to clear cache:", error);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get cache status
|
|
351
|
+
*
|
|
352
|
+
* Returns information about the current cache state.
|
|
353
|
+
*/
|
|
354
|
+
getCacheStatus() {
|
|
355
|
+
try {
|
|
356
|
+
if (!existsSync(CACHE_FILE)) {
|
|
357
|
+
return { exists: false };
|
|
358
|
+
}
|
|
359
|
+
const data = readFileSync(CACHE_FILE, "utf-8");
|
|
360
|
+
const cached = JSON.parse(data);
|
|
361
|
+
const isValid = new Date(cached.expiresAt).getTime() > Date.now();
|
|
362
|
+
return {
|
|
363
|
+
exists: true,
|
|
364
|
+
timestamp: cached.timestamp,
|
|
365
|
+
expiresAt: cached.expiresAt,
|
|
366
|
+
isValid
|
|
367
|
+
};
|
|
368
|
+
} catch {
|
|
369
|
+
return { exists: false };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Set fallback data
|
|
374
|
+
*
|
|
375
|
+
* Updates the local fallback data used when cloud API is unavailable.
|
|
376
|
+
*
|
|
377
|
+
* @param data - Fallback plugin recommendations
|
|
378
|
+
*/
|
|
379
|
+
setFallbackData(data) {
|
|
380
|
+
this.fallbackData = data;
|
|
381
|
+
}
|
|
382
|
+
// ==========================================================================
|
|
383
|
+
// Private Methods - Cache Management
|
|
384
|
+
// ==========================================================================
|
|
385
|
+
/**
|
|
386
|
+
* Get cached recommendations if valid
|
|
387
|
+
*
|
|
388
|
+
* @private
|
|
389
|
+
*/
|
|
390
|
+
getCachedRecommendations(request) {
|
|
391
|
+
try {
|
|
392
|
+
if (!existsSync(CACHE_FILE)) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const data = readFileSync(CACHE_FILE, "utf-8");
|
|
396
|
+
const cached = JSON.parse(data);
|
|
397
|
+
if (new Date(cached.expiresAt).getTime() < Date.now()) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
if (!this.requestMatches(cached.request, request)) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
return cached;
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Cache recommendations to disk
|
|
410
|
+
*
|
|
411
|
+
* @private
|
|
412
|
+
*/
|
|
413
|
+
cacheRecommendations(request, recommendations) {
|
|
414
|
+
try {
|
|
415
|
+
if (!existsSync(CACHE_DIR)) {
|
|
416
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
417
|
+
}
|
|
418
|
+
const now = /* @__PURE__ */ new Date();
|
|
419
|
+
const expiresAt = new Date(now.getTime() + CACHE_TTL);
|
|
420
|
+
const cached = {
|
|
421
|
+
data: recommendations,
|
|
422
|
+
timestamp: now.toISOString(),
|
|
423
|
+
request,
|
|
424
|
+
expiresAt: expiresAt.toISOString()
|
|
425
|
+
};
|
|
426
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cached, null, 2), "utf-8");
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.warn("Failed to cache recommendations:", error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Check if two requests match for caching purposes
|
|
433
|
+
*
|
|
434
|
+
* @private
|
|
435
|
+
*/
|
|
436
|
+
requestMatches(cached, current) {
|
|
437
|
+
return cached.os === current.os && cached.codeTool === current.codeTool && cached.preferredLang === current.preferredLang && cached.category === current.category && JSON.stringify(cached.userTags || []) === JSON.stringify(current.userTags || []);
|
|
438
|
+
}
|
|
439
|
+
// ==========================================================================
|
|
440
|
+
// Private Methods - Cloud API
|
|
441
|
+
// ==========================================================================
|
|
442
|
+
/**
|
|
443
|
+
* Fetch recommendations from cloud API
|
|
444
|
+
*
|
|
445
|
+
* @private
|
|
446
|
+
*/
|
|
447
|
+
async fetchFromCloud(request) {
|
|
448
|
+
return this.apiClient.post("/plugins/recommend", {
|
|
449
|
+
os: request.os,
|
|
450
|
+
codeTool: request.codeTool,
|
|
451
|
+
installedPlugins: request.installedPlugins,
|
|
452
|
+
preferredLang: request.preferredLang,
|
|
453
|
+
userTags: request.userTags,
|
|
454
|
+
category: request.category,
|
|
455
|
+
limit: request.limit || 10
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
// ==========================================================================
|
|
459
|
+
// Private Methods - Local Fallback
|
|
460
|
+
// ==========================================================================
|
|
461
|
+
/**
|
|
462
|
+
* Get recommendations from local fallback data
|
|
463
|
+
*
|
|
464
|
+
* Filters and scores local plugins based on request parameters.
|
|
465
|
+
*
|
|
466
|
+
* @private
|
|
467
|
+
*/
|
|
468
|
+
getLocalRecommendations(request) {
|
|
469
|
+
let recommendations = [...this.fallbackData];
|
|
470
|
+
recommendations = recommendations.filter(
|
|
471
|
+
(plugin) => plugin.compatibility.os.includes(request.os) || plugin.compatibility.os.includes("all")
|
|
472
|
+
);
|
|
473
|
+
recommendations = recommendations.filter(
|
|
474
|
+
(plugin) => plugin.compatibility.codeTools.includes(request.codeTool) || plugin.compatibility.codeTools.includes("all")
|
|
475
|
+
);
|
|
476
|
+
if (request.category) {
|
|
477
|
+
recommendations = recommendations.filter(
|
|
478
|
+
(plugin) => plugin.category === request.category
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (request.userTags && request.userTags.length > 0) {
|
|
482
|
+
recommendations = recommendations.filter(
|
|
483
|
+
(plugin) => request.userTags.some((tag) => plugin.tags.includes(tag))
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
recommendations = recommendations.filter(
|
|
487
|
+
(plugin) => !request.installedPlugins.includes(plugin.id)
|
|
488
|
+
);
|
|
489
|
+
recommendations = recommendations.map((plugin) => {
|
|
490
|
+
const score = this.calculateRecommendationScore(plugin, request);
|
|
491
|
+
return {
|
|
492
|
+
...plugin,
|
|
493
|
+
recommendationScore: score
|
|
494
|
+
};
|
|
495
|
+
});
|
|
496
|
+
recommendations.sort(
|
|
497
|
+
(a, b) => (b.recommendationScore || 0) - (a.recommendationScore || 0)
|
|
498
|
+
);
|
|
499
|
+
const limit = request.limit || 10;
|
|
500
|
+
return recommendations.slice(0, limit);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Calculate recommendation score for a plugin
|
|
504
|
+
*
|
|
505
|
+
* Combines popularity, rating, and relevance to generate a score.
|
|
506
|
+
*
|
|
507
|
+
* @private
|
|
508
|
+
*/
|
|
509
|
+
calculateRecommendationScore(plugin, request) {
|
|
510
|
+
let score = 0;
|
|
511
|
+
score += plugin.popularity / 100 * 40;
|
|
512
|
+
score += plugin.rating / 5 * 30;
|
|
513
|
+
if (request.userTags && request.userTags.length > 0) {
|
|
514
|
+
const matchingTags = plugin.tags.filter(
|
|
515
|
+
(tag) => request.userTags.includes(tag)
|
|
516
|
+
).length;
|
|
517
|
+
score += matchingTags / request.userTags.length * 20;
|
|
518
|
+
} else {
|
|
519
|
+
score += 10;
|
|
520
|
+
}
|
|
521
|
+
if (plugin.verified) {
|
|
522
|
+
score += 10;
|
|
523
|
+
}
|
|
524
|
+
return Math.round(score);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
let serviceInstance = null;
|
|
528
|
+
function getPluginRecommendationService(fallbackData) {
|
|
529
|
+
if (!serviceInstance) {
|
|
530
|
+
serviceInstance = new PluginRecommendationService(
|
|
531
|
+
CLOUD_API_BASE_URL,
|
|
532
|
+
fallbackData
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
return serviceInstance;
|
|
536
|
+
}
|
|
537
|
+
async function getRecommendations(request) {
|
|
538
|
+
const service = getPluginRecommendationService();
|
|
539
|
+
return service.getRecommendations(request);
|
|
540
|
+
}
|
|
541
|
+
function getCurrentPlatform() {
|
|
542
|
+
const p = platform();
|
|
543
|
+
switch (p) {
|
|
544
|
+
case "darwin":
|
|
545
|
+
return "darwin";
|
|
546
|
+
case "win32":
|
|
547
|
+
return "win32";
|
|
548
|
+
case "linux":
|
|
549
|
+
return "linux";
|
|
550
|
+
default:
|
|
551
|
+
return "linux";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export { PluginRecommendationService, getCurrentPlatform, getPluginRecommendationService, getRecommendations };
|