Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ dkms.conf

# debug information files
*.dwo
.sync_tmp/
104 changes: 100 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ set(APP_INC
${APP_PATH}/gateway
${APP_PATH}/skills
${APP_PATH}/agent
${APP_PATH}/ai_components/assets/include
)

########################################
Expand All @@ -43,11 +42,108 @@ target_sources(${EXAMPLE_LIB}
${APP_SRCS}
)

# Snapshot before subdirectories so we can identify what the app itself adds.
set(_APP_COMP_LIBS_BEFORE "${COMPONENT_LIBS}")
set(_APP_COMP_PUBINC_BEFORE "${COMPONENT_PUBINC}")

add_subdirectory(${APP_PATH}/IM ${CMAKE_BINARY_DIR}/IM)
add_subdirectory(${APP_PATH}/components/lua ${CMAKE_BINARY_DIR}/components/lua)

# Propagate new component libs/includes to EXAMPLE_LIB.
# ai_components uses the self-registering pattern (COMPONENT_LIBS/PUBINC +
# PARENT_SCOPE) that TuyaOpen's top-level deal_with_components/target_link_libraries
# already handles for TuyaOpen's own components but NOT for app-level subdirs
# (HEADER_DIR and COMPONENTS_ALL_LIB are computed before this app's include()).
set(_APP_NEW_LIBS "${COMPONENT_LIBS}")
list(REMOVE_ITEM _APP_NEW_LIBS ${_APP_COMP_LIBS_BEFORE})
set(_APP_NEW_PUBINC "${COMPONENT_PUBINC}")
list(REMOVE_ITEM _APP_NEW_PUBINC ${_APP_COMP_PUBINC_BEFORE})

# Now declare EXAMPLE_LIB's include paths -- APP_INC + the public includes of
# all app-component libs we just added (so app sources can include their headers).
target_include_directories(${EXAMPLE_LIB}
PRIVATE
${APP_INC}
${_APP_NEW_PUBINC}
)

add_subdirectory(${APP_PATH}/ai_components ${CMAKE_BINARY_DIR}/ai_components)
add_subdirectory(${APP_PATH}/IM ${CMAKE_BINARY_DIR}/IM)
add_subdirectory(${APP_PATH}/components/lua ${CMAKE_BINARY_DIR}/components/lua)
if(_APP_NEW_LIBS)
# Give each new lib the full header set: TuyaOpen SDK + sibling app-component headers.
set(_APP_FULL_HDIR ${HEADER_DIR} ${_APP_NEW_PUBINC})
deal_with_components("${_APP_NEW_LIBS}" "${_APP_FULL_HDIR}")
target_link_libraries(${EXAMPLE_LIB} ${_APP_NEW_LIBS})

# TuyaOpen's PLATFORM_NEED_LIBS = "${EXAMPLE_LIB} tuyaos".
# tuyaos is built from TuyaOpen's COMPONENT_LIBS snapshot taken BEFORE this
# app's CMakeLists ran, so app-local components (ai_components/IM/lua) are
# NOT packed into it. Fold their .o files into EXAMPLE_LIB (tuyaapp) so the
# final platform link can resolve them.
foreach(_lib ${_APP_NEW_LIBS})
target_sources(${EXAMPLE_LIB} PRIVATE $<TARGET_OBJECTS:${_lib}>)
endforeach()
endif()

########################################
# DuckyClaw ai_components overlay
########################################
# Surgically replace individual translation units inside the upstream
# TuyaOpen ai_components library targets (ai_mcp, ai_agent) with
# DuckyClaw-side patches kept under overlay/. The SDK submodule stays
# pristine; only this app's CMake graph diverges. TuyaOpen processes
# src/ai_components/ before this file is `include`d, so the upstream targets
# already exist when we reach here.
#
# Note: ai_main and ai_audio used to be overlayed too, but those diffs have
# been folded back into upstream / proven trivial — DuckyClaw now consumes
# them straight from TuyaOpen/src/ai_components/ai_{main,audio}/.
set(_DC_OVERLAY ${APP_PATH}/overlay/ai_components)

