---
id: "c37a0f370e9aefb217f5"
url: "https://www.xmcsl.cn/project/feishu-cli-embed/01-agent-use-cli/02-%E8%87%AA%E7%A0%94%20Agent%20%E6%8E%A5%E5%85%A5%E9%A3%9E%E4%B9%A6%20CLI.html"
source_url: "https://open.feishu.cn/document/mcp_open_tools/feishu-cli/embed-feishu-cli-in-agent"
source_date: null
checked_at: "2026-11-05T04:21:48.909937+00:00"
status: "ready"
check_scope: "structure_only; not a factual or human review"
body_sha256: "92206ca676817a6a8c83126c0ff3fd20f49dc922847c11aefe1fcd9aaeda8da4"
---

# 自研 Agent 接入飞书 CLI · 飞书 Agent 接入知识库

## 适用场景

 

飞书 CLI 内置了一套<strong>扩展机制</strong>，让你在不修改 CLI 源码的情况下，替换或增强 CLI 的核心行为。

 

如果你是企业内部开发者或 ISV，希望<strong>在自己的 Agent 或应用中直接内嵌飞书 CLI 能力</strong>，并需要：

 

<ul>&#10;<li>&#10;<p>从自己的凭证系统（数据库、Vault、配置中心）获取 AppID / Token 等</p>&#10;</li>&#10;<li>&#10;<p>对所有 HTTP 请求统一加监控、日志，或改写请求目标等</p>&#10;</li>&#10;<li>&#10;<p>给 Agent 划能力边界，只让它用特定身份（用户或应用）调用特定命令</p>&#10;</li>&#10;<li>&#10;<p>统一审计所有命令的执行情况</p>&#10;</li>&#10;<li>&#10;<p>写类命令接审批 / 限流后才放行</p>&#10;</li>&#10;<li>&#10;<p>进程启动时初始化资源、退出前刷盘清理</p>&#10;</li>&#10;</ul>

 

你只需要：写一个 Go 包，实现指定接口，在 wrapper main 里 import 它，重新编译——就得到一个<strong>行为完全不同的增强版 CLI</strong>。

 

<img alt="image.png" src="https://www.xmcsl.cn/project/feishu-cli-embed/images/d22aa2354fde.png">

 

飞书 CLI 提供六个扩展点，分为<strong>基础能力扩展</strong>和<strong>命令行为扩展</strong>两大类。基础能力扩展改变 CLI 底层的凭证获取和网络通信方式；命令行为扩展控制命令的可用范围、运行时行为和进程生命周期。

 

<table>&#10;<thead>&#10;<tr>&#10;<th>扩展点</th>&#10;<th>分层定位</th>&#10;<th>核心能力</th>&#10;<th>使用场景</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><strong>Credential</strong></td>&#10;<td>基础能力 · 凭证层</td>&#10;<td>自定义凭证来源，让 CLI 从数据库、Vault、配置中心等任意来源获取 AppID / Token</td>&#10;<td>内嵌 CLI 到自有系统，需要对接企业凭证管理</td>&#10;</tr>&#10;<tr>&#10;<td><strong>Transport</strong></td>&#10;<td>基础能力 · 网络层</td>&#10;<td>拦截 CLI 每个 HTTP 请求，在请求前后注入 Header、改写目标、记录日志、上报监控</td>&#10;<td>统一网络监控、日志审计、请求路由改写</td>&#10;</tr>&#10;<tr>&#10;<td><strong>Restrict</strong></td>&#10;<td>命令行为 · 裁剪层</td>&#10;<td>声明命令白名单 / 黑名单 / 风险上限 / 身份限定，启动时将不合规命令改写为桩</td>&#10;<td>给 Agent 划能力边界，只暴露安全的命令子集</td>&#10;</tr>&#10;<tr>&#10;<td><strong>Observer</strong></td>&#10;<td>命令行为 · Hook 层</td>&#10;<td>命令前/后旁观，只看不拦；panic 安全，不影响命令执行</td>&#10;<td>审计日志、埋点、命令监控（含被拒命令）</td>&#10;</tr>&#10;<tr>&#10;<td><strong>Wrap</strong></td>&#10;<td>命令行为 · Hook 层</td>&#10;<td>洋葱模式中间件，可在命令前后做事，也可直接拦截（返回 AbortError）</td>&#10;<td>审批拦截、限流、运行时 token 注入</td>&#10;</tr>&#10;<tr>&#10;<td><strong>On</strong></td>&#10;<td>命令行为 · 生命周期层</td>&#10;<td>进程级 Startup / Shutdown 处理器，启动时初始化、退出前清理</td>&#10;<td>初始化连接池、退出前刷审计日志</td>&#10;</tr>&#10;</tbody>&#10;</table>

 

## 扩展 1：Credential — 凭证来源扩展

 

### 它是什么

 

Credential 扩展让你<strong>自定义凭证的来源</strong>。CLI 默认会从环境变量（<code>LARKSUITE&#95;CLI&#95;APP&#95;ID</code>、<code>LARKSUITE&#95;CLI&#95;APP&#95;SECRET</code>、<code>LARKSUITE&#95;CLI&#95;USER&#95;ACCESS&#95;TOKEN</code>、<code>LARKSUITE&#95;CLI&#95;TENANT&#95;ACCESS&#95;TOKEN</code>）或本地 keychain 读取凭证。通过注册自定义 Provider，你可以让 CLI 从数据库、Vault、配置中心等任意来源获取凭证。本文为了直观，就采取从本地读取凭证来进行演示。

 

### 怎么用

 

#### 第 1 步：创建项目

 

```bash
mkdir my-cli && cd my-cli
go mod init my-cli
```

 

#### 第 2 步：编写 Provider

 

创建 <code>mycred/mycred.go</code>，实现 <code>credential.Provider</code> 接口：

 

