|
1 | 1 | // plugin-market.ts — 插件市场数据获取 & 解析 |
| 2 | +import { existsSync, readFileSync, writeFileSync } from 'fs' |
| 3 | +import { join } from 'path' |
| 4 | +import { homedir } from 'os' |
2 | 5 | import { ofetch } from 'ofetch' |
| 6 | +import { basename } from 'path' |
3 | 7 | import type { PluginData } from '../types/plugin.js' |
| 8 | +import { inferPluginType } from '../types/plugin.js' |
4 | 9 |
|
5 | 10 | const PLUGIN_DATA_URL = |
6 | 11 | 'https://raw.githubusercontent.com/fastapi-practices/plugins/refs/heads/master/plugins-data.ts' |
7 | 12 |
|
8 | | -/** |
9 | | - * 从远程获取并解析插件市场数据 |
10 | | - */ |
11 | | -export async function fetchPluginMarketData(): Promise<PluginData[]> { |
12 | | - const content = await ofetch(PLUGIN_DATA_URL, { responseType: 'text' }) |
| 13 | +const CACHE_PATH = join(homedir(), '.fba-plugins-cache.json') |
| 14 | + |
| 15 | +// ─── 缓存 ─── |
13 | 16 |
|
14 | | - // 提取 pluginDataList 数组 JSON |
15 | | - const match = content.match(/pluginDataList[^=]*=\s*(\[[\s\S]*\])/) |
16 | | - if (!match?.[1]) { |
17 | | - throw new Error('Failed to parse plugin market data: pluginDataList not found') |
| 17 | +function readCache(): PluginData[] | null { |
| 18 | + if (!existsSync(CACHE_PATH)) return null |
| 19 | + try { |
| 20 | + return JSON.parse(readFileSync(CACHE_PATH, 'utf-8')) as PluginData[] |
| 21 | + } catch { |
| 22 | + return null |
18 | 23 | } |
| 24 | +} |
19 | 25 |
|
| 26 | +function writeCache(data: PluginData[]): void { |
20 | 27 | try { |
21 | | - // TS 文件中的值本身就是合法 JSON |
22 | | - return JSON.parse(match[1]) as PluginData[] |
23 | | - } catch (e) { |
24 | | - throw new Error(`Failed to parse plugin data JSON: ${e}`) |
| 28 | + writeFileSync(CACHE_PATH, JSON.stringify(data), 'utf-8') |
| 29 | + } catch { |
| 30 | + // 缓存写入失败不影响主流程 |
25 | 31 | } |
26 | 32 | } |
27 | 33 |
|
| 34 | +// ─── 数据获取 ─── |
| 35 | + |
28 | 36 | /** |
29 | | - * 按关键词搜索插件 |
| 37 | + * 从远程获取并解析插件市场数据,失败时回退到本地缓存 |
| 38 | + * |
| 39 | + * @returns `{ data, fromCache }` — fromCache 为 true 时说明使用了缓存 |
30 | 40 | */ |
31 | | -export function searchPlugins(plugins: PluginData[], query: string): PluginData[] { |
32 | | - if (!query.trim()) return plugins |
33 | | - const q = query.toLowerCase() |
34 | | - return plugins.filter(p => { |
35 | | - const plugin = p.plugin |
36 | | - return ( |
37 | | - plugin.summary.toLowerCase().includes(q) || |
38 | | - plugin.description.toLowerCase().includes(q) || |
39 | | - plugin.author.toLowerCase().includes(q) || |
40 | | - plugin.tags?.some(t => t.toLowerCase().includes(q)) |
41 | | - ) |
42 | | - }) |
| 41 | +export async function fetchPluginMarketData(): Promise<{ data: PluginData[]; fromCache: boolean }> { |
| 42 | + try { |
| 43 | + const content = await ofetch(PLUGIN_DATA_URL, { responseType: 'text' }) |
| 44 | + |
| 45 | + const match = content.match(/pluginDataList[^=]*=\s*(\[[\s\S]*\])/) |
| 46 | + if (!match?.[1]) { |
| 47 | + throw new Error('pluginDataList not found') |
| 48 | + } |
| 49 | + |
| 50 | + const data = JSON.parse(match[1]) as PluginData[] |
| 51 | + writeCache(data) |
| 52 | + return { data, fromCache: false } |
| 53 | + } catch { |
| 54 | + const cached = readCache() |
| 55 | + if (cached) return { data: cached, fromCache: true } |
| 56 | + throw new Error('Failed to fetch plugin market data and no local cache available') |
| 57 | + } |
43 | 58 | } |
44 | 59 |
|
45 | 60 | /** |
46 | | - * 按 tag 过滤插件 |
| 61 | + * 按 type 过滤插件(基于 git.path 名称推断类型) |
47 | 62 | */ |
48 | | -export function filterByTag(plugins: PluginData[], tag: string): PluginData[] { |
49 | | - if (!tag || tag === 'all') return plugins |
50 | | - return plugins.filter(p => p.plugin.tags?.includes(tag as any)) |
| 63 | +export function filterByType(plugins: PluginData[], type: string): PluginData[] { |
| 64 | + if (!type || type === 'all') return plugins |
| 65 | + return plugins.filter(p => { |
| 66 | + const name = basename(p.git.path) |
| 67 | + return inferPluginType(name) === type |
| 68 | + }) |
51 | 69 | } |
52 | 70 |
|
53 | 71 | /** |
54 | | - * 按 type 过滤插件 |
| 72 | + * 根据市场插件的 git.path 推断插件类型 |
55 | 73 | */ |
56 | | -export function filterByType(plugins: PluginData[], type: string): PluginData[] { |
57 | | - if (!type || type === 'all') return plugins |
58 | | - return plugins.filter(p => p.plugin.type === type) |
| 74 | +export function getMarketPluginType(p: PluginData): 'web' | 'server' { |
| 75 | + return inferPluginType(basename(p.git.path)) |
59 | 76 | } |
0 commit comments