162 lines
12 KiB
JavaScript
162 lines
12 KiB
JavaScript
(() => {
|
||
'use strict';
|
||
|
||
const $ = (selector) => document.querySelector(selector);
|
||
const openButton = $('#gitea-workflow-open');
|
||
const panel = $('#gitea-workflow-panel');
|
||
const root = $('#gitea-workflow-root');
|
||
const status = $('#gitea-workflow-status');
|
||
const refreshButton = $('#gitea-workflow-refresh');
|
||
const closeButton = $('#gitea-workflow-close');
|
||
if (!openButton || !panel || !root) return;
|
||
|
||
const SYSTEM_AUDIT_ACTOR = 'system';
|
||
const SYSTEM_AUDIT_REASON = 'cc-web 管理页自动操作';
|
||
|
||
let overview = null;
|
||
let loading = false;
|
||
|
||
function escapeHtml(value) {
|
||
return String(value == null ? '' : value)
|
||
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
||
.replaceAll('"', '"').replaceAll("'", ''');
|
||
}
|
||
|
||
function token() { return localStorage.getItem('cc-web-token') || ''; }
|
||
function api(path, init = {}) {
|
||
const headers = { ...(init.body ? { 'Content-Type': 'application/json' } : {}), ...(init.headers || {}) };
|
||
if (token()) headers.Authorization = `Bearer ${token()}`;
|
||
return fetch(path, { ...init, headers }).then(async (response) => {
|
||
const data = await response.json().catch(() => ({}));
|
||
if (!response.ok) throw Object.assign(new Error(data.message || data.code || `HTTP ${response.status}`), { response, data });
|
||
return data;
|
||
});
|
||
}
|
||
|
||
function stateLabel(task) {
|
||
return task.stateLabel || ({ queued: '排队中', preparing: '准备中', running: '运行中', waiting_user: '等待用户',
|
||
verifying_reply: '核验回帖', retry_wait: '等待重试', blocked_workspace: '工作区阻塞', aborting: '中止中',
|
||
succeeded: '已完成', succeeded_with_rest_fallback: '已完成(REST 兜底)', failed: '失败', failed_reply: '回帖失败',
|
||
cancelled: '已取消', aborted: '已中止' }[task.state] || task.state);
|
||
}
|
||
|
||
function date(value) {
|
||
if (!value) return '—';
|
||
const parsed = new Date(value);
|
||
return Number.isNaN(parsed.getTime()) ? escapeHtml(value) : escapeHtml(parsed.toLocaleString());
|
||
}
|
||
|
||
function actionLabel(value) {
|
||
const labels = {
|
||
'admin.pause': '暂停工作流',
|
||
'admin.resume': '恢复工作流',
|
||
'admin.enable': '启用仓库',
|
||
'admin.disable': '停用仓库',
|
||
'admin.cancel': '取消排队',
|
||
'admin.abort': '中止 Turn',
|
||
'admin.configuration_updated': '更新配置',
|
||
};
|
||
return labels[value] || String(value || '工作流操作').replace(/^admin\./, '');
|
||
}
|
||
|
||
function systemAuditFields(requestId = '') {
|
||
const payload = {
|
||
actor: SYSTEM_AUDIT_ACTOR,
|
||
reason: SYSTEM_AUDIT_REASON,
|
||
};
|
||
if (requestId) payload.requestId = requestId;
|
||
return payload;
|
||
}
|
||
|
||
function actionButtonLabel(paused) {
|
||
return paused ? '恢复全局' : '暂停全局';
|
||
}
|
||
|
||
function render() {
|
||
const data = overview || { summary: {}, control: {}, repositories: [], tasks: [], audits: [], logs: [] };
|
||
const control = data.control || {};
|
||
const summary = data.summary || {};
|
||
const paused = !!control.globalPaused;
|
||
const repositories = Array.isArray(data.repositories) ? data.repositories : [];
|
||
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
|
||
const audits = Array.isArray(data.audits) ? data.audits : [];
|
||
const logs = Array.isArray(data.logs) ? data.logs : [];
|
||
const settings = data.settings || {};
|
||
root.innerHTML = `<div class="gitea-workflow__toolbar">
|
||
<div><strong>${paused ? '全局已暂停' : '工作流运行中'}</strong><br><small>${paused ? '工作流已暂停' : '暂停只阻止新任务领取,运行中任务不受影响'} · ${date(data.generatedAt)}</small></div>
|
||
<div class="gitea-workflow__actions"><button class="gitea-workflow__button" data-action="${paused ? 'resume' : 'pause'}">${actionButtonLabel(paused)}</button></div>
|
||
</div>
|
||
${!data.available ? '<div class="gitea-workflow__error">核心 Workflow 服务尚未挂接:当前页面只展示已持久化的镜像状态,控制操作会安全返回 503。</div>' : ''}
|
||
<section class="gitea-workflow__section"><h3>Gitea 连接与工作区配置</h3><div class="gitea-workflow__config-grid">
|
||
<label>Gitea 地址<input id="gitea-config-host" value="${escapeHtml(settings.host || '')}" placeholder="https://gitea.example"></label>
|
||
<label>Bot 用户名<input id="gitea-config-bot-login" value="${escapeHtml(settings.botLogin || 'ccweb-bot')}" placeholder="ccweb-bot"></label>
|
||
<label>工作区根目录<input id="gitea-config-workspace" value="${escapeHtml(settings.workspaceRoot || '')}" placeholder="/var/lib/ccweb/workspaces"></label>
|
||
<label>默认分支覆盖<input id="gitea-config-branch" value="${escapeHtml(settings.defaultBranch || '')}" placeholder="留空使用仓库默认分支"></label>
|
||
<label>Bot Token<input id="gitea-config-bot-token" type="password" placeholder="${settings.botTokenConfigured ? '已配置,留空保持不变' : '填写评论/Git Token'}"></label>
|
||
<div class="gitea-workflow__config-actions"><button class="gitea-workflow__button" data-config-save>保存配置</button></div>
|
||
</div></section>
|
||
<div class="gitea-workflow__metrics"><div class="gitea-workflow__metric"><span>运行中</span><strong>${Number(summary.running || 0)}</strong></div><div class="gitea-workflow__metric"><span>队列</span><strong>${Number(summary.queued || 0)}</strong></div><div class="gitea-workflow__metric"><span>仓库</span><strong>${Number(summary.repositories || 0)}</strong></div><div class="gitea-workflow__metric"><span>脏目录 / 阻塞</span><strong>${Number(summary.blocked || 0)}</strong></div></div>
|
||
<section class="gitea-workflow__section"><h3>仓库与工作区</h3><div class="gitea-workflow__table-wrap"><table class="gitea-workflow__table"><thead><tr><th>仓库</th><th>状态</th><th>工作区</th><th>脏目录原因</th><th>队列</th><th>操作</th></tr></thead><tbody>${repositories.length ? repositories.map((repo) => `<tr><td><strong>${escapeHtml(repo.owner ? `${repo.owner}/${repo.name}` : repo.repoKey)}</strong><br><span class="gitea-workflow__muted gitea-workflow__mono">${escapeHtml(repo.repoKey)}</span></td><td><span class="gitea-workflow__pill" data-state="${escapeHtml(repo.status)}">${repo.enabled ? '启用' : '停用'}</span></td><td class="gitea-workflow__mono">${escapeHtml(repo.workspacePath || '—')}</td><td class="gitea-workflow__dirty">${repo.dirty ? escapeHtml(repo.dirtyReason || '检测到未提交修改或冲突') : '—'}</td><td>${Number(repo.queuedCount || 0)}</td><td><button class="gitea-workflow__button" data-repo-action="${repo.enabled ? 'disable' : 'enable'}" data-repo-key="${escapeHtml(repo.repoKey)}">${repo.enabled ? '停用' : '启用'}</button></td></tr>`).join('') : '<tr><td colspan="6" class="gitea-workflow__empty">暂无仓库记录</td></tr>'}</tbody></table></div></section>
|
||
<section class="gitea-workflow__section"><h3>任务队列与 Turn</h3><div class="gitea-workflow__table-wrap"><table class="gitea-workflow__table"><thead><tr><th>任务 / 资源</th><th>状态</th><th>Turn</th><th>脏目录原因</th><th>更新时间</th><th>操作</th></tr></thead><tbody>${tasks.length ? tasks.map((task) => `<tr><td><strong class="gitea-workflow__mono">${escapeHtml(task.taskId || '—')}</strong><br><span class="gitea-workflow__muted">${escapeHtml(task.resourceKey || task.repoKey || '—')}</span></td><td><span class="gitea-workflow__pill" data-state="${escapeHtml(task.state)}">${escapeHtml(stateLabel(task))}</span>${task.errorMessage ? `<br><span class="gitea-workflow__dirty">${escapeHtml(task.errorMessage)}</span>` : ''}</td><td><span class="gitea-workflow__mono">${escapeHtml(task.turnId || '—')}</span><br><span class="gitea-workflow__muted">${escapeHtml(task.turnState || '—')}</span></td><td class="gitea-workflow__dirty">${escapeHtml(task.dirtyReason || '—')}</td><td>${date(task.updatedAt || task.turnUpdatedAt || task.createdAt)}</td><td>${task.state === 'queued' || task.state === 'waiting_user' ? `<button class="gitea-workflow__button" data-task-action="cancel" data-task-id="${escapeHtml(task.taskId)}">取消排队</button>` : ''}${task.state === 'running' || task.state === 'preparing' || task.state === 'aborting' ? `<button class="gitea-workflow__button" data-danger data-task-action="abort" data-task-id="${escapeHtml(task.taskId)}">中止 Turn</button>` : ''}</td></tr>`).join('') : '<tr><td colspan="6" class="gitea-workflow__empty">暂无任务记录</td></tr>'}</tbody></table></div></section>
|
||
<section class="gitea-workflow__section"><h3>运行日志</h3><div class="gitea-workflow__log">${logs.length ? logs.slice().reverse().map((entry) => `<div class="gitea-workflow__log-entry"><span class="gitea-workflow__muted">${date(entry.timestamp)}</span><span>${escapeHtml(entry.level)}</span><span>${escapeHtml(entry.message || entry.event || '—')}</span></div>`).join('') : '<div class="gitea-workflow__empty">暂无日志</div>'}</div></section>
|
||
<section class="gitea-workflow__section"><h3>操作记录</h3><div class="gitea-workflow__table-wrap"><table class="gitea-workflow__table"><thead><tr><th>时间</th><th>动作</th><th>任务 / 仓库</th></tr></thead><tbody>${audits.length ? audits.slice().reverse().map((entry) => `<tr><td>${date(entry.timestamp)}</td><td class="gitea-workflow__mono">${escapeHtml(actionLabel(entry.action))}</td><td class="gitea-workflow__mono">${escapeHtml(entry.taskId || entry.repoKey || '—')}</td></tr>`).join('') : '<tr><td colspan="3" class="gitea-workflow__empty">暂无记录</td></tr>'}</tbody></table></div></section>`;
|
||
root.querySelectorAll('[data-action], [data-repo-action], [data-task-action], [data-config-save]').forEach((button) => button.addEventListener('click', button.dataset.configSave !== undefined ? saveConfig : onAction));
|
||
}
|
||
|
||
async function load() {
|
||
if (loading) return;
|
||
loading = true;
|
||
status.textContent = '正在刷新工作流状态…';
|
||
try {
|
||
const [nextOverview, config] = await Promise.all([api('/api/gitea-workflow/overview'), api('/api/gitea-workflow/config')]);
|
||
overview = { ...nextOverview, settings: config.settings || {} };
|
||
render();
|
||
status.textContent = overview.available ? '状态已更新' : '核心服务未挂接,显示镜像状态';
|
||
} catch (error) {
|
||
root.innerHTML = `<div class="gitea-workflow__error">加载失败:${escapeHtml(error.message)}</div>`;
|
||
status.textContent = '加载失败';
|
||
} finally { loading = false; }
|
||
}
|
||
|
||
async function saveConfig(event) {
|
||
const button = event.currentTarget;
|
||
button.disabled = true;
|
||
try {
|
||
await api('/api/gitea-workflow/config', { method: 'PUT', body: JSON.stringify({
|
||
...systemAuditFields(),
|
||
host: $('#gitea-config-host')?.value || '', botLogin: $('#gitea-config-bot-login')?.value || '',
|
||
workspaceRoot: $('#gitea-config-workspace')?.value || '', defaultBranch: $('#gitea-config-branch')?.value || '',
|
||
botToken: $('#gitea-config-bot-token')?.value || '',
|
||
}) });
|
||
await load();
|
||
} catch (error) { window.alert(`配置保存失败:${error.message}`); } finally { button.disabled = false; }
|
||
}
|
||
|
||
async function onAction(event) {
|
||
const button = event.currentTarget;
|
||
button.disabled = true;
|
||
try {
|
||
let path;
|
||
let method = 'POST';
|
||
if (button.dataset.action) path = `/api/gitea-workflow/control/${button.dataset.action}`;
|
||
if (button.dataset.repoAction) path = `/api/gitea-workflow/repos/${encodeURIComponent(button.dataset.repoKey)}/${button.dataset.repoAction}`;
|
||
if (button.dataset.taskAction) path = `/api/gitea-workflow/tasks/${encodeURIComponent(button.dataset.taskId)}/${button.dataset.taskAction}`;
|
||
await api(path, { method, body: JSON.stringify({
|
||
...systemAuditFields(window.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`),
|
||
}) });
|
||
await load();
|
||
} catch (error) {
|
||
window.alert(`操作未执行:${error.message}`);
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
function open() {
|
||
panel.hidden = false; panel.setAttribute('aria-hidden', 'false'); openButton.setAttribute('aria-expanded', 'true'); load();
|
||
}
|
||
function close() { panel.hidden = true; panel.setAttribute('aria-hidden', 'true'); openButton.setAttribute('aria-expanded', 'false'); }
|
||
openButton.addEventListener('click', open);
|
||
closeButton?.addEventListener('click', close);
|
||
refreshButton?.addEventListener('click', load);
|
||
})();
|