ultisuite-backend/services/openwebui/static/ulti-session.js
R3D347HR4Y 621b0099d6
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(deploy): enhance Nginx configuration and API integration for UltiAI
- Updated .env.example to include new configuration options for the UltiAI branding and API endpoints.
- Enhanced Nginx configuration to support new API routes for the MCP and WebSocket connections.
- Introduced sub-filters for branding adjustments in Nginx responses.
- Added new JavaScript patch for API endpoint adjustments.
- Implemented tests for new API functionalities and improved error handling in the AI gateway.
2026-06-15 00:22:23 +02:00

324 lines
10 KiB
JavaScript

/**
* UltiAI embed bootstrap — must run before OpenWebUI SvelteKit init.
* Layout requires localStorage.token; without embed signin it redirects to /auth → redirect loop → 500.
*/
;(function () {
const STORAGE_KEY = "ulti.ai.session"
const CONTEXT_KEY = "ulti.ai.context"
const DEFAULT_MODEL_KEY = "ulti.ai.default_model"
const LAST_CHAT_PATH_KEY = "ulti.ai.last_chat_path"
const SIGNIN_URL = "/ai/api/v1/auths/signin"
const CONFIG_URL = "/api/v1/ai/config"
const BASE_SYSTEM_PROMPT = [
"Tu es UltiAI, l'assistant intégré à la suite Ultimail (mail, drive, contacts, agenda).",
"Réponds en français sauf demande contraire. Utilise les tools disponibles pour agir sur les données utilisateur.",
"Après chaque appel d'outil, réponds toujours en langage naturel : résume le résultat, cite les sources, propose la suite.",
"Ne termine jamais un tour utilisateur avec uniquement un appel d'outil sans texte explicatif.",
"Respecte strictement le paramètre limit des tools.",
].join(" ")
function readPageUrl() {
try {
return new URL(window.location.href)
} catch {
return null
}
}
function isTemporaryEmbed() {
const url = readPageUrl()
return url?.searchParams.get("temporary-chat") === "true"
}
function modelFromUrl() {
const url = readPageUrl()
if (!url) return ""
const raw = url.searchParams.get("model") || url.searchParams.get("models") || ""
return raw.split(",")[0]?.trim() || ""
}
function applySelectedModels(modelIds) {
const ids = modelIds.map((id) => String(id || "").trim()).filter(Boolean)
if (!ids.length) return
try {
sessionStorage.setItem("selectedModels", JSON.stringify(ids))
localStorage.setItem(DEFAULT_MODEL_KEY, ids[0])
} catch {
// storage unavailable
}
}
function bootstrapSelectedModels(modelHint) {
const resolved = modelFromUrl() || String(modelHint || "").trim() || localStorage.getItem(DEFAULT_MODEL_KEY) || ""
if (resolved) applySelectedModels([resolved])
return resolved
}
function restoreLastChatIfHome() {
if (isTemporaryEmbed()) return
const url = readPageUrl()
if (!url) return
const path = url.pathname.replace(/\/$/, "") || "/"
if (path !== "/ai") return
if (url.searchParams.get("model") || url.searchParams.get("models")) return
try {
const last = localStorage.getItem(LAST_CHAT_PATH_KEY)
if (!last || !/\/c\/[^/]+/.test(last)) return
const target = last.startsWith("/") ? last : `/ai${last}`
if (target === url.pathname + url.search) return
window.location.replace(target)
} catch {
// ignore
}
}
function watchChatRoute() {
const save = () => {
if (isTemporaryEmbed()) return
try {
const path = window.location.pathname
if (/\/c\/[^/]+/.test(path)) {
localStorage.setItem(LAST_CHAT_PATH_KEY, path + window.location.search)
}
} catch {
// ignore
}
}
window.addEventListener("popstate", save)
const timer = window.setInterval(save, 2000)
window.addEventListener("beforeunload", () => window.clearInterval(timer))
save()
}
function ensureEmbedSignin() {
try {
if (localStorage.getItem("token")) return
const xhr = new XMLHttpRequest()
xhr.open("POST", SIGNIN_URL, false)
xhr.withCredentials = true
xhr.setRequestHeader("Content-Type", "application/json")
xhr.send(JSON.stringify({ email: "", password: "" }))
if (xhr.status !== 200) return
const data = JSON.parse(xhr.responseText)
if (data && data.token) {
localStorage.setItem("token", data.token)
}
} catch {
// Ultimail session not ready yet — parent may post ULTI_SESSION later
}
}
async function fetchEmbedConfig() {
try {
const response = await fetch(CONFIG_URL, { credentials: "include" })
if (!response.ok) return null
return await response.json()
} catch {
return null
}
}
async function syncUserModelPreference(modelId) {
const model = String(modelId || "").trim()
if (!model) return false
const token = localStorage.getItem("token")
if (!token) return false
try {
const settingsRes = await fetch("/ai/api/v1/users/user/settings", {
headers: { Authorization: `Bearer ${token}` },
credentials: "include",
})
if (!settingsRes.ok) return false
const settings = await settingsRes.json()
const ui = settings?.ui || {}
const current = ui.models || ui.model_ids
if (Array.isArray(current) && current.length > 0 && String(current[0] || "").trim()) {
return true
}
const modelsRes = await fetch("/ai/api/models", {
headers: { Authorization: `Bearer ${token}` },
credentials: "include",
})
if (modelsRes.ok) {
const modelsPayload = await modelsRes.json()
const items = Array.isArray(modelsPayload?.data)
? modelsPayload.data
: Array.isArray(modelsPayload)
? modelsPayload
: []
const known = items.some((entry) => entry && entry.id === model)
if (!known) return false
}
const updateRes = await fetch("/ai/api/v1/users/user/settings/update", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ ui: { models: [model], model_ids: [model] } }),
})
if (!updateRes.ok) return false
applySelectedModels([model])
return true
} catch {
return false
}
}
function scheduleModelSync(modelId) {
const model = String(modelId || "").trim()
if (!model) return
let attempts = 0
const tick = async () => {
attempts += 1
const done = await syncUserModelPreference(model)
if (done || attempts >= 12) return
window.setTimeout(tick, 1000)
}
window.setTimeout(tick, 500)
}
function readContextPrompt() {
try {
const raw = sessionStorage.getItem(CONTEXT_KEY)
if (!raw) return BASE_SYSTEM_PROMPT
const parsed = JSON.parse(raw)
const parts = [BASE_SYSTEM_PROMPT, parsed.systemPrompt].filter(Boolean)
return parts.join("\n\n")
} catch {
return BASE_SYSTEM_PROMPT
}
}
function injectSystemPrompt(body) {
if (!body || !Array.isArray(body.messages)) return body
const prompt = readContextPrompt()
if (!prompt) return body
const messages = body.messages.slice()
const systemIndex = messages.findIndex((m) => m && m.role === "system")
if (systemIndex >= 0) {
messages[systemIndex] = { ...messages[systemIndex], content: prompt }
} else {
messages.unshift({ role: "system", content: prompt })
}
return { ...body, messages }
}
function patchFetch() {
if (window.__ultiFetchPatched) return
window.__ultiFetchPatched = true
const originalFetch = window.fetch.bind(window)
window.fetch = async function patchedFetch(input, init) {
const url = typeof input === "string" ? input : input instanceof Request ? input.url : ""
const method = (init && init.method) || (input instanceof Request ? input.method : "GET")
if (
method.toUpperCase() === "POST" &&
/\/api\/(v1\/)?chat\/completions(?:\?|$)/.test(url) &&
init &&
typeof init.body === "string"
) {
try {
const body = JSON.parse(init.body)
const patched = injectSystemPrompt(body)
if (patched !== body) {
init = { ...init, body: JSON.stringify(patched) }
}
} catch {
// ignore malformed bodies
}
}
return originalFetch(input, init)
}
}
function brandDocument() {
const titleEl = document.querySelector("title")
if (titleEl) {
const t = titleEl.textContent || ""
if (t.includes("Open WebUI")) {
titleEl.textContent = t.replace(/UltiAI \(Open WebUI\)/g, "UltiAI").replace(/Open WebUI/g, "UltiAI")
} else if (!t.trim()) {
titleEl.textContent = "UltiAI"
}
}
}
function watchDocumentTitle() {
brandDocument()
const titleEl = document.querySelector("title")
if (!titleEl) return
new MutationObserver(() => brandDocument()).observe(titleEl, {
childList: true,
characterData: true,
subtree: true,
})
}
restoreLastChatIfHome()
bootstrapSelectedModels("")
ensureEmbedSignin()
patchFetch()
watchDocumentTitle()
watchChatRoute()
void (async () => {
const config = await fetchEmbedConfig()
const model = bootstrapSelectedModels(config?.default_model)
if (!localStorage.getItem("token")) ensureEmbedSignin()
if (model) scheduleModelSync(model)
})()
window.addEventListener("message", (event) => {
if (event.source !== window.parent) return
const data = event.data
if (!data || typeof data !== "object") return
if (data.type === "ULTI_SESSION") {
try {
sessionStorage.setItem(
STORAGE_KEY,
JSON.stringify({
token_secret: data.token_secret,
session_id: data.session_id,
mcp_url: data.mcp_url,
enabled_tools: data.enabled_tools,
default_model: data.default_model,
updated_at: Date.now(),
})
)
} catch {
// sessionStorage unavailable
}
if (data.default_model && typeof data.default_model === "string") {
const model = data.default_model.trim()
if (model) {
applySelectedModels([model])
scheduleModelSync(model)
}
}
if (!localStorage.getItem("token")) {
ensureEmbedSignin()
}
return
}
if (data.type === "ULTI_CONTEXT_UPDATE") {
try {
sessionStorage.setItem(
CONTEXT_KEY,
JSON.stringify({
systemPrompt: data.systemPrompt,
context: data.context,
updated_at: Date.now(),
})
)
} catch {
// sessionStorage unavailable
}
}
})
})()