Files
sub2api-cn-relay-manager/docs/2026-05-21-BATCH_AUTO_IMPORT_TDD_PLAN.md
phamnazage-jpg afce3da3df docs(v2): refine batch import architecture
Expand the batch auto-import V2 spec and TDD plan with stability requirements, result state persistence, and result page design. Add a dedicated architecture document for run state, APIs, pages, and UI field layout, and sync the execution board to the new V2 scope.
2026-05-22 13:18:51 +08:00

17 KiB
Raw Blame History

TDD 实施计划 v2 — Batch Auto-Import

日期2026-05-21 技术架构:docs/2026-05-22-BATCH_AUTO_IMPORT_V2_ARCHITECTURE.md

目标

让管理员只提供 (base_url, api_key),系统即可自动完成:

  1. 上游模型发现
  2. 模型名归一化与纠错
  3. 兼容能力画像生成
  4. 宿主资源创建与 channel 演化
  5. 异步确认账号与宿主稳定状态
  6. 最终 /v1/chat/completions 闭环验证
  7. 运行态状态持久化与结果恢复
  8. 结果查看 API 与页面

本计划与 2026-05-21-BATCH_AUTO_IMPORT_SPEC.md 保持一致,重点把“多次真实验收中踩出的经验”落实成可测试的实现顺序。

依赖顺序

必须按以下顺序实现,前一个未完成前不开始后一个:

probe/models + probe/aliases
          ↓
probe/capability + probe/completion
          ↓
batch/provider_id + batch/capability_profile
          ↓
host/channel_patch + batch/run_state
          ↓
batch/service
          ↓
batch/confirmation
          ↓
app/http_batch_import + app/http_batch_runs
          ↓
cmd/cli/batch_import
          ↓
tests/integration/batch_import

关键原则:

  • 先把“上游真实返回什么”查清楚,再决定写入宿主什么
  • 先把“兼容能力”显式建模,再决定 /responses/chat/completions、Anthropic 兼容入口如何分流
  • 先把“异步确认窗口”建模,再讨论最终 active/degraded/broken
  • 先把“状态如何持久化和投影”建模,再做结果页;页面只读运行态状态库,不直接拼接宿主实时返回

Stage 1: probe 模块(上游发现)

1.1 internal/probe/models.go

职责:调用 GET {base_url}/v1/models,解析 OpenAI-compatible 响应,返回原始模型 ID 列表。

type ModelsResult struct {
    RawModels   []string
    HTTPStatus  int
    LatencyMs   int64
    Error       string
}

func ProviderModels(ctx context.Context, baseURL, apiKey string) (*ModelsResult, error)

错误分类:

  • ErrAuthFailed401/403
  • ErrRateLimited429
  • ErrUpstreamUnreachable502/503/timeout/connection
  • ErrUnexpected:其他 HTTP / decode 错误

单测

func TestProviderModels_OpenAIFormat_ReturnsModelList(t *testing.T)
func TestProviderModels_EmptyData_ReturnsEmptySlice(t *testing.T)
func TestProviderModels_AuthFailed_ReturnsErrAuthFailed(t *testing.T)
func TestProviderModels_Timeout_ReturnsErrUpstreamUnreachable(t *testing.T)
func TestProviderModels_RecordsLatency(t *testing.T)

1.2 internal/probe/aliases.go

职责:归一化模型名,消除大小写、供应商前缀、常见格式差异,并生成推荐模型。

type AliasResult struct {
    Raw        string
    Normalized string
    Canonical  string
}

func NormalizeModelID(raw string) string
func CanonicalModelID(raw string) string
func BuildAliasTable(rawModels []string) map[string]AliasResult
func ResolveRequestedModel(requested string, rawModels []string) (resolved string, ok bool)

归一化最少覆盖:

  • 大小写归一
  • vendor/model 前缀剥离
  • 点号/连字符差异
  • 典型人工误写场景,例如 m27 vs m2.7

