查看原文
科技

Noi:Bard 变更为 Gemini,插件失效怎么办?

lencx 浮之静 2024-02-11

这是一篇偏编程类干货,如果你想将 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 输入和批量提问功能的核心插件:

  1. 将插件(脚本:noi-ask/main.js)注入网站,通过获取输入框信息,来实现同步输入和发送的方法。

  2. 通过调用脚本实现的方法将输入内容同步到网页的输入框中。

😅

对这部分内容不感兴趣的可以跳过,直接看“修复 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 已经修复了这个问题,但无法直接将插件应用到本地,需要用户手动对插件进行覆盖。请操作以下步骤:

  1. 打开链接:https://download-directory.github.io

  2. 复制链接到输入框 https://github.com/lencx/Noi/tree/main/extensions/noi-ask 后,按下回车键。

  3. 找到下载的文件压缩包,解压后重命名为 noi-ask

📌 Download directory

因为 GitHub 本身并不支持特定文件夹下载功能,需要借助 https://download-directory.github.io 开源项目来实现。如果频繁操作会导致失败,则需要添加自己的 GitHub Token 来进行操作,这一部分就不做展开了,可以查看其文档。

如果不想使用下载文件夹的方式,也可以将代码直接复制到本地文件中对其进行覆盖,为了避免遗漏,建议将此插件目录下的文件全部复制一遍。

Step 2:添加插件

  1. 打开设置窗口(侧栏左下角,或使用快捷键:macOS Cmd+,,Windows Ctrl+,),找到插件(Extensions)菜单,进入后点击添加插件按钮。

  2. 直接添加插件如果出现错误提示”插件已存在“,则需要先对插件进行删除后,再次尝试添加,添加完成后会看到版本(Version)变化。

  3. 添加完成后,关闭设置窗口,回到主界面。

Step 3:更新侧栏

这时直接回到主界面进行尝试,还是无法实现 prompt 的输入和发送,这是因为插件会与侧栏 URL 进行匹配,如果 URL 不一致,插件仍无法正常工作。点击右下角同步链接,重新更新侧栏即可。

Step 4:开始使用

完成侧栏链接编辑或更新后,需要重新切换侧栏链接进入 Gemini 来确保插件可以正常工作(理论上不需要重启应用)。使用技巧:

  • Prompt 指令:

  1. 使用 / 进入搜索模式

  2. 使用键盘 键可以快速选择

  3. 使用 ⇥ 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

    [1]

    Noi Extensions: https://github.com/lencx/Noi/tree/main/extensions#noi-extensions

    继续滑动看下一个

    Noi:Bard 变更为 Gemini,插件失效怎么办?

    lencx 浮之静
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存