From 874561a9d15a7d6792aad37f7843ce72af7a7751 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:47:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=9B=E7=A8=8B=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=20-=20=E4=BF=A1=E5=8F=B7=E5=8F=91=E9=80=81?= =?UTF-8?q?=E3=80=81=E6=8E=92=E5=BA=8F=E7=AD=9B=E9=80=89=E3=80=81=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=92=8C=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95=20(#1194?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * feat: 实现进程管理增强功能 - 信号发送、排序筛选、搜索和右键菜单 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 修复代码审查问题并删除遗留文件 task/SystemView.vue Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> --- internal/http/request/process.go | 23 ++ internal/route/http.go | 2 + internal/service/process.go | 154 +++++++- pkg/types/process.go | 2 + web/src/api/panel/process/index.ts | 20 +- web/src/components/common/AppFooter.vue | 2 +- web/src/components/page/AppPage.vue | 2 +- web/src/layout/IndexView.vue | 12 +- web/src/layout/header/IndexView.vue | 6 +- web/src/layout/sidebar/IndexView.vue | 2 +- web/src/views/home/IndexView.vue | 4 +- web/src/views/monitor/IndexView.vue | 12 +- web/src/views/task/SystemView.vue | 171 --------- web/src/views/toolbox/BenchmarkView.vue | 2 +- web/src/views/toolbox/ProcessView.vue | 490 +++++++++++++++++++++--- web/src/views/toolbox/WebHookView.vue | 4 +- web/src/views/website/EditView.vue | 2 +- 17 files changed, 646 insertions(+), 264 deletions(-) delete mode 100644 web/src/views/task/SystemView.vue diff --git a/internal/http/request/process.go b/internal/http/request/process.go index fdffcf6d..d7692b93 100644 --- a/internal/http/request/process.go +++ b/internal/http/request/process.go @@ -1,5 +1,28 @@ package request +// ProcessKill 结束进程请求 type ProcessKill struct { PID int32 `json:"pid" validate:"required"` } + +// ProcessDetail 获取进程详情请求 +type ProcessDetail struct { + PID int32 `json:"pid" form:"pid" query:"pid" validate:"required"` +} + +// ProcessSignal 发送信号请求 +// 支持的信号: SIGHUP(1), SIGINT(2), SIGKILL(9), SIGUSR1(10), SIGUSR2(12), SIGTERM(15), SIGCONT(18), SIGSTOP(19) +type ProcessSignal struct { + PID int32 `json:"pid" validate:"required"` + Signal int `json:"signal" validate:"required|in:1,2,9,10,12,15,18,19"` +} + +// ProcessList 进程列表请求 +type ProcessList struct { + Page uint `json:"page" form:"page" query:"page"` + Limit uint `json:"limit" form:"limit" query:"limit"` + Sort string `json:"sort" form:"sort" query:"sort"` // pid, name, cpu, rss, start_time + Order string `json:"order" form:"order" query:"order"` // asc, desc + Status string `json:"status" form:"status" query:"status"` // R, S, T, I, Z, W, L + Keyword string `json:"keyword" form:"keyword" query:"keyword"` +} diff --git a/internal/route/http.go b/internal/route/http.go index b4b21103..f6c4b643 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -309,7 +309,9 @@ func (route *Http) Register(r *chi.Mux) { r.Route("/process", func(r chi.Router) { r.Get("/", route.process.List) + r.Get("/detail", route.process.Detail) r.Post("/kill", route.process.Kill) + r.Post("/signal", route.process.Signal) }) r.Route("/safe", func(r chi.Router) { diff --git a/internal/service/process.go b/internal/service/process.go index c3cfeefd..30d473d5 100644 --- a/internal/service/process.go +++ b/internal/service/process.go @@ -3,6 +3,10 @@ package service import ( "net/http" "slices" + "sort" + "strconv" + "strings" + "syscall" "time" "github.com/libtnb/chix" @@ -20,22 +24,103 @@ func NewProcessService() *ProcessService { } func (s *ProcessService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ProcessList](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + // 设置默认值 + if req.Page == 0 { + req.Page = 1 + } + if req.Limit == 0 { + req.Limit = 20 + } + if req.Order == "" { + req.Order = "asc" + } + processes, err := process.Processes() if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return } - data := make([]types.ProcessData, 0) + data := make([]types.ProcessData, 0, len(processes)) for proc := range slices.Values(processes) { - data = append(data, s.processProcess(proc)) + procData := s.processProcessBasic(proc) + + // 状态筛选 + if req.Status != "" && procData.Status != req.Status { + continue + } + + // 关键词搜索(按 PID 或进程名) + if req.Keyword != "" { + keyword := strings.ToLower(req.Keyword) + pidStr := strconv.FormatInt(int64(procData.PID), 10) + nameMatch := strings.Contains(strings.ToLower(procData.Name), keyword) + pidMatch := strings.Contains(pidStr, keyword) + if !nameMatch && !pidMatch { + continue + } + } + + data = append(data, procData) } - paged, total := Paginate(r, data) + // 排序 + if req.Sort != "" { + s.sortProcesses(data, req.Sort, req.Order) + } + + // 分页 - 使用 int64 避免溢出 + total := uint(len(data)) + start := uint64(req.Page-1) * uint64(req.Limit) + end := uint64(req.Page) * uint64(req.Limit) + + if start > uint64(total) { + data = []types.ProcessData{} + } else { + if end > uint64(total) { + end = uint64(total) + } + data = data[start:end] + } Success(w, chix.M{ "total": total, - "items": paged, + "items": data, + }) +} + +// sortProcesses 对进程列表进行排序 +func (s *ProcessService) sortProcesses(data []types.ProcessData, sortBy, order string) { + sort.Slice(data, func(i, j int) bool { + var less bool + switch sortBy { + case "pid": + less = data[i].PID < data[j].PID + case "name": + less = strings.ToLower(data[i].Name) < strings.ToLower(data[j].Name) + case "cpu": + less = data[i].CPU < data[j].CPU + case "rss": + less = data[i].RSS < data[j].RSS + case "start_time": + less = data[i].StartTime < data[j].StartTime + case "ppid": + less = data[i].PPID < data[j].PPID + case "num_threads": + less = data[i].NumThreads < data[j].NumThreads + default: + less = data[i].PID < data[j].PID + } + if order == "desc" { + return !less + } + return less }) } @@ -60,8 +145,49 @@ func (s *ProcessService) Kill(w http.ResponseWriter, r *http.Request) { Success(w, nil) } -// processProcess 处理进程数据 -func (s *ProcessService) processProcess(proc *process.Process) types.ProcessData { +// Signal 向进程发送信号 +func (s *ProcessService) Signal(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ProcessSignal](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + proc, err := process.NewProcess(req.PID) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + if err = proc.SendSignal(syscall.Signal(req.Signal)); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +// Detail 获取进程详情 +func (s *ProcessService) Detail(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ProcessDetail](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + proc, err := process.NewProcess(req.PID) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + data := s.processProcessFull(proc) + + Success(w, data) +} + +// processProcessBasic 处理进程基本数据(用于列表,减少数据获取) +func (s *ProcessService) processProcessBasic(proc *process.Process) types.ProcessData { data := types.ProcessData{ PID: proc.Pid, } @@ -83,6 +209,18 @@ func (s *ProcessService) processProcess(proc *process.Process) types.ProcessData } data.NumThreads, _ = proc.NumThreads() data.CPU, _ = proc.CPUPercent() + if mem, err := proc.MemoryInfo(); err == nil { + data.RSS = mem.RSS + } + + return data +} + +// processProcessFull 处理进程完整数据(用于详情) +func (s *ProcessService) processProcessFull(proc *process.Process) types.ProcessData { + data := s.processProcessBasic(proc) + + // 获取更多内存信息 if mem, err := proc.MemoryInfo(); err == nil { data.RSS = mem.RSS data.Data = mem.Data @@ -106,5 +244,9 @@ func (s *ProcessService) processProcess(proc *process.Process) types.ProcessData data.OpenFiles = slices.Compact(data.OpenFiles) data.Envs = slices.Compact(data.Envs) + // 获取可执行文件路径和工作目录 + data.Exe, _ = proc.Exe() + data.Cwd, _ = proc.Cwd() + return data } diff --git a/pkg/types/process.go b/pkg/types/process.go index 21dae912..8d1f36cb 100644 --- a/pkg/types/process.go +++ b/pkg/types/process.go @@ -20,6 +20,8 @@ type ProcessData struct { DiskWrite uint64 `json:"disk_write"` CmdLine string `json:"cmd_line"` + Exe string `json:"exe"` // 可执行文件路径 + Cwd string `json:"cwd"` // 工作目录 RSS uint64 `json:"rss"` VMS uint64 `json:"vms"` diff --git a/web/src/api/panel/process/index.ts b/web/src/api/panel/process/index.ts index 9bb2fd21..32bcb695 100644 --- a/web/src/api/panel/process/index.ts +++ b/web/src/api/panel/process/index.ts @@ -1,8 +1,22 @@ import { http } from '@/utils' +export interface ProcessListParams { + page: number + limit: number + sort?: string // pid, name, cpu, rss, start_time + order?: string // asc, desc + status?: string // R, S, T, I, Z, W, L + keyword?: string +} + export default { // 获取进程列表 - list: (page: number, limit: number) => http.Get(`/process`, { params: { page, limit } }), - // 杀死进程 - kill: (pid: number) => http.Post(`/process/kill`, { pid }) + list: (params: ProcessListParams) => + http.Get(`/process`, { params }), + // 获取进程详情 + detail: (pid: number) => http.Get(`/process/detail`, { params: { pid } }), + // 杀死进程 (SIGKILL) + kill: (pid: number) => http.Post(`/process/kill`, { pid }), + // 向进程发送信号 + signal: (pid: number, signal: number) => http.Post(`/process/signal`, { pid, signal }) } diff --git a/web/src/components/common/AppFooter.vue b/web/src/components/common/AppFooter.vue index bf627fd4..a021be14 100644 --- a/web/src/components/common/AppFooter.vue +++ b/web/src/components/common/AppFooter.vue @@ -6,7 +6,7 @@ const year = new Date().getFullYear()