单测

func TestNormalizeModelID_MinimaxCanonical(t *testing.T)
func TestNormalizeModelID_DeepSeekVendorPrefix(t *testing.T)
func TestCanonicalModelID_KimiCaseInsensitive(t *testing.T)
func TestResolveRequestedModel_ExactHit(t *testing.T)
func TestResolveRequestedModel_UsesNormalizedAlias(t *testing.T)
func TestResolveRequestedModel_MissReturnsFalse(t *testing.T)

1.3 internal/probe/capability.go

职责:对同一把 key 进行最小兼容探测,生成 capability profile。

探测对象最少包括:

  • GET /v1/models
  • POST /v1/chat/completions
  • POST /v1/responses
  • POST /v1/messagesAnthropic compatible
type CapabilityProfile struct {
    SupportsOpenAIModels          bool
    SupportsOpenAIChatCompletions bool
    SupportsOpenAIResponses       bool
    SupportsAnthropicMessages     bool
    SupportsStream                string
    SupportsTools                 string
    SupportsReasoningFields       string
    AuthStyle                     string
    ModelIDStyle                  string
    KnownAdvisories               []string
}

func ProbeCapabilities(ctx context.Context, baseURL, apiKey string, rawModels []string) (*CapabilityProfile, error)

约束:

  • 对第三方 OpenAI-compatible 上游,/responses=403 不得机械判成 supported
  • 要能记录 responses_unsupported_but_chat_ok
  • 要能记录 initial_probe_race_expected

单测

func TestProbeCapabilities_Responses403Chat200_MarksResponsesUnsupported(t *testing.T)
func TestProbeCapabilities_AnthropicMessages200_MarksSupported(t *testing.T)
func TestProbeCapabilities_ModelsOnly_MarksPartialProfile(t *testing.T)
func TestProbeCapabilities_RecordsKnownAdvisories(t *testing.T)

1.4 internal/probe/completion.go

职责:从 discovered models 中选择 smoke model执行最小 completion 测试。

type CompletionResult struct {
    Model          string
    HTTPStatus     int
    LatencyMs      int64
    Classification string
    Error          string
}

func ResolveSmokeModel(requested []string, rawModels []string, profile *CapabilityProfile) (string, error)
func SmokeCompletion(ctx context.Context, baseURL, apiKey, model string, profile *CapabilityProfile) (*CompletionResult, error)

规则:

  • 优先使用 ResolveRequestedModel
  • 若人工指定模型无效,则自动退回上游真实可用模型
  • 若 profile 已知不支持 /responses,必须直接走 raw /chat/completions

单测

func TestResolveSmokeModel_UsesRequestedAliasWhenMatched(t *testing.T)
func TestResolveSmokeModel_FallsBackToDiscoveredModel(t *testing.T)
func TestSmokeCompletion_ResponsesUnsupported_UsesChatCompletions(t *testing.T)
func TestSmokeCompletion_AllCandidatesFail_ReturnsErrNoUsableModel(t *testing.T)

Stage 2: batch 模块(批量导入编排)

2.1 internal/batch/provider_id.go

职责:把 URL 规范化成稳定 provider_id

func NormalizeProviderID(baseURL string) string

策略:

  • 取 host 为主体
  • 用完整 URL hash 防碰撞
  • 同 host 不同 path 生成不同 ID

单测

func TestNormalizeProviderID_Basic(t *testing.T)
func TestNormalizeProviderID_WithPath_IncludesPathHash(t *testing.T)
func TestNormalizeProviderID_Idempotent(t *testing.T)
func TestNormalizeProviderID_DifferentPaths_DifferentIDs(t *testing.T)

2.2 internal/batch/capability_profile.go

职责:将 Stage 1 的 capability profile 映射为导入策略。

type ImportRoutingStrategy struct {
    UseRawChatCompletions bool
    SkipResponsesChecks   bool
    RetryInitial503       bool
    TreatProbe403AsAdvisory bool
}

