feat: кнопка Переустановить для Tachyon на странице обновлений
This commit is contained in:
@@ -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'] : [],
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
Reference in New Issue
Block a user