Build and Publish Docker Image / build-and-push (pull_request) Successful in 15m54s
- service: EgressCacheKey 从 handler 下沉复用;MergeCachedEgressGeo 在合集 重命名前合并缓存 geo 字段(纯缓存读、零网络 I/O);StripEgressGeoFields 防止 geo 字段泄漏进订阅输出 - filter: geo 检测失败的节点保留 '[别名] 原名'(或纯原名)参与编号/排序, 不再丢弃 - handler: 每小时 StartEgressRefresher 对启用源补探测缺失/过期的 egress 缓存(含 TTL 内 error 结果一律跳过);RegisterRoutes 返回 *Deps; cmd/server.go 启动定时任务并在 shutdown 时取消 - tests: service 缓存 key 稳定性/合并/管道测试、handler 补探测跳过/TTL/ 过期/取消测试、filter 降级测试;修复 2 个过时测试(mihomo YAML 配置、 缓存命中路径)
84 lines
2.6 KiB
Go
84 lines
2.6 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/peterqiu0516/sub-store/internal/database"
|
|
"github.com/peterqiu0516/sub-store/internal/model"
|
|
)
|
|
|
|
// egressCacheKeySkip lists node fields excluded from the egress cache key so
|
|
// the key stays stable across pipeline stages (parse → filter → alias tag →
|
|
// rename) and matches the key the background probe wrote.
|
|
var egressCacheKeySkip = map[string]bool{
|
|
"id": true, "name": true, "_sourceAlias": true, "_previewId": true,
|
|
"latencyMs": true, "latencyError": true,
|
|
"egressIp": true, "egressCountry": true, "egressRegion": true, "egressError": true,
|
|
"country": true, "countryCode": true, "region": true, "city": true, "isp": true, "flag": true, "cached": true,
|
|
}
|
|
|
|
// EgressCacheKey returns the source_cache key for a node's egress probe
|
|
// result. Pushed down from the handler (HH-773) so the download pipeline
|
|
// reads the exact entries the probe wrote.
|
|
func EgressCacheKey(node model.ProxyNode) string {
|
|
clean := model.ProxyNode{}
|
|
for k, v := range node {
|
|
if !egressCacheKeySkip[k] {
|
|
clean[k] = v
|
|
}
|
|
}
|
|
data, _ := json.Marshal(clean)
|
|
sum := sha256.Sum256(data)
|
|
return fmt.Sprintf("egress:%x", sum)
|
|
}
|
|
|
|
// egressGeoFields are the cached egress-probe geo fields merged into nodes
|
|
// for collection-level renaming.
|
|
var egressGeoFields = []string{"country", "countryCode", "region", "city", "flag"}
|
|
|
|
// MergeCachedEgressGeo merges cached geo fields into nodes so collection
|
|
// renaming can use probed geo data. Pure cache reads — no network I/O, no
|
|
// probing inside the download pipeline. geoFromNode in the filter package
|
|
// consumes the merged fields.
|
|
func MergeCachedEgressGeo(repo *database.CacheRepo, nodes []model.ProxyNode) []model.ProxyNode {
|
|
if repo == nil {
|
|
return nodes
|
|
}
|
|
for _, node := range nodes {
|
|
if node == nil {
|
|
continue
|
|
}
|
|
entry, ok := repo.SafeGet(EgressCacheKey(node))
|
|
if !ok {
|
|
continue
|
|
}
|
|
var cached map[string]any
|
|
if json.Unmarshal([]byte(entry.Content), &cached) != nil {
|
|
continue
|
|
}
|
|
for _, field := range egressGeoFields {
|
|
if v, ok := cached[field]; ok && v != nil {
|
|
node[field] = v
|
|
}
|
|
}
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// StripEgressGeoFields removes the merged geo fields after renaming so they
|
|
// do not leak into rendered subscription output. No parser or filter sets
|
|
// these fields natively (they exist on nodes only via the egress merge), so
|
|
// stripping is safe.
|
|
func StripEgressGeoFields(nodes []model.ProxyNode) {
|
|
for _, node := range nodes {
|
|
if node == nil {
|
|
continue
|
|
}
|
|
for _, field := range egressGeoFields {
|
|
delete(node, field)
|
|
}
|
|
}
|
|
}
|