Skip to content
hyls9527Public

About

NetEnv:Windows 统一网络代理层 —— mihomo(Clash.Meta) 唯一入口 + 订阅聚合分流 + github/GPT 自适应选点与专项探针自愈(supervisor-loop) + new-api LLM 网关 + MCP 只读状态检测 + 密钥治理;PowerShell 5.1 工具链(doctor/status/apply/nodes),计划任务常驻自愈。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

34 Commits

Folders and files

Repository files navigation

NetEnv

本机统一网络代理层 + AI 软件网络适配 + 密钥治理(Windows / PowerShell 工具链)。

  • 一个入口:mihomo(Clash.Meta)承担全部代理职责;端口表固定,冲突即报错(见 docs/PORTS.md)
  • 订阅管线:nodes refresh 聚合多源免费订阅、剥离危险字段,生成 data/merged.yaml
  • 档位管理:apply -profile direct|proxy|github 切换系统代理 / Git / npm / 环境变量,带快照可回滚
  • LLM 网关:本机 new-api 实例(端口表 8093)作为模型 API 统一入口
  • AI 可达性:netenv-mcp.ps1 提供只读、输出脱敏的 MCP 状态检测
  • 密钥治理:订阅 URL 只进 Windows 凭据管理器;GitHub token 验证 / 清理;AES-256 7z 归档(见 docs/SECRETS.md)

快速开始

.\netenv.ps1 bootstrap          # 首次:镜像下载 mihomo / new-api 并 SHA256 校验
.\netenv.ps1 doctor             # 只读体检
.\netenv.ps1 status             # 状态摘要
.\netenv.ps1 config set-secret NetEnv/sub-free-nodes <订阅URL>   # 订阅 URL 只进凭据管理器
.\netenv.ps1 nodes refresh      # 聚合订阅 → data/merged.yaml
.\netenv.ps1 apply -profile proxy

命令全表:doctor / status / apply / nodes / clients / adopt / migrate / setup / install / uninstall / bootstrap / clean / config / secrets / mcp,详见 .\netenv.ps1 --help。

目录结构

netenv.ps1          主入口(调度器 + 实例锁)
netenv-mcp.ps1      MCP stdio server(JSON-RPC 2.0,默认只读)
fix-browser-proxy.ps1  一键切档修"浏览器打不开 GitHub"(github 档 systemProxy=false,浏览器直连即超时)并做落盘验收
lib/                core.ps1 + 命令模块 + supervisor/supervisor-loop(常驻自愈)+ run-supervisor-hidden.vbs(零窗口启动器)
config/             netenv.json / sources.json / clients.json(默认值,不含密钥)
docs/               文档(见下方索引)
tests/              Pester 测试与回归脚本
data/  logs/  backups/  export/     运行态与归档(.gitignore,不入库)