function(_dc_overlay_source target rel_path)
if(NOT TARGET ${target})
message(WARNING "[DC overlay] target '${target}' not defined; skip ${rel_path}")
return()
endif()
set(_overlay_file ${_DC_OVERLAY}/${rel_path})
if(NOT EXISTS ${_overlay_file})
message(FATAL_ERROR "[DC overlay] missing overlay file: ${_overlay_file}")
endif()
get_target_property(_srcs ${target} SOURCES)
get_filename_component(_basename ${rel_path} NAME)
set(_new_srcs)
set(_found_upstream FALSE)
foreach(_s ${_srcs})
get_filename_component(_n ${_s} NAME)
if(_n STREQUAL "${_basename}")
set(_found_upstream TRUE)
else()
list(APPEND _new_srcs ${_s})
endif()
endforeach()
list(APPEND _new_srcs ${_overlay_file})
set_target_properties(${target} PROPERTIES SOURCES "${_new_srcs}")
if(_found_upstream)
message(STATUS "[DC overlay] ${target} <- ${_overlay_file} (replaced upstream ${_basename})")
else()
message(WARNING "[DC overlay] ${target}: upstream ${_basename} not found in SOURCES; overlay appended anyway")
endif()
endfunction()

# Header overlays (BEFORE PUBLIC on the lib + BEFORE PRIVATE on EXAMPLE_LIB)
# The lib-level prepend covers the lib's own .c files and anything that does
# target_link_libraries(... <lib>). But EXAMPLE_LIB (tuyaapp) gets its
# include paths from the COMPONENT_PUBINC variable that was snapshotted in
# the lib's own CMakeLists BEFORE our overlay ran, so EXAMPLE_LIB also needs
# the overlay path explicitly — otherwise app sources that compile into
# EXAMPLE_LIB (agent_loop.c, ducky_claw_chat.c, app_im.c, …) keep seeing the
# upstream headers without DuckyClaw's added declarations.
foreach(_lib_hdir ai_mcp ai_agent)
if(TARGET ${_lib_hdir})
target_include_directories(${_lib_hdir} BEFORE PUBLIC ${_DC_OVERLAY}/${_lib_hdir}/include)
message(STATUS "[DC overlay] ${_lib_hdir} include path prepended: ${_DC_OVERLAY}/${_lib_hdir}/include")
endif()
target_include_directories(${EXAMPLE_LIB} BEFORE PRIVATE ${_DC_OVERLAY}/${_lib_hdir}/include)
message(STATUS "[DC overlay] ${EXAMPLE_LIB} include path prepended: ${_DC_OVERLAY}/${_lib_hdir}/include")
endforeach()

_dc_overlay_source(ai_mcp ai_mcp/src/ai_mcp_server.c)
_dc_overlay_source(ai_agent ai_agent/src/ai_agent.c)
29 changes: 29 additions & 0 deletions IM/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,33 @@ menu "IM Config"
default 8192
help
Stack size in bytes for the IM qqbot token refresh thread (default 8*1024).

config IM_WX_UPDATES_BUF_SIZE
int "Weixin get_updates cursor buffer (bytes)"
default 4096
help
Cursor/state buffer for weixin long-poll. Allocated from PSRAM at
weixin_bot_init. Lower on tight-SRAM boards.

config IM_FS_DEDUP_CACHE_SIZE
int "Feishu dedup ring cache entries (count)"
default 512
help
Number of 8-byte FNV hashes kept per dedup ring (one for messages,
one for events). 512 = 4 KiB per ring = 8 KiB total. Allocated
from PSRAM at feishu_bot_init. Lower on tight-SRAM boards.

config IM_TG_DEDUP_CACHE_SIZE
int "Telegram dedup ring cache entries (count)"
default 64
help
Number of 8-byte FNV hashes kept for telegram message dedup.
Allocated from PSRAM at telegram_bot_init.

config IM_QQ_DEDUP_CACHE_SIZE
int "QQ bot dedup ring cache entries (count)"
default 64
help
Number of 8-byte FNV hashes kept for QQ bot message dedup.
Allocated from PSRAM at qqbot init.
endmenu
3 changes: 3 additions & 0 deletions IM/channels/discord_bot.c
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,9 @@ OPERATE_RET discord_bot_start(void)
cfg.stackDepth = IM_DC_POLL_STACK;
cfg.priority = THREAD_PRIO_1;
cfg.thrdname = "im_dc_gw";
#if defined(ENABLE_EXT_RAM) && (ENABLE_EXT_RAM == 1)
cfg.psram_mode = 1;
#endif

