const express = require('express'); const { exec, execSync } = require('child_process'); const util = require('util'); const execPromise = util.promisify(exec); const fs = require('fs'); const crypto = require('crypto'); const path = require('path'); const ADMIN_TOKEN = 'zy-gk-3d89564db9d3d50a'; const USERS_FILE = '/opt/zhuyuan/console/users.json'; const REPO_BASE = '/opt/zhuyuan/repos'; const app = express(); app.use(express.json()); app.use(express.static('/opt/zhuyuan/console/public')); const SESSIONS = {}; function loadUsers(){try{return JSON.parse(fs.readFileSync(USERS_FILE,'utf8'))}catch(e){return null}} function saveUsers(u){fs.writeFileSync(USERS_FILE,JSON.stringify(u,null,2),'utf8')} app.post('/api/login',(req,res)=>{ const{username,password}=req.body; const u=loadUsers();if(!u)return res.status(500).json({error:'no users'}); const user=u[username]; if(!user||user.pass!==password)return res.status(401).json({error:'wrong'}); const token=crypto.randomBytes(16).toString('hex'); SESSIONS[token]={username,...user}; res.json({token,user:{username,display:user.display,role:user.role,repos:user.repos}}); }); app.get('/api/session',(req,res)=>{ const s=SESSIONS[req.headers['x-session-token']]; if(!s)return res.status(401).json({error:'unauth'}); res.json({user:{username:s.username,display:s.display,role:s.role,repos:s.repos}}); }); app.post('/api/change-password',(req,res)=>{ const s=SESSIONS[req.headers['x-session-token']]; if(!s)return res.status(401).json({error:'unauth'}); const{oldPassword,newPassword}=req.body; if(!newPassword||newPassword.length<6)return res.status(400).json({error:'min 6 chars'}); const u=loadUsers();if(!u||!u[s.username])return res.status(500).json({error:'data error'}); if(u[s.username].pass!==oldPassword)return res.status(403).json({error:'old wrong'}); u[s.username].pass=newPassword;saveUsers(u);res.json({ok:true}); }); app.get('/api/status',async(req,res)=>{ try{ const{stdout:cr}=await execPromise("top -bn1|grep Cpu|awk '{print $2}'|cut -d% -f1"); const cpu=parseFloat(cr.trim())||0; const{stdout:mr}=await execPromise("free -m|grep Mem|awk '{print $3,$2,$7}'"); const[used,total,avail]=mr.trim().split(' ').map(Number); const{stdout:dr}=await execPromise("df -h /|tail -1|awk '{print $5,$2,$4}'"); const[du,dt,df]=dr.trim().split(' '); res.json({cpu:{usage:cpu.toFixed(1),status:cpu>80?'warn':cpu>50?'normal':'good'},memory:{used,total,available:avail,percent:((used/total)*100).toFixed(1),status:(used/total)>0.8?'warn':(used/total)>0.5?'normal':'good'},disk:{used:du,total:dt,free:df,status:parseInt(du)>80?'warn':'good'}}); }catch(e){res.status(500).json({error:e.message})} }); app.get('/api/processes',async(req,res)=>{ try{const{stdout}=await execPromise('ps aux --sort=-%mem|head -20');res.json({procs:stdout.trim().split('\n').slice(1).map(l=>{const p=l.trim().split(/\s+/);return{name:p[10],pid:p[1],mem:p[3],cpu:p[2]}})})}catch(e){res.status(500).json({error:e.message})} }); app.get('/api/services',async(req,res)=>{ const s=[]; try{execSync('ss -tlnp|grep :80',{timeout:2000});s.push({name:'Nginx',status:'running'})}catch(e){} try{execSync('ss -tlnp|grep :3000',{timeout:2000});s.push({name:'Dify',status:'running'})}catch(e){} try{JSON.parse(execSync('pm2 jlist 2>/dev/null',{timeout:2000}).toString()).forEach(x=>s.push({name:'PM2:'+x.name,status:x.pm2_env.status}))}catch(e){} res.json({services:s}); }); app.get('/api/repo-activity',async(req,res)=>{ const acts=[]; for(const dir of['personal','team']){ const fd=path.join(REPO_BASE,dir);if(!fs.existsSync(fd))continue; for(const repo of fs.readdirSync(fd)){ const rp=path.join(fd,repo);if(!fs.statSync(rp).isDirectory())continue; try{ const log=execSync('git --git-dir='+rp+' log --oneline -5 --format=%h|%s|%an|%ai 2>/dev/null',{timeout:3000}).toString().trim(); if(log)log.split('\n').forEach(l=>{const[h,m,a,d]=l.split('|');acts.push({repo:dir+'/'+repo,hash:h,msg:m,author:a,date:d})}); }catch(e){} } } acts.sort((a,b)=>new Date(b.date)-new Date(a.date)); res.json({activities:acts.slice(0,20)}); }); app.get('/api/repo-files/:repoPath(*)',(req,res)=>{ const rp2=req.params.repoPath;if(rp2.includes('..'))return res.status(400).json({error:'bad path'}); const rp=path.join(REPO_BASE,rp2);if(!fs.existsSync(rp))return res.status(404).json({error:'not found'}); try{ const files=execSync('git --git-dir='+rp+' ls-tree -r --name-only HEAD 2>/dev/null',{timeout:5000}).toString().trim().split('\n').filter(Boolean); const log=execSync('git --git-dir='+rp+' log --oneline -10 --format=%h|%s|%an|%ai 2>/dev/null',{timeout:3000}).toString().trim(); const commits=log?log.split('\n').map(l=>{const[h,m,a,d]=l.split('|');return{hash:h,msg:m,author:a,date:d}}):[]; res.json({files,commits}); }catch(e){res.json({files:[],commits:[],error:e.message})} }); app.get('/api/heartbeat',(req,res)=>{ res.json({servers:[{id:'HL-CN-001',name:'国内·Awen',ip:'43.139.207.172',status:'online',spec:'4核4GB'},{id:'HL-SG-001',name:'硅谷·Awen',ip:'170.106.72.246',status:'online',spec:'2核2GB'}],gatekeeper:{status:'待部署'}}); }); app.post('/admin/exec',(req,res)=>{ if(req.headers['x-admin-token']!==ADMIN_TOKEN)return res.status(403).json({error:'unauthorized'}); const cmd=req.body.command;if(!cmd||cmd.length>5000)return res.status(400).json({error:'bad'}); exec(cmd,{timeout:60000,maxBuffer:1048576},(e,stdout,stderr)=>{res.json({output:stdout+(stderr||''),error:e?e.message:null})}); }); app.listen(3920,'127.0.0.1',()=>console.log('OK:3920'));