mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
Compare commits
4 Commits
fix/image-
...
493ee168b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
493ee168b1 | ||
|
|
037f32973a | ||
|
|
7bdc1fe612 | ||
|
|
03ac9a79de |
@@ -12,7 +12,11 @@ import fs from "fs/promises"
|
|||||||
import { jsonrepair } from "jsonrepair"
|
import { jsonrepair } from "jsonrepair"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers"
|
import {
|
||||||
|
getAIModel,
|
||||||
|
supportsImageInput,
|
||||||
|
supportsPromptCaching,
|
||||||
|
} from "@/lib/ai-providers"
|
||||||
import { findCachedResponse } from "@/lib/cached-responses"
|
import { findCachedResponse } from "@/lib/cached-responses"
|
||||||
import {
|
import {
|
||||||
checkAndIncrementRequest,
|
checkAndIncrementRequest,
|
||||||
@@ -295,6 +299,17 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
lastUserMessage?.parts?.filter((part: any) => part.type === "file") ||
|
lastUserMessage?.parts?.filter((part: any) => part.type === "file") ||
|
||||||
[]
|
[]
|
||||||
|
|
||||||
|
// Check if user is sending images to a model that doesn't support them
|
||||||
|
// AI SDK silently drops unsupported parts, so we need to catch this early
|
||||||
|
if (fileParts.length > 0 && !supportsImageInput(modelId)) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: `The model "${modelId}" does not support image input. Please use a vision-capable model (e.g., GPT-4o, Claude, Gemini) or remove the image.`,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// User input only - XML is now in a separate cached system message
|
// User input only - XML is now in a separate cached system message
|
||||||
const formattedUserInput = `User input:
|
const formattedUserInput = `User input:
|
||||||
"""md
|
"""md
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ export function ChatMessageDisplay({
|
|||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
setCopyState(messageId, isToolCall, true)
|
setCopyState(messageId, isToolCall, true)
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// Fallback for non-secure contexts (HTTP) or permission denied
|
// Fallback for non-secure contexts (HTTP) or permission denied
|
||||||
const textarea = document.createElement("textarea")
|
const textarea = document.createElement("textarea")
|
||||||
textarea.value = text
|
textarea.value = text
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ import {
|
|||||||
flattenModels,
|
flattenModels,
|
||||||
type ModelConfig,
|
type ModelConfig,
|
||||||
type MultiModelConfig,
|
type MultiModelConfig,
|
||||||
PROVIDER_INFO,
|
|
||||||
type ProviderConfig,
|
type ProviderConfig,
|
||||||
type ProviderName,
|
type ProviderName,
|
||||||
} from "@/lib/types/model-config"
|
} from "@/lib/types/model-config"
|
||||||
|
|||||||
@@ -786,7 +786,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
`data: ${JSON.stringify(data)}\n\n`,
|
`data: ${JSON.stringify(data)}\n\n`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
// If parsing fails, forward the original message to avoid breaking the stream.
|
// If parsing fails, forward the original message to avoid breaking the stream.
|
||||||
controller.enqueue(
|
controller.enqueue(
|
||||||
new TextEncoder().encode(
|
new TextEncoder().encode(
|
||||||
@@ -906,3 +906,34 @@ export function supportsPromptCaching(modelId: string): boolean {
|
|||||||
modelId.startsWith("eu.anthropic")
|
modelId.startsWith("eu.anthropic")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a model supports image/vision input.
|
||||||
|
* Some models silently drop image parts without error (AI SDK warning only).
|
||||||
|
*/
|
||||||
|
export function supportsImageInput(modelId: string): boolean {
|
||||||
|
const lowerModelId = modelId.toLowerCase()
|
||||||
|
|
||||||
|
// Helper to check if model has vision capability indicator
|
||||||
|
const hasVisionIndicator =
|
||||||
|
lowerModelId.includes("vision") || lowerModelId.includes("vl")
|
||||||
|
|
||||||
|
// Models that DON'T support image/vision input (unless vision variant)
|
||||||
|
// Kimi K2 models don't support images
|
||||||
|
if (lowerModelId.includes("kimi") && !hasVisionIndicator) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepSeek text models (not vision variants)
|
||||||
|
if (lowerModelId.includes("deepseek") && !hasVisionIndicator) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen text models (not vision variants like qwen-vl)
|
||||||
|
if (lowerModelId.includes("qwen") && !hasVisionIndicator) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: assume model supports images
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ Use the standard MCP configuration with:
|
|||||||
- **Natural Language**: Describe diagrams in plain text - flowcharts, architecture diagrams, etc.
|
- **Natural Language**: Describe diagrams in plain text - flowcharts, architecture diagrams, etc.
|
||||||
- **Edit Support**: Modify existing diagrams with natural language instructions
|
- **Edit Support**: Modify existing diagrams with natural language instructions
|
||||||
- **Export**: Save diagrams as `.drawio` files
|
- **Export**: Save diagrams as `.drawio` files
|
||||||
- **Self-contained**: Embedded server, works offline (except draw.io UI which loads from embed.diagrams.net)
|
- **Self-contained**: Embedded server, works offline (except draw.io UI which loads from `embed.diagrams.net` by default, configurable via `DRAWIO_BASE_URL`)
|
||||||
|
|
||||||
## Available Tools
|
## Available Tools
|
||||||
|
|
||||||
@@ -130,6 +130,33 @@ Use the standard MCP configuration with:
|
|||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `PORT` | `6002` | Port for the embedded HTTP server |
|
| `PORT` | `6002` | Port for the embedded HTTP server |
|
||||||
|
| `DRAWIO_BASE_URL` | `https://embed.diagrams.net` | Base URL for the draw.io embed. Set this to use a self-hosted draw.io instance for private deployments. |
|
||||||
|
|
||||||
|
### Private Deployment (Self-hosted draw.io)
|
||||||
|
|
||||||
|
For security-sensitive environments that require private deployment of draw.io:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"],
|
||||||
|
"env": {
|
||||||
|
"DRAWIO_BASE_URL": "https://drawio.your-company.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can deploy your own draw.io instance using the official Docker image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 8080:8080 jgraph/drawio
|
||||||
|
```
|
||||||
|
|
||||||
|
Then set `DRAWIO_BASE_URL=http://localhost:8080` (or your server's URL).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.10",
|
"version": "0.1.11",
|
||||||
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -13,6 +13,28 @@ import {
|
|||||||
} from "./history.js"
|
} from "./history.js"
|
||||||
import { log } from "./logger.js"
|
import { log } from "./logger.js"
|
||||||
|
|
||||||
|
// Configurable draw.io embed URL for private deployments
|
||||||
|
const DRAWIO_BASE_URL =
|
||||||
|
process.env.DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
||||||
|
|
||||||
|
// Extract origin (scheme + host + port) from URL for postMessage security check
|
||||||
|
function getOrigin(url: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url)
|
||||||
|
return `${parsed.protocol}//${parsed.host}`
|
||||||
|
} catch {
|
||||||
|
return url // Fallback if parsing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DRAWIO_ORIGIN = getOrigin(DRAWIO_BASE_URL)
|
||||||
|
|
||||||
|
// Normalize URL for iframe src - ensure no double slashes
|
||||||
|
function normalizeUrl(url: string): string {
|
||||||
|
// Remove trailing slash to avoid double slashes
|
||||||
|
return url.replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
interface SessionState {
|
interface SessionState {
|
||||||
xml: string
|
xml: string
|
||||||
version: number
|
version: number
|
||||||
@@ -127,7 +149,12 @@ function cleanupExpiredSessions(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(cleanupExpiredSessions, 5 * 60 * 1000)
|
const cleanupIntervalId = setInterval(cleanupExpiredSessions, 5 * 60 * 1000)
|
||||||
|
|
||||||
|
export function shutdown(): void {
|
||||||
|
clearInterval(cleanupIntervalId)
|
||||||
|
stopHttpServer()
|
||||||
|
}
|
||||||
|
|
||||||
export function getServerPort(): number {
|
export function getServerPort(): number {
|
||||||
return serverPort
|
return serverPort
|
||||||
@@ -398,7 +425,7 @@ function getHtmlPage(sessionId: string): string {
|
|||||||
</div>
|
</div>
|
||||||
<div id="status" class="status disconnected">Connecting...</div>
|
<div id="status" class="status disconnected">Connecting...</div>
|
||||||
</div>
|
</div>
|
||||||
<iframe id="drawio" src="https://embed.diagrams.net/?embed=1&proto=json&spin=1&libraries=1"></iframe>
|
<iframe id="drawio" src="${normalizeUrl(DRAWIO_BASE_URL)}/?embed=1&proto=json&spin=1&libraries=1"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<button id="history-btn" title="History" ${sessionId ? "" : "disabled"}>
|
<button id="history-btn" title="History" ${sessionId ? "" : "disabled"}>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
@@ -428,7 +455,7 @@ function getHtmlPage(sessionId: string): string {
|
|||||||
let pendingAiSvg = false;
|
let pendingAiSvg = false;
|
||||||
|
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
if (e.origin !== 'https://embed.diagrams.net') return;
|
if (e.origin !== '${DRAWIO_ORIGIN}') return;
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
if (msg.event === 'init') {
|
if (msg.event === 'init') {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
getState,
|
getState,
|
||||||
requestSync,
|
requestSync,
|
||||||
setState,
|
setState,
|
||||||
|
shutdown,
|
||||||
startHttpServer,
|
startHttpServer,
|
||||||
waitForSync,
|
waitForSync,
|
||||||
} from "./http-server.js"
|
} from "./http-server.js"
|
||||||
@@ -47,7 +48,7 @@ import { validateAndFixXml } from "./xml-validation.js"
|
|||||||
|
|
||||||
// Server configuration
|
// Server configuration
|
||||||
const config = {
|
const config = {
|
||||||
port: parseInt(process.env.PORT || "6002"),
|
port: parseInt(process.env.PORT || "6002", 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session state (single session for simplicity)
|
// Session state (single session for simplicity)
|
||||||
@@ -618,6 +619,31 @@ server.registerTool(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Graceful shutdown handler
|
||||||
|
let isShuttingDown = false
|
||||||
|
function gracefulShutdown(reason: string) {
|
||||||
|
if (isShuttingDown) return
|
||||||
|
isShuttingDown = true
|
||||||
|
log.info(`Shutting down: ${reason}`)
|
||||||
|
shutdown()
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle stdin close (primary method - works on all platforms including Windows)
|
||||||
|
process.stdin.on("close", () => gracefulShutdown("stdin closed"))
|
||||||
|
process.stdin.on("end", () => gracefulShutdown("stdin ended"))
|
||||||
|
|
||||||
|
// Handle signals (may not work reliably on Windows)
|
||||||
|
process.on("SIGINT", () => gracefulShutdown("SIGINT"))
|
||||||
|
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"))
|
||||||
|
|
||||||
|
// Handle broken pipe (writing to closed stdout)
|
||||||
|
process.stdout.on("error", (err) => {
|
||||||
|
if (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED") {
|
||||||
|
gracefulShutdown("stdout error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Start the MCP server
|
// Start the MCP server
|
||||||
async function main() {
|
async function main() {
|
||||||
log.info("Starting MCP server for Next AI Draw.io (embedded mode)...")
|
log.info("Starting MCP server for Next AI Draw.io (embedded mode)...")
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ async function main() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
console.log("👀 Watching for preset configuration changes...")
|
console.log("👀 Watching for preset configuration changes...")
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// File might not exist yet, that's ok
|
// File might not exist yet, that's ok
|
||||||
setTimeout(setupConfigWatcher, 5000)
|
setTimeout(setupConfigWatcher, 5000)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user