我把 Claude Code 发掘金的 token 成本砍到 1/50:web-publish 设计实录

我把 Claude Code 发掘金的 token 成本砍到 1/50:web-publish 设计实录

让 AI 写完文章直接发掘金听起来很美。实操中 Claude 会把整篇 markdown 反复 base64 进出 context,一次发文烧 5--14k tokens。本文拆解 web-publishPython CLI + 双 Backend 把这个数字压到 ~300 tokens 的设计、取舍与踩坑。

📦 项目地址(开源 MIT):github.com/PsChina/web... ------ 觉得有用欢迎 Star,平台 adapter 也欢迎 PR。

读这篇文章你能拿到的:

  • 一个真实的 Agent token 经济学案例------不是"理论上能省",是从 14k 降到 300 的实测数据
  • CLI vs JS eval 两种"让 AI 干活"的路线对比,及为什么 CLI 路线更可持续
  • urllib vs 浏览器同源 fetch 的实战取舍------为什么我们一开始选错了 default
  • 平台 API adapter 的 yaml 驱动设计,以及如何用 list 接口绕开签名墙
  • Agent + CLI 职责分层:Claude 做什么、Python 做什么的边界

如果你也在给 Claude Code / Cursor / Cline 这类 Agent 写工具集成,希望能少走一些弯路。


一、痛点:让 Claude 发文章,为什么"看起来很 trivial 实际烧爆 token"?

掘金的发文 API 不复杂------一个 article_draft/create,一个 article/publish,几个固定字段。让 Claude 直接调用看起来一句话就能搞定:

/tmp/article.md 发到掘金,标题 X,tag Y。

v0.2 我们就是这么干的,让 Claude 现场生成 fetch JS 字符串塞给 OpenCLI 让浏览器 eval。能用,但每次发文会触发以下事情:

阶段 进入 Claude context 的内容 估算 token
Read markdown 整篇 markdown 全文(中文每字 ~2 token) 3,000 -- 6,000
生成 JS payload 把 markdown base64 拼到 JS 模板字符串里 5,000 -- 8,000
OpenCLI eval 返回 API response 里包含发布后的 mark_content 全文 3,000 -- 7,000
验证 + 解析 Claude 再 grep 一遍找 article_id 几百
合计 ~5,000 -- 14,000 tokens / 篇

如果文章长一点(5000 字),单次发布烧到 14k tokens 一点不夸张。写得越多,发得越贵

更糟的是 update 已发文章场景:要拿原文做拼接,掘金 article_draft/detail 返回里也带 mark_content,又是一次全文进出 context。一次"在文末加一段"轻松 12k+ tokens。

按 Claude Sonnet 4.6 的输入价钱($3/MTok),单篇 14k tokens 约 ¥0.3 。看起来不多,但你试一下每周发 5 篇、每篇被 update 3 次......月成本就上去了,而且这只是调用工具消耗,文章本身的写作、优化、审稿那些 token 还没算。

更本质的问题不是钱,是上下文污染。Claude 的 context 里挤进去几万 token 的 markdown 原文 + base64 + JSON response,后续对话每轮都要带这些垃圾,回答质量肉眼可见地下降。

二、设计目标:把 Agent 当客户端,而不是当 API 调用器

v0.3 重新审视这个问题的角度:Agent 不擅长干的事,就别让它干

任务 该谁做 为什么
文章内容优化(标题字数、tag 推荐、80 字摘要) Claude 需要中文 SEO 直觉、平台特性理解
HTTP 请求、JSON 编解码、cookie 管理 Python CLI 确定性高,没必要让 LLM 推理
API endpoint / 字段 schema / 分类 ID 表 YAML adapter 平台改 API 时只改 yaml,不动代码
长 markdown 读盘 / 发送 CLI 内部 文件路径进 Agent context 就够,全文别进

落地成这个分层:

