更新客户端渲染,更新了壳

This commit is contained in:
QWQLwToo
2026-07-06 23:05:40 +08:00
parent e7dd87bf7e
commit 31d778710b
1311 changed files with 172662 additions and 1582 deletions
+146
View File
@@ -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"
}
}
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
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
}
]
}
}
+181
View File
@@ -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);
});