juejin-skills 系列 ②:我把「看掘金热榜」也做成了 Agent 技能

juejin-skills 系列 ②:看掘金热榜

本系列 ① 我把「自动发文章到掘金」做成了一个 Agent 技能:juejin-skills ② 本篇:给技能包加上「掘金热榜」(v1.4.0) ③ 预告:F3 掘金沸点发布
🔗 仓库地址:github.com/liuleihang/... (MIT,Node ≥ 18,发布脚本零依赖)

🔗 workbuddy链接:www.workbuddy.cn/share/asset...

上一篇发出去时,文末留了一张 F1--F6 的待办表,F2「掘金热榜」标着 📋 规划中。

这篇把它划掉。v1.4.0 里,你在 Agent 里说一句「看看今天掘金前端热榜」,它就会返回排名、标题、作者、热度、浏览 / 点赞 / 收藏 / 评论和文章链接------全程不需要登录。

一、为什么先做热榜

先看一眼这两个功能的复杂度差:

F1 文章发布 F2 掘金热榜
鉴权 需要 Cookie,登录态会过期 公开接口,无需登录
链路 会话体检 → 标签缓存 → 图片上传 → 建草稿 → 二次确认发布 一次 GET → 字段映射 → 输出
风险 对外写操作,误发不可逆 只读,无副作用

热榜是整张待办表里唯一不需要登录的能力,也是唯一没有危险动作的能力。

这里有个常被忽略的判断:这个需求真的需要 Agent 吗? 不需要。一次 HTTP GET、一次字段映射、一次格式化输出,是标准的确定性固定工作流------没有决策分支,不需要自主循环,不需要记忆,更不需要停止条件。给这种任务套一个"自主 Agent",只会凭空引入不确定性。

所以实现上只加了一个 CLI 脚本 scripts/hot.mjs,SKILL.md 里补充触发词和命令说明就够了。Agent 在这里的角色是把自然语言翻译成一条命令,不是替你做决策。

能被一次调用解决的问题,别包装成 Agent 的自主决策。技能包不是越"智能"越好,越可预测越好。

二、接口不能凭记忆硬编

掘金没有官方公开 API 文档,网上能搜到的端点与字段、包括各种博客里的示例,都可能已经过期。第一步不是写代码,是核实:

  1. 多个独立来源交叉比对,找出候选端点;
  2. 直接发一次请求,把返回结构逐字段看清楚,再决定映射;
  3. 把核实结果(含日期)写进文档,而不是记在脑子里。

最终确认的是:

bash 复制代码
GET https://api.juejin.cn/content_api/v1/content/article_rank
    ?category_id={id}&type=hot&aid=2608&spider=0
项 实测结果(2026-11-11)
鉴权 不传 Cookie 返回 err_no=0,公开可读
返回 {err_no:0, err_msg:"success", data:[...]},综合榜 50 条
字段 author.name、content.{content_id,title}、content_counter.{hot_rank,view,like,collect,comment_count}
链接 https://juejin.cn/post/{content_id}
分类 query_category_briefs 返回 8 类(后端 / 前端 / Android / iOS / 人工智能 / 开发工具 / 代码人生 / 阅读)
综合榜 category_id=1 是榜单约定值,并不在那 8 个分类里
分类筛选 实测生效(前端 id 返回前端文章 20 条)

两个容易想当然的地方:author_counter 里的计数全是 0,没有使用价值;综合榜的 1 不能从分类列表里查出来,得单独做别名映射。

三、先把规格里那句「摘要」删掉

原来的 SPEC 里,F2 的输出列是这样写的:

文章列表:标题、作者、点赞 / 浏览数、评论数、文章链接、摘要

实测接口一看:根本没有摘要字段。

于是 SPEC 的输出列改成了实际存在的字段,并在文档里显式加了一句「接口不返回摘要,不得向用户承诺或编造摘要」。

这件事值得单独写一节,因为它是最典型的一类错误:把"应该有"写成"接口有" 。规格里多写一个不存在的字段,下游会照着去取 undefined;更糟的情况是,如果这段规格喂给了 AI,它可能为了"把字段凑齐"直接编一段摘要出来,而读者完全看不出那是编的。

同样处理的还有 type=new:这次请求返回的是 Internal Server Error。参数入口保留了,但文档标注「待核实、不作能力承诺」,而不是含糊地写成"支持热门 / 最新两种榜单"。

不确定就标出来,不要编。宁可少写一个能力,也不要多写一个幻觉。

四、hot.mjs 怎么写的

脚本分成几个小函数:parseArgs(参数与边界校验)、fetchCategories(拉分类)、resolveCategory(名称 → id)、fetchHotList(请求 + 字段映射)、formatText(人类可读输出)。

核心的字段映射是这样:

js 复制代码
const GENERAL_ID = '1'; // 综合榜约定值,不在 query_category_briefs 返回列表中
const POST_BASE = 'https://juejin.cn/post/';

// 2026-11-11 实机核实:公开 GET,不传 Cookie
// data[] = { author:{name}, content:{content_id,title},
//            content_counter:{hot_rank,view,like,collect,comment_count} }
export async function fetchHotList({ categoryId, type, limit } = {}) {
  const params = new URLSearchParams({
    category_id: categoryId || GENERAL_ID,
    type: type || 'hot',
    aid: '2608',
    spider: '0',
  });
  const r = await api(`${ENDPOINTS.articleRank}?${params}`);
  if (!r || r.err_no !== 0) {
    throw new Error(`热榜获取失败:err_no=${r && r.err_no} err_msg=${r && r.err_msg}(非官方接口,可能已变更)`);
  }
  const list = Array.isArray(r.data) ? r.data : [];
  return list.slice(0, limit || CFG.hot.maxLimit).map((x, i) => {
    const c = x.content || {};
    const cc = x.content_counter || {};
    return {
      rank: i + 1,
      title: c.title || '(无标题)',
      author: (x.author && x.author.name) || '(未知)',
      articleId: String(c.content_id || ''),
      url: c.content_id ? `${POST_BASE}${c.content_id}` : '',
      hotRank: cc.hot_rank ?? 0,
      view: cc.view ?? 0,
      like: cc.like ?? 0,
      collect: cc.collect ?? 0,
      comment: cc.comment_count ?? 0,
    };
  });
}

几个设计取舍:

  • GET 优先、失败降级 POST :分类列表端点对方法敏感,沿用 tags.mjs 里已经验证过的双保险;
  • 分类名两级匹配 :先精确匹配,再兜底包含匹配;综合 / 全部 / all 统一归一到 category_id=1;
  • --limit 卡在 1--50:接口一次最多给 50 条,与其让脚本静默截断,不如把边界写进参数校验;
  • 文本 + --json 双输出 :文本给人看,JSON 给 Agent 和管道消费,后者输出 {category, categoryId, type, fetchedAt, items};
  • isMain 守卫 :export 出去给别的脚本复用时,不能因为被 import 就把 CLI 跑一遍;
  • 不落缓存 :榜单是实时的。实测两次相邻请求,同一篇文章的热度从 2862 涨到 2880、浏览从 4738 到 4773------缓存反而会给出过期答案。

跑起来是这样:

text 复制代码
$ node scripts/hot.mjs --limit 3
掘金热榜 · 综合 · 热门 · 3 条
 1. 永别了,程序员!(转)
    XCaptain  热度2826 浏览4662 点赞45 收藏18 评论37
    https://juejin.cn/post/7705987138957328427
 2. AI 写页面像说明书,修 Bug 像贴膏药?把这两句话写进 AGENTS.md
    ikoala  热度891 浏览1443 点赞22 收藏25 评论5
    https://juejin.cn/post/7705724831249432591
 3. AI 正在蚕食我们的编码能力
    石小石Orz  热度765 浏览1335 点赞12 收藏8 评论7
    https://juejin.cn/post/7705961716425490447

6 条冒烟(综合榜 / 分类筛选 / JSON 管道 / 分类列表 / 非法参数拦截 / import 无副作用)全部跑通,SKILL.md 也过了平台的格式校验。

五、一个真实的坑:默认配置被整体覆盖

新增功能时,我在 config.mjs 的 DEFAULTS 里加了一个 hot 分组:

js 复制代码
hot: {
  type: 'hot',
  defaultLimit: 20,
  maxLimit: 50,
},

看起来就够了?不够。loadUserConfig() 是这么合并用户配置的:

js 复制代码
const merged = { ...DEFAULTS, ...raw };
merged.defaults = { ...DEFAULTS.defaults, ...(raw.defaults || {}) };
merged.imageUpload = { ...DEFAULTS.imageUpload, ...(raw.imageUpload || {}) };
merged.tagCache = { ...DEFAULTS.tagCache, ...(raw.tagCache || {}) };

每个嵌套分组都单独做了一次浅合并 。如果新增 hot 时漏掉这一行,用户只要在 ~/.juejin/config.json 里写一句 {"hot":{"defaultLimit":5}},type 和 maxLimit 就会被整体冲掉变成 undefined------后果有两个:--limit 的上限校验形同虚设(5 > undefined 恒为 false),且未显式传条数时 fetchHotList 会拿到 slice(0, undefined),直接返回空列表。

补上这一行即可:

diff 复制代码
  merged.tagCache = { ...DEFAULTS.tagCache, ...(raw.tagCache || {}) };
+ merged.hot = { ...DEFAULTS.hot, ...(raw.hot || {}) };

这个坑的隐蔽之处在于:默认路径完全测不出来 。只有用户显式覆盖了新分组,才会暴露。所以每次给 DEFAULTS 加嵌套分组,都要同步检查合并逻辑------配置结构的扩展点,比新增一个字段更值得写进 checklist。

六、现在能问什么

你说 它跑
「看看今天掘金热榜前 10 篇文章」 node scripts/hot.mjs --limit 10
「前端分类最近最火的文章有哪些」 node scripts/hot.mjs 前端 --limit 5
「掘金有哪些分类」 node scripts/hot.mjs --categories
需要结构化数据时 node scripts/hot.mjs --json --limit 20

输出字段固定为:排名、标题、作者、文章 id、文章链接、热度值、浏览数、点赞数、收藏数、评论数。没有摘要------接口不给,脚本不编。

七、待办表更新

# 功能 状态
F1 文章发布(含本地图片上传) ✅ 系列 ①
F2 掘金热榜 ✅ 系列 ②(本篇)
F3 掘金沸点发布 📋 下一篇:短内容 + 话题 + 配图,复用 ImageX 链路,发布前回显预览确认
F4 掘金签到 📋 规划中
F5 文章下载(单篇) 📋 规划中
F6 文章批量下载 📋 规划中

🔗 仓库:github.com/liuleihang/... (MIT,Node ≥ 18,发布脚本零依赖)

🔗 workbuddy链接:www.workbuddy.cn/share/asset...

本项目为社区工具,与掘金官方无关。文中接口均为社区抓包 + 实测核实,非官方接口随时可能变更;err_no != 0 请原样上报,勿盲目重试

相关推荐
fthux1 小时前
总想着“忙完再休息”?让 Tick Rest 提醒你,先歇一会儿
开源·github·mac·swift·提醒·消息提醒·tick rest·tick rest for mac·消息提醒app
SL_staff2 小时前
企业AI落地难?JVS-AI渐进式实践:安全可控、小切口验证、标准化沉淀与规模化复制
java·开源·aigc
小虎AI生活2 小时前
Anatomy 接入 WorkBuddy 实操:一句话生成 12 零件可交互科普模型(附2组提示词)
aigc·ai编程
OpsEye3 小时前
别再挨个充会员了:多模型编码,最麻烦的从来不是选模型
javascript·ai编程
一知半解仙3 小时前
乌镇2026:当“智能互联网”第一次被数据照进现实
人工智能·物联网·机器人·开源
程序员老刘3 小时前
GUI的时代过去了
ai编程
用户3721574261353 小时前
C# 实现 Excel 单元格保护:锁定公式并允许编辑指定区域
c#·ai编程·编程语言
浪遏5 小时前
06|AI 失败不扣次,这句话要靠什么兑现
ai编程
浪遏5 小时前
01|把简历操作交给 AI:流萤简历的 CLI 自动化实践
ai编程