Files
cc-web/public/gitea-workflow.js

162 lines
12 KiB
JavaScript
Raw Permalink 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.

(() => {
'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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
.replaceAll('"', '&quot;').replaceAll("'", '&#39;');
}
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);
})();