ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cursor 高级指南(七):用 Hooks 扩展 Agent 循环,Canvas 可视化交付实战

Cursor 高级指南(七):用 Hooks 扩展 Agent 循环,Canvas 可视化交付实战 1. 为什么要在 React 项目里给 Agent 装 Hooks 和 CanvasCursor 的 Agent 循环默认是个黑盒它读文件、跑命令、改代码你在聊天窗口里只能看到结果摘要。项目一大问题就来了——Agent 改了哪些文件、中间产物长什么样、某一步为什么被跳过全靠翻 transcript 猜。我试过在一个中型 React 后台项目里让 Agent 连续重构十几个组件结果它中途把某个工具调用静默失败了聊天里只显示「已完成」实际有四个文件没动。Hooks 解决的正是这个「确定性」问题。Rules 和 Skills 管的是 Agent 知道什么、会做什么而 Hooks 管的是在 Agent 循环的每个关键节点上一定会发生什么。它通过 JSON 配置挂载 shell 脚本脚本用 stdin 收 JSON、stdout 回 JSON和 Cursor 双向通信不依赖模型判断。也就是说格式化、审计、权限拦截这类操作只要配了 Hook就一定会执行。Canvas 补的是交付形态。当 Agent 的分析结果是一张变更热力图、一份审计清单、一个指标仪表盘时塞进聊天 Markdown 里既难读也难交互。Canvas 是一个.canvas.tsx文件本质是独立渲染的 React 面板出现在聊天旁边还能 Publish 成团队可访问的链接。这篇要做的是把两者串起来用 Hooks 拦截 Agent 执行节点把中间产物实时写到一个 JSON 文件里再用一个 Canvas 组件读取并渲染成可视化面板。适合已经在用 Cursor 做 React 开发、想让 Agent 流程可控可观测的同学。下面所有配置和代码都可以直接复制到项目里跑。2. TaoToken 前置给 Agent 循环准备一个稳定的模型入口Hooks 和 Canvas 本身是 Cursor 的本地能力但 Agent 循环背后要调模型。如果你在团队里做工程化落地模型入口的稳定性直接决定 Hook 触发是否连续——模型请求一断Agent 循环就停在中途stop事件可能都不触发你的可视化面板就永远等不到数据。我现在的做法是把模型调用统一走 TaoToken。它的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 这个地址不加 UTM 参数。对 Cursor 这类工具来说你只需要在模型配置里填好 base URL 和 API KeyAgent 循环的每一次工具调用、每一轮推理都走这个入口。具体操作路径是这样先到控制台创建密钥地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面生成一个 key页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。拿到 key 之后如果你要验证模型是否通可以直接在模型对话页面试一条地址 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。这里有个关键点Hooks 脚本里如果要读环境变量别把 key 硬编码进.cursor/hooks.json那个文件是随仓库提交的。正确做法是写进本地.env或者系统环境变量Hook 脚本里用$TAOTOKEN_API_KEY引用。后面第 3 节的配置骨架里我会标出这个位置。如果你打算长期跑编码类 Agent 任务比如让 Agent 连续重构、批量生成组件可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合这种高频、长链路的循环场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 配置细节以文档为准。3. 可复制配置Hooks 骨架 Canvas 组件代码这一节是全文的核心分三块Hook 配置 JSON、Hook 脚本、Canvas 组件。目标是把 Agent 每次文件编辑和工具调用记录到一个agent-events.jsonCanvas 读它渲染。3.1 Hook 配置 JSON在项目根目录建.cursor/hooks.json内容如下。这里挂了三个事件afterFileEdit记录文件变更postToolUse记录工具调用stop在 Agent 循环结束时打一个收尾标记。{ version: 1, hooks: { afterFileEdit: [ { command: ./hooks/record-event.sh file_edit, timeout: 10 } ], postToolUse: [ { command: ./hooks/record-event.sh tool_use, timeout: 10 } ], stop: [ { command: ./hooks/record-event.sh agent_stop, timeout: 10 } ] } }字段说明version当前固定为 1hooks下每个事件名映射一个命令数组command是脚本路径加参数timeout防止 Hook 卡死阻塞 Agent。同一个事件可以挂多个 Hook按配置顺序执行。3.2 Hook 脚本把中间产物写进 JSON建hooks/record-event.sh记得chmod x。脚本从 stdin 读 Cursor 传来的 JSON追加一条带时间戳的记录到.cursor/agent-events.json。#!/bin/bash set -euo pipefail EVENT_TYPE${1:-unknown} PROJECT_DIR${CURSOR_PROJECT_DIR:-$(pwd)} EVENTS_FILE$PROJECT_DIR/.cursor/agent-events.json mkdir -p $(dirname $EVENTS_FILE) [ -f $EVENTS_FILE ] || echo [] $EVENTS_FILE INPUT$(cat) python3 - $EVENT_TYPE $EVENTS_FILE PY import json, sys, time, os event_type sys.argv[1] events_file sys.argv[2] raw sys.stdin.read() if not sys.stdin.isatty() else try: payload json.loads(raw) if raw.strip() else {} except json.JSONDecodeError: payload {raw: raw[:500]} record { type: event_type, ts: int(time.time() * 1000), payload: payload, } with open(events_file, r, encodingutf-8) as f: try: data json.load(f) except json.JSONDecodeError: data [] data.append(record) f.seek(0) f.truncate() json.dump(data, f, ensure_asciiFalse, indent2) print(json.dumps({continue: True})) PY注意最后一行print(json.dumps({continue: True}))这是回给 Cursor 的 stdout表示 Hook 执行成功、Agent 可以继续。如果你要做拦截比如beforeShellExecution里判断命令危险就返回{continue: false, reason: ...}。脚本里用到了CURSOR_PROJECT_DIR这个环境变量Cursor 会自动注入指向当前工作区根目录。其他可用变量还有CURSOR_VERSION、CURSOR_USER_EMAIL、CURSOR_TRANSCRIPT_PATH。3.3 Canvas 组件读取事件并可视化建agent-dashboard.canvas.tsx。Canvas 组件只从cursor/canvas包导入 UI 组件数据必须内联或从本地文件读不支持运行时网络请求。这里我们直接读上面生成的 JSON。import { Canvas, Chart, Table, Card } from cursor/canvas; import eventsData from ./.cursor/agent-events.json; type AgentEvent { type: string; ts: number; payload: Recordstring, unknown; }; const events eventsData as AgentEvent[]; const fileEdits events.filter((e) e.type file_edit); const toolUses events.filter((e) e.type tool_use); const stops events.filter((e) e.type agent_stop); const timeline events.map((e) ({ time: new Date(e.ts).toLocaleTimeString(), type: e.type, detail: JSON.stringify(e.payload).slice(0, 80), })); export default function AgentDashboard() { return ( Canvas titleAgent 循环可视化 Card title事件统计 Table columns{[事件类型, 次数]} rows{[ [文件编辑, String(fileEdits.length)], [工具调用, String(toolUses.length)], [循环结束, String(stops.length)], ]} / /Card Card title事件时间线 Table columns{[时间, 类型, 详情]} rows{timeline.map((t) [t.time, t.type, t.detail])} / /Card Card title文件编辑分布 Chart typebar data{fileEdits.map((e) ({ label: String(e.payload.file_path ?? unknown), value: 1, }))} / /Card /Canvas ); }组件里Chart、Table、Card都来自cursor/canvas具体可用组件以你本地版本为准。数据是构建时静态导入的所以每次 Hook 写入新事件后重新打开 Canvas 就能看到更新。4. 验证请求确认 Hook 触发与 Canvas 同步配置写完得验证两件事Hook 到底有没有被触发Canvas 有没有读到数据。第一步确认脚本可执行。在项目根目录跑chmod x hooks/record-event.sh echo {file_path:src/App.tsx} | ./hooks/record-event.sh file_edit cat .cursor/agent-events.json如果输出里出现一条type: file_edit的记录说明脚本本身没问题。第二步触发真实 Agent 循环。在 Cursor 里让 Agent 做一个最小改动比如「把 src/App.tsx 里的标题改成 Hello Agent」。Agent 执行文件编辑后afterFileEdit应该被触发。再让它跑一条命令比如「运行 npm run lint」postToolUse应该被触发。第三步检查事件文件。再次cat .cursor/agent-events.json你应该看到至少三条记录file_edit、tool_use、agent_stop。如果只有前两条没有agent_stop说明 Agent 循环没正常结束可能是模型请求中断了——这时候回到第 2 节检查你的模型入口配置。第四步打开 Canvas。在 Cursor 命令面板运行Open Canvas选择agent-dashboard.canvas.tsx。面板应该显示事件统计表、时间线表和文件编辑柱状图。如果表格是空的说明 JSON 导入路径不对检查.cursor/agent-events.json是否在组件同级目录下。第五步验证同步。让 Agent 再改一个文件重新打开 Canvas统计数字应该增加。这一步能确认 Hook 写入和 Canvas 读取是联动的。5. 本篇常见错排查Hook 不触发最常见原因是.cursor/hooks.json位置不对。它必须在项目根目录的.cursor/下不是用户目录。另外检查command路径是相对项目根目录的./hooks/record-event.sh前面那个./不能省。脚本报 permission denied忘了chmod x。在 macOS/Linux 上必须给执行权限Windows 下建议用 Git Bash 或 WSL 跑。stdin 读不到内容有些事件类型传的 JSON 字段不一样脚本里做了try/except兜底。如果payload一直是空的打印一下原始输入调试在脚本里临时加echo $INPUT /tmp/hook-debug.log。Canvas 打不开或白屏Canvas 只从cursor/canvas导入组件别用第三方 UI 库。数据必须内联或静态导入不支持fetch。如果导入 JSON 报错确认tsconfig里开了resolveJsonModule。事件文件越来越大agent-events.json会随会话累积。生产项目里建议在sessionStartHook 里做一次归档把旧文件移到.cursor/archive/下避免 Canvas 加载过慢。Hook 阻塞 Agenttimeout设太小会导致脚本被 kill设太大又可能卡住循环。文件记录类 Hook 给 10 秒足够涉及网络或重命令的给 30 秒。脚本里避免同步等待外部服务。模型请求中断导致 stop 不触发这是循环层面的问题不是 Hook 本身。检查你的模型入口是否稳定必要时换用更稳的接入方式参考第 2 节的配置路径。6. 把 Agent 循环变成可观测的工程流水线Hooks 加 Canvas 这套组合本质是把 Agent 从「聊天工具」变成「可观测的工程流水线」。你可以在afterFileEdit里接 ESLint 自动修复在beforeShellExecution里拦截危险命令在stop里触发一次完整测试所有中间产物都落到 JSON再由 Canvas 渲染成团队能看懂的面板。下一步可以做的扩展把agent-events.json按会话分文件Canvas 里加一个会话选择器或者用afterAgentResponseHook 把每轮响应摘要也记进去时间线会更完整。如果你想让 Agent 长时间跑批量任务模型入口的稳定性是前提Coding Plan 那类长期方案更适合这种场景配置参考 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入细节和参数以官方文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 为准别照搬博客里的过期字段。最后提醒一句Hook 脚本里永远不要硬编码 API Key.cursor/hooks.json是随仓库走的。密钥放本地环境变量脚本里用变量引用这是团队协作的底线。
返回列表