```Go
package mycred

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"

"github.com/larksuite/cli/extension/credential"
)

const (
    appID     = "cli_aXXXXXXXXXXXXXXXX" // 替换为真实 AppID
    appSecret = "XXXXXXXXXXXXXXXXXXXXXXXX" // 替换为真实 AppSecret
)

type Provider struct{}

func (p *Provider) Name() string { return "mycred" }

// ResolveAccount 返回应用凭证
// 返回 &Account{} → 命中，CLI 用这组凭证
// 返回 nil, nil   → 跳过，交给下一个 Provider
// 返回 nil, err   → 报错，终止
func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, error) {
    // 这里替换成你自己的逻辑：查数据库、调 Vault、读配置中心...
    return &credential.Account{
        AppID:     appID,
        AppSecret: appSecret,
        Brand:     credential.BrandFeishu,
        DefaultAs: credential.IdentityUser,
    }, nil
}

// ResolveToken 返回访问令牌
// 重要：一旦你的 Provider 接管了 Account，Token 也由你负责。
// 返回 nil, nil 不会 fallback 到默认换 Token 逻辑，而是直接报错。
func (p *Provider) ResolveToken(ctx context.Context, req credential.TokenSpec) (*credential.Token, error) {
    switch req.Type {
    case credential.TokenTypeTAT:
        // bot 身份：用 AppID + AppSecret 向飞书换取 tenant_access_token
        token, err := exchangeTenantAccessToken(appID, appSecret)
        if err != nil {
            return nil, err
        }
        return &credential.Token{Value: token, Source: "mycred:tat"}, nil

case credential.TokenTypeUAT:
        // user 身份：从任意来源获取 user_access_token
        // 例如：从数据库查询、从 Redis 缓存读取、从 OAuth 回调获取、
        // 从配置文件读取，甚至直接写死一个用于测试...
        uat := getUserAccessToken()
        return &credential.Token{Value: uat, Source: "mycred:uat"}, nil
    }
    return nil, nil
}

// getUserAccessToken 从你的来源获取 UAT
// 这里你可以对接任何存储：数据库、Redis、文件、环境变量等
func getUserAccessToken() string {
    // 示例：从环境变量获取
    // return os.Getenv("MY_USER_ACCESS_TOKEN")

// 示例：从数据库查询
    // return db.Query("SELECT uat FROM tokens WHERE user_id = ?", userID)

// 示例：写死一个用于本地测试（从 lark-cli auth login 后获取）
    return "u-XXXXXXXX"
}

// exchangeTenantAccessToken 用 AppID + AppSecret 向飞书换取 tenant_access_token
func exchangeTenantAccessToken(appID, appSecret string) (string, error) {
    body, _ := json.Marshal(map[string]string{
        "app_id":     appID,
        "app_secret": appSecret,
    })
    resp, err := http.Post(
        //此处以feishu Brand作为演示，如果为LARK品牌host切换为：open.larksuite.com
        "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
        "application/json", bytes.NewReader(body),
    )
    if err != nil {
        return "", fmt.Errorf("exchange tat: %w", err)
    }
    defer resp.Body.Close()

var result struct {
        Code              int    `json:"code"`
        TenantAccessToken string `json:"tenant_access_token"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return "", err
    }
    if result.Code != 0 {
        return "", fmt.Errorf("exchange tat failed, code=%d", result.Code)
    }
    return result.TenantAccessToken, nil
}

func init() {
    credential.Register(&Provider{})
}
```

 

#### 第 3 步：编写 wrapper main

 

创建 <code>main.go</code>：

 

```Go
package main

import (
    "os"
    "github.com/larksuite/cli/cmd"

_ "my-cli/mycred"  // blank import 触发 init() → 注册你的 Provider
)

func main() {
    os.Exit(cmd.Execute())
}
```

 

#### 第 4 步：编译验证

 

```Bash
go mod tidy
go build -o my-lark-cli .

# 用 --dry-run 验证凭证来源（不会真正发请求）
./my-lark-cli api GET /open-apis/calendar/v4/calendars --dry-run
```

 

输出：

 

```JSON
{
  "appId": "cli_a1234567890",
  "as": "user"
}
```

 

<code>appId</code> 显示的是你写死的值，说明自定义 Provider 生效了。

 

### 原理

 

Credential 采用<strong>链式数组</strong>存储多个 Provider，按注册顺序依次查询，第一个返回非 nil 的胜出：

 

```text
import 顺序:  mycred → env
注册结果:     providers = [mycred, env]

CLI 需要凭证时:
  ┌─ mycred.ResolveAccount()
  │   查到了? → 返回 Account → 命中! 后面全跳过
  │   没查到? → 返回 nil    → 继续
  │
  ├─ env.ResolveAccount()
  │   环境变量有? → 返回 Account → 命中
  │   没有?      → 返回 nil    → 继续
  │
  └─ defaultAcct (keychain 兜底)
```

 

<strong>延迟执行</strong>：Provider 在启动时只是注册，不会立即被调用。只有当用户执行的命令真正需要凭证时，才会触发链式遍历。结果通过 <code>sync.Once</code> 缓存，整个进程只解析一次。

 

#### 核心源码

 

<table>&#10;<thead>&#10;<tr>&#10;<th>文件</th>&#10;<th>作用</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><code>extension/credential/types.go</code></td>&#10;<td>Provider 接口、Account / Token 类型定义</td>&#10;</tr>&#10;<tr>&#10;<td><code>extension/credential/registry.go</code></td>&#10;<td><code>Register()</code> 追加到全局数组，<code>Providers()</code> 返回快照</td>&#10;</tr>&#10;<tr>&#10;<td><code>extension/credential/env/env.go</code></td>&#10;<td>内置实现：从环境变量读取凭证</td>&#10;</tr>&#10;</tbody>&#10;</table>

 

## 扩展 2：Transport — HTTP 请求拦截扩展

 

### 它是什么

 

Transport 扩展让你<strong>拦截 CLI 发出的每一个 HTTP 请求</strong>。你可以在请求发出前修改 Header、改写请求目标（Host / URL）、打印日志，在请求完成后记录耗时、上报监控。

 

### 怎么用

 

#### 第 1 步：编写 Interceptor

 

创建 <code>tracing/tracing.go</code>，实现 <code>transport.Provider</code> 和 <code>transport.Interceptor</code> 接口：

 

```Go
package tracing

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "time"

