> 一键启动 / 停止本地 LLM 模型,实时监控系统资源,支持模糊匹配自动发现 BAT 文件。

---

## 项目简介

**Llama 模型管理器** 是一个基于 WinForms (.NET Framework 4.8) 的桌面应用程序,专门用于管理本地 AI 推理服务器。如果你手头有多个 GGUF/GGML 格式的本地模型,每个模型对应一个 `.bat` 启动脚本,那么这个工具就是你的"一站式控制台"。

### 核心功能

| 功能 | 说明 |

|:---|:---|

| 模型卡片列表 | 每个模型一张卡片,显示名称、端口号、运行状态 |

| 一键启动/停止 | 点击 RUN/HALT 按钮,自动查找 BAT 文件并启动进程 |

| 模糊匹配 | 文件名不精确匹配时自动按模型名关键词查找 |

| 智能滚动条 | 卡片超出可视区时自动出现滚动条 |

| 系统监控 | 实时显示 CPU / GPU / 内存 / 磁盘占用率 |

| 日志面板 | 颜色区分的运行日志,自动滚动,上限 1000 条 |

| 进程树终止 | 停止模型时递归杀死所有子进程 |

| 端口检测 | 启动前检查端口是否已被占用 |

| 内嵌资源 | Logo 和图标已嵌入 EXE,单文件运行无需额外依赖 |

---

## 项目结构

```

D:\Qwen3XL\

├── LlamaManager.exe      ← 主程序(双击运行,64KB)

├── MainForm.cs           ← 完整源码(约 24KB,700 行)

├── app.ico               ← 图标源文件

├── logo.png              ← Logo 图片源文件

├── DeepSeek-R1-14B服务器.bat

├── Gemma-4-26B 推理服务器.bat

├── Gemma-4-31B服务器.bat

├── ...                   ← 各模型的启动脚本(共 13 个)

├── Qwopus-9B-Coder服务器.bat

└── Llama模型管理器.lnk    ← 桌面快捷方式

```

---

## 技术架构

```

                 ┌─────────────────────────────┐

                 │      Llama 模型管理器        │

                 │      (WinForms 窗体)         │

                 ├──────────────┬──────────────┤

                 │  左侧面板    │  右侧面板     │

                 │  ┌────────┐  │  ┌────────┐  │

                 │  │ MODELS │  │  │  LOG   │  │

                 │  │ (卡片)  │  │  │(日志)  │  │

                 │  │ 手动   │  │  │ 颜色   │  │

                 │  │ 滚动条 │  │  │ 标记   │  │

                 │  └────────┘  │  └────────┘  │

                 ├──────────────┴──────────────┤

                 │  状态栏: 活跃数 / 路径       │

                 └─────────────────────────────┘

```

- **.NET Framework 4.8** — 利用 WinForms 原生控件

- **System.Management** — WMI 查询 CPU/内存/进程树

- **nvidia-smi** — 获取 GPU 利用率和温度

- **PerformanceCounter** — 实时 CPU 占用率

- **手动 Panel 布局** — 替代 FlowLayoutPanel,消除渲染偏差

---

## 完整代码

