Upload 19 files
Browse files- internal/service/anthropic.go +1602 -0
- internal/service/api.go +143 -0
- internal/service/autogen.go +453 -0
- internal/service/credential.go +122 -0
- internal/service/debug.go +185 -0
- internal/service/errors.go +10 -0
- internal/service/gemini.go +360 -0
- internal/service/grok.go +273 -0
- internal/service/headers.go +84 -0
- internal/service/jwt.go +74 -0
- internal/service/openai.go +868 -0
- internal/service/pool.go +766 -0
- internal/service/proxy_client.go +143 -0
- internal/service/refresh.go +764 -0
- internal/service/request.go +3 -0
- internal/service/scheduler.go +39 -0
- internal/service/stream.go +57 -0
- internal/service/token.go +128 -0
- internal/service/zencoder.go +217 -0
internal/service/anthropic.go
ADDED
|
@@ -0,0 +1,1602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"bytes"
|
| 6 |
+
"context"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"fmt"
|
| 9 |
+
"io"
|
| 10 |
+
"log"
|
| 11 |
+
"math/rand"
|
| 12 |
+
"net/http"
|
| 13 |
+
"strings"
|
| 14 |
+
"time"
|
| 15 |
+
|
| 16 |
+
"zencoder-2api/internal/model"
|
| 17 |
+
"zencoder-2api/internal/service/provider"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
// sanitizeRequestBody 清理请求体中的敏感信息,保留结构但替换内容
|
| 21 |
+
func sanitizeRequestBody(body []byte) string {
|
| 22 |
+
var reqMap map[string]interface{}
|
| 23 |
+
if err := json.Unmarshal(body, &reqMap); err != nil {
|
| 24 |
+
return string(body) // 如果解析失败,返回原始内容
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// 处理messages数组
|
| 28 |
+
if messages, ok := reqMap["messages"].([]interface{}); ok {
|
| 29 |
+
for i, msg := range messages {
|
| 30 |
+
if msgMap, ok := msg.(map[string]interface{}); ok {
|
| 31 |
+
// 处理content字段
|
| 32 |
+
if content, exists := msgMap["content"]; exists {
|
| 33 |
+
// content可能是字符串或数组
|
| 34 |
+
switch c := content.(type) {
|
| 35 |
+
case string:
|
| 36 |
+
// 如果是字符串,直接替换
|
| 37 |
+
msgMap["content"] = "Content omitted"
|
| 38 |
+
case []interface{}:
|
| 39 |
+
// 如果是数组(结构化内容),保留结构但替换文本
|
| 40 |
+
for j, block := range c {
|
| 41 |
+
if blockMap, ok := block.(map[string]interface{}); ok {
|
| 42 |
+
// 保留type字段
|
| 43 |
+
if blockType, hasType := blockMap["type"]; hasType {
|
| 44 |
+
// 根据type处理不同的内容块
|
| 45 |
+
switch blockType {
|
| 46 |
+
case "text":
|
| 47 |
+
// 替换text内容
|
| 48 |
+
blockMap["text"] = "Content omitted"
|
| 49 |
+
case "thinking", "redacted_thinking":
|
| 50 |
+
// thinking块:替换thinking内容
|
| 51 |
+
if _, hasThinking := blockMap["thinking"]; hasThinking {
|
| 52 |
+
blockMap["thinking"] = "Content omitted"
|
| 53 |
+
}
|
| 54 |
+
// 保留signature字段不变
|
| 55 |
+
case "image":
|
| 56 |
+
// 图片块:清理source内容
|
| 57 |
+
if source, hasSource := blockMap["source"]; hasSource {
|
| 58 |
+
if sourceMap, ok := source.(map[string]interface{}); ok {
|
| 59 |
+
// 保留类型但清理数据
|
| 60 |
+
if _, hasData := sourceMap["data"]; hasData {
|
| 61 |
+
sourceMap["data"] = "Image data omitted"
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
case "tool_use":
|
| 66 |
+
// 工具使用块:清理input内容
|
| 67 |
+
if _, hasInput := blockMap["input"]; hasInput {
|
| 68 |
+
blockMap["input"] = map[string]interface{}{
|
| 69 |
+
"note": "Tool input omitted",
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
case "tool_result":
|
| 73 |
+
// 工具结果块:清理content内容
|
| 74 |
+
if _, hasContent := blockMap["content"]; hasContent {
|
| 75 |
+
blockMap["content"] = "Tool result omitted"
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
c[j] = blockMap
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
msgMap["content"] = c
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
messages[i] = msgMap
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
reqMap["messages"] = messages
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// 处理tools字段 - 改为空数组
|
| 92 |
+
if _, hasTools := reqMap["tools"]; hasTools {
|
| 93 |
+
reqMap["tools"] = []interface{}{}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// 处理system字段 - 替换为固定文本
|
| 97 |
+
if _, hasSystem := reqMap["system"]; hasSystem {
|
| 98 |
+
reqMap["system"] = "System prompt omitted"
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// 序列化为JSON字符串
|
| 102 |
+
sanitized, _ := json.MarshalIndent(reqMap, "", " ")
|
| 103 |
+
return string(sanitized)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// logRequestDetails 记录请求详细信息
|
| 107 |
+
func logRequestDetails(prefix string, headers http.Header, body []byte) {
|
| 108 |
+
log.Printf("%s 请求详情:", prefix)
|
| 109 |
+
|
| 110 |
+
// 记录请求头
|
| 111 |
+
log.Printf("%s 请求头:", prefix)
|
| 112 |
+
for k, v := range headers {
|
| 113 |
+
// 过滤敏感请求头
|
| 114 |
+
if strings.Contains(strings.ToLower(k), "auth") ||
|
| 115 |
+
strings.Contains(strings.ToLower(k), "key") ||
|
| 116 |
+
strings.Contains(strings.ToLower(k), "token") {
|
| 117 |
+
log.Printf(" %s: [REDACTED]", k)
|
| 118 |
+
} else {
|
| 119 |
+
log.Printf(" %s: %s", k, strings.Join(v, ", "))
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// 记录请求体(已清理敏感信息)
|
| 124 |
+
log.Printf("%s 请求体 (已清理):", prefix)
|
| 125 |
+
log.Printf("%s", sanitizeRequestBody(body))
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
const AnthropicBaseURL = "https://api.zencoder.ai/anthropic"
|
| 129 |
+
|
| 130 |
+
type AnthropicService struct{}
|
| 131 |
+
|
| 132 |
+
func NewAnthropicService() *AnthropicService {
|
| 133 |
+
return &AnthropicService{}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Messages 处理/v1/messages请求,直接透传到Anthropic API
|
| 137 |
+
func (s *AnthropicService) Messages(ctx context.Context, body []byte, isStream bool) (*http.Response, error) {
|
| 138 |
+
var req struct {
|
| 139 |
+
Model string `json:"model"`
|
| 140 |
+
MaxTokens float64 `json:"max_tokens,omitempty"`
|
| 141 |
+
Thinking map[string]interface{} `json:"thinking,omitempty"`
|
| 142 |
+
}
|
| 143 |
+
if err := json.Unmarshal(body, &req); err != nil {
|
| 144 |
+
return nil, fmt.Errorf("invalid request body: %w", err)
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// 记录请求的模型和thinking状态
|
| 148 |
+
thinkingStatus := "disabled"
|
| 149 |
+
if req.Thinking != nil {
|
| 150 |
+
if enabled, ok := req.Thinking["enabled"].(bool); ok && enabled {
|
| 151 |
+
thinkingStatus = "enabled"
|
| 152 |
+
} else if thinkingType, ok := req.Thinking["type"].(string); ok && thinkingType == "enabled" {
|
| 153 |
+
thinkingStatus = "enabled"
|
| 154 |
+
}
|
| 155 |
+
// 如果有thinking配置且有budget_tokens,也记录
|
| 156 |
+
if budget, ok := req.Thinking["budget_tokens"].(float64); ok && budget > 0 {
|
| 157 |
+
thinkingStatus = fmt.Sprintf("enabled(budget=%g)", budget)
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
// 只在非限速测试时输出请求信息
|
| 161 |
+
if IsDebugMode() && !strings.Contains(req.Model, "test") {
|
| 162 |
+
log.Printf("[Anthropic] 请求 - Model: %s, Thinking: %s", req.Model, thinkingStatus)
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// 检查是否需要映射到对应的thinking模型
|
| 166 |
+
originalModel := req.Model
|
| 167 |
+
if req.Thinking != nil {
|
| 168 |
+
// 检查是否开启了thinking
|
| 169 |
+
thinkingEnabled := false
|
| 170 |
+
if enabled, ok := req.Thinking["enabled"].(bool); ok && enabled {
|
| 171 |
+
thinkingEnabled = true
|
| 172 |
+
} else if thinkingType, ok := req.Thinking["type"].(string); ok && thinkingType == "enabled" {
|
| 173 |
+
thinkingEnabled = true
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
if thinkingEnabled {
|
| 177 |
+
// 检查是否存在对应的thinking模型
|
| 178 |
+
thinkingModelID := req.Model + "-thinking"
|
| 179 |
+
if _, exists := model.GetZenModel(thinkingModelID); exists {
|
| 180 |
+
req.Model = thinkingModelID
|
| 181 |
+
DebugLog(ctx, "[Anthropic] 映射到thinking模型: %s -> %s", originalModel, req.Model)
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// 检查模型是否存在于模型字典中
|
| 187 |
+
_, exists := model.GetZenModel(req.Model)
|
| 188 |
+
if !exists {
|
| 189 |
+
DebugLog(ctx, "[Anthropic] 模型不存在: %s", req.Model)
|
| 190 |
+
return nil, ErrNoAvailableAccount
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
DebugLogRequest(ctx, "Anthropic", "/v1/messages", req.Model)
|
| 194 |
+
|
| 195 |
+
// 处理max_tokens和thinking.budget_tokens的关系
|
| 196 |
+
// 如果用户传入了thinking配置,检查并调整max_tokens
|
| 197 |
+
if req.Thinking != nil {
|
| 198 |
+
budgetTokens := 0.0
|
| 199 |
+
if budget, ok := req.Thinking["budget_tokens"].(float64); ok {
|
| 200 |
+
budgetTokens = budget
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// 如果max_tokens小于等于budget_tokens,调整max_tokens
|
| 204 |
+
if budgetTokens > 0 && req.MaxTokens > 0 && req.MaxTokens <= budgetTokens {
|
| 205 |
+
// 按用户要求:max_tokens = max_tokens + budget_tokens
|
| 206 |
+
newMaxTokens := req.MaxTokens + budgetTokens
|
| 207 |
+
|
| 208 |
+
// 修改原始请求体中的max_tokens
|
| 209 |
+
var reqMap map[string]interface{}
|
| 210 |
+
if err := json.Unmarshal(body, &reqMap); err == nil {
|
| 211 |
+
reqMap["max_tokens"] = newMaxTokens
|
| 212 |
+
if modifiedBody, err := json.Marshal(reqMap); err == nil {
|
| 213 |
+
body = modifiedBody
|
| 214 |
+
DebugLog(ctx, "[Anthropic] 调整max_tokens: %.0f -> %.0f (原值+budget_tokens)", req.MaxTokens, newMaxTokens)
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
var lastErr error
|
| 221 |
+
for i := 0; i < MaxRetries; i++ {
|
| 222 |
+
account, err := GetNextAccountForModel(req.Model)
|
| 223 |
+
if err != nil {
|
| 224 |
+
DebugLogRequestEnd(ctx, "Anthropic", false, err)
|
| 225 |
+
return nil, err
|
| 226 |
+
}
|
| 227 |
+
DebugLogAccountSelected(ctx, "Anthropic", account.ID, account.Email)
|
| 228 |
+
|
| 229 |
+
resp, err := s.doRequest(ctx, account, req.Model, body)
|
| 230 |
+
if err != nil {
|
| 231 |
+
// 请求失败,释放账号
|
| 232 |
+
ReleaseAccount(account)
|
| 233 |
+
// MarkAccountError(account)
|
| 234 |
+
lastErr = err
|
| 235 |
+
DebugLogRetry(ctx, "Anthropic", i+1, account.ID, err)
|
| 236 |
+
continue
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// 只在调试模式下且非限速测试时输出详细响应信息
|
| 240 |
+
if IsDebugMode() && !strings.Contains(req.Model, "test") {
|
| 241 |
+
DebugLogResponseReceived(ctx, "Anthropic", resp.StatusCode)
|
| 242 |
+
|
| 243 |
+
// 只输出积分信息,不输出所有响应头
|
| 244 |
+
if resp.Header.Get("Zen-Pricing-Period-Limit") != "" ||
|
| 245 |
+
resp.Header.Get("Zen-Pricing-Period-Cost") != "" ||
|
| 246 |
+
resp.Header.Get("Zen-Request-Cost") != "" {
|
| 247 |
+
DebugLog(ctx, "[Anthropic] 积分信息 - 周期限额: %s, 周期消耗: %s, 本次消耗: %s",
|
| 248 |
+
resp.Header.Get("Zen-Pricing-Period-Limit"),
|
| 249 |
+
resp.Header.Get("Zen-Pricing-Period-Cost"),
|
| 250 |
+
resp.Header.Get("Zen-Request-Cost"))
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
if resp.StatusCode >= 400 {
|
| 255 |
+
// 读取错误响应内容
|
| 256 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 257 |
+
resp.Body.Close()
|
| 258 |
+
|
| 259 |
+
// 检查是否是官方API直接抛出的错误(413、400、429)
|
| 260 |
+
// 这些错误不是token池问题,应直接返回给客户端
|
| 261 |
+
if resp.StatusCode == 413 || resp.StatusCode == 400 || resp.StatusCode == 429 {
|
| 262 |
+
// 对于400错误,根据错误类型决定日志级别
|
| 263 |
+
if resp.StatusCode == 400 {
|
| 264 |
+
// 解析thinking状态用于日志
|
| 265 |
+
thinkingStatus := "disabled"
|
| 266 |
+
if req.Thinking != nil {
|
| 267 |
+
if enabled, ok := req.Thinking["enabled"].(bool); ok && enabled {
|
| 268 |
+
thinkingStatus = "enabled"
|
| 269 |
+
} else if thinkingType, ok := req.Thinking["type"].(string); ok && thinkingType == "enabled" {
|
| 270 |
+
thinkingStatus = "enabled"
|
| 271 |
+
}
|
| 272 |
+
// 如果有thinking配置且有budget_tokens,也记录
|
| 273 |
+
if budget, ok := req.Thinking["budget_tokens"].(float64); ok && budget > 0 {
|
| 274 |
+
thinkingStatus = fmt.Sprintf("enabled(budget=%g)", budget)
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
// 尝试解析错误类型
|
| 279 |
+
var errResp struct {
|
| 280 |
+
Error struct {
|
| 281 |
+
Type string `json:"type"`
|
| 282 |
+
Message string `json:"message"`
|
| 283 |
+
} `json:"error"`
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
isKnownError := false
|
| 287 |
+
isPromptTooLongError := false
|
| 288 |
+
if err := json.Unmarshal(errBody, &errResp); err == nil && errResp.Error.Type != "" {
|
| 289 |
+
// 检查是否是已知的错误类型
|
| 290 |
+
knownErrors := []string{
|
| 291 |
+
"prompt is too long",
|
| 292 |
+
"max_tokens",
|
| 293 |
+
"invalid_request_error",
|
| 294 |
+
"authentication_error",
|
| 295 |
+
"permission_error",
|
| 296 |
+
"rate_limit_error",
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
errorMessage := strings.ToLower(errResp.Error.Message)
|
| 300 |
+
for _, known := range knownErrors {
|
| 301 |
+
if strings.Contains(errorMessage, known) || errResp.Error.Type == known {
|
| 302 |
+
isKnownError = true
|
| 303 |
+
if known == "prompt is too long" || strings.Contains(errorMessage, "prompt is too long") {
|
| 304 |
+
isPromptTooLongError = true
|
| 305 |
+
}
|
| 306 |
+
break
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
if isKnownError {
|
| 311 |
+
// 已知错误,只输出简单日志,包含请求模型ID和thinking状态
|
| 312 |
+
log.Printf("[Anthropic] 400错误: %s - %s (Model: %s, Thinking: %s)", errResp.Error.Type, errResp.Error.Message, req.Model, thinkingStatus)
|
| 313 |
+
|
| 314 |
+
// 对于非"prompt is too long"错误,在DEBUG模式下输出详细信息
|
| 315 |
+
if !isPromptTooLongError && IsDebugMode() {
|
| 316 |
+
if originalHeaders, ok := ctx.Value("originalHeaders").(http.Header); ok {
|
| 317 |
+
logRequestDetails("[Anthropic] 原始客户端", originalHeaders, body)
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
} else {
|
| 321 |
+
// 未知错误,输出详细日志用于调试,包含请求模型ID和thinking状态
|
| 322 |
+
log.Printf("[Anthropic] 400未知错误: %s (Model: %s, Thinking: %s)", string(errBody), req.Model, thinkingStatus)
|
| 323 |
+
if IsDebugMode() {
|
| 324 |
+
// DEBUG模式下输出原始请求信息
|
| 325 |
+
if originalHeaders, ok := ctx.Value("originalHeaders").(http.Header); ok {
|
| 326 |
+
logRequestDetails("[Anthropic] 原始客户端", originalHeaders, body)
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
} else {
|
| 331 |
+
// 解析失败,输出完整错误用于调试,包含请求模型ID和thinking状态
|
| 332 |
+
log.Printf("[Anthropic] 400错误(无法解析): %s (Model: %s, Thinking: %s)", string(errBody), req.Model, thinkingStatus)
|
| 333 |
+
if IsDebugMode() {
|
| 334 |
+
// DEBUG模式下输出原始请求信息
|
| 335 |
+
if originalHeaders, ok := ctx.Value("originalHeaders").(http.Header); ok {
|
| 336 |
+
logRequestDetails("[Anthropic] 原始客户端", originalHeaders, body)
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
} else if resp.StatusCode == 429 {
|
| 341 |
+
// 简化429错误日志输出
|
| 342 |
+
s.classifyAndLog429Error(string(errBody), account.ID, account.Email)
|
| 343 |
+
|
| 344 |
+
// 检查是否是Claude官方的429错误
|
| 345 |
+
isClaudeOfficialError := s.isClaudeOfficial429Error(string(errBody))
|
| 346 |
+
|
| 347 |
+
// 尝试使用代理池重试
|
| 348 |
+
proxyResp, proxyErr := s.retryWithProxy(ctx, account, req.Model, body)
|
| 349 |
+
if proxyErr == nil && proxyResp != nil {
|
| 350 |
+
// 代理重试成功
|
| 351 |
+
ReleaseAccount(account)
|
| 352 |
+
return proxyResp, nil
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
if proxyErr != nil {
|
| 356 |
+
log.Printf("[Anthropic] 代理重试失败 账号ID:%d %s", account.ID, account.Email)
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// 只有Claude官方的429错误才返回原始响应,其他429错误返回通用错误
|
| 360 |
+
if isClaudeOfficialError {
|
| 361 |
+
// Claude官方429错误,返回原始响应
|
| 362 |
+
ReleaseAccount(account)
|
| 363 |
+
return &http.Response{
|
| 364 |
+
StatusCode: resp.StatusCode,
|
| 365 |
+
Header: resp.Header,
|
| 366 |
+
Body: io.NopCloser(bytes.NewReader(errBody)),
|
| 367 |
+
}, nil
|
| 368 |
+
} else {
|
| 369 |
+
// 非Claude官方429错误,不返回原始响应,继续重试其他账号
|
| 370 |
+
ReleaseAccount(account)
|
| 371 |
+
lastErr = fmt.Errorf("non-official 429 error")
|
| 372 |
+
if IsDebugMode() {
|
| 373 |
+
DebugLogRetry(ctx, "Anthropic", i+1, account.ID, lastErr)
|
| 374 |
+
}
|
| 375 |
+
continue
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
// 对于其他官方API错误(400、413):
|
| 379 |
+
// 1. 释放账号
|
| 380 |
+
// 2. 不计算账号错误次数
|
| 381 |
+
// 3. 直接返回原始响应
|
| 382 |
+
ReleaseAccount(account)
|
| 383 |
+
return &http.Response{
|
| 384 |
+
StatusCode: resp.StatusCode,
|
| 385 |
+
Header: resp.Header,
|
| 386 |
+
Body: io.NopCloser(bytes.NewReader(errBody)),
|
| 387 |
+
}, nil
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
// 503和529错误:上游API错误,不是token问题
|
| 391 |
+
if resp.StatusCode == 503 || resp.StatusCode == 529 {
|
| 392 |
+
// 只记录简单的错误日志
|
| 393 |
+
log.Printf("错误响应 [%d]: %s", resp.StatusCode, string(errBody))
|
| 394 |
+
// 释放账号,不计算错误次数,返回通用错误
|
| 395 |
+
ReleaseAccount(account)
|
| 396 |
+
return nil, ErrNoAvailableAccount
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
// 500错误处理
|
| 400 |
+
if resp.StatusCode == 500 {
|
| 401 |
+
// 检查是否是限速问题
|
| 402 |
+
if strings.Contains(string(errBody), "Rate limit tracking problem") {
|
| 403 |
+
log.Printf("[Anthropic] 限速跟踪问题,尝试使用代理重试")
|
| 404 |
+
|
| 405 |
+
// 尝试使用代理池重试
|
| 406 |
+
proxyResp, proxyErr := s.retryWithProxy(ctx, account, req.Model, body)
|
| 407 |
+
if proxyErr == nil && proxyResp != nil {
|
| 408 |
+
// 代理重试成功
|
| 409 |
+
ReleaseAccount(account)
|
| 410 |
+
return proxyResp, nil
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
log.Printf("[Anthropic] 代理重试失败: %v", proxyErr)
|
| 414 |
+
|
| 415 |
+
// 代理重试失败,继续原有逻���:冻结账号5-10秒随机时间
|
| 416 |
+
freezeTime := 5 + rand.Intn(6) // 5-10秒随机
|
| 417 |
+
|
| 418 |
+
// 非调试模式下只输出简单信息
|
| 419 |
+
if !IsDebugMode() {
|
| 420 |
+
log.Printf("[Anthropic] 限速错误,冻结账号 ID:%d %s %d秒,重试 #%d", account.ID, account.Email, freezeTime, i+1)
|
| 421 |
+
} else {
|
| 422 |
+
log.Printf("[Anthropic] 检测到限速错误,冻结账号 ID:%d %s %d秒", account.ID, account.Email, freezeTime)
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// 冻结账号并释放(不计算错误次数,这是临时限速问题)
|
| 426 |
+
FreezeAccount(account, time.Duration(freezeTime)*time.Second) // 这个函数内部会释放账号
|
| 427 |
+
|
| 428 |
+
// 设置错误并继续重试其他账号
|
| 429 |
+
lastErr = fmt.Errorf("rate limit tracking problem")
|
| 430 |
+
|
| 431 |
+
// 只在调试模式下输出详细重试日志
|
| 432 |
+
if IsDebugMode() {
|
| 433 |
+
DebugLogRetry(ctx, "Anthropic", i+1, account.ID, lastErr)
|
| 434 |
+
}
|
| 435 |
+
continue
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
// 其他500错误,释放账号并直接返回
|
| 439 |
+
ReleaseAccount(account)
|
| 440 |
+
return &http.Response{
|
| 441 |
+
StatusCode: resp.StatusCode,
|
| 442 |
+
Header: resp.Header,
|
| 443 |
+
Body: io.NopCloser(bytes.NewReader(errBody)),
|
| 444 |
+
}, nil
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// 其他错误,释放账号并继续重试
|
| 448 |
+
ReleaseAccount(account)
|
| 449 |
+
// MarkAccountError(account)
|
| 450 |
+
lastErr = fmt.Errorf("API error: %d", resp.StatusCode)
|
| 451 |
+
|
| 452 |
+
// 只在调试模式下输出详细错误信息
|
| 453 |
+
if IsDebugMode() {
|
| 454 |
+
DebugLogErrorResponse(ctx, "Anthropic", resp.StatusCode, string(errBody))
|
| 455 |
+
DebugLogRetry(ctx, "Anthropic", i+1, account.ID, lastErr)
|
| 456 |
+
} else {
|
| 457 |
+
// 非调试模式下只输出简单的重试信息
|
| 458 |
+
log.Printf("[Anthropic] API错误 %d,重试 #%d", resp.StatusCode, i+1)
|
| 459 |
+
}
|
| 460 |
+
continue
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// 请求成功,释放账号
|
| 464 |
+
ReleaseAccount(account)
|
| 465 |
+
|
| 466 |
+
ResetAccountError(account)
|
| 467 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 468 |
+
if !exists {
|
| 469 |
+
// 模型不存在,使用默认倍率
|
| 470 |
+
UpdateAccountCreditsFromResponse(account, resp, 1.0)
|
| 471 |
+
} else {
|
| 472 |
+
// 使用统一的积分更新函数,自动处理响应头中的积分信息
|
| 473 |
+
UpdateAccountCreditsFromResponse(account, resp, zenModel.Multiplier)
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
DebugLogRequestEnd(ctx, "Anthropic", true, nil)
|
| 477 |
+
return resp, nil
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
// 只在调试模式下输出详细的请求结束日志
|
| 481 |
+
if IsDebugMode() {
|
| 482 |
+
DebugLogRequestEnd(ctx, "Anthropic", false, lastErr)
|
| 483 |
+
} else {
|
| 484 |
+
// 非调试模式下只输出简单的失败信息
|
| 485 |
+
log.Printf("[Anthropic] 所有重试失败: %v", lastErr)
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
// 检查是否是网络连接错误,如果是则返回统一的错误信息,避免暴露内部网络详情
|
| 489 |
+
if lastErr != nil {
|
| 490 |
+
errStr := lastErr.Error()
|
| 491 |
+
// 检查常见的网络连接错误
|
| 492 |
+
if strings.Contains(errStr, "dial tcp") ||
|
| 493 |
+
strings.Contains(errStr, "connection refused") ||
|
| 494 |
+
strings.Contains(errStr, "no such host") ||
|
| 495 |
+
strings.Contains(errStr, "cannot assign requested address") ||
|
| 496 |
+
strings.Contains(errStr, "timeout") ||
|
| 497 |
+
strings.Contains(errStr, "network is unreachable") {
|
| 498 |
+
return nil, ErrNoAvailableAccount
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
return nil, fmt.Errorf("all retries failed: %w", lastErr)
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
func (s *AnthropicService) doRequest(ctx context.Context, account *model.Account, modelID string, body []byte) (*http.Response, error) {
|
| 506 |
+
zenModel, exists := model.GetZenModel(modelID)
|
| 507 |
+
if !exists {
|
| 508 |
+
// 模型不存在,返回错误
|
| 509 |
+
return nil, ErrNoAvailableAccount
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
// 注意:已移除模型替换逻辑,直接使用原始请求体
|
| 513 |
+
modifiedBody := body
|
| 514 |
+
|
| 515 |
+
// 对于需要 thinking 的模型,强制添加 thinking 配置
|
| 516 |
+
var err error
|
| 517 |
+
modifiedBody, err = s.ensureThinkingConfig(modifiedBody, modelID)
|
| 518 |
+
if err != nil {
|
| 519 |
+
return nil, fmt.Errorf("failed to ensure thinking config: %w", err)
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
// 根据模型要求调整参数(温度、top_p等)
|
| 523 |
+
modifiedBody, err = s.adjustParametersForModel(modifiedBody, modelID)
|
| 524 |
+
if err != nil {
|
| 525 |
+
return nil, fmt.Errorf("failed to adjust parameters: %w", err)
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
// 注意:已移除模型重定向逻辑,直接使用用户请求的模型名
|
| 529 |
+
DebugLogActualModel(ctx, "Anthropic", modelID, modelID)
|
| 530 |
+
|
| 531 |
+
reqURL := AnthropicBaseURL + "/v1/messages"
|
| 532 |
+
DebugLogRequestSent(ctx, "Anthropic", reqURL)
|
| 533 |
+
|
| 534 |
+
resp, err := s.makeRequest(ctx, modifiedBody, account, zenModel)
|
| 535 |
+
if err != nil {
|
| 536 |
+
return nil, err
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
// 检查是否是400错误,需要特殊处理
|
| 540 |
+
if resp.StatusCode == 400 {
|
| 541 |
+
bodyBytes, readErr := io.ReadAll(resp.Body)
|
| 542 |
+
resp.Body.Close()
|
| 543 |
+
|
| 544 |
+
if readErr == nil {
|
| 545 |
+
errorBody := string(bodyBytes)
|
| 546 |
+
|
| 547 |
+
// 检查是否是thinking格式错误,但不再进行模型切换
|
| 548 |
+
if s.isThinkingFormatError(errorBody) {
|
| 549 |
+
log.Printf("[Anthropic] thinking格式错误: %s", errorBody)
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
// 检查是否是thinking signature过期错误
|
| 553 |
+
if s.isThinkingSignatureError(errorBody) {
|
| 554 |
+
// 解析当前请求的模型和thinking状态
|
| 555 |
+
var reqInfo struct {
|
| 556 |
+
Model string `json:"model"`
|
| 557 |
+
Thinking map[string]interface{} `json:"thinking,omitempty"`
|
| 558 |
+
}
|
| 559 |
+
json.Unmarshal(modifiedBody, &reqInfo)
|
| 560 |
+
|
| 561 |
+
thinkingStatus := "disabled"
|
| 562 |
+
if reqInfo.Thinking != nil {
|
| 563 |
+
if enabled, ok := reqInfo.Thinking["enabled"].(bool); ok && enabled {
|
| 564 |
+
thinkingStatus = "enabled"
|
| 565 |
+
} else if thinkingType, ok := reqInfo.Thinking["type"].(string); ok && thinkingType == "enabled" {
|
| 566 |
+
thinkingStatus = "enabled"
|
| 567 |
+
}
|
| 568 |
+
if budget, ok := reqInfo.Thinking["budget_tokens"].(float64); ok && budget > 0 {
|
| 569 |
+
thinkingStatus = fmt.Sprintf("enabled(budget=%g)", budget)
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
if IsDebugMode() {
|
| 574 |
+
log.Printf("[Anthropic] thinking signature过期,尝试转换assistant消息为user消息重试")
|
| 575 |
+
} else {
|
| 576 |
+
log.Printf("[Anthropic] thinking signature过期,尝试转换assistant消息为user消息重试 model:%s thinking:%s", reqInfo.Model, thinkingStatus)
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
// 转换请求体:将assistant消息转换为user消息
|
| 580 |
+
fixedBody, fixErr := s.convertAssistantMessagesToUser(modifiedBody)
|
| 581 |
+
if fixErr == nil {
|
| 582 |
+
return s.makeRequest(ctx, fixedBody, account, zenModel)
|
| 583 |
+
} else {
|
| 584 |
+
log.Printf("[Anthropic] 转换assistant消息失败: %v", fixErr)
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
// 检查是否是参数冲突错误(temperature 和 top_p 不能同时指定)
|
| 589 |
+
if s.isParameterConflictError(errorBody) {
|
| 590 |
+
DebugLogRequestSent(ctx, "Anthropic", "Retrying with only temperature parameter")
|
| 591 |
+
|
| 592 |
+
// 移除 top_p 参数,只保留 temperature
|
| 593 |
+
fixedBody, fixErr := s.removeTopP(modifiedBody)
|
| 594 |
+
if fixErr == nil {
|
| 595 |
+
return s.makeRequest(ctx, fixedBody, account, zenModel)
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
// 检查是否是温度参数错误
|
| 600 |
+
if s.isTemperatureError(errorBody) {
|
| 601 |
+
DebugLogRequestSent(ctx, "Anthropic", "Retrying with temperature=1.0")
|
| 602 |
+
|
| 603 |
+
// 强制设置温度为1.0并重试
|
| 604 |
+
fixedBody, fixErr := s.forceTemperature(modifiedBody, 1.0)
|
| 605 |
+
if fixErr == nil {
|
| 606 |
+
return s.makeRequest(ctx, fixedBody, account, zenModel)
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
// 如果不是thinking相关的可修复错误,返回原始响应
|
| 613 |
+
return &http.Response{
|
| 614 |
+
StatusCode: resp.StatusCode,
|
| 615 |
+
Header: resp.Header,
|
| 616 |
+
Body: io.NopCloser(bytes.NewReader(bodyBytes)),
|
| 617 |
+
}, nil
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
return resp, nil
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
func (s *AnthropicService) makeRequest(ctx context.Context, body []byte, account *model.Account, zenModel model.ZenModel) (*http.Response, error) {
|
| 624 |
+
httpReq, err := http.NewRequest("POST", AnthropicBaseURL+"/v1/messages", bytes.NewReader(body))
|
| 625 |
+
if err != nil {
|
| 626 |
+
return nil, err
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
// 设置Zencoder自定义请求头
|
| 630 |
+
SetZencoderHeaders(httpReq, account, zenModel)
|
| 631 |
+
|
| 632 |
+
// Anthropic特有请求头
|
| 633 |
+
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
| 634 |
+
|
| 635 |
+
// 添加模型配置的额外请求头
|
| 636 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 637 |
+
for k, v := range zenModel.Parameters.ExtraHeaders {
|
| 638 |
+
httpReq.Header.Set(k, v)
|
| 639 |
+
}
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
// 只在非限速测试且调试模式下记录请求头
|
| 643 |
+
if IsDebugMode() {
|
| 644 |
+
// 检查请求体中的模型以判断是否为限速测试
|
| 645 |
+
var reqCheck struct {
|
| 646 |
+
Model string `json:"model"`
|
| 647 |
+
}
|
| 648 |
+
if json.Unmarshal(body, &reqCheck) == nil && !strings.Contains(reqCheck.Model, "test") {
|
| 649 |
+
DebugLogRequestHeaders(ctx, "Anthropic", httpReq.Header)
|
| 650 |
+
}
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
httpClient := provider.NewHTTPClient(account.Proxy, 0)
|
| 654 |
+
resp, err := httpClient.Do(httpReq)
|
| 655 |
+
if err != nil {
|
| 656 |
+
return nil, err
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
// 不输出响应头调试信息以减少日志量
|
| 660 |
+
|
| 661 |
+
// 如果是400错误,记录详细的请求信息
|
| 662 |
+
if resp.StatusCode == 400 {
|
| 663 |
+
// 读取错误响应内容
|
| 664 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 665 |
+
resp.Body.Close()
|
| 666 |
+
|
| 667 |
+
// 检查是否是"prompt is too long"错误
|
| 668 |
+
isPromptTooLongError := false
|
| 669 |
+
// 检查是否是thinking格式错误(将在doRequest中处理并重试)
|
| 670 |
+
isThinkingFormatError := false
|
| 671 |
+
// 检查是否是thinking signature过期错误(将在doRequest中处理并重试)
|
| 672 |
+
isThinkingSignatureError := false
|
| 673 |
+
var errResp struct {
|
| 674 |
+
Error struct {
|
| 675 |
+
Type string `json:"type"`
|
| 676 |
+
Message string `json:"message"`
|
| 677 |
+
} `json:"error"`
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
if err := json.Unmarshal(errBody, &errResp); err == nil {
|
| 681 |
+
errorMessage := strings.ToLower(errResp.Error.Message)
|
| 682 |
+
if strings.Contains(errorMessage, "prompt is too long") {
|
| 683 |
+
isPromptTooLongError = true
|
| 684 |
+
// 对于prompt过长错误,只输出简单的错误信息
|
| 685 |
+
log.Printf("[Anthropic] 400错误: %s - %s", errResp.Error.Type, errResp.Error.Message)
|
| 686 |
+
}
|
| 687 |
+
// 检查是否是thinking格式错误
|
| 688 |
+
if strings.Contains(errResp.Error.Message, "When `thinking` is enabled") ||
|
| 689 |
+
strings.Contains(errResp.Error.Message, "Expected `thinking` or `redacted_thinking`") {
|
| 690 |
+
isThinkingFormatError = true
|
| 691 |
+
// 输出详细的thinking格式错误信息
|
| 692 |
+
log.Printf("[Anthropic] thinking格式错误详情: %s", errResp.Error.Message)
|
| 693 |
+
log.Printf("[Anthropic] 发送给zencoder的请求体:")
|
| 694 |
+
log.Printf("%s", sanitizeRequestBody(body))
|
| 695 |
+
}
|
| 696 |
+
// 检查是否是thinking signature过期错误
|
| 697 |
+
if strings.Contains(errResp.Error.Message, "Invalid `signature` in `thinking` block") {
|
| 698 |
+
isThinkingSignatureError = true
|
| 699 |
+
// 对于thinking signature过期错误,只输出简单信息,详细处理留给doRequest
|
| 700 |
+
}
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
// 只在非调试模式且非已知可重试错误时才输出详细debug信息
|
| 704 |
+
// thinking相关错误会在doRequest中处理,如果重试成功就不需要输出debug日志
|
| 705 |
+
shouldOutputDetails := !isPromptTooLongError && !isThinkingFormatError && !isThinkingSignatureError
|
| 706 |
+
if shouldOutputDetails {
|
| 707 |
+
log.Printf("[Anthropic] API返回400错误: %s", string(errBody))
|
| 708 |
+
// 只在调试模式下输出详细的请求信息
|
| 709 |
+
if IsDebugMode() {
|
| 710 |
+
logRequestDetails("[Anthropic] 实际API", httpReq.Header, body)
|
| 711 |
+
}
|
| 712 |
+
} else if isThinkingSignatureError && IsDebugMode() {
|
| 713 |
+
// thinking signature错误只在调试模式下输出简单信息
|
| 714 |
+
log.Printf("[Anthropic] API返回400错误: %s", string(errBody))
|
| 715 |
+
logRequestDetails("[Anthropic] 实际API", httpReq.Header, body)
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
// 重新构建响应,因为body已经被读取
|
| 719 |
+
resp.Body = io.NopCloser(bytes.NewReader(errBody))
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
return resp, nil
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
// isThinkingFormatError 检查是否是thinking格式相关的错误
|
| 726 |
+
func (s *AnthropicService) isThinkingFormatError(errorBody string) bool {
|
| 727 |
+
return strings.Contains(errorBody, "When `thinking` is enabled, a final `assistant` message must start with a thinking block") ||
|
| 728 |
+
strings.Contains(errorBody, "Expected `thinking` or `redacted_thinking`") ||
|
| 729 |
+
strings.Contains(errorBody, "To avoid this requirement, disable `thinking`")
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
// isThinkingSignatureError 检查是否是thinking signature过期错误
|
| 733 |
+
func (s *AnthropicService) isThinkingSignatureError(errorBody string) bool {
|
| 734 |
+
return strings.Contains(errorBody, "Invalid `signature` in `thinking` block") ||
|
| 735 |
+
strings.Contains(errorBody, "invalid_request_error") && strings.Contains(errorBody, "signature")
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
// isTemperatureError 检查是否是温度参数相关的错误
|
| 739 |
+
func (s *AnthropicService) isTemperatureError(errorBody string) bool {
|
| 740 |
+
return strings.Contains(errorBody, "requires temperature=1.0") ||
|
| 741 |
+
strings.Contains(errorBody, "Parallel Thinking' requires temperature")
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
// isParameterConflictError 检查是否是参数冲突错误
|
| 745 |
+
func (s *AnthropicService) isParameterConflictError(errorBody string) bool {
|
| 746 |
+
return strings.Contains(errorBody, "`temperature` and `top_p` cannot both be specified")
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
// isClaudeOfficial429Error 检查是否是Claude官方的429限流错误
|
| 750 |
+
func (s *AnthropicService) isClaudeOfficial429Error(errorBody string) bool {
|
| 751 |
+
// 尝试解析错误响应
|
| 752 |
+
var errResp struct {
|
| 753 |
+
Type string `json:"type"`
|
| 754 |
+
Error struct {
|
| 755 |
+
Type string `json:"type"`
|
| 756 |
+
Message string `json:"message"`
|
| 757 |
+
} `json:"error"`
|
| 758 |
+
RequestID string `json:"request_id"`
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
// 如果能解析成功且符合Claude官方格式
|
| 762 |
+
if err := json.Unmarshal([]byte(errorBody), &errResp); err == nil {
|
| 763 |
+
// Claude官方错误特征:
|
| 764 |
+
// 1. type = "error"
|
| 765 |
+
// 2. error.type = "rate_limit_error"
|
| 766 |
+
// 3. 错误消息包含anthropic.com或claude.com域名
|
| 767 |
+
if errResp.Type == "error" &&
|
| 768 |
+
errResp.Error.Type == "rate_limit_error" &&
|
| 769 |
+
(strings.Contains(errResp.Error.Message, "anthropic.com") ||
|
| 770 |
+
strings.Contains(errResp.Error.Message, "claude.com") ||
|
| 771 |
+
strings.Contains(errResp.Error.Message, "docs.claude.com")) {
|
| 772 |
+
return true
|
| 773 |
+
}
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
// 检查是否是非Claude官方的错误格式(如Google API格式)
|
| 777 |
+
var nonClaudeErr struct {
|
| 778 |
+
Error struct {
|
| 779 |
+
Code int `json:"code"`
|
| 780 |
+
Message string `json:"message"`
|
| 781 |
+
Status string `json:"status"`
|
| 782 |
+
} `json:"error"`
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
if err := json.Unmarshal([]byte(errorBody), &nonClaudeErr); err == nil {
|
| 786 |
+
// 非Claude官方错误特征:有code和status字段
|
| 787 |
+
if nonClaudeErr.Error.Code == 429 &&
|
| 788 |
+
nonClaudeErr.Error.Status == "RESOURCE_EXHAUSTED" {
|
| 789 |
+
return false
|
| 790 |
+
}
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
// 默认情况下,如果无法确定,保守处理:不返回原始响应
|
| 794 |
+
return false
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
// classifyAndLog429Error 分类并记录429错误的简化日志
|
| 798 |
+
func (s *AnthropicService) classifyAndLog429Error(errorBody string, accountID uint, email string) {
|
| 799 |
+
// 尝试解析Claude官方错误
|
| 800 |
+
var claudeErr struct {
|
| 801 |
+
Type string `json:"type"`
|
| 802 |
+
Error struct {
|
| 803 |
+
Type string `json:"type"`
|
| 804 |
+
Message string `json:"message"`
|
| 805 |
+
} `json:"error"`
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
if err := json.Unmarshal([]byte(errorBody), &claudeErr); err == nil {
|
| 809 |
+
if claudeErr.Type == "error" && claudeErr.Error.Type == "rate_limit_error" {
|
| 810 |
+
// Claude官方限流错误
|
| 811 |
+
log.Printf("[Anthropic] Claude rate_limit_error 账号ID:%d %s", accountID, email)
|
| 812 |
+
return
|
| 813 |
+
}
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
// 尝试解析GCP错误
|
| 817 |
+
var gcpErr struct {
|
| 818 |
+
Error struct {
|
| 819 |
+
Code int `json:"code"`
|
| 820 |
+
Message string `json:"message"`
|
| 821 |
+
Status string `json:"status"`
|
| 822 |
+
} `json:"error"`
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
if err := json.Unmarshal([]byte(errorBody), &gcpErr); err == nil {
|
| 826 |
+
if gcpErr.Error.Code == 429 && gcpErr.Error.Status == "RESOURCE_EXHAUSTED" {
|
| 827 |
+
// GCP限流错误
|
| 828 |
+
log.Printf("[Anthropic] GCP RESOURCE_EXHAUSTED 账号ID:%d %s", accountID, email)
|
| 829 |
+
return
|
| 830 |
+
}
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
// 其他未识别的429错误
|
| 834 |
+
log.Printf("[Anthropic] 429限流错误 账号ID:%d %s", accountID, email)
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
// MessagesProxy 直接代理请求和响应
|
| 838 |
+
func (s *AnthropicService) MessagesProxy(ctx context.Context, w http.ResponseWriter, body []byte) error {
|
| 839 |
+
var req struct {
|
| 840 |
+
Model string `json:"model"`
|
| 841 |
+
Stream bool `json:"stream"`
|
| 842 |
+
}
|
| 843 |
+
// 忽略错误,Messages方法会再次解析
|
| 844 |
+
_ = json.Unmarshal(body, &req)
|
| 845 |
+
|
| 846 |
+
resp, err := s.Messages(ctx, body, false)
|
| 847 |
+
if err != nil {
|
| 848 |
+
return err
|
| 849 |
+
}
|
| 850 |
+
defer resp.Body.Close()
|
| 851 |
+
|
| 852 |
+
// 判断是否需要过滤thinking内容
|
| 853 |
+
// 规则:如果用户调用的是非thinking版本,但平台强制开启了thinking,则需要过滤
|
| 854 |
+
needsFiltering := false
|
| 855 |
+
|
| 856 |
+
// 获取模型配置
|
| 857 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 858 |
+
|
| 859 |
+
// 如果模型配置中有thinking参数(平台强制thinking)
|
| 860 |
+
if exists && zenModel.Parameters != nil && zenModel.Parameters.Thinking != nil {
|
| 861 |
+
// 检查用户是否明确请求了thinking版本
|
| 862 |
+
// 如果模型ID不包含 "thinking" 后缀,说明用户要的是非thinking版本
|
| 863 |
+
if !strings.HasSuffix(req.Model, "-thinking") {
|
| 864 |
+
needsFiltering = true
|
| 865 |
+
}
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
if needsFiltering {
|
| 869 |
+
if req.Stream {
|
| 870 |
+
return s.streamFilteredResponse(w, resp)
|
| 871 |
+
}
|
| 872 |
+
return s.handleNonStreamFilteredResponse(w, resp)
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
return StreamResponse(w, resp)
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
func (s *AnthropicService) handleNonStreamFilteredResponse(w http.ResponseWriter, resp *http.Response) error {
|
| 879 |
+
// 读取全部响应体
|
| 880 |
+
bodyBytes, err := io.ReadAll(resp.Body)
|
| 881 |
+
if err != nil {
|
| 882 |
+
return err
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
// 复制响应头
|
| 886 |
+
for k, v := range resp.Header {
|
| 887 |
+
// 过滤掉 Content-Length 和 Content-Encoding
|
| 888 |
+
if k != "Content-Length" && k != "Content-Encoding" {
|
| 889 |
+
for _, vv := range v {
|
| 890 |
+
w.Header().Add(k, vv)
|
| 891 |
+
}
|
| 892 |
+
}
|
| 893 |
+
}
|
| 894 |
+
w.WriteHeader(resp.StatusCode)
|
| 895 |
+
|
| 896 |
+
// 尝试解析响应
|
| 897 |
+
var raw map[string]interface{}
|
| 898 |
+
if err := json.Unmarshal(bodyBytes, &raw); err != nil {
|
| 899 |
+
w.Write(bodyBytes)
|
| 900 |
+
return nil
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
// 过滤 content 中的 thinking block
|
| 904 |
+
if content, ok := raw["content"].([]interface{}); ok {
|
| 905 |
+
var newContent []interface{}
|
| 906 |
+
for _, block := range content {
|
| 907 |
+
if b, ok := block.(map[string]interface{}); ok {
|
| 908 |
+
if typeStr, ok := b["type"].(string); ok && (typeStr == "thinking" || typeStr == "thought") {
|
| 909 |
+
continue
|
| 910 |
+
}
|
| 911 |
+
}
|
| 912 |
+
newContent = append(newContent, block)
|
| 913 |
+
}
|
| 914 |
+
raw["content"] = newContent
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
return json.NewEncoder(w).Encode(raw)
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
// adjustTemperatureForModel 根据模型要求调整温度参数
|
| 921 |
+
func (s *AnthropicService) adjustTemperatureForModel(body []byte, modelID string) ([]byte, error) {
|
| 922 |
+
// 获取模型配置
|
| 923 |
+
zenModel, exists := model.GetZenModel(modelID)
|
| 924 |
+
|
| 925 |
+
// 检查模型配置中是否有特定的温度要求
|
| 926 |
+
if exists && zenModel.Parameters != nil && zenModel.Parameters.Temperature != nil {
|
| 927 |
+
return s.forceTemperature(body, *zenModel.Parameters.Temperature)
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
return body, nil
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
// forceTemperature 强制设置温度参数
|
| 934 |
+
func (s *AnthropicService) forceTemperature(body []byte, temperature float64) ([]byte, error) {
|
| 935 |
+
// 解析请求体
|
| 936 |
+
var reqMap map[string]interface{}
|
| 937 |
+
if err := json.Unmarshal(body, &reqMap); err != nil {
|
| 938 |
+
return body, nil // 如果解析失败,返回原始body
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
// 强制设置 temperature
|
| 942 |
+
reqMap["temperature"] = temperature
|
| 943 |
+
|
| 944 |
+
// 如果同时存在 top_p,移除它(某些模型不允许同时指定)
|
| 945 |
+
delete(reqMap, "top_p")
|
| 946 |
+
|
| 947 |
+
// 重新序列化
|
| 948 |
+
return json.Marshal(reqMap)
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
// removeTopP 移除 top_p 参数,避免与 temperature 冲突
|
| 952 |
+
func (s *AnthropicService) removeTopP(body []byte) ([]byte, error) {
|
| 953 |
+
// 解析请求体
|
| 954 |
+
var reqMap map[string]interface{}
|
| 955 |
+
if err := json.Unmarshal(body, &reqMap); err != nil {
|
| 956 |
+
return body, nil // 如果解析失败,返回原始body
|
| 957 |
+
}
|
| 958 |
+
|
| 959 |
+
// 移除 top_p 参数
|
| 960 |
+
delete(reqMap, "top_p")
|
| 961 |
+
|
| 962 |
+
// 重新序列化
|
| 963 |
+
return json.Marshal(reqMap)
|
| 964 |
+
}
|
| 965 |
+
|
| 966 |
+
// hasMatchingToolResult 检查消息中是否包含指定tool_use_id的tool_result
|
| 967 |
+
func hasMatchingToolResult(msg map[string]interface{}, toolUseID interface{}) bool {
|
| 968 |
+
if msg == nil || toolUseID == nil {
|
| 969 |
+
return false
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
toolUseIDStr, ok := toolUseID.(string)
|
| 973 |
+
if !ok {
|
| 974 |
+
return false
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
content, ok := msg["content"].([]interface{})
|
| 978 |
+
if !ok {
|
| 979 |
+
return false
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
for _, block := range content {
|
| 983 |
+
if b, ok := block.(map[string]interface{}); ok {
|
| 984 |
+
if b["type"] == "tool_result" {
|
| 985 |
+
if id, ok := b["tool_use_id"].(string); ok && id == toolUseIDStr {
|
| 986 |
+
return true
|
| 987 |
+
}
|
| 988 |
+
}
|
| 989 |
+
}
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
return false
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
// ensureThinkingConfig 确保需要 thinking 的模型有正确的配置
|
| 996 |
+
func (s *AnthropicService) ensureThinkingConfig(body []byte, modelID string) ([]byte, error) {
|
| 997 |
+
// 获取模型配置
|
| 998 |
+
zenModel, exists := model.GetZenModel(modelID)
|
| 999 |
+
|
| 1000 |
+
// 检查模型配置中是否包含thinking参数
|
| 1001 |
+
needsThinking := false
|
| 1002 |
+
var modelBudgetTokens int
|
| 1003 |
+
if exists && zenModel.Parameters != nil && zenModel.Parameters.Thinking != nil {
|
| 1004 |
+
needsThinking = true
|
| 1005 |
+
modelBudgetTokens = zenModel.Parameters.Thinking.BudgetTokens
|
| 1006 |
+
if modelBudgetTokens == 0 {
|
| 1007 |
+
modelBudgetTokens = 4096 // 默认值
|
| 1008 |
+
}
|
| 1009 |
+
}
|
| 1010 |
+
|
| 1011 |
+
if !needsThinking {
|
| 1012 |
+
return body, nil
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
// 解析请求体
|
| 1016 |
+
var reqMap map[string]interface{}
|
| 1017 |
+
if err := json.Unmarshal(body, &reqMap); err != nil {
|
| 1018 |
+
return body, nil
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
// 检查用户是否明确不想要thinking模式
|
| 1022 |
+
userDisablesThinking := false
|
| 1023 |
+
if existingThinking, ok := reqMap["thinking"].(map[string]interface{}); ok {
|
| 1024 |
+
if thinkingType, ok := existingThinking["type"].(string); ok && thinkingType == "disabled" {
|
| 1025 |
+
userDisablesThinking = true
|
| 1026 |
+
}
|
| 1027 |
+
if enabled, ok := existingThinking["enabled"].(bool); ok && !enabled {
|
| 1028 |
+
userDisablesThinking = true
|
| 1029 |
+
}
|
| 1030 |
+
} else {
|
| 1031 |
+
// 如果没有thinking配置,检查是否是非thinking版本的模型调用
|
| 1032 |
+
// 例如 claude-haiku-4-5-20251001 而不是 claude-haiku-4-5-20251001-thinking
|
| 1033 |
+
if !strings.HasSuffix(modelID, "-thinking") {
|
| 1034 |
+
userDisablesThinking = true
|
| 1035 |
+
}
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
// 如果用户不想要thinking但模型强制thinking,转换assistant消息为user消息
|
| 1039 |
+
if userDisablesThinking {
|
| 1040 |
+
if IsDebugMode() {
|
| 1041 |
+
log.Printf("[Anthropic] 用户不想要thinking模式,但模型强制thinking,转换assistant消息为user消息")
|
| 1042 |
+
}
|
| 1043 |
+
if messages, ok := reqMap["messages"].([]interface{}); ok {
|
| 1044 |
+
for i, msg := range messages {
|
| 1045 |
+
if msgMap, ok := msg.(map[string]interface{}); ok {
|
| 1046 |
+
if role, ok := msgMap["role"].(string); ok && role == "assistant" {
|
| 1047 |
+
// 转换thinking内容为text并改变角色为user
|
| 1048 |
+
if err := s.convertAssistantToUserMessage(msgMap); err != nil {
|
| 1049 |
+
log.Printf("[Anthropic] 转换assistant消息为user消息失败: %v", err)
|
| 1050 |
+
}
|
| 1051 |
+
}
|
| 1052 |
+
messages[i] = msgMap
|
| 1053 |
+
}
|
| 1054 |
+
}
|
| 1055 |
+
reqMap["messages"] = messages
|
| 1056 |
+
}
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
// 注意:即使有tool_choice,某些模型仍然需要thinking配置
|
| 1060 |
+
// 因此不再因为tool_choice的存在而跳过thinking配置
|
| 1061 |
+
|
| 1062 |
+
// 检查请求体中是否已有thinking配置
|
| 1063 |
+
if existingThinking, ok := reqMap["thinking"].(map[string]interface{}); ok {
|
| 1064 |
+
// 如果已有thinking配置,确保budget_tokens与模型配置一致
|
| 1065 |
+
if _, hasBudget := existingThinking["budget_tokens"]; hasBudget {
|
| 1066 |
+
// 强制使用模型配置中的budget_tokens值
|
| 1067 |
+
existingThinking["budget_tokens"] = modelBudgetTokens
|
| 1068 |
+
if IsDebugMode() {
|
| 1069 |
+
log.Printf("[Anthropic] 调整thinking.budget_tokens为模型配置值: %d", modelBudgetTokens)
|
| 1070 |
+
}
|
| 1071 |
+
} else {
|
| 1072 |
+
// 如果没有budget_tokens,添加
|
| 1073 |
+
existingThinking["budget_tokens"] = modelBudgetTokens
|
| 1074 |
+
}
|
| 1075 |
+
// 确保type字段正确
|
| 1076 |
+
if _, hasType := existingThinking["type"]; !hasType {
|
| 1077 |
+
existingThinking["type"] = "enabled"
|
| 1078 |
+
} else {
|
| 1079 |
+
// 强制启用thinking(因为模型要求)
|
| 1080 |
+
existingThinking["type"] = "enabled"
|
| 1081 |
+
}
|
| 1082 |
+
reqMap["thinking"] = existingThinking
|
| 1083 |
+
} else {
|
| 1084 |
+
// 添加 thinking 配置 - 使用模型配置中的值
|
| 1085 |
+
reqMap["thinking"] = map[string]interface{}{
|
| 1086 |
+
"type": "enabled",
|
| 1087 |
+
"budget_tokens": modelBudgetTokens,
|
| 1088 |
+
}
|
| 1089 |
+
if IsDebugMode() {
|
| 1090 |
+
log.Printf("[Anthropic] 添加thinking配置,budget_tokens: %d", modelBudgetTokens)
|
| 1091 |
+
}
|
| 1092 |
+
if IsDebugMode() {
|
| 1093 |
+
log.Printf("[Anthropic] 原始请求体 (处理前):")
|
| 1094 |
+
log.Printf("%s", sanitizeRequestBody(body))
|
| 1095 |
+
}
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
// 当启用 thinking 时,必须设置 temperature = 1.0
|
| 1099 |
+
reqMap["temperature"] = 1.0
|
| 1100 |
+
// 移除 top_p 以避免冲突
|
| 1101 |
+
delete(reqMap, "top_p")
|
| 1102 |
+
|
| 1103 |
+
// 注意:不再尝试为assistant消息添加thinking块,因为signature信息无法正确生成
|
| 1104 |
+
// 如果模型要求thinking模式但用户消息不符合格式,让API返回错误由上层处理
|
| 1105 |
+
|
| 1106 |
+
// 重新序列化
|
| 1107 |
+
modifiedBody, err := json.Marshal(reqMap)
|
| 1108 |
+
if err != nil {
|
| 1109 |
+
return body, err
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
// 输出处理后的请求体日志
|
| 1113 |
+
if IsDebugMode() {
|
| 1114 |
+
log.Printf("[Anthropic] 处理后的请求体 (发送给实际API):")
|
| 1115 |
+
log.Printf("%s", sanitizeRequestBody(modifiedBody))
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
return modifiedBody, nil
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
// 已移除fixAssistantMessageForThinking函数,因为signature信息无法正确生成
|
| 1122 |
+
|
| 1123 |
+
// convertThinkingToText 将thinking内容转换为普通文本格式(当用户不想要thinking模式时)
|
| 1124 |
+
func (s *AnthropicService) convertThinkingToText(msgMap map[string]interface{}) error {
|
| 1125 |
+
content, ok := msgMap["content"]
|
| 1126 |
+
if !ok {
|
| 1127 |
+
return nil
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
switch c := content.(type) {
|
| 1131 |
+
case []interface{}:
|
| 1132 |
+
var newContent []interface{}
|
| 1133 |
+
for _, block := range c {
|
| 1134 |
+
if blockMap, ok := block.(map[string]interface{}); ok {
|
| 1135 |
+
blockType, _ := blockMap["type"].(string)
|
| 1136 |
+
if blockType == "thinking" || blockType == "redacted_thinking" {
|
| 1137 |
+
// 将thinking块转换为text块
|
| 1138 |
+
if thinkingText, ok := blockMap["thinking"].(string); ok {
|
| 1139 |
+
newContent = append(newContent, map[string]interface{}{
|
| 1140 |
+
"type": "text",
|
| 1141 |
+
"text": "[thinking] " + thinkingText,
|
| 1142 |
+
})
|
| 1143 |
+
}
|
| 1144 |
+
} else {
|
| 1145 |
+
// 保留其他类型的块
|
| 1146 |
+
newContent = append(newContent, block)
|
| 1147 |
+
}
|
| 1148 |
+
}
|
| 1149 |
+
}
|
| 1150 |
+
msgMap["content"] = newContent
|
| 1151 |
+
if IsDebugMode() {
|
| 1152 |
+
log.Printf("[Anthropic] 将thinking块转换为普通文本格式")
|
| 1153 |
+
}
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
return nil
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
// convertAssistantToUserMessage 将assistant消息转换为user消息,避免thinking格式要求
|
| 1160 |
+
// 使用range循环逐个处理块,保留缓存信息,不合并消息
|
| 1161 |
+
func (s *AnthropicService) convertAssistantToUserMessage(msgMap map[string]interface{}) error {
|
| 1162 |
+
content, ok := msgMap["content"]
|
| 1163 |
+
if !ok {
|
| 1164 |
+
return nil
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
// 将角色从assistant改为user
|
| 1168 |
+
msgMap["role"] = "user"
|
| 1169 |
+
|
| 1170 |
+
switch c := content.(type) {
|
| 1171 |
+
case string:
|
| 1172 |
+
// 如果是字符串content,保持不变,只改角色
|
| 1173 |
+
if IsDebugMode() {
|
| 1174 |
+
log.Printf("[Anthropic] 将assistant字符串消息转换为user消息")
|
| 1175 |
+
}
|
| 1176 |
+
case []interface{}:
|
| 1177 |
+
// 使用range循环逐个处理每个块,保留结构和缓存信息
|
| 1178 |
+
for i, block := range c {
|
| 1179 |
+
if blockMap, ok := block.(map[string]interface{}); ok {
|
| 1180 |
+
blockType, _ := blockMap["type"].(string)
|
| 1181 |
+
|
| 1182 |
+
// 保留原有的缓存控制信息
|
| 1183 |
+
var cacheControl interface{}
|
| 1184 |
+
if cache, hasCacheControl := blockMap["cache_control"]; hasCacheControl {
|
| 1185 |
+
cacheControl = cache
|
| 1186 |
+
}
|
| 1187 |
+
|
| 1188 |
+
switch blockType {
|
| 1189 |
+
case "thinking", "redacted_thinking":
|
| 1190 |
+
// 将thinking块转换为text块,保留缓存信息
|
| 1191 |
+
if thinkingText, ok := blockMap["thinking"].(string); ok {
|
| 1192 |
+
newBlock := map[string]interface{}{
|
| 1193 |
+
"type": "text",
|
| 1194 |
+
"text": "[thinking] " + thinkingText,
|
| 1195 |
+
}
|
| 1196 |
+
if cacheControl != nil {
|
| 1197 |
+
newBlock["cache_control"] = cacheControl
|
| 1198 |
+
}
|
| 1199 |
+
c[i] = newBlock
|
| 1200 |
+
}
|
| 1201 |
+
case "tool_use":
|
| 1202 |
+
// 将tool_use块转换为text描述,保留缓存信息
|
| 1203 |
+
toolName, _ := blockMap["name"].(string)
|
| 1204 |
+
toolId, _ := blockMap["id"].(string)
|
| 1205 |
+
newBlock := map[string]interface{}{
|
| 1206 |
+
"type": "text",
|
| 1207 |
+
"text": fmt.Sprintf("[tool_use] %s (ID: %s)", toolName, toolId),
|
| 1208 |
+
}
|
| 1209 |
+
if cacheControl != nil {
|
| 1210 |
+
newBlock["cache_control"] = cacheControl
|
| 1211 |
+
}
|
| 1212 |
+
c[i] = newBlock
|
| 1213 |
+
case "tool_result":
|
| 1214 |
+
// 将tool_result块转换为text描述,保留缓存信息
|
| 1215 |
+
toolUseId, _ := blockMap["tool_use_id"].(string)
|
| 1216 |
+
isError, _ := blockMap["is_error"].(bool)
|
| 1217 |
+
var resultText string
|
| 1218 |
+
if isError {
|
| 1219 |
+
resultText = fmt.Sprintf("[tool_error] (ID: %s)", toolUseId)
|
| 1220 |
+
} else {
|
| 1221 |
+
resultText = fmt.Sprintf("[tool_result] (ID: %s)", toolUseId)
|
| 1222 |
+
}
|
| 1223 |
+
newBlock := map[string]interface{}{
|
| 1224 |
+
"type": "text",
|
| 1225 |
+
"text": resultText,
|
| 1226 |
+
}
|
| 1227 |
+
if cacheControl != nil {
|
| 1228 |
+
newBlock["cache_control"] = cacheControl
|
| 1229 |
+
}
|
| 1230 |
+
c[i] = newBlock
|
| 1231 |
+
default:
|
| 1232 |
+
// text块和其他类型的块保持不变,包括缓存信息
|
| 1233 |
+
// 不需要修改,保持原样
|
| 1234 |
+
}
|
| 1235 |
+
}
|
| 1236 |
+
// 非map类型的块也保持不变
|
| 1237 |
+
}
|
| 1238 |
+
|
| 1239 |
+
msgMap["content"] = c
|
| 1240 |
+
if IsDebugMode() {
|
| 1241 |
+
log.Printf("[Anthropic] 将assistant消息转换为user消息,逐个处理内容块并保留缓存信息")
|
| 1242 |
+
}
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
return nil
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
// convertAssistantMessagesToUser 将请求体中的所有assistant消息转换为user消息
|
| 1249 |
+
func (s *AnthropicService) convertAssistantMessagesToUser(body []byte) ([]byte, error) {
|
| 1250 |
+
// 解析请求体
|
| 1251 |
+
var reqMap map[string]interface{}
|
| 1252 |
+
if err := json.Unmarshal(body, &reqMap); err != nil {
|
| 1253 |
+
return body, err
|
| 1254 |
+
}
|
| 1255 |
+
|
| 1256 |
+
// 处理messages数组,同时处理工具调用关系
|
| 1257 |
+
if messages, ok := reqMap["messages"].([]interface{}); ok {
|
| 1258 |
+
for i, msg := range messages {
|
| 1259 |
+
if msgMap, ok := msg.(map[string]interface{}); ok {
|
| 1260 |
+
// 无论是assistant还是user消息,都要检查并转换工具相关块
|
| 1261 |
+
if role, ok := msgMap["role"].(string); ok {
|
| 1262 |
+
if role == "assistant" {
|
| 1263 |
+
// 转换assistant消息为user消息
|
| 1264 |
+
if err := s.convertAssistantToUserMessage(msgMap); err != nil {
|
| 1265 |
+
log.Printf("[Anthropic] 转换第%d个assistant消息失败: %v", i, err)
|
| 1266 |
+
continue
|
| 1267 |
+
}
|
| 1268 |
+
} else if role == "user" {
|
| 1269 |
+
// 对于user消息,也要确保tool_result被正确处理
|
| 1270 |
+
if err := s.convertToolBlocksToText(msgMap); err != nil {
|
| 1271 |
+
log.Printf("[Anthropic] 转换第%d个user消息中的工具块失败: %v", i, err)
|
| 1272 |
+
continue
|
| 1273 |
+
}
|
| 1274 |
+
}
|
| 1275 |
+
messages[i] = msgMap
|
| 1276 |
+
}
|
| 1277 |
+
}
|
| 1278 |
+
}
|
| 1279 |
+
reqMap["messages"] = messages
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
// 重新序列化
|
| 1283 |
+
modifiedBody, err := json.Marshal(reqMap)
|
| 1284 |
+
if err != nil {
|
| 1285 |
+
return body, err
|
| 1286 |
+
}
|
| 1287 |
+
|
| 1288 |
+
if IsDebugMode() {
|
| 1289 |
+
log.Printf("[Anthropic] 已转换所有工具调用消息,处理后的请求体:")
|
| 1290 |
+
log.Printf("%s", sanitizeRequestBody(modifiedBody))
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
return modifiedBody, nil
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
// convertToolBlocksToText 将消息中的所有工具相关块转换为文本
|
| 1297 |
+
func (s *AnthropicService) convertToolBlocksToText(msgMap map[string]interface{}) error {
|
| 1298 |
+
content, ok := msgMap["content"]
|
| 1299 |
+
if !ok {
|
| 1300 |
+
return nil
|
| 1301 |
+
}
|
| 1302 |
+
|
| 1303 |
+
switch c := content.(type) {
|
| 1304 |
+
case []interface{}:
|
| 1305 |
+
// 使用range循环逐个处理每个块,将工具相关块转换为文本
|
| 1306 |
+
for i, block := range c {
|
| 1307 |
+
if blockMap, ok := block.(map[string]interface{}); ok {
|
| 1308 |
+
blockType, _ := blockMap["type"].(string)
|
| 1309 |
+
|
| 1310 |
+
// 保留原有的缓存控制信息
|
| 1311 |
+
var cacheControl interface{}
|
| 1312 |
+
if cache, hasCacheControl := blockMap["cache_control"]; hasCacheControl {
|
| 1313 |
+
cacheControl = cache
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
switch blockType {
|
| 1317 |
+
case "tool_use":
|
| 1318 |
+
// 将tool_use块转换为text块
|
| 1319 |
+
toolName, _ := blockMap["name"].(string)
|
| 1320 |
+
toolId, _ := blockMap["id"].(string)
|
| 1321 |
+
newBlock := map[string]interface{}{
|
| 1322 |
+
"type": "text",
|
| 1323 |
+
"text": fmt.Sprintf("[tool_use] %s (ID: %s)", toolName, toolId),
|
| 1324 |
+
}
|
| 1325 |
+
if cacheControl != nil {
|
| 1326 |
+
newBlock["cache_control"] = cacheControl
|
| 1327 |
+
}
|
| 1328 |
+
c[i] = newBlock
|
| 1329 |
+
case "tool_result":
|
| 1330 |
+
// 将tool_result块转换为text块
|
| 1331 |
+
toolUseId, _ := blockMap["tool_use_id"].(string)
|
| 1332 |
+
isError, _ := blockMap["is_error"].(bool)
|
| 1333 |
+
var resultText string
|
| 1334 |
+
if isError {
|
| 1335 |
+
resultText = fmt.Sprintf("[tool_error] (ID: %s)", toolUseId)
|
| 1336 |
+
} else {
|
| 1337 |
+
resultText = fmt.Sprintf("[tool_result] (ID: %s)", toolUseId)
|
| 1338 |
+
}
|
| 1339 |
+
newBlock := map[string]interface{}{
|
| 1340 |
+
"type": "text",
|
| 1341 |
+
"text": resultText,
|
| 1342 |
+
}
|
| 1343 |
+
if cacheControl != nil {
|
| 1344 |
+
newBlock["cache_control"] = cacheControl
|
| 1345 |
+
}
|
| 1346 |
+
c[i] = newBlock
|
| 1347 |
+
default:
|
| 1348 |
+
// text块和其他类型的块保持不变
|
| 1349 |
+
// 不需要修改,保持原样
|
| 1350 |
+
}
|
| 1351 |
+
}
|
| 1352 |
+
}
|
| 1353 |
+
|
| 1354 |
+
msgMap["content"] = c
|
| 1355 |
+
if IsDebugMode() {
|
| 1356 |
+
log.Printf("[Anthropic] 已将消息中的工具块转换为文本格式")
|
| 1357 |
+
}
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
return nil
|
| 1361 |
+
}
|
| 1362 |
+
|
| 1363 |
+
// adjustParametersForModel 根据模型要求调整参数,避免冲突
|
| 1364 |
+
func (s *AnthropicService) adjustParametersForModel(body []byte, modelID string) ([]byte, error) {
|
| 1365 |
+
// 对于 claude-opus-4-5-20251101 等模型,不能同时有 temperature 和 top_p
|
| 1366 |
+
modelsNoTopP := []string{
|
| 1367 |
+
"claude-opus-4-5-20251101",
|
| 1368 |
+
"claude-opus-4-1-20250805",
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
for _, model := range modelsNoTopP {
|
| 1372 |
+
if modelID == model {
|
| 1373 |
+
body, _ = s.removeTopP(body)
|
| 1374 |
+
break
|
| 1375 |
+
}
|
| 1376 |
+
}
|
| 1377 |
+
|
| 1378 |
+
// 继续处理温度参数
|
| 1379 |
+
return s.adjustTemperatureForModel(body, modelID)
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
func (s *AnthropicService) streamFilteredResponse(w http.ResponseWriter, resp *http.Response) error {
|
| 1383 |
+
// 复制响应头
|
| 1384 |
+
for k, v := range resp.Header {
|
| 1385 |
+
if k != "Content-Encoding" && k != "Content-Length" {
|
| 1386 |
+
for _, vv := range v {
|
| 1387 |
+
w.Header().Add(k, vv)
|
| 1388 |
+
}
|
| 1389 |
+
}
|
| 1390 |
+
}
|
| 1391 |
+
w.WriteHeader(resp.StatusCode)
|
| 1392 |
+
|
| 1393 |
+
flusher, ok := w.(http.Flusher)
|
| 1394 |
+
if !ok {
|
| 1395 |
+
_, err := io.Copy(w, resp.Body)
|
| 1396 |
+
return err
|
| 1397 |
+
}
|
| 1398 |
+
|
| 1399 |
+
reader := bufio.NewReader(resp.Body)
|
| 1400 |
+
isThinking := false // 标记当前是否处于 thinking block 中
|
| 1401 |
+
|
| 1402 |
+
for {
|
| 1403 |
+
line, err := reader.ReadString('\n')
|
| 1404 |
+
if err != nil {
|
| 1405 |
+
if err == io.EOF {
|
| 1406 |
+
return nil
|
| 1407 |
+
}
|
| 1408 |
+
return err
|
| 1409 |
+
}
|
| 1410 |
+
|
| 1411 |
+
trimmedLine := strings.TrimSpace(line)
|
| 1412 |
+
if trimmedLine == "" {
|
| 1413 |
+
fmt.Fprintf(w, "\n")
|
| 1414 |
+
flusher.Flush()
|
| 1415 |
+
continue
|
| 1416 |
+
}
|
| 1417 |
+
|
| 1418 |
+
if strings.HasPrefix(trimmedLine, "event:") {
|
| 1419 |
+
// 读取下一行 data
|
| 1420 |
+
dataLine, err := reader.ReadString('\n')
|
| 1421 |
+
if err != nil {
|
| 1422 |
+
return err
|
| 1423 |
+
}
|
| 1424 |
+
|
| 1425 |
+
// 解析 event 类型
|
| 1426 |
+
event := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "event:"))
|
| 1427 |
+
data := strings.TrimSpace(strings.TrimPrefix(dataLine, "data:"))
|
| 1428 |
+
|
| 1429 |
+
var shouldFilter bool
|
| 1430 |
+
|
| 1431 |
+
if event == "content_block_start" {
|
| 1432 |
+
var payload struct {
|
| 1433 |
+
ContentBlock struct {
|
| 1434 |
+
Type string `json:"type"`
|
| 1435 |
+
} `json:"content_block"`
|
| 1436 |
+
}
|
| 1437 |
+
if json.Unmarshal([]byte(data), &payload) == nil {
|
| 1438 |
+
if payload.ContentBlock.Type == "thinking" || payload.ContentBlock.Type == "thought" {
|
| 1439 |
+
isThinking = true
|
| 1440 |
+
shouldFilter = true
|
| 1441 |
+
}
|
| 1442 |
+
|
| 1443 |
+
}
|
| 1444 |
+
} else if event == "content_block_delta" {
|
| 1445 |
+
if isThinking {
|
| 1446 |
+
shouldFilter = true
|
| 1447 |
+
}
|
| 1448 |
+
} else if event == "content_block_stop" {
|
| 1449 |
+
if isThinking {
|
| 1450 |
+
shouldFilter = true
|
| 1451 |
+
isThinking = false
|
| 1452 |
+
}
|
| 1453 |
+
}
|
| 1454 |
+
|
| 1455 |
+
if !shouldFilter {
|
| 1456 |
+
fmt.Fprint(w, line) // event: ...
|
| 1457 |
+
fmt.Fprint(w, dataLine) // data: ...
|
| 1458 |
+
flusher.Flush()
|
| 1459 |
+
}
|
| 1460 |
+
} else {
|
| 1461 |
+
// 其他格式(如 ping),直接透传
|
| 1462 |
+
fmt.Fprint(w, line)
|
| 1463 |
+
flusher.Flush()
|
| 1464 |
+
}
|
| 1465 |
+
}
|
| 1466 |
+
}
|
| 1467 |
+
|
| 1468 |
+
// retryWithProxy 使用代理池重试请求
|
| 1469 |
+
func (s *AnthropicService) retryWithProxy(ctx context.Context, account *model.Account, modelID string, body []byte) (*http.Response, error) {
|
| 1470 |
+
// 获取模型配置
|
| 1471 |
+
zenModel, exists := model.GetZenModel(modelID)
|
| 1472 |
+
if !exists {
|
| 1473 |
+
return nil, fmt.Errorf("模型配置不存在: %s", modelID)
|
| 1474 |
+
}
|
| 1475 |
+
|
| 1476 |
+
// 预处理请求体 - 确保包含所需的thinking配置和参数调整
|
| 1477 |
+
processedBody, err := s.preprocessRequestBody(body, modelID, zenModel)
|
| 1478 |
+
if err != nil {
|
| 1479 |
+
log.Printf("[Anthropic] 代理重试请求体预处理失败: %v", err)
|
| 1480 |
+
// 如果预处理失败,使用原始body
|
| 1481 |
+
processedBody = body
|
| 1482 |
+
}
|
| 1483 |
+
|
| 1484 |
+
proxyPool := provider.GetProxyPool()
|
| 1485 |
+
if !proxyPool.HasProxies() {
|
| 1486 |
+
return nil, fmt.Errorf("没有可用的代理")
|
| 1487 |
+
}
|
| 1488 |
+
|
| 1489 |
+
maxRetries := 3
|
| 1490 |
+
for i := 0; i < maxRetries; i++ {
|
| 1491 |
+
// 获取随机代理
|
| 1492 |
+
proxyURL := proxyPool.GetRandomProxy()
|
| 1493 |
+
if proxyURL == "" {
|
| 1494 |
+
continue
|
| 1495 |
+
}
|
| 1496 |
+
|
| 1497 |
+
log.Printf("[Anthropic] 尝试代理 %s (重试 %d/%d)", proxyURL, i+1, maxRetries)
|
| 1498 |
+
|
| 1499 |
+
// 创建使用代理的HTTP客户端
|
| 1500 |
+
proxyClient, err := provider.NewHTTPClientWithProxy(proxyURL, 0)
|
| 1501 |
+
if err != nil {
|
| 1502 |
+
log.Printf("[Anthropic] 创建代理客户端失败: %v", err)
|
| 1503 |
+
continue
|
| 1504 |
+
}
|
| 1505 |
+
|
| 1506 |
+
// 创建新请求
|
| 1507 |
+
httpReq, err := http.NewRequest("POST", AnthropicBaseURL+"/v1/messages", bytes.NewReader(processedBody))
|
| 1508 |
+
if err != nil {
|
| 1509 |
+
log.Printf("[Anthropic] 创建请求失败: %v", err)
|
| 1510 |
+
continue
|
| 1511 |
+
}
|
| 1512 |
+
|
| 1513 |
+
// 设置请求头
|
| 1514 |
+
SetZencoderHeaders(httpReq, account, zenModel)
|
| 1515 |
+
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
| 1516 |
+
|
| 1517 |
+
// 添加模型配置的额外请求头
|
| 1518 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 1519 |
+
for k, v := range zenModel.Parameters.ExtraHeaders {
|
| 1520 |
+
httpReq.Header.Set(k, v)
|
| 1521 |
+
}
|
| 1522 |
+
}
|
| 1523 |
+
|
| 1524 |
+
// 只在非限速测试且调试模式下记录代理请求详情
|
| 1525 |
+
var reqCheck struct {
|
| 1526 |
+
Model string `json:"model"`
|
| 1527 |
+
}
|
| 1528 |
+
if IsDebugMode() && json.Unmarshal(body, &reqCheck) == nil && !strings.Contains(reqCheck.Model, "test") {
|
| 1529 |
+
log.Printf("[Anthropic] 代理请求详情 - URL: %s", httpReq.URL.String())
|
| 1530 |
+
logRequestDetails("[Anthropic] 代理请求", httpReq.Header, processedBody)
|
| 1531 |
+
}
|
| 1532 |
+
|
| 1533 |
+
// 执行请求
|
| 1534 |
+
resp, err := proxyClient.Do(httpReq)
|
| 1535 |
+
if err != nil {
|
| 1536 |
+
log.Printf("[Anthropic] 代理请求失败: %v", err)
|
| 1537 |
+
continue
|
| 1538 |
+
}
|
| 1539 |
+
|
| 1540 |
+
// 检查响应状态
|
| 1541 |
+
if resp.StatusCode == 429 {
|
| 1542 |
+
// 仍然是429,尝试下一个代理
|
| 1543 |
+
resp.Body.Close()
|
| 1544 |
+
log.Printf("[Anthropic] 代理 %s 仍返回429,尝试下一个", proxyURL)
|
| 1545 |
+
continue
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
if resp.StatusCode >= 400 {
|
| 1549 |
+
// 其他错误,记录并尝试下一个代理
|
| 1550 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 1551 |
+
resp.Body.Close()
|
| 1552 |
+
|
| 1553 |
+
// 解析thinking状态
|
| 1554 |
+
thinkingStatus := "disabled"
|
| 1555 |
+
var reqCheck struct {
|
| 1556 |
+
Thinking map[string]interface{} `json:"thinking,omitempty"`
|
| 1557 |
+
}
|
| 1558 |
+
json.Unmarshal(body, &reqCheck)
|
| 1559 |
+
if reqCheck.Thinking != nil {
|
| 1560 |
+
if enabled, ok := reqCheck.Thinking["enabled"].(bool); ok && enabled {
|
| 1561 |
+
thinkingStatus = "enabled"
|
| 1562 |
+
} else if thinkingType, ok := reqCheck.Thinking["type"].(string); ok && thinkingType == "enabled" {
|
| 1563 |
+
thinkingStatus = "enabled"
|
| 1564 |
+
}
|
| 1565 |
+
// 如果有thinking配置且有budget_tokens,也记录
|
| 1566 |
+
if budget, ok := reqCheck.Thinking["budget_tokens"].(float64); ok && budget > 0 {
|
| 1567 |
+
thinkingStatus = fmt.Sprintf("enabled(budget=%g)", budget)
|
| 1568 |
+
}
|
| 1569 |
+
}
|
| 1570 |
+
|
| 1571 |
+
log.Printf("[Anthropic] 代理 %s 返回错误 %d: %s (Model: %s, Thinking: %s)", proxyURL, resp.StatusCode, string(errBody), modelID, thinkingStatus)
|
| 1572 |
+
continue
|
| 1573 |
+
}
|
| 1574 |
+
|
| 1575 |
+
// 成功
|
| 1576 |
+
log.Printf("[Anthropic] 代理 %s 请求成功", proxyURL)
|
| 1577 |
+
return resp, nil
|
| 1578 |
+
}
|
| 1579 |
+
|
| 1580 |
+
return nil, fmt.Errorf("所有代理重试均失败")
|
| 1581 |
+
}
|
| 1582 |
+
|
| 1583 |
+
// preprocessRequestBody 预处理请求体,应用所有必要的配置和调整
|
| 1584 |
+
func (s *AnthropicService) preprocessRequestBody(body []byte, modelID string, zenModel model.ZenModel) ([]byte, error) {
|
| 1585 |
+
// 注意:已移除模型替换逻辑,直接使用原始请求体
|
| 1586 |
+
modifiedBody := body
|
| 1587 |
+
|
| 1588 |
+
// 2. 确保thinking配置
|
| 1589 |
+
var err error
|
| 1590 |
+
modifiedBody, err = s.ensureThinkingConfig(modifiedBody, modelID)
|
| 1591 |
+
if err != nil {
|
| 1592 |
+
return modifiedBody, fmt.Errorf("确保thinking配置失败: %w", err)
|
| 1593 |
+
}
|
| 1594 |
+
|
| 1595 |
+
// 3. 根据模型调整参数
|
| 1596 |
+
modifiedBody, err = s.adjustParametersForModel(modifiedBody, modelID)
|
| 1597 |
+
if err != nil {
|
| 1598 |
+
return modifiedBody, fmt.Errorf("调整模型参数失败: %w", err)
|
| 1599 |
+
}
|
| 1600 |
+
|
| 1601 |
+
return modifiedBody, nil
|
| 1602 |
+
}
|
internal/service/api.go
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"net/http"
|
| 6 |
+
|
| 7 |
+
"zencoder-2api/internal/model"
|
| 8 |
+
"zencoder-2api/internal/service/provider"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type APIService struct {
|
| 12 |
+
manager *provider.Manager
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
func NewAPIService() *APIService {
|
| 16 |
+
return &APIService{
|
| 17 |
+
manager: provider.GetManager(),
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func (s *APIService) Chat(req *model.ChatCompletionRequest) (*model.ChatCompletionResponse, error) {
|
| 22 |
+
// 检查模型是否存在于模型字典中
|
| 23 |
+
_, exists := model.GetZenModel(req.Model)
|
| 24 |
+
if !exists {
|
| 25 |
+
return nil, ErrNoAvailableAccount
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
var lastErr error
|
| 29 |
+
|
| 30 |
+
for i := 0; i < MaxRetries; i++ {
|
| 31 |
+
account, err := GetNextAccountForModel(req.Model)
|
| 32 |
+
if err != nil {
|
| 33 |
+
return nil, err
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
resp, err := s.doChat(account, req)
|
| 37 |
+
if err != nil {
|
| 38 |
+
MarkAccountError(account)
|
| 39 |
+
lastErr = err
|
| 40 |
+
continue
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
ResetAccountError(account)
|
| 44 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 45 |
+
if !exists {
|
| 46 |
+
// 模型不存在,使用默认倍率
|
| 47 |
+
UseCredit(account, 1.0)
|
| 48 |
+
} else {
|
| 49 |
+
// API服务没有HTTP响应,只能使用模型倍率
|
| 50 |
+
UseCredit(account, zenModel.Multiplier)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
return resp, nil
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
return nil, fmt.Errorf("all retries failed: %w", lastErr)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func (s *APIService) doChat(account *model.Account, req *model.ChatCompletionRequest) (*model.ChatCompletionResponse, error) {
|
| 60 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 61 |
+
if !exists {
|
| 62 |
+
return nil, ErrNoAvailableAccount
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
cfg := s.buildConfig(account, zenModel)
|
| 66 |
+
p, err := s.manager.GetProvider(account.ID, zenModel, cfg)
|
| 67 |
+
if err != nil {
|
| 68 |
+
return nil, err
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// 注意:已移除模型重定向逻辑,直接使用用户请求的模型名
|
| 72 |
+
|
| 73 |
+
return p.Chat(req)
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
func (s *APIService) buildConfig(account *model.Account, zenModel model.ZenModel) provider.Config {
|
| 77 |
+
cfg := provider.Config{
|
| 78 |
+
APIKey: account.AccessToken,
|
| 79 |
+
Proxy: account.Proxy,
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// 设置额外请求头
|
| 83 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 84 |
+
cfg.ExtraHeaders = zenModel.Parameters.ExtraHeaders
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
return cfg
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
func (s *APIService) ChatStream(req *model.ChatCompletionRequest, writer http.ResponseWriter) error {
|
| 91 |
+
// 检查模型是否存在于模型字典中
|
| 92 |
+
_, exists := model.GetZenModel(req.Model)
|
| 93 |
+
if !exists {
|
| 94 |
+
return ErrNoAvailableAccount
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
var lastErr error
|
| 98 |
+
|
| 99 |
+
for i := 0; i < MaxRetries; i++ {
|
| 100 |
+
account, err := GetNextAccountForModel(req.Model)
|
| 101 |
+
if err != nil {
|
| 102 |
+
return err
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
err = s.doChatStream(account, req, writer)
|
| 106 |
+
if err != nil {
|
| 107 |
+
MarkAccountError(account)
|
| 108 |
+
lastErr = err
|
| 109 |
+
continue
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
ResetAccountError(account)
|
| 113 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 114 |
+
if !exists {
|
| 115 |
+
// 模型不存在,使用默认倍率
|
| 116 |
+
UseCredit(account, 1.0)
|
| 117 |
+
} else {
|
| 118 |
+
// 流式响应,使用模型倍率
|
| 119 |
+
UseCredit(account, zenModel.Multiplier)
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
return nil
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return fmt.Errorf("all retries failed: %w", lastErr)
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
func (s *APIService) doChatStream(account *model.Account, req *model.ChatCompletionRequest, writer http.ResponseWriter) error {
|
| 129 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 130 |
+
if !exists {
|
| 131 |
+
return ErrNoAvailableAccount
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
cfg := s.buildConfig(account, zenModel)
|
| 135 |
+
p, err := s.manager.GetProvider(account.ID, zenModel, cfg)
|
| 136 |
+
if err != nil {
|
| 137 |
+
return err
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// 注意:已移除模型重定向逻辑,直接使用用户请求的模型名
|
| 141 |
+
|
| 142 |
+
return p.ChatStream(req, writer)
|
| 143 |
+
}
|
internal/service/autogen.go
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"errors"
|
| 5 |
+
"fmt"
|
| 6 |
+
"log"
|
| 7 |
+
"strings"
|
| 8 |
+
"sync"
|
| 9 |
+
"time"
|
| 10 |
+
"zencoder-2api/internal/database"
|
| 11 |
+
"zencoder-2api/internal/model"
|
| 12 |
+
|
| 13 |
+
"gorm.io/gorm"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
type AutoGenerationService struct {
|
| 17 |
+
mu sync.Mutex
|
| 18 |
+
lastTriggered map[uint]time.Time // tokenID -> last triggered time
|
| 19 |
+
isGenerating map[uint]bool // tokenID -> is generating
|
| 20 |
+
debounceTime time.Duration // 防抖时间
|
| 21 |
+
generationDelay time.Duration // 生成任务间隔时间
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
var autoGenService *AutoGenerationService
|
| 25 |
+
|
| 26 |
+
func InitAutoGenerationService() {
|
| 27 |
+
autoGenService = &AutoGenerationService{
|
| 28 |
+
lastTriggered: make(map[uint]time.Time),
|
| 29 |
+
isGenerating: make(map[uint]bool),
|
| 30 |
+
debounceTime: 5 * time.Minute, // 5分钟防抖
|
| 31 |
+
generationDelay: 1 * time.Hour, // 生成任务间隔1小时
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// 启动监控协程
|
| 35 |
+
go autoGenService.startMonitoring()
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// SaveGenerationToken 保存生成模式使用的token
|
| 39 |
+
func SaveGenerationToken(token string, description string) error {
|
| 40 |
+
db := database.GetDB()
|
| 41 |
+
|
| 42 |
+
// 检查是否已存在
|
| 43 |
+
var existing model.TokenRecord
|
| 44 |
+
if err := db.Where("token = ?", token).First(&existing).Error; err == nil {
|
| 45 |
+
// 更新最后生成时间
|
| 46 |
+
existing.LastGeneratedAt = time.Now()
|
| 47 |
+
existing.GeneratedCount += 1
|
| 48 |
+
return db.Save(&existing).Error
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// 创建新记录
|
| 52 |
+
record := model.TokenRecord{
|
| 53 |
+
Token: token,
|
| 54 |
+
Description: description,
|
| 55 |
+
GeneratedCount: 1,
|
| 56 |
+
LastGeneratedAt: time.Now(),
|
| 57 |
+
AutoGenerate: true,
|
| 58 |
+
Threshold: 10,
|
| 59 |
+
GenerateBatch: 30,
|
| 60 |
+
IsActive: true,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return db.Create(&record).Error
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// SaveGenerationTokenWithRefresh 保存生成模式使用的 refresh_token
|
| 67 |
+
func SaveGenerationTokenWithRefresh(refreshToken string, accessToken string, description string, expiresIn int) error {
|
| 68 |
+
db := database.GetDB()
|
| 69 |
+
|
| 70 |
+
// 计算过期时间
|
| 71 |
+
expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second)
|
| 72 |
+
|
| 73 |
+
// 解析JWT获取用户信息,特别是邮箱
|
| 74 |
+
var email, planType string
|
| 75 |
+
var subscriptionDate time.Time
|
| 76 |
+
|
| 77 |
+
if accessToken != "" {
|
| 78 |
+
if payload, err := ParseJWT(accessToken); err == nil {
|
| 79 |
+
email = payload.Email
|
| 80 |
+
planType = payload.CustomClaims.Plan
|
| 81 |
+
if planType != "" {
|
| 82 |
+
planType = strings.ToUpper(planType[:1]) + planType[1:]
|
| 83 |
+
}
|
| 84 |
+
subscriptionDate = GetSubscriptionDate(payload)
|
| 85 |
+
log.Printf("[SaveGenerationToken] 解析JWT成功: Email=%s, Plan=%s, SubStart=%s",
|
| 86 |
+
email, planType, subscriptionDate.Format("2006-01-02"))
|
| 87 |
+
} else {
|
| 88 |
+
log.Printf("[SaveGenerationToken] 解析JWT失败: %v", err)
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// 如果有邮箱,按邮箱查找;否则按refresh_token查找
|
| 93 |
+
var existing model.TokenRecord
|
| 94 |
+
var err error
|
| 95 |
+
|
| 96 |
+
if email != "" {
|
| 97 |
+
// 优先按邮箱查找,实现相同邮箱的记录合并
|
| 98 |
+
err = db.Where("email = ?", email).First(&existing).Error
|
| 99 |
+
} else {
|
| 100 |
+
// 没有邮箱时,按refresh_token查找
|
| 101 |
+
err = db.Where("refresh_token = ?", refreshToken).First(&existing).Error
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
if err == nil {
|
| 105 |
+
// 更新现有记录
|
| 106 |
+
updates := map[string]interface{}{
|
| 107 |
+
"token": accessToken,
|
| 108 |
+
"refresh_token": refreshToken,
|
| 109 |
+
"token_expiry": expiresAt,
|
| 110 |
+
"description": description,
|
| 111 |
+
"updated_at": time.Now(),
|
| 112 |
+
"plan_type": planType,
|
| 113 |
+
"subscription_start_date": subscriptionDate,
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// 如果之前没有refresh_token,标记为有
|
| 117 |
+
if existing.RefreshToken == "" {
|
| 118 |
+
updates["has_refresh_token"] = true
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
return db.Model(&existing).Updates(updates).Error
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// 创建新记录
|
| 125 |
+
record := model.TokenRecord{
|
| 126 |
+
Token: accessToken,
|
| 127 |
+
RefreshToken: refreshToken,
|
| 128 |
+
TokenExpiry: expiresAt,
|
| 129 |
+
Description: description,
|
| 130 |
+
Email: email,
|
| 131 |
+
PlanType: planType,
|
| 132 |
+
SubscriptionStartDate: subscriptionDate,
|
| 133 |
+
HasRefreshToken: true,
|
| 134 |
+
CreatedAt: time.Now(),
|
| 135 |
+
UpdatedAt: time.Now(),
|
| 136 |
+
AutoGenerate: true,
|
| 137 |
+
Threshold: 10,
|
| 138 |
+
GenerateBatch: 30,
|
| 139 |
+
IsActive: true,
|
| 140 |
+
GeneratedCount: 0,
|
| 141 |
+
TotalSuccess: 0,
|
| 142 |
+
TotalFail: 0,
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
if err := db.Create(&record).Error; err != nil {
|
| 146 |
+
return fmt.Errorf("failed to save generation token: %w", err)
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
return nil
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// GetActiveTokenRecords 获取所有活跃的token记录
|
| 153 |
+
func GetActiveTokenRecords() ([]model.TokenRecord, error) {
|
| 154 |
+
var records []model.TokenRecord
|
| 155 |
+
err := database.GetDB().Where("is_active = ?", true).Find(&records).Error
|
| 156 |
+
return records, err
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// GetAllTokenRecords 获取所有token记录
|
| 160 |
+
func GetAllTokenRecords() ([]model.TokenRecord, error) {
|
| 161 |
+
var records []model.TokenRecord
|
| 162 |
+
err := database.GetDB().Order("created_at DESC").Find(&records).Error
|
| 163 |
+
return records, err
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// GetGenerationTasks 获取生成任务历史
|
| 167 |
+
func GetGenerationTasks(tokenRecordID uint) ([]model.GenerationTask, error) {
|
| 168 |
+
var tasks []model.GenerationTask
|
| 169 |
+
query := database.GetDB().Order("created_at DESC")
|
| 170 |
+
if tokenRecordID > 0 {
|
| 171 |
+
query = query.Where("token_record_id = ?", tokenRecordID)
|
| 172 |
+
}
|
| 173 |
+
err := query.Find(&tasks).Error
|
| 174 |
+
return tasks, err
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// UpdateTokenRecord 更新token记录设置
|
| 178 |
+
func UpdateTokenRecord(id uint, updates map[string]interface{}) error {
|
| 179 |
+
return database.GetDB().Model(&model.TokenRecord{}).Where("id = ?", id).Updates(updates).Error
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// 监控账号池并触发自动生成
|
| 183 |
+
func (s *AutoGenerationService) startMonitoring() {
|
| 184 |
+
ticker := time.NewTicker(1 * time.Minute) // 每分钟检查一次
|
| 185 |
+
defer ticker.Stop()
|
| 186 |
+
|
| 187 |
+
for range ticker.C {
|
| 188 |
+
s.checkAndTriggerGeneration()
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// 检查并触发生成
|
| 193 |
+
func (s *AutoGenerationService) checkAndTriggerGeneration() {
|
| 194 |
+
// 获取所有活跃的token记录
|
| 195 |
+
records, err := GetActiveTokenRecords()
|
| 196 |
+
if err != nil {
|
| 197 |
+
log.Printf("[AutoGen] 获取token记录失败: %v", err)
|
| 198 |
+
return
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// 计算当前可用账号数量
|
| 202 |
+
var activeAccountCount int64
|
| 203 |
+
database.GetDB().Model(&model.Account{}).
|
| 204 |
+
Where("status = ?", "normal").
|
| 205 |
+
Where("token_expiry > ?", time.Now()).
|
| 206 |
+
Count(&activeAccountCount)
|
| 207 |
+
|
| 208 |
+
log.Printf("[AutoGen] 当前活跃账号数量: %d", activeAccountCount)
|
| 209 |
+
|
| 210 |
+
// 检查每个token记录的阈值
|
| 211 |
+
for _, record := range records {
|
| 212 |
+
if !record.AutoGenerate || !record.IsActive {
|
| 213 |
+
continue
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
// 检查是否达到阈值
|
| 217 |
+
if int(activeAccountCount) <= record.Threshold {
|
| 218 |
+
s.triggerGeneration(record)
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// 触发生成任务(带防抖)
|
| 224 |
+
func (s *AutoGenerationService) triggerGeneration(record model.TokenRecord) {
|
| 225 |
+
s.mu.Lock()
|
| 226 |
+
defer s.mu.Unlock()
|
| 227 |
+
|
| 228 |
+
// 检查是否正在生成
|
| 229 |
+
if s.isGenerating[record.ID] {
|
| 230 |
+
log.Printf("[AutoGen] Token %d 正在生成中,跳过", record.ID)
|
| 231 |
+
return
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// 检查防抖时间
|
| 235 |
+
if lastTime, ok := s.lastTriggered[record.ID]; ok {
|
| 236 |
+
if time.Since(lastTime) < s.debounceTime {
|
| 237 |
+
log.Printf("[AutoGen] Token %d 防抖中,距上次触发 %v", record.ID, time.Since(lastTime))
|
| 238 |
+
return
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// 检查生成间隔
|
| 243 |
+
if !record.LastGeneratedAt.IsZero() && time.Since(record.LastGeneratedAt) < s.generationDelay {
|
| 244 |
+
log.Printf("[AutoGen] Token %d 未达到生成间隔时间,距上次生成 %v", record.ID, time.Since(record.LastGeneratedAt))
|
| 245 |
+
return
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// 标记开始生成
|
| 249 |
+
s.isGenerating[record.ID] = true
|
| 250 |
+
s.lastTriggered[record.ID] = time.Now()
|
| 251 |
+
|
| 252 |
+
// 异步执行生成任务
|
| 253 |
+
go s.executeGeneration(record)
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
// 执行生成任务
|
| 257 |
+
func (s *AutoGenerationService) executeGeneration(record model.TokenRecord) {
|
| 258 |
+
defer func() {
|
| 259 |
+
s.mu.Lock()
|
| 260 |
+
s.isGenerating[record.ID] = false
|
| 261 |
+
s.mu.Unlock()
|
| 262 |
+
}()
|
| 263 |
+
|
| 264 |
+
log.Printf("[AutoGen] 开始自动生成任务 - Token %d, 批次大小: %d", record.ID, record.GenerateBatch)
|
| 265 |
+
|
| 266 |
+
// 检查token记录状态
|
| 267 |
+
if record.Status != "active" {
|
| 268 |
+
log.Printf("[AutoGen] Token记录 %d 状态异常 (%s),跳过生成任务", record.ID, record.Status)
|
| 269 |
+
return
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// 检查token是否需要刷新
|
| 273 |
+
if record.RefreshToken != "" && time.Now().After(record.TokenExpiry.Add(-time.Hour)) {
|
| 274 |
+
log.Printf("[AutoGen] Token记录 %d 的token即将过期,尝试刷新", record.ID)
|
| 275 |
+
if err := UpdateTokenRecordToken(&record); err != nil {
|
| 276 |
+
log.Printf("[AutoGen] Token记录 %d 刷新失败,停止生成任务: %v", record.ID, err)
|
| 277 |
+
return
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// 创建生成任务记录
|
| 282 |
+
task := model.GenerationTask{
|
| 283 |
+
TokenRecordID: record.ID,
|
| 284 |
+
Token: record.Token,
|
| 285 |
+
BatchSize: record.GenerateBatch,
|
| 286 |
+
Status: "running",
|
| 287 |
+
StartedAt: time.Now(),
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
if err := database.GetDB().Create(&task).Error; err != nil {
|
| 291 |
+
log.Printf("[AutoGen] 创建任务记录失败: %v", err)
|
| 292 |
+
return
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// 批量生成凭证
|
| 296 |
+
credentials, errs := BatchGenerateCredentials(record.Token, record.GenerateBatch)
|
| 297 |
+
|
| 298 |
+
// 检查生成过程中是否有token失效的错误
|
| 299 |
+
for _, err := range errs {
|
| 300 |
+
if strings.Contains(err.Error(), "locked out") || strings.Contains(err.Error(), "User is locked out") {
|
| 301 |
+
log.Printf("[AutoGen] 检测到原始token被锁定,禁用token记录 %d: %v", record.ID, err)
|
| 302 |
+
// 将token记录标记为封禁状态
|
| 303 |
+
if markErr := markTokenRecordAsBanned(&record, "原始token被锁定: "+err.Error()); markErr != nil {
|
| 304 |
+
log.Printf("[AutoGen] 标记token记录封禁状态失败: %v", markErr)
|
| 305 |
+
}
|
| 306 |
+
// 根据邮箱禁用相关的token记录
|
| 307 |
+
if record.Email != "" {
|
| 308 |
+
if disableErr := disableTokenRecordsByEmail(record.Email, "关联账号被锁定"); disableErr != nil {
|
| 309 |
+
log.Printf("[AutoGen] 禁用相关token记录失败: %v", disableErr)
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
// 提前结束任务
|
| 313 |
+
task.Status = "failed"
|
| 314 |
+
task.ErrorMessage = "原始token被锁定"
|
| 315 |
+
task.CompletedAt = time.Now()
|
| 316 |
+
database.GetDB().Save(&task)
|
| 317 |
+
return
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
successCount := 0
|
| 322 |
+
failCount := len(errs)
|
| 323 |
+
|
| 324 |
+
// 处理生成的凭证
|
| 325 |
+
for _, cred := range credentials {
|
| 326 |
+
account := model.Account{
|
| 327 |
+
ClientID: cred.ClientID,
|
| 328 |
+
ClientSecret: cred.Secret,
|
| 329 |
+
IsActive: true,
|
| 330 |
+
Status: "normal",
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// 获取Token并解析信息
|
| 334 |
+
if _, err := RefreshToken(&account); err != nil {
|
| 335 |
+
failCount++
|
| 336 |
+
// 检查是否是账号锁定错误
|
| 337 |
+
if lockoutErr, ok := err.(*AccountLockoutError); ok {
|
| 338 |
+
log.Printf("[AutoGen] 账号 %s 被锁定: %s", cred.ClientID, lockoutErr.Body)
|
| 339 |
+
} else {
|
| 340 |
+
log.Printf("[AutoGen] 账号 %s 认证失败: %v", cred.ClientID, err)
|
| 341 |
+
}
|
| 342 |
+
continue
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// 解析JWT获取详细信息
|
| 346 |
+
if payload, err := ParseJWT(account.AccessToken); err == nil {
|
| 347 |
+
account.Email = payload.Email
|
| 348 |
+
account.SubscriptionStartDate = GetSubscriptionDate(payload)
|
| 349 |
+
|
| 350 |
+
if payload.Expiration > 0 {
|
| 351 |
+
account.TokenExpiry = time.Unix(payload.Expiration, 0)
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
// 设置计划类型
|
| 355 |
+
plan := "Free"
|
| 356 |
+
if payload.CustomClaims.Plan != "" {
|
| 357 |
+
plan = payload.CustomClaims.Plan
|
| 358 |
+
}
|
| 359 |
+
account.PlanType = model.PlanType(plan)
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// 保存账号
|
| 363 |
+
var existing model.Account
|
| 364 |
+
err := database.GetDB().Where("client_id = ?", account.ClientID).First(&existing).Error
|
| 365 |
+
|
| 366 |
+
if err == nil {
|
| 367 |
+
// 更新已存在的账号
|
| 368 |
+
existing.AccessToken = account.AccessToken
|
| 369 |
+
existing.TokenExpiry = account.TokenExpiry
|
| 370 |
+
existing.PlanType = account.PlanType
|
| 371 |
+
existing.Email = account.Email
|
| 372 |
+
existing.SubscriptionStartDate = account.SubscriptionStartDate
|
| 373 |
+
existing.IsActive = true
|
| 374 |
+
existing.Status = "normal"
|
| 375 |
+
existing.ClientSecret = account.ClientSecret
|
| 376 |
+
|
| 377 |
+
if err := database.GetDB().Save(&existing).Error; err != nil {
|
| 378 |
+
failCount++
|
| 379 |
+
log.Printf("[AutoGen] 更新账号 %s 失败: %v", account.ClientID, err)
|
| 380 |
+
} else {
|
| 381 |
+
successCount++
|
| 382 |
+
}
|
| 383 |
+
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 384 |
+
// 记录不存在是正常的,创建新账号(不输出错误日志)
|
| 385 |
+
if err := database.GetDB().Create(&account).Error; err != nil {
|
| 386 |
+
failCount++
|
| 387 |
+
log.Printf("[AutoGen] 创建账号 %s 失败: %v", account.ClientID, err)
|
| 388 |
+
} else {
|
| 389 |
+
successCount++
|
| 390 |
+
}
|
| 391 |
+
} else {
|
| 392 |
+
// 其他数据库错误(非record not found的真实错误)
|
| 393 |
+
failCount++
|
| 394 |
+
log.Printf("[AutoGen] 查询账号 %s 时发生数据库错误: %v", account.ClientID, err)
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
// 更新任务状态
|
| 399 |
+
task.SuccessCount = successCount
|
| 400 |
+
task.FailCount = failCount
|
| 401 |
+
task.Status = "completed"
|
| 402 |
+
if successCount == 0 && failCount > 0 {
|
| 403 |
+
task.Status = "failed"
|
| 404 |
+
task.ErrorMessage = fmt.Sprintf("所有账号生成失败")
|
| 405 |
+
}
|
| 406 |
+
task.CompletedAt = time.Now()
|
| 407 |
+
|
| 408 |
+
if err := database.GetDB().Save(&task).Error; err != nil {
|
| 409 |
+
log.Printf("[AutoGen] 更新任务记录失败: %v", err)
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// 更新token记录,累计所有统计数据
|
| 413 |
+
updates := map[string]interface{}{
|
| 414 |
+
"last_generated_at": time.Now(),
|
| 415 |
+
"generated_count": gorm.Expr("generated_count + ?", successCount),
|
| 416 |
+
"total_success": gorm.Expr("total_success + ?", successCount),
|
| 417 |
+
"total_fail": gorm.Expr("total_fail + ?", failCount),
|
| 418 |
+
"total_tasks": gorm.Expr("total_tasks + 1"),
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
if err := database.GetDB().Model(&model.TokenRecord{}).
|
| 422 |
+
Where("id = ?", record.ID).
|
| 423 |
+
Updates(updates).Error; err != nil {
|
| 424 |
+
log.Printf("[AutoGen] 更新token记录失败: %v", err)
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
// 刷新账号池
|
| 428 |
+
RefreshAccountPool()
|
| 429 |
+
|
| 430 |
+
log.Printf("[AutoGen] 自动生成完成 - 成功: %d, 失败: %d", successCount, failCount)
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
// ManualTriggerGeneration 手动触发生成
|
| 434 |
+
func ManualTriggerGeneration(tokenRecordID uint) error {
|
| 435 |
+
var record model.TokenRecord
|
| 436 |
+
if err := database.GetDB().First(&record, tokenRecordID).Error; err != nil {
|
| 437 |
+
return fmt.Errorf("token记录不存在: %v", err)
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
if !record.IsActive {
|
| 441 |
+
return fmt.Errorf("token记录未激活")
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
go autoGenService.executeGeneration(record)
|
| 445 |
+
return nil
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
// RefreshAccountPool 刷新账号池
|
| 449 |
+
func RefreshAccountPool() {
|
| 450 |
+
if pool != nil {
|
| 451 |
+
pool.refresh()
|
| 452 |
+
}
|
| 453 |
+
}
|
internal/service/credential.go
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"math/rand"
|
| 9 |
+
"net/http"
|
| 10 |
+
"time"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
const (
|
| 14 |
+
CredentialGenerateURL = "https://fe.zencoder.ai/frontegg/identity/resources/users/api-tokens/v1"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
type CredentialGenerateRequest struct {
|
| 18 |
+
Description string `json:"description"`
|
| 19 |
+
ExpiresInMinutes int `json:"expiresInMinutes"`
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
type CredentialGenerateResponse struct {
|
| 23 |
+
ClientID string `json:"clientId"`
|
| 24 |
+
Description string `json:"description"`
|
| 25 |
+
CreatedAt string `json:"createdAt"`
|
| 26 |
+
Secret string `json:"secret"`
|
| 27 |
+
Expires string `json:"expires"`
|
| 28 |
+
RefreshToken string `json:"refreshToken,omitempty"` // 添加 RefreshToken 字段
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// GenerateRandomDescription 生成随机5字符描述
|
| 32 |
+
func GenerateRandomDescription() string {
|
| 33 |
+
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
| 34 |
+
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
| 35 |
+
b := make([]byte, 5)
|
| 36 |
+
for i := range b {
|
| 37 |
+
b[i] = charset[rng.Intn(len(charset))]
|
| 38 |
+
}
|
| 39 |
+
return string(b)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// GenerateCredential 使用 token 生成一个新凭证
|
| 43 |
+
func GenerateCredential(token string) (*CredentialGenerateResponse, error) {
|
| 44 |
+
reqBody := CredentialGenerateRequest{
|
| 45 |
+
Description: GenerateRandomDescription(),
|
| 46 |
+
ExpiresInMinutes: 525600, // 1 year
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
bodyBytes, err := json.Marshal(reqBody)
|
| 50 |
+
if err != nil {
|
| 51 |
+
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
req, err := http.NewRequest("POST", CredentialGenerateURL, bytes.NewReader(bodyBytes))
|
| 55 |
+
if err != nil {
|
| 56 |
+
return nil, fmt.Errorf("failed to create request: %w", err)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// 设置请求头
|
| 60 |
+
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
| 61 |
+
req.Header.Set("Connection", "keep-alive")
|
| 62 |
+
req.Header.Set("accept", "*/*")
|
| 63 |
+
req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,ja;q=0.6")
|
| 64 |
+
req.Header.Set("authorization", "Bearer "+token)
|
| 65 |
+
req.Header.Set("cache-control", "no-cache")
|
| 66 |
+
req.Header.Set("content-type", "application/json")
|
| 67 |
+
req.Header.Set("frontegg-source", "admin-portal")
|
| 68 |
+
req.Header.Set("origin", "https://auth.zencoder.ai")
|
| 69 |
+
req.Header.Set("pragma", "no-cache")
|
| 70 |
+
req.Header.Set("priority", "u=1, i")
|
| 71 |
+
req.Header.Set("referer", "https://auth.zencoder.ai/")
|
| 72 |
+
req.Header.Set("sec-ch-ua", `"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"`)
|
| 73 |
+
req.Header.Set("sec-ch-ua-mobile", "?0")
|
| 74 |
+
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
|
| 75 |
+
req.Header.Set("sec-fetch-dest", "empty")
|
| 76 |
+
req.Header.Set("sec-fetch-mode", "cors")
|
| 77 |
+
req.Header.Set("sec-fetch-site", "same-site")
|
| 78 |
+
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36")
|
| 79 |
+
req.Header.Set("x-frontegg-framework", "next@15.3.8")
|
| 80 |
+
req.Header.Set("x-frontegg-sdk", "@frontegg/nextjs@9.2.10")
|
| 81 |
+
|
| 82 |
+
client := &http.Client{Timeout: 30 * time.Second}
|
| 83 |
+
resp, err := client.Do(req)
|
| 84 |
+
if err != nil {
|
| 85 |
+
return nil, fmt.Errorf("failed to send request: %w", err)
|
| 86 |
+
}
|
| 87 |
+
defer resp.Body.Close()
|
| 88 |
+
|
| 89 |
+
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
| 90 |
+
body, _ := io.ReadAll(resp.Body)
|
| 91 |
+
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
var result CredentialGenerateResponse
|
| 95 |
+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
| 96 |
+
return nil, fmt.Errorf("failed to decode response: %w", err)
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
return &result, nil
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// BatchGenerateCredentials 批量生成凭证
|
| 103 |
+
func BatchGenerateCredentials(token string, count int) ([]*CredentialGenerateResponse, []error) {
|
| 104 |
+
var results []*CredentialGenerateResponse
|
| 105 |
+
var errors []error
|
| 106 |
+
|
| 107 |
+
for i := 0; i < count; i++ {
|
| 108 |
+
cred, err := GenerateCredential(token)
|
| 109 |
+
if err != nil {
|
| 110 |
+
errors = append(errors, fmt.Errorf("credential %d: %w", i+1, err))
|
| 111 |
+
continue
|
| 112 |
+
}
|
| 113 |
+
results = append(results, cred)
|
| 114 |
+
|
| 115 |
+
// 添加短暂延迟避免请求过快
|
| 116 |
+
if i < count-1 {
|
| 117 |
+
time.Sleep(500 * time.Millisecond)
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
return results, errors
|
| 122 |
+
}
|
internal/service/debug.go
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"log"
|
| 7 |
+
"os"
|
| 8 |
+
"sync"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
var (
|
| 12 |
+
debugMode bool
|
| 13 |
+
debugModeOnce sync.Once
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// IsDebugMode 检查是否启用调试模式
|
| 17 |
+
func IsDebugMode() bool {
|
| 18 |
+
debugModeOnce.Do(func() {
|
| 19 |
+
debugMode = os.Getenv("DEBUG") == "true" || os.Getenv("DEBUG") == "1"
|
| 20 |
+
})
|
| 21 |
+
return debugMode
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// RequestLogger 用于收集请求级日志
|
| 25 |
+
type RequestLogger struct {
|
| 26 |
+
logs []string
|
| 27 |
+
mu sync.Mutex
|
| 28 |
+
hasError bool
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// NewRequestLogger 创建新的请求日志记录器
|
| 32 |
+
func NewRequestLogger() *RequestLogger {
|
| 33 |
+
return &RequestLogger{
|
| 34 |
+
logs: make([]string, 0, 20),
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Log 记录一条日志
|
| 39 |
+
func (l *RequestLogger) Log(format string, args ...interface{}) {
|
| 40 |
+
msg := fmt.Sprintf(format, args...)
|
| 41 |
+
|
| 42 |
+
// 如果全局 DEBUG 开启,直接打印
|
| 43 |
+
if IsDebugMode() {
|
| 44 |
+
log.Print("[DEBUG] " + msg)
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// 否则缓冲
|
| 49 |
+
l.mu.Lock()
|
| 50 |
+
l.logs = append(l.logs, "[DEBUG] " + msg)
|
| 51 |
+
l.mu.Unlock()
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// MarkError 标记发生错误
|
| 55 |
+
func (l *RequestLogger) MarkError() {
|
| 56 |
+
l.mu.Lock()
|
| 57 |
+
l.hasError = true
|
| 58 |
+
l.mu.Unlock()
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Flush 输出缓冲的日志(如果有错误)
|
| 62 |
+
func (l *RequestLogger) Flush() {
|
| 63 |
+
// 只有在非 Debug 模式且发生错误时才需要 Flush (Debug 模式下已经实时打印了)
|
| 64 |
+
if !IsDebugMode() && l.hasError {
|
| 65 |
+
l.mu.Lock()
|
| 66 |
+
defer l.mu.Unlock()
|
| 67 |
+
for _, msg := range l.logs {
|
| 68 |
+
log.Print(msg)
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
type contextKey string
|
| 74 |
+
|
| 75 |
+
const loggerContextKey contextKey = "request_logger"
|
| 76 |
+
|
| 77 |
+
// WithLogger 将 logger 注入 context
|
| 78 |
+
func WithLogger(ctx context.Context, logger *RequestLogger) context.Context {
|
| 79 |
+
return context.WithValue(ctx, loggerContextKey, logger)
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// GetLogger 从 context 获取 logger
|
| 83 |
+
func GetLogger(ctx context.Context) *RequestLogger {
|
| 84 |
+
val := ctx.Value(loggerContextKey)
|
| 85 |
+
if val != nil {
|
| 86 |
+
if logger, ok := val.(*RequestLogger); ok {
|
| 87 |
+
return logger
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
return nil
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// 辅助函数:获取 logger 并记录
|
| 94 |
+
func logToContext(ctx context.Context, format string, args ...interface{}) {
|
| 95 |
+
logger := GetLogger(ctx)
|
| 96 |
+
if logger != nil {
|
| 97 |
+
logger.Log(format, args...)
|
| 98 |
+
} else if IsDebugMode() {
|
| 99 |
+
log.Printf("[DEBUG] "+format, args...)
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// DebugLog 调试日志输出
|
| 104 |
+
func DebugLog(ctx context.Context, format string, args ...interface{}) {
|
| 105 |
+
logToContext(ctx, format, args...)
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// DebugLogRequest 请求开始日志
|
| 109 |
+
func DebugLogRequest(ctx context.Context, provider, endpoint, model string) {
|
| 110 |
+
logToContext(ctx, "[%s] >>> 请求开始: endpoint=%s, model=%s", provider, endpoint, model)
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// DebugLogRetry 重试日志
|
| 114 |
+
func DebugLogRetry(ctx context.Context, provider string, attempt int, accountID uint, err error) {
|
| 115 |
+
if logger := GetLogger(ctx); logger != nil {
|
| 116 |
+
logger.MarkError()
|
| 117 |
+
}
|
| 118 |
+
logToContext(ctx, "[%s] ↻ 重试 #%d: accountID=%d, error=%v", provider, attempt, accountID, err)
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// DebugLogAccountSelected 账号选择日志
|
| 122 |
+
func DebugLogAccountSelected(ctx context.Context, provider string, accountID uint, email string) {
|
| 123 |
+
logToContext(ctx, "[%s] ✓ 选择账号: id=%d, email=%s", provider, accountID, email)
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// DebugLogRequestSent 请求发送日志
|
| 127 |
+
func DebugLogRequestSent(ctx context.Context, provider, url string) {
|
| 128 |
+
logToContext(ctx, "[%s] → 发送请求: %s", provider, url)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// DebugLogResponseReceived 响应接收日志
|
| 132 |
+
func DebugLogResponseReceived(ctx context.Context, provider string, statusCode int) {
|
| 133 |
+
logToContext(ctx, "[%s] ← 收到响应: status=%d", provider, statusCode)
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// DebugLogRequestEnd 请求结束日志
|
| 137 |
+
func DebugLogRequestEnd(ctx context.Context, provider string, success bool, err error) {
|
| 138 |
+
if !success || err != nil {
|
| 139 |
+
if logger := GetLogger(ctx); logger != nil {
|
| 140 |
+
logger.MarkError()
|
| 141 |
+
}
|
| 142 |
+
logToContext(ctx, "[%s] <<< 请求完成: success=false, error=%v", provider, err)
|
| 143 |
+
} else {
|
| 144 |
+
logToContext(ctx, "[%s] <<< 请求完成: success=true", provider)
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// DebugLogRequestHeaders 请求头日志
|
| 149 |
+
func DebugLogRequestHeaders(ctx context.Context, provider string, headers map[string][]string) {
|
| 150 |
+
logToContext(ctx, "[%s] 请求头:", provider)
|
| 151 |
+
for k, v := range headers {
|
| 152 |
+
// 隐藏敏感信息
|
| 153 |
+
if k == "Authorization" || k == "x-api-key" {
|
| 154 |
+
logToContext(ctx, "[%s] %s: ***", provider, k)
|
| 155 |
+
} else {
|
| 156 |
+
logToContext(ctx, "[%s] %s: %v", provider, k, v)
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// DebugLogResponseHeaders 响应头日志
|
| 162 |
+
func DebugLogResponseHeaders(ctx context.Context, provider string, headers map[string][]string) {
|
| 163 |
+
logToContext(ctx, "[%s] 响应头:", provider)
|
| 164 |
+
for k, v := range headers {
|
| 165 |
+
// 隐藏敏感信息
|
| 166 |
+
if k == "X-Api-Key" || k == "Authorization" {
|
| 167 |
+
logToContext(ctx, "[%s] %s: ***", provider, k)
|
| 168 |
+
} else {
|
| 169 |
+
logToContext(ctx, "[%s] %s: %v", provider, k, v)
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// DebugLogActualModel 实际调用模型日志
|
| 175 |
+
func DebugLogActualModel(ctx context.Context, provider, requestModel, actualModel string) {
|
| 176 |
+
logToContext(ctx, "[%s] 模型映射: %s → %s", provider, requestModel, actualModel)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// DebugLogErrorResponse 错误响应内容日志
|
| 180 |
+
func DebugLogErrorResponse(ctx context.Context, provider string, statusCode int, body string) {
|
| 181 |
+
if logger := GetLogger(ctx); logger != nil {
|
| 182 |
+
logger.MarkError()
|
| 183 |
+
}
|
| 184 |
+
logToContext(ctx, "[%s] ✗ 错误响应 [%d]: %s", provider, statusCode, body)
|
| 185 |
+
}
|
internal/service/errors.go
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import "errors"
|
| 4 |
+
|
| 5 |
+
var (
|
| 6 |
+
ErrNoAvailableAccount = errors.New("没有可用token")
|
| 7 |
+
ErrNoPermission = errors.New("没有账号有权限使用此模型")
|
| 8 |
+
ErrTokenExpired = errors.New("token已过期")
|
| 9 |
+
ErrRequestFailed = errors.New("请求失败")
|
| 10 |
+
)
|
internal/service/gemini.go
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"log"
|
| 9 |
+
"net/http"
|
| 10 |
+
|
| 11 |
+
"zencoder-2api/internal/model"
|
| 12 |
+
"zencoder-2api/internal/service/provider"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
const GeminiBaseURL = "https://api.zencoder.ai/gemini"
|
| 16 |
+
|
| 17 |
+
type GeminiService struct{}
|
| 18 |
+
|
| 19 |
+
func NewGeminiService() *GeminiService {
|
| 20 |
+
return &GeminiService{}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// GenerateContent 处理generateContent请求
|
| 24 |
+
func (s *GeminiService) GenerateContent(ctx context.Context, modelName string, body []byte) (*http.Response, error) {
|
| 25 |
+
// 检查模型是否存在于模型字典中
|
| 26 |
+
_, exists := model.GetZenModel(modelName)
|
| 27 |
+
if !exists {
|
| 28 |
+
DebugLog(ctx, "[Gemini] 模型不存在: %s", modelName)
|
| 29 |
+
return nil, ErrNoAvailableAccount
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
DebugLogRequest(ctx, "Gemini", "generateContent", modelName)
|
| 33 |
+
|
| 34 |
+
var lastErr error
|
| 35 |
+
for i := 0; i < MaxRetries; i++ {
|
| 36 |
+
account, err := GetNextAccountForModel(modelName)
|
| 37 |
+
if err != nil {
|
| 38 |
+
DebugLogRequestEnd(ctx, "Gemini", false, err)
|
| 39 |
+
return nil, err
|
| 40 |
+
}
|
| 41 |
+
DebugLogAccountSelected(ctx, "Gemini", account.ID, account.Email)
|
| 42 |
+
|
| 43 |
+
resp, err := s.doRequest(ctx, account, modelName, body, false)
|
| 44 |
+
if err != nil {
|
| 45 |
+
MarkAccountError(account)
|
| 46 |
+
lastErr = err
|
| 47 |
+
DebugLogRetry(ctx, "Gemini", i+1, account.ID, err)
|
| 48 |
+
continue
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
DebugLogResponseReceived(ctx, "Gemini", resp.StatusCode)
|
| 52 |
+
DebugLogResponseHeaders(ctx, "Gemini", resp.Header)
|
| 53 |
+
|
| 54 |
+
// 总是输出重要的响应头信息
|
| 55 |
+
if resp.Header.Get("Zen-Pricing-Period-Limit") != "" ||
|
| 56 |
+
resp.Header.Get("Zen-Pricing-Period-Cost") != "" ||
|
| 57 |
+
resp.Header.Get("Zen-Request-Cost") != "" {
|
| 58 |
+
log.Printf("[Gemini] 积分信息 - 周期限额: %s, 周期消耗: %s, 本次消耗: %s",
|
| 59 |
+
resp.Header.Get("Zen-Pricing-Period-Limit"),
|
| 60 |
+
resp.Header.Get("Zen-Pricing-Period-Cost"),
|
| 61 |
+
resp.Header.Get("Zen-Request-Cost"))
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
if resp.StatusCode >= 400 {
|
| 65 |
+
// 读取错误响应内容用于日志
|
| 66 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 67 |
+
resp.Body.Close()
|
| 68 |
+
DebugLogErrorResponse(ctx, "Gemini", resp.StatusCode, string(errBody))
|
| 69 |
+
|
| 70 |
+
// 400和500错误直接返回,不进行账号错误计数
|
| 71 |
+
if resp.StatusCode == 400 || resp.StatusCode == 500 {
|
| 72 |
+
DebugLogRequestEnd(ctx, "Gemini", false, fmt.Errorf("API error: %d", resp.StatusCode))
|
| 73 |
+
return nil, fmt.Errorf("API error: %d - %s", resp.StatusCode, string(errBody))
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// 429 错误特殊处理
|
| 77 |
+
if resp.StatusCode == 429 {
|
| 78 |
+
log.Printf("[Gemini] 429限流错误,尝试使用代理重试")
|
| 79 |
+
|
| 80 |
+
// 尝试使用代理池重试
|
| 81 |
+
proxyResp, proxyErr := s.retryWithProxy(ctx, account, modelName, body, false)
|
| 82 |
+
if proxyErr == nil && proxyResp != nil {
|
| 83 |
+
// 代理重试成功
|
| 84 |
+
return proxyResp, nil
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
log.Printf("[Gemini] 代理重试失败: %v", proxyErr)
|
| 88 |
+
MarkAccountRateLimitedWithResponse(account, resp)
|
| 89 |
+
} else {
|
| 90 |
+
MarkAccountError(account)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
lastErr = fmt.Errorf("API error: %d", resp.StatusCode)
|
| 94 |
+
DebugLogRetry(ctx, "Gemini", i+1, account.ID, lastErr)
|
| 95 |
+
continue
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
ResetAccountError(account)
|
| 99 |
+
zenModel, exists := model.GetZenModel(modelName)
|
| 100 |
+
if !exists {
|
| 101 |
+
// 模型不存在,使用默认倍率
|
| 102 |
+
UpdateAccountCreditsFromResponse(account, resp, 1.0)
|
| 103 |
+
} else {
|
| 104 |
+
// 使用统一的积分更新函数,自动处理响应头中的积分信息
|
| 105 |
+
UpdateAccountCreditsFromResponse(account, resp, zenModel.Multiplier)
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
DebugLogRequestEnd(ctx, "Gemini", true, nil)
|
| 109 |
+
return resp, nil
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
DebugLogRequestEnd(ctx, "Gemini", false, lastErr)
|
| 113 |
+
return nil, fmt.Errorf("all retries failed: %w", lastErr)
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// StreamGenerateContent 处理streamGenerateContent请求
|
| 117 |
+
func (s *GeminiService) StreamGenerateContent(ctx context.Context, modelName string, body []byte) (*http.Response, error) {
|
| 118 |
+
// 检查模型是否存在于模型字典中
|
| 119 |
+
_, exists := model.GetZenModel(modelName)
|
| 120 |
+
if !exists {
|
| 121 |
+
DebugLog(ctx, "[Gemini] 模型不存在: %s", modelName)
|
| 122 |
+
return nil, ErrNoAvailableAccount
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
DebugLogRequest(ctx, "Gemini", "streamGenerateContent", modelName)
|
| 126 |
+
|
| 127 |
+
var lastErr error
|
| 128 |
+
for i := 0; i < MaxRetries; i++ {
|
| 129 |
+
account, err := GetNextAccountForModel(modelName)
|
| 130 |
+
if err != nil {
|
| 131 |
+
DebugLogRequestEnd(ctx, "Gemini", false, err)
|
| 132 |
+
return nil, err
|
| 133 |
+
}
|
| 134 |
+
DebugLogAccountSelected(ctx, "Gemini", account.ID, account.Email)
|
| 135 |
+
|
| 136 |
+
resp, err := s.doRequest(ctx, account, modelName, body, true)
|
| 137 |
+
if err != nil {
|
| 138 |
+
MarkAccountError(account)
|
| 139 |
+
lastErr = err
|
| 140 |
+
DebugLogRetry(ctx, "Gemini", i+1, account.ID, err)
|
| 141 |
+
continue
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
DebugLogResponseReceived(ctx, "Gemini", resp.StatusCode)
|
| 145 |
+
DebugLogResponseHeaders(ctx, "Gemini", resp.Header)
|
| 146 |
+
|
| 147 |
+
// 总是输出重要的响应头信息
|
| 148 |
+
if resp.Header.Get("Zen-Pricing-Period-Limit") != "" ||
|
| 149 |
+
resp.Header.Get("Zen-Pricing-Period-Cost") != "" ||
|
| 150 |
+
resp.Header.Get("Zen-Request-Cost") != "" {
|
| 151 |
+
log.Printf("[Gemini] 积分信息 - 周期限额: %s, 周期消耗: %s, 本次消耗: %s",
|
| 152 |
+
resp.Header.Get("Zen-Pricing-Period-Limit"),
|
| 153 |
+
resp.Header.Get("Zen-Pricing-Period-Cost"),
|
| 154 |
+
resp.Header.Get("Zen-Request-Cost"))
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
if resp.StatusCode >= 400 {
|
| 158 |
+
// 读取错误响应内容用于日志
|
| 159 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 160 |
+
resp.Body.Close()
|
| 161 |
+
DebugLogErrorResponse(ctx, "Gemini", resp.StatusCode, string(errBody))
|
| 162 |
+
|
| 163 |
+
// 400和500错误直接返回,不进行账号错误计数
|
| 164 |
+
if resp.StatusCode == 400 || resp.StatusCode == 500 {
|
| 165 |
+
DebugLogRequestEnd(ctx, "Gemini", false, fmt.Errorf("API error: %d", resp.StatusCode))
|
| 166 |
+
return nil, fmt.Errorf("API error: %d - %s", resp.StatusCode, string(errBody))
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// 429 错误特殊处理
|
| 170 |
+
if resp.StatusCode == 429 {
|
| 171 |
+
log.Printf("[Gemini] 429限流错误,尝试使用代理重试")
|
| 172 |
+
|
| 173 |
+
// 尝试使用代理池重试
|
| 174 |
+
proxyResp, proxyErr := s.retryWithProxy(ctx, account, modelName, body, true)
|
| 175 |
+
if proxyErr == nil && proxyResp != nil {
|
| 176 |
+
// 代理重试成功
|
| 177 |
+
return proxyResp, nil
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
log.Printf("[Gemini] 代理重试失败: %v", proxyErr)
|
| 181 |
+
MarkAccountRateLimitedWithResponse(account, resp)
|
| 182 |
+
} else {
|
| 183 |
+
MarkAccountError(account)
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
lastErr = fmt.Errorf("API error: %d", resp.StatusCode)
|
| 187 |
+
DebugLogRetry(ctx, "Gemini", i+1, account.ID, lastErr)
|
| 188 |
+
continue
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
ResetAccountError(account)
|
| 192 |
+
zenModel, exists := model.GetZenModel(modelName)
|
| 193 |
+
if !exists {
|
| 194 |
+
// 模型不存在,使用默认倍率
|
| 195 |
+
UseCredit(account, 1.0)
|
| 196 |
+
} else {
|
| 197 |
+
// 流式响应,暂时使用模型倍率(因为没有完整响应头)
|
| 198 |
+
UseCredit(account, zenModel.Multiplier)
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
DebugLogRequestEnd(ctx, "Gemini", true, nil)
|
| 202 |
+
return resp, nil
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
DebugLogRequestEnd(ctx, "Gemini", false, lastErr)
|
| 206 |
+
return nil, fmt.Errorf("all retries failed: %w", lastErr)
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
func (s *GeminiService) doRequest(ctx context.Context, account *model.Account, modelName string, body []byte, stream bool) (*http.Response, error) {
|
| 210 |
+
zenModel, exists := model.GetZenModel(modelName)
|
| 211 |
+
if !exists {
|
| 212 |
+
return nil, ErrNoAvailableAccount
|
| 213 |
+
}
|
| 214 |
+
httpClient := provider.NewHTTPClient(account.Proxy, 0)
|
| 215 |
+
|
| 216 |
+
action := "generateContent"
|
| 217 |
+
queryParam := ""
|
| 218 |
+
if stream {
|
| 219 |
+
action = "streamGenerateContent"
|
| 220 |
+
queryParam = "?alt=sse"
|
| 221 |
+
}
|
| 222 |
+
reqURL := fmt.Sprintf("%s/v1beta/models/%s:%s%s", GeminiBaseURL, modelName, action, queryParam)
|
| 223 |
+
DebugLogRequestSent(ctx, "Gemini", reqURL)
|
| 224 |
+
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewReader(body))
|
| 225 |
+
if err != nil {
|
| 226 |
+
return nil, err
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// 设置Zencoder自定义请求头
|
| 230 |
+
SetZencoderHeaders(httpReq, account, zenModel)
|
| 231 |
+
|
| 232 |
+
// 流式请求禁用压缩,确保可以逐行读取
|
| 233 |
+
if stream {
|
| 234 |
+
httpReq.Header.Set("Accept-Encoding", "identity")
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// 添加模型配置的额外请求头
|
| 238 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 239 |
+
for k, v := range zenModel.Parameters.ExtraHeaders {
|
| 240 |
+
httpReq.Header.Set(k, v)
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// 记录请求头用于调试
|
| 245 |
+
DebugLogRequestHeaders(ctx, "Gemini", httpReq.Header)
|
| 246 |
+
|
| 247 |
+
return httpClient.Do(httpReq)
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// GenerateContentProxy 代理generateContent请求
|
| 251 |
+
func (s *GeminiService) GenerateContentProxy(ctx context.Context, w http.ResponseWriter, modelName string, body []byte) error {
|
| 252 |
+
resp, err := s.GenerateContent(ctx, modelName, body)
|
| 253 |
+
if err != nil {
|
| 254 |
+
return err
|
| 255 |
+
}
|
| 256 |
+
defer resp.Body.Close()
|
| 257 |
+
|
| 258 |
+
return StreamResponse(w, resp)
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// retryWithProxy 使用代理池重试Gemini请求
|
| 262 |
+
func (s *GeminiService) retryWithProxy(ctx context.Context, account *model.Account, modelName string, body []byte, stream bool) (*http.Response, error) {
|
| 263 |
+
// 获取模型配置
|
| 264 |
+
zenModel, exists := model.GetZenModel(modelName)
|
| 265 |
+
if !exists {
|
| 266 |
+
return nil, fmt.Errorf("模型配置不存在: %s", modelName)
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
proxyPool := provider.GetProxyPool()
|
| 270 |
+
if !proxyPool.HasProxies() {
|
| 271 |
+
return nil, fmt.Errorf("没有可用的代理")
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
maxRetries := 3
|
| 275 |
+
for i := 0; i < maxRetries; i++ {
|
| 276 |
+
// 获取随机代理
|
| 277 |
+
proxyURL := proxyPool.GetRandomProxy()
|
| 278 |
+
if proxyURL == "" {
|
| 279 |
+
continue
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
log.Printf("[Gemini] 尝试代理 %s (重试 %d/%d)", proxyURL, i+1, maxRetries)
|
| 283 |
+
|
| 284 |
+
// 创建使用代理的HTTP客户端
|
| 285 |
+
proxyClient, err := provider.NewHTTPClientWithProxy(proxyURL, 0)
|
| 286 |
+
if err != nil {
|
| 287 |
+
log.Printf("[Gemini] 创建代理客户端失败: %v", err)
|
| 288 |
+
continue
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
// 创建新请求
|
| 292 |
+
action := "generateContent"
|
| 293 |
+
queryParam := ""
|
| 294 |
+
if stream {
|
| 295 |
+
action = "streamGenerateContent"
|
| 296 |
+
queryParam = "?alt=sse"
|
| 297 |
+
}
|
| 298 |
+
reqURL := fmt.Sprintf("%s/v1beta/models/%s:%s%s", GeminiBaseURL, modelName, action, queryParam)
|
| 299 |
+
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewReader(body))
|
| 300 |
+
if err != nil {
|
| 301 |
+
log.Printf("[Gemini] 创建请求失败: %v", err)
|
| 302 |
+
continue
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
// 设置请求头
|
| 306 |
+
SetZencoderHeaders(httpReq, account, zenModel)
|
| 307 |
+
|
| 308 |
+
// 流式请求禁用压缩,确保��以逐行读取
|
| 309 |
+
if stream {
|
| 310 |
+
httpReq.Header.Set("Accept-Encoding", "identity")
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
// 添加模型配置的额外请求头
|
| 314 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 315 |
+
for k, v := range zenModel.Parameters.ExtraHeaders {
|
| 316 |
+
httpReq.Header.Set(k, v)
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
// 执行请求
|
| 321 |
+
resp, err := proxyClient.Do(httpReq)
|
| 322 |
+
if err != nil {
|
| 323 |
+
log.Printf("[Gemini] 代理请求失败: %v", err)
|
| 324 |
+
continue
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
// 检查响应状态
|
| 328 |
+
if resp.StatusCode == 429 {
|
| 329 |
+
// 仍然是429,尝试下一个代理
|
| 330 |
+
resp.Body.Close()
|
| 331 |
+
log.Printf("[Gemini] 代理 %s 仍返回429,尝试下一个", proxyURL)
|
| 332 |
+
continue
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
if resp.StatusCode >= 400 {
|
| 336 |
+
// 其他错误,记录并尝试下一个代理
|
| 337 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 338 |
+
resp.Body.Close()
|
| 339 |
+
log.Printf("[Gemini] 代理 %s 返回错误 %d: %s", proxyURL, resp.StatusCode, string(errBody))
|
| 340 |
+
continue
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// 成功
|
| 344 |
+
log.Printf("[Gemini] 代理 %s 请求成功", proxyURL)
|
| 345 |
+
return resp, nil
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
return nil, fmt.Errorf("所有代理重试均失败")
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
// StreamGenerateContentProxy 代理streamGenerateContent请求
|
| 352 |
+
func (s *GeminiService) StreamGenerateContentProxy(ctx context.Context, w http.ResponseWriter, modelName string, body []byte) error {
|
| 353 |
+
resp, err := s.StreamGenerateContent(ctx, modelName, body)
|
| 354 |
+
if err != nil {
|
| 355 |
+
return err
|
| 356 |
+
}
|
| 357 |
+
defer resp.Body.Close()
|
| 358 |
+
|
| 359 |
+
return StreamResponse(w, resp)
|
| 360 |
+
}
|
internal/service/grok.go
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"log"
|
| 10 |
+
"net/http"
|
| 11 |
+
"strings"
|
| 12 |
+
|
| 13 |
+
"zencoder-2api/internal/model"
|
| 14 |
+
"zencoder-2api/internal/service/provider"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
const GrokBaseURL = "https://api.zencoder.ai/xai"
|
| 18 |
+
|
| 19 |
+
type GrokService struct{}
|
| 20 |
+
|
| 21 |
+
func NewGrokService() *GrokService {
|
| 22 |
+
return &GrokService{}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// ChatCompletions 处理/v1/chat/completions请求
|
| 26 |
+
func (s *GrokService) ChatCompletions(ctx context.Context, body []byte) (*http.Response, error) {
|
| 27 |
+
var req struct {
|
| 28 |
+
Model string `json:"model"`
|
| 29 |
+
}
|
| 30 |
+
if err := json.Unmarshal(body, &req); err != nil {
|
| 31 |
+
return nil, fmt.Errorf("invalid request body: %w", err)
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// 检查模型是否存在于模型字典中
|
| 35 |
+
_, exists := model.GetZenModel(req.Model)
|
| 36 |
+
if !exists {
|
| 37 |
+
DebugLog(ctx, "[Grok] 模型不存在: %s", req.Model)
|
| 38 |
+
return nil, ErrNoAvailableAccount
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
DebugLogRequest(ctx, "Grok", "/v1/chat/completions", req.Model)
|
| 42 |
+
|
| 43 |
+
var lastErr error
|
| 44 |
+
for i := 0; i < MaxRetries; i++ {
|
| 45 |
+
account, err := GetNextAccountForModel(req.Model)
|
| 46 |
+
if err != nil {
|
| 47 |
+
DebugLogRequestEnd(ctx, "Grok", false, err)
|
| 48 |
+
return nil, err
|
| 49 |
+
}
|
| 50 |
+
DebugLogAccountSelected(ctx, "Grok", account.ID, account.Email)
|
| 51 |
+
|
| 52 |
+
resp, err := s.doRequest(ctx, account, req.Model, body)
|
| 53 |
+
if err != nil {
|
| 54 |
+
MarkAccountError(account)
|
| 55 |
+
lastErr = err
|
| 56 |
+
DebugLogRetry(ctx, "Grok", i+1, account.ID, err)
|
| 57 |
+
continue
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
DebugLogResponseReceived(ctx, "Grok", resp.StatusCode)
|
| 61 |
+
DebugLogResponseHeaders(ctx, "Grok", resp.Header)
|
| 62 |
+
|
| 63 |
+
// 总是输出重要的响应头信息
|
| 64 |
+
if resp.Header.Get("Zen-Pricing-Period-Limit") != "" ||
|
| 65 |
+
resp.Header.Get("Zen-Pricing-Period-Cost") != "" ||
|
| 66 |
+
resp.Header.Get("Zen-Request-Cost") != "" {
|
| 67 |
+
log.Printf("[Grok] 积分信息 - 周期限额: %s, 周期消耗: %s, 本次消耗: %s",
|
| 68 |
+
resp.Header.Get("Zen-Pricing-Period-Limit"),
|
| 69 |
+
resp.Header.Get("Zen-Pricing-Period-Cost"),
|
| 70 |
+
resp.Header.Get("Zen-Request-Cost"))
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if resp.StatusCode >= 400 {
|
| 74 |
+
// 读取错误响应内容
|
| 75 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 76 |
+
resp.Body.Close()
|
| 77 |
+
|
| 78 |
+
// 429 错误特殊处理 - 直接返回,不重试
|
| 79 |
+
if resp.StatusCode == 429 {
|
| 80 |
+
log.Printf("[Grok] 429限流错误,尝试使用代理重试")
|
| 81 |
+
|
| 82 |
+
// 尝试使用代理池重试
|
| 83 |
+
proxyResp, proxyErr := s.retryWithProxy(ctx, account, req.Model, body)
|
| 84 |
+
if proxyErr == nil && proxyResp != nil {
|
| 85 |
+
// 代理重试成功
|
| 86 |
+
return proxyResp, nil
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
log.Printf("[Grok] 代理重试失败: %v", proxyErr)
|
| 90 |
+
// 在DEBUG模式下记录详细信息
|
| 91 |
+
DebugLogErrorResponse(ctx, "Grok", resp.StatusCode, string(errBody))
|
| 92 |
+
// 将账号放入短期冷却(5秒)
|
| 93 |
+
MarkAccountRateLimitedShort(account)
|
| 94 |
+
// 标记错误并结束请求
|
| 95 |
+
DebugLogRequestEnd(ctx, "Grok", false, ErrNoAvailableAccount)
|
| 96 |
+
// 返回通用错误
|
| 97 |
+
return nil, ErrNoAvailableAccount
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
DebugLogErrorResponse(ctx, "Grok", resp.StatusCode, string(errBody))
|
| 101 |
+
|
| 102 |
+
// 400和500错误直接返回,不进行账号错误计数
|
| 103 |
+
if resp.StatusCode == 400 || resp.StatusCode == 500 {
|
| 104 |
+
DebugLogRequestEnd(ctx, "Grok", false, fmt.Errorf("API error: %d", resp.StatusCode))
|
| 105 |
+
return nil, fmt.Errorf("API error: %d - %s", resp.StatusCode, string(errBody))
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
MarkAccountError(account)
|
| 109 |
+
lastErr = fmt.Errorf("API error: %d", resp.StatusCode)
|
| 110 |
+
DebugLogRetry(ctx, "Grok", i+1, account.ID, lastErr)
|
| 111 |
+
continue
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
ResetAccountError(account)
|
| 115 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 116 |
+
if !exists {
|
| 117 |
+
// 模型不存在,使用默认倍率
|
| 118 |
+
UpdateAccountCreditsFromResponse(account, resp, 1.0)
|
| 119 |
+
} else {
|
| 120 |
+
// 使用统一的积分更新函数,自动处理响应头中的积分信息
|
| 121 |
+
UpdateAccountCreditsFromResponse(account, resp, zenModel.Multiplier)
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
DebugLogRequestEnd(ctx, "Grok", true, nil)
|
| 125 |
+
return resp, nil
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
DebugLogRequestEnd(ctx, "Grok", false, lastErr)
|
| 129 |
+
return nil, fmt.Errorf("all retries failed: %w", lastErr)
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
func (s *GrokService) doRequest(ctx context.Context, account *model.Account, modelID string, body []byte) (*http.Response, error) {
|
| 133 |
+
zenModel, exists := model.GetZenModel(modelID)
|
| 134 |
+
if !exists {
|
| 135 |
+
return nil, ErrNoAvailableAccount
|
| 136 |
+
}
|
| 137 |
+
httpClient := provider.NewHTTPClient(account.Proxy, 0)
|
| 138 |
+
|
| 139 |
+
// 处理请求体,Grok Code 模型要求 temperature=0
|
| 140 |
+
modifiedBody := body
|
| 141 |
+
if strings.Contains(modelID, "grok-code") {
|
| 142 |
+
modifiedBody, _ = s.setTemperatureZero(body)
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
reqURL := GrokBaseURL + "/v1/chat/completions"
|
| 146 |
+
DebugLogRequestSent(ctx, "Grok", reqURL)
|
| 147 |
+
|
| 148 |
+
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewReader(modifiedBody))
|
| 149 |
+
if err != nil {
|
| 150 |
+
return nil, err
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// 设置Zencoder自定义请求头
|
| 154 |
+
SetZencoderHeaders(httpReq, account, zenModel)
|
| 155 |
+
|
| 156 |
+
// 添加模型配置的额外请求头
|
| 157 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 158 |
+
for k, v := range zenModel.Parameters.ExtraHeaders {
|
| 159 |
+
httpReq.Header.Set(k, v)
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// 记录请求头用于调试
|
| 164 |
+
DebugLogRequestHeaders(ctx, "Grok", httpReq.Header)
|
| 165 |
+
|
| 166 |
+
return httpClient.Do(httpReq)
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// setTemperatureZero 设置 temperature=0
|
| 170 |
+
func (s *GrokService) setTemperatureZero(body []byte) ([]byte, error) {
|
| 171 |
+
var reqMap map[string]interface{}
|
| 172 |
+
if err := json.Unmarshal(body, &reqMap); err != nil {
|
| 173 |
+
return body, err
|
| 174 |
+
}
|
| 175 |
+
reqMap["temperature"] = 0
|
| 176 |
+
return json.Marshal(reqMap)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// ChatCompletionsProxy 代理chat completions请求
|
| 180 |
+
func (s *GrokService) ChatCompletionsProxy(ctx context.Context, w http.ResponseWriter, body []byte) error {
|
| 181 |
+
resp, err := s.ChatCompletions(ctx, body)
|
| 182 |
+
if err != nil {
|
| 183 |
+
return err
|
| 184 |
+
}
|
| 185 |
+
defer resp.Body.Close()
|
| 186 |
+
|
| 187 |
+
return StreamResponse(w, resp)
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// retryWithProxy 使用代理池重试Grok请求
|
| 191 |
+
func (s *GrokService) retryWithProxy(ctx context.Context, account *model.Account, modelID string, body []byte) (*http.Response, error) {
|
| 192 |
+
// 获取模型配置
|
| 193 |
+
zenModel, exists := model.GetZenModel(modelID)
|
| 194 |
+
if !exists {
|
| 195 |
+
return nil, fmt.Errorf("模型配置不存在: %s", modelID)
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
proxyPool := provider.GetProxyPool()
|
| 199 |
+
if !proxyPool.HasProxies() {
|
| 200 |
+
return nil, fmt.Errorf("没有可用的代理")
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
maxRetries := 3
|
| 204 |
+
for i := 0; i < maxRetries; i++ {
|
| 205 |
+
// 获取随机代理
|
| 206 |
+
proxyURL := proxyPool.GetRandomProxy()
|
| 207 |
+
if proxyURL == "" {
|
| 208 |
+
continue
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
log.Printf("[Grok] 尝试代理 %s (重试 %d/%d)", proxyURL, i+1, maxRetries)
|
| 212 |
+
|
| 213 |
+
// 创建使用代理的HTTP客户端
|
| 214 |
+
proxyClient, err := provider.NewHTTPClientWithProxy(proxyURL, 0)
|
| 215 |
+
if err != nil {
|
| 216 |
+
log.Printf("[Grok] 创建代理客户端失败: %v", err)
|
| 217 |
+
continue
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// 处理请求体,Grok Code 模型要求 temperature=0
|
| 221 |
+
modifiedBody := body
|
| 222 |
+
if strings.Contains(modelID, "grok-code") {
|
| 223 |
+
modifiedBody, _ = s.setTemperatureZero(body)
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
// 创建新请求
|
| 227 |
+
reqURL := GrokBaseURL + "/v1/chat/completions"
|
| 228 |
+
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewReader(modifiedBody))
|
| 229 |
+
if err != nil {
|
| 230 |
+
log.Printf("[Grok] 创建请求失败: %v", err)
|
| 231 |
+
continue
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// 设置请求头
|
| 235 |
+
SetZencoderHeaders(httpReq, account, zenModel)
|
| 236 |
+
|
| 237 |
+
// 添加模型配置的额外请求头
|
| 238 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 239 |
+
for k, v := range zenModel.Parameters.ExtraHeaders {
|
| 240 |
+
httpReq.Header.Set(k, v)
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// 执行请求
|
| 245 |
+
resp, err := proxyClient.Do(httpReq)
|
| 246 |
+
if err != nil {
|
| 247 |
+
log.Printf("[Grok] 代理请求失败: %v", err)
|
| 248 |
+
continue
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// 检查响应状态
|
| 252 |
+
if resp.StatusCode == 429 {
|
| 253 |
+
// 仍然是429,尝试下一个代理
|
| 254 |
+
resp.Body.Close()
|
| 255 |
+
log.Printf("[Grok] 代理 %s 仍返回429,尝试下一个", proxyURL)
|
| 256 |
+
continue
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
if resp.StatusCode >= 400 {
|
| 260 |
+
// 其他错误,记录并尝试下一个代理
|
| 261 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 262 |
+
resp.Body.Close()
|
| 263 |
+
log.Printf("[Grok] 代理 %s 返回错误 %d: %s", proxyURL, resp.StatusCode, string(errBody))
|
| 264 |
+
continue
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
// 成功
|
| 268 |
+
log.Printf("[Grok] 代理 %s 请求成功", proxyURL)
|
| 269 |
+
return resp, nil
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
return nil, fmt.Errorf("所有代理重试均失败")
|
| 273 |
+
}
|
internal/service/headers.go
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"math/rand"
|
| 5 |
+
"net/http"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"zencoder-2api/internal/model"
|
| 9 |
+
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
var (
|
| 14 |
+
// 可变的 User-Agent 列表
|
| 15 |
+
userAgents = []string{
|
| 16 |
+
"zen-cli/0.9.0-SNAPSHOT_4c6ffdd-windows-x64",
|
| 17 |
+
"zen-cli/0.9.0-SNAPSHOT_5d7ggee-windows-x64",
|
| 18 |
+
"zen-cli/0.9.0-SNAPSHOT_6e8hhff-windows-x64",
|
| 19 |
+
"zen-cli/0.8.9-SNAPSHOT_3b5eedd-windows-x64",
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// 可变的 Node 版本
|
| 23 |
+
nodeVersions = []string{
|
| 24 |
+
"v24.3.0",
|
| 25 |
+
"v24.2.0",
|
| 26 |
+
"v24.1.0",
|
| 27 |
+
"v23.5.0",
|
| 28 |
+
"v22.11.0",
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// 可变的 zencoder 版本
|
| 32 |
+
zencoderVersions = []string{
|
| 33 |
+
"3.24.0",
|
| 34 |
+
"3.23.9",
|
| 35 |
+
"3.23.8",
|
| 36 |
+
"3.24.1",
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// 可变的 package 版本
|
| 40 |
+
packageVersions = []string{
|
| 41 |
+
"6.9.1",
|
| 42 |
+
"6.9.0",
|
| 43 |
+
"6.8.9",
|
| 44 |
+
"6.8.8",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
// 随机选择一个元素
|
| 51 |
+
func randomChoice(items []string) string {
|
| 52 |
+
return items[rng.Intn(len(items))]
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// SetZencoderHeaders 设置Zencoder自定义请求头
|
| 56 |
+
func SetZencoderHeaders(req *http.Request, account *model.Account, zenModel model.ZenModel) {
|
| 57 |
+
// 基础请求头 - 使用随机 User-Agent
|
| 58 |
+
req.Header.Set("User-Agent", "zen-cli/0.9.0-SNAPSHOT_4c6ffdd-windows-x64")
|
| 59 |
+
req.Header.Set("Accept", "application/json")
|
| 60 |
+
req.Header.Set("Content-Type", "application/json")
|
| 61 |
+
req.Header.Set("Connection", "keep-alive")
|
| 62 |
+
|
| 63 |
+
// 认证头
|
| 64 |
+
req.Header.Set("Authorization", "Bearer "+account.AccessToken)
|
| 65 |
+
|
| 66 |
+
// x-stainless 系列
|
| 67 |
+
req.Header.Set("x-stainless-arch", "x64")
|
| 68 |
+
req.Header.Set("x-stainless-lang", "js")
|
| 69 |
+
req.Header.Set("x-stainless-os", "Windows")
|
| 70 |
+
req.Header.Set("x-stainless-package-version", "0.70.1")
|
| 71 |
+
req.Header.Set("x-stainless-retry-count", "0")
|
| 72 |
+
req.Header.Set("x-stainless-runtime", "node")
|
| 73 |
+
req.Header.Set("x-stainless-runtime-version", "v24.3.0")
|
| 74 |
+
|
| 75 |
+
// zen/zencoder 系列 - 使用随机版本和唯一 ID
|
| 76 |
+
req.Header.Set("zen-model-id", zenModel.ID)
|
| 77 |
+
req.Header.Set("zencoder-arch", "x64")
|
| 78 |
+
req.Header.Set("zencoder-auto-model", "false")
|
| 79 |
+
req.Header.Set("zencoder-client-type", "vscode")
|
| 80 |
+
req.Header.Set("zencoder-operation-id", uuid.New().String())
|
| 81 |
+
req.Header.Set("zencoder-operation-type", "agent_call")
|
| 82 |
+
req.Header.Set("zencoder-os", "windows")
|
| 83 |
+
req.Header.Set("zencoder-version", "3.24.0")
|
| 84 |
+
}
|
internal/service/jwt.go
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/base64"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"errors"
|
| 7 |
+
"strings"
|
| 8 |
+
"time"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type CustomClaims struct {
|
| 12 |
+
Plan string `json:"plan"`
|
| 13 |
+
Autobots struct {
|
| 14 |
+
SubscriptionStartDate string `json:"subscription_start_date"`
|
| 15 |
+
} `json:"autobots"`
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
type JWTPayload struct {
|
| 19 |
+
Subject string `json:"sub"`
|
| 20 |
+
ClientID string `json:"client_id"`
|
| 21 |
+
Email string `json:"email"`
|
| 22 |
+
CustomClaims CustomClaims `json:"customClaims"`
|
| 23 |
+
IssuedAt int64 `json:"iat"`
|
| 24 |
+
Expiration int64 `json:"exp"`
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func ParseJWT(tokenString string) (*JWTPayload, error) {
|
| 28 |
+
parts := strings.Split(tokenString, ".")
|
| 29 |
+
if len(parts) != 3 {
|
| 30 |
+
return nil, errors.New("invalid token format")
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
payloadPart := parts[1]
|
| 34 |
+
|
| 35 |
+
// Add padding if missing
|
| 36 |
+
if l := len(payloadPart) % 4; l > 0 {
|
| 37 |
+
payloadPart += strings.Repeat("=", 4-l)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
decoded, err := base64.URLEncoding.DecodeString(payloadPart)
|
| 41 |
+
if err != nil {
|
| 42 |
+
// Try standard encoding if URL encoding fails
|
| 43 |
+
decoded, err = base64.StdEncoding.DecodeString(payloadPart)
|
| 44 |
+
if err != nil {
|
| 45 |
+
return nil, err
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
var payload JWTPayload
|
| 50 |
+
if err := json.Unmarshal(decoded, &payload); err != nil {
|
| 51 |
+
return nil, err
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return &payload, nil
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func GetSubscriptionDate(payload *JWTPayload) time.Time {
|
| 58 |
+
// Try to parse SubscriptionStartDate from CustomClaims
|
| 59 |
+
if v := payload.CustomClaims.Autobots.SubscriptionStartDate; v != "" {
|
| 60 |
+
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
| 61 |
+
return t
|
| 62 |
+
}
|
| 63 |
+
if t, err := time.Parse("2006-01-02", v); err == nil {
|
| 64 |
+
return t
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// Fallback to IssuedAt
|
| 69 |
+
if payload.IssuedAt > 0 {
|
| 70 |
+
return time.Unix(payload.IssuedAt, 0)
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return time.Now()
|
| 74 |
+
}
|
internal/service/openai.go
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"bytes"
|
| 6 |
+
"context"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"fmt"
|
| 9 |
+
"io"
|
| 10 |
+
"log"
|
| 11 |
+
"net/http"
|
| 12 |
+
"strings"
|
| 13 |
+
"time"
|
| 14 |
+
|
| 15 |
+
"zencoder-2api/internal/model"
|
| 16 |
+
"zencoder-2api/internal/service/provider"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
const OpenAIBaseURL = "https://api.zencoder.ai/openai"
|
| 20 |
+
|
| 21 |
+
type OpenAIService struct{}
|
| 22 |
+
|
| 23 |
+
func NewOpenAIService() *OpenAIService {
|
| 24 |
+
return &OpenAIService{}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// ChatCompletions 处理/v1/chat/completions请求
|
| 28 |
+
func (s *OpenAIService) ChatCompletions(ctx context.Context, body []byte) (*http.Response, error) {
|
| 29 |
+
var req struct {
|
| 30 |
+
Model string `json:"model"`
|
| 31 |
+
}
|
| 32 |
+
if err := json.Unmarshal(body, &req); err != nil {
|
| 33 |
+
return nil, fmt.Errorf("invalid request body: %w", err)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// 检查模型是否存在于模型字典中
|
| 37 |
+
_, exists := model.GetZenModel(req.Model)
|
| 38 |
+
if !exists {
|
| 39 |
+
DebugLog(ctx, "[OpenAI] 模型不存在: %s", req.Model)
|
| 40 |
+
return nil, ErrNoAvailableAccount
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
DebugLogRequest(ctx, "OpenAI", "/v1/chat/completions", req.Model)
|
| 44 |
+
|
| 45 |
+
var lastErr error
|
| 46 |
+
for i := 0; i < MaxRetries; i++ {
|
| 47 |
+
account, err := GetNextAccountForModel(req.Model)
|
| 48 |
+
if err != nil {
|
| 49 |
+
DebugLogRequestEnd(ctx, "OpenAI", false, err)
|
| 50 |
+
return nil, err
|
| 51 |
+
}
|
| 52 |
+
DebugLogAccountSelected(ctx, "OpenAI", account.ID, account.Email)
|
| 53 |
+
|
| 54 |
+
// Zencoder API使用/v1/responses端点
|
| 55 |
+
// 需要转换请求体:messages -> input
|
| 56 |
+
convertedBody, err := s.convertChatToResponsesBody(body)
|
| 57 |
+
if err != nil {
|
| 58 |
+
DebugLogRequestEnd(ctx, "OpenAI", false, err)
|
| 59 |
+
return nil, fmt.Errorf("failed to convert request body: %w", err)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
resp, err := s.doRequest(ctx, account, req.Model, "/v1/responses", convertedBody)
|
| 63 |
+
if err != nil {
|
| 64 |
+
MarkAccountError(account)
|
| 65 |
+
lastErr = err
|
| 66 |
+
DebugLogRetry(ctx, "OpenAI", i+1, account.ID, err)
|
| 67 |
+
continue
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
DebugLogResponseReceived(ctx, "OpenAI", resp.StatusCode)
|
| 71 |
+
DebugLogResponseHeaders(ctx, "OpenAI", resp.Header)
|
| 72 |
+
|
| 73 |
+
// 总是输出重要的响应头信息
|
| 74 |
+
if resp.Header.Get("Zen-Pricing-Period-Limit") != "" ||
|
| 75 |
+
resp.Header.Get("Zen-Pricing-Period-Cost") != "" ||
|
| 76 |
+
resp.Header.Get("Zen-Request-Cost") != "" {
|
| 77 |
+
log.Printf("[OpenAI] 积分信息 - 周期限额: %s, 周期消耗: %s, 本次消耗: %s",
|
| 78 |
+
resp.Header.Get("Zen-Pricing-Period-Limit"),
|
| 79 |
+
resp.Header.Get("Zen-Pricing-Period-Cost"),
|
| 80 |
+
resp.Header.Get("Zen-Request-Cost"))
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
if resp.StatusCode >= 400 {
|
| 84 |
+
// 读取错误响应内容
|
| 85 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 86 |
+
resp.Body.Close()
|
| 87 |
+
DebugLogErrorResponse(ctx, "OpenAI", resp.StatusCode, string(errBody))
|
| 88 |
+
|
| 89 |
+
// 400和500错误直接返回,不进行账号错误计数
|
| 90 |
+
if resp.StatusCode == 400 || resp.StatusCode == 500 {
|
| 91 |
+
DebugLogRequestEnd(ctx, "OpenAI", false, fmt.Errorf("API error: %d", resp.StatusCode))
|
| 92 |
+
return nil, fmt.Errorf("API error: %d - %s", resp.StatusCode, string(errBody))
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// 429 错误特殊处理
|
| 96 |
+
if resp.StatusCode == 429 {
|
| 97 |
+
log.Printf("[OpenAI] 429限流错误,尝试使用代理重试")
|
| 98 |
+
|
| 99 |
+
// 尝试使用代理池重试
|
| 100 |
+
proxyResp, proxyErr := s.retryWithProxy(ctx, account, req.Model, "/v1/responses", convertedBody)
|
| 101 |
+
if proxyErr == nil && proxyResp != nil {
|
| 102 |
+
// 代理重试成功
|
| 103 |
+
return proxyResp, nil
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
log.Printf("[OpenAI] 代理重试失败: %v", proxyErr)
|
| 107 |
+
MarkAccountRateLimitedWithResponse(account, resp)
|
| 108 |
+
} else {
|
| 109 |
+
MarkAccountError(account)
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
lastErr = fmt.Errorf("API error: %d", resp.StatusCode)
|
| 113 |
+
DebugLogRetry(ctx, "OpenAI", i+1, account.ID, lastErr)
|
| 114 |
+
continue
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
ResetAccountError(account)
|
| 118 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 119 |
+
if !exists {
|
| 120 |
+
// 模型不存在,使用默认倍率
|
| 121 |
+
UpdateAccountCreditsFromResponse(account, resp, 1.0)
|
| 122 |
+
} else {
|
| 123 |
+
// 使用统一的积分更新函数,自动处理响应头中的积分信息
|
| 124 |
+
UpdateAccountCreditsFromResponse(account, resp, zenModel.Multiplier)
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
DebugLogRequestEnd(ctx, "OpenAI", true, nil)
|
| 128 |
+
return resp, nil
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
DebugLogRequestEnd(ctx, "OpenAI", false, lastErr)
|
| 132 |
+
return nil, fmt.Errorf("all retries failed: %w", lastErr)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Responses 处理/v1/responses请求
|
| 136 |
+
func (s *OpenAIService) Responses(ctx context.Context, body []byte) (*http.Response, error) {
|
| 137 |
+
var req struct {
|
| 138 |
+
Model string `json:"model"`
|
| 139 |
+
}
|
| 140 |
+
if err := json.Unmarshal(body, &req); err != nil {
|
| 141 |
+
return nil, fmt.Errorf("invalid request body: %w", err)
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// 检查模型是否存在于模型字典中
|
| 145 |
+
_, exists := model.GetZenModel(req.Model)
|
| 146 |
+
if !exists {
|
| 147 |
+
DebugLog(ctx, "[OpenAI] 模型不存在: %s", req.Model)
|
| 148 |
+
return nil, ErrNoAvailableAccount
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
DebugLogRequest(ctx, "OpenAI", "/v1/responses", req.Model)
|
| 152 |
+
|
| 153 |
+
var lastErr error
|
| 154 |
+
for i := 0; i < MaxRetries; i++ {
|
| 155 |
+
account, err := GetNextAccountForModel(req.Model)
|
| 156 |
+
if err != nil {
|
| 157 |
+
DebugLogRequestEnd(ctx, "OpenAI", false, err)
|
| 158 |
+
return nil, err
|
| 159 |
+
}
|
| 160 |
+
DebugLogAccountSelected(ctx, "OpenAI", account.ID, account.Email)
|
| 161 |
+
|
| 162 |
+
resp, err := s.doRequest(ctx, account, req.Model, "/v1/responses", body)
|
| 163 |
+
if err != nil {
|
| 164 |
+
MarkAccountError(account)
|
| 165 |
+
lastErr = err
|
| 166 |
+
DebugLogRetry(ctx, "OpenAI", i+1, account.ID, err)
|
| 167 |
+
continue
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
DebugLogResponseReceived(ctx, "OpenAI", resp.StatusCode)
|
| 171 |
+
DebugLogResponseHeaders(ctx, "OpenAI", resp.Header)
|
| 172 |
+
|
| 173 |
+
// 总是输出重要的响应头信息
|
| 174 |
+
if resp.Header.Get("Zen-Pricing-Period-Limit") != "" ||
|
| 175 |
+
resp.Header.Get("Zen-Pricing-Period-Cost") != "" ||
|
| 176 |
+
resp.Header.Get("Zen-Request-Cost") != "" {
|
| 177 |
+
log.Printf("[OpenAI] 积分信息 - 周期限额: %s, 周期消耗: %s, 本次消耗: %s",
|
| 178 |
+
resp.Header.Get("Zen-Pricing-Period-Limit"),
|
| 179 |
+
resp.Header.Get("Zen-Pricing-Period-Cost"),
|
| 180 |
+
resp.Header.Get("Zen-Request-Cost"))
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
if resp.StatusCode >= 400 {
|
| 184 |
+
// 读取错误响应内容
|
| 185 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 186 |
+
resp.Body.Close()
|
| 187 |
+
|
| 188 |
+
// 429 错误特殊处理 - 直接返回,不重试
|
| 189 |
+
if resp.StatusCode == 429 {
|
| 190 |
+
log.Printf("[OpenAI] 429限流错误,尝试使用代理重试")
|
| 191 |
+
|
| 192 |
+
// 尝试使用代理池重试
|
| 193 |
+
proxyResp, proxyErr := s.retryWithProxy(ctx, account, req.Model, "/v1/responses", body)
|
| 194 |
+
if proxyErr == nil && proxyResp != nil {
|
| 195 |
+
// 代理重试成功
|
| 196 |
+
return proxyResp, nil
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
log.Printf("[OpenAI] 代理重试失败: %v", proxyErr)
|
| 200 |
+
// 将账号放入短期冷却(5秒)
|
| 201 |
+
MarkAccountRateLimitedShort(account)
|
| 202 |
+
// 不输出错误日志,直接返回
|
| 203 |
+
return nil, ErrNoAvailableAccount
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
DebugLogErrorResponse(ctx, "OpenAI", resp.StatusCode, string(errBody))
|
| 207 |
+
|
| 208 |
+
// 400和500错误直接返回,不进行账号错误计数
|
| 209 |
+
if resp.StatusCode == 400 || resp.StatusCode == 500 {
|
| 210 |
+
DebugLogRequestEnd(ctx, "OpenAI", false, fmt.Errorf("API error: %d", resp.StatusCode))
|
| 211 |
+
return nil, fmt.Errorf("API error: %d - %s", resp.StatusCode, string(errBody))
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
MarkAccountError(account)
|
| 215 |
+
lastErr = fmt.Errorf("API error: %d", resp.StatusCode)
|
| 216 |
+
DebugLogRetry(ctx, "OpenAI", i+1, account.ID, lastErr)
|
| 217 |
+
continue
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
ResetAccountError(account)
|
| 221 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 222 |
+
if !exists {
|
| 223 |
+
// 模型不存在,使用默认倍率
|
| 224 |
+
UpdateAccountCreditsFromResponse(account, resp, 1.0)
|
| 225 |
+
} else {
|
| 226 |
+
// 使用统一的积分更新函数,自动处理响应头中的积分信息
|
| 227 |
+
UpdateAccountCreditsFromResponse(account, resp, zenModel.Multiplier)
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
DebugLogRequestEnd(ctx, "OpenAI", true, nil)
|
| 231 |
+
return resp, nil
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
DebugLogRequestEnd(ctx, "OpenAI", false, lastErr)
|
| 235 |
+
return nil, fmt.Errorf("all retries failed: %w", lastErr)
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// convertChatToResponsesBody 将 Chat Completion 的请求体转换为 Responses API 的请求体
|
| 239 |
+
func (s *OpenAIService) convertChatToResponsesBody(body []byte) ([]byte, error) {
|
| 240 |
+
var raw map[string]interface{}
|
| 241 |
+
if err := json.Unmarshal(body, &raw); err != nil {
|
| 242 |
+
return nil, err
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// 移除 /v1/responses API 不支持的参数
|
| 246 |
+
delete(raw, "stream_options") // 不支持 stream_options.include_usage 等
|
| 247 |
+
delete(raw, "function_call") // 旧版函数调用参数
|
| 248 |
+
delete(raw, "functions") // 旧版函数定义参数
|
| 249 |
+
|
| 250 |
+
// 转换 token 限制参数
|
| 251 |
+
// max_completion_tokens (新) / max_tokens (旧) -> max_output_tokens (Responses API)
|
| 252 |
+
if val, ok := raw["max_completion_tokens"]; ok {
|
| 253 |
+
raw["max_output_tokens"] = val
|
| 254 |
+
delete(raw, "max_completion_tokens")
|
| 255 |
+
} else if val, ok := raw["max_tokens"]; ok {
|
| 256 |
+
raw["max_output_tokens"] = val
|
| 257 |
+
delete(raw, "max_tokens")
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
modelStr, _ := raw["model"].(string)
|
| 261 |
+
|
| 262 |
+
// 检查是否有 messages 字段
|
| 263 |
+
if messages, ok := raw["messages"].([]interface{}); ok {
|
| 264 |
+
if modelStr == "gpt-5-nano-2025-08-07" {
|
| 265 |
+
// gpt-5-nano 特殊处理:转换为复杂的 input 结构
|
| 266 |
+
newInput := make([]map[string]interface{}, 0)
|
| 267 |
+
for _, m := range messages {
|
| 268 |
+
if msgMap, ok := m.(map[string]interface{}); ok {
|
| 269 |
+
role, _ := msgMap["role"].(string)
|
| 270 |
+
content := msgMap["content"]
|
| 271 |
+
|
| 272 |
+
newItem := map[string]interface{}{
|
| 273 |
+
"type": "message",
|
| 274 |
+
"role": role,
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
newContent := make([]map[string]interface{}, 0)
|
| 278 |
+
if contentStr, ok := content.(string); ok {
|
| 279 |
+
newContent = append(newContent, map[string]interface{}{
|
| 280 |
+
"type": "input_text",
|
| 281 |
+
"text": contentStr,
|
| 282 |
+
})
|
| 283 |
+
}
|
| 284 |
+
// 这里的 content 如果是数组,暂时忽略或假设是纯文本场景
|
| 285 |
+
// 如果需要支持多模态,需要进一步解析 content 数组
|
| 286 |
+
|
| 287 |
+
newItem["content"] = newContent
|
| 288 |
+
newInput = append(newInput, newItem)
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
raw["input"] = newInput
|
| 292 |
+
} else {
|
| 293 |
+
// 标准转换:直接移动到 input
|
| 294 |
+
raw["input"] = messages
|
| 295 |
+
}
|
| 296 |
+
delete(raw, "messages")
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
// gpt-5-nano-2025-08-07 特殊处理参数
|
| 300 |
+
if modelStr == "gpt-5-nano-2025-08-07" {
|
| 301 |
+
// 添加该模型所需的特定参数
|
| 302 |
+
raw["prompt_cache_key"] = "generate-name"
|
| 303 |
+
raw["store"] = false
|
| 304 |
+
raw["include"] = []string{"reasoning.encrypted_content"}
|
| 305 |
+
raw["service_tier"] = "auto"
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
return json.Marshal(raw)
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
func (s *OpenAIService) doRequest(ctx context.Context, account *model.Account, modelID, path string, body []byte) (*http.Response, error) {
|
| 312 |
+
zenModel, exists := model.GetZenModel(modelID)
|
| 313 |
+
if !exists {
|
| 314 |
+
return nil, ErrNoAvailableAccount
|
| 315 |
+
}
|
| 316 |
+
httpClient := provider.NewHTTPClient(account.Proxy, 0)
|
| 317 |
+
|
| 318 |
+
// 将模型参数合并到请求体中
|
| 319 |
+
modifiedBody := body
|
| 320 |
+
if zenModel.Parameters != nil {
|
| 321 |
+
var raw map[string]interface{}
|
| 322 |
+
if json.Unmarshal(modifiedBody, &raw) == nil {
|
| 323 |
+
// 添加 reasoning 配置
|
| 324 |
+
if zenModel.Parameters.Reasoning != nil && raw["reasoning"] == nil {
|
| 325 |
+
reasoningMap := map[string]interface{}{
|
| 326 |
+
"effort": zenModel.Parameters.Reasoning.Effort,
|
| 327 |
+
}
|
| 328 |
+
if zenModel.Parameters.Reasoning.Summary != "" {
|
| 329 |
+
reasoningMap["summary"] = zenModel.Parameters.Reasoning.Summary
|
| 330 |
+
}
|
| 331 |
+
raw["reasoning"] = reasoningMap
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
// 添加 text 配置
|
| 335 |
+
if zenModel.Parameters.Text != nil && raw["text"] == nil {
|
| 336 |
+
raw["text"] = map[string]interface{}{
|
| 337 |
+
"verbosity": zenModel.Parameters.Text.Verbosity,
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
// 添加 temperature 配置
|
| 342 |
+
if zenModel.Parameters.Temperature != nil && raw["temperature"] == nil {
|
| 343 |
+
raw["temperature"] = *zenModel.Parameters.Temperature
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
modifiedBody, _ = json.Marshal(raw)
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// gpt-5-nano-2025-08-07 特殊处理参数
|
| 351 |
+
if modelID == "gpt-5-nano-2025-08-07" {
|
| 352 |
+
var raw map[string]interface{}
|
| 353 |
+
if json.Unmarshal(modifiedBody, &raw) == nil {
|
| 354 |
+
// 添加 text 参数
|
| 355 |
+
if _, ok := raw["text"]; !ok {
|
| 356 |
+
raw["text"] = map[string]string{"verbosity": "medium"}
|
| 357 |
+
}
|
| 358 |
+
// 添加 temperature 参数 (如果缺失)
|
| 359 |
+
if _, ok := raw["temperature"]; !ok {
|
| 360 |
+
raw["temperature"] = 1
|
| 361 |
+
}
|
| 362 |
+
// 强制开启 stream,因为该模型似乎不支持非流式
|
| 363 |
+
raw["stream"] = true
|
| 364 |
+
|
| 365 |
+
// 修正 reasoning 参数,添加 summary
|
| 366 |
+
if reasoning, ok := raw["reasoning"].(map[string]interface{}); ok {
|
| 367 |
+
reasoning["summary"] = "auto"
|
| 368 |
+
raw["reasoning"] = reasoning
|
| 369 |
+
} else {
|
| 370 |
+
raw["reasoning"] = map[string]interface{}{
|
| 371 |
+
"effort": "minimal",
|
| 372 |
+
"summary": "auto",
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
modifiedBody, _ = json.Marshal(raw)
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// 注意:已移除模型重定向逻辑,直接使用用户请求的模型名
|
| 380 |
+
DebugLogActualModel(ctx, "OpenAI", modelID, modelID)
|
| 381 |
+
|
| 382 |
+
reqURL := OpenAIBaseURL + path
|
| 383 |
+
DebugLogRequestSent(ctx, "OpenAI", reqURL)
|
| 384 |
+
|
| 385 |
+
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewReader(modifiedBody))
|
| 386 |
+
if err != nil {
|
| 387 |
+
return nil, err
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
// 设置Zencoder自定义请求头
|
| 391 |
+
SetZencoderHeaders(httpReq, account, zenModel)
|
| 392 |
+
|
| 393 |
+
// 添加模型配置的额外请求头
|
| 394 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 395 |
+
for k, v := range zenModel.Parameters.ExtraHeaders {
|
| 396 |
+
httpReq.Header.Set(k, v)
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// 记录请求头用于调试
|
| 401 |
+
DebugLogRequestHeaders(ctx, "OpenAI", httpReq.Header)
|
| 402 |
+
|
| 403 |
+
// 强制记录请求体用于调试
|
| 404 |
+
log.Printf("[DEBUG] [OpenAI] 请求体:")
|
| 405 |
+
log.Printf("[DEBUG] [OpenAI] %s", string(modifiedBody))
|
| 406 |
+
|
| 407 |
+
return httpClient.Do(httpReq)
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// ChatCompletionsProxy 代理chat completions请求
|
| 411 |
+
func (s *OpenAIService) ChatCompletionsProxy(ctx context.Context, w http.ResponseWriter, body []byte) error {
|
| 412 |
+
// 解析 model 和 stream 参数
|
| 413 |
+
var req struct {
|
| 414 |
+
Model string `json:"model"`
|
| 415 |
+
Stream bool `json:"stream"`
|
| 416 |
+
}
|
| 417 |
+
// 忽略错误,因为ChatCompletions会再次解析并处理错误
|
| 418 |
+
_ = json.Unmarshal(body, &req)
|
| 419 |
+
|
| 420 |
+
resp, err := s.ChatCompletions(ctx, body)
|
| 421 |
+
if err != nil {
|
| 422 |
+
return err
|
| 423 |
+
}
|
| 424 |
+
defer resp.Body.Close()
|
| 425 |
+
|
| 426 |
+
if req.Stream {
|
| 427 |
+
return s.streamConvertedResponse(w, resp, req.Model)
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
return s.handleNonStreamResponse(w, resp, req.Model)
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
func (s *OpenAIService) handleNonStreamResponse(w http.ResponseWriter, resp *http.Response, modelID string) error {
|
| 434 |
+
// 读取全部响应体
|
| 435 |
+
bodyBytes, err := io.ReadAll(resp.Body)
|
| 436 |
+
if err != nil {
|
| 437 |
+
return err
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// 复制响应头
|
| 441 |
+
for k, v := range resp.Header {
|
| 442 |
+
// 过滤掉 Content-Length (会重新计算) 和 Content-Encoding (Go会自动解压)
|
| 443 |
+
if k != "Content-Length" && k != "Content-Encoding" {
|
| 444 |
+
for _, vv := range v {
|
| 445 |
+
w.Header().Add(k, vv)
|
| 446 |
+
}
|
| 447 |
+
}
|
| 448 |
+
}
|
| 449 |
+
w.WriteHeader(resp.StatusCode)
|
| 450 |
+
|
| 451 |
+
// 尝试解析响应
|
| 452 |
+
var raw map[string]interface{}
|
| 453 |
+
if err := json.Unmarshal(bodyBytes, &raw); err != nil {
|
| 454 |
+
// 如果不是 JSON,检查是否是 SSE 流 (可能是因为我们强制开启了 stream)
|
| 455 |
+
bodyStr := string(bodyBytes)
|
| 456 |
+
trimmedBody := strings.TrimSpace(bodyStr)
|
| 457 |
+
contentType := resp.Header.Get("Content-Type")
|
| 458 |
+
isSSE := strings.Contains(contentType, "text/event-stream") ||
|
| 459 |
+
strings.HasPrefix(trimmedBody, "data:") ||
|
| 460 |
+
strings.HasPrefix(trimmedBody, "event:") ||
|
| 461 |
+
strings.HasPrefix(trimmedBody, ":") ||
|
| 462 |
+
modelID == "gpt-5-nano-2025-08-07" // 强制该模型走 SSE 解析
|
| 463 |
+
|
| 464 |
+
if isSSE {
|
| 465 |
+
var fullContent string
|
| 466 |
+
scanner := bufio.NewScanner(bytes.NewReader(bodyBytes))
|
| 467 |
+
for scanner.Scan() {
|
| 468 |
+
line := strings.TrimSpace(scanner.Text())
|
| 469 |
+
if !strings.HasPrefix(line, "data: ") {
|
| 470 |
+
continue
|
| 471 |
+
}
|
| 472 |
+
data := strings.TrimPrefix(line, "data: ")
|
| 473 |
+
if data == "[DONE]" {
|
| 474 |
+
break
|
| 475 |
+
}
|
| 476 |
+
var chunk map[string]interface{}
|
| 477 |
+
if json.Unmarshal([]byte(data), &chunk) == nil {
|
| 478 |
+
// 尝试提取 content
|
| 479 |
+
if val, ok := chunk["text"].(string); ok {
|
| 480 |
+
fullContent += val
|
| 481 |
+
} else if val, ok := chunk["content"].(string); ok {
|
| 482 |
+
fullContent += val
|
| 483 |
+
} else if val, ok := chunk["response"].(string); ok {
|
| 484 |
+
fullContent += val
|
| 485 |
+
}
|
| 486 |
+
// 标准 chunk
|
| 487 |
+
if choices, ok := chunk["choices"].([]interface{}); ok && len(choices) > 0 {
|
| 488 |
+
if choice, ok := choices[0].(map[string]interface{}); ok {
|
| 489 |
+
if delta, ok := choice["delta"].(map[string]interface{}); ok {
|
| 490 |
+
if content, ok := delta["content"].(string); ok {
|
| 491 |
+
fullContent += content
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// 如果提取到了内容,或者是强制模型(即使没提取到也返回空内容以避免透传错误格式)
|
| 500 |
+
if fullContent != "" || modelID == "gpt-5-nano-2025-08-07" {
|
| 501 |
+
timestamp := time.Now().Unix()
|
| 502 |
+
respObj := model.ChatCompletionResponse{
|
| 503 |
+
ID: fmt.Sprintf("chatcmpl-%d", timestamp),
|
| 504 |
+
Object: "chat.completion",
|
| 505 |
+
Created: timestamp,
|
| 506 |
+
Model: modelID,
|
| 507 |
+
Choices: []model.Choice{
|
| 508 |
+
{
|
| 509 |
+
Index: 0,
|
| 510 |
+
Message: model.ChatMessage{
|
| 511 |
+
Role: "assistant",
|
| 512 |
+
Content: fullContent,
|
| 513 |
+
},
|
| 514 |
+
FinishReason: "stop",
|
| 515 |
+
},
|
| 516 |
+
},
|
| 517 |
+
}
|
| 518 |
+
return json.NewEncoder(w).Encode(respObj)
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
// 既不是 JSON 也不是 SSE,直接透传
|
| 523 |
+
w.Write(bodyBytes)
|
| 524 |
+
return nil
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
// 检查是否已经是 OpenAI 格式 (包含 choices)
|
| 528 |
+
if _, ok := raw["choices"]; ok {
|
| 529 |
+
w.Write(bodyBytes)
|
| 530 |
+
return nil
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// 尝试从常见字段提取内容进行转换
|
| 534 |
+
var content string
|
| 535 |
+
if val, ok := raw["text"].(string); ok {
|
| 536 |
+
content = val
|
| 537 |
+
} else if val, ok := raw["content"].(string); ok {
|
| 538 |
+
content = val
|
| 539 |
+
} else if val, ok := raw["response"].(string); ok {
|
| 540 |
+
content = val
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
if content != "" {
|
| 544 |
+
timestamp := time.Now().Unix()
|
| 545 |
+
respObj := model.ChatCompletionResponse{
|
| 546 |
+
ID: fmt.Sprintf("chatcmpl-%d", timestamp),
|
| 547 |
+
Object: "chat.completion",
|
| 548 |
+
Created: timestamp,
|
| 549 |
+
Model: modelID,
|
| 550 |
+
Choices: []model.Choice{
|
| 551 |
+
{
|
| 552 |
+
Index: 0,
|
| 553 |
+
Message: model.ChatMessage{
|
| 554 |
+
Role: "assistant",
|
| 555 |
+
Content: content,
|
| 556 |
+
},
|
| 557 |
+
FinishReason: "stop",
|
| 558 |
+
},
|
| 559 |
+
},
|
| 560 |
+
}
|
| 561 |
+
return json.NewEncoder(w).Encode(respObj)
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
// 无法识别格式,直接透传
|
| 565 |
+
w.Write(bodyBytes)
|
| 566 |
+
return nil
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
func (s *OpenAIService) streamConvertedResponse(w http.ResponseWriter, resp *http.Response, modelID string) error {
|
| 570 |
+
// 复制响应头
|
| 571 |
+
for k, v := range resp.Header {
|
| 572 |
+
// 过滤掉 Content-Encoding 和 Content-Length
|
| 573 |
+
if k != "Content-Encoding" && k != "Content-Length" {
|
| 574 |
+
for _, vv := range v {
|
| 575 |
+
w.Header().Add(k, vv)
|
| 576 |
+
}
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
w.WriteHeader(resp.StatusCode)
|
| 580 |
+
|
| 581 |
+
flusher, ok := w.(http.Flusher)
|
| 582 |
+
if !ok {
|
| 583 |
+
// 如果不支持Flusher,回退到普通复制
|
| 584 |
+
_, err := io.Copy(w, resp.Body)
|
| 585 |
+
return err
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
reader := bufio.NewReader(resp.Body)
|
| 589 |
+
timestamp := time.Now().Unix()
|
| 590 |
+
id := fmt.Sprintf("chatcmpl-%d", timestamp)
|
| 591 |
+
|
| 592 |
+
for {
|
| 593 |
+
line, err := reader.ReadString('\n')
|
| 594 |
+
if err != nil {
|
| 595 |
+
if err == io.EOF {
|
| 596 |
+
return nil
|
| 597 |
+
}
|
| 598 |
+
return err
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
// 处理空行
|
| 602 |
+
trimmedLine := strings.TrimSpace(line)
|
| 603 |
+
if trimmedLine == "" {
|
| 604 |
+
fmt.Fprintf(w, "\n")
|
| 605 |
+
flusher.Flush()
|
| 606 |
+
continue
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// 解析 data: 前缀
|
| 610 |
+
if !strings.HasPrefix(trimmedLine, "data: ") {
|
| 611 |
+
// 尝试解析为 JSON 对象 (处理被强制转为非流式的响应)
|
| 612 |
+
var rawObj map[string]interface{}
|
| 613 |
+
if json.Unmarshal([]byte(trimmedLine), &rawObj) == nil {
|
| 614 |
+
// 尝试从 JSON 中提取内容
|
| 615 |
+
var content string
|
| 616 |
+
if val, ok := rawObj["text"].(string); ok {
|
| 617 |
+
content = val
|
| 618 |
+
} else if val, ok := rawObj["content"].(string); ok {
|
| 619 |
+
content = val
|
| 620 |
+
} else if val, ok := rawObj["response"].(string); ok {
|
| 621 |
+
content = val
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
if content != "" {
|
| 625 |
+
// 构造并发送 SSE chunk
|
| 626 |
+
chunk := model.ChatCompletionChunk{
|
| 627 |
+
ID: id,
|
| 628 |
+
Object: "chat.completion.chunk",
|
| 629 |
+
Created: timestamp,
|
| 630 |
+
Model: modelID,
|
| 631 |
+
Choices: []model.StreamChoice{
|
| 632 |
+
{
|
| 633 |
+
Index: 0,
|
| 634 |
+
Delta: model.ChatMessage{
|
| 635 |
+
Content: content,
|
| 636 |
+
},
|
| 637 |
+
FinishReason: nil,
|
| 638 |
+
},
|
| 639 |
+
},
|
| 640 |
+
}
|
| 641 |
+
newBytes, _ := json.Marshal(chunk)
|
| 642 |
+
fmt.Fprintf(w, "data: %s\n\n", string(newBytes))
|
| 643 |
+
|
| 644 |
+
// 发送结束标记
|
| 645 |
+
fmt.Fprintf(w, "data: [DONE]\n\n")
|
| 646 |
+
flusher.Flush()
|
| 647 |
+
return nil
|
| 648 |
+
}
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
// 非 data 行直接通过
|
| 652 |
+
fmt.Fprint(w, line)
|
| 653 |
+
flusher.Flush()
|
| 654 |
+
continue
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
data := strings.TrimPrefix(trimmedLine, "data: ")
|
| 658 |
+
if data == "[DONE]" {
|
| 659 |
+
fmt.Fprintf(w, "data: [DONE]\n\n")
|
| 660 |
+
flusher.Flush()
|
| 661 |
+
return nil
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
// 尝试解析 JSON
|
| 665 |
+
var raw map[string]interface{}
|
| 666 |
+
if err := json.Unmarshal([]byte(data), &raw); err != nil {
|
| 667 |
+
// 解析失败,直接透传
|
| 668 |
+
fmt.Fprint(w, line)
|
| 669 |
+
flusher.Flush()
|
| 670 |
+
continue
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
// 检查是否已经是 OpenAI 格式
|
| 674 |
+
if _, hasChoices := raw["choices"]; hasChoices {
|
| 675 |
+
fmt.Fprint(w, line)
|
| 676 |
+
flusher.Flush()
|
| 677 |
+
continue
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
// 尝试转换非标准格式
|
| 681 |
+
// 假设可能有 text, content, response 等字段
|
| 682 |
+
var content string
|
| 683 |
+
if val, ok := raw["text"].(string); ok {
|
| 684 |
+
content = val
|
| 685 |
+
} else if val, ok := raw["content"].(string); ok {
|
| 686 |
+
content = val
|
| 687 |
+
} else if val, ok := raw["response"].(string); ok {
|
| 688 |
+
content = val
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
if content != "" {
|
| 692 |
+
// 构造标准 OpenAI Chunk
|
| 693 |
+
chunk := model.ChatCompletionChunk{
|
| 694 |
+
ID: id,
|
| 695 |
+
Object: "chat.completion.chunk",
|
| 696 |
+
Created: timestamp,
|
| 697 |
+
Model: modelID,
|
| 698 |
+
Choices: []model.StreamChoice{
|
| 699 |
+
{
|
| 700 |
+
Index: 0,
|
| 701 |
+
Delta: model.ChatMessage{
|
| 702 |
+
Content: content,
|
| 703 |
+
},
|
| 704 |
+
FinishReason: nil,
|
| 705 |
+
},
|
| 706 |
+
},
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
newBytes, _ := json.Marshal(chunk)
|
| 710 |
+
fmt.Fprintf(w, "data: %s\n\n", string(newBytes))
|
| 711 |
+
flusher.Flush()
|
| 712 |
+
} else {
|
| 713 |
+
// 无法识别内容,直接透传
|
| 714 |
+
fmt.Fprint(w, line)
|
| 715 |
+
flusher.Flush()
|
| 716 |
+
}
|
| 717 |
+
}
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
// ResponsesProxy 代理responses请求
|
| 721 |
+
func (s *OpenAIService) ResponsesProxy(ctx context.Context, w http.ResponseWriter, body []byte) error {
|
| 722 |
+
resp, err := s.Responses(ctx, body)
|
| 723 |
+
if err != nil {
|
| 724 |
+
return err
|
| 725 |
+
}
|
| 726 |
+
defer resp.Body.Close()
|
| 727 |
+
|
| 728 |
+
return StreamResponse(w, resp)
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
// retryWithProxy 使用代理池重试OpenAI请求
|
| 732 |
+
func (s *OpenAIService) retryWithProxy(ctx context.Context, account *model.Account, modelID, path string, body []byte) (*http.Response, error) {
|
| 733 |
+
// 获取模型配置
|
| 734 |
+
zenModel, exists := model.GetZenModel(modelID)
|
| 735 |
+
if !exists {
|
| 736 |
+
return nil, fmt.Errorf("模型配置不存在: %s", modelID)
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
proxyPool := provider.GetProxyPool()
|
| 740 |
+
if !proxyPool.HasProxies() {
|
| 741 |
+
return nil, fmt.Errorf("没有可用的代理")
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
maxRetries := 3
|
| 745 |
+
for i := 0; i < maxRetries; i++ {
|
| 746 |
+
// 获取随机代理
|
| 747 |
+
proxyURL := proxyPool.GetRandomProxy()
|
| 748 |
+
if proxyURL == "" {
|
| 749 |
+
continue
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
log.Printf("[OpenAI] 尝试代理 %s (重试 %d/%d)", proxyURL, i+1, maxRetries)
|
| 753 |
+
|
| 754 |
+
// 创建使用代理的HTTP客户端
|
| 755 |
+
proxyClient, err := provider.NewHTTPClientWithProxy(proxyURL, 0)
|
| 756 |
+
if err != nil {
|
| 757 |
+
log.Printf("[OpenAI] 创建代理客户端失败: %v", err)
|
| 758 |
+
continue
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
// 将模型参数合并到请求体中
|
| 762 |
+
modifiedBody := body
|
| 763 |
+
if zenModel.Parameters != nil {
|
| 764 |
+
var raw map[string]interface{}
|
| 765 |
+
if json.Unmarshal(modifiedBody, &raw) == nil {
|
| 766 |
+
// 添加 reasoning 配置
|
| 767 |
+
if zenModel.Parameters.Reasoning != nil && raw["reasoning"] == nil {
|
| 768 |
+
reasoningMap := map[string]interface{}{
|
| 769 |
+
"effort": zenModel.Parameters.Reasoning.Effort,
|
| 770 |
+
}
|
| 771 |
+
if zenModel.Parameters.Reasoning.Summary != "" {
|
| 772 |
+
reasoningMap["summary"] = zenModel.Parameters.Reasoning.Summary
|
| 773 |
+
}
|
| 774 |
+
raw["reasoning"] = reasoningMap
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
// 添加 text 配置
|
| 778 |
+
if zenModel.Parameters.Text != nil && raw["text"] == nil {
|
| 779 |
+
raw["text"] = map[string]interface{}{
|
| 780 |
+
"verbosity": zenModel.Parameters.Text.Verbosity,
|
| 781 |
+
}
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
// 添加 temperature 配置
|
| 785 |
+
if zenModel.Parameters.Temperature != nil && raw["temperature"] == nil {
|
| 786 |
+
raw["temperature"] = *zenModel.Parameters.Temperature
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
modifiedBody, _ = json.Marshal(raw)
|
| 790 |
+
}
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
// 特殊模型的额外处理
|
| 794 |
+
if modelID == "gpt-5-nano-2025-08-07" {
|
| 795 |
+
var raw map[string]interface{}
|
| 796 |
+
if json.Unmarshal(modifiedBody, &raw) == nil {
|
| 797 |
+
if _, ok := raw["text"]; !ok {
|
| 798 |
+
raw["text"] = map[string]string{"verbosity": "medium"}
|
| 799 |
+
}
|
| 800 |
+
if _, ok := raw["temperature"]; !ok {
|
| 801 |
+
raw["temperature"] = 1
|
| 802 |
+
}
|
| 803 |
+
raw["stream"] = true
|
| 804 |
+
if reasoning, ok := raw["reasoning"].(map[string]interface{}); ok {
|
| 805 |
+
reasoning["summary"] = "auto"
|
| 806 |
+
raw["reasoning"] = reasoning
|
| 807 |
+
} else {
|
| 808 |
+
raw["reasoning"] = map[string]interface{}{
|
| 809 |
+
"effort": "minimal",
|
| 810 |
+
"summary": "auto",
|
| 811 |
+
}
|
| 812 |
+
}
|
| 813 |
+
modifiedBody, _ = json.Marshal(raw)
|
| 814 |
+
}
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
// 创建新请求
|
| 818 |
+
reqURL := OpenAIBaseURL + path
|
| 819 |
+
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewReader(modifiedBody))
|
| 820 |
+
if err != nil {
|
| 821 |
+
log.Printf("[OpenAI] 创建请求失败: %v", err)
|
| 822 |
+
continue
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
// 设置请求头
|
| 826 |
+
SetZencoderHeaders(httpReq, account, zenModel)
|
| 827 |
+
|
| 828 |
+
// 添加模型配置的额外请求头
|
| 829 |
+
if zenModel.Parameters != nil && zenModel.Parameters.ExtraHeaders != nil {
|
| 830 |
+
for k, v := range zenModel.Parameters.ExtraHeaders {
|
| 831 |
+
httpReq.Header.Set(k, v)
|
| 832 |
+
}
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
// 强制记录代理请求体用于调试
|
| 836 |
+
log.Printf("[DEBUG] [OpenAI] 代理请求体:")
|
| 837 |
+
log.Printf("[DEBUG] [OpenAI] %s", string(modifiedBody))
|
| 838 |
+
|
| 839 |
+
// 执行请求
|
| 840 |
+
resp, err := proxyClient.Do(httpReq)
|
| 841 |
+
if err != nil {
|
| 842 |
+
log.Printf("[OpenAI] 代理请求失败: %v", err)
|
| 843 |
+
continue
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
// 检查响应状态
|
| 847 |
+
if resp.StatusCode == 429 {
|
| 848 |
+
// 仍然是429,尝试下一个代理
|
| 849 |
+
resp.Body.Close()
|
| 850 |
+
log.Printf("[OpenAI] 代理 %s 仍返回429,尝试下一个", proxyURL)
|
| 851 |
+
continue
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
if resp.StatusCode >= 400 {
|
| 855 |
+
// 其他错误,记录并尝试下一个代理
|
| 856 |
+
errBody, _ := io.ReadAll(resp.Body)
|
| 857 |
+
resp.Body.Close()
|
| 858 |
+
log.Printf("[OpenAI] 代理 %s 返回错误 %d: %s", proxyURL, resp.StatusCode, string(errBody))
|
| 859 |
+
continue
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
// 成功
|
| 863 |
+
log.Printf("[OpenAI] 代理 %s 请求成功", proxyURL)
|
| 864 |
+
return resp, nil
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
return nil, fmt.Errorf("所有代理重试均失败")
|
| 868 |
+
}
|
internal/service/pool.go
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"log"
|
| 6 |
+
"net/http"
|
| 7 |
+
"sync"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"zencoder-2api/internal/database"
|
| 11 |
+
"zencoder-2api/internal/model"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
type AccountPool struct {
|
| 15 |
+
mu sync.RWMutex
|
| 16 |
+
accounts []*model.Account
|
| 17 |
+
index uint64
|
| 18 |
+
maxErrs int
|
| 19 |
+
stopChan chan struct{}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
var pool *AccountPool
|
| 23 |
+
|
| 24 |
+
func init() {
|
| 25 |
+
pool = &AccountPool{
|
| 26 |
+
maxErrs: 3,
|
| 27 |
+
accounts: make([]*model.Account, 0),
|
| 28 |
+
stopChan: make(chan struct{}),
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// InitAccountPool 初始化账号池并启动刷新协程
|
| 33 |
+
func InitAccountPool() {
|
| 34 |
+
// 数据迁移:将旧字段状态迁移到 Status
|
| 35 |
+
pool.migrateData()
|
| 36 |
+
|
| 37 |
+
// 初始加载
|
| 38 |
+
pool.refresh()
|
| 39 |
+
// 启动后台刷新
|
| 40 |
+
go pool.refreshLoop()
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
func (p *AccountPool) migrateData() {
|
| 44 |
+
db := database.GetDB()
|
| 45 |
+
// 默认设为 normal
|
| 46 |
+
db.Model(&model.Account{}).Where("status = '' OR status IS NULL").Update("status", "normal")
|
| 47 |
+
|
| 48 |
+
// 迁移冷却状态
|
| 49 |
+
db.Model(&model.Account{}).Where("is_cooling = ?", true).Update("status", "cooling")
|
| 50 |
+
|
| 51 |
+
// 迁移错误封禁状态
|
| 52 |
+
db.Model(&model.Account{}).Where("is_active = ? AND error_count >= ?", false, p.maxErrs).Update("status", "error")
|
| 53 |
+
|
| 54 |
+
// 迁移手动禁用状态 (!Active && !Cooling && Error < Max)
|
| 55 |
+
db.Model(&model.Account{}).Where("is_active = ? AND is_cooling = ? AND error_count < ?", false, false, p.maxErrs).Update("status", "disabled")
|
| 56 |
+
|
| 57 |
+
// 迁移 category 到 status (如果 category 是 banned/error/cooling/abnormal)
|
| 58 |
+
db.Model(&model.Account{}).Where("category = ?", "banned").Update("status", "banned")
|
| 59 |
+
db.Model(&model.Account{}).Where("category = ?", "error").Update("status", "error")
|
| 60 |
+
db.Model(&model.Account{}).Where("category = ?", "cooling").Update("status", "cooling")
|
| 61 |
+
db.Model(&model.Account{}).Where("category = ?", "abnormal").Update("status", "cooling")
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
func (p *AccountPool) refreshLoop() {
|
| 65 |
+
ticker := time.NewTicker(30 * time.Second)
|
| 66 |
+
defer ticker.Stop()
|
| 67 |
+
|
| 68 |
+
for {
|
| 69 |
+
select {
|
| 70 |
+
case <-ticker.C:
|
| 71 |
+
p.refresh()
|
| 72 |
+
p.cleanupTimeoutAccounts() // 清理超时账号
|
| 73 |
+
case <-p.stopChan:
|
| 74 |
+
return
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// cleanupTimeoutAccounts 定期清理超时的账号状态
|
| 80 |
+
func (p *AccountPool) cleanupTimeoutAccounts() {
|
| 81 |
+
now := time.Now()
|
| 82 |
+
statusMu.Lock()
|
| 83 |
+
defer statusMu.Unlock()
|
| 84 |
+
|
| 85 |
+
cleanedCount := 0
|
| 86 |
+
for _, status := range accountStatuses {
|
| 87 |
+
// 清理超过60秒还在使用中的账号
|
| 88 |
+
if status.InUse && !status.InUseSince.IsZero() && now.Sub(status.InUseSince) > 60*time.Second {
|
| 89 |
+
status.InUse = false
|
| 90 |
+
status.InUseSince = time.Time{}
|
| 91 |
+
cleanedCount++
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
if cleanedCount > 0 {
|
| 96 |
+
log.Printf("[INFO] 定期清理:释放了 %d 个超时账号", cleanedCount)
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
func (p *AccountPool) refresh() {
|
| 101 |
+
// 先恢复冷却账号
|
| 102 |
+
recoverCoolingAccounts()
|
| 103 |
+
|
| 104 |
+
// 刷新即将过期的token(1小时内过期)
|
| 105 |
+
p.refreshExpiredTokens()
|
| 106 |
+
|
| 107 |
+
var dbAccounts []model.Account
|
| 108 |
+
// 只查询状态为 normal 的账号
|
| 109 |
+
result := database.GetDB().Where("status = ?", "normal").
|
| 110 |
+
Where("token_expiry > ?", time.Now()).
|
| 111 |
+
Find(&dbAccounts)
|
| 112 |
+
|
| 113 |
+
if result.Error != nil {
|
| 114 |
+
log.Printf("[Error] Failed to refresh account pool: %v", result.Error)
|
| 115 |
+
return
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
p.mu.Lock()
|
| 119 |
+
defer p.mu.Unlock()
|
| 120 |
+
|
| 121 |
+
// 重新构建缓存,但保留现有对象的指针以维持状态(如果ID匹配)
|
| 122 |
+
// 或者简单全量替换,依赖 30s 的一致性窗口
|
| 123 |
+
// 为了简化并防止并发问题,这里使用全量替换,将 DB 数据作为 Source of Truth
|
| 124 |
+
newAccounts := make([]*model.Account, len(dbAccounts))
|
| 125 |
+
for i := range dbAccounts {
|
| 126 |
+
newAccounts[i] = &dbAccounts[i]
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// 如果账号数量有显著变化,记录日志
|
| 130 |
+
oldCount := len(p.accounts)
|
| 131 |
+
newCount := len(newAccounts)
|
| 132 |
+
if oldCount != newCount {
|
| 133 |
+
log.Printf("[AccountPool] 账号池刷新:%d -> %d 个可用账号", oldCount, newCount)
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
p.accounts = newAccounts
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// refreshExpiredTokens 刷新即将过期的账号token
|
| 140 |
+
func (p *AccountPool) refreshExpiredTokens() {
|
| 141 |
+
now := time.Now()
|
| 142 |
+
threshold := now.Add(time.Hour) // 1小时内即将过期的token
|
| 143 |
+
|
| 144 |
+
var expiredAccounts []model.Account
|
| 145 |
+
// 只排除banned状态的账号,其他状态的账号仍可以刷新token
|
| 146 |
+
result := database.GetDB().Where("status != ?", "banned").
|
| 147 |
+
Where("client_id != '' AND client_secret != ''").
|
| 148 |
+
Where("token_expiry < ?", threshold).
|
| 149 |
+
Find(&expiredAccounts)
|
| 150 |
+
|
| 151 |
+
if result.Error != nil {
|
| 152 |
+
log.Printf("[AccountPool] 查询即将过期的账号失败: %v", result.Error)
|
| 153 |
+
return
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// 额外验证:再次过滤掉banned状态的账号
|
| 157 |
+
var validAccounts []model.Account
|
| 158 |
+
for _, acc := range expiredAccounts {
|
| 159 |
+
if acc.Status != "banned" {
|
| 160 |
+
validAccounts = append(validAccounts, acc)
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
expiredAccounts = validAccounts
|
| 164 |
+
|
| 165 |
+
if len(expiredAccounts) == 0 {
|
| 166 |
+
return
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
log.Printf("[AccountPool] 发现 %d 个非封禁账号的token需要刷新", len(expiredAccounts))
|
| 170 |
+
|
| 171 |
+
// 限制并发刷新数量,避免对API造成压力
|
| 172 |
+
semaphore := make(chan struct{}, 10) // 最多10个并发
|
| 173 |
+
var refreshCount int32
|
| 174 |
+
var successCount int32
|
| 175 |
+
|
| 176 |
+
// 并发刷新token
|
| 177 |
+
for i := range expiredAccounts {
|
| 178 |
+
account := &expiredAccounts[i]
|
| 179 |
+
|
| 180 |
+
go func(acc *model.Account) {
|
| 181 |
+
semaphore <- struct{}{} // 获取信号量
|
| 182 |
+
defer func() { <-semaphore }() // 释放信号量
|
| 183 |
+
|
| 184 |
+
refreshCount++
|
| 185 |
+
|
| 186 |
+
// 根据账号类型选择不同的刷新方式
|
| 187 |
+
if acc.ClientSecret == "refresh-token-login" {
|
| 188 |
+
// refresh-token-login 账号使用 refresh_token 刷新
|
| 189 |
+
if err := p.refreshRefreshTokenAccount(acc); err != nil {
|
| 190 |
+
log.Printf("[AccountPool] refresh-token账号 %s (ID:%d) token刷新失败: %v",
|
| 191 |
+
acc.ClientID, acc.ID, err)
|
| 192 |
+
} else {
|
| 193 |
+
successCount++
|
| 194 |
+
log.Printf("[AccountPool] refresh-token账号 %s (ID:%d) token刷新成功,新过期时间: %s",
|
| 195 |
+
acc.ClientID, acc.ID, acc.TokenExpiry.Format("2006-01-02 15:04:05"))
|
| 196 |
+
}
|
| 197 |
+
} else {
|
| 198 |
+
// 普通账号使用 OAuth client credentials 刷新
|
| 199 |
+
if err := p.refreshSingleAccountToken(acc); err != nil {
|
| 200 |
+
log.Printf("[AccountPool] 账号 %s (ID:%d) token刷新失败: %v",
|
| 201 |
+
acc.ClientID, acc.ID, err)
|
| 202 |
+
} else {
|
| 203 |
+
successCount++
|
| 204 |
+
log.Printf("[AccountPool] 账号 %s (ID:%d) token刷新成功,新过期时间: %s",
|
| 205 |
+
acc.ClientID, acc.ID, acc.TokenExpiry.Format("2006-01-02 15:04:05"))
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
}(account)
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// 等待所有刷新完成(最多等待30秒)
|
| 212 |
+
timeout := time.After(30 * time.Second)
|
| 213 |
+
ticker := time.NewTicker(100 * time.Millisecond)
|
| 214 |
+
defer ticker.Stop()
|
| 215 |
+
|
| 216 |
+
for {
|
| 217 |
+
select {
|
| 218 |
+
case <-timeout:
|
| 219 |
+
log.Printf("[AccountPool] Token刷新超时,已完成 %d/%d", refreshCount, len(expiredAccounts))
|
| 220 |
+
return
|
| 221 |
+
case <-ticker.C:
|
| 222 |
+
if int(refreshCount) >= len(expiredAccounts) {
|
| 223 |
+
log.Printf("[AccountPool] Token刷新完成:成功 %d/%d", successCount, len(expiredAccounts))
|
| 224 |
+
return
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// refreshSingleAccountToken 刷新单个账号的token
|
| 231 |
+
func (p *AccountPool) refreshSingleAccountToken(account *model.Account) error {
|
| 232 |
+
return refreshAccountToken(account)
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
func GetNextAccount() (*model.Account, error) {
|
| 236 |
+
return GetNextAccountForModel("")
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// AccountStatus 账号运行时状态
|
| 240 |
+
type AccountStatus struct {
|
| 241 |
+
LastUsed time.Time
|
| 242 |
+
InUse bool
|
| 243 |
+
FrozenUntil time.Time
|
| 244 |
+
InUseSince time.Time // 记录开始使用的时间
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// 账号运行时状态管理
|
| 248 |
+
var (
|
| 249 |
+
accountStatuses = make(map[uint]*AccountStatus)
|
| 250 |
+
statusMu sync.RWMutex
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
// GetNextAccountForModel 获取可用于指定模型的账号
|
| 254 |
+
// 使用内存状态管理,避免高并发下的竞态条件
|
| 255 |
+
func GetNextAccountForModel(modelID string) (*model.Account, error) {
|
| 256 |
+
pool.mu.RLock()
|
| 257 |
+
accounts := pool.accounts // 获取账号列表引用
|
| 258 |
+
pool.mu.RUnlock()
|
| 259 |
+
|
| 260 |
+
if len(accounts) == 0 {
|
| 261 |
+
return nil, ErrNoAvailableAccount
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// 获取候选账号
|
| 265 |
+
var candidates []*model.Account
|
| 266 |
+
now := time.Now()
|
| 267 |
+
statusMu.RLock()
|
| 268 |
+
for _, acc := range accounts {
|
| 269 |
+
// 检查模型权限
|
| 270 |
+
if modelID != "" && !model.CanUseModel(acc.PlanType, modelID) {
|
| 271 |
+
continue
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// 获取或初始化状态
|
| 275 |
+
status, exists := accountStatuses[acc.ID]
|
| 276 |
+
if !exists {
|
| 277 |
+
// 初始化状态
|
| 278 |
+
accountStatuses[acc.ID] = &AccountStatus{
|
| 279 |
+
LastUsed: acc.LastUsed,
|
| 280 |
+
InUse: false,
|
| 281 |
+
FrozenUntil: acc.CoolingUntil,
|
| 282 |
+
InUseSince: time.Time{},
|
| 283 |
+
}
|
| 284 |
+
status = accountStatuses[acc.ID]
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// 自动释放超时账号(超过30秒未释放的账号)
|
| 288 |
+
if status.InUse && !status.InUseSince.IsZero() && now.Sub(status.InUseSince) > 30*time.Second {
|
| 289 |
+
status.InUse = false
|
| 290 |
+
status.InUseSince = time.Time{}
|
| 291 |
+
log.Printf("[WARN] 账号 %s (ID:%d) 使用超时,已自动释放", acc.Email, acc.ID)
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// 检查是否可用(未被使用且未被冻结)
|
| 295 |
+
if !status.InUse && now.After(status.FrozenUntil) {
|
| 296 |
+
candidates = append(candidates, acc)
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
statusMu.RUnlock()
|
| 300 |
+
|
| 301 |
+
if len(candidates) == 0 {
|
| 302 |
+
// 提供详细的调试信息
|
| 303 |
+
totalAccounts := len(accounts)
|
| 304 |
+
inUseCount := 0
|
| 305 |
+
frozenCount := 0
|
| 306 |
+
noPermissionCount := 0
|
| 307 |
+
|
| 308 |
+
statusMu.RLock()
|
| 309 |
+
for _, acc := range accounts {
|
| 310 |
+
if modelID != "" && !model.CanUseModel(acc.PlanType, modelID) {
|
| 311 |
+
noPermissionCount++
|
| 312 |
+
continue
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
if status, exists := accountStatuses[acc.ID]; exists {
|
| 316 |
+
if status.InUse {
|
| 317 |
+
inUseCount++
|
| 318 |
+
} else if !now.After(status.FrozenUntil) {
|
| 319 |
+
frozenCount++
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
statusMu.RUnlock()
|
| 324 |
+
|
| 325 |
+
log.Printf("[ERROR] 无可用账号 - 总账号数: %d, 权限不足: %d, 使用中: %d, 冻结中: %d, 模型: %s",
|
| 326 |
+
totalAccounts, noPermissionCount, inUseCount, frozenCount, modelID)
|
| 327 |
+
|
| 328 |
+
return nil, ErrNoPermission
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// 选择最长时间未使用的账号
|
| 332 |
+
var selected *model.Account
|
| 333 |
+
oldestTime := time.Now()
|
| 334 |
+
|
| 335 |
+
statusMu.RLock()
|
| 336 |
+
for _, acc := range candidates {
|
| 337 |
+
status := accountStatuses[acc.ID]
|
| 338 |
+
if status == nil {
|
| 339 |
+
continue
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
// 如果账号从未使用过,优先选择
|
| 343 |
+
if status.LastUsed.IsZero() {
|
| 344 |
+
selected = acc
|
| 345 |
+
break
|
| 346 |
+
}
|
| 347 |
+
// 选择最长时间未使用的账号
|
| 348 |
+
if status.LastUsed.Before(oldestTime) {
|
| 349 |
+
oldestTime = status.LastUsed
|
| 350 |
+
selected = acc
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
statusMu.RUnlock()
|
| 354 |
+
|
| 355 |
+
// 如果没有找到合适的账号,使用轮询
|
| 356 |
+
if selected == nil {
|
| 357 |
+
selected = candidates[time.Now().UnixNano()%int64(len(candidates))]
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// 立即在内存中标记账号为使用中
|
| 361 |
+
statusMu.Lock()
|
| 362 |
+
currentTime := time.Now()
|
| 363 |
+
if status, exists := accountStatuses[selected.ID]; exists {
|
| 364 |
+
status.InUse = true
|
| 365 |
+
status.LastUsed = currentTime
|
| 366 |
+
status.InUseSince = currentTime
|
| 367 |
+
} else {
|
| 368 |
+
accountStatuses[selected.ID] = &AccountStatus{
|
| 369 |
+
LastUsed: currentTime,
|
| 370 |
+
InUse: true,
|
| 371 |
+
FrozenUntil: time.Time{},
|
| 372 |
+
InUseSince: currentTime,
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
statusMu.Unlock()
|
| 376 |
+
|
| 377 |
+
// 异步更新数据库
|
| 378 |
+
go func(acc *model.Account, usedTime time.Time) {
|
| 379 |
+
database.GetDB().Model(acc).Update("last_used", usedTime)
|
| 380 |
+
}(selected, time.Now())
|
| 381 |
+
|
| 382 |
+
return selected, nil
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// ReleaseAccount 释放账号(标记为未使用)
|
| 386 |
+
func ReleaseAccount(account *model.Account) {
|
| 387 |
+
if account == nil {
|
| 388 |
+
return
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
statusMu.Lock()
|
| 392 |
+
defer statusMu.Unlock()
|
| 393 |
+
|
| 394 |
+
if status, exists := accountStatuses[account.ID]; exists {
|
| 395 |
+
status.InUse = false
|
| 396 |
+
status.InUseSince = time.Time{} // 重置使用开始时间
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// recoverCoolingAccounts 恢复冷却期已过的账号
|
| 401 |
+
func recoverCoolingAccounts() {
|
| 402 |
+
var coolingAccounts []model.Account
|
| 403 |
+
// 查询 status = cooling 且时间已到的账号(使用 UTC 时间)
|
| 404 |
+
nowUTC := time.Now().UTC()
|
| 405 |
+
database.GetDB().Where("status = ?", "cooling").
|
| 406 |
+
Where("cooling_until < ?", nowUTC).
|
| 407 |
+
Find(&coolingAccounts)
|
| 408 |
+
|
| 409 |
+
for _, acc := range coolingAccounts {
|
| 410 |
+
acc.IsCooling = false
|
| 411 |
+
acc.IsActive = true
|
| 412 |
+
acc.Category = "normal" // 保持兼容
|
| 413 |
+
acc.Status = "normal" // 恢复状态
|
| 414 |
+
acc.BanReason = "" // 清除封禁原因
|
| 415 |
+
database.GetDB().Save(&acc)
|
| 416 |
+
log.Printf("[INFO] 账号 %s (ID:%d) 冷却期结束,已恢复 (冷却结束时间: %s UTC)",
|
| 417 |
+
acc.Email, acc.ID, acc.CoolingUntil.Format("2006-01-02 15:04:05"))
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
func MarkAccountError(account *model.Account) {
|
| 422 |
+
account.ErrorCount++
|
| 423 |
+
if account.ErrorCount >= pool.maxErrs {
|
| 424 |
+
account.IsActive = false
|
| 425 |
+
account.Status = "error" // 更新状态
|
| 426 |
+
account.Category = "error"
|
| 427 |
+
account.BanReason = "Error count exceeded limit"
|
| 428 |
+
}
|
| 429 |
+
database.GetDB().Save(account)
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// MarkAccountRateLimited 标记账号遇到 429 限流错误
|
| 433 |
+
func MarkAccountRateLimited(account *model.Account) {
|
| 434 |
+
account.RateLimitHits++
|
| 435 |
+
account.IsCooling = true
|
| 436 |
+
account.IsActive = false
|
| 437 |
+
|
| 438 |
+
// 设置冷却时间:1小时(使用UTC时间)
|
| 439 |
+
account.CoolingUntil = time.Now().UTC().Add(1 * time.Hour)
|
| 440 |
+
|
| 441 |
+
// 更新状态
|
| 442 |
+
oldStatus := account.Status
|
| 443 |
+
account.Status = "cooling"
|
| 444 |
+
account.Category = "cooling"
|
| 445 |
+
account.BanReason = "Rate limited (429)"
|
| 446 |
+
|
| 447 |
+
database.GetDB().Save(account)
|
| 448 |
+
|
| 449 |
+
log.Printf("[WARN] 账号 %s (ID:%d) 遇到 429 限流 (第 %d 次),已移至冷却分组,冷却至 %s UTC",
|
| 450 |
+
account.Email, account.ID, account.RateLimitHits, account.CoolingUntil.Format("2006-01-02 15:04:05"))
|
| 451 |
+
|
| 452 |
+
if oldStatus != "cooling" {
|
| 453 |
+
log.Printf("[INFO] 账号 %s 状态变更: %s -> cooling", account.Email, oldStatus)
|
| 454 |
+
}
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// MarkAccountRateLimitedWithResponse 根据响应头信息处理429限流错误
|
| 458 |
+
func MarkAccountRateLimitedWithResponse(account *model.Account, resp *http.Response) {
|
| 459 |
+
if resp == nil || resp.Header == nil {
|
| 460 |
+
// 如果没有响应头,使用默认处理
|
| 461 |
+
MarkAccountRateLimited(account)
|
| 462 |
+
return
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// 获取响应头中的积分信息
|
| 466 |
+
periodLimit := resp.Header.Get("Zen-Pricing-Period-Limit")
|
| 467 |
+
periodCost := resp.Header.Get("Zen-Pricing-Period-Cost")
|
| 468 |
+
periodEnd := resp.Header.Get("Zen-Pricing-Period-End")
|
| 469 |
+
|
| 470 |
+
// 检查是否为积分耗尽导致的429
|
| 471 |
+
isQuotaExhausted := false
|
| 472 |
+
if periodLimit != "" && periodCost != "" {
|
| 473 |
+
limit := parseFloat(periodLimit)
|
| 474 |
+
used := parseFloat(periodCost)
|
| 475 |
+
|
| 476 |
+
// 如果使用积分 >= 最大积分,说明积分已满
|
| 477 |
+
if limit > 0 && used >= limit {
|
| 478 |
+
isQuotaExhausted = true
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
account.RateLimitHits++
|
| 483 |
+
account.IsCooling = true
|
| 484 |
+
account.IsActive = false
|
| 485 |
+
|
| 486 |
+
oldStatus := account.Status
|
| 487 |
+
account.Status = "cooling"
|
| 488 |
+
account.Category = "cooling"
|
| 489 |
+
|
| 490 |
+
if isQuotaExhausted {
|
| 491 |
+
// 积分耗尽导致的429,根据periodEnd设置冷却时间
|
| 492 |
+
if periodEnd != "" {
|
| 493 |
+
if endTime, err := time.Parse(time.RFC3339, periodEnd); err == nil {
|
| 494 |
+
account.CoolingUntil = endTime
|
| 495 |
+
account.BanReason = "Quota exhausted (429)"
|
| 496 |
+
// 同时更新积分刷新时间
|
| 497 |
+
account.CreditRefreshTime = endTime
|
| 498 |
+
|
| 499 |
+
log.Printf("[WARN] 账号 %s (ID:%d) 积分耗尽导致429限���,冷却至积分刷新时间: %s UTC",
|
| 500 |
+
account.Email, account.ID, endTime.Format("2006-01-02 15:04:05"))
|
| 501 |
+
} else {
|
| 502 |
+
// 解析失败,使用默认冷却时间
|
| 503 |
+
account.CoolingUntil = time.Now().UTC().Add(1 * time.Hour)
|
| 504 |
+
account.BanReason = "Quota exhausted (429) - fallback cooling"
|
| 505 |
+
|
| 506 |
+
log.Printf("[WARN] 账号 %s (ID:%d) 积分耗尽但无法解析刷新时间,使用默认冷却: %s UTC",
|
| 507 |
+
account.Email, account.ID, account.CoolingUntil.Format("2006-01-02 15:04:05"))
|
| 508 |
+
}
|
| 509 |
+
} else {
|
| 510 |
+
// 没有periodEnd,使用默认冷却时间
|
| 511 |
+
account.CoolingUntil = time.Now().UTC().Add(1 * time.Hour)
|
| 512 |
+
account.BanReason = "Quota exhausted (429) - no end time"
|
| 513 |
+
|
| 514 |
+
log.Printf("[WARN] 账号 %s (ID:%d) 积分耗尽但无刷新时间信息,使用默认冷却: %s UTC",
|
| 515 |
+
account.Email, account.ID, account.CoolingUntil.Format("2006-01-02 15:04:05"))
|
| 516 |
+
}
|
| 517 |
+
} else {
|
| 518 |
+
// 常规429限流错误,使用默认冷却时间
|
| 519 |
+
account.CoolingUntil = time.Now().UTC().Add(1 * time.Hour)
|
| 520 |
+
account.BanReason = "Rate limited (429)"
|
| 521 |
+
|
| 522 |
+
log.Printf("[WARN] 账号 %s (ID:%d) 遇到常规429限流 (第 %d 次),冷却至: %s UTC",
|
| 523 |
+
account.Email, account.ID, account.RateLimitHits, account.CoolingUntil.Format("2006-01-02 15:04:05"))
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
database.GetDB().Save(account)
|
| 527 |
+
|
| 528 |
+
if oldStatus != "cooling" {
|
| 529 |
+
log.Printf("[INFO] 账号 %s 状态变更: %s -> cooling", account.Email, oldStatus)
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// MarkAccountRateLimitedShort 标记账号遇到 429 限流错误(短期冷却)
|
| 534 |
+
func MarkAccountRateLimitedShort(account *model.Account) {
|
| 535 |
+
account.RateLimitHits++
|
| 536 |
+
account.IsCooling = true
|
| 537 |
+
account.IsActive = false
|
| 538 |
+
|
| 539 |
+
// 设置短期冷却时间:5秒(使用UTC时间)
|
| 540 |
+
account.CoolingUntil = time.Now().UTC().Add(5 * time.Second)
|
| 541 |
+
|
| 542 |
+
// 更新状态
|
| 543 |
+
account.Status = "cooling"
|
| 544 |
+
account.Category = "cooling"
|
| 545 |
+
account.BanReason = "Rate limited (429) - short cooling"
|
| 546 |
+
|
| 547 |
+
database.GetDB().Save(account)
|
| 548 |
+
|
| 549 |
+
log.Printf("[INFO] 账号 %s (ID:%d) 短期冷却,冷却至 %s UTC",
|
| 550 |
+
account.Email, account.ID, account.CoolingUntil.Format("2006-01-02 15:04:05"))
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
// FreezeAccount 冻结账号指定时间(用于500错误限速)
|
| 554 |
+
func FreezeAccount(account *model.Account, duration time.Duration) {
|
| 555 |
+
if account == nil {
|
| 556 |
+
return
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
freezeUntil := time.Now().Add(duration)
|
| 560 |
+
|
| 561 |
+
// 立即在内存中更新冻结状态
|
| 562 |
+
statusMu.Lock()
|
| 563 |
+
if status, exists := accountStatuses[account.ID]; exists {
|
| 564 |
+
status.FrozenUntil = freezeUntil
|
| 565 |
+
status.InUse = false // 释放账号
|
| 566 |
+
status.InUseSince = time.Time{} // 重置使用开始时间
|
| 567 |
+
} else {
|
| 568 |
+
accountStatuses[account.ID] = &AccountStatus{
|
| 569 |
+
LastUsed: time.Now(),
|
| 570 |
+
InUse: false,
|
| 571 |
+
FrozenUntil: freezeUntil,
|
| 572 |
+
InUseSince: time.Time{},
|
| 573 |
+
}
|
| 574 |
+
}
|
| 575 |
+
statusMu.Unlock()
|
| 576 |
+
|
| 577 |
+
// 异步更新数据库
|
| 578 |
+
go func() {
|
| 579 |
+
// 设置冷却时间(使用UTC时间)
|
| 580 |
+
account.CoolingUntil = freezeUntil.UTC()
|
| 581 |
+
account.IsCooling = true
|
| 582 |
+
account.IsActive = false
|
| 583 |
+
|
| 584 |
+
// 更新状态
|
| 585 |
+
account.Status = "cooling"
|
| 586 |
+
account.Category = "cooling"
|
| 587 |
+
account.BanReason = "Rate limit tracking problem (500)"
|
| 588 |
+
|
| 589 |
+
database.GetDB().Save(account)
|
| 590 |
+
}()
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
func ResetAccountError(account *model.Account) {
|
| 594 |
+
account.ErrorCount = 0
|
| 595 |
+
database.GetDB().Save(account)
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
// 扣减积分并检查是否需要冷却
|
| 599 |
+
func UseCredit(account *model.Account, multiplier float64) {
|
| 600 |
+
account.DailyUsed += multiplier
|
| 601 |
+
account.TotalUsed += multiplier
|
| 602 |
+
account.LastUsed = time.Now() // 更新最后使用时间
|
| 603 |
+
|
| 604 |
+
limit := float64(model.PlanLimits[account.PlanType])
|
| 605 |
+
if account.DailyUsed >= limit {
|
| 606 |
+
account.IsCooling = true
|
| 607 |
+
account.Status = "cooling" // 更新状态
|
| 608 |
+
account.Category = "cooling"
|
| 609 |
+
account.BanReason = "Daily quota exceeded"
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
database.GetDB().Save(account)
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
// UpdateAccountCreditsFromResponse 根据响应头中的积分信息更新账号
|
| 616 |
+
// 如果响应头中有积分信息,使用实际值;否则使用模型倍率
|
| 617 |
+
func UpdateAccountCreditsFromResponse(account *model.Account, resp *http.Response, modelMultiplier float64) {
|
| 618 |
+
// 无论如何都要更新最后使用时间
|
| 619 |
+
account.LastUsed = time.Now()
|
| 620 |
+
|
| 621 |
+
if resp == nil || resp.Header == nil {
|
| 622 |
+
// 如果没有响应头,使用模型倍率
|
| 623 |
+
UseCredit(account, modelMultiplier)
|
| 624 |
+
return
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
// 获取响应头中的积分信息
|
| 628 |
+
periodLimit := resp.Header.Get("Zen-Pricing-Period-Limit")
|
| 629 |
+
periodCost := resp.Header.Get("Zen-Pricing-Period-Cost")
|
| 630 |
+
requestCost := resp.Header.Get("Zen-Request-Cost")
|
| 631 |
+
periodEnd := resp.Header.Get("Zen-Pricing-Period-End")
|
| 632 |
+
|
| 633 |
+
// 解析本次请求消耗的积分
|
| 634 |
+
var creditUsed float64
|
| 635 |
+
hasAPICredits := false
|
| 636 |
+
|
| 637 |
+
if requestCost != "" {
|
| 638 |
+
if val := parseFloat(requestCost); val > 0 {
|
| 639 |
+
creditUsed = val
|
| 640 |
+
hasAPICredits = true
|
| 641 |
+
}
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
// 如果有 periodCost,更新账号的总使用量(当日总计)
|
| 645 |
+
if periodCost != "" {
|
| 646 |
+
if val := parseFloat(periodCost); val >= 0 {
|
| 647 |
+
// 直接使用API返回的当日使用量
|
| 648 |
+
account.DailyUsed = val
|
| 649 |
+
hasAPICredits = true
|
| 650 |
+
}
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
// 如果有 periodLimit,可以用于验证账号计划类型
|
| 654 |
+
if periodLimit != "" {
|
| 655 |
+
if limit := parseFloat(periodLimit); limit > 0 {
|
| 656 |
+
// 可选:验证或更新账号的计划类型
|
| 657 |
+
// 这里只记录日志,不改变计划类型
|
| 658 |
+
expectedLimit := float64(model.PlanLimits[account.PlanType])
|
| 659 |
+
if limit != expectedLimit && IsDebugMode() {
|
| 660 |
+
log.Printf("[INFO] 账号 %s (ID:%d) API限额(%v)与本地限额(%v)不一致",
|
| 661 |
+
account.Email, account.ID, limit, expectedLimit)
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
// 解析冷却到期时间(UTC时间)和积分刷新时间
|
| 667 |
+
var coolingEndTime time.Time
|
| 668 |
+
if periodEnd != "" {
|
| 669 |
+
if t, err := time.Parse(time.RFC3339, periodEnd); err == nil {
|
| 670 |
+
coolingEndTime = t
|
| 671 |
+
// 同时更新积分刷新时间
|
| 672 |
+
account.CreditRefreshTime = t
|
| 673 |
+
} else {
|
| 674 |
+
// 如果解析失败,记录日志
|
| 675 |
+
log.Printf("[WARN] 无法解析 Zen-Pricing-Period-End: %s, error: %v", periodEnd, err)
|
| 676 |
+
}
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
if hasAPICredits {
|
| 680 |
+
// 使用API返回的积分值
|
| 681 |
+
if requestCost != "" && creditUsed > 0 {
|
| 682 |
+
account.TotalUsed += creditUsed
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
// 检查是否需要冷却
|
| 686 |
+
limit := float64(model.PlanLimits[account.PlanType])
|
| 687 |
+
if account.DailyUsed >= limit {
|
| 688 |
+
account.IsCooling = true
|
| 689 |
+
account.Status = "cooling"
|
| 690 |
+
account.Category = "cooling"
|
| 691 |
+
account.BanReason = "Daily quota exceeded"
|
| 692 |
+
|
| 693 |
+
// 如果有响应头中的冷却到期时间,使用它;否则使用默认时间
|
| 694 |
+
if !coolingEndTime.IsZero() {
|
| 695 |
+
account.CoolingUntil = coolingEndTime
|
| 696 |
+
log.Printf("[INFO] 账号 %s (ID:%d) 积分耗尽,进入冷却,到期时间: %s (UTC)",
|
| 697 |
+
account.Email, account.ID, coolingEndTime.Format("2006-01-02 15:04:05"))
|
| 698 |
+
} else {
|
| 699 |
+
// 默认冷却到第二天的 UTC 0点
|
| 700 |
+
now := time.Now().UTC()
|
| 701 |
+
tomorrow := now.Add(24 * time.Hour)
|
| 702 |
+
account.CoolingUntil = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, time.UTC)
|
| 703 |
+
log.Printf("[INFO] 账号 %s (ID:%d) 积分耗尽,进入冷却至: %s (UTC)",
|
| 704 |
+
account.Email, account.ID, account.CoolingUntil.Format("2006-01-02 15:04:05"))
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
database.GetDB().Save(account)
|
| 709 |
+
|
| 710 |
+
// 输出调试日志(仅在调试模式下)
|
| 711 |
+
if IsDebugMode() && (requestCost != "" || periodCost != "") {
|
| 712 |
+
log.Printf("[DEBUG] 使用API积分: 账号=%s, RequestCost=%s, PeriodCost=%s, PeriodLimit=%s, PeriodEnd=%s",
|
| 713 |
+
account.Email, requestCost, periodCost, periodLimit, periodEnd)
|
| 714 |
+
}
|
| 715 |
+
} else {
|
| 716 |
+
// 没有API积分信息,使用模型倍率(UseCredit 会自动更新 LastUsed)
|
| 717 |
+
UseCredit(account, modelMultiplier)
|
| 718 |
+
}
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
// parseFloat 安全地解析字符串为浮点数
|
| 722 |
+
func parseFloat(s string) float64 {
|
| 723 |
+
if s == "" {
|
| 724 |
+
return 0
|
| 725 |
+
}
|
| 726 |
+
var val float64
|
| 727 |
+
_, _ = fmt.Sscanf(s, "%f", &val)
|
| 728 |
+
return val
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
// refreshRefreshTokenAccount 使用 refresh_token 刷新账号 token (用于 refresh-token-login 类型的账号)
|
| 732 |
+
func (p *AccountPool) refreshRefreshTokenAccount(account *model.Account) error {
|
| 733 |
+
if account.RefreshToken == "" {
|
| 734 |
+
return fmt.Errorf("账号 %s 缺少 refresh_token", account.ClientID)
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
// 调用 zencoder auth API 刷新 token
|
| 738 |
+
tokenResp, err := RefreshAccessToken(account.RefreshToken, account.Proxy)
|
| 739 |
+
if err != nil {
|
| 740 |
+
return fmt.Errorf("调用 zencoder auth API 失败: %w", err)
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
// 计算过期时间
|
| 744 |
+
expiry := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
| 745 |
+
|
| 746 |
+
// 更新数据库
|
| 747 |
+
updates := map[string]interface{}{
|
| 748 |
+
"access_token": tokenResp.AccessToken,
|
| 749 |
+
"refresh_token": tokenResp.RefreshToken,
|
| 750 |
+
"token_expiry": expiry,
|
| 751 |
+
"updated_at": time.Now(),
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
if err := database.GetDB().Model(&model.Account{}).
|
| 755 |
+
Where("id = ?", account.ID).
|
| 756 |
+
Updates(updates).Error; err != nil {
|
| 757 |
+
return fmt.Errorf("更新数据库失败: %w", err)
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
// 更新内存中的值
|
| 761 |
+
account.AccessToken = tokenResp.AccessToken
|
| 762 |
+
account.RefreshToken = tokenResp.RefreshToken
|
| 763 |
+
account.TokenExpiry = expiry
|
| 764 |
+
|
| 765 |
+
return nil
|
| 766 |
+
}
|
internal/service/proxy_client.go
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"net/http"
|
| 7 |
+
"strings"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"zencoder-2api/internal/service/provider"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
// ProxyRequestOptions 代理请求选项
|
| 14 |
+
type ProxyRequestOptions struct {
|
| 15 |
+
UseProxy bool // 是否使用代理
|
| 16 |
+
MaxRetries int // 最大重试次数
|
| 17 |
+
RetryDelay time.Duration // 重试延迟
|
| 18 |
+
OnError func(error) bool // 错误判断函数,返回true表示需要重试
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// DefaultProxyRequestOptions 默认代理请求选项
|
| 22 |
+
func DefaultProxyRequestOptions() ProxyRequestOptions {
|
| 23 |
+
return ProxyRequestOptions{
|
| 24 |
+
UseProxy: true,
|
| 25 |
+
MaxRetries: 3,
|
| 26 |
+
RetryDelay: time.Second,
|
| 27 |
+
OnError: isNetworkError,
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// isNetworkError 判断是否为网络错误(可重试的错误)
|
| 32 |
+
func isNetworkError(err error) bool {
|
| 33 |
+
if err == nil {
|
| 34 |
+
return false
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
errStr := err.Error()
|
| 38 |
+
|
| 39 |
+
// 网络连接错误
|
| 40 |
+
if strings.Contains(errStr, "connection refused") ||
|
| 41 |
+
strings.Contains(errStr, "connection reset") ||
|
| 42 |
+
strings.Contains(errStr, "connection timed out") ||
|
| 43 |
+
strings.Contains(errStr, "timeout") ||
|
| 44 |
+
strings.Contains(errStr, "network is unreachable") ||
|
| 45 |
+
strings.Contains(errStr, "no such host") ||
|
| 46 |
+
strings.Contains(errStr, "dial tcp") ||
|
| 47 |
+
strings.Contains(errStr, "i/o timeout") ||
|
| 48 |
+
strings.Contains(errStr, "EOF") {
|
| 49 |
+
return true
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// SOCKS代理相关错误
|
| 53 |
+
if strings.Contains(errStr, "socks connect") ||
|
| 54 |
+
strings.Contains(errStr, "proxy") {
|
| 55 |
+
return true
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return false
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// DoRequestWithProxyRetry 执行带代理重试的HTTP请求
|
| 62 |
+
func DoRequestWithProxyRetry(ctx context.Context, req *http.Request, originalProxy string, options ProxyRequestOptions) (*http.Response, error) {
|
| 63 |
+
// 首先尝试使用原始代理(如果有的话)
|
| 64 |
+
client := provider.NewHTTPClient(originalProxy, 0)
|
| 65 |
+
|
| 66 |
+
resp, err := client.Do(req)
|
| 67 |
+
if err == nil {
|
| 68 |
+
// 请求成功,返回结果
|
| 69 |
+
return resp, nil
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// 检查错误是否可重试
|
| 73 |
+
if !options.OnError(err) {
|
| 74 |
+
return nil, err
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// 如果不使用代理池,直接返回错误
|
| 78 |
+
if !options.UseProxy {
|
| 79 |
+
return nil, err
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
proxyPool := provider.GetProxyPool()
|
| 83 |
+
if !proxyPool.HasProxies() {
|
| 84 |
+
return nil, fmt.Errorf("原始请求失败且无可用代理: %v", err)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
var lastErr error = err
|
| 88 |
+
|
| 89 |
+
// 使用代理池进行重试
|
| 90 |
+
for i := 0; i < options.MaxRetries; i++ {
|
| 91 |
+
// 获取下一个代理
|
| 92 |
+
proxyURL := proxyPool.GetNextProxy()
|
| 93 |
+
if proxyURL == "" {
|
| 94 |
+
break
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// 创建使用代理的HTTP客户端
|
| 98 |
+
proxyClient, clientErr := provider.NewHTTPClientWithProxy(proxyURL, 0)
|
| 99 |
+
if clientErr != nil {
|
| 100 |
+
lastErr = clientErr
|
| 101 |
+
time.Sleep(options.RetryDelay)
|
| 102 |
+
continue
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// 克隆请求(因为request body可能已经被消费)
|
| 106 |
+
newReq := req.Clone(ctx)
|
| 107 |
+
|
| 108 |
+
resp, err := proxyClient.Do(newReq)
|
| 109 |
+
if err == nil {
|
| 110 |
+
// 请求成功
|
| 111 |
+
return resp, nil
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// 检查错误是否继续重试
|
| 115 |
+
if !options.OnError(err) {
|
| 116 |
+
return nil, err
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
lastErr = err
|
| 120 |
+
time.Sleep(options.RetryDelay)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
return nil, fmt.Errorf("所有代理重试均失败,最后错误: %v", lastErr)
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// CreateHTTPClientWithFallback 创建支持代理fallback的HTTP客户端
|
| 127 |
+
func CreateHTTPClientWithFallback(originalProxy string, useProxyPool bool) *http.Client {
|
| 128 |
+
// 如果不使用代理池,使用原始逻辑
|
| 129 |
+
if !useProxyPool {
|
| 130 |
+
return provider.NewHTTPClient(originalProxy, 0)
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// 如果有原始代理,先尝试原始代理
|
| 134 |
+
if originalProxy != "" {
|
| 135 |
+
client, err := provider.NewHTTPClientWithProxy(originalProxy, 0)
|
| 136 |
+
if err == nil {
|
| 137 |
+
return client
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// 使用代理池
|
| 142 |
+
return provider.NewHTTPClientWithPoolProxy(true, 0)
|
| 143 |
+
}
|
internal/service/refresh.go
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"log"
|
| 9 |
+
"net/http"
|
| 10 |
+
"strings"
|
| 11 |
+
"time"
|
| 12 |
+
|
| 13 |
+
"zencoder-2api/internal/model"
|
| 14 |
+
"zencoder-2api/internal/database"
|
| 15 |
+
"zencoder-2api/internal/service/provider"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
// RefreshTokenRequest 请求刷新token的结构
|
| 19 |
+
type RefreshTokenRequest struct {
|
| 20 |
+
GrantType string `json:"grant_type"`
|
| 21 |
+
RefreshToken string `json:"refresh_token"`
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// RefreshTokenResponse 刷新token的响应结构
|
| 25 |
+
type RefreshTokenResponse struct {
|
| 26 |
+
TokenType string `json:"token_type"`
|
| 27 |
+
AccessToken string `json:"access_token"`
|
| 28 |
+
IDToken string `json:"id_token"`
|
| 29 |
+
RefreshToken string `json:"refresh_token"`
|
| 30 |
+
ExpiresIn int `json:"expires_in"`
|
| 31 |
+
Federated map[string]interface{} `json:"federated"`
|
| 32 |
+
|
| 33 |
+
// 这些字段可能不在响应中,但我们可以从JWT解析
|
| 34 |
+
UserID string `json:"-"`
|
| 35 |
+
Email string `json:"-"`
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// AccountLockoutError 表示账号被锁定的错误
|
| 39 |
+
type AccountLockoutError struct {
|
| 40 |
+
StatusCode int
|
| 41 |
+
Body string
|
| 42 |
+
AccountID string
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
func (e *AccountLockoutError) Error() string {
|
| 46 |
+
return fmt.Sprintf("account %s is locked out: status %d, body: %s", e.AccountID, e.StatusCode, e.Body)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// isAccountLockoutError 检查是否是账号锁定错误
|
| 50 |
+
func isAccountLockoutError(statusCode int, body string) bool {
|
| 51 |
+
if statusCode == 400 {
|
| 52 |
+
// 检查响应体中是否包含锁定信息
|
| 53 |
+
return strings.Contains(body, "User is locked out") ||
|
| 54 |
+
strings.Contains(body, "user is locked out") ||
|
| 55 |
+
strings.Contains(body, "locked out")
|
| 56 |
+
}
|
| 57 |
+
return false
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// markAccountAsBanned 将账号标记为被封禁状态
|
| 61 |
+
func markAccountAsBanned(account *model.Account, reason string) error {
|
| 62 |
+
updates := map[string]interface{}{
|
| 63 |
+
"status": "banned",
|
| 64 |
+
"is_active": false,
|
| 65 |
+
"is_cooling": false,
|
| 66 |
+
"ban_reason": reason,
|
| 67 |
+
"updated_at": time.Now(),
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if err := database.GetDB().Model(&model.Account{}).
|
| 71 |
+
Where("id = ?", account.ID).
|
| 72 |
+
Updates(updates).Error; err != nil {
|
| 73 |
+
return fmt.Errorf("failed to update account status: %w", err)
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
log.Printf("[账号管理] 账号 %s (ID:%d) 已标记为封禁状态: %s", account.ClientID, account.ID, reason)
|
| 77 |
+
return nil
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// isRefreshTokenInvalidError 检查是否是refresh token无效错误
|
| 81 |
+
func isRefreshTokenInvalidError(statusCode int, body string) bool {
|
| 82 |
+
if statusCode == 401 {
|
| 83 |
+
return strings.Contains(body, "Refresh token is not valid") ||
|
| 84 |
+
strings.Contains(body, "refresh token is not valid") ||
|
| 85 |
+
strings.Contains(body, "invalid refresh token") ||
|
| 86 |
+
strings.Contains(body, "refresh_token is invalid")
|
| 87 |
+
}
|
| 88 |
+
return false
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// markTokenRecordAsBanned 将token记录标记为封禁状态
|
| 92 |
+
func markTokenRecordAsBanned(record *model.TokenRecord, reason string) error {
|
| 93 |
+
updates := map[string]interface{}{
|
| 94 |
+
"status": "banned",
|
| 95 |
+
"is_active": false,
|
| 96 |
+
"ban_reason": reason,
|
| 97 |
+
"updated_at": time.Now(),
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if err := database.GetDB().Model(&model.TokenRecord{}).
|
| 101 |
+
Where("id = ?", record.ID).
|
| 102 |
+
Updates(updates).Error; err != nil {
|
| 103 |
+
return fmt.Errorf("failed to update token record status: %w", err)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
log.Printf("[Token管理] Token记录 #%d 已标记为封禁状态: %s", record.ID, reason)
|
| 107 |
+
return nil
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// markTokenRecordAsExpired 将token记录标记为过期状态
|
| 111 |
+
func markTokenRecordAsExpired(record *model.TokenRecord, reason string) error {
|
| 112 |
+
updates := map[string]interface{}{
|
| 113 |
+
"status": "expired",
|
| 114 |
+
"is_active": false,
|
| 115 |
+
"ban_reason": reason,
|
| 116 |
+
"updated_at": time.Now(),
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
if err := database.GetDB().Model(&model.TokenRecord{}).
|
| 120 |
+
Where("id = ?", record.ID).
|
| 121 |
+
Updates(updates).Error; err != nil {
|
| 122 |
+
return fmt.Errorf("failed to update token record status: %w", err)
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
log.Printf("[Token管理] Token记录 #%d 已标记为过期状态: %s", record.ID, reason)
|
| 126 |
+
return nil
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// disableTokenRecordsByEmail 根据邮箱禁用相关的token记录
|
| 130 |
+
func disableTokenRecordsByEmail(email string, reason string) error {
|
| 131 |
+
updates := map[string]interface{}{
|
| 132 |
+
"status": "banned",
|
| 133 |
+
"is_active": false,
|
| 134 |
+
"ban_reason": reason,
|
| 135 |
+
"updated_at": time.Now(),
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
result := database.GetDB().Model(&model.TokenRecord{}).
|
| 139 |
+
Where("email = ? AND status = ?", email, "active").
|
| 140 |
+
Updates(updates)
|
| 141 |
+
|
| 142 |
+
if result.Error != nil {
|
| 143 |
+
return fmt.Errorf("failed to disable token records: %w", result.Error)
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
if result.RowsAffected > 0 {
|
| 147 |
+
log.Printf("[Token管理] 已禁用邮箱 %s 相关的 %d 条token记录: %s", email, result.RowsAffected, reason)
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
return nil
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// RefreshAccessToken 使用 refresh_token 获取新的 access_token
|
| 154 |
+
func RefreshAccessToken(refreshToken string, proxy string) (*RefreshTokenResponse, error) {
|
| 155 |
+
url := "https://auth.zencoder.ai/api/frontegg/oauth/token"
|
| 156 |
+
|
| 157 |
+
// 打印调试日志
|
| 158 |
+
if IsDebugMode() {
|
| 159 |
+
log.Printf("[DEBUG] [RefreshToken] >>> 开始刷新Token")
|
| 160 |
+
log.Printf("[DEBUG] [RefreshToken] 请求URL: %s", url)
|
| 161 |
+
if len(refreshToken) > 20 {
|
| 162 |
+
log.Printf("[DEBUG] [RefreshToken] RefreshToken: %s...", refreshToken[:20])
|
| 163 |
+
} else {
|
| 164 |
+
log.Printf("[DEBUG] [RefreshToken] RefreshToken: %s", refreshToken)
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
reqBody := RefreshTokenRequest{
|
| 169 |
+
GrantType: "refresh_token",
|
| 170 |
+
RefreshToken: refreshToken,
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
jsonData, err := json.Marshal(reqBody)
|
| 174 |
+
if err != nil {
|
| 175 |
+
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
if IsDebugMode() {
|
| 179 |
+
log.Printf("[DEBUG] [RefreshToken] 请求Body: %s", string(jsonData))
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
| 183 |
+
if err != nil {
|
| 184 |
+
return nil, fmt.Errorf("failed to create request: %w", err)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// 设置请求头
|
| 188 |
+
req.Header.Set("Accept", "*/*")
|
| 189 |
+
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
| 190 |
+
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,ja;q=0.6")
|
| 191 |
+
req.Header.Set("Cache-Control", "no-cache")
|
| 192 |
+
req.Header.Set("Content-Type", "application/json")
|
| 193 |
+
req.Header.Set("Origin", "https://auth.zencoder.ai")
|
| 194 |
+
req.Header.Set("Pragma", "no-cache")
|
| 195 |
+
req.Header.Set("Priority", "u=1, i")
|
| 196 |
+
req.Header.Set("Sec-Ch-Ua", `"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"`)
|
| 197 |
+
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
|
| 198 |
+
req.Header.Set("Sec-Ch-Ua-Platform", `"Windows"`)
|
| 199 |
+
req.Header.Set("Sec-Fetch-Dest", "empty")
|
| 200 |
+
req.Header.Set("Sec-Fetch-Mode", "cors")
|
| 201 |
+
req.Header.Set("Sec-Fetch-Site", "same-origin")
|
| 202 |
+
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36")
|
| 203 |
+
req.Header.Set("X-Frontegg-Framework", "react@18.2.0")
|
| 204 |
+
req.Header.Set("X-Frontegg-Sdk", "@frontegg/react@7.12.14")
|
| 205 |
+
|
| 206 |
+
// 使用客户端执行请求
|
| 207 |
+
client := provider.NewHTTPClient(proxy, 30*time.Second)
|
| 208 |
+
|
| 209 |
+
if IsDebugMode() {
|
| 210 |
+
log.Printf("[DEBUG] [RefreshToken] → 发送请求...")
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
resp, err := client.Do(req)
|
| 214 |
+
if err != nil {
|
| 215 |
+
if IsDebugMode() {
|
| 216 |
+
log.Printf("[DEBUG] [RefreshToken] ✗ 请求失败: %v", err)
|
| 217 |
+
}
|
| 218 |
+
return nil, fmt.Errorf("failed to send request: %w", err)
|
| 219 |
+
}
|
| 220 |
+
defer resp.Body.Close()
|
| 221 |
+
|
| 222 |
+
if IsDebugMode() {
|
| 223 |
+
log.Printf("[DEBUG] [RefreshToken] ← 收到响应: status=%d", resp.StatusCode)
|
| 224 |
+
// 输出响应头
|
| 225 |
+
log.Printf("[DEBUG] [RefreshToken] 响应头:")
|
| 226 |
+
for k, v := range resp.Header {
|
| 227 |
+
log.Printf("[DEBUG] [RefreshToken] %s: %v", k, v)
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
body, err := io.ReadAll(resp.Body)
|
| 232 |
+
if err != nil {
|
| 233 |
+
return nil, fmt.Errorf("failed to read response: %w", err)
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
if IsDebugMode() {
|
| 237 |
+
log.Printf("[DEBUG] [RefreshToken] 响应Body: %s", string(body))
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
if resp.StatusCode != http.StatusOK {
|
| 241 |
+
if IsDebugMode() {
|
| 242 |
+
log.Printf("[DEBUG] [RefreshToken] ✗ API错误: %d - %s", resp.StatusCode, string(body))
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// 检查是否是账号锁定错误
|
| 246 |
+
if isAccountLockoutError(resp.StatusCode, string(body)) {
|
| 247 |
+
return nil, &AccountLockoutError{
|
| 248 |
+
StatusCode: resp.StatusCode,
|
| 249 |
+
Body: string(body),
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// 检查是否是refresh token无效错误
|
| 254 |
+
if isRefreshTokenInvalidError(resp.StatusCode, string(body)) {
|
| 255 |
+
return nil, fmt.Errorf("refresh token expired or invalid: status %d, body: %s", resp.StatusCode, string(body))
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
return nil, fmt.Errorf("failed to refresh token: status %d, body: %s", resp.StatusCode, string(body))
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
var tokenResp RefreshTokenResponse
|
| 262 |
+
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
| 263 |
+
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
// 如果响应中没有UserID,尝试从access_token中解析
|
| 267 |
+
if tokenResp.UserID == "" && tokenResp.AccessToken != "" {
|
| 268 |
+
if payload, err := ParseJWT(tokenResp.AccessToken); err == nil {
|
| 269 |
+
// 优先使用 Email,没有则使用 Subject
|
| 270 |
+
if payload.Email != "" {
|
| 271 |
+
tokenResp.UserID = payload.Email
|
| 272 |
+
tokenResp.Email = payload.Email
|
| 273 |
+
} else if payload.Subject != "" {
|
| 274 |
+
tokenResp.UserID = payload.Subject
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
if IsDebugMode() {
|
| 278 |
+
log.Printf("[DEBUG] [RefreshToken] 从JWT解析UserID: %s", tokenResp.UserID)
|
| 279 |
+
log.Printf("[DEBUG] [RefreshToken] JWT Payload - Email: %s, Subject: %s",
|
| 280 |
+
payload.Email, payload.Subject)
|
| 281 |
+
}
|
| 282 |
+
} else {
|
| 283 |
+
if IsDebugMode() {
|
| 284 |
+
log.Printf("[DEBUG] [RefreshToken] 解析JWT失败: %v", err)
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
if IsDebugMode() {
|
| 290 |
+
accessTokenPreview := tokenResp.AccessToken
|
| 291 |
+
if len(accessTokenPreview) > 20 {
|
| 292 |
+
accessTokenPreview = accessTokenPreview[:20]
|
| 293 |
+
}
|
| 294 |
+
log.Printf("[DEBUG] [RefreshToken] <<< 刷新成功: UserID=%s, AccessToken=%s..., ExpiresIn=%d",
|
| 295 |
+
tokenResp.UserID,
|
| 296 |
+
accessTokenPreview,
|
| 297 |
+
tokenResp.ExpiresIn)
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
return &tokenResp, nil
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
// min 辅助函数
|
| 304 |
+
func min(a, b int) int {
|
| 305 |
+
if a < b {
|
| 306 |
+
return a
|
| 307 |
+
}
|
| 308 |
+
return b
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
// UpdateAccountToken 更新账号的 token
|
| 312 |
+
func UpdateAccountToken(account *model.Account) error {
|
| 313 |
+
if account.RefreshToken == "" {
|
| 314 |
+
return fmt.Errorf("account %s has no refresh token", account.ClientID)
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// 调用刷新接口
|
| 318 |
+
tokenResp, err := RefreshAccessToken(account.RefreshToken, account.Proxy)
|
| 319 |
+
if err != nil {
|
| 320 |
+
// 检查是否是账号锁定错误
|
| 321 |
+
if lockoutErr, ok := err.(*AccountLockoutError); ok {
|
| 322 |
+
// 将账号标记为封禁状态
|
| 323 |
+
if markErr := markAccountAsBanned(account, "用户被锁定: "+lockoutErr.Body); markErr != nil {
|
| 324 |
+
log.Printf("[账号管理] 标记账号封禁状态失败: %v", markErr)
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
return fmt.Errorf("failed to refresh token for account %s: %w", account.ClientID, err)
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// 计算过期时间
|
| 331 |
+
expiry := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
| 332 |
+
|
| 333 |
+
// 更新数据库
|
| 334 |
+
updates := map[string]interface{}{
|
| 335 |
+
"access_token": tokenResp.AccessToken,
|
| 336 |
+
"refresh_token": tokenResp.RefreshToken, // 更新新的 refresh_token
|
| 337 |
+
"token_expiry": expiry,
|
| 338 |
+
"updated_at": time.Now(),
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
if err := database.DB.Model(&model.Account{}).
|
| 342 |
+
Where("id = ?", account.ID).
|
| 343 |
+
Updates(updates).Error; err != nil {
|
| 344 |
+
return fmt.Errorf("failed to update account token: %w", err)
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
// 更新内存中的值
|
| 348 |
+
account.AccessToken = tokenResp.AccessToken
|
| 349 |
+
account.RefreshToken = tokenResp.RefreshToken
|
| 350 |
+
account.TokenExpiry = expiry
|
| 351 |
+
|
| 352 |
+
debugLogf("✅ Refreshed token for account %s, expires at %s", account.ClientID, expiry.Format(time.RFC3339))
|
| 353 |
+
|
| 354 |
+
return nil
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// UpdateTokenRecordToken 更新 TokenRecord 的 token
|
| 358 |
+
func UpdateTokenRecordToken(record *model.TokenRecord) error {
|
| 359 |
+
if record.RefreshToken == "" {
|
| 360 |
+
return fmt.Errorf("token record %d has no refresh token", record.ID)
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// 调用刷新接口
|
| 364 |
+
tokenResp, err := RefreshAccessToken(record.RefreshToken, "")
|
| 365 |
+
if err != nil {
|
| 366 |
+
// 检查是否是账号锁定错误
|
| 367 |
+
if lockoutErr, ok := err.(*AccountLockoutError); ok {
|
| 368 |
+
// 将token记录标记为封禁状态
|
| 369 |
+
if markErr := markTokenRecordAsBanned(record, "账号被锁定: "+lockoutErr.Body); markErr != nil {
|
| 370 |
+
log.Printf("[Token管理] 标记token记录封禁状态失败: %v", markErr)
|
| 371 |
+
}
|
| 372 |
+
// 根据邮箱禁用相关的token记录
|
| 373 |
+
if record.Email != "" {
|
| 374 |
+
if disableErr := disableTokenRecordsByEmail(record.Email, "关联账号被锁定"); disableErr != nil {
|
| 375 |
+
log.Printf("[Token管理] 禁用相关token记录失败: %v", disableErr)
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
return fmt.Errorf("token record %d account locked out: %w", record.ID, err)
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// 检查是否是refresh token过期错误
|
| 382 |
+
if strings.Contains(err.Error(), "refresh token expired or invalid") {
|
| 383 |
+
// 将token记录标记为过期状态
|
| 384 |
+
if markErr := markTokenRecordAsExpired(record, "Refresh token过期或无效"); markErr != nil {
|
| 385 |
+
log.Printf("[Token管理] 标记token记录过期状态失败: %v", markErr)
|
| 386 |
+
}
|
| 387 |
+
return fmt.Errorf("token record %d refresh token expired: %w", record.ID, err)
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
return fmt.Errorf("failed to refresh token for record %d: %w", record.ID, err)
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
// 计算过期时间
|
| 394 |
+
expiry := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
| 395 |
+
|
| 396 |
+
// 更新数据库
|
| 397 |
+
updates := map[string]interface{}{
|
| 398 |
+
"token": tokenResp.AccessToken,
|
| 399 |
+
"refresh_token": tokenResp.RefreshToken, // 更新新的 refresh_token
|
| 400 |
+
"token_expiry": expiry,
|
| 401 |
+
"status": "active", // 刷新成功时重新激活
|
| 402 |
+
"updated_at": time.Now(),
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
if err := database.DB.Model(&model.TokenRecord{}).
|
| 406 |
+
Where("id = ?", record.ID).
|
| 407 |
+
Updates(updates).Error; err != nil {
|
| 408 |
+
return fmt.Errorf("failed to update token record: %w", err)
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
// 更新内存中的值
|
| 412 |
+
record.Token = tokenResp.AccessToken
|
| 413 |
+
record.RefreshToken = tokenResp.RefreshToken
|
| 414 |
+
record.TokenExpiry = expiry
|
| 415 |
+
record.Status = "active"
|
| 416 |
+
|
| 417 |
+
debugLogf("✅ Refreshed token for record %d, expires at %s", record.ID, expiry.Format(time.RFC3339))
|
| 418 |
+
|
| 419 |
+
return nil
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
// CheckAndRefreshToken 检查并刷新即将过期的 token
|
| 423 |
+
func CheckAndRefreshToken(account *model.Account) error {
|
| 424 |
+
// 如果没有 RefreshToken,跳过
|
| 425 |
+
if account.RefreshToken == "" {
|
| 426 |
+
return nil
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
// 如果 token 在一小时内过期,则刷新
|
| 430 |
+
if time.Until(account.TokenExpiry) < time.Hour {
|
| 431 |
+
debugLogf("⚠️ Token for account %s expires in %v, refreshing...",
|
| 432 |
+
account.ClientID, time.Until(account.TokenExpiry))
|
| 433 |
+
return UpdateAccountToken(account)
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
return nil
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// CheckAndRefreshTokenRecord 检查并刷新即将过期的 TokenRecord
|
| 440 |
+
func CheckAndRefreshTokenRecord(record *model.TokenRecord) error {
|
| 441 |
+
// 如果没有 RefreshToken,跳过
|
| 442 |
+
if record.RefreshToken == "" {
|
| 443 |
+
return nil
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
// 如果 token 在一小时内过期,则刷新
|
| 447 |
+
if time.Until(record.TokenExpiry) < time.Hour {
|
| 448 |
+
debugLogf("⚠️ Token for record %d expires in %v, refreshing...",
|
| 449 |
+
record.ID, time.Until(record.TokenExpiry))
|
| 450 |
+
return UpdateTokenRecordToken(record)
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
return nil
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
// StartTokenRefreshScheduler 启动定时刷新 token 的调度器
|
| 457 |
+
func StartTokenRefreshScheduler() {
|
| 458 |
+
go func() {
|
| 459 |
+
// 立即执行一次
|
| 460 |
+
refreshExpiredTokens()
|
| 461 |
+
|
| 462 |
+
// 然后每分钟检查一次
|
| 463 |
+
ticker := time.NewTicker(1 * time.Minute)
|
| 464 |
+
defer ticker.Stop()
|
| 465 |
+
|
| 466 |
+
for range ticker.C {
|
| 467 |
+
refreshExpiredTokens()
|
| 468 |
+
}
|
| 469 |
+
}()
|
| 470 |
+
|
| 471 |
+
log.Printf("🔄 Token refresh scheduler started - checking every minute")
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
// refreshExpiredTokens 刷新即将过期的 tokens
|
| 475 |
+
func refreshExpiredTokens() {
|
| 476 |
+
now := time.Now()
|
| 477 |
+
threshold := now.Add(time.Hour) // 1小时内即将过期的token
|
| 478 |
+
|
| 479 |
+
// 查询所有即将过期的账号(排除banned状态)
|
| 480 |
+
var accounts []model.Account
|
| 481 |
+
if err := database.DB.Where("token_expiry < ?", threshold).
|
| 482 |
+
Where("status != ?", "banned").
|
| 483 |
+
Find(&accounts).Error; err == nil {
|
| 484 |
+
|
| 485 |
+
for _, account := range accounts {
|
| 486 |
+
// 根据账号类型选择不同的刷新方式
|
| 487 |
+
if account.ClientSecret == "refresh-token-login" {
|
| 488 |
+
// refresh-token-login 账号使用 refresh_token 刷新
|
| 489 |
+
if account.RefreshToken != "" {
|
| 490 |
+
if err := UpdateAccountToken(&account); err != nil {
|
| 491 |
+
log.Printf("[Token刷新] ❌ refresh-token账号 %s 刷新失败: %v", account.ClientID, err)
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
} else {
|
| 495 |
+
// 普通账号使用 OAuth client credentials 刷新
|
| 496 |
+
if account.ClientID != "" && account.ClientSecret != "" {
|
| 497 |
+
if err := refreshAccountToken(&account); err != nil {
|
| 498 |
+
log.Printf("[Token刷新] ❌ 账号 %s OAuth刷新失败: %v", account.ClientID, err)
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
// 刷新 TokenRecord 的 tokens - 只排除banned状态的记录
|
| 506 |
+
var records []model.TokenRecord
|
| 507 |
+
if err := database.DB.Where("refresh_token != '' AND token_expiry < ?", threshold).
|
| 508 |
+
Where("status != ?", "banned").
|
| 509 |
+
Find(&records).Error; err == nil {
|
| 510 |
+
|
| 511 |
+
for _, record := range records {
|
| 512 |
+
if err := UpdateTokenRecordToken(&record); err != nil {
|
| 513 |
+
log.Printf("[Token刷新] ❌ 生成token #%d 刷新失败: %v", record.ID, err)
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
}
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
// debugLogf 简单的调试日志函数
|
| 520 |
+
func debugLogf(format string, args ...interface{}) {
|
| 521 |
+
if IsDebugMode() {
|
| 522 |
+
log.Printf("[DEBUG] "+format, args...)
|
| 523 |
+
}
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
// RefreshTokenAndAccounts 刷新token记录并异步刷新相同邮箱的账号
|
| 527 |
+
func RefreshTokenAndAccounts(tokenRecordID uint) error {
|
| 528 |
+
// 获取token记录
|
| 529 |
+
var record model.TokenRecord
|
| 530 |
+
if err := database.GetDB().First(&record, tokenRecordID).Error; err != nil {
|
| 531 |
+
return fmt.Errorf("获取token记录失败: %w", err)
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
if record.RefreshToken == "" {
|
| 535 |
+
return fmt.Errorf("token记录没有refresh_token")
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
// 1. 刷新token记录的token
|
| 539 |
+
log.Printf("[Token刷新] 开始刷新token记录 #%d", tokenRecordID)
|
| 540 |
+
|
| 541 |
+
// 调用刷新接口
|
| 542 |
+
tokenResp, err := RefreshAccessToken(record.RefreshToken, "")
|
| 543 |
+
if err != nil {
|
| 544 |
+
// 检查是否是账号锁定错误
|
| 545 |
+
if lockoutErr, ok := err.(*AccountLockoutError); ok {
|
| 546 |
+
// 将token记录标记为封禁状态
|
| 547 |
+
if markErr := markTokenRecordAsBanned(&record, "账号被锁定: "+lockoutErr.Body); markErr != nil {
|
| 548 |
+
log.Printf("[Token管理] 标记token记录封禁状态失败: %v", markErr)
|
| 549 |
+
}
|
| 550 |
+
// 根据邮箱禁用相关的token记录
|
| 551 |
+
if record.Email != "" {
|
| 552 |
+
if disableErr := disableTokenRecordsByEmail(record.Email, "关联账号被锁定"); disableErr != nil {
|
| 553 |
+
log.Printf("[Token管理] 禁用相关token记录失败: %v", disableErr)
|
| 554 |
+
}
|
| 555 |
+
}
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
// 检查是否是refresh token过期错误
|
| 559 |
+
if strings.Contains(err.Error(), "refresh token expired or invalid") {
|
| 560 |
+
// 将token记录标记为过期状态
|
| 561 |
+
if markErr := markTokenRecordAsExpired(&record, "Refresh token过期或无效"); markErr != nil {
|
| 562 |
+
log.Printf("[Token管理] 标记token记录过期状态失败: %v", markErr)
|
| 563 |
+
}
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
return fmt.Errorf("刷新token失败: %w", err)
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
// 计算过期时间
|
| 570 |
+
expiry := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
| 571 |
+
|
| 572 |
+
// 更新数据库
|
| 573 |
+
updates := map[string]interface{}{
|
| 574 |
+
"token": tokenResp.AccessToken,
|
| 575 |
+
"refresh_token": tokenResp.RefreshToken,
|
| 576 |
+
"token_expiry": expiry,
|
| 577 |
+
"status": "active", // 刷新成功时重置为活跃状态
|
| 578 |
+
"ban_reason": "", // 清除封禁原因
|
| 579 |
+
"updated_at": time.Now(),
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
if err := database.GetDB().Model(&model.TokenRecord{}).
|
| 583 |
+
Where("id = ?", tokenRecordID).
|
| 584 |
+
Updates(updates).Error; err != nil {
|
| 585 |
+
return fmt.Errorf("更新token记录失败: %w", err)
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
// 2. 解析新token获取邮箱
|
| 589 |
+
email := ""
|
| 590 |
+
if payload, err := ParseJWT(tokenResp.AccessToken); err == nil {
|
| 591 |
+
email = payload.Email
|
| 592 |
+
log.Printf("[Token刷新] 解析到邮箱: %s", email)
|
| 593 |
+
} else {
|
| 594 |
+
log.Printf("[Token刷新] 无法解析JWT获取邮箱: %v", err)
|
| 595 |
+
return nil // 不影响token记录的刷新
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
if email == "" {
|
| 599 |
+
log.Printf("[Token刷新] 邮箱为空,跳过账号刷新")
|
| 600 |
+
return nil
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
// 3. 异步刷新相同邮箱的账号
|
| 604 |
+
go refreshAccountsByEmail(email)
|
| 605 |
+
|
| 606 |
+
return nil
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// refreshAccountsByEmail 刷新指定邮箱的所有账号
|
| 610 |
+
func refreshAccountsByEmail(email string) {
|
| 611 |
+
log.Printf("[账号刷新] 开始刷新邮箱 %s 的所有账号", email)
|
| 612 |
+
|
| 613 |
+
// 查询所有相同邮箱的账号
|
| 614 |
+
var accounts []model.Account
|
| 615 |
+
if err := database.GetDB().Where("email = ?", email).Find(&accounts).Error; err != nil {
|
| 616 |
+
log.Printf("[账号刷新] 查询邮箱 %s 的账号失败: %v", email, err)
|
| 617 |
+
return
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
if len(accounts) == 0 {
|
| 621 |
+
log.Printf("[账号刷新] 没有找到邮箱 %s 的账号", email)
|
| 622 |
+
return
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
log.Printf("[账号刷新] 找到 %d 个账号需要刷新", len(accounts))
|
| 626 |
+
|
| 627 |
+
// 逐个刷新账号
|
| 628 |
+
successCount := 0
|
| 629 |
+
failCount := 0
|
| 630 |
+
|
| 631 |
+
for _, account := range accounts {
|
| 632 |
+
// 如果账号没有client_id和client_secret,跳过
|
| 633 |
+
if account.ClientID == "" || account.ClientSecret == "" {
|
| 634 |
+
log.Printf("[账号刷新] 账号 ID:%d 缺少client_id或client_secret,跳过", account.ID)
|
| 635 |
+
continue
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
log.Printf("[账号刷新] 正在刷新账号 ID:%d (ClientID: %s)", account.ID, account.ClientID)
|
| 639 |
+
|
| 640 |
+
// 使用OAuth方式刷新token
|
| 641 |
+
if err := refreshAccountToken(&account); err != nil {
|
| 642 |
+
log.Printf("[账号刷新] 账号 ID:%d 刷新失败: %v", account.ID, err)
|
| 643 |
+
failCount++
|
| 644 |
+
} else {
|
| 645 |
+
log.Printf("[账号刷新] 账号 ID:%d 刷新成功", account.ID)
|
| 646 |
+
successCount++
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
// 添加短暂延迟,避免请求过快
|
| 650 |
+
time.Sleep(100 * time.Millisecond)
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
log.Printf("[账号刷新] 邮箱 %s 的账号刷新完成 - 成功: %d, 失败: %d",
|
| 654 |
+
email, successCount, failCount)
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
// RefreshAccountToken 使用client credentials刷新账号token(导出函数)
|
| 658 |
+
func RefreshAccountToken(account *model.Account) error {
|
| 659 |
+
return refreshAccountToken(account)
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
// refreshAccountToken 使用client credentials刷新账号token
|
| 663 |
+
func refreshAccountToken(account *model.Account) error {
|
| 664 |
+
// 构建OAuth token请求
|
| 665 |
+
url := "https://fe.zencoder.ai/oauth/token"
|
| 666 |
+
|
| 667 |
+
reqBody := map[string]string{
|
| 668 |
+
"grant_type": "client_credentials",
|
| 669 |
+
"client_id": account.ClientID,
|
| 670 |
+
"client_secret": account.ClientSecret,
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
jsonData, err := json.Marshal(reqBody)
|
| 674 |
+
if err != nil {
|
| 675 |
+
return fmt.Errorf("序列化请求失败: %w", err)
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
| 679 |
+
if err != nil {
|
| 680 |
+
return fmt.Errorf("创建请求失败: %w", err)
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// 设置请求头
|
| 684 |
+
req.Header.Set("Content-Type", "application/json")
|
| 685 |
+
req.Header.Set("Accept", "application/json")
|
| 686 |
+
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
| 687 |
+
|
| 688 |
+
// 使用代理(如果有)
|
| 689 |
+
client := provider.NewHTTPClient(account.Proxy, 30*time.Second)
|
| 690 |
+
|
| 691 |
+
resp, err := client.Do(req)
|
| 692 |
+
if err != nil {
|
| 693 |
+
return fmt.Errorf("发送请求失败: %w", err)
|
| 694 |
+
}
|
| 695 |
+
defer resp.Body.Close()
|
| 696 |
+
|
| 697 |
+
body, err := io.ReadAll(resp.Body)
|
| 698 |
+
if err != nil {
|
| 699 |
+
return fmt.Errorf("读取响应失败: %w", err)
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
if resp.StatusCode != http.StatusOK {
|
| 703 |
+
// 检查是否是账号锁定错误
|
| 704 |
+
if isAccountLockoutError(resp.StatusCode, string(body)) {
|
| 705 |
+
// 将账号标记为封禁状态
|
| 706 |
+
if markErr := markAccountAsBanned(account, "OAuth认证失败-用户被锁定: "+string(body)); markErr != nil {
|
| 707 |
+
log.Printf("[账号管理] 标记账号封禁状态失败: %v", markErr)
|
| 708 |
+
}
|
| 709 |
+
return &AccountLockoutError{
|
| 710 |
+
StatusCode: resp.StatusCode,
|
| 711 |
+
Body: string(body),
|
| 712 |
+
AccountID: account.ClientID,
|
| 713 |
+
}
|
| 714 |
+
}
|
| 715 |
+
return fmt.Errorf("API返回错误: %d - %s", resp.StatusCode, string(body))
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
// 解析响应
|
| 719 |
+
var tokenResp struct {
|
| 720 |
+
AccessToken string `json:"access_token"`
|
| 721 |
+
TokenType string `json:"token_type"`
|
| 722 |
+
ExpiresIn int `json:"expires_in"`
|
| 723 |
+
RefreshToken string `json:"refresh_token"`
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
| 727 |
+
return fmt.Errorf("解析响应失败: %w", err)
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
// 计算过期时间
|
| 731 |
+
expiry := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
| 732 |
+
|
| 733 |
+
// 解析token获取更多信息
|
| 734 |
+
planType := account.PlanType // 保留原有计划类型
|
| 735 |
+
dailyUsed := account.DailyUsed // 保留原有使用量
|
| 736 |
+
totalUsed := account.TotalUsed // 保留原有总使用量
|
| 737 |
+
|
| 738 |
+
if payload, err := ParseJWT(tokenResp.AccessToken); err == nil {
|
| 739 |
+
// 更新计划类型(如果有)
|
| 740 |
+
if payload.CustomClaims.Plan != "" {
|
| 741 |
+
planType = model.PlanType(payload.CustomClaims.Plan)
|
| 742 |
+
}
|
| 743 |
+
// 验证邮箱
|
| 744 |
+
if account.Email != "" && payload.Email != account.Email {
|
| 745 |
+
log.Printf("[账号刷新] 警告: 账号 ID:%d 邮箱不匹配 (期望: %s, 实际: %s)",
|
| 746 |
+
account.ID, account.Email, payload.Email)
|
| 747 |
+
}
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
// 更新数据库
|
| 751 |
+
updates := map[string]interface{}{
|
| 752 |
+
"access_token": tokenResp.AccessToken,
|
| 753 |
+
"refresh_token": tokenResp.RefreshToken,
|
| 754 |
+
"token_expiry": expiry,
|
| 755 |
+
"plan_type": planType,
|
| 756 |
+
"daily_used": dailyUsed, // 保持原有使用量
|
| 757 |
+
"total_used": totalUsed, // 保持原有总使用量
|
| 758 |
+
"updated_at": time.Now(),
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
return database.GetDB().Model(&model.Account{}).
|
| 762 |
+
Where("id = ?", account.ID).
|
| 763 |
+
Updates(updates).Error
|
| 764 |
+
}
|
internal/service/request.go
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
// 注意:ReplaceModelInBody 函数已被删除,不再进行模型重定向/替换
|
internal/service/scheduler.go
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"time"
|
| 6 |
+
|
| 7 |
+
"zencoder-2api/internal/database"
|
| 8 |
+
"zencoder-2api/internal/model"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func StartCreditResetScheduler() {
|
| 12 |
+
go func() {
|
| 13 |
+
for {
|
| 14 |
+
now := time.Now()
|
| 15 |
+
next := time.Date(now.Year(), now.Month(), now.Day(), 9, 9, 0, 0, now.Location())
|
| 16 |
+
if now.After(next) {
|
| 17 |
+
next = next.Add(24 * time.Hour)
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
time.Sleep(time.Until(next))
|
| 21 |
+
ResetAllCredits()
|
| 22 |
+
}
|
| 23 |
+
}()
|
| 24 |
+
log.Println("Credit reset scheduler started (daily at 09:09)")
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func ResetAllCredits() {
|
| 28 |
+
today := time.Now().Format("2006-01-02")
|
| 29 |
+
|
| 30 |
+
database.GetDB().Model(&model.Account{}).
|
| 31 |
+
Where("last_reset_date != ? OR last_reset_date IS NULL", today).
|
| 32 |
+
Updates(map[string]interface{}{
|
| 33 |
+
"daily_used": 0,
|
| 34 |
+
"is_cooling": false,
|
| 35 |
+
"last_reset_date": today,
|
| 36 |
+
})
|
| 37 |
+
|
| 38 |
+
log.Printf("Credits reset completed at %s", time.Now().Format("2006-01-02 15:04:05"))
|
| 39 |
+
}
|
internal/service/stream.go
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"io"
|
| 6 |
+
"net/http"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
// StreamResponse 流式传输响应到客户端
|
| 10 |
+
func StreamResponse(w http.ResponseWriter, resp *http.Response) error {
|
| 11 |
+
// 复制响应头
|
| 12 |
+
for k, v := range resp.Header {
|
| 13 |
+
for _, vv := range v {
|
| 14 |
+
w.Header().Add(k, vv)
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
w.WriteHeader(resp.StatusCode)
|
| 18 |
+
|
| 19 |
+
// 获取Flusher接口
|
| 20 |
+
flusher, ok := w.(http.Flusher)
|
| 21 |
+
if !ok {
|
| 22 |
+
// 如果不支持Flusher,直接复制
|
| 23 |
+
_, err := io.Copy(w, resp.Body)
|
| 24 |
+
return err
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// 使用bufio读取并逐行刷新
|
| 28 |
+
reader := bufio.NewReader(resp.Body)
|
| 29 |
+
for {
|
| 30 |
+
line, err := reader.ReadBytes('\n')
|
| 31 |
+
if len(line) > 0 {
|
| 32 |
+
_, writeErr := w.Write(line)
|
| 33 |
+
if writeErr != nil {
|
| 34 |
+
return writeErr
|
| 35 |
+
}
|
| 36 |
+
flusher.Flush()
|
| 37 |
+
}
|
| 38 |
+
if err != nil {
|
| 39 |
+
if err == io.EOF {
|
| 40 |
+
return nil
|
| 41 |
+
}
|
| 42 |
+
return err
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// CopyResponse 普通响应复制
|
| 48 |
+
func CopyResponse(w http.ResponseWriter, resp *http.Response) error {
|
| 49 |
+
for k, v := range resp.Header {
|
| 50 |
+
for _, vv := range v {
|
| 51 |
+
w.Header().Add(k, vv)
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
w.WriteHeader(resp.StatusCode)
|
| 55 |
+
_, err := io.Copy(w, resp.Body)
|
| 56 |
+
return err
|
| 57 |
+
}
|
internal/service/token.go
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"io"
|
| 7 |
+
"log"
|
| 8 |
+
"net/http"
|
| 9 |
+
"net/url"
|
| 10 |
+
"strings"
|
| 11 |
+
"time"
|
| 12 |
+
|
| 13 |
+
"zencoder-2api/internal/database"
|
| 14 |
+
"zencoder-2api/internal/model"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
type TokenResponse struct {
|
| 18 |
+
AccessToken string `json:"access_token"`
|
| 19 |
+
TokenType string `json:"token_type"`
|
| 20 |
+
ExpiresIn int `json:"expires_in"`
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const (
|
| 24 |
+
ZencoderTokenURL = "https://fe.zencoder.ai/oauth/token"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
func GetToken(account *model.Account) (string, error) {
|
| 28 |
+
if account.AccessToken != "" && time.Now().Before(account.TokenExpiry) {
|
| 29 |
+
return account.AccessToken, nil
|
| 30 |
+
}
|
| 31 |
+
return RefreshToken(account)
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func RefreshToken(account *model.Account) (string, error) {
|
| 35 |
+
// 每次创建新的 HTTP 客户端,禁用连接复用
|
| 36 |
+
transport := &http.Transport{
|
| 37 |
+
DisableKeepAlives: true, // 禁用 Keep-Alive
|
| 38 |
+
DisableCompression: false,
|
| 39 |
+
MaxIdleConns: 0, // 不保持空闲连接
|
| 40 |
+
MaxIdleConnsPerHost: 0,
|
| 41 |
+
IdleConnTimeout: 0,
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
if account.Proxy != "" {
|
| 45 |
+
proxyURL, err := url.Parse(account.Proxy)
|
| 46 |
+
if err == nil {
|
| 47 |
+
transport.Proxy = http.ProxyURL(proxyURL)
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
client := &http.Client{
|
| 52 |
+
Transport: transport,
|
| 53 |
+
Timeout: 30 * time.Second,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
data := url.Values{}
|
| 57 |
+
data.Set("grant_type", "client_credentials")
|
| 58 |
+
data.Set("client_id", account.ClientID)
|
| 59 |
+
data.Set("client_secret", account.ClientSecret)
|
| 60 |
+
|
| 61 |
+
req, err := http.NewRequest("POST", ZencoderTokenURL, strings.NewReader(data.Encode()))
|
| 62 |
+
if err != nil {
|
| 63 |
+
return "", err
|
| 64 |
+
}
|
| 65 |
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
| 66 |
+
req.Header.Set("Connection", "close") // 明确要求关闭连接
|
| 67 |
+
|
| 68 |
+
resp, err := client.Do(req)
|
| 69 |
+
if err != nil {
|
| 70 |
+
return "", err
|
| 71 |
+
}
|
| 72 |
+
defer resp.Body.Close()
|
| 73 |
+
|
| 74 |
+
body, err := io.ReadAll(resp.Body)
|
| 75 |
+
if err != nil {
|
| 76 |
+
return "", err
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
if resp.StatusCode != http.StatusOK {
|
| 80 |
+
// 检查是否是账号锁定错误
|
| 81 |
+
if isAccountLockoutError(resp.StatusCode, string(body)) {
|
| 82 |
+
// 将账号标记为封禁状态
|
| 83 |
+
if markErr := markAccountAsBanned(account, "OAuth认证失败-用户被锁定: "+string(body)); markErr != nil {
|
| 84 |
+
log.Printf("[账号管理] 标记账号封禁状态失败: %v", markErr)
|
| 85 |
+
}
|
| 86 |
+
return "", &AccountLockoutError{
|
| 87 |
+
StatusCode: resp.StatusCode,
|
| 88 |
+
Body: string(body),
|
| 89 |
+
AccountID: account.ClientID,
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
return "", fmt.Errorf("token request failed: %s", string(body))
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
var tokenResp TokenResponse
|
| 96 |
+
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
| 97 |
+
return "", err
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
account.AccessToken = tokenResp.AccessToken
|
| 101 |
+
account.TokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn-60) * time.Second)
|
| 102 |
+
|
| 103 |
+
// 只有已存在的账号才保存到数据库
|
| 104 |
+
if account.ID > 0 {
|
| 105 |
+
database.GetDB().Save(account)
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// 显式关闭传输层,确保连接被清理
|
| 109 |
+
transport.CloseIdleConnections()
|
| 110 |
+
|
| 111 |
+
return account.AccessToken, nil
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
func createHTTPClient(proxy string) *http.Client {
|
| 115 |
+
transport := &http.Transport{}
|
| 116 |
+
|
| 117 |
+
if proxy != "" {
|
| 118 |
+
proxyURL, err := url.Parse(proxy)
|
| 119 |
+
if err == nil {
|
| 120 |
+
transport.Proxy = http.ProxyURL(proxyURL)
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
return &http.Client{
|
| 125 |
+
Transport: transport,
|
| 126 |
+
Timeout: 30 * time.Second,
|
| 127 |
+
}
|
| 128 |
+
}
|
internal/service/zencoder.go
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"bytes"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"net/http"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
"github.com/google/uuid"
|
| 13 |
+
"zencoder-2api/internal/model"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
const (
|
| 17 |
+
ZencoderChatURL = "https://api.zencoder.ai/v1/chat/completions"
|
| 18 |
+
MaxRetries = 3
|
| 19 |
+
ZencoderVersion = "3.24.0"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
type ZencoderService struct{}
|
| 23 |
+
|
| 24 |
+
func NewZencoderService() *ZencoderService {
|
| 25 |
+
return &ZencoderService{}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func setZencoderHeaders(req *http.Request, token, modelID string) {
|
| 29 |
+
req.Header.Set("Accept", "application/json")
|
| 30 |
+
req.Header.Set("Content-Type", "application/json")
|
| 31 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 32 |
+
req.Header.Set("User-Agent", "zen-cli/0.9.0-windows-x64")
|
| 33 |
+
req.Header.Set("zen-model-id", modelID)
|
| 34 |
+
req.Header.Set("zencoder-arch", "x64")
|
| 35 |
+
req.Header.Set("zencoder-os", "windows")
|
| 36 |
+
req.Header.Set("zencoder-version", ZencoderVersion)
|
| 37 |
+
req.Header.Set("zencoder-client-type", "vscode")
|
| 38 |
+
req.Header.Set("zencoder-operation-id", uuid.New().String())
|
| 39 |
+
req.Header.Set("zencoder-operation-type", "agent_call")
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func (s *ZencoderService) Chat(req *model.ChatCompletionRequest) (*model.ChatCompletionResponse, error) {
|
| 43 |
+
// 检查模型是否存在于模型字典中
|
| 44 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 45 |
+
if !exists {
|
| 46 |
+
return nil, ErrNoAvailableAccount
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
var lastErr error
|
| 50 |
+
for i := 0; i < MaxRetries; i++ {
|
| 51 |
+
account, err := GetNextAccountForModel(req.Model)
|
| 52 |
+
if err != nil {
|
| 53 |
+
return nil, err
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
resp, err := s.doRequest(account, req)
|
| 57 |
+
if err != nil {
|
| 58 |
+
MarkAccountError(account)
|
| 59 |
+
lastErr = err
|
| 60 |
+
continue
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
ResetAccountError(account)
|
| 64 |
+
|
| 65 |
+
// ZenCoder服务没有HTTP响应,只能使用模型倍率
|
| 66 |
+
UseCredit(account, zenModel.Multiplier)
|
| 67 |
+
|
| 68 |
+
return resp, nil
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
return nil, fmt.Errorf("all retries failed: %w", lastErr)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
func (s *ZencoderService) doRequest(account *model.Account, req *model.ChatCompletionRequest) (*model.ChatCompletionResponse, error) {
|
| 75 |
+
token, err := GetToken(account)
|
| 76 |
+
if err != nil {
|
| 77 |
+
return nil, err
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// 获取模型映射
|
| 81 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 82 |
+
if !exists {
|
| 83 |
+
return nil, ErrNoAvailableAccount
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
body, err := json.Marshal(req)
|
| 87 |
+
if err != nil {
|
| 88 |
+
return nil, err
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
client := createHTTPClient(account.Proxy)
|
| 92 |
+
httpReq, err := http.NewRequest("POST", ZencoderChatURL, bytes.NewReader(body))
|
| 93 |
+
if err != nil {
|
| 94 |
+
return nil, err
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
setZencoderHeaders(httpReq, token, zenModel.ID)
|
| 98 |
+
|
| 99 |
+
resp, err := client.Do(httpReq)
|
| 100 |
+
if err != nil {
|
| 101 |
+
return nil, err
|
| 102 |
+
}
|
| 103 |
+
defer resp.Body.Close()
|
| 104 |
+
|
| 105 |
+
respBody, err := io.ReadAll(resp.Body)
|
| 106 |
+
if err != nil {
|
| 107 |
+
return nil, err
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
if resp.StatusCode != http.StatusOK {
|
| 111 |
+
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(respBody))
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
var chatResp model.ChatCompletionResponse
|
| 115 |
+
if err := json.Unmarshal(respBody, &chatResp); err != nil {
|
| 116 |
+
return nil, err
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
return &chatResp, nil
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
func (s *ZencoderService) ChatStream(req *model.ChatCompletionRequest, writer http.ResponseWriter) error {
|
| 123 |
+
// 检查模型是否存在于模型字典中
|
| 124 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 125 |
+
if !exists {
|
| 126 |
+
return ErrNoAvailableAccount
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
var lastErr error
|
| 130 |
+
for i := 0; i < MaxRetries; i++ {
|
| 131 |
+
account, err := GetNextAccountForModel(req.Model)
|
| 132 |
+
if err != nil {
|
| 133 |
+
return err
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
err = s.doStreamRequest(account, req, writer)
|
| 137 |
+
if err != nil {
|
| 138 |
+
MarkAccountError(account)
|
| 139 |
+
lastErr = err
|
| 140 |
+
continue
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
ResetAccountError(account)
|
| 144 |
+
|
| 145 |
+
// 流式响应,使用模型倍率
|
| 146 |
+
UseCredit(account, zenModel.Multiplier)
|
| 147 |
+
|
| 148 |
+
return nil
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
return fmt.Errorf("all retries failed: %w", lastErr)
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
func (s *ZencoderService) doStreamRequest(account *model.Account, req *model.ChatCompletionRequest, writer http.ResponseWriter) error {
|
| 155 |
+
token, err := GetToken(account)
|
| 156 |
+
if err != nil {
|
| 157 |
+
return err
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// 获取模型映射
|
| 161 |
+
zenModel, exists := model.GetZenModel(req.Model)
|
| 162 |
+
if !exists {
|
| 163 |
+
return ErrNoAvailableAccount
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
req.Stream = true
|
| 167 |
+
body, err := json.Marshal(req)
|
| 168 |
+
if err != nil {
|
| 169 |
+
return err
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
client := createHTTPClient(account.Proxy)
|
| 173 |
+
client.Timeout = 5 * time.Minute
|
| 174 |
+
|
| 175 |
+
httpReq, err := http.NewRequest("POST", ZencoderChatURL, bytes.NewReader(body))
|
| 176 |
+
if err != nil {
|
| 177 |
+
return err
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
setZencoderHeaders(httpReq, token, zenModel.ID)
|
| 181 |
+
|
| 182 |
+
resp, err := client.Do(httpReq)
|
| 183 |
+
if err != nil {
|
| 184 |
+
return err
|
| 185 |
+
}
|
| 186 |
+
defer resp.Body.Close()
|
| 187 |
+
|
| 188 |
+
if resp.StatusCode != http.StatusOK {
|
| 189 |
+
respBody, _ := io.ReadAll(resp.Body)
|
| 190 |
+
return fmt.Errorf("status %d: %s", resp.StatusCode, string(respBody))
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
return s.streamResponse(resp.Body, writer)
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
func (s *ZencoderService) streamResponse(body io.Reader, writer http.ResponseWriter) error {
|
| 197 |
+
flusher, ok := writer.(http.Flusher)
|
| 198 |
+
if !ok {
|
| 199 |
+
return fmt.Errorf("streaming not supported")
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
writer.Header().Set("Content-Type", "text/event-stream")
|
| 203 |
+
writer.Header().Set("Cache-Control", "no-cache")
|
| 204 |
+
writer.Header().Set("Connection", "keep-alive")
|
| 205 |
+
|
| 206 |
+
scanner := bufio.NewScanner(body)
|
| 207 |
+
for scanner.Scan() {
|
| 208 |
+
line := scanner.Text()
|
| 209 |
+
if line == "" {
|
| 210 |
+
continue
|
| 211 |
+
}
|
| 212 |
+
fmt.Fprintf(writer, "%s\n\n", line)
|
| 213 |
+
flusher.Flush()
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
return scanner.Err()
|
| 217 |
+
}
|