拓冰建站拓冰建站
首页 / 资讯中心 / 正文

gstack hackernews-frontpage 参考浏览器技能:把 HN 首页抓取变成确定性 JSON 脚本

gstack hackernews-frontpage 参考浏览器技能把 HN 首页抓取变成确定性 JSON 脚本【免费下载链接】gstackUse Garry Tans exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstackhackernews-frontpage是 gstack 内置的参考级 browser-skill它把“抓取 Hacker News 首页”这一重复浏览器流程固化成一个确定性的 Bun/TypeScript 脚本通过本地 Chromium 守护进程驱动页面最终在标准输出上打印一份包含前 30 条故事的 JSON。读完本文你将掌握该技能的完整目录结构、$B skill run的输出协议、parseStoriesFromHtml解析器的逐段实现含招聘帖、discuss链接等边界情况以及它背后的三层存储、scoped token 信任模型与 fixture 回放测试体系。技能定位与目录结构gstack 的 browser-skills 是一类“每任务一个目录”的组件把重复的浏览器流程固化为确定性脚本一次编写之后每次调用以约 200ms 的开销直接出 JSON而不必让 Agent 再花约 30 秒用$B原语重新探索页面。完整设计脉络见 docs/designs/BROWSER_SKILLS_V1.md运行时命令与信任模型的总览见 BROWSER.md 的 “Browser-skills runtime” 一节。hackernews-frontpage是该机制随安装分发的捆绑参考技能bundled reference其目录结构正是所有 browser-skill 的标准解剖browser-skills/hackernews-frontpage/ ├── SKILL.md # frontmatter 散文契约 ├── script.ts # 确定性解析 抓取入口 ├── _lib/browse-client.ts # vendored SDK 副本与正式版逐字节一致 ├── fixtures/hn-2026-04-26.html # 抓取的页面快照供 fixture 回放测试 └── script.test.ts # 针对 fixture 的解析器测试无需守护进程对应文件SKILL.md、script.ts、script.test.ts、fixtures/hn-2026-04-26.html、_lib/browse-client.ts。SKILL.md 的 frontmatter 契约SKILL.md 的 YAML frontmatter 是整个技能的唯一事实来源设计决策 #7无meta.jsonfrontmatter 即元数据。以 HN 技能为例各字段含义如下字段取值作用namehackernews-frontpage技能唯一名$B skill run name的入参descriptionScrape the Hacker News front page (titles, points, comment counts).供/scrape意图匹配与$B skill list展示hostnews.ycombinator.com目标站点Phase 3 的 resolver 注入按 host 过滤技能trustedtrue进程侧环境透传开关见“信任模型”一节sourcehuman技能作者来源人类手写的参考技能区别于 Phase 2 由 Agent 生成version1.0.0技能自身版本args[]该技能不接受任何--arg kv参数triggersscrape hacker news frontpage、get hn top stories等 4 条/scrape intent的语义匹配词表Agent 运行$B skill list后用意图与triggersdescriptionhost做匹配置信度判断命中即走$B skill run否则回退到原型路径使用方式与输出协议技能文档给出的调用方式是$ $B skill run hackernews-frontpage { stories: [ { rank: 1, title: ..., url: ..., points: 412, comments: 87 }, ... ], count: 30 }围绕 browser-skills 运行时的完整子命令面为$B skill list # 列出三层中所有技能并标注解析到的层bundled $B skill show hackernews-frontpage # 打印 SKILL.md $B skill run hackernews-frontpage # 带 per-spawn scoped token 生成脚本进程~200ms 返回 30 条故事 JSON $B skill test hackernews-frontpage # 运行 script.test.ts 对 fixture 回放测试 $B skill rm name [--global] # 用户层技能打墓碑bundled 层只读不可删输出协议是跨所有 browser-skill 的统一契约设计决策 #9与gh/kubectl/docker的惯例一致stdout成功时只输出单个 JSON 文档本技能即{ stories: Story[], count }stderr流式日志本技能当前不写任何日志退出码0 成功解析或网络失败时非零超时默认 60 秒可用--timeoutNs覆盖stdout 上限1MB超出则截断并以非零退出。从 script.ts 的入口实现可以确认这一协议的落地点脚本只在import.meta.main为真即作为脚本被生成运行、而非被测试导入时执行main()通过browse.goto(https://news.ycombinator.com/)与browse.html()取回页面最后process.stdout.write(JSON.stringify(output) \n)script.ts 入口。一个值得注意的细节SKILL.md 示例中省略了id字段但源码导出的Story接口实际包含 6 个字段——export interface Story { rank: number; // HN 页面上展示的 1-based 排名 id: string; // HN item idtr.athing 的 id 属性中的整数 title: string; url: string; // 标题链接指向的外链 URL points: number | null; // 招聘帖无分数 → null comments: number | null; // 招聘帖无评论链接 → null }见 Story 接口定义。消费端解析时应当把points/comments当作可空值处理。解析器实现parseStoriesFromHtml逐段拆解技能文档的 “How it works” 概括为四步经守护进程导航到https://news.ycombinator.com→ 读取页面 HTML → 把每行故事HN 稳定的tr.athing结构解析为类型化的Story记录 → 输出单一 JSON。真正承担第 3 步的是导出的纯函数parseStoriesFromHtml它对 HTML 字符串做正则解析不依赖任何 DOM 库。HN 首页的稳定 HTML 骨架解析器依赖 HN 的“双行制”结构每条故事由一对tr组成——tr classathing submission iditemid td classrankN./td td classtitlespan classtitlelinea hrefurltitle/a .../span/td /tr trtd colspan2/tdtd classsubtextspan classsubline span classscore idscore_itemidN points/span ... a hrefitem?iditemidN comments/a /span/td/tr第 1 步逐行定位tr.athing行级匹配用全局正则/tr\s[^]*\bclassathing[^]*[^]*\bid(\d)[^]*([\s\S]*?)\/tr/g捕获id属性与行体while ((match rowRegex.exec(html)) ! null)循环中用自增计数器rank生成 1-based 排名script.ts 行定位。排名不取自td classrank文本而是由文档顺序推导——这与测试断言“按文档顺序分配 1..N”的行为一致。第 2 步提取标题与外链行体内再用/span\sclasstitleline[^]*\s*a\shref([^])[^]*([\s\S]*?)\/a/取href与标题文本随后经过两个纯函数清理decodeHtmlEntities还原amp;、quot;、#x27;、#39;、lt;、gt;、nbsp;实体解码stripTags正则/[^]*/g剥掉标题内嵌标签。若某行匹配到athing却没有titleline直接continue跳过——测试用例 “does not fabricate stories from arbitrary tr.athing rows missing titleline” 专门钉死这一行为防止伪造条目。第 3 步subtext 边界切割防分数泄漏分数与评论数位于下一行subtext中。源码在这里显式处理了一个 bug 模式如果直接向后无界搜索span classscore当故事 N 是招聘帖自身没有分数时会把故事 N1 的分数泄漏进故事 N 的记录。解法是把搜索窗口限制在“当前行结束 → 下一个边界”之间边界取tr classspacer与下一个tr classathing两者中更早出现的位置边界切割逻辑。窗口内再分别匹配const scoreMatch subtextSlice.match(/span\sclassscore[^]*(\d)\s*points?\/span/); const commentsMatch subtextSlice.match( /a\shrefitem\?id\d[^]*(\d)\s*(?:nbsp;)?\s*comments?\/a/);命中span classscoreN points/span→points N单复数points?都兼容评论数只认hrefitem?id数字锚点内的 “N comments”天然跳过hide/from/context等干扰链接若没有评论锚点但存在discuss/a则记为comments 0两者都未命中招聘帖如 fixture 中的 “Acme (YC W26) is hiring...” 行→points与comments保持null。边界与空输入行为parseStoriesFromHtml()与“无故事行的 HTML”都返回[]任意缺少titleline的tr.athing不会产生伪造条目。这些都是 script.test.ts 中被显式断言的行为。vendored SDK_lib/browse-client.ts如何连接守护进程script.ts的头部注释说明了两条分发原则输出协议stdout 单 JSON / stderr 日志 / 退出码以及“parseStoriesFromHtml被导出让script.test.ts不必启动守护进程即可对 HTML fixture 做测试”。script.ts只依赖一个导入import { browse } from ./_lib/browse-client。这个_lib/browse-client.ts是规范 SDK browse/src/browse-client.ts 的逐字节 vendored 副本约 3KB。设计决策 #4Option E解释了为什么每个技能各带一份而不是共享全局 SDK技能目录完全可移植拷到任何地方都能运行、SDK 版本在技能编写时冻结不可能发生版本漂移、无需 npm 发布流程。SDK 侧的关键机制在 vendored 副本中同样可查鉴权两级解析resolveBrowseAuth首选环境变量GSTACK_PORTGSTACK_SKILL_TOKEN——由$B skill run在生成脚本进程时注入这是一个绑定 readwrite 范围的 per-spawn scoped tokenspawn 结束即吊销回退到状态文件BROWSE_STATE_FILE环境变量或git-root/.gstack/browse.json守护进程启动时写入、chmod 600 的 port root token供开发者直接bun run script.ts调试时使用两条路都失败则抛出包含明确操作指引的错误信息。命令通道所有便捷方法goto、html、text、click等最终都走command(cmd, args)向http://127.0.0.1:port/command发带Authorization: Bearer token的 POST默认超时 30000ms非 2xx 抛BrowseClientErrorcommand 分发。懒加载单例导出的browse是LazyBrowseClient首次方法调用时才解析鉴权因此测试或普通脚本导入它不会产生任何副作用。注意“无新能力”的信任边界SDK 只暴露守护进程既有的 HTTP 表面POST /command真正能执行哪些命令由 token 的 scope 决定——技能即使调用eval这类 admin 命令也会被守护进程 403。信任模型scoped token 与trusted: truebrowser-skills 的信任模型是两条正交轴设计文档 “Trust model” 一节与 BROWSER.md 的 “Trust model” 表格一致轴机制默认守护进程侧能力per-spawn scoped token绑定 readwrite 范围浏览器驱动命令中剔除eval、js、cookies、storage等 admin 命令单用 clientId 编码技能名 spawn idspawn 退出即吊销永远 scoped绝不下发守护进程 root token进程侧环境变量trusted: true时透传process.env剔除GSTACK_TOKENtrusted: false默认只保留最小白名单LANG、LC_ALL、TERM、TZ 及锁定 PATH并按模式剥离密钥类键TOKEN/KEY/SECRET/PASSWORD、AWS_、ANTHROPIC_、OPENAI_、GITHUB_等非受信需显式 opt-inGSTACK_PORT与GSTACK_SKILL_TOKEN由生成方最后注入父进程无法通过预置同名环境变量劫持它们可对照 browse/src/browser-skill-commands.ts 中 env 组装处的注释与out.GSTACK_PORT .../out.GSTACK_SKILL_TOKEN ...赋值。hackernews-frontpage的 frontmatter 显式声明trusted: true因为它由人类编写、随安装分发需要透传常规环境如代理变量而默认路径下第三方技能的环境会被严格收窄。需要强调设计文档中的诚实边界环境剥离是“卫生措施而非沙箱”——真正的能力边界在守护进程侧的 scoped tokenOS 级文件系统沙箱属于 Phase 4 的未竟工作。Fixture 回放测试为什么它是“参考技能”SKILL.md 专门用一节回答 “Why this is the reference skill”没有鉴权、HTML 稳定、输出确定、适合 fixture 测试Phase 1 的全部组件SDK、scoped token、三层查找、spawn 生命周期都被$B skill run hackernews-frontpage和随附的script.test.ts实际演练。“当 HN 的 HTML 改版、选择器失效时测试会先在抓取的 fixture 上失败早于用户察觉——这正是设计意图。”这个说法在仓库中得到逐条印证fixturefixtures/hn-2026-04-26.html 是一份 5 条故事的完整首页快照刻意覆盖了全部解析分支——故事 1/2常规条目412/87、298/152故事 3招聘帖 “Acme (YC W26) is hiring senior engineers (remote)”subtext 行无 score、无评论锚点 →points: null, comments: null故事 4Ask HN 标题含#x27;实体、subtext 只有discuss/a无 “N comments” →comments: 0故事 5URL 含quantumamp;chess→ 实体解码断言https://example.io/quantumchess。测试script.test.ts 共 13 个断言直接读取 fixture 字符串调用纯函数parseStoriesFromHtml不启动守护进程、不触网测试验证点returns 5 stories条目数与 fixture 一致assigns 1-based ranks in document orderrank 序列为[1,2,3,4,5]extracts ids matching the tr.athing[id] attributeid 序列为40000001..40000005extracts titles and decodes HTML entitiesWhats your most underrated tool?等实体还原extracts URLs and decodes ampersandsamp;→parses point counts / comment counts as numbers数值解析treats discuss links as 0 commentsdiscuss 分支returns null points null comments for job postings招聘帖分支returns [] for empty HTML/no story rows空输入不抛错does not fabricate stories ... missing titleline拒绝伪造条目output shape: every story has all required keys类型契约points/comments 可为 null这套“导出纯函数 捆绑 fixture 无守护进程测试”的两段式模式SDK 单测用 mock HTTP 服务器端到端技能测试用 fixture 回放正是设计决策 #10 为两种测试类型预留的轨道也是后续用/skillify生成新技能时的验收门槛——$B skill test name对 fixture 跑绿才允许技能原子性地落入最终层路径。三层存储与解析优先级$B skill list按优先级走查三层首个命中生效并在列表中内联打印解析到的层避免“为什么跑的是这一份”成为调试谜团层路径定位Projectproject/.gstack/browser-skills/name/项目专属可提交或 gitignoreGlobal~/.gstack/browser-skills/name/用户级跨项目/skillify默认层Bundledgstack-install/browser-skills/name/随 gstack 安装只读分发hackernews-frontpage属于 Bundled 层本仓库的 browser-skills/hackernews-frontpage/ 即其安装目录形态因此不可被skill rm墓碑化但同名 Project/Global 技能会遮蔽它。查找不依赖任何 INDEX.json$B skill list直接枚举三层并解析各 SKILL.md 的 frontmatter设计决策 #850 个技能约 5-10ms从根上消除“索引与磁盘漂移”这一类 bug。实操要点当 HN 改版之后基于以上源码与测试结构维护或仿写该技能时的可操作路径是验证现状运行$B skill test hackernews-frontpage——解析器对 fixture 的回归是纯本地操作秒级完成再$B skill run hackernews-frontpage走一遍真实守护进程链路生成 scoped token → spawn 脚本 → JSON 上屏。选择器失效时症状是run输出count: 0或字段错乱。按 script.ts 头部注释中记录的tr.athing/titleline/subtext契约对照新版首页 HTML 修正行定位与 subtext 边界正则然后同步替换 fixture 并补齐script.test.ts断言确保“测试先于用户失败”的哨兵作用不被削弱。仿写新站点技能复用五文件解剖SKILL.md frontmatter、script.ts 导出纯解析函数、vendored_lib/browse-client.ts、fixtures/host-date.html、script.test.ts保持输出协议stdout 单 JSON / 退出码 / 默认 60s 超时与“招聘帖类无分数行 → null”的防御性字段设计trusted默认留false除非有明确理由透传环境。小结hackernews-frontpage以最小的体量承载了 gstack browser-skills 的完整契约面frontmatter 单一事实来源、stdout-JSON 输出协议、per-spawn scoped token 与trusted环境透传双轴信任模型、逐字节 vendored 的browse-clientSDK以及“fixture 回放 纯函数导出”的离线测试范式。它的价值不在于抓了 30 条新闻而在于为/scrape/skillify流水线提供了可直接对照、可直接回归的基准实现——理解了它就理解了 gstack 如何把浏览器流程从“Agent 每次重新探索”压成“一次编写、200ms 复现”的确定性代码。【免费下载链接】gstackUse Garry Tans exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门