十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OmniRoute Radar 目录缓存补齐 generatedAt:区分「数据构建时间」与「本实例下载时间」(11435)

OmniRoute Radar 目录缓存补齐 generatedAt:区分「数据构建时间」与「本实例下载时间」(11435) OmniRoute Radar 目录缓存补齐 generatedAt区分「数据构建时间」与「本实例下载时间」#11435【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150 free), 1200 models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline Copilot. Quota-aware auto-fallback, RTKCaveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550 contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute本文围绕 OmniRoute 的一次数据新鲜度修复展开Radar 目录 feed 的本地缓存此前只保留了fetchedAt本实例下载 feed 的时间把 feed 自带的generatedAt数据构建时间丢弃了。修复后radar_feed_cache同时持久化这两个时间戳getRadarCatalog().meta与GET /api/radar/status分别对外报告操作者第一次能够区分「几分钟前下载」和「数据本身有几周旧」。读完本文你将理解这两个时间戳的语义差异、缓存丢字段的根因与迁移 163 的修复路径以及状态接口「字段缺失而非 null」的设计取舍。背景Radar 客户端与四个本地 feed 缓存OmniRoute 的 Radar 是一个签名的外部目录 feed 客户端服务端定期构建并签名一份目录 feed本实例按调度拉取、验签、做版本下限校验后缓存到本地 SQLite再由 applyFeed 合并层 叠加到静态 baseline 之上。Radar 模块的整体机制feed URL、公钥固定、版本下限等在 RADAR 框架文档 中有完整说明。从 本地 DB 模块 的注释与多张迁移文件可以看到Radar 在本地维护四张单行 feed 缓存表各自对应一个独立迁移缓存表迁移承载字段radar_feed_cache136_radar_cache_settings.sqlversion、tier、payload、signature、fetched_at修复后新增generated_atradar_referrals_cache142_radar_referrals_cache.sqlgenerated_at、tier、payload、signature、fetched_atradar_offers_cache144_radar_offers_cache.sqlversion、tier、payload、signature、fetched_atradar_intel_cache145_radar_intel_cache.sqlversion、tier、payload、signature、supporter_identity、fetched_at注意一个关键事实目录 feed 的构建数据是带签名的generatedAt位于签名 body 之内因此它不受客户端篡改影响——它是「数据本身有多旧」的可靠答案。而fetchedAt是本实例时钟记录的「什么时候下载到的」。两者回答的问题不同混用它们就是本次修复要解决的核心问题。问题schema 要求、sync 校验缓存却把它丢了目录 feed 的顶层 schemafeedSchema.ts把构建时间设为必填字段const RadarFeedV1Schema z.object({ feed: z.literal(omniroute-radar), schemaVersion: z.literal(1), version: z.string(), generatedAt: z.string().datetime(), // 必填数据构建时间 tier: TierEnum, counts: z.object({ providers: z.number().int(), models: z.number().int() }), // ...providers / models / quirks / referrals / totals });同步路径在验签、解析、通过 schema 校验之后写缓存。从 sync.ts 的缓存写入步骤 可以看到写入内容一直包含generatedAt// Step 9: Cache the result const cacheEntry: RadarCacheEntry { version: feed.version, generatedAt: feed.generatedAt, // 来自签名 body已按 schema 校验 tier: servedTier, payload: rawBytes.toString(utf-8), signature, fetchedAt: now().toISOString(), };也就是说数据链路的上游从未丢失这个日期——问题出在存储层迁移 136 建表时radar_feed_cache只有fetched_at一列落库时generatedAt被静默丢弃。后果正如 changelog 所述一份几分钟前拉取的 feed 和一份携带数周旧数据的 feed在下游一切消费者包括仪表盘的 “Last fetched” 一行看来完全相同。仅凭fetchedAt操作者无法判断本地 overlay 是否比它所叠加的 baseline 更新。修复细节迁移 163为缓存表补上generated_at列修复本体是一个单行 DDL163_radar_feed_cache_generated_at.sql-- radar_feed_cache (migration 136) kept only fetched_at — when this install -- downloaded the feed — while the feed itself carries generatedAt, the date -- its data was built. Nothing downstream could tell a recent download from -- recent data: a feed fetched minutes ago can carry weeks-old figures. -- -- radar_referrals_cache (migration 142) already persists that date; this -- brings the catalog cache in line. NULL on rows cached before this column -- existed — the date is unknown, and stays unknown rather than being stood in -- for by fetched_at. ALTER TABLE radar_feed_cache ADD COLUMN generated_at TEXT DEFAULT NULL;两个值得注意的设计决策都写在注释里其一radar_referrals_cache早在迁移 142 就持久化了同一语义的日期本迁移是把目录缓存对齐到既有先例而非新发明其二历史行显式DEFAULT NULL——列出现之前的行「日期未知」且永远保持未知而不是用fetched_at冒充。对应地DB 访问层 的RadarCache接口与读写函数同步更新export interface RadarCache { version: string; /** Date the feeds data was built, from the feed itself. Null when unknown. */ generatedAt: string | null; tier: string; payload: string; signature: string; fetchedAt: string; }getRadarCache() 的 SELECT 增加generated_at AS generatedAtsetRadarCache() 的 upsert 在ON CONFLICT(id) DO UPDATE中同时更新generated_at入参允许generatedAt?: string | null缺省落NULL。读路径getRadarCatalog().meta双日期上报Radar 客户端对 UI 的唯一入口是 getRadarCatalog()index.ts 头注释明确屏幕层只从此模块导入。修复后其返回类型RadarCatalogResult.meta同时携带两个日期类型定义meta: { version: string; /** * Date the feeds data was built. Null for a cache row written before the * column existed — unknown, never substituted by fetchedAt, which only * says when this install downloaded it. */ generatedAt: string | null; tier: string; fetchedAt: string; } | null;组装点在函数末尾return { entries, meta: { version: cache.version, generatedAt: cache.generatedAt ?? null, // 历史行读出 NULL → null tier: cache.tier, fetchedAt: cache.fetchedAt, }, };注意?? null这一步迁移前写入的行generated_at为NULL这里把它规范为 JSON 语义的null上报——「未知就是未知」的纪律从迁移注释一路贯彻到了 API 层。该函数在RADAR_ENABLED关闭、无缓存、或 payload 防御性再校验失败时返回 baseline 且meta: null双日期只在确实命中有效缓存时出现。状态接口generatedAt独立成字段且对 offers/intel 整体省略GET /api/radar/status 是运维视角的只读聚合端点需RADAR_ENABLED开启且已认证否则 404/401。本次修复把它的cacheStatus()改造成route.tsfunction cacheStatus( cache: { version?: string; generatedAt?: string | null; tier: string; fetchedAt: string } | null ) { if (!cache) return { available: false }; return { available: true, version: cache.version ?? cache.generatedAt, // Reported on its own where the cache carries it — folding the build date // into version loses the distinction between when a feed was built and // when this install downloaded it. Absent for the offers and intel caches, // which store no build date: a null there would claim the date is unknown // when in fact it was never kept. ...(generatedAt in cache ? { generatedAt: cache.generatedAt ?? null } : {}), tier: cache.tier, fetchedAt: cache.fetchedAt, }; }这段实现编码了三条精确的语义边界独立字段而非折进version。目录缓存没有version列时version会回退取generatedAtreferrals 缓存即此情形但构建日期一旦有值就单独输出generatedAt——把它折叠进version会抹掉「何时构建」与「何时下载」的区别这正是本次修复要恢复的区分能力。对象上无该属性时整体省略。offers 与 intel 两张缓存表见 getRadarOffersCache / getRadarIntelCache根本不存构建日期——它们的 schema 接口里就没有generatedAt属性。此时返回体里没有generatedAt键。源码注释解释了为什么不返回nullnull会被读作「日期未知」而事实是「从未存过」两种状态的运维含义不同。有属性但为 NULL 时输出nullcache.generatedAt ?? null对应迁移 163 之前的历史行。也就是说generatedAt在响应中有三种状态具体 ISO 时间新建/更新的行、null历史行日期未知、键不存在该缓存类型从不存储。对照系referrals 缓存与 replay 防护changelog 末句提到的「referrals 缓存自迁移 142 起就持久化同一日期」在源码中有完整证据链radar_referrals_cache 迁移 建表即含generated_atgetRadarReferralsCache() 直接选出该列referrals feed 的 schema 同样把generatedAt定义为必填 datetimereferralsFeedSchema.tsreferralsSync.ts 更进一步把generatedAt用作重放防护的下限步骤 7 明确「reject an incominggeneratedAtolder than the cache」相同时间戳放行同一generatedAt下的不同权益变体有意共享确定性的构建日期。这与目录 feed 的版本下限形成对照目录 feed 的 stale 判定比较的是version而非任一日期RADAR 文档 亦专门强调 “The version floor above comparesversion, not either date”。三者各司其职version管新旧门禁generatedAt管数据年龄与重放拒绝fetchedAt管本地下载时刻。已知边界与后续工作修复是克制的changelog 与 RADAR 文档 都明确记录了两个刻意留下的缺口仪表盘仍只显示Last fetched。要把构建日期展示出来需要新增一个翻译标签覆盖全部 i18n locale被留作后续工作generatedAt目前可通过getRadarCatalog().meta与GET /api/radar/status程序化获取。offers 与 intel 缓存不存构建日期尽管它们的 feed schema 携带该字段——状态接口因此对这两类省略generatedAt字段而非上报一个会被误读为「未知」的null。另一个适用前提历史行的null行为只存在于升级前已缓存的实例新写入的缓存行只要 feed 通过了 schema 校验generatedAt必有值。关键文件索引内容路径修复 changelogchangelog.d/fixes/11435-radar-feed-cache-generated-at.md迁移 163新增generated_at列src/lib/db/migrations/163_radar_feed_cache_generated_at.sql迁移 142referrals 先例src/lib/db/migrations/142_radar_referrals_cache.sqlDB 访问层RadarCache双日期读写src/lib/db/radar.tsgetRadarCatalog()与meta类型src/lib/radar/index.ts同步路径Step 9 写缓存含generatedAtsrc/lib/radar/sync.tsfeed 顶层 schemageneratedAt必填src/lib/radar/feedSchema.tsreferrals 重放下限src/lib/radar/referralsSync.ts状态端点cacheStatus()src/app/api/radar/status/route.ts「Two dates, and why both are kept」章节docs/frameworks/RADAR.md小结这次修复的价值不在于多存了一列而在于让三个互不替代的时间语义各归其位generatedAt签名 body 内的数据构建时间可防篡改回答「数据多旧」fetchedAt本实例时钟回答「何时下载」version回答「门禁是否放行」。修复链路完整贯穿 DDL迁移 163、DB 访问层、getRadarCatalog().meta与GET /api/radar/status并对三种日期状态已知 / 未知 / 从未存储给出了互不混淆的上报方式——历史行读回null、offers/intel 直接省略键。对维护多 feed 缓存的开发者而言「未知 ≠ 从未存储」「未知不得借用相邻字段冒充」这两条纪律是比单列迁移本身更值得借鉴的取舍。【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150 free), 1200 models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline Copilot. Quota-aware auto-fallback, RTKCaveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550 contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表