Initial upload for secondary development
This commit is contained in:
101
electron-launcher/electron-builder.config.cjs
Normal file
101
electron-launcher/electron-builder.config.cjs
Normal file
@@ -0,0 +1,101 @@
|
||||
const path = require('path');
|
||||
|
||||
function envText(name) {
|
||||
const value = process.env[name];
|
||||
if (value == null) return '';
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
function envFlag(name) {
|
||||
return /^(1|true|yes|y|on)$/i.test(envText(name));
|
||||
}
|
||||
|
||||
const pfxFile = envText('CHATLAB_PFX_FILE');
|
||||
const pfxPassword = envText('CHATLAB_PFX_PASSWORD');
|
||||
const publisherName = envText('CHATLAB_CERT_PUBLISHER_NAME');
|
||||
const forceSigning = envFlag('CHATLAB_FORCE_SIGN');
|
||||
const timestampServer = envText('CHATLAB_TIMESTAMP_SERVER') || 'http://timestamp.digicert.com';
|
||||
const shouldSign = Boolean(pfxFile);
|
||||
const buildLabel = envText('CHATLAB_BUILD_LABEL')
|
||||
|| new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
||||
|
||||
if (forceSigning && !shouldSign) {
|
||||
throw new Error('CHATLAB_FORCE_SIGN=1 requires CHATLAB_PFX_FILE to point to a .pfx/.p12 code signing certificate.');
|
||||
}
|
||||
|
||||
if (pfxPassword) {
|
||||
process.env.WIN_CSC_KEY_PASSWORD = pfxPassword;
|
||||
process.env.CSC_KEY_PASSWORD = pfxPassword;
|
||||
}
|
||||
|
||||
const win = {
|
||||
target: 'nsis',
|
||||
icon: 'build/icon.ico',
|
||||
artifactName: `ChatLab-Setup-\${version}-${buildLabel}.\${ext}`,
|
||||
signAndEditExecutable: shouldSign,
|
||||
forceCodeSigning: forceSigning,
|
||||
};
|
||||
|
||||
if (publisherName) {
|
||||
win.publisherName = publisherName;
|
||||
}
|
||||
|
||||
if (shouldSign) {
|
||||
win.signtoolOptions = {
|
||||
certificateFile: pfxFile,
|
||||
signingHashAlgorithms: ['sha256'],
|
||||
rfc3161TimeStampServer: timestampServer,
|
||||
};
|
||||
|
||||
if (publisherName) {
|
||||
win.signtoolOptions.publisherName = publisherName;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
appId: 'com.chatlab.desktop',
|
||||
productName: 'ChatLab售后智能助手',
|
||||
icon: 'build/icon.ico',
|
||||
directories: {
|
||||
output: 'dist',
|
||||
},
|
||||
files: [
|
||||
'main.js',
|
||||
'preload.js',
|
||||
'index.html',
|
||||
'build/company-logo.jpg',
|
||||
'build/icon.ico',
|
||||
'build/icon.png',
|
||||
'package.json',
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: path.join(__dirname, 'build-resources'),
|
||||
to: '.',
|
||||
filter: [
|
||||
'**/*',
|
||||
'!**/.env',
|
||||
'!**/knowledge*.db',
|
||||
'!**/__pycache__/**',
|
||||
'!**/*.pfx',
|
||||
'!**/*.p12',
|
||||
'!**/*.pvk',
|
||||
'!**/*.cer',
|
||||
'!**/*.crt',
|
||||
'!**/*.key',
|
||||
'!**/certs/**',
|
||||
],
|
||||
},
|
||||
],
|
||||
win,
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
installerIcon: 'build/icon.ico',
|
||||
uninstallerIcon: 'build/icon.ico',
|
||||
allowToChangeInstallationDirectory: true,
|
||||
perMachine: false,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
shortcutName: 'ChatLab售后智能助手',
|
||||
},
|
||||
};
|
||||
967
electron-launcher/index.html
Normal file
967
electron-launcher/index.html
Normal file
@@ -0,0 +1,967 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>微信知识库 - 服务控制台</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #030405;
|
||||
--panel: rgba(20, 22, 26, 0.78);
|
||||
--panel-strong: rgba(28, 30, 36, 0.92);
|
||||
--line: rgba(255, 255, 255, 0.1);
|
||||
--line-strong: rgba(255, 255, 255, 0.18);
|
||||
--text: #f3f7fb;
|
||||
--muted: #9aa5b1;
|
||||
--subtle: #5f6875;
|
||||
--cyan: #20d3d2;
|
||||
--blue: #7ca5ff;
|
||||
--green: #2cd889;
|
||||
--red: #ff6c91;
|
||||
--warning: #ffd166;
|
||||
--shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
|
||||
--robot-x: 0deg;
|
||||
--robot-y: 0deg;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 1180px;
|
||||
min-height: 760px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
color: #06100f;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
transition: transform 0.18s ease, opacity 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.launcher {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 52% 48%, rgba(255, 255, 255, 0.08), transparent 22%),
|
||||
radial-gradient(circle at 75% 35%, rgba(32, 211, 210, 0.08), transparent 28%),
|
||||
linear-gradient(180deg, #050607 0%, #020303 100%);
|
||||
}
|
||||
|
||||
.launcher::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
mask-image: linear-gradient(to bottom, rgba(0,0,0,0.45), transparent 68%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.launcher::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(0,0,0,0.58), transparent 38%, rgba(0,0,0,0.62)),
|
||||
linear-gradient(180deg, rgba(0,0,0,0.1), rgba(0,0,0,0.78));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: 36px;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: rgba(255,255,255,0.86);
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: 58px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.brand span {
|
||||
text-shadow: 0 0 24px rgba(32, 211, 210, 0.24);
|
||||
}
|
||||
|
||||
.copy {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
left: 36px;
|
||||
top: 320px;
|
||||
width: 820px;
|
||||
}
|
||||
|
||||
.copy h1 {
|
||||
margin: 0;
|
||||
font-size: 46px;
|
||||
line-height: 1.18;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
color: transparent;
|
||||
background: linear-gradient(90deg, #8d8cff 0%, #42b9e8 38%, #22d0b0 78%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
text-shadow: 0 22px 60px rgba(20, 150, 165, 0.26);
|
||||
}
|
||||
|
||||
.copy p {
|
||||
margin: 24px 0 0;
|
||||
color: rgba(255,255,255,0.66);
|
||||
font-size: 19px;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.robot-stage {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
left: 50%;
|
||||
top: 53%;
|
||||
width: 560px;
|
||||
height: 660px;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 48px 55px rgba(0,0,0,0.78));
|
||||
}
|
||||
|
||||
.robot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.robot-head {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
transition: transform 0.12s ease-out;
|
||||
}
|
||||
|
||||
.robot-left-arm {
|
||||
transform-origin: 206px 277px;
|
||||
animation: armFloatLeft 3.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.robot-right-arm {
|
||||
transform-origin: 354px 277px;
|
||||
animation: armFloatRight 3.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes armFloatLeft {
|
||||
0%, 100% { transform: translateY(0) rotate(-3deg); }
|
||||
50% { transform: translateY(-18px) rotate(4deg); }
|
||||
}
|
||||
|
||||
@keyframes armFloatRight {
|
||||
0%, 100% { transform: translateY(-4px) rotate(3deg); }
|
||||
50% { transform: translateY(16px) rotate(-4deg); }
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
right: 72px;
|
||||
top: 105px;
|
||||
width: 420px;
|
||||
padding: 26px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, rgba(27,29,34,0.94), rgba(15,16,19,0.9));
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin-bottom: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panel-title strong {
|
||||
display: block;
|
||||
color: #f6f8fb;
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.panel-title span {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.service-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.service-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: rgba(0,0,0,0.24);
|
||||
}
|
||||
|
||||
.service-name {
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--red);
|
||||
box-shadow: 0 0 16px rgba(255,108,145,0.45);
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 18px rgba(44,216,137,0.65);
|
||||
}
|
||||
|
||||
.service-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-start,
|
||||
.btn-stop {
|
||||
width: 48px;
|
||||
height: 34px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-start {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.btn-stop {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.btn-open,
|
||||
.btn-refresh-account {
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
padding: 0 18px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.btn-open {
|
||||
background: linear-gradient(90deg, #7d94f3, #20d3d2, #2ba35f);
|
||||
color: #fff;
|
||||
box-shadow: 0 16px 30px rgba(32, 211, 210, 0.18);
|
||||
}
|
||||
|
||||
.btn-open.ready {
|
||||
animation: readyPulse 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.btn-refresh-account {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #dbe3ee;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
@keyframes readyPulse {
|
||||
0%, 100% { box-shadow: 0 0 0 rgba(44,216,137,0.0), 0 16px 30px rgba(32,211,210,0.18); }
|
||||
50% { box-shadow: 0 0 28px rgba(44,216,137,0.34), 0 16px 30px rgba(32,211,210,0.18); }
|
||||
}
|
||||
|
||||
.account-refresh-status {
|
||||
min-height: 20px;
|
||||
margin-top: 12px;
|
||||
color: var(--subtle);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.account-refresh-status.success {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.account-refresh-status.error {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.decrypt-bar-wrap {
|
||||
display: none;
|
||||
margin-top: 18px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.decrypt-bar-wrap.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.decrypt-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 9px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.decrypt-track {
|
||||
height: 7px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.decrypt-fill {
|
||||
width: 35%;
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, var(--blue), var(--cyan));
|
||||
animation: slide 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.decrypt-fill.done {
|
||||
width: 100%;
|
||||
background: var(--green);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% { transform: translateX(-105%); }
|
||||
100% { transform: translateX(310%); }
|
||||
}
|
||||
|
||||
.log-panel {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
left: 36px;
|
||||
right: 36px;
|
||||
bottom: 28px;
|
||||
height: 170px;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0 0 10px;
|
||||
color: #e7ecff;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.log-title::before {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--cyan);
|
||||
box-shadow: 0 0 16px rgba(32,211,210,0.8);
|
||||
}
|
||||
|
||||
.logs {
|
||||
height: calc(100% - 32px);
|
||||
overflow-y: auto;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: rgba(6, 7, 11, 0.84);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.02);
|
||||
color: #b9c4d4;
|
||||
font-family: "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.log-source {
|
||||
color: var(--cyan);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
#close-warning {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
top: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 18px;
|
||||
border: 1px solid rgba(255,108,145,0.4);
|
||||
border-radius: 12px;
|
||||
background: rgba(255,108,145,0.92);
|
||||
color: #120407;
|
||||
font-weight: 900;
|
||||
box-shadow: 0 18px 50px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.modal-overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-box {
|
||||
width: min(720px, 88vw);
|
||||
padding: 34px 38px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(180deg, rgba(35,37,44,0.96), rgba(18,19,24,0.96));
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 18px;
|
||||
color: #9fc0ff;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
color: #d7ddeb;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.modal-steps {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 18px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
counter-reset: step;
|
||||
}
|
||||
|
||||
.modal-steps li {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
counter-increment: step;
|
||||
}
|
||||
|
||||
.modal-steps li::before {
|
||||
content: counter(step);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: #9fc0ff;
|
||||
color: #07101f;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-body .tip {
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border-left: 3px solid var(--cyan);
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.28);
|
||||
color: #d0b8ff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 22px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.modal-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(255,255,255,0.18);
|
||||
border-top-color: var(--cyan);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="launcher" id="launcher">
|
||||
<div id="close-warning">请先停止所有运行中的服务再退出。</div>
|
||||
|
||||
<div class="brand">
|
||||
<img src="build/company-logo.jpg" alt="">
|
||||
<span>灵泽万川 ChatLab</span>
|
||||
</div>
|
||||
|
||||
<section class="copy" aria-label="启动页介绍">
|
||||
<h1>智能售后知识库服务控制台</h1>
|
||||
<p>Next-Generation Intelligent Service Knowledge Platform</p>
|
||||
</section>
|
||||
|
||||
<section class="robot-stage" aria-hidden="true">
|
||||
<svg class="robot" viewBox="0 0 560 660" role="img">
|
||||
<defs>
|
||||
<radialGradient id="metal" cx="38%" cy="22%" r="75%">
|
||||
<stop offset="0%" stop-color="#f4f7fb"/>
|
||||
<stop offset="18%" stop-color="#8e949c"/>
|
||||
<stop offset="46%" stop-color="#20242a"/>
|
||||
<stop offset="100%" stop-color="#050607"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="body" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#5c626c"/>
|
||||
<stop offset="35%" stop-color="#111419"/>
|
||||
<stop offset="100%" stop-color="#020304"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="edge" x1="0" x2="1">
|
||||
<stop offset="0%" stop-color="#ffffff"/>
|
||||
<stop offset="45%" stop-color="#252a31"/>
|
||||
<stop offset="100%" stop-color="#000000"/>
|
||||
</linearGradient>
|
||||
<filter id="softGlow" x="-40%" y="-40%" width="180%" height="180%">
|
||||
<feGaussianBlur stdDeviation="9" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<ellipse cx="280" cy="612" rx="145" ry="26" fill="rgba(0,0,0,0.75)"/>
|
||||
<g class="robot-left-arm">
|
||||
<path d="M210 282 C158 272 126 310 120 370" stroke="url(#edge)" stroke-width="46" stroke-linecap="round" fill="none"/>
|
||||
<ellipse cx="116" cy="384" rx="48" ry="40" fill="url(#metal)" transform="rotate(-15 116 384)"/>
|
||||
<path d="M85 386 C103 403 132 404 150 384" stroke="#070809" stroke-width="10" stroke-linecap="round" fill="none"/>
|
||||
</g>
|
||||
<g class="robot-right-arm">
|
||||
<path d="M350 282 C402 272 434 310 440 370" stroke="url(#edge)" stroke-width="46" stroke-linecap="round" fill="none"/>
|
||||
<ellipse cx="444" cy="384" rx="48" ry="40" fill="url(#metal)" transform="rotate(15 444 384)"/>
|
||||
<path d="M410 384 C428 404 457 402 475 386" stroke="#070809" stroke-width="10" stroke-linecap="round" fill="none"/>
|
||||
</g>
|
||||
|
||||
<path d="M232 248 L328 248 C355 294 362 394 338 482 C322 515 238 515 222 482 C198 394 205 294 232 248Z" fill="url(#body)"/>
|
||||
<path d="M248 508 L236 610" stroke="url(#edge)" stroke-width="24" stroke-linecap="round"/>
|
||||
<path d="M312 508 L324 610" stroke="url(#edge)" stroke-width="24" stroke-linecap="round"/>
|
||||
<ellipse cx="230" cy="622" rx="44" ry="18" fill="url(#metal)"/>
|
||||
<ellipse cx="330" cy="622" rx="44" ry="18" fill="url(#metal)"/>
|
||||
<path d="M248 230 L246 198 L314 198 L312 230" stroke="url(#edge)" stroke-width="22" stroke-linecap="round" fill="none"/>
|
||||
|
||||
<g class="robot-head">
|
||||
<ellipse cx="280" cy="132" rx="78" ry="88" fill="url(#metal)"/>
|
||||
<ellipse cx="280" cy="139" rx="57" ry="65" fill="#030405"/>
|
||||
<path d="M222 106 C245 58 315 58 338 106 C325 88 236 88 222 106Z" fill="rgba(255,255,255,0.78)"/>
|
||||
<g filter="url(#softGlow)" fill="#d9f7ff">
|
||||
<circle cx="254" cy="136" r="2.2"/>
|
||||
<circle cx="260" cy="136" r="2.2"/>
|
||||
<circle cx="266" cy="136" r="2.2"/>
|
||||
<circle cx="254" cy="143" r="2.2"/>
|
||||
<circle cx="260" cy="143" r="2.2"/>
|
||||
<circle cx="266" cy="143" r="2.2"/>
|
||||
<circle cx="294" cy="136" r="2.2"/>
|
||||
<circle cx="300" cy="136" r="2.2"/>
|
||||
<circle cx="306" cy="136" r="2.2"/>
|
||||
<circle cx="294" cy="143" r="2.2"/>
|
||||
<circle cx="300" cy="143" r="2.2"/>
|
||||
<circle cx="306" cy="143" r="2.2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</section>
|
||||
|
||||
<aside class="control-panel" aria-label="服务控制面板">
|
||||
<div class="panel-title">
|
||||
<strong>服务控制台</strong>
|
||||
<span>启动本地服务并进入售后知识库</span>
|
||||
</div>
|
||||
|
||||
<div class="service-list">
|
||||
<div class="service-row" id="card-chatlog">
|
||||
<div>
|
||||
<div class="service-name">底层服务 (Go)</div>
|
||||
<div class="status"><div class="dot" id="dot-chatlog"></div><span id="text-chatlog">未运行</span></div>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="btn-start" onclick="window.electronAPI.startChatlog()">启动</button>
|
||||
<button class="btn-stop" onclick="window.electronAPI.stopChatlog()">停止</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-row" id="card-fastapi">
|
||||
<div>
|
||||
<div class="service-name">业务层 (Python)</div>
|
||||
<div class="status"><div class="dot" id="dot-fastapi"></div><span id="text-fastapi">未运行</span></div>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="btn-start" onclick="window.electronAPI.startFastapi()">启动</button>
|
||||
<button class="btn-stop" onclick="window.electronAPI.stopFastapi()">停止</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-row" id="card-frontend">
|
||||
<div>
|
||||
<div class="service-name">UI 界面 (React)</div>
|
||||
<div class="status"><div class="dot" id="dot-frontend"></div><span id="text-frontend">未运行</span></div>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="btn-start" onclick="window.electronAPI.startFrontend()">启动</button>
|
||||
<button class="btn-stop" onclick="window.electronAPI.stopFrontend()">停止</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="decrypt-bar-wrap" id="decrypt-wrap">
|
||||
<div class="decrypt-label">
|
||||
<span id="decrypt-text">数据库解密中...</span>
|
||||
<span id="decrypt-elapsed"></span>
|
||||
</div>
|
||||
<div class="decrypt-track"><div class="decrypt-fill" id="decrypt-fill"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<button class="btn-open" id="btn-enter">启动并进入系统</button>
|
||||
<button class="btn-refresh-account" id="btn-refresh-account">重新识别当前微信账号</button>
|
||||
</div>
|
||||
<div class="account-refresh-status" id="account-refresh-status"></div>
|
||||
</aside>
|
||||
|
||||
<section class="log-panel" aria-label="终端运行日志">
|
||||
<h2 class="log-title">终端运行日志</h2>
|
||||
<div class="logs" id="logs-container"></div>
|
||||
</section>
|
||||
|
||||
<div class="modal-overlay" id="decrypt-dialog">
|
||||
<div class="modal-box">
|
||||
<div class="modal-title">检测到新账号,正在解密数据</div>
|
||||
<div class="modal-body">
|
||||
系统正在为此微信账号首次解密数据库和图片,同时已开启自动解密。
|
||||
<ol class="modal-steps">
|
||||
<li>请切换到微信窗口</li>
|
||||
<li>随意点击任意一个聊天对话</li>
|
||||
<li>翻看一下历史消息记录</li>
|
||||
<li>等待本窗口自动关闭即可</li>
|
||||
</ol>
|
||||
<div class="tip">这与您在 chatlog.exe 界面中手动解密的操作完全相同,只需配合操作微信一次,后续将全程自动化。</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-spinner"></div>
|
||||
<span id="decrypt-dialog-status">解密进行中,请在微信里随意点击一个聊天窗口...</span>
|
||||
<span id="decrypt-dialog-elapsed" style="margin-left:auto;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
const btnEnter = document.getElementById('btn-enter');
|
||||
const btnRefreshAccount = document.getElementById('btn-refresh-account');
|
||||
const accountRefreshStatus = document.getElementById('account-refresh-status');
|
||||
const decryptWrap = document.getElementById('decrypt-wrap');
|
||||
const decryptText = document.getElementById('decrypt-text');
|
||||
const decryptElapsed = document.getElementById('decrypt-elapsed');
|
||||
const decryptFill = document.getElementById('decrypt-fill');
|
||||
const decryptDialog = document.getElementById('decrypt-dialog');
|
||||
const decryptDialogStatus = document.getElementById('decrypt-dialog-status');
|
||||
const decryptDialogElapsed = document.getElementById('decrypt-dialog-elapsed');
|
||||
const launcher = document.getElementById('launcher');
|
||||
const robotHead = document.querySelector('.robot-head');
|
||||
|
||||
let decryptReady = false;
|
||||
let frontendRunning = false;
|
||||
let startingAll = false;
|
||||
let refreshingAccount = false;
|
||||
|
||||
launcher.addEventListener('mousemove', (event) => {
|
||||
const rect = launcher.getBoundingClientRect();
|
||||
const x = ((event.clientX - rect.left) / rect.width - 0.5) * 2;
|
||||
const y = ((event.clientY - rect.top) / rect.height - 0.5) * 2;
|
||||
const clampedX = Math.max(-1, Math.min(1, x));
|
||||
const clampedY = Math.max(-1, Math.min(1, y));
|
||||
robotHead.style.transform = `translate(${clampedX * 9}px, ${clampedY * 6}px) rotate(${clampedX * 7}deg)`;
|
||||
});
|
||||
|
||||
launcher.addEventListener('mouseleave', () => {
|
||||
robotHead.style.transform = 'translate(0, 0) rotate(0deg)';
|
||||
});
|
||||
|
||||
function setRefreshStatus(text, type = '') {
|
||||
accountRefreshStatus.textContent = text || '';
|
||||
accountRefreshStatus.className = `account-refresh-status${type ? ` ${type}` : ''}`;
|
||||
}
|
||||
|
||||
function setRefreshButtonBusy(busy) {
|
||||
refreshingAccount = busy;
|
||||
btnRefreshAccount.disabled = busy;
|
||||
btnRefreshAccount.textContent = busy ? '识别中...' : '重新识别当前微信账号';
|
||||
}
|
||||
|
||||
function updateEnterButton() {
|
||||
if (decryptReady) {
|
||||
btnEnter.disabled = false;
|
||||
btnEnter.className = 'btn-open ready';
|
||||
btnEnter.textContent = '进入系统界面';
|
||||
btnEnter.onclick = () => window.electronAPI.openInApp();
|
||||
}
|
||||
}
|
||||
|
||||
function setServiceUI(key, running) {
|
||||
const dot = document.getElementById(`dot-${key}`);
|
||||
const text = document.getElementById(`text-${key}`);
|
||||
if (!dot || !text) return;
|
||||
if (running) {
|
||||
dot.classList.add('active');
|
||||
text.innerText = '运行中';
|
||||
text.style.color = 'var(--green)';
|
||||
} else {
|
||||
dot.classList.remove('active');
|
||||
text.innerText = '未运行';
|
||||
text.style.color = 'var(--muted)';
|
||||
}
|
||||
if (key === 'frontend') frontendRunning = running;
|
||||
}
|
||||
|
||||
function appendLog(source, text) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'log-entry';
|
||||
div.innerHTML = `<span class="log-source">[${source}]</span> ${String(text || '').trim()}`;
|
||||
logsContainer.appendChild(div);
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
|
||||
window.electronAPI.getProcessStatus().then((status) => {
|
||||
['chatlog', 'fastapi', 'frontend'].forEach((key) => setServiceUI(key, Boolean(status[key])));
|
||||
if (status.chatlog) {
|
||||
fetch('http://127.0.0.1:5030/api/v1/chatroom?format=json&limit=1')
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (!data.error || !data.error.includes('decrypting')) {
|
||||
decryptReady = true;
|
||||
decryptWrap.classList.remove('show');
|
||||
updateEnterButton();
|
||||
} else {
|
||||
decryptWrap.classList.add('show');
|
||||
decryptText.textContent = '数据库解密中...';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
decryptWrap.classList.add('show');
|
||||
decryptText.textContent = '等待底层服务就绪...';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
btnEnter.onclick = async () => {
|
||||
if (startingAll || refreshingAccount) return;
|
||||
startingAll = true;
|
||||
btnEnter.disabled = true;
|
||||
btnEnter.className = 'btn-open';
|
||||
btnEnter.textContent = '正在启动本地服务...';
|
||||
try {
|
||||
await window.electronAPI.startAll();
|
||||
} catch (e) {
|
||||
btnEnter.disabled = false;
|
||||
btnEnter.textContent = '启动并进入系统';
|
||||
appendLog('App(Error)', e?.message || '启动失败');
|
||||
} finally {
|
||||
startingAll = false;
|
||||
}
|
||||
};
|
||||
|
||||
btnRefreshAccount.onclick = async () => {
|
||||
if (refreshingAccount || startingAll) return;
|
||||
setRefreshButtonBusy(true);
|
||||
decryptReady = false;
|
||||
btnEnter.disabled = true;
|
||||
btnEnter.className = 'btn-open';
|
||||
btnEnter.textContent = '正在重新识别账号...';
|
||||
setRefreshStatus('正在重新识别当前微信账号,并重建消息索引...');
|
||||
try {
|
||||
const result = await window.electronAPI.refreshCurrentAccount();
|
||||
decryptReady = Boolean(result?.messageIndexReady);
|
||||
if (decryptReady) {
|
||||
const removed = Number(result?.removedMessageCache || 0);
|
||||
setRefreshStatus(removed > 0 ? `已重新识别并重建消息索引,清理了 ${removed} 个消息缓存。` : '已重新识别当前微信账号,消息索引已就绪。', 'success');
|
||||
updateEnterButton();
|
||||
} else {
|
||||
btnEnter.disabled = true;
|
||||
btnEnter.className = 'btn-open';
|
||||
btnEnter.textContent = '消息索引未就绪,请重试重新识别';
|
||||
setRefreshStatus('重新识别已完成,但消息索引仍未就绪。请在微信打开聊天窗口翻看历史消息后重试。', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
decryptReady = false;
|
||||
btnEnter.disabled = false;
|
||||
btnEnter.className = 'btn-open';
|
||||
btnEnter.textContent = '启动并进入系统';
|
||||
setRefreshStatus(e?.message || '重新识别失败,请确认 PC 微信已登录。', 'error');
|
||||
appendLog('App(Error)', e?.message || '重新识别失败');
|
||||
} finally {
|
||||
setRefreshButtonBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.electronAPI.onLog((data) => {
|
||||
appendLog(data.source, data.text);
|
||||
});
|
||||
|
||||
window.electronAPI.onProcessError?.((data) => {
|
||||
decryptDialog.classList.remove('show');
|
||||
decryptDialogElapsed.textContent = '';
|
||||
decryptDialogStatus.textContent = data?.message || '服务启动失败';
|
||||
btnEnter.disabled = false;
|
||||
btnEnter.className = 'btn-open';
|
||||
btnEnter.textContent = '启动并进入系统';
|
||||
setRefreshButtonBusy(false);
|
||||
setRefreshStatus(data?.message || '服务启动失败', 'error');
|
||||
appendLog('Error', data?.message || '服务启动失败');
|
||||
});
|
||||
|
||||
window.electronAPI.onProcessStarted((key) => {
|
||||
setServiceUI(key, true);
|
||||
if (key === 'chatlog') {
|
||||
decryptWrap.classList.add('show');
|
||||
decryptText.textContent = '数据库解密中...';
|
||||
decryptFill.className = 'decrypt-fill';
|
||||
btnEnter.disabled = true;
|
||||
btnEnter.className = 'btn-open';
|
||||
btnEnter.textContent = '等待数据库解密完成...';
|
||||
btnEnter.onclick = null;
|
||||
}
|
||||
});
|
||||
|
||||
window.electronAPI.onProcessStopped((key) => {
|
||||
setServiceUI(key, false);
|
||||
if (key === 'chatlog') {
|
||||
decryptReady = false;
|
||||
decryptWrap.classList.remove('show');
|
||||
btnEnter.disabled = true;
|
||||
btnEnter.className = 'btn-open';
|
||||
btnEnter.textContent = '请先启动底层服务并等待解密完成';
|
||||
btnEnter.onclick = null;
|
||||
}
|
||||
});
|
||||
|
||||
window.electronAPI.onDecryptStatus(({ elapsed }) => {
|
||||
const min = Math.floor(elapsed / 60);
|
||||
const sec = elapsed % 60;
|
||||
const timeStr = min > 0 ? `${min}分${sec}秒` : `${sec}秒`;
|
||||
decryptElapsed.textContent = timeStr;
|
||||
if (decryptDialog.classList.contains('show')) {
|
||||
decryptDialogElapsed.textContent = timeStr;
|
||||
}
|
||||
});
|
||||
|
||||
window.electronAPI.onDecryptReady(() => {
|
||||
decryptReady = true;
|
||||
decryptDialog.classList.remove('show');
|
||||
decryptText.textContent = '解密完成';
|
||||
decryptElapsed.textContent = '';
|
||||
decryptFill.className = 'decrypt-fill done';
|
||||
updateEnterButton();
|
||||
});
|
||||
|
||||
window.electronAPI.onShowDecryptDialog(() => {
|
||||
decryptDialog.classList.add('show');
|
||||
decryptDialogStatus.textContent = '解密进行中,请在微信里随意点击一个聊天窗口...';
|
||||
decryptDialogElapsed.textContent = '';
|
||||
});
|
||||
|
||||
window.electronAPI.onDecryptReset(() => {
|
||||
decryptReady = false;
|
||||
decryptWrap.classList.remove('show');
|
||||
btnEnter.disabled = true;
|
||||
btnEnter.className = 'btn-open';
|
||||
btnEnter.textContent = '请先启动底层服务并等待解密完成';
|
||||
btnEnter.onclick = null;
|
||||
});
|
||||
|
||||
let warningTimer = null;
|
||||
window.electronAPI.onShowCloseWarning(() => {
|
||||
const el = document.getElementById('close-warning');
|
||||
el.style.display = 'block';
|
||||
if (warningTimer) clearTimeout(warningTimer);
|
||||
warningTimer = setTimeout(() => { el.style.display = 'none'; }, 3000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1336
electron-launcher/main.js
Normal file
1336
electron-launcher/main.js
Normal file
File diff suppressed because it is too large
Load Diff
3643
electron-launcher/package-lock.json
generated
Normal file
3643
electron-launcher/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
electron-launcher/package.json
Normal file
18
electron-launcher/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "chatlab-desktop",
|
||||
"version": "1.0.1",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder --win --config electron-builder.config.cjs",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"electron": "^42.0.0",
|
||||
"electron-builder": "^26.8.1"
|
||||
}
|
||||
}
|
||||
24
electron-launcher/preload.js
Normal file
24
electron-launcher/preload.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
startAll: () => ipcRenderer.invoke('start-all'),
|
||||
startChatlog: () => ipcRenderer.send('start-chatlog'),
|
||||
stopChatlog: () => ipcRenderer.send('stop-chatlog'),
|
||||
startFastapi: () => ipcRenderer.send('start-fastapi'),
|
||||
stopFastapi: () => ipcRenderer.send('stop-fastapi'),
|
||||
startFrontend: () => ipcRenderer.send('start-frontend'),
|
||||
stopFrontend: () => ipcRenderer.send('stop-frontend'),
|
||||
openInApp: () => ipcRenderer.send('open-in-app'),
|
||||
getProcessStatus: () => ipcRenderer.invoke('get-process-status'),
|
||||
refreshCurrentAccount: () => ipcRenderer.invoke('refresh-current-account'),
|
||||
|
||||
onLog: (callback) => ipcRenderer.on('log', (_event, value) => callback(value)),
|
||||
onProcessStarted: (callback) => ipcRenderer.on('process-started', (_event, value) => callback(value)),
|
||||
onProcessStopped: (callback) => ipcRenderer.on('process-stopped', (_event, value) => callback(value)),
|
||||
onProcessError: (callback) => ipcRenderer.on('process-error', (_event, value) => callback(value)),
|
||||
onShowCloseWarning: (callback) => ipcRenderer.on('show-close-warning', () => callback()),
|
||||
onDecryptStatus: (callback) => ipcRenderer.on('decrypt-status', (_event, value) => callback(value)),
|
||||
onDecryptReady: (callback) => ipcRenderer.on('decrypt-ready', () => callback()),
|
||||
onDecryptReset: (callback) => ipcRenderer.on('decrypt-reset', () => callback()),
|
||||
onShowDecryptDialog: (callback) => ipcRenderer.on('show-decrypt-dialog', () => callback())
|
||||
});
|
||||
Reference in New Issue
Block a user