fix: close marketplace client review findings

This commit is contained in:
2026-08-28 21:01:51 +08:00
parent 8dfa542860
commit 1614f7efc1
25 changed files with 1224 additions and 143 deletions

View File

@@ -111,17 +111,22 @@ function telemetryRoute(path: string): string {
async function parseResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
let message = `${response.status} ${response.statusText}`;
let backendCode: string | undefined;
try {
const payload = await response.json() as { error?: string };
const payload = await response.json() as { error?: string; code?: unknown };
if (payload?.error) {
message = payload.error;
}
if (typeof payload?.code === 'string' && /^[a-z][a-z0-9_]{0,63}$/u.test(payload.code)) {
backendCode = payload.code;
}
} catch {
// ignore body parse failure
}
throw normalizeAppError(new Error(message), {
source: 'browser-fallback',
status: response.status,
...(backendCode ? { backendCode } : {}),
});
}

View File

@@ -277,6 +277,13 @@ async function installationMutation(method: 'POST' | 'DELETE', segment: 'install
return parseMarketplaceInstallation(await fetcher(`/api/coding/plugin-marketplace/${segment}/${encodeURIComponent(pluginId(id))}`, { method, body: '{}' }));
}
export async function installBetaMarketplacePlugin(id: string, fetcher = defaultFetch) {
return parseMarketplaceInstallation(await fetcher(
`/api/coding/plugin-marketplace/install/${encodeURIComponent(pluginId(id))}/beta`,
{ method: 'POST', body: '{}' },
));
}
export async function installMarketplacePlugin(id: string, fetcher = defaultFetch) {
return installationMutation('POST', 'install', id, fetcher);
}

View File

@@ -12,8 +12,13 @@ function failureText(reason?: string): string | null {
if (!reason) return null;
const messages: string[] = [];
if (reason.includes('signature')) messages.push('签名校验失败,旧版本仍保留。');
if (reason.includes('yanked') || reason.includes('not_ready')) messages.push('版本已撤回或未就绪。');
if (reason.includes('incompatible')) messages.push('当前 MakeLore 版本不兼容。');
if (reason.includes('artifact_invalid')) messages.push('插件包校验失败,旧版本仍保留。');
if (reason.includes('yanked')) messages.push('此版本已撤回,旧版本仍保留。');
if (reason.includes('not_ready')) messages.push('此版本尚未就绪,旧版本仍保留。');
if (reason.includes('incompatible')) messages.push('当前 MakeLore 版本不兼容,旧版本仍保留。');
if (reason.includes('runtime_suspended')) messages.push('插件运行已暂停,设备版本未替换。');
if (reason.includes('backend_unavailable')) messages.push('Marketplace 服务暂不可用,设备版本未替换。');
if (reason.includes('library_required')) messages.push('请先在账号插件库中获取此插件。');
if (messages.length === 0) messages.push('当前版本暂不可用,旧版本仍保留。');
return messages.join(' ');
}
@@ -21,7 +26,13 @@ function failureText(reason?: string): string | null {
function actionErrorText(error?: string | null): string | null {
if (!error) return null;
if (error.includes('plugin_artifact_invalid')) return '插件签名或包校验失败,设备上此前可用版本仍保留。';
if (error.includes('plugin_release_not_ready')) return '版本已撤回、尚未就绪或与当前 MakeLore 不兼容;不会替换此前可用版本。';
if (error.includes('plugin_signature_invalid')) return '插件签名校验失败,设备上此前可用版本仍保留。';
if (error.includes('plugin_release_yanked')) return '此版本已撤回;不会替换此前可用版本。';
if (error.includes('plugin_release_not_ready')) return '版本尚未就绪;不会替换此前可用版本。';
if (error.includes('plugin_incompatible_client')) return '当前 MakeLore 版本不兼容;不会替换此前可用版本。';
if (error.includes('plugin_runtime_suspended')) return '插件运行已暂停;不会替换此前可用版本。';
if (error.includes('plugin_backend_unavailable')) return 'Marketplace 服务暂不可用;不会替换此前可用版本。';
if (error.includes('plugin_library_required')) return '请先在账号插件库中获取此插件。';
return error;
}
@@ -33,6 +44,7 @@ export type MyPluginsViewProps = {
pending: Record<string, true>;
onRefresh(): void | Promise<void>;
onInstall(pluginId: string): void | Promise<void>;
onInstallBeta(pluginId: string): void | Promise<void>;
onUpdate(pluginId: string): void | Promise<void>;
onUninstall(pluginId: string): void | Promise<void>;
onRemove(pluginId: string): void | Promise<void>;
@@ -66,6 +78,7 @@ export function MyPluginsView(props: MyPluginsViewProps) {
: !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>
: updateAvailable ? <Button type="button" className="min-h-10" disabled={busy || suspended} aria-label={`更新${plugin.title}`} onClick={() => void props.onUpdate(plugin.pluginId)}><RefreshCw className="mr-2 h-4 w-4" /></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 ? <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}
<Button asChild variant="outline" className="min-h-10"><Link to="/project-plugins"></Link></Button>
@@ -94,5 +107,5 @@ export function MyPlugins() {
toast.error(reason instanceof Error ? reason.message : String(reason));
});
const store = pluginMarketplaceStore.getState;
return <MyPluginsView library={library} installations={installations} state={state} error={error} pending={pending} onRefresh={() => safe(store().loadLibrary())} onInstall={(id) => safe(store().install(id))} onUpdate={(id) => safe(store().update(id))} onUninstall={(id) => safe(store().uninstall(id))} onRemove={(id) => safe(store().remove(id))} onReacquire={(id) => safe(store().acquire(id))} />;
return <MyPluginsView library={library} installations={installations} state={state} error={error} pending={pending} onRefresh={() => safe(store().loadLibrary())} onInstall={(id) => safe(store().install(id))} onInstallBeta={(id) => safe(store().installBeta(id))} onUpdate={(id) => safe(store().update(id))} onUninstall={(id) => safe(store().uninstall(id))} onRemove={(id) => safe(store().remove(id))} onReacquire={(id) => safe(store().acquire(id))} />;
}

