01 · 系统总览与前端
架构剖析第一篇:系统拓扑、Electron 主进程、Vue 3 renderer 与 Bridge 的通信链路。 对应
ARCHITECTURE.md§1–§2。
§1 系统拓扑
┌─────────────────────────────────────────────────────────────────┐
│ Electron 主进程 (main.js) │
│ ├─ BrowserWindow (主窗口 / 弹出聊天 / QuickAsk / PhoneCall) │
│ ├─ Tray 系统托盘 (隐藏不退出; 退出需菜单触发) │
│ ├─ globalShortcut / clipboard / protocol handler │
│ ├─ autoUpdater (electron-updater) │
│ └─ spawn 子进程: python -m src.bridge.run │
└──────────────────────┬──────────────────────────────────────────┘
│ HTTPS/HTTP2, 127.0.0.1:18711 (默认)
┌──────────────────────┴──────────────────────────────────────────┐
│ Python Bridge (src/bridge/run.py → src/bridge/app.py) │
│ ├─ hypercorn + HTTP/2 (自签证书 workspace/.bridge.crt) │
│ ├─ FastAPI + 60+ 路由 (src/bridge/routes/*.py) │
│ ├─ APScheduler (cron / schedule / reminder) │
│ ├─ 本地小模型 (rewriter / decider / memorizer-writer) │
│ └─ 工具系统 (browser / DH / multimodal / skill / …) │
└──────────────────────┬──────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
OpenAI 兼容 LLM 本地模型群 外部服务
(BASE_URL/API_KEY) (bge-m3, (GPT-SoVITS,
rewriter, 浏览器 CDP,
decider) 社交平台)为什么 Windows-first:CUDA/Windows WDDM 对线程首触敏感;Chrome CDP 共享 Profile;Windows toast/AUMID 穿透勿扰。macOS 分支存在但不完整(src/base/auth/device.py 仅 macOS 有真实实现,Windows 只有 hostname fingerprint)。
§2 Electron 主进程(electron/electron/main.js)
2.1 进程模型
main.js 是 Electron 主进程入口,require 关键模块:
const { startPythonBridge, stopPythonBridge, isBridgeOurChild,
getBridgePort, getBridgeToken, onBridgePortChange } = require('./python')
const { mountTray, destroyTray } = require('./tray')
const { grabSelectedTextViaCopy } = require('./clipboard_grab')
const { createQuickAskWindow, getQuickAskWindow } = require('./quick_ask_window')
const { createPhoneCallWindow, getPhoneCallWindow } = require('./phone_call_window')
const { bridgeUrl, bridgeFetch } = require('./bridgeHttp')窗口清单:
| 窗口 | 变量 | 用途 |
|---|---|---|
| 主窗口 | mainWindow | 主 Vue SPA |
| 覆盖层 | overlayWindow | 屏幕覆盖 |
| QuickAsk | quickAskWindow | 快捷键弹出提问 |
| PhoneCall | phoneCallWindow | 语音通话 |
| 聊天弹出 | chatWindowsByKey (Map) | 按键 "<agent>:<dialogue>" 去重 |
| 子窗口集合 | childWindows (Set) | 跟踪全部弹出,关闭时清 |
隐藏不退出:关闭主窗口 → isQuitting=false → event.preventDefault() → 隐藏窗口;退出只通过托盘菜单 "退出后台" 触发 app.quit()。首次隐藏弹 Notification 提示用户托盘位置,标记文件 .tray-hint-shown 防重复。
2.2 EPIPE 防护
// main.js 顶部 IIFE:
for (const stream of [process.stdout, process.stderr]) {
stream.on('error', () => {}) // 吞异步 EPIPE
const orig = stream.write.bind(stream)
stream.write = function (...args) { // 包装同步 write
try { return orig(...args) } catch { return false }
}
}
process.on('uncaughtException', (e) => { if (isEpipe(e)) return; throw e })
process.on('unhandledRejection', (e) => { if (isEpipe(e)) return; throw e })父终端关闭时 stdout/stderr 触发 EPIPE → 三层防护(事件、同步、未捕获异常)。
§3 Bridge 生命周期(electron/electron/python.js)
3.1 端口决策
const PORTFILE = path.join(PROJECT_ROOT, 'workspace', '.bridge.port')
const TOKENFILE = path.join(PROJECT_ROOT, 'workspace', '.bridge_token')- 先检查 18711:
isPortFree(18711)—— 若空闲,用默认端口(保持 autorun watchdog 稳定性) - 若占用:
pickFreePort()—— 绑定127.0.0.1:0,OS 分配随机端口,关闭让出 - 健康探测:
pingHealth(port, timeoutMs=2000)—— 轮询/health,250ms 间隔 - 深度探测:
deepProbe(port)—— 命中 FastAPI worker pool 端点,确认 asyncio 调度健康 - 写入端口文件:
workspace/.bridge.port+workspace/.bridges.json
3.2 Python 解析
function resolvePython(projectRoot) {
if (process.env.FAMILIARS_PYTHON) return process.env.FAMILIARS_PYTHON
const venv = process.platform === 'win32'
? path.join(projectRoot, '.venv', 'Scripts', 'python.exe')
: path.join(projectRoot, '.venv', 'bin', 'python')
if (fs.existsSync(venv)) return venv
return process.platform === 'win32' ? 'python' : 'python3'
}优先级:FAMILIARS_PYTHON 环境变量 > .venv/Scripts/python.exe > 系统 python。
3.3 启动参数
python -m src.bridge.run环境变量:
BRIDGE_PORT(默认 18711)FAMILIARS_HTTP_INSECURE=1→ 回退到 uvicorn + HTTPFAMILIARS_DAEMON=1→ autorun 模式(pidfile 锁 + 日志重定向)
§4 Vue 3 Renderer(electron/renderer/)
4.1 技术栈
- Vue 3 + TypeScript + Pinia
- Vite 构建(dev 服务器
http://localhost:5173) - VueFlow(流程图编辑器)
- axios(HTTP + SSE,preload 暴露
window.familiarsAPI)
4.2 目录结构
electron/renderer/src/
├── assets/ # 静态资源 (mascot PNG 等)
├── components/
│ ├── Desktop.vue # 新默认桌面壳 (manager landing)
│ ├── ManagerLanding.vue # 旧 landing,localStorage.familiars:legacyLanding 激活
│ ├── ChatBubble.vue # 聊天气泡 (含 emit_event 富渲染)
│ ├── CreativeWorkshop/ # 工坊 UI
│ ├── FlowEditor/ # 流编辑器 V2
│ ├── BrokerMarketplace/ # 市场 UI
│ └── Ventaris/ # 公会系统
├── stores/ # Pinia stores
├── router/ # Vue Router
└── main.ts # 入口4.3 Renderer ↔ Bridge 通信
renderer → preload → main.js → bridge
↓
window.familiars.bridgeFetch(url, init)
↓
bridgeUrl(port, path) → https://127.0.0.1:{port}{path}CORS 白名单(src/bridge/app.py):
allow_origins=[
"http://localhost:5173", # Vite dev
"http://127.0.0.1:5173", # Vite alt
"null", # file:// (packaged build)
]Token 认证:X-Bridge-Token header(axios)或 ?token=... query(SSE/WS/<img src>)。豁免路径:/health、/api/auth/*、/api/broker_app/*(iframe 静态资产)、/api/demo/*、/api/chat/*(demo SPA 使用 demo token)。
4.4 端口热更新
onBridgePortChange((newPort, newToken) => {
// 重新配置 bridgeFetch 的 base URL
// 重新注入 preload 的 token
})当 bridge 重启换端口时,python.js 触发 _portChangeListeners → renderer 热切换。
§5 关键设计决策
| 决策 | 原因 |
|---|---|
| 单进程 bridge | 内存共享、无 IPC 复杂度、CUDA 首触线程安全 |
| HTTPS/HTTP2 | 自签证书 workspace/.bridge.crt(src/bridge/certs.py);HTTP/2 多路复用解决 Chromium 6-socket/host 限制 |
| Electron 而非 Tauri | 复用系统 API(托盘/自启动/浏览器共享 Profile)、桌面限制少 |
| 不套 LangChain/AutoGen | 自研管线把「规划→工具→记忆→报告」作一等公民;本地小模型深度耦合 |
| 端口文件而非硬编码 | autorun watchdog 始终用 18711;Electron 探测后选端口;CLI 读端口文件 |
下一篇:02-Bridge 层 →