"github.com/larksuite/cli/extension/transport"
)

type Provider struct{}

func (p *Provider) Name() string { return "tracing" }

func (p *Provider) ResolveInterceptor(ctx context.Context) transport.Interceptor {
    return &tracingInterceptor{}
}

type tracingInterceptor struct{}

// PreRoundTrip 在每个请求发出前被调用
// 返回的函数在请求完成后被调用
func (i *tracingInterceptor) PreRoundTrip(req *http.Request) func(*http.Response, error) {
    // ===== 请求前 =====
    start := time.Now()
    fmt.Fprintf(os.Stderr, "🚀 请求开始: %s %s\n", req.Method, req.URL)

// ===== 请求后（返回的闭包）=====
    return func(resp *http.Response, err error) {
        elapsed := time.Since(start)
        if err != nil {
            fmt.Fprintf(os.Stderr, "❌ 请求失败: %s %s err=%v (耗时 %v)\n",
                req.Method, req.URL, err, elapsed)
        } else {
            fmt.Fprintf(os.Stderr, "✅ 请求完成: %s %s 状态码=%d (耗时 %v)\n",
                req.Method, req.URL, resp.StatusCode, elapsed)
        }
    }
}

func init() {
    transport.Register(&Provider{})
}
```

 

#### 第 2 步：在 main.go 中引入

 

```Go
package main

import (
    "os"
    "github.com/larksuite/cli/cmd"

_ "my-cli/tracing"  // blank import 注册 Transport Interceptor
    _ "github.com/larksuite/cli/extension/credential/env"
)

func main() {
    os.Exit(cmd.Execute())
}
```

 

#### 第 3 步：编译验证

 

```Bash
go build -o my-lark-cli .
```

 

验证 UAT（user 身份）：

 

```Bash
./my-lark-cli api GET /open-apis/calendar/v4/calendars --as user
```

 

输出：

 

```text
🚀 请求开始: GET https://open.feishu.cn/open-apis/authen/v1/user_info
✅ 请求完成: GET https://open.feishu.cn/open-apis/authen/v1/user_info 状态码=200 (耗时 644ms)
🚀 请求开始: GET https://open.feishu.cn/open-apis/calendar/v4/calendars
✅ 请求完成: GET https://open.feishu.cn/open-apis/calendar/v4/calendars 状态码=200 (耗时 1070ms)
{ "code": 0, "data": { "calendar_list": [...] } }
```

 

验证 TAT（bot 身份）：

 

```Bash
./my-lark-cli api GET /open-apis/calendar/v4/calendars --as bot
```

 

输出：

 

```text
🚀 请求开始: GET https://open.feishu.cn/open-apis/calendar/v4/calendars
✅ 请求完成: GET https://open.feishu.cn/open-apis/calendar/v4/calendars 状态码=200 (耗时 736ms)
{ "code": 0, "data": { ... } }
```

 

### 原理

 

Transport 采用<strong>单 Provider</strong> 模式，注册后会被包装为一个 HTTP 中间件，插入到 CLI 的请求链中：

 

```text
CLI 发 HTTP 请求
      │
      ▼
你的 PreRoundTrip(req)         ← 请求前: 加 Header / 改写 Host / 打日志 / 开始计时
      │  返回一个 postFn
      ▼
CLI 内置 transport 链           ← Retry → SecurityHeader → 实际网络请求
      │
      ▼
postFn(resp, err)              ← 请求后: 记录耗时 / 上报监控
```

 

CLI 在构建 HTTP Client 时，会调用 <code>wrapWithExtension()</code> 把你的 Interceptor 包装成中间件。之后 CLI 发出的<strong>每一个请求</strong>（包括换 Token、调业务 API）都会经过这个中间件。

 

<strong>安全保障</strong>：CLI 内置安全层会统一重写基础安全 Header（如 <code>X-Cli-&#42;</code>、<code>User-Agent</code>）。<code>Authorization</code> 不属于这层统一覆盖：它由请求构建阶段设置；如果发生跨 host redirect，会在 redirect 策略中被剥离。因此你可以添加自定义 Header，但<strong>不要依赖</strong> Transport 扩展去覆盖凭证头。

 

#### 核心源码

 

<table>&#10;<thead>&#10;<tr>&#10;<th>文件</th>&#10;<th>作用</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><code>extension/transport/types.go</code></td>&#10;<td>Provider / Interceptor 接口定义</td>&#10;</tr>&#10;<tr>&#10;<td><code>extension/transport/registry.go</code></td>&#10;<td><code>Register()</code> 注册单个 Provider（last-write-wins）</td>&#10;</tr>&#10;</tbody>&#10;</table>

 

## 扩展 3：Restrict — 命令裁剪（启动期）

 

### 它是什么

 

Restrict 让你声明一条或多条"裁剪规则"（<code>Rule</code>）：哪些命令能用、哪些不能、最高到什么风险、限定什么身份。框架启动时把整棵命令树过一遍——<strong>不符合规则的命令被改写成桩</strong>：从 help 里隐藏，一旦执行就返回 <code>command&#95;denied</code>。对命令分发来说它们还是合法命令，只是一跑就报错。

 

### 怎么用

 

```Go
platform.NewPlugin("secaudit", "1.0.0").
    Restrict(&platform.Rule{
        Name:    "agent-docs-readonly",
        Allow:   []string{"docs/**"}, // glob，"/" 是分隔符，"**" 匹配多段
        MaxRisk: "read",              // 风险上限：read | write | high-risk-write
    }).
    MustBuild()
