C++单元测试利器Mockcpp:从原理到实战的Mock框架深度解析
1. 从“写死”到“模拟”为什么我们需要Mock框架在单元测试里我们经常会遇到一个头疼的问题被测函数依赖了一个外部服务比如一个数据库查询接口、一个网络请求、或者一个复杂的第三方库。为了测试这个函数难道我们真的要去启动一个数据库、部署一个服务、或者安装一个庞大的第三方库吗这显然不现实。早期很多开发者的做法是“写死”一个返回值或者创建一个简单的“桩Stub”对象来模拟依赖。但这种方法很快会暴露问题当你想测试依赖对象抛出异常时怎么办当你想验证被测函数是否以特定参数调用了依赖对象时怎么办当依赖对象的返回值需要根据输入动态变化时又怎么办这就是Mock框架诞生的背景。它不仅仅是一个返回固定值的“桩”而是一个功能强大的“演员”可以按照你编写的“剧本”即测试预期来精确地模拟一个对象的行为。你可以告诉它“当调用getUserById(123)时返回一个名为‘张三’的用户对象当调用getUserById(456)时抛出一个NotFoundException并且请记录saveUser方法是否被调用以及调用时的参数是什么。”Mockcpp就是这样一个专门为C设计的Mock框架。在C的世界里由于语言本身的复杂性如多继承、模板、运算符重载等编写Mock对象一直是个挑战。Mockcpp通过精巧的设计提供了强大而灵活的Mock能力让我们能够为C的接口抽象类或普通类轻松创建Mock对象从而编写出隔离性好、断言精准的单元测试。它解决的正是C单元测试中“依赖隔离”这个核心痛点。2. Mockcpp核心机制剖析它如何“扮演”一个对象要理解Mockcpp的使用首先得明白它是如何工作的。Mockcpp的核心魔法在于它拦截了你对Mock对象的虚函数调用并将这些调用导向你预先设定好的“行为”和“预期”。2.1 核心概念Mock对象、Mock方法与期望想象一下你正在导演一出戏。MOCKER宏就是你的选角导演它负责创建一个特定类型的Mock对象演员。MOCK_METHOD则是你给演员的剧本片段它告诉Mock对象“你有一个叫methodA的方法需要被模拟。”而EXPECT_CALL和stub/will/then这一系列调用就是你写的具体戏份和台词。EXPECT_CALL设定了预期“我期望methodA会被调用并且调用时的参数应该是value1。”stub、will、then则设定了行为“当它被调用时你应该返回value2。”这里的关键在于**“期望Expectation”和“行为Stub”**的分离。你可以只设定行为而不关心它是否被调用用于提供测试数据也可以只设定期望而不指定行为用于验证调用是否发生当然大多数情况下是两者结合。2.2 基于虚函数表的拦截原理C的多态依赖于虚函数表vtable。当一个类含有虚函数时每个对象内部都有一个指针指向该类的虚函数表表中存放着各个虚函数的实际地址。Mockcpp在创建Mock对象时会动态生成一个派生自被Mock类的子类并重写其虚函数。重写后的函数并不执行原始逻辑而是跳转到Mockcpp自身的调度器。这个调度器根据当前的调用上下文哪个对象、哪个方法、什么参数去查找匹配的“期望-行为”对并执行相应的行为返回指定值、抛出异常、调用回调函数等同时记录这次调用以备后续验证。这种基于继承和虚函数重写的机制决定了Mockcpp主要适用于Mock含有虚函数的类即接口或抽象类。对于非虚函数Mockcpp也提供了一些支持如使用MOCK_NON_VIRTUAL_METHOD但通常需要更多的配置且可能涉及链接期替换等技术限制会多一些。3. 从零开始一个完整的Mockcpp使用示例理论说得再多不如动手一试。我们假设有一个简单的用户服务接口IUserService和一个依赖它的业务类UserManager。我们的目标是测试UserManager而不依赖真实的IUserService实现。3.1 定义接口与业务类首先定义我们待测试的接口和业务类。// IUserService.h - 被模拟的接口 #ifndef IUSERSERVICE_H #define IUSERSERVICE_H #include string #include stdexcept class UserNotFoundException : public std::runtime_error { public: UserNotFoundException(const std::string msg) : std::runtime_error(msg) {} }; class IUserService { public: virtual ~IUserService() default; // 根据ID获取用户名 virtual std::string getUserName(int userId) 0; // 验证用户凭据 virtual bool verifyCredentials(const std::string username, const std::string password) 0; // 更新用户最后登录时间返回是否成功 virtual bool updateLastLogin(int userId) 0; }; #endif // IUSERSERVICE_H// UserManager.h - 待测试的业务类 #ifndef USERMANAGER_H #define USERMANAGER_H #include IUserService.h #include string class UserManager { public: explicit UserManager(IUserService userService) : m_userService(userService) {} // 业务方法获取用户显示名称如果用户不存在则返回Guest std::string getUserDisplayName(int userId); // 业务方法用户登录逻辑 bool login(const std::string username, const std::string password); private: IUserService m_userService; }; #endif // USERMANAGER_H// UserManager.cpp #include UserManager.h std::string UserManager::getUserDisplayName(int userId) { try { return m_userService.getUserName(userId); } catch (const UserNotFoundException) { return Guest; } } bool UserManager::login(const std::string username, const std::string password) { if (m_userService.verifyCredentials(username, password)) { // 假设登录成功后更新最后登录时间userId这里简化为通过username哈希得到 int fakeUserId std::hashstd::string{}(username) % 10000; return m_userService.updateLastLogin(fakeUserId); } return false; }3.2 编写单元测试接下来我们使用Google Test作为测试框架并集成Mockcpp来编写对UserManager的单元测试。// UserManagerTest.cpp #include gtest/gtest.h #include mockcpp/mockcpp.hpp #include UserManager.h // 1. 创建Mock对象 // 使用MOCKER宏为IUserService接口创建一个Mock类。 // 模板参数是待Mock的类名。这个宏会生成一个名为mockIUserService的类。 MOCKER(IUserService); class UserManagerTest : public ::testing::Test { protected: void SetUp() override { // 在每个测试用例开始前创建Mock对象并置入全局容器。 // 注意Mockcpp要求Mock对象必须被GlobalMockObject管理否则可能导致内存问题或行为异常。 mockObject MOCK_OBJECT(IUserService); } void TearDown() override { // 在每个测试用例结束后清理全局Mock对象和所有期望。 // 这是至关重要的一步能防止测试用例间的期望互相干扰。 GlobalMockObject::verify(); GlobalMockObject::clear(); } // 声明一个Mock对象的指针方便在测试中使用。 IUserService* mockObject; }; // 测试用例1测试getUserDisplayName当用户存在时 TEST_F(UserManagerTest, ShouldReturnUserNameWhenUserExists) { // 2. 创建被测对象注入Mock对象 UserManager manager(*mockObject); // 3. 设置期望和行为 (Expectation and Stub) // 期望getUserName方法会被调用一次且参数为1001。 // 行为当被调用时返回字符串Alice。 EXPECT_CALL(*mockObject, getUserName(1001)) .stubs() .will(returnValue(std::string(Alice))); // 4. 执行被测方法 std::string result manager.getUserDisplayName(1001); // 5. 断言结果 EXPECT_EQ(result, Alice); // Mockcpp会在TearDown中的GlobalMockObject::verify()自动验证所有期望是否满足。 } // 测试用例2测试getUserDisplayName当用户不存在时抛出异常 TEST_F(UserManagerTest, ShouldReturnGuestWhenUserNotFound) { UserManager manager(*mockObject); // 设置期望和行为当调用getUserName(9999)时抛出UserNotFoundException异常。 EXPECT_CALL(*mockObject, getUserName(9999)) .stubs() .will(throwException(UserNotFoundException(User 9999 not found))); std::string result manager.getUserDisplayName(9999); // 业务逻辑规定捕获异常后返回Guest EXPECT_EQ(result, Guest); } // 测试用例3测试login验证凭据成功并更新登录时间 TEST_F(UserManagerTest, ShouldReturnTrueWhenLoginSucceeds) { UserManager manager(*mockObject); // 设置第一个期望verifyCredentials被调用参数为(bob, secret)返回true。 EXPECT_CALL(*mockObject, verifyCredentials(bob, secret)) .stubs() .will(returnValue(true)); // 设置第二个期望updateLastLogin被调用一次。 // 这里使用了参数匹配器ANY表示接受任何int参数。 // 行为是返回true。 EXPECT_CALL(*mockObject, updateLastLogin(ANY(int))) .stubs() .will(returnValue(true)); bool result manager.login(bob, secret); EXPECT_TRUE(result); } // 测试用例4测试login验证凭据失败 TEST_F(UserManagerTest, ShouldReturnFalseWhenLoginFails) { UserManager manager(*mockObject); // 设置期望verifyCredentials被调用参数为(eve, wrong)返回false。 // 由于返回false业务逻辑不会调用updateLastLogin所以我们不设置它的期望。 EXPECT_CALL(*mockObject, verifyCredentials(eve, wrong)) .stubs() .will(returnValue(false)); bool result manager.login(eve, wrong); EXPECT_FALSE(result); }3.3 编译与运行你需要将Mockcpp库链接到你的测试项目中。通常的CMakeLists.txt关键部分如下cmake_minimum_required(VERSION 3.10) project(UserManagerTest) # 查找GoogleTest和Mockcpp find_package(GTest REQUIRED) # 假设Mockcpp通过源码集成或已安装 include_directories(${PROJECT_SOURCE_DIR}/third_party/mockcpp/include) # Mockcpp头文件路径 add_library(mockcpp STATIC ${PROJECT_SOURCE_DIR}/third_party/mockcpp/src/*.cpp) # Mockcpp源码路径 add_executable(runTests UserManagerTest.cpp UserManager.cpp ) target_link_libraries(runTests GTest::gtest GTest::gtest_main mockcpp pthread # Linux下GTest可能需要 ) enable_testing() add_test(NAME UserManagerTests COMMAND runTests)运行测试后你将看到所有测试通过。Mockcpp确保了UserManager的逻辑在与真实IUserService完全隔离的情况下得到了充分验证。4. 进阶技巧与实战中的“坑”掌握了基本用法后在实际项目中应用Mockcpp你会遇到一些更复杂的需求和常见的陷阱。4.1 参数匹配器让期望更灵活在上面的例子中我们使用了ANY(int)。Mockcpp提供了丰富的参数匹配器Matcher让你可以编写更灵活、更精确的期望。eq(value): 严格等于某个值。EXPECT_CALL(*mock, func(eq(10)))。ne(value): 不等于某个值。gt(value),ge(value),lt(value),le(value): 大于、大于等于、小于、小于等于。mirror(const T): 匹配与给定变量值相等的参数常用于在will中引用变量。startWith(str),endWith(str),contains(str): 用于字符串匹配。any(): 匹配任何类型的任何值是ANY的类型安全版本。例如测试一个查找函数我们可能不关心具体的ID但关心它被调用了EXPECT_CALL(*mockObject, findUser(gt(1000))) // 期望ID大于1000 .times(2) // 期望被调用恰好2次 .will(returnValue(User(DefaultUser)));4.2 调用次数约束与顺序验证times()约束方法被调用的次数.times(3): 恰好3次。.times(0, 5): 0到5次之间。.once(): 恰好一次等同于.times(1)。.atLeast(2): 至少2次。.atMost(4): 最多4次。before(),after()可以验证调用发生的相对顺序这对于测试有严格顺序要求的逻辑非常有用。auto exp1 EXPECT_CALL(*mock, step1()).once(); auto exp2 EXPECT_CALL(*mock, step2()).once().after(exp1); auto exp3 EXPECT_CALL(*mock, step3()).once().after(exp2); // 这确保了调用顺序必须是 step1 - step2 - step34.3 动态行为与副作用will()可以做的不仅仅是返回固定值returnValueList: 依次返回一个列表中的值适用于模拟多次调用返回不同值。invoke(func): 调用一个自定义的函数或函数对象实现复杂的返回值逻辑。ignoreReturnValue: 忽略返回值适用于返回void或我们不关心返回值的情况。increase(from, to): 每次调用返回一个递增的值。static int callCount 0; std::string dynamicName(int id) { callCount; return User_ std::to_string(id) _Call std::to_string(callCount); } EXPECT_CALL(*mockObject, getUserName(ANY(int))) .stubs() .will(invoke(dynamicName)); // 每次调用都执行dynamicName函数4.4 常见陷阱与调试心得忘记GlobalMockObject::clear()这是最常犯的错误。如果不在每个测试用例的TearDown中调用clear()上一个测试用例设置的期望会“泄漏”到下一个测试用例中导致难以理解的测试失败。务必在测试夹具的TearDown方法中调用GlobalMockObject::verify()和GlobalMockObject::clear()。Mock非虚函数Mockcpp对非虚函数的Mock支持有限且更复杂通常需要借助链接器包装Link Seam或更高级的Mock技术如HippoMocks。在项目初期尽量将需要Mock的依赖设计为接口虚函数这不仅是测试友好的也是良好的设计实践。过于严格或过于宽松的匹配过度使用ANY可能导致测试不够精确掩盖了bug而过度使用严格匹配如eq精确值又可能让测试变得脆弱比如一个无关紧要的默认参数值改变就会导致测试失败。我的经验是对业务关键参数使用严格匹配对辅助性或无关紧要的参数使用宽松匹配如ANY。验证未被调用的方法有时你需要确保某个方法没有被调用。Mockcpp默认不验证未被设置期望的方法。你可以通过设置一个期望调用次数为0.times(0)来达到这个目的但这通常意味着你需要为所有不希望被调用的方法都设置期望这在大型接口上很繁琐。更好的模式是使用“严格Mock”Strict Mock概念即所有未被明确允许stub的调用都会导致测试失败。Mockcpp本身不直接提供严格Mock模式但你可以通过在一个测试用例开始时为所有方法设置一个默认的、调用次数为0的期望来近似实现不过这需要较多样板代码。更常见的做法是依赖代码审查和良好的测试用例设计。调试输出当测试失败提示“Unmatched Invocation”或“Expectation violated”时Mockcpp的错误信息会打印出未匹配的调用签名。仔细阅读这些信息它们能精准地告诉你哪个方法的哪个参数不匹配。配合调试器查看在调用时刻的参数实际值是快速定位问题的关键。5. Mockcpp在持续集成与大型项目中的实践在大型C项目中单元测试是持续集成CI流水线的基石。Mockcpp在这里扮演着关键角色。项目结构组织我们通常将测试代码放在单独的test目录下与被测源码平行。Mock对象的创建和常用期望设置可以封装在测试夹具Fixture或辅助类中避免重复代码。例如可以为IUserService创建一个MockUserServiceHelper类提供诸如setupForValidUser(int id, const string name)这样的方法。与测试框架的集成除了Google TestMockcpp同样可以无缝集成到CppUnit、Catch2等主流C测试框架中。集成模式大同小异在SetUp中初始化Mock在TearDown中验证和清理。性能考量Mockcpp在运行时动态创建类型和设置期望这会带来轻微的开销。但在单元测试中这通常可以忽略不计。需要注意的是不要在每个测试用例中重复创建和销毁大量复杂的Mock对象如果Mock对象构造成本高可以考虑在夹具的SetUpTestSuite类级别中创建一次并在各个测试用例中重置其状态。测试替身的选择策略Mock不是唯一的测试替身。根据Martin Fowler的总结还有Dummy、Fake、Stub、Spy等。Mockcpp主要用来创建Mock强调行为验证和Stub强调提供间接输入。对于一些简单的、状态固定的依赖手动写一个轻量级的Fake实现可能比配置复杂的Mock更清晰、运行更快。例如对于一个内存中的KeyValueStore接口直接用一个std::map实现的Fake类可能比用Mockcpp更合适。决策的关键在于你测试的重点是交互行为还是最终状态在我经历的一个音视频处理引擎项目中我们使用Mockcpp模拟了编解码器接口、网络传输模块和硬件抽象层。这使得我们能在没有真实硬件和网络环境的开发机上对核心业务逻辑如会话管理、流量控制、错误恢复进行了高达90%分支覆盖率的单元测试。当CI流水线每天运行数千个这样的测试用例时它能快速捕捉到因代码修改而引入的回归错误极大地提升了代码质量和开发效率。Mockcpp的稳定性和灵活性是支撑起这套测试体系的重要一环。

