阅读视图

发现新文章,点击刷新页面。

用Trae做了个公众号小工具

是什么

MD标题外链转二维码

如图给 markdown 内容中带连接的标题下插入一个二维码和连接内容。

方便用户阅读时扫码直接访问或者复制链接。

已支持能力

  • 自动解析 MD 文档,识别带超链接的标题
  • 自动在标题下方生成对应的二维码和链接文本
  • 提供原始预览和转换后预览的对比视图
  • 支持一键复制转换后的内容(会将图片以 base64 形式通过img标签导入)
  • 自动保存编辑内容到本地缓存
  • 移动端提供便捷的预览切换功能

体验地址:

  1. aicoding.juejin.cn/aicoding/wo…
  2. atqq.github.io/md-qrlink/
  3. GitHub:github.com/atqq/md-qrl…

发公众号的话,搭配 微信 Markdown 编辑器md.doocs.org/) 效果更加。

为什么搞这个

微信公众号的个人的文章无法直接跳转第三方的外链,文章通常会是外链转脚注,将相关链接放在最后。

但周刊类型的文章外链较多,不太方便用户 CV 跳转。

之前在其它公众号有看到过类似的展示,就想着复刻过来!

怎么搞得

用 Trae(Claude-3,7-sonnet)生成核心代码(99%),人工主要在一些特定的样式问题上做了介入。

技术原理

Vue 3 + Vite

  • Markdown解析: markdown-it
  • 二维码生成: qrcodeqrcode.vue
    • 前者负责复制的时候 base64 二维码内容生成,后者负责预览里的二维码渲染
  • 本地存储: localStorage API

人工介入部分

  1. 布局的样式,AI 每次在修改其它问题的时候会把我已经调整的布局扔掉用新生成的覆盖
  2. 复制到 markdown:HTML 标签在复制到微信里的时候 微信会做标签的转换,一些布局样式转换后被丢了。这块需要人工介入在微信的规则下做一下调整。

我的 Prompt

第一轮对话的“提示词”

使用Vite Vue实现一个md 格式转换项目,将Markdown中标题是超链接的提取出来,
放在标题下方 并在左侧生成对应的二维码图片

支持预览原Markdown和转换后Markdown内容

总共进行了 15 轮的连续对话,就达到了这个效果。

其中部署 GitHub Pages 的 GitHub Action 脚本也是用其生成的。

最后

现在有了 VibeCoding 的编码方式,出 Demo 非常的快,效率比人工高 N 倍。

可以快速验证想法。

顺便推荐一波此类型的在线工具

Trae 整体体验我觉得还行,3$ 的首月体验还可以接受,大模型都一样,这比 Cursor 便宜太多了。还没搞上AI IDE 的可以试用一波,每天也有些免费额度。

读者有发一些奇思妙想的应用想法,也可评论区来一波。

不多说了“两个字” AI NB

Coze+Trae+掘金MCP = "✨掘金微热榜"

1. 引言

🤡 "MCP" 和 "Vibe Coding" 这两个词刷屏好一阵子了,搜了下资料,好像不是什么高深的东西,🤷‍♀️ 搞不懂为啥这么火,简单聊聊~

1.1. MCP

  • Model Context Protocol,模型上下文协议,由 Anthropic 公司于 2024年11月推出推出的开源协议,旨在为 大语言模型 (LLM) 与 外部数据源、工具及服务之间建立标准化、安全的双向连接
  • 在它之前 LLM与外部资源交互 通过 Function Call 来实现,需先实现 自定义协议 (如 JSON Schema),模型直接生成调用指令 (如特定JSON),开发者需实自行实现 函数编排层-解析 (验证格式、匹配函数、提取参数) 和 函数处理逻辑-执行 (调用 API、处理权限和错误),并将处理结果以适当的形式 反馈给用户 (格式化数据、维护上下文),跨平台兼容麻烦。
  • 而在 MCP架构 中,"函数编排层" 被下沉到 宿主应用(Host,连接用户、LLM和外部工具的核心枢纽) ,开发者无需手动实现协议解析、服务发现、参数校验等底层逻辑,而是专注于业务功能的实现。

Talk is cheap,show you the Code,以调用天气服务为例,Function Call 开发者 需实现完整编排逻辑:

# 1. 定义函数 Schema(JSON 结构)
schema = {
    "name": "get_weather",
    "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"]
    }
}

# 2. 解析模型生成的 JSON 请求
def parse_request(raw_json):
    # 验证 Schema、提取参数、错误处理
    if "city" not in raw_json["arguments"]:
        raise ValueError("缺少城市参数")

# 3. 调用外部服务
def call_weather_api(city):
    # 处理 API 密钥、网络请求、重试逻辑
    response = requests.get(f"https://api.weather.com?key=XXX&city={city}")

# 4. 格式化结果并返回给模型
def format_result(api_data):
    return f"温度:{api_data['temp']}℃,天气:{api_data['condition']}"

# 5. 整合到 LLM 对话流程
def handle_query(user_input):
    llm_response = model.generate(user_input)  # 模型可能返回 JSON 请求
    parsed = parse_request(llm_response)
    data = call_weather_api(parsed["arguments"]["city"])
    return format_result(data)

而 MCP 开发者只需用 @mcp.tool() 声明功能,然后实现业务逻辑,协议层 由宿主应用自动处理:

# 1. 定义 MCP 工具(声明式语法)
from mcp.server import MCPTool

@MCPTool(name="get_weather", description="获取天气")
def get_weather(city: str) -> str:
    # 直接编写业务逻辑,无需处理协议细节
    return f"温度:{query_weather_api(city)}℃,天气:{get_condition(city)}"

# 2. 启动 MCP 服务器(宿主应用自动处理通信)
if __name__ == "__main__":
    server = MCPServer()
    server.register_tool(get_weather)
    server.run()  # 宿主应用负责协议解析、安全校验、多工具路由

