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_MIN_FREE_BYTES=10737418240
|
||||||
AI_GATEWAY_LOCAL_RESULT_MAX_BYTES=268435456
|
AI_GATEWAY_LOCAL_RESULT_MAX_BYTES=268435456
|
||||||
AI_GATEWAY_LOCAL_RESULT_MAX_TASK_BYTES=536870912
|
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
|
CORS_ALLOWED_ORIGIN=http://localhost:5178,http://127.0.0.1:5178
|
||||||
VITE_GATEWAY_API_BASE_URL=http://localhost:8088
|
VITE_GATEWAY_API_BASE_URL=http://localhost:8088
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type Config struct {
|
|||||||
GlobalHTTPProxySource string
|
GlobalHTTPProxySource string
|
||||||
LogLevel slog.Level
|
LogLevel slog.Level
|
||||||
BillingEngineMode string
|
BillingEngineMode string
|
||||||
|
AsyncQueueWorkerEnabled bool
|
||||||
AsyncWorkerHardLimit int
|
AsyncWorkerHardLimit int
|
||||||
AsyncWorkerRefreshIntervalSeconds int
|
AsyncWorkerRefreshIntervalSeconds int
|
||||||
}
|
}
|
||||||
@@ -112,6 +113,7 @@ func Load() Config {
|
|||||||
GlobalHTTPProxySource: globalProxy.Source,
|
GlobalHTTPProxySource: globalProxy.Source,
|
||||||
LogLevel: logLevel(env("LOG_LEVEL", "info")),
|
LogLevel: logLevel(env("LOG_LEVEL", "info")),
|
||||||
BillingEngineMode: strings.ToLower(env("BILLING_ENGINE_MODE", "observe")),
|
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),
|
AsyncWorkerHardLimit: envIntValidated("AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT", 2048),
|
||||||
AsyncWorkerRefreshIntervalSeconds: envIntValidated("AI_GATEWAY_ASYNC_WORKER_REFRESH_INTERVAL_SECONDS", 5),
|
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) {
|
func TestValidateTaskHistorySettings(t *testing.T) {
|
||||||
cfg := Load()
|
cfg := Load()
|
||||||
cfg.TaskRetentionDays = 30
|
cfg.TaskRetentionDays = 30
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
|||||||
return server.identityRuntime.LegacyJWTEnabled()
|
return server.identityRuntime.LegacyJWTEnabled()
|
||||||
}
|
}
|
||||||
server.auth.LocalAPIKeyVerifier = db.VerifyLocalAPIKey
|
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.StartBillingSettlementWorker(ctx)
|
||||||
server.runner.StartTaskHistoryWorkers(ctx)
|
server.runner.StartTaskHistoryWorkers(ctx)
|
||||||
server.startLocalTempAssetCleanup(ctx)
|
server.startLocalTempAssetCleanup(ctx)
|
||||||
|
|||||||
@@ -578,7 +578,7 @@ func normalizeFileStorageScene(scene string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultFileStorageScenes() []string {
|
func defaultFileStorageScenes() []string {
|
||||||
return []string{FileStorageSceneUpload, FileStorageSceneImageResult}
|
return []string{FileStorageSceneUpload, FileStorageSceneImageResult, FileStorageSceneRequestAsset}
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultFileStorageRetryPolicyIfEmpty(policy map[string]any) map[string]any {
|
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' },
|
{ value: 'tencent_cos', label: '腾讯云 COS' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultScenes = ['upload', 'image_result'];
|
const defaultScenes = ['upload', 'image_result', 'request_asset'];
|
||||||
const sceneOptions = [
|
const sceneOptions = [
|
||||||
{ value: 'upload', label: '上传', description: 'OpenAPI / 管理端主动上传文件' },
|
{ value: 'upload', label: '上传', description: 'OpenAPI / 管理端主动上传文件' },
|
||||||
{ value: 'image_result', label: '生成媒体结果', description: '模型返回 base64 / buffer 图片、音频或视频后的转存' },
|
{ value: 'image_result', label: '生成媒体结果', description: '模型返回 base64 / buffer 图片、音频或视频后的转存' },
|
||||||
|
{ value: 'request_asset', label: '请求素材', description: '将 multipart / base64 输入转换为跨节点可读取的公共 URL' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const resultUploadPolicyOptions = [
|
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_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_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_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:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
Reference in New Issue
Block a user