func BuildImportRoutingStrategy(profile *probe.CapabilityProfile) ImportRoutingStrategy

单测

func TestBuildImportRoutingStrategy_ResponsesUnsupported_UsesRawChat(t *testing.T)
func TestBuildImportRoutingStrategy_ProbeRaceAdvisory_EnablesProbe403Advisory(t *testing.T)
func TestBuildImportRoutingStrategy_WarmupExpected_Enables503Retry(t *testing.T)

2.3 internal/batch/channel_evolution.go

职责:对比 channel 现有模型和新探测模型,计算 patch。

func ModelMappingDelta(existing []string, discovered []string) (add []string)
func BuildPatchModelMapping(existing map[string]string, aliases map[string]probe.AliasResult) map[string]string
func BuildPatchModelPricing(models []string) map[string]any

要求:

  • upstream raw model 和 gateway canonical model 的映射必须同时可追踪
  • patch 后不得破坏旧模型

单测

func TestModelMappingDelta_NoOverlap_AddsAll(t *testing.T)
func TestModelMappingDelta_PartialOverlap_AddsMissingOnly(t *testing.T)
func TestBuildPatchModelMapping_PreservesExistingEntries(t *testing.T)
func TestBuildPatchModelMapping_AddsCanonicalAliases(t *testing.T)

2.4 internal/batch/service.go

职责:编排 Probe → Provision → Async Confirm → Validate 四阶段。

type BatchImportRequest struct {
    BaseURL         string
    APIKey          string
    RequestedModels []string
    AccessMode      string
}

type BatchImportService struct {
    Host       hostadapter.HostAdapter
    Probe      *probe.Client
    Provision  *provision.ImportService
    Confirm    *ConfirmationService
}

func (s *BatchImportService) ImportBatch(ctx context.Context, req BatchImportRequest) (*BatchImportResult, error)

职责细化:

  • 不再信任 requested_models 为最终事实
  • 必须把 raw_models / normalized_models / resolved_smoke_model 写入结果
  • 首次 account test 的 403 Forbidden 允许按 advisory 处理
  • 首次 gateway completion 的短暂 503 no available accounts 允许短时重试

单测

func TestBatchImport_AllProbeOk_ProvisionsAndValidates(t *testing.T)
func TestBatchImport_RequestedModelMiss_UsesDiscoveredModel(t *testing.T)
func TestBatchImport_Probe403Race_DowngradesToWarning(t *testing.T)
func TestBatchImport_Initial503Warmup_RetriesBeforeBroken(t *testing.T)
func TestBatchImport_PartialMode_ContinuesOnFailure(t *testing.T)
func TestBatchImport_Idempotent_SkipsExistingAccount(t *testing.T)
func TestBatchImport_PersistsRunAndItemState(t *testing.T)

2.5 internal/batch/run_state.go

职责:持久化 import run / item 的阶段结果,并生成页面读取所需的统计投影。

type ImportRunState struct {
    RunID        string
    State        string
    TotalItems   int
    ActiveItems  int
    DegradedItems int
    BrokenItems  int
    StartedAt    time.Time
    UpdatedAt    time.Time
    FinishedAt   *time.Time
}

type ImportRunItemState struct {
    RunID             string
    ItemID            string
    BaseURL           string
    ProviderID        string
    CurrentStage      string
    StageStatus       string
    RetryCount        int
    AdvisoryMessages  []string
    LastErrorStage    string
    LastError         string
}

type RunStateStore interface {
    CreateRun(ctx context.Context, run ImportRunState) error
    UpdateRun(ctx context.Context, run ImportRunState) error
    UpsertItem(ctx context.Context, item ImportRunItemState) error
    ListRuns(ctx context.Context, limit int) ([]ImportRunState, error)
    ListRunItems(ctx context.Context, runID string) ([]ImportRunItemState, error)
}

单测

