Skip to content

01 · System Overview & Frontend ​

First installment of the architecture deep-dive: system topology, Electron main process, Vue 3 renderer, and the communication link to the Bridge. Corresponds to ARCHITECTURE.md §1–§2.


§1 System Topology ​

┌─────────────────────────────────────────────────────────────────┐
│  Electron Main Process (main.js)                                 │
│  ├─ BrowserWindow (main window / popup chat / QuickAsk / PhoneCall)│
│  ├─ Tray (hide-on-close; quit only via tray menu)                │
│  ├─ globalShortcut / clipboard / protocol handler                │
│  ├─ autoUpdater (electron-updater)                               │
│  └─ spawn child: python -m src.bridge.run                        │
└──────────────────────┬──────────────────────────────────────────┘
                       │ HTTPS/HTTP2, 127.0.0.1:18711 (default)
┌──────────────────────┴──────────────────────────────────────────┐
│  Python Bridge (src/bridge/run.py → src/bridge/app.py)           │
│  ├─ hypercorn + HTTP/2 (self-signed cert workspace/.bridge.crt)  │
│  ├─ FastAPI + 60+ routes (src/bridge/routes/*.py)                │
│  ├─ APScheduler (cron / schedule / reminder)                     │
│  ├─ Local small models (rewriter / decider / memorizer-writer)   │
│  └─ Tool system (browser / DH / multimodal / skill / …)          │
└──────────────────────┬──────────────────────────────────────────┘
                       │
        ┌──────────────┼──────────────┐
        ▼              ▼              ▼
   OpenAI-compatible  Local models   External
   LLM                (bge-m3,       services
   (BASE_URL/API_KEY)  rewriter,     (GPT-SoVITS,
                      decider)       browser CDP,
                                     social platforms)

Why Windows-first: CUDA / Windows WDDM is sensitive to first-touch threading; Chrome CDP shares a single Profile; Windows toast / AUMID penetrates Focus Assist. macOS branches exist but are incomplete (src/base/auth/device.py has a real implementation only for macOS; Windows is stuck on a hostname-only fingerprint).

§2 Electron Main Process (electron/electron/main.js) ​

2.1 Process Model ​

main.js is the Electron main-process entry point. It requires the key modules:

javascript
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')

Window inventory:

WindowVariablePurpose
Main windowmainWindowMain Vue SPA
OverlayoverlayWindowScreen overlay
QuickAskquickAskWindowHotkey-triggered question popup
PhoneCallphoneCallWindowVoice call
Chat popupchatWindowsByKey (Map)Deduplicated by "<agent>:<dialogue>" key
Child window setchildWindows (Set)Tracks all popups; cleared on close

Hide-on-close: Closing the main window → isQuitting=false → event.preventDefault() → hide the window; quitting is triggered only through the tray menu "Exit Backend" calling app.quit(). The first hide fires a Notification telling the user where the tray icon is; a marker file .tray-hint-shown prevents repeat notifications.

2.2 EPIPE Protection ​

javascript
// Top-of-file IIFE in main.js:
for (const stream of [process.stdout, process.stderr]) {
  stream.on('error', () => {})              // swallow async EPIPE
  const orig = stream.write.bind(stream)
  stream.write = function (...args) {       // wrap synchronous 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 })

When the parent terminal closes, stdout/stderr trigger EPIPE → three layers of protection (event listener, synchronous wrapper, uncaught-exception handler).

§3 Bridge Lifecycle (electron/electron/python.js) ​

3.1 Port Decision ​

javascript
const PORTFILE = path.join(PROJECT_ROOT, 'workspace', '.bridge.port')
const TOKENFILE = path.join(PROJECT_ROOT, 'workspace', '.bridge_token')
  1. Check 18711 first: isPortFree(18711) — if free, use the default port (keeps the autorun watchdog stable)
  2. If occupied: pickFreePort() — bind 127.0.0.1:0, OS assigns a random port, then release
  3. Health probe: pingHealth(port, timeoutMs=2000) — poll /health at 250ms intervals
  4. Deep probe: deepProbe(port) — hits a FastAPI worker-pool endpoint to confirm asyncio scheduling is healthy
  5. Write port file: workspace/.bridge.port + workspace/.bridges.json

3.2 Python Resolution ​

javascript
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'
}

Priority: FAMILIARS_PYTHON env var > .venv/Scripts/python.exe > system python.

3.3 Launch Arguments ​

python -m src.bridge.run

Environment variables:

  • BRIDGE_PORT (default 18711)
  • FAMILIARS_HTTP_INSECURE=1 → fall back to uvicorn + HTTP
  • FAMILIARS_DAEMON=1 → autorun mode (pidfile lock + log redirection)

§4 Vue 3 Renderer (electron/renderer/) ​

4.1 Tech Stack ​

  • Vue 3 + TypeScript + Pinia
  • Vite build (dev server at http://localhost:5173)
  • VueFlow (flow-graph editor)
  • axios (HTTP + SSE; preload exposes window.familiars API)

4.2 Directory Structure ​

electron/renderer/src/
├── assets/                   # Static resources (mascot PNGs, etc.)
├── components/
│   ├── Desktop.vue           # New default desktop shell (manager landing)
│   ├── ManagerLanding.vue    # Legacy landing; activated by localStorage.familiars:legacyLanding
│   ├── ChatBubble.vue        # Chat bubble (with emit_event rich rendering)
│   ├── CreativeWorkshop/     # Workshop UI
│   ├── FlowEditor/           # Flow editor V2
│   ├── BrokerMarketplace/    # Marketplace UI
│   └── Ventaris/             # Guild system
├── stores/                   # Pinia stores
├── router/                   # Vue Router
└── main.ts                   # Entry point

4.3 Renderer ↔ Bridge Communication ​

renderer → preload → main.js → bridge
         ↓
    window.familiars.bridgeFetch(url, init)
         ↓
    bridgeUrl(port, path) → https://127.0.0.1:{port}{path}

CORS allowlist (src/bridge/app.py):

python
allow_origins=[
    "http://localhost:5173",     # Vite dev
    "http://127.0.0.1:5173",     # Vite alt
    "null",                       # file:// (packaged build)
]

Token authentication: X-Bridge-Token header (axios) or ?token=... query param (SSE / WS / <img src>). Exempt paths: /health, /api/auth/*, /api/broker_app/* (iframe static assets), /api/demo/*, /api/chat/* (demo SPA uses demo token).

4.4 Hot Port Update ​

javascript
onBridgePortChange((newPort, newToken) => {
  // Reconfigure bridgeFetch base URL
  // Re-inject preload token
})

When the bridge restarts on a different port, python.js fires _portChangeListeners → the renderer hot-switches.

§5 Key Design Decisions ​

DecisionRationale
Single-process bridgeMemory sharing, no IPC complexity, CUDA first-touch thread safety
HTTPS / HTTP/2Self-signed cert workspace/.bridge.crt (src/bridge/certs.py); HTTP/2 multiplexing solves Chromium's 6-socket-per-host limit
Electron over TauriReuse system APIs (tray / auto-start / shared browser profile), fewer desktop constraints
No LangChain / AutoGen wrapperCustom pipeline treats "plan → tool → memory → report" as first-class citizens; local small models are deeply coupled
Port file over hardcodingAutorun watchdog always uses 18711; Electron probes then picks a port; CLI reads the port file

Next: 02 - Bridge Core Layer →

Familiars · 多 Agent AI 桌面应用