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:
@@ -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."
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user