feat: milestone 5b — frontend bundle host + VerstakPluginAPI stub

- Bundle contract: window.VerstakPluginRegister(id, {components: {...}})
- PluginBundleHost.svelte: loads bundle via GetPluginAssetContent, mounts components
- VerstakPluginAPI.js: restricted API (capabilities, events, settings, commands — all stub)
- ViewContainer: PluginBundleHost replaces placeholder when frontend bundle exists
- PluginManager: settings panel via PluginBundleHost (removed hardcoded form)
- Backend: GetPluginFrontendInfo, GetPluginAssetContent with path security
- Security: reject absolute paths, path traversal, escape from plugin root
- Error boundary: bundle load/execute/mount errors show fallback, not crash
- Tests: 11 backend tests (asset API), frontend bundle checks in smoke
- Docs: bundle contract, VerstakPluginAPI, security constraints
This commit is contained in:
2026-06-17 17:39:50 +08:00
parent 1dff97a9c0
commit 05ef1449bc
13 changed files with 887 additions and 30 deletions
+41 -7
View File
@@ -1,4 +1,5 @@
<script>
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
import { onMount } from 'svelte';
import * as App from '../../../wailsjs/go/api/App';
@@ -27,11 +28,18 @@
? plugins.find(p => p.manifest?.id === currentView.pluginId)
: null;
$: pluginStatus = currentPlugin ? currentPlugin.status : 'unknown';
$: hasFrontend = currentPlugin?.manifest?.frontend?.entry != null;
$: hostPluginId = currentView?.pluginId || activeViewPluginId;
$: hostComponentId = currentView?.component || null;
// Reset render error when view changes
$: if (activeView) {
renderError = null;
}
function onHostError(e) {
renderError = e.detail?.message || 'Plugin view error';
}
</script>
{#key `${activeViewPluginId}:${activeView}`}
@@ -51,17 +59,27 @@
<div class="view-header">
<span class="view-icon">{currentView.icon || '📦'}</span>
<h2>{currentView.title}</h2>
{#if hasFrontend}
<span class="frontend-badge">frontend bundle</span>
{:else}
<span class="no-frontend-badge">no frontend bundle</span>
{/if}
</div>
<div class="view-content">
<div class="plugin-view-host" data-view-id={currentView.id} data-component={currentView.component}>
{#if hasFrontend}
<PluginBundleHost
pluginId={hostPluginId}
componentId={hostComponentId}
/>
{:else}
<div class="placeholder">
<p class="placeholder-label">Plugin View Host</p>
<p class="placeholder-info"><span class="placeholder-key">Plugin:</span> <strong>{currentView.pluginId}</strong></p>
<p class="placeholder-info"><span class="placeholder-key">View ID:</span> <code>{currentView.id}</code></p>
<p class="placeholder-info"><span class="placeholder-key">Component:</span> <code>{currentView.component}</code></p>
<p class="placeholder-badge">frontend bundle host not implemented yet</p>
<p class="placeholder-badge">frontend bundle not available</p>
</div>
</div>
{/if}
</div>
</div>
</div>
@@ -109,14 +127,30 @@
flex: 1;
}
.view-icon { font-size: 1.3rem; }
.frontend-badge {
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
background: rgba(78, 204, 163, 0.15);
color: #4ecca3;
border-radius: 8px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.no-frontend-badge {
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
background: rgba(233, 69, 96, 0.1);
color: #e94560;
border-radius: 8px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.view-content {
flex: 1;
overflow: auto;
}
.plugin-view-host {
width: 100%;
min-height: 200px;
}
.placeholder {
color: #666;
font-style: italic;