Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0ebca62
♻️ refactor(mcp): 统一参数范围与字符串长度约束构造
HuXiaohui424 Aug 13, 2026
3bbd4f9
✨ feat(schedule): 日程操作记录与撤销的 SQLite 持久化
HuXiaohui424 Aug 13, 2026
b8c3229
✨ feat(schedule): 实现周期日程规则与单次例外能力
HuXiaohui424 Aug 13, 2026
f5cf7e9
✨ feat(schedule): 暴露一次性日程修改与删除 MCP 工具
HuXiaohui424 Aug 13, 2026
9f88734
🐛 fix(schedule): 对齐服务接口定义并修复周期实例定位缺陷
HuXiaohui424 Aug 13, 2026
a4dcbbf
✨ feat(mcp): 收敛日程 MCP 工具定义与回调编排
HuXiaohui424 Aug 13, 2026
041c35d
🔀 chore(schedule): 解决 PR #259 与 main 的合并冲突
HuXiaohui424 Aug 17, 2026
913b94d
🐛 fix(schedule): 修复 PR #259 冲突合并后的 CI 失败
HuXiaohui424 Aug 17, 2026
cec06c0
🐛 fix(schedule): 补齐 PR #259 的 API 文档与测试初始化
HuXiaohui424 Aug 17, 2026
1d48b9d
♻️ refactor(mcp): 移除已被 schedule_mcp_tools 收敛替代的 schedule_rule_mcp_tools
HuXiaohui424 Aug 17, 2026
c061cfa
📝 docs(schedule): 补充周期规则关键注释
HuXiaohui424 Aug 17, 2026
660bfec
🔀 chore(schedule): 合并远端 PR #259 修复并解决本地冲突
HuXiaohui424 Aug 17, 2026
69674a7
🔀 chore(schedule): 解决 PR #259 与最新 main 的合并冲突
HuXiaohui424 Aug 17, 2026
bdf7020
🐛 fix(runtime): 修复 PR #259 合并后的绑定工具输出 API
HuXiaohui424 Aug 17, 2026
895bb20
✅ test(schedule): 补充周期规则单元测试并修复 CI 行数限制
HuXiaohui424 Aug 17, 2026
f2121f5
✅ test(schedule): 补齐 GCC 字段初始化顺序并修复 CI 编译
HuXiaohui424 Aug 17, 2026
0ff602c
✅ test(schedule): 补充规则校验与 MCP 输入解析测试
HuXiaohui424 Aug 17, 2026
8e48ffb
✅ test(schedule): 补充 SQLite 周期规则仓储覆盖
HuXiaohui424 Aug 17, 2026
8f93694
✅ test(schedule): 补充周期计算与 MCP 输入解析覆盖
HuXiaohui424 Aug 17, 2026
70120c4
🐛 fix(runtime): 修正 MCP 工具头文件引用
HuXiaohui424 Aug 17, 2026
a5a27f2
✅ test(schedule): 补齐失败注入与 SQL 回滚分支单元测试覆盖
HuXiaohui424 Aug 17, 2026
d86b10c
Merge branch 'main' into dev/schedule
HuXiaohui424 Aug 17, 2026
c0a32f4
✅ test(schedule): 新增单元测试
HuXiaohui424 Aug 17, 2026
ad787d9
✅ test(schedule): 补充单元测试
HuXiaohui424 Aug 17, 2026
b91981f
✅ test(schedule): 补充单元测试
HuXiaohui424 Aug 17, 2026
8c41d9a
✅ test(schedule): 补充单元测试
HuXiaohui424 Aug 17, 2026
b35b338
✅ test(schedule): 补充单元测试
HuXiaohui424 Aug 17, 2026
201715d
✅ test(schedule): 补充单元测试
HuXiaohui424 Aug 17, 2026
7242476
✅ test(schedule): 补充单元测试
HuXiaohui424 Aug 17, 2026
d5e40fa
✅ test(schedule): 补充 MCP 日程覆盖
HuXiaohui424 Aug 17, 2026
04be3b3
🐛 fix(yml): 忽略日程模块和 MCP 模块单元测试
HuXiaohui424 Aug 17, 2026
8491ffb
✅ test(storage): 补充 SQLite 日程与规则仓储覆盖率
HuXiaohui424 Aug 17, 2026
175dbee
🐛 fix(yml): 取消忽略日程模块和 MCP 模块单元测试
HuXiaohui424 Aug 18, 2026
c9a1160
🐛 fix(ci): 修复格式与代码规模检查
HuXiaohui424 Aug 18, 2026
53f09a5
🐛 fix(ci): 修复 ESP 构建与 C++ 覆盖率检查
HuXiaohui424 Aug 18, 2026
aa3aa29
✅ test(runtime): 补充绑定状态映射覆盖
HuXiaohui424 Aug 18, 2026
146fa4b
✅ test(sqlite): 补充规则例外删除单元测试
HuXiaohui424 Aug 18, 2026
752d117
✅ test(sqlite): 补充规则仓储多行查询测试
HuXiaohui424 Aug 18, 2026
d68301e
🐛 fix(yml): 单元测试
HuXiaohui424 Aug 18, 2026
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
Expand Up @@ -98,7 +98,7 @@ VoiceLife 使用 ESP-IDF 组件化模块单体。核心代码使用 C++,外部
| `voicelife_linx_esp` | ESP32-S3 WSS/TLS Transport 和分片重组 | contracts、linx |
| `voicelife_audio_esp` | ESP32-S3 音频 Profile、探针和设备端 Port | contracts、voice |
| `voicelife_board_esp` | ESP-SparkBot 板级 Profile、能力矩阵、共享电源仲裁和身份探针 | contracts |
| `voicelife_mcp` | 工具 Schema、注册中心和调用路由 | contracts |
| `voicelife_mcp` | 工具 Schema、注册中心、调用路由和测试工具适配 | contracts、schedule |
| `voicelife_runtime` | 唯一组装入口,按生命周期启动和回滚基础设施 | contracts、mcp、voice、linx、storage adapters |