MCP 协议的核心架构除了上面定义的 MCP Server 外,还有 MCP Client,前者 暴露具体功能 (如文件操作、API调用),实现业务逻辑 (如数据库查询、邮件发送),业务逻辑千差万别,须定制开发,大部分案例都是捣鼓这个。后者实现 协议转换、服务发现、结果整合,需处理协议解析、多工具路由、安全校验,协议层高度标准化 (JSON-RPC 2.0),已有成熟实现 (😀Trae、Cursor 等AI编程助手基本都内置,支持所有标准协议,开发者无需重复实现)。举个 Trae + MCP Server 调用天气服务的执行链条🌰:

=== MCP Server 配置 ===

添加MCP Server配置 (直接市场安装或手动配置),然后一般是要配置API密钥的,如Figma的配置:
{
  "mcpServers": {
    "Figma AI Bridge": {
      "command": "npx",
      "args": [
        "-y",
        "figma-developer-mcp",
        "--stdio"
      ],
      "env": {
        "FIGMA_API_KEY": ""
      }
    }
  }
}

=== MCP Server 调用 ===


用户输入提示词 —— 如:"北京今天天气怎么样?需要带伞吗?"
  │
  ▼
Trae的AI模型解析意图 —— 识别出需要调用「天气查询」功能,并提取参数(城市=北京)
  │
  ▼
Trae 内置的 MCP Client 查找工具
【动态服务发现】自动向所有已配置的 MCP Server 发送 Capability Exchange 请求,获取可用工具列表。
【匹配逻辑】根据提示词中的关键词(如 "天气"),筛选出已安装的 Weather MCP Server。
  │
  ▼
调用天气 MCP Server
【协议通信】使用 JSON-RPC 2.0 格式发送请求:{
  "method": "get_weather",
  "params": {
    "city": "北京",
    "include_rain": true  // 用户隐含需求:是否需要带伞
  }
}
【服务器执行】调用第三方天气 API,解析返回的 JSON 数据,提取天气状况,返回结构化结果:{
  "temperature": "28℃",
  "condition": "多云转晴",
  "rain_probability": 10
}
  │
  ▼
结果反馈
【成功】Trae 收到数据后,生成自然语言回复:“北京今天多云转晴,气温28℃,降水概率10%。无需带伞。”
【失败】若 API 超时,返回 { "error": "无法连接天气服务" },Trae提示:“暂时无法获取天气信息,请稍后再试”

顺带分享几个 MCP Server 聚合 站点:

1.2. Vibe Coding

😄 MCP 了解完,接着说下 Vibe Coding (氛围编程) —— 2025年初由 OpenAI 联合创始人Andrej Karpathy 提出的概念,一种通过 自然语言提示驱动AI生成可执行代码软件开发范式。与传统AI辅助编程工具不同,其强调开发者对生成代码的 "有限理解接受度",只要功能实现符合预期,无需深究具体实现细节 🤡。

传统编程要求开发者建立 双重认知映射先将业务需求转化为算法逻辑,再将逻辑表达为特定语法结构。而在Vibe Coding范式下,这种双重映射被简化为 单阶段语义传递,开发者只需 确保需求描述的准确性,其余转换工作由AI模型完成。Vibe Coding 的技术实现依赖于两个关键要素的协同作用:

  • 专用代码生成模型:以Claude 3.7等为代表的代码专用LLM,通过海量开源代码库和技术文档训练,具备 理解编程语义和生成合规代码 的能力。这些模型在参数规模 (普遍超过500B) 和训练数据量 (涵盖GitHub前1%优质仓库) 上远超早期代码生成工具,能够处理复杂的技术需求。
  • 自然语言交互框架:开发工具提供 对话式编程界面,允许开发者通过 渐进式提示 来完善描述,典型交互模式包括:初始需求陈述、功能细化追问、异常处理补充

🐶 简单点说,以前写个 简单程序 (网页、APP等),你得先学对应的编程语言,学点基础的数据结构和算法,然后报错得自己定位,上网检索答案。

🤡 现在你只需要会 好好说人话 就行 (写 提示词,,甚至通过Superwhisper等工具语音转文字直接battle),如 "给我做一个XXX App",AI 就会啪啪啪一顿生成,搭建完基本架构,你要做的是继续 描述清楚需求 (如"点击按钮后跳转XX页"),完善APP的过程可能会报错,纠错也是靠 口嗨,如 "这里显示红色不对"。🤔 这种编程范式确实减低了开发门槛,开发仔能提效,普通人也能快速实现自己的想法,有种"人人都是产品经理的趋势"~

1.3. 掘金MCP

😏 掘金前些天也推出了自己的 MCP Server《通过 Trae 等 AI IDE 配置 MCP一键发布到掘金》,配置起来非常简单,先 生成下Token

这个Token是固定的,跟掘金账号绑定,忘记了也没关系,下次获取返回的值也是一样的,但也注意别泄露,以免引起不必要的麻烦。接着在 Trae、Cursor支持MCP 的AI编程工具,找到添加MCP配置的入口,粘贴下配置

{
    "mcpServers": {
        "juejin-deploy-mcp": {
            "command": "npx",
            "args": [
                "--registry=https://registry.npmjs.org",
                "-y",
                "@juejin-team/mcp-server@latest"
            ],
            "env": {
                "JUEJIN_TOKEN": "粘贴上面生成的Token"
            }
        }
    }
}

配置完确认:

💁‍♂️ 这个 MCP Server 的作用:将纯前端项目(HTML/CSS/JS) 一键部署到掘金,注意是 "纯前端项目" ❗️❗️❗️ 依赖Node.js 或是 需要编译的框架 (如 React/Vue 未打包成静态文件) 都是不支持的哈!!!

😄 本质:将 纯前端项目 作为 静态资源 托管到 CDN好处:无需繁琐配置,调下MCP就能完成部署。通过不同 项目文件夹名 区分不同项目,只要不改名字,每次调MCP都会覆盖。😐 用到的 图片 等资源文件不会上传!!!需要你自己找 图床 或者 静态托管网站,生成 远程url 访问,最简单的可以用 GithubGitee

😳 端午前,掘金酱发了篇《掘金 AI 编程社区- 人人都是 AI 编程家竞赛》,省流:

使用 Trae+掘金 MCP 实现 VibeCoding 创意作品参赛,奖励丰厚,截止日期:6月30日

