290 lines
12 KiB
JavaScript
290 lines
12 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const SDK = window.__HERMES_PLUGIN_SDK__;
|
|
const { React } = SDK;
|
|
const { useState, useEffect, useCallback } = SDK.hooks;
|
|
const { Card, CardHeader, CardTitle, CardContent, Button } = SDK.components;
|
|
|
|
// Auth-aware fetch: use SDK.fetchJSON if available (handles profile token/headers),
|
|
// fallback to raw fetch with ?profile= forwarding
|
|
async function safeFetchJSON(path, opts) {
|
|
// Prefer SDK's auth-aware helper
|
|
if (SDK && SDK.fetchJSON) {
|
|
try {
|
|
const data = await SDK.fetchJSON(path, opts || {});
|
|
// SDK.fetchJSON returns parsed JSON directly
|
|
return data;
|
|
} catch (e) {
|
|
// SDK.fetchJSON may throw with body, try to extract
|
|
if (e && e.data) return e.data;
|
|
// fallback to raw fetch below
|
|
if (e && e.message && e.message.includes("Unauthorized")) {
|
|
// will try raw with profile below
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
// Fallback: raw fetch but forward ?profile= from current URL (hermes needs it behind nginx)
|
|
let url = path;
|
|
try {
|
|
const cur = new URL(window.location.href);
|
|
const profile = cur.searchParams.get("profile");
|
|
if (profile) {
|
|
const u = new URL(path, window.location.origin);
|
|
if (!u.searchParams.get("profile")) u.searchParams.set("profile", profile);
|
|
url = u.pathname + u.search;
|
|
}
|
|
} catch (_) { url = path; }
|
|
const res = await fetch(url, opts || {});
|
|
const text = await res.text();
|
|
let json;
|
|
try { json = JSON.parse(text); } catch (_) {
|
|
throw new Error("Server returned non-JSON (status " + res.status + "): " + text.slice(0,800));
|
|
}
|
|
if (!res.ok) {
|
|
// attach body for error display but also return json so UI can show debug
|
|
const err = new Error(json.message || json.detail || ("HTTP " + res.status + ": " + text.slice(0,300)));
|
|
err.data = json;
|
|
err.status = res.status;
|
|
// if 401/403, include profile hint
|
|
if (res.status === 401) err.message += " (Unauthorized — thiếu profile/token, đang thử lại với ?profile=)";
|
|
throw err;
|
|
}
|
|
return json;
|
|
}
|
|
|
|
// Wrapper that returns JSON even on error status so UI can show debug
|
|
async function fetchJSONAllowError(path, opts) {
|
|
if (SDK && SDK.fetchJSON) {
|
|
try { return await SDK.fetchJSON(path, opts || {}); } catch (e) {
|
|
if (e && e.data) return e.data;
|
|
throw e;
|
|
}
|
|
}
|
|
let url = path;
|
|
try {
|
|
const cur = new URL(window.location.href);
|
|
const profile = cur.searchParams.get("profile");
|
|
if (profile) {
|
|
const u = new URL(path, window.location.origin);
|
|
if (!u.searchParams.get("profile")) u.searchParams.set("profile", profile);
|
|
url = u.pathname + u.search;
|
|
}
|
|
} catch (_) {}
|
|
const res = await fetch(url, opts || {});
|
|
const text = await res.text();
|
|
try { return JSON.parse(text); } catch (_) { return { status: "error", message: text.slice(0,800), http_status: res.status }; }
|
|
}
|
|
|
|
function SSHXPage() {
|
|
const [link, setLink] = useState(null);
|
|
const [status, setStatus] = useState("idle");
|
|
const [error, setError] = useState(null);
|
|
const [debug, setDebug] = useState(null);
|
|
|
|
const fetchDebug = useCallback(async function(){
|
|
try { const d = await fetchJSONAllowError("/api/plugins/sshx-link/debug"); setDebug(d); } catch(e){ setDebug({error: e.message}); }
|
|
}, []);
|
|
|
|
const start = useCallback(async function () {
|
|
setStatus("loading");
|
|
setError(null);
|
|
setDebug(null);
|
|
try {
|
|
var st = await fetchJSONAllowError("/api/plugins/sshx-link/status");
|
|
if (st && st.detail === "Unauthorized") {
|
|
throw new Error("Unauthorized — thử dùng SDK.fetchJSON (đã fix). Hãy hard reload Ctrl+Shift+R sau khi update plugin v2.0.2");
|
|
}
|
|
if (st.status === "installing" || st.status === "starting") {
|
|
setStatus("installing");
|
|
setError(st.message || "Đang xử lý...");
|
|
fetchDebug();
|
|
return;
|
|
}
|
|
if (st.status === "running" && st.link) {
|
|
setLink(st.link);
|
|
setStatus("running");
|
|
return;
|
|
}
|
|
var res = await fetchJSONAllowError("/api/plugins/sshx-link/start");
|
|
if (res.detail === "Unauthorized") {
|
|
throw new Error("Unauthorized khi gọi /start — plugin frontend chưa dùng SDK auth. Đã fix ở v2.0.2, hãy hard reload.");
|
|
}
|
|
if (res.link) {
|
|
setLink(res.link);
|
|
setStatus("running");
|
|
} else if (res.status === "starting") {
|
|
setStatus("installing");
|
|
setError(res.message || "sshx đang khởi động, chờ 2s rồi thử lại... " + (res.output_tail || ""));
|
|
fetchDebug();
|
|
} else {
|
|
setError((res.message || res.detail || JSON.stringify(res)) + (res.output_tail ? "\nTail: " + res.output_tail : "") + (res.debug ? "\nDebug: " + JSON.stringify(res.debug) : ""));
|
|
setStatus("error");
|
|
fetchDebug();
|
|
}
|
|
} catch (err) {
|
|
const detail = err.data ? (err.data.detail || err.data.message || JSON.stringify(err.data)) : "";
|
|
setError((err.message || "Failed to start sshx") + (detail ? " | detail: " + detail : ""));
|
|
setStatus("error");
|
|
fetchDebug();
|
|
}
|
|
}, [fetchDebug]);
|
|
|
|
const stop = useCallback(async function () {
|
|
try {
|
|
await fetchJSONAllowError("/api/plugins/sshx-link/stop", { method: "POST" });
|
|
setLink(null);
|
|
setStatus("idle");
|
|
setError(null);
|
|
setDebug(null);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
}
|
|
}, []);
|
|
|
|
const checkStatus = useCallback(async function () {
|
|
try {
|
|
const res = await fetchJSONAllowError("/api/plugins/sshx-link/status");
|
|
if (res.detail === "Unauthorized") return;
|
|
if (res.status === "running" && res.link) {
|
|
setLink(res.link);
|
|
setStatus("running");
|
|
setError(null);
|
|
} else if (res.status === "installing" || res.status === "starting") {
|
|
setStatus("installing");
|
|
if (res.message) setError(res.message);
|
|
} else if (res.status === "error") {
|
|
setStatus("error");
|
|
setError(res.message + (res.output_tail ? "\nTail: " + res.output_tail : ""));
|
|
setDebug(res);
|
|
} else if (res.status === "stopped") {
|
|
if (status === "running") {
|
|
setLink(null);
|
|
setStatus("idle");
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
}, [status]);
|
|
|
|
useEffect(function () {
|
|
checkStatus();
|
|
}, [checkStatus]);
|
|
|
|
useEffect(function () {
|
|
if (status !== "installing" && status !== "loading") return;
|
|
var timer = setInterval(function () {
|
|
checkStatus();
|
|
}, 2500);
|
|
return function () { clearInterval(timer); };
|
|
}, [status, checkStatus]);
|
|
|
|
if (status === "installing" || status === "loading") {
|
|
return React.createElement(Card, null,
|
|
React.createElement(CardHeader, null,
|
|
React.createElement(CardTitle, null, "sshx.io Shared Terminal"),
|
|
React.createElement("p", { className: "text-xs text-yellow-500", style: { fontWeight: 600 } }, status==="loading" ? "Đang tạo shell ở sshx.io..." : "Đang cài / khởi động sshx...")
|
|
),
|
|
React.createElement(CardContent, null,
|
|
React.createElement("p", { className: "text-sm", style: { whiteSpace:"pre-wrap", fontWeight:600, color:"#eab308"}}, error || "Đang tải binary từ s3.amazonaws.com/sshx..."),
|
|
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } }, "Tự động thử lại mỗi 2.5s. Nếu kẹt quá 20s, kiểm tra outbound tới s3.amazonaws.com và sshx.io trên server 13.140.132.105."),
|
|
debug && React.createElement("pre", { style:{marginTop:"0.75rem", fontSize:"0.68rem", background:"#111", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"200px", whiteSpace:"pre-wrap"}}, JSON.stringify(debug, null, 2)),
|
|
React.createElement("div", { style:{display:"flex", gap:"0.5rem", marginTop:"0.75rem"}},
|
|
React.createElement(Button, { onClick: checkStatus, variant:"outline", size:"sm"}, "Refresh"),
|
|
React.createElement(Button, { onClick: stop, variant:"destructive", size:"sm"}, "Stop")
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
if (status === "running" && link) {
|
|
return React.createElement(Card, null,
|
|
React.createElement(CardHeader, null,
|
|
React.createElement(CardTitle, null, "sshx.io Shared Terminal"),
|
|
React.createElement("p", { className: "text-xs text-green-500", style: { fontWeight: 600 } }, "● Running — shell đã tạo ở sshx.io")
|
|
),
|
|
React.createElement(CardContent, null,
|
|
React.createElement("a", {
|
|
href: link,
|
|
target: "_blank",
|
|
rel: "noopener noreferrer",
|
|
style: {
|
|
color: "#60a5fa",
|
|
fontSize: "1.05rem",
|
|
wordBreak: "break-all",
|
|
textDecoration: "underline",
|
|
display: "block",
|
|
marginBottom: "0.75rem",
|
|
fontWeight: 600
|
|
}
|
|
}, link),
|
|
React.createElement("div", { style: { display: "flex", gap: "0.5rem", marginBottom: "1rem", flexWrap: "wrap" } },
|
|
React.createElement(Button, {
|
|
onClick: function() { navigator.clipboard && navigator.clipboard.writeText(link); },
|
|
variant: "outline",
|
|
size: "sm"
|
|
}, "Copy Link"),
|
|
React.createElement(Button, {
|
|
onClick: function() { window.open(link, "_blank"); },
|
|
variant: "outline",
|
|
size: "sm"
|
|
}, "Open"),
|
|
React.createElement(Button, {
|
|
onClick: stop,
|
|
variant: "destructive",
|
|
size: "sm"
|
|
}, "Stop")
|
|
),
|
|
React.createElement("div", {
|
|
style: {
|
|
border: "1px solid #333",
|
|
borderRadius: "8px",
|
|
overflow: "hidden",
|
|
height: "560px",
|
|
background: "#000"
|
|
}
|
|
},
|
|
React.createElement("iframe", {
|
|
src: link,
|
|
style: { width: "100%", height: "100%", border: "0" },
|
|
title: "sshx Terminal",
|
|
allow: "clipboard-read; clipboard-write"
|
|
})
|
|
),
|
|
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } },
|
|
"Terminal nhúng ở trên. Nếu iframe bị chặn, bấm Open. Link dạng https://sshx.io/s/<id>#<key> — chia sẻ cho user là xong."
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
return React.createElement(Card, null,
|
|
React.createElement(CardHeader, null,
|
|
React.createElement(CardTitle, null, "sshx.io Shared Terminal"),
|
|
React.createElement("p", { className: "text-xs text-muted-foreground" }, "Tạo shell ở sshx.io và trả link chia sẻ — auto arch, không cần restart")
|
|
),
|
|
React.createElement(CardContent, null,
|
|
React.createElement("p", {
|
|
className: "text-sm text-muted-foreground",
|
|
style: { marginBottom: "1rem" }
|
|
}, "Bấm Start để tạo shell cộng tác trên sshx.io. Plugin sẽ tự tải binary sshx (S3), chạy sshx --quiet và parse link https://sshx.io/s/..."),
|
|
error && React.createElement("pre", {
|
|
style: { marginBottom: "1rem", whiteSpace: "pre-wrap", fontSize:"0.85rem", color:"#ef4444", background:"#1a0a0a", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"300px"}
|
|
}, "Error: " + error),
|
|
debug && React.createElement("pre", { style:{marginBottom:"1rem", fontSize:"0.68rem", background:"#111", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"200px", whiteSpace:"pre-wrap"}}, "Debug: " + JSON.stringify(debug, null, 2)),
|
|
React.createElement("div", { style:{display:"flex", gap:"0.5rem", flexWrap:"wrap"}},
|
|
React.createElement(Button, {
|
|
onClick: start,
|
|
disabled: status === "loading"
|
|
}, status === "loading" ? "Đang tạo shell..." : "Start sshx → Lấy link"),
|
|
React.createElement(Button, { onClick: fetchDebug, variant:"outline", size:"sm"}, "Debug")
|
|
),
|
|
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.75rem"}}, "Nếu lỗi Unauthorized, hãy hard reload (Ctrl+Shift+R) để load v2.0.2 — bản này dùng SDK.fetchJSON có auth và tự forward ?profile=.")
|
|
)
|
|
);
|
|
}
|
|
|
|
window.__HERMES_PLUGINS__.register("sshx-link", SSHXPage);
|
|
})();
|