chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。 - app/:uni-app 小程序前端(基于 unibest,MIT) - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID - 内网后端地址 → admin.iotlcm.example.com - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明) - server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0) - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件) - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化 - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位) - 依赖号码尾部的断言同步更新 - 补 server/LICENSE(Apache-2.0) - 移除内部资产:CI 触发器 - 补根 README,LF 行尾 + .gitattributes 已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
internalai "git.5iot.com/iotlcm-public/iotlcm-app/internal/ai"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
const (
|
||||
cardRecognitionCodeRecognized = "CARD_RECOGNIZED"
|
||||
cardRecognitionCodeAmbiguous = "CARD_AMBIGUOUS"
|
||||
cardRecognitionCodeUnreadable = "CARD_UNREADABLE"
|
||||
cardRecognitionCodeTimeout = "RECOGNITION_TIMEOUT"
|
||||
cardRecognitionCodeFailed = "RECOGNITION_FAILED"
|
||||
cardRecognitionCodeDisabled = "RECOGNITION_DISABLED"
|
||||
cardRecognitionCodeRateLimited = "RECOGNITION_RATE_LIMITED"
|
||||
|
||||
cardRecognitionDefaultBaseURL = "https://xueyangai.com/v1"
|
||||
cardRecognitionDefaultModel = "gpt-4o"
|
||||
cardRecognitionDefaultMaxImageSize = int64(8 * 1024 * 1024)
|
||||
cardRecognitionDefaultMaxTokens = 256
|
||||
cardRecognitionDefaultRateCount = 20
|
||||
cardRecognitionDefaultRatePeriod = time.Minute
|
||||
cardRecognitionRateLimitRedisKey = "iotlcm:app:card-recognition:rate:"
|
||||
)
|
||||
|
||||
var cardRecognitionCandidatePattern = regexp.MustCompile(`(^|[^0-9])(89([-[:space:]]*[0-9]){17,18})($|[^0-9])`)
|
||||
|
||||
type cardRecognitionConfig struct {
|
||||
Enabled bool
|
||||
Provider string
|
||||
APIKey string
|
||||
BaseURL string
|
||||
Endpoint string
|
||||
Model string
|
||||
Timeout time.Duration
|
||||
MaxImageBytes int64
|
||||
MaxTokens int
|
||||
RateLimitEnabled bool
|
||||
RateLimitCount int
|
||||
RateLimitPeriod time.Duration
|
||||
}
|
||||
|
||||
type cardRecognitionModelOutput struct {
|
||||
Code string `json:"code"`
|
||||
CardType string `json:"cardType"`
|
||||
Message string `json:"message"`
|
||||
ICCID string `json:"iccid"`
|
||||
CardValue string `json:"cardValue"`
|
||||
Candidates []string `json:"candidates"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
type cardRecognitionParsedResult struct {
|
||||
Code string
|
||||
Candidates []string
|
||||
Confidence float64
|
||||
}
|
||||
|
||||
func (s *sAppCard) RecognizeCard(ctx context.Context) (*model.AppCardRecognitionModel, error) {
|
||||
result := &model.AppCardRecognitionModel{
|
||||
Code: cardRecognitionCodeFailed,
|
||||
CardType: "iccid",
|
||||
Candidates: []string{},
|
||||
}
|
||||
|
||||
req := g.RequestFromCtx(ctx)
|
||||
if req == nil {
|
||||
result.Message = "无法读取上传请求,请稍后重试"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
file := req.GetUploadFile("file")
|
||||
if file == nil {
|
||||
result.Code = cardRecognitionCodeUnreadable
|
||||
result.Message = "请上传一张包含 ICCID 的图片"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
config := loadCardRecognitionConfig(ctx)
|
||||
if !config.Enabled || config.APIKey == "" {
|
||||
result.Code = cardRecognitionCodeDisabled
|
||||
result.Message = "图片识别服务暂未配置"
|
||||
return result, nil
|
||||
}
|
||||
allowed, err := checkCardRecognitionRateLimit(ctx, config, req)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "card recognition rate limit check failed: %v", err)
|
||||
} else if !allowed {
|
||||
result.Code = cardRecognitionCodeRateLimited
|
||||
result.Message = "图片识别请求过于频繁,请稍后再试"
|
||||
return result, nil
|
||||
}
|
||||
content, contentType, err := readCardRecognitionImage(file, config.MaxImageBytes)
|
||||
if err != nil {
|
||||
result.Code = cardRecognitionCodeUnreadable
|
||||
result.Message = err.Error()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rawContent, err := requestCardRecognitionModel(ctx, config, content, contentType)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "card recognition model request failed: %v", err)
|
||||
if isCardRecognitionTimeoutError(ctx, err) {
|
||||
result.Code = cardRecognitionCodeTimeout
|
||||
result.Message = "图片识别超时,请稍后重试"
|
||||
} else {
|
||||
result.Code = cardRecognitionCodeFailed
|
||||
result.Message = "图片识别服务暂时不可用,请稍后重试"
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
parsed := parseCardRecognitionResult(rawContent)
|
||||
result.Confidence = parsed.Confidence
|
||||
result.Candidates = parsed.Candidates
|
||||
switch {
|
||||
case parsed.Code == cardRecognitionCodeUnreadable && len(parsed.Candidates) == 0:
|
||||
result.Code = cardRecognitionCodeUnreadable
|
||||
result.Message = "未能从图片中识别出清晰的 ICCID,请重新拍摄"
|
||||
return result, nil
|
||||
case parsed.Code == cardRecognitionCodeAmbiguous && len(parsed.Candidates) > 1:
|
||||
result.Code = cardRecognitionCodeAmbiguous
|
||||
result.Message = "图片中存在多个可能的 ICCID,请重新拍摄单张卡片"
|
||||
return result, nil
|
||||
}
|
||||
switch len(parsed.Candidates) {
|
||||
case 0:
|
||||
result.Code = cardRecognitionCodeUnreadable
|
||||
result.Message = "未能从图片中识别出清晰的 ICCID,请重新拍摄"
|
||||
return result, nil
|
||||
case 1:
|
||||
result.CardValue = parsed.Candidates[0]
|
||||
result.Code = cardRecognitionCodeRecognized
|
||||
result.Message = "已识别出 ICCID"
|
||||
return result, nil
|
||||
default:
|
||||
result.Code = cardRecognitionCodeAmbiguous
|
||||
result.Message = "图片中存在多个可能的 ICCID,请重新拍摄单张卡片"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func loadCardRecognitionConfig(ctx context.Context) cardRecognitionConfig {
|
||||
timeout := 25 * time.Second
|
||||
if value := strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.timeout", "25s").String()); value != "" {
|
||||
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
|
||||
timeout = parsed
|
||||
}
|
||||
}
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_TIMEOUT"); value != "" {
|
||||
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
|
||||
timeout = parsed
|
||||
}
|
||||
}
|
||||
|
||||
maxImageBytes := g.Cfg().MustGet(ctx, "appCardRecognition.maxImageBytes", cardRecognitionDefaultMaxImageSize).Int64()
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_MAX_IMAGE_BYTES"); value != "" {
|
||||
if parsed, err := strconv.ParseInt(value, 10, 64); err == nil && parsed > 0 {
|
||||
maxImageBytes = parsed
|
||||
}
|
||||
}
|
||||
if maxImageBytes <= 0 {
|
||||
maxImageBytes = cardRecognitionDefaultMaxImageSize
|
||||
}
|
||||
|
||||
maxTokens := g.Cfg().MustGet(ctx, "appCardRecognition.maxTokens", cardRecognitionDefaultMaxTokens).Int()
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_MAX_TOKENS"); value != "" {
|
||||
if parsed, err := strconv.Atoi(value); err == nil && parsed > 0 {
|
||||
maxTokens = parsed
|
||||
}
|
||||
}
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = cardRecognitionDefaultMaxTokens
|
||||
}
|
||||
|
||||
rateLimitPeriod := cardRecognitionDefaultRatePeriod
|
||||
if value := strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.rateLimit.period", rateLimitPeriod.String()).String()); value != "" {
|
||||
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
|
||||
rateLimitPeriod = parsed
|
||||
}
|
||||
}
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_RATE_LIMIT_PERIOD"); value != "" {
|
||||
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
|
||||
rateLimitPeriod = parsed
|
||||
}
|
||||
}
|
||||
|
||||
rateLimitCount := g.Cfg().MustGet(ctx, "appCardRecognition.rateLimit.count", cardRecognitionDefaultRateCount).Int()
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_RATE_LIMIT_COUNT"); value != "" {
|
||||
if parsed, err := strconv.Atoi(value); err == nil && parsed > 0 {
|
||||
rateLimitCount = parsed
|
||||
}
|
||||
}
|
||||
if rateLimitCount <= 0 {
|
||||
rateLimitCount = cardRecognitionDefaultRateCount
|
||||
}
|
||||
baseURL := appEnvFirst("APP_CARD_RECOGNITION_BASE_URL")
|
||||
endpoint := appEnvFirst("APP_CARD_RECOGNITION_ENDPOINT")
|
||||
if baseURL == "" && endpoint == "" {
|
||||
baseURL = strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.baseUrl", "").String())
|
||||
endpoint = strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.endpoint", "").String())
|
||||
}
|
||||
if baseURL == "" && endpoint == "" {
|
||||
baseURL = cardRecognitionDefaultBaseURL
|
||||
}
|
||||
modelName := strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.model", cardRecognitionDefaultModel).String())
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_MODEL"); value != "" {
|
||||
modelName = value
|
||||
}
|
||||
if modelName == "" {
|
||||
modelName = cardRecognitionDefaultModel
|
||||
}
|
||||
apiKey := strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.apiKey", "").String())
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_API_KEY", "XUEYANGAI_API_KEY"); value != "" {
|
||||
apiKey = value
|
||||
}
|
||||
|
||||
provider := appEnvFirst("APP_CARD_RECOGNITION_PROVIDER")
|
||||
if provider == "" {
|
||||
provider = strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.provider", internalai.ProviderOpenAICompatible).String())
|
||||
}
|
||||
|
||||
return cardRecognitionConfig{
|
||||
Enabled: appEnvBool("APP_CARD_RECOGNITION_ENABLED", g.Cfg().MustGet(ctx, "appCardRecognition.enabled", false).Bool()),
|
||||
Provider: provider,
|
||||
APIKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
Endpoint: endpoint,
|
||||
Model: modelName,
|
||||
Timeout: timeout,
|
||||
MaxImageBytes: maxImageBytes,
|
||||
MaxTokens: maxTokens,
|
||||
RateLimitEnabled: appEnvBool("APP_CARD_RECOGNITION_RATE_LIMIT_ENABLED", g.Cfg().MustGet(ctx, "appCardRecognition.rateLimit.enabled", true).Bool()),
|
||||
RateLimitCount: rateLimitCount,
|
||||
RateLimitPeriod: rateLimitPeriod,
|
||||
}
|
||||
}
|
||||
|
||||
func readCardRecognitionImage(file *ghttp.UploadFile, maxImageBytes int64) ([]byte, string, error) {
|
||||
if file == nil || file.FileHeader == nil {
|
||||
return nil, "", gerror.New("上传图片无效")
|
||||
}
|
||||
if maxImageBytes <= 0 {
|
||||
maxImageBytes = cardRecognitionDefaultMaxImageSize
|
||||
}
|
||||
if file.Size > maxImageBytes {
|
||||
return nil, "", gerror.Newf("图片不能超过 %d MB", maxImageBytes/(1024*1024))
|
||||
}
|
||||
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, "", gerror.New("无法读取图片")
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
content, err := io.ReadAll(io.LimitReader(reader, maxImageBytes+1))
|
||||
if err != nil {
|
||||
return nil, "", gerror.New("无法读取图片")
|
||||
}
|
||||
if int64(len(content)) > maxImageBytes {
|
||||
return nil, "", gerror.Newf("图片不能超过 %d MB", maxImageBytes/(1024*1024))
|
||||
}
|
||||
|
||||
contentType := strings.ToLower(strings.TrimSpace(file.Header.Get("Content-Type")))
|
||||
if contentType == "" || contentType == "application/octet-stream" {
|
||||
contentType = http.DetectContentType(content)
|
||||
}
|
||||
switch contentType {
|
||||
case "image/jpeg", "image/png", "image/webp":
|
||||
default:
|
||||
return nil, "", gerror.New("仅支持 JPG、PNG 或 WEBP 图片")
|
||||
}
|
||||
return content, contentType, nil
|
||||
}
|
||||
|
||||
func checkCardRecognitionRateLimit(ctx context.Context, config cardRecognitionConfig, req *ghttp.Request) (bool, error) {
|
||||
if !config.RateLimitEnabled || config.RateLimitCount <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
periodSeconds := int(config.RateLimitPeriod.Seconds())
|
||||
if periodSeconds <= 0 {
|
||||
periodSeconds = int(cardRecognitionDefaultRatePeriod.Seconds())
|
||||
}
|
||||
clientIP := "unknown"
|
||||
if req != nil {
|
||||
if value := strings.TrimSpace(req.GetClientIp()); value != "" {
|
||||
clientIP = value
|
||||
}
|
||||
}
|
||||
key := cardRecognitionRateLimitRedisKey + cleanCardRecognitionRateLimitKey(clientIP)
|
||||
script := `
|
||||
local v = redis.call("INCR", KEYS[1])
|
||||
if v == 1 then
|
||||
redis.call("EXPIRE", KEYS[1], ARGV[1])
|
||||
end
|
||||
return v`
|
||||
res, err := g.Redis().Do(ctx, "EVAL", script, 1, key, periodSeconds)
|
||||
if err != nil {
|
||||
return true, gerror.Wrap(err, "检查图片识别限流失败")
|
||||
}
|
||||
return res.Int() <= config.RateLimitCount, nil
|
||||
}
|
||||
|
||||
func cleanCardRecognitionRateLimitKey(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, value)
|
||||
}
|
||||
|
||||
func requestCardRecognitionModel(ctx context.Context, config cardRecognitionConfig, content []byte, contentType string) (string, error) {
|
||||
requestCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
chatModel, err := internalai.NewChatModel(requestCtx, internalai.ChatModelConfig{
|
||||
Provider: config.Provider,
|
||||
APIKey: config.APIKey,
|
||||
BaseURL: config.BaseURL,
|
||||
Endpoint: config.Endpoint,
|
||||
Model: config.Model,
|
||||
Timeout: config.Timeout,
|
||||
MaxTokens: config.MaxTokens,
|
||||
JSONOutput: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(content)
|
||||
response, err := chatModel.Generate(requestCtx, []*schema.Message{
|
||||
{
|
||||
Role: schema.User,
|
||||
UserInputMultiContent: []schema.MessageInputPart{
|
||||
{
|
||||
Type: schema.ChatMessagePartTypeText,
|
||||
Text: "请只识别图片中物联网卡的 ICCID。忽略 MSISDN、IMSI、IMEI、订单号和其他数字。请仅返回 JSON,不要返回 Markdown 或解释:{\"code\":\"CARD_RECOGNIZED|CARD_AMBIGUOUS|CARD_UNREADABLE\",\"cardType\":\"iccid\",\"iccid\":\"19或20位数字,没有则为空\",\"candidates\":[\"候选ICCID\"],\"confidence\":0到1之间的小数}。图片模糊、遮挡、反光或无法确定时,iccid 置空并返回 CARD_UNREADABLE;有多个卡号时返回 CARD_AMBIGUOUS,并把全部候选 ICCID 放入 candidates。",
|
||||
},
|
||||
{
|
||||
Type: schema.ChatMessagePartTypeImageURL,
|
||||
Image: &schema.MessageInputImage{
|
||||
MessagePartCommon: schema.MessagePartCommon{
|
||||
Base64Data: &imageBase64,
|
||||
MIMEType: contentType,
|
||||
},
|
||||
Detail: schema.ImageURLDetailHigh,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "Eino调用图片识别模型失败")
|
||||
}
|
||||
if response == nil || strings.TrimSpace(response.Content) == "" {
|
||||
return "", gerror.New("图片识别响应为空")
|
||||
}
|
||||
return response.Content, nil
|
||||
}
|
||||
|
||||
func parseCardRecognitionResult(raw string) cardRecognitionParsedResult {
|
||||
raw = trimCardRecognitionJSON(raw)
|
||||
|
||||
output := cardRecognitionModelOutput{}
|
||||
if json.Unmarshal([]byte(raw), &output) == nil {
|
||||
rawValues := append([]string{output.ICCID, output.CardValue}, output.Candidates...)
|
||||
candidates := normalizeCardRecognitionCandidates(rawValues)
|
||||
code := normalizeCardRecognitionCode(output.Code)
|
||||
if code == "" {
|
||||
switch len(candidates) {
|
||||
case 0:
|
||||
code = cardRecognitionCodeUnreadable
|
||||
case 1:
|
||||
code = cardRecognitionCodeRecognized
|
||||
default:
|
||||
code = cardRecognitionCodeAmbiguous
|
||||
}
|
||||
}
|
||||
return cardRecognitionParsedResult{
|
||||
Code: code,
|
||||
Candidates: candidates,
|
||||
Confidence: clampCardRecognitionConfidence(output.Confidence),
|
||||
}
|
||||
}
|
||||
candidates := normalizeCardRecognitionCandidates([]string{raw})
|
||||
code := cardRecognitionCodeUnreadable
|
||||
if len(candidates) == 1 {
|
||||
code = cardRecognitionCodeRecognized
|
||||
} else if len(candidates) > 1 {
|
||||
code = cardRecognitionCodeAmbiguous
|
||||
}
|
||||
return cardRecognitionParsedResult{
|
||||
Code: code,
|
||||
Candidates: candidates,
|
||||
Confidence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCardRecognitionCandidates(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
for _, candidate := range extractCardRecognitionCandidates(value) {
|
||||
if _, ok := seen[candidate]; ok {
|
||||
continue
|
||||
}
|
||||
seen[candidate] = struct{}{}
|
||||
result = append(result, candidate)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func extractCardRecognitionCandidates(value string) []string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]string, 0, 1)
|
||||
if cleaned := cleanCardRecognitionCandidate(value); isValidCardRecognitionCandidate(cleaned) {
|
||||
result = append(result, cleaned)
|
||||
}
|
||||
for _, match := range cardRecognitionCandidatePattern.FindAllStringSubmatch(value, -1) {
|
||||
if len(match) < 3 {
|
||||
continue
|
||||
}
|
||||
if cleaned := cleanCardRecognitionCandidate(match[2]); isValidCardRecognitionCandidate(cleaned) {
|
||||
result = append(result, cleaned)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func cleanCardRecognitionCandidate(value string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r >= '0' && r <= '9' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func isValidCardRecognitionCandidate(value string) bool {
|
||||
return (len(value) == 19 || len(value) == 20) && strings.HasPrefix(value, "89")
|
||||
}
|
||||
|
||||
func clampCardRecognitionConfidence(value float64) float64 {
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
if value > 1 {
|
||||
return 1
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func trimCardRecognitionJSON(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
for _, prefix := range []string{"```json", "```JSON", "```"} {
|
||||
if strings.HasPrefix(raw, prefix) {
|
||||
raw = strings.TrimSpace(strings.TrimPrefix(raw, prefix))
|
||||
break
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(raw, "```") {
|
||||
raw = strings.TrimSpace(strings.TrimSuffix(raw, "```"))
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func normalizeCardRecognitionCode(code string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(code)) {
|
||||
case cardRecognitionCodeRecognized:
|
||||
return cardRecognitionCodeRecognized
|
||||
case cardRecognitionCodeAmbiguous:
|
||||
return cardRecognitionCodeAmbiguous
|
||||
case cardRecognitionCodeUnreadable:
|
||||
return cardRecognitionCodeUnreadable
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func isCardRecognitionTimeoutError(ctx context.Context, err error) bool {
|
||||
if ctx != nil && errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(strings.ToLower(err.Error()), "timeout")
|
||||
}
|
||||
Reference in New Issue
Block a user