mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-03 16:27:47 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3edceb9c0 | ||
|
|
5f87ffae1e |
@@ -5,7 +5,6 @@ import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
|
|||||||
|
|
||||||
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
|
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
|
||||||
const EXTRACT_TIMEOUT_MS = 15000
|
const EXTRACT_TIMEOUT_MS = 15000
|
||||||
const USER_AGENT = "Mozilla/5.0 (compatible; NextAIDrawio/1.0)"
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -35,31 +34,6 @@ export async function POST(req: Request) {
|
|||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const headController = new AbortController()
|
|
||||||
const headTimeout = setTimeout(() => headController.abort(), 3000)
|
|
||||||
try {
|
|
||||||
const headResponse = await fetch(url, {
|
|
||||||
method: "HEAD",
|
|
||||||
headers: { "User-Agent": USER_AGENT },
|
|
||||||
signal: headController.signal,
|
|
||||||
})
|
|
||||||
const contentType = headResponse.headers.get("content-type")
|
|
||||||
if (contentType?.includes("application/pdf")) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "PDF URLs are not supported. Please download and upload the PDF file directly",
|
|
||||||
},
|
|
||||||
{ status: 422 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(
|
|
||||||
"HEAD pre-check failed, proceeding with extraction:",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
clearTimeout(headTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract article content with timeout to avoid tying up server resources
|
// Extract article content with timeout to avoid tying up server resources
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
@@ -70,7 +44,9 @@ export async function POST(req: Request) {
|
|||||||
let article
|
let article
|
||||||
try {
|
try {
|
||||||
article = await extract(url, undefined, {
|
article = await extract(url, undefined, {
|
||||||
headers: { "User-Agent": USER_AGENT },
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (compatible; NextAIDrawio/1.0)",
|
||||||
|
},
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -174,21 +174,10 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "ollama": {
|
case "ollama": {
|
||||||
// SECURITY: Mirror ai-providers.ts guard — only use server
|
const ollama = createOllama({
|
||||||
// OLLAMA_API_KEY when the URL is also from server config.
|
baseURL: baseUrl || "http://localhost:11434",
|
||||||
const ollamaApiKey = baseUrl
|
|
||||||
? apiKey || undefined
|
|
||||||
: apiKey || process.env.OLLAMA_API_KEY || undefined
|
|
||||||
const ollamaProvider = createOllama({
|
|
||||||
baseURL:
|
|
||||||
baseUrl ||
|
|
||||||
process.env.OLLAMA_BASE_URL ||
|
|
||||||
"https://ollama.com/api",
|
|
||||||
...(ollamaApiKey && {
|
|
||||||
headers: { Authorization: `Bearer ${ollamaApiKey}` },
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
model = ollamaProvider(modelId)
|
model = ollama(modelId)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,7 +282,6 @@ export function ModelConfigDialog({
|
|||||||
// Check credentials based on provider type
|
// Check credentials based on provider type
|
||||||
const isBedrock = selectedProvider.provider === "bedrock"
|
const isBedrock = selectedProvider.provider === "bedrock"
|
||||||
const isEdgeOne = selectedProvider.provider === "edgeone"
|
const isEdgeOne = selectedProvider.provider === "edgeone"
|
||||||
const isOllama = selectedProvider.provider === "ollama"
|
|
||||||
const isVertexAI = selectedProvider.provider === "vertexai"
|
const isVertexAI = selectedProvider.provider === "vertexai"
|
||||||
if (isBedrock) {
|
if (isBedrock) {
|
||||||
if (
|
if (
|
||||||
@@ -297,7 +296,7 @@ export function ModelConfigDialog({
|
|||||||
if (!selectedProvider.vertexApiKey) {
|
if (!selectedProvider.vertexApiKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (!isEdgeOne && !isOllama && !selectedProvider.apiKey) {
|
} else if (!isEdgeOne && !selectedProvider.apiKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,7 +1030,9 @@ export function ModelConfigDialog({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : selectedProvider.provider ===
|
) : selectedProvider.provider ===
|
||||||
"edgeone" ? (
|
"ollama" ||
|
||||||
|
selectedProvider.provider ===
|
||||||
|
"edgeone" ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -1099,9 +1100,6 @@ export function ModelConfigDialog({
|
|||||||
dict.modelConfig
|
dict.modelConfig
|
||||||
.apiKey
|
.apiKey
|
||||||
}
|
}
|
||||||
{selectedProvider.provider ===
|
|
||||||
"ollama" &&
|
|
||||||
` ${dict.modelConfig.optional}`}
|
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
@@ -1165,9 +1163,7 @@ export function ModelConfigDialog({
|
|||||||
handleValidate
|
handleValidate
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
(selectedProvider.provider !==
|
!selectedProvider.apiKey ||
|
||||||
"ollama" &&
|
|
||||||
!selectedProvider.apiKey) ||
|
|
||||||
validationStatus ===
|
validationStatus ===
|
||||||
"validating"
|
"validating"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,9 +359,9 @@ const PROVIDER_ENV_MAP: Record<string, { apiKey: string; baseUrl: string }> = {
|
|||||||
baseUrl: "MODELSCOPE_BASE_URL",
|
baseUrl: "MODELSCOPE_BASE_URL",
|
||||||
},
|
},
|
||||||
gateway: { apiKey: "AI_GATEWAY_API_KEY", baseUrl: "AI_GATEWAY_BASE_URL" },
|
gateway: { apiKey: "AI_GATEWAY_API_KEY", baseUrl: "AI_GATEWAY_BASE_URL" },
|
||||||
// bedrock doesn't use API keys in the same way
|
// bedrock and ollama don't use API keys in the same way
|
||||||
bedrock: { apiKey: "", baseUrl: "" },
|
bedrock: { apiKey: "", baseUrl: "" },
|
||||||
ollama: { apiKey: "OLLAMA_API_KEY", baseUrl: "OLLAMA_BASE_URL" },
|
ollama: { apiKey: "", baseUrl: "OLLAMA_BASE_URL" },
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -94,8 +94,7 @@ if (!gotTheLock) {
|
|||||||
if (
|
if (
|
||||||
url.includes("diagrams.net") ||
|
url.includes("diagrams.net") ||
|
||||||
url.includes("draw.io") ||
|
url.includes("draw.io") ||
|
||||||
url.startsWith("http://localhost") ||
|
url.startsWith("http://localhost")
|
||||||
url.startsWith("http://127.0.0.1")
|
|
||||||
) {
|
) {
|
||||||
return { action: "allow" }
|
return { action: "allow" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export async function startNextServer(): Promise<string> {
|
|||||||
const env: Record<string, string> = {
|
const env: Record<string, string> = {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
PORT: String(port),
|
PORT: String(port),
|
||||||
HOSTNAME: "127.0.0.1",
|
HOSTNAME: "localhost",
|
||||||
// Enable Node.js built-in proxy support for fetch (Node.js 24+)
|
// Enable Node.js built-in proxy support for fetch (Node.js 24+)
|
||||||
NODE_USE_ENV_PROXY: "1",
|
NODE_USE_ENV_PROXY: "1",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import { app } from "electron"
|
|||||||
const PORT_CONFIG = {
|
const PORT_CONFIG = {
|
||||||
// Development mode uses fixed port for hot reload compatibility
|
// Development mode uses fixed port for hot reload compatibility
|
||||||
development: 6002,
|
development: 6002,
|
||||||
// Legacy production port — tried first to preserve localStorage for existing users
|
// Production mode uses fixed port (61337) to preserve localStorage
|
||||||
legacyProduction: 61337,
|
// Falls back to sequential ports if unavailable
|
||||||
// New production port below the ephemeral range (49152-65535)
|
production: 61337,
|
||||||
// to avoid conflicts with Windows Hyper-V / ephemeral port reservations
|
|
||||||
production: 13370,
|
|
||||||
// Maximum attempts to find an available port (fallback)
|
// Maximum attempts to find an available port (fallback)
|
||||||
maxAttempts: 100,
|
maxAttempts: 100,
|
||||||
}
|
}
|
||||||
@@ -29,10 +27,7 @@ let allocatedPort: number | null = null
|
|||||||
export function isPortAvailable(port: number): Promise<boolean> {
|
export function isPortAvailable(port: number): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const server = net.createServer()
|
const server = net.createServer()
|
||||||
server.once("error", (err: NodeJS.ErrnoException) => {
|
server.once("error", () => resolve(false))
|
||||||
console.warn(`Port ${port} unavailable: ${err.code}`)
|
|
||||||
resolve(false)
|
|
||||||
})
|
|
||||||
server.once("listening", () => {
|
server.once("listening", () => {
|
||||||
server.close()
|
server.close()
|
||||||
resolve(true)
|
resolve(true)
|
||||||
@@ -44,12 +39,12 @@ export function isPortAvailable(port: number): Promise<boolean> {
|
|||||||
/**
|
/**
|
||||||
* Find an available port
|
* Find an available port
|
||||||
* - In development: uses fixed port (6002)
|
* - In development: uses fixed port (6002)
|
||||||
* - In production: uses fixed port (13370) to preserve localStorage
|
* - In production: uses fixed port (61337) to preserve localStorage
|
||||||
* - Falls back to sequential ports if preferred port is unavailable
|
* - 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
|
* @param reuseExisting If true, try to reuse the previously allocated port
|
||||||
* @returns Promise<number> The available port
|
* @returns Promise<number> The available port
|
||||||
|
* @throws Error if no available port found after max attempts
|
||||||
*/
|
*/
|
||||||
export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
||||||
const isDev = !app.isPackaged
|
const isDev = !app.isPackaged
|
||||||
@@ -69,16 +64,7 @@ export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
|||||||
allocatedPort = null
|
allocatedPort = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// In production, try legacy port first to preserve existing users' localStorage
|
// Try preferred port first
|
||||||
if (!isDev) {
|
|
||||||
const legacyPort = PORT_CONFIG.legacyProduction
|
|
||||||
if (await isPortAvailable(legacyPort)) {
|
|
||||||
allocatedPort = legacyPort
|
|
||||||
return legacyPort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try preferred port
|
|
||||||
if (await isPortAvailable(preferredPort)) {
|
if (await isPortAvailable(preferredPort)) {
|
||||||
allocatedPort = preferredPort
|
allocatedPort = preferredPort
|
||||||
return preferredPort
|
return preferredPort
|
||||||
@@ -98,23 +84,9 @@ export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: let the OS pick an available port
|
throw new Error(
|
||||||
console.warn(
|
`Failed to find available port after ${PORT_CONFIG.maxAttempts} attempts`,
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,5 +113,5 @@ export function getServerUrl(): string {
|
|||||||
"No port allocated yet. Call findAvailablePort() first.",
|
"No port allocated yet. Call findAvailablePort() first.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return `http://127.0.0.1:${allocatedPort}`
|
return `http://localhost:${allocatedPort}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,11 +66,7 @@ export function createWindow(serverUrl: string): BrowserWindow {
|
|||||||
|
|
||||||
// Handle page title updates
|
// Handle page title updates
|
||||||
mainWindow.webContents.on("page-title-updated", (event, title) => {
|
mainWindow.webContents.on("page-title-updated", (event, title) => {
|
||||||
if (
|
if (title && !title.includes("localhost")) {
|
||||||
title &&
|
|
||||||
!title.includes("localhost") &&
|
|
||||||
!title.includes("127.0.0.1")
|
|
||||||
) {
|
|
||||||
mainWindow?.setTitle(title)
|
mainWindow?.setTitle(title)
|
||||||
} else {
|
} else {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
+2
-3
@@ -59,9 +59,8 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|||||||
# AZURE_REASONING_EFFORT=low # Optional: Azure reasoning effort (low, medium, high)
|
# AZURE_REASONING_EFFORT=low # Optional: Azure reasoning effort (low, medium, high)
|
||||||
# AZURE_REASONING_SUMMARY=detailed
|
# AZURE_REASONING_SUMMARY=detailed
|
||||||
|
|
||||||
# Ollama Configuration (Local or Cloud)
|
# Ollama (Local) Configuration
|
||||||
# OLLAMA_BASE_URL=https://ollama.com/api # Optional, defaults to Ollama Cloud
|
# OLLAMA_BASE_URL=http://localhost:11434/api # Optional, defaults to localhost
|
||||||
# OLLAMA_API_KEY=your-ollama-cloud-api-key # Optional: For Ollama Cloud or authenticated remote instances
|
|
||||||
# OLLAMA_ENABLE_THINKING=true # Optional: Enable thinking for models that support it (e.g., qwen3)
|
# OLLAMA_ENABLE_THINKING=true # Optional: Enable thinking for models that support it (e.g., qwen3)
|
||||||
|
|
||||||
# OpenRouter Configuration
|
# OpenRouter Configuration
|
||||||
|
|||||||
+6
-24
@@ -596,7 +596,7 @@ function validateProviderCredentials(
|
|||||||
* - GOOGLE_GENERATIVE_AI_API_KEY: Google API key
|
* - GOOGLE_GENERATIVE_AI_API_KEY: Google API key
|
||||||
* - AZURE_RESOURCE_NAME, AZURE_API_KEY: Azure OpenAI credentials
|
* - AZURE_RESOURCE_NAME, AZURE_API_KEY: Azure OpenAI credentials
|
||||||
* - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: AWS Bedrock credentials
|
* - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: AWS Bedrock credentials
|
||||||
* - OLLAMA_BASE_URL: Ollama server URL (optional, defaults to https://ollama.com/api)
|
* - OLLAMA_BASE_URL: Ollama server URL (optional, defaults to http://localhost:11434)
|
||||||
* - OPENROUTER_API_KEY: OpenRouter API key
|
* - OPENROUTER_API_KEY: OpenRouter API key
|
||||||
* - DEEPSEEK_API_KEY: DeepSeek API key
|
* - DEEPSEEK_API_KEY: DeepSeek API key
|
||||||
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
||||||
@@ -611,15 +611,13 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
|
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
|
||||||
// If a custom baseUrl is provided, an API key MUST also be provided.
|
// If a custom baseUrl is provided, an API key MUST also be provided.
|
||||||
// This prevents attackers from redirecting server API keys to malicious endpoints.
|
// This prevents attackers from redirecting server API keys to malicious endpoints.
|
||||||
// Exception: EdgeOne doesn't require API keys.
|
// Exception: EdgeOne and Ollama providers don't require API keys
|
||||||
// Ollama is exempt only when no server OLLAMA_API_KEY is configured;
|
|
||||||
// when it IS configured, the outer guard also enforces client apiKey for custom baseUrls.
|
|
||||||
if (
|
if (
|
||||||
overrides?.baseUrl &&
|
overrides?.baseUrl &&
|
||||||
!overrides?.apiKey &&
|
!overrides?.apiKey &&
|
||||||
!(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
|
!(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
|
||||||
overrides?.provider !== "edgeone" &&
|
overrides?.provider !== "edgeone" &&
|
||||||
!(overrides?.provider === "ollama" && !process.env.OLLAMA_API_KEY)
|
overrides?.provider !== "ollama"
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`API key is required when using a custom base URL. ` +
|
`API key is required when using a custom base URL. ` +
|
||||||
@@ -880,19 +878,8 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
|
|
||||||
case "ollama": {
|
case "ollama": {
|
||||||
const baseURL = overrides?.baseUrl || process.env.OLLAMA_BASE_URL
|
const baseURL = overrides?.baseUrl || process.env.OLLAMA_BASE_URL
|
||||||
// SECURITY: When client provides a custom base URL, only use
|
if (baseURL) {
|
||||||
// client-provided API key. Never fall back to server OLLAMA_API_KEY
|
const customOllama = createOllama({ baseURL })
|
||||||
// to prevent leaking server credentials to user-controlled endpoints.
|
|
||||||
const apiKey = overrides?.baseUrl
|
|
||||||
? overrides?.apiKey || undefined
|
|
||||||
: resolveApiKey(overrides, "OLLAMA_API_KEY")
|
|
||||||
if (baseURL || apiKey) {
|
|
||||||
const customOllama = createOllama({
|
|
||||||
...(baseURL && { baseURL }),
|
|
||||||
...(apiKey && {
|
|
||||||
headers: { Authorization: `Bearer ${apiKey}` },
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
model = customOllama(modelId)
|
model = customOllama(modelId)
|
||||||
} else {
|
} else {
|
||||||
model = ollama(modelId)
|
model = ollama(modelId)
|
||||||
@@ -1230,12 +1217,7 @@ export function supportsImageInput(modelId: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Qwen text models (not vision variants like qwen-vl)
|
// Qwen text models (not vision variants like qwen-vl)
|
||||||
// qwen3.5-plus is a vision model
|
if (lowerModelId.includes("qwen") && !hasVisionIndicator) {
|
||||||
if (
|
|
||||||
lowerModelId.includes("qwen") &&
|
|
||||||
!hasVisionIndicator &&
|
|
||||||
!lowerModelId.includes("qwen3.5-plus")
|
|
||||||
) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const PROVIDER_INFO: Record<
|
|||||||
bedrock: { label: "Amazon Bedrock" },
|
bedrock: { label: "Amazon Bedrock" },
|
||||||
ollama: {
|
ollama: {
|
||||||
label: "Ollama",
|
label: "Ollama",
|
||||||
defaultBaseUrl: "https://ollama.com/api",
|
defaultBaseUrl: "http://localhost:11434",
|
||||||
},
|
},
|
||||||
openrouter: {
|
openrouter: {
|
||||||
label: "OpenRouter",
|
label: "OpenRouter",
|
||||||
@@ -264,7 +264,6 @@ export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
|
|||||||
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
||||||
"Qwen/Qwen2.5-7B-Instruct",
|
"Qwen/Qwen2.5-7B-Instruct",
|
||||||
"Qwen/Qwen2-VL-72B-Instruct",
|
"Qwen/Qwen2-VL-72B-Instruct",
|
||||||
"qwen3.5-plus",
|
|
||||||
],
|
],
|
||||||
sglang: [
|
sglang: [
|
||||||
// SGLang is OpenAI-compatible, models depend on deployment
|
// SGLang is OpenAI-compatible, models depend on deployment
|
||||||
@@ -294,7 +293,6 @@ export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
|
|||||||
"Qwen/Qwen3-235B-A22B-Instruct-2507",
|
"Qwen/Qwen3-235B-A22B-Instruct-2507",
|
||||||
"Qwen/Qwen3-VL-235B-A22B-Instruct",
|
"Qwen/Qwen3-VL-235B-A22B-Instruct",
|
||||||
"Qwen/Qwen3-32B",
|
"Qwen/Qwen3-32B",
|
||||||
"qwen3.5-plus",
|
|
||||||
// DeepSeek
|
// DeepSeek
|
||||||
"deepseek-ai/DeepSeek-R1-0528",
|
"deepseek-ai/DeepSeek-R1-0528",
|
||||||
"deepseek-ai/DeepSeek-V3.2",
|
"deepseek-ai/DeepSeek-V3.2",
|
||||||
|
|||||||
Generated
+508
-739
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -43,7 +43,7 @@
|
|||||||
"@aws-sdk/client-dynamodb": "^3.957.0",
|
"@aws-sdk/client-dynamodb": "^3.957.0",
|
||||||
"@aws-sdk/credential-providers": "^3.943.0",
|
"@aws-sdk/credential-providers": "^3.943.0",
|
||||||
"@extractus/article-extractor": "^8.0.18",
|
"@extractus/article-extractor": "^8.0.18",
|
||||||
"@formatjs/intl-localematcher": "^0.8.0",
|
"@formatjs/intl-localematcher": "^0.7.2",
|
||||||
"@langfuse/client": "^4.4.9",
|
"@langfuse/client": "^4.4.9",
|
||||||
"@langfuse/otel": "^4.4.4",
|
"@langfuse/otel": "^4.4.4",
|
||||||
"@langfuse/tracing": "^4.4.9",
|
"@langfuse/tracing": "^4.4.9",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"@opennextjs/cloudflare": "1.16.1",
|
"@opennextjs/cloudflare": "1.16.1",
|
||||||
"@openrouter/ai-sdk-provider": "^1.5.4",
|
"@openrouter/ai-sdk-provider": "^1.5.4",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.209.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.23.25",
|
"motion": "^12.23.25",
|
||||||
"nanoid": "^5.0.0",
|
"nanoid": "^5.0.0",
|
||||||
"negotiator": "^1.0.0",
|
"negotiator": "^1.0.0",
|
||||||
|
|||||||
Generated
+28
-41
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.16",
|
"version": "0.1.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.16",
|
"version": "0.1.12",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
@@ -469,9 +469,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@hono/node-server": {
|
"node_modules/@hono/node-server": {
|
||||||
"version": "1.19.9",
|
"version": "1.19.7",
|
||||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
|
||||||
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14.1"
|
"node": ">=18.14.1"
|
||||||
@@ -481,12 +481,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.26.0",
|
"version": "1.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
|
||||||
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
"integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.19.9",
|
"@hono/node-server": "^1.19.7",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
@@ -494,15 +494,14 @@
|
|||||||
"cross-spawn": "^7.0.5",
|
"cross-spawn": "^7.0.5",
|
||||||
"eventsource": "^3.0.2",
|
"eventsource": "^3.0.2",
|
||||||
"eventsource-parser": "^3.0.0",
|
"eventsource-parser": "^3.0.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.0.1",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^7.5.0",
|
||||||
"hono": "^4.11.4",
|
"jose": "^6.1.1",
|
||||||
"jose": "^6.1.3",
|
|
||||||
"json-schema-typed": "^8.0.2",
|
"json-schema-typed": "^8.0.2",
|
||||||
"pkce-challenge": "^5.0.0",
|
"pkce-challenge": "^5.0.0",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.0",
|
||||||
"zod": "^3.25 || ^4.0",
|
"zod": "^3.25 || ^4.0",
|
||||||
"zod-to-json-schema": "^3.25.1"
|
"zod-to-json-schema": "^3.25.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -521,9 +520,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.12",
|
"version": "24.10.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.6.tgz",
|
||||||
"integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==",
|
"integrity": "sha512-B8h60xgJMR/xmgyX9fncRzEW9gCxoJjdenUhke2v1JGOd/V66KopmWrLPXi5oUI4VuiGK+d+HlXJjDRZMj21EQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1075,13 +1074,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-rate-limit": {
|
"node_modules/express-rate-limit": {
|
||||||
"version": "8.2.1",
|
"version": "7.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
||||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"ip-address": "10.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
},
|
},
|
||||||
@@ -1264,9 +1260,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hono": {
|
"node_modules/hono": {
|
||||||
"version": "4.11.9",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.1.tgz",
|
||||||
"integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
|
"integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1352,15 +1348,6 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ip-address": {
|
|
||||||
"version": "10.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
|
||||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -2064,9 +2051,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -2074,9 +2061,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod-to-json-schema": {
|
"node_modules/zod-to-json-schema": {
|
||||||
"version": "3.25.1",
|
"version": "3.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
|
||||||
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
|
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25 || ^4"
|
"zod": "^3.25 || ^4"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
import {
|
import {
|
||||||
getAIModel,
|
|
||||||
resolveBaseURL,
|
resolveBaseURL,
|
||||||
supportsImageInput,
|
supportsImageInput,
|
||||||
supportsPromptCaching,
|
supportsPromptCaching,
|
||||||
@@ -190,120 +189,3 @@ describe("supportsImageInput", () => {
|
|||||||
expect(supportsImageInput("gemini-pro")).toBe(true)
|
expect(supportsImageInput("gemini-pro")).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock("ollama-ai-provider-v2", () => {
|
|
||||||
const mockModel = { modelId: "test-model" }
|
|
||||||
const mockProviderFn = vi.fn(() => mockModel)
|
|
||||||
const mockCreateOllama = vi.fn(() => mockProviderFn)
|
|
||||||
const mockOllama = vi.fn(() => mockModel)
|
|
||||||
return { createOllama: mockCreateOllama, ollama: mockOllama }
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Ollama API key security", () => {
|
|
||||||
let createOllamaMock: ReturnType<typeof vi.fn>
|
|
||||||
const savedEnv: Record<string, string | undefined> = {}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
savedEnv.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY
|
|
||||||
savedEnv.OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL
|
|
||||||
delete process.env.OLLAMA_BASE_URL
|
|
||||||
|
|
||||||
const mod = await import("ollama-ai-provider-v2")
|
|
||||||
createOllamaMock = mod.createOllama as ReturnType<typeof vi.fn>
|
|
||||||
createOllamaMock.mockClear()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
process.env.OLLAMA_API_KEY = savedEnv.OLLAMA_API_KEY
|
|
||||||
process.env.OLLAMA_BASE_URL = savedEnv.OLLAMA_BASE_URL
|
|
||||||
})
|
|
||||||
|
|
||||||
it("applies server OLLAMA_API_KEY when no client baseUrl is provided", () => {
|
|
||||||
process.env.OLLAMA_API_KEY = "server-secret-key"
|
|
||||||
|
|
||||||
getAIModel({ provider: "ollama", modelId: "llama2" })
|
|
||||||
|
|
||||||
expect(createOllamaMock).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
headers: { Authorization: "Bearer server-secret-key" },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("does NOT leak server OLLAMA_API_KEY when client provides a custom baseUrl", () => {
|
|
||||||
process.env.OLLAMA_API_KEY = "server-secret-key"
|
|
||||||
|
|
||||||
// When server has OLLAMA_API_KEY, the SSRF guard rejects
|
|
||||||
// client-provided baseUrl without an apiKey outright
|
|
||||||
expect(() =>
|
|
||||||
getAIModel({
|
|
||||||
provider: "ollama",
|
|
||||||
baseUrl: "https://evil-server.com",
|
|
||||||
modelId: "llama2",
|
|
||||||
}),
|
|
||||||
).toThrow("API key is required")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("uses client API key when client provides both baseUrl and apiKey", () => {
|
|
||||||
process.env.OLLAMA_API_KEY = "server-secret-key"
|
|
||||||
|
|
||||||
getAIModel({
|
|
||||||
provider: "ollama",
|
|
||||||
baseUrl: "https://my-ollama.com",
|
|
||||||
apiKey: "client-key",
|
|
||||||
modelId: "llama2",
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(createOllamaMock).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
baseURL: "https://my-ollama.com",
|
|
||||||
headers: { Authorization: "Bearer client-key" },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("applies both server OLLAMA_BASE_URL and OLLAMA_API_KEY when no client overrides", () => {
|
|
||||||
process.env.OLLAMA_BASE_URL = "https://cloud.ollama.com"
|
|
||||||
process.env.OLLAMA_API_KEY = "server-key"
|
|
||||||
|
|
||||||
getAIModel({ provider: "ollama", modelId: "llama2" })
|
|
||||||
|
|
||||||
expect(createOllamaMock).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
baseURL: "https://cloud.ollama.com",
|
|
||||||
headers: { Authorization: "Bearer server-key" },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("works when OLLAMA_API_KEY is set but OLLAMA_BASE_URL is not", () => {
|
|
||||||
process.env.OLLAMA_API_KEY = "server-key"
|
|
||||||
delete process.env.OLLAMA_BASE_URL
|
|
||||||
|
|
||||||
getAIModel({ provider: "ollama", modelId: "llama2" })
|
|
||||||
|
|
||||||
expect(createOllamaMock).toHaveBeenCalledTimes(1)
|
|
||||||
const callArgs = createOllamaMock.mock.calls[0][0]
|
|
||||||
expect(callArgs).not.toHaveProperty("baseURL")
|
|
||||||
expect(callArgs).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
headers: { Authorization: "Bearer server-key" },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("allows client custom baseUrl without apiKey when no server OLLAMA_API_KEY", () => {
|
|
||||||
delete process.env.OLLAMA_API_KEY
|
|
||||||
|
|
||||||
getAIModel({
|
|
||||||
provider: "ollama",
|
|
||||||
baseUrl: "https://my-ollama.com",
|
|
||||||
modelId: "llama2",
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(createOllamaMock).toHaveBeenCalledTimes(1)
|
|
||||||
const callArgs = createOllamaMock.mock.calls[0][0]
|
|
||||||
expect(callArgs.baseURL).toBe("https://my-ollama.com")
|
|
||||||
expect(callArgs).not.toHaveProperty("headers")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
Reference in New Issue
Block a user