screenshot.png

1. 摘要

「AI聚合问答工具」是一款桌面客户端,核心目标是把多个主流大模型网页并排装进同一个窗口:用户在底部输入框写一次问题,点一下即可同步发送给所有面板,并排对比各家回答。它不调用任何大模型 API,而是复用各站点的网页登录态,以 webview 嵌入真实对话框;强调本地优先、无遥测、数据仅存本机。本文从问题定义、技术选型、系统架构、核心模块、工程化交付与关键问题六个维度,复盘这一项目的实践过程。


2. 问题定义与项目目标

大模型能力高度分化:有的擅长推理,有的擅长长文,有的联网检索更强。用户日常需要在多个站点之间反复复制粘贴、来回切换,既低效又难以横向对比。本项目据此设定四项目标:

  • 一窗并排,一问多答:同一窗口内并排打开 2–12 个 AI 面板。
  • 一次输入,全部发送:一个输入框,一键广播给所有面板。
  • 复用网页、本地优先:复用各 AI 网页版登录态,不索取 API Key;登录态与配置存于本机。
  • 隐私友好:无统计、无追踪、无云端上报。


3. 技术选型

维度 选择 理由
壳层框架 **Electron 44** 原生支持 `webview` 嵌入任意站点、分区持久化(`partition`)、`nativeTheme` 系统级色彩联动,最契合"聚合网页"的场景。
打包工具 **electron-builder 26** 统一产出 Windows(NSIS/便携)与 macOS(dmg),支持代码签名与 CI。
图标生成 **sharp 0.35** 用一条命令把矢量 `icon.svg` 栅格化为 1024×1024 `icon.png`,纳入构建流程。
聚合方式 **网页注入**,非 API 聚合 调用 API 需各平台 Key、计费且接口频繁变动;直接嵌入官方网页可零成本、零授权地复用其完整能力。

关键判断:不重写对话 UI,而是"借用"各站点的对话 UI。这把工作量从"适配每个模型的 API"转移为"定位每个站点的输入框与发送按钮"。


4. 系统架构

4.1 进程与目录结构

src/
  main/        主进程:窗口、会话、主题联动、版本检测、本地存储
    main.js
    store.js
  preload/     安全桥接(contextIsolation)
    preload.js
  renderer/    界面、布局、站点适配、注入、提示词库
    index.html  app.js  styles.css
    providers.js  layouts.js  inject.js  prompts.js
scripts/       图标生成、应用截图、自检脚本
build/         图标与 README 截图(icon.png / screenshot.png)
assets/        矢量图标(icon.svg)
`</pre>

### 4.2 进程模型与 IPC

采用 Electron 推荐的**主进程 / 预加载 / 渲染进程**三层隔离:
`</pre>
  • 预加载(preload.js) 通过 contextBridge 只暴露最小 API 集合,绝不开启 nodeIntegration:
    `contextBridge.exposeInMainWorld('api', {
      storeGet:  (k, f) => ipcRenderer.invoke('store:get', k, f),
      storeSet:  (k, v) => ipcRenderer.invoke('store:set', k, v),
      openExternal: (url) => ipcRenderer.invoke('open-external', url),
      setNativeTheme: (mode) => ipcRenderer.invoke('set-native-theme', mode),
      onUpdateAvailable: (cb) => ipcRenderer.on('update-available', (_e, i) => cb(i))
    });
    `
  • 主进程(main.js) 用 ipcMain.handle 实现这些通道;其中 open-external 强制校验 ^https?://,避免任意协议跳转。

    * * *

    5. 核心模块实现

    5.1 多面板并排与 22 种布局

    每个面板是一个 webview,用 partition=&amp;quot;persist:&amp;lt;providerId&amp;gt;&amp;quot; 实现登录态隔离与重启存活:
    `function createWebview(provider) {
      var wv = document.createElement('webview');
      wv.setAttribute('partition', 'persist:' + provider.id);
      wv.setAttribute('src', provider.url);
      wv.className = 'pane-web';
      return wv;
    }
    `
    布局由 layouts.js 以 0–1 归一化坐标描述,共 22 种:2 屏(左右/上下)→ 3 屏(三栏/主从)→ … → 12 屏(3×4 / 4×3),涵盖网格、左右/上下、主从(一大带多小)等形态,渲染时映射为百分比定位。

    切换布局不刷新对话是整个交互的关键。实现要点:

  • paneEls 缓存已创建的窗格实例;
  • relayout() 增删窗格后,用 syncPaneProvider() 判断——仅当某窗格的 provider 变化时才重建 webview,否则原网页原样保留;
  • 切布局时统一复位 is-fullscreen / is-collapsed,避免与新的几何位置冲突。

    5.2 一次输入、全发送的站点适配

    providers.js 为每个站点维护 inputs(输入框候选选择器)与 send(发送按钮候选选择器)。真正执行在 inject.js——一段注入到目标页面隔离世界的脚本,全程只读写该标签页 DOM、不触网:
  1. 定位输入框:先按站点专属选择器匹配,未命中则回退通用发现——在 textarea / contenteditable / input[type=text] 中按可见性、位置(偏下方)、宽度、placeholder 关键词打分取最优。
  2. 框架感知填充:
    `*   `textarea/input`:用原型 `value` setter + 派发 `input/change` 事件;
    `
    *   `contenteditable`:清空后用 `InputEvent(&amp;#39;beforeinput&amp;#39;, {inputType:&amp;#39;insertText&amp;#39;})` 兼容 **Slate**,失败回退 `execCommand(&amp;#39;insertText&amp;#39;)`;
    
    • 个别站点(如智谱清言)的提交绑定在 mousedown,脚本据此改走 mousedown。
  3. 定位并触发发送:优先 aria-label/title 含"发送/Send"的按钮,兜底按按钮文本匹配;触发时派发 Pointer/Mouse/Click 序列,或回退为在输入框 pressEnter。

    渲染进程对每个面板并行 executeJavaScript,用 Promise 聚合结果,逐窗标记 ok / fail 并 toast 汇总。

    5.3 单窗全屏 / 收缩(不刷新)

    全屏与收缩均为纯 CSS class 切换,不重建、不刷新 webview:
    `pane.classList.toggle('is-fullscreen');  // 置顶覆盖,同一时刻仅一个
    pane.classList.toggle('is-collapsed');   // 隐藏网页区,webview 仍驻留 DOM
    `
    这保证了"专注单个 AI"时既不丢失其他对话,也不产生额外的页面加载。

    5.4 白 / 夜模式联动

    渲染进程 applyTheme() 经 api.setNativeTheme() 通知主进程,后者设置 nativeTheme.themeSource = mode(light/dark/system)。由于 webview 内的 AI 站点普遍响应系统 prefers-color-scheme,应用切主题即联动所有内嵌站点同步深/浅色。

    同时,主进程在 session-created 时清洗 User-Agent,剥离 Electron/* 与应用名令牌,让各站点按正常桌面浏览器渲染、降低被限流风险:

    `.replace(/\s*Electron\/[0-9.]+/i, '').replace(/\s*AI聚合问答工具\/[0-9.]+/i, '')

5.5 提示词库

`prompts.js` 提供本地增删改查、分类与搜索;支持单 `{变量}` 与双 `{{变量}}` 占位符,使用时自动弹出变量表单(`buildVarFields` / `confirmVars`),把填好的内容回填到输入框。该模块与&quot;发送注入&quot;解耦,可独立维护。

### 5.6 版本检测与自动升级

主进程 `app ready` 后请求 `https://mutiai.0315e.com/curr_version`,用 `compareVersion()` 做语义化比较,发现远端更新即 `webContents.send(&amp;#39;update-available&amp;#39;, …)`,渲染进程弹窗并提供&quot;去升级&quot;按钮跳转官网;离线或接口不可达时静默忽略。

### 5.7 本地优先与隐私
  • 每个 AI 独立 persist: 分区,登录态互不串扰;

  • 应用配置(布局、provider、主题)以 store.json 落盘于 userData,由 store.js 用 fs 读写;

  • 全程无遥测接口;requestSingleInstanceLock 保证单实例运行。

    * * *
    

    6. 工程化与交付实践

    6.1 国内镜像源加速

    .npmrc 将 npm、Electron 二进制、electron-builder 二进制、sharp 及其 libvips 全部指向 npmmirror,显著缩短依赖与二进制下载时间:

    `registry=https://registry.npmmirror.com

electron_mirror=https://registry.npmmirror.com/-/binary/electron/

electron_builder_binaries_mirror=https://registry.npmmirror.com/-/binary/electron-builder-binaries/

sharp_binary_host=https://registry.npmmirror.com/-/binary/sharp

sharp_libvips_binary_host=https://registry.npmmirror.com/-/binary/sharp-libvips

6.2 图标与截图自动化

  • 图标:scripts/make-icon.js 用 sharp 把 assets/icon.svg 栅格化为 build/icon.png(1024×1024),纳入 npm run icon 流程。
  • 截图:scripts/shot.js 启动独立 Electron 实例加载真实 index.html,注册最小 IPC 桩(store:get/set、open-external、set-native-theme)后 capturePage() 输出 build/screenshot.png。实践中的关键坑:必须 disableHardwareAcceleration() 并加 --disable-gpu,否则 GPU 进程崩溃;且必须提供最小 IPC 桩,否则渲染进程启动中断、窗格不渲染。

6.3 打包与分发

electron-builder 产出:

  • Windows:AI聚合问答工具-&amp;lt;版本&amp;gt;-win-x64.exe(NSIS 安装版)+ -portable.exe(免安装便携版);
  • macOS:*.dmg(x64 / arm64 通用)。

跨平台限制:Electron 不支持在单一系统交叉编译另一系统的安装包,macOS 包必须在 Mac 上执行 npm run dist:mac 或通过 CI 构建。仓库已提供 GitHub Actions 工作流,推送 v* tag 自动构建双平台。

6.4 版本管理与协作

代码托管于 Gitee(tyerp/ai-aggregated-qa-tool),HTTPS + 账号密码推送;.gitignore 排除 node_modules/、release/ 等体积较大的产物目录,仅纳入源码、配置与 build/ 资源。


7. 关键问题与解决方案

问题 现象 解决
运行态退化 环境 `ELECTRON_RUN_AS_NODE=1` 使 Electron 退化成 Node,`app is undefined` `start` 脚本先 `delete process.env.ELECTRON_RUN_AS_NODE` 再拉起
GPU/沙箱受限 虚拟桌面/服务器上 GPU 进程 FATAL,整体闪退 `disableHardwareAcceleration()` + `--no-sandbox --disable-gpu --disable-software-rasterizer`
后台进程被回收 用 `&` 后台启动随 shell 结束被回收,误判启动失败 改用守护方式常驻拉起
跨平台打包 Windows 无法产出 macOS 包 明确在 Mac 或 CI 构建,文档标注限制
截图窗格空白 渲染进程缺 IPC 桩,中断启动 `shot.js` 补齐最小 IPC 桩后三栏正常渲染


8. 安全与合规

  • 进程隔离:contextIsolation: true、nodeIntegration: false,预加载仅暴露必要 API;
  • 外部跳转白名单:open-external 仅放行 http(s);
  • 最小权限:不索取任何 API Key、不读取用户隐私文件,所有数据存于本机 userData。


9. 总结与展望

「AI聚合问答工具」用"轻壳层 + 网页注入"的组合,以极低的维护成本实现了多 AI 并排对比这一高频诉求。其可复用经验有三:

  1. 复用网页优于调用 API——在能力快速迭代的大模型领域,嵌入官方网页比适配 API 更稳更省;
  2. 缓存与复用是流畅体验的底座——窗格实例与 webview 的复用,让"切布局/切主题不刷新对话"成为现实;
  3. 工程化前置——镜像源、图标/截图脚本、CI 打包、.gitignore 设计,决定了项目能否稳定交付。

后续可演进方向:补齐 macOS 构建与签名、提示词跨设备同步、可拖拽自定义布局、更多站点适配,以及把"变量表单"能力下沉到发送流程。