feat(cluster): 增加跨节点任务执行与请求素材场景
新增 AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED,默认保持启用;关闭时仅停止 River 异步任务执行器,HTTP、健康检查、结算与其他后台任务继续运行。 新增 0089 迁移并同步管理端默认场景,将 request_asset 加入现有上传渠道,确保 multipart 请求素材先转换为跨节点共享 URL。 验证:gofmt、go test ./... -count=1、go vet ./...、pnpm lint、pnpm test、pnpm build、迁移测试。
This commit is contained in:
@@ -84,6 +84,7 @@ AI_GATEWAY_LOCAL_RESULT_TTL_HOURS=24
|
||||
AI_GATEWAY_LOCAL_RESULT_MIN_FREE_BYTES=10737418240
|
||||
AI_GATEWAY_LOCAL_RESULT_MAX_BYTES=268435456
|
||||
AI_GATEWAY_LOCAL_RESULT_MAX_TASK_BYTES=536870912
|
||||
AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED=true
|
||||
|
||||
CORS_ALLOWED_ORIGIN=http://localhost:5178,http://127.0.0.1:5178
|
||||
VITE_GATEWAY_API_BASE_URL=http://localhost:8088
|
||||
|
||||
@@ -57,6 +57,7 @@ type Config struct {
|
||||
GlobalHTTPProxySource string
|
||||
LogLevel slog.Level
|
||||
BillingEngineMode string
|
||||
AsyncQueueWorkerEnabled bool
|
||||
AsyncWorkerHardLimit int
|
||||
AsyncWorkerRefreshIntervalSeconds int
|
||||
}
|
||||
@@ -112,6 +113,7 @@ func Load() Config {
|
||||
GlobalHTTPProxySource: globalProxy.Source,
|
||||
LogLevel: logLevel(env("LOG_LEVEL", "info")),
|
||||
BillingEngineMode: strings.ToLower(env("BILLING_ENGINE_MODE", "observe")),
|
||||
AsyncQueueWorkerEnabled: env("AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED", "true") == "true",
|
||||
AsyncWorkerHardLimit: envIntValidated("AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT", 2048),
|
||||
AsyncWorkerRefreshIntervalSeconds: envIntValidated("AI_GATEWAY_ASYNC_WORKER_REFRESH_INTERVAL_SECONDS", 5),
|
||||
}
|
||||
|
||||
@@ -82,6 +82,18 @@ func TestValidateAsyncWorkerSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAsyncQueueWorkerEnabled(t *testing.T) {
|
||||
cfg := Load()
|
||||
if !cfg.AsyncQueueWorkerEnabled {
|
||||
t.Fatal("async queue worker must be enabled by default")
|
||||
}
|
||||
t.Setenv("AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED", "false")
|
||||
cfg = Load()
|
||||
if cfg.AsyncQueueWorkerEnabled {
|
||||
t.Fatal("async queue worker remained enabled after explicit disable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTaskHistorySettings(t *testing.T) {
|
||||
cfg := Load()
|
||||
cfg.TaskRetentionDays = 30
|
||||
|
||||
@@ -120,7 +120,11 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
return server.identityRuntime.LegacyJWTEnabled()
|
||||
}
|
||||
server.auth.LocalAPIKeyVerifier = db.VerifyLocalAPIKey
|
||||
server.runner.StartAsyncQueueWorker(ctx)
|
||||
if cfg.AsyncQueueWorkerEnabled {
|
||||
server.runner.StartAsyncQueueWorker(ctx)
|
||||
} else if logger != nil {
|
||||
logger.Info("asynchronous queue worker disabled for this process")
|
||||
}
|
||||
server.runner.StartBillingSettlementWorker(ctx)
|
||||
server.runner.StartTaskHistoryWorkers(ctx)
|
||||
server.startLocalTempAssetCleanup(ctx)
|
||||
|
||||
@@ -578,7 +578,7 @@ func normalizeFileStorageScene(scene string) string {
|
||||
}
|
||||
|
||||
func defaultFileStorageScenes() []string {
|
||||
return []string{FileStorageSceneUpload, FileStorageSceneImageResult}
|
||||
return []string{FileStorageSceneUpload, FileStorageSceneImageResult, FileStorageSceneRequestAsset}
|
||||
}
|
||||
|
||||
func defaultFileStorageRetryPolicyIfEmpty(policy map[string]any) map[string]any {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultFileStorageScenesIncludeCrossNodeRequestAssets(t *testing.T) {
|
||||
want := []string{
|
||||
FileStorageSceneUpload,
|
||||
FileStorageSceneImageResult,
|
||||
FileStorageSceneRequestAsset,
|
||||
}
|
||||
if got := defaultFileStorageScenes(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("default file storage scenes = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
UPDATE file_storage_channels
|
||||
SET
|
||||
config = jsonb_set(
|
||||
config,
|
||||
'{scenes}',
|
||||
CASE
|
||||
WHEN jsonb_typeof(config->'scenes') = 'array'
|
||||
THEN (config->'scenes') || '["request_asset"]'::jsonb
|
||||
ELSE '["upload", "image_result", "request_asset"]'::jsonb
|
||||
END,
|
||||
true
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE deleted_at IS NULL
|
||||
AND provider = 'server_main_openapi'
|
||||
AND (
|
||||
jsonb_typeof(config->'scenes') IS DISTINCT FROM 'array'
|
||||
OR NOT (COALESCE(config->'scenes', '[]'::jsonb) ? 'request_asset')
|
||||
);
|
||||
@@ -42,10 +42,11 @@ const providerOptions = [
|
||||
{ value: 'tencent_cos', label: '腾讯云 COS' },
|
||||
];
|
||||
|
||||
const defaultScenes = ['upload', 'image_result'];
|
||||
const defaultScenes = ['upload', 'image_result', 'request_asset'];
|
||||
const sceneOptions = [
|
||||
{ value: 'upload', label: '上传', description: 'OpenAPI / 管理端主动上传文件' },
|
||||
{ value: 'image_result', label: '生成媒体结果', description: '模型返回 base64 / buffer 图片、音频或视频后的转存' },
|
||||
{ value: 'request_asset', label: '请求素材', description: '将 multipart / base64 输入转换为跨节点可读取的公共 URL' },
|
||||
];
|
||||
|
||||
const resultUploadPolicyOptions = [
|
||||
|
||||
@@ -33,6 +33,7 @@ x-api-environment: &api-environment
|
||||
AI_GATEWAY_LOCAL_RESULT_MIN_FREE_BYTES: ${AI_GATEWAY_LOCAL_RESULT_MIN_FREE_BYTES:-10737418240}
|
||||
AI_GATEWAY_LOCAL_RESULT_MAX_BYTES: ${AI_GATEWAY_LOCAL_RESULT_MAX_BYTES:-268435456}
|
||||
AI_GATEWAY_LOCAL_RESULT_MAX_TASK_BYTES: ${AI_GATEWAY_LOCAL_RESULT_MAX_TASK_BYTES:-536870912}
|
||||
AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED: ${AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED:-true}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
||||
Reference in New Issue
Block a user