Files
xinmi/internal/application/service/wiki_provider_circuit.go
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

91 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"fmt"
"regexp"
"strconv"
"sync"
"time"
)
const wikiProviderCooldown = 10 * time.Minute
const wikiProviderRateLimitCooldown = 60 * time.Second
var wikiPermanentStatus = regexp.MustCompile(`(?i)(?:status(?: code)?[ :=]+|http[ :=]+)(401|402|403|429)\b`)
type wikiProviderBlock struct {
status int
until time.Time
}
// The local fallback also protects Lite mode and a temporarily unavailable Redis.
var wikiProviderBlocks sync.Map
var wikiProviderSlots sync.Map
// Bound a single process's Wiki fan-out across KBs sharing a tenant/model.
// Redis shares the cooldown across processes; these slots are process-local.
func acquireWikiProviderSlot(ctx context.Context, key string) (func(), error) {
value, _ := wikiProviderSlots.LoadOrStore(key, make(chan struct{}, 2))
slots := value.(chan struct{})
select {
case slots <- struct{}{}:
return func() { <-slots }, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func wikiProviderCircuitKey(tenant uint64, model string) string {
return fmt.Sprintf("wiki:provider-block:%d:%s", tenant, model)
}
func (s *wikiIngestService) checkWikiProvider(ctx context.Context, key string) error {
if value, ok := wikiProviderBlocks.Load(key); ok {
block := value.(wikiProviderBlock)
if time.Now().Before(block.until) {
return wikiProviderBlockedError(block.status)
}
wikiProviderBlocks.CompareAndDelete(key, value)
}
if s.redisClient != nil {
status, err := s.redisClient.Get(ctx, key).Int()
if err == nil && (status == 401 || status == 402 || status == 403 || status == 429) {
return wikiProviderBlockedError(status)
}
}
return nil
}
func (s *wikiIngestService) blockWikiProvider(ctx context.Context, key string, err error) {
if err == nil {
return
}
match := wikiPermanentStatus.FindStringSubmatch(err.Error())
if len(match) != 2 {
return
}
status, _ := strconv.Atoi(match[1])
cooldown := wikiProviderCooldown
if status == 429 {
cooldown = wikiProviderRateLimitCooldown
}
block := wikiProviderBlock{status: status, until: time.Now().Add(cooldown)}
wikiProviderBlocks.Store(key, block)
time.AfterFunc(cooldown, func() { wikiProviderBlocks.CompareAndDelete(key, block) })
if s.redisClient != nil {
_ = s.redisClient.Set(ctx, key, status, cooldown).Err()
}
}
func wikiProviderBlockedError(status int) error {
if status == 429 {
return fmt.Errorf("模型供应商限流(HTTP 429),Wiki 已暂停对此模型的自动请求 60 秒")
}
reason := "模型认证或访问权限异常"
if status == 402 {
reason = "模型供应商余额不足"
}
return fmt.Errorf("%s(HTTP %d),Wiki 已暂停对此模型的自动请求 10 分钟;请检查模型配置和供应商余额后重试", reason, status)
}