upgrade: adopt V8.34.1 (multi-image banner carousel + spec-table source modes)

Plain overlay (33 items, same-source lineage, zero patch replay).
Theme suites 35/35; full run 453 passed + 10 known Windows env fails
(suspect-module rerun confirms identical failure set).
Live: container 8.34.1-dev.1; hesupipe home leftover industrial-preset
blocks cleared via studio draft+publish flow — home now renders pure
trade-pro template (sf-section/Chinese blocks = 0).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-26 18:03:07 +08:00
parent 391d22f1a8
commit df2a183dbf
38 changed files with 1652 additions and 521 deletions
+28
View File
@@ -0,0 +1,28 @@
"""V8.29 isolated regression with per-batch deadline and raw logs."""
import argparse,datetime,json,subprocess,sys,time
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
def main():
ap=argparse.ArgumentParser();ap.add_argument('--timeout',type=int,default=90);ap.add_argument('--group-size',type=int,default=3);ap.add_argument('--start',type=int,default=1);ap.add_argument('--end',type=int,default=999);args=ap.parse_args()
all_files=sorted((ROOT/'tests').glob('test_*.py'))
groups=[all_files[i:i+args.group_size] for i in range(0,len(all_files),args.group_size)]
dest=ROOT/'reports/v834-regression';dest.mkdir(parents=True,exist_ok=True)
outcomes=[]
for index,files in enumerate(groups,1):
if not args.start <= index <= args.end:continue
start=time.monotonic();command=[sys.executable,'-m','pytest','-q',*[str(f.relative_to(ROOT)) for f in files]]
try:
proc=subprocess.run(command,cwd=ROOT,capture_output=True,text=True,timeout=args.timeout)
code=proc.returncode;output=proc.stdout+'\n'+proc.stderr;status='passed' if code==0 else 'failed'
except subprocess.TimeoutExpired as e:
output=(e.stdout or b'').decode(errors='replace') if isinstance(e.stdout,bytes) else (e.stdout or '')
output+='\nTIMEOUT exceeded';status='timeout'
elapsed=round(time.monotonic()-start,2)
(dest/f'batch-{index:02}.log').write_text(output,encoding='utf-8')
outcomes.append({'batch':index,'files':[f.name for f in files],'status':status,'seconds':elapsed,'summary':output[-350:]})
print(f'[{index:02}/{len(groups)}] {status.upper()} {elapsed:.1f}s: '+', '.join(f.name for f in files),flush=True)
result={'version':'8.34.0-dev.1','generated_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'groups':outcomes,'all_passed':all(x['status']=='passed' for x in outcomes)}
(dest/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
print('RESULT:',result['all_passed'],sum(x['status']=='passed' for x in outcomes),len(groups),flush=True)
return 0 if result['all_passed'] else 1
if __name__=='__main__':raise SystemExit(main())
+28
View File
@@ -0,0 +1,28 @@
"""V8.29 isolated regression with per-batch deadline and raw logs."""
import argparse,datetime,json,subprocess,sys,time
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
def main():
ap=argparse.ArgumentParser();ap.add_argument('--timeout',type=int,default=90);ap.add_argument('--group-size',type=int,default=3);ap.add_argument('--start',type=int,default=1);ap.add_argument('--end',type=int,default=999);args=ap.parse_args()
all_files=sorted((ROOT/'tests').glob('test_*.py'))
groups=[all_files[i:i+args.group_size] for i in range(0,len(all_files),args.group_size)]
dest=ROOT/'reports/v8341-regression';dest.mkdir(parents=True,exist_ok=True)
outcomes=[]
for index,files in enumerate(groups,1):
if not args.start <= index <= args.end:continue
start=time.monotonic();command=[sys.executable,'-m','pytest','-q',*[str(f.relative_to(ROOT)) for f in files]]
try:
proc=subprocess.run(command,cwd=ROOT,capture_output=True,text=True,timeout=args.timeout)
code=proc.returncode;output=proc.stdout+'\n'+proc.stderr;status='passed' if code==0 else 'failed'
except subprocess.TimeoutExpired as e:
output=(e.stdout or b'').decode(errors='replace') if isinstance(e.stdout,bytes) else (e.stdout or '')
output+='\nTIMEOUT exceeded';status='timeout'
elapsed=round(time.monotonic()-start,2)
(dest/f'batch-{index:02}.log').write_text(output,encoding='utf-8')
outcomes.append({'batch':index,'files':[f.name for f in files],'status':status,'seconds':elapsed,'summary':output[-350:]})
print(f'[{index:02}/{len(groups)}] {status.upper()} {elapsed:.1f}s: '+', '.join(f.name for f in files),flush=True)
result={'version':'8.34.1-dev.1','generated_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'groups':outcomes,'all_passed':all(x['status']=='passed' for x in outcomes)}
(dest/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
print('RESULT:',result['all_passed'],sum(x['status']=='passed' for x in outcomes),len(groups),flush=True)
return 0 if result['all_passed'] else 1
if __name__=='__main__':raise SystemExit(main())
+43
View File
@@ -0,0 +1,43 @@
"""Test offline actual renderer composed HTML and 390px responsiveness."""
import json
from pathlib import Path
from playwright.sync_api import sync_playwright
root=Path(__file__).resolve().parents[1]
raw=(root/'examples/V8_34_主题Banner与产品卡片_真实渲染交互预览.html').read_text()
out=root/'reports/v834';out.mkdir(parents=True,exist_ok=True)
results=[]
with sync_playwright() as p:
browser=p.chromium.launch(executable_path='/usr/bin/chromium',headless=True,args=['--no-sandbox'])
page=browser.new_page(viewport={'width':1480,'height':950},device_scale_factor=1)
errors=[];page.on('pageerror',lambda err:errors.append(str(err)))
page.set_content(raw,wait_until='load')
page.locator('#canvas').wait_for()
for theme in ['automotive','industrial']:
page.locator('#theme').select_option(theme)
for variant in ['split','image-first','centered']:
page.locator('#banner').select_option(variant)
for cards in ['standard','compact','showcase']:
page.locator('#cards').select_option(cards)
for width in ['100%','390px']:
page.locator(f'[data-width="{width}"]').click()
page.wait_for_timeout(80)
frame=page.frame_locator('#canvas')
hero=frame.locator('.sf-hero-grid')
item=frame.locator('.sf-section.sf-collection > .sf-container > .sf-collection')
assert hero.count()==1,(theme,variant,cards,width)
assert item.count()==1,(theme,variant,cards,width)
assert f'sf-hero-layout-{variant}' in hero.get_attribute('class')
assert f'sf-card-style-{cards}' in item.get_attribute('class')
overflow=frame.locator('body').evaluate('(e)=>e.scrollWidth>document.documentElement.clientWidth+1')
results.append({'theme':theme,'banner':variant,'cards':cards,'width':width,'overflow':bool(overflow)})
# canonical screenshots
page.locator('#theme').select_option('industrial');page.locator('#banner').select_option('centered');page.locator('#cards').select_option('showcase');page.locator('[data-width="100%"]').click()
page.wait_for_timeout(120)
page.screenshot(path=str(out/'v834-desktop.png'),full_page=True)
page.locator('[data-width="390px"]').click();page.wait_for_timeout(120)
page.screenshot(path=str(out/'v834-phone.png'),full_page=True)
assert not errors,errors
browser.close()
report={'cases':len(results),'overflows':[x for x in results if x['overflow']],'js_errors':errors,'screenshots':['v834-desktop.png','v834-phone.png']}
(out/'browser-check.json').write_text(json.dumps(report,indent=2,ensure_ascii=False))
print(json.dumps({'cases':len(results),'overflows':len(report['overflows']),'js_errors':errors},ensure_ascii=False))
+29
View File
@@ -0,0 +1,29 @@
"""Reproducible offline preview assembled from actual V8.34 server rendering."""
import html,json
from pathlib import Path
from app.blueprints import new_block,render_blocks
ROOT=Path(__file__).resolve().parents[1]
OUT=ROOT/'examples/V8_34_主题Banner与产品卡片_真实渲染交互预览.html'
css=(ROOT/'app/static/blocks.css').read_text()
styles={k:((ROOT/f'themes/{k}/blocks.css').read_text() if (ROOT/f'themes/{k}/blocks.css').exists() else '') for k in ('automotive','industrial')}
entries={
'automotive':[{'route':f'/product/model-{i}/','title':v,'type':'product','cover':'','summary':'出口车型展示 · 可按实际车型数据替换','meta_json':'{}'} for i,v in enumerate(['SUV 车型 A','新能源车型 B','皮卡车型 C','商务车型 D','紧凑车型 E','乘用车型 F'],1)],
'industrial':[{'route':f'/product/pipe-{i}/','title':v,'type':'product','cover':'','summary':'管道产品展示 · 可查看技术参数','meta_json':'{}'} for i,v in enumerate(['HDPE 给水管','PVC-U 排水管','双壁波纹管','钢丝网骨架管','土工布','土工膜'],1)]
}
views={}
for theme in entries:
views[theme]={}
for banner in ('split','image-first','centered'):
for cards in ('standard','compact','showcase'):
blocks=[new_block('hero',{'title': '中国汽车 · 全球交付' if theme=='automotive' else '专业管道 · 工程应用', 'subtitle': 'GLOBAL AUTOMOTIVE' if theme=='automotive' else 'PIPE SYSTEM SOLUTIONS','description':'这是实际服务端渲染的内容模块,点击选项可比较布局。','image':'','layout_variant':banner,'image_fit':'contain','button':'查看产品','href':'/product/'}),new_block('collection',{'title':'精选车型' if theme=='automotive' else '主要产品','subtitle':'PRODUCT SELECTION','source':'product','limit':6,'columns':3,'card_style':cards,'image_fit':'contain'})]
views[theme][banner+'-'+cards]=render_blocks(blocks,{'name':'DEMO'},{'route':'/','type':'home'},entries[theme])
# escape script closure
payload=json.dumps({'views':views,'styles':styles,'blocks':css},ensure_ascii=False).replace('</','<\\/')
base=f'''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>SiteForge V8.34 · 真正生成的主题布局</title>
<style>*{{box-sizing:border-box}}body{{margin:0;background:#edf1f6;color:#24344b;font-family:system-ui,"Microsoft YaHei",sans-serif}}button,select{{font:inherit}}.toolbar{{padding:16px 20px;background:white;border-bottom:1px solid #dfe6ed;display:flex;gap:15px;align-items:center;flex-wrap:wrap}}.toolbar strong{{font-size:17px}}.toolbar small{{display:block;color:#75869a;font-size:11px}}.controls{{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-left:auto}}select{{background:#f7fafc;border:1px solid #c9d6e2;border-radius:8px;padding:9px;color:#2c485e;max-width:185px}}.size button{{padding:9px;border:1px solid #ccd7e2;background:white;border-radius:6px;cursor:pointer}}.size button[aria-pressed=true]{{background:#205a8a;color:#fff}}#canvas{{display:block;width:100%;height:calc(100vh - 100px);min-height:520px;margin:auto;border:0;background:white;transition:width .2s}}@media(max-width:680px){{.toolbar{{padding:12px;gap:10px}}.controls{{margin-left:0}}#canvas{{height:calc(100vh - 155px)}}}}</style></head><body>
<div class="toolbar"><div><strong>SiteForge V8.34 · Banner 与产品卡片</strong><small>来自实际服务端区块渲染 · 仅演示布局,不连接业务后台</small></div><div class="controls"><select id="theme" aria-label="主题"><option value="automotive">汽车主题</option><option value="industrial">工业建材主题</option></select><select id="banner" aria-label="Banner 布局"><option value="split">Banner · 文字左</option><option value="image-first">Banner · 图片左</option><option value="centered">Banner · 标题居中</option></select><select id="cards" aria-label="卡片形式"><option value="standard">卡片 · 标准</option><option value="compact">卡片 · 紧凑</option><option value="showcase">卡片 · 图片优先</option></select><div class="size"><button data-width="100%" aria-pressed="true">电脑</button><button data-width="820px" aria-pressed="false">平板</button><button data-width="390px" aria-pressed="false">手机</button></div></div></div>
<iframe id="canvas" title="主题页面真实渲染" sandbox="allow-scripts"></iframe><script type="application/json" id="data">{payload}</script><script>
const data=JSON.parse(document.getElementById('data').textContent), frame=document.getElementById('canvas');
function paint(){{const t=document.getElementById('theme').value,key=document.getElementById('banner').value+'-'+document.getElementById('cards').value;const page='<html><head><meta name="viewport" content="width=device-width, initial-scale=1"><style>'+data.blocks+'</style><style>'+data.styles[t]+'</style><style>body{{margin:0;font-family:system-ui,"Microsoft YaHei",sans-serif;background:white;color:#20354a}}.sf-hero-abstract{{background:linear-gradient(140deg,#eff4f8,#d5e3ed);border-radius:12px}}.sf-item{{height:100%}}</style></head><body><div class="sf-theme-'+t+'"><main class="pro-custom-content">'+data.views[t][key]+'</main></div></body></html>';frame.srcdoc=page;}}
['theme','banner','cards'].forEach(id=>document.getElementById(id).addEventListener('change',paint));document.querySelectorAll('[data-width]').forEach(b=>b.addEventListener('click',()=>{{frame.style.width=b.dataset.width;document.querySelectorAll('[data-width]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));}}));paint();</script></body></html>'''
OUT.write_text(base,encoding='utf-8');print('CREATED',OUT,OUT.stat().st_size)
+1 -1
View File
@@ -6,7 +6,7 @@ import sys
ROOT=Path(__file__).resolve().parent.parent
sys.path.insert(0,str(ROOT))
from app.version import VERSION
assert VERSION=='8.33.1-dev.1'
assert VERSION=='8.34.1-dev.1'
api=(ROOT/'app'/'main.py').read_text(encoding='utf-8')
assert 'return builder.build(' not in api, 'A synchronous request-thread build has reappeared'
assert "@app.post('/api/sites/{site_id}/builds', status_code=202)" in api