❗️ 提醒下:发布完记得报名填下参赛信息:vibecoding

😏 刚好有个💡,妥妥得参加一波,虽然没有前端开发经验,但刚花3刀开了 Trae Pro 我的感觉强得可怕🔥。

1.4. 灵感乍现

😶 随着 AI编程工具 的逐渐普及,以前那种喜欢 钻研技术细节 的热情也逐渐褪去,现在打开掘金,也就签个到,然后刷下 掘金热榜

🤔 很多时候,我并不能直接从文章的 标题,得知这是一篇 写什么内容的文章,我得点开,粗略看下写的啥,是不是自己感兴趣的内容,再选择是否做详细阅读,😐 感觉这样获取信息的效率有点低。😏 我突然有个想法:为什么不自己写一个 "掘金丶微热榜" 呢?思路立马就有:

  • 写脚本定时爬热榜接口,获取热榜文章数据。
  • AI做文章预读,批量生成标题和内容摘要。
  • Trae 生成我想要的页面,然后利用 掘金MCP 部署成一个静态网站。

2. 实践

2.1. 接口分析

浏览器 F12 打开开发者模式,过滤 XHR,截取不同分类的url:

😄 明显只是 category_id 不一样,试了下不传uuid,一样可以获取到数据,OK,Trae 直接新建一个 py 文件,写提示词尝试数据爬取:

😂 然后生成结果惊到我了,我前面有个链接复制是重复的,Trae 觉得可能有错,给我贴心的标注出来了:

🙂 category_id 修正后,直接运行,数据解析,一步到位,雀食🐂🍺:

2.2. Coze

2.2.1. 代码节点-模拟请求

Coze 空间 直接新起一个 工作流,拖一个 代码节点,不支持pip装三方库,没有 requests 库,但提供了 requests_async 库,用法基本一样,导包设置下别名:import requests_async as requests,把上面 Trae 生成的爬取代码CV过来,改改就能用:

import requests_async as requests

async def main(args: Args) -> Output:
    category = args.params['category']
    category_map = {
        '综合': '1',
        '后端': '6809637769959178254',
        '前端': '6809637767543259144',
        'Android': '6809635626879549454',
        'iOS': '6809635626661445640',
        '人工智能': '6809637773935378440',
        '开发工具': '6809637771511070734',
        '代码人生': '6809637776263217160',
        '阅读': '6809637772874219534'
    }
    # 检查分类是否有效
    if category not in category_map:
        raise ValueError(f"无效的分类: {category},有效分类为: {', '.join(category_map.keys())}")

    # 构建请求URL
    category_id = category_map[category]
    url = f"https://api.juejin.cn/content_api/v1/content/article_rank?category_id={category_id}&type=hot&aid=2608&spider=0"

    # 发送请求
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    }
    response = await requests.get(url, headers=headers)
    response.raise_for_status()  # 如果响应状态码不是200,将引发HTTPError异常
    data = response.json()
    # 构建输出对象
    ret: Output = {
        "result": data,
    }
    return ret

接着点下 测试代码,传入category值为 "综合",运行,ok,数据正常输出:

这里为了方便调试,直接输出 接口返回的json字符串,后续处理,需要转换成 Array 的形式返回:

💡 Tips:如果修改输出参数后报错,点右上角X,关掉节点,然后再点 在IDE中编辑 就好了。

再次试运行:

👏 Nice,数据解析完毕,这里加了三个字段:order (排行榜序号)、ai_title (ai生成的文章标题)、ai_desc (ai生成的摘要)。

2.2.2. LinkReaderPlugin插件-读取文章内容

如题,添加一个 LinkReaderPlugin的插件,用于读取文章内容,简单做下配置:

配完试运行:

🐂 啊,50个页面只花了13s,我还想着会很慢,然后用老套路,拖五个插件,分任务并发执行呢🤣。时代变了,现在最多支持 200次批处理+10个并行任务

2.2.3. LLM节点-生成文章标题和摘要

接着拉个大模型节点,批处理生成标题和摘要,先配下批处理和输出参数:

接着简单写下提示词,让AI自动优化:

AI优化后的提示词:

🤣 中规中矩,补全输出参数,试运行:

😳 卧槽,6s干完,🐂啊,不过输出效果不是特别满意,再自己微调下提示词,最终效果:

2.2.4. 代码节点-数据整合

😶 拖个代码节点,用 LLM节点返回的标题和摘要,更新下前面代码节点返回数据的对应字段:

🐶别问我为什么这么写,因为这样写不报错,我也想直接返回origin_data ...

结束节点,返回变量,输出变量,引用下代码节点输出的值就行啦~

试运行无误后,点击右上角 发布 按钮,填下 版本号版本描述,进行发布~

2.2.5. API调用

发布完就可以通过API来调用工作流了,打开 执行工作流 页面进行测试,先整个Token,点授权 → 添加新令牌:

输入Token名称,设置过期时间,勾选工作流,工作空间选个人,点确定:

会生成一个Token,只会显示一次,自己保存好哈,不然就得删了重建了~

回到工作流、执行工作流,依次输入:token工作流id (编辑工作流的url中workflow_id后面跟着的数字字符串就是) 和 参数:

点击运行,等待片刻,返回结果没啥问题,接着让 Traecurl 命令模拟请求翻译成 python 实现:

😀 Trae 如约写出py请求代码,试了下一把过,数据能正常获取到,理论上Coze部分应该就到这,但实践过程还是遇到了一些问题...

2.2.6. 问题:资源点额度 & 插件调用

😶 Coze 现在是 算资源点 的,个人免费版一天只有 500点,工作流试运行也扣资源点,加了LLM节点,没玩几次就说免费资源点用尽:

😳 资源点那么不经用的吗?后面看了下计费标准才知道,这玩意主要是调LLM扣的:

右上角看到跑一次工作流消耗 30多w的tokens,后面找到消费历史:

我去,跑一次100点,怪不得没玩两下就没了,🤡 唉,想继续体验只能氪金,个人进阶版限时特惠 9.9元/月~

🐶 搞一次得调8次工作流,1000 点也扛不住造,赶紧找个最便宜的模型:

