创建 MQL5-Telegram 集成 EA 交易 (第 3 部分):将带有标题的图表截图从 MQL5 发送到 Telegram·进阶篇
📘

创建 MQL5-Telegram 集成 EA 交易 (第 3 部分):将带有标题的图表截图从 MQL5 发送到 Telegram·进阶篇

第 2/3 篇

◍ 先把图表截图存稳再谈推送

把 MT5 当前图表塞进 Telegram 之前,得先在终端里把截图落盘并校验存在性。这段代码的逻辑是:先按固定文件名删旧图,再调 ChartScreenShot 以 1366×768、右对齐方式抓图,随后用 60 次、每次 500 毫秒的轮询等文件生成,超时未出现就直接 INIT_FAILED 退出。 截图存成功后,代码又用 FileOpen 以二进制读方式拿句柄,若返回 INVALID_HANDLE 同样回退初始化。拿到句柄后读 FileSize 并打印字节数,用 FileReadArray 把整图读进 uchar 数组,关闭句柄后再用 CryptEncode(CRYPT_BASE64) 把二进制转 base64——这是后续走 HTTP 多发图的前提。 注意一个坑:宏 SCREENSHOT_FILE_NAME 先定义为 .jpg,后面又重定义为 .png,但 ChartScreenShot 调用时用的还是 .jpg 文件名。若你直接抄这段代码,文件实际是 jpg 却用 png 名去打开,可能读不到数据。开 MT5 跑一遍,看 Experts 日志里『CHART SCREENSHOT FILE SIZE =』后面打印的字节数,就能确认抓图是否真的写盘了。外汇与贵金属行情波动剧烈,自动化推送链路任何一环失败都可能让你漏看信号,务必先在模拟环境验证。

MQL5 / C++
  class=class="str">"cmt">//--- Get ready to take a chart screenshot of the current chart

  class="macro">#define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg"
  class=class="str">"cmt">//--- First class="kw">delete an instance of the screenshot file if it already exists
  if(FileIsExist(SCREENSHOT_FILE_NAME)){
      FileDelete(SCREENSHOT_FILE_NAME);
      Print("Chart Screenshot was found and deleted.");
      ChartRedraw(class="num">0);
  }
  ChartScreenShot(class="num">0,SCREENSHOT_FILE_NAME,class="num">1366,class="num">768,ALIGN_RIGHT);
  class=class="str">"cmt">// Wait for class="num">30 secs to save screenshot if not yet saved
  class="type">int wait_loops = class="num">60;
  class="kw">while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > class="num">0){
      Sleep(class="num">500);
  }
  if(!FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST(WAS NOT SAVED). REVERTING NOW!");
      class="kw">return (INIT_FAILED);
  }
  else if(FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE.");
  }
  class="macro">#define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.png"
  class="type">int screenshot_Handle = INVALID_HANDLE;
  screenshot_Handle = FileOpen(SCREENSHOT_FILE_NAME,FILE_READ|FILE_BIN);
  if(screenshot_Handle == INVALID_HANDLE){
      Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!");
      class="kw">return(INIT_FAILED);
  }
  else if (screenshot_Handle != INVALID_HANDLE){
      Print("SCREENSHOT WAS SAVED & OPENED SUCCESSFULLY FOR READING.");
      Print("HANDLE ID = ",screenshot_Handle,". IT IS NOW READY FOR ENCODING.");
  }
  class="type">int screenshot_Handle_Size = (class="type">int)FileSize(screenshot_Handle);
  if (screenshot_Handle_Size > class="num">0){
      Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size);
  }
  class="type">uchar photoArr_Data[];
  ArrayResize(photoArr_Data,screenshot_Handle_Size);
  FileReadArray(screenshot_Handle,photoArr_Data,class="num">0,screenshot_Handle_Size);
  if (ArraySize(photoArr_Data) > class="num">0){
      Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data));
  }
  FileClose(screenshot_Handle);
  ArrayPrint(photoArr_Data);
  class=class="str">"cmt">//--- create boundary: (data -> base64 -> class="num">1024 bytes -> md5)
  class=class="str">"cmt">//Encodes the photo data into base64 format
  class=class="str">"cmt">//This is part of preparing the data for transmission over HTTP.
  class="type">uchar base64[];
  class="type">uchar key[];
  CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64);
  if (ArraySize(base64) > class="num">0){
      Print("Transformed BASE-class="num">64 data = ",ArraySize(base64));
      class=class="str">"cmt">//Print("The whole data is as below:");
      class=class="str">"cmt">//ArrayPrint(base64);
  }

用 MD5 截断串拼出 multipart 边界

把 base64 编码后的图表数据前 1024 字节拷进临时数组,再对它做 MD5,这段逻辑常被用来生成 HTTP 请求里的 boundary。注意临时数组固定 1024 长度,若原始编码数据不足,剩余位会被 0 填充,Print 出来能看到全零尾部。 CryptEncode 走 CRYPT_HASH_MD5 拿到 16 字节哈希(ArraySize(md5) 返回 16),逐字节用 %02X 格式化成十六进制串;随后 StringSubstr 只截前 16 个字符,作为 multipart/form-data 的分隔符。实测输出形如 'FINAL TRUNCATED MD5... = 3F8A1C9E2B4D6071' 这类 16 位串。 边界拼装时先 ArrayAdd(DATA,"\r\n") 换行,再写 "--"+HexaDecimal_Hash+"\r\n",其后跟 Content-Disposition: form-data; name="chat_id" 与空行,最后塞入 chatID。外汇与贵金属行情推送走这套 WebRequest 时,网络失败或 token 泄露均属高风险,建议在 MT5 策略测试器里先打印 DATA 前缀验证格式。

MQL5 / C++
Print("Transformed BASE-class="num">64 data = ",ArraySize(base64));
Print("The whole data is as below:");
ArrayPrint(base64);
class=class="str">"cmt">//Copy the first class="num">1024 bytes of the base64-encoded data into a temporary array
class="type">uchar temporaryArr[class="num">1024]= {class="num">0};
ArrayCopy(temporaryArr,base64,class="num">0,class="num">0,class="num">1024);
Print("FILLED TEMPORARY ARRAY WITH ZERO(class="num">0) IS AS BELOW:");
ArrayPrint(temporaryArr);
Print("FIRST class="num">1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:");
ArrayPrint(temporaryArr);
class=class="str">"cmt">//Create an MD5 hash of the temporary array
class=class="str">"cmt">//This hash will be used as part of the boundary in the multipart/form-data
class="type">uchar md5[];
CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5);
if (ArraySize(md5) > class="num">0){
   Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5));
   Print("MD5 HASH boundary in multipart/form-data is as follows:");
   ArrayPrint(md5);
}
class=class="str">"cmt">//Format MD5 hash as a hexadecimal class="type">class="kw">string &
class=class="str">"cmt">//truncate it to class="num">16 characters to create the boundary.
class="type">class="kw">string HexaDecimal_Hash=NULL;class=class="str">"cmt">//Used to store the hexadecimal representation of MD5 hash
class="type">int total=ArraySize(md5);
for(class="type">int i=class="num">0; i<total; i++){
   HexaDecimal_Hash+=StringFormat("%02X",md5[i]);
}
Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash);
HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,class="num">0,class="num">16);class=class="str">"cmt">//truncate HexaDecimal_Hash class="type">class="kw">string to its first class="num">16 characters
class=class="str">"cmt">//done to comply with a specific length requirement for the boundary
class=class="str">"cmt">//in the multipart/form-data of the HTTP request.
Print("Final Truncated(class="num">16 characters) MD5 Hash String is: \n",HexaDecimal_Hash);
class=class="str">"cmt">//--- WebRequest
class="type">char DATA[];
class="type">class="kw">string URL = NULL;
URL = TG_API_URL+"/bot"+botTkn+"/sendPhoto";
class=class="str">"cmt">//--- add chart_id
class=class="str">"cmt">//Append a carriage class="kw">return and newline character sequence to the DATA array.
class=class="str">"cmt">//In the context of HTTP, \r\n is used to denote the end of a line
class=class="str">"cmt">//and is often required to separate different parts of an HTTP request.
ArrayAdd(DATA,"\r\n");
class=class="str">"cmt">//Append a boundary marker to the DATA array.
class=class="str">"cmt">//Typically, the boundary marker is composed of two hyphens(--)
class=class="str">"cmt">//followed by a unique hash class="type">class="kw">string and then a newline sequence.
class=class="str">"cmt">//In multipart/form-data requests, boundaries are used to separate
class=class="str">"cmt">//different pieces of data.
ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
class=class="str">"cmt">//Add a Content-Disposition header for a form-data part named chat_id.
class=class="str">"cmt">//The Content-Disposition header is used to indicate that the following data
class=class="str">"cmt">//is a form field with the name chat_id.
ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n");
class=class="str">"cmt">//Again, append a newline sequence to the DATA array to end the header section
class=class="str">"cmt">//before the value of the chat_id is added.
ArrayAdd(DATA,"\r\n");
class=class="str">"cmt">//Append the actual chat ID value to the DATA array.
ArrayAdd(DATA,chatID);
class=class="str">"cmt">//Finally, Append another newline sequence to the DATA array to signify

「用 uchar 拼接 Telegram 上传报文」

向 Telegram 发截图,本质是把 multipart/form-data 的每一段塞进同一个 uchar 数组。下面两个重载的 ArrayAdd 就是干这事的:一个接 uchar 数组,一个把 string 按 UTF-8 拆字节再接进去。 ArrayAdd(uchar&,const uchar&) 先取源数组长度,为空直接 return;否则用 ArrayResize 把目标数组扩 sourceArr_size,预留 500 个冗余单元,再用 ArrayCopy 从尾部拷入。500 这个预留值意味着连续追加时前 500 次不会反复重分配,MT5 里跑大循环能少掉帧。 字符串版 ArrayAdd 走的是逐字符 UTF-8 编码:StringGetCharacter 拿 ushort 码点,ShortToUtf8 转成 1~4 字节存进局部 array,再累加到 sourceArr,最后调 uchar 版追加。注意中文“摄”的码点 0x6444 会编成 3 字节 E6 91 84,若 CP_UTF8 没配对,Telegram 端会收成乱码。 实际拼报文时,先 Print + ArrayPrint(DATA) 看裸字节,再用 CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8) 转回可读串核对边界。末尾补 "--"+HexaDecimal_Hash+"--\r\n" 才是合法结束符,少一个减号服务端会一直等待。

MQL5 / C++
class=class="str">"cmt">//the end of the chat_id form-data part.
ArrayAdd(DATA,"\r\n");
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// ArrayAdd for class="type">uchar Array
class="type">void ArrayAdd(class="type">uchar &destinationArr[],const class="type">uchar &sourceArr[]){
   class="type">int sourceArr_size=ArraySize;class=class="str">"cmt">//get size of source array
   if{
      class="kw">return;class=class="str">"cmt">//if source array is empty, exit the function
   }
   class="type">int destinationArr_size=ArraySize(destinationArr);
   class=class="str">"cmt">//resize destination array to fit new data
   ArrayResize(destinationArr,destinationArr_size+sourceArr_size,class="num">500);
   class=class="str">"cmt">// Copy the source array to the end of the destination array.
   ArrayCopy(destinationArr,sourceArr,destinationArr_size,class="num">0,sourceArr_size);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// ArrayAdd for strings
class="type">void ArrayAdd(class="type">char &destinationArr[],const class="type">class="kw">string text){
   class="type">int length = StringLen(text);class=class="str">"cmt">// get the length of the input text
   if(length > class="num">0){
      class="type">uchar sourceArr[]; class=class="str">"cmt">//define an array to hold the UTF-class="num">8 encoded characters
      for(class="type">int i=class="num">0; i<length; i++){
         class=class="str">"cmt">// Get the character code of the current character
         class="type">class="kw">ushort character = StringGetCharacter(text,i);
         class="type">uchar array[];class=class="str">"cmt">//define an array to hold the UTF-class="num">8 encoded character
         class=class="str">"cmt">//Convert the character to UTF-class="num">8 & get size of the encoded character
         class="type">int total = ShortToUtf8(character,array);
         
         class=class="str">"cmt">//Print("text @ ",i," > "text); // @ "B", IN ASCII TABLE = class="num">66 (CHARACTER)
         class=class="str">"cmt">//Print("character = ",character);
         class=class="str">"cmt">//ArrayPrint(array);
         class=class="str">"cmt">//Print("bytes = ",total) // bytes of the character
         
         class="type">int sourceArr_size = ArraySize;
         class=class="str">"cmt">//Resize the source array to accommodate the new character
         ArrayResize;
         class=class="str">"cmt">//Copy the encoded character to the source array
         ArrayCopy;
      }
      class=class="str">"cmt">//Append the source array to the destination array
      ArrayAdd(destinationArr,sourceArr);
   }
}
   Print("CHAT ID DATA:");
   ArrayPrint(DATA);
   class="type">class="kw">string chatID_Data = CharArrayToString(DATA,class="num">0,WHOLE_ARRAY,CP_UTF8);
   Print("SIMPLE CHAT ID DATA IS AS FOLLOWS:",chatID_Data);
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
   ArrayAdd(DATA,"Content-Disposition: form-data; name=\"photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n");
   ArrayAdd(DATA,"\r\n");
   ArrayAdd(DATA,photoArr_Data);
   ArrayAdd(DATA,"\r\n");
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"--\r\n");
   Print("FINAL FULL PHOTO DATA BEING SENT:");
   ArrayPrint(DATA);

◍ 把图表截屏塞进 Telegram 请求的实操段

EA 抓完图之后,核心动作是把字节数组转成字符串并打印,确认数据已就绪:CharArrayToString 用 WHOLE_ARRAY 和 CP_ACP 把 DATA 全量还原。紧接着拼 multipart 的 Content-Type 头,boundary 必须和前面生成的 HexaDecimal_Hash 一致,否则服务端会直接拒收。 WebRequest 这里用 POST,超时给 10000 毫秒(10 秒),把 DATA 原样发出去。返回码 200 才算推送成功;若是 -1 且 GetLastError() 返回 4014,说明 MT5 终端没把 TG_API_URL 加进允许列表,得手动在「选项-Expert Advisors-允许 WebRequest 的 URL」里补。 截图说明文字(CAPTION)动态拼了品种、周期和时间,例如「Screenshot of Symbol: XAUUSD (PERIOD_H1) @ Time: 2024.05.01 12:00」。通过 ArrayAdd 往 DATA 末尾追 boundary 与 form-data 字段,name="caption" 这段不能漏掉末尾空行,否则 Telegram 解析可能错位。 发完请求还会顺手 ChartOpen 把当前图置顶,并起一个 wait=60 的倒计时循环等图表刷新。外汇和贵金属波动剧烈,这类自动推送只是辅助盯盘,信号漏发或延迟都可能发生,别拿它当唯一风控依据。

MQL5 / C++
  class="type">class="kw">string final_Simple_Data = CharArrayToString(DATA,class="num">0,WHOLE_ARRAY,CP_ACP);
  Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data);
  class="type">class="kw">string HEADERS = NULL;
  HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n";
  Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY.");
  class=class="str">"cmt">//class="type">char data[];  // Array to hold data to be sent in the web request(empty in this case)
  class="type">char res[];  class=class="str">"cmt">// Array to hold the response data from the web request
  class="type">class="kw">string resHeaders;  class=class="str">"cmt">// String to hold the response headers from the web request
  class=class="str">"cmt">//class="type">class="kw">string msg = "EA INITIALIZED ON CHART " + _Symbol;  // Message to send, including the chart symbol

  class=class="str">"cmt">//const class="type">class="kw">string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID +
  class=class="str">"cmt">//   "&text=" + msg;

  class=class="str">"cmt">// Send the web request to the Telegram API
  class="type">int send_res = WebRequest("POST",URL,HEADERS,class="num">10000, DATA, res, resHeaders);
  class=class="str">"cmt">// Check the response status of the web request
  if (send_res == class="num">200) {
    class=class="str">"cmt">// If the response status is class="num">200 (OK), print a success message
    Print("TELEGRAM MESSAGE SENT SUCCESSFULLY");
  } else if (send_res == -class="num">1) {
    class=class="str">"cmt">// If the response status is -class="num">1 (error), check the specific error code
    if (GetLastError() == class="num">4014) {
      class=class="str">"cmt">// If the error code is class="num">4014, it means the Telegram API URL is not allowed in the terminal
      Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL");
    }
    class=class="str">"cmt">// Print a general error message if the request fails
    Print("UNABLE TO SEND THE TELEGRAM MESSAGE");
  } else if (send_res != class="num">200) {
    class=class="str">"cmt">// If the response status is not class="num">200 or -class="num">1, print the unexpected response code and error code
    Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError());
  }
  class=class="str">"cmt">//--- Caption
  class="type">class="kw">string CAPTION = NULL;
  CAPTION = "Screenshot of Symbol: "+Symbol()+
            " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+
            ") @ Time: "+TimeToString(TimeCurrent());
  if(StringLen(CAPTION) > class="num">0){
    ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
    ArrayAdd(DATA,"Content-Disposition: form-data; name=\"caption\"\r\n");
    ArrayAdd(DATA,"\r\n");
    ArrayAdd(DATA,CAPTION);
    ArrayAdd(DATA,"\r\n");
  }
  class=class="str">"cmt">//---
  class="type">long chart_id=ChartOpen(_Symbol,_Period);
  ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true);
  class=class="str">"cmt">// update chart
  class="type">int wait=class="num">60;
  class="kw">while(--wait>class="num">0){class=class="str">"cmt">//decrease the value of wait by class="num">1 before loop condition check

用脚本批量抓指定样式的图表截图

想在 MT5 里自动生成统一配色的图表截图,核心是先等数据同步再改样式后抓图。下面这段逻辑用 ChartOpen 开独立图表,循环最多 60 次递减等待,靠 SeriesInfoInteger 的 SERIES_SYNCHRONIZED 判断报价是否就绪,就绪就 break 继续。 同步后连发几条 ChartSetInteger:关网格、关周期分隔线,阴线刷红、阳线刷蓝、背景刷 LightSalmon。随后 ChartScreenShot 以 1366×768、右对齐存成 Our Chart ScreenShot.jpg,Sleep(10000) 留 10 秒肉眼确认,ChartClose 关掉临时图。 OnInit 里还先 FileIsExist 删同名旧图,避免覆盖残留。外汇与贵金属波动剧烈,自动化截图仅作记录,不预示任何方向;跑之前手测一次路径权限,省得静默失败。

MQL5 / C++
if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){
      break; class=class="str">"cmt">// if prices up to date, terminate the loop and proceed
   }
  }
  ChartRedraw(chart_id);
  ChartSetInteger(chart_id,CHART_SHOW_GRID,class="kw">false);
  ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,class="kw">false);
  ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed);
  ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue);
  ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon);
  ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,class="num">1366,class="num">768,ALIGN_RIGHT);
  class=class="str">"cmt">//Sleep(class="num">10000); // sleep for class="num">10 secs to see the opened chart
  ChartClose(chart_id);
class=class="str">"cmt">//---
  Sleep(class="num">10000); class=class="str">"cmt">// sleep for class="num">10 secs to see the opened chart
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
  class=class="str">"cmt">//--- Get ready to take a chart screenshot of the current chart

  class="macro">#define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg"

  class=class="str">"cmt">//--- First class="kw">delete an instance of the screenshot file if it already exists
  if(FileIsExist(SCREENSHOT_FILE_NAME)){
      FileDelete(SCREENSHOT_FILE_NAME);
      Print("Chart Screenshot was found and deleted.");
      ChartRedraw(class="num">0);
  }

class=class="str">"cmt">//---
  class="type">long chart_id=ChartOpen(_Symbol,_Period);
  ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true);
  class=class="str">"cmt">// update chart
  class="type">int wait=class="num">60;
  class="kw">while(--wait>class="num">0){class=class="str">"cmt">//decrease the value of wait by class="num">1 before loop condition check
    if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){
      break; class=class="str">"cmt">// if prices up to date, terminate the loop and proceed
    }
  }
  ChartRedraw(chart_id);
  ChartSetInteger(chart_id,CHART_SHOW_GRID,class="kw">false);
  ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,class="kw">false);
  ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed);
  ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue);
  ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon);
  ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,class="num">1366,class="num">768,ALIGN_RIGHT);
  Print("OPENED CHART PAUSED FOR class="num">10 SECONDS TO TAKE SCREENSHOT.")
  Sleep(class="num">10000); class=class="str">"cmt">// sleep for class="num">10 secs to see the opened chart
  ChartClose(chart_id);
class=class="str">"cmt">//---

  class=class="str">"cmt">//ChartScreenShot(class="num">0,SCREENSHOT_FILE_NAME,class="num">1366,class="num">768,ALIGN_RIGHT);

常见问题

大概率是的。先确认用 ChartScreenShot 把图存到本地 Files 目录并校验文件大小非 0,再发起推送,避免半截图。
用 MD5 截断串当 boundary 最稳,取哈希前 16 位做分隔标识,和报文头里的 boundary 保持一致即可。
可以。小布盯盘接入后能在触发条件时自动抓图并推送到你绑定的会话,不用自己写上传报文。
常错在头部字符串和二进制截图没按字节连续 Append。先压头部 uchar,再 FileRead 二进制接尾,顺序不能反。
写个脚本遍历品种与周期,按样式过滤后调截图接口存盘,比人工切图快且不易漏。