chore: import SiteForge Factory V5 source (baseline)

导入 SiteForge Factory V5 基线源码:
- FastAPI + SQLite + Docker Compose(Web + Worker)架构
- 四套商业主题 × 14 页面模板(1.1.0,含历史版本快照)
- 四种建站模式骨架 + 迁移审计 + 发布门禁
- 52 项 pytest 测试
- 补全计划主文档 docs/superpowers/plans/

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 10:42:19 +08:00
commit 3c57c167f2
264 changed files with 11682 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
param(
[string]$Python = "python"
)
$ErrorActionPreference = "Stop"
& $Python -m pip check
if ($LASTEXITCODE -ne 0) { throw "Python dependency check failed" }
& $Python -m compileall -q app
if ($LASTEXITCODE -ne 0) { throw "Python compilation failed" }
& $Python -m pytest -q --basetemp=.pytest_cache/check-tmp
if ($LASTEXITCODE -ne 0) { throw "Pytest failed" }
if (Get-Command node -ErrorAction SilentlyContinue) {
foreach ($file in @("app/static/admin.js", "app/static/editor.js", "app/static/factory-v2.js", "app/static/pro.js")) {
& node --check $file
if ($LASTEXITCODE -ne 0) { throw "JavaScript syntax check failed: $file" }
}
} else {
Write-Warning "Node.js is unavailable; JavaScript syntax checks were skipped."
}
Write-Host "SiteForge V5 checks passed."
+19
View File
@@ -0,0 +1,19 @@
"""Generate a local .env using only Docker's Python image (no host Python needed)."""
import os
import secrets
from pathlib import Path
root=Path(__file__).resolve().parent.parent
config=root/'.env'
if config.exists():
print('已存在 .env:为了防止覆盖现有密码和密钥,本次不做修改。')
raise SystemExit(0)
password=secrets.token_urlsafe(24)
secret=secrets.token_urlsafe(48)
config.write_text(f'''# SiteForge Web Docker v0.3 - 不要上传至代码仓库\nSITEFORGE_PORT=8787\nSITEFORGE_ADMIN_PASSWORD={password}\nSITEFORGE_SESSION_SECRET={secret}\nSITEFORGE_COOKIE_SECURE=false\nSITEFORGE_DOMAIN=siteforge.example.com\n''', encoding='utf-8')
try:os.chmod(config,0o600)
except OSError:pass
print('SiteForge 已生成 .env 配置。')
print('首次登录管理员密码:'+password)
print('请将密码保存在安全的位置,忘记后可编辑 .env 的密码并重启容器。')
print('本机启动命令:docker compose up -d --build')
+40
View File
@@ -0,0 +1,40 @@
"""Offline installer for SiteForge components into a user-provided official 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.
"""
import argparse,shutil,re
from pathlib import Path
SOURCE=Path(__file__).resolve().parents[1]/'integrations'/'art-design-pro'/'src'/'views'/'siteforge'
MARKER='// SITEFORGE_FACTORY_V2_REGISTERED'
ROUTES='''\n // SITEFORGE_FACTORY_V2_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:'迁移与交付'}},\n ]\n },\n'''
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
print(f'官方工程: {root}\n文件目标: {target}\n待复制页面: {len(list(SOURCE.glob("*.vue")))}\n路由需要更新: {original!=modified}')
if dry_run:return
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 完成验收。')
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)
+30
View File
@@ -0,0 +1,30 @@
"""Self-contained HTML preview from the actual four built static sites.
It inlines production CSS; navigation/build/SEO remains in the accompanying ZIP.
"""
import json,re,base64
from pathlib import Path
root=Path(__import__('sys').argv[1]).resolve() if len(__import__('sys').argv)>1 else Path(__file__).resolve().parent.parent/'demo_sites'
out=Path(__import__('sys').argv[2]).resolve() if len(__import__('sys').argv)>2 else Path(__file__).resolve().parent.parent/'theme-preview.html'
styles=['theme.css','blocks.css','factory.css','pro-base.css','pro.css','commercial.css','custom.css']
data={}
for theme in ('automotive','industrial','corporate','minimal'):
base=root/theme
pages={'home':'index.html','products':'products/index.html','detail':'products/1/index.html','news':'news/index.html','case':'cases/1/index.html','about':'about/index.html','contact':'contact/index.html'}
data[theme]={}
for name,file in pages.items():
src=(base/file).read_text(encoding='utf-8')
for css in styles:
text=(base/'assets'/css).read_text(encoding='utf-8')
visual=base/'assets'/'visual'/'hero.svg'
if visual.is_file():
text=text.replace('./visual/hero.svg','data:image/svg+xml;base64,'+base64.b64encode(visual.read_bytes()).decode())
src=src.replace(f'<link rel="stylesheet" href="assets/{css}">','<style>'+text+'</style>')
src=src.replace('<base href="/">','<base href="about:srcdoc">')
src=re.sub(r'<script\s+defer\s+src="assets/[^\"]+"></script>','',src)
data[theme][name]=src
payload=json.dumps(data,ensure_ascii=False).replace('</','<\\/')
html='''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge V5 · 四套商业视觉主题 · 真实构建预览</title><style>body{margin:0;background:#edf1f5;font:14px system-ui,-apple-system,'Microsoft YaHei',sans-serif;color:#1d2d41}header{background:#fff;border-bottom:1px solid #dfe4ea;padding:18px 24px;display:flex;gap:14px;align-items:center;justify-content:space-between}header strong{font-size:18px}header span{color:#72869b;font-size:12px}main{max-width:1550px;margin:auto;padding:20px 22px}.tools{display:flex;flex-wrap:wrap;gap:12px;align-items:end;margin-bottom:18px}.tools label{display:grid;gap:6px;font-size:11px;font-weight:bold;color:#6a7c8c}.tools select{padding:10px 13px;border:1px solid #d0dae2;border-radius:7px;background:#fff;min-width:135px;font-size:13px;color:#1b2d40}.devices{margin-left:auto;display:flex;background:#fff;border:1px solid #d0dae2;border-radius:7px;padding:3px}.devices button{cursor:pointer;border:0;background:none;color:#63788c;padding:9px 15px}.devices button.active{background:#183b60;color:#fff;border-radius:5px}.stage{display:flex;justify-content:center}.frame{width:100%;height:760px;background:white;box-shadow:0 18px 40px #253c4b25;transition:width .2s;border:1px solid #d5dee5;overflow:hidden;border-radius:8px}.frame iframe{width:100%;height:100%;border:0}p.note{color:#738697;line-height:1.7;font-size:12px;text-align:center;margin-top:15px}@media(max-width:680px){.devices{margin-left:0}header{display:block}header span{display:block;margin-top:8px}main{padding:12px}}</style></head>
<body><header><strong>SiteForge V5 · 专业主题效果预览</strong><span>直接打开本文件 · 不需要 Docker · 展示真实构建的 HTML/CSS</span></header><main><div class="tools"><label>选择主题<select id="theme"><option value="automotive">汽车 Automotive</option><option value="industrial">工业 Industrial</option><option value="corporate">企业 Corporate</option><option value="minimal">极简 Minimal</option></select></label><label>查看页面<select id="page"><option value="home">首页</option><option value="products">产品列表</option><option value="detail">产品详情</option><option value="news">新闻资讯</option><option value="case">案例详情</option><option value="about">关于我们</option><option value="contact">联系我们</option></select></label><div class="devices"><button class="active" data-width="100%">PC</button><button data-width="820px">平板</button><button data-width="390px">手机</button></div></div><div class="stage"><div class="frame" id="frame"><iframe id="preview" title="实际静态网站预览" sandbox=""></iframe></div></div><p class="note">演示包含虚构企业与产品数据。此单文件用于查看实际生成页面的视觉布局;完整网站的导航、搜索和筛选请在 Docker 或静态 Web 服务器部署演示站后体验。</p></main>
<script id="data" type="application/json">__PAYLOAD__</script><script>const data=JSON.parse(document.getElementById('data').textContent),theme=document.getElementById('theme'),page=document.getElementById('page'),iframe=document.getElementById('preview');function render(){iframe.srcdoc=data[theme.value][page.value]}theme.onchange=render;page.onchange=render;document.querySelectorAll('[data-width]').forEach(button=>button.onclick=()=>{document.querySelectorAll('[data-width]').forEach(b=>b.classList.remove('active'));button.classList.add('active');document.getElementById('frame').style.width=button.dataset.width;});render();</script></body></html>'''.replace('__PAYLOAD__',payload)
out.write_text(html,encoding='utf-8')
print(str(out),out.stat().st_size,'bytes')
+71
View File
@@ -0,0 +1,71 @@
"""Build four fully static demonstration sites from the real V3 builder.
All sample names and content here are clearly fictitious demonstration data.
"""
import json, os, shutil
from pathlib import Path
BASE=Path(__file__).resolve().parent.parent
DATA=Path(os.environ.get('SITEFORGE_DATA_DIR',str(BASE/'demo_storage')))
os.environ['SITEFORGE_DATA_DIR']=str(DATA)
from app import db,theme_store,builder,quality
from app.core import uid,save_entry
from app.factory_api import bootstrap_site
def put(sid,typ,title,route,summary='',body='',category='',brand='',extra=None):
fields={'category':category,'brand':brand}
if extra:fields.update(extra)
return save_entry(sid,{'type':typ,'title':title,'route':route,'summary':summary,'body':body,
'meta':{'fields':fields}},source='demo')
def build_demo():
db.init();theme_store.setup()
demos={
'automotive':('NOVA MOBILITY','探索更广阔的出行可能','汽车与出行演示主题','Cars'),
'industrial':('ARC INDUSTRIES','面向现代工程的产品与服务','工业制造演示主题','Materials'),
'corporate':('NORTHLINE GROUP','以专业的服务连接企业与未来','企业品牌演示主题','Services'),
'minimal':('MONO STUDIO','少而精,让每一项作品更清晰','极简设计演示主题','Objects'),
}
out=Path(os.environ.get('SITEFORGE_DEMO_OUT',str(BASE/'demo_sites')));out.mkdir(parents=True,exist_ok=True)
results={}
for theme,(name,tagline,about,term) in demos.items():
sid=uid()
settings={'nav':[{'title':x,'route':r} for x,r in [('首页','/'),('产品','/products/'),('品牌','/brands/'),('新闻','/news/'),('案例','/cases/'),('关于','/about/'),('联系','/contact/')]],
'email':'hello@example.com','footer_text':'此为 SiteForge 主题演示网站。公司、产品及内容均为虚构示例。'}
db.insert('INSERT INTO sites VALUES (?,?,?,?,?,?,?,?,?,?,?)',
(sid,name,'https://'+theme+'.example.test',tagline,theme,'#315f92','', 'new',json.dumps(settings,ensure_ascii=False),db.now(),db.now()))
theme_store.pin(sid,theme,'1.1.0')
put(sid,'page','首页','/',tagline)
for i,(route,title,description) in enumerate([
('/about/','关于我们','企业介绍、发展理念、合作能力与团队故事。'),
('/contact/','联系我们','欢迎通过邮件与我们的演示团队取得联系。'),
('/products/','产品中心','查看系列、类别及产品资料。'),
('/brands/','品牌中心','探索与产品相关的品牌。'),
('/news/','新闻资讯','了解最新消息与团队动态。'),
('/cases/','项目案例','精选项目展示。'),
('/services/','服务与解决方案','从咨询到交付的全链路服务。'),
('/faq/','常见问题','更多问题可以联系我们。')]):
put(sid,'page',title,route,description,'<p>本页为演示内容。您可以在网站工厂中直接修改和替换。</p>')
for i,(prod,cat,brand) in enumerate([
(f'{term} Alpha','旗舰系列','NOVA'),(f'{term} Beta','旗舰系列','NOVA'),
(f'{term} Gamma','畅销系列','ARC'),(f'{term} Delta','畅销系列','ARC'),
(f'{term} Epsilon','定制系列','MONO'),(f'{term} Zeta','定制系列','MONO'),
(f'{term} Eta','专业系列','NOVA'),(f'{term} Theta','专业系列','ARC')]):
put(sid,'product',prod,f'/products/{i+1}/',f'{prod} — 针对多样应用的演示产品。',
'<h2>产品说明</h2><p>此处可以展示产品特点、应用场景和可用规格。</p>',cat,brand,
{'model':'MX-'+str(200+i),'material':'Example / Demo','version':'2026','energy':'EV' if theme=='automotive' else 'Standard'})
for name_b in ('NOVA','ARC','MONO'):
put(sid,'brand',name_b,'/brands/'+name_b.lower()+'/',f'{name_b} 品牌产品与应用。')
for i in range(3):
put(sid,'post',f'企业动态 · {i+1:02d}',f'/news/{i+1}/','在这里发布新闻资讯与品牌动态。',
'<p>这是可替换的新闻正文内容,可包含图片、表格和链接。</p>',extra={'publish_date':f'2026-10-{11+i:02}'})
put(sid,'case',f'精选项目 · {i+1:02d}',f'/cases/{i+1}/','展示项目背景、过程与成果。',
'<p>本页仅为项目结构示例,并非实际完成的工程项目。</p>',extra={'location':'Demo location','industry':'Demo industry'})
put(sid,'service','供应与交付方案','/services/solution/','灵活、可配置的供应交付服务。')
r=builder.build(sid)
src=db.DATA/'builds'/sid/r['version']/'dist'
dst=out/theme
if dst.exists():shutil.rmtree(dst)
shutil.copytree(src,dst)
results[theme]={'page_count':r['pages'],'warnings':r['warnings'],'quality':quality.report(sid,r['version'])}
(out/'BUILD_REPORT.json').write_text(json.dumps(results,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({k:{'pages':v['page_count'],'warnings':len(v['warnings']),'quality_errors':v['quality']['errors']} for k,v in results.items()},ensure_ascii=False,indent=2))
if __name__=='__main__':build_demo()
+56
View File
@@ -0,0 +1,56 @@
"""Offline restore of SiteForge backups. STOP API and worker containers first!
Usage: python scripts/restore_backup.py /path/to/backup.zip --confirmed-offline
The application must be stopped to prevent WAL writes during restore.
"""
import argparse
import os
import shutil
import sqlite3
import stat
import tempfile
import zipfile
from pathlib import Path
def restore(path, target, confirmed=False):
if not confirmed:raise RuntimeError('停止 API 与 worker 后,使用 --confirmed-offline 才能恢复')
target=Path(target).resolve();archive=Path(path).resolve()
if not archive.exists():raise FileNotFoundError(archive)
with zipfile.ZipFile(archive) as z:
infos=z.infolist()
if len(infos)>250000:raise ValueError('备份文件数量过大')
if sum(i.file_size for i in infos)>30_000_000_000:raise ValueError('解压数据总量过大')
for item in infos:
p=Path(item.filename)
if p.is_absolute() or any(x in ('..','.') for x in p.parts) or not p.parts:
raise ValueError('备份包含不安全路径')
if stat.S_ISLNK(item.external_attr>>16):raise ValueError('不允许符号链接')
if 'siteforge.sqlite3' not in z.namelist():raise ValueError('缺少数据库文件')
with tempfile.TemporaryDirectory(dir=target.parent) as tmp:
z.extractall(tmp)
restored=Path(tmp)/'siteforge.sqlite3'
with sqlite3.connect(restored) as db:
if db.execute('PRAGMA integrity_check').fetchone()[0]!='ok':
raise ValueError('备份数据库完整性校验失败')
target.mkdir(parents=True,exist_ok=True)
# Keep a copy of the old DB outside the restored data volume.
old_db=target/'siteforge.sqlite3'
if old_db.exists():
dest=target.parent/'siteforge-pre-restore.sqlite3'
shutil.copy2(old_db,dest)
for special in ('siteforge.sqlite3-wal','siteforge.sqlite3-shm'):
(target/special).unlink(missing_ok=True)
for src in Path(tmp).rglob('*'):
if src.is_file():
dest=target/src.relative_to(tmp)
dest.parent.mkdir(parents=True,exist_ok=True)
shutil.copy2(src,dest)
print('恢复完成。请检查文件权限后再启动 SiteForge。')
if __name__=='__main__':
parser=argparse.ArgumentParser()
parser.add_argument('archive')
parser.add_argument('--data',default=os.getenv('SITEFORGE_DATA_DIR','/data'))
parser.add_argument('--confirmed-offline',action='store_true')
args=parser.parse_args()
restore(args.archive,args.data,args.confirmed_offline)
+24
View File
@@ -0,0 +1,24 @@
"""One-time developer script: break V3 full themes into independently maintainable Jinja page templates."""
from pathlib import Path
import re
base=Path(__file__).resolve().parent.parent/'themes'
types={'home':['home'],'search':['search'],'listing':['listing','category'],'product':['product'],
'brand':['brand'],'article':['article'],'case':['case'],'info':['about','service','contact','faq','standard'],'notfound':['notfound']}
markers=[("{% if kind=='home' %}",'home'),("{% elif kind=='search' %}",'search'),("{% elif kind in ['listing','category'] %}",'listing'),("{% elif kind=='product' %}",'product'),("{% elif kind=='brand' %}",'brand'),("{% elif kind=='article' %}",'article'),("{% elif kind=='case' %}",'case'),("{% elif kind in ('about','service','contact','faq','standard') %}",'info'),("{% elif kind=='notfound' %}",'notfound')]
for name in ('automotive','industrial','corporate','minimal'):
p=base/name/'pro.html'
t=p.read_text(encoding='utf-8')
beginning=t.index(markers[0][0]); ending=t.index('{% endif %}\n{% if has_custom',beginning)
common=t[:beginning]
remainder=t[ending+len('{% endif %}'):] # preserve custom blocks and footer
dir=base/name/'pages';dir.mkdir(parents=True,exist_ok=True)
pieces={}
for i,(marker,key) in enumerate(markers):
start=t.index(marker,beginning)+len(marker)
finish=t.index(markers[i+1][0],start) if i+1<len(markers) else ending
pieces[key]=t[start:finish].strip()+'\n'
for key,kinds in types.items():
for kind in kinds:
(dir/(kind+'.html')).write_text(pieces[key],encoding='utf-8')
p.write_text(common+"{% include '"+name+"/pages/' ~ kind ~ '.html' %}\n"+remainder,encoding='utf-8')
print(name,len(list(dir.glob('*.html'))),'page family files')