Files
浅念 601451f22b upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)
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>
2026-10-23 12:31:03 +08:00

168 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>