scss 复制代码
┌─────────────────────────────────────────────┐
│ Claude(Agent 视角)                        │
│ - Read markdown 一次(做 SEO 决策)        │
│ - Bash 调一行 web-publish publish ...      │
│ - 解析 JSON stdout(~300 tokens)          │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│ web-publish CLI(Python)                  │
│ - argparse 解析 flags                      │
│ - 内部读 markdown 全文(不进 Agent)       │
│ - YAML adapter 拿 endpoint / 分类 / tag    │
│ - 调 Backend.post(...) 拿 data            │
│ - 输出单行 JSON                            │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│ Backend(两种实现,同一协议)              │
│ - UrllibBackend       (.env + urllib)     │
│ - OpenCLIBridgeBackend (浏览器同源 fetch)  │
└─────────────────────────────────────────────┘

Backend 协议本身极简(一个 post 方法),但承担了"屏蔽鉴权差异"的核心职责:

python 复制代码
class Backend(Protocol):
    name: str
    def post(self, path: str, payload: dict,
             query_params: dict | None = None) -> dict:
        """POST 到平台 API,返回 data dict,err_no != 0 抛 BackendError"""

JuejinClient 持有一个 backend,完全不感知 backend 是 urllib 还是浏览器同源 fetch------这是后面 dual backend 能干净切换的基础。

落到 token 账本:

路径 每次 publish 烧 Claude tokens Agent 看到的
v0.1(用户旧文 raw urllib) ~1,000 用户级 Python 库调用,半结构化
v0.2(Claude 直生成 JS) 5,000 -- 14,000 Read + eval JS + response data
v0.3(Python CLI 包装) ~300 1 行 Bash + 1 行 JSON

~50× 的成本降幅 ,关键不是技术上做了什么牛逼的事,而是把不该让 LLM 处理的字节挡在 context 之外

三、Dual Backend:为什么 default 选 urllib,不选"零配置"那个

v0.3 第一版选了 opencli-bridge 当 default------通过 OpenCLI 浏览器扩展让 Chrome 在已登录的掘金页面里跑 fetch,cookie 是 same-origin 自动带的,用户连 .env 都不用配

听起来美好,实测打脸:

失效场景 urllib opencli-bridge
Chrome 没开 ✅ work ❌ extension not connected
Chrome 开着但用户切去别的 tab ✅ work ⚠️ 需 background tab,opencli v1.7.16 超时
Chrome 重启后 extension service worker 睡了 ✅ work ❌ doctor reports not connected
服务器 / CI / headless 环境 ✅ work ❌ 完全跑不了
Active tab 在 about:blank / chrome:// ✅ work ❌ 相对 URL fetch 解析失败
长期可移植性(OpenCLI / Chrome / extension API 升级) ✅ Python stdlib 零依赖 ⚠️ 多层依赖链单点失败叠加

"零配置"的代价是脆。 用户体感是"昨天还好的,今天怎么不行了"------而原因可能在 Chrome 重启、extension 自动更新、操作系统休眠......一长串无法预测的因素。

urllib 的唯一额外成本是用户跑一次 web-publish setup 用编辑器粘 cookie(~30 秒),从此 forever stable。

最终设计:

  • urllib = default,覆盖 90% 场景(个人桌面 / 服务器 / CI)
  • opencli-bridge = --backend opencli-bridge 显式选项,留给"我就想一次性发个文章不想抓 cookie"的临时场景

教训:给 AI Agent 写的工具,可靠性 > 零配置。Agent 不像人会读"重启一下 Chrome 试试"------它会傻乎乎重试,把 context 烧穿。

四、Cookie 引导状态机:不让 AI 烦用户

但 urllib backend 总要让用户配一次 cookie。这是 dialog 设计的硬骨头:

  • 第一次发文:当然要引导用户配 cookie,否则 .env 不存在直接报错
  • 第二次发文:用户上次配好了,应该静默走 urllib
  • 第二次发文,但用户上次拒绝了:不能再问,他知道有这个选项
  • 第二次发文,用户上次说"先发吧晚点配"------10 分钟过去了:他显然忘了,但不要再 block 他发文章
  • 第十次发文,用户从未配过:彻底放弃引导,永远 fallback

