// ==UserScript==
// @name Bing 自动搜索
// @namespace bing-auto-search
// @version 1.1.2
// @description 在 Bing 页面内自动执行搜索,模拟真人输入与随机间隔。每日目标次数在设定范围内随机,纯浏览器内运行,无需 Python / Playwright。
// @author pwhscr
// @license MIT
// @homepageURL https://bing-auto-search.app.workbuddy.host/
// @supportURL https://bing-auto-search.app.workbuddy.host/
// @match https://www.bing.com/*
// @match https://cn.bing.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @noframes
// ==/UserScript==
/*
* 工作原理
* --------
* 脚本在每一个 Bing 页面上注入。页面加载后先读持久化状态:
* - running=false → 只显示控制面板,不动页面
* - running=true → 等一个随机延迟,取下一个搜索词,填进搜索框并提交
* 提交后页面跳转到结果页,脚本重新注入,继续下一轮。
* 所以整个流程是靠「页面重载 + 状态持久化」串起来的,跨页面不会断。
*
* 只在你自己的浏览器里跑,用的是你真实的登录态和浏览器指纹,
* 不存在 Playwright 那种「受控浏览器」的特征。
*
* 注意:自动化搜索违反 Bing 服务条款,请使用不重要的账号。
*/
(function () {
'use strict';
// ---------------------------------------------------------------- 配置
const STORE_KEY = 'bing_auto_search_v1';
const PANEL_ID = 'bing-auto-search-panel';
const MAX_LOG = 12;
// 搜索词库:15 个分类共 450 条真人会搜的完整短语
const TERM_BANK = /*__TERM_BANK__*/
{
"编程开发": [
"python 字典按 value 排序",
"pandas 读取 excel 指定 sheet",
"git 撤销上一次 commit 保留修改",
"docker 容器时间不对 时区",
"mysql 索引失效的几种情况",
"js 数组去重 性能对比",
"正则表达式 匹配中文",
"linux 查看端口占用",
"nginx 反向代理配置示例",
"redis 缓存穿透和雪崩的区别",
"typescript 泛型约束",
"python 虚拟环境 venv 用法",
"sql 窗口函数 排名",
"http 状态码 301 和 302 区别",
"jwt token 过期刷新方案",
"vscode 常用插件推荐",
"如何优雅地写 commit message",
"cors 跨域问题解决方案",
"webpack 和 vite 的区别",
"单元测试覆盖率多少合适",
"fastapi 异步接口写法",
"playwright 和 selenium 区别",
"如何阅读开源项目源码",
"设计模式 单例 线程安全",
"消息队列 削峰填谷 实践",
"k8s pod 一直 pending 排查",
"python 异步编程 asyncio 入门",
"接口限流 令牌桶算法",
"如何做代码审查",
"程序员的键盘推荐 2026"
],
"数码科技": [
"骁龙和天玑 哪个更省电",
"手机电池健康度多少该换",
"笔记本 32g 内存有必要吗",
"固态硬盘 pcie4 和 pcie5 差距",
"无线耳机 降噪 排名",
"充电宝 10000 毫安 能带上飞机吗",
"路由器 wifi6 和 wifi7 怎么选",
"显示器 2k 还是 4k 办公",
"机械键盘 轴体 手感 区别",
"手机摄影 夜景 拍摄技巧",
"nas 家用 入门 推荐",
"智能手表 睡眠监测 准确度",
"显示器 色域 色准 是什么意思",
"type-c 接口 全功能 区别",
"显卡 显存 多大够用",
"迷你主机 办公 够用吗",
"扫地机器人 有必要买吗",
"投影仪 白天看得清吗",
"墨水屏 阅读器 对比",
"无人机 新手 推荐",
"手机 快充 协议 兼容性",
"电子墨水屏 伤眼吗",
"蓝牙 5.3 和 5.0 有什么区别",
"如何挑选第一台相机",
"相机 全画幅 和 半画幅 差距",
"照片 备份 方案 推荐",
"电脑 散热 硅脂 更换 周期",
"机械硬盘 和 固态硬盘 哪个适合存资料",
"手机 内存扩展 有用吗",
"平板 生产力 真的行吗"
],
"生活常识": [
"洗衣机 多久清洗一次",
"冰箱 温度 调到几度合适",
"微波炉 加热 塑料 安全吗",
"空调 除湿 和 制冷 区别",
"被子 多久晒一次",
"菜刀 生锈 怎么处理",
"衣服 起球 怎么解决",
"鞋子 发臭 快速去除",
"头发 一天掉多少根正常",
"晚上跑步 好还是早上跑步好",
"喝水 一天多少 合适",
"久坐 腰疼 缓解动作",
"眼睛 干涩 怎么办",
"睡眠 质量差 改善方法",
"咖啡 一天 喝几杯合适",
"外卖 塑料盒 加热 危害",
"水果 饭前吃 还是饭后吃",
"隔夜菜 到底能不能吃",
"拖鞋 材质 哪种好",
"除螨 真的有用吗",
"阳台 种菜 入门",
"蚊香 和 电蚊液 哪个好",
"加湿器 用纯净水还是自来水",
"防潮 除湿 小妙招",
"门锁 反锁 从外面能打开吗",
"燃气灶 打不着火 原因",
"水龙头 漏水 自己修",
"墙壁 发霉 怎么彻底解决",
"宠物 掉毛 清理办法",
"窗帘 什么颜色 遮光好"
],
"美食烹饪": [
"红烧肉 怎么做 不腻",
"糖醋排骨 正宗做法",
"番茄炒蛋 先炒蛋还是先炒番茄",
"麻婆豆腐 家常做法",
"清蒸鱼 几分钟",
"可乐鸡翅 简单做法",
"蛋炒饭 粒粒分明 技巧",
"水煮肉片 嫩滑 秘诀",
"酸辣土豆丝 脆爽 做法",
"饺子馅 怎么调才香",
"馒头 怎么发面 更松软",
"烤鸡翅 空气炸锅 温度时间",
"空气炸锅 薯条 几分钟",
"电饭煲 蛋糕 做法",
"银耳汤 出胶 技巧",
"皮蛋瘦肉粥 做法",
"凉拌黄瓜 好吃 做法",
"小龙虾 清洗 方法",
"牛排 几分熟 好吃",
"意面 煮多久",
"咖啡 手冲 参数",
"奶茶 在家 做法",
"柠檬水 正确泡法",
"减脂餐 一周 菜谱",
"健身餐 蛋白质 搭配",
"家常菜 一周不重样",
"一日三餐 怎么搭配营养",
"汤 炖多久 最有营养",
"炒菜 什么时候放盐",
"如何挑西瓜 甜不甜"
],
"健康养生": [
"颈椎病 自我锻炼 方法",
"血脂高 饮食 注意什么",
"血糖高 能不能吃水果",
"血压 正常范围 是多少",
"体检 报告 怎么看",
"转氨酶偏高 原因",
"甲状腺结节 需要治疗吗",
"幽门螺杆菌 怎么检查",
"维生素d 缺乏 怎么补",
"补钙 什么时间 吸收好",
"减脂 平台期 怎么突破",
"有氧和力量训练 顺序",
"跑步 膝盖疼 怎么办",
"游泳 多久 能减肥",
"平板支撑 每天多久有效",
"拉伸 运动后 有用吗",
"睡眠 呼吸暂停 症状",
"焦虑 情绪 调节方法",
"近视 度数 会遗传吗",
"牙结石 洗牙 多久一次",
"控糖 主食 怎么吃",
"粗粮 吃多了 会怎样",
"蛋白质 每天 摄入量",
"益生菌 真的有用吗",
"中医 湿气重 表现",
"艾灸 注意事项",
"拔罐 之后 能洗澡吗",
"八段锦 练习 要点",
"早睡 几点 算早睡",
"如何科学地 午睡"
],
"旅行出行": [
"高铁 改签 规则",
"机票 什么时候买 最便宜",
"行李 托运 重量 限制",
"护照 办理 需要多久",
"自驾 川西 路线 推荐",
"云南 旅游 最佳季节",
"西安 三天 行程安排",
"成都 必吃 小吃 推荐",
"三亚 几月份 去最好",
"北京 故宫 预约 攻略",
"杭州 西湖 一日游路线",
"重庆 洪崖洞 怎么去",
"青海湖 环湖 自驾 几天",
"新疆 自驾 注意事项",
"露营 装备 清单 新手",
"徒步 入门 装备 推荐",
"爬山 膝盖保护 技巧",
"民宿 和 酒店 怎么选",
"青旅 安全 注意事项",
"单人旅行 安全 建议",
"签证 面试 常见问题",
"境外 上网 电话卡 推荐",
"旅行 必备 药品",
"行李箱 尺寸 登机 标准",
"自驾游 高速 注意事项",
"山区 开车 技巧",
"景区 门票 网上买 便宜吗",
"淡季 旅游 推荐 目的地",
"适合 带父母 的旅游城市",
"海边 旅游 防晒 攻略"
],
"影视娱乐": [
"电影 推荐 高分 悬疑",
"国产剧 口碑 好的",
"纪录片 推荐 自然",
"动漫 推荐 2026",
"综艺 下饭 推荐",
"美剧 学英语 推荐",
"科幻电影 排行榜",
"经典老片 必看",
"演员 演技 好的 有哪些",
"导演 风格 分析",
"剧透 和 预告 区别",
"原著 和 改编 差别大的影视",
"音乐 制作人 是做什么的",
"吉他 新手 推荐 曲目",
"钢琴 自学 可行性",
"唱歌 跑调 怎么练",
"舞台剧 和 音乐剧 区别",
"演唱会 门票 抢票 技巧",
"电影 票价 为什么 越来越贵",
"影院 imax 和 杜比 区别",
"短视频 剪辑 软件 推荐",
"剪辑 卡点 技巧",
"vlog 拍摄 入门",
"摄影 构图 技巧",
"照片 调色 思路",
"游戏 推荐 单人 剧情",
"主机 和 pc 游戏 哪个好",
"电竞 外设 推荐",
"游戏本 散热 改造",
"独立游戏 推荐 小而美"
],
"学习教育": [
"英语 口语 怎么练",
"背单词 方法 有效",
"考研 数学 复习 规划",
"考公 行测 提高 速度",
"教师资格证 报考 条件",
"普通话 二甲 怎么考",
"雅思 和 托福 区别",
"四六级 听力 技巧",
"python 学习 路线",
"数据分析 入门 学什么",
"产品经理 需要 什么技能",
"如何 做 读书笔记",
"思维导图 怎么用",
"记忆 曲线 复习 安排",
"如何 提高 专注力",
"论文 查重 原理",
"文献 检索 技巧",
"毕业论文 选题 方法",
"考研 复试 准备",
"出国 留学 费用 大概",
"奖学金 申请 条件",
"自学 编程 多久 能就业",
"初学者 练字 方法",
"写作 素材 积累",
"如何 做 时间管理",
"笔记 软件 推荐",
"公开课 推荐 免费",
"在线学习 平台 对比",
"家长 如何 辅导 作业",
"孩子 专注力 培养"
],
"财经理财": [
"基金 定投 怎么开始",
"指数基金 和 主动基金 区别",
"国债 和 存款 哪个划算",
"房贷 等额本息 和 等额本金",
"提前还房贷 划算吗",
"个人养老金 账户 值得开吗",
"公积金 提取 条件",
"社保 断缴 有什么影响",
"个税 专项附加扣除 有哪些",
"年终奖 计税 方式",
"信用卡 免息期 怎么算",
"征信 查询 次数 影响",
"理财产品 风险等级",
"货币基金 会亏吗",
"黄金 投资 方式 对比",
"保险 怎么买 才不踩坑",
"重疾险 和 医疗险 区别",
"存款 利率 对比 2026",
"通货膨胀 怎么 应对",
"记账 方法 推荐",
"家庭 资产 配置 比例",
"股票 开户 流程",
"可转债 是什么",
"市盈率 市净率 怎么看",
"财报 怎么看 三张表",
"公司 估值 方法 入门",
"如何 判断 一家公司 好坏",
"定投 止盈 策略",
"省钱 方法 实用",
"副业 推荐 靠谱"
],
"职场办公": [
"简历 怎么写 吸引人",
"面试 常见问题 回答",
"离职 交接 注意事项",
"试用期 被辞退 有补偿吗",
"劳动合同 到期 不续签 赔偿",
"加班 调休 规定",
"年假 怎么算",
"五险一金 是什么",
"如何 跟 领导 沟通",
"职场 沟通技巧",
"会议纪要 怎么写",
"ppt 排版 技巧",
"excel 常用函数 大全",
"vlookup 和 xlookup 区别",
"数据透视表 怎么用",
"word 排版 论文 格式",
"远程办公 效率 工具",
"项目管理 工具 推荐",
"如何 做 工作汇报",
"职业规划 怎么写",
"转行 需要 准备什么",
"如何 谈 涨薪",
"团队协作 问题 处理",
"工作 压力大 怎么 调节",
"如何 提高 工作 效率",
"跨部门 沟通 技巧",
"邮件 怎么 写 专业",
"商务 礼仪 注意事项",
"如何 做 需求 评审",
"新人 入职 快速 上手"
],
"兴趣爱好": [
"多肉 怎么养 不烂根",
"绿萝 叶子发黄 原因",
"君子兰 养护 技巧",
"阳台 种番茄 方法",
"养猫 新手 准备什么",
"狗狗 训练 定点 排便",
"仓鼠 饲养 注意事项",
"金鱼 多久 换水",
"钓鱼 入门 装备",
"围棋 入门 规则",
"象棋 开局 套路",
"魔方 三阶 还原 公式",
"拼图 收纳 方法",
"手工 皮具 入门",
"陶艺 体验 课程",
"书法 入门 练什么字帖",
"国画 基础 技法",
"水彩 入门 工具",
"烘焙 新手 失败 原因",
"面包 发酵 技巧",
"咖啡 拉花 怎么练",
"调酒 入门 配方",
"露营 天幕 怎么搭",
"骑行 入门 车型 推荐",
"滑板 新手 注意事项",
"羽毛球 拍 推荐",
"乒乓球 胶皮 选择",
"台球 杆法 教学",
"爬山 装备 清单",
"养花 施肥 时间"
],
"汽车驾驶": [
"新车 磨合期 注意什么",
"自动挡 停车 顺序",
"汽车 保养 周期 表",
"轮胎 多久 换一次",
"机油 全合成 半合成 区别",
"刹车片 多久 换",
"电瓶 亏电 怎么 搭电",
"新能源车 冬天 续航 打折",
"充电桩 安装 条件",
"混动 和 纯电 怎么选",
"二手车 怎么 验车",
"车险 怎么买 划算",
"交强险 和 商业险 区别",
"违章 处理 流程",
"驾照 扣分 周期",
"高速 上 爆胎 怎么办",
"雨天 开车 注意 事项",
"夜间 开车 技巧",
"倒车入库 技巧",
"新车 除味 方法",
"汽车 贴膜 有必要吗",
"行车记录仪 怎么选",
"车载 手机支架 推荐",
"洗车 多久 一次",
"玻璃水 怎么加",
"防冻液 能混加吗",
"汽车 异响 常见原因",
"油耗 突然 变高 原因",
"自驾 长途 准备 什么",
"新能源 车牌 怎么申请"
],
"母婴亲子": [
"婴儿 几个月 添加辅食",
"宝宝 发烧 怎么 物理降温",
"新生儿 睡眠 时间 表",
"宝宝 出牙 顺序",
"奶粉 冲泡 水温",
"纸尿裤 尺码 怎么选",
"婴儿 抚触 手法",
"宝宝 湿疹 护理",
"幼儿 急疹 症状",
"小孩 咳嗽 食疗",
"宝宝 补钙 什么时候 开始",
"疫苗接种 时间表",
"幼儿园 分离焦虑 怎么办",
"孩子 发脾气 怎么引导",
"亲子 阅读 推荐 书单",
"儿童 绘本 推荐 3岁",
"孩子 挑食 怎么办",
"儿童 视力 保护",
"孩子 身高 标准 表",
"如何 培养 孩子 独立性",
"二胎 家庭 注意 什么",
"婴儿 车 怎么选",
"安全座椅 怎么安装",
"儿童 房 布置 建议",
"宝宝 洗澡 水温",
"幼儿 感冒 用药 注意",
"孩子 学 什么 兴趣班 好",
"幼小衔接 准备 什么",
"孩子 社交 能力 培养",
"亲子 游戏 推荐"
],
"科普百科": [
"黑洞 是 怎么 形成的",
"为什么 天空 是 蓝色的",
"地球 年龄 是 多少",
"月球 上 有 水吗",
"光速 是 多少",
"为什么 会 有 四季",
"台风 是 怎么 形成的",
"地震 预警 原理",
"为什么 深海 生物 长得 奇怪",
"人类 为什么 会 做梦",
"为什么 有人 天生 卷发",
"左撇子 是 遗传 吗",
"为什么 会 打嗝",
"为什么 会 起 鸡皮疙瘩",
"为什么 猫 会 呼噜",
"蜜蜂 是 怎么 采蜜的",
"树木 能 活 多少年",
"为什么 海水 是 咸的",
"沙漠 里 怎么 找水",
"南极 和 北极 哪个 更冷",
"为什么 会 有 极光",
"世界上最深的海沟",
"世界上 最长 的 河流",
"人类 能 活 多少岁",
"dna 是 什么",
"为什么 会 有 指纹",
"疫苗 是 怎么 起作用 的",
"为什么 抗生素 不能 乱吃",
"人工智能 是 怎么 学习 的",
"量子计算 是 什么 意思"
],
"社会话题": [
"为什么 现在 年轻人 不 愿意 结婚",
"当代 年轻人 的 消费观",
"怎么 看待 居家办公",
"远程工作 会不会 成为 主流",
"为什么 大家 都 在 考公",
"如何 面对 同龄人 压力",
"怎么 和 父母 沟通 观念 冲突",
"社交 恐惧 怎么 缓解",
"如何 建立 边界感",
"成年人 如何 交朋友",
"独居 生活 注意 什么",
"如何 存下 第一笔钱",
"工作 和 生活 怎么 平衡",
"如何 应对 职业 倦怠",
"什么是 断舍离",
"极简主义 生活方式",
"如何 减少 无效 社交",
"怎么 培养 长期 爱好",
"如何 保持 学习 习惯",
"怎样 才算 好好 生活",
"情绪 稳定 是 怎么 做到 的",
"如何 应对 信息 焦虑",
"手机 使用 时间 管理",
"如何 戒掉 熬夜",
"如何 提升 审美",
"怎么 找到 自己 的 节奏",
"普通人 如何 做 长期 规划",
"如何 面对 失败",
"如何 培养 批判性 思维",
"怎么 做 一个 靠谱 的人"
]
};
const ALL_TERMS = Object.keys(TERM_BANK).reduce(function (acc, k) {
return acc.concat(TERM_BANK[k]);
}, []);
// ---------------------------------------------------------------- 工具
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
function randInt(a, b) {
return a + Math.floor(Math.random() * (b - a + 1));
}
function todayStr() {
const d = new Date();
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
function nowStr() {
const d = new Date();
return String(d.getHours()).padStart(2, '0') + ':' +
String(d.getMinutes()).padStart(2, '0') + ':' +
String(d.getSeconds()).padStart(2, '0');
}
function shuffle(arr) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const t = a[i]; a[i] = a[j]; a[j] = t;
}
return a;
}
async function waitFor(fn, timeout, step) {
timeout = timeout || 10000;
step = step || 200;
const t0 = Date.now();
while (Date.now() - t0 < timeout) {
let v = null;
try { v = fn(); } catch (e) { /* 忽略 */ }
if (v) return v;
await sleep(step);
}
return null;
}
// ---------------------------------------------------------------- 状态
function defaultState() {
return {
running: false,
date: todayStr(),
done: 0,
// 当日实际目标次数,由 minTarget~maxTarget 随机摇出,每天摇一次
target: 13,
targetDate: '',
minTarget: 5,
maxTarget: 20,
minDelay: 4,
maxDelay: 11,
autoDaily: false,
queue: [],
cursor: 0,
lastTerm: '',
lastTickAt: 0,
lastDoneAtTick: -1,
rapid: 0,
log: []
};
}
function loadState() {
let raw = null;
try { raw = GM_getValue(STORE_KEY, null); } catch (e) { raw = null; }
if (!raw) return defaultState();
let s = null;
try { s = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch (e) { s = null; }
if (!s || typeof s !== 'object') return defaultState();
// 补齐缺失字段,兼容旧版本存档
const d = defaultState();
Object.keys(d).forEach(function (k) {
if (s[k] === undefined || s[k] === null) s[k] = d[k];
});
if (!Array.isArray(s.queue)) s.queue = [];
if (!Array.isArray(s.log)) s.log = [];
return s;
}
function saveState(s) {
try { GM_setValue(STORE_KEY, JSON.stringify(s)); } catch (e) { /* 忽略 */ }
}
function pushLog(s, msg) {
s.log.push(nowStr() + ' ' + msg);
if (s.log.length > MAX_LOG) s.log = s.log.slice(-MAX_LOG);
}
/** 在 minTarget~maxTarget 之间摇一个当日目标次数 */
function rollDailyTarget(s) {
const lo = Math.min(s.minTarget, s.maxTarget);
const hi = Math.max(s.minTarget, s.maxTarget);
s.target = randInt(lo, hi);
s.targetDate = todayStr();
return s.target;
}
/**
* 每天保证有一个目标次数。
* 同一天内不重复摇——否则每次启动都换目标,进度会显得莫名其妙。
*/
function ensureDailyTarget(s) {
if (s.targetDate !== todayStr()) {
rollDailyTarget(s);
return true;
}
return false;
}
// ---------------------------------------------------------------- 面板
let ui = null; // { host, shadow, els... }
function buildPanel(state) {
const host = document.createElement('div');
host.id = PANEL_ID;
host.style.cssText =
'position:fixed;right:16px;bottom:16px;z-index:2147483647;' +
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif;';
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = [
'',
'
',
'
',
' Bing 自动搜索',
' ',
'
',
'
',
'
0 / 13
',
'
',
'
未开始
',
'
',
' ',
' ',
' \u2013 ',
'
',
'
',
' ',
' ',
' \u2013 ',
'
',
'
',
' ',
' ',
'
',
'
',
' ',
' ',
' ',
'
',
'
',
'
',
'
',
'
'
].join('\n');
(document.documentElement || document.body).appendChild(host);
const q = function (id) { return shadow.getElementById(id); };
ui = {
host: host,
shadow: shadow,
dot: q('dot'),
prog: q('prog'),
bar: q('bar'),
status: q('status'),
minTarget: q('minTarget'),
maxTarget: q('maxTarget'),
minDelay: q('minDelay'),
maxDelay: q('maxDelay'),
autoDaily: q('autoDaily'),
start: q('start'),
stop: q('stop'),
reset: q('reset'),
log: q('log'),
note: q('note'),
body: q('bd')
};
// --- 事件绑定 ---
ui.start.addEventListener('click', function () { startRun(); });
ui.stop.addEventListener('click', function () {
const s = loadState();
s.running = false;
pushLog(s, '已手动停止');
saveState(s);
render(s, '已停止');
});
ui.reset.addEventListener('click', function () {
GM_deleteValue(STORE_KEY);
const s = defaultState();
saveState(s);
render(s, '已重置');
});
q('min').addEventListener('click', function () {
const hidden = ui.body.style.display === 'none';
ui.body.style.display = hidden ? '' : 'none';
q('min').textContent = hidden ? '\u2014' : '\u25A1';
});
ui.autoDaily.addEventListener('change', function () {
const s = loadState();
s.autoDaily = ui.autoDaily.checked;
saveState(s);
render(s);
});
[ui.minTarget, ui.maxTarget, ui.minDelay, ui.maxDelay].forEach(function (el) {
el.addEventListener('change', function () {
const s = loadState();
const v = function (x, lo, hi, dft) {
const n = parseInt(x.value, 10);
return (isNaN(n) || n < lo || n > hi) ? dft : n;
};
const oldMin = s.minTarget;
const oldMax = s.maxTarget;
s.minTarget = v(ui.minTarget, 1, 450, 5);
s.maxTarget = v(ui.maxTarget, 1, 450, 20);
if (s.minTarget > s.maxTarget) {
const t = s.minTarget; s.minTarget = s.maxTarget; s.maxTarget = t;
}
s.minDelay = v(ui.minDelay, 1, 120, 4);
s.maxDelay = v(ui.maxDelay, 1, 300, 11);
if (s.minDelay > s.maxDelay) {
const t = s.minDelay; s.minDelay = s.maxDelay; s.maxDelay = t;
}
// 次数范围变了就重摇当日目标,让改动立刻可见。
// 正在跑的话保留已完成的进度,只换目标值。
if (oldMin !== s.minTarget || oldMax !== s.maxTarget) {
rollDailyTarget(s);
if (!s.running) s.done = 0;
pushLog(s, '次数范围 ' + s.minTarget + '~' + s.maxTarget +
',今日目标重设为 ' + s.target + ' 次');
}
saveState(s);
render(s);
});
});
}
function render(s, statusOverride) {
if (!ui) return;
ui.dot.className = 'dot' + (s.running ? ' on' : '');
ui.prog.textContent = s.done + ' / ' + s.target;
ui.prog.title = '今日目标 ' + s.target + ' 次(在 ' +
s.minTarget + '~' + s.maxTarget + ' 之间随机,每天摇一次)';
const pct = s.target > 0 ? Math.min(100, (s.done / s.target) * 100) : 0;
ui.bar.style.width = pct + '%';
ui.bar.style.background = s.done >= s.target ? '#1a7f37' : '#0969da';
// 输入框只在未聚焦时同步,免得用户打字打到一半被覆盖
if (document.activeElement !== ui.minTarget) ui.minTarget.value = s.minTarget;
if (document.activeElement !== ui.maxTarget) ui.maxTarget.value = s.maxTarget;
if (document.activeElement !== ui.minDelay) ui.minDelay.value = s.minDelay;
if (document.activeElement !== ui.maxDelay) ui.maxDelay.value = s.maxDelay;
ui.autoDaily.checked = !!s.autoDaily;
let text = statusOverride;
if (!text) {
if (s.running) {
text = s.lastTerm ? ('搜索中… 「' + s.lastTerm + '」') : '准备中…';
} else if (s.done >= s.target && s.target > 0) {
text = '今日已完成 ' + s.done + ' 次(今日目标 ' + s.target + ')';
} else {
text = '未开始 · 今日目标 ' + s.target + ' 次';
}
}
ui.status.textContent = text;
ui.status.title = text;
ui.start.textContent = s.running ? '重新开始' : '开始';
ui.start.className = 'act go';
ui.stop.disabled = !s.running;
ui.stop.style.opacity = s.running ? '1' : '.5';
ui.log.textContent = s.log.slice(-MAX_LOG).join('\n');
const host = location.hostname;
const isCn = host.indexOf('cn.bing.com') >= 0;
ui.note.textContent = isCn
? 'cn.bing.com(中国区,搜索照常执行)'
: host;
}
// ---------------------------------------------------------------- 启动/停止
function startRun() {
const s = loadState();
if (!ALL_TERMS.length) {
render(s, '词库为空,无法运行');
return;
}
// 每次启动都重新洗牌,避免每天都是同一批词、同一顺序
s.queue = shuffle(ALL_TERMS);
s.cursor = 0;
s.done = 0;
s.date = todayStr();
ensureDailyTarget(s); // 新的一天就摇一个新的目标次数
s.running = true;
s.lastTerm = '';
s.rapid = 0;
s.log = [];
pushLog(s, '开始 · 今日目标 ' + s.target + ' 次(' +
s.minTarget + '~' + s.maxTarget + ' 随机)');
pushLog(s, '延迟 ' + s.minDelay + '~' + s.maxDelay + ' 秒');
saveState(s);
render(s, '准备中…');
// 当前页面直接接着跑,不用等重载
tick('start');
}
// ---------------------------------------------------------------- 搜索
function findSearchBox() {
return document.querySelector('#sb_form_q') ||
document.querySelector('input[name="q"][type="text"]') ||
document.querySelector('textarea[name="q"]') ||
document.querySelector('input[name="q"]');
}
function findSearchForm() {
return document.querySelector('#sb_form') ||
document.querySelector('form[action*="/search"]');
}
/** 关掉可能挡住操作的 cookie / 地区提示框 */
function dismissDialogs() {
const texts = ['接受', '同意', '我知道了', 'Accept', 'OK', '确定'];
const btns = document.querySelectorAll('button, a[role="button"], input[type=button]');
for (let i = 0; i < btns.length; i++) {
const b = btns[i];
const label = (b.textContent || b.value || '').trim();
if (label && texts.indexOf(label) >= 0 && b.offsetParent !== null) {
try { b.click(); } catch (e) { /* 忽略 */ }
return true;
}
}
return false;
}
/** 把词填进搜索框并提交。返回是否成功提交 */
async function submitSearch(term) {
const box = findSearchBox();
const form = findSearchForm();
if (box && form) {
try {
box.focus();
box.click();
// 逐字符输入,比直接赋值更像真人
if (box.tagName === 'INPUT' || box.tagName === 'TEXTAREA') {
box.value = '';
const chars = Array.from(term);
for (let i = 0; i < chars.length; i++) {
box.value += chars[i];
box.dispatchEvent(new Event('input', { bubbles: true }));
box.dispatchEvent(new KeyboardEvent('keydown', {
key: chars[i], bubbles: true
}));
box.dispatchEvent(new KeyboardEvent('keyup', {
key: chars[i], bubbles: true
}));
await sleep(randInt(45, 150));
}
} else {
box.value = term;
}
box.dispatchEvent(new Event('input', { bubbles: true }));
box.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(randInt(250, 700));
// 优先走回车键,让 Bing 自己的逻辑接管
box.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true
}));
box.dispatchEvent(new KeyboardEvent('keypress', {
key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true
}));
box.dispatchEvent(new KeyboardEvent('keyup', {
key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true
}));
await sleep(700);
if (hasNavigated(term)) return 'enter';
// 回车没生效就自己提交表单
if (typeof form.requestSubmit === 'function') {
form.requestSubmit();
} else {
form.submit();
}
await sleep(900);
if (hasNavigated(term)) return 'form';
// 还是不跳,直接改地址栏,保证一定发生导航
hardNavigate(term);
return 'hard';
} catch (e) {
// 掉到下面的 fallback
}
}
// 兜底:直接跳结果页
hardNavigate(term);
return 'fallback';
}
/**
* 判断是否已经跳到本次搜索的结果页。
* 只看 q 参数是否等于本次的词——不能用「路径是 /search」来判断,
* 因为在结果页上搜第二个词时路径本来就是 /search,那样会误判成
* 「已经跳转」,导致后续搜索被跳过。
*/
function hasNavigated(term) {
try {
const q = new URLSearchParams(location.search).get('q');
return norm(q) === norm(term);
} catch (e) {
return false;
}
}
function norm(s) {
return String(s || '').replace(/\s+/g, ' ').trim();
}
/** 兜底:直接跳结果页,确保一定发生导航 */
function hardNavigate(term) {
location.href = location.origin + '/search?q=' + encodeURIComponent(term);
}
let watchdogTimer = 0;
/**
* 提交后的看门狗。
*
* 正常流程里提交会引发整页导航,本页面的定时器随文档一起销毁,不会触发。
* 但如果 Bing 换成前端路由(地址变了但不重新加载文档),脚本就没有机会
* 再被注入,整个流程会停在原地——表现就是「搜了一两次之后不动了」。
* 这里兜底把流程接下去。
*/
function armWatchdog(term) {
clearTimeout(watchdogTimer);
watchdogTimer = setTimeout(function () {
let s;
try { s = loadState(); } catch (e) { return; }
if (!s.running || s.done >= s.target) return;
if (hasNavigated(term)) {
// 地址已更新但文档没重载 —— 直接推进下一轮
pushLog(s, '页面未重载,主动继续下一轮');
saveState(s);
tick('watchdog');
} else {
// 连地址都没变,说明提交彻底失败 —— 强制跳转
pushLog(s, '提交未生效,强制跳转');
saveState(s);
hardNavigate(term);
}
}, 9000);
}
/** 一次推进:等待 → 选词 → 提交 */
async function tick(reason) {
const s = loadState();
// 日期变了就重置当日计数,并摇新的目标次数
if (s.date !== todayStr()) {
s.date = todayStr();
s.done = 0;
ensureDailyTarget(s);
saveState(s);
}
// 防跑飞:只有「页面极快重载」且「计数没有推进」同时成立才判定为空转。
// 正常流程每次 tick 都会让 done 加一,不会误判。
const now = Date.now();
const tooFast = s.lastTickAt > 0 && (now - s.lastTickAt) < 1500;
const stalled = s.lastDoneAtTick === s.done;
if (tooFast && stalled) {
s.rapid += 1;
} else {
s.rapid = 0;
}
s.lastTickAt = now;
s.lastDoneAtTick = s.done;
if (s.rapid >= 4) {
s.running = false;
pushLog(s, '检测到页面空转(快速重载且无进展),已自动停止');
saveState(s);
render(s, '已自动停止(页面空转)');
return;
}
if (!s.running) {
saveState(s);
render(s);
return;
}
if (s.done >= s.target) {
s.running = false;
pushLog(s, '完成 ' + s.done + ' 次搜索');
saveState(s);
render(s, '今日完成 ' + s.done + ' 次');
return;
}
// 随机延迟,集中在区间中段附近
const lo = s.minDelay * 1000;
const hi = Math.max(lo, s.maxDelay * 1000);
const mid = (lo + hi) / 2;
const spread = Math.max(1, (hi - lo) / 2);
let wait = 0;
for (let i = 0; i < 6; i++) { wait += (Math.random() + Math.random() - 1) * spread; }
wait = Math.max(lo, Math.min(hi, mid + wait / 3));
render(s, '等待 ' + (wait / 1000).toFixed(1) + ' 秒后搜索…');
await sleep(wait);
// 延迟期间用户可能点了停止
const fresh = loadState();
if (!fresh.running) { render(fresh); return; }
s.running = fresh.running;
s.rapid = fresh.rapid;
if (!s.queue.length) s.queue = shuffle(ALL_TERMS);
if (s.cursor >= s.queue.length) s.cursor = 0;
const term = s.queue[s.cursor];
s.cursor += 1;
s.done += 1;
s.lastTerm = term;
pushLog(s, '(' + s.done + ') ' + term);
saveState(s);
render(s);
dismissDialogs();
const how = await submitSearch(term);
// 正常路径是 enter(回车)。走到 form/hard/fallback 说明页面结构有变化,
// 记一笔,免得下次排查时只能靠猜。
if (how === 'hard' || how === 'fallback') {
pushLog(s, '未走正常跳转(' + how + '),已强制跳转');
saveState(s);
render(s);
}
armWatchdog(term);
}
// ---------------------------------------------------------------- 入口
/** Bing 首屏加载后会重建 DOM,面板可能被冲掉,定期检查补挂 */
function keepPanelAlive() {
setInterval(function () {
if (!ui || !ui.host || ui.host.isConnected) return;
const root = document.documentElement || document.body;
if (root) root.appendChild(ui.host);
}, 1500);
}
function boot() {
// Bing 页面里也有 iframe,只在顶层文档建面板和执行
if (window.top !== window.self) return;
if (!document.documentElement) {
setTimeout(boot, 50);
return;
}
let s = loadState();
// 日期变了先把当日计数归零,并摇一个新的目标次数
if (s.date !== todayStr()) {
s.date = todayStr();
s.done = 0;
}
ensureDailyTarget(s);
saveState(s);
buildPanel(s);
render(s);
keepPanelAlive();
try {
GM_registerMenuCommand('开始自动搜索', function () { startRun(); });
GM_registerMenuCommand('停止', function () {
const t = loadState();
t.running = false;
pushLog(t, '已通过菜单停止');
saveState(t);
render(t, '已停止');
});
GM_registerMenuCommand('今日重置', function () {
const t = loadState();
t.done = 0;
t.running = false;
t.log = [];
saveState(t);
render(t, '已重置');
});
} catch (e) { /* 部分管理器不支持,忽略 */ }
if (s.running) {
tick('page-load');
} else if (s.autoDaily && s.done < s.target) {
// 勾了「每天自动跑」:当天打开 Bing 就自己开始
startRun();
}
}
if (document.documentElement) {
boot();
} else if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot, { once: true });
} else {
boot();
}
})();