- service_check: lan_interface() запрашивала несуществующий конфиг forkop/podkop, из-за чего все проверки сервисов смотрели в br-lan вместо настроенных LAN-интерфейсов - секции AWG/WireGuard всегда получали вердикт "не отвечает" при рабочем kernel-конфиге (#41, #32, #39) - rag: у push() были перепутаны аргументы - коллекция эмбеддингов оставалась пустой для всех провайдеров OpenAI-формата; кэш эмбеддингов теперь инвалидируется по отпечатку индекса (mtime+size), null model_override обрабатывается корректно - warp_generator: curl к Clash API без таймаута и матрица ретраев без общего дедлайна могли блокировать выполнение минутами и ловить XHR timeout LuCI (#39); добавлены -m 5 на локальные вызовы и бюджеты 40с/90с на весь цикл - build.sh: релизные пакеты теперь содержат тот же состав файлов, что и сборка через OpenWrt Makefile (hotplug WAN-монитор, агентский CGI в /usr/lib/cgi-bin, defaults/config, профили servicecheck); конфиги с секретами ставятся с правами 0600; тест паритета инвентаря - updates UI: после самообновления tachyon страница перезагружается только когда перезапущенный бэкенд снова отвечает на get_ui_state, вместо фиксированной задержки 8 секунд (#23)
This commit is contained in:
@@ -247,14 +247,27 @@ build_backend_root() {
|
||||
rm -rf "$output_root"
|
||||
make_dir "$output_root/etc/init.d"
|
||||
make_dir "$output_root/etc/config"
|
||||
make_dir "$output_root/etc/hotplug.d/iface"
|
||||
make_dir "$output_root/usr/bin"
|
||||
make_dir "$output_root/usr/lib/tachyon"
|
||||
make_dir "$output_root/usr/lib/cgi-bin"
|
||||
make_dir "$output_root/usr/share/tachyon"
|
||||
|
||||
install -m 0755 "$ROOT_DIR/tachyon/files/etc/init.d/tachyon" "$output_root/etc/init.d/tachyon"
|
||||
install -m 0644 "$ROOT_DIR/tachyon/files/etc/config/tachyon" "$output_root/etc/config/tachyon"
|
||||
install -m 0755 "$ROOT_DIR/tachyon/files/usr/bin/tachyon" "$output_root/usr/bin/tachyon"
|
||||
cp -a "$ROOT_DIR/tachyon/files/usr/lib/." "$output_root/usr/lib/tachyon/"
|
||||
|
||||
# Mirror Package/tachyon/install from tachyon/Makefile exactly: the release
|
||||
# artifacts are built here, so any file the Makefile ships must ship here too.
|
||||
install -m 0600 "$ROOT_DIR/tachyon/files/etc/config/tachyon" "$output_root/usr/lib/tachyon/defaults/config"
|
||||
install -m 0755 "$ROOT_DIR/tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor" \
|
||||
"$output_root/etc/hotplug.d/iface/99-tachyon-wan-monitor"
|
||||
install -m 0755 "$ROOT_DIR/tachyon/files/usr/lib/cgi-bin/tachyon-agent" \
|
||||
"$output_root/usr/lib/cgi-bin/tachyon-agent"
|
||||
install -m 0644 "$ROOT_DIR/tachyon/files/usr/share/tachyon/servicecheck_profiles.json" \
|
||||
"$output_root/usr/share/tachyon/servicecheck_profiles.json"
|
||||
|
||||
local commit_sha="${GIT_COMMIT_SHA:-}"
|
||||
if [[ -z "$commit_sha" || "$commit_sha" == "unknown" ]]; then
|
||||
commit_sha="$(git -C "$ROOT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")"
|
||||
@@ -265,7 +278,11 @@ build_backend_root() {
|
||||
"$output_root/usr/lib/tachyon/core/constants.uc"
|
||||
|
||||
normalize_package_root_modes "$output_root"
|
||||
chmod 0755 "$output_root/etc/init.d/tachyon" "$output_root/usr/bin/tachyon"
|
||||
chmod 0755 "$output_root/etc/init.d/tachyon" "$output_root/usr/bin/tachyon" \
|
||||
"$output_root/etc/hotplug.d/iface/99-tachyon-wan-monitor" \
|
||||
"$output_root/usr/lib/cgi-bin/tachyon-agent"
|
||||
# Contains the Telegram bot token placeholder and receives user secrets.
|
||||
chmod 0600 "$output_root/etc/config/tachyon" "$output_root/usr/lib/tachyon/defaults/config"
|
||||
}
|
||||
|
||||
build_app_root() {
|
||||
|
||||
@@ -375,13 +375,32 @@ function notifyActionProvidersAvailabilityChanged(
|
||||
);
|
||||
}
|
||||
|
||||
const RELOAD_POLL_INTERVAL_MS = 1000;
|
||||
const RELOAD_POLL_MAX_WAIT_MS = 30000;
|
||||
|
||||
async function waitForTachyonResponsive() {
|
||||
const deadline = Date.now() + RELOAD_POLL_MAX_WAIT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await TachyonShellMethods.getUiState();
|
||||
if (response.success) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Backend is restarting — keep polling until it answers again.
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, RELOAD_POLL_INTERVAL_MS));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function reloadPageAfterTachyonUpdate() {
|
||||
// Give the backend time to restart services before reloading; the default
|
||||
// 1200ms was too short and caused the page to reload before tachyon init
|
||||
// finished, making the updates tab hang on stale component action state.
|
||||
window.setTimeout(() => {
|
||||
// Reload only after the restarted backend actually answers get_ui_state:
|
||||
// a fixed delay either reloaded too early (tab hung on stale component
|
||||
// action state) or wasted seconds on fast routers.
|
||||
void waitForTachyonResponsive().finally(() => {
|
||||
window.location.reload();
|
||||
}, 8000);
|
||||
});
|
||||
}
|
||||
|
||||
function patchSystemInfoAfterMutation(result: Tachyon.ComponentActionResult) {
|
||||
|
||||
@@ -15,6 +15,16 @@ function rag_index_path() {
|
||||
return INDEX_BUILTIN;
|
||||
}
|
||||
|
||||
// Fingerprint of the active index file. Cached embeddings are only valid for
|
||||
// the exact index they were computed from; without this check a rebuilt index
|
||||
// with the same chunk count silently serves stale vectors forever.
|
||||
function index_stamp() {
|
||||
let st = fs.stat(rag_index_path());
|
||||
if (!st)
|
||||
return "missing";
|
||||
return sprintf("%d-%d", int(st.st_mtime || 0), int(st.st_size || 0));
|
||||
}
|
||||
|
||||
function load_raw_index() {
|
||||
if (rag_index)
|
||||
return rag_index;
|
||||
@@ -40,6 +50,7 @@ function load_embedding_cache() {
|
||||
function save_embedding_cache(embeddings, model) {
|
||||
let data = {
|
||||
model: model,
|
||||
index_stamp: index_stamp(),
|
||||
timestamp: time(),
|
||||
embeddings: embeddings
|
||||
};
|
||||
@@ -76,6 +87,7 @@ function get_api_url(provider, api_key, custom_url) {
|
||||
|
||||
function get_embedding_model(provider, model_override) {
|
||||
provider = lc(trim(as_string(provider)));
|
||||
model_override = trim(as_string(model_override));
|
||||
if (model_override != "")
|
||||
return model_override;
|
||||
if (provider == "openai")
|
||||
@@ -124,7 +136,7 @@ function call_embedding_api(api_url, api_key, model, texts) {
|
||||
if (type(data.data) == "array" && length(data.data) > 0) {
|
||||
let embeddings = [];
|
||||
for (let i = 0; i < length(data.data); i++)
|
||||
push(data.data[i].embedding, embeddings);
|
||||
push(embeddings, data.data[i].embedding);
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
@@ -153,8 +165,10 @@ function cosine_similarity(a, b) {
|
||||
function ensure_embeddings(index, provider, api_key, custom_url, model_override) {
|
||||
let cached = load_embedding_cache();
|
||||
let model = get_embedding_model(provider, model_override);
|
||||
let stamp = index_stamp();
|
||||
|
||||
if (cached && length(cached.embeddings) == length(index.chunks) && cached.model == model) {
|
||||
if (cached && length(cached.embeddings) == length(index.chunks) &&
|
||||
cached.model == model && cached.index_stamp == stamp) {
|
||||
for (let i = 0; i < length(index.chunks); i++)
|
||||
index.chunks[i].embedding = cached.embeddings[i];
|
||||
return true;
|
||||
|
||||
@@ -411,8 +411,7 @@ function sing_box_config_path() {
|
||||
}
|
||||
|
||||
function lan_interface() {
|
||||
let namespace = backend_id() == "podkop" ? "podkop" : "forkop";
|
||||
let configured = words(uci_get(namespace + ".settings.source_network_interfaces"));
|
||||
let configured = words(uci_get("tachyon.settings.source_network_interfaces"));
|
||||
if (length(configured) > 0)
|
||||
return configured[0];
|
||||
return "br-lan";
|
||||
|
||||
@@ -79,6 +79,13 @@ function exec_output(command) {
|
||||
return data == null ? "" : trim(data);
|
||||
}
|
||||
|
||||
function now_ms() {
|
||||
let c = clock();
|
||||
if (type(c) != "array" || length(c) < 2)
|
||||
return 0;
|
||||
return c[0] * 1000 + int(c[1] / 1000000);
|
||||
}
|
||||
|
||||
function contains(arr, val) {
|
||||
for (let x in arr) {
|
||||
if (x == val) return true;
|
||||
@@ -89,6 +96,10 @@ function contains(arr, val) {
|
||||
function call_api(method, url, auth_header, body_file) {
|
||||
let resolve_ips = [ "162.159.192.1", "162.159.193.1", "188.114.98.1", "188.114.99.1" ];
|
||||
let last_res = "";
|
||||
// Hard wall-clock budget: without it the full retry matrix (transports x
|
||||
// resolve IPs x fallbacks) can block for minutes and trip the LuCI XHR
|
||||
// timeout before any answer is produced.
|
||||
let deadline = now_ms() + 40000;
|
||||
|
||||
let transport_flags = [];
|
||||
if (proxy_interface != "") {
|
||||
@@ -105,8 +116,10 @@ function call_api(method, url, auth_header, body_file) {
|
||||
|
||||
// 1. Try selected tunnel transport(s)
|
||||
for (let tr in transport_flags) {
|
||||
if (now_ms() > deadline) break;
|
||||
// A. With --resolve to Cloudflare IPs
|
||||
for (let ip in resolve_ips) {
|
||||
if (now_ms() > deadline) break;
|
||||
let auth_str = auth_header != "" ? "-H 'Authorization: Bearer " + auth_header + "' " : "";
|
||||
let data_str = body_file != "" ? "-d @" + body_file + " " : "";
|
||||
let cmd = "curl -s --connect-timeout 3 --max-time 7 " + tr + " --resolve api.cloudflareclient.com:443:" + ip + " -X " + method + " -H 'Content-Type: application/json' -H 'User-Agent: okhttp/3.12.1' " + auth_str + data_str + url;
|
||||
@@ -121,6 +134,7 @@ function call_api(method, url, auth_header, body_file) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (now_ms() > deadline) break;
|
||||
// B. Directly via DNS on tunnel
|
||||
let auth_str = auth_header != "" ? "-H 'Authorization: Bearer " + auth_header + "' " : "";
|
||||
let data_str = body_file != "" ? "-d @" + body_file + " " : "";
|
||||
@@ -137,8 +151,13 @@ function call_api(method, url, auth_header, body_file) {
|
||||
}
|
||||
}
|
||||
|
||||
if (now_ms() > deadline) {
|
||||
return last_res != "" ? { success: false, message: last_res } : null;
|
||||
}
|
||||
|
||||
// 2. Direct WAN fallback if no tunnel transport worked
|
||||
for (let ip in resolve_ips) {
|
||||
if (now_ms() > deadline) break;
|
||||
let auth_str = auth_header != "" ? "-H 'Authorization: Bearer " + auth_header + "' " : "";
|
||||
let data_str = body_file != "" ? "-d @" + body_file + " " : "";
|
||||
let cmd = "curl -s --connect-timeout 3 --max-time 6 --resolve api.cloudflareclient.com:443:" + ip + " -X " + method + " -H 'Content-Type: application/json' -H 'User-Agent: okhttp/3.12.1' " + auth_str + data_str + url;
|
||||
@@ -160,7 +179,7 @@ function call_api(method, url, auth_header, body_file) {
|
||||
// Query current proxy selection from Clash API for automatic rate limit rotation if proxy is used
|
||||
let original_server = "";
|
||||
let available_servers = [];
|
||||
let clash_data_str = exec_output("curl -s " + get_clash_base_url() + "/proxies/MAIN-out");
|
||||
let clash_data_str = exec_output("curl -s -m 5 " + get_clash_base_url() + "/proxies/MAIN-out");
|
||||
if (clash_data_str != "") {
|
||||
try {
|
||||
let parsed = json(clash_data_str);
|
||||
@@ -177,7 +196,7 @@ function restore_original_server() {
|
||||
let body = { name: original_server };
|
||||
let tmp_file = "/tmp/restore_clash.json";
|
||||
fs.writefile(tmp_file, sprintf("%J", body));
|
||||
exec_output("curl -s -X PUT -H 'Content-Type: application/json' -d @" + tmp_file + " " + get_clash_base_url() + "/proxies/MAIN-out");
|
||||
exec_output("curl -s -m 5 -X PUT -H 'Content-Type: application/json' -d @" + tmp_file + " " + get_clash_base_url() + "/proxies/MAIN-out");
|
||||
fs.unlink(tmp_file);
|
||||
}
|
||||
}
|
||||
@@ -186,7 +205,7 @@ function switch_server(srv_name) {
|
||||
let body = { name: srv_name };
|
||||
let tmp_file = "/tmp/switch_clash.json";
|
||||
fs.writefile(tmp_file, sprintf("%J", body));
|
||||
exec_output("curl -s -X PUT -H 'Content-Type: application/json' -d @" + tmp_file + " " + get_clash_base_url() + "/proxies/MAIN-out");
|
||||
exec_output("curl -s -m 5 -X PUT -H 'Content-Type: application/json' -d @" + tmp_file + " " + get_clash_base_url() + "/proxies/MAIN-out");
|
||||
fs.unlink(tmp_file);
|
||||
exec_output("sleep 1");
|
||||
}
|
||||
@@ -257,8 +276,15 @@ let response_data = null;
|
||||
let last_error = "";
|
||||
let chosen_prefix = "";
|
||||
let server_try_index = 0;
|
||||
// Overall budget for the whole registration attempt matrix: each call_api()
|
||||
// is bounded, but the prefix list is long enough to multiply into minutes.
|
||||
let registration_deadline = now_ms() + 90000;
|
||||
|
||||
for (let prefix in api_prefixes) {
|
||||
if (now_ms() > registration_deadline) {
|
||||
last_error = "Registration aborted: overall time budget exceeded";
|
||||
break;
|
||||
}
|
||||
let url = "https://api.cloudflareclient.com/" + prefix + "/reg";
|
||||
let parsed = call_api("POST", url, "", tmp_req_file);
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
MAKEFILE="$ROOT_DIR/tachyon/Makefile"
|
||||
BUILD_SH="$ROOT_DIR/build.sh"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -f "$MAKEFILE" ] || fail "tachyon/Makefile not found"
|
||||
[ -f "$BUILD_SH" ] || fail "build.sh not found"
|
||||
|
||||
# Release packages are produced by build.sh, while the OpenWrt feed build uses
|
||||
# tachyon/Makefile. Both must ship an identical file inventory or installs
|
||||
# silently lose hotplug handlers, the agent CGI and reset defaults.
|
||||
required_sources=(
|
||||
"files/etc/init.d/tachyon"
|
||||
"files/etc/config/tachyon"
|
||||
"files/usr/bin/tachyon"
|
||||
"files/etc/hotplug.d/iface/99-tachyon-wan-monitor"
|
||||
"files/usr/lib/cgi-bin/tachyon-agent"
|
||||
"files/usr/share/tachyon/servicecheck_profiles.json"
|
||||
)
|
||||
|
||||
for rel in "${required_sources[@]}"; do
|
||||
[ -f "$ROOT_DIR/tachyon/$rel" ] || fail "source file missing: tachyon/$rel"
|
||||
grep -Fq "$rel" "$MAKEFILE" || fail "Makefile no longer ships $rel — update tests/package_inventory.sh"
|
||||
grep -Fq "$rel" "$BUILD_SH" || fail "build.sh does not package $rel (diverges from Makefile)"
|
||||
done
|
||||
|
||||
# Derived file installed only by the packaging recipes (not present in files/).
|
||||
grep -Fq '/usr/lib/tachyon/defaults/config' "$MAKEFILE" ||
|
||||
fail "Makefile no longer installs defaults/config"
|
||||
grep -Fq 'usr/lib/tachyon/defaults/config' "$BUILD_SH" ||
|
||||
fail "build.sh does not package /usr/lib/tachyon/defaults/config (reset_settings would break)"
|
||||
|
||||
# Secrets-bearing files must not be world-readable in release packages.
|
||||
grep -Eq 'chmod 0600 .*etc/config/tachyon' "$BUILD_SH" ||
|
||||
fail "build.sh must enforce mode 0600 on /etc/config/tachyon"
|
||||
grep -Eq 'chmod 0600 .*usr/lib/tachyon/defaults/config' "$BUILD_SH" ||
|
||||
fail "build.sh must enforce mode 0600 on defaults/config"
|
||||
|
||||
printf 'package inventory checks passed\n'
|
||||
Reference in New Issue
Block a user