12  第10章 插件系统

12.1 插件是什么

Herdr 的核心是一个终端工作空间管理器。但 Herdr 的真正威力来自于它的插件系统——一个让你(和社区)封装、分享和复用工作流的能力。

简单来说,插件 = 可分享的可执行工作流包

你写了一个工作流:“当 Agent 完成任务时,自动发送 Telegram 通知 + 运行测试 + 提交代码”。把这个工作流打包成插件,分享到 GitHub。其他人一条命令就能安装使用。

12.1.1 插件能做什么

能力 示例
响应事件 Agent 完成时发送通知
执行动作 一键启动完整开发环境
管理 Pane 自动创建日志面板并附加
处理链接 点击 GitHub URL 自动 clone 并打开
运行启动脚本 进入项目时自动加载环境变量
自定义命令 添加 herdr my-command

12.1.2 插件 vs Skill

维度 插件 Skill
运行位置 Herdr 进程内 Agent 的上下文中
执行方式 代码执行 作为 prompt 注入
语言 JavaScript/TypeScript Markdown
能操作什么 Herdr 的所有功能 只能通过 Agent 间接操作
谁安装 开发者 Agent
面向对象 Herdr 工具 AI Agent

一句话总结:Skill 教 Agent 怎么做,插件教 Herdr 怎么做

12.2 插件 Manifest 结构

每个插件的核心是一个 herdr-plugin.toml 文件——它是插件的”身份证”和”说明书”。

12.2.1 完整结构

# herdr-plugin.toml

[metadata]
name = "my-awesome-plugin"
version = "1.0.0"
description = "一个超棒的 Herdr 插件"
author = "your-name"
license = "MIT"
homepage = "https://github.com/your-name/my-awesome-plugin"
min_herdr_version = "1.0.0"

# ─── 构建命令 ───
[build]
commands = [
  "npm install",
  "npm run build"
]

# ─── 启动钩子 ───
[[startup]]
event = "workspace.load"
command = "node index.js --init"

[[startup]]
event = "session.attach"
command = "echo 'Welcome back!'"

# ─── 动作 ───
[[actions]]
name = "start-dev"
description = "启动开发环境"
command = "node index.js --action start-dev"
shortcut = "ctrl+d s"

[[actions]]
name = "run-tests"
description = "运行所有测试"
command = "node index.js --action run-tests"
shortcut = "ctrl+d t"

# ─── 事件钩子 ───
[[events]]
trigger = "agent.status_change"
condition = { from = "working", to = "idle" }
command = "node index.js --event agent-done --agent-id ${AGENT_ID}"

[[events]]
trigger = "agent.status_change"
condition = { to = "blocked" }
command = "node index.js --event agent-blocked --agent-id ${AGENT_ID}"

# ─── Pane 定义 ───
[[panes]]
name = "notifications"
title = "🔔 Notifications"
size = 0.15
command = "node index.js --pane notifications"

# ─── 链接处理器 ───
[[link_handlers]]
pattern = "https://github.com/(.+)/(.+)"
command = "node index.js --handle-github --url ${URL}"

12.2.2 字段详解

12.2.2.1 [metadata] — 元数据

[metadata]
name = "my-plugin"           # 唯一标识符(npm 风格)
version = "1.0.0"            # 语义化版本
description = "简短描述"      # 显示在插件列表中
author = "your-name"
license = "MIT"
homepage = "https://..."     # 文档/源码地址
min_herdr_version = "1.0.0"  # 最低兼容版本

12.2.2.2 [build] — 构建命令

安装插件时自动执行的命令:

[build]
commands = [
  "npm install",           # 安装依赖
  "npm run build",         # 编译 TypeScript
  "chmod +x scripts/*.sh"  # 设置权限
]

12.2.2.3 [[startup]] — 启动钩子

当特定事件发生时自动执行的命令:

[[startup]]
event = "workspace.load"       # 工作空间加载时
command = "node index.js --init"

[[startup]]
event = "session.attach"       # 会话重连时
command = "echo 'Welcome back!'"

[[startup]]
event = "session.detach"       # 会话分离时
command = "node index.js --save-state"

12.2.2.4 [[actions]] — 用户可触发的动作

[[actions]]
name = "deploy"               # 动作名称(herdr plugin run deploy)
description = "部署到生产环境"
command = "node index.js --action deploy"
shortcut = "ctrl+d d"         # 可选快捷键
confirm = true                # 执行前需要确认(危险操作)

12.2.2.5 [[events]] — 事件钩子

[[events]]
trigger = "agent.status_change"       # 触发条件
condition = { to = "blocked" }        # 过滤条件
command = "node index.js --on-blocked --agent-id ${AGENT_ID}"

可用的事件触发器:

触发器 条件字段 说明
agent.status_change from, to Agent 状态变化
agent.start model, cwd Agent 启动
agent.exit exit_code Agent 退出
pane.created tab, name 新 pane 创建
tab.switched from, to 标签页切换
workspace.focus 工作空间获得焦点

12.2.2.6 [[panes]] — Pane 定义

插件可以定义自己的 pane,自动在工作空间中创建:

[[panes]]
name = "notifications"
title = "🔔 Notifications"    # 标题栏显示
size = 0.15                   # 占比(15%)
command = "node index.js --pane notifications"
position = "bottom"           # bottom | top | left | right

12.3 插件管理命令

12.3.1 安装插件

# 从 GitHub 安装
herdr plugin install owner/repo

# 从 GitHub 子目录安装
herdr plugin install owner/repo/subdir

# 从本地路径安装
herdr plugin install ./my-plugin

# 从 Git URL 安装
herdr plugin install https://github.com/owner/repo.git

# 指定版本
herdr plugin install owner/repo --version 1.2.0

# 全局安装(所有工作空间可用)
herdr plugin install owner/repo --global

12.3.2 链接本地插件(开发模式)

开发插件时,每次修改都重新安装太麻烦。用 link 模式:

# 链接本地插件(修改即时生效)
herdr plugin link /path/to/my-plugin

# 取消链接
herdr plugin unlink my-plugin
Tip插件开发工作流
# 1. 创建插件项目
mkdir my-plugin && cd my-plugin

# 2. 编写 herdr-plugin.toml 和 index.js

# 3. 链接到 Herdr
herdr plugin link .

# 4. 修改代码,在 Herdr 中立即测试

# 5. 满意后发布到 GitHub
git init && git remote add origin ...
git push -u origin main

# 6. 其他人安装
herdr plugin install your-name/my-plugin

12.3.3 其他管理命令

# 列出已安装的插件
herdr plugin list
# my-plugin        | 1.0.0 | linked   | ~/projects/my-plugin
# notifications    | 0.3.2 | installed| ~/.herdr/plugins/notifications
# auto-deploy      | 2.1.0 | installed| ~/.herdr/plugins/auto-deploy

# 查看插件详情
herdr plugin info my-plugin

# 更新插件
herdr plugin update my-plugin
herdr plugin update --all  # 更新所有

# 禁用/启用插件
herdr plugin disable my-plugin   # 暂时禁用,不卸载
herdr plugin enable my-plugin    # 重新启用

# 卸载插件
herdr plugin uninstall my-plugin

# 运行插件的动作
herdr plugin run my-plugin start-dev

# 查看插件日志
herdr plugin logs my-plugin

12.4 编写第一个插件:从零开始

让我们从零开始编写一个完整的 Herdr 插件。

12.4.1 目标

创建一个名为 dev-setup 的插件,功能包括:

  1. 注册一个 start 动作——一键启动开发环境
  2. Agent 完成时自动运行测试
  3. Agent 阻塞时发出提醒
  4. 添加一个状态面板

12.4.2 步骤一:创建项目结构

mkdir herdr-plugin-dev-setup && cd herdr-plugin-dev-setup

# 项目结构
.
├── herdr-plugin.toml
├── package.json
├── index.js
└── README.md

12.4.3 步骤二:编写 herdr-plugin.toml

# herdr-plugin.toml

[metadata]
name = "dev-setup"
version = "1.0.0"
description = "一键开发环境 + Agent 自动测试"
author = "wuzhiguo"
license = "MIT"
min_herdr_version = "1.0.0"

[build]
commands = ["npm install"]

# ─── 动作 ───
[[actions]]
name = "start"
description = "🚀 启动开发环境(服务器 + Agent + 日志)"
command = "node index.js --action start"

[[actions]]
name = "test"
description = "🧪 运行测试"
command = "node index.js --action test"
shortcut = "ctrl+d t"

[[actions]]
name = "deploy"
description = "📦 部署到 staging"
command = "node index.js --action deploy"
confirm = true

# ─── 事件钩子 ───
[[events]]
trigger = "agent.status_change"
condition = { from = "working", to = "idle" }
command = "node index.js --event agent-done --agent-id ${AGENT_ID}"

[[events]]
trigger = "agent.status_change"
condition = { to = "blocked" }
command = "node index.js --event agent-blocked --agent-id ${AGENT_ID}"

# ─── 启动钩子 ───
[[startup]]
event = "workspace.load"
command = "node index.js --init"

# ─── Pane ───
[[panes]]
name = "status"
title = "📊 Status"
size = 0.12
position = "bottom"
command = "node index.js --pane status"

# ─── 链接处理器 ───
[[link_handlers]]
pattern = "https://github.com/(.+)/(.+)/issues/(\\d+)"
command = "node index.js --handle-issue --url ${URL} --owner ${1} --repo ${2} --issue ${3}"

12.4.4 步骤三:编写 package.json

{
  "name": "herdr-plugin-dev-setup",
  "version": "1.0.0",
  "description": "Herdr 插件:一键开发环境 + Agent 自动测试",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "child_process": "^1.0.0"
  }
}

12.4.5 步骤四:编写 index.js

#!/usr/bin/env node

const { execSync, spawn } = require("child_process");
const fs = require("fs");
const path = require("path");

// ─── 环境变量 ───
// Herdr 提供的环境变量
const HERDR_BIN_PATH = process.env.HERDR_BIN_PATH || "herdr";
const HERDR_WORKSPACE = process.env.HERDR_WORKSPACE || "";
const HERDR_SESSION = process.env.HERDR_SESSION || "";
const HERDR_TAB = process.env.HERDR_TAB || "";
const HERDR_PANE_ID = process.env.HERDR_PANE_ID || "";

// 解析命令行参数
const args = process.argv.slice(2);

function getArg(name) {
  const idx = args.indexOf(`--${name}`);
  return idx >= 0 ? args[idx + 1] : null;
}

// ─── Herdr CLI 封装 ───
function herdr(...cmdArgs) {
  const cmd = `${HERDR_BIN_PATH} ${cmdArgs.join(" ")}`;
  try {
    return execSync(cmd, { encoding: "utf-8", timeout: 10000 });
  } catch (e) {
    console.error(`Herdr command failed: ${cmd}`, e.message);
    return null;
  }
}

// ─── 初始化 ───
function init() {
  console.log("🎯 dev-setup 插件已加载");
  console.log(`   工作空间: ${HERDR_WORKSPACE}`);
  console.log(`   会话: ${HERDR_SESSION}`);
}

// ─── 启动开发环境 ───
function startDev() {
  console.log("🚀 启动开发环境...\n");

  // 创建标签页
  herdr("tab", "new", "server");
  herdr("tab", "new", "agent");
  herdr("tab", "new", "logs");

  // 在 server 标签页启动开发服务器
  const serverPane = herdr("tab", "goto", "server");
  herdr("pane", "run", "current", "npm run dev");

  // 在 agent 标签页启动 Claude Code
  herdr("tab", "goto", "agent");
  herdr("pane", "run", "current", "claude");

  // 在 logs 标签页启动日志
  herdr("tab", "goto", "logs");
  herdr("pane", "run", "current", "tail -f logs/*.log");

  // 回到 main 标签页
  herdr("tab", "goto", "main");

  console.log("✅ 开发环境已启动!");
  console.log("   - server: npm run dev");
  console.log("   - agent: claude");
  console.log("   - logs: tail -f");
}

// ─── 运行测试 ───
function runTests() {
  console.log("🧪 运行测试...\n");
  const output = execSync("npm test 2>&1", { encoding: "utf-8" });
  console.log(output);

  const passed = output.includes("passing");
  if (passed) {
    console.log("✅ 测试通过!");
  } else {
    console.log("❌ 测试失败!");
    process.exit(1);
  }
}

// ─── 部署 ───
function deploy() {
  console.log("📦 部署到 staging...\n");

  // 先运行测试
  try {
    execSync("npm test", { encoding: "utf-8", stdio: "inherit" });
  } catch (e) {
    console.error("❌ 测试未通过,取消部署");
    process.exit(1);
  }

  // 构建并部署
  console.log("🔨 构建中...");
  execSync("npm run build", { stdio: "inherit" });

  console.log("📤 部署中...");
  execSync("npx vercel --prod --yes", { stdio: "inherit" });

  console.log("✅ 部署完成!");
}

// ─── Agent 事件处理 ───
function onAgentDone(agentId) {
  console.log(`🎉 Agent ${agentId} 完成了任务!`);

  // 自动运行测试
  console.log("🧪 自动运行测试...");
  const result = execSync("npm test 2>&1", { encoding: "utf-8" });

  if (result.includes("passing")) {
    console.log("✅ 测试通过!");

    // 发送系统通知
    if (process.platform === "darwin") {
      execSync(
        `osascript -e 'display notification "Agent ${agentId} 完成,测试通过" with title "Herdr" sound name "Glass"'`
      );
    }
  } else {
    console.log("❌ 测试失败!");
    console.log(result);

    // 反馈给 Agent
    herdr("agent", "prompt", agentId, "测试失败了,请检查并修复");
  }
}

function onAgentBlocked(agentId) {
  console.log(`⚠️ Agent ${agentId} 被阻塞了!`);

  // 读取 Agent 的输出,看看它需要什么
  const output = herdr("agent", "read", agentId);
  console.log("Agent 输出:", output);

  // 发送系统通知
  if (process.platform === "darwin") {
    execSync(
      `osascript -e 'display notification "Agent ${agentId} 需要你的注意" with title "Herdr" sound name "Basso"'`
    );
  }
}

// ─── 状态面板 ───
function statusPane() {
  // 清屏并显示状态
  setInterval(() => {
    console.log("\x1b[2J\x1b[H"); // 清屏

    const time = new Date().toLocaleTimeString("zh-CN");

    // 获取 Agent 列表
    const agents = herdr("agent", "list");
    const tabs = herdr("tab", "list");

    console.log("┌────────────────────────────────────┐");
    console.log("│  📊 Herdr 状态面板                  │");
    console.log("├────────────────────────────────────┤");
    console.log(`│  时间: ${time}                    │`);
    console.log(`│  标签页: ${tabs ? tabs.trim() : "N/A"}             │`);
    console.log(`│  Agents:                           │`);
    console.log(`│  ${agents ? agents.trim() : "无"}                                │`);
    console.log("└────────────────────────────────────┘");
  }, 2000);
}

// ─── GitHub Issue 链接处理 ───
function handleGitHubIssue(url, owner, repo, issue) {
  console.log(`📌 GitHub Issue: ${owner}/${repo}#${issue}`);
  console.log(`   URL: ${url}`);

  // 自动获取 issue 详情
  const issueData = execSync(
    `gh issue view ${issue} --repo ${owner}/${repo} --json title,body,labels`,
    { encoding: "utf-8" }
  ).trim();

  const issueObj = JSON.parse(issueData);
  console.log(`   标题: ${issueObj.title}`);
  console.log(`   标签: ${issueObj.labels.map((l) => l.name).join(", ")}`);

  // 询问是否创建 worktree 并用 Agent 处理
  console.log("\n是否要用 Agent 处理这个 issue?");
}

// ─── 主入口 ───
function main() {
  if (args.includes("--init")) {
    init();
    return;
  }

  if (args.includes("--pane") && args.includes("status")) {
    statusPane();
    return;
  }

  const action = getArg("action");
  if (action === "start") {
    startDev();
  } else if (action === "test") {
    runTests();
  } else if (action === "deploy") {
    deploy();
  }

  const event = getArg("event");
  if (event === "agent-done") {
    onAgentDone(getArg("agent-id"));
  } else if (event === "agent-blocked") {
    onAgentBlocked(getArg("agent-id"));
  }

  if (args.includes("--handle-issue")) {
    handleGitHubIssue(
      getArg("url"),
      getArg("owner"),
      getArg("repo"),
      getArg("issue")
    );
  }
}

main();

12.4.6 步骤五:测试插件

# 链接到 Herdr
herdr plugin link .

# 验证安装
herdr plugin list
# dev-setup | 1.0.0 | linked | ~/projects/herdr-plugin-dev-setup

# 测试动作
herdr plugin run dev-setup start
herdr plugin run dev-setup test

# 查看日志
herdr plugin logs dev-setup

12.4.7 步骤六:发布

# 创建 Git 仓库
git init
git add -A
git commit -m "feat: initial release of dev-setup plugin"
git remote add origin https://github.com/your-name/herdr-plugin-dev-setup.git
git push -u origin main

# 其他人安装
herdr plugin install your-name/herdr-plugin-dev-setup

12.5 信任与安全模型

12.5.1 信任问题

插件是可执行代码——它能做任何 Node.js 程序能做的事:读写文件、发网络请求、执行系统命令。所以你安装的插件必须是可信的

12.5.2 Herdr 的安全措施

措施 说明
来源验证 GitHub 安装时显示仓库信息,让你确认
权限声明 插件需要在 manifest 中声明所需权限
沙箱执行 插件在受限的环境中运行(计划中)
确认机制 危险操作需要 confirm = true
日志记录 所有插件操作记录在 ~/.herdr/logs/plugins/

12.5.3 权限声明(计划中)

未来的版本中,插件需要在 manifest 中声明权限:

[permissions]
filesystem = ["read:project", "write:project"]  # 只能读写项目目录
network = ["localhost", "api.github.com"]       # 只能访问这些域名
process = ["execute"]                           # 可以执行子进程
environment = ["HERDR_*", "PATH"]               # 可访问的环境变量

12.5.4 安全最佳实践

# 1. 安装前审查源码
herdr plugin inspect owner/repo
# 显示 manifest + 主要源文件

# 2. 使用 --dry-run 测试
herdr plugin install owner/repo --dry-run
# 模拟安装,不实际执行

# 3. 限制权限(未来)
herdr plugin install owner/repo --no-filesystem --no-network

# 4. 在隔离环境中测试
# 使用 Docker 容器或虚拟机测试不信任的插件
Warning永远不要安装不信任的插件

插件可以执行任意代码。在安装社区插件前: 1. 查看作者的 GitHub profile 2. 阅读 manifest 和主要源文件 3. 检查是否有已知安全问题 4. 先在测试环境中运行

12.6 插件运行时环境变量

Herdr 在执行插件命令时,会注入以下环境变量:

变量 说明 示例值
HERDR_BIN_PATH Herdr CLI 的路径 /usr/local/bin/herdr
HERDR_VERSION Herdr 版本号 1.5.0
HERDR_WORKSPACE 当前工作空间路径 ~/projects/my-app
HERDR_SESSION 当前会话名 main
HERDR_SESSION_ID 会话唯一 ID sess-a1b2c3d4
HERDR_TAB 当前标签页名 agent
HERDR_TAB_ID 标签页唯一 ID tab-e5f6g7h8
HERDR_PANE_ID 当前 pane ID pane-i9j0k1l2
HERDR_PANE_CWD 当前 pane 工作目录 ~/projects/my-app/src
HERDR_PLUGIN_DIR 插件安装目录 ~/.herdr/plugins/dev-setup
HERDR_PLUGIN_NAME 插件名称 dev-setup
HERDR_PLUGIN_VERSION 插件版本 1.0.0
HERDR_AGENT_ID 触发事件的 Agent ID(仅事件钩子中) agent-m3n4o5p6
HERDR_AGENT_MODEL Agent 使用的模型 claude-sonnet-4.5
HERDR_EVENT_TYPE 触发的事件类型 agent.status_change

12.6.1 在代码中使用

// 调用 Herdr CLI
const herdrPath = process.env.HERDR_BIN_PATH;
const result = execSync(`${herdrPath} agent list`, { encoding: "utf-8" });

// 获取当前工作空间
const workspace = process.env.HERDR_WORKSPACE;

// 识别事件上下文
const eventType = process.env.HERDR_EVENT_TYPE;
const agentId = process.env.HERDR_AGENT_ID;

if (eventType === "agent.status_change") {
  console.log(`Agent ${agentId} 状态变化`);
}

12.7 Marketplace(未来)

目前插件通过 GitHub 直接安装。未来 Herdr 计划推出官方 Marketplace

# 浏览插件市场
herdr plugin search "notification"
herdr plugin search "auto-deploy"

# 查看热门插件
herdr plugin trending

# 查看分类
herdr plugin categories
# > 通知类 (12)
# > 部署类 (8)
# > 开发环境 (23)
# > 测试类 (15)
# > 监控类 (7)

12.7.1 示例插件 Cookbook

社区已有一些优秀插件可供参考:

# 安装示例插件合集
herdr plugin install ogulcancelik/herdr-plugin-examples
插件 功能 复杂度
auto-notify Agent 完成时系统通知 ⭐ 简单
git-flow Git 工作流自动化 ⭐⭐ 中等
test-runner 自动运行测试并报告 ⭐⭐ 中等
deploy-bot CI/CD 部署集成 ⭐⭐⭐ 复杂
metrics-dashboard Agent 性能指标面板 ⭐⭐⭐ 复杂
team-sync 团队协作通知 ⭐⭐⭐ 复杂

12.8 实战案例:Telegram 通知插件

让我们编写一个实用的插件——当 Agent 完成任务时发送 Telegram 通知。这样你就可以离开电脑,在手机上收到通知。

12.8.1 项目结构

mkdir herdr-plugin-telegram && cd herdr-plugin-telegram
herdr-plugin-telegram/
├── herdr-plugin.toml
├── package.json
├── index.js
└── README.md

12.8.2 herdr-plugin.toml

[metadata]
name = "telegram-notify"
version = "1.0.0"
description = "Agent 状态变化时发送 Telegram 通知"
author = "wuzhiguo"
license = "MIT"
min_herdr_version = "1.0.0"

[build]
commands = ["npm install"]

# ─── 配置 ───
[config]
bot_token = { type = "string", required = true, description = "Telegram Bot Token" }
chat_id = { type = "string", required = true, description = "Telegram Chat ID" }
notify_on = { type = "array", default = ["idle", "blocked", "error"], description = "哪些状态触发通知" }

# ─── 事件钩子 ───
[[events]]
trigger = "agent.status_change"
condition = { to = "idle" }
command = "node index.js --event done --agent-id ${AGENT_ID}"

[[events]]
trigger = "agent.status_change"
condition = { to = "blocked" }
command = "node index.js --event blocked --agent-id ${AGENT_ID}"

[[events]]
trigger = "agent.status_change"
condition = { to = "error" }
command = "node index.js --event error --agent-id ${AGENT_ID}"

# ─── 动作 ───
[[actions]]
name = "test"
description = "发送测试通知"
command = "node index.js --action test"

[[actions]]
name = "config"
description = "配置 Bot Token 和 Chat ID"
command = "node index.js --action config"

12.8.3 index.js

#!/usr/bin/env node

const https = require("https");
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");

const HERDR_BIN_PATH = process.env.HERDR_BIN_PATH || "herdr";
const PLUGIN_DIR = process.env.HERDR_PLUGIN_DIR || __dirname;

// ─── 配置管理 ───
function loadConfig() {
  const configPath = path.join(PLUGIN_DIR, ".config.json");
  if (fs.existsSync(configPath)) {
    return JSON.parse(fs.readFileSync(configPath, "utf-8"));
  }
  return null;
}

function saveConfig(config) {
  const configPath = path.join(PLUGIN_DIR, ".config.json");
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}

// ─── Telegram 发送 ───
function sendTelegram(message) {
  const config = loadConfig();
  if (!config) {
    console.error("❌ 未配置 Telegram Bot Token");
    console.log("运行: herdr plugin run telegram-notify config");
    return;
  }

  const data = JSON.stringify({
    chat_id: config.chat_id,
    text: message,
    parse_mode: "Markdown",
  });

  const options = {
    hostname: "api.telegram.org",
    path: `/bot${config.bot_token}/sendMessage`,
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Content-Length": data.length,
    },
  };

  return new Promise((resolve, reject) => {
    const req = https.request(options, (res) => {
      let body = "";
      res.on("data", (chunk) => (body += chunk));
      res.on("end", () => {
        if (res.statusCode === 200) {
          resolve(JSON.parse(body));
        } else {
          reject(new Error(`Telegram API error: ${res.statusCode}`));
        }
      });
    });

    req.on("error", reject);
    req.write(data);
    req.end();
  });
}

// ─── 获取 Agent 信息 ───
function getAgentInfo(agentId) {
  try {
    const output = execSync(`${HERDR_BIN_PATH} agent status ${agentId}`, {
      encoding: "utf-8",
    });
    return JSON.parse(output);
  } catch {
    return { id: agentId, model: "unknown", duration: "unknown" };
  }
}

function getAgentOutput(agentId, lines = 20) {
  try {
    return execSync(
      `${HERDR_BIN_PATH} agent read ${agentId} --lines ${lines}`,
      { encoding: "utf-8" }
    ).trim();
  } catch {
    return "(无法读取输出)";
  }
}

// ─── 事件处理 ───
async function onAgentDone(agentId) {
  const info = getAgentInfo(agentId);
  const output = getAgentOutput(agentId, 10);

  const message = [
    `✅ *Agent 完成了任务*`,
    ``,
    `🤖 模型: \`${info.model}\``,
    `⏱ 耗时: ${info.duration || "未知"}`,
    `📁 修改文件: ${info.filesChanged || "未知"}`,
    ``,
    `*最后输出:*`,
    "```",
    output.split("\n").slice(-10).join("\n"),
    "```",
  ].join("\n");

  try {
    await sendTelegram(message);
    console.log("✅ Telegram 通知已发送");
  } catch (e) {
    console.error("❌ 发送失败:", e.message);
  }
}

async function onAgentBlocked(agentId) {
  const info = getAgentInfo(agentId);
  const output = getAgentOutput(agentId, 15);

  const message = [
    `⚠️ *Agent 需要你的注意*`,
    ``,
    `🤖 模型: \`${info.model}\``,
    `🔴 状态: blocked`,
    ``,
    `*Agent 的消息:*`,
    "```",
    output.split("\n").slice(-15).join("\n"),
    "```",
    ``,
    `请回到 Herdr 查看详情`,
  ].join("\n");

  try {
    await sendTelegram(message);
    console.log("✅ Telegram 通知已发送");
  } catch (e) {
    console.error("❌ 发送失败:", e.message);
  }
}

async function onAgentError(agentId) {
  const message = [
    `❌ *Agent 发生错误*`,
    ``,
    `Agent ID: \`${agentId}\``,
    `时间: ${new Date().toLocaleString("zh-CN")}`,
    ``,
    `请检查 Herdr 日志获取详情`,
  ].join("\n");

  try {
    await sendTelegram(message);
  } catch (e) {
    console.error("❌ 发送失败:", e.message);
  }
}

// ─── 配置命令 ───
function interactiveConfig() {
  const readline = require("readline");
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  console.log("🔧 配置 Telegram 通知\n");

  rl.question("Bot Token: ", (botToken) => {
    rl.question("Chat ID: ", (chatId) => {
      saveConfig({ bot_token: botToken.trim(), chat_id: chatId.trim() });
      console.log("\n✅ 配置已保存!");
      rl.close();
    });
  });
}

// ─── 测试命令 ───
async function testNotification() {
  console.log("📤 发送测试通知...");
  try {
    await sendTelegram(
      "🧪 *Herdr Telegram 插件测试*\n\n如果你看到这条消息,说明配置正确!"
    );
    console.log("✅ 测试通知已发送!");
  } catch (e) {
    console.error("❌ 发送失败:", e.message);
    console.log("\n请检查配置: herdr plugin run telegram-notify config");
  }
}

// ─── 主入口 ───
function main() {
  const args = process.argv.slice(2);
  const event = args[args.indexOf("--event") + 1];
  const action = args[args.indexOf("--action") + 1];
  const agentId = args[args.indexOf("--agent-id") + 1];

  if (action === "config") {
    interactiveConfig();
  } else if (action === "test") {
    testNotification();
  } else if (event === "done") {
    onAgentDone(agentId);
  } else if (event === "blocked") {
    onAgentBlocked(agentId);
  } else if (event === "error") {
    onAgentError(agentId);
  }
}

main();

12.8.4 安装和配置

# 1. 安装插件
herdr plugin install your-name/herdr-plugin-telegram

# 2. 配置(交互式)
herdr plugin run telegram-notify config
# Bot Token: 123456:ABC-DEF...
# Chat ID: 987654321

# 3. 测试
herdr plugin run telegram-notify test
# ✅ 测试通知已发送!

12.8.5 获取 Telegram Bot Token

  1. 在 Telegram 中搜索 @BotFather
  2. 发送 /newbot
  3. 按提示创建 bot,获取 token
  4. 获取 Chat ID:给你的 bot 发一条消息,然后访问: https://api.telegram.org/bot<TOKEN>/getUpdates
  5. 在返回的 JSON 中找到 chat.id

12.8.6 使用效果

当你让 Agent 跑一个长任务,然后去喝咖啡:

📱 手机收到 Telegram 消息:

✅ Agent 完成了任务

🤖 模型: claude-sonnet-4.5
⏱ 耗时: 12m 34s
📁 修改文件: 8

最后输出:
> ✅ 所有功能已实现
> 创建的文件:
> - src/models/User.js
> - src/controllers/authController.js
> ...

如果 Agent 被阻塞:

📱 手机收到 Telegram 消息:

⚠️ Agent 需要你的注意

🤖 模型: claude-sonnet-4.5
🔴 状态: blocked

Agent 的消息:
> 我需要确认:数据库使用 PostgreSQL 还是 MySQL?
> 请回复你的选择

请回到 Herdr 查看详情

12.9 小结

Herdr 的插件系统是它最具扩展性的能力。本章核心:

  1. 插件 = 可分享的工作流包——封装你的最佳实践
  2. Manifest 驱动——一个 herdr-plugin.toml 定义一切
  3. 六大能力——actions、events、startup hooks、panes、link handlers、build
  4. 安全第一——只安装信任的插件,审查源码
  5. 环境变量——通过 HERDR_* 环境变量与 Herdr 交互
  6. 开发流程——link 模式实时测试,GitHub 发布,一条命令安装

到这里,我们完成了第三部分「实战工作流篇」的全部内容。你已经掌握了从单 Agent 到多 Agent、从本地到远程、从内置功能到插件扩展的完整 Herdr 工作流。

下一部分「自动化与 API 篇」,我们将深入 Herdr 的自动化能力和编程接口。