fix: resolve latest official plugin releases

This commit is contained in:
tianma-if committed 2026-09-09 00:54:32 +08:00
1 parent 5070578363
commit 0ec00c8887
7 files changed
+180 -16

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
{
"registryVersion": "1",
"updatedAt": "2026-09-08T16:21:56.000Z",
"updatedAt": "2026-09-08T16:40:19.000Z",
"entries": [
{
"id": "org.edgeever.plugins.ai-rss",
@@ -15,10 +15,10 @@
"repositoryUrl": "https://github.com/tianma-if/edgeever-ai-rss"
},
"verification": {
"version": "0.5.2",
"version": "0.5.3",
"checksums": {
"manifestJson": "b8063f246730da33f9cee342670ca776c9319eb5a07585cb44f8641d49caa989",
"mainJs": "46bc00002a647f1f087ac2444e02192ec5ae2d1f04719e027de9386e8bf04934",
"manifestJson": "b164bf6ab199f0ef4282319dd87294a876ef3e7701962bae35a7b65c4e3c1156",
"mainJs": "7828d256ad758f85bd736742a72cac62ba46db6a2108c3a974bf9f429fcc3be4",
"stylesCss": "05cd135a1fe70c3f38d34850a2e9abf6b0c0530b09477960036f567375b2082e"
}
}
+6 -2
View File
@@ -100,7 +100,7 @@ import { useWorkspacePreferences } from "@/hooks/useWorkspacePreferences";
import { useWorkspaceSelection } from "@/hooks/useWorkspaceSelection";
import { useWorkspaceQueuedSync } from "@/hooks/useWorkspaceQueuedSync";
import { EdgeEverPluginHost, type RegisteredPluginPanel } from "@/lib/plugins/plugin-host";
import { loadPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
import { loadResolvedPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
import { updateOfficialMarketplacePlugins } from "@/lib/plugins/plugin-updates";
import { createPublicNetworkAdapter } from "@/lib/plugins/public-network-adapter";
import { clearRendererRecoveryRequired, isRendererRecoveryRequired } from "@/lib/renderer-recovery";
@@ -803,8 +803,12 @@ export const WorkspaceApp = ({
if (!active || running) return;
running = true;
try {
const marketplace = await loadPluginMarketplace();
const marketplace = await loadResolvedPluginMarketplace();
const result = await updateOfficialMarketplacePlugins(pluginHost, marketplace.entries);
const firstResolutionError = Object.entries(marketplace.resolutionErrors)[0];
if (firstResolutionError) {
console.error(`Official plugin ${firstResolutionError[0]} update resolution failed.`, firstResolutionError[1]);
}
if (active && result.updated.length > 0) {
setAppNoticeDialog({
title: t("plugins.noticeTitle"),
@@ -16,4 +16,9 @@ describe("plugin manager card layout", () => {
expect(source).toContain("href={entry.repositoryUrl}");
expect(source).toContain("href={extension.source.repositoryUrl}");
});
test("shows update checks when marketplace plugins exist without installed extensions", () => {
expect(source).toContain("snapshot.extensions.length > 0 || (marketplaceQuery.data?.entries.length ?? 0) > 0");
expect(source).not.toContain("{snapshot.extensions.length > 0 ? (\n <Button");
});
});
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import type { EdgeEverPluginHost, InstalledExtension, RegisteredPluginCommand, RegisteredPluginPanel } from "@/lib/plugins/plugin-host";
import { PluginPanelDialog } from "@/components/plugins/PluginPanelDialog";
import { loadPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
import { loadResolvedPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
import { GitHubMark, GitHubRepositoryLink } from "@/components/GitHubRepositoryLink";
import { applyPluginUpdate, checkPluginUpdates, type PluginUpdateInfo } from "@/lib/plugins/plugin-updates";
import { PluginUpdateDialog } from "@/components/plugins/PluginUpdateDialog";
@@ -310,7 +310,7 @@ export const PluginManagerCard = ({
const [activePanel, setActivePanel] = useState<RegisteredPluginPanel | null>(null);
const [pendingUpdate, setPendingUpdate] = useState<PluginUpdateInfo | null>(null);
const [pendingTrustPluginId, setPendingTrustPluginId] = useState<string | null>(null);
const marketplaceQuery = useQuery({ queryKey: ["plugin-marketplace", "v1"], queryFn: () => loadPluginMarketplace(), staleTime: 5 * 60_000 });
const marketplaceQuery = useQuery({ queryKey: ["plugin-marketplace", "v1"], queryFn: () => loadResolvedPluginMarketplace(), staleTime: 5 * 60_000 });
const extensionVersionKey = snapshot.extensions
.map((extension) => `${extension.manifest.id}:${extension.manifest.version}:${extension.source.kind}`)
.join("|");
@@ -374,8 +374,11 @@ export const PluginManagerCard = ({
["plugin-updates", extensionVersionKey, refreshedMarketplace.data?.updatedAt ?? "unavailable"],
result,
);
setLastManualCheckCount(result.updates.length);
const firstCheckError = Object.values(result.errors)[0];
const firstCheckError = Object.values({
...(refreshedMarketplace.data?.resolutionErrors ?? {}),
...result.errors,
})[0];
setLastManualCheckCount(firstCheckError ? null : result.updates.length);
if (firstCheckError) setError(t("plugins.updates.checkFailed", { message: firstCheckError }));
} catch (checkError) {
setError(checkError instanceof Error ? checkError.message : String(checkError));
@@ -423,7 +426,7 @@ export const PluginManagerCard = ({
</span>
</CardTitle>
<div className="flex items-center gap-1">
{snapshot.extensions.length > 0 ? (
{snapshot.extensions.length > 0 || (marketplaceQuery.data?.entries.length ?? 0) > 0 ? (
<Button
variant="ghost"
size="sm"
@@ -90,7 +90,7 @@ const requireAsset = (release: GithubReleaseResponse, name: string) => {
return asset;
};
type GithubAssetDownloader = (
export type GithubAssetDownloader = (
coordinates: GithubRepositoryCoordinates,
releaseTag: string,
asset: GithubReleaseAsset,
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { parseMarketplaceRegistry } from "@edgeever/plugin-api";
import { sha256Hex } from "./github-plugin-distribution.ts";
import { resolveOfficialPluginMarketplace } from "./plugin-marketplace.ts";
describe("bundled plugin marketplace", () => {
test("keeps verified checksums aligned with bundled extension files", async () => {
@@ -29,13 +30,100 @@ describe("bundled plugin marketplace", () => {
repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss",
distribution: { type: "github", repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss" },
verification: {
version: "0.5.2",
version: "0.5.3",
checksums: {
manifestJson: "b8063f246730da33f9cee342670ca776c9319eb5a07585cb44f8641d49caa989",
mainJs: "46bc00002a647f1f087ac2444e02192ec5ae2d1f04719e027de9386e8bf04934",
manifestJson: "b164bf6ab199f0ef4282319dd87294a876ef3e7701962bae35a7b65c4e3c1156",
mainJs: "7828d256ad758f85bd736742a72cac62ba46db6a2108c3a974bf9f429fcc3be4",
stylesCss: "05cd135a1fe70c3f38d34850a2e9abf6b0c0530b09477960036f567375b2082e",
},
},
});
});
test("resolves a newer official GitHub release with verified asset checksums", async () => {
const registry = parseMarketplaceRegistry({
registryVersion: "1",
updatedAt: "2026-09-08T00:00:00.000Z",
entries: [{
id: "org.edgeever.plugins.ai-rss",
name: "EdgeEver AI RSS",
description: "RSS",
author: "EdgeEver",
publisher: "edgeever",
category: "News & AI",
repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss",
distribution: { type: "github", repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss" },
verification: { version: "0.5.2", checksums: { manifestJson: "a".repeat(64), mainJs: "b".repeat(64) } },
}],
});
const manifest = {
type: "plugin",
id: "org.edgeever.plugins.ai-rss",
name: "EdgeEver AI RSS",
version: "0.5.3",
apiVersion: "2",
settingsUi: "host",
entry: "./main.js",
permissions: ["ui:notices"],
};
const manifestText = JSON.stringify(manifest);
const assets = {
"manifest.json": new TextEncoder().encode(manifestText).buffer,
"main.js": new TextEncoder().encode("export default {};").buffer,
};
const request = async (input) => {
const url = String(input);
if (url.includes("/contents/manifest.json")) return new Response(manifestText);
if (url.includes("/releases/tags/")) return Response.json({
tag_name: "v0.5.3",
draft: false,
assets: Object.entries(assets).map(([name, buffer], index) => ({
id: index + 1,
name,
size: buffer.byteLength,
url: `https://api.github.test/assets/${index + 1}`,
browser_download_url: `https://github.test/v0.5.3/${name}`,
})),
});
throw new Error(`Unexpected request: ${url}`);
};
const download = async (_coordinates, _releaseTag, asset) => assets[asset.name];
const resolved = await resolveOfficialPluginMarketplace(registry, request, download);
expect(resolved.resolutionErrors).toEqual({});
expect(resolved.entries[0].verification).toEqual({
version: "0.5.3",
checksums: {
manifestJson: await sha256Hex(assets["manifest.json"]),
mainJs: await sha256Hex(assets["main.js"]),
},
});
});
test("keeps the pinned release and reports an error when live resolution fails", async () => {
const registry = parseMarketplaceRegistry({
registryVersion: "1",
updatedAt: "2026-09-08T00:00:00.000Z",
entries: [{
id: "org.edgeever.plugins.ai-rss",
name: "EdgeEver AI RSS",
description: "RSS",
author: "EdgeEver",
publisher: "edgeever",
category: "News & AI",
repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss",
distribution: { type: "github", repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss" },
verification: { version: "0.5.3", checksums: { manifestJson: "a".repeat(64), mainJs: "b".repeat(64) } },
}],
});
const resolved = await resolveOfficialPluginMarketplace(
registry,
async () => new Response(null, { status: 503 }),
);
expect(resolved.entries[0]).toEqual(registry.entries[0]);
expect(resolved.resolutionErrors[registry.entries[0].id]).toContain("503");
});
});
+65 -1
View File
@@ -1,4 +1,9 @@
import { parseMarketplaceRegistry, type MarketplaceRegistry } from "@edgeever/plugin-api";
import { parseMarketplaceRegistry, type MarketplaceEntry, type MarketplaceRegistry } from "@edgeever/plugin-api";
import {
downloadGithubExtension,
type GithubAssetDownloader,
} from "@/lib/plugins/github-plugin-distribution";
import { isVersionOutdated } from "@/lib/version-check";
export const DEFAULT_PLUGIN_REGISTRY_URL = "/extensions/registry.json";
@@ -11,3 +16,62 @@ export const loadPluginMarketplace = async (
if (!response.ok) throw new Error(`Plugin marketplace request failed with HTTP ${response.status}.`);
return parseMarketplaceRegistry(await response.json());
};
export interface ResolvedPluginMarketplace extends MarketplaceRegistry {
resolutionErrors: Record<string, string>;
}
const resolveOfficialGithubEntry = async (
entry: MarketplaceEntry,
request: typeof fetch,
downloadAssetBytes?: GithubAssetDownloader,
): Promise<MarketplaceEntry> => {
if (entry.publisher !== "edgeever" || entry.distribution.type !== "github") return entry;
const downloaded = await downloadGithubExtension(entry.distribution.repositoryUrl, request, downloadAssetBytes);
if (downloaded.manifest.id !== entry.id) {
throw new Error("Official repository manifest id does not match the marketplace entry.");
}
if (isVersionOutdated(downloaded.manifest.version, entry.verification.version)) {
throw new Error("Official repository version is older than the marketplace verified version.");
}
if (!downloaded.checksums.manifestJson) throw new Error("Official extension release is missing a manifest checksum.");
if (downloaded.manifest.type === "plugin" && !downloaded.checksums.mainJs) {
throw new Error("Official plugin release is missing a main.js checksum.");
}
return {
...entry,
verification: {
version: downloaded.manifest.version,
checksums: downloaded.checksums,
},
};
};
export const resolveOfficialPluginMarketplace = async (
registry: MarketplaceRegistry,
request: typeof fetch = window.fetch.bind(window),
downloadAssetBytes?: GithubAssetDownloader,
): Promise<ResolvedPluginMarketplace> => {
const settled = await Promise.allSettled(
registry.entries.map((entry) => resolveOfficialGithubEntry(entry, request, downloadAssetBytes)),
);
const resolutionErrors: Record<string, string> = {};
const entries = settled.map((result, index) => {
if (result.status === "fulfilled") return result.value;
const entry = registry.entries[index];
if (!entry) throw result.reason;
resolutionErrors[entry.id] = result.reason instanceof Error ? result.reason.message : String(result.reason);
return entry;
});
return { ...registry, entries, resolutionErrors };
};
export const loadResolvedPluginMarketplace = async (
registryUrl = DEFAULT_PLUGIN_REGISTRY_URL,
request: typeof fetch = window.fetch.bind(window),
downloadAssetBytes?: GithubAssetDownloader,
) => resolveOfficialPluginMarketplace(
await loadPluginMarketplace(registryUrl, request),
request,
downloadAssetBytes,
);