Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

501 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 写入类 API 路由 · 语言驱动操作系统写入能力层
*
* POST /api/tickets/create — 创建工单
* PATCH /api/tickets/:ticketId — 更新工单状态
* POST /api/syslog/submit — 提交 SYSLOG
* POST /api/broadcasts/create — 创建广播
* POST /api/deploy/preview — 触发预览站部署
* POST /api/deploy/production — 触发正式站部署(需二次确认)
* PATCH /api/agents/:agentId — 更新 Agent 状态
* POST /api/maintenance/log — 写入维护日志
* POST /api/receipts/submit — 提交指令回执
* PATCH /api/dev/:devId/status — 更新开发者状态
*
* 安全规则:所有写入接口需要身份验证 + 权限检查 + 审计日志
* 版权:国作登字-2026-A-00037559
*/
'use strict';
var express = require('express');
var router = express.Router();
var notionService = require('../services/notion');
var githubService = require('../services/github');
var dbConfig = require('../config/databases');
var authMiddleware = require('../middleware/auth');
var sandboxMiddleware = require('../middleware/sandbox');
var auditMiddleware = require('../middleware/audit');
// 所有写入接口需要身份验证 + 审计 + 沙箱检查
router.use(authMiddleware.requireAuth);
router.use(auditMiddleware.auditLog);
router.use(sandboxMiddleware.sandboxGuard);
// 频率限制每个开发者每分钟最多10次写入
var rateLimitMap = new Map();
var RATE_LIMIT = 10;
var RATE_WINDOW = 60 * 1000;
function rateLimit(req, res, next) {
var key = req.user.devId;
var now = Date.now();
var entry = rateLimitMap.get(key);
if (!entry || now - entry.start >= RATE_WINDOW) {
rateLimitMap.set(key, { start: now, count: 1 });
return next();
}
if (entry.count >= RATE_LIMIT) {
return res.status(429).json({
error: true,
code: 'RATE_LIMITED',
message: '写入频率超限(每分钟最多 ' + RATE_LIMIT + ' 次)',
reply: '⏳ 操作太频繁了,请稍等一分钟再试。'
});
}
entry.count++;
next();
}
router.use(rateLimit);
// 模块路径归一化(确保带末尾斜杠)
function normalizeModule(mod) {
if (!mod) return '';
mod = mod.trim();
if (mod !== '*' && !mod.endsWith('/')) mod += '/';
return mod;
}
// 检查开发者是否拥有指定模块
function hasModuleAccess(userModules, module) {
if (!userModules || !module) return false;
if (userModules.includes('*')) return true;
var normalized = normalizeModule(module);
for (var i = 0; i < userModules.length; i++) {
if (normalizeModule(userModules[i]) === normalized) return true;
}
return false;
}
// 获取实际数据库 ID沙箱环境使用沙箱 DB
function getDbId(req, configKey) {
if (req.sandbox && req.sandboxDbIds && req.sandboxDbIds[configKey]) {
return req.sandboxDbIds[configKey];
}
return dbConfig[configKey];
}
// ====== 创建工单 ======
router.post('/tickets/create',
authMiddleware.checkPermission('ticket:create'),
async function(req, res) {
try {
var title = req.body.title;
var description = req.body.description || '';
var priority = req.body.priority || 'P2';
var module = req.body.module || '';
var devId = req.user.devId;
if (!title) {
return res.status(400).json({
error: true,
code: 'MISSING_TITLE',
message: '工单标题不能为空'
});
}
var ticketDbId = getDbId(req, 'ticketBook');
if (!ticketDbId) {
return res.status(503).json({
error: true,
code: 'DB_NOT_CONFIGURED',
message: '工单数据库未配置'
});
}
var properties = {
'工单标题': { title: [{ text: { content: title } }] },
'描述': { rich_text: [{ text: { content: description } }] },
'优先级': { select: { name: priority } },
'提交人': { rich_text: [{ text: { content: devId } }] },
'状态': { select: { name: '待处理' } }
};
if (module) {
properties['模块'] = { select: { name: module } };
}
var result = await notionService.writeToDB(ticketDbId, properties);
var prefix = req.sandbox ? '[沙箱] ' : '';
res.json({
success: true,
message: prefix + '工单已创建',
ticketUrl: result.url || '',
reply: '✅ ' + prefix + '工单「' + title + '」已创建,优先级 ' + priority + ',状态:待处理。'
});
} catch (e) {
res.status(500).json({ error: true, code: 'CREATE_FAILED', message: e.message });
}
}
);
// ====== 更新工单状态 ======
router.patch('/tickets/:ticketId',
authMiddleware.checkPermission('ticket:update'),
async function(req, res) {
try {
var ticketId = req.params.ticketId;
var status = req.body.status;
if (!status) {
return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' });
}
// 使用 Notion page update API
if (!notionService.notion) {
return res.status(503).json({ error: true, code: 'NOTION_UNAVAILABLE', message: 'Notion 未连接' });
}
await notionService.notion.pages.update({
page_id: ticketId,
properties: {
'状态': { select: { name: status } }
}
});
res.json({
success: true,
reply: '✅ 工单状态已更新为:' + status
});
} catch (e) {
res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message });
}
}
);
// ====== 提交 SYSLOG ======
router.post('/syslog/submit',
authMiddleware.checkPermission('syslog:submit'),
async function(req, res) {
try {
var content = req.body.content;
var type = req.body.type || '日常';
var devId = req.user.devId;
if (!content) {
return res.status(400).json({ error: true, code: 'MISSING_CONTENT', message: 'SYSLOG 内容不能为空' });
}
var syslogDbId = getDbId(req, 'syslogInbox');
if (!syslogDbId) {
return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: 'SYSLOG 数据库未配置' });
}
await notionService.writeToDB(syslogDbId, {
'SYSLOG内容': { title: [{ text: { content: content } }] },
'类型': { select: { name: type } },
'开发者编号': { rich_text: [{ text: { content: devId } }] }
});
var prefix = req.sandbox ? '[沙箱] ' : '';
var summary = content.length > 50 ? content.substring(0, 50) + '...' : content;
res.json({
success: true,
reply: '✅ ' + prefix + 'SYSLOG 已提交:「' + summary + '」'
});
} catch (e) {
res.status(500).json({ error: true, code: 'SUBMIT_FAILED', message: e.message });
}
}
);
// ====== 创建广播 ======
router.post('/broadcasts/create',
authMiddleware.checkPermission('broadcast:create'),
async function(req, res) {
try {
var title = req.body.title;
var content = req.body.content;
var target = req.body.target || 'ALL';
var devId = req.user.devId;
if (!title || !content) {
return res.status(400).json({ error: true, code: 'MISSING_FIELDS', message: '广播标题和内容不能为空' });
}
var broadcastId = 'BC-' + Date.now();
var broadcastData = {
broadcast_id: broadcastId,
title: title,
content: content,
target: target,
created_by: devId,
created_at: new Date().toISOString(),
status: 'active'
};
// 写入 GitHub 广播文件
var filePath = '.github/broadcasts/' + broadcastId + '.json';
await githubService.createFile(
filePath,
JSON.stringify(broadcastData, null, 2),
'📢 新广播: ' + title + ' · by ' + devId
);
res.json({
success: true,
broadcast_id: broadcastId,
reply: '📢 广播「' + title + '」已创建并发送,编号:' + broadcastId
});
} catch (e) {
res.status(500).json({ error: true, code: 'CREATE_FAILED', message: e.message });
}
}
);
// ====== 触发预览站部署 ======
router.post('/deploy/preview',
authMiddleware.checkPermission('deploy:preview'),
async function(req, res) {
try {
var module = req.body.module;
var devId = req.user.devId;
if (!module) {
return res.status(400).json({ error: true, code: 'MISSING_MODULE', message: '需要指定部署模块' });
}
// 检查开发者是否拥有该模块
if (!hasModuleAccess(req.user.modules, module)) {
return res.status(403).json({
error: true,
code: 'MODULE_DENIED',
message: '你没有该模块的部署权限',
reply: '❌ 你没有 ' + module + ' 模块的部署权限。你的模块是:' + req.user.modules.join(', ')
});
}
await githubService.triggerWorkflow('preview-deploy.yml', {
module: module,
dev_id: devId,
target: 'preview'
});
res.json({
success: true,
reply: '🚀 已触发 ' + module + ' 模块的预览站部署。稍等几分钟,部署完成后我会通知你。'
});
} catch (e) {
res.status(500).json({ error: true, code: 'DEPLOY_FAILED', message: e.message });
}
}
);
// ====== 触发正式站部署(需二次确认)======
router.post('/deploy/production',
authMiddleware.checkPermission('deploy:production'),
async function(req, res) {
try {
var module = req.body.module;
var confirmToken = req.body.confirmToken;
var devId = req.user.devId;
if (!module) {
return res.status(400).json({ error: true, code: 'MISSING_MODULE', message: '需要指定部署模块' });
}
// 检查模块权限
if (!hasModuleAccess(req.user.modules, module)) {
return res.status(403).json({
error: true,
code: 'MODULE_DENIED',
reply: '❌ 你没有 ' + module + ' 模块的部署权限。'
});
}
// 二次确认
if (!confirmToken) {
return res.json({
success: false,
requireConfirmation: true,
reply: '⚠️ 你正在请求部署 ' + module + ' 到**正式站guanghulab.com**。\n这会影响所有用户。\n\n请说「确认部署 ' + module + '」来确认。'
});
}
await githubService.triggerWorkflow('deploy-to-server.yml', {
module: module,
dev_id: devId,
target: 'production'
});
res.json({
success: true,
reply: '🚀 已触发 ' + module + ' 模块的**正式站部署**。部署日志会同步到维护日志中。'
});
} catch (e) {
res.status(500).json({ error: true, code: 'DEPLOY_FAILED', message: e.message });
}
}
);
// ====== 更新 Agent 状态 ======
router.patch('/agents/:agentId',
authMiddleware.checkPermission('agent:update'),
async function(req, res) {
try {
var agentId = req.params.agentId;
var status = req.body.status;
if (!status) {
return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' });
}
if (!notionService.notion) {
return res.status(503).json({ error: true, code: 'NOTION_UNAVAILABLE', message: 'Notion 未连接' });
}
await notionService.notion.pages.update({
page_id: agentId,
properties: {
'状态': { select: { name: status } }
}
});
res.json({
success: true,
reply: '✅ Agent ' + agentId + ' 状态已更新为:' + status
});
} catch (e) {
res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message });
}
}
);
// ====== 写入维护日志 ======
router.post('/maintenance/log',
authMiddleware.checkPermission('maintenance:log'),
async function(req, res) {
try {
var title = req.body.title;
var type = req.body.type || '常规维护';
var devId = req.user.devId;
if (!title) {
return res.status(400).json({ error: true, code: 'MISSING_TITLE', message: '维护日志标题不能为空' });
}
var maintenanceDbId = getDbId(req, 'maintenanceLog');
if (!maintenanceDbId) {
return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '维护日志数据库未配置' });
}
await notionService.writeToDB(maintenanceDbId, {
'标题': { title: [{ text: { content: title } }] },
'类型': { select: { name: type } },
'操作者': { rich_text: [{ text: { content: devId } }] }
});
res.json({
success: true,
reply: '✅ 维护日志已记录:' + title
});
} catch (e) {
res.status(500).json({ error: true, code: 'LOG_FAILED', message: e.message });
}
}
);
// ====== 提交指令回执 ======
router.post('/receipts/submit',
authMiddleware.checkPermission('syslog:submit'),
async function(req, res) {
try {
var instructionId = req.body.instruction_id;
var status = req.body.status || '已完成';
var receipt = req.body.receipt || '';
if (!instructionId) {
return res.status(400).json({ error: true, code: 'MISSING_ID', message: '指令编号不能为空' });
}
if (!dbConfig.receiptTracker) {
return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '回执数据库未配置' });
}
await notionService.writeToDB(dbConfig.receiptTracker, {
'指令编号': { rich_text: [{ text: { content: instructionId } }] },
'执行状态': { select: { name: status } },
'铸渊回执': { rich_text: [{ text: { content: receipt } }] },
'回执时间': { date: { start: new Date().toISOString() } }
});
res.json({
success: true,
reply: '✅ 指令 ' + instructionId + ' 回执已提交'
});
} catch (e) {
res.status(500).json({ error: true, code: 'SUBMIT_FAILED', message: e.message });
}
}
);
// ====== 更新开发者状态 ======
router.patch('/dev/:devId/status',
authMiddleware.checkPermission('dev:update_self'),
async function(req, res) {
try {
var devId = req.params.devId;
var status = req.body.status;
// 验证 devId 格式
if (!/^DEV-\d{3}$/.test(devId) && !/^TCS-\d{4}$/.test(devId)) {
return res.status(400).json({ error: true, code: 'INVALID_DEV_ID', message: '无效的开发者编号' });
}
// 开发者只能更新自己的状态(管理者除外)
if (req.user.devId !== devId && !req.user.permissions.includes('dev:update_all')) {
return res.status(403).json({
error: true,
code: 'SELF_ONLY',
message: '只能更新自己的状态',
reply: '🔒 你只能更新自己的状态,不能修改其他开发者的信息。'
});
}
if (!status) {
return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' });
}
// 更新 Notion 主控台中的状态
if (dbConfig.controlPanel) {
var result = await notionService.queryDB(dbConfig.controlPanel, {
property: 'DEV编号',
rich_text: { equals: devId }
}, null, 1);
if (result.results && result.results.length > 0) {
await notionService.notion.pages.update({
page_id: result.results[0].id,
properties: {
'状态': { rich_text: [{ text: { content: status } }] }
}
});
}
}
res.json({
success: true,
reply: '✅ ' + devId + ' 的状态已更新为:' + status
});
} catch (e) {
res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message });
}
}
);
module.exports = router;