feat: add plugin marketplace renderer

This commit is contained in:
2026-08-28 18:41:23 +08:00
parent 8b6824a8ba
commit 97c9ad1aad
27 changed files with 1585 additions and 23 deletions

View File

@@ -0,0 +1,253 @@
import { createStore, type StoreApi } from 'zustand';
import { useStore } from 'zustand';
import {
acquireMarketplacePlugin,
installMarketplacePlugin,
readMarketplaceCatalog,
readMarketplaceDetail,
readMarketplaceLibrary,
removeMarketplacePlugin,
uninstallMarketplacePlugin,
updateMarketplacePlugin,
type MarketplaceCatalogPage,
type MarketplaceCatalogQuery,
type MarketplaceInstallation,
type MarketplaceLibraryProjection,
type MarketplaceLibrarySnapshot,
type MarketplacePluginDetail,
} from '@/lib/plugin-marketplace';
type LoadState = 'idle' | 'loading' | 'ready' | 'error';
export type PluginMarketplaceDependencies = {
readCatalog(input?: MarketplaceCatalogQuery): Promise<MarketplaceCatalogPage>;
readDetail(pluginId: string): Promise<MarketplacePluginDetail>;
readLibrary(): Promise<MarketplaceLibraryProjection>;
acquire(pluginId: string): Promise<MarketplaceLibraryProjection>;
remove(pluginId: string): Promise<MarketplaceLibraryProjection>;
install(pluginId: string): Promise<MarketplaceInstallation>;
update(pluginId: string): Promise<MarketplaceInstallation>;
uninstall(pluginId: string): Promise<MarketplaceInstallation>;
};
export type PluginMarketplaceState = {
accountKey: string | null;
catalog: MarketplaceCatalogPage | null;
catalogState: LoadState;
catalogError: string | null;
catalogQuery: MarketplaceCatalogQuery;
library: MarketplaceLibrarySnapshot | null;
libraryState: LoadState;
libraryError: string | null;
installations: Record<string, MarketplaceInstallation>;
details: Record<string, MarketplacePluginDetail>;
detailState: Record<string, LoadState>;
pending: Record<string, true>;
activateAccount(accountKey: string | null): void;
loadCatalog(input?: MarketplaceCatalogQuery): Promise<void>;
loadDetail(pluginId: string): Promise<void>;
loadLibrary(): Promise<void>;
acquire(pluginId: string): Promise<void>;
remove(pluginId: string): Promise<void>;
install(pluginId: string): Promise<void>;
update(pluginId: string): Promise<void>;
uninstall(pluginId: string): Promise<void>;
};
const defaults: PluginMarketplaceDependencies = {
readCatalog: readMarketplaceCatalog,
readDetail: readMarketplaceDetail,
readLibrary: readMarketplaceLibrary,
acquire: acquireMarketplacePlugin,
remove: removeMarketplacePlugin,
install: installMarketplacePlugin,
update: updateMarketplacePlugin,
uninstall: uninstallMarketplacePlugin,
};
function message(error: unknown): string {
const base = error instanceof Error ? error.message : String(error);
if (!error || typeof error !== 'object') return base;
const details = 'details' in error && error.details && typeof error.details === 'object'
? error.details as Record<string, unknown>
: null;
const backendCode = details?.backendCode;
return typeof backendCode === 'string' ? `${backendCode}: ${base}` : base;
}
function installationMap(items: readonly MarketplaceInstallation[]): Record<string, MarketplaceInstallation> {
return Object.fromEntries(items.map((item) => [item.pluginId, item]));
}
export function createPluginMarketplaceStore(
overrides: Partial<PluginMarketplaceDependencies> = {},
): StoreApi<PluginMarketplaceState> {
const deps = { ...defaults, ...overrides };
let scopeGeneration = 0;
let catalogGeneration = 0;
let libraryMutationEpoch = 0;
return createStore<PluginMarketplaceState>((set, get) => {
const pending = async <T>(key: string, operation: () => Promise<T>): Promise<T> => {
set((state) => ({ pending: { ...state.pending, [key]: true } }));
try {
return await operation();
} finally {
set((state) => {
const next = { ...state.pending };
delete next[key];
return { pending: next };
});
}
};
const libraryAction = async (
key: string,
pluginId: string,
action: (pluginId: string) => Promise<MarketplaceLibraryProjection>,
): Promise<void> => pending(key, async () => {
const generation = scopeGeneration;
const accountKey = get().accountKey;
if (!accountKey) throw new Error('请先登录后管理我的插件');
try {
const projection = await action(pluginId);
if (generation !== scopeGeneration || get().accountKey !== accountKey) return;
libraryMutationEpoch += 1;
set({
library: projection.library,
installations: installationMap(projection.installations),
libraryState: 'ready',
libraryError: null,
});
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey) {
set({ libraryError: message(error) });
}
throw error;
}
});
const installAction = async (
key: string,
pluginId: string,
action: (pluginId: string) => Promise<MarketplaceInstallation>,
): Promise<void> => pending(key, async () => {
const generation = scopeGeneration;
const accountKey = get().accountKey;
if (!accountKey) throw new Error('请先登录后管理设备插件');
try {
const result = await action(pluginId);
if (generation !== scopeGeneration || get().accountKey !== accountKey) return;
libraryMutationEpoch += 1;
set((state) => {
const installations = { ...state.installations };
if (result.status === 'removed') delete installations[pluginId];
else installations[pluginId] = result;
return { installations, libraryError: null };
});
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey) {
set({ libraryError: message(error) });
}
throw error;
}
});
return {
accountKey: null,
catalog: null,
catalogState: 'idle',
catalogError: null,
catalogQuery: {},
library: null,
libraryState: 'idle',
libraryError: null,
installations: {},
details: {},
detailState: {},
pending: {},
activateAccount(accountKey) {
if (get().accountKey === accountKey) return;
scopeGeneration += 1;
libraryMutationEpoch = 0;
set({
accountKey,
library: null,
libraryState: 'idle',
libraryError: null,
installations: {},
pending: {},
});
},
async loadCatalog(input = {}) {
const generation = ++catalogGeneration;
set({ catalogState: 'loading', catalogError: null, catalogQuery: input });
try {
const catalog = await deps.readCatalog(input);
if (generation === catalogGeneration) set({ catalog, catalogState: 'ready', catalogError: null });
} catch (error) {
if (generation === catalogGeneration) set({ catalogState: 'error', catalogError: message(error) });
throw error;
}
},
async loadDetail(pluginId) {
const generation = scopeGeneration;
set((state) => ({ detailState: { ...state.detailState, [pluginId]: 'loading' } }));
try {
const detail = await deps.readDetail(pluginId);
if (generation !== scopeGeneration) return;
set((state) => ({
details: { ...state.details, [pluginId]: detail },
detailState: { ...state.detailState, [pluginId]: 'ready' },
}));
} catch (error) {
if (generation === scopeGeneration) {
set((state) => ({
detailState: { ...state.detailState, [pluginId]: 'error' },
catalogError: message(error),
}));
}
throw error;
}
},
async loadLibrary() {
const generation = scopeGeneration;
const accountKey = get().accountKey;
const mutationEpoch = libraryMutationEpoch;
if (!accountKey) {
set({ library: null, installations: {}, libraryState: 'idle', libraryError: null });
return;
}
set({ libraryState: 'loading', libraryError: null });
try {
const projection = await deps.readLibrary();
if (generation === scopeGeneration && get().accountKey === accountKey
&& mutationEpoch === libraryMutationEpoch) {
set({
library: projection.library,
installations: installationMap(projection.installations),
libraryState: 'ready',
libraryError: null,
});
}
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey) {
set({ libraryState: 'error', libraryError: message(error) });
}
throw error;
}
},
acquire: (pluginId) => libraryAction(`acquire:${pluginId}`, pluginId, deps.acquire),
remove: (pluginId) => libraryAction(`remove:${pluginId}`, pluginId, deps.remove),
install: (pluginId) => installAction(`install:${pluginId}`, pluginId, deps.install),
update: (pluginId) => installAction(`update:${pluginId}`, pluginId, deps.update),
uninstall: (pluginId) => installAction(`uninstall:${pluginId}`, pluginId, deps.uninstall),
};
});
}
export const pluginMarketplaceStore = createPluginMarketplaceStore();
export function usePluginMarketplaceStore<T>(selector: (state: PluginMarketplaceState) => T): T {
return useStore(pluginMarketplaceStore, selector);
}