把这堆条件写在 SKILL 里让 Claude 推理?等着翻车。

web-publish 把这逻辑用一个状态机命令封装:

bash 复制代码
$ web-publish state
{
  "version": 1,
  "publish_count": 0,
  "setup_completed": false,
  "cookie_setup_started_at": null,
  "cookie_skip_count": 0,
  "cookie_dismissed": false,
  "last_publish_at": null,
  "last_backend_used": null,
  "_env_exists": true,
  "_recommendation": {
    "action": "use_urllib",
    "reason": ".env 在 (旁路 setup)"
  }
}

关键是 _recommendation.action,6 种可能:

action Claude 行为
use_urllib 静默走 urllib,不问问题
guide_setup_first_time 提示用户 30 秒配 cookie,记录"已引导"
use_opencli_after_timeout 引导过但用户拖了 10 分钟,静默用 opencli-bridge 不再 block
use_opencli_skipped 用户明确说"先发吧",本次用 opencli,不重复引导
use_opencli_dismissed skip 3 次,永远不再问,静默 opencli
ask_after_publish publish 成功后温和问一次"要不要配 cookie 之后更稳"

web-publish state record-skiprecord-setup-started 是 Claude 调的副作用命令,让状态机推进。

设计要点:别把"如果......就......"的 if-else 写在 Prompt 里让 LLM 推理 ------把它写成代码,Agent 只读 action 字段执行。这条经验跨工具普适:状态决策能用代码表达就用代码,Prompt 不是逻辑容器。

五、Adapter YAML:从代码里拽出"会过期的东西"

平台 API 会改。endpoint 改路径、字段加 required、新签名机制------半年到一年一定会被打脸一次。

如果把这些写在 Python 里,每次平台改版都要发新版本、用户都要升级。所以单独抽 adapters/juejin.yaml

yaml 复制代码
platform: juejin
api_base: https://api.juejin.cn

endpoints:
  # ✓ 不需签名,credentials:include 同源带 cookie 即可
  list_by_user:   POST /content_api/v1/article/list_by_user
  draft_detail:   POST /content_api/v1/article_draft/detail
  draft_create:   POST /content_api/v1/article_draft/create
  draft_update:   POST /content_api/v1/article_draft/update
  publish:        POST /content_api/v1/article/publish

  # ✗ 需 msToken + a_bogus 反爬签名(page 内 axios wrapper 自动算)
  article_detail: POST /content_api/v1/article/detail

categories:
  后端:    "6809637769959178254"
  前端:    "6809637767543259144"
  Android: "6809635626879549454"
  人工智能: "6809637773935378440"
  开发工具: "6809637771511070734"
  代码人生: "6809637776263217160"

known_tags:
  AI编程: "7467857238494020000"
  OpenAI: "6809641073527226000"
  AIGC:   "7197380506562871000"

content_rules:
  title_max: 80
  title_recommended_range: [20, 30]
  brief_max: 100
  tags_max: 5

这份 yaml 是 SDK 的"协议存档"。它告诉你:

  1. 哪些 endpoint 可以裸 fetch 用(写操作 + list 接口)
  2. 哪些 endpoint 撞签名墙article_detail------掘金给已发布文章详情加了 msToken + a_bogus 反爬,普通 fetch 拿不到)
  3. 分类 / tag ID 在哪儿查表(不调网络,纯内存映射)
  4. 优化规则数字(Claude 做 SEO 决策时直接读这些数字)

平台改 endpoint?改 yaml 重发布 adapter,不动 Python 代码。

5.1 实战:用 list 接口绕开签名墙

掘金 article/detail 接口需要 msToken + a_bogus 反爬签名,这个签名是页面内 axios wrapper 在请求拦截器里现算的。我们的 backend 是裸 fetch,算不出来。