func TestRunStateStore_CreateAndUpdateRun(t *testing.T)
func TestRunStateStore_UpsertItemPreservesLatestStage(t *testing.T)
func TestRunStateStore_ListRunsReturnsSummary(t *testing.T)
func TestRunStateStore_ListRunItemsReturnsRetryAndAdvisory(t *testing.T)

Stage 3: host adapter 扩展

3.1 internal/host/sub2api/channel.go

新增:

func (h *HostAdapter) PatchChannel(ctx context.Context, channelID int64, addModels []string) error
func (h *HostAdapter) PatchChannelPricing(ctx context.Context, channelID int64, pricing map[string]any) error

单测

func TestPatchChannel_AddsModelMappingEntries(t *testing.T)
func TestPatchChannel_PreservesExistingEntries(t *testing.T)
func TestPatchChannelPricing_AddsNewModels(t *testing.T)

3.2 internal/host/sub2api/accounts.go

若当前 host adapter 的 account test / models 读取逻辑无法暴露 advisory 信息,需要最小增强:

type AccountProbeSnapshot struct {
    Models         []string
    ProbeStatus    string
    ProbeAdvisory  bool
    ProbeMessage   string
}

func (h *HostAdapter) GetAccountProbeSnapshot(ctx context.Context, accountID int64) (*AccountProbeSnapshot, error)

单测

func TestGetAccountProbeSnapshot_403RaceCapturedAsAdvisory(t *testing.T)
func TestGetAccountProbeSnapshot_ReturnsModelsAndMessage(t *testing.T)

Stage 4: async confirm 模块

4.1 internal/batch/confirmation.go

职责:把“账号刚建好但宿主异步行为未稳定”的窗口显式建模。

type ConfirmationStatus string

const (
    ConfirmationPending  ConfirmationStatus = "pending"
    ConfirmationActive   ConfirmationStatus = "confirmed_active"
    ConfirmationWarning  ConfirmationStatus = "confirmed_warning"
    ConfirmationBroken   ConfirmationStatus = "confirmed_broken"
)

type ConfirmationService struct {
    Host hostadapter.HostAdapter
}

func (s *ConfirmationService) ConfirmAccount(ctx context.Context, req ConfirmationRequest) (*ConfirmationResult, error)

行为:

  • 先查 /accounts/:id/models
  • 若 models 已正确,但 /test 为首次 403 Forbidden 且 profile 指示 third-party responses unsupported则判为 advisory
  • 若 completion 首次 503 no available accounts,在预算内短暂重试
  • 最终将结果归入:
    • confirmed_active
    • confirmed_warning
    • confirmed_broken

单测

func TestConfirmAccount_ModelsOkProbe403Race_ReturnsWarning(t *testing.T)
func TestConfirmAccount_Initial503Then200_ReturnsActive(t *testing.T)
func TestConfirmAccount_AllRetriesExhausted_ReturnsBroken(t *testing.T)
func TestConfirmAccount_RecordsRetryAttempts(t *testing.T)

Stage 5: 结果 API 与页面

5.1 internal/app/http_batch_import.go

职责:暴露运行结果查询 API。

func (a *App) listBatchImportRuns(w http.ResponseWriter, r *http.Request)
func (a *App) getBatchImportRun(w http.ResponseWriter, r *http.Request)
func (a *App) listBatchImportRunItems(w http.ResponseWriter, r *http.Request)
func (a *App) getBatchImportRunItem(w http.ResponseWriter, r *http.Request)

返回内容至少包含:

  • run summary
  • item current stage
  • normalized/resolved model 信息
  • confirmation / access 状态
  • advisory / retry / last_error

单测

func TestListBatchImportRuns_ReturnsSummary(t *testing.T)
func TestGetBatchImportRun_ReturnsRunDetail(t *testing.T)
func TestListBatchImportRunItems_ReturnsProjectedItems(t *testing.T)
func TestGetBatchImportRunItem_ReturnsAdvisoryAndRetryTrail(t *testing.T)

