# 为 GitHub Copilot CLI 创建扩展

构建扩展，向 Copilot CLI 添加您自己的工具和斜杠命令。

> \[!NOTE]
> GitHub Copilot CLI 扩展目前是实验性功能，可能会更改。

扩展使你能够将自己的功能添加到 Copilot CLI。 每个扩展都是一个小型 Node.js 模块，作为独立进程与交互式会话并行运行，并回连到该会话。 通过该连接，扩展可以添加**工具**，供Copilot在代表你执行任务时调用；还可以添加由你自己运行的**斜杠命令**。

在本教程中，你将生成两个简单的扩展，作为可以执行的操作的示例：

* 一个名为 `tool-time` 的工具，Copilot 可以调用该工具来报告其工具调用在一次会话中到目前为止已花费的时间。
* 一个名为 `/tokencount` 的斜杠命令，用于报告自开始计数以来你已使用了多少个令牌。

这两个示例仅依赖于随 Copilot CLI 一起提供的 SDK，因此无需另外安装任何内容。 有关扩展工作原理的背景信息，请参阅 [关于 GitHub Copilot CLI 的扩展](/zh/copilot/concepts/agents/copilot-cli/about-cli-extensions)。

> \[!WARNING]
> 扩展程序会在你的计算机上运行，并使用你的权限。 仅加载信任的扩展代码，就像只运行未自行编写的任何其他脚本一样。

## Prerequisites

* **GitHub Copilot CLI**：需要 Copilot CLI 安装和设置。 请参阅“[开始使用 GitHub Copilot CLI](/zh/copilot/how-tos/copilot-cli/cli-getting-started)”。
* **启用了实验性功能**：扩展当前是一项实验性功能。 本教程中的步骤会在每次启动 CLI 时，使用 `‑‑experimental` 命令行选项启用实验功能。
* **JavaScript**：扩展是用 JavaScript 编写的，因此需要熟悉此语言才能创建自己的扩展。
* **存储库**：第二个示例添加项目级扩展，因此需要 Git 存储库的本地副本才能在其中添加扩展。

## 示例扩展程序 1：“工具时间”工具

此示例添加一个名为 \*\*\*\* 的`tool-time`扩展。 它新增了一个名为 `session_tool_time` 的工具，Copilot 可调用该工具来报告当前会话截至目前工具调用已耗费的时间，以及该时长所涵盖的调用次数。

若要确保 CLI 使用工具，该工具必须自行执行 CLI 无法执行的操作。 在这种情况下，`session_tool_time` 工具通过监听来自 CLI 的工具调用开始和结束事件，来跟踪工具调用的耗时，并自行记录这些时间数据。 由于 CLI 不会在任何 Copilot 可以读取的地方记录这些时间信息，因此 Copilot 想知道这些时间信息，唯一的办法就是调用该工具。 该工具的说明向模型解释了这一点，当询问工具调用计时时，该模型会引导它使用该工具。

### 步骤 1：创建扩展文件

1. 在主目录中创建以下目录和文件：

   ```text
   ~/.copilot/extensions/tool-time/extension.mjs
   ```

   由于该扩展位于 `~/.copilot/extensions/` 下，因此你的**所有** CLI 会话都可在任何目录中使用它，而不只是某一个代码仓库。

2. 将此代码添加到 `extension.mjs`：

   ```javascript copy
   // Extension: tool-time
   // Adds a session_tool_time tool that reports how long Copilot's tool calls
   // have taken this session. Copilot is never told these timings and they
   // aren't written anywhere, so calling the tool is the only way to find
   // them out.

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

   // The tool's own name, so it can avoid timing its own calls.
   const TOOL_NAME = "session_tool_time";

   // Module-level state persists for the whole session, because the extension
   // runs as a single long-lived process.
   const startTimes = new Map(); // tool call id -> Date.now() when call started
   let totalMs = 0; // total milliseconds spent across finished tool calls
   let callCount = 0; // number of finished tool calls measured

   const session = await joinSession({
       tools: [
           {
               name: TOOL_NAME,
               description:
                   "Report how long your tool calls have taken in total so far " +
                   "in THIS session, and how many calls that covers. You are " +
                   "never told how long your tool calls take and the timings " +
                   "aren't recorded anywhere you can read, so call this tool " +
                   "whenever you are asked about it rather than estimating.",
               // Always keep this tool's description in the model's tool list,
               // even when tool search is active, so Copilot reliably sees it:
               defer: "never",
               // Force a permissions approval prompt once, for this extension,
               // rather than on every call of this tool:
               skipPermission: true,
               parameters: { type: "object", properties: {} },
               handler: async () => {
                   const seconds = (totalMs / 1000).toFixed(1);
                   return `So far this session, Copilot's tool calls have taken ${seconds}s in total across ${callCount} call(s).`;
               },
           },
       ],
   });

   // When a tool starts, record the time, keyed by the tool call id so the
   // matching completion can be found later. The tool's own calls are skipped.
   session.on("tool.execution_start", (event) => {
       const data = event.data ?? {};
       if (data.toolName && data.toolName !== TOOL_NAME) {
           startTimes.set(data.toolCallId, Date.now());
       }
   });

   // When a tool finishes, add the elapsed time to the running total.
   session.on("tool.execution_complete", (event) => {
       const data = event.data ?? {};
       const startedAt = startTimes.get(data.toolCallId);
       if (startedAt === undefined) {
           return;
       }
       startTimes.delete(data.toolCallId);
       totalMs += Date.now() - startedAt;
       callCount += 1;
   });
   ```

> \[!NOTE]
> \*
> `@github/copilot-sdk/extension` 是扩展 SDK，它与 CLI 捆绑在一起。 CLI 在运行扩展时自动解析此导入，因此无需将其添加到 `package.json` 包管理器或运行包管理器。
> \*
> `startTimes`、`totalMs` 和 `callCount` 的值位于模块作用域中。 由于扩展作为整个会话的单个长期进程运行，因此只要会话处于打开状态，它们就会累积。

### 步骤 2：加载扩展

1. 启动启用了实验功能的交互式会话：

   ```shell copy
   copilot ‑‑experimental
   ```

   由于扩展位于以下位置 `~/.copilot/extensions/`，因此可以从任何目录启动 CLI，并且该扩展将可用。

   如果会话已打开，请运行 `/clear` 以启动一个新的会话，这会从磁盘重新加载扩展。

2. 如果不向新扩展、所有扩展或所有工具授予提升的权限，系统会提示你允许新扩展跳过工具权限提示。 选择 **“是**”或 **“是”，并始终允许此目录中的“user：tool-time”。**

   > \[!NOTE]
   > 若要在启动 CLI 时不再看到此消息，请将以下最低提升权限添加到 CLI 启动命令中：
   >
   > ```shell copy
   > --allow-tool='extension-permission-access(user:tool-time)'
   > ```
   >
   > 有关详细信息，请参阅“[允许和拒绝工具使用](/zh/copilot/how-tos/copilot-cli/use-copilot-cli/allowing-tools)”。

### 步骤 3：确认扩展正在运行

`/extensions manage`运行命令以打开扩展管理器。 扩展 `tool-time` 应列在“ **用户组** ”下，状态为 **“正在运行**”。 按 <kbd>Esc</kbd> 关闭经理。

### 步骤 4：试用

1. 与斜杠命令不同，你不会自己调用一个工具，Copilot 当它很有用时调用它。 首先，给 Copilot 分配一些需要调用几个工具的工作，例如：

   ```copilot copy
   Explore the files in the current directory and give me a short summary of what's here.
   ```

2. 等 Copilot 回复完毕后，询问：

   ```copilot copy
   How long have tool calls taken so far this session?
   ```

   代理从新扩展调用 `session_tool_time` 该工具，并使用它来回答你的问题。

   > \[!TIP]
   > 你可以通过查看 Copilot 的响应开头，确认该代理使用了新工具。 响应应以所用工具的名称为前缀;在本例中，为 `session_tool_time`.

### 该工具的工作原理

对 `joinSession` 的这一次调用，正是将一个普通的 Node.js 文件变成 Copilot CLI 扩展的关键。 它将正在运行的进程连接到会话，并注册扩展添加到 CLI 的所有内容，在本例中为单个工具。

工具由以下字段定义：

* **`name`**— 标识符 Copilot 用于调用该工具。
* **`description`**- 该工具的作用。 模型依靠这段文本来决定何时调用该工具，因此有必要表述得明确一些。 此说明告诉模型，其本身并不会被直接告知这些时间信息，从而促使它调用该工具，而不是尝试通过其他方式自行推算这些时间信息。
* **`parameters`**— 描述工具参数的 JSON 架构。 该工具不接受任何参数，因此其模式是一个不包含任何属性的对象。
* **`handler`**- 调用该工具时 Copilot 运行的异步函数。 它返回的任何字符串都将成为工具的结果，模型将读取它。

另外两个字段塑造了向模型提供该工具的方式：

* **`defer: "never"`**—使该工具的描述始终保留在模型的工具列表中。 默认情况下，当许多工具可用时，CLI 可能会延迟很少使用的工具，并允许模型按需搜索它们。 将`defer`设为`"never"`会使此工具不采用这种行为，因此Copilot始终会看到它。

  > \[!IMPORTANT]
  > `defer: "never"`只是向\_\_ 提供该工具Copilot。 它不\_强制\_Copilot去调用它。 如果存在生成对提示的适当响应的替代方法，则扩展无法强制使用特定工具。 模型始终自行决定使用哪种工具。 在此示例中，新工具可靠使用，因为模型没有其他方法知道该工具提供的信息。

* **`skipPermission: true`** 允许该工具在不要求你批准每个调用的情况下运行。 此处适用，因为该工具只读取已收集扩展的总和;它不会触摸文件或运行命令。

通过监控会话来收集时间数据。 该扩展订阅了 CLI 在每次工具调用前后发出的两个事件：

```javascript
session.on("tool.execution_start", (event) => { /* ... */ });
session.on("tool.execution_complete", (event) => { /* ... */ });
```

事件 `tool.execution_start` 包含工具的名称（`event.data.toolName`）。 一个`tool.execution_complete`事件携带一个`success`标志。 这两个事件都不携带另一个事件的信息，因此扩展通过每个事件中出现的 `toolCallId` 将它们关联起来：当工具启动时，它会以该 ID 记录当前时间。 当相应的完成事件到达时，会将已耗用的毫秒数加到累计总数中。 该工具会跳过自身的调用，因此该数值反映的是 Copilot 的实际工作量。

由于这些累计值驻留在长期运行的扩展进程中，因此它们覆盖整个会话期间。 这是一种良好的作业扩展：观察会话中发生的情况，并在调用之间保持状态。

> \[!NOTE]
>
> * 总数保存在内存中，因此每当重新加载扩展程序或会话重新开始时，这些总数都会被重置（例如，在 `/clear` 之后）。
> * 该时间值是从扩展程序的视角测得的挂钟时间——即每次工具调用的开始事件与完成事件之间的时间间隔——因此也包括该调用在等待你批准期间所耗费的任何时间。

## 示例扩展 2：令牌用量斜杠命令

此示例添加了一个名为 `token-counter` 的项目扩展。 该扩展添加了一个 `/tokencount` 斜杠命令，可用于查看在 CLI 中与 Copilot 交互时已使用的令牌数量。 在开始测量令牌使用情况时运行 `/tokencount start` ，然后可以在以后的任何时间点运行 `/tokencount` ，以检查自开始计数以来已使用的令牌数。

该扩展通过订阅 CLI 发出的事件，持续统计会话期间已使用的令牌总数，而这是一次性 shell 命令做不到的。

### 步骤 1：创建扩展文件

1. 在项目的 Git 存储库的根目录中，创建以下目录和文件：

   ```text
   .github/extensions/token-counter/extension.mjs
   ```

2. 将此代码添加到 `extension.mjs`：

   ```javascript copy
   // Extension: token-counter
   // Adds a /tokencount slash command that reports how many tokens you've used
   // since you started counting.

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

   // Module-level state. The extension runs as a single long-lived process for
   // the whole session, so these values persist between command invocations.
   let tokensUsed = 0; // Running total of tokens used so far this session.
   let startedAt = null; // tokensUsed when "/tokencount start" was last run.
   // null = not started.
   // startedAt allows you to count tokens multiple times in a session.

   const session = await joinSession({
       commands: [
           {
               name: "tokencount",
               description: "Report how many tokens you've used since " +
                   "'/tokencount start'.",
               handler: async (ctx) => {
                   const arg = (ctx.args ?? "").trim();

                   if (arg === "start") {
                       // Reset: remember the current total as the new baseline.
                       startedAt = tokensUsed;
                       await session.log(
                           "Token counter started. Run '/tokencount' later to " +
                           "see how many tokens you've used.",
                           { level: "info" },
                       );
                       return;
                   }

                   if (startedAt === null) {
                       await session.log(
                           "The token counter has not been started. Start by " +
                           "entering '/tokencount start'.",
                           { level: "info" },
                       );
                       return;
                   }

                   const used = tokensUsed - startedAt;
                   await session.log(
                       `You have used ${used} tokens since entering ` +
                       "'/tokencount start'.",
                       { level: "info" },
                   );
               },
           },
       ],
   });

   // The CLI emits an "assistant.usage" event after each assistant turn. Add the
   // tokens it reports to a running total kept in the extension's memory.
   session.on("assistant.usage", (event) => {
       const { inputTokens = 0, outputTokens = 0 } = event.data ?? {};
       tokensUsed += inputTokens + outputTokens;
   });
   ```

### 步骤 2：加载扩展

从同一存储库启动交互式会话，启用实验性功能：

```shell copy
copilot ‑‑experimental
```

如果会话已打开，可以运行 `/clear` 以启动新的会话，这会从磁盘重新加载扩展。

### 步骤 3：确认扩展正在运行

`/extensions manage`运行命令以打开扩展管理器。 您的 `token-counter` 扩展应列在 **Project** 组下，状态为 **正在运行**。 按 <kbd>Esc</kbd> 关闭经理。

还可以运行 `/env` 以查看加载到会话中的所有内容的摘要，包括扩展。

### 步骤 4：试用

与工具不同，斜杠命令需要由你自行调用。 启动计数器：

```copilot copy
/tokencount start
```

发送 Copilot 一个或两个提示，以便它使用一些令牌，例如，要求它解释文件。 然后检查已使用的令牌数：

```copilot copy
/tokencount
```

如果再次运行 `/tokencount start` ，计数将从零开始。

### 示例的工作原理

对 `joinSession` 的调用会注册该扩展添加到 CLI 的所有内容——在本例中，即一个斜杠命令。

斜杠命令由三个字段定义：

* **`name`**- 没有前导斜杠的命令名称。 注册 `tokencount` 会使 `/tokencount` 在会话中可用。
* **`description`**— 斜杠命令选取器中命令旁边显示的文本。
* **`handler`**- 调用命令时运行的异步函数。 它接收一个上下文对象，该 `args` 对象的属性保存在命令名称之后键入的原始文本。 对于 `/tokencount start`，`ctx.args` 为 `"start"`；对于单独的 `/tokencount`，则为空字符串。

处理程序通过 `session.log(message, { level: "info" })` 将其输出写回会话，这会将该消息打印在记录中。

要了解已使用了多少个令牌，扩展程序订阅了一个会话事件：

```javascript
session.on("assistant.usage", (event) => {
    const { inputTokens = 0, outputTokens = 0 } = event.data ?? {};
    tokensUsed += inputTokens + outputTokens;
});
```

CLI 在每个助手轮次后发出一个 `assistant.usage` 事件，并承载该轮次的输入和输出令牌计数。 扩展程序会将它们添加到 `tokensUsed` 中的累计总数。 运行 `/tokencount start` 时，处理程序会将当前总数记录到 `startedAt` 中。 在该会话稍后的时候输入不带参数的 `/tokencount`，会报告差异。 由于这两个变量都存在于长生命周期的扩展进程中，因此它们会在整个会话期间持续存在。

> \[!NOTE]
> 总数保存在内存中，因此每当重新加载扩展程序或会话重新开始时，这些总数都会被重置（例如，在 `/clear` 之后）。

## 编辑和重新加载扩展

开发扩展时，你会编辑 `extension.mjs`，并想查看所做的更改。 保存文件后，可以通过以下任一方式选取新版本：

* 请求 Copilot 重新加载扩展，例如 `Reload my extensions`。
* 运行 `/clear` 以启动从磁盘重新加载扩展的新会话。
* 重启 CLI。

如果某个扩展无法启动或行为异常，请运行 `/extensions manage` 并检查该扩展，查看其状态以及日志文件路径。 每个扩展都会在 `~/.copilot/logs/extensions/` 下写入日志；当出现问题时，这里是最佳的排查位置。

## 后续步骤

* 根据自己的需求调整其中一个示例。 在 CLI 中，要求 Copilot 修改任一示例扩展的行为。
* 共享用户级扩展。 例如，将 `tool-time` 扩展移动到存储库的 `.github/extensions/` 目录中，以便与在该存储库中工作的每个人共享该扩展。
* 要求 Copilot 从头开始为你创建新扩展。
  Copilot CLI 扩展由 Copilot SDK 提供支持，因此扩展可以实现 SDK 所能实现的任何功能，让你能够添加带有按钮和表单的交互式视图，以及新增工具和命令。 请参阅“[Copilot SDK](/zh/copilot/how-tos/copilot-sdk)”。

## 延伸阅读

* [GitHub Copilot CLI 命令参考](/zh/copilot/reference/copilot-cli-reference/cli-command-reference)