ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Hermes权限审批沙箱三重防护机制解析

Hermes权限审批沙箱三重防护机制解析

1. 这不是“加个权限开关”那么简单:Hermes 里权限、审批与沙箱的真实分量

你打开 Hermes 的源码,翻到core/permission目录,第一反应可能是:“哦,又是 RBAC 那套——角色、资源、操作,配个 YAML 就完事了?”
错。
Hermes 的权限体系根本不是传统后端那种“用户登录 → 查角色 → 查权限 → 放行/拦截”的线性校验逻辑。它把权限这件事,拆成了三个彼此咬合、又各自独立的齿轮:权限(Permission)是静态契约,审批(Approval)是动态博弈,沙箱(Sandbox)是物理隔离。三者共同构成 Agent 行动前必须穿上的“防护服”——不是为了限制它,而是为了让它敢动、能动、动得明白。

我去年在给一家金融风控平台做 Hermes 定制时,客户提了个需求:“让 Agent 自动调取客户征信报告,但必须确保每一份报告的调用都经过风控主管人工确认,且调用过程不能接触原始身份证号、手机号等敏感字段。”
当时团队第一反应是加个审批流、再做个字段脱敏。结果跑通后发现两个致命问题:一是审批通过后,Agent 在后续执行中仍可能通过日志、缓存或临时文件泄露脱敏前的数据;二是审批流本身被嵌在业务逻辑里,一旦流程变更,就得重写 Agent 的执行链。最后我们退回 Hermes 源码,从sandbox/runtime.go和approval/handler.go两处重梳设计,才真正落地。这让我彻底明白:Hermes 的这套机制,本质是把“安全”从功能层下沉到了执行层——它不假设开发者会写对代码,而是用架构强制你写不出错代码。

核心关键词就藏在这句话里:Hermes、权限、审批、沙箱、Agent。它们不是并列模块,而是纵向分层的防御纵深。权限定义“能做什么”,审批决定“此刻能不能做”,沙箱确保“就算做了也伤不到别人”。适合正在开发 AI Agent 产品的技术负责人、需要对接 Hermes 的平台工程师、以及想搞懂智能体安全边界的架构师。如果你还在用 if-else 做权限判断,或者把审批硬塞进业务 service 层,那这篇解读就是给你省下两周返工时间的清单。

2. 权限系统:不是 CRUD,而是“能力契约”的声明式建模

2.1 权限不是“允许访问某接口”,而是“授予某类能力”

Hermes 的权限模型绕开了传统 ACL/RBAC 的路径映射陷阱。它不定义“用户 A 能否访问/api/v1/report/download”,而是定义“Agent 具备read:credit_report这一能力契约”。这个区别看似微小,实则重构了整个授权逻辑的底层语义。

为什么这么设计?因为 Agent 的行为不可预测。一个report_analyzerAgent 可能调用下载接口,也可能调用 OCR 接口解析 PDF,还可能调用数据库直查原始表——它不会老老实实走你预设的 API 路径。如果权限绑定在路径上,要么放得过宽(给 download 权限就等于给了读库权限),要么卡得太死(每次新增能力都要改权限配置)。Hermes 选择把权限锚定在能力原子(Capability Atom)上:每个能力对应一组最小化、可验证的操作集合,比如:

  • read:credit_report= 允许调用征信服务 SDK 的GetReport()方法 + 读取返回结构体中的score和risk_level字段
  • write:audit_log= 允许向审计日志服务写入 JSON 记录,但禁止修改operator_id和timestamp字段
  • exec:shell_command= 允许在沙箱内执行ls,cat,grep,但禁止rm,wget,curl

这些能力在pkg/permission/capability.go中以结构体声明:

type Capability struct { ID string `json:"id"` // read:credit_report Description string `json:"desc"` // 读取征信报告核心指标 Resources []string `json:"resources"` // ["credit-report-service"] Actions []string `json:"actions"` // ["GET"] Fields []string `json:"fields"` // ["score", "risk_level"] —— 字段级控制 Constraints map[string]interface{} `json:"constraints"` // {"max_records": 100} }

提示:Fields字段级控制是 Hermes 权限最常被忽略的亮点。它不是靠 ORM 层过滤,而是在序列化响应前,由permission/field_filter.go的反射引擎实时裁剪。实测下来,比 Spring Security 的@PreFilter性能高 3.2 倍(压测 5k QPS 下平均延迟 8ms vs 26ms),因为它是编译期生成的字段白名单检查器,而非运行时表达式解析。

2.2 权限分配不是“给角色贴标签”,而是“签署能力契约”

Hermes 不设 Role 表,也不做 role-permission 关联。它用Policy(策略)替代角色概念。一个 Policy 是一份 JSON 文件,内容如下:

{ "id": "credit_analyst_v2", "description": "征信分析岗二期权限", "capabilities": [ "read:credit_report", "read:customer_profile", "write:audit_log" ], "conditions": { "time_window": "09:00-18:00", "ip_whitelist": ["10.1.2.0/24", "172.16.0.0/16"], "require_mfa": true } }

关键点在于conditions——这是权限的上下文约束。传统 RBAC 无法表达“只在工作时间、内网 IP、且通过 MFA 后才生效”的复合条件。Hermes 把条件引擎(permission/condition_evaluator.go)做成可插拔模块:默认支持时间、IP、MFA,你也可以注册自定义条件,比如“需满足当前风控评分 > 70 分”或“所在部门预算余额 > 0”。

Policy 不绑定用户,而是绑定到 Agent 实例。当你创建一个credit_analyst_agent,启动参数里指定--policy=credit_analyst_v2,该 Agent 就只能行使这份契约里的能力。哪怕它内部代码试图调用delete:user,运行时会直接 panic 并记录CapabilityViolationError。

注意:Policy 的加载发生在 Agent 初始化阶段,而非每次请求时。这意味着权限变更需重启 Agent 或触发热重载(hermesctl policy reload)。我们曾因忽略这点,在灰度发布新 Policy 时导致部分 Agent 权限未及时更新。后来在 CI/CD 流程中加入检查项:hermesctl policy validate --diff,对比线上与待发布 Policy 的 capability 差异,自动阻断存在高危能力增删的发布。

2.3 权限校验不是“拦在网关”,而是“嵌入执行链”

Hermes 的权限检查点不在 HTTP Middleware,而在executor/engine.go的执行器核心。当 Agent 调用agent.Call("credit_service.GetReport", params)时,执行链如下:

  1. executor.PrepareCall()解析目标能力 ID(从方法名映射为read:credit_report)
  2. permission.Check(ctx, capabilityID, params)查询当前 Policy 是否包含该能力,并验证conditions
  3. 若通过,进入sandbox.Run()执行;若失败,返回ErrCapabilityDenied,且不记录任何 trace(防信息泄露)

这种设计带来两个硬性保障:

  • 零信任执行:即使 Agent 绕过 HTTP 层直连内部服务,权限校验依然生效(因为所有跨服务调用都经由 Hermes 的agent.Call封装)
  • 精准审计:每条权限拒绝日志都包含capability_id、policy_id、violation_reason(如"time_window_failed"),而不是模糊的 “403 Forbidden”

我们曾用这套机制定位过一个诡异问题:某 Agent 在凌晨 2 点频繁报错ErrCapabilityDenied,但日志里没写明原因。启用--debug-permission后发现,错误详情里明确写着condition 'time_window' failed: current=02:15, allowed=09:00-18:00。原来客户把 Agent 部署在 UTC 时区服务器上,而 Policy 的 time_window 是按本地时区写的。这个细节,只有嵌入执行链的校验才能暴露。

3. 审批系统:不是“走个流程”,而是“执行权的临时授信”

3.1 审批不是业务逻辑的一部分,而是执行前的“能力解锁令牌”

很多团队把审批做成一个独立服务,Agent 调用业务接口前先调approval/request,拿到 approval_id 后再带着它去调业务接口。Hermes 反其道而行之:审批是权限系统的前置闸门,不是业务系统的旁路插件。

当你给 Agent 配置一个需要审批的能力(如write:customer_data),它在Policy中的声明是:

"capabilities": [ { "id": "write:customer_data", "requires_approval": true, "approval_type": "supervisor", "timeout_minutes": 15 } ]

注意:这里capabilities是对象数组,而非字符串数组。requires_approval: true表示该能力默认被锁定,必须获得审批令牌才能激活。

审批令牌(Approval Token)是一个 JWT,由approval/token.go签发,payload 包含:

{ "capability_id": "write:customer_data", "agent_id": "credit_analyst_agent_001", "approved_by": "supervisor@bank.com", "expires_at": 1717023600, "scope": "single_use" // 或 "session" }

Agent 在执行write:customer_data前,必须提供此 Token。Token 校验在permission.Check()中完成,且仅校验一次——用完即焚。这杜绝了“审批通过后,Agent 滥用权限做其他事”的风险。

实操心得:我们最初把timeout_minutes设为 60,结果风控主管经常忘记处理待审请求,导致 Agent 卡死。后来改成双阈值:timeout_minutes: 15(超时自动拒绝)+reminder_minutes: 5(5 分钟后发企业微信提醒)。提醒逻辑不在审批服务里,而是在approval/watcher.go的独立 goroutine 中监听 pending 请求,避免审批服务耦合通知渠道。

3.2 审批流不是固定模板,而是“能力驱动的动态路由”

Hermes 不预设“一级审批→二级审批→终审”的固定流程。审批路由由能力 ID 动态决定。approval/router.go的核心函数是:

func RouteTo(approverType string, capabilityID string) ([]string, error) { switch capabilityID { case "write:customer_data": return []string{"risk@bank.com"}, nil case "exec:shell_command": return []string{"infra@bank.com", "sec@bank.com"}, nil case "read:credit_report": if strings.Contains(capabilityID, "high_risk") { return []string{"risk@bank.com", "compliance@bank.com"}, nil } return []string{"risk@bank.com"}, nil default: return nil, ErrUnknownCapability } }

这意味着:同一个write:customer_data能力,对普通客户数据只需风控主管审批,但对 VIP 客户数据,会自动触发合规部联合审批。这种路由逻辑写在代码里,而非 BPMN 流程图中——它更轻量、更易测试、更能与能力模型保持一致。

我们曾用此机制实现“分级审批”:Agent 调用read:credit_report时,传入参数{"customer_level": "vip"},RouteTo函数解析参数,动态增加审批节点。这比在 NocoBase 里配 10 个审批流模板要干净得多。

3.3 审批状态不是“成功/失败”,而是“已授权/已拒绝/已过期/待处理”

Hermes 审批状态机只有 4 个终态,没有“审批中”这种中间态。为什么?因为 Agent 的执行是原子性的:它要么拿到 Token 立即执行,要么拿不到 Token 直接失败。不存在“等审批结果期间 Agent 干别的事”这种场景。

状态流转严格遵循:

  • pending→approved:审批人点击“通过”,签发 Token
  • pending→rejected:审批人点击“拒绝”,记录拒绝理由
  • pending→expired:超时未处理,自动标记过期
  • approved/rejected/expired→ 不可逆

所有状态变更都触发approval/event.go的事件广播,下游服务可监听ApprovalApprovedEvent做后续动作(如发送通知、更新仪表盘)。我们监听ApprovalRejectedEvent,自动将被拒的 Agent 执行请求转为人工工单,推送给风控主管邮箱——这样既保证流程闭环,又避免审批系统变成消息黑洞。

注意:pending状态不占用数据库锁。Hermes 用 Redis 的SETNX实现分布式 pending 状态登记,超时时间设为timeout_minutes * 60 + 30(多留 30 秒缓冲),避免网络延迟导致误判过期。这个细节在官方文档里没写,是我们压测时发现SETEX在 Redis Cluster 模式下有 100ms 误差,才换成SETNX+ Lua 脚本手动维护 TTL。

4. 沙箱系统:不是“容器隔离”,而是“执行环境的可信根”

4.1 沙箱不是 Docker 容器,而是进程级的“能力执行域”

Hermes 的沙箱(sandbox/runtime.go)不依赖 Docker 或 Pod。它基于 Linux namespace(user, pid, mount, network)和 seccomp-bpf 构建,但最关键的创新在于:沙箱的 rootfs 不是镜像,而是能力契约的运行时投影。

当你声明exec:shell_command能力时,Hermes 不给你一个完整 Linux 环境,而是:

  • 创建一个空目录作为 rootfs
  • 仅cp必需的二进制文件:/bin/ls,/bin/cat,/bin/grep(从宿主机/usr/bin/复制)
  • 用chroot切换到该目录
  • 用 seccomp 过滤掉所有非必需 syscall(如openat只允许读/proc/self/fd/*,禁止写)
  • 设置RLIMIT_NOFILE=32(文件描述符上限)和RLIMIT_AS=134217728(内存上限 128MB)

这意味着:Agent 在沙箱里执行ls /etc,看到的是一个只有 3 个文件的空/etc;执行cat /etc/passwd,返回Permission denied(因为/etc/passwd根本没被复制进去);尝试wget http://evil.com,直接被 seccomp 拦截,进程收到SIGSYS信号退出。

我们做过对比测试:同样执行find / -name "*.log" | head -n 10,Docker 容器耗时 2.3s(启动开销 + 完整文件系统扫描),Hermes 沙箱耗时 0.4s(无启动开销 + 只扫描实际挂载的 12 个目录)。性能差距来自架构本质——Hermes 沙箱是“按需装配”,Docker 是“全量加载”。

4.2 沙箱通信不是“网络调用”,而是“能力代理的受控通道”

沙箱内外通信不走 HTTP 或 gRPC。Hermes 用sandbox/channel.go实现了一种极简 IPC:

  • 沙箱进程启动时,Hermes 主进程通过unix socket传递一个fd(文件描述符)
  • 沙箱内所有能力调用(如credit_service.GetReport)都序列化为 protobuf message,写入该 fd
  • Hermes 主进程从 fd 读取消息,执行真实调用,再把结果序列化写回 fd
  • 沙箱进程从 fd 读取结果,反序列化后返回给 Agent 代码

这个设计砍掉了所有网络栈开销,且天然具备能力级隔离:沙箱进程只能通过这个 fd 与外界通信,且通信内容被严格限定为预定义的CapabilityRequest/CapabilityResponse结构。它甚至不认 HTTP 协议——想发 HTTP 请求?必须声明http:outbound能力,由 Hermes 主进程代为转发。

我们曾用此机制修复一个严重漏洞:某 Agent 因第三方库 bug,试图在沙箱内建立 TLS 连接。seccomp 规则直接拦截了connectsyscall,进程崩溃。但崩溃日志里清晰显示syscall=connect, args=[AF_INET, ...],让我们立刻定位到问题根源——不是 Agent 代码问题,而是能力契约没覆盖 TLS 底层 syscall。后来在exec:shell_command能力里显式添加allow_syscalls: ["connect", "sendto", "recvfrom"],并限定allowed_hosts: ["credit-api.bank.com"]。

4.3 沙箱生命周期不是“随 Agent 启动”,而是“按需创建、用完即毁”

Hermes 沙箱没有“长期运行”的概念。每次 Agent 调用需要沙箱的能力(如exec:shell_command),Hermes 都会:

  1. 从沙箱模板池(sandbox/template_pool.go)获取一个预构建的 rootfs 模板(基于 capability ID 缓存)
  2. 用clone()创建新进程,设置 namespace 和 seccomp
  3. 执行沙箱入口(/sbin/init,实际是 Hermes 的沙箱 loader)
  4. 完成调用后,waitpid()回收进程,umount清理挂载点,rmdir删除临时目录

整个过程平均耗时 18ms(实测 10k 次)。模板池的存在让首次创建稍慢(120ms),但后续复用极快。我们监控过沙箱创建失败率,99.99% 的失败源于clone()系统调用被 cgroup 限制(pids.max耗尽),而非权限问题——这说明 Hermes 的沙箱设计已把安全边界推到了内核层。

提示:sandbox/template_pool.go的清理策略很重要。我们最初用 LRU 缓存 100 个模板,结果发现高频能力(如read:credit_report)的模板永远不被淘汰,而低频能力(如exec:db_migrate)的模板反复创建销毁。后来改成 LFU(Least Frequently Used)+ TTL(24 小时),并暴露hermesctl sandbox stats命令查看各模板命中率,才让内存占用下降 65%。

5. 三者协同:当权限、审批、沙箱在一次 Agent 调用中咬合

5.1 一次典型调用的全链路拆解

我们以一个真实场景为例:Agent 需调用credit_service.GetReport(customer_id="CUST-8821"),该能力在 Policy 中配置为:

{ "id": "read:credit_report", "requires_approval": true, "approval_type": "supervisor", "sandbox": true }

调用发生时,Hermes 内部执行链如下:

  1. 权限预检(Prepare)
    executor.PrepareCall()解析出 capability IDread:credit_report,查询当前 Policy,确认存在且requires_approval=true,sandbox=true。此时不执行,只记录准备就绪。

  2. 审批触发(Approve)
    approval/request.go生成审批请求,调用RouteTo("supervisor", "read:credit_report")得到["risk@bank.com"],发送企业微信审批卡片。Agent 线程在此阻塞,等待 Token 或超时。

  3. 沙箱准备(Sandbox)
    审批通过后,sandbox/runtime.go从模板池获取read:credit_report沙箱模板(已预装credit_serviceSDK 和证书),创建新进程,设置 namespace/seccomp。

  4. 能力代理(Proxy)
    沙箱进程启动,Hermes 主进程通过 unix socket fd 传递CapabilityRequest{Method: "GetReport", Params: {"customer_id":"CUST-8821"}}。沙箱内代码反序列化后,调用 SDK,但 SDK 的网络请求被拦截,转由 Hermes 主进程代为发起。

  5. 字段裁剪(Filter)
    credit_service返回完整报告(含id_card,phone,score,risk_level),Hermes 主进程根据 Policy 中read:credit_report的Fields字段(["score", "risk_level"]),用反射引擎裁剪响应,只保留这两个字段。

  6. 结果返回(Return)
    裁剪后的 JSON 写回 fd,沙箱进程读取并返回给 Agent。整个过程对 Agent 代码完全透明——它只写了agent.Call("credit_service.GetReport", params),却享受了审批、沙箱、字段级权限的三重保护。

5.2 协同失效的排查:当某环断裂时,日志怎么说?

我们整理了三者协同失败时的典型日志模式,供快速定位:

失败环节日志关键词典型原因排查命令
权限拒绝ErrCapabilityDenied,capability_id=read:credit_reportPolicy 未包含该能力,或 conditions 不满足hermesctl policy show credit_analyst_v2
审批超时ErrApprovalTimeout,approval_id=apr-7f3a审批人未处理,或timeout_minutes设置过短hermesctl approval list --status=pending
沙箱崩溃SandboxExitCode=137,syscall=connectseccomp 拦截了未授权 syscall,或内存超限hermesctl sandbox logs apr-7f3a
字段裁剪异常FieldFilterError,field=phone, reason=not_in_whitelistPolicy 的Fields列表未包含返回字段hermesctl capability show read:credit_report

实操心得:我们把这四类日志模式写进了 ELK 的告警规则。当ErrCapabilityDenied出现频率 > 5 次/分钟,自动创建 Jira 工单并 @ 权限管理员;当SandboxExitCode=137出现,自动触发hermesctl sandbox debug --last抓取崩溃现场。这套机制让我们在上线首周就发现了 3 个 Policy 配置疏漏,远早于业务方投诉。

5.3 性能损耗实测:三重防护下的真实开销

很多人担心“加这么多层,性能会不会崩”。我们用生产环境真实流量做了压测(100 并发,持续 30 分钟):

场景P95 延迟CPU 使用率内存占用备注
直接调用 credit_service(无 Hermes)128ms32%1.2GB基线
Hermes + 权限(无审批/沙箱)135ms35%1.3GB+5.5% 延迟,主要来自 capability 解析
Hermes + 权限 + 审批(mock 通过)142ms37%1.4GB+11% 延迟,审批 mock 走内存,真实企业微信回调约 +20ms
Hermes + 权限 + 审批 + 沙箱168ms41%1.6GB+31% 延迟,沙箱创建占 18ms,能力代理占 12ms

结论很明确:沙箱是最大开销项,但仍在可接受范围(+31% < 50%)。而权限和审批的开销几乎可忽略。更重要的是,这 31% 换来了:零数据泄露事故、100% 审批可追溯、沙箱内 0 漏洞利用——对金融场景,这笔账非常划算。

我们后来做了优化:对高频能力(如read:credit_report)开启沙箱进程复用(sandbox/reuse.go),P95 延迟降到 152ms,CPU 降至 38%。这个优化没改架构,只是加了 37 行代码——说明 Hermes 的设计预留了足够的弹性空间。

6. 常见问题与避坑指南:那些文档里没写的实战经验

6.1 “为什么我的 Agent 调用总是被拒绝,但日志里没写明原因?”

这是最常遇到的问题。根本原因在于:Hermes 默认关闭详细错误日志,防止敏感信息泄露。解决方案分三步:

  1. 临时开启调试:在 Agent 启动时加参数--debug-permission --debug-approval,日志会输出capability check detail: condition 'ip_whitelist' failed: client_ip=192.168.1.100, allowed=[10.1.2.0/24]
  2. 检查 Policy 的 conditions:常见坑是ip_whitelist写了内网 IP,但 Agent 部署在云服务器上,实际出口 IP 是公网 IP
  3. 验证审批状态:用hermesctl approval list --agent-id=your-agent-id --status=pending查看是否有卡住的审批请求

注意:--debug-*参数只能用于调试环境。生产环境必须关闭,否则日志里会暴露用户 IP、审批人邮箱等敏感信息。我们 CI/CD 流程里加入了检查:grep -q "debug-permission" deploy.yaml && exit 1,强制阻断带调试参数的上线。

6.2 “审批通过了,但 Agent 还是报权限错误,是不是 Token 没生效?”

大概率是 Token 校验失败。Hermes 的 Token 校验有三个关键点:

  • 时间同步:Token 的exp字段是 Unix 时间戳,要求 Hermes 主进程、审批服务、Agent 宿主机三者时间误差 < 30 秒。我们统一部署chrony并监控chronyc tracking输出
  • 签名密钥:Token 由审批服务用私钥签发,Hermes 主进程用公钥校验。密钥不匹配会导致token signature invalid。密钥更新后,必须滚动重启所有 Hermes 实例
  • Scope 限制:single_useToken 只能用一次。如果 Agent 代码里循环调用同一能力,第二次就会失败。解决方案是改用sessionscope,或在循环外一次性申请 Token

我们曾因密钥不同步,导致灰度环境 Token 全部失效。后来在approval/token.go里加了ValidateKeyPair()函数,启动时自动校验公私钥是否匹配,不匹配直接 panic。

6.3 “沙箱里执行 shell 命令,为什么看不到文件内容?”

这不是 Bug,是设计使然。Hermes 沙箱的 rootfs 是精简的,/etc,/usr,/var等目录默认为空。要让cat /etc/hosts工作,必须:

  1. 在exec:shell_command能力的 Policy 中,显式声明required_files: ["/etc/hosts"]
  2. Hermes 启动时,会从宿主机复制/etc/hosts到沙箱模板中
  3. 沙箱进程启动后,该文件才可见

这个机制叫“按需挂载”,目的是最小化攻击面。我们曾误以为这是缺陷,试图用mount --bind强行挂载,结果被 seccomp 拦截——因为mountsyscall 不在白名单里。正确做法是:在能力定义里补全required_files,然后重建沙箱模板。

6.4 “如何给新能力快速配齐权限、审批、沙箱?”

Hermes 提供了hermesctl capability init命令,一键生成三件套:

hermesctl capability init \ --id write:customer_data \ --desc "写入客户基础信息" \ --resources ["customer-db"] \ --actions ["UPDATE"] \ --fields ["name", "address", "email"] \ --requires-approval \ --approval-type supervisor \ --sandbox \ --template exec:shell_command

它会生成:

  • capability/write_customer_data.go(能力定义)
  • policy/templates/write_customer_data.json(Policy 模板)
  • sandbox/templates/write_customer_data/(沙箱模板)

提示:--template参数指定继承哪个现有沙箱模板,避免从零构建。我们维护了一个base模板(含基础工具链)和network模板(额外开放connectsyscall),新能力优先继承base,需要网络时再选network。

6.5 “审批流怎么对接企业微信?官方文档太简略”

Hermes 审批的Notifier接口是抽象的,企业微信对接只需实现approval/notifier/wecom.go:

type WecomNotifier struct { webhookURL string } func (w *WecomNotifier) Notify(req *approval.Request) error { payload := map[string]interface{}{ "msgtype": "textcard", "textcard": map[string]string{ "title": "请审批:" + req.CapabilityID, "description": fmt.Sprintf("Agent %s 请求执行 %s,详情:%s", req.AgentID, req.CapabilityID, req.Params), "url": "https://hermes-admin/approval/" + req.ID, }, } return http.Post(w.webhookURL, "application/json", bytes.NewBuffer(payload)) }

关键点:url必须指向 Hermes Admin 的审批页面(/approval/{id}),该页面由admin/approval_handler.go提供,内置 Token 校验和一键通过/拒绝按钮。我们没用官方 SDK,而是直接 POST JSON 到企业微信 webhook,因为更轻量、更可控。

最后分享一个小技巧:我们在approval/notifier/wecom.go里加了重试逻辑(指数退避 + 最大 3 次),并监听NotifyFailedEvent发送钉钉告警——这样即使企业微信宕机,审批请求也不会丢失。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进