{"meta":{"title":"使用挂钩","intro":"挂钩使你能够将自定义逻辑插入到Copilot会话的每个阶段，从启动的那一刻起，到每次用户提示和工具调用，到它结束的那一刻。 本指南将演练实际用例，以便在不修改核心代理行为的情况下交付权限、审核、通知等。","product":"GitHub Copilot","breadcrumbs":[{"href":"/zh/enterprise-cloud@latest/copilot","title":"GitHub Copilot"},{"href":"/zh/enterprise-cloud@latest/copilot/how-tos","title":"操作方法"},{"href":"/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk","title":"Copilot SDK"},{"href":"/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features","title":"功能"},{"href":"/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/hooks","title":"钩子"}],"documentType":"article"},"body":"# 使用挂钩\n\n挂钩使你能够将自定义逻辑插入到Copilot会话的每个阶段，从启动的那一刻起，到每次用户提示和工具调用，到它结束的那一刻。 本指南将演练实际用例，以便在不修改核心代理行为的情况下交付权限、审核、通知等。\n\n<!-- markdownlint-disable GHD046 GHD005 -->\n\n<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->\n\n## 概述\n\n挂钩是创建会话时注册一次的回调。 SDK 在会话生命周期中定义完善的点调用它，传递上下文输入，并选择性地接受修改会话行为的输出。\n\n![图示：显示所述过程的流程图。](/assets/images/help/copilot/copilot-sdk/features-hooks-diagram-0.png)\n\n| 挂钩                                                                                                        | 当它触发时           | 你能做什么           |\n| --------------------------------------------------------------------------------------------------------- | --------------- | --------------- |\n| [会话生命周期挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-start) | 会话开始（新建会话或恢复会话） | 注入上下文，加载首选项     |\n| [用户提示提交挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)           | 用户发送消息          | 重写提示，添加上下文，筛选输入 |\n| [用户提示转换钩子](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)         | 运行时生成模型提示       | 检查或替换面向模型的内容    |\n| [工具使用前挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)                     | 在工具执行之前         | 允许/拒绝/修改调用      |\n| [工具使用后挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use)                    | 工具返回后（仅在成功时）    | 转换结果，屏蔽机密，审核    |\n| [工具使用后挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use#failure-variant)    | 工具返回失败结果后       | 添加重试指引，记录失败日志   |\n| [会话生命周期挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-end)   | 会话结束            | 清理、记录度量指标       |\n| [错误处理挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/error-handling)                    | 已引发错误           | 自定义日志记录、重试逻辑、警报 |\n\n所有挂钩都是**可选的**，您只需注册所需要的挂钩。 从任何挂钩中返回 `null`（或该语言的等效表达）会告知 SDK 继续执行默认行为。\n\n## 注册挂钩\n\n创建或恢复会话时传递一个`hooks`对象。 下面的每个示例都遵循此模式。\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nimport { CopilotClient } from \"@github/copilot-sdk\";\n\nconst client = new CopilotClient();\nawait client.start();\n\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input, invocation) => {\n      /* ... */\n    },\n    onPreToolUse: async (input, invocation) => {\n      /* ... */\n    },\n    onPostToolUse: async (input, invocation) => {\n      /* ... */\n    },\n    // ... add only the hooks you need\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nfrom copilot import CopilotClient, PermissionDecisionApproveOnce\n\nclient = CopilotClient()\nawait client.start()\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\n        \"on_session_start\": on_session_start,\n        \"on_pre_tool_use\":  on_pre_tool_use,\n        \"on_post_tool_use\": on_post_tool_use,\n        # ... add only the hooks you need\n    },\n)\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n```golang\nclient := copilot.NewClient(nil)\n\nsession, err := client.CreateSession(ctx, &copilot.SessionConfig{\n    Hooks: &copilot.SessionHooks{\n        OnSessionStart: onSessionStart,\n        OnPreToolUse:   onPreToolUse,\n        OnPostToolUse:  onPostToolUse,\n        // ... add only the hooks you need\n    },\n    OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) {\n        return &rpc.PermissionDecisionApproveOnce{}, nil\n    },\n})\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n```csharp\nvar client = new CopilotClient();\n\nvar session = await client.CreateSessionAsync(new SessionConfig\n{\n    Hooks = new SessionHooks\n    {\n        OnSessionStart = onSessionStart,\n        OnPreToolUse   = onPreToolUse,\n        OnPostToolUse  = onPostToolUse,\n        // ... add only the hooks you need\n    },\n    OnPermissionRequest = (req, inv) =>\n        Task.FromResult(PermissionDecision.ApproveOnce()),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n```java\nimport com.github.copilot.CopilotClient;\nimport com.github.copilot.rpc.*;\nimport java.util.concurrent.CompletableFuture;\n\ntry (var client = new CopilotClient()) {\n    client.start().get();\n\n    var hooks = new SessionHooks()\n        .setOnSessionStart((input, inv) -> CompletableFuture.completedFuture(null))\n        .setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null))\n        .setOnPostToolUse((input, inv) -> CompletableFuture.completedFuture(null));\n        // ... add only the hooks you need\n\n    var session = client.createSession(\n        new SessionConfig()\n            .setHooks(hooks)\n            .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)\n    ).get();\n}\n```\n\n</div>\n\n</div>\n\n> \\[!TIP]\n> 每个挂钩处理程序都会接收一个包含 `invocation` 的 `sessionId` 参数，这对关联日志和维护每会话状态很有用。\n\n## 用例：权限控制\n\n用于 `onPreToolUse` 生成一个权限层，该层决定代理可以运行哪些工具、允许哪些参数，以及是否应在执行前提示用户。\n\n### 将一组安全的工具加入允许列表\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nconst READ_ONLY_TOOLS = [\"read_file\", \"glob\", \"grep\", \"view\"];\n\nconst session = await client.createSession({\n  hooks: {\n    onPreToolUse: async (input) => {\n      if (!READ_ONLY_TOOLS.includes(input.toolName)) {\n        return {\n          permissionDecision: \"deny\",\n          permissionDecisionReason: `Only read-only tools are allowed. \"${input.toolName}\" was blocked.`,\n        };\n      }\n      return { permissionDecision: \"allow\" };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nfrom copilot import PermissionDecisionApproveOnce\n\nREAD_ONLY_TOOLS = [\"read_file\", \"glob\", \"grep\", \"view\"]\n\nasync def on_pre_tool_use(input_data, invocation):\n    if input_data[\"toolName\"] not in READ_ONLY_TOOLS:\n        return {\n            \"permissionDecision\": \"deny\",\n            \"permissionDecisionReason\":\n                f'Only read-only tools are allowed. \"{input_data[\"toolName\"]}\" was blocked.',\n        }\n    return {\"permissionDecision\": \"allow\"}\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\"on_pre_tool_use\": on_pre_tool_use},\n)\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n```golang\nreadOnlyTools := map[string]bool{\"read_file\": true, \"glob\": true, \"grep\": true, \"view\": true}\n\nsession, _ := client.CreateSession(ctx, &copilot.SessionConfig{\n    Hooks: &copilot.SessionHooks{\n        OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {\n            if !readOnlyTools[input.ToolName] {\n                return &copilot.PreToolUseHookOutput{\n                    PermissionDecision:       \"deny\",\n                    PermissionDecisionReason: fmt.Sprintf(\"Only read-only tools are allowed. %q was blocked.\", input.ToolName),\n                }, nil\n            }\n            return &copilot.PreToolUseHookOutput{PermissionDecision: \"allow\"}, nil\n        },\n    },\n})\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n```csharp\nvar readOnlyTools = new HashSet<string> { \"read_file\", \"glob\", \"grep\", \"view\" };\n\nvar session = await client.CreateSessionAsync(new SessionConfig\n{\n    Hooks = new SessionHooks\n    {\n        OnPreToolUse = (input, invocation) =>\n        {\n            if (!readOnlyTools.Contains(input.ToolName))\n            {\n                return Task.FromResult<PreToolUseHookOutput?>(new PreToolUseHookOutput\n                {\n                    PermissionDecision = \"deny\",\n                    PermissionDecisionReason = $\"Only read-only tools are allowed. \\\"{input.ToolName}\\\" was blocked.\",\n                });\n            }\n            return Task.FromResult<PreToolUseHookOutput?>(\n                new PreToolUseHookOutput { PermissionDecision = \"allow\" });\n        },\n    },\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n<!-- docs-validate: skip -->\n\n```java\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\n\nimport com.github.copilot.rpc.PermissionHandler;\nimport com.github.copilot.rpc.SessionConfig;\nimport com.github.copilot.rpc.SessionHooks;\nimport com.github.copilot.rpc.PreToolUseHookOutput;\nvar readOnlyTools = Set.of(\"read_file\", \"glob\", \"grep\", \"view\");\n\nvar hooks = new SessionHooks()\n    .setOnPreToolUse((input, invocation) -> {\n        if (!readOnlyTools.contains(input.getToolName())) {\n            return CompletableFuture.completedFuture(\n                PreToolUseHookOutput.deny(\n                    \"Only read-only tools are allowed. \\\"\" + input.getToolName() + \"\\\" was blocked.\")\n            );\n        }\n        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());\n    });\n\nvar session = client.createSession(\n    new SessionConfig()\n        .setHooks(hooks)\n        .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)\n).get();\n```\n\n</div>\n\n</div>\n\n### 限制对特定目录的文件访问\n\n```typescript\nconst ALLOWED_DIRS = [\"/home/user/projects\", \"/tmp\"];\n\nconst session = await client.createSession({\n  hooks: {\n    onPreToolUse: async (input) => {\n      if ([\"read_file\", \"write_file\", \"edit\"].includes(input.toolName)) {\n        const filePath = (input.toolArgs as { path: string }).path;\n        const allowed = ALLOWED_DIRS.some((dir) => filePath.startsWith(dir));\n\n        if (!allowed) {\n          return {\n            permissionDecision: \"deny\",\n            permissionDecisionReason: `Access to \"${filePath}\" is outside the allowed directories.`,\n          };\n        }\n      }\n      return { permissionDecision: \"allow\" };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n### 在破坏性操作之前询问用户\n\n```typescript\nconst DESTRUCTIVE_TOOLS = [\"delete_file\", \"shell\", \"bash\"];\n\nconst session = await client.createSession({\n  hooks: {\n    onPreToolUse: async (input) => {\n      if (DESTRUCTIVE_TOOLS.includes(input.toolName)) {\n        return { permissionDecision: \"ask\" };\n      }\n      return { permissionDecision: \"allow\" };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n如果返回 `\"ask\"`，会在运行时将决策委托给用户，这对于需要人工干预的破坏性操作非常有用。\n\n## 用例：审核和符合性\n\n结合 `onPreToolUse`、`onPostToolUse` 和会话生命周期挂钩，构建一个完整的审核线索，以记录代理所执行的每个操作。\n\n### 结构化审核日志\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\ninterface AuditEntry {\n  timestamp: Date;\n  sessionId: string;\n  event: string;\n  toolName?: string;\n  toolArgs?: unknown;\n  toolResult?: unknown;\n  prompt?: string;\n}\n\nconst auditLog: AuditEntry[] = [];\n\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"session_start\",\n      });\n      return null;\n    },\n    onUserPromptSubmitted: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"user_prompt\",\n        prompt: input.prompt,\n      });\n      return null;\n    },\n    onPreToolUse: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"tool_call\",\n        toolName: input.toolName,\n        toolArgs: input.toolArgs,\n      });\n      return { permissionDecision: \"allow\" };\n    },\n    onPostToolUse: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"tool_result\",\n        toolName: input.toolName,\n        toolResult: input.toolResult,\n      });\n      return null;\n    },\n    onSessionEnd: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"session_end\",\n      });\n\n      // Persist the log — swap this with your own storage backend\n      await fs.promises.writeFile(\n        `audit-${invocation.sessionId}.json`,\n        JSON.stringify(auditLog, null, 2),\n      );\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n<!-- docs-validate: skip -->\n\n```python\nimport json, aiofiles\nfrom copilot import PermissionDecisionApproveOnce\n\naudit_log = []\n\nasync def on_session_start(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"session_start\",\n    })\n    return None\n\nasync def on_user_prompt_submitted(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"user_prompt\",\n        \"prompt\": input_data[\"prompt\"],\n    })\n    return None\n\nasync def on_pre_tool_use(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"tool_call\",\n        \"tool_name\": input_data[\"toolName\"],\n        \"tool_args\": input_data[\"toolArgs\"],\n    })\n    return {\"permissionDecision\": \"allow\"}\n\nasync def on_post_tool_use(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"tool_result\",\n        \"tool_name\": input_data[\"toolName\"],\n        \"tool_result\": input_data[\"toolResult\"],\n    })\n    return None\n\nasync def on_session_end(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"session_end\",\n    })\n    async with aiofiles.open(f\"audit-{invocation['session_id']}.json\", \"w\") as f:\n        await f.write(json.dumps(audit_log, indent=2))\n    return None\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\n        \"on_session_start\": on_session_start,\n        \"on_user_prompt_submitted\": on_user_prompt_submitted,\n        \"on_pre_tool_use\": on_pre_tool_use,\n        \"on_post_tool_use\": on_post_tool_use,\n        \"on_session_end\": on_session_end,\n    },\n)\n```\n\n</div>\n\n</div>\n\n### 从工具的结果中删除敏感信息\n\n```typescript\nconst SECRET_PATTERNS = [\n  /(?:api[_-]?key|token|secret|password)\\s*[:=]\\s*[\"']?[\\w\\-\\.]+[\"']?/gi,\n];\n\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      if (typeof input.toolResult !== \"string\") return null;\n\n      let redacted = input.toolResult;\n      for (const pattern of SECRET_PATTERNS) {\n        redacted = redacted.replace(pattern, \"[REDACTED]\");\n      }\n\n      return redacted !== input.toolResult\n        ? { modifiedResult: redacted }\n        : null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## 用例：通知和声音\n\n挂钩在应用程序的进程中触发，因此可能会引起任何副作用，如桌面通知、声音、Slack 消息或 Webhook 调用。\n\n### 会话事件的桌面通知\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nimport notifier from \"node-notifier\"; // npm install node-notifier\n\nconst session = await client.createSession({\n  hooks: {\n    onSessionEnd: async (input, invocation) => {\n      notifier.notify({\n        title: \"Copilot Session Complete\",\n        message: `Session ${invocation.sessionId.slice(0, 8)} finished (${input.reason}).`,\n      });\n      return null;\n    },\n    onErrorOccurred: async (input) => {\n      notifier.notify({\n        title: \"Copilot Error\",\n        message: input.error.slice(0, 200),\n      });\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nimport subprocess\nfrom copilot import PermissionDecisionApproveOnce\n\nasync def on_session_end(input_data, invocation):\n    sid = invocation[\"session_id\"][:8]\n    reason = input_data[\"reason\"]\n    subprocess.Popen([\n        \"notify-send\", \"Copilot Session Complete\",\n        f\"Session {sid} finished ({reason}).\",\n    ])\n    return None\n\nasync def on_error_occurred(input_data, invocation):\n    subprocess.Popen([\n        \"notify-send\", \"Copilot Error\",\n        input_data[\"error\"][:200],\n    ])\n    return None\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\n        \"on_session_end\": on_session_end,\n        \"on_error_occurred\": on_error_occurred,\n    },\n)\n```\n\n</div>\n\n</div>\n\n### 工具完成后播放声音\n\n```typescript\nimport { exec } from \"node:child_process\";\n\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      // macOS: play a system sound after every tool call\n      exec(\"afplay /System/Library/Sounds/Pop.aiff\");\n      return null;\n    },\n    onErrorOccurred: async () => {\n      exec(\"afplay /System/Library/Sounds/Basso.aiff\");\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n### 出现错误时发布到 Slack\n\n```typescript\nconst SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;\n\nconst session = await client.createSession({\n  hooks: {\n    onErrorOccurred: async (input, invocation) => {\n      if (!input.recoverable) {\n        await fetch(SLACK_WEBHOOK_URL, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({\n            text: `🚨 Unrecoverable error in session \\`${invocation.sessionId.slice(0, 8)}\\`:\\n\\`\\`\\`${input.error}\\`\\`\\``,\n          }),\n        });\n      }\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## 用例：提示扩充\n\n使用 `onSessionStart` 和 `onUserPromptSubmitted` 自动注入上下文，以便用户不必重复自己。\n\n### 在会话开始时注入项目元数据\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input) => {\n      const pkg = JSON.parse(\n        await fs.promises.readFile(\"package.json\", \"utf-8\"),\n      );\n      return {\n        additionalContext: [\n          `Project: ${pkg.name} v${pkg.version}`,\n          `Node: ${process.version}`,\n          `Working directory: ${input.workingDirectory}`,\n        ].join(\"\\n\"),\n      };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n### 在提示中展开简短命令\n\n```typescript\nconst SHORTCUTS: Record<string, string> = {\n  \"/fix\": \"Find and fix all errors in the current file\",\n  \"/test\": \"Write comprehensive unit tests for this code\",\n  \"/explain\": \"Explain this code in detail\",\n  \"/refactor\": \"Refactor this code to improve readability\",\n};\n\nconst session = await client.createSession({\n  hooks: {\n    onUserPromptSubmitted: async (input) => {\n      for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) {\n        if (input.prompt.startsWith(shortcut)) {\n          const rest = input.prompt.slice(shortcut.length).trim();\n          return { modifiedPrompt: rest ? `${expansion}: ${rest}` : expansion };\n        }\n      }\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## 用例：错误处理和恢复\n\n通过使用 `onErrorOccurred` 挂钩，你可以对失败做出反应，无论你是要重试、通知人员还是正常关闭。\n\n### 重试暂时性模型错误\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onErrorOccurred: async (input) => {\n      if (input.errorContext === \"model_call\" && input.recoverable) {\n        return {\n          errorHandling: \"retry\",\n          retryCount: 3,\n          userNotification: \"Temporary model issue — retrying…\",\n        };\n      }\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n### 友好的错误消息\n\n```typescript\nconst FRIENDLY_MESSAGES: Record<string, string> = {\n  model_call: \"The AI model is temporarily unavailable. Please try again.\",\n  tool_execution: \"A tool encountered an error. Check inputs and try again.\",\n  system: \"A system error occurred. Please try again later.\",\n};\n\nconst session = await client.createSession({\n  hooks: {\n    onErrorOccurred: async (input) => {\n      return {\n        userNotification: FRIENDLY_MESSAGES[input.errorContext] ?? input.error,\n      };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## 用例：会话指标\n\n跟踪会话的运行时间、调用的工具数以及会话结束的原因（对于仪表板和成本监视非常有用）。\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nconst metrics = new Map<\n  string,\n  { start: Date; toolCalls: number; prompts: number }\n>();\n\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input, invocation) => {\n      metrics.set(invocation.sessionId, {\n        start: input.timestamp,\n        toolCalls: 0,\n        prompts: 0,\n      });\n      return null;\n    },\n    onUserPromptSubmitted: async (_input, invocation) => {\n      metrics.get(invocation.sessionId)!.prompts++;\n      return null;\n    },\n    onPreToolUse: async (_input, invocation) => {\n      metrics.get(invocation.sessionId)!.toolCalls++;\n      return { permissionDecision: \"allow\" };\n    },\n    onSessionEnd: async (input, invocation) => {\n      const m = metrics.get(invocation.sessionId)!;\n      const durationSec =\n        (input.timestamp.getTime() - m.start.getTime()) / 1000;\n\n      console.log(\n        `Session ${invocation.sessionId.slice(0, 8)}: ` +\n          `${durationSec.toFixed(1)}s, ${m.prompts} prompts, ` +\n          `${m.toolCalls} tool calls, ended: ${input.reason}`,\n      );\n\n      metrics.delete(invocation.sessionId);\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nfrom copilot import PermissionDecisionApproveOnce\n\nsession_metrics = {}\n\nasync def on_session_start(input_data, invocation):\n    session_metrics[invocation[\"session_id\"]] = {\n        \"start\": input_data[\"timestamp\"],\n        \"tool_calls\": 0,\n        \"prompts\": 0,\n    }\n    return None\n\nasync def on_user_prompt_submitted(input_data, invocation):\n    session_metrics[invocation[\"session_id\"]][\"prompts\"] += 1\n    return None\n\nasync def on_pre_tool_use(input_data, invocation):\n    session_metrics[invocation[\"session_id\"]][\"tool_calls\"] += 1\n    return {\"permissionDecision\": \"allow\"}\n\nasync def on_session_end(input_data, invocation):\n    m = session_metrics.pop(invocation[\"session_id\"])\n    duration = (input_data[\"timestamp\"] - m[\"start\"]).total_seconds()\n    sid = invocation[\"session_id\"][:8]\n    print(\n        f\"Session {sid}: {duration:.1f}s, {m['prompts']} prompts, \"\n        f\"{m['tool_calls']} tool calls, ended: {input_data['reason']}\"\n    )\n    return None\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\n        \"on_session_start\": on_session_start,\n        \"on_user_prompt_submitted\": on_user_prompt_submitted,\n        \"on_pre_tool_use\": on_pre_tool_use,\n        \"on_session_end\": on_session_end,\n    },\n)\n```\n\n</div>\n\n</div>\n\n## 组合挂钩\n\n挂钩会自然组合。 单个 `hooks` 对象即可处理权限 **、** 审计**和**通知 — 每个挂钩各司其职。\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input) => {\n      console.log(`[audit] session started in ${input.workingDirectory}`);\n      return { additionalContext: \"Project uses TypeScript and Vitest.\" };\n    },\n    onPreToolUse: async (input) => {\n      console.log(`[audit] tool requested: ${input.toolName}`);\n      if (input.toolName === \"shell\") {\n        return { permissionDecision: \"ask\" };\n      }\n      return { permissionDecision: \"allow\" };\n    },\n    onPostToolUse: async (input) => {\n      console.log(`[audit] tool completed: ${input.toolName}`);\n      return null;\n    },\n    onErrorOccurred: async (input) => {\n      console.error(`[alert] ${input.errorContext}: ${input.error}`);\n      return null;\n    },\n    onSessionEnd: async (input, invocation) => {\n      console.log(\n        `[audit] session ${invocation.sessionId.slice(0, 8)} ended: ${input.reason}`,\n      );\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## 最佳做法\n\n1. **保持挂钩紧固。** 每个挂钩都内联运行 - 缓慢的挂钩会延迟对话。 尽可能将大量工作（数据库写入、HTTP 调用）卸载到后台队列。\n\n2. **在没有任何更改时返回 `null` 。** 这会告知 SDK 继续执行默认值，并避免不必要的对象分配。\n\n3. **明确权限决策。** 返回 `{ permissionDecision: \"allow\" }` 比返回 `null` 更明了，尽管两者都可以使用该工具。\n\n4. **不要忽略关键错误。** 可以抑制可恢复的工具错误，但始终记录或针对无法恢复的工具错误发出警报。\n\n5. **尽可能使用 `additionalContext` 而不是 `modifiedPrompt`。** 追加上下文会保留用户的原始意向，同时仍指导模型。\n\n6. **根据会话 ID 确定范围状态。** 如果你跟踪每会话数据，请针对 `invocation.sessionId` 将其键入，并在 `onSessionEnd` 中进行清理。\n\n## Reference\n\n有关完整类型定义、输入/输出字段表以及每个挂钩的其他示例，请参阅 API 参考：\n\n* [会话挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/hooks-overview)\n* [工具使用前挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)\n* [工具使用后挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use)\n* [用户提示提交挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)\n* [用户提示转换钩子](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)\n* [会话生命周期挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle)\n* [错误处理挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/error-handling)\n\n## 另见\n\n* [构建你的第一个由 Copilot 提供支持的应用](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/getting-started)\n* [自定义代理和子代理编排](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/custom-agents)\n* [流式处理会话事件](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/streaming-events)\n* [调试指南](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/troubleshooting/debugging)"}