fix7: закрыть P0/P1 блокеры runtime/install и перевести hysteria UI в read-only

This commit is contained in:
2026-04-28 18:36:41 +05:00
parent 087d9f616a
commit 5455e41fc1
7 changed files with 26 additions and 357 deletions
+1
View File
@@ -7,6 +7,7 @@ export {}
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
+11 -349
View File
@@ -2,28 +2,6 @@
<div class="app-container"> <div class="app-container">
<div class="search"> <div class="search">
<el-form :inline="true"> <el-form :inline="true">
<el-form-item>
<el-button type="primary" @click="submitForm" :icon="Select">
{{ $t("common.save") }}
</el-button>
</el-form-item>
<el-form-item>
<el-upload
v-model:file-list="fileList"
:http-request="handleImport"
:show-file-list="false"
accept=".yaml"
:limit="1"
:before-upload="beforeImport"
>
<el-button>
<template #icon>
<i-ep-upload />
</template>
{{ $t("common.import") }}
</el-button>
</el-upload>
</el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleExport"> <el-button @click="handleExport">
<template #icon> <template #icon>
@@ -32,53 +10,18 @@
{{ $t("common.export") }} {{ $t("common.export") }}
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item>
<el-button @click="handleReset">
<template #icon>
<i-ep-refresh />
</template>
{{ $t("common.reset") }}
</el-button>
</el-form-item>
</el-form> </el-form>
</div> </div>
<el-card shadow="never"> <el-alert
<el-form ref="enableFormRef" :model="enableForm" inline> title="Hysteria runtime config is managed by hy2xs-orchestrator reconfigure."
<el-tooltip :content="$t('hysteria.config.enable')" placement="bottom"> type="info"
<el-form-item prop="enable"> :closable="false"
<el-switch style="margin-bottom: 12px"
v-model="enableForm.enable"
active-value="1"
inactive-value="0"
@change="handleChangeEnable"
:active-text="$t('hysteria.enable')"
:inactive-text="$t('hysteria.disable')"
style="height: 32px"
/> />
</el-form-item>
</el-tooltip> <el-card shadow="never">
<el-form-item> <el-form inline>
<el-dropdown @command="handleDropdownClick">
<el-button
type="primary"
:icon="CirclePlusFilled"
style="height: 32px"
>
{{ $t("hysteria.addConfigItem") }}
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:key="item"
v-for="item in tabs"
:command="item.name"
>{{ item.desc }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-form-item>
<el-form-item> <el-form-item>
<el-tag style="height: 32px"> <el-tag style="height: 32px">
{{ $t("hysteria.hysteria2Version") }}: {{ $t("hysteria.hysteria2Version") }}:
@@ -105,12 +48,11 @@
:rules="dataFormRules" :rules="dataFormRules"
label-position="top" label-position="top"
:model="dataForm" :model="dataForm"
:disabled="true"
> >
<el-tabs <el-tabs
v-model="activeName" v-model="activeName"
class="tabs" class="tabs"
closable
@tab-remove="closeTabPane"
> >
<el-tab-pane :label="$t('hysteria.extension')" name="extension"> <el-tab-pane :label="$t('hysteria.extension')" name="extension">
<el-tooltip <el-tooltip
@@ -1057,21 +999,16 @@ import {
Tab, Tab,
} from "@/api/config/types"; } from "@/api/config/types";
import Outbounds from "./components/Outbounds/index.vue"; import Outbounds from "./components/Outbounds/index.vue";
import { CirclePlusFilled, Select } from "@element-plus/icons-vue";
import { import {
exportHysteria2ConfigApi, exportHysteria2ConfigApi,
getHysteria2ConfigApi, getHysteria2ConfigApi,
importHysteria2ConfigApi,
listConfigApi, listConfigApi,
updateConfigsApi,
updateHysteria2ConfigApi,
uploadCertFileApi, uploadCertFileApi,
} from "@/api/config"; } from "@/api/config";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { assignWith, deepCopy } from "@/utils/copy"; import { assignWith, deepCopy } from "@/utils/copy";
import { import {
UploadFile, UploadFile,
UploadRawFile,
UploadRequestOptions, UploadRequestOptions,
} from "element-plus/lib/components"; } from "element-plus/lib/components";
import { monitorHysteria2Api } from "@/api/monitor"; import { monitorHysteria2Api } from "@/api/monitor";
@@ -1079,7 +1016,6 @@ import { UploadUserFile } from "element-plus";
const { t } = useI18n(); const { t } = useI18n();
const hysteria2EnableKey = "HYSTERIA2_ENABLE";
const hysteria2Remark = "HYSTERIA2_CONFIG_REMARK"; const hysteria2Remark = "HYSTERIA2_CONFIG_REMARK";
const clashExtension = "CLASH_EXTENSION"; const clashExtension = "CLASH_EXTENSION";
const dataFormRef = ref(ElForm); const dataFormRef = ref(ElForm);
@@ -1119,9 +1055,6 @@ const resolverTypes = ref<string[]>(["tcp", "udp", "tls", "https"]);
const masqueradeTypes = ref<string[]>(["file", "proxy", "string"]); const masqueradeTypes = ref<string[]>(["file", "proxy", "string"]);
const state = reactive({ const state = reactive({
enableForm: {
enable: "0",
},
configForm: { configForm: {
remark: "", remark: "",
clashExtension: "", clashExtension: "",
@@ -1140,7 +1073,6 @@ const state = reactive({
acl: false, acl: false,
outbounds: false, outbounds: false,
masquerade: false, masquerade: false,
fileList: [] as UploadFile[],
hysteria2Monitor: { hysteria2Monitor: {
version: "", version: "",
running: false, running: false,
@@ -1150,7 +1082,6 @@ const state = reactive({
}); });
const { const {
enableForm,
configForm, configForm,
activeName, activeName,
dataForm, dataForm,
@@ -1166,7 +1097,6 @@ const {
acl, acl,
outbounds, outbounds,
masquerade, masquerade,
fileList,
hysteria2Monitor, hysteria2Monitor,
crtFileList, crtFileList,
keyFileList, keyFileList,
@@ -1237,28 +1167,6 @@ const tabs = computed(() => {
return tabs; return tabs;
}); });
const handleImport = (params: UploadRequestOptions) => {
if (state.fileList.length > 0) {
let formData = new FormData();
formData.append("file", params.file);
importHysteria2ConfigApi(formData).then(() => {
ElMessage.success(t("common.success"));
});
state.fileList = [];
}
};
const beforeImport = (file: UploadRawFile) => {
if (!file.name.endsWith(".yaml")) {
ElMessage.error("file format not supported");
return false;
}
if (file.size / 1024 / 1024 > 2) {
ElMessage.error("the file is too big, less than 2 MB");
return false;
}
};
const handleExport = async () => { const handleExport = async () => {
let response = await exportHysteria2ConfigApi(); let response = await exportHysteria2ConfigApi();
try { try {
@@ -1279,199 +1187,13 @@ const handleExport = async () => {
} }
}; };
const handleReset = () => {
state.configForm = {
remark: "",
clashExtension: "",
};
state.dataForm = deepCopy(defaultHysteria2ServerConfig);
state.activeName = "extension";
state.tlsType = "acme";
state.aclType = "inline";
state.obfs = false;
state.quic = false;
state.bandwidth = false;
state.speedTest = false;
state.udp = false;
state.resolver = false;
state.sniff = false;
state.acl = false;
state.outbounds = false;
state.masquerade = false;
state.fileList = [];
};
const handleChangeEnable = async () => {
const enable = state.enableForm.enable === "1" ? "0" : "1";
try {
await updateConfigsApi({
configUpdateDtos: [
{ key: hysteria2EnableKey, value: state.enableForm.enable },
],
});
if (state.enableForm.enable === "1") {
ElMessage.success(t("common.enableSuccess"));
} else {
ElMessage.error(t("common.disableSuccess"));
}
} catch (e) {
state.enableForm.enable = enable;
}
await setHysteria2Monitor();
};
const submitForm = () => {
dataFormRef.value.validate((valid: any) => {
if (valid) {
let configs: ConfigsUpdateDto[] = [
{
key: hysteria2Remark,
value: state.configForm.remark,
},
{
key: clashExtension,
value: state.configForm.clashExtension,
},
];
updateConfigsApi({ configUpdateDtos: configs });
if (state.tlsType == "tls") {
state.dataForm.acme = undefined;
} else if (state.tlsType == "acme") {
state.dataForm.tls = undefined;
if (state.dataForm.acme?.type == "http") {
state.dataForm.acme.tls = undefined;
state.dataForm.acme.dns = undefined;
} else if (state.dataForm.acme?.type == "tls") {
state.dataForm.acme.http = undefined;
state.dataForm.acme.dns = undefined;
} else if (state.dataForm.acme?.type == "dns") {
state.dataForm.acme.http = undefined;
state.dataForm.acme.tls = undefined;
} else if (state.dataForm.acme?.type == "") {
state.dataForm.acme.http = undefined;
state.dataForm.acme.tls = undefined;
state.dataForm.acme.dns = undefined;
}
}
if (state.dataForm.acl) {
if (state.aclType == "inline") {
state.dataForm.acl.file = undefined;
} else if (state.aclType == "file") {
state.dataForm.acl.inline = undefined;
}
}
if (state.dataForm.resolver?.type == "tcp") {
state.dataForm.resolver.udp = undefined;
state.dataForm.resolver.tls = undefined;
state.dataForm.resolver.https = undefined;
} else if (state.dataForm.resolver?.type == "udp") {
state.dataForm.resolver.tcp = undefined;
state.dataForm.resolver.tls = undefined;
state.dataForm.resolver.https = undefined;
} else if (state.dataForm.resolver?.type == "tls") {
state.dataForm.resolver.tcp = undefined;
state.dataForm.resolver.udp = undefined;
state.dataForm.resolver.https = undefined;
} else if (state.dataForm.resolver?.type == "https") {
state.dataForm.resolver.tcp = undefined;
state.dataForm.resolver.udp = undefined;
state.dataForm.resolver.tls = undefined;
}
if (state.dataForm.masquerade?.type == "file") {
state.dataForm.masquerade.proxy = undefined;
state.dataForm.masquerade.string = undefined;
} else if (state.dataForm.masquerade?.type == "proxy") {
state.dataForm.masquerade.file = undefined;
state.dataForm.masquerade.string = undefined;
} else if (state.dataForm.masquerade?.type == "string") {
state.dataForm.masquerade.file = undefined;
state.dataForm.masquerade.proxy = undefined;
}
if (!state.obfs) {
state.dataForm.obfs = undefined;
}
if (!state.quic) {
state.dataForm.quic = undefined;
}
if (!state.bandwidth) {
state.dataForm.bandwidth = undefined;
state.dataForm.ignoreClientBandwidth = undefined;
}
if (!state.speedTest) {
state.dataForm.speedTest = undefined;
}
if (!state.udp) {
state.dataForm.disableUDP = undefined;
state.dataForm.udpIdleTimeout = undefined;
}
if (!state.resolver) {
state.dataForm.resolver = undefined;
}
if (!state.sniff) {
state.dataForm.sniff = undefined;
}
if (!state.acl) {
state.dataForm.acl = undefined;
}
if (!state.outbounds) {
state.dataForm.outbounds = undefined;
}
if (!state.masquerade) {
state.dataForm.masquerade = undefined;
}
if (state.dataForm?.udpIdleTimeout === "") {
state.dataForm!.udpIdleTimeout = "60s";
}
if (state.dataForm?.quic?.maxIdleTimeout === "") {
state.dataForm!.quic!.maxIdleTimeout = "30s";
}
if (state.dataForm?.resolver?.tcp?.timeout === "") {
state.dataForm!.resolver!.tcp!.timeout = "4s";
}
if (state.dataForm?.resolver?.udp?.timeout === "") {
state.dataForm!.resolver!.udp!.timeout = "4s";
}
if (state.dataForm?.resolver?.tls?.timeout === "") {
state.dataForm!.resolver!.tls!.timeout = "10s";
}
if (state.dataForm?.resolver?.https?.timeout === "") {
state.dataForm!.resolver!.https!.timeout = "10s";
}
if (state.dataForm?.sniff?.timeout === "") {
state.dataForm!.sniff!.timeout = "2s";
}
if (state.dataForm?.acl?.geoUpdateInterval === "") {
state.dataForm!.acl!.geoUpdateInterval = "168h";
}
const params = { ...state.dataForm };
updateHysteria2ConfigApi(params).then(() => {
ElMessage.success(t("common.success"));
setConfig();
});
}
});
};
const setConfig = () => { const setConfig = () => {
listConfigApi({ listConfigApi({
keys: [ keys: [hysteria2Remark, clashExtension],
hysteria2EnableKey,
hysteria2Remark,
clashExtension,
],
}).then((response) => { }).then((response) => {
const data = response.data; const data = response.data;
data.forEach((configVo) => { data.forEach((configVo) => {
if (configVo.key === hysteria2EnableKey) { if (configVo.key === hysteria2Remark) {
state.enableForm.enable = configVo.value;
} else if (configVo.key === hysteria2Remark) {
state.configForm.remark = configVo.value; state.configForm.remark = configVo.value;
} else if (configVo.key === clashExtension) { } else if (configVo.key === clashExtension) {
state.configForm.clashExtension = configVo.value; state.configForm.clashExtension = configVo.value;
@@ -1502,66 +1224,6 @@ const setConfig = () => {
}); });
}; };
const closeTabPane = (tabPaneName: string) => {
if (
tabPaneName === "extension" ||
tabPaneName === "listen" ||
tabPaneName === "tls" ||
tabPaneName === "http"
) {
ElMessage.error("This tab is required");
return;
}
if (tabPaneName === "obfs") {
state.obfs = false;
} else if (tabPaneName === "quic") {
state.quic = false;
} else if (tabPaneName === "bandwidth") {
state.bandwidth = false;
} else if (tabPaneName === "speedTest") {
state.speedTest = false;
} else if (tabPaneName === "udp") {
state.udp = false;
} else if (tabPaneName === "resolver") {
state.resolver = false;
} else if (tabPaneName === "sniff") {
state.sniff = false;
} else if (tabPaneName === "acl") {
state.acl = false;
} else if (tabPaneName === "outbounds") {
state.outbounds = false;
} else if (tabPaneName === "masquerade") {
state.masquerade = false;
}
state.activeName = "listen";
};
const handleDropdownClick = (command: string) => {
if (command === "obfs") {
state.obfs = true;
} else if (command === "quic") {
state.quic = true;
} else if (command === "bandwidth") {
state.bandwidth = true;
} else if (command === "speedTest") {
state.speedTest = true;
} else if (command === "udp") {
state.udp = true;
} else if (command === "resolver") {
state.resolver = true;
} else if (command === "sniff") {
state.sniff = true;
} else if (command === "acl") {
state.acl = true;
} else if (command === "outbounds") {
state.outbounds = true;
} else if (command === "masquerade") {
state.masquerade = true;
}
state.activeName = command;
};
const setHysteria2Monitor = async () => { const setHysteria2Monitor = async () => {
const { data } = await monitorHysteria2Api(); const { data } = await monitorHysteria2Api();
Object.assign(state.hysteria2Monitor, data); Object.assign(state.hysteria2Monitor, data);
+1 -1
View File
@@ -114,7 +114,7 @@
## CLI baseline ## CLI baseline
Команды: Команды:
- `install --package-dir <path> [--config /etc/hy2xs/hy2xs.env]` - `install --package-dir <path> [--config <source-env>]`
- `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --dry-run` - `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --dry-run`
- `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --apply` - `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --apply`
@@ -71,7 +71,6 @@ Install-only orchestrator ставит систему на чистый Debian 1
CLI baseline CLI baseline
Допустимые флаги: Допустимые флаги:
- --non-interactive - --non-interactive
- --domain
- --port - --port
- --ssh-port - --ssh-port
- --skip-firewall - --skip-firewall
+7
View File
@@ -66,6 +66,13 @@ export async function preflight(context: InstallContext): Promise<void> {
fail("acme mode requires HY2XS_DOMAIN and HY2XS_ACME_EMAIL"); fail("acme mode requires HY2XS_DOMAIN and HY2XS_ACME_EMAIL");
} }
if (!isReconfigure && context.config.tlsMode === "acme") {
const acmeChallengePort = context.config.acmeType === "http" ? 80 : 443;
if (await isPortBusy(acmeChallengePort)) {
fail(`ACME ${context.config.acmeType}-challenge port is already in use: ${acmeChallengePort}`);
}
}
if (context.config.domain) { if (context.config.domain) {
try { try {
const a = await run`getent ahostsv4 ${context.config.domain}`; const a = await run`getent ahostsv4 ${context.config.domain}`;
+2
View File
@@ -36,6 +36,8 @@ cp -a "$PACKAGE_DIR/docs" "$RUNTIME_PACKAGE_DIR/"
cp -a "$PACKAGE_DIR/systemd" "$RUNTIME_PACKAGE_DIR/" cp -a "$PACKAGE_DIR/systemd" "$RUNTIME_PACKAGE_DIR/"
cp -a "$PACKAGE_DIR/templates" "$RUNTIME_PACKAGE_DIR/" cp -a "$PACKAGE_DIR/templates" "$RUNTIME_PACKAGE_DIR/"
cp -a "$PACKAGE_DIR/metadata" "$RUNTIME_PACKAGE_DIR/" cp -a "$PACKAGE_DIR/metadata" "$RUNTIME_PACKAGE_DIR/"
cp -a "$PACKAGE_DIR/ui" "$RUNTIME_PACKAGE_DIR/"
cp -a "$PACKAGE_DIR/install.sh" "$RUNTIME_PACKAGE_DIR/"
log "package directory: $PACKAGE_DIR" log "package directory: $PACKAGE_DIR"
log "starting install-only orchestrator" log "starting install-only orchestrator"
+3 -5
View File
@@ -144,6 +144,7 @@ ensure_node() {
ensure_pnpm() { ensure_pnpm() {
local managed="$TOOLCHAIN_DIR/pnpm/bin/pnpm" local managed="$TOOLCHAIN_DIR/pnpm/bin/pnpm"
export PATH="$TOOLCHAIN_DIR/pnpm/bin:$PATH"
if [ -x "$managed" ] && [ "$($managed --version)" = "$PNPM_REQUIRED" ]; then if [ -x "$managed" ] && [ "$($managed --version)" = "$PNPM_REQUIRED" ]; then
PNPM_BIN="$managed" PNPM_BIN="$managed"
else else
@@ -151,11 +152,8 @@ ensure_pnpm() {
mkdir -p "$TOOLCHAIN_DIR/pnpm/bin" mkdir -p "$TOOLCHAIN_DIR/pnpm/bin"
corepack enable --install-directory "$TOOLCHAIN_DIR/pnpm/bin" corepack enable --install-directory "$TOOLCHAIN_DIR/pnpm/bin"
corepack prepare "pnpm@$PNPM_REQUIRED" --activate corepack prepare "pnpm@$PNPM_REQUIRED" --activate
if command -v pnpm >/dev/null 2>&1; then [ -x "$managed" ] || fail "pnpm binary was not installed into $managed"
cp "$(command -v pnpm)" "$managed" PNPM_BIN="$managed"
chmod 0755 "$managed"
fi
PNPM_BIN="$(command -v pnpm)"
fi fi
export PNPM_BIN export PNPM_BIN