Skip to main content
Select language: current language is Simplified Chinese
搜索或询问 Copilot
打开菜单

使用挂钩

使用钩子自定义 Copilot SDK 会话的行为。

谁可以使用此功能?

GitHub Copilot SDK 适用于所有 Copilot 计划。

注意

          Copilot SDK 当前处于 公共预览版. 功能和可用性可能会发生更改。

挂钩使你能够将自定义逻辑插入会话的每个阶段 Copilot SDK (从启动的那一刻起,到用户提示和工具调用)到它结束的那一刻。 可以使用挂钩来实现权限、审核、通知等,而无需修改核心代理行为。

概述

挂钩是创建会话时注册一次的回调。 SDK 在会话生命周期中定义完善的点调用它,传递上下文输入,并选择性地接受修改会话行为的输出。 有关会话流的详细序列图,请参阅 github/copilot-sdk 存储库

挂钩当它触发时你能做什么
onSessionStart会话开始(新建会话或恢复会话)注入上下文,加载首选项
onUserPromptSubmitted用户发送消息重写提示,添加上下文,筛选输入
onPreToolUse在工具执行之前允许、拒绝或修改调用
onPostToolUse工具返回后转换结果,屏蔽机密,审核
onSessionEnd会话结束清理、记录度量指标
onErrorOccurred已引发错误自定义日志记录、重试逻辑、警报

所有挂钩都是可选的,您只需注册所需要的挂钩。 从任何挂钩中返回 null(或该语言的等效表达)会告知 SDK 继续执行默认行为。

注册挂钩

创建或恢复会话时传递一个hooks对象。 下面的每个示例都遵循此模式。

import { CopilotClient } from "@github/copilot-sdk";

const client = new CopilotClient();
await client.start();

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => { /* ... */ },
        onPreToolUse:   async (input, invocation) => { /* ... */ },
        onPostToolUse:  async (input, invocation) => { /* ... */ },
        // ... add only the hooks you need
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python、Go 和 .NET 中的示例,请参阅 github/copilot-sdk 存储库

提示

每个挂钩处理程序都会接收一个包含 sessionIdinvocation 参数,这对关联日志和维护每会话状态很有用。

权限控制

用于 onPreToolUse 生成一个权限层,该层决定代理可以运行哪些工具、允许哪些参数,以及是否应在执行前提示用户。

将一组安全的工具加入允许列表

const READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"];

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (!READ_ONLY_TOOLS.includes(input.toolName)) {
                return {
                    permissionDecision: "deny",
                    permissionDecisionReason:
                        `Only read-only tools are allowed. "${input.toolName}" was blocked.`,
                };
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python、Go 和 .NET 中的示例,请参阅 github/copilot-sdk 存储库

限制对特定目录的文件访问

const ALLOWED_DIRS = ["/home/user/projects", "/tmp"];

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (["read_file", "write_file", "edit"].includes(input.toolName)) {
                const filePath = (input.toolArgs as { path: string }).path;
                const allowed = ALLOWED_DIRS.some((dir) => filePath.startsWith(dir));

                if (!allowed) {
                    return {
                        permissionDecision: "deny",
                        permissionDecisionReason:
                            `Access to "${filePath}" is outside the allowed directories.`,
                    };
                }
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

在破坏性操作之前询问用户

const DESTRUCTIVE_TOOLS = ["delete_file", "shell", "bash"];

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (DESTRUCTIVE_TOOLS.includes(input.toolName)) {
                return { permissionDecision: "ask" };
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

如果返回 "ask",会在运行时将决策委托给用户,这对于需要人工干预的破坏性操作非常有用。

审核和合规性

结合 onPreToolUseonPostToolUse 和会话生命周期挂钩,构建一个完整的审核线索,以记录代理所执行的每个操作。

结构化审核日志

interface AuditEntry {
    timestamp: number;
    sessionId: string;
    event: string;
    toolName?: string;
    toolArgs?: unknown;
    toolResult?: unknown;
    prompt?: string;
}

const auditLog: AuditEntry[] = [];

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "session_start",
            });
            return null;
        },
        onUserPromptSubmitted: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "user_prompt",
                prompt: input.prompt,
            });
            return null;
        },
        onPreToolUse: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "tool_call",
                toolName: input.toolName,
                toolArgs: input.toolArgs,
            });
            return { permissionDecision: "allow" };
        },
        onPostToolUse: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "tool_result",
                toolName: input.toolName,
                toolResult: input.toolResult,
            });
            return null;
        },
        onSessionEnd: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "session_end",
            });

            // Persist the log — swap this with your own storage backend
            await fs.promises.writeFile(
                `audit-${invocation.sessionId}.json`,
                JSON.stringify(auditLog, null, 2),
            );
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python 中的示例,请参阅 github/copilot-sdk 存储库

