first commit

This commit is contained in:
Longtail committed 2026-10-31 16:15:04 +08:00
commit 1e6504b4a2
41 files changed
+4964

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
'use strict';
const $=id=>document.getElementById(id);
let token='',state=null,loadedKey='',refreshing=false;
const labels={queued:'待处理',preparing:'准备中',importing:'上传与导入',formatting:'排版中',awaiting_user_review:'等待人工审核',user_submitting:'已观察到人工提交',submit_unknown:'发布结果待核对',accepted_pending_moderation:'已受理 · 审核中',published:'已确认公开',needs_attention:'需要处理',cancelled:'已移出'};
function notice(message){$('notice').hidden=!message;$('notice').textContent=message;}
async function api(route,body={}) {
const response=await fetch('/api/'+route,{method:'POST',headers:{'Content-Type':'application/json','X-Session-Token':token},body:JSON.stringify(body)});
const data=await response.json();if(!response.ok)throw new Error(data.error||'请求失败');return data;
}
function action(id,fn){$(id).addEventListener('click',async()=>{const button=$(id);button.disabled=true;try{await fn();await refresh();}catch(error){notice(error.message);}finally{button.disabled=false;}});}
function text(tag,value,className){const e=document.createElement(tag);e.textContent=value;if(className)e.className=className;return e;}
function render(data){
state=data;const {jobs,run,download}=data;
$('total').textContent=jobs.length;$('review').textContent=jobs.filter(j=>j.state==='awaiting_user_review').length;
$('accepted').textContent=jobs.filter(j=>['published','accepted_pending_moderation'].includes(j.state)).length;
$('batch').textContent=run.batch?`${run.batch.count} / ${run.batch.target}`:'—';$('account').textContent=run.batch?run.batch.account.name:'尚未绑定账号';
$('run-state').textContent=run.running?'流程运行中':'流程已暂停';$('run-reason').textContent=run.reason;
$('run-light').classList.toggle('gray',!run.running);$('empty').hidden=jobs.length>0;
$('jobs').replaceChildren(...jobs.map(job=>{
const tr=document.createElement('tr'),title=document.createElement('td');title.append(text('div',job.title),text('small',`${job.image_count} 张配图 · ${job.source}`));if(job.original_title!==job.title)title.append(text('small','已缩短标题,原题:'+job.original_title));
const status=document.createElement('td');status.append(text('span',labels[job.state]||job.state,'badge'));if(job.error)status.append(text('small',job.error));
const account=text('td',job.account?`${job.account.name} / ${job.fid}`:'待领取');const result=document.createElement('td');
if(job.post_url){const link=text('a','查看帖子');link.href=job.post_url;link.target='_blank';link.rel='noopener';result.append(link);}
if(!['published','accepted_pending_moderation'].includes(job.state)){
const retry=text('button','人工核对 / 处理');retry.className='secondary';retry.onclick=async()=>{
if(state.run.running){notice('请先暂停流程,再处理任务');return;}
if(!confirm('只有确认网站未受理这篇文章,且已清理编辑器/草稿后才能继续。\n已受理、审核中或结果不明的文章请取消。\n你是否已经确认未受理?'))return;
const again=confirm('重新加入队列?\n确定:重新排队。取消:移出队列。');
try{await api('resolve',{id:job.id,confirmed_not_accepted:true,retry:again});await refresh();}catch(error){notice(error.message);}
};result.append(retry);
}
tr.append(title,status,account,result);return tr;
}));
const dlLabels={idle:'等待开始',running:'下载中',completed:'下载完成',partial:'未达到目标',cancelled:'已停止',error:'下载出错'};
$('download-state').textContent=dlLabels[download.status]||download.status;$('download-progress').textContent=`已保存 ${download.saved}/${download.target} 篇 · 跳过 ${download.skipped} · 失败 ${download.failed}`;
$('progress').value=download.target?Math.floor(download.saved/download.target*100):0;
$('download').disabled=download.status==='running';$('logs').textContent=download.logs.map(x=>`${x.time} ${x.message}`).join('\n')||'暂无日志';
$('browser-state').textContent=data.clients.length?`${data.clients.length} 个标签在线`:'尚未连接';
$('clients').textContent=data.clients.map(c=>`${c.account?.name||'未识别账号'} · 换号插件${c.extension?.ready?'已就绪':'未连接或需更新'}`).join('\n')||'没有在线执行标签';
}
async function refresh(){if(refreshing||!token)return;refreshing=true;try{render(await api('state'));}catch(error){notice(error.message);$('run-state').textContent='本地服务连接中断';}finally{refreshing=false;}}
action('pair',async()=>{const value=await api('pair');$('pair-code').hidden=false;$('pair-code').type='text';$('pair-code').value=value.token;$('pair-code').select();});
action('channels',async()=>{const value=await api('channels',{key:$('key').value.trim()});loadedKey=$('key').value.trim();$('channel').replaceChildren(...value.channels.map(c=>new Option(c,c)));});
$('key').oninput=()=>{loadedKey='';$('channel').replaceChildren(new Option('请重新获取频道',''));};
$('download-form').onsubmit=async event=>{event.preventDefault();try{if(!loadedKey||loadedKey!==$('key').value.trim())throw new Error('请先获取频道');await api('download',{key:loadedKey,channel:$('channel').value,target:Number($('target').value)});notice('下载已开始,完整保存的文章会自动入队');await refresh();}catch(error){notice(error.message);}};
action('download-stop',()=>api('download-stop'));
action('scan',async()=>{notice('正在检查文章目录…');const value=await api('scan',{root:$('root').value.trim()});$('scan-result').textContent=value.results.map(x=>`${x.directory}:${x.status}`).join('\n')||'目录中未找到文章';notice('目录检查完成');});
$('config-form').onsubmit=async event=>{event.preventDefault();try{await api('configure',{fid:Number($('fid').value),minimum:Number($('minimum').value),maximum:Number($('maximum').value),interval:Number($('interval').value),history_checked:$('history-checked').checked});notice('处理规则已保存');await refresh();}catch(error){notice(error.message);}};
for(const name of ['start','pause','stop'])action(name,async()=>{await api(name);notice(name==='stop'?'流程已终止;浏览器将在下一次连接检查时停止后续操作。已发出的请求可能仍已完成。':'');});
$('history-file').onchange=async()=>{try{const file=$('history-file').files[0];if(!file)return;if(file.size>2*1024*1024)throw new Error('记录文件过大');const value=JSON.parse(await file.text());await api('history',{records:value.records});notice('历史记录已导入;仅使用标记为 block_repost 的记录');await refresh();}catch(error){notice(error.message);}};
(async()=>{try{const response=await fetch('/api/session');const session=await response.json();if(!response.ok)throw new Error(session.error);token=session.token;$('port').value=location.port;await refresh();if(state?.run.config?.fid){for(const id of ['fid','minimum','maximum','interval'])$(id).value=state.run.config[id];$('history-checked').checked=state.run.config.history_checked;}}catch(error){notice(error.message);}setInterval(refresh,2500);})();
+26
View File
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>新闻工作台</title><link rel="stylesheet" href="/style.css"></head>
<body>
<aside><div class="brand"><span class="mark">闻</span><div>新闻工作台<small>本地内容工作流</small></div></div><nav><a href="#overview" class="selected">工作概览</a><a href="#source">下载与文章</a><a href="#settings">处理规则</a><a href="#queue">文章队列</a><a href="#connection">浏览器连接</a></nav><div class="aside-note"><span class="dot"></span> 数据保存在本机<p>文章自动准备与排版<br>发表由你亲手完成</p></div></aside>
<main>
<header id="overview"><div><div class="eyebrow">CONTENT WORKSPACE</div><h1>把内容准备好,把审核留给你。</h1><p>下载、导入、排版与账号切换,在一个工作台管理。</p></div><button id="stop" class="danger">终止流程</button></header>
<div id="notice" role="status" aria-live="polite" class="notice" hidden></div>
<section class="flow"><div><span>01</span>下载新闻</div><i>→</i><div><span>02</span>导入与排版</div><i>→</i><div class="highlight"><span>03</span>人工审核与发表</div><i>→</i><div><span>04</span>结果核对与换号</div></section>
<div class="pending-note"><b>当前接入进度</b> 下载、导入、排版与换号接口已整合;结果页自动核对等待页面 HTML。人工发表后会暂停,不自动计数或继续下一篇。</div>
<section class="metrics"><article><small>队列文章</small><strong id="total">0</strong><span>完整文件检查后入队</span></article><article><small>等待你审核</small><strong id="review">0</strong><span>请在网站检查并亲手发表</span></article><article><small>明确受理</small><strong id="accepted">0</strong><span>审核中与已公开分别记录</span></article><article><small>当前账号批次</small><strong id="batch">—</strong><span id="account">尚未绑定账号</span></article></section>
<div class="workspace-grid">
<section id="source" class="card"><div class="section-head"><div><span class="eyebrow">SOURCE</span><h2>下载与文章</h2></div><span id="download-state" class="badge">等待开始</span></div>
<form id="download-form"><label>探数 API Key<input id="key" type="password" autocomplete="off" placeholder="仅在本次运行中使用"></label><div class="row"><label class="grow">新闻频道<select id="channel" required><option value="">先获取频道</option></select></label><button id="channels" type="button" class="secondary">获取频道</button></div><div class="row"><label class="grow">下载篇数<input id="target" type="number" min="1" max="100000" value="5" required></label><button id="download" type="submit">开始下载</button><button id="download-stop" type="button" class="secondary">停止下载</button></div></form>
<div class="progress"><progress id="progress" max="100" value="0"></progress><small id="download-progress">等待下载</small></div>
<details><summary>导入已有文章目录</summary><p class="muted">填写包含文章子文件夹的完整路径。留空时扫描本工具的 downloads 目录。请在文件完整保存后扫描。</p><label>文章根目录<input id="root" placeholder="/完整路径/新闻目录"></label><button id="scan" class="secondary">检查并加入队列</button><pre id="scan-result"></pre></details>
</section>
<section id="settings" class="card"><div class="section-head"><div><span class="eyebrow">WORKFLOW</span><h2>处理规则</h2></div><span class="badge">人工发表</span></div>
<form id="config-form"><label>目标版块 ID(fid)<input id="fid" type="number" min="1" placeholder="从新帖页地址中的 fid 获取" required></label><div class="row"><label class="grow">每账号最少篇数<input id="minimum" type="number" min="1" max="1000" placeholder="最小值" required></label><label class="grow">每账号最多篇数<input id="maximum" type="number" min="1" max="1000" placeholder="最大值" required></label></div><label>两篇之间间隔(秒)<input id="interval" type="number" min="0" max="86400" placeholder="请输入间隔" required></label><label class="check"><input id="history-checked" type="checkbox" required><span>我已核对待处理文章与历史发布记录,避免重复发表。</span></label><button type="submit" class="secondary">保存处理规则</button></form><p class="muted">每个账号只抽取一次目标篇数;只有明确受理的文章才计数。历史演示去重记录会自动读取。</p>
</section></div>
<section class="runbar"><div><span id="run-light" class="dot gray"></span><b id="run-state">正在连接本地服务…</b><p id="run-reason">等待状态</p></div><div class="actions"><button id="start">开始 / 继续</button><button id="pause" class="secondary">暂停</button></div></section>
<section id="queue" class="card"><div class="section-head"><div><span class="eyebrow">ARTICLE QUEUE</span><h2>文章队列</h2></div><label class="file-button">导入历史去重记录<input id="history-file" type="file" accept=".json,application/json" hidden></label></div><div class="table-wrap"><table><thead><tr><th>文章</th><th>状态</th><th>账号 / 版块</th><th>结果与操作</th></tr></thead><tbody id="jobs"></tbody></table><div id="empty" class="empty"><span>+</span><b>从第一篇文章开始</b><p>下载新闻,或检查已有文章目录。准备好的文章会出现在这里。</p></div></div></section>
<section id="connection" class="card"><div class="section-head"><div><span class="eyebrow">CONNECTION</span><h2>连接你的浏览器</h2></div><span id="browser-state" class="badge">尚未连接</span></div><div class="connection-grid"><div><ol><li>在油猴中安装以下两个脚本,并停用原导入、排版脚本。</li><li>重新加载原目录中的换号插件,然后刷新网站。</li><li>在网站左下角展开连接设置,填入本地端口和连接码。</li></ol><div class="actions"><a class="button secondary" href="/browser/local-bridge.user.js">安装本地连接脚本</a><a class="button secondary" href="/browser/auto-news.user.js">安装导入排版脚本</a></div></div><div><label>本地端口<input id="port" readonly></label><button id="pair" class="secondary">显示本次连接码</button><input id="pair-code" type="password" readonly hidden aria-label="本次连接码"><small class="muted">服务重启后需要更新连接码。仅复制到你安装的本地连接脚本。</small><p id="clients" class="muted">没有在线执行标签</p></div></div></section>
<details class="card"><summary>下载日志</summary><pre id="logs">暂无日志</pre></details><footer>新闻工作台 · 本机运行 · v0.1 &nbsp; / &nbsp; 终止后不会自动恢复,已发出的请求可能仍已完成。</footer>
</main><script src="/app.js"></script>
</body></html>
+1
View File
@@ -0,0 +1 @@
:root{font-family:Inter,-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:#203b38;background:#f3f6f4;font-synthesis:none;font-size:14px}*{box-sizing:border-box}body{margin:0}aside{position:fixed;inset:0 auto 0 0;width:220px;background:#113e37;color:#f4fbf7;padding:32px 22px;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:12px;font-size:20px;font-weight:650;letter-spacing:.5px}.mark{display:grid;place-items:center;background:#d8efb9;color:#17483e;width:42px;height:42px;border-radius:13px}.brand small{display:block;font-size:11px;font-weight:400;color:#b3cdc5;margin-top:5px}nav{display:grid;gap:9px;margin-top:54px}nav a{color:#bad1c9;text-decoration:none;padding:12px 16px;border-radius:8px}nav a:hover,nav .selected{background:#28534a;color:#fff}.aside-note{margin-top:auto;color:#bad1c9;font-size:12px;line-height:1.9}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#a3db80;margin-right:7px}.dot.gray{background:#a1ada7}main{max-width:1660px;margin-left:220px;padding:42px 40px}header{display:flex;align-items:center;justify-content:space-between;gap:20px}.eyebrow{font-size:10px;letter-spacing:2px;color:#6b8980;font-weight:700}h1{font-size:28px;letter-spacing:-.6px;margin:12px 0}header p{color:#73857e;margin:0;font-size:13px}button,.button,.file-button{font:inherit;font-weight:550;border:1px solid #1c6554;border-radius:7px;background:#1c6554;color:white;padding:10px 15px;cursor:pointer;white-space:nowrap;text-decoration:none;display:inline-block}button:hover,.button:hover{filter:brightness(.94)}button:disabled{opacity:.45;cursor:wait}.secondary,.file-button{background:white;color:#36574d;border-color:#d4dfd7}.danger{background:#fff8f6;border-color:#e7c5bb;color:#a34d37}.flow{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:34px;background:white;border:1px solid #e2e9e2;padding:22px 25px;border-radius:12px;font-size:12px}.flow span{background:#edf3ef;color:#5f7d70;padding:5px 7px;border-radius:5px;font-size:10px;margin-right:8px}.flow i{color:#b1c0b6;font-style:normal}.flow .highlight{font-weight:650;color:#2e6947}.flow .highlight span{background:#e3efcc;color:#476635}.pending-note{background:#f8f2df;border:1px solid #eae0bd;color:#7b6938;padding:12px 16px;border-radius:8px;margin:16px 0;font-size:12px;line-height:1.8}.pending-note b{margin-right:10px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin:22px 0}.metrics article{padding:20px 23px;background:white;border:1px solid #e0e8e0;border-radius:10px}.metrics small{font-size:12px;color:#6b7f73}.metrics strong{display:block;font-size:30px;font-weight:550;margin:12px 0 8px}.metrics span{font-size:11px;color:#8a988f}.workspace-grid{display:grid;grid-template-columns:1.1fr 1fr;gap:20px}.card{background:white;border:1px solid #e0e8e0;border-radius:12px;padding:24px;margin-bottom:20px;min-width:0}.section-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px}h2{font-size:17px;margin:6px 0 0;font-weight:600}.badge{font-size:11px;border-radius:20px;background:#f0f5ef;color:#6c826d;padding:5px 10px}.row{display:flex;gap:12px;align-items:flex-end}.grow{flex:1;min-width:0}label{display:block;font-size:12px;color:#61756b;margin-bottom:14px}input,select{display:block;width:100%;font:inherit;color:#304a3f;padding:10px 12px;margin-top:7px;border:1px solid #dbe4dd;border-radius:6px;background:#fcfdfb;outline:none}input:focus,select:focus{border-color:#659981;box-shadow:0 0 0 3px #6599811a}input::placeholder{color:#a4afa8}.row button{margin-bottom:14px}.check{display:flex;align-items:flex-start;gap:8px;line-height:1.7}.check input{width:15px;height:15px;margin:2px 0 0;accent-color:#276b54}.muted{font-size:12px;line-height:1.8;color:#8b988e}.progress{margin:12px 0 20px}progress{appearance:none;width:100%;height:5px;border:0;border-radius:4px;display:block;background:#edf2ec}progress::-webkit-progress-bar{background:#edf2ec}progress::-webkit-progress-value{background:#76a77f;border-radius:4px}.progress small{display:block;color:#82917f;margin-top:8px;font-size:11px}summary{cursor:pointer;color:#617d6d;font-size:12px}details label{margin-top:12px}.runbar{display:flex;align-items:center;justify-content:space-between;border-radius:10px;background:#e5eee5;padding:19px 23px;margin-bottom:22px;gap:16px}.runbar b{font-weight:550}.runbar p{margin:7px 0 0 15px;font-size:12px;color:#758b76}.actions{display:flex;gap:10px;flex-wrap:wrap}table{border-collapse:collapse;width:100%;text-align:left;font-size:12px}th{color:#8a998f;font-weight:500;background:#f7f9f5;padding:12px}td{padding:16px 12px;border-bottom:1px solid #eef1eb;vertical-align:top;line-height:1.8}td:first-child{max-width:350px}td small{display:block;color:#91a093;font-size:11px}td button{font-size:11px;padding:4px 8px}td a{color:#1c6554}.table-wrap{overflow-x:auto}.empty{padding:44px 16px;text-align:center;color:#81927f}.empty span{display:block;foLine truncated