diff --git a/docker.compose.yaml b/docker.compose.yaml index bbeab3d4..1aa76c73 100644 --- a/docker.compose.yaml +++ b/docker.compose.yaml @@ -1,6 +1,6 @@ services: sublane: - image: ${SUBLANE_IMAGE:-ghcr.io/murongg/sublane:latest} + image: ${SUBLANE_IMAGE:-ghcr.io/alumos/sublane:latest} ports: - "${SUBLANE_BIND_ADDRESS:-127.0.0.1}:${SUBLANE_PORT:-8080}:8080" environment: diff --git a/internal/channels/channels.go b/internal/channels/channels.go index a227cd45..8264802c 100644 --- a/internal/channels/channels.go +++ b/internal/channels/channels.go @@ -108,9 +108,7 @@ func (s *Service) Delete(ctx context.Context, id string) error { // Update renames a channel and optionally replaces its key while keeping the channel ID. func (s *Service) Update(ctx context.Context, id string, input Input) (Channel, error) { - if _, err := s.Get(ctx, id); err != nil { - return Channel{}, err - } + // UpdateAPIChannel rejects missing and non-channel resources itself. row, err := s.resources.UpdateAPIChannel(ctx, id, accounts.APIChannelUpdate{Name: input.Name, APIKey: input.APIKey, BaseURL: input.BaseURL, Platform: input.Platform, Protocol: input.Protocol}) if errors.Is(err, accounts.ErrNotFound) { return Channel{}, ErrNotFound diff --git a/internal/gateway/sticky_test.go b/internal/gateway/sticky_test.go index 5c0c7dfc..36de6776 100644 --- a/internal/gateway/sticky_test.go +++ b/internal/gateway/sticky_test.go @@ -12,15 +12,13 @@ import ( func stickyGateway(t *testing.T, sticky bool) (*Service, string, string) { t.Helper() s, parent, other := reviewGateway(t) - return stickyGatewayFrom(t, s, sticky, parent, other) + setSticky(t, s, sticky) + return s, parent, other } -func stickyGatewayFrom(t *testing.T, s *Service, sticky bool, ids ...string) (*Service, string, string) { +// setSticky toggles session stickiness on the fixture's group, keeping its resources. +func setSticky(t *testing.T, s *Service, sticky bool) { t.Helper() - parent, other := "", "" - if len(ids) == 2 { - parent, other = ids[0], ids[1] - } detail, err := groups.New(s.db).Get(context.Background(), 1) if err != nil { t.Fatal(err) @@ -28,7 +26,6 @@ func stickyGatewayFrom(t *testing.T, s *Service, sticky bool, ids ...string) (*S if _, err := groups.New(s.db).Save(context.Background(), 1, groups.Input{Name: detail.Name, Enabled: true, AccountIDs: detail.AccountIDs, Routing: &groups.Routing{Preference: "protocol", SessionSticky: sticky}}); err != nil { t.Fatal(err) } - return s, parent, other } func openSticky(t *testing.T, s *Service, headers http.Header, body string) string { @@ -210,7 +207,7 @@ func TestStickinessHonorsBindingsMadeBeforeOptIn(t *testing.T) { if err != nil { t.Fatal(err) } - s, _, _ = stickyGatewayFrom(t, s, true) + setSticky(t, s, true) if _, err := s.accounts.SetEnabled(ctx, original, false); err != nil { t.Fatal(err) } @@ -246,7 +243,7 @@ func TestNormalizedBindingsSurviveOptOut(t *testing.T) { if err != nil { t.Fatal(err) } - s, _, _ = stickyGatewayFrom(t, s, false) + setSticky(t, s, false) if _, err := s.accounts.SetEnabled(ctx, original, false); err != nil { t.Fatal(err) } diff --git a/internal/updates/updates.go b/internal/updates/updates.go index 34b3a80a..1ef27503 100644 --- a/internal/updates/updates.go +++ b/internal/updates/updates.go @@ -18,10 +18,13 @@ import ( "time" ) -const releasesURL = "https://api.github.com/repos/murongg/SubLane/releases/latest" -const releaseBase = "https://github.com/murongg/SubLane/releases/tag/v" +// This fork publishes its own releases; updates never switch an installation to another repository. +const repository = "Alumos/SubLane" +const releasesURL = "https://api.github.com/repos/" + repository + "/releases/latest" +const releaseBase = "https://github.com/" + repository + "/releases/tag/v" -var stable = regexp.MustCompile(`^(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})$`) +// Stable fork releases are X.Y.Z or X.Y.Z-revised.N, ordered by (X, Y, Z, N) with N=0 for X.Y.Z. +var stable = regexp.MustCompile(`^(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})(?:-revised\.([1-9][0-9]{0,5}))?$`) var jobID = regexp.MustCompile(`^[a-f0-9]{32}$`) var ( ErrCheck = errors.New("release check failed") @@ -186,11 +189,12 @@ func (s *Service) fetch(ctx context.Context) (string, error) { return version, nil } func newer(next, current string) bool { - if !stable.MatchString(next) || !stable.MatchString(current) { + a, b := stable.FindStringSubmatch(next), stable.FindStringSubmatch(current) + if a == nil || b == nil { return false } - a, b := strings.Split(next, "."), strings.Split(current, ".") - for i := range a { + for i := 1; i <= 4; i++ { + // An absent revision sorts before every revision of the same base release. x, _ := strconv.Atoi(a[i]) y, _ := strconv.Atoi(b[i]) if x != y { diff --git a/internal/updates/updates_test.go b/internal/updates/updates_test.go index fb32694b..3a1250f8 100644 --- a/internal/updates/updates_test.go +++ b/internal/updates/updates_test.go @@ -192,3 +192,35 @@ func TestMailboxRefusesSymlinksAndOversizedStatus(t *testing.T) { t.Fatal("oversized status trusted") } } + +func TestForkRevisionsOrderAfterTheirBaseRelease(t *testing.T) { + for _, tc := range []struct { + next, current string + want bool + }{ + {"0.19.2-revised.8", "0.19.2-revised.7", true}, + {"0.19.2-revised.10", "0.19.2-revised.9", true}, + {"0.19.2-revised.1", "0.19.2", true}, + {"0.19.3", "0.19.2-revised.9", true}, + {"0.19.2-revised.7", "0.19.2-revised.7", false}, + {"0.19.2", "0.19.2-revised.1", false}, + {"0.19.2-revised.7", "0.20.0", false}, + {"0.19.2-rc.1", "0.19.1", false}, + {"0.19.2-revised.01", "0.19.1", false}, + {"0.19.2-revised.0", "0.19.1", false}, + } { + if got := newer(tc.next, tc.current); got != tc.want { + t.Fatal(tc.next, tc.current, got) + } + } + var requested string + s := New("0.19.2-revised.7", "", transport(func(r *http.Request) (*http.Response, error) { + requested = r.URL.String() + return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"tag_name":"v0.19.2-revised.8"}`))}, nil + })) + defer s.Close() + state, err := s.Check(context.Background(), true) + if err != nil || !state.Available || requested != "https://api.github.com/repos/Alumos/SubLane/releases/latest" { + t.Fatal("fork release was not offered", requested, state, err) + } +} diff --git a/internal/upstream/images.go b/internal/upstream/images.go index 08f612f7..959f0827 100644 --- a/internal/upstream/images.go +++ b/internal/upstream/images.go @@ -89,7 +89,8 @@ func (c *Client) Images(ctx context.Context, credential accounts.Credential, raw } return nil, ErrUpstream } - return &Stream{Response: response, registry: c.registry, model: model, original: raw, request: body}, nil + // Images responses are complete JSON documents; they never use stream translation state. + return &Stream{Response: response}, nil } // MultipartImageEdit converts an OpenAI multipart image edit into the JSON form the Codex endpoint accepts. diff --git a/internal/upstream/ipreport.go b/internal/upstream/ipreport.go index 871d4589..a8fdeb5b 100644 --- a/internal/upstream/ipreport.go +++ b/internal/upstream/ipreport.go @@ -21,29 +21,25 @@ var IPReportClient = &http.Client{Timeout: 15 * time.Second} // IPReport is a bounded subset of the third-party lookup. Unknown values stay empty or nil. type IPReport struct { - IP string `json:"ip"` - TrustScore *int64 `json:"trust_score"` - CompanyType string `json:"company_type"` - Residential bool `json:"residential"` - Datacenter bool `json:"datacenter"` - Mobile bool `json:"mobile"` - VPN bool `json:"vpn"` - Proxy bool `json:"proxy"` - Tor bool `json:"tor"` - Abuser bool `json:"abuser"` - Crawler bool `json:"crawler"` - ASN int64 `json:"asn"` - ASOrganization string `json:"as_organization"` - ISP string `json:"isp"` - CountryCode string `json:"country_code"` - Country string `json:"country"` - Region string `json:"region"` - City string `json:"city"` - RDNS string `json:"rdns"` - AbuserLevel string `json:"abuser_level"` - Verdict string `json:"verdict"` - Threats []string `json:"threats"` - FetchedAt int64 `json:"fetched_at"` + IP string `json:"ip"` + TrustScore *int64 `json:"trust_score"` + CompanyType string `json:"company_type"` + Residential bool `json:"residential"` + Datacenter bool `json:"datacenter"` + Mobile bool `json:"mobile"` + VPN bool `json:"vpn"` + Proxy bool `json:"proxy"` + Tor bool `json:"tor"` + Abuser bool `json:"abuser"` + Crawler bool `json:"crawler"` + ASN int64 `json:"asn"` + ASOrganization string `json:"as_organization"` + ISP string `json:"isp"` + CountryCode string `json:"country_code"` + Country string `json:"country"` + Region string `json:"region"` + City string `json:"city"` + Verdict string `json:"verdict"` } func boundedText(value string, limit int) string { @@ -110,14 +106,9 @@ func LookupIPReport(ctx context.Context, ip string) (IPReport, error) { Country string `json:"country"` Region string `json:"region"` City string `json:"city"` - RDNS string `json:"rdns"` Verdict *struct { Label string `json:"label"` } `json:"ai_verdict"` - Intelligence *struct { - Level string `json:"abuser_level"` - Threats []json.RawMessage `json:"threats"` - } `json:"intelligence"` } if json.Unmarshal(raw, &payload) != nil { return IPReport{}, ErrResponse @@ -127,7 +118,7 @@ func LookupIPReport(ctx context.Context, ip string) (IPReport, error) { } report := IPReport{IP: address.String(), CompanyType: boundedText(payload.CompanyType, 32), Residential: payload.Residential, Datacenter: payload.Datacenter, Mobile: payload.Mobile, VPN: payload.VPN, Proxy: payload.Proxy, Tor: payload.Tor, Abuser: payload.Abuser, Crawler: payload.Crawler, ASOrganization: boundedText(payload.ASOrg, 128), ISP: boundedText(payload.ISP, 128), CountryCode: strings.ToUpper(boundedText(payload.CountryCode, 2)), Country: boundedText(payload.Country, 64), - Region: boundedText(payload.Region, 64), City: boundedText(payload.City, 64), RDNS: boundedText(payload.RDNS, 253), Threats: []string{}, FetchedAt: time.Now().Unix()} + Region: boundedText(payload.Region, 64), City: boundedText(payload.City, 64)} if payload.ASN > 0 && payload.ASN < 1<<32 { report.ASN = payload.ASN } @@ -140,29 +131,5 @@ func LookupIPReport(ctx context.Context, ip string) (IPReport, error) { if payload.Verdict != nil { report.Verdict = boundedText(payload.Verdict.Label, 48) } - if payload.Intelligence != nil { - report.AbuserLevel = boundedText(payload.Intelligence.Level, 32) - for _, raw := range payload.Intelligence.Threats { - if len(report.Threats) == 8 { - break - } - var name string - if json.Unmarshal(raw, &name) != nil { - var named struct { - Name string `json:"name"` - Type string `json:"type"` - } - if json.Unmarshal(raw, &named) == nil { - name = named.Name - if name == "" { - name = named.Type - } - } - } - if name = boundedText(name, 48); name != "" { - report.Threats = append(report.Threats, name) - } - } - } return report, nil } diff --git a/internal/upstream/ipreport_test.go b/internal/upstream/ipreport_test.go index 06cb15f4..73fab649 100644 --- a/internal/upstream/ipreport_test.go +++ b/internal/upstream/ipreport_test.go @@ -25,7 +25,7 @@ func TestLookupIPReportBoundsThirdPartyFields(t *testing.T) { if err != nil { t.Fatal(err) } - if report.TrustScore == nil || *report.TrustScore != 87 || !report.Residential || !report.Proxy || report.CountryCode != "JP" || report.ASN != 64500 || report.Verdict != "住宅宽带" || len(report.Threats) != 2 || report.Threats[1] != "scanner" || report.FetchedAt == 0 { + if report.TrustScore == nil || *report.TrustScore != 87 || !report.Residential || !report.Proxy || report.CountryCode != "JP" || report.ASN != 64500 || report.Verdict != "住宅宽带" { t.Fatalf("report: %+v", report) } for _, private := range []string{"10.0.0.1", "127.0.0.1", "", "not-an-ip", "::1"} { diff --git a/scripts/install.sh b/scripts/install.sh index 8a2af0bf..821c0e4c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash set -euo pipefail +# Releases and images come from this fork so installs and updates stay on its channel. +install_repository=Alumos/SubLane fail() { printf 'Error: %s\n' "$*" >&2 @@ -232,7 +234,7 @@ release_metadata() { install_api_status=$(curl --silent --show-error --location --proto '=https' --proto-redir '=https' \ --connect-timeout 10 --max-time 120 --retry 2 --header 'Accept: application/vnd.github+json' \ --output "$install_stage/releases.json" --write-out '%{http_code}' \ - "https://api.github.com/repos/murongg/SubLane/$1") || fail 'Could not fetch GitHub releases. Retry or pass --version explicitly.' + "https://api.github.com/repos/$install_repository/$1") || fail 'Could not fetch GitHub releases. Retry or pass --version explicitly.' } resolve_version() { @@ -630,8 +632,8 @@ prepare_release() { trap 'exit 130' INT TERM if [[ -z $install_version ]]; then resolve_version; fi validate_version - install_release="https://github.com/murongg/SubLane/releases/download/v$install_version" - install_image="ghcr.io/murongg/sublane:$install_version" + install_release="https://github.com/$install_repository/releases/download/v$install_version" + install_image="ghcr.io/alumos/sublane:$install_version" } download_release() { diff --git a/scripts/install.test.mjs b/scripts/install.test.mjs index fd73088b..f6d5aab4 100644 --- a/scripts/install.test.mjs +++ b/scripts/install.test.mjs @@ -170,7 +170,7 @@ function existing(t, runtime = "docker", mode = "", directory = "sublane") { writeFileSync(join(f.target, ".env"), [ "# Operator settings", "COMPOSE_PROJECT_NAME=sublane-synthetic", - "SUBLANE_IMAGE=ghcr.io/murongg/sublane:1.2.2", + "SUBLANE_IMAGE=ghcr.io/alumos/sublane:1.2.2", "SUBLANE_BIND_ADDRESS=127.0.0.1", "SUBLANE_PORT=18080", "SUBLANE_PUBLIC_URL=https://gateway.example.test", @@ -310,7 +310,7 @@ test("updates an existing Docker deployment to the latest release while preservi for (const call of calls) { assert.equal(call.args[call.args.indexOf("--project-name") + 1], "sublane-synthetic"); assert.equal(realpathSync(call.args[call.args.indexOf("--project-directory") + 1]), realpathSync(f.target)); - assert.equal(call.image, "ghcr.io/murongg/sublane:1.2.3"); + assert.equal(call.image, "ghcr.io/alumos/sublane:1.2.3"); assert.equal(call.port, "18080"); assert.equal(call.demo, "true"); assert.equal(call.logLevel, "warn"); @@ -784,7 +784,7 @@ test("installs a pinned release with checksums and waits for healthy Compose sta assert.equal(result.status, 0, result.stderr); assert.match( readFileSync(join(f.target, ".env"), "utf8"), - /SUBLANE_IMAGE=ghcr.io\/murongg\/sublane:0\.1\.0-rc\.1/, + /SUBLANE_IMAGE=ghcr.io\/alumos\/sublane:0\.1\.0-rc\.1/, ); assert.equal( readFileSync(join(f.target, "docker.compose.yaml"), "utf8"), @@ -813,7 +813,7 @@ test("supports piped execution, explicit versions, paths with spaces and custom ); assert.equal(result.status, 0, result.stderr); const env = readFileSync(join(target, ".env"), "utf8"); - assert.match(env, /SUBLANE_IMAGE=ghcr.io\/murongg\/sublane:1\.2\.3-beta\.2/); + assert.match(env, /SUBLANE_IMAGE=ghcr.io\/alumos\/sublane:1\.2\.3-beta\.2/); assert.match(env, /SUBLANE_PORT=18851/); assert.match(env, /COMPOSE_PROJECT_NAME=sublane-[a-z0-9-]+/); assert.match(result.stdout, /127\.0\.0\.1:18851/); @@ -1022,7 +1022,7 @@ test("explicit installer settings take precedence over inherited Compose interpo .filter((call) => call.args.includes("--project-name")); assert.ok(calls.length >= 3); for (const call of calls) { - assert.equal(call.image, "ghcr.io/murongg/sublane:0.1.0-rc.1"); + assert.equal(call.image, "ghcr.io/alumos/sublane:0.1.0-rc.1"); assert.equal(call.port, "8080"); assert.notEqual( call.args[call.args.indexOf("--project-name") + 1], @@ -1065,7 +1065,7 @@ test("prefers the latest stable release without consulting prereleases", (t) => assert.equal(result.status, 0, result.stderr); assert.match( readFileSync(join(f.target, ".env"), "utf8"), - /SUBLANE_IMAGE=ghcr.io\/murongg\/sublane:1\.2\.3\n/, + /SUBLANE_IMAGE=ghcr.io\/alumos\/sublane:1\.2\.3\n/, ); assert.equal( f @@ -1112,7 +1112,7 @@ test("without stable releases, selects the most recently published prerelease ac assert.equal(result.status, 0, result.stderr); assert.match( readFileSync(join(f.target, ".env"), "utf8"), - /SUBLANE_IMAGE=ghcr.io\/murongg\/sublane:2\.0\.0-rc\.2\n/, + /SUBLANE_IMAGE=ghcr.io\/alumos\/sublane:2\.0\.0-rc\.2\n/, ); }); diff --git a/scripts/release.mjs b/scripts/release.mjs index d6de3ddf..9b0d6d0f 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -24,18 +24,26 @@ export function releaseMetadata(tag, repository) { ) { throw new Error("Expected a GitHub owner/repository name."); } + // Fork revisions (X.Y.Z-revised.N) are stable releases of this fork, not prereleases. + const revision = /^revised\.([1-9]\d*)$/.exec(match[4] ?? ""); return { tag, version: tag.slice(1), - prerelease: Boolean(match[4]), + prerelease: Boolean(match[4]) && !revision, image: `ghcr.io/${repository.toLowerCase()}`, }; } +// Stable versions order by (major, minor, patch, revision); a base release has revision 0. +function stableKey(version) { + const [base, revision = "0"] = version.split("-revised."); + return [...base.split(".").map(BigInt), BigInt(revision)]; +} + export function isLatestStable(tag, tags) { const current = releaseMetadata(tag, "owner/repo"); if (current.prerelease) return false; - const version = current.version.split(".").map(BigInt); + const version = stableKey(current.version); for (const candidate of tags) { let other; try { @@ -44,8 +52,8 @@ export function isLatestStable(tag, tags) { continue; } if (other.prerelease) continue; - const parts = other.version.split(".").map(BigInt); - for (let i = 0; i < 3; i++) { + const parts = stableKey(other.version); + for (let i = 0; i < version.length; i++) { if (parts[i] > version[i]) return false; if (parts[i] < version[i]) break; } diff --git a/scripts/release.test.mjs b/scripts/release.test.mjs index 63f74b6a..68a9b46b 100644 --- a/scripts/release.test.mjs +++ b/scripts/release.test.mjs @@ -47,3 +47,17 @@ test("keeps prereleases and older maintenance versions off the stable channel", assert.equal(isLatestStable("v2.0.0-rc.1", ["v1.10.0"]), false); assert.equal(isLatestStable("v0.1.0", ["not-a-version", "v0.1.0"]), true); }); + +test("treats fork revisions as stable and orders them after their base release", () => { + assert.equal(releaseMetadata("v0.19.2-revised.8", "Alumos/SubLane").prerelease, false); + assert.equal(releaseMetadata("v0.19.2-revised.8", "Alumos/SubLane").image, "ghcr.io/alumos/sublane"); + assert.equal(releaseMetadata("v0.19.2-rc.1", "Alumos/SubLane").prerelease, true); + assert.equal(releaseMetadata("v0.19.2-revised.0", "Alumos/SubLane").prerelease, true); + assert.equal( + isLatestStable("v0.19.2-revised.10", ["v0.19.2", "v0.19.2-revised.9", "v0.19.2-revised.10"]), + true, + ); + assert.equal(isLatestStable("v0.19.2-revised.9", ["v0.19.2-revised.10"]), false); + assert.equal(isLatestStable("v0.19.2", ["v0.19.2-revised.1"]), false); + assert.equal(isLatestStable("v0.19.3", ["v0.19.2-revised.99"]), true); +}); diff --git a/scripts/updater.sh b/scripts/updater.sh index 167f8ba5..e132c90e 100644 --- a/scripts/updater.sh +++ b/scripts/updater.sh @@ -26,7 +26,7 @@ valid_job() { jq -e ' type == "object" and keys == ["id","status","updated_at","version"] and (.id | type == "string" and test("^[a-f0-9]{32}$")) and - (.version | type == "string" and test("^(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})$")) and + (.version | type == "string" and test("^(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})(-revised\\.[1-9][0-9]{0,5})?$")) and (.updated_at | type == "number" and . > 0 and floor == .) and (.status == "queued" or .status == "running" or .status == "succeeded" or .status == "failed" or .status == "interrupted") ' "$1" >/dev/null 2>&1 @@ -38,14 +38,16 @@ is_upgrade() { installed=$("$installation/sublane" --version) || return 1 elif [[ -f $installation/.env && ! -L $installation/.env ]]; then installed=$(awk -F= '$1 == "SUBLANE_IMAGE" { count++; value=$2 } END { if(count != 1) exit 1; print value }' "$installation/.env") || return 1 - [[ $installed == ghcr.io/murongg/sublane:* ]] || return 1 - installed=${installed#ghcr.io/murongg/sublane:} + [[ $installed == ghcr.io/alumos/sublane:* ]] || return 1 + installed=${installed#ghcr.io/alumos/sublane:} else return 1 fi + # Fork revisions order after their base release: (major, minor, patch, revision). jq -ne --arg current "$installed" --arg target "$request_version" ' - ($current | test("^(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})$")) and - (($target | split(".") | map(tonumber)) > ($current | split(".") | map(tonumber))) + def key: split("-revised.") | (.[0] | split(".") | map(tonumber)) + [(.[1] // "0") | tonumber]; + ($current | test("^(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})(-revised\\.[1-9][0-9]{0,5})?$")) and + (($target | key) > ($current | key)) ' >/dev/null } diff --git a/scripts/updater.test.mjs b/scripts/updater.test.mjs index bb7f00ad..6c2e1b2d 100644 --- a/scripts/updater.test.mjs +++ b/scripts/updater.test.mjs @@ -17,12 +17,12 @@ async function until(predicate,child){ } throw new Error("Synthetic worker did not publish its state"); } -function fixture(t,fail=false){ +function fixture(t,fail=false,installed="1.2.0"){ const dir=realpathSync(mkdtempSync(join(tmpdir(),"sublane-updater-"))); const privateDir=join(dir,".updater");mkdirSync(privateDir,{mode:0o700}); const mailbox=join(privateDir,"mailbox");mkdirSync(mailbox,{mode:0o777}); const calls=join(dir,"args"); - writeFileSync(join(dir,".env"),"SUBLANE_IMAGE=ghcr.io/murongg/sublane:1.2.0\n"); + writeFileSync(join(dir,".env"),`SUBLANE_IMAGE=ghcr.io/alumos/sublane:${installed}\n`); writeFileSync(join(privateDir,"install.sh"),`#!/bin/bash\nprintf '%s\\n' "$@" > "$UPDATE_TEST_ARGS"\necho 'Synthetic private diagnostic'\nexit ${fail?1:0}\n`,{mode:0o700}); const child=spawn("bash",[script,"--run",dir],{env:{...process.env,UPDATE_TEST_ARGS:calls},detached:true,stdio:"ignore"}); t.after(async()=>{ @@ -84,3 +84,13 @@ test("the host worker independently refuses downgrades and repeated installation assert.equal(existsSync(f.calls),false); assert.match(readFileSync(join(f.privateDir,"last.log"),"utf8"),/newer stable version/); }); +test("fork revisions upgrade within a base release and never repeat the installed revision",async t=>{ + const f=fixture(t,false,"0.19.2-revised.7"); + await until(()=>existsSync(join(f.mailbox,"heartbeat")),f.child); + f.request({...job,version:"0.19.2-revised.7"}); + await until(()=>f.status()?.status==="failed"&&!existsSync(join(f.mailbox,"request.json")),f.child); + assert.equal(existsSync(f.calls),false); + f.request({...job,id:"b".repeat(32),version:"0.19.2-revised.8"}); + await until(()=>f.status()?.status==="succeeded"&&f.status()?.id==="b".repeat(32),f.child); + assert.deepEqual(readFileSync(f.calls,"utf8").trim().split("\n"),["--update","--dir",f.dir,"--version","0.19.2-revised.8","--non-interactive"]); +}); diff --git a/web/src/components/DeleteGroup.tsx b/web/src/components/ConfirmDelete.tsx similarity index 78% rename from web/src/components/DeleteGroup.tsx rename to web/src/components/ConfirmDelete.tsx index 47d704c9..adf23380 100644 --- a/web/src/components/DeleteGroup.tsx +++ b/web/src/components/ConfirmDelete.tsx @@ -1,7 +1,6 @@ import { useMutation } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { LoaderCircle } from 'lucide-react' -import { deleteGroup, groupDeleteErrorKey, type Group } from '@/lib/groups' import { Button } from './ui/Button' import { Dialog, @@ -12,18 +11,27 @@ import { DialogTitle, } from './ui/Dialog' -export function DeleteGroup({ - group, +// Destructive confirmation shared by resources that delete with one request and no extra input. +export function ConfirmDelete({ + title, + description, + action, + remove, + errorText, onClose, onDeleted, }: { - group: Pick + title: string + description: string + action: string + remove: () => Promise + errorText: (error: Error) => string onClose: () => void onDeleted: () => Promise }) { const { t } = useTranslation() const mutation = useMutation({ - mutationFn: () => deleteGroup(group.id), + mutationFn: remove, onSuccess: async () => { await onDeleted() onClose() @@ -47,14 +55,12 @@ export function DeleteGroup({ }} > - {t('deleteGroupTitle')} - - {t('deleteGroupDescription', { name: group.name })} - + {title} + {description} {mutation.isError && (

- {t(groupDeleteErrorKey(mutation.error))} + {errorText(mutation.error)}

)} @@ -76,7 +82,7 @@ export function DeleteGroup({ aria-hidden="true" /> )} - {t('deleteGroup')} + {action} diff --git a/web/src/components/DeleteAllocation.tsx b/web/src/components/DeleteAllocation.tsx deleted file mode 100644 index d5937557..00000000 --- a/web/src/components/DeleteAllocation.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useMutation } from '@tanstack/react-query' -import { useTranslation } from 'react-i18next' -import { LoaderCircle } from 'lucide-react' -import { - allocationDeleteErrorKey, - deleteScheme, - type Scheme, -} from '@/lib/allocations' -import { Button } from './ui/Button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from './ui/Dialog' - -export function DeleteAllocation({ - scheme, - onClose, - onDeleted, -}: { - scheme: Pick - onClose: () => void - onDeleted: () => Promise -}) { - const { t } = useTranslation() - const mutation = useMutation({ - mutationFn: () => deleteScheme(scheme.id), - onSuccess: async () => { - await onDeleted() - onClose() - }, - }) - return ( - { - if (!open && !mutation.isPending) onClose() - }} - > - { - if (mutation.isPending) event.preventDefault() - }} - onEscapeKeyDown={(event) => { - if (mutation.isPending) event.preventDefault() - }} - > - - {t('allocationDeleteTitle')} - - {t('allocationDeleteDescription', { name: scheme.name })} - - - {mutation.isError && ( -