从工具的结果中删除敏感信息

const SECRET_PATTERNS = [
    /(?:api[_-]?key|token|secret|password)\s*[:=]\s*["']?[\w\-\.]+["']?/gi,
];

const session = await client.createSession({
    hooks: {
        onPostToolUse: async (input) => {
            if (typeof input.toolResult !== "string") return null;

            let redacted = input.toolResult;
            for (const pattern of SECRET_PATTERNS) {
                redacted = redacted.replace(pattern, "[REDACTED]");
            }

            return redacted !== input.toolResult
                ? { modifiedResult: redacted }
                : null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

通知

挂钩在应用程序的进程中触发,因此可能会引起任何副作用,例如桌面通知、声音、Slack 消息或 Webhook 调用。

会话事件的桌面通知

import notifier from "node-notifier"; // npm install node-notifier

const session = await client.createSession({
    hooks: {
        onSessionEnd: async (input, invocation) => {
            notifier.notify({
                title: "Copilot Session Complete",
                message: `Session ${invocation.sessionId.slice(0, 8)} finished (${input.reason}).`,
            });
            return null;
        },
        onErrorOccurred: async (input) => {
            notifier.notify({
                title: "Copilot Error",
                message: input.error.slice(0, 200),
            });
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python 中的示例,请参阅 github/copilot-sdk 存储库

工具完成后播放声音

import { exec } from "node:child_process";

const session = await client.createSession({
    hooks: {
        onPostToolUse: async (input) => {
            // macOS: play a system sound after every tool call
            exec("afplay /System/Library/Sounds/Pop.aiff");
            return null;
        },
        onErrorOccurred: async () => {
            exec("afplay /System/Library/Sounds/Basso.aiff");
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

出现错误时发布到 Slack

const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input, invocation) => {
            if (!input.recoverable) {
                await fetch(SLACK_WEBHOOK_URL, {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                        text: `🚨 Unrecoverable error in session \`${invocation.sessionId.slice(0, 8)}\`:\n\`\`\`${input.error}\`\`\``,
                    }),
                });
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

提示扩充

使用 onSessionStartonUserPromptSubmitted 自动注入上下文,以便用户不必重复自己。

在会话开始时注入项目元数据

import * as fs from "node:fs";

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input) => {
            const pkg = JSON.parse(
                await fs.promises.readFile("package.json", "utf-8"),
            );
            return {
                additionalContext: [
                    `Project: ${pkg.name} v${pkg.version}`,
                    `Node: ${process.version}`,
                    `CWD: ${input.cwd}`,
                ].join("\n"),
            };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

在提示中展开简短命令

const SHORTCUTS: Record<string, string> = {
    "/fix":      "Find and fix all errors in the current file",
    "/test":     "Write comprehensive unit tests for this code",
    "/explain":  "Explain this code in detail",
    "/refactor": "Refactor this code to improve readability",
};

const session = await client.createSession({
    hooks: {
        onUserPromptSubmitted: async (input) => {
            for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) {
                if (input.prompt.startsWith(shortcut)) {
                    const rest = input.prompt.slice(shortcut.length).trim();
                    return { modifiedPrompt: rest ? `${expansion}: ${rest}` : expansion };
                }
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

错误处理和恢复

通过使用 onErrorOccurred 挂钩,你可以对失败做出反应,无论你是要重试、通知人员还是正常关闭。

重试暂时性模型错误

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input) => {
            if (input.errorContext === "model_call" && input.recoverable) {
                return {
                    errorHandling: "retry",
                    retryCount: 3,
                    userNotification: "Temporary model issue—retrying…",
                };
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

友好的错误消息

const FRIENDLY_MESSAGES: Record<string, string> = {
    model_call:      "The AI model is temporarily unavailable. Please try again.",
    tool_execution:  "A tool encountered an error. Check inputs and try again.",
    system:          "A system error occurred. Please try again later.",
};

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input) => {
            return {
                userNotification: FRIENDLY_MESSAGES[input.errorContext] ?? input.error,
            };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

会话指标

跟踪会话的运行时间、调用的工具数以及会话结束的原因(对于仪表板和成本监视非常有用)。

const metrics = new Map<string, { start: number; toolCalls: number; prompts: number }>();

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => {
            metrics.set(invocation.sessionId, {
                start: input.timestamp,
                toolCalls: 0,
                prompts: 0,
            });
            return null;
        },
        onUserPromptSubmitted: async (_input, invocation) => {
            metrics.get(invocation.sessionId)!.prompts++;
            return null;
        },
        onPreToolUse: async (_input, invocation) => {
            metrics.get(invocation.sessionId)!.toolCalls++;
            return { permissionDecision: "allow" };
        },
        onSessionEnd: async (input, invocation) => {
            const m = metrics.get(invocation.sessionId)!;
            const durationSec = (input.timestamp - m.start) / 1000;

            console.log(
                `Session ${invocation.sessionId.slice(0, 8)}: ` +
                `${durationSec.toFixed(1)}s, ${m.prompts} prompts, ` +
                `${m.toolCalls} tool calls, ended: ${input.reason}`,
            );

            metrics.delete(invocation.sessionId);
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python 中的示例,请参阅 github/copilot-sdk 存储库

组合挂钩

挂钩会自然组合。 单个 hooks 对象可以处理权限、审核和通知 - 每个挂钩都执行自己的作业。

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input) => {
            console.log(`[audit] session started in ${input.cwd}`);
            return { additionalContext: "Project uses TypeScript and Vitest." };
        },
        onPreToolUse: async (input) => {
            console.log(`[audit] tool requested: ${input.toolName}`);
            if (input.toolName === "shell") {
                return { permissionDecision: "ask" };
            }
            return { permissionDecision: "allow" };
        },
        onPostToolUse: async (input) => {
            console.log(`[audit] tool completed: ${input.toolName}`);
            return null;
        },
        onErrorOccurred: async (input) => {
            console.error(`[alert] ${input.errorContext}: ${input.error}`);
            return null;
        },
        onSessionEnd: async (input, invocation) => {
            console.log(`[audit] session ${invocation.sessionId.slice(0, 8)} ended: ${input.reason}`);
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

最佳做法

  •         **保持挂钩紧固。** 每个挂钩都内联运行 - 缓慢的挂钩会延迟对话。 尽可能将大量工作(数据库写入、HTTP 调用)卸载到后台队列。
    
  •         **在没有任何更改时返回 `null` 。** 这会告知 SDK 继续执行默认值,并避免不必要的对象分配。
    
  •           **明确权限决策。** 返回 `{ permissionDecision: "allow" }` 比返回 `null` 更明了,尽管两者都可以使用该工具。
    
  •         **不要忽略关键错误。** 可以抑制可恢复的工具错误,但始终记录或针对无法恢复的工具错误发出警报。
    
  •         **尽可能使用 `additionalContext` 而不是 `modifiedPrompt`。** 追加上下文会保留用户的原始意向,同时仍指导模型。
    
  •         **根据会话 ID 确定范围状态。** 如果你跟踪每会话数据,请针对 `invocation.sessionId` 将其键入,并在 `onSessionEnd` 中进行清理。
    

延伸阅读

有关详细信息,请参阅 github/copilot-sdk 存储库中的挂钩参考

Morty Proxy This is a proxified and sanitized view of the page, visit original site.