upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)

Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.

New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.

compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).

Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-23 12:31:03 +08:00
parent 257c89e159
commit 601451f22b
218 changed files with 10441 additions and 628 deletions
+44
View File
@@ -19,6 +19,7 @@ DEFAULTS={
'show_cta':True,'cta_text':'联系我们','cta_href':'/contact/'},
'footer':{'title':'','description':'','email':'','show_nav':True},
'nav':[],
'mobile_tabs':{'enabled':False,'items':[]},
'tokens':{'font':'system','heading_scale':1.0,'radius':12,'section_spacing':72,
'container_width':1200,'button_style':'rounded','header_style':'standard'},
'responsive':{'tablet_gap':64,'mobile_gap':40,'mobile_hide_announcement':True,
@@ -82,6 +83,22 @@ def _nav(rows,depth=0,counter=None):
'children':_nav(children,depth+1,counter) if depth<2 else []})
return result
def _mobile_tab_url(value):
"""Allow site-internal routes plus safe, explicit user-facing contact links."""
from urllib.parse import urlsplit
if not isinstance(value,str) or not value or len(value)>350 or any(c in value for c in ('<','>','"',"'",'\\','\n','\r','\t')):
raise HTTPException(400,'底部导航网址不安全')
if value.startswith('/') and not value.startswith('//'):
return _url(value)
if re.fullmatch(r'tel:\+?[0-9()\- ]{5,30}',value):
return value
if re.fullmatch(r'mailto:[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}',value):
return value
parsed=urlsplit(value)
if parsed.scheme=='https' and parsed.hostname and not parsed.username and not parsed.password and not any(c.isspace() for c in value):
return value
raise HTTPException(400,'仅支持站内路径、HTTPS、电话或邮箱链接')
def validate(raw):
if not isinstance(raw,dict) or set(raw)-set(DEFAULTS):raise HTTPException(400,'未知的全站设计设置')
normalized={}
@@ -117,6 +134,33 @@ def validate(raw):
if type(v) not in (float,int) or not lo<=v<=hi:raise HTTPException(400,'设计变量超出范围: '+field)
normalized['tokens']=tokens
normalized['nav']=_nav(raw.get('nav',[]))
bar=raw.get('mobile_tabs',{})
if not isinstance(bar,dict) or set(bar)-{'enabled','items'}:
raise HTTPException(400,'底部导航配置格式不正确')
enabled=bar.get('enabled',False)
items=bar.get('items',[])
if type(enabled) is not bool or not isinstance(items,list) or len(items)>5:
raise HTTPException(400,'底部导航仅支持 0 至 5 项')
allowed_icons={'home','grid','products','news','contact','phone','info','search','menu','star'}
result=[]
for item in items:
if not isinstance(item,dict) or set(item)-{'title','route','icon','action'}:
raise HTTPException(400,'底部导航项目格式不正确')
title=item.get('title')
icon=item.get('icon','grid')
action=item.get('action','link')
if not isinstance(title,str) or not 1<=len(title.strip())<=12:
raise HTTPException(400,'底部导航标题须为 1 至 12 字')
if icon not in allowed_icons or action not in ('link','menu'):
raise HTTPException(400,'底部导航图标或操作不受支持')
if action=='menu':
route='/'
else:
route=_mobile_tab_url(item.get('route','/'))
result.append({'title':title.strip(),'icon':icon,'route':route,'action':action})
if enabled and not result:
raise HTTPException(400,'启用底部导航时至少添加一项;不需要可直接关闭')
normalized['mobile_tabs']={'enabled':enabled,'items':result}
if len(json.dumps(normalized,ensure_ascii=False))>35_000:raise HTTPException(400,'全站设计过大')
return normalized