你的用户名 hai 1 mes
pai
achega
4b0d48c2f2
Modificáronse 7 ficheiros con 744 adicións e 10 borrados
  1. 16 0
      API.md
  2. 8 0
      data/www/assets/app.css
  3. 181 6
      data/www/assets/app.js
  4. 58 4
      data/www/index.html
  5. 66 0
      src/api/api_server.cpp
  6. 404 0
      src/software/nginx/nginx.cpp
  7. 11 0
      src/software/nginx/nginx.h

+ 16 - 0
API.md

@@ -226,6 +226,22 @@ password=ngsadmin
 | POST | `/api/nginx/start` | 启动 | - |
 | POST | `/api/nginx/stop` | 停止 | - |
 | POST | `/api/nginx/reload` | 重载配置 | - |
+| GET | `/api/nginx/config` | 读取基础配置 | - |
+| POST | `/api/nginx/config` | 保存基础配置并重载 | 见下 |
+
+基础配置 Body:
+
+```json
+{
+  "worker_processes": "auto",
+  "worker_connections": 1024,
+  "keepalive_timeout": "65",
+  "client_max_body_size": "50m",
+  "gzip": false
+}
+```
+
+保存后会写入 `nginx.conf`、执行配置检测;若 Nginx 正在运行则自动重载生效。
 
 ---
 

+ 8 - 0
data/www/assets/app.css

@@ -892,6 +892,14 @@ html, body {
   overflow: hidden;
   background: #1e1e1e;
 }
+.soft-cfg-monaco {
+  min-height: 320px;
+  height: min(460px, 55vh);
+  border: 1px solid #1e293b;
+  border-radius: 8px;
+  overflow: hidden;
+  background: #1e1e1e;
+}
 .site-log-view {
   margin: 0;
   max-height: min(360px, 50vh);

+ 181 - 6
data/www/assets/app.js

@@ -39,6 +39,9 @@
     siteSettings: null, // current site object
     siteGroupFilter: "", // "" = all, "__none__" = ungrouped, else group name
     redisConfigModal: null,
+    nginxConfigModal: null,
+    redisCfgEditor: null,
+    nginxCfgEditor: null,
     siteLogKind: "access",
     siteLogIp: "",
     siteLogQuery: "",
@@ -577,6 +580,9 @@
     $("#redis-tab-file")?.classList.toggle("active", !quick);
     $("#redis-pane-quick").hidden = !quick;
     $("#redis-pane-file").hidden = quick;
+    if (!quick) {
+      requestAnimationFrame(() => layoutSoftCfgEditor(state.redisCfgEditor));
+    }
   }
 
   async function loadRedisQuickConfig() {
@@ -588,11 +594,74 @@
     return data;
   }
 
+  function layoutSoftCfgEditor(editor) {
+    if (editor && typeof editor.layout === "function") {
+      editor.layout();
+    }
+  }
+
+  async function ensureSoftCfgEditor(kind) {
+    const isRedis = kind === "redis";
+    const hostId = isRedis ? "redis-cfg-monaco" : "nginx-cfg-monaco";
+    const host = $(`#${hostId}`);
+    if (!host) throw new Error("编辑器容器不存在");
+    const existing = isRedis ? state.redisCfgEditor : state.nginxCfgEditor;
+    if (existing) {
+      layoutSoftCfgEditor(existing);
+      return existing;
+    }
+    const monaco = await ensureMonaco();
+    host.innerHTML = "";
+    const editor = monaco.editor.create(host, {
+      value: "",
+      language: "ini",
+      theme: "vs-dark",
+      automaticLayout: true,
+      minimap: { enabled: false },
+      fontSize: 13,
+      lineNumbers: "on",
+      scrollBeyondLastLine: false,
+      wordWrap: "off",
+      tabSize: 4,
+      renderWhitespace: "selection",
+    });
+    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
+      withBusy(async () => {
+        if (isRedis) await saveRedisConfFile();
+        else await saveNginxConfFile();
+      });
+    });
+    if (isRedis) state.redisCfgEditor = editor;
+    else state.nginxCfgEditor = editor;
+    layoutSoftCfgEditor(editor);
+    return editor;
+  }
+
   async function loadRedisConfFile() {
     const path = $("#redis-cfg-path").textContent.trim();
     if (!path) throw new Error("配置文件路径未知");
     const data = await api(`/api/files/read?path=${encodeURIComponent(path)}`);
-    $("#redis-cfg-text").value = data.content || "";
+    const content = (data && data.content) || "";
+    const editor = await ensureSoftCfgEditor("redis");
+    const monaco = await ensureMonaco();
+    const model = editor.getModel();
+    const lang = langFromPath(path) || "ini";
+    if (model && monaco.editor.setModelLanguage) {
+      monaco.editor.setModelLanguage(model, lang);
+    }
+    editor.setValue(content);
+    requestAnimationFrame(() => layoutSoftCfgEditor(editor));
+  }
+
+  async function saveRedisConfFile() {
+    const path = $("#redis-cfg-path").textContent.trim();
+    if (!path) throw new Error("配置文件路径未知");
+    const editor = await ensureSoftCfgEditor("redis");
+    await api("/api/files/write", {
+      method: "POST",
+      body: { path, content: editor.getValue() || "" },
+    });
+    toast("配置文件已保存");
   }
 
   async function openRedisConfigModal() {
@@ -604,6 +673,64 @@
     showStackedModal(state.redisConfigModal, $("#redis-config-modal"));
   }
 
