style:优化代码格式,提高可读性
This commit is contained in:
2
index.js
2
index.js
@@ -102,7 +102,7 @@ app.post('/hooks/jira', rateLimiter, async (c) => {
|
|||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
logger.info(`[JIRA HOOK] Received request`, { event: body?.webhookEvent });
|
logger.info(`[JIRA HOOK] Received request`, { event: body?.webhookEvent });
|
||||||
|
|
||||||
// Jira Webhook通常没有签名头,依赖IP白名单或URL secret参数,此处仅校验结构
|
//Jira Webhook通常没有签名头,依赖IP白名单或URL secret参数,此处仅校验结构
|
||||||
if (!body || !body.webhookEvent) {
|
if (!body || !body.webhookEvent) {
|
||||||
logger.warn(`[JIRA HOOK] Invalid payload: missing webhookEvent`);
|
logger.warn(`[JIRA HOOK] Invalid payload: missing webhookEvent`);
|
||||||
return c.text('Invalid Jira payload', 400);
|
return c.text('Invalid Jira payload', 400);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const config = {
|
|||||||
app: {
|
app: {
|
||||||
port: process.env.PORT || 3000,
|
port: process.env.PORT || 3000,
|
||||||
dbPath: process.env.DB_FILE_PATH || './sync.sqlite',
|
dbPath: process.env.DB_FILE_PATH || './sync.sqlite',
|
||||||
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
// 读取配置文件路径
|
//读取配置文件路径
|
||||||
const configPath = path.join(__dirname, '../../mappings.json');
|
const configPath = path.join(__dirname, '../../mappings.json');
|
||||||
|
|
||||||
let mappingsConfig = null;
|
let mappingsConfig = null;
|
||||||
@@ -14,14 +14,14 @@ function loadMappings() {
|
|||||||
if (mappingsConfig) {
|
if (mappingsConfig) {
|
||||||
return mappingsConfig;
|
return mappingsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||||
mappingsConfig = JSON.parse(configContent);
|
mappingsConfig = JSON.parse(configContent);
|
||||||
|
|
||||||
// 处理环境变量替换
|
//处理环境变量替换
|
||||||
processEnvVariables(mappingsConfig);
|
processEnvVariables(mappingsConfig);
|
||||||
|
|
||||||
return mappingsConfig;
|
return mappingsConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`无法加载映射配置文件 ${configPath}: ${error.message}`);
|
throw new Error(`无法加载映射配置文件 ${configPath}: ${error.message}`);
|
||||||
@@ -86,11 +86,11 @@ const defaultMappings = new Proxy({}, {
|
|||||||
function getRepoConfig(repoFullName) {
|
function getRepoConfig(repoFullName) {
|
||||||
const config = loadMappings();
|
const config = loadMappings();
|
||||||
const repoConfig = config.repositories[repoFullName];
|
const repoConfig = config.repositories[repoFullName];
|
||||||
|
|
||||||
if (!repoConfig) {
|
if (!repoConfig) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jira: repoConfig.jira,
|
jira: repoConfig.jira,
|
||||||
priorities: repoConfig.priorities || config.defaultMappings.priorities,
|
priorities: repoConfig.priorities || config.defaultMappings.priorities,
|
||||||
@@ -117,7 +117,7 @@ function getConfiguredRepos() {
|
|||||||
*/
|
*/
|
||||||
function getRepoByJiraProject(jiraProjectKey) {
|
function getRepoByJiraProject(jiraProjectKey) {
|
||||||
const config = loadMappings();
|
const config = loadMappings();
|
||||||
|
|
||||||
for (const [repoKey, repoConfig] of Object.entries(config.repositories)) {
|
for (const [repoKey, repoConfig] of Object.entries(config.repositories)) {
|
||||||
if (repoConfig.jira && repoConfig.jira.projectKey === jiraProjectKey) {
|
if (repoConfig.jira && repoConfig.jira.projectKey === jiraProjectKey) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ const j2m = require('j2m');
|
|||||||
//返回null表示类型未配置,不应同步
|
//返回null表示类型未配置,不应同步
|
||||||
function buildJiraFields(title, body, labels, milestone, repoConfig) {
|
function buildJiraFields(title, body, labels, milestone, repoConfig) {
|
||||||
const { jira, priorities, types, sprints } = repoConfig;
|
const { jira, priorities, types, sprints } = repoConfig;
|
||||||
|
|
||||||
let issueTypeId = null;
|
let issueTypeId = null;
|
||||||
let priorityId = "3";
|
let priorityId = "3";
|
||||||
let typeFound = false;
|
let typeFound = false;
|
||||||
|
|
||||||
//处理标签
|
//处理标签
|
||||||
if (labels && labels.length > 0) {
|
if (labels && labels.length > 0) {
|
||||||
for (const label of labels) {
|
for (const label of labels) {
|
||||||
@@ -21,7 +21,7 @@ function buildJiraFields(title, body, labels, milestone, repoConfig) {
|
|||||||
if (priorities[name]) priorityId = priorities[name];
|
if (priorities[name]) priorityId = priorities[name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果没有找到匹配的类型标签,检查是否使用默认类型
|
//如果没有找到匹配的类型标签,检查是否使用默认类型
|
||||||
if (!typeFound) {
|
if (!typeFound) {
|
||||||
if (jira.defaultType) {
|
if (jira.defaultType) {
|
||||||
@@ -32,13 +32,13 @@ function buildJiraFields(title, body, labels, milestone, repoConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let sprintId = null;
|
let sprintId = null;
|
||||||
|
|
||||||
if (milestone && sprints[milestone.title]) {
|
if (milestone && sprints[milestone.title]) {
|
||||||
sprintId = sprints[milestone.title];
|
sprintId = sprints[milestone.title];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果body为空,给默认值,否则将Markdown转换为Jira Wiki Markup
|
//如果body为空,给默认值,否则将Markdown转换为Jira Wiki Markup
|
||||||
let description = "No description";
|
let description = "No description";
|
||||||
if (body) {
|
if (body) {
|
||||||
description = j2m.toJ(body);
|
description = j2m.toJ(body);
|
||||||
|
|||||||
@@ -53,26 +53,26 @@ function findLabelByTypeName(typeName, typesMap) {
|
|||||||
//处理Java对象toString格式: "com.atlassian...Sprint@xxx[id=37,name=xxx,...]"
|
//处理Java对象toString格式: "com.atlassian...Sprint@xxx[id=37,name=xxx,...]"
|
||||||
function parseSprintId(sprintData) {
|
function parseSprintId(sprintData) {
|
||||||
if (!sprintData) return null;
|
if (!sprintData) return null;
|
||||||
|
|
||||||
// 如果是数组,取最后一个(当前活动Sprint)
|
//如果是数组,取最后一个(当前活动Sprint)
|
||||||
if (Array.isArray(sprintData)) {
|
if (Array.isArray(sprintData)) {
|
||||||
if (sprintData.length === 0) return null;
|
if (sprintData.length === 0) return null;
|
||||||
return parseSprintId(sprintData[sprintData.length - 1]);
|
return parseSprintId(sprintData[sprintData.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已经是对象,直接返回id
|
//如果已经是对象,直接返回id
|
||||||
if (typeof sprintData === 'object' && sprintData.id) {
|
if (typeof sprintData === 'object' && sprintData.id) {
|
||||||
return parseInt(sprintData.id);
|
return parseInt(sprintData.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是字符串(Java对象toString),用正则提取id
|
//如果是字符串(Java对象toString),用正则提取id
|
||||||
if (typeof sprintData === 'string') {
|
if (typeof sprintData === 'string') {
|
||||||
const match = sprintData.match(/\bid=(\d+)/);
|
const match = sprintData.match(/\bid=(\d+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
return parseInt(match[1]);
|
return parseInt(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,16 +95,16 @@ async function handleJiraHook(payload) {
|
|||||||
logger.warn(`[JIRA->GITEA] Invalid payload: missing issue or issue.key`);
|
logger.warn(`[JIRA->GITEA] Invalid payload: missing issue or issue.key`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//检测是否为/resync评论命令
|
//检测是否为/resync评论命令
|
||||||
//Jira的评论事件通常作为 jira:issue_updated 发送,但会包含comment字段
|
//Jira的评论事件通常作为 jira:issue_updated 发送,但会包含comment字段
|
||||||
const hasComment = !!comment;
|
const hasComment = !!comment;
|
||||||
const isResyncCommand = (hasComment && comment.body && comment.body.trim() === '/resync');
|
const isResyncCommand = (hasComment && comment.body && comment.body.trim() === '/resync');
|
||||||
|
|
||||||
if (hasComment) {
|
if (hasComment) {
|
||||||
logger.info(`[JIRA->GITEA] Comment detected in ${issue.key}, body: "${comment.body?.trim()}", isResync: ${isResyncCommand}`);
|
logger.info(`[JIRA->GITEA] Comment detected in ${issue.key}, body: "${comment.body?.trim()}", isResync: ${isResyncCommand}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
//检查标题是否包含#不同步标记
|
//检查标题是否包含#不同步标记
|
||||||
if (issue.fields && issue.fields.summary && issue.fields.summary.includes("#不同步")) {
|
if (issue.fields && issue.fields.summary && issue.fields.summary.includes("#不同步")) {
|
||||||
if (!isResyncCommand) {
|
if (!isResyncCommand) {
|
||||||
@@ -115,28 +115,28 @@ async function handleJiraHook(payload) {
|
|||||||
|
|
||||||
//查找映射关系
|
//查找映射关系
|
||||||
const mapping = dbMap.getGiteaInfo(issue.key);
|
const mapping = dbMap.getGiteaInfo(issue.key);
|
||||||
|
|
||||||
logger.info(`[JIRA->GITEA] Processing ${issue.key}, event: ${webhookEvent}, isResync: ${isResyncCommand}, hasMapping: ${!!mapping}`);
|
logger.info(`[JIRA->GITEA] Processing ${issue.key}, event: ${webhookEvent}, isResync: ${isResyncCommand}, hasMapping: ${!!mapping}`);
|
||||||
|
|
||||||
//处理Jira工单创建事件 - 在Gitea创建对应工单
|
//处理Jira工单创建事件 - 在Gitea创建对应工单
|
||||||
if (webhookEvent === 'jira:issue_created' || (isResyncCommand && !mapping)) {
|
if (webhookEvent === 'jira:issue_created' || (isResyncCommand && !mapping)) {
|
||||||
logger.info(`[JIRA->GITEA] Entering create logic for ${issue.key}`);
|
logger.info(`[JIRA->GITEA] Entering create logic for ${issue.key}`);
|
||||||
|
|
||||||
if (mapping && !isResyncCommand) {
|
if (mapping && !isResyncCommand) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据Jira项目Key查找对应的Gitea仓库
|
//根据Jira项目Key查找对应的Gitea仓库
|
||||||
const projectKey = issue.fields.project.key;
|
const projectKey = issue.fields.project.key;
|
||||||
const repoInfo = getRepoByJiraProject(projectKey);
|
const repoInfo = getRepoByJiraProject(projectKey);
|
||||||
|
|
||||||
if (!repoInfo) {
|
if (!repoInfo) {
|
||||||
logger.warn(`[JIRA->GITEA] No repo configured for project ${projectKey}`);
|
logger.warn(`[JIRA->GITEA] No repo configured for project ${projectKey}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [owner, repo] = repoInfo.repoKey.split('/');
|
const [owner, repo] = repoInfo.repoKey.split('/');
|
||||||
|
|
||||||
//检查类型是否在配置中
|
//检查类型是否在配置中
|
||||||
//优先检查标题中的[类型名],如果存在且能匹配则允许创建
|
//优先检查标题中的[类型名],如果存在且能匹配则允许创建
|
||||||
//否则检查Jira问题类型是否配置
|
//否则检查Jira问题类型是否配置
|
||||||
@@ -154,30 +154,30 @@ async function handleJiraHook(payload) {
|
|||||||
const typeLabel = findGiteaLabel(issueTypeId, repoInfo.config.types);
|
const typeLabel = findGiteaLabel(issueTypeId, repoInfo.config.types);
|
||||||
hasValidType = !!typeLabel;
|
hasValidType = !!typeLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasValidType) {
|
if (!hasValidType) {
|
||||||
logger.info(`[${repoInfo.repoKey}] [JIRA->GITEA] Skipped ${issue.key}: no valid type found in title or issue type`);
|
logger.info(`[${repoInfo.repoKey}] [JIRA->GITEA] Skipped ${issue.key}: no valid type found in title or issue type`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const issueData = {
|
const issueData = {
|
||||||
title: issue.fields.summary || 'Untitled',
|
title: issue.fields.summary || 'Untitled',
|
||||||
body: issue.fields.description ? j2m.toM(issue.fields.description) : ''
|
body: issue.fields.description ? j2m.toM(issue.fields.description) : ''
|
||||||
};
|
};
|
||||||
|
|
||||||
//创建Gitea工单
|
//创建Gitea工单
|
||||||
const giteaIssue = await giteaService.createIssue(owner, repo, issueData);
|
const giteaIssue = await giteaService.createIssue(owner, repo, issueData);
|
||||||
|
|
||||||
//保存映射关系
|
//保存映射关系
|
||||||
dbMap.saveMapping(repoInfo.repoKey, giteaIssue.number, issue.key, issue.id);
|
dbMap.saveMapping(repoInfo.repoKey, giteaIssue.number, issue.key, issue.id);
|
||||||
|
|
||||||
logger.sync(`[${repoInfo.repoKey}] [JIRA->GITEA] Created #${giteaIssue.number} from ${issue.key}`);
|
logger.sync(`[${repoInfo.repoKey}] [JIRA->GITEA] Created #${giteaIssue.number} from ${issue.key}`);
|
||||||
|
|
||||||
//同步标签(类型和优先级)
|
//同步标签(类型和优先级)
|
||||||
try {
|
try {
|
||||||
const labels = [];
|
const labels = [];
|
||||||
|
|
||||||
//添加类型标签:优先从标题中提取[类型名]
|
//添加类型标签:优先从标题中提取[类型名]
|
||||||
let typeLabel = null;
|
let typeLabel = null;
|
||||||
const titleType = extractTypeFromTitle(issue.fields.summary);
|
const titleType = extractTypeFromTitle(issue.fields.summary);
|
||||||
@@ -194,7 +194,7 @@ async function handleJiraHook(payload) {
|
|||||||
if (typeLabel) {
|
if (typeLabel) {
|
||||||
labels.push(typeLabel);
|
labels.push(typeLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
//添加优先级标签
|
//添加优先级标签
|
||||||
if (issue.fields.priority) {
|
if (issue.fields.priority) {
|
||||||
const priorityLabel = findGiteaLabel(issue.fields.priority.id, repoInfo.config.priorities);
|
const priorityLabel = findGiteaLabel(issue.fields.priority.id, repoInfo.config.priorities);
|
||||||
@@ -202,7 +202,7 @@ async function handleJiraHook(payload) {
|
|||||||
labels.push(priorityLabel);
|
labels.push(priorityLabel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果有标签则设置
|
//如果有标签则设置
|
||||||
if (labels.length > 0) {
|
if (labels.length > 0) {
|
||||||
await giteaService.replaceLabels(owner, repo, giteaIssue.number, labels);
|
await giteaService.replaceLabels(owner, repo, giteaIssue.number, labels);
|
||||||
@@ -214,28 +214,28 @@ async function handleJiraHook(payload) {
|
|||||||
//通过评论记录来源链接
|
//通过评论记录来源链接
|
||||||
const jiraIssueUrl = `${config.jira.baseUrl}/browse/${issue.key}`;
|
const jiraIssueUrl = `${config.jira.baseUrl}/browse/${issue.key}`;
|
||||||
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
||||||
// 使用repoInfo中的owner和repo,确保使用当前配置的仓库名
|
//使用repoInfo中的owner和repo,确保使用当前配置的仓库名
|
||||||
const giteaIssueUrl = `${giteaWebUrl}/${owner}/${repo}/issues/${giteaIssue.number}`;
|
const giteaIssueUrl = `${giteaWebUrl}/${owner}/${repo}/issues/${giteaIssue.number}`;
|
||||||
|
|
||||||
const successMsg = isResyncCommand
|
const successMsg = isResyncCommand
|
||||||
? `手动同步:已补建Gitea工单 [#${giteaIssue.number}](${giteaIssueUrl})`
|
? `手动同步:已补建Gitea工单 [#${giteaIssue.number}](${giteaIssueUrl})`
|
||||||
: `Gitea来源: ${giteaIssueUrl}\n由工单机器人创建`;
|
: `Gitea来源: ${giteaIssueUrl}\n由工单机器人创建`;
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
//在Gitea工单上添加评论,格式与Gitea->Jira一致
|
//在Gitea工单上添加评论,格式与Gitea->Jira一致
|
||||||
giteaService.addComment(owner, repo, giteaIssue.number,
|
giteaService.addComment(owner, repo, giteaIssue.number,
|
||||||
`已由工单机器人同步至Jira:[${issue.key}](${jiraIssueUrl})`
|
`已由工单机器人同步至Jira:[${issue.key}](${jiraIssueUrl})`
|
||||||
),
|
),
|
||||||
//在Jira工单上添加评论
|
//在Jira工单上添加评论
|
||||||
jiraService.addComment(issue.key, successMsg)
|
jiraService.addComment(issue.key, successMsg)
|
||||||
]).catch(err => logger.error('Comment write-back failed', err.message));
|
]).catch(err => logger.error('Comment write-back failed', err.message));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${repoInfo.repoKey}] [JIRA->GITEA] Failed to create issue: ${error.message}`);
|
logger.error(`[${repoInfo.repoKey}] [JIRA->GITEA] Failed to create issue: ${error.message}`);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//其他事件需要已存在的映射关系
|
//其他事件需要已存在的映射关系
|
||||||
if (!mapping) {
|
if (!mapping) {
|
||||||
logger.info(`[JIRA->GITEA] No mapping found for ${issue.key}, skipping non-create/non-resync event`);
|
logger.info(`[JIRA->GITEA] No mapping found for ${issue.key}, skipping non-create/non-resync event`);
|
||||||
@@ -251,36 +251,36 @@ async function handleJiraHook(payload) {
|
|||||||
logger.warn(`[${mapping.repo_key}] [JIRA->GITEA] Repository not configured`);
|
logger.warn(`[${mapping.repo_key}] [JIRA->GITEA] Repository not configured`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//处理工单更新(状态/标题/描述/优先级/类型/迭代/经办人)
|
//处理工单更新(状态/标题/描述/优先级/类型/迭代/经办人)
|
||||||
if ((webhookEvent === 'jira:issue_updated' && changelog) || isResyncCommand) {
|
if ((webhookEvent === 'jira:issue_updated' && changelog) || isResyncCommand) {
|
||||||
logger.info(`[JIRA->GITEA] Entering update logic for ${issue.key}, isResync: ${isResyncCommand}`);
|
logger.info(`[JIRA->GITEA] Entering update logic for ${issue.key}, isResync: ${isResyncCommand}`);
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
const changes = isResyncCommand ? [] : (changelog.items || []);
|
const changes = isResyncCommand ? [] : (changelog.items || []);
|
||||||
let needsUpdate = isResyncCommand; // resync时强制更新
|
let needsUpdate = isResyncCommand; //resync时强制更新
|
||||||
let needsLabelUpdate = isResyncCommand; // resync时强制更新标签
|
let needsLabelUpdate = isResyncCommand; //resync时强制更新标签
|
||||||
let needsMilestoneUpdate = isResyncCommand; // resync时强制更新里程碑
|
let needsMilestoneUpdate = isResyncCommand; //resync时强制更新里程碑
|
||||||
|
|
||||||
//获取当前Gitea工单详情以处理标签
|
//获取当前Gitea工单详情以处理标签
|
||||||
let currentGiteaIssue = null;
|
let currentGiteaIssue = null;
|
||||||
|
|
||||||
//检查是否配置了状态流转映射
|
//检查是否配置了状态流转映射
|
||||||
const hasTransitions = repoConfig.transitions && (repoConfig.transitions.close || repoConfig.transitions.reopen);
|
const hasTransitions = repoConfig.transitions && (repoConfig.transitions.close || repoConfig.transitions.reopen);
|
||||||
|
|
||||||
//如果是resync命令,强制同步所有字段
|
//如果是resync命令,强制同步所有字段
|
||||||
if (isResyncCommand) {
|
if (isResyncCommand) {
|
||||||
//同步标题
|
//同步标题
|
||||||
if (issue.fields.summary) {
|
if (issue.fields.summary) {
|
||||||
updateData.title = issue.fields.summary;
|
updateData.title = issue.fields.summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
//同步描述
|
//同步描述
|
||||||
if (issue.fields.description) {
|
if (issue.fields.description) {
|
||||||
updateData.body = j2m.toM(issue.fields.description);
|
updateData.body = j2m.toM(issue.fields.description);
|
||||||
} else {
|
} else {
|
||||||
updateData.body = '';
|
updateData.body = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
//同步状态(仅在配置了状态流转时)
|
//同步状态(仅在配置了状态流转时)
|
||||||
if (hasTransitions) {
|
if (hasTransitions) {
|
||||||
const statusCategory = issue.fields.status?.statusCategory?.key;
|
const statusCategory = issue.fields.status?.statusCategory?.key;
|
||||||
@@ -290,7 +290,7 @@ async function handleJiraHook(payload) {
|
|||||||
updateData.state = 'open';
|
updateData.state = 'open';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//同步经办人
|
//同步经办人
|
||||||
if (issue.fields.assignee) {
|
if (issue.fields.assignee) {
|
||||||
updateData.assignees = [issue.fields.assignee.name];
|
updateData.assignees = [issue.fields.assignee.name];
|
||||||
@@ -357,18 +357,18 @@ async function handleJiraHook(payload) {
|
|||||||
const priorityLabels = Object.keys(repoConfig.priorities);
|
const priorityLabels = Object.keys(repoConfig.priorities);
|
||||||
const typeLabels = Object.keys(repoConfig.types);
|
const typeLabels = Object.keys(repoConfig.types);
|
||||||
const mappedLabels = [...priorityLabels, ...typeLabels];
|
const mappedLabels = [...priorityLabels, ...typeLabels];
|
||||||
|
|
||||||
//保留非映射标签
|
//保留非映射标签
|
||||||
let newLabels = currentGiteaIssue.labels
|
let newLabels = currentGiteaIssue.labels
|
||||||
.map(l => l.name)
|
.map(l => l.name)
|
||||||
.filter(name => !mappedLabels.includes(name));
|
.filter(name => !mappedLabels.includes(name));
|
||||||
|
|
||||||
//添加新的优先级标签
|
//添加新的优先级标签
|
||||||
if (issue.fields.priority) {
|
if (issue.fields.priority) {
|
||||||
const priorityLabel = findGiteaLabel(issue.fields.priority.id, repoConfig.priorities);
|
const priorityLabel = findGiteaLabel(issue.fields.priority.id, repoConfig.priorities);
|
||||||
if (priorityLabel) newLabels.push(priorityLabel);
|
if (priorityLabel) newLabels.push(priorityLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
//添加新的类型标签:优先从标题中提取[类型名]
|
//添加新的类型标签:优先从标题中提取[类型名]
|
||||||
let typeLabel = null;
|
let typeLabel = null;
|
||||||
const titleType = extractTypeFromTitle(issue.fields.summary);
|
const titleType = extractTypeFromTitle(issue.fields.summary);
|
||||||
@@ -387,7 +387,7 @@ async function handleJiraHook(payload) {
|
|||||||
} else if (issue.fields.issuetype) {
|
} else if (issue.fields.issuetype) {
|
||||||
logger.info(`[${mapping.repo_key}] [JIRA->GITEA] Type ${issue.fields.issuetype.id} not configured, skipping type label`);
|
logger.info(`[${mapping.repo_key}] [JIRA->GITEA] Type ${issue.fields.issuetype.id} not configured, skipping type label`);
|
||||||
}
|
}
|
||||||
|
|
||||||
//使用专门的标签API替换标签
|
//使用专门的标签API替换标签
|
||||||
await giteaService.replaceLabels(owner, repo, giteaId, newLabels);
|
await giteaService.replaceLabels(owner, repo, giteaId, newLabels);
|
||||||
}
|
}
|
||||||
@@ -402,13 +402,13 @@ async function handleJiraHook(payload) {
|
|||||||
//获取Sprint字段值
|
//获取Sprint字段值
|
||||||
const sprintField = repoConfig.jira.sprintField || 'customfield_10105';
|
const sprintField = repoConfig.jira.sprintField || 'customfield_10105';
|
||||||
const sprintData = issue.fields[sprintField];
|
const sprintData = issue.fields[sprintField];
|
||||||
|
|
||||||
if (sprintData) {
|
if (sprintData) {
|
||||||
const sprintId = parseSprintId(sprintData);
|
const sprintId = parseSprintId(sprintData);
|
||||||
|
|
||||||
if (sprintId) {
|
if (sprintId) {
|
||||||
const milestoneName = findGiteaMilestone(sprintId, repoConfig.sprints);
|
const milestoneName = findGiteaMilestone(sprintId, repoConfig.sprints);
|
||||||
|
|
||||||
if (milestoneName) {
|
if (milestoneName) {
|
||||||
const milestones = await giteaService.getMilestones(owner, repo);
|
const milestones = await giteaService.getMilestones(owner, repo);
|
||||||
const milestone = milestones.find(m => m.title === milestoneName);
|
const milestone = milestones.find(m => m.title === milestoneName);
|
||||||
@@ -432,12 +432,12 @@ async function handleJiraHook(payload) {
|
|||||||
if (needsUpdate) {
|
if (needsUpdate) {
|
||||||
await giteaService.updateIssue(owner, repo, giteaId, updateData);
|
await giteaService.updateIssue(owner, repo, giteaId, updateData);
|
||||||
logger.sync(`[${mapping.repo_key}] [JIRA->GITEA] Updated #${giteaId}`);
|
logger.sync(`[${mapping.repo_key}] [JIRA->GITEA] Updated #${giteaId}`);
|
||||||
|
|
||||||
//如果是resync命令,添加反馈评论
|
//如果是resync命令,添加反馈评论
|
||||||
if (isResyncCommand) {
|
if (isResyncCommand) {
|
||||||
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
||||||
const giteaIssueUrl = `${giteaWebUrl}/${owner}/${repo}/issues/${giteaId}`;
|
const giteaIssueUrl = `${giteaWebUrl}/${owner}/${repo}/issues/${giteaId}`;
|
||||||
await jiraService.addComment(issue.key,
|
await jiraService.addComment(issue.key,
|
||||||
`手动同步:工单已存在,已更新 [Gitea #${giteaId}](${giteaIssueUrl})`
|
`手动同步:工单已存在,已更新 [Gitea #${giteaId}](${giteaIssueUrl})`
|
||||||
).catch(err => logger.error('Comment write-back failed', err.message));
|
).catch(err => logger.error('Comment write-back failed', err.message));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const logger = require('../utils/logger');
|
|||||||
const config = require('../config/env');
|
const config = require('../config/env');
|
||||||
const { checkCircuitBreaker } = require('../utils/circuitBreaker');
|
const { checkCircuitBreaker } = require('../utils/circuitBreaker');
|
||||||
|
|
||||||
// 处理Gitea Issue事件的主逻辑
|
//处理Gitea Issue事件的主逻辑
|
||||||
const processingIds = new Set();
|
const processingIds = new Set();
|
||||||
const LOCK_TIMEOUT = 10000;
|
const LOCK_TIMEOUT = 10000;
|
||||||
const RETRY_DELAY = 1500;
|
const RETRY_DELAY = 1500;
|
||||||
@@ -31,13 +31,13 @@ function isGiteaBot(sender) {
|
|||||||
|
|
||||||
async function handleIssueEvent(payload, retryCount = 0) {
|
async function handleIssueEvent(payload, retryCount = 0) {
|
||||||
const { action, issue, repository, comment, sender } = payload;
|
const { action, issue, repository, comment, sender } = payload;
|
||||||
|
|
||||||
//如果操作者是机器人,直接忽略
|
//如果操作者是机器人,直接忽略
|
||||||
if (isGiteaBot(sender)) {
|
if (isGiteaBot(sender)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证payload完整性
|
//验证payload完整性
|
||||||
if (!issue || !issue.number || !repository) {
|
if (!issue || !issue.number || !repository) {
|
||||||
logger.error('Invalid payload: missing required fields');
|
logger.error('Invalid payload: missing required fields');
|
||||||
return;
|
return;
|
||||||
@@ -47,10 +47,10 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
if (!checkCircuitBreaker()) {
|
if (!checkCircuitBreaker()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//构建仓库标识 (owner/repo格式)
|
//构建仓库标识 (owner/repo格式)
|
||||||
const repoKey = `${repository.owner.username}/${repository.name}`;
|
const repoKey = `${repository.owner.username}/${repository.name}`;
|
||||||
|
|
||||||
//获取该仓库的配置
|
//获取该仓库的配置
|
||||||
const repoConfig = getRepoConfig(repoKey);
|
const repoConfig = getRepoConfig(repoKey);
|
||||||
if (!repoConfig) {
|
if (!repoConfig) {
|
||||||
@@ -58,7 +58,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
logger.error(errorMsg);
|
logger.error(errorMsg);
|
||||||
throw new Error(errorMsg);
|
throw new Error(errorMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
//检测是否为/resync
|
//检测是否为/resync
|
||||||
const isResyncCommand = (action === 'created' && comment && comment.body.trim() === '/resync');
|
const isResyncCommand = (action === 'created' && comment && comment.body.trim() === '/resync');
|
||||||
const giteaId = issue.number;
|
const giteaId = issue.number;
|
||||||
@@ -101,7 +101,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
|
|
||||||
//只有已存在的工单才能进行更新/关闭/重开操作
|
//只有已存在的工单才能进行更新/关闭/重开操作
|
||||||
if (mapping) {
|
if (mapping) {
|
||||||
|
|
||||||
//处理关闭事件
|
//处理关闭事件
|
||||||
if (action === 'closed') {
|
if (action === 'closed') {
|
||||||
if (transitions && transitions.close) {
|
if (transitions && transitions.close) {
|
||||||
@@ -132,14 +132,14 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
repository.name,
|
repository.name,
|
||||||
giteaId
|
giteaId
|
||||||
);
|
);
|
||||||
|
|
||||||
let assigneeToSync = null;
|
let assigneeToSync = null;
|
||||||
if (fullIssue.assignees && fullIssue.assignees.length > 0) {
|
if (fullIssue.assignees && fullIssue.assignees.length > 0) {
|
||||||
assigneeToSync = fullIssue.assignees[0];
|
assigneeToSync = fullIssue.assignees[0];
|
||||||
} else if (fullIssue.assignee) {
|
} else if (fullIssue.assignee) {
|
||||||
assigneeToSync = fullIssue.assignee;
|
assigneeToSync = fullIssue.assignee;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assigneeToSync) {
|
if (assigneeToSync) {
|
||||||
const jiraUser = await jiraService.findUser(assigneeToSync.username);
|
const jiraUser = await jiraService.findUser(assigneeToSync.username);
|
||||||
if (jiraUser) {
|
if (jiraUser) {
|
||||||
@@ -147,7 +147,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
await jiraService.updateIssue(mapping.jira_key, { assignee: { name: jiraUser.name } });
|
await jiraService.updateIssue(mapping.jira_key, { assignee: { name: jiraUser.name } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transitions && transitions.in_progress) {
|
if (transitions && transitions.in_progress) {
|
||||||
await jiraService.transitionIssue(mapping.jira_key, transitions.in_progress);
|
await jiraService.transitionIssue(mapping.jira_key, transitions.in_progress);
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
repository.name,
|
repository.name,
|
||||||
giteaId
|
giteaId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fullIssue.assignees && fullIssue.assignees.length > 0) {
|
if (fullIssue.assignees && fullIssue.assignees.length > 0) {
|
||||||
const firstAssignee = fullIssue.assignees[0];
|
const firstAssignee = fullIssue.assignees[0];
|
||||||
const jiraUser = await jiraService.findUser(firstAssignee.username);
|
const jiraUser = await jiraService.findUser(firstAssignee.username);
|
||||||
@@ -190,12 +190,12 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
issue.milestone,
|
issue.milestone,
|
||||||
repoConfig
|
repoConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
//已有映射的工单,即使类型未配置也允许同步(只是不更新类型字段)
|
//已有映射的工单,即使类型未配置也允许同步(只是不更新类型字段)
|
||||||
//这样当类型从未配置变为已配置时,也能正常同步
|
//这样当类型从未配置变为已配置时,也能正常同步
|
||||||
if (jiraFields) {
|
if (jiraFields) {
|
||||||
// 处理指派人同步(resync 时)
|
//处理指派人同步(resync 时)
|
||||||
// Gitea 支持多个指派人,Jira 只支持一个经办人,取第一个
|
//Gitea 支持多个指派人,Jira 只支持一个经办人,取第一个
|
||||||
if (isResyncCommand) {
|
if (isResyncCommand) {
|
||||||
if (issue.assignees && issue.assignees.length > 0) {
|
if (issue.assignees && issue.assignees.length > 0) {
|
||||||
const firstAssignee = issue.assignees[0];
|
const firstAssignee = issue.assignees[0];
|
||||||
@@ -230,9 +230,9 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
//只有opened事件和resync命令可以创建新工单
|
//只有opened事件和resync命令可以创建新工单
|
||||||
if (!isResyncCommand && action !== 'opened') {
|
if (!isResyncCommand && action !== 'opened') {
|
||||||
logger.info(`[${repoKey}] [GITEA->JIRA] Skipped #${giteaId}: no mapping exists and action is '${action}' (only 'opened' or resync can create)`);
|
logger.info(`[${repoKey}] [GITEA->JIRA] Skipped #${giteaId}: no mapping exists and action is '${action}' (only 'opened' or resync can create)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const jiraFields = buildJiraFields(
|
const jiraFields = buildJiraFields(
|
||||||
issue.title,
|
issue.title,
|
||||||
issue.body,
|
issue.body,
|
||||||
@@ -240,7 +240,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
issue.milestone,
|
issue.milestone,
|
||||||
repoConfig
|
repoConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
//类型未配置,跳过同步
|
//类型未配置,跳过同步
|
||||||
if (!jiraFields) {
|
if (!jiraFields) {
|
||||||
logger.info(`[${repoKey}] [GITEA->JIRA] Skipped #${giteaId}: type not configured in mappings`);
|
logger.info(`[${repoKey}] [GITEA->JIRA] Skipped #${giteaId}: type not configured in mappings`);
|
||||||
@@ -258,9 +258,9 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
jiraFields.assignee = { name: jiraUser.name };
|
jiraFields.assignee = { name: jiraUser.name };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newIssue = await jiraService.createIssue(jiraFields);
|
const newIssue = await jiraService.createIssue(jiraFields);
|
||||||
|
|
||||||
dbMap.saveMapping(repoKey, giteaId, newIssue.key, newIssue.id);
|
dbMap.saveMapping(repoKey, giteaId, newIssue.key, newIssue.id);
|
||||||
logger.sync(`[${repoKey}] [GITEA->JIRA] Created ${newIssue.key} from #${giteaId}`);
|
logger.sync(`[${repoKey}] [GITEA->JIRA] Created ${newIssue.key} from #${giteaId}`);
|
||||||
|
|
||||||
@@ -269,11 +269,11 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
await jiraService.transitionIssue(newIssue.key, transitions.in_progress)
|
await jiraService.transitionIssue(newIssue.key, transitions.in_progress)
|
||||||
.catch(e => logger.error('Initial transition failed', e.message));
|
.catch(e => logger.error('Initial transition failed', e.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
||||||
const giteaIssueUrl = `${giteaWebUrl}/${repository.owner.username}/${repository.name}/issues/${giteaId}`;
|
const giteaIssueUrl = `${giteaWebUrl}/${repository.owner.username}/${repository.name}/issues/${giteaId}`;
|
||||||
|
|
||||||
const successMsg = isResyncCommand
|
const successMsg = isResyncCommand
|
||||||
? `手动同步:已补建Jira工单 [${newIssue.key}](${config.jira.baseUrl}/browse/${newIssue.key})`
|
? `手动同步:已补建Jira工单 [${newIssue.key}](${config.jira.baseUrl}/browse/${newIssue.key})`
|
||||||
: `Jira来源:[${newIssue.key}](${config.jira.baseUrl}/browse/${newIssue.key})\n由工单机器人创建`;
|
: `Jira来源:[${newIssue.key}](${config.jira.baseUrl}/browse/${newIssue.key})\n由工单机器人创建`;
|
||||||
|
|
||||||
@@ -290,12 +290,12 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${repoKey}] [GITEA->JIRA] Failed to sync #${giteaId}: ${error.message}`);
|
logger.error(`[${repoKey}] [GITEA->JIRA] Failed to sync #${giteaId}: ${error.message}`);
|
||||||
|
|
||||||
if (isResyncCommand) {
|
if (isResyncCommand) {
|
||||||
await giteaService.addComment(
|
await giteaService.addComment(
|
||||||
repository.owner.username,
|
repository.owner.username,
|
||||||
repository.name,
|
repository.name,
|
||||||
giteaId,
|
giteaId,
|
||||||
`同步失败: ${error.message}`
|
`同步失败: ${error.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ editor.get('/status', (c) => {
|
|||||||
let todaySyncs = 0;
|
let todaySyncs = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
let fatalCount = 0;
|
let fatalCount = 0;
|
||||||
|
|
||||||
if (fs.existsSync(logFile)) {
|
if (fs.existsSync(logFile)) {
|
||||||
const content = fs.readFileSync(logFile, 'utf8');
|
const content = fs.readFileSync(logFile, 'utf8');
|
||||||
const createdMatches = content.match(/Created/g);
|
const createdMatches = content.match(/Created/g);
|
||||||
@@ -41,7 +41,7 @@ editor.get('/status', (c) => {
|
|||||||
const fatalMatches = content.match(/\[FATAL\]/g);
|
const fatalMatches = content.match(/\[FATAL\]/g);
|
||||||
fatalCount = fatalMatches ? fatalMatches.length : 0;
|
fatalCount = fatalMatches ? fatalMatches.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
status: 'running',
|
status: 'running',
|
||||||
@@ -61,22 +61,22 @@ editor.get('/status', (c) => {
|
|||||||
editor.get('/history', (c) => {
|
editor.get('/history', (c) => {
|
||||||
try {
|
try {
|
||||||
const history = [];
|
const history = [];
|
||||||
|
|
||||||
//读取最近7天的日志文件
|
//读取最近7天的日志文件
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setDate(date.getDate() - i);
|
date.setDate(date.getDate() - i);
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
const dateStr = date.toISOString().split('T')[0];
|
||||||
const logFile = path.join(LOGS_DIR, `sync-${dateStr}.log`);
|
const logFile = path.join(LOGS_DIR, `sync-${dateStr}.log`);
|
||||||
|
|
||||||
if (fs.existsSync(logFile)) {
|
if (fs.existsSync(logFile)) {
|
||||||
const content = fs.readFileSync(logFile, 'utf8');
|
const content = fs.readFileSync(logFile, 'utf8');
|
||||||
|
|
||||||
//统计各项指标
|
//统计各项指标
|
||||||
const createdMatches = content.match(/Created/g);
|
const createdMatches = content.match(/Created/g);
|
||||||
const errorMatches = content.match(/\[ERROR\]/g);
|
const errorMatches = content.match(/\[ERROR\]/g);
|
||||||
const fatalMatches = content.match(/\[FATAL\]/g);
|
const fatalMatches = content.match(/\[FATAL\]/g);
|
||||||
|
|
||||||
history.push({
|
history.push({
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
syncs: createdMatches ? createdMatches.length : 0,
|
syncs: createdMatches ? createdMatches.length : 0,
|
||||||
@@ -92,7 +92,7 @@ editor.get('/history', (c) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
history: history.reverse() //从旧到新排序
|
history: history.reverse() //从旧到新排序
|
||||||
@@ -109,7 +109,7 @@ editor.get('/logs', (c) => {
|
|||||||
//获取今天的日志文件
|
//获取今天的日志文件
|
||||||
const today = new Date().toISOString().split('T')[0]; //YYYY-MM-DD
|
const today = new Date().toISOString().split('T')[0]; //YYYY-MM-DD
|
||||||
const logFile = path.join(LOGS_DIR, `sync-${today}.log`);
|
const logFile = path.join(LOGS_DIR, `sync-${today}.log`);
|
||||||
|
|
||||||
if (!fs.existsSync(logFile)) {
|
if (!fs.existsSync(logFile)) {
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -117,12 +117,12 @@ editor.get('/logs', (c) => {
|
|||||||
logs: ['[INFO] 今日暂无日志记录']
|
logs: ['[INFO] 今日暂无日志记录']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//读取日志文件(最后1000行)
|
//读取日志文件(最后1000行)
|
||||||
const content = fs.readFileSync(logFile, 'utf8');
|
const content = fs.readFileSync(logFile, 'utf8');
|
||||||
const lines = content.split('\n').filter(line => line.trim());
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
const recentLogs = lines.slice(-1000); //只返回最后1000行
|
const recentLogs = lines.slice(-1000); //只返回最后1000行
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
filename: `sync-${today}.log`,
|
filename: `sync-${today}.log`,
|
||||||
@@ -139,12 +139,12 @@ editor.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`);
|
||||||
|
|
||||||
if (fs.existsSync(logFile)) {
|
if (fs.existsSync(logFile)) {
|
||||||
fs.writeFileSync(logFile, '', 'utf8');
|
fs.writeFileSync(logFile, '', 'utf8');
|
||||||
logger.info('[Editor] Logs cleared');
|
logger.info('[Editor] Logs cleared');
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ success: true, message: '日志已清空' });
|
return c.json({ success: true, message: '日志已清空' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('[Editor] Clear logs error:', e.message);
|
logger.error('[Editor] Clear logs error:', e.message);
|
||||||
@@ -156,9 +156,9 @@ editor.post('/logs/clear', (c) => {
|
|||||||
editor.post('/control', async (c) => {
|
editor.post('/control', async (c) => {
|
||||||
try {
|
try {
|
||||||
const { action } = await c.req.json();
|
const { action } = await c.req.json();
|
||||||
|
|
||||||
logger.info(`[Editor] Control action received: ${action}`);
|
logger.info(`[Editor] Control action received: ${action}`);
|
||||||
|
|
||||||
//注意:实际的重启需要外部进程管理器(如 PM2)
|
//注意:实际的重启需要外部进程管理器(如 PM2)
|
||||||
//这里只是记录日志
|
//这里只是记录日志
|
||||||
if (action === 'restart') {
|
if (action === 'restart') {
|
||||||
@@ -168,7 +168,7 @@ editor.post('/control', async (c) => {
|
|||||||
message: '重启请求已记录,请使用 PM2 或其他进程管理器执行重启'
|
message: '重启请求已记录,请使用 PM2 或其他进程管理器执行重启'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: '不支持的操作'
|
error: '不支持的操作'
|
||||||
@@ -183,14 +183,14 @@ editor.post('/control', async (c) => {
|
|||||||
editor.get('/env', (c) => {
|
editor.get('/env', (c) => {
|
||||||
try {
|
try {
|
||||||
const envPath = path.join(__dirname, '../../.env');
|
const envPath = path.join(__dirname, '../../.env');
|
||||||
|
|
||||||
if (!fs.existsSync(envPath)) {
|
if (!fs.existsSync(envPath)) {
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
content: '# 环境变量配置文件\n# 请根据需要配置以下变量\n'
|
content: '# 环境变量配置文件\n# 请根据需要配置以下变量\n'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(envPath, 'utf8');
|
const content = fs.readFileSync(envPath, 'utf8');
|
||||||
return c.json({ success: true, content });
|
return c.json({ success: true, content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -204,18 +204,18 @@ editor.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');
|
||||||
|
|
||||||
//备份现有文件
|
//备份现有文件
|
||||||
if (fs.existsSync(envPath)) {
|
if (fs.existsSync(envPath)) {
|
||||||
const backupPath = path.join(__dirname, '../../.env.backup');
|
const backupPath = path.join(__dirname, '../../.env.backup');
|
||||||
fs.copyFileSync(envPath, backupPath);
|
fs.copyFileSync(envPath, backupPath);
|
||||||
logger.info('[Editor] .env file backed up');
|
logger.info('[Editor] .env file backed up');
|
||||||
}
|
}
|
||||||
|
|
||||||
//写入新内容
|
//写入新内容
|
||||||
fs.writeFileSync(envPath, content, 'utf8');
|
fs.writeFileSync(envPath, content, 'utf8');
|
||||||
logger.info('[Editor] .env file updated');
|
logger.info('[Editor] .env file updated');
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '配置已保存,重启服务后生效'
|
message: '配置已保存,重启服务后生效'
|
||||||
@@ -229,13 +229,13 @@ editor.post('/env', async (c) => {
|
|||||||
editor.get('/guide', (c) => {
|
editor.get('/guide', (c) => {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(README_PATH)) {
|
if (!fs.existsSync(README_PATH)) {
|
||||||
return c.json({
|
return c.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'how-to-use.md not found',
|
error: 'how-to-use.md not found',
|
||||||
content: '# 使用指南\n\n使用指南文件不存在'
|
content: '# 使用指南\n\n使用指南文件不存在'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(README_PATH, 'utf8');
|
const content = fs.readFileSync(README_PATH, 'utf8');
|
||||||
return c.json({ success: true, content });
|
return c.json({ success: true, content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -250,10 +250,10 @@ editor.get('/mappings', (c) => {
|
|||||||
if (!fs.existsSync(MAPPINGS_PATH)) {
|
if (!fs.existsSync(MAPPINGS_PATH)) {
|
||||||
return c.json({ success: true, data: { repositories: {} } });
|
return c.json({ success: true, data: { repositories: {} } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(MAPPINGS_PATH, 'utf8');
|
const content = fs.readFileSync(MAPPINGS_PATH, 'utf8');
|
||||||
const config = JSON.parse(content);
|
const config = JSON.parse(content);
|
||||||
|
|
||||||
return c.json({ success: true, data: config });
|
return c.json({ success: true, data: config });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('[Editor] Read mappings error:', e.message);
|
logger.error('[Editor] Read mappings error:', e.message);
|
||||||
@@ -265,24 +265,24 @@ editor.get('/mappings', (c) => {
|
|||||||
editor.post('/mappings', async (c) => {
|
editor.post('/mappings', async (c) => {
|
||||||
try {
|
try {
|
||||||
const { repoName, config } = await c.req.json();
|
const { repoName, config } = await c.req.json();
|
||||||
|
|
||||||
let fullConfig = { repositories: {} };
|
let fullConfig = { repositories: {} };
|
||||||
|
|
||||||
//读取现有配置
|
//读取现有配置
|
||||||
if (fs.existsSync(MAPPINGS_PATH)) {
|
if (fs.existsSync(MAPPINGS_PATH)) {
|
||||||
const content = fs.readFileSync(MAPPINGS_PATH, 'utf8');
|
const content = fs.readFileSync(MAPPINGS_PATH, 'utf8');
|
||||||
fullConfig = JSON.parse(content);
|
fullConfig = JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
//确保结构存在
|
//确保结构存在
|
||||||
if (!fullConfig.repositories) fullConfig.repositories = {};
|
if (!fullConfig.repositories) fullConfig.repositories = {};
|
||||||
|
|
||||||
//更新指定仓库的配置
|
//更新指定仓库的配置
|
||||||
fullConfig.repositories[repoName] = config;
|
fullConfig.repositories[repoName] = config;
|
||||||
|
|
||||||
//写回文件
|
//写回文件
|
||||||
fs.writeFileSync(MAPPINGS_PATH, JSON.stringify(fullConfig, null, 2), 'utf8');
|
fs.writeFileSync(MAPPINGS_PATH, JSON.stringify(fullConfig, null, 2), 'utf8');
|
||||||
|
|
||||||
logger.info(`[Editor] Updated configuration for ${repoName}`);
|
logger.info(`[Editor] Updated configuration for ${repoName}`);
|
||||||
return c.json({ success: true, message: `配置已保存到 mappings.json` });
|
return c.json({ success: true, message: `配置已保存到 mappings.json` });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -295,26 +295,26 @@ editor.post('/mappings', async (c) => {
|
|||||||
editor.delete('/mappings/:repoName', async (c) => {
|
editor.delete('/mappings/:repoName', async (c) => {
|
||||||
try {
|
try {
|
||||||
const repoName = decodeURIComponent(c.req.param('repoName'));
|
const repoName = decodeURIComponent(c.req.param('repoName'));
|
||||||
|
|
||||||
if (!fs.existsSync(MAPPINGS_PATH)) {
|
if (!fs.existsSync(MAPPINGS_PATH)) {
|
||||||
return c.json({ success: false, error: '配置文件不存在' }, 404);
|
return c.json({ success: false, error: '配置文件不存在' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
//读取现有配置
|
//读取现有配置
|
||||||
const content = fs.readFileSync(MAPPINGS_PATH, 'utf8');
|
const content = fs.readFileSync(MAPPINGS_PATH, 'utf8');
|
||||||
const fullConfig = JSON.parse(content);
|
const fullConfig = JSON.parse(content);
|
||||||
|
|
||||||
//检查仓库是否存在
|
//检查仓库是否存在
|
||||||
if (!fullConfig.repositories || !fullConfig.repositories[repoName]) {
|
if (!fullConfig.repositories || !fullConfig.repositories[repoName]) {
|
||||||
return c.json({ success: false, error: '仓库配置不存在' }, 404);
|
return c.json({ success: false, error: '仓库配置不存在' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
//删除指定仓库
|
//删除指定仓库
|
||||||
delete fullConfig.repositories[repoName];
|
delete fullConfig.repositories[repoName];
|
||||||
|
|
||||||
//写回文件
|
//写回文件
|
||||||
fs.writeFileSync(MAPPINGS_PATH, JSON.stringify(fullConfig, null, 2), 'utf8');
|
fs.writeFileSync(MAPPINGS_PATH, JSON.stringify(fullConfig, null, 2), 'utf8');
|
||||||
|
|
||||||
logger.info(`[Editor] Deleted configuration for ${repoName}`);
|
logger.info(`[Editor] Deleted configuration for ${repoName}`);
|
||||||
return c.json({ success: true, message: `仓库配置已删除` });
|
return c.json({ success: true, message: `仓库配置已删除` });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -327,38 +327,38 @@ editor.delete('/mappings/:repoName', async (c) => {
|
|||||||
editor.post('/mappings/rename', async (c) => {
|
editor.post('/mappings/rename', async (c) => {
|
||||||
try {
|
try {
|
||||||
const { oldName, newName } = await c.req.json();
|
const { oldName, newName } = await c.req.json();
|
||||||
|
|
||||||
if (!oldName || !newName) {
|
if (!oldName || !newName) {
|
||||||
return c.json({ success: false, error: '缺少必要参数' }, 400);
|
return c.json({ success: false, error: '缺少必要参数' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(MAPPINGS_PATH)) {
|
if (!fs.existsSync(MAPPINGS_PATH)) {
|
||||||
return c.json({ success: false, error: '配置文件不存在' }, 404);
|
return c.json({ success: false, error: '配置文件不存在' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
//读取现有配置
|
//读取现有配置
|
||||||
const content = fs.readFileSync(MAPPINGS_PATH, 'utf8');
|
const content = fs.readFileSync(MAPPINGS_PATH, 'utf8');
|
||||||
const fullConfig = JSON.parse(content);
|
const fullConfig = JSON.parse(content);
|
||||||
|
|
||||||
//检查旧名称是否存在
|
//检查旧名称是否存在
|
||||||
if (!fullConfig.repositories || !fullConfig.repositories[oldName]) {
|
if (!fullConfig.repositories || !fullConfig.repositories[oldName]) {
|
||||||
return c.json({ success: false, error: '源仓库配置不存在' }, 404);
|
return c.json({ success: false, error: '源仓库配置不存在' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
//检查新名称是否已存在
|
//检查新名称是否已存在
|
||||||
if (fullConfig.repositories[newName]) {
|
if (fullConfig.repositories[newName]) {
|
||||||
return c.json({ success: false, error: '目标仓库名称已存在' }, 400);
|
return c.json({ success: false, error: '目标仓库名称已存在' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
//复制配置到新名称
|
//复制配置到新名称
|
||||||
fullConfig.repositories[newName] = fullConfig.repositories[oldName];
|
fullConfig.repositories[newName] = fullConfig.repositories[oldName];
|
||||||
|
|
||||||
//删除旧名称
|
//删除旧名称
|
||||||
delete fullConfig.repositories[oldName];
|
delete fullConfig.repositories[oldName];
|
||||||
|
|
||||||
//写回文件
|
//写回文件
|
||||||
fs.writeFileSync(MAPPINGS_PATH, JSON.stringify(fullConfig, null, 2), 'utf8');
|
fs.writeFileSync(MAPPINGS_PATH, JSON.stringify(fullConfig, null, 2), 'utf8');
|
||||||
|
|
||||||
logger.info(`[Editor] Renamed configuration from ${oldName} to ${newName}`);
|
logger.info(`[Editor] Renamed configuration from ${oldName} to ${newName}`);
|
||||||
return c.json({ success: true, message: `仓库配置已改名` });
|
return c.json({ success: true, message: `仓库配置已改名` });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -408,7 +408,7 @@ editor.post('/save', async (c) => {
|
|||||||
editor.post('/scan', async (c) => {
|
editor.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() : '';
|
||||||
|
|
||||||
//构造认证头
|
//构造认证头
|
||||||
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}`;
|
||||||
@@ -442,23 +442,23 @@ editor.post('/scan', async (c) => {
|
|||||||
//尝试获取流转 - 从不同状态的工单收集所有可能的流转
|
//尝试获取流转 - 从不同状态的工单收集所有可能的流转
|
||||||
const transitionsMap = new Map();
|
const transitionsMap = new Map();
|
||||||
let sampleIssues = [];
|
let sampleIssues = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//获取多个工单以覆盖不同状态
|
//获取多个工单以覆盖不同状态
|
||||||
const search = await client.get(`/rest/api/2/search?jql=project="${realKey}"&maxResults=20&fields=id,key,status`);
|
const search = await client.get(`/rest/api/2/search?jql=project="${realKey}"&maxResults=20&fields=id,key,status`);
|
||||||
if (search.data.issues?.length > 0) {
|
if (search.data.issues?.length > 0) {
|
||||||
sampleIssues = search.data.issues;
|
sampleIssues = search.data.issues;
|
||||||
result.sampleIssueKey = sampleIssues[0].key;
|
result.sampleIssueKey = sampleIssues[0].key;
|
||||||
|
|
||||||
//对每个工单获取其可用的transitions
|
//对每个工单获取其可用的transitions
|
||||||
const transPromises = sampleIssues.slice(0, 15).map(issue =>
|
const transPromises = sampleIssues.slice(0, 15).map(issue =>
|
||||||
client.get(`/rest/api/2/issue/${issue.key}/transitions`)
|
client.get(`/rest/api/2/issue/${issue.key}/transitions`)
|
||||||
.then(trans => trans.data.transitions)
|
.then(trans => trans.data.transitions)
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
);
|
);
|
||||||
|
|
||||||
const allTransitions = await Promise.all(transPromises);
|
const allTransitions = await Promise.all(transPromises);
|
||||||
|
|
||||||
//合并所有transitions并去重
|
//合并所有transitions并去重
|
||||||
allTransitions.flat().forEach(t => {
|
allTransitions.flat().forEach(t => {
|
||||||
if (!transitionsMap.has(t.id)) {
|
if (!transitionsMap.has(t.id)) {
|
||||||
@@ -466,7 +466,7 @@ editor.post('/scan', async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
result.transitions = Array.from(transitionsMap.values());
|
result.transitions = Array.from(transitionsMap.values());
|
||||||
|
|
||||||
if (result.transitions.length > 0) {
|
if (result.transitions.length > 0) {
|
||||||
result.warning = `已从 ${sampleIssues.length} 个工单中扫描到 ${result.transitions.length} 个状态流转。注意:Jira的流转取决于工单当前状态,未被扫描的必须手动配置。`;
|
result.warning = `已从 ${sampleIssues.length} 个工单中扫描到 ${result.transitions.length} 个状态流转。注意:Jira的流转取决于工单当前状态,未被扫描的必须手动配置。`;
|
||||||
}
|
}
|
||||||
@@ -487,11 +487,11 @@ editor.post('/scan-sprint', async (c) => {
|
|||||||
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}`;
|
||||||
else if (auth.username) headers['Authorization'] = `Basic ${Buffer.from(auth.username + ':' + auth.password).toString('base64')}`;
|
else if (auth.username) headers['Authorization'] = `Basic ${Buffer.from(auth.username + ':' + auth.password).toString('base64')}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = axios.create({ baseURL: baseUrl.replace(/\/$/, ''), headers, timeout: 10000 });
|
const client = axios.create({ baseURL: baseUrl.replace(/\/$/, ''), headers, timeout: 10000 });
|
||||||
const fields = (await client.get(`/rest/api/2/issue/${issueKey}`)).data.fields;
|
const fields = (await client.get(`/rest/api/2/issue/${issueKey}`)).data.fields;
|
||||||
|
|
||||||
let fieldId = null, sprints = [];
|
let fieldId = null, sprints = [];
|
||||||
for (const [k, v] of Object.entries(fields)) {
|
for (const [k, v] of Object.entries(fields)) {
|
||||||
if (Array.isArray(v) && v[0]?.toString().includes('com.atlassian.greenhopper.service.sprint.Sprint')) {
|
if (Array.isArray(v) && v[0]?.toString().includes('com.atlassian.greenhopper.service.sprint.Sprint')) {
|
||||||
@@ -512,7 +512,7 @@ editor.post('/scan-sprint', async (c) => {
|
|||||||
//代理 Jira API 请求
|
//代理 Jira API 请求
|
||||||
editor.post('/proxy-jira', async (c) => {
|
editor.post('/proxy-jira', async (c) => {
|
||||||
const { url, auth } = await c.req.json();
|
const { url, auth } = await c.req.json();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -521,13 +521,13 @@ editor.post('/proxy-jira', async (c) => {
|
|||||||
},
|
},
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json({ success: true, data: response.data });
|
return c.json({ success: true, data: response.data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('[Editor] Proxy Jira Error:', e.message);
|
logger.error('[Editor] Proxy Jira Error:', e.message);
|
||||||
return c.json({
|
return c.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: e.response?.data?.errorMessages?.[0] || e.message
|
error: e.response?.data?.errorMessages?.[0] || e.message
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ async function createIssue(fields) {
|
|||||||
try {
|
try {
|
||||||
const res = await jiraClient.post('/rest/api/2/issue', { fields });
|
const res = await jiraClient.post('/rest/api/2/issue', { fields });
|
||||||
logger.info(`Jira created issue: ${res.data.key}`);
|
logger.info(`Jira created issue: ${res.data.key}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Jira create issue failed', error.response?.data || error.message);
|
logger.error('Jira create issue failed', error.response?.data || error.message);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -31,7 +31,7 @@ async function updateIssue(key, fields) {
|
|||||||
logger.info(`Jira updated ${key}`);
|
logger.info(`Jira updated ${key}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Jira update issue failed (${key})`, error.response?.data || error.message);
|
logger.error(`Jira update issue failed (${key})`, error.response?.data || error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ async function transitionIssue(key, transitionId) {
|
|||||||
logger.info(`Jira transitioned ${key} to state ID ${transitionId}`);
|
logger.info(`Jira transitioned ${key} to state ID ${transitionId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Jira transition failed (${key})`, error.response?.data || error.message);
|
logger.error(`Jira transition failed (${key})`, error.response?.data || error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +63,15 @@ async function findUser(query) {
|
|||||||
if (!query) return null;
|
if (!query) return null;
|
||||||
try {
|
try {
|
||||||
const res = await jiraClient.get('/rest/api/2/user/search', {
|
const res = await jiraClient.get('/rest/api/2/user/search', {
|
||||||
params: {
|
params: {
|
||||||
username: query,
|
username: query,
|
||||||
maxResults: 10
|
maxResults: 10
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (res.data && res.data.length > 0) {
|
if (res.data && res.data.length > 0) {
|
||||||
const exactMatch = res.data.find(u =>
|
const exactMatch = res.data.find(u =>
|
||||||
u.name === query ||
|
u.name === query ||
|
||||||
u.key === query ||
|
u.key === query ||
|
||||||
u.emailAddress === query ||
|
u.emailAddress === query ||
|
||||||
u.displayName === query
|
u.displayName === query
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ function checkCircuitBreaker() {
|
|||||||
}
|
}
|
||||||
requestCount++;
|
requestCount++;
|
||||||
if (requestCount > config.app.maxRequests) {
|
if (requestCount > config.app.maxRequests) {
|
||||||
const msg = `Circuit breaker triggered: Exceeded ${config.app.maxRequests} requests in ${config.app.rate/1000}s. Exiting...`;
|
const msg = `Circuit breaker triggered: Exceeded ${config.app.maxRequests} requests in ${config.app.rate / 1000}s. Exiting...`;
|
||||||
|
|
||||||
//同步写入fatal日志
|
//同步写入fatal日志
|
||||||
logger.fatal("============================");
|
logger.fatal("============================");
|
||||||
logger.fatal(`${msg}`);
|
logger.fatal(`${msg}`);
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ function getTimestamp() {
|
|||||||
function getLogFileName() {
|
function getLogFileName() {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const utc8Time = new Date(date.getTime() + (8 * 60 * 60 * 1000));
|
const utc8Time = new Date(date.getTime() + (8 * 60 * 60 * 1000));
|
||||||
|
|
||||||
const year = utc8Time.getUTCFullYear();
|
const year = utc8Time.getUTCFullYear();
|
||||||
const month = String(utc8Time.getUTCMonth() + 1).padStart(2, '0');
|
const month = String(utc8Time.getUTCMonth() + 1).padStart(2, '0');
|
||||||
const day = String(utc8Time.getUTCDate()).padStart(2, '0');
|
const day = String(utc8Time.getUTCDate()).padStart(2, '0');
|
||||||
|
|
||||||
return `sync-${year}-${month}-${day}.log`;
|
return `sync-${year}-${month}-${day}.log`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +39,9 @@ function getLogFileName() {
|
|||||||
function writeLog(level, message, data = null) {
|
function writeLog(level, message, data = null) {
|
||||||
const timestamp = getTimestamp();
|
const timestamp = getTimestamp();
|
||||||
const logFile = path.join(LOG_DIR, getLogFileName());
|
const logFile = path.join(LOG_DIR, getLogFileName());
|
||||||
|
|
||||||
let logLine = `[${timestamp}] [${level}] ${message}`;
|
let logLine = `[${timestamp}] [${level}] ${message}`;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
if (typeof data === 'object') {
|
if (typeof data === 'object') {
|
||||||
logLine += ' ' + JSON.stringify(data);
|
logLine += ' ' + JSON.stringify(data);
|
||||||
@@ -49,9 +49,9 @@ function writeLog(level, message, data = null) {
|
|||||||
logLine += ' ' + data;
|
logLine += ' ' + data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logLine += '\n';
|
logLine += '\n';
|
||||||
|
|
||||||
//异步写入,不阻塞主流程
|
//异步写入,不阻塞主流程
|
||||||
fs.appendFile(logFile, logLine, (err) => {
|
fs.appendFile(logFile, logLine, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -64,9 +64,9 @@ function writeLog(level, message, data = null) {
|
|||||||
function writeLogSync(level, message, data = null) {
|
function writeLogSync(level, message, data = null) {
|
||||||
const timestamp = getTimestamp();
|
const timestamp = getTimestamp();
|
||||||
const logFile = path.join(LOG_DIR, getLogFileName());
|
const logFile = path.join(LOG_DIR, getLogFileName());
|
||||||
|
|
||||||
let logLine = `[${timestamp}] [${level}] ${message}`;
|
let logLine = `[${timestamp}] [${level}] ${message}`;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
if (typeof data === 'object') {
|
if (typeof data === 'object') {
|
||||||
logLine += ' ' + JSON.stringify(data);
|
logLine += ' ' + JSON.stringify(data);
|
||||||
@@ -74,9 +74,9 @@ function writeLogSync(level, message, data = null) {
|
|||||||
logLine += ' ' + data;
|
logLine += ' ' + data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logLine += '\n';
|
logLine += '\n';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.appendFileSync(logFile, logLine);
|
fs.appendFileSync(logFile, logLine);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -90,22 +90,22 @@ const logger = {
|
|||||||
console.log(`[INFO] ${message}`);
|
console.log(`[INFO] ${message}`);
|
||||||
writeLog('INFO', message, data);
|
writeLog('INFO', message, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
warn: (message, data) => {
|
warn: (message, data) => {
|
||||||
console.warn(`[WARN] ${message}`);
|
console.warn(`[WARN] ${message}`);
|
||||||
writeLog('WARN', message, data);
|
writeLog('WARN', message, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
error: (message, data) => {
|
error: (message, data) => {
|
||||||
console.error(`[ERROR] ${message}`);
|
console.error(`[ERROR] ${message}`);
|
||||||
writeLog('ERROR', message, data);
|
writeLog('ERROR', message, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
security: (message, data) => {
|
security: (message, data) => {
|
||||||
console.warn(`[SECURITY] ${message}`);
|
console.warn(`[SECURITY] ${message}`);
|
||||||
writeLog('SECURITY', message, data);
|
writeLog('SECURITY', message, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
sync: (message, data) => {
|
sync: (message, data) => {
|
||||||
console.log(`[SYNC] ${message}`);
|
console.log(`[SYNC] ${message}`);
|
||||||
writeLog('SYNC', message, data);
|
writeLog('SYNC', message, data);
|
||||||
@@ -114,19 +114,19 @@ const logger = {
|
|||||||
console.error(`[FATAL] ${message}`);
|
console.error(`[FATAL] ${message}`);
|
||||||
writeLogSync('FATAL', message, data);
|
writeLogSync('FATAL', message, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
//清理旧日志
|
//清理旧日志
|
||||||
cleanOldLogs: (daysToKeep = 30) => {
|
cleanOldLogs: (daysToKeep = 30) => {
|
||||||
try {
|
try {
|
||||||
const files = fs.readdirSync(LOG_DIR);
|
const files = fs.readdirSync(LOG_DIR);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const maxAge = daysToKeep * 24 * 60 * 60 * 1000;
|
const maxAge = daysToKeep * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
const filePath = path.join(LOG_DIR, file);
|
const filePath = path.join(LOG_DIR, file);
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
const age = now - stats.mtime.getTime();
|
const age = now - stats.mtime.getTime();
|
||||||
|
|
||||||
if (age > maxAge && file.endsWith('.log')) {
|
if (age > maxAge && file.endsWith('.log')) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
logger.info(`Deleted old log file: ${file}`);
|
logger.info(`Deleted old log file: ${file}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user