关键设计(不可关闭的底线)

  • GitHub 可用性保底:github.com、两个内容域(raw.githubusercontent.com / objects.githubusercontent.com)以及三个凭据端点 (api.github.com / codeload.github.com / ssh.github.com)全部交给 github-adaptive 组,组内同时持有 github-node / proxy-select / DIRECT, 按 github 自身端点探活后自动择通择优 —— 直连抖动时自动改走节点,节点挂了自动回直连。 凭据端点原设计强制 DIRECT,2026-09-11 因"直连抖动时没有任何退路,git clone 与 device-flow 登录直接失败"改为同组保底。 ⚠️ 知情选择:可用性优先,等于接受凭据流量经免费节点(TLS 仍端到端加密,但出口节点可见目标元数据);回退步骤见 docs/GITHUB-SAFETY.md。
  • github-adaptive 组的选点依据必须是 github 自身端点,默认 https://github.com/robots.txt,可由 config/netenv.json 的 subscription.githubUrlTest.url 覆盖。 用 google/gstatic 之类的通用 URL 会得出错误结论 —— 节点能通 google 不代表能通 github(实测同一时刻 DIRECT 探活 504 而节点 921 ms)。
  • GPT 长期可达(地区判据,不能只按延迟选点):chatgpt.com / openai.com / oaistatic.com / oaiusercontent.com / chatgpt.livekit.cloud 统一交给 gpt-adaptive 组(组内 gpt-node / proxy-select / DIRECT)。 ⚠️ 不能沿用通用组"按延迟选点":实测 mihomo 的 /delay 只看连通、不看状态码(api.openai.com 的 401/403、 chatgpt.com 的 403 挑战都返回正延迟),于是延迟最低的香港节点必被选中 —— 而香港是 OpenAI 不支持地区, 症状是 TCP/TLS 全通、api.openai.com 却返回 unsupported_country_region_territory。 实测同一次重测速就把出口从 SG(401 可用)换成 HK(403 不可用)。 因此 gpt-node 是 select 而非 url-test:由 supervisor-loop 的 GPT 专项探针用真实响应码逐个复测候选 (200/401/429=可达;403=不可达,并区分是否地区封锁),第一个通过者胜出并写入 data/gpt-state.json, 每轮重放(mihomo 重载后 select 组会回到默认成员)。 ⚠️ 控制器 JSON 不带 charset,节点名含 emoji:PS 5.1 的 Invoke-WebRequest 会按 ISO-8859-1 解码成乱码, 再拿去选节点必然匹配不到 —— 涉及节点名的控制器调用一律走 Get-NetEnvJsonUtf8 / Invoke-NetEnvJsonPut。
  • 敏感域名直连:sensitiveDomains + githubAuthDomains 生成 DOMAIN-SUFFIX,<domain>,DIRECT 规则。 ⚠️ 同一域名不得同时出现在直连列表与 githubAdaptiveDomains:DIRECT 规则在前会胜出,自适应规则永远不可达(曾是真实缺陷)。
  • 订阅仅 HTTPS,剥离 script 等危险字段;订阅 URL 存 Windows 凭据管理器,不入盘
  • 代理面不对外:mihomo 的 mixed-port / http / external-controller 均绑定 127.0.0.1,allow-lan: false
  • TLS 协议必须含 TLS 1.3:core.ps1 顶部 Set-NetEnvTlsProtocol 把 Tls13 并进系统原有协议位(SystemDefault → Tls13;已是 Tls12 的机器 → Tls12, Tls13),枚举不存在时降级到 Tls12 且不抛错。 ⚠️ 本机链路上 TLS 1.2 握手被干扰:2026-10-14 实测三个订阅源在 nodes refresh 里全部报 基础连接已经关闭: 发送时发生错误,而同一 URL 走 curl.exe 直连返回 http=200 并取回完整正文(277776 / 640791 / 21674 字节);在 .NET 里显式指定 Tls12 仍失败、改 Tls13 立刻 200。 症状是"订阅看起来过期、GitHub 却还能打开"——能打开只是 github-adaptive 组的 DIRECT 成员在兜底,924 个节点实际全部不可达,节点池冻结 106 小时、doctor 报 4 项红而 status 照样报绿。 修正前请先确认:curl.exe 能取回正文而 Invoke-WebRequest 不能,就是本缺陷;不要先去怀疑订阅 URL 失效或节点全挂。 另注:这类干扰是间歇性的(同一批 URL 连续两轮,一轮 2/6 失败、一轮全成),因此 nodes refresh 对每个源做 3 次重试;单源失败率约 33% → <5%。
  • .ps1 一律带 UTF-8 BOM:脚本含中文,且计划任务以 powershell.exe(Windows PowerShell 5.1)-File 方式执行;无 BOM 时 5.1 会按 ANSI/GBK 解码导致中文乱码。改动任何 .ps1 后请复查 BOM。 检查/修补用 .\tests\ensure-bom.ps1 -Check(只检查)或 .\tests\ensure-bom.ps1(补 BOM,幂等);tests\regression.ps1 已把它作为前置守卫, 缺 BOM 时直接中止并点名文件。2026-10-06 实测过一次自伤:编辑工具剥掉 4 个 lib\*.ps1 的 BOM,生产自愈循环连续 7 分钟报 所在位置 ...core.ps1:260 字符: 29,日志里还夹着中文乱码 —— 这类"中文错误文本 + 字符偏移"一律先怀疑无 BOM,而不是逻辑错误。
  • 兼容 Windows PowerShell 5.1:环境可能只有 powershell.exe 而无 pwsh.exe。因此脚本内不得使用 PS7 专属语法(::new() 构造、??、三元 ? :、-Parallel 等),统一用 New-Object 与显式 if/else。
  • 路径全部通用化:不写死用户名或安装盘符 —— 用户目录用 $env:USERPROFILE,仓库内部用 $PSScriptRoot 推导,外部工具(7z)用 Get-NetEnvSevenZip 探测,客户端注入点用 %USERPROFILE% 占位符。换机只需改 config/netenv.json。
  • 可选外部依赖缺失必须降级而非崩溃:npm、gh、7-Zip 等未安装时跳过该步并记 WARN;在 $ErrorActionPreference='Stop' 下直接调用不存在的命令会终止整条流程。

