feat(weixin_ilink): 出站媒体走通(主人已批的三项)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

按官方包 @tencent-weixin/openclaw-weixin 2.4.9 源码对齐:
① 上传由 PUT 改 POST;② 上传凭据读响应头 x-encrypted-param;
③ nativeMediaItem 改为官方 *_item.media.encrypt_query_param 嵌套形状(含 aes_key,
图片优先 image_item.aeskey)。
配合前一版已修的两点(入站按条密钥、媒体失败不阻断文本),机器人现在可主动发出文件/图片。
(逐项测试、变异红行与诚实边界见该切片报告)
This commit is contained in:
AskaEth committed 2026-10-05 08:04:50 +08:00
1 parent 59a1ac4056
commit f8f2e9c54c
7 files changed
+847 -246

No files matched your search

@@ -23,15 +23,20 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// mediaSendServer 是假 iLink:getuploadurl 按 upload 作答,上传成功,sendmessage 回 ret:0。
// mediaSendServer 是假 iLink:getuploadurl 按 upload 作答,**CDN 上传按官方口径回
// 200 + `x-encrypted-param` 头**(POST),sendmessage 回 ret:0。
func mediaSendServer(t *testing.T, rec *reqRecorder, upload handlerResult) *httptest.Server {
t.Helper()
return newJSONServer(t, rec, func(req recordedReq) handlerResult {
switch {
case req.method == http.MethodPost && req.path == "/ilink/bot/getuploadurl":
return upload
case req.method == http.MethodPut:
return handlerResult{status: http.StatusOK, body: []byte("ok")}
case req.method == http.MethodPost && req.path == "/cdn/up":
return handlerResult{
status: http.StatusOK,
body: []byte("ok"),
headers: map[string]string{uploadParamHeader: downloadParamFixture},
}
case req.method == http.MethodPost && req.path == "/ilink/bot/sendmessage":
return handlerResult{body: jsonBody(map[string]any{"ret": 0})}
default:
@@ -40,6 +45,10 @@ func mediaSendServer(t *testing.T, rec *reqRecorder, upload handlerResult) *http
})
}
// downloadParamFixture 是假 CDN 回填的下载凭据(真实值由服务端生成,测试只关心它被
// 原样带进 item 的 media.encrypt_query_param)。
const downloadParamFixture = "dl+param/1="
// newLoggingSendAdapter 装配一个把日志写进 buf 的已连接适配器,用来断言失败**不静默**。
func newLoggingSendAdapter(t *testing.T, srv *httptest.Server, buf *bytes.Buffer) *Adapter {
t.Helper()
@@ -193,9 +202,9 @@ func TestGetUploadURL_RequestCarriesProtocolFields(t *testing.T) {
if err != nil {
t.Fatalf("aeskey 不是十六进制:%v", err)
}
puts := rec.find(func(q recordedReq) bool { return q.method == http.MethodPut })
if len(puts) != 0 {
t.Fatalf("上传地址缺失时不该发起上传:%d 次", len(puts))
// 上传地址缺失时**一次上传请求都不该发出**(只应有那一次 getuploadurl)。
if reqs := rec.find(func(q recordedReq) bool { return q.method == http.MethodPost }); len(reqs) != 1 {
t.Fatalf("POST 请求 = %d 次, want 1(只有 getuploadurl;地址缺失就不该上传)", len(reqs))
}
if len(rawKey) != 16 {
t.Fatalf("aeskey = %d 字节, want 16", len(rawKey))
@@ -212,8 +221,12 @@ func TestOutboundMedia_UsesUploadFullURL(t *testing.T) {
switch {
case req.method == http.MethodPost && req.path == "/ilink/bot/getuploadurl":
return handlerResult{body: jsonBody(map[string]any{"upload_full_url": srv.URL + "/cdn/up"})}
case req.method == http.MethodPut && req.path == "/cdn/up":
return handlerResult{status: http.StatusOK, body: []byte("ok")}
case req.method == http.MethodPost && req.path == "/cdn/up":
return handlerResult{
status: http.StatusOK,
body: []byte("ok"),
headers: map[string]string{uploadParamHeader: downloadParamFixture},
}
case req.method == http.MethodPost && req.path == "/ilink/bot/sendmessage":
return handlerResult{body: jsonBody(map[string]any{"ret": 0})}
default:
@@ -248,8 +261,12 @@ func TestOutboundMedia_BuildsUploadURLFromUploadParam(t *testing.T) {
switch {
case req.method == http.MethodPost && req.path == "/ilink/bot/getuploadurl":
return handlerResult{body: jsonBody(map[string]any{"upload_param": "ab+cd="})}
case req.method == http.MethodPut && req.path == "/upload":
return handlerResult{status: http.StatusOK, body: []byte("ok")}
case req.method == http.MethodPost && req.path == "/upload":
return handlerResult{
status: http.StatusOK,
body: []byte("ok"),
headers: map[string]string{uploadParamHeader: downloadParamFixture},
}
case req.method == http.MethodPost && req.path == "/ilink/bot/sendmessage":
return handlerResult{body: jsonBody(map[string]any{"ret": 0})}
default:
@@ -268,10 +285,13 @@ func TestOutboundMedia_BuildsUploadURLFromUploadParam(t *testing.T) {
t.Fatalf("SendMessage(media, upload_param): %v", err)
}
puts := rec.find(func(q recordedReq) bool { return q.method == http.MethodPut })
puts := rec.find(func(q recordedReq) bool { return q.path == "/upload" })
if len(puts) != 1 {
t.Fatalf("上传请求 = %d 次, want 1", len(puts))
}
if puts[0].method != http.MethodPost {
t.Fatalf("上传方法 = %s, want POST(官方口径)", puts[0].method)
}
if !strings.Contains(puts[0].query, "encrypted_query_param=ab%2Bcd%3D") {
t.Fatalf("上传查询串 = %q, want 转义后的 encrypted_query_param", puts[0].query)
}