但 update 已发文章的流程需要"拿原 mark_content 拼接"------怎么办?

答案在 yaml 注释里:

article_detail: 用 list_by_user + draft_detail 组合绕开

观察到 list_by_user 返回每篇文章的 metadata 里带了 draft_id ,而 article_draft/detail(草稿详情)不需要签名------掘金没把反爬装在草稿接口上。所以 update 流程变成:

python 复制代码
def find_draft_id(self, article_id):
    for entry in self.list_by_user(page_size=100):
        info = entry.get("article_info", {})
        if str(info.get("article_id")) == str(article_id):
            return info.get("draft_id")
    return None

# update 流程
draft_id = client.find_draft_id(article_id)        # ← 用 list 反查
cur = client.get_draft(draft_id)                   # ← 草稿接口拿原 markdown
new_content = cur["mark_content"] + supplement
client.update_draft(draft_id=draft_id, mark_content=new_content, ...)
client.publish(draft_id)

绕了一圈但完全没碰签名墙 。这也是为什么 list_by_user 接口被设计成 SDK 的核心 primitive 而不是辅助功能。

教训:API adapter 设计时认真区分"哪些接口要签名、哪些不要",能优雅绕开就别去逆向签名算法------后者维护成本极高,平台一改你就废。

六、Agent 视角:发一篇文章到底烧了多少 token

把上面所有设计串起来,看一次完整的 /publish juejin /tmp/article.md 在 Claude 视角是什么样:

bash 复制代码
Claude:
  Read /tmp/article.md              ← 仅这一次进 context(用于 SEO 决策)
  Bash:
    web-publish state                ← 1 行 JSON in,决策 action
  Bash:
    web-publish publish juejin /tmp/article.md \
      --title "我把 Claude 发掘金的 token 成本砍到 1/50:web-publish 设计实录" \
      --brief "..." \
      --tag-ids "AI编程,OpenAI,AIGC" \
      --category "开发工具"
    → {"platform":"juejin", "article_id":"...", "post_url":"https://juejin.cn/post/..."}
  Output:
    ✅ 已发布到掘金(审核中)
    URL: https://juejin.cn/post/...

token 账:

  • 文章 Read 一次:~3k--6k(无法避免------Claude 要看内容才能做 SEO 决策)
  • web-publish state JSON:~150
  • web-publish publish 命令行 + JSON 返回:~200
  • 总 overhead(Read 之外):~350 tokens

vs v0.2 的 5k--14k overhead------把 markdown 本身从"反复进出 context 的中间产物"变成"只读一次的决策依据"。

