背景
目前正在修改的xiaoju-bot项目,使用uni-q-payload项目中的proto生成的js代码,当前正在做的内容是,监听wechaty的dong事件,根据返回的朋友圈事件,使用uni-q-payload封装好的代码序列化后放进队列,供automation项目使用。
之前xiaoju-bot使用这个uni-q-payload都是正常使用的,不会报任何错。但是这次接入了dong事件后,xiaoju-bot启动就会报错,具体错误如下:
22:17:40 INFO PuppetService stopGrpcStream()
22:17:40 INFO PuppetService stopGrpcStream() finished
22:17:40 ERR PuppetService start() rejection: 13 INTERNAL: Request message serialization failure: Cannot read properties of undefined (reading 'StartRequest')
Error: 13 INTERNAL: Request message serialization failure: Cannot read properties of undefined (reading 'StartRequest')
at Object.callErrorFromStatus (/Users/mac/Documents/juzi/gitlab/xiaoju-bot/node_modules/@grpc/grpc-js/src/call.ts:81:24)
...
22:17:40 WARN Puppet reset() exception: Error: 13 INTERNAL: Request message serialization failure: Cannot read properties of undefined (reading 'StartRequest')
22:17:40 ERR errorListener Error happened:
Error
at PuppetService.<anonymous> (/Users/mac/Documents/juzi/gitlab/xiaoju-bot/node_modules/wechaty/src/wechaty.ts:444:32)
...
22:17:40 ERR PuppetService stop() stop GRPC rejection: 13 INTERNAL: Request message serialization failure: Cannot read properties of undefined (reading 'StopRequest')
但是如果移出本次写的逻辑,xiaoju-bot可以正常使用.
import { BOT_CONFIG, log } from '../config';
import { BotDongEventType, MomentType, DongEvent } from '../model';
import { RequestPool } from '../service/request_pool_manager';
import { MomentPublish } from '../service/mq/publishHandler/moment_publish';
const PRE = 'dongListener';
export const dongListener = async (data?: string) => {
...
try {
const resInfo = JSON.parse(data);
dispatchMessage(resInfo);
} catch (e) {
log.error(PRE, `dong message dispatch error, botId: ${BOT_CONFIG.BOT_ID}, message: ${data}, error: ${e?.message}`);
}
};
function dispatchMessage (data) {
...
switch (api) {
case DongEvent.MOMENT_EVENT:
void MomentPublish.dispatchEventPayload(result);
break;
default:
...
}
}
初步定位
- 首先通过逐渐删除本次增加内容,尤其是import导入的模块,发现,只要把
uni-q-payload的相关引用去掉,xiaoju-bot能够正常运行。反之,加上就不行。(所以怀疑uni-q-payload打包有问题) - 搜索错误
13 INTERNAL: Request message serialization failure:找到了这篇文章「排障系列」记一次 Node gRPC 静态生成文件引发的问题 ,该文章描述了,2个不同模块使用了同一个npm包的不同版本(该npm里面就是proto生成的js代码),导致的上述错误,主要的原因就是2个不同版本由于proto的命名冲突导致的全局变量被覆盖然后在序列化的时候出现了该问题。
所有我根据这个线索一直查找uni-q-payload中是否有与puppt-service引用的grpc(wechaty-grpc)命令冲突的地方,然而并没有找到相关线索,此处耗费大量时间。。。
深入探索
线索一:PuppetService
这里面并没有太多有用的东西,只是顺着他找到了wechaty-grpc模块,这也是真正出错的地方
线索二:Cannot read properties of undefined (reading 'StartRequest')
该报错就来自于wechaty-grpc模块。报错入口来自于puppet-service.js的这个方法
await this.grpcClientStart();
async grpcClientStart() {
try {
const a = new wechaty_grpc_1.StartRequest();
await util_1.default.promisify(this.grpcClient.start
.bind(this.grpcClient))(new wechaty_grpc_1.StartRequest());
}
catch (error) {
...
}
}
// base_pb.js
proto.wechaty.puppet.StartRequest.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
// 这里的打印,显示proto.wechaty.puppet已经是undefined了,所以他肯定是被覆盖了。
console.log('aaaaa', proto.wechaty);
// 这个地方就是报错的地方
proto.wechaty.puppet.StartRequest.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
线索三:proto.wechaty.puppet为undefined
结合上面dong里面的关于uni-q-payload引用,来调试 proto.wechaty.puppet为什么会被覆盖。

可以看到,最开始的时候proto.wechaty.puppet是会被赋值的,然后断点会接着进入uni-q-payload的赋值

可以看到,uni-q-payload的变量与wechaty-grpc的变量是没有冲突的。
proto.wechaty.puppet.StartRequest
proto.uni_q_payload.wechaty.PostTextMomentActionPayload
到这里,其实都还是没有找到为什么会覆盖,其实最后的结果就是proto.wechaty.puppet.*的值,就是被proto.uni_q_payload.wechaty.*的值覆盖的。
因为最后还会进入uni-q-payload的index_pb.js的引用,该文件变量定义如下:

这个其实也没有什么问题,与proto.wechaty.puppet变量不相同。引起变量覆盖的是下面这一行代码:

在 exports 上挂载 proto.uni-q-payload 对象作为导出。

此时的proto.uni-q-payload的结构如上图所示。
goog.object.extend(exports, proto.uni_q_payload)会把proto.uni_q_payload的结构都挂在到proto上去,相当于了去掉了uni-q-payload。那么当执行了这个语句后,原来的proto.wechaty就被proto.uni-q-payload.wechaty覆盖了。
至此,问题找到。
解决方案
-
调整加载顺序,我把dong的引入放在puppet-service之前,在覆盖的时候wechaty-grpc还没有挂在全局变量,就不会覆盖。

但是此方案,不优雅,也不实用,容易被其他同事移动位置,然后报错。所以放弃。 -
更改uni-q-payload中关于wechaty的包名,尽量特殊一点,或者层次结构调整,应该需要automation那边项目一起改下包名,工作量还好,需要同事配合下。

以上方案,验证可行
经验
- 包名尽量唯一,考虑重复的可能
- 考虑了第一点后,还要考虑是不是会被其他方式覆盖包名