【问题标题】:Need to prevent GAS web app instances from colliding需要防止 GAS Web 应用实例发生冲突
【发布时间】:2026-01-06 06:00:02
【问题描述】:

我正在使用 Google Apps 脚本为我的 Telegram 机器人创建一个网络应用程序。该网络应用程序由机器人通过网络挂钩调用。

我的机器人有一个内联键盘,您可以在下面的 GIF 1 中看到它的运行情况。轻按键盘按钮,您可以输入出现在屏幕上的代码。

我的问题是,当我更快地点击按钮时,对 Web 应用程序的回调查询会发生冲突,从而导致输入混乱。参见 GIF 2。

我一直在绞尽脑汁试图找出一种方法来防止网络应用程序实例发生冲突,但到目前为止我没有运气。

我在下面发布我的代码。请帮忙。

我的看法是,脚本的每个实例都需要更多时间才能完成,然后才能启动下一个实例。尽我所知,我已尝试使用 async/await 和 lockService。有人建议我尝试对查询进行排队,但遗憾的是无法使其正常工作。

var lock = LockService.getScriptLock();
    
function doPost(e){

      var contents = JSON.parse(e.postData.contents);
   
      var query_id = contents.callback_query.id;
        
      var mes_id = contents.callback_query.message.message_id;
        
      var userinput = contents.callback_query.data;
        
      var message_now = contents.callback_query.message.text;
      
      var inline_keyboard = contents.callback_query.message.reply_markup;  
      
  
      var message_upd = message_now + " " + userinput;
           
        var keydata = {
            method: "post",
            payload: {
              method: "editMessageText",
              chat_id: String(chat_id),
              message_id: mes_id,
              text: message_upd ,
              parse_mode: "HTML",
              reply_markup: JSON.stringify(inline_keyboard)
            }
          }
   
    lock.waitLock(10000);

    UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', keydata); 
        
    UrlFetchApp.fetch(url + "/answerCallbackQuery?callback_query_id=" + query_id);
      
    lock.releaseLock();

    } 

【问题讨论】:

  • 您是否在 Web 应用程序中编写了向doGet() 发出请求的客户端代码?您对客户端代码有任何控制权吗?可以重写客户端代码吗?
  • 您还说 LockService 不起作用。您能否通过显示您的 lockservice 实现和日志来证明这一点?
  • @AlanWells,客户端是应用程序 Telegram。我无法控制它的代码。
  • @TheMaster,我已经编辑了上面的代码,以展示我在使用 LockService 时的卑微尝试。它对手头的问题没有任何影响。我会很感激任何关于它是否错、为什么错以及如何更好地解决它的想法。

标签: google-apps-script web-applications locking telegram-bot telegram-webhook


【解决方案1】:

问题:

我相信lock 确实有效。问题可能是电报机器人发送的回调查询。在你的第二个 gif 中,

在按2、3、4的时候,附加的消息是空的。因此,所有 4 个回调的 message.text 都将为空

var message_now = contents.callback_query.message.text; 

message_now 对于所有 4 条消息都是空的,所有 4 条 mesage_upd 都会不同:

var message_upd = message_now + " " + userinput;

即使您使用LockService 在服务器端对所有内容进行排队,如果电报提供的message_now 对于所有 4 条消息都是空的,那么排队对于创建这样的串联字符串也是无用的。

可能的解决方案:

  • 队列回调应该在客户端完成。只有在收到第一个按钮按下的响应后,才应该激活第二个回调。我不确定电报是否提供如此精细的控制。但如果是这样,这是首选的解决方案。

  • 使用缓存服务服务器端为特定用户的特定message.id 缓存最后一个message_now。将其保存到缓存服务 30 秒左右。如果在 30 秒后出现另一个具有相同 message_id 的回调,并且 message.text 为空,请改用缓存的消息。

    • key:某种类型的message_iduser_id 组合
    • value:当前串联message_now

片段:

  let message_now = contents.callback_query.message.text;
  if (message_now === '') message_now = cache.get(String(mes_id)) || '';
  /*....*/
  cache.put(String(mes_id), String(message_upd), 30);

function doPost(e) {
  const lock = LockService.getScriptLock();
  lock.waitLock(10000);
  const cache = CacheService.getScriptCache();
  const contents = JSON.parse(e.postData.contents);
  const query_id = contents.callback_query.id;
  const mes_id = contents.callback_query.message.message_id;
  const userinput = contents.callback_query.data;
  let message_now = contents.callback_query.message.text;
  if (message_now === '') message_now = cache.get(String(mes_id)) || '';
  const inline_keyboard = contents.callback_query.message.reply_markup;
  const message_upd = message_now + ' ' + userinput;
  const keydata = {
    method: 'post',
    payload: {
      method: 'editMessageText',
      chat_id: String(chat_id),
      message_id: mes_id,
      text: message_upd,
      parse_mode: 'HTML',
      reply_markup: JSON.stringify(inline_keyboard),
    },
  };
  UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', keydata);
  UrlFetchApp.fetch(url + '/answerCallbackQuery?callback_query_id=' + query_id);
  cache.put(String(mes_id), String(message_upd), 30);
  lock.releaseLock();
}

参考资料:

【讨论】:

  • 谢谢你,@TheMaster。我尝试使用您的代码稍作调整,它工作。然后,出于好奇,我继续绕过锁定脚本,它也起作用了。所以,对我来说,这只是表明从未发生过真正的锁定,原因我无法解释。之后,我继续将缓存 message_upd 替换为将其保存到 scriptProperties,因为缓存毕竟是临时的,而 scriptProperties 永远存在。它也有效,无需锁定。欢呼!这种方法唯一的缺点是它比直接从 message.text 中提取花费的时间明显更长。
  • 稍后我将在这里发布我的最终代码作为答案。事实上,当我来这里询问时,我已经有了这个依赖 scriptProperties 作为后备选项的解决方案。再加上另一个可以写入和读取 Google 表格的功能。后者似乎具有最难以忍受的延迟,但不知何故也是防碰撞的。所以,可以这么说,我一直在寻找更直截了当的东西。好吧,似乎现在写入和读取 scriptProperties 占了上风。
  • @hey 然后,出于好奇,我继续绕过锁定脚本,它也起作用了。所以,对我来说,它只是表明没有发生实际的锁定 并不能证明锁定不起作用。这可能只是意味着您的速度不足以触发锁定。如果脚本运行时间很长,比如 3 分钟或更长时间,这可能会变得清晰。