feat: auto-update official plugins
This commit is contained in:
1 parent
131248b530
commit
c43e574611
13 files changed
+188
-20
No files matched your search
@@ -100,6 +100,8 @@ 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 { updateOfficialMarketplacePlugins } from "@/lib/plugins/plugin-updates";
|
||||
import { createPublicNetworkAdapter } from "@/lib/plugins/public-network-adapter";
|
||||
import { clearRendererRecoveryRequired, isRendererRecoveryRequired } from "@/lib/renderer-recovery";
|
||||
import { EditorPaneErrorBoundary, EditorRecoveryPane } from "./EditorPaneErrorBoundary";
|
||||
@@ -793,6 +795,38 @@ export const WorkspaceApp = ({
|
||||
void pluginHost.dispose();
|
||||
};
|
||||
}, [pluginHost]);
|
||||
useEffect(() => {
|
||||
if (!pluginHostReady) return;
|
||||
let active = true;
|
||||
let running = false;
|
||||
const updateOfficialPlugins = async () => {
|
||||
if (!active || running) return;
|
||||
running = true;
|
||||
try {
|
||||
const marketplace = await loadPluginMarketplace();
|
||||
const result = await updateOfficialMarketplacePlugins(pluginHost, marketplace.entries);
|
||||
if (active && result.updated.length > 0) {
|
||||
setAppNoticeDialog({
|
||||
title: t("plugins.noticeTitle"),
|
||||
description: t("plugins.updates.officialAutoUpdated", { count: result.updated.length }),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Official plugin update check failed.", error);
|
||||
} finally {
|
||||
running = false;
|
||||
}
|
||||
};
|
||||
const handleFocus = () => { void updateOfficialPlugins(); };
|
||||
const intervalId = window.setInterval(() => void updateOfficialPlugins(), 30 * 60_000);
|
||||
window.addEventListener("focus", handleFocus);
|
||||
void updateOfficialPlugins();
|
||||
return () => {
|
||||
active = false;
|
||||
window.clearInterval(intervalId);
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
};
|
||||
}, [pluginHost, pluginHostReady, t]);
|
||||
const scheduledTasksQuery = useQuery({
|
||||
queryKey: ["scheduled-tasks", scheduledTaskDeviceId],
|
||||
queryFn: () => api.listScheduledTasks(scheduledTaskDeviceId ?? undefined),
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { EdgeEverPluginHost, InstalledExtension, RegisteredPluginCommand, R
|
||||
import { PluginPanelDialog } from "@/components/plugins/PluginPanelDialog";
|
||||
import { loadPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
|
||||
import { GitHubMark } from "@/components/GitHubRepositoryLink";
|
||||
import { checkPluginUpdates, type PluginUpdateInfo } from "@/lib/plugins/plugin-updates";
|
||||
import { applyPluginUpdate, checkPluginUpdates, type PluginUpdateInfo } from "@/lib/plugins/plugin-updates";
|
||||
import { PluginUpdateDialog } from "@/components/plugins/PluginUpdateDialog";
|
||||
import { PluginSettingsSection } from "@/components/plugins/PluginSettingsSection";
|
||||
import { getPluginDetailPage, getPluginDetailPath, hasPluginSettings, type PluginDetailPage } from "@/lib/plugins/plugin-navigation";
|
||||
@@ -404,17 +404,7 @@ export const PluginManagerCard = ({
|
||||
};
|
||||
|
||||
const applyUpdate = async (update: PluginUpdateInfo) => {
|
||||
const extension = snapshot.extensions.find((candidate) => candidate.manifest.id === update.pluginId);
|
||||
if (!extension) throw new Error("Extension is no longer installed.");
|
||||
if (extension.source.kind === "marketplace") {
|
||||
if (!update.marketplaceEntry) throw new Error("The verified marketplace entry is no longer available.");
|
||||
await host.installMarketplaceEntry(update.marketplaceEntry, update.latestManifest);
|
||||
} else if (extension.source.kind === "github") {
|
||||
if (!extension.source.repositoryUrl) throw new Error("Installed GitHub extension is missing its repository URL.");
|
||||
await host.installFromGithubRepository(extension.source.repositoryUrl, undefined, update.latestManifest);
|
||||
} else {
|
||||
await host.installFromManifestUrl(extension.manifestUrl, undefined, update.latestManifest);
|
||||
}
|
||||
await applyPluginUpdate(host, update);
|
||||
setPendingUpdate(null);
|
||||
setLastManualCheckCount(null);
|
||||
await updateQuery.refetch();
|
||||
@@ -517,6 +507,11 @@ export const PluginManagerCard = ({
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate text-sm font-semibold text-slate-900">{entry.name}</span>
|
||||
<BadgeCheck className="h-4 w-4 shrink-0 text-emerald-600" aria-label={t("plugins.marketplace.verified")} />
|
||||
{entry.publisher === "edgeever" ? (
|
||||
<span className="rounded-full bg-emerald-50 px-2 py-0.5 text-[10px] font-semibold text-emerald-700">
|
||||
{t("plugins.marketplace.officialAutoUpdate")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-slate-400">{entry.author} · {entry.category} · v{entry.verification.version}</div>
|
||||
</div>
|
||||
|
||||
@@ -1463,6 +1463,15 @@ export class EdgeEverPluginHost {
|
||||
if (manifest.type === "plugin" && manifest.apiVersion !== PLUGIN_API_VERSION) {
|
||||
throw new Error(`Marketplace plugins must use plugin API v${PLUGIN_API_VERSION}.`);
|
||||
}
|
||||
if (entry.publisher === "edgeever") {
|
||||
if (!entry.verification.checksums?.manifestJson) throw new Error("Official extensions must pin the manifest checksum.");
|
||||
if (manifest.type === "plugin" && !entry.verification.checksums.mainJs) {
|
||||
throw new Error("Official plugins must pin the main.js checksum.");
|
||||
}
|
||||
if (actualChecksums.stylesCss && !entry.verification.checksums.stylesCss) {
|
||||
throw new Error("Official plugins must pin the styles.css checksum when styles are distributed.");
|
||||
}
|
||||
}
|
||||
for (const [name, expected] of Object.entries(entry.verification.checksums ?? {})) {
|
||||
const actual = actualChecksums[name as keyof CachedPluginPackage["checksums"]];
|
||||
if (!actual || actual.toLocaleLowerCase() !== expected) throw new Error(`${name} does not match the marketplace verified checksum.`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { checkInstalledExtensionUpdate, checkPluginUpdates } from "./plugin-updates.ts";
|
||||
import { checkInstalledExtensionUpdate, checkPluginUpdates, updateOfficialMarketplacePlugins } from "./plugin-updates.ts";
|
||||
|
||||
const pluginManifest = {
|
||||
type: "plugin",
|
||||
@@ -73,4 +73,51 @@ describe("plugin update checks", () => {
|
||||
expect(result.updates.map((update) => update.pluginId)).toEqual([pluginManifest.id]);
|
||||
expect(result.errors["org.edgeever.broken"]).toContain("503");
|
||||
});
|
||||
|
||||
test("automatically applies only checksum-pinned official marketplace updates", async () => {
|
||||
const communityManifest = { ...pluginManifest, id: "org.edgeever.community-test" };
|
||||
const extensions = [
|
||||
installed({ source: { kind: "marketplace", verified: true } }),
|
||||
installed({ manifest: communityManifest, source: { kind: "marketplace", verified: true } }),
|
||||
];
|
||||
const entries = [
|
||||
{
|
||||
id: pluginManifest.id,
|
||||
name: pluginManifest.name,
|
||||
description: "Official plugin",
|
||||
author: "EdgeEver",
|
||||
publisher: "edgeever",
|
||||
category: "Productivity",
|
||||
repositoryUrl: "https://github.com/example/official-plugin",
|
||||
distribution: { type: "manifest", manifestUrl: "https://example.com/official/manifest.json" },
|
||||
verification: { version: "1.2.0", checksums: { manifestJson: "a".repeat(64), mainJs: "b".repeat(64) } },
|
||||
},
|
||||
{
|
||||
id: communityManifest.id,
|
||||
name: communityManifest.name,
|
||||
description: "Community plugin",
|
||||
author: "Community",
|
||||
category: "Productivity",
|
||||
repositoryUrl: "https://github.com/example/community-plugin",
|
||||
distribution: { type: "manifest", manifestUrl: "https://example.com/community/manifest.json" },
|
||||
verification: { version: "1.2.0", checksums: { manifestJson: "c".repeat(64), mainJs: "d".repeat(64) } },
|
||||
},
|
||||
];
|
||||
const installedIds = [];
|
||||
const host = {
|
||||
getSnapshot: () => ({ extensions }),
|
||||
installMarketplaceEntry: async (entry) => { installedIds.push(entry.id); },
|
||||
installFromGithubRepository: async () => { throw new Error("unexpected GitHub install"); },
|
||||
installFromManifestUrl: async () => { throw new Error("unexpected manifest install"); },
|
||||
};
|
||||
const request = async (input) => Response.json({
|
||||
...(String(input).includes("community") ? communityManifest : pluginManifest),
|
||||
version: "1.2.0",
|
||||
});
|
||||
|
||||
const result = await updateOfficialMarketplacePlugins(host, entries, request);
|
||||
|
||||
expect(installedIds).toEqual([pluginManifest.id]);
|
||||
expect(result.updated.map((update) => update.pluginId)).toEqual([pluginManifest.id]);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,11 @@ export interface PluginUpdateCheckResult {
|
||||
errors: Record<string, string>;
|
||||
}
|
||||
|
||||
type PluginUpdateHost = Pick<
|
||||
import("@/lib/plugins/plugin-host").EdgeEverPluginHost,
|
||||
"getSnapshot" | "installMarketplaceEntry" | "installFromGithubRepository" | "installFromManifestUrl"
|
||||
>;
|
||||
|
||||
const fetchManifest = async (url: string, request: typeof fetch) => {
|
||||
const response = await request(url, { cache: "no-store", credentials: "omit" });
|
||||
if (!response.ok) throw new Error(`Manifest request failed with HTTP ${response.status}.`);
|
||||
@@ -101,3 +106,46 @@ export const checkPluginUpdates = async (
|
||||
});
|
||||
return { updates, errors };
|
||||
};
|
||||
|
||||
export const applyPluginUpdate = async (host: PluginUpdateHost, update: PluginUpdateInfo) => {
|
||||
const extension = host.getSnapshot().extensions.find((candidate) => candidate.manifest.id === update.pluginId);
|
||||
if (!extension) throw new Error("Extension is no longer installed.");
|
||||
if (extension.source.kind === "marketplace") {
|
||||
if (!update.marketplaceEntry) throw new Error("The verified marketplace entry is no longer available.");
|
||||
await host.installMarketplaceEntry(update.marketplaceEntry, update.latestManifest);
|
||||
} else if (extension.source.kind === "github") {
|
||||
if (!extension.source.repositoryUrl) throw new Error("Installed GitHub extension is missing its repository URL.");
|
||||
await host.installFromGithubRepository(extension.source.repositoryUrl, undefined, update.latestManifest);
|
||||
} else {
|
||||
await host.installFromManifestUrl(extension.manifestUrl, undefined, update.latestManifest);
|
||||
}
|
||||
};
|
||||
|
||||
export interface OfficialPluginUpdateResult {
|
||||
updated: PluginUpdateInfo[];
|
||||
errors: Record<string, string>;
|
||||
}
|
||||
|
||||
export const updateOfficialMarketplacePlugins = async (
|
||||
host: PluginUpdateHost,
|
||||
marketplaceEntries: MarketplaceEntry[],
|
||||
request: typeof fetch = window.fetch.bind(window),
|
||||
): Promise<OfficialPluginUpdateResult> => {
|
||||
const checked = await checkPluginUpdates(host.getSnapshot().extensions, marketplaceEntries, request);
|
||||
const officialUpdates = checked.updates.filter((update) => {
|
||||
if (update.marketplaceEntry?.publisher !== "edgeever") return false;
|
||||
if (!update.marketplaceEntry.verification.checksums?.manifestJson) return false;
|
||||
return update.latestManifest.type !== "plugin" || Boolean(update.marketplaceEntry.verification.checksums.mainJs);
|
||||
});
|
||||
const updated: PluginUpdateInfo[] = [];
|
||||
const errors = { ...checked.errors };
|
||||
for (const update of officialUpdates) {
|
||||
try {
|
||||
await applyPluginUpdate(host, update);
|
||||
updated.push(update);
|
||||
} catch (error) {
|
||||
errors[update.pluginId] = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
return { updated, errors };
|
||||
};
|
||||
@@ -45,7 +45,7 @@ styles.css (optional)
|
||||
|
||||
GitHub plugins must use `./main.js` as `entry`, and `main.js` must be a single-file bundle without relative module imports. EdgeEver reads the default-branch manifest, locates the matching Release, downloads assets in parallel, verifies GitHub's SHA-256 digest when present, and caches the verified package in device-local IndexedDB. `main.js` is limited to 5 MB and `styles.css` to 1 MB.
|
||||
|
||||
EdgeEver checks for updates when the Plugin Marketplace opens, when the window regains focus, and every 30 minutes, but never installs silently. The user must click Update and confirm. If a new version changes declared capabilities or legacy network-host metadata, the confirmation lists those changes for review. For GitHub distribution, the Release `manifest.json` must exactly match the default-branch manifest used for the update prompt or installation is rejected. Marketplace installs only follow newer versions verified in the Registry.
|
||||
EdgeEver checks for updates when the Plugin Marketplace opens, when the window regains focus, and every 30 minutes. Marketplace installs whose Registry entry declares `"publisher": "edgeever"` are official EdgeEver extensions and update automatically to the latest checksum-pinned Registry version. Community Marketplace extensions and extensions installed directly from GitHub or a Manifest URL are never updated silently: the user must click Update and confirm. If a manually confirmed version changes declared capabilities or legacy network-host metadata, the confirmation lists those changes for review. For GitHub distribution, the Release `manifest.json` must exactly match the default-branch manifest used for the update prompt or installation is rejected. Marketplace installs only follow newer versions verified in the Registry.
|
||||
|
||||
Updates use a rollback-capable switch. Old and new package versions are cached separately. If the new version cannot activate, EdgeEver restores the previous manifest, enabled state, and package instead of leaving the plugin broken or disabled.
|
||||
|
||||
@@ -61,7 +61,7 @@ Only public GitHub repositories are supported for now; private-repository tokens
|
||||
|
||||
The official marketplace only lists free and open-source plugins. Complete human-readable source, an accepted open-source license, build information, and a traceable public source revision are required for every listed version. This requirement applies only to official marketplace admission; users remain free to install other plugins from GitHub or a Manifest URL. See the [Plugin Marketplace Submission Policy](plugin-marketplace-policy.md) for the complete requirements.
|
||||
|
||||
The marketplace is a verified Registry and does not take ownership of plugin files. For each version, the Registry pins the plugin ID, GitHub repository, version, and SHA-256 hashes for `manifest.json`, `main.js`, and optional `styles.css`. Installation still downloads from the developer's GitHub Release or registered public URL and verifies those hashes again.
|
||||
The marketplace is a verified Registry and does not take ownership of plugin files. For each version, the Registry pins the plugin ID, GitHub repository, version, and SHA-256 hashes for `manifest.json`, `main.js`, and optional `styles.css`. Installation still downloads from the developer's GitHub Release or registered public URL and verifies those hashes again. The optional `"publisher": "edgeever"` marker is reserved for Registry entries maintained by the EdgeEver project; it enables automatic updates and must not be used for community submissions.
|
||||
|
||||
Registry format:
|
||||
|
||||
@@ -73,7 +73,8 @@ Registry format:
|
||||
"id": "com.example.recent-notes",
|
||||
"name": "Recent Notes",
|
||||
"description": "Shows recently updated notes.",
|
||||
"author": "Example",
|
||||
"author": "EdgeEver",
|
||||
"publisher": "edgeever",
|
||||
"category": "Productivity",
|
||||
"repositoryUrl": "https://github.com/example/edgeever-recent-notes",
|
||||
"distribution": {
|
||||
|
||||
@@ -45,7 +45,7 @@ styles.css(可选)
|
||||
|
||||
GitHub 插件的 `entry` 固定为 `./main.js`,`main.js` 必须是无需相对模块导入的单文件 Bundle。EdgeEver 会读取默认分支 Manifest、查找相同版本的 Release、并行下载资产、验证 GitHub 提供的 SHA-256 Digest(如果存在),然后把验证后的包缓存到当前设备的 IndexedDB。`main.js` 上限为 5 MB,`styles.css` 上限为 1 MB。
|
||||
|
||||
EdgeEver 会在插件市场页面打开、窗口重新获得焦点及每 30 分钟检查一次更新,但不会静默安装。用户必须点击「更新」并确认;如果新版改变能力声明或旧版网络域名元数据,确认框会列出这些变化供用户查看。GitHub 分发的 Release `manifest.json` 必须与默认分支中用于提示更新的 Manifest 完全一致,否则安装会被拒绝。市场安装只跟随 Registry 中已经验证的新版本。
|
||||
EdgeEver 会在插件市场页面打开、窗口重新获得焦点及每 30 分钟检查一次更新。Registry 条目声明 `"publisher": "edgeever"` 的市场安装属于 EdgeEver 官方扩展,会自动更新到 Registry 中通过校验和固定的最新版本。社区市场扩展以及从 GitHub 或 Manifest 地址直接安装的扩展绝不会静默更新,用户必须点击「更新」并确认;如果手动确认的新版改变能力声明或旧版网络域名元数据,确认框会列出这些变化供用户查看。GitHub 分发的 Release `manifest.json` 必须与默认分支中用于提示更新的 Manifest 完全一致,否则安装会被拒绝。市场安装只跟随 Registry 中已经验证的新版本。
|
||||
|
||||
升级采用可回滚切换:新旧版本的包会分别缓存;如果新版无法激活,EdgeEver 会恢复原 Manifest、原启用状态和上一版本代码,而不是留下一个被破坏或被停用的插件。
|
||||
|
||||
@@ -61,7 +61,7 @@ https://github.com/owner/edgeever-plugin
|
||||
|
||||
官方插件市场仅收录自由及开源插件。每个上架版本都必须提供完整且人类可读的源码、认可的开源许可证、构建信息,以及可追溯的公开源码版本。该要求仅适用于官方市场准入;用户仍可自由通过 GitHub 或 Manifest 地址安装其他插件。完整要求参阅[官方插件市场上架政策](plugin-marketplace-policy.zh-CN.md)。
|
||||
|
||||
插件市场是一个经过校验的 Registry,不接管插件所有权。Registry 为每个版本固定插件 ID、GitHub 仓库、版本号及 `manifest.json`/`main.js`/`styles.css` 的 SHA-256;安装时仍从开发者的 GitHub Release 或登记的公开地址下载,并再次核对校验和。
|
||||
插件市场是一个经过校验的 Registry,不接管插件所有权。Registry 为每个版本固定插件 ID、GitHub 仓库、版本号及 `manifest.json`/`main.js`/`styles.css` 的 SHA-256;安装时仍从开发者的 GitHub Release 或登记的公开地址下载,并再次核对校验和。可选的 `"publisher": "edgeever"` 标记仅保留给 EdgeEver 项目维护的 Registry 条目;它会启用自动更新,社区投稿不得使用。
|
||||
|
||||
Registry 格式:
|
||||
|
||||
@@ -73,7 +73,8 @@ Registry 格式:
|
||||
"id": "com.example.recent-notes",
|
||||
"name": "Recent Notes",
|
||||
"description": "Shows recently updated notes.",
|
||||
"author": "Example",
|
||||
"author": "EdgeEver",
|
||||
"publisher": "edgeever",
|
||||
"category": "Productivity",
|
||||
"repositoryUrl": "https://github.com/example/edgeever-recent-notes",
|
||||
"distribution": {
|
||||
|
||||
Vendored
+1
@@ -85,6 +85,7 @@ export interface MarketplaceEntry {
|
||||
name: string;
|
||||
description: string;
|
||||
author: string;
|
||||
publisher?: "edgeever";
|
||||
category: string;
|
||||
repositoryUrl: string;
|
||||
distribution: {
|
||||
|
||||
Vendored
+4
@@ -264,6 +264,9 @@ var parseMarketplaceRegistry = (value) => {
|
||||
const name = item.name;
|
||||
const description = item.description;
|
||||
const author = item.author;
|
||||
if (item.publisher !== undefined && item.publisher !== "edgeever") {
|
||||
throw new Error(`Marketplace entry ${item.id} has an invalid publisher.`);
|
||||
}
|
||||
const category = item.category;
|
||||
const repositoryUrl = item.repositoryUrl;
|
||||
if (!GITHUB_REPOSITORY_PATTERN.test(repositoryUrl))
|
||||
@@ -293,6 +296,7 @@ var parseMarketplaceRegistry = (value) => {
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
author: author.trim(),
|
||||
...item.publisher === "edgeever" ? { publisher: "edgeever" } : {},
|
||||
category: category.trim(),
|
||||
repositoryUrl: repositoryUrl.trim(),
|
||||
distribution,
|
||||
|
||||
@@ -173,12 +173,31 @@ describe("marketplace registry", () => {
|
||||
name: "Example",
|
||||
description: "Example plugin",
|
||||
author: "EdgeEver",
|
||||
publisher: "edgeever",
|
||||
category: "Productivity",
|
||||
repositoryUrl: "https://github.com/edgeever/example",
|
||||
distribution: { type: "github", repositoryUrl: "https://github.com/edgeever/example" },
|
||||
verification: { version: "1.0.0", checksums: { manifestJson: "a".repeat(64), mainJs: "b".repeat(64) } },
|
||||
}],
|
||||
}).entries[0]).toMatchObject({ id: "org.edgeever.example", verification: { version: "1.0.0" } });
|
||||
}).entries[0]).toMatchObject({ id: "org.edgeever.example", publisher: "edgeever", verification: { version: "1.0.0" } });
|
||||
});
|
||||
|
||||
test("rejects unknown automatic-update publishers", () => {
|
||||
expect(() => parseMarketplaceRegistry({
|
||||
registryVersion: "1",
|
||||
updatedAt: "2026-08-16T00:00:00.000Z",
|
||||
entries: [{
|
||||
id: "org.edgeever.example",
|
||||
name: "Example",
|
||||
description: "Example plugin",
|
||||
author: "Example",
|
||||
publisher: "third-party",
|
||||
category: "Productivity",
|
||||
repositoryUrl: "https://github.com/example/plugin",
|
||||
distribution: { type: "github", repositoryUrl: "https://github.com/example/plugin" },
|
||||
verification: { version: "1.0.0", checksums: { manifestJson: "a".repeat(64) } },
|
||||
}],
|
||||
})).toThrow("invalid publisher");
|
||||
});
|
||||
|
||||
test("rejects duplicate plugin ids", () => {
|
||||
|
||||
@@ -123,6 +123,7 @@ export interface MarketplaceEntry {
|
||||
name: string;
|
||||
description: string;
|
||||
author: string;
|
||||
publisher?: "edgeever";
|
||||
category: string;
|
||||
repositoryUrl: string;
|
||||
distribution:
|
||||
@@ -685,6 +686,9 @@ export const parseMarketplaceRegistry = (value: unknown): MarketplaceRegistry =>
|
||||
const name = item.name as string;
|
||||
const description = item.description as string;
|
||||
const author = item.author as string;
|
||||
if (item.publisher !== undefined && item.publisher !== "edgeever") {
|
||||
throw new Error(`Marketplace entry ${item.id} has an invalid publisher.`);
|
||||
}
|
||||
const category = item.category as string;
|
||||
const repositoryUrl = item.repositoryUrl as string;
|
||||
if (!GITHUB_REPOSITORY_PATTERN.test(repositoryUrl)) throw new Error(`Marketplace entry ${item.id} repositoryUrl must be a GitHub repository.`);
|
||||
@@ -715,6 +719,7 @@ export const parseMarketplaceRegistry = (value: unknown): MarketplaceRegistry =>
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
author: author.trim(),
|
||||
...(item.publisher === "edgeever" ? { publisher: "edgeever" as const } : {}),
|
||||
category: category.trim(),
|
||||
repositoryUrl: repositoryUrl.trim(),
|
||||
distribution,
|
||||
|
||||
@@ -736,6 +736,7 @@ export const enUS = {
|
||||
addedPermissions: "New capability declarations",
|
||||
addedNetworkHosts: "New website declarations (legacy metadata)",
|
||||
allowAndUpdate: "Confirm and update",
|
||||
officialAutoUpdated: "Updated {{count}} official plugin(s) to the latest verified version.",
|
||||
},
|
||||
noticeTitle: "Plugin",
|
||||
panel: "Plugin panel",
|
||||
@@ -751,6 +752,7 @@ export const enUS = {
|
||||
refresh: "Refresh plugin marketplace",
|
||||
loading: "Loading plugin marketplace…",
|
||||
verified: "Verified by EdgeEver",
|
||||
officialAutoUpdate: "Official · auto-updates",
|
||||
installed: "Verified version installed",
|
||||
installVerified: "Replace with verified version",
|
||||
openRepository: "Open the source repository for {{name}}",
|
||||
|
||||
@@ -736,6 +736,7 @@ export const zhCN = {
|
||||
addedPermissions: "新增能力声明",
|
||||
addedNetworkHosts: "新增网站声明(旧版元数据)",
|
||||
allowAndUpdate: "确认并更新",
|
||||
officialAutoUpdated: "已将 {{count}} 个官方插件自动更新至最新验证版本。",
|
||||
},
|
||||
noticeTitle: "插件",
|
||||
panel: "插件面板",
|
||||
@@ -751,6 +752,7 @@ export const zhCN = {
|
||||
refresh: "刷新插件市场",
|
||||
loading: "正在加载插件市场…",
|
||||
verified: "EdgeEver 已验证",
|
||||
officialAutoUpdate: "官方 · 自动更新",
|
||||
installed: "已安装验证版本",
|
||||
installVerified: "替换为验证版本",
|
||||
openRepository: "打开 {{name}} 的源代码仓库",
|
||||
|
||||
Reference in new issue
Block a user