mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-02 06:44:00 +08:00
feat(plugin): add local plugin discovery and management with cache busting
This commit is contained in:
@@ -80,6 +80,11 @@ export function CanvasPluginManagerModal({ open, onClose }: { open: boolean; onC
|
||||
<span className="rounded-full px-1.5 py-0.5 text-[10px]" style={{ background: theme.toolbar.activeBg, color: theme.node.muted }}>
|
||||
v{record.version}
|
||||
</span>
|
||||
{record.local && (
|
||||
<span className="rounded-full px-1.5 py-0.5 text-[10px]" style={{ background: "#22c55e22", color: "#16a34a" }}>
|
||||
本地
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-xs" style={{ color: theme.node.muted }}>
|
||||
{record.description || record.url}
|
||||
@@ -91,10 +96,14 @@ export function CanvasPluginManagerModal({ open, onClose }: { open: boolean; onC
|
||||
loading={busyId === record.id}
|
||||
onChange={(checked) => runOnPlugin(record, () => setPluginEnabled(record, checked), checked ? "已启用" : "已禁用")}
|
||||
/>
|
||||
<Button type="text" size="small" icon={<RefreshCw className="size-4" />} loading={busyId === record.id} title="从来源更新" onClick={() => runOnPlugin(record, async () => void (await updatePlugin(record)), "已更新")} />
|
||||
<Popconfirm title="卸载该插件?" okText="卸载" cancelText="取消" onConfirm={() => uninstallPlugin(record.id)}>
|
||||
<Button type="text" size="small" danger icon={<Trash2 className="size-4" />} title="卸载" />
|
||||
</Popconfirm>
|
||||
{!record.local && (
|
||||
<>
|
||||
<Button type="text" size="small" icon={<RefreshCw className="size-4" />} loading={busyId === record.id} title="从来源更新" onClick={() => runOnPlugin(record, async () => void (await updatePlugin(record)), "已更新")} />
|
||||
<Popconfirm title="卸载该插件?" okText="卸载" cancelText="取消" onConfirm={() => uninstallPlugin(record.id)}>
|
||||
<Button type="text" size="small" danger icon={<Trash2 className="size-4" />} title="卸载" />
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -50,6 +50,11 @@ async function fetchPluginSource(url: string) {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// 加缓存穿透参数,配合 watch 构建拿到最新产物
|
||||
function withCacheBust(url: string) {
|
||||
return `${url}${url.includes("?") ? "&" : "?"}t=${Date.now()}`;
|
||||
}
|
||||
|
||||
// 从 URL 安装(或覆盖更新)一个插件,成功后立即启用
|
||||
export async function installPluginFromUrl(url: string) {
|
||||
const source = await fetchPluginSource(url);
|
||||
@@ -70,7 +75,9 @@ export async function setPluginEnabled(record: InstalledPlugin, enabled: boolean
|
||||
deactivatePlugin(record.id);
|
||||
return;
|
||||
}
|
||||
const plugin = await evaluatePluginSource(record.source);
|
||||
// 本地插件启用时按 url 重新拉取,拿到最新构建(缓存 source 可能已过期)
|
||||
const source = record.local ? await fetchPluginSource(withCacheBust(record.url)) : record.source;
|
||||
const plugin = await evaluatePluginSource(source);
|
||||
activatePlugin(plugin);
|
||||
}
|
||||
|
||||
@@ -86,11 +93,14 @@ export async function ensurePluginsLoaded() {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
await usePluginStore.persist.rehydrate();
|
||||
await loadLocalPlugins(); // 先发现本地插件(默认关闭),再统一按 enabled 激活
|
||||
const records = usePluginStore.getState().plugins.filter((record) => record.enabled);
|
||||
await Promise.all(
|
||||
records.map(async (record) => {
|
||||
try {
|
||||
activatePlugin(await evaluatePluginSource(record.source));
|
||||
// 本地插件用最新产物,其余用缓存的源码
|
||||
const source = record.local ? await fetchPluginSource(withCacheBust(record.url)) : record.source;
|
||||
activatePlugin(await evaluatePluginSource(source));
|
||||
} catch (error) {
|
||||
console.error(`[plugin] 加载失败: ${record.id}`, error);
|
||||
}
|
||||
@@ -99,6 +109,34 @@ export async function ensurePluginsLoaded() {
|
||||
await loadDevPlugins();
|
||||
}
|
||||
|
||||
// 自动发现 web/public/plugins 下的本地插件:加入列表但默认关闭,
|
||||
// 本地开发放好插件文件即可在管理器里看到并一键启用,无需手动填 URL。
|
||||
// 已在列表中的(用户装过/发现过)不覆盖,尊重其现有开关。
|
||||
async function loadLocalPlugins() {
|
||||
let urls: unknown;
|
||||
try {
|
||||
const response = await fetch("/plugins/index.json");
|
||||
if (!response.ok) return;
|
||||
urls = await response.json();
|
||||
} catch {
|
||||
return; // 无本地清单(如生产环境未构建插件)则跳过
|
||||
}
|
||||
if (!Array.isArray(urls) || !urls.length) return;
|
||||
const store = usePluginStore.getState();
|
||||
await Promise.all(
|
||||
urls.map(async (url: string) => {
|
||||
try {
|
||||
const source = await fetchPluginSource(withCacheBust(url));
|
||||
const plugin = await evaluatePluginSource(source);
|
||||
if (store.plugins.some((item) => item.id === plugin.id)) return;
|
||||
store.upsert({ id: plugin.id, name: plugin.name || plugin.id, version: plugin.version || "0.0.0", description: plugin.description, url, source, enabled: false, local: true });
|
||||
} catch (error) {
|
||||
console.error(`[plugin] 本地插件发现失败: ${url}`, error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 本地开发:VITE_DEV_PLUGINS 里的 URL 每次启动都重新拉取(不缓存、不落库),
|
||||
// 配合 watch 构建即可「改代码→刷新页面」看到最新插件,无需反复安装。
|
||||
async function loadDevPlugins() {
|
||||
@@ -108,7 +146,7 @@ async function loadDevPlugins() {
|
||||
await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
try {
|
||||
const source = await fetchPluginSource(`${url}${url.includes("?") ? "&" : "?"}t=${Date.now()}`);
|
||||
const source = await fetchPluginSource(withCacheBust(url));
|
||||
const plugin = await evaluatePluginSource(source);
|
||||
deactivatePlugin(plugin.id);
|
||||
activatePlugin(plugin);
|
||||
|
||||
@@ -11,6 +11,7 @@ export type InstalledPlugin = {
|
||||
url: string; // 安装来源,可用于更新
|
||||
source: string; // 缓存的插件源码,离线可用、版本固定
|
||||
enabled: boolean;
|
||||
local?: boolean; // 自动发现于 web/public/plugins 的本地插件(默认关闭,启用时按 url 重新拉取)
|
||||
installedAt: string;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user