调了两次工作流花了88点左右,感觉一次差不多50点,一天更新两次热榜还是够用的:

😐 当我满怀期待跑 py调工作流api 的脚本时,后台却给我返回了这个:

啥玩意?工具请求超过限制?接着我打开工作流又是运行了一下:

🙂 想找个客服问问,才发现 个人版不配

🐶 加了交流群没人鸟,只能老老实实 反馈-提工单

🤡 只有两个选项... 直接选的第二个:

过了十分钟有个技术工程师让我提供下异常logid,提供完过了快一小时都没回我...

🤡 等了一个多小时,就得到一个 免费插件稳定性没法保证,请用付费插件或自定义插件 的回复,问题是 《文档中心:插件费用》也没有 链接读取 的插件,🙃 这才刚开始,就结束了?

🙂 地铁回家路上,我试了写提示词让 Kimi 分析url的内容并提取标题和摘要,竟然可以:

😆 那是不是我 不用插件,直接调豆包模型也可以解析url生成标题和摘要?直接开试:

分别试下两个模型的输出:

🤡 Lite 直接胡说八道,明明是 Trae Pro付费的文章,Pro 说无法访问链接让我手贴内容,看来是模型不支持联网,但支持 技能配置,选下 链接读取 插件就正常了:

🤔 能否通过 模型自己调插件的方式 绕开上面 用户调用插件 的限制呢?同样试试:

🤣 卧槽,果然可以,机智如我:

😄 之前为了避免那个访问限制,只能同步加延时(5分钟请求一次),但依旧报错:

现在:

😏 而且速度快多了,不过一次还是得等上几分钟,8个不得半个小时?用毛 同步request,直接上 并发协程Trae 听我号令🔥:

哗哗哗,没几分钟就搞完了

😳 正当我以为万事大吉准备收工,却瞥见输出结果不对劲:

😭 难受,这个方法也行不通,那就只能自己写个插件了...

2.2.7. 写个获取文章内容的插件

😄 这里支持安装三方依赖包,直接装一波 requests

接着先配 元数据 (插件的输入/输出参数)

接着Trae写

接着Ctrl + I 写提示词让AI写代码:

运行结果:

OK,都打印出来了,接着用 Trae 用正则写提取代码 (🐶 Cluade 4.0 靠谱一点):

🤡 试了下,单纯靠正则不太行,有些正文会获取不到,还是让 Trae 抠 HTML 标签吧:

加了bs4+lxml库,运行后生成结果还可以,代码复制到 Coze:

from runtime import Args
from typings.juejin_article_content.juejin_article_content import Input, Output
import requests
import re
from bs4 import BeautifulSoup


def extract_article_content(url):
    """
    从URL获取掘金文章内容并提取正文,使用BeautifulSoup解析HTML
    
    Args:
        url: 文章URL地址
    
    Returns:
        str: 提取后的文章内容字符串
    """
    try:
        # 发送HTTP请求获取网页内容
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
        response = requests.get(url, headers=headers)
        response.raise_for_status()  # 如果请求不成功则抛出异常
        html_content = response.text

        # 使用BeautifulSoup解析HTML
        soup = BeautifulSoup(html_content, 'lxml')

        # 提取文章标题
        title_element = soup.select_one('h1.article-title')
        title = title_element.text.strip() if title_element else "未找到标题"

        # 提取文章作者
        author_element = soup.select_one('span.name')
        author = author_element.text.strip() if author_element else "未知作者"

        # 提取文章内容 - 查找文章主体部分
        # 尝试多种可能的选择器,增强通用性
        content_element = None
        possible_selectors = [
            'div.article-viewer.markdown-body',  # 常规文章页面
            'div.markdown-body',                # 可能的备选结构
            'article.article-content',          # 另一种可能的结构
            'div.article-content'               # 再一种可能的结构
        ]

        for selector in possible_selectors:
            content_element = soup.select_one(selector)
            if content_element:
                break

        if content_element:
            # 提取所有段落和列表项
            paragraphs = []
            list_items = []

            # 提取段落
            for p in content_element.find_all('p'):
                # 移除style标签
                for style in p.find_all('style'):
                    style.decompose()

                # 获取文本内容
                text = p.get_text().strip()
                if text:
                    paragraphs.append(text)

            # 提取列表项
            for li in content_element.find_all('li'):
                text = li.get_text().strip()
                if text:
                    list_items.append(text)

            # 去重处理
            unique_paragraphs = remove_duplicates(paragraphs)
            unique_list_items = remove_duplicates(list_items)

            # 构建返回内容 - 只包含正文
            content_parts = []

            # 添加段落
            for p in unique_paragraphs:
                content_parts.append(p)

            # 添加列表项
            for li in unique_list_items:
                content_parts.append(f"• {li}")

            # 合并所有内容
            return "\n\n".join(content_parts)
        else:
            # 尝试更通用的方法提取内容
            # 获取所有段落和列表项,不限定在特定容器内
            paragraphs = [p.get_text().strip() for p in soup.find_all('p') if p.get_text().strip()]
            list_items = [li.get_text().strip() for li in soup.find_all('li') if li.get_text().strip()]
            
            if paragraphs or list_items:
                # 去重处理
                unique_paragraphs = remove_duplicates(paragraphs)
                unique_list_items = remove_duplicates(list_items)
                
                # 构建返回内容
                content_parts = []
                content_parts.extend(unique_paragraphs)
                content_parts.extend([f"• {li}" for li in unique_list_items])
                
                return "\n\n".join(content_parts)
            else:
                return "未找到文章内容"
    
    except Exception as e:
        return f"处理URL时出错: {e}"


def clean_html_content(html):
    """
    清理HTML内容,移除标签但保留文本和表情符号
    注意:此函数在使用BeautifulSoup后不再需要,但保留以兼容旧代码
    
    Args:
        html: HTML内容
    
    Returns:
        清理后的文本内容
    """
    # 使用BeautifulSoup清理HTML
    soup = BeautifulSoup(html, 'lxml')
    # 获取纯文本
    clean_text = soup.get_text()
    # 移除多余空白
    clean_text = re.sub(r'\s+', ' ', clean_text).strip()
    return clean_text


