v2.2.8
Changed
- WebUI 重构:合并探测页 + 局部刷新 + 国家 emoji (fork).
packages/gui/extra/webui/index.html重写:合并"Xray 节点"和"探测状态"为"探测"页(Stage1/Stage2/Stage3 三卡片),去掉"上轮汇总"。Stage1 卡片显示主 xray 进程状态/当前选中节点(含国家 emoji)/出站节点数,右下角放 Balancer 锁定/解锁控件 + 时长下拉(锁定时禁用),完整 20 节点详情表可折叠展开(列排序)。Stage2 卡片显示开关/间隔/下一轮预计时间/上轮耗时/上轮抓取数。Stage3 卡片显示开关/轮次/批次进度/节点进度/耗时/下一轮时间/本轮可用/本轮失败。仪表盘新增版本号卡(GET /api/version,含 dev-sidecar + Xray core + Node.js 三版本),端口卡补充 31180 HTTP 代理,去掉"重启服务"按钮。日志页加手动刷新按钮 + 自动刷新切换 + 间隔下拉(2s/5s/10s/30s)。所有面板改局部刷新(textContent更新,不重建 DOM),避免 10s 自动刷新时界面抖动。缓存/配置页骨架优先渲染(固定卡片布局立即显示 +-占位 + "加载中..." tbody,API 返回后局部填充)。国家字段用String.fromCodePoint算法生成 emoji 国旗(ISO 2 字母代码),内联TwemojiCountryFlags.woff2字体(base64 data URL)解决 Linux Chromium 不渲染国旗 emoji 问题。导航精简为 5 项(仪表盘/探测/缓存/日志/配置)。 - Stage1 bootstrap 后立即临时锁定出口节点 (fork). 解决"启动后 5 分钟内无可用出口"问题(observatory
probeInterval=300s,第一次探完前 balancer 无延时数据无法 leastPing 选节点)。Stage1 ado 注入完成后立即调用overrideBalancer锁定延时最低的节点,sticky 状态通过setTimeout(probeInterval * 1000)自动到期后removeBalancerOverride解锁,让 observatory 的 leastPing 策略接管。手动解锁会clearTimeout取消自动解锁 timer,与定时器互斥无竞态。 - WebUI Xray 节点字段提取规则 (fork). 从
_TypedMessage_(带尾部下划线)提取协议,从proxySettings.server(trojan)或proxySettings.vnext(vless)提取地址,从streamSettings.protocolName提取传输协议,从securitySettings[0].serverName提取 SNI,从metricsRes.observatory(不是observation)提取延时和存活。 - WebUI Balancer 改为解析式展示 (fork). 不再展示 YAML 原文,解析出当前选中节点 tag 显示;锁定/解锁按钮 + 时长下拉框(5min/10min/30min/1h/24h/永久)右侧布局。
- WebUI 延时显示单位 (fork). 延时直接显示毫秒(不除以 1e6)。
- 远程配置未启用时不删除已有文件 (fork).
packages/core/src/config-api.js的downloadRemoteConfig/doDownloadRemoteConfig原逻辑在远程配置未启用或 URL 为空时调用deleteRemoteConfigFile删除本地文件。用户可能手动放置了remote_config_personal.json5,不应被空 URL 删除。改为只跳过下载,不删除已有文件。 - config.update 用 mergeWith 替代 merge 避免数组合并 (fork).
packages/core/src/config-api.js的update(partConfig)原用lodash.merge合并配置,对数组字段(如plugin.xray.rules)会按索引合并而非整体替换,导致用户想替换整个数组时旧元素残留。改为lodash.mergeWith+ customizer,对源端数组返回srcValue(整体替换),同时cloneDeep目标避免污染原配置。 - WebUI Stage2/Stage3 三态状态 + Stage2 运行实时进度 (fork). 用户反馈 Stage2/Stage3 应区分三种状态(已关闭/空闲/运行中),且 Stage2 运行中看不到进度。
getStageStatus()新增结构化state字段(stage2:off/idle/running,running=远端订阅抓取进行中,新增isStage2Running标志覆盖启动时后台同步与 Stage3 轮末触发两个入口,三处出口(含异常兜底)复位;stage3 同款三态 +enabled字段);删除与state === 'running'完全等价的冗余isRunning字段。Stage2 新增本轮实时数据:progress: {current, total}(正在抓第几个订阅/订阅总数,loadSubscriptionNodes新增onSubscriptionProgress回调,模块级函数经 options 传回插件闭包)、startedAt(本轮开始时间,前端算实时耗时)、fetched(本轮已抓取节点数,实时累计;结束后回落lastSyncFetchedCount)。nextTriggerAt = max(nextSyncAt, 下一轮 Stage3 开始时间)替代原"待触发"显示——Stage2 在 Stage3 轮末按需触发,前端显示"约 HH:MM:SS"。前端 Stage2 卡片:状态三态、新增"进度"格、"上轮耗时"→"耗时"(运行中显示本轮实时耗时并随自动刷新跳动,结束后为本轮总耗时)、"上轮抓取"→"抓取";Stage3 耗时字段同步改用state。
Added
-
WebUI 监控面板 (fork). 新增
packages/gui/extra/webui/index.html单页面 Web UI(端口 31182),用于无显示器服务器场景的 dev-sidecar 状态监控。面板包含:Dashboard(Xray/系统代理/服务器/Stage 状态概览)、Xray 节点(协议/地址/端口/SNI/传输/延时/存活状态 + 列排序)、缓存(统计卡片 + 已探测节点详情 + 国家分布 + 最优节点 + 订阅源)、Stage(探测进度/批次/候选数)、日志(tail 实时滚动)、配置(只读视图)、Balancer(解析式展示当前选中节点 + sticky 锁定/解锁下拉时长 5min/10min/30min/1h/24h/永久)。骨架优先渲染(render(null)占位 → API 返回后填充),避免加载闪烁。每 10 秒自动刷新当前激活面板。 -
/api/xray/nodes节点列表 API (fork).packages/core/src/modules/plugin/webui/routes.js新增节点列表接口,调用xray api lso(ListOutbounds)获取 live xray 进程的 outbound 列表,JSON.parse 后过滤 direct/block/metrics 节点,提取 protocol/address/port/SNI/transport 字段;合并 observatory metrics 数据(从/debug/vars拉取)展示延时和存活状态。 -
/api/xray/cache/nodes分页节点接口 (fork). 新增缓存节点分页接口(page/pageSize/sort参数),sort=smart时按延时升序 + 失效节点置底。 -
/api/xray/cache/stats缓存统计接口 (fork). 返回{ totalNodes, dbSizeBytes, countryDistribution },新增packages/core/src/modules/plugin/xray/cache.js的readCountryDistribution(cacheFilePath, limit)SQLGROUP BY country聚合国家分布。 -
/api/xray/probed-stats已探测节点统计接口 (fork). 读取probed-node-stats.json,返回{ totalProbed, countryDistribution, nodes }。 -
/api/xray/balancer返回 sticky 状态 (fork). balancer 接口返回{ balancer, xrayEnabled, sticky },sticky 来自getStickyStatus()(比解析 balancer 文本更可靠)。 -
/api/xray/stickyPOST/DELETE 锁定/解锁接口 (fork). POSTduration参数(秒),0 表示永久(10 年);DELETE 手动解锁,会clearTimeout取消自动解锁 timer,避免重复触发。 -
getStageStatus扩展 Stage1/2/3 字段 (fork).packages/core/src/modules/plugin/xray/index.js的getStageStatus()从 6 个字段扩展为结构化输出:stage1(processStarted/livePort/apiPort/metricsPort/liveNodes/currentSelectTag)、stage2(enabled/intervalHours/lastSyncAt/lastSyncDurationMs/lastSyncFetchedCount/nextSyncAt/nextSyncOverdue)、stage3(isRunning/generation/roundStartedAt/nextRefreshAt/totalDue/processed/batchIndex/plannedBatchCount/successBatchCount/availableCount/explicitFailureCount/removedCount)。nextRefreshAt改为真实时间戳(替换原Date.now()+1占位符)。新增getLiveNodeFingerprints()方法返回tag→fingerprint反向映射,供/api/xray/nodes关联 country,不暴露到getStageStatus避免响应过大。 -
Stage2 同步耗时/抓取数持久化 (fork).
packages/core/src/modules/plugin/xray/cache.js新增setStage2LastSyncStats(cacheFilePath, durationMs, fetchedCount)和getStage2LastSyncStats(cacheFilePath),持久化到 SQLite cache meta(stage2_last_sync_duration_ms/stage2_last_sync_fetched_count)。index.jsStage2 同步开始时记录stage2SyncStartedAt,完成后写入耗时和subscriptionNodeCount。Stage2 下一轮预计时间 =lastSyncAt + intervalHours*3600*1000(超过当前时间显示"待触发",因 Stage2 在 Stage3 轮末按需触发,非定时调度)。 -
Stage3 轮次计时暴露 (fork).
index.js新增外层变量stage3RoundStartedAt/stage3NextRefreshAt/stage3Progress,在refreshCacheFromCacheOnly入口、批次成功、轮次结束 3 个nextDelay计算点更新。WebUI 实时显示本轮耗时和下一轮触发时间。 -
/api/xray/nodes关联 country/exitIp (fork).routes.js的/api/xray/nodes路由调getLiveNodeFingerprints()拿tag→fingerprint,再用readCacheEntriesByFingerprints查缓存,返回nodeMetadata: {tag: {country, exitIp, owner}},前端展示节点所属国家。 -
mitmproxy 子进程启用 SIGUSR2 堆快照诊断信号 (fork).
packages/core/src/modules/server/index.js的 forkexecArgv新增--heapsnapshot-signal=SIGUSR2和--diagnostic-dir=<userBasePath>/logs:长期运行的 mitmproxy 子进程可随时kill -USR2 <pid>生成.heapsnapshot(Chrome DevTools Memory 面板加载对比,三快照法定位内存泄漏),进程不会被杀死(Node 注册 handler 捕获信号写快照后恢复;未加 flag 的进程收 USR2 会按 POSIX 默认行为终止——运维时只对 mitmproxy PID 发)。平时零开销;触发时 stop-the-world(实测 19MB 堆 ~秒级暂停,代理功能暂停后自动恢复),快照文件几十至几百 MB 需定期清理。未设--report-signal(同信号会冲突);诊断报告可经运行时process.report.writeReport()获取,无需 flag。实测:发 USR2 后进程存活、快照落盘~/.dev-sidecar/logs/、代理恢复 HTTP 200。
Fixed
- 修复 WebUI
readBody解析失败导致 unhandled rejection 崩进程 (fork).packages/core/src/modules/plugin/webui/routes.js的readBody对非法 JSON body reject,而 5 处调用点都在路由的 try 之外 await 它——非法 JSON 请求(如PUT /api/configbody 为not json)使 server 端 async handler 未捕获 reject,客户端永远等不到响应(fetch 挂起 → mocha 用例超时 + after-all 钩子挂起),CI 三平台test packages/core全部挂死在同一用例。修复:readBody解析失败改为 resolve(null),由路由已有的 body 类型校验(isPlainObject/Array.isArray)统一拦截返回 400INVALID_BODY;POST /api/xray/sticky补同款校验(原实现 body 为 null 时body.duration访问会 TypeError 崩进程)。另修复configApiSave.test.js的进程级HOME污染:该文件为隔离配置把process.env.HOME指向临时目录,但 mocha 单进程顺序执行使后续依赖真实 HOME 的测试(versionTest 等发真实网络请求的用例)读到已删除的目录而失败——after 钩子现恢复原 HOME 并configApi.reload()单例,临时目录保留(log appenders 仍指向它,删除会 ENOENT);core 测试脚本加--timeout 10000(CI 冷环境下首次require expose初始化超 mocha 默认 2s)。测试基建(CI 全量挂死的其余根因):(1)xrayGenConfig.test.js的"零节点无 balancer"断言是 v2.2.7 固定模板行为变更前的过时期望,更新为"零节点也总是生成 balancer";(2)webui.test.js是集成式测试(14 处路由 requireexpose加载完整 app),与同进程其他测试文件交互引发 worker 异常退出(mocha parallel 表现为 Workerpool Worker terminated,单进程表现为文件交接处 exit 1 无汇总)——reInjectXrayRules改为支持context.xrayApi注入(xrayApiOverride !== undefined即跳过全局 require,测试 fake context 传xrayApi: null使写用例不再触达真实 expose),webui.test.js 从全量排除(.mocharc.jsonignore)并新增test:webuiscript 独立进程运行(--no-config绕过 ignore);(3) core 测试切换 mocha--parallel --jobs 2(每文件独立 worker 进程,隔离进程级副作用)。全量pnpm test131 passing、pnpm run test:webui50 passing。mitmproxy 测试同治(第三次 CI 失败定位):(1)dnsLookupTest.mjs/dnsTest*.mjs是上游的真实 DNS 网络测试(DoH 到 quad9 等),GitHub runner 被 quad9 返回 HTML 页导致解析失败——.mocharc.json的 spec 限定test/*.js排除 .mjs(保留本地手动node test/dnsLookupTest.mjs运行);(2)wwwAuthenticateTest.js硬编码/home/uif79392/.dev-sidecar绝对路径的 CA 证书——CI runner 必然挂——改为os.homedir()推导(支持DS_CA_CERT/DS_CA_KEY环境变量覆盖),CA 不存在时describe.skip整套件(CI 上 14 passing + 3 pending,本地有 CA 全跑)。(3)tlsUtilsAkiTest.js的"真实落盘 CA"用例存在两处同型地雷:/home/uif79392/硬编码路径 + 箭头函数里this.skip()(this 非 mocha context,CI 无 CA 走该分支时this.skip is not a functionTypeError 崩掉)——改为同款 homedir 推导 + 用例级条件it.skip。 - 修复 Stage3 轮末 Stage2 周期触发是死代码,长期运行服务订阅永不刷新 (fork).
refreshCacheFromCacheOnly入口置isStageRunning = true(v2.2.7 修复"未设置 isStageRunning"时引入),但"Stage3 后触发 Stage2"的轮末检查用的是!isStageRunning守卫——该轮末段运行在refreshCacheFromCacheOnly体内,isStageRunning恒为true,守卫恒 false,周期性 Stage2 订阅刷新从未执行过(全部历史日志 0 次出现该触发)。影响:dev-sidecar 长期不重启时,subscriptionSyncIntervalHours=24h的订阅重抓永不发生,免费订阅节点逐渐失效、节点池枯竭;此前靠服务频繁重启(每次启动 start() 触发一次 Stage2)掩盖。修复:守卫改为!isStage2Running(防与启动时后台 Stage2 并发——isStage2Running是"订阅抓取进行中"的精确信号),generation守卫保留。触发块内的冷却判断(shouldSkipRemoteFetchDueToCooldown)已有双保险,无误触发风险。 - 恢复 Stage1 bootstrap 探测 (fork). v2.2.7 commit 92601af 误删了 Stage1 bootstrap 探测逻辑(
readCacheEntriesForStartup→probeNodesBatch→annotateProbeEntries→ 按 delay/country/owner 过滤 → 排序 → 切片startupNodeLimit),导致启动后无节点注入。已重新实现:bootstrap 探测成功后通过ado注入startupNodeLimit个节点(默认 20)到 live xray 进程。 - 修复
refreshCacheFromCacheOnly未设置isStageRunning(fork).refreshCacheFromCacheOnly函数入口未设置isStageRunning = true,导致 Stage3 运行时 Dashboard 显示"空闲"。修复:入口处设true,finally块设false。 - 修复 WebUI Xray 面板 enabled 读取错误 (fork). 前端从
stage/status读取enabled字段(不存在),导致 Xray 状态显示"关"。修复:改为从status.plugin.xray.enabled读取,支持 3 态显示(关/启动中/开)。 - 修复
xray lso返回 JSON 字符串未解析 (fork)./api/xray/nodes接口直接JSON.stringify了xray lso的 stdout 字符串,前端nodes.map is not a function报错。修复:后端JSON.parse后取outbounds数组。 - 修复
bootstrapSelectedEntries变量作用域 (fork). 变量声明在if块内部,ado 注入块访问时报ReferenceError: bootstrapSelectedEntries is not defined。修复:将声明提到外层作用域(与startupNodes同级)。 - 修复
www-authenticate响应头被错误拆分导致docker buildx imagetools inspect失败 (fork).packages/mitmproxy/src/lib/proxy/mitmproxy/createRequestHandler.js用split(',')拆分www-authenticate头,无法区分"多个 challenge 之间的逗号"与"单个 Bearer challenge 内 auth-param 参数分隔逗号"。Docker registry 返回的单个 Bearer challenge(Bearer realm="...",service="...",scope="...")被错误拆成多段,导致docker buildx imagetools inspect的 Go HTTP 客户端无法解析完整 challenge,OAuth token 获取流程中断,报unexpected status from HEAD request: 401 Unauthorized。修复:移除split(','),保留字符串原值传给res.setHeader()——单行含逗号分隔的多 challenge 符合 RFC 7235,HTTP/1 与 HTTP/2 模块均能正确处理字符串头值。 - 修复 MITM 叶子证书缺有效 Authority Key Identifier 导致 OpenSSL 3.2+ 验证失败 (fork).
packages/mitmproxy/src/lib/proxy/tls/tlsUtils.js的createFakeCertificateByDomain/createFakeCertificateByCA用{ name: 'authorityKeyIdentifier' }不给keyIdentifier值,node-forge 1.4.0 据此序列化出空 AKI(OID 存在但无 keyIdentifier)。OpenSSL 3.2+ 严格校验器判为Missing Authority Key Identifier并拒签,经 dev-sidecar 访问任何被 MITM 的 HTTPS 站点都报SSLCertVerificationError;OpenSSL 3.0 宽松故长期被掩盖。修复:改为{ name: 'authorityKeyIdentifier', keyIdentifier: caCert.generateSubjectKeyIdentifier().getBytes() },叶子 AKI 指向 issuer CA 的 SKI。仅改叶子签发,CA 证书不变,客户端无需重新信任;叶子证书纯内存 LRU 缓存,重启即用新逻辑重签。 - 修复
configApi.save()持久化 Xray 插件自动注入的 intercepts 污染 config.json (fork).packages/core/src/config-api.js的save(newConfig)直接用传入的newConfig(含运行时内存配置)与默认配置doDiff,把 Xray 插件injectRules()注入到globalConfig.get().server.intercepts的desc: 'Auto-injected by Xray Plugin'条目当作用户自定义差异写入~/.dev-sidecar/config.json。任何configApi.save()/update()调用(WebUI 保存配置、config reload、GUI 保存)都会污染 config.json,且这些条目下次 xray 启动会重新注入,持久化无意义。修复:save()深拷贝newConfig后过滤掉server.intercepts中desc === 'Auto-injected by Xray Plugin'的域名条目,再doDiff,不持久化运行时注入的规则;深拷贝避免污染内存配置,Xray 注入的 intercepts 仍在内存中工作。 - 修复 WebUI 热重载后 Xray 路由规则未重新注入 (fork).
packages/core/src/modules/plugin/webui/routes.js的PUT /api/config、PUT /api/xray/rules、POST /api/config/reload三处路由调用DevSidecar.api.plugin.xray.injectRules?.()时未传参数,而injectRules(rules, port)签名要求两个参数,首行if (!rules || !Array.isArray(rules)) return直接返回,导致热重载(ctxServer.reload()重置内存 intercepts 为 config.json 状态)后 Xray 的Auto-injectedintercepts 从内存消失,chatgpt/openai/linux.do 等域名流量不再走 xray 隧道,需重启服务才能恢复。根因:configApi.save()第 236 行configApi.set(diffConfig)用过滤后的 diffConfig 重新加载内存配置,导致内存中 Auto-injected intercepts 被覆盖,任何调用save()/update()/reload()的路由都受影响。修复:提取reInjectXrayRules()辅助函数(先removeRules()清空injectedRules数组避免累积,再从globalConfig.get().plugin.xray.rules和server.setting.xrayPort取参数调injectRules(rules, port),仅在 xray 启用且端口有效时注入),统一应用到 5 处热重载路由:PUT /api/config、PUT /api/intercepts、PUT /api/presetiplist、PUT /api/xray/rules、POST /api/config/reload。PUT /api/intercepts和PUT /api/presetiplist原本完全未调用 injectRules,同样导致 Auto-injected 丢失。 - 修复
preSetIpList: []被持久化并在启动时清空全部预设 IP (fork). 用户报告~/.dev-sidecar/config.json中出现"server": {"preSetIpList": []},导致依赖预设 IP 的域名(如 google.com)间歇性 ECONNRESET——启动时getConfigFromFiles()最后一步doMerge(merged, userConfig)用空数组把远程/默认配置合并出的全部预设 IP(63 键对象)整体替换为空(lodash 对"对象 vs 空数组"默认取数组类型),DNS 回退到 cf-DoT 实时解析出被墙 IP。三层缺陷叠加:(1)merge.js的doDiff对数组字段是整段替换语义,空数组也会原样写入 config.json;doMerge未防御"srcValue 是空数组、objValue 是普通对象"的情况,仅处理了反向(objValue 是数组)。(2)PUT /api/presetiplist等三个 WebUI 写路由无 body 类型校验,[]直接持久化。(3)packages/mitmproxy/src/index.js的updateConfig()用if (newProxyOptions.dnsConfig.preSetIpList)truthy 判断,而domainMapRegexply([])返回{origin:{}}为 truthy,热重载时空表覆盖内存中正确的 preSetIpList,且 SpeedTester 构造时捕获 dnsMap,老域名保留旧表、新域名拿到空表,造成"时好时坏"的间歇故障。修复:(1)doMergecustomizer 新增防御:srcValue是空数组且objValue是 plain object 时返回objValue(数组字段如plugin.xray.rules对空数组的整体替换语义不受影响);同时doDiff写入侧新增对称防御:新值为空数组且旧值为 plain object 时跳过不写入 diff,避免[]持久化污染 config.json,且已有脏数据在下次save()时自愈消失(diff 不再产生该差异,文件整体重写);(2)PUT /api/config、PUT /api/intercepts、PUT /api/presetiplist要求 body 为 JSON object,PUT /api/xray/rules要求 body 为 JSON array,其他类型返回 400INVALID_BODY;(3)updateConfig改为origin键数 > 0 才替换 preSetIpList(判断逻辑提取为options.js的shouldReplacePreSetIpList导出函数以便测试)。补充:SpeedTester._doTest中曾测速成功的 IP(item.time非空)后续失败不会被标记failed(第 219 行if (item.time == null)才标记),持续失败会留在pickNextForProbing()轮转池中;但因reportProbeResult(按需探测反馈)已无条件标记失败,实际影响有限,且无条件降级会牺牲网络抖动容忍(上游原设计意图),暂不改动。 - 修复 sticky 锁定在 observatory 首轮探测前被提前解除 (fork). 用户报告 dev-sidecar 启动后 observatory 还没开始第一次探测,锁定就被解除,xray 主进程 balancer 无节点可选。两个时序缺陷:(1) Stage3 热刷新路径(
maybeRegenerateLiveConfigFromCache)在移除 sticky 锁定节点时裸调removeBalancerOverride解锁——实测启动后仅 29 秒即解锁(sticky 节点恰好在移除集中),远早于 300s 探测周期;(2) 300s 自动解锁定时器假设 observatory 一轮内必然完成探测,但 observatory 只在每个周期边界发现 ado 注入的新节点——节点在周期开始后才注入时要再等一个完整周期(最多 600s),解锁时数据仍可能为空。balancer 的 leastPing 策略无 observatory 数据时不选择任何节点(v2.2.6 已移除fallbackTag: direct防止暴露真实 IP),流量直接失败。修复:提取fetchLiveObservatoryAliveCount()辅助函数(查询主进程/debug/vars的 observatory alive 节点数);新增armStickyAutoUnlock(logLabel, delayMs)共享函数,到期时先检查 observatory 存活节点数,为 0 则延长锁定 60s(最多 5 次扩展),Stage1 bootstrap 定时器与enableSticky自动释放定时器统一改用之(同时消除disableSticky中的重复内联代码);热刷新移除 sticky 节点时改为改锁新节点——从缓存查询剩余活节点延时(Stage3 刚用observatoryProbeUrl探测过,数据可信),改锁延时最低者并重置 300s 自动释放定时器,无候选时才解除锁定。实测:300s 定时器到期后正确延长 60s,observatory 第二轮探测完成后安全解锁,balancer leastPing 恢复正常选节点。新增xrayObservatoryMetrics.test.js(10 用例)覆盖getObservatoryStatusMap的 4 种 metrics 键名兼容解析与 alive 计数语义(0=延长锁定、-1=metrics 未就绪),防止键名解析回归导致防护静默失效。