feat: manual landing endpoints for forward nodes (item 8)

A forward node's exit target is now a choice: a managed panel server
(existing behavior) or a manual landing endpoint — address + port +
protocol (VLESS / SS2022 / SOCKS5) + operator-supplied credentials,
including third-party nodes the panel does not run. The panel only
builds the entry-side sing-box outbound and route rule; there is no
exit resource, no exit job, and the node is exit-ready by definition.

- model: Node.ExitMode/ExitAddress; manual exits force fixed port mode
  and skip the exit-server port table entirely
- validation: address/port required; per-protocol credential presence;
  SS2022 passwords must be base64 key material of the method's key
  length (one PSK segment or serverKey:userKey) — the exact form
  sing-box accepts, rejected up front instead of failing at deploy
- orchestration: resource building, deploy validation and rebuilds all
  branch on manualExit; switching managed->manual queues removal of the
  old exit resource, manual->managed mints a fresh exit resource id
  (deploy validation previously dereferenced the nil exit server —
  regression test locks the fix)
- UI: exit target segmented control, link paste box that parses
  vless:// / ss:// / socks5:// links into the form, per-protocol
  credential fields (blank keeps the stored value on edit)
- golden: new manualCreate/manualAssign/manualDeploy/manualJobs/
  manualNodes sections — exactly one entry-side job whose outbound
  targets the manual address; all prior sections unchanged
- the generated combined sing-box config passes the real sing-box
  1.13.21 check
This commit is contained in:
aaaamoooo
2026-10-21 11:18:00 +08:00
parent d316d0f8bb
commit 0037116edc
9 changed files with 911 additions and 31 deletions
+3
View File
@@ -322,6 +322,9 @@ label>input:focus,label>select:focus,label>textarea:focus{border-color:var(--pri
.mono{font-family:var(--mono);font-size:12.5px;word-break:break-all}
/* 页签 */
.seg{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden}
.seg button{padding:6px 16px;background:var(--card);border:none;color:var(--text-2);cursor:pointer;font:inherit;font-size:13px}
.seg button.active{background:var(--primary);color:#fff}
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--border-soft);margin-bottom:14px}
.tabs button{border:none;background:transparent;padding:7px 14px;font-size:13.5px;color:var(--text-2);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}
.tabs button.active{color:var(--primary-text);border-bottom-color:var(--primary);font-weight:500}