feat: the initial commit

This commit is contained in:
loop
2026-04-03 15:48:11 +08:00
committed by zhouli
parent 2f022ca58a
commit 2cb94a3055
147 changed files with 27827 additions and 8 deletions
+41
View File
@@ -0,0 +1,41 @@
# Build output
build/
dist/
tmp/
releases/
# ESP-IDF
sdkconfig
sdkconfig.old
dependencies.lock
managed_components/
# IDE / Editor
.vscode/
.devcontainer/
.idea/
.clangd
*.swp
*.swo
*~
# Environment
.env
# Cache / Compiled
.cache
*.pyc
*.bin
# MCP / Memov
.mcp.json
.mem/
.memignore
# OS
.DS_Store
Thumbs.db
references/
.venv/
.codex
openspec
AGENTS.md
+33
View File
@@ -0,0 +1,33 @@
spec:
inputs:
force_push_to_github:
type: boolean
default: false
---
stages:
- build
workflow:
rules:
# Disable those non-protected push triggered pipelines
- if: '$CI_COMMIT_REF_NAME != "master" && $CI_COMMIT_BRANCH !~ /^release\/v/ && $CI_COMMIT_TAG !~ /^v\d+\.\d+(\.\d+)?($|-)/ && $CI_PIPELINE_SOURCE == "push"'
when: never
# when running merged result pipelines, it would create a temp commit id. use $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA instead of $CI_COMMIT_SHA.
# Please use PIPELINE_COMMIT_SHA at all places that require a commit sha
- if: $CI_OPEN_MERGE_REQUESTS != null
variables:
PIPELINE_COMMIT_SHA: $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA
- if: $CI_OPEN_MERGE_REQUESTS == null
variables:
PIPELINE_COMMIT_SHA: $CI_COMMIT_SHA
- when: always
variables:
IOT_SOLUTION_PATH: "$CI_PROJECT_DIR"
CI_FORCE_PUSH_TO_GITHUB: "$[[ inputs.force_push_to_github ]]"
include:
- '.gitlab/ci/rules.yml'
- '.gitlab/ci/build.yml'
+4 -4
View File
@@ -44,7 +44,7 @@
script:
- pip install idf_build_apps
- |
python build_apps.py ${EXAMPLE_DIR} \
python .gitlab/ci/build_apps.py ${EXAMPLE_DIR} \
--config ${EXAMPLE_CONFIG} \
-t all \
-r ${BUILD_RECURSIVE} \
@@ -77,10 +77,10 @@
- IMAGE: espressif/idf:release-v5.5
- IMAGE: espressif/idf:release-v6.0
build_example_ai_esp_dl_human_activity_recognition:
build_application_basic_demo:
extends:
- .build_examples_template
- .rules:build:example_ai_esp_dl_human_activity_recognition
- .rules:build:application_basic_demo
- .build_idf_active_release_version
variables:
EXAMPLE_DIR: examples/ai/esp_dl/human_activity_recognition
EXAMPLE_DIR: application/basic_demo
+2 -4
View File
@@ -35,7 +35,7 @@ logger = logging.getLogger('idf_build_apps')
IDF_PATH = os.getenv('IDF_PATH', '')
BOARD_NAME = 'default'
PROJECT_ROOT = Path(__file__).parent.parent.absolute()
PROJECT_ROOT = Path(__file__).parent.parent.parent.absolute()
APPS_BUILD_PER_JOB = 30
IGNORE_WARNINGS = [
r'memory region \`iram_loader_seg\' not declared',
@@ -255,9 +255,7 @@ def get_cmake_apps(
no_preserve=False,
default_build_targets=default_build_targets,
manifest_files=[
str(Path(PROJECT_ROOT) /'components'/'.build-rules.yml'),
str(Path(PROJECT_ROOT) /'examples'/'.build-rules.yml'),
str(Path(PROJECT_ROOT) /'tools'/'.build-rules.yml'),
str(Path(PROJECT_ROOT) /'application'/'.build-rules.yml'),
],
build_system=CustomApp,
)
+149
View File
@@ -0,0 +1,149 @@
############
# Patterns #
############
# build system, if changed, build all apps
.patterns-build_system: &patterns-build_system
# For test
# - "tools/build_apps.py"
- "conftest.py"
- "pytest.ini"
# components folder, in the alphabetic order
.patterns-components_cap_cli: &patterns-components_cap_cli
- "components/cap_cli/**/*"
.patterns-components_cap_files: &patterns-components_cap_files
- "components/cap_files/**/*"
.patterns-components_cap_im_attachment: &patterns-components_cap_im_attachment
- "components/cap_im_attachment/**/*"
.patterns-components_cap_im_qq: &patterns-components_cap_im_qq
- "components/cap_im_qq/**/*"
.patterns-components_cap_im_tg: &patterns-components_cap_im_tg
- "components/cap_im_tg/**/*"
.patterns-components_cap_im_wechat: &patterns-components_cap_im_wechat
- "components/cap_im_wechat/**/*"
.patterns-components_cap_llm_inspect: &patterns-components_cap_llm_inspect
- "components/cap_llm_inspect/**/*"
.patterns-components_cap_lua: &patterns-components_cap_lua
- "components/cap_lua/**/*"
.patterns-components_cap_mcp_client: &patterns-components_cap_mcp_client
- "components/cap_mcp_client/**/*"
.patterns-components_cap_mcp_server: &patterns-components_cap_mcp_server
- "components/cap_mcp_server/**/*"
.patterns-components_cap_skill: &patterns-components_cap_skill
- "components/cap_skill/**/*"
.patterns-components_cap_time: &patterns-components_cap_time
- "components/cap_time/**/*"
.patterns-components_cap_web_search: &patterns-components_cap_web_search
- "components/cap_web_search/**/*"
.patterns-components_claw_cap: &patterns-components_claw_cap
- "components/claw_cap/**/*"
.patterns-components_claw_core: &patterns-components_claw_core
- "components/claw_core/**/*"
.patterns-components_claw_event_router: &patterns-components_claw_event_router
- "components/claw_event_router/**/*"
.patterns-components_claw_memory: &patterns-components_claw_memory
- "components/claw_memory/**/*"
.patterns-components_claw_skill: &patterns-components_claw_skill
- "components/claw_skill/**/*"
# application folder, in the alphabetic order
.patterns-application_basic_demo: &patterns-application_basic_demo
- "application/basic_demo/**/*"
.patterns-yml: &patterns-yml
- "**/*.yml"
##############
# if anchors #
##############
.if-protected: &if-protected
if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_BRANCH =~ /^release\/v/ || $CI_COMMIT_TAG =~ /^v\d+\.\d+(\.\d+)?($|-)/)'
.if-dev-push: &if-dev-push
if: '$CI_COMMIT_REF_NAME != "master" && $CI_COMMIT_BRANCH !~ /^release\/v/ && $CI_COMMIT_TAG !~ /^v\d+\.\d+(\.\d+)?($|-)/ && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event")'
##################
# Auto Generated #
##################
.if-trigger-job: &if-trigger-job
if: "$BOT_DYNAMIC_TEST_JOBS && $BOT_DYNAMIC_TEST_JOBS =~ $CI_JOB_NAME"
.if-label-build: &if-label-build
if: '$BOT_LABEL_BUILD || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*build(?:,[^,\n\r]+)*$/i'
.if-label-target_test: &if-label-target_test
if: '$BOT_LABEL_TARGET_TEST || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*target_test(?:,[^,\n\r]+)*$/i'
.if-label-build_docs: &if-label-build_docs
if: '$BOT_LABEL_BUILD_DOCS || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*build_docs(?:,[^,\n\r]+)*$/i'
.if-label-pre_check: &if-label-pre_check
if: '$BOT_LABEL_PRE_CHECK || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*pre_check(?:,[^,\n\r]+)*$/i'
.if_label-deploy: &if-label-deploy
if: '$BOT_LABEL_DEPLOY || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*deploy(?:,[^,\n\r]+)*$/i'
# rules for applications
.rules:build:application_basic_demo:
rules:
- <<: *if-protected
- <<: *if-label-build
- <<: *if-trigger-job
- <<: *if-dev-push
changes: *patterns-build_system
- <<: *if-dev-push
changes: *patterns-components_cap_cli
- <<: *if-dev-push
changes: *patterns-components_cap_files
- <<: *if-dev-push
changes: *patterns-components_cap_im_attachment
- <<: *if-dev-push
changes: *patterns-components_cap_im_qq
- <<: *if-dev-push
changes: *patterns-components_cap_im_tg
- <<: *if-dev-push
changes: *patterns-components_cap_im_wechat
- <<: *if-dev-push
changes: *patterns-components_cap_llm_inspect
- <<: *if-dev-push
changes: *patterns-components_cap_lua
- <<: *if-dev-push
changes: *patterns-components_cap_mcp_client
- <<: *if-dev-push
changes: *patterns-components_cap_mcp_server
- <<: *if-dev-push
changes: *patterns-components_cap_skill
- <<: *if-dev-push
changes: *patterns-components_cap_time
- <<: *if-dev-push
changes: *patterns-components_cap_web_search
- <<: *if-dev-push
changes: *patterns-components_claw_cap
- <<: *if-dev-push
changes: *patterns-components_claw_core
- <<: *if-dev-push
changes: *patterns-components_claw_event_router
- <<: *if-dev-push
changes: *patterns-components_claw_memory
- <<: *if-dev-push
changes: *patterns-components_claw_skill
- <<: *if-dev-push
changes: *patterns-application_basic_demo
+235
View File
@@ -0,0 +1,235 @@
# ESP-Clawgent
**事件驱动的 OpenClaw,为嵌入式场景量身定制。**
`ESP-Clawgent` 是一套运行于 ESP32 上的事件驱动 AI assistant,适用于需要长期运行、可持续扩展、功能持续迭代的设备侧 AI 场景
- 事件驱动:不局限于消息输入,多种外部事件都可以触发 Agent Loop
- 组件化:可按需裁切模块
- 离线自动化:在无网络条件下执行本地规则
- 自编程:内嵌 Lua 解释器,可由 AI 自主规划实现功能
- 渐进式工具披露:当前 agent 能做什么,由已加载和已激活的 skills 决定
- 超低资源占用:ESP32-C 系列上也能流畅运行
## 1. What is ESP-Clawgent
项目包含示例与基础功能组件。
- `application/basic_demo/`:当前主应用,用于将这些模块装配成可运行固件
`components/` 包含以下核心部分:
- `claw_core`:负责完整的 agent 执行流
- `claw_cap`:负责能力注册、调度和工具调用
- `claw_memory`:负责会话历史和长期记忆
- `claw_skill`:负责技能加载、技能文档和激活态技能上下文
- `claw_event_router`:负责标准事件接入、规则路由、Agent/脚本分发和出站路由
- `cap_*`:负责拓展具体能力,比如 QQ、Telegram、文件、时间、Web Search、MCP、Lua 等
## 2. How it works
主启动入口位于 `application/basic_demo/main/main.c`
设备启动后,整体流程如下:
1. 初始化 NVS,加载设备配置
2. 挂载 FATFS 到 `/fatfs/data`
3. 初始化 Wi-Fi 和本地 HTTP 配置服务
4. 进入 `app_clawgent_start()`
5. 初始化 event router、memory、skills、capabilities
6. 初始化并启动 `claw_core`
7. 启动 CLI,开始响应请求和事件
当前运行时依赖以下本地目录:
- `/fatfs/data/sessions`:会话历史
- `/fatfs/data/memory/MEMORY.md`:长期记忆
- `/fatfs/data/skills`skills 文档和清单
- `/fatfs/data/lua`Lua 脚本
- `/fatfs/data/automation/automations.json`:自动化规则
- `/fatfs/data/inbox`:消息附件存储目录
### Event-driven
`ESP-Clawgent` 的核心是“收到事件,触发行为”。
事件可以来自:
- 即时通讯入口
- 配置界面
- 本地事件规则
- 文件或附件输入
- 后续扩展的 cap 事件源
行为可以是:
- 调模型
- 调工具
- 读写本地文件
- 执行 Lua
- 触发事件路由链路
- 给外部 IM 回消息
### Progressive tool exposure
工具不会默认一次性全部暴露给模型。
`claw_core` 在运行时会接入这些上下文提供器:
- 长期记忆
- 会话历史
- skills list
- 已激活 skill 的文档
- 当前 cap 工具描述
初始情况下,仅暴露由 `skills_list.json` 声明的基础能力。
- skill 没加载,模型看不到对应能力的说明
- skill 没激活,模型拿不到对应文档上下文
- 当前会话能做什么,是逐步展开的
### Self-programming with Lua
项目内嵌 Lua 解释器,支持 Lua 脚本编辑与运行。
这使得以下能力成为可能:
- 把设备逻辑写成 Lua
- 把某些 agent 行为抽成脚本
- 让 assistant 在现有能力之上继续组合出新的功能
许多扩展可以先在 Lua 层完成,无需重新烧录固件。只要向 LLM 提出具体需求,系统即可直接生成并实现对应功能。
## 3. Project architecture
仓库结构如下:
```text
esp-clawgent-master/
├── components/
│ ├── claw_core/
│ ├── claw_cap/
│ ├── claw_event_router/
│ ├── claw_memory/
│ ├── claw_skill/
│ └── cap_*/
└── application/
└── basic_demo/
├── main/
└── build/
```
### Runtime layers
- Application layer
负责启动、配置、文件系统、Wi-Fi、HTTP 配置页和整机装配
- Core layer
`claw_core` 负责 agent 执行流
- cap layer
`claw_cap` 和各类 `cap_*` 负责工具和具体能力实现
- Event routing layer
`claw_event_router` 负责统一事件入口、规则匹配、脚本/agent 调度和出站分发
- Memory layer
`claw_memory` 负责持久化上下文
- Skill layer
`claw_skill` 负责按 skill 控制上下文与功能暴露
### Current capabilities
当前 `basic_demo` 已接入的能力包括:
- `cap_im_qq`
- `cap_im_tg`
- `cap_files`
- `cap_lua`
- `cap_mcp_client`
- `cap_mcp_server`
- `cap_skill`
- `cap_time`
- `cap_llm_inspect`
- `cap_web_search`
### Design style
项目的架构关键词包括:
- event-driven
- componentized
- local-first
联网并不是系统运行的唯一前提。无网络时,本地事件路由、Lua、文件系统和既有记忆仍可继续工作。
## 4. Quick Start
### Prerequisites
- ESP-IDF 环境已安装并导出
- 建议使用 `ESP-IDF v5.5.1`
- 默认目标芯片为 `esp32s3`
```bash
. /esp-idf/export.sh
```
### Build
所有 ESP-IDF 命令都在 `application/basic_demo/` 下执行:
```bash
cd application/basic_demo
idf.py set-target esp32s3
idf.py build
```
### Configure
当前 Demo 的关键配置包括:
- Wi-Fi SSID / Password
- LLM API Key / Provider / Model
- QQ App ID / App Secret
- Telegram Bot Token
- Brave / Tavily Search Key
- Timezone
可以通过 `menuconfig` 调整编译期默认值:
```bash
cd application/basic_demo
idf.py menuconfig
```
设备运行后,配置也会通过 NVS 持久化。
### Notes on Keys
- IM bot token:可通过 Telegram 的 [@BotFather](https://t.me/BotFather) 或 [QQ Bot](https://q.qq.com/qqbot/openclaw/login.html) 获取
- LLM API key:可使用 [Anthropic Console](https://console.anthropic.com)、[OpenAI Platform](https://platform.openai.com) 或 [阿里云百炼](https://bailian.console.aliyun.com/#/api-key) 提供的 Key
### Flash
```bash
cd application/basic_demo
idf.py flash monitor
```
如果串口不是默认值:
```bash
cd application/basic_demo
idf.py flash monitor -p /dev/ttyUSB0
```
### First boot
首次启动后,通常会看到以下阶段:
- NVS 初始化
- FATFS 挂载
- 设置加载
- Wi-Fi 和本地配置服务启动
- memory / skills / capabilities 初始化
- `claw_core` 启动
- CLI 启动
+8
View File
@@ -0,0 +1,8 @@
# Note: All operators are binary operators. For more than two operands, you may use the nested parentheses trick.
# For example:
# * A == 1 or (B == 2 and C in [1,2,3])
# * (A == 1 and B == 2) or (C not in ["3", "4", 5])
application/basic_demo:
enable:
- if: INCLUDE_DEFAULT == 1
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS "../../components")
if(NOT DEFINED IDF_TARGET)
set(IDF_TARGET "esp32s3" CACHE STRING "ESP-IDF target")
endif()
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(basic_demo)
fatfs_create_spiflash_image(storage fatfs_image FLASH_IN_PROJECT)
@@ -0,0 +1,97 @@
[
{
"id": "im_hello_clawgent",
"description": "Intercept IM hello! clawgent and reply directly.",
"enabled": true,
"ack": "{{event.source_channel}} hello intercepted",
"match": {
"event_type": "message",
"event_key": "text",
"content_type": "text",
"text": "hello! clawgent"
},
"actions": [
{
"type": "send_message",
"input": {
"channel": "{{event.source_channel}}",
"chat_id": "{{event.chat_id}}",
"message": "Nice to meet you!"
}
}
]
},
{
"id": "im_attachment_saved_reply",
"description": "Reply when an IM attachment is saved.",
"enabled": true,
"ack": "{{event.source_channel}} attachment saved",
"match": {
"event_type": "attachment_saved"
},
"actions": [
{
"type": "send_message",
"input": {
"channel": "{{event.source_channel}}",
"chat_id": "{{event.chat_id}}",
"message": "File received from {{event.source_channel}}"
}
}
]
},
{
"id": "mcp_state_report_to_script",
"description": "Route MCP state reports into Lua for deterministic handling.",
"enabled": true,
"ack": "mcp state report scripted",
"match": {
"event_type": "mcp_device_state_report",
"source_cap": "mcp_server",
"source_channel": "mcp"
},
"actions": [
{
"type": "run_script",
"input": {
"path": "/fatfs/data/lua/hello.lua",
"async": false,
"event_type": "{{event.event_type}}",
"source_channel": "{{event.source_channel}}",
"text": "{{event.text}}",
"payload_json": "{{event.payload_json}}",
"chat_id": "{{event.chat_id}}"
}
}
]
},
{
"id": "im_any_message_agent",
"description": "Route IM text messages to the agent.",
"enabled": true,
"consume_on_match": true,
"ack": "{{event.source_channel}} routed to agent",
"match": {
"event_type": "message",
"event_key": "text",
"content_type": "text"
},
"actions": [
{
"type": "run_agent",
"input": {
"target_channel": "{{event.source_channel}}",
"session_policy": "chat"
}
},
{
"type": "send_message",
"input": {
"channel": "{{event.source_channel}}",
"chat_id": "{{event.chat_id}}",
"message": "{{last.output}}"
}
}
]
}
]
@@ -0,0 +1 @@
print("hello lua!")
@@ -0,0 +1,3 @@
# Long-term Memory
(empty - Clawgent will write memories here as it learns)
@@ -0,0 +1,57 @@
# IM File Return
Use this skill when the user wants the device to send an existing local non-image file back to a chat.
## When to use
- The user asks to send back a file, report, log, archive, JSON, CSV, or other non-image output.
- The target conversation is already the active Telegram, QQ, or WeChat chat, or the user provides an explicit target `chat_id`.
## Available tools
- `list_dir`: inspect device storage and confirm the file path
- `read_file`: inspect small text files before sending when needed
- `cap_cli`: run `cap call <cap_name> '<json>'` to invoke the transport-specific send capability indirectly
## WeChat status
- This firmware variant exposes WeChat text and image send, but does not expose `wechat_send_file`.
- If the user asks to send a generic non-image file to WeChat, explain that the current WeChat path only supports text and image send.
- If the requested asset is actually an image, switch to the `im_send_picture` skill and use `wechat_send_image` through `cap_cli`.
## Path guidance
- Prefer real local paths that already exist on the device.
- Common roots in this demo are `/spiffs`, `/spiffs/lua`, `/spiffs/qq`, `/spiffs/tg`, or application-managed subdirectories.
- Use `list_dir` first if the exact path is unknown.
- Use `read_file` only for small text inspection, not for binary payloads.
## Sending rules
- Use this skill only for non-image files such as `.txt`, `.json`, `.log`, `.csv`, `.zip`, or archives.
- Use `cap_cli` with `cap call tg_send_file '<json>'` or `cap call qq_send_file '<json>'`.
- The JSON payload should include an explicit `chat_id`, `path`, and optional `caption`.
- Use Telegram file capability for Telegram chats and QQ file capability for QQ chats.
- Do not claim WeChat generic file-send support unless a real `wechat_send_file` capability exists.
- Pass `caption` only when the user wants an accompanying message.
- The second argument of `cap call` must be one complete JSON string. Do not rewrite it as CLI flags or `key=value`.
## Examples
Send a file to a Telegram chat through `cap_cli`:
```text
cap call tg_send_file '{"chat_id":"123456789","path":"/spiffs/reports/status.json","caption":"Latest status report."}'
```
Send a file to a QQ group through `cap_cli`:
```text
cap call qq_send_file '{"chat_id":"group1234567890","path":"/spiffs/reports/status.json","caption":"Latest status report."}'
```
## Workflow
1. Confirm the target file exists with `list_dir` if needed.
2. Identify the target channel: Telegram, QQ, or WeChat.
3. Choose `tg_send_file` or `qq_send_file` based on the target channel.
4. For WeChat, stop and explain that this firmware variant does not expose `wechat_send_file`.
5. Execute the capability through `cap_cli` as `cap call <cap_name> '<json>'` with explicit `chat_id`, `path`, and optional `caption`.
6. Tell the user whether the send succeeded.
## Notes
- This skill only sends files that already exist on the device filesystem.
- Do not use this skill for `.jpg`, `.jpeg`, `.png`, `.gif`, or `.webp`; use the im_send_picture skill instead.
- If the user wants WeChat delivery and the asset is an image, use `wechat_send_image` through the picture skill instead.
@@ -0,0 +1,53 @@
# IM Picture Return
Use this skill when the user wants the device to send an existing local image back to a chat.
## When to use
- The user asks to send back a picture, image, photo, screenshot, camera frame, or visual result.
- The target conversation is already the active Telegram, QQ, or WeChat chat, or the user provides an explicit target `chat_id`.
## Available tools
- `list_dir`: inspect device storage and confirm the image path
- `cap_cli`: run `cap call <cap_name> '<json>'` to invoke the transport-specific send capability indirectly
## Path guidance
- Prefer real local paths that already exist on the device.
- Common roots in this demo are `/spiffs`, `/spiffs/qq`, `/spiffs/tg`, `/spiffs/wechat`, `/spiffs/captures`, or application-managed subdirectories.
- Use `list_dir` first if the exact path is unknown.
## Sending rules
- Use this skill only for image files such as `.jpg`, `.jpeg`, `.png`, `.gif`, or `.webp`.
- Use `cap_cli` with `cap call tg_send_image '<json>'`, `cap call qq_send_image '<json>'`, or `cap call wechat_send_image '<json>'`.
- The JSON payload should include an explicit `chat_id`, `path`, and optional `caption`.
- Use `tg_send_image` for Telegram chats, `qq_send_image` for QQ chats, and `wechat_send_image` for WeChat chats.
- Pass `caption` only when the user wants an accompanying message.
- The second argument of `cap call` must be one complete JSON string. Do not rewrite it as `--chat_id`, `path=...`, or other non-JSON forms.
## Examples
Send an image to a Telegram chat through `cap_cli`:
```text
cap call tg_send_image '{"chat_id":"123456","path":"/spiffs/captures/latest.jpg","caption":"Here is the image."}'
```
Send an image to a QQ chat through `cap_cli`:
```text
cap call qq_send_image '{"chat_id":"group123","path":"/spiffs/qq/capture.jpg","caption":"Here is the image."}'
```
Send an image to a WeChat chat through `cap_cli`:
```text
cap call wechat_send_image '{"chat_id":"room123","path":"/spiffs/wechat/capture.jpg","caption":"Here is the image."}'
```
## Workflow
1. Confirm the target image exists with `list_dir` if needed.
2. Identify the target channel: Telegram, QQ, or WeChat.
3. Build the matching capability call: `tg_send_image`, `qq_send_image`, or `wechat_send_image`.
4. Execute it through `cap_cli` as `cap call <cap_name> '<json>'` with explicit `chat_id`, `path`, and optional `caption`.
5. Tell the user whether the send succeeded.
## Notes
- This skill only sends images that already exist on the device filesystem.
- If the user wants to send `.txt`, `.json`, `.log`, `.csv`, or archives, use the im_send_file skill instead.
- In this firmware variant, WeChat supports text and image send, but generic non-image file send is not exposed through the current capability surface.
@@ -0,0 +1,46 @@
# Lua Script Execution
Use this skill when the user wants to see existing Lua scripts, run one, or inspect async execution jobs.
## Current Managed Scripts
At the moment the image includes:
- `hello.lua`
Current script content summary:
- `hello.lua`: prints `hello lua!`
## Listing Scripts
Use `lua_list_scripts` to inspect the current managed script set.
- Optional input: `prefix`
- Example: list everything with `{}` or filter a subdirectory with `{"prefix":"effects"}`
## Running a Script Synchronously
Use `lua_run_script` when the user wants immediate output.
- Required: `path`
- Optional: `args`, `timeout_ms`
- Prefer relative paths such as `hello.lua`
Example:
```json
{
"path": "hello.lua"
}
```
If the script expects structured inputs, pass them through `args`. The runtime exposes them to Lua as the global `args`.
## Running a Script Asynchronously
Use `lua_run_script_async` for long-running or continuous scripts.
- Required: `path`
- Optional: `args`, `timeout_ms`
- Returns a `job_id`
After starting an async script:
- Use `lua_list_async_jobs` to see all jobs or filter by status
- Use `lua_get_async_job` with the returned `job_id` to inspect one job
## Execution Notes
- Paths must resolve under `/spiffs/lua` and end with `.lua`.
- Prefer `lua_run_script` for short scripts that should finish and return text.
- Prefer `lua_run_script_async` for loops, animations, watchers, or long-running device behaviors.
- If the user asks to run a script that does not exist yet, switch to the Lua authoring flow first.
@@ -0,0 +1,64 @@
# Lua Script Authoring
Use this skill when the user wants to write, generate, or modify a managed Lua script for this device.
## Runtime Constraints
- Managed Lua scripts must stay under `/spiffs/lua`.
- The script path must end with `.lua`.
- Prefer relative paths such as `blink.lua` or `rainbow.lua`; the runtime resolves them under `/spiffs/lua`.
- Use `lua_write_script` to save or overwrite script content.
## Available Lua Modules
The runtime includes these built-in and application-registered modules:
### `delay`
- `delay.delay_ms(ms)`
- Use for short blocking delays inside a script.
### `storage`
- `storage.mkdir(path)`
- `storage.write_file(path, content)`
- `storage.read_file(path)`
- Use only for files the script needs to manage.
### `gpio`
- `gpio.set_direction(pin, mode)`
- `gpio.set_level(pin, level)`
- `gpio.get_level(pin)`
- Supported modes: `input`, `output`, `input_output`, `output_od`, `input_output_od`, `disable`
### `led_strip`
- `local strip = led_strip.new(gpio_pin, max_leds)`
- `strip:set_pixel(index, r, g, b)`
- `strip:refresh()`
- `strip:clear()`
- `strip:close()`
- This is for WS2812-style LED strips on a GPIO pin.
## Writing Guidance
- Write plain Lua script files, not markdown or pseudocode.
- Keep dependencies limited to standard Lua plus the modules listed above.
- Prefer small scripts with a clear entry flow and explicit comments for pin usage.
- If the script touches GPIO or LED hardware, state the pin numbers and expected electrical behavior in comments.
- If a requested peripheral is not covered by `gpio` or `led_strip`, say that the current runtime does not expose that peripheral module.
## Example Shape
```lua
local gpio = require("gpio")
local delay = require("delay")
gpio.set_direction(2, "output")
while true do
gpio.set_level(2, 1)
delay.delay_ms(500)
gpio.set_level(2, 0)
delay.delay_ms(500)
end
```
## Save Rule
When the script is ready, call `lua_write_script` with:
- `path`: relative `.lua` path under `/spiffs/lua`
- `content`: full Lua source
- `overwrite`: `true` only when replacing an existing script intentionally
@@ -0,0 +1,48 @@
# QQ File Return
Use this skill when the user wants the device to send a local file or image back to a QQ chat.
## When to use
- The user asks to send back a file, attachment, image, photo, log, or generated output through QQ.
- The target conversation is already the active QQ chat, or the user provides an explicit QQ `chat_id`.
## Available tools
- `list_dir`: inspect device storage and confirm the file path
- `read_file`: inspect small text files before sending when needed
- `cap_cli`: run `cap call qq_send_image '<json>'` or `cap call qq_send_file '<json>'`
## Path guidance
- Prefer real local paths already stored on the device.
- Common roots in this demo are `/spiffs`, `/spiffs/qq`, `/spiffs/lua`, or other application-managed storage paths.
- Use `list_dir` first if the exact file path is unknown.
- Use `read_file` only for small text inspection, not for binary payloads.
## Sending rules
- Use `qq_send_image` for image files such as `.jpg`, `.jpeg`, `.png`, `.gif`, or `.webp`.
- Use `qq_send_file` for non-image files such as `.txt`, `.json`, `.log`, `.csv`, or archives.
- Execute the chosen QQ capability through `cap_cli` as `cap call <cap_name> '<json>'`.
- Pass `caption` only when the user wants an accompanying message.
- The JSON payload should include an explicit QQ `chat_id`, `path`, and optional `caption`.
- The second argument of `cap call` must be one complete JSON string. Do not rewrite it as `--chat_id` flags or `key=value`.
## Examples
Send an image to a QQ chat through `cap_cli`:
```text
cap call qq_send_image '{"chat_id":"group123","path":"/spiffs/qq/capture.jpg","caption":"Here is the image."}'
```
Send a file to a QQ group through `cap_cli`:
```text
cap call qq_send_file '{"chat_id":"group1234567890","path":"/spiffs/reports/status.json","caption":"Latest status report."}'
```
## Workflow
1. Confirm the target file exists with `list_dir` if needed.
2. Choose `qq_send_image` or `qq_send_file` based on file type.
3. Execute the QQ capability through `cap_cli` with `cap call <cap_name> '<json>'`.
4. Tell the user whether the send succeeded.
## Notes
- This skill only sends files that already exist on the device filesystem.
- QQ generic file delivery may depend on platform-side enablement. If `qq_send_file` fails, prefer falling back to `qq_send_image` for images or explain that QQ rejected generic file upload.
@@ -0,0 +1,34 @@
{
"skills": [
{
"id": "weather",
"file": "weather.md",
"title": "Weather",
"summary": "Get current weather and forecast information with web search."
},
{
"id": "lua_write",
"file": "lua_write.md",
"title": "Lua Script Authoring",
"summary": "Write managed Lua scripts with the correct runtime modules, path rules, and syntax constraints."
},
{
"id": "lua_run",
"file": "lua_run.md",
"title": "Lua Script Execution",
"summary": "List and run managed Lua scripts, including async execution and job inspection."
},
{
"id": "im_send_file",
"file": "im_send_file.md",
"title": "IM Send File",
"summary": "Send an existing local non-image file to a Telegram or QQ chat."
},
{
"id": "im_send_picture",
"file": "im_send_picture.md",
"title": "IM Send Picture",
"summary": "Send an existing local image to a Telegram or QQ chat."
}
]
}
@@ -0,0 +1,18 @@
# Weather 天气
Get current weather and forecasts through `cap_cli`. Support weather, forecast, temperature, 天气, 预报, 温度 queries.
## When to use
When the user asks about weather, temperature, forecasts, 天气, 温度, or 预报.
## How to use
1. Use `cap_cli` to run `time --now` and learn the current date
2. Use `cap_cli` to run `web_search --query "weather in [city] today"`
3. Extract temperature, conditions, and forecast from results
4. Present in a concise, friendly format
## Example
User: "What's the weather in Tokyo?"
-> cap_cli: `time --now`
-> cap_cli: `web_search --query "weather Tokyo today February 2026"`
-> "Tokyo: 8C, partly cloudy. High 12C, low 4C. Light wind from the north."
@@ -0,0 +1,46 @@
idf_component_register(
SRCS
"app_clawgent.c"
"main.c"
"basic_demo_cli.c"
"basic_demo_lua_modules.c"
"basic_demo_settings.c"
"basic_demo_wifi.c"
"config_http_server.c"
"lua_module/lua_module_gpio.c"
"lua_module/lua_module_led_strip.c"
INCLUDE_DIRS
"."
REQUIRES
cap_cli
cap_files
cap_im_qq
cap_im_tg
cap_im_wechat
cap_llm_inspect
cap_lua
cap_mcp_client
cap_mcp_server
cap_skill
cap_time
cap_web_search
claw_event_router
claw_cap
claw_core
claw_memory
claw_skill
console
driver
esp_event
esp_http_server
esp_netif
esp_wifi
fatfs
json
nvs_flash
wear_levelling
EMBED_TXTFILES
"web/index.html"
"web/styles.css"
"web/app.js"
)
@@ -0,0 +1,117 @@
menu "Basic Demo Config"
config BASIC_DEMO_WIFI_SSID
string "Default Wi-Fi SSID"
default ""
help
Default Wi-Fi SSID used to populate basic demo settings.
config BASIC_DEMO_WIFI_PASSWORD
string "Default Wi-Fi password"
default ""
help
Default Wi-Fi password used to populate basic demo settings.
config BASIC_DEMO_LLM_API_KEY
string "Default LLM API key"
default ""
help
Default API key used to populate the basic demo LLM settings.
config BASIC_DEMO_LLM_BACKEND_TYPE
string "Default LLM backend type"
default "openai_compatible"
help
Default backend type for claw_core, for example openai_compatible or custom.
config BASIC_DEMO_LLM_PROFILE
string "Default LLM profile"
default "qwen_compatible"
help
Default runtime profile for claw_core, for example openai, qwen_compatible, or custom_openai_compatible.
config BASIC_DEMO_LLM_MODEL
string "Default LLM model"
default "qwen3-max-2026-01-23"
help
Default model name for claw_core requests.
config BASIC_DEMO_LLM_BASE_URL
string "Default LLM base URL"
default ""
help
Optional override for the selected LLM profile base URL.
config BASIC_DEMO_LLM_AUTH_TYPE
string "Default LLM auth type"
default "bearer"
help
Auth mode for the LLM backend, for example bearer, api-key, or none.
config BASIC_DEMO_LLM_TIMEOUT_MS
string "Default LLM timeout ms"
default "120000"
help
Timeout in milliseconds for outbound LLM HTTP requests.
config BASIC_DEMO_QQ_APP_ID
string "Default QQ app ID"
default ""
help
Default QQ app ID used by the IM cap.
config BASIC_DEMO_QQ_APP_SECRET
string "Default QQ app secret"
default ""
help
Default QQ app secret used by the IM cap.
config BASIC_DEMO_TG_BOT_TOKEN
string "Default Telegram bot token"
default ""
help
Default Telegram bot token used by the IM cap.
config BASIC_DEMO_WECHAT_TOKEN
string "Default WeChat token"
default ""
help
Default WeChat bot token used by the IM cap.
config BASIC_DEMO_WECHAT_BASE_URL
string "Default WeChat base URL"
default "https://ilinkai.weixin.qq.com"
help
Default WeChat API base URL used by the IM cap.
config BASIC_DEMO_WECHAT_CDN_BASE_URL
string "Default WeChat CDN base URL"
default "https://novac2c.cdn.weixin.qq.com/c2c"
help
Default WeChat CDN base URL used for media upload and download.
config BASIC_DEMO_WECHAT_ACCOUNT_ID
string "Default WeChat account ID"
default "default"
help
Default logical account ID for local WeChat channel state.
config BASIC_DEMO_SEARCH_BRAVE_KEY
string "Default Brave Search API key"
default ""
help
Default Brave Search API key for the web search cap.
config BASIC_DEMO_SEARCH_TAVILY_KEY
string "Default Tavily API key"
default ""
help
Default Tavily API key for the web search cap.
config BASIC_DEMO_TIME_TIMEZONE
string "Default time cap timezone"
default "UTC0"
help
POSIX-style timezone string used by the time cap.
endmenu

Some files were not shown because too many files have changed in this diff Show More