diff --git a/README.md b/README.md index 37ab5774d..4a16deb28 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ docker compose -f docker-compose.local.yml up -d --build - 用户自定义 AI API Key 保存在浏览器本地;登录态拉取模型目录时会临时提交给自部署后端但不会保存,创建异步任务时会加密入队;仅应使用可信部署,生产环境必须启用 HTTPS。 - 画布和素材登录后同步到后端,本地 `localForage` 继续承担缓存和降级存储。 -- 媒体资源在启用对象存储时可保存到私有阿里云 OSS 或腾讯云 COS,否则保存到后端数据目录;两种对象存储均可选填 CDN 加速域名,让上传继续使用 Endpoint、下载与预览使用 CDN;删除业务记录不会自动清理远端对象。 +- 媒体资源在启用对象存储时可保存到私有阿里云 OSS 或腾讯云 COS,否则保存到后端数据目录;两种对象存储均可选填 CDN 加速域名,让上传继续使用 Endpoint、下载与预览使用 CDN。CDN 地址不携带 OSS/COS 签名,敏感资源必须在 CDN 侧配置 URL 鉴权;删除业务记录不会自动清理远端对象。 - 用户主动上传、Agent 会话附件和 AI 生成资源的单文件上限、账号容量及 UTC 日上传总量由后台“资源与策略”统一维护,默认分别为 50MB、32MB、64MB、2GB 和 200MB;管理员可按可信部署需要调整,单文件业务上限最高 999MB,Nginx 请求体硬上限为 1024MB。 ## 公网部署安全 diff --git a/backend/internal/service/resource.go b/backend/internal/service/resource.go index 27636851e..ce7af6a95 100644 --- a/backend/internal/service/resource.go +++ b/backend/internal/service/resource.go @@ -979,7 +979,8 @@ func ossCDNObjectURL(raw string, objectKey string) (string, error) { return "", errors.New("对象存储对象路径为空") } // CDN 使用自己的访问鉴权与私有桶回源鉴权,不能携带 OSS/COS 的预签名参数。 - baseURL.Path = "/" + escapeObjectKey(objectKey) + // url.URL.String 会负责转义 Path;这里保留未转义值,避免把 %20 再编码为 %2520。 + baseURL.Path = "/" + objectKey return baseURL.String(), nil } diff --git a/backend/internal/service/resource_test.go b/backend/internal/service/resource_test.go index e56bf0ca1..771d902d7 100644 --- a/backend/internal/service/resource_test.go +++ b/backend/internal/service/resource_test.go @@ -75,7 +75,7 @@ func TestSignedOSSObjectURLUsesAliyunCDNBaseURLForDownloads(t *testing.T) { if err != nil { t.Fatal(err) } - if parsed.Host != "media.example.com" || parsed.Path != "/users/u-1/image/test image.png" || parsed.RawQuery != "" { + if parsed.Host != "media.example.com" || parsed.Path != "/users/u-1/image/test image.png" || parsed.RawQuery != "" || !strings.Contains(value, "test%20image.png") { t.Fatalf("Aliyun OSS CDN URL = %q", value) } } @@ -105,7 +105,7 @@ func TestSignedOSSObjectURLUsesTencentCOSCDNBaseURLWithoutCOSSignature(t *testin if err != nil { t.Fatal(err) } - if parsed.Host != "media.example.com" || parsed.Path != "/users/u-1/image/test image.png" || parsed.RawQuery != "" { + if parsed.Host != "media.example.com" || parsed.Path != "/users/u-1/image/test image.png" || parsed.RawQuery != "" || !strings.Contains(value, "test%20image.png") { t.Fatalf("signed COS CDN URL = %q", value) } } diff --git a/web/src/components/layout/user-oss-settings-form.tsx b/web/src/components/layout/user-oss-settings-form.tsx index 13fd697a9..5b18c3c3b 100644 --- a/web/src/components/layout/user-oss-settings-form.tsx +++ b/web/src/components/layout/user-oss-settings-form.tsx @@ -117,8 +117,8 @@ export function UserOSSSettingsForm() { name="cdnBaseUrl" label="CDN 加速域名" extra={isTencentCOS - ? "选填。上传仍走 Endpoint,下载与预览改走 CDN;私有桶需开启 CDN 私有存储桶访问,CDN URL 按官方要求不附带 COS 签名。" - : "选填。上传仍走 Endpoint,下载与预览改走 CDN;阿里云私有 Bucket 需开启 CDN 私有 Bucket 回源,CDN URL 按官方要求不附带 OSS 签名。"} + ? "选填。上传仍走 Endpoint,下载与预览改走 CDN;私有桶需开启 CDN 私有存储桶访问。CDN URL 不附带 COS 签名,未配置 CDN URL 鉴权时链接将长期可访问。" + : "选填。上传仍走 Endpoint,下载与预览改走 CDN;阿里云私有 Bucket 需开启 CDN 私有 Bucket 回源。CDN URL 不附带 OSS 签名,未配置 CDN URL 鉴权时链接将长期可访问。"} rules={[{ type: "url", message: "请填写完整的 http/https CDN 加速域名" }]} className="mb-3" > diff --git a/web/src/pages/admin/settings/storage-settings-page.tsx b/web/src/pages/admin/settings/storage-settings-page.tsx index 82250f093..897215198 100644 --- a/web/src/pages/admin/settings/storage-settings-page.tsx +++ b/web/src/pages/admin/settings/storage-settings-page.tsx @@ -18,6 +18,7 @@ export default function StorageSettingsPage() { const [saving, setSaving] = useState(false); const [form] = Form.useForm(); const mode = Form.useWatch("mode", form) || "local"; + const cdnBaseUrl = Form.useWatch("cdnBaseUrl", form); const isObjectStorage = mode !== "local"; const isTencentCOS = mode === "tencent"; const selectedProviderLabel = isTencentCOS ? "腾讯云 COS" : "阿里云 OSS"; @@ -90,7 +91,7 @@ export default function StorageSettingsPage() { <> - + @@ -127,7 +128,7 @@ export default function StorageSettingsPage() { -
: } text={isObjectStorage ? `新资源写入${selectedProviderLabel}` : "新资源写入服务器数据卷"} />} text="历史资源位置保持不变" />} text="外部链接仅在签名有效期内可用" />
+
: } text={isObjectStorage ? `新资源写入${selectedProviderLabel}` : "新资源写入服务器数据卷"} />} text="历史资源位置保持不变" />} text={isObjectStorage && cdnBaseUrl?.trim() ? "CDN 链接依赖 CDN 自身访问控制" : "外部链接仅在签名有效期内可用"} />
);