相关新闻

OpenClaw AI智能体框架:从部署到实战的完整工程指南

OpenClaw AI智能体框架:从部署到实战的完整工程指南

1. 从单兵到军团:为什么我们需要OpenClaw这样的AI团队框架?如果你最近在折腾AI应用,尤其是想搞点能自动处理复杂任务的智能体(Agent),那你大概率已经听过OpenClaw这个名字了。它不是一个新的大模型&#xf…

2026/8/15 7:16:42 阅读更多 →
构建可验证、可评测的AI Agent工具调用工程框架

构建可验证、可评测的AI Agent工具调用工程框架

1. 项目概述:从“玩具”到“工程”的跨越最近和几个做AI应用的朋友聊天,发现一个挺普遍的现象:大家用LangChain、AutoGPT或者OpenAI的Assistant API搭个能聊天的Agent,跑通一个Demo,感觉挺酷,但一到要真正上…

2026/8/15 7:16:42 阅读更多 →
JEP Guard:Java Native调用安全防护与稳定性实践

JEP Guard:Java Native调用安全防护与稳定性实践

1. 项目概述:从“养虾”到“护城河”的代码安全实践最近在做一个挺有意思的插件项目,名字叫JEP Guard。乍一听“养虾人”,可能有点摸不着头脑,但如果你在Java生态里摸爬滚打过,尤其是处理过一些需要调用本地代码&#…

2026/8/15 7:15:41 阅读更多 →

最新新闻

网工毕业设计最新项目选题分享

网工毕业设计最新项目选题分享

文章目录 🚩 1 前言1.1 选题注意事项1.1.1 难度怎么把控?1.1.2 题目名称怎么取? 1.2 选题推荐1.2.1 起因1.2.2 核心- 如何避坑(重中之重)1.2.3 怎么办呢? 🚩2 选题概览🚩 3 项目概览题目1 : 图像隐写算法研…

2026/8/15 7:59:54 阅读更多 →
MySQL 8.0密码修改:从ERROR 1064到ALTER USER的正确姿势

MySQL 8.0密码修改:从ERROR 1064到ALTER USER的正确姿势

1. 问题缘起:一个看似简单的操作引发的“血案” 那天下午,我正在给一台新部署的服务器配置MySQL环境。一切都按部就班,初始化、启动服务、获取临时密码,然后准备用那个我闭着眼睛都能敲出来的 mysqladmin 命令去修改 root 用户…

2026/8/15 7:59:54 阅读更多 →
Python实战能力构建:从环境配置到项目驱动的学习路径

Python实战能力构建:从环境配置到项目驱动的学习路径

1. 从“学Python”到“用Python”:一个老码农的十年弯路与正道 “学Python”这三个字,我猜你已经在搜索引擎里输入过无数次了。铺天盖地的“三天入门”、“七天精通”教程,海量的免费源码和“从入门到放弃”的段子,构成了大多数初…

