更新客户端渲染,更新了壳
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>YMhut Plugin Host</title>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0f141a;
|
||||
--panel: #171d24;
|
||||
--panel-strong: #0a0d12;
|
||||
--stroke: #2a323d;
|
||||
--text: #f4f7fb;
|
||||
--muted: #9aa8b8;
|
||||
--accent: #4da3ff;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 44px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.title-stack { min-width: 0; }
|
||||
#title { display: block; font-size: 15px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#meta { margin-top: 2px; color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.header-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
|
||||
|
||||
button {
|
||||
border: 1px solid var(--stroke);
|
||||
border-radius: 6px;
|
||||
padding: 7px 11px;
|
||||
color: var(--text);
|
||||
background: #1f2730;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover { border-color: #445365; background: #26313d; }
|
||||
button.primary { border-color: #2f6fb3; background: #184f8f; }
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
position: fixed;
|
||||
inset: auto 0 0 0;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
grid-template-rows: 44px 1fr;
|
||||
border-top: 1px solid var(--stroke);
|
||||
background: var(--panel-strong);
|
||||
transition: height 160ms ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.drawer.expanded { height: min(42vh, 320px); }
|
||||
|
||||
.drawer-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 6px 12px;
|
||||
background: #111821;
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.drawer-title strong { color: var(--text); font-size: 13px; }
|
||||
.drawer-actions { display: flex; gap: 8px; flex: 0 0 auto; }
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 12px 16px 18px;
|
||||
overflow: auto;
|
||||
color: #d7e3f4;
|
||||
background: #05070a;
|
||||
font: 12px/1.45 Consolas, "Cascadia Mono", monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<div class="title-stack">
|
||||
<strong id="title">YMhut Plugin Host</strong>
|
||||
<div id="meta"></div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="open-folder-top" type="button">Open Plugin Folder</button>
|
||||
</div>
|
||||
</header>
|
||||
<iframe id="plugin-frame" title="Plugin surface"></iframe>
|
||||
</main>
|
||||
<aside id="drawer" class="drawer" aria-label="Shell output drawer">
|
||||
<div class="drawer-bar">
|
||||
<div class="drawer-title">
|
||||
<strong>Shell Output</strong>
|
||||
<span id="drawer-summary">ready</span>
|
||||
</div>
|
||||
<div class="drawer-actions">
|
||||
<button id="open-folder-bottom" type="button">Open Plugin Folder</button>
|
||||
<button id="toggle-drawer" class="primary" type="button" aria-expanded="false">Expand</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="shell-log">Waiting for plugin shell output...</pre>
|
||||
</aside>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "ymhut-box-plugin-tauri-host",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"build": "tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.0"
|
||||
}
|
||||
}
|
||||
+4390
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "ymhut-box-plugin-tauri-host"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,116 @@
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tauri::Manager;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RuntimeArgs {
|
||||
session: String,
|
||||
plugin_id: String,
|
||||
surface_id: String,
|
||||
runtime_kind: String,
|
||||
plugin_root: String,
|
||||
manifest: String,
|
||||
entry: String,
|
||||
command: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn runtime_args(state: tauri::State<'_, RuntimeArgs>) -> RuntimeArgs {
|
||||
state.inner().clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_plugin_folder(path: String) -> Result<(), String> {
|
||||
if path.trim().is_empty() {
|
||||
return Err("Plugin folder path is empty.".into());
|
||||
}
|
||||
|
||||
let status = if cfg!(target_os = "windows") {
|
||||
Command::new("explorer").arg(path).status()
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Command::new("open").arg(path).status()
|
||||
} else {
|
||||
Command::new("xdg-open").arg(path).status()
|
||||
}
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Open folder command exited with {status}."))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn read_plugin_entry(path: String, plugin_root: String) -> Result<String, String> {
|
||||
if path.trim().is_empty() {
|
||||
return Err("Plugin entry path is empty.".into());
|
||||
}
|
||||
|
||||
let root = fs::canonicalize(Path::new(&plugin_root)).map_err(|error| error.to_string())?;
|
||||
let entry = fs::canonicalize(Path::new(&path)).map_err(|error| error.to_string())?;
|
||||
if !entry.starts_with(&root) {
|
||||
return Err("Plugin entry must stay inside the plugin directory.".into());
|
||||
}
|
||||
|
||||
fs::read_to_string(entry).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn parse_runtime_args() -> RuntimeArgs {
|
||||
let raw: Vec<String> = std::env::args().collect();
|
||||
let mut values = HashMap::new();
|
||||
let mut index = 1;
|
||||
while index < raw.len() {
|
||||
if let Some(key) = raw[index].strip_prefix("--") {
|
||||
if index + 1 < raw.len() && !raw[index + 1].starts_with("--") {
|
||||
values.insert(key.to_string(), raw[index + 1].clone());
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
RuntimeArgs {
|
||||
session: values.remove("session").unwrap_or_default(),
|
||||
plugin_id: values.remove("plugin-id").unwrap_or_else(|| "unknown".into()),
|
||||
surface_id: values.remove("surface-id").unwrap_or_else(|| "default".into()),
|
||||
runtime_kind: values.remove("runtime-kind").unwrap_or_else(|| "tauri".into()),
|
||||
plugin_root: values.remove("plugin-root").unwrap_or_default(),
|
||||
manifest: values.remove("manifest").unwrap_or_default(),
|
||||
entry: values.remove("entry").unwrap_or_default(),
|
||||
command: values.remove("command"),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let runtime = parse_runtime_args();
|
||||
let window_title_runtime = runtime.clone();
|
||||
let ready = serde_json::json!({
|
||||
"type": "ready",
|
||||
"version": "1",
|
||||
"session": &runtime.session,
|
||||
"pluginId": &runtime.plugin_id,
|
||||
"surfaceId": &runtime.surface_id,
|
||||
"runtimeKind": &runtime.runtime_kind
|
||||
});
|
||||
println!("{ready}");
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(runtime.clone())
|
||||
.invoke_handler(tauri::generate_handler![runtime_args, open_plugin_folder, read_plugin_entry])
|
||||
.setup(move |app| {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let title = format!("{} - YMhut Plugin Host", window_title_runtime.plugin_id);
|
||||
let _ = window.set_title(&title);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running YMhut plugin host");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "YMhut Plugin Host",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.ymhut.box.pluginhost",
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "YMhut Plugin Host",
|
||||
"width": 1100,
|
||||
"height": 760
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
const params = new URLSearchParams(location.search);
|
||||
const invoke = window.__TAURI__?.core?.invoke ?? (async () => {
|
||||
throw new Error("Tauri invoke API is not available.");
|
||||
});
|
||||
const title = document.querySelector("#title");
|
||||
const meta = document.querySelector("#meta");
|
||||
const frame = document.querySelector("#plugin-frame");
|
||||
const log = document.querySelector("#shell-log");
|
||||
const drawer = document.querySelector("#drawer");
|
||||
const drawerSummary = document.querySelector("#drawer-summary");
|
||||
const toggleDrawer = document.querySelector("#toggle-drawer");
|
||||
const openFolderTop = document.querySelector("#open-folder-top");
|
||||
const openFolderBottom = document.querySelector("#open-folder-bottom");
|
||||
|
||||
let runtime = {
|
||||
session: params.get("session") || "",
|
||||
pluginId: params.get("pluginId") || "YMhut Plugin Host",
|
||||
surfaceId: params.get("surfaceId") || "unknown",
|
||||
runtimeKind: params.get("runtimeKind") || "tauri",
|
||||
pluginRoot: params.get("pluginRoot") || "",
|
||||
entry: params.get("entry") || ""
|
||||
};
|
||||
|
||||
function normalizePath(path) {
|
||||
if (!path) return "";
|
||||
return path.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function directoryUrl(path) {
|
||||
const normalized = normalizePath(path);
|
||||
if (!normalized) return "";
|
||||
const directory = normalized.slice(0, normalized.lastIndexOf("/") + 1);
|
||||
return new URL(`file:///${directory}`).href;
|
||||
}
|
||||
|
||||
function createYmhutBridgeScript() {
|
||||
return `
|
||||
<script>
|
||||
(() => {
|
||||
if (window.ymhut) return;
|
||||
const hostPost = (line) => {
|
||||
try { parent.window.ymhutPluginHost?.appendShellLine?.(line); } catch {}
|
||||
};
|
||||
const storagePrefix = "ymhut-plugin:" + ${JSON.stringify(runtime.pluginId || "unknown")} + ":";
|
||||
async function ok(value) { return value; }
|
||||
async function fetchViaBrowser(request) {
|
||||
const input = typeof request === "string" ? { url: request } : (request || {});
|
||||
const response = await fetch(input.url, {
|
||||
method: input.method || "GET",
|
||||
headers: input.headers || {},
|
||||
body: input.body
|
||||
});
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: response.url,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
content: await response.text()
|
||||
};
|
||||
}
|
||||
window.ymhut = {
|
||||
input: { get: () => ok(""), set: value => ok(value), onInputChanged: () => {} },
|
||||
output: {
|
||||
set: value => { hostPost("[output] " + String(value ?? "")); return ok(true); },
|
||||
append: value => { hostPost("[output] " + String(value ?? "")); return ok(true); },
|
||||
clear: () => ok(true)
|
||||
},
|
||||
log: {
|
||||
info: (message, detail) => { hostPost("[info] " + message + (detail ? " " + detail : "")); return ok(true); },
|
||||
warn: (message, detail) => { hostPost("[warn] " + message + (detail ? " " + detail : "")); return ok(true); },
|
||||
error: (message, detail) => { hostPost("[error] " + message + (detail ? " " + detail : "")); return ok(true); }
|
||||
},
|
||||
storage: {
|
||||
get: key => ok(localStorage.getItem(storagePrefix + key)),
|
||||
set: (key, value) => { localStorage.setItem(storagePrefix + key, value); return ok(true); },
|
||||
remove: key => { localStorage.removeItem(storagePrefix + key); return ok(true); },
|
||||
list: () => ok(Object.keys(localStorage).filter(k => k.startsWith(storagePrefix)).map(k => k.slice(storagePrefix.length)))
|
||||
},
|
||||
http: { fetch: fetchViaBrowser },
|
||||
network: {
|
||||
diagnostics: () => ok({ interfaces: [], summary: {}, proxy: {}, note: "Tauri compatibility bridge: native diagnostics are not connected yet." }),
|
||||
ping: request => ok({ request, note: "Ping is not available in the Tauri compatibility bridge yet." }),
|
||||
dnsLookup: request => ok({ request, addresses: [], note: "DNS lookup is not available in the Tauri compatibility bridge yet." }),
|
||||
traceRoute: request => ok({ request, hops: [], note: "Trace route is not available in the Tauri compatibility bridge yet." })
|
||||
},
|
||||
clipboard: {
|
||||
readText: () => navigator.clipboard?.readText?.() ?? ok(""),
|
||||
writeText: text => navigator.clipboard?.writeText?.(String(text ?? "")) ?? ok(false)
|
||||
},
|
||||
file: {
|
||||
openPicker: () => ok(null),
|
||||
savePicker: (name, value) => {
|
||||
const blob = new Blob([String(value ?? "")], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = name || "plugin-output.txt";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
return ok({ name: a.download });
|
||||
}
|
||||
},
|
||||
tool: { run: (toolId, input) => ok({ toolId, input, note: "Tool bridge is not available in the Tauri compatibility bridge yet." }) },
|
||||
openExternal: (url) => { window.open(url, "_blank", "noopener,noreferrer"); return ok(true); }
|
||||
};
|
||||
})();
|
||||
<\/script>`;
|
||||
}
|
||||
|
||||
async function loadPluginEntry() {
|
||||
const entry = runtime.entry || `${runtime.pluginRoot}\\index.html`;
|
||||
if (!entry) {
|
||||
appendShellLine("[host:error] Plugin entry is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
const base = directoryUrl(entry);
|
||||
try {
|
||||
const html = await invoke("read_plugin_entry", { path: entry, pluginRoot: runtime.pluginRoot });
|
||||
frame.srcdoc = html.replace(/<head([^>]*)>/i, `<head$1><base href="${base}">${createYmhutBridgeScript()}`);
|
||||
appendShellLine(`[host] Loaded plugin entry through compatibility bridge: ${entry}`);
|
||||
} catch (error) {
|
||||
frame.src = new URL(`file:///${normalizePath(entry)}`).href;
|
||||
appendShellLine(`[host:warn] Falling back to direct file load: ${error}`);
|
||||
appendShellLine(`[host] Loaded plugin entry: ${entry}`);
|
||||
}
|
||||
}
|
||||
|
||||
function appendShellLine(line) {
|
||||
if (!line) return;
|
||||
if (log.textContent === "Waiting for plugin shell output...") {
|
||||
log.textContent = "";
|
||||
}
|
||||
|
||||
log.textContent += `${log.textContent ? "\n" : ""}${line}`;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
drawerSummary.textContent = line.length > 96 ? `${line.slice(0, 96)}...` : line;
|
||||
}
|
||||
|
||||
async function openPluginFolder() {
|
||||
try {
|
||||
if (!runtime.pluginRoot) {
|
||||
throw new Error("Plugin folder path is empty.");
|
||||
}
|
||||
|
||||
await invoke("open_plugin_folder", { path: runtime.pluginRoot });
|
||||
appendShellLine(`[host] Opened plugin folder: ${runtime.pluginRoot}`);
|
||||
} catch (error) {
|
||||
appendShellLine(`[host:error] ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function applyRuntime(runtimeArgs) {
|
||||
runtime = { ...runtime, ...runtimeArgs };
|
||||
title.textContent = runtime.pluginId || "YMhut Plugin Host";
|
||||
meta.textContent = `session=${runtime.session || "unknown"} surface=${runtime.surfaceId || "unknown"} runtime=${runtime.runtimeKind || "tauri"}`;
|
||||
|
||||
loadPluginEntry();
|
||||
}
|
||||
|
||||
window.ymhutPluginHost = {
|
||||
appendShellLine
|
||||
};
|
||||
|
||||
toggleDrawer.addEventListener("click", () => {
|
||||
const expanded = !drawer.classList.contains("expanded");
|
||||
drawer.classList.toggle("expanded", expanded);
|
||||
toggleDrawer.textContent = expanded ? "Collapse" : "Expand";
|
||||
toggleDrawer.setAttribute("aria-expanded", String(expanded));
|
||||
});
|
||||
|
||||
openFolderTop.addEventListener("click", openPluginFolder);
|
||||
openFolderBottom.addEventListener("click", openPluginFolder);
|
||||
|
||||
invoke("runtime_args")
|
||||
.then(applyRuntime)
|
||||
.catch((error) => {
|
||||
appendShellLine(`[host:error] Failed to load runtime args: ${error}`);
|
||||
applyRuntime(runtime);
|
||||
});
|
||||
Reference in New Issue
Block a user