diff --git a/local/scripts/selfhost-shell.test.ts b/local/scripts/selfhost-shell.test.ts index a3bb485..3895239 100644 --- a/local/scripts/selfhost-shell.test.ts +++ b/local/scripts/selfhost-shell.test.ts @@ -282,8 +282,8 @@ ENV expect(result.status).toBe(0); expect(result.stdout).toContain("健康检查: 正常"); expect(result.stdout).not.toContain("健康检查: 异常"); - // wait_for_health stops once ready succeeds, then status_cmd performs one final live+ready check. - expect(result.stdout).toContain("curl_count=7"); + // wait_for_health checks live once per attempt, then status_cmd performs one final live+ready check. + expect(result.stdout).toContain("curl_count=8"); }, 10_000); it("uses refreshed release metadata before pulling during update", () => { diff --git a/local/scripts/subboost.sh b/local/scripts/subboost.sh index 51a13f1..c564fd7 100644 --- a/local/scripts/subboost.sh +++ b/local/scripts/subboost.sh @@ -3,6 +3,7 @@ set -Eeuo pipefail DEFAULT_HOME="/opt/subboost" DEFAULT_STABLE_RELEASE_URL="https://github.com/SubBoost/subboost/releases/latest/download/release.json" +DEFAULT_BACKUP_RETENTION_COUNT="10" SUBBOOST_HOME="${SUBBOOST_HOME:-$DEFAULT_HOME}" ENV_FILE="$SUBBOOST_HOME/.env" COMPOSE_FILE="$SUBBOOST_HOME/docker-compose.yml" @@ -225,17 +226,25 @@ health_status_text() { } health_status_code() { - local port base + local port base live_ok port="$(port_number "${SUBBOOST_PORT:-3000}")" base="http://127.0.0.1:$port" if ! command -v curl>/dev/null 2>&1; then printf 'curl-missing\n' - elif curl -fsS "$base/api/health/live">/dev/null 2>&1 && curl -fsS "$base/api/health/ready">/dev/null 2>&1; then - printf 'ok\n' - elif curl -fsS "$base/api/health/live">/dev/null 2>&1; then - printf 'not-ready\n' else - printf 'unhealthy\n' + if curl -fsS "$base/api/health/live">/dev/null 2>&1; then + live_ok=1 + else + live_ok=0 + fi + + if [ "$live_ok" = "1" ] && curl -fsS "$base/api/health/ready">/dev/null 2>&1; then + printf 'ok\n' + elif [ "$live_ok" = "1" ]; then + printf 'not-ready\n' + else + printf 'unhealthy\n' + fi fi } @@ -249,6 +258,7 @@ health_status_label() { } wait_for_health() { + # Default max wait is about 30 seconds: 15 attempts with a 2-second interval. local attempts="${SUBBOOST_DOCTOR_HEALTH_ATTEMPTS:-15}" local interval="${SUBBOOST_DOCTOR_HEALTH_INTERVAL_SECONDS:-2}" local index status @@ -336,12 +346,25 @@ backup_cmd() { sudo_do mkdir -p "$BACKUP_DIR" local stamp db_tmp db_out env_out local -a sql_backups env_backups - local i + local -a backup_status + local i backup_retention_count + backup_retention_count="${SUBBOOST_BACKUP_RETENTION_COUNT:-$DEFAULT_BACKUP_RETENTION_COUNT}" + if ! [[ "$backup_retention_count" =~ ^[0-9]+$ ]] || (( backup_retention_count < 1 )); then + die "SUBBOOST_BACKUP_RETENTION_COUNT must be a positive integer" + fi stamp="$(date -u +%Y%m%dT%H%M%SZ)" db_tmp="$BACKUP_DIR/subboost-$stamp.sql.gz.partial" db_out="$BACKUP_DIR/subboost-$stamp.sql.gz" env_out="$BACKUP_DIR/subboost-$stamp.env" + set +e compose exec -T db pg_dump -U "${POSTGRES_USER:-subboost}" -d "${POSTGRES_DB:-subboost}" | gzip -c | sudo_do tee "$db_tmp">/dev/null + backup_status=("${PIPESTATUS[@]}") + set -e + if (( backup_status[0] != 0 || backup_status[1] != 0 || backup_status[2] != 0 )); then + sudo_do rm -f -- "$db_tmp" + say "Backup failed: pg_dump=${backup_status[0]} gzip=${backup_status[1]} write=${backup_status[2]}" + return 1 + fi sudo_do mv "$db_tmp" "$db_out" sudo_do install -m 600 "$ENV_FILE" "$env_out" @@ -350,10 +373,10 @@ backup_cmd() { env_backups=("$BACKUP_DIR"/subboost-*.env) shopt -u nullglob - for ((i = 0; i < ${#sql_backups[@]} - 10; i++)); do + for ((i = 0; i < ${#sql_backups[@]} - backup_retention_count; i++)); do sudo_do rm -f -- "${sql_backups[$i]}" done - for ((i = 0; i < ${#env_backups[@]} - 10; i++)); do + for ((i = 0; i < ${#env_backups[@]} - backup_retention_count; i++)); do sudo_do rm -f -- "${env_backups[$i]}" done say "Backup written:" diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-added-rule-sets.tsx b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-added-rule-sets.tsx index 79ad1fc..726f9a3 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-added-rule-sets.tsx +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-added-rule-sets.tsx @@ -72,7 +72,6 @@ export function ProxyGroupsAddedRuleSets({ customProxyGroups = [], proxyGroupNameOverrides = {}, toggleProxyGroup, - addModuleRules, updateModuleRule, removeModuleRule, moveModuleRule, diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel.tsx b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel.tsx index 8c7b384..872909a 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel.tsx +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel.tsx @@ -23,10 +23,7 @@ import { isRuleSetMoveTarget, type RuleSetMoveTarget, } from "./proxy-group-rule-row"; -import { - ProxyGroupTypeMenu, - type ProxyGroupTypeMenuValue, -} from "./proxy-group-type-menu"; +import type { ProxyGroupTypeMenuValue } from "./proxy-group-type-menu"; import { ProxyGroupAdvancedPanel } from "./proxy-group-advanced-panel"; import { buildProxyGroupName, diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-rules.tsx b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-rules.tsx index ab83548..5525b2c 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-rules.tsx +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-rules.tsx @@ -168,20 +168,21 @@ export function ProxyGroupsCustomRules() { const handleAddCustomRule = () => { const value = newRuleValue.trim(); if (!value || !newRuleTarget) return; + const addedRuleType = newRuleType; addCustomRule({ id: createCustomRuleId(), - type: newRuleType, + type: addedRuleType, value, target: newRuleTarget, noResolve: newRuleNoResolve, }); interactions.ruleAdded?.({ source: "manual", - kind: getProductRuleKind(newRuleType), + kind: getProductRuleKind(addedRuleType), }); setNewRuleValue(""); - setNewRuleNoResolve(isIpCidrRuleType(newRuleType)); + setNewRuleNoResolve(isIpCidrRuleType(addedRuleType)); }; const handleNewRuleTypeChange = (value: string) => { diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-rules-library.test.ts b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-rules-library.test.ts index ca8b2e6..7ed964c 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-rules-library.test.ts +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-rules-library.test.ts @@ -110,11 +110,11 @@ vi.mock("@subboost/ui/components/ui/select", () => ({ })); vi.mock("@subboost/ui/components/ui/toaster", () => ({ toast: mocks.toast })); vi.mock("@subboost/core/generator/proxy-groups", () => ({ - PROXY_GROUP_MODULES: [ - { id: "auto", name: "Auto", rules: [{ id: "netflix" }] }, - { id: "fallback", name: "Fallback", rules: [] }, - { id: "bare", name: "Bare" }, - ], + PROXY_GROUP_MODULES: [ + { id: "auto", name: "Auto", rules: [{ id: "netflix" }] }, + { id: "fallback", name: "Fallback", rules: [] }, + { id: "bare", name: "Bare" }, + ], })); vi.mock("@subboost/core/generator/module-rules", () => ({ getModuleRuleOrderKey: (moduleId: string, ruleId: string) => `module:${moduleId}:${ruleId}`, diff --git a/packages/ui/src/store/config-store/actions/custom-actions.ts b/packages/ui/src/store/config-store/actions/custom-actions.ts index a92dd87..fa125a0 100644 --- a/packages/ui/src/store/config-store/actions/custom-actions.ts +++ b/packages/ui/src/store/config-store/actions/custom-actions.ts @@ -20,7 +20,7 @@ type CustomActions = Pick< | "updateCustomProxyGroup">; -function normalizeRuleOrderForState(state: { +type NormalizeRuleOrderState = { enabledProxyGroups: string[]; customProxyGroups: Parameters[0]["customProxyGroups"]; customRules: Parameters[0]["customRules"]; @@ -30,7 +30,9 @@ function normalizeRuleOrderForState(state: { experimentalCnUseCnRuleSet: boolean; cnIpNoResolve: boolean; ruleOrder: string[]; -}): string[] { +}; + +function normalizeRuleOrderForState(state: NormalizeRuleOrderState): string[] { return normalizePersistedRuleOrder({ enabledModules: state.enabledProxyGroups, customProxyGroups: state.customProxyGroups, @@ -161,9 +163,9 @@ export function createCustomActions( const nextCustomProxyGroups = state.customProxyGroups.filter( (g) => g.id !== id, ); - const removedTarget = removedGroup?.name?.trim() || ""; - const nextCustomRuleSets = removedTarget - ? state.customRuleSets.filter((ruleSet) => ruleSet.target !== removedTarget) + const removedGroupName = removedGroup?.name?.trim() || ""; + const nextCustomRuleSets = removedGroupName + ? state.customRuleSets.filter((ruleSet) => ruleSet.target !== removedGroupName) : state.customRuleSets; return { customProxyGroups: nextCustomProxyGroups, diff --git a/packages/ui/src/store/config-store/actions/proxy-group-rule-set-helpers.ts b/packages/ui/src/store/config-store/actions/proxy-group-rule-set-helpers.ts index 7ea0b34..08f9442 100644 --- a/packages/ui/src/store/config-store/actions/proxy-group-rule-set-helpers.ts +++ b/packages/ui/src/store/config-store/actions/proxy-group-rule-set-helpers.ts @@ -1,4 +1,3 @@ -import { getModuleRuleOrderKey, isPresetModuleRule } from "@subboost/core/generator/module-rules"; import { PROXY_GROUP_MODULES } from "@subboost/core/generator/proxy-groups"; import { normalizePersistedRuleOrder } from "@subboost/core/generator/rules"; import { resolveProxyGroupModuleName } from "@subboost/core/proxy-group-name"; diff --git a/scripts/selfhost-release-assets.cjs b/scripts/selfhost-release-assets.cjs index 5f32392..5522ce7 100644 --- a/scripts/selfhost-release-assets.cjs +++ b/scripts/selfhost-release-assets.cjs @@ -67,36 +67,36 @@ function parseArgs(argv) { for (const item of INSTALLER_DEFAULTS) { args[item.key] = process.env[item.env] || ""; } - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index]; + for (let argIndex = 0; argIndex < argv.length; argIndex += 1) { + const arg = argv[argIndex]; if (arg === "--base-url") { - args.baseUrl = argv[index + 1] || ""; - index += 1; + args.baseUrl = argv[argIndex + 1] || ""; + argIndex += 1; } else if (arg === "--build-sha") { - args.buildSha = argv[index + 1] || ""; - index += 1; + args.buildSha = argv[argIndex + 1] || ""; + argIndex += 1; } else if (arg === "--dry-run") { args.dryRun = true; } else if (arg === "--image") { - args.image = argv[index + 1] || ""; - index += 1; + args.image = argv[argIndex + 1] || ""; + argIndex += 1; } else if (arg === "--image-repository") { - args.imageRepository = argv[index + 1] || ""; - index += 1; + args.imageRepository = argv[argIndex + 1] || ""; + argIndex += 1; } else if (arg === "--image-tag") { - args.imageTag = argv[index + 1] || ""; - index += 1; + args.imageTag = argv[argIndex + 1] || ""; + argIndex += 1; } else if (arg === "--output") { - args.output = argv[index + 1] || ""; - index += 1; + args.output = argv[argIndex + 1] || ""; + argIndex += 1; } else if (arg === "--tag" || arg === "--release-tag") { - args.releaseTag = argv[index + 1] || ""; - index += 1; + args.releaseTag = argv[argIndex + 1] || ""; + argIndex += 1; } else { const installerDefault = INSTALLER_DEFAULTS.find((item) => item.arg === arg); if (installerDefault) { - args[installerDefault.key] = argv[index + 1] || ""; - index += 1; + args[installerDefault.key] = argv[argIndex + 1] || ""; + argIndex += 1; } else if (arg === "--help" || arg === "-h") { args.help = true; } else { @@ -145,7 +145,8 @@ function buildManifest(publicRoot, args) { const shortSha = buildSha.slice(0, 12); const releaseTag = args.releaseTag || `v${version}`; const imageRepository = args.imageRepository || DEFAULT_IMAGE_REPOSITORY; - const imageTag = args.imageTag || `${imageRepository}:${releaseTag}`; + const defaultImageTag = `${imageRepository}:${releaseTag}`; + const imageTag = args.imageTag || defaultImageTag; const image = args.image || imageTag; const buildVersion = `${version}+sha.${shortSha}`; @@ -172,14 +173,22 @@ function copyFile(publicRoot, from, to, options = {}) { fs.copyFileSync(source, to); } +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function replaceInstallerDefault(content, item, replacement) { - const expected = `${item.name}="${item.value}"`; const replacementLine = `${item.name}="${replacement}"`; - const count = content.split(expected).length - 1; + const assignmentPattern = new RegExp( + `^\\s*${escapeRegExp(item.name)}\\s*=\\s*(["'])${escapeRegExp(item.value)}\1円\\s*$`, + "gm", + ); + const matches = content.match(assignmentPattern); + const count = matches ? matches.length : 0; if (count !== 1) { throw new Error(`Expected exactly one ${item.name} assignment in install.sh, found ${count}.`); } - return content.replace(expected, replacementLine); + return content.replace(assignmentPattern, () => replacementLine); } function withInferredInstallerDefaults(args) {

AltStyle によって変換されたページ (->オリジナル) /