```csharp

using System;

using System.Diagnostics;

using System.Drawing;

using System.IO;

using System.Linq;

using System.Management;

using System.Net.NetworkInformation;

using System.Windows.Forms;

class ModelInfo

{

    public string Name { get; set; }

    public string BatName { get; set; }

    public string DisplayName { get; set; }

    public int Port { get; set; }

    public Process Proc { get; set; }

    public DateTime StartTime { get; set; }

    public Label StatusLabel { get; set; }

    public Button Btn { get; set; }

    public Panel Card { get; set; }

}

class MainForm : Form

{

    private ListBox globalLog;

    private Timer statsTimer;

    private Label cpuVal, gpuVal, memVal, diskVal;

    private Label upTime, statusLabel, deckCount;

    private Panel cardContainer;

    private VScrollBar cardScroll;

    private int scrollOffset = 0;

    private Label cardCountLabel;

    private readonly string baseDir = @"D:\Qwen3XL";

    private ModelInfo[] models;

    private PerformanceCounter cpuCounter, memCounter;

    private readonly Stopwatch uptimeWatch = Stopwatch.StartNew();

    private const int CardWidth = 297;

    private const int CardHeight = 52;

    private const int CardGap = 8;

    private static class Colors

    {

        public static readonly Color Bg = Color.FromArgb(12, 12, 20);

        public static readonly Color PanelBg = Color.FromArgb(22, 22, 38);

        public static readonly Color CardBg = Color.FromArgb(28, 28, 48);

        public static readonly Color CardHover = Color.FromArgb(38, 38, 68);

        public static readonly Color Green = Color.FromArgb(0, 220, 150);

        public static readonly Color GreenDk = Color.FromArgb(0, 180, 120);

        public static readonly Color Red = Color.FromArgb(240, 80, 80);

        public static readonly Color Yellow = Color.FromArgb(255, 180, 80);

        public static readonly Color Text = Color.FromArgb(240, 240, 255);

        public static readonly Color Sub = Color.FromArgb(140, 140, 160);

        public static readonly Color Dim = Color.FromArgb(90, 90, 110);

        public static readonly Color Border = Color.FromArgb(45, 45, 70);

        public const int LeftWidth = 355;

    }

    public MainForm()

    {

        SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);

        Text = "llama\u6a21\u578b\u7ba1\u7406\u5668";

        Size = new Size(1150, 920);

        MinimumSize = new Size(850, 760);

        StartPosition = FormStartPosition.CenterScreen;

        BackColor = Colors.Bg;

        Font = new Font("Segoe UI", 10, FontStyle.Regular);

        InitCounters();

        LoadModels();

        BuildUI();

        statsTimer = new Timer { Interval = 2000 };

        statsTimer.Tick += (st, et) => UpdateStats();

        statsTimer.Start();

        Shown += (st, et) => { UpdateScroll(); LayoutCards(); };

        FormClosing += (st, et) => { StopAll(); statsTimer.Stop(); };

        Resize += (st, et) => { if (WindowState == FormWindowState.Minimized) statsTimer.Stop(); else if (!statsTimer.Enabled) statsTimer.Start(); };

    }

    private void InitCounters()

    {

        try

        {

            cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); cpuCounter.NextValue();

            memCounter = new PerformanceCounter("Memory", "% Committed Bytes In Use"); memCounter.NextValue();

        }

        catch { }

    }

    private void LoadModels()

    {

        models = new ModelInfo[] {

            NewModel("Gemma-4-26B", "Gemma-4-26B 推理服务器.bat", "Gemma 4 26B", 8082),

            NewModel("Gemma-4-31B", "Gemma-4-31B服务器.bat", "Gemma 4 31B", 8083),

            NewModel("Gemma-4-E2B", "Gemma-4-E2B服务器.bat", "Gemma 4 E2B", 8084),

            NewModel("Gemma-4-E4B", "Gemma-4-E4B服务器.bat", "Gemma 4 E4B", 8085),

            NewModel("Granite-4.1-3B-Q4", "Granite-4.1-3B-Q4服务器.bat", "Granite 4.1 3B Q4", 8086),

            NewModel("Granite-4.1-8B", "Granite-4.1-8B服务器.bat", "Granite 4.1 8B", 8087),

            NewModel("Qwen3.5-0.8B-BF16", "Qwen3.5-0.8B-BF16服务器.bat", "Qwen 3.5 0.8B BF16", 8088),

            NewModel("Qwen3.5-2B-BF16", "Qwen3.5-2B-BF16服务器.bat", "Qwen 3.5 2B BF16", 8089),

            NewModel("Qwen3.5-4B-Q4", "Qwen3.5-4B-Q4服务器.bat", "Qwen 3.5 4B Q4", 8090),

            NewModel("Qwen3.5-4B-Q8", "Qwen3.5-4B-Q8服务器.bat", "Qwen 3.5 4B Q8", 8091),

            NewModel("Qwen3.6-27B-Q4", "Qwen3.6-27B-Q4服务器.bat", "Qwen 3.6 27B Q4", 8092),

            NewModel("Qwen3.6-35B-Uncensored", "Qwen3.6-35B-Uncensored服务器.bat", "Qwen 3.6 35B Uncensored", 8093),

            NewModel("Qwopus-9B-Coder", "Qwopus-9B-Coder服务器.bat", "Qwopus 9B Coder", 8094),

        };

    }

    private ModelInfo NewModel(string name, string bat, string display, int port)

        => new ModelInfo { Name = name, BatName = bat, DisplayName = display, Port = port };

    private string FindBat(ModelInfo m)

    {

        string exact = Path.Combine(baseDir, m.BatName);

        if (File.Exists(exact)) return exact;

        if (Directory.Exists(baseDir))

        {

            try

            {

                foreach (string f in Directory.GetFiles(baseDir, "*.bat"))

                    if (Path.GetFileName(f).IndexOf(m.Name, StringComparison.OrdinalIgnoreCase) >= 0)

                        return f;

            }

            catch { }

        }

        return null;

    }

    private void BuildUI()

    {

        BuildTop();

        var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 1, BackColor = Colors.Bg };

        layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, Colors.LeftWidth));

        layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));

        layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));

        var left = new Panel { Dock = DockStyle.Fill, Padding = new Padding(6, 4, 6, 4) };

        var right = new Panel { Dock = DockStyle.Fill, Padding = new Padding(6, 4, 6, 4) };

        BuildRight(right);

        BuildLeft(left);

        layout.Controls.Add(left, 0, 0);

        layout.Controls.Add(right, 1, 0);

        Controls.Add(layout);

        BuildBottom();

    }

    private void BuildTop()

    {

        var top = new Panel { Dock = DockStyle.Top, Height = 64, BackColor = Colors.PanelBg, Padding = new Padding(12, 6, 12, 6) };

        top.Paint += (st, et) => { using (var p = new Pen(Color.FromArgb(100, Colors.Green), 2)) et.Graphics.DrawLine(p, 0, top.Height - 1, top.Width, top.Height - 1); };

        try

        {

            var asm = System.Reflection.Assembly.GetExecutingAssembly();

            using (var s = asm.GetManifestResourceStream("logo.png"))

                if (s != null) top.Controls.Add(new PictureBox { Image = Image.FromStream(s), SizeMode = PictureBoxSizeMode.Zoom, Location = new Point(4, 2), Size = new Size(170, 54), BackColor = Color.Transparent });

        }

        catch { }

        deckCount = new Label { Text = "", ForeColor = Colors.Sub, Font = new Font("Consolas", 8), Location = new Point(182, 42), AutoSize = true };

        top.Controls.Add(deckCount);

        var sf = new FlowLayoutPanel { Anchor = AnchorStyles.Top | AnchorStyles.Right, Location = new Point(360, 6), Size = new Size(460, 46), BackColor = Color.Transparent, WrapContents = false };

        sf.Controls.AddRange(new Control[] { StatBox("CPU", out cpuVal), StatBox("GPU", out gpuVal), StatBox("MEM", out memVal), StatBox("DSK", out diskVal) });

        top.Controls.Add(sf);

        upTime = new Label { Text = "UPTIME: --", ForeColor = Colors.Sub, Font = new Font("Consolas", 8), Location = new Point(360, 46), AutoSize = true };

        top.Controls.Add(upTime);

        Controls.Add(top);

    }

    private Panel StatBox(string title, out Label val)

    {

        val = new Label { Text = "--", ForeColor = Colors.Green, Font = new Font("Consolas", 12, FontStyle.Bold), Location = new Point(8, 22), AutoSize = true };

        var box = new Panel { Size = new Size(130, 42), Margin = new Padding(4, 0, 4, 0), BackColor = Color.FromArgb(35, 35, 58) };

        box.Paint += (st, et) => { using (var p = new Pen(Colors.Border, 1)) et.Graphics.DrawRectangle(p, 0, 0, box.Width - 1, box.Height - 1); };

        box.Controls.Add(new Label { Text = title, ForeColor = Colors.Dim, Font = new Font("Consolas", 8), Location = new Point(8, 4), AutoSize = true });

        box.Controls.Add(val);

        return box;

    }

    private void BuildLeft(Control panel)

    {

        panel.BackColor = Colors.Bg;

        var hdr = new Panel { Dock = DockStyle.Top, Height = 26, BackColor = Colors.Bg };

        hdr.Controls.Add(new Label { Text = "MODELS", Font = new Font("Consolas", 10, FontStyle.Bold), ForeColor = Colors.Green, Location = new Point(4, 2), AutoSize = true });

        cardCountLabel = new Label { Text = "", ForeColor = Colors.Yellow, Font = new Font("Consolas", 8), Location = new Point(100, 5), AutoSize = true };

        hdr.Controls.Add(cardCountLabel);

        panel.Controls.Add(hdr);

        cardScroll = new VScrollBar { Dock = DockStyle.Right, Visible = false };

        cardScroll.Scroll += (sv, ev) => { scrollOffset = ev.NewValue; LayoutCards(); };

        panel.Controls.Add(cardScroll);

        cardContainer = new Panel { Dock = DockStyle.Fill, BackColor = Colors.Bg };

        panel.Controls.Add(cardContainer);

        panel.Resize += (sp, ep) => { UpdateScroll(); LayoutCards(); };

        BuildCards();

    }

    private void UpdateScroll()

    {

        int total = models.Length * (CardHeight + CardGap) + CardGap;

        int view = cardContainer.ClientSize.Height;

        if (total > view && view > 0)

        {

            cardScroll.Visible = true;

            cardScroll.Minimum = 0;

            cardScroll.Maximum = total;

            cardScroll.LargeChange = view;

            cardScroll.SmallChange = CardHeight + CardGap;

        }

        else { cardScroll.Visible = false; scrollOffset = 0; }

    }

    private void LayoutCards()

    {

        cardContainer.SuspendLayout();

        int y = CardGap - scrollOffset;

        foreach (Control c in cardContainer.Controls) { c.Top = y; c.Left = CardGap; y += CardHeight + CardGap; }

        cardContainer.ResumeLayout();

    }

    private void BuildCards()

    {

        cardContainer.Controls.Clear();

        int ok = 0, miss = 0;

        for (int i = 0; i < models.Length; i++)

        {

            ModelInfo m = models[i];

            var card = CreateCard(m);

            m.Card = card;

            cardContainer.Controls.Add(card);

            if (m.Btn != null && m.Btn.Enabled) ok++; else miss++;

        }

        UpdateScroll();

        LayoutCards();

        if (deckCount != null) deckCount.Text = ok + " ready / " + miss + " missing";

        if (cardCountLabel != null) cardCountLabel.Text = cardContainer.Controls.Count + "/" + models.Length + " cards";

    }

    private Panel CreateCard(ModelInfo m)

    {

        var card = new Panel { Size = new Size(CardWidth, CardHeight), BackColor = Colors.CardBg };

        card.MouseEnter += (st, et) => { if (m.Proc == null) card.BackColor = Colors.CardHover; };

        card.MouseLeave += (st, et) => { if (m.Proc == null) card.BackColor = Colors.CardBg; };

        bool exists = FindBat(m) != null;

        m.StatusLabel = new Label { Text = "*", ForeColor = exists ? Colors.Sub : Color.FromArgb(100, 50, 50), Font = new Font("Segoe UI", 14), Location = new Point(8, 12), Size = new Size(18, 22), TextAlign = ContentAlignment.MiddleCenter };

        card.Controls.Add(m.StatusLabel);

        card.Controls.Add(new Label { Text = m.DisplayName, ForeColor = Colors.Text, Font = new Font("Segoe UI", 9, FontStyle.Bold), Location = new Point(30, 8), Size = new Size(145, 18), AutoEllipsis = true });

        card.Controls.Add(new Label { Text = exists ? ":" + m.Port : "MISSING", ForeColor = exists ? Colors.Sub : Colors.Red, Font = new Font("Consolas", 7), Location = new Point(30, 26), Size = new Size(80, 14) });

        m.Btn = new Button { Text = "RUN", FlatStyle = FlatStyle.Flat, BackColor = exists ? Colors.Green : Color.FromArgb(60, 60, 80), ForeColor = Color.White, Font = new Font("Consolas", 8, FontStyle.Bold), Location = new Point(CardWidth - 90, 8), Size = new Size(80, 30), Enabled = exists, Tag = m, Cursor = Cursors.Hand };

        m.Btn.FlatAppearance.BorderSize = 1;

        m.Btn.FlatAppearance.BorderColor = exists ? Colors.Green : Colors.Dim;

        m.Btn.FlatAppearance.MouseOverBackColor = exists ? Colors.GreenDk : Color.FromArgb(70, 70, 90);

        m.Btn.FlatAppearance.MouseDownBackColor = exists ? Color.FromArgb(0, 150, 100) : Color.FromArgb(50, 50, 70);

        m.Btn.Click += BtnClick;

        card.Controls.Add(m.Btn);

        return card;

    }

    private void BtnClick(object sender, EventArgs e)

    {

        var m = ((Button)sender).Tag as ModelInfo;

        if (m == null) return;

        if (m.Proc == null || m.Proc.HasExited) StartModel(m); else StopModel(m);

    }

    private void BuildRight(Control panel)

    {

        panel.BackColor = Colors.Bg;

        panel.Controls.Add(new Label { Text = "LOG", Font = new Font("Consolas", 10, FontStyle.Bold), ForeColor = Color.FromArgb(0, 200, 255), Dock = DockStyle.Top, Height = 24, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(4, 0, 0, 0) });

        globalLog = new ListBox { Dock = DockStyle.Fill, BackColor = Color.FromArgb(8, 8, 12), ForeColor = Colors.Text, Font = new Font("Consolas", 9), BorderStyle = BorderStyle.None, IntegralHeight = false, HorizontalScrollbar = true };

        panel.Controls.Add(globalLog);

        var tb = new Panel { Dock = DockStyle.Bottom, Height = 30, BackColor = Colors.PanelBg, Padding = new Padding(4, 2, 4, 2) };

        var clr = new Button { Text = "CLR", FlatStyle = FlatStyle.Flat, ForeColor = Colors.Sub, BackColor = Color.FromArgb(30, 25, 25), Location = new Point(0, 2), Size = new Size(56, 22), Font = new Font("Consolas", 8), Cursor = Cursors.Hand };

        clr.Click += (st, et) => globalLog.Items.Clear(); tb.Controls.Add(clr);

        var halt = new Button { Text = "HALT", FlatStyle = FlatStyle.Flat, ForeColor = Colors.Red, BackColor = Color.FromArgb(45, 20, 20), Location = new Point(62, 2), Size = new Size(56, 22), Font = new Font("Consolas", 8), Cursor = Cursors.Hand };

        halt.Click += (st, et) => StopAll(); tb.Controls.Add(halt);

        var rel = new Button { Text = "RELOAD", FlatStyle = FlatStyle.Flat, ForeColor = Colors.Yellow, BackColor = Color.FromArgb(45, 40, 20), Location = new Point(124, 2), Size = new Size(56, 22), Font = new Font("Consolas", 8), Cursor = Cursors.Hand };

        rel.Click += (st, et) => { BuildCards(); Log("[OK] Reloaded"); }; tb.Controls.Add(rel);

        panel.Controls.Add(tb);

    }

    private void BuildBottom()

    {

        var bar = new Panel { Dock = DockStyle.Bottom, Height = 24, BackColor = Colors.PanelBg };

        bar.Paint += (st, et) => { using (var p = new Pen(Colors.Border, 1)) et.Graphics.DrawLine(p, 0, 0, bar.Width, 0); };

        statusLabel = new Label { Text = "Ready", ForeColor = Colors.Dim, Font = new Font("Consolas", 8), Location = new Point(10, 4), AutoSize = true };

        bar.Controls.Add(statusLabel);

        var quit = new Button { Text = "QUIT", FlatStyle = FlatStyle.Flat, ForeColor = Colors.Red, BackColor = Color.FromArgb(30, 20, 20), Anchor = AnchorStyles.Right, Location = new Point(bar.Width - 60, 2), Size = new Size(50, 20), Font = new Font("Consolas", 7, FontStyle.Bold), Cursor = Cursors.Hand };

        quit.FlatAppearance.BorderSize = 1; quit.FlatAppearance.BorderColor = Colors.Red;

        quit.Click += (st, et) => Close(); bar.Controls.Add(quit);

        Controls.Add(bar);

    }

    private void StartModel(ModelInfo m)

    {

        string bat = FindBat(m);

        if (bat == null) { Log("[ERR] BAT not found: " + m.DisplayName, LogLevel.Err); return; }

        if (PortBusy(m.Port)) { Log("[WRN] Port " + m.Port + " in use", LogLevel.Warn); return; }

        try

        {

            var psi = new ProcessStartInfo { FileName = bat, WorkingDirectory = Path.GetDirectoryName(bat), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = System.Text.Encoding.UTF8, StandardErrorEncoding = System.Text.Encoding.UTF8 };

            var p = new Process { StartInfo = psi, EnableRaisingEvents = true };

            m.Proc = p; m.StartTime = DateTime.Now;

            p.OutputDataReceived += (st, ev) => SafeRun(() => { if (!string.IsNullOrEmpty(ev.Data)) Log("[" + m.DisplayName + "] " + ev.Data); });

            p.ErrorDataReceived += (st, ev) => SafeRun(() => { if (!string.IsNullOrEmpty(ev.Data)) Log("[" + m.DisplayName + "] ! " + ev.Data, LogLevel.Warn); });

            p.Exited += (st, et) => SafeRun(() => { Log("[" + m.DisplayName + "] exited"); CardUI(m, false); if (m.Proc != null) m.Proc.Dispose(); m.Proc = null; });

            p.Start(); p.BeginOutputReadLine(); p.BeginErrorReadLine();

            CardUI(m, true);

            Log("[OK] " + m.DisplayName + " started (PID:" + p.Id + " Port:" + m.Port + ")");

        }

        catch (Exception ex) { Log("[ERR] " + m.DisplayName + " failed: " + ex.Message, LogLevel.Err); m.Proc = null; }

    }

    private void StopModel(ModelInfo m)

    {

        if (m.Proc == null || m.Proc.HasExited) return;

        try { KillTree(m.Proc.Id); if (!m.Proc.WaitForExit(5000)) m.Proc.Kill(); m.Proc.Dispose(); m.Proc = null; CardUI(m, false); Log("[HALT] " + m.DisplayName + " stopped"); }

        catch (Exception ex) { Log("[ERR] Stop failed: " + ex.Message, LogLevel.Err); }

    }

    private void CardUI(ModelInfo m, bool running)

    {

        if (m.StatusLabel != null) m.StatusLabel.ForeColor = running ? Colors.Green : Colors.Sub;

        if (m.Btn != null) { m.Btn.Text = running ? "HALT" : "RUN"; m.Btn.BackColor = running ? Colors.Red : Colors.Green; }

        if (m.Card != null) m.Card.Refresh();

    }

    private void KillTree(int pid)

    {

        try { using (var s = new ManagementObjectSearcher("SELECT ProcessId FROM Win32_Process WHERE ParentProcessId=" + pid)) { foreach (var o in s.Get()) KillTree(Convert.ToInt32(o["ProcessId"])); } var p = Process.GetProcessById(pid); if (!p.HasExited) p.Kill(); } catch { }

    }

    private void StopAll() { foreach (var m in models) StopModel(m); Log("[HALT] All stopped"); }

    private bool PortBusy(int port)

    {

        try { var p = IPGlobalProperties.GetIPGlobalProperties(); return p.GetActiveTcpListeners().Any(e => e.Port == port) || p.GetActiveUdpListeners().Any(e => e.Port == port); }

        catch { return false; }

    }

    private void SafeRun(Action a) { if (!IsDisposed && !Disposing && globalLog != null) { if (globalLog.InvokeRequired) globalLog.BeginInvoke(a); else a(); } }

    private enum LogLevel { Info, Warn, Err }

    private void Log(string msg, LogLevel level = LogLevel.Info)

    {

        SafeRun(() =>

        {

            string ts = DateTime.Now.ToString("HH:mm:ss");

            Color c = level == LogLevel.Warn ? Colors.Yellow : level == LogLevel.Err ? Colors.Red : Colors.Sub;

            globalLog.Items.Add(new LogItem("[" + ts + "] " + msg, c));

            globalLog.TopIndex = globalLog.Items.Count - 1;

            while (globalLog.Items.Count > 1000) globalLog.Items.RemoveAt(0);

        });

    }

    private class LogItem { public string T; public Color C; public LogItem(string t, Color c) { T = t; C = c; } public override string ToString() { return T; } }

    protected override void OnHandleCreated(EventArgs e)

    {

        base.OnHandleCreated(e);

        globalLog.DrawMode = DrawMode.OwnerDrawFixed;

        globalLog.DrawItem += (st, args) =>

        {

            if (args.Index < 0) return;

            var it = globalLog.Items[args.Index] as LogItem;

            if (it == null) return;

            args.DrawBackground();

            using (var b = new SolidBrush(it.C)) args.Graphics.DrawString(it.T, globalLog.Font, b, new PointF(args.Bounds.X + 2, args.Bounds.Y + 2));

            args.DrawFocusRectangle();

        };

    }

    private void UpdateStats()

    {

        try { CPU(); GPU(); MEM(); DSK(); Uptime(); Title(); Timers(); Bar(); }

        catch { }

    }

    private void CPU() {

        float v = cpuCounter != null ? cpuCounter.NextValue() : 0;

        cpuVal.Text = v.ToString("F1") + "%";

        cpuVal.ForeColor = v > 80 ? Colors.Red : v > 50 ? Colors.Yellow : Colors.Green;

    }

    private void GPU() {

        try {

            var psi = new ProcessStartInfo("nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu --format=csv,noheader,nounits") { UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };

            using (var p = Process.Start(psi))

                if (p != null && p.WaitForExit(2000)) {

                    var parts = p.StandardOutput.ReadToEnd().Trim().Split(',');

                    if (parts.Length >= 2) { float u = float.Parse(parts[0].Trim()), t = float.Parse(parts[1].Trim());

                        gpuVal.Text = u.ToString("F0") + "% " + t.ToString("F0") + "C";

                        gpuVal.ForeColor = u > 80 ? Colors.Red : u > 50 ? Colors.Yellow : Colors.Green; return; }

                }

        } catch { }

        gpuVal.Text = "N/A";

    }

    private void MEM() {

        float v = 0; using (var s = new ManagementObjectSearcher("SELECT TotalVisibleMemorySize,FreePhysicalMemory FROM Win32_OperatingSystem"))

            foreach (var o in s.Get()) { ulong t = Convert.ToUInt64(o["TotalVisibleMemorySize"]), f = Convert.ToUInt64(o["FreePhysicalMemory"]); v = (float)((t - f) * 100.0 / t); }

        memVal.Text = v.ToString("F0") + "%"; memVal.ForeColor = v > 80 ? Colors.Red : v > 50 ? Colors.Yellow : Colors.Green;

    }

    private void DSK() {

        float v = 0; foreach (var d in DriveInfo.GetDrives()) if (d.IsReady && d.Name == "C:\\") { v = (float)((d.TotalSize - d.TotalFreeSpace) * 100.0 / d.TotalSize); break; }

        diskVal.Text = v.ToString("F0") + "%"; diskVal.ForeColor = v > 80 ? Colors.Red : v > 50 ? Colors.Yellow : Colors.Green;

    }

    private void Uptime() { var u = uptimeWatch.Elapsed; upTime.Text = string.Format("UPTIME: {0}d {1:D2}h {2:D2}m", u.Days, u.Hours, u.Minutes); }

    private void Bar() { int r = 0; foreach (var m in models) if (m.Proc != null && !m.Proc.HasExited) r++; statusLabel.Text = r + "/" + models.Length + " active  |  " + baseDir; }

    private void Timers() {

        foreach (var m in models)

            if (m.Proc != null && !m.Proc.HasExited && m.Btn != null) {

                var el = DateTime.Now - m.StartTime;

                m.Btn.Text = string.Format("{0:D2}:{1:D2}:{2:D2}", el.Hours, el.Minutes, el.Seconds);

            }

    }

    private void Title() {

        int c = 0, cards = 0; foreach (var m in models) { if (m.Proc != null && !m.Proc.HasExited) c++; if (m.Card != null) cards++; }

        Text = c > 0 ? string.Format("llama\u6a21\u578b\u7ba1\u7406\u5668 [{0}/{1}crds] ACTIVE:{2}", cards, models.Length, c) : string.Format("llama\u6a21\u578b\u7ba1\u7406\u5668 [{0}/{1}crds]", cards, models.Length);

    }

    [STAThread]

    static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); }

}

```

---

## 编译方法

```bash

# .NET Framework 4.8 编译器(Windows 自带)

csc /target:winexe /win32icon:app.ico /resource:logo.png /reference:System.Management.dll /reference:System.Drawing.dll /reference:System.Windows.Forms.dll /out:LlamaManager.exe MainForm.cs

```

需要将 `logo.png` 和 `app.ico` 放在与 `MainForm.cs` 同目录下。

---

## 使用方式

1. 将所有模型的 `.bat` 启动脚本放入 `D:\Qwen3XL\`

2. 双击 `LlamaManager.exe`

3. 看到卡片列表后,点击 **RUN** 启动任意模型

4. 状态指示灯从灰色变为绿色,按钮变为 **HALT**

5. 右侧日志区显示模型输出,底部状态栏显示活跃数

6. 点击 **HALT** 停止模型,进程树会被递归杀死

---

## 设计思路

**为什么不用 FlowLayoutPanel?** — FlowLayoutPanel 的 Margin/高度计算在滚动条出现时容易产生偏差,改用手动 Panel + VScrollBar 精确控制每张卡片的位置。

**为什么保留硬编码模型列表?** — 硬编码保持端口号和显示名称的确定性。配合 `FindBat` 的模糊匹配,即使文件名有细微差异也能自动找到。

**C# 5 兼容** — .NET Framework 4.8 自带的编译器只支持到 C# 5,代码中避免使用 `?.`、`_` 丢弃参数等新语法,保证直接用 `csc` 就能编译。

---

## 许可证

MIT License — 自由使用、修改、分享。

---

*欢迎 Star / Fork / PR!*

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