feat: component version picker — list last 3 releases, install specific version (1.3.20)

This commit is contained in:
Dushnilin
2026-09-04 21:42:33 +03:00
parent 5b2083bfcc
commit a4538311f1
10 changed files with 844 additions and 11 deletions
+2 -2
View File
@@ -16,8 +16,8 @@ export const DOMAIN_LIST_OPTIONS = {
russia_inside: 'Russia inside',
russia_outside: 'Russia outside',
ukraine_inside: 'Ukraine',
geoblock: 'Geo Block (снаружи РФ)',
block: 'Block (РКН / внутри РФ)',
geoblock: 'Geo Block',
block: 'Block (RKN)',
porn: 'Porn',
news: 'News',
anime: 'Anime',
@@ -51,6 +51,33 @@ function parseJsonObjectOutput<T>(output: string): T | null {
}
}
function parseJsonArrayOutput<T>(output: string): T[] | null {
if (!output) {
return null;
}
try {
const parsed = JSON.parse(output);
if (Array.isArray(parsed)) {
return parsed as T[];
}
return null;
} catch (_error) {
const jsonMatch = output.match(/(\[[\s\S]*\])\s*$/);
if (!jsonMatch) {
return null;
}
try {
const parsed = JSON.parse(jsonMatch[1]);
return Array.isArray(parsed) ? (parsed as T[]) : null;
} catch (_jsonError) {
return null;
}
}
}
function parseComponentActionOutput(output: string) {
return parseJsonObjectOutput<Tachyon.ComponentActionResult>(output);
}
@@ -675,6 +702,64 @@ export const TachyonShellMethods = {
callBaseMethod<Tachyon.ComponentUpdateCheckCache>(
Tachyon.AvailableMethods.COMPONENT_UPDATE_CHECK_CACHE,
),
componentListReleases: async (
component: Tachyon.ComponentName,
count = 3,
) => {
const response = await executeShellCommand({
command: '/usr/bin/tachyon',
args: [
Tachyon.AvailableMethods.COMPONENT_LIST_RELEASES,
component,
String(count),
],
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS,
});
const parsed = parseJsonArrayOutput<Tachyon.ComponentRelease>(
response.stdout,
);
if ((response.code ?? 0) !== 0 || !parsed) {
return {
success: false,
error: response.stderr || _('Failed to fetch releases'),
} as Tachyon.MethodFailureResponse;
}
return {
success: true,
data: parsed,
} as Tachyon.MethodSuccessResponse<Tachyon.ComponentRelease[]>;
},
componentInstallVersion: async (
component: Tachyon.ComponentName,
tag: string,
) => {
const response = await executeShellCommand({
command: '/usr/bin/tachyon',
args: [
Tachyon.AvailableMethods.COMPONENT_INSTALL_VERSION,
component,
tag,
],
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS,
});
const parsed = parseJsonObjectOutput<Tachyon.ComponentActionResult>(
response.stdout,
);
if ((response.code ?? 0) !== 0 || !parsed) {
return {
success: false,
error: response.stderr || _('Failed to install version'),
} as Tachyon.MethodFailureResponse;
}
return {
success: true,
data: parsed,
} as Tachyon.MethodSuccessResponse<Tachyon.ComponentActionResult>;
},
waitComponentActionJob: async (
jobId: string,
component: Tachyon.ComponentName,
@@ -65,7 +65,7 @@ class Logger {
download(filename = 'logs.txt'): void {
if (typeof document === 'undefined') {
console.warn('Logger.download() доступен только в браузере');
console.warn('Logger.download() available only in browser');
return;
}
downloadAsTxt(this.getLogs(), filename);
@@ -117,6 +117,7 @@ interface ComponentCard {
repoUrl?: string;
actions: ComponentActionButton[];
badgeNode?: Node | null;
supportsVersions?: boolean;
}
let updatesLifecycleRegistered = false;
@@ -1265,6 +1266,7 @@ function getComponentCards(): ComponentCard[] {
releaseUrl: getGitHubReleaseUrl('tachyon'),
repoUrl: COMPONENT_REPO_URLS.tachyon,
actions: tachyonActions,
supportsVersions: true,
},
{
component: 'sing_box',
@@ -1286,6 +1288,7 @@ function getComponentCards(): ComponentCard[] {
? 'https://github.com/shtorm-7/sing-box-extended'
: COMPONENT_REPO_URLS.sing_box),
actions: singBoxActions,
supportsVersions: true,
},
{
component: 'zapret',
@@ -1300,6 +1303,7 @@ function getComponentCards(): ComponentCard[] {
releaseUrl: getGitHubReleaseUrl('zapret'),
repoUrl: COMPONENT_REPO_URLS.zapret,
actions: zapretActions,
supportsVersions: true,
},
{
component: 'zapret2',
@@ -1314,6 +1318,7 @@ function getComponentCards(): ComponentCard[] {
releaseUrl: getGitHubReleaseUrl('zapret2'),
repoUrl: COMPONENT_REPO_URLS.zapret2,
actions: zapret2Actions,
supportsVersions: true,
},
{
component: 'byedpi',
@@ -1346,6 +1351,156 @@ function getComponentCards(): ComponentCard[] {
];
}
async function showVersionPicker(component: Tachyon.ComponentName) {
const container = document.getElementById('tachyon_updates-components');
if (!container) return;
let dropdown = container.querySelector(
'.tachyon-version-picker',
) as HTMLElement | null;
if (dropdown) {
dropdown.remove();
return;
}
dropdown = E('div', { class: 'tachyon-version-picker' }, [
E(
'div',
{ class: 'tachyon-version-picker__loading' },
_('Loading versions...'),
),
]);
const componentCards = container.querySelectorAll(
`.tachyon_updates-page__component`,
);
const card = Array.from(componentCards).find((el) => {
const title = el.querySelector('.tachyon_updates-page__component__title');
return title?.textContent === getComponentCardTitle(component);
});
if (card) {
const actionsEl = card.querySelector(
'.tachyon_updates-page__component__actions',
);
actionsEl?.appendChild(dropdown);
}
try {
const response = await TachyonShellMethods.componentListReleases(
component,
3,
);
if (!response.success) {
dropdown.innerHTML = '';
dropdown.appendChild(
E(
'div',
{ class: 'tachyon-version-picker__error' },
response.error || _('No versions found'),
),
);
return;
}
if (!response.data || response.data.length === 0) {
dropdown.innerHTML = '';
dropdown.appendChild(
E(
'div',
{ class: 'tachyon-version-picker__error' },
_('No versions found'),
),
);
return;
}
dropdown.innerHTML = '';
const list = E('div', { class: 'tachyon-version-picker__list' });
for (const release of response.data) {
const children: Node[] = [
E('span', { class: 'tachyon-version-picker__tag' }, release.tag),
E('span', { class: 'tachyon-version-picker__date' }, release.published),
];
if (release.prerelease) {
children.push(
E(
'span',
{ class: 'tachyon-version-picker__prerelease' },
_('pre-release'),
),
);
}
children.push(
renderButton({
text: _('Install'),
loading: false,
disabled: false,
onClick: () => void installVersion(component, release.tag, dropdown!),
}),
);
const item = E(
'div',
{ class: 'tachyon-version-picker__item' },
children,
);
list.appendChild(item);
}
dropdown.appendChild(list);
dropdown.appendChild(
E(
'button',
{
class: 'cbi-button tachyon-version-picker__close',
click: () => dropdown?.remove(),
},
_('Close'),
),
);
} catch (_error) {
dropdown.innerHTML = '';
dropdown.appendChild(
E(
'div',
{ class: 'tachyon-version-picker__error' },
_('Failed to load versions'),
),
);
}
}
async function installVersion(
component: Tachyon.ComponentName,
tag: string,
dropdown: HTMLElement,
) {
const loadingEl = dropdown.querySelector(
'.tachyon-version-picker__loading',
) as HTMLElement | null;
if (loadingEl) {
loadingEl.textContent = _('Installing ') + tag + '...';
}
try {
const response = await TachyonShellMethods.componentInstallVersion(
component,
tag,
);
if (response?.success) {
showToast(_('Installed ') + tag, 'success');
dropdown.remove();
window.location.reload();
} else {
showToast(_('Installation failed: ') + (response?.error || ''), 'error');
}
} catch (_error) {
showToast(_('Installation failed'), 'error');
}
}
function renderComponentCard(card: ComponentCard) {
const updatesActions = store.get().updatesActions;
const anyActionLoading = isAnyActionLoading();
@@ -1576,6 +1731,20 @@ function renderComponentCard(card: ComponentCard) {
);
}
if (card.supportsVersions) {
const versionsButton = renderButton({
text: _('Versions'),
loading: false,
disabled: systemInfoLoading || anyActionLoading,
onClick: () => void showVersionPicker(card.component),
});
actionElements.push(
E('div', { class: 'tachyon_updates-page__component__versions' }, [
versionsButton,
]),
);
}
const actionsContainer = E(
'div',
{
@@ -314,4 +314,65 @@ export const styles = `
flex: 1 1 auto;
min-height: 0;
}
.tachyon-version-picker {
margin-top: 8px;
padding: 8px;
border: 1px solid var(--border-color, rgba(255,255,255,0.12));
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
}
.tachyon-version-picker__loading,
.tachyon-version-picker__error {
padding: 6px 0;
font-size: 12px;
color: var(--text-color, #aaa);
}
.tachyon-version-picker__error {
color: var(--error-color, #e74c3c);
}
.tachyon-version-picker__list {
display: flex;
flex-direction: column;
gap: 6px;
}
.tachyon-version-picker__item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06));
}
.tachyon-version-picker__item:last-child {
border-bottom: none;
}
.tachyon-version-picker__tag {
font-weight: bold;
font-size: 13px;
color: var(--text-color, #ddd);
}
.tachyon-version-picker__date {
font-size: 11px;
color: var(--text-color, #aaa);
}
.tachyon-version-picker__prerelease {
font-size: 10px;
color: var(--warn-color, #f39c12);
border: 1px solid var(--warn-color, #f39c12);
border-radius: 3px;
padding: 1px 4px;
}
.tachyon-version-picker__close {
margin-top: 8px;
width: 100%;
}
`;
+10
View File
@@ -95,6 +95,8 @@ export namespace Tachyon {
COMPONENT_ACTION_STATUS = 'component_action_status',
COMPONENT_ACTION_LOG = 'component_action_log',
COMPONENT_UPDATE_CHECK_CACHE = 'component_update_check_cache',
COMPONENT_LIST_RELEASES = 'component_list_releases',
COMPONENT_INSTALL_VERSION = 'component_install_version',
SUBSCRIPTION_UPDATE_ASYNC = 'subscription_update_async',
SUBSCRIPTION_UPDATE_STATUS = 'subscription_update_status',
SERVICE_HEALTH_CHECK = 'service_health_check',
@@ -723,6 +725,14 @@ export namespace Tachyon {
offset: number;
}
export interface ComponentRelease {
tag: string;
name: string;
published: string;
prerelease: boolean;
release_url: string;
}
export type ComponentActionStartResult = UiActionStartResult;
export type SubscriptionUpdateStartResult = UiActionStartResult;
@@ -1054,7 +1054,7 @@ var Logger = class {
}
download(filename = "logs.txt") {
if (typeof document === "undefined") {
console.warn("Logger.download() \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435");
console.warn("Logger.download() available only in browser");
return;
}
downloadAsTxt(this.getLogs(), filename);
@@ -1095,8 +1095,8 @@ var DOMAIN_LIST_OPTIONS = {
russia_inside: "Russia inside",
russia_outside: "Russia outside",
ukraine_inside: "Ukraine",
geoblock: "Geo Block (\u0441\u043D\u0430\u0440\u0443\u0436\u0438 \u0420\u0424)",
block: "Block (\u0420\u041A\u041D / \u0432\u043D\u0443\u0442\u0440\u0438 \u0420\u0424)",
geoblock: "Geo Block",
block: "Block (RKN)",
porn: "Porn",
news: "News",
anime: "Anime",
@@ -3182,6 +3182,8 @@ var Tachyon;
AvailableMethods2["COMPONENT_ACTION_STATUS"] = "component_action_status";
AvailableMethods2["COMPONENT_ACTION_LOG"] = "component_action_log";
AvailableMethods2["COMPONENT_UPDATE_CHECK_CACHE"] = "component_update_check_cache";
AvailableMethods2["COMPONENT_LIST_RELEASES"] = "component_list_releases";
AvailableMethods2["COMPONENT_INSTALL_VERSION"] = "component_install_version";
AvailableMethods2["SUBSCRIPTION_UPDATE_ASYNC"] = "subscription_update_async";
AvailableMethods2["SUBSCRIPTION_UPDATE_STATUS"] = "subscription_update_status";
AvailableMethods2["SERVICE_HEALTH_CHECK"] = "service_health_check";
@@ -3285,6 +3287,29 @@ function parseJsonObjectOutput(output) {
}
}
}
function parseJsonArrayOutput(output) {
if (!output) {
return null;
}
try {
const parsed = JSON.parse(output);
if (Array.isArray(parsed)) {
return parsed;
}
return null;
} catch (_error) {
const jsonMatch = output.match(/(\[[\s\S]*\])\s*$/);
if (!jsonMatch) {
return null;
}
try {
const parsed = JSON.parse(jsonMatch[1]);
return Array.isArray(parsed) ? parsed : null;
} catch (_jsonError) {
return null;
}
}
}
function parseComponentActionOutput(output) {
return parseJsonObjectOutput(output);
}
@@ -3745,6 +3770,54 @@ var TachyonShellMethods = {
componentUpdateCheckCache: async () => callBaseMethod(
Tachyon.AvailableMethods.COMPONENT_UPDATE_CHECK_CACHE
),
componentListReleases: async (component, count = 3) => {
const response = await executeShellCommand({
command: "/usr/bin/tachyon",
args: [
Tachyon.AvailableMethods.COMPONENT_LIST_RELEASES,
component,
String(count)
],
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS
});
const parsed = parseJsonArrayOutput(
response.stdout
);
if ((response.code ?? 0) !== 0 || !parsed) {
return {
success: false,
error: response.stderr || _("Failed to fetch releases")
};
}
return {
success: true,
data: parsed
};
},
componentInstallVersion: async (component, tag) => {
const response = await executeShellCommand({
command: "/usr/bin/tachyon",
args: [
Tachyon.AvailableMethods.COMPONENT_INSTALL_VERSION,
component,
tag
],
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS
});
const parsed = parseJsonObjectOutput(
response.stdout
);
if ((response.code ?? 0) !== 0 || !parsed) {
return {
success: false,
error: response.stderr || _("Failed to install version")
};
}
return {
success: true,
data: parsed
};
},
waitComponentActionJob: async (jobId, component, action, expectedLatestVersion) => {
const jobStartedAt = Date.now();
const isSelfUpdate = component === "tachyon" && (action === "install" || action === "reinstall");
@@ -20211,7 +20284,8 @@ function getComponentCards() {
latestVersion: getLatestVersion("tachyon"),
releaseUrl: getGitHubReleaseUrl("tachyon"),
repoUrl: COMPONENT_REPO_URLS.tachyon,
actions: tachyonActions
actions: tachyonActions,
supportsVersions: true
},
{
component: "sing_box",
@@ -20222,7 +20296,8 @@ function getComponentCards() {
latestVersion: getLatestVersion("sing_box"),
releaseUrl: getGitHubReleaseUrl("sing_box"),
repoUrl: systemInfo.sing_box_repo_url || (singBoxLx ? "https://github.com/Leadaxe/sing-box-lx" : singBoxExtended || singBoxExtendedCompressed ? "https://github.com/shtorm-7/sing-box-extended" : COMPONENT_REPO_URLS.sing_box),
actions: singBoxActions
actions: singBoxActions,
supportsVersions: true
},
{
component: "zapret",
@@ -20232,7 +20307,8 @@ function getComponentCards() {
latestVersion: getLatestVersion("zapret"),
releaseUrl: getGitHubReleaseUrl("zapret"),
repoUrl: COMPONENT_REPO_URLS.zapret,
actions: zapretActions
actions: zapretActions,
supportsVersions: true
},
{
component: "zapret2",
@@ -20242,7 +20318,8 @@ function getComponentCards() {
latestVersion: getLatestVersion("zapret2"),
releaseUrl: getGitHubReleaseUrl("zapret2"),
repoUrl: COMPONENT_REPO_URLS.zapret2,
actions: zapret2Actions
actions: zapret2Actions,
supportsVersions: true
},
{
component: "byedpi",
@@ -20266,6 +20343,139 @@ function getComponentCards() {
}
];
}
async function showVersionPicker(component) {
const container = document.getElementById("tachyon_updates-components");
if (!container) return;
let dropdown = container.querySelector(
".tachyon-version-picker"
);
if (dropdown) {
dropdown.remove();
return;
}
dropdown = E("div", { class: "tachyon-version-picker" }, [
E(
"div",
{ class: "tachyon-version-picker__loading" },
_("Loading versions...")
)
]);
const componentCards = container.querySelectorAll(
`.tachyon_updates-page__component`
);
const card = Array.from(componentCards).find((el) => {
const title = el.querySelector(".tachyon_updates-page__component__title");
return title?.textContent === getComponentCardTitle(component);
});
if (card) {
const actionsEl = card.querySelector(
".tachyon_updates-page__component__actions"
);
actionsEl?.appendChild(dropdown);
}
try {
const response = await TachyonShellMethods.componentListReleases(
component,
3
);
if (!response.success) {
dropdown.innerHTML = "";
dropdown.appendChild(
E(
"div",
{ class: "tachyon-version-picker__error" },
response.error || _("No versions found")
)
);
return;
}
if (!response.data || response.data.length === 0) {
dropdown.innerHTML = "";
dropdown.appendChild(
E(
"div",
{ class: "tachyon-version-picker__error" },
_("No versions found")
)
);
return;
}
dropdown.innerHTML = "";
const list = E("div", { class: "tachyon-version-picker__list" });
for (const release of response.data) {
const children = [
E("span", { class: "tachyon-version-picker__tag" }, release.tag),
E("span", { class: "tachyon-version-picker__date" }, release.published)
];
if (release.prerelease) {
children.push(
E(
"span",
{ class: "tachyon-version-picker__prerelease" },
_("pre-release")
)
);
}
children.push(
renderButton({
text: _("Install"),
loading: false,
disabled: false,
onClick: () => void installVersion(component, release.tag, dropdown)
})
);
const item = E(
"div",
{ class: "tachyon-version-picker__item" },
children
);
list.appendChild(item);
}
dropdown.appendChild(list);
dropdown.appendChild(
E(
"button",
{
class: "cbi-button tachyon-version-picker__close",
click: () => dropdown?.remove()
},
_("Close")
)
);
} catch (_error) {
dropdown.innerHTML = "";
dropdown.appendChild(
E(
"div",
{ class: "tachyon-version-picker__error" },
_("Failed to load versions")
)
);
}
}
async function installVersion(component, tag, dropdown) {
const loadingEl = dropdown.querySelector(
".tachyon-version-picker__loading"
);
if (loadingEl) {
loadingEl.textContent = _("Installing ") + tag + "...";
}
try {
const response = await TachyonShellMethods.componentInstallVersion(
component,
tag
);
if (response?.success) {
showToast(_("Installed ") + tag, "success");
dropdown.remove();
window.location.reload();
} else {
showToast(_("Installation failed: ") + (response?.error || ""), "error");
}
} catch (_error) {
showToast(_("Installation failed"), "error");
}
}
function renderComponentCard(card) {
const updatesActions = store.get().updatesActions;
const anyActionLoading = isAnyActionLoading();
@@ -20468,6 +20678,19 @@ function renderComponentCard(card) {
])
);
}
if (card.supportsVersions) {
const versionsButton = renderButton({
text: _("Versions"),
loading: false,
disabled: systemInfoLoading || anyActionLoading,
onClick: () => void showVersionPicker(card.component)
});
actionElements.push(
E("div", { class: "tachyon_updates-page__component__versions" }, [
versionsButton
])
);
}
const actionsContainer = E(
"div",
{
@@ -20917,6 +21140,67 @@ var styles6 = `
flex: 1 1 auto;
min-height: 0;
}
.tachyon-version-picker {
margin-top: 8px;
padding: 8px;
border: 1px solid var(--border-color, rgba(255,255,255,0.12));
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
}
.tachyon-version-picker__loading,
.tachyon-version-picker__error {
padding: 6px 0;
font-size: 12px;
color: var(--text-color, #aaa);
}
.tachyon-version-picker__error {
color: var(--error-color, #e74c3c);
}
.tachyon-version-picker__list {
display: flex;
flex-direction: column;
gap: 6px;
}
.tachyon-version-picker__item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06));
}
.tachyon-version-picker__item:last-child {
border-bottom: none;
}
.tachyon-version-picker__tag {
font-weight: bold;
font-size: 13px;
color: var(--text-color, #ddd);
}
.tachyon-version-picker__date {
font-size: 11px;
color: var(--text-color, #aaa);
}
.tachyon-version-picker__prerelease {
font-size: 10px;
color: var(--warn-color, #f39c12);
border: 1px solid var(--warn-color, #f39c12);
border-radius: 3px;
padding: 1px 4px;
}
.tachyon-version-picker__close {
margin-top: 8px;
width: 100%;
}
`;
// src/tachyon/tabs/updates/index.ts
+27
View File
@@ -5725,3 +5725,30 @@ msgstr "CleanBrowsing (Семейный)"
msgid "Yandex Safe"
msgstr "Yandex (Безопасный)"
msgid "Versions"
msgstr "Версии"
msgid "Loading versions..."
msgstr "Загрузка версий..."
msgid "No versions found"
msgstr "Версии не найдены"
msgid "Failed to load versions"
msgstr "Не удалось загрузить версии"
msgid "Installing "
msgstr "Установка "
msgid "Installation failed"
msgstr "Ошибка установки"
msgid "Installation failed: "
msgstr "Ошибка установки: "
msgid "Installed "
msgstr "Установлено "
msgid "pre-release"
msgstr "пре-релиз"
+6
View File
@@ -127,6 +127,10 @@ function show_help() {
print(" component_action_status Get background component action status\n");
print(" component_action_log [job_id] [offset]\n");
print(" Get background component action log from offset\n");
print(" component_list_releases <component> [count]\n");
print(" List last N releases for a component (default 3)\n");
print(" component_install_version <component> <tag>\n");
print(" Install a specific component release version\n");
print(" component_updates_if_due Check installed components for updates when due\n");
print(" component_update_check_cache Get cached component update check results\n");
print(" hosts_list_update [url] Update custom hosts lists (all or specific URL)\n");
@@ -231,6 +235,8 @@ function command_spec(command) {
component_action_async: [ "components/updates.uc", "component-action-async", 2 ],
component_action_status: [ "components/updates.uc", "component-action-status", 1 ],
component_action_log: [ "components/updates.uc", "component-action-log", 2 ],
component_list_releases: [ "components/action.uc", "list-component-releases", 2 ],
component_install_version: [ "components/action.uc", "install-component-version", 2 ],
component_updates_if_due: [ "components/updates.uc", "component-updates-if-due", 0 ],
component_auto_update_apply: [ "components/updates.uc", "component-auto-update-apply", 0 ],
component_update_check_cache: [ "components/updates.uc", "component-update-check-cache", 0 ],
+192 -1
View File
@@ -2840,10 +2840,201 @@ function component_action(component, action) {
action_fail(component != "" ? component : "unknown", action != "" ? action : "unknown", "Unknown component action");
}
function list_component_releases(component, count) {
component = normalize_component_name(component);
count = int(count || 3);
if (count < 1) count = 1;
if (count > 10) count = 10;
let owner = "";
let repo = "";
let per_page = as_string(count);
if (component == "tachyon") {
let parts = split(TACHYON_RELEASE_REPO, "/");
if (length(parts) != 2) { print("[]"); return; }
owner = parts[0]; repo = parts[1];
} else if (component == "sing_box") {
let variant = sing_box_runtime_output("variant", []);
if (variant == "lx") { owner = "Leadaxe"; repo = "sing-box-lx"; }
else if (variant == "extended" || variant == "extended-compressed") { owner = "shtorm-7"; repo = "sing-box-extended"; }
else { owner = "SagerNet"; repo = "sing-box"; }
} else if (component == "zapret") {
owner = "remittor"; repo = "zapret-openwrt";
} else if (component == "zapret2") {
owner = "Dushnilin"; repo = "zapret2-openwrt";
} else if (component == "byedpi") {
owner = "DPITrickster"; repo = "ByeDPI-OpenWrt";
} else {
print("[]"); return;
}
let releases_json = fetch_github_releases_json(owner, repo, per_page);
if (releases_json == "") { print("[]"); return; }
let releases = [];
try {
let parsed = json(releases_json);
if (type(parsed) == "array") {
for (let r in parsed) {
let tag = trim(as_string(r.tag_name || ""));
let name = trim(as_string(r.name || tag));
let published = trim(as_string(r.published_at || r.created_at || ""));
let html_url = trim(as_string(r.html_url || ""));
if (tag == "") continue;
let prerelease = !!r.prerelease;
push(releases, { tag, name, published, prerelease, release_url: html_url });
}
}
} catch (e) {}
write_json(releases);
}
function resolve_component_release(component, tag) {
component = normalize_component_name(component);
tag = trim(as_string(tag));
if (tag == "") return null;
if (component == "tachyon") {
let parts = split(TACHYON_RELEASE_REPO, "/");
if (length(parts) != 2) return null;
let base_dl = "https://github.com/" + parts[0] + "/" + parts[1] + "/releases/download/" + tag + "/";
let ext = is_apk() ? "apk" : "ipk";
let tag_clean = replace(tag, /^v/, "");
return {
tag,
packages: [
base_dl + "tachyon_" + tag_clean + "." + ext,
base_dl + "luci-app-tachyon_" + tag_clean + "." + ext,
base_dl + "luci-i18n-tachyon-ru_" + tag_clean + "." + ext
]
};
}
if (component == "sing_box") {
let variant = sing_box_runtime_output("variant", []);
let tag_clean = replace(tag, /^v/, "");
let base_dl = "";
if (variant == "lx") {
let arch_suffix = resolve_sing_box_extended_arch_suffix();
if (arch_suffix == "") return null;
base_dl = "https://github.com/Leadaxe/sing-box-lx/releases/download/" + tag + "/";
let asset_name = "sing-box-lx_" + tag_clean + "_linux-" + arch_suffix + ".tar.gz";
return { tag, packages: [ base_dl + asset_name ] };
} else if (variant == "extended" || variant == "extended-compressed") {
let compressed = (variant == "extended-compressed");
base_dl = "https://github.com/shtorm-7/sing-box-extended/releases/download/" + tag + "/";
if (compressed) {
let arch_suffix = resolve_sing_box_extended_arch_suffix();
if (arch_suffix == "") return null;
return { tag, packages: [ base_dl + "sing-box-extended_" + tag_clean + "_linux-" + arch_suffix + "-compressed.tar.gz" ] };
} else {
let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH");
if (distrib_arch == "") return null;
let asset_ext = is_apk() ? "apk" : "ipk";
return { tag, packages: [ base_dl + "sing-box-extended_" + tag_clean + "_openwrt_" + distrib_arch + "." + asset_ext ] };
}
} else {
let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH");
if (distrib_arch == "") return null;
let asset_ext = is_apk() ? "apk" : "ipk";
base_dl = "https://github.com/SagerNet/sing-box/releases/download/" + tag + "/";
return { tag, packages: [ base_dl + "sing-box_" + tag_clean + "_openwrt_" + distrib_arch + "." + asset_ext ] };
}
}
if (component == "zapret") {
let arch = resolve_arch_candidates();
if (arch == null) return null;
let base_dl = "https://github.com/remittor/zapret-openwrt/releases/download/" + tag + "/";
let tag_clean = replace(tag, /^v/, "");
return { tag, packages: [ base_dl + "zapret_" + tag_clean + "_openwrt_" + arch.candidates + ".zip" ] };
}
if (component == "zapret2") {
let arch = resolve_arch_candidates();
if (arch == null) return null;
let base_dl = "https://github.com/Dushnilin/zapret2-openwrt/releases/download/" + tag + "/";
let tag_clean = replace(tag, /^v/, "");
let ext = is_apk() ? "apk" : "ipk";
return { tag, packages: [ base_dl + "zapret2_" + tag_clean + "_openwrt_" + arch.candidates + "." + ext ] };
}
if (component == "byedpi") {
let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH");
if (distrib_arch == "") return null;
let tag_clean = replace(tag, /^v/, "");
let base_dl = "https://github.com/DPITrickster/ByeDPI-OpenWrt/releases/download/" + tag + "/";
let ext = is_apk() ? "apk" : "ipk";
return { tag, packages: [ base_dl + "byedpi_" + tag_clean + "_openwrt_" + distrib_arch + "." + ext ] };
}
return null;
}
function install_component_version(component, tag) {
component = normalize_component_name(component);
tag = trim(as_string(tag));
if (tag == "" || component == "") {
updates_response(false, component, "install_version", "Invalid component or tag", "", "", 0, "", "", null);
cleanup_action();
return;
}
let plan = resolve_component_release(component, tag);
if (plan == null || plan.packages == null || length(plan.packages) == 0) {
updates_response(false, component, "install_version", "Failed to resolve release assets for " + component + " " + tag, "", "", 0, "", "", null);
cleanup_action();
return;
}
let installed = 0;
let failed = 0;
for (let url in plan.packages) {
let fname = path_basename(url);
let args = [ "curl", "-sL", "--connect-timeout", "10", "-m", "120", "-o", fname ];
let proxy_addr = service_proxy_address();
if (proxy_addr != "") {
push(args, "-x");
push(args, "http://" + proxy_addr);
}
push(args, url);
let dl_ok = run_logged("Downloading " + fname, command_from_args(args));
if (!dl_ok || !file_exists(fname)) {
failed++;
continue;
}
let inst_ok = run_logged("Installing " + fname, pkg_install_files_command([ fname ], false));
if (inst_ok) installed++;
else failed++;
remove_file(fname);
}
if (component == "sing_box") {
write_file("/etc/tachyon/sing-box-version", tag + "\n");
let variant = sing_box_runtime_output("variant", []);
write_file("/etc/tachyon/sing-box-variant", variant + "\n");
}
let success = installed > 0 && failed == 0;
let msg = installed + " packages installed";
if (failed > 0) msg += ", " + failed + " failed";
updates_response(success, component, "install_version", msg, "", tag, success ? 1 : 0, success ? "latest" : "", "", null);
if (success) restart_tachyon_after_successful_change();
cleanup_action();
}
let mode = ARGV[0] || "";
if (mode == "component-action")
component_action(ARGV[1], ARGV[2]);
else if (mode == "list-component-releases")
list_component_releases(ARGV[1], ARGV[2]);
else if (mode == "install-component-version")
install_component_version(ARGV[1], ARGV[2]);
else if (mode == "latest-tachyon-release-json")
print(latest_tachyon_release_json());
else if (mode == "latest-tachyon-version")
@@ -2853,6 +3044,6 @@ else if (mode == "tachyon-release-metadata")
else if (mode == "pkg-install-files-command")
print(pkg_install_files_command(slice(ARGV, 2), ARGV[1] == "1"), "\n");
else {
warn("Usage: components/action.uc <component-action|latest-tachyon-version|tachyon-release-metadata> ...\n");
warn("Usage: components/action.uc <component-action|list-component-releases|install-component-version|latest-tachyon-version|tachyon-release-metadata> ...\n");
exit(1);
}