fix(marketplace): close client review findings

This commit is contained in:
2026-08-29 10:31:38 +08:00
parent 57962591de
commit 3df794c2e7
14 changed files with 840 additions and 68 deletions

View File

@@ -64,26 +64,27 @@ export function MyPluginsView(props: MyPluginsViewProps) {
<section aria-label="账号插件库" className="mt-6 space-y-4">
{props.library?.items.map((plugin) => {
const installed = props.installations[plugin.pluginId];
const hasDevicePackage = Boolean(installed?.version);
const removed = plugin.removedAt !== null;
const suspended = plugin.runtimeStatus === 'suspended';
const retired = plugin.catalogStatus === 'retired';
const installedChannel = installed?.channel ?? 'stable';
const installedChannel = hasDevicePackage ? installed?.channel ?? 'stable' : 'stable';
const channelVersion = installedChannel === 'beta' ? plugin.betaVersion : plugin.stableVersion;
const updateAvailable = Boolean(installed?.version && channelVersion && installed.version !== channelVersion);
const betaChannelUnavailable = Boolean(installed && installedChannel === 'beta' && !plugin.betaVersion);
const updateAvailable = Boolean(hasDevicePackage && channelVersion && installed?.version !== channelVersion);
const betaChannelUnavailable = Boolean(hasDevicePackage && installedChannel === 'beta' && !plugin.betaVersion);
const busy = Object.keys(props.pending).some((key) => key.endsWith(`:${plugin.pluginId}`));
const failure = failureText(installed?.reason);
return <article key={plugin.pluginId} className="rounded-2xl bg-background p-5 shadow-soft ring-1 ring-border/70">
<div className="flex flex-wrap items-start justify-between gap-4"><div><div className="flex flex-wrap items-center gap-2"><h2 className="text-balance text-xl font-semibold">{plugin.title}</h2>{plugin.acquisition === 'system_included' ? <Badge variant="outline"></Badge> : removed ? <Badge variant="outline"></Badge> : <Badge variant="secondary"></Badge>}</div><p className="mt-2 text-pretty text-sm text-muted-foreground">{plugin.summary}</p></div><div className="text-right text-sm"><p className="tabular-nums"> {plugin.stableVersion ?? '不可用'}</p>{plugin.betaVersion ? <p className="mt-1 tabular-nums text-muted-foreground">Beta {plugin.betaVersion}</p> : null}{installed?.version ? <><p className="mt-1 tabular-nums text-muted-foreground"> {installed.version}</p><p className="mt-1 text-muted-foreground">{installedChannel === 'beta' ? 'Beta' : '稳定'}</p></> : <p className="mt-1 text-muted-foreground"></p>}</div></div>
<div className="flex flex-wrap items-start justify-between gap-4"><div><div className="flex flex-wrap items-center gap-2"><h2 className="text-balance text-xl font-semibold">{plugin.title}</h2>{plugin.acquisition === 'system_included' ? <Badge variant="outline"></Badge> : removed ? <Badge variant="outline"></Badge> : <Badge variant="secondary"></Badge>}</div><p className="mt-2 text-pretty text-sm text-muted-foreground">{plugin.summary}</p></div><div className="text-right text-sm"><p className="tabular-nums"> {plugin.stableVersion ?? '不可用'}</p>{plugin.betaVersion ? <p className="mt-1 tabular-nums text-muted-foreground">Beta {plugin.betaVersion}</p> : null}{hasDevicePackage ? <><p className="mt-1 tabular-nums text-muted-foreground"> {installed?.version}</p><p className="mt-1 text-muted-foreground">{installedChannel === 'beta' ? 'Beta' : '稳定'}</p></> : <p className="mt-1 text-muted-foreground"></p>}</div></div>
<div className="mt-4 space-y-1 text-pretty text-sm">{suspended ? <p className="text-destructive"></p> : null}{retired ? <p>{removed ? '已退役,移除后不可重新获取' : '已退役;现有账号插件库仍可继续使用受支持版本'}</p> : null}{betaChannelUnavailable ? <p> Beta </p> : null}{failure ? <p className="text-destructive"><AlertCircle className="mr-1 inline h-4 w-4" />{failure}</p> : null}</div>
<div className="mt-5 flex flex-wrap gap-2">
{removed ? <Button type="button" className="min-h-10" disabled={busy || retired || suspended} aria-label={`重新免费获取${plugin.title}`} onClick={() => void props.onReacquire(plugin.pluginId)}></Button>
: !installed ? <Button type="button" className="min-h-10" disabled={busy || suspended} aria-label={`下载${plugin.title}`} onClick={() => void props.onInstall(plugin.pluginId)}><Download className="mr-2 h-4 w-4" /></Button>
: !hasDevicePackage ? <Button type="button" className="min-h-10" disabled={busy || suspended} aria-label={`下载${plugin.title}`} onClick={() => void props.onInstall(plugin.pluginId)}><Download className="mr-2 h-4 w-4" /></Button>
: updateAvailable ? <Button type="button" className="min-h-10" disabled={busy || suspended} aria-label={`${installedChannel === 'beta' ? '更新 Beta' : '更新'}${plugin.title}`} onClick={() => void (installedChannel === 'beta' ? props.onInstallBeta(plugin.pluginId) : props.onUpdate(plugin.pluginId))}><RefreshCw className="mr-2 h-4 w-4" />{installedChannel === 'beta' ? '更新 Beta' : '更新'}</Button>
: <span className="inline-flex min-h-10 items-center px-2 text-sm font-medium"><PackageCheck className="mr-2 h-4 w-4 text-brand" /></span>}
{!removed && plugin.acquisition !== 'system_included' && plugin.betaVersion && installedChannel !== 'beta' ? <Button type="button" variant="outline" className="min-h-10" disabled={busy || suspended} aria-label={`安装 Beta${plugin.title}`} onClick={() => void props.onInstallBeta(plugin.pluginId)}> Beta</Button> : null}
{!removed && plugin.acquisition !== 'system_included' ? <Button type="button" variant="outline" className="min-h-10" disabled={busy} aria-label={`移除${plugin.title}`} onClick={() => void props.onRemove(plugin.pluginId)}><Trash2 className="mr-2 h-4 w-4" /></Button> : null}
{installed ? <Button type="button" variant="ghost" className="min-h-10" disabled={busy} aria-label={`删除设备上的${plugin.title}`} onClick={() => void props.onUninstall(plugin.pluginId)}></Button> : null}
{hasDevicePackage ? <Button type="button" variant="ghost" className="min-h-10" disabled={busy} aria-label={`删除设备上的${plugin.title}`} onClick={() => void props.onUninstall(plugin.pluginId)}></Button> : null}
<Button asChild variant="outline" className="min-h-10"><Link to="/project-plugins"></Link></Button>
<Button asChild variant="outline" className="min-h-10"><Link to="/project-config"></Link></Button>
</div>

View File

@@ -93,12 +93,13 @@ export function createPluginMarketplaceStore(
let catalogGeneration = 0;
let libraryIntent = 0;
let latestLibraryReadIntent = 0;
const latestPluginMutationIntent = new Map<string, number>();
const latestLibraryMutationIntent = new Map<string, number>();
const latestDeviceMutationIntent = new Map<string, number>();
return createStore<PluginMarketplaceState>((set, get) => {
const beginMutation = (pluginId: string): number => {
const beginMutation = (pluginId: string, domain: 'library' | 'device'): number => {
const intent = ++libraryIntent;
latestPluginMutationIntent.set(pluginId, intent);
(domain === 'library' ? latestLibraryMutationIntent : latestDeviceMutationIntent).set(pluginId, intent);
return intent;
};
@@ -107,12 +108,18 @@ export function createPluginMarketplaceStore(
intent: number,
generation: number,
accountKey: string,
domain: 'library' | 'device',
): boolean => generation === scopeGeneration
&& get().accountKey === accountKey
&& latestPluginMutationIntent.get(pluginId) === intent
// A later explicit read is the newest account projection for every
// plugin, so an older mutation must not replace it.
&& latestLibraryReadIntent <= intent;
&& (domain === 'library' ? latestLibraryMutationIntent : latestDeviceMutationIntent).get(pluginId) === intent
// A later explicit read is the newest Library projection, so an older
// Library mutation must not replace it. Device state is independent.
&& (domain === 'device' || latestLibraryReadIntent <= intent);
const latestPluginMutationIntent = (pluginId: string): number => Math.max(
latestLibraryMutationIntent.get(pluginId) ?? 0,
latestDeviceMutationIntent.get(pluginId) ?? 0,
);
const mergeLibraryForPlugin = (
current: MarketplaceLibrarySnapshot | null,
@@ -125,8 +132,8 @@ export function createPluginMarketplaceStore(
if (incomingItem) byPlugin.set(pluginId, incomingItem);
else byPlugin.delete(pluginId);
const items = [...byPlugin.values()].sort((left, right) => {
const leftIntent = latestPluginMutationIntent.get(left.pluginId) ?? 0;
const rightIntent = latestPluginMutationIntent.get(right.pluginId) ?? 0;
const leftIntent = latestPluginMutationIntent(left.pluginId);
const rightIntent = latestPluginMutationIntent(right.pluginId);
return rightIntent - leftIntent;
});
return {
@@ -170,10 +177,10 @@ export function createPluginMarketplaceStore(
const generation = scopeGeneration;
const accountKey = get().accountKey;
if (!accountKey) throw new Error('请先登录后管理我的插件');
const intent = beginMutation(pluginId);
const intent = beginMutation(pluginId, 'library');
try {
const projection = await action(pluginId);
if (!canCommitMutation(pluginId, intent, generation, accountKey)) return;
if (!canCommitMutation(pluginId, intent, generation, accountKey, 'library')) return;
const merged = mergeLibraryProjection(pluginId, projection);
set({
library: merged.library,
@@ -182,7 +189,7 @@ export function createPluginMarketplaceStore(
libraryError: null,
});
} catch (error) {
if (canCommitMutation(pluginId, intent, generation, accountKey)) {
if (canCommitMutation(pluginId, intent, generation, accountKey, 'library')) {
set({ libraryError: message(error) });
}
throw error;
@@ -197,10 +204,10 @@ export function createPluginMarketplaceStore(
const generation = scopeGeneration;
const accountKey = get().accountKey;
if (!accountKey) throw new Error('请先登录后管理设备插件');
const intent = beginMutation(pluginId);
const intent = beginMutation(pluginId, 'device');
try {
const result = await action(pluginId);
if (!canCommitMutation(pluginId, intent, generation, accountKey)) return;
if (!canCommitMutation(pluginId, intent, generation, accountKey, 'device')) return;
set((state) => {
const installations = { ...state.installations };
if (result.status === 'removed') delete installations[pluginId];
@@ -208,7 +215,7 @@ export function createPluginMarketplaceStore(
return { installations, libraryError: null };
});
} catch (error) {
if (canCommitMutation(pluginId, intent, generation, accountKey)) {
if (canCommitMutation(pluginId, intent, generation, accountKey, 'device')) {
const reason = message(error);
set((state) => ({
installations: {
@@ -250,7 +257,8 @@ export function createPluginMarketplaceStore(
scopeGeneration += 1;
libraryIntent += 1;
latestLibraryReadIntent = libraryIntent;
latestPluginMutationIntent.clear();
latestLibraryMutationIntent.clear();
latestDeviceMutationIntent.clear();
set({
accountKey,
library: null,