feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-21 18:47:54 +08:00
|
|
|
|
"sort"
|
|
|
|
|
|
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
2026-04-21 01:05:14 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// AvailableChannelHandler 处理用户侧「可用渠道」查询。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 用户侧接口委托 ChannelService.ListAvailable,并在返回前做三层过滤:
|
|
|
|
|
|
// 1. 行过滤:只保留状态为 Active 且与当前用户可访问分组有交集的渠道;
|
|
|
|
|
|
// 2. 分组过滤:渠道的 Groups 只保留用户可访问的那些;
|
|
|
|
|
|
// 3. 平台过滤:渠道的 SupportedModels 只保留平台在用户可见 Groups 中出现过的模型,
|
|
|
|
|
|
// 防止"渠道同时挂在 antigravity / anthropic 两个平台的分组上,用户只访问
|
|
|
|
|
|
// antigravity,却看到 anthropic 模型"这类跨平台信息泄漏;
|
|
|
|
|
|
// 4. 字段白名单:仅返回用户需要的字段(省略 BillingModelSource / RestrictModels
|
|
|
|
|
|
// / 内部 ID / Status 等管理字段)。
|
|
|
|
|
|
type AvailableChannelHandler struct {
|
|
|
|
|
|
channelService *service.ChannelService
|
|
|
|
|
|
apiKeyService *service.APIKeyService
|
2026-04-21 17:23:20 +08:00
|
|
|
|
settingService *service.SettingService
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewAvailableChannelHandler 创建用户侧可用渠道 handler。
|
|
|
|
|
|
func NewAvailableChannelHandler(
|
|
|
|
|
|
channelService *service.ChannelService,
|
|
|
|
|
|
apiKeyService *service.APIKeyService,
|
2026-04-21 17:23:20 +08:00
|
|
|
|
settingService *service.SettingService,
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
) *AvailableChannelHandler {
|
|
|
|
|
|
return &AvailableChannelHandler{
|
|
|
|
|
|
channelService: channelService,
|
|
|
|
|
|
apiKeyService: apiKeyService,
|
2026-04-21 17:23:20 +08:00
|
|
|
|
settingService: settingService,
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 17:23:20 +08:00
|
|
|
|
// featureEnabled 返回 available-channels 开关是否启用。默认关闭(opt-in)。
|
|
|
|
|
|
func (h *AvailableChannelHandler) featureEnabled(c *gin.Context) bool {
|
|
|
|
|
|
if h.settingService == nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return h.settingService.GetAvailableChannelsRuntime(c.Request.Context()).Enabled
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
// userAvailableGroup 用户可见的分组概要(白名单字段)。
|
2026-04-21 21:44:34 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 前端据此区分专属 vs 公开分组(IsExclusive)、订阅 vs 标准分组(SubscriptionType,
|
|
|
|
|
|
// 订阅视觉加深),并用 RateMultiplier 作为默认倍率;用户专属倍率前端走
|
|
|
|
|
|
// /groups/rates,和 API 密钥页面保持一致。
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
type userAvailableGroup struct {
|
2026-04-21 21:44:34 +08:00
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
Platform string `json:"platform"`
|
|
|
|
|
|
SubscriptionType string `json:"subscription_type"`
|
|
|
|
|
|
RateMultiplier float64 `json:"rate_multiplier"`
|
|
|
|
|
|
IsExclusive bool `json:"is_exclusive"`
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// userSupportedModelPricing 用户可见的定价字段白名单。
|
|
|
|
|
|
type userSupportedModelPricing struct {
|
|
|
|
|
|
BillingMode string `json:"billing_mode"`
|
|
|
|
|
|
InputPrice *float64 `json:"input_price"`
|
|
|
|
|
|
OutputPrice *float64 `json:"output_price"`
|
|
|
|
|
|
CacheWritePrice *float64 `json:"cache_write_price"`
|
|
|
|
|
|
CacheReadPrice *float64 `json:"cache_read_price"`
|
|
|
|
|
|
ImageOutputPrice *float64 `json:"image_output_price"`
|
|
|
|
|
|
PerRequestPrice *float64 `json:"per_request_price"`
|
|
|
|
|
|
Intervals []userPricingIntervalDTO `json:"intervals"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// userPricingIntervalDTO 定价区间白名单(去掉内部 ID、SortOrder 等前端不渲染的字段)。
|
|
|
|
|
|
type userPricingIntervalDTO struct {
|
|
|
|
|
|
MinTokens int `json:"min_tokens"`
|
|
|
|
|
|
MaxTokens *int `json:"max_tokens"`
|
|
|
|
|
|
TierLabel string `json:"tier_label,omitempty"`
|
|
|
|
|
|
InputPrice *float64 `json:"input_price"`
|
|
|
|
|
|
OutputPrice *float64 `json:"output_price"`
|
|
|
|
|
|
CacheWritePrice *float64 `json:"cache_write_price"`
|
|
|
|
|
|
CacheReadPrice *float64 `json:"cache_read_price"`
|
|
|
|
|
|
PerRequestPrice *float64 `json:"per_request_price"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// userSupportedModel 用户可见的支持模型条目。
|
|
|
|
|
|
type userSupportedModel struct {
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
Platform string `json:"platform"`
|
|
|
|
|
|
Pricing *userSupportedModelPricing `json:"pricing"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 19:46:55 +08:00
|
|
|
|
// userChannelPlatformSection 单渠道内某个平台的子视图:用户可见的分组 + 该平台
|
|
|
|
|
|
// 支持的模型。按 platform 聚合后让前端可以把渠道名作为 row-group 一次渲染,
|
|
|
|
|
|
// 后面的平台行按 sections 顺序铺开。
|
|
|
|
|
|
type userChannelPlatformSection struct {
|
2026-04-21 18:47:54 +08:00
|
|
|
|
Platform string `json:"platform"`
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
Groups []userAvailableGroup `json:"groups"`
|
|
|
|
|
|
SupportedModels []userSupportedModel `json:"supported_models"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 19:46:55 +08:00
|
|
|
|
// userAvailableChannel 用户可见的渠道条目(白名单字段)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 每个渠道聚合为一条记录,内嵌 platforms 子数组:每个 section 对应一个平台,
|
|
|
|
|
|
// 包含该平台的 groups 和 supported_models。
|
|
|
|
|
|
type userAvailableChannel struct {
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
|
Platforms []userChannelPlatformSection `json:"platforms"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
// List 列出当前用户可见的「可用渠道」。
|
|
|
|
|
|
// GET /api/v1/channels/available
|
|
|
|
|
|
func (h *AvailableChannelHandler) List(c *gin.Context) {
|
2026-04-21 01:05:14 +08:00
|
|
|
|
subject, ok := middleware.GetAuthSubjectFromContext(c)
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
if !ok {
|
|
|
|
|
|
response.Unauthorized(c, "User not authenticated")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 17:23:20 +08:00
|
|
|
|
// Feature 未启用时返回空数组(不暴露渠道信息)。检查放在认证之后,
|
|
|
|
|
|
// 保持与未开关前的 401 行为一致:未登录先 401,登录后再按开关决定。
|
|
|
|
|
|
if !h.featureEnabled(c) {
|
|
|
|
|
|
response.Success(c, []userAvailableChannel{})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
userGroups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), subject.UserID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.ErrorFrom(c, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
allowedGroupIDs := make(map[int64]struct{}, len(userGroups))
|
|
|
|
|
|
for i := range userGroups {
|
|
|
|
|
|
allowedGroupIDs[userGroups[i].ID] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
channels, err := h.channelService.ListAvailable(c.Request.Context())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.ErrorFrom(c, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
out := make([]userAvailableChannel, 0, len(channels))
|
|
|
|
|
|
for _, ch := range channels {
|
|
|
|
|
|
if ch.Status != service.StatusActive {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
visibleGroups := filterUserVisibleGroups(ch.Groups, allowedGroupIDs)
|
|
|
|
|
|
if len(visibleGroups) == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-04-21 19:46:55 +08:00
|
|
|
|
sections := buildPlatformSections(ch, visibleGroups)
|
|
|
|
|
|
if len(sections) == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, userAvailableChannel{
|
|
|
|
|
|
Name: ch.Name,
|
|
|
|
|
|
Description: ch.Description,
|
|
|
|
|
|
Platforms: sections,
|
|
|
|
|
|
})
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.Success(c, out)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 19:46:55 +08:00
|
|
|
|
// buildPlatformSections 把一个渠道按 visibleGroups 的平台集合拆成有序的 section 列表:
|
|
|
|
|
|
// 每个 section 对应一个平台,只包含该平台的 groups 和 supported_models。
|
|
|
|
|
|
// 输出按 platform 字母序稳定排序,便于前端等效比较与回归测试。
|
|
|
|
|
|
func buildPlatformSections(
|
2026-04-21 18:47:54 +08:00
|
|
|
|
ch service.AvailableChannel,
|
|
|
|
|
|
visibleGroups []userAvailableGroup,
|
2026-04-21 19:46:55 +08:00
|
|
|
|
) []userChannelPlatformSection {
|
2026-04-21 18:47:54 +08:00
|
|
|
|
groupsByPlatform := make(map[string][]userAvailableGroup, 4)
|
|
|
|
|
|
for _, g := range visibleGroups {
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
if g.Platform == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-04-21 18:47:54 +08:00
|
|
|
|
groupsByPlatform[g.Platform] = append(groupsByPlatform[g.Platform], g)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(groupsByPlatform) == 0 {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
platforms := make([]string, 0, len(groupsByPlatform))
|
|
|
|
|
|
for p := range groupsByPlatform {
|
|
|
|
|
|
platforms = append(platforms, p)
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Strings(platforms)
|
|
|
|
|
|
|
2026-04-21 19:46:55 +08:00
|
|
|
|
sections := make([]userChannelPlatformSection, 0, len(platforms))
|
2026-04-21 18:47:54 +08:00
|
|
|
|
for _, platform := range platforms {
|
|
|
|
|
|
platformSet := map[string]struct{}{platform: {}}
|
2026-04-21 19:46:55 +08:00
|
|
|
|
sections = append(sections, userChannelPlatformSection{
|
2026-04-21 18:47:54 +08:00
|
|
|
|
Platform: platform,
|
|
|
|
|
|
Groups: groupsByPlatform[platform],
|
|
|
|
|
|
SupportedModels: toUserSupportedModels(ch.SupportedModels, platformSet),
|
|
|
|
|
|
})
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
}
|
2026-04-21 19:46:55 +08:00
|
|
|
|
return sections
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// filterUserVisibleGroups 仅保留用户可访问的分组。
|
|
|
|
|
|
func filterUserVisibleGroups(
|
|
|
|
|
|
groups []service.AvailableGroupRef,
|
|
|
|
|
|
allowed map[int64]struct{},
|
|
|
|
|
|
) []userAvailableGroup {
|
|
|
|
|
|
visible := make([]userAvailableGroup, 0, len(groups))
|
|
|
|
|
|
for _, g := range groups {
|
|
|
|
|
|
if _, ok := allowed[g.ID]; !ok {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
visible = append(visible, userAvailableGroup{
|
2026-04-21 21:44:34 +08:00
|
|
|
|
ID: g.ID,
|
|
|
|
|
|
Name: g.Name,
|
|
|
|
|
|
Platform: g.Platform,
|
|
|
|
|
|
SubscriptionType: g.SubscriptionType,
|
|
|
|
|
|
RateMultiplier: g.RateMultiplier,
|
|
|
|
|
|
IsExclusive: g.IsExclusive,
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return visible
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// toUserSupportedModels 将 service 层支持模型转换为用户 DTO(字段白名单)。
|
|
|
|
|
|
// 仅保留平台在 allowedPlatforms 中的条目,防止跨平台模型信息泄漏。
|
|
|
|
|
|
// allowedPlatforms 为 nil 时不做平台过滤(保留全部,供测试或明确无过滤场景使用)。
|
|
|
|
|
|
func toUserSupportedModels(
|
|
|
|
|
|
src []service.SupportedModel,
|
|
|
|
|
|
allowedPlatforms map[string]struct{},
|
|
|
|
|
|
) []userSupportedModel {
|
|
|
|
|
|
out := make([]userSupportedModel, 0, len(src))
|
|
|
|
|
|
for i := range src {
|
|
|
|
|
|
m := src[i]
|
|
|
|
|
|
if allowedPlatforms != nil {
|
|
|
|
|
|
if _, ok := allowedPlatforms[m.Platform]; !ok {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, userSupportedModel{
|
|
|
|
|
|
Name: m.Name,
|
|
|
|
|
|
Platform: m.Platform,
|
|
|
|
|
|
Pricing: toUserPricing(m.Pricing),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// toUserPricing 将 service 层定价转换为用户 DTO;入参为 nil 时返回 nil。
|
|
|
|
|
|
func toUserPricing(p *service.ChannelModelPricing) *userSupportedModelPricing {
|
|
|
|
|
|
if p == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
intervals := make([]userPricingIntervalDTO, 0, len(p.Intervals))
|
|
|
|
|
|
for _, iv := range p.Intervals {
|
|
|
|
|
|
intervals = append(intervals, userPricingIntervalDTO{
|
|
|
|
|
|
MinTokens: iv.MinTokens,
|
|
|
|
|
|
MaxTokens: iv.MaxTokens,
|
|
|
|
|
|
TierLabel: iv.TierLabel,
|
|
|
|
|
|
InputPrice: iv.InputPrice,
|
|
|
|
|
|
OutputPrice: iv.OutputPrice,
|
|
|
|
|
|
CacheWritePrice: iv.CacheWritePrice,
|
|
|
|
|
|
CacheReadPrice: iv.CacheReadPrice,
|
|
|
|
|
|
PerRequestPrice: iv.PerRequestPrice,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
billingMode := string(p.BillingMode)
|
|
|
|
|
|
if billingMode == "" {
|
|
|
|
|
|
billingMode = string(service.BillingModeToken)
|
|
|
|
|
|
}
|
|
|
|
|
|
return &userSupportedModelPricing{
|
|
|
|
|
|
BillingMode: billingMode,
|
|
|
|
|
|
InputPrice: p.InputPrice,
|
|
|
|
|
|
OutputPrice: p.OutputPrice,
|
|
|
|
|
|
CacheWritePrice: p.CacheWritePrice,
|
|
|
|
|
|
CacheReadPrice: p.CacheReadPrice,
|
|
|
|
|
|
ImageOutputPrice: p.ImageOutputPrice,
|
|
|
|
|
|
PerRequestPrice: p.PerRequestPrice,
|
|
|
|
|
|
Intervals: intervals,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|