601451f22b
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>
75 lines
4.9 KiB
Python
75 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Offline WordPress WXR preflight and optional static output URL coverage check."""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import html
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
if str(ROOT) not in sys.path:sys.path.insert(0,str(ROOT))
|
|
from app.wp_complex_v820 import inspect_wxr, MAX_XML
|
|
from app.builder import output_path
|
|
|
|
|
|
def inspect_file(source:Path,dist:Path|None=None):
|
|
if source.stat().st_size>MAX_XML:raise ValueError('WXR 超过 16 MB,需分割后分别预检')
|
|
result=inspect_wxr(source.read_bytes())
|
|
if dist is not None:
|
|
if not dist.is_dir():raise ValueError('静态构建输出目录不存在')
|
|
# Every actual published URL must be matched. WXR preflight by design
|
|
# never claims 100% coverage if it has invalid or duplicate routes.
|
|
from defusedxml import ElementTree as ET
|
|
root=ET.parse(source).getroot()
|
|
routes=[]
|
|
from app.core import route_path
|
|
from urllib.parse import urlsplit
|
|
for item in root.findall('./channel/item'):
|
|
fields={c.tag.rsplit('}',1)[-1]:c.text or '' for c in item if c.tag.rsplit('}',1)[-1] in ('post_type','status','link')}
|
|
if fields.get('post_type') in ('attachment','revision','nav_menu_item') or fields.get('status','publish')!='publish':continue
|
|
p=urlsplit(fields.get('link',''))
|
|
if p.scheme in ('http','https') and p.hostname and not p.query and not p.fragment and p.path:
|
|
try:routes.append(route_path(p.path))
|
|
except ValueError:pass
|
|
missing=[route for route in sorted(set(routes)) if not output_path(dist,route).is_file()]
|
|
result['static_build_coverage']={'expected_routes':len(set(routes)),'missing_count':len(missing),
|
|
'missing_sample':missing[:100],
|
|
'passed':len(missing)==0 and result['blocking_issues']==0}
|
|
return result
|
|
|
|
|
|
def render_html(report):
|
|
rows=''.join('<tr><td>'+html.escape(x['severity'])+'</td><td>'+html.escape(x['code'])+
|
|
'</td><td>'+html.escape(x['route'])+'</td><td>'+html.escape(x['message'])+'</td></tr>' for x in report['issues'])
|
|
metrics=' '.join(f'<span><strong>{html.escape(str(k))}</strong>: {html.escape(str(v))}</span>' for k,v in report['counts'].items() if k in ('published_content','attachments','findings_blocker','findings_review'))
|
|
return 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.20 WordPress 兼容预检</title><style>
|
|
body{{font:14px/1.65 system-ui,"Microsoft YaHei",sans-serif;color:#192d43;background:#f3f6f9;margin:0}}
|
|
header{{background:#193046;color:#fff;padding:32px 6%}}main{{max-width:1100px;margin:20px auto;padding:25px;background:#fff;border-radius:12px;overflow-x:auto}}
|
|
header h1{{margin:0}}p{{color:#576679}}.metrics span{{display:inline-block;padding:10px;border:1px solid #e4e9f0;margin:4px;border-radius:7px}}
|
|
table{{border-collapse:collapse;width:100%}}th,td{{padding:9px;text-align:left;border-bottom:1px solid #edf0f2;vertical-align:top}}tr:nth-child(even){{background:#f8fafc}}</style></head><body>
|
|
<header><h1>SiteForge V8.20 · WordPress 兼容预检</h1><p style="color:#eee">离线分析,不连接原站,不修改已有站点</p></header><main><div class="metrics">{metrics}</div><h2>待处理问题</h2>
|
|
<table><thead><tr><th>等级</th><th>问题类型</th><th>URL</th><th>处理说明</th></tr></thead><tbody>{rows}</tbody></table>
|
|
<p>本报告不能证明 1:1 迁移、媒体本地化、动态功能、视觉一致性或正式发布验收通过。</p></main></body></html>'''
|
|
|
|
|
|
def main(argv=None):
|
|
parser=argparse.ArgumentParser(description='离线分析 WordPress WXR 迁移风险与 URL 覆盖')
|
|
parser.add_argument('wxr',type=Path)
|
|
parser.add_argument('--dist',type=Path,help='可选:静态构建 dist 目录,检查对应 HTML 是否存在')
|
|
parser.add_argument('--output',type=Path,default=Path('reports/v820-wxr-preflight.json'))
|
|
args=parser.parse_args(argv)
|
|
try:report=inspect_file(args.wxr,args.dist)
|
|
except (OSError,ValueError,Exception) as exc:
|
|
# A single fail-closed error message avoids a massive traceback for malformed input.
|
|
print('无法完成 WXR 预检:'+str(exc)[:350],file=sys.stderr)
|
|
return 2
|
|
args.output.parent.mkdir(parents=True,exist_ok=True)
|
|
args.output.write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
|
|
args.output.with_suffix('.html').write_text(render_html(report),encoding='utf-8')
|
|
print(f"WXR 预检:{report['unique_published_routes']} 个静态路由,{report['blocking_issues']} 个阻断问题;报告:{args.output}")
|
|
if report['blocking_issues'] or report.get('static_build_coverage',{}).get('passed') is False:return 1
|
|
return 0
|
|
|
|
if __name__=='__main__':raise SystemExit(main())
|