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

飞书第三方ISV服务商应用开发及上架教程

第一章 服务商入驻

1、使用管理员登录服务商管理后台

飞书ISV服务商注册地址应用敏捷开发,服务高效入驻。飞书开放平台致力于以先进的协同办公理念和产品助力企业成长,帮助企业打造愉悦高效的专属办公平台。https://open.feishu.cn/2、打开网站后,直接把页面向下拉取,找到成为ISV服务商【成为飞书 ISV,提供多元应用服务,与先进企业合作共赢】。

 3、然后点击进去,注册,填写需要的信息。注意这里一定要有管理员权限去操作,不要使用子管理员或普通权限。

4、企也认证的话,需要企业法人用自己的帐号登录,然后去填写才可以,其余的人是无法提交信息的。这里要注意,避免花时间填写完了却无法提交。

飞书企业认证网址应用敏捷开发,服务高效入驻。飞书开放平台致力于以先进的协同办公理念和产品助力企业成长,帮助企业打造愉悦高效的专属办公平台。https://open.feishu.cn/isv/manage/info

 

第二章 应用配置

1、登录后,直接创建创建网页应用,如下图。

飞书创建应用登录地址应用敏捷开发,服务高效入驻。飞书开放平台致力于以先进的协同办公理念和产品助力企业成长,帮助企业打造愉悦高效的专属办公平台。https://open.feishu.cn/应用列表应用敏捷开发,服务高效入驻。飞书开放平台致力于以先进的协同办公理念和产品助力企业成长,帮助企业打造愉悦高效的专属办公平台。https://open.feishu.cn/app2、登录到应用列表后,点击【创建应用商店应用】,这里的应用商店应用就是企微中的【第三方服务商应用】,也就是钉钉中的【ISV服务商应用】,不过在飞书中叫做【ISV应用商店应用】。

 3、创建应用的时候,选择【应用商店应用】,这就是ISV服务商应用。输入应用名称,应用描述。这里需要注意,如果企业没有认证成为ISV服务商,这里的应用商店应用按钮是灰色的,无法点击,直接点击右边的成为ISV服务商链接。

 4、进入到应用详情中,打开【成员管理】,选择可见的开发者。

5、这里比较重要,填写【网页】配置项。我们先打开【网页】的开关,然后输入桌面端主页的网址,这里需要填写encode之后的网址,而不是自己的明文网址,下面的地址大家可以参照一下。

这里大家一定要写对自己的app_id,不同的环境的app_id是不一样的。

// 例如你的地址是http://my.feishu.com
// 那么你的地址就是下面的样子
// 其中%3A=英文的冒号
// %2F=英文的斜杠
http://open.feishu.cn/open-apis/authen/v1/index?app_id=cli_a38273c9f2e1100d&redirect_uri=http%3A%2F%2Fmy.feishu.com%2F&state=RANDOMSTATE

 6、配置机器人,如果自己需要和用户交互,那么需要配置消息卡片请求的网址,否则只需要开启机器人就可以发送消息了。我们这里的DEMO填写了地址,是为了监听用户发送的消息,例如,用户在工作台打开,ISV应用,然后输入【上海天气】,这时候,飞书的服务台会把消息推送到下面的请求网址,也就是常规的callback地址,这时候我们接收到了用户的信息,就可以启用我们的AI机器人,将上海的天气发送给用户,这里的发送消息是通过服务商应用发送的。

 7、其中的【小程序】,【扩展】,【小组件】,【移动应用登录】这些功能暂时不用就不需要配置了。

8、然后配置【安全设置】,这个很重要,这里的重定向URL和上面的【桌面端网页】配置的地址必须要一致,不然会提示非法请求,无法跳转到指定的地址。其中和企微的区别是企微的重定向地址是需要encode的,而飞书是不需要,飞书需要encode的是网页端地址和移动端地址。这个要注意。