依赖方向只有一条:适配器依赖用例,用例依赖领域,领域不认识 ESP-IDF、HTTP 或平台 SDK。CI 会运行 `scripts/check_architecture.sh` 检查组件清单、命名空间和依赖图。
Expand Down
4 changes: 4 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ ignore:
- "components/voicelife_linx_esp/**"
- "components/voicelife_audio_esp/**"
- "components/voicelife_runtime/**"
# 日程模块
- "components/voicelife_schedule/**"
# MCP模块
- "components/voicelife_mcp/**"
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ struct Result {
}
};

/** @brief 命令/用例返回结果,统一承载状态、业务值和错误说明。 */
template <typename T>
struct CommandResult {
Status status;
T value;
std::string error;

/** @brief 判断命令是否成功。 @return status 为成功时返回 true。 */
[[nodiscard]] bool ok() const { return status.ok(); }

/** @brief 创建成功结果。 @param value 命令返回的业务值。 @return 携带业务值的成功结果。 */
static CommandResult Success(T value) { return {Status::Ok(), std::move(value), {}}; }

/**
* @brief 创建失败结果。
* @param status 失败状态。
* @return 不携带业务值且保留错误说明的失败结果。
*/
static CommandResult Failure(Status status) {
const std::string error = status.message;
return {std::move(status), {}, error};
}
};

/** @brief 返回稳定的错误码名称。 @param code 要描述的错误码。 @return 静态字符串形式的名称。 */
const char* ErrorCodeName(ErrorCode code);

Expand Down
96 changes: 93 additions & 3 deletions components/voicelife_contracts/include/voicelife/contracts/tool.h
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
#pragma once

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>

#include "voicelife/contracts/json.h"
#include "voicelife/contracts/status.h"

