Releases
v2.2.9
Compare
Sorry, something went wrong.
No results found
Changed
发布 v2.2.9:工作区包版本号 bump(core/gui/mitmproxy/cli 2.2.8 → 2.2.9),/api/version 随之生效 (fork). 下游反馈:export 等 v2.2.9 新能力已随 deb 部署,但运行中服务 /api/version 仍报 2.2.8,消费方无法据版本号探测"export 可用与否"。本次发布正式 bump 四包版本(/api/version 读 core package.json),deb 文件名随之变为 DevSidecar-2.2.9-amd64.deb。顺带澄清 export total 语义(文档 webui-api.md):total = 当前过滤条件下的总条数——带 available 等过滤时为过滤后总数,完全无过滤时为全库节点数 (实测 38.6 万);并把"非 2xx 响应不含 total/data 字段"写入契约——下游实测的 total=None(Python)是把 429 RATE_LIMITED 限流响应体当正常响应解析所致,消费方必须先检查 HTTP 状态码/error 字段。
WebUI 日志页接入 mitmproxy 子进程日志:模块下拉不再只有 core(core + server) (fork). v2.2.9 日志重构的已知边界(当时 CHANGELOG 明示"mitmproxy 子进程的 server.log 不再进 WebUI"):mitmproxy 是 fork 的独立进程,其 getLogger('server') 的 log4js 实例在子进程内存里,主进程的环形缓冲天然收不到——服务模式下 gui category 又无人写日志,于是模块下拉永远只有一项、模块过滤形同虚设。本次把 server 日志接进实时缓冲:IPC 转发 ——core fork mitmproxy 时注入 DS_LOG_IPC_FORWARD=1 标记 env,util.log-ring.js appender 在标记+process.send 同时存在时不本地存储、直接 process.send({ __dsLog: entry })(消息全 JSON 安全字段;断开时静默降级回文件/stdout);主进程 server/index.js 的既有 message 监听头部新增 __dsLog 分支调 appendExternal() 防御性清洗(非法 level/category/非对象丢弃、超长二次截断)后写入主进程环形缓冲,categories 下拉自动出现 server,WS 实时推送同时覆盖。坑 :不能只判 typeof process.send === 'function'——pnpm/mocha 跑测试时 mocha 本身就是 IPC fork 子进程(首版实现即在此翻车,本地存储被误判成转发),必须以显式 env 标记为准。message 监听里 __dsLog 分支放在既有 debug 序列化之前提前 return,避免每条日志一次 JSON.stringify 高频开销。测试 :logRing.test.js +5 用例(appendExternal 入 ring+订阅推送、防御性丢弃+截断、转发不本地存、IPC 断开静默降级、pnpm fork 有 send 但无标记不转发)。主套件 172 passing、webui 65 passing、mitmproxy 17 passing。
WebUI 日志页重构:文件轮询改为结构化实时日志(时间/等级/模块/消息) (fork). 原实现每 5s 读日志文件尾巴 dump 进 <pre>,无结构、无过滤。新架构:core 新增 log4js 自定义 appender util.log-ring.js——结构化环形缓冲(容量 3000、消息截断 2000 字符、内存上界 ~6MB),挂到全部 category;GET /api/logs 改查环形缓冲(level=最低等级、q=消息/模块子串不分大小写、category=精确模块、limit=最新 N 条时间升序,并返回去重模块清单与容量);实时增量经既有 WS /ws 新增 log 频道推送(webui 插件启动时订阅环形缓冲、关闭时退订)。前端日志页重写:四列表格(sticky 表头、等级色标 debug 灰/info 蓝/warn 黄/error 红)、搜索防抖、等级/模块下拉过滤(模块清单自动收集)、实时追尾(贴近底部自动滚、上翻暂停并出现"↓ 底部")、行点击展开全文、清屏、显示 X / 匹配 Y / 缓冲 Z 统计;DOM 行数上限 1000 防长会话膨胀。readLogTail 文件读取路径整体移除;注意 :日志范围从"core/server/gui 三个日志文件"变为"本进程实时缓冲"(service 模式即 core+gui 分类;mitmproxy 子进程的 server.log 不再进 WebUI,仍落盘——已于后续条目经 IPC 转发接回 )。测试 :logRing.test.js 9 用例(结构化捕获含对象/Error 堆栈、容量淘汰、截断限长、level/q/category/limit 过滤、订阅推送/退订/订阅方异常隔离、模块去重)+ webui.test.js 路由级 4 用例(结构化返回、min-level、q 大小写、category+limit);旧文件式契约 2 用例随端点退役移除、stable code field 用例改锚 404。webui 套件 65 passing、主套件 163 passing。
WebUI 探测页出站节点表 SNI 列改为 ASN 列 (fork). SNI 对节点选择没有实际参考价值(多为空或节点自身伪装域名),换成缓存元数据的 ASN 组织归属(nodeMetadata.owner,即 ASN mmdb 查得的归属组织,如 "AMAZON.COM, INC."),与 allowedOwners 筛选语义直接对应、便于核对 !cloudflare 排除是否生效;列排序同步切换,缓存页"已探测节点"表同语义列头一并由 owner 改为 ASN(仅显示标签,内部字段/DB/API/export 契约的 owner 命名不动)。
Stage3 轮次显示改为独立计数 roundNumber(从 1 起),不再复用内部 generation 代际 (fork). WebUI"轮次"原显示 generation——它是进程内失效代际(启动 Stage2 抓取占 docmirror#1 、close() 也会 +1),导致服务重启后首个 Stage3 探测轮显示 docmirror#2 。新增 stage3RoundCount 只在 refreshCacheFromCacheOnly 真实入轮时 +1,经 getStageStatus().stage3.roundNumber 暴露;generation 保留为内部失效保护(Stage2/批次守卫的 generation === refreshGeneration 检查不变)。WebUI 轮次显示切换到 roundNumber,webui-api.md 示例同步。回归测试 :xrayStageGating.test.js 新增用例——空缓存连续两轮断言 roundNumber 0→1→2 且每轮后 state='idle'(第二轮顺带复验空轮 isStageRunning 复位)。
Stage3 节点判死阈值从 3 连败放宽到 4 连败,退避阶梯 [7,30,90] 天完整生效 (fork). 原实现 nextFailureStreak >= 3 在第 3 次连续探测失败时即删除节点+写墓碑——continue 跳过了退避赋值,导致 CACHE_FAILURE_BACKOFF_DAYS 的 90 天档位成为不可达死代码 (第 3 败永远走不到 getFailureBackoffMs(3)),节点从入库到判死最短仅 ~37 天(7+30),"退避最长 3 个月"名不副实。改为 >= 4:第 3 败保留并退避 90 天,第 4 败才删除+写墓碑,最短判死周期 ~127 天 (7+30+90),死节点翻身窗口完整覆盖整个阶梯。live 池过滤保持 3 不变 (maybeRegenerateLiveConfigFromCache 的 failureStreak < 3 保留过滤与批次 rmo 超限裁剪的 streak >= 3)——3 连败节点仍会被及时移出 xray 运行时池不扛真实流量,只有缓存保留/墓碑判定放宽。xrayStageGating.test.js 三个用例同步更新:third-strike 用例改名 fourth-strike 且 streak 种子 2→3,多轮模拟补第 4 轮(90 天退避后的 2026-09-26)并断言前 3 轮节点均保留。
Fixed
修复服务重启后 Xray 报"端口 10801 被占用 (Strict Mode)"且插件永久停摆——孤儿 Xray 进程自愈清理 (fork). 生产实锤:服务 restart 后 Xray 启动失败: 端口 10801 被占用 (Strict Mode),但 ss -tlnp 显示 10801 被上一代服务残留的 xray (ppid=1 孤儿)监听着。根因链:主 Xray 为内存优化被移入隔离 cgroup(dev-sidecar-xray-probe.scope),systemd KillMode=control-group 在 stop 时根本杀不到它 ;上一代服务若被 SIGKILL 兜底(shutdown 超时)或 stop 竞态漏杀,xray 就成孤儿占住本地端口,新服务 Strict 检查发现端口被占直接放弃,且无任何自愈手段(2026-05-23 就出现过一次,当时手动恢复)。修复:process.js 主 Xray spawn 成功后写 pidfile(<xrayDir>/xray.pid)、stop() 完成后删除;新增 cleanupStaleProcess(binPath, configPath)——读 pidfile → 验存活 → /proc/<pid>/cmdline 双重身份验证(必须同时含 xray 二进制路径与 live config 路径,pid 复用绝不误杀,非 Linux 无 /proc 不杀) → SIGTERM 等 3s → SIGKILL 兜底 → 清 pidfile;index.js Strict 分支端口被占时先自愈清理再重查一次(清理不了才报原错误)。测试 :新增 xrayStaleProcess.test.js 4 用例(真实进程:自家进程按 pidfile 被杀+pidfile 删除、cmdline 不匹配的外来进程拒绝杀、死 pid 清 pidfile 返 false、无 pidfile 返 false)。主套件 167 passing。
修复启动/轮末候选窗口 SQL 层不应用 country/owner/maxDelay 筛选,轮末重生成把 live 池砍到 1 个节点(用户报 7→1) (fork). readCacheEntriesForStartup(bootstrap 与轮末 maybeRegenerateLiveConfigFromCache 回填的共同候选源)只按延迟取 probed_node_ids 前 limit 个,country/owner/maxDelay 全留给 JS 后置——生产实锤:宽测轮跑完后存活池 537 个,top-100 低延迟窗口被 cloudflare-owned 的合规国家节点霸榜 71 个 (CF WARP 延迟最低),!cloudflare 后置过滤后只剩 1 个候选;轮末重生成据此把 live 池里 19 个健康注入节点全部 rmo(liveNodes=1, kept=0, added=0, removed=19),全量合规节点其实有 183 个。bootstrap 同雷:下次重启候选也会饿死到 0-1 个。修复:窗口改为直接 SQL 查询 node_runtime_v2,用现成的 buildCompactV2FilterClauses 在 SQL 层应用 country/owner/maxDelay/stable 筛选后再 ORDER BY delay ASC, stable DESC, updated_at DESC LIMIT n(同 partial index,代价与每批维护 probed 列表等价);生产 DB 验证窗口 100/100 全合规。回归测试 :xrayCacheOrdering.test.js 新增 CF-starvation 用例(低延迟 CF-SG/HK 霸榜 + 高延迟非CF合规节点必须入选 + 无筛选取全局 top + 全筛空返回 []);xrayStageGating.test.js 既有用例从"ignores filters"断言更新为"filters 窗口内生效"。主套件 154 passing。
修复 WebUI 探测页出站节点表 vmess 协议地址/端口列为空 (fork). 出站节点详情表从 /api/xray/nodes(xray API 的 TypedMessage 序列化形态)提取地址/端口,链路覆盖 trojan/ss 的 proxySettings.server、vless 的 proxySettings.vnext、ss-2022 扁平字段——但 vmess 在该序列化下的字段是大写 Receiver (proxySettings.Receiver.address/port),恰好 live 池 20 节点中仅有的 2 个 vmess(proxy_85/proxy_127)地址端口显示为空。修复:addrOf/portOf 补 Receiver 回退级。已用生产 live API 真实数据验证:旧链空 2 个、新链 20/20 全提取(proxy_127→168.138.43.75:80、proxy_85→165.140.216.142:443)。
恢复 Stage3 宽松探测 / Stage1 严格探测的宽严分离设计(用户澄清被 92601af 重构漂移破坏) (fork). 原设计(4c2efb2f):Stage1 bootstrap 与 live observatory 用 observatoryProbeUrl(严格,接近真实目标),Stage3 周期探测用 probeUrl(宽松,gstatic 204)粗筛——缓存是共享资产,严格探测会把"能到 gstatic 但到不了 chatgpt.com"的节点判死并经 4 连败阶梯写墓碑,误删对下游 export(按国家/owner 取节点)有价值的节点。92601af3(08-21,固定模板+ado/rmo 重构)在重构时把 Stage3 的常驻探测与逐批探测两处调用点复制成了 bootstrap 的严格表达式(提交未提及探测语义,属无意漂移)。修复:两处 Stage3 调用点恢复 cfg.probeUrl || pluginConfig.probeUrl,probeNodesBatch 注释同步恢复宽严分离说明。契约锁定 :两处 URL 解析抽成具名纯函数 resolveStage3ProbeUrl/resolveStrictProbeUrl(全部 5 个调用点改走它们),xrayStageGating.test.js 新增宽严分离契约用例 2 个——"Stage3 只认 probeUrl,observatoryProbeUrl 配了也不得渗入"(92601af3 漂移现场反向验证过:人为复刻漂移该用例精确变红)+ "Stage1 严格优先、未配置时回退 probeUrl",未来重构再复制粘贴严格表达式进 Stage3 会直接挂测试。已知权衡:轮内批次注入现以宽松存活为准注入 live 池,严格 observatory 不过的节点占槽但 leastPing 永不选中(流量不受影响);live 池可用节点多样性取决于严格/宽松通过率之差,重启 bootstrap(严格 burst)会净化。
修复 Stage3 轮内逐批次"边测边注入"路径完全绕过 allowedCountries/allowedOwners 筛选 (fork). 生产实锤:allowedCountries=["US","DE","SG","FR","GB"] 配置下,live 池 20 节点中 6 个越界(HK/JP/NL×2/FI×2),balancer 甚至把 🇭🇰 节点选为当前出口。根因:国家/owner 筛选只存在于 Stage1 启动 bootstrap 与轮末 maybeRegenerateLiveConfigFromCache 回填两条路径,而 Stage3 轮内每批次探测成功后的 ado 热注入(缓存周期探测批次已写回 → Inject available nodes from this batch)只检查 delay>0 && 节点合法,rmo 超限裁剪也只按失败/延迟——任何探测存活的越界节点都会被直接注入 live 并可能凭低延迟挤掉合法节点。修复:新增 selectLiveInjectableEntries(复用 collectBootstrapCandidateEntries 的双重筛选语义,country 未知在白名单非空时同样拦截),批次注入前统一过滤,被筛掉时打 注入前国家/owner筛选: X/Y 通过 日志。轮末回填对 keptNodes 只查存活不复查国家(节点 country 漂移仍会保留,属已知边界)。回归测试 :xrayStageGating.test.js 新增 5 用例(HK 白名单外拦截、country 未知拦截、!cloudflare owner 排除、空筛选全放行、空输入)。
修复 Stage3 空批次轮永久卡死"运行中",后续所有轮被跳过(用户报 WebUI 显示异常) (fork). refreshCacheFromCacheOnly 在入口置 isStageRunning = true 后、主路径 try/finally(复位所在)开始之前 有一个"当前没有到期的可探测节点"早退 return——空批次轮从此不复位 :WebUI 永显"运行中"、耗时无限增长(实测卡 4h+)、nextRefreshAt 过期显示"(已过期)";更严重的是后续所有定时触发都命中"Stage 正在运行,跳过本轮"守卫,Stage3 整体停摆直到服务重启 (生产实测:13:22 空轮后 16:22 的下一轮被跳过)。修复:早退前复位 isStageRunning = false。回归测试 :xrayStageGating.test.js 新增空批次轮用例——实例化真实 Plugin 工厂(fake context)+ 临时目录播种零节点缓存,直接调用 refreshCacheFromCacheOnly 命中早退路径,断言 getStageStatus().stage3.state === 'idle';已在撤掉修复行的代码上验证该用例精确复现 'running' !== 'idle' 卡死症状。
修复 WebUI /api/xray/cache/nodes/export 全部过滤参数静默失效 + 默认 sharelink 格式崩溃(下游反馈) (fork). 四个叠加缺陷:① nodeToShareLink 是 packages/core/src/modules/plugin/xray/index.js 的模块顶层函数但从未导出——export 路由 require('../xray/index') 解构出 undefined,默认 format=sharelink 首次调用即抛 "nodeToShareLink is not a function"(生产包压缩后为 e is not a function),被 catch 包装成误导性的 CACHE_NOT_READY;② export 路由传给 readCacheEntries/countCacheEntries 的选项键名(sort/available/maxDelay/countries/owners/protocols/since)与缓存层 buildCompactV2FilterClauses 认的键(orderBy/maxDelayMs/countryInclude/ownerInclude)完全不匹配——所有过滤静默 no-op ,total 返回全库数而非过滤后数;③ available 连败阈值过滤在缓存层不存在;④ nodeToShareLink 的 ss 分支只读 settings.servers[0],ss-2022 扁平结构返回空分享链接。修复:导出 nodeToShareLink 并给 ss 分支补 ss-2022 扁平回退;键名重映射 + 接 availableOnly/maxFailureStreak(delay>0 AND failure_streak < 阈值,默认 3 可调);getCompactV2OrderByClause 补 delay/stable 排序模式(顺带修复前端缓存页 sort=delay|stable 自 v2.2.8 起即 no-op 的问题);meta 补 stable 字段。该接口现满足下游按条件取完整可起 xray 的 outbound(如每注册任务按国家过滤随机取 N 节点)的场景,契约见 doc/webui-api.md。
修复 WebUI /api/xray/cache/nodes 行投影 address/port 对 vless/vmess/ss-2022 为空串(下游反馈) (fork). 行投影只读 settings.servers[0](trojan/旧 ss 形态)——vless/vmess 的地址在 settings.vnext[0]、ss-2022 在扁平 settings.address/port,均取不到(与 v2.2.9 前端 addrOf/portOf 同型缺口,后端漏改)。修复:按协议结构三级回退提取(settings.server || settings.servers[0] || settings.vnext[0] || 扁平字段)。webui-api.md 同步:rows 示例对齐实测 10 字段(原文档误含不存在的 total 字段)、export 章节按实际契约重写(原文档误写成"后台导出任务+download token"形态)、补充 gzip 压缩说明。测试 :缓存层新增 availableOnly/maxFailureStreak 过滤与 delay/stable 排序用例(xrayCacheOrdering.test.js);路由层新增播种缓存的多协议/过滤/限流集成用例 6 个(webui.test.js,假时钟绕模块级 10s 限流)。主套件 144 passing、webui 套件 63 passing。
修复 WebUI 节点表 shadowsocks 家族协议的地址/端口列显示为空 (fork). packages/gui/extra/webui/index.html 的节点字段提取链只处理了 trojan(proxySettings.server)和 vless/vmess(proxySettings.vnext)两种结构——shadowsocks-2022(xray.proxy.shadowsocks_2022.ClientConfig)的地址/端口是 proxySettings 顶层扁平字段 (address/port 直接在下),旧 shadowsocks 是 proxySettings.servers[],两者都匹配不到 → 节点详情表的地址/端口列显示空。修复:提取链补全 ss-2022 扁平 proxySettings.address/port 与旧 ss 的 proxySettings.servers[0] 两级回退。实测:proxy_150(shadowsocks-2022 节点)从地址/端口全空修复为 130.61.233.63 | 59924 完整显示,国家关联(🇩🇪 DE)不受影响。
修复 WebUI sticky"永久"选项被 \|\| 300 吞为 300 秒自动解锁 (fork). packages/core/src/modules/plugin/webui/routes.js 的 sticky POST 路由用 parseInt(body.duration) || 300 解析时长——JS falsy 陷阱使 0 || 300 = 300,前端"永久"选项(传 duration: 0)被静默转为 300s,到期自动解锁,用户表现为"选永久锁过一会就被自动解锁"。修复:const rawDuration = parseInt(body.duration),Number.isFinite(rawDuration) && rawDuration >= 0 ? rawDuration : 300,0 再映射 10 年哨兵值 315360000s。实测:duration=0 → {"status":"ok","duration":315360000}、600 → 600、垃圾值 "xyz" → 300。
修复 sticky/引导锁状态机:Stage3 移除锁定节点导致 xray 无出站可选(流量 fallback direct) (fork). 生产日志实锤两个缺陷:(1) 批次级 rmo 超限裁剪 对含 sticky 锁定节点的裁剪集静默裸释放 override——无改锁、无 observatory 数据守卫、无日志;Stage3 首个批次在引导锁后 11-13 秒即触发(引导锁本为桥接 observatory 首轮 300s 探测空窗),释放后 leastPing 无数据选不出任何节点,xray 路由 fallback 到 direct,chatgpt.com 等全部流量绕过代理节点 3.8-4.6 分钟(当天 9 次重启 2 次触发)。(2) 永久锁 setTimeout 溢出 :duration=0 → 10 年哨兵 315360000s → 315360000000ms 超过 Node setTimeout int32 上限(2147483647ms≈24.8 天)被钳为 1ms,永久锁启用后 26ms 即"到期自动解除"。修复(两条不变量:override 永指向存活出站;一切解锁必须过 observatory 数据门):新增 packages/core/src/modules/plugin/xray/sticky.js 可注入模块——createStickyAutoUnlockTimer(delay 封顶 2147483641ms、链式续期至 unlockAt,到期检查 observatory 存活数、为 0 顺延 60s×5)与 pickStickySurvivorTag(缓存 delay 最低存活者优先、兜底任一存活);index.js 全部移除路径(批次裁剪/轮次热刷新)统一走 migrateOverrideBeforeRemove——先 overrideBalancer 迁移存活节点再 rmo(零悬空窗口),并保留原解锁定时器(手动锁时长语义不被降级,替代旧"改锁新节点+重置 300s");轮次热刷新 API 失败的重启回退在重启后按 heldStickyFp 指纹(清空旧 tag map 前捕获)或最优存活重锁,剩余时长取原 unlockAt;裸释放仅剩"无存活节点"最后手段并 log warn。新增 xrayStickyTimer.test.js 12 用例(手动时钟 harness:秒解回归、跨 24.8 天链式续期、守卫释放/顺延、re-arm/disarm、survivor 挑选)。生产实测:锁定节点 13 秒后被热刷新移除 → 原子迁移 proxy_33、全程 0 条 >> direct;原定时器 300s 到期未因迁移重置 → 无数据顺延 → observatory 出数据后守卫式释放;永久锁 150s 后仍 active(旧 bug 26ms 自解)。全量 143 tests passing。
修复 WebUI sticky 锁定后刷新页面时长下拉回跳"5 分钟" (fork). 现象:选"永久"锁定后刷新页面,时长下拉显示"5 分钟",用户误以为永久锁丢失。根因是纯前端显示误导——后端 10 年哨兵 timer 一直正常(routes 已把 duration:0 映射 315360000s),但下拉是静态 HTML、刷新后回默认首项,锁定中又被 disabled 冻结,显示与真实锁定状态毫无关联。修复(前后端):enableSticky 记录 stickyDurationMs;getStickyStatus() 新增返回 durationMs/unlockAt(xray 重启 re-apply 等不经过 enableSticky 的路径回退 unlockAt - now 剩余时长,active=false 时恒为 0);WebUI 锁定时把下拉恢复为真实时长对应选项(10 年哨兵 315360000000ms → "永久"),新增状态文本"已锁定:永久/剩余 xx"(随 loadProbe 轮询自动刷新),未锁定时清空。测试基建 :routes.js 三处 sticky require('../../../expose') 改为 resolveXrayPlugin()(优先 context.xrayPlugin 注入、生产回退 expose,与 reInjectXrayRules 的注入模式一致),webui.test.js 新增 "webui xray sticky routes (injected plugin)" 组 6 用例(duration=0 → 10 年哨兵不落回 300、600 原样透传、缺失/非法/负值兜底 300、非 JSON body → 400 INVALID_BODY 且不触插件、插件抛错 → 500 STICKY_FAILED、DELETE 调用 disableSticky);pnpm run test:webui 56 passing、xraySticky* 22 passing。生产实测:POST duration=0 → durationMs=315360000000、unlockAt 剩余 3650 天;锁定永久 → 刷新 → 下拉"永久"[selected] + "已锁定:永久";短时长锁定 → 刷新 → "已锁定:剩余 4m33s"。
You can’t perform that action at this time.