当前位置: 首页 > news >正文

鸿蒙 WiFi 连接 流程

那当界面上显示扫描到的所有Ap时,我们选择其中的一个Ap发起连接,看下代码流程是怎样的。

// applications/standard/settings/product/phone/src/main/ets/model/wifiImpl/WifiModel.tsconnectWiFi(password: string) {let apInfo = this.userSelectedAp.getApInfo();let ret = false;let connectParam: any = {"ssid": apInfo.ssid,"bssid": apInfo.bssid,"preSharedKey": password,"isHiddenSsid": false, // we don't support connect to hidden ap yet"securityType": apInfo.securityType};LogUtil.info(MODULE_TAG + 'disconnect WiFi isConnected is ' + wifi.isConnected());     ---》当前如果没有连接就是falseif (wifi.isConnected() === true) {ret = wifi.disconnect();LogUtil.info(MODULE_TAG + 'disconnect WiFi ret is ' + ret);this.registerWiFiConnectionObserver((code: Number) => {if (code === 0) {ret = wifi.connectToDevice(connectParam);this.unregisterWiFiConnectionObserver();}})}else{ret = wifi.connectToDevice(connectParam);                        ---> 就会走这里发起连接LogUtil.info(MODULE_TAG + 'connect WiFi ret is ' + ret);}return ret;}
// 有了前面的基础我们知道Wifi_Device的实现是wifi_device_impl, 然后在通过代理和服务端交互,所以我们直接看服务端的实现即可,调用流程这里就省略掉
// foundation/communication/wifi/wifi/services/wifi_standard/wifi_framework/wifi_manage/wifi_device_service_impl.cpp
ErrCode WifiDeviceServiceImpl::ConnectToDevice(const WifiDeviceConfig &config)
{if (!WifiAuthCenter::IsSystemAppByToken()) {WIFI_LOGE("ConnectToDevice:NOT System APP, PERMISSION_DENIED!");return WIFI_OPT_NON_SYSTEMAPP;}if (WifiPermissionUtils::VerifySetWifiInfoPermission() == PERMISSION_DENIED) {WIFI_LOGE("ConnectToDevice:VerifySetWifiInfoPermission PERMISSION_DENIED!");return WIFI_OPT_PERMISSION_DENIED;}if (WifiPermissionUtils::VerifyWifiConnectionPermission() == PERMISSION_DENIED) {WIFI_LOGE("ConnectToDevice:VerifyWifiConnectionPermission PERMISSION_DENIED!");return WIFI_OPT_PERMISSION_DENIED;}if (WifiPermissionUtils::VerifySetWifiConfigPermission() == PERMISSION_DENIED) {WIFI_LOGE("ConnectToDevice:VerifySetWifiConfigPermission PERMISSION_DENIED!");return WIFI_OPT_PERMISSION_DENIED;}if (!CheckConfigPwd(config)) {WIFI_LOGE("CheckConfigPwd failed!");return WIFI_OPT_INVALID_PARAM;}if (!IsStaServiceRunning()) {WIFI_LOGE("ConnectToDevice: sta service is not running!");return WIFI_OPT_STA_NOT_OPENED;}IStaService *pService = WifiServiceManager::GetInstance().GetStaServiceInst();if (pService == nullptr) {WIFI_LOGE("ConnectToNetwork: pService is nullptr!");return WIFI_OPT_STA_NOT_OPENED;}return pService->ConnectToDevice(config);
}// foundation/communication/wifi/wifi/services/wifi_standard/wifi_framework/wifi_manage/wifi_sta/sta_interface.cpp
ErrCode StaInterface::ConnectToDevice(const WifiDeviceConfig &config)
{LOGD("Enter StaInterface::Connect.\n");CHECK_NULL_AND_RETURN(pStaService, WIFI_OPT_FAILED);if (pStaService->ConnectToDevice(config) != WIFI_OPT_SUCCESS) {LOGD("ConnectTo failed.\n");return WIFI_OPT_FAILED;}return WIFI_OPT_SUCCESS;
}// foundation/communication/wifi/wifi/services/wifi_standard/wifi_framework/wifi_manage/wifi_sta/sta_service.cpp
ErrCode StaService::ConnectToDevice(const WifiDeviceConfig &config) const
{LOGI("Enter StaService::ConnectToDevice, ssid = %{public}s.\n", SsidAnonymize(config.ssid).c_str());CHECK_NULL_AND_RETURN(pStaStateMachine, WIFI_OPT_FAILED);int netWorkId = AddDeviceConfig(config);       ---》 对于新的AP,先添加,和Android都是一样的if(netWorkId == INVALID_NETWORK_ID) {LOGD("StaService::ConnectToDevice, AddDeviceConfig failed!");return WIFI_OPT_FAILED;}LOGI("StaService::ConnectToDevice, netWorkId: %{public}d", netWorkId);pStaStateMachine->SendMessage(WIFI_SVR_CMD_STA_CONNECT_NETWORK, netWorkId, NETWORK_SELECTED_BY_USER);    ---> 让状态机处理连接return WIFI_OPT_SUCCESS;
}