OPERATE_RET rt = tal_thread_create_and_start(&s_gateway_thread, NULL, NULL, discord_gateway_task, NULL, &cfg);
if (rt != OPRT_OK) {
Expand Down
41 changes: 39 additions & 2 deletions IM/channels/feishu_bot.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ static THREAD_HANDLE s_ws_thread = NULL;
#define FS_WS_FRAME_MAX_HEADERS 32
#define FS_WS_FRAME_MAX_KEY 64
#define FS_WS_FRAME_MAX_VALUE 256
/* Kconfig override; was a hard-coded 512 (= 4 KiB × 2 rings = 8 KiB .bss). */
#if defined(IM_FS_DEDUP_CACHE_SIZE)
#define FS_DEDUP_CACHE_SIZE IM_FS_DEDUP_CACHE_SIZE
#else
#define FS_DEDUP_CACHE_SIZE 512
#endif
#define FS_MAX_FRAG_PARTS 8
#ifndef IM_FS_POLL_STACK
#define IM_FS_POLL_STACK (16 * 1024)
Expand Down Expand Up @@ -161,15 +166,20 @@ static void append_sender_id(char *sender_ids, size_t sender_ids_size, const cha
/* -------- dedup ring buffer -------- */

typedef struct {
uint64_t keys[FS_DEDUP_CACHE_SIZE];
size_t idx;
/* PSRAM-allocated at feishu_bot_init. Inline [FS_DEDUP_CACHE_SIZE] was
* ~4 KiB internal SRAM .bss per ring (× 2 rings = ~8 KiB). */
uint64_t *keys;
size_t idx;
} dedup_ring_t;

static dedup_ring_t s_seen_msg = {0};
static dedup_ring_t s_seen_event = {0};

static bool dedup_contains(const dedup_ring_t *ring, uint64_t key)
{
if (!ring->keys) {
return false;
}
for (size_t i = 0; i < FS_DEDUP_CACHE_SIZE; i++) {
if (ring->keys[i] == key) {
return true;
Expand All @@ -180,10 +190,26 @@ static bool dedup_contains(const dedup_ring_t *ring, uint64_t key)

static void dedup_insert(dedup_ring_t *ring, uint64_t key)
{
if (!ring->keys) {
return;
}
ring->keys[ring->idx] = key;
ring->idx = (ring->idx + 1) % FS_DEDUP_CACHE_SIZE;
}

static OPERATE_RET dedup_ring_ensure(dedup_ring_t *ring)
{
if (ring->keys) {
return OPRT_OK;
}
ring->keys = (uint64_t *)im_calloc(FS_DEDUP_CACHE_SIZE, sizeof(uint64_t));
if (!ring->keys) {
return OPRT_MALLOC_FAILED;
}
ring->idx = 0;
return OPRT_OK;
}

/* -------- TLS + HTTP -------- */

static OPERATE_RET ensure_fs_cert(const char *host)
Expand Down Expand Up @@ -2613,6 +2639,14 @@ static void feishu_ws_task(void *arg)

OPERATE_RET feishu_bot_init(void)
{
/* Alloc dedup rings in PSRAM (was ~8 KiB internal SRAM .bss). */
if (dedup_ring_ensure(&s_seen_msg) != OPRT_OK ||
dedup_ring_ensure(&s_seen_event) != OPRT_OK) {
IM_LOGE(TAG, "feishu: dedup ring alloc failed (need %u bytes ×2)",
(unsigned)(FS_DEDUP_CACHE_SIZE * sizeof(uint64_t)));
return OPRT_MALLOC_FAILED;
}

if (IM_SECRET_FS_APP_ID[0] != '\0') {
im_safe_copy(s_app_id, sizeof(s_app_id), IM_SECRET_FS_APP_ID);
}
Expand Down Expand Up @@ -2678,6 +2712,9 @@ OPERATE_RET feishu_bot_start(void)
cfg.stackDepth = IM_FS_POLL_STACK;
cfg.priority = THREAD_PRIO_1;
cfg.thrdname = "im_fs_ws";
#if defined(ENABLE_EXT_RAM) && (ENABLE_EXT_RAM == 1)
cfg.psram_mode = 1;
#endif

PR_INFO("Device Free heap %d", tal_system_get_free_heap_size());
rt = tal_thread_create_and_start(&s_ws_thread, NULL, NULL, feishu_ws_task, NULL, &cfg);
Expand Down
30 changes: 26 additions & 4 deletions IM/channels/qqbot_channel.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ static const char *TAG = "qqbot";
#define QQ_RECONNECT_MAX_MS IM_QQ_FAIL_MAX_MS
#define QQ_TOKEN_FAIL_BASE_MS IM_QQ_TOKEN_FAIL_BASE_MS
#define QQ_TOKEN_FAIL_MAX_MS IM_QQ_TOKEN_FAIL_MAX_MS
/* Kconfig override; default 64 entries = 512 B. */
#if defined(IM_QQ_DEDUP_CACHE_SIZE)
#define QQ_DEDUP_CACHE_SIZE IM_QQ_DEDUP_CACHE_SIZE
#else
#define QQ_DEDUP_CACHE_SIZE 64
#endif
#define QQ_PROXY_READ_SLICE_MS 1000
#define QQ_PROXY_READ_TOTAL_MS 15000

Expand All @@ -50,8 +55,9 @@ static MUTEX_HANDLE s_token_mutex = NULL;
static THREAD_HANDLE s_token_thread = NULL;
static THREAD_HANDLE s_ws_thread = NULL;

static uint64_t s_seen_msg_keys[QQ_DEDUP_CACHE_SIZE] = {0};
static size_t s_seen_msg_idx = 0;
/* PSRAM-allocated at qqbot_channel_init (was inline .bss). */
static uint64_t *s_seen_msg_keys = NULL;
static size_t s_seen_msg_idx = 0;

/* ================================================================
* Connection Struct
Expand Down Expand Up @@ -832,7 +838,7 @@ static OPERATE_RET qq_http_post(const char *host, const char *path, const char *

static bool seen_msg_contains(const char *msg_id)
{
if (!msg_id || msg_id[0] == '\0') {
if (!msg_id || msg_id[0] == '\0' || !s_seen_msg_keys) {
return false;
}
uint64_t key = im_fnv1a64(msg_id);
Expand All @@ -846,7 +852,7 @@ static bool seen_msg_contains(const char *msg_id)

static void seen_msg_insert(const char *msg_id)
{
if (!msg_id || msg_id[0] == '\0') {
if (!msg_id || msg_id[0] == '\0' || !s_seen_msg_keys) {
return;
}
s_seen_msg_keys[s_seen_msg_idx] = im_fnv1a64(msg_id);
Expand Down Expand Up @@ -1349,6 +1355,16 @@ OPERATE_RET qqbot_send_message(const char *chat_id, const char *text)

OPERATE_RET qqbot_channel_init(void)
{
/* PSRAM-allocate dedup ring (was inline .bss). */
if (!s_seen_msg_keys) {
s_seen_msg_keys = (uint64_t *)im_calloc(QQ_DEDUP_CACHE_SIZE, sizeof(uint64_t));
if (!s_seen_msg_keys) {
IM_LOGE(TAG, "qqbot: dedup ring alloc failed size=%u",
(unsigned)(QQ_DEDUP_CACHE_SIZE * sizeof(uint64_t)));
return OPRT_MALLOC_FAILED;
}
}

if (IM_SECRET_QQ_APP_ID[0] != '\0') {
im_safe_copy(s_app_id, sizeof(s_app_id), IM_SECRET_QQ_APP_ID);
}
Expand Down Expand Up @@ -1393,6 +1409,9 @@ OPERATE_RET qqbot_channel_start(void)
cfg.stackDepth = IM_QQ_TOKEN_STACK;
cfg.priority = IM_QQ_POLL_PRIO;
cfg.thrdname = "im_qq_token";
#if defined(ENABLE_EXT_RAM) && (ENABLE_EXT_RAM == 1)
cfg.psram_mode = 1;
#endif
OPERATE_RET rt = tal_thread_create_and_start(&s_token_thread, NULL, NULL,
qqbot_token_task, NULL, &cfg);
if (rt != OPRT_OK) {
Expand All @@ -1406,6 +1425,9 @@ OPERATE_RET qqbot_channel_start(void)
cfg.stackDepth = IM_QQ_POLL_STACK;
cfg.priority = IM_QQ_POLL_PRIO;
cfg.thrdname = "im_qq_ws";
#if defined(ENABLE_EXT_RAM) && (ENABLE_EXT_RAM == 1)
cfg.psram_mode = 1;
#endif
OPERATE_RET rt = tal_thread_create_and_start(&s_ws_thread, NULL, NULL,
qqbot_ws_task, NULL, &cfg);
if (rt != OPRT_OK) {
Expand Down
Loading
Loading