mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-10 19:49:52 +08:00
Found by the PR review, each with a test that failed first: - Quota: any key header skipped it, even one the provider never reads (x-aws-access-key-id with OpenAI), so a request ran on the server's key without being counted. The check now runs after the model is resolved and uses usesServerCredentials. On main already. - usesServerCredentials read the raw base URL; "/" cleans up to none, so an Ollama request ran on the server's key past the server-model check. - SGLang's default 127.0.0.1:8000 only fills the settings form. Chat and the model list used it as a real address, so the server called its own machine even with private URLs blocked. Now a base URL is required. - With a user's OpenAI key and no base URL, the SDK read the server's OPENAI_BASE_URL. The official endpoint is now passed. On main already. - The Test button refused nothing on the server's keys (Ollama Cloud), and a 15 s timeout reported "connected, no tool call". - The model list for Ollama without a base URL came from ollama.com while chat went to the server's Ollama. - Bedrock's "Too many tokens, please wait" counted as context too long. - On the server's keys the provider's error text stays in the server log; it can name the server's AWS account, role or internal hosts. - Desktop app: the preset keys are the user's own (NEXT_AI_DRAWIO_DESKTOP), so Max Output Tokens can be raised and keyless models in settings work again. A launch that found the remembered port taken no longer replaces it, which hid the user's chats and settings for good.
197 lines
6.1 KiB
TypeScript
197 lines
6.1 KiB
TypeScript
import { readFileSync, writeFileSync } from "node:fs"
|
|
import net from "node:net"
|
|
import path from "node:path"
|
|
import { app } from "electron"
|
|
|
|
/**
|
|
* Port configuration
|
|
* Using fixed ports to preserve localStorage across restarts
|
|
* (localStorage is origin-specific, so changing ports loses all saved data)
|
|
*/
|
|
const PORT_CONFIG = {
|
|
// Development mode uses fixed port for hot reload compatibility
|
|
development: 6002,
|
|
// Legacy production port — tried first to preserve localStorage for existing users
|
|
legacyProduction: 61337,
|
|
// New production port below the ephemeral range (49152-65535)
|
|
// to avoid conflicts with Windows Hyper-V / ephemeral port reservations
|
|
production: 13370,
|
|
// Maximum attempts to find an available port (fallback)
|
|
maxAttempts: 100,
|
|
}
|
|
|
|
/**
|
|
* Currently allocated port (cached after first allocation)
|
|
*/
|
|
let allocatedPort: number | null = null
|
|
|
|
/**
|
|
* File that remembers the production port from the last launch, so the app
|
|
* keeps the same origin (and its localStorage) instead of switching between
|
|
* the legacy and new port depending on which one is free at startup
|
|
*/
|
|
function getSavedPortPath(): string {
|
|
return path.join(app.getPath("userData"), "server-port.json")
|
|
}
|
|
|
|
function loadSavedPort(): number | null {
|
|
try {
|
|
const { port } = JSON.parse(readFileSync(getSavedPortPath(), "utf-8"))
|
|
return Number.isInteger(port) ? port : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remember the port of the first production launch. A later launch that
|
|
* found it taken keeps it remembered: the user's data lives under that
|
|
* origin, and the next launch goes back to it once it is free.
|
|
*/
|
|
export function saveServerPort(port: number): void {
|
|
if (!app.isPackaged || loadSavedPort() !== null) {
|
|
return
|
|
}
|
|
try {
|
|
writeFileSync(getSavedPortPath(), JSON.stringify({ port }), "utf-8")
|
|
} catch (error) {
|
|
console.error("Failed to save server port:", error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a specific port is available
|
|
*/
|
|
export function isPortAvailable(port: number): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const server = net.createServer()
|
|
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
console.warn(`Port ${port} unavailable: ${err.code}`)
|
|
resolve(false)
|
|
})
|
|
server.once("listening", () => {
|
|
server.close()
|
|
resolve(true)
|
|
})
|
|
server.listen(port, "127.0.0.1")
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Find an available port
|
|
* - In development: uses fixed port (6002)
|
|
* - In production: uses the port from the last launch, then the legacy
|
|
* port (61337), then 13370, to preserve localStorage
|
|
* - Falls back to sequential ports if preferred port is unavailable
|
|
* - Last resort: lets the OS assign a port (port 0)
|
|
*
|
|
* @param reuseExisting If true, try to reuse the previously allocated port
|
|
* @returns Promise<number> The available port
|
|
*/
|
|
export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
|
const isDev = !app.isPackaged
|
|
const preferredPort = isDev
|
|
? PORT_CONFIG.development
|
|
: PORT_CONFIG.production
|
|
|
|
// Try to reuse cached port if requested and available
|
|
if (reuseExisting && allocatedPort !== null) {
|
|
const available = await isPortAvailable(allocatedPort)
|
|
if (available) {
|
|
return allocatedPort
|
|
}
|
|
console.warn(
|
|
`Previously allocated port ${allocatedPort} is no longer available`,
|
|
)
|
|
allocatedPort = null
|
|
}
|
|
|
|
// In production, use the port from the last launch first
|
|
if (!isDev) {
|
|
const savedPort = loadSavedPort()
|
|
if (savedPort !== null) {
|
|
if (await isPortAvailable(savedPort)) {
|
|
allocatedPort = savedPort
|
|
return savedPort
|
|
}
|
|
console.warn(
|
|
`Port ${savedPort} from the last launch is unavailable. Data saved under it will not show on the new port.`,
|
|
)
|
|
}
|
|
}
|
|
|
|
// In production, try legacy port first to preserve existing users' localStorage
|
|
if (!isDev) {
|
|
const legacyPort = PORT_CONFIG.legacyProduction
|
|
if (await isPortAvailable(legacyPort)) {
|
|
allocatedPort = legacyPort
|
|
return legacyPort
|
|
}
|
|
}
|
|
|
|
// Try preferred port
|
|
if (await isPortAvailable(preferredPort)) {
|
|
allocatedPort = preferredPort
|
|
return preferredPort
|
|
}
|
|
|
|
console.warn(
|
|
`Preferred port ${preferredPort} is in use, finding alternative...`,
|
|
)
|
|
|
|
// Fallback: try sequential ports starting from preferred + 1
|
|
for (let attempt = 1; attempt <= PORT_CONFIG.maxAttempts; attempt++) {
|
|
const port = preferredPort + attempt
|
|
if (await isPortAvailable(port)) {
|
|
allocatedPort = port
|
|
console.log(`Allocated fallback port: ${port}`)
|
|
return port
|
|
}
|
|
}
|
|
|
|
// Last resort: let the OS pick an available port
|
|
console.warn(
|
|
"All sequential ports failed. Requesting OS-assigned port (localStorage may not persist across restarts).",
|
|
)
|
|
const osPort = await new Promise<number>((resolve, reject) => {
|
|
const server = net.createServer()
|
|
server.once("error", reject)
|
|
server.once("listening", () => {
|
|
const addr = server.address()
|
|
const port = (addr as net.AddressInfo).port
|
|
server.close(() => resolve(port))
|
|
})
|
|
server.listen(0, "127.0.0.1")
|
|
})
|
|
allocatedPort = osPort
|
|
console.log(`OS assigned port: ${osPort}`)
|
|
return osPort
|
|
}
|
|
|
|
/**
|
|
* Get the currently allocated port
|
|
* Returns null if no port has been allocated yet
|
|
*/
|
|
export function getAllocatedPort(): number | null {
|
|
return allocatedPort
|
|
}
|
|
|
|
/**
|
|
* Reset the allocated port (useful for testing or restart scenarios)
|
|
*/
|
|
export function resetAllocatedPort(): void {
|
|
allocatedPort = null
|
|
}
|
|
|
|
/**
|
|
* Get the server URL with the allocated port
|
|
*/
|
|
export function getServerUrl(): string {
|
|
if (allocatedPort === null) {
|
|
throw new Error(
|
|
"No port allocated yet. Call findAvailablePort() first.",
|
|
)
|
|
}
|
|
return `http://127.0.0.1:${allocatedPort}`
|
|
}
|