2026/8/15 7:59:54 阅读更多 →
百度网盘直链解析工具实测:下载速度从 12KB/s 飙到 2.5MB/s 的完整攻略

百度网盘直链解析工具实测:下载速度从 12KB/s 飙到 2.5MB/s 的完整攻略

百度网盘直链解析工具实测:下载速度从 12KB/s 飙到 2.5MB/s 的完整攻略 【免费下载链接】baidu-wangpan-parse 获取百度网盘分享文件的下载地址 项目地址: https://gitcode.com/gh_mirrors/ba/baidu-wangpan-parse 晚上十一点,你盯着百度网盘的进…

2026/8/15 7:59:54 阅读更多 →
Qt与SQLite集成开发:从入门到实战的完整指南

Qt与SQLite集成开发:从入门到实战的完整指南

1. 项目概述:为什么Qt开发者绕不开SQLite 如果你正在用Qt做桌面端、移动端或者嵌入式开发,并且需要处理一些本地数据,比如用户配置、日志记录、缓存文件,或者一个中小型应用的核心数据存储,那么你大概率会和我一样&…

2026/8/15 7:59:54 阅读更多 →
C++零基础入门指南:从命令行编译到STL实战项目

C++零基础入门指南:从命令行编译到STL实战项目

1. 从“Hello World”到“我能写点什么”:零基础的心理建设与起点选择 看到这个标题,你可能会想,又是一篇老生常谈的“学习路线”。但我想说的是,这份路线图,是我和身边很多朋友,从对着黑框框敲下第一个“H…