def remove_duplicates(items):
    """
    移除列表中的重复项
    
    Args:
        items: 文本列表
    
    Returns:
        去重后的列表
    """
    unique_items = []
    seen = set()
    
    for item in items:
        # 规范化文本以便更好地去重
        normalized = re.sub(r'\s+', ' ', item).strip()
        # 使用规范化文本作为唯一标识
        if normalized and normalized not in seen:
            seen.add(normalized)
            unique_items.append(item)
    
    return unique_items

def handler(args: Args[Input])->Output:
    url = args.input.url
    content = extract_article_content(url)
    return {"content": content}

运行:

😄 试了好几个url,都能正常获取,接着发布,成功后回到工作流,LLM节点将 工具 替换成我们写的插件:

改下提示词:

接着试运行,成功拿到数据,接着本地跑脚本,发现高频访问还是触发了掘金的反爬,导致获取不到页面内容,🐶 先暂时加了一个错误延时重试,🌚 这个方法可行,但我没留意今天的 1000点 都花完,还在那里跑脚本试。

😳 结果没过多久,手机就收到欠款短信:说我欠款0.07元,我充了0.1元,没隔多久又提示我欠款0.46元,我擦:

🤡 这... 一时没找到设置额度用完就不能用的地方,又怕充完又扣,忍痛又氪了 10块 买了10000点...

2.3. Gitee 文件托管

😄 就是把上面coze解析返回的内容保存成 json文件,发到托管网站,可以 直接通过链接进行访问,比较 简单且免费的就 GithubGitee, 这里选后者,国内访问会顺畅些🐶。Gitee 直接新建一个 公共仓库,接着到 设置→安全设置→个人访问令牌→生成新令牌,只给这三个权限:

提交创建:

这个key要保存好,只会显示一次 !!!同样是忘了只能删掉重建,怎么把文件push到gitee?🤣不需要我们知道,写提示词让Trae代劳就好了:

啪啪啪,立马给我生成:

结果运行报错,复制粘贴给Trae纠错,纠错过程:

Trae 666,修改后自己验证,失败联网搜索,纠正后再验证,👏

提交成功,Gitee仓库也可以看到上传的文件:

接着让Trae完善脚本,调 Coze API,获取响应数据,保存文件,然后push到Git仓库,最终生成代码:

import json
from datetime import datetime
import os
import base64
import time
import asyncio
import aiohttp
from typing import Dict, List, Any, Optional

# 请求Coze工作流
async def fetch_coze_workflow(category: str, session: aiohttp.ClientSession) -> List[Any]:
    """异步请求Coze工作流获取掘金热门文章数据"""
    url = "https://api.coze.cn/v1/workflow/run"
    headers = {
        "Authorization": "Bearer pat_xxxx",
        "Content-Type": "application/json"
    }
    params = {
        "parameters": {
            "category": category
        },
        "workflow_id": "7511534131105251378"
    }
    try:
        async with session.post(url, headers=headers, json=params) as response:
            if response.status == 200:
                data = await response.json()
                print(f"Coze API返回: {data}")  # 打印Coze API的响应
                if data.get('code') == 0:
                    # 从data字段获取output并解析为Python对象
                    output_data = json.loads(data['data'])
                    return output_data.get('output', [])
                return []
            return []
    except Exception as e:
        print(f"调用Coze API失败: {str(e)}")
        return []

# 生成JSON文件
async def gen_json_file(result: List[Any], category: str, category_id: str, json_dir: str) -> None:
    """异步更新JSON文件中的掘金热门文章数据"""
    # 根据分类生成文件名
    category_filename = os.path.join(json_dir, f"{category_id}.json")
    try:
        # 使用异步文件操作库可能更好,但为简单起见,使用loop.run_in_executor执行同步IO操作
        def write_file():
            with open(category_filename, "w", encoding="utf-8") as f:
                json.dump(result, f, ensure_ascii=False, indent=2)
        
        # 在线程池中执行IO操作
        await asyncio.get_event_loop().run_in_executor(None, write_file)
        print(f"成功更新 {category} 分类的掘金热门文章到 {category_filename}")
    except Exception as e:
        print(f"更新JSON文件失败: {str(e)}")
    