+  function setNginxConfigTab(tab) {
+    const quick = tab !== "file";
+    $("#nginx-tab-quick")?.classList.toggle("active", quick);
+    $("#nginx-tab-file")?.classList.toggle("active", !quick);
+    $("#nginx-pane-quick").hidden = !quick;
+    $("#nginx-pane-file").hidden = quick;
+    if (!quick) {
+      requestAnimationFrame(() => layoutSoftCfgEditor(state.nginxCfgEditor));
+    }
+  }
+
+  async function loadNginxQuickConfig() {
+    const data = await api("/api/nginx/config");
+    $("#nginx-cfg-workers").value = data.worker_processes || "auto";
+    $("#nginx-cfg-connections").value = data.worker_connections || 1024;
+    $("#nginx-cfg-keepalive").value = data.keepalive_timeout || "65";
+    $("#nginx-cfg-body").value = data.client_max_body_size || "50m";
+    $("#nginx-cfg-gzip").checked = !!data.gzip;
+    $("#nginx-cfg-path").textContent = data.conf || "";
+    return data;
+  }
+
+  async function loadNginxConfFile() {
+    const path = $("#nginx-cfg-path").textContent.trim();
+    if (!path) throw new Error("配置文件路径未知");
+    const data = await api(`/api/files/read?path=${encodeURIComponent(path)}`);
+    const content = (data && data.content) || "";
+    const editor = await ensureSoftCfgEditor("nginx");
+    const monaco = await ensureMonaco();
+    const model = editor.getModel();
+    const lang = langFromPath(path) || "ini";
+    if (model && monaco.editor.setModelLanguage) {
+      monaco.editor.setModelLanguage(model, lang);
+    }
+    editor.setValue(content);
+    requestAnimationFrame(() => layoutSoftCfgEditor(editor));
+  }
+
+  async function saveNginxConfFile() {
+    const path = $("#nginx-cfg-path").textContent.trim();
+    if (!path) throw new Error("配置文件路径未知");
+    const editor = await ensureSoftCfgEditor("nginx");
+    await api("/api/files/write", {
+      method: "POST",
+      body: { path, content: editor.getValue() || "" },
+    });
+    toast("配置文件已保存");
+  }
+
+  async function openNginxConfigModal() {
+    if (!state.nginxConfigModal) {
+      state.nginxConfigModal = new bootstrap.Modal($("#nginx-config-modal"));
+    }
+    setNginxConfigTab("quick");
+    await loadNginxQuickConfig();
+    showStackedModal(state.nginxConfigModal, $("#nginx-config-modal"));
+  }
+
   function formatRate(bps) {
     const v = Math.max(0, Number(bps) || 0);
     if (v < 1024) return `${Math.round(v)} B/s`;
@@ -1709,6 +1836,10 @@
       await withBusy(async () => openRedisConfigModal(), btn);
       return;
     }