```

 

编译后可以查看当前生效的裁剪规则：

 

```text
$ ./my-lark-cli config policy show
{
  "source": "plugin",
  "source_name": "secaudit",
  "rules": [{"name": "agent-docs-readonly", "allow": ["docs/**"], "max_risk": "read", "allow_unannotated": false}],
  "denied_paths": 258
}
```

 

#### Rule 的字段

 

<table>&#10;<thead>&#10;<tr>&#10;<th>字段</th>&#10;<th>含义</th>&#10;<th>例子</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><code>Name</code></td>&#10;<td>规则名（必填）。<code>config policy show</code> 输出和 envelope 的 <code>rule&#95;name</code> 会带上它，方便排查</td>&#10;<td><code>"agent-docs-readonly"</code></td>&#10;</tr>&#10;<tr>&#10;<td><code>Description</code></td>&#10;<td>可选描述，仅供 <code>config policy show</code> 展示</td>&#10;<td><code>"Agent 只读 docs 命令"</code></td>&#10;</tr>&#10;<tr>&#10;<td><code>Allow</code></td>&#10;<td>白名单 glob，命中即通过；空表示"不限制路径"</td>&#10;<td><code>&#91;"docs/&#42;&#42;", "im/+messages-send"&#93;</code></td>&#10;</tr>&#10;<tr>&#10;<td><code>Deny</code></td>&#10;<td>黑名单 glob，命中即拒绝（优先于 Allow）</td>&#10;<td><code>&#91;"&#42;&#42;/+&#42;-delete"&#93;</code></td>&#10;</tr>&#10;<tr>&#10;<td><code>MaxRisk</code></td>&#10;<td>风险等级上限（read / write / high-risk-write）。也可用常量 <code>platform.RiskRead</code> / <code>RiskWrite</code> / <code>RiskHighRiskWrite</code></td>&#10;<td><code>"read"</code> 排除所有写类命令</td>&#10;</tr>&#10;<tr>&#10;<td><code>Identities</code></td>&#10;<td>限定身份白名单（user / bot）。空表示不按身份过滤</td>&#10;<td><code>&#91;&#93;platform.Identity&#123;"user"&#125;</code> 排除 bot 类命令</td>&#10;</tr>&#10;<tr>&#10;<td><code>AllowUnannotated</code></td>&#10;<td>是否放行未标注 risk&#95;level 的命令。<strong>默认 false（fail-closed）</strong>：未标注命令直接拒并报 <code>risk&#95;not&#95;annotated</code>；改 true 则这类命令跳过 MaxRisk 检查</td>&#10;<td><code>true</code>（把第三方 plugin 的老命令平滑接入）</td>&#10;</tr>&#10;</tbody>&#10;</table>

 

<strong>多条规则怎么组合（容易踩坑）：</strong>

 

<ul>&#10;<li>&#10;<p><strong>单条 Rule 内</strong>的四个维度（Allow / Deny / MaxRisk / Identities）是 <strong>AND</strong>：必须同时满足才放行。</p>&#10;</li>&#10;<li>&#10;<p><strong>多条 Rule 之间</strong>是 <strong>OR</strong>：命中<strong>任意一条</strong>的全部维度即放行。同一个 plugin 可以多次调 <code>Restrict</code>，每条是一个独立授权。</p>&#10;</li>&#10;<li>&#10;<p>每条 Rule 的 <code>Deny</code> 只在<strong>本条内</strong>生效，不能否决另一条的 Allow。要全局禁某命令，得在每条 Rule 里都 Deny（或都不 Allow）。</p>&#10;</li>&#10;<li>&#10;<p><strong>只接受一个 plugin 贡献规则</strong>：启动期发现 2 个以上不同 plugin 都贡献了 Rule，直接 abort 整个 CLI（<code>plugin&#95;conflict</code> / <code>multiple&#95;restrict&#95;plugins</code>），不做静默合并。</p>&#10;</li>&#10;<li>&#10;<p><code>&#126;/.lark-cli/policy.yml</code> 也能写规则,但优先级最低:只要程序里有插件自带了规则,这个 yaml 就不会被加载。</p>&#10;</li>&#10;</ul>

 

#### 多条 Rule 示例

 

不同命令组要不同 risk / 身份：

 

```Go
platform.NewPlugin("secaudit", "1.0.0").
    Restrict(&platform.Rule{ // docs 只读、限 user 身份
        Name:       "docs-readonly",
        Allow:      []string{"docs/**"},
        MaxRisk:    platform.RiskRead,
        Identities: []platform.Identity{"user"},
    }).
    Restrict(&platform.Rule{ // im 可写、user+bot 都允许
        Name:       "im-write",
        Allow:      []string{"im/**"},
        MaxRisk:    platform.RiskWrite,
        Identities: []platform.Identity{"user", "bot"},
    }).
    MustBuild()
```

 

命令命中任意一条的全部维度即放行；全都没命中时 envelope 报 <code>no&#95;matching&#95;rule</code>（只有一条 Rule 时仍报各自具体的 reason\_code）。

 

### 诊断：命令为什么被拒？

 

两个只读、不调任何 API 的诊断命令，排查时先用它们：

 

<table>&#10;<thead>&#10;<tr>&#10;<th>命令</th>&#10;<th>关注</th>&#10;<th>数据</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><code>config policy show</code></td>&#10;<td>"当前生效<strong>裁剪规则</strong>是啥"</td>&#10;<td>Rule + denied&#95;paths + source</td>&#10;</tr>&#10;<tr>&#10;<td><code>config plugins show</code></td>&#10;<td>"都装了哪些 plugin、各自贡献啥"</td>&#10;<td>完整 plugin 清单 + 各自 hook 列表</td>&#10;</tr>&#10;</tbody>&#10;</table>

 

#### config policy show

 

查看 CLI 当前生效的裁剪规则、来源和被裁路径数。适用于：排查"为什么我的命令被拒"、验证 plugin 是否成功贡献了 Rule、检查用户 yaml 是否被 plugin 影子覆盖。

 

```text
$ ./my-cli config policy show
{
  "source": "plugin",
  "source_name": "secaudit",
  "rules": [{
    "name": "agent-docs-readonly",
    "description": "",
    "allow": ["docs/**"],
    "deny": null,
    "max_risk": "read",
    "identities": null,
    "allow_unannotated": false
  }],
  "denied_paths": 258
}
```

 

<table>&#10;<thead>&#10;<tr>&#10;<th>字段</th>&#10;<th>含义</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><code>source</code></td>&#10;<td>当前生效来源：<code>"plugin"</code> / <code>"yaml"</code> / <code>"none"</code></td>&#10;</tr>&#10;<tr>&#10;<td><code>source&#95;name</code></td>&#10;<td>来源标签：仅当 <code>source</code> 为 plugin 时是插件名；yaml 与 none 来源下为空(定位 yaml 配置路径请查 <code>&#126;/.lark-cli/policy.yml</code>)</td>&#10;</tr>&#10;<tr>&#10;<td><code>rules</code></td>&#10;<td>已决议、已落地的 Rule 列表（单条时数组长度为 1）</td>&#10;</tr>&#10;<tr>&#10;<td><code>denied&#95;paths</code></td>&#10;<td>被裁的命令路径总数（含父组聚合）</td>&#10;</tr>&#10;</tbody>&#10;</table>

 

#### config plugins show

 

列出<strong>所有成功装载的 plugin</strong> 及其完整贡献：能力声明、贡献的 Rule、注册的 Hook（Observer / Wrapper / Lifecycle）。适用于：拿到别人编译的二进制想知道"到底装了啥"、排查"为啥我的命令前后多了一堆日志"、审计某 plugin 注册了哪些 Wrapper / Lifecycle、验证 plugin 是否完整安装。

 

```text
$ ./my-cli config plugins show
{
  "plugins": [
    {
      "name": "secaudit",
      "version": "1.0.0",
      "capabilities": { "restricts": true, "failure_policy": "FailClosed" },
      "rules": [{
        "name": "agent-docs-readonly",
        "allow": ["docs/**"], "deny": null,
        "max_risk": "read", "identities": null, "allow_unannotated": false
      }],
      "hooks": {
        "count": 5,
        "observers": [
          { "name": "secaudit.audit-pre",  "when": "Before" },
          { "name": "secaudit.audit-post", "when": "After"  }
        ],
        "wrappers": [ { "name": "secaudit.approval" } ],
        "lifecycle": [
          { "name": "secaudit.init",  "event": "Startup"  },
          { "name": "secaudit.flush", "event": "Shutdown" }
        ]
      }
    }
  ],
  "total": 1
}
```

 

<strong>如何辨认 hook 归属：</strong> 框架在注册时把 hookName 自动加上 plugin 名前缀，分隔符是 "."。所以 <code>secaudit.boot</code> 就是 <code>secaudit</code> plugin 注册的 <code>boot</code> hook。plugin 名禁用 "."，分隔无歧义。

 

## 扩展 4：Observer — 旁观 Hook（只看不拦）

 

### 它是什么

 

Observer 让你注册一个"观察者"，命令执行<strong>前</strong>（<code>Before</code>）或<strong>后</strong>（<code>After</code>）跑你的代码。它不参与主流程、<strong>不能拦命令</strong>，<strong>它出错或 panic 都不影响命令本身，</strong>常用于审计、埋点、监控。

 

### 怎么用

 

```Go
platform.NewPlugin("secaudit", "1.0.0").
    // 命令运行前：记审计
    Observer(platform.Before, "audit-pre", platform.All(),
        func(ctx context.Context, inv platform.Invocation) {
            log.Printf("pre  %s denied=%v", inv.Cmd().Path(), inv.DeniedByPolicy())
        }).
    // 命令运行后：记结果
    Observer(platform.After, "audit-post", platform.All(),
        func(ctx context.Context, inv platform.Invocation) {
            log.Printf("post %s err=%v", inv.Cmd().Path(), inv.Err())
        }).
    FailOpen().
    MustBuild()
```

 

#### Selector：决定 Hook 在哪些命令上生效

 

<code>platform.All()</code> 是"所有命令"。还有按维度筛选的 Selector：

 

```Go
platform.ByDomain("docs")     // docs 域所有命令
platform.ByExactRisk("write") // 风险等级精确为 write 的命令
platform.ByWrite()            // 风险为 write 或 high-risk-write
platform.ByReadOnly()         // 风险为 read 的命令
platform.ByIdentity("bot")    // 限定身份
```

 

用 <code>.And()</code> / <code>.Or()</code> / <code>.Not()</code> 组合：

 

```Go
// im 域，但排除 +chats-list
platform.ByDomain("im").And(platform.ByCommandPath("im/+chats-list").Not())

// docs 或 im 域（任一命中即可）
platform.ByDomain("docs").Or(platform.ByDomain("im"))
```

 

#### Observer 的三条边界

 

<ul>&#10;<li>&#10;<p><strong>不能短路</strong>：返回值会被丢弃，改变不了命令是否执行。</p>&#10;</li>&#10;<li>&#10;<p><strong>panic 安全</strong>：框架用 <code>recover</code> 包着，Observer panic 不会冒到外层。</p>&#10;</li>&#10;<li>&#10;<p><strong>被裁命令也跑</strong>：<code>inv.DeniedByPolicy()</code> 为 true 时也会触发（这是 Observer 跟 Wrapper 最大的不同），方便审计被拒命令。</p>&#10;</li>&#10;</ul>

 

#### Invocation：Observer 和 Wrap 的运行期上下文

 

<table>&#10;<thead>&#10;<tr>&#10;<th>方法</th>&#10;<th>返回 / 含义</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><code>Cmd() CommandView</code></td>&#10;<td>命令视图，可调 <code>.Path()</code> / <code>.Risk()</code> / <code>.Identities()</code> / <code>.Domain()</code></td>&#10;</tr>&#10;<tr>&#10;<td><code>Args() &#91;&#93;string</code></td>&#10;<td>本次调用的位置参数（不含 flag）</td>&#10;</tr>&#10;<tr>&#10;<td><code>Started() time.Time</code></td>&#10;<td>命令开始时间，可在 After 里算耗时</td>&#10;</tr>&#10;<tr>&#10;<td><code>Err() error</code></td>&#10;<td>仅 After 有意义：命令 RunE 的返回错误（含 AbortError）</td>&#10;</tr>&#10;<tr>&#10;<td><code>DeniedByPolicy() bool</code></td>&#10;<td>是否被裁（user-layer Rule 或 strict-mode 都算）</td>&#10;</tr>&#10;<tr>&#10;<td><code>DenialLayer() string</code></td>&#10;<td>裁的来源：<code>"policy"</code> / <code>"strict&#95;mode"</code>；没被裁时为空</td>&#10;</tr>&#10;<tr>&#10;<td><code>DenialPolicySource() string</code></td>&#10;<td>policy 层 Rule 的来源标签：<code>plugin:&lt;name&gt;</code> / <code>yaml</code></td>&#10;</tr>&#10;</tbody>&#10;</table>

 

## 扩展 5：Wrap — 中间件 Hook（能拦能改）

 

### 它是什么

 

Wrap 是"洋葱模式"中间件，像洋葱一样包住命令的真正逻辑。你拿到一个 <code>next</code> 函数，在调用 <code>next</code> 之前 / 之后做事，也可以直接拦下（返回 AbortError）。常用于审批、限流、注入 token。

 

Wrap 链按注册顺序左→右组合，<strong>外层先跑</strong>。

 

### 怎么用

 

经典场景：<strong>写类命令必须过审批</strong>。

 

```Go
.Wrap("approval", platform.ByWrite(),
    func(next platform.Handler) platform.Handler {
        return func(ctx context.Context, inv platform.Invocation) error {
            if !approvalClient.Check(ctx, inv.Cmd().Path()) {
                return &platform.AbortError{
                    HookName: "approval",
                    Reason:   "write requires approval",
                }
            }
            return next(ctx, inv)
        }
    })
