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
This commit is contained in:
erio
2026-04-21 00:27:10 +08:00
parent c46744f366
commit 654cfb6480
29 changed files with 2012 additions and 45 deletions

View File

@@ -344,6 +344,7 @@ export default {
users: 'Users',
groups: 'Groups',
channels: 'Channels',
availableChannels: 'Available Channels',
subscriptions: 'Subscriptions',
accounts: 'Accounts',
proxies: 'Proxies',
@@ -929,6 +930,38 @@ export default {
}
},
// Available Channels (user-facing)
availableChannels: {
title: 'Available Channels',
description: 'Channels you can access, along with their supported models and pricing',
searchPlaceholder: 'Search channels or models...',
empty: 'No available channels',
noModels: 'No models configured',
noPricing: 'Pricing not configured',
columns: {
name: 'Channel',
groups: 'Your Accessible Groups',
supportedModels: 'Supported Models'
},
pricing: {
billingMode: 'Billing Mode',
billingModeToken: 'Per Token',
billingModePerRequest: 'Per Request',
billingModeImage: 'Per Image',
inputPrice: 'Input',
outputPrice: 'Output',
cacheWritePrice: 'Cache Write',
cacheReadPrice: 'Cache Read',
imageOutputPrice: 'Image Output',
perRequestPrice: 'Per Request',
intervals: 'Tiered Pricing',
tierLabel: 'Tier',
tokenRange: 'Token Range',
unitPerMillion: '/ 1M tokens',
unitPerRequest: '/ request'
}
},
// Redeem
redeem: {
title: 'Redeem Code',
@@ -1980,6 +2013,48 @@ export default {
}
},
// Available Channels (aggregated read-only view)
availableChannels: {
title: 'Available Channels',
description: 'Aggregated view: each channel with its linked groups and supported models (wildcards expanded)',
searchPlaceholder: 'Search channels or models...',
columns: {
name: 'Channel',
status: 'Status',
billingSource: 'Billing Model Source',
groups: 'Linked Groups',
supportedModels: 'Supported Models'
},
empty: 'No data',
noGroups: 'No linked groups',
noModels: 'No model mapping configured',
noPricing: 'Pricing not configured',
statusActive: 'Active',
statusDisabled: 'Disabled',
billingSource: {
requested: 'Requested model',
upstream: 'Upstream model',
channel_mapped: 'Channel-mapped model'
},
pricing: {
billingMode: 'Billing Mode',
billingModeToken: 'Per Token',
billingModePerRequest: 'Per Request',
billingModeImage: 'Per Image',
inputPrice: 'Input',
outputPrice: 'Output',
cacheWritePrice: 'Cache Write',
cacheReadPrice: 'Cache Read',
imageOutputPrice: 'Image Output',
perRequestPrice: 'Per Request',
intervals: 'Tiered Pricing',
tierLabel: 'Tier',
tokenRange: 'Token Range',
unitPerMillion: '/ 1M tokens',
unitPerRequest: '/ request'
}
},
// Channel Management
channels: {
title: 'Channel Management',

View File

@@ -344,6 +344,7 @@ export default {
users: '用户管理',
groups: '分组管理',
channels: '渠道管理',
availableChannels: '可用渠道',
subscriptions: '订阅管理',
accounts: '账号管理',
proxies: 'IP管理',
@@ -933,6 +934,38 @@ export default {
}
},
// Available Channels (user-facing)
availableChannels: {
title: '可用渠道',
description: '查看您可访问的渠道与其支持的模型、定价',
searchPlaceholder: '搜索渠道或模型...',
empty: '暂无可用渠道',
noModels: '未配置模型',
noPricing: '未配置定价',
columns: {
name: '渠道名',
groups: '我可访问的分组',
supportedModels: '支持模型'
},
pricing: {
billingMode: '计费模式',
billingModeToken: '按 Token',
billingModePerRequest: '按次',
billingModeImage: '按图片',
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
cacheReadPrice: '缓存读取',
imageOutputPrice: '图片输出',
perRequestPrice: '每次请求',
intervals: '阶梯定价',
tierLabel: '层级',
tokenRange: 'Token 区间',
unitPerMillion: '/ 1M token',
unitPerRequest: '/ 次'
}
},
// Redeem
redeem: {
title: '兑换码',
@@ -2059,6 +2092,48 @@ export default {
}
},
// Available Channels (aggregated read-only view)
availableChannels: {
title: '可用渠道',
description: '按渠道聚合查看关联分组与支持模型(已展开通配符)',
searchPlaceholder: '搜索渠道或模型...',
columns: {
name: '渠道名',
status: '状态',
billingSource: '计费模型来源',
groups: '关联分组',
supportedModels: '支持模型'
},
empty: '暂无数据',
noGroups: '未关联分组',
noModels: '未配置模型映射',
noPricing: '未配置定价',
statusActive: '启用',
statusDisabled: '停用',
billingSource: {
requested: '请求模型',
upstream: '上游模型',
channel_mapped: '映射后模型'
},
pricing: {
billingMode: '计费模式',
billingModeToken: '按 Token',
billingModePerRequest: '按次',
billingModeImage: '按图片',
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
cacheReadPrice: '缓存读取',
imageOutputPrice: '图片输出',
perRequestPrice: '每次请求',
intervals: '阶梯定价',
tierLabel: '层级',
tokenRange: 'Token 区间',
unitPerMillion: '/ 1M token',
unitPerRequest: '/ 次'
}
},
// Channel Management
channels: {
title: '渠道管理',