总结这次开发历程
遇山开路,遇水架桥。
微信小程序api给开发者造成了很大的局限性、甚至可以说是障碍,但我要根据自己需求使用它,就得想方设法绕过这些障碍。
微信云托管
问题
微信小程序的网络请求必须走域名且要求https且要求网站经过ICP备案...总之就是麻烦+麻烦
需要的东西可以总结下:
- 网站要有域名
- 网站要经过ICP备案
- 网站要有SSL证书
- 网站要能够使用https访问
- ...麻烦死了
解决方案
使用腾讯自家的东西呗: 微信云托管
部署确实很简单,把项目发布到github上,然后在微信云托管中选择对应仓库即可。每当仓库更新,微信云托管就会自动pull新的版本并发布。
而且微信云托管还有对应的mysql,支持内网访问,也就是说微信云托管的服务可以连接微信云托管的mysql
目前没有带Dockerfile的部署都失败了。
docker
什么是docker
一直感觉docker是个高大上的东西,用了以后,依然觉得高大上,对其实现原理并不懂。关于什么是docker,也不想在这篇文章赘述。
但使用一番后,可给出自己体会: "可以在Dockerfile中配置执行某个项目的总体环境,并且可以在Dockerfile中配置执行这个项目的流水线步骤。对于python(其他暂时不清楚),可以在requirements.txt中配置需要pip install的包以及版本。然后便可以开始执行docker指令,将该项目装在一个容器里面。这个容器中不仅有该项目的源码,还有该项目依赖运行的一切环境,且该容器不依赖也不冲突于windows中的任何环境, 像是一个抽象出来的独立空间。"
可见docker的好处就是:"方便移植项目本身以及项目当前的开发环境到其他主机中,在其他不同环境的主机中,抽象出一个独立空间(容器), 在该容器中安装一个相同的项目运行环境。"
windows10上使用docker
docker好像只能在linux上使用,所以在win10上想要用docker,要么在虚拟机中,要么双系统。
我用到了一个叫做wsl2的东西,配置的系统是kali(🤫i am used to be a hacker)
具体使用docker过程
- 项目中创建Dockerfile,放于根目录
FROM python
WORKDIR /app
ADD . /app
RUN pip install -r requirements.txt
EXPOSE 5000
CMD ["flask", "--app", "ChatGptApi", "run", "--host=0.0.0.0"]
- 项目中创建requirements.txt,放于根目录
PyMySQL~=1.0.2
requests~=2.28.1
openai~=0.25.0
Flask~=2.2.2
oss2~=2.16.0
- 将项目上传到github,然后在微信云托管中选择相应仓库部署即可。至于docker容器的创建、容器中环境的安装那就是微信云托管的事情了,它帮我们完成。
小程序前端基于base64编码传送图片到python Flask后端
需求
在微信小程序前端将用户头像上传到部署在微信云托管的flask后端,然后后端将图片上传到阿里云OSS
难点
微信小程序前端提供的api现在只能获取用户上传的头像的一个本地临时地址,如何将读取这个临时地址的图片并将其发送到后端,后端又如何接收
实现
微信小程序前端获取用户头像的模块(html):
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar" hidden="{{isRegis}}">
<image class="avatar" src="{{avatarUrl}}" mode="aspectFit"></image>
</button>
微信小程序前端获取用户头像的模块(js):
onChooseAvatar(e) {
const { avatarUrl } = e.detail
this.setData({
avatarUrl,
})
},
微信小程序前端发送图片到后端的模块(js):
// 将临时路径的图片进行base64编码:
var image_binary = wx.getFileSystemManager().readFileSync(this.data.avatarUrl, "base64")
// 提取文件名:
var index1=this.data.avatarUrl.lastIndexOf("/");
var index2=this.data.avatarUrl.length;
var name=this.data.avatarUrl.substring(index1 + 1, index2).toUpperCase();
console.log(name)
// 将编码后的图片上传到后端
wx.cloud.callContainer({
config: {
"env": app.globalData.WeChatCloudConfig
},
path: "/uploadImg",
header: {
"X-WX-SERVICE": app.globalData.WechatCloudHeader,
"content-type": "application/x-www-form-urlencoded"
},
method: "POST",
data: {
"file": image_binary,
"name": name
},
success: (resp) => {
this.setData({
remoteAvatarUrl: resp.data.avatarUrl
}),
后端接收图片的模块(python3 Flask2.x.x):
@app.route("/uploadImg", methods=['POST'])
def uploadImg():
"""
接收前端的图片字节流
:return:
"""
image_binary = base64.b64decode(request.form['file']) # 对base64进行解码还原
alioss = AliOSS()
avatar = alioss.uploadImage(image_binary, request.form['name'])
avatarUrl = "https://slamd14bot.oss-cn-hangzhou.aliyuncs.com/" + avatar
return {"avatarUrl": avatarUrl}
为什么是base64编码
我们知道在计算机中任何数据都是按ascii码存储的,而ascii码的128~255之间的值是不可见字符。而在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个Base64编码,统统变成可见字符,这样出错的可能性就大降低了。
先看看对二进制内容不进行base64编码,读取图片得到的结果:
// 将临时路径的图片不进行编码:
var image_binary = wx.getFileSystemManager().readFileSync(this.data.avatarUrl, "binary")
image_binary(下面显示的是二进制数据对应的ASCII码):
有很多的不可见字符
对二进制内容进行base64编码,读取图片得到的结果:
// 将临时路径的图片进行base64编码:
var image_binary = wx.getFileSystemManager().readFileSync(this.data.avatarUrl, "binary")
image_binary(下面显示的是二进制数据经过base64编码后的结果):
字符全部可见
我认为将图片内容经过base64编码后传输到后端,有利于后端接收图片。因为传输的都是可见字符串,后端完全可以用常规的方法来获取参数值,比如当前项目的flask,获取base64编码后的图片字符串与获取普通字符串数据都是用的一个方法: request.form['']
base64编码的另一个好处:
当不可见字节流在网络上传输时,比如说从 A 计算机传到 B 计算机,往往要经过多个路由设备,由于不同的设备(特指老的路由设备)对字节流的处理方式有一些不同,这样那些不可见字节就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个 base64 编码,统统变成可见字节,也就是 ascii 码可表示的可见字符,确保数据可靠传输。base64 的内容是有 0 ~9,a ~z,A ~Z,+,/组成,正好 64 个字符,这些字符是在 ascii 可表示的范围内,属于 95 个可见字符的一部分。
我们往往使用 json 对要发送的数据进行序列化,然而,json 无法序列化字节类型的数据。那我们如果想发送字节类型的数据(图片,音频,视频,语音等二进制数据),应该怎么办呢?
可能会想,我们可以把字节类型数据转为字符串类型数据。然而不幸的是,对于字节类型的图片,音频,视频,语音等二进制数据里面含有很多不可见字节,而这些不可见字节无法转为字符串。但是,我们可以先用 base64 把这些不可见字节数据编码为可见字节数据,然后再转为字符串类型,然后在通过 json 进行序列化,然后再编码为字节流通过网络发送出去。而对方收到数据后,要先把字节流解码为字符串,然后再反序列化,然后再转为字节类型数据,然后再使用 base64 解码还原成原始数据。
一些概念
大多数编码都是由字符串转换为二进制的过程,而Base64的编码则是从二进制转为字符串。
base64利用流程:
先用 base64 把这些不可见字节数据(指的是ASCII不可见字符)编码为可见字节数据,然后再转为字符串类型,然后再通过 json 进行序列化,然后再编码为字节流通过网络发送出去。而对方收到数据后,要先把字节流解码为字符串,然后再反序列化,然后再转为字节类型数据,然后再使用 base64 解码还原成原始数据。
命令行启动python项目,明明文件相对路径没问题,还可以跳转,却报"file not found error"
报错位置
原因
原因真是百思不得其解
网上四处搜索, 大家都在科普什么是相对路径什么是绝对路径...😇...
不知道在哪个网页看到了一个api: os.getcwd() 输出当前目录
我倒要看看当前目录,难道不是myChatGpt/utils/AliOssUploadImg.py或者myChatGpt/utils/吗?
在AliOssUploadImg.py中调用了这个os.getcwd(), 输出匪夷所思: myChatGpt/
🤯 what happend?
又不知道在哪个网页看到了一个关键词: 命令行
命令行?
命令行怎么了
然后仔细看了看console中的一段报错信息:file "../config/config.json" not found
感觉很奇怪,为什么file是"../config/config.json", 我这写的不是相对路径吗
我当前通过命令行运行的ChatGptApi.py所处的路径是/myChatGpt, 那么file "../config/config.json"应该指的是相对于ChatGptApi.py的路径吧? 至少从这个报错的语义来看是这样,难怪file not found
所以正确相对路径写法应该这样写:
bug
聊天消息换边了
bug描述:
我发送的消息从小狗那边显示出来了,然后小狗回复的消息从我这边显示出来了
交互流程图:
bug分析:
1、2、3、4、5、6步都是同步且阻塞的啊,真的想不通为什么会出现图中的那种bug
bug频率:
这个bug在正式版发布后,只出现了这一次。
待优化
长时间消息无响应
情况描述
后端长时间消息无响应会导致前端bot一直无法回复用户信息,并且用户输入框卡死,用户解决该问题的唯一方法就是后台关闭小程序重进,用户体验极差。
情况分析
- 前端:
(问题见注释)
wx.cloud.callContainer({
"config": {
"env": app.globalData.WeChatCloudConfig
},
"path": "/start",
"header": {
"X-WX-SERVICE": app.globalData.WechatCloudHeader,
"content-type": "application/x-www-form-urlencoded"
},
"method": "POST",
"data": {
"mes": mes
},
// 不知道默认的timeout是多少,微信云托管规定最长可以设置为15s
// 之前就是没写该参数用的默认的timeout,导致微信云托管部署的服务稍微响应慢一点(超过了默认timeout),微信云托管就回复给前端500,即使后面服务做出了响应,前端也再也接收不到了
// 所以在这里显式加上最长timeout
"timeout": 15000
})
- 后端
不好写呀。
wx.cloud.callContainer()的最大15s超时把问题局限住了。很多创作性问题openAi的回复时间都是超过15s的。
两个办法:
-
为刚刚通过ICP备案的域名配阿里云ip,申请https,然后wx.request()去调接口,毕竟wx.request()没有超时限制。实在麻烦,刚通过ICP备案的域名是个.cn的顶级域,还要公安部备案...无语。暂时不考虑这个办法。
-
前端仍然使用wx.cloud.callContainer(),但迭代使用。
可以总结下后端遇到的问题:
在一些开发中,可能会遇到需要长时间处理的任务,此时就需要使用异步的方式来实现,让长时间任务在后台运行,先将本次请求的响应状态返回给前端,不让前端界面「卡顿」,当异步任务处理好后,如果需要返回状态,再将状态返回。
具体来说就是15s内,openai是有可能无法完成一次请求响应的,尤其是对于一些创作性问题。我需要延长前端等待的时间,但前端由于调用的是微信云托管接口,无法维持15s以上的连接,所以我想的是前端迭代向后端发送最多三次请求,每次请求在后端限制响应时间小于14s。
也就是说,我把45s作为openai卡死的标志。
- 为什么会卡死(一直不回消息
拜微信所赐,调用wx.cloud.callContainer(),若15s内后端未给出响应,则微信云托管就帮我们做出响应: 响应码500
也就是说智能小狗在思考的时候,微信云托管就给我擅作主张把连接断了,然后当时小程序前端又没加fail回调函数,所以对这种情况未做出处理,就导致了消息框一直无法输入,小狗一直没回复,也永远回复不了了。
解决
- 前端思路:
- 后端思路:
后端多线程实现异步与redis代码:
主线程1
from concurrent.futures import ThreadPoolExecutor
import time
@app.route('/start2', methods=['POST'])
def start2():
time_start = time.time()
openid = request.form['openid']
message = request.form['mes']
cnt = int(request.form['cnt'])
print(message)
print(cnt)
if cnt == 1: # 由线程去执行与openAi交互的任务
redisHandle.initForMes(openid) # 为openid初始化redis
executor.submit(withOpenAi, message, openid)
while True:
time_now = time.time()
if time_now - time_start > 14: # 严格控制后端响应时间,避免由微信云托管造成的500错误(15s超时)
if cnt == 3: # 前端第三次请求的14s快到了openai仍然没响应,认定openai宕机
return {
"data": {
"respCode": "500",
"message": "智能小狗开小差了,该次提问机会不消耗,可尝试重新提问。"
}
}
if cnt ==1 or cnt ==2: # 前端第一二次请求的14s快到了,则需要前端重新请求一次,为了避免15s超时
return {
"data": {
"respCode": "302",
"message": "需要重新向后端请求一次"
}
}
if redisHandle.redisGetMsg(openid) != '': # TODO redis中的openid对应的value是否有值
return {
"data": {
"respCode": "200",
"message": redisHandle.redisGetMsg(openid)
}
}
线程2(异步任务):
def withOpenAi(message, openid): # 在新线程中跑, session_会失效. 因为flask的session是线程隔离的
"""
处理与openai的长时间交互
:param message: 用户的问题
:param openid:
:return:
"""
data["prompt"] = message
print("咨询中...") # info "asking..."
resp = requests.post('https://api.openai.com/v1/completions', headers=headers, data=json.dumps(data)).text
dic = json.loads(resp)
print(dic)
dic['choices'][0]['text'] = strHandle(dic['choices'][0]['text'])
redisHandle.redisSave(openid, dic['choices'][0]['text'])
redis部分:
import redis
pool = redis.ConnectionPool(host='阿里云ip', port=6379, password="嘎嘎嘎", decode_responses=True)
r = redis.Redis(connection_pool=pool)
def initForMes(openid):
"""
初始化映射关系: openid<->''
"""
r.set(openid, '')
def redisSave(openid, mes):
"""
:param openid:
:param mes: openAi的答复
:return:
"""
r.set(openid, mes)
def redisGetMsg(openid):
"""
:param openid:
:return: openAi的答复
"""
return r.get(openid)
Flask session线程隔离
其实一度想用session来充当存储openAi的回复的中间件,但是发现始终无法成功。无论是在主线程开新线程执行异步任务时将session作为参数传递给异步任务,还是在异步任务中直接使用全局的session,都一律失败。
想到了原来Flask的session是线程隔离的。
想到web服务器最好是无状态,又想到需要跨线程读取异步写入的数据,并且对数据CRUD,便用到redis
感谢redis😘