feat:访问控制和dashboard重启功能
This commit is contained in:
26
index.js
26
index.js
@@ -6,7 +6,7 @@ const config = require('./src/config/env');
|
|||||||
const { getConfiguredRepos } = require('./src/config/mappings');
|
const { getConfiguredRepos } = require('./src/config/mappings');
|
||||||
const { handleIssueEvent } = require('./src/logic/syncManager');
|
const { handleIssueEvent } = require('./src/logic/syncManager');
|
||||||
const { handleJiraHook } = require('./src/logic/jiraSyncManager');
|
const { handleJiraHook } = require('./src/logic/jiraSyncManager');
|
||||||
const editorRoutes = require('./src/routes/editor');
|
const editorRoutes = require('./src/routes/control');
|
||||||
const logger = require('./src/utils/logger');
|
const logger = require('./src/utils/logger');
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
@@ -47,6 +47,30 @@ setInterval(() => {
|
|||||||
}
|
}
|
||||||
}, 5 * 60 * 1000);
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
//内网访问控制中间件:保护管理界面,只允许dotenv配置的域名访问
|
||||||
|
const internalOnlyMiddleware = async (c, next) => {
|
||||||
|
const pathname = new URL(c.req.url).pathname;
|
||||||
|
|
||||||
|
if (pathname.startsWith('/hooks/')) {
|
||||||
|
return await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = (c.req.header('host') || '').split(':')[0];
|
||||||
|
const allowedHosts = config.app.dashboardAllowedHosts;
|
||||||
|
|
||||||
|
if (!allowedHosts.some(allowed => host === allowed || host.endsWith('.' + allowed))) {
|
||||||
|
logger.security(`Blocked access from unauthorized host: ${host}`, {
|
||||||
|
path: pathname,
|
||||||
|
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
|
||||||
|
});
|
||||||
|
return c.text('Forbidden - Access denied from this domain', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use('*', internalOnlyMiddleware);
|
||||||
|
|
||||||
//Gitea webhook处理入口
|
//Gitea webhook处理入口
|
||||||
app.post('/hooks/gitea', rateLimiter, async (c) => {
|
app.post('/hooks/gitea', rateLimiter, async (c) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,35 +5,35 @@ function switchTab(tab) {
|
|||||||
btn.classList.remove('bg-indigo-600', 'text-white');
|
btn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
btn.classList.add('hover:bg-slate-800', 'text-slate-400', 'hover:text-white');
|
btn.classList.add('hover:bg-slate-800', 'text-slate-400', 'hover:text-white');
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeBtn = document.getElementById(`tab-${tab}`);
|
const activeBtn = document.getElementById(`tab-${tab}`);
|
||||||
if (activeBtn) {
|
if (activeBtn) {
|
||||||
activeBtn.classList.add('bg-indigo-600', 'text-white');
|
activeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
activeBtn.classList.remove('hover:bg-slate-800', 'text-slate-400', 'hover:text-white');
|
activeBtn.classList.remove('hover:bg-slate-800', 'text-slate-400', 'hover:text-white');
|
||||||
}
|
}
|
||||||
|
|
||||||
//切换内容区
|
//切换内容区
|
||||||
document.querySelectorAll('.tab-content').forEach(content => {
|
document.querySelectorAll('.tab-content').forEach(content => {
|
||||||
content.classList.add('hidden');
|
content.classList.add('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeContent = document.getElementById(`content-${tab}`);
|
const activeContent = document.getElementById(`content-${tab}`);
|
||||||
if (activeContent) {
|
if (activeContent) {
|
||||||
activeContent.classList.remove('hidden');
|
activeContent.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果切换到日志页,开始实时加载
|
//如果切换到日志页,开始实时加载
|
||||||
if (tab === 'logs') {
|
if (tab === 'logs') {
|
||||||
startLogStreaming();
|
startLogStreaming();
|
||||||
} else {
|
} else {
|
||||||
stopLogStreaming();
|
stopLogStreaming();
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果切换到设置页,加载 .env 文件
|
//如果切换到设置页,加载 .env 文件
|
||||||
if (tab === 'settings') {
|
if (tab === 'settings') {
|
||||||
loadEnvFile();
|
loadEnvFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果切换到使用指南页,加载 README
|
//如果切换到使用指南页,加载 README
|
||||||
if (tab === 'guide') {
|
if (tab === 'guide') {
|
||||||
loadGuide();
|
loadGuide();
|
||||||
@@ -47,7 +47,7 @@ let lastLogSize = 0;
|
|||||||
async function startLogStreaming() {
|
async function startLogStreaming() {
|
||||||
//立即加载一次
|
//立即加载一次
|
||||||
await loadLogs();
|
await loadLogs();
|
||||||
|
|
||||||
//每2秒刷新一次
|
//每2秒刷新一次
|
||||||
logInterval = setInterval(loadLogs, 2000);
|
logInterval = setInterval(loadLogs, 2000);
|
||||||
}
|
}
|
||||||
@@ -63,19 +63,19 @@ async function loadLogs() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('/api/logs');
|
const res = await fetch('/api/logs');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const logViewer = document.getElementById('log-viewer');
|
const logViewer = document.getElementById('log-viewer');
|
||||||
document.getElementById('log-filename').textContent = data.filename || 'sync_service.log';
|
document.getElementById('log-filename').textContent = data.filename || 'sync_service.log';
|
||||||
|
|
||||||
if (data.logs && data.logs.length > 0) {
|
if (data.logs && data.logs.length > 0) {
|
||||||
//只在日志有变化时更新
|
//只在日志有变化时更新
|
||||||
const newContent = data.logs.map((log, index) =>
|
const newContent = data.logs.map((log, index) =>
|
||||||
`<div class="log-line border-l-2 border-transparent hover:border-slate-700 hover:bg-slate-800/50 pl-2 py-0.5">
|
`<div class="log-line border-l-2 border-transparent hover:border-slate-700 hover:bg-slate-800/50 pl-2 py-0.5">
|
||||||
<span class="opacity-50 select-none mr-2">${index + 1}</span>${escapeHtml(log)}
|
<span class="opacity-50 select-none mr-2">${index + 1}</span>${escapeHtml(log)}
|
||||||
</div>`
|
</div>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
if (logViewer.innerHTML !== newContent) {
|
if (logViewer.innerHTML !== newContent) {
|
||||||
logViewer.innerHTML = newContent;
|
logViewer.innerHTML = newContent;
|
||||||
//自动滚动到底部
|
//自动滚动到底部
|
||||||
@@ -102,18 +102,18 @@ async function loadDashboardData() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('/api/status');
|
const res = await fetch('/api/status');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
//更新统计数据
|
//更新统计数据
|
||||||
document.getElementById('today-syncs').textContent = data.todaySyncs || '--';
|
document.getElementById('today-syncs').textContent = data.todaySyncs || '--';
|
||||||
document.getElementById('repo-count').textContent = data.repoCount || '--';
|
document.getElementById('repo-count').textContent = data.repoCount || '--';
|
||||||
document.getElementById('error-count').textContent = (data.errorCount + data.fatalCount) || '--';
|
document.getElementById('error-count').textContent = (data.errorCount + data.fatalCount) || '--';
|
||||||
document.getElementById('uptime').textContent = data.uptime ? `系统运行时间: ${data.uptime}` : '加载中...';
|
document.getElementById('uptime').textContent = data.uptime ? `系统运行时间: ${data.uptime}` : '加载中...';
|
||||||
|
|
||||||
//更新服务状态
|
//更新服务状态
|
||||||
updateServiceStatus(data.status);
|
updateServiceStatus(data.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
//加载历史记录
|
//加载历史记录
|
||||||
loadHistory();
|
loadHistory();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -125,15 +125,15 @@ async function loadHistory() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('/api/history');
|
const res = await fetch('/api/history');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success && data.history) {
|
if (data.success && data.history) {
|
||||||
const tbody = document.getElementById('history-table');
|
const tbody = document.getElementById('history-table');
|
||||||
|
|
||||||
if (data.history.length === 0) {
|
if (data.history.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">暂无历史记录</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">暂无历史记录</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = data.history.map(day => `
|
tbody.innerHTML = data.history.map(day => `
|
||||||
<tr class="hover:bg-slate-50">
|
<tr class="hover:bg-slate-50">
|
||||||
<td class="px-4 py-3 text-sm text-slate-900">${day.date}</td>
|
<td class="px-4 py-3 text-sm text-slate-900">${day.date}</td>
|
||||||
@@ -153,7 +153,7 @@ async function loadHistory() {
|
|||||||
function updateServiceStatus(status) {
|
function updateServiceStatus(status) {
|
||||||
const badge = document.getElementById('status-badge');
|
const badge = document.getElementById('status-badge');
|
||||||
const statusText = document.getElementById('service-status');
|
const statusText = document.getElementById('service-status');
|
||||||
|
|
||||||
if (status === 'running') {
|
if (status === 'running') {
|
||||||
badge.className = 'px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-emerald-50 text-emerald-600 border-emerald-200';
|
badge.className = 'px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-emerald-50 text-emerald-600 border-emerald-200';
|
||||||
badge.innerHTML = `
|
badge.innerHTML = `
|
||||||
@@ -178,17 +178,24 @@ function updateServiceStatus(status) {
|
|||||||
//控制机器人
|
//控制机器人
|
||||||
async function controlBot(action) {
|
async function controlBot(action) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/control', {
|
const res = await fetch('/api/restart', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ action })
|
body: JSON.stringify({ action })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert(`操作成功: ${data.message || action}`);
|
alert(`操作成功: ${data.message || action}`);
|
||||||
loadDashboardData();
|
//服务重启后延迟刷新页面
|
||||||
|
if (action === 'restart') {
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload();
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
loadDashboardData();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
alert(`操作失败: ${data.error}`);
|
alert(`操作失败: ${data.error}`);
|
||||||
}
|
}
|
||||||
@@ -202,14 +209,14 @@ async function clearLogs() {
|
|||||||
if (!confirm('确定要清空日志吗?此操作不可恢复。')) {
|
if (!confirm('确定要清空日志吗?此操作不可恢复。')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/logs/clear', {
|
const res = await fetch('/api/logs/clear', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert('日志已清空');
|
alert('日志已清空');
|
||||||
if (document.getElementById('content-logs').classList.contains('hidden') === false) {
|
if (document.getElementById('content-logs').classList.contains('hidden') === false) {
|
||||||
@@ -230,11 +237,11 @@ function refreshStatus() {
|
|||||||
|
|
||||||
async function loadEnvFile() {
|
async function loadEnvFile() {
|
||||||
const container = document.getElementById('envEditor');
|
const container = document.getElementById('envEditor');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/env');
|
const res = await fetch('/api/env');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const envContent = data.content;
|
const envContent = data.content;
|
||||||
const envItems = parseEnvContent(envContent);
|
const envItems = parseEnvContent(envContent);
|
||||||
@@ -250,10 +257,10 @@ async function loadEnvFile() {
|
|||||||
function parseEnvContent(content) {
|
function parseEnvContent(content) {
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
if (trimmed === '' || trimmed.startsWith('#')) {
|
if (trimmed === '' || trimmed.startsWith('#')) {
|
||||||
items.push({ type: 'comment', value: line });
|
items.push({ type: 'comment', value: line });
|
||||||
} else if (trimmed.includes('=')) {
|
} else if (trimmed.includes('=')) {
|
||||||
@@ -265,7 +272,7 @@ function parseEnvContent(content) {
|
|||||||
items.push({ type: 'comment', value: line });
|
items.push({ type: 'comment', value: line });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,10 +282,10 @@ function renderEnvForm(items) {
|
|||||||
if (item.type === 'comment') {
|
if (item.type === 'comment') {
|
||||||
return `<div class="text-slate-400 text-xs font-mono py-1">${escapeHtml(item.value)}</div>`;
|
return `<div class="text-slate-400 text-xs font-mono py-1">${escapeHtml(item.value)}</div>`;
|
||||||
} else {
|
} else {
|
||||||
const isSecret = item.key.toLowerCase().includes('token') ||
|
const isSecret = item.key.toLowerCase().includes('token') ||
|
||||||
item.key.toLowerCase().includes('secret') ||
|
item.key.toLowerCase().includes('secret') ||
|
||||||
item.key.toLowerCase().includes('password') ||
|
item.key.toLowerCase().includes('password') ||
|
||||||
item.key.toLowerCase().includes('pat');
|
item.key.toLowerCase().includes('pat');
|
||||||
return `
|
return `
|
||||||
<div class="flex items-center gap-3 bg-slate-50 hover:bg-slate-100 p-3 rounded border border-slate-200 transition-colors">
|
<div class="flex items-center gap-3 bg-slate-50 hover:bg-slate-100 p-3 rounded border border-slate-200 transition-colors">
|
||||||
<label class="text-slate-700 font-mono text-sm flex-shrink-0 w-48 font-medium">${escapeHtml(item.key)}</label>
|
<label class="text-slate-700 font-mono text-sm flex-shrink-0 w-48 font-medium">${escapeHtml(item.key)}</label>
|
||||||
@@ -296,25 +303,25 @@ function renderEnvForm(items) {
|
|||||||
|
|
||||||
async function saveEnvFile() {
|
async function saveEnvFile() {
|
||||||
const btn = document.getElementById('saveEnvBtn');
|
const btn = document.getElementById('saveEnvBtn');
|
||||||
|
|
||||||
if (!confirm('确定要保存配置吗?保存后需要重启服务才能生效。')) {
|
if (!confirm('确定要保存配置吗?保存后需要重启服务才能生效。')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '保存中...';
|
btn.textContent = '保存中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = buildEnvContent();
|
const content = buildEnvContent();
|
||||||
|
|
||||||
const res = await fetch('/api/env', {
|
const res = await fetch('/api/env', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content })
|
body: JSON.stringify({ content })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert(`保存成功!\n\n${data.message}`);
|
alert(`保存成功!\n\n${data.message}`);
|
||||||
loadEnvFile();
|
loadEnvFile();
|
||||||
@@ -332,7 +339,7 @@ async function saveEnvFile() {
|
|||||||
function buildEnvContent() {
|
function buildEnvContent() {
|
||||||
const container = document.getElementById('envEditor');
|
const container = document.getElementById('envEditor');
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
|
||||||
container.childNodes.forEach(node => {
|
container.childNodes.forEach(node => {
|
||||||
if (node.classList && node.classList.contains('text-slate-400')) {
|
if (node.classList && node.classList.contains('text-slate-400')) {
|
||||||
lines.push(node.textContent);
|
lines.push(node.textContent);
|
||||||
@@ -343,17 +350,17 @@ function buildEnvContent() {
|
|||||||
lines.push(`${key}=${value}`);
|
lines.push(`${key}=${value}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadGuide() {
|
async function loadGuide() {
|
||||||
const container = document.getElementById('guide-content');
|
const container = document.getElementById('guide-content');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/guide');
|
const res = await fetch('/api/guide');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success && data.content) {
|
if (data.success && data.content) {
|
||||||
//使用marked.js渲染markdown
|
//使用marked.js渲染markdown
|
||||||
if (typeof marked !== 'undefined') {
|
if (typeof marked !== 'undefined') {
|
||||||
@@ -391,10 +398,10 @@ async function loadGuide() {
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
//默认显示dashboard
|
//默认显示dashboard
|
||||||
switchTab('dashboard');
|
switchTab('dashboard');
|
||||||
|
|
||||||
//加载初始数据
|
//加载初始数据
|
||||||
loadDashboardData();
|
loadDashboardData();
|
||||||
|
|
||||||
//定期刷新仪表盘数据(每30秒)
|
//定期刷新仪表盘数据(每30秒)
|
||||||
setInterval(loadDashboardData, 30000);
|
setInterval(loadDashboardData, 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const config = {
|
|||||||
rate: process.env.RATE_LIMIT_WINDOW || 10000,
|
rate: process.env.RATE_LIMIT_WINDOW || 10000,
|
||||||
maxRequests: process.env.MAX_REQUESTS_PER_WINDOW || 20,
|
maxRequests: process.env.MAX_REQUESTS_PER_WINDOW || 20,
|
||||||
debugMode: process.env.DEBUG_MODE === 'true',
|
debugMode: process.env.DEBUG_MODE === 'true',
|
||||||
logRetentionDays: process.env.LOG_RETENTION_DAYS || 30
|
logRetentionDays: process.env.LOG_RETENTION_DAYS || 30,
|
||||||
|
dashboardAllowedHosts: (process.env.DASHBOARD_ALLOWED_HOSTS || 'localhost,127.0.0.1').split(',').map(h => h.trim())
|
||||||
},
|
},
|
||||||
gitea: {
|
gitea: {
|
||||||
baseUrl: process.env.GITEA_BASE_URL,
|
baseUrl: process.env.GITEA_BASE_URL,
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* 映射关系编辑器路由模块
|
* 控制面板路由模块
|
||||||
* 提供映射配置的 CRUD 操作和 Jira API 代理
|
* 提供映射配置的 CRUD 操作、Jira API 代理和服务控制
|
||||||
*/
|
*/
|
||||||
const { Hono } = require('hono');
|
const { Hono } = require('hono');
|
||||||
|
const { exec } = require('child_process');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
const editor = new Hono();
|
const control = new Hono();
|
||||||
|
|
||||||
const MAPPINGS_PATH = path.join(__dirname, '../../mappings.json');
|
const MAPPINGS_PATH = path.join(__dirname, '../../mappings.json');
|
||||||
const LOGS_DIR = path.join(__dirname, '../../logs');
|
const LOGS_DIR = path.join(__dirname, '../../logs');
|
||||||
const README_PATH = path.join(__dirname, '../../how-to-use.md');
|
const README_PATH = path.join(__dirname, '../../how-to-use.md');
|
||||||
|
|
||||||
editor.get('/status', (c) => {
|
control.get('/status', (c) => {
|
||||||
try {
|
try {
|
||||||
let repoCount = 0;
|
let repoCount = 0;
|
||||||
if (fs.existsSync(MAPPINGS_PATH)) {
|
if (fs.existsSync(MAPPINGS_PATH)) {
|
||||||
@@ -58,7 +59,7 @@ editor.get('/status', (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//获取历史统计数据
|
//获取历史统计数据
|
||||||
editor.get('/history', (c) => {
|
control.get('/history', (c) => {
|
||||||
try {
|
try {
|
||||||
const history = [];
|
const history = [];
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ editor.get('/history', (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//获取当日日志
|
//获取当日日志
|
||||||
editor.get('/logs', (c) => {
|
control.get('/logs', (c) => {
|
||||||
try {
|
try {
|
||||||
//获取今天的日志文件
|
//获取今天的日志文件
|
||||||
const today = new Date().toISOString().split('T')[0]; //YYYY-MM-DD
|
const today = new Date().toISOString().split('T')[0]; //YYYY-MM-DD
|
||||||
@@ -135,7 +136,7 @@ editor.get('/logs', (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//清空当日日志
|
//清空当日日志
|
||||||
editor.post('/logs/clear', (c) => {
|
control.post('/logs/clear', (c) => {
|
||||||
try {
|
try {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
const logFile = path.join(LOGS_DIR, `sync-${today}.log`);
|
const logFile = path.join(LOGS_DIR, `sync-${today}.log`);
|
||||||
@@ -152,35 +153,38 @@ editor.post('/logs/clear', (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//控制机器人(重启等)
|
//控制机器人(支持 PM2 软重启)
|
||||||
editor.post('/control', async (c) => {
|
control.post('/restart', async (c) => {
|
||||||
try {
|
try {
|
||||||
const { action } = await c.req.json();
|
const { action } = await c.req.json();
|
||||||
|
logger.info(`[Control] Action received: ${action}`);
|
||||||
|
|
||||||
logger.info(`[Editor] Control action received: ${action}`);
|
|
||||||
|
|
||||||
//注意:实际的重启需要外部进程管理器(如 PM2)
|
|
||||||
//这里只是记录日志
|
|
||||||
if (action === 'restart') {
|
if (action === 'restart') {
|
||||||
logger.info('[Editor] Restart requested (requires PM2 or similar)');
|
logger.info('[Control] PM2 restart requested via dashboard');
|
||||||
return c.json({
|
|
||||||
success: true,
|
//延迟执行,让响应先返回给客户端
|
||||||
message: '重启请求已记录,请使用 PM2 或其他进程管理器执行重启'
|
setTimeout(() => {
|
||||||
});
|
exec('pm2 restart gitea-jira-sync', (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('[Control] PM2 restart failed:', err.message);
|
||||||
|
} else {
|
||||||
|
logger.info('[Control] PM2 restart executed successfully');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return c.json({ success: true, message: '正在重启服务,请稍候刷新页面...' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json({ success: false, error: '不支持的操作' });
|
||||||
success: false,
|
|
||||||
error: '不支持的操作'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('[Editor] Control error:', e.message);
|
logger.error('[Control] Restart error:', e.message);
|
||||||
return c.json({ success: false, error: e.message }, 500);
|
return c.json({ success: false, error: e.message }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//读取 .env 文件
|
//读取 .env 文件
|
||||||
editor.get('/env', (c) => {
|
control.get('/env', (c) => {
|
||||||
try {
|
try {
|
||||||
const envPath = path.join(__dirname, '../../.env');
|
const envPath = path.join(__dirname, '../../.env');
|
||||||
|
|
||||||
@@ -200,7 +204,7 @@ editor.get('/env', (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//保存 .env 文件
|
//保存 .env 文件
|
||||||
editor.post('/env', async (c) => {
|
control.post('/env', async (c) => {
|
||||||
try {
|
try {
|
||||||
const { content } = await c.req.json();
|
const { content } = await c.req.json();
|
||||||
const envPath = path.join(__dirname, '../../.env');
|
const envPath = path.join(__dirname, '../../.env');
|
||||||
@@ -226,7 +230,7 @@ editor.post('/env', async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.get('/guide', (c) => {
|
control.get('/guide', (c) => {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(README_PATH)) {
|
if (!fs.existsSync(README_PATH)) {
|
||||||
return c.json({
|
return c.json({
|
||||||
@@ -245,7 +249,7 @@ editor.get('/guide', (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//读取现有的 mappings.json
|
//读取现有的 mappings.json
|
||||||
editor.get('/mappings', (c) => {
|
control.get('/mappings', (c) => {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(MAPPINGS_PATH)) {
|
if (!fs.existsSync(MAPPINGS_PATH)) {
|
||||||
return c.json({ success: true, data: { repositories: {} } });
|
return c.json({ success: true, data: { repositories: {} } });
|
||||||
@@ -262,7 +266,7 @@ editor.get('/mappings', (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//保存/更新 mappings.json
|
//保存/更新 mappings.json
|
||||||
editor.post('/mappings', async (c) => {
|
control.post('/mappings', async (c) => {
|
||||||
try {
|
try {
|
||||||
const { repoName, config } = await c.req.json();
|
const { repoName, config } = await c.req.json();
|
||||||
|
|
||||||
@@ -292,7 +296,7 @@ editor.post('/mappings', async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//删除仓库配置
|
//删除仓库配置
|
||||||
editor.delete('/mappings/:repoName', async (c) => {
|
control.delete('/mappings/:repoName', async (c) => {
|
||||||
try {
|
try {
|
||||||
const repoName = decodeURIComponent(c.req.param('repoName'));
|
const repoName = decodeURIComponent(c.req.param('repoName'));
|
||||||
|
|
||||||
@@ -324,7 +328,7 @@ editor.delete('/mappings/:repoName', async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//改名仓库配置
|
//改名仓库配置
|
||||||
editor.post('/mappings/rename', async (c) => {
|
control.post('/mappings/rename', async (c) => {
|
||||||
try {
|
try {
|
||||||
const { oldName, newName } = await c.req.json();
|
const { oldName, newName } = await c.req.json();
|
||||||
|
|
||||||
@@ -368,7 +372,7 @@ editor.post('/mappings/rename', async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//保存配置接口(兼容旧版)
|
//保存配置接口(兼容旧版)
|
||||||
editor.post('/save', async (c) => {
|
control.post('/save', async (c) => {
|
||||||
try {
|
try {
|
||||||
const newConfigObj = await c.req.json();
|
const newConfigObj = await c.req.json();
|
||||||
const repoName = Object.keys(newConfigObj)[0];
|
const repoName = Object.keys(newConfigObj)[0];
|
||||||
@@ -405,7 +409,7 @@ editor.post('/save', async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//扫描 Jira 项目信息
|
//扫描 Jira 项目信息
|
||||||
editor.post('/scan', async (c) => {
|
control.post('/scan', async (c) => {
|
||||||
const { baseUrl, auth, projectKey: rawKey } = await c.req.json();
|
const { baseUrl, auth, projectKey: rawKey } = await c.req.json();
|
||||||
const inputKey = rawKey ? rawKey.trim() : '';
|
const inputKey = rawKey ? rawKey.trim() : '';
|
||||||
|
|
||||||
@@ -482,7 +486,7 @@ editor.post('/scan', async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//扫描 Sprint 信息
|
//扫描 Sprint 信息
|
||||||
editor.post('/scan-sprint', async (c) => {
|
control.post('/scan-sprint', async (c) => {
|
||||||
const { baseUrl, auth, issueKey } = await c.req.json();
|
const { baseUrl, auth, issueKey } = await c.req.json();
|
||||||
let headers = { 'Accept': 'application/json' };
|
let headers = { 'Accept': 'application/json' };
|
||||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
|
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
|
||||||
@@ -510,7 +514,7 @@ editor.post('/scan-sprint', async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//代理 Jira API 请求
|
//代理 Jira API 请求
|
||||||
editor.post('/proxy-jira', async (c) => {
|
control.post('/proxy-jira', async (c) => {
|
||||||
const { url, auth } = await c.req.json();
|
const { url, auth } = await c.req.json();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -532,4 +536,4 @@ editor.post('/proxy-jira', async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = editor;
|
module.exports = control;
|
||||||
Reference in New Issue
Block a user