跳到主要内容

在 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)并输入密钥:

npx mastra@latest init

这会创建一个 src/mastra 文件夹,其中包含示例天气 Agent 和以下文件:

  • index.ts - Mastra 配置,包括 memory
  • tools/weather-tool.ts - 获取指定位置天气的 Tool
  • agents/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 install @mastra/ai-sdk@latest @ai-sdk/react ai

创建聊天路由
创建聊天路由的直接链接

打开 src/mastra/index.ts,在配置中添加 chatRoute()。这会创建一个 Electron 前端可调用的 API 路由,用于返回兼容 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。在生产部署中,请将 origin 限制为应用的实际来源。

添加聊天 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 端点,把提示词发送到该端点,并分块流式返回响应。

测试 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 添加类似人类的 memory

准备就绪后,可进一步了解 Mastra 与 AI SDK UI 和 React 的集成方式,以及如何将 Agent 部署到任意位置: