1. 笨AI
DeepSeek好固然好,但是他常摆出一脸臭脸,令人不爽。
你转“好久”,就告诉我这?繁忙,想了0秒?
既然可以稍后再试,为什么不能自动替我试一下呢?
2. 写脚本
2.1 决定写脚本
我决定写个 tampermonkey 脚本,见到繁忙就重新提交。
2.2 决定由AI写
为什么要我来写呢,不是有AI吗,应该AI干活啊。
2.3 与 Kimi 相看两厌
因为讨厌 DeepSeek繁忙的消息,我找到了 Kimi,和他聊了一会儿。
接下来,我希望他能自动发现这些网页元素的特征,他希望这事由我来做。如果特征要我自己去找特征,不如我自己把脚本写了呢。
2.4 决定由 DeepSeek写
还是找 DeepSeek,繁忙我就先忍着。提交了同样的要求。DeepSeek特意提醒我,别提交页面的截图,应该用F12。提交页面的过程中,范围越扩越大,我把整个div交上去了。
DeepSeek超限了。
2.5 考虑下要求,如何精确而省力地描述
对话框c,看着它的id就像唯一的稳定的标识
要求变化不大,用按回车代替点击发送按钮。我以为这样可以少讨论一个网页元素。
繁忙a,是DeepSeek说的那段“我忙”;
要求b,是我的最后一条消息;
对话框c,是下面那个文本框,输入消息的。
2.5.1 繁忙a
我没看到示例,但是英雄所见略同,我贴了HTML代码。
后面还有很长,截图略。后来我发现这路数不好,稍后会提到。
2.5.2 jquery,以及在 console 测试
因为DeepSeek并没有加载jquery,因此jquery相关的函数都未定义。我放弃了在 console中测试,准备冒险直接在 tampermonkey 中测。如果一再错,改不出来,那就放弃。
我读 DeepSeek的消息 读得急躁,跳过了重要信息。
所以我看他还一再希望我按最初的计划测试,再次要求。
他不理我(他经常高度自信,甚至过于自信,跟传说中的雪地犬一样,认为自己更专业,按你说的做只是你刚好说得对),继续要求我测。我有次脑抽忘了不打算测,测了,居然好使了,发现他已经去除了对jquery的依赖,全用js代码写的。
2.5.3 繁忙a 和 要求b
“繁忙”字样可能出现多次,只有之后有个 New chat,之后有个文本框……的才是繁忙a。
如何描述这些,我纠结了几个来回。后来突然想到,他不是AI么,他应该挺聪明啊。
我用人类语言描述了繁忙a和要求b的特征:交替对话,deepseek的最后一条消息,以及我的最后一条消息。
在同一条消息中,我给出了“我发送的消息”的示例。
在同一条消息中,我给出了“deepseek发送的消息”的示例。
这样,tokens的数量比贴整个div(父一级的)要小很多。
2.5.4 消息发送
脚本找到了繁忙a(并且当繁忙字样不在最后一条,是历史消息时,并未误判),复制了正确的命令b文字,粘贴到了正确的对话框c里。但是,并不发送。
他问我,按钮状态是不是禁用的,这样 ,还是这样
?
代码里某个属性,具体地说,aria-disabled的值在按钮禁用时是什么?
于是,这样来回测了几次。
在这个具体的步骤中,我是他的眼睛和手指,他是我的大脑。
2.5.5 测试环境
你想提问时,DeepSeek繁忙;你希望他繁忙时,他不忙了。很快我就把历史上留下来的最后一条消息是“繁忙”的会话消耗光了。
我让DeepSeek自己说“繁忙”。我不知道他是真忙,还是按我要求才忙的。但是他表现出了忙的样子,很好。
2.5.6 成功了
不一刻,在某个版本(v5,不知道为什么还叫作 @version 1.4),成功了。
我此前没有提到,DeepSeek还考虑到可能频繁提交类似DoS效果/抖动,所以他要求脚本的观察时隔为2秒,提交次数上限5次。并且在开发过程中,他屡次提醒,我一直不吱声。直到最后才夸了他。他说除了2秒检测间隔,还有3秒初始延迟。
3. 体会
我全程一行代码也没写。
对我的要求如下。
(1)在浏览器里F12,会在Console里贴代码、跑、复制出错信息,用眼睛观察结果,用文字描述。
(2) 在浏览器里F12,会用 Inspector复制一段html代码。
(3) 在浏览器里F12,DeepSeek认为存在可能需要看Network,但是这次没有用到。
(4) 描述需求。
(5) 在 tampermonkey中建立新脚本,向里面贴代码。并非必要的,为了退出运行,如果会禁用tampermonkey或tampermonkey插件,更好。
4. 代码,enjoy
// ==UserScript==
// @name DeepSeek 繁忙自动重试(v5)
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 修复输入框聚焦和内容保持问题
// @author You
// @match https://chat.deepseek.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const config = {
checkInterval: 2000,
busyText: "服务器繁忙,请稍后再试。",
inputSelector: 'textarea[placeholder="Message DeepSeek"]',
sendButtonSelector: 'div[role="button"][aria-disabled="false"].f6d670'
};
// 增强输入处理
const safeInput = (inputElement, text) => {
return new Promise(resolve => {
// 确保输入框聚焦
inputElement.focus();
inputElement.select();
// 清除现有内容
inputElement.value = '';
['input', 'change'].forEach(eventType => {
inputElement.dispatchEvent(new Event(eventType, {
bubbles: true,
cancelable: true
}));
});
// 使用document.execCommand实现更真实的输入
const pasteText = () => {
const success = document.execCommand('insertText', false, text);
if (success) {
// 触发必要事件
['input', 'change'].forEach(eventType => {
inputElement.dispatchEvent(new Event(eventType, {
bubbles: true,
cancelable: true
}));
});
resolve(true);
} else {
resolve(false);
}
};
// 如果execCommand不可用,使用备用方案
if (!document.execCommand) {
inputElement.value = text;
['input', 'change'].forEach(eventType => {
inputElement.dispatchEvent(new Event(eventType, {
bubbles: true,
cancelable: true
}));
});
resolve(true);
} else {
setTimeout(pasteText, 100);
}
});
};
const safeClickButton = () => {
const button = document.querySelector(config.sendButtonSelector);
if (!button || button.getAttribute('aria-disabled') !== 'false') return false;
// 创建更真实的点击事件
const mouseEvents = ['mousedown', 'mouseup', 'click'];
mouseEvents.forEach(eventType => {
button.dispatchEvent(new MouseEvent(eventType, {
bubbles: true,
cancelable: true,
view: window
}));
});
return button.getAttribute('aria-disabled') === 'true';
};
const getLastUserMessage = () => {
const userMessages = document.querySelectorAll('div.fa81');
return userMessages.length > 0
? userMessages[userMessages.length -1].querySelector('div.fbb737a4')?.textContent?.trim()
: null;
};
const checkBusyState = () => {
const botMessages = document.querySelectorAll('div.f9bf7997.d7dc56a8');
return botMessages.length > 0
&& botMessages[botMessages.length -1].querySelector('.ds-markdown p')?.textContent === config.busyText;
};
const resendMessage = async () => {
const message = getLastUserMessage();
if (!message) return;
const input = document.querySelector(config.inputSelector);
if (!input) return;
// 等待输入完成
const inputSuccess = await safeInput(input, message);
if (!inputSuccess) {
console.error('[AutoRetry] 输入失败');
return;
}
// 等待按钮状态更新
await new Promise(resolve => setTimeout(resolve, 500));
// 点击按钮
const clickSuccess = safeClickButton();
if (!clickSuccess) {
console.error('[AutoRetry] 点击失败');
return;
}
console.log('[AutoRetry] 消息重发成功');
};
let lastState = false;
const detectionLoop = () => {
const currentState = checkBusyState();
if (currentState && !lastState) {
console.log('[AutoRetry] 触发重试机制');
resendMessage();
}
lastState = currentState;
setTimeout(detectionLoop, config.checkInterval);
};
window.addEventListener('load', () => {
setTimeout(detectionLoop, 3000);
});
})();
此文也发布在以下站点。
----
知乎 https://www.zhihu.com/people/yang-gui-fu-52
微信公众号 杨贵福
----
以下是我曾经发布博客的站点,有些旧文。
----
豆瓣 - 因为审核"我的日记",不再更新。
https://www.douban.com/people/younggift/?_i=0098558fqLUL9h
CSDN – 因为要求我登记手机号码的原因是“为了您的安全”,不再更新。
https://blog.csdn.net/younggift?type=blog
blogsopt – 因为从我的机器不可达,无法更新