feat(storage): 统一二进制对象存储与公开错误

新增 Aliyun OSS 与 S3 协议、通道内重试和按优先级跨通道切换,保留 server-main 兼容与环境 OSS 内存通道。

将请求及结果中的 Base64、Data URI、Buffer、multipart 和内联二进制统一对象化,生产路径不再写入本机静态目录,历史本地资源仅保留只读兼容。

引入 PublicErrorV1 并统一 API、异步查询、兼容协议和失败回调的安全错误输出,同时补充迁移、管理端、指标、OpenAPI 与本地模拟验收。

验证:go test ./... -count=1;go vet ./...;pnpm lint;pnpm test;pnpm build;pnpm openapi;tests/ci/migrations-test.sh。
This commit is contained in:
2026-08-04 08:14:39 +08:00
parent d129bcccbd
commit 0f0998cbcf
55 changed files with 3649 additions and 1008 deletions
+70
View File
@@ -161,6 +161,9 @@ func run(ctx context.Context, opts options) error {
if err := ensureAcceptanceAccessRules(ctx, database, groupID); err != nil {
return err
}
if err := ensureAcceptanceObjectStorage(ctx, database, opts.emulatorBaseURL); err != nil {
return err
}
geminiModel, videoModel, err := selectedModels(ctx, database)
if err != nil {
return err
@@ -240,6 +243,73 @@ func run(ctx context.Context, opts options) error {
return nil
}
func ensureAcceptanceObjectStorage(ctx context.Context, database *store.Store, emulatorBaseURL string) error {
baseURL := strings.TrimRight(strings.TrimSpace(emulatorBaseURL), "/")
if baseURL == "" {
return errors.New("acceptance object storage requires the emulator URL")
}
channels := []struct {
key string
name string
provider string
endpoint string
priority int
pathStyle bool
}{
{
key: "local-acceptance-oss", name: "Local Acceptance Aliyun OSS", provider: "aliyun_oss",
endpoint: baseURL + "/storage/oss/bucket", priority: 10, pathStyle: true,
},
{
key: "local-acceptance-s3", name: "Local Acceptance S3", provider: "s3",
endpoint: baseURL + "/storage/s3", priority: 20, pathStyle: true,
},
}
for _, channel := range channels {
_, err := database.Pool().Exec(ctx, `
INSERT INTO file_storage_channels (
channel_key, name, provider, credentials, config, retry_policy, priority, status
)
VALUES (
$1, $2, $3,
'{"accessKeyId":"local-acceptance","accessKeySecret":"local-acceptance-secret"}'::jsonb,
jsonb_build_object(
'endpoint', $4::text,
'region', 'local-acceptance-1',
'bucket', 'bucket',
'objectPrefix', 'acceptance/media',
'accessScope', 'private',
'forcePathStyle', $5::boolean,
'scenes', jsonb_build_array('upload', 'image_result', 'request_asset'),
'acceptanceEmulatorOnly', true
),
'{"enabled":true,"maxRetries":2,"backoffSeconds":[0.25,1],"strategy":"exponential"}'::jsonb,
$6, 'enabled'
)
ON CONFLICT (channel_key) DO UPDATE
SET name = EXCLUDED.name,
provider = EXCLUDED.provider,
credentials = EXCLUDED.credentials,
config = EXCLUDED.config,
retry_policy = EXCLUDED.retry_policy,
priority = EXCLUDED.priority,
status = 'enabled',
deleted_at = NULL,
last_error = NULL,
updated_at = now()`, channel.key, channel.name, channel.provider, channel.endpoint, channel.pathStyle, channel.priority)
if err != nil {
return fmt.Errorf("configure %s object storage channel: %w", channel.provider, err)
}
}
_, err := database.Pool().Exec(ctx, `
INSERT INTO system_settings (setting_key, value)
VALUES ('file_storage', '{"resultUploadPolicy":"default"}'::jsonb)
ON CONFLICT (setting_key) DO UPDATE
SET value = jsonb_set(COALESCE(system_settings.value, '{}'::jsonb), '{resultUploadPolicy}', '"default"'::jsonb, true),
updated_at = now()`)
return err
}
func resetPreviousLocalRun(ctx context.Context, database *store.Store, opts options) error {
mode, err := database.GetGatewayTrafficMode(ctx)
if err != nil {