常驻与自愈

  • 双层健康判据:supervisor.ps1 只负责进程存活(端口在听);supervisor-loop.ps1 另按 health.probeIntervalMinutes 做端到端出品探针(真实经代理请求 health.probeUrls,任一目标可达即算可用)。 端口在听 ≠ 能上网 —— 实测 423 个节点中仅 8 个能到 google,而端口照样 LISTEN。 判据取多目标 OR 而非单目标:单探 google 会把"google 被墙、github 正常"误判成整机出品不可用, 从而每 5 分钟空刷一次订阅源(真实故障,见 logs/20260917.log)。
  • github 专项探针(判据独立于上面的 OR):health.githubProbe.enabled 为真时,supervisor-loop 每轮再单独探一次 github(默认 https://github.com/robots.txt),连败达 githubProbe.failThreshold 即触发该组重新测速择通(Invoke-NetEnvGithubGroupRecovery → GET /group/<组>/delay),复测通过则记 INFO,recoverMinIntervalMinutes 负责退避。
    • 为什么必须独立:出品判据是 OR,google 通时 github 单独挂掉不会触发任何自愈 —— 实测 2026-09-17 github.com 经代理握手失败(git push/clone 全废),而 health-state.json 一路 lastOk。
    • 为什么恢复动作不是刷订阅:github-adaptive 组是 lazy: true(没流量就不测速),一旦某轮被判定 alive:false 就再没有流量进来、也就永远不再测速,坏状态被冻结;而组内节点其实健康 (同一次测速实测 github-node 595ms / proxy-select 686ms)。触发一次组测速即恢复,代价远低于刷订阅。
  • GPT 专项探针(判据独立,且带"地区"语义):health.gptProbe 为真时 supervisor-loop 每轮单独探 https://api.openai.com/v1/models,连败 gptProbe.failThreshold 次即按地区判据轮换 gpt-node (Invoke-NetEnvGptNodeRotation:先按 auto-urltest 全池延迟取前 N 个候选,再逐个用真实响应码复测), 复测通过记 INFO,recoverMinIntervalMinutes 负责退避。 ⚠️ gptProbe.failThreshold 默认取 1(2026-09-29 实测后收紧):免费节点会闪断 —— 实测 11:35 钉住的 hysteria2-1232696196 在 11:58 单次探针失败后进入"待判窗", 要再等一个探针周期(5 分钟)才够 2 次失败触发轮换,而用户正好卡在这 5 分钟里(12:00 报"GPT 卡住", 12:03:50 才重放/轮换)。因此 GPT 单次失败即轮换,recoverMinIntervalMinutes 由 10 收到 5 (轮换内的全池测速不便宜,靠退避防止硬锤)。副作用:单次抖动也切节点,换来的是"卡顿窗口 ≈ 探针周期"。 ⚠️ loop 只在启动时读一次配置:supervisor-loop.ps1 在进程启动时把 health.* 解析成变量,之后不再重读; 改完 config/netenv.json 里的阈值/退避必须重启自愈循环才生效(Stop-Process 掉旧实例即可, 计划任务 NetEnv-Supervisor-Periodic 每 5 分钟会以新配置拉起;该任务以 RunLevel=Highest 运行, 非提权会话杀不掉它,这是实测踩过的坑)。
    • 为什么不能并进 health.probeUrls 的 OR 判据:google 通只说明"能上网",与 OpenAI 是否接受该出口地区无关; OR 判据下"google 正常但 GPT 全废"不会触发任何自愈(与 github 专项探针同一类盲区)。
    • 为什么恢复动作是轮换节点而不是刷订阅:地区不受支持是节点属性,刷订阅换一批节点照样可能全是香港。
    • 探针入口隔离(ports.mihomoProbe,默认 7898)—— 2026-10-06 事故的根治:早期实现是直接切线上组 gpt-node 来逐个候选复测。mihomo 没有"只让某一次请求走某节点"的接口,于是每个候选的复测窗口 (最长 ProbeTimeoutSec)内真实流量全程跟着候选走。实测 2026-10-06 16:53:07 探针连败触发轮换, 16:53:27 用户正好在窗口内打开 chatgpt.com,流量落到香港候选,Cloudflare 返回 unsupported_country_region_territory「无法加载网站」;16:53:32 轮换失败才还原 —— 用户看到的不是"节点挂了",而是轮换本身。 现在 lib/nodes.ps1 额外生成三样东西:listeners: 里一个 mixed 入口(ports.mihomoProbe)+ rules: 首条 IN-PORT,<port>,gpt-probe + 与 gpt-node 同成员的 select 组 gpt-probe。 复测全部经探针入口出网,线上组只在某候选真的通过地区判据之后做一次原子切换。 ⚠️ 三条硬约束:①IN-PORT 必须排在规则表最前(落到 DOMAIN-SUFFIX/MATCH 之后就永不生效); ②探针端口不得与 ports 内任何既有端口重复(重复会让 mihomo 直接拒绝加载配置); ③gptProbe.probeGroup 与 ports.mihomoProbe 是配套的,只改一个会让复测走错出口。 自检锚点:日志出现 GPT 节点轮换:探针专用入口不可用(组 gpt-probe / 端口 0)→ 退回切线上组的旧路径 即代表隔离没生效(doctor 与 tests\netenv.tests.ps1 的"GPT 探针入口隔离"用例都会红)。
    • 地区封锁名单(data/gpt-blocklist.json,gptProbe.blocklistHours 默认 12):被判 region-unsupported 的节点写入名单,并在候选排序阶段剔除。必要性同"为什么不是刷订阅"—— region-unsupported 是节点属性, 不会自己恢复;2026-10-06 一天之内同一个香港节点被重复复测 8 次,等于每次都在线上窗口里再捅一次。 名单带 TTL、过期即失效(地区策略会变,不做永久屏蔽)。
    • 候选枯竭时的全池兜底扫描(gptProbe.poolScanMax 默认 40 / poolScanTimeoutSec 4 / poolScanBudgetSec 120) —— 2026-10-06 18:00 二次故障的修法:那次不是地区问题,而是池子整体枯死 —— 对 auto-urltest 全部 650 个成员做一次组测速,8 秒只回来 1 个响应(hysteria2-1561765064), 649 个节点已死。旧实现只看预筛榜(rankGroup 并行测速的 top N),榜上全军覆没就报"轮换失败", 于是自愈循环只能干等 —— 明明池子里还有几百个没被预筛命中的节点没试过。 现在候选构建完之后,若预筛结果不足且探针隔离可用,就从 gpt-probe 组的全量成员里 随机起点(Fisher-Yates,poolScanSeed 可复现)追加最多 poolScanMax 个候选,跳过 DIRECT/REJECT/GLOBAL、已在候选里的、以及地区名单里的节点;兜底段单点超时降到 poolScanTimeoutSec,整体受 poolScanBudgetSec 预算约束(超预算记 WARN 全池兜底扫描预算 …s 用尽 后收手)。 另有一处配套放宽:rankGroup 首次测速零结果(整组 504)时,用 rankRetryTimeoutMs(默认 15 s)重测一次, 因为瞬时抖动会让整组测速假性全灭。 ⚠️ 兜底扫描只在探针隔离可用时启用:退路模式下每试一个候选都会切线上组,全池扫一遍 等于把用户的流量扔进海选(tests\netenv.tests.ps1 有专门用例钉住这条约束)。 调参注意:poolScanMax × poolScanTimeoutSec 不应超过 poolScanBudgetSec,否则预算先到、扫不到 poolScanMax 个。
    • ⚠️ 变量名大小写:PowerShell 不区分大小写。2026-10-06 实测两起静默事故,现由 tests\netenv.tests.ps1 的"源码静态检查:变量名大小写冲突"用例兜住: ①Invoke-NetEnvGptNodeRotation 内局部 $probePort = 0 与参数 $ProbePort 是同一个变量, 赋值即把参数清零 → 隔离分支永远走不到,上面那条"退回旧路径"的降级静默生效; ②Save-NetEnvGptBlocklist 的参数 $Entries 被局部 $entries = [ordered]@{} 清成空表 → 地区名单永远写成空。两者都不抛错,只在运行期表现成"功能没生效"。命名局部变量时不要只靠大小写区分。
  • 系统代理漂移自愈:supervisor-loop 每轮比对 WinINET 实际值与当前档位期望值并重写(5 分钟退避)。 必要性:第三方 VPN / 代理客户端连接时会接管系统代理,而进程探活与出品探针都发现不了 —— 端口照样 LISTEN、经隧道探针照样通,吃系统代理的程序却已全部退回直连(典型的"全绿着坏")。 判据与 doctor 共用 Get-NetEnvSystemProxyState;doctor 另有 代理绕过清单、VPN 类接管、 VPN 类自启项 三项,专治 TUN 接管下的探针假阳性(见 docs/TROUBLESHOOTING.md)。
  • 降级链:探针连续失败达 health.failThreshold → 触发 nodes refresh(自带"失败保留旧配置") → 经控制器 PUT /configs?force=true 重载 mihomo(刷新只改 data/merged.yaml;运行中的 mihomo 不会自己读新配置, 不重载则刷新出的节点永远不生效)→ 仍不可用且 health.autoFallbackToDirect 为真时回退 apply -profile direct。 状态写入 data/health-state.json;其中 lastRefreshAt 配合 health.autoRefreshMinIntervalMinutes(默认 30 分钟) 限制自动刷新频率,避免目标长期不可达时每 5 分钟锤一次订阅源。
  • 计划任务 NetEnv-Supervisor(开机)+ NetEnv-Supervisor-Periodic(每 5 分钟)→ lib\run-supervisor-hidden.vbs → lib\supervisor-loop.ps1:探活并拉起 mihomo / new-api
    • install --autostart 需要管理员权限;两条任务都用 wscript.exe 启动 VBS,不依赖 PATH 里的 pwsh.exe(PATH 缺 pwsh 时计划任务会静默失败),且不闪黑窗
    • VBS 内有单实例守卫:周期任务只在自愈循环已死时补拉,不会出现两个循环抢重启与日志
    • ⚠️ 但 CommandLine 匹配靠不住:计划任务以 RunLevel=Highest 启动循环,而非管理员会话 读不到高完整性进程的 Win32_Process.CommandLine(实测为空),VBS 的 LIKE 守卫因此看不见它 —— 实测 2026-09-24 启动的一个循环与后来的循环并存了 5 天,两边交替写 health-state.json, 把 github/GPT 的连败计数反复清零、专项探针阈值形同虚设(日志零痕迹)。因此循环自身另有 命名互斥量单实例守卫(Local\NetEnv.SupervisorLoop,与命令行可见性无关),并每轮写 data/loop-heartbeat-<pid>.json 心跳;doctor 的 自愈循环存活/单实例 项据此判定 「没在跑」与「跑了多个」,陈旧心跳由活着的实例回收。
  • 失败统计只认"已部署"的服务:二进制不存在的可选服务(new-api/Sub-Store)直接跳过, 不参与失败计数(否则未部署的 new-api 会让日志每分钟刷一条 ERROR,把真故障淹没)
  • 日志按天轮转、脱敏、UTF-8 编码,保留天数由 logging.rotateDays(默认 14)决定,netenv clean 用同一口径(logs/,不入库)
  • 订阅刷新不是自动的:需手动 nodes refresh(或用你自己的调度;data/sub-state.json 里 lastSuccessAt = 上次真正重写 merged.yaml 的时间,updatedAt = 上次刷新尝试的时间)。 ⚠️ 两个时间戳必须分开记:2026-10-08 17:18 实测 4 个已配置源全部下载失败、merged.yaml 一个字节没动 (与 sub-state.hash 逐字节相同),而旧实现只写 updatedAt,doctor 的「订阅新鲜度」照样报"8 小时前" —— 节点池实际已冻结 28 小时、订阅源全挂,却一路全绿(又一次"绿着坏")。 现在 doctor 一律按 lastSuccessAt 算 age,并在最近一次刷新零产出时把失败源清单写进 detail; 没有该字段的旧状态文件按 merged.yaml 的落盘时间算(不得退回 updatedAt —— 那等于把缺陷原样放回来), merged.yaml 缺失直接判红。
  • 非管理员可用的只读动作:doctor / status;adopt -Apply 等接管动作需要管理员权限

维护与自检

  • 回归:Import-Module Pester; .\tests\regression.ps1(Pester 3.4,跑 3 轮);.\tests\regression.ps1 -Live 会真起 mihomo 并临时切系统代理,仅在需要冒烟时用
    • ⚠️ -Live 需要独占 ports.mihomoHttp(默认 7897):生产 mihomo 正在监听时它会前置中止(exit 2), 而不是给出一个假绿。必要性:临时实例端口冲突会立刻退出,而紧随的"端口在听吗"读到的其实是生产进程, 于是旧实现打印 listening=True(PID 指向已死进程)继续跑,还会在 finally 里对真实档位执行 apply --undo —— 2026-10-09 实测。要跑真冒烟:先停生产 mihomo,或把 ports.mihomoHttp 改到空闲端口。
  • 体检:netenv doctor(失败项退出码 1);doctor --json 提供给 MCP 的 netenv_doctor_summary
  • 改完任何 .ps1 必须复查 UTF-8 BOM:编辑工具常常顺手剥掉 BOM,5.1 会按 GBK 解码使中文破坏引号配对(doctor 有 脚本 UTF-8 BOM 项兜底)
  • Windows PowerShell 5.1 实测坑位(改代码前先看,避免重新踩):
    • @($genericListOfObject) 会抛 Argument types do not match(List[string]/Object[] 正常)→ 统一用 .ToArray()
    • Get-Content -Raw 默认按 ANSI(GBK) 解码 → 读配置/状态一律走 Read-NetEnvFileText(显式 UTF-8,兼容有/无 BOM)
    • curl 连接失败时 %{http_code} 返回 000,"3 位数字"不等于成功 → 必须结合退出码判断
    • 外部命令缺失(npm/gh/7z)在 $ErrorActionPreference='Stop' 下会终止整条流程 → 先 Get-Command 守卫再降级

文档索引

文档 内容
docs/PORTS.md 端口表(固定,冲突即报错)
docs/GITHUB-SAFETY.md GitHub 认证流量与凭据安全细则
docs/SECRETS.md 密钥盘点 / 验证 / 归档 / 清理
docs/MCP.md MCP 接入与权限
docs/AI-CLIENTS.md AI 软件(编辑器 / CLI)代理接入
docs/SETUP-NEW-PC.md 新机装配
docs/TROUBLESHOOTING.md 排障

合规提示

本工具链用于网络代理配置管理,使用者自行承担当地法律法规责任;仅限个人设备使用。

About

NetEnv:Windows 统一网络代理层 —— mihomo(Clash.Meta) 唯一入口 + 订阅聚合分流 + github/GPT 自适应选点与专项探针自愈(supervisor-loop) + new-api LLM 网关 + MCP 只读状态检测 + 密钥治理;PowerShell 5.1 工具链(doctor/status/apply/nodes),计划任务常驻自愈。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages