From c1172d7714b93cad4ab5d2d86f18f55be3572cdb Mon Sep 17 00:00:00 2001 From: phamnazage-jpg Date: Wed, 27 May 2026 07:56:24 +0800 Subject: [PATCH] fix: harden deepseek official remote43 import closure --- internal/provision/import_service.go | 24 +++-- internal/provision/import_service_test.go | 96 ++++++++++++++++++- internal/store/sqlite/db.go | 5 + internal/store/sqlite/db_test.go | 18 ++++ packs/openai-cn-pack/README.md | 4 + packs/openai-cn-pack/checksums.txt | 25 ++--- packs/openai-cn-pack/pack.json | 2 +- .../providers/deepseek-chat-official.json | 1 + .../providers/deepseek-reasoner-official.json | 1 + scripts/import_remote43_provider.sh | 14 +-- scripts/remote43_patched_stack_lib.sh | 7 +- scripts/test_real_host_scripts.sh | 4 + 12 files changed, 172 insertions(+), 29 deletions(-) diff --git a/internal/provision/import_service.go b/internal/provision/import_service.go index 4ec635a2..25ce06df 100644 --- a/internal/provision/import_service.go +++ b/internal/provision/import_service.go @@ -128,6 +128,9 @@ func isAdvisoryAccountProbeFailure(probe sub2api.ProbeResult) bool { if strings.Contains(message, "api returned 403: forbidden") { return true } + if message == "api returned 404:" { + return true + } if !strings.Contains(message, "responses api") { return false @@ -226,6 +229,11 @@ func (s *ImportService) Import(ctx context.Context, req ImportRequest) (report I return report, fmt.Errorf("batch create accounts: %w", err) } rollback.AddAccounts(accounts) + if shouldPreemptivelyDisableOpenAIResponses(req.Provider) { + if err := access.RepairOpenAIResponsesCapability(ctx, s.host, accountRefIDs(accounts)); err != nil { + return failOrDegrade(report, req.Mode, fmt.Errorf("preemptively repair openai responses capability: %w", err)) + } + } for _, account := range accounts { probe, err := s.host.TestAccount(ctx, account.ID, req.Provider.SmokeTestModel) if err != nil { @@ -256,12 +264,6 @@ func (s *ImportService) Import(ctx context.Context, req ImportRequest) (report I report.AccessStatus = AccessStatusBroken return report, fmt.Errorf("strict import failed: %d account(s) did not pass smoke validation", failedAccounts) } - if shouldPreemptivelyDisableOpenAIResponses(req.Provider) { - if err := access.RepairOpenAIResponsesCapability(ctx, s.host, importedAccountIDs(report.Accounts)); err != nil { - return failOrDegrade(report, req.Mode, fmt.Errorf("preemptively repair openai responses capability: %w", err)) - } - } - closureService := access.NewService(s.host) gateway, err := closureService.Close(ctx, access.ClosureRequest{ Mode: req.Access.Mode, @@ -344,6 +346,16 @@ func importedAccountIDs(accounts []AccountImportResult) []string { return ids } +func accountRefIDs(accounts []sub2api.AccountRef) []string { + ids := make([]string, 0, len(accounts)) + for _, account := range accounts { + if trimmed := strings.TrimSpace(account.ID); trimmed != "" { + ids = append(ids, trimmed) + } + } + return ids +} + func importedAccountsSuspectResponsesCapabilityMismatch(accounts []AccountImportResult) bool { for _, account := range accounts { if !account.SmokeModelSeen { diff --git a/internal/provision/import_service_test.go b/internal/provision/import_service_test.go index 75f71057..7e12e3ad 100644 --- a/internal/provision/import_service_test.go +++ b/internal/provision/import_service_test.go @@ -291,6 +291,53 @@ func TestImportServiceTreatsForbiddenProbeRaceAsAdvisoryWhenGatewaySucceeds(t *t } } +func TestImportServiceTreatsBare404ProbeAsAdvisoryWhenGatewaySucceeds(t *testing.T) { + host := &fakeHostAdapter{ + batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}}, + testResults: map[string]sub2api.ProbeResult{ + "account_1": { + OK: false, + Status: "failed", + Message: "API returned 404:", + }, + }, + models: map[string][]sub2api.AccountModel{ + "account_1": {{ID: "deepseek-chat"}}, + }, + gatewayResult: sub2api.GatewayAccessResult{ + OK: true, + StatusCode: 200, + HasExpectedModel: true, + Models: []string{"deepseek-chat"}, + CompletionOK: true, + CompletionStatus: 200, + CompletionType: "application/json", + }, + } + + report, err := NewImportService(host).Import(context.Background(), ImportRequest{ + Provider: sampleProviderManifest(), + Mode: ImportModePartial, + Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, + Keys: []string{"key-1"}, + }) + if err != nil { + t.Fatalf("Import() error = %v", err) + } + if report.BatchStatus != BatchStatusSucceeded { + t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded) + } + if report.ProviderStatus != ProviderStatusActive { + t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive) + } + if report.AccessStatus != AccessStatusSelfServiceReady { + t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSelfServiceReady) + } + if got := report.Accounts[0].ValidationStatus(); got != AccountStatusWarning { + t.Fatalf("ValidationStatus = %q, want %q", got, AccountStatusWarning) + } +} + func TestImportServiceRetriesTransientGatewayCompletionFailure(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}}, @@ -596,6 +643,52 @@ func sampleChatOnlyProviderManifest() pack.ProviderManifest { return provider } +func TestImportServicePreemptiveResponsesRepairHappensBeforeAccountProbe(t *testing.T) { + host := &fakeHostAdapter{ + batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}}, + testResults: map[string]sub2api.ProbeResult{ + "account_1": {OK: true, Status: "passed"}, + }, + models: map[string][]sub2api.AccountModel{ + "account_1": {{ID: "kimi-k2.6"}}, + }, + gatewayResult: sub2api.GatewayAccessResult{ + OK: true, + StatusCode: 200, + HasExpectedModel: true, + Models: []string{"kimi-k2.6"}, + CompletionOK: true, + CompletionStatus: 200, + }, + } + + report, err := NewImportService(host).Import(context.Background(), ImportRequest{ + Provider: sampleChatOnlyProviderManifest(), + Mode: ImportModePartial, + Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, + Keys: []string{"key-1"}, + }) + if err != nil { + t.Fatalf("Import() error = %v", err) + } + if report.BatchStatus != BatchStatusSucceeded { + t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded) + } + wantSequence := []string{"disable_responses", "clear_temp_unschedulable", "testAccount:account_1", "gateway", "completion"} + if !reflect.DeepEqual(host.callSequence, wantSequence) { + t.Fatalf("callSequence = %v, want %v", host.callSequence, wantSequence) + } + if host.disableResponsesCalls != 1 { + t.Fatalf("disable responses calls = %d, want 1", host.disableResponsesCalls) + } + if len(host.disabledResponsesAccountIDs) != 1 || host.disabledResponsesAccountIDs[0] != "account_1" { + t.Fatalf("disabled responses account ids = %v, want [account_1]", host.disabledResponsesAccountIDs) + } + if host.clearTempUnschedulableCalls != 1 { + t.Fatalf("clear temp unschedulable calls = %d, want 1", host.clearTempUnschedulableCalls) + } +} + func TestImportReconcilesExistingChannelConfiguration(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}}, @@ -666,7 +759,7 @@ func TestImportDeletesExistingProviderAccountsBeforeGatewayClosure(t *testing.T) if !reflect.DeepEqual(host.deletedResources, wantDeleted) { t.Fatalf("deleted resources = %#v, want %#v", host.deletedResources, wantDeleted) } - if !reflect.DeepEqual(host.callSequence, []string{"deleteAccount:account_old_2", "deleteAccount:account_old_1", "gateway", "completion"}) { + if !reflect.DeepEqual(host.callSequence, []string{"testAccount:account_new_1", "deleteAccount:account_old_2", "deleteAccount:account_old_1", "gateway", "completion"}) { t.Fatalf("call sequence = %#v, want stale-account cleanup before gateway probe", host.callSequence) } } @@ -810,6 +903,7 @@ func (f *fakeHostAdapter) DeleteAccount(_ context.Context, accountID string) err return nil } func (f *fakeHostAdapter) TestAccount(_ context.Context, accountID, modelID string) (sub2api.ProbeResult, error) { + f.callSequence = append(f.callSequence, "testAccount:"+accountID) if f.testedModels == nil { f.testedModels = map[string]string{} } diff --git a/internal/store/sqlite/db.go b/internal/store/sqlite/db.go index 97f155ec..0d415abe 100644 --- a/internal/store/sqlite/db.go +++ b/internal/store/sqlite/db.go @@ -44,6 +44,11 @@ func Open(ctx context.Context, dsn string) (*DB, error) { if err != nil { return nil, fmt.Errorf("open sqlite database: %w", err) } + // SQLite only tolerates a single writer at a time. Pin the pool to one + // connection so HTTP flows that chain several writes (for example host + // probe refresh + import persistence) do not self-deadlock into SQLITE_BUSY. + sqlDB.SetMaxOpenConns(1) + sqlDB.SetMaxIdleConns(1) if err := sqlDB.PingContext(ctx); err != nil { _ = sqlDB.Close() diff --git a/internal/store/sqlite/db_test.go b/internal/store/sqlite/db_test.go index 07b44e01..773ca9da 100644 --- a/internal/store/sqlite/db_test.go +++ b/internal/store/sqlite/db_test.go @@ -15,6 +15,24 @@ func TestOpenClose(t *testing.T) { } } +func TestOpenPinsSingleSQLiteConnection(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "single-conn.db") + store, err := Open(context.Background(), "file:"+filepath.ToSlash(dbPath)+"?_busy_timeout=5000") + if err != nil { + t.Fatalf("Open() error = %v", err) + } + defer func() { + if err := store.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } + }() + + stats := store.SQLDB().Stats() + if stats.MaxOpenConnections != 1 { + t.Fatalf("MaxOpenConnections = %d, want 1", stats.MaxOpenConnections) + } +} + func TestOpenInvalidDSN(t *testing.T) { _, err := Open(context.Background(), "file:/nonexistent/dir/test.db?_pragma=foreign_keys(0)") if err == nil { diff --git a/packs/openai-cn-pack/README.md b/packs/openai-cn-pack/README.md index e98e45fe..4890c877 100644 --- a/packs/openai-cn-pack/README.md +++ b/packs/openai-cn-pack/README.md @@ -23,6 +23,7 @@ - `kimi-k2-5-official.json` - `kimi-k2-thinking-official.json` - `minimax-m2-7-official.json` +- `minimax-53hk.json` - `step-3-5-flash-official.json` 对应的 `provider_id` 与首选模型如下: @@ -45,6 +46,8 @@ - `kimi-k2-thinking` - `minimax-m2-7-official` - `MiniMax-M2.7-highspeed`, `MiniMax-M2.5-highspeed` +- `minimax-53hk` + - `MiniMax-M2.7-highspeed`, `MiniMax-M2.5-highspeed` - `step-3-5-flash-official` - `step-3.5-flash` @@ -81,3 +84,4 @@ go run ./cmd/cli apply-host-overlay \ - `qwen.json` - `glm.json` - `minimax.json` +- `minimax-53hk.json` diff --git a/packs/openai-cn-pack/checksums.txt b/packs/openai-cn-pack/checksums.txt index 50ba5fdb..15e9a18c 100644 --- a/packs/openai-cn-pack/checksums.txt +++ b/packs/openai-cn-pack/checksums.txt @@ -1,17 +1,18 @@ -303bfaea55379f4a4329bd7f720b9c692981dd486a31176b6b93731f56b18cbc providers/minimax-m2-7-official.json +95f1ffc6f764b221b260fd62d649a2f91d136b8e7cf627ac65600cf92c015fd9 pack.json +8158ef1427d59ed10b9f1f91924b8100f5666eeb50d66799be59e1a3fd9fff02 providers/deepseek-chat-official.json +06373a0d6c0337b2cc5e3b4b9269fdd0399b782e092a77ccbcce6d36f7b3354f providers/deepseek-reasoner-official.json +5d33003fb6fefaf2dbb8445be7c18cc817d266215bc996ae91b30fbed5a98e7b providers/deepseek.json 391c3aa02b669eb591f2a3dda45500d66e6a519f07a05c158c8de6be69313e54 providers/glm-4-7-official.json +fe8c84b1b316e8c2e9eb60411383d04bfc0f0ff0c029db3b7d83aa976a6f6d54 providers/glm-5-1-official.json +2db47989a9715464a34b00f7e322ceb1396f96617ddcfa7dee5bd3e7b262c17d providers/kimi-a7m.json +51c8ab51666002c40f0f91744fbb5039d4a97c8c4bbb0b67c5936a5b5f8bf0bc providers/kimi-k2-5-official.json +65e3a1a5e56889ddb0474a3b55294aceb6920fa72dcf6f2c56d3199462daa4cf providers/kimi-k2-thinking-official.json +2da4777edeaa58db5cf79ecfba6cdac4dd3349bd8ff8d804c2e64ee42de4a377 providers/minimax-53hk.json +303bfaea55379f4a4329bd7f720b9c692981dd486a31176b6b93731f56b18cbc providers/minimax-m2-7-official.json +5dcc402daddacce6dcaceb1501020342f1b1121fbffe9097ede4d5aae072f84e providers/minimax.json +fa486a449407f38de8b180ff301568deccef5177ca0436158b1d5b0e6d9328b2 providers/openai-zhongzhuan.json 437028a75810bd970915ec31db1b415d384052143e404fcaa66e335ef2865ae3 providers/qwen-coder-official.json 4817f3faa54dd7fb20cf25a2ded0e702286d55ad829e686c125b70fc55cd52d6 providers/qwen-official.json -51c8ab51666002c40f0f91744fbb5039d4a97c8c4bbb0b67c5936a5b5f8bf0bc providers/kimi-k2-5-official.json -5d33003fb6fefaf2dbb8445be7c18cc817d266215bc996ae91b30fbed5a98e7b providers/deepseek.json -5dcc402daddacce6dcaceb1501020342f1b1121fbffe9097ede4d5aae072f84e providers/minimax.json -65e3a1a5e56889ddb0474a3b55294aceb6920fa72dcf6f2c56d3199462daa4cf providers/kimi-k2-thinking-official.json -a39de44fa68fcb5ee9c3ef38ed3bd5c30acd23cacd2f618d670de0bf9e7096e3 providers/deepseek-reasoner-official.json -2db47989a9715464a34b00f7e322ceb1396f96617ddcfa7dee5bd3e7b262c17d providers/kimi-a7m.json +eda16afc83e12055d3a41b5e37fd0923d3741b66da5af780bcea53ff34fa130e providers/step-3-5-flash-official.json 584991c1a5a3973bda9701ad15bb1c1b167038baa513cb67985708a65bdf6ca6 overlays/kimi-a7m-sub2api-v0.1.129.md 2b2597694ab03409360bf73de43a5cfcea0e26369c4ab18cc8552ff3278729aa overlays/kimi-a7m-sub2api-v0.1.129.patch -4d0069e7bb014b886d4b21fa9a2144fcf65e835f158eda1bb98e092efebd93f3 pack.json -eda16afc83e12055d3a41b5e37fd0923d3741b66da5af780bcea53ff34fa130e providers/step-3-5-flash-official.json -fa486a449407f38de8b180ff301568deccef5177ca0436158b1d5b0e6d9328b2 providers/openai-zhongzhuan.json -fdf7fa2e1ff4aa4f5dcd3f3ec2f55db11d6197625a467d6d0afa8a554a6ba6e6 providers/deepseek-chat-official.json -fe8c84b1b316e8c2e9eb60411383d04bfc0f0ff0c029db3b7d83aa976a6f6d54 providers/glm-5-1-official.json diff --git a/packs/openai-cn-pack/pack.json b/packs/openai-cn-pack/pack.json index 952a37ca..80a7864b 100644 --- a/packs/openai-cn-pack/pack.json +++ b/packs/openai-cn-pack/pack.json @@ -1,6 +1,6 @@ { "pack_id": "openai-cn-pack", - "version": "1.1.3", + "version": "1.1.4", "vendor": "YourTeam", "target_host": "sub2api", "min_host_version": "0.1.126", diff --git a/packs/openai-cn-pack/providers/deepseek-chat-official.json b/packs/openai-cn-pack/providers/deepseek-chat-official.json index 876e8ac3..07e7f4e1 100644 --- a/packs/openai-cn-pack/providers/deepseek-chat-official.json +++ b/packs/openai-cn-pack/providers/deepseek-chat-official.json @@ -4,6 +4,7 @@ "base_url": "https://api.deepseek.com/v1", "platform": "openai", "account_type": "apikey", + "force_disable_openai_responses_api": true, "default_models": ["deepseek-chat"], "smoke_test_model": "deepseek-chat", "group_template": { diff --git a/packs/openai-cn-pack/providers/deepseek-reasoner-official.json b/packs/openai-cn-pack/providers/deepseek-reasoner-official.json index bcddf4e9..c367bdde 100644 --- a/packs/openai-cn-pack/providers/deepseek-reasoner-official.json +++ b/packs/openai-cn-pack/providers/deepseek-reasoner-official.json @@ -4,6 +4,7 @@ "base_url": "https://api.deepseek.com/v1", "platform": "openai", "account_type": "apikey", + "force_disable_openai_responses_api": true, "default_models": ["deepseek-reasoner"], "smoke_test_model": "deepseek-reasoner", "group_template": { diff --git a/scripts/import_remote43_provider.sh b/scripts/import_remote43_provider.sh index 55d145d8..58f23a71 100755 --- a/scripts/import_remote43_provider.sh +++ b/scripts/import_remote43_provider.sh @@ -9,6 +9,7 @@ key_file="${4:-}" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck disable=SC1091 source "$ROOT_DIR/scripts/host_access_prep_lib.sh" +ARTIFACT_REDACTION_SCRIPT="$ROOT_DIR/scripts/artifact_redaction.py" KEY="${KEY:-/home/long/下载/zjsea.pem}" REMOTE="${REMOTE:-ubuntu@43.155.133.187}" @@ -326,12 +327,11 @@ PY remote_pg_query "$sql" > "$output_path" } -write_json_file "$ART/00-local-key-source.json" "$(python3 - <<'PY' "$key_source" "$provider_id" "$upstream_key" +write_json_file "$ART/00-local-key-source.json" "$(python3 - <<'PY' "$ARTIFACT_REDACTION_SCRIPT" "$key_source" "$provider_id" "$upstream_key" import json, sys -source, provider_id, key = sys.argv[1:4] -from pathlib import Path +redaction_script, source, provider_id, key = sys.argv[1:5] import subprocess -result = subprocess.check_output([sys.executable, 'scripts/artifact_redaction.py', 'redact-key', key], text=True) +result = subprocess.check_output([sys.executable, redaction_script, 'redact-key', key], text=True) redacted = json.loads(result) print(json.dumps({ 'source': source, @@ -555,10 +555,10 @@ subscription_cache_key="$(build_subscription_billing_cache_key "$sub_uid" "$subs prep_sql="$(build_subscription_access_prep_sql "$sub_uid" "$sub_key" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$admin_uid" "$SUBSCRIPTION_NOTES")" remote_pg_exec "$prep_sql" > "$ART/06-subscription-access-prep.psql.txt" -write_json_file "$ART/05-subscription-access-prep.summary.json" "$(python3 - <<'PY' "$sub_uid" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$sub_key" +write_json_file "$ART/05-subscription-access-prep.summary.json" "$(python3 - <<'PY' "$ARTIFACT_REDACTION_SCRIPT" "$sub_uid" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$sub_key" import json, subprocess, sys -sub_uid, group_id, min_balance, subscription_days, sub_key = sys.argv[1:6] -redacted = json.loads(subprocess.check_output([sys.executable, 'scripts/artifact_redaction.py', 'redact-key', sub_key], text=True)) +redaction_script, sub_uid, group_id, min_balance, subscription_days, sub_key = sys.argv[1:7] +redacted = json.loads(subprocess.check_output([sys.executable, redaction_script, 'redact-key', sub_key], text=True)) print(json.dumps({ 'subscription_user_id_hash': __import__('hashlib').sha256(sub_uid.encode('utf-8')).hexdigest(), 'subscription_group_id': int(group_id), diff --git a/scripts/remote43_patched_stack_lib.sh b/scripts/remote43_patched_stack_lib.sh index 59746735..e07c7d2d 100644 --- a/scripts/remote43_patched_stack_lib.sh +++ b/scripts/remote43_patched_stack_lib.sh @@ -70,11 +70,14 @@ render_remote43_crm_env() { local crm_port="$1" local sqlite_dsn="$2" local admin_token="$3" + local sqlite_dsn_q admin_token_q + printf -v sqlite_dsn_q '%q' "$sqlite_dsn" + printf -v admin_token_q '%q' "$admin_token" cat <