框架与平台
Minaya 是一个单独的脚本标签,因此无需任何框架集成即可在任意网站上运行。 以下是各平台的具体接入方式——代码片段该放在哪里,以及需要注意的事项。
哪种方式适合你
| 平台 | 方式 |
|---|---|
| HTML / 任意网站 | 加载 widget.js |
| Next.js | 加载 widget.js |
| React(Vite / CRA) | 加载 widget.js |
| Angular | 加载 widget.js |
| WordPress | 加载 widget.js |
| Shopify | 加载 widget.js |
| React Native | WebView |
| Android(Kotlin) | WebView 或 REST API |
| iOS(Swift) | WebView 或 REST API |
所有浏览器端平台加载的都是同一个 widget.js。原生移动端没有 DOM, 因此这些平台要么在 WebView 中承载小组件,要么调用公开的 REST API, 并在各自的 UI 中渲染对话内容。
HTML / 任意网站
在结束的 body 标签前添加一个脚本标签即可。
粘贴代码片段
在每个需要显示小组件的页面中,将其添加到 </body> 之前。
<script
src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"
async
></script>Next.js
使用 next/script,使小组件在页面可交互后再加载。
添加到根布局
App Router —— app/layout.tsx。lazyOnload 策略可以让小组件不占用关键渲染路径。
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"
strategy="lazyOnload"
/>
</body>
</html>
);
}Pages Router 的替代方案
如果你使用的是 pages/_app.tsx,改为在那里渲染同样的 <Script> 组件即可。
提示。不要把脚本放进 next/head——Next 会剥离其中的脚本标签。请使用 next/script。
React(Vite / CRA)
将标签添加到 index.html,或通过一个 hook 注入。
最简单——index.html
将代码片段粘贴到 index.html 中的 </body> 之前。无需其他操作。
<script
src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"
async
></script>或从组件中挂载
适用于只想在特定路由上显示小组件的场景。清理函数会在组件卸载时移除该标签。
import { useEffect } from "react";
function MinayaWidget() {
useEffect(() => {
const script = document.createElement("script");
script.src = "https://widget.minaya.ai/widget.js";
script.dataset.siteKey = "your-site-key";
script.dataset.apiUrl = "https://api.minaya.ai";
script.async = true;
document.body.appendChild(script);
return () => {
script.remove();
};
}, []);
return null;
}Angular
将标签添加到 index.html,或通过组件注入。
最简单——src/index.html
将代码片段粘贴到 </body> 之前。
<script
src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"
async
></script>或从组件中加载
使用 Renderer2 可以让这段 DOM 操作与服务端渲染兼容。
import { Component, OnInit, Renderer2, Inject } from "@angular/core";
import { DOCUMENT } from "@angular/common";
@Component({ selector: "app-minaya", template: "" })
export class MinayaComponent implements OnInit {
constructor(
private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document,
) {}
ngOnInit(): void {
const script = this.renderer.createElement("script");
script.src = "https://widget.minaya.ai/widget.js";
script.setAttribute("data-site-key", "your-site-key");
script.setAttribute("data-api-url", "https://api.minaya.ai");
script.async = true;
this.renderer.appendChild(this.document.body, script);
}
}WordPress
粘贴到主题页脚,或使用页眉/页脚脚本插件。
方式 A——使用插件(推荐)
安装 WPCode 或 Insert Headers and Footers,打开其设置,将代码片段粘贴到 Footer 框中。此方式不受主题更新影响。
<script
src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"
async
></script>方式 B——编辑主题
外观 → 主题文件编辑器 → footer.php。粘贴到 </body> 之前。请使用子主题,否则更新会覆盖你的改动。
验证
在隐私窗口中打开你的网站,启动按钮会在一两秒内出现在右下角。
提示。兼容 Elementor、Divi 及其他页面构建工具——它是纯 JavaScript,并非某个插件的专属集成。
Shopify
添加到 theme.liquid 中结束的 body 标签之前。
打开主题编辑器
在线商店 → 主题 → …… → 编辑代码。
编辑 theme.liquid
在 Layout 下打开 theme.liquid,将代码片段粘贴到 </body> 之前,然后保存。
<script
src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"
async
></script>允许你的商店域名
在 Minaya 中,将你的 myshopify.com 域名和自定义域名都添加到 Allowed origins。
提示。除非使用 Shopify Plus,否则 Shopify 会在结账页面屏蔽第三方脚本,因此小组件在结账过程中不会显示。
React Native
React Native 中没有 DOM,因此需要在 WebView 中承载小组件。
安装 react-native-webview
执行 npm install react-native-webview,随后在 iOS 上运行 pod install。
npm install react-native-webview在 WebView 中渲染小组件
小组件会自动打开,这样访客就不需要在 WebView 内寻找启动按钮。
import { WebView } from "react-native-webview";
const html = `<!doctype html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
<body>
<script
src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"
></script>
</body>
</html>`;
function SupportChat() {
return (
<WebView
originWhitelist={["*"]}
source={{ html, baseUrl: "https://api.minaya.ai" }}
/>
);
}开启自动打开
在“小组件外观”中开启“自动打开”,使面板在 WebView 内已经处于打开状态。
提示。必须设置 baseUrl,否则请求来源会是 null,站点密钥校验将拒绝该请求。请将该来源添加到 Allowed origins。
Android(Kotlin)
可以选择在 WebView 中承载小组件,或者调用 REST API 并使用你自己的 UI。
方式 A——WebView
最快的方式。必须启用 JavaScript,否则小组件无法运行。
val webView = findViewById<WebView>(R.id.webView)
webView.settings.javaScriptEnabled = true
val html = """
<!doctype html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
<body>
<script src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"></script>
</body>
</html>
""".trimIndent()
webView.loadDataWithBaseURL("https://api.minaya.ai", html, "text/html", "UTF-8", null)方式 B——基于 REST API 的原生 UI
在 Compose 或 Views 中构建聊天界面,并直接调用 API。保留返回的 sessionId 并在后续请求中回传,以延续同一段对话。
data class ChatRequest(val message: String, val sessionId: String? = null)
data class ChatResponse(val sessionId: String, val reply: String)
// Retrofit setup
val api = Retrofit.Builder()
.baseUrl("https://api.minaya.ai/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MinayaApi::class.java)
val response = api.chat("your-site-key", ChatRequest("What are your hours?"))提示。你的站点密钥会打包进应用二进制文件中,因此应将其视为公开信息。Allowed origins 无法保护原生流量——请依赖各计划的消息数量限制。
iOS(Swift)
可以选择在 WKWebView 中承载小组件,或者调用 REST API 并使用你自己的 UI。
方式 A——WKWebView
加载与你网站相同的小组件。
import WebKit
let webView = WKWebView(frame: view.bounds)
view.addSubview(webView)
let html = """
<!doctype html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
<body>
<script src="https://widget.minaya.ai/widget.js"
data-site-key="your-site-key"
data-api-url="https://api.minaya.ai"></script>
</body>
</html>
"""
webView.loadHTMLString(html, baseURL: URL(string: "https://api.minaya.ai"))方式 B——基于 REST API 的原生 UI
在 SwiftUI 中构建聊天界面,并直接调用 API。存储 sessionId 以便在多条消息之间延续对话。
struct ChatResponse: Decodable {
let sessionId: String
let reply: String
}
func sendMessage(_ text: String, sessionId: String?) async throws -> ChatResponse {
var request = URLRequest(url: URL(string: "https://api.minaya.ai/public/chat")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("your-site-key", forHTTPHeaderField: "X-Site-Key")
var body: [String: Any] = ["message": text]
if let sessionId { body["sessionId"] = sessionId }
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(ChatResponse.self, from: data)
}提示。你的站点密钥会打包进应用二进制文件中,因此应将其视为公开信息。Allowed origins 无法保护原生流量——请依赖各计划的消息数量限制。
故障排查
小组件没有出现
打开浏览器控制台。缺少 data-site-key 或 data-api-url 会记录一条明确的错误信息。如果控制台没有报错, 请检查脚本标签是否位于 <body> 内,以及是否被内容安全策略(CSP)拦截。
每个请求都返回 403
发起请求的来源不在小组件的允许列表中。请在Allowed origins下 添加确切的来源——包括协议部分。没有设置 baseUrl 的 WebView 会发送 null 来源,永远无法匹配。
页面上出现两个小组件
脚本被引入了两次。Minaya 会忽略第二份副本,因此你应当只看到一个启动按钮; 如果看到两个,其中一个来自不同的聊天产品。