feat: add skill verification CLI commands and preflash SPIFFS image

This commit is contained in:
Bo
2026-02-17 01:26:18 +08:00
parent e196c88c49
commit 4ce0735c9a
3 changed files with 203 additions and 4 deletions

View File

@@ -4,3 +4,6 @@ cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(mimiclaw)
# Pre-flash a valid SPIFFS image so first boot does not need runtime formatting.
spiffs_create_partition_image(spiffs spiffs_data FLASH_IN_PROJECT)

View File

@@ -7,12 +7,12 @@
#include "memory/session_mgr.h"
#include "proxy/http_proxy.h"
#include "tools/tool_web_search.h"
#include "tools/tool_registry.h"
#include "cron/cron_service.h"
#include "heartbeat/heartbeat.h"
#include "skills/skill_loader.h"
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <dirent.h>
#include "esp_log.h"
#include "esp_console.h"
#include "esp_system.h"
@@ -253,6 +253,173 @@ static int cmd_wifi_scan(int argc, char **argv)
return 0;
}
/* --- skill_list command --- */
static int cmd_skill_list(int argc, char **argv)
{
(void)argc;
(void)argv;
char *buf = malloc(4096);
if (!buf) {
printf("Out of memory.\n");
return 1;
}
size_t n = skill_loader_build_summary(buf, 4096);
if (n == 0) {
printf("No skills found under /spiffs/skills/.\n");
} else {
printf("=== Skills ===\n%s", buf);
}
free(buf);
return 0;
}
/* --- skill_show command --- */
static struct {
struct arg_str *name;
struct arg_end *end;
} skill_show_args;
static bool has_md_suffix(const char *name)
{
size_t len = strlen(name);
return (len >= 3) && strcmp(name + len - 3, ".md") == 0;
}
static bool build_skill_path(const char *name, char *out, size_t out_size)
{
if (!name || !name[0]) return false;
if (strstr(name, "..") != NULL) return false;
if (strchr(name, '/') != NULL || strchr(name, '\\') != NULL) return false;
if (has_md_suffix(name)) {
snprintf(out, out_size, "/spiffs/skills/%s", name);
} else {
snprintf(out, out_size, "/spiffs/skills/%s.md", name);
}
return true;
}
static int cmd_skill_show(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&skill_show_args);
if (nerrors != 0) {
arg_print_errors(stderr, skill_show_args.end, argv[0]);
return 1;
}
char path[128];
if (!build_skill_path(skill_show_args.name->sval[0], path, sizeof(path))) {
printf("Invalid skill name.\n");
return 1;
}
FILE *f = fopen(path, "r");
if (!f) {
printf("Skill not found: %s\n", path);
return 1;
}
printf("=== %s ===\n", path);
char line[256];
while (fgets(line, sizeof(line), f)) {
fputs(line, stdout);
}
fclose(f);
printf("\n============\n");
return 0;
}
/* --- skill_search command --- */
static struct {
struct arg_str *keyword;
struct arg_end *end;
} skill_search_args;
static bool contains_nocase(const char *text, const char *keyword)
{
if (!text || !keyword || !keyword[0]) return false;
size_t key_len = strlen(keyword);
for (const char *p = text; *p; p++) {
size_t i = 0;
while (i < key_len && p[i] &&
tolower((unsigned char)p[i]) == tolower((unsigned char)keyword[i])) {
i++;
}
if (i == key_len) return true;
}
return false;
}
static int cmd_skill_search(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&skill_search_args);
if (nerrors != 0) {
arg_print_errors(stderr, skill_search_args.end, argv[0]);
return 1;
}
const char *keyword = skill_search_args.keyword->sval[0];
DIR *dir = opendir("/spiffs");
if (!dir) {
printf("Cannot open /spiffs.\n");
return 1;
}
const char *prefix = "skills/";
const size_t prefix_len = strlen(prefix);
int matches = 0;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
const char *name = ent->d_name;
size_t name_len = strlen(name);
if (strncmp(name, prefix, prefix_len) != 0) continue;
if (name_len < prefix_len + 4) continue;
if (strcmp(name + name_len - 3, ".md") != 0) continue;
char full_path[296];
snprintf(full_path, sizeof(full_path), "/spiffs/%s", name);
bool file_matched = contains_nocase(name, keyword);
int matched_line = 0;
FILE *f = fopen(full_path, "r");
if (!f) continue;
char line[256];
int line_no = 0;
while (!file_matched && fgets(line, sizeof(line), f)) {
line_no++;
if (contains_nocase(line, keyword)) {
file_matched = true;
matched_line = line_no;
}
}
fclose(f);
if (file_matched) {
matches++;
if (matched_line > 0) {
printf("- %s (matched at line %d)\n", full_path, matched_line);
} else {
printf("- %s (matched in filename)\n", full_path);
}
}
}
closedir(dir);
if (matches == 0) {
printf("No skills matched keyword: %s\n", keyword);
} else {
printf("Total matches: %d\n", matches);
}
return 0;
}
/* --- config_show command --- */
static void print_config(const char *label, const char *ns, const char *key,
const char *build_val, bool mask)
@@ -463,6 +630,36 @@ esp_err_t serial_cli_init(void)
};
esp_console_cmd_register(&provider_cmd);
/* skill_list */
esp_console_cmd_t skill_list_cmd = {
.command = "skill_list",
.help = "List installed skills from /spiffs/skills/",
.func = &cmd_skill_list,
};
esp_console_cmd_register(&skill_list_cmd);
/* skill_show */
skill_show_args.name = arg_str1(NULL, NULL, "<name>", "Skill name (e.g. weather or weather.md)");
skill_show_args.end = arg_end(1);
esp_console_cmd_t skill_show_cmd = {
.command = "skill_show",
.help = "Print full content of one skill file",
.func = &cmd_skill_show,
.argtable = &skill_show_args,
};
esp_console_cmd_register(&skill_show_cmd);
/* skill_search */
skill_search_args.keyword = arg_str1(NULL, NULL, "<keyword>", "Keyword to search in skills");
skill_search_args.end = arg_end(1);
esp_console_cmd_t skill_search_cmd = {
.command = "skill_search",
.help = "Search skill files by keyword (filename + content)",
.func = &cmd_skill_search,
.argtable = &skill_search_args,
};
esp_console_cmd_register(&skill_search_cmd);
/* memory_read */
esp_console_cmd_t mem_read_cmd = {
.command = "memory_read",

View File

@@ -9,7 +9,6 @@
#include <sys/stat.h>
#include "esp_log.h"
#include "cJSON.h"
#include <stdbool.h>
static const char *TAG = "tool_files";