diff --git a/README_V8_33.md b/README_V8_33.md new file mode 100644 index 0000000..945ac63 --- /dev/null +++ b/README_V8_33.md @@ -0,0 +1,44 @@ +# SiteForge Enterprise V8.33 — 五套主题安全融合与主线稳定性 + +**基线:V8.32.0-dev.1;交付:V8.33.0-dev.1;新增主题:Trade Pro 1.0.0。** + +此项目包含完整 FastAPI/Worker 网站工厂源码、5 套内置主题、编辑器、迁移、数据装配、审批构建、测试和验收报告。不是仅有 Trade Pro 的增量包。 + +## 本轮实装 +- V8.32 主线之上,选择性引入 Trade Pro 主题 14 类布局与 13 类页面预设;保留旧主题各版本和已有编辑器/发布功能。 +- 主题发现统一为 `themes/*/manifest.json`(`app/core.py`),核心、专业主题、主题商店共用一个主题清单。 +- 主题本地组件宏覆盖共享宏;上传的自定义主题仍限制为声明式 CSS/预设,不能上传任意 Jinja 或脚本。 +- 后台工作台、主题图库、编辑器、建站向导补齐第五主题选项、说明和色板。 +- 为 Trade Pro 增加受控的 `blocks.css`,构建和主题预览共享同一来源;历史汽车主题版本与优化资源包不被改变。 +- 支持独立英文站点的语言设置,Trade Pro 默认 `en`(语言组配置优先),保留路径与 SEO 字段;修复只选取 settings 字段导致构建报错。 +- 保留 V8.32 主题预览 iframe 的同源安全限制与编辑器 `sandbox=allow-scripts`(**不授予同源脚本权限**)。 + +## 主要文件 +- 新主题 `themes/trade-pro/`(manifest、预设、14 个页面、主题 CSS、blocks.css、组件宏) +- 主题服务 `app/core.py`、`app/theme_pro.py`、`app/theme_store.py`、`app/factory_api.py`、`app/builder.py` +- UI `app/templates/theme-gallery.html`、`app/templates/editor.html`、`app/static/studio-experience-v829.js` +- 测试 `tests/test_theme_registry.py`、`tests/test_v833_theme_integration.py` +- 隔离演示 `examples/V8_33_五套主题真实页面_离线交互预览.html` +- 验收 `reports/v833/V8_33_五套主题融合验收.html`、`reports/v833-regression/` + +## 验收(本机开发/隔离环境) +- Python:**435 passed,19/19 独立批次通过**(`scripts/regression_v833.py`)。 +- JavaScript:**39 项通过**,嵌套组件额外专项通过。 +- HTTP + Worker:真实启动、登录、CSRF、建站、异步入队、Worker 生成 Trade Pro 静态 HTML、ZIP、预览通过。 +- Chromium:实际渲染的 Trade Pro 产品详情在电脑 1365px 与手机 390px 测试通过,外层无横向溢出、脚本错误 0;此浏览器测试是**离线隔离**环境,不是正式登录后台 E2E。 +- 主题:5 × 14 页面类型可通过后台 API 生成;离线演示可切换 5 × 14 × 3 设备(设备宽度为模拟视口)。 + +## 运行 +安装依赖:`pip install -r requirements-dev.lock`(按 Python 项目自带说明设置数据目录、环境变量)。 +测试:`python -m pytest -q tests/test_v833_theme_integration.py tests/test_theme_registry.py`。 +回归:`python scripts/regression_v833.py --timeout 90 --group-size 3`。 +真实 HTTP/Worker 冒烟:`python scripts/http_v833_tradepro.py`。 +可直接打开 `examples/V8_33_五套主题真实页面_离线交互预览.html`。 + +## 尚未完成 / 下阶段 +- 上传计划所述 `D:\project\cms` 原始 Astro 工程不在交付包,像素级还原无法完成正式对照。 +- 企业真实完整内容(例如 193 条样本)及原站链接、媒体、SEO 差异尚未做完整业务验收;未从上传包直接移植数据库或企业数据。 +- Art Design Pro 官方工程完整编译、真实 PostgreSQL/Redis 多 Worker、正式服务器部署/回滚/权限 E2E、Web Vitals 真实网络性能仍未验收。 +- Trade Pro 专用 blocks.css 现沿用平台稳定规则加少量语义化扩展,设计精细度需按 V8.34 再完善;离线演示使用虚构数据且禁用 iframe 内脚本。 + +**严格遵守:不自动替换已有网站主题,不覆盖固定历史版本,不改写导入 URL/SEO,不把技术功能隐藏为无法理解的操作。** diff --git a/README_V8_33_1.md b/README_V8_33_1.md new file mode 100644 index 0000000..9db5ecb --- /dev/null +++ b/README_V8_33_1.md @@ -0,0 +1,33 @@ +# SiteForge Enterprise V8.33.1 — HESU PIPE 首页布局专项修复 + +**基线**:V8.33.0-dev.1。**本补丁版本**:V8.33.1-dev.1。**工业主题**:1.3.0 → 1.3.1。 + +## 修复原因(已通过实际构建确认) + +1. 原渲染结果同时具有 `
` 和内部 `
`。共有 CSS 将二者都设为网格,导致整个 section 自身出现多列,内容宽度被压缩。同样的问题影响 `.sf-features`、`.sf-stats` 和部分其他区块。 +2. 浏览器运行时代码会对所有 `.sf-collection` 插入筛选工具栏,导致首页展示区也出现产品筛选,并可能重复作用于外层 section。 +3. 工业建材主题缺少与其页面视觉一致的核心优势和产品展示专用样式,卡片列数、图片比例与文字高度在不同设备下不能合理协调。 + +## 实际代码调整 + +- `app/static/blocks.css`:禁止区块外层误套内部 Grid 布局;维持内部网格。 +- `app/blueprints.py`:核心优势根据 1~4 条目选择相应桌面列数;动态列表根据页面用途决定是否显示筛选,首页只呈现产品卡片。 +- `app/static/site-runtime.js`:仅对 `data-sf-filterable="true"` 的内部网格启用筛选。 +- `themes/industrial/blocks.css`:独立工业主题卡片样式,适配 1440/820/390/375px;产品图片采用 `object-fit:contain` 不裁切管材。 +- `themes/industrial/manifest.json`:升级至 `1.3.1`;`themes/industrial/versions/1.3.0/` 保存原 1.3.0 完整主题文件。 +- `tests/test_hesupipe_layout_v8331.py`:增加 4 项专项测试;升级已硬编码主题版本号的 3 个历史测试模块。 + +## 用户端如何查看 + +在 SiteForge 后台找到 HESU PIPE 对应站点,检查其主题是否为 **工业制造 / Industrial**,然后在主题管理中选择新版本 **1.3.1**(这一步不自动修改网站内容)。进入首页可视化编辑器,确认“核心优势”和“动态内容列表”区块已经填好内容。保存草稿,再进行主题预览与构建。经预览检查、审批和部署完成后,线上才会更新。 + +> 旧站如固定 1.3.0,将继续使用原始布局。主题版本升级须由使用者明确选择,不直接修改现有站点或在线数据。 + +## 离线预览、验证范围 + +- `examples/V8_33_1_HesuPipe_首页布局真实构建预览.html`:使用真正的静态构建 HTML/CSS/JS,合成的示范站点(非线上实际数据),支持电脑、平板、手机宽度切换。 +- `reports/v8331/hesupipe-home-blocks-1440.png` 和 `...-390.png`:实际 Chromium 浏览器截图。 +- `reports/v8331/browser-results.json`:四种屏宽的 computed CSS 验证。 +- `scripts/hesupipe_v8331_preview.py`、`scripts/hesupipe_v8331_browser.py`:可复现的构建和浏览器核验脚本。 + +**限制**:本次未取得 hesupipe.com 线上部署权限,也没有用户后台中的真实首页草稿和照片。公开的 hesupipe.com 当前为不同的已发布网站;本补丁针对 SiteForge 项目版本的组件渲染,不代表线上网站已更新。请在测试环境先检查真实图片、数据与移动端,再走现有审批发布流程。 diff --git a/app/blueprints.py b/app/blueprints.py index dd9a451..7bd9630 100644 --- a/app/blueprints.py +++ b/app/blueprints.py @@ -254,7 +254,9 @@ def render_blocks(blocks, site, page, entries): elif typ=='features': items=block.get('items') if isinstance(block.get('items'),list) else [] cards=''.join(f'
{i+1:02}

{escape(str(item.get("title","")))}

{escape(str(item.get("text","")))}

' for i,item in enumerate(items[:12]) if isinstance(item,dict)) - parts.append(start+f'

{title}

{cards}
'+end) + visible_items=sum(1 for item in items[:12] if isinstance(item,dict)) + feature_cols=visible_items if 1<=visible_items<=4 else 3 + parts.append(start+f'

{title}

{cards}
'+end) elif typ=='stats': items=block.get('items') if isinstance(block.get('items'),list) else [] cards=''.join(f'
{escape(str(item.get("value","")))}{escape(str(item.get("label","")))}
' for item in items[:12] if isinstance(item,dict)) @@ -269,7 +271,10 @@ def render_blocks(blocks, site, page, entries): bound=[r for r in bound if brand_of(r)==str(page.get('title','')).casefold().strip()] count=max(1,min(48,int(block.get('limit') or 6)));cols=max(2,min(4,int(block.get('columns') or 3))) cards=''.join(f'
'+(f'{escape(r.get(' if safe_media(r.get('cover')) else '
'+escape(r.get('title','')[:2])+'
')+f'
{escape(r.get("type",""))}

{escape(r.get("title",""))}

{escape((r.get("summary") or "")[:140])}

查看详情 →
' for r in bound[:count]) - parts.append(start+f'
{cards if cards else "

当前暂无相关内容,请导入或添加数据。

"}
'+end) + # Homepage highlights are not catalogues: avoid injecting search/filters before featured cards. + # Other listing pages may opt into filters for sufficiently large result sets. + filterable=(page.get('route')!='/' and (page.get('type') in ('category','brand') or count>=9)) + parts.append(start+f'
{cards if cards else "

当前暂无相关内容,请导入或添加数据。

"}
'+end) elif typ=='image_text': im=safe_media(resolve('image')); img=f'{title}' if im else '
✧
' side=' sf-reverse' if block.get('image_side')=='left' else '' diff --git a/app/factory_api.py b/app/factory_api.py index 054f47f..e12ddc5 100644 --- a/app/factory_api.py +++ b/app/factory_api.py @@ -269,6 +269,16 @@ def theme_preview_css(theme_id:str,version:str=''): except ValueError as exc:raise HTTPException(404,str(exc)) return Response((dir/'theme.css').read_text(encoding='utf-8'),media_type='text/css') +@router.get('/themes/{theme_id}/preview.blocks.css') +def theme_preview_blocks_css(theme_id:str,version:str=''): + """Serve the identical block stylesheet used by a static theme build.""" + from fastapi.responses import Response + try:folder=theme_store.resolve(theme_id,version or None) + except ValueError as exc:raise HTTPException(404,str(exc)) + chosen=folder/'blocks.css' + if not chosen.is_file():chosen=theme_store.BASE.parent/'app'/'static'/'blocks.css' + return Response(chosen.read_text(encoding='utf-8'),media_type='text/css',headers={'Cache-Control':'no-store'}) + @router.get('/themes/{theme_id}/preview.pro.css') def theme_preview_pro_css(theme_id:str,version:str=''): from fastapi.responses import Response @@ -367,6 +377,6 @@ def theme_preview(theme_id:str,version:str='',page_type:str='home'): seo_description='SiteForge 主题模板预览') html=static_site.shell(html,site,page,records,[]) html=html.replace('assets/theme.css',f'/api/factory/themes/{theme_id}/preview.css?version={version or theme_store.latest(theme_id)}') - html=html.replace('assets/blocks.css','/ui/blocks.css').replace('assets/factory.css','/ui/factory.css') + html=html.replace('assets/blocks.css',f'/api/factory/themes/{theme_id}/preview.blocks.css?version={version or theme_store.latest(theme_id)}').replace('assets/factory.css','/ui/factory.css') html=html.replace('assets/custom.css','/ui/factory.css').replace('assets/site-runtime.js','/ui/site-runtime.js') return HTMLResponse(html) diff --git a/app/main.py b/app/main.py index 7a031d0..358fc63 100644 --- a/app/main.py +++ b/app/main.py @@ -540,7 +540,7 @@ def editor_preview(site_id:str,entry_id:str,payload:BlueprintPayload): styles='\n'.join([ (ROOT/'app'/'static'/'pro-base.css').read_text(encoding='utf-8'), (ROOT/'app'/'static'/'commercial.css').read_text(encoding='utf-8'), - (ROOT/'app'/'static'/'blocks.css').read_text(encoding='utf-8'), + ((theme_dir/'blocks.css') if (theme_dir/'blocks.css').is_file() else (ROOT/'app'/'static'/'blocks.css')).read_text(encoding='utf-8'), (ROOT/'app'/'static'/'factory.css').read_text(encoding='utf-8'), (theme_dir/'pro.css').read_text(encoding='utf-8') if (theme_dir/'pro.css').exists() else '', (theme_dir/'theme.css').read_text(encoding='utf-8'), diff --git a/app/multilingual.py b/app/multilingual.py index 3a67dd8..f7a41ca 100644 --- a/app/multilingual.py +++ b/app/multilingual.py @@ -97,7 +97,7 @@ def set_site_locale(site_id:str,data:LocaleIn,request:Request): enterprise.require(request,site_id,'owner') try:locale=locale_value(data.locale) except ValueError as exc:raise HTTPException(400,str(exc)) - site=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,)) + site=db.one('SELECT settings_json,theme FROM sites WHERE id=?',(site_id,)) if not site:raise HTTPException(404,'网站不存在') settings=json.loads(site['settings_json'] or '{}') settings['locale']=locale @@ -195,12 +195,13 @@ def site_locale(site_id): row=db.one('SELECT locale FROM language_sites WHERE site_id=?',(site_id,)) if row:return row['locale'] # Per-site locale override (e.g. standalone English sites outside any language group) - site=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,)) + site=db.one('SELECT settings_json,theme FROM sites WHERE id=?',(site_id,)) try:override=json.loads(site['settings_json'] or '{}').get('locale') if site else None except ValueError:override=None if override: try:return locale_value(override) except ValueError:pass + if site and site['theme']=='trade-pro':return 'en' return 'zh-CN' def assert_build_safe(site_id): diff --git a/app/static/blocks.css b/app/static/blocks.css index 9ce1279..0f7dd27 100644 --- a/app/static/blocks.css +++ b/app/static/blocks.css @@ -27,3 +27,9 @@ .sf-map-address{font-style:normal;font-size:18px;white-space:pre-line;line-height:1.8} @media (prefers-reduced-motion:reduce){.sf-slides{scroll-behavior:auto}.sf-item{transition:none}} @media(max-width:540px){.sf-slide{flex-basis:92%}} + +/* V8.33.1 grid-class collision fix: block section and inner collection/features + share a class. The section must NOT become a grid container itself. + Restrict grid layout to its dedicated inner element. */ +.sf-section.sf-features,.sf-section.sf-collection,.sf-section.sf-stats,.sf-section.sf-gallery,.sf-section.sf-attributes{display:block;grid-template-columns:none} +.sf-section.sf-faq{max-width:none} diff --git a/app/static/editor.js b/app/static/editor.js index 559ae99..5ab0ec5 100644 --- a/app/static/editor.js +++ b/app/static/editor.js @@ -284,7 +284,7 @@ const Studio={ }, filterPalette(value){paletteSearch=String(value||'').slice(0,70);Studio.renderPalette()}, clearPaletteSearch(){paletteSearch='';const inp=$('#paletteSearch');if(inp){inp.value='';inp.focus()}Studio.renderPalette()}, - showThemePreview(){const dialog=$('#themePreviewModal');if(!dialog)return;const current=['automotive','industrial','corporate','minimal'].includes(site?.theme)?site.theme:'corporate';$('#themePreviewTheme').value=current;if(!dialog.open)dialog.showModal();Studio.themeDevice('desktop');Studio.updateThemePreview()}, + showThemePreview(){const dialog=$('#themePreviewModal');if(!dialog)return;const current=['automotive','industrial','corporate','minimal','trade-pro'].includes(site?.theme)?site.theme:'corporate';$('#themePreviewTheme').value=current;if(!dialog.open)dialog.showModal();Studio.themeDevice('desktop');Studio.updateThemePreview()}, closeThemePreview(){themePreviewSeq++;themePreviewAbort?.abort();const dialog=$('#themePreviewModal');if(dialog?.open)dialog.close();const frame=$('#themePreviewFrame');if(frame){frame.removeAttribute('src');frame.removeAttribute('srcdoc')}}, themeDevice(device){ const width={desktop:'100%',tablet:'780px',phone:'390px'}[device];if(!width)return; diff --git a/app/static/site-runtime.js b/app/static/site-runtime.js index e4715e8..1be7c91 100644 --- a/app/static/site-runtime.js +++ b/app/static/site-runtime.js @@ -3,7 +3,7 @@ const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)]; const menu=$('.sf-menu-toggle');if(menu){menu.addEventListener('click',()=>{let open=menu.getAttribute('aria-expanded')!=='true';menu.setAttribute('aria-expanded',String(open));$('#sf-primary-links')?.classList.toggle('is-open',open);});} // Filter actual static content cards, no API is needed at runtime. - $$('.sf-collection').forEach(grid=>{ + $$('.sf-collection[data-sf-filterable="true"]').forEach(grid=>{ const cards=$$('.sf-item',grid); if(cards.length<2)return; const toolbar=document.createElement('div');toolbar.className='sf-filters'; diff --git a/app/static/studio-experience-v829.js b/app/static/studio-experience-v829.js index 08ab7c4..8f94b32 100644 --- a/app/static/studio-experience-v829.js +++ b/app/static/studio-experience-v829.js @@ -1,7 +1,7 @@ /* SiteForge V8.29: presentation-only studio helpers. No persisted-schema changes. */ 'use strict'; (function(root){ - const themes=new Set(['automotive','industrial','corporate','minimal']); + const themes=new Set(['automotive','industrial','corporate','minimal','trade-pro']); const pages=new Set(['home','listing','product','article','about','contact']); const categories={ visual:new Set(['hero','banner','carousel','gallery','image','image_text','image-grid','video','slider']), @@ -29,7 +29,8 @@ automotive:['#ffffff','#14283d','#f3f6f9','#102e47'], industrial:['#ffffff','#193f59','#eef5f8','#104a6d'], corporate:['#ffffff','#262d58','#f4f3fb','#34397b'], - minimal:['#faf9f5','#292924','#f0efe9','#30302c'] + minimal:['#faf9f5','#292924','#f0efe9','#30302c'], + 'trade-pro':['#ffffff','#17365c','#eef4fc','#0b3d91'] }; function stylePreset(theme,preset){ const [base,ink,soft,dark]=designPalettes[themes.has(theme)?theme:'corporate']; diff --git a/app/templates/editor.html b/app/templates/editor.html index e3d2c39..da7192f 100644 --- a/app/templates/editor.html +++ b/app/templates/editor.html @@ -55,7 +55,7 @@
仅预览 · 不修改网站内容

选择适合网站的主题效果

先比较布局和手机显示效果,再决定是否到「网站设计」中应用主题。

-
+

示例使用主题最新版本,可能与你网站锁定的主题版本不同。不会修改当前页面。

diff --git a/app/templates/theme-gallery.html b/app/templates/theme-gallery.html index b60c680..d261c46 100644 --- a/app/templates/theme-gallery.html +++ b/app/templates/theme-gallery.html @@ -45,6 +45,6 @@ theme.addEventListener('change',loadVersions);[page,version].forEach(control=>co retry.addEventListener('click',show); document.querySelectorAll('.devices button').forEach(button=>button.addEventListener('click',()=>{document.querySelectorAll('.devices button').forEach(other=>other.classList.remove('active'));button.classList.add('active');frame.style.width=button.dataset.size;})); const requestedTheme=new URLSearchParams(location.search).get('theme'); -if(['automotive','industrial','corporate','minimal'].includes(requestedTheme))theme.value=requestedTheme; +if(['automotive','industrial','corporate','minimal','trade-pro'].includes(requestedTheme))theme.value=requestedTheme; loadVersions(); diff --git a/app/version.py b/app/version.py index 4e313e8..d050c45 100644 --- a/app/version.py +++ b/app/version.py @@ -1,5 +1,5 @@ """Single source of truth for the SiteForge platform release identity.""" import os -VERSION = "8.32.0-dev.1" +VERSION = "8.33.1-dev.1" PLATFORM_NAME = "SiteForge Factory" BUILD_SHA = os.environ.get("SITEFORGE_GIT_SHA", "unknown").strip()[:40] or "unknown" diff --git a/docs/V8_33_FUSION_ROADMAP.md b/docs/V8_33_FUSION_ROADMAP.md new file mode 100644 index 0000000..b20a261 --- /dev/null +++ b/docs/V8_33_FUSION_ROADMAP.md @@ -0,0 +1,23 @@ +# V8.33–V8.38 融合执行计划(以用户上传计划为依据) + +## 优先级与状态 + +| 阶段 | 项目 | 本阶段状态 | 下一步交付要求 | +|---|---|---|---| +| P0 | 安全回归、历史主题、数据隔离 | 本轮隔离测试通过 | 真实授权用户权限回归/发布演练 | +| P1 | 主题管理与平台底座 | 五主题主线注册/预览/构建已集成 | 主题自动化升级、资源缓存基准 | +| P2 | WordPress 完整迁移 | 既有能力保留但未在真实站验收 | 真实 XML/媒体/SEO/旧路径/重定向差异表 | +| P3 | 数据装配 | 既有字段映射保留 | 真实 Excel 车型/品牌/产品映射误差验收 | +| P4 | 主题与高级可视化编辑 | V8.32 简洁编辑保留、第五主题接入 | V8.34 图片/Banner/菜单/规格直接编辑,预览→构建对照 | +| P5 | 审批、构建、部署、回滚 | HTTP+Worker 冒烟通过 | V8.37 真实 SFTP/S3/Agent 部署与回滚 | +| P6 | 多语言与 SEO | 语言组+站点默认语言保留 | V8.36 阿语/俄语/英文页面及 hreflang 校验 | +| P7 | 企业级生产化 | 尚未验收 | V8.38+ PG/Redis/多 Worker、Art Design Pro 官方工程整合、安全压测 | + +## V8.34 下一轮重点 +1. 五套主题的页面完整性与行业差异,重点将 Trade Pro 14 类页面用于真实内容而非样例。 +2. 可视化编辑中的图片、Banner、导航、产品参数直接修改;默认简洁、按需高级设置。 +3. 同一网站同一版本的主题预览、编辑器和静态构建画面对照测试;旧站已固定主题不变。 +4. 指标:CSS/JS 体积、图片宽高与懒加载、LCP/CLS/INP 实网采集;语义化图标不得到处使用箭头。 +5. 真实网站迁移需保存 URL、Title、Description、Canonical、结构化数据、媒体路径,输出差异表。 + +本文件是更新后的主线计划。`docs/COMPLETION_PLAN.md` 是用户上传的历史原始规划,包含过时版本和本地路径,必须以最新源码、测试和实际环境验收为准;不能把旧计划中勾选项目直接算作本轮已完成。 diff --git a/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md b/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md index 6041b13..5188afa 100644 --- a/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md +++ b/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md @@ -399,7 +399,9 @@ def test_theme_color_map_covers_trade_pro(): 4. noindex 空栏目:填真实内容或从导航移除,不留空页 5. 生产部署包只含构建产物与运行所需文件(不带 tests/examples/reports/demo) -## 补丁登记(2026-10-23 V8_21_3 升级后现行清单,下次升级重放) +## 补丁登记(已退役存档,2026-10-26 终局合并) + +V8.33.0 起上游原生吸收了我们的全部平台补丁(主题发现/env_for 宏覆盖/blocks.css 优先/类型修复/前端入口/locale 覆盖,trade-pro 主题同步自我们的 Git 仓库)——**补丁重放时代结束**,此后升级=纯覆盖+验证原生实现+compose 钉名(siteforge-v8-10 项目名 + Web 侧 BUILD_TIMEOUT 透传)+双基线回归。历史清单见 git 历史(601451f/8884e10/95982fa)。 1. `app/core.py` discover_themes() 单一源;2. `app/theme_pro.py` env_for + theme_color + THEMES import;3. `app/theme_store.py` BUILTIN import;4. `app/builder.py` copy_blocks_css;5. `app/v8_workflows.py` 类型/分类;6. `app/multilingual.py` 站点级 locale 覆盖 + `PUT /api/i18n/sites/{id}/locale`(V8.15 locale 改写管线下英文站的正解);7. 前端 6 入口 + workspace.css 预览色;8. `tests/test_theme_pro_v3.py` 三处;9. compose 项目名钉 siteforge-v8-10 + .gitignore 追加;10. 保留目录:themes/trade-pro、tests/test_theme_registry.py、docs/superpowers。 diff --git a/examples/V8_33_1_HesuPipe_首页布局真实构建预览.html b/examples/V8_33_1_HesuPipe_首页布局真实构建预览.html new file mode 100644 index 0000000..c01f1df --- /dev/null +++ b/examples/V8_33_1_HesuPipe_首页布局真实构建预览.html @@ -0,0 +1 @@ +HesuPipe · 首页核心优势与产品列表
HESU PIPE · 核心优势 / 动态内容列表V8.33.1 从实际静态构建生成的离线页面 · 示范文字与占位产品图
仅检查预览 · 不连接线上网站 \ No newline at end of file diff --git a/examples/V8_33_五套主题真实页面_离线交互预览.html b/examples/V8_33_五套主题真实页面_离线交互预览.html new file mode 100644 index 0000000..713907e --- /dev/null +++ b/examples/V8_33_五套主题真实页面_离线交互预览.html @@ -0,0 +1,5 @@ +SiteForge V8.33 · 五主题对比预览

SiteForge · 五套主题实际页面

V8.33 融合验证 · 14 类页面 · 电脑 / 平板 / 手机

本地交互演示 · 不修改真实网站

使用真实主题的模板和样式生成预览,内容为演示数据;本演示禁用画布内脚本,业务交互应在真实站点验收。

\ No newline at end of file diff --git a/examples/v8331-real-built-page.html b/examples/v8331-real-built-page.html new file mode 100644 index 0000000..d178c45 --- /dev/null +++ b/examples/v8331-real-built-page.html @@ -0,0 +1,501 @@ +HESU PIPE · 排版验证样站跳转至正文 + +
ENGINEERING · MATERIALS · SUPPLYINDUSTRIAL SOLUTIONS
+
+
ENGINEERED FOR WHAT'S NEXT / 001

HESU PIPE · 排版验证样站 BUILT TO PERFORM.

工业管道与工程材料 · 完整产品方案

FORM / FUNCTION
+ + +
APPLICATIONS & SUPPORT

专注可靠交付,服务多元项目。

工业管道与工程材料 · 完整产品方案

服务与解决方案 ↗
+ + + +

核心优势

01

管道产品供应

覆盖多种工程管材和配件,便于项目集中采购。

02

规格与质量管理

根据实际应用提供产品选型和规格资料。

03

工程应用支持

面向供水、排水及工业管网项目的应用场景。

04

全球交付服务

从订单沟通到物流交付,提供清晰的合作流程。

+
LET'S CONNECT

从这里开始下一步。

工业管道与工程材料 · 完整产品方案

联系我们 ↗
+ + + + \ No newline at end of file diff --git a/reports/v833-regression/result.json b/reports/v833-regression/result.json new file mode 100644 index 0000000..c95aaa6 --- /dev/null +++ b/reports/v833-regression/result.json @@ -0,0 +1,215 @@ +{ + "version": "8.33.1-dev.1", + "generated_utc": "2026-10-26T05:43:38.989499+00:00", + "groups": [ + { + "batch": 1, + "files": [ + "test_commercial_v5.py", + "test_connectors_v84.py", + "test_connectors_v85.py" + ], + "status": "passed", + "seconds": 6.15, + "summary": "................................... [100%]\n35 passed in 3.85s\n\n" + }, + { + "batch": 2, + "files": [ + "test_console_v819.py", + "test_core_v4.py", + "test_deep_nested_v824.py" + ], + "status": "passed", + "seconds": 6.24, + "summary": "........................... [100%]\n27 passed in 3.81s\n\n" + }, + { + "batch": 3, + "files": [ + "test_editor_easy_v8212.py", + "test_editor_ux_v8211.py", + "test_enterprise.py" + ], + "status": "passed", + "seconds": 4.35, + "summary": "........... [100%]\n11 passed in 2.05s\n\n" + }, + { + "batch": 4, + "files": [ + "test_enterprise_v7.py", + "test_factory.py", + "test_factory_v2.py" + ], + "status": "passed", + "seconds": 8.37, + "summary": "................................... [100%]\n35 passed in 5.92s\n\n" + }, + { + "batch": 5, + "files": [ + "test_hesupipe_layout_v8331.py", + "test_mobile_tabs_v8213.py", + "test_multilingual_v815.py" + ], + "status": "passed", + "seconds": 6.84, + "summary": "........................ [100%]\n24 passed in 4.35s\n\n" + }, + { + "batch": 6, + "files": [ + "test_nested_v823.py", + "test_p0_71.py", + "test_p0_backup.py" + ], + "status": "passed", + "seconds": 3.49, + "summary": "......... [100%]\n9 passed in 1.06s\n\n" + }, + { + "batch": 7, + "files": [ + "test_p0_p4_completion_v88.py", + "test_p0_p4_completion_v89.py", + "test_p1_platform.py" + ], + "status": "passed", + "seconds": 7.66, + "summary": "................................ [100%]\n32 passed in 5.37s\n\n" + }, + { + "batch": 8, + "files": [ + "test_production_v816.py", + "test_professional_ui_v811.py", + "test_regression_runner_v817.py" + ], + "status": "passed", + "seconds": 4.9, + "summary": "................... [100%]\n19 passed in 2.46s\n\n" + }, + { + "batch": 9, + "files": [ + "test_release_native_v814.py", + "test_release_reviews_v7.py", + "test_release_v813.py" + ], + "status": "passed", + "seconds": 4.0, + "summary": "..................... [100%]\n21 passed in 1.57s\n\n" + }, + { + "batch": 10, + "files": [ + "test_site_design_v87.py", + "test_siteforge_v817_audit.py", + "test_studio_v86.py" + ], + "status": "passed", + "seconds": 6.38, + "summary": "............................................. [100%]\n45 passed in 4.09s\n\n" + }, + { + "batch": 11, + "files": [ + "test_theme_pro_v3.py", + "test_theme_registry.py", + "test_v818_pg_recovery.py" + ], + "status": "passed", + "seconds": 12.3, + "summary": ".......................................... [100%]\n42 passed in 9.88s\n\n" + }, + { + "batch": 12, + "files": [ + "test_v818_sqlite_pg_transfer.py", + "test_v821_editor_history_and_mapping.py", + "test_v822_canvas.py" + ], + "status": "passed", + "seconds": 3.74, + "summary": "............ [100%]\n12 passed in 1.37s\n\n" + }, + { + "batch": 13, + "files": [ + "test_v825_shared_components.py", + "test_v826_global_design.py", + "test_v827_guided_workspace.py" + ], + "status": "passed", + "seconds": 4.77, + "summary": "................... [100%]\n19 passed in 2.40s\n\n" + }, + { + "batch": 14, + "files": [ + "test_v828_launch_center.py", + "test_v829_theme_studio.py", + "test_v830_real_pages.py" + ], + "status": "passed", + "seconds": 9.52, + "summary": "........... [100%]\n11 passed in 7.42s\n\n" + }, + { + "batch": 15, + "files": [ + "test_v8311_performance_icons.py", + "test_v831_automotive_ref.py", + "test_v832_preview_easy_editor.py" + ], + "status": "passed", + "seconds": 5.72, + "summary": ".......... [100%]\n10 passed in 3.59s\n\n" + }, + { + "batch": 16, + "files": [ + "test_v833_theme_integration.py", + "test_v8_art_installer.py", + "test_v8_workflows.py" + ], + "status": "passed", + "seconds": 3.98, + "summary": ".................... [100%]\n20 passed in 1.84s\n\n" + }, + { + "batch": 17, + "files": [ + "test_wordpress_baseline.py", + "test_worker_queue_v6.py", + "test_workspace_v810.py" + ], + "status": "passed", + "seconds": 6.93, + "summary": "................ [100%]\n16 passed in 4.65s\n\n" + }, + { + "batch": 18, + "files": [ + "test_workspace_v812.py", + "test_wp_complex_v820.py", + "test_wp_migration_v81.py" + ], + "status": "passed", + "seconds": 4.39, + "summary": "............................... [100%]\n31 passed in 2.21s\n\n" + }, + { + "batch": 19, + "files": [ + "test_wp_migration_v82.py", + "test_wp_migration_v83.py" + ], + "status": "passed", + "seconds": 5.67, + "summary": ".................... [100%]\n20 passed in 3.53s\n\n" + } + ], + "all_passed": true +} \ No newline at end of file diff --git a/reports/v833/V8_33_五套主题融合验收.html b/reports/v833/V8_33_五套主题融合验收.html new file mode 100644 index 0000000..d48702b --- /dev/null +++ b/reports/v833/V8_33_五套主题融合验收.html @@ -0,0 +1 @@ +SiteForge V8.33 融合验收

SiteForge Enterprise V8.33 验收

五主题安全融合 · 基线 V8.32 · 版本 8.33.0-dev.1 · 2026-10-26

435Python 测试通过
19/19独立测试批次
39/39JavaScript 用例
通过Trade Pro HTTP + Worker

本轮实际集成

  • 第五套 Trade Pro(14 页面类型与页面预设)加入统一主题发现、安装、主题预览与编辑器选项。
  • 内置主题组件宏优先、共享宏回退;区块 CSS 的构建与预览路径一致。
  • 英语站点默认 locale 与站点级语言字段,保留用户自定义 SEO 与 URL。
  • 原四主题历史版本、V8.32 简洁编辑器及主题预览嵌入安全策略保持。

视觉测试范围

隔离 Chromium 预览:1365px 桌面和 390px 手机;无页面外层横向溢出,无脚本错误。该结果不代表真实服务器性能或真实后台完整 E2E。

证据:reports/v833/trade-pro-desktop.png、reports/v833/trade-pro-phone.png、reports/v833/browser-results.json。

历史回归逐批记录

批次测试模块状态通过用例用时
01test_commercial_v5.py, test_connectors_v84.py, test_connectors_v85.pypassed356.5s
02test_console_v819.py, test_core_v4.py, test_deep_nested_v824.pypassed276.4s
03test_editor_easy_v8212.py, test_editor_ux_v8211.py, test_enterprise.pypassed114.1s
04test_enterprise_v7.py, test_factory.py, test_factory_v2.pypassed357.7s
05test_mobile_tabs_v8213.py, test_multilingual_v815.py, test_nested_v823.pypassed245.5s
06test_p0_71.py, test_p0_backup.py, test_p0_p4_completion_v88.pypassed234.5s
07test_p0_p4_completion_v89.py, test_p1_platform.py, test_production_v816.pypassed216.3s
08test_professional_ui_v811.py, test_regression_runner_v817.py, test_release_native_v814.pypassed194.5s
09test_release_reviews_v7.py, test_release_v813.py, test_site_design_v87.pypassed275.8s
10test_siteforge_v817_audit.py, test_studio_v86.py, test_theme_pro_v3.pypassed387.4s
11test_theme_registry.py, test_v818_pg_recovery.py, test_v818_sqlite_pg_transfer.pypassed407.8s
12test_v821_editor_history_and_mapping.py, test_v822_canvas.py, test_v825_shared_components.pypassed184.5s
13test_v826_global_design.py, test_v827_guided_workspace.py, test_v828_launch_center.pypassed133.8s
14test_v829_theme_studio.py, test_v830_real_pages.py, test_v8311_performance_icons.pypassed109.5s
15test_v831_automotive_ref.py, test_v832_preview_easy_editor.py, test_v833_theme_integration.pypassed135.4s
16test_v8_art_installer.py, test_v8_workflows.py, test_wordpress_baseline.pypassed203.4s
17test_worker_queue_v6.py, test_workspace_v810.py, test_workspace_v812.pypassed197.8s
18test_wp_complex_v820.py, test_wp_migration_v81.py, test_wp_migration_v82.pypassed326.5s
19test_wp_migration_v83.pypassed103.7s

未验收事项

  • 原始 Astro 设计源不在压缩包,Trade Pro 视觉未进行正式像素对照。
  • 真实 WordPress/企业内容全面迁移、生产 PG/Redis、多 Worker、Art Design Pro 官方工程以及正式远程发布回滚未验收。
  • 尚未进行真实网络 Web Vitals、长期负载与移动设备实机测试。

详见 README_V8_33.md 和 docs/V8_33_FUSION_ROADMAP.md。

\ No newline at end of file diff --git a/reports/v833/browser-results.json b/reports/v833/browser-results.json new file mode 100644 index 0000000..fcdeefc --- /dev/null +++ b/reports/v833/browser-results.json @@ -0,0 +1,18 @@ +[ + { + "device": "desktop", + "width": 1365, + "theme_body_found": 1, + "horizontal_overflow": false, + "script_errors": [], + "screenshot": "reports/v833/trade-pro-desktop.png" + }, + { + "device": "phone", + "width": 390, + "theme_body_found": 1, + "horizontal_overflow": false, + "script_errors": [], + "screenshot": "reports/v833/trade-pro-phone.png" + } +] \ No newline at end of file diff --git a/reports/v833/trade-pro-desktop.png b/reports/v833/trade-pro-desktop.png new file mode 100644 index 0000000..3faaed2 Binary files /dev/null and b/reports/v833/trade-pro-desktop.png differ diff --git a/reports/v833/trade-pro-phone.png b/reports/v833/trade-pro-phone.png new file mode 100644 index 0000000..b580fb5 Binary files /dev/null and b/reports/v833/trade-pro-phone.png differ diff --git a/reports/v8331/V8_33_1_HesuPipe_首页布局修复验收.html b/reports/v8331/V8_33_1_HesuPipe_首页布局修复验收.html new file mode 100644 index 0000000..3ee635a --- /dev/null +++ b/reports/v8331/V8_33_1_HesuPipe_首页布局修复验收.html @@ -0,0 +1 @@ +SiteForge V8.33.1 · HesuPipe 首页布局验收

HESU PIPE · 首页布局专项修复

SiteForge Enterprise V8.33.1 · 核心优势 + 动态内容列表 · 真实静态构建、响应式布局、安全版本隔离

439Python 回归测试通过
19/19独立回归批次通过
4Chromium 屏幕宽度验证

根本原因与解决方式

问题具体原因修复
外层内容缩窄外层 section 和内部 Grid 使用相同的 sf-features / sf-collection 类名,CSS 把外层也设成 Grid外层强制恢复 block 布局,内层才负责网格
优势卡片参差固定三列,四张卡片最后一张另起一行工业主题根据数量自适应,桌面 4 列 / 平板 2 列 / 手机 1 列
首页产品区拥挤所有动态列表均被自动添加搜索筛选条,布局重复命中外层区块首页仅展示精选内容;目录页仍支持过滤
卡片错位产品图、标题与摘要高度不一致等高卡片、4:3 图片区域、完整产品图 object-fit: contain、行数限制

实际 Chromium 页面截图

电脑版优势卡片与产品列表
电脑版 1440px:优势四列,精选产品三列
手机版优势与产品单列排布
手机 390px:单列布局,无横向溢出

浏览器布局验证

宽度优势产品卡片首页筛选栏JS 异常 / 横向溢出
1440px4 列3 列0无 / 无
820px2 列2 列0无 / 无
390px1 列1 列0无 / 无
375px1 列1 列0无 / 无

验证结果及交付范围

19 个隔离批次通过,439 项 Python 测试通过,包含新增的 4 项 HesuPipe 专项测试。`verify_release.py`、JavaScript 语法检查、HTTP + Worker 构建 → ZIP → 页面预览链路通过。可用的历史工业主题 1.3.0 独立存档,不自动影响已固定旧版的网站。

项目仍保留完整五主题平台、已有内容数据模型、编辑器、迁移与发布流程;本次只修复区块布局和工业主题卡片样式。

使用范围:样例为 SiteForge 构建的测试站,使用示例产品名称与图片占位符,未连接真实 HesuPipe 数据;尚未部署到 hesupipe.com。请先在 SiteForge 主题管理中手动选择工业主题 v1.3.1,预览实际草稿并走原有审批发布流程。
SiteForge Enterprise V8.33.1-dev.1 · HESU PIPE 首页组件布局专项 · 2026-10-26
\ No newline at end of file diff --git a/reports/v8331/browser-results.json b/reports/v8331/browser-results.json new file mode 100644 index 0000000..f78e8e9 --- /dev/null +++ b/reports/v8331/browser-results.json @@ -0,0 +1,170 @@ +[ + { + "width": 1440, + "screenshot": "hesupipe-home-blocks-1440.png", + "facts": { + "bodyScroll": 1440, + "viewport": 1440, + "featuresOuter": { + "x": 0, + "width": 1440, + "display": "block", + "cols": "none" + }, + "listOuter": { + "x": 0, + "width": 1440, + "display": "block", + "cols": "none" + }, + "featuresInner": { + "x": 107, + "width": 1226, + "display": "grid", + "cols": "293px 293px 293px 293px" + }, + "listInner": { + "x": 107, + "width": 1226, + "display": "grid", + "cols": "394.656px 394.672px 394.656px" + }, + "featureCount": 4, + "productCount": 6, + "filterCount": 0, + "module": { + "x": 0, + "width": 1440, + "display": "block", + "cols": "none" + } + }, + "script_errors": [] + }, + { + "width": 820, + "screenshot": "hesupipe-home-blocks-820.png", + "facts": { + "bodyScroll": 820, + "viewport": 820, + "featuresOuter": { + "x": 0, + "width": 820, + "display": "block", + "cols": "none" + }, + "listOuter": { + "x": 0, + "width": 820, + "display": "block", + "cols": "none" + }, + "featuresInner": { + "x": 24, + "width": 772, + "display": "grid", + "cols": "377px 377px" + }, + "listInner": { + "x": 24, + "width": 772, + "display": "grid", + "cols": "375.5px 375.5px" + }, + "featureCount": 4, + "productCount": 6, + "filterCount": 0, + "module": { + "x": 0, + "width": 820, + "display": "block", + "cols": "none" + } + }, + "script_errors": [] + }, + { + "width": 390, + "screenshot": "hesupipe-home-blocks-390.png", + "facts": { + "bodyScroll": 390, + "viewport": 390, + "featuresOuter": { + "x": 0, + "width": 390, + "display": "block", + "cols": "none" + }, + "listOuter": { + "x": 0, + "width": 390, + "display": "block", + "cols": "none" + }, + "featuresInner": { + "x": 18, + "width": 354, + "display": "grid", + "cols": "354px" + }, + "listInner": { + "x": 18, + "width": 354, + "display": "grid", + "cols": "354px" + }, + "featureCount": 4, + "productCount": 6, + "filterCount": 0, + "module": { + "x": 0, + "width": 390, + "display": "block", + "cols": "none" + } + }, + "script_errors": [] + }, + { + "width": 375, + "screenshot": "hesupipe-home-blocks-375.png", + "facts": { + "bodyScroll": 375, + "viewport": 375, + "featuresOuter": { + "x": 0, + "width": 375, + "display": "block", + "cols": "none" + }, + "listOuter": { + "x": 0, + "width": 375, + "display": "block", + "cols": "none" + }, + "featuresInner": { + "x": 18, + "width": 339, + "display": "grid", + "cols": "339px" + }, + "listInner": { + "x": 18, + "width": 339, + "display": "grid", + "cols": "339px" + }, + "featureCount": 4, + "productCount": 6, + "filterCount": 0, + "module": { + "x": 0, + "width": 375, + "display": "block", + "cols": "none" + } + }, + "script_errors": [] + } +] \ No newline at end of file diff --git a/reports/v8331/hesupipe-home-blocks-1440.png b/reports/v8331/hesupipe-home-blocks-1440.png new file mode 100644 index 0000000..7a62b79 Binary files /dev/null and b/reports/v8331/hesupipe-home-blocks-1440.png differ diff --git a/reports/v8331/hesupipe-home-blocks-375.png b/reports/v8331/hesupipe-home-blocks-375.png new file mode 100644 index 0000000..b1fc82d Binary files /dev/null and b/reports/v8331/hesupipe-home-blocks-375.png differ diff --git a/reports/v8331/hesupipe-home-blocks-390.png b/reports/v8331/hesupipe-home-blocks-390.png new file mode 100644 index 0000000..573bcef Binary files /dev/null and b/reports/v8331/hesupipe-home-blocks-390.png differ diff --git a/reports/v8331/hesupipe-home-blocks-820.png b/reports/v8331/hesupipe-home-blocks-820.png new file mode 100644 index 0000000..64648e0 Binary files /dev/null and b/reports/v8331/hesupipe-home-blocks-820.png differ diff --git a/reports/v8331/test-summary.json b/reports/v8331/test-summary.json new file mode 100644 index 0000000..e3b176e --- /dev/null +++ b/reports/v8331/test-summary.json @@ -0,0 +1,15 @@ +{ + "release": "8.33.1-dev.1", + "industrial": "1.3.1", + "python_total_passed": 439, + "regression_batches": 19, + "all_batches_passed": true, + "browser_viewports": [ + 1440, + 820, + 390, + 375 + ], + "http_worker": "pass", + "caveat": "sample data; not production hesupipe.com" +} \ No newline at end of file diff --git a/scripts/browser_v833_preview.py b/scripts/browser_v833_preview.py new file mode 100644 index 0000000..1aa079f --- /dev/null +++ b/scripts/browser_v833_preview.py @@ -0,0 +1,33 @@ +"""Isolated Chromium smoke of shipped standalone theme comparison; no real server.""" +import asyncio,json +from pathlib import Path +from playwright.async_api import async_playwright +ROOT=Path(__file__).resolve().parents[1];REPORT=ROOT/'reports/v833';REPORT.mkdir(parents=True,exist_ok=True) +async def main(): + async with async_playwright() as p: + browser=await p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage']) + records=[] + for device,width in (('desktop',1365),('phone',390)): + page=await browser.new_page(viewport={'width':width,'height':850},device_scale_factor=1) + errs=[];page.on('pageerror',lambda e:errs.append(str(e))) + html=(ROOT/'examples/V8_33_五套主题真实页面_离线交互预览.html').read_text(encoding='utf-8') + await page.set_content(html,wait_until='load',timeout=60000) + await page.select_option('#theme','trade-pro') + await page.select_option('#page','product') + await page.frame_locator('#preview').locator('.sf-tp-product-hero').wait_for(state='visible',timeout=12000) + await page.wait_for_timeout(1600) + if device=='phone':await page.locator('[data-width="390px"]').click() + title=await page.title() + count=await page.frame_locator('#preview').locator('.pro-trade-pro').count() + overflow=await page.evaluate('document.documentElement.scrollWidth>window.innerWidth') + img=REPORT/f'trade-pro-{device}.png' + await page.screenshot(path=str(img),full_page=True) + assert count>0,(device,'theme body missing') + assert not overflow,(device,'overflow') + assert not errs,(device,errs) + records.append({'device':device,'width':width,'theme_body_found':count,'horizontal_overflow':overflow,'script_errors':errs,'screenshot':str(img.relative_to(ROOT))}) + await page.close() + await browser.close() + (REPORT/'browser-results.json').write_text(json.dumps(records,ensure_ascii=False,indent=2),encoding='utf-8') + print('Chromium PASS:', records) +if __name__=='__main__':asyncio.run(main()) diff --git a/scripts/hesupipe_v8331_browser.py b/scripts/hesupipe_v8331_browser.py new file mode 100644 index 0000000..5912a29 --- /dev/null +++ b/scripts/hesupipe_v8331_browser.py @@ -0,0 +1,46 @@ +"""Chromium isolated visual check for industrial features and collection.""" +import json +from pathlib import Path +from playwright.sync_api import sync_playwright +root=Path(__file__).resolve().parents[1] +pagehtml=(root/'examples/v8331-real-built-page.html').read_text() +dest=root/'reports/v8331';dest.mkdir(parents=True,exist_ok=True) +rows=[] +with sync_playwright() as p: + browser=p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage']) + for width in [1440,820,390,375]: + page=browser.new_page(viewport={'width':width,'height':920},device_scale_factor=1) + errors=[] + page.on('pageerror',lambda exc: errors.append(str(exc))) + page.set_content(pagehtml,wait_until='load',timeout=30000) + page.wait_for_timeout(150) + facts=page.evaluate('''() => { + const shell=document.querySelector('.pro-custom-content'); + const outerFeature=document.querySelector('.sf-section.sf-features'); + const outerList=document.querySelector('.sf-section.sf-collection'); + const innerFeature=document.querySelector('.sf-section.sf-features .sf-container > .sf-features'); + const innerList=document.querySelector('.sf-section.sf-collection .sf-container > .sf-collection'); + const box=el=>el?({x:Math.round(el.getBoundingClientRect().x),width:Math.round(el.getBoundingClientRect().width),display:getComputedStyle(el).display,cols:getComputedStyle(el).gridTemplateColumns}):null; + return {bodyScroll:document.documentElement.scrollWidth,viewport:innerWidth, + featuresOuter:box(outerFeature),listOuter:box(outerList),featuresInner:box(innerFeature),listInner:box(innerList), + featureCount:document.querySelectorAll('.sf-feature').length, + productCount:document.querySelectorAll('.sf-item').length, + filterCount:document.querySelectorAll('.pro-custom-content .sf-filters').length, + module:box(shell)}; + }''') + assert facts['featuresOuter']['display']=='block',facts + assert facts['listOuter']['display']=='block',facts + assert facts['featuresInner']['display']=='grid',facts + assert facts['listInner']['display']=='grid',facts + assert facts['featuresInner']['width']>=min(900,width-80),facts + assert facts['productCount']==6 and facts['featureCount']==4,facts + assert facts['filterCount']==0,facts + assert facts['bodyScroll']<=width+1,facts + assert not errors,errors + shot=dest/f'hesupipe-home-blocks-{width}.png' + page.locator('.pro-custom-content').screenshot(path=str(shot),timeout=30000) + rows.append({'width':width,'screenshot':shot.name,'facts':facts,'script_errors':errors}) + page.close() + browser.close() +(dest/'browser-results.json').write_text(json.dumps(rows,ensure_ascii=False,indent=2)) +for x in rows:print(x['width'], 'feature grid:',x['facts']['featuresInner']['cols'], 'product grid:', x['facts']['listInner']['cols'], 'filters:',x['facts']['filterCount']) diff --git a/scripts/hesupipe_v8331_preview.py b/scripts/hesupipe_v8331_preview.py new file mode 100644 index 0000000..fc5958c --- /dev/null +++ b/scripts/hesupipe_v8331_preview.py @@ -0,0 +1,62 @@ +"""Make a fully offline industrial-theme homepage preview from the real static builder.""" +import os,sys,json,html,re,tempfile +from pathlib import Path +ROOT=Path(__file__).resolve().parents[1] +sys.path[:0]=[str(ROOT),str(ROOT/'tests')] +from tests.legacy import LegacyTestClient +from app import db,theme_store +from app.main import app + +def generate(): + target=ROOT/'examples'; target.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory() as temporary: + db.DATA=Path(temporary);db.DB_FILE=db.DATA/'data.sqlite3';theme_store.THEME_ROOT=db.DATA/'theme_packages' + os.environ['SITEFORGE_ALLOW_LEGACY_DIRECT_DESIGN_WRITE']='1' + os.environ['SITEFORGE_ADMIN_PASSWORD']='demo-test-admin-secret-2026' + os.environ['SITEFORGE_SESSION_SECRET']='demo-secret-not-for-use-outside-test-2026' + with LegacyTestClient(app) as c: + assert c.post('/api/auth/login',json={'username':'admin','password':os.environ['SITEFORGE_ADMIN_PASSWORD']}).status_code==200 + c.headers.update({'X-SiteForge-CSRF':c.get('/api/auth/me').json()['csrf']}) + r=c.post('/api/sites',json={'name':'HESU PIPE · 排版验证样站','theme':'industrial','domain':'https://example.test','mode':'migration','tagline':'工业管道与工程材料 · 完整产品方案'}) + assert r.status_code==201,r.text; sid=r.json()['id'] + home=next(e for e in c.get(f'/api/sites/{sid}/entries').json() if e['route']=='/') + assert c.put(f'/api/factory/sites/{sid}/theme',json={'theme':'industrial','version':'1.3.1'}).status_code==200 + products=[('HDPE 给水管','适用于市政给水及工业供水管网。'),('双壁波纹排水管','适用于市政雨污水管网和地下排水工程。'),('PVC-U 排水管','耐腐蚀、规格丰富的塑料管材系统。'),('PPR 冷热水管','建筑冷热水输送常用管道系列。'),('钢丝网骨架复合管','适用于承压输水与工程管线系统。'),('HDPE 管道连接件','用于热熔及电熔连接的各类管件。')] + for i,(title,summary) in enumerate(products,1): + res=c.post(f'/api/sites/{sid}/entries',json={'type':'product','title':title,'route':f'/products/pipe-{i}/','summary':summary}) + assert res.status_code==201,res.text + blocks=[{'id':'features-hp-v8331','type':'features','title':'核心优势','items':[ + {'title':'管道产品供应','text':'覆盖多种工程管材和配件,便于项目集中采购。'}, + {'title':'规格与质量管理','text':'根据实际应用提供产品选型和规格资料。'}, + {'title':'工程应用支持','text':'面向供水、排水及工业管网项目的应用场景。'}, + {'title':'全球交付服务','text':'从订单沟通到物流交付,提供清晰的合作流程。'}, + ]}, {'id':'collection-hp-v8331','type':'collection','title':'动态内容列表 · 精选产品', + 'subtitle':'PRODUCT COLLECTION','source':'product','limit':6,'columns':3, + 'href':'/products/','button':'查看全部产品'}] + saved=c.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':blocks,'page_template':'home'}) + assert saved.status_code==200,saved.text + built=c.post(f'/api/sites/{sid}/builds',json={}) + assert built.status_code==200,built.text + bid=built.json()['version']; dist=db.DATA/'builds'/sid/bid/'dist' + assert (dist/'index.html').exists(),dist + source=(dist/'index.html').read_text() + # Inline exact packaged CSS/JS, so the demo is readable without a server or external requests. + def stylesheet(m): + path=m.group(1) + local=dist/path + return '' if local.is_file() else '' + source=re.sub(r'',stylesheet,source) + def script(m): + local=dist/m.group(1) + if local.is_file(): + return '' + return '' + source=re.sub(r'',script,source) + # These paths are demonstrative and intentionally not published. + isolated=target/'V8_33_1_HesuPipe_首页布局真实构建预览.html' + snippet=json.dumps(source,ensure_ascii=False).replace('<','\\u003c') + wrapper='''HesuPipe · 首页核心优势与产品列表
HESU PIPE · 核心优势 / 动态内容列表V8.33.1 从实际静态构建生成的离线页面 · 示范文字与占位产品图
仅检查预览 · 不连接线上网站''' + isolated.write_text(wrapper.replace('PAYLOAD',snippet),encoding='utf-8') + (target/'v8331-real-built-page.html').write_text(source,encoding='utf-8') + print('generated',isolated,'bytes',isolated.stat().st_size,'build',bid) +if __name__=='__main__':generate() diff --git a/scripts/http_v833_tradepro.py b/scripts/http_v833_tradepro.py new file mode 100644 index 0000000..3365c44 --- /dev/null +++ b/scripts/http_v833_tradepro.py @@ -0,0 +1,78 @@ +"""End-to-end HTTP + Worker smoke test on an isolated disposable SQLite database. + +Launches an actual uvicorn process, calls authenticated HTTP APIs, runs a real +Worker claim + isolated subprocess build, then downloads and checks the archive. +Never points at production data or remote deployment services. +""" +from __future__ import annotations +import io +import json +import os +import socket +import subprocess +import sys +import tempfile +import time +import zipfile +from pathlib import Path +from urllib.error import URLError +from urllib.request import urlopen +import httpx + +ROOT=Path(__file__).resolve().parent.parent + +def run(): + with tempfile.TemporaryDirectory(prefix='siteforge-e2e-') as folder: + with socket.socket() as sock: + sock.bind(('127.0.0.1',0)); port=sock.getsockname()[1] + base=f'http://127.0.0.1:{port}' + env=os.environ.copy() + env.update(SITEFORGE_ADMIN_PASSWORD='e2e-only-dummy-password-123456', + SITEFORGE_SESSION_SECRET='e2e-only-session-secret-long-value-123456', + SITEFORGE_DATA_DIR=folder,SITEFORGE_DB_FILE=str(Path(folder)/'siteforge.sqlite3'), + SITEFORGE_DATABASE_URL='',SITEFORGE_REDIS_URL='',SITEFORGE_COOKIE_SECURE='false') + proc=subprocess.Popen([sys.executable,'-m','uvicorn','app.main:app','--host','127.0.0.1','--port',str(port)], + cwd=ROOT,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE) + try: + for n in range(120): + if proc.poll() is not None:raise RuntimeError('Web startup failed: '+proc.stderr.read().decode(errors='replace')[-3000:]) + try: + with urlopen(base+'/api/ready',timeout=0.5) as result: + if result.status==200:break + except (OSError,URLError):time.sleep(0.1) + else:raise RuntimeError('Web readiness timeout') + with httpx.Client(base_url=base,timeout=20,follow_redirects=False) as client: + auth=client.post('/api/auth/login',json={'username':'admin','password':env['SITEFORGE_ADMIN_PASSWORD']}) + assert auth.status_code==200,auth.text + me=client.get('/api/auth/me') + assert me.status_code==200,me.text + client.headers['X-SiteForge-CSRF']=me.json()['csrf'] + site=client.post('/api/sites',json={'name':'V8.33 Trade Pro Worker','theme':'trade-pro'}) + assert site.status_code==201,site.text + sid=site.json()['id'] + page=client.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Test about','route':'/about/','body':'E2E smoke content'}) + assert page.status_code==201,page.text + job=client.post(f'/api/sites/{sid}/builds',json={'mirror_images':False},headers={'Idempotency-Key':'e2e-build-000001'}) + assert job.status_code==202,job.text + completed=subprocess.run([sys.executable,'-c','from app import db,enterprise,worker; db.init(); enterprise.setup(); assert worker.run_once()'],cwd=ROOT,env=env,capture_output=True,text=True,timeout=70) + assert completed.returncode==0,completed.stderr[-2500:] + builds=client.get(f'/api/sites/{sid}/builds') + assert builds.status_code==200,builds.text + done=[x for x in builds.json() if x['status']=='complete'] + if not done: + raise AssertionError('No completed static build: '+str(builds.json())[:1200]) + archive=client.get(f"/api/sites/{sid}/builds/{done[0]['id']}/download") + assert archive.status_code==200,archive.text[:300] + with zipfile.ZipFile(io.BytesIO(archive.content)) as z: + assert 'index.html' in z.namelist() + assert 'about/index.html' in z.namelist() + assert ' login/CSRF -> create site/page -> enqueue -> Worker build -> ZIP -> preview') + finally: + proc.terminate() + try:proc.communicate(timeout=5) + except subprocess.TimeoutExpired:proc.kill();proc.communicate() + +if __name__=='__main__':run() diff --git a/scripts/regression_v833.py b/scripts/regression_v833.py new file mode 100644 index 0000000..4cc8da3 --- /dev/null +++ b/scripts/regression_v833.py @@ -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/v833-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.33.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()) diff --git a/scripts/verify_release.py b/scripts/verify_release.py index 61a076e..056fe34 100644 --- a/scripts/verify_release.py +++ b/scripts/verify_release.py @@ -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.32.0-dev.1' +assert VERSION=='8.33.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 @@ -22,4 +22,4 @@ for file in (ROOT/'requirements.lock',ROOT/'requirements-dev.lock'): assert (ROOT/'.github/workflows/ci.yml').is_file() for file in (ROOT/'app/static').glob('*.js'): subprocess.run(['node','--check',str(file)],check=True) -print('PASS: version, 4× themes, worker route, pinned dependencies, CI workflow, JS syntax') +print('PASS: version, 5× themes, worker route, pinned dependencies, CI workflow, JS syntax') diff --git a/tests/test_hesupipe_layout_v8331.py b/tests/test_hesupipe_layout_v8331.py new file mode 100644 index 0000000..b741fef --- /dev/null +++ b/tests/test_hesupipe_layout_v8331.py @@ -0,0 +1,80 @@ +"""V8.33.1: industrial/hesupipe advantage and dynamic-card regression.""" +import json +from pathlib import Path +from bs4 import BeautifulSoup +from app import blueprints, theme_store +from test_factory_v2 import factory, site + + +def _blocks(): + return [ + {'id':'benefits-1234','type':'features','title':'我们的核心优势','items':[ + {'title':'专业管道解决方案','text':'支持项目选型、施工方案和规格咨询。'}, + {'title':'严格质量控制','text':'规格清晰,支持检验与资料交付。'}, + {'title':'丰富的项目经验','text':'多种应用场景和工程项目。'}, + {'title':'国际供应能力','text':'支持国际物流和技术服务。'}]}, + {'id':'productss-1234','type':'collection','title':'热销管道产品','subtitle':'PRODUCT SOLUTIONS', + 'source':'product','limit':6,'columns':3,'button':'浏览所有产品','href':'/products/'}, + ] + + +def test_features_four_cards_form_balanced_rows(): + markup=blueprints.render_blocks(_blocks()[:1],{}, {'route':'/','type':'page'}, []) + assert 'style="--sf-feature-cols:4"' in markup + soup=BeautifulSoup(markup,'html.parser') + assert len(soup.select('.sf-features .sf-feature'))==4 + assert all(x.select_one('h3') for x in soup.select('.sf-feature')) + + +def test_home_dynamic_list_no_filters_and_listing_filter_present(): + rows=[{'route':'/products/p'+str(i)+'/','title':'测试管件 '+str(i),'type':'product','cover':'', + 'summary':'HDPE 工程管材','meta_json':'{}'} for i in range(12)] + home=blueprints.render_blocks(_blocks()[1:],{}, {'route':'/','type':'page'},rows) + assert 'data-sf-filterable="false"' in home + assert home.count('class="sf-item"')==6 + listing=blueprints.render_blocks([dict(_blocks()[1],limit=12)],{}, {'route':'/products/','type':'page'},rows) + assert 'data-sf-filterable="true"' in listing + assert listing.count('class="sf-item"')==12 + + +def test_industrial_new_css_and_version_archive(): + theme=theme_store.resolve('industrial') + assert theme_store.latest('industrial')=='1.3.1' + assert theme_store.resolve('industrial','1.3.0')!=theme + css=(theme/'blocks.css').read_text() + assert '.sf-theme-industrial .pro-custom-content .sf-section.sf-features > .sf-container > .sf-features' in css + assert 'object-fit:contain' in css + assert '.sf-section.sf-collection,.sf-section.sf-stats' in (Path(__file__).parents[1]/'app/static/blocks.css').read_text() + assert not (theme_store.resolve('industrial','1.3.0')/'blocks.css').exists() + + +def test_hesupipe_real_builder_preview_and_archive(factory): + sid=site(factory) + assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'industrial','version':'1.3.1'}).status_code==200 + home=next(x for x in factory.get(f'/api/sites/{sid}/entries').json() if x['route']=='/') + for i in range(1,7): + item=factory.post(f'/api/sites/{sid}/entries',json={ + 'type':'product','title':f'HDPE 管道产品 {i}','route':f'/products/hdpe-{i}/', + 'summary':'适用于市政供水和工业管线的专业产品'}) + assert item.status_code==201,item.text + saved=factory.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':_blocks(),'page_template':'home'}) + assert saved.status_code==200,saved.text + built=factory.post(f'/api/sites/{sid}/builds',json={}) + assert built.status_code==200,built.text + version=built.json()['version'] + html=factory.get(f'/preview/{sid}/{version}/').text + soup=BeautifulSoup(html,'html.parser') + assert soup.select_one('.sf-theme-industrial .pro-custom-content .sf-features[style*="--sf-feature-cols:4"]') + assert len(soup.select('.pro-custom-content .sf-collection .sf-item'))==6 + assert soup.select_one('.pro-custom-content .sf-collection[data-sf-filterable]').get('data-sf-filterable')=='false' + styles=factory.get(f'/preview/{sid}/{version}/assets/blocks.css').text + assert '.sf-theme-industrial .pro-custom-content .sf-item-img' in styles + js=factory.get(f'/preview/{sid}/{version}/assets/site-runtime.js').text + assert 'data-sf-filterable' in js + report=factory.get(f'/api/sites/{sid}/builds/{version}/report').json() + assert not report['errors'] + assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'industrial','version':'1.3.0'}).status_code==200 + older=factory.post(f'/api/sites/{sid}/builds',json={}) + assert older.status_code==200,older.text + oldcss=factory.get(f'/preview/{sid}/{older.json()["version"]}/assets/blocks.css').text + assert '.sf-theme-industrial .pro-custom-content .sf-item-img' not in oldcss diff --git a/tests/test_v829_theme_studio.py b/tests/test_v829_theme_studio.py index fcbd520..a0a0621 100644 --- a/tests/test_v829_theme_studio.py +++ b/tests/test_v829_theme_studio.py @@ -11,7 +11,7 @@ ROOT=Path(__file__).resolve().parents[1] def test_all_themes_preserve_v110_and_publish_v120(factory): for theme in THEMES: - assert theme_store.latest(theme)==('1.4.1' if theme=='automotive' else '1.3.0') + assert theme_store.latest(theme)==('1.4.1' if theme=='automotive' else '1.3.1' if theme=='industrial' else '1.3.0') assert theme_store.resolve(theme,'1.1.0').is_dir() assert theme_store.compatibility(theme,'1.1.0')['valid'] assert theme_store.compatibility(theme,'1.2.0')['valid'] diff --git a/tests/test_v830_real_pages.py b/tests/test_v830_real_pages.py index 24c158b..7c4805d 100644 --- a/tests/test_v830_real_pages.py +++ b/tests/test_v830_real_pages.py @@ -11,7 +11,7 @@ KINDS=('home','listing','product','article','about','contact') def test_new_and_legacy_page_layouts_are_version_isolated(factory): for name in THEMES: - assert theme_store.latest(name)==('1.4.1' if name=='automotive' else '1.3.0') + assert theme_store.latest(name)==('1.4.1' if name=='automotive' else '1.3.1' if name=='industrial' else '1.3.0') assert theme_store.compatibility(name,'1.2.0')['valid'] assert theme_store.compatibility(name,'1.3.0')['valid'] old=theme_store.resolve(name,'1.2.0') diff --git a/tests/test_v831_automotive_ref.py b/tests/test_v831_automotive_ref.py index 6877caa..829adbb 100644 --- a/tests/test_v831_automotive_ref.py +++ b/tests/test_v831_automotive_ref.py @@ -16,7 +16,8 @@ def test_automotive_new_and_old_versions_remain_available(factory): assert theme_store.compatibility('automotive',version)['valid'] assert ('auto31-home' in (p/'pages/home.html').read_text()) == (version == '1.4.0') assert "'pages/' ~ kind" in (p/'pro.html').read_text() - assert theme_store.latest('industrial')=='1.3.0' + assert theme_store.latest('industrial')=='1.3.1' + assert theme_store.resolve('industrial','1.3.0') != theme_store.resolve('industrial') assert 'auto31-home' in (theme_store.resolve('automotive','1.4.0')/'pages/home.html').read_text() diff --git a/tests/test_v833_theme_integration.py b/tests/test_v833_theme_integration.py new file mode 100644 index 0000000..5194201 --- /dev/null +++ b/tests/test_v833_theme_integration.py @@ -0,0 +1,76 @@ +"""V8.33 five-theme, preview parity and business-safe migration checks.""" +import json +from pathlib import Path +from test_factory_v2 import factory,site +from app import core, theme_store, theme_pro + +ROOT=Path(__file__).resolve().parents[1] + +def test_registry_includes_five_themes_and_old_manifests(): + assert set(core.THEMES)==set(theme_pro.THEMES)==set(theme_store.BUILTIN) + assert len(core.THEMES)==5 + assert 'trade-pro' in core.THEMES + for name in ('automotive','industrial','corporate','minimal'): + assert theme_store.resolve(name,'1.1.0').is_dir() + assert theme_store.compatibility('trade-pro')['valid'] + + +def test_theme_gallery_and_editor_offer_trade_pro(factory): + gallery=factory.get('/theme-gallery') + assert gallery.status_code==200 + assert '