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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<p><img src="https://raw.githubusercontent.com/RMinte-AI/.github/06b4b2a/profile/assets/banner.png" alt="RMinte AI — AI that works. Right where you are. RM-01 product photograph." width="1600"></p>
![TianshanOS](assets/tianshanos-banner-en.png)

[English](README_EN.md) | [中文](README.md)

Expand Down
2 changes: 1 addition & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
![TianshanOS](assets/tsintro.png)
![TianshanOS](assets/tianshanos-banner-en.png)

[English](README_EN.md) | [中文](README.md)

Expand Down
Binary file added assets/tianshanos-banner-en.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion components/ts_api/src/ts_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

#include "ts_api.h"
#include "ts_config_pack.h"
#include "ts_core.h" /* TS_STRDUP_PSRAM, TS_CALLOC_PSRAM */
#include "ts_log.h"
#include "freertos/FreeRTOS.h"
Expand Down Expand Up @@ -316,7 +317,8 @@ esp_err_t ts_api_call_str(const char *name, const char *params_json, ts_api_resu
cJSON *params = NULL;

if (params_json && strlen(params_json) > 0) {
params = cJSON_Parse(params_json);
params = name&&!strcmp(name,"automation.rules.import") ?
ts_config_pack_parse_import_request(params_json,strlen(params_json)) : cJSON_Parse(params_json);
if (params == NULL) {
if (result) {
ts_api_result_error(result, TS_API_ERR_INVALID_ARG, "Invalid JSON");
Expand Down
186 changes: 92 additions & 94 deletions components/ts_api/src/ts_api_automation.c
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ static esp_err_t api_automation_resume(const cJSON *params, ts_api_result_t *res
*/
static esp_err_t api_automation_reload(const cJSON *params, ts_api_result_t *result)
{
if (ts_rule_restart_pending()) {
ts_api_result_error(result, TS_API_ERR_BUSY, "restart_pending");
return ESP_OK;
}
esp_err_t ret = ts_automation_reload();

if (ret == ESP_OK) {
Expand Down Expand Up @@ -478,11 +482,20 @@ static esp_err_t api_automation_variables_set(const cJSON *params, ts_api_result
*/
static esp_err_t api_automation_rules_list(const cJSON *params, ts_api_result_t *result)
{
result->data = cJSON_CreateObject();
bool loaded, recovery;
ts_rule_config_status(&loaded,&recovery);
bool view_gate=loaded||recovery;
if(view_gate&&!ts_rule_edit_begin()){
ts_api_result_error(result,TS_API_ERR_INTERNAL,"config_loading");return ESP_OK;
}
/* One configuration admission boundary keeps row payloads and saved
* metadata in the same view while an import or ordinary edit is committing. */
(void)ts_rule_refresh_saved();
result->data = cJSON_CreateObject();
ts_rule_config_status(&loaded, &recovery);
cJSON_AddBoolToObject(result->data, "loaded", loaded);
cJSON_AddBoolToObject(result->data, "recovery_required", recovery);
cJSON_AddStringToObject(result->data,"load_error",ts_rule_load_error());
cJSON *rules_array = cJSON_AddArrayToObject(result->data, "rules");

// 遍历所有规则
Expand All @@ -507,13 +520,21 @@ static esp_err_t api_automation_rules_list(const cJSON *params, ts_api_result_t
cJSON_AddNumberToObject(rule_obj, "cooldown_ms", rule.cooldown_ms);
cJSON_AddNumberToObject(rule_obj, "conditions_count", rule.conditions.count);
cJSON_AddNumberToObject(rule_obj, "actions_count", rule.action_count);
ts_rule_saved_status(rule.id, rule_obj);

cJSON_AddItemToArray(rules_array, rule_obj);
ts_rule_release(&rule);
}

cJSON_AddNumberToObject(result->data, "count", count);
cJSON *pending=ts_rule_pending_list();
if(!pending){if(view_gate)ts_rule_edit_end();ts_api_result_error(result,TS_API_ERR_INTERNAL,"recovery_required");return ESP_OK;}
while(cJSON_GetArraySize(pending))
cJSON_AddItemToArray(rules_array,cJSON_DetachItemFromArray(pending,0));
cJSON_Delete(pending);
cJSON_AddNumberToObject(result->data, "count", cJSON_GetArraySize(rules_array));
cJSON_AddBoolToObject(result->data, "restart_required", ts_rule_restart_pending());
result->code = TS_API_OK;
if(view_gate)ts_rule_edit_end();
return ESP_OK;
}

Expand Down Expand Up @@ -542,6 +563,8 @@ static void rule_commit_reply(ts_api_result_t *result, esp_err_t ret,
cJSON_AddNumberToObject(result->data, "revision", commit->revision);
cJSON_AddStringToObject(result->data, "error_code",
commit->error_code ? commit->error_code : "invalid_configuration");
if (commit->missing_template_id[0])
cJSON_AddStringToObject(result->data, "missing_template_id", commit->missing_template_id);
}
static esp_err_t rule_mutate_locked(const cJSON *params, ts_api_result_t *result, int operation) {
const cJSON *id = cJSON_GetObjectItemCaseSensitive(params, "id"),
Expand Down Expand Up @@ -695,7 +718,11 @@ static esp_err_t api_automation_rules_trigger(const cJSON *params, ts_api_result
result->message = strdup("Rule not found");
} else {
result->code = TS_API_ERR_INTERNAL;
result->message = strdup("Failed to trigger rule");
result->data=cJSON_CreateObject();
if(result->data)ts_rule_saved_status(id_param->valuestring,result->data);
const cJSON *change=cJSON_GetObjectItemCaseSensitive(result->data,"pending_change");
result->message = strdup(cJSON_IsString(change)&&!strcmp(change->valuestring,"delete")?
"restart_pending":"Failed to trigger rule");
}

return ESP_OK;
Expand All @@ -710,15 +737,21 @@ static esp_err_t api_automation_rules_trigger(const cJSON *params, ts_api_result
*/
static esp_err_t api_automation_rules_get(const cJSON *params, ts_api_result_t *result) {
const cJSON *id = cJSON_GetObjectItemCaseSensitive(params, "id");
if(!ts_rule_edit_begin()){
ts_api_result_error(result,TS_API_ERR_INTERNAL,"configuration_busy");return ESP_OK;
}
ts_auto_rule_t rule;
if (!cJSON_IsString(id) || ts_rule_acquire(id->valuestring, &rule) != ESP_OK) {
ts_rule_edit_end();
ts_api_result_error(result, TS_API_ERR_NOT_FOUND, "Rule not found");
return ESP_OK;
}
ts_rule_resolve_presentation(&rule);
result->data = ts_rule_encode(&rule);
if(result->data)ts_rule_saved_status(rule.id,result->data);
ts_rule_release(&rule);
result->code = result->data ? TS_API_OK : TS_API_ERR_INTERNAL;
ts_rule_edit_end();
return ESP_OK;
}

Expand Down Expand Up @@ -2335,7 +2368,8 @@ static esp_err_t api_automation_actions_delete(const cJSON *params, ts_api_resul
}

const char *action_id = id->valuestring;
esp_err_t ret = ts_action_template_remove(action_id);
ts_action_template_delete_result_t detail = {0};
esp_err_t ret = ts_action_template_remove_checked(action_id, &detail);

if (ret == ESP_OK) {
// 同时删除 SD 卡上的配置文件(.json 和 .tscfg)
Expand All @@ -2351,17 +2385,38 @@ static esp_err_t api_automation_actions_delete(const cJSON *params, ts_api_resul

result->code = TS_API_OK;
result->message = strdup("Action template deleted");
} else if (detail.reason) {
result->code = TS_API_ERR_BUSY;
result->message = strdup(detail.reason);
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "action_id", action_id);
if (detail.references_confirmed) {
cJSON_AddBoolToObject(data, "references_confirmed", true);
cJSON *rules = cJSON_CreateArray();
bool complete = rules && detail.rules;
for (size_t i = 0; complete && i < detail.rule_count; ++i) {
cJSON *rule = cJSON_CreateObject();
complete = rule && cJSON_AddStringToObject(rule, "id", detail.rules[i].id) &&
cJSON_AddStringToObject(rule, "name", detail.rules[i].name);
if (complete) complete = cJSON_AddItemToArray(rules, rule);
if (!complete) cJSON_Delete(rule);
}
if (complete) {
complete = cJSON_AddItemToObject(data, "rules", rules);
if (!complete) cJSON_Delete(rules);
} else cJSON_Delete(rules);
cJSON_AddBoolToObject(data, "details_available", complete);
}
if (detail.check_reason) cJSON_AddStringToObject(data, "check_reason", detail.check_reason);
result->data = data;
} else if (ret == ESP_ERR_NOT_FOUND) {
result->code = TS_API_ERR_NOT_FOUND;
result->message = strdup("Template not found");
} else if (ret == ESP_ERR_INVALID_STATE) {
result->code = TS_API_ERR_BUSY;
result->message = strdup("service_delete_protected");
} else {
result->code = TS_API_ERR_INTERNAL;
result->message = strdup("Failed to delete template");
}

free(detail.rules);
return ESP_OK;
}

Expand Down Expand Up @@ -3996,93 +4051,36 @@ static esp_err_t api_automation_rules_export(const cJSON *params, ts_api_result_
*/
static esp_err_t api_automation_rules_import(const cJSON *params, ts_api_result_t *result)
{
if (!params) {
ts_api_result_error(result, TS_API_ERR_INVALID_ARG, "Missing parameters");
return ESP_OK;
}

const cJSON *tscfg = cJSON_GetObjectItem(params, "tscfg");
const cJSON *filename = cJSON_GetObjectItem(params, "filename");
if (!tscfg || !cJSON_IsString(tscfg) || !tscfg->valuestring[0]) {
ts_api_result_error(result, TS_API_ERR_INVALID_ARG, "Missing 'tscfg' parameter");
return ESP_OK;
}

const cJSON *overwrite = cJSON_GetObjectItem(params, "overwrite");
const cJSON *preview = cJSON_GetObjectItem(params, "preview");
bool do_overwrite = overwrite && cJSON_IsTrue(overwrite);
bool do_preview = preview && cJSON_IsTrue(preview);

// Lightweight verification
ts_config_pack_sig_info_t sig_info = {0};
ts_config_pack_result_t pack_result = ts_config_pack_verify_mem(
tscfg->valuestring, strlen(tscfg->valuestring), &sig_info);

if (pack_result != TS_CONFIG_PACK_OK) {
const char *err_msg = "Failed to verify config pack";
if (pack_result == TS_CONFIG_PACK_ERR_RECIPIENT) {
err_msg = "This config pack is not for this device";
} else if (pack_result == TS_CONFIG_PACK_ERR_SIGNATURE) {
err_msg = "Invalid signature";
}
ts_api_result_error(result, TS_API_ERR_INVALID_ARG, err_msg);
return ESP_OK;
}

// Extract config ID from filename
char config_id[64] = {0};
if (filename && cJSON_IsString(filename) && filename->valuestring[0]) {
const char *name = filename->valuestring;
if (strncmp(name, "rule_", 5) == 0) {
name += 5;
}
const char *dot = strrchr(name, '.');
if (dot && strcmp(dot, ".tscfg") == 0) {
size_t len = dot - name;
if (len > sizeof(config_id) - 1) len = sizeof(config_id) - 1;
strncpy(config_id, name, len);
} else {
strncpy(config_id, name, sizeof(config_id) - 1);
}
} else {
snprintf(config_id, sizeof(config_id), "rule_%lld", (long long)time(NULL));
}

// Check if exists
char filepath[128];
snprintf(filepath, sizeof(filepath), "%s/%s.tscfg", RULES_SDCARD_DIR, config_id);
struct stat st;
bool exists = (stat(filepath, &st) == 0);
if (!exists) {
snprintf(filepath, sizeof(filepath), "%s/%s.json", RULES_SDCARD_DIR, config_id);
exists = (stat(filepath, &st) == 0);
}

// Preview mode
if (do_preview) {
cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "valid", true);
cJSON_AddStringToObject(data, "type", "automation_rule");
cJSON_AddStringToObject(data, "id", config_id);
cJSON_AddBoolToObject(data, "exists", exists);
cJSON_AddStringToObject(data, "signer", sig_info.signer_cn[0] ? sig_info.signer_cn : "unknown");
cJSON_AddBoolToObject(data, "official", sig_info.is_official);
cJSON_AddStringToObject(data, "note", "Content will be decrypted on system restart");
ts_api_result_ok(result, data);
return ESP_OK;
}

if (exists && !do_overwrite) {
cJSON *data = cJSON_CreateObject();
cJSON_AddStringToObject(data, "id", config_id);
cJSON_AddBoolToObject(data, "exists", true);
cJSON_AddStringToObject(data, "message", "Rule config already exists, set overwrite=true to replace");
ts_api_result_ok(result, data);
return ESP_OK;
const cJSON *bytes=cJSON_GetObjectItemCaseSensitive(params,"tscfg");
bool preview=cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(params,"preview"));
const cJSON *revision=cJSON_GetObjectItemCaseSensitive(params,"expected_revision"),
*generation=cJSON_GetObjectItemCaseSensitive(params,"expected_generation"),
*credential=cJSON_GetObjectItemCaseSensitive(params,"credential_generation"),
*digest=cJSON_GetObjectItemCaseSensitive(params,"package_digest");
if(!cJSON_IsString(bytes)||!bytes->valuestring[0]) {
ts_api_result_error(result,TS_API_ERR_INVALID_ARG,"invalid_pack");return ESP_OK;
}
if(!preview&&(!cJSON_IsNumber(revision)||!cJSON_IsNumber(generation)||!cJSON_IsNumber(credential)||
!cJSON_IsString(digest)||!isfinite(revision->valuedouble)||!isfinite(generation->valuedouble)||
!isfinite(credential->valuedouble)||credential->valuedouble<0||credential->valuedouble>UINT32_MAX||
credential->valuedouble!=(uint32_t)credential->valuedouble||
revision->valuedouble<0||revision->valuedouble>UINT32_MAX||
generation->valuedouble<0||generation->valuedouble>UINT32_MAX||
revision->valuedouble!=(uint32_t)revision->valuedouble||
generation->valuedouble!=(uint32_t)generation->valuedouble)) {
ts_api_result_error(result,TS_API_ERR_INVALID_ARG,"preview_required");return ESP_OK;
}
cJSON *data=NULL;const char *reason=NULL;
esp_err_t ret=ts_rule_import_pack(bytes->valuestring,strlen(bytes->valuestring),preview,
cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(params,"overwrite")),
preview?0:(uint32_t)revision->valuedouble,preview?0:(uint32_t)generation->valuedouble,
preview?0:(uint32_t)credential->valuedouble,preview?NULL:digest->valuestring,&data,&reason);
if(ret==ESP_OK)ts_api_result_ok(result,data);
else {
ts_api_result_error(result,ret==ESP_ERR_NO_MEM?TS_API_ERR_NO_MEM:TS_API_ERR_INVALID_ARG,
reason?reason:"invalid_pack");
result->data=data;
}

ts_api_result_error(result, TS_API_ERR_INVALID_ARG,
"source_read_only: encrypted rule import requires a recoverable writer");
return ESP_OK;
}

Expand Down
2 changes: 2 additions & 0 deletions components/ts_automation/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ idf_component_register(
"src/ts_rule_engine.c"
"src/ts_rule_codec.c"
"src/ts_rule_store.c"
"src/ts_rule_store_set.c"
"src/ts_rule_pack.c"
"src/ts_action_manager.c"
"src/ts_action_filter.c"
"src/ts_action_store.c"
Expand Down
15 changes: 15 additions & 0 deletions components/ts_automation/include/ts_action_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ esp_err_t ts_action_manager_resume(void);
* @return true if initialized
*/
bool ts_action_manager_is_initialized(void);
bool ts_action_templates_ready(void);
esp_err_t ts_action_templates_load_state(void);

/*===========================================================================*/
/* SSH Host Management */
Expand Down Expand Up @@ -190,6 +192,7 @@ esp_err_t ts_action_unregister_ssh_host(const char *host_id);
* @return ESP_OK if found
*/
esp_err_t ts_action_get_ssh_host(const char *host_id, ts_action_ssh_host_t *host_out);
esp_err_t ts_action_get_ssh_host_ex(const char *host_id, ts_action_ssh_host_t *host_out, bool *internal);

/**
* @brief Get number of registered SSH hosts
Expand Down Expand Up @@ -465,6 +468,18 @@ esp_err_t ts_action_template_add(const ts_action_template_t *tpl);
* @param id Template ID to remove
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if not exists
*/
typedef struct {
char id[TS_AUTO_NAME_MAX_LEN];
char name[TS_AUTO_LABEL_MAX_LEN];
} ts_action_template_reference_t;
typedef struct {
const char *reason;
const char *check_reason;
bool references_confirmed;
size_t rule_count;
ts_action_template_reference_t *rules; /* Caller frees; NULL when details are unavailable. */
} ts_action_template_delete_result_t;
esp_err_t ts_action_template_remove_checked(const char *id, ts_action_template_delete_result_t *result);
esp_err_t ts_action_template_remove(const char *id);

/**
Expand Down
19 changes: 18 additions & 1 deletion components/ts_automation/include/ts_rule_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include "ts_automation_types.h"
#include "esp_err.h"
typedef struct cJSON cJSON;
#include <stdbool.h>
#include <stdint.h>

Expand Down Expand Up @@ -83,7 +84,12 @@ esp_err_t ts_rule_disable(const char *id);
* @param id Rule ID
* @return Rule pointer or NULL
*/
typedef struct {int applied,durable,mirror_synced;uint32_t revision;const char *error_code;} ts_rule_commit_result_t;
typedef struct {
int applied, durable, mirror_synced;
uint32_t revision;
const char *error_code;
char missing_template_id[TS_AUTO_NAME_MAX_LEN]; /* Request-local diagnostic, never persisted. */
} ts_rule_commit_result_t;
esp_err_t ts_rule_acquire(const char *id,ts_auto_rule_t *out);
void ts_rule_release(ts_auto_rule_t *rule);
esp_err_t ts_rule_commit(const ts_auto_rule_t *candidate,const char *id,uint32_t expected_revision,ts_rule_commit_result_t *result);
Expand Down Expand Up @@ -354,6 +360,17 @@ void ts_rule_resolve_presentation(ts_auto_rule_t *rule);
bool ts_rule_edit_begin(void);
void ts_rule_edit_end(void);
void ts_rule_config_status(bool *loaded, bool *recovery);
bool ts_rule_restart_pending(void);
esp_err_t ts_rule_refresh_saved(void);
const char *ts_rule_load_error(void);
void ts_rule_saved_status(const char *id, cJSON *object);
cJSON *ts_rule_pending_list(void);
esp_err_t ts_rule_import_pack(const char *bytes, size_t length, bool preview, bool overwrite,
uint32_t expected_revision, uint32_t expected_generation, uint32_t expected_credential,
const char *expected_digest, cJSON **data, const char **reason);
typedef enum { TS_RULE_DEP_TEMPLATE, TS_RULE_DEP_COMMAND, TS_RULE_DEP_HOST, TS_RULE_DEP_ACTION_HOST } ts_rule_dependency_t;
/* Configuration writers already hold ts_ssh_binding_lock; no network/execution side effects. */
esp_err_t ts_rule_dependency_change(ts_rule_dependency_t kind, const char *id, const void *next);

#ifdef __cplusplus
}
Expand Down
Loading
Loading