+    if (act === "nginx-config") {
+      await withBusy(async () => openNginxConfigModal(), btn);
+      return;
+    }
     if (act.endsWith("-config")) {
       const conf = btn.dataset.conf;
       await withBusy(async () => openConfigFile(conf), btn);
@@ -3900,16 +4031,59 @@
       }, e.currentTarget);
     });
     $("#redis-cfg-file-save")?.addEventListener("click", (e) => {
+      withBusy(async () => saveRedisConfFile(), e.currentTarget);
+    });
+    $("#redis-config-modal")?.addEventListener("shown.bs.modal", () => {
+      layoutSoftCfgEditor(state.redisCfgEditor);
+    });
+  }
+
+  function bindNginxConfigModal() {
+    if (!$("#nginx-config-modal")) return;
+    state.nginxConfigModal = new bootstrap.Modal($("#nginx-config-modal"));
+    $("#nginx-config-close")?.addEventListener("click", () => state.nginxConfigModal.hide());
+    $("#nginx-tab-quick")?.addEventListener("click", () => setNginxConfigTab("quick"));
+    $("#nginx-tab-file")?.addEventListener("click", () => {
+      setNginxConfigTab("file");
+      withBusy(async () => loadNginxConfFile());
+    });
+    $("#nginx-cfg-save")?.addEventListener("click", (e) => {
       withBusy(async () => {
-        const path = $("#redis-cfg-path").textContent.trim();
-        if (!path) throw new Error("配置文件路径未知");
-        await api("/api/files/write", {
+        const worker_processes = String($("#nginx-cfg-workers").value || "").trim() || "auto";
+        const worker_connections = Number($("#nginx-cfg-connections").value || 0);
+        const keepalive_timeout = String($("#nginx-cfg-keepalive").value || "").trim() || "65";
+        const client_max_body_size = String($("#nginx-cfg-body").value || "").trim() || "50m";
+        const gzip = !!$("#nginx-cfg-gzip").checked;
+        if (!worker_connections || worker_connections < 1) {
+          throw new Error("worker_connections 无效");
+        }
+        await api("/api/nginx/config", {
           method: "POST",
-          body: { path, content: $("#redis-cfg-text").value || "" },
+          body: {
+            worker_processes,
+            worker_connections,
+            keepalive_timeout,
+            client_max_body_size,
+            gzip,
+          },
         });
-        toast("配置文件已保存");
+        toast("Nginx 配置已应用");
+        await refresh();
+      }, e.currentTarget);
+    });
+    $("#nginx-cfg-reload")?.addEventListener("click", (e) => {
+      withBusy(async () => {
+        await loadNginxQuickConfig();
+        await loadNginxConfFile();
+        toast("已重新加载");
       }, e.currentTarget);
     });
+    $("#nginx-cfg-file-save")?.addEventListener("click", (e) => {
+      withBusy(async () => saveNginxConfFile(), e.currentTarget);
+    });
+    $("#nginx-config-modal")?.addEventListener("shown.bs.modal", () => {
+      layoutSoftCfgEditor(state.nginxCfgEditor);
+    });
   }
 
   function formatMs(ms) {
@@ -4128,6 +4302,7 @@
     bindUploadModal();
     bindSiteSettingsModal();
     bindRedisConfigModal();
+    bindNginxConfigModal();
 
     $("#sites-group-bar")?.addEventListener("click", (e) => {
       const chip = e.target.closest(".sites-group-chip");

+ 58 - 4
data/www/index.html

@@ -407,16 +407,16 @@
           <div id="redis-pane-quick">
             <p class="small text-secondary mb-3">快捷修改常用项;保存后若 Redis 正在运行会自动重启生效。</p>
             <div class="mb-2">
-              <label class="form-label">监听地址</label>
+              <label class="form-label">监听地址 <span class="text-secondary fw-normal">bind</span></label>
               <input class="form-control form-control-sm" id="redis-cfg-bind" placeholder="127.0.0.1" autocomplete="off" />
               <div class="form-text">多个地址用空格分隔,例如 <code>127.0.0.1 0.0.0.0</code></div>
             </div>
             <div class="mb-2">
-              <label class="form-label">端口</label>
+              <label class="form-label">端口 <span class="text-secondary fw-normal">port</span></label>
               <input class="form-control form-control-sm" id="redis-cfg-port" type="number" min="1" max="65535" />
             </div>
             <div class="mb-3">
-              <label class="form-label">密码</label>
+              <label class="form-label">密码 <span class="text-secondary fw-normal">requirepass</span></label>
               <input class="form-control form-control-sm" id="redis-cfg-pass" type="password" placeholder="留空表示不设密码" autocomplete="new-password" />
             </div>
             <button type="button" class="btn btn-sm btn-primary" id="redis-cfg-save">保存并应用</button>
@@ -426,7 +426,7 @@
               <code class="small text-secondary text-truncate" id="redis-cfg-path"></code>
               <button type="button" class="btn btn-sm btn-outline-secondary" id="redis-cfg-reload">重新加载</button>
             </div>
-            <textarea class="form-control font-monospace" id="redis-cfg-text" rows="18" spellcheck="false"></textarea>
+            <div id="redis-cfg-monaco" class="soft-cfg-monaco" aria-label="Redis 配置编辑器"></div>
             <div class="mt-2 d-flex gap-2">
               <button type="button" class="btn btn-sm btn-primary" id="redis-cfg-file-save">保存文件</button>
               <span class="small text-secondary align-self-center">仅写入文件;如需生效请自行重启 Redis。</span>
@@ -437,6 +437,60 @@
     </div>
   </div>
 
+  <div class="modal fade" id="nginx-config-modal" tabindex="-1" aria-hidden="true">
+    <div class="modal-dialog modal-lg modal-dialog-scrollable">
+      <div class="modal-content">
+        <div class="modal-header py-2">
+          <h5 class="modal-title mb-0">Nginx 配置</h5>
+          <button type="button" class="btn-close" id="nginx-config-close" aria-label="关闭"></button>
+        </div>
+        <div class="modal-body">
+          <div class="btn-group btn-group-sm mb-3" role="group">
+            <button type="button" class="btn btn-outline-primary active" id="nginx-tab-quick" data-nginx-tab="quick">基础配置</button>
+            <button type="button" class="btn btn-outline-primary" id="nginx-tab-file" data-nginx-tab="file">配置文件</button>
+          </div>
+          <div id="nginx-pane-quick">
+            <p class="small text-secondary mb-3">快捷修改常用项;保存后会检测配置,若 Nginx 正在运行则自动重载生效。</p>
+            <div class="mb-2">
+              <label class="form-label">工作进程数 <span class="text-secondary fw-normal">worker_processes</span></label>
+              <input class="form-control form-control-sm" id="nginx-cfg-workers" placeholder="auto" autocomplete="off" />
+              <div class="form-text">填 <code>auto</code> 或正整数</div>
+            </div>
+            <div class="mb-2">
+              <label class="form-label">最大连接数 <span class="text-secondary fw-normal">worker_connections</span></label>
+              <input class="form-control form-control-sm" id="nginx-cfg-connections" type="number" min="1" max="1048576" />
+            </div>
+            <div class="mb-2">
+              <label class="form-label">保持连接超时 <span class="text-secondary fw-normal">keepalive_timeout</span></label>
+              <input class="form-control form-control-sm" id="nginx-cfg-keepalive" placeholder="65" autocomplete="off" />
+            </div>
+            <div class="mb-2">
+              <label class="form-label">上传大小限制 <span class="text-secondary fw-normal">client_max_body_size</span></label>
+              <input class="form-control form-control-sm" id="nginx-cfg-body" placeholder="50m" autocomplete="off" />
+              <div class="form-text">例如 <code>1m</code>、<code>50m</code>、<code>1g</code></div>
+            </div>
+            <div class="mb-3 form-check">
+              <input class="form-check-input" type="checkbox" id="nginx-cfg-gzip" />
+              <label class="form-check-label" for="nginx-cfg-gzip">启用压缩 <span class="text-secondary fw-normal">gzip</span></label>
+            </div>
+            <button type="button" class="btn btn-sm btn-primary" id="nginx-cfg-save">保存并应用</button>
+          </div>
+          <div id="nginx-pane-file" hidden>
+            <div class="d-flex justify-content-between align-items-center mb-2 gap-2">
+              <code class="small text-secondary text-truncate" id="nginx-cfg-path"></code>
+              <button type="button" class="btn btn-sm btn-outline-secondary" id="nginx-cfg-reload">重新加载</button>
+            </div>
+            <div id="nginx-cfg-monaco" class="soft-cfg-monaco" aria-label="Nginx 配置编辑器"></div>
+            <div class="mt-2 d-flex gap-2">
+              <button type="button" class="btn btn-sm btn-primary" id="nginx-cfg-file-save">保存文件</button>
+              <span class="small text-secondary align-self-center">仅写入文件;如需生效请自行重载 Nginx。</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
   <div class="modal fade" id="site-settings-modal" tabindex="-1" aria-hidden="true">
     <div class="modal-dialog modal-xl modal-dialog-scrollable">
       <div class="modal-content">

+ 66 - 0
src/api/api_server.cpp

@@ -778,6 +778,71 @@ void h_nginx_reload(request* req, response* resp) {
     reply_ok(resp, ylib::json(), "nginx reloaded");
 }
 
+void h_nginx_config(request* req, response* resp) {
+    const std::string method = req->method();
+    if (method == "GET") {
+        nginx::QuickConfig cfg;
+        std::string err;
+        if (!nginx::read_quick_config(cfg, err)) {
+            reply_err(resp, err.empty() ? "read nginx config failed" : err);
+            return;
+        }
+        ylib::json data;
+        data["worker_processes"] = cfg.worker_processes;
+        data["worker_connections"] = cfg.worker_connections;
+        data["keepalive_timeout"] = cfg.keepalive_timeout;
+        data["client_max_body_size"] = cfg.client_max_body_size;
+        data["gzip"] = cfg.gzip;
+        data["conf"] = nginx::conf_file();
+        data["running"] = nginx::is_running();
+        reply_ok(resp, data);
+        return;
+    }
+    if (method == "POST") {
+        auto body = parse_body(req);
+        nginx::QuickConfig cfg;
+        std::string err;
+        if (!nginx::read_quick_config(cfg, err)) {
+            reply_err(resp, err.empty() ? "read nginx config failed" : err);
+            return;
+        }
+        if (body.exist("worker_processes")) {
+            cfg.worker_processes =
+                json_str(body, "worker_processes", cfg.worker_processes);
+        }
+        if (body.exist("worker_connections")) {
+            cfg.worker_connections =
+                json_int(body, "worker_connections", cfg.worker_connections);
+        }
+        if (body.exist("keepalive_timeout")) {
+            cfg.keepalive_timeout =
+                json_str(body, "keepalive_timeout", cfg.keepalive_timeout);
+        }
+        if (body.exist("client_max_body_size")) {
+            cfg.client_max_body_size =
+                json_str(body, "client_max_body_size", cfg.client_max_body_size);
+        }
+        if (body.exist("gzip")) {
+            cfg.gzip = json_bool(body, "gzip", cfg.gzip);
+        }
+        if (!nginx::apply_quick_config(cfg, err)) {
+            reply_err(resp, err.empty() ? "apply nginx config failed" : err);
+            return;
+        }
+        ylib::json data;
+        data["worker_processes"] = cfg.worker_processes;
+        data["worker_connections"] = cfg.worker_connections;
+        data["keepalive_timeout"] = cfg.keepalive_timeout;
+        data["client_max_body_size"] = cfg.client_max_body_size;
+        data["gzip"] = cfg.gzip;
+        data["conf"] = nginx::conf_file();
+        data["running"] = nginx::is_running();
+        reply_ok(resp, data, "nginx config updated");
+        return;
+    }
+    reply_err(resp, "method not allowed", 405);
+}
+
 void h_fastweb_status(request* req, response* resp) {
     if (!require_method(req, resp, "GET")) {
         return;
@@ -2210,6 +2275,7 @@ void register_routes(ylib::network::http::router* router) {
     reg(router, "/api/nginx/start", h_nginx_start);
     reg(router, "/api/nginx/stop", h_nginx_stop);
     reg(router, "/api/nginx/reload", h_nginx_reload);
+    reg(router, "/api/nginx/config", h_nginx_config);
 
     reg(router, "/api/fastweb/status", h_fastweb_status);
     reg(router, "/api/fastweb/install", h_fastweb_install);

+ 404 - 0
src/software/nginx/nginx.cpp

@@ -2,10 +2,12 @@
 
 #include "../../utils.h"
 
+#include <cctype>
 #include <fstream>
 #include <iostream>
 #include <iterator>
 #include <sstream>
+#include <vector>
 #include <pwd.h>
 #include <unistd.h>
 
@@ -22,6 +24,319 @@ std::string work_dir() {
     return join_path(software_root(), ".build");
 }
 
+std::string trim_copy(std::string s) {
+    while (!s.empty() &&
+           (s.front() == ' ' || s.front() == '\t' || s.front() == '\r')) {
+        s.erase(s.begin());
+    }
+    while (!s.empty() &&
+           (s.back() == ' ' || s.back() == '\t' || s.back() == '\r' ||
+            s.back() == '\n')) {
+        s.pop_back();
+    }
+    return s;
+}
+
+bool read_conf_lines(std::vector<std::string>& lines, std::string& err) {
+    lines.clear();
+    if (!path_exists(conf_file())) {
+        err = "配置文件不存在";
+        return false;
+    }
+    std::ifstream in(conf_file());
+    if (!in) {
+        err = "无法读取配置文件";
+        return false;
+    }
+    std::string line;
+    while (std::getline(in, line)) {
+        if (!line.empty() && line.back() == '\r') {
+            line.pop_back();
+        }
+        lines.push_back(line);
+    }
+    return true;
+}
+
+bool write_conf_lines(const std::vector<std::string>& lines, std::string& err) {
+    std::ofstream out(conf_file(), std::ios::trunc);
+    if (!out) {
+        err = "无法写入配置文件";
+        return false;
+    }
+    for (const auto& line : lines) {
+        out << line << '\n';
+    }
+    return true;
+}
+
+// Strip leading comment marker (#) and whitespace for directive matching.
+std::string directive_body(const std::string& line, bool& commented) {
+    commented = false;
+    std::string t = trim_copy(line);
+    if (t.empty()) {
+        return "";
+    }
+    if (t[0] == '#') {
+        commented = true;
+        t = trim_copy(t.substr(1));
+    }
+    return t;
+}
+
+bool line_directive(const std::string& line, std::string& key, std::string& value,
+                    bool& commented) {
+    const std::string body = directive_body(line, commented);
+    if (body.empty()) {
+        return false;
+    }
+    size_t i = 0;
+    while (i < body.size() && body[i] != ' ' && body[i] != '\t' &&
+           body[i] != '{' && body[i] != ';' && body[i] != '}') {
+        ++i;
+    }
+    if (i == 0) {
+        return false;
+    }
+    key = body.substr(0, i);
+    std::string rest = trim_copy(body.substr(i));
+    if (!rest.empty() && rest.back() == ';') {
+        rest.pop_back();
+        rest = trim_copy(rest);
+    }
+    value = rest;
+    return true;
+}
+
+enum class ConfCtx { Main, Events, Http, Other };
+
+// Walk lines and report which top-level context each line is in.
+// events/http are only tracked at depth of the block itself (not nested server).
+void annotate_contexts(const std::vector<std::string>& lines,
+                       std::vector<ConfCtx>& ctxs) {
+    ctxs.assign(lines.size(), ConfCtx::Main);
+    int depth = 0;
+    ConfCtx block = ConfCtx::Main;
+    for (size_t i = 0; i < lines.size(); ++i) {
+        bool commented = false;
+        std::string key;
+        std::string value;
+        line_directive(lines[i], key, value, commented);
+
+        ConfCtx here = ConfCtx::Main;
+        if (depth == 0) {
+            here = ConfCtx::Main;
+        } else if (depth == 1) {
+            here = block;
+        } else {
+            here = ConfCtx::Other;
+        }
+        ctxs[i] = here;
+
+        // Count braces on the line (ignore commented braces for simplicity:
+        // only look at active/non-commented structure markers).
+        const std::string raw = trim_copy(lines[i]);
+        if (!raw.empty() && raw[0] != '#') {
+            for (char c : raw) {
+                if (c == '{') {
+                    if (depth == 0) {
+                        if (key == "events") {
+                            block = ConfCtx::Events;
+                        } else if (key == "http") {
+                            block = ConfCtx::Http;
+                        } else {
+                            block = ConfCtx::Other;
+                        }
+                    }
+                    ++depth;
+                } else if (c == '}') {
+                    if (depth > 0) {
+                        --depth;
+                    }
+                    if (depth == 0) {
+                        block = ConfCtx::Main;
+                    }
+                }
+            }
+        }
+    }
+}
+
+void upsert_in_context(std::vector<std::string>& lines, ConfCtx want,
+                       const std::string& key, const std::string& value,
+                       const std::string& indent) {
+    std::vector<ConfCtx> ctxs;
+    annotate_contexts(lines, ctxs);
+
+    int first_active = -1;
+    int first_commented = -1;
+    for (size_t i = 0; i < lines.size(); ++i) {
+        if (ctxs[i] != want) {
+            continue;
+        }
+        bool commented = false;
+        std::string k;
+        std::string v;
+        if (!line_directive(lines[i], k, v, commented) || k != key) {
+            continue;
+        }
+        if (!commented && first_active < 0) {
+            first_active = static_cast<int>(i);
+        } else if (commented && first_commented < 0) {
+            first_commented = static_cast<int>(i);
+        }
+    }
+
+    const std::string new_line = indent + key + "  " + value + ";";
+    if (first_active >= 0) {
+        lines[static_cast<size_t>(first_active)] = new_line;
+        // Comment out duplicate active directives in same context.
+        bool seen = false;
+        annotate_contexts(lines, ctxs);
+        for (size_t i = 0; i < lines.size(); ++i) {
+            if (ctxs[i] != want) {
+                continue;
+            }
+            bool commented = false;
+            std::string k;
+            std::string v;
+            if (!line_directive(lines[i], k, v, commented) || commented ||
+                k != key) {
+                continue;
+            }
+            if (!seen) {
+                seen = true;
+                continue;
+            }
+            lines[i] = "#" + lines[i];
+        }
+        return;
+    }
+    if (first_commented >= 0) {
+        lines[static_cast<size_t>(first_commented)] = new_line;
+        return;
+    }
+
+    // Insert: after opening brace of the target block, or top of main.
+    annotate_contexts(lines, ctxs);
+    if (want == ConfCtx::Main) {
+        // Prefer after `user` directive; else at file start.
+        int insert_at = 0;
+        for (size_t i = 0; i < lines.size(); ++i) {
+            bool commented = false;
+            std::string k;
+            std::string v;
+            if (line_directive(lines[i], k, v, commented) && !commented &&
+                k == "user") {
+                insert_at = static_cast<int>(i) + 1;
+                break;
+            }
+        }
+        lines.insert(lines.begin() + insert_at, new_line);
+        return;
+    }
+
+    for (size_t i = 0; i < lines.size(); ++i) {
+        bool commented = false;
+        std::string k;
+        std::string v;
+        if (!line_directive(lines[i], k, v, commented) || commented) {
+            continue;
+        }
+        const char* block_name =
+            (want == ConfCtx::Events) ? "events" : "http";
+        if (k != block_name) {
+            continue;
+        }
+        // Insert right after this line (which contains `{` or next line).
+        size_t insert_at = i + 1;
+        if (lines[i].find('{') == std::string::npos &&
+            insert_at < lines.size() &&
+            trim_copy(lines[insert_at]) == "{") {
+            insert_at += 1;
+        }
+        lines.insert(lines.begin() + static_cast<long>(insert_at), new_line);
+        return;
+    }
+    // Fallback: append.
+    lines.push_back(new_line);
+}
+
+void parse_quick_from_lines(const std::vector<std::string>& lines,
+                            QuickConfig& out) {
+    out = QuickConfig{};
+    std::vector<ConfCtx> ctxs;
+    annotate_contexts(lines, ctxs);
+    for (size_t i = 0; i < lines.size(); ++i) {
+        bool commented = false;
+        std::string k;
+        std::string v;
+        if (!line_directive(lines[i], k, v, commented) || commented) {
+            continue;
+        }
+        if (ctxs[i] == ConfCtx::Main && k == "worker_processes") {
+            out.worker_processes = trim_copy(v);
+        } else if (ctxs[i] == ConfCtx::Events && k == "worker_connections") {
+            try {
+                const int n = std::stoi(trim_copy(v));
+                if (n > 0) {
+                    out.worker_connections = n;
+                }
+            } catch (...) {
+            }
+        } else if (ctxs[i] == ConfCtx::Http && k == "keepalive_timeout") {
+            out.keepalive_timeout = trim_copy(v);
+        } else if (ctxs[i] == ConfCtx::Http && k == "client_max_body_size") {
+            out.client_max_body_size = trim_copy(v);
+        } else if (ctxs[i] == ConfCtx::Http && k == "gzip") {
+            const std::string gv = trim_copy(v);
+            out.gzip = (gv == "on" || gv == "1" || gv == "yes");
+        }
+    }
+}
+
+bool valid_worker_processes(const std::string& s) {
+    if (s == "auto") {
+        return true;
+    }
+    if (s.empty()) {
+        return false;
+    }
+    for (char c : s) {
+        if (!std::isdigit(static_cast<unsigned char>(c))) {
+            return false;
+        }
+    }
+    try {
+        return std::stoi(s) > 0;
+    } catch (...) {
+        return false;
+    }
+}
+
+bool valid_size_or_time(const std::string& s) {
+    if (s.empty()) {
+        return false;
+    }
+    // Allow values like 65, 65s, 50m, 1g, 1024k
+    size_t i = 0;
+    while (i < s.size() && std::isdigit(static_cast<unsigned char>(s[i]))) {
+        ++i;
+    }
+    if (i == 0) {
+        return false;
+    }
+    if (i == s.size()) {
+        return true;
+    }
+    if (i + 1 == s.size()) {
+        const char u = static_cast<char>(std::tolower(static_cast<unsigned char>(s[i])));
+        return u == 's' || u == 'm' || u == 'h' || u == 'd' || u == 'k' ||
+               u == 'g' || u == 't';
+    }
+    return false;
+}
+
 bool ensure_build_deps() {
     std::cout << "检查编译依赖...\n";
     // Prefer apt when available; skip quietly if packages already present.
@@ -511,6 +826,95 @@ bool ensure_vhost_include() {
     return true;
 }
 
+bool read_quick_config(QuickConfig& out, std::string& err) {
+    err.clear();
+    if (!is_installed()) {
+        err = "Nginx 未安装";
+        return false;
+    }
+    if (!path_exists(conf_file())) {
+        out = QuickConfig{};
+        return true;
+    }
+    std::vector<std::string> lines;
+    if (!read_conf_lines(lines, err)) {
+        return false;
+    }
+    parse_quick_from_lines(lines, out);
+    return true;
+}
+
+bool apply_quick_config(const QuickConfig& in, std::string& err) {
+    err.clear();
+    if (!is_installed()) {
+        err = "Nginx 未安装";
+        return false;
+    }
+    QuickConfig cfg = in;
+    cfg.worker_processes = trim_copy(cfg.worker_processes);
+    cfg.keepalive_timeout = trim_copy(cfg.keepalive_timeout);
+    cfg.client_max_body_size = trim_copy(cfg.client_max_body_size);
+    if (!valid_worker_processes(cfg.worker_processes)) {
+        err = "worker_processes 无效(应为 auto 或正整数)";
+        return false;
+    }
+    if (cfg.worker_connections < 1 || cfg.worker_connections > 1048576) {
+        err = "worker_connections 无效";
+        return false;
+    }
+    if (!valid_size_or_time(cfg.keepalive_timeout)) {
+        err = "keepalive_timeout 无效";
+        return false;
+    }
+    if (!valid_size_or_time(cfg.client_max_body_size)) {
+        err = "client_max_body_size 无效";
+        return false;
+    }
+
+    std::vector<std::string> lines;
+    if (!read_conf_lines(lines, err)) {
+        return false;
+    }
+    upsert_in_context(lines, ConfCtx::Main, "worker_processes",
+                      cfg.worker_processes, "");
+    upsert_in_context(lines, ConfCtx::Events, "worker_connections",
+                      std::to_string(cfg.worker_connections), "    ");
+    upsert_in_context(lines, ConfCtx::Http, "keepalive_timeout",
+                      cfg.keepalive_timeout, "    ");
+    upsert_in_context(lines, ConfCtx::Http, "client_max_body_size",
+                      cfg.client_max_body_size, "    ");
+    upsert_in_context(lines, ConfCtx::Http, "gzip",
+                      cfg.gzip ? "on" : "off", "    ");
+
+    if (!write_conf_lines(lines, err)) {
+        return false;
+    }
+
+    // Keep NGS markers / user intact after manual edits.
+    ensure_nginx_user();
+    ensure_vhost_include();
+
+    std::string detail;
+    if (!test_config(detail)) {
+        err = "配置已写入,但检测失败: " + detail;
+        return false;
+    }
+
+    if (is_running()) {
+        if (!reload()) {
+            err = "配置已写入,但重载 Nginx 失败";
+            return false;
+        }
+    }
+    log_info("nginx quick config applied worker_processes=" +
+             cfg.worker_processes +
+             " worker_connections=" + std::to_string(cfg.worker_connections) +
+             " keepalive_timeout=" + cfg.keepalive_timeout +
+             " client_max_body_size=" + cfg.client_max_body_size +
+             " gzip=" + (cfg.gzip ? "on" : "off"));
+    return true;
+}
+
 std::string installed_version() {
     const std::string path = join_path(install_dir(), ".ngs_version");
     if (path_exists(path)) {

+ 11 - 0
src/software/nginx/nginx.h

@@ -8,6 +8,14 @@ namespace nginx {
 
 constexpr const char* kDefaultVersion = "1.26.3";
 
+struct QuickConfig {
+    std::string worker_processes = "auto";
+    int worker_connections = 1024;
+    std::string keepalive_timeout = "65";
+    std::string client_max_body_size = "50m";
+    bool gzip = false;
+};
+
 std::string install_dir();
 std::string bin_path();
 std::string conf_file();
@@ -15,6 +23,9 @@ bool is_installed();
 bool is_running();
 std::string installed_version();
 
+bool read_quick_config(QuickConfig& out, std::string& err);
+bool apply_quick_config(const QuickConfig& in, std::string& err);
+
 bool install(const std::string& version = kDefaultVersion);
 bool uninstall();
 bool start();