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
91 lines
2.7 KiB
Go
91 lines
2.7 KiB
Go
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)
|
||
}
|