View File

@@ -2,6 +2,7 @@ import { createStore, type StoreApi } from 'zustand';
import { useStore } from 'zustand';
import {
acquireMarketplacePlugin,
installBetaMarketplacePlugin,
installMarketplacePlugin,
readMarketplaceCatalog,
readMarketplaceDetail,
@@ -26,6 +27,7 @@ export type PluginMarketplaceDependencies = {
acquire(pluginId: string): Promise<MarketplaceLibraryProjection>;
remove(pluginId: string): Promise<MarketplaceLibraryProjection>;
install(pluginId: string): Promise<MarketplaceInstallation>;
installBeta(pluginId: string): Promise<MarketplaceInstallation>;
update(pluginId: string): Promise<MarketplaceInstallation>;
uninstall(pluginId: string): Promise<MarketplaceInstallation>;
};
@@ -50,6 +52,7 @@ export type PluginMarketplaceState = {
acquire(pluginId: string): Promise<void>;
remove(pluginId: string): Promise<void>;
install(pluginId: string): Promise<void>;
installBeta(pluginId: string): Promise<void>;
update(pluginId: string): Promise<void>;
uninstall(pluginId: string): Promise<void>;
};
@@ -61,6 +64,7 @@ const defaults: PluginMarketplaceDependencies = {
acquire: acquireMarketplacePlugin,
remove: removeMarketplacePlugin,
install: installMarketplacePlugin,
installBeta: installBetaMarketplacePlugin,
update: updateMarketplacePlugin,
uninstall: uninstallMarketplacePlugin,
};
@@ -68,11 +72,13 @@ const defaults: PluginMarketplaceDependencies = {
function message(error: unknown): string {
const base = error instanceof Error ? error.message : String(error);
if (!error || typeof error !== 'object') return base;
const errorCode = 'code' in error && typeof error.code === 'string' ? error.code : null;
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;
const prefixed = typeof backendCode === 'string' ? `${backendCode}: ${base}` : base;
return errorCode && !prefixed.includes(errorCode) ? `${errorCode}: ${prefixed}` : prefixed;
}
function installationMap(items: readonly MarketplaceInstallation[]): Record<string, MarketplaceInstallation> {
@@ -86,6 +92,7 @@ export function createPluginMarketplaceStore(
let scopeGeneration = 0;
let catalogGeneration = 0;
let libraryMutationEpoch = 0;
let libraryReadGeneration = 0;
return createStore<PluginMarketplaceState>((set, get) => {
const pending = async <T>(key: string, operation: () => Promise<T>): Promise<T> => {
@@ -109,9 +116,11 @@ export function createPluginMarketplaceStore(
const generation = scopeGeneration;
const accountKey = get().accountKey;
if (!accountKey) throw new Error('请先登录后管理我的插件');
const mutationEpoch = ++libraryMutationEpoch;
try {
const projection = await action(pluginId);
if (generation !== scopeGeneration || get().accountKey !== accountKey) return;
if (generation !== scopeGeneration || get().accountKey !== accountKey
|| mutationEpoch !== libraryMutationEpoch) return;
libraryMutationEpoch += 1;
set({
library: projection.library,
@@ -120,7 +129,8 @@ export function createPluginMarketplaceStore(
libraryError: null,
});
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey) {
if (generation === scopeGeneration && get().accountKey === accountKey
&& mutationEpoch === libraryMutationEpoch) {
set({ libraryError: message(error) });
}
throw error;
@@ -135,9 +145,11 @@ export function createPluginMarketplaceStore(
const generation = scopeGeneration;
const accountKey = get().accountKey;
if (!accountKey) throw new Error('请先登录后管理设备插件');
const mutationEpoch = ++libraryMutationEpoch;
try {
const result = await action(pluginId);
if (generation !== scopeGeneration || get().accountKey !== accountKey) return;
if (generation !== scopeGeneration || get().accountKey !== accountKey
|| mutationEpoch !== libraryMutationEpoch) return;
libraryMutationEpoch += 1;
set((state) => {
const installations = { ...state.installations };
@@ -146,8 +158,26 @@ export function createPluginMarketplaceStore(
return { installations, libraryError: null };
});
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey) {
set({ libraryError: message(error) });
if (generation === scopeGeneration && get().accountKey === accountKey
&& mutationEpoch === libraryMutationEpoch) {
const reason = message(error);
set((state) => ({
installations: {
...state.installations,
[pluginId]: {
status: 'unavailable',
pluginId,
...(state.installations[pluginId]?.releaseId
? { releaseId: state.installations[pluginId].releaseId }
: {}),
...(state.installations[pluginId]?.version
? { version: state.installations[pluginId].version }
: {}),
reason,
},
},
libraryError: reason,
}));
}
throw error;
}
@@ -169,7 +199,8 @@ export function createPluginMarketplaceStore(
activateAccount(accountKey) {
if (get().accountKey === accountKey) return;
scopeGeneration += 1;
libraryMutationEpoch = 0;
libraryReadGeneration += 1;
libraryMutationEpoch += 1;
set({
accountKey,
library: null,
@@ -211,6 +242,7 @@ export function createPluginMarketplaceStore(
}
},
async loadLibrary() {
const readGeneration = ++libraryReadGeneration;
const generation = scopeGeneration;
const accountKey = get().accountKey;
const mutationEpoch = libraryMutationEpoch;
@@ -222,6 +254,7 @@ export function createPluginMarketplaceStore(
try {
const projection = await deps.readLibrary();
if (generation === scopeGeneration && get().accountKey === accountKey
&& readGeneration === libraryReadGeneration
&& mutationEpoch === libraryMutationEpoch) {
set({
library: projection.library,
@@ -231,7 +264,9 @@ export function createPluginMarketplaceStore(
});
}
} catch (error) {
if (generation === scopeGeneration && get().accountKey === accountKey) {
if (generation === scopeGeneration && get().accountKey === accountKey
&& readGeneration === libraryReadGeneration
&& mutationEpoch === libraryMutationEpoch) {
set({ libraryState: 'error', libraryError: message(error) });
}
throw error;
@@ -240,6 +275,7 @@ export function createPluginMarketplaceStore(
acquire: (pluginId) => libraryAction(`acquire:${pluginId}`, pluginId, deps.acquire),
remove: (pluginId) => libraryAction(`remove:${pluginId}`, pluginId, deps.remove),
install: (pluginId) => installAction(`install:${pluginId}`, pluginId, deps.install),
installBeta: (pluginId) => installAction(`install-beta:${pluginId}`, pluginId, deps.installBeta),
update: (pluginId) => installAction(`update:${pluginId}`, pluginId, deps.update),
uninstall: (pluginId) => installAction(`uninstall:${pluginId}`, pluginId, deps.uninstall),
};