WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
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
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
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user