From 32c68e000b22822bb313630c5e4056cae094cd16 Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Mon, 23 Feb 2026 21:48:10 +0800 Subject: [PATCH 001/173] i18n: improve zh-CN translation --- src/lib/i18n/locales/zh-CN/translation.json | 84 ++++++++++----------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index 3d2529a1e9..8236443778 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -74,13 +74,13 @@ "Add tag": "添加标签", "Add Tag": "添加标签", "Add text content": "添加文本内容", - "Add to favorites": "", + "Add to favorites": "添加到收藏", "Add User": "添加用户", "Add User Group": "添加权限组", "Add webpage": "添加网页", "Additional Config": "额外配置项", "Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "Datalab Marker 的额外配置项,可以填写一个包含键值对的 JSON 字符串。例如:{\"key\": \"value\"}。支持的键包括:disable_links、keep_pageheader_in_output、keep_pagefooter_in_output、filter_blank_pages、drop_repeated_text、layout_coverage_threshold、merge_threshold、height_tolerance、gap_threshold、image_threshold、min_line_length、level_count 和 default_level。", - "Additional feedback comments": "", + "Additional feedback comments": "补充反馈说明", "Additional Parameters": "额外参数", "Adds filenames, titles, sections, and snippets into the BM25 text to improve lexical recall.": "将文件名、标题、章节和内容片段添加到 BM25 文本中,以提升词汇召回率。", "Adjusting these settings will apply changes universally to all users.": "调整这些设置将会对所有用户生效", @@ -120,7 +120,7 @@ "Allow Text to Speech": "允许文本转语音", "Allow User Location": "获取您的位置", "Allow Voice Interruption in Call": "允许语音通话时打断对话", - "Allow Web Upload": "", + "Allow Web Upload": "允许上传文件", "Allowed Endpoints": "允许的接口", "Allowed File Extensions": "允许的文件扩展名", "Allowed file extensions for upload. Separate multiple extensions with commas. Leave empty for all file types.": "文件上传允许的扩展名。多个扩展名用逗号分隔。留空以允许所有文件类型。", @@ -252,9 +252,9 @@ "Capture": "截图", "Capture Audio": "录制音频", "Certificate Path": "证书路径", - "Change folder icon": "", + "Change folder icon": "更换分组图标", "Change Password": "更改密码", - "Change User Role": "", + "Change User Role": "更换用户角色", "Channel": "频道", "Channel deleted successfully": "删除频道成功", "Channel Name": "频道名称", @@ -296,7 +296,7 @@ "Citations": "引用", "Clear memory": "清除记忆", "Clear Memory": "清除记忆", - "Clear search": "", + "Clear search": "清空搜索内容", "Clear status": "清除状态", "click here": "点击此处", "Click here for filter guides.": "点击此处查看筛选指南", @@ -318,13 +318,13 @@ "Clone of {{TITLE}}": "{{TITLE}} 的副本", "Close": "关闭", "Close Banner": "关闭横幅", - "Close chat controls": "", - "Close citation modal": "", + "Close chat controls": "关闭对话设置", + "Close citation modal": "关闭引用详情", "Close Configure Connection Modal": "关闭外部连接配置弹窗", - "Close feedback": "", + "Close feedback": "关闭反馈弹窗", "Close modal": "关闭弹窗", "Close Modal": "关闭弹窗", - "Close overview": "", + "Close overview": "关闭概览窗口", "Close settings modal": "关闭设置弹窗", "Close Sidebar": "收起侧边栏", "cloud": "云服务", @@ -395,8 +395,8 @@ "Copied shared chat URL to clipboard!": "已复制对话的分享链接到剪贴板!", "Copied to clipboard": "已复制到剪贴板", "Copy": "复制", - "Copy API Key": "", - "Copy content": "", + "Copy API Key": "复制接口密钥", + "Copy content": "复制内容", "Copy Formatted Text": "复制文本时包含特殊格式", "Copy Last Code Block": "复制最后一个代码块", "Copy Last Response": "复制最后一个回答", @@ -405,7 +405,7 @@ "Copy Prompt": "复制提示词", "Copy Share Link": "复制分享链接", "Copy to clipboard": "复制到剪贴板", - "Copy Token": "", + "Copy Token": "复制用户身份令牌", "Copy URL": "复制 URL", "Copying to clipboard was successful!": "成功复制到剪贴板!", "CORS must be properly configured by the provider to allow requests from Open WebUI.": "为允许 Open WebUI 发出的请求,提供商必须正确配置 CORS", @@ -436,7 +436,7 @@ "Current Password": "当前密码", "Custom": "自定义", "Custom description enabled": "自定义描述已启用", - "Custom Gender": "", + "Custom Gender": "自定义性别", "Custom Parameter Name": "自定义参数名称", "Custom Parameter Value": "自定义参数值", "Danger Zone": "危险区域", @@ -462,12 +462,12 @@ "Default model updated": "默认模型已更新", "Default permissions": "默认权限", "Default permissions updated successfully": "默认权限更新成功", - "Default Prompt Suggestions": "默认提示词建议", + "Default Prompt Suggestions": "默认推荐提示词", "Default to 389 or 636 if TLS is enabled": "启用 TLS 将默认使用 389 或 636 端口", "Default to ALL": "默认为:ALL", "Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.": "默认进行分段检索以提取重点和相关内容(推荐)", "Default User Role": "默认用户角色", - "Defaults": "", + "Defaults": "默认值", "Delete": "删除", "Delete a model": "删除模型", "Delete All": "全部删除", @@ -563,7 +563,7 @@ "Drop any files here to upload": "拖拽文件至此上传", "DuckDuckGo": "DuckDuckGo", "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "例如:“30s”,“10m”。有效的时间单位包括:“s”(秒), “m”(分), “h”(时)", - "e.g. 'low', 'medium', 'high'": "", + "e.g. 'low', 'medium', 'high'": "如:'low'、'medium'、'high'", "e.g. \"json\" or a JSON schema": "例如:\"json\" 或一个 JSON schema", "e.g. 60": "例如:60", "e.g. A filter to remove profanity from text": "例如:一个用于剔除文本中不当内容的过滤器", @@ -820,7 +820,7 @@ "Failed to load chat preview": "对话预览加载失败", "Failed to load Excel/CSV file. Please try downloading it instead.": "加载 Excel/CSV 文件失败,请尝试直接下载文件。", "Failed to load file content.": "文件内容加载失败", - "Failed to load Interface settings": "", + "Failed to load Interface settings": "“界面设置”数据加载失败", "Failed to move chat": "移动对话失败", "Failed to process URL: {{url}}": "处理链接失败: {{url}}", "Failed to read clipboard contents": "读取剪贴板内容失败", @@ -846,7 +846,7 @@ "Female": "女性", "File": "文件", "File added successfully.": "文件成功添加", - "File content": "", + "File content": "文件内容", "File content updated successfully.": "文件内容成功更新", "File Context": "文件上下文", "File deleted successfully.": "文件已成功删除。", @@ -876,13 +876,13 @@ "Folder Name": "分组名称", "Folder name cannot be empty.": "分组名称不能为空", "Folder name updated successfully": "分组名称更新成功", - "Folder options": "", + "Folder options": "分组选项", "Folder updated successfully": "分组更新成功", "Folders": "分组", "Follow up": "追问", "Follow Up Generation": "追问生成", "Follow Up Generation Prompt": "追问生成提示词", - "Follow up: {{question}}": "", + "Follow up: {{question}}": "追问:{{question}}", "Follow-Up Auto-Generation": "自动生成追问", "Followed instructions perfectly": "完全遵循指令", "for placeholders": "用于占位符", @@ -1191,7 +1191,7 @@ "Model can execute code and perform calculations": "模型可执行代码并进行计算", "Model can generate images based on text prompts": "模型可根据文本提示生成图像", "Model can search the web for information": "模型可进行联网搜索", - "Model Capabilities": "", + "Model Capabilities": "模型能力", "Model created successfully!": "模型创建成功!", "Model filesystem path detected. Model shortname is required for update, cannot continue.": "检测到模型名字为文件路径。需要提供模型简称才能继续执行更新。", "Model Filtering": "模型白名单", @@ -1204,7 +1204,7 @@ "Model names and usage frequency": "模型名称与使用频率", "Model not found": "未找到模型", "Model not selected": "未选择模型", - "Model Parameters": "", + "Model Parameters": "模型参数", "Model Params": "模型参数", "Model Permissions": "模型权限", "Model responses or outputs": "模型响应或输出", @@ -1403,7 +1403,7 @@ "Pin": "置顶", "Pinned": "已置顶", "Pinned Messages": "置顶消息", - "Pinned Models": "", + "Pinned Models": "固定在侧边栏的模型", "Pioneer insights": "洞悉未来", "Pipe": "Pipe", "Pipeline deleted successfully": "Pipeline 删除成功", @@ -1454,7 +1454,7 @@ "Prompt Content": "提示词内容", "Prompt created successfully": "提示词创建成功", "Prompt Name": "提示词名称", - "Prompt Suggestions": "", + "Prompt Suggestions": "推荐提示词", "Prompt updated successfully": "提示词更新成功", "Prompts": "提示词", "Prompts Access": "访问提示词", @@ -1469,7 +1469,7 @@ "Querying": "查询中", "Quick Actions": "快捷操作", "RAG Template": "RAG 提示词模板", - "Rate {{rating}} out of 10": "", + "Rate {{rating}} out of 10": "评分:{{rating}}/10", "Rating": "评价", "Re-rank models by topic similarity": "根据主题相似性对模型重新排名", "Read": "只读", @@ -1504,10 +1504,10 @@ "Remember Dismissal": "记住关闭状态", "Remove": "移除", "Remove {{MODELID}} from list.": "从列表中移除 {{MODELID}}", - "Remove action": "", + "Remove action": "删除当前操作", "Remove file": "移除文件", "Remove File": "移除文件", - "Remove from favorites": "", + "Remove from favorites": "从收藏中移除", "Remove image": "移除图像", "Remove Model": "移除模型", "Rename": "重命名", @@ -1629,7 +1629,7 @@ "Select view": "选择视图", "Selected model: {{modelName}}": "已选择:{{modelName}}", "Selected model(s) do not support image inputs": "所选择的模型不支持处理图像", - "Selected Models": "", + "Selected Models": "已选模型", "semantic": "语义", "Send": "发送", "Send a Message": "输入消息", @@ -1649,8 +1649,8 @@ "Set embedding model": "设置嵌入模型", "Set embedding model (e.g. {{model}})": "设置嵌入模型(例如:{{model}})", "Set reranking model (e.g. {{model}})": "设置重排序模型(例如:{{model}})", - "Set the default models that are automatically selected for all users when a new chat is created.": "", - "Set the models that are automatically pinned to the sidebar for all users.": "", + "Set the default models that are automatically selected for all users when a new chat is created.": "设置新建对话时系统自动为所有用户选择的默认模型。", + "Set the models that are automatically pinned to the sidebar for all users.": "设置自动为所有用户固定在侧边栏的模型。", "Set the number of layers, which will be off-loaded to GPU. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.": "设置将加载到 GPU 的层数。增加此值可以显著提高对 GPU 加速优化的模型性能,但也可能占用更多 GPU 资源,增加功耗。", "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "设置用于计算的工作线程数量。该选项可控制并发处理传入请求的线程数量。增加该值可以提高高并发工作负载下的性能,但也可能消耗更多的 CPU 资源。", "Set Voice": "设置音色", @@ -1702,7 +1702,7 @@ "Skill Description": "技能描述", "Skill ID": "技能 ID", "Skill imported successfully": "技能导入成功", - "Skill Instructions": "", + "Skill Instructions": "技能说明", "Skill Name": "技能名称", "Skill updated successfully": "技能更新成功", "Skills": "技能", @@ -1726,7 +1726,7 @@ "Speech recognition error: {{error}}": "语音识别错误:{{error}}", "Speech-to-Text": "语音转文本", "Speech-to-Text Engine": "语音转文本引擎", - "Speech-to-Text Language": "", + "Speech-to-Text Language": "语音转文字的语言", "Split documents by markdown headers before applying character/token splitting.": "在字符或 Token 分割之前,优先按 Markdown 的标题分割文档。", "Start a new conversation": "开始新对话", "Start of the channel": "频道起点", @@ -1750,8 +1750,8 @@ "STT Model": "语音转文本模型", "STT Settings": "语音转文本设置", "Stylized PDF Export": "美化 PDF 导出", - "Submit question": "", - "Submit suggestion": "", + "Submit question": "提交问题", + "Submit suggestion": "提交建议", "Subtitle": "副标题", "Success": "成功", "Successfully imported {{userCount}} users.": "成功导入 {{userCount}} 个用户。", @@ -1862,10 +1862,10 @@ "Toast notifications for new updates": "检测到新版本时显示更新通知", "Today": "今天", "Today at {{LOCALIZED_TIME}}": "今天 {{LOCALIZED_TIME}}", - "Toggle {{COUNT}} sources": "", - "Toggle 1 source": "", + "Toggle {{COUNT}} sources": "展开/收起 {{COUNT}} 个来源", + "Toggle 1 source": "展开/收起 1 个来源", "Toggle Sidebar": "展开或收起侧边栏", - "Toggle status history": "", + "Toggle status history": "展开/收起历史状态", "Toggle whether current connection is active.": "切换当前连接的启用状态", "Token": "Token", "Token counts are estimates and may not reflect actual API usage": "Token 数为估算值,可能与实际接口用量不一致", @@ -1935,7 +1935,7 @@ "Upload Files": "上传文件", "Upload Model": "上传模型", "Upload Pipeline": "上传 Pipeline", - "Upload profile image": "", + "Upload profile image": "上传头像", "Upload Progress": "上传进度", "Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "上传进度:{{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)", "Uploaded files or images": "上传的文件或图片", @@ -1982,8 +1982,8 @@ "Version deleted": "版本已删除", "View Replies": "查看回复", "View Result from **{{NAME}}**": "查看来自 **{{NAME}}** 的结果", - "View source: {{name}}": "", - "View source: {{title}}": "", + "View source: {{name}}": "查看来源:{{name}}", + "View source: {{title}}": "查看来源:{{title}}", "Visibility": "可见性", "Visible": "可见", "Visible to all users": "对所有用户可见", @@ -2060,7 +2060,7 @@ "You do not have permission to send messages in this thread.": "您没有权限在当前主题中发送消息。", "You do not have permission to upload files to this knowledge base.": "您没有权限上传文件到此知识库。", "You do not have permission to upload files.": "您没有上传文件的权限。", - "You do not have permission to upload web content.": "", + "You do not have permission to upload web content.": "您没有上传文件的权限。", "You have no archived conversations.": "没有已归档的对话。", "You have no shared conversations.": "您没有已分享的对话。", "You have shared this chat": "此对话已经分享过", From ec4fe4f390077ab703ef15fabdce048d4b18f7b0 Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Mon, 23 Feb 2026 21:55:14 +0800 Subject: [PATCH 002/173] i18n: improve zh-TW translation --- src/lib/i18n/locales/zh-TW/translation.json | 82 ++++++++++----------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index ff76b7cbb2..a6cd533bf2 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -74,13 +74,13 @@ "Add tag": "新增標籤", "Add Tag": "新增標籤", "Add text content": "新增文字內容", - "Add to favorites": "", + "Add to favorites": "新增至收藏", "Add User": "新增使用者", "Add User Group": "新增使用者群組", "Add webpage": "新增網頁", "Additional Config": "額外設定", "Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "Datalab Marker 的額外設定選項,可以填寫一個包含鍵值對的 JSON 字串。例如:{\"key\": \"value\"}。支援的鍵包括:disable_links、keep_pageheader_in_output、keep_pagefooter_in_output、filter_blank_pages、drop_repeated_text、layout_coverage_threshold、merge_threshold、height_tolerance、gap_threshold、image_threshold、min_line_length、level_count 和 default_level。", - "Additional feedback comments": "", + "Additional feedback comments": "補充回饋說明", "Additional Parameters": "額外參數", "Adds filenames, titles, sections, and snippets into the BM25 text to improve lexical recall.": "將檔名、標題、章節與內容片段加入 BM25 文字中,以提升關鍵字召回率。", "Adjusting these settings will apply changes universally to all users.": "調整這些設定將會影響所有使用者。", @@ -120,7 +120,7 @@ "Allow Text to Speech": "允許文字轉語音", "Allow User Location": "允許使用者位置", "Allow Voice Interruption in Call": "允許在通話中打斷語音", - "Allow Web Upload": "", + "Allow Web Upload": "允許上傳檔案", "Allowed Endpoints": "允許的端點", "Allowed File Extensions": "允許的檔案副檔名", "Allowed file extensions for upload. Separate multiple extensions with commas. Leave empty for all file types.": "允許上傳的檔案副檔名。多個副檔名請用逗號分隔,留空則允許所有檔案類型。", @@ -252,9 +252,9 @@ "Capture": "相機", "Capture Audio": "錄製音訊", "Certificate Path": "憑證路徑", - "Change folder icon": "", + "Change folder icon": "更換資料夾圖示", "Change Password": "修改密碼", - "Change User Role": "", + "Change User Role": "更換使用者角色", "Channel": "頻道", "Channel deleted successfully": "成功刪除頻道", "Channel Name": "頻道名稱", @@ -296,7 +296,7 @@ "Citations": "引用", "Clear memory": "清除記憶", "Clear Memory": "清除記憶", - "Clear search": "", + "Clear search": "清空搜尋內容", "Clear status": "清除狀態", "click here": "點選此處", "Click here for filter guides.": "點選此處檢視篩選器指南。", @@ -318,13 +318,13 @@ "Clone of {{TITLE}}": "{{TITLE}} 的副本", "Close": "關閉", "Close Banner": "關閉橫幅", - "Close chat controls": "", - "Close citation modal": "", + "Close chat controls": "關閉對話設定", + "Close citation modal": "關閉引用詳情", "Close Configure Connection Modal": "關閉外部連線設定彈出視窗", - "Close feedback": "", + "Close feedback": "關閉回饋視窗", "Close modal": "關閉彈出視窗", "Close Modal": "關閉彈出視窗", - "Close overview": "", + "Close overview": "關閉總覽視窗", "Close settings modal": "關閉設定彈出視窗", "Close Sidebar": "收起側邊欄", "cloud": "雲端服務", @@ -395,8 +395,8 @@ "Copied shared chat URL to clipboard!": "已複製共用對話 URL 到剪貼簿!", "Copied to clipboard": "已複製到剪貼簿", "Copy": "複製", - "Copy API Key": "", - "Copy content": "", + "Copy API Key": "複製介面金鑰", + "Copy content": "複製內容", "Copy Formatted Text": "複製格式化文字", "Copy Last Code Block": "複製最後的程式碼區塊", "Copy Last Response": "複製最後的回應", @@ -405,7 +405,7 @@ "Copy Prompt": "複製提示詞", "Copy Share Link": "複製分享連結", "Copy to clipboard": "複製到剪貼簿", - "Copy Token": "", + "Copy Token": "複製使用者身分憑證", "Copy URL": "複製 URL", "Copying to clipboard was successful!": "成功複製到剪貼簿!", "CORS must be properly configured by the provider to allow requests from Open WebUI.": "CORS 必須由供應商正確設定,以允許來自 Open WebUI 的請求。", @@ -436,7 +436,7 @@ "Current Password": "目前密碼", "Custom": "自訂", "Custom description enabled": "自訂描述已啟用", - "Custom Gender": "", + "Custom Gender": "自訂性別", "Custom Parameter Name": "自訂參數名稱", "Custom Parameter Value": "自訂參數值", "Danger Zone": "危險區域", @@ -467,7 +467,7 @@ "Default to ALL": "預設到所有", "Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.": "預設使用分段檢索以提取聚焦且相關的內容,建議用於大多數情況。", "Default User Role": "預設使用者角色", - "Defaults": "", + "Defaults": "預設值", "Delete": "刪除", "Delete a model": "刪除模型", "Delete All": "全部刪除", @@ -563,7 +563,7 @@ "Drop any files here to upload": "拖曳檔案至此處進行上傳", "DuckDuckGo": "DuckDuckGo", "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "例如:'30s'、'10m'。有效的時間單位為 's'、'm'、'h'。", - "e.g. 'low', 'medium', 'high'": "", + "e.g. 'low', 'medium', 'high'": "如:'low'、'medium'、'high'", "e.g. \"json\" or a JSON schema": "範例:\"json\" 或一個 JSON schema", "e.g. 60": "例如:60", "e.g. A filter to remove profanity from text": "例如:用來移除不雅詞彙的過濾器", @@ -820,7 +820,7 @@ "Failed to load chat preview": "對話預覽載入失敗", "Failed to load Excel/CSV file. Please try downloading it instead.": "無法載入 Excel/CSV 檔案。請嘗試直接下載檔案。", "Failed to load file content.": "載入檔案內容失敗。", - "Failed to load Interface settings": "", + "Failed to load Interface settings": "「介面設定」資料載入失敗", "Failed to move chat": "移動對話失敗", "Failed to process URL: {{url}}": "處理連結失敗:{{url}}", "Failed to read clipboard contents": "讀取剪貼簿內容失敗", @@ -846,7 +846,7 @@ "Female": "女性", "File": "檔案", "File added successfully.": "成功新增檔案。", - "File content": "", + "File content": "檔案內容", "File content updated successfully.": "成功更新檔案內容。", "File Context": "檔案上下文", "File deleted successfully.": "檔案已成功刪除。", @@ -876,13 +876,13 @@ "Folder Name": "分組名稱", "Folder name cannot be empty.": "分組名稱不能為空。", "Folder name updated successfully": "成功更新分組名稱", - "Folder options": "", + "Folder options": "資料夾選項", "Folder updated successfully": "分組更新成功", "Folders": "分組", "Follow up": "跟進", "Follow Up Generation": "跟進內容產生", "Follow Up Generation Prompt": "跟進內容產生提示詞", - "Follow up: {{question}}": "", + "Follow up: {{question}}": "追問:{{question}}", "Follow-Up Auto-Generation": "跟進內容自動產生", "Followed instructions perfectly": "完全遵循指示", "for placeholders": "用於佔位符", @@ -1191,7 +1191,7 @@ "Model can execute code and perform calculations": "模型可執行程式碼並進行運算", "Model can generate images based on text prompts": "模型可根據文字提示詞產生影像", "Model can search the web for information": "模型可透過網路搜尋資訊", - "Model Capabilities": "", + "Model Capabilities": "模型能力", "Model created successfully!": "成功建立模型!", "Model filesystem path detected. Model shortname is required for update, cannot continue.": "偵測到模型檔案系統路徑。更新需要模型簡稱,因此無法繼續。", "Model Filtering": "模型篩選", @@ -1204,7 +1204,7 @@ "Model names and usage frequency": "模型名稱和使用頻率", "Model not found": "未找到模型", "Model not selected": "未選取模型", - "Model Parameters": "", + "Model Parameters": "模型參數", "Model Params": "模型參數", "Model Permissions": "模型權限", "Model responses or outputs": "模型回應或輸出", @@ -1403,7 +1403,7 @@ "Pin": "釘選", "Pinned": "已釘選", "Pinned Messages": "置頂訊息", - "Pinned Models": "", + "Pinned Models": "固定於側邊欄的模型", "Pioneer insights": "先驅見解", "Pipe": "Pipe", "Pipeline deleted successfully": "成功刪除管線", @@ -1454,7 +1454,7 @@ "Prompt Content": "提示詞內容", "Prompt created successfully": "成功建立提示詞", "Prompt Name": "提示詞名稱", - "Prompt Suggestions": "", + "Prompt Suggestions": "推薦提示詞", "Prompt updated successfully": "成功更新提示詞", "Prompts": "提示詞", "Prompts Access": "提示詞存取", @@ -1469,7 +1469,7 @@ "Querying": "查詢中", "Quick Actions": "快速操作", "RAG Template": "RAG 範本", - "Rate {{rating}} out of 10": "", + "Rate {{rating}} out of 10": "評分:{{rating}}/10", "Rating": "評分", "Re-rank models by topic similarity": "根據主題相似度重新排序模型", "Read": "讀取", @@ -1504,10 +1504,10 @@ "Remember Dismissal": "記住關閉狀態", "Remove": "移除", "Remove {{MODELID}} from list.": "從清單中移除 {{MODELID}}", - "Remove action": "", + "Remove action": "刪除目前操作", "Remove file": "移除檔案", "Remove File": "移除檔案", - "Remove from favorites": "", + "Remove from favorites": "從收藏中移除", "Remove image": "移除圖片", "Remove Model": "移除模型", "Rename": "重新命名", @@ -1629,7 +1629,7 @@ "Select view": "選擇檢視", "Selected model: {{modelName}}": "已選擇:{{modelName}}", "Selected model(s) do not support image inputs": "選取的模型不支援圖片輸入", - "Selected Models": "", + "Selected Models": "已選模型", "semantic": "語義", "Send": "傳送", "Send a Message": "傳送訊息", @@ -1649,8 +1649,8 @@ "Set embedding model": "設定嵌入模型", "Set embedding model (e.g. {{model}})": "設定嵌入模型(例如:{{model}})", "Set reranking model (e.g. {{model}})": "設定重新排序模型(例如:{{model}})", - "Set the default models that are automatically selected for all users when a new chat is created.": "", - "Set the models that are automatically pinned to the sidebar for all users.": "", + "Set the default models that are automatically selected for all users when a new chat is created.": "設定新建對話時系統自動為所有使用者選擇的預設模型。", + "Set the models that are automatically pinned to the sidebar for all users.": "設定自動為所有使用者固定於側邊欄的模型。", "Set the number of layers, which will be off-loaded to GPU. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.": "設定要轉移至 GPU 的層數。增加此數值可以顯著提升針對 GPU 加速最佳化的模型的效能,但也可能消耗更多電力和 GPU 資源。", "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "設定用於計算的工作執行緒數量。此選項控制使用多少執行緒來同時處理傳入的請求。增加此值可以在高併發工作負載下提升效能,但也可能消耗更多 CPU 資源。", "Set Voice": "設定語音", @@ -1702,7 +1702,7 @@ "Skill Description": "技能描述", "Skill ID": "技能 ID", "Skill imported successfully": "技能匯入成功", - "Skill Instructions": "", + "Skill Instructions": "技能說明", "Skill Name": "技能名稱", "Skill updated successfully": "技能已更新", "Skills": "技能", @@ -1726,7 +1726,7 @@ "Speech recognition error: {{error}}": "語音辨識錯誤:{{error}}", "Speech-to-Text": "語音轉文字 (STT) ", "Speech-to-Text Engine": "語音轉文字 (STT) 引擎", - "Speech-to-Text Language": "", + "Speech-to-Text Language": "語音轉文字的語言", "Split documents by markdown headers before applying character/token splitting.": "在字元或 Token 分割之前,優先按 Markdown 的標題分割文件。", "Start a new conversation": "開始新對話", "Start of the channel": "頻道起點", @@ -1750,8 +1750,8 @@ "STT Model": "語音轉文字 (STT) 模型", "STT Settings": "語音轉文字 (STT) 設定", "Stylized PDF Export": "風格化 PDF 匯出", - "Submit question": "", - "Submit suggestion": "", + "Submit question": "提交問題", + "Submit suggestion": "提交建議", "Subtitle": "副標題", "Success": "成功", "Successfully imported {{userCount}} users.": "成功匯入 {{userCount}} 個使用者", @@ -1862,10 +1862,10 @@ "Toast notifications for new updates": "快顯通知新的更新", "Today": "今天", "Today at {{LOCALIZED_TIME}}": "今天 {{LOCALIZED_TIME}}", - "Toggle {{COUNT}} sources": "", - "Toggle 1 source": "", + "Toggle {{COUNT}} sources": "展開/收合 {{COUNT}} 個來源", + "Toggle 1 source": "展開/收合 1 個來源", "Toggle Sidebar": "切換側邊欄", - "Toggle status history": "", + "Toggle status history": "展開/收合歷史狀態", "Toggle whether current connection is active.": "切換目前連線的啟用狀態", "Token": "Token", "Token counts are estimates and may not reflect actual API usage": "Token 數為估算值,可能與實際 API 用量不一致", @@ -1935,7 +1935,7 @@ "Upload Files": "上傳檔案", "Upload Model": "上傳模型", "Upload Pipeline": "上傳管線", - "Upload profile image": "", + "Upload profile image": "上傳頭像", "Upload Progress": "上傳進度", "Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "上傳進度:{{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)", "Uploaded files or images": "上傳的檔案或圖片", @@ -1982,8 +1982,8 @@ "Version deleted": "版本已刪除", "View Replies": "檢視回覆", "View Result from **{{NAME}}**": "檢視來自 **{{NAME}}** 的結果", - "View source: {{name}}": "", - "View source: {{title}}": "", + "View source: {{name}}": "查看來源:{{name}}", + "View source: {{title}}": "查看來源:{{title}}", "Visibility": "可見度", "Visible": "可見", "Visible to all users": "對所有使用者可見", @@ -2060,7 +2060,7 @@ "You do not have permission to send messages in this thread.": "您沒有在此討論串中傳送訊息的權限。", "You do not have permission to upload files to this knowledge base.": "您沒有權限上傳檔案到此知識庫。", "You do not have permission to upload files.": "您沒有權限上傳檔案。", - "You do not have permission to upload web content.": "", + "You do not have permission to upload web content.": "您沒有上傳網頁內容的權限。", "You have no archived conversations.": "您沒有已封存的對話。", "You have no shared conversations.": "您沒有已分享的對話。", "You have shared this chat": "您已分享此對話", From 22f074cf592ba70bce46ab58d28745587332972e Mon Sep 17 00:00:00 2001 From: EntropyYue Date: Mon, 23 Feb 2026 22:16:26 +0800 Subject: [PATCH 003/173] fix: dictation toggle shortcuts i18n --- src/lib/components/chat/ShortcutsModal.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/components/chat/ShortcutsModal.svelte b/src/lib/components/chat/ShortcutsModal.svelte index 89bef8d43e..965229c22c 100644 --- a/src/lib/components/chat/ShortcutsModal.svelte +++ b/src/lib/components/chat/ShortcutsModal.svelte @@ -74,6 +74,7 @@ + From 140ab270af2fbe05a149cfbb676de22077f29557 Mon Sep 17 00:00:00 2001 From: "Jannik S." Date: Mon, 23 Feb 2026 18:52:29 +0100 Subject: [PATCH 004/173] fix: correct ENABLE_AUDIT_STDOUT stdout filter (#21777) --- backend/open_webui/utils/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/logger.py b/backend/open_webui/utils/logger.py index 283f37ea66..26a525fc0b 100644 --- a/backend/open_webui/utils/logger.py +++ b/backend/open_webui/utils/logger.py @@ -153,7 +153,7 @@ def start_logger(): logger.remove() audit_filter = lambda record: ( - "auditable" not in record["extra"] if ENABLE_AUDIT_STDOUT else True + True if ENABLE_AUDIT_STDOUT else "auditable" not in record["extra"] ) if LOG_FORMAT == "json": logger.add( From 3761b3ac28ec99eaeebab4bccbf457f5d7882119 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 11:52:35 -0600 Subject: [PATCH 005/173] refac --- package-lock.json | 10 ++++++++++ package.json | 1 + 2 files changed, 11 insertions(+) diff --git a/package-lock.json b/package-lock.json index b77dca902a..fce7f6e2c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@tiptap/pm": "^3.0.7", "@tiptap/starter-kit": "^3.0.7", "@tiptap/suggestion": "^3.4.2", + "@xterm/xterm": "^6.0.0", "@xyflow/svelte": "^0.1.19", "alpinejs": "^3.15.0", "async": "^3.2.5", @@ -4619,6 +4620,15 @@ "node": ">=10.0.0" } }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, "node_modules/@xyflow/svelte": { "version": "0.1.19", "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.19.tgz", diff --git a/package.json b/package.json index 8c024b76e2..f983e9067d 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@tiptap/pm": "^3.0.7", "@tiptap/starter-kit": "^3.0.7", "@tiptap/suggestion": "^3.4.2", + "@xterm/xterm": "^6.0.0", "@xyflow/svelte": "^0.1.19", "alpinejs": "^3.15.0", "async": "^3.2.5", From febc66ef2bb05606b59719e737ac5ad839002977 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 12:03:56 -0600 Subject: [PATCH 006/173] enh: sbom docker gh action --- .github/workflows/docker-build.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 5010307fb3..0307593476 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -95,6 +95,7 @@ jobs: outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + sbom: true build-args: | BUILD_HASH=${{ github.sha }} @@ -199,6 +200,7 @@ jobs: outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + sbom: true build-args: | BUILD_HASH=${{ github.sha }} USE_CUDA=true @@ -304,6 +306,7 @@ jobs: outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + sbom: true build-args: | BUILD_HASH=${{ github.sha }} USE_CUDA=true @@ -407,6 +410,7 @@ jobs: outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + sbom: true build-args: | BUILD_HASH=${{ github.sha }} USE_OLLAMA=true @@ -509,6 +513,7 @@ jobs: outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + sbom: true build-args: | BUILD_HASH=${{ github.sha }} USE_SLIM=true From 8f49725aa5f2d9b87e559e7d3f02f037335b7914 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 12:17:36 -0600 Subject: [PATCH 007/173] refac --- backend/open_webui/utils/middleware.py | 5 +++-- backend/open_webui/utils/misc.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 763d74bd13..7823605bfd 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -86,6 +86,7 @@ get_message_list, add_or_update_system_message, add_or_update_user_message, + set_last_user_message_content, get_last_user_message, get_last_user_message_item, get_last_assistant_message, @@ -4241,8 +4242,8 @@ async def flush_pending_delta_data(threshold: int = 0): all_tool_call_sources.extend(tool_call_sources) if all_tool_call_sources and user_message: # Restore original user message before re-applying to avoid recursive nesting - form_data["messages"] = add_or_update_user_message( - user_message, form_data["messages"], append=False + set_last_user_message_content( + user_message, form_data["messages"] ) form_data["messages"] = apply_source_context_to_messages( request, diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 447e334227..ced6fd74a8 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -277,6 +277,26 @@ def get_last_user_message(messages: list[dict]) -> Optional[str]: return get_content_from_message(message) +def set_last_user_message_content( + content: str, messages: list[dict] +) -> list[dict]: + """ + Replace the text content of the last user message in-place. + Handles both plain-string and list-of-parts content formats. + """ + for message in reversed(messages): + if message.get("role") == "user": + if isinstance(message.get("content"), list): + for item in message["content"]: + if item.get("type") == "text": + item["text"] = content + break + else: + message["content"] = content + break + return messages + + def get_last_assistant_message_item(messages: list[dict]) -> Optional[dict]: for message in reversed(messages): if message["role"] == "assistant": From f4a1d99f001bf9b3ebe7f5d1b37ec5aa0fc304a7 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 12:52:46 -0600 Subject: [PATCH 008/173] refac --- backend/open_webui/models/oauth_sessions.py | 1 + backend/open_webui/utils/oauth.py | 25 ++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py index fbcd763f34..68b2eeb0f5 100644 --- a/backend/open_webui/models/oauth_sessions.py +++ b/backend/open_webui/models/oauth_sessions.py @@ -135,6 +135,7 @@ def create_session( db.refresh(result) if result: + db.expunge(result) # Detach so dict swap is never flushed result.token = token # Return decrypted token return OAuthSessionModel.model_validate(result) else: diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index b23c5c90a3..6e59317f88 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1706,17 +1706,22 @@ async def handle_callback(self, request, provider, response, db=None): db=db, ) - response.set_cookie( - key="oauth_session_id", - value=session.id, - httponly=True, - samesite=WEBUI_AUTH_COOKIE_SAME_SITE, - secure=WEBUI_AUTH_COOKIE_SECURE, - ) + if session: + response.set_cookie( + key="oauth_session_id", + value=session.id, + httponly=True, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, + ) - log.info( - f"Stored OAuth session server-side for user {user.id}, provider {provider}" - ) + log.info( + f"Stored OAuth session server-side for user {user.id}, provider {provider}" + ) + else: + log.warning( + f"Failed to create OAuth session for user {user.id}, provider {provider}" + ) except Exception as e: log.error(f"Failed to store OAuth session server-side: {e}") From 1808d7fd2fd32920ef14b8a2502abe7f3c0d9351 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:52:12 +0100 Subject: [PATCH 009/173] feat: sort action buttons by valve priority (#21790) feat: sort action buttons by valve priority Action buttons under assistant messages were rendered in non-deterministic order due to set() deduplication. They now respect the priority field from function Valves, sorted ascending (lower value = appears first, default 0), matching the existing filter priority mechanism. --- backend/open_webui/utils/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 6040c0645d..d890dc0a2d 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -331,12 +331,18 @@ def get_filter_items_from_module(function, module): elif meta.get(key) is None: meta[key] = copy.deepcopy(value) + def get_action_priority(action_id): + valves = Functions.get_function_valves_by_id(action_id) + return valves.get("priority", 0) if valves else 0 + for model in models: action_ids = [ action_id for action_id in list(set(model.pop("action_ids", []) + global_action_ids)) if action_id in enabled_action_ids ] + action_ids.sort(key=get_action_priority) + filter_ids = [ filter_id for filter_id in list(set(model.pop("filter_ids", []) + global_filter_ids)) From a52e6c2d571e34a837e38f2986edf85bf20d6d3d Mon Sep 17 00:00:00 2001 From: Peter L Jones Date: Mon, 23 Feb 2026 20:09:13 +0000 Subject: [PATCH 010/173] Filter by public/private (#21797) --- src/lib/components/admin/Settings/Models.svelte | 11 +++++++++++ .../admin/Settings/Models/AdminViewSelector.svelte | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/components/admin/Settings/Models.svelte b/src/lib/components/admin/Settings/Models.svelte index 7c480028ac..56ef8ce4f7 100644 --- a/src/lib/components/admin/Settings/Models.svelte +++ b/src/lib/components/admin/Settings/Models.svelte @@ -69,6 +69,15 @@ const perPage = 30; let currentPage = 1; + const isPublicModel = (model) => { + return (model?.access_grants ?? []).some( + (g) => + g.principal_type === 'user' && + g.principal_id === '*' && + g.permission === 'read' + ); + }; + $: if (models) { filteredModels = models .filter((m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())) @@ -77,6 +86,8 @@ if (viewOption === 'disabled') return !(m?.is_active ?? true); if (viewOption === 'visible') return !(m?.meta?.hidden ?? false); if (viewOption === 'hidden') return m?.meta?.hidden === true; + if (viewOption === 'public') return isPublicModel(m); + if (viewOption === 'private') return !isPublicModel(m); return true; // All }) .sort((a, b) => { diff --git a/src/lib/components/admin/Settings/Models/AdminViewSelector.svelte b/src/lib/components/admin/Settings/Models/AdminViewSelector.svelte index 5778ba21ab..b3c8006745 100644 --- a/src/lib/components/admin/Settings/Models/AdminViewSelector.svelte +++ b/src/lib/components/admin/Settings/Models/AdminViewSelector.svelte @@ -16,7 +16,9 @@ { value: 'enabled', label: $i18n.t('Enabled') }, { value: 'disabled', label: $i18n.t('Disabled') }, { value: 'visible', label: $i18n.t('Visible') }, - { value: 'hidden', label: $i18n.t('Hidden') } + { value: 'hidden', label: $i18n.t('Hidden') }, + { value: 'public', label: $i18n.t('Public') }, + { value: 'private', label: $i18n.t('Private') } ]; From 3d99de67716774af2f95f2e3c8e7cc4879464c71 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 15:49:05 -0600 Subject: [PATCH 011/173] enh: access grant level perms --- backend/open_webui/config.py | 7 ++++ backend/open_webui/models/access_grants.py | 27 +++++++++++++ backend/open_webui/routers/knowledge.py | 38 ++++++++++++++++++- backend/open_webui/routers/models.py | 14 ++++++- backend/open_webui/routers/notes.py | 26 ++++++++++++- backend/open_webui/routers/prompts.py | 14 ++++++- backend/open_webui/routers/skills.py | 14 ++++++- backend/open_webui/routers/tools.py | 14 ++++++- backend/open_webui/routers/users.py | 8 ++++ .../admin/Users/Groups/Permissions.svelte | 22 +++++++++++ src/lib/components/notes/NoteEditor.svelte | 3 ++ .../Knowledge/CreateKnowledgeBase.svelte | 1 + .../workspace/Knowledge/KnowledgeBase.svelte | 1 + .../workspace/Models/ModelEditor.svelte | 1 + .../workspace/Prompts/PromptEditor.svelte | 1 + .../workspace/Skills/SkillEditor.svelte | 1 + .../workspace/Tools/ToolkitEditor.svelte | 1 + .../workspace/common/AccessControl.svelte | 5 ++- .../common/AccessControlModal.svelte | 2 + .../workspace/common/AddAccessModal.svelte | 3 +- .../workspace/common/MemberSelector.svelte | 3 ++ src/lib/constants/permissions.ts | 3 ++ 22 files changed, 201 insertions(+), 8 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index e2e7d7ea12..b276f9de90 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1433,6 +1433,10 @@ def reachable(host: str, port: int) -> bool: == "true" ) +USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = ( + os.environ.get("USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS", "True").lower() == "true" +) + USER_PERMISSIONS_CHAT_CONTROLS = ( os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true" @@ -1590,6 +1594,9 @@ def reachable(host: str, port: int) -> bool: "notes": USER_PERMISSIONS_NOTES_ALLOW_SHARING, "public_notes": USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING, }, + "access_grants": { + "allow_users": USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS, + }, "chat": { "controls": USER_PERMISSIONS_CHAT_CONTROLS, "valves": USER_PERMISSIONS_CHAT_VALVES, diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py index 227621becd..93563bec85 100644 --- a/backend/open_webui/models/access_grants.py +++ b/backend/open_webui/models/access_grants.py @@ -204,6 +204,33 @@ def has_public_read_access_grant(access_grants: Optional[list]) -> bool: return False +def has_user_access_grant(access_grants: Optional[list]) -> bool: + """ + Returns True when a direct grant list includes any non-wildcard user grant. + """ + for grant in normalize_access_grants(access_grants): + if grant["principal_type"] == "user" and grant["principal_id"] != "*": + return True + return False + + +def strip_user_access_grants(access_grants: Optional[list]) -> list: + """ + Remove all non-wildcard user grants from the list. + Keeps group grants and the public wildcard (user:*) intact. + """ + if not access_grants: + return [] + return [ + grant + for grant in access_grants + if not ( + (grant.get("principal_type") if isinstance(grant, dict) else getattr(grant, "principal_type", None)) == "user" + and (grant.get("principal_id") if isinstance(grant, dict) else getattr(grant, "principal_id", None)) != "*" + ) + ] + + def grants_to_access_control(grants: list) -> Optional[dict]: """ Convert a list of grant objects (AccessGrantModel or AccessGrantResponse) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 1fedab4466..b5cecb6fac 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -30,7 +30,7 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.utils.auth import get_verified_user, get_admin_user from open_webui.utils.access_control import has_permission -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL @@ -274,6 +274,18 @@ async def create_new_knowledge( ): form_data.access_grants = [] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + knowledge = Knowledges.insert_new_knowledge(user.id, form_data) if knowledge: @@ -494,6 +506,18 @@ async def update_knowledge_by_id( ): form_data.access_grants = [] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data) if knowledge: # Re-embed knowledge base for semantic search @@ -573,6 +597,18 @@ async def update_knowledge_access_by_id( ) ] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db) return KnowledgeFilesResponse( diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index c417453486..9feb0e8548 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -17,7 +17,7 @@ ModelAccessResponse, Models, ) -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants from pydantic import BaseModel from open_webui.constants import ERROR_MESSAGES @@ -584,6 +584,18 @@ async def update_model_access_by_id( ) ] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + AccessGrants.set_access_grants( "model", form_data.id, form_data.access_grants, db=db ) diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 41bb65f55a..7c971fa312 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -28,7 +28,7 @@ from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_permission -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants from open_webui.internal.db import get_session from sqlalchemy.orm import Session @@ -296,6 +296,18 @@ async def update_note_by_id( ): form_data.access_grants = [] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + try: note = Notes.update_note_by_id(id, form_data, db=db) await sio.emit( @@ -376,6 +388,18 @@ async def update_note_access_by_id( ) ] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db) return Notes.get_note_by_id(id, db=db) diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 9653571fbb..d79fdbbd6e 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -9,7 +9,7 @@ PromptModel, Prompts, ) -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants from open_webui.models.groups import Groups from open_webui.models.prompt_history import ( PromptHistories, @@ -492,6 +492,18 @@ async def update_prompt_access_by_id( ) ] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db) return Prompts.get_prompt_by_id(prompt_id, db=db) diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index fb7b01b87f..6532b6ee04 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -17,7 +17,7 @@ SkillAccessListResponse, Skills, ) -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_access, has_permission @@ -360,6 +360,18 @@ async def update_skill_access_by_id( ) ] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db) return Skills.get_skill_by_id(id, db=db) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 531cfe16a7..d60bcfb01b 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -21,7 +21,7 @@ ToolAccessResponse, Tools, ) -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants from open_webui.utils.plugin import ( load_tool_module_by_id, replace_imports, @@ -595,6 +595,18 @@ async def update_tool_access_by_id( ) ] + # Strip individual user sharing if user lacks permission + if ( + user.role != "admin" + and has_user_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "access_grants.allow_users", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = strip_user_access_grants(form_data.access_grants) + AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db) return Tools.get_tool_by_id(id, db=db) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 1ecf1c019f..143a374d9c 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -196,6 +196,10 @@ class SharingPermissions(BaseModel): public_notes: bool = True +class AccessGrantsPermissions(BaseModel): + allow_users: bool = True + + class ChatPermissions(BaseModel): controls: bool = True valves: bool = True @@ -239,6 +243,7 @@ class SettingsPermissions(BaseModel): class UserPermissions(BaseModel): workspace: WorkspacePermissions sharing: SharingPermissions + access_grants: AccessGrantsPermissions chat: ChatPermissions features: FeaturesPermissions settings: SettingsPermissions @@ -253,6 +258,9 @@ async def get_default_user_permissions(request: Request, user=Depends(get_admin_ "sharing": SharingPermissions( **request.app.state.config.USER_PERMISSIONS.get("sharing", {}) ), + "access_grants": AccessGrantsPermissions( + **request.app.state.config.USER_PERMISSIONS.get("access_grants", {}) + ), "chat": ChatPermissions( **request.app.state.config.USER_PERMISSIONS.get("chat", {}) ), diff --git a/src/lib/components/admin/Users/Groups/Permissions.svelte b/src/lib/components/admin/Users/Groups/Permissions.svelte index 820228de04..64273dcbcd 100644 --- a/src/lib/components/admin/Users/Groups/Permissions.svelte +++ b/src/lib/components/admin/Users/Groups/Permissions.svelte @@ -395,6 +395,28 @@
+
+
{$i18n.t('Access Grants')}
+ +
+
+
+ {$i18n.t('Allow Sharing With Users')} +
+ +
+ {#if defaultPermissions?.access_grants?.allow_users && !permissions.access_grants.allow_users} +
+
+ {$i18n.t('This is a default user permission and will remain enabled.')} +
+
+ {/if} +
+
+ +
+
{$i18n.t('Chat Permissions')}
diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte index d16afa01b8..c878314a07 100644 --- a/src/lib/components/notes/NoteEditor.svelte +++ b/src/lib/components/notes/NoteEditor.svelte @@ -871,6 +871,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, bind:show={showAccessControlModal} bind:accessGrants={note.access_grants} accessRoles={['read', 'write']} + share={$user?.permissions?.sharing?.notes || $user?.role === 'admin'} + sharePublic={$user?.permissions?.sharing?.public_notes || $user?.role === 'admin'} + shareUsers={$user?.permissions?.access_grants?.allow_users || $user?.role === 'admin'} onChange={async () => { if (id) { try { diff --git a/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte index 9ccae10095..ca29a79486 100644 --- a/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte @@ -115,6 +115,7 @@ accessRoles={['read', 'write']} share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'} sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'} + shareUsers={$user?.permissions?.access_grants?.allow_users || $user?.role === "admin"} />
diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 5f50328024..909c3eb5d5 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -837,6 +837,7 @@ bind:accessGrants={knowledge.access_grants} share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'} sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'} + shareUsers={$user?.permissions?.access_grants?.allow_users || $user?.role === "admin"} onChange={async () => { try { await updateKnowledgeAccessGrants(localStorage.token, id, knowledge.access_grants ?? []); diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index b396373532..b043c67039 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -332,6 +332,7 @@ accessRoles={preset ? ['read', 'write'] : ['read']} share={$user?.permissions?.sharing?.models || $user?.role === 'admin'} sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin'} + shareUsers={$user?.permissions?.access_grants?.allow_users || $user?.role === "admin"} onChange={async () => { if (edit && model?.id) { try { diff --git a/src/lib/components/workspace/Prompts/PromptEditor.svelte b/src/lib/components/workspace/Prompts/PromptEditor.svelte index e92e5936a3..5b63b08f58 100644 --- a/src/lib/components/workspace/Prompts/PromptEditor.svelte +++ b/src/lib/components/workspace/Prompts/PromptEditor.svelte @@ -283,6 +283,7 @@ accessRoles={['read', 'write']} share={$user?.permissions?.sharing?.prompts || $user?.role === 'admin'} sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin'} + shareUsers={$user?.permissions?.access_grants?.allow_users || $user?.role === 'admin'} onChange={async () => { if (edit && prompt?.id) { try { diff --git a/src/lib/components/workspace/Skills/SkillEditor.svelte b/src/lib/components/workspace/Skills/SkillEditor.svelte index 569809a5b6..6131ea3b46 100644 --- a/src/lib/components/workspace/Skills/SkillEditor.svelte +++ b/src/lib/components/workspace/Skills/SkillEditor.svelte @@ -114,6 +114,7 @@ accessRoles={['read', 'write']} share={$user?.permissions?.sharing?.skills || $user?.role === 'admin'} sharePublic={$user?.permissions?.sharing?.public_skills || $user?.role === 'admin'} + shareUsers={$user?.permissions?.access_grants?.allow_users || $user?.role === 'admin'} onChange={async () => { if (edit && skill?.id) { try { diff --git a/src/lib/components/workspace/Tools/ToolkitEditor.svelte b/src/lib/components/workspace/Tools/ToolkitEditor.svelte index a0f8e57281..89c050367b 100644 --- a/src/lib/components/workspace/Tools/ToolkitEditor.svelte +++ b/src/lib/components/workspace/Tools/ToolkitEditor.svelte @@ -193,6 +193,7 @@ class Tools: accessRoles={['read', 'write']} share={$user?.permissions?.sharing?.tools || $user?.role === 'admin'} sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin'} + shareUsers={$user?.permissions?.access_grants?.allow_users || $user?.role === 'admin'} onChange={async () => { if (edit && id) { try { diff --git a/src/lib/components/workspace/common/AccessControl.svelte b/src/lib/components/workspace/common/AccessControl.svelte index 6e211e7ba8..35965edbf8 100644 --- a/src/lib/components/workspace/common/AccessControl.svelte +++ b/src/lib/components/workspace/common/AccessControl.svelte @@ -33,6 +33,7 @@ export let share = true; export let sharePublic = true; + export let shareUsers = true; let groups: any[] = []; const resolvingGroupIds = new Set(); @@ -419,7 +420,7 @@ }); - +
@@ -562,6 +563,7 @@ {/each} + {#if shareUsers} {#each selectedUsers as user}
{/each} + {/if} {#if !hasPublicReadGrant(accessGrants ?? []) && accessGroups.length === 0 && selectedUsers.length === 0}
diff --git a/src/lib/components/workspace/common/AccessControlModal.svelte b/src/lib/components/workspace/common/AccessControlModal.svelte index 1456952829..caa8cbfd0d 100644 --- a/src/lib/components/workspace/common/AccessControlModal.svelte +++ b/src/lib/components/workspace/common/AccessControlModal.svelte @@ -20,6 +20,7 @@ export let share = true; export let sharePublic = true; + export let shareUsers = true; export let onChange = () => {}; @@ -48,6 +49,7 @@ {accessRoles} {share} {sharePublic} + {shareUsers} />
diff --git a/src/lib/components/workspace/common/AddAccessModal.svelte b/src/lib/components/workspace/common/AddAccessModal.svelte index 5180fef10a..ff4b778a9f 100644 --- a/src/lib/components/workspace/common/AddAccessModal.svelte +++ b/src/lib/components/workspace/common/AddAccessModal.svelte @@ -7,6 +7,7 @@ import MemberSelector from '$lib/components/workspace/common/MemberSelector.svelte'; export let show = false; + export let shareUsers = true; export let onAdd = (payload: { userIds: string[]; groupIds: string[] }) => {}; let userIds: string[] = []; @@ -51,7 +52,7 @@ }} >
- +
diff --git a/src/lib/components/workspace/common/MemberSelector.svelte b/src/lib/components/workspace/common/MemberSelector.svelte index 5cc46f33e4..773188ab73 100644 --- a/src/lib/components/workspace/common/MemberSelector.svelte +++ b/src/lib/components/workspace/common/MemberSelector.svelte @@ -19,6 +19,7 @@ import { getGroups } from '$lib/apis/groups'; export let includeGroups = true; + export let includeUsers = true; export let pagination = false; export let groupIds = []; @@ -237,6 +238,7 @@
{/if} + {#if includeUsers}
{$i18n.t('Users')}
@@ -295,6 +297,7 @@ {/if} {/each}
+ {/if} diff --git a/src/lib/constants/permissions.ts b/src/lib/constants/permissions.ts index 531f3fea82..52f93cecf7 100644 --- a/src/lib/constants/permissions.ts +++ b/src/lib/constants/permissions.ts @@ -26,6 +26,9 @@ export const DEFAULT_PERMISSIONS = { notes: false, public_notes: false }, + access_grants: { + allow_users: true + }, chat: { controls: true, valves: true, From 176f9a781619d836be003d28d53904639cad4128 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 16:01:03 -0600 Subject: [PATCH 012/173] refac --- backend/open_webui/routers/knowledge.py | 103 +++++---------------- backend/open_webui/routers/models.py | 41 ++------ backend/open_webui/routers/notes.py | 73 ++++----------- backend/open_webui/routers/prompts.py | 41 ++------ backend/open_webui/routers/skills.py | 39 ++------ backend/open_webui/routers/tools.py | 39 ++------ backend/open_webui/utils/access_control.py | 50 ++++++++++ 7 files changed, 125 insertions(+), 261 deletions(-) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index b5cecb6fac..2531f8b92a 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -29,8 +29,8 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.utils.auth import get_verified_user, get_admin_user -from open_webui.utils.access_control import has_permission -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants +from open_webui.models.access_grants import AccessGrants from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL @@ -251,7 +251,7 @@ async def create_new_knowledge( user=Depends(get_verified_user), ): # NOTE: We intentionally do NOT use Depends(get_session) here. - # Database operations (has_permission, insert_new_knowledge) manage their own sessions. + # Database operations (has_permission, filter_allowed_access_grants, insert_new_knowledge) manage their own sessions. # This prevents holding a connection during embed_knowledge_base_metadata() # which makes external embedding API calls (1-5+ seconds). if user.role != "admin" and not has_permission( @@ -262,29 +262,13 @@ async def create_new_knowledge( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - # Check if user can share publicly - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_knowledge", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = [] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_knowledge", + ) knowledge = Knowledges.insert_new_knowledge(user.id, form_data) @@ -494,29 +478,13 @@ async def update_knowledge_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - # Check if user can share publicly - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_knowledge", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = [] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_knowledge", + ) knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data) if knowledge: @@ -578,36 +546,13 @@ async def update_knowledge_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - # Strip public sharing if user lacks permission - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_knowledge", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = [ - grant - for grant in form_data.access_grants - if not ( - grant.get("principal_type") == "user" - and grant.get("principal_id") == "*" - ) - ] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_knowledge" + ) AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 9feb0e8548..ce89eb5af8 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -17,7 +17,7 @@ ModelAccessResponse, Models, ) -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants +from open_webui.models.access_grants import AccessGrants from pydantic import BaseModel from open_webui.constants import ERROR_MESSAGES @@ -33,7 +33,7 @@ from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STATIC_DIR from open_webui.internal.db import get_session from sqlalchemy.orm import Session @@ -565,36 +565,13 @@ async def update_model_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - # Strip public sharing if user lacks permission - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_models", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = [ - grant - for grant in form_data.access_grants - if not ( - grant.get("principal_type") == "user" - and grant.get("principal_id") == "*" - ) - ] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_models" + ) AccessGrants.set_access_grants( "model", form_data.id, form_data.access_grants, db=db diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 7c971fa312..f25a5dcfd0 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -27,8 +27,8 @@ from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_permission -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants +from open_webui.models.access_grants import AccessGrants from open_webui.internal.db import get_session from sqlalchemy.orm import Session @@ -283,30 +283,14 @@ async def update_note_by_id( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() ) - # Check if user can share publicly - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_notes", - request.app.state.config.USER_PERMISSIONS, - db=db, - ) - ): - form_data.access_grants = [] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_notes", + db=db, + ) try: note = Notes.update_note_by_id(id, form_data, db=db) @@ -369,36 +353,13 @@ async def update_note_access_by_id( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() ) - # Strip public sharing if user lacks permission - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_notes", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = [ - grant - for grant in form_data.access_grants - if not ( - grant.get("principal_type") == "user" - and grant.get("principal_id") == "*" - ) - ] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_notes" + ) AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index d79fdbbd6e..0e2799d7b4 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -9,7 +9,7 @@ PromptModel, Prompts, ) -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants +from open_webui.models.access_grants import AccessGrants from open_webui.models.groups import Groups from open_webui.models.prompt_history import ( PromptHistories, @@ -18,7 +18,7 @@ ) from open_webui.constants import ERROR_MESSAGES from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.internal.db import get_session from sqlalchemy.orm import Session @@ -473,36 +473,13 @@ async def update_prompt_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - # Strip public sharing if user lacks permission - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_prompts", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = [ - grant - for grant in form_data.access_grants - if not ( - grant.get("principal_type") == "user" - and grant.get("principal_id") == "*" - ) - ] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_prompts" + ) AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index 6532b6ee04..c91dbc5b79 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -17,7 +17,7 @@ SkillAccessListResponse, Skills, ) -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants +from open_webui.models.access_grants import AccessGrants from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_access, has_permission @@ -341,36 +341,13 @@ async def update_skill_access_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - # Strip public sharing if user lacks permission - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_skills", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = [ - grant - for grant in form_data.access_grants - if not ( - grant.get("principal_type") == "user" - and grant.get("principal_id") == "*" - ) - ] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_skills" + ) AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index d60bcfb01b..7032b1b4b1 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -21,7 +21,7 @@ ToolAccessResponse, Tools, ) -from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_user_access_grant, strip_user_access_grants +from open_webui.models.access_grants import AccessGrants from open_webui.utils.plugin import ( load_tool_module_by_id, replace_imports, @@ -576,36 +576,13 @@ async def update_tool_access_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - # Strip public sharing if user lacks permission - if ( - user.role != "admin" - and has_public_read_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "sharing.public_tools", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = [ - grant - for grant in form_data.access_grants - if not ( - grant.get("principal_type") == "user" - and grant.get("principal_id") == "*" - ) - ] - - # Strip individual user sharing if user lacks permission - if ( - user.role != "admin" - and has_user_access_grant(form_data.access_grants) - and not has_permission( - user.id, - "access_grants.allow_users", - request.app.state.config.USER_PERMISSIONS, - ) - ): - form_data.access_grants = strip_user_access_grants(form_data.access_grants) + form_data.access_grants = filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + "sharing.public_tools" + ) AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db) diff --git a/backend/open_webui/utils/access_control.py b/backend/open_webui/utils/access_control.py index b7ea9830db..63fa8b26ca 100644 --- a/backend/open_webui/utils/access_control.py +++ b/backend/open_webui/utils/access_control.py @@ -194,3 +194,53 @@ def migrate_access_control( data[grants_key] = grants data.pop(ac_key, None) + +from open_webui.models.access_grants import ( + has_public_read_access_grant, + has_user_access_grant, + strip_user_access_grants, +) + + +def filter_allowed_access_grants( + default_permissions: Dict[str, Any], + user_id: str, + user_role: str, + access_grants: list, + public_permission_key: str, + db: Optional[Any] = None, +) -> list: + """ + Checks if the user has the required permissions to grant access to a resource. + Returns the filtered list of access grants if permissions are missing. + """ + if user_role == "admin" or not access_grants: + return access_grants + + # Check if user can share publicly + if has_public_read_access_grant(access_grants) and not has_permission( + user_id, + public_permission_key, + default_permissions, + db=db, + ): + access_grants = [ + grant + for grant in access_grants + if not ( + (grant.get("principal_type") if isinstance(grant, dict) else getattr(grant, "principal_type", None)) == "user" + and (grant.get("principal_id") if isinstance(grant, dict) else getattr(grant, "principal_id", None)) == "*" + ) + ] + + # Strip individual user sharing if user lacks permission + if has_user_access_grant(access_grants) and not has_permission( + user_id, + "access_grants.allow_users", + default_permissions, + db=db, + ): + access_grants = strip_user_access_grants(access_grants) + + return access_grants + From 3c8d658160809f6d651837cf93d89dddc1d17caf Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 16:25:38 -0600 Subject: [PATCH 013/173] fix: tools_dict issue --- backend/open_webui/utils/middleware.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 7823605bfd..db76c9560d 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2531,16 +2531,15 @@ async def tool_function(**kwargs): {"type": "function", "function": tool.get("spec", {})} for tool in tools_dict.values() ] - - else: - # If the function calling is not native, then call the tools function calling handler - try: - form_data, flags = await chat_completion_tools_handler( - request, form_data, extra_params, user, models, tools_dict - ) - sources.extend(flags.get("sources", [])) - except Exception as e: - log.exception(e) + else: + # If the function calling is not native, then call the tools function calling handler + try: + form_data, flags = await chat_completion_tools_handler( + request, form_data, extra_params, user, models, tools_dict + ) + sources.extend(flags.get("sources", [])) + except Exception as e: + log.exception(e) # Check if file context extraction is enabled for this model (default True) file_context_enabled = ( @@ -4322,7 +4321,8 @@ async def flush_pending_delta_data(threshold: int = 0): code = sanitize_code(code) if CODE_INTERPRETER_BLOCKED_MODULES: - blocking_code = textwrap.dedent(f""" + blocking_code = textwrap.dedent( + f""" import builtins BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES} @@ -4338,7 +4338,8 @@ def restricted_import(name, globals=None, locals=None, fromlist=(), level=0): return _real_import(name, globals, locals, fromlist, level) builtins.__import__ = restricted_import - """) + """ + ) code = blocking_code + "\n" + code if ( From 0b867590a8487c69c676d85fef9241623614410c Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 18:23:34 -0600 Subject: [PATCH 014/173] refac --- backend/open_webui/utils/response.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/utils/response.py b/backend/open_webui/utils/response.py index 5a4028f11b..8785e374b0 100644 --- a/backend/open_webui/utils/response.py +++ b/backend/open_webui/utils/response.py @@ -144,6 +144,7 @@ def convert_response_ollama_to_openai(ollama_response: dict) -> dict: async def convert_streaming_response_ollama_to_openai(ollama_streaming_response): + has_tool_calls = False async for data in ollama_streaming_response.body_iterator: data = json.loads(data) @@ -155,6 +156,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response) if tool_calls: openai_tool_calls = convert_ollama_tool_call_to_openai(tool_calls) + has_tool_calls = True done = data.get("done", False) @@ -166,7 +168,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response) model, message_content, reasoning_content, openai_tool_calls, usage ) - if done and openai_tool_calls: + if done and has_tool_calls: data["choices"][0]["finish_reason"] = "tool_calls" line = f"data: {json.dumps(data)}\n\n" From e6fe3ba8ef35855bc0bf547fc7f929abca3a6f51 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 18:23:47 -0600 Subject: [PATCH 015/173] refac --- backend/open_webui/utils/tools.py | 43 ++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 310fa999c7..52b53553d1 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -560,6 +560,7 @@ def is_builtin_tool_enabled(category: str) -> bool: # Generate spec from function pydantic_model = convert_function_to_pydantic_model(func) spec = convert_pydantic_model_to_openai_function_spec(pydantic_model) + spec = clean_openai_tool_schema(spec) tools_dict[func.__name__] = { "tool_id": f"builtin:{func.__name__}", @@ -668,6 +669,44 @@ def convert_function_to_pydantic_model(func: Callable) -> type[BaseModel]: return model +def clean_properties(schema: dict): + if not isinstance(schema, dict): + return + + if "anyOf" in schema: + non_null_types = [t for t in schema["anyOf"] if t.get("type") != "null"] + if len(non_null_types) == 1: + schema.update(non_null_types[0]) + del schema["anyOf"] + else: + schema["anyOf"] = non_null_types + + if "default" in schema and schema["default"] is None: + del schema["default"] + + # fix missing type + if "type" not in schema and "anyOf" not in schema and "properties" not in schema: + schema["type"] = "string" + + if "properties" in schema: + for prop_name, prop_schema in schema["properties"].items(): + clean_properties(prop_schema) + + if "items" in schema: + clean_properties(schema["items"]) + + +def clean_openai_tool_schema(spec: dict) -> dict: + import copy + + cleaned_spec = copy.deepcopy(spec) + + if "parameters" in cleaned_spec: + clean_properties(cleaned_spec["parameters"]) + + return cleaned_spec + + def get_functions_from_tool(tool: object) -> list[Callable]: return [ getattr(tool, func) @@ -690,7 +729,9 @@ def get_tool_specs(tool_module: object) -> list[dict]: ) specs = [ - convert_pydantic_model_to_openai_function_spec(function_model) + clean_openai_tool_schema( + convert_pydantic_model_to_openai_function_spec(function_model) + ) for function_model in function_models ] From 2461121637b3547019e8b9e519009811e4550a5c Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Feb 2026 18:31:26 -0600 Subject: [PATCH 016/173] refac --- src/lib/components/chat/Controls/Controls.svelte | 2 +- src/lib/components/common/Collapsible.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Controls/Controls.svelte b/src/lib/components/chat/Controls/Controls.svelte index 180b40225a..4e396c8bcc 100644 --- a/src/lib/components/chat/Controls/Controls.svelte +++ b/src/lib/components/chat/Controls/Controls.svelte @@ -19,7 +19,7 @@
-
{$i18n.t('Chat Controls')}
+
{$i18n.t('Controls')}
+ + {#if showAdvanced} +
+
+
+
+
+ {$i18n.t('OpenAPI Spec')} +
+
+
+ +
+
+
+ + +
+
+
+ +
+ {$i18n.t(`WebUI will make requests to "{{url}}"`, { + url: path.includes('://') + ? path + : `${url}${path.startsWith('/') ? '' : '/'}${path}` + })} +
+
+
+ {/if} + +
+
+
+
+
+ {$i18n.t('Auth')} +
+
+
+ +
+
+ +
+ +
+ {#if auth_type === 'bearer'} + + {:else if auth_type === 'none'} +
+ {$i18n.t('No authentication')} +
+ {:else if auth_type === 'session'} +
+ {$i18n.t('Forwards system user session credentials to authenticate')} +
+ {/if} +
+
+
+
+ +
+
+
+ {#if edit} + + {/if} + + +
+
+
+ +
+ + + diff --git a/src/lib/components/AddToolServerModal.svelte b/src/lib/components/AddToolServerModal.svelte index 878fcc5264..aac8f71ee1 100644 --- a/src/lib/components/AddToolServerModal.svelte +++ b/src/lib/components/AddToolServerModal.svelte @@ -542,80 +542,80 @@ {#if showAdvanced} - {#if ['', 'openapi'].includes(type)} -
-
-
-
-
- {$i18n.t('OpenAPI Spec')} + {#if ['', 'openapi'].includes(type)} +
+
+
+
+
+ {$i18n.t('OpenAPI Spec')} +
-
-
-
- -
+
+
+ +
-
- {#if spec_type === 'url'} -
- + {#if spec_type === 'url'} +
+ + +
+ {:else if spec_type === 'json'} +
- -
- {:else if spec_type === 'json'} -
- -