5.2 internal/app/http_batch_runs.go

职责:提供最小页面输出,不要求复杂前端,但必须让人直接看懂结果。

页面:

  • /batch-import/runs
  • /batch-import/runs/{run_id}

页面要求

  • 列表页可见 run 总状态
  • 详情页可见 item 级状态、模型纠错结果、capability profile 摘要、warning / broken 原因

单测

func TestBatchImportRunsPage_RendersRunSummary(t *testing.T)
func TestBatchImportRunDetailPage_RendersItemStates(t *testing.T)

Stage 6: CLI

6.1 cmd/cli/batch_import.go

go run ./cmd/cli batch-import \
  --host-base-url string \
  --host-api-key string \
  --entry "url,key" \
  --batch-file string \
  --mode "strict|partial" \
  --access-mode "subscription|self_service" \
  --requested-model string \
  --confirm-timeout duration

补充要求:

  • 输出必须包含:
    • run_id
    • result_page
    • raw_models
    • normalized_models
    • resolved_smoke_model
    • capability_profile
    • confirmation_status
  • 如果人工输入模型名不匹配CLI 要明确给出“推荐模型名”

CLI 集成测试

func TestBatchImportCLI_ReportsResolvedModel(t *testing.T)
func TestBatchImportCLI_ReportsCapabilityProfile(t *testing.T)
func TestBatchImportCLI_ReportsConfirmationStatus(t *testing.T)
func TestBatchImportCLI_ReportsRunResultPage(t *testing.T)

Stage 7: 集成测试

tests/integration/batch_import_test.go

使用真实 SQLite + fake/httptest upstream覆盖

  1. 标准 OpenAI-compatible 上游成功导入
  2. 人工输入模型名错误,但通过 alias 解析成功
  3. /responses=403/chat/completions=200 的第三方兼容场景
  4. 首次 /accounts/:id/test=403,稍后 advisory 翻正
  5. 首次 /v1/chat/completions=503 no available accounts,重试后 200
  6. capability profile 驱动路由分流
  7. 导入进行中即可查询 run / item 状态
  8. 控制面重启后,历史 run 结果仍可查看
func TestBatchImport_FullPipeline(t *testing.T)
func TestBatchImport_RequestedModelTypo_IsAutoCorrected(t *testing.T)
func TestBatchImport_ThirdPartyResponsesUnsupported_StillSucceeds(t *testing.T)
func TestBatchImport_ProbeRace_BecomesWarningNotBroken(t *testing.T)
func TestBatchImport_Initial503Warmup_RetrySucceeds(t *testing.T)
func TestBatchImport_RunStatusIsQueryableDuringExecution(t *testing.T)
func TestBatchImport_RunResultSurvivesRestart(t *testing.T)

验收命令

go test ./internal/probe/... -v -count=1
go test ./internal/batch/... -v -count=1
go test ./internal/app/... -v -count=1
go test ./internal/host/sub2api/... -v -count=1
go test ./tests/integration/... -count=1
go test -cover ./internal/... -count=1
go vet ./...
gofmt -l .

覆盖率目标:

  • internal/probe: >= 80%
  • internal/batch: >= 75%
  • internal/provision: >= 75%

任务清单

  • internal/probe/models.go
  • internal/probe/aliases.go
  • internal/probe/capability.go
  • internal/probe/completion.go
  • internal/batch/provider_id.go
  • internal/batch/capability_profile.go
  • internal/batch/channel_evolution.go
  • internal/batch/service.go
  • internal/batch/confirmation.go
  • internal/batch/run_state.go
  • internal/host/sub2api/channel.go
  • internal/host/sub2api/accounts.go
  • internal/app/http_batch_import.go
  • internal/app/http_batch_runs.go
  • cmd/cli/batch_import.go
  • tests/integration/batch_import_test.go
  • 更新 EXECUTION_BOARD.md 跟踪 V2 实施状态