2026/8/15 7:58:54 阅读更多 →

日新闻

内景 空间站内部 中国空间站 太空 内仓

内景 空间站内部 中国空间站 太空 内仓

本项目为前几天收费帮学妹做的一个项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。 一、项目描述 空间站内部 中国空间站 太空 内仓 地址:本地PC端运行(或Web…

2026/8/15 0:00:30 阅读更多 →
重新定义数据接口:3个突破性场景让通达信数据读取更智能

重新定义数据接口:3个突破性场景让通达信数据读取更智能

重新定义数据接口:3个突破性场景让通达信数据读取更智能 【免费下载链接】mootdx 通达信数据读取的一个简便使用封装 项目地址: https://gitcode.com/GitHub_Trending/mo/mootdx 当我们面对海量金融数据时,传统的数据获取方式往往让我们陷入困境—…

2026/8/15 0:00:30 阅读更多 →
一文读懂快消WMS怎么选?2026年国内外10大主流WMS品牌盘点

一文读懂快消WMS怎么选?2026年国内外10大主流WMS品牌盘点

快消品(FMCG)是流通速度较快、竞争较为激烈的行业之一。一瓶饮料从出厂到消费者手中,往往只有几十天甚至几天的周转窗口。这决定了快消行业的仓储管理系统(WMS)与制造业、电商行业存在明显区别:它不仅需要管…

2026/8/15 0:02:30 阅读更多 →

周新闻

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁 【免费下载链接】baidupankey 在线查询网盘提取码(维护中 rm repo) 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 你是否曾经在深夜寻找一份重要资料&#x…

2026/8/13 2:38:34 阅读更多 →
如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南 【免费下载链接】chinese_license_plate_generator 中国车牌生成器 项目地址: https://gitcode.com/gh_mirrors/ch/chinese_license_plate_generator 中国车牌生成器是一个基于Python的开源项目&#xff0c…

2026/8/13 10:41:52 阅读更多 →
收藏!小白程序员轻松入门大模型,从Harness工程开始实践

收藏!小白程序员轻松入门大模型,从Harness工程开始实践

文章强调学习大模型不应只关注模型本身,而应重视模型外的系统搭建,即Harness。提出AgentModelHarness的实用公式,详细介绍Harness的四个层次:持久化层、执行层、控制层和观察与验证层。文章还探讨了上下文工程、工具设计、AGENTS.…

2026/8/13 10:41:51 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/14 13:40:53 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/14 14:06:45 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/15 2:35:29 阅读更多 →