这里主要是两个函数:
一个是 AddDeviceConfig ,
一个是SendMessage(WIFI_SVR_CMD_STA_CONNECT_NETWORK, netWorkId, NETWORK_SELECTED_BY_USER)
流程都是一样的,通过,通过RPC访问HAL层,然后再把命令发给wpa,wpa收到命令后触发相应动作,这里我们看后者,状态机处理消息
// foundation/communication/wifi/wifi/services/wifi_standard/wifi_framework/wifi_manage/wifi_sta/sta_state_machine.cpp

// 调用的这个函数:
void StaStateMachine::DealConnectToUserSelectedNetwork(InternalMessage *msg)
{LOGI("enter DealConnectToUserSelectedNetwork.\n");if (msg == nullptr) {LOGE("msg is null.\n");return;}int networkId = msg->GetParam1();int connTriggerMode = msg->GetParam2();if (connTriggerMode != NETWORK_SELECTED_BY_RETRY) {linkedInfo.retryedConnCount = 0;}if (networkId == linkedInfo.networkId) {if (linkedInfo.connState == ConnState::CONNECTED) {staCallback.OnStaConnChanged(OperateResState::CONNECT_AP_CONNECTED, linkedInfo);WIFI_LOGI("This network is in use and does not need to be reconnected.\n");return;}if (linkedInfo.connState == ConnState::CONNECTING &&linkedInfo.detailedState == DetailedState::OBTAINING_IPADDR) {WIFI_LOGI("This network is connecting and does not need to be reconnected.\n");return;}}/* Save connection information. */SaveDiscReason(DisconnectedReason::DISC_REASON_DEFAULT);SaveLinkstate(ConnState::CONNECTING, DetailedState::CONNECTING);/* Callback result to InterfaceService. */staCallback.OnStaConnChanged(OperateResState::CONNECT_CONNECTING, linkedInfo);if (StartConnectToNetwork(networkId) != WIFI_OPT_SUCCESS) {     ---》 继续看这个流程OnConnectFailed(networkId);return;}/* Sets network status. */WifiSettings::GetInstance().EnableNetwork(networkId, connTriggerMode == NETWORK_SELECTED_BY_USER);WifiSettings::GetInstance().SetDeviceAfterConnect(networkId);WifiSettings::GetInstance().SetDeviceState(networkId, (int)WifiDeviceConfigStatus::ENABLED, false);
}//
ErrCode StaStateMachine::StartConnectToNetwork(int networkId)
{targetNetworkId = networkId;SetRandomMac(targetNetworkId);     ---> 随机mac地址WifiDeviceConfig deviceConfig;if (WifiSettings::GetInstance().GetDeviceConfig(networkId, deviceConfig) != 0) {LOGE("StartConnectToNetwork get GetDeviceConfig failed!");return WIFI_OPT_FAILED;}WifiStaHalInterface::GetInstance().SetBssid(networkId, deviceConfig.userSelectBssid.c_str());// 使能apif (WifiStaHalInterface::GetInstance().EnableNetwork(targetNetworkId) != WIFI_IDL_OPT_OK) {LOGE("EnableNetwork() failed!");return WIFI_OPT_FAILED;}// 连接if (WifiStaHalInterface::GetInstance().Connect(targetNetworkId) != WIFI_IDL_OPT_OK) {LOGE("Connect failed!");staCallback.OnStaConnChanged(OperateResState::CONNECT_SELECT_NETWORK_FAILED, linkedInfo);return WIFI_OPT_FAILED;}// 保存if (WifiStaHalInterface::GetInstance().SaveDeviceConfig() != WIFI_IDL_OPT_OK) {/* OHOS's wpa don't support save command, so don't judge as failure */LOGE("SaveDeviceConfig() failed!");}StopTimer(static_cast<int>(CMD_NETWORK_CONNECT_TIMEOUT));StartTimer(static_cast<int>(CMD_NETWORK_CONNECT_TIMEOUT), STA_NETWORK_CONNECTTING_DELAY);return WIFI_OPT_SUCCESS;// 上面的三步骤和Android都差不多,基本上大家都熟悉
// foundation/communication/wifi/wifi/services/wifi_standard/wifi_framework/wifi_manage/idl_client/wifi_sta_hal_interface.cpp
WifiErrorNo WifiStaHalInterface::Connect(int networkId)
{CHECK_NULL_AND_RETURN(mIdlClient, WIFI_IDL_OPT_FAILED);return mIdlClient->ReqConnect(networkId);
}
// idl_client , 前面几篇都讲过了,这里直接贴HAL层的代码了
// foundation/communication/wifi/wifi/services/wifi_standard/wifi_hal/wifi_hal_crpc_sta.c
int RpcConnect(RpcServer *server, Context *context)
{if (server == NULL || context == NULL) {return HAL_FAILURE;}int networkId = 0;if (ReadInt(context, &networkId) < 0) {return HAL_FAILURE;}WifiErrorNo err = Connect(networkId);    ---> 看这个的实现WriteBegin(context, 0);WriteInt(context, err);WriteEnd(context);return HAL_SUCCESS;
}// foundation/communication/wifi/wifi/services/wifi_standard/wifi_hal/wifi_hal_sta_interface.c
WifiErrorNo Connect(int networkId)
{LOGD("Connect() networkid %{public}d", networkId);WifiWpaStaInterface *pStaIfc = GetWifiStaInterface(0);if (pStaIfc == NULL) {return WIFI_HAL_SUPPLICANT_NOT_INIT;}int ret = pStaIfc->wpaCliCmdSelectNetwork(pStaIfc, networkId);if (ret < 0) {LOGE("WpaCliCmdSelectNetwork failed! ret=%{public}d", ret);return WIFI_HAL_FAILED;}return WIFI_HAL_SUCCESS;
}

继续看 pStaIfc->wpaCliCmdSelectNetwork 的调用,

//foundation/communication/wifi/wifi/services/wifi_standard/wifi_hal/wifi_hal_module/wpa_supplicant_hal/wpa_sta_hal/wifi_supplicant_hal.c 
static int WpaCliCmdSelectNetwork(WifiWpaStaInterface *this, int networkId)
{if (this == NULL) {return -1;}char cmd[CMD_BUFFER_SIZE] = {0};char buf[REPLY_BUF_SMALL_LENGTH] = {0};if (snprintf_s(cmd, sizeof(cmd), sizeof(cmd) - 1, "IFNAME=%s SELECT_NETWORK %d", this->ifname, networkId) < 0) {LOGE("snprintf err");return -1;}return WpaCliCmd(cmd, buf, sizeof(buf));
}// foundation/communication/wifi/wifi/services/wifi_standard/wifi_hal/wifi_hal_module/wpa_supplicant_hal/wifi_wpa_common.cint WpaCliCmd(const char *cmd, char *buf, size_t bufLen)
{if (cmd == NULL || buf == NULL || bufLen <= 0) {LOGE("WpaCliCmd, invalid parameters!");return -1;}WpaCtrl *ctrl = GetWpaCtrl();if (ctrl == NULL || ctrl->pSend == NULL) {LOGE("WpaCliCmd, ctrl/ctrl->pSend is NULL!");return -1;}size_t len = bufLen - 1;LOGI("wpa_ctrl_request -> cmd: %{private}s", cmd);int ret = wpa_ctrl_request(ctrl->pSend, cmd, strlen(cmd), buf, &len, NULL);    ---》 发送给wpaif (ret == WPA_CMD_RETURN_TIMEOUT) {LOGE("[%{private}s] command timed out.", cmd);return WPA_CMD_RETURN_TIMEOUT;} else if (ret < 0) {LOGE("[%{private}s] command failed.", cmd);return -1;}buf[len] = '\0';LOGI("wpa_ctrl_request -> buf: %{private}s", buf);if (strncmp(buf, "FAIL\n", strlen("FAIL\n")) == 0 ||strncmp(buf, "UNKNOWN COMMAND\n", strlen("UNKNOWN COMMAND\n")) == 0) {LOGE("%{private}s request success, but response %{public}s", cmd, buf);return -1;}return 0;
}

至此,连接的命令就发给wpa了,接下来就是协议上的连接了(四次握手),由wpa去交互完成,连接成功后,wpa上报 CTRL-EVENT-CONNECTED 事件,上层就收到,就开始走DHCP/或者是静态ip的流程,下一篇继续梳理这个。

相关文章:

鸿蒙 WiFi 连接 流程

那当界面上显示扫描到的所有Ap时&#xff0c;我们选择其中的一个Ap发起连接&#xff0c;看下代码流程是怎样的。 // applications/standard/settings/product/phone/src/main/ets/model/wifiImpl/WifiModel.tsconnectWiFi(password: string) {let apInfo this.userSelectedAp…...

golang 创建unix socket http服务端

服务端 package mainimport ("fmt""net""net/http""os" )func main() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {w.Write([]byte("hello"))})http.HandleFunc("/world", …...

annaconda如何切换当前python环境

annaconda默认的python环境是base&#xff1a; 把各种项目的依赖都安装到base环境中不是一个好的习惯&#xff0c;比如说我们做爬虫项目和做自动化测试项目等所需要的依赖是不一样的&#xff0c;我们可以将为每个项目创建自己的环境&#xff0c;在各自的环境中安装自己的依赖&…...

gtkmm 与 Cambalache 与 Gtk::Builder (新手向)_

文章目录 前言Cambalache检查Xml.cpp文件如何写才能显示UI首先creat获取ui里的对象显示 前言 新手刚刚使用时的笔记 Cambalache检查Xml 窗口右键inspect UI Definition切换到Xml视图, 可以全选复制粘贴到你的ui文件里, Cambalache 只能保存为.cmb工程文件, 导出也不知道导出…...

uniapp小程序端使用计算属性动态绑定style样式踩坑

踩坑点: 使用uniapp编译小程序端动态绑定复杂style使用计算属性方式&#xff0c;return必须返回json字符串格式&#xff0c;不能返回object&#xff0c;否则会不起作用。 代码总览 视图层 逻辑层&#xff08;注意这里是使用的计算属性哈&#xff09; 这里我封装成了一个个性化…...

计算机网络概念、组成、功能和分类

文章目录 概要1.怎么学习计算机网络2.概念3.功能、组成4.工作方式、功能组成5.分类 概要 概念、组成、功能和分类 1.怎么学习计算机网络 2.概念 通信设备&#xff1a;比如路由器、路由器 线路&#xff1a;将系统和通信设备两者联系的介质之类的 计算机网络是互连的、自治的的计…...

MyBatisPlus基础操作之增删改查

目录 一、基本使用 1.1 插入数据 1.2 删除操作 1.3 更新操作 二、条件构造器Wrapper 2.1 常用AbstractWrapper方法 2.1.1 示例一 2.2.2 示例二 2.2.3 示例三 2.2 常用QueryWrapper方法 2.2.1 示例一 2.2.2 示例二 2.2.3 示例三&#xff08;常用&#xff09; 2.3 常…...

视频处理学习笔记1:YUYV422、NV12和h264

最近因为工作关系在恶补视频相关知识点&#xff0c;在此做一记录便于日后复习。 以下均是个人学习经验总结&#xff0c;可能存在错误和坑&#xff0c;欢迎大佬指教。 工作中用到的是YUYV422存储格式。存储的就是裸流YUYV422格式文件。 YUYV422是两个像素点共用一个UV分量&am…...

CTFshow web(命令执行29-36)

?ceval($_GET[shy]);&shypassthru(cat flag.php); #逃逸过滤 ?cinclude%09$_GET[shy]?>&shyphp://filter/readconvert.base64-encode/resourceflag.php #文件包含 ?cinclude%0a$_GET[cmd]?>&cmdphp://filter/readconvert.base64-encode/…...

PyTorch深度学习实战(23)——从零开始实现SSD目标检测

PyTorch深度学习实战&#xff08;23&#xff09;——从零开始实现SSD目标检测 0. 前言1. SSD 目标检测模型1.1 SSD 网络架构1.2 利用不同网络层执行边界框和类别预测1.3 不同网络层中默认框的尺寸和宽高比1.4 数据准备1.5 模型训练 2. 实现 SSD 目标检测2.1 SSD300 架构2.2 Mul…...

【附代码】NumPy加速库NumExpr(大数据)

文章目录 相关文献测试电脑配置数组加减乘除数组乘方Pandas加减乘除总结 作者&#xff1a;小猪快跑 基础数学&计算数学&#xff0c;从事优化领域5年&#xff0c;主要研究方向&#xff1a;MIP求解器、整数规划、随机规划、智能优化算法 如有错误&#xff0c;欢迎指正。如有…...

4、安全开发-Python-蓝队项目流量攻击分析文件动态监控图片隐写技术

用途&#xff1a;个人学习笔记&#xff0c;有所借鉴&#xff0c;欢迎指正&#xff01; 总结&#xff1a; &#xff08;1&#xff09;使用python脚本Scapy库实现指定网卡的流量抓包分析 &#xff08;2&#xff09;使用python脚本Watchdog实现指定目录文件行为监控 &#xff08;…...

MySQL 日志管理

4.6&#xff09;日志管理 MySQL 支持丰富的日志类型&#xff0c;如下&#xff1a; 事务日志&#xff1a;transaction log 事务日志的写入类型为 "追加"&#xff0c;因此其操作为 "顺序IO"&#xff1b; 通常也被称为&#xff1a;预写式日志 write ahead…...

Python CSV文件读取和写入

本文主要为Python 实现CSV文件读取和写入操作。 CSV文件写入和读取 因为没有现成的csv文件&#xff0c;所以csv的顺序为先写入后读取。 写入 创建csv文件并把数据写入&#xff0c;有两种实现方式&#xff1a;直接插入所有行和插入单行。 示例如下&#xff1a; import csv i…...

如何使用C#调用LabVIEW算法

新建一个工程 这是必须的&#xff1b; 创建项目 项目 点击完成&#xff1b; 将项目另存为&#xff1b;方便后续的使用&#xff1b; 创建 一个测试VI 功能很简单&#xff0c;用的一个加法&#xff1b;将加数A&#xff0c;B设置为输入&#xff0c;和C设置为输出&#xff0c;…...

调用百度文心AI作画API实现中文-图像跨模态生成

作者介绍 乔冠华&#xff0c;女&#xff0c;西安工程大学电子信息学院&#xff0c;2020级硕士研究生&#xff0c;张宏伟人工智能课题组。 研究方向&#xff1a;机器视觉与人工智能。 电子邮件&#xff1a;1078914066qq.com 一&#xff0e;文心AI作画API介绍 1. 文心AI作画 文…...

JAVA SpringBoot中使用redis的事务

目录 一、Java语言介绍 二、SpringBoot框架介绍 三、Redis缓存介绍 四、什么是redis的事务 一、Java语言介绍 Java是一种广泛使用的高级编程语言,由Sun Microsystems公司于1995年推出。它的设计目标是要求“一次编写,到处运行”&#xff08;Write Once, Run Anywhere, WOR…...

docker部署自己的网站wordpress

目录 安装 1.创建目录 2.创建并启动mysql 3.创建并启动wordpress 使用 1.设置语言 2.设置基础信息 3.首页 安装 1.创建目录 mkdir -p /opt/wordpress/{db,data} 2.创建并启动mysql docker run -d --name my_mysql --restart always -e MYSQL_ROOT_PASSWORD123456 -e …...

基于ISO13400 (DoIP) 实现车辆刷写

近年来&#xff0c;在整车研发中基于以太网实现车辆高带宽通讯无疑是人们热议的话题。无论是车内基于车载以太网来减少线束成本&#xff0c;实现ADAS、信息娱乐系统等技术&#xff0c;还是基于新的电子电气架构以及远程诊断需求来实现以太网诊断&#xff08;DoIP&#xff09;&a…...

Chrome 沙箱逃逸 -- Plaid CTF 2020 mojo

文章目录 前置知识参考文章环境搭建题目环境调试环境 题目分析附件分析漏洞分析OOBUAF 漏洞利用总结 前置知识 Mojo & Services 简介 chromium mojo 快速入门 Mojo docs Intro to Mojo & Services 译文&#xff1a;利用Mojo IPC的UAF漏洞实现Chrome浏览器沙箱逃逸原文…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

【入坑系列】TiDB 强制索引在不同库下不生效问题

文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...

深入理解JavaScript设计模式之单例模式

目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式&#xff08;Singleton Pattern&#…...

【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】

1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件&#xff08;System Property Definition File&#xff09;&#xff0c;用于声明和管理 Bluetooth 模块相…...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中&#xff0c;元素的定位通过 position 属性控制&#xff0c;共有 5 种定位模式&#xff1a;static&#xff08;静态定位&#xff09;、relative&#xff08;相对定位&#xff09;、absolute&#xff08;绝对定位&#xff09;、fixed&#xff08;固定定位&#xff09;和…...

Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?

在大数据处理领域&#xff0c;Hive 作为 Hadoop 生态中重要的数据仓库工具&#xff0c;其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式&#xff0c;很多开发者常常陷入选择困境。本文将从底…...

Python ROS2【机器人中间件框架】 简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...

VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP

编辑-虚拟网络编辑器-更改设置 选择桥接模式&#xff0c;然后找到相应的网卡&#xff08;可以查看自己本机的网络连接&#xff09; windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置&#xff0c;选择刚才配置的桥接模式 静态ip设置&#xff1a; 我用的ubuntu24桌…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...