client: Windows 客户端(WinUI3 壳 + agent-core;调试用 Electron 壳存档);localserver token 改为常数时间比较

This commit is contained in:
Qiufeng
2026-08-20 20:38:21 +08:00
parent 33b7d57498
commit 9abf7f8213
60 changed files with 10025 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
// EveryPublish 客户端渲染进程逻辑
const $ = (id) => document.getElementById(id);
// ---- 页签切换 ----
document.querySelectorAll('.tab').forEach((tab) => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
document.querySelectorAll('.page').forEach((p) => p.classList.remove('active'));
tab.classList.add('active');
$('page-' + tab.dataset.tab).classList.add('active');
});
});
// ---- 状态页 ----
let currentServer = '';
async function refreshStatus() {
try {
const s = await window.api.status();
if (s && s.server) currentServer = s.server;
$('st-server').textContent = (s && s.server) || '-';
$('st-device').textContent = s && s.deviceId > 0 ? '#' + s.deviceId + '(' + (s.name || '') + ')' : '未配对';
$('st-session').textContent = (s && s.session) || '-';
const dot = $('st-dot');
if (!s) { $('st-online').textContent = '核心未就绪'; dot.className = 'dot gray'; }
else if (s.online) { $('st-online').textContent = '在线'; dot.className = 'dot green'; }
else { $('st-online').textContent = s.deviceId > 0 ? '离线(自动重连中)' : '未配对'; dot.className = 'dot orange'; }
} catch (e) {
$('st-online').textContent = '核心未就绪';
$('st-dot').className = 'dot gray';
}
}
setInterval(refreshStatus, 3000);
refreshStatus();
$('btn-open-web').addEventListener('click', () => {
const url = currentServer || 'http://127.0.0.1:8090';
window.api.openWeb(url);
});
$('btn-open-data').addEventListener('click', () => window.api.openDataDir());
// ---- 配对页 ----
$('btn-pair').addEventListener('click', async () => {
const code = $('pair-code').value.trim();
const name = $('pair-name').value.trim();
const res = $('pair-result');
if (!code) { res.textContent = '请输入配对码'; res.className = 'msg err'; return; }
$('btn-pair').disabled = true;
res.textContent = '配对中...';
res.className = 'msg';
try {
const r = await window.api.pair(code, name);
if (r && r.code === 0) {
res.textContent = '配对成功,正在连接服务器(可到「状态」页查看)';
res.className = 'msg ok';
} else {
res.textContent = (r && r.message) || '配对失败';
res.className = 'msg err';
}
} catch (e) {
res.textContent = '配对失败:' + e;
res.className = 'msg err';
}
$('btn-pair').disabled = false;
});
// ---- 挑战页 ----
let selectedChallenge = null;
let pollTimer = null;
const KIND_LABEL = { qr: '扫码', sms: '短信验证码', confirm: 'APP 确认', captcha: '图形验证码', pending: '等待设备' };
async function refreshChallenges() {
try {
const r = await window.api.challenges();
const list = (r && r.list) || [];
const badge = $('ch-badge');
if (list.length > 0) { badge.textContent = list.length; badge.classList.remove('hidden'); }
else badge.classList.add('hidden');
const box = $('ch-list');
box.innerHTML = '';
list.forEach((ch) => {
const div = document.createElement('div');
div.className = 'item' + (selectedChallenge && selectedChallenge.challengeId === ch.challengeId ? ' selected' : '');
const inner = [
'<span><b>',
ch.platform || '?',
'</b> · ',
KIND_LABEL[ch.kind] || ch.kind,
'</span><span class="p">',
ch.prompt || '',
'</span>',
].join('');
div.innerHTML = inner;
div.addEventListener('click', () => selectChallenge(ch));
box.appendChild(div);
});
if (!list.length) {
box.innerHTML = '<p class="hint">暂无待处理挑战</p>';
}
} catch (e) { /* 忽略 */ }
}
function selectChallenge(ch) {
selectedChallenge = ch;
refreshChallenges();
const detail = $('ch-detail');
detail.classList.remove('hidden');
const qr = ch.qrUrl || ch.qrToken || '';
const img = $('ch-qr');
if (qr && (qr.startsWith('http') || qr.startsWith('file://'))) {
img.src = qr;
img.classList.remove('hidden');
} else {
img.classList.add('hidden');
}
$('ch-value').value = '';
$('ch-result').textContent = '';
}
$('btn-solve').addEventListener('click', async () => {
if (!selectedChallenge) return;
const value = $('ch-value').value.trim() || 'confirmed';
const res = $('ch-result');
try {
const r = await window.api.solve(selectedChallenge.challengeId, value);
if (r && r.code === 0) {
res.textContent = '已提交,等待服务器确认';
res.className = 'msg ok';
selectedChallenge = null;
$('ch-detail').classList.add('hidden');
refreshChallenges();
} else {
res.textContent = (r && r.message) || '提交失败';
res.className = 'msg err';
}
} catch (e) {
res.textContent = '提交失败:' + e;
res.className = 'msg err';
}
});
function startPolling() {
if (pollTimer) return;
pollTimer = setInterval(refreshChallenges, 2000);
refreshChallenges();
}
startPolling();
// ---- 设置页 ----
async function loadSettings() {
try {
const cfg = await window.api.getConfig();
$('set-server').value = (cfg && cfg.server) || '';
$('set-fake').checked = !!(cfg && cfg.fakeAll);
$('set-autostart').checked = await window.api.getAutoStart();
} catch (e) { /* 忽略 */ }
}
loadSettings();
$('btn-save').addEventListener('click', async () => {
const server = $('set-server').value.trim();
const res = $('set-result');
if (!server) { res.textContent = '请填写服务器地址(内网部署填 http://服务器IP:8090)'; res.className = 'msg err'; return; }
const cfg = { server: server, fakeAll: $('set-fake').checked };
await window.api.saveConfig(cfg);
await window.api.setAutoStart($('set-autostart').checked);
currentServer = server;
res.textContent = '已保存,核心进程已重启';
res.className = 'msg ok';
});
$('btn-quit').addEventListener('click', () => window.api.quit());
// 核心就绪提示
if (window.api.onCoreReady) {
window.api.onCoreReady(() => {
$('st-online').textContent = '核心已就绪';
refreshStatus();
});
}
+93
View File
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>EveryPublish 客户端</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="topbar">
<div class="brand">EveryPublish <span class="sub">发布客户端</span></div>
<nav class="tabs">
<button class="tab active" data-tab="status">状态</button>
<button class="tab" data-tab="pair">配对</button>
<button class="tab" data-tab="challenges">挑战<span id="ch-badge" class="badge hidden">0</span></button>
<button class="tab" data-tab="settings">设置</button>
</nav>
</header>
<main id="main">
<!-- 状态页 -->
<section id="page-status" class="page active">
<div class="card">
<h2>运行状态</h2>
<div class="kv"><span>连接状态</span><b id="st-online">检测中...</b><span id="st-dot" class="dot gray"></span></div>
<div class="kv"><span>服务器</span><b id="st-server">-</b></div>
<div class="kv"><span>设备</span><b id="st-device">-</b></div>
<div class="kv"><span>会话</span><b id="st-session">-</b></div>
</div>
<div class="card actions">
<button id="btn-open-web" class="btn primary">打开网页控制台</button>
<button id="btn-open-data" class="btn">打开数据目录</button>
</div>
</section>
<!-- 配对页 -->
<section id="page-pair" class="page">
<div class="card">
<h2>设备配对</h2>
<ol class="steps">
<li>打开网页控制台(需与服务器内网互通)</li>
<li>登录后进入「设置 → Agent 设备」生成配对码</li>
<li>在下方输入配对码并点击「配对」</li>
</ol>
<label>配对码(6 位,5 分钟有效)</label>
<input id="pair-code" placeholder="例如:AB12CD" maxlength="6" />
<label>设备名(选填)</label>
<input id="pair-name" placeholder="例如:办公室电脑-01" />
<button id="btn-pair" class="btn primary">配对</button>
<p id="pair-result" class="msg"></p>
</div>
</section>
<!-- 挑战页 -->
<section id="page-challenges" class="page">
<div class="card">
<h2>二次验证挑战</h2>
<p class="hint">平台要求扫码/验证码时在此处理;超时可在网页端一键重发。</p>
<div id="ch-list" class="list"></div>
<div id="ch-detail" class="detail hidden">
<img id="ch-qr" alt="二维码" />
<label>验证码 / 确认值</label>
<input id="ch-value" placeholder="扫码后输入验证码;扫码挑战留空直接提交" />
<button id="btn-solve" class="btn primary">提交</button>
<p id="ch-result" class="msg"></p>
</div>
</div>
</section>
<!-- 设置页 -->
<section id="page-settings" class="page">
<div class="card">
<h2>设置</h2>
<label>服务器地址(内网部署时填 http://服务器IP:8090)</label>
<input id="set-server" placeholder="http://192.168.x.x:8090" />
<label class="switch-row">
<input type="checkbox" id="set-fake" />
<span>模拟执行(无真实账号的安装联调)</span>
</label>
<label class="switch-row">
<input type="checkbox" id="set-autostart" />
<span>开机自启</span>
</label>
<div class="actions">
<button id="btn-save" class="btn primary">保存并重启核心</button>
<button id="btn-quit" class="btn danger">退出客户端</button>
</div>
<p id="set-result" class="msg"></p>
</div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Microsoft YaHei", "PingFang SC", sans-serif; background: #f5f6f8; color: #1f2329; height: 100vh; display: flex; flex-direction: column; }
.topbar { background: #fff; border-bottom: 1px solid #e5e7eb; padding: 0 24px; display: flex; align-items: center; height: 56px; gap: 32px; }
.brand { font-size: 18px; font-weight: 700; color: #0a63f0; }
.brand .sub { font-size: 13px; font-weight: 400; color: #86909c; margin-left: 6px; }
.tabs { display: flex; gap: 8px; }
.tab { border: none; background: none; padding: 8px 16px; font-size: 14px; cursor: pointer; border-radius: 6px; color: #4e5969; }
.tab:hover { background: #f2f4f7; }
.tab.active { background: #e8f1ff; color: #0a63f0; font-weight: 600; }
.badge { display: inline-block; margin-left: 4px; background: #f53f3f; color: #fff; border-radius: 8px; font-size: 11px; padding: 0 6px; }
.hidden { display: none !important; }
#main { flex: 1; overflow-y: auto; padding: 24px; }
.page { display: none; max-width: 680px; margin: 0 auto; }
.page.active { display: block; }
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin-bottom: 16px; }
.card h2 { font-size: 16px; margin-bottom: 14px; }
.kv { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px dashed #f0f1f3; font-size: 14px; }
.kv span { color: #86909c; width: 80px; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-left: 6px; }
.dot.gray { background: #c9cdd4; } .dot.green { background: #00b42a; } .dot.orange { background: #ff7d00; } .dot.red { background: #f53f3f; }
label { display: block; font-size: 13px; color: #4e5969; margin: 12px 0 6px; }
input[type="text"], input:not([type]) { width: 100%; padding: 9px 12px; border: 1px solid #d9dde3; border-radius: 6px; font-size: 14px; outline: none; }
input:focus { border-color: #0a63f0; }
.btn { padding: 9px 20px; border: 1px solid #d9dde3; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }
.btn:hover { background: #f7f8fa; }
.btn.primary { background: #0a63f0; border-color: #0a63f0; color: #fff; }
.btn.primary:hover { background: #0b57d0; }
.btn.danger { color: #f53f3f; border-color: #f53f3f; }
.actions { display: flex; gap: 10px; margin-top: 16px; }
.msg { margin-top: 10px; font-size: 13px; color: #4e5969; min-height: 18px; }
.msg.ok { color: #00b42a; } .msg.err { color: #f53f3f; }
.hint { font-size: 13px; color: #86909c; margin-bottom: 10px; }
.steps { margin: 0 0 14px 18px; font-size: 13px; color: #4e5969; line-height: 1.9; }
.list .item { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 8px; cursor: pointer; font-size: 13px; }
.list .item:hover { border-color: #0a63f0; }
.list .item.selected { border-color: #0a63f0; background: #f0f7ff; }
.list .item .p { color: #86909c; }
.detail { margin-top: 14px; }
.detail img { max-width: 220px; max-height: 220px; border: 1px solid #e5e7eb; border-radius: 6px; display: block; margin-bottom: 10px; }
.switch-row { display: flex; align-items: center; gap: 8px; font-size: 14px; margin: 10px 0; cursor: pointer; }