//go:build llm_script package main import ( "fmt" "regexp" "strings" ) const ( defaultCTYunCodingPlanURL = "https://www.ctyun.cn/document/11061839/11092368" defaultCTYunTokenPlanURL = "https://www.ctyun.cn/act/AI/zhuanxiang" ) func parseCTYunSubscriptionCatalog(codingRaw string, tokenRaw string) ([]subscriptionImportRecord, error) { publishedAt, known := publishedAtFromText(firstNonEmptyText(codingRaw, tokenRaw)) codingRecords, err := parseCTYunCodingPlan(codingRaw, publishedAt) if err != nil { return nil, err } tokenRecords, err := parseCTYunTokenPlan(tokenRaw, publishedAt) if err != nil { return nil, err } records := append(codingRecords, tokenRecords...) for i := range records { records[i].PublishedAtKnown = known } return records, nil } func parseCTYunCodingPlan(raw string, publishedAt string) ([]subscriptionImportRecord, error) { if !strings.Contains(raw, "GLM Lite") || !strings.Contains(raw, "GLM Max") { return nil, fmt.Errorf("ctyun coding plan tiers not found") } pricePattern := regexp.MustCompile(`包月价格\s+(\d+)元/月\s+(\d+)元/月\s+(\d+)元/月`) priceMatch := pricePattern.FindStringSubmatch(raw) if len(priceMatch) != 4 { return nil, fmt.Errorf("ctyun coding plan monthly prices not found") } limitPattern := regexp.MustCompile(`每月最多约([\d,]+)次prompts`) limitMatches := limitPattern.FindAllStringSubmatch(raw, -1) if len(limitMatches) < 3 { return nil, fmt.Errorf("ctyun coding plan monthly limits not found") } modelScope := extractCTYunCodingModels(raw) records := []subscriptionImportRecord{ { ProviderName: "Telecom", ProviderNameCn: "中国电信", ProviderCountry: "CN", ProviderWebsite: "https://www.ctyun.cn", OperatorName: "CTYun", OperatorNameCn: "天翼云", OperatorCountry: "CN", OperatorWebsite: "https://www.ctyun.cn", OperatorType: "cloud", PlanFamily: "coding_plan", PlanCode: "ctyun-coding-plan-lite-monthly", PlanName: "天翼云 Coding Plan Lite(月付)", Tier: "Lite", BillingCycle: "monthly", Currency: "CNY", ListPrice: mustParseSubscriptionPrice(priceMatch[1]), PriceUnit: "CNY/month", QuotaValue: mustParseSubscriptionInt64(limitMatches[0][1]), QuotaUnit: "prompts/month", PlanScope: "Coding Plan", ModelScope: modelScope, SourceURL: defaultCTYunCodingPlanURL, PublishedAt: publishedAt, EffectiveDate: effectiveDateFromPublishedAt(publishedAt), Notes: "每 5 小时约 80 次 prompts;每周约 400 次 prompts。", }, { ProviderName: "Telecom", ProviderNameCn: "中国电信", ProviderCountry: "CN", ProviderWebsite: "https://www.ctyun.cn", OperatorName: "CTYun", OperatorNameCn: "天翼云", OperatorCountry: "CN", OperatorWebsite: "https://www.ctyun.cn", OperatorType: "cloud", PlanFamily: "coding_plan", PlanCode: "ctyun-coding-plan-pro-monthly", PlanName: "天翼云 Coding Plan Pro(月付)", Tier: "Pro", BillingCycle: "monthly", Currency: "CNY", ListPrice: mustParseSubscriptionPrice(priceMatch[2]), PriceUnit: "CNY/month", QuotaValue: mustParseSubscriptionInt64(limitMatches[1][1]), QuotaUnit: "prompts/month", PlanScope: "Coding Plan", ModelScope: modelScope, SourceURL: defaultCTYunCodingPlanURL, PublishedAt: publishedAt, EffectiveDate: effectiveDateFromPublishedAt(publishedAt), Notes: "每 5 小时约 400 次 prompts;每周约 2,000 次 prompts。", }, { ProviderName: "Telecom", ProviderNameCn: "中国电信", ProviderCountry: "CN", ProviderWebsite: "https://www.ctyun.cn", OperatorName: "CTYun", OperatorNameCn: "天翼云", OperatorCountry: "CN", OperatorWebsite: "https://www.ctyun.cn", OperatorType: "cloud", PlanFamily: "coding_plan", PlanCode: "ctyun-coding-plan-max-monthly", PlanName: "天翼云 Coding Plan Max(月付)", Tier: "Max", BillingCycle: "monthly", Currency: "CNY", ListPrice: mustParseSubscriptionPrice(priceMatch[3]), PriceUnit: "CNY/month", QuotaValue: mustParseSubscriptionInt64(limitMatches[2][1]), QuotaUnit: "prompts/month", PlanScope: "Coding Plan", ModelScope: modelScope, SourceURL: defaultCTYunCodingPlanURL, PublishedAt: publishedAt, EffectiveDate: effectiveDateFromPublishedAt(publishedAt), Notes: "每 5 小时约 1,600 次 prompts;每周约 8,000 次 prompts。", }, } return records, nil } func parseCTYunTokenPlan(raw string, publishedAt string) ([]subscriptionImportRecord, error) { pattern := regexp.MustCompile(`Token Plan ([^\n]+?)(\d+(?:\.\d+)?亿|\d+万)Tokens包[\s\S]*?支持模型:([^\n]+)[\s\S]*?(\d+\s*\.\s*\d+)\s*元/个`) matches := pattern.FindAllStringSubmatch(raw, -1) if len(matches) != 6 { return nil, fmt.Errorf("unexpected ctyun token plan count: %d", len(matches)) } codeByTier := map[string]string{ "Lite": "lite", "Pro": "pro", "Max": "max", "轻享包": "starter", "畅享包": "plus", "尊享包": "vip", } records := make([]subscriptionImportRecord, 0, len(matches)) for _, match := range matches { rawTier := strings.TrimSpace(match[1]) tierCode := codeByTier[rawTier] quotaValue := parseChineseTokenQuota(match[2]) price := mustParseSubscriptionPrice(strings.ReplaceAll(match[4], " ", "")) planName := "天翼云 Token Plan " + rawTier if rawTier == "Lite" || rawTier == "Pro" || rawTier == "Max" { planName = "天翼云 Token Plan " + rawTier } records = append(records, subscriptionImportRecord{ ProviderName: "Telecom", ProviderNameCn: "中国电信", ProviderCountry: "CN", ProviderWebsite: "https://www.ctyun.cn", OperatorName: "CTYun", OperatorNameCn: "天翼云", OperatorCountry: "CN", OperatorWebsite: "https://www.ctyun.cn", OperatorType: "cloud", PlanFamily: "token_plan", PlanCode: "ctyun-token-plan-" + tierCode, PlanName: planName, Tier: rawTier, BillingCycle: "monthly", Currency: "CNY", ListPrice: price, PriceUnit: "CNY/pack", QuotaValue: quotaValue, QuotaUnit: "tokens/pack", PlanScope: "Token Plan", ModelScope: []string{strings.TrimSpace(match[3])}, SourceURL: defaultCTYunTokenPlanURL, PublishedAt: publishedAt, EffectiveDate: effectiveDateFromPublishedAt(publishedAt), Notes: "天翼云大模型 AI 专项活动页套餐。", }) } return records, nil } func parseChineseTokenQuota(raw string) int64 { cleaned := strings.TrimSpace(strings.TrimSuffix(raw, "Tokens包")) cleaned = strings.ReplaceAll(cleaned, " ", "") switch { case strings.Contains(cleaned, "亿"): return parseDecimalMultiplier(strings.TrimSuffix(cleaned, "亿"), 100000000) case strings.Contains(cleaned, "万"): return parseDecimalMultiplier(strings.TrimSuffix(cleaned, "万"), 10000) default: return mustParseSubscriptionInt64(cleaned) } } func extractCTYunCodingModels(raw string) []string { lines := strings.Split(raw, "\n") models := make([]string, 0, 8) capturing := false for _, line := range lines { line = strings.TrimSpace(line) switch { case line == "支持模型": capturing = true continue case line == "用量限制": return models case !capturing || line == "": continue default: models = append(models, line) } } return models }