* feat(catalog): 模型可用性 - 模型窗口支持展示往前推最近10次调用的红绿状态条 在模型选择弹窗中,单纯展示 24h 成功率难以反映模型短期的真实状态。 现支持往前推取最近最多 10 次实际调用(create 请求)的执行结果: - 成功一次展示绿色小点,失败一次展示红色小点,一共固定显示 10 个槽位点 - 槽位未发生调用时展示半透明灰色点;鼠标悬浮显示成功与失败次数的详细统计 - 后端从当前时刻往前推查询最近的 create 日志,按时间演进正序(最右侧为最新)输出 recentResults 切片 验证: - 后端:`go test -v ./internal/app -run TestPublicModelAvailability` 通过 - 前端:`tsc --noEmit`、`bun run build` 通过;`bun test` 2210 项通过 - 新增 `web/test/model-picker-availability-runs.test.ts` 单测并完成反向验证(改坏时测试变红) - 后端已无损平滑重启并监听 8080 端口 * feat(create): 模型选择器 - 左侧列表模型项直接展示10点可用性状态条 用户在模型选择弹窗中,希望无需点进二级卡片就能直接感知模型的可用率。 现将 10 点状态指示条挂载到左侧模型目录列表的每一项中: - 在每一个模型的子描述栏右侧常驻展示 10 个圆点的状态条 - 成功=绿点、失败=红点、未调用=灰点;鼠标悬浮显示最近成功与失败统计 - 保持列表项排版整齐,小点与渠道描述自适应两端对齐 验证: - `tsc --noEmit`、`bun run build` 通过 - `bun test` 2211 项测试通过(唯一失败为上游基线问题) - `test/model-picker-availability-runs.test.ts` 补充左侧列表挂载断言并通过
91 lines
3.6 KiB
Go
91 lines
3.6 KiB
Go
package app
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"yingce/backend/internal/model"
|
|
"yingce/backend/internal/repository"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestPublicModelAvailabilityUsesCreateAttemptsAndFixedSevenDayTrend(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&model.ApiCallLog{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := &Service{repo: repository.New(db)}
|
|
now := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
|
logs := make([]model.ApiCallLog, 0, 20)
|
|
for index := 0; index < 12; index++ {
|
|
status := model.ApiCallStatusSucceeded
|
|
if index >= 9 {
|
|
status = model.ApiCallStatusFailed
|
|
}
|
|
logs = append(logs, model.ApiCallLog{
|
|
ID: "ready-" + string(rune('a'+index)),
|
|
UserID: "user-1",
|
|
ChannelID: "channel-1",
|
|
Model: "model-a",
|
|
RequestKind: "create",
|
|
Status: status,
|
|
CreatedAt: now.Add(-time.Duration(index+1) * time.Hour),
|
|
})
|
|
}
|
|
logs = append(logs,
|
|
model.ApiCallLog{ID: "insufficient-1", UserID: "user-1", ChannelID: "channel-1", Model: "model-b", RequestKind: "create", Status: model.ApiCallStatusSucceeded, CreatedAt: now.Add(-2 * time.Hour)},
|
|
model.ApiCallLog{ID: "insufficient-2", UserID: "user-1", ChannelID: "channel-1", Model: "model-b", RequestKind: "create", Status: model.ApiCallStatusFailed, CreatedAt: now.Add(-3 * time.Hour)},
|
|
model.ApiCallLog{ID: "poll-only", UserID: "user-1", ChannelID: "channel-1", Model: "model-a", RequestKind: "poll", Status: model.ApiCallStatusFailed, CreatedAt: now.Add(-30 * time.Minute)},
|
|
model.ApiCallLog{ID: "download-only", UserID: "user-1", ChannelID: "channel-1", Model: "model-a", RequestKind: "download", Status: model.ApiCallStatusFailed, CreatedAt: now.Add(-20 * time.Minute)},
|
|
)
|
|
if err := db.Create(&logs).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
channels := []PublicChannelCatalog{{ID: "channel-1", Models: []PublicChannelModel{
|
|
{ModelKey: "model-a"},
|
|
{ModelKey: "model-b"},
|
|
{ModelKey: "model-c"},
|
|
}}}
|
|
availability, err := svc.publicModelAvailability(channels, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ready := availability[publicModelAvailabilityKey{channelID: "channel-1", modelKey: "model-a"}]
|
|
if ready == nil || ready.DataState != PublicModelAvailabilityReady || ready.SampleCount != 12 || ready.Rate24h == nil {
|
|
t.Fatalf("ready availability = %#v", ready)
|
|
}
|
|
if math.Abs(*ready.Rate24h-0.75) > 0.0001 {
|
|
t.Fatalf("ready rate = %v, want 0.75", *ready.Rate24h)
|
|
}
|
|
if len(ready.Trend7d) != 7 {
|
|
t.Fatalf("trend length = %d, want 7", len(ready.Trend7d))
|
|
}
|
|
if len(ready.RecentResults) != 10 {
|
|
t.Fatalf("ready recent results length = %d, want 10", len(ready.RecentResults))
|
|
}
|
|
|
|
insufficient := availability[publicModelAvailabilityKey{channelID: "channel-1", modelKey: "model-b"}]
|
|
if insufficient == nil || insufficient.DataState != PublicModelAvailabilityInsufficient || insufficient.SampleCount != 2 || insufficient.Rate24h != nil {
|
|
t.Fatalf("insufficient availability = %#v", insufficient)
|
|
}
|
|
if len(insufficient.RecentResults) != 2 || insufficient.RecentResults[0] != false || insufficient.RecentResults[1] != true {
|
|
t.Fatalf("insufficient recent results = %#v, want [false, true]", insufficient.RecentResults)
|
|
}
|
|
|
|
noData := availability[publicModelAvailabilityKey{channelID: "channel-1", modelKey: "model-c"}]
|
|
if noData == nil || noData.DataState != PublicModelAvailabilityNoData || noData.SampleCount != 0 || noData.Rate24h != nil {
|
|
t.Fatalf("no-data availability = %#v", noData)
|
|
}
|
|
if len(noData.RecentResults) != 0 {
|
|
t.Fatalf("no-data recent results length = %d, want 0", len(noData.RecentResults))
|
|
}
|
|
}
|