mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-03 01:50:23 +08:00
Compare commits
8 Commits
feat/auto-
...
fix/stream
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d210d0e44e | ||
|
|
b5abbd8e6f | ||
|
|
622aa8683d | ||
|
|
43ddb7a999 | ||
|
|
b9fdf9538c | ||
|
|
1f31692701 | ||
|
|
31819f413c | ||
|
|
41c410c2ba |
13
.github/workflows/electron-release.yml
vendored
13
.github/workflows/electron-release.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Download draw.io static files for offline use
|
- name: Download draw.io static files for offline use
|
||||||
run: |
|
run: |
|
||||||
rm -rf public/drawio
|
rm -rf public/drawio
|
||||||
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
|
git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio
|
||||||
mkdir -p public/drawio
|
mkdir -p public/drawio
|
||||||
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
|
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
|
||||||
rm -rf public/drawio/WEB-INF
|
rm -rf public/drawio/WEB-INF
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
rm -rf public/drawio
|
rm -rf public/drawio
|
||||||
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
|
git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio
|
||||||
mkdir -p public/drawio
|
mkdir -p public/drawio
|
||||||
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
|
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
|
||||||
rm -rf public/drawio/WEB-INF
|
rm -rf public/drawio/WEB-INF
|
||||||
@@ -105,14 +105,9 @@ jobs:
|
|||||||
wait-for-completion: true
|
wait-for-completion: true
|
||||||
output-artifact-directory: release-signed
|
output-artifact-directory: release-signed
|
||||||
|
|
||||||
- name: Fix latest.yml with signed exe hashes
|
- name: Upload signed artifacts to release
|
||||||
run: node scripts/fix-latest-yml.mjs
|
|
||||||
|
|
||||||
- name: Upload signed artifacts and update metadata to release
|
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: release-signed/*.exe
|
||||||
release-signed/*.exe
|
|
||||||
release/latest*.yml
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
|||||||
- [Table of Contents](#table-of-contents)
|
- [Table of Contents](#table-of-contents)
|
||||||
- [Examples](#examples)
|
- [Examples](#examples)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [MCP Server (Preview)](#mcp-server-preview)
|
- [MCP Server](#mcp-server)
|
||||||
- [Claude Code CLI](#claude-code-cli)
|
- [Claude Code CLI](#claude-code-cli)
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Try it Online](#try-it-online)
|
- [Try it Online](#try-it-online)
|
||||||
@@ -99,9 +99,7 @@ Here are some example prompts and their generated diagrams:
|
|||||||
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
||||||
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
||||||
|
|
||||||
## MCP Server (Preview)
|
## MCP Server
|
||||||
|
|
||||||
> **Preview Feature**: This feature is experimental and may not be stable.
|
|
||||||
|
|
||||||
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).
|
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).
|
||||||
|
|
||||||
|
|||||||
@@ -100,12 +100,8 @@ export default function Home() {
|
|||||||
const handleDrawioAutoSave = useCallback(
|
const handleDrawioAutoSave = useCallback(
|
||||||
(data: { xml?: string }) => {
|
(data: { xml?: string }) => {
|
||||||
handleDiagramAutoSave(data)
|
handleDiagramAutoSave(data)
|
||||||
// Only suppress modified state when persistence is available
|
|
||||||
if (canPersist) {
|
|
||||||
drawioRef.current?.status({ message: "", modified: false })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[canPersist, drawioRef, handleDiagramAutoSave],
|
[handleDiagramAutoSave],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDarkModeChange = () => {
|
const handleDarkModeChange = () => {
|
||||||
|
|||||||
@@ -141,9 +141,6 @@ export default function ExamplePanel({
|
|||||||
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
|
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
|
||||||
{dict.examples.mcpServer}
|
{dict.examples.mcpServer}
|
||||||
</span>
|
</span>
|
||||||
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-purple-500 text-white rounded">
|
|
||||||
{dict.examples.preview}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{dict.examples.mcpDescription}
|
{dict.examples.mcpDescription}
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ export function ChatMessageDisplay({
|
|||||||
|
|
||||||
// Track previous message count to detect bulk loads vs streaming
|
// Track previous message count to detect bulk loads vs streaming
|
||||||
const prevMessageCountRef = useRef(0)
|
const prevMessageCountRef = useRef(0)
|
||||||
|
const scrollThrottleRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current && messages.length > 0) {
|
if (messagesEndRef.current && messages.length > 0) {
|
||||||
@@ -430,8 +431,17 @@ export function ChatMessageDisplay({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single message added - smooth scroll
|
// Throttle scroll during streaming to avoid layout thrashing
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
// Leading + trailing: scroll immediately, then once more after cooldown
|
||||||
|
if (!scrollThrottleRef.current) {
|
||||||
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
|
scrollThrottleRef.current = setTimeout(() => {
|
||||||
|
scrollThrottleRef.current = null
|
||||||
|
messagesEndRef.current?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}, 150)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import Image from "@/components/image-with-basepath"
|
|||||||
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
import { SettingsDialog } from "@/components/settings-dialog"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { useAutoUpdate } from "@/hooks/use-auto-update"
|
|
||||||
import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers"
|
import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
||||||
@@ -1254,22 +1253,10 @@ export default function ChatPanel({
|
|||||||
sendChatMessage(newParts, savedXml, previousXml, sessionId)
|
sendChatMessage(newParts, savedXml, previousXml, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-update listener (must be before early return so it's always active)
|
|
||||||
useAutoUpdate()
|
|
||||||
|
|
||||||
// Collapsed view (desktop only)
|
// Collapsed view (desktop only)
|
||||||
if (!isVisible && !isMobile) {
|
if (!isVisible && !isMobile) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
|
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
|
||||||
<Toaster
|
|
||||||
position="bottom-left"
|
|
||||||
richColors
|
|
||||||
expand
|
|
||||||
toastOptions={{
|
|
||||||
style: { maxWidth: "480px" },
|
|
||||||
duration: 2000,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent={dict.nav.showPanel}
|
tooltipContent={dict.nav.showPanel}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -195,7 +195,22 @@ export function ToolCallCard({
|
|||||||
{input && isExpanded && (
|
{input && isExpanded && (
|
||||||
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
|
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
|
||||||
{typeof input === "object" && input.xml ? (
|
{typeof input === "object" && input.xml ? (
|
||||||
<CodeBlock code={input.xml} language="xml" />
|
state === "input-streaming" ||
|
||||||
|
state === "input-available" ? (
|
||||||
|
<pre
|
||||||
|
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all whitespace-pre-wrap"
|
||||||
|
style={{
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-mono), ui-monospace, monospace",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{input.xml}
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
<CodeBlock code={input.xml} language="xml" />
|
||||||
|
)
|
||||||
) : typeof input === "object" &&
|
) : typeof input === "object" &&
|
||||||
input.operations &&
|
input.operations &&
|
||||||
Array.isArray(input.operations) ? (
|
Array.isArray(input.operations) ? (
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import type React from "react"
|
|
||||||
|
|
||||||
interface UpdateToastAvailableProps {
|
|
||||||
variant: "download"
|
|
||||||
version: string
|
|
||||||
onDownload: () => void
|
|
||||||
onDismiss: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateToastManualProps {
|
|
||||||
variant: "manual"
|
|
||||||
version: string
|
|
||||||
url: string
|
|
||||||
onDismiss: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateToastDownloadingProps {
|
|
||||||
variant: "downloading"
|
|
||||||
percent: number
|
|
||||||
onDismiss: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateToastProps =
|
|
||||||
| UpdateToastAvailableProps
|
|
||||||
| UpdateToastManualProps
|
|
||||||
| UpdateToastDownloadingProps
|
|
||||||
|
|
||||||
export function UpdateToast(props: UpdateToastProps) {
|
|
||||||
const { variant, onDismiss } = props
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault()
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
aria-live="polite"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 flex-shrink-0">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-primary"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
||||||
<polyline points="7 10 12 15 17 10" />
|
|
||||||
<line x1="12" y1="15" x2="12" y2="3" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
{variant === "downloading" ? (
|
|
||||||
<span className="text-sm text-foreground">
|
|
||||||
Downloading update... {Math.round(props.percent)}%
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-foreground">
|
|
||||||
Version {props.version} is available
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{variant === "download" && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
props.onDownload()
|
|
||||||
}}
|
|
||||||
className="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex-shrink-0"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{variant === "manual" && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
window.open(props.url, "_blank")
|
|
||||||
}}
|
|
||||||
className="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex-shrink-0"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onDismiss}
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
|
|
||||||
aria-label="Dismiss"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- [目录](#目录)
|
- [目录](#目录)
|
||||||
- [示例](#示例)
|
- [示例](#示例)
|
||||||
- [功能特性](#功能特性)
|
- [功能特性](#功能特性)
|
||||||
- [MCP服务器(预览)](#mcp服务器预览)
|
- [MCP服务器](#mcp服务器)
|
||||||
- [Claude Code CLI](#claude-code-cli)
|
- [Claude Code CLI](#claude-code-cli)
|
||||||
- [快速开始](#快速开始)
|
- [快速开始](#快速开始)
|
||||||
- [在线试用](#在线试用)
|
- [在线试用](#在线试用)
|
||||||
@@ -98,9 +98,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
||||||
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
||||||
|
|
||||||
## MCP服务器(预览)
|
## MCP服务器
|
||||||
|
|
||||||
> **预览功能**:此功能为实验性功能,可能不稳定。
|
|
||||||
|
|
||||||
通过MCP(模型上下文协议)在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。
|
通过MCP(模型上下文协议)在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- [目次](#目次)
|
- [目次](#目次)
|
||||||
- [例](#例)
|
- [例](#例)
|
||||||
- [機能](#機能)
|
- [機能](#機能)
|
||||||
- [MCPサーバー(プレビュー)](#mcpサーバープレビュー)
|
- [MCPサーバー](#mcpサーバー)
|
||||||
- [Claude Code CLI](#claude-code-cli)
|
- [Claude Code CLI](#claude-code-cli)
|
||||||
- [はじめに](#はじめに)
|
- [はじめに](#はじめに)
|
||||||
- [オンラインで試す](#オンラインで試す)
|
- [オンラインで試す](#オンラインで試す)
|
||||||
@@ -96,9 +96,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
||||||
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
||||||
|
|
||||||
## MCPサーバー(プレビュー)
|
## MCPサーバー
|
||||||
|
|
||||||
> **プレビュー機能**:この機能は実験的であり、安定しない可能性があります。
|
|
||||||
|
|
||||||
MCP(Model Context Protocol)を介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。
|
MCP(Model Context Protocol)を介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。
|
||||||
|
|
||||||
|
|||||||
14
electron/electron.d.ts
vendored
14
electron/electron.d.ts
vendored
@@ -2,14 +2,6 @@
|
|||||||
* Type declarations for Electron API exposed via preload script
|
* Type declarations for Electron API exposed via preload script
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Update status data sent from main process */
|
|
||||||
type UpdateStatusData =
|
|
||||||
| { status: "available"; version: string }
|
|
||||||
| { status: "available-manual"; version: string; url: string }
|
|
||||||
| { status: "downloading"; percent: number }
|
|
||||||
| { status: "downloaded" }
|
|
||||||
| { status: "error"; message: string }
|
|
||||||
|
|
||||||
/** Configuration preset interface */
|
/** Configuration preset interface */
|
||||||
interface ConfigPreset {
|
interface ConfigPreset {
|
||||||
id: string
|
id: string
|
||||||
@@ -82,12 +74,6 @@ declare global {
|
|||||||
>
|
>
|
||||||
/** Set user's preferred locale */
|
/** Set user's preferred locale */
|
||||||
setUserLocale: (locale: string) => Promise<SetUserLocaleResult>
|
setUserLocale: (locale: string) => Promise<SetUserLocaleResult>
|
||||||
/** Listen for update status events from main process */
|
|
||||||
onUpdateStatus: (
|
|
||||||
callback: (data: UpdateStatusData) => void,
|
|
||||||
) => () => void
|
|
||||||
/** Start downloading the available update */
|
|
||||||
startDownload: () => Promise<void>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Settings window Electron API */
|
/** Settings window Electron API */
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
type MenuItemConstructorOptions,
|
type MenuItemConstructorOptions,
|
||||||
shell,
|
shell,
|
||||||
} from "electron"
|
} from "electron"
|
||||||
import { checkForUpdatesManual } from "./auto-updater"
|
|
||||||
import {
|
import {
|
||||||
applyPresetToEnv,
|
applyPresetToEnv,
|
||||||
getAllPresets,
|
getAllPresets,
|
||||||
@@ -157,11 +156,6 @@ function getMenuTemplate(): MenuItemConstructorOptions[] {
|
|||||||
template.push({
|
template.push({
|
||||||
label: t.help,
|
label: t.help,
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
|
||||||
label: t.checkForUpdates,
|
|
||||||
click: () => checkForUpdatesManual(),
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
{
|
||||||
label: t.documentation,
|
label: t.documentation,
|
||||||
click: async () => {
|
click: async () => {
|
||||||
|
|||||||
@@ -1,282 +0,0 @@
|
|||||||
import https from "node:https"
|
|
||||||
import { app, dialog, ipcMain } from "electron"
|
|
||||||
import electronUpdater from "electron-updater"
|
|
||||||
import { getMainWindow } from "./window-manager"
|
|
||||||
|
|
||||||
const { autoUpdater } = electronUpdater
|
|
||||||
|
|
||||||
const CHECK_INTERVAL = 4 * 60 * 60 * 1000 // 4 hours
|
|
||||||
const STARTUP_DELAY = 10_000 // 10 seconds
|
|
||||||
const GITHUB_API_URL =
|
|
||||||
"https://api.github.com/repos/DayuanJiang/next-ai-draw-io/releases/latest"
|
|
||||||
|
|
||||||
let isChecking = false
|
|
||||||
let updateDownloaded = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this platform supports electron-updater auto-update.
|
|
||||||
* macOS: disabled because builds are ad-hoc signed (no Apple Developer cert).
|
|
||||||
*/
|
|
||||||
function supportsAutoUpdate(): boolean {
|
|
||||||
if (process.platform === "darwin") return false
|
|
||||||
if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR)
|
|
||||||
return false
|
|
||||||
if (process.platform === "linux" && !process.env.APPIMAGE) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two semver-like version strings numerically.
|
|
||||||
* Returns true if remote > local.
|
|
||||||
* Strips prerelease/build metadata (e.g., "1.2.3-beta.1" → "1.2.3").
|
|
||||||
*/
|
|
||||||
function isNewerVersion(remote: string, local: string): boolean {
|
|
||||||
const strip = (v: string) => v.replace(/^v/, "").split("-")[0].split("+")[0]
|
|
||||||
const r = strip(remote).split(".").map(Number)
|
|
||||||
const l = strip(local).split(".").map(Number)
|
|
||||||
const len = Math.max(r.length, l.length)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const rv = r[i] || 0
|
|
||||||
const lv = l[i] || 0
|
|
||||||
if (rv > lv) return true
|
|
||||||
if (rv < lv) return false
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send update status to the renderer via IPC
|
|
||||||
*/
|
|
||||||
function sendStatus(data: Record<string, unknown>) {
|
|
||||||
const win = getMainWindow()
|
|
||||||
if (win && !win.isDestroyed()) {
|
|
||||||
win.webContents.send("update-status", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check GitHub API for latest release (used on macOS and Linux DEB)
|
|
||||||
*/
|
|
||||||
function checkGitHubRelease(manual: boolean) {
|
|
||||||
const req = https.get(
|
|
||||||
GITHUB_API_URL,
|
|
||||||
{
|
|
||||||
headers: { "User-Agent": "next-ai-draw-io" },
|
|
||||||
timeout: 15000,
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
console.error(`GitHub API returned status ${res.statusCode}`)
|
|
||||||
if (manual) {
|
|
||||||
dialog.showMessageBox({
|
|
||||||
type: "error",
|
|
||||||
title: "Update Check Failed",
|
|
||||||
message:
|
|
||||||
"Could not check for updates. Please try again later.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
isChecking = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.setEncoding("utf8")
|
|
||||||
let body = ""
|
|
||||||
res.on("data", (chunk: string) => {
|
|
||||||
body += chunk
|
|
||||||
})
|
|
||||||
res.on("end", () => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(body)
|
|
||||||
const remoteVersion = data.tag_name || ""
|
|
||||||
const localVersion = app.getVersion()
|
|
||||||
|
|
||||||
if (isNewerVersion(remoteVersion, localVersion)) {
|
|
||||||
sendStatus({
|
|
||||||
status: "available-manual",
|
|
||||||
version: remoteVersion.replace(/^v/, ""),
|
|
||||||
url: data.html_url,
|
|
||||||
})
|
|
||||||
} else if (manual) {
|
|
||||||
dialog.showMessageBox({
|
|
||||||
type: "info",
|
|
||||||
title: "No Updates",
|
|
||||||
message: "You're up to date!",
|
|
||||||
detail: `Version ${localVersion} is the latest version.`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to parse GitHub release:", err)
|
|
||||||
if (manual) {
|
|
||||||
dialog.showMessageBox({
|
|
||||||
type: "error",
|
|
||||||
title: "Update Check Failed",
|
|
||||||
message:
|
|
||||||
"Could not check for updates. Please try again later.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isChecking = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
req.on("timeout", () => {
|
|
||||||
req.destroy()
|
|
||||||
isChecking = false
|
|
||||||
})
|
|
||||||
req.on("error", (err) => {
|
|
||||||
console.error("GitHub API request failed:", err)
|
|
||||||
if (manual) {
|
|
||||||
dialog.showMessageBox({
|
|
||||||
type: "error",
|
|
||||||
title: "Update Check Failed",
|
|
||||||
message: "Could not check for updates. Please try again later.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
isChecking = false
|
|
||||||
})
|
|
||||||
req.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up electron-updater event handlers (Windows NSIS / Linux AppImage)
|
|
||||||
*/
|
|
||||||
function setupAutoUpdater() {
|
|
||||||
autoUpdater.autoDownload = false
|
|
||||||
autoUpdater.autoInstallOnAppQuit = true
|
|
||||||
|
|
||||||
autoUpdater.on("update-available", (info) => {
|
|
||||||
console.log("Update available:", info.version)
|
|
||||||
sendStatus({ status: "available", version: info.version })
|
|
||||||
isChecking = false
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on("update-not-available", () => {
|
|
||||||
console.log("No update available")
|
|
||||||
isChecking = false
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on("download-progress", (progress) => {
|
|
||||||
sendStatus({
|
|
||||||
status: "downloading",
|
|
||||||
percent: progress.percent,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on("update-downloaded", () => {
|
|
||||||
updateDownloaded = true
|
|
||||||
console.log("Update downloaded")
|
|
||||||
sendStatus({ status: "downloaded" })
|
|
||||||
showRestartDialog()
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on("error", (err) => {
|
|
||||||
console.error("Auto-update error:", err)
|
|
||||||
sendStatus({ status: "error", message: String(err) })
|
|
||||||
isChecking = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// IPC: renderer requests download
|
|
||||||
ipcMain.handle("updater:start-download", () => {
|
|
||||||
return autoUpdater.downloadUpdate()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show restart dialog after update downloaded
|
|
||||||
*/
|
|
||||||
async function showRestartDialog() {
|
|
||||||
const result = await dialog.showMessageBox({
|
|
||||||
type: "info",
|
|
||||||
buttons: ["Restart Now", "Later"],
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1,
|
|
||||||
title: "Update Ready",
|
|
||||||
message: "A new version has been downloaded.",
|
|
||||||
detail: "Restart the app to install the update.",
|
|
||||||
})
|
|
||||||
if (result.response === 0) {
|
|
||||||
autoUpdater.quitAndInstall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a single update check
|
|
||||||
*/
|
|
||||||
function doCheck(manual: boolean) {
|
|
||||||
if (isChecking) return
|
|
||||||
isChecking = true
|
|
||||||
|
|
||||||
// If update already downloaded, just re-show restart dialog
|
|
||||||
if (updateDownloaded && supportsAutoUpdate()) {
|
|
||||||
isChecking = false
|
|
||||||
showRestartDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportsAutoUpdate()) {
|
|
||||||
// For manual check, register listeners BEFORE triggering check
|
|
||||||
// to avoid race where event fires before listeners are attached
|
|
||||||
if (manual) {
|
|
||||||
const onNotAvailable = () => {
|
|
||||||
dialog.showMessageBox({
|
|
||||||
type: "info",
|
|
||||||
title: "No Updates",
|
|
||||||
message: "You're up to date!",
|
|
||||||
detail: `Version ${app.getVersion()} is the latest version.`,
|
|
||||||
})
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
const onAvailable = () => cleanup()
|
|
||||||
const onError = () => cleanup()
|
|
||||||
const cleanup = () => {
|
|
||||||
autoUpdater.off("update-not-available", onNotAvailable)
|
|
||||||
autoUpdater.off("update-available", onAvailable)
|
|
||||||
autoUpdater.off("error", onError)
|
|
||||||
}
|
|
||||||
autoUpdater.once("update-not-available", onNotAvailable)
|
|
||||||
autoUpdater.once("update-available", onAvailable)
|
|
||||||
autoUpdater.once("error", onError)
|
|
||||||
}
|
|
||||||
autoUpdater.checkForUpdates().catch((err) => {
|
|
||||||
console.error("checkForUpdates failed:", err)
|
|
||||||
isChecking = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
checkGitHubRelease(manual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize auto-updater. Call once after createWindow().
|
|
||||||
*/
|
|
||||||
export function initAutoUpdater() {
|
|
||||||
if (!app.isPackaged) return
|
|
||||||
|
|
||||||
if (supportsAutoUpdate()) {
|
|
||||||
setupAutoUpdater()
|
|
||||||
} else {
|
|
||||||
// Register no-op handler so renderer doesn't get an unhandled error
|
|
||||||
ipcMain.handle("updater:start-download", () => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// First check after startup delay
|
|
||||||
setTimeout(() => doCheck(false), STARTUP_DELAY)
|
|
||||||
|
|
||||||
// Periodic checks
|
|
||||||
setInterval(() => doCheck(false), CHECK_INTERVAL)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manual update check from menu item
|
|
||||||
*/
|
|
||||||
export function checkForUpdatesManual() {
|
|
||||||
if (!app.isPackaged) {
|
|
||||||
dialog.showMessageBox({
|
|
||||||
type: "info",
|
|
||||||
title: "Development Mode",
|
|
||||||
message: "Auto-update is not available in development mode.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
doCheck(true)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { app, BrowserWindow, dialog, shell } from "electron"
|
import { app, BrowserWindow, dialog, shell } from "electron"
|
||||||
import { buildAppMenu } from "./app-menu"
|
import { buildAppMenu } from "./app-menu"
|
||||||
import { initAutoUpdater } from "./auto-updater"
|
|
||||||
import { getCurrentPresetEnv } from "./config-manager"
|
import { getCurrentPresetEnv } from "./config-manager"
|
||||||
import { loadEnvFile } from "./env-loader"
|
import { loadEnvFile } from "./env-loader"
|
||||||
import { registerIpcHandlers } from "./ipc-handlers"
|
import { registerIpcHandlers } from "./ipc-handlers"
|
||||||
@@ -59,11 +58,6 @@ if (!gotTheLock) {
|
|||||||
|
|
||||||
// Create main window
|
// Create main window
|
||||||
createWindow(serverUrl)
|
createWindow(serverUrl)
|
||||||
|
|
||||||
// Initialize auto-updater (production only)
|
|
||||||
if (!isDev) {
|
|
||||||
initAutoUpdater()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start application:", error)
|
console.error("Failed to start application:", error)
|
||||||
dialog.showErrorBox(
|
dialog.showErrorBox(
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export interface MenuTranslations {
|
|||||||
help: string
|
help: string
|
||||||
documentation: string
|
documentation: string
|
||||||
reportIssue: string
|
reportIssue: string
|
||||||
checkForUpdates: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const translations: Record<MenuLocale, MenuTranslations> = {
|
const translations: Record<MenuLocale, MenuTranslations> = {
|
||||||
@@ -63,7 +62,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
|||||||
help: "Help",
|
help: "Help",
|
||||||
documentation: "Documentation",
|
documentation: "Documentation",
|
||||||
reportIssue: "Report Issue",
|
reportIssue: "Report Issue",
|
||||||
checkForUpdates: "Check for Updates...",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
zh: {
|
zh: {
|
||||||
@@ -92,7 +90,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
|||||||
help: "帮助",
|
help: "帮助",
|
||||||
documentation: "文档",
|
documentation: "文档",
|
||||||
reportIssue: "报告问题",
|
reportIssue: "报告问题",
|
||||||
checkForUpdates: "检查更新...",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
ja: {
|
ja: {
|
||||||
@@ -121,7 +118,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
|||||||
help: "ヘルプ",
|
help: "ヘルプ",
|
||||||
documentation: "ドキュメント",
|
documentation: "ドキュメント",
|
||||||
reportIssue: "問題を報告",
|
reportIssue: "問題を報告",
|
||||||
checkForUpdates: "アップデートを確認...",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"zh-Hant": {
|
"zh-Hant": {
|
||||||
@@ -150,7 +146,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
|||||||
help: "說明",
|
help: "說明",
|
||||||
documentation: "文件",
|
documentation: "文件",
|
||||||
reportIssue: "回報問題",
|
reportIssue: "回報問題",
|
||||||
checkForUpdates: "檢查更新...",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import { contextBridge, type IpcRendererEvent, ipcRenderer } from "electron"
|
import { contextBridge, ipcRenderer } from "electron"
|
||||||
|
|
||||||
// Locally defined to avoid dependency on electron.d.ts compilation scope
|
|
||||||
type UpdateStatus =
|
|
||||||
| { status: "available"; version: string }
|
|
||||||
| { status: "available-manual"; version: string; url: string }
|
|
||||||
| { status: "downloading"; percent: number }
|
|
||||||
| { status: "downloaded" }
|
|
||||||
| { status: "error"; message: string }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expose safe APIs to the renderer process
|
* Expose safe APIs to the renderer process
|
||||||
@@ -39,13 +31,4 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
getUserLocale: () => ipcRenderer.invoke("get-user-locale"),
|
getUserLocale: () => ipcRenderer.invoke("get-user-locale"),
|
||||||
setUserLocale: (locale: string) =>
|
setUserLocale: (locale: string) =>
|
||||||
ipcRenderer.invoke("set-user-locale", locale),
|
ipcRenderer.invoke("set-user-locale", locale),
|
||||||
|
|
||||||
// Auto-update
|
|
||||||
onUpdateStatus: (callback: (data: UpdateStatus) => void) => {
|
|
||||||
const handler = (_event: IpcRendererEvent, data: UpdateStatus) =>
|
|
||||||
callback(data)
|
|
||||||
ipcRenderer.on("update-status", handler)
|
|
||||||
return () => ipcRenderer.removeListener("update-status", handler)
|
|
||||||
},
|
|
||||||
startDownload: () => ipcRenderer.invoke("updater:start-download"),
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { createElement, useEffect, useRef } from "react"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import { UpdateToast } from "@/components/update-toast"
|
|
||||||
|
|
||||||
export function useAutoUpdate() {
|
|
||||||
const downloadToastId = useRef<string | number | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const api = (window as Window).electronAPI
|
|
||||||
if (!api?.onUpdateStatus) return
|
|
||||||
|
|
||||||
const cleanup = api.onUpdateStatus((data) => {
|
|
||||||
switch (data.status) {
|
|
||||||
case "available":
|
|
||||||
toast.custom(
|
|
||||||
(t) =>
|
|
||||||
createElement(UpdateToast, {
|
|
||||||
variant: "download",
|
|
||||||
version: data.version,
|
|
||||||
onDownload: () => {
|
|
||||||
toast.dismiss(t)
|
|
||||||
api.startDownload().catch(() => {
|
|
||||||
// Error will come through update-status channel
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onDismiss: () => toast.dismiss(t),
|
|
||||||
}),
|
|
||||||
{ duration: 15000 },
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "available-manual":
|
|
||||||
toast.custom(
|
|
||||||
(t) =>
|
|
||||||
createElement(UpdateToast, {
|
|
||||||
variant: "manual",
|
|
||||||
version: data.version,
|
|
||||||
url: data.url,
|
|
||||||
onDismiss: () => toast.dismiss(t),
|
|
||||||
}),
|
|
||||||
{ duration: Number.POSITIVE_INFINITY },
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "downloading": {
|
|
||||||
const id =
|
|
||||||
downloadToastId.current ??
|
|
||||||
`download-progress-${Date.now()}`
|
|
||||||
downloadToastId.current = id
|
|
||||||
toast.custom(
|
|
||||||
(t) =>
|
|
||||||
createElement(UpdateToast, {
|
|
||||||
variant: "downloading",
|
|
||||||
percent: data.percent,
|
|
||||||
onDismiss: () => toast.dismiss(t),
|
|
||||||
}),
|
|
||||||
{ id, duration: Number.POSITIVE_INFINITY },
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "downloaded":
|
|
||||||
// Dismiss progress toast — native restart dialog handles the rest
|
|
||||||
if (downloadToastId.current) {
|
|
||||||
toast.dismiss(downloadToastId.current)
|
|
||||||
downloadToastId.current = null
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case "error":
|
|
||||||
if (downloadToastId.current) {
|
|
||||||
// Error during active download — inform the user
|
|
||||||
toast.dismiss(downloadToastId.current)
|
|
||||||
downloadToastId.current = null
|
|
||||||
toast.error(
|
|
||||||
"Update download failed. Please try again later.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Otherwise: silent — background check failure, logged in main process
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return cleanup
|
|
||||||
}, [])
|
|
||||||
}
|
|
||||||
@@ -1356,12 +1356,11 @@ 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
|
// Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input
|
||||||
if (
|
if (
|
||||||
lowerModelId.includes("qwen") &&
|
lowerModelId.includes("qwen") &&
|
||||||
!hasVisionIndicator &&
|
!hasVisionIndicator &&
|
||||||
!lowerModelId.includes("qwen3.5-plus") &&
|
!lowerModelId.includes("qwen3.5")
|
||||||
!lowerModelId.includes("qwen3.5-flash")
|
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,8 +76,7 @@
|
|||||||
"creativeDescription": "Draw something fun and creative",
|
"creativeDescription": "Draw something fun and creative",
|
||||||
"cachedNote": "Examples are cached for instant response",
|
"cachedNote": "Examples are cached for instant response",
|
||||||
"mcpServer": "MCP Server",
|
"mcpServer": "MCP Server",
|
||||||
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor",
|
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor"
|
||||||
"preview": "PREVIEW"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
|
|||||||
@@ -76,8 +76,7 @@
|
|||||||
"creativeDescription": "楽しくてクリエイティブなものを描く",
|
"creativeDescription": "楽しくてクリエイティブなものを描く",
|
||||||
"cachedNote": "例はキャッシュされ、即座に応答します",
|
"cachedNote": "例はキャッシュされ、即座に応答します",
|
||||||
"mcpServer": "MCP サーバー",
|
"mcpServer": "MCP サーバー",
|
||||||
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用",
|
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用"
|
||||||
"preview": "プレビュー"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "設定",
|
"title": "設定",
|
||||||
|
|||||||
@@ -76,8 +76,7 @@
|
|||||||
"creativeDescription": "繪製有趣且富有創意的內容",
|
"creativeDescription": "繪製有趣且富有創意的內容",
|
||||||
"cachedNote": "範例已快取,可即時回應",
|
"cachedNote": "範例已快取,可即時回應",
|
||||||
"mcpServer": "MCP 伺服器",
|
"mcpServer": "MCP 伺服器",
|
||||||
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
|
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
|
||||||
"preview": "預覽"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "設定",
|
"title": "設定",
|
||||||
|
|||||||
@@ -76,8 +76,7 @@
|
|||||||
"creativeDescription": "绘制有趣且富有创意的内容",
|
"creativeDescription": "绘制有趣且富有创意的内容",
|
||||||
"cachedNote": "示例已缓存,可即时响应",
|
"cachedNote": "示例已缓存,可即时响应",
|
||||||
"mcpServer": "MCP 服务器",
|
"mcpServer": "MCP 服务器",
|
||||||
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
|
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
|
||||||
"preview": "预览"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "设置",
|
"title": "设置",
|
||||||
|
|||||||
118
package-lock.json
generated
118
package-lock.json
generated
@@ -48,9 +48,10 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"electron-updater": "^6.8.3",
|
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
|
"lightningcss": "^1.32.0",
|
||||||
|
"lightningcss-linux-x64-gnu": "^1.32.0",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"motion": "^12.23.25",
|
"motion": "^12.23.25",
|
||||||
"nanoid": "^5.0.0",
|
"nanoid": "^5.0.0",
|
||||||
@@ -2225,6 +2226,9 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2242,6 +2246,9 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2259,6 +2266,9 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2276,6 +2286,9 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8246,6 +8259,9 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -10046,6 +10062,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
@@ -10681,6 +10698,7 @@
|
|||||||
"version": "9.5.1",
|
"version": "9.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
|
||||||
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
|
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@@ -12779,69 +12797,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/electron-updater": {
|
|
||||||
"version": "6.8.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
|
|
||||||
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"builder-util-runtime": "9.5.1",
|
|
||||||
"fs-extra": "^10.1.0",
|
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"lazy-val": "^1.0.5",
|
|
||||||
"lodash.escaperegexp": "^4.1.2",
|
|
||||||
"lodash.isequal": "^4.5.0",
|
|
||||||
"semver": "~7.7.3",
|
|
||||||
"tiny-typed-emitter": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-updater/node_modules/fs-extra": {
|
|
||||||
"version": "10.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
|
||||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"graceful-fs": "^4.2.0",
|
|
||||||
"jsonfile": "^6.0.1",
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-updater/node_modules/jsonfile": {
|
|
||||||
"version": "6.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
|
||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"graceful-fs": "^4.1.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-updater/node_modules/semver": {
|
|
||||||
"version": "7.7.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-updater/node_modules/universalify": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-winstaller": {
|
"node_modules/electron-winstaller": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
||||||
@@ -14960,6 +14915,7 @@
|
|||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/gzip-size": {
|
"node_modules/gzip-size": {
|
||||||
@@ -16192,6 +16148,7 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -16409,6 +16366,7 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
@@ -16609,6 +16567,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -16792,6 +16753,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -16812,6 +16776,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -16832,6 +16799,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -17099,19 +17069,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.escaperegexp": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isequal": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
|
||||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
|
||||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -21098,6 +21055,7 @@
|
|||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
||||||
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
||||||
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=11.0.0"
|
"node": ">=11.0.0"
|
||||||
@@ -22517,12 +22475,6 @@
|
|||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tiny-typed-emitter": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|||||||
@@ -70,7 +70,6 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"electron-updater": "^6.8.3",
|
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.17",
|
"version": "0.1.19",
|
||||||
"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",
|
||||||
|
|||||||
@@ -4,6 +4,29 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import http from "node:http"
|
import http from "node:http"
|
||||||
|
|
||||||
|
const MAX_BODY_BYTES = 10 * 1024 * 1024 // 10 MiB
|
||||||
|
|
||||||
|
function readBody(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
cb: (body: string) => void,
|
||||||
|
): void {
|
||||||
|
let body = ""
|
||||||
|
let size = 0
|
||||||
|
req.on("data", (chunk: Buffer) => {
|
||||||
|
size += chunk.length
|
||||||
|
if (size > MAX_BODY_BYTES) {
|
||||||
|
res.writeHead(413, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ error: "Payload too large" }))
|
||||||
|
req.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body += chunk
|
||||||
|
})
|
||||||
|
req.on("end", () => cb(body))
|
||||||
|
}
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addHistory,
|
addHistory,
|
||||||
clearHistory,
|
clearHistory,
|
||||||
@@ -155,7 +178,7 @@ export function startHttpServer(port = 6002): Promise<number> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, "127.0.0.1", () => {
|
||||||
serverPort = port
|
serverPort = port
|
||||||
log.info(`HTTP server running on http://localhost:${port}`)
|
log.info(`HTTP server running on http://localhost:${port}`)
|
||||||
resolve(port)
|
resolve(port)
|
||||||
@@ -266,11 +289,7 @@ function handleStateApi(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
} else if (req.method === "POST") {
|
} else if (req.method === "POST") {
|
||||||
let body = ""
|
readBody(req, res, (body) => {
|
||||||
req.on("data", (chunk) => {
|
|
||||||
body += chunk
|
|
||||||
})
|
|
||||||
req.on("end", () => {
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(body)
|
const data = JSON.parse(body)
|
||||||
const { sessionId } = data
|
const { sessionId } = data
|
||||||
@@ -347,11 +366,7 @@ function handleRestoreApi(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = ""
|
readBody(req, res, (body) => {
|
||||||
req.on("data", (chunk) => {
|
|
||||||
body += chunk
|
|
||||||
})
|
|
||||||
req.on("end", () => {
|
|
||||||
try {
|
try {
|
||||||
const { sessionId, index } = JSON.parse(body)
|
const { sessionId, index } = JSON.parse(body)
|
||||||
if (!sessionId || index === undefined) {
|
if (!sessionId || index === undefined) {
|
||||||
@@ -393,11 +408,7 @@ function handleHistorySvgApi(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = ""
|
readBody(req, res, (body) => {
|
||||||
req.on("data", (chunk) => {
|
|
||||||
body += chunk
|
|
||||||
})
|
|
||||||
req.on("end", () => {
|
|
||||||
try {
|
try {
|
||||||
const { sessionId, svg } = JSON.parse(body)
|
const { sessionId, svg } = JSON.parse(body)
|
||||||
if (!sessionId || !svg) {
|
if (!sessionId || !svg) {
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
/**
|
|
||||||
* Fix latest.yml metadata after Windows code signing.
|
|
||||||
*
|
|
||||||
* electron-builder generates latest.yml with hashes of unsigned executables.
|
|
||||||
* After SignPath signs them, the hashes change. This script reads each
|
|
||||||
* latest*.yml in release/, finds the matching signed exe in release-signed/,
|
|
||||||
* and rewrites the sha512 and size fields.
|
|
||||||
*
|
|
||||||
* Usage: node scripts/fix-latest-yml.mjs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createHash } from "node:crypto"
|
|
||||||
import {
|
|
||||||
createReadStream,
|
|
||||||
existsSync,
|
|
||||||
readdirSync,
|
|
||||||
readFileSync,
|
|
||||||
statSync,
|
|
||||||
writeFileSync,
|
|
||||||
} from "node:fs"
|
|
||||||
import { join } from "node:path"
|
|
||||||
|
|
||||||
const RELEASE_DIR = "release"
|
|
||||||
const SIGNED_DIR = "release-signed"
|
|
||||||
|
|
||||||
// Verify directories exist
|
|
||||||
if (!existsSync(RELEASE_DIR)) {
|
|
||||||
console.error(`Error: ${RELEASE_DIR}/ directory does not exist`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
if (!existsSync(SIGNED_DIR)) {
|
|
||||||
console.error(`Error: ${SIGNED_DIR}/ directory does not exist`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all latest*.yml files
|
|
||||||
const ymlFiles = readdirSync(RELEASE_DIR).filter(
|
|
||||||
(f) => f.startsWith("latest") && f.endsWith(".yml"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (ymlFiles.length === 0) {
|
|
||||||
console.log("No latest*.yml files found in release/")
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute SHA-512 hash of a file using streaming (avoids loading large exe into memory)
|
|
||||||
*/
|
|
||||||
function hashFile(filePath) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const hash = createHash("sha512")
|
|
||||||
const stream = createReadStream(filePath)
|
|
||||||
stream.on("data", (chunk) => hash.update(chunk))
|
|
||||||
stream.on("end", () => resolve(hash.digest("base64")))
|
|
||||||
stream.on("error", reject)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a map of signed exe filenames to their hash and size
|
|
||||||
const signedFiles = new Map()
|
|
||||||
for (const f of readdirSync(SIGNED_DIR)) {
|
|
||||||
if (!f.endsWith(".exe")) continue
|
|
||||||
const filePath = join(SIGNED_DIR, f)
|
|
||||||
const sha512 = await hashFile(filePath)
|
|
||||||
const size = statSync(filePath).size
|
|
||||||
signedFiles.set(f, { sha512, size })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signedFiles.size === 0) {
|
|
||||||
console.error("No signed .exe files found in release-signed/")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Found ${signedFiles.size} signed exe(s):`,
|
|
||||||
[...signedFiles.keys()].join(", "),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const ymlFile of ymlFiles) {
|
|
||||||
const ymlPath = join(RELEASE_DIR, ymlFile)
|
|
||||||
const content = readFileSync(ymlPath, "utf-8")
|
|
||||||
const lines = content.split("\n")
|
|
||||||
const outputLines = []
|
|
||||||
|
|
||||||
// Track the current file entry being processed (by url field)
|
|
||||||
let currentExeName = null
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
// Match " url: SomeFile.exe" inside the files array
|
|
||||||
const urlMatch = line.match(/^\s+url:\s+(.+\.exe)\s*$/)
|
|
||||||
if (urlMatch) {
|
|
||||||
currentExeName = urlMatch[1]
|
|
||||||
outputLines.push(line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match top-level "path: SomeFile.exe"
|
|
||||||
const pathMatch = line.match(/^path:\s+(.+\.exe)\s*$/)
|
|
||||||
if (pathMatch) {
|
|
||||||
currentExeName = pathMatch[1]
|
|
||||||
outputLines.push(line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace sha512 lines
|
|
||||||
const sha512Match = line.match(/^(\s*)sha512:\s+/)
|
|
||||||
if (sha512Match && currentExeName && signedFiles.has(currentExeName)) {
|
|
||||||
const indent = sha512Match[1]
|
|
||||||
outputLines.push(
|
|
||||||
`${indent}sha512: ${signedFiles.get(currentExeName).sha512}`,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace size lines
|
|
||||||
const sizeMatch = line.match(/^(\s*)size:\s+\d+/)
|
|
||||||
if (sizeMatch && currentExeName && signedFiles.has(currentExeName)) {
|
|
||||||
const indent = sizeMatch[1]
|
|
||||||
outputLines.push(
|
|
||||||
`${indent}size: ${signedFiles.get(currentExeName).size}`,
|
|
||||||
)
|
|
||||||
// Reset current exe after processing both sha512 and size
|
|
||||||
currentExeName = null
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
outputLines.push(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileSync(ymlPath, outputLines.join("\n"))
|
|
||||||
console.log(`Updated ${ymlFile}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Done!")
|
|
||||||
@@ -200,6 +200,8 @@ describe("supportsImageInput", () => {
|
|||||||
|
|
||||||
it("returns true for Qwen vision models", () => {
|
it("returns true for Qwen vision models", () => {
|
||||||
expect(supportsImageInput("qwen-vl")).toBe(true)
|
expect(supportsImageInput("qwen-vl")).toBe(true)
|
||||||
|
expect(supportsImageInput("Qwen3.5")).toBe(true)
|
||||||
|
expect(supportsImageInput("qwen3.5")).toBe(true)
|
||||||
expect(supportsImageInput("qwen3.5-plus")).toBe(true)
|
expect(supportsImageInput("qwen3.5-plus")).toBe(true)
|
||||||
expect(supportsImageInput("qwen3.5-flash")).toBe(true)
|
expect(supportsImageInput("qwen3.5-flash")).toBe(true)
|
||||||
expect(supportsImageInput("qwen3-vl-plus")).toBe(true)
|
expect(supportsImageInput("qwen3-vl-plus")).toBe(true)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"packages",
|
"packages",
|
||||||
"electron",
|
"electron",
|
||||||
"electron-standalone",
|
"electron-standalone",
|
||||||
"dist-electron"
|
"dist-electron",
|
||||||
|
"release"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user