news 2026/9/28 7:04:00

gsd-core 可选更新横幅(Update Banner Opt-In)机制:为不使用 statusline 的用户提供 `SessionStart` 更新提醒

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gsd-core 可选更新横幅(Update Banner Opt-In)机制:为不使用 statusline 的用户提供 `SessionStart` 更新提醒

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

导读

本文围绕 gsd-core 的「可选更新横幅(update banner opt-in)」特性展开,讲解 GSD 安装器如何为拒绝或未安装 GSD statusline 的用户提供一个可选的SessionStart钩子,使其在会话启动时借助已有的~/.cache/gsd/gsd-update-check.json缓存静默地获知新版本可用。读完本文,你将掌握该特性的触发条件、缓存数据契约、横幅输出与限流逻辑、以及与gsd-statusline.js的职责划分,并能基于源码定位对应实现与测试,理解其「优雅降级、绝不打断会话启动」的设计原则。

该特性由 hooks/gsd-update-banner.js 实现,对应的变更记录见 .changeset/archived/update-banner-opt-in.md。

一、特性背景:为什么需要「可选」更新横幅

GSD(Git. Ship. Done)在安装时会注册一组托管钩子(managed hooks),其中 hooks/gsd-statusline.js 负责在会话中渲染状态栏(statusline),并在其中内嵌更新提示(⬆ /gsd:update与陈旧钩子警告,见该文件中约第 930 行起对缓存的读取与evaluateUpdateCache的调用)。也就是说,安装了 statusline 的用户不需要额外的更新提醒机制——状态栏本身就会消费更新缓存。

但有一类用户会在安装器询问时拒绝安装或保留非 GSD 的 statusline。对这类用户而言,更新信息没有呈现入口。于是本特性提供了一条**可选的(opt-in)**补充通道:当安装器检测到用户拒绝/保留非 GSD statusline 时,会额外注册一个SessionStart横幅钩子,通过systemMessage信封在会话启动时呈现更新可用信息。

从 hooks/gsd-update-banner.js 头部注释可以确认三条关键设计约束:

  1. 注册即 Opt-in:bin/install.js仅在用户拒绝安装(或替换)GSD statusline 时注册该钩子,「SessionStart条目的存在本身就是 opt-in」,不存在独立的运行时开关;
  2. 复用已有缓存:横幅不自己联网检查,而是读取gsd-check-update-worker.js写入的~/.cache/gsd/下缓存文件(per-package 命名);
  3. 以 issue #2795 为设计依据,保证更新提示在多种运行时(Claude、OpenCode、Kilo、Gemini 等)下的一致呈现。

二、数据来源:更新检查缓存(update-check cache)契约

横幅本身是「只读消费者」,真正的更新探测由后台 worker 完成。整条链路为:

SessionStart 主钩子→ 派生子进程 worker → 写缓存文件 →gsd-update-banner.js(或 statusline)读取

2.1 后台检查:gsd-check-update.js

hooks/gsd-check-update.js 是标准SessionStart钩子(每会话运行一次),其职责:

  • 通过detectConfigDir探测运行时配置目录,依次支持CLAUDE_CONFIG_DIR环境变量覆盖,以及.claude、.gemini、.config/kilo、.kilo、.config/opencode、.opencode等目录;
  • 计算项目级/全局级VERSION文件路径(gsd-core/VERSION,项目优先、全局兜底);
  • 统一将缓存目录收敛到~/.cache/gsd/(os.homedir() + '.cache/gsd'),该目录在写入前用fs.mkdirSync(..., { recursive: true })确保存在;
  • 通过spawn(process.execPath, [workerPath], { stdio: 'ignore', detached: true, windowsHide: true })派生出独立的后台 worker,并通过环境变量GSD_CACHE_FILE、GSD_PROJECT_VERSION_FILE、GSD_GLOBAL_VERSION_FILE传递路径,最后child.unref()分离子进程。

之所以拆成独立 worker 文件而非node -e '<inline code>',源码注释明确说明是为了避免模板字符串正则转义问题,并让 worker 可以独立测试。

2.2 写入端:gsd-check-update-worker.js

hooks/gsd-check-update-worker.js 在后台完成版本比对并写缓存,其产出结果对象为:

const result = { update_available: latest && isSemverNewer(latest, installed), installed, latest: latest || 'unknown', checked: Math.floor(Date.now() / 1000), stale_hooks: staleHooks.length > 0 ? staleHooks : undefined, package_name: PACKAGE_NAME, };

