Files
replypier/frontend/src/utils/widgetEmbed.ts
T
WangShen007 0cc6299d1c feat(widget): Widget 配置、loader 与右下角悬浮入口
生成可嵌入站点的 loader 与悬浮按钮,支持展开收起和移动端;附独立宿主测试页。
2026-09-25 20:50:00 +08:00

79 lines
3.2 KiB
TypeScript

/*
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { getApiUrl } from '@/config/api'
// The loader (replypier.min.js) is served by THIS frontend, so resolve its
// origin at snippet-generation time from where the dashboard is running — no
// build-time VITE_WIDGET_URL bake, so a self-hosted install just works.
function getLoaderOrigin(): string {
return typeof window !== 'undefined' ? window.location.origin : ''
}
/**
* Build the embeddable <script> snippet for a widget.
* - Simple (anonymous) variant: drops `window.replypierId` + the loader script.
* - Token-auth variant: fetches a short-lived token from the host's backend
* before loading the widget (used when the agent requires token auth).
* Shared by the AI Agents list and the onboarding Launch step. Its exact text
* is pinned in __tests__/fixtures/widget-snippet.txt, which other code that
* builds the snippet tests against too.
*
* `window.replypierBaseUrl` is baked into the snippet from the runtime API URL
* so the embedded widget (running on the customer's own site, where APP_CONFIG is
* absent) talks to THIS install's backend instead of the vendor cloud.
*/
export function buildWidgetEmbed(widgetId: string, requireTokenAuth?: boolean): string {
const loaderOrigin = getLoaderOrigin()
const apiUrl = getApiUrl()
if (requireTokenAuth) {
return `<!-- Get token from your backend: POST /api/v1/generate-token with API key -->
<!-- Security Note: Widget ID and token are cryptographically bound in the JWT. -->
<script>
(function() {
fetch('/api/replypier')
.then(r => r.json())
.then(d => {
let token, widget_id;
if (d.data && d.data.token && d.data.widget_id) {
token = d.data.token;
widget_id = d.data.widget_id;
} else if (d.token && d.token.data && d.token.data.data) {
token = d.token.data.data.token;
widget_id = d.token.data.data.widget_id;
} else if (d.token && d.widget_id) {
token = d.token;
widget_id = d.widget_id;
}
if (!token || !widget_id) throw new Error('Failed to extract token or widget_id');
window.replypierId = widget_id;
window.replypierBaseUrl = '${apiUrl}';
localStorage.setItem('rpid', token);
const script = document.createElement('script');
script.src = '${loaderOrigin}/webclient/replypier.min.js';
document.head.appendChild(script);
})
.catch(e => console.error('[ReplyPier] Initialization failed:', e));
})();
<\/script>`
}
return `<script>
window.replypierId = '${widgetId}';
window.replypierBaseUrl = '${apiUrl}';
<\/script>
<script src="${loaderOrigin}/webclient/replypier.min.js"><\/script>`
}