Skip to main content

使用挂钩

挂钩使你能够将自定义逻辑插入到Copilot会话的每个阶段,从启动的那一刻起,到每次用户提示和工具调用,到它结束的那一刻。 本指南将演练实际用例,以便在不修改核心代理行为的情况下交付权限、审核、通知等。

概述

挂钩是创建会话时注册一次的回调。 SDK 在会话生命周期中定义完善的点调用它,传递上下文输入,并选择性地接受修改会话行为的输出。

图示:显示所述过程的流程图。

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

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

注册挂钩

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

代码语言 navigation

TypeScript
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: "approve-once" }),
});

提示

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

用例:权限控制

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

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

代码语言 navigation

TypeScript
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: "approve-once" }),
});

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

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: "approve-once" }),
});

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

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: "approve-once" }),
});

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

用例:审核和符合性

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

结构化审核日志

代码语言 navigation

TypeScript
interface AuditEntry {
  timestamp: Date;
  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: "approve-once" }),
});

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

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: "approve-once" }),
});

用例:通知和声音

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

会话事件的桌面通知

代码语言 navigation

TypeScript
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: "approve-once" }),
});

工具完成后播放声音

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: "approve-once" }),
});

出现错误时发布到 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: "approve-once" }),
});

用例:提示扩充

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

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

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}`,
          `Working directory: ${input.workingDirectory}`,
        ].join("\n"),
      };
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

在提示中展开简短命令

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: "approve-once" }),
});

用例:错误处理和恢复

通过使用 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: "approve-once" }),
});

友好的错误消息

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: "approve-once" }),
});

用例:会话指标

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

代码语言 navigation

TypeScript
const metrics = new Map<
  string,
  { start: Date; 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.getTime() - m.start.getTime()) / 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: "approve-once" }),
});

组合挂钩

挂钩会自然组合。 单个 hooks 对象即可处理权限 审计通知 — 每个挂钩各司其职。

const session = await client.createSession({
  hooks: {
    onSessionStart: async (input) => {
      console.log(`[audit] session started in ${input.workingDirectory}`);
      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: "approve-once" }),
});

最佳做法

  1. 保持挂钩紧固。 每个挂钩都内联运行 - 缓慢的挂钩会延迟对话。 尽可能将大量工作(数据库写入、HTTP 调用)卸载到后台队列。

  2. 在没有任何更改时返回 null 这会告知 SDK 继续执行默认值,并避免不必要的对象分配。

  3.           **明确权限决策。** 返回 `{ permissionDecision: "allow" }` 比返回 `null` 更明了,尽管两者都可以使用该工具。
    
  4. 不要忽略关键错误。 可以抑制可恢复的工具错误,但始终记录或针对无法恢复的工具错误发出警报。

  5. 尽可能使用 additionalContext 而不是 modifiedPrompt 追加上下文会保留用户的原始意向,同时仍指导模型。

  6. 根据会话 ID 确定范围状态。 如果你跟踪每会话数据,请针对 invocation.sessionId 将其键入,并在 onSessionEnd 中进行清理。

Reference

有关完整类型定义、输入/输出字段表以及每个挂钩的其他示例,请参阅 API 参考:

另见