跳至主要內容

在 Electron 項目中整合 Mastra

在本指南中,你會使用 Mastra 建立一個可呼叫 Tool 的 AI Agent,然後直接從 Mastra 伺服器呼叫該 Agent,將它連接至 Electron 桌面應用程式。

你會使用 AI SDK UI,建立美觀且具互動性的聊天體驗。

開始之前
開始之前 的直接連結

  • 你需要一個受支援 Provider 的 API 金鑰。如沒有偏好,可使用 OpenAI
  • 安裝 Node.js v22.13.0 或更新版本

建立新的 Electron 應用程式(可選)
建立新的 Electron 應用程式(可選) 的直接連結

如果你已有 Electron 應用程式,可跳至下一步。

使用 electron-vite 建立新的 Electron 應用程式骨架:

npm create @quick-start/electron@latest electron-chat -- --template react-ts --skip

這會建立名為 electron-chat、採用 React 和 TypeScript 的新 Electron 應用程式。進入項目目錄:

cd electron-chat

編輯 CSP 設定
編輯 CSP 設定 的直接連結

要讓 Electron 應用程式呼叫 Mastra 伺服器,你需要調整內容安全政策(CSP)設定。

開啟 src/renderer/index.html,更新 <meta http-equiv="Content-Security-Policy"> 標籤,在 connect-src 指令中加入 http://localhost:4111

src/renderer/index.html
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src http://localhost:4111; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
資訊

部署至生產環境時,你需要調整此設定,以符合實際的伺服器 URL。

編輯 main.css
edit-maincss 的直接連結

開啟 src/renderer/src/assets/main.css,將其內容改為:

src/renderer/src/assets/main.css
@import './base.css';

body {
background: var(--ev-c-white);
color: var(--ev-c-black);
}

初始化 Mastra
初始化 Mastra 的直接連結

執行 mastra init。出現提示時,選擇一個 Provider(例如 OpenAI)並輸入 API 金鑰:

npx mastra@latest init

這會建立一個 src/mastra 資料夾,當中包含天氣 Agent 範例及以下檔案:

  • index.ts - Mastra 設定,包括記憶
  • tools/weather-tool.ts - 用來擷取指定地點天氣的 Tool
  • agents/weather-agent.ts- 天氣 Agent,其 prompt 會使用該 Tool

在接下來的步驟中,你會從聊天 UI 呼叫 weather-agent.ts

安裝 AI SDK UI 與 AI Elements
安裝 AI SDK UI 與 AI Elements 的直接連結

安裝 AI SDK UI 和 Mastra adapter:

npm install @mastra/ai-sdk@latest @ai-sdk/react ai

建立聊天路由
建立聊天路由 的直接連結

開啟 src/mastra/index.ts,在設定中加入 chatRoute()。這會建立一個 API 路由,讓 Electron 前端可呼叫它並取得與 AI SDK 相容的聊天回應;下一步你會將它配合 useChat() 使用。

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'
// Existing imports...
import { chatRoute } from '@mastra/ai-sdk'

export const mastra = new Mastra({
// Existing config...
server: {
cors: {
origin: '*', // Restrict this to your app's origin in production
allowMethods: ['*'],
allowHeaders: ['*'],
},
apiRoutes: [
chatRoute({
path: '/chat/:agentId',
}),
],
},
})
資訊

由於 Electron renderer 與 Mastra 伺服器從不同來源載入,因此需要 CORS。部署至生產環境時,請將來源限制為應用程式的實際來源。

加入聊天 UI
加入聊天 UI 的直接連結

開啟 src/renderer/src/App.tsx,將其內容替換成聊天元件:

src/renderer/src/App.tsx
import { useState } from 'react'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport, type ToolUIPart } from 'ai'

const STATE_TO_LABEL_MAP: Record<string, string> = {
'input-streaming': 'Streaming input...',
'input-available': 'Input ready',
'approval-requested': 'Approval requested',
'approval-responded': 'Approval responded',
'output-available': 'Complete',
'output-error': 'Error',
}

export default function App(): React.JSX.Element {
const [input, setInput] = useState('')

const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: `http://localhost:4111/chat/weather-agent`,
}),
})

const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault()
if (!input.trim()) return
sendMessage({ text: input })
setInput('')
}

return (
<main
style={{
maxWidth: '48rem',
marginLeft: 'auto',
marginRight: 'auto',
padding: '1.5rem',
width: '100%',
height: '100vh',
}}
>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: '1 1 0%', minHeight: 0, overflowY: 'auto' }} data-name="conversation">
<div
data-name="conversation-content"
style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
>
{messages.map((message, messageIndex) => (
<div key={messageIndex}>
{message.parts.map((part, partIndex) => {
if (part.type === 'text') {
return (
<div
key={partIndex}
data-name="message"
style={{
display: 'flex',
width: '100%',
maxWidth: '95%',
flexDirection: 'column',
gap: '0.5rem',
...(message.role === 'user'
? { marginLeft: 'auto', justifyContent: 'flex-end' }
: {}),
}}
>
<div
data-name="message-content"
style={{
display: 'flex',
width: 'fit-content',
maxWidth: '100%',
minWidth: 0,
flexDirection: 'column',
gap: '0.5rem',
overflow: 'hidden',
fontSize: '0.875rem',
...(message.role === 'user'
? {
marginLeft: 'auto',
borderRadius: '0.5rem',
backgroundColor: '#dbeafe',
paddingLeft: '1rem',
paddingRight: '1rem',
paddingTop: '0.75rem',
paddingBottom: '0.75rem',
}
: {}),
}}
>
<div
data-name="message-response"
style={{ width: '100%', height: '100%' }}
>
{part.text}
</div>
</div>
</div>
)
}
if (part.type.startsWith('tool-')) {
const toolPart = part as unknown as ToolUIPart
return (
<div
key={partIndex}
data-name="tool"
style={{
marginBottom: '1.5rem',
width: '100%',
borderRadius: '0.5rem',
border: '1px solid #d1d5db',
boxShadow: '0 1px 3px 0 rgba(0,0,0,0.1)',
}}
>
<details
data-name="tool-header"
style={{ width: '100%', padding: '0.75rem', cursor: 'pointer' }}
>
<summary style={{ fontWeight: 500, fontSize: '0.875rem' }}>
{toolPart.type.split('-').slice(1).join('-')} -{' '}
{STATE_TO_LABEL_MAP[toolPart.state ?? 'output-available']}
</summary>
<div data-name="tool-content">
<div
data-name="tool-input"
style={{
overflow: 'hidden',
paddingTop: '1rem',
paddingBottom: '1rem',
}}
>
<div
style={{
fontWeight: 500,
color: '#6b7280',
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
Parameters
</div>
<pre
style={{
width: '100%',
overflowX: 'auto',
borderRadius: '0.375rem',
border: '1px solid #d1d5db',
backgroundColor: '#f9fafb',
padding: '0.75rem',
fontSize: '0.875rem',
}}
>
<code>{JSON.stringify(toolPart.input, null, 2)}</code>
</pre>
</div>
<div
data-name="tool-output"
style={{
overflow: 'hidden',
paddingTop: '1rem',
paddingBottom: '1rem',
}}
>
<div
style={{
fontWeight: 500,
color: '#6b7280',
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
{toolPart.errorText ? 'Error' : 'Result'}
</div>
<pre
style={{
width: '100%',
overflowX: 'auto',
borderRadius: '0.375rem',
border: '1px solid #d1d5db',
backgroundColor: '#f9fafb',
padding: '0.75rem',
fontSize: '0.875rem',
}}
>
<code>{JSON.stringify(toolPart.output, null, 2)}</code>
</pre>
{toolPart.errorText && (
<div data-name="tool-error" style={{ color: '#dc2626' }}>
{toolPart.errorText}
</div>
)}
</div>
</div>
</details>
</div>
)
}
return null
})}
</div>
))}
</div>
</div>
<form
style={{
width: '100%',
display: 'grid',
gridTemplateColumns: '1fr auto',
gap: '1.5rem',
flexShrink: 0,
paddingTop: '1rem',
}}
onSubmit={handleSubmit}
data-name="prompt-input"
>
<input
name="chat-input"
style={{
borderRadius: '0.5rem',
border: '1px solid #d1d5db',
boxShadow: '0 1px 2px 0 rgba(0,0,0,0.05)',
height: '2.5rem',
padding: '0 0.75rem',
}}
placeholder="City name"
value={input}
onChange={e => setInput(e.target.value)}
/>
<button
style={{
backgroundColor: '#2563eb',
color: 'white',
boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1)',
border: '1px solid #60a5fa',
paddingLeft: '1rem',
paddingRight: '1rem',
whiteSpace: 'nowrap',
borderRadius: '0.5rem',
fontSize: '0.875rem',
fontWeight: 500,
transition: 'all',
flexShrink: 0,
outline: 'none',
}}
type="submit"
>
Send
</button>
</form>
</div>
</main>
)
}

這會將 useChat() 連接至 /chat/weather-agent 端點,把 prompt 傳送至該端點,並以分段串流方式接收回應。

測試你的 Agent
測試你的 Agent 的直接連結

要透過聊天介面測試 Agent,請同時執行 Mastra 伺服器和 Electron 應用程式。

  1. 啟動 Mastra 開發伺服器:

    npx mastra dev
  2. 在另一個終端機啟動 Electron 應用程式:

    npm run dev
  3. Electron 視窗會開啟並顯示聊天介面

  4. 嘗試查詢天氣。如果 API 金鑰設定正確,你便會收到回應

下一步
下一步 的直接連結

恭喜你使用 Electron 建立了 Mastra Agent!🎉

接下來,你可以使用自己的 Tool 和邏輯擴充項目:

  • 進一步了解 Agent
  • 為 Agent 加入專屬 Tool
  • 為 Agent 加入仿如人類的記憶

準備好後,你可以進一步了解 Mastra 如何與 AI SDK UI 和 React 整合,以及如何將 Agent 部署至任何地方: