server: 平台抽象层+bilibili、服务端加密凭据库、角色中间件;web: 精简页面/路由、macOS 客户端(Swift)与多份方案文档;移除误入库的编译产物

This commit is contained in:
Qiufeng
2026-08-21 10:53:32 +08:00
parent 0daa9782c9
commit 3585c39bab
103 changed files with 5842 additions and 3197 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
// 生成 icons/app.png(256x256 纯蓝方块图标)
// 生成 icons/app.png(512x512 纯蓝方块图标,兼容 macOS DMG)
const zlib = require("zlib");
const fs = require("fs");
const path = require("path");
const W = 256, H = 256;
const W = 512, H = 512;
const row = Buffer.alloc(1 + W * 4);
row[0] = 0;
for (let x = 0; x < W; x++) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 B

+30 -7
View File
@@ -1,4 +1,4 @@
const { app, BrowserWindow, Tray, Menu, ipcMain, shell, nativeImage } = require('electron');
const { app, BrowserWindow, Tray, Menu, Notification, ipcMain, shell, nativeImage } = require('electron');
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
@@ -17,6 +17,7 @@ let localJsonTimer = null;
let quitting = false;
const dataDir = path.join(app.getPath('userData'), 'data');
const localJsonPath = path.join(dataDir, 'local.json');
const coreBin = isWin ? 'agent-core.exe' : 'agent-core';
const coreExe = app.isPackaged
? path.join(process.resourcesPath, coreBin)
@@ -30,7 +31,14 @@ function startCore() {
}
fs.mkdirSync(dataDir, { recursive: true });
try { coreProc?.kill(); } catch (e) { /* ignore */ }
corePort = 0;
coreToken = '';
try { fs.rmSync(localJsonPath, { force: true }); } catch (e) { /* ignore */ }
coreProc = spawn(coreExe, ['-dir', dataDir], { windowsHide: true });
coreProc.on('error', (err) => {
console.error('[core] process error: ' + err.message);
win?.webContents.send('core-error', '核心程序启动失败:' + err.message);
});
coreProc.on('exit', (code) => {
coreProc = null;
win?.webContents.send('core-exit', code);
@@ -42,7 +50,7 @@ function pollLocalJson() {
if (localJsonTimer) clearInterval(localJsonTimer);
localJsonTimer = setInterval(() => {
try {
const info = JSON.parse(fs.readFileSync(path.join(dataDir, 'local.json'), 'utf8'));
const info = JSON.parse(fs.readFileSync(localJsonPath, 'utf8'));
if (info.port && info.token) {
corePort = info.port;
coreToken = info.token;
@@ -91,20 +99,31 @@ ipcMain.handle('core:save-config', (e, cfg) => { writeConfig(cfg); startCore();
ipcMain.handle('core:restart', () => { startCore(); return true; });
ipcMain.handle('app:open-data-dir', () => shell.openPath(dataDir));
ipcMain.handle('app:open-web', (e, url) => shell.openExternal(url));
ipcMain.handle('app:notify', (e, title, body) => {
if (Notification.isSupported()) {
new Notification({ title: String(title || 'EveryPublish'), body: String(body || '') }).show();
}
return true;
});
ipcMain.handle('app:set-autostart', (e, on) => {
app.setLoginItemSettings({ openAtLogin: !!on });
app.setLoginItemSettings({ openAtLogin: !!on, openAsHidden: true, args: ['--minimized'] });
return app.getLoginItemSettings().openAtLogin;
});
ipcMain.handle('app:get-autostart', () => app.getLoginItemSettings().openAtLogin);
ipcMain.handle('app:quit', () => { quitting = true; app.quit(); });
function readConfig() {
try { return JSON.parse(fs.readFileSync(path.join(dataDir, 'config.json'), 'utf8')); }
catch (e) { return { server: '', fakeAll: false }; }
const defaults = {
server: 'http://127.0.0.1:8090',
webUrl: 'http://127.0.0.1:3003',
fakeAll: false,
};
try { return { ...defaults, ...JSON.parse(fs.readFileSync(path.join(dataDir, 'config.json'), 'utf8')) }; }
catch (e) { return defaults; }
}
function writeConfig(cfg) {
fs.mkdirSync(dataDir, { recursive: true });
fs.writeFileSync(path.join(dataDir, 'config.json'), JSON.stringify(cfg, null, 2));
fs.writeFileSync(path.join(dataDir, 'config.json'), JSON.stringify({ ...readConfig(), ...cfg }, null, 2), { mode: 0o600 });
}
function createTray() {
@@ -172,6 +191,10 @@ async function runSmoke() {
}
}
app.on('before-quit', () => { quitting = true; try { coreProc?.kill(); } catch (e) { /* ignore */ } });
app.on('before-quit', () => {
quitting = true;
if (localJsonTimer) clearInterval(localJsonTimer);
try { coreProc?.kill(); } catch (e) { /* ignore */ }
});
app.on('window-all-closed', () => { /* 驻留托盘不退出 */ });
app.on('activate', showWin);
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "everypublish-client",
"version": "0.2.0",
"description": "EveryPublish Windows 客户端(Electron 壳 + Go agent-core)",
"description": "EveryPublish 历史 Electron 调试壳(非 macOS 正式交付)",
"main": "main.js",
"scripts": {
"start": "electron .",
@@ -17,12 +17,12 @@
"productName": "EveryPublish",
"directories": { "output": "release" },
"files": ["main.js", "preload.js", "renderer/**/*", "icons/**/*"],
"extraResources": [
{ "from": "../dist/agent-core.exe", "to": "agent-core.exe" }
],
"win": {
"target": [{ "target": "nsis", "arch": ["x64"] }],
"icon": "icons/app.ico",
"extraResources": [
{ "from": "../dist/agent-core.exe", "to": "agent-core.exe" }
],
"signAndEditExecutable": false
},
"nsis": {
+1
View File
@@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('api', {
restart: () => ipcRenderer.invoke('core:restart'),
openDataDir: () => ipcRenderer.invoke('app:open-data-dir'),
openWeb: (url) => ipcRenderer.invoke('app:open-web', url),
notify: (title, body) => ipcRenderer.invoke('app:notify', title, body),
setAutoStart: (on) => ipcRenderer.invoke('app:set-autostart', on),
getAutoStart: () => ipcRenderer.invoke('app:get-autostart'),
quit: () => ipcRenderer.invoke('app:quit'),
+28 -15
View File
@@ -13,6 +13,7 @@ document.querySelectorAll('.tab').forEach((tab) => {
// ---- 状态页 ----
let currentServer = '';
let currentWeb = 'http://127.0.0.1:3003';
async function refreshStatus() {
try {
const s = await window.api.status();
@@ -33,7 +34,7 @@ setInterval(refreshStatus, 3000);
refreshStatus();
$('btn-open-web').addEventListener('click', () => {
const url = currentServer || 'http://127.0.0.1:8090';
const url = currentWeb || currentServer || 'http://127.0.0.1:3003';
window.api.openWeb(url);
});
$('btn-open-data').addEventListener('click', () => window.api.openDataDir());
@@ -66,35 +67,42 @@ $('btn-pair').addEventListener('click', async () => {
// ---- 挑战页 ----
let selectedChallenge = null;
let pollTimer = null;
let knownChallengeIds = new Set();
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 fresh = list.filter((ch) => ch.challengeId && !knownChallengeIds.has(ch.challengeId));
if (fresh.length && knownChallengeIds.size > 0 && window.api.notify) {
window.api.notify('EveryPublish 需要处理挑战', fresh.map((ch) => ch.platform || '平台').join('、') + ' 有新的扫码或验证码任务');
}
knownChallengeIds = new Set(list.map((ch) => ch.challengeId).filter(Boolean));
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 = '';
box.replaceChildren();
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;
const label = document.createElement('span');
const platform = document.createElement('b');
platform.textContent = ch.platform || '?';
label.append(platform, document.createTextNode(' · ' + (KIND_LABEL[ch.kind] || ch.kind)));
const prompt = document.createElement('span');
prompt.className = 'p';
prompt.textContent = ch.prompt || '';
div.append(label, prompt);
div.addEventListener('click', () => selectChallenge(ch));
box.appendChild(div);
});
if (!list.length) {
box.innerHTML = '<p class="hint">暂无待处理挑战</p>';
const empty = document.createElement('p');
empty.className = 'hint';
empty.textContent = '暂无待处理挑战';
box.appendChild(empty);
}
} catch (e) { /* 忽略 */ }
}
@@ -149,7 +157,9 @@ startPolling();
async function loadSettings() {
try {
const cfg = await window.api.getConfig();
$('set-server').value = (cfg && cfg.server) || '';
$('set-server').value = (cfg && cfg.server) || 'http://127.0.0.1:8090';
$('set-web').value = (cfg && cfg.webUrl) || 'http://127.0.0.1:3003';
currentWeb = $('set-web').value;
$('set-fake').checked = !!(cfg && cfg.fakeAll);
$('set-autostart').checked = await window.api.getAutoStart();
} catch (e) { /* 忽略 */ }
@@ -158,12 +168,15 @@ loadSettings();
$('btn-save').addEventListener('click', async () => {
const server = $('set-server').value.trim();
const webUrl = $('set-web').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 };
if (!webUrl) { res.textContent = '请填写网站地址'; res.className = 'msg err'; return; }
const cfg = { server: server, webUrl: webUrl, fakeAll: $('set-fake').checked };
await window.api.saveConfig(cfg);
await window.api.setAutoStart($('set-autostart').checked);
currentServer = server;
currentWeb = webUrl;
res.textContent = '已保存,核心进程已重启';
res.className = 'msg ok';
});
+2
View File
@@ -72,6 +72,8 @@
<h2>设置</h2>
<label>服务器地址(内网部署时填 http://服务器IP:8090)</label>
<input id="set-server" placeholder="http://192.168.x.x:8090" />
<label>网站地址(本地开发默认 http://127.0.0.1:3003)</label>
<input id="set-web" placeholder="http://127.0.0.1:3003" />
<label class="switch-row">
<input type="checkbox" id="set-fake" />
<span>模拟执行(无真实账号的安装联调)</span>