// 例如重定向URL为http://www.baidu.com/
// 那么,配置的桌面端网页的地址也必须是http://www.baidu.com/
// 注意的是,这个地址结尾多了一个斜杠,两边都必须同时有斜杠,不能一个有一个没有,那么也会导致无法访问的。
// 网页地址DEMO
http://open.feishu.cn/open-apis/authen/v1/index?app_id=cli_a38286c9f2e1485d&redirect_uri=http%3A%2F%2Fmy.feishu.com%2F&state=RANDOMSTATE
// 重定向URLDEMO
http://my.feishu.com/

 9、然后飞书就会跳转到重定向的URL,并且带上code参数,例如下面的地址是飞书服务器跳转过来的地址。

http://my.feishu.com/?code=e73va06d9f1e402dad46f51319c21154&state=RANDOMSTATE

 10、IP白名单设置,如果开发阶段建议先不要设置白名单,这样可以直接在开发环境上用浏览器进行访问,直接打开浏览器,在地址栏输入自己配置的桌面端地址,如果配置正确,就会出现授权登录的页面。这个还是比钉钉和企微要人性化一点。

http://open.feishu.cn/open-apis/authen/v1/index?app_id=cli_a38286c9f2e1485d&redirect_uri=http%3A%2F%2Fmy.feishu.com%2F&state=RANDOMSTATE

 11、然后就是重要的配置权限功能。以下的这些权限必须都获取。直接在权力配置中输入下面的权限就会出现,然后点击【开通权限】,测试环境是直接开通就行。这里有个功能点,所有的配置修改后,需要点击【版本管理与发布】,然后点击列表中的版本或者新增版本,然后点击编辑,直接保存就行,这里的保存相当于刷新的功能,然后新加入的权限和配置就会生效。这里发布的时候必须选择测试版本。

必须获取
contact:user.base:readonly
im:chat
im:message
im:message:readonly
im:message.group_at_msg
im:message.group_at_msg:readonly
im:message.p2p_msg
im:message.p2p_msg:readonly
im:message:send_as_bot
im:message:send_multi_users
im:resource
tenant:tenant:readonly

 

第三章 应用开发

1、飞书的比较简单,pom文件中不需要添加其余的jar。

2、解密工具类【FeishuDecrypt.java】。

package cn.renkai721.util;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;


public class FeishuDecrypt {
    private byte[] keyBs;

    public FeishuDecrypt(String key) {
        MessageDigest digest = null;
        try {
            digest = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            // won't happen
        }
        keyBs = digest.digest(key.getBytes(StandardCharsets.UTF_8));
    }

    public String decrypt(String base64) throws Exception {
        byte[] decode = Base64.getDecoder().decode(base64);
        Cipher cipher = Cipher.getInstance("AES/CBC/NOPADDING");
        byte[] iv = new byte[16];
        System.arraycopy(decode, 0, iv, 0, 16);
        byte[] data = new byte[decode.length - 16];
        System.arraycopy(decode, 16, data, 0, data.length);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBs, "AES"), new IvParameterSpec(iv));
        byte[] r = cipher.doFinal(data);
        if (r.length > 0) {
            int p = r.length - 1;
            for (; p >= 0 && r[p] <= 16; p--) {
            }
            if (p != r.length - 1) {
                byte[] rr = new byte[p + 1];
                System.arraycopy(r, 0, rr, 0, p + 1);
                r = rr;
            }
        }
        return new String(r, StandardCharsets.UTF_8);
    }

    public static void main(String[] args) throws Exception {
        FeishuDecrypt d = new FeishuDecrypt("test key");
        System.out.println(d.decrypt("P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk="));
    }


}

3、后台接口的callback接口。

package cn.renkai721.controller;

import cn.renkai721.bean.*;
import cn.renkai721.configuration.QywxProperties;
import cn.renkai721.service.*;
import cn.renkai721.util.*;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;


@EnableAsync
@RestController
@RequestMapping("/d3f")
@Slf4j
public class D3fController {
    @Resource
    private RedissonClient redissonClient;
    @Autowired
    private D3fService d3fService;


    @PostMapping(value = "/feiShuCallback")
    public Object d3fPost(
            @RequestBody(required = false) JSONObject body) throws Exception {
        log.info("接收d3f post请求:body={}",body);
        Map<String, String> resultMap = new HashMap();
        String encrypt = body.getString("encrypt");
        if(StringUtils.isEmpty(encrypt)){String type = body.getString("type");
            String challenge = body.getString("challenge");
            if("url_verification".equalsIgnoreCase(type)){
                log.info("机器人,消息卡片请求网址验证url_verification");
                resultMap.put("challenge", challenge);
                return resultMap;
            }
        }
        try{
            FeishuDecrypt d = new FeishuDecrypt(MsgUtil.val("encryptKey"));
            String message = d.decrypt(encrypt);
            log.info("message={}",message);
            FeiShuCallbackBean messageObj = JSON.parseObject(message,FeiShuCallbackBean.class);
            resultMap.put("challenge", messageObj.getChallenge());

            if("url_verification".equals(messageObj.getType())){
                log.info("验回调URL有效性");
                return resultMap;
            }else if("app_ticket".equals(messageObj.getEvent().getType())){
                // 对于应用商店应用,开放平台会每隔1小时推送一次 app_ticket
                RBucket<String> idBucket = redissonClient.getBucket(QywxProperties.app_ticket);
                idBucket.set(messageObj.getEvent().getApp_ticket());
                log.info("app_ticket={}",idBucket.get());
                d3fService.get_app_access_token();
            }else if("app_uninstalled".equals(messageObj.getEvent().getType())){
                log.info("应用卸载");
                log.info("app_uninstalled AuthCorpId={}",messageObj.getEvent().getTenant_key());
            }else if("app_open".equals(messageObj.getEvent().getType())){
                log.info("首次启用应用");
                String tenant_access_token = d3fService.get_tenant_access_token(messageObj.getEvent().getTenant_key());
                FeiShuTenantBean feiShuTenantBean = d3fService.get_tenant_info(tenant_access_token);
                String corpId = messageObj.getEvent().getTenant_key();
                String open_userid = messageObj.getEvent().getInstaller().getUnion_id();
                String enterpriseName = feiShuTenantBean.getTenant().getName();
            }

            if(message.indexOf("im.message.receive_v1") != -1){
                FeiShuCallbackV2Bean messageObjV2 = JSON.parseObject(message,FeiShuCallbackV2Bean.class);
                log.info("接收到消息,messageObjV2={}",messageObjV2);
                String corpId = messageObjV2.getHeader().getTenant_key();
//                receive_id_type_union_id = "union_id";
//                receive_id_type_open_id = "open_id";
//                receive_id_type_user_id = "user_id";
//                receive_id_type_chat_id = "chat_id";
//                飞书返回的用户ID很多,建议从头到尾都使用一个,不要换着使用
                String union_id = messageObjV2.getEvent().getSender().getSender_id().getUnion_id();
                d3fService.sendD3fTextMsg(corpId,MyConstants.receive_id_type_union_id,union_id,"暂未开启聊天功能。");
            }
        }catch (Exception e){
            resultMap.put("challenge", "error");
            e.printStackTrace();
            throw e;
        }
        return resultMap;
    }



}

4、d3fService.java中的方法。


public String get_app_access_token(){
    RBucket<String> idBucket = redissonClient.getBucket(QywxProperties.app_access_token);
    String app_access_token = idBucket.get();
    log.info("get_app_access_token={}",app_access_token);
    if(StringUtils.isEmpty(app_access_token)){
        String app_ticket = this.get_app_ticket();
        // token 有效期为 2 小时,在此期间调用该接口 token 不会改变。
        // 当 token 有效期小于 30 分的时候,再次请求获取 token 的时候,会生成一个新的 token
        // 与此同时老的 token 依然有效。
        String url1= "https://open.feishu.cn/open-apis/auth/v3/app_access_token";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Map map = new HashMap<String, Object>();
        map.put("app_id",MsgUtil.val("appId"));
        map.put("app_secret",MsgUtil.val("appSecret"));
        map.put("app_ticket",app_ticket);
        HttpEntity httpEntity = new HttpEntity(map,headers);
        try {
            ResponseEntity<Object> postForEntity = restTemplate.postForEntity(url1, httpEntity, Object.class);
            String postData1 = JSON.toJSONString(postForEntity.getBody());
            log.info("get_app_access_token postData1={}",postData1);
            String code = JSON.parseObject(postData1).getString("code");
            if ("0".equals(code)) {
                app_access_token = JSON.parseObject(postData1).getString("app_access_token");
                String expires_in = JSON.parseObject(postData1).getString("expire");
                if(!StringUtils.isEmpty(expires_in)){
                    idBucket.set(app_access_token,Integer.parseInt(expires_in), TimeUnit.SECONDS);
                }else{
                    log.error("get_app_access_token is error");
                }
            }else{
                log.error("get_app_access_token error");
            }
        }catch (Exception e){
            log.error("调用"+url1+" 失败。 e={}",e);
            e.printStackTrace();
        }
    }
    return app_access_token;
}




public String get_tenant_access_token(String tenant_key){
    String tenant_access_token = "";
    String app_access_token = this.get_app_access_token();
    String url1= "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token";
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    Map map = new HashMap<String, Object>();
    map.put("app_access_token",app_access_token);
    map.put("tenant_key",tenant_key);
    HttpEntity httpEntity = new HttpEntity(map,headers);
    try {
        ResponseEntity<Object> postForEntity = restTemplate.postForEntity(url1, httpEntity, Object.class);
        String postData1 = JSON.toJSONString(postForEntity.getBody());
        log.info("get_tenant_access_token postData1={}",postData1);
        String code = JSON.parseObject(postData1).getString("code");
        if ("0".equals(code)) {
            tenant_access_token = JSON.parseObject(postData1).getString("tenant_access_token");
        }else{
            log.error("get_tenant_access_token error");
        }
    }catch (Exception e){
        log.error("调用"+url1+" 失败。 e={}",e);
        e.printStackTrace();
    }
    return tenant_access_token;
}




public FeiShuTenantBean get_tenant_info(String tenant_access_token){
    log.info("get_tenant_info tenant_access_token={}",tenant_access_token);
    String url1= "https://open.feishu.cn/open-apis/tenant/v2/tenant/query";
    FeiShuTenantBean feiShuTenantBean = null;
    try {
        String postData1 = HttpUtil.sendGetByFeishu(url1,tenant_access_token);
        log.info("get_tenant_info postData1={}",postData1);
        String postResultCode = JSON.parseObject(postData1).getString("code");
        if ("0".equals(postResultCode)) {
            String userData = JSON.parseObject(postData1).getString("data");
            feiShuTenantBean = JSON.parseObject(userData,FeiShuTenantBean.class);
        }else{
            log.error("获取飞书企业信息失败");
        }
    }catch (Exception e){
        log.error("调用"+url1+" 失败。 e={}",e);
        e.printStackTrace();
        log.error("获取飞书企业信息失败");
    }
    return feiShuTenantBean;
}



public void sendD3fTextMsg(String corpId, String toUserType, String toUser, String message){
    // toUserType消息接收者id类型 open_id/user_id/union_id/email/chat_id
    log.info("sendD3fTextMsg corpId={},toUser={},message={}"
            ,corpId,toUser,message);
    String tenant_access_token = this.get_tenant_access_token(corpId);
    String url1 = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type="+toUserType;
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.set("Authorization","Bearer "+tenant_access_token);
    Map messageMap = new HashMap<String, Object>();
    messageMap.put("msg_type","text");
    messageMap.put("receive_id",toUser);
    Map textMessageMap = new HashMap<String, Object>();
    textMessageMap.put("text",message);
    messageMap.put("content",JSON.toJSONString(textMessageMap, SerializerFeature.WriteMapNullValue));
    HttpEntity httpEntity = new HttpEntity(messageMap,headers);
    //log.info("sendD3fTextMsg url={},HttpEntity={}",url1,httpEntity);
    ResponseEntity<Object> postForEntity = restTemplate.postForEntity(url1, httpEntity, Object.class);
    log.info("sendD3fTextMsg postForEntity={}",postForEntity);
}



public void sendTryFreeAccount(String corpId, String toUser,String toUserType){
    String title = "申请试用";
    String describe = "轻松选择适合您的自动化版本";
    String url = "https://naturobot.com/freetrial/";
    // imageKey需要自己用postman调用上传文件的接口,然后上传文件成功后,把返回的imageKey记录下来一直用
    // 不知道飞书为什么发送一个外网图片一定要先上传到自己的服务器。
    String imageKey = "img_v2_73501c09-031b-41ab-b8c0-4ab5dbe04c2g";
    this.sendD3fPostMsg(corpId,toUser,toUserType,title,describe,url,imageKey);
}



public void sendD3fPostMsg(String corpId, String toUser,String toUserType, String title,String describe, String url,String imageKey){
    log.info("sendD3fPostMsg corpId={},toUser={},title={},describe={},url={},imageKey={}"
            ,corpId,toUser,title,describe,url,imageKey);
    String tenant_access_token = this.get_tenant_access_token(corpId);
    String url1 = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type="+toUserType;
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.set("Authorization","Bearer "+tenant_access_token);
    Map messageMap = new HashMap<String, Object>();
    messageMap.put("msg_type","post");
    messageMap.put("receive_id",toUser);
    StringBuffer html = new StringBuffer();
    html.append("{\"zh_cn\":{\"title\":\""+title+"\",\"content\":[");
    html.append("[{\"tag\":\"a\",\"href\":\""+url+"\",\"text\":\""+describe+"\"}],");
    html.append("[{\"tag\":\"img\",\"image_key\":\""+imageKey+"\"}]]}}");
    messageMap.put("content",html.toString());
    HttpEntity httpEntity = new HttpEntity(messageMap,headers);
    // log.info("sendD3fTextMsg url={},HttpEntity={}",url1,httpEntity);
    ResponseEntity<Object> postForEntity = restTemplate.postForEntity(url1, httpEntity, Object.class);
    log.info("sendD3fTextMsg postForEntity={}",postForEntity);
}

5、飞书所有的代码开发到此就结束了。至于代码中用到的bean,大家自行按照官网的消息解析。

第四章 官网需要的API地址

1、应用商店发布指南,必须看。

应用上架开发指南-必须看飞书开发文档中包含丰富多样的开发指南、教程和示例,让开发者获得愉悦、高效的应用开发体验。https://open.feishu.cn/document/uMzNwEjLzcDMx4yM3ATM/ugzNwEjL4cDMx4CO3ATM2、API调试台-是可视化在线API调用工具。

API调试台-是可视化在线API调用工具飞书开发文档中包含丰富多样的开发指南、教程和示例,让开发者获得愉悦、高效的应用开发体验。https://open.feishu.cn/api-explorer/cli_a38af0c276f8d00e?apiName=tenant_access_token&project=auth&resource=auth&version=v33、发送消息 content 说明。

发送消息 content 说明飞书开发文档中包含丰富多样的开发指南、教程和示例,让开发者获得愉悦、高效的应用开发体验。https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/im-v1/message/create_json4、图片 image的content说明。

图片 image飞书开发文档中包含丰富多样的开发指南、教程和示例,让开发者获得愉悦、高效的应用开发体验。https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/im-v1/message/create_json#7111df055、发送消息接口。

发送消息飞书开发文档中包含丰富多样的开发指南、教程和示例,让开发者获得愉悦、高效的应用开发体验。https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create6、上传图片接口。

上传图片飞书开发文档中包含丰富多样的开发指南、教程和示例,让开发者获得愉悦、高效的应用开发体验。https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/image/create7、Encrypt Key 配置示例。

Encrypt Key 配置示例飞书开发文档中包含丰富多样的开发指南、教程和示例,让开发者获得愉悦、高效的应用开发体验。https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case

第五章 发布版本 

1、每一次的配置修改后,需要点击【版本管理与发布】,然后点击列表中的版本或者新增版本,然后点击编辑,直接保存就行。

2、这里的保存相当于刷新的功能,然后新加入的权限和配置就会生效。这里发布的时候必须选择测试版本。选择测试版本只有第一次才需要设置。

3、保存点击后,直接在客户端打开应用就行。编辑中把网页下拉到底,然后点击保存。

 

第六章 上架 

1、目前还没有上架,后续上架后在继续更新该文章…… 

相关文章:

  • JavaScript 运算符和表达式(二)
  • js arr.reduce() reduce方法应用
  • Day 56 Django 连接数据库 ORM
  • 深度学习中的激活函数有哪些?
  • Image through Atmospheric Turbulence笔记(一)
  • 遇到的一些奇怪的bug(非代码问题)与解决方法
  • 鸟哥私房菜linux就该这么学-学习记录
  • 猿创征文| Mybatis报错原因和解决方法:Invalid bound statement (not found): com.xxx.mapper.xxx
  • 算法学习-单调栈,接雨水经典题目
  • 2.Dos命令
  • 操作系统复习:进程
  • 经典/最新计算机视觉论文及代码推荐
  • python搞笑表白
  • Mycat的概述及MySQL主从复制部署安装
  • 江汉大学计算机考研资料汇总
  • 【Amaple教程】5. 插件
  • 【技术性】Search知识
  • codis proxy处理流程
  • crontab执行失败的多种原因
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • Debian下无root权限使用Python访问Oracle
  • ES6核心特性
  • MySQL主从复制读写分离及奇怪的问题
  • Solarized Scheme
  • supervisor 永不挂掉的进程 安装以及使用
  • Tornado学习笔记(1)
  • vue和cordova项目整合打包,并实现vue调用android的相机的demo
  • 基于axios的vue插件,让http请求更简单
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 通过获取异步加载JS文件进度实现一个canvas环形loading图
  • 一些css基础学习笔记
  • 智能网联汽车信息安全
  • 主流的CSS水平和垂直居中技术大全
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • ​软考-高级-信息系统项目管理师教程 第四版【第23章-组织通用管理-思维导图】​
  • #QT(智能家居界面-界面切换)
  • (10)工业界推荐系统-小红书推荐场景及内部实践【排序模型的特征】
  • (pytorch进阶之路)扩散概率模型
  • (二)c52学习之旅-简单了解单片机
  • (二)springcloud实战之config配置中心
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (转)http协议
  • ***微信公众号支付+微信H5支付+微信扫码支付+小程序支付+APP微信支付解决方案总结...
  • .NET 5种线程安全集合
  • .NET 中让 Task 支持带超时的异步等待
  • .netcore 获取appsettings
  • @Bean有哪些属性
  • [ IO.File ] FileSystemWatcher
  • [ Linux ] git工具的基本使用(仓库的构建,提交)
  • []error LNK2001: unresolved external symbol _m
  • [2021]Zookeeper getAcl命令未授权访问漏洞概述与解决
  • [3D游戏开发实践] Cocos Cyberpunk 源码解读-高中低端机性能适配策略
  • [AIGC 大数据基础]hive浅谈
  • [bzoj1901]: Zju2112 Dynamic Rankings
  • [C#]C# OpenVINO部署yolov8图像分类模型