feat: support MCP elicitation and rebuild release

This commit is contained in:
shiyue
2026-08-24 17:39:41 +08:00
parent dd233a40e8
commit bd20a79d4b
69 changed files with 8978 additions and 13 deletions

161
public/gitea-workflow.js Normal file
View File

@@ -0,0 +1,161 @@
(() => {
'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);
})();