diff --git a/backend/apps/system/api/assistant.py b/backend/apps/system/api/assistant.py
index 4dbf1aa09..a8df58bf3 100644
--- a/backend/apps/system/api/assistant.py
+++ b/backend/apps/system/api/assistant.py
@@ -1,6 +1,7 @@
import json
import os
-from datetime import timedelta
+from datetime import datetime, timedelta, timezone
+from zoneinfo import ZoneInfo
from typing import List, Optional
from fastapi import APIRouter, Form, HTTPException, Path, Query, Request, Response, UploadFile
@@ -22,30 +23,83 @@
from common.core.security import create_access_token
from common.core.sqlbot_cache import clear_cache
from common.utils.utils import get_origin_from_referer, origin_match_domain
-
router = APIRouter(tags=["system_assistant"], prefix="/system/assistant")
from common.audit.models.log_model import OperationType, OperationModules
from common.audit.schemas.logger_decorator import LogConfig, system_log
-
+from sqlbot_xpack.core import decrypt_embedded_sign
@router.get("/info/{id}", include_in_schema=False)
-async def info(request: Request, response: Response, session: SessionDep, trans: Trans, id: int) -> AssistantModel:
+async def info(request: Request, response: Response, session: SessionDep, trans: Trans, id: int, virtual: Optional[int] = Query(None)):
if not id:
raise Exception('miss assistant id')
db_model = await get_assistant_info(session=session, assistant_id=id)
if not db_model:
raise RuntimeError(f"assistant application not exist")
db_model = AssistantModel.model_validate(db_model)
-
- origin = request.headers.get("origin") or get_origin_from_referer(request)
- if not origin:
- raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=origin or ''))
- origin = origin.rstrip('/')
- if not origin_match_domain(origin, db_model.domain):
- raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=origin or ''))
-
+
+ # 校验 SQLBOT-EMBEDDED-SIGN 请求头
+ sign_header = request.headers.get("SQLBOT-EMBEDDED-SIGN")
+ if not sign_header:
+ raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=''))
+
+ sign_data = await decrypt_embedded_sign(sign_header)
+
+ # 校验 assistant_id 与 id 参数一致
+ if str(sign_data.get("assistant_id")) != str(id):
+ raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=''))
+
+ # 校验 target(来源域名)是否合法
+ target = sign_data.get("target", "")
+ if not origin_match_domain(target, db_model.domain):
+ raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
+
+ # 校验 sign_time 是否在 10 秒内
+ sign_time_str = sign_data.get("sign_time", "")
+ sign_time = datetime.fromisoformat(sign_time_str)
+ now_utc = datetime.now(timezone.utc)
+ sign_time_utc = sign_time.astimezone(timezone.utc)
+ if abs((now_utc - sign_time_utc).total_seconds()) > 10:
+ raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
+
+ # 校验是否为真实浏览器请求(非自动化工具)
+ if sign_data.get("webdriver", False):
+ raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
+
+ # 校验 User-Agent 一致性(签名中的 navigator.userAgent 与请求头一致)
+ sign_user_agent = sign_data.get("user_agent", "")
+ request_user_agent = request.headers.get("User-Agent", "")
+ if sign_user_agent != request_user_agent:
+ raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
+
+ # 校验 timezone 与 sign_time 偏移一致性(防时区伪造)
+ tz_name = sign_data.get("timezone", "")
+ tz = ZoneInfo(tz_name)
+ sign_time_naive = sign_time.replace(tzinfo=None)
+ if tz.utcoffset(sign_time_naive) != sign_time.utcoffset():
+ raise RuntimeError(trans('i18n_embedded.invalid_origin', origin=target or ''))
+
+ origin = target.rstrip('/')
+
response.headers["Access-Control-Allow-Origin"] = origin
- return db_model
+
+
+ assistant_oid = 1
+ if (db_model.type == 0):
+ configuration = db_model.configuration
+ config_obj = json.loads(configuration) if configuration else {}
+ assistant_oid = config_obj.get('oid', 1)
+
+ access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+ assistantDict = {
+ "id": virtual, "account": 'sqlbot-inner-assistant', "oid": assistant_oid, "assistant_id": id
+ }
+ access_token = create_access_token(
+ assistantDict, expires_delta=access_token_expires
+ )
+
+ result = db_model.model_dump()
+ result["token"] = access_token
+ return result
@router.get("/app/{appId}", include_in_schema=False)
@@ -67,7 +121,7 @@ async def getApp(request: Request, response: Response, session: SessionDep, tran
return db_model
-@router.get("/validator", response_model=AssistantValidator, include_in_schema=False)
+""" @router.get("/validator", response_model=AssistantValidator, include_in_schema=False)
async def validator(session: SessionDep, id: int, virtual: Optional[int] = Query(None)):
if not id:
raise Exception('miss assistant id')
@@ -89,7 +143,7 @@ async def validator(session: SessionDep, id: int, virtual: Optional[int] = Query
access_token = create_access_token(
assistantDict, expires_delta=access_token_expires
)
- return AssistantValidator(True, True, True, access_token)
+ return AssistantValidator(True, True, True, access_token) """
@router.get('/picture/{file_id}', summary=f"{PLACEHOLDER_PREFIX}assistant_picture_api", description=f"{PLACEHOLDER_PREFIX}assistant_picture_api")
diff --git a/backend/apps/system/crud/assistant.py b/backend/apps/system/crud/assistant.py
index 3cecbba21..0c9d60083 100644
--- a/backend/apps/system/crud/assistant.py
+++ b/backend/apps/system/crud/assistant.py
@@ -22,6 +22,21 @@
from common.core.response_middleware import ResponseMiddleware
+def _update_cors_middleware_instance(app: FastAPI, updated_origins: list[str]):
+ """遍历 middleware 栈,找到 CORSMiddleware 实例并更新其 allow_origins。
+
+ 仅修改 middleware.kwargs 不会影响已构建的中间件实例,
+ 需要直接更新实例的 allow_origins 属性。
+ """
+ stack = getattr(app, 'middleware_stack', None)
+ while stack is not None and hasattr(stack, 'app'):
+ if isinstance(stack, CORSMiddleware):
+ stack.allow_origins = updated_origins
+ return
+ stack = stack.app
+
+
+
@cache(namespace=CacheNamespace.EMBEDDED_INFO, cacheName=CacheName.ASSISTANT_INFO, keyExpression="assistant_id")
async def get_assistant_info(*, session: Session, assistant_id: int) -> AssistantModel | None:
db_model = session.get(AssistantModel, assistant_id)
@@ -98,6 +113,7 @@ def init_dynamic_cors(app: FastAPI):
updated_origins = list(set(settings.all_cors_origins + unique_domains))
if cors_middleware:
cors_middleware.kwargs['allow_origins'] = updated_origins
+ _update_cors_middleware_instance(app, updated_origins)
if response_middleware:
for instance in ResponseMiddleware.instances:
instance.update_allow_origins(updated_origins)
diff --git a/backend/apps/system/crud/assistant_manage.py b/backend/apps/system/crud/assistant_manage.py
index adec96793..f9dce3675 100644
--- a/backend/apps/system/crud/assistant_manage.py
+++ b/backend/apps/system/crud/assistant_manage.py
@@ -12,6 +12,17 @@
from common.core.response_middleware import ResponseMiddleware
+def _update_cors_middleware_instance(app: FastAPI, updated_origins: list[str]):
+ """遍历 middleware 栈,找到 CORSMiddleware 实例并更新其 allow_origins。"""
+ stack = getattr(app, 'middleware_stack', None)
+ while stack is not None and hasattr(stack, 'app'):
+ if isinstance(stack, CORSMiddleware):
+ stack.allow_origins = updated_origins
+ return
+ stack = stack.app
+
+
+
def dynamic_upgrade_cors(request: Request, session: Session):
list_result = session.exec(select(AssistantModel).order_by(AssistantModel.create_time)).all()
seen = set()
@@ -37,6 +48,7 @@ def dynamic_upgrade_cors(request: Request, session: Session):
updated_origins = list(set(settings.all_cors_origins + unique_domains))
if cors_middleware:
cors_middleware.kwargs['allow_origins'] = updated_origins
+ _update_cors_middleware_instance(app, updated_origins)
if response_middleware:
for instance in ResponseMiddleware.instances:
instance.update_allow_origins(updated_origins)
diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py
index 423f9ea78..feeb246aa 100644
--- a/backend/apps/system/middleware/auth.py
+++ b/backend/apps/system/middleware/auth.py
@@ -1,15 +1,17 @@
import base64
import json
+import re
from typing import Optional
from fastapi import Request
from fastapi.responses import JSONResponse
+from starlette.responses import Response
import jwt
from sqlmodel import Session
from starlette.middleware.base import BaseHTTPMiddleware
from apps.system.crud.apikey_manage import get_api_key
from apps.system.models.system_model import ApiKeyModel, AssistantModel
-from common.core.db import engine
+from common.core.db import engine
from apps.system.crud.assistant import get_assistant_info, get_assistant_user
from apps.system.crud.user import get_user_by_account, get_user_info
from apps.system.schemas.system_schema import AssistantHeader, UserInfoDTO
@@ -17,7 +19,7 @@
from common.core.config import settings
from common.core.schemas import TokenPayload
from common.utils.locale import I18n
-from common.utils.utils import SQLBotLogUtil, get_origin_from_referer
+from common.utils.utils import SQLBotLogUtil, get_origin_from_referer, origin_match_domain
from common.utils.whitelist import whiteUtils
from fastapi.security.utils import get_authorization_scheme_param
from common.core.deps import get_i18n
@@ -31,6 +33,26 @@ def __init__(self, app):
async def dispatch(self, request, call_next):
if self.is_options(request) or whiteUtils.is_whitelisted(request.url.path):
+ # 动态处理 /system/assistant/info/{id} 的 CORS 预检
+ if request.method == "OPTIONS":
+ origin = request.headers.get("origin", "")
+ if origin:
+ match = re.search(r'/system/assistant/info/(\d+)', request.url.path)
+ if match:
+ assistant_id = int(match.group(1))
+ with Session(engine) as session:
+ db_model = session.get(AssistantModel, assistant_id)
+ if db_model and origin_match_domain(origin, db_model.domain):
+ return Response(
+ status_code=200,
+ headers={
+ "Access-Control-Allow-Origin": origin,
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
+ "Access-Control-Allow-Headers": "*",
+ "Access-Control-Allow-Credentials": "true",
+ "Access-Control-Max-Age": "600",
+ },
+ )
return await call_next(request)
assistantTokenKey = settings.ASSISTANT_TOKEN_KEY
assistantToken = request.headers.get(assistantTokenKey)
diff --git a/frontend/public/assistant.js b/frontend/public/assistant.js
deleted file mode 100644
index 5d74d5c72..000000000
--- a/frontend/public/assistant.js
+++ /dev/null
@@ -1,895 +0,0 @@
-;(function () {
- window.sqlbot_assistant_handler = window.sqlbot_assistant_handler || {}
- const defaultData = {
- id: '1',
- show_guide: false,
- float_icon: '',
- domain_url: 'http://localhost:5173',
- header_font_color: 'rgb(100, 106, 115)',
- x_type: 'right',
- y_type: 'bottom',
- x_val: '30',
- y_val: '30',
- float_icon_drag: false,
- }
- const script_id_prefix = 'sqlbot-assistant-float-script-'
- const guideHtml = `
-
-
-
-
-
🌟 遇见问题,不再有障碍!
-
你好,我是你的智能小助手。
- 点我,开启高效解答模式,让问题变成过去式。
-
-
-
-
-
-`
-
- const chatButtonHtml = (data) => `
-`
-
- const getChatContainerHtml = (data) => {
- let srcUrl = `${data.domain_url}/#/assistant?id=${data.id}&online=${!!data.online}&name=${encodeURIComponent(data.name)}`
- if (data.userFlag) {
- srcUrl += `&userFlag=${data.userFlag || ''}`
- }
- if (data.history) {
- srcUrl += `&history=${data.history}`
- }
- if (data.lang) {
- srcUrl += `&lang=${data.lang}`
- }
- return `
-
-
-
-`
- }
-
- function getHighestZIndexValue() {
- try {
- let maxZIndex = -Infinity
- let foundAny = false
-
- const allElements = document.all || document.querySelectorAll('*')
-
- for (let i = 0; i < allElements.length; i++) {
- const element = allElements[i]
-
- if (!element || element.nodeType !== 1) continue
-
- const styles = window.getComputedStyle(element)
-
- const position = styles.position
- if (position === 'static') continue
-
- const zIndex = styles.zIndex
- let zIndexValue
-
- if (zIndex === 'auto') {
- zIndexValue = 0
- } else {
- zIndexValue = parseInt(zIndex, 10)
- if (isNaN(zIndexValue)) continue
- }
-
- foundAny = true
-
- // 快速返回:如果找到很大的z-index,很可能就是最大值
- /* if (zIndexValue > 10000) {
- return zIndexValue;
- } */
-
- if (zIndexValue > maxZIndex) {
- maxZIndex = zIndexValue
- }
- }
- return foundAny ? maxZIndex : 0
- } catch (error) {
- console.warn('获取最高z-index时出错,返回默认值0:', error)
- return 0
- }
- }
-
- /**
- * 初始化引导
- * @param {*} root
- */
- const initGuide = (root) => {
- root.insertAdjacentHTML('beforeend', guideHtml)
- const button = root.querySelector('.sqlbot-assistant-button')
- const close_icon = root.querySelector('.sqlbot-assistant-close')
- const close_func = () => {
- root.removeChild(root.querySelector('.sqlbot-assistant-tips'))
- root.removeChild(root.querySelector('.sqlbot-assistant-mask'))
- localStorage.setItem('sqlbot_assistant_mask_tip', true)
- }
- button.onclick = close_func
- close_icon.onclick = close_func
- }
- const initChat = (root, data) => {
- // 添加对话icon
- root.insertAdjacentHTML('beforeend', chatButtonHtml(data))
- // 添加对话框
- root.insertAdjacentHTML('beforeend', getChatContainerHtml(data))
- // 按钮元素
- const chat_button = root.querySelector('.sqlbot-assistant-chat-button')
- let chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > svg')
- if (data.float_icon) {
- chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > img')
- }
- chat_button_img.style.display = 'block'
- function resizeImg() {
- const rate = window.outerWidth / window.innerWidth
- chat_button_img.style.width = `${30 * (1 / rate)}px`
- chat_button_img.style.height = `${30 * (1 / rate)}px`
- }
- resizeImg()
- window.addEventListener('resize', resizeImg)
- // 对话框元素
- const chat_container = root.querySelector('#sqlbot-assistant-chat-container')
- // 引导层
- const mask_content = root.querySelector('.sqlbot-assistant-mask > .sqlbot-assistant-content')
- const mask_tips = root.querySelector('.sqlbot-assistant-tips')
- chat_button_img.onload = (event) => {
- if (mask_content) {
- mask_content.style.width = chat_button_img.width + 'px'
- mask_content.style.height = chat_button_img.height + 'px'
- if (data.x_type == 'left') {
- mask_tips.style.marginLeft =
- (chat_button_img.naturalWidth > 500 ? 500 : chat_button_img.naturalWidth) - 64 + 'px'
- } else {
- mask_tips.style.marginRight =
- (chat_button_img.naturalWidth > 500 ? 500 : chat_button_img.naturalWidth) - 64 + 'px'
- }
- }
- }
-
- const viewport = root.querySelector('.sqlbot-assistant-openviewport')
- const closeviewport = root.querySelector('.sqlbot-assistant-closeviewport')
- const close_func = () => {
- chat_container.style['display'] =
- chat_container.style['display'] == 'block' ? 'none' : 'block'
- chat_button.style['display'] = chat_container.style['display'] == 'block' ? 'none' : 'block'
- }
- close_icon = chat_container.querySelector('.sqlbot-assistant-chat-close')
- chat_button.onclick = close_func
- close_icon.onclick = close_func
- const viewport_func = () => {
- if (chat_container.classList.contains('sqlbot-assistant-enlarge')) {
- chat_container.classList.remove('sqlbot-assistant-enlarge')
- viewport.classList.remove('sqlbot-assistant-viewportnone')
- closeviewport.classList.add('sqlbot-assistant-viewportnone')
- } else {
- chat_container.classList.add('sqlbot-assistant-enlarge')
- viewport.classList.add('sqlbot-assistant-viewportnone')
- closeviewport.classList.remove('sqlbot-assistant-viewportnone')
- }
- }
- if (data.float_icon_drag) {
- chat_button.setAttribute('draggable', 'true')
-
- let startX = 0
- let startY = 0
- const img = new Image()
- img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='
- chat_button.addEventListener('dragstart', (e) => {
- startX = e.clientX - chat_button.offsetLeft
- startY = e.clientY - chat_button.offsetTop
- e.dataTransfer.setDragImage(img, 0, 0)
- })
-
- chat_button.addEventListener('drag', (e) => {
- if (e.clientX && e.clientY) {
- const left = e.clientX - startX
- const top = e.clientY - startY
-
- const maxX = window.innerWidth - chat_button.offsetWidth
- const maxY = window.innerHeight - chat_button.offsetHeight
-
- chat_button.style.left = Math.min(Math.max(0, left), maxX) + 'px'
- chat_button.style.top = Math.min(Math.max(0, top), maxY) + 'px'
- }
- })
-
- let touchStartX = 0
- let touchStartY = 0
-
- chat_button.addEventListener('touchstart', (e) => {
- touchStartX = e.touches[0].clientX - chat_button.offsetLeft
- touchStartY = e.touches[0].clientY - chat_button.offsetTop
- e.preventDefault()
- })
-
- chat_button.addEventListener('touchmove', (e) => {
- const left = e.touches[0].clientX - touchStartX
- const top = e.touches[0].clientY - touchStartY
-
- const maxX = window.innerWidth - chat_button.offsetWidth
- const maxY = window.innerHeight - chat_button.offsetHeight
-
- chat_button.style.left = Math.min(Math.max(0, left), maxX) + 'px'
- chat_button.style.top = Math.min(Math.max(0, top), maxY) + 'px'
-
- e.preventDefault()
- })
- }
- /* const drag = (e) => {
- if (['touchmove', 'touchstart'].includes(e.type)) {
- chat_button.style.top = e.touches[0].clientY - chat_button_img.clientHeight / 2 + 'px'
- chat_button.style.left = e.touches[0].clientX - chat_button_img.clientHeight / 2 + 'px'
- } else {
- chat_button.style.top = e.y - chat_button_img.clientHeight / 2 + 'px'
- chat_button.style.left = e.x - chat_button_img.clientHeight / 2 + 'px'
- }
- chat_button.style.width = chat_button_img.clientHeight + 'px'
- chat_button.style.height = chat_button_img.clientHeight + 'px'
- }
- if (data.float_icon_drag) {
- chat_button.setAttribute('draggable', 'true')
- chat_button.addEventListener('drag', drag)
- chat_button.addEventListener('dragover', (e) => {
- e.preventDefault()
- })
- chat_button.addEventListener('dragend', drag)
- chat_button.addEventListener('touchstart', drag)
- chat_button.addEventListener('touchmove', drag)
- } */
- viewport.onclick = viewport_func
- closeviewport.onclick = viewport_func
- }
- /**
- * 第一次进来的引导提示
- */
- function initsqlbot_assistant(data) {
- const sqlbot_div = document.createElement('div')
- const root = document.createElement('div')
- const sqlbot_root_id = 'sqlbot-assistant-root-' + data.id
- root.id = sqlbot_root_id
- initsqlbot_assistantStyle(sqlbot_div, sqlbot_root_id, data)
- sqlbot_div.appendChild(root)
- document.body.appendChild(sqlbot_div)
- const sqlbot_assistant_mask_tip = localStorage.getItem('sqlbot_assistant_mask_tip')
- if (sqlbot_assistant_mask_tip == null && data.show_guide) {
- initGuide(root)
- }
- initChat(root, data)
- }
-
- // 初始化全局样式
- function initsqlbot_assistantStyle(root, sqlbot_assistantId, data) {
- const maxZIndex = getHighestZIndexValue()
- const zIndex = Math.max((maxZIndex || 0) + 1, 10000)
- const maskZIndex = zIndex + 1
- style = document.createElement('style')
- style.type = 'text/css'
- style.innerText = `
- /* 放大 */
- #sqlbot-assistant .sqlbot-assistant-enlarge {
- width: 50%!important;
- height: 100%!important;
- bottom: 0!important;
- right: 0 !important;
- }
- @media only screen and (max-width: 768px){
- #sqlbot-assistant .sqlbot-assistant-enlarge {
- width: 100%!important;
- height: 100%!important;
- right: 0 !important;
- bottom: 0!important;
- }
- }
-
- /* 引导 */
-
- #sqlbot-assistant .sqlbot-assistant-mask {
- position: fixed;
- z-index: ${maskZIndex};
- background-color: transparent;
- height: 100%;
- width: 100%;
- top: 0;
- left: 0;
- }
- #sqlbot-assistant .sqlbot-assistant-mask .sqlbot-assistant-content {
- width: 64px;
- height: 64px;
- box-shadow: 1px 1px 1px 9999px rgba(0,0,0,.6);
- position: absolute;
- ${data.x_type}: ${data.x_val}px;
- ${data.y_type}: ${data.y_val}px;
- z-index: ${maskZIndex};
- }
- #sqlbot-assistant .sqlbot-assistant-tips {
- position: fixed;
- ${data.x_type}:calc(${data.x_val}px + 75px);
- ${data.y_type}: calc(${data.y_val}px + 0px);
- padding: 22px 24px 24px;
- border-radius: 6px;
- color: #ffffff;
- font-size: 14px;
- background: #3370FF;
- z-index: ${maskZIndex};
- }
- #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-arrow {
- position: absolute;
- background: #3370FF;
- width: 10px;
- height: 10px;
- pointer-events: none;
- transform: rotate(45deg);
- box-sizing: border-box;
- /* left */
- ${data.x_type}: -5px;
- ${data.y_type}: 33px;
- border-left-color: transparent;
- border-bottom-color: transparent
- }
- #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-title {
- font-size: 20px;
- font-weight: 500;
- margin-bottom: 8px;
- }
- #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-button {
- text-align: right;
- margin-top: 24px;
- }
- #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-button button {
- border-radius: 4px;
- background: #FFF;
- padding: 3px 12px;
- color: #3370FF;
- cursor: pointer;
- outline: none;
- border: none;
- }
- #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-button button::after{
- border: none;
- }
- #sqlbot-assistant .sqlbot-assistant-tips .sqlbot-assistant-close {
- position: absolute;
- right: 20px;
- top: 20px;
- cursor: pointer;
-
- }
- #sqlbot-assistant-chat-container {
- width: 460px;
- height: 640px;
- display:none;
- }
- @media only screen and (max-width: 768px) {
- #sqlbot-assistant-chat-container {
- width: 100%;
- height: 70%;
- right: 0 !important;
- }
- }
-
- #sqlbot-assistant .sqlbot-assistant-chat-button{
- position: fixed;
- ${data.x_type}: ${data.x_val}px;
- ${data.y_type}: ${data.y_val}px;
- cursor: pointer;
- z-index: ${zIndex};
- }
- #sqlbot-assistant #sqlbot-assistant-chat-container{
- z-index: ${zIndex};
- position: relative;
- border-radius: 8px;
- //border: 1px solid #ffffff;
- background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1;
- box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.10);
- position: fixed;bottom: 16px;right: 16px;overflow: hidden;
- }
-
- .ed-overlay-dialog {
- margin-top: 50px;
- }
- .ed-drawer {
- margin-top: 50px;
- }
-
- #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-operate{
- top: 18px;
- right: 15px;
- position: absolute;
- display: flex;
- align-items: center;
- line-height: 18px;
- }
- #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-operate .sqlbot-assistant-chat-close{
- margin-left:15px;
- cursor: pointer;
- }
- #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-operate .sqlbot-assistant-openviewport{
-
- cursor: pointer;
- }
- #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-operate .sqlbot-assistant-closeviewport{
-
- cursor: pointer;
- }
- #sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-viewportnone{
- display:none;
- }
- #sqlbot-assistant #sqlbot-assistant-chat-container #sqlbot-assistant-chat-iframe-${data.id} {
- height:100%;
- width:100%;
- border: none;
- }
- #sqlbot-assistant #sqlbot-assistant-chat-container {
- animation: appear .4s ease-in-out;
- }
- @keyframes appear {
- from {
- height: 0;;
- }
-
- to {
- height: 600px;
- }
- }`.replaceAll('#sqlbot-assistant ', `#${sqlbot_assistantId} `)
- root.appendChild(style)
- }
- function getParam(src, key) {
- const url = new URL(src)
- return url.searchParams.get(key)
- }
- function parsrCertificate(config) {
- const certificateList = config.certificate
- if (!certificateList?.length) {
- return null
- }
- const list = certificateList.map((item) => formatCertificate(item)).filter((item) => !!item)
- return JSON.stringify(list)
- }
- function isEmpty(obj) {
- return obj == null || typeof obj == 'undefined'
- }
- function formatCertificate(item) {
- const { type, source, target, target_key, target_val } = item
- let source_val = null
- if (type.toLocaleLowerCase() == 'localstorage') {
- source_val = localStorage.getItem(source)
- }
- if (type.toLocaleLowerCase() == 'sessionstorage') {
- source_val = sessionStorage.getItem(source)
- }
- if (type.toLocaleLowerCase() == 'cookie') {
- source_val = getCookie(source)
- }
- if (type.toLocaleLowerCase() == 'custom') {
- source_val = source
- }
- if (isEmpty(source_val)) {
- return null
- }
- return {
- target,
- key: target_key || source,
- value: (target_val && eval(target_val)) || source_val,
- }
- }
- function getCookie(key) {
- if (!key || !document.cookie) {
- return null
- }
- const cookies = document.cookie.split(';')
- for (let i = 0; i < cookies.length; i++) {
- const cookie = cookies[i].trim()
-
- if (cookie.startsWith(key + '=')) {
- return decodeURIComponent(cookie.substring(key.length + 1))
- }
- }
- return null
- }
- function registerMessageEvent(id, data) {
- const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
- const url = iframe.src
- const eventName = 'sqlbot_assistant_event'
- window.addEventListener('message', (event) => {
- if (event.data?.eventName === eventName) {
- if (event.data?.messageId !== id) {
- return
- }
- if (event.data?.busi == 'ready' && event.data?.ready) {
- params = {
- eventName,
- messageId: id,
- hostOrigin: window.location.origin,
- }
- if (data.type === 1) {
- const certificate = parsrCertificate(data)
- params['busi'] = 'certificate'
- params['certificate'] = certificate
- }
- const contentWindow = iframe.contentWindow
- contentWindow.postMessage(params, url)
- }
- }
- })
- }
- function loadScript(src, id) {
- const domain_url = getDomain(src)
- const online = getParam(src, 'online')
- const userFlag = getParam(src, 'userFlag')
- const history = getParam(src, 'history')
- const lang = getParam(src, 'lang')
- let url = `${domain_url}/api/v1/system/assistant/info/${id}`
- if (domain_url.includes('5173')) {
- url = url.replace('5173', '8000')
- }
- fetch(url)
- .then((response) => response.json())
- .then((res) => {
- if (!res.data) {
- throw new Error(res)
- }
- const data = res.data
- const config_json = data.configuration
- let tempData = Object.assign(defaultData, data)
- if (tempData.configuration) {
- delete tempData.configuration
- }
- if (config_json) {
- const config = JSON.parse(config_json)
- if (config) {
- delete config.id
- tempData = Object.assign(tempData, config)
- }
- }
- tempData['id'] = id
- tempData['domain_url'] = domain_url
-
- if (tempData['float_icon'] && !tempData['float_icon'].startsWith('http://')) {
- tempData['float_icon'] =
- `${domain_url}/api/v1/system/assistant/picture/${tempData['float_icon']}`
-
- if (domain_url.includes('5173')) {
- tempData['float_icon'] = tempData['float_icon'].replace('5173', '8000')
- }
- }
-
- tempData['online'] = online && online.toString().toLowerCase() == 'true'
- tempData['userFlag'] = userFlag
- tempData['history'] = history
- tempData['lang'] = lang
- initsqlbot_assistant(tempData)
- registerMessageEvent(id, tempData)
- })
- .catch((e) => {
- showMsg('嵌入失败', e.message)
- })
- }
- function getDomain(src) {
- return src.substring(0, src.indexOf('/assistant.js'))
- }
- function init() {
- const sqlbotScripts = document.querySelectorAll(`script[id^="${script_id_prefix}"]`)
- const scriptsArray = Array.from(sqlbotScripts)
- const src_list = scriptsArray.map((script) => script.src)
- src_list.forEach((src) => {
- const id = getParam(src, 'id')
- window.sqlbot_assistant_handler[id] = window.sqlbot_assistant_handler[id] || {}
- window.sqlbot_assistant_handler[id]['id'] = id
- const propName = script_id_prefix + id + '-state'
- if (window[propName]) {
- return true
- }
- window[propName] = true
- loadScript(src, id)
- expposeGlobalMethods(id)
- })
- }
-
- function showMsg(title, content) {
- // 检查并创建容器(如果不存在)
- let container = document.getElementById('messageContainer')
- if (!container) {
- container = document.createElement('div')
- container.id = 'messageContainer'
- container.style.position = 'fixed'
- container.style.bottom = '20px'
- container.style.right = '20px'
- container.style.zIndex = '1000'
- document.body.appendChild(container)
- } else {
- // 如果容器已存在,先移除旧弹窗
- const oldMessage = container.querySelector('div')
- if (oldMessage) {
- oldMessage.style.transform = 'translateX(120%)'
- oldMessage.style.opacity = '0'
- setTimeout(() => {
- container.removeChild(oldMessage)
- }, 300)
- }
- }
-
- // 创建弹窗元素
- const messageBox = document.createElement('div')
- messageBox.style.width = '240px'
- messageBox.style.minHeight = '100px'
- messageBox.style.background = 'linear-gradient(135deg, #ff6b6b, #ff8e8e)'
- messageBox.style.borderRadius = '8px'
- messageBox.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'
- messageBox.style.padding = '15px'
- messageBox.style.color = 'white'
- messageBox.style.fontFamily = 'Arial, sans-serif'
- messageBox.style.display = 'flex'
- messageBox.style.flexDirection = 'column'
- messageBox.style.transform = 'translateX(120%)'
- messageBox.style.transition = 'transform 0.3s ease-out'
- messageBox.style.opacity = '0'
- messageBox.style.transition = 'opacity 0.3s ease, transform 0.3s ease'
- messageBox.style.overflow = 'hidden'
-
- // 创建标题元素
- const titleElement = document.createElement('div')
- titleElement.style.fontSize = '18px'
- titleElement.style.fontWeight = 'bold'
- titleElement.style.marginBottom = '10px'
- titleElement.style.borderBottom = '1px solid rgba(255, 255, 255, 0.3)'
- titleElement.style.paddingBottom = '8px'
- titleElement.textContent = title
-
- // 创建内容元素
- const contentElement = document.createElement('div')
- contentElement.style.fontSize = '14px'
- contentElement.style.flexGrow = '1'
- contentElement.style.overflow = 'auto'
- contentElement.textContent = content
-
- // 组装元素
- messageBox.appendChild(titleElement)
- messageBox.appendChild(contentElement)
-
- // 添加到容器
- container.appendChild(messageBox)
-
- // 触发显示动画
- setTimeout(() => {
- messageBox.style.transform = 'translateX(0)'
- messageBox.style.opacity = '1'
- }, 10)
-
- // 3秒后自动隐藏
- setTimeout(() => {
- messageBox.style.transform = 'translateX(120%)'
- messageBox.style.opacity = '0'
- setTimeout(() => {
- container.removeChild(messageBox)
- // 如果容器是空的,也移除容器
- if (container.children.length === 0) {
- document.body.removeChild(container)
- }
- }, 300)
- }, 5000)
- }
-
- /* function hideMsg() {
- const container = document.getElementById('messageContainer');
- if (container) {
- const messageBox = container.querySelector('div');
- if (messageBox) {
- messageBox.style.transform = 'translateX(120%)';
- messageBox.style.opacity = '0';
- setTimeout(() => {
- container.removeChild(messageBox);
- // 如果容器是空的,也移除容器
- if (container.children.length === 0) {
- document.body.removeChild(container);
- }
- }, 300);
- }
- }
- } */
-
- function updateParam(target_url, key, newValue) {
- try {
- const url = new URL(target_url)
- const [hashPath, hashQuery] = url.hash.split('?')
- let searchParams
- if (hashQuery) {
- searchParams = new URLSearchParams(hashQuery)
- } else {
- searchParams = url.searchParams
- }
- searchParams.set(key, newValue)
- if (hashQuery) {
- url.hash = `${hashPath}?${searchParams.toString()}`
- } else {
- url.search = searchParams.toString()
- }
- return url.toString()
- } catch (e) {
- console.error('Invalid URL:', target_url)
- return target_url
- }
- }
- function expposeGlobalMethods(id) {
- window.sqlbot_assistant_handler[id]['setOnline'] = (online) => {
- if (online != null && typeof online != 'boolean') {
- throw new Error('The parameter can only be of type boolean')
- }
- const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
- if (iframe) {
- const url = iframe.src
- const eventName = 'sqlbot_assistant_event'
- const params = {
- busi: 'setOnline',
- online,
- eventName,
- messageId: id,
- }
- const contentWindow = iframe.contentWindow
- contentWindow.postMessage(params, url)
- }
- }
- window.sqlbot_assistant_handler[id]['setLang'] = (lang) => {
- if (lang != null && typeof lang != 'string') {
- throw new Error('The parameter can only be of type string')
- }
- const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
- if (iframe) {
- const url = iframe.src
- const eventName = 'sqlbot_assistant_event'
- const params = {
- busi: 'setLang',
- lang,
- eventName,
- messageId: id,
- }
- const contentWindow = iframe.contentWindow
- contentWindow.postMessage(params, url)
- }
- }
- window.sqlbot_assistant_handler[id]['refresh'] = (online, userFlag) => {
- if (online != null && typeof online != 'boolean') {
- throw new Error('The parameter can only be of type boolean')
- }
- const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
- if (iframe) {
- const url = iframe.src
- let new_url = updateParam(url, 't', Date.now())
- if (online != null) {
- new_url = updateParam(new_url, 'online', online)
- }
- if (userFlag != null) {
- new_url = updateParam(new_url, 'userFlag', userFlag)
- }
- iframe.src = 'about:blank'
- setTimeout(() => {
- iframe.src = new_url
- }, 500)
- }
- }
- window.sqlbot_assistant_handler[id]['destroy'] = () => {
- const sqlbot_root_id = 'sqlbot-assistant-root-' + id
- const container_div = document.getElementById(sqlbot_root_id)
- if (container_div) {
- const root_div = container_div.parentNode
- if (root_div?.parentNode) {
- root_div.parentNode.removeChild(root_div)
- }
- }
-
- const scriptDom = document.getElementById(`sqlbot-assistant-float-script-${id}`)
- if (scriptDom) {
- scriptDom.parentNode.removeChild(scriptDom)
- }
- const propName = script_id_prefix + id + '-state'
- if (window[propName]) {
- delete window[propName]
- }
- delete window.sqlbot_assistant_handler[id]
- }
- window.sqlbot_assistant_handler[id]['setHistory'] = (show) => {
- if (show != null && typeof show != 'boolean') {
- throw new Error('The parameter can only be of type boolean')
- }
- const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
- if (iframe) {
- const url = iframe.src
- const eventName = 'sqlbot_assistant_event'
- const params = {
- busi: 'setHistory',
- show,
- eventName,
- messageId: id,
- }
- const contentWindow = iframe.contentWindow
- contentWindow.postMessage(params, url)
- }
- }
- window.sqlbot_assistant_handler[id]['createConversation'] = (param) => {
- const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
- if (iframe) {
- const url = iframe.src
- const eventName = 'sqlbot_assistant_event'
- const params = {
- busi: 'createConversation',
- param,
- eventName,
- messageId: id,
- }
- const contentWindow = iframe.contentWindow
- contentWindow.postMessage(params, url)
- }
- }
- }
- // window.addEventListener('load', init)
- const executeWhenReady = (fn) => {
- if (
- document.readyState === 'complete' ||
- (document.readyState !== 'loading' && !document.documentElement.doScroll)
- ) {
- setTimeout(fn, 0)
- } else {
- const onReady = () => {
- document.removeEventListener('DOMContentLoaded', onReady)
- window.removeEventListener('load', onReady)
- fn()
- }
- document.addEventListener('DOMContentLoaded', onReady)
- window.addEventListener('load', onReady)
- }
- }
-
- executeWhenReady(init)
-})()
diff --git a/frontend/src/views/embedded/index.vue b/frontend/src/views/embedded/index.vue
index 9bfe191e1..23cd372f4 100644
--- a/frontend/src/views/embedded/index.vue
+++ b/frontend/src/views/embedded/index.vue
@@ -20,7 +20,7 @@
{
chatRef.value?.showFloatPopover()
}
-const validator = ref({
+/* const validator = ref({
id: '',
valid: false,
id_match: false,
token: '',
-})
+}) */
const appName = ref('')
const loading = ref(true)
+const tokenReady = ref(false)
const eventName = 'sqlbot_assistant_event'
+
+let resolveTokenReady: (() => void) | null = null
+const tokenReadyPromise = new Promise((resolve) => {
+ resolveTokenReady = resolve
+})
const communicationCb = async (event: any) => {
if (event.data?.eventName === eventName) {
if (event.data?.messageId !== route.query.id) {
return
}
+ if (event.data['sqlbot_embedded_token']) {
+ assistantStore.setToken(event.data['sqlbot_embedded_token'])
+ resolveTokenReady?.()
+ tokenReady.value = true
+ }
if (event.data?.busi == 'certificate') {
const certificate = event.data['certificate']
assistantStore.setType(1)
@@ -201,13 +212,13 @@ onBeforeMount(async () => {
const now = Date.now()
assistantStore.setFlag(now)
assistantStore.setId(assistantId?.toString() || '')
- const param = {
+ /* const param = {
id: assistantId,
virtual: userFlag || assistantStore.getFlag,
online,
}
validator.value = await assistantApi.validate(param)
- assistantStore.setToken(validator.value.token)
+ assistantStore.setToken(validator.value.token) */
assistantStore.setAssistant(true)
loading.value = false
@@ -220,6 +231,8 @@ onBeforeMount(async () => {
}
window.parent.postMessage(readyData, '*')
+ await tokenReadyPromise
+
request.get(`/system/assistant/${assistantId}`).then((res) => {
if (res.name) {
appName.value = res.name
diff --git a/frontend/src/views/embedded/page.vue b/frontend/src/views/embedded/page.vue
index 8e398b347..b20dee9b9 100644
--- a/frontend/src/views/embedded/page.vue
+++ b/frontend/src/views/embedded/page.vue
@@ -4,7 +4,7 @@
:class="dynamicType === 4 ? 'sqlbot--embedded-page' : 'sqlbot-embedded-assistant-page'"
>
void) | null = null
+const tokenReadyPromise = new Promise((resolve) => {
+ resolveTokenReady = resolve
+})
const communicationCb = async (event: any) => {
if (event.data?.eventName === eventName) {
if (event.data?.messageId !== route.query.id) {
return
}
+ if (
+ event.data['type'] &&
+ parseInt(event.data['type']) !== 4 &&
+ event.data['sqlbot_embedded_token']
+ ) {
+ assistantStore.setToken(event.data['sqlbot_embedded_token'])
+ resolveTokenReady?.()
+ tokenReady.value = true
+ }
if (event.data?.busi == 'certificate') {
const type = parseInt(event.data['type'])
const certificate = event.data['certificate']
@@ -84,6 +99,7 @@ const communicationCb = async (event: any) => {
if (type === 4) {
assistantStore.setToken(certificate)
assistantStore.setAssistant(true)
+ tokenReady.value = true
try {
await userStore.info()
} catch (e) {
@@ -201,18 +217,19 @@ onBeforeMount(async () => {
registerReady(assistantId)
return
}
- const param = {
+ /* const param = {
id: assistantId,
virtual: userFlag || assistantStore.getFlag,
online,
}
validator.value = await assistantApi.validate(param)
- assistantStore.setToken(validator.value.token)
+ assistantStore.setToken(validator.value.token) */
assistantStore.setAssistant(true)
- loading.value = false
registerReady(assistantId)
+ await tokenReadyPromise
+ loading.value = false
request.get(`/system/assistant/${assistantId}`).then((res) => {
if (res?.configuration) {
const rawData = JSON.parse(res?.configuration)