compatibility-layer.js 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  1. /**
  2. * 兼容层 - 确保旧版API接口继续工作
  3. */
  4. const logger = require('./logger');
  5. const { requireLogin } = require('./middleware/auth');
  6. const { execCommand } = require('./server-utils');
  7. const os = require('os');
  8. const { exec } = require('child_process');
  9. const util = require('util');
  10. const execPromise = util.promisify(exec);
  11. module.exports = function(app) {
  12. logger.info('加载API兼容层...');
  13. // 会话检查接口
  14. app.get('/api/check-session', (req, res) => {
  15. if (req.session && req.session.user) {
  16. res.json({ authenticated: true, user: req.session.user });
  17. } else {
  18. res.json({ authenticated: false });
  19. }
  20. });
  21. // 添加Docker状态检查接口,并使用 requireLogin 中间件
  22. app.get('/api/docker/status', requireLogin, async (req, res) => {
  23. try {
  24. const dockerService = require('./services/dockerService');
  25. const dockerStatus = await dockerService.checkDockerAvailability();
  26. res.json({ isRunning: dockerStatus });
  27. } catch (error) {
  28. logger.error('检查Docker状态失败:', error);
  29. res.status(500).json({ error: '检查Docker状态失败', details: error.message });
  30. }
  31. });
  32. // 验证码接口
  33. app.get('/api/captcha', (req, res) => {
  34. try {
  35. const num1 = Math.floor(Math.random() * 10);
  36. const num2 = Math.floor(Math.random() * 10);
  37. const captcha = `${num1} + ${num2} = ?`;
  38. req.session.captcha = num1 + num2;
  39. res.json({ captcha });
  40. } catch (error) {
  41. logger.error('生成验证码失败:', error);
  42. res.status(500).json({ error: '生成验证码失败' });
  43. }
  44. });
  45. // 停止容器列表接口
  46. app.get('/api/stopped-containers', requireLogin, async (req, res) => {
  47. try {
  48. const monitoringService = require('./services/monitoringService');
  49. const stoppedContainers = await monitoringService.getStoppedContainers();
  50. res.json(stoppedContainers);
  51. } catch (error) {
  52. logger.error('获取已停止容器列表失败:', error);
  53. res.status(500).json({ error: '获取已停止容器列表失败', details: error.message });
  54. }
  55. });
  56. // 修复Docker Hub搜索接口 - 直接使用axios请求,避免dockerHubService的依赖问题
  57. app.get('/api/dockerhub/search', async (req, res) => {
  58. try {
  59. const axios = require('axios');
  60. const term = req.query.term;
  61. const page = req.query.page || 1;
  62. if (!term) {
  63. return res.status(400).json({ error: '搜索词不能为空' });
  64. }
  65. logger.info(`搜索Docker Hub: ${term} (页码: ${page})`);
  66. const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${page}&page_size=25`;
  67. const response = await axios.get(url, {
  68. timeout: 15000,
  69. headers: {
  70. 'User-Agent': 'DockerHubSearchClient/1.0',
  71. 'Accept': 'application/json'
  72. }
  73. });
  74. res.json(response.data);
  75. } catch (error) {
  76. logger.error('搜索Docker Hub失败:', error.message || error);
  77. res.status(500).json({
  78. error: '搜索失败',
  79. details: error.message || '未知错误',
  80. retryable: true
  81. });
  82. }
  83. });
  84. // Docker Hub 标签计数接口
  85. app.get('/api/dockerhub/tag-count', async (req, res) => {
  86. try {
  87. const axios = require('axios');
  88. const name = req.query.name;
  89. const isOfficial = req.query.official === 'true';
  90. if (!name) {
  91. return res.status(400).json({ error: '镜像名称不能为空' });
  92. }
  93. const fullImageName = isOfficial ? `library/${name}` : name;
  94. const apiUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags/?page_size=1`;
  95. logger.info(`获取标签计数: ${fullImageName}`);
  96. const response = await axios.get(apiUrl, {
  97. timeout: 15000,
  98. headers: {
  99. 'User-Agent': 'DockerHubSearchClient/1.0',
  100. 'Accept': 'application/json'
  101. }
  102. });
  103. res.json({
  104. count: response.data.count,
  105. recommended_mode: response.data.count > 500 ? 'paginated' : 'full'
  106. });
  107. } catch (error) {
  108. logger.error('获取标签计数失败:', error.message || error);
  109. res.status(500).json({
  110. error: '获取标签计数失败',
  111. details: error.message || '未知错误',
  112. retryable: true
  113. });
  114. }
  115. });
  116. // Docker Hub 标签接口
  117. app.get('/api/dockerhub/tags', async (req, res) => {
  118. try {
  119. const axios = require('axios');
  120. const imageName = req.query.name;
  121. const isOfficial = req.query.official === 'true';
  122. const page = parseInt(req.query.page) || 1;
  123. const page_size = parseInt(req.query.page_size) || 25;
  124. const getAllTags = req.query.all === 'true';
  125. if (!imageName) {
  126. return res.status(400).json({ error: '镜像名称不能为空' });
  127. }
  128. const fullImageName = isOfficial ? `library/${imageName}` : imageName;
  129. logger.info(`获取镜像标签: ${fullImageName}, 页码: ${page}, 每页数量: ${page_size}, 获取全部: ${getAllTags}`);
  130. // 如果请求所有标签,需要递归获取所有页
  131. if (getAllTags) {
  132. // 暂不实现全部获取,返回错误
  133. return res.status(400).json({ error: '获取全部标签功能暂未实现,请使用分页获取' });
  134. } else {
  135. // 获取特定页的标签
  136. const tagsUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${page_size}`;
  137. const tagsResponse = await axios.get(tagsUrl, {
  138. timeout: 15000,
  139. headers: {
  140. 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
  141. }
  142. });
  143. // 检查响应数据有效性
  144. if (!tagsResponse.data || typeof tagsResponse.data !== 'object') {
  145. logger.warn(`镜像 ${fullImageName} 返回的数据格式不正确`);
  146. return res.status(500).json({ error: '响应数据格式不正确' });
  147. }
  148. if (!tagsResponse.data.results || !Array.isArray(tagsResponse.data.results)) {
  149. logger.warn(`镜像 ${fullImageName} 没有返回有效的标签数据`);
  150. return res.status(500).json({ error: '没有找到有效的标签数据' });
  151. }
  152. // 过滤掉无效平台信息
  153. const cleanedResults = tagsResponse.data.results.map(tag => {
  154. if (tag.images && Array.isArray(tag.images)) {
  155. tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown'));
  156. }
  157. return tag;
  158. });
  159. return res.json({
  160. ...tagsResponse.data,
  161. results: cleanedResults
  162. });
  163. }
  164. } catch (error) {
  165. logger.error('获取标签列表失败:', error.message || error);
  166. res.status(500).json({
  167. error: '获取标签列表失败',
  168. details: error.message || '未知错误',
  169. retryable: true
  170. });
  171. }
  172. });
  173. // 文档接口
  174. app.get('/api/documentation', async (req, res) => {
  175. try {
  176. const docService = require('./services/documentationService');
  177. const documents = await docService.getPublishedDocuments();
  178. res.json(documents);
  179. } catch (error) {
  180. logger.error('获取已发布文档失败:', error);
  181. res.status(500).json({ error: '获取文档失败', details: error.message });
  182. }
  183. });
  184. // 监控配置接口
  185. app.get('/api/monitoring-config', async (req, res) => {
  186. try {
  187. logger.info('兼容层处理监控配置请求');
  188. const fs = require('fs').promises;
  189. const path = require('path');
  190. // 监控配置文件路径
  191. const CONFIG_FILE = path.join(__dirname, './config/monitoring.json');
  192. // 确保配置文件存在
  193. try {
  194. await fs.access(CONFIG_FILE);
  195. } catch (err) {
  196. // 文件不存在,创建默认配置
  197. const defaultConfig = {
  198. isEnabled: false,
  199. notificationType: 'wechat',
  200. webhookUrl: '',
  201. telegramToken: '',
  202. telegramChatId: '',
  203. monitorInterval: 60
  204. };
  205. await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
  206. await fs.writeFile(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), 'utf8');
  207. return res.json(defaultConfig);
  208. }
  209. // 文件存在,读取配置
  210. const data = await fs.readFile(CONFIG_FILE, 'utf8');
  211. res.json(JSON.parse(data));
  212. } catch (err) {
  213. logger.error('获取监控配置失败:', err);
  214. res.status(500).json({ error: '获取监控配置失败' });
  215. }
  216. });
  217. // 保存监控配置接口
  218. app.post('/api/monitoring-config', async (req, res) => {
  219. try {
  220. logger.info('兼容层处理保存监控配置请求');
  221. const fs = require('fs').promises;
  222. const path = require('path');
  223. const {
  224. notificationType,
  225. webhookUrl,
  226. telegramToken,
  227. telegramChatId,
  228. monitorInterval,
  229. isEnabled
  230. } = req.body;
  231. // 简单验证
  232. if (notificationType === 'wechat' && !webhookUrl) {
  233. return res.status(400).json({ error: '企业微信通知需要设置 webhook URL' });
  234. }
  235. if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
  236. return res.status(400).json({ error: 'Telegram 通知需要设置 Token 和 Chat ID' });
  237. }
  238. // 监控配置文件路径
  239. const CONFIG_FILE = path.join(__dirname, './config/monitoring.json');
  240. // 确保配置文件存在
  241. let config = {
  242. isEnabled: false,
  243. notificationType: 'wechat',
  244. webhookUrl: '',
  245. telegramToken: '',
  246. telegramChatId: '',
  247. monitorInterval: 60
  248. };
  249. try {
  250. const data = await fs.readFile(CONFIG_FILE, 'utf8');
  251. config = JSON.parse(data);
  252. } catch (err) {
  253. // 如果读取失败,使用默认配置
  254. logger.warn('读取监控配置失败,将使用默认配置:', err);
  255. }
  256. // 更新配置
  257. const updatedConfig = {
  258. ...config,
  259. notificationType,
  260. webhookUrl: webhookUrl || '',
  261. telegramToken: telegramToken || '',
  262. telegramChatId: telegramChatId || '',
  263. monitorInterval: parseInt(monitorInterval, 10) || 60,
  264. isEnabled: isEnabled !== undefined ? isEnabled : config.isEnabled
  265. };
  266. await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
  267. await fs.writeFile(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2), 'utf8');
  268. res.json({ success: true, message: '监控配置已保存' });
  269. // 通知监控服务重新加载配置
  270. if (global.monitoringService && typeof global.monitoringService.reload === 'function') {
  271. global.monitoringService.reload();
  272. }
  273. } catch (err) {
  274. logger.error('保存监控配置失败:', err);
  275. res.status(500).json({ error: '保存监控配置失败' });
  276. }
  277. });
  278. // 获取单个文档接口
  279. app.get('/api/documentation/:id', async (req, res) => {
  280. try {
  281. const docService = require('./services/documentationService');
  282. const document = await docService.getDocument(req.params.id);
  283. // 如果文档不是发布状态,只有已登录用户才能访问
  284. if (!document.published && !req.session.user) {
  285. return res.status(403).json({ error: '没有权限访问该文档' });
  286. }
  287. res.json(document);
  288. } catch (error) {
  289. logger.error(`获取文档 ID:${req.params.id} 失败:`, error);
  290. if (error.code === 'ENOENT') {
  291. return res.status(404).json({ error: '文档不存在' });
  292. }
  293. res.status(500).json({ error: '获取文档失败', details: error.message });
  294. }
  295. });
  296. // 文档列表接口
  297. app.get('/api/documentation-list', requireLogin, async (req, res) => {
  298. try {
  299. const docService = require('./services/documentationService');
  300. const documents = await docService.getDocumentationList();
  301. res.json(documents);
  302. } catch (error) {
  303. logger.error('获取文档列表失败:', error);
  304. res.status(500).json({ error: '获取文档列表失败', details: error.message });
  305. }
  306. });
  307. // 切换监控状态接口
  308. app.post('/api/toggle-monitoring', async (req, res) => {
  309. try {
  310. logger.info('兼容层处理切换监控状态请求');
  311. const fs = require('fs').promises;
  312. const path = require('path');
  313. const { isEnabled } = req.body;
  314. // 监控配置文件路径
  315. const CONFIG_FILE = path.join(__dirname, './config/monitoring.json');
  316. // 确保配置文件存在
  317. let config = {
  318. isEnabled: false,
  319. notificationType: 'wechat',
  320. webhookUrl: '',
  321. telegramToken: '',
  322. telegramChatId: '',
  323. monitorInterval: 60
  324. };
  325. try {
  326. const data = await fs.readFile(CONFIG_FILE, 'utf8');
  327. config = JSON.parse(data);
  328. } catch (err) {
  329. // 如果读取失败,使用默认配置
  330. logger.warn('读取监控配置失败,将使用默认配置:', err);
  331. }
  332. // 更新启用状态
  333. config.isEnabled = !!isEnabled;
  334. await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
  335. await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
  336. res.json({
  337. success: true,
  338. message: `监控已${isEnabled ? '启用' : '禁用'}`
  339. });
  340. // 通知监控服务重新加载配置
  341. if (global.monitoringService && typeof global.monitoringService.reload === 'function') {
  342. global.monitoringService.reload();
  343. }
  344. } catch (err) {
  345. logger.error('切换监控状态失败:', err);
  346. res.status(500).json({ error: '切换监控状态失败' });
  347. }
  348. });
  349. // 测试通知接口
  350. app.post('/api/test-notification', async (req, res) => {
  351. try {
  352. logger.info('兼容层处理测试通知请求');
  353. const {
  354. notificationType,
  355. webhookUrl,
  356. telegramToken,
  357. telegramChatId
  358. } = req.body;
  359. // 简单验证
  360. if (notificationType === 'wechat' && !webhookUrl) {
  361. return res.status(400).json({ error: '企业微信通知需要设置 webhook URL' });
  362. }
  363. if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
  364. return res.status(400).json({ error: 'Telegram 通知需要设置 Token 和 Chat ID' });
  365. }
  366. // 发送测试通知
  367. const notifier = require('./services/notificationService');
  368. const testMessage = {
  369. title: '测试通知',
  370. content: '这是一条测试通知,如果您收到这条消息,说明您的通知配置工作正常。',
  371. time: new Date().toLocaleString()
  372. };
  373. await notifier.sendNotification(testMessage, {
  374. type: notificationType,
  375. webhookUrl,
  376. telegramToken,
  377. telegramChatId
  378. });
  379. res.json({ success: true, message: '测试通知已发送' });
  380. } catch (err) {
  381. logger.error('发送测试通知失败:', err);
  382. res.status(500).json({ error: '发送测试通知失败: ' + err.message });
  383. }
  384. });
  385. // 获取已停止的容器接口
  386. app.get('/api/stopped-containers', requireLogin, async (req, res) => {
  387. try {
  388. logger.info('兼容层处理获取已停止容器请求');
  389. const { exec } = require('child_process');
  390. const util = require('util');
  391. const execPromise = util.promisify(exec);
  392. const { stdout } = await execPromise('docker ps -f "status=exited" --format "{{.ID}}\\t{{.Names}}\\t{{.Status}}"');
  393. const containers = stdout.trim().split('\n')
  394. .filter(line => line.trim())
  395. .map(line => {
  396. const [id, name, ...statusParts] = line.split('\t');
  397. return {
  398. id: id.substring(0, 12),
  399. name,
  400. status: statusParts.join(' ')
  401. };
  402. });
  403. res.json(containers);
  404. } catch (err) {
  405. logger.error('获取已停止容器失败:', err);
  406. res.status(500).json({ error: '获取已停止容器失败', details: err.message });
  407. }
  408. });
  409. // 系统状态接口
  410. app.get('/api/system-status', requireLogin, async (req, res) => {
  411. try {
  412. const systemRouter = require('./routes/system');
  413. return await systemRouter.getSystemStats(req, res);
  414. } catch (error) {
  415. logger.error('获取系统状态失败:', error);
  416. res.status(500).json({ error: '获取系统状态失败', details: error.message });
  417. }
  418. });
  419. // Docker容器状态接口
  420. app.get('/api/docker-status', async (req, res) => {
  421. try {
  422. const dockerService = require('./services/dockerService');
  423. const containerStatus = await dockerService.getContainersStatus();
  424. res.json(containerStatus);
  425. } catch (error) {
  426. logger.error('获取Docker状态失败:', error);
  427. res.status(500).json({ error: '获取Docker状态失败', details: error.message });
  428. }
  429. });
  430. // 单个容器状态接口
  431. app.get('/api/docker/status/:id', requireLogin, async (req, res) => {
  432. try {
  433. const dockerService = require('./services/dockerService');
  434. const containerInfo = await dockerService.getContainerStatus(req.params.id);
  435. res.json(containerInfo);
  436. } catch (error) {
  437. logger.error('获取容器状态失败:', error);
  438. res.status(500).json({ error: '获取容器状态失败', details: error.message });
  439. }
  440. });
  441. // 添加Docker容器操作API兼容层 - 解决404问题
  442. // 容器日志获取接口
  443. app.get('/api/docker/containers/:id/logs', requireLogin, async (req, res) => {
  444. try {
  445. logger.info(`兼容层处理获取容器日志请求: ${req.params.id}`);
  446. const dockerService = require('./services/dockerService');
  447. const logs = await dockerService.getContainerLogs(req.params.id);
  448. res.send(logs);
  449. } catch (error) {
  450. logger.error(`获取容器日志失败:`, error);
  451. res.status(500).json({ error: '获取容器日志失败', details: error.message });
  452. }
  453. });
  454. // 容器详情接口
  455. app.get('/api/docker/containers/:id', requireLogin, async (req, res) => {
  456. try {
  457. logger.info(`兼容层处理获取容器详情请求: ${req.params.id}`);
  458. const dockerService = require('./services/dockerService');
  459. const containerInfo = await dockerService.getContainerStatus(req.params.id);
  460. res.json(containerInfo);
  461. } catch (error) {
  462. logger.error(`获取容器详情失败:`, error);
  463. res.status(500).json({ error: '获取容器详情失败', details: error.message });
  464. }
  465. });
  466. // 启动容器接口
  467. app.post('/api/docker/containers/:id/start', requireLogin, async (req, res) => {
  468. try {
  469. logger.info(`兼容层处理启动容器请求: ${req.params.id}`);
  470. const dockerService = require('./services/dockerService');
  471. await dockerService.startContainer(req.params.id);
  472. res.json({ success: true, message: '容器启动成功' });
  473. } catch (error) {
  474. logger.error(`启动容器失败:`, error);
  475. res.status(500).json({ error: '启动容器失败', details: error.message });
  476. }
  477. });
  478. // 停止容器接口
  479. app.post('/api/docker/containers/:id/stop', requireLogin, async (req, res) => {
  480. try {
  481. logger.info(`兼容层处理停止容器请求: ${req.params.id}`);
  482. const dockerService = require('./services/dockerService');
  483. await dockerService.stopContainer(req.params.id);
  484. res.json({ success: true, message: '容器停止成功' });
  485. } catch (error) {
  486. logger.error(`停止容器失败:`, error);
  487. res.status(500).json({ error: '停止容器失败', details: error.message });
  488. }
  489. });
  490. // 重启容器接口
  491. app.post('/api/docker/containers/:id/restart', requireLogin, async (req, res) => {
  492. try {
  493. logger.info(`兼容层处理重启容器请求: ${req.params.id}`);
  494. const dockerService = require('./services/dockerService');
  495. await dockerService.restartContainer(req.params.id);
  496. res.json({ success: true, message: '容器重启成功' });
  497. } catch (error) {
  498. logger.error(`重启容器失败:`, error);
  499. res.status(500).json({ error: '重启容器失败', details: error.message });
  500. }
  501. });
  502. // 更新容器接口
  503. app.post('/api/docker/containers/:id/update', requireLogin, async (req, res) => {
  504. try {
  505. logger.info(`兼容层处理更新容器请求: ${req.params.id}`);
  506. const dockerService = require('./services/dockerService');
  507. const { tag } = req.body;
  508. await dockerService.updateContainer(req.params.id, tag);
  509. res.json({ success: true, message: '容器更新成功' });
  510. } catch (error) {
  511. logger.error(`更新容器失败:`, error);
  512. res.status(500).json({ error: '更新容器失败', details: error.message });
  513. }
  514. });
  515. // 删除容器接口
  516. app.post('/api/docker/containers/:id/remove', requireLogin, async (req, res) => {
  517. try {
  518. logger.info(`兼容层处理删除容器请求: ${req.params.id}`);
  519. const dockerService = require('./services/dockerService');
  520. await dockerService.deleteContainer(req.params.id);
  521. res.json({ success: true, message: '容器删除成功' });
  522. } catch (error) {
  523. logger.error(`删除容器失败:`, error);
  524. res.status(500).json({ error: '删除容器失败', details: error.message });
  525. }
  526. });
  527. // 登录接口 (兼容层备份)
  528. app.post('/api/login', async (req, res) => {
  529. try {
  530. const { username, password, captcha } = req.body;
  531. if (req.session.captcha !== parseInt(captcha)) {
  532. logger.warn(`Captcha verification failed for user: ${username}`);
  533. return res.status(401).json({ error: '验证码错误' });
  534. }
  535. const userService = require('./services/userService');
  536. const users = await userService.getUsers();
  537. const user = users.users.find(u => u.username === username);
  538. if (!user) {
  539. logger.warn(`User ${username} not found`);
  540. return res.status(401).json({ error: '用户名或密码错误' });
  541. }
  542. const bcrypt = require('bcrypt');
  543. if (bcrypt.compareSync(password, user.password)) {
  544. req.session.user = { username: user.username };
  545. // 更新用户登录信息
  546. await userService.updateUserLoginInfo(username);
  547. logger.info(`User ${username} logged in successfully`);
  548. res.json({ success: true });
  549. } else {
  550. logger.warn(`Login failed for user: ${username}`);
  551. res.status(401).json({ error: '用户名或密码错误' });
  552. }
  553. } catch (error) {
  554. logger.error('登录失败:', error);
  555. res.status(500).json({ error: '登录处理失败', details: error.message });
  556. }
  557. });
  558. // 修复搜索函数问题 - 完善错误处理
  559. app.get('/api/search', async (req, res) => {
  560. try {
  561. const dockerHubService = require('./services/dockerHubService');
  562. const term = req.query.term;
  563. if (!term) {
  564. return res.status(400).json({ error: '搜索词不能为空' });
  565. }
  566. // 直接处理搜索,不依赖缓存
  567. try {
  568. const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${req.query.page || 1}&page_size=25`;
  569. const axios = require('axios');
  570. const response = await axios.get(url, {
  571. timeout: 15000,
  572. headers: {
  573. 'User-Agent': 'DockerHubSearchClient/1.0',
  574. 'Accept': 'application/json'
  575. }
  576. });
  577. res.json(response.data);
  578. } catch (searchError) {
  579. logger.error('Docker Hub搜索请求失败:', searchError.message);
  580. res.status(500).json({
  581. error: '搜索Docker Hub失败',
  582. details: searchError.message,
  583. retryable: true
  584. });
  585. }
  586. } catch (error) {
  587. logger.error('搜索Docker Hub失败:', error);
  588. res.status(500).json({ error: '搜索失败', details: error.message });
  589. }
  590. });
  591. // 获取磁盘空间信息的API
  592. app.get('/api/disk-space', requireLogin, async (req, res) => {
  593. try {
  594. // 使用server-utils中的execCommand函数执行df命令
  595. const diskInfo = await execCommand('df -h | grep -E "/$|/home" | head -1');
  596. const diskParts = diskInfo.split(/\s+/);
  597. if (diskParts.length >= 5) {
  598. res.json({
  599. diskSpace: `${diskParts[2]}/${diskParts[1]}`, // 已用/总量
  600. usagePercent: parseInt(diskParts[4].replace('%', '')) // 使用百分比
  601. });
  602. } else {
  603. throw new Error('磁盘信息格式不正确');
  604. }
  605. } catch (error) {
  606. logger.error('获取磁盘空间信息失败:', error);
  607. res.status(500).json({
  608. error: '获取磁盘空间信息失败',
  609. details: error.message,
  610. diskSpace: '未知',
  611. usagePercent: 0
  612. });
  613. }
  614. });
  615. // 兼容config API
  616. app.get('/api/config', async (req, res) => {
  617. try {
  618. logger.info('兼容层处理配置请求');
  619. const fs = require('fs').promises;
  620. const path = require('path');
  621. // 配置文件路径
  622. const configFilePath = path.join(__dirname, './data/config.json');
  623. // 默认配置
  624. const DEFAULT_CONFIG = {
  625. proxyDomain: 'registry-1.docker.io',
  626. logo: '',
  627. theme: 'light',
  628. menuItems: [
  629. {
  630. text: "首页",
  631. link: "/",
  632. newTab: false
  633. },
  634. {
  635. text: "文档",
  636. link: "/docs",
  637. newTab: false
  638. }
  639. ]
  640. };
  641. // 确保配置存在
  642. let config = DEFAULT_CONFIG;
  643. try {
  644. await fs.access(configFilePath);
  645. const data = await fs.readFile(configFilePath, 'utf8');
  646. config = JSON.parse(data);
  647. } catch (err) {
  648. // 如果文件不存在或解析失败,使用默认配置
  649. logger.warn('读取配置文件失败,将使用默认配置:', err);
  650. // 尝试创建配置文件
  651. try {
  652. await fs.mkdir(path.dirname(configFilePath), { recursive: true });
  653. await fs.writeFile(configFilePath, JSON.stringify(DEFAULT_CONFIG, null, 2));
  654. } catch (writeErr) {
  655. logger.error('创建默认配置文件失败:', writeErr);
  656. }
  657. }
  658. res.json(config);
  659. } catch (err) {
  660. logger.error('获取配置失败:', err);
  661. res.status(500).json({ error: '获取配置失败' });
  662. }
  663. });
  664. // 保存配置API
  665. app.post('/api/config', async (req, res) => {
  666. try {
  667. logger.info('兼容层处理保存配置请求');
  668. const fs = require('fs').promises;
  669. const path = require('path');
  670. const newConfig = req.body;
  671. // 验证请求数据
  672. if (!newConfig || typeof newConfig !== 'object') {
  673. return res.status(400).json({
  674. error: '无效的配置数据',
  675. details: '配置必须是一个对象'
  676. });
  677. }
  678. const configFilePath = path.join(__dirname, './data/config.json');
  679. // 读取现有配置
  680. let existingConfig = {};
  681. try {
  682. const data = await fs.readFile(configFilePath, 'utf8');
  683. existingConfig = JSON.parse(data);
  684. } catch (err) {
  685. // 文件不存在或解析失败时创建目录
  686. await fs.mkdir(path.dirname(configFilePath), { recursive: true });
  687. }
  688. // 合并配置
  689. const mergedConfig = { ...existingConfig, ...newConfig };
  690. // 保存到文件
  691. await fs.writeFile(configFilePath, JSON.stringify(mergedConfig, null, 2));
  692. res.json({ success: true, message: '配置已保存' });
  693. } catch (err) {
  694. logger.error('保存配置失败:', err);
  695. res.status(500).json({
  696. error: '保存配置失败',
  697. details: err.message
  698. });
  699. }
  700. });
  701. // 文档管理API - 获取文档列表
  702. app.get('/api/documents', requireLogin, async (req, res) => {
  703. try {
  704. logger.info('兼容层处理获取文档列表请求');
  705. const docService = require('./services/documentationService');
  706. const documents = await docService.getDocumentationList();
  707. res.json(documents);
  708. } catch (err) {
  709. logger.error('获取文档列表失败:', err);
  710. res.status(500).json({ error: '获取文档列表失败', details: err.message });
  711. }
  712. });
  713. // 文档管理API - 获取单个文档
  714. app.get('/api/documents/:id', async (req, res) => {
  715. try {
  716. logger.info(`兼容层处理获取文档请求: ${req.params.id}`);
  717. const docService = require('./services/documentationService');
  718. const document = await docService.getDocument(req.params.id);
  719. // 如果文档不是发布状态,只有已登录用户才能访问
  720. if (!document.published && !req.session.user) {
  721. return res.status(403).json({ error: '没有权限访问该文档' });
  722. }
  723. res.json(document);
  724. } catch (err) {
  725. logger.error(`获取文档 ID:${req.params.id} 失败:`, err);
  726. if (err.code === 'ENOENT') {
  727. return res.status(404).json({ error: '文档不存在' });
  728. }
  729. res.status(500).json({ error: '获取文档失败', details: err.message });
  730. }
  731. });
  732. // 文档管理API - 保存或更新文档
  733. app.put('/api/documents/:id', requireLogin, async (req, res) => {
  734. try {
  735. logger.info(`兼容层处理更新文档请求: ${req.params.id}`);
  736. const { title, content, published } = req.body;
  737. const docService = require('./services/documentationService');
  738. // 检查必需参数
  739. if (!title) {
  740. return res.status(400).json({ error: '文档标题不能为空' });
  741. }
  742. const docId = req.params.id;
  743. await docService.saveDocument(docId, title, content || '', published);
  744. res.json({ success: true, id: docId, message: '文档已保存' });
  745. } catch (err) {
  746. logger.error(`更新文档 ID:${req.params.id} 失败:`, err);
  747. res.status(500).json({ error: '保存文档失败', details: err.message });
  748. }
  749. });
  750. // 文档管理API - 创建新文档
  751. app.post('/api/documents', requireLogin, async (req, res) => {
  752. try {
  753. logger.info('兼容层处理创建文档请求');
  754. const { title, content, published } = req.body;
  755. const docService = require('./services/documentationService');
  756. // 检查必需参数
  757. if (!title) {
  758. return res.status(400).json({ error: '文档标题不能为空' });
  759. }
  760. // 创建新文档ID (使用时间戳)
  761. const docId = Date.now().toString();
  762. await docService.saveDocument(docId, title, content || '', published);
  763. res.status(201).json({ success: true, id: docId, message: '文档已创建' });
  764. } catch (err) {
  765. logger.error('创建文档失败:', err);
  766. res.status(500).json({ error: '创建文档失败', details: err.message });
  767. }
  768. });
  769. // 文档管理API - 删除文档
  770. app.delete('/api/documents/:id', requireLogin, async (req, res) => {
  771. try {
  772. logger.info(`兼容层处理删除文档请求: ${req.params.id}`);
  773. const docService = require('./services/documentationService');
  774. await docService.deleteDocument(req.params.id);
  775. res.json({ success: true, message: '文档已删除' });
  776. } catch (err) {
  777. logger.error(`删除文档 ID:${req.params.id} 失败:`, err);
  778. res.status(500).json({ error: '删除文档失败', details: err.message });
  779. }
  780. });
  781. // 文档管理API - 切换文档发布状态
  782. app.put('/api/documentation/toggle-publish/:id', requireLogin, async (req, res) => {
  783. try {
  784. logger.info(`兼容层处理切换文档发布状态请求: ${req.params.id}`);
  785. const docService = require('./services/documentationService');
  786. const result = await docService.toggleDocumentPublish(req.params.id);
  787. res.json({
  788. success: true,
  789. published: result.published,
  790. message: `文档已${result.published ? '发布' : '取消发布'}`
  791. });
  792. } catch (err) {
  793. logger.error(`切换文档 ID:${req.params.id} 发布状态失败:`, err);
  794. res.status(500).json({ error: '切换文档发布状态失败', details: err.message });
  795. }
  796. });
  797. // 网络测试接口
  798. app.post('/api/network-test', requireLogin, async (req, res) => {
  799. const { type, domain } = req.body;
  800. // 验证输入
  801. if (!domain || !domain.match(/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)) {
  802. return res.status(400).json({ error: '无效的域名格式' });
  803. }
  804. if (!type || !['ping', 'traceroute'].includes(type)) {
  805. return res.status(400).json({ error: '无效的测试类型' });
  806. }
  807. try {
  808. const command = type === 'ping'
  809. ? `ping -c 4 ${domain}`
  810. : `traceroute -m 10 ${domain}`;
  811. logger.info(`执行网络测试: ${command}`);
  812. const result = await execCommand(command, { timeout: 30000 });
  813. res.send(result);
  814. } catch (error) {
  815. logger.error(`执行网络测试命令错误:`, error);
  816. if (error.killed) {
  817. return res.status(408).send('测试超时');
  818. }
  819. res.status(500).send('测试执行失败: ' + (error.message || '未知错误'));
  820. }
  821. });
  822. // 用户信息接口
  823. app.get('/api/user-info', requireLogin, async (req, res) => {
  824. try {
  825. const userService = require('./services/userService');
  826. const userStats = await userService.getUserStats(req.session.user.username);
  827. res.json(userStats);
  828. } catch (error) {
  829. logger.error('获取用户信息失败:', error);
  830. res.status(500).json({ error: '获取用户信息失败', details: error.message });
  831. }
  832. });
  833. // 修改密码接口
  834. app.post('/api/change-password', requireLogin, async (req, res) => {
  835. const { currentPassword, newPassword } = req.body;
  836. const username = req.session.user.username;
  837. if (!currentPassword || !newPassword) {
  838. return res.status(400).json({ error: '当前密码和新密码不能为空' });
  839. }
  840. try {
  841. const userService = require('./services/userService');
  842. await userService.changePassword(username, currentPassword, newPassword);
  843. res.json({ success: true, message: '密码修改成功' });
  844. } catch (error) {
  845. logger.error(`用户 ${username} 修改密码失败:`, error);
  846. res.status(400).json({ error: error.message || '修改密码失败' }); // 返回具体的错误信息
  847. }
  848. });
  849. // 系统资源兼容路由
  850. app.get('/api/system-resources', requireLogin, async (req, res) => {
  851. try {
  852. const startTime = Date.now();
  853. logger.info('兼容层: 请求 /api/system-resources');
  854. // 获取CPU信息
  855. const cpuCores = os.cpus().length;
  856. const cpuModel = os.cpus()[0].model;
  857. const cpuSpeed = os.cpus()[0].speed;
  858. const loadAvg = os.loadavg();
  859. // 获取内存信息
  860. const totalMem = os.totalmem();
  861. const freeMem = os.freemem();
  862. const usedMem = totalMem - freeMem;
  863. const memoryPercent = ((usedMem / totalMem) * 100).toFixed(1) + '%';
  864. // 获取磁盘信息
  865. let diskCommand = '';
  866. if (process.platform === 'win32') {
  867. diskCommand = 'wmic logicaldisk get size,freespace,caption';
  868. } else {
  869. // 在 macOS 和 Linux 上使用 df 命令
  870. diskCommand = 'df -h /';
  871. }
  872. try {
  873. // 执行磁盘命令
  874. logger.debug(`执行磁盘命令: ${diskCommand}`);
  875. const { stdout } = await execPromise(diskCommand, { timeout: 5000 });
  876. logger.debug(`磁盘命令输出: ${stdout}`);
  877. // 解析磁盘信息
  878. let disk = { size: "未知", used: "未知", available: "未知", percent: "未知" };
  879. if (process.platform === 'win32') {
  880. // Windows解析逻辑不变
  881. // ... (省略Windows解析代码)
  882. } else {
  883. // macOS/Linux格式解析
  884. const lines = stdout.trim().split('\n');
  885. if (lines.length >= 2) {
  886. const headerParts = lines[0].trim().split(/\s+/);
  887. const dataParts = lines[1].trim().split(/\s+/);
  888. logger.debug(`解析磁盘信息, 头部: ${headerParts}, 数据: ${dataParts}`);
  889. // 检查MacOS格式 (通常是Filesystem Size Used Avail Capacity iused ifree %iused Mounted on)
  890. const isMacOS = headerParts.includes('Capacity') && headerParts.includes('iused');
  891. if (isMacOS) {
  892. // macOS格式处理
  893. const fsIndex = 0; // Filesystem
  894. const sizeIndex = 1; // Size
  895. const usedIndex = 2; // Used
  896. const availIndex = 3; // Avail
  897. const percentIndex = 4; // Capacity
  898. const mountedIndex = headerParts.indexOf('Mounted') + 1; // Mounted on
  899. disk = {
  900. filesystem: dataParts[fsIndex],
  901. size: dataParts[sizeIndex],
  902. used: dataParts[usedIndex],
  903. available: dataParts[availIndex],
  904. percent: dataParts[percentIndex],
  905. mountedOn: dataParts[mountedIndex] || '/'
  906. };
  907. } else {
  908. // 标准Linux格式处理 (通常是Filesystem Size Used Avail Use% Mounted on)
  909. const fsIndex = 0; // Filesystem
  910. const sizeIndex = 1; // Size
  911. const usedIndex = 2; // Used
  912. const availIndex = 3; // Avail
  913. const percentIndex = 4; // Use%
  914. const mountedIndex = 5; // Mounted on
  915. disk = {
  916. filesystem: dataParts[fsIndex],
  917. size: dataParts[sizeIndex],
  918. used: dataParts[usedIndex],
  919. available: dataParts[availIndex],
  920. percent: dataParts[percentIndex],
  921. mountedOn: dataParts[mountedIndex] || '/'
  922. };
  923. }
  924. }
  925. }
  926. // 构建最终结果
  927. const result = {
  928. cpu: {
  929. cores: cpuCores,
  930. model: cpuModel,
  931. speed: cpuSpeed,
  932. loadAvg: loadAvg
  933. },
  934. memory: {
  935. total: totalMem,
  936. free: freeMem,
  937. used: usedMem,
  938. percent: memoryPercent
  939. },
  940. disk: disk,
  941. uptime: os.uptime()
  942. };
  943. logger.debug(`系统资源API返回结果: ${JSON.stringify(result)}`);
  944. // 计算处理时间并返回结果
  945. const endTime = Date.now();
  946. logger.info(`兼容层: /api/system-resources 请求完成,耗时 ${endTime - startTime}ms`);
  947. res.json(result);
  948. } catch (diskError) {
  949. // 磁盘信息获取失败时,仍然返回CPU和内存信息
  950. logger.error(`获取磁盘信息失败: ${diskError.message}`);
  951. const result = {
  952. cpu: {
  953. cores: cpuCores,
  954. model: cpuModel,
  955. speed: cpuSpeed,
  956. loadAvg: loadAvg
  957. },
  958. memory: {
  959. total: totalMem,
  960. free: freeMem,
  961. used: usedMem,
  962. percent: memoryPercent
  963. },
  964. disk: { size: "未知", used: "未知", available: "未知", percent: "未知" },
  965. uptime: os.uptime(),
  966. diskError: diskError.message
  967. };
  968. // 计算处理时间并返回结果(即使有错误)
  969. const endTime = Date.now();
  970. logger.info(`兼容层: /api/system-resources 请求完成(但磁盘信息失败),耗时 ${endTime - startTime}ms`);
  971. res.json(result);
  972. }
  973. } catch (error) {
  974. logger.error(`系统资源API错误: ${error.message}`);
  975. res.status(500).json({ error: '获取系统资源信息失败', message: error.message });
  976. }
  977. });
  978. // 登出接口
  979. app.post('/api/logout', (req, res) => {
  980. if (req.session) {
  981. req.session.destroy(err => {
  982. if (err) {
  983. logger.error('销毁会话失败:', err);
  984. return res.status(500).json({ error: '退出登录失败' });
  985. }
  986. // 清除客户端的 connect.sid cookie
  987. res.clearCookie('connect.sid', { path: '/' }); // 确保路径与设置时一致
  988. logger.info('用户已成功登出');
  989. res.json({ success: true, message: '已成功登出' });
  990. });
  991. } else {
  992. // 如果没有会话,也认为登出成功
  993. logger.info('用户已登出(无会话)');
  994. res.json({ success: true, message: '已成功登出' });
  995. }
  996. });
  997. logger.success('API兼容层加载完成');
  998. };