跳转到内容

如何实现 Capability

本节介绍如何为 ESP-Claw 编写一个自定义 Capability,完整地覆盖从文件结构、描述符定义到 Group 注册的全流程。

实现 Capability 前需理解两个核心结构体,均定义于 claw_cap.h

claw_cap_descriptor_t — 单个能力描述符

Section titled “claw_cap_descriptor_t — 单个能力描述符”
typedef struct {
    const char *id;                  // 唯一标识,调用时使用
    const char *name;                // 展示名(通常与 id 相同)
    const char *family;              // 逻辑分组标签(如 "im"、"files"、"system")
    const char *description;         // 给 LLM 看的功能描述
    claw_cap_kind_t kind;            // CALLABLE / EVENT_SOURCE / HYBRID
    uint32_t cap_flags;              // 标志位,如 CLAW_CAP_FLAG_CALLABLE_BY_LLM
    const char *input_schema_json;   // JSON Schema 格式的输入参数定义
    // 生命周期钩子(可选)
    esp_err_t (*init)(void);
    esp_err_t (*start)(void);
    esp_err_t (*stop)(void);
    // 执行回调(CALLABLE 必须实现)
    esp_err_t (*execute)(const char *input_json,
                         const claw_cap_call_context_t *ctx,
                         char *output,
                         size_t output_size);
} claw_cap_descriptor_t;
typedef struct {
    const char *group_id;                    // Group 唯一标识
    const char *plugin_name;                 // 插件名(可选)
    const char *version;                     // 版本号(可选)
    void *plugin_ctx;                        // 插件私有上下文(可选)
    const claw_cap_descriptor_t *descriptors; // 描述符数组
    size_t descriptor_count;
    // Group 级生命周期钩子(可选,与描述符内钩子互补)
    esp_err_t (*group_init)(void);
    esp_err_t (*group_start)(void);
    esp_err_t (*group_stop)(void);
} claw_cap_group_t;
  • 文件夹components/claw_capabilities/cap_my_feature
    • CMakeLists.txt
    • 文件夹include
      • cap_my_feature.h 头文件
    • 文件夹src
      • cap_my_feature.c 具体实现
    • 文件夹skills skills 目录,可选,见下文文档
      • cap_my_feature.md Skill 使用文档(Markdown)
      • skills_list.json Skill 元数据

CMakeLists.txt 最小示例:

idf_component_register(
    SRCS "src/cap_my_feature.c"
    INCLUDE_DIRS "include"
    REQUIRES claw_cap cJSON # 至少需要依赖 claw_cap 核心库与 cJSON 库用于解析 JSON 数据
)

头文件只暴露注册函数,避免泄露内部状态:

// include/cap_my_feature.h
#pragma once
#include "esp_err.h"

#ifdef __cplusplus
extern "C" {
#endif

esp_err_t cap_my_feature_register_group(void);

#ifdef __cplusplus
}
#endif

execute 是 Capability 的核心:接收 JSON 字符串输入,将结果写入 output 缓冲区。

static esp_err_t my_feature_execute(const char *input_json,
                                     const claw_cap_call_context_t *ctx,
                                     char *output,
                                     size_t output_size)
{
    cJSON *root = cJSON_Parse(input_json ? input_json : "{}");
    if (!root) {
        snprintf(output, output_size, "Error: invalid JSON");
        return ESP_ERR_INVALID_ARG;
    }

    // 从 ctx 中获取调用上下文(会话 ID、频道、chat_id 等)
    const char *session_id = ctx ? ctx->session_id : NULL;

    cJSON *param = cJSON_GetObjectItem(root, "param");
    if (!cJSON_IsString(param)) {
        cJSON_Delete(root);
        snprintf(output, output_size, "Error: param is required");
        return ESP_ERR_INVALID_ARG;
    }

    // 执行实际逻辑 ...
    snprintf(output, output_size, "Done: %s", param->valuestring);

    cJSON_Delete(root);
    return ESP_OK;
}

关键规则:

  • 输出必须写入 output 缓冲区,不要直接打印到串口
  • 返回 ESP_OK 表示执行成功;非零返回码会被框架记录并返回给调用方
  • 错误信息同样写入 output,以 "Error: ..." 开头是惯例
  • claw_cap_call_context_t 包含 session_idchat_idsource_channelcaller 等,可用于多会话/多渠道场景
