This commit is contained in:
eric
2026-05-28 15:59:03 -05:00
parent 1d89aa29e0
commit 08155500fc
47 changed files with 1126 additions and 122 deletions

View File

@@ -60,6 +60,14 @@ type ServiceItem = {
port?: string;
message?: string;
};
type ShellCrashApplyResponse = {
message?: string;
path?: string;
target_node?: string;
default_groups?: string[];
selected?: string;
service?: ServiceItem;
};
type SystemSnapshot = {
cpu_load?: LoadAvg;
disk?: Capacity;
@@ -120,6 +128,8 @@ type PreviewMode = "tap" | "double_tap" | "long_press";
const TABS: TabKey[] = ["overview", "business", "modules", "shellcrash", "android", "config"];
const SHELLCRASH_ROOT_IDS = new Set(["shellcrash-configs", "shellcrash-yamls", "shellcrash-jsons"]);
const SHELLCRASH_DEFAULT_PROFILE = "LsRzcR3pHoA9.yaml";
const SHELLCRASH_DEFAULT_NODE = "5d723acc-singbox_tuic3";
const apiBase = () =>
(typeof process !== "undefined" && process.env.NEXT_PUBLIC_API_URL) || "";
@@ -711,6 +721,37 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
}
};
const applyShellCrashProfile = async (profilePath?: string) => {
const nextPath =
profilePath ||
(rootId === "shellcrash-yamls" && filePath ? filePath : SHELLCRASH_DEFAULT_PROFILE);
setBusy("shellcrash:apply");
try {
if (rootId === "shellcrash-yamls" && filePath === nextPath) {
await fetchJson("/managed_file", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ root: rootId, path: filePath, content: fileContent }),
});
}
const data = await fetchJson<ShellCrashApplyResponse>("/shellcrash/apply_profile", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: nextPath, target_node: SHELLCRASH_DEFAULT_NODE }),
});
const selected = data.selected ? ` | ${data.selected}` : "";
notify(`${data.message || tr("ShellCrash 配置已应用", "ShellCrash profile applied")}${selected}`);
await Promise.all([refreshServices(), refreshFiles("shellcrash-yamls", "config.yaml")]);
setRootId("shellcrash-yamls");
setFilePath("config.yaml");
setConfigInsightVersion((value) => value + 1);
} catch (reason) {
notify((reason as Error).message);
} finally {
setBusy(null);
}
};
const saveFile = async () => {
if (!rootId || !filePath) return;
setBusy("file:save");
@@ -1301,12 +1342,19 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
<Button className="bg-emerald-600" disabled={!shellcrashService.available || !!busy} onClick={() => void serviceAction("shellcrash", "start")}>{tr("启动", "Start")}</Button>
<Button className="bg-amber-600" disabled={!shellcrashService.available || !!busy} onClick={() => void serviceAction("shellcrash", "restart")}>{tr("重启", "Restart")}</Button>
<Button className="bg-rose-600" disabled={!shellcrashService.available || !!busy} onClick={() => void serviceAction("shellcrash", "stop")}>{tr("停止", "Stop")}</Button>
<Button className="bg-sky-600" disabled={!shellcrashService.available || !!busy} onClick={() => void applyShellCrashProfile(SHELLCRASH_DEFAULT_PROFILE)}>{busy === "shellcrash:apply" ? tr("应用中...", "Applying...") : tr("应用默认配置", "Apply Default")}</Button>
</div>
</div>
) : null}
<div className="mt-4 rounded-2xl border border-sky-400/20 bg-sky-500/10 p-4 text-xs text-sky-100">
<p className="font-semibold">{tr("默认代理节点", "Default proxy node")}</p>
<p className="mt-2 break-all font-mono text-[11px] text-sky-200">{SHELLCRASH_DEFAULT_NODE}</p>
<p className="mt-2 text-sky-100/80">{tr("应用后会写入 config.yaml、重启 ShellCrash并把包含该节点的选择组切到此节点。", "Applying writes config.yaml, restarts ShellCrash, and selects this node in matching groups.")}</p>
</div>
<div className="mt-4 rounded-2xl border border-white/8 bg-black/20 p-4 text-xs text-slate-400">
<p className="text-white">{tr("快捷文件", "Quick Files")}</p>
<div className="mt-3 grid gap-2">
<button className="btn-focus rounded-xl border border-white/10 px-3 py-2 text-left text-slate-200" onClick={() => void openManagedFile("shellcrash-yamls", SHELLCRASH_DEFAULT_PROFILE)}>{SHELLCRASH_DEFAULT_PROFILE}</button>
<button className="btn-focus rounded-xl border border-white/10 px-3 py-2 text-left text-slate-200" onClick={() => void openManagedFile("shellcrash-yamls", "config.yaml")}>config.yaml</button>
<button className="btn-focus rounded-xl border border-white/10 px-3 py-2 text-left text-slate-200" onClick={() => void openManagedFile("shellcrash-configs", "ShellCrash.cfg")}>ShellCrash.cfg</button>
<button className="btn-focus rounded-xl border border-white/10 px-3 py-2 text-left text-slate-200" onClick={() => void openManagedFile("shellcrash-configs", "servers.list")}>servers.list</button>
@@ -1325,6 +1373,7 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
{busy === "file:import" ? tr("导入中...", "Importing...") : tr("导入/替换", "Import/Replace")}
<input key={fileImportKey} className="hidden" disabled={!!busy} type="file" accept=".yaml,.yml,.json,.cfg,.env,.list,.txt" onChange={(event) => void importManagedFile(event)} />
</label>
<Button className="bg-sky-600" disabled={rootId !== "shellcrash-yamls" || !filePath || !!busy} onClick={() => void applyShellCrashProfile(filePath)}>{busy === "shellcrash:apply" ? tr("应用中...", "Applying...") : tr("应用当前 YAML", "Apply YAML")}</Button>
<Button className="bg-cyan-600" disabled={!filePath || !!busy} onClick={() => void saveFile()}>{busy === "file:save" ? tr("保存中...", "Saving...") : tr("保存", "Save")}</Button>
</div>
</div>