各字段含义:

字段说明
update_available布尔值,latest存在且isSemverNewer(latest, installed)为真时为true
installed当前安装版本,从项目级/全局级VERSION文件读取(项目优先),缺省为'0.0.0'
latest通过checkLatestVersion()查询到的最新版本;非ok结果下为'unknown'
checked检查发生的 Unix 时间戳(秒)
stale_hooks陈旧托管钩子数组(比对每个钩子文件头部的gsd-hook-version注释与已安装版本),无则为undefined
package_name包身份标识,来自包身份缝(Package Identity seam),用于下游的「血统守卫」

值得注意的实现细节:

  • 版本来源:worker 依次读取项目级与全局级VERSION文件;项目目录优先(本地安装优先于全局安装)。
  • 陈旧钩子检测:仅扫描MANAGED_HOOKS清单(来自 hooks/managed-hooks-registry.cjs)中列出的文件;被移除特性的孤儿文件(如gsd-intel-*.js)不会列入,以免产生永久性陈旧警告(#1750)。gsd-update-banner.js正是该清单中的第 42 项。
  • 原子写入:为避免与并发读取者(statusline、横幅、多个运行时 worker)发生撕裂读,worker 采用「同目录临时文件 +renameSync」的原子发布策略(#4091):先写cacheFile + '.tmp-' + process.pid,再renameSync到位,失败时尽力清理临时文件。
  • 血统字段:package_name由打包的 Package Identity 缝提供(而不是从../package.json向上查找,后者在已安装树中解析不到名字,#2544/#378)。

2.3 缓存文件命名

缓存文件名由包身份决定。updateCacheFileName由package-identity.cjs提供;从 tests/gsd-statusline.test.cjs(第 2343–2345 行)可以确认:

  • PACKAGE_NAME为@opengsd/gsd-core;
  • updateCacheFileName为gsd-update-check-opengsd-gsd-core.json(per-package 命名,避免多运行时/多包冲突)。

也就是说,横幅实际读取的路径为~/.cache/gsd/gsd-update-check-opengsd-gsd-core.json,与变更记录中的~/.cache/gsd/gsd-update-check.json是「通用回退名 + 包专属名」的关系——当运行库缺失时退回到通用文件名(见下文降级设计)。

三、横幅实现:gsd-update-banner.js 的核心逻辑

hooks/gsd-update-banner.js 是可选的SessionStart钩子,其运行流程可概括为三个纯函数 + 一个主入口:

  1. readCache(cacheFile)—— 读取并解析缓存;
  2. buildBannerOutput(state)—— 依据缓存状态决策「输出 or 静默」;
  3. shouldSuppressFailureWarning(sentinelFile, nowSeconds)/recordFailureWarning(...)—— 失败诊断的 24h 限流;
  4. main()—— 组装以上逻辑,向stdout输出 JSON 信封。

3.1 输出契约:systemMessage信封

横幅的输出格式与 statusline 的更新段保持一致的 JSON 信封:

{ "systemMessage": "GSD update available: 1.2.0 → 1.3.0. Run /gsd:update." }

其中版本号取自缓存的installed/latest(缺失时显示'unknown')。buildBannerOutput是纯函数(无 I/O),返回null表示「本会话静默、什么都不输出」。

3.2 决策矩阵

buildBannerOutput(state)的判定顺序如下:

缓存状态结果
缓存文件存在但JSON.parse失败(parseError = true)且未在限流窗口内输出{ systemMessage: 'GSD update check failed.' }
同上但处于限流窗口(suppressFailureWarning = true)返回null(静默)
缓存缺失/不可读(cache = null)返回null(静默)
缓存存在但package_name缺失或与PACKAGE_NAME不一致(血统守卫失败)返回null(视为不可信缓存)
update_available不为真返回null(静默)
以上均通过输出更新可用横幅

要点解读:

  • 更新可用时才有横幅:update_available: false时完全静默,符合「更新已是最新时不打扰」的定位;
  • 血统守卫(lineage guard):package_name必须存在且与当前包一致(!cache.package_name || cache.package_name !== PACKAGE_NAME即拒绝)。缺失package_name的缓存被视为「早于血统追踪的旧记录」,一律不可信——该逻辑与 statusline 中evaluateUpdateCache的守卫完全一致;
  • 解析失败限流:只有「文件存在但 JSON 损坏」这一类错误才会触发一次性诊断横幅,且同一错误 24 小时内最多提示一次(RATE_LIMIT_SECONDS = 24 * 60 * 60),避免坏缓存文件在每个会话都打扰用户。

3.3 失败限流的实现

限流通过哨兵文件~/.cache/gsd/banner-failure-warned-at实现:

  • shouldSuppressFailureWarning(sentinelFile, nowSeconds):读取哨兵文件中的时间戳,若nowSeconds - last < RATE_LIMIT_SECONDS则返回true;文件缺失或内容非有限数字时返回false(放行提示);
  • 当parseError且未限流时,main()先以recursive: true确保缓存目录存在(覆盖「目录被清空」的首次运行场景),再写入当前时间戳(recordFailureWarning,尽力而为,失败仅意味着下个会话重新提示)。

四、优雅降级:构建失败绝不打断会话启动

横幅是「可选」的SessionStart钩子,因此它对失败的态度是降级而非崩溃(#3582)。具体表现为:

  • gsd-update-banner.js顶部对运行库采用try/require/ensureRuntimeBuild/require/catch形状:gsd-core/bin/lib/package-identity.cjs是 tsc 构建产物(ADR-457),在未执行npm run build:lib的裸插件市场/git-clone 安装中不存在;此时PACKAGE_NAME保持null,updateCacheFileName保持通用回退名'gsd-update-check.json';
  • 由于PACKAGE_NAME为null,buildBannerOutput的血统守卫!cache.package_name || cache.package_name !== PACKAGE_NAME恒为真,缓存永远被判定为不可信,main()自然落入「什么都不打印」的静默路径——无需单独的分支代码;
  • 读取、限流、写哨兵等环节均有 try/catch 兜底,任何 I/O 失败都只影响「本会话是否提示」,不会让会话启动失败。

同样的降级形状也独立存在于gsd-check-update.js与gsd-check-update-worker.js中。源码注释说明这一重复是刻意的:脚本 scripts/lint-hooks-runtime-build-seam.cjs 按文件逐一要求ensure-runtime-build.cjs的require/调用与编译产物require字面量同时出现在该文件自身中,提取公共 helper 会让字面量 require 移出文件,破坏该 lint 的文本扫描。

五、与 statusline 的职责划分

同一份缓存有两个消费端,判定逻辑保持一致:

维度gsd-statusline.jsgsd-update-banner.js
消费入口状态栏渲染(会话持续可见)可选的SessionStart一次性横幅
决策函数evaluateUpdateCache(cache)buildBannerOutput(state)
血统守卫package_name缺失或与PACKAGE_NAME不一致 → 视为无缓存同上,返回null
更新提示状态栏内嵌⬆ /gsd:update与⚠ stale hooks段systemMessage: 'GSD update available: X → Y. Run /gsd:update.'
使用前提已安装 GSD statusline用户拒绝/保留非 GSD statusline(由安装器注册)

两者共享同一个 per-package 缓存文件(~/.cache/gsd/gsd-update-check-opengsd-gsd-core.json),因此不会出现「检查写入一个位置、读取另一个位置」的多运行时错位问题(#607/#1421)。

六、测试验证与源码佐证

仓库对buildBannerOutput的行为有直接的单测覆盖,位于 tests/gsd-statusline.test.cjs(第 2361–2409 行「buildBannerOutput lineage guard」分组):

  • 外来血统被拒绝:缓存package_name为'get-shit-done-cc'时返回null(foreign lineage must be rejected);
  • 同源血统输出横幅:package_name为'@opengsd/gsd-core'且update_available: true时返回信封,且systemMessage同时包含installed、latest与/gsd:update;
  • 缺失血统视为不可信:缓存无package_name字段时返回null。

同一测试文件中还断言了包身份常量:PACKAGE_NAME === '@opengsd/gsd-core'、updateCacheFileName === 'gsd-update-check-opengsd-gsd-core.json'。此外,hooks/managed-hooks-registry.cjs 将gsd-update-banner.js列入托管钩子清单(第 42 项),worker 会将其纳入陈旧钩子扫描,保证升级后横幅脚本自身也能被识别为待更新对象。

七、适用前提与使用说明

本特性是可选注册的,其生效链路为:

  1. 运行安装器(如npx get-shit-done-cc等)时,在 statusline 询问处选择拒绝安装/保留非 GSD statusline;
  2. 安装器因此注册gsd-update-banner.js为SessionStart钩子(SessionStart条目的存在即 opt-in);
  3. 每会话由gsd-check-update.js派生子进程 worker 检查更新并写缓存;
  4. 横幅钩子读取缓存,仅在「有更新」或「缓存损坏且 24h 未提示过」时输出systemMessage信封,其余情况静默;
  5. 卸载时通过npx get-shit-done-cc --uninstall干净移除,该钩子随托管钩子一并清理。

需要说明的边界:

  • 若已安装 GSD statusline,则无需本横幅——状态栏已内嵌更新提示,横幅不会重复注册;
  • 横幅不主动联网,只消费 worker 写入的缓存;缓存缺失、损坏、血统不符或已是最新版本时均静默;
  • 源码注释强调「SessionStart条目的存在本身就是 opt-in,没有单独的运行时开关」,因此不要试图通过环境变量或配置文件单独开关横幅——其开关完全由安装时的选择决定;
  • 本文所描述的文件路径(~/.cache/gsd/、哨兵文件、缓存文件名)以当前仓库实现为准,若想深入阅读,可继续查看 hooks/gsd-update-banner.js、hooks/gsd-check-update.js、hooks/gsd-check-update-worker.js 与 tests/gsd-statusline.test.cjs。

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载
上一篇:DeepChat窗口状态持久化:electron-window-state实现原理
下一篇:GitHub_Trending/re/resources-learning-spring消息队列:Spring集成消息中间件资源

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 7:03:36

IEC104转SNMP协议转换实战:从总召到时序到Trap防风暴

做电力自动化和动环监控的朋友&#xff0c;对IEC104协议应该不陌生——站内的RTU、DCS、保护装置&#xff0c;基本都靠它往主站送遥测遥信&#xff0c;也靠它接收调度端的遥控命令。可一旦这套系统要往“网管侧”对接&#xff0c;问题就来了&#xff1a;网管平台看的是SNMP&…

作者头像 李华
网站建设 2026/9/28 7:03:28

建网站可以赚钱吗源码下载

建网站能赚多少钱?别被模板坑了,真实成本与变现逻辑 看着那些花里胡哨的模板站,你是不是也觉得丑得掉渣,根本撑不起专业形象?很多人一上来就问“建网站多少钱”,结果被坑得底裤都不剩,最后发现根本赚不到钱。其实,建网站本身不是目的,通过网站搞流量、接广告、卖服务才是真金白银的源头。…

作者头像 李华
网站建设 2026/9/28 7:03:25

商城微信网站开发避坑速查手册备案证书全流程拆解

商城微信网站开发避坑速查手册备案证书全流程拆解 刚接到一个急单,客户做微信商城网站,上线前发现SSL证书过期了三天,HTTPS访问全是警告,流量直接腰斩。他当时那个懵圈劲儿,问我:“老师,这备案流程我是一头雾水,证书到底咋搞?会不会又卡壳?” 这种场景太常见了。很多老板觉得网站做好了就行,没意识到…

作者头像 李华
网站建设 2026/9/28 7:03:17

如何做视频网站首页注意事项

手把手教你做视频网站首页图解步骤避坑 域名买好了吗?服务器租下了吗?很多老板卡在这一步,看着后台那一堆术语,脑子里全是浆糊。别慌,今天咱们不聊虚的,直接上 图解步骤…

作者头像 李华
网站建设 2026/9/28 7:02:44

3个免费工具搞定网站模板资源,拒绝被建站公司拖一周

3个免费工具搞定网站模板资源,拒绝被建站公司拖一周 改个导航栏颜色,建站公司说要排期一周? 看着后台改个文案,报价单又飘来“定制开发费”? 别忍了,其实搞定【网站模板资源】根本不用求着甲方,手里攥着几套靠谱的【免费工具】,你自己就能把主动权拿回来。…

作者头像 李华
网站建设 2026/9/28 7:02:42

网站负责人查询避坑指南:从零搭建信任体系

网站负责人查询避坑指南:从零搭建信任体系 改个需求建站公司拖一周,你找谁?合同里写的“项目经理”换了三茬,电话打不通,微信不回。这时候,你才意识到手里连个能真正拍板的人都没有。很多老板以为建个站就是找个技术写代码,其实从 从零搭建…

作者头像 李华