更关键的是 markdown 永远不进 Bash 字符串------文件路径作为位置参数传给 CLI,CLI 自己读盘。这避免了:

  • 把 markdown 嵌入 shell 命令导致的 quoting 灾难(中文、引号、反引号、$
  • markdown 体积放大上下文
  • 二次 Read 浪费

七、一个值得展开的细节:subprocess 启动 + JSON 双重 stringify

OpenCLIBridgeBackendsubprocess.runopencli browser eval <js>。OpenCLI 把 JS 的 return value 输出到 stdout。

第一次实测发现 stdout 是 "{\"err_no\": 0, ...}"------多了一层引号。原因是我们的 JS 做了:

javascript 复制代码
return JSON.stringify(await r.json());

而 OpenCLI 把这个返回值又 stringify 了一次包成 JSON string。所以 backend 解析时要 robust 一点:

python 复制代码
data = json.loads(raw)
if isinstance(data, str):
    # opencli 双重 stringify,再解一层
    data = json.loads(data)

教训:跨 subprocess + JSON 边界要假设有"包装层" ------能 wrap 一次的工具未必不会 wrap 两次。isinstance(data, str) 这个分支看似冗余,但实测必要。

八、还没解决的

  • CSDN / 知乎 adapter:endpoint schema 不同,写过 PoC 但还没固化(v0.4 计划)
  • 撞反垃圾 :同账号短时间发"高度相似内容"会被掘金拦 err_no=2,目前只能让用户改内容或等几分钟
  • 审核状态轮询spost_urlpost_url 的跳转目前不做自动检测,给用户的 URL 是 post_url(生效后能访问)
  • 多账号调度:urllib backend 实际上已经支持,但 SDK 上层还没暴露
  • 多平台一键发:故意没做------平台特性差异(标题字数、tag 规则、分类)让"一键多平台"是个伪需求;用户应该明确选哪个平台

九、可以借走的方法论

如果你也在做 AI Agent 工具集成,几条可复用的经验:

  1. 算 token 账,比算性能账更重要。Agent 工具的瓶颈不是延迟,是 context 污染------每次工具调用都问"什么字节必须进 context,什么不必须"
  2. 把不该让 LLM 处理的事推回代码。Cookie 引导状态机、API endpoint 表、字段 schema------能写成确定性代码就别让 Prompt 推理
  3. adapter / config / endpoint 表分离。平台 API 改版的频率比你预期的高,把易变的东西从代码里拽出来
  4. default 选可靠的,不选漂亮的。"零配置"听起来很美,但脆。给 Agent 用的工具尤其要 boring
  5. 状态机命令优于 Prompt 中的 if-else。状态决策能用代码表达就用代码

试试看

项目地址github.com/PsChina/web...(MIT 开源,欢迎 Star ⭐)

一行装好:

bash 复制代码
curl -sSL https://raw.githubusercontent.com/PsChina/web-publish/main/curl-install.sh | bash
web-publish setup            # 配一次 cookie(30 秒)

然后在 Claude Code 里:

bash 复制代码
/publish juejin ./你的文章.md

这篇博客本身就是用 web-publish 发的------你正在读的版面、tag、分类,全是 Claude 调 web-publish publish 一行命令产出的。一个螺旋的成就感时刻 🌀。

参与贡献

仓库地址:github.com/PsChina/web...

最想要的 PR 方向:

  • CSDN / 知乎 / 思否 / 博客园 adapter :照着 adapters/juejin.yaml 抄一份,跑通你账号的 endpoint 就行
  • Backend 新实现:除了 urllib / opencli-bridge,也欢迎 Playwright / Selenium backend
  • 多平台一键发 / 草稿同步 / 文章迁移:基础 SDK 已经稳定,上层组合应用大有空间
  • Issue 反馈:哪个 endpoint 失效了?哪个平台你想要?提 Issue 直接说

如果你觉得这个工具救过你的 token 账单 / 让你写博客更轻松,点个 Star 是对作者最直接的鼓励。也欢迎在掘金评论区聊"AI Agent 工具集成"的更多踩坑------大家一起把 AI 工具链的可靠性提上来。

相关推荐
Hector_zh1 小时前
容器化部署踩坑记:测试环境 Git 凭证外挂方案验证
人工智能·ai编程
cici158741 小时前
基于 BP 神经网络的语音信号分类系统
人工智能·神经网络·分类
AI街潜水的八角1 小时前
PyTorch框架——基于深度学习SRN-DeblurNet神经网络AI去模糊图像增强系统
人工智能·pytorch·深度学习
alex2751 小时前
🔥 Spring AI 流式输出深度实战:SSE + 停止按钮 + JSON 事件,一文全搞定
人工智能
alex2751 小时前
深入 Spring AI 聊天补全:ChatClient、PromptTemplate、Advisor 一网打尽!
人工智能
IVEN_1 小时前
Hermes Agent 接入 Kimi Coding 套餐:修复 Vision 图像分析功能
人工智能
Bode_20021 小时前
AI时代制造企业创新的需要的关键技术
人工智能
Arvid1 小时前
Transformer 隐藏的另一半:Attention 之后,大模型靠什么变聪明?
人工智能
极客老王说Agent1 小时前
实在Agent委外加工智能化管控方案与落地案例:重构2026制造业协同新范式
人工智能·ai·chatgpt