| import { |
| PROVIDER_DEFAULTS, |
| isWiredOAuthProvider, |
| providerAuthRequiresSecret, |
| providerSupportsModelDiscovery, |
| type ConnectionAuth, |
| type ConnectionLastTestStatus, |
| type LlmConnection, |
| type ProviderType, |
| } from './llm-connections.js'; |
| |
| export const PROVIDER_AUTH_SETUP_MODES = ['api_key', 'oauth', 'oauth_preview', 'none'] as const; |
| export type ProviderAuthSetupMode = (typeof PROVIDER_AUTH_SETUP_MODES)[number]; |
| |
| export const PROVIDER_AUTH_STATES = [ |
| 'disabled', |
| 'not_configured', |
| 'configured', |
| 'validated', |
| 'needs_reauth', |
| 'error', |
| 'preview_only', |
| ] as const; |
| export type ProviderAuthState = (typeof PROVIDER_AUTH_STATES)[number]; |
| |
| export const PROVIDER_AUTH_ACTIONS = [ |
| 'save_secret', |
| 'test_credentials', |
| 'fetch_models', |
| 'start_oauth', |
| 'refresh_oauth', |
| 'revoke_auth', |
| ] as const; |
| export type ProviderAuthAction = (typeof PROVIDER_AUTH_ACTIONS)[number]; |
| |
| export type ProviderAuthActionAvailability = 'available' | 'preview_only' | 'hidden'; |
| |
| export interface ProviderAuthContractInput { |
| providerType: ProviderType; |
| enabled?: boolean; |
| hasSecret?: boolean; |
| lastTestStatus?: ConnectionLastTestStatus; |
| } |
| |
| export interface ProviderAuthContract { |
| providerType: ProviderType; |
| setupMode: ProviderAuthSetupMode; |
| state: ProviderAuthState; |
| /** |
| * Credential validation only. This is intentionally separate from |
| * HealthSignal runtime probes and must not be rendered as "agent is |
| * operational". |
| */ |
| validationStatus: ConnectionLastTestStatus | 'not_run' | 'not_required'; |
| requiresSecret: boolean; |
| sendMayUseWithoutSecret: boolean; |
| actionAvailability: Record<ProviderAuthAction, ProviderAuthActionAvailability>; |
| copy: { |
| label: string; |
| detail: string; |
| }; |
| } |
| |
| export function deriveProviderAuthContract(input: ProviderAuthContractInput): ProviderAuthContract { |
| const defaults = PROVIDER_DEFAULTS[input.providerType]; |
| const enabled = input.enabled ?? true; |
| const hasSecret = Boolean(input.hasSecret); |
| // Unknown providerType (legacy seed, or a connection persisted on a branch |
| // that registers a provider this build doesn't know) → surface a non-real, |
| // non-actionable contract so the settings row renders instead of crashing. |
| // Mirrors `isFakeBackend` in connection-readiness.ts. |
| if (!defaults) { |
| return { |
| providerType: input.providerType, |
| setupMode: 'none', |
| state: enabled ? 'not_configured' : 'disabled', |
| validationStatus: 'not_required', |
| requiresSecret: false, |
| sendMayUseWithoutSecret: false, |
| actionAvailability: hiddenActions(), |
| copy: { |
| label: `${input.providerType} 未知或已迁移`, |
| detail: |
| '该连接使用的 provider 在当前版本未注册;配置会保留,切回支持它的版本即可继续使用。', |
| }, |
| }; |
| } |
| const supportsModelDiscovery = providerSupportsModelDiscovery(input.providerType); |
| const actionAvailability = hiddenActions(); |
| |
| if (!enabled) { |
| return { |
| providerType: input.providerType, |
| setupMode: setupModeForProvider(input.providerType), |
| state: 'disabled', |
| validationStatus: |
| input.lastTestStatus ?? |
| (providerAuthRequiresSecret(input.providerType) ? 'not_run' : 'not_required'), |
| requiresSecret: providerAuthRequiresSecret(input.providerType), |
| sendMayUseWithoutSecret: !providerAuthRequiresSecret(input.providerType), |
| actionAvailability, |
| copy: { |
| label: `${defaults.label} 已关闭`, |
| detail: '连接被显式关闭;不会作为发送默认连接,也不会触发凭据测试。', |
| }, |
| }; |
| } |
| |
| if (defaults.authKind === 'oauth_token') { |
| if (isWiredOAuthProvider(input.providerType)) { |
| const validationStatus = input.lastTestStatus ?? 'not_run'; |
| const state: ProviderAuthState = authStateFromSecretAndTest(hasSecret, input.lastTestStatus); |
| return { |
| providerType: input.providerType, |
| setupMode: 'oauth', |
| state, |
| validationStatus, |
| requiresSecret: true, |
| sendMayUseWithoutSecret: false, |
| actionAvailability: { |
| ...actionAvailability, |
| test_credentials: hasSecret ? 'available' : 'hidden', |
| fetch_models: hasSecret && supportsModelDiscovery ? 'available' : 'hidden', |
| start_oauth: hasSecret ? 'hidden' : 'available', |
| refresh_oauth: hasSecret ? 'available' : 'hidden', |
| revoke_auth: hasSecret ? 'available' : 'hidden', |
| }, |
| copy: copyForOAuth(defaults.label, state), |
| }; |
| } |
| return { |
| providerType: input.providerType, |
| setupMode: 'oauth_preview', |
| state: 'preview_only', |
| validationStatus: 'not_run', |
| requiresSecret: true, |
| sendMayUseWithoutSecret: false, |
| actionAvailability: { |
| ...actionAvailability, |
| start_oauth: 'preview_only', |
| refresh_oauth: 'preview_only', |
| revoke_auth: 'preview_only', |
| }, |
| copy: { |
| label: `${defaults.label} 账号登录预览`, |
| detail: '当前仅展示账号登录状态入口;普通模型密钥连接仍可在聊天模型中使用。', |
| }, |
| }; |
| } |
| |
| if (defaults.authKind === 'optional_api_key') { |
| const state = authStateFromSecretAndTest(true, input.lastTestStatus); |
| return { |
| providerType: input.providerType, |
| setupMode: 'api_key', |
| state, |
| validationStatus: input.lastTestStatus ?? (hasSecret ? 'not_run' : 'not_required'), |
| requiresSecret: false, |
| sendMayUseWithoutSecret: true, |
| actionAvailability: { |
| ...actionAvailability, |
| save_secret: 'available', |
| test_credentials: 'available', |
| fetch_models: supportsModelDiscovery ? 'available' : 'hidden', |
| revoke_auth: hasSecret ? 'available' : 'hidden', |
| }, |
| copy: copyForOptionalApiKey(defaults.label, state, hasSecret), |
| }; |
| } |
| |
| if (defaults.authKind === 'none') { |
| return { |
| providerType: input.providerType, |
| setupMode: 'none', |
| state: 'configured', |
| validationStatus: 'not_required', |
| requiresSecret: false, |
| sendMayUseWithoutSecret: true, |
| actionAvailability: { |
| ...actionAvailability, |
| test_credentials: 'available', |
| fetch_models: supportsModelDiscovery ? 'available' : 'hidden', |
| }, |
| copy: { |
| label: `${defaults.label} 不需要凭据`, |
| detail: '此模型服务不需要密钥;可用性仍取决于本地服务和模型列表。', |
| }, |
| }; |
| } |
| |
| const validationStatus = input.lastTestStatus ?? 'not_run'; |
| const state: ProviderAuthState = authStateFromSecretAndTest(hasSecret, input.lastTestStatus); |
| return { |
| providerType: input.providerType, |
| setupMode: 'api_key', |
| state, |
| validationStatus, |
| requiresSecret: true, |
| sendMayUseWithoutSecret: false, |
| actionAvailability: { |
| ...actionAvailability, |
| save_secret: 'available', |
| test_credentials: hasSecret ? 'available' : 'hidden', |
| fetch_models: hasSecret && supportsModelDiscovery ? 'available' : 'hidden', |
| revoke_auth: hasSecret ? 'available' : 'hidden', |
| }, |
| copy: copyForApiKey(defaults.label, state), |
| }; |
| } |
| |
| export function deriveProviderAuthContractFromConnection( |
| connection: Pick<LlmConnection, 'providerType' | 'enabled' | 'lastTestStatus'>, |
| hasSecret: boolean, |
| ): ProviderAuthContract { |
| return deriveProviderAuthContract({ |
| providerType: connection.providerType, |
| enabled: connection.enabled, |
| hasSecret, |
| lastTestStatus: connection.lastTestStatus, |
| }); |
| } |
| |
| export function isProviderAuthState(value: unknown): value is ProviderAuthState { |
| return typeof value === 'string' && (PROVIDER_AUTH_STATES as readonly string[]).includes(value); |
| } |
| |
| function authStateFromSecretAndTest( |
| hasSecret: boolean, |
| lastTestStatus: ConnectionLastTestStatus | undefined, |
| ): ProviderAuthState { |
| if (!hasSecret) return 'not_configured'; |
| if (lastTestStatus === 'verified') return 'validated'; |
| if (lastTestStatus === 'needs_reauth') return 'needs_reauth'; |
| if (lastTestStatus === 'error') return 'error'; |
| return 'configured'; |
| } |
| |
| function hiddenActions(): Record<ProviderAuthAction, ProviderAuthActionAvailability> { |
| return { |
| save_secret: 'hidden', |
| test_credentials: 'hidden', |
| fetch_models: 'hidden', |
| start_oauth: 'hidden', |
| refresh_oauth: 'hidden', |
| revoke_auth: 'hidden', |
| }; |
| } |
| |
| function setupModeForAuthKind(authKind: ConnectionAuth['kind']): ProviderAuthSetupMode { |
| if (authKind === 'none') return 'none'; |
| if (authKind === 'oauth_token') return 'oauth_preview'; |
| return 'api_key'; |
| } |
| |
| function setupModeForProvider(providerType: ProviderType): ProviderAuthSetupMode { |
| const authKind = PROVIDER_DEFAULTS[providerType]?.authKind; |
| if (authKind === 'oauth_token' && isWiredOAuthProvider(providerType)) return 'oauth'; |
| return setupModeForAuthKind(authKind); |
| } |
| |
| function copyForApiKey(label: string, state: ProviderAuthState): ProviderAuthContract['copy'] { |
| switch (state) { |
| case 'not_configured': |
| return { |
| label: `${label} 等待模型密钥`, |
| detail: '保存凭据后才能测试连接或拉取模型列表。', |
| }; |
| case 'validated': |
| return { |
| label: `${label} 凭据验证通过`, |
| detail: '这只代表凭据和端点验证通过,不代表消息发送、流式响应或中断恢复已经运行可用。', |
| }; |
| case 'needs_reauth': |
| return { |
| label: `${label} 需要重新授权`, |
| detail: '上次凭据测试显示鉴权失败;请替换凭据后重新测试。', |
| }; |
| case 'error': |
| return { |
| label: `${label} 凭据测试失败`, |
| detail: '上次测试未通过;详情必须使用概括后的错误信息,不展示服务商原始响应。', |
| }; |
| case 'configured': |
| return { |
| label: `${label} 已保存凭据`, |
| detail: '凭据已保存,等待验证;测试通过前不要把它展示成运行可用。', |
| }; |
| case 'disabled': |
| case 'preview_only': |
| return { |
| label, |
| detail: '当前状态不走模型密钥凭据流程。', |
| }; |
| } |
| } |
| |
| function copyForOptionalApiKey( |
| label: string, |
| state: ProviderAuthState, |
| hasSecret: boolean, |
| ): ProviderAuthContract['copy'] { |
| switch (state) { |
| case 'validated': |
| return { |
| label: `${label} 连接验证通过`, |
| detail: |
| '这只代表实例端点和鉴权配置验证通过,不代表消息发送、流式响应或中断恢复已经运行可用。', |
| }; |
| case 'needs_reauth': |
| return { |
| label: `${label} 需要重新授权`, |
| detail: '上次连接测试显示鉴权失败;请检查实例鉴权设置或可选模型密钥后重试。', |
| }; |
| case 'error': |
| return { |
| label: `${label} 连接测试失败`, |
| detail: '上次测试未通过;详情必须使用概括后的错误信息,不展示服务商原始响应。', |
| }; |
| case 'configured': |
| return { |
| label: `${label} 可选模型密钥`, |
| detail: hasSecret |
| ? '已保存可选模型密钥;也可删除密钥连接未启用鉴权的实例。' |
| : '模型密钥可选;未启用鉴权的实例可直接连接。', |
| }; |
| case 'not_configured': |
| case 'disabled': |
| case 'preview_only': |
| return { |
| label, |
| detail: '当前状态不走可选模型密钥流程。', |
| }; |
| } |
| } |
| |
| function copyForOAuth(label: string, state: ProviderAuthState): ProviderAuthContract['copy'] { |
| switch (state) { |
| case 'not_configured': |
| return { |
| label: `${label} 等待 OAuth 登录`, |
| detail: '完成账号登录后才能测试连接、拉取模型列表或用于聊天发送。', |
| }; |
| case 'validated': |
| return { |
| label: `${label} OAuth 已验证`, |
| detail: '这只代表账号令牌和端点验证通过,不代表消息发送、流式响应或中断恢复已经运行可用。', |
| }; |
| case 'needs_reauth': |
| return { |
| label: `${label} 需要重新登录`, |
| detail: '上次 OAuth 测试显示鉴权失败;请回到模型设置重新登录后再测试。', |
| }; |
| case 'error': |
| return { |
| label: `${label} OAuth 测试失败`, |
| detail: '上次测试未通过;详情必须使用概括后的错误信息,不展示服务商原始响应或账号令牌。', |
| }; |
| case 'configured': |
| return { |
| label: `${label} OAuth 已登录`, |
| detail: '账号令牌已保存,等待验证;测试通过前不要把它展示成运行可用。', |
| }; |
| case 'disabled': |
| case 'preview_only': |
| return { |
| label, |
| detail: '当前状态不走 OAuth 账号流程。', |
| }; |
| } |
| } |