# 上传json文件夹到Gitee仓库
async def upload_json_to_gitee(json_dir: str) -> None:
    """异步上传json文件夹到Gitee仓库"""
    # 配置信息(根据用户提供的信息)
    ACCESS_TOKEN = ""
    OWNER = "coder-pig"
    REPO = "juejin_file_save"
    BRANCH = "master"
    REMOTE_DIR = "hot_articles"
    BASE_URL = 'https://gitee.com/api/v5'
    
    # 扫描json目录
    json_files = [f for f in os.listdir(json_dir) if f.endswith('.json')]
    if not json_files:
        print("📂 当前目录下没有找到JSON文件")
        return
    
    print(f"📋 找到 {len(json_files)} 个JSON文件:") 
    for i, file in enumerate(json_files, 1):
        print(f"  {i}. {file}")
    
    # 定义辅助函数
    async def get_file_content(file_path: str) -> Optional[str]:
        """异步获取文件内容并转换为base64编码"""
        try:
            # 使用run_in_executor执行同步IO操作
            def read_file():
                with open(file_path, 'rb') as f:
                    content = f.read()
                    return base64.b64encode(content).decode('utf-8')
            
            return await asyncio.get_event_loop().run_in_executor(None, read_file)
        except FileNotFoundError:
            print(f"文件不存在: {file_path}")
            return None
        except Exception as e:
            print(f"读取文件失败: {e}")
            return None
    
    async def check_file_exists(remote_path: str, session: aiohttp.ClientSession) -> Optional[Dict]:
        """异步检查远程文件是否存在"""
        url = f"{BASE_URL}/repos/{OWNER}/{REPO}/contents/{remote_path}"
        params = {
            'access_token': ACCESS_TOKEN,
            'ref': BRANCH
        }
        
        try:
            async with session.get(url, params=params) as response:
                if response.status == 200:
                    file_info = await response.json()
                    # 如果返回的是列表(目录内容),说明这是目录而不是文件
                    if isinstance(file_info, list):
                        # 空列表说明目录存在但文件不存在
                        return None
                    elif isinstance(file_info, dict) and 'sha' in file_info:
                        # 返回的是文件信息且包含sha字段
                        return file_info
                    else:
                        # 其他情况视为文件不存在
                        return None
                elif response.status == 404:
                    return None
                else:
                    response_text = await response.text()
                    print(f"检查文件存在性时出错: {response.status} - {response_text}")
                    return None
        except Exception as e:
            print(f"检查文件存在性时发生异常: {e}")
            return None
    
    async def upload_file(local_file_path: str, remote_path: str, session: aiohttp.ClientSession, commit_message: Optional[str] = None) -> Dict:
        """异步上传文件到Gitee仓库"""
        # 获取文件内容
        file_content = await get_file_content(local_file_path)
        if not file_content:
            return {'success': False, 'error': '获取文件内容失败'}
        
        # 检查文件是否已存在
        existing_file = await check_file_exists(remote_path, session)
        
        # 打印调试信息
        if existing_file:
            print(f"📝 检测到文件已存在,将进行更新操作")
        else:
            print(f"✨ 文件不存在,将进行新建操作")
        
        # 构建API URL
        url = f"{BASE_URL}/repos/{OWNER}/{REPO}/contents/{remote_path}"
        
        # 生成提交信息
        if not commit_message:
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            action = "更新" if existing_file else "添加"
            commit_message = f"{action}文件 {os.path.basename(local_file_path)} - {timestamp}"
        
        # 构建请求数据
        data = {
            'access_token': ACCESS_TOKEN,
            'content': file_content,
            'message': commit_message,
            'branch': BRANCH
        }
        
        # 如果文件已存在,需要提供sha值
        if existing_file and 'sha' in existing_file:
            data['sha'] = existing_file['sha']
        
        try:
            # 根据文件是否存在选择不同的HTTP方法
            if existing_file and 'sha' in existing_file:
                # 更新已存在的文件,使用PUT方法
                async with session.put(url, json=data) as response:
                    if response.status in [200, 201]:
                        result = await response.json()
                        return {
                            'success': True,
                            'message': f"文件上传成功: {remote_path}",
                            'commit_sha': result.get('commit', {}).get('sha'),
                            'download_url': result.get('content', {}).get('download_url')
                        }
                    else:
                        response_text = await response.text()
                        error_msg = f"上传失败: {response.status} - {response_text}"
                        return {'success': False, 'error': error_msg}
            else:
                # 创建新文件,使用POST方法
                async with session.post(url, json=data) as response:
                    if response.status in [200, 201]:
                        result = await response.json()
                        return {
                            'success': True,
                            'message': f"文件上传成功: {remote_path}",
                            'commit_sha': result.get('commit', {}).get('sha'),
                            'download_url': result.get('content', {}).get('download_url')
                        }
                    else:
                        response_text = await response.text()
                        error_msg = f"上传失败: {response.status} - {response_text}"
                        return {'success': False, 'error': error_msg}
                
        except Exception as e:
            return {'success': False, 'error': f"上传过程中发生异常: {e}"}
    
    # 批量上传文件
    print("🚀 开始批量上传JSON文件到Gitee仓库...")
    results = []
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    
    # 创建一个新的HTTP会话用于上传文件
    async with aiohttp.ClientSession() as session:
        # 创建所有上传任务
        upload_tasks = []
        for file in json_files:
            local_path = os.path.join(json_dir, file)
            remote_path = f"{REMOTE_DIR}/{file}"
            commit_msg = f"更新掘金热门文章JSON数据 - {file} - {timestamp}"
            
            task = upload_file(local_path, remote_path, session, commit_msg)
            upload_tasks.append((file, local_path, remote_path, task))
        
        # 等待所有上传任务完成
        for file, local_path, remote_path, task in upload_tasks:
            result = await task
            results.append({
                'local_path': local_path,
                'remote_path': remote_path,
                'result': result
            })
            
            # 打印结果
            if result['success']:
                print(f"✅ {local_path} -> {remote_path} 上传成功")
            else:
                print(f"❌ {local_path} -> {remote_path} 上传失败: {result['error']}")
    
    # 统计结果
    success_count = sum(1 for r in results if r['result']['success'])
    total_count = len(results)
    print(f"\n📊 上传完成: {success_count}/{total_count} 个文件成功")

async def process_category(category: str, category_id: str, session: aiohttp.ClientSession, json_dir: str) -> None:
    """异步处理单个分类的文章"""
    print(f"开始处理 {category} 分类的文章...")
    result = await fetch_coze_workflow(category, session)
    if result:
        await gen_json_file(result, category, category_id, json_dir)



async def main():
    """主函数,协调所有异步任务"""
    category_map = {
        '综合': '1',
        '后端': '6809637769959178254',
        '前端': '6809637767543259144',
        'Android': '6809635626879549454',
        'iOS': '6809635626661445640',
        '人工智能': '6809637773935378440',
        '开发工具': '6809637771511070734',
        '代码人生': '6809637776263217160',
        '阅读': '6809637772874219534'
    }
    
    # 确保json目录存在
    json_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'json')
    if not os.path.exists(json_dir):
        os.makedirs(json_dir)
    
    # 创建一个共享的HTTP会话
    async with aiohttp.ClientSession() as session:
        # 创建所有分类的任务列表
        tasks = []
        for category, category_id in category_map.items():
            task = process_category(category, category_id, session, json_dir)
            tasks.append(task)
        
        # 并发执行所有任务
        await asyncio.gather(*tasks)
    
    print("所有分类的文章处理完成!")
    # 上传JSON文件到Gitee(异步操作,但在所有文章处理完成后执行)
    await upload_json_to_gitee(json_dir)

if __name__ == "__main__":
    # 运行异步主函数
    asyncio.run(main())

运行结果:

2.4. 嘴遁-🔥静态页面生成术

文档先行,简单写写开发文档:

# 掘金微热榜

开个一个静态的纯前端项目(不依赖Node.js,或是需要编译的框架),用于掘金微热榜的展示。

