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.
17 KiB
TDD 实施计划 v2 — Batch Auto-Import
日期:2026-05-21
技术架构:docs/2026-05-22-BATCH_AUTO_IMPORT_V2_ARCHITECTURE.md
目标
让管理员只提供 (base_url, api_key),系统即可自动完成:
- 上游模型发现
- 模型名归一化与纠错
- 兼容能力画像生成
- 宿主资源创建与 channel 演化
- 异步确认账号与宿主稳定状态
- 最终
/v1/chat/completions闭环验证 - 运行态状态持久化与结果恢复
- 结果查看 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)
错误分类:
ErrAuthFailed:401/403ErrRateLimited:429ErrUpstreamUnreachable:502/503/timeout/connectionErrUnexpected:其他 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前缀剥离- 点号/连字符差异
- 典型人工误写场景,例如
m27vsm2.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/modelsPOST /v1/chat/completionsPOST /v1/responsesPOST /v1/messages(Anthropic 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_activeconfirmed_warningconfirmed_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_idresult_pageraw_modelsnormalized_modelsresolved_smoke_modelcapability_profileconfirmation_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,覆盖:
- 标准 OpenAI-compatible 上游成功导入
- 人工输入模型名错误,但通过 alias 解析成功
/responses=403,/chat/completions=200的第三方兼容场景- 首次
/accounts/:id/test=403,稍后 advisory 翻正 - 首次
/v1/chat/completions=503 no available accounts,重试后200 - capability profile 驱动路由分流
- 导入进行中即可查询 run / item 状态
- 控制面重启后,历史 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.gointernal/probe/aliases.gointernal/probe/capability.gointernal/probe/completion.gointernal/batch/provider_id.gointernal/batch/capability_profile.gointernal/batch/channel_evolution.gointernal/batch/service.gointernal/batch/confirmation.gointernal/batch/run_state.gointernal/host/sub2api/channel.gointernal/host/sub2api/accounts.gointernal/app/http_batch_import.gointernal/app/http_batch_runs.gocmd/cli/batch_import.gotests/integration/batch_import_test.go- 更新
EXECUTION_BOARD.md跟踪 V2 实施状态