Files
gitlab-instance-0a899031_d2…/index.html
eric b77a8cadb2
Some checks failed
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
'init'
2026-03-24 14:46:41 -05:00

829 lines
24 KiB
HTML
Raw 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">
<meta name="color-scheme" content="dark light">
<title>多进程录制控制台</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--font-sans: "Noto Sans SC", system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "Cascadia Code", monospace;
--bg0: #070a12;
--bg1: #0f1424;
--surface: rgba(22, 28, 48, 0.72);
--surface-solid: #161c30;
--elevated: rgba(28, 36, 62, 0.85);
--border: rgba(148, 163, 184, 0.12);
--border-strong: rgba(148, 163, 184, 0.22);
--text: #f1f5f9;
--text-muted: #94a3b8;
--text-dim: #64748b;
--accent: #38bdf8;
--accent-dim: rgba(56, 189, 248, 0.14);
--accent-glow: rgba(56, 189, 248, 0.35);
--violet: #a78bfa;
--success: #4ade80;
--warning: #fbbf24;
--danger: #fb7185;
--radius-sm: 8px;
--radius: 14px;
--radius-lg: 20px;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.35), 0 0 0 1px var(--border);
--shadow-lg: 0 25px 50px -12px rgba(0, 0, 0, 0.55);
--ease: cubic-bezier(0.4, 0, 0.2, 1);
}
@media (prefers-color-scheme: light) {
:root {
--bg0: #f0f4fc;
--bg1: #e2e8f4;
--surface: rgba(255, 255, 255, 0.82);
--surface-solid: #ffffff;
--elevated: rgba(255, 255, 255, 0.94);
--border: rgba(15, 23, 42, 0.08);
--border-strong: rgba(15, 23, 42, 0.14);
--text: #0f172a;
--text-muted: #475569;
--text-dim: #64748b;
--accent-dim: rgba(14, 165, 233, 0.12);
--shadow: 0 4px 24px rgba(15, 23, 42, 0.08), 0 0 0 1px var(--border);
--shadow-lg: 0 25px 50px -12px rgba(15, 23, 42, 0.12);
}
#pm2Log {
background: #0c1222 !important;
color: #a5f3fc !important;
border-color: rgba(56, 189, 248, 0.2) !important;
}
.configStatus { color: var(--success) !important; }
textarea, #processSelect {
background: #f8fafc !important;
color: #0f172a !important;
border-color: var(--border-strong) !important;
}
}
*, *::before, *::after { box-sizing: border-box; }
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--font-sans);
font-size: 15px;
line-height: 1.55;
color: var(--text);
background: var(--bg0);
background-image:
radial-gradient(ellipse 120% 80% at 100% -20%, rgba(56, 189, 248, 0.18), transparent 50%),
radial-gradient(ellipse 80% 60% at -10% 110%, rgba(167, 139, 250, 0.15), transparent 45%),
linear-gradient(165deg, var(--bg0) 0%, var(--bg1) 100%);
display: flex;
justify-content: center;
padding: clamp(16px, 4vw, 40px);
}
body.ready .shell {
animation: shellIn 0.65s var(--ease) both;
}
@keyframes shellIn {
from {
opacity: 0;
transform: translateY(12px) scale(0.985);
}
to {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
body.ready .shell { animation: none; }
* { transition-duration: 0.01ms !important; }
}
.shell {
width: 100%;
max-width: 760px;
background: var(--surface);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
}
.shell-inner {
padding: clamp(20px, 4vw, 32px);
}
/* 顶栏 */
.app-header {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 8px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.app-title {
margin: 0;
font-size: clamp(1.25rem, 4vw, 1.5rem);
font-weight: 700;
letter-spacing: -0.02em;
display: flex;
flex-direction: column;
gap: 4px;
}
.app-title span.eyebrow {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent);
opacity: 0.95;
}
.process-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 14px;
}
.process-row label {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-muted);
}
#processSelect {
appearance: none;
font-family: var(--font-sans);
font-size: 0.9375rem;
font-weight: 500;
color: var(--text);
background: var(--elevated);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
padding: 10px 36px 10px 14px;
min-width: min(100%, 280px);
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
#processSelect:hover {
border-color: rgba(56, 189, 248, 0.45);
}
#processSelect:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
#statusIndicator {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.9375rem;
font-weight: 600;
padding: 8px 14px;
border-radius: 999px;
background: var(--accent-dim);
border: 1px solid rgba(56, 189, 248, 0.25);
white-space: nowrap;
}
#processScriptNote {
margin: 0 0 20px;
padding: 10px 14px;
font-size: 0.8125rem;
font-family: var(--font-mono);
color: var(--text-dim);
background: rgba(0, 0, 0, 0.2);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
word-break: break-all;
}
@media (prefers-color-scheme: light) {
#processScriptNote { background: rgba(15, 23, 42, 0.04); }
}
/* 区块 */
.section-title {
margin: 0 0 14px;
font-size: 1.05rem;
font-weight: 600;
letter-spacing: -0.01em;
display: flex;
align-items: center;
gap: 10px;
}
.section-title::before {
content: "";
width: 4px;
height: 1.1em;
border-radius: 2px;
background: linear-gradient(180deg, var(--accent), var(--violet));
}
.panel {
margin-bottom: 24px;
}
.panel:last-child { margin-bottom: 0; }
.configEditor h3 {
margin: 0 0 10px;
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-muted);
font-family: var(--font-mono);
}
textarea {
width: 100%;
margin: 0 0 12px;
padding: 14px 16px;
font-family: var(--font-mono);
font-size: 0.8125rem;
line-height: 1.6;
color: var(--text);
background: var(--elevated);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
resize: vertical;
min-height: 120px;
transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.configButtons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
margin-bottom: 10px;
}
button {
font-family: var(--font-sans);
font-size: 0.875rem;
font-weight: 600;
padding: 11px 16px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: transform 0.15s var(--ease), filter 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
button:active:not(:disabled) {
transform: scale(0.97);
}
button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.configButtons button {
background: var(--elevated);
color: var(--text);
border: 1px solid var(--border-strong);
}
.configButtons button:hover {
border-color: rgba(56, 189, 248, 0.4);
background: var(--accent-dim);
}
#loadYoutubeBtn, #loadUrlBtn {
border-color: rgba(148, 163, 184, 0.25);
}
#saveYoutubeBtn, #saveUrlBtn {
background: linear-gradient(135deg, #0ea5e9, #6366f1);
color: #fff;
border: none;
box-shadow: 0 4px 14px rgba(14, 165, 233, 0.35);
}
#saveYoutubeBtn:hover, #saveUrlBtn:hover {
filter: brightness(1.08);
box-shadow: 0 6px 20px rgba(14, 165, 233, 0.4);
}
button.loading {
opacity: 0.75;
pointer-events: none;
}
button.loading::after {
content: " …";
animation: pulse 1s var(--ease) infinite;
}
@keyframes pulse {
50% { opacity: 0.4; }
}
.configStatus {
margin: 0;
padding: 12px 14px;
font-family: var(--font-mono);
font-size: 0.8125rem;
line-height: 1.5;
color: var(--success);
background: rgba(74, 222, 128, 0.08);
border: 1px solid rgba(74, 222, 128, 0.2);
border-radius: var(--radius-sm);
white-space: pre-wrap;
word-break: break-word;
}
#obsSection p {
margin: 8px 0;
color: var(--text-muted);
font-size: 0.9rem;
}
#obsAddress {
padding: 14px 16px;
font-family: var(--font-mono);
font-size: 0.9rem;
color: var(--accent);
background: var(--accent-dim);
border: 1px dashed rgba(56, 189, 248, 0.35);
border-radius: var(--radius-sm);
margin: 10px 0;
word-break: break-all;
}
/* 进程控制按钮 */
#pm2Buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 0;
}
@media (max-width: 520px) {
#pm2Buttons {
grid-template-columns: repeat(2, 1fr);
}
}
#pm2Buttons button {
padding: 12px 10px;
font-size: 0.8125rem;
}
#pm2Buttons [data-action="start"] {
background: linear-gradient(180deg, #22c55e, #16a34a);
color: #fff;
box-shadow: 0 4px 14px rgba(34, 197, 94, 0.35);
}
#pm2Buttons [data-action="start"]:hover { filter: brightness(1.06); }
#pm2Buttons [data-action="stop"] {
background: linear-gradient(180deg, #fb7185, #e11d48);
color: #fff;
box-shadow: 0 4px 14px rgba(251, 113, 133, 0.3);
}
#pm2Buttons [data-action="stop"]:hover { filter: brightness(1.06); }
#pm2Buttons [data-action="restart"] {
background: linear-gradient(180deg, #fbbf24, #d97706);
color: #1c1917;
box-shadow: 0 4px 14px rgba(251, 191, 36, 0.3);
}
#pm2Buttons [data-action="restart"]:hover { filter: brightness(1.05); }
#pm2Buttons [data-action="status"] {
background: var(--elevated);
color: var(--text);
border: 1px solid var(--border-strong);
}
#pm2Buttons [data-action="status"]:hover {
border-color: var(--accent);
background: var(--accent-dim);
}
/* 日志终端 */
.log-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.log-badge {
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-dim);
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.25);
}
#pm2Log {
margin: 0;
height: min(380px, 52vh);
min-height: 220px;
padding: 16px 18px;
font-family: var(--font-mono);
font-size: 0.78rem;
line-height: 1.55;
color: #7dd3fc;
background: linear-gradient(180deg, #0a0f18 0%, #06090f 100%);
border: 1px solid rgba(56, 189, 248, 0.15);
border-radius: var(--radius);
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
#pm2Log::-webkit-scrollbar {
width: 8px;
height: 8px;
}
#pm2Log::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.25);
border-radius: 4px;
}
#pm2Log::-webkit-scrollbar-thumb {
background: rgba(56, 189, 248, 0.25);
border-radius: 4px;
}
#pm2Log::-webkit-scrollbar-thumb:hover {
background: rgba(56, 189, 248, 0.4);
}
.hint-muted {
font-size: 0.8125rem;
color: var(--text-muted);
margin: 8px 0 0;
}
</style>
</head>
<body>
<div class="shell">
<div class="shell-inner">
<header class="app-header">
<h1 class="app-title">
<span class="eyebrow">Live Recorder</span>
录制控制台
</h1>
<div class="process-row">
<label for="processSelect">当前进程</label>
<select id="processSelect" aria-label="选择录制进程"></select>
<span id="statusIndicator" role="status" aria-live="polite">⚪ 加载中...</span>
</div>
</header>
<p id="processScriptNote"></p>
<div id="youtubeOnlySection" style="display:none;" class="panel">
<h2 class="section-title">YouTube 配置</h2>
<div class="configEditor">
<h3>youtube.ini</h3>
<textarea id="youtubeConfig" spellcheck="false" autocomplete="off"></textarea>
<div class="configButtons">
<button type="button" id="loadYoutubeBtn">加载配置</button>
<button type="button" id="saveYoutubeBtn">保存配置</button>
</div>
<pre class="configStatus" id="youtubeStatus">就绪</pre>
</div>
</div>
<div id="urlConfigSection" style="display:none;" class="panel">
<h2 class="section-title">URL 列表</h2>
<div class="configEditor">
<h3>URL_config.ini</h3>
<textarea id="urlConfig" spellcheck="false" autocomplete="off"></textarea>
<div class="configButtons">
<button type="button" id="loadUrlBtn">加载配置</button>
<button type="button" id="saveUrlBtn">保存配置</button>
</div>
<pre class="configStatus" id="urlStatus">就绪</pre>
</div>
</div>
<div id="obsSection" style="display:none;" class="panel">
<h2 class="section-title">OBS 推流</h2>
<p>请在 OBS → 设置 → 推流 → 服务选择「自定义」,服务器填写以下地址:</p>
<div id="obsAddress">srt://live.local:10080/live/obs</div>
<p>密钥Stream Key留空即可。</p>
</div>
<div class="panel">
<h2 class="section-title">进程控制</h2>
<div id="pm2Buttons">
<button type="button" data-action="start">启动</button>
<button type="button" data-action="stop">停止</button>
<button type="button" data-action="restart">重启</button>
<button type="button" data-action="status">手动刷新</button>
</div>
</div>
<div class="panel">
<div class="log-header">
<h2 class="section-title" style="margin:0;">实时日志</h2>
<span class="log-badge">自动刷新 · 1s</span>
</div>
<p class="hint-muted">输出与错误分流显示;状态每秒轮询。</p>
<pre id="pm2Log" aria-label="运行日志">正在加载状态与日志...</pre>
</div>
</div>
</div>
<script>
/** 进程列表仅来自 GET /process_monitorweb2.py 启动时扫描项目根目录自动识别);进程名 = 脚本主文件名(无扩展名)。 */
/** 完整列表(不删);下拉框仅用 YouTube 时用 PROCESS_MONITOR */
let PROCESS_MONITOR_ALL = [];
let PROCESS_MONITOR = [];
/** 为 true 时:下拉只展示 youtube*.py其它入口仍存在于 PROCESS_MONITOR_ALL */
const UI_DROPDOWN_YOUTUBE_ONLY = true;
function pm2NameFromScript(script) {
const base = String(script).split(/[/\\]/).pop() || '';
const i = base.lastIndexOf('.');
return i > 0 ? base.slice(0, i) : base;
}
function isYoutubeScript(script) {
return /\.py$/i.test(script) && pm2NameFromScript(script).startsWith('youtube');
}
function isTiktokScript(script) {
return /\.py$/i.test(script) && pm2NameFromScript(script).startsWith('tiktok');
}
function isObsScript(script) {
return pm2NameFromScript(script).startsWith('obs');
}
async function loadProcessMonitor() {
const res = await fetch('/process_monitor');
if (!res.ok) throw new Error(String(res.status));
const data = await res.json();
const entries = data.entries || [];
PROCESS_MONITOR_ALL = entries.map((e) => ({
script: e.script,
label: e.label || e.script,
pm2: e.pm2 || pm2NameFromScript(e.script),
}));
PROCESS_MONITOR = UI_DROPDOWN_YOUTUBE_ONLY
? PROCESS_MONITOR_ALL.filter((e) => isYoutubeScript(e.script))
: PROCESS_MONITOR_ALL;
}
function getEntryByPm2(name) {
return PROCESS_MONITOR.find((e) => e.pm2 === name) || null;
}
function fillProcessSelect() {
processSelect.innerHTML = '';
PROCESS_MONITOR.forEach((e) => {
const opt = document.createElement('option');
opt.value = e.pm2;
opt.textContent = `${e.label}${e.script}`;
processSelect.appendChild(opt);
});
}
const logEl = document.getElementById('pm2Log');
const statusIndicator = document.getElementById('statusIndicator');
const processSelect = document.getElementById('processSelect');
let currentProcess = '';
const youtubeOnlySection = document.getElementById('youtubeOnlySection');
const urlConfigSection = document.getElementById('urlConfigSection');
const obsSection = document.getElementById('obsSection');
const youtubeConfig = document.getElementById('youtubeConfig');
const loadYoutubeBtn = document.getElementById('loadYoutubeBtn');
const saveYoutubeBtn = document.getElementById('saveYoutubeBtn');
const youtubeStatus = document.getElementById('youtubeStatus');
const urlConfig = document.getElementById('urlConfig');
const loadUrlBtn = document.getElementById('loadUrlBtn');
const saveUrlBtn = document.getElementById('saveUrlBtn');
const urlStatus = document.getElementById('urlStatus');
function refreshProcessScriptNote() {
const el = document.getElementById('processScriptNote');
const ent = getEntryByPm2(currentProcess);
el.textContent = ent ? `脚本: ${ent.script} · 进程名: ${ent.pm2}` : '';
el.style.display = ent ? 'block' : 'none';
}
processSelect.addEventListener('change', () => {
currentProcess = processSelect.value;
refreshProcessScriptNote();
fetchStatus();
updateSectionsVisibility();
});
function updateSectionsVisibility() {
const ent = getEntryByPm2(currentProcess);
const s = ent ? ent.script : '';
const isYoutube = ent && isYoutubeScript(s);
const isTiktok = ent && isTiktokScript(s);
const isObs = ent && isObsScript(s);
youtubeOnlySection.style.display = isYoutube ? 'block' : 'none';
urlConfigSection.style.display = (isYoutube || isTiktok) ? 'block' : 'none';
obsSection.style.display = UI_DROPDOWN_YOUTUBE_ONLY ? 'none' : (isObs ? 'block' : 'none');
if (isYoutube) loadYoutubeConfig();
if (isYoutube || isTiktok) loadUrlConfig();
}
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = (textarea.scrollHeight) + 'px';
}
[youtubeConfig, urlConfig].forEach(ta => {
ta.addEventListener('input', () => autoResize(ta));
autoResize(ta);
});
async function loadConfig(endpoint, textarea, statusEl) {
statusEl.textContent = '加载中...';
try {
const res = await fetch(`${endpoint}?process=${currentProcess}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
textarea.value = data.content || '';
autoResize(textarea);
statusEl.textContent = '加载成功';
} catch (err) {
statusEl.textContent = `加载失败: ${err.message}`;
}
}
async function saveConfig(endpoint, textarea, statusEl, saveBtn) {
statusEl.textContent = '保存中...';
const content = textarea.value;
try {
const res = await fetch(`${endpoint}?process=${currentProcess}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({content})
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
statusEl.textContent = data.message || '保存成功';
} catch (err) {
statusEl.textContent = `保存失败: ${err.message}`;
} finally {
if (saveBtn) saveBtn.classList.remove('loading');
}
}
function loadYoutubeConfig() { loadConfig('/get_config', youtubeConfig, youtubeStatus); }
function saveYoutubeConfig() {
saveYoutubeBtn.classList.add('loading');
saveConfig('/save_config', youtubeConfig, youtubeStatus, saveYoutubeBtn);
}
loadYoutubeBtn.onclick = loadYoutubeConfig;
saveYoutubeBtn.onclick = saveYoutubeConfig;
function loadUrlConfig() { loadConfig('/get_url_config', urlConfig, urlStatus); }
function saveUrlConfig() {
saveUrlBtn.classList.add('loading');
saveConfig('/save_url_config', urlConfig, urlStatus, saveUrlBtn);
}
loadUrlBtn.onclick = loadUrlConfig;
saveUrlBtn.onclick = saveUrlConfig;
function scrollLog(){ logEl.scrollTop = logEl.scrollHeight; }
function updateStatus(process_status){
let emoji = '⚪', text = '未知';
if(process_status === 'online'){ emoji = '🟢'; text = '运行中'; }
else if(process_status === 'stopped'){ emoji = '🔴'; text = '已停止'; }
else if(process_status === 'errored'){ emoji = '🔴'; text = '错误'; }
else if(process_status === 'launching' || process_status === 'starting'){ emoji = '🟡'; text = '启动中'; }
else if(process_status === 'waiting restart'){ emoji = '🟡'; text = '等待重启'; }
else if(process_status === 'not_found'){ emoji = '❌'; text = '未启动'; }
statusIndicator.innerHTML = `${emoji} ${text}`;
}
async function pm2Action(action){
const btn = document.querySelector(`[data-action="${action}"]`);
if(btn) btn.classList.add('loading');
const url = `/${action}?process=${currentProcess}`;
try{
const res = await fetch(url);
if(!res.ok) throw new Error('网络错误');
const data = await res.json();
if(action === 'status'){
updateStatus(data.process_status);
let logText = `【进程状态总览】(${new Date().toLocaleTimeString()}\n${data.raw_status}\n\n`;
if(data.process_status === 'not_found'){
const ent = getEntryByPm2(currentProcess);
const hint = ent ? `${ent.script}(进程名: ${ent.pm2}` : currentProcess;
logText += `【提示】未找到或未启动: ${hint}\n`;
}else{
logText += `【实时输出日志】`;
if(data.recent_log.includes('不存在') || data.recent_log.includes('无日志路径')){
logText += `\n${data.recent_log}`;
}else{
logText += `(最近 ${data.recent_log.trim().split('\n').length} 行)\n${data.recent_log || '无新输出'}`;
}
logText += `\n\n【实时错误日志】`;
if(data.recent_error.includes('不存在') || data.recent_error.includes('无日志路径')){
logText += `\n${data.recent_error}`;
}else{
logText += `(最近 ${data.recent_error.trim().split('\n').length} 行)\n${data.recent_error || '无错误'}`;
}
}
logEl.textContent = logText;
scrollLog();
}else{
const ent = getEntryByPm2(currentProcess);
const who = ent ? `${ent.script} (${ent.pm2})` : currentProcess;
logEl.textContent = `操作: ${action}${who}\n${data.output || '成功'}\n\n正在刷新状态...`;
scrollLog();
setTimeout(fetchStatus, 1500);
}
}catch(err){
logEl.textContent = `请求失败: ${err.message}`;
scrollLog();
updateStatus('unknown');
}finally{
if(btn) btn.classList.remove('loading');
}
}
async function fetchStatus(){ pm2Action('status'); }
document.getElementById('pm2Buttons').addEventListener('click', e=>{
const btn = e.target.closest('button');
if(!btn) return;
const action = btn.dataset.action;
if(action && action !== 'status') pm2Action(action);
});
(async function boot() {
try {
await loadProcessMonitor();
} catch (e) {
logEl.textContent = '无法加载 /process_monitor请用本服务打开的页面访问或检查 web2 是否已启动)\n' + (e && e.message ? e.message : '');
statusIndicator.textContent = '❌ 未连接';
document.body.classList.add('ready');
return;
}
if (!PROCESS_MONITOR.length) {
logEl.textContent = UI_DROPDOWN_YOUTUBE_ONLY
? '未发现 youtube*.py 入口,下拉框为空。(服务端仍返回其它入口,见 PROCESS_MONITOR_ALL'
: '未发现任何入口脚本(项目根应有 tiktok*.py / youtube*.py / obs*.sh 或 obs*.bat且 Web 为当前 web*.py';
document.body.classList.add('ready');
return;
}
fillProcessSelect();
currentProcess = PROCESS_MONITOR[0].pm2;
processSelect.value = currentProcess;
refreshProcessScriptNote();
setInterval(fetchStatus, 1000);
fetchStatus();
updateSectionsVisibility();
document.body.classList.add('ready');
})();
</script>
</body>
</html>