Merge branch 'main' into panchen/dev_hide

This commit is contained in:
Sylar Chen 2025-11-25 09:20:01 +08:00 committed by GitHub
commit fb711c6964
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 5165 additions and 2911 deletions

View file

@ -1435,83 +1435,135 @@ async def chat_completion(
form_data: dict,
user=Depends(get_verified_user),
):
if not request.app.state.MODELS:
await get_all_models(request, user=user)
"""
聊天完成接口 - 处理用户与 AI 模型的对话请求
model_id = form_data.get("model", None)
model_item = form_data.pop("model_item", {})
tasks = form_data.pop("background_tasks", None)
核心功能:
1. 模型验证: 检查模型是否存在及用户访问权限
2. 元数据构建: 提取 chat_id, message_id, session_id 等上下文信息
3. Payload 处理: 通过 process_chat_payload 处理消息工具调用文件等
4. 聊天执行: 调用 chat_completion_handler LLM 交互
5. 响应处理: 通过 process_chat_response 处理流式/非流式响应
6. 异步任务: 如果有 session_id创建后台任务异步执行
Args:
request: FastAPI Request 对象
form_data: 聊天请求数据包含:
- model: 模型 ID
- messages: 对话历史 (OpenAI 格式)
- chat_id: 聊天会话 ID
- id: 消息 ID
- session_id: 会话 ID (用于异步任务)
- tool_ids: 工具 ID 列表
- files: 附加文件列表
- stream: 是否流式响应
user: 已验证的用户对象
Returns:
- 同步模式: 返回 LLM 响应 (流式 StreamingResponse 或完整 JSON)
- 异步模式: 返回 {"status": True, "task_id": "xxx"}
Raises:
HTTPException 400: 模型不存在无访问权限参数错误
HTTPException 404: Chat 不存在
处理流程:
1. 加载所有模型到 app.state.MODELS
2. 验证模型访问权限 (check_model_access)
3. 构建 metadata (包含 user_id, chat_id, tool_ids )
4. 定义 process_chat 内部函数:
- 调用 process_chat_payload (处理 Pipeline/Filter/Tools)
- 调用 chat_completion_handler ( LLM 交互)
- 更新数据库消息记录
- 调用 process_chat_response (处理响应事件发射)
5. 根据是否有 session_id 决定同步/异步执行
"""
# === 1. 初始化阶段:加载模型列表 ===
if not request.app.state.MODELS:
await get_all_models(request, user=user) # 从数据库和后端服务加载所有可用模型
# === 2. 提取请求参数 ===
model_id = form_data.get("model", None) # 用户选择的模型 ID (如 "gpt-4")
model_item = form_data.pop("model_item", {}) # 模型元数据 (包含 direct 标志)
tasks = form_data.pop("background_tasks", None) # 后台任务列表
metadata = {}
try:
# === 3. 模型验证与权限检查 ===
if not model_item.get("direct", False):
# 标准模式:使用平台内置模型
if model_id not in request.app.state.MODELS:
raise Exception("Model not found")
model = request.app.state.MODELS[model_id] # 从缓存获取模型配置
model_info = Models.get_model_by_id(model_id) # 从数据库获取模型详细信息
model = request.app.state.MODELS[model_id]
model_info = Models.get_model_by_id(model_id)
# Check if user has access to the model
# 检查用户是否有权限访问该模型
if not BYPASS_MODEL_ACCESS_CONTROL and (
user.role != "admin" or not BYPASS_ADMIN_ACCESS_CONTROL
):
try:
check_model_access(user, model)
check_model_access(user, model) # 检查 RBAC 权限
except Exception as e:
raise e
else:
# Direct 模式:用户直接传入 OpenAI API 等外部模型配置
model = model_item
model_info = None
request.state.direct = True
request.state.direct = True # 标记为直连模式
request.state.model = model
# === 4. 提取模型参数 ===
model_info_params = (
model_info.params.model_dump() if model_info and model_info.params else {}
)
# Chat Params
# 流式响应分块大小 (用于控制 SSE 推送频率)
stream_delta_chunk_size = form_data.get("params", {}).get(
"stream_delta_chunk_size"
)
# 推理标签 (用于标记 AI 的思考过程,如 <think>...</think>)
reasoning_tags = form_data.get("params", {}).get("reasoning_tags")
# Model Params
# 模型参数优先级高于请求参数
if model_info_params.get("stream_delta_chunk_size"):
stream_delta_chunk_size = model_info_params.get("stream_delta_chunk_size")
if model_info_params.get("reasoning_tags") is not None:
reasoning_tags = model_info_params.get("reasoning_tags")
# === 5. 构建元数据 (metadata) - 贯穿整个处理流程的上下文 ===
metadata = {
"user_id": user.id,
"chat_id": form_data.pop("chat_id", None),
"message_id": form_data.pop("id", None),
"session_id": form_data.pop("session_id", None),
"filter_ids": form_data.pop("filter_ids", []),
"tool_ids": form_data.get("tool_ids", None),
"tool_servers": form_data.pop("tool_servers", None),
"files": form_data.get("files", None),
"features": form_data.get("features", {}),
"variables": form_data.get("variables", {}),
"model": model,
"direct": model_item.get("direct", False),
"chat_id": form_data.pop("chat_id", None), # 聊天会话 ID
"message_id": form_data.pop("id", None), # 当前消息 ID
"session_id": form_data.pop("session_id", None), # WebSocket 会话 ID (异步任务)
"filter_ids": form_data.pop("filter_ids", []), # Pipeline Filter ID 列表
"tool_ids": form_data.get("tool_ids", None), # 工具/函数调用 ID 列表
"tool_servers": form_data.pop("tool_servers", None), # 外部工具服务器配置
"files": form_data.get("files", None), # 用户上传的文件列表
"features": form_data.get("features", {}), # 功能开关 (如 web_search)
"variables": form_data.get("variables", {}), # 模板变量
"model": model, # 模型配置对象
"direct": model_item.get("direct", False), # 是否直连模式
"params": {
"stream_delta_chunk_size": stream_delta_chunk_size,
"reasoning_tags": reasoning_tags,
"function_calling": (
"native"
"native" # 原生函数调用 (如 OpenAI Function Calling)
if (
form_data.get("params", {}).get("function_calling") == "native"
or model_info_params.get("function_calling") == "native"
)
else "default"
else "default" # 默认模式 (通过 Prompt 实现)
),
},
}
# === 6. 权限二次验证:检查用户是否拥有该 chat ===
if metadata.get("chat_id") and (user and user.role != "admin"):
if not metadata["chat_id"].startswith("local:"):
if not metadata["chat_id"].startswith("local:"): # local: 前缀表示临时会话
chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id)
if chat is None:
raise HTTPException(
@ -1519,8 +1571,9 @@ async def chat_completion(
detail=ERROR_MESSAGES.DEFAULT(),
)
request.state.metadata = metadata
form_data["metadata"] = metadata
# === 7. 保存元数据到请求状态和 form_data ===
request.state.metadata = metadata # 供其他中间件/处理器访问
form_data["metadata"] = metadata # 传递给下游处理函数
except Exception as e:
log.debug(f"Error processing chat metadata: {e}")
@ -1529,13 +1582,19 @@ async def chat_completion(
detail=str(e),
)
# === 8. 定义内部处理函数 process_chat ===
async def process_chat(request, form_data, user, metadata, model):
"""处理完整的聊天流程Payload 处理 → LLM 调用 → 响应处理"""
try:
# 8.1 Payload 预处理:执行 Pipeline Filters、工具注入、RAG 检索等
form_data, metadata, events = await process_chat_payload(
request, form_data, user, metadata, model
)
# 8.2 调用 LLM 完成对话 (核心)
response = await chat_completion_handler(request, form_data, user)
# 8.3 更新数据库:保存模型 ID 到消息记录
if metadata.get("chat_id") and metadata.get("message_id"):
try:
if not metadata["chat_id"].startswith("local:"):
@ -1549,23 +1608,28 @@ async def chat_completion(
except:
pass
# 8.4 响应后处理:执行后置 Pipeline、事件发射、任务回调等
return await process_chat_response(
request, response, form_data, user, metadata, model, events, tasks
)
# 8.5 异常处理:取消任务
except asyncio.CancelledError:
log.info("Chat processing was cancelled")
try:
event_emitter = get_event_emitter(metadata)
await event_emitter(
{"type": "chat:tasks:cancel"},
{"type": "chat:tasks:cancel"}, # 通知前端任务已取消
)
except Exception as e:
pass
# 8.6 异常处理:记录错误到数据库并通知前端
except Exception as e:
log.debug(f"Error processing chat payload: {e}")
if metadata.get("chat_id") and metadata.get("message_id"):
# Update the chat message with the error
try:
# 将错误信息保存到消息记录
if not metadata["chat_id"].startswith("local:"):
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
@ -1575,6 +1639,7 @@ async def chat_completion(
},
)
# 通过 WebSocket 发送错误事件到前端
event_emitter = get_event_emitter(metadata)
await event_emitter(
{
@ -1588,21 +1653,25 @@ async def chat_completion(
except:
pass
# 8.7 清理资源:断开 MCP 客户端连接
finally:
try:
if mcp_clients := metadata.get("mcp_clients"):
for client in mcp_clients.values():
await client.disconnect()
await client.disconnect() # 断开 Model Context Protocol 客户端
except Exception as e:
log.debug(f"Error cleaning up: {e}")
pass
# === 9. 决定执行模式:异步任务 vs 同步执行 ===
if (
metadata.get("session_id")
and metadata.get("chat_id")
and metadata.get("message_id")
):
# Asynchronous Chat Processing
# 异步模式:创建后台任务,立即返回 task_id 给前端
# 前端通过 WebSocket 监听任务状态和流式响应
task_id, _ = await create_task(
request.app.state.redis,
process_chat(request, form_data, user, metadata, model),
@ -1610,6 +1679,7 @@ async def chat_completion(
)
return {"status": True, "task_id": task_id}
else:
# 同步模式:直接执行并返回响应 (流式或完整)
return await process_chat(request, form_data, user, metadata, model)

View file

@ -590,7 +590,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
try:
role = "admin" if not has_users else request.app.state.config.DEFAULT_USER_ROLE
role = "admin" if not has_users else "user"
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
if len(form_data.password.encode("utf-8")) > 72:

7213
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -65,24 +65,24 @@
"@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@tiptap/core": "^3.0.7",
"@tiptap/extension-bubble-menu": "^2.26.1",
"@tiptap/core": "^3.11.0",
"@tiptap/extension-bubble-menu": "^3.11.0",
"@tiptap/extension-code-block-lowlight": "^3.0.7",
"@tiptap/extension-drag-handle": "^3.4.5",
"@tiptap/extension-drag-handle": "^3.11.0",
"@tiptap/extension-file-handler": "^3.0.7",
"@tiptap/extension-floating-menu": "^2.26.1",
"@tiptap/extension-highlight": "^3.3.0",
"@tiptap/extension-floating-menu": "^3.11.0",
"@tiptap/extension-highlight": "^3.11.0",
"@tiptap/extension-image": "^3.0.7",
"@tiptap/extension-link": "^3.0.7",
"@tiptap/extension-list": "^3.0.7",
"@tiptap/extension-mention": "^3.0.9",
"@tiptap/extension-mention": "^3.11.0",
"@tiptap/extension-table": "^3.0.7",
"@tiptap/extension-typography": "^3.0.7",
"@tiptap/extension-youtube": "^3.0.7",
"@tiptap/extensions": "^3.0.7",
"@tiptap/pm": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.4.2",
"@tiptap/starter-kit": "^3.11.0",
"@tiptap/suggestion": "^3.11.0",
"@xyflow/svelte": "^0.1.19",
"alpinejs": "^3.15.0",
"async": "^3.2.5",

View file

@ -161,9 +161,9 @@
</button>
</Tooltip>
{/if}
{/if}
{/if} -->
{#if $mobile && !$temporaryChatEnabled && chat && chat.id}
<!-- {#if $mobile && !$temporaryChatEnabled && chat && chat.id}
<Tooltip content={$i18n.t('New Chat')}>
<button
class=" flex {$showSidebar
@ -179,9 +179,9 @@
</div>
</button>
</Tooltip>
{/if}
{/if} -->
{#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
<!-- {#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
<Menu
{chat}
{shareEnabled}
@ -202,7 +202,7 @@
</div>
</button>
</Menu>
{/if}
{/if} -->
<!-- ai-friend 屏蔽对话高级设置 -->
{#if false}
@ -223,7 +223,7 @@
{/if}
{/if}
{#if $user !== undefined && $user !== null}
<!-- {#if $user !== undefined && $user !== null}
<UserMenu
className="max-w-[240px]"
role={$user?.role}
@ -248,7 +248,7 @@
</div>
</div>
</UserMenu>
{/if}
{/if} -->
</div>
</div>
</div>

View file

@ -617,8 +617,8 @@
<div class=" self-center flex items-center justify-center size-9">
<img
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png"
class="sidebar-new-chat-icon size-6 rounded-full group-hover:hidden"
src="static/favicon.png"
class="sidebar-new-chat-icon size-6 rounded-full group-hover:hidden invert"
alt=""
/>
@ -650,8 +650,31 @@
</a>
</Tooltip>
</div>
<div class="">
<Tooltip content={$i18n.t('Memory')} placement="right">
<a
id="sidebar-memory-button"
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
href="/memories"
draggable="false"
on:click={async (e) => {
e.stopImmediatePropagation();
e.preventDefault();
goto('/memories');
itemClickHandler();
}}
aria-label={$i18n.t('Memory')}
>
<div class=" self-center flex items-center justify-center size-9">
<Sparkles strokeWidth="2" className="size-4.5" />
</div>
</a>
</Tooltip>
</div>
<!-- <div class="">
<Tooltip content={$i18n.t('Search')} placement="right">
<button
class=" cursor-pointer flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
@ -669,9 +692,9 @@
</div>
</button>
</Tooltip>
</div>
</div> -->
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
<!-- {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
<div class="">
<Tooltip content={$i18n.t('Notes')} placement="right">
<a
@ -693,9 +716,9 @@
</a>
</Tooltip>
</div>
{/if}
{/if} -->
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
<!-- {#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
<div class="">
<Tooltip content={$i18n.t('Workspace')} placement="right">
<a
@ -730,7 +753,7 @@
</a>
</Tooltip>
</div>
{/if}
{/if} -->
</div>
</button>
@ -795,8 +818,8 @@
>
<img
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png"
class="sidebar-new-chat-icon size-6 rounded-full"
src="static/favicon.png"
class="sidebar-new-chat-icon size-6 rounded-full invert"
alt=""
/>
</a>
@ -862,7 +885,7 @@
</a>
</div>
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
<!-- <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
<button
id="sidebar-search-button"
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
@ -880,7 +903,7 @@
<div class=" self-center text-sm font-primary">{$i18n.t('Search')}</div>
</div>
</button>
</div>
</div> -->
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
<a
@ -901,7 +924,7 @@
</a>
</div>
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
<!-- {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
<a
id="sidebar-notes-button"
@ -920,9 +943,9 @@
</div>
</a>
</div>
{/if}
{/if} -->
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
<!-- {#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
<a
id="sidebar-workspace-button"
@ -954,14 +977,14 @@
</div>
</a>
</div>
{/if}
{/if} -->
</div>
{#if ($models ?? []).length > 0 && ($settings?.pinnedModels ?? []).length > 0}
<PinnedModelList bind:selectedChatId {shiftKey} />
{/if}
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
<!-- {#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
<Folder
className="px-2 mt-0.5"
name={$i18n.t('Channels')}
@ -987,9 +1010,9 @@
/>
{/each}
</Folder>
{/if}
{/if} -->
{#if folders}
<!-- {#if folders}
<Folder
className="px-2 mt-0.5"
name={$i18n.t('Folders')}
@ -1039,7 +1062,7 @@
}}
/>
</Folder>
{/if}
{/if} -->
<Folder
className="px-2 mt-0.5"

513
src/lib/constants/legal.ts Normal file
View file

@ -0,0 +1,513 @@
export const agreementContent = `
使XXX()XXX()XXX有限公司及其关联公司使使XXX()使
使18使使使
使使
XXX@163.com与我们联系
1
1.1
XXX.cn网站使用XXX()
1.2
使
1.3
2
2.1 XXX()XXX()XXX()
2.2
2.3 使使
2.4
XXX()XXX()XXX()XXX() XXX()使使XXX()
3
3.1 使XXX()XXX()
3.2 18使使
3.3 使使XXX()线
3.4 使XXX()
3.6 使使XXX()XXX()XXX()XXX()
3.8 使XXX()退
3.9 使
3.10 XXX()使使使
3.11 使XXX()
4 使
4.1 使XXX()使XXX()使XXX()XXX()使
1
2
3使
4.2 使XXX()
4.3 XXX()使XXX()
4.4 XXX()使使
4.4.1 XXX()
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
4.4.2
(1)
(2)
(3)
(4)
(5)使
(6)
(7)
(8)
(9)
(10)
(11)
(12)
4.4.3
(1)
(2)使4.4.14.4.2
4.4.4 XXX()
(1)
(2)
(3)
(4)
(5)XXX()XXX()
(6)
(7)(8)XXX()XXX()
(9)XXX()XXX()
(10)XXX()XXX()使XXX()
4.4.5使
4.4.6XXX() 使XXX() XXX()使
5
5.1 XXX()
5.2XXX()XXX()
5.3使XXX()
5.4使/使XXX()使
5.5使XXX()/AI对话机器人XXX()//
广 使使
/XXX()/ XXX()XXX()AI对话机器人AI对话内容使使使
6
6.1 XXX()XXX()使
7
7.1 XXX()XXX()使XXX()
7.2 XXX()
7.3 XXX()使
8
8.1 XXX()使XXX()XXX()使XXX()XXX()XXX()XXX()XXX()XXX()
8.2 使XXX()
8.3 4 使XXX()XXX()9.2
9
9.1 使XXX@163.com我们会在收到您的投诉材料后进行处理
9.2 使XXX@163.com申诉所需的材料至少包括您的账号信息使使
9.3 15
10
10.1 XXX()使XXX()XXX()
10.2 XXX()使
10.3 XXX()XXX()使XXX()XXX()使XXX()
10.4 XXX()使
11
11.1
11.2 XXX有限公司住所地有管辖权的法院起诉
12
12.1 使使
`;
export const privacyContent = `
XXX科技有限公司运营的产品XXXXXX.cn"我们"
使使/使使/
使
使
使
1/ TODO()
1 便
/
使/使XXX()
2使使使/
3
2
////
///使/
//使
3
user ID (user IDopen ID信息)/使/使
4
使
5
XXX()便///
6SDK统计服务
1SDK统计服务SDK数据将不含与我们提供产品或服务无关的数据SDK服务请具体查阅本协议附件二所示XXX()使SDK类服务前先行查看其隐私条款
SDK统计服务SDK或其他类似的应用程序侵权您的个人信息
XXX@163.com
2SDK/API/JS代码版本/WiFi等CPU和电池使用情况等
7
使使XXX()
8
使
使 Cookie
Cookie
1 Cookie Cookie Cookie
2 Cookie Cookie Cookie Cookie 访
Cookie 使 URL退
Do Not Track
Do Not Track Do Not Track Do Not Track
1
2
3
4访
5
1
2
1
2
1
1使使
2使使
3
2
1使访使"服务" SSL https 使使访访
2
3使
4访便
5
1
2
3
使
访
1访使访访
访访 XXX()--
访XXX@163.com30访
2使使访XXX@163.com
"(一)访问您的个人信息"
XXX@163.com30
1
2使
3
4使
5
XXX()-XXX()XXX@163.com发送邮件申请删除您的个人信息XXX()/
"第一部分"使
访 XXX()
1XXX()-
2XXX@163.com
3
4
XXX@163.com
1
2
3
4
5
/
使
14
使XXX()
XXX@163.com
XXX@163.com
XXX有限公司住所地享有管辖权的人民法院提起诉讼
XXX()
"您"使访使/
使
使使/
便使/访XXX()使/使
14使/
使
1使/使
2/使使使使
3使使
1
2
3
4
5
/
使
访
WEB端查询和访问儿童的相关个人信息
/
WEB端更正//
使
1WEB端联系我们删除儿童的相关个人信息使/
2
1使
2使
3
4使
5
/
/
访
使/
·XXX有限公司
·XXX@163.com
XXX()SDK服务商
SDK服务
IMEI/IMSISIM卡序列号/MAC地址 https://weixin.qq.com/cgi-bin/readtemplate?lang=3Dzh_CN&t=3Dweixin_agreement&s=3Dprivacy
QQ主体 QQ第三方登录功能 App开发者和/QQ直接收集的个人信息App终端用户在使用QQ互联SDK产品和/App开发者或者其他第三方获取的App开发者和/ https://wiki.connect.qq.com/qq
IMEI/IMSISIM卡序列号/MAC地址 https://opendocs.alipay.com/open/01g6qm
`;

View file

@ -171,7 +171,7 @@
{$i18n.t('Add Memory')}
</div>
</button>
<button
<!-- <button
class="px-4 py-2 text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition"
on:click={() => {
if ($memories.length > 0) {
@ -198,7 +198,7 @@
</svg>
{$i18n.t('Clear memory')}
</div>
</button>
</button> -->
</div>
</div>

View file

@ -12,11 +12,13 @@
import { ldapUserSignIn, getSessionUser, userSignIn, userSignUp } from '$lib/apis/auths';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { agreementContent as defaultAgreementContent, privacyContent as defaultPrivacyContent } from '$lib/constants/legal';
import { WEBUI_NAME, config, user, socket } from '$lib/stores';
import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
import Spinner from '$lib/components/common/Spinner.svelte';
import Modal from '$lib/components/common/Modal.svelte';
import OnBoarding from '$lib/components/OnBoarding.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import { redirect } from '@sveltejs/kit';
@ -35,6 +37,12 @@
let confirmPassword = '';
let ldapUsername = '';
let agreeToTerms = false;
let showAgreementModal = false;
let agreementContent = defaultAgreementContent;
let agreeToPrivacy = false;
let showPrivacyModal = false;
let privacyContent = defaultPrivacyContent;
const setSessionUser = async (sessionUser, redirectPath: string | null = null) => {
if (sessionUser) {
@ -95,8 +103,16 @@
if (mode === 'ldap') {
await ldapSignInHandler();
} else if (mode === 'signin') {
if (!agreeToTerms) {
toast.error('如果要登陆,请先同意用户协议');
return;
}
await signInHandler();
} else {
if (!agreeToPrivacy) {
toast.error('如果要注册,请先同意隐私协议');
return;
}
await signUpHandler();
}
};
@ -229,7 +245,7 @@
<img
id="logo"
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png"
src="static/favicon.png"
class="size-24 rounded-full"
alt=""
/>
@ -334,6 +350,29 @@
/>
</div>
{#if mode === 'signin'}
<div class="mt-3 flex items-start text-sm text-left text-gray-700 dark:text-gray-300 gap-2">
<input
id="agree-terms"
type="checkbox"
class="mt-0.5 rounded border-gray-300 bg-transparent text-gray-800 dark:text-gray-100 focus:ring-0"
bind:checked={agreeToTerms}
/>
<label for="agree-terms" class="leading-tight">
<span>我已阅读并同意</span>
<button
type="button"
class="ml-1 underline font-medium"
on:click={() => {
showAgreementModal = true;
}}
>
用户协议
</button>
</label>
</div>
{/if}
{#if mode === 'signup' && $config?.features?.enable_signup_password_confirmation}
<div class="mt-2">
<label
@ -353,6 +392,29 @@
/>
</div>
{/if}
{#if mode === 'signup'}
<div class="mt-3 flex items-start text-sm text-left text-gray-700 dark:text-gray-300 gap-2">
<input
id="agree-privacy"
type="checkbox"
class="mt-0.5 rounded border-gray-300 bg-transparent text-gray-800 dark:text-gray-100 focus:ring-0"
bind:checked={agreeToPrivacy}
/>
<label for="agree-privacy" class="leading-tight">
<span>我已阅读并同意</span>
<button
type="button"
class="ml-1 underline font-medium"
on:click={() => {
showPrivacyModal = true;
}}
>
隐私协议
</button>
</label>
</div>
{/if}
</div>
{/if}
<div class="mt-5">
@ -575,7 +637,7 @@
<img
id="logo"
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png"
src="static/favicon.png"
class=" w-6 rounded-full"
alt=""
/>
@ -585,3 +647,52 @@
{/if}
{/if}
</div>
<Modal bind:show={showAgreementModal} size="lg">
<div class="p-8 space-y-5 max-w-4xl">
<!-- <div class="space-y-1">
<div class="text-xl font-semibold text-gray-900 dark:text-white">用户协议</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
以下为占位说明,请替换为正式的服务条款与隐私政策。
</div>
</div> -->
<div class="text-sm text-gray-800 dark:text-gray-100 leading-relaxed space-y-3 max-h-[60vh] overflow-y-auto pr-1 marked">
{@html DOMPurify.sanitize(marked.parse(agreementContent || '这里是用户协议占位内容。请根据实际需求替换为正式的使用条款文本。'))}
</div>
<div class="flex justify-end gap-2">
<button
type="button"
class="px-4 py-2 text-sm rounded-full border border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={() => {
showAgreementModal = false;
}}
>
关闭
</button>
</div>
</div>
</Modal>
<Modal bind:show={showPrivacyModal} size="lg">
<div class="p-8 space-y-5 max-w-4xl">
<div class="text-sm text-gray-800 dark:text-gray-100 leading-relaxed space-y-3 max-h-[60vh] overflow-y-auto pr-1 marked">
{@html DOMPurify.sanitize(marked.parse(privacyContent || '这里是隐私协议占位内容。请根据实际需求替换为正式的隐私政策文本。'))}
</div>
<div class="flex justify-end gap-2">
<button
type="button"
class="px-4 py-2 text-sm rounded-full border border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={() => {
showPrivacyModal = false;
}}
>
关闭
</button>
</div>
</div>
</Modal>