```

 

效果：

 

```JSON
{
  "ok": false,
  "error": {
    "type": "hook",
    "message": "hook \"secaudit.approval\" aborted: write requires approval",
    "detail": {
      "hook_name": "secaudit.approval",
      "reason": "write requires approval",
      "reason_code": "aborted"
    }
  }
}
```

 

### 多个 Wrapper 的组合顺序

 

注册顺序左→右，外层先跑：

 

```Go
.Wrap("logging",   All(),     loggingWrapper).
.Wrap("approval",  ByWrite(), approvalWrapper).
.Wrap("ratelimit", All(),     ratelimitWrapper)
```

 

```text
logging → approval → ratelimit → 原 RunE
```

 

## Observer vs Wrap：到底用哪个？

 

两者长得像（都靠 <code>Invocation</code> 拿上下文），但语义差很远。一句话：<strong>只想"看"用 </strong><strong>Observer</strong><strong>，要"拦 / 改"用 Wrap。</strong>

 

<table>&#10;<thead>&#10;<tr>&#10;<th>维度</th>&#10;<th>Observer（旁观）</th>&#10;<th>Wrapper（中间件）</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td>能拦下命令吗</td>&#10;<td>不能，返回值被丢弃</td>&#10;<td><strong>能</strong>，返回 <code>AbortError</code> 即短路</td>&#10;</tr>&#10;<tr>&#10;<td>能改命令是否执行吗</td>&#10;<td>不能</td>&#10;<td><strong>能</strong></td>&#10;</tr>&#10;<tr>&#10;<td>panic 怎么处理</td>&#10;<td>框架 recover，不影响命令</td>&#10;<td>框架 recover，不污染外层</td>&#10;</tr>&#10;<tr>&#10;<td>被裁（denied）的命令</td>&#10;<td><strong>照样跑</strong>（方便审计被拒命令）</td>&#10;<td>不跑</td>&#10;</tr>&#10;<tr>&#10;<td>触发时机</td>&#10;<td>Before / After 两个点</td>&#10;<td>包在命令真正逻辑外面（洋葱）</td>&#10;</tr>&#10;<tr>&#10;<td>典型用途</td>&#10;<td>审计、埋点、监控</td>&#10;<td>审批、限流、token 注入</td>&#10;</tr>&#10;</tbody>&#10;</table>

 

## 扩展 6：On — 生命周期（进程级）

 

### 它是什么

 

On 注册进程级生命周期处理器：

 

<ul>&#10;<li>&#10;<p><strong>Startup</strong>：CLI 启动期、所有 plugin 装好、命令树定型后<strong>只跑一次</strong>。返错或 panic = <strong>整树罢工</strong>（fail-closed）。</p>&#10;</li>&#10;<li>&#10;<p><strong>Shutdown</strong>：<code>cmd.Execute</code> 返回前跑。<strong>2 秒硬超时</strong>，错误<strong>只记录不上抛</strong><strong>。</strong></p>&#10;</li>&#10;</ul>

 

常用于初始化连接池、退出前刷盘。

 

### 怎么用

 

```Go
.On(platform.Startup, "init",
    func(ctx context.Context, lc *platform.LifecycleContext) error {
        return auditBuf.Open(ctx) // 启动期：加载审计 buffer
    }).
.On(platform.Shutdown, "flush",
    func(ctx context.Context, lc *platform.LifecycleContext) error {
        return auditBuf.Flush(ctx) // 退出前：刷盘
    })
```

 

<strong>Startup 失败的影响：</strong> 任何命令都会拿到下面这个 envelope，直到把 Startup 错误修复。

 

```JSON
{
  "ok": false,
  "error": {
    "type": "plugin_lifecycle",
    "message": "lifecycle hook \"secaudit.init\" failed: connect: connection refused",
    "detail": {
      "reason_code": "lifecycle_failed",
      "hook_name": "secaudit.init",
      "event": "startup"
    }
  }
}
```

 

#### LifecycleContext 字段

 

<table>&#10;<thead>&#10;<tr>&#10;<th>字段</th>&#10;<th>含义</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><code>Event LifecycleEvent</code></td>&#10;<td>当前事件：<code>Startup</code> / <code>Shutdown</code>。一个 handler 注册多个 event 时用来分流</td>&#10;</tr>&#10;<tr>&#10;<td><code>Err error</code></td>&#10;<td>仅 Shutdown 有意义：<code>cmd.Execute()</code> 返回的命令错误。可据此区分"正常退出"与"异常退出"</td>&#10;</tr>&#10;</tbody>&#10;</table>

 

## 扩展 1 及扩展 2 完整示例

 

将 Credential \+ Transport 两个基础扩展组合在一起的 wrapper main：

 

```Go
package main

