ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

使用 @github/copilot-sdk 编写 Copilot CLI 扩展:工具、Hook 与会话事件实战指南

使用 @github/copilot-sdk 编写 Copilot CLI 扩展:工具、Hook 与会话事件实战指南 使用 github/copilot-sdk 编写 Copilot CLI 扩展工具、Hook 与会话事件实战指南【免费下载链接】copilot-sdkMulti-platform SDK for integrating GitHub Copilot Agent into apps and services项目地址: https://gitcode.com/GitHub_Trending/co/copilot-sdk本文是一份面向 Node.js 开发者的 Copilot CLI 扩展实战指南以github/copilot-sdk的扩展 APIjoinSession为主线完整讲解扩展骨架、自定义工具注册、生命周期 Hook、会话事件订阅、程序化消息发送以及权限/用户输入处理器并给出可直接运行的完整示例。读完本文你将能够基于 nodejs/docs/examples.md 的场景独立编写出能向 CLI 时间线输出日志、拦截危险命令、注入额外上下文、监听文件变更并响应 agent 提问的生产级扩展。前置准备扩展的运行环境在动手之前先明确扩展的定位。Copilot CLI 扩展是一段运行在独立 Node.js 子进程中的代码通过 JSON-RPC 与 CLI 主进程经 stdio 通信。CLI 负责发现并孵化扩展进程扩展则通过 SDK 注册工具、注册 Hook、监听事件详见 扩展工作机制。扩展的入口文件结构约定如下.github/extensions/ my-extension/ extension.mjs ← 入口文件必需且必须是 .mjs仅支持.mjsES Module文件名必须为extension.mjs每个扩展独占一个子目录github/copilot-sdk的导入由 CLI 自动解析无需自行安装。SDK 对 Node.js 版本的要求为^20.19.0或22.12.0见 nodejs/README.md。扩展骨架一切从 joinSession 开始每个扩展的起点都是同一段样板代码——调用扩展 API 的入口joinSessionimport { joinSession } from github/copilot-sdk/extension; const session await joinSession({ hooks: { /* ... */ }, tools: [ /* ... */ ], });joinSession返回一个CopilotSession对象你可以用它发送消息、订阅事件、向时间线写日志。从源码看joinSession的实现位于 nodejs/src/extension.ts它有几个值得注意的底层行为它读取环境变量SESSION_ID若不存在则直接抛出错误提示该 API 仅用于作为 Copilot CLI 子进程运行的扩展它通过_internalConnection: { kind: parent-process }建立与父进程 CLI 的连接并调用client.resumeSessionForExtension(...)挂接到用户当前的前台会话默认权限处理器为defaultJoinSessionPermissionHandler会话恢复事件默认被抑制suppressResumeEvent: true。如果你需要访问被 CLI 剥离的敏感环境变量如GITHUB_TOKEN可以在配置中声明requestedEnvironmentVariables: [GITHUB_TOKEN]。CLI 会向用户展示扩展名与请求的变量清单批准后这些值会在joinSessionresolve 前写入process.env拒绝则joinSessionreject扩展不会加载其工具永远不会到达模型。平台差异Windows vs macOS/Linux扩展很可能需要调用外部命令不同平台差异显著用process.platform win32在运行时检测 Windows剪贴板命令macOS 用pbcopyWindows 用clip在 Windows 上执行code、npx、npm等.cmd脚本时用exec()而不是execFile()PowerShell 的 stderr 重定向用*1而不是21。向 Timeline 输出日志使用session.log()可以在 CLI 时间线中向用户展示消息const session await joinSession({ hooks: { onSessionStart: async () { await session.log(My extension loaded); }, onPreToolUse: async (input) { if (input.toolName bash) { await session.log(Running: ${input.toolArgs?.command}, { ephemeral: true }); } }, }, tools: [], });日志级别支持info默认、warning、error。设置ephemeral: true表示临时消息不会被持久化。从源码看session.log()最终调用的是底层 RPCthis.rpc.log({ message, ...options })见 nodejs/src/session.ts。注册自定义工具工具是 agent 可以调用的函数。每个工具需要名称、描述、JSON Schema 参数以及处理函数handler。基础工具tools: [ { name: my_tool, description: Does something useful, parameters: { type: object, properties: { input: { type: string, description: The input value }, }, required: [input], }, handler: async (args) { return Processed: ${args.input}; }, }, ];调用外部 shell 命令的工具import { execFile } from node:child_process; { name: run_command, description: Runs a shell command and returns its output, parameters: { type: object, properties: { command: { type: string, description: The command to run }, }, required: [command], }, handler: async (args) { const isWindows process.platform win32; const shell isWindows ? powershell : bash; const shellArgs isWindows ? [-NoProfile, -Command, args.command] : [-c, args.command]; return new Promise((resolve) { execFile(shell, shellArgs, (err, stdout, stderr) { if (err) resolve(Error: ${stderr || err.message}); else resolve(stdout); }); }); }, }调用外部 API 的工具{ name: fetch_data, description: Fetches data from an API endpoint, parameters: { type: object, properties: { url: { type: string, description: The URL to fetch }, }, required: [url], }, handler: async (args) { const res await fetch(args.url); if (!res.ok) return Error: HTTP ${res.status}; return await res.text(); }, }工具处理函数的调用上下文handler 的第二个参数携带本次调用的元数据handler: async (args, invocation) { // invocation.sessionId — current session ID // invocation.toolCallId — unique ID for this tool call // invocation.toolName — name of the tool being called return done; };toolCallId在后续区分 agent 编辑与用户编辑的场景中非常关键可以用它把tool.execution_start与tool.execution_complete事件关联起来。Hooks在关键生命周期点拦截与改写Hook 在关键生命周期点拦截并修改行为全部注册在hooks选项中。SDK 类型层面定义的SessionHooks接口见 nodejs/src/types.ts除文档表格中的成员外还包含onPreMcpToolCallMCP 工具调用前、onUserPromptTransformed运行时转换用户提示后、写入历史前、onAgentStop顶层 agent 自然停止时可返回{ decision: block, reason }让 agent 继续执行。可用 Hooks 一览Hook触发时机可修改内容onUserPromptSubmitted用户发送消息提示文本、追加上下文onPreToolUse工具执行前工具参数、权限决策、追加上下文onPostToolUse工具成功执行后工具结果、追加上下文onPostToolUseFailure工具执行返回失败后向模型追加隐藏指引onSessionStart会话开始或恢复追加上下文onSessionEnd会话结束清理动作、摘要onErrorOccurred发生错误时错误处理策略retry/skip/abort所有 hook 的输入都包含timestampDate类型和workingDirectory。源码中的BaseHookInput见 nodejs/src/types.ts还额外提供sessionId字段。改写用户消息在 agent 看到用户输入前用onUserPromptSubmitted重写或增强hooks: { onUserPromptSubmitted: async (input) { // Rewrite the prompt return { modifiedPrompt: input.prompt.toUpperCase() }; }, }向每条消息注入额外上下文返回additionalContext可以静默追加 agent 会遵循的指令hooks: { onUserPromptSubmitted: async (input) { return { additionalContext: Always respond in bullet points. Follow our team coding standards., }; }, }类型定义中UserPromptSubmittedHookOutput除modifiedPrompt、additionalContext外还支持suppressOutput见 nodejs/src/types.ts。基于关键字发送跟进消息用session.send()程序化注入一条新的用户消息hooks: { onUserPromptSubmitted: async (input) { if (/\burgent\b/i.test(input.prompt)) { // Fire-and-forget a follow-up message setTimeout(() session.send({ prompt: Please prioritize this. }), 0); } }, }提示如果跟进消息可能再次触发同一个 hook务必做好防护避免无限循环。阻止危险的工具调用用onPreToolUse检查并可选地拒绝工具执行。返回值支持permissionDecision: allow | deny | ask并可用permissionDecisionReason向模型说明原因见 nodejs/src/types.tshooks: { onPreToolUse: async (input) { if (input.toolName bash) { const cmd String(input.toolArgs?.command || ); if (/rm\s-rf/i.test(cmd) || /Remove-Item\s.*-Recurse/i.test(cmd)) { return { permissionDecision: deny, permissionDecisionReason: Destructive commands are not allowed., }; } } // Allow everything else return { permissionDecision: allow }; }, }在工具执行前修改参数hooks: { onPreToolUse: async (input) { if (input.toolName bash) { const redirect process.platform win32 ? *1 : 21; return { modifiedArgs: { ...input.toolArgs, command: ${input.toolArgs.command} ${redirect}, }, }; } }, }注意 Windows 下 stderr 重定向要用 PowerShell 的*1语法——这正是扩展开发中小平台差异大影响的典型例子。响应 agent 创建或编辑文件onPostToolUse在工具成功完成后触发可执行副作用如用 VS Code 打开文件import { exec } from node:child_process; hooks: { onPostToolUse: async (input) { if (input.toolName create || input.toolName edit) { const filePath input.toolArgs?.path; if (filePath) { // Open the file in VS Code exec(code ${filePath}, () {}); } } }, }响应工具失败onPostToolUse只对成功的工具执行触发。要观察或响应失败注册onPostToolUseFailure。其输入包含input.error字符串化的失败信息返回值中只有additionalContext会被运行时消费并作为隐藏指引与失败的工具结果一起追加给模型对应类型 nodejs/src/types.tshooks: { onPostToolUseFailure: async (input) { if (input.toolName bash) { return { additionalContext: The command failed. Try a different approach., }; } }, }从源码注释还可以了解到rejected、denied、timeout等结果目前同样不会触发此 hook——只有failure会。每次文件编辑后运行 linterimport { exec } from node:child_process; hooks: { onPostToolUse: async (input) { if (input.toolName edit) { const filePath input.toolArgs?.path; if (filePath?.endsWith(.ts)) { const result await new Promise((resolve) { exec(npx eslint ${filePath}, (err, stdout) { resolve(err ? stdout : No lint errors.); }); }); return { additionalContext: Lint result: ${result} }; } } }, }带重试逻辑的错误处理hooks: { onErrorOccurred: async (input) { if (input.recoverable input.errorContext model_call) { return { errorHandling: retry, retryCount: 2 }; } return { errorHandling: abort, userNotification: An error occurred: ${input.error}, }; }, }类型层面ErrorOccurredHookInput的errorContext取值包括model_call | tool_execution | system | user_inputErrorOccurredHookOutput支持errorHandling: retry | skip | abort、retryCount、userNotification与suppressOutput见 nodejs/src/types.ts。会话生命周期 Hookhooks: { onSessionStart: async (input) { // input.source is startup, resume, or new return { additionalContext: Remember to write tests for all changes. }; }, onSessionEnd: async (input) { // input.reason is complete, error, abort, timeout, or user_exit }, }SessionStartHookInput还包含可选的initialPromptSessionEndHookInput提供finalMessage与error返回值支持cleanupActions与sessionSummary见 nodejs/src/types.ts。会话事件实时响应 agent 的动态调用joinSession之后用session.on()实时响应事件。监听特定事件类型session.on(assistant.message, (event) { // event.data.content has the agents response text });监听全部事件session.on((event) { // event.type and event.data are available for all events });取消订阅session.on()返回退订函数const unsubscribe session.on(tool.execution_complete, (event) { // event.data.success, event.data.result, event.data.error }); // Later, stop listening unsubscribe();示例自动把 agent 回复复制到剪贴板结合一个 hook检测关键字与一个会话事件捕获回复import { execFile } from node:child_process; let copyNextResponse false; function copyToClipboard(text) { const cmd process.platform win32 ? clip : pbcopy; const proc execFile(cmd, [], () {}); proc.stdin.write(text); proc.stdin.end(); } const session await joinSession({ hooks: { onUserPromptSubmitted: async (input) { if (/\bcopy\b/i.test(input.prompt)) { copyNextResponse true; } }, }, tools: [], }); session.on(assistant.message, (event) { if (copyNextResponse) { copyNextResponse false; copyToClipboard(event.data.content); } });最常用的 10 种事件类型事件类型描述关键数据字段assistant.messageagent 的最终回复content,messageId,toolRequestsassistant.message_delta消息内容分块临时deltaContenttool.execution_start工具即将执行toolCallId,toolName,argumentstool.execution_complete工具执行完成toolCallId,success,result,erroruser.message用户发送了消息content,attachments,sourcesession.idle会话完成一轮处理abortedsession.error发生错误errorType,message,stackpermission.requestedagent 需要权限shell、文件写入等requestId,permissionRequest.kindsession.shutdown会话即将结束shutdownType,totalPremiumRequests,codeChangesassistant.turn_startagent 开始新的思考/响应周期turnId示例检测 plan 文件被创建或编辑用session.workspacePath定位会话的plan.md启用 infinite sessions 时工作区目录通常形如~/.copilot/session-state/id再配合fs.watchFile检测变化。用toolCallId关联tool.execution_start/tool.execution_complete事件以区分 agent 编辑与用户编辑import { existsSync, watchFile, readFileSync } from node:fs; import { join } from node:path; import { joinSession } from github/copilot-sdk/extension; const agentEdits new Set(); // toolCallIds for in-flight agent edits const recentAgentPaths new Set(); // paths recently written by the agent const session await joinSession(); const workspace session.workspacePath; // e.g. ~/.copilot/session-state/id if (workspace) { const planPath join(workspace, plan.md); let lastContent existsSync(planPath) ? readFileSync(planPath, utf-8) : null; // Track agent edits to suppress false triggers session.on(tool.execution_start, (event) { if ( (event.data.toolName edit || event.data.toolName create) String(event.data.arguments?.path || ).endsWith(plan.md) ) { agentEdits.add(event.data.toolCallId); recentAgentPaths.add(planPath); } }); session.on(tool.execution_complete, (event) { if (agentEdits.delete(event.data.toolCallId)) { setTimeout(() { recentAgentPaths.delete(planPath); lastContent existsSync(planPath) ? readFileSync(planPath, utf-8) : null; }, 2000); } }); watchFile(planPath, { interval: 1000 }, () { if (recentAgentPaths.has(planPath) || agentEdits.size 0) return; const content existsSync(planPath) ? readFileSync(planPath, utf-8) : null; if (content lastContent) return; const wasCreated lastContent null content ! null; lastContent content; if (content ! null) { session.send({ prompt: The plan was ${wasCreated ? created : edited} by the user., }); } }); }这里把workspacePath的用途体现得很直观CopilotSession.workspacePath仅在启用 infinite sessions 时存在指向包含checkpoints/、plan.md、files/子目录的工作区对应 nodejs/src/session.ts 的 getter。示例响应用户在仓库中手动编辑文件对process.cwd()使用fs.watch的recursive: true检测文件变更并通过跟踪tool.execution_start/tool.execution_complete事件过滤掉 agent 自身的编辑import { watch, readFileSync, statSync } from node:fs; import { join, relative, resolve } from node:path; import { joinSession } from github/copilot-sdk/extension; const agentEditPaths new Set(); const session await joinSession(); const cwd process.cwd(); const IGNORE new Set([node_modules, .git, dist]); // Track agent file edits session.on(tool.execution_start, (event) { if (event.data.toolName edit || event.data.toolName create) { const p String(event.data.arguments?.path || ); if (p) agentEditPaths.add(resolve(p)); } }); session.on(tool.execution_complete, (event) { // Clear after a delay to avoid race with fs.watch const p [...agentEditPaths].find((x) x); // any tracked path setTimeout(() agentEditPaths.clear(), 3000); }); const debounce new Map(); watch(cwd, { recursive: true }, (eventType, filename) { if (!filename || eventType ! change) return; if (filename.split(/[\\\/]/).some((p) IGNORE.has(p))) return; if (debounce.has(filename)) clearTimeout(debounce.get(filename)); debounce.set(filename, setTimeout(() { debounce.delete(filename); const fullPath join(cwd, filename); if (agentEditPaths.has(resolve(fullPath))) return; try { if (!statSync(fullPath).isFile()) return; } catch { return; } const relPath relative(cwd, fullPath); session.send({ prompt: The user edited \${relPath}\., attachments: [{ type: file, path: fullPath }], }); }, 500)); });程序化发送消息即发即弃Fire-and-forgetawait session.send({ prompt: Analyze the test results. });发送并等待回复const response await session.sendAndWait({ prompt: What is 2 2? }); // response?.data.content contains the agents reply带文件附件发送await session.send({ prompt: Review this file, attachments: [{ type: file, path: ./src/index.ts }], });session.send()返回消息 ID选项还支持sourceuser、system或agent-id溯源与modeenqueue/immediate投递模式等字段sendAndWait()额外接受毫秒级timeout返回最终的 assistant 消息事件未收到时返回undefined详见 nodejs/README.md 的CopilotSession章节。权限与用户输入处理器自定义权限逻辑通过onPermissionRequest注入自定义审批逻辑。该选项也可以用于client.createSessionSDK 主入口而非仅仅扩展场景const session await joinSession({ onPermissionRequest: async (request) { if (request.kind shell) { // request.fullCommandText has the shell command return { kind: approve-once }; } if (request.kind write) { return { kind: approve-once }; } return { kind: reject }; }, });request.kind用于区分操作类型shell、write、read、mcp、custom-tool、url、memory、hook等。审批结果支持approve-once、approve-for-session、approve-for-location等作用域详见 nodejs/README.md 的 Permission Handling 章节。处理 agent 提问ask_user注册onUserInputRequest以启用 agent 的ask_user工具const session await joinSession({ onUserInputRequest: async (request) { // request.question has the agents question // request.choices has the options (if multiple choice) return { answer: yes, wasFreeform: false }; }, });完整示例多特性扩展以下扩展把工具、Hook 与事件三者结合遇到 copy this 关键字时自动复制下一条回复为每条消息注入团队规范拦截危险 shell 命令文件创建/编辑后自动在编辑器中打开并提供一个copy_to_clipboard自定义工具import { execFile, exec } from node:child_process; import { joinSession } from github/copilot-sdk/extension; const isWindows process.platform win32; let copyNextResponse false; function copyToClipboard(text) { const proc execFile(isWindows ? clip : pbcopy, [], () {}); proc.stdin.write(text); proc.stdin.end(); } function openInEditor(filePath) { if (isWindows) exec(code ${filePath}, () {}); else execFile(code, [filePath], () {}); } const session await joinSession({ hooks: { onUserPromptSubmitted: async (input) { if (/\bcopy this\b/i.test(input.prompt)) { copyNextResponse true; } return { additionalContext: Follow our team style guide. Use 4-space indentation., }; }, onPreToolUse: async (input) { if (input.toolName bash) { const cmd String(input.toolArgs?.command || ); if (/rm\s-rf\s\//i.test(cmd) || /Remove-Item\s.*-Recurse/i.test(cmd)) { return { permissionDecision: deny, permissionDecisionReason: Destructive commands are not allowed., }; } } }, onPostToolUse: async (input) { if (input.toolName create || input.toolName edit) { const filePath input.toolArgs?.path; if (filePath) openInEditor(filePath); } }, }, tools: [ { name: copy_to_clipboard, description: Copies text to the system clipboard., parameters: { type: object, properties: { text: { type: string, description: Text to copy }, }, required: [text], }, handler: async (args) { return new Promise((resolve) { const proc execFile(isWindows ? clip : pbcopy, [], (err) { if (err) resolve(Error: ${err.message}); else resolve(Copied to clipboard.); }); proc.stdin.write(args.text); proc.stdin.end(); }); }, }, ], }); session.on(assistant.message, (event) { if (copyNextResponse) { copyNextResponse false; copyToClipboard(event.data.content); } }); session.on(tool.execution_complete, (event) { // event.data.success, event.data.result });延伸阅读扩展工作机制与文件结构Discovery、Launch、Connection、Registration、Lifecycle 全流程以及requestedEnvironmentVariables的敏感变量授权机制Node.js SDK 完整 API 参考CopilotClient/CopilotSession、事件类型、流式输出、自定义 Provider、系统消息定制等Agent Factories 扩展authoring、running、resuming、observing Agent FactoriesAgent 程序化编写扩展面向 agent 的分步工作流交互式 Chat 示例使用CopilotClient主 SDK API 的完整可运行聊天程序Hook 类型定义SessionHooks接口与全部 Hook 输入/输出类型的权威定义joinSession 实现扩展入口的实际连接逻辑。【免费下载链接】copilot-sdkMulti-platform SDK for integrating GitHub Copilot Agent into apps and services项目地址: https://gitcode.com/GitHub_Trending/co/copilot-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表