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>
159 lines
7.7 KiB
Python
159 lines
7.7 KiB
Python
"""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.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(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))
|