From f508ecc1b9ccfab4d134b4a916f357ddbc266fe7 Mon Sep 17 00:00:00 2001 From: tursom Date: Sat, 27 Jun 2026 20:15:29 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 + cmd/gateway/admin_api.go | 6 ++ cmd/gateway/admin_api_test.go | 2 + cmd/gateway/admin_audit.go | 2 + cmd/gateway/admin_auth_handlers.go | 2 + cmd/gateway/admin_frontend/src/alerts.ts | 2 + cmd/gateway/admin_frontend/src/api.ts | 2 + cmd/gateway/admin_frontend/src/config.ts | 2 + cmd/gateway/admin_frontend/src/dom.ts | 2 + cmd/gateway/admin_frontend/src/i18n.ts | 2 + cmd/gateway/admin_frontend/src/main.ts | 15 ++++ cmd/gateway/admin_frontend/src/session.ts | 2 + cmd/gateway/admin_frontend/src/state.ts | 2 + cmd/gateway/admin_frontend/src/types.ts | 2 + cmd/gateway/admin_frontend/src/views/audit.ts | 2 + .../admin_frontend/src/views/metrics.ts | 2 + .../admin_frontend/src/views/plugins.ts | 34 ++++++++ .../admin_frontend/src/views/routes.ts | 2 + .../admin_frontend/src/views/services.ts | 2 + .../admin_frontend/src/views/status.ts | 2 + cmd/gateway/admin_frontend/src/views/users.ts | 2 + cmd/gateway/admin_metric_handlers.go | 2 + cmd/gateway/admin_plugin_handlers.go | 2 + cmd/gateway/admin_route_handlers.go | 2 + cmd/gateway/admin_routes.go | 12 ++- cmd/gateway/admin_runtime.go | 8 ++ cmd/gateway/admin_service_handlers.go | 2 + cmd/gateway/admin_services.go | 2 + cmd/gateway/admin_sessions.go | 2 + cmd/gateway/admin_static.go | 2 + cmd/gateway/admin_static/app.css | 2 + cmd/gateway/admin_static/index.html | 2 + cmd/gateway/admin_user_handlers.go | 2 + cmd/gateway/admin_users.go | 2 + cmd/gateway/config.go | 2 + cmd/gateway/config_test.go | 2 + cmd/gateway/err.go | 2 + cmd/gateway/handle_request_test.go | 2 + cmd/gateway/haproxy.go | 4 +- cmd/gateway/haproxy_test.go | 2 + cmd/gateway/kcp.go | 6 ++ cmd/gateway/log.go | 2 + cmd/gateway/log_notunix.go | 8 +- cmd/gateway/log_pid_test.go | 2 + cmd/gateway/log_unix.go | 2 + cmd/gateway/main.go | 32 +++++++ cmd/gateway/main_test.go | 2 + cmd/gateway/pid.go | 2 + cmd/gateway/pid_unix.go | 2 + cmd/gateway/pid_unix_test.go | 2 + cmd/gateway/pid_windows.go | 2 + cmd/gateway/plugin.go | 10 ++- cmd/gateway/plugin_cli.go | 2 + cmd/gateway/plugin_cli_manifest.go | 2 + cmd/gateway/plugin_cli_remote.go | 2 + cmd/gateway/plugin_cli_toolchain.go | 6 +- cmd/gateway/plugin_cli_toolchain_test.go | 2 + cmd/gateway/plugin_test.go | 2 + cmd/gateway/quic.go | 30 +++++-- cmd/gateway/quic_test.go | 2 + cmd/gateway/relay.go | 10 +++ cmd/gateway/relay_benchmark_test.go | 2 + cmd/gateway/relay_test.go | 2 + cmd/gateway/tcp.go | 8 +- cmd/gateway/tcp_test.go | 2 + cmd/gateway/tcp_web_port_reuse.go | 10 ++- cmd/gateway/tcp_web_port_reuse_test.go | 2 + cmd/gateway/test_helpers_test.go | 2 + cmd/gateway/websocket.go | 9 +- cmd/gateway/websocket_test.go | 2 + cmd/kcp/main.go | 2 + cmd/kcp/main_test.go | 2 + cmd/quic/main.go | 4 +- cmd/quic/main_test.go | 2 + compose.override.yaml | 2 + compose.yaml | 2 + config.example.toml | 2 + .../plugins/extension-ecosystem/manifest.yaml | 4 +- examples/plugins/mc-auth-proxy/build.sh | 2 + examples/plugins/mc-auth-proxy/main.go | 2 + examples/plugins/mc-auth-proxy/main_test.go | 2 + examples/plugins/mc-auth-proxy/manifest.yaml | 4 +- examples/plugins/upstream-rewrite/build.sh | 2 + examples/plugins/upstream-rewrite/main.go | 2 + .../plugins/upstream-rewrite/manifest.yaml | 4 +- internal/adminaudit/audit.go | 2 + internal/adminaudit/audit_test.go | 2 + internal/adminconfig/config.go | 2 + internal/adminconfig/config_test.go | 2 + internal/admindb/db.go | 15 ++++ internal/admindb/db_test.go | 2 + internal/admindb/db_unsupported.go | 2 + internal/adminhttp/api.go | 2 + internal/adminhttp/api_test.go | 2 + internal/adminhttp/gateway.go | 2 + internal/adminhttp/http.go | 2 + internal/adminhttp/http_test.go | 2 + internal/adminhttp/requests.go | 2 + internal/adminhttp/requests_test.go | 2 + internal/adminroute/repository.go | 10 ++- internal/adminroute/repository_test.go | 2 + internal/adminroute/snapshot.go | 6 ++ internal/adminroute/snapshot_test.go | 2 + internal/adminroute/validate.go | 4 + internal/adminroute/validate_test.go | 2 + internal/adminservice/repository.go | 8 +- internal/adminservice/repository_test.go | 2 + internal/adminservice/service.go | 12 +++ internal/adminservice/service_test.go | 2 + internal/adminsession/session.go | 9 +- internal/adminsession/session_test.go | 2 + internal/adminuser/repository.go | 2 + internal/adminuser/repository_test.go | 2 + internal/adminuser/user.go | 9 ++ internal/adminuser/user_test.go | 2 + internal/gatewayconfig/config.go | 2 + internal/gatewayconfig/plugin.go | 2 + internal/gatewayconfig/plugin_test.go | 2 + internal/gatewaymetrics/metrics.go | 5 ++ internal/gatewaymetrics/metrics_test.go | 2 + internal/pluginmanager/artifact.go | 2 + internal/pluginmanager/artifact_test.go | 2 + internal/pluginmanager/builder.go | 2 + internal/pluginmanager/extensions.go | 2 + internal/pluginmanager/future.go | 2 + internal/pluginmanager/future_test.go | 2 + internal/pluginmanager/gc.go | 2 + internal/pluginmanager/governance.go | 2 + internal/pluginmanager/governance_test.go | 2 + internal/pluginmanager/manager.go | 69 +++++++++++++++ internal/pluginmanager/manager_test.go | 2 + internal/pluginmanager/operations.go | 87 ++++++++++++++++++- internal/pluginmanager/repository.go | 2 + internal/pluginmanager/types.go | 2 + internal/tcphttpmux/mux.go | 31 +++++++ internal/tcphttpmux/mux_test.go | 2 + internal/upstreamtarget/target.go | 2 + internal/upstreamtarget/target_test.go | 2 + plugin/api/api.go | 48 ++++++++-- plugin/api/api_test.go | 2 + plugin/api/hook.go | 40 ++++++++- plugin/official/rulepolicy/rulepolicy.go | 2 + protocol/mc.go | 18 ++++ protocol/mc_test.go | 2 + protocol/smoke/helper.go | 2 + 145 files changed, 774 insertions(+), 41 deletions(-) diff --git a/Dockerfile b/Dockerfile index c40cb04..ec6d472 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +# Dockerfile 构建网关二进制、编译管理前端,并打包带 SQLite 友好默认值的运行镜像。 + # syntax=docker/dockerfile:1 FROM --platform=$BUILDPLATFORM node:24.11.1-alpine AS admin-frontend diff --git a/cmd/gateway/admin_api.go b/cmd/gateway/admin_api.go index c8d602c..f641e85 100644 --- a/cmd/gateway/admin_api.go +++ b/cmd/gateway/admin_api.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_api.go 组装 Admin API 的共享依赖,并提供嵌入式控制台使用的顶层 HTTP 路由。 + package main import ( @@ -7,6 +9,8 @@ import ( ) func newAdminAPIHandler() http.HandlerFunc { + // Admin API 的路径解析放在 internal/adminhttp 中,主包只提供各业务 handler。 + // 这样测试可以复用同一套路由表,而不会依赖真实监听器。 return adminhttp.NewAPIHandler(adminStartup.AdminAPIPrefix, adminhttp.APIHandlers{ SetupStatus: handleAdminSetupStatus, Setup: handleAdminSetup, @@ -29,6 +33,8 @@ func newAdminAPIHandler() http.HandlerFunc { AuditLogs: handleAdminAuditLogs, + // 插件相关接口数量较多,统一在这里接入,确保嵌入式 UI 和远程 CLI + // 看到的是同一套 Admin API 行为。 PluginArtifacts: handleAdminPluginArtifacts, PluginArtifact: handleAdminPluginArtifact, PluginSources: handleAdminPluginSources, diff --git a/cmd/gateway/admin_api_test.go b/cmd/gateway/admin_api_test.go index e4963b4..86c7955 100644 --- a/cmd/gateway/admin_api_test.go +++ b/cmd/gateway/admin_api_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_api_test.go 包含用于约束 admin api 行为的测试。 + package main import ( diff --git a/cmd/gateway/admin_audit.go b/cmd/gateway/admin_audit.go index 8070e74..1234c14 100644 --- a/cmd/gateway/admin_audit.go +++ b/cmd/gateway/admin_audit.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_audit.go 把 HTTP 请求上下文转换为持久化审计记录,用于追踪管理端变更。 + package main import ( diff --git a/cmd/gateway/admin_auth_handlers.go b/cmd/gateway/admin_auth_handlers.go index 5c219d9..4aea8fc 100644 --- a/cmd/gateway/admin_auth_handlers.go +++ b/cmd/gateway/admin_auth_handlers.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_auth_handlers.go 处理初始化、登录、登出和当前会话查询等嵌入式管理端认证接口。 + package main import ( diff --git a/cmd/gateway/admin_frontend/src/alerts.ts b/cmd/gateway/admin_frontend/src/alerts.ts index 3e0249e..cefd83d 100644 --- a/cmd/gateway/admin_frontend/src/alerts.ts +++ b/cmd/gateway/admin_frontend/src/alerts.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/alerts.ts 集中处理告警展示,让异步界面流程可以一致地清空或显示错误。 + import { el } from "./dom.js"; import { localizeMessage } from "./i18n.js"; diff --git a/cmd/gateway/admin_frontend/src/api.ts b/cmd/gateway/admin_frontend/src/api.ts index d189e2c..d51b390 100644 --- a/cmd/gateway/admin_frontend/src/api.ts +++ b/cmd/gateway/admin_frontend/src/api.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/api.ts 封装 fetch,统一处理 Admin API 前缀、令牌、JSON 编码和错误返回。 + import { state } from "./state.js"; interface APIOptions { diff --git a/cmd/gateway/admin_frontend/src/config.ts b/cmd/gateway/admin_frontend/src/config.ts index 41c632f..bb69647 100644 --- a/cmd/gateway/admin_frontend/src/config.ts +++ b/cmd/gateway/admin_frontend/src/config.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/config.ts 读取嵌入式管理端 HTML 壳注入的运行时配置。 + import type { RuntimeConfig } from "./types.js"; declare global { diff --git a/cmd/gateway/admin_frontend/src/dom.ts b/cmd/gateway/admin_frontend/src/dom.ts index 38d3878..6eec10e 100644 --- a/cmd/gateway/admin_frontend/src/dom.ts +++ b/cmd/gateway/admin_frontend/src/dom.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/dom.ts 集中 DOM 辅助方法,包括查询、转义、徽标、去抖和表单取值。 + export function el(id: string): T { const node = document.getElementById(id); if (!node) { diff --git a/cmd/gateway/admin_frontend/src/i18n.ts b/cmd/gateway/admin_frontend/src/i18n.ts index 0a3a94f..1370671 100644 --- a/cmd/gateway/admin_frontend/src/i18n.ts +++ b/cmd/gateway/admin_frontend/src/i18n.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/i18n.ts 保存嵌入式管理端翻译字典,并提供语言切换辅助方法。 + import { el } from "./dom.js"; import { languageStorageKey, state } from "./state.js"; diff --git a/cmd/gateway/admin_frontend/src/main.ts b/cmd/gateway/admin_frontend/src/main.ts index 9c55353..087dfa2 100644 --- a/cmd/gateway/admin_frontend/src/main.ts +++ b/cmd/gateway/admin_frontend/src/main.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/main.ts 启动嵌入式管理端,选择初始化/登录/应用视图,并协调按角色加载数据。 + import { api } from "./api.js"; import { showAlert } from "./alerts.js"; import { runtimeConfig } from "./config.js"; @@ -15,12 +17,14 @@ import { loadStatus } from "./views/status.js"; import { loadUsers, openUserDialog, renderUsers, saveUser } from "./views/users.js"; async function boot(): Promise { + // API 前缀由后端嵌入到 HTML 中,前端启动时先读取它,避免部署在子路径时写死地址。 state.apiBase = runtimeConfig().apiPrefix; initializeLanguage(); bindEvents(); try { const setup = await api("/setup"); if (setup.required) { + // 没有任何管理账号时只展示初始化界面,不尝试加载其他运行态数据。 setView("setupView"); setSubtitle("setupSubtitle"); return; @@ -30,6 +34,7 @@ async function boot(): Promise { } if (!state.token) { + // token 保存在本地状态中;没有 token 时直接进入登录视图。 setView("loginView"); setSubtitle("login"); return; @@ -39,6 +44,7 @@ async function boot(): Promise { state.user = await api("/me"); await showApp(); } catch { + // token 失效时清空本地状态,避免后续 API 调用持续带着过期凭证。 setToken(""); setView("loginView"); setSubtitle("login"); @@ -46,6 +52,7 @@ async function boot(): Promise { } function bindEvents(): void { + // 所有顶层事件在启动时绑定一次,视图重渲染只更新内容区域。 el("languageSelect").addEventListener("change", (event) => { changeLanguage((event.currentTarget as HTMLSelectElement).value, rerenderCurrentView); }); @@ -75,6 +82,7 @@ async function submitSetup(event: SubmitEvent): Promise { event.preventDefault(); const form = new FormData(event.currentTarget as HTMLFormElement); try { + // 初始化只创建首个管理员账号,创建成功后仍要求用户走登录流程获取会话 token。 await api("/setup", { method: "POST", body: { @@ -94,6 +102,7 @@ async function submitLogin(event: SubmitEvent): Promise { event.preventDefault(); const form = new FormData(event.currentTarget as HTMLFormElement); try { + // 登录成功后立即保存 token 和用户信息,再统一进入应用态加载流程。 const data = await api("/auth/login", { method: "POST", body: { @@ -114,6 +123,7 @@ async function logout(): Promise { try { await api("/auth/logout", { method: "POST", body: {} }); } catch { + // 服务端登出失败不阻塞本地清理,避免用户卡在失效会话上。 } setToken(""); state.user = null; @@ -124,6 +134,7 @@ async function logout(): Promise { } async function showApp(): Promise { + // 路由列表是成员和管理员都可见的基础视图,因此先加载它。 setView("appView"); setSubtitle("adminSubtitle"); renderSessionUser(); @@ -131,12 +142,14 @@ async function showApp(): Promise { applyRoleVisibility(); await loadRoutes(); if (isMember()) { + // 成员权限可以查看运行态、服务、指标和插件,但不能管理用户与审计。 await loadStatus(); await loadServices(); await loadMetrics(); await loadPlugins(); } if (isAdmin()) { + // 管理员专属数据放在最后加载,减少普通成员的无权限请求。 await loadUsers(); await loadAudit(); } @@ -145,6 +158,7 @@ async function showApp(): Promise { function applyRoleVisibility(): void { const member = isMember(); const admin = isAdmin(); + // 角色控制只隐藏入口;服务端仍会按 token 做权限校验。 el("statusGrid").classList.toggle("hidden", !member); el("newRouteBtn").classList.toggle("hidden", !member); toggleTab("services", member); @@ -176,6 +190,7 @@ function setView(name: string): void { } function rerenderCurrentView(): void { + // 切换语言后复用当前内存状态重绘静态文案,再刷新会随语言展示的远端数据。 renderSessionUser(); renderRoutes(); renderServices(); diff --git a/cmd/gateway/admin_frontend/src/session.ts b/cmd/gateway/admin_frontend/src/session.ts index da29893..215cf6f 100644 --- a/cmd/gateway/admin_frontend/src/session.ts +++ b/cmd/gateway/admin_frontend/src/session.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/session.ts 渲染会话用户状态,并提供管理端界面使用的角色判断。 + import { el } from "./dom.js"; import { t } from "./i18n.js"; import { state } from "./state.js"; diff --git a/cmd/gateway/admin_frontend/src/state.ts b/cmd/gateway/admin_frontend/src/state.ts index 32a56dd..fc97c52 100644 --- a/cmd/gateway/admin_frontend/src/state.ts +++ b/cmd/gateway/admin_frontend/src/state.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/state.ts 保存各视图模块共享的可变客户端状态。 + import type { PluginArtifact, PluginBuild, PluginInstrumentation, PluginServiceStatus, PluginView, RouteRecord, ServiceRecord, User } from "./types.js"; export const tokenStorageKey = "mcGatewayAdminToken"; diff --git a/cmd/gateway/admin_frontend/src/types.ts b/cmd/gateway/admin_frontend/src/types.ts index f31edcb..dd69c95 100644 --- a/cmd/gateway/admin_frontend/src/types.ts +++ b/cmd/gateway/admin_frontend/src/types.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/types.ts 声明 Admin API 返回并被各视图消费的 TypeScript 数据结构。 + export type Role = "admin" | "member" | "guest"; export interface User { diff --git a/cmd/gateway/admin_frontend/src/views/audit.ts b/cmd/gateway/admin_frontend/src/views/audit.ts index 2258520..41e6e0d 100644 --- a/cmd/gateway/admin_frontend/src/views/audit.ts +++ b/cmd/gateway/admin_frontend/src/views/audit.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/views/audit.ts 渲染管理员用于复核运行态变更的审计日志。 + import { api } from "../api.js"; import { showAlert } from "../alerts.js"; import { badge, el, escapeHTML } from "../dom.js"; diff --git a/cmd/gateway/admin_frontend/src/views/metrics.ts b/cmd/gateway/admin_frontend/src/views/metrics.ts index 6bff199..b20fe5f 100644 --- a/cmd/gateway/admin_frontend/src/views/metrics.ts +++ b/cmd/gateway/admin_frontend/src/views/metrics.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/views/metrics.ts 渲染管理端成员可见的网关指标计数器。 + import { api } from "../api.js"; import { showAlert } from "../alerts.js"; import { el, escapeHTML, stat } from "../dom.js"; diff --git a/cmd/gateway/admin_frontend/src/views/plugins.ts b/cmd/gateway/admin_frontend/src/views/plugins.ts index 1e8ea5f..412e9b6 100644 --- a/cmd/gateway/admin_frontend/src/views/plugins.ts +++ b/cmd/gateway/admin_frontend/src/views/plugins.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/views/plugins.ts 渲染插件清单、插件详情、配置/密钥/治理动作和运维工具。 + import { api } from "../api.js"; import { showAlert } from "../alerts.js"; import { badge, el, escapeAttr, escapeHTML, getFormInput } from "../dom.js"; @@ -50,6 +52,8 @@ interface OperationsResponse { export async function loadPlugins(): Promise { try { + // 插件页首屏依赖插件记录、制品、构建、插件服务模式和观测数据; + // 并行请求可以减少进入页面时的等待时间。 const [data, artifacts, builds, service, instrumentation] = await Promise.all([ api("/plugins"), api("/plugin-artifacts"), @@ -64,6 +68,7 @@ export async function loadPlugins(): Promise { state.pluginInstrumentation = instrumentation.instrumentation || []; const firstPlugin = state.plugins[0]; if (!state.selectedPluginID && firstPlugin) { + // 初次进入时默认选中第一个已纳管插件;未纳管制品会在列表中单独展示。 state.selectedPluginID = firstPlugin.id; } renderPlugins(); @@ -79,6 +84,7 @@ export async function loadPlugins(): Promise { export function renderPlugins(): void { const managed = new Set(state.plugins.map((plugin) => plugin.id)); + // 未纳管制品还没有 plugins 表记录,但仍要展示,方便管理员创建期望状态。 const unmanagedArtifacts = state.pluginArtifacts.filter((artifact) => !managed.has(artifact.plugin_id)); renderPluginServicePanel(); el("pluginsBody").innerHTML = state.plugins.map((plugin) => ` @@ -136,6 +142,7 @@ export async function loadPluginDetail(pluginID: string): Promise { try { const data = await api(`/plugins/${encodeURIComponent(pluginID)}`); if (data.plugin) { + // 详情接口返回完整插件视图,用它回填列表中的摘要记录。 state.plugins = state.plugins.map((plugin) => plugin.id === data.plugin?.id ? data.plugin : plugin); if (!state.plugins.some((plugin) => plugin.id === data.plugin?.id)) { state.plugins.push(data.plugin); @@ -152,11 +159,13 @@ export async function loadPluginDetail(pluginID: string): Promise { export function renderPluginDetail(plugin: PluginView | null = selectedPlugin()): void { const detail = el("pluginDetail"); if (!plugin) { + // 没有选中纳管插件时展示制品库存和源码构建入口。 detail.innerHTML = uploadInventoryDetail(); bindInventoryEvents(); return; } const canWrite = isAdmin(); + // 插件详情拆成多个小面板,避免配置、治理、构建和运维信息混成一个长表格。 detail.innerHTML = `
@@ -252,6 +261,7 @@ export function renderPluginDetail(plugin: PluginView | null = selectedPlugin()) } export function bindPluginEvents(): void { + // 顶层插件页事件只绑定一次;详情区会在每次重绘后重新绑定动态按钮。 el("pluginUploadInput").addEventListener("change", uploadPluginPackage); el("refreshPluginsBtn").addEventListener("click", loadPlugins); } @@ -263,6 +273,7 @@ function renderPluginServicePanel(): void { } const service = state.pluginService?.service; const canWrite = isAdmin(); + // 插件服务模式决定插件在进程内运行还是进入未来的独立/沙箱运行模式。 container.innerHTML = `
@@ -305,6 +316,7 @@ async function updatePluginServiceMode(event: Event): Promise { event.preventDefault(); const form = event.currentTarget as HTMLFormElement; try { + // 服务模式变更可能需要后端迁移或重启,因此保存后立即刷新插件页状态。 await api("/plugin-service", { method: "PUT", body: { desired_mode: getFormInput(form, "desired_mode") }, @@ -342,6 +354,7 @@ async function uploadPluginPackage(event: Event): Promise { const formData = new FormData(); formData.set("artifact", file); try { + // 浏览器只负责上传文件;manifest 校验、哈希和制品类型判断由后端完成。 await api("/plugin-artifacts", { method: "POST", formData }); input.value = ""; await loadPlugins(); @@ -352,6 +365,7 @@ async function uploadPluginPackage(event: Event): Promise { } function bindPluginDetailEvents(plugin: PluginView): void { + // 详情区每次重绘都会替换 DOM,因此按钮事件必须在重绘后重新绑定。 document.getElementById("pluginDryRunBtn")?.addEventListener("click", () => dryRunConfig(plugin)); document.getElementById("pluginSaveConfigBtn")?.addEventListener("click", () => saveConfig(plugin)); const secretForm = document.getElementById("pluginSecretForm"); @@ -389,6 +403,7 @@ function bindPluginDetailEvents(plugin: PluginView): void { async function dryRunConfig(plugin: PluginView): Promise { try { + // dry-run 不保存配置,只返回脱敏后的校验结果、diff 和是否需要重启。 const data = await api(`/plugins/${encodeURIComponent(plugin.id)}/config/dry-run`, { method: "POST", body: { @@ -405,6 +420,7 @@ async function dryRunConfig(plugin: PluginView): Promise { async function saveConfig(plugin: PluginView): Promise { try { + // 配置保存写入期望状态;后端会根据当前制品和运行态判断是否可热加载。 await api(`/plugins/${encodeURIComponent(plugin.id)}/config`, { method: "PUT", body: { @@ -425,6 +441,7 @@ async function saveSecret(event: SubmitEvent, plugin: PluginView): Promise event.preventDefault(); const form = event.currentTarget as HTMLFormElement; try { + // 密钥值不回显,保存后通过重新加载详情刷新版本号和 reload 标记。 await api(`/plugins/${encodeURIComponent(plugin.id)}/secrets`, { method: "POST", body: { @@ -448,6 +465,7 @@ async function runPluginAction(pluginID: string, action: string): Promise return; } try { + // enable/disable/load/delete 等动作都走统一动作接口,后端负责审计和操作日志。 await api(`/plugins/${encodeURIComponent(pluginID)}/${action}`, { method: "POST", body: {} }); if (action === "delete") { state.selectedPluginID = ""; @@ -463,6 +481,7 @@ async function runPluginAction(pluginID: string, action: string): Promise async function createDesiredFromArtifact(artifact: PluginArtifact): Promise { try { + // 从未纳管制品创建 disabled 期望状态,管理员随后可以编辑配置再启用。 await api(`/plugins/${encodeURIComponent(artifact.plugin_id)}`, { method: "PUT", body: { @@ -483,6 +502,7 @@ async function createDesiredFromArtifact(artifact: PluginArtifact): Promise { try { + // 制品回滚只改期望制品;后端仍会执行治理检查和配置 dry-run。 await api(`/plugins/${encodeURIComponent(pluginID)}/rollback/artifact`, { method: "POST", body: { artifact_id: artifactID }, @@ -499,6 +519,7 @@ async function runBuildAction(pluginID: string, buildID: number, action: string) return; } try { + // 构建动作可能耗时,当前界面以刷新详情的方式展示最新构建状态。 await api(`/plugin-builds/${buildID}/${encodeURIComponent(action)}`, { method: "POST", body: {} }); await loadPluginDetail(pluginID); showAlert(""); @@ -512,6 +533,7 @@ async function rollbackSnapshot(pluginID: string, snapshotID: number, fullDesire return; } try { + // 配置快照回滚可只恢复配置,也可连同 artifact/desired state/priority 一起恢复。 await api(`/plugins/${encodeURIComponent(pluginID)}/rollback/config`, { method: "POST", body: { snapshot_id: snapshotID, full_desired: fullDesired }, @@ -528,6 +550,7 @@ async function showSnapshotDiff(pluginID: string, snapshotID: number): Promise(`/plugins/${encodeURIComponent(pluginID)}/config/snapshots/${snapshotID}/diff`); el("pluginDryRunResult").textContent = formatJSON(data.diff || {}); showAlert(""); @@ -538,6 +561,7 @@ async function showSnapshotDiff(pluginID: string, snapshotID: number): Promise { try { + // 评审记录绑定当前 desired artifact 和配置哈希,用于后续启用或回滚门禁。 await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/review`, { method: "POST", body: { artifact_id: plugin.desired_artifact_id, profile: "prod", decision: "approved" }, @@ -555,6 +579,7 @@ async function createGovernanceOverride(plugin: PluginView): Promise { return; } try { + // override 是带 TTL 的临时治理豁免,必须记录人工原因。 await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/override`, { method: "POST", body: { artifact_id: plugin.desired_artifact_id, profile: "prod", action: "enable", reason, ttl_seconds: 3600 }, @@ -568,6 +593,7 @@ async function createGovernanceOverride(plugin: PluginView): Promise { async function runGovernancePreflight(plugin: PluginView): Promise { try { + // preflight 由插件或宿主返回检查项,结果会持久化到治理面板。 const data = await api>(`/plugins/${encodeURIComponent(plugin.id)}/governance/preflight`, { method: "POST", body: { artifact_id: plugin.desired_artifact_id, config_json: configEditorValue() }, @@ -582,6 +608,7 @@ async function runGovernancePreflight(plugin: PluginView): Promise { async function runGovernanceSelfTest(plugin: PluginView): Promise { try { + // self-test 用于验证制品自身能力,不直接修改 desired state。 const data = await api>(`/plugins/${encodeURIComponent(plugin.id)}/governance/self-test`, { method: "POST", body: { artifact_id: plugin.desired_artifact_id }, @@ -600,6 +627,7 @@ async function recordGovernanceBenchmark(plugin: PluginView): Promise { return; } try { + // 手动录入基准差异用于治理门禁判断,避免高风险性能回退直接启用。 await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/benchmark`, { method: "POST", body: { @@ -630,6 +658,7 @@ async function createArtifactRevokeAdvisory(plugin: PluginView): Promise { return; } try { + // 撤销公告会让命中的制品进入隔离/阻断路径,详情刷新后展示最新治理状态。 await api("/plugin-advisories", { method: "POST", body: { @@ -649,6 +678,7 @@ async function createArtifactRevokeAdvisory(plugin: PluginView): Promise { async function loadPluginOperations(plugin: PluginView): Promise { try { + // 运维快照包含事件、日志、trace、任务、外部依赖和 GC 候选项,按需刷新即可。 const data = await api(`/plugins/${encodeURIComponent(plugin.id)}/operations`); el("pluginOperationsOutput").textContent = formatJSON(data.operations || {}); showAlert(""); @@ -659,6 +689,7 @@ async function loadPluginOperations(plugin: PluginView): Promise { async function dryRunOperationsGC(plugin: PluginView): Promise { try { + // GC dry-run 不删除文件,只展示哪些运行态数据会被保护或清理。 const data = await api(`/plugins/${encodeURIComponent(plugin.id)}/operations/gc`); el("pluginOperationsOutput").textContent = formatJSON(data); showAlert(""); @@ -669,6 +700,7 @@ async function dryRunOperationsGC(plugin: PluginView): Promise { async function loadDiagnosticPackage(plugin: PluginView): Promise { try { + // 诊断包由后端生成并脱敏,前端以 JSON 文本形式展示给管理员。 const data = await api(`/plugins/${encodeURIComponent(plugin.id)}/operations/diagnostic`); el("pluginOperationsOutput").textContent = formatJSON(data); showAlert(""); @@ -678,6 +710,7 @@ async function loadDiagnosticPackage(plugin: PluginView): Promise { } function uploadInventoryDetail(): string { + // 库存视图聚合未纳管制品和构建记录,支撑上传、构建、纳管的完整流程。 const artifact = selectedArtifact(); if (!artifact) { return ` @@ -732,6 +765,7 @@ function uploadInventoryDetail(): string { } function bindInventoryEvents(): void { + // 库存视图也是动态渲染,制品详情和纳管表单事件需要在渲染后绑定。 const artifact = selectedArtifact(); const form = document.getElementById("artifactDesiredForm"); if (artifact && form instanceof HTMLFormElement) { diff --git a/cmd/gateway/admin_frontend/src/views/routes.ts b/cmd/gateway/admin_frontend/src/views/routes.ts index 8f5af84..aab6a9e 100644 --- a/cmd/gateway/admin_frontend/src/views/routes.ts +++ b/cmd/gateway/admin_frontend/src/views/routes.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/views/routes.ts 渲染路由列表,并通过 Admin API 保存主机到上游的变更。 + import { api } from "../api.js"; import { showAlert } from "../alerts.js"; import { badge, el, escapeAttr, escapeHTML, getFormInput } from "../dom.js"; diff --git a/cmd/gateway/admin_frontend/src/views/services.ts b/cmd/gateway/admin_frontend/src/views/services.ts index ceca415..8e4537d 100644 --- a/cmd/gateway/admin_frontend/src/views/services.ts +++ b/cmd/gateway/admin_frontend/src/views/services.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/views/services.ts 渲染监听服务设置,并持久化启停、端口和选项更新。 + import { api } from "../api.js"; import { showAlert } from "../alerts.js"; import { el, escapeAttr, escapeHTML, getFormInput } from "../dom.js"; diff --git a/cmd/gateway/admin_frontend/src/views/status.ts b/cmd/gateway/admin_frontend/src/views/status.ts index 7c095c3..36244c3 100644 --- a/cmd/gateway/admin_frontend/src/views/status.ts +++ b/cmd/gateway/admin_frontend/src/views/status.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/views/status.ts 渲染管理面板上的网关健康摘要。 + import { api } from "../api.js"; import { showAlert } from "../alerts.js"; import { el, stat } from "../dom.js"; diff --git a/cmd/gateway/admin_frontend/src/views/users.ts b/cmd/gateway/admin_frontend/src/views/users.ts index 6ea7921..e533e00 100644 --- a/cmd/gateway/admin_frontend/src/views/users.ts +++ b/cmd/gateway/admin_frontend/src/views/users.ts @@ -1,3 +1,5 @@ +// cmd/gateway/admin_frontend/src/views/users.ts 渲染管理用户列表,并保存账号、角色、密码和禁用状态变更。 + import { api } from "../api.js"; import { showAlert } from "../alerts.js"; import { badge, el, escapeAttr, escapeHTML, getFormInput, getFormSelect } from "../dom.js"; diff --git a/cmd/gateway/admin_metric_handlers.go b/cmd/gateway/admin_metric_handlers.go index 5d873c2..ba7cc49 100644 --- a/cmd/gateway/admin_metric_handlers.go +++ b/cmd/gateway/admin_metric_handlers.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_metric_handlers.go 返回管理面板状态卡片使用的轻量运行时计数器。 + package main import ( diff --git a/cmd/gateway/admin_plugin_handlers.go b/cmd/gateway/admin_plugin_handlers.go index 1100aec..d763831 100644 --- a/cmd/gateway/admin_plugin_handlers.go +++ b/cmd/gateway/admin_plugin_handlers.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_plugin_handlers.go 承载插件制品、期望状态、配置、密钥、运维操作和治理检查相关的 Admin API。 + package main import ( diff --git a/cmd/gateway/admin_route_handlers.go b/cmd/gateway/admin_route_handlers.go index c000a1e..2003a21 100644 --- a/cmd/gateway/admin_route_handlers.go +++ b/cmd/gateway/admin_route_handlers.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_route_handlers.go 提供修改路由记录的 HTTP 接口,并在提交后立刻刷新内存路由快照。 + package main import ( diff --git a/cmd/gateway/admin_routes.go b/cmd/gateway/admin_routes.go index 49eaff8..757e41e 100644 --- a/cmd/gateway/admin_routes.go +++ b/cmd/gateway/admin_routes.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_routes.go 让内存中的主机到上游映射快照与 SQLite 路由记录保持同步。 + package main import ( @@ -8,10 +10,14 @@ import ( ) var ( - routeSnapshot = adminroute.NewSnapshot() + // routeSnapshot 是连接热路径读取的不可变快照;写路径通过 Store 整体替换它。 + routeSnapshot = adminroute.NewSnapshot() + // routeWriteLock 串行化路由写入和快照刷新,避免并发写导致后写库、先发布的顺序错乱。 routeWriteLock sync.Mutex ) +// refreshRouteSnapshot 从 SQLite 读取启用路由并发布到热路径。数据库尚未初始化时 +// 发布空快照,方便测试和早期启动路径调用。 func refreshRouteSnapshot(ctx context.Context) error { if adminDB == nil { publishRouteSnapshot(map[string]string{}) @@ -26,10 +32,12 @@ func refreshRouteSnapshot(ctx context.Context) error { return nil } +// publishRouteSnapshot 原子替换当前路由快照;调用方应传入新 map,避免发布后继续修改。 func publishRouteSnapshot(routes map[string]string) { routeSnapshot.Store(routes) } +// lookupRoute 是连接热路径使用的只读查找函数,不访问 SQLite。 func lookupRoute(host string) (string, bool) { return routeSnapshot.Lookup(host) } @@ -42,6 +50,7 @@ func upsertRoute(ctx context.Context, actor, host, upstream string, enabled bool routeWriteLock.Lock() defer routeWriteLock.Unlock() + // 路由写入成功后必须立即刷新内存快照,否则管理端保存的配置不会影响新连接。 if err := adminroute.NewRepository(adminDB).Upsert(ctx, actor, host, upstream, enabled, note); err != nil { return err } @@ -52,6 +61,7 @@ func deleteRoute(ctx context.Context, actor, host string) error { routeWriteLock.Lock() defer routeWriteLock.Unlock() + // 删除也走同一把锁,确保快照刷新顺序与数据库提交顺序一致。 if err := adminroute.NewRepository(adminDB).Delete(ctx, host); err != nil { return err } diff --git a/cmd/gateway/admin_runtime.go b/cmd/gateway/admin_runtime.go index 4359d77..1135c4a 100644 --- a/cmd/gateway/admin_runtime.go +++ b/cmd/gateway/admin_runtime.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_runtime.go 打开 SQLite 运行态数据库、写入默认数据,并为在线流量发布首个路由快照。 + package main import ( @@ -36,6 +38,7 @@ const ( ) var ( + // adminStartup 是启动时解析出的管理端配置;后续 HTTP handler 和静态资源注入都会读取它。 adminStartup = adminconfig.Config{ DBPath: defaultAdminDBPath, TCPAdminPort: defaultTCPPort, @@ -50,6 +53,8 @@ var ( processStartAt = time.Now() ) +// initializeGatewayRuntime 按固定顺序准备运行态:解析配置、打开数据库、迁移 schema、 +// 写入默认服务、应用服务配置、创建初始管理员、发布路由快照、最后启动插件管理器。 func initializeGatewayRuntime() error { startup, err := parseStartupConfig(os.Getenv) if err != nil { @@ -71,6 +76,7 @@ func initializeGatewayRuntime() error { if err := admindb.Migrate(db); err != nil { return err } + // 默认服务必须先存在,applyServiceConfig 才能把 SQLite 中的运行态端口写回 config。 if err := ensureDefaultServices(context.Background(), db, startup.TCPAdminPort); err != nil { return err } @@ -84,6 +90,7 @@ func initializeGatewayRuntime() error { return err } + // 插件制品放在数据库同级目录下,便于容器挂载一个 data volume 即可保留全部运行态。 pluginsManager = pluginmanager.New(pluginmanager.Options{ DB: db, ArtifactRoot: filepath.Join(filepath.Dir(startup.DBPath), "plugins", "artifacts"), @@ -93,6 +100,7 @@ func initializeGatewayRuntime() error { return pluginsManager.Reconcile(context.Background()) } +// closeGatewayRuntime 只关闭当前进程持有的数据库连接;SQLite 文件和插件制品都保留在数据目录中。 func closeGatewayRuntime() { if adminDB != nil { _ = adminDB.Close() diff --git a/cmd/gateway/admin_service_handlers.go b/cmd/gateway/admin_service_handlers.go index 6985f1a..f6065b8 100644 --- a/cmd/gateway/admin_service_handlers.go +++ b/cmd/gateway/admin_service_handlers.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_service_handlers.go 提供监听服务配置接口,用于维护端口、启停状态和是否需要重启。 + package main import ( diff --git a/cmd/gateway/admin_services.go b/cmd/gateway/admin_services.go index 1879f9e..f35b865 100644 --- a/cmd/gateway/admin_services.go +++ b/cmd/gateway/admin_services.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_services.go 从 SQLite 加载监听服务配置,并暴露规范化后的运行时服务选项。 + package main import ( diff --git a/cmd/gateway/admin_sessions.go b/cmd/gateway/admin_sessions.go index da3e63a..e061ddb 100644 --- a/cmd/gateway/admin_sessions.go +++ b/cmd/gateway/admin_sessions.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_sessions.go 提供 Admin API 认证中间件使用的内存会话管理器。 + package main import ( diff --git a/cmd/gateway/admin_static.go b/cmd/gateway/admin_static.go index 719047e..45ba04d 100644 --- a/cmd/gateway/admin_static.go +++ b/cmd/gateway/admin_static.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_static.go 嵌入构建后的管理前端,并通过网关 HTTP 处理器对外提供。 + package main import ( diff --git a/cmd/gateway/admin_static/app.css b/cmd/gateway/admin_static/app.css index e406231..b54ae46 100644 --- a/cmd/gateway/admin_static/app.css +++ b/cmd/gateway/admin_static/app.css @@ -1,3 +1,5 @@ +/* cmd/gateway/admin_static/app.css 定义嵌入式管理端仪表盘、表格、表单、对话框和响应式布局样式。 */ + :root { color-scheme: light; --bg: #f6f7f4; diff --git a/cmd/gateway/admin_static/index.html b/cmd/gateway/admin_static/index.html index 7a5008c..a79a0b7 100644 --- a/cmd/gateway/admin_static/index.html +++ b/cmd/gateway/admin_static/index.html @@ -1,3 +1,5 @@ + + diff --git a/cmd/gateway/admin_user_handlers.go b/cmd/gateway/admin_user_handlers.go index 2d6ae96..746d921 100644 --- a/cmd/gateway/admin_user_handlers.go +++ b/cmd/gateway/admin_user_handlers.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_user_handlers.go 提供管理员维护管理账号的接口,包括创建、更新、禁用和列表查询。 + package main import ( diff --git a/cmd/gateway/admin_users.go b/cmd/gateway/admin_users.go index 1ecb453..21d8ca6 100644 --- a/cmd/gateway/admin_users.go +++ b/cmd/gateway/admin_users.go @@ -1,3 +1,5 @@ +// cmd/gateway/admin_users.go 初始化管理用户仓库,并在没有账号时创建首次初始化用户。 + package main import ( diff --git a/cmd/gateway/config.go b/cmd/gateway/config.go index cf94cd2..7dfb3e0 100644 --- a/cmd/gateway/config.go +++ b/cmd/gateway/config.go @@ -1,3 +1,5 @@ +// cmd/gateway/config.go 加载静态网关配置,并与管理数据库提供的运行态状态组合使用。 + package main import ( diff --git a/cmd/gateway/config_test.go b/cmd/gateway/config_test.go index 59c67ea..2c22cb9 100644 --- a/cmd/gateway/config_test.go +++ b/cmd/gateway/config_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/config_test.go 包含用于约束 config 行为的测试。 + package main import ( diff --git a/cmd/gateway/err.go b/cmd/gateway/err.go index 5f31e35..f21ad2e 100644 --- a/cmd/gateway/err.go +++ b/cmd/gateway/err.go @@ -1,3 +1,5 @@ +// cmd/gateway/err.go 集中放置网关请求路径使用的少量哨兵错误。 + package main import "errors" diff --git a/cmd/gateway/handle_request_test.go b/cmd/gateway/handle_request_test.go index 36becec..22e5a9f 100644 --- a/cmd/gateway/handle_request_test.go +++ b/cmd/gateway/handle_request_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/handle_request_test.go 包含用于约束 handle request 行为的测试。 + package main import ( diff --git a/cmd/gateway/haproxy.go b/cmd/gateway/haproxy.go index bf09f80..ac6b327 100644 --- a/cmd/gateway/haproxy.go +++ b/cmd/gateway/haproxy.go @@ -1,3 +1,5 @@ +// cmd/gateway/haproxy.go 为需要 HAProxy PROXY 头的上游 TCP 连接先写入代理头,再回放 Minecraft 流量。 + package main import ( @@ -45,7 +47,7 @@ func haProxyUpstream(source net.Conn, host string) net.Conn { SourceAddr: sourceAddr, DestinationAddr: target, } - // After the connection was created write the proxy headers first + // 连接建立后先写入 PROXY 头,再转发 Minecraft 首包。 _, err = header.WriteTo(conn) if err != nil { log.Err(err).Msg("failed to write proxy header") diff --git a/cmd/gateway/haproxy_test.go b/cmd/gateway/haproxy_test.go index 23ce397..4dbb353 100644 --- a/cmd/gateway/haproxy_test.go +++ b/cmd/gateway/haproxy_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/haproxy_test.go 包含用于约束 haproxy 行为的测试。 + package main import ( diff --git a/cmd/gateway/kcp.go b/cmd/gateway/kcp.go index bee38e2..4b36856 100644 --- a/cmd/gateway/kcp.go +++ b/cmd/gateway/kcp.go @@ -1,3 +1,5 @@ +// cmd/gateway/kcp.go 启动可选的 KCP 监听器,并把接收到的会话转入统一网关请求处理流程。 + package main import ( @@ -14,6 +16,7 @@ func runKcp(wg *sync.WaitGroup) { defer wg.Done() } + // KCP 监听使用运行态服务配置中的分片参数,和上游拨号保持一致。 listener, err := kcp.ListenWithOptions(fmt.Sprintf(":%d", config.Kcp.Port), nil, config.Kcp.DataShards, config.Kcp.ParityShards) if err != nil { log.Fatal().Err(err). @@ -37,11 +40,13 @@ func runKcp(wg *sync.WaitGroup) { tuneKcpConn(conn) + // KCP session 实现 net.Conn,可以直接进入统一网关请求流程。 go handleRequest(conn) } } func upstreamKcp(host string) net.Conn { + // KCP 上游使用与入口相同的 data/parity shards,确保两端编码参数匹配。 conn, err := kcp.DialWithOptions(host, nil, config.Kcp.DataShards, config.Kcp.ParityShards) if err != nil { gatewayMetrics.UpstreamDialError() @@ -55,6 +60,7 @@ func upstreamKcp(host string) net.Conn { } func tuneKcpConn(conn *kcp.UDPSession) { + // 这里偏向低延迟交互:stream mode 模拟 TCP 字节流,禁用写延迟并打开快速 ACK。 conn.SetStreamMode(true) conn.SetWriteDelay(false) conn.SetNoDelay(1, 10, 2, 1) diff --git a/cmd/gateway/log.go b/cmd/gateway/log.go index 03366c2..5d9ae0b 100644 --- a/cmd/gateway/log.go +++ b/cmd/gateway/log.go @@ -1,3 +1,5 @@ +// cmd/gateway/log.go 配置网关日志、日志文件、日志级别和日志轮转钩子。 + package main import ( diff --git a/cmd/gateway/log_notunix.go b/cmd/gateway/log_notunix.go index 3a031a0..3478905 100644 --- a/cmd/gateway/log_notunix.go +++ b/cmd/gateway/log_notunix.go @@ -1,3 +1,5 @@ +// cmd/gateway/log_notunix.go 在没有 Unix 信号的平台上提供空的日志轮转信号钩子。 + // pid_unix.go //go:build !unix && !plan9 @@ -8,8 +10,8 @@ import ( ) func handleLogRotate() { - // No-op for non-unix platforms - // Log rotation is not supported on this platform - // This function can be left empty or removed if not needed + // 非 Unix 平台不执行日志轮转信号处理。 + // 该平台不支持通过信号触发日志轮转。 + // 保留空实现是为了让跨平台调用点保持一致。 log.Info().Msg("Log rotation is not supported on this platform") } diff --git a/cmd/gateway/log_pid_test.go b/cmd/gateway/log_pid_test.go index 84be2f7..1f2d8dc 100644 --- a/cmd/gateway/log_pid_test.go +++ b/cmd/gateway/log_pid_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/log_pid_test.go 包含用于约束 log pid 行为的测试。 + package main import ( diff --git a/cmd/gateway/log_unix.go b/cmd/gateway/log_unix.go index cf1446c..f70222b 100644 --- a/cmd/gateway/log_unix.go +++ b/cmd/gateway/log_unix.go @@ -1,3 +1,5 @@ +// cmd/gateway/log_unix.go 注册 Unix 信号处理,让进程无需完整重启即可重新打开日志文件。 + // pid_unix.go //go:build unix || plan9 diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index 8200c6c..0004192 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -1,3 +1,5 @@ +// cmd/gateway/main.go 负责网关进程启动、监听器选择、Minecraft 握手路由、插件钩子分发以及上游转发交接。 + package main import ( @@ -17,6 +19,8 @@ import ( ) func main() { + // 插件子命令复用网关二进制。这里先于运行态配置加载处理它们, + // 这样本地构建和清单命令不需要一份可用的网关部署配置。 if handled, code := runPluginCLI(os.Args[1:]); handled { os.Exit(code) } @@ -39,8 +43,13 @@ func main() { } func startEnabledServices() { + // TCP 和 Admin HTTP 始终通过共享监听器启动。共享监听器按每条连接 + // 的首包判断它是 HTTP 还是 Minecraft 协议数据,因此不需要额外维护 + // 一个手动模式开关。 startService(runTcpWebPortReuse) + // 可选传输最终仍进入 handleRequest,这让插件过滤、路由解析和上游拨号 + // 在 TCP、KCP、QUIC 和 WebSocket 入口之间保持一致。 if config.Kcp.Enable { startService(runKcp) } @@ -61,6 +70,8 @@ func handleRequest(conn net.Conn) { gatewayMetrics.ConnectionStarted() defer gatewayMetrics.ConnectionFinished() + // 插件或协议解析器的 panic 不能杀掉监听协程;当前连接会被放弃, + // 进程继续服务其他客户端。 defer func() { rec := recover() if rec == nil { @@ -91,6 +102,8 @@ func handleRequest(conn net.Conn) { } func mapToHost(conn net.Conn) net.Conn { + // 连接过滤器在读取 Minecraft 握手前执行,因此可以按来源地址或传输类型 + // 拒绝连接,同时不消耗客户端发送的协议字节。 if pluginsManager != nil { transport, _, _ := connectionIngress(conn) filter, err := pluginsManager.FilterConnection(context.Background(), api.ConnectionFilterRequest{ @@ -124,6 +137,8 @@ func mapToHost(conn net.Conn) net.Conn { return nil } + // 第一次读取包含 Minecraft 握手数据。所有过滤器和路由决策完成后, + // 这段数据必须原样或按插件改写后回放给选中的上游。 initialData := append([]byte(nil), buf[:n]...) handshake := protocol.ParseHandshake(initialData) if handshake.ServerHost == "" { @@ -133,6 +148,8 @@ func mapToHost(conn net.Conn) net.Conn { return nil } + // 握手过滤器可以改写目标主机名。发生改写时要立刻重建首包, + // 确保上游看到的是改写后的 Minecraft 主机名,而不是客户端原始值。 if pluginsManager != nil { filter, err := pluginsManager.FilterHandshake(context.Background(), api.HandshakeFilterRequest{ SourceAddr: conn.RemoteAddr().String(), @@ -155,6 +172,8 @@ func mapToHost(conn net.Conn) net.Conn { } } + // 状态查询使用 NextState=1,并且可以由插件直接完整响应。 + // 如果这里已经处理,就不会再为该查询打开上游连接。 if handshake.NextState == 1 { if handled := handleStatusPing(conn, handshake); handled { return nil @@ -226,6 +245,8 @@ func mapToHost(conn net.Conn) net.Conn { if client == nil { target := upstreamtarget.Parse(host) + // 路由值可以通过前缀选择非 TCP 传输;普通地址仍按 TCP 处理, + // 以保持旧配置的行为不变。 switch target.Protocol { case upstreamtarget.ProtocolQUIC: client = upstreamQuic(target.Address) @@ -241,6 +262,8 @@ func mapToHost(conn net.Conn) net.Conn { return nil } + // 只有在上游路径确定后才回放握手数据。这样插件在任何上游字节发出前, + // 都还有机会阻断、代理或改写连接。 if err := writeAll(client, initialData); err != nil { log.Err(err). Str("client", conn.RemoteAddr().String()). @@ -256,6 +279,8 @@ func mapToHost(conn net.Conn) net.Conn { func resolveGatewayRoute(conn net.Conn, handshake protocol.Handshake) pluginmanager.RouteResolveResult { upstream, hit := lookupRoute(handshake.ServerHost) + // SQLite 快照始终作为本地兜底。插件会同时拿到兜底决策和刷新回调, + // 因此可以选择性覆盖路由,而不必在插件里复制一套路由仓库逻辑。 req := api.RouteResolveRequest{ Host: handshake.ServerHost, RawServerHost: handshake.RawServerHost, @@ -283,6 +308,7 @@ func resolveGatewayRoute(conn net.Conn, handshake protocol.Handshake) pluginmana action := api.RouteDecisionFallback source := "sqlite_fallback" if upstream == "" { + // 没有命中兜底路由时统一表示为拒绝决策,便于热路径记录一致的失败形态。 action = api.RouteDecisionReject source = "fallback_miss" } @@ -296,6 +322,8 @@ func handleStatusPing(conn net.Conn, handshake protocol.Handshake) bool { if pluginsManager == nil { return false } + // Minecraft 状态响应是带长度前缀的 JSON 数据包。插件只提供高层字段, + // Minecraft 协议封包由 protocol.StatusResponsePacket 统一完成。 result, err := pluginsManager.StatusPing(context.Background(), api.StatusPingRequest{ Host: handshake.ServerHost, RawServerHost: handshake.RawServerHost, @@ -339,6 +367,8 @@ func handleStatusPing(conn net.Conn, handshake protocol.Handshake) bool { func newUpstreamConnectRequest(conn net.Conn, upstream string, handshake protocol.Handshake, initialData []byte, routeHit bool) api.UpstreamConnectRequest { target := upstreamtarget.Parse(upstream) transport, serviceName, listenerPort := connectionIngress(conn) + // InitialData 使用副本,避免上游插件在其他处理器或日志路径仍引用回放缓冲区时 + // 意外修改调用方持有的数据。 req := api.UpstreamConnectRequest{ Source: conn, Host: handshake.ServerHost, @@ -367,6 +397,8 @@ func newUpstreamConnectRequest(conn net.Conn, upstream string, handshake protoco func connectionIngress(conn net.Conn) (transport string, serviceName string, listenerPort int) { transport = "tcp" serviceName = serviceNameTCPAdmin + // 具体连接包装类型记录了客户端来自哪个监听器。该元数据会传给插件, + // 并出现在运维诊断中,同时不需要改变 net.Conn 接口。 switch conn.(type) { case *webSocketConn: transport = "websocket" diff --git a/cmd/gateway/main_test.go b/cmd/gateway/main_test.go index 9f62653..32d3b3c 100644 --- a/cmd/gateway/main_test.go +++ b/cmd/gateway/main_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/main_test.go 包含用于约束 gateway 行为的测试。 + package main import ( diff --git a/cmd/gateway/pid.go b/cmd/gateway/pid.go index 62f58a4..10bcc71 100644 --- a/cmd/gateway/pid.go +++ b/cmd/gateway/pid.go @@ -1,3 +1,5 @@ +// cmd/gateway/pid.go 维护 pid 文件的写入和清理,供进程管理器按文件追踪网关进程。 + package main import ( diff --git a/cmd/gateway/pid_unix.go b/cmd/gateway/pid_unix.go index a741f5d..348a9e3 100644 --- a/cmd/gateway/pid_unix.go +++ b/cmd/gateway/pid_unix.go @@ -1,3 +1,5 @@ +// cmd/gateway/pid_unix.go 实现 Unix 平台的 pid 文件占用检查,避免覆盖仍在运行的进程记录。 + // pid_unix.go //go:build unix || plan9 diff --git a/cmd/gateway/pid_unix_test.go b/cmd/gateway/pid_unix_test.go index 79b132e..1c461a1 100644 --- a/cmd/gateway/pid_unix_test.go +++ b/cmd/gateway/pid_unix_test.go @@ -1,5 +1,7 @@ //go:build unix || plan9 +// cmd/gateway/pid_unix_test.go 包含用于约束 pid unix 行为的测试。 + package main import "testing" diff --git a/cmd/gateway/pid_windows.go b/cmd/gateway/pid_windows.go index 66e5087..b970e96 100644 --- a/cmd/gateway/pid_windows.go +++ b/cmd/gateway/pid_windows.go @@ -1,3 +1,5 @@ +// cmd/gateway/pid_windows.go 在 Windows 上提供可移植的 pid 文件占用检查替代实现。 + // pid_windows.go //go:build windows diff --git a/cmd/gateway/plugin.go b/cmd/gateway/plugin.go index fd4c8a2..2d5d396 100644 --- a/cmd/gateway/plugin.go +++ b/cmd/gateway/plugin.go @@ -1,3 +1,5 @@ +// cmd/gateway/plugin.go 把网关运行时接入 pluginmanager,负责钩子分发和插件生命周期加载。 + package main import ( @@ -96,12 +98,12 @@ func loadPlugins() { } } -// HandleConn implements api.Gateway. +// HandleConn 实现 api.Gateway,用于让插件把连接交回网关主流程。 func (g *Gateway) HandleConn(conn net.Conn) { go handleRequest(conn) } -// Hook implements api.Gateway. +// Hook 实现 api.Gateway,用于注册旧版内存钩子处理器。 func (g *Gateway) Hook(hook string, handler any) error { pluginLock.Lock() defer pluginLock.Unlock() @@ -110,7 +112,7 @@ func (g *Gateway) Hook(hook string, handler any) error { return nil } -// ExitWaitGroup implements api.Gateway. +// ExitWaitGroup 实现 api.Gateway,用于把插件后台任务纳入进程退出等待。 func (g *Gateway) ExitWaitGroup() *sync.WaitGroup { return &exitWaitGroup } @@ -152,7 +154,7 @@ func (g *Gateway) RegisterBackgroundTask(task api.BackgroundTask) error { return nil } -// TestOp implements api.Gateway. +// TestOp 实现 api.Gateway,保留给测试或调试插件能力探测。 func (g *Gateway) TestOp() { panic("unimplemented") } diff --git a/cmd/gateway/plugin_cli.go b/cmd/gateway/plugin_cli.go index 0223c14..71855dc 100644 --- a/cmd/gateway/plugin_cli.go +++ b/cmd/gateway/plugin_cli.go @@ -1,3 +1,5 @@ +// cmd/gateway/plugin_cli.go 分发插件相关子命令,包括本地脚手架、构建、清单和远程管理操作。 + package main import ( diff --git a/cmd/gateway/plugin_cli_manifest.go b/cmd/gateway/plugin_cli_manifest.go index 7eb635b..87c8c78 100644 --- a/cmd/gateway/plugin_cli_manifest.go +++ b/cmd/gateway/plugin_cli_manifest.go @@ -1,3 +1,5 @@ +// cmd/gateway/plugin_cli_manifest.go 实现插件包清单的查看、校验、特性列表和格式化命令。 + package main import ( diff --git a/cmd/gateway/plugin_cli_remote.go b/cmd/gateway/plugin_cli_remote.go index bf5571a..6711a61 100644 --- a/cmd/gateway/plugin_cli_remote.go +++ b/cmd/gateway/plugin_cli_remote.go @@ -1,3 +1,5 @@ +// cmd/gateway/plugin_cli_remote.go 实现通过 Admin API 驱动插件管理操作的命令行客户端。 + package main import ( diff --git a/cmd/gateway/plugin_cli_toolchain.go b/cmd/gateway/plugin_cli_toolchain.go index cfc6466..bb8db71 100644 --- a/cmd/gateway/plugin_cli_toolchain.go +++ b/cmd/gateway/plugin_cli_toolchain.go @@ -1,3 +1,5 @@ +// cmd/gateway/plugin_cli_toolchain.go 包含插件开发者使用的本地源码模板、构建打包流程和测试辅助逻辑。 + package main import ( @@ -1669,7 +1671,7 @@ func manifestYAMLTemplate(opts pluginInitCLIOptions) string { if opts.Template == "protocol-proxy" { mode = pluginmanager.UpstreamModeProtocolProxy } - return fmt.Sprintf(`# Human-maintained plugin manifest. Build packages normalize this into manifest.json. + return fmt.Sprintf(`# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。 schema_version: mc-gateway.plugin/v1 id: %q name: %q @@ -1713,7 +1715,7 @@ func manifestTOMLTemplate(opts pluginInitCLIOptions) string { if opts.Template == "protocol-proxy" { mode = pluginmanager.UpstreamModeProtocolProxy } - return fmt.Sprintf(`# Human-maintained plugin manifest. Build packages normalize this into manifest.json. + return fmt.Sprintf(`# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。 schema_version = "mc-gateway.plugin/v1" id = %q name = %q diff --git a/cmd/gateway/plugin_cli_toolchain_test.go b/cmd/gateway/plugin_cli_toolchain_test.go index 2b7aa6d..2d08d82 100644 --- a/cmd/gateway/plugin_cli_toolchain_test.go +++ b/cmd/gateway/plugin_cli_toolchain_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/plugin_cli_toolchain_test.go 包含用于约束 plugin cli toolchain 行为的测试。 + package main import ( diff --git a/cmd/gateway/plugin_test.go b/cmd/gateway/plugin_test.go index 39d5309..d102271 100644 --- a/cmd/gateway/plugin_test.go +++ b/cmd/gateway/plugin_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/plugin_test.go 包含用于约束 plugin 行为的测试。 + package main import ( diff --git a/cmd/gateway/quic.go b/cmd/gateway/quic.go index eb4cbbe..c9b7618 100644 --- a/cmd/gateway/quic.go +++ b/cmd/gateway/quic.go @@ -1,3 +1,5 @@ +// cmd/gateway/quic.go 启动可选的 QUIC 监听器,并把 QUIC 流适配到普通网关连接流程。 + package main import ( @@ -19,6 +21,8 @@ import ( ) type ( + // quicConn 把 QUIC connection 和单条 stream 组合成 net.Conn 风格对象, + // 使后续转发逻辑不用区分 TCP 与 QUIC。 quicConn struct { quic.Connection quic.Stream @@ -30,6 +34,7 @@ func runQuic(wg *sync.WaitGroup) { defer wg.Done() } + // QUIC 基于 UDP 监听,端口来自运行态服务配置。 udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: config.Quic.Port}) if err != nil { log.Panic().Err(err).Msg("Failed to listen UDP") @@ -41,6 +46,7 @@ func runQuic(wg *sync.WaitGroup) { log.Panic().Err(err).Msg("Failed to generate TLS config") } + // quic-go 的 listener 接收 connection,真正的字节流在 stream 中。 ln, err := quic.Listen(udpConn, tlsConf, nil) if err != nil { log.Panic().Err(err).Msg("Failed to listen QUIC") @@ -63,11 +69,13 @@ func runQuic(wg *sync.WaitGroup) { func upstreamQuic(host string) net.Conn { tlsConf := &tls.Config{ + // 网关自管的 QUIC 上游默认使用临时证书,当前先跳过证书校验。 InsecureSkipVerify: true, // 跳过证书检查 NextProtos: getQuicNextProtos(), } - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s handshake timeout + // 上游握手使用短超时,避免连接协程在不可达上游上长期等待。 + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() conn, err := quic.DialAddr(ctx, host, tlsConf, nil) @@ -86,6 +94,7 @@ func upstreamQuic(host string) net.Conn { } log.Debug().Str("host", host).Msg("QUIC stream opened") + // 返回的 quicConn 后续会收到 Minecraft 首包回放并进入普通双向转发。 return quicConn{ Connection: conn, Stream: stream, @@ -95,6 +104,7 @@ func upstreamQuic(host string) net.Conn { func handleQuicRequest(conn quic.Connection) { defer conn.CloseWithError(0, "Closing connection") + // 入口连接只等待第一条 stream;该 stream 承载完整 Minecraft 字节流。 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() @@ -111,33 +121,33 @@ func handleQuicRequest(conn quic.Connection) { } func generateTLSConfig() (*tls.Config, error) { - // 生成私钥 + // 生成临时私钥;当前 QUIC 入口不依赖磁盘证书文件。 priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } - // 创建证书模板 + // 创建自签证书模板,满足 QUIC TLS 握手要求。 template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{"Example Org"}, }, NotBefore: time.Now(), - NotAfter: time.Now().Add(365 * 24 * time.Hour), // 有效期 1 年 + NotAfter: time.Now().Add(365 * 24 * time.Hour), // 有效期 1 年。 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } - // 自签名证书 + // 自签名证书用于当前进程生命周期内的 QUIC 监听。 certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return nil, err } - // 编码证书和私钥 + // 编码证书和私钥,再交给 tls.X509KeyPair 解析为标准证书结构。 certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) keyPEM, err := x509.MarshalECPrivateKey(priv) if err != nil { @@ -145,13 +155,13 @@ func generateTLSConfig() (*tls.Config, error) { } keyPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyPEM}) - // 加载到 tls.Certificate + // 加载到 tls.Certificate。 cert, err := tls.X509KeyPair(certPEM, keyPEMBlock) if err != nil { return nil, err } - // 返回 tls.Config + // 返回 QUIC listener 使用的 TLS 配置。 return &tls.Config{ Certificates: []tls.Certificate{cert}, NextProtos: getQuicNextProtos(), @@ -161,7 +171,7 @@ func generateTLSConfig() (*tls.Config, error) { func getQuicNextProtos() []string { nextProtos := config.Quic.ApplicationProtocols if len(nextProtos) == 0 { - return []string{"minecraft", "quic", "raw", "h3"} // 默认协议 + return []string{"minecraft", "quic", "raw", "h3"} // 默认协议列表。 } return nextProtos } @@ -172,10 +182,12 @@ func (c quicConn) Close() error { } func (c quicConn) CloseWrite() error { + // QUIC stream 关闭写方向即可通知对端没有更多数据。 return c.Stream.Close() } func (c quicConn) CloseRead() error { + // CancelRead 用于停止接收方向,匹配 relay.go 中的半关闭调用。 c.Stream.CancelRead(0) return nil } diff --git a/cmd/gateway/quic_test.go b/cmd/gateway/quic_test.go index c4b6fee..b009c85 100644 --- a/cmd/gateway/quic_test.go +++ b/cmd/gateway/quic_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/quic_test.go 包含用于约束 quic 行为的测试。 + package main import ( diff --git a/cmd/gateway/relay.go b/cmd/gateway/relay.go index efdf6b8..e595149 100644 --- a/cmd/gateway/relay.go +++ b/cmd/gateway/relay.go @@ -1,3 +1,5 @@ +// cmd/gateway/relay.go 实现客户端与上游之间的双向复制循环和转发缓冲池。 + package main import ( @@ -10,6 +12,7 @@ import ( const proxyBufferSize = 64 * 1024 +// proxyBufferPool 为普通 io.CopyBuffer 路径复用 64KiB 缓冲区,降低长连接转发时的分配压力。 var proxyBufferPool = sync.Pool{ New: func() any { buf := make([]byte, proxyBufferSize) @@ -30,6 +33,7 @@ type ( func proxyConnections(a, b io.ReadWriter) { var wg sync.WaitGroup + // 两个方向独立复制,任意一侧读到 EOF 后通过半关闭通知对端。 wg.Add(2) go func() { defer wg.Done() @@ -44,6 +48,7 @@ func proxyConnections(a, b io.ReadWriter) { } func proxyCopy(dst io.Writer, src io.Reader) { + // 转发协程不能把 panic 带出到连接处理主协程;记录后关闭对应方向即可。 defer recoverProxyCopy() defer closeRead(src) defer closeWrite(dst) @@ -55,6 +60,8 @@ func proxyCopy(dst io.Writer, src io.Reader) { } func copyForward(dst io.Writer, src io.Reader) (int64, error) { + // 优先使用标准库为具体类型提供的零拷贝/优化路径,只有普通 reader/writer + // 才落到共享缓冲区。 if _, ok := src.(io.WriterTo); ok { return io.Copy(dst, src) } @@ -81,6 +88,7 @@ func putProxyBuffer(buf []byte) { } func writeAll(w io.Writer, buf []byte) error { + // net.Conn.Write 允许短写;首包回放和 PROXY 头写入必须循环直到写完。 for len(buf) > 0 { n, err := w.Write(buf) if n > 0 { @@ -98,6 +106,7 @@ func writeAll(w io.Writer, buf []byte) error { } func closeWrite(conn any) { + // TCP 支持半关闭时只关闭写方向,让反向复制还有机会读完剩余数据。 if closer, ok := conn.(closeWriter); ok { if err := closer.CloseWrite(); err != nil { log.Debug().Err(err).Msg("failed to close write side") @@ -113,6 +122,7 @@ func closeWrite(conn any) { } func closeRead(conn any) { + // 支持 CloseRead 的连接可以显式停止读方向,帮助对端更快感知转发结束。 if closer, ok := conn.(closeReader); ok { if err := closer.CloseRead(); err != nil { log.Debug().Err(err).Msg("failed to close read side") diff --git a/cmd/gateway/relay_benchmark_test.go b/cmd/gateway/relay_benchmark_test.go index b72e928..5fd72f8 100644 --- a/cmd/gateway/relay_benchmark_test.go +++ b/cmd/gateway/relay_benchmark_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/relay_benchmark_test.go 包含用于约束 relay benchmark 行为的测试。 + package main import ( diff --git a/cmd/gateway/relay_test.go b/cmd/gateway/relay_test.go index bf54210..0c9ec8c 100644 --- a/cmd/gateway/relay_test.go +++ b/cmd/gateway/relay_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/relay_test.go 包含用于约束 relay 行为的测试。 + package main import ( diff --git a/cmd/gateway/tcp.go b/cmd/gateway/tcp.go index 0957d9b..e3d131f 100644 --- a/cmd/gateway/tcp.go +++ b/cmd/gateway/tcp.go @@ -1,3 +1,5 @@ +// cmd/gateway/tcp.go 在未与 Admin HTTP 共用端口时启动普通 TCP Minecraft 监听器。 + package main import ( @@ -34,13 +36,14 @@ func runTcp(wg *sync.WaitGroup) { continue } setSocketOptions(conn) - // 处理连接 + // 处理连接;后续握手解析、插件过滤和路由解析都在 handleRequest 中完成。 gatewayMetrics.TCPConnectionStarted() go handleRequest(conn) } } func upstreamTcp(host string) net.Conn { + // TCP 是默认上游传输,路由值没有协议前缀时都会走这里。 conn, err := tcpDialer.Dial("tcp", host) if err != nil { gatewayMetrics.UpstreamDialError() @@ -53,13 +56,14 @@ func upstreamTcp(host string) net.Conn { } var tcpDialer = net.Dialer{ + // 上游拨号失败应尽快返回给客户端连接处理流程,避免连接协程长期堆积。 Timeout: 3 * time.Second, KeepAlive: 30 * time.Second, } func setSocketOptions(conn net.Conn) { if tcpConn, ok := conn.(*net.TCPConn); ok { - tcpConn.SetNoDelay(true) // 禁用 Nagle 算法 + tcpConn.SetNoDelay(true) // 禁用 Nagle 算法,降低 Minecraft 交互延迟。 tcpConn.SetKeepAlive(true) tcpConn.SetKeepAlivePeriod(30 * time.Second) } diff --git a/cmd/gateway/tcp_test.go b/cmd/gateway/tcp_test.go index 3bd7d0c..b560317 100644 --- a/cmd/gateway/tcp_test.go +++ b/cmd/gateway/tcp_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/tcp_test.go 包含用于约束 tcp 行为的测试。 + package main import ( diff --git a/cmd/gateway/tcp_web_port_reuse.go b/cmd/gateway/tcp_web_port_reuse.go index ff5707d..66a4a4c 100644 --- a/cmd/gateway/tcp_web_port_reuse.go +++ b/cmd/gateway/tcp_web_port_reuse.go @@ -1,3 +1,5 @@ +// cmd/gateway/tcp_web_port_reuse.go 启动共享 TCP/Admin 监听器,按连接首包自动区分 HTTP 流量和 Minecraft 流量。 + package main import ( @@ -12,11 +14,13 @@ import ( ) const ( - defaultTCPPort = 25565 + defaultTCPPort = 25565 + // 首包超时沿用 tcphttpmux 默认值,保持同端口分流逻辑的单一来源。 tcpWebInitialPacketTimeout = tcphttpmux.DefaultInitialPacketTimeout ) func normalizedTCPPort() int { + // 静态配置未指定端口时保持 Minecraft 默认端口。 if config.Tcp.Port == 0 { return defaultTCPPort } @@ -31,6 +35,7 @@ func normalizedWebSocketPort() int { } func normalizedWebSocketPath() string { + // WebSocket 路径为空时回退到根路径,避免生成空的 HTTP 路由。 if config.WebSocket.Path == "" { return "/" } @@ -38,6 +43,7 @@ func normalizedWebSocketPath() string { } func tcpWebPortReuseEnabled() bool { + // 是否共用端口完全由启用状态和端口相等推导,不引入额外配置开关。 return config.Tcp.Enable && config.WebSocket.Enable && normalizedTCPPort() == normalizedWebSocketPort() @@ -49,6 +55,7 @@ func runTcpWebPortReuse(wg *sync.WaitGroup) { } port := normalizedTCPPort() + // 同一个 listener 同时承载 Minecraft TCP 和 Admin HTTP,由 serveTcpWebPortReuse 分流。 listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { log.Fatal().Err(err). @@ -69,6 +76,7 @@ func runTcpWebPortReuse(wg *sync.WaitGroup) { } func serveTcpWebPortReuse(listener net.Listener, handler http.Handler, tcpHandler func(net.Conn)) error { + // tcphttpmux 只负责协议分流;指标、socket 选项和日志通过回调接回主包。 return tcphttpmux.Serve(listener, handler, tcpHandler, tcphttpmux.Options{ InitialPacketTimeout: tcpWebInitialPacketTimeout, SetSocketOptions: setSocketOptions, diff --git a/cmd/gateway/tcp_web_port_reuse_test.go b/cmd/gateway/tcp_web_port_reuse_test.go index d4c84d5..24fbaf9 100644 --- a/cmd/gateway/tcp_web_port_reuse_test.go +++ b/cmd/gateway/tcp_web_port_reuse_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/tcp_web_port_reuse_test.go 包含用于约束 tcp web port reuse 行为的测试。 + package main import ( diff --git a/cmd/gateway/test_helpers_test.go b/cmd/gateway/test_helpers_test.go index c53a0b8..70880a3 100644 --- a/cmd/gateway/test_helpers_test.go +++ b/cmd/gateway/test_helpers_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/test_helpers_test.go 包含用于约束 test helpers 行为的测试。 + package main import ( diff --git a/cmd/gateway/websocket.go b/cmd/gateway/websocket.go index 9a7cf79..ab643f9 100644 --- a/cmd/gateway/websocket.go +++ b/cmd/gateway/websocket.go @@ -1,3 +1,5 @@ +// cmd/gateway/websocket.go 把 WebSocket 会话适配为 net.Conn,让浏览器客户端复用网关请求路径。 + package main import ( @@ -14,7 +16,7 @@ import ( var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { - // 允许所有来源的连接(生产环境中应该更严格) + // 当前网关把 WebSocket 当作传输层入口,先允许所有来源;生产暴露时应在反向代理层收紧来源。 return true }, } @@ -38,12 +40,14 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) { defer conn.Close() gatewayMetrics.WebSocketConnectionStarted() + // WebSocket 连接包装为 net.Conn 后进入同一个 handleRequest,复用插件、路由和转发逻辑。 handleRequest(&webSocketConn{Conn: conn}) } func (w *webSocketConn) Read(b []byte) (n int, err error) { for { if w.reader != nil { + // 当前消息帧没读完前持续从同一个 reader 读取,模拟流式 net.Conn。 n, err = w.reader.Read(b) if errors.Is(err, io.EOF) { w.reader = nil @@ -60,6 +64,7 @@ func (w *webSocketConn) Read(b []byte) (n int, err error) { return 0, err } if messageType != websocket.BinaryMessage && messageType != websocket.TextMessage { + // 控制帧不进入 Minecraft 协议流。 continue } w.reader = reader @@ -67,6 +72,7 @@ func (w *webSocketConn) Read(b []byte) (n int, err error) { } func (w *webSocketConn) Write(b []byte) (n int, err error) { + // 每次 Write 输出一个二进制 WebSocket 消息,保持与 Minecraft packet 边界无关的字节流语义。 writer, err := w.NextWriter(websocket.BinaryMessage) if err != nil { return 0, err @@ -96,6 +102,7 @@ func (w *webSocketConn) SetDeadline(t time.Time) error { func newWebSocketHandler() http.Handler { mux := http.NewServeMux() + // 路径来自运行态服务配置,允许管理端把 WebSocket 入口挂到子路径。 mux.HandleFunc(normalizedWebSocketPath(), handleWebSocket) return mux } diff --git a/cmd/gateway/websocket_test.go b/cmd/gateway/websocket_test.go index 1b2c0e2..3d2fcad 100644 --- a/cmd/gateway/websocket_test.go +++ b/cmd/gateway/websocket_test.go @@ -1,3 +1,5 @@ +// cmd/gateway/websocket_test.go 包含用于约束 websocket 行为的测试。 + package main import ( diff --git a/cmd/kcp/main.go b/cmd/kcp/main.go index eb89189..17c7ebe 100644 --- a/cmd/kcp/main.go +++ b/cmd/kcp/main.go @@ -1,3 +1,5 @@ +// cmd/kcp/main.go 提供独立的 KCP 到 TCP 代理工具,用于测试或演示 KCP 传输行为。 + package main import ( diff --git a/cmd/kcp/main_test.go b/cmd/kcp/main_test.go index f0232a0..46e8a92 100644 --- a/cmd/kcp/main_test.go +++ b/cmd/kcp/main_test.go @@ -1,3 +1,5 @@ +// cmd/kcp/main_test.go 包含用于约束 kcp 行为的测试。 + package main import ( diff --git a/cmd/quic/main.go b/cmd/quic/main.go index e814d95..c021ec7 100644 --- a/cmd/quic/main.go +++ b/cmd/quic/main.go @@ -1,3 +1,5 @@ +// cmd/quic/main.go 提供独立的 QUIC 到 TCP 代理工具,用于测试或演示 QUIC 传输行为。 + package main import ( @@ -84,7 +86,7 @@ func handlerConn(conn net.Conn) { log.Info(). Msg("QUIC stream opened") - // read and write stream data + // 读写 QUIC 流数据。 buf := make([]byte, 1024) n, err := conn.Read(buf) diff --git a/cmd/quic/main_test.go b/cmd/quic/main_test.go index 865c9a1..f27a98c 100644 --- a/cmd/quic/main_test.go +++ b/cmd/quic/main_test.go @@ -1,3 +1,5 @@ +// cmd/quic/main_test.go 包含用于约束 quic 行为的测试。 + package main import ( diff --git a/compose.override.yaml b/compose.override.yaml index b110805..17c9331 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -1,3 +1,5 @@ +# compose.override.yaml 把本地 Compose 覆盖项与偏生产形态的基础服务定义分开维护。 + services: mc-gateway: build: diff --git a/compose.yaml b/compose.yaml index 8ef3bf2..081366b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,3 +1,5 @@ +# compose.yaml 定义偏生产形态的网关容器、数据卷、端口和避开代理干扰的健康检查。 + services: mc-gateway: image: ${MC_GATEWAY_IMAGE:-ghcr.io/tursom/mc-gateway:latest} diff --git a/config.example.toml b/config.example.toml index 0456689..d3ad783 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,3 +1,5 @@ +# config.example.toml 说明启动监听器和运行态默认值所需的静态网关配置字段。 + # pid 文件 pid_file = "gateway.pid" diff --git a/examples/plugins/extension-ecosystem/manifest.yaml b/examples/plugins/extension-ecosystem/manifest.yaml index a603682..186afdf 100644 --- a/examples/plugins/extension-ecosystem/manifest.yaml +++ b/examples/plugins/extension-ecosystem/manifest.yaml @@ -1,4 +1,6 @@ -# Human-maintained plugin manifest. Build packages normalize this into manifest.json. +# examples/plugins/extension-ecosystem/manifest.yaml 是示例插件代码,用于演示托管插件接入方式。 + +# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。 schema_version: mc-gateway.plugin/v1 id: extension-ecosystem-example name: Extension Ecosystem Example diff --git a/examples/plugins/mc-auth-proxy/build.sh b/examples/plugins/mc-auth-proxy/build.sh index 19abfa4..10aab45 100755 --- a/examples/plugins/mc-auth-proxy/build.sh +++ b/examples/plugins/mc-auth-proxy/build.sh @@ -1,4 +1,6 @@ #!/usr/bin/env sh +# examples/plugins/mc-auth-proxy/build.sh 是示例插件代码,用于演示托管插件接入方式。 + set -eu repo_root=$(cd ../../.. && pwd) diff --git a/examples/plugins/mc-auth-proxy/main.go b/examples/plugins/mc-auth-proxy/main.go index 8e30bee..fcc9faf 100644 --- a/examples/plugins/mc-auth-proxy/main.go +++ b/examples/plugins/mc-auth-proxy/main.go @@ -1,3 +1,5 @@ +// examples/plugins/mc-auth-proxy/main.go 演示托管插件如何拦截登录流量、发出认证事件并按条件拒绝客户端。 + package main import ( diff --git a/examples/plugins/mc-auth-proxy/main_test.go b/examples/plugins/mc-auth-proxy/main_test.go index cb9bcc2..88677bd 100644 --- a/examples/plugins/mc-auth-proxy/main_test.go +++ b/examples/plugins/mc-auth-proxy/main_test.go @@ -1,3 +1,5 @@ +// examples/plugins/mc-auth-proxy/main_test.go 包含用于约束 mc auth proxy 行为的测试。 + package main import ( diff --git a/examples/plugins/mc-auth-proxy/manifest.yaml b/examples/plugins/mc-auth-proxy/manifest.yaml index 1d17b5e..3e39fee 100644 --- a/examples/plugins/mc-auth-proxy/manifest.yaml +++ b/examples/plugins/mc-auth-proxy/manifest.yaml @@ -1,4 +1,6 @@ -# Human-maintained plugin manifest. Build packages normalize this into manifest.json. +# examples/plugins/mc-auth-proxy/manifest.yaml 是示例插件代码,用于演示托管插件接入方式。 + +# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。 schema_version: mc-gateway.plugin/v1 id: mc-auth-proxy name: Minecraft Auth Proxy diff --git a/examples/plugins/upstream-rewrite/build.sh b/examples/plugins/upstream-rewrite/build.sh index 80a1846..a5a423c 100755 --- a/examples/plugins/upstream-rewrite/build.sh +++ b/examples/plugins/upstream-rewrite/build.sh @@ -1,4 +1,6 @@ #!/usr/bin/env sh +# examples/plugins/upstream-rewrite/build.sh 是示例插件代码,用于演示托管插件接入方式。 + set -eu repo_root=$(cd ../../.. && pwd) diff --git a/examples/plugins/upstream-rewrite/main.go b/examples/plugins/upstream-rewrite/main.go index 8c45d09..ff564a1 100644 --- a/examples/plugins/upstream-rewrite/main.go +++ b/examples/plugins/upstream-rewrite/main.go @@ -1,3 +1,5 @@ +// examples/plugins/upstream-rewrite/main.go 演示托管插件如何在网关拨号上游前改写路由决策。 + package main import ( diff --git a/examples/plugins/upstream-rewrite/manifest.yaml b/examples/plugins/upstream-rewrite/manifest.yaml index bd583d3..6bc7ed0 100644 --- a/examples/plugins/upstream-rewrite/manifest.yaml +++ b/examples/plugins/upstream-rewrite/manifest.yaml @@ -1,4 +1,6 @@ -# Human-maintained plugin manifest. Build packages normalize this into manifest.json. +# examples/plugins/upstream-rewrite/manifest.yaml 是示例插件代码,用于演示托管插件接入方式。 + +# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。 schema_version: mc-gateway.plugin/v1 id: upstream-rewrite name: Upstream Rewrite diff --git a/internal/adminaudit/audit.go b/internal/adminaudit/audit.go index b08f2b7..fc96c63 100644 --- a/internal/adminaudit/audit.go +++ b/internal/adminaudit/audit.go @@ -1,3 +1,5 @@ +// internal/adminaudit/audit.go 持久化 Admin API 变更产生的审计事件,并向管理界面提供查询。 + package adminaudit import ( diff --git a/internal/adminaudit/audit_test.go b/internal/adminaudit/audit_test.go index 3df7375..9c071d1 100644 --- a/internal/adminaudit/audit_test.go +++ b/internal/adminaudit/audit_test.go @@ -1,3 +1,5 @@ +// internal/adminaudit/audit_test.go 包含用于约束 audit 行为的测试。 + package adminaudit import ( diff --git a/internal/adminconfig/config.go b/internal/adminconfig/config.go index 1bd23ef..9884071 100644 --- a/internal/adminconfig/config.go +++ b/internal/adminconfig/config.go @@ -1,3 +1,5 @@ +// internal/adminconfig/config.go 在 SQLite 中保存服务级运行态配置,并用类型化方法包装 JSON 选项。 + package adminconfig import ( diff --git a/internal/adminconfig/config_test.go b/internal/adminconfig/config_test.go index 402e2c8..317bc40 100644 --- a/internal/adminconfig/config_test.go +++ b/internal/adminconfig/config_test.go @@ -1,3 +1,5 @@ +// internal/adminconfig/config_test.go 包含用于约束 config 行为的测试。 + package adminconfig import "testing" diff --git a/internal/admindb/db.go b/internal/admindb/db.go index ecbb41e..64e2d96 100644 --- a/internal/admindb/db.go +++ b/internal/admindb/db.go @@ -1,5 +1,7 @@ //go:build (darwin && (amd64 || arm64)) || (freebsd && (amd64 || arm64)) || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)) || (openbsd && (amd64 || arm64)) || (windows && (386 || amd64 || arm64)) +// internal/admindb/db.go 打开跨平台 SQLite 数据库,并应用管理、路由和插件运行态共用的表结构。 + package admindb import ( @@ -7,9 +9,12 @@ import ( "os" "path/filepath" + // modernc.org/sqlite 是纯 Go SQLite 驱动,便于容器和跨平台构建时避免 CGO 依赖。 _ "modernc.org/sqlite" ) +// Open 创建或打开管理运行态数据库。调用方传入的路径可以包含尚不存在的目录, +// 这里会先创建目录,再打开 SQLite 连接。 func Open(dbPath string) (*sql.DB, error) { if dir := filepath.Dir(dbPath); dir != "." && dir != "" { if err := os.MkdirAll(dir, 0755); err != nil { @@ -21,12 +26,16 @@ func Open(dbPath string) (*sql.DB, error) { if err != nil { return nil, err } + // SQLite 对单写者最友好;限制连接数可以避免 database/sql 在高并发下 + // 打开多条连接后互相争用写锁。 db.SetMaxOpenConns(1) + // WAL 让读请求不会被普通写事务完全阻塞,适合管理端读多写少的状态库。 if _, err := db.Exec(`PRAGMA journal_mode=WAL`); err != nil { db.Close() return nil, err } + // 写锁短暂冲突时等待一小段时间,减少管理端并发操作产生的偶发 busy 错误。 if _, err := db.Exec(`PRAGMA busy_timeout=5000`); err != nil { db.Close() return nil, err @@ -35,6 +44,8 @@ func Open(dbPath string) (*sql.DB, error) { return db, nil } +// Migrate 以幂等方式应用当前 schema。所有 CREATE TABLE 都使用 +// IF NOT EXISTS,后续字段演进通过 ensureColumn 补齐,便于老数据库平滑升级。 func Migrate(db *sql.DB) error { const schema = ` CREATE TABLE IF NOT EXISTS schema_migrations ( @@ -447,6 +458,8 @@ INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (1, strftime if _, err := db.Exec(schema); err != nil { return err } + // CREATE TABLE 不会修改已存在的表,因此历史版本新增字段需要显式补齐。 + // 每个 ensureColumn 都是幂等的,可以安全地在每次启动迁移时执行。 if err := ensureColumn(db, "audit_logs", "metadata_json", "TEXT NOT NULL DEFAULT '{}'"); err != nil { return err } @@ -465,6 +478,8 @@ INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (1, strftime return nil } +// ensureColumn 在表缺字段时执行 ALTER TABLE。table/column/definition 只由 +// 受控迁移代码传入,不接收外部输入,避免把 PRAGMA 语句做成动态用户入口。 func ensureColumn(db *sql.DB, table, column, definition string) error { rows, err := db.Query(`PRAGMA table_info(` + table + `)`) if err != nil { diff --git a/internal/admindb/db_test.go b/internal/admindb/db_test.go index d123fb7..f754fbb 100644 --- a/internal/admindb/db_test.go +++ b/internal/admindb/db_test.go @@ -1,3 +1,5 @@ +// internal/admindb/db_test.go 包含用于约束 db 行为的测试。 + package admindb import ( diff --git a/internal/admindb/db_unsupported.go b/internal/admindb/db_unsupported.go index ff215b6..180b218 100644 --- a/internal/admindb/db_unsupported.go +++ b/internal/admindb/db_unsupported.go @@ -1,5 +1,7 @@ //go:build !((darwin && (amd64 || arm64)) || (freebsd && (amd64 || arm64)) || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)) || (openbsd && (amd64 || arm64)) || (windows && (386 || amd64 || arm64))) +// internal/admindb/db_unsupported.go 在纯 Go SQLite 驱动未被构建标签启用的平台上返回明确错误。 + package admindb import ( diff --git a/internal/adminhttp/api.go b/internal/adminhttp/api.go index e2da155..d4327d9 100644 --- a/internal/adminhttp/api.go +++ b/internal/adminhttp/api.go @@ -1,3 +1,5 @@ +// internal/adminhttp/api.go 构建隔离的进程内 Admin API 服务,供测试和包级消费者使用。 + package adminhttp import ( diff --git a/internal/adminhttp/api_test.go b/internal/adminhttp/api_test.go index 3341a17..296131d 100644 --- a/internal/adminhttp/api_test.go +++ b/internal/adminhttp/api_test.go @@ -1,3 +1,5 @@ +// internal/adminhttp/api_test.go 包含用于约束 api 行为的测试。 + package adminhttp import ( diff --git a/internal/adminhttp/gateway.go b/internal/adminhttp/gateway.go index 4b9d34f..31b08c2 100644 --- a/internal/adminhttp/gateway.go +++ b/internal/adminhttp/gateway.go @@ -1,3 +1,5 @@ +// internal/adminhttp/gateway.go 把仓库驱动的网关依赖适配为 Admin API 处理器集合。 + package adminhttp import ( diff --git a/internal/adminhttp/http.go b/internal/adminhttp/http.go index 292c488..1e1a48a 100644 --- a/internal/adminhttp/http.go +++ b/internal/adminhttp/http.go @@ -1,3 +1,5 @@ +// internal/adminhttp/http.go 提供 Admin API 处理器共用的 JSON、路径片段和来源 IP 辅助方法。 + package adminhttp import ( diff --git a/internal/adminhttp/http_test.go b/internal/adminhttp/http_test.go index c0f67ab..8b927ff 100644 --- a/internal/adminhttp/http_test.go +++ b/internal/adminhttp/http_test.go @@ -1,3 +1,5 @@ +// internal/adminhttp/http_test.go 包含用于约束 http 行为的测试。 + package adminhttp import ( diff --git a/internal/adminhttp/requests.go b/internal/adminhttp/requests.go index d538989..e18df36 100644 --- a/internal/adminhttp/requests.go +++ b/internal/adminhttp/requests.go @@ -1,3 +1,5 @@ +// internal/adminhttp/requests.go 定义 Admin API 测试网关和命令处理器接受的请求载荷。 + package adminhttp type LoginRequest struct { diff --git a/internal/adminhttp/requests_test.go b/internal/adminhttp/requests_test.go index 4f7e76b..9807f97 100644 --- a/internal/adminhttp/requests_test.go +++ b/internal/adminhttp/requests_test.go @@ -1,3 +1,5 @@ +// internal/adminhttp/requests_test.go 包含用于约束 requests 行为的测试。 + package adminhttp import ( diff --git a/internal/adminroute/repository.go b/internal/adminroute/repository.go index 5c2923c..a391b5c 100644 --- a/internal/adminroute/repository.go +++ b/internal/adminroute/repository.go @@ -1,3 +1,5 @@ +// internal/adminroute/repository.go 持久化 Minecraft 主机路由记录,并为在线网关返回有序快照。 + package adminroute import ( @@ -18,7 +20,8 @@ type Record struct { } type Repository struct { - db *sql.DB + db *sql.DB + // now 可在测试中注入固定时间,避免断言依赖真实时钟。 now func() time.Time } @@ -38,6 +41,7 @@ func NewRepositoryWithClock(db *sql.DB, now func() time.Time) Repository { } func (r Repository) EnabledMap(ctx context.Context) (map[string]string, error) { + // 只读取启用路由,结果直接用于连接热路径的内存快照。 rows, err := r.db.QueryContext(ctx, `SELECT host, upstream FROM routes WHERE enabled = 1`) if err != nil { return nil, err @@ -61,6 +65,7 @@ SELECT host, upstream, enabled, note, created_at, updated_at, updated_by FROM routes` var args []any if query = strings.TrimSpace(query); query != "" { + // 管理端搜索同时覆盖 host、upstream 和 note,便于按服务名或备注定位路由。 sqlQuery += ` WHERE host LIKE ? OR upstream LIKE ? OR note LIKE ?` like := "%" + query + "%" args = append(args, like, like, like) @@ -87,6 +92,7 @@ FROM routes` } func (r Repository) Upsert(ctx context.Context, actor, host, upstream string, enabled bool, note string) error { + // 写入前统一校验,避免无效 host/upstream 进入 SQLite 后再被热路径读取。 if err := ValidateHost(host); err != nil { return err } @@ -101,6 +107,7 @@ func (r Repository) Upsert(ctx context.Context, actor, host, upstream string, en } defer tx.Rollback() + // host 是主键;重复保存时只更新可变字段并保留 created_at。 if _, err := tx.ExecContext(ctx, ` INSERT INTO routes(host, upstream, enabled, note, created_at, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?) @@ -117,6 +124,7 @@ ON CONFLICT(host) DO UPDATE SET } func (r Repository) Delete(ctx context.Context, host string) error { + // 删除同样校验 host,防止管理端路径参数中的非法值直接进入 SQL。 if err := ValidateHost(host); err != nil { return err } diff --git a/internal/adminroute/repository_test.go b/internal/adminroute/repository_test.go index cc3f748..ecd1a6b 100644 --- a/internal/adminroute/repository_test.go +++ b/internal/adminroute/repository_test.go @@ -1,3 +1,5 @@ +// internal/adminroute/repository_test.go 包含用于约束 repository 行为的测试。 + package adminroute import ( diff --git a/internal/adminroute/snapshot.go b/internal/adminroute/snapshot.go index 871b3d8..4c4ccd2 100644 --- a/internal/adminroute/snapshot.go +++ b/internal/adminroute/snapshot.go @@ -1,8 +1,11 @@ +// internal/adminroute/snapshot.go 把路由行转换为热路径使用的不可变查找映射。 + package adminroute import "sync/atomic" type Snapshot struct { + // atomic.Value 保存整张路由表,连接热路径读取时无需加锁。 value atomic.Value } @@ -14,6 +17,7 @@ func (s *Snapshot) Store(routes map[string]string) { if routes == nil { routes = map[string]string{} } + // Store 前复制 map,避免调用方发布后继续修改导致并发读写 map。 copied := make(map[string]string, len(routes)) for host, upstream := range routes { copied[host] = upstream @@ -30,6 +34,7 @@ func (s *Snapshot) Clone() map[string]string { if !ok { return nil } + // Clone 返回副本,管理端或测试修改结果不会影响热路径快照。 copied := make(map[string]string, len(routes)) for host, upstream := range routes { copied[host] = upstream @@ -50,6 +55,7 @@ func (s *Snapshot) Lookup(host string) (string, bool) { if ok { return upstream, true } + // default 是显式兜底路由,只有精确 host 未命中时才使用。 upstream, ok = routes["default"] return upstream, ok } diff --git a/internal/adminroute/snapshot_test.go b/internal/adminroute/snapshot_test.go index 6ed90f8..d763dca 100644 --- a/internal/adminroute/snapshot_test.go +++ b/internal/adminroute/snapshot_test.go @@ -1,3 +1,5 @@ +// internal/adminroute/snapshot_test.go 包含用于约束 snapshot 行为的测试。 + package adminroute import "testing" diff --git a/internal/adminroute/validate.go b/internal/adminroute/validate.go index 90bc97e..8579492 100644 --- a/internal/adminroute/validate.go +++ b/internal/adminroute/validate.go @@ -1,3 +1,5 @@ +// internal/adminroute/validate.go 在写入运行态状态前校验路由主机名和上游地址。 + package adminroute import ( @@ -12,6 +14,7 @@ func ValidateHost(host string) error { if host == "" { return errors.New("host is required") } + // host 会出现在 URL path 和 Minecraft 路由键中,因此禁止空白和斜杠。 if strings.ContainsAny(host, " \t\r\n") { return errors.New("host must not contain whitespace") } @@ -27,6 +30,7 @@ func ValidateUpstream(upstream string) error { return errors.New("upstream is required") } + // 传输协议前缀只影响拨号方式,去掉前缀后仍必须是 host:port。 for _, prefix := range []string{"kcp://", "quic://", "haproxy://"} { upstream = strings.TrimPrefix(upstream, prefix) } diff --git a/internal/adminroute/validate_test.go b/internal/adminroute/validate_test.go index 1f55e2c..3cb489f 100644 --- a/internal/adminroute/validate_test.go +++ b/internal/adminroute/validate_test.go @@ -1,3 +1,5 @@ +// internal/adminroute/validate_test.go 包含用于约束 validate 行为的测试。 + package adminroute import "testing" diff --git a/internal/adminservice/repository.go b/internal/adminservice/repository.go index bc80ea0..69c0e39 100644 --- a/internal/adminservice/repository.go +++ b/internal/adminservice/repository.go @@ -1,3 +1,5 @@ +// internal/adminservice/repository.go 把监听服务记录和选项保存到 SQLite,供管理端驱动配置。 + package adminservice import ( @@ -8,7 +10,8 @@ import ( ) type Repository struct { - db *sql.DB + db *sql.DB + // now 可由测试注入,保证更新时间断言稳定。 now func() time.Time } @@ -30,6 +33,7 @@ func NewRepositoryWithClock(db *sql.DB, now func() time.Time) Repository { func (r Repository) EnsureDefaults(ctx context.Context, tcpAdminPort int) error { now := r.now().Unix() for _, service := range DefaultRecords(tcpAdminPort) { + // 默认服务只在缺失时插入,避免覆盖管理员已经保存的运行态配置。 options, err := json.Marshal(service.Options) if err != nil { return err @@ -47,6 +51,7 @@ ON CONFLICT(name) DO NOTHING`, } func (r Repository) List(ctx context.Context) ([]Record, error) { + // 固定排序让管理端列表稳定展示:核心入口在前,可选传输在后。 rows, err := r.db.QueryContext(ctx, ` SELECT name, enabled, port, options_json, restart_required, created_at, updated_at, updated_by FROM services @@ -83,6 +88,7 @@ func (r Repository) Update(ctx context.Context, actor, name string, enabled bool return err } + // 服务配置变更只标记 restart_required;当前进程不会在请求中间重启监听器。 optionsJSON, err := json.Marshal(NormalizeOptions(name, options)) if err != nil { return err diff --git a/internal/adminservice/repository_test.go b/internal/adminservice/repository_test.go index 7a5a7e9..ea849ae 100644 --- a/internal/adminservice/repository_test.go +++ b/internal/adminservice/repository_test.go @@ -1,3 +1,5 @@ +// internal/adminservice/repository_test.go 包含用于约束 repository 行为的测试。 + package adminservice import ( diff --git a/internal/adminservice/service.go b/internal/adminservice/service.go index b6c5494..f02937d 100644 --- a/internal/adminservice/service.go +++ b/internal/adminservice/service.go @@ -1,3 +1,5 @@ +// internal/adminservice/service.go 在原始服务仓库之上应用服务校验、默认值和更新语义。 + package adminservice import ( @@ -36,6 +38,8 @@ type Record struct { Running bool `json:"running"` } +// DefaultRecords 给新数据库写入可管理的监听服务。只有 TCP/Admin 默认启用, +// 其他传输保留配置但不自动开放端口。 func DefaultRecords(tcpAdminPort int) []Record { return []Record{ {Name: NameTCPAdmin, Enabled: true, Port: tcpAdminPort, Options: map[string]any{}}, @@ -52,6 +56,7 @@ func DefaultRecords(tcpAdminPort int) []Record { } } +// DefaultPort 返回服务的默认端口;TCP/Admin 使用启动配置传入的端口。 func DefaultPort(name string, tcpAdminPort int) int { switch name { case NameTCPAdmin: @@ -67,6 +72,7 @@ func DefaultPort(name string, tcpAdminPort int) int { } } +// ValidateUpdate 校验管理端提交的服务更新。TCP/Admin 是控制面入口,不能禁用。 func ValidateUpdate(name string, enabled bool, port int) error { if !IsKnown(name) { return fmt.Errorf("unknown service %q", name) @@ -80,6 +86,7 @@ func ValidateUpdate(name string, enabled bool, port int) error { return nil } +// IsKnown 判断服务名是否属于当前网关支持的内置监听服务。 func IsKnown(name string) bool { switch name { case NameTCPAdmin, NameKCP, NameQUIC, NameWebSocket: @@ -89,6 +96,7 @@ func IsKnown(name string) bool { } } +// DecodeOptions 容错解析 JSON 选项;坏数据不会让整个服务列表不可读。 func DecodeOptions(optionsJSON string) map[string]any { options := map[string]any{} if strings.TrimSpace(optionsJSON) == "" { @@ -100,6 +108,7 @@ func DecodeOptions(optionsJSON string) map[string]any { return options } +// NormalizeOptions 为不同服务补齐选项默认值,并修正 WebSocket path 这种可恢复输入。 func NormalizeOptions(name string, options map[string]any) map[string]any { if options == nil { options = map[string]any{} @@ -132,6 +141,7 @@ func NormalizeOptions(name string, options map[string]any) map[string]any { return normalized } +// IntOption 从 JSON 解码后的 map 中读取整数,兼容 number 和字符串形式。 func IntOption(options map[string]any, key string, fallback int) int { value, ok := options[key] if !ok { @@ -158,6 +168,7 @@ func IntOption(options map[string]any, key string, fallback int) int { return fallback } +// StringOption 从 JSON 选项中读取非空字符串。 func StringOption(options map[string]any, key, fallback string) string { value, ok := options[key] if !ok { @@ -169,6 +180,7 @@ func StringOption(options map[string]any, key, fallback string) string { return fallback } +// StringSliceOption 从 JSON 选项中读取字符串数组,兼容 []any 的解码结果。 func StringSliceOption(options map[string]any, key string) []string { value, ok := options[key] if !ok { diff --git a/internal/adminservice/service_test.go b/internal/adminservice/service_test.go index ed0c935..7a1121f 100644 --- a/internal/adminservice/service_test.go +++ b/internal/adminservice/service_test.go @@ -1,3 +1,5 @@ +// internal/adminservice/service_test.go 包含用于约束 service 行为的测试。 + package adminservice import ( diff --git a/internal/adminsession/session.go b/internal/adminsession/session.go index 72790f1..4824831 100644 --- a/internal/adminsession/session.go +++ b/internal/adminsession/session.go @@ -1,3 +1,5 @@ +// internal/adminsession/session.go 管理短生命周期的内存管理会话,并在用户状态变化时让相关会话失效。 + package adminsession import ( @@ -8,6 +10,7 @@ import ( ) type Session struct { + // Token 只保存在内存和客户端,不写入 SQLite;进程重启会让所有会话失效。 Token string Username string Role string @@ -17,7 +20,8 @@ type Session struct { type Manager struct { mu sync.Mutex sessions map[string]Session - now func() time.Time + // now 可在测试中注入,便于验证过期清理逻辑。 + now func() time.Time } func NewManager() *Manager { @@ -36,6 +40,7 @@ func NewManagerWithClock(now func() time.Time) *Manager { } func (m *Manager) Create(username, role string, ttl time.Duration) (Session, error) { + // 32 字节随机数再做 URL 安全 base64,足够作为 bearer token 使用。 tokenBytes := make([]byte, 32) if _, err := rand.Read(tokenBytes); err != nil { return Session{}, err @@ -62,6 +67,7 @@ func (m *Manager) Get(token string) (Session, bool) { return Session{}, false } if m.now().After(session.ExpiresAt) { + // 读取时顺手清理过期会话,避免后台清理 goroutine。 delete(m.sessions, token) return Session{}, false } @@ -80,6 +86,7 @@ func (m *Manager) RemoveUser(username string) { for token, session := range m.sessions { if session.Username == username { + // 用户密码、角色或禁用状态变化后,调用方用该方法使旧 token 立即失效。 delete(m.sessions, token) } } diff --git a/internal/adminsession/session_test.go b/internal/adminsession/session_test.go index faf26dc..022346d 100644 --- a/internal/adminsession/session_test.go +++ b/internal/adminsession/session_test.go @@ -1,3 +1,5 @@ +// internal/adminsession/session_test.go 包含用于约束 session 行为的测试。 + package adminsession import ( diff --git a/internal/adminuser/repository.go b/internal/adminuser/repository.go index ed6a6a2..f12c409 100644 --- a/internal/adminuser/repository.go +++ b/internal/adminuser/repository.go @@ -1,3 +1,5 @@ +// internal/adminuser/repository.go 在 SQLite 中保存管理用户和密码哈希,并支持角色与禁用状态筛选。 + package adminuser import ( diff --git a/internal/adminuser/repository_test.go b/internal/adminuser/repository_test.go index accc2b5..c843053 100644 --- a/internal/adminuser/repository_test.go +++ b/internal/adminuser/repository_test.go @@ -1,3 +1,5 @@ +// internal/adminuser/repository_test.go 包含用于约束 repository 行为的测试。 + package adminuser import ( diff --git a/internal/adminuser/user.go b/internal/adminuser/user.go index 262a68c..25ab66f 100644 --- a/internal/adminuser/user.go +++ b/internal/adminuser/user.go @@ -1,3 +1,5 @@ +// internal/adminuser/user.go 定义管理用户角色、校验规则、密码哈希和对外用户视图。 + package adminuser import ( @@ -7,6 +9,8 @@ import ( ) const ( + // 角色按权限从高到低排列:admin 管理所有资源,member 管理路由和查看运行态, + // guest 只保留基础只读能力。 RoleAdmin = "admin" RoleMember = "member" RoleGuest = "guest" @@ -24,6 +28,7 @@ func ValidateUsername(username string) error { if username == "" { return errors.New("username is required") } + // 用户名会出现在 URL path 和审计记录中,因此禁止空白和斜杠。 if strings.ContainsAny(username, " \t\r\n/") { return errors.New("username must not contain whitespace or /") } @@ -31,6 +36,7 @@ func ValidateUsername(username string) error { } func ValidateRole(role string) error { + // 所有角色必须在这里登记,避免数据库里出现管理端无法解释的角色。 switch role { case RoleAdmin, RoleMember, RoleGuest: return nil @@ -40,6 +46,7 @@ func ValidateRole(role string) error { } func ValidatePassword(password string) error { + // 当前只做非空校验;更复杂的密码策略应放在产品策略确定后再补。 if strings.TrimSpace(password) == "" { return errors.New("password is required") } @@ -47,6 +54,7 @@ func ValidatePassword(password string) error { } func RoleRank(role string) int { + // rank 让权限判断保持单调:高角色天然包含低角色能力。 switch role { case RoleAdmin: return 3 @@ -63,6 +71,7 @@ func HasRole(actual, required string) bool { return RoleRank(actual) >= RoleRank(required) } +// Permissions 返回前端可直接消费的权限位;后端仍以角色校验为准。 func Permissions(role string) map[string]bool { return map[string]bool{ "read_routes": HasRole(role, RoleGuest), diff --git a/internal/adminuser/user_test.go b/internal/adminuser/user_test.go index aa23245..dfe8ebc 100644 --- a/internal/adminuser/user_test.go +++ b/internal/adminuser/user_test.go @@ -1,3 +1,5 @@ +// internal/adminuser/user_test.go 包含用于约束 user 行为的测试。 + package adminuser import ( diff --git a/internal/gatewayconfig/config.go b/internal/gatewayconfig/config.go index 27770ca..431d4d5 100644 --- a/internal/gatewayconfig/config.go +++ b/internal/gatewayconfig/config.go @@ -1,3 +1,5 @@ +// internal/gatewayconfig/config.go 定义运行态管理数据库可用前使用的静态 TOML 配置。 + package gatewayconfig type Config struct { diff --git a/internal/gatewayconfig/plugin.go b/internal/gatewayconfig/plugin.go index 43dce80..46d0872 100644 --- a/internal/gatewayconfig/plugin.go +++ b/internal/gatewayconfig/plugin.go @@ -1,3 +1,5 @@ +// internal/gatewayconfig/plugin.go 定义进程启动时可加载的静态插件配置项。 + package gatewayconfig import "github.com/mitchellh/mapstructure" diff --git a/internal/gatewayconfig/plugin_test.go b/internal/gatewayconfig/plugin_test.go index 8c4df7a..f377e3f 100644 --- a/internal/gatewayconfig/plugin_test.go +++ b/internal/gatewayconfig/plugin_test.go @@ -1,3 +1,5 @@ +// internal/gatewayconfig/plugin_test.go 包含用于约束 plugin 行为的测试。 + package gatewayconfig import "testing" diff --git a/internal/gatewaymetrics/metrics.go b/internal/gatewaymetrics/metrics.go index 3a92d8f..0a593ea 100644 --- a/internal/gatewaymetrics/metrics.go +++ b/internal/gatewaymetrics/metrics.go @@ -1,3 +1,5 @@ +// internal/gatewaymetrics/metrics.go 用原子计数器记录连接、路由和上游错误等管理状态指标。 + package gatewaymetrics import ( @@ -6,6 +8,7 @@ import ( ) type Counters struct { + // 连接级计数使用原子值,避免转发热路径在每次连接开始/结束时争用锁。 totalConnections atomic.Uint64 activeConnections atomic.Int64 tcpConnections atomic.Uint64 @@ -13,6 +16,7 @@ type Counters struct { routeMisses atomic.Uint64 upstreamDialErrs atomic.Uint64 + // routeHits 按 host 聚合,需要 map,因此用一把小锁保护。 routeHitsMu sync.Mutex routeHits map[string]uint64 } @@ -62,6 +66,7 @@ func (m *Counters) Snapshot() map[string]any { } m.routeHitsMu.Unlock() + // 返回普通 map,方便 Admin API 直接 JSON 编码。 return map[string]any{ "total_connections": m.totalConnections.Load(), "active_connections": m.activeConnections.Load(), diff --git a/internal/gatewaymetrics/metrics_test.go b/internal/gatewaymetrics/metrics_test.go index 77800ca..7473f35 100644 --- a/internal/gatewaymetrics/metrics_test.go +++ b/internal/gatewaymetrics/metrics_test.go @@ -1,3 +1,5 @@ +// internal/gatewaymetrics/metrics_test.go 包含用于约束 metrics 行为的测试。 + package gatewaymetrics import "testing" diff --git a/internal/pluginmanager/artifact.go b/internal/pluginmanager/artifact.go index 0d33b25..8af81a9 100644 --- a/internal/pluginmanager/artifact.go +++ b/internal/pluginmanager/artifact.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/artifact.go 校验、保存、哈希并描述上传到网关的插件制品包。 + package pluginmanager import ( diff --git a/internal/pluginmanager/artifact_test.go b/internal/pluginmanager/artifact_test.go index 9f98b1f..110d828 100644 --- a/internal/pluginmanager/artifact_test.go +++ b/internal/pluginmanager/artifact_test.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/artifact_test.go 包含用于约束 artifact 行为的测试。 + package pluginmanager import ( diff --git a/internal/pluginmanager/builder.go b/internal/pluginmanager/builder.go index 940b65b..92f0407 100644 --- a/internal/pluginmanager/builder.go +++ b/internal/pluginmanager/builder.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/builder.go 把源码插件包构建为网关可加载制品,并记录构建元数据。 + package pluginmanager import ( diff --git a/internal/pluginmanager/extensions.go b/internal/pluginmanager/extensions.go index 17d286d..8cf4079 100644 --- a/internal/pluginmanager/extensions.go +++ b/internal/pluginmanager/extensions.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/extensions.go 索引扩展点注册信息,并分发连接、路由、状态和提供方钩子。 + package pluginmanager import ( diff --git a/internal/pluginmanager/future.go b/internal/pluginmanager/future.go index b08b698..0a61008 100644 --- a/internal/pluginmanager/future.go +++ b/internal/pluginmanager/future.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/future.go 建模未来运行时和分发能力,但不把它们接入当前热路径。 + package pluginmanager import ( diff --git a/internal/pluginmanager/future_test.go b/internal/pluginmanager/future_test.go index c11b6e7..3cf8d7d 100644 --- a/internal/pluginmanager/future_test.go +++ b/internal/pluginmanager/future_test.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/future_test.go 包含用于约束 future 行为的测试。 + package pluginmanager import ( diff --git a/internal/pluginmanager/gc.go b/internal/pluginmanager/gc.go index 9894af2..664d43b 100644 --- a/internal/pluginmanager/gc.go +++ b/internal/pluginmanager/gc.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/gc.go 选择并删除不再被期望状态或活动运行态引用的插件制品。 + package pluginmanager import ( diff --git a/internal/pluginmanager/governance.go b/internal/pluginmanager/governance.go index d4ebc6f..a025748 100644 --- a/internal/pluginmanager/governance.go +++ b/internal/pluginmanager/governance.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/governance.go 在涉及发布风险的操作前评估插件评审、公告、供应链和策略门禁。 + package pluginmanager import ( diff --git a/internal/pluginmanager/governance_test.go b/internal/pluginmanager/governance_test.go index 092ba9d..bf69a8f 100644 --- a/internal/pluginmanager/governance_test.go +++ b/internal/pluginmanager/governance_test.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/governance_test.go 包含用于约束 governance 行为的测试。 + package pluginmanager import ( diff --git a/internal/pluginmanager/manager.go b/internal/pluginmanager/manager.go index 511aa1f..ed67a48 100644 --- a/internal/pluginmanager/manager.go +++ b/internal/pluginmanager/manager.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/manager.go 协调插件记录、制品加载、钩子分发快照和生命周期迁移。 + package pluginmanager import ( @@ -25,10 +27,13 @@ type RuntimeAdapter interface { Load(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord, gateway *Gateway) (api.Plugin, error) } +// ConfigDryRunAdapter 是运行时适配器的可选能力。支持该能力时,配置保存前 +// 可以真正实例化插件并调用 ReloadConfig,从而提前发现 schema 之外的错误。 type ConfigDryRunAdapter interface { DryRunConfig(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord) error } +// GoPluginAdapter 加载 Go plugin 或内置插件,是当前 in-process 插件运行模式的默认实现。 type GoPluginAdapter struct{} func (a GoPluginAdapter) Load(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord, gateway *Gateway) (api.Plugin, error) { @@ -82,6 +87,8 @@ func (a GoPluginAdapter) instantiate(ctx context.Context, artifact ArtifactRecor if artifact.RuntimeType == RuntimeBuiltin || artifact.PluginID == "official.rule-policy" { return instantiateBuiltinPlugin(artifact, pluginRecord, gateway, init) } + // Go plugin 只能加载与当前进程 Go 版本、架构和 ABI 匹配的 .so 文件。 + // 这些兼容性检查在制品校验和构建阶段完成,这里只负责打开和实例化。 opened, err := stdplugin.Open(artifact.FilePath) if err != nil { return nil, err @@ -91,6 +98,8 @@ func (a GoPluginAdapter) instantiate(ctx context.Context, artifact ArtifactRecor if err := json.Unmarshal([]byte(artifact.MetadataJSON), &manifest); err == nil && manifest.Runtime.EntrySymbol != "" { symbolName = manifest.Runtime.EntrySymbol } + // 默认入口符号是 Plugin,也允许 manifest 指定自定义入口,便于未来兼容 + // 不同构建工具生成的插件包。 symbol, err := opened.Lookup(symbolName) if err != nil { return nil, err @@ -100,6 +109,8 @@ func (a GoPluginAdapter) instantiate(ctx context.Context, artifact ArtifactRecor return nil, fmt.Errorf("plugin symbol %q has invalid signature", symbolName) } instance := factory() + // 插件配置对象由插件自己声明;宿主只负责把持久化 JSON 解入该对象, + // 再交给 ReloadConfig 做插件内部校验。 cfg := instance.NewConfigObj() if cfg != nil && pluginRecord.ConfigJSON != "" && canUnmarshalInto(cfg) { if err := json.Unmarshal([]byte(pluginRecord.ConfigJSON), cfg); err != nil { @@ -117,6 +128,8 @@ func (a GoPluginAdapter) instantiate(ctx context.Context, artifact ArtifactRecor return instance, nil } +// instantiateBuiltinPlugin 让官方内置插件走同一套 Plugin 接口和配置流程, +// 避免在调用路径上区分内置插件与外部上传插件。 func instantiateBuiltinPlugin(artifact ArtifactRecord, pluginRecord PluginRecord, gateway *Gateway, init bool) (api.Plugin, error) { var instance api.Plugin switch artifact.PluginID { @@ -166,17 +179,21 @@ type Manager struct { routeCacheMu sync.Mutex routeCache map[string]routeCacheEntry + // proxyConns 只跟踪由插件托管代理的连接,用于停用插件时的 drain 和强制关闭。 proxyMu sync.Mutex proxySeq uint64 proxyConns map[uint64]*proxyConnection drainingIDs map[string]bool operations *Operations + // serviceMode/hosts 预留给插件运行时从进程内迁移到独立宿主的服务模式。 serviceMode string hostMu sync.Mutex hosts map[string]*pluginHostProcess } +// loadedPlugin 是内存中的插件实例和它注册的扩展快照。数据库记录说明期望状态, +// loadedPlugin 说明当前进程实际已经加载了什么。 type loadedPlugin struct { record PluginRecord artifact ArtifactRecord @@ -186,6 +203,7 @@ type loadedPlugin struct { extensions pluginExtensions } +// pluginExtensions 按扩展类型拆分注册结果,便于发布不可变快照给不同热路径使用。 type pluginExtensions struct { routes []*routeHandler statuses []*statusHandler @@ -194,6 +212,7 @@ type pluginExtensions struct { providers []ProviderSummary } +// upstreamHandler 包装一个上游连接钩子,并保存调用、错误、超时和代理流量指标。 type upstreamHandler struct { pluginID string artifactID string @@ -256,6 +275,8 @@ type Options struct { PolicyProfile string } +// New 构造插件管理器并初始化内存快照。官方内置插件和插件服务模式会在这里 +// 尽力注册/应用,失败不会阻止网关启动,后续 Admin API 仍可修复状态。 func New(options Options) *Manager { adapter := options.Adapter if adapter == nil { @@ -277,6 +298,7 @@ func New(options Options) *Manager { } manager.operations = NewOperations(manager.repo, options.ArtifactRoot) if manager.builders == nil { + // 默认同时提供本地进程构建和容器构建能力;部署方可在 Options 中收窄。 manager.builders = map[string]SourceBuilder{ BuilderTypeLocalProcess: LocalProcessBuilder{StoreRoot: options.ArtifactRoot}, BuilderTypeContainer: ContainerBuilder{}, @@ -289,6 +311,8 @@ func New(options Options) *Manager { return manager } +// EnsureOfficialPlugins 将内置官方插件登记为普通制品记录。这样 UI、治理、 +// 配置和启停流程都可以复用同一套插件管理模型。 func (m *Manager) EnsureOfficialPlugins(ctx context.Context, actor string) error { now := time.Now().Unix() manifest := Manifest{ @@ -341,6 +365,8 @@ func (m *Manager) EnsureOfficialPlugins(ctx context.Context, actor string) error return nil } +// UploadArtifact 校验并保存二进制插件制品;源码包会转交给源码保存流程, +// 因为源码上传后还需要自动排队构建。 func (m *Manager) UploadArtifact(ctx context.Context, upload ArtifactUpload) (ArtifactRecord, error) { artifact, err := m.store.ValidateAndStore(upload) if err != nil { @@ -362,6 +388,8 @@ func (m *Manager) UploadArtifact(ctx context.Context, upload ArtifactUpload) (Ar return artifact, nil } +// UploadSource 保存源码插件包并创建构建记录。真正构建可以立即运行,也可以 +// 由管理端稍后触发 RunBuild。 func (m *Manager) UploadSource(ctx context.Context, upload ArtifactUpload) (ArtifactRecord, error) { artifact, err := m.store.ValidateAndStoreSource(upload) if err != nil { @@ -406,6 +434,7 @@ func (m *Manager) CreateBuild(ctx context.Context, actor string, req BuildReques return BuildRecord{}, fmt.Errorf("artifact %s is %q, want source", source.ID, source.ArtifactType) } req = defaultBuildRequest(req, source) + // Go plugin 与宿主进程存在 ABI 约束,目前只允许构建当前网关所在平台的目标。 if req.GOOS != runtime.GOOS || req.GOARCH != runtime.GOARCH { return BuildRecord{}, fmt.Errorf("build target %s/%s does not match gateway %s/%s", req.GOOS, req.GOARCH, runtime.GOOS, runtime.GOARCH) } @@ -449,6 +478,8 @@ func (m *Manager) CreateBuild(ctx context.Context, actor string, req BuildReques return build, nil } +// RunBuild 执行已排队的源码构建,并把产出的二进制制品重新写入制品仓库。 +// 构建记录始终会落库,失败时也会保存日志摘要,便于管理端诊断。 func (m *Manager) RunBuild(ctx context.Context, actor string, buildID int64) (BuildRecord, error) { build, err := m.repo.Build(ctx, buildID) if err != nil { @@ -559,11 +590,14 @@ func (m *Manager) RunBuild(ctx context.Context, actor string, buildID int64) (Bu return m.repo.Build(ctx, build.ID) } +// SetDesired 只修改插件的期望状态,不直接改变当前进程已加载的插件。 +// 调用方需要再执行 Enable/Disable/Reconcile 才会推动运行态收敛。 func (m *Manager) SetDesired(ctx context.Context, actor, pluginID, artifactID, desiredState, configJSON string, priority int) (PluginRecord, error) { if desiredState == "" { desiredState = DesiredDisabled } if desiredState != DesiredDeleted { + // 任何非删除状态都先做配置 dry-run,避免把无法加载的配置写成新的期望状态。 if _, err := m.DryRunConfig(ctx, pluginID, artifactID, configJSON); err != nil { _ = m.repo.RecordOperation(ctx, pluginID, artifactID, "config_dry_run", "failed", actor, err.Error(), map[string]any{ "active_changed": false, @@ -584,6 +618,8 @@ func (m *Manager) SetDesired(ctx context.Context, actor, pluginID, artifactID, d return pluginRecord, nil } +// DryRunConfig 执行保存配置前的完整预检:JSON 合法性、制品归属、治理门禁、 +// schema、密钥引用以及运行时 ReloadConfig 都会在这里验证。 func (m *Manager) DryRunConfig(ctx context.Context, pluginID, artifactID, configJSON string) (ConfigDryRunResult, error) { result := ConfigDryRunResult{ OK: false, @@ -631,6 +667,7 @@ func (m *Manager) DryRunConfig(ctx context.Context, pluginID, artifactID, config return result, err } if dryRunner, ok := m.adapter.(ConfigDryRunAdapter); ok { + // 运行时 dry-run 会实例化插件但不调用 Init,避免注册钩子或启动后台任务。 if err := dryRunner.DryRunConfig(ctx, artifact, pluginRecord); err != nil { result.Error = err.Error() return result, err @@ -758,6 +795,8 @@ func (m *Manager) Load(ctx context.Context, actor, pluginID string) (PluginRecor m.mu.Lock() defer m.mu.Unlock() + // Load 只把插件实例化到内存并登记为 loaded,不发布到热路径。 + // 管理端可用它验证制品和配置,而不立即影响在线连接。 pluginRecord, err := m.repo.Plugin(ctx, pluginID) if err != nil { return PluginRecord{}, err @@ -771,6 +810,8 @@ func (m *Manager) Load(ctx context.Context, actor, pluginID string) (PluginRecor return m.repo.Plugin(ctx, pluginID) } +// Enable 将期望状态推进为启用,并把插件处理器发布到连接热路径。 +// 发布前会先通过治理门禁,避免高风险制品绕过评审直接生效。 func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRecord, error) { pluginRecord, err := m.repo.Plugin(ctx, pluginID) if err != nil { @@ -797,6 +838,8 @@ func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRec m.mu.Lock() defer m.mu.Unlock() + // 真正加载与发布都在同一把锁内完成,保证 snapshot、extensions 和 loaded + // 三类内存状态不会被并发读到半更新结果。 loaded, err := m.loadLocked(ctx, pluginRecord) if err != nil { _ = m.repo.RecordOperation(ctx, pluginID, pluginRecord.DesiredArtifactID, "enable", "failed", actor, err.Error(), nil) @@ -817,6 +860,8 @@ func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRec if err := m.markEnabled(ctx, loaded); err != nil { return PluginRecord{}, err } + // 数据库运行态先写成功,再发布内存快照;这样 UI 看到 enabled 时, + // 连接热路径也已经具备对应处理器。 m.markHostStarted(pluginID, loaded.artifact.ID) m.clearDrainingLocked(pluginID) m.publish(next) @@ -830,6 +875,8 @@ func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRec return m.repo.Plugin(ctx, pluginID) } +// Disable 从热路径移除插件并进入 drain。Go plugin 不能从进程卸载, +// 因此这里停止任务、移除分发入口,并等待已有 protocol-proxy 连接结束。 func (m *Manager) Disable(ctx context.Context, actor, pluginID string) (PluginRecord, error) { m.mu.Lock() defer m.mu.Unlock() @@ -844,6 +891,8 @@ func (m *Manager) Disable(ctx context.Context, actor, pluginID string) (PluginRe } m.removeFromDispatchLocked(pluginID) m.removeExtensionsLocked(pluginID) + // 先标记 draining,再 Destroy 插件实例,确保后续管理操作能看到仍在 + // 转发中的插件代理连接。 m.markDrainingLocked(pluginID) m.markHostDraining(pluginID) m.operations.StopPlugin(pluginID) @@ -866,6 +915,8 @@ func (m *Manager) Disable(ctx context.Context, actor, pluginID string) (PluginRe return m.repo.Plugin(ctx, pluginID) } +// Delete 与 Disable 类似,但把期望状态写为 deleted。实际制品清理仍由 GC +// 根据引用关系判断,避免删除仍被快照或历史操作引用的文件。 func (m *Manager) Delete(ctx context.Context, actor, pluginID string) error { m.mu.Lock() defer m.mu.Unlock() @@ -892,6 +943,8 @@ func (m *Manager) Delete(ctx context.Context, actor, pluginID string) error { return nil } +// Reconcile 根据数据库中的期望启用列表重建内存分发快照,主要用于进程启动 +// 或运行态状态漂移后的自愈。 func (m *Manager) Reconcile(ctx context.Context) error { m.mu.Lock() defer m.mu.Unlock() @@ -903,6 +956,7 @@ func (m *Manager) Reconcile(ctx context.Context) error { nextByPlugin := make(map[string][]*upstreamHandler) extensionsByPlugin := make(map[string]pluginExtensions) for _, pluginRecord := range desired { + // 单个插件失败不阻断其他插件收敛;失败会记录到 runtime_state 和操作日志。 decision, err := m.EvaluateGovernance(ctx, pluginRecord.ID, pluginRecord.DesiredArtifactID, GovernanceActionEnable, m.currentPolicyProfile(), pluginRecord.ConfigJSON) if err == nil && !decision.OK { err = governanceBlockedError(decision) @@ -930,11 +984,14 @@ func (m *Manager) Reconcile(ctx context.Context) error { m.markHostStarted(pluginRecord.ID, loaded.artifact.ID) m.clearDrainingLocked(pluginRecord.ID) } + // 所有插件都处理完后一次性发布快照,避免热路径在收敛过程中看到部分插件。 m.publish(flattenHandlers(nextByPlugin)) m.publishExtensionsLocked(extensionsByPlugin) return nil } +// ConnectUpstream 依次调用当前快照中的上游连接处理器。处理器返回 ErrPass +// 表示让下一个插件继续尝试,返回连接则由网关使用插件提供的上游。 func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRequest) (UpstreamResult, error) { value := m.snapshot.Load() if value == nil { @@ -949,6 +1006,7 @@ func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRe } req.InitialData = append([]byte(nil), req.InitialData...) for _, handler := range handlers { + // accept 阶段应尽量轻量,用于快速过滤不关心的主机或上游。 accepted, err := handler.accepts(req) if err != nil { return UpstreamResult{Handled: true}, err @@ -983,6 +1041,8 @@ func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRe return UpstreamResult{Handled: true}, err } if conn != nil { + // protocol-proxy 模式由插件代理完整协议流;普通 dialer 模式只提供 + // 已连接的上游 net.Conn,后续转发仍由网关主流程完成。 if handler.mode == UpstreamModeDialer { _ = m.repo.SaveTrace(context.Background(), TraceSummary{ PluginID: handler.pluginID, @@ -1010,6 +1070,8 @@ func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRe return UpstreamResult{}, nil } +// startProtocolProxy 把客户端连接交给插件提供的协议代理端点。网关仍跟踪连接, +// 以便停用插件时可以 drain 或强制关闭。 func (m *Manager) startProtocolProxy(ctx context.Context, handler *upstreamHandler, result UpstreamResult, req api.UpstreamConnectRequest) (UpstreamResult, error) { endpoint := result.Conn initial := append([]byte(nil), req.InitialData...) @@ -1454,6 +1516,7 @@ func (m *Manager) findHandler(pluginID, handlerID string) *upstreamHandler { } func (m *Manager) finishProxyConnection(id uint64, stats ProxyConnectionStats) { + // 代理连接结束时汇总字节数和耗时,供 Admin UI 展示插件代理健康情况。 m.proxyMu.Lock() proxyConn := m.proxyConns[id] delete(m.proxyConns, id) @@ -1477,10 +1540,13 @@ func (m *Manager) finishProxyConnection(id uint64, stats ProxyConnectionStats) { } } +// loadLocked 加载或复用插件实例。调用方必须持有 m.mu,确保 loaded 缓存和 +// 运行态标记不会与 Enable/Disable/Reconcile 并发冲突。 func (m *Manager) loadLocked(ctx context.Context, pluginRecord PluginRecord) (*loadedPlugin, error) { if loaded := m.loaded[pluginRecord.ID]; loaded != nil && loaded.artifact.ID == pluginRecord.DesiredArtifactID && loaded.record.DesiredGeneration == pluginRecord.DesiredGeneration { + // 同一制品、同一期望代数已经加载时直接复用,避免重复 Init 和重复注册任务。 return loaded, nil } artifact, err := m.repo.Artifact(ctx, pluginRecord.DesiredArtifactID) @@ -1503,6 +1569,7 @@ func (m *Manager) loadLocked(ctx context.Context, pluginRecord PluginRecord) (*l } handlers := buildHandlers(pluginRecord, artifact, gateway) extensions := buildExtensions(pluginRecord, artifact, gateway) + // 钩子和扩展是从 gateway 注册记录中构建出来的;插件 Init 期间完成注册。 loaded := &loadedPlugin{ record: pluginRecord, artifact: artifact, @@ -1523,6 +1590,8 @@ func (m *Manager) loadLocked(ctx context.Context, pluginRecord PluginRecord) (*l return loaded, nil } +// validateArtifactGate 确认制品能被当前网关进程加载。Go plugin 对 Go 版本和 +// 目标平台敏感,沙箱/wasm 运行时则受插件服务模式控制。 func (m *Manager) validateArtifactGate(artifact ArtifactRecord) error { if artifact.Status == ArtifactStatusDeleted || artifact.Status == ArtifactStatusRejected { return fmt.Errorf("artifact status %q is not loadable", artifact.Status) diff --git a/internal/pluginmanager/manager_test.go b/internal/pluginmanager/manager_test.go index 910b2a8..4a7ab88 100644 --- a/internal/pluginmanager/manager_test.go +++ b/internal/pluginmanager/manager_test.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/manager_test.go 包含用于约束 manager 行为的测试。 + package pluginmanager import ( diff --git a/internal/pluginmanager/operations.go b/internal/pluginmanager/operations.go index df93aaf..30eff7b 100644 --- a/internal/pluginmanager/operations.go +++ b/internal/pluginmanager/operations.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/operations.go 实现插件运行时运维能力,包括事件、指标、文件/数据存储、外部客户端、任务和诊断。 + package pluginmanager import ( @@ -26,11 +28,13 @@ import ( ) const ( + // 简单熔断状态使用字符串保存,便于直接落库和输出到诊断包。 circuitClosed = "closed" circuitOpen = "open" ) var ( + // 插件上报的指标名和存储 key 需要收敛到可观测系统容易消费的字符集。 metricNamePattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_.:-]{0,127}$`) storeKeyPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.:/-]{0,255}$`) ) @@ -48,9 +52,11 @@ type Operations struct { repo Repository root string + // plugins 保存每个插件的运行时运维上下文,按 manifest 重新配置但保留统计摘要。 mu sync.RWMutex plugins map[string]*PluginOperations + // eventQueue 负责把插件事件异步落库,避免连接热路径被 SQLite 写入阻塞。 eventQueue chan queuedEvent queued atomic.Uint64 dropped atomic.Uint64 @@ -61,6 +67,7 @@ type Operations struct { subscriberDropped atomic.Uint64 subscriberDeadLetter atomic.Uint64 + // subscribers 是当前启用插件注册的事件订阅者快照,由 Manager 发布。 subscriberMu sync.RWMutex subscribers []*subscriberHandler } @@ -81,6 +88,7 @@ type PluginOperations struct { artifactID string manifest Manifest + // 下列 schema 来自 manifest,用于在插件运行时限制事件、指标、任务和外部依赖。 mu sync.Mutex eventSchemas map[string]map[string]bool metricSchemas map[string]MetricSpec @@ -100,6 +108,7 @@ type PluginOperations struct { type externalRuntime struct { spec ExternalSpec + // 外部依赖统计用于诊断包和熔断策略,全部用原子值减少请求路径锁竞争。 requests atomic.Uint64 errors atomic.Uint64 inflight atomic.Int64 @@ -120,6 +129,7 @@ type taskRuntime struct { task api.BackgroundTask confirmToken string + // 每个后台任务独立持有调度状态和 cancel 函数,插件停用时可逐个停止。 mu sync.Mutex cancel context.CancelFunc running bool @@ -132,6 +142,7 @@ type taskRuntime struct { consecutiveFailures uint64 } +// NewOperations 创建插件运维协调器,并启动事件落库和订阅投递两个后台消费者。 func NewOperations(repo Repository, root string) *Operations { ops := &Operations{ repo: repo, @@ -145,6 +156,8 @@ func NewOperations(repo Repository, root string) *Operations { return ops } +// SetSubscribers 用新的订阅者快照替换旧快照。Manager 在插件启停后调用它, +// 事件消费者只读取快照副本,不直接依赖 Manager 锁。 func (o *Operations) SetSubscribers(subscribers []*subscriberHandler) { o.subscriberMu.Lock() defer o.subscriberMu.Unlock() @@ -164,6 +177,7 @@ func (o *Operations) ForPlugin(pluginID, artifactID string, manifest Manifest) * defer o.mu.Unlock() po := o.plugins[pluginID] if po == nil { + // 首次看到插件时创建运维上下文;后续版本切换会复用它的近期统计。 po = &PluginOperations{ parent: o, pluginID: pluginID, @@ -200,6 +214,7 @@ func (o *Operations) StartTasks(pluginID string) { func (o *Operations) consumeEvents() { for event := range o.eventQueue { o.queued.Add(1) + // 落库使用短超时,避免后台消费者在数据库异常时堆积过久。 ctx, cancel := context.WithTimeout(context.Background(), time.Second) err := o.repo.SaveEvent(ctx, EventSummary{ PluginID: event.pluginID, @@ -217,6 +232,7 @@ func (o *Operations) queueEvent(event queuedEvent) { select { case o.eventQueue <- event: default: + // 队列满时仍写一条 dropped 记录,保留“发生过丢弃”的审计线索。 o.dropped.Add(1) ctx, cancel := context.WithTimeout(context.Background(), time.Second) _ = o.repo.SaveEvent(ctx, EventSummary{ @@ -230,6 +246,7 @@ func (o *Operations) queueEvent(event queuedEvent) { } func (o *Operations) queueSubscriberEvent(event queuedEvent) { + // 已标记 dropped 的事件只进入持久化路径,不再交给订阅者重复处理。 o.subscriberMu.RLock() hasSubscribers := len(o.subscribers) > 0 o.subscriberMu.RUnlock() @@ -275,6 +292,7 @@ func (o *Operations) deliverSubscriberEvent(subscriber *subscriberHandler, event if maxRetry <= 0 { maxRetry = DefaultSubscriberMaxRetry } + // best_effort 订阅最多投递一次;at_least_once 按订阅者配置进行有限重试。 for attempt := 1; attempt <= maxRetry; attempt++ { req.Attempt = attempt result, err := subscriber.invoke(req) @@ -293,6 +311,8 @@ func (o *Operations) deliverSubscriberEvent(subscriber *subscriberHandler, event }) } +// configure 根据 manifest 重新构建插件运维能力边界。统计对象尽量复用, +// 但 schema、配额和外部依赖声明每次都以当前制品为准。 func (po *PluginOperations) configure(artifactID string, manifest Manifest) { po.mu.Lock() defer po.mu.Unlock() @@ -352,12 +372,15 @@ func (po *PluginOperations) configure(artifactID string, manifest Manifest) { } } if len(po.fileSpecs) == 0 { + // 未声明文件存储时提供默认命名空间,方便简单插件直接使用常见分类。 for _, namespace := range []string{"data", "cache", "tmp", "log", "diagnostic"} { po.fileSpecs[namespace] = FileStoreSpec{Namespace: namespace, QuotaBytes: DefaultPluginFileQuota} } } } +// EmitEvent 校验并记录插件事件。即使字段非法,也会排队一条 dropped 事件, +// 便于诊断插件为什么没有产出预期事件。 func (po *PluginOperations) EmitEvent(ctx context.Context, name string, fields map[string]string) error { trace := traceFromContext(ctx) clean, dropReason, err := po.validateEvent(name, fields) @@ -385,6 +408,8 @@ func (po *PluginOperations) EmitEvent(ctx context.Context, name string, fields m return nil } +// ObserveMetric 校验并更新插件自定义指标的最近摘要;这里不做时序存储, +// 只维护管理界面需要的当前观测值。 func (po *PluginOperations) ObserveMetric(ctx context.Context, name string, value float64, labels map[string]string) error { _ = ctx clean, metricType, err := po.validateMetric(name, labels) @@ -443,6 +468,8 @@ func (po *PluginOperations) RegisterBackgroundTask(task api.BackgroundTask) erro defer po.mu.Unlock() spec := po.taskSpecs[task.ID] if len(po.taskSpecs) > 0 && spec.ID == "" { + // manifest 声明了任务清单时,只允许注册清单中的任务,避免插件运行时 + // 动态创建管理端不可见的后台任务。 return fmt.Errorf("background task %q is not declared by manifest", task.ID) } if spec.ID == "" { @@ -471,6 +498,7 @@ func (po *PluginOperations) RegisterBackgroundTask(task api.BackgroundTask) erro } rt := po.tasks[task.ID] if rt == nil { + // confirmToken 用于高风险手动任务的二次确认,避免误点直接执行。 rt = &taskRuntime{ pluginID: po.pluginID, spec: spec, @@ -483,6 +511,8 @@ func (po *PluginOperations) RegisterBackgroundTask(task api.BackgroundTask) erro return nil } +// StartTasks 启动当前插件注册的后台任务调度器。只在插件 ID 匹配时执行, +// 防止调用方传错 ID 时启动其他插件的任务。 func (po *PluginOperations) StartTasks(pluginID string) { if pluginID != po.pluginID { return @@ -498,6 +528,7 @@ func (po *PluginOperations) StartTasks(pluginID string) { } } +// stopTasks 停止所有后台任务调度器。已经在执行的任务通过 cancel 感知停用。 func (po *PluginOperations) stopTasks() { po.mu.Lock() tasks := make([]*taskRuntime, 0, len(po.tasks)) @@ -536,6 +567,7 @@ func (po *PluginOperations) startTask(task *taskRuntime) { } go func() { for { + // 抖动值按任务 ID 确定,避免多个网关实例同一时间集中触发相同任务。 delay := interval + deterministicJitter(task.task.Jitter, task.task.ID) task.mu.Lock() if !task.schedulerOn { @@ -560,6 +592,7 @@ func (po *PluginOperations) startTask(task *taskRuntime) { func (po *PluginOperations) runTask(task *taskRuntime) { task.mu.Lock() if task.running { + // 同一任务不并发执行;调度周期追上时只记录跳过次数。 task.skipped++ task.mu.Unlock() return @@ -592,6 +625,8 @@ func (po *PluginOperations) runTask(task *taskRuntime) { task.mu.Unlock() } +// TriggerTask 手动触发后台任务。confirmToken 来自任务摘要,调用方必须显式回传, +// 用来降低误触发有副作用任务的风险。 func (po *PluginOperations) TriggerTask(taskID, confirmToken string) (BackgroundTaskSummary, error) { po.mu.Lock() task := po.tasks[taskID] @@ -609,6 +644,8 @@ func (po *PluginOperations) TriggerTask(taskID, confirmToken string) (Background return task.summary(), nil } +// Snapshot 汇总插件运行态观测信息。内存中的近期摘要和数据库中的历史摘要会合并, +// 形成管理端和诊断包都能使用的一份视图。 func (po *PluginOperations) Snapshot(ctx context.Context, pluginID string, handlers []DispatchHandlerSummary, builds []BuildRecord, gc []GCCandidate) OperationsSnapshot { _ = ctx po.mu.Lock() @@ -632,6 +669,7 @@ func (po *PluginOperations) Snapshot(ctx context.Context, pluginID string, handl recentEvents, _ := po.parent.repo.RecentEvents(ctx, pluginID, DefaultEventRecentLimit) if len(recentEvents) > 0 { + // 数据库中的事件能覆盖进程重启前的近期历史,内存摘要则提供当前进程最新值。 events = mergeEventSummaries(events, recentEvents) } logs, _ := po.parent.repo.RecentLogs(ctx, pluginID, DefaultLogRecentLimit) @@ -702,6 +740,8 @@ func (o *Operations) TriggerTask(pluginID, taskID, confirmToken string) (Backgro return po.TriggerTask(taskID, confirmToken) } +// DiagnosticPackage 生成可下载的插件诊断包。输出前会统一脱敏,避免把密钥、 +// token 或完整协议载荷写入可共享文件。 func (o *Operations) DiagnosticPackage(ctx context.Context, plugin PluginRecord, manifest Manifest, handlers []DispatchHandlerSummary, builds []BuildRecord, gc []GCCandidate) ([]byte, DiagnosticPackageSummary, error) { snapshot := o.Snapshot(ctx, plugin.ID, handlers, builds, gc) operations, _ := o.repo.ListOperations(ctx, plugin.ID, 50) @@ -745,6 +785,8 @@ func (o *Operations) runtimeRoot() string { func (o *Operations) GCCandidates(ctx context.Context, pluginID string) ([]GCCandidate, error) { now := time.Now().Unix() var candidates []GCCandidate + // 插件数据和文件只有在过期后才允许删除;未过期记录作为受保护候选项返回, + // 方便 dry-run 解释为什么没有删除它们。 data, err := o.repo.ListPluginData(ctx, pluginID) if err != nil { return nil, err @@ -799,6 +841,7 @@ func (o *Operations) GCCandidates(ctx context.Context, pluginID string) ([]GCCan if seenFiles[filePath] { return nil } + // 文件系统里存在但仓库没有记录的文件视为孤儿文件,可以由 GC 清理。 info, err := d.Info() if err != nil { return nil @@ -842,6 +885,8 @@ func (o *Operations) GCCandidates(ctx context.Context, pluginID string) ([]GCCan return candidates, nil } +// RunGC 执行插件运维数据清理。dryRun 只返回候选项并写操作日志, +// 真正删除时会跳过受保护项。 func (o *Operations) RunGC(ctx context.Context, actor, pluginID string, dryRun bool) ([]GCCandidate, error) { candidates, err := o.GCCandidates(ctx, pluginID) if err != nil { @@ -874,6 +919,8 @@ func (o *Operations) RunGC(ctx context.Context, actor, pluginID string, dryRun b return removed, nil } +// validateEvent 校验事件声明和字段集合,并限制字段值基数,避免插件事件把 +// 管理端和后续指标系统拖入高基数数据。 func (po *PluginOperations) validateEvent(name string, fields map[string]string) (map[string]string, string, error) { if !metricNamePattern.MatchString(name) { return nil, "invalid_event_name", fmt.Errorf("invalid event name %q", name) @@ -907,6 +954,7 @@ func (po *PluginOperations) validateEvent(name string, fields map[string]string) return clean, "", nil } +// validateMetric 校验自定义指标名和标签,确保插件只能上报 manifest 声明过的指标。 func (po *PluginOperations) validateMetric(name string, labels map[string]string) (map[string]string, string, error) { if !metricNamePattern.MatchString(name) { return nil, "", fmt.Errorf("invalid metric name %q", name) @@ -954,6 +1002,7 @@ func (l pluginLogger) write(ctx context.Context, level, message string, fields m } trace := traceFromContext(ctx) clean, _ := sanitizeLabels(fields, nil) + // 插件日志只保存摘要并脱敏,避免把完整请求、密钥或 token 写入运行态数据库。 item := LogSummary{ PluginID: l.ops.pluginID, Level: level, @@ -989,6 +1038,7 @@ func (s pluginDataStore) Put(ctx context.Context, record api.DataRecord) error { } current, _ := s.ops.parent.repo.PluginDataUsage(ctx, s.ops.pluginID) old, _, _ := s.ops.parent.repo.GetPluginData(ctx, s.ops.pluginID, key) + // 更新已有 key 时只计算净增长,避免重复写同一 key 被误判为超配额。 nextUsage := current - old.SizeBytes + int64(len(record.Value)) if nextUsage > s.ops.dataQuota { return fmt.Errorf("plugin_data quota exceeded: %d > %d", nextUsage, s.ops.dataQuota) @@ -1024,7 +1074,8 @@ func (s pluginDataStore) Get(ctx context.Context, key string) (api.DataRecord, e return api.DataRecord{}, err } return api.DataRecord{ - Key: record.Key, + Key: record.Key, + // 返回副本,避免调用方修改仓库层读取出来的缓冲区。 Value: append([]byte(nil), value...), SchemaVersion: record.SchemaVersion, DataClass: record.DataClass, @@ -1060,6 +1111,7 @@ func (s pluginFileStore) ResourcePath(name string) (string, error) { if !isSubpath(filepath.Join(artifactDir, "resources"), resource) { return "", errors.New("unsafe resource path") } + // ResourcePath 只返回随制品发布的只读资源路径,不写运行态文件记录。 return resource, nil } @@ -1091,6 +1143,7 @@ func (s pluginFileStore) Write(ctx context.Context, namespace, name string, data if !isSubpath(root, target) { return errors.New("unsafe file path") } + // 路径校验后再创建目录,防止插件通过 ../ 写出自己的命名空间。 if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { return err } @@ -1174,6 +1227,7 @@ func (s pluginFileStore) Delete(ctx context.Context, namespace, name string) err func (s pluginFileStore) namespaceSpec(namespace string) (string, FileStoreSpec, error) { namespace = strings.TrimSpace(namespace) if namespace == "" { + // 默认命名空间让简单插件不必显式声明每次读写的分类。 namespace = "data" } if !metricNamePattern.MatchString(namespace) { @@ -1217,6 +1271,7 @@ func (c pluginExternalClient) DoHTTP(ctx context.Context, req api.ExternalReques if err := c.beforeRequest(); err != nil { return api.ExternalResponse{}, err } + // beforeRequest 会增加 inflight,后续必须在 defer 中成对减少。 start := time.Now() defer c.runtime.inflight.Add(-1) @@ -1239,6 +1294,7 @@ func (c pluginExternalClient) DoHTTP(ctx context.Context, req api.ExternalReques if spec.Traceparent { trace := traceFromContext(ctx) if trace.TraceID != "" { + // 只透传 trace id,不暴露内部 connection id 或插件处理器 id。 httpReq.Header.Set("traceparent", "00-"+limitHex(trace.TraceID, 32)+"-0000000000000000-01") } } @@ -1254,6 +1310,7 @@ func (c pluginExternalClient) DoHTTP(ctx context.Context, req api.ExternalReques if lastErr == nil && resp.StatusCode < 500 { break } + // 需要关闭失败响应体,避免重试时泄漏连接。 if resp != nil && resp.Body != nil { _ = resp.Body.Close() } @@ -1297,6 +1354,7 @@ func (c pluginExternalClient) DialTCP(ctx context.Context, address string, timeo } start := time.Now() defer c.runtime.inflight.Add(-1) + // 外部 TCP 连接返回给插件后由插件负责关闭;这里仅记录拨号阶段的观测信息。 var dialer net.Dialer conn, err := dialer.DialContext(ctx, "tcp", address) if err != nil { @@ -1308,6 +1366,8 @@ func (c pluginExternalClient) DialTCP(ctx context.Context, address string, timeo return conn, nil } +// HealthCheck 使用 manifest 声明的 endpoint 做轻量检查。TCP 依赖只建立后关闭, +// HTTP 依赖优先使用 HEAD,避免拉取大响应体。 func (c pluginExternalClient) HealthCheck(ctx context.Context) error { spec, err := c.declaredSpec() if err != nil { @@ -1343,6 +1403,8 @@ func (c pluginExternalClient) declaredSpec() (ExternalSpec, error) { return spec, nil } +// beforeRequest 检查熔断窗口并记录并发请求数。成功进入请求路径后, +// 调用方必须在结束时减少 inflight。 func (c pluginExternalClient) beforeRequest() error { c.runtime.mu.Lock() defer c.runtime.mu.Unlock() @@ -1354,6 +1416,8 @@ func (c pluginExternalClient) beforeRequest() error { return nil } +// finish 更新外部依赖统计并维护一个简单熔断器。连续三次失败会短暂打开熔断, +// 防止插件把故障依赖打爆。 func (c pluginExternalClient) finish(start time.Time, status string, err error) { duration := time.Since(start) c.runtime.durationCount.Add(1) @@ -1376,6 +1440,7 @@ func (c pluginExternalClient) finish(start time.Time, status string, err error) c.runtime.circuitUntil = time.Time{} } +// recordTrace 把外部依赖调用写入 trace 摘要,endpoint 和 purpose 会先脱敏。 func (c pluginExternalClient) recordTrace(ctx context.Context, start time.Time, kind, status string) { trace := traceFromContext(ctx) _ = c.ops.parent.repo.SaveTrace(context.Background(), TraceSummary{ @@ -1392,6 +1457,7 @@ func (c pluginExternalClient) recordTrace(ctx context.Context, start time.Time, }) } +// summary 返回外部依赖的可展示状态,并隐藏 endpoint 中可能带账号的信息。 func (rt *externalRuntime) summary(pluginID, name string) ExternalDependencySummary { rt.mu.Lock() defer rt.mu.Unlock() @@ -1420,6 +1486,7 @@ func (rt *externalRuntime) summary(pluginID, name string) ExternalDependencySumm } } +// summary 返回后台任务的当前调度状态,供运维快照和管理端展示。 func (rt *taskRuntime) summary() BackgroundTaskSummary { rt.mu.Lock() defer rt.mu.Unlock() @@ -1450,6 +1517,8 @@ func (rt *taskRuntime) summary() BackgroundTaskSummary { } } +// WithTraceContext 把插件处理链路信息塞入 context,供日志、事件和外部依赖 +// 记录复用同一个 trace/connection 标识。 func WithTraceContext(ctx context.Context, pluginID, traceID, connectionID, handlerID string) context.Context { return context.WithValue(ctx, traceContextKey{}, traceContext{ PluginID: pluginID, @@ -1467,6 +1536,8 @@ func traceFromContext(ctx context.Context) traceContext { return trace } +// sanitizeLabels 过滤插件上报字段:数量、名称、声明范围、敏感字段和单值长度 +// 都会被限制,避免低成本插件事件变成高基数或敏感数据出口。 func sanitizeLabels(fields map[string]string, allowed map[string]bool) (map[string]string, error) { if len(fields) == 0 { return map[string]string{}, nil @@ -1497,6 +1568,7 @@ func sanitizeLabels(fields map[string]string, allowed map[string]bool) (map[stri return clean, nil } +// cleanStoreKey 校验插件数据存储 key,禁止绝对路径、反斜杠和上级目录片段。 func cleanStoreKey(key string) (string, error) { key = strings.TrimSpace(key) if key == "" || !storeKeyPattern.MatchString(key) { @@ -1508,6 +1580,7 @@ func cleanStoreKey(key string) (string, error) { return key, nil } +// cleanStorePath 校验插件文件路径,要求传入值已经是规范相对路径。 func cleanStorePath(name string) (string, error) { if name == "" || strings.Contains(name, `\`) || strings.HasPrefix(name, "/") { return "", fmt.Errorf("unsafe file path %q", name) @@ -1519,6 +1592,7 @@ func cleanStorePath(name string) (string, error) { return clean, nil } +// isSubpath 判断 target 是否仍在 root 内,作为最终路径穿越保护。 func isSubpath(root, target string) bool { root = filepath.Clean(root) target = filepath.Clean(target) @@ -1526,6 +1600,7 @@ func isSubpath(root, target string) bool { return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) } +// retentionDeadline 将保留时间转换为 Unix 时间戳;0 表示不过期。 func retentionDeadline(retention time.Duration) int64 { if retention <= 0 { return 0 @@ -1533,6 +1608,7 @@ func retentionDeadline(retention time.Duration) int64 { return time.Now().Add(retention).Unix() } +// parseDurationDefault 在 manifest 配置缺失或非法时返回默认时长。 func parseDurationDefault(value string, fallback time.Duration) time.Duration { if value == "" { return fallback @@ -1544,6 +1620,7 @@ func parseDurationDefault(value string, fallback time.Duration) time.Duration { return parsed } +// deterministicJitter 基于任务 key 生成稳定抖动,避免每次重启后调度时间完全随机。 func deterministicJitter(jitter time.Duration, key string) time.Duration { if jitter <= 0 || key == "" { return 0 @@ -1562,6 +1639,8 @@ func limitString(value string, max int) string { return value[:max] } +// redactSensitive 对明显敏感的文本做粗粒度脱敏。它不替代结构化密钥管理, +// 只作为日志、诊断和摘要输出前的最后防线。 func redactSensitive(value string) string { if value == "" { return "" @@ -1575,6 +1654,7 @@ func redactSensitive(value string) string { return value } +// redactEndpoint 隐藏包含账号信息的 endpoint,并限制展示长度。 func redactEndpoint(endpoint string) string { if endpoint == "" { return "" @@ -1585,6 +1665,7 @@ func redactEndpoint(endpoint string) string { return limitString(endpoint, 256) } +// randomToken 生成确认令牌;随机源失败时退化为时间戳,保证调用方仍能完成流程。 func randomToken() string { var data [16]byte if _, err := rand.Read(data[:]); err != nil { @@ -1593,6 +1674,7 @@ func randomToken() string { return hex.EncodeToString(data[:]) } +// limitHex 将 trace id 规范为指定长度的十六进制字符串,用于 traceparent 头。 func limitHex(value string, max int) string { value = strings.ToLower(value) var out strings.Builder @@ -1607,6 +1689,7 @@ func limitHex(value string, max int) string { return out.String()[:max] } +// readLimited 读取外部响应时设置硬上限,避免插件依赖返回超大 body 占满内存。 func readLimited(reader io.Reader, max int64) ([]byte, error) { var buf bytes.Buffer if _, err := io.CopyN(&buf, reader, max+1); err != nil && !errors.Is(err, io.EOF) { @@ -1618,6 +1701,7 @@ func readLimited(reader io.Reader, max int64) ([]byte, error) { return buf.Bytes(), nil } +// mergeEventSummaries 合并内存事件摘要和数据库近期事件,按插件和事件名聚合计数。 func mergeEventSummaries(current, recent []EventSummary) []EventSummary { byKey := make(map[string]EventSummary) for _, event := range current { @@ -1643,6 +1727,7 @@ func mergeEventSummaries(current, recent []EventSummary) []EventSummary { return out } +// noop* 类型用于在插件未启用完整 Operations 时仍返回满足接口的安全空实现。 type noopOperationsLogger struct{} func (noopOperationsLogger) Debug(context.Context, string, map[string]string) {} diff --git a/internal/pluginmanager/repository.go b/internal/pluginmanager/repository.go index 037ac2c..ddb6eb4 100644 --- a/internal/pluginmanager/repository.go +++ b/internal/pluginmanager/repository.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/repository.go 持久化插件制品、插件记录、快照、构建、密钥、评审、公告和操作日志。 + package pluginmanager import ( diff --git a/internal/pluginmanager/types.go b/internal/pluginmanager/types.go index 28b4123..d0578ab 100644 --- a/internal/pluginmanager/types.go +++ b/internal/pluginmanager/types.go @@ -1,3 +1,5 @@ +// internal/pluginmanager/types.go 定义仓库、管理器、Admin API 和前端共用的插件管理数据模型。 + package pluginmanager import ( diff --git a/internal/tcphttpmux/mux.go b/internal/tcphttpmux/mux.go index c81974f..ff69d5c 100644 --- a/internal/tcphttpmux/mux.go +++ b/internal/tcphttpmux/mux.go @@ -1,3 +1,5 @@ +// internal/tcphttpmux/mux.go 通过窥探首包并回放数据,把同一个监听器拆分给 HTTP 和原始 TCP 处理器。 + package tcphttpmux import ( @@ -11,12 +13,15 @@ import ( ) const ( + // 默认只等待一秒首包,避免慢连接长期占住共享监听器的分流协程。 DefaultInitialPacketTimeout = time.Second DefaultHTTPConnBacklog = 128 DefaultReadBufferSize = 64 * 1024 maxHTTPMethodPrefixLen = len("OPTIONS ") ) +// httpMethodPrefixes 是共享端口识别 HTTP 的方法前缀白名单。Minecraft +// 握手首字节是 VarInt 长度,不会以这些明文方法开头,因此首包前缀足够分流。 var httpMethodPrefixes = [][]byte{ []byte("GET "), []byte("POST "), @@ -29,6 +34,8 @@ var httpMethodPrefixes = [][]byte{ []byte("TRACE "), } +// Options 收集共享监听器的可调参数和观测回调。调用方通过回调接入日志、 +// 指标和 socket 选项,避免 tcphttpmux 反向依赖网关主包。 type Options struct { InitialPacketTimeout time.Duration HTTPConnBacklog int @@ -40,6 +47,8 @@ type Options struct { OnHTTPDeliveryFailed func(net.Conn) } +// replayConn 先读已经窥探到的首包,再继续读底层连接。这样分流逻辑可以检查 +// 首包,同时 HTTP 或 TCP 处理器仍能看到完整原始字节流。 type replayConn struct { net.Conn reader io.Reader @@ -49,6 +58,8 @@ func (c *replayConn) Read(p []byte) (int, error) { return c.reader.Read(p) } +// ChanListener 把已识别为 HTTP 的连接投递给 http.Server。它实现 net.Listener, +// 但没有真实 accept socket,只消费 Deliver 写入的连接。 type ChanListener struct { conns chan net.Conn closed chan struct{} @@ -56,6 +67,7 @@ type ChanListener struct { addr net.Addr } +// readBufferPool 降低首包窥探时的临时分配;后续回放给处理器的数据来自 peeked 副本。 var readBufferPool = sync.Pool{ New: func() any { buf := make([]byte, DefaultReadBufferSize) @@ -63,9 +75,13 @@ var readBufferPool = sync.Pool{ }, } +// Serve 在同一个底层监听器上同时服务 Admin HTTP 和 Minecraft TCP。每条连接 +// 会先读取少量字节判断协议,再被投递给 http.Server 或 tcpHandler。 func Serve(listener net.Listener, handler http.Handler, tcpHandler func(net.Conn), opts Options) error { defer listener.Close() + // http.Server 仍使用标准库模型,只是它的 listener 是内存通道。 + // 这样管理端路由、中间件和超时语义都保持为普通 HTTP 服务。 webListener := NewChanListener(listener.Addr(), opts.normalizedHTTPConnBacklog()) webServer := &http.Server{Handler: handler} webServerDone := make(chan error, 1) @@ -100,10 +116,13 @@ func Serve(listener net.Listener, handler http.Handler, tcpHandler func(net.Conn if opts.SetSocketOptions != nil { opts.SetSocketOptions(conn) } + // 每条连接独立分流,避免慢客户端阻塞共享监听器继续 accept。 go HandleConn(conn, webListener, tcpHandler, opts) } } +// HandleConn 完成单连接分流。它只负责协议识别和投递,认证、路由和转发 +// 仍由 HTTP handler 或 tcpHandler 里的业务层完成。 func HandleConn(conn net.Conn, webListener *ChanListener, tcpHandler func(net.Conn), opts Options) { peeked, err := ReadInitialPacket(conn, opts.normalizedInitialPacketTimeout()) if err != nil { @@ -123,6 +142,8 @@ func HandleConn(conn net.Conn, webListener *ChanListener, tcpHandler func(net.Co replayed := NewReplayConn(conn, peeked) if IsHTTPInitialPacket(peeked) { + // HTTP 连接投递失败通常意味着 HTTP server 已关闭或通道已满; + // 此时不应降级为 Minecraft TCP,直接关闭更明确。 if !webListener.Deliver(replayed) { if opts.OnHTTPDeliveryFailed != nil { opts.OnHTTPDeliveryFailed(conn) @@ -138,6 +159,8 @@ func HandleConn(conn net.Conn, webListener *ChanListener, tcpHandler func(net.Co tcpHandler(replayed) } +// ReadInitialPacket 读取足够判断协议的首包片段。对于可能是 HTTP 方法名的 +// 短前缀会继续读取,直到确认是 HTTP、确认不是 HTTP 或达到最长方法名前缀。 func ReadInitialPacket(conn net.Conn, timeout time.Duration) ([]byte, error) { if timeout > 0 { if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { @@ -172,6 +195,7 @@ func ReadInitialPacket(conn net.Conn, timeout time.Duration) ([]byte, error) { } } +// NewReplayConn 用已窥探字节包裹连接,让下游处理器无需知道首包曾被提前读取。 func NewReplayConn(conn net.Conn, peeked []byte) net.Conn { return &replayConn{ Conn: conn, @@ -179,6 +203,7 @@ func NewReplayConn(conn net.Conn, peeked []byte) net.Conn { } } +// IsHTTPInitialPacket 判断首包是否已经完整匹配某个 HTTP 方法前缀。 func IsHTTPInitialPacket(buf []byte) bool { for _, prefix := range httpMethodPrefixes { if bytes.HasPrefix(buf, prefix) { @@ -188,6 +213,8 @@ func IsHTTPInitialPacket(buf []byte) bool { return false } +// IsPotentialHTTPInitialPacket 判断当前字节是否仍可能发展成 HTTP 方法名。 +// 例如只读到 "GE" 时还不能判定为 Minecraft,需要继续等 "GET " 或排除。 func IsPotentialHTTPInitialPacket(buf []byte) bool { if len(buf) == 0 { return true @@ -206,6 +233,7 @@ func getReadBuffer() []byte { } func putReadBuffer(buf []byte) { + // 只回收标准容量的缓冲区,避免外部错误切片把异常大小塞回池中。 if cap(buf) != DefaultReadBufferSize { return } @@ -213,6 +241,7 @@ func putReadBuffer(buf []byte) { readBufferPool.Put(&buf) } +// NewChanListener 创建供 http.Server 消费的内存 listener。 func NewChanListener(addr net.Addr, backlog int) *ChanListener { if backlog <= 0 { backlog = DefaultHTTPConnBacklog @@ -245,6 +274,8 @@ func (l *ChanListener) Addr() net.Addr { } func (l *ChanListener) Deliver(conn net.Conn) bool { + // Deliver 是非阻塞的:HTTP accept 队列满时返回 false,由调用方关闭连接。 + // 这可以保护共享监听器不被管理端慢请求拖住。 select { case <-l.closed: return false diff --git a/internal/tcphttpmux/mux_test.go b/internal/tcphttpmux/mux_test.go index fb8871d..1d6ccbb 100644 --- a/internal/tcphttpmux/mux_test.go +++ b/internal/tcphttpmux/mux_test.go @@ -1,3 +1,5 @@ +// internal/tcphttpmux/mux_test.go 包含用于约束 mux 行为的测试。 + package tcphttpmux import ( diff --git a/internal/upstreamtarget/target.go b/internal/upstreamtarget/target.go index 2227e45..605fbdc 100644 --- a/internal/upstreamtarget/target.go +++ b/internal/upstreamtarget/target.go @@ -1,3 +1,5 @@ +// internal/upstreamtarget/target.go 把上游目标字符串解析为传输协议和拨号地址。 + package upstreamtarget import "strings" diff --git a/internal/upstreamtarget/target_test.go b/internal/upstreamtarget/target_test.go index 1b52985..da95201 100644 --- a/internal/upstreamtarget/target_test.go +++ b/internal/upstreamtarget/target_test.go @@ -1,3 +1,5 @@ +// internal/upstreamtarget/target_test.go 包含用于约束 target 行为的测试。 + package upstreamtarget import "testing" diff --git a/plugin/api/api.go b/plugin/api/api.go index 48e16db..b0f9ed1 100644 --- a/plugin/api/api.go +++ b/plugin/api/api.go @@ -1,3 +1,5 @@ +// plugin/api/api.go 定义托管网关插件从宿主进程获得的稳定 API 能力。 + package api import ( @@ -33,6 +35,8 @@ type ( Message string `json:"message"` } + // PreflightContext 是治理预检时传给插件的上下文,包含当前配置、 + // 发布动作、运行限制和 manifest 中声明的范围信息。 PreflightContext struct { PluginID string `json:"plugin_id"` ArtifactID string `json:"artifact_id"` @@ -49,6 +53,7 @@ type ( Checks []PreflightCheck `json:"checks"` } + // SelfTestProfile 描述插件自检运行的配置档位,例如 dev 或 prod。 SelfTestProfile struct { Name string `json:"name"` } @@ -65,33 +70,48 @@ type ( SelfTest(profile SelfTestProfile) (SelfTestResult, error) } + // Gateway 是宿主暴露给插件的能力集合。插件只能通过这些方法注册钩子、 + // 上报观测数据、访问受限存储或创建后台任务。 Gateway interface { + // HandleConn 将连接交还给网关主流程,适合协议代理插件在完成前置处理后继续复用路由。 HandleConn(conn net.Conn) + // ExitWaitGroup 返回进程退出等待组,插件启动的长期 goroutine 应纳入该等待组。 ExitWaitGroup() *sync.WaitGroup + // Hook 注册钩子处理器。推荐通过 RegisterHookHandler 使用类型安全包装。 Hook(hook string, handler any) error + // EmitEvent 上报 manifest 声明的低基数事件。 EmitEvent(ctx context.Context, name string, fields map[string]string) error + // ObserveMetric 上报 manifest 声明的自定义指标最近值。 ObserveMetric(ctx context.Context, name string, value float64, labels map[string]string) error + // Logger 返回会自动脱敏并落库摘要的插件日志器。 Logger() Logger + // DataStore 返回按插件隔离、受配额限制的键值数据存储。 DataStore() DataStore + // FileStore 返回按插件和命名空间隔离的运行态文件存储。 FileStore() FileStore + // ExternalClient 返回 manifest 中声明的外部依赖客户端。 ExternalClient(name string) ExternalClient + // RegisterBackgroundTask 注册可由宿主调度或手动触发的后台任务。 RegisterBackgroundTask(task BackgroundTask) error } + // EventSchema 声明插件可上报的事件名和字段白名单。 EventSchema struct { Name string `json:"name"` Fields []string `json:"fields,omitempty"` } + // CustomMetricSchema 声明插件可上报的指标名、类型和标签白名单。 CustomMetricSchema struct { Name string `json:"name"` Type string `json:"type,omitempty"` Labels []string `json:"labels,omitempty"` } + // Logger 是插件日志接口。字段会被宿主清洗和脱敏后保存为摘要。 Logger interface { Debug(ctx context.Context, message string, fields map[string]string) Info(ctx context.Context, message string, fields map[string]string) @@ -99,6 +119,7 @@ type ( Error(ctx context.Context, message string, fields map[string]string) } + // DataRecord 是插件键值存储的一条记录。Retention 为 0 表示不过期。 DataRecord struct { Key string Value []byte @@ -108,19 +129,26 @@ type ( Retention time.Duration } + // DataStore 为插件提供受配额限制的持久化键值存储。 DataStore interface { Put(ctx context.Context, record DataRecord) error Get(ctx context.Context, key string) (DataRecord, error) Delete(ctx context.Context, key string) error } + // FileStore 为插件提供受命名空间和路径校验保护的运行态文件存储。 FileStore interface { + // ResourcePath 返回制品随包发布的只读资源路径。 ResourcePath(name string) (string, error) + // Write 写入运行态文件,并记录数据分类和保留时间。 Write(ctx context.Context, namespace, name string, data []byte, dataClass string, retention time.Duration) error + // Read 读取运行态文件,maxBytes 用于限制单次读取大小。 Read(ctx context.Context, namespace, name string, maxBytes int64) ([]byte, error) + // Delete 删除运行态文件和对应仓库记录。 Delete(ctx context.Context, namespace, name string) error } + // ExternalRequest 描述一次通过宿主外部依赖客户端发出的 HTTP 请求。 ExternalRequest struct { Method string URL string @@ -129,27 +157,35 @@ type ( Timeout time.Duration } + // ExternalResponse 保存外部 HTTP 调用返回的状态、头和受限大小的响应体。 ExternalResponse struct { StatusCode int Header http.Header Body []byte } + // ExternalClient 只允许访问 manifest 声明过的外部依赖,并由宿主负责超时、 + // 观测、简单重试和熔断。 ExternalClient interface { DoHTTP(ctx context.Context, req ExternalRequest) (ExternalResponse, error) DialTCP(ctx context.Context, address string, timeout time.Duration) (net.Conn, error) HealthCheck(ctx context.Context) error } + // BackgroundTask 描述插件注册给宿主调度的后台任务。 BackgroundTask struct { - ID string - Name string + // ID 必须稳定且唯一,用于管理端触发、日志和调度状态展示。 + ID string + Name string + // Interval 为 0 表示不自动周期执行。 Interval time.Duration RunOnStart bool - Jitter time.Duration - Timeout time.Duration - Manual bool - Run func(context.Context) error + // Jitter 用于打散周期任务,避免多个插件或实例同时触发。 + Jitter time.Duration + Timeout time.Duration + // Manual 为 true 时只允许通过管理端手动触发。 + Manual bool + Run func(context.Context) error } AbstractPlugin struct{} diff --git a/plugin/api/api_test.go b/plugin/api/api_test.go index 01290a4..f39cb76 100644 --- a/plugin/api/api_test.go +++ b/plugin/api/api_test.go @@ -1,3 +1,5 @@ +// plugin/api/api_test.go 包含用于约束 api 行为的测试。 + package api import ( diff --git a/plugin/api/hook.go b/plugin/api/hook.go index 162ca66..c64f38d 100644 --- a/plugin/api/hook.go +++ b/plugin/api/hook.go @@ -1,3 +1,5 @@ +// plugin/api/hook.go 定义插件钩子键、类型化钩子契约、请求模型和默认决策。 + package api import ( @@ -9,24 +11,32 @@ import ( ) var ( + // UnsupportedHookType 表示宿主不认识插件注册的钩子类型。 UnsupportedHookType = errors.New("unsupported hook type") - ErrPass = errors.New("plugin handler pass") - ErrBlocked = errors.New("plugin handler blocked") + // ErrPass 表示当前处理器主动放弃处理,让后续处理器继续尝试。 + ErrPass = errors.New("plugin handler pass") + // ErrBlocked 表示插件明确阻断当前连接或操作。 + ErrBlocked = errors.New("plugin handler blocked") ) type ( + // ConnectionIDContextKey 和 TraceIDContextKey 保留给需要通过 context 传递链路标识的插件。 ConnectionIDContextKey struct{} TraceIDContextKey struct{} + // HookType 描述一个类型安全的钩子键,Accept 是筛选函数类型,Handler 是处理函数类型。 HookType[Accept, Handler any] struct { key string } + // HookHandler 把筛选函数和处理函数成对注册到同一个钩子上。 HookHandler[Accept, Handler any] struct { acceptor Accept handler Handler } + // UpstreamConnectRequest 是上游连接钩子的完整上下文。插件可读取首包、 + // 路由结果、连接来源和链路 ID,以决定是否提供自己的上游连接。 UpstreamConnectRequest struct { Context context.Context Source net.Conn @@ -51,9 +61,12 @@ type ( ListenerPort int } + // UpstreamConnectAcceptor 返回 true 时,对应 Handler 才会被调用。 UpstreamConnectAcceptor func(UpstreamConnectRequest) bool - UpstreamConnectHandler func(UpstreamConnectRequest) (net.Conn, error) + // UpstreamConnectHandler 返回 net.Conn 表示插件提供上游连接;返回 ErrPass 表示跳过。 + UpstreamConnectHandler func(UpstreamConnectRequest) (net.Conn, error) + // RouteResolveRequest 描述一次主机路由解析请求,并携带 SQLite 快照的兜底结果。 RouteResolveRequest struct { Context context.Context `json:"-"` Host string `json:"host"` @@ -68,6 +81,7 @@ type ( Handshake UpstreamHandshakeRef `json:"handshake,omitempty"` } + // UpstreamHandshakeRef 是路由请求中稳定的握手摘要,便于插件记录或转发。 UpstreamHandshakeRef struct { ServerHost string `json:"server_host,omitempty"` RawServerHost string `json:"raw_server_host,omitempty"` @@ -75,6 +89,7 @@ type ( NextState int `json:"next_state,omitempty"` } + // RouteDecision 是插件路由解析的返回值。Action 决定覆盖、兜底、拒绝或继续传递。 RouteDecision struct { Action string `json:"action"` Upstream string `json:"upstream,omitempty"` @@ -89,6 +104,7 @@ type ( RouteResolveAcceptor func(RouteResolveRequest) bool RouteResolveHandler func(RouteResolveRequest) (RouteDecision, error) + // StatusPingRequest 描述 Minecraft 状态查询请求,插件可以直接生成响应。 StatusPingRequest struct { Context context.Context `json:"-"` Host string `json:"host"` @@ -98,6 +114,7 @@ type ( Metadata map[string]string `json:"metadata,omitempty"` } + // StatusPingResponse 是插件返回给客户端的状态信息,最终会被宿主封成 Minecraft packet。 StatusPingResponse struct { MOTD string `json:"motd,omitempty"` Favicon string `json:"favicon,omitempty"` @@ -113,6 +130,7 @@ type ( StatusPingAcceptor func(StatusPingRequest) bool StatusPingHandler func(StatusPingRequest) (StatusPingResponse, error) + // FilterDecision 描述连接或握手过滤结果。Allow 和 Reject 用于兼容不同插件写法。 FilterDecision struct { Allow bool `json:"allow"` Reject bool `json:"reject,omitempty"` @@ -121,6 +139,7 @@ type ( Metadata map[string]string `json:"metadata,omitempty"` } + // ConnectionFilterRequest 在读取 Minecraft 握手前触发,只包含来源和传输信息。 ConnectionFilterRequest struct { Context context.Context `json:"-"` SourceAddr string `json:"source_addr,omitempty"` @@ -131,6 +150,7 @@ type ( ConnectionFilterAcceptor func(ConnectionFilterRequest) bool ConnectionFilterHandler func(ConnectionFilterRequest) (FilterDecision, error) + // HandshakeFilterRequest 在握手解析后触发,可按主机名、协议版本和 next state 过滤。 HandshakeFilterRequest struct { Context context.Context `json:"-"` SourceAddr string `json:"source_addr,omitempty"` @@ -141,6 +161,7 @@ type ( Metadata map[string]string `json:"metadata,omitempty"` } + // HandshakeFilterDecision 在过滤结果之外允许改写目标主机名。 HandshakeFilterDecision struct { FilterDecision RewriteHost string `json:"rewrite_host,omitempty"` @@ -149,6 +170,7 @@ type ( HandshakeFilterAcceptor func(HandshakeFilterRequest) bool HandshakeFilterHandler func(HandshakeFilterRequest) (HandshakeFilterDecision, error) + // EventDeliveryRequest 是插件事件订阅者收到的投递请求。 EventDeliveryRequest struct { Context context.Context `json:"-"` PluginID string `json:"plugin_id"` @@ -161,6 +183,7 @@ type ( Metadata map[string]string `json:"metadata,omitempty"` } + // EventDeliveryResult 控制事件订阅投递是否成功以及是否需要重试。 EventDeliveryResult struct { OK bool `json:"ok"` Retry bool `json:"retry,omitempty"` @@ -170,6 +193,7 @@ type ( EventSubscriberAcceptor func(EventDeliveryRequest) bool EventSubscriberHandler func(EventDeliveryRequest) (EventDeliveryResult, error) + // ProviderRegistration 描述插件向宿主声明的能力提供方,例如路由提供方。 ProviderRegistration struct { Type string `json:"type"` Name string `json:"name"` @@ -184,6 +208,7 @@ type ( ) var ( + // 路由决策动作使用字符串,方便 manifest、JSON API 和插件代码共享。 RouteDecisionPass = "pass" RouteDecisionOverride = "override" RouteDecisionFallback = "fallback" @@ -195,6 +220,7 @@ var ( FailPolicyOpen = "fail_open" FailPolicyClose = "fail_closed" + // HookUpstreamConnect 是新版上游连接钩子,携带完整请求上下文。 HookUpstreamConnect = HookType[ UpstreamConnectAcceptor, UpstreamConnectHandler, @@ -202,6 +228,7 @@ var ( key: "upstream.connect/v1", } + // HookUpstream 是旧版上游钩子,仅保留 source 和 host,供老插件兼容使用。 HookUpstream = HookType[ func(source net.Conn, host string) bool, func(source net.Conn, host string) (net.Conn, error), @@ -209,6 +236,7 @@ var ( key: "upstream", } + // HookRouteResolve 允许插件覆盖或拒绝主机到上游的路由结果。 HookRouteResolve = HookType[ RouteResolveAcceptor, RouteResolveHandler, @@ -216,6 +244,7 @@ var ( key: "route.resolve/v1", } + // HookRouteResolver 是 RouteResolve 的兼容别名。 HookRouteResolver = HookType[ RouteResolveAcceptor, RouteResolveHandler, @@ -223,6 +252,7 @@ var ( key: "route.resolver/v1", } + // HookStatusPing 允许插件直接回答 Minecraft 状态查询。 HookStatusPing = HookType[ StatusPingAcceptor, StatusPingHandler, @@ -230,6 +260,7 @@ var ( key: "status.ping/v1", } + // HookConnectionFilter 在握手读取前执行,适合按 IP 或传输类型做轻量拦截。 HookConnectionFilter = HookType[ ConnectionFilterAcceptor, ConnectionFilterHandler, @@ -237,6 +268,7 @@ var ( key: "connection.filter/v1", } + // HookHandshakeFilter 在握手解析后执行,适合按目标主机名或协议版本过滤。 HookHandshakeFilter = HookType[ HandshakeFilterAcceptor, HandshakeFilterHandler, @@ -244,6 +276,7 @@ var ( key: "handshake.filter/v1", } + // HookEventSubscriber 让插件订阅其他插件上报的事件。 HookEventSubscriber = HookType[ EventSubscriberAcceptor, EventSubscriberHandler, @@ -251,6 +284,7 @@ var ( key: "event.subscriber/v1", } + // HookProvider 让插件声明自己提供的能力,供管理端和调度逻辑展示。 HookProvider = HookType[ ProviderAcceptor, ProviderHandler, diff --git a/plugin/official/rulepolicy/rulepolicy.go b/plugin/official/rulepolicy/rulepolicy.go index b23b5cd..2194091 100644 --- a/plugin/official/rulepolicy/rulepolicy.go +++ b/plugin/official/rulepolicy/rulepolicy.go @@ -1,3 +1,5 @@ +// plugin/official/rulepolicy/rulepolicy.go 实现内置 rule-policy 插件,用于 IP 允许/拒绝、维护响应和限流策略。 + package rulepolicy import ( diff --git a/protocol/mc.go b/protocol/mc.go index fae8d5d..2fec9ce 100644 --- a/protocol/mc.go +++ b/protocol/mc.go @@ -1,3 +1,5 @@ +// protocol/mc.go 解析和改写用于主机路由与状态响应的 Minecraft 握手数据包。 + package protocol import ( @@ -8,6 +10,8 @@ import ( ) type Handshake struct { + // RawServerHost 保留客户端原始主机字段。部分代理协议会在主机名后附加 + // NUL 分隔的扩展数据,路由时要剥离,转发或改写时仍要保留。 RawServerHost string ServerHost string ProtocolVersion int @@ -17,6 +21,8 @@ type Handshake struct { // ReplaceMcHost 替换 Minecraft 主机名 // 必须是连接的第一个数据包 func ReplaceMcHost(buf []byte, host string) []byte { + // 这里只处理连接的第一个 Minecraft packet。若首包不完整或不是握手包, + // 返回 nil 让调用方按解析失败处理。 packet, consumed, err := readPacket(buf) if err != nil || len(packet) == 0 { return nil @@ -37,9 +43,11 @@ func ReplaceMcHost(buf []byte, host string) []byte { hostEnd := prefixEnd + n if spliterIndex := strings.IndexRune(rawHost, 0); spliterIndex != -1 { + // 保留 Forge/Bungee 等协议可能附带的 NUL 后缀,只改写真正用于路由的主机名。 host = host + rawHost[spliterIndex:] } + // 主机名长度变化会影响 packet 长度,因此需要重建 payload 和外层 packet 长度。 var payload bytes.Buffer payload.Write(packet[:prefixEnd]) payload.Write(encodeVarInt(len(host))) @@ -60,6 +68,8 @@ func GetMcHost(buf []byte) string { } func ParseHandshake(buf []byte) Handshake { + // Minecraft 握手包格式为: + // packet length、packet id、protocol version、server address、server port、next state。 packet, _, err := readPacket(buf) if err != nil || len(packet) == 0 { return Handshake{} @@ -95,6 +105,7 @@ func ParseHandshake(buf []byte) Handshake { } if spliterIndex := strings.IndexRune(host, 0); spliterIndex != -1 { + // NUL 前的部分是网关路由使用的主机名,NUL 后扩展数据只保留在 RawServerHost。 parsed.ServerHost = host[0:spliterIndex] } else { parsed.ServerHost = host @@ -115,6 +126,8 @@ func ReadString(buf []byte) (string, int, error) { } func StatusResponsePacket(value any) ([]byte, error) { + // 状态响应 packet id 为 0,body 是一个 JSON 字符串,外层仍使用 Minecraft + // VarInt 长度前缀封包。 data, err := json.Marshal(value) if err != nil { return nil, err @@ -130,6 +143,7 @@ func StatusResponsePacket(value any) ([]byte, error) { } func readPacket(buf []byte) ([]byte, int, error) { + // Minecraft packet 以 VarInt 表示 payload 长度;返回值 consumed 包含长度字段本身。 length, n, err := readVarInt(buf) if err != nil { return nil, 0, err @@ -147,6 +161,7 @@ func readVarInt(buf []byte) (int, int, error) { return 0, 0, errors.New("incomplete varint") } b := buf[i] + // 每个字节低 7 位是数值,高位为 1 表示后面还有字节。 value |= int(b&0x7f) << (7 * i) if b&0x80 == 0 { return value, i + 1, nil @@ -156,6 +171,7 @@ func readVarInt(buf []byte) (int, int, error) { } func readString(buf []byte) (string, int, error) { + // Minecraft 字符串同样使用 VarInt 长度前缀,长度按字节计算。 length, n, err := readVarInt(buf) if err != nil { return "", 0, err @@ -167,6 +183,7 @@ func readString(buf []byte) (string, int, error) { } func readUnsignedShort(buf []byte) (int, int, error) { + // server port 是网络字节序的无符号短整型;当前只需要跳过并验证长度。 if len(buf) < 2 { return 0, 0, errors.New("incomplete unsigned short") } @@ -176,6 +193,7 @@ func readUnsignedShort(buf []byte) (int, int, error) { func encodeVarInt(value int) []byte { var out []byte for { + // 与 readVarInt 对应,每轮写低 7 位,并用最高位标记是否还有后续字节。 b := byte(value & 0x7f) value >>= 7 if value != 0 { diff --git a/protocol/mc_test.go b/protocol/mc_test.go index 1ea2eca..e7097e4 100644 --- a/protocol/mc_test.go +++ b/protocol/mc_test.go @@ -1,3 +1,5 @@ +// protocol/mc_test.go 包含用于约束 mc 行为的测试。 + package protocol import ( diff --git a/protocol/smoke/helper.go b/protocol/smoke/helper.go index 87e469a..19a238e 100644 --- a/protocol/smoke/helper.go +++ b/protocol/smoke/helper.go @@ -1,3 +1,5 @@ +// protocol/smoke/helper.go 提供小型内存测试夹具,让协议代理测试可以使用真实 net.Conn 行为。 + package smoke import (