feat(ops): add object storage management and site-mapping CSV import/export

Ship the Ops backstage 对象存储 page (browse / upload / move / delete /
folder ops) backed by a new object-storage service + handler, with the
shared storage client extended with List/Stat/Copy/Usage/DownloadURL so
both MinIO and Aliyun providers cover the new surface. Add
ForcePathStyle to the object-storage config and treat r2/s3/custom_s3
as external providers so Cloudflare R2 and other S3-compatibles can
share the MinIO client path without spinning up the bundled MinIO.

Also add CSV import/export + template download to the site-domain
mappings page, raise gin MaxMultipartMemory to 8 MiB for the new
multipart endpoints, register Ops swagger routes, and extend the
descriptions-parity test to cover the ops router.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 01:19:01 +08:00
parent 5f6e9f11da
commit 7d8e82c69f
37 changed files with 5111 additions and 66 deletions
+16 -2
View File
@@ -303,6 +303,17 @@ func queryParameterNames(route gin.RouteInfo) []string {
}
switch {
case strings.Contains(path, "/ops/site-domain-mappings"):
add("page", "size", "keyword", "is_active")
case strings.Contains(path, "/ops/object-storage/objects/detail") ||
strings.Contains(path, "/ops/object-storage/objects/url") ||
strings.Contains(path, "/ops/object-storage/objects/download-url") ||
strings.Contains(path, "/ops/object-storage/objects/download"):
add("key")
case strings.Contains(path, "/ops/object-storage/objects"):
add("prefix", "keyword", "continuation_token", "start_after", "recursive", "size", "key")
case strings.Contains(path, "/ops/object-storage/folders"):
add("prefix")
case strings.Contains(path, "/workspace/ai-point-usage"):
add("page", "page_size")
case strings.Contains(path, "/tenant/articles"):
@@ -357,13 +368,13 @@ func queryParameter(name string) map[string]any {
func schemaForQueryParameter(name string) map[string]any {
switch name {
case "page", "page_size", "limit", "offset", "days", "period_days":
case "page", "page_size", "limit", "offset", "days", "period_days", "size":
return map[string]any{"type": "integer"}
case "brand_id", "keyword_id", "question_id", "template_id", "kol_prompt_id", "prompt_rule_id", "group_id", "folder_id", "if_sync_version":
return map[string]any{"type": "integer", "format": "int64"}
case "force":
return map[string]any{"type": "string", "enum": []string{"1"}}
case "ungrouped", "undo":
case "ungrouped", "undo", "recursive", "is_active":
return map[string]any{"type": "string", "enum": []string{"true", "false"}}
case "created_from", "created_to", "date_from", "date_to", "business_date":
return map[string]any{"type": "string", "format": "date"}
@@ -403,6 +414,9 @@ func isMultipartRoute(route gin.RouteInfo) bool {
}
path := route.Path
return path == "/api/tenant/images" ||
path == "/api/ops/site-domain-mappings/import" ||
path == "/api/ops/object-storage/objects/upload" ||
path == "/api/ops/desktop-client/releases/upload" ||
path == "/api/tenant/knowledge/items/file" ||
path == "/api/tenant/kol/manage/profile/avatar" ||
path == "/api/tenant/articles/:id/images"