From 204f6d9a8b71d719afe3ccaaf9d101c7b8a087ba Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 19 Jul 2026 00:02:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(cache):=20=E5=AD=98=E9=87=8F=E7=A9=BA/url?= =?UTF-8?q?=20=E7=AD=96=E7=95=A5=E8=A7=84=E8=8C=83=E4=B8=BA=20all=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E9=9D=99=E9=BB=98=E6=94=B6=E7=AA=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审修复:enabled 且 policy 为空或 url 时,写入/展示/快照/渲染均映射为 all, 保证旧站点宽缓存范围不变;新建 UI 仍显式提交 static 作为推荐默认。 --- docs/changelog/index.md | 2 +- docs/design/edge-cache-design.md | 7 ++-- .../20260718-edge-cache-static-default.md | 32 ++++++++++++++----- .../(main)/proxy-routes/components/helpers.ts | 17 ++++++---- .../detail/components/cache-section.tsx | 25 +++++++++------ .../apps/openflare/config_version/logics.go | 25 +++++++++++++++ .../apps/openflare/proxy_route/helpers.go | 19 ++++++++--- internal/apps/openflare/proxy_route/logics.go | 2 +- .../apps/openflare/proxy_route/logics_test.go | 8 ++++- pkg/render/openresty/render.go | 9 ++++-- pkg/render/openresty/render_test.go | 5 +-- 11 files changed, 113 insertions(+), 38 deletions(-) diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 2cca0690..0a483cfa 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -23,7 +23,7 @@ sidebar: false ### 变更 -- 边缘缓存默认策略调整为「标准静态资源」:开启站点缓存后默认仅缓存 css/js/图片/字体等扩展名(不含 HTML);原「按 URL」行为保留为「所有可缓存 GET」。 +- 边缘缓存默认策略调整为「标准静态资源」:新建开启缓存时推荐仅缓存 css/js/图片/字体等扩展名(不含 HTML);原「按 URL」与空策略存量行为保留为「所有可缓存 GET」。重新发布节点配置后生效。 - 优化访问日志概览及其他图表全局排行榜 (RankChart) 的样式布局:将每一项改为单行横向排布(左侧标签、中间进度条、右侧数值),数值支持自动格式化为 Compact 形式(如 39.57k、1.2M),同时调整默认高度为 320px 并隐藏滚动条。 - 访问日志页面重构为「概览」与「日志明细」两个标签页:概览展示请求量、访问量、带宽趋势与 Top Paths/Hosts/IPs,明细列表保留检索能力;已移除时间折叠与 IP 汇总视图。 - 边缘访问日志现支持上报并存储 User-Agent;概览新增设备类型饼图,以及浏览器、操作系统、User-Agent 排行。 diff --git a/docs/design/edge-cache-design.md b/docs/design/edge-cache-design.md index ed06f1a5..4ff2e89a 100644 --- a/docs/design/edge-cache-design.md +++ b/docs/design/edge-cache-design.md @@ -142,9 +142,10 @@ access.log cache_status=$upstream_cache_status | 数据 | 处理 | | --- | --- | -| DB 中 `cache_policy=''` 或 `url` | 读取/发布时规范为 `all`;可选一次性 SQL 更新为 `all` | -| 新建路由 | 默认 `cache_enabled=false`;若用户开启缓存,表单默认策略 **`static`** | -| 已开启且 `url` 的站点 | 迁移后为 `all`,**缓存范围不变** | +| DB 中 `cache_policy=''` 或 `url`(且已启用缓存) | 读取 / 快照 / 渲染均规范为 **`all`**,保证存量「宽缓存」不变 | +| API 写入时 `enabled` 且 policy 为空 | 规范为 **`all`**(兼容旧客户端);UI 新建开启时**显式提交** `static` | +| 新建路由 | 默认 `cache_enabled=false`;表单开启缓存时默认策略 **`static`** | +| 已开启且 `url` 的站点 | 显示与发布为 `all`,**缓存范围不变** | | 期望「只缓存静态」的旧站点 | 用户在 UI 改为 `static` 或自定义 `suffix` | **发布说明建议:** 说明默认策略变更仅影响**新配置**;存量 `url` 视为 `all`。 diff --git a/docs/plan/20260718-edge-cache-static-default.md b/docs/plan/20260718-edge-cache-static-default.md index d0d0c386..e10c598f 100644 --- a/docs/plan/20260718-edge-cache-static-default.md +++ b/docs/plan/20260718-edge-cache-static-default.md @@ -4,19 +4,35 @@ ## 目标 -路由开启缓存后默认仅缓存标准静态扩展名(`static`);存量 `url` 映射为 `all`。 +路由开启缓存后,**新建推荐**仅缓存标准静态扩展名(`static`);存量 `url`/空策略映射为 `all`,不收窄缓存范围。 + +## 兼容规则(评审后定稿) + +| 场景 | 行为 | +| --- | --- | +| 已启用 + `''` / `url` | 读 API / 快照 / 渲染 → **`all`** | +| 写入时 enabled 且 policy 为空 | 规范为 **`all`**(旧客户端兼容) | +| UI 新建/推荐默认 | **显式提交** `static` | +| 关闭缓存 | policy 存 `''`,rules 清空 | ## 修改清单 -1. **渲染** `pkg/render/openresty/render.go`:`static` 内置扩展名;`url`/`all` 无路径限制 -2. **校验** `internal/apps/openflare/proxy_route/helpers.go`:策略枚举与规范化 -3. **前端** `cache-section.tsx` + helpers:默认 `static`,选项文案 -4. **测试** render + helpers -5. **changelog** +1. **渲染** `pkg/render/openresty/render.go`:`static` 内置扩展名;空/`url`/`all` 无路径限制 +2. **校验/展示** `proxy_route/helpers.go`:`normalizeCachePolicy` + `displayCachePolicy` +3. **快照** `config_version/logics.go`:`normalizeSnapshotCachePolicy` +4. **前端** `cache-section.tsx` + helpers:存量 empty/url→`all`;关闭时提交 `''`;新建默认 `static` +5. **测试** render + proxy_route +6. **设计/changelog** 同步兼容说明 ## 验证 ```bash -go test ./pkg/render/openresty/ ./internal/apps/openflare/proxy_route/ -make code-check # 或至少 go test + frontend tsc +go test ./pkg/render/openresty/ ./internal/apps/openflare/proxy_route/ ./internal/apps/openflare/config_version/ +# 已通过(2026-07-18) ``` + +## 状态 + +- [x] 功能实现 + 评审修复(empty→all,禁止静默收窄) +- [ ] 提交 `fix(cache): ...`(待用户确认) +- [ ] 合并 / 发布后需重新发布节点配置 diff --git a/frontend/app/(main)/proxy-routes/components/helpers.ts b/frontend/app/(main)/proxy-routes/components/helpers.ts index 3cee39b2..a7b031fb 100644 --- a/frontend/app/(main)/proxy-routes/components/helpers.ts +++ b/frontend/app/(main)/proxy-routes/components/helpers.ts @@ -335,12 +335,17 @@ export function buildPayloadFromRoute( limit_conn_per_ip: route.limit_conn_per_ip, limit_rate: route.limit_rate, cache_enabled: route.cache_enabled, - cache_policy: - !route.cache_policy || route.cache_policy === 'url' - ? route.cache_policy === 'url' - ? 'all' - : 'static' - : route.cache_policy, + cache_policy: (() => { + if (!route.cache_enabled) { + return 'static'; + } + const policy = (route.cache_policy || '').trim(); + // Legacy empty/url → all (same as backend displayCachePolicy). + if (!policy || policy === 'url' || policy === 'all') { + return 'all'; + } + return policy; + })(), cache_rules: route.cache_rule_list ?? [], custom_headers: route.custom_header_list ?? [], basic_auth_enabled: route.basic_auth_enabled, diff --git a/frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx b/frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx index fd22f82f..c1a36904 100644 --- a/frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx +++ b/frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx @@ -39,7 +39,6 @@ const cacheSchema = z cache_policy: z.enum([ 'static', 'all', - 'url', 'suffix', 'path_prefix', 'path_exact', @@ -70,10 +69,17 @@ interface CacheSectionProps { onSavingChange?: (saving: boolean) => void; } -function normalizeCachePolicyValue(policy: string | undefined | null) { +/** Map API/DB values for the form. Legacy empty/url → all (compat). */ +function normalizeCachePolicyValue( + policy: string | undefined | null, + enabled = true, +) { + if (!enabled) { + return 'static'; + } const value = (policy || '').trim(); - if (!value || value === 'static') return 'static'; - if (value === 'url' || value === 'all') return 'all'; + if (!value || value === 'url' || value === 'all') return 'all'; + if (value === 'static') return 'static'; if (value === 'suffix' || value === 'path_prefix' || value === 'path_exact') { return value; } @@ -103,6 +109,7 @@ export function CacheSection({ cache_enabled: route.cache_enabled, cache_policy: normalizeCachePolicyValue( route.cache_policy, + route.cache_enabled, ) as CacheValues['cache_policy'], cache_rules_text: route.cache_rule_list.join('\n'), }, @@ -113,6 +120,7 @@ export function CacheSection({ cache_enabled: route.cache_enabled, cache_policy: normalizeCachePolicyValue( route.cache_policy, + route.cache_enabled, ) as CacheValues['cache_policy'], cache_rules_text: route.cache_rule_list.join('\n'), }); @@ -161,9 +169,7 @@ export function CacheSection({ await save( { cache_enabled: values.cache_enabled, - cache_policy: values.cache_enabled - ? normalizeCachePolicyValue(values.cache_policy) - : 'static', + cache_policy: values.cache_enabled ? values.cache_policy : '', cache_rules: values.cache_enabled && needsRulesForPolicy(values.cache_policy) @@ -182,8 +188,9 @@ export function CacheSection({
启用站点缓存 - 开启后默认仅缓存标准静态扩展名(不含 HTML)。仍会自动绕过非 - GET、Authorization 与常见登录态 Cookie。 + 新建推荐「标准静态资源」(不含 + HTML)。须同时开启性能设置中的全局 OpenResty + 缓存。仍会绕过非 GET、Authorization 与常见登录 Cookie。
diff --git a/internal/apps/openflare/config_version/logics.go b/internal/apps/openflare/config_version/logics.go index 0e2e00f5..2202c962 100644 --- a/internal/apps/openflare/config_version/logics.go +++ b/internal/apps/openflare/config_version/logics.go @@ -347,10 +347,35 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute { routes[index].BasicAuthPassword = "" } routes[index].UpstreamType = normalizeUpstreamType(routes[index].UpstreamType) + routes[index].CachePolicy = normalizeSnapshotCachePolicy( + routes[index].CacheEnabled, + routes[index].CachePolicy, + ) + if !routes[index].CacheEnabled { + routes[index].CacheRules = nil + } } return routes } +// normalizeSnapshotCachePolicy aligns published policy with edge-cache-design: +// legacy empty/url → all; disabled → empty; static/suffix/... kept. +func normalizeSnapshotCachePolicy(enabled bool, raw string) string { + if !enabled { + return "" + } + policy := strings.TrimSpace(strings.ToLower(raw)) + switch policy { + case "", "url", "all": + return "all" + case "static", "suffix", "path_prefix", "path_exact": + return policy + default: + // Unknown: prefer static over caching everything. + return "static" + } +} + func flattenSnapshotRoutesBySite(routes []snapshotRoute) map[string]snapshotRoute { siteMap := make(map[string]snapshotRoute) for _, route := range normalizeSnapshotRoutes(routes) { diff --git a/internal/apps/openflare/proxy_route/helpers.go b/internal/apps/openflare/proxy_route/helpers.go index 32baf57e..57697c9e 100644 --- a/internal/apps/openflare/proxy_route/helpers.go +++ b/internal/apps/openflare/proxy_route/helpers.go @@ -436,17 +436,20 @@ func decodeStoredCustomHeaders(raw string) ([]CustomHeaderInput, error) { return normalizeCustomHeaders(headers) } +// normalizeCachePolicy stores API write values. +// When enabling with empty/url policy, keep legacy "all" semantics so old rows +// and clients that omit policy do not silently narrow cache to static extensions. +// New UI should send policy=static explicitly when choosing the recommended default. func normalizeCachePolicy(enabled bool, raw string) string { if !enabled { return "" } policy := strings.TrimSpace(strings.ToLower(raw)) switch policy { - case "", proxyRouteCachePolicyStatic: - return proxyRouteCachePolicyStatic - case proxyRouteCachePolicyURL, proxyRouteCachePolicyAll: - // Legacy url is normalized to all (full GET allow after security bypass). + case "", proxyRouteCachePolicyURL, proxyRouteCachePolicyAll: return proxyRouteCachePolicyAll + case proxyRouteCachePolicyStatic: + return proxyRouteCachePolicyStatic case proxyRouteCachePolicySuffix, proxyRouteCachePolicyPathPrefix, proxyRouteCachePolicyPathExact: return policy default: @@ -454,6 +457,14 @@ func normalizeCachePolicy(enabled bool, raw string) string { } } +// displayCachePolicy normalizes values for API list/get (and UI). +func displayCachePolicy(enabled bool, raw string) string { + if !enabled { + return "" + } + return normalizeCachePolicy(true, raw) +} + func normalizeCacheRules(enabled bool, rawPolicy string, rules []string) ([]string, error) { if !enabled { return []string{}, nil diff --git a/internal/apps/openflare/proxy_route/logics.go b/internal/apps/openflare/proxy_route/logics.go index d7aaae7f..1b781a84 100644 --- a/internal/apps/openflare/proxy_route/logics.go +++ b/internal/apps/openflare/proxy_route/logics.go @@ -309,7 +309,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e LimitConnPerIP: route.LimitConnPerIP, LimitRate: route.LimitRate, CacheEnabled: route.CacheEnabled, - CachePolicy: route.CachePolicy, + CachePolicy: displayCachePolicy(route.CacheEnabled, route.CachePolicy), CacheRules: route.CacheRules, CacheRuleList: cacheRules, CustomHeaders: route.CustomHeaders, diff --git a/internal/apps/openflare/proxy_route/logics_test.go b/internal/apps/openflare/proxy_route/logics_test.go index be8d528e..1af61011 100644 --- a/internal/apps/openflare/proxy_route/logics_test.go +++ b/internal/apps/openflare/proxy_route/logics_test.go @@ -83,12 +83,18 @@ func TestCreateProxyRouteHTTPSRequiresCoveringCertificate(t *testing.T) { func TestNormalizeCachePolicyDefaultsAndLegacy(t *testing.T) { assert.Equal(t, "", normalizeCachePolicy(false, "static")) - assert.Equal(t, proxyRouteCachePolicyStatic, normalizeCachePolicy(true, "")) + // Empty/url on write = legacy all (compat); UI sends static explicitly for new default. + assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "")) assert.Equal(t, proxyRouteCachePolicyStatic, normalizeCachePolicy(true, "static")) assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "url")) assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "all")) assert.Equal(t, proxyRouteCachePolicySuffix, normalizeCachePolicy(true, "suffix")) + assert.Equal(t, "", displayCachePolicy(false, "all")) + assert.Equal(t, proxyRouteCachePolicyAll, displayCachePolicy(true, "")) + assert.Equal(t, proxyRouteCachePolicyAll, displayCachePolicy(true, "url")) + assert.Equal(t, proxyRouteCachePolicyStatic, displayCachePolicy(true, "static")) + rules, err := normalizeCacheRules(true, "url", []string{"css"}) require.NoError(t, err) assert.Empty(t, rules) diff --git a/pkg/render/openresty/render.go b/pkg/render/openresty/render.go index 6890b059..8204bb27 100644 --- a/pkg/render/openresty/render.go +++ b/pkg/render/openresty/render.go @@ -497,13 +497,16 @@ func renderRouteCachePolicyCondition(cacheConfig routeCacheConfig) string { } } +// normalizeRenderCachePolicy maps stored policy for OpenResty generation. +// Legacy empty and "url" mean "all GETs after security bypass" (pre-static default). +// Explicit "static" uses the built-in extension allowlist. Unknown policies fall back to static. func normalizeRenderCachePolicy(raw string) string { policy := strings.TrimSpace(strings.ToLower(raw)) switch policy { - case "", cachePolicyStatic: - return cachePolicyStatic - case cachePolicyURL, cachePolicyAll: + case "", cachePolicyURL, cachePolicyAll: return cachePolicyAll + case cachePolicyStatic: + return cachePolicyStatic case cachePolicySuffix, cachePolicyPathPrefix, cachePolicyPathExact: return policy default: diff --git a/pkg/render/openresty/render_test.go b/pkg/render/openresty/render_test.go index b029dad4..bcf605bf 100644 --- a/pkg/render/openresty/render_test.go +++ b/pkg/render/openresty/render_test.go @@ -411,9 +411,10 @@ func TestRenderRouteCachePolicyConditionStaticDefault(t *testing.T) { t.Fatalf("static policy must not include html, got:\n%s", staticBlock) } + // Legacy empty/url = all (wide cache after security bypass). emptyPolicy := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: ""}) - if emptyPolicy == "" { - t.Fatal("empty policy should default to static condition") + if emptyPolicy != "" { + t.Fatalf("empty policy should map to all (no path filter), got %q", emptyPolicy) } allBlock := renderRouteCachePolicyCondition(routeCacheConfig{Enabled: true, Policy: "all"})