gat1400 digest摘要验证 保活 GOFRAME NSQ gorm etcd token 订阅通知 延迟队列
备忘
人脸结构化数据: function/intelli/face
平台数据库 192.168.1.204:31196 root testing对吗
有疑惑的地方
- 是什么,定义接口规范,完成了一部分注册和校时功能,待完成保活和注销。
ga1400
1400标准有4部分:
GA-T 1400.1-2017 公安视频图像信息应用系统 第1部分:通用技术要求;
GA-T 1400.2-2017 公安视频图像信息应用系统 第2部分:应用平台技术要求;
GA-T 1400.3-2017 公安视频图像信息应用系统 第3部分:数据库技术要求;
GA-T 1400.4-2017 公安视频图像信息应用系统 第4部分 接口协议要求;
1400与28181有共性的地方,比如设备编码规范、信令交互规范等,肤浅一点说,28181定义的是视频联网,1400定义的图片传输,目前几乎没有安防厂家能完全吃透1400标准,应用最多的是ipc传输图片及相关信息到后端设备/平台,以及视图库平台与平台对接等,比如人脸抓拍机传输人脸图片、人脸特征等到人脸应用平台,车辆抓拍机传输车辆图片、车牌信息到车辆卡口平台。
东方网力视频图像解析系统
GA/T 1400《公安视频图像信息应用系统》是公安部发布的公共安全行业标准,系统由应用平台、视图库、公安视频图像分析设备/系统以及在线视频信息采集设备/系统等部分组成。东方网力视频图像解析系统在GA/T 1400的标准上提供多源、多级数据的接入、存储、共享、分析,及对外标准化接口服务的能力。依托于物联网多维感知采集技术、视图智能结构化解析技术、混合云计算技术、视图大数据聚类挖掘技术等,对原始视频、图像信息进行内容解析,将获得的结构化信息和其他感知数据(RFID、GPS)相结合,面向公安视频图像类的上层业务应用, 提供统一的视图资源服务支持能力。
据悉,GA/T 1400.2 《公安视频图像信息应用系统 第2部分:应用平台技术要求》目前检测中心已经开启了摸底测试,东方网力即将开始进行摸底测试,未来,东方网力将继续深耕视频图像解析系统核心技术研发与公共安全领域的应用与落地,切实解决市场需求,助力产业智能化升级。
GA/T1400《公安视频图像信息应用系统》标准说明:
GA/T 1400.3 规定了公安视频图像信息数据库的组成、存储对象、功能、性能、安全性等技术要求。
GA/T 1400.4 规定了公安视频图像信息应用系统的接口分类与协议结构、接口功能、接口资源描述、接口消息、关键消息交互流程、消息交互安全性等技术要求。
注册
需要发送json数据 String sendJson = "{"RegisterObject":{"DeviceID":"41000000005030312222"}}";
第一次请求,json数据+header信息+发送post请求
{"RegisterObject":{"DeviceID":"33010299011190000253"}}
第二次请求,将请求次数转为八位16进制 String noncecount = getHex(countList.size()); ?试了,不可以
digest摘要验证
使用 Digest auth,客户端向 API 发送第一个请求,服务器响应一些细节,包括只能使用一次的数字(现时)、领域值和401
未经授权的响应。然后,您将包含用户名和密码的加密数据数组与在第一个请求中从服务器收到的数据一起发回。服务器使用传递的数据生成加密字符串,并将其与您发送的内容进行比较,以验证您的请求。
在请求的授权选项卡中,从类型下拉列表中选择摘要式身份验证。Postman 将显示身份验证请求的两个阶段的字段 - 但是它会使用第一个请求从服务器返回的数据自动完成第二个请求的字段。为了让 Postman 自动化流程,输入用户名和密码值(或变量),这些将与第二个请求一起发送。
如果您不希望 Postman 自动提取数据,请选中该框以禁用重试请求。如果您这样做,您将需要完成高级字段并手动运行每个请求。
高级字段是可选的,Postman 会在您的请求运行时尝试自动填充它们。
- 领域:服务器在
WWW-Authenticate
响应头中指定的字符串。 - Nonce:服务器在
WWW-Authenticate
响应头中指定的唯一字符串。 - 算法:一个字符串,表示用于生成摘要和校验和的一对算法。邮递员支持
MD5
和SHA
算法。 - qop:应用于消息的保护质量。该值必须是服务器在
WWW-Authenticate
响应标头中指定的备选方案之一。 - Nonce Count:客户端在本次请求中使用 nonce 值发送的请求(包括当前请求)数量的十六进制计数。
- 客户端随机数:客户端提供的不透明引用字符串值,客户端和服务器都使用它来避免选择明文攻击,提供相互身份验证,并提供一些消息完整性保护。
- Opaque:服务器在
WWW-Authenticate
响应头中指定的一串数据,在相同的保护空间中与URIs一起使用时应保持不变。
实现RoundTripper接口更规范
- 先发送post请求,等待返回
- 响应头里面取响应码401,md5加密
- 再次请求,携带Authenticate头信息
Response execute = OkGo.post(url_register) // .headers("DeviceID", DEVICEID) // .headers(HttpHeaders.HEAD_KEY_CONNECTION,HttpHeaders.HEAD_VALUE_CONNECTION_KEEP_ALIVE) .execute(); int code = execute.code(); Log.i(HEAD,"register code >> " + code); if (code == 401){ Headers headers = execute.headers(); int size = headers.size(); Set<String> names = headers.names(); Log.i(HEAD,"register cacheResponse headers size >> " + size + " ; names size >> " + names.size()); for (String name : names) { String value = headers.get(name); Log.i(HEAD,"key = " + name + " ; value = " + value); } String authValue = headers.get(KEY_AUTHENTICATE); Log.i(HEAD,"authValue >> " + authValue); String[] split = authValue.split(","); AuthenticateBean authenticateBean = new AuthenticateBean(); for (String s : split) { Log.i(HEAD,"split v >> " + s); String[] vv = s.split("="); String key = vv[0]; String value = vv[1]; value = value.replace("\"",""); Log.i(HEAD,"split vv key >> " + key + " ; value >> " +value); if (TextUtils.equals(key.trim(),"Digest realm")){ authenticateBean.setDigest_realm(value); }else if (TextUtils.equals(key.trim(),"qop")){ authenticateBean.setQop(value); }else if (TextUtils.equals(key.trim(),"nonce")){ authenticateBean.setNonce(value); } } Log.i(HEAD,"authenticateBean to string >> " + authenticateBean); String nc = "00000001"; String cnonce = DigestUtils.generateSalt2(8); String response = DigestUtils.getResponse(username, authenticateBean.getDigest_realm(), password, authenticateBean.getNonce(), nc, cnonce, authenticateBean.getQop(), "POST","/VIID/System/Register"); String authorization = DigestUtils.getAuthorization(username, authenticateBean.getDigest_realm(), authenticateBean.getNonce(), "/VIID/System/Register", authenticateBean.getQop(), nc, cnonce, response, ""); //FPsBdyB5iEN/w4Ja74DyKw== JSONObject jsonObject = new JSONObject(); jsonObject.put("DeviceID",DEVICEID); JSONObject js = new JSONObject(); js.put("RegisterObject",jsonObject); String s = js.toString(); Log.i(HEAD,"js str >> " + s); Response execute1 = OkGo.post(url_register) .upJson(s) .headers("Authorization", authorization) // .headers(HttpHeaders.HEAD_KEY_CONNECTION, HttpHeaders.HEAD_VALUE_CONNECTION_KEEP_ALIVE) .execute(); int code1 = execute1.code(); Log.i(HEAD,"register2 code >> " + code1 );
Digest realm="me@kennethreitz.com", nonce="9205409fee3d96ef3a9090ab1364ac5e", qop="auth, auth-int", opaque="fe09c530c66582e99b0cebefb7db37bf", algorithm=MD5, stale=FALSE Authorization: Digest username="2", realm="me@kennethreitz.com", nonce="9ad1c018c7002b7e513e0f421ec3e461", uri="/digest-auth/2/2/2", algorithm=MD5, response="3038ff9503a7fb9b792ae7850a6e935c", opaque="bd85ce08f4b10fe5ffea5fc72dce218c", qop=auth, nc=00000002, cnonce="f4ae4e77421cbac8"
response: 客户端根据算法算出的摘要值
cnonce(clinetNonce): 客户端发送给服务器的随机字符串
qop: 保护质量参数,一般是auth,或auth-int,这会影响摘要的算法
nc(nonceCount):请求的次数,用于标记,计数,防止重放攻击
保活 keep-alive
https://blog.csdn.net/xiaoduanayu/article/details/78386508
1.为什么要有Connection: keep-alive?
在早期的HTTP/1.0中,每次http请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重用连接。在后来的HTTP/1.0中以及HTTP/1.1中,引入了重用连接的机制,就是在http请求头中加入Connection: keep-alive来告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流。
谷歌浏览器默认总会带上Connection: keep-alive
,另外通过httpclient使用HTTP/1.0协议去请求tomcat时,即使带上Connection: keep-alive
请求头也保持不了长连接。如果HTTP/1.1版本的http请求报文不希望使用长连接,则要在请求头中加上Connection: close
,接收到这个请求头的对端服务就会主动关闭连接。
但是http长连接会一直保持吗?肯定是不会的。一般服务端都会设置keep-alive超时时间。超过指定的时间间隔,服务端就会主动关闭连接。同时服务端还会设置一个参数叫最大请求数,比如当最大请求数是300时,只要请求次数超过300次,即使还没到超时时间,服务端也会主动关闭连接。
人脸分析
face_dev.go
GetTrackResult 获取智能分析结果
face.go
GetTrackResult 获取人脸跟踪结果
http://192.168.1.140/v1/intelli/face/analyse/search/caps
{ "Code": 200, "Message": "msg: success", "Translate": "操作成功", "Detail": "", "Data": { "Channels": [ 0, 1 ], "Region": null, "Gender": [ { "Key": 0, "Value": "未知" }, { "Key": 1, "Value": "男" }, { "Key": 2, "Value": "女" } ], "Age": [ { "Key": 1, "Value": "儿童" }, { "Key": 2, "Value": "少年" }, { "Key": 3, "Value": "青年" }, { "Key": 4, "Value": "中年" }, { "Key": 5, "Value": "老年" } ], "Hair": [ { "Key": 0, "Value": "未知" }, { "Key": 1, "Value": "秃头" }, { "Key": 2, "Value": "头发稀疏" }, { "Key": 3, "Value": "短发" }, { "Key": 4, "Value": "长发" }, { "Key": 5, "Value": "其他发型" } ], "Hat": [ { "Key": 0, "Value": "未知" }, { "Key": 1, "Value": "有" }, { "Key": 2, "Value": "无" } ], "Respirator": [ { "Key": 0, "Value": "未知" }, { "Key": 1, "Value": "有" }, { "Key": 2, "Value": "无" } ], "Glasses": [ { "Key": 0, "Value": "未知" }, { "Key": 1, "Value": "有" }, { "Key": 2, "Value": "无" } ] } }
{ "Code": 200, "Message": "msg: success", "Translate": "操作成功", "Detail": "", "Data": { "Total": 1, "Num": 1, "Info": [ { "ID": 112, "Chn": 0, "Time": 1629884310257, "Rect": { "Left": 193, "Top": 264, "Right": 229, "Bottom": 306 }, "Region": 0, "Type": 0, "Gender": 1, "Age": 3, "Hair": 3, "Hat": 2, "Respirator": 2, "Glasses": 1, "Temperature": 36.4, "TempAlarm": false, "ObjPicPath": "/tmp/mmcblk1p7/0/1629884310257.jpg", "FullPicPath": "/tmp/mmcblk1p7/0/1629884310256.jpg" } ] } }
recognition_arm.go
GetRecognitionResult 获取人脸识别结果
http patch请求
patch是对put的补充,更新部分资源,使用patch时候就不是了,默认是以x-www-form-urlencoded的contentType来发送信息,并且信息内容是放在request的body里。
redis 订阅通知
https://blog.csdn.net/weixin_44187730/article/details/95061139?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163048039516780271526478%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163048039516780271526478&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-3-95061139.first_rank_v2_pc_rank_v29&utm_term=redis+%E8%AE%A2%E9%98%85&spm=1018.2226.3001.4187
NSQ
https://www.liwenzhou.com/posts/Go/go_nsq/
分布式的消息队列(中间件)
组件
nsqd:是一个守护进程,它接收、排队并向客户端发送消息
nsqlookupd:是维护所有nsqd状态、提供服务发现的守护进程。
nsqadmin:一个实时监控集群状态、执行各种管理任务的web管理平台。
架构
nsq启动
E:\1study\nsq\nsq-1.2.1.windows-amd64.go1.16.6\bin nsqd.exe -broadcast-address=192.168.1.12 -lookupd-tcp-address=192.168.1.12:4160 nsqlookupd.exe nsqadmin.exe -lookupd-http-address=192.168.1.12:4161
etcd
etcd是使用go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。
自动建表
```xml
GORM
注意gorm查找struct名对应数据库中的表名的时候会默认把你的struct中的大写字母转换为小写并加上“s”,所以可以加上 db.SingularTable(true) 让grom转义struct名字的时候不用加上s。
package main import ( "fmt" "time" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" //这个一定要引入哦!! ) //返回datatime func dateTime() (formatTimeStr string) { NowTimeZone := time.FixedZone("CST", 8*3600) //东八区,返回时区指针 formatTimeStr = time.Now().In(NowTimeZone).Format("2006-01-02 15:04:05") //2020-04-28 23:27:50 return formatTimeStr } type Animal struct { Id uint32 Name string Password string Create_time string } type AutoGenerated struct { FaceListObject struct { FaceObject []struct { Name string `json:"Name"` Password string `json:"Password"` CreateTime string `json:"Create_time"` } `json:"FaceObject"` } `json:"FaceListObject"` } // A.19 订阅对象 Subscribe type Subscribe struct { SubscribeID string `description:"订阅标识符"` Title string `description:"订阅标题"` SubscribeDetail string `description:"订阅类别"` ResourceURI string `description:"订阅资源路径"` ApplicantName string `description:"申请人"` ApplicantOrg string `description:"申请单位"` Begint64ime string `description:"开始时间"` EndTime string `description:"结束时间"` ReceiveAddr string `description:"信息接收地址"` ReportInt64erval int `description:"信息上报间隔时间"` Reason string `description:"理由"` OperateType int `description:"操作类型"` SubscribeStatus int `description:"订阅执行状态"` SubscribeCancelOrg string `description:"订阅取消单位"` SubscribeCancelPerson string `description:"订阅取消人"` CancelTime string `description:"取消时间"` CancelReason string `description:"取消原因"` } // A.20 通知对象 Notification type Notification struct { NotificationID string `gorm:"type:varchar(255) primary key";description:"通知标识"` SubscribeID string `description:"订阅标识"` Title string `description:"订阅标题"` TriggerTime string `description:"触发时间"` InfoIDs string `description:"信息标识"` CaseObjectList string `description:"视频案事件"` Tollgate string `description:"视频卡口"` Lane string `description:"车道"` DeviceList string `description:"设备"` DeviceStatusList string `description:"设备状态"` APSObjectList string `description:"采集系统"` APSStatusObjectList string `description:"采集系统状态"` PersonObjectList string `description:"人员信息"` FaceObjectList string `description:"人脸信息"` MotorVehicleObjectList string `description:"机动车信息"` NonMotorVehicleObjectList string `description:"非机动车信息"` ThingObjectList string `description:"物品信息"` SceneObjectList string `description:"场景信息"` } func main() { db, errDb := gorm.Open("mysql", "root:123123@(127.0.0.1)/test?charset=utf8mb4&loc=Local") if errDb != nil { fmt.Println(errDb) } defer db.Close() //用完之后关闭数据库连接 db.LogMode(true) //开启sql debug 模式 // 根据结构体,自动建表 // table := Subscribe{} table2 := Notification{} // db.AutoMigrate(&table) db.AutoMigrate(&table2) // 让grom转义struct名字的时候不用加上s // db.SingularTable(true) // jsonData := []byte(`{ // "FaceListObject": { // "FaceObject": [ // { // "1Name1": "ling", // "Password": "2342342", // "Create_time": "2021-09-06 00:00:00" // } // ] // } // }`) // animalJson := AutoGenerated{} // err := json.Unmarshal(jsonData, &animalJson) // fmt.Println(err) // fmt.Println(animalJson.FaceListObject.FaceObject[0].Name) // fmt.Println() // animalInsert := Animal{ // Name: animalJson.FaceListObject.FaceObject[0].Name, // Password: animalJson.FaceListObject.FaceObject[0].Password, // Create_time: animalJson.FaceListObject.FaceObject[0].CreateTime} // // // 检查主键是否为空 // db.Create(&animalInsert) // fmt.Println(db.NewRecord(animalInsert)) // db.Table("animas") // animalSelect := &Animal{} // db.Where("name = ?", "ling12").Find(animalSelect) // j, _ := json.Marshal(animalSelect) // fmt.Println(string(j)) // fmt.Println(animalSelect.Id, animalSelect.Name, animalSelect.Password) // var animalSelect2 []Animal // db.Where("Id in (?)",[]uint{1,2,3}).Find(&animalSelect2) // fmt.Println(animalSelect2) // fmt.Println(len(animalSelect2)) // animalUpdate := Animal{Name: "ling1", Password: "1111111111", Create_time: dateTime()} // db.First(&animalUpdate) // animalUpdate.Name="update11111" // animalUpdate.Password="password" // db.Save(&animalUpdate) // animalUpdate := Animal{} // db.Model(&animalUpdate).Where("password = ?","5745765").Update("password","666666") // animalUpdate := Animal{Id: 1,} // db.Model(&animalUpdate).Updates(map[string]interface{}{"name":"jjjjjjjjj","password":"jjjjjjjj",}) // animalUpdate := Animal{Id: 2} // db.Model(&animalUpdate).Updates(Animal{Name: "eeeeee", Password: "eeeeeeee"}) // animalDel:=Animal{} // db.Delete(Animal{}, "id > 2000") } func main1() { db, errDb := gorm.Open("mysql", "root:123123@(127.0.0.1)/test?charset=utf8mb4&loc=Local") if errDb != nil { fmt.Println(errDb) } defer db.Close() //用完之后关闭数据库连接 db.Delete("id", 2941) // db.LogMode(true) //开启sql debug 模式 //SELECT * FROM `animals` WHERE (id>0 and id<999) ORDER BY id desc LIMIT 2 OFFSET 1 var owls []Animal db.Where("id>? and id<?", 0, 999).Order("id desc").Offset(2).Limit(2).Find(&owls) fmt.Println(owls) //获取条数 total := 0 db.Model(&Animal{}).Count(&total) fmt.Println(total) //插入数据1 var animal = Animal{Password: "123456", Name: "monkey", Create_time: dateTime()} db.Create(&animal) //插入数据2 结构体这种的表名称是结构体名后面+s insErr := db.Create(&Animal{Password: "123", Name: "boy", Create_time: dateTime()}).Error fmt.Println(insErr) //插入数据3 指定表名称:animals insErr3 := db.Table("animals").Create(&Animal{Name: "吴亦凡", Create_time: dateTime()}).Error fmt.Println(insErr3) animal2 := &Animal{} //获取结构体 //查询数据1 SELECT * FROM `animals` WHERE (Id=1) db.Where("Id=?", 1).Find(animal2) //结果集存放在animal这个struct里面 fmt.Println(animal2, animal2.Id, animal2.Name, animal2.Create_time) var infos []Animal //定义一个数组来接收多条结果 db.Where("Id in (?)", []uint32{1, 2, 3}).Find(&infos) fmt.Println(infos) fmt.Println(len(infos)) //结果条数 var notValue []Animal db.Where("id=?", 3).Find(¬Value) if len(notValue) == 0 { fmt.Println("没有查询到数据!") } else { fmt.Println(notValue) } //http://gorm.book.jasperxu.com/crud.html#q //https://gorm.io/zh_CN/docs/create.html }
图片、视频、文件
http://192.168.1.179/v2/picture?path=/tmp/hda21/1/1630654142023000001.jpg
GoFrame
generated: app/dao/face_objects.go
generated: app/dao/internal/face_objects.go
generated: app/model/model.go
done!
string和int类型相互转换
string转成int: int, err := strconv.Atoi(string) string转成int64: int64, err := strconv.ParseInt(string, 10, 64) int转成string: string := strconv.Itoa(int) int64转成string: string := strconv.FormatInt(int64,10)
dao代码生成
配置config.toml或yaml
# HTTP Server [server] Address = ":8199" ServerRoot = "public" ServerAgent = "gf-app" LogPath = "/tmp/log/gf-app/server" # Logger. [logger] Path = "/tmp/log/gf-app" Level = "all" Stdout = true # Template. [viewer] Path = "template" DefaultFile = "index.html" Delimiters = ["{{", "}}"] # Database. [database] link = "mysql:root:123123@tcp(192.168.1.12:3306)/test" debug = true # Database logger. [database.logger] Path = "/tmp/log/gf-app/sql" Level = "all" Stdout = true # GF-CLI工具配置 [gfcli] # 自定义DAO生成配置(默认是读取database配置) [[gfcli.gen.dao]] link = "mysql:root:123123@tcp(192.168.1.12:3306)/test" tables = "people"
gf gen dao -h /查看帮助 gf gen dao
model代码生成
gf gen model ./model -c config/config.toml -p sys_ -t sys_user 2020-04-26 23:35:31.682 [DEBU] [ 51 ms] SHOW FULL COLUMNS FROM `sys_user` generated: ./model\user\user.go generated: ./model\user\user_entity.go generated: ./model\user\user_model.go done!
JWT-token
package main import ( "fmt" "net/http" "time" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" ) //自定义一个字符串 var jwtkey = []byte("www.topg1oer.com") var str string type Claims struct { UserId uint jwt.StandardClaims } func main() { r := gin.Default() r.GET("/set", setting) r.GET("/get", getting) //监听端口默认为8080 r.Run(":8080") } //颁发token func setting(ctx *gin.Context) { expireTime := time.Now().Add(7 * 24 * time.Hour) claims := &Claims{ UserId: 2, StandardClaims: jwt.StandardClaims{ ExpiresAt: expireTime.Unix(), //过期时间 IssuedAt: time.Now().Unix(), Issuer: "127.0.0.1", // 签名颁发者 Subject: "usertoken11", //签名主题 }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // fmt.Println(token) tokenString, err := token.SignedString(jwtkey) if err != nil { fmt.Println(err) } str = tokenString ctx.JSON(200, gin.H{"token": tokenString}) } //解析token func getting(ctx *gin.Context) { tokenString := ctx.GetHeader("Authorization") //vcalidate token formate if tokenString == "" { ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"}) ctx.Abort() return } token, claims, err := ParseToken(tokenString) if err != nil || !token.Valid { ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"}) ctx.Abort() returnnvr } fmt.Println(111) fmt.Println(claims.UserId) } func ParseToken(tokenString string) (*jwt.Token, *Claims, error) { Claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, Claims, func(token *jwt.Token) (i interface{}, err error) { return jwtkey, nil }) return token, Claims, err }
订阅通知
新增订阅
10:20发送的请求
工作时间 10:30~10:50
10分钟后开始(600秒) 任务执行20分钟(1200秒) 条件:每秒循环判断data是否为空,不为空把数据发出去
10秒内
这里用的time.sleep()显然不适用于修改和删除;改用time.NewTImer(),支持Reset()
修改订阅
updateTime:=time.NewTImer(5*time.Second)
updateTime.Reset(3*time.Second)
// 修改延迟时间 if updataDelayTime > 0 { Jobtime.Reset(time.Duration(updataDelayTime) * time.Second) }
删除订阅
deleteTime:=time.NewTImer(5*time.Second)
deleteTime.Reset(0)
// 删除延迟时间 if deleteDelayTime > 0 { Jobtime.Stop() }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架