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:
@@ -1,52 +1,158 @@
|
||||
"""Offline installer for SiteForge components into a user-provided official Art Design Pro checkout.
|
||||
"""Install the SiteForge V8.19 module into a user-provided Art Design Pro checkout.
|
||||
|
||||
Run with --dry-run first. It does NOT download, modify auth schemes, or
|
||||
pretend to have compiled the official repository in this sandbox.
|
||||
No network, npm, or destructive operations. Route insertion is deterministic; the
|
||||
official Art checkout is not bundled. Usage: --apply, --verify, --migrate-legacy.
|
||||
"""
|
||||
import argparse,shutil,re
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
SOURCE=Path(__file__).resolve().parents[1]/'integrations'/'art-design-pro'/'src'/'views'/'siteforge'
|
||||
MARKER='// SITEFORGE_FACTORY_V7_REGISTERED'
|
||||
V8_MARKER='// SITEFORGE_FACTORY_V8_REGISTERED'
|
||||
V8_ROUTES="""
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / 'integrations/art-design-pro/src/views/siteforge'
|
||||
ROUTE_MARKER = '// SITEFORGE_FACTORY_V8_REGISTERED'
|
||||
BEGIN = '// SITEFORGE_V819_ROUTES_BEGIN'
|
||||
END = '// SITEFORGE_V819_ROUTES_END'
|
||||
LEGACY = ('// SITEFORGE_FACTORY_V7_REGISTERED', ROUTE_MARKER)
|
||||
ROUTES = '''
|
||||
// SITEFORGE_V819_ROUTES_BEGIN
|
||||
// SITEFORGE_FACTORY_V8_REGISTERED
|
||||
{ path:'/siteforge/production', name:'SiteForgeV8Production', component:'/siteforge/V8Production',
|
||||
meta:{title:'V8 生产控制台',icon:'ri:factory-line'} },
|
||||
"""
|
||||
ROUTES='''\n // SITEFORGE_FACTORY_V7_REGISTERED\n {\n path: '/siteforge', name: 'SiteForge', component: '/index/index',\n meta: {title:'网站工厂',icon:'ri:layout-grid-line'},\n children: [\n {path:'factory',name:'SiteForgeFactory',component:'/siteforge/WebsiteFactory',meta:{title:'网站项目'}},\n {path:'themes',name:'SiteForgeThemes',component:'/siteforge/ThemeCenter',meta:{title:'主题与版本'}},\n {path:'enterprise',name:'SiteForgeEnterprise',component:'/siteforge/EnterpriseCenter',meta:{title:'企业控制'}},
|
||||
{path:'editor',name:'SiteForgeEditor',component:'/siteforge/VisualEditor',meta:{title:'可视化编辑'}},
|
||||
{path:'migration',name:'SiteForgeMigration',component:'/siteforge/MigrationCenter',meta:{title:'CMS 迁移'}},
|
||||
{path:'releases',name:'SiteForgeReleases',component:'/siteforge/ReleaseCenter',meta:{title:'审批与发布'}},\n ]\n },\n'''
|
||||
// SITEFORGE_FACTORY_V7_REGISTERED
|
||||
{
|
||||
path: '/siteforge', name: 'SiteForge', component: '/index/index',
|
||||
meta: { title: '网站工厂', icon: 'ri:layout-grid-line' },
|
||||
children: [
|
||||
{ path: 'dashboard', name: 'SiteForgeDashboard', component: '/siteforge/ConsoleHome', meta: { title: '工作台' } },
|
||||
{ path: 'factory', name: 'SiteForgeFactory', component: '/siteforge/WebsiteFactory', meta: { title: '网站管理' } },
|
||||
{ path: 'content', name: 'SiteForgeContent', component: '/siteforge/ContentCenter', meta: { title: '内容管理' } },
|
||||
{ path: 'editor', name: 'SiteForgeEditor', component: '/siteforge/VisualEditor', meta: { title: '网站设计' } },
|
||||
{ path: 'migration', name: 'SiteForgeMigration', component: '/siteforge/MigrationCenter', meta: { title: '数据与迁移' } },
|
||||
{ path: 'releases', name: 'SiteForgeReleases', component: '/siteforge/ReleaseCenter', meta: { title: '构建与发布' } },
|
||||
{ path: 'enterprise', name: 'SiteForgeEnterprise', component: '/siteforge/EnterpriseCenter', meta: { title: '系统管理' } },
|
||||
{ path: 'themes', name: 'SiteForgeThemes', component: '/siteforge/ThemeCenter', meta: { title: '主题库', isHide: true } },
|
||||
{ path: 'production', name: 'SiteForgeV8Production', component: '/siteforge/V8Production', meta: { title: '高级生产工具', isHide: true } },
|
||||
],
|
||||
},
|
||||
// SITEFORGE_V819_ROUTES_END
|
||||
'''
|
||||
ROUTE_RE = re.compile(r'(export\s+const\s+asyncRoutes\s*(?::[^=]+)?=\s*\[)')
|
||||
|
||||
def install(root:Path,dry_run:bool):
|
||||
base=root/'src'/'router'/'routes'/'asyncRoutes.ts'
|
||||
target=root/'src'/'views'/'siteforge'
|
||||
if not base.is_file() or not (root/'package.json').is_file():raise ValueError('不是受支持的 Art Design Pro 源码目录')
|
||||
original=base.read_text(encoding='utf-8')
|
||||
pat=r'(export\s+const\s+asyncRoutes\s*:[^=]*=\s*\[)'
|
||||
if MARKER not in original:
|
||||
modified,count=re.subn(pat,lambda m:m.group(1)+ROUTES,original,count=1)
|
||||
if count!=1:raise ValueError('无法定位 asyncRoutes 定义;该官方版本的路由结构可能不同,请人工集成')
|
||||
else:modified=original
|
||||
if V8_MARKER not in modified:
|
||||
modified,count=re.subn(pat,lambda m:m.group(1)+V8_ROUTES,modified,count=1)
|
||||
if count!=1:raise ValueError('无法注册 V8 业务页面')
|
||||
print(f'官方工程: {root}\n文件目标: {target}\n待复制页面: {len(list(SOURCE.glob("*.vue")))}\n路由需要更新: {original!=modified}')
|
||||
if dry_run:return
|
||||
|
||||
def _extract_legacy_block(text: str, marker: str) -> tuple[int, int]:
|
||||
start = text.index(marker)
|
||||
# Old installer placed the marker immediately before its own route object.
|
||||
pos = text.find('{', start)
|
||||
if pos < 0: raise ValueError('旧路由没有对象起始符号')
|
||||
depth = 0
|
||||
quoted = None
|
||||
escape = False
|
||||
for i in range(pos, len(text)):
|
||||
c = text[i]
|
||||
if quoted:
|
||||
if escape: escape = False
|
||||
elif c == '\\': escape = True
|
||||
elif c == quoted: quoted = None
|
||||
elif c in ('"', "'", '`'): quoted = c
|
||||
elif c == '{': depth += 1
|
||||
elif c == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
end = i + 1
|
||||
if end < len(text) and text[end] == ',': end += 1
|
||||
# The marker and the block must be adjacent except whitespace.
|
||||
if text[start + len(marker):pos].strip():
|
||||
raise ValueError('旧路由已人工修改,拒绝自动迁移')
|
||||
return start, end
|
||||
raise ValueError('旧路由结构不完整')
|
||||
|
||||
|
||||
def transform_routes(text: str, migrate_legacy: bool = False) -> str:
|
||||
if 'SITEFORGE_V819_ROUTES_BEGIN' in text or 'SITEFORGE_V819_ROUTES_END' in text:
|
||||
if text.count(BEGIN) != 1 or text.count(END) != 1:
|
||||
raise ValueError('V8.19 路由标记不完整')
|
||||
a = text.index(BEGIN)
|
||||
b = text.index(END) + len(END)
|
||||
if text[a:b] != ROUTES.strip():
|
||||
raise ValueError('已安装的 SiteForge 路由被人工修改;请人工审核后升级')
|
||||
return text
|
||||
if any(m in text for m in LEGACY):
|
||||
if not migrate_legacy:
|
||||
raise ValueError('发现 V8 旧版路由;请备份后使用 --migrate-legacy 显式升级')
|
||||
# Remove highest-offset blocks first. Never touch an unrelated route.
|
||||
for m in sorted((m for m in LEGACY if m in text), key=lambda v:text.index(v), reverse=True):
|
||||
a,b=_extract_legacy_block(text,m)
|
||||
text=text[:a]+text[b:]
|
||||
modified,n = ROUTE_RE.subn(lambda m:m.group(1)+ROUTES, text, count=1)
|
||||
if n!=1: raise ValueError('无法识别 asyncRoutes 变量;需手动适配 Art 官方版本')
|
||||
return modified
|
||||
|
||||
|
||||
def _sha(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def install(root: Path, dry_run: bool = True, verify: bool = False,
|
||||
migrate_legacy: bool = False, overwrite_views: bool = False):
|
||||
root = root.resolve()
|
||||
route_file = root / 'src/router/routes/asyncRoutes.ts'
|
||||
if not (root/'package.json').is_file() or not route_file.is_file():
|
||||
raise ValueError('不是受支持的 Art Design Pro 工程:缺少 package.json 或 asyncRoutes.ts')
|
||||
current = route_file.read_text(encoding='utf-8')
|
||||
new = transform_routes(current, migrate_legacy=migrate_legacy)
|
||||
target = root/'src/views/siteforge'
|
||||
sources = sorted(p for p in SOURCE.iterdir() if p.suffix in ('.vue','.ts') and p.is_file())
|
||||
if not sources: raise ValueError('本地业务页面资源缺失')
|
||||
existing = [p for p in sources if (target/p.name).exists() and (target/p.name).read_bytes()!=p.read_bytes()]
|
||||
if verify:
|
||||
errors=[]
|
||||
if current!=new: errors.append('路由未安装')
|
||||
for src in sources:
|
||||
out=target/src.name
|
||||
if not out.is_file() or _sha(out.read_bytes()) != _sha(src.read_bytes()):
|
||||
errors.append(src.name+' 缺失或不匹配')
|
||||
if errors: raise ValueError('; '.join(errors))
|
||||
return {'installed':True,'views':len(sources),'routes':'verified'}
|
||||
if existing and not overwrite_views and not dry_run:
|
||||
raise ValueError('以下页面存在不同内容,拒绝覆盖:'+', '.join(p.name for p in existing)+';请加 --overwrite-views')
|
||||
result={'dry_run':dry_run,'routes_need_update':current!=new,'views':len(sources),
|
||||
'conflicting_views':[x.name for x in existing],'target':str(root)}
|
||||
if dry_run:return result
|
||||
target.mkdir(parents=True,exist_ok=True)
|
||||
for src in SOURCE.glob('*.vue'):
|
||||
dest=target/src.name
|
||||
if dest.exists():shutil.copy2(dest,dest.with_suffix('.vue.siteforge-backup'))
|
||||
shutil.copy2(src,dest)
|
||||
if modified!=original:
|
||||
shutil.copy2(base,base.with_suffix('.ts.siteforge-backup'))
|
||||
base.write_text(modified,encoding='utf-8')
|
||||
print('业务页面与菜单已写入;下一步请配置 /api、/editor、/preview 代理并执行 pnpm build 完成验收。')
|
||||
for src in sources:
|
||||
out=target/src.name
|
||||
if out.exists() and out.read_bytes()!=src.read_bytes():
|
||||
backup=out.with_name(out.name+'.siteforge-backup')
|
||||
if not backup.exists():shutil.copy2(out,backup)
|
||||
shutil.copy2(src,out)
|
||||
if current!=new:
|
||||
backup=route_file.with_name(route_file.name+'.siteforge-backup')
|
||||
if not backup.exists():shutil.copy2(route_file,backup)
|
||||
temp=route_file.with_suffix('.ts.siteforge-tmp')
|
||||
try:
|
||||
temp.write_text(new,encoding='utf-8')
|
||||
os.replace(temp,route_file)
|
||||
finally:
|
||||
temp.unlink(missing_ok=True)
|
||||
manifest={'siteforge_version':'8.21.3-dev.1','files':{p.name:_sha(p.read_bytes()) for p in sources}}
|
||||
(target/'.siteforge-v819.json').write_text(json.dumps(manifest,indent=2),encoding='utf-8')
|
||||
result['installed']=True
|
||||
return result
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument('official_repo',type=Path)
|
||||
parser.add_argument('--apply',action='store_true',help='执行源代码变更;不传则只检查')
|
||||
args=parser.parse_args()
|
||||
install(args.official_repo.resolve(),dry_run=not args.apply)
|
||||
parser=argparse.ArgumentParser(description='SiteForge V8.19 → Art Design Pro 离线适配安装')
|
||||
parser.add_argument('official_repo', type=Path)
|
||||
parser.add_argument('--apply',action='store_true')
|
||||
parser.add_argument('--verify',action='store_true')
|
||||
parser.add_argument('--migrate-legacy',action='store_true')
|
||||
parser.add_argument('--overwrite-views',action='store_true')
|
||||
a=parser.parse_args()
|
||||
try:
|
||||
print(json.dumps(install(a.official_repo,dry_run=not(a.apply or a.verify),verify=a.verify,
|
||||
migrate_legacy=a.migrate_legacy,overwrite_views=a.overwrite_views),ensure_ascii=False,indent=2))
|
||||
except ValueError as e:
|
||||
parser.error(str(e))
|
||||
|
||||
Reference in New Issue
Block a user