namespace voicelife {

/// 工具调用参数当前支持的运行时值类型。
using ToolValue = std::variant<bool, int64_t, std::string>;
using ToolValue = std::variant<bool, int64_t, std::string, JsonValue>;
using ToolArguments = std::unordered_map<std::string, ToolValue>;

/// 描述一次进入设备侧的工具调用。
Expand All @@ -19,10 +25,94 @@ struct ToolCall {
ToolArguments arguments;
};

/// 保存工具调用的状态和具名输出值。
/// 工具返回的结构化 JSON 值。
struct ToolOutputValue;

/// 工具返回的数组元素集合。
using ToolOutputArray = std::vector<std::shared_ptr<ToolOutputValue>>;

/// 工具返回的对象成员集合;使用 vector 保持业务声明顺序。
using ToolOutputObject = std::vector<std::pair<std::string, std::shared_ptr<ToolOutputValue>>>;

/// 工具返回的结构化 JSON 值。
struct ToolOutputValue {
/** @brief 工具输出节点支持的运行时类型。 */
enum class Kind { kNull, kBoolean, kInteger, kString, kArray, kObject };

Kind kind = Kind::kNull;
bool boolean = false;
std::int64_t integer = 0;
std::string string;
std::shared_ptr<ToolOutputArray> array;
std::shared_ptr<ToolOutputObject> object;

/** @brief 构造空值。 @return 空值节点。 */
static ToolOutputValue Null() { return {}; }
/** @brief 构造布尔值节点。 @param value 布尔值。 @return 布尔节点。 */
static ToolOutputValue Boolean(bool value) {
ToolOutputValue output;
output.kind = Kind::kBoolean;
output.boolean = value;
return output;
}
/** @brief 构造整数节点。 @param value 整数值。 @return 整数节点。 */
static ToolOutputValue Integer(std::int64_t value) {
ToolOutputValue output;
output.kind = Kind::kInteger;
output.integer = value;
return output;
}
/** @brief 构造字符串节点。 @param value 字符串。 @return 字符串节点。 */
static ToolOutputValue String(std::string value) {
ToolOutputValue output;
output.kind = Kind::kString;
output.string = std::move(value);
return output;
}
/** @brief 构造数组节点。 @param value 数组元素。 @return 数组节点。 */
static ToolOutputValue Array(ToolOutputArray value) {
ToolOutputValue output;
output.kind = Kind::kArray;
output.array = std::make_shared<ToolOutputArray>(std::move(value));
return output;
}
/** @brief 构造对象节点。 @param value 有序成员。 @return 对象节点。 */
static ToolOutputValue Object(ToolOutputObject value) {
ToolOutputValue output;
output.kind = Kind::kObject;
output.object = std::make_shared<ToolOutputObject>(std::move(value));
return output;
}

/** @brief 判断当前值是否为对象。 @return 是对象时返回 true。 */
[[nodiscard]] bool IsObject() const { return kind == Kind::kObject; }
/** @brief 判断当前值是否为数组。 @return 是数组时返回 true。 */
[[nodiscard]] bool IsArray() const { return kind == Kind::kArray; }
/** @brief 判断当前值是否为字符串。 @return 是字符串时返回 true。 */
[[nodiscard]] bool IsString() const { return kind == Kind::kString; }
};

/** @brief 创建工具输出数组中的一个元素。 @param value 节点。 @return 节点共享指针。 */
inline std::shared_ptr<ToolOutputValue> MakeToolOutput(ToolOutputValue value) {
return std::make_shared<ToolOutputValue>(std::move(value));
}

/** @brief 创建工具输出对象成员。 @param key 成员名。 @param value 节点。 @return 有序成员。 */
inline std::pair<std::string, std::shared_ptr<ToolOutputValue>> MakeToolOutput(std::string key, ToolOutputValue value) {
return {std::move(key), MakeToolOutput(std::move(value))};
}

/// 保存工具调用的状态和结构化输出值。
struct ToolResult {
Status status;
std::unordered_map<std::string, std::string> output;
ToolOutputValue output = ToolOutputValue::Null();
/// 面向用户的精确文本;未设置时由边界适配器序列化结构化输出生成文本。
std::optional<std::string> text_output = std::nullopt;

/** @brief 创建成功结果。 @param output 结构化输出。 @return 成功结果。 */
static ToolResult Success(ToolOutputValue output) { return {Status::Ok(), std::move(output), std::nullopt}; }
/** @brief 创建失败结果。 @param status 失败状态。 @return 无输出的失败结果。 */
static ToolResult Failure(Status status) { return {std::move(status), ToolOutputValue::Null(), std::nullopt}; }
};

} // namespace voicelife
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ struct EspWebSocketTransportOptions {
uint32_t network_timeout_ms = 10000;
uint32_t reconnect_timeout_ms = 1000;
uint32_t websocket_task_stack_size = 12288;
uint32_t worker_task_stack_size = 12288;
// MCP 日程工具(schedule.create/query)在此 worker 任务上执行 SQLite/FATFS 操作,
// SQLite 的 sqlite3_step 需要较大栈,12KB 会导致栈溢出崩溃。
uint32_t worker_task_stack_size = 32768;
bool enable_close_reconnect = true;
bool allow_insecure_ws = false;
};
Expand Down
5 changes: 3 additions & 2 deletions components/voicelife_mcp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
idf_component_register(
SRCS "src/mcp_server.cc" "src/mcp_json_writer.cc"
SRCS "src/mcp_server.cc" "src/mcp_json_writer.cc" "src/tools/schedule_mcp_tools.cc"
"src/tools/schedule_mcp_tools_input.cc"
INCLUDE_DIRS "include"
REQUIRES voicelife_contracts
PRIV_REQUIRES yyjson
PRIV_REQUIRES voicelife_schedule yyjson
)
82 changes: 68 additions & 14 deletions components/voicelife_mcp/include/voicelife/mcp/mcp_server.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
Expand All @@ -11,13 +12,17 @@
namespace voicelife::mcp {

/// MCP 工具参数支持的数据类型。
enum class ToolInputType { kString, kInteger, kBoolean };
enum class ToolInputType { kString, kInteger, kBoolean, kObject };

/// MCP 工具输入参数的 JSON Schema 前向声明。
struct ToolInputSchema;

/// MCP 工具的单个输入字段定义。
struct ToolInputField {
ToolInputType type = ToolInputType::kString;
std::optional<ToolValue> default_value;
std::string description;
std::shared_ptr<ToolInputSchema> object_schema;
std::optional<int64_t> minimum;
std::optional<int64_t> maximum;
std::optional<std::size_t> min_length;
Expand Down Expand Up @@ -48,7 +53,10 @@ struct ListToolsResult {
};

/// 工具参数支持的类型。
enum class PropertyType { kBoolean, kInteger, kString };
enum class PropertyType { kBoolean, kInteger, kString, kObject };

/// 面向业务代码的 MCP 参数集合前向声明。
class PropertyList;

/// 面向业务代码的单个工具参数声明。
class Property {
Expand All @@ -69,25 +77,39 @@ class Property {
*/
Property(std::string name, PropertyType type, ToolValue default_value);
/**
* @brief 创建带整数范围约束的参数声明
* @brief 创建带内部字段定义的对象参数声明
* @param name 参数名称。
* @param type 参数类型,必须为整数。
* @param minimum 最小值。
* @param maximum 最大值。
* @param object_properties 对象内部字段定义。
* @return 无。
*/
Property(std::string name, PropertyType type, int64_t minimum, int64_t maximum);

Property(std::string name, PropertyList object_properties);
/**
* @brief 创建带字符串长度约束的参数声明
* @brief 创建带数值或字符串长度约束的参数声明
* @param name 参数名称。
* @param minimum 最小字符数。
* @param maximum 最大字符数。
* @param type 参数类型;整数类型使用数值范围,字符串类型使用字符长度。
* @param minimum 最小值或最小字符数。
* @param maximum 最大值或最大字符数。
* @param default_value 默认值;未设置时该参数为必填。
* @return 参数声明
* @return
*/
static Property WithStringLength(std::string name, std::size_t minimum, std::size_t maximum,
std::optional<ToolValue> default_value = std::nullopt);
Property(std::string name, PropertyType type, int64_t minimum, int64_t maximum,
std::optional<ToolValue> default_value = std::nullopt);

/**
* @brief 设置参数字段描述。
* @param description 输出到 JSON Schema 字段上的描述。
* @return 当前参数声明,便于链式构造。
*/
Property& with_description(std::string description);
/**
* @brief 设置对象参数内部字段定义。
* @param object_properties 对象内部字段定义。
* @return 当前参数声明,便于链式构造。
*/
Property& with_object_properties(PropertyList object_properties);

/** @brief 释放参数声明占用的资源。 */
~Property();

/**
* @brief 创建带整数范围约束的参数声明。
Expand All @@ -107,6 +129,13 @@ class Property {
* @return 可选参数声明。
*/
static Property Optional(std::string name, PropertyType type);
/**
* @brief 创建可省略的对象参数声明并设置内部字段定义。
* @param name 参数名称。
* @param object_properties 对象内部字段定义。
* @return 可选对象参数声明。
*/
static Property OptionalObject(std::string name, PropertyList object_properties);

/**
* @brief 获取参数名称。
Expand All @@ -118,6 +147,16 @@ class Property {
* @return 参数类型。
*/
[[nodiscard]] PropertyType type() const { return type_; }
/**
* @brief 获取参数字段描述。
* @return 字段描述;未设置时为空字符串。
*/
[[nodiscard]] const std::string& description() const { return description_; }
/**
* @brief 获取对象参数内部字段定义。
* @return 内部字段定义;未设置时为空。
*/
[[nodiscard]] const std::shared_ptr<PropertyList>& object_properties() const { return object_properties_; }
/**
* @brief 获取参数默认值。
* @return 默认值;未设置时为空。
Expand All @@ -137,6 +176,11 @@ class Property {
[[nodiscard]] std::optional<std::size_t> min_length() const { return min_length_; }
/** @brief 获取字符串最大长度。 @return 最大长度;未设置时为空。 */
[[nodiscard]] std::optional<std::size_t> max_length() const { return max_length_; }
/**
* @brief 判断约束参数是否能按声明类型解释。
* @return 约束有效时返回 true。
*/
[[nodiscard]] bool constraint_valid() const { return constraint_valid_; }
/**
* @brief 判断参数缺失时是否应拒绝调用。
* @return 参数必填时返回 true。
Expand All @@ -146,11 +190,14 @@ class Property {
private:
std::string name_;
PropertyType type_;
std::string description_;
std::shared_ptr<PropertyList> object_properties_;
std::optional<ToolValue> default_value_;
std::optional<int64_t> minimum_;
std::optional<int64_t> maximum_;
std::optional<std::size_t> min_length_;
std::optional<std::size_t> max_length_;
bool constraint_valid_ = true;
bool required_ = true;
};

Expand Down Expand Up @@ -272,4 +319,11 @@ class McpServer {
std::vector<std::string> registration_order_;
};

/**
* @brief 将结构化工具输出序列化为紧凑 JSON 文本。
* @param output 待序列化的工具输出。
* @return 序列化成功时返回 JSON 文本,失败时返回空对象。
*/
std::string SerializeToolOutputValue(const ToolOutputValue& output);

} // namespace voicelife::mcp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include "voicelife/contracts/status.h"

namespace voicelife::schedule {
/// 提供一次性日程服务能力。
class ScheduleService;
/// 提供周期日程规则服务能力。
class ScheduleRuleService;
} // namespace voicelife::schedule

namespace voicelife::mcp {

/// 用于注册日程 MCP 工具的 MCP Server 前向声明。
class McpServer;

/**
* @brief 向 MCP Server 注册当前日程工具。
* @param server 要注册工具的 MCP Server。
* @param service 一次性日程服务。
* @return 注册结果。
*/
Status RegisterScheduleMcpTools(McpServer& server, schedule::ScheduleService& service);

/**
* @brief 向 MCP Server 注册包含周期日程能力的日程工具。
* @param server 要注册工具的 MCP Server。
* @param service 一次性日程服务。
* @param rule_service 周期日程规则服务。
* @return 注册结果。
*/
Status RegisterScheduleMcpTools(McpServer& server, schedule::ScheduleService& service,
schedule::ScheduleRuleService& rule_service);

} // namespace voicelife::mcp
Loading
Loading