refactor(web): secure Pico websocket access behind launcher auth

- stop exposing the raw Pico token to the frontend
- add /api/pico/info for non-secret Pico connection metadata
- proxy /pico/ws through the launcher with same-origin and dashboard auth checks
- inject the upstream Pico websocket protocol server-side
- update frontend chat connection flow and Vite websocket proxy path
- refresh related docs and tests
This commit is contained in:
wenjie
2026-04-16 16:47:23 +08:00
parent 6126ede963
commit 4b76196e2c
14 changed files with 253 additions and 171 deletions
+8 -8
View File
@@ -2,16 +2,16 @@ import { launcherFetch } from "@/api/http"
// API client for Pico Channel configuration.
interface PicoTokenResponse {
token: string
interface PicoInfoResponse {
ws_url: string
enabled: boolean
configured?: boolean
}
interface PicoSetupResponse {
token: string
ws_url: string
enabled: boolean
configured?: boolean
changed: boolean
}
@@ -25,16 +25,16 @@ async function request<T>(path: string, options?: RequestInit): Promise<T> {
return res.json() as Promise<T>
}
export async function getPicoToken(): Promise<PicoTokenResponse> {
return request<PicoTokenResponse>("/api/pico/token")
export async function getPicoInfo(): Promise<PicoInfoResponse> {
return request<PicoInfoResponse>("/api/pico/info")
}
export async function regenPicoToken(): Promise<PicoTokenResponse> {
return request<PicoTokenResponse>("/api/pico/token", { method: "POST" })
export async function regenPicoToken(): Promise<PicoInfoResponse> {
return request<PicoInfoResponse>("/api/pico/token", { method: "POST" })
}
export async function setupPico(): Promise<PicoSetupResponse> {
return request<PicoSetupResponse>("/api/pico/setup", { method: "POST" })
}
export type { PicoTokenResponse, PicoSetupResponse }
export type { PicoInfoResponse, PicoSetupResponse }
+1 -11
View File
@@ -1,7 +1,6 @@
import { getDefaultStore } from "jotai"
import { toast } from "sonner"
import { getPicoToken } from "@/api/pico"
import {
loadSessionMessages,
mergeHistoryMessages,
@@ -131,7 +130,6 @@ export async function connectChat() {
updateChatStore({ connectionState: "connecting" })
try {
const { token } = await getPicoToken()
const sessionId = activeSessionIdRef
if (generation !== connectionGeneration) {
@@ -139,18 +137,10 @@ export async function connectChat() {
return
}
if (!token) {
console.error("No pico token available")
updateChatStore({ connectionState: "error" })
isConnecting = false
scheduleReconnect(generation, sessionId)
return
}
const wsScheme = window.location.protocol === "https:" ? "wss:" : "ws:"
const wsUrl = `${wsScheme}//${window.location.host}/pico/ws`
const url = `${wsUrl}?session_id=${encodeURIComponent(sessionId)}`
const socket = new WebSocket(url, [`token.${token}`])
const socket = new WebSocket(url)
if (generation !== connectionGeneration) {
isConnecting = false
+1 -1
View File
@@ -29,7 +29,7 @@ export default defineConfig({
target: "http://localhost:18800",
changeOrigin: true,
},
"/ws": {
"/pico/ws": {
target: "ws://localhost:18800",
ws: true,
},