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:
+125
-1
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user