Noi:Bard 变更为 Gemini,插件失效怎么办?
这是一篇偏编程类干货,如果你想将 Noi 作为真正的生产力工具或想开发自己的插件,建议收藏细读,也可以配合 ChatGPT 之类的工具来辅助理解。未来插件管理更新方式可能会发生变化,但插件运行机制不会有太大变化。
最近 Google Bard 更名为 Gemini,域名也从 bard.google.com
更新到了 gemini.google.com
。这一系列更新直接导致 Noi 的 Prompt 输入失效,本文除了会给出具体修复方案,也会为大家聊聊 Noi 的插件机制(如果还未下载 Noi,可以看这篇文章:Noi:跨平台定制化浏览器,最得力 AI 助手)。
Noi 插件
Noi 插件是 Chrome Extensions 的子集,但它并不支持 Chrome Extension 的所有 API(了解更多:Noi Extensions[1])。
要解决 Prompt 输入不生效的问题并不难,但为了大家能够更好地理解和使用 Noi,下面会简单介绍一下 Noi 插件的工作原理。以 @noi/ask
插件为例,它是 Prompt 输入和批量提问功能的核心插件:
将插件(脚本:
noi-ask/main.js
)注入网站,通过获取输入框信息,来实现同步输入和发送的方法。通过调用脚本实现的方法将输入内容同步到网页的输入框中。
😅对这部分内容不感兴趣的可以跳过,直接看“修复 Gemini”部分。
插件详解
以 @noi/ask
插件为例(其他插件类似),主要包含三个文件:
[noi-ask] # 文件夹名称
|- main.js # 核心逻辑脚本
|- manifest.json # 包含扩展的基本信息和如何控制其行为的配置
`- README.md # 插件介绍
manifest.json
Noi 插件的 manifest.json 结构与 Chrome Extension manifest.json 类似:
{
"manifest_version": 3,
"name": "@noi/ask", // 插件名称
"version": "0.1.3", // 插件版本
"homepage": "https://github.com/lencx/Noi/tree/main/extensions/noi-ask", // 插件链接
"description": "The best assistant for batch asking and quick typing of prompts.", // 插件描述
"content_scripts": [
{
// 按以下规则匹配,将脚本注入特定网站
"matches": [
"https://chat.openai.com/*",
"https://gemini.google.com/*",
"https://poe.com/*",
"https://claude.ai/*",
"https://huggingface.co/chat/*",
"https://www.perplexity.ai/*",
"https://copilot.microsoft.com/*",
"https://pi.ai/talk/*",
"https://www.coze.com/home/*",
"https://you.com/*",
"https://www.coze.cn/home/*",
"https://chatglm.cn/*",
"https://www.doubao.com/*",
"https://tongyi.aliyun.com/qianwen/*"
],
// 插件包含的脚本文件,可以是多个
"js": ["main.js"],
// 脚本的注入时机,可选值:document_start | document_end | document_idle
"run_at": "document_end",
// 脚本执行环境:ISOLATED | MAIN
"world": "MAIN"
}
]
}
main.js
当 AI 网站的域名变更,布局更新都可能会导致插件代码失效。以下截取 @noi/ask
插件部分代码进行注解,完整代码可在 https://github.com/lencx/Noi/blob/main/extensions/noi-ask
中查看:
/**
* NoiAsk: 用于向 AI 聊天平台批量发送消息
*/
// 定义 NoiAsk 类
class NoiAsk {
// 静态方法 sync,用于在页面的 textarea 中输入消息
static sync(message) {
// 获取页面中的输入框(textarea)元素
const inputElement = document.querySelector('textarea');
if (inputElement) {
// 获取并调用原生 textarea 的 value 属性的 setter
const nativeTextareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
nativeTextareaSetter.call(inputElement, message);
// 创建并派发一个 input 事件来模拟用户输入
const inputEvent = new InputEvent('input', {
bubbles: true, // 事件冒泡
cancelable: true, // 事件可以取消
});
inputElement.dispatchEvent(inputEvent);
}
}
// 静态方法 simulateUserInput,用于模拟用户输入
static simulateUserInput(element, text) {
// 创建一个 input 事件
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
});
// 聚焦到元素,设置值,并派发 input 事件
element.focus();
element.value = text;
element.dispatchEvent(inputEvent);
}
// 静态方法 autoClick,用于自动点击按钮
static autoClick(btn) {
btn.focus(); // 聚焦按钮
btn.disabled = false; // 确保按钮可点击
btn.click(); // 点击按钮
}
}
// 定义 OpenAIAsk 类,继承自 NoiAsk
class OpenAIAsk extends NoiAsk {
static name = 'ChatGPT'; // 无实际意义,可随意修改
// 注意:url 十分重要,它是用来匹配批量发送的关键,与侧栏中的 URL 一致
// 可参考:https://github.com/lencx/Noi/blob/main/configs/noi.mode.json
static url = 'https://chat.openai.com';
// 静态方法 submit,用于在 ChatGPT 平台上提交消息
static submit() {
// 选择页面上的发送按钮
const btn = document.querySelector('button[data-testid="send-button"]');
if (btn) this.autoClick(btn); // 如果找到按钮,则自动点击
}
}
// 定义 GeminiAsk 类,继承自 NoiAsk
class GeminiAsk extends NoiAsk {
static name = 'Gemini';
static url = 'https://gemini.google.com';
// 重写 sync 方法,用于在 Gemini 平台上输入消息
static sync(message) {
// 选择页面上的输入元素
const inputElement = document.querySelector('.ql-editor.textarea');
if (inputElement) {
// 创建一个 input 事件
const inputEvent = new Event('input', { bubbles: true });
// 设置值并派发事件
inputElement.value = message;
inputElement.dispatchEvent(inputEvent);
// Gemini 平台的特殊处理
inputElement.querySelector('p').textContent = message;
}
}
// 重写 submit 方法,用于在 Gemini 平台上提交消息
static submit() {
// 选择页面上的发送按钮
const btn = document.querySelector('button[aria-label*="Send message"]');
if (btn) {
// 设置按钮为可用状态并聚焦,然后点击
btn.setAttribute('aria-disabled', 'false');
btn.focus();
btn.click();
}
}
}
// 将 OpenAIAsk 和 GeminiAsk 类挂载到 window.NoiAsk 上,以便在全局范围内访问
// Noi 会在内部完成对 window.NoiAsk 方法的调用
window.NoiAsk = {
OpenAIAsk,
GeminiAsk,
};
支持自定义
目前 Noi Ask
(批量提问、Prompt 输入)维护了两个插件:@noi/ask
和 @noi/ask-custom
。@noi/ask
会持续进行更新,以满足更多主流 AI 的需要,但针对目前市面上层出不穷的 AI,它是心有余而力不足。这时就需要大家来发挥自己的 DIY 能力,对其进行扩展了。但不建议直接修改 @noi/ask
中的代码,软件更新随时会被覆盖。@noi/ask-custom
是专门为用户自定义扩展预留的插件,在这里你可以实现自己的逻辑,或贡献给 Noi,让更多人受益(贡献方式:向 https://github.com/lencx/Noi/tree/main/extensions
发起 PR)。
@noi/ask-custom
插件与 @noi/ask
代码结构相似,这里就不过多介绍了,以下是部分代码注解:
/**
* NoiAskCustom: 用于向 AI 聊天平台批量发送消息
*/
// 基础类,如果不满足要求,则重写内部方法
class NoiAskCustom {
static sync(message) {
const inputElement = document.querySelector('textarea');
if (inputElement) {
const nativeTextareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
nativeTextareaSetter.call(inputElement, message);
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
});
inputElement.dispatchEvent(inputEvent);
}
}
static simulateUserInput(element, text) {
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
});
element.focus();
element.value = text;
element.dispatchEvent(inputEvent);
}
static autoClick(btn) {
btn.focus();
btn.disabled = false;
btn.click();
}
}
// TODO: 实现自定义 Ask
// TODO: 在 manifest.json `content_scripts.matches` 中添加需要匹配的 URL
// 可参考: https://github.com/lencx/Noi/tree/main/extensions/noi-ask
class MyAsk extends NoiAskCustom {
// TODO: AI 名称
static name = 'MyChat';
// TODO: AI URL(一般为可直接打开 AI 应用的最短 URL 路径,不要定位到某一个具体聊天记录)
static url = '';
// TODO: 如果 NoiAskCustom 中的 sync (输入)不满足需要,则对其进行覆盖
static sync() {
// code...
}
// TODO: 如果 NoiAskCustom 中的 submit (发送)不满足需要,则对其进行覆盖
static submit() {
// code...
}
}
// 将自定义类注册到 window 中,供 Noi 内部调用
window.NoiAsk = {
...window.NoiAsk || {},
// TODO: 添加自定义类到这里
MyAsk,
};
修复 Gemini
目前 Noi 处于早期阶段,还未实现完整的插件检测更新机制,需要用户手动来更新,在未来会简化这一操作流程。
Step 1:下载插件
目前 @noi/ask
已经修复了这个问题,但无法直接将插件应用到本地,需要用户手动对插件进行覆盖。请操作以下步骤:
打开链接:
https://download-directory.github.io
复制链接到输入框
https://github.com/lencx/Noi/tree/main/extensions/noi-ask
后,按下回车键。找到下载的文件压缩包,解压后重命名为
noi-ask
。
📌 Download directory因为 GitHub 本身并不支持特定文件夹下载功能,需要借助
https://download-directory.github.io
开源项目来实现。如果频繁操作会导致失败,则需要添加自己的 GitHub Token 来进行操作,这一部分就不做展开了,可以查看其文档。如果不想使用下载文件夹的方式,也可以将代码直接复制到本地文件中对其进行覆盖,为了避免遗漏,建议将此插件目录下的文件全部复制一遍。
Step 2:添加插件
打开设置窗口(侧栏左下角,或使用快捷键:macOS
Cmd+,
,WindowsCtrl+,
),找到插件(Extensions)菜单,进入后点击添加插件按钮。直接添加插件如果出现错误提示”插件已存在“,则需要先对插件进行删除后,再次尝试添加,添加完成后会看到版本(Version)变化。
添加完成后,关闭设置窗口,回到主界面。
Step 3:更新侧栏
这时直接回到主界面进行尝试,还是无法实现 prompt 的输入和发送,这是因为插件会与侧栏 URL 进行匹配,如果 URL 不一致,插件仍无法正常工作。点击右下角同步链接,重新更新侧栏即可。
Step 4:开始使用
完成侧栏链接编辑或更新后,需要重新切换侧栏链接进入 Gemini 来确保插件可以正常工作(理论上不需要重启应用)。使用技巧:
Prompt 指令:
使用
/
进入搜索模式使用键盘
↑
和↓
键可以快速选择使用
⇥ Tab
键来确认选择
发送消息:macOS Cmd + Enter
,Windows Ctrl + Enter
,可以在设置中进行快捷键编辑。
侧栏
默认侧栏会加载主流 AI,为了满足多元化需要,侧栏同步链接支持自定义。目前 Noi 维护了两个 AI 链接同步。
主流 AI 及主流社区:
https://github.com/lencx/Noi/blob/main/configs/noi.mode.json
主流 AI 及国内 AI:
https://github.com/lencx/Noi/blob/main/configs/noi.mode.cn.json
目前国内 AI 已经部分实现了 Prompt 输入功能(如:扣子、豆包、智谱清言、通义千问)。
📌 侧栏同步同步操作只会覆盖默认 URL,对用户自定义添加的 URL 不会进行覆盖,请放心操作。
References
Noi Extensions: https://github.com/lencx/Noi/tree/main/extensions#noi-extensions