# 接口API

category_map = {
    '综合': '1',
    '后端': '6809637769959178254',
    '前端': '6809637767543259144',
    'Android': '6809635626879549454',
    'iOS': '6809635626661445640',
    '人工智能': '6809637773935378440',
    '开发工具': '6809637771511070734',
    '代码人生': '6809637776263217160',
    '阅读': '6809637772874219534'
}

不同类别,对应不同的json文件地址,如"综合" 对应 https://gitee.com/coder-pig/juejin_file_save/blob/master/hot_articles/1.json,项目最终会托管到服务器上。请使用这些json文件作为数据源,不要使用假数据!

# 接口返回Json数据样例

```
[{
    "ai_desc": "2025年5月30日,由rolldown驱动的vite,rolldown-vite正式内测。作者用前端项目实测,替换后编译打包4231个文件,操作10次,打包性能提升约2倍,暂未发现问题,可供尝鲜。",
    "ai_title": "实测:下一代Vite速度快一倍",
    "author_id": "1028798615918983",
    "author_name": "粥里有勺糖",
    "collect": 0,
    "comment_count": 0,
    "content_id": "7510477725476339766",
    "content_url": "https://juejin.cn/post/7510477725476339766",
    "like": 1,
    "order": 9,
    "title": "视野修炼-技术周刊第121期 | Rolldown-Vite",
    "view": 79
  }],
  ```

  # 页面需展示的字段

  ai_title (没有的话显示title)、ai_desc、view、like、comment_count、collect、content_url

  # 交互逻辑

  点击文章,打开新标签页跳转到content_url对应的页面

写提示词让 Trae 开干:

以及各种 截图标注,指出问题,让Trae改改改,最终本地预览效果:

本地预览没啥问题,接着发布到 掘金MCP,切下Agent,然后键入 "发布到掘金MCP"

发布成功后,可以点开上面的预览链接进行预览,没啥问题点击右上角的 "发布",依次填下:作品名描述封面对话记录项目Rules 然后发布等审核就行啦,作品链接:🌟 掘金微热榜

3. 小结

🤡 花了差不多2天 (🙂1.5天整Coze+0.5天指挥Trae写页面),仓促弄完,以为很简单一个东西,结果踩坑不少,最受伤的莫过于😭 荷包-20 (都够在 东哥那里喝一月奶茶了😐桑心... )

💁‍♂️ 项目基本支棱起来了,但还存在下面这三个主要问题:

  • 并发执行Coze工作流爬掘金文章太快,会触发反爬,导致获取不到内容🤡,AI 自然生成不了标题和摘要。细水长流,最重要 ,还是改成 同步爬取+长延时策略,慢没关系,热榜数据变化不会太频繁,🐶 而且1000点的额度,一天也就够搞2~3次。
  • Gitee 托管文件不行 🐶,也不知道 json 文件哪里有敏感信息,有几个文件访问几次后就报 "The content may contain violation information",🤷‍♀️ 妥妥需要换一个,🙇大佬 有什么免费/便宜,操作起来简单,不用搞域名备案啥的 文件托管网站, 请务必到评论区安利一波😋~

🤡 浏览器的同源策略 坑了我一把,本地预览正常,部署了却一直加载不出来,我 F12 抓了一波包:

🤡 我以为是这两个文件没有上传上去,还把这两玩意也放到gitee上了,然后通过URL的方式引用,结果还是404,后面群里问大佬,说是我 浏览器问题

🙂 看了下,还真是:

🤔 虽然 404,但也不影响展示,真的不显示的原因还是 "跨域问题",搜了下 Chrome如何关闭CORS策略,试了好几种方法都不行,没咋搞过前端,这玩意我还真不了解,问了下 Trae,给了一个最简单的解决方法——第三方CORS代理服务,通过中间层转发机制,将客户端请求经代理服务器中转至目标服务器,并在响应中添加必要的CORS头信息。用的 api.allorigins.win,🤡 暂时解决了问题~

😃 其它就是一些功能和UI的细化,有什么建议欢迎评论区留言💐~

浏览器指纹-探究前端如何识别用户设备

什么是浏览器指纹?

浏览器指纹,是用来唯一标识你浏览器的一组“特征值”。它不是我们理解中的那种真实指纹,而是通过收集浏览器、操作系统、设备分辨率、字体、插件等信息,组合成的一个独特 ID。

和传统的 Cookie 不同,浏览器指纹不需要在用户设备上存储任何东西,完全是“读取现有信息”来识别用户。

使用背景

在最近的项目中,有个小需求:想用用户的设备作为唯一凭证,来验证身份

一开始我想着简单粗暴点,用 JS 获取手机的 IMEI 或 PC 的序列号。但查了下资料后才发现,这根本行不通——JS 根本没权限访问这些底层硬件信息,安全机制早就把这条路堵死了。

后来才反应过来,我真正想要的,是一个“设备唯一标识”,也就是——浏览器指纹。

可行方案

查阅了一些资料之后,目前比较常见的几种浏览器指纹方案如下:

  • Navigator 指纹:浏览器类型、版本、系统平台等信息。
  • Canvas 指纹:让浏览器绘制一段隐藏的图像,然后读取图像的像素差异,不同设备会有微小区别。
  • WebGL 指纹:利用显卡和图形驱动渲染差异,获取设备的唯一特征。
  • 字体、插件、时区、屏幕分辨率等:这些信息组合起来也能提供一定的识别度。

当然,单一方案识别率可能不高,但多种信息结合后,指纹的唯一性就会明显提升。

Navigator 指纹

Navigator 是前端获取浏览器和部分设备环境信息的重要接口。

下面是一些常用的属性和方法(跨浏览器兼容性较好的为主):

