feat: кнопка Переустановить для Tachyon на странице обновлений

This commit is contained in:
Dushnilin
2026-07-31 17:34:11 +03:00
parent ed0def7886
commit ac209457d7
8 changed files with 229 additions and 21 deletions
@@ -6,6 +6,7 @@ export type UpdatesActionKey = keyof StoreType['updatesActions'];
const componentActionKeyMap: Record<string, UpdatesActionKey> = {
'tachyon:check_update': 'tachyonCheck',
'tachyon:install': 'tachyonInstall',
'tachyon:reinstall': 'tachyonReinstall',
'sing_box:check_update': 'singBoxCheck',
'sing_box:install': 'singBoxInstall',
'sing_box:install_extended': 'singBoxInstallExtended',
@@ -215,6 +215,7 @@ export interface StoreType {
updatesActions: {
tachyonCheck: { loading: boolean };
tachyonInstall: { loading: boolean };
tachyonReinstall: { loading: boolean };
singBoxCheck: { loading: boolean };
singBoxInstall: { loading: boolean };
singBoxInstallExtended: { loading: boolean };
@@ -15,6 +15,7 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
return {
tachyonCheck: { loading: false },
tachyonInstall: { loading: false },
tachyonReinstall: { loading: false },
singBoxCheck: { loading: false },
singBoxInstall: { loading: false },
singBoxInstallExtended: { loading: false },
@@ -133,6 +133,7 @@ export const initialDiagnosticStore: Pick<
updatesActions: {
tachyonCheck: { loading: false },
tachyonInstall: { loading: false },
tachyonReinstall: { loading: false },
singBoxCheck: { loading: false },
singBoxInstall: { loading: false },
singBoxInstallExtended: { loading: false },
@@ -273,7 +273,10 @@ async function ackComponentActionJob(jobId: string) {
}
function getExpectedLatestVersionForAction(button: ComponentActionButton) {
if (button.component !== 'tachyon' || button.action !== 'install') {
if (
button.component !== 'tachyon' ||
(button.action !== 'install' && button.action !== 'reinstall')
) {
return undefined;
}
@@ -328,7 +331,10 @@ function patchSystemInfoAfterMutation(result: Tachyon.ComponentActionResult) {
const version =
result.current_version || result.latest_version || _('unknown');
if (result.component === 'tachyon' && result.action === 'install') {
if (
result.component === 'tachyon' &&
(result.action === 'install' || result.action === 'reinstall')
) {
nextSystemInfo.tachyon_version = version;
}
@@ -469,7 +475,11 @@ async function applyCompletedComponentAction({
return;
}
if (result.action === 'install' || result.action.startsWith('install_')) {
if (
result.action === 'install' ||
result.action === 'reinstall' ||
result.action.startsWith('install_')
) {
setCheckResult(result.component, 'latest', result.latest_version || '');
} else {
resetCheckResult(result.component);
@@ -478,7 +488,10 @@ async function applyCompletedComponentAction({
patchSystemInfoAfterMutation(result);
setActionLoading(key, false);
if (result.component === 'tachyon' && result.action === 'install') {
if (
result.component === 'tachyon' &&
(result.action === 'install' || result.action === 'reinstall')
) {
if (notify && result.message) {
showToast(result.message, 'success', 1200);
}
@@ -842,11 +855,20 @@ function getComponentCards(): ComponentCard[] {
Boolean(systemInfo.sing_box_extended) && Boolean(systemInfo.sing_box_lx);
const singBoxTiny = Boolean(systemInfo.sing_box_tiny);
const tachyonActions = getInstalledUpdateActions(
'tachyon',
'tachyonCheck',
'tachyonInstall',
);
const tachyonActions = [
...getInstalledUpdateActions(
'tachyon',
'tachyonCheck',
'tachyonInstall',
),
{
key: 'tachyonReinstall' as const,
text: _('Reinstall'),
icon: renderRotateCcwIcon24,
component: 'tachyon' as const,
action: 'reinstall' as const,
},
];
const singBoxActions = getInstalledUpdateActions(
'sing_box',
'singBoxCheck',
@@ -1122,7 +1144,8 @@ function renderComponentCard(card: ComponentCard) {
// Render primary and danger buttons in a main row
const primaryButtons = primaryActions.map((action) => {
const loading = updatesActions[action.key].loading;
const isUpdateOrInstall = action.action === 'install';
const isUpdateOrInstall =
action.action === 'install' || action.action === 'reinstall';
return renderButton({
classNames: isUpdateOrInstall ? ['cbi-button-save'] : [],
+1
View File
@@ -573,6 +573,7 @@ export namespace Tachyon {
export type ComponentAction =
| 'check_update'
| 'install'
| 'reinstall'
| 'remove'
| 'install_extended'
| 'install_extended_compressed'
@@ -810,6 +810,124 @@ function validateHysteria2Url(url) {
}
}
// src/validators/validateTuicUrl.ts
function isValidUuid(value) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
value
);
}
function validateTuicUrl(url) {
try {
if (!url.startsWith("tuic://")) {
return {
valid: false,
message: _("Invalid TUIC URL: must start with tuic://")
};
}
if (/\s/.test(url)) {
return {
valid: false,
message: _("Invalid TUIC URL: must not contain spaces")
};
}
const body = url.slice("tuic://".length);
const [mainPart] = body.split("#");
const [authHostPort, queryString] = mainPart.split("?");
if (!authHostPort)
return {
valid: false,
message: _("Invalid TUIC URL: missing credentials/server")
};
const [uuidPasswordPart, hostPortPart] = authHostPort.split("@");
if (!uuidPasswordPart)
return {
valid: false,
message: _("Invalid TUIC URL: missing UUID")
};
if (!hostPortPart)
return {
valid: false,
message: _("Invalid TUIC URL: missing host & port")
};
const [uuidPart, passwordPart] = uuidPasswordPart.split(":");
const uuid = uuidPart || "";
const _password = passwordPart || "";
if (!uuid) {
return { valid: false, message: _("Invalid TUIC URL: missing UUID") };
}
if (!isValidUuid(uuid)) {
return {
valid: false,
message: _("Invalid TUIC URL: UUID must be a valid UUID v4 format")
};
}
const parsedHostPort = parseHostPort(hostPortPart);
if (!parsedHostPort) {
return {
valid: false,
message: _("Invalid TUIC URL: invalid host & port")
};
}
const { host, port } = parsedHostPort;
if (!host) {
return { valid: false, message: _("Invalid TUIC URL: missing host") };
}
if (!port) {
return { valid: false, message: _("Invalid TUIC URL: missing port") };
}
const cleanedPort = port.replace("/", "");
if (!isValidPort(cleanedPort)) {
return {
valid: false,
message: _("Invalid TUIC URL: invalid port number")
};
}
if (queryString) {
const params = parseQueryString(queryString);
const paramsKeys = Object.keys(params);
const validCc = ["bbr", "cubic", "new_reno"];
if (paramsKeys.includes("congestion_control") && !validCc.includes(params.congestion_control)) {
return {
valid: false,
message: _(
"Invalid TUIC URL: congestion_control must be bbr, cubic, or new_reno"
)
};
}
const validUdpMode = ["native", "quic", "qux"];
if (paramsKeys.includes("udp_relay_mode") && !validUdpMode.includes(params.udp_relay_mode)) {
return {
valid: false,
message: _(
"Invalid TUIC URL: udp_relay_mode must be native, quic, or qux"
)
};
}
if (paramsKeys.includes("insecure") && !["0", "1"].includes(params.insecure)) {
return {
valid: false,
message: _("Invalid TUIC URL: insecure must be 0 or 1")
};
}
if (paramsKeys.includes("zero_rtt_handshake") && !["0", "1", "true", "false"].includes(params.zero_rtt_handshake)) {
return {
valid: false,
message: _("Invalid TUIC URL: zero_rtt_handshake must be 0, 1, true, or false")
};
}
if (paramsKeys.includes("sni") && !params.sni) {
return {
valid: false,
message: _("Invalid TUIC URL: sni cannot be empty")
};
}
}
return { valid: true, message: _("Valid") };
} catch (_e) {
return { valid: false, message: _("Invalid TUIC URL: parsing failed") };
}
}
// src/validators/validateProxyUrl.ts
function validateProxyUrl(url) {
const trimmedUrl = url.trim();
@@ -831,10 +949,13 @@ function validateProxyUrl(url) {
if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) {
return validateHysteria2Url(trimmedUrl);
}
if (trimmedUrl.startsWith("tuic://")) {
return validateTuicUrl(trimmedUrl);
}
return {
valid: false,
message: _(
"URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
"URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, hy2://, or tuic://"
)
};
}
@@ -4694,6 +4815,7 @@ var initialDiagnosticStore = {
updatesActions: {
tachyonCheck: { loading: false },
tachyonInstall: { loading: false },
tachyonReinstall: { loading: false },
singBoxCheck: { loading: false },
singBoxInstall: { loading: false },
singBoxInstallExtended: { loading: false },
@@ -5049,6 +5171,7 @@ var LogNotificationDeduper = class {
var componentActionKeyMap = {
"tachyon:check_update": "tachyonCheck",
"tachyon:install": "tachyonInstall",
"tachyon:reinstall": "tachyonReinstall",
"sing_box:check_update": "singBoxCheck",
"sing_box:install": "singBoxInstall",
"sing_box:install_extended": "singBoxInstallExtended",
@@ -5160,6 +5283,7 @@ function getEmptyUpdatesActions() {
return {
tachyonCheck: { loading: false },
tachyonInstall: { loading: false },
tachyonReinstall: { loading: false },
singBoxCheck: { loading: false },
singBoxInstall: { loading: false },
singBoxInstallExtended: { loading: false },
@@ -13604,7 +13728,7 @@ async function ackComponentActionJob(jobId) {
}
}
function getExpectedLatestVersionForAction(button) {
if (button.component !== "tachyon" || button.action !== "install") {
if (button.component !== "tachyon" || button.action !== "install" && button.action !== "reinstall") {
return void 0;
}
return store.get().updatesChecks[button.component].latest_version || void 0;
@@ -13644,7 +13768,7 @@ function patchSystemInfoAfterMutation(result) {
const systemInfo = store.get().diagnosticsSystemInfo;
const nextSystemInfo = { ...systemInfo, loading: false, loaded: true };
const version = result.current_version || result.latest_version || _("unknown");
if (result.component === "tachyon" && result.action === "install") {
if (result.component === "tachyon" && (result.action === "install" || result.action === "reinstall")) {
nextSystemInfo.tachyon_version = version;
}
if (result.component === "sing_box") {
@@ -13753,14 +13877,14 @@ async function applyCompletedComponentAction({
}
return;
}
if (result.action === "install" || result.action.startsWith("install_")) {
if (result.action === "install" || result.action === "reinstall" || result.action.startsWith("install_")) {
setCheckResult(result.component, "latest", result.latest_version || "");
} else {
resetCheckResult(result.component);
}
patchSystemInfoAfterMutation(result);
setActionLoading(key, false);
if (result.component === "tachyon" && result.action === "install") {
if (result.component === "tachyon" && (result.action === "install" || result.action === "reinstall")) {
if (notify && result.message) {
showToast(result.message, "success", 1200);
}
@@ -14028,11 +14152,20 @@ function getComponentCards() {
const singBoxExtendedCompressed = Boolean(systemInfo.sing_box_extended) && Boolean(systemInfo.sing_box_compressed);
const singBoxLx = Boolean(systemInfo.sing_box_extended) && Boolean(systemInfo.sing_box_lx);
const singBoxTiny = Boolean(systemInfo.sing_box_tiny);
const tachyonActions = getInstalledUpdateActions(
"tachyon",
"tachyonCheck",
"tachyonInstall"
);
const tachyonActions = [
...getInstalledUpdateActions(
"tachyon",
"tachyonCheck",
"tachyonInstall"
),
{
key: "tachyonReinstall",
text: _("Reinstall"),
icon: renderRotateCcwIcon24,
component: "tachyon",
action: "reinstall"
}
];
const singBoxActions = getInstalledUpdateActions(
"sing_box",
"singBoxCheck",
@@ -14265,7 +14398,7 @@ function renderComponentCard(card) {
const actionElements = [];
const primaryButtons = primaryActions.map((action) => {
const loading2 = updatesActions[action.key].loading;
const isUpdateOrInstall = action.action === "install";
const isUpdateOrInstall = action.action === "install" || action.action === "reinstall";
return renderButton({
classNames: isUpdateOrInstall ? ["cbi-button-save"] : [],
text: action.text,
@@ -2169,6 +2169,51 @@ function resolve_tachyon_release(latest_version) {
};
}
function reinstall_tachyon() {
let latest_version = latest_tachyon_version();
if (latest_version == "")
latest_version = "unknown";
if (latest_version == "unknown")
action_fail("tachyon", "reinstall", "Failed to resolve Tachyon release", TACHYON_VERSION, latest_version);
write_tachyon_latest_version_cache(latest_version, now_seconds());
init_tmp_dir() || action_fail("tachyon", "reinstall", "Failed to create temporary directory", TACHYON_VERSION, latest_version);
updates_log("Resolving Tachyon release " + latest_version + " packages");
let release = resolve_tachyon_release(latest_version);
if (release == null)
action_fail("tachyon", "reinstall", "Failed to resolve Tachyon release packages", TACHYON_VERSION, latest_version);
let backend_file = tmp_dir + "/" + release.backend_name;
let app_file = tmp_dir + "/" + release.app_name;
let i18n_file = release.i18n_url != "" ? tmp_dir + "/" + release.i18n_name : "";
if (!download_with_retry(release.backend_url, backend_file, release.backend_name) ||
!download_with_retry(release.app_url, app_file, release.app_name) ||
(release.i18n_url != "" && !download_with_retry(release.i18n_url, i18n_file, release.i18n_name)))
action_fail("tachyon", "reinstall", "Failed to download Tachyon release packages", TACHYON_VERSION, latest_version);
if (!run_logged("Reinstalling LuCI app package " + release.app_name, pkg_install_files_command([ app_file ])))
action_fail("tachyon", "reinstall", "Failed to reinstall LuCI app package", TACHYON_VERSION, latest_version);
if (i18n_file != "" && !run_logged("Reinstalling LuCI Russian i18n package " + release.i18n_name, pkg_install_files_command([ i18n_file ])))
action_fail("tachyon", "reinstall", "Failed to reinstall LuCI Russian i18n package", TACHYON_VERSION, latest_version);
if (!run_logged("Reinstalling Tachyon package " + release.backend_name, pkg_install_files_command([ backend_file ])))
action_fail("tachyon", "reinstall", "Failed to reinstall Tachyon package", TACHYON_VERSION, latest_version);
remove_file("/var/luci-indexcache");
command_success("rm -f /var/luci-indexcache* /tmp/luci-indexcache* 2>/dev/null");
command_success("rm -rf /tmp/luci-modulecache/ 2>/dev/null");
if (file_exists("/etc/init.d/rpcd") && !command_success_from_args([ "/etc/init.d/rpcd", "reload" ]))
command_success_from_args([ "/etc/init.d/rpcd", "restart" ]);
command_success_from_args([ "killall", "-HUP", "rpcd" ]);
restart_tachyon_after_successful_change();
clear_version_caches();
let new_version = installed_package_version("tachyon");
if (new_version == "")
new_version = latest_version;
updates_log("Tachyon reinstalled to " + new_version);
action_success("tachyon", "reinstall", "Tachyon has been reinstalled", new_version, latest_version, 1, "latest", release.release_url);
}
function install_tachyon() {
let latest_version = latest_tachyon_version();
if (latest_version == "")
@@ -2271,6 +2316,8 @@ function component_action(component, action) {
check_tachyon();
else if (component == "tachyon" && action == "install")
install_tachyon();
else if (component == "tachyon" && action == "reinstall")
reinstall_tachyon();
else if (component == "sing_box" && (action == "check_update" || action == "install" ||
action == "install_extended" || action == "install_extended_compressed" ||
action == "install_lx" ||