import (
    "os"

"github.com/larksuite/cli/cmd" 

// 自定义扩展（写在前面 = 凭证优先级更高）
    _ "my-cli/mycred"    // 自定义凭证来源
    _ "my-cli/tracing"   // HTTP 请求拦截

// 原版 CLI 兜底
    _ "github.com/larksuite/cli/extension/credential/env"
)

func main() {
    os.Exit(cmd.Execute())
}
```

 

<strong>对比原版 CLI 的 main.go</strong>，只多了 2 行 import。业务代码 <code>cmd.Execute()</code> 完全不变，编译出来就是一个功能完整的增强版 CLI。

 

### 项目结构

 

```text
my-cli/
├── go.mod             # require github.com/larksuite/cli v1.0.6
├── main.go            # Wrapper main
├── mycred/
│   └── mycred.go      # Credential: 自定义凭证来源
└── tracing/
    └── tracing.go     # Transport: HTTP 请求拦截
```

 

### 构建和运行

 

```Bash
mkdir my-cli && cd my-cli
go mod init my-cli
# 编写 main.go、mycred/mycred.go、tracing/tracing.go（见上方代码）
go get github.com/larksuite/cli@v1.0.6
go mod tidy
go build -o my-lark-cli .

# 验证凭证扩展（--dry-run 不发真实请求，只看用了哪组凭证）
./my-lark-cli api GET /open-apis/calendar/v4/calendars --dry-run
```

 

输出：

 

```JSON
{
  "appId": "cli_aXXXXXXXXXXXXXXXX",
  "as": "user"
}
```

 

```Bash
# 验证请求拦截（发送真实请求，查看日志输出）
./my-lark-cli api GET /open-apis/calendar/v4/calendars --as user
```

 

输出：

 

```text
🚀 请求开始: GET https://open.feishu.cn/open-apis/authen/v1/user_info
✅ 请求完成: GET .../user_info 状态码=200 (耗时 644ms)
🚀 请求开始: GET https://open.feishu.cn/open-apis/calendar/v4/calendars
✅ 请求完成: GET .../calendars 状态码=200 (耗时 1070ms)
{ "code": 0, "data": { "calendar_list": [...] } }
```

 

两个扩展同时生效：Credential 提供了凭证和 Token，Transport 拦截并记录了每个请求。

 

## 附录 A：进阶能力\-更精细化的 hook 能力

 

前面 Restrict / Observer / Wrap / On 都用 Builder 写法。Builder 其实是对下面底层接口的封装。<strong>需要在结构体上挂依赖（审批客户端、连接池）、或 Install 逻辑复杂时</strong>，直接实现接口更清晰。每个 plugin 必须实现 4 个方法：

 

```Go
type Plugin interface {
    Name() string               // 全局唯一，正则 ^[a-z0-9][a-z0-9-]*$，禁用 "."
    Version() string            // 任意字符串，仅用于诊断
    Capabilities() Capabilities // 安全声明
    Install(r Registrar) error  // 启动期被调一次，在这里通过 r 注册具体能力
}
```

 

<code>Capabilities</code> 是<strong>安全契约</strong>，每次写新 plugin 必须明确填写：

 

```Go
type Capabilities struct {
    Restricts          bool          // 我会不会调 r.Restrict（命令裁剪）？默认 false
    FailurePolicy      FailurePolicy // FailOpen（装失败就跳过）/ FailClosed（装失败整树 abort）
    RequiredCLIVersion string        // 例如 ">=1.0.0"；不满足按 FailurePolicy 处理
}
```

 

<strong>硬性约束：</strong> <code>Restricts=true</code> <strong>必须</strong>配 <code>FailurePolicy=FailClosed</code>。框架一致性检查不通过会<strong>无条件 abort</strong>（即使声明 FailOpen）——安全相关的 plugin 不应该被 FailOpen 静默跳过。 <strong>用 Builder 就不用操心这些</strong>：<code>NewPlugin(...).Restrict(...)</code> 会自动把 <code>Restricts=true</code> 和 <code>FailClosed</code> 配齐。

 

<strong>Builder vs 接口实现，怎么选：</strong>

 

<ul>&#10;<li>&#10;<p>简单 plugin（只挂 Observer / Wrap）→ 用 <strong>Builder</strong>，一行一个能力，零样板</p>&#10;</li>&#10;<li>&#10;<p>持有外部依赖（审批客户端、连接池）→ 实现 <strong>interface</strong> 让依赖落在 struct 字段上更清晰</p>&#10;</li>&#10;</ul>

 

Builder 还提供 <code>FailOpen()</code> / <code>FailClosed()</code> 显式 setter：只挂 Observer / Wrap 不调 Restrict 的 plugin 默认 <code>FailOpen</code>，但你可能想强制 <code>FailClosed</code>（plugin 装失败整树罢工，保证审计不丢）。一旦调过 <code>Restrict()</code>，Builder 会强制 <code>FailClosed</code>，再调 <code>FailOpen()</code> 会在 <code>Build()</code> 阶段报错。

 

## 附录 B：错误对照表

 

所有命令拒绝都走 <code>command&#95;denied</code>,<code>detail.layer</code> 区分来源——<code>policy</code> 为用户层 Rule,<code>strict&#95;mode</code> 为内置 strict\-mode 裁剪。

 

<table>&#10;<thead>&#10;<tr>&#10;<th>错误类型</th>&#10;<th>错误码</th>&#10;<th>含义</th>&#10;<th>怎么修</th>&#10;</tr>&#10;</thead>&#10;<tbody>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>restricts&#95;mismatch</code></td>&#10;<td>Restricts=true 但 FailurePolicy≠FailClosed</td>&#10;<td>改成 <code>FailurePolicy: platform.FailClosed</code></td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>invalid&#95;capability</code><br></td>&#10;<td>FailurePolicy 不是 FailOpen/FailClosed,或 RequiredCLIVersion 格式非法</td>&#10;<td>用合法枚举值;version 用单段 semver(≤3 段)</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>invalid&#95;plugin&#95;name</code></td>&#10;<td>Name 不符合 <code>^&#91;a-z0-9&#93;&#91;a-z0-9-&#93;&#42;$</code></td>&#10;<td>改名(小写、连字符、不含点)</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>duplicate&#95;plugin&#95;name</code></td>&#10;<td>两个 plugin 用了同一个 Name</td>&#10;<td>改名让其中一个唯一</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>plugin&#95;name&#95;panic</code></td>&#10;<td>Plugin.Name() 在调用中 panic</td>&#10;<td>排查 Name();不要做 I/O 或访问可能 nil 的字段</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>capabilities&#95;panic</code></td>&#10;<td>Plugin.Capabilities() panic</td>&#10;<td>同上</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>capability&#95;unmet</code></td>&#10;<td>RequiredCLIVersion 不满足</td>&#10;<td>升级 CLI 或放宽约束</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>install&#95;panic</code></td>&#10;<td>Install 里 panic 了</td>&#10;<td>排查 Install 函数体</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>install&#95;failed</code></td>&#10;<td>Install 返回非 nil 的 error</td>&#10;<td>看 envelope detail 里包裹的原始 error</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>invalid&#95;rule</code></td>&#10;<td>Rule 字段非法(如 max&#95;risk 不在枚举内、glob 解析失败)</td>&#10;<td>校验 Rule 字段;MaxRisk 用 read/write/high-risk-write</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>invalid&#95;hook&#95;name</code></td>&#10;<td>hookName 不符合 <code>^&#91;a-z0-9&#93;&#91;a-z0-9-&#93;&#42;$</code>(不能含 ".")</td>&#10;<td>改名;框架自动加 plugin 名前缀</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>duplicate&#95;hook&#95;name</code></td>&#10;<td>同一 plugin 注册了两个同名 hook</td>&#10;<td>改名让其唯一</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;install</code></td>&#10;<td><code>invalid&#95;hook&#95;registration</code></td>&#10;<td>Wrapper factory 返回 nil 或 selector 不合法</td>&#10;<td>确保 Wrapper 构造返回非 nil Handler</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;conflict</code></td>&#10;<td><code>multiple&#95;restrict&#95;plugins</code></td>&#10;<td>2+ 不同 plugin 都贡献 Rule(同一 plugin 多条不算冲突)</td>&#10;<td>只保留一个贡献 Rule 的 plugin</td>&#10;</tr>&#10;<tr>&#10;<td><code>plugin&#95;lifecycle</code></td>&#10;<td><code>lifecycle&#95;failed</code> / <code>lifecycle&#95;panic</code></td>&#10;<td>Startup handler 返错 / panic</td>&#10;<td>排查 Startup 逻辑</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>domain&#95;not&#95;allowed</code></td>&#10;<td>命令不在 Allow 里</td>&#10;<td>加进 Allow 或换条命令</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>command&#95;denylisted</code></td>&#10;<td>命令命中 Deny 列表</td>&#10;<td>从 Deny 移除或换命令</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>write&#95;not&#95;allowed</code></td>&#10;<td>写类命令风险超过 MaxRisk</td>&#10;<td>提高 MaxRisk 或不用该命令</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>risk&#95;too&#95;high</code></td>&#10;<td>命令风险超过 MaxRisk(非写类,保留位)</td>&#10;<td>提高 MaxRisk</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>risk&#95;not&#95;annotated</code></td>&#10;<td>命令未标注 risk&#95;level,Rule 默认 fail-closed。<strong>注</strong>:内置命令已全部标注(commit <code>5d9d438</code>),该错误主要出现在第三方 plugin 注册的命令</td>&#10;<td>给命令补 risk&#95;level 注解,或 Rule 设 AllowUnannotated=true</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>risk&#95;invalid</code></td>&#10;<td>risk&#95;level 拼错(如 wrtie),不在枚举内</td>&#10;<td>改成合法枚举(错误信息含 did-you-mean 提示)</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>identity&#95;mismatch</code></td>&#10;<td>命令身份与 Rule.Identities 不交集</td>&#10;<td>放宽 Identities 或换合适身份</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>no&#95;matching&#95;rule</code></td>&#10;<td>注册了多条 Rule,命令一条都没命中(单条时仍报各自具体 reason&#95;code)</td>&#10;<td>放宽某条 Rule,或新增一条覆盖该命令的 Rule</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>identity&#95;not&#95;supported</code></td>&#10;<td>strict-mode 层裁命令:命令支持的身份与当前 strict-mode 不交集</td>&#10;<td>调整 <code>config strict-mode</code> 或选支持当前身份的命令</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>all&#95;children&#95;denied</code></td>&#10;<td>父组下所有子命令全部被拒,父节点也自动被裁(聚合)</td>&#10;<td>放宽该域的 Allow 或允许部分子命令</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>mixed&#95;children&#95;strict&#95;mode</code></td>&#10;<td>父组下部分子命令被 strict-mode 裁;父节点聚合显示,<code>detail.layer = strict&#95;mode</code></td>&#10;<td>看具体子命令的拒绝详情;通常是 strict-mode 配置问题</td>&#10;</tr>&#10;<tr>&#10;<td><code>command&#95;denied</code></td>&#10;<td><code>mixed&#95;children&#95;policy</code></td>&#10;<td>父组下部分子命令被 user-layer policy 裁;父节点聚合显示,<code>detail.layer = policy</code></td>&#10;<td>放宽该域的 Allow / Deny 或允许特定子命令</td>&#10;</tr>&#10;<tr>&#10;<td><code>hook</code></td>&#10;<td><code>aborted</code><br></td>&#10;<td>Wrap 体内显式返回 <code>&#42;AbortError</code> 短路</td>&#10;<td>看 envelope detail 的 <code>hook&#95;name</code> + <code>reason</code></td>&#10;</tr>&#10;<tr>&#10;<td><code>hook</code></td>&#10;<td><code>panic</code></td>&#10;<td>Wrap 体内 panic 被框架兜底捕获</td>&#10;<td>修复 Wrapper 代码;框架已 recover,不会污染外层</td>&#10;</tr>&#10;</tbody>&#10;</table>