属性/方法 作用说明 示例代码
navigator.userAgent 获取浏览器的用户代理字符串,可以用于判断浏览器类型、系统类型 navigator.userAgent
navigator.platform 获取运行环境的操作系统平台类型(如 Win32、Linux x86_64、MacIntel) navigator.platform
navigator.appVersion 获取浏览器版本信息和部分平台信息 navigator.appVersion
navigator.appName 获取浏览器名称(大多数现代浏览器返回 “Netscape”) navigator.appName
navigator.language 返回当前浏览器的首选语言(如 “zh-CN”、“en-US”) navigator.language
navigator.languages 返回用户的首选语言列表 navigator.languages
navigator.hardwareConcurrency 返回可用的逻辑处理器数量(CPU核心数) navigator.hardwareConcurrency
navigator.plugins 返回当前安装的插件列表(仅桌面浏览器有意义,且有兼容性限制) navigator.plugins
navigator.onLine 判断当前浏览器是否联网 navigator.onLine
navigator.cookieEnabled 判断浏览器是否启用 Cookie navigator.cookieEnabled
navigator.geolocation 提供地理位置定位服务(需要用户授权) navigator.geolocation.getCurrentPosition(...)
navigator.maxTouchPoints 支持的最大触控点个数(触屏设备可用) navigator.maxTouchPoints
navigator.mediaDevices 访问音视频设备管理 API(如获取麦克风、摄像头) navigator.mediaDevices.getUserMedia(...)
navigator.clipboard 读写系统剪贴板(部分浏览器需要 https 环境和权限) navigator.clipboard.writeText("Hello")
navigator.connection 获取网络连接信息对象(如带宽、类型,部分浏览器支持) navigator.connection.effectiveType
navigator.userAgentData 在新标准中可用的一种用户代理信息对象,部分浏览器已支持,用户隐私性更高 navigator.userAgentData

偷个懒,让Tare直接帮我写个Navigator 指纹示例吧。

<!DOCTYPE html>
<html>

<head>
    <title>Navigator 指纹示例</title>
</head>

<body>
    <h2>Navigator 指纹示例</h2>
    <pre id="output"></pre>
    <script>
        async function getNavigatorFingerprint() {
            // 收集 navigator 相关信息
            const data = {
                userAgent: navigator.userAgent,
                platform: navigator.platform,
                language: navigator.language,
                languages: navigator.languages,
                cookieEnabled: navigator.cookieEnabled,
                hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
                deviceMemory: navigator.deviceMemory || 'N/A',
                webdriver: navigator.webdriver || false,
            };
            // 将数据转成字符串
            const dataString = JSON.stringify(data);
            // 计算 SHA-256 哈希
            const hashBuffer = await crypto.subtle.digest(
                "SHA-256",
                new TextEncoder().encode(dataString)
            );
            // 转成十六进制字符串
            const hashArray = Array.from(new Uint8Array(hashBuffer));
            const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
            return { data, fingerprint: hashHex };
        }
        getNavigatorFingerprint().then(result => {
            const output = document.getElementById('output');
            output.textContent =
                "采集到的 Navigator 信息:\n" + JSON.stringify(result.data, null, 2) +
                "\n\n生成的指纹(SHA-256):\n" + result.fingerprint;
        });
    </script>
</body>

</html>

代码生成完毕,点击应用直接预览:

经过测试,在同一个电脑上,这个指纹是稳定的,多次执行,这个值不会变。

但这这个指纹明显有缺陷,我系统语言或者浏览器升级后,这个指纹肯定会改变。

Canvas 指纹

由于不同设备(包括操作系统、显卡、驱动、字体渲染引擎等)在绘制同一段 Canvas 内容时会存在细微差异,最终得到的图像数据(通常是像素或转成 base64)在不同设备上往往是不同的。

这些细微差异生成的哈希值就是“指纹”,由于只与设备性能有关,指纹稳定性显然比Navigator 指纹高一些。

<!DOCTYPE html>
<html>
<head>
    <title>简单Canvas指纹示例</title>
</head>
<body>
    <h2>简单Canvas指纹示例</h2>
    <p>请打开控制台(F12)查看结果</p>

    <script>
        // 创建一个简单的Canvas指纹生成函数
        function generateCanvasFingerprint() {
            // 创建canvas元素
            const canvas = document.createElement('canvas');
            canvas.width = 200;
            canvas.height = 100;
            
            // 获取绘图上下文
            const ctx = canvas.getContext('2d');
            
            // 填充背景
            ctx.fillStyle = 'white';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // 绘制一些图形和文字
            // 绘制红色矩形
            ctx.fillStyle = 'red';
            ctx.fillRect(20, 20, 50, 50);
            
            // 绘制蓝色圆形
            ctx.fillStyle = 'blue';
            ctx.beginPath();
            ctx.arc(120, 45, 25, 0, Math.PI * 2);
            ctx.fill();
            
            // 绘制文本
            ctx.fillStyle = 'black';
            ctx.font = '16px Arial';
            ctx.fillText('Canvas指纹', 60, 80);
            
            // 获取canvas数据URL
            const dataURL = canvas.toDataURL();
            
            // 简单哈希函数
            function simpleHash(str) {
                let hash = 0;
                for (let i = 0; i < str.length; i++) {
                    const char = str.charCodeAt(i);
                    hash = ((hash << 5) - hash) + char;
                    hash = hash & hash; // 转换为32位整数
                }
                return hash.toString(16); // 转换为16进制
            }
            
            // 计算指纹
            const fingerprint = simpleHash(dataURL);
            
            return {
                fingerprint: fingerprint,
                dataURL: dataURL
            };
        }
        
        // 生成并输出指纹
        const result = generateCanvasFingerprint();
        console.log('Canvas指纹:', result.fingerprint);
        console.log('Canvas数据URL前100个字符:', result.dataURL.substring(0, 100) + '...');
        
        // 如果浏览器支持更安全的哈希算法,也可以使用它
        if (window.crypto && window.crypto.subtle) {
            const encoder = new TextEncoder();
            const data = encoder.encode(result.dataURL);
            
            window.crypto.subtle.digest('SHA-256', data)
                .then(hashBuffer => {
                    // 将哈希缓冲区转换为十六进制字符串
                    const hashArray = Array.from(new Uint8Array(hashBuffer));
                    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
                    
                    console.log('Canvas指纹(SHA-256):', hashHex);
                });
        }
    </script>
</body>
</html>

生成的指纹还是很不错的。

其他几种方式生成浏览器指纹都大同小异,这里就不介绍了。

❌