mioku-service-webui 2.0.0 → 2.1.0
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/package.json +1 -1
- package/routes/store.ts +20 -271
package/package.json
CHANGED
package/routes/store.ts
CHANGED
|
@@ -6,12 +6,7 @@ const OFFICIAL_REGISTRY_PATH = path.join(
|
|
|
6
6
|
process.cwd(),
|
|
7
7
|
"official-registry.json",
|
|
8
8
|
);
|
|
9
|
-
const NPM_SEARCH_URL = "https://registry.npmjs.org/-/v1/search";
|
|
10
9
|
const NPM_PACKAGE_URL = "https://registry.npmjs.org";
|
|
11
|
-
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
12
|
-
const DEFAULT_LIMIT = 20;
|
|
13
|
-
const MAX_LIMIT = 50;
|
|
14
|
-
const DEFAULT_SEARCH_SIZE = 100;
|
|
15
10
|
const SERVICE_ROOT = "src/services";
|
|
16
11
|
|
|
17
12
|
interface OfficialPluginEntry {
|
|
@@ -29,66 +24,16 @@ interface OfficialRegistry {
|
|
|
29
24
|
services?: Record<string, OfficialServiceEntry>;
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
interface NpmPackageLinks {
|
|
33
|
-
npm?: string;
|
|
34
|
-
homepage?: string;
|
|
35
|
-
repository?: string;
|
|
36
|
-
bugs?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface NpmSearchPackage {
|
|
40
|
-
name: string;
|
|
41
|
-
version?: string;
|
|
42
|
-
description?: string;
|
|
43
|
-
date?: string;
|
|
44
|
-
keywords?: string[];
|
|
45
|
-
links?: NpmPackageLinks;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface NpmSearchObject {
|
|
49
|
-
package: NpmSearchPackage;
|
|
50
|
-
score?: Record<string, number>;
|
|
51
|
-
searchScore?: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface StoreItem {
|
|
55
|
-
name: string;
|
|
56
|
-
npm: string;
|
|
57
|
-
type: "plugin" | "service";
|
|
58
|
-
description: string;
|
|
59
|
-
version: string;
|
|
60
|
-
keywords: string[];
|
|
61
|
-
tags: string[];
|
|
62
|
-
official: boolean;
|
|
63
|
-
builtin: boolean;
|
|
64
|
-
repo: string;
|
|
65
|
-
homepage: string;
|
|
66
|
-
npmUrl: string;
|
|
67
|
-
date: string;
|
|
68
|
-
searchScore: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface StoreListResponse {
|
|
72
|
-
items: StoreItem[];
|
|
73
|
-
total: number;
|
|
74
|
-
offset: number;
|
|
75
|
-
limit: number;
|
|
76
|
-
hasMore: boolean;
|
|
77
|
-
q: string;
|
|
78
|
-
type: "plugin" | "service" | "all";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
27
|
let cachedOfficialRegistry: OfficialRegistry | null = null;
|
|
82
28
|
let officialRegistryCachedAt = 0;
|
|
83
|
-
const
|
|
84
|
-
const npmPackageCache = new Map<string, { cachedAt: number; data: any }>();
|
|
29
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
85
30
|
|
|
86
|
-
function isCacheFresh(
|
|
87
|
-
return Date.now() -
|
|
31
|
+
function isCacheFresh(): boolean {
|
|
32
|
+
return Date.now() - officialRegistryCachedAt < CACHE_TTL_MS;
|
|
88
33
|
}
|
|
89
34
|
|
|
90
35
|
function readOfficialRegistry(force = false): OfficialRegistry {
|
|
91
|
-
if (!force && cachedOfficialRegistry && isCacheFresh(
|
|
36
|
+
if (!force && cachedOfficialRegistry && isCacheFresh()) {
|
|
92
37
|
return cachedOfficialRegistry;
|
|
93
38
|
}
|
|
94
39
|
|
|
@@ -103,34 +48,6 @@ function readOfficialRegistry(force = false): OfficialRegistry {
|
|
|
103
48
|
}
|
|
104
49
|
}
|
|
105
50
|
|
|
106
|
-
function normalizeType(input: string | undefined): "plugin" | "service" | "all" {
|
|
107
|
-
if (input === "plugin" || input === "service" || input === "all") {
|
|
108
|
-
return input;
|
|
109
|
-
}
|
|
110
|
-
return "all";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function parsePositiveInt(input: string | undefined, fallback: number): number {
|
|
114
|
-
const value = Number(input);
|
|
115
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
116
|
-
return fallback;
|
|
117
|
-
}
|
|
118
|
-
return Math.floor(value);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function normalizeLimit(input: string | undefined): number {
|
|
122
|
-
const value = parsePositiveInt(input, DEFAULT_LIMIT);
|
|
123
|
-
return Math.min(Math.max(value, 1), MAX_LIMIT);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function normalizeOffset(input: string | undefined): number {
|
|
127
|
-
return parsePositiveInt(input, 0);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function normalizeQuery(input: string | undefined): string {
|
|
131
|
-
return String(input || "").trim();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
51
|
function buildOfficialMaps(registry: OfficialRegistry): {
|
|
135
52
|
plugins: Map<string, OfficialPluginEntry>;
|
|
136
53
|
services: Map<string, OfficialServiceEntry>;
|
|
@@ -153,17 +70,13 @@ function buildOfficialMaps(registry: OfficialRegistry): {
|
|
|
153
70
|
return { plugins, services };
|
|
154
71
|
}
|
|
155
72
|
|
|
156
|
-
function isPluginPackage(name: string): boolean {
|
|
157
|
-
return name.startsWith("mioku-plugin-");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
73
|
function isServicePackage(name: string): boolean {
|
|
161
74
|
return name.startsWith("mioku-service-");
|
|
162
75
|
}
|
|
163
76
|
|
|
164
77
|
function inferTypeFromPackageName(name: string): "plugin" | "service" | null {
|
|
165
|
-
if (
|
|
166
|
-
if (
|
|
78
|
+
if (name.startsWith("mioku-plugin-")) return "plugin";
|
|
79
|
+
if (name.startsWith("mioku-service-")) return "service";
|
|
167
80
|
return null;
|
|
168
81
|
}
|
|
169
82
|
|
|
@@ -208,46 +121,6 @@ function normalizeRepositoryUrl(repository: any): string {
|
|
|
208
121
|
return url.replace(/\.git$/, "");
|
|
209
122
|
}
|
|
210
123
|
|
|
211
|
-
function scoreStoreItem(item: StoreItem, query: string): number {
|
|
212
|
-
const q = query.toLowerCase();
|
|
213
|
-
if (!q) return item.searchScore || 0;
|
|
214
|
-
|
|
215
|
-
const fields = [
|
|
216
|
-
item.name.toLowerCase(),
|
|
217
|
-
item.npm.toLowerCase(),
|
|
218
|
-
item.description.toLowerCase(),
|
|
219
|
-
...item.keywords.map((keyword) => keyword.toLowerCase()),
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
let score = item.searchScore || 0;
|
|
223
|
-
for (const field of fields) {
|
|
224
|
-
if (field === q) score += 200;
|
|
225
|
-
else if (field.startsWith(q)) score += 80;
|
|
226
|
-
else if (field.includes(q)) score += 30;
|
|
227
|
-
}
|
|
228
|
-
if (item.official) score += 10;
|
|
229
|
-
return score;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function compareStoreItems(a: StoreItem, b: StoreItem, query: string): number {
|
|
233
|
-
if (a.official !== b.official) {
|
|
234
|
-
return a.official ? -1 : 1;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const scoreDiff = scoreStoreItem(b, query) - scoreStoreItem(a, query);
|
|
238
|
-
if (scoreDiff !== 0) {
|
|
239
|
-
return scoreDiff;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const dateA = a.date ? new Date(a.date).getTime() : 0;
|
|
243
|
-
const dateB = b.date ? new Date(b.date).getTime() : 0;
|
|
244
|
-
if (dateA !== dateB) {
|
|
245
|
-
return dateB - dateA;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return a.npm.localeCompare(b.npm);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
124
|
async function fetchJson(url: string): Promise<any> {
|
|
252
125
|
const res = await fetch(url, {
|
|
253
126
|
headers: {
|
|
@@ -261,123 +134,15 @@ async function fetchJson(url: string): Promise<any> {
|
|
|
261
134
|
return res.json();
|
|
262
135
|
}
|
|
263
136
|
|
|
264
|
-
async function
|
|
265
|
-
|
|
266
|
-
const cached = npmSearchCache.get(cacheKey);
|
|
267
|
-
if (!force && cached && isCacheFresh(cached.cachedAt)) {
|
|
268
|
-
return cached.data;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const terms: string[] = ["mioku"];
|
|
272
|
-
if (query) terms.push(query);
|
|
273
|
-
|
|
274
|
-
const url = new URL(NPM_SEARCH_URL);
|
|
275
|
-
url.searchParams.set("text", terms.join(" "));
|
|
276
|
-
url.searchParams.set("size", String(DEFAULT_SEARCH_SIZE));
|
|
277
|
-
|
|
278
|
-
const data = await fetchJson(url.toString());
|
|
279
|
-
const objects = Array.isArray(data?.objects) ? (data.objects as NpmSearchObject[]) : [];
|
|
280
|
-
npmSearchCache.set(cacheKey, { cachedAt: Date.now(), data: objects });
|
|
281
|
-
return objects;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async function fetchNpmPackage(name: string, force = false): Promise<any> {
|
|
285
|
-
const cached = npmPackageCache.get(name);
|
|
286
|
-
if (!force && cached && isCacheFresh(cached.cachedAt)) {
|
|
287
|
-
return cached.data;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const data = await fetchJson(`${NPM_PACKAGE_URL}/${encodeURIComponent(name)}`);
|
|
291
|
-
npmPackageCache.set(name, { cachedAt: Date.now(), data });
|
|
292
|
-
return data;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function toStoreItem(
|
|
296
|
-
pkg: NpmSearchPackage,
|
|
297
|
-
officialMaps: ReturnType<typeof buildOfficialMaps>,
|
|
298
|
-
): StoreItem | null {
|
|
299
|
-
const npm = String(pkg?.name || "").trim();
|
|
300
|
-
const type = inferTypeFromPackageName(npm);
|
|
301
|
-
if (!npm || !type) return null;
|
|
302
|
-
|
|
303
|
-
const keywords = normalizeKeywords(pkg.keywords);
|
|
304
|
-
const officialEntry = type === "plugin" ? officialMaps.plugins.get(npm) : officialMaps.services.get(npm);
|
|
305
|
-
|
|
306
|
-
return {
|
|
307
|
-
name: inferDisplayName(npm, type),
|
|
308
|
-
npm,
|
|
309
|
-
type,
|
|
310
|
-
description: String(pkg.description || "").trim(),
|
|
311
|
-
version: String(pkg.version || "").trim(),
|
|
312
|
-
keywords,
|
|
313
|
-
tags: extractTags(keywords),
|
|
314
|
-
official: Boolean(officialEntry),
|
|
315
|
-
builtin: Boolean(officialEntry?.builtin),
|
|
316
|
-
repo: normalizeRepositoryUrl(pkg.links?.repository),
|
|
317
|
-
homepage: String(pkg.links?.homepage || "").trim(),
|
|
318
|
-
npmUrl: String(pkg.links?.npm || `https://www.npmjs.com/package/${npm}`),
|
|
319
|
-
date: String(pkg.date || "").trim(),
|
|
320
|
-
searchScore: Number(pkg ? 0 : 0),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function getStoreList(params: {
|
|
325
|
-
type: "plugin" | "service" | "all";
|
|
326
|
-
query: string;
|
|
327
|
-
offset: number;
|
|
328
|
-
limit: number;
|
|
329
|
-
force?: boolean;
|
|
330
|
-
}): Promise<StoreListResponse> {
|
|
331
|
-
const officialRegistry = readOfficialRegistry(params.force);
|
|
332
|
-
const officialMaps = buildOfficialMaps(officialRegistry);
|
|
333
|
-
const searchResults = await searchNpmPackages(params.type, params.query, params.force);
|
|
334
|
-
|
|
335
|
-
const items = searchResults
|
|
336
|
-
.map((entry) => {
|
|
337
|
-
const item = toStoreItem(entry.package, officialMaps);
|
|
338
|
-
if (!item) return null;
|
|
339
|
-
return {
|
|
340
|
-
...item,
|
|
341
|
-
searchScore: Number(entry.searchScore || entry.score?.final || 0),
|
|
342
|
-
};
|
|
343
|
-
})
|
|
344
|
-
.filter((item): item is StoreItem => Boolean(item))
|
|
345
|
-
.filter((item) => {
|
|
346
|
-
if (params.type !== "all" && item.type !== params.type) {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
if (!params.query) return true;
|
|
350
|
-
const q = params.query.toLowerCase();
|
|
351
|
-
return (
|
|
352
|
-
item.name.toLowerCase().includes(q) ||
|
|
353
|
-
item.npm.toLowerCase().includes(q) ||
|
|
354
|
-
item.description.toLowerCase().includes(q) ||
|
|
355
|
-
item.keywords.some((keyword) => keyword.toLowerCase().includes(q))
|
|
356
|
-
);
|
|
357
|
-
})
|
|
358
|
-
.sort((a, b) => compareStoreItems(a, b, params.query));
|
|
359
|
-
|
|
360
|
-
const total = items.length;
|
|
361
|
-
const sliced = items.slice(params.offset, params.offset + params.limit);
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
items: sliced,
|
|
365
|
-
total,
|
|
366
|
-
offset: params.offset,
|
|
367
|
-
limit: params.limit,
|
|
368
|
-
hasMore: params.offset + params.limit < total,
|
|
369
|
-
q: params.query,
|
|
370
|
-
type: params.type,
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function inferManagedTargetFromPackage(name: string): "plugin" | "service" {
|
|
375
|
-
return isServicePackage(name) ? "service" : "plugin";
|
|
137
|
+
async function fetchNpmPackage(name: string): Promise<any> {
|
|
138
|
+
return fetchJson(`${NPM_PACKAGE_URL}/${encodeURIComponent(name)}`);
|
|
376
139
|
}
|
|
377
140
|
|
|
378
141
|
function getServiceInstallPath(pkg: any, packageName: string): string {
|
|
379
142
|
const mioku = pkg?.mioku;
|
|
380
|
-
const serviceName = String(
|
|
143
|
+
const serviceName = String(
|
|
144
|
+
mioku?.serviceName || inferDisplayName(packageName, "service") || "",
|
|
145
|
+
).trim();
|
|
381
146
|
if (!serviceName) {
|
|
382
147
|
return "";
|
|
383
148
|
}
|
|
@@ -397,27 +162,6 @@ export function createStoreRoutes() {
|
|
|
397
162
|
}
|
|
398
163
|
});
|
|
399
164
|
|
|
400
|
-
app.get("/search", async (c) => {
|
|
401
|
-
const force = c.req.query("force") === "1";
|
|
402
|
-
const query = normalizeQuery(c.req.query("q"));
|
|
403
|
-
const type = normalizeType(c.req.query("type"));
|
|
404
|
-
const offset = normalizeOffset(c.req.query("offset"));
|
|
405
|
-
const limit = normalizeLimit(c.req.query("limit"));
|
|
406
|
-
|
|
407
|
-
try {
|
|
408
|
-
const result = await getStoreList({
|
|
409
|
-
type,
|
|
410
|
-
query,
|
|
411
|
-
offset,
|
|
412
|
-
limit,
|
|
413
|
-
force,
|
|
414
|
-
});
|
|
415
|
-
return c.json({ ok: true, data: result });
|
|
416
|
-
} catch (error: any) {
|
|
417
|
-
return c.json({ ok: false, error: error?.message || "FETCH_FAILED" }, 502);
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
|
|
421
165
|
app.get("/package/:name", async (c) => {
|
|
422
166
|
const force = c.req.query("force") === "1";
|
|
423
167
|
const packageName = String(c.req.param("name") || "").trim();
|
|
@@ -428,7 +172,7 @@ export function createStoreRoutes() {
|
|
|
428
172
|
try {
|
|
429
173
|
const officialRegistry = readOfficialRegistry(force);
|
|
430
174
|
const officialMaps = buildOfficialMaps(officialRegistry);
|
|
431
|
-
const data = await fetchNpmPackage(packageName
|
|
175
|
+
const data = await fetchNpmPackage(packageName);
|
|
432
176
|
const latestVersion = String(data?.["dist-tags"]?.latest || "").trim();
|
|
433
177
|
const latest = latestVersion ? data?.versions?.[latestVersion] || {} : {};
|
|
434
178
|
const type = inferTypeFromPackageName(packageName);
|
|
@@ -437,7 +181,10 @@ export function createStoreRoutes() {
|
|
|
437
181
|
}
|
|
438
182
|
|
|
439
183
|
const keywords = normalizeKeywords(latest?.keywords);
|
|
440
|
-
const officialEntry =
|
|
184
|
+
const officialEntry =
|
|
185
|
+
type === "plugin"
|
|
186
|
+
? officialMaps.plugins.get(packageName)
|
|
187
|
+
: officialMaps.services.get(packageName);
|
|
441
188
|
const repositoryUrl = normalizeRepositoryUrl(latest?.repository);
|
|
442
189
|
|
|
443
190
|
return c.json({
|
|
@@ -458,8 +205,10 @@ export function createStoreRoutes() {
|
|
|
458
205
|
readme: String(data?.readme || "").trim(),
|
|
459
206
|
license: String(latest?.license || "").trim(),
|
|
460
207
|
dependencies: latest?.dependencies || {},
|
|
461
|
-
requiredServices: Array.isArray(latest?.mioku?.services)
|
|
462
|
-
|
|
208
|
+
requiredServices: Array.isArray(latest?.mioku?.services)
|
|
209
|
+
? latest.mioku.services
|
|
210
|
+
: [],
|
|
211
|
+
installTarget: isServicePackage(packageName) ? "service" : "plugin",
|
|
463
212
|
installPath:
|
|
464
213
|
type === "plugin"
|
|
465
214
|
? `plugins/${inferDisplayName(packageName, "plugin")}`
|