"""Install the SiteForge V8.19 module into a user-provided Art Design Pro checkout. No network, npm, or destructive operations. Route insertion is deterministic; the official Art checkout is not bundled. Usage: --apply, --verify, --migrate-legacy. """ from __future__ import annotations import argparse import hashlib import json import os from pathlib import Path import re import shutil 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 // 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 _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 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.29.0-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(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))