
Cursor AI 可以秒级扫描整个代码仓库并给出逐行建议,但大多数工程师只把它当普通自动补全用。这个思路等于把一把瑞士军刀当指甲刀,太浪费了。今天聊聊怎么把这工具的真正能力释放出来,在 Serverless Lambda 环境下做全上下文代码生成。
为什么重要 – LLM(大语言模型)本质上是个统计引擎,根据它见过的所有内容来预测下一个 token(词或符号)。如果你只喂给它一个文件,它只能基于这个文件的局部符号瞎猜。给它整个仓库,模型就能看到模块之间的关系、共享类型和项目级约定。打个比方,就像一个侦探在写报告前把整本案卷都读完了,而不是只看了最后一页。
关键术语
怎么让 Cursor 读取整个仓库 – Cursor 的 SDK 提供了一个叫 uploadRepoTree 的辅助方法。它会遍历目录、读取每个文件,然后发送一个压缩快照给服务端。服务端随后在内部构建上下文,这样后续每次 suggest 调用都能引用任意文件。
import { Cursor } from "cursor"; /** * Send an entire repository to Cursor so it can build a global view. * @param repoPath Absolute path on the Lambda's /tmp storage where the repo lives. * @returns A repoId that you'll use for later suggestion calls. */
async function uploadWholeRepo(repoPath: string): Promise<string> { const cursor = new Cursor({ apiKey: process.env.CURSOR_API_KEY! }); // `uploadRepoTree` recursively reads files, strips binaries, and returns an identifier. const { repoId } = await cursor.uploadRepoTree({ root: repoPath, // optional: ignore patterns (node_modules, .git, etc.) ignore: ["node_modules/**", ".git/**"], }); console.log(`✅ Uploaded repo, got repoId=${repoId}`); return repoId;
}
说人话 – 一次性把整个仓库传上去,等于给 AI 开了“上帝视角”,它能给出的建议是尊重整体架构的,而不是孤零零的代码片段。
为什么这个配置很关键 – Lambda 运行在受限环境中(/tmp 空间有限,冷启动时间短)。如果你打包 SDK 的方式不对,可能会碰到 require(esm) 坑:Node 22 对 ESM 模块的处理方式不同,某些老旧的 Lambda 层会静默加载失败,导致运行时错误但日志里根本看不出来。
一步步打包
mkdir cursor-lambda && cd cursor-lambda
npm init -y
# Pin exact versions; these are the ones verified to work with Node 22 on Lambda
npm install cursor@2.4.1 @aws-sdk/client-lambda@3.560.0
package.json 里加 type: "module",让 Node 把你的代码当作 ESM(ES 模块)处理,与 Cursor SDK 保持一致。{ "name": "cursor-lambda", "version": "1.0.0", "type": "module", // <-- 告诉 Node 使用 ESM 的 import 语法 "dependencies": { "cursor": "2.4.1", "@aws-sdk/client-lambda": "3.560.0" }
}
node_modules。Tip – 保持 zip 包在 50 MB 以下。如果超过这个大小,用 Lambda Layers 来管理 SDK,不要直接打包进去。
diagnostics_channel 流式接收建议为什么流式处理有用 – Cursor 可以一边生成建议一边返回,而不是等完整响应。在一个 PR 助手场景里,你想尽快开始推送反馈来保持对话流畅。diagnostics_channel 是 Node 内置的功能,让你可以监听 Cursor SDK 发出的自定义事件,而不污染自己的代码。
关键术语
启用通道 – Cursor SDK 会为每个生成的 token 发出一条叫 "cursor.suggestion" 的通道消息。在 Lambda 顶部订阅一次,后续每次 suggest 调用都会往同一个通道推事件。
import { createChannel, channel } from "node:diagnostics_channel"; /** * Subscribe to the "cursor.suggestion" channel. * Every time Cursor generates a piece of a suggestion, this listener runs. */
function startSuggestionStream(repoId: string, filePath: string) { const suggestionChannel = channel("cursor.suggestion"); // The listener receives an object with `repoId`, `filePath`, and the `text` chunk. suggestionChannel.subscribe((msg) => { if (msg.repoId !== repoId || msg.filePath !== filePath) return; // For demo purposes we just log; in production you would buffer and send later. console.log(`🧩 Received chunk for ${filePath}: ${msg.text}`); });
}
说人话 – 把通道想象成对讲机:SDK 说话,你的代码听着,每收到一段对话就能实时处理。
为什么必须自己处理 – Cursor 有每分钟 token 配额限制。超过时服务端会返回 HTTP 429(请求过多),响应头里带一个 Retry-After 告诉你等几秒。SDK 默认会自动重试三次。在 Lambda 里这个隐藏的重试会把执行时间拉长到超时,你在 CloudWatch 里看到的延迟尖峰会像是“随机慢调用”。
关闭自动重试 – 创建客户端时传入 { retry: false },然后自己处理 CursorRateLimitError。
import { Cursor, CursorRateLimitError } from "cursor"; /** * Create a Cursor client that does *not* automatically retry. */
function makeCursorClient(): Cursor { return new Cursor({ apiKey: process.env.CURSOR_API_KEY!, // Turn off the SDK's built-in retry logic. retry: false, });
} /** * Wrapper that calls cursor.suggest and deals with 429 errors. */
async function safeSuggest( client: Cursor, params: Parameters<Cursor["suggest"]>[0]
): Promise<void> { try { await client.suggest(params); } catch (err) { if (err instanceof CursorRateLimitError) { // The error object contains the raw `Retry-After` header value. const waitSec = Number(err.retryAfter); console.warn(`⚠️ Hit rate limit, waiting ${waitSec}s before retry`); // Simple back-off – Lambda can `await` a timeout. await new Promise((r) => setTimeout(r, waitSec * 1000)); // Retry once manually; you could add exponential back-off here. await client.suggest(params); } else { // Re-throw unknown errors so Lambda records a failure. throw err; } }
}
Tip – 每次遇到 429 都把
retryAfter值记下来。跑一周你就能发现规律(比如每次 CI 集中跑的时候每隔 10 秒就撞一次),然后调整 webhook 的触发频率。
这个例子的价值 – 演示了完整流程:
/tmp,把整个目录树上传给 Cursor。suggest,通过 diagnostics_channel 流式接收结果。完整 handler(TypeScript,注释很详细)。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { execSync } from "node:child_process";
import { promises as fs } from "node:fs";
import { join } from "node:path";
import { Cursor } from "cursor";
import { LambdaClient, InvokeCommand,
} from "@aws-sdk/client-lambda";
import { channel } from "node:diagnostics_channel"; /** * Helper: clone the repo to the Lambda's /tmp directory. * Git is available in the Lambda runtime (Amazon Linux). */
async function cloneRepo(repoUrl: string, commitSha: string): Promise<string> { const dest = join("/tmp", "repo"); // Clean up any previous run. await fs.rm(dest, { recursive: true, force: true }); execSync(`git clone ${repoUrl} ${dest}`, { stdio: "ignore" }); execSync(`git -C ${dest} checkout ${commitSha}`, { stdio: "ignore" }); return dest;
} /** * Helper: post a comment to GitHub (simplified; in production use Octokit). */
async function postGitHubComment( owner: string, repo: string, prNumber: number, body: string
) { const token = process.env.GITHUB_TOKEN!; const url = `https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`; const payload = JSON.stringify({ body }); execSync( `curl -s -X POST -H "Authorization: token ${token}" -H "Content-Type: application/json" -d '${payload}' ${url}` );
} /** * Main Lambda entry point – receives the GitHub webhook payload. */
export const handler = async ( event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => { // ----------------------------------------------------------------- // 1️⃣ Extract useful data from the webhook. // ----------------------------------------------------------------- const payload = JSON.parse(event.body ?? "{}"); const pr = payload.pull_request; const repoUrl