first commit

This commit is contained in:
qwq
2026-07-24 11:34:38 +08:00
commit 94f4f174f4
26 changed files with 2544 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const config = require('../config');
const { queryLogs, cleanupOldLogs } = require('../middleware/operation-log');
const { sendAlert } = require('../utils/alert');
const logger = require('../utils/logger');
/**
* 管理后台鉴权中间件
*/
function adminAuth(req, res, next) {
const password = req.headers['x-admin-password'] || req.query.password;
if (password !== config.adminPassword) {
return res.status(401).json({ error: '管理密码错误' });
}
next();
}
router.use(adminAuth);
// ===== 操作日志 =====
/**
* GET /api/admin/logs - 查询操作日志
*/
router.get('/logs', (req, res) => {
const { page, pageSize, workflowType, status, startDate, endDate, keyword } = req.query;
const result = queryLogs({
page: parseInt(page) || 1,
pageSize: parseInt(pageSize) || 20,
workflowType,
status,
startDate,
endDate,
keyword,
});
res.json(result);
});
/**
* DELETE /api/admin/logs/cleanup - 手动清理过期日志
*/
router.delete('/logs/cleanup', (req, res) => {
cleanupOldLogs();
res.json({ message: '清理完成' });
});
// ===== API 配置管理 =====
/**
* GET /api/admin/config - 获取所有配置
*/
router.get('/config', (req, res) => {
const rows = db.prepare('SELECT * FROM api_configs ORDER BY key').all();
// 敏感字段脱敏
const masked = rows.map(r => {
if (r.key.toLowerCase().includes('password') || r.key.toLowerCase().includes('secret')) {
return { ...r, value: '******' };
}
return r;
});
res.json(masked);
});
/**
* PUT /api/admin/config - 更新配置
*/
router.put('/config', (req, res) => {
const { key, value, description } = req.body;
if (!key || !value) {
return res.status(400).json({ error: '缺少 key 或 value' });
}
const stmt = db.prepare(`
INSERT INTO api_configs (key, value, description, updated_at)
VALUES (?, ?, ?, datetime('now', 'localtime'))
ON CONFLICT(key) DO UPDATE SET value = ?, description = ?, updated_at = datetime('now', 'localtime')
`);
stmt.run(key, value, description || '', value, description || '');
logger.info(`[管理] 配置已更新: ${key}`);
res.json({ message: `配置 ${key} 已更新` });
});
// ===== 预设命令管理 =====
/**
* GET /api/admin/commands - 获取预设命令列表
*/
router.get('/commands', (req, res) => {
const rows = db.prepare('SELECT * FROM preset_commands ORDER BY id DESC').all();
res.json(rows);
});
/**
* POST /api/admin/commands - 添加预设命令
*/
router.post('/commands', (req, res) => {
const { name, command, target, description } = req.body;
if (!name || !command) {
return res.status(400).json({ error: '缺少 name 或 command' });
}
const stmt = db.prepare(`
INSERT INTO preset_commands (name, command, target, description) VALUES (?, ?, ?, ?)
`);
const result = stmt.run(name, command, target || 'ic1', description || '');
logger.info(`[管理] 预设命令已添加: ${name}`);
res.json({ id: result.lastInsertRowid, message: '命令已添加' });
});
/**
* PUT /api/admin/commands/:id - 更新预设命令
*/
router.put('/commands/:id', (req, res) => {
const { name, command, target, description, enabled } = req.body;
const stmt = db.prepare(`
UPDATE preset_commands SET name = ?, command = ?, target = ?, description = ?, enabled = ?
WHERE id = ?
`);
stmt.run(name, command, target || 'ic1', description || '', enabled !== undefined ? (enabled ? 1 : 0) : 1, req.params.id);
res.json({ message: '命令已更新' });
});
/**
* DELETE /api/admin/commands/:id - 删除预设命令
*/
router.delete('/commands/:id', (req, res) => {
db.prepare('DELETE FROM preset_commands WHERE id = ?').run(req.params.id);
res.json({ message: '命令已删除' });
});
// ===== 系统状态 =====
/**
* GET /api/admin/status - 系统状态
*/
router.get('/status', (req, res) => {
const todayLogs = db.prepare(`
SELECT COUNT(*) as total,
SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) as success,
SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) as failed,
SUM(CASE WHEN status='running' THEN 1 ELSE 0 END) as running
FROM operation_logs WHERE date(created_at) = date('now', 'localtime')
`).get();
res.json({
uptime: process.uptime(),
memory: process.memoryUsage(),
todayStats: todayLogs,
version: require('../../package.json').version,
});
});
/**
* POST /api/admin/test-alert - 测试告警
*/
router.post('/test-alert', async (req, res) => {
await sendAlert('测试告警', '这是一条测试告警消息,系统运行正常。', 'info');
res.json({ message: '测试告警已发送' });
});
module.exports = router;
+210
View File
@@ -0,0 +1,210 @@
const express = require('express');
const router = express.Router();
const config = require('../config');
const feishu = require('../services/feishu');
const { handleFileIn } = require('../workflows/file-in');
const { handleFileOut } = require('../workflows/file-out');
const { handleGroupPermission } = require('../workflows/group-permission');
const { nameToPinyin } = require('../utils/pinyin');
const { createLog, updateLog, isApprovalProcessed } = require('../middleware/operation-log');
const { sendAlert } = require('../utils/alert');
const logger = require('../utils/logger');
/**
* POST /api/feishu/event
* 飞书事件订阅回调入口
*/
router.post('/event', async (req, res) => {
const body = req.body;
// 1. URL 验证挑战(首次配置事件订阅时飞书会发送)
if (body.type === 'url_verification') {
return res.json({ challenge: body.challenge });
}
// 2. 验证 token
const token = body.token || body.header?.token;
if (token && token !== config.feishu.verificationToken) {
logger.warn('[飞书事件] token 验证失败');
return res.status(403).json({ error: 'token mismatch' });
}
// 3. 立即返回 200(飞书要求3秒内响应)
res.json({ code: 0 });
// 4. 异步处理事件
try {
await processEvent(body);
} catch (err) {
logger.error(`[飞书事件] 处理失败: ${err.message}`, { stack: err.stack });
await sendAlert('事件处理失败', `错误: ${err.message}`, 'error');
}
});
/**
* 处理飞书事件
*/
async function processEvent(body) {
// 飞书事件订阅 v2 格式
const header = body.header || {};
const event = body.event || {};
const eventType = header.event_type;
// 只处理审批实例状态变更事件
if (eventType !== 'approval_instance') return;
const instanceCode = event.instance_code;
const approvalCode = event.approval_code;
const status = event.status; // APPROVED / REJECTED / PENDING
// 只处理"已通过"的审批(即审批人同意后触发自动化)
// 根据你的流程设计,也可能是 PENDING 时触发(机器人自动审批)
if (status !== 'APPROVED' && status !== 'PENDING') return;
// 防重复处理
if (isApprovalProcessed(instanceCode)) {
logger.info(`[飞书事件] 审批已处理过,跳过: ${instanceCode}`);
return;
}
logger.info(`[飞书事件] 收到审批事件: code=${approvalCode}, instance=${instanceCode}, status=${status}`);
// 根据审批定义 code 路由到不同工作流
if (approvalCode === config.feishu.approvalCodes.fileIn) {
await processFileIn(approvalCode, instanceCode);
} else if (approvalCode === config.feishu.approvalCodes.fileOut) {
await processFileOut(approvalCode, instanceCode);
} else if (approvalCode === config.feishu.approvalCodes.groupPerm) {
await processGroupPermission(approvalCode, instanceCode);
} else {
logger.info(`[飞书事件] 非目标审批类型,忽略: ${approvalCode}`);
}
}
/**
* 处理文件传入审批
*/
async function processFileIn(approvalCode, instanceId) {
// 获取审批详情
const instance = await feishu.getApprovalInstance(approvalCode, instanceId);
const form = feishu.parseApprovalForm(instance.form);
const approvalData = {
applicantName: form['申请人'] || instance.user_name || '',
fileName: form['文件名称'] || form['文件名'] || '',
};
if (!approvalData.applicantName || !approvalData.fileName) {
throw new Error(`审批表单缺少必要字段: 申请人=${approvalData.applicantName}, 文件=${approvalData.fileName}`);
}
const pinyin = nameToPinyin(approvalData.applicantName);
const logId = createLog({
approvalCode,
instanceId,
workflowType: 'file_in',
applicantName: approvalData.applicantName,
applicantPinyin: pinyin,
action: '文件传入',
detail: `文件: ${approvalData.fileName}`,
});
try {
const result = await handleFileIn(approvalData, logId);
updateLog(logId, 'success');
// 提交审批意见
await feishu.approveInstance(approvalCode, instanceId, result.message);
logger.info(`[文件传入] 完成: ${result.detail}`);
} catch (err) {
updateLog(logId, 'failed', err.message);
await sendAlert('文件传入失败', `审批单: ${instanceId}\n申请人: ${approvalData.applicantName}\n错误: ${err.message}`, 'error');
throw err;
}
}
/**
* 处理文件传出审批
*/
async function processFileOut(approvalCode, instanceId) {
const instance = await feishu.getApprovalInstance(approvalCode, instanceId);
const form = feishu.parseApprovalForm(instance.form);
const approvalData = {
applicantName: form['申请人'] || instance.user_name || '',
filePath: form['文件路径'] || form['文件位置'] || '',
destination: form['传出目的'] || form['用途'] || '',
customerAbbr: form['客户名缩写'] || form['客户简称'] || '',
expireDate: form['文件过期日期'] || form['过期日期'] || '',
};
if (!approvalData.applicantName || !approvalData.filePath || !approvalData.destination) {
throw new Error('审批表单缺少必要字段(申请人/文件路径/传出目的)');
}
const pinyin = nameToPinyin(approvalData.applicantName);
const logId = createLog({
approvalCode,
instanceId,
workflowType: 'file_out',
applicantName: approvalData.applicantName,
applicantPinyin: pinyin,
action: `文件传出-${approvalData.destination}`,
detail: `文件: ${approvalData.filePath}, 目的: ${approvalData.destination}`,
});
try {
const result = await handleFileOut(approvalData, logId);
updateLog(logId, 'success');
await feishu.approveInstance(approvalCode, instanceId, result.message);
logger.info(`[文件传出] 完成: ${result.detail}`);
} catch (err) {
updateLog(logId, 'failed', err.message);
await sendAlert('文件传出失败', `审批单: ${instanceId}\n申请人: ${approvalData.applicantName}\n错误: ${err.message}`, 'error');
throw err;
}
}
/**
* 处理红区组权限审批
*/
async function processGroupPermission(approvalCode, instanceId) {
const instance = await feishu.getApprovalInstance(approvalCode, instanceId);
const form = feishu.parseApprovalForm(instance.form);
const approvalData = {
personnelName: form['待添加人员'] || form['人员'] || '',
requestType: form['申请类型'] || form['类型'] || '',
groupName: form['group名称'] || form['组名'] || form['Group'] || '',
};
if (!approvalData.personnelName || !approvalData.requestType || !approvalData.groupName) {
throw new Error('审批表单缺少必要字段(待添加人员/申请类型/group名称)');
}
const pinyin = nameToPinyin(approvalData.personnelName);
const logId = createLog({
approvalCode,
instanceId,
workflowType: 'group_permission',
applicantName: approvalData.personnelName,
applicantPinyin: pinyin,
action: `红区组权限-${approvalData.requestType}`,
detail: `人员: ${approvalData.personnelName}, group: ${approvalData.groupName}`,
});
try {
const result = await handleGroupPermission(approvalData, logId);
updateLog(logId, 'success');
await feishu.approveInstance(approvalCode, instanceId, result.message);
logger.info(`[红区组权限] 完成: ${result.detail}`);
} catch (err) {
updateLog(logId, 'failed', err.message);
await sendAlert('红区组权限操作失败', `审批单: ${instanceId}\n人员: ${approvalData.personnelName}\n错误: ${err.message}`, 'error');
throw err;
}
}
module.exports = router;