在 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
- pnpm
- Yarn
- Bun
npm create @quick-start/electron@latest electron-chat -- --template react-ts --skip
pnpm create @quick-start/electron electron-chat --template react-ts --skip
yarn create @quick-start/electron electron-chat --template react-ts --skip
bunx @quick-start/create-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:
<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.cssedit-maincss的直接链接
打开 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)并输入密钥:
- npm
- pnpm
- Yarn
- Bun
npx mastra@latest init
pnpm dlx mastra@latest init
yarn dlx mastra@latest init
bun x mastra@latest init
这会创建一个 src/mastra 文件夹,其中包含示例天气 Agent 和以下文件:
index.ts- Mastra 配置,包括 memorytools/weather-tool.ts- 获取指定位置天气的 Toolagents/weather-agent.ts- 使用该 Tool 的天气 Agent 及其提示词
接下来,你会从聊天 UI 调用 weather-agent.ts。
安装 AI SDK UI 和 AI Elements安装 AI SDK UI 和 AI Elements的直接链接
安装 AI SDK UI 和 Mastra adapter:
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/ai-sdk@latest @ai-sdk/react ai
pnpm add @mastra/ai-sdk@latest @ai-sdk/react ai
yarn add @mastra/ai-sdk@latest @ai-sdk/react ai
bun add @mastra/ai-sdk@latest @ai-sdk/react ai
创建聊天路由创建聊天路由的直接链接
打开 src/mastra/index.ts,在配置中添加 chatRoute()。这会创建一个 Electron 前端可调用的 API 路由,用于返回兼容 AI SDK 的聊天响应;下一步会将它与 useChat() 配合使用。
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。在生产部署中,请将 origin 限制为应用的实际来源。
添加聊天 UI添加聊天 UI的直接链接
打开 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 端点,把提示词发送到该端点,并分块流式返回响应。
测试 Agent测试 Agent的直接链接
要通过聊天界面测试 Agent,请同时运行 Mastra 服务器和 Electron 应用。
-
启动 Mastra 开发服务器:
- npm
- pnpm
- Yarn
- Bun
npx mastra devpnpm dlx mastra devyarn dlx mastra devbun x mastra dev -
在另一个终端中启动 Electron 应用:
- npm
- pnpm
- Yarn
- Bun
npm run devpnpm run devyarn devbun run dev -
Electron 窗口会打开并显示聊天界面
-
尝试询问天气。如果 API 密钥配置正确,你会收到响应
后续步骤后续步骤的直接链接
恭喜你使用 Electron 构建了 Mastra Agent!🎉
接下来,你可以使用自己的 Tool 和逻辑扩展项目:
准备就绪后,可进一步了解 Mastra 与 AI SDK UI 和 React 的集成方式,以及如何将 Agent 部署到任意位置: