feat(infra): route Gemini WS through SOCKS5 proxy (WARP)
Some checks are pending
CI / quality (push) Waiting to run
Some checks are pending
CI / quality (push) Waiting to run
- Add socks-proxy-agent dependency - Add resolveGeminiProxyAgent() helper reading GEMINI_PROXY_URL env - Apply agent to T1 and T2 Gemini WS factory defaults - No proxy when GEMINI_PROXY_URL is unset (local dev unchanged) - Tests: 311/311 green
This commit is contained in:
parent
74770b6402
commit
5263372839
7 changed files with 139 additions and 4 deletions
|
|
@ -6,6 +6,9 @@ SUPABASE_SERVICE_ROLE_KEY=xxx
|
|||
DEEPSEEK_API_KEY=xxx
|
||||
GEMINI_API_KEY=xxx
|
||||
DEEPGRAM_API_KEY=xxx
|
||||
# Proxy SOCKS5 optionnel pour les WS Gemini Live (ex: Cloudflare WARP sur le VPS
|
||||
# prod dont l'IP est bloquée par Google). Absent = connexion directe (dev local).
|
||||
GEMINI_PROXY_URL=
|
||||
|
||||
# Stripe
|
||||
STRIPE_SECRET_KEY=xxx
|
||||
|
|
|
|||
|
|
@ -6,6 +6,26 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/).
|
|||
|
||||
---
|
||||
|
||||
## [Unreleased] — 2026-06-30 — Proxy SOCKS5 (Cloudflare WARP) pour les WS Gemini Live
|
||||
|
||||
### Added
|
||||
|
||||
- `resolveGeminiProxyAgent()` (`geminiLive.ts`) — helper qui lit la variable d'environnement **optionnelle** `GEMINI_PROXY_URL` et renvoie un `SocksProxyAgent` si elle est définie, sinon `undefined` (connexion directe). Contexte : l'IP du VPS de production (datacenter) est bloquée par Google ; Cloudflare WARP tourne en mode proxy SOCKS5 sur le VPS (`socks5://127.0.0.1:40000`). Seul le trafic WS Gemini est routé via ce proxy ; Supabase, DeepSeek et les clients restent en direct.
|
||||
- Dépendance `socks-proxy-agent` (`package.json`).
|
||||
- `GEMINI_PROXY_URL` ajoutée à `.env.example` (vide par défaut → dev local inchangé).
|
||||
- Tests `geminiLive.test.ts` — 2 tests pour `resolveGeminiProxyAgent` (absente → `undefined` ; `socks5://…` → instance `SocksProxyAgent`).
|
||||
|
||||
### Changed
|
||||
|
||||
- Factory WS par défaut de `openGeminiLiveSession` (T2, `geminiLive.ts`) et `openGeminiLiveT1Session` (T1, `geminiLiveT1.ts`) — passe désormais `{ agent }` au constructeur `new WebSocket(url, options)` quand un proxy est résolu. La factory injectée par les tests (`clientFactory`) n'est pas affectée. Prompt système, interruption, flush, `runT1/T2LiveCorrection` et close codes inchangés.
|
||||
|
||||
### Notes
|
||||
|
||||
- Tests backend : 311/311 verts. `tsc --noEmit` OK.
|
||||
- Activation en prod : définir `GEMINI_PROXY_URL=socks5://127.0.0.1:40000` côté VPS/Render (config d'env, hors code).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased] — 2026-06-30 — Sprint 7a-patch — T1 Live : suppression de la dépendance au questionnaire
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
59
package-lock.json
generated
59
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"@supabase/supabase-js": "^2.49.4",
|
||||
"dotenv": "^17.4.2",
|
||||
"hono": "^4.7.7",
|
||||
"socks-proxy-agent": "^10.1.0",
|
||||
"stripe": "^17.7.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
|
|
@ -1275,6 +1276,15 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
|
||||
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
|
|
@ -1451,7 +1461,6 @@
|
|||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
|
|
@ -1849,6 +1858,15 @@
|
|||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
|
|
@ -2034,7 +2052,6 @@
|
|||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
|
|
@ -2366,6 +2383,44 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
|
||||
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.1.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks-proxy-agent": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz",
|
||||
"integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "9.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"@supabase/supabase-js": "^2.49.4",
|
||||
"dotenv": "^17.4.2",
|
||||
"hono": "^4.7.7",
|
||||
"socks-proxy-agent": "^10.1.0",
|
||||
"stripe": "^17.7.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||
import {
|
||||
openGeminiLiveSession,
|
||||
buildT2SystemPrompt,
|
||||
resolveGeminiProxyAgent,
|
||||
GEMINI_LIVE_MODEL,
|
||||
type WebSocketLike,
|
||||
} from "../geminiLive";
|
||||
|
|
@ -44,6 +46,32 @@ describe("buildT2SystemPrompt", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("resolveGeminiProxyAgent", () => {
|
||||
let originalProxy: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalProxy = process.env.GEMINI_PROXY_URL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalProxy === undefined) {
|
||||
delete process.env.GEMINI_PROXY_URL;
|
||||
} else {
|
||||
process.env.GEMINI_PROXY_URL = originalProxy;
|
||||
}
|
||||
});
|
||||
|
||||
it("retourne undefined quand GEMINI_PROXY_URL est absente (connexion directe)", () => {
|
||||
delete process.env.GEMINI_PROXY_URL;
|
||||
expect(resolveGeminiProxyAgent()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("retourne un SocksProxyAgent quand GEMINI_PROXY_URL est définie", () => {
|
||||
process.env.GEMINI_PROXY_URL = "socks5://127.0.0.1:40000";
|
||||
expect(resolveGeminiProxyAgent()).toBeInstanceOf(SocksProxyAgent);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openGeminiLiveSession (raw WS)", () => {
|
||||
let originalKey: string | undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,23 @@
|
|||
*/
|
||||
|
||||
import { WebSocket as NodeWebSocket } from "ws";
|
||||
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||
|
||||
/**
|
||||
* Résout l'agent proxy SOCKS5 pour les connexions WebSocket vers Gemini Live.
|
||||
*
|
||||
* Contexte : l'IP du VPS de production (datacenter) est bloquée par Google.
|
||||
* Cloudflare WARP tourne en mode proxy sur le VPS (socks5://127.0.0.1:40000) ;
|
||||
* router UNIQUEMENT le trafic Gemini via ce proxy le débloque, sans affecter
|
||||
* le reste (Supabase, DeepSeek, clients).
|
||||
*
|
||||
* `GEMINI_PROXY_URL` est optionnelle : absente → connexion directe (dev local
|
||||
* intact). Présente (ex: socks5://127.0.0.1:40000) → SocksProxyAgent.
|
||||
*/
|
||||
export function resolveGeminiProxyAgent(): SocksProxyAgent | undefined {
|
||||
const url = process.env.GEMINI_PROXY_URL;
|
||||
return url ? new SocksProxyAgent(url) : undefined;
|
||||
}
|
||||
|
||||
export const GEMINI_LIVE_URL =
|
||||
"wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
|
||||
|
|
@ -294,9 +311,14 @@ export function openGeminiLiveSession(
|
|||
});
|
||||
|
||||
const url = `${GEMINI_LIVE_URL}?key=${apiKey}`;
|
||||
const proxyAgent = resolveGeminiProxyAgent();
|
||||
const factory =
|
||||
opts.clientFactory ??
|
||||
((u: string) => new NodeWebSocket(u) as unknown as WebSocketLike);
|
||||
((u: string) =>
|
||||
new NodeWebSocket(
|
||||
u,
|
||||
proxyAgent ? { agent: proxyAgent } : undefined,
|
||||
) as unknown as WebSocketLike);
|
||||
|
||||
console.log("[T2] Gemini WS URL:", GEMINI_LIVE_URL + "?key=***");
|
||||
console.log("[T2] Gemini WS model:", GEMINI_LIVE_MODEL);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
isEndSignal,
|
||||
parseAudioChunk,
|
||||
reconstructTranscript,
|
||||
resolveGeminiProxyAgent,
|
||||
tryParseGeminiJson,
|
||||
type TranscriptEntry,
|
||||
type WebSocketLike,
|
||||
|
|
@ -203,9 +204,14 @@ export function openGeminiLiveT1Session(
|
|||
const systemPrompt = buildT1SystemPrompt();
|
||||
|
||||
const url = `${GEMINI_LIVE_URL}?key=${apiKey}`;
|
||||
const proxyAgent = resolveGeminiProxyAgent();
|
||||
const factory =
|
||||
opts.clientFactory ??
|
||||
((u: string) => new NodeWebSocket(u) as unknown as WebSocketLike);
|
||||
((u: string) =>
|
||||
new NodeWebSocket(
|
||||
u,
|
||||
proxyAgent ? { agent: proxyAgent } : undefined,
|
||||
) as unknown as WebSocketLike);
|
||||
|
||||
const geminiWs = factory(url);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue