2026-10-22 16:49:25 +08:00
|
|
|
|
'use strict';
|
|
|
|
|
|
const $=s=>document.querySelector(s), esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
|
|
|
|
let csrf='',sites=[],siteId='',jobs=[],builds=[],releases=[],lastPreview=null;
|
|
|
|
|
|
const notify=(message,error=false)=>{$('#notice').textContent=message;$('#notice').style.color=error?'#ac3d45':'#256448'};
|
|
|
|
|
|
async function request(url,{method='GET',body=null,upload=false}={}){
|
|
|
|
|
|
const headers={};if(method!=='GET')headers['X-SiteForge-CSRF']=csrf;
|
|
|
|
|
|
if(body&&!upload)headers['Content-Type']='application/json';
|
|
|
|
|
|
const res=await fetch(url,{method,credentials:'same-origin',headers,body:body?(upload?body:JSON.stringify(body)):undefined});
|
|
|
|
|
|
if(res.status===401){location.assign('/login');throw Error('登录已过期')}
|
|
|
|
|
|
const result=await res.json();if(!res.ok)throw Error(result.detail||'请求失败');return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
const post=(url,body)=>request(url,{method:'POST',body});
|
|
|
|
|
|
function formatJobs(list){return list.length?list.map(j=>`<div class="row"><div><b>${esc(j.kind||'build')} <span class="flag">${esc(j.state)}</span></b><small>${esc(j.id)} · 尝试 ${esc(j.attempts||0)} 次</small></div>${['queued','running','retrying'].includes(j.state)?`<button class="secondary" onclick="cancelJob('${esc(j.id)}')">取消</button>`:''}</div>`).join(''):'当前没有任务';}
|
2026-10-22 17:02:45 +08:00
|
|
|
|
async function chooseSite(){siteId=$('#site').value;window.SiteForgeShell?.selectSite(siteId);lastPreview=null;$('#commitImport').disabled=true;await reload()}
|
2026-10-22 16:49:25 +08:00
|
|
|
|
async function init(){try{csrf=(await request('/api/auth/me')).csrf;sites=await request('/api/sites');$('#site').innerHTML=sites.map(s=>`<option value="${esc(s.id)}">${esc(s.name)}</option>`).join('');siteId=sites[0]?.id||'';$('#site').onchange=chooseSite;$('#reload').onclick=reload;$('#previewImport').onclick=()=>importJSON(false);$('#commitImport').onclick=()=>importJSON(true);$('#build').onclick=buildSite;$('#deploy').onclick=deploySite;$('#refreshRelease').onclick=reload;await reload()}catch(e){notify(e.message,true)}}
|
|
|
|
|
|
async function reload(){if(!siteId)return;try{
|
2026-10-23 12:31:03 +08:00
|
|
|
|
const [list,jobsData,buildData,approvals,history,reviewData,deliveryData]=await Promise.all([
|
|
|
|
|
|
request(`/api/sites/${siteId}`),request(`/api/enterprise/sites/${siteId}/jobs`),request(`/api/sites/${siteId}/builds`),request(`/api/factory/sites/${siteId}/release-requests`),request(`/api/enterprise/sites/${siteId}/releases`),request(`/api/migrations/sites/${siteId}/reviews`),request(`/api/production/sites/${siteId}/delivery-status`)]);
|
2026-10-22 16:49:25 +08:00
|
|
|
|
jobs=jobsData;builds=buildData;releases=history;$('#statBuild').textContent=builds.length;$('#statJobs').textContent=jobs.filter(j=>['queued','running','retrying'].includes(j.state)).length;$('#statApprovals').textContent=approvals.filter(j=>j.state==='pending').length;$('#statMode').textContent=list.mode==='migration'?'迁移':'新建';$('#jobList').innerHTML=formatJobs(jobs);
|
|
|
|
|
|
$('#buildList').innerHTML=builds.length?builds.slice(0,14).map(b=>`<div class="row"><div><b>${esc(b.id)}</b><small>${esc(b.created_at)} · ${esc(b.total_pages)} 页</small></div><button class="secondary" onclick="requestApproval('${esc(b.id)}')">提交审批</button></div>`).join(''):'当前没有构建';
|
|
|
|
|
|
$('#releaseBuild').innerHTML=builds.map(b=>`<option value="${esc(b.id)}">${esc(b.id)} · ${esc(b.total_pages)} 页</option>`).join('');
|
|
|
|
|
|
$('#approvals').innerHTML=approvals.length?approvals.slice(0,12).map(a=>`<div class="row"><div><b>${esc(a.build_id)} <span class="flag ${a.state==='pending'?'warn':''}">${esc(a.state)}</span></b><small>由 ${esc(a.requested_by)} 提交</small></div>${a.state==='pending'?`<button class="secondary" onclick="approve('${esc(a.id)}')">批准</button>`:''}</div>`).join(''):'暂无审批记录';
|
|
|
|
|
|
$('#reviews').innerHTML=reviewData.items.length?reviewData.items.map(x=>`<div class="row"><div><b>${esc(x.platform)} · 第 ${esc(x.row_number)} 条</b><small>${esc(x.issue)}</small></div><button class="secondary" onclick="resolveReview('${esc(x.id)}')">已检查并处理</button></div>`).join(''):'暂无待复核事项';
|
2026-10-23 12:31:03 +08:00
|
|
|
|
$('#releaseBackend').textContent=['staging','production'].map(t=>`${t}: ${deliveryData.backends?.[t]||'agent'} · ${deliveryData.configured?.[t]?'已配置':'未配置'}`).join(' | ');
|
|
|
|
|
|
$('#releases').innerHTML=deliveryData.history?.length?deliveryData.history.slice(0,10).map(x=>`<div class="row"><div><b>${esc(x.build_id)} · ${esc(x.target)} · ${esc(x.release_id)}</b><small>${esc(x.deployed_at)}</small></div><button class="secondary" onclick="rollbackRelease('${esc(x.release_id)}','${esc(x.target)}')">回滚</button></div>`).join(''): '暂无远程发布记录';
|
2026-10-22 16:49:25 +08:00
|
|
|
|
const entries=await request(`/api/sites/${siteId}/entries`);const entry=entries.find(p=>p.route==='/')||entries[0];$('#edit').href=entry?`/editor/${siteId}/${entry.id}`:'/factory-studio';
|
|
|
|
|
|
}catch(e){notify(e.message,true)}}
|
|
|
|
|
|
async function importJSON(commit){const file=$('#file').files?.[0];if(!file)return notify('请选择 JSON 文件',true);if(!siteId)return;const platform=$('#platform').value;
|
|
|
|
|
|
if(commit&&!lastPreview)return;const data=new FormData();data.append('platform',platform);data.append('file',file);if(commit)data.append('conflict','skip');
|
|
|
|
|
|
try{const result=await request(`/api/migrations/sites/${siteId}/${commit?'import':'preview'}`,{method:'POST',body:data,upload:true});if(commit){notify(`导入完成:${result.result.imported} 条,人工复核 ${result.migration_warnings.length} 项`);lastPreview=null;$('#commitImport').disabled=true;await reload()}else{lastPreview=result;$('#commitImport').disabled=result.errors.length>0;$('#report').innerHTML=`<b>输入 ${result.input} 条 · 可映射 ${result.mapped} 条 · 错误 ${result.errors.length} 条 · 复核项 ${result.warnings.length} 条</b><div>${result.errors.slice(0,12).map(x=>`<p>第${x.row}条:${esc(x.error)}</p>`).join('')}${result.warnings.slice(0,12).map(x=>`<p>待复核 第${x.row}条:${esc(x.issue)}</p>`).join('')}</div>`}}
|
|
|
|
|
|
catch(e){notify(e.message,true)}}
|
|
|
|
|
|
async function resolveReview(id){
|
|
|
|
|
|
const answer=prompt('请详细填写对照旧站完成检查/修复的说明(至少12个字符)。未经核查不要批准。');
|
|
|
|
|
|
if(answer===null)return;
|
|
|
|
|
|
try{await post(`/api/migrations/sites/${siteId}/reviews/${id}/resolve`,{resolution:answer});notify('复核记录已保存');await reload()}catch(e){notify(e.message,true)}
|
|
|
|
|
|
}
|
|
|
|
|
|
async function buildSite(){try{const task=await post(`/api/sites/${siteId}/builds`,{mirror_images:false});notify(`任务 ${task.id} 已进入 Worker 队列`);await reload()}catch(e){notify(e.message,true)}}
|
|
|
|
|
|
async function cancelJob(id){try{await post(`/api/enterprise/sites/${siteId}/jobs/${id}/cancel`,{});await reload()}catch(e){notify(e.message,true)}}
|
|
|
|
|
|
async function requestApproval(build){try{await post(`/api/factory/sites/${siteId}/release-requests`,{build_id:build});notify('已提交发布审批');await reload()}catch(e){notify(e.message,true)}}
|
|
|
|
|
|
async function approve(id){if(!confirm('审批人必须与申请人不同。确定审批通过吗?'))return;try{await post(`/api/factory/sites/${siteId}/release-requests/${id}/review`,{state:'approved'});notify('审批已完成');await reload()}catch(e){notify(e.message,true)}}
|
2026-10-23 12:31:03 +08:00
|
|
|
|
async function deploySite(){if(!confirm('系统将使用目标环境预先配置的发布适配器。请确认构建已审批、DNS 与版本回退方案已准备好。继续发布?'))return;try{const data=await post(`/api/production/sites/${siteId}/deploy`,{build_id:$('#releaseBuild').value,target:$('#target').value});notify(`发布任务 ${data.id} 已提交,Worker 将先上传、校验,再切换线上版本。`);await reload()}catch(e){notify(e.message,true)}}
|
|
|
|
|
|
async function rollbackRelease(release,target){if(!confirm(`确认将 ${target} 恢复为历史版本 ${release}?`))return;
|
|
|
|
|
|
try{const result=await post(`/api/production/sites/${siteId}/rollback`,{release_id:release,target});notify(`回滚任务 ${result.id} 已进入队列`);await reload()}catch(e){notify(e.message,true)}}
|
|
|
|
|
|
window.rollbackRelease=rollbackRelease;
|
2026-10-22 16:49:25 +08:00
|
|
|
|
window.resolveReview=resolveReview;window.cancelJob=cancelJob;window.requestApproval=requestApproval;window.approve=approve;
|
|
|
|
|
|
window.addEventListener('DOMContentLoaded',init);
|