- {t(allocationDeleteErrorKey(mutation.error))} -

- )} - - - - -
-
- ) -} diff --git a/web/src/lib/proxies.ts b/web/src/lib/proxies.ts index 9d3a3201..bdd6aeec 100644 --- a/web/src/lib/proxies.ts +++ b/web/src/lib/proxies.ts @@ -123,11 +123,7 @@ const ipReportSchema = z.object({ country: z.string().max(64).default(''), region: z.string().max(64).default(''), city: z.string().max(64).default(''), - rdns: z.string().max(253).default(''), - abuser_level: z.string().max(32).default(''), verdict: z.string().max(48).default(''), - threats: z.array(z.string().max(48)).max(8).default([]), - fetched_at: z.number().int().positive(), }) export type IPReport = z.infer diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index 4760e882..26935739 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "لم يبلغ عنها من قبل", "apiUpstreamProxyHint": "الاكتشافات النموذجية والتوليد يستعملان مسار شبكة الخادم المختارة.", "connectAPI": "Connect API", - "replaceAPIKey": "يستعاض عن عبارة " API المفتاح", "upstreamBaseURL": "القاعدة URL", "upstreamAPIKey": "في الأعلى API المفتاح", "apiUpstreamDescription": "اختر منصة أو خدمة مخصصة وأدخل مفتاح API وعنوان URL الأساسي.", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "فشل الإرتباط", "proxyLookupUnavailable": "مراقبة الموقع غير متاحة", "proxyLookupRateLimited": "معدل البحث عن المواقع محدود", - "proxyExitIP": "مخرج IP: {{ip}}", - "proxyLocation": "الموقع: {{location}}", "proxyLocationUnknown": "غير معروف", "proxyLatency": "{{latency}} ms", "proxyChanged": "العميل تغير أثناء الإختبار، أعد تشغيله ثانيةً", @@ -1528,7 +1525,7 @@ "keyGroupUnavailableHint": "لم تعد المجموعة أو حصة الاستخدام المحددة متاحة. حدّث الخيارات أو تواصل مع مسؤول.", "allocationExistingKeysHint": "عند الحفظ، تُربط المفاتيح الحالية غير الملغاة للأعضاء المحددين في هذه المجموعة بحصة الاستخدام. تبقى المفاتيح وإعدادات العميل كما هي. ستفقد المفاتيح الحالية للأعضاء غير المشمولين بالحصة الوصول إلى هذه المجموعة.", "updatesTitle": "تحديثات البرنامج", - "updatesDescription": "تحقق من إصدارات SubLane الرسمية وحدّث هذه النسخة. يمكن لمالك المنصة فقط تثبيت التحديثات.", + "updatesDescription": "تحقق من إصدارات هذا الفرع من SubLane (Alumos/SubLane) وحدّث هذه النسخة. يمكن لمالك المنصة فقط تثبيت التحديثات.", "updateCurrent": "الإصدار الحالي", "updateLatest": "أحدث إصدار مستقر", "updateCheck": "التحقق من التحديثات", diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 0353be17..a0ff4440 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "Nicht vorgelagert gemeldet", "apiUpstreamProxyHint": "Modellerkennung und -generierung verwenden die ausgewählte Server-Netzwerkroute.", "connectAPI": "Anschluss API", - "replaceAPIKey": "Ersetzen API Schlüssel", "upstreamBaseURL": "Basis-URL", "upstreamAPIKey": "Upstream API Schlüssel", "apiUpstreamDescription": "Verbinde eine Plattform oder einen eigenen Dienst mit API-Schlüssel und Basis-URL.", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "Verbindung fehlgeschlagen", "proxyLookupUnavailable": "Standortsuche nicht verfügbar", "proxyLookupRateLimited": "Standort-Lookup Rate begrenzt", - "proxyExitIP": "Ausfahrts-IP: {{ip}}", - "proxyLocation": "Standort: {{location}}", "proxyLocationUnknown": "Unbekannt", "proxyLatency": "{{latency}} ms", "proxyChanged": "Der Proxy änderte sich während des Tests. Führen Sie ihn erneut aus.", @@ -1508,7 +1505,7 @@ "keyGroupUnavailableHint": "Die gewählte Gruppe oder das Kontingent ist nicht mehr verfügbar. Aktualisieren Sie die Auswahl oder kontaktieren Sie einen Administrator.", "allocationExistingKeysHint": "Beim Speichern werden die vorhandenen, nicht widerrufenen Schlüssel der ausgewählten Mitglieder in dieser Gruppe mit dem Kontingent verknüpft. Schlüssel und Client-Einstellungen bleiben gleich. Vorhandene Schlüssel nicht eingeschlossener Mitglieder verlieren den Zugriff auf diese Gruppe.", "updatesTitle": "Softwareupdates", - "updatesDescription": "Prüfen Sie offizielle SubLane-Versionen und aktualisieren Sie diese Instanz. Nur der Plattforminhaber darf Updates installieren.", + "updatesDescription": "Prüfen Sie Releases dieses SubLane-Forks (Alumos/SubLane) und aktualisieren Sie diese Instanz. Nur der Plattformbesitzer kann Updates installieren.", "updateCurrent": "Aktuelle Version", "updateLatest": "Neueste stabile Version", "updateCheck": "Nach Updates suchen", diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index b953129e..2ad7c53f 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1,7 +1,7 @@ export const en = { updatesTitle: 'Software updates', updatesDescription: - 'Check official SubLane releases and update this instance. Only the platform owner can install updates.', + 'Check releases of this SubLane fork (Alumos/SubLane) and update this instance. Only the platform owner can install updates.', updateCurrent: 'Current version', updateLatest: 'Latest stable release', updateCheck: 'Check for updates', @@ -1145,7 +1145,6 @@ export const en = { apiUpstreamProxyHint: 'Model discovery and generation use the selected server network route.', connectAPI: 'Connect API', - replaceAPIKey: 'Replace API key', upstreamBaseURL: 'Base URL', upstreamAPIKey: 'Upstream API key', apiUpstreamDescription: @@ -1775,8 +1774,6 @@ export const en = { proxyConnectionFailed: 'Connection failed', proxyLookupUnavailable: 'Location lookup unavailable', proxyLookupRateLimited: 'Location lookup rate limited', - proxyExitIP: 'Exit IP: {{ip}}', - proxyLocation: 'Location: {{location}}', proxyLocationUnknown: 'Unknown', proxyLatency: '{{latency}} ms', proxyChanged: 'The proxy changed during the test. Run it again.', diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 1a717228..6a739f0a 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "No informado por upstream", "apiUpstreamProxyHint": "El descubrimiento y la generación de modelos utilizan la ruta de red del servidor seleccionada.", "connectAPI": "Conectar API", - "replaceAPIKey": "Reemplazar la clave API", "upstreamBaseURL": "URL base", "upstreamAPIKey": "Tecla aguas arriba API", "apiUpstreamDescription": "Conecta una plataforma o un servicio personalizado con su clave API y URL base.", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "La conexión falló", "proxyLookupUnavailable": "Búsqueda de ubicación no disponible", "proxyLookupRateLimited": "Tasa de búsqueda de ubicación limitada", - "proxyExitIP": "IP de salida: {{ip}}", - "proxyLocation": "Ubicación: {{location}}", "proxyLocationUnknown": "Desconocido", "proxyLatency": "{{latency}} ms", "proxyChanged": "El proxy cambió durante la prueba. Ejecútalo de nuevo.", @@ -1513,7 +1510,7 @@ "keyGroupUnavailableHint": "El grupo o la asignación seleccionados ya no están disponibles. Actualice las opciones o contacte con un administrador.", "allocationExistingKeysHint": "Al guardar, las claves existentes no revocadas de los miembros seleccionados en este grupo se vinculan a la asignación. Las claves y la configuración del cliente no cambian. Las claves existentes de los miembros excluidos perderán el acceso a este grupo.", "updatesTitle": "Actualizaciones de software", - "updatesDescription": "Consulta las versiones oficiales de SubLane y actualiza esta instancia. Solo el propietario de la plataforma puede instalar actualizaciones.", + "updatesDescription": "Comprueba las versiones de esta bifurcación de SubLane (Alumos/SubLane) y actualiza esta instancia. Solo el propietario de la plataforma puede instalar actualizaciones.", "updateCurrent": "Versión actual", "updateLatest": "Última versión estable", "updateCheck": "Buscar actualizaciones", diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index 2c08d708..8d8086f8 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "Non signalé en amont", "apiUpstreamProxyHint": "La découverte et la génération de modèles utilisent la route réseau de serveur sélectionnée.", "connectAPI": "Connexion API", - "replaceAPIKey": "Remplacer API clé", "upstreamBaseURL": "URL de base", "upstreamAPIKey": "En amont API clé", "apiUpstreamDescription": "Connectez une plateforme ou un service personnalisé avec sa clé API et son URL de base.", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "La connexion a échoué", "proxyLookupUnavailable": "Recherche de localisation non disponible", "proxyLookupRateLimited": "Limité du taux de recherche d'emplacement", - "proxyExitIP": "Sortie IP & #160;: {{ip}}", - "proxyLocation": "Lieu: {{location}}", "proxyLocationUnknown": "Inconnu", "proxyLatency": "{{latency}} ms", "proxyChanged": "Le mandataire a changé pendant le test.", @@ -1513,7 +1510,7 @@ "keyGroupUnavailableHint": "Le groupe ou l’allocation sélectionné n’est plus disponible. Actualisez les choix ou contactez un administrateur.", "allocationExistingKeysHint": "L’enregistrement associe à cette allocation les clés existantes non révoquées des membres sélectionnés dans ce groupe. Les clés et les paramètres du client restent inchangés. Les clés existantes des membres exclus de l’allocation perdront l’accès à ce groupe.", "updatesTitle": "Mises à jour logicielles", - "updatesDescription": "Vérifiez les versions officielles de SubLane et mettez à jour cette instance. Seul le propriétaire de la plateforme peut installer des mises à jour.", + "updatesDescription": "Vérifiez les versions de ce fork de SubLane (Alumos/SubLane) et mettez à jour cette instance. Seul le propriétaire de la plateforme peut installer les mises à jour.", "updateCurrent": "Version actuelle", "updateLatest": "Dernière version stable", "updateCheck": "Rechercher des mises à jour", diff --git a/web/src/locales/ja.json b/web/src/locales/ja.json index d2336978..701b0acb 100644 --- a/web/src/locales/ja.json +++ b/web/src/locales/ja.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "上流から報告されていない", "apiUpstreamProxyHint": "モデルの検出と生成には、選択したサーバー ネットワーク ルートが使用されます。", "connectAPI": "API に接続します", - "replaceAPIKey": "APIキーを置き換えます", "upstreamBaseURL": "ベースURL", "upstreamAPIKey": "アップストリーム API キー", "apiUpstreamDescription": "プラットフォームまたはカスタムサービスを選び、API キーとベース URL を入力します。", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "接続に失敗しました", "proxyLookupUnavailable": "位置情報の検索は利用できません", "proxyLookupRateLimited": "位置検索レートが制限されている", - "proxyExitIP": "終了IP: {{ip}}", - "proxyLocation": "場所: {{location}}", "proxyLocationUnknown": "未知", "proxyLatency": "{{latency}} ミリ秒", "proxyChanged": "テスト中にプロキシが変更されました。もう一度実行してください。", @@ -1508,7 +1505,7 @@ "keyGroupUnavailableHint": "選択したグループまたは利用枠は利用できなくなりました。選択肢を更新するか、管理者に連絡してください。", "allocationExistingKeysHint": "保存すると、選択したメンバーがこのグループで持つ未失効の既存キーを利用枠に関連付けます。キーとクライアント設定は変わりません。利用枠に含まれないメンバーの既存キーは、このグループを利用できなくなります。", "updatesTitle": "ソフトウェア更新", - "updatesDescription": "SubLane の公式リリースを確認し、このインスタンスを更新します。インストールはプラットフォーム所有者のみ行えます。", + "updatesDescription": "この SubLane フォーク(Alumos/SubLane)のリリースを確認し、このインスタンスを更新します。更新をインストールできるのはプラットフォーム所有者のみです。", "updateCurrent": "現在のバージョン", "updateLatest": "最新の安定版", "updateCheck": "更新を確認", diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json index db8cd86e..991ac9ae 100644 --- a/web/src/locales/ko.json +++ b/web/src/locales/ko.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "업스트림에서 보고되지 않음", "apiUpstreamProxyHint": "모델 검색 및 생성은 선택된 서버 네트워크 경로를 사용합니다.", "connectAPI": "연결 API", - "replaceAPIKey": "API 키 교체", "upstreamBaseURL": "기본 URL", "upstreamAPIKey": "업스트림 API 키", "apiUpstreamDescription": "플랫폼 또는 사용자 지정 서비스를 선택하고 API 키와 기본 URL을 입력하세요.", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "연결 실패", "proxyLookupUnavailable": "위치 조회 불가", "proxyLookupRateLimited": "위치 조회 속도 제한됨", - "proxyExitIP": "종료 IP: {{ip}}", - "proxyLocation": "위치: {{location}}", "proxyLocationUnknown": "알려지지 않은", "proxyLatency": "{{latency}} ms", "proxyChanged": "테스트 중에 프록시가 변경되었습니다. 다시 실행하세요.", @@ -1508,7 +1505,7 @@ "keyGroupUnavailableHint": "선택한 그룹 또는 사용 한도를 더 이상 사용할 수 없습니다. 선택 목록을 새로 고치거나 관리자에게 문의하세요.", "allocationExistingKeysHint": "저장하면 선택한 구성원이 이 그룹에 보유한 기존의 취소되지 않은 키를 사용 한도에 연결합니다. 키와 클라이언트 설정은 유지됩니다. 사용 한도에 포함되지 않은 구성원의 기존 키는 이 그룹에 접근할 수 없게 됩니다.", "updatesTitle": "소프트웨어 업데이트", - "updatesDescription": "SubLane 공식 릴리스를 확인하고 이 인스턴스를 업데이트합니다. 플랫폼 소유자만 설치할 수 있습니다.", + "updatesDescription": "이 SubLane 포크(Alumos/SubLane)의 릴리스를 확인하고 이 인스턴스를 업데이트합니다. 플랫폼 소유자만 업데이트를 설치할 수 있습니다.", "updateCurrent": "현재 버전", "updateLatest": "최신 안정 버전", "updateCheck": "업데이트 확인", diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json index 8a8556dc..15816d14 100644 --- a/web/src/locales/pt.json +++ b/web/src/locales/pt.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "Não reportado a montante", "apiUpstreamProxyHint": "A descoberta do modelo e a geração usam a rota de rede selecionada do servidor.", "connectAPI": "Conectar API", - "replaceAPIKey": "Substituir API chave", "upstreamBaseURL": "URL base", "upstreamAPIKey": "A montante API chave", "apiUpstreamDescription": "Conecte uma plataforma ou serviço personalizado com a chave de API e a URL base.", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "A ligação falhou", "proxyLookupUnavailable": "Pesquisa de localização indisponível", "proxyLookupRateLimited": "Taxa de procura de localização limitada", - "proxyExitIP": "Sair do IP: {{ip}}", - "proxyLocation": "Localização: {{location}}", "proxyLocationUnknown": "Desconhecido", "proxyLatency": "{{latency}} ms", "proxyChanged": "O proxy mudou durante o teste.", @@ -1513,7 +1510,7 @@ "keyGroupUnavailableHint": "O grupo ou a alocação selecionados não estão mais disponíveis. Atualize as opções ou contate um administrador.", "allocationExistingKeysHint": "Ao salvar, as chaves existentes não revogadas dos membros selecionados neste grupo são vinculadas à alocação. As chaves e as configurações do cliente permanecem iguais. As chaves existentes dos membros não incluídos perderão o acesso a este grupo.", "updatesTitle": "Atualizações de software", - "updatesDescription": "Verifique as versões oficiais do SubLane e atualize esta instância. Apenas o proprietário da plataforma pode instalar atualizações.", + "updatesDescription": "Verifique as versões deste fork do SubLane (Alumos/SubLane) e atualize esta instância. Somente o proprietário da plataforma pode instalar atualizações.", "updateCurrent": "Versão atual", "updateLatest": "Última versão estável", "updateCheck": "Verificar atualizações", diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json index 85a7dcba..7db42a2e 100644 --- a/web/src/locales/ru.json +++ b/web/src/locales/ru.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "Об этом не сообщает Upstream", "apiUpstreamProxyHint": "Обнаружение модели и генерация используют выбранный маршрут серверной сети.", "connectAPI": "Соединить API", - "replaceAPIKey": "Замена API ключ", "upstreamBaseURL": "Базовый URL", "upstreamAPIKey": "Вверх по течению API ключ", "apiUpstreamDescription": "Подключите платформу или другой сервис с помощью ключа API и базового URL.", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "Связь провалилась", "proxyLookupUnavailable": "Поиск местоположения недоступен", "proxyLookupRateLimited": "Скорость поиска местоположения ограничена", - "proxyExitIP": "Выход IP: {{ip}}", - "proxyLocation": "Местонахождение: {{location}}", "proxyLocationUnknown": "неизвестный", "proxyLatency": "{{latency}} мс", "proxyChanged": "Прокси-сервер изменился во время теста.", @@ -1518,7 +1515,7 @@ "keyGroupUnavailableHint": "Выбранная группа или лимит больше недоступны. Обновите список или обратитесь к администратору.", "allocationExistingKeysHint": "При сохранении существующие неотозванные ключи выбранных участников в этой группе привязываются к лимиту. Ключи и настройки клиента не меняются. Существующие ключи участников, не включённых в лимит, потеряют доступ к этой группе.", "updatesTitle": "Обновления ПО", - "updatesDescription": "Проверяйте официальные выпуски SubLane и обновляйте этот экземпляр. Установка доступна только владельцу платформы.", + "updatesDescription": "Проверяйте выпуски этого форка SubLane (Alumos/SubLane) и обновляйте этот экземпляр. Устанавливать обновления может только владелец платформы.", "updateCurrent": "Текущая версия", "updateLatest": "Последняя стабильная версия", "updateCheck": "Проверить обновления", diff --git a/web/src/locales/zh-TW.json b/web/src/locales/zh-TW.json index c433ff02..904fc079 100644 --- a/web/src/locales/zh-TW.json +++ b/web/src/locales/zh-TW.json @@ -736,7 +736,6 @@ "upstreamKeyExpiryUnknown": "上游未提供", "apiUpstreamProxyHint": "模型發現和產生請求使用所選的服務端網路出口。", "connectAPI": "接入 API", - "replaceAPIKey": "更換 API 金鑰", "upstreamBaseURL": "Base URL", "upstreamAPIKey": "上游 API 金鑰", "apiUpstreamDescription": "選擇平台或自訂服務,填寫 API 金鑰和 Base URL。", @@ -1242,8 +1241,6 @@ "proxyConnectionFailed": "連線失敗", "proxyLookupUnavailable": "位置查詢不可用", "proxyLookupRateLimited": "位置查詢受限", - "proxyExitIP": "出口 IP:{{ip}}", - "proxyLocation": "位置:{{location}}", "proxyLocationUnknown": "未知", "proxyLatency": "{{latency}} 毫秒", "proxyChanged": "檢測期間代理程式已變更,請重新測試。", @@ -1508,7 +1505,7 @@ "keyGroupUnavailableHint": "所選資源群組或用量分配已無法使用,請重新整理選項或聯絡管理員。", "allocationExistingKeysHint": "儲存時,會將所選成員在此資源群組中現有且未撤銷的金鑰關聯至本方案,金鑰值和用戶端設定保持不變。未加入方案的成員,其現有金鑰將無法繼續使用此資源群組。", "updatesTitle": "軟體更新", - "updatesDescription": "檢查 SubLane 官方版本並更新目前執行個體。僅平台擁有者可安裝更新。", + "updatesDescription": "檢查本 SubLane 修改版(Alumos/SubLane)的發佈版本並更新目前實例。僅平台擁有者可以安裝更新。", "updateCurrent": "目前版本", "updateLatest": "最新正式版", "updateCheck": "檢查更新", diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index bdaeada1..f1d380a1 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -3,7 +3,7 @@ import type { en } from './en' export const zh: Record = { updatesTitle: '软件更新', updatesDescription: - '检查 SubLane 官方版本并更新当前实例。仅平台所有者可以安装更新。', + '检查本 SubLane 修改版(Alumos/SubLane)的发布版本并更新当前实例。仅平台所有者可以安装更新。', updateCurrent: '当前版本', updateLatest: '最新正式版', updateCheck: '检查更新', @@ -1044,7 +1044,6 @@ export const zh: Record = { upstreamKeyExpiryUnknown: '上游未提供', apiUpstreamProxyHint: '模型发现和生成请求使用所选的服务端网络出口。', connectAPI: '接入 API', - replaceAPIKey: '更换 API 密钥', upstreamBaseURL: 'Base URL', upstreamAPIKey: '上游 API 密钥', apiUpstreamDescription: '选择平台或自定义服务,填写 API 密钥和 Base URL。', @@ -1616,8 +1615,6 @@ export const zh: Record = { proxyConnectionFailed: '连接失败', proxyLookupUnavailable: '位置查询不可用', proxyLookupRateLimited: '位置查询受限', - proxyExitIP: '出口 IP:{{ip}}', - proxyLocation: '位置:{{location}}', proxyLocationUnknown: '未知', proxyLatency: '{{latency}} 毫秒', proxyChanged: '检测期间代理已变更,请重新测试。', diff --git a/web/src/pages/Allocations.tsx b/web/src/pages/Allocations.tsx index 49e6f1b0..5a0e2ca2 100644 --- a/web/src/pages/Allocations.tsx +++ b/web/src/pages/Allocations.tsx @@ -9,6 +9,8 @@ import { saveScheme, previewInitial, setSchemeEnabled, + deleteScheme, + allocationDeleteErrorKey, modeLabels, allocationErrorKey, type Scheme, @@ -19,7 +21,7 @@ import { SchemeForm } from '@/components/SchemeForm' import { AllocationPriceWarning } from '@/components/AllocationPriceWarning' import { AllocationReport } from '@/components/AllocationReport' import { AllocationSchedule } from '@/components/AllocationSchedule' -import { DeleteAllocation } from '@/components/DeleteAllocation' +import { ConfirmDelete } from '@/components/ConfirmDelete' import { Button } from '@/components/ui/Button' import { Status } from '@/components/Status' import { formatInstanceDate, useTimeZone } from '@/lib/timezone' @@ -415,8 +417,14 @@ export function Allocations() { )} {removing && ( - deleteScheme(removing.id)} + errorText={(error) => t(allocationDeleteErrorKey(error))} onClose={() => setRemoving(null)} onDeleted={async () => { await Promise.all([ diff --git a/web/src/pages/Groups.tsx b/web/src/pages/Groups.tsx index 38250e7d..d997c39e 100644 --- a/web/src/pages/Groups.tsx +++ b/web/src/pages/Groups.tsx @@ -2,9 +2,14 @@ import { useState } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { FolderClosed, LoaderCircle, Plus, Trash2 } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { groupOptions, type Group } from '@/lib/groups' +import { + deleteGroup, + groupDeleteErrorKey, + groupOptions, + type Group, +} from '@/lib/groups' import { CatalogDialog } from '@/components/CatalogDialog' -import { DeleteGroup } from '@/components/DeleteGroup' +import { ConfirmDelete } from '@/components/ConfirmDelete' import { GroupEditor } from '@/components/GroupEditor' import { Button } from '@/components/ui/Button' import { Status } from '@/components/Status' @@ -167,8 +172,12 @@ export function Groups() { /> )} {removing && ( - deleteGroup(removing.id)} + errorText={(error) => t(groupDeleteErrorKey(error))} onClose={() => setRemoving(null)} onDeleted={saved} /> diff --git a/web/src/pages/Proxies.test.tsx b/web/src/pages/Proxies.test.tsx index a445d88b..e8497016 100644 --- a/web/src/pages/Proxies.test.tsx +++ b/web/src/pages/Proxies.test.tsx @@ -277,11 +277,7 @@ it('imports multiple proxies and displays a checked exit location', async () => country: 'United States', region: 'Test region', city: 'Test city', - rdns: '', - abuser_level: 'safe', verdict: '', - threats: [], - fetched_at: 1900000000, }), ) if (url === '/api/proxies') return Promise.resolve(response({ proxies }))