Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解)

马肤

温馨提示:这篇文章已超过449天没有更新,请注意相关的内容是否还可用!

摘要:本文详细介绍了Android系统中添加系统服务的完整流程,包括SystemService的创建、注册、启动和初始化等各个环节。本文旨在提供最全面的指南,帮助开发者了解和掌握在Android系统中添加系统服务的方法和步骤。通过遵循本文的指导,开发者可以高效地添加系统服务,优化系统性能,提升用户体验。

目录

  • 前言
  • 一、编写AIDL文件
  • 二、编写Manager类
  • 三、编写系统服务
  • 四、注册系统服务
  • 五、注册Manager
  • 六、Selinux权限解决
    • 6.1 注册系统服务
    • 6.2 注册Manager
    • 七、App调用
    • 八、SDK接口限制
    • 九、系统服务的JNI部分代码

      前言

      系统服务是Android中非常重要的一部分, 像ActivityManagerService, PackageManagerSersvice, WindowManagerService, 这些系统服务都是Framework层的关键服务, 本篇文章主要讲一下如何基于Android源码添加一个系统服务的完整流程, 除了添加基本系统服务, 其中还包含添加App通过AIDL调用的演示Demo, 包含App调用服务端, 也包含服务端回调App, 也就是完成一个简单的binder双向通信。

      通过本章,以下知识应该了解和掌握:

      aidl的使用

      系统服务的注册与使用

      初识Selinux

      注: 测试代码基于Android 10, 其他Android版本大同小异.

      一、编写AIDL文件

      添加服务首先是编写AIDL文件, AIDL文件路径如下:

      frameworks/base/core/java/com/android/henryservice

      henryInterface.aidl 内容如下:

      // henryInterface.aidl
      package com.android.henryservice;
      // Declare any non-default types here with import statements
      import com.android.henryservice.Callback;
      interface henryInterface {
           void registerCallback(Callback callback);
           void unregisterCallback(Callback callback);
           void sendMessage(int type, String value);
      }
      

      Callback.aidl 内容如下

      // Callback.aidl
      package com.android.henryservice;
      // Declare any non-default types here with import statements
      interface Callback {
       oneway void onMessageReceived(int type, String value);
      }
      

      AIDL文件编写, 教程很多, 就不详细说明了, 需要注意的是, 由于要实现回调功能, 所以必须写一个回调接口 Callback , 另外AIDL文件中 oneway 关键字表明调用此函数不会阻塞当前线程, 调用端调用此函数会立即返回, 接收端收到函数调用是在Binder线程池中的某个线程中. 可以根据实际项目需求选择是否需要加 oneway 关键字.

      AIDL只支持传输基本java类型数据, 要想传递自定义类, 类需要实现 Parcelable 接口, 另外, 如果传递基本类型数组, 需要指定 in out 关键字, 比如 void test(in byte[] input, out byte[] output) , 用 in 还是 out, 只需要记住: 数组如果作为参数, 通过调用端传给被调端, 则使用 in, 如果数组只是用来接受数据, 实际数据是由被调用端来填充的, 则使用 out。

      文件写完后, 添加到编译文件Android.bp中

      frameworks/base/Android.bp

      java_defaults {
          name: "framework-defaults",
          installable: true,
          srcs: [
              // From build/make/core/pathmap.mk FRAMEWORK_BASE_SUBDIRS
              "core/java/**/*.java",
              "graphics/java/**/*.java",
              "location/java/**/*.java",
              "lowpan/java/**/*.java",
              "media/java/**/*.java",
              "media/mca/effect/java/**/*.java",
              "media/mca/filterfw/java/**/*.java",
              "media/mca/filterpacks/java/**/*.java",
              "drm/java/**/*.java",
              "opengl/java/**/*.java",
              "sax/java/**/*.java",
              "telecomm/java/**/*.java",
              "telephony/java/**/*.java",
              "wifi/java/**/*.java",
              "keystore/java/**/*.java",
              "rs/java/**/*.java",
              ":framework-javastream-protos",
              "core/java/com/android/henryservice/henryInterface.aidl",
              "core/java/com/android/henryservice/Callback.aidl",
              "core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl",
              //省略代码
      

      编译代码, 编译前需执行 make update-api, 更新接口, 否则会报如下错误:

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第1张

      Android 10还有提示,Android 后期有些版本好像提示都不给了…

      然后再编译代码,确保AIDL编写没有错误, 编译后会生成对应java文件, 服务端要实现对应接口.

      make api-stubs-docs-update-current-api
      

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第2张

      看一下主要修改了哪个文件:

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第3张


      二、编写Manager类

      可以看到Android API 中有很多Manager类, 这些类一般都是某个系统服务的客户端代理类, 其实如果不写Manager类, 只通过AIDL文件自动生成的类, 也可以完成功能, 但封装一下AIDL接口使用起来更方便, 我们测试用的Manager类为 henryManager, 代码如下:

      frameworks/base/core/java/com/android/henryservice/henryManager.java

      package com.android.henryservice;
      import android.content.Context;
      import android.os.RemoteException;
      import android.util.Log;
      public class henryManager {
          private static final String TAG = henryManager.class.getSimpleName();
          // 系统服务注册时使用的名字, 确保和已有的服务名字不冲突
          public static final String SERVICE = "henryservice";
          private final Context mContext;
          private final henryInterface mService;
          public henryManager(Context context, henryInterface service) {
              mContext = context;
              mService = service;
              Log.d(TAG, "henryManager init");
          }
          public void register(Callback callback) {
              try {
                  mService.registerCallback(callback);
              } catch (RemoteException e) {
                  Log.w(TAG, "remote exception happen");
                  e.printStackTrace();
              }
          }
          public void unregister(Callback callback) {
              try {
                  mService.unregisterCallback(callback);
              } catch (RemoteException e) {
                  Log.w(TAG, "remote exception happen");
                  e.printStackTrace();
              }
          }
          /**
           * Send event to SystemEventService.
           */
          public void sendEvent(int type, String value) {
              try {
                  mService.sendMessage(type, value);
              } catch (RemoteException e) {
                  Log.w(TAG, "remote exception happen");
                  e.printStackTrace();
              }
          }
      }
      

      代码很简单, 就封装了下AIDL接口, 定义了系统服务注册时用的名字.

      public henryManager(Context context, henryInterface service)
      

      构造函数中的 henryInterface 参数在后面注册Manager时候会通过Binder相关接口获取.

      编译代码, 确保没有错误, 下面编写系统服务.

      三、编写系统服务

      路径以及代码如下:

      frameworks/base/services/core/java/com/android/server/henryservice/henryservice.java

      package com.android.server.henryservice;
      import android.content.Context;
      import android.os.Binder;
      import android.os.RemoteCallbackList;
      import android.os.RemoteException;
      import android.util.Log;
      import com.android.henryservice.Callback;
      import com.android.henryservice.henryInterface;
      public class henryservice extends henryInterface.Stub {
          private static final String TAG = henryservice.class.getSimpleName();
          private RemoteCallbackList mCallbackList = new RemoteCallbackList();
          private Context mContext;
          public henryservice(Context context) {
              mContext = context;
              Log.d(TAG, "henryservice init");
          }
          @Override
          public void registerCallback(Callback callback) {
              boolean result = mCallbackList.register(callback);
              Log.d(TAG, "register pid:" + Binder.getCallingPid()
                      + " uid:" + Binder.getCallingUid() + " result:" + result);
          }
          @Override
          public void unregisterCallback(Callback callback) {
              boolean result = mCallbackList.unregister(callback);
              Log.d(TAG, "unregister pid:" + Binder.getCallingPid()
                      + " uid:" + Binder.getCallingUid() + " result:" + result);
          }
          @Override
          public void sendMessage(int type, String value) throws RemoteException {
              sendEventToRemote(type, value + " remote");
          }
          public void sendEventToRemote(int type, String value) {
              int count = mCallbackList.getRegisteredCallbackCount();
              Log.d(TAG, "remote callback count:" + count);
              if (count > 0) {
                  // 注意: 遍历过程如果存在多线程操作, 需要加锁, 不然可能会抛出异常
                  final int size = mCallbackList.beginBroadcast();
                  for (int i = 0; i  
      

      服务端继承自 henryInterface.Stub, 实现对应的三个方法即可, 需要注意的是, 由于有回调功能, 所以要把注册的 Callback 加到链表里面, 这里使用了 RemoteCallbackList, 之所以不能使用普通的 List 或者 Map, 原因是, 跨进程调用, App调用 registerCallback 和 unregisterCallback 时, 即便每次传递的都是同一个 Callback 对象, 但到服务端, 经过跨进程处理后, 就会生成不同的对象, 所以不能通过直接比较是否是同一个对象来判断是不是同一个客户端对象, Android中专门用来处理跨进程调用回调的类就是RemoteCallbackList, RemoteCallbackList 还能自动处理App端异常死亡情况, 这种情况会自动移除已经注册的回调.

      RemoteCallbackList 使用非常简单, 注册和移除分别调用 register() 和 unregister() 即可, 遍历所有Callback 稍微麻烦一点, 代码参考上面的 sendEventToRemote() 方法.

      可以看到测试用的的系统服务逻辑很简单, 注册和移除 Callback 调用 RemoteCallbackList 对应方法即可, sendMessage() 方法在App端调用的基础上, 在字符串后面加上 " remote" 后回调给App, 每个方法也加了log方便理解流程, 服务端代码就完成了。

      四、注册系统服务

      代码写好后, 要注册到SystemServer中, 所有系统服务都运行在名为 system_server 的进程中, 我们要把编写好的服务加进去, SystemServer中有很多服务, 把自己的系统服务加到最后面, 对应路径和代码如下:

      frameworks/base/services/java/com/android/server/SystemServer.java

      import com.android.server.henryservice.henryservice;
      import com.android.henryservice.henryManager;
      private void startOtherServices() {
          // 部分代码省略...
          // start SystemEventService
          try {
              ServiceManager.addService(henryManager.SERVICE,
                          new henryservice(context));
          } catch (Throwable e) {
              reportWtf("starting Henry Service", e);
          }
          // 部分代码省略...
      }
      

      通过 ServiceManager 将服务加到SystemServer中, 名字使用 henryManager.SERVICE, 后面获取服务会通过名字来获取. 此时, 如果直接编译运行, 开机后会出现如下错误:Selinux权限问题

      05-08 10:46:04.900  1429  1429 D henryservice: henryservice init
      05-08 10:46:04.901   475   475 E SELinux : avc:  denied  { add } for service=henryservice pid=1429 uid=1000 scontext=u:r:system_server:s0 tcontext=u:object_r:default_android_service:s0 tclass=service_manager permissive=0
      05-08 10:46:04.901   475   475 E ServiceManager: add_service('henryservice',85) uid=1000 - PERMISSION DENIED
      

      放到标题六去解决。

      五、注册Manager

      系统服务运行好了, 接下来就是App怎么获取的问题了, App获取系统服务, 我们也用通用接口:context.getSystemService()

      在调用 getSystemService() 之前, 需要先注册, 代码如下:

      frameworks/base/core/java/android/app/SystemServiceRegistry.java

      import com.android.henryservice.henryInterface;
      import com.android.henryservice.henryManager;
       final class SystemServiceRegistry {
                   public MediaProjectionManager createService(ContextImpl ctx) {
                       return new MediaProjectionManager(ctx);
                   }});
              registerService(henryManager.SERVICE, henryManager.class,
                      new CachedServiceFetcher() {
                  @Override
                  public henryManager createService(ContextImpl ctx) {
                  // 获取服务
                  IBinder b = ServiceManager.getService(henryManager.SERVICE);
                  // 转为 henryInterface
                  henryInterface service = henryInterface.Stub.asInterface(b);
                  return new henryManager(ctx, service);
                  }});
               registerService(Context.APPWIDGET_SERVICE, AppWidgetManager.class,
                       new CachedServiceFetcher() {
      

      注册后, 如果你在App里面通过 getSystemService(SystemEventManager.SERVICE); 获取Manager并调用接口, 会发现又会出错, 又是Selinux权限问题:

      SELinux : avc:  denied  { find } for service=henryservice pid=8189 uid=10141 scontext=u:r:untrusted_app:s0:c141,c256,c512,c768 tcontext=u:object_r:henryservice_service:s0 tclass=service_manager permissive=1
      

      放到标题六去解决。

      至此, 系统代码修改完成了, 编译系统刷机,通过adb shell setenforce 0 先将selinux设置为宽容模式:只报错但不影响使用。

      整体修改的部分如下(除去SElinux权限):

      		修改:     frameworks/base/Android.bp
              修改:     frameworks/base/api/current.txt
              修改:     frameworks/base/core/java/android/app/SystemServiceRegistry.java
              修改:     frameworks/base/services/java/com/android/server/SystemServer.java
              添加:frameworks/base/core/java/com/android/henryservice/													
      													├── Callback.aidl
      													├── henryInterface.aidl
      													└── henryManager.java
      		添加:frameworks/base/services/core/java/com/android/server/henryservice/
      																	└── henryservice.java
      

      六、Selinux权限解决

      上面说了可以通过adb shell setenforce 0 先将selinux设置为宽容模式:只报错但不影响使用

      但我们理应在源码中加入权限解决这类报错。

      6.1 注册系统服务

      首先定义类型, henryservice要和添加服务用的名字保持一致

      system/sepolicy/prebuilts/api/29.0/private/service_contexts

      wificond                                  u:object_r:wificond_service:s0
      wifiaware                                 u:object_r:wifiaware_service:s0
      wifirtt                                   u:object_r:rttmanager_service:s0
      window                                    u:object_r:window_service:s0
      henryservice                              u:object_r:henryservice_service:s0
      *                                         u:object_r:default_android_service:s0
      

      加入刚刚定义好的 henryservice_service类型, 表明它是系统服务

      system/sepolicy/prebuilts/api/29.0/private/service.te

      type statscompanion_service,        system_server_service, service_manager_type;
      ......
      type henryservice_service,          system_api_service, system_server_service, service_manager_type;
      

      此时编译,会报错提示如下:

      文件 system/sepolicy/prebuilts/api/29.0/private/service.te 和 system/sepolicy/private/service.te 不同
      文件 system/sepolicy/prebuilts/api/29.0/private/service_contexts 和 system/sepolicy/private/service_contexts 不同
      

      将修改同步至system/sepolicy/private/service.te 和 system/sepolicy/private/service_contexts 即可

      加入上面代码后, 编译刷机开机后, 服务就能正常运行了。

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第4张

      adb shell service list 检查一下:

      trinket:/ # service list|grep henry
      44      henryservice: [com.android.henryservice.henryInterface]
      

      6.2 注册Manager

      系统注册已经成功了,解决一下使用manager调用服务时报的selinux权限。

      还需要再加一下权限,修改代码如下:

      system/sepolicy/prebuilts/api/29.0/private/untrusted_app.te

      allow untrusted_app henryservice_service:service_manager find;
      

      编译报错,修改同步至 system/sepolicy/untrusted_app.te

      文件 system/sepolicy/prebuilts/api/29.0/private/untrusted_app.te 和 system/sepolicy/private/untrusted_app.te 不同
      

      七、App调用

      文件拷贝和准备:

      我们需要复制三个文件到App中, 两个AIDL文件, 一个Manager文件:

      Callback.aidl
      henryInterface.aidl
      henryManager.java
      

      .aidl文件放到App工程的aidl文件夹下面(没有这个文件夹的话要新建),

      新版Android Studio建立aidl文件需要如下设置:

      android {
          defaultConfig {
          ......
          }
          buildTypes {
      	......
          }
          compileOptions {
          ......
          }
          buildFeatures{aidl true}
      }
      

      .java文件放到java目录下,

      所有aidl文件和java文件, 在App工程中的包名需要和系统保持一致, 这三个文件App不能做任何修改, 除非系统源码中也做对应修改,最终App工程目录结构如下:

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第5张

      记得make一下

      只需要看一下MainActivity的编写即可

      package com.android.henryservice;
      import androidx.appcompat.app.AppCompatActivity;
      import android.content.Context;
      import android.os.Bundle;
      import android.os.RemoteException;
      import android.util.Log;
      import android.view.View;
      import android.widget.Button;
      public class MainActivity extends AppCompatActivity {
          henryManager eventManager;
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
              eventManager = (henryManager) getApplicationContext().getSystemService(henryManager.SERVICE);
              //获取服务
          }
          private Callback.Stub eventCallback = new Callback.Stub() {
              @Override
              public void onMessageReceived(int type, String value) throws RemoteException {
                  Log.d("henrymanager", "type:" + type + " value:" + value);
              }
          };
          public void register(View view) {
              eventManager.register(eventCallback);
          }
          public void unregister(View view) {
              eventManager.unregister(eventCallback);
          }
          public void send(View view) {
              eventManager.sendEvent(666, "henry");
          }
      }
      

      这里Android Studio可能会报 getSystemService() 参数不是Context里面的某个服务的错误, 可以直接忽略, 不影响编译.

      布局文件:

      
          
          
          
      
      

      测试一下:

      依次按下注册、发送、解绑、发送、发送

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第6张

      八、SDK接口限制

      android9以上运行时,button按下后可能会报如下错误:

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第7张

      Caused by: java.lang.NoSuchMethodError: No virtual method register(Lcom/android/henryservice/Callback;)
      V in class Lcom/android/henryservice/henryManager; 
      or its super classes (declaration of 'com.android.henryservice.henryManager' 
      appears in /system/framework/framework.jar!classes3.dex)      	at com.android.henryservice.MainActivity.register(MainActivity.java:32)
      

      报错的原因涉及到SDK接口限制的问题,在 Android 9以后 ,针对非 SDK 接口进行了限制,默认是 blacklist 的,

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第8张

      通过命令

       m out/soong/hiddenapi/hiddenapi-flags.csv
      

      可以生成包含所有非 SDK 接口及其对应的名单

      vim out/soong/hiddenapi/hiddenapi-flags.csv
      

      打开改文件,发现自己添加服务的部分接口都默认在blacklist清单里,导致自己上层调用报错,

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第9张Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第10张

      解决方法:

      ①临时设置为可以调用 adb shell settings put global hidden_api_policy 1

      ②编辑 greylist,将自己包名加入 greylist 中。

      vim frameworks/base/config/hiddenapi-greylist-packages.txt 
      

      最后一行增加,设置在清单里,之后可以正常调用该包下的接口。

      Android 添加系统服务的完整流程SystemService(史上最全),Android SystemService添加系统服务的完整流程(史上最全详解) 第11张

      m out/soong/hiddenapi/hiddenapi-flags.csv 再次编译发现自己包名的接口再也没有blacklist的了。

      至于白名单、灰名单、黑名单具体概念,之后再详细研究吧。

      九、系统服务的JNI部分代码

      一般添加系统服务, 可能是为了调用驱动里面的代码, 所以一般要用JNI部分代码, 这里简单说下系统服务中已有的JNI代码, 可以直接在这基础上增加我们的功能.

      JNI部分代码位置为:

      frameworks/base/services/core/jni/
      

      编译对应bp为:

      frameworks/base/services/Android.bp
      frameworks/base/services/core/jni/Android.bp
      

      frameworks/base/services/Android.bp:

      // merge all required services into one jar
      // ============================================================
      java_library {
          name: "services",
          installable: true,
          dex_preopt: {
              app_image: true,
              profile: "art-profile",
          },
          srcs: [
              "java/**/*.java",
          ],
          // The convention is to name each service module 'services.$(module_name)'
          static_libs: [
              "services.core",
              "services.accessibility",
              "services.appprediction",
              "services.appwidget",
              "services.autofill",
              "services.backup",
              "services.companion",
              "services.contentcapture",
              "services.contentsuggestions",
              "services.coverage",
              "services.devicepolicy",
              "services.midi",
              "services.net",
              "services.print",
              "services.restrictions",
              "services.startop",
              "services.systemcaptions",
              "services.usage",
              "services.usb",
              "services.voiceinteraction",
              "android.hidl.base-V1.0-java",
          ],
          libs: [
              "android.hidl.manager-V1.0-java",
          ],
          // Uncomment to enable output of certain warnings (deprecated, unchecked)
          //javacflags: ["-Xlint"],
      }
      // native library
      // =============================================================
      cc_library_shared {
          name: "libandroid_servers",
          defaults: ["libservices.core-libs"],
          whole_static_libs: ["libservices.core"],
      }
      

      cc_library_shared模块定义了一个共享的C/C++库模块,名称为libandroid_servers,默认依赖于libservices.core-libs,并且整体依赖于libservices.core。

      此部分代码直接编译为 libandroid_servers 动态库, 在SystemServer进行加载:

      frameworks/base/services/java/com/android/server/SystemServer.java

                  // Prepare the main looper thread (this thread).
                  android.os.Process.setThreadPriority(
                          android.os.Process.THREAD_PRIORITY_FOREGROUND);
                  android.os.Process.setCanSelfBackground(false);
                  Looper.prepareMainLooper();
                  Looper.getMainLooper().setSlowLogThresholdMs(
                          SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS);
                  // Initialize native services.
                  System.loadLibrary("android_servers");
      

      如果需要添加JNI部分代码, 直接在 frameworks/base/services/core/jni/ 目录下增加对应文件,

      在frameworks/base/services/core/jni/Android.bp中加入新增文件进行编译即可.

      同时按照已有文件中JNI函数注册方式, 写好对应注册方法, 统一在frameworks/base/services/core/jni/onload.cpp 中动态注册函数.

      参考链接:

      Android11 —— 自定义添加一个System Services

      Android java.lang.NoSuchMethodError:


0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码