// Package oci 封装 Oracle Cloud Infrastructure 官方 Go SDK, // 对上层提供统一的客户端抽象;真实云请求只允许出现在本包。 package oci import ( "context" "errors" "fmt" "io" "strings" "sync" "time" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/identity" "oci-portal/internal/aiwire" ) // cloudcmServiceName 是 OCI 云消费(Cloud Consumption Model)订阅的服务名, // 账户类别信息挂在该订阅之下。 const cloudcmServiceName = "CLOUDCM" // ErrNoCloudcmSubscription 表示租户下没有 serviceName=CLOUDCM 的订阅。 var ErrNoCloudcmSubscription = errors.New("no CLOUDCM subscription found") // TenancyInfo 是测活成功后取得的租户基本信息。 type TenancyInfo struct { Name string HomeRegionKey string } // Client 是真实 OCI 调用的统一入口,service 层只依赖此接口。 type Client interface { // ValidateKey 用 Identity GetTenancy 验证凭据有效性(测活)。 ValidateKey(ctx context.Context, cred Credentials) (TenancyInfo, error) // FetchAccountProfile 查询 CLOUDCM 订阅并判定账户类别。 FetchAccountProfile(ctx context.Context, cred Credentials) (AccountProfile, error) // ListCompartments 递归列出租户下全部 ACTIVE compartment(不含租户根)。 ListCompartments(ctx context.Context, cred Credentials) ([]Compartment, error) // ListRegionSubscriptions 列出租户已订阅的区域。 ListRegionSubscriptions(ctx context.Context, cred Credentials) ([]RegionSubscription, error) // SubscribeRegion 为租户订阅新区域;请求发往 homeRegion,操作不可撤销。 SubscribeRegion(ctx context.Context, cred Credentials, homeRegion, regionKey string) error // ListLimits 查询服务配额,name 为大小写不敏感的模糊过滤,按需附带用量。 ListLimits(ctx context.Context, cred Credentials, q LimitsQuery) ([]LimitValue, error) // ListLimitServices 列出配额体系支持的全部服务。 ListLimitServices(ctx context.Context, cred Credentials, region string) ([]LimitService, error) // ListSubscriptions 列出租户全部订阅摘要。 ListSubscriptions(ctx context.Context, cred Credentials) ([]SubscriptionInfo, error) // GetSubscription 查询单个订阅的完整信息。 GetSubscription(ctx context.Context, cred Credentials, subscriptionID string) (SubscriptionDetail, error) // 账单(OSP Gateway,仅主区域):发票列表/明细/PDF/付款与订阅付款方式。 // ListInvoices 的 year>0 时按自然年过滤(timeInvoice 窗口),0 为全量。 ListInvoices(ctx context.Context, cred Credentials, year int) ([]Invoice, error) ListInvoiceLines(ctx context.Context, cred Credentials, internalInvoiceID string) ([]InvoiceLine, error) DownloadInvoicePdf(ctx context.Context, cred Credentials, internalInvoiceID string, maxBytes int64) ([]byte, error) PayInvoice(ctx context.Context, cred Credentials, internalInvoiceID, email string) error ListPaymentMethods(ctx context.Context, cred Credentials) ([]PaymentMethod, error) // ListShapes 列出租户在目标区域可用的实例规格(带缓存)。 ListShapes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]ComputeShape, error) // ListImages 列出可用的实例镜像。 ListImages(ctx context.Context, cred Credentials, q ImagesQuery) ([]Image, error) // GetImage 查询单个镜像。 GetImage(ctx context.Context, cred Credentials, region, imageID string) (Image, error) // VCN 增删改查与一键 IPv6。 ListVCNs(ctx context.Context, cred Credentials, region string) ([]VCN, error) CreateVCN(ctx context.Context, cred Credentials, in CreateVCNInput) (VCN, error) GetVCN(ctx context.Context, cred Credentials, region, vcnID string) (VCN, error) UpdateVCN(ctx context.Context, cred Credentials, region, vcnID, displayName string) (VCN, error) DeleteVCN(ctx context.Context, cred Credentials, region, vcnID string) error EnableVCNIPv6(ctx context.Context, cred Credentials, region, vcnID string) ([]IPv6Step, error) // 子网增删改查。 ListSubnets(ctx context.Context, cred Credentials, region, vcnID string) ([]Subnet, error) CreateSubnet(ctx context.Context, cred Credentials, in CreateSubnetInput) (Subnet, error) GetSubnet(ctx context.Context, cred Credentials, region, subnetID string) (Subnet, error) UpdateSubnet(ctx context.Context, cred Credentials, region, subnetID, displayName string) (Subnet, error) DeleteSubnet(ctx context.Context, cred Credentials, region, subnetID string) error // 安全列表增删改查。 ListSecurityLists(ctx context.Context, cred Credentials, region, vcnID string) ([]SecurityList, error) CreateSecurityList(ctx context.Context, cred Credentials, in CreateSecurityListInput) (SecurityList, error) GetSecurityList(ctx context.Context, cred Credentials, region, securityListID string) (SecurityList, error) UpdateSecurityList(ctx context.Context, cred Credentials, securityListID string, in UpdateSecurityListInput) (SecurityList, error) DeleteSecurityList(ctx context.Context, cred Credentials, region, securityListID string) error // ListAvailabilityDomains 列出区域内可用域,创建实例的前置查询。 ListAvailabilityDomains(ctx context.Context, cred Credentials, region string) ([]string, error) // 实例增删改查与电源操作。 ListInstances(ctx context.Context, cred Credentials, region string) ([]Instance, error) LaunchInstance(ctx context.Context, cred Credentials, in CreateInstanceInput) (Instance, error) GetInstance(ctx context.Context, cred Credentials, region, instanceID string) (Instance, error) UpdateInstance(ctx context.Context, cred Credentials, instanceID string, in UpdateInstanceInput) (Instance, error) TerminateInstance(ctx context.Context, cred Credentials, region, instanceID string, preserveBootVolume bool) error InstanceAction(ctx context.Context, cred Credentials, region, instanceID, action string) (Instance, error) // 引导卷查删改;引导卷由创建实例产生,不单独创建。 ListBootVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BootVolume, error) GetBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) (BootVolume, error) UpdateBootVolume(ctx context.Context, cred Credentials, bootVolumeID string, in UpdateBootVolumeInput) (BootVolume, error) DeleteBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) error // 实例 IP:更换主 VNIC 临时公网 IP、添加与取消分配 IPv6 地址。 ChangeInstancePublicIP(ctx context.Context, cred Credentials, region, instanceID string) (string, error) ChangeVnicPublicIP(ctx context.Context, cred Credentials, region, vnicID string) (string, error) // 对象存储:namespace、桶 CRUD、对象列表/删除/重命名/取回/元数据、预签名请求(PAR)。 GetObjectStorageNamespace(ctx context.Context, cred Credentials, region string) (string, error) ListBuckets(ctx context.Context, cred Credentials, region, compartmentID string) ([]Bucket, error) CreateBucket(ctx context.Context, cred Credentials, region string, in CreateBucketInput) (Bucket, error) UpdateBucket(ctx context.Context, cred Credentials, region, name string, in UpdateBucketInput) (Bucket, error) DeleteBucket(ctx context.Context, cred Credentials, region, name string) error // AbortAllMultipartUploads 中止桶内全部未完成分片上传(清空删桶前置步骤,404 幂等) AbortAllMultipartUploads(ctx context.Context, cred Credentials, region, bucket string) error ListObjects(ctx context.Context, cred Credentials, region, bucket, prefix, startWith string, limit int) (ListObjectsResult, error) ListObjectVersions(ctx context.Context, cred Credentials, region, bucket, page string) ([]ObjectVersion, string, error) DeleteObjectVersion(ctx context.Context, cred Credentials, region, bucket, object, versionID string) error DeleteObject(ctx context.Context, cred Credentials, region, bucket, object string) error RenameObject(ctx context.Context, cred Credentials, region, bucket, src, dst string) error RestoreObject(ctx context.Context, cred Credentials, region, bucket, object string, hours int) error HeadObject(ctx context.Context, cred Credentials, region, bucket, object string) (ObjectDetail, error) // 小文件中转:预览/编辑经面板直读直写,不签发 PAR;PutObject ifMatch 做并发保护。 GetObject(ctx context.Context, cred Credentials, region, bucket, object string, maxBytes int64) (ObjectContent, error) PutObject(ctx context.Context, cred Credentials, region, bucket, object string, data []byte, contentType, ifMatch string) (string, error) CreatePAR(ctx context.Context, cred Credentials, region, bucket string, in CreatePARInput) (PAR, error) ListPARs(ctx context.Context, cred Credentials, region, bucket string) ([]PAR, error) // ListPARsPage 单页列出 PAR:page 为上页游标(空取首页),返回下一页游标(空为末页)。 ListPARsPage(ctx context.Context, cred Credentials, region, bucket, page string, limit int) ([]PAR, string, error) DeletePAR(ctx context.Context, cred Credentials, region, bucket, parID string) error // 保留公网 IP:列出 / 创建 / 绑定(instanceID 空为解绑) / 删除。 ListReservedIPs(ctx context.Context, cred Credentials, region, compartmentID string) ([]ReservedIP, error) CreateReservedIP(ctx context.Context, cred Credentials, region, compartmentID, displayName string) (ReservedIP, error) AssignReservedIP(ctx context.Context, cred Credentials, region, publicIPID, instanceID string) error AssignReservedIPToVnic(ctx context.Context, cred Credentials, region, publicIPID, vnicID string) error DeleteReservedIP(ctx context.Context, cred Credentials, region, publicIPID string) error AddInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) (string, error) DeleteInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) error // VNIC 管理:列出实例网卡、附加次要网卡、分离(主网卡由 OCI 拒绝)、按网卡加 IPv6。 ListInstanceVnics(ctx context.Context, cred Credentials, region, instanceID string) ([]Vnic, error) AttachVnic(ctx context.Context, cred Credentials, region, instanceID string, in AttachVnicInput) (Vnic, error) DetachVnic(ctx context.Context, cred Credentials, region, attachmentID string) error AddVnicIpv6(ctx context.Context, cred Credentials, region, vnicID, address string) (string, error) // GenAI 网关:区域模型列表、聊天(非流式/流式)、配额探测、文本向量化。 ListGenAiModels(ctx context.Context, cred Credentials, region string) ([]GenAiModel, error) GenAiProbeChat(ctx context.Context, cred Credentials, region, modelOcid, modelName string) (int, error) GenAiEmbed(ctx context.Context, cred Credentials, region, modelOcid string, inputs []string, dimensions *int) ([][]float32, *aiwire.Usage, error) // GenAiCompatResponses 直通 OpenAI Responses 请求体到 /actions/v1/responses(xAI 服务端工具通路); // wait 是上游无响应预算(整请求总超时),multi-agent/搜索类模型需远超 SDK 默认 60s。 GenAiCompatResponses(ctx context.Context, cred Credentials, region string, body []byte, wait time.Duration) ([]byte, error) // GenAiCompatResponsesStream 流式直通 /actions/v1/responses,建立成功返回 SSE body; // wait 仅约束等待响应头阶段,建立后的流生命周期由 ctx 决定。 GenAiCompatResponsesStream(ctx context.Context, cred Credentials, region string, body []byte, wait time.Duration) (io.ReadCloser, error) // GenAiCompatSpeech 直通 OpenAI Audio Speech 请求体到 /openai/v1/audio/speech,返回音频与 Content-Type。 GenAiCompatSpeech(ctx context.Context, cred Credentials, region string, body []byte) ([]byte, string, error) // GenAiRerank 文档重排,返回按相关度排序的下标与得分。 GenAiRerank(ctx context.Context, cred Credentials, region, modelOcid, query string, documents []string, topN *int) ([]RerankRank, error) // GenAiApplyGuardrails 对单条文本执行内容审核 / PII / 提示注入检测。 GenAiApplyGuardrails(ctx context.Context, cred Credentials, region, text string) (*GuardrailsOutcome, error) // 控制台连接:创建(VNC/串口连接串)、列出、删除。 CreateConsoleConnection(ctx context.Context, cred Credentials, region, instanceID, sshPublicKey string) (ConsoleConnection, error) ListConsoleConnections(ctx context.Context, cred Credentials, region, instanceID string) ([]ConsoleConnection, error) DeleteConsoleConnection(ctx context.Context, cred Credentials, region, connectionID string) error // 引导卷挂载:列出、挂载、分离、替换。 ListBootVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]BootVolumeAttachment, error) AttachBootVolume(ctx context.Context, cred Credentials, region, instanceID, bootVolumeID string) (BootVolumeAttachment, error) DetachBootVolume(ctx context.Context, cred Credentials, region, attachmentID string) error ReplaceBootVolume(ctx context.Context, cred Credentials, region, instanceID, newBootVolumeID string) (BootVolumeAttachment, error) // 块卷挂载:列出块卷、列出挂载、附加、分离。 ListBlockVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BlockVolume, error) ListVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]VolumeAttachment, error) AttachVolume(ctx context.Context, cred Credentials, region, instanceID, volumeID string, readOnly bool) (VolumeAttachment, error) DetachVolume(ctx context.Context, cred Credentials, region, attachmentID string) error // 租户观测:实例流量统计、成本分析。 SummarizeInstanceTraffic(ctx context.Context, cred Credentials, q TrafficQuery) (InstanceTraffic, error) SummarizeCosts(ctx context.Context, cred Credentials, q CostQuery) ([]CostItem, error) // ListAuditEvents 实时查询租户根 compartment 的审计事件(最多翻 maxAuditPages 页, // 到限截断并回传续查游标);page 传上次结果的 NextPage 可从断点续翻。 // 批式懒加载走 ListAuditEventsBatch,本方法保留给详情小窗反查。 ListAuditEvents(ctx context.Context, cred Credentials, region string, start, end time.Time, page string) (AuditEventsResult, error) // ListAuditEventsBatch 从游标位置向更早方向收集约 limit 条保留事件, // 分窗回溯(空窗倍增),跨批不重不漏;到 365 天保留期尽头置 Exhausted。 ListAuditEventsBatch(ctx context.Context, cred Credentials, region string, cur AuditCursor, limit int) (AuditBatchResult, error) // 身份域:租户 ACTIVE 域枚举(多域租户选择器数据源)。 ListIdentityDomains(ctx context.Context, cred Credentials, region string) ([]IdentityDomain, error) // 账户能力:暂停标记/账户状态/免费层信息(对暂停租户仍可访问,测活据此判「暂停」)。 GetAccountCapabilities(ctx context.Context, cred Credentials, region string) (AccountCapabilities, error) // 租户用户管理:domainID 非空按域走 Identity Domains SCIM, // 为空保持经典 IAM 为主、新式租户自动回退的旧行为。 ListTenantUsers(ctx context.Context, cred Credentials, homeRegion, domainID string) ([]TenantUser, error) GetTenantUserDetail(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) (TenantUserDetail, error) CreateTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID string, in CreateTenantUserInput) (TenantUser, error) UpdateTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID, userID string, in UpdateTenantUserInput) (TenantUser, error) DeleteTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) error ResetTenantUserPassword(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) (string, error) DeleteTenantUserMfaDevices(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) (int, error) DeleteTenantUserApiKeys(ctx context.Context, cred Credentials, homeRegion, userID string, includeCurrent bool) (int, error) // 用户 API 签名 key 管理:列出(标注当前使用)、上传公钥(返回指纹)、按指纹删单把。 ListTenantUserApiKeys(ctx context.Context, cred Credentials, homeRegion, userID string) ([]TenantUserApiKey, error) UploadTenantUserApiKey(ctx context.Context, cred Credentials, homeRegion, userID, publicKeyPEM string) (string, error) DeleteTenantUserApiKey(ctx context.Context, cred Credentials, homeRegion, userID, fingerprint string) error // 域设置:通知收件人、密码策略、身份设置。 GetNotificationRecipients(ctx context.Context, cred Credentials, region, domainID string) (NotificationRecipients, error) UpdateNotificationRecipients(ctx context.Context, cred Credentials, region, domainID string, emails []string) (NotificationRecipients, error) ListPasswordPolicies(ctx context.Context, cred Credentials, region, domainID string) ([]PasswordPolicyInfo, error) UpdatePasswordPolicy(ctx context.Context, cred Credentials, region, domainID, policyID string, in UpdatePasswordPolicyInput) (PasswordPolicyInfo, error) GetIdentitySetting(ctx context.Context, cred Credentials, region, domainID string) (IdentitySettingInfo, error) UpdateIdentitySetting(ctx context.Context, cred Credentials, region, domainID string, primaryEmailRequired bool) (IdentitySettingInfo, error) // Federation:SAML IdP 管理、域 SP 元数据、sign-on 免 MFA 规则。 ListIdentityProviders(ctx context.Context, cred Credentials, region, domainID string) ([]IdentityProviderInfo, error) CreateSamlIdentityProvider(ctx context.Context, cred Credentials, region, domainID string, in CreateIdpInput) (IdentityProviderInfo, error) SetIdentityProviderEnabled(ctx context.Context, cred Credentials, region, domainID, idpID string, enabled bool) (IdentityProviderInfo, error) DeleteIdentityProvider(ctx context.Context, cred Credentials, region, domainID, idpID string) error DownloadDomainSamlMetadata(ctx context.Context, cred Credentials, region, domainID string) ([]byte, error) // UploadDomainImage 上传公开图片到身份域存储(IdP 图标用),返回公网 URL 与存储内文件名。 UploadDomainImage(ctx context.Context, cred Credentials, region, domainID, fileName string, data []byte) (string, string, error) // DeleteDomainImage 按存储内文件名删除身份域公开图片。 DeleteDomainImage(ctx context.Context, cred Credentials, region, domainID, fileName string) error ListConsoleSignOnRules(ctx context.Context, cred Credentials, region, domainID string) ([]SignOnRuleInfo, error) CreateMfaExemptionRule(ctx context.Context, cred Credentials, region, domainID, idpID, ruleName string) (SignOnRuleInfo, error) DeleteMfaExemptionRule(ctx context.Context, cred Credentials, region, domainID, ruleID string) error // 日志回传链路(方案A):Topic/CUSTOM_HTTPS 订阅/IAM Policy/Service Connector // 的幂等创建、状态聚合与销毁;IAM 写操作发往 homeRegion。 EnsureRelayTopic(ctx context.Context, cred Credentials) (RelayResource, error) EnsureRelaySubscription(ctx context.Context, cred Credentials, topicID, endpoint string) (RelayResource, error) GetRelaySubscription(ctx context.Context, cred Credentials, subscriptionID string) (RelayResource, error) EnsureRelayPolicy(ctx context.Context, cred Credentials, homeRegion string) (RelayResource, error) EnsureRelayConnector(ctx context.Context, cred Credentials, topicID, condition string) (RelayResource, error) RelayState(ctx context.Context, cred Credentials, endpoint string) (RelayState, error) DeleteRelayConnector(ctx context.Context, cred Credentials, connectorID string) error DeleteRelayPolicy(ctx context.Context, cred Credentials, homeRegion, policyID string) error DeleteRelaySubscription(ctx context.Context, cred Credentials, subscriptionID string) error DeleteRelayTopic(ctx context.Context, cred Credentials, topicID string) error } // RealClient 基于官方 SDK 实现 Client。 type RealClient struct { limitDefs sync.Map // tenancy|region|service → limitDefEntry,配额定义预筛缓存 shapes sync.Map // tenancy|region|ad → shapeCacheEntry,shape 清单缓存 namespaces sync.Map // tenancy → 对象存储 namespace,租户常量不过期 homeRegions sync.Map // tenancy → 主区域公共名,租户常量不过期(OSP 账单用) } // NewClient 返回生产使用的真实客户端。 func NewClient() *RealClient { return &RealClient{} } func provider(cred Credentials) common.ConfigurationProvider { var pass *string if cred.Passphrase != "" { pass = &cred.Passphrase } return common.NewRawConfigurationProvider( cred.TenancyOCID, cred.UserOCID, cred.Region, cred.Fingerprint, cred.PrivateKey, pass, ) } // normalizeRegion 兼容区域短名:iad → us-ashburn-1;空值与全名原样返回。 // 实例等资源对象上的 region 字段是短名,直接透传时借此归一。 func normalizeRegion(region string) string { if region == "" { return "" } return string(common.StringToRegion(region)) } // ValidateKey 实现 Client。 func (c *RealClient) ValidateKey(ctx context.Context, cred Credentials) (TenancyInfo, error) { ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred)) if err != nil { return TenancyInfo{}, fmt.Errorf("new identity client: %w", err) } applyProxy(&ic.BaseClient, cred) resp, err := ic.GetTenancy(ctx, identity.GetTenancyRequest{TenancyId: &cred.TenancyOCID}) if err != nil { return TenancyInfo{}, fmt.Errorf("get tenancy: %w", err) } return TenancyInfo{ Name: deref(resp.Name), HomeRegionKey: deref(resp.HomeRegionKey), }, nil } // FetchAccountProfile 实现 Client:从订阅列表选中 CLOUDCM, // 再查询完整订阅并按付费模式、促销状态判定账户类别。 func (c *RealClient) FetchAccountProfile(ctx context.Context, cred Credentials) (AccountProfile, error) { subs, err := c.ListSubscriptions(ctx, cred) if err != nil { return AccountProfile{}, err } subID := "" for _, sub := range subs { if strings.EqualFold(sub.ServiceName, cloudcmServiceName) { subID = sub.ID break } } if subID == "" { return AccountProfile{}, ErrNoCloudcmSubscription } detail, err := c.GetSubscription(ctx, cred, subID) if err != nil { return AccountProfile{}, err } return buildAccountProfile(detail), nil } func buildAccountProfile(detail SubscriptionDetail) AccountProfile { p := AccountProfile{ SubscriptionID: detail.ID, ServiceName: detail.ServiceName, PaymentModel: detail.PaymentModel, SubscriptionTier: detail.SubscriptionTier, } p.PromotionStatus, p.PromotionAmount, p.PromotionExpires = pickPromotion(detail.Promotions) p.AccountType = ClassifyAccount(p.PaymentModel, p.SubscriptionTier, p.PromotionStatus) return p } // pickPromotion 从多条促销记录中选出最能代表当前状态的一条: // 优先 ACTIVE,其次 EXPIRED,否则回退第一条。 func pickPromotion(promos []PromotionInfo) (string, float32, *time.Time) { best, bestRank := -1, -1 for i, p := range promos { if rank := promotionRank(p.Status); rank > bestRank { best, bestRank = i, rank } } if best < 0 { return "", 0, nil } p := promos[best] return p.Status, p.Amount, p.TimeExpired } func promotionRank(status string) int { switch strings.ToUpper(status) { case "ACTIVE": return 2 case "EXPIRED": return 1 default: return 0 } } func deref(s *string) string { if s != nil { return *s } return "" } // orEmpty 把 nil 切片归一为空切片:JSON 序列化输出 [] 而非 null, // 避免前端对 null 调用 .length / .map / .join 崩溃。 func orEmpty[T any](s []T) []T { if s == nil { return []T{} } return s }