static const claw_cap_descriptor_t s_my_descriptors[] = {
    {
        .id = "my_action",
        .name = "my_action",
        .family = "custom",
        .description = "Perform my custom action with the given param.",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .input_schema_json =
            "{\"type\":\"object\","
            "\"properties\":{\"param\":{\"type\":\"string\"}},"
            "\"required\":[\"param\"]}",
        .execute = my_feature_execute,
    },
};

static const claw_cap_group_t s_my_group = {
    .group_id = "cap_my_feature",
    .descriptors = s_my_descriptors,
    .descriptor_count = sizeof(s_my_descriptors) / sizeof(s_my_descriptors[0]),
};
esp_err_t cap_my_feature_register_group(void)
{
    if (claw_cap_group_exists(s_my_group.group_id)) {
        return ESP_OK;
    }
    return claw_cap_register_group(&s_my_group);
}

app_claw.c(或你的应用初始化文件)中调用:

#include "cap_my_feature.h"

void app_claw_start(void)
{
    // ... 其他注册 ...
    cap_my_feature_register_group();
    // 如需对 LLM 可见,加入可见 group 列表
}

如果 Capability 需要持有后台任务(如轮询、定时器),通过 init / start / stop 钩子管理:

static esp_err_t my_feature_start(void)
{
    // 创建 FreeRTOS 任务、定时器等
    s_running = true;
    return xTaskCreate(my_poll_task, "my_poll", 4096, NULL, 5, &s_task) == pdPASS
               ? ESP_OK : ESP_FAIL;
}

static esp_err_t my_feature_stop(void)
{
    s_running = false;
    // 等待任务退出...
    return ESP_OK;
}

// 在描述符中填写钩子
static const claw_cap_descriptor_t s_my_descriptors[] = {
    {
        // ...
        .init  = NULL,           // 一次性初始化(可选)
        .start = my_feature_start,
        .stop  = my_feature_stop,
    },
};

若 Capability 需要主动产生事件(如 IM 接收消息),将 kind 设为 CLAW_CAP_KIND_EVENT_SOURCE 并在后台任务中调用 Event Router 接口:

// 产生文本消息事件(最常见)
claw_event_router_publish_message(
    "my_gateway",   // source_cap
    "my_channel",   // source_channel
    chat_id,        // chat_id
    text,           // 消息正文
    sender_id,      // 发送者 ID
    message_id      // 消息 ID
);

// 产生自定义事件
claw_event_t event = {0};
strlcpy(event.source_cap, "my_gateway", sizeof(event.source_cap));
strlcpy(event.event_type, "my_custom_event", sizeof(event.event_type));
// ... 填充其他字段 ...
claw_event_router_publish(&event);

事件产生后,claw_event_router 会根据规则决定是触发自动化、还是路由给 claw_core 做 Agent 推理。

在组件目录下添加 skills/ 子目录:

  • 文件夹cap_my_feature
    • 文件夹skills skills 目录
      • cap_my_feature.md Skill 使用文档(Markdown)
      • skills_list.json Skill 元数据

实际部署时,这些文件需拷贝到 FATFS 的 Skills 根目录(默认 /fatfs/skills/)下。

{
  "skills": [
    {
      "id": "my_feature",           // Skill 唯一标识,全局不重复
      "file": "cap_my_feature.md",  // .md 文件名(相对于 skills 根目录)
      "summary": "一句话描述这个 Skill 的用途,LLM 未激活时看到的摘要",
      "cap_groups": ["cap_my_feature"]  // 激活时同步开放的 Capability Group
    }
  ]
}

激活 Skill 时,cap_skill 会将列表中所有 Group 加入当前 session 的 LLM 工具可见白名单。一个 Skill 可以绑定多个 Group,例如一个「文件编辑」Skill 同时开放 cap_filescap_lua

关于 Skill 文档的撰写规范(模板、要素说明),请参阅 Skills 指南

需求模式参考实现
纯工具,无副作用简单 execute 回调cap_timecap_llm_inspect
需要读写文件系统路径校验 + POSIX IOcap_filescap_lua
持续轮询/推送事件后台任务 + EVENT_SOURCEcap_im_tg
调用 LLM 推理claw_core_llm_infer_*cap_llm_inspect
管理 core 运行时状态调用 claw_skill_* / claw_cap_*cap_skill