601451f22b
Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.
New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.
compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).
Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.
Co-Authored-By: Claude Code <noreply@anthropic.com>
168 lines
32 KiB
HTML
168 lines
32 KiB
HTML
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge · 数据与迁移工具</title>
|
||
<style>
|
||
:root{font-family:Inter,"Microsoft YaHei",system-ui,sans-serif;color:#202a39;background:#f4f7fb}*{box-sizing:border-box}body{margin:0}header{display:flex;align-items:center;justify-content:space-between;background:#101d31;color:white;padding:18px 28px;gap:16px}header h1{font-size:18px;margin:0}header small{color:#a4b9d4}main{max-width:1320px;margin:24px auto;padding:0 20px}.pill{border-radius:20px;background:#eaf2ff;color:#164a8d;font-size:12px;padding:5px 10px}.muted{color:#6d7685;font-size:13px}.row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.nav{display:flex;gap:10px}.nav a{color:#d8e8f9;font-size:13px;text-decoration:none}.kpi{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:18px 0}.kpi article,.panel{background:white;border:1px solid #e1e8f1;border-radius:12px;padding:20px;box-shadow:0 2px 12px #10203806}.kpi strong{display:block;font-size:25px;margin-top:8px}.layout{display:grid;grid-template-columns:1fr 1fr;gap:18px}.panel h2{font-size:17px;margin:0 0 6px}.panel h3{font-size:14px;margin:16px 0 8px}label{display:block;font-size:12px;color:#526075;font-weight:600;margin-top:13px;margin-bottom:5px}input,select,textarea{width:100%;padding:10px 11px;font:inherit;font-size:13px;border:1px solid #cbd6e6;border-radius:7px;background:white}textarea{min-height:115px;resize:vertical}button{cursor:pointer;border:1px solid #c5d2e4;border-radius:8px;background:white;color:#213249;font-weight:600;font-size:13px;padding:10px 14px}button:hover{border-color:#2274bd}button.primary{background:#1658a6;color:#fff;border-color:#1658a6}button.danger{color:#b22d2d}button:disabled{opacity:.45;cursor:not-allowed}hr{border:0;border-top:1px solid #e8edf5;margin:20px 0}.message{white-space:pre-wrap;font-size:12px;line-height:1.55;overflow:auto;max-height:270px;background:#f5f8fc;padding:12px;border-radius:8px}.results{max-height:270px;overflow:auto}.results>div{padding:10px;border-bottom:1px solid #eef1f5;font-size:13px;display:flex;gap:10px;justify-content:space-between}.foot{margin:20px 0;color:#6f7b8b;font-size:12px}.ok{color:#176d45}.err{color:#c0392b}@media(max-width:850px){.layout,.kpi{grid-template-columns:1fr}header{align-items:flex-start;flex-direction:column}main{margin:12px auto}}
|
||
|
||
[hidden]{display:none!important}.tool-switch{display:flex;flex-wrap:wrap;gap:9px;background:#fff;padding:12px;margin:18px 0 0;border-radius:12px;border:1px solid #e1e8f1}.tool-btn{background:#f6f8fc;color:#50627c;border-color:transparent;padding:11px 14px}.tool-btn.active{background:#e8f0ff;color:#174ea7;border:1px solid #a5c5f5}.tool-help{font-size:12px;color:#64748b;margin:12px 5px 16px}.advanced-layout{grid-template-columns:minmax(0,1fr)}.advanced-layout>.panel{width:100%}.nav a:first-child{border:1px solid #7793bd;padding:9px 11px;border-radius:7px;background:#fff1}@media(max-width:720px){.tool-switch{gap:5px}.tool-btn{padding:9px 11px}}
|
||
</style><link rel="stylesheet" href="/ui/professional-ui.css"><script defer src="/ui/professional-ui.js"></script></head><body data-sf-page="production">
|
||
<header><div><h1>SiteForge · 网站生产控制台 <span style="font-size:11px;color:#acd5ff">V8.21 · 高级工具</span></h1><small>整站模板 · 数据批次导入 · 可审计回滚 · 静态构建</small></div><div class="nav"><a href="/">返回网站工作台</a><a href="/factory-studio">页面编辑器</a><a href="/theme-gallery">主题中心</a><a href="/operations">任务中心</a></div></header>
|
||
<main>
|
||
<div class="panel"><div class="row" style="justify-content:space-between"><div><h2>选择要操作的网站</h2><div class="muted">所有批次、模板和构建记录均按网站隔离;新网站可先在网站工作台创建。</div></div><select id="site" style="max-width:410px" aria-label="选择网站"></select></div></div>
|
||
<section class="kpi"><article><span class="muted">当前网站</span><strong id="siteName">—</strong></article><article><span class="muted">内容记录</span><strong id="entryCount">—</strong></article><article><span class="muted">导入批次</span><strong id="batchCount">—</strong></article></section>
|
||
<div class="tool-switch" role="tablist" aria-label="高级工具分类">
|
||
<button type="button" class="tool-btn active" data-tool="batch" role="tab" aria-selected="true">① 批次导入</button>
|
||
<button type="button" class="tool-btn" data-tool="templates" role="tab" aria-selected="false">② 整站模板</button>
|
||
<button type="button" class="tool-btn" data-tool="wp" role="tab" aria-selected="false">③ WordPress 迁移</button>
|
||
<button type="button" class="tool-btn" data-tool="api" role="tab" aria-selected="false">④ XML / API</button>
|
||
<button type="button" class="tool-btn" data-tool="mysql" role="tab" aria-selected="false">⑤ MySQL 数据源</button>
|
||
</div><p class="tool-help" id="toolHelp" role="status">先核对字段与 URL 冲突,再将数据正式写入网站。</p>
|
||
<section class="layout advanced-layout">
|
||
<div class="panel" id="tool-batch"><h2>批次数据导入</h2><div class="muted">先预览字段与 URL 冲突,确认后再提交;支持按批次回滚且保护导入后的人工作业。</div>
|
||
<label for="source">数据格式</label><select id="source"><option value="json">JSON 行数组</option><option value="wxr">WordPress WXR XML 文件</option><option value="table">Excel / CSV / JSON 文件</option></select>
|
||
<label for="kind">内容类型</label><select id="kind"><option value="product">产品</option><option value="vehicle">车型</option><option value="page">普通页面</option><option value="post">新闻文章</option><option value="case">案例</option><option value="brand">品牌</option><option value="service">服务</option></select>
|
||
<div id="jsonInputs"><label for="records">JSON 数据</label><textarea id="records" spellcheck="false">[{"车型名称":"Example Vehicle A","品牌":"Example Brand","动力形式":"EV"}]</textarea>
|
||
<div class="muted">第一行字段自动建议映射。中文字段可对应标题、品牌、分类、参数等。</div></div>
|
||
<div id="tableInputs" style="display:none"><label for="tableFile">上传 XLSX / CSV / JSON</label><input type="file" id="tableFile" accept=".xlsx,.csv,.json"></div><div id="wxrInputs" style="display:none"><label for="wxrFile">上传 WordPress 全量 WXR XML</label><input type="file" id="wxrFile" accept=".xml"></div>
|
||
<div class="row" style="margin-top:16px"><button class="primary" id="stage">1. 校验并暂存</button><button id="refreshBatch">刷新批次</button></div><pre class="message" id="importResult">请选择网站后导入。</pre>
|
||
<h3>已暂存/已导入批次</h3><div class="results" id="batches"></div>
|
||
</div>
|
||
<div class="panel" id="tool-templates" hidden><h2>整站模板生产线</h2><div class="muted">保存当前网站的页面区块、导航与设计令牌。模板不复制产品、文章、域名、Logo、SEO 或用户密钥。</div>
|
||
<label for="snapshotName">新模板名称</label><input id="snapshotName" value="企业官网整站模板"><button id="saveTemplate" style="margin-top:10px" class="primary">保存当前网站的模板快照</button>
|
||
<hr><label for="sourceSite">模板来源网站</label><select id="sourceSite"></select><label for="templateId">模板版本</label><select id="templateId"></select>
|
||
<div class="row" style="margin-top:14px"><button id="preflight">2. 兼容性预检</button><button id="apply" class="primary">3. 暂存整站模板</button><button id="publishTemplate" class="primary">4. Owner 发布模板</button></div>
|
||
<pre class="message" id="templateResult">先保存模板,再在另一网站上预检并应用。若存在同路径页面,需显式授权替换布局。</pre>
|
||
<hr><h3>静态网站生产</h3><div class="muted">使用现有独立 Worker 构建队列,提交后到任务中心检查超时、重试和取消。</div>
|
||
<div class="row" style="margin-top:12px"><button class="primary" id="build">提交静态构建任务</button><a href="/operations">查看任务进度 →</a></div>
|
||
<pre class="message" id="buildResult">尚未提交</pre>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel" id="tool-wp" style="margin-top:18px" aria-labelledby="wpTitle" hidden>
|
||
<h2 id="wpTitle">WordPress 全站迁移工作台</h2>
|
||
<div class="muted">适用于你有权迁移的公开 WordPress 网站。每次安全读取一页(最多 50 条),保留进度,所有内容先暂存,确认后才生成原站 URL/SEO 基线。旧站复杂插件与页面效果仍须单独复核。</div>
|
||
<div class="row" style="align-items:flex-end;margin-top:12px">
|
||
<div style="flex:2;min-width:250px"><label for="wpOrigin">原 WordPress 网站地址</label><input id="wpOrigin" placeholder="https://old.example.com/"></div>
|
||
<div style="flex:1;min-width:140px"><label for="wpLimit">最多采集记录</label><input id="wpLimit" type="number" min="1" max="20000" value="2000"></div>
|
||
<button id="wpCreate" class="primary">新建迁移会话</button>
|
||
</div>
|
||
<div class="row" style="margin-top:10px">
|
||
<select aria-label="WordPress 迁移会话" id="wpSession" style="min-width:240px;max-width:430px"></select>
|
||
<button id="wpNext">继续采集一页</button><button id="wpWorker" class="primary">提交 Worker 后台采集</button><button id="wpSitemap">发现 Sitemap</button><button id="wpMedia">本地化图片(20 张)</button><select id="wpCatalogType" aria-label="同步 WordPress 目录"><option value="categories">分类</option><option value="tags">标签</option><option value="media">媒体库</option></select><button id="wpCatalogSync">同步目录下一页</button><button id="wpCatalogStatus">目录进度</button><button id="wpAuto">连续采集</button><button id="wpStop">停止连续采集</button>
|
||
<button id="wpFinalize" class="primary">确认并导入全部暂存</button>
|
||
<button id="wpCancel" class="danger">取消迁移</button>
|
||
<button id="wpReload">刷新状态</button><button id="wpAudit">对比最新构建的 URL/SEO</button>
|
||
</div>
|
||
<div class="row" style="margin-top:12px;align-items:flex-end">
|
||
<div style="flex:2;min-width:250px"><label for="wpPreflightFile">V8.20 离线 WXR 兼容预检(不会导入或修改内容)</label><input id="wpPreflightFile" type="file" accept=".xml,application/xml"></div>
|
||
<button id="wpPreflight" class="primary">检查旧 URL / Elementor / ACF / 动态功能</button>
|
||
</div>
|
||
<pre id="wpPreflightResult" class="message">上传有权迁移的 WordPress WXR 文件,可检查路径冲突、复杂字段和动态功能。预检不等于迁移完成。</pre>
|
||
<pre id="wpResult" class="message">选择站点并新建迁移会话。每页请求独立保存检查点;采集完成后可明确审核 URL、SEO、缺失内容。</pre>
|
||
</section>
|
||
|
||
<section class="panel" id="tool-api" style="margin-top:18px" aria-labelledby="p3Title" hidden>
|
||
<h2 id="p3Title">P3 · XML / HTTP API 数据连接器</h2>
|
||
<div class="muted">XML 仅支持扁平或两层记录,拒绝外部实体;HTTP API 支持公开或密钥引用认证的 HTTPS JSON。全部数据先进入暂存批次,需要单独审核并提交;API 采集在 Worker 后台执行。</div>
|
||
<div class="layout" style="margin-top:12px">
|
||
<div>
|
||
<h3>XML 导入</h3>
|
||
<label for="xmlFile">XML 文件(最大 8 MB)</label><input id="xmlFile" type="file" accept=".xml,text/xml,application/xml">
|
||
<label for="xmlRecord">记录节点标签(如 product / item)</label><input id="xmlRecord" value="item">
|
||
<label for="xmlMapping">字段映射 JSON(可为空自动识别)</label><textarea id="xmlMapping" spellcheck="false" rows="3">{}</textarea>
|
||
<button class="primary" id="xmlStage">校验并暂存 XML</button>
|
||
</div>
|
||
<div>
|
||
<h3>JSON API(公开或认证)</h3>
|
||
<label for="apiUrl">HTTPS 数据接口</label><input id="apiUrl" placeholder="https://api.example.com/v1/products">
|
||
<label for="apiAuth">认证方式</label><select id="apiAuth"><option value="none">公开 API</option><option value="bearer">Bearer Token</option><option value="api_key">X-API-Key</option><option value="custom">自定义 X- 请求头</option></select>
|
||
<label for="apiSecretRef">密钥引用(仅 Worker 读取,不填写密钥本身)</label><input id="apiSecretRef" placeholder="file:partner_api_token 或 env:SITEFORGE_API_TOKEN">
|
||
<label for="apiAuthHeader">自定义请求头(仅 custom)</label><input id="apiAuthHeader" value="X-Partner-Key">
|
||
<label for="apiItems">列表 JSON 路径</label><input id="apiItems" value="data.items">
|
||
<div class="row"><div style="flex:1"><label for="apiPageSize">每页数量</label><input id="apiPageSize" type="number" value="50" min="1" max="100"></div><div style="flex:1"><label for="apiMaxPages">最多页数</label><input id="apiMaxPages" type="number" value="10" min="1" max="50"></div></div>
|
||
<label for="apiMapping">字段映射 JSON(可为空自动识别)</label><textarea id="apiMapping" spellcheck="false" rows="3">{}</textarea>
|
||
<div class="row"><button class="primary" id="apiCreate">创建 API 数据源</button><button id="apiReload">刷新采集记录</button></div>
|
||
<label for="apiSource">已保存的数据源</label><select id="apiSource" aria-label="API 数据源"></select>
|
||
<div class="row"><button class="primary" id="apiRun">提交 Worker 采集</button><button id="apiStatus">查看采集与批次</button><button class="danger" id="apiCancel">取消采集</button></div>
|
||
</div>
|
||
</div>
|
||
<pre class="message" id="p3Result">创建数据源、提交 Worker 后到“任务中心”查看执行;采集完后到上方“已暂存批次”确认导入。</pre>
|
||
</section>
|
||
<section class="panel" id="tool-mysql" style="margin-top:18px" aria-labelledby="mysqlTitle" hidden>
|
||
<h2 id="mysqlTitle">P3 · 只读 MySQL 连接器</h2>
|
||
<div class="muted">需要管理员预先在服务器环境配置允许的主机和数据库表,并建立数据库侧仅 SELECT 的账号。强制 TLS 证书校验,拒绝任意 SQL;数据分批暂存,不自动发布。</div>
|
||
<div class="layout">
|
||
<div><label for="myHost">管理员批准的 MySQL 主机</label><input id="myHost" placeholder="db.internal.example">
|
||
<label for="myDatabase">数据库名</label><input id="myDatabase" placeholder="catalog">
|
||
<label for="myTable">表或视图</label><input id="myTable" placeholder="products_public">
|
||
<label for="myKey">有唯一索引的游标列</label><input id="myKey" value="id">
|
||
<label for="myColumns">允许读取的列(英文逗号分隔)</label><input id="myColumns" placeholder="id,name,brand,category">
|
||
</div>
|
||
<div><label for="myUserRef">MySQL 只读账号密钥引用</label><input id="myUserRef" placeholder="file:mysql_reader_user">
|
||
<label for="myPassRef">MySQL 密码密钥引用</label><input id="myPassRef" placeholder="file:mysql_reader_password">
|
||
<label for="myMapping">字段映射 JSON</label><textarea id="myMapping" rows="2">{"title":"name","brand":"brand"}</textarea>
|
||
<div class="row"><div style="flex:1"><label for="myPageSize">单页</label><input id="myPageSize" type="number" value="100" min="1" max="250"></div><div style="flex:1"><label for="myMaxPages">最多页数</label><input id="myMaxPages" type="number" value="10" min="1" max="50"></div></div>
|
||
<div class="row"><button class="primary" id="myCreate">创建 MySQL 数据源</button><button id="myReload">刷新</button></div>
|
||
<label for="mySource">已保存的数据源</label><select id="mySource" aria-label="MySQL 数据源"></select>
|
||
<div class="row"><button class="primary" id="myRun">提交后台采集</button><button id="myStatus">检查批次</button><button class="danger" id="myCancel">取消</button></div>
|
||
</div>
|
||
</div>
|
||
<pre class="message" id="myResult">配置源数据后交由 Worker 执行;使用最小权限账号、TLS CA 证书和服务器侧白名单。</pre>
|
||
</section>
|
||
<p class="foot">提示:V8 当前是分阶段开发版本。远程正式发布、Art Design Pro 官方工程编译、复杂 WordPress 组件无损迁移仍需专项验收。</p>
|
||
</main><script>
|
||
'use strict';const $=id=>document.getElementById(id);let sites=[],csrf='',templates=[];
|
||
const toolNames=['batch','templates','wp','api','mysql'];
|
||
const toolHelp={batch:'先核对字段与 URL 冲突,再将数据正式写入网站。',templates:'保存整站布局 → 预检 → 暂存 → Owner 确认应用。',wp:'逐页采集旧站、同步图片与分类;确认导入后进行 SEO 验收。',api:'先建立 XML 或 HTTPS API 数据源,执行后在“批次导入”中审核。',mysql:'此处面向管理员:只读账号、TLS 和数据源白名单必须事先配置。'};
|
||
function changeTool(key){if(!toolNames.includes(key))return;for(const k of toolNames){const el=$('tool-'+k);el.hidden=k!==key;}for(const btn of document.querySelectorAll('[data-tool]')){let yes=btn.dataset.tool===key;btn.classList.toggle('active',yes);btn.setAttribute('aria-selected',yes?'true':'false');btn.tabIndex=yes?0:-1;}$('toolHelp').textContent=toolHelp[key]||'';window.scrollTo({top:0,behavior:'instant'})}
|
||
document.querySelectorAll('[data-tool]').forEach(btn=>btn.addEventListener('click',()=>changeTool(btn.dataset.tool)));
|
||
|
||
async function api(path,method='GET',body){const opts={method,credentials:'same-origin',headers:{'Accept':'application/json'}};if(method!=='GET')opts.headers['X-SiteForge-CSRF']=csrf;if(body instanceof FormData)opts.body=body;else if(body!==undefined){opts.headers['Content-Type']='application/json';opts.body=JSON.stringify(body)}const r=await fetch(path,opts);let data;try{data=await r.json()}catch{data={detail:'服务器返回非 JSON'}}if(!r.ok)throw Error(typeof data.detail==='string'?data.detail:JSON.stringify(data.detail||data));return data}
|
||
function show(id,obj){$(id).textContent=typeof obj==='string'?obj:JSON.stringify(obj,null,2)}
|
||
function current(){return $('site').value}function opt(value,text){const el=document.createElement('option');el.value=value;el.textContent=text;return el}
|
||
async function load(){try{const me=await api('/api/auth/me');csrf=me.csrf;sites=await api('/api/sites');$('site').replaceChildren(...sites.map(s=>opt(s.id,s.name)));$('sourceSite').replaceChildren(...sites.map(s=>opt(s.id,s.name)));await refresh();}catch(e){alert('需要先登录或加载失败:'+e.message);location.href='/login'}}
|
||
async function refresh(){const s=sites.find(x=>x.id===current());if(!s)return;$('siteName').textContent=s.name;$('entryCount').textContent=s.entries_count;await loadBatches();await loadTemplates();await loadWpSessions();await loadApiSources();await loadMySources()}
|
||
async function loadBatches(){try{const data=await api(`/api/factory/sites/${current()}/staging`);$('batchCount').textContent=data.items.length;const box=$('batches');box.replaceChildren();for(const b of data.items){const div=document.createElement('div');const title=document.createElement('span');title.textContent=`${b.origin} · ${b.state} · ${b.input_count} 条`;div.append(title);const inspect=document.createElement('button');inspect.textContent='关系检查';inspect.onclick=async()=>{try{show('importResult',await api(`/api/connectors/sites/${current()}/import-batches/${b.id}/relationships`))}catch(e){show('importResult','关系检查失败:'+e.message)}};div.append(inspect);if(b.state==='staged'){const c=document.createElement('button');c.textContent='提交';c.onclick=()=>processBatch(b.id,'commit');div.append(c)}if(['committed','partial'].includes(b.state)){const c=document.createElement('button');c.textContent='回滚';c.className='danger';c.onclick=()=>processBatch(b.id,'rollback');div.append(c)}box.append(div)}}catch(e){show('importResult',e.message)}}
|
||
async function processBatch(id,action){let payload={};if(action==='commit'){const c=confirm('确认提交本批数据?遇到已存在的路径,默认跳过,不覆盖。');if(!c)return;payload={on_conflict:'skip'}}else if(!confirm('确认回滚本批导入?导入后被人工修改的内容将被保护,不能强制覆盖。'))return;try{show('importResult',await api(`/api/factory/sites/${current()}/staging/${id}/${action}`,'POST',payload));await loadBatches()}catch(e){show('importResult','错误:'+e.message)}}
|
||
$('source').onchange=()=>{$('jsonInputs').style.display=$('source').value==='json'?'block':'none';$('wxrInputs').style.display=$('source').value==='wxr'?'block':'none';$('tableInputs').style.display=$('source').value==='table'?'block':'none'};
|
||
$('site').onchange=refresh;$('sourceSite').onchange=loadTemplates;
|
||
$('stage').onclick=async()=>{try{let data;if($('source').value==='table'){const form=new FormData();if(!$('tableFile').files[0])throw Error('请选择 Excel/CSV/JSON 文件');form.append('file',$('tableFile').files[0]);form.append('content_type',$('kind').value);data=await api(`/api/factory/sites/${current()}/staging/file`,'POST',form)}else if($('source').value==='wxr'){const form=new FormData();if(!$('wxrFile').files[0])throw Error('请选择 WXR XML');form.append('file',$('wxrFile').files[0]);data=await api(`/api/factory/sites/${current()}/staging/wxr`,'POST',form)}else{const rows=JSON.parse($('records').value);data=await api(`/api/factory/sites/${current()}/staging`,'POST',{rows,content_type:$('kind').value})}show('importResult',data);await loadBatches()}catch(e){show('importResult','错误:'+e.message)}};
|
||
$('refreshBatch').onclick=loadBatches;
|
||
$('saveTemplate').onclick=async()=>{try{show('templateResult',await api(`/api/factory/sites/${current()}/whole-site-templates`,'POST',{name:$('snapshotName').value}));await loadTemplates()}catch(e){show('templateResult','错误:'+e.message)}};
|
||
async function loadTemplates(){const source=$('sourceSite').value;if(!source)return;try{templates=await api(`/api/factory/sites/${source}/whole-site-templates`);$('templateId').replaceChildren(...templates.map(t=>opt(t.id,`${t.name} · ${t.version}`)))}catch(e){show('templateResult',e.message)}}
|
||
async function applyTemplate(confirmApply){try{const payload={source_site_id:$('sourceSite').value,template_id:$('templateId').value,confirm:confirmApply,overwrite_layouts:confirmApply};if(!payload.template_id)throw Error('请先保存或选择整站模板');const document=await api(`/api/factory/sites/${payload.source_site_id}/whole-site-templates/${payload.template_id}`);const report=await api(`/api/factory/sites/${current()}/whole-site-templates/preflight`,'POST',{template:document});if(!report.compatible||!confirmApply){show('templateResult',report);return}if(!confirm('兼容性通过。应用模板将替换同路径页面布局,但保留内容及 SEO。确认?'))return;const draft=await api(`/api/factory/sites/${current()}/whole-site-templates/staged/current`);payload.expected_revision=draft.revision||0;show('templateResult',await api(`/api/factory/sites/${current()}/whole-site-templates/apply`,'POST',payload))}catch(e){show('templateResult','错误:'+e.message)}}
|
||
$('preflight').onclick=()=>applyTemplate(false);$('apply').onclick=()=>applyTemplate(true);$('publishTemplate').onclick=async()=>{try{const s=await api(`/api/factory/sites/${current()}/whole-site-templates/staged/current`);if(s.state!=='draft')throw Error('没有待发布的整站模板');if(s.conflict)throw Error('网站已经变化,请重新预检并暂存');if(!confirm('将由 Owner 正式应用整站布局,并保留原网站业务内容与 SEO。继续?'))return;show('templateResult',await api(`/api/factory/sites/${current()}/whole-site-templates/staged/publish`,'POST',{expected_revision:s.revision}))}catch(e){show('templateResult','错误:'+e.message)}};
|
||
$('build').onclick=async()=>{try{show('buildResult',await api(`/api/sites/${current()}/builds`,'POST',{mirror_images:false}))}catch(e){show('buildResult','错误:'+e.message)}};
|
||
async function loadWpSessions(){try{if(!current())return;const result=await api(`/api/migrations/sites/${current()}/wordpress/sessions`);const select=$('wpSession');const chosen=select.value;select.replaceChildren(...result.items.map(s=>opt(s.id,`${s.origin} · ${s.state} · ${s.fetched} 条`)));if(result.items.some(s=>s.id===chosen))select.value=chosen;if(select.value){const item=await api(`/api/migrations/sites/${current()}/wordpress/sessions/${select.value}`);show('wpResult',item)}}catch(e){show('wpResult','查询错误:'+e.message)}}
|
||
async function wpAction(action){const sid=$('wpSession').value;if(!sid)throw Error('请先创建迁移会话');return api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/${action}`,'POST',{})}
|
||
$('wpPreflight').onclick=async()=>{try{if(!current())throw Error('请先选择网站');const file=$('wpPreflightFile').files[0];if(!file)throw Error('请选择 WordPress WXR XML 文件');const form=new FormData();form.append('file',file);const result=await api(`/api/migrations/sites/${current()}/wordpress/wxr-preflight`,'POST',form);show('wpPreflightResult',result)}catch(e){show('wpPreflightResult','兼容性预检失败:'+e.message)}};
|
||
$('wpCreate').onclick=async()=>{try{const data=await api(`/api/migrations/sites/${current()}/wordpress/sessions`,'POST',{origin:$('wpOrigin').value,max_items:Number($('wpLimit').value)});await loadWpSessions();$('wpSession').value=data.id;show('wpResult',data)}catch(e){show('wpResult','错误:'+e.message)}};
|
||
let wpAutoRunning=false;
|
||
$('wpAuto').onclick=async()=>{if(wpAutoRunning)return;wpAutoRunning=true;let count=0;try{while(wpAutoRunning&&count<200){const r=await wpAction('advance');show('wpResult',r);count++;if(['ready','completed','cancelled','limit_reached'].includes(r.state))break;await new Promise(done=>setTimeout(done,350))}await loadWpSessions()}catch(e){show('wpResult','暂停:'+e.message)}finally{wpAutoRunning=false}};
|
||
$('wpStop').onclick=()=>{wpAutoRunning=false};
|
||
$('wpWorker').onclick=async()=>{try{const sid=$('wpSession').value;show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/scan`,'POST',{}));await loadWpSessions()}catch(e){show('wpResult','提交失败:'+e.message)}};
|
||
$('wpSitemap').onclick=async()=>{try{const sid=$('wpSession').value;show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/sitemap`,'POST',{}));}catch(e){show('wpResult','Sitemap 失败:'+e.message)}};
|
||
$('wpMedia').onclick=async()=>{try{const sid=$('wpSession').value;show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/media/localize`,'POST',{limit:20}));}catch(e){show('wpResult','媒体迁移失败:'+e.message)}};
|
||
$('wpCatalogSync').onclick=async()=>{try{const sid=$('wpSession').value;const resource=$('wpCatalogType').value;show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${sid}/catalog/${resource}/next`,'POST',{}));}catch(e){show('wpResult','目录同步失败:'+e.message)}};
|
||
$('wpCatalogStatus').onclick=async()=>{try{show('wpResult',await api(`/api/migrations/sites/${current()}/wordpress/sessions/${$('wpSession').value}/catalog`));}catch(e){show('wpResult','读取目录失败:'+e.message)}};
|
||
$('wpNext').onclick=async()=>{try{show('wpResult',await wpAction('advance'));await loadWpSessions()}catch(e){show('wpResult','错误:'+e.message)}};
|
||
$('wpFinalize').onclick=async()=>{if(!confirm('采集完成后,确认将全部暂存内容正式导入并登记原 URL/SEO 基线?'))return;try{show('wpResult',await wpAction('finalize'));await loadWpSessions();await loadBatches()}catch(e){show('wpResult','错误:'+e.message)}};
|
||
$('wpCancel').onclick=async()=>{if(!confirm('取消此迁移会话?已经暂存的批次不会自动删除。'))return;try{show('wpResult',await wpAction('cancel'));await loadWpSessions()}catch(e){show('wpResult','错误:'+e.message)}};
|
||
$('wpReload').onclick=loadWpSessions;
|
||
$('wpAudit').onclick=async()=>{try{const builds=await api(`/api/sites/${current()}/builds`);const latest=builds.find(b=>b.status==='complete'||b.status==='done'||b.status==='success'||b.status==='completed');if(!latest)throw Error('尚未找到已完成的静态构建版本');const result=await api(`/api/audit/sites/${current()}/builds/${latest.id}/report`);show('wpResult',{build_id:latest.id,...result})}catch(e){show('wpResult','SEO 验收错误:'+e.message)}};
|
||
$('wpSession').onchange=loadWpSessions;
|
||
|
||
async function loadApiSources(){if(!current())return;try{const res=await api(`/api/connectors/sites/${current()}/api-sources`);const el=$('apiSource'),was=el.value;el.replaceChildren(...res.items.map(s=>opt(s.id,`${s.url} · ${s.state} · ${s.staged_count} 条`)));if(res.items.some(x=>x.id===was))el.value=was;}catch(e){show('p3Result','查询 API 数据源失败:'+e.message)}}
|
||
$('xmlStage').onclick=async()=>{try{if(!$('xmlFile').files[0])throw Error('请选择 XML 文件');const form=new FormData();form.append('file',$('xmlFile').files[0]);form.append('record_tag',$('xmlRecord').value);form.append('content_type',$('kind').value);form.append('mapping_json',$('xmlMapping').value);show('p3Result',await api(`/api/connectors/sites/${current()}/xml/staging`,'POST',form));await loadBatches()}catch(e){show('p3Result','XML 导入失败:'+e.message)}};
|
||
$('apiCreate').onclick=async()=>{try{const result=await api(`/api/connectors/sites/${current()}/api-sources`,'POST',{url:$('apiUrl').value,items_path:$('apiItems').value,content_type:$('kind').value,page_size:Number($('apiPageSize').value),max_pages:Number($('apiMaxPages').value),mapping:JSON.parse($('apiMapping').value||'{}'),auth_type:$('apiAuth').value,secret_ref:$('apiAuth').value==='none'?'':$('apiSecretRef').value,auth_header:$('apiAuthHeader').value});await loadApiSources();$('apiSource').value=result.id;show('p3Result',result)}catch(e){show('p3Result','创建失败:'+e.message)}};
|
||
$('apiReload').onclick=loadApiSources;
|
||
$('apiRun').onclick=async()=>{try{if(!$('apiSource').value)throw Error('请先选择数据源');show('p3Result',await api(`/api/connectors/sites/${current()}/api-sources/${$('apiSource').value}/scan`,'POST',{}));await loadApiSources()}catch(e){show('p3Result','提交失败:'+e.message)}};
|
||
$('apiStatus').onclick=async()=>{try{show('p3Result',await api(`/api/connectors/sites/${current()}/api-sources/${$('apiSource').value}`));await loadBatches()}catch(e){show('p3Result','查询失败:'+e.message)}};
|
||
$('apiCancel').onclick=async()=>{if(!confirm('取消当前 API 数据源的后台采集?已暂存的批次不自动删除。'))return;try{show('p3Result',await api(`/api/connectors/sites/${current()}/api-sources/${$('apiSource').value}/cancel`,'POST',{}));await loadApiSources()}catch(e){show('p3Result','取消失败:'+e.message)}};
|
||
async function loadMySources(){if(!current())return;try{const data=await api(`/api/connectors/sites/${current()}/mysql-sources`);const el=$('mySource'),was=el.value;el.replaceChildren(...data.items.map(s=>opt(s.id,`${s.db_name}.${s.table_name} · ${s.state} · ${s.staged_count} 条`)));if(data.items.some(s=>s.id===was))el.value=was}catch(e){show('myResult','读取 MySQL 数据源失败:'+e.message)}}
|
||
$('myCreate').onclick=async()=>{try{const result=await api(`/api/connectors/sites/${current()}/mysql-sources`,'POST',{host:$('myHost').value,database:$('myDatabase').value,table:$('myTable').value,key_column:$('myKey').value,columns:$('myColumns').value.split(',').map(s=>s.trim()).filter(Boolean),user_ref:$('myUserRef').value,password_ref:$('myPassRef').value,page_size:Number($('myPageSize').value),max_pages:Number($('myMaxPages').value),content_type:$('kind').value,mapping:JSON.parse($('myMapping').value||'{}')});await loadMySources();$('mySource').value=result.id;show('myResult',result)}catch(e){show('myResult','创建失败:'+e.message)}};
|
||
$('myReload').onclick=loadMySources;
|
||
$('myRun').onclick=async()=>{try{show('myResult',await api(`/api/connectors/sites/${current()}/mysql-sources/${$('mySource').value}/scan`,'POST',{}))}catch(e){show('myResult','提交失败:'+e.message)}};
|
||
$('myStatus').onclick=async()=>{try{show('myResult',await api(`/api/connectors/sites/${current()}/mysql-sources/${$('mySource').value}`));await loadBatches()}catch(e){show('myResult','查询失败:'+e.message)}};
|
||
$('myCancel').onclick=async()=>{if(!confirm('取消 MySQL 后台采集?已暂存的批次保留。'))return;try{show('myResult',await api(`/api/connectors/sites/${current()}/mysql-sources/${$('mySource').value}/cancel`,'POST',{}));await loadMySources()}catch(e){show('myResult','取消失败:'+e.message)}};
|
||
load();
|
||
</script></body></html>
|