upgrade: adopt V8.31.1 (theme 1.3/1.4 redesigns, editor deep-canvas, guided launch, component library)

Overlay 190 upstream items (V8.22→8.31) preserving trade-pro theme,
registry tests, and design docs. Re-applied full patch set on identical
anchors (discovery/env_for/blocks.css/type-mapping/locale-override/
theme_color/frontend entries/test_theme_pro_v3 mods) plus compose pins.

Verified: theme suites 35/35; dual-baseline full run 415 passed vs
pristine 386+29 ours, FAILED-set diff EMPTY (same 10 Windows env fails).
Live: container 8.31.1-dev.1 healthy, hesupipe 192 entries intact,
trade-pro build 203 pages 0 errors.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-26 09:43:17 +08:00
parent a63548c645
commit 8884e100f9
647 changed files with 20853 additions and 271 deletions
+125 -1
View File
@@ -198,9 +198,10 @@ def write(site_id,payload):
(site_id,revision+1,base,json.dumps(plan,ensure_ascii=False),db.now()))
return {'saved':True,'state':'draft','revision':revision+1,'build_unchanged':True}
def publish(site_id,expected_revision,actor):
def publish(site_id,expected_revision,actor,expected_plan_token=None):
site=_site(site_id)
with db.connect() as c:
c.execute('BEGIN IMMEDIATE')
draft=c.execute('SELECT * FROM site_design_drafts WHERE site_id=?',(site_id,)).fetchone()
if not draft:raise HTTPException(409,'尚无全站设计草稿')
if type(expected_revision) is not int or expected_revision!=draft['revision']:
@@ -210,6 +211,10 @@ def publish(site_id,expected_revision,actor):
if draft['base_hash']!=_hash(settings):raise HTTPException(409,'网站信息已变更,请重新审核')
before=published(settings)
config=validate(json.loads(draft['config_json']))
if expected_plan_token is not None:
proof=_review_token(site_id,draft['revision'],settings,config)
if not isinstance(expected_plan_token,str) or not __import__('hmac').compare_digest(expected_plan_token,proof):
raise HTTPException(409,'审核预览已过期,请重新查看差异后确认')
settings['site_design']=config
settings['design_tokens']=config['tokens']
# Keep old nav/settings for non-professional legacy templates too.
@@ -223,6 +228,87 @@ def publish(site_id,expected_revision,actor):
c.execute('DELETE FROM site_design_drafts WHERE site_id=?',(site_id,))
return {'published':True,'publication_id':publication_id,'build_required':True}
# V8.26 - reviewable, immutable full-site design changes. This workflow uses
# the existing site_design_drafts/publications tables: no destructive migration.
REVIEW_SECTIONS = ('header', 'nav', 'footer', 'mobile_tabs', 'tokens', 'responsive')
def _changes(before, after):
"""Return a bounded, deterministic, safe structured diff for human review."""
old, new = validate(before), validate(after)
rows = []
def inspect(path, a, b):
if a == b:
return
if isinstance(a, dict) and isinstance(b, dict):
for key in sorted(set(a) | set(b)):
inspect(path + '.' + key, a.get(key), b.get(key))
elif isinstance(a, list) and isinstance(b, list):
# Lists (menu order) are atomic to avoid obscuring a move as deletes.
rows.append({'field': path, 'before': a, 'after': b})
else:
rows.append({'field': path, 'before': a, 'after': b})
for section in REVIEW_SECTIONS:
inspect(section, old[section], new[section])
return rows
def _review_token(site_id, revision, live_settings, draft_config):
payload={'site_id':site_id,'revision':revision,
'settings_hash':_hash(live_settings), 'draft':validate(draft_config)}
return hashlib.sha256(json.dumps(payload,ensure_ascii=False,
sort_keys=True,separators=(',',':')).encode()).hexdigest()
def review(site_id):
with db.connect() as c:
site=c.execute('SELECT settings_json FROM sites WHERE id=?',(site_id,)).fetchone()
if not site:raise HTTPException(404,'网站不存在')
draft=c.execute('SELECT * FROM site_design_drafts WHERE site_id=?',(site_id,)).fetchone()
if not draft:raise HTTPException(409,'没有待审核的全站设计草稿')
settings=json.loads(site['settings_json'] or '{}')
if draft['base_hash']!=_hash(settings):raise HTTPException(409,'网站设置已经修改,请先处理草稿冲突')
proposed=validate(json.loads(draft['config_json']))
delta=_changes(published(settings),proposed)
sections=[name for name in REVIEW_SECTIONS if any(
r['field']==name or r['field'].startswith(name+'.') for r in delta)]
return {'revision':draft['revision'], 'changed_fields':delta,
'changed_sections':sections, 'change_count':len(delta),
'affected_scope':'entire_site_next_build',
'published_site_unchanged':True,
'plan_token':_review_token(site_id,draft['revision'],settings,proposed)}
def restore_publication(site_id,publication_id,which,expected_revision,expected_live_hash):
"""Restore a historical snapshot as a NEW draft, never touch live settings."""
if which not in ('before','after'):raise HTTPException(400,'恢复来源仅支持 before 或 after')
if type(expected_revision) is not int:raise HTTPException(400,'需要草稿版本号')
if not isinstance(expected_live_hash,str) or len(expected_live_hash)!=64:
raise HTTPException(400,'需要当前全站发布版本指纹')
with db.connect() as c:
c.execute('BEGIN IMMEDIATE')
site=c.execute('SELECT settings_json FROM sites WHERE id=?',(site_id,)).fetchone()
if not site:raise HTTPException(404,'网站不存在')
historic=c.execute('SELECT * FROM site_design_publications WHERE site_id=? AND id=?',
(site_id,publication_id)).fetchone()
if not historic:raise HTTPException(404,'发布记录不存在')
settings=json.loads(site['settings_json'] or '{}')
live_hash=_hash(settings)
if live_hash!=expected_live_hash:raise HTTPException(409,'全站发布版本已变化,请重新载入')
raw=historic['old_config_json'] if which=='before' else historic['new_config_json']
plan=validate(json.loads(raw))
old=c.execute('SELECT revision FROM site_design_drafts WHERE site_id=?',(site_id,)).fetchone()
current=old['revision'] if old else 0
if expected_revision!=current:raise HTTPException(409,'草稿版本已变化,请重新加载')
new_rev=current+1
c.execute('INSERT INTO site_design_drafts(site_id,revision,base_hash,config_json,updated_at) '
'VALUES(?,?,?,?,?) ON CONFLICT(site_id) DO UPDATE SET '
'revision=excluded.revision,base_hash=excluded.base_hash, '
'config_json=excluded.config_json,updated_at=excluded.updated_at',
(site_id,new_rev,live_hash,json.dumps(plan,ensure_ascii=False),db.now()))
return {'restored_as_draft':True,'revision':new_rev,'published_unchanged':True}
def responsive_css(config):
r=validate(config)['responsive']
tablet=r['tablet_gap'];mobile=r['mobile_gap'];padding=r['mobile_container_padding']
@@ -276,3 +362,41 @@ def api_history(request:Request,site_id:str):
_site(site_id)
return db.rows('''SELECT id,site_id,actor,revision,published_at FROM site_design_publications
WHERE site_id=? ORDER BY published_at DESC LIMIT 50''',(site_id,))
@router.get('/{site_id}/site-design/review')
def api_review(request:Request,site_id:str):
enterprise.require(request,site_id,'viewer')
return review(site_id)
@router.post('/{site_id}/site-design/publish-reviewed')
def api_publish_reviewed(request:Request,site_id:str,payload:dict=Body(...)):
enterprise.require(request,site_id,'owner')
return publish(site_id,payload.get('expected_revision'),enterprise.actor(request),
payload.get('plan_token',''))
@router.get('/{site_id}/site-design/publications/{publication_id}')
def api_publication(request:Request,site_id:str,publication_id:str):
enterprise.require(request,site_id,'viewer')
_site(site_id)
row=db.one('SELECT * FROM site_design_publications WHERE site_id=? AND id=?',
(site_id,publication_id))
if not row:raise HTTPException(404,'发布版本不存在')
old=validate(json.loads(row['old_config_json']))
new=validate(json.loads(row['new_config_json']))
return {'id':row['id'],'published_at':row['published_at'],'actor':row['actor'],
'before':old,'after':new,'changes':_changes(old,new)}
@router.get('/{site_id}/site-design/live-hash')
def api_live_hash(request:Request,site_id:str):
enterprise.require(request,site_id,'viewer')
return {'live_hash':_hash(_settings(_site(site_id)))}
@router.post('/{site_id}/site-design/publications/{publication_id}/restore')
def api_restore_publication(request:Request,site_id:str,publication_id:str,payload:dict=Body(...)):
enterprise.require(request,site_id,'editor')
return restore_publication(site_id,publication_id,payload.get('snapshot'),
payload.get('expected_revision'),payload.get('expected_live_hash'))