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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/routes/store.ts +20 -271
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "keywords": [
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 npmSearchCache = new Map<string, { cachedAt: number; data: NpmSearchObject[] }>();
84
- const npmPackageCache = new Map<string, { cachedAt: number; data: any }>();
29
+ const CACHE_TTL_MS = 10 * 60 * 1000;
85
30
 
86
- function isCacheFresh(cachedAt: number): boolean {
87
- return Date.now() - cachedAt < CACHE_TTL_MS;
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(officialRegistryCachedAt)) {
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 (isPluginPackage(name)) return "plugin";
166
- if (isServicePackage(name)) return "service";
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 searchNpmPackages(type: "plugin" | "service" | "all", query: string, force = false): Promise<NpmSearchObject[]> {
265
- const cacheKey = `${type}:${query}`;
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(mioku?.serviceName || inferDisplayName(packageName, "service") || "").trim();
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, force);
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 = type === "plugin" ? officialMaps.plugins.get(packageName) : officialMaps.services.get(packageName);
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) ? latest.mioku.services : [],
462
- installTarget: inferManagedTargetFromPackage(packageName),
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")}`