| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- package service
- import (
- "context"
- "net/url"
- "strings"
- )
- // 渠道监控参数校验与归一化辅助函数。
- // 校验失败一律返回 channel_monitor_const.go 中预定义的 Err* 错误,错误信息不含具体 IP/hostname,避免泄露内网拓扑。
- // monitorProviders 渠道监控支持的全部 provider(与迁移 226 的 CHECK 约束一致)。
- // 不再以 adapter 表为唯一来源:antigravity 没有探活 adapter,但支持配额模式。
- //
- //nolint:gochecknoglobals // 静态查表,初始化后不变。
- var monitorProviders = map[string]struct{}{
- MonitorProviderOpenAI: {},
- MonitorProviderAnthropic: {},
- MonitorProviderGemini: {},
- MonitorProviderGrok: {},
- MonitorProviderAntigravity: {},
- MonitorProviderKimi: {},
- MonitorProviderZhipu: {},
- MonitorProviderDeepseek: {},
- }
- // probeCapableProviders 支持探活(probe / quota_probe)的 provider。
- // antigravity 上游无 Chat/Responses 可打(仅 IDE 代理形态),只允许配额模式。
- //
- //nolint:gochecknoglobals // 静态查表,初始化后不变。
- var probeCapableProviders = map[string]struct{}{
- MonitorProviderOpenAI: {},
- MonitorProviderAnthropic: {},
- MonitorProviderGemini: {},
- MonitorProviderGrok: {},
- MonitorProviderKimi: {},
- MonitorProviderZhipu: {},
- MonitorProviderDeepseek: {},
- }
- // validateProvider 校验 provider 字符串。
- func validateProvider(p string) error {
- if _, ok := monitorProviders[p]; !ok {
- return ErrChannelMonitorInvalidProvider
- }
- return nil
- }
- // providerSupportsProbe 该 provider 是否注册了探活 adapter(antigravity 为 false)。
- func providerSupportsProbe(p string) bool {
- _, ok := probeCapableProviders[p]
- return ok
- }
- // defaultCheckMode 空串归一为 probe,保证存量数据与旧客户端兼容。
- func defaultCheckMode(checkMode string) string {
- if strings.TrimSpace(checkMode) == "" {
- return MonitorCheckModeProbe
- }
- return strings.TrimSpace(checkMode)
- }
- // monitorCheckModeUsesQuota 该模式是否需要关联账号查配额。
- func monitorCheckModeUsesQuota(checkMode string) bool {
- return checkMode == MonitorCheckModeQuota || checkMode == MonitorCheckModeQuotaProbe
- }
- // validateCheckMode 校验 check_mode 与 provider 的组合矩阵:
- //
- // provider | probe | quota | quota_probe
- // ------------------------+-------+-------+------------
- // openai/anthropic/... | Y | Y | Y
- // antigravity(无 adapter)| N | Y | N
- func validateCheckMode(provider, checkMode string) error {
- checkMode = defaultCheckMode(checkMode)
- switch checkMode {
- case MonitorCheckModeProbe, MonitorCheckModeQuota, MonitorCheckModeQuotaProbe:
- default:
- return ErrChannelMonitorInvalidCheckMode
- }
- if checkMode != MonitorCheckModeQuota && !providerSupportsProbe(provider) {
- return ErrChannelMonitorInvalidCheckMode
- }
- return nil
- }
- // validateAPIMode 校验 provider 与 api_mode 的组合。
- // responses 只对 OpenAI 有意义;其它 provider 使用 chat_completions 作为默认占位。
- func validateAPIMode(provider, apiMode string) error {
- apiMode = defaultAPIMode(apiMode)
- switch apiMode {
- case MonitorAPIModeChatCompletions:
- return nil
- case MonitorAPIModeResponses:
- if provider == "" || provider == MonitorProviderOpenAI {
- return nil
- }
- return ErrChannelMonitorInvalidAPIMode
- default:
- return ErrChannelMonitorInvalidAPIMode
- }
- }
- // validateInterval 校验 interval_seconds 范围。
- func validateInterval(sec int) error {
- if sec < monitorMinIntervalSeconds || sec > monitorMaxIntervalSeconds {
- return ErrChannelMonitorInvalidInterval
- }
- return nil
- }
- // validateJitter 校验 jitter_seconds(调度 ± 随机抖动):
- // 非负,且 interval - jitter 不得低于最小检测间隔,防止随机偏移后实际间隔过短打爆上游。
- func validateJitter(jitterSec, intervalSec int) error {
- if jitterSec < 0 || intervalSec-jitterSec < monitorMinIntervalSeconds {
- return ErrChannelMonitorInvalidJitter
- }
- return nil
- }
- // validateEndpoint 校验 endpoint:
- // - scheme 强制 https(拒绝 http,避免明文凭证 + 部分 SSRF 利用面)
- // - 必须为 origin(无 path/query/fragment),防止用户填 https://api.openai.com/v1
- // 导致 joinURL 拼出 /v1/v1/chat/completions
- // - hostname 不能是 localhost/metadata 等已知元数据 hostname
- // - 解析所有 IP,任一落在 loopback/RFC1918/link-local/ULA 段即拒绝(防 SSRF)
- //
- // 错误信息不暴露具体 IP / hostname,避免泄露内网拓扑。
- func validateEndpoint(ep string) error {
- ep = strings.TrimSpace(ep)
- if ep == "" {
- return ErrChannelMonitorInvalidEndpoint
- }
- u, err := url.Parse(ep)
- if err != nil {
- return ErrChannelMonitorInvalidEndpoint
- }
- if u.Scheme != "https" {
- return ErrChannelMonitorEndpointScheme
- }
- if u.Host == "" {
- return ErrChannelMonitorInvalidEndpoint
- }
- if u.Path != "" && u.Path != "/" {
- return ErrChannelMonitorEndpointPath
- }
- if u.RawQuery != "" || u.Fragment != "" {
- return ErrChannelMonitorEndpointPath
- }
- hostname := u.Hostname()
- ctx, cancel := context.WithTimeout(context.Background(), monitorEndpointResolveTimeout)
- defer cancel()
- blocked, err := isPrivateOrLoopbackHost(ctx, hostname)
- if err != nil {
- return ErrChannelMonitorEndpointUnreachable
- }
- if blocked {
- return ErrChannelMonitorEndpointPrivate
- }
- return nil
- }
- // normalizeEndpoint 去除前后空白与末尾 `/`,保证存储统一为 origin。
- // validateEndpoint 已确保格式合法(仅 origin),这里只做最终归一化。
- func normalizeEndpoint(ep string) string {
- ep = strings.TrimSpace(ep)
- ep = strings.TrimRight(ep, "/")
- return ep
- }
- // normalizeModels 去除空白、重复模型名。保留输入顺序(map 的迭代顺序无关)。
- func normalizeModels(in []string) []string {
- if len(in) == 0 {
- return []string{}
- }
- seen := make(map[string]struct{}, len(in))
- out := make([]string, 0, len(in))
- for _, m := range in {
- m = strings.TrimSpace(m)
- if m == "" {
- continue
- }
- if _, ok := seen[m]; ok {
- continue
- }
- seen[m] = struct{}{}
- out = append(out, m)
- }
- return out
- }
- // normalizeMonitorPrimaryModel applies provider/check_mode defaults:
- // - pure quota mode never sends requests: placeholder "quota" keeps
- // primary_model NOT NULL (history rows / timeline need no special-casing)
- // - quota_probe still sends a real probe request: empty model returns ""
- // so validateCreateParams / applyMonitorUpdate report
- // ErrChannelMonitorMissingPrimaryModel instead of probing model="quota"
- // - Grok probing (probe/quota_probe) defaults to the lightweight check model
- func normalizeMonitorPrimaryModel(provider, checkMode, model string) string {
- model = strings.TrimSpace(model)
- if model == "" && defaultCheckMode(checkMode) == MonitorCheckModeQuota {
- return MonitorDefaultQuotaModel
- }
- if model == "" && provider == MonitorProviderGrok {
- return MonitorDefaultGrokModel
- }
- return model
- }
- // monitorAccountQuotaCapability 校验关联账号能否充当配额数据源,与
- // fetchUncached 的路由一一对应(coding→CN 额度端点 / payg→CN 余额端点 /
- // 其余→AccountUsageService)。在创建/更新期拦截注定运行期永久 error 的组合:
- // - kimi/zhipu/deepseek/minimax coding:GetCodingPlanProvider 须识别官方域名
- // (deepseek coding、自定义中转、minimax payg 无法路由额度端点)
- // - kimi/zhipu/deepseek/minimax payg:仅 kimi/deepseek 有公开余额端点
- // - anthropic:OAuth / Setup Token(API-Key 型无 usage 通道,永久 error)
- // - openai:OAuth(API-Key 型无 usage 通道)
- // - gemini/grok/antigravity:本地统计/值通道降级,不会永久 error,放行
- func monitorAccountQuotaCapability(account *Account) error {
- switch account.Platform {
- case PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
- if account.IsCodingPlan() {
- if p := account.GetCodingPlanProvider(); p != PlatformKimi && p != PlatformZhipu && p != PlatformMiniMax {
- return ErrChannelMonitorAccountNotSupportable
- }
- return nil
- }
- if account.Platform == PlatformZhipu || account.Platform == PlatformMiniMax {
- return ErrChannelMonitorAccountNotSupportable
- }
- return nil
- case PlatformAnthropic:
- if account.Type == AccountTypeOAuth || account.Type == AccountTypeSetupToken {
- return nil
- }
- return ErrChannelMonitorAccountNotSupportable
- case PlatformOpenAI:
- if account.Type == AccountTypeOAuth {
- return nil
- }
- return ErrChannelMonitorAccountNotSupportable
- default:
- return nil
- }
- }
- // defaultAPIMode 空串归一为 chat_completions,保证历史数据与旧客户端兼容。
- func defaultAPIMode(apiMode string) string {
- if strings.TrimSpace(apiMode) == "" {
- return MonitorAPIModeChatCompletions
- }
- return strings.TrimSpace(apiMode)
- }
|