mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-15 04:18:18 +00:00
Merge pull request #32431 from open-webui/dev
Some checks are pending
Python CI / Ruff Format (3.11) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Blocked by required conditions
Python CI / Ruff Format (3.12) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / notify-helm-charts (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Frontend Build / Unit Tests (push) Waiting to run
Release / publish (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Blocked by required conditions
Frontend Build / Format & Build (push) Waiting to run
Release to PyPI / release (push) Waiting to run
Some checks are pending
Python CI / Ruff Format (3.11) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Blocked by required conditions
Python CI / Ruff Format (3.12) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / notify-helm-charts (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Frontend Build / Unit Tests (push) Waiting to run
Release / publish (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Blocked by required conditions
Frontend Build / Format & Build (push) Waiting to run
Release to PyPI / release (push) Waiting to run
0.12.2
This commit is contained in:
commit
6a18585ff4
97 changed files with 647 additions and 122 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.12.2] - 2026-10-14
|
||||
|
||||
### Added
|
||||
|
||||
- 📑 **MinerU 4.x servers.** The MinerU document engine now works with self-hosted MinerU 4.x servers, where every document failed with a 404 because 4.x removed the old parsing interface; 3.x servers work as before, 4.x servers take a tier (flash, basic, standard or advanced) and enable_ocr in the MinerU parameters while language and model_version stay 3.x only, and images that 4.x returns inside the document text are left out of the indexed content. [#32422](https://github.com/open-webui/open-webui/pull/32422), [Commit](https://github.com/open-webui/open-webui/commit/81f5b8adf80e49bd2213390b59dc867074c3738e), [#31516](https://github.com/open-webui/open-webui/issues/31516)
|
||||
- 🤝 **Keep both changes when comparing.** The Compare revision view now has a Keep both button that puts the original and the proposed text of the selected change into the proposed revision, original first, so you no longer have to pick one side. [Commit](https://github.com/open-webui/open-webui/commit/97b7ed73c84767611d3a26d594c36f63619d92ea)
|
||||
- 📚 **Open attached knowledge from its preview.** Clicking the name in the preview of an attached knowledge collection now opens that collection in the workspace, and file and link names are real links that open in a new tab. [Commit](https://github.com/open-webui/open-webui/commit/40bf4a1b99aa0aae316095eff100c07570c542da)
|
||||
- 🏷️ **Model ID in custom headers.** Custom headers on connections and tool servers can now use {{MODEL_ID}}, which holds the ID of the model handling the chat request, the workspace model's own ID for a workspace model and each model's own ID in a multi-model chat, so downstream services can route or log usage per model. [#32426](https://github.com/open-webui/open-webui/pull/32426), [Commit](https://github.com/open-webui/open-webui/commit/bae22752d3995f573a373ee1ae4735b746d952a9), [#30006](https://github.com/open-webui/open-webui/issues/30006)
|
||||
- 🧾 **Requested model in the audit log.** Audit log entries for chat and completion requests, including the OpenAI, Ollama and Anthropic compatible endpoints, now carry a model_id field with the model the client asked for at every AUDIT_LOG_LEVEL, where the model only appeared in the request body and went missing when MAX_BODY_LOG_SIZE cut a long prompt; other requests log it as null. [#32429](https://github.com/open-webui/open-webui/pull/32429), [Commit](https://github.com/open-webui/open-webui/commit/fb3d92a6ef56b5a8a6820f615f6e6294c44eaad7), [#30097](https://github.com/open-webui/open-webui/issues/30097)
|
||||
- 🔐 **Custom TLS certificate for Milvus.** Open WebUI can now connect to a Milvus server with a self-signed or company-issued TLS certificate by pointing the new "MILVUS_SERVER_PEM_PATH" environment variable at the certificate file, in normal and multi-tenancy mode. [#32427](https://github.com/open-webui/open-webui/pull/32427), [Commit](https://github.com/open-webui/open-webui/commit/3bc82bb2c110fc3ed7064e6107a4731b8d9ab5ae), [#31632](https://github.com/open-webui/open-webui/issues/31632)
|
||||
- 🧭 **call_tool for Tool Search.** With Tool Search on, the model now gets a new call_tool tool and runs a tool it found with search_tools through it, passing the tool's name and its arguments, instead of calling the deferred tool directly by a name that is not in the request's tool list, which strict providers rejected; approvals and other tool behavior stay the same. [Commit](https://github.com/open-webui/open-webui/commit/7b47c3b22ce983d57f4a13073748a935c4614889), [#32243](https://github.com/open-webui/open-webui/issues/32243)
|
||||
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔏 **Channel message metadata kept server side.** Channel messages posted or edited through the API no longer take their metadata from the request, so a user can no longer make a message appear to come from a channel webhook or a model. [#32421](https://github.com/open-webui/open-webui/pull/32421), [Commit](https://github.com/open-webui/open-webui/commit/44953df70ed758a6321f7a2e139c93aca29129f1)
|
||||
- 👮 **SCIM cannot delete admins.** An identity provider can no longer delete administrator accounts, including the first admin, through a SCIM delete request, which now answers with 403 and leaves the account in place, so a sync that prunes unassigned users can no longer leave the instance without an administrator; admins are removed in the admin panel. [#32415](https://github.com/open-webui/open-webui/pull/32415), [Commit](https://github.com/open-webui/open-webui/commit/6276e81cec38f5c5cce067e4be905c5710c2d7d6), [#27662](https://github.com/open-webui/open-webui/issues/27662)
|
||||
- 🗝️ **Collection names checked as stored.** Retrieval access checks now compare collection names the way vector stores resolve them, ignoring letter case and treating hyphens and underscores alike, so a name written differently can no longer pass the check as something other than the collection that is then read. [#32435](https://github.com/open-webui/open-webui/pull/32435), [Commit](https://github.com/open-webui/open-webui/commit/30f3c49fb02c22269b42fe3b310209926ee7e8f9)
|
||||
- 📱 **Voice on Android Chrome.** Voice calls and dictation on Android Chrome no longer go silent or hang on Listening after about ten uses until the page is reloaded, since the microphone level display now releases its audio output after each turn. [#32417](https://github.com/open-webui/open-webui/pull/32417), [Commit](https://github.com/open-webui/open-webui/commit/f35b1993271e6f0cf1ffc55375d634a65c9e0ab1), [#29969](https://github.com/open-webui/open-webui/issues/29969)
|
||||
- 🖥️ **Per chat terminal file actions.** With the Open Terminal chat context set to Per chat, opening HTML files, running notebooks, saving edited files, creating new files, deleting several selected files and downloading a folder or selection as a zip now use the chat's own workspace, where HTML files showed "File not found" and saved files never appeared in the chat. [#32418](https://github.com/open-webui/open-webui/pull/32418), [Commit](https://github.com/open-webui/open-webui/commit/77fd8aea613c42e0d590804c7f9fdcf6f5f4af02), [#29965](https://github.com/open-webui/open-webui/issues/29965)
|
||||
- 🧮 **Memory reindex rate limits.** Reindexing memories, from the admin Reindex Knowledge and Memory Vectors button, a user resetting their memory vectors or the automatic reindex after an embedding model change, now follows the Embedding Concurrent Requests limit and batch size, where it sent one request per memory at once and rate-limited providers failed most of them with 429 errors. [#32420](https://github.com/open-webui/open-webui/pull/32420), [Commit](https://github.com/open-webui/open-webui/commit/8bddf9bc62c3ffcaa3e6433bc5ac0f1d04a94421), [#32212](https://github.com/open-webui/open-webui/issues/32212)
|
||||
- ⏰ **Automation run results.** An automation run is now recorded as successful only after its reply finishes, and a reply that fails or is cancelled is recorded as an error that links to the chat, where runs counted as successful as soon as the reply started. [Commit](https://github.com/open-webui/open-webui/commit/de446952c2db1168dca5c6e3da6d961e6f55a8eb)
|
||||
- 📜 **Long polling kept out of the audit log.** With websocket support turned off, every socket.io long polling request is no longer written to the audit log as its own entry, which flooded it and could not be filtered with AUDIT_EXCLUDED_PATHS, so the audit log looks the same with either transport. [#32425](https://github.com/open-webui/open-webui/pull/32425), [Commit](https://github.com/open-webui/open-webui/commit/209b15b9633c63ce1b7cf3c1fdc6e032d67978c6), [#31871](https://github.com/open-webui/open-webui/issues/31871)
|
||||
- 📎 **Sources of approved tools.** With tool approval set to ask, a knowledge or file tool the user approved now saves and shows its sources below the reply, where they were lost after approval. [#32439](https://github.com/open-webui/open-webui/pull/32439), [Commit](https://github.com/open-webui/open-webui/commit/5fde0485a5adda046d283416a5c4c7a543d7a5fe), [#32436](https://github.com/open-webui/open-webui/issues/32436)
|
||||
- 🗂️ **Sidebar hover previews.** Moving the pointer from one sidebar folder to the next no longer leaves the first folder's preview open or keeps the next one from opening, and folder and chat previews now also open when the pointer rests on the padding around a name. [#32438](https://github.com/open-webui/open-webui/pull/32438), [Commit](https://github.com/open-webui/open-webui/commit/5e2748a03edd2459d07fcfea168daf740092bf94), [#32437](https://github.com/open-webui/open-webui/issues/32437)
|
||||
|
||||
### Changed
|
||||
|
||||
- 🔖 **Valkey connections tagged as Open WebUI.** Open WebUI's Valkey vector store connections now report their library name as GlidePySync(open-webui), so operators of a shared Valkey or ElastiCache can tell its traffic apart; this needs valkey-glide-sync 2.5.2 or newer, so installs that use VECTOR_DB=valkey with an older manually installed version must upgrade it, or startup fails. [#32428](https://github.com/open-webui/open-webui/pull/32428), [Commit](https://github.com/open-webui/open-webui/commit/980225e5823808de7ace86a8ee73a0b880b37023), [#31654](https://github.com/open-webui/open-webui/issues/31654)
|
||||
|
||||
## [0.12.1] - 2026-10-14
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -603,6 +603,7 @@ if VECTOR_DB == 'mariadb-vector':
|
|||
MILVUS_URI = os.getenv('MILVUS_URI', f'{DATA_DIR}/vector_db/milvus.db')
|
||||
MILVUS_DB = os.getenv('MILVUS_DB', 'default')
|
||||
MILVUS_TOKEN = os.getenv('MILVUS_TOKEN', None)
|
||||
MILVUS_SERVER_PEM_PATH = os.getenv('MILVUS_SERVER_PEM_PATH', '')
|
||||
MILVUS_INDEX_TYPE = os.getenv('MILVUS_INDEX_TYPE', 'HNSW')
|
||||
MILVUS_METRIC_TYPE = os.getenv('MILVUS_METRIC_TYPE', 'COSINE')
|
||||
MILVUS_HNSW_M = int(os.getenv('MILVUS_HNSW_M', '16'))
|
||||
|
|
|
|||
|
|
@ -1152,6 +1152,7 @@ async def chat_completion(
|
|||
await get_all_models(request, user=user)
|
||||
|
||||
model_id = form_data.get('model', None)
|
||||
request.state.model_id = model_id
|
||||
model_item = form_data.pop('model_item', {})
|
||||
tasks = form_data.pop('background_tasks', None)
|
||||
|
||||
|
|
@ -1813,7 +1814,7 @@ async def chat_completion(
|
|||
|
||||
# Fan out: one task per model
|
||||
if metadata.get('session_id') and metadata.get('chat_id'):
|
||||
task_ids = []
|
||||
chat_tasks = {}
|
||||
subagent_results = []
|
||||
is_internal = getattr(request.state, 'internal', False) is True
|
||||
chat_id = metadata['chat_id']
|
||||
|
|
@ -1869,13 +1870,13 @@ async def chat_completion(
|
|||
subagent_results.append(await process)
|
||||
continue
|
||||
|
||||
task_id, _ = await create_task(
|
||||
task_id, task = await create_task(
|
||||
request.app.state.redis,
|
||||
process,
|
||||
id=chat_id,
|
||||
task_id=per_model_metadata['task_id'],
|
||||
)
|
||||
task_ids.append(task_id)
|
||||
chat_tasks[task_id] = task
|
||||
|
||||
if is_internal:
|
||||
return {
|
||||
|
|
@ -1886,7 +1887,7 @@ async def chat_completion(
|
|||
}
|
||||
|
||||
# Emit chat:active=true
|
||||
if task_ids:
|
||||
if chat_tasks:
|
||||
event_emitter = await get_event_emitter(
|
||||
{**metadata, 'message_id': message_ids[0]['message_id']},
|
||||
update_db=False,
|
||||
|
|
@ -1895,9 +1896,13 @@ async def chat_completion(
|
|||
folder_id = metadata.get('folder_id') or await Chats.get_chat_folder_id(chat_id, user.id)
|
||||
await event_emitter({'type': 'chat:active', 'data': {'active': True, 'folder_id': folder_id}})
|
||||
|
||||
# Automation chat runs must finish before their caller records success.
|
||||
if metadata.get('automation_id') and is_saved_chat_id(chat_id):
|
||||
await asyncio.gather(*chat_tasks.values())
|
||||
|
||||
return {
|
||||
'status': True,
|
||||
'task_ids': task_ids,
|
||||
'task_ids': list(chat_tasks),
|
||||
'chat_id': chat_id,
|
||||
}
|
||||
else:
|
||||
|
|
@ -2055,6 +2060,7 @@ async def generate_messages(
|
|||
Anthropic's x-api-key header (via middleware translation).
|
||||
"""
|
||||
requested_model = form_data.get('model', '')
|
||||
request.state.model_id = requested_model
|
||||
input_tokens = None
|
||||
try:
|
||||
input_tokens = await openai.count_anthropic_tokens(request, form_data, user)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
|
|
@ -11,6 +12,10 @@ from langchain_core.documents import Document
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# The V1 API embeds every image as a base64 data URI, which is not text worth indexing
|
||||
MARKDOWN_IMAGE = re.compile(r'!\[([^\[\]\n]*)\]\(data:[^()\s]*\)')
|
||||
HTML_IMAGE = re.compile(r'<img\b[^<>]*>', re.IGNORECASE)
|
||||
|
||||
|
||||
class MinerULoader:
|
||||
"""
|
||||
|
|
@ -44,6 +49,7 @@ class MinerULoader:
|
|||
self.enable_table = self.params.get('enable_table', True)
|
||||
self.language = self.params.get('language', 'en')
|
||||
self.model_version = self.params.get('model_version', 'pipeline')
|
||||
self.tier = self.params.get('tier')
|
||||
|
||||
self.page_ranges = self.params.pop('page_ranges', '')
|
||||
|
||||
|
|
@ -63,6 +69,8 @@ class MinerULoader:
|
|||
try:
|
||||
if self.api_mode == 'cloud':
|
||||
return self._load_cloud_api()
|
||||
elif self._is_v1_api():
|
||||
return self._load_v1_api()
|
||||
else:
|
||||
return self._load_local_api()
|
||||
except Exception as e:
|
||||
|
|
@ -175,6 +183,116 @@ class MinerULoader:
|
|||
|
||||
return [Document(page_content=markdown_content, metadata=metadata)]
|
||||
|
||||
def _is_v1_api(self) -> bool:
|
||||
"""
|
||||
Detect MinerU 4.x, whose V1 API replaced the /file_parse endpoint of 3.x.
|
||||
"""
|
||||
try:
|
||||
return requests.get(f'{self.api_url}/v1/health', timeout=10).ok
|
||||
except requests.RequestException:
|
||||
return False
|
||||
|
||||
def _load_v1_api(self) -> List[Document]:
|
||||
"""
|
||||
Load document using the MinerU 4.x V1 API (asynchronous).
|
||||
Uploads the file, submits a parse job, polls it and downloads the markdown.
|
||||
"""
|
||||
log.info('Using MinerU V1 API at %s', self.api_url)
|
||||
|
||||
filename = os.path.basename(self.file_path)
|
||||
|
||||
upload = self._request_v1_api(
|
||||
'POST',
|
||||
'/v1/uploads',
|
||||
json={
|
||||
'filename': filename,
|
||||
'bytes': os.path.getsize(self.file_path),
|
||||
'mime_type': 'application/octet-stream',
|
||||
},
|
||||
).json()
|
||||
with open(self.file_path, 'rb') as f:
|
||||
self._request_v1_api('PUT', f'/v1/uploads/{upload["id"]}/content', data=f)
|
||||
upload = self._request_v1_api('POST', f'/v1/uploads/{upload["id"]}/complete').json()
|
||||
|
||||
file_entry = {'source': {'type': 'file_id', 'file_id': upload['file']['id']}}
|
||||
if self.page_ranges and filename.lower().endswith('.pdf'):
|
||||
file_entry['page_range'] = self.page_ranges
|
||||
|
||||
# The job rejects unknown fields, so the 3.x params are not passed through
|
||||
job = self._request_v1_api(
|
||||
'POST',
|
||||
'/v1/parse/jobs',
|
||||
json={
|
||||
'files': [file_entry],
|
||||
'tier': self.tier,
|
||||
'ocr_mode': 'ocr' if self.enable_ocr else 'auto',
|
||||
},
|
||||
).json()
|
||||
|
||||
log.info('Polling MinerU V1 job: %s', job['job_id'])
|
||||
deadline = time.monotonic() + self.timeout if self.timeout else None
|
||||
while job['status'] in ['queued', 'running']:
|
||||
if deadline and time.monotonic() > deadline:
|
||||
raise HTTPException(
|
||||
status.HTTP_504_GATEWAY_TIMEOUT,
|
||||
detail=f'MinerU processing timed out after {self.timeout} seconds',
|
||||
)
|
||||
time.sleep(2)
|
||||
job = self._request_v1_api('GET', f'/v1/parse/jobs/{job["job_id"]}').json()
|
||||
|
||||
file_result = job['files'][0]
|
||||
if job['status'] != 'completed':
|
||||
error = file_result.get('error') or {}
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail=f'MinerU processing {job["status"]}: {error.get("message", "Unknown error")}',
|
||||
)
|
||||
|
||||
markdown_id = file_result['output_files']['markdown']['file_id']
|
||||
markdown_content = self._request_v1_api('GET', f'/v1/files/{markdown_id}/content').content.decode('utf-8')
|
||||
markdown_content = HTML_IMAGE.sub('', MARKDOWN_IMAGE.sub(r'\1', markdown_content))
|
||||
|
||||
if not markdown_content:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail='MinerU returned empty markdown content',
|
||||
)
|
||||
|
||||
log.info('Successfully parsed document with MinerU V1 API: %s', filename)
|
||||
|
||||
metadata = {
|
||||
'source': filename,
|
||||
'api_mode': 'local',
|
||||
'tier': job['tier'],
|
||||
'version': (file_result.get('parse') or {}).get('parser_version', 'unknown'),
|
||||
}
|
||||
|
||||
return [Document(page_content=markdown_content, metadata=metadata)]
|
||||
|
||||
def _request_v1_api(self, method: str, path: str, **kwargs) -> requests.Response:
|
||||
"""
|
||||
Send one request to the MinerU V1 API and raise an HTTPException on failure.
|
||||
"""
|
||||
try:
|
||||
response = requests.request(method, f'{self.api_url}{path}', timeout=self.timeout, **kwargs)
|
||||
response.raise_for_status()
|
||||
except requests.Timeout:
|
||||
raise HTTPException(
|
||||
status.HTTP_504_GATEWAY_TIMEOUT,
|
||||
detail='MinerU V1 API request timed out',
|
||||
)
|
||||
except requests.HTTPError as e:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail=f'MinerU V1 API request failed: {e} - {e.response.text}',
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f'Error calling MinerU V1 API: {str(e)}',
|
||||
)
|
||||
return response
|
||||
|
||||
def _load_cloud_api(self) -> List[Document]:
|
||||
"""
|
||||
Load document using Cloud API (asynchronous).
|
||||
|
|
|
|||
|
|
@ -1318,20 +1318,22 @@ async def filter_accessible_collections(
|
|||
|
||||
validated = set()
|
||||
for name in safe_names:
|
||||
if name == 'knowledge-bases':
|
||||
# Some vector stores fold case and _/- together, so authorize a name as the collection it can resolve to.
|
||||
canonical = name.lower().replace('_', '-')
|
||||
if canonical == 'knowledge-bases':
|
||||
# System meta-collection — never exposed to non-admins.
|
||||
continue
|
||||
elif name.startswith('file-'):
|
||||
file_id = name[len('file-') :]
|
||||
elif canonical.startswith('file-'):
|
||||
file_id = canonical[len('file-') :]
|
||||
if await has_access_to_file(file_id=file_id, access_type=access_type, user=user):
|
||||
validated.add(name)
|
||||
elif name.startswith('user-memory-'):
|
||||
if name == f'user-memory-{user.id}':
|
||||
elif canonical.startswith('user-memory-'):
|
||||
if canonical == f'user-memory-{user.id}':
|
||||
validated.add(name)
|
||||
elif name.startswith('web-search-'):
|
||||
elif canonical.startswith('web-search-'):
|
||||
# Ephemeral per-query collections, owner-bound: process_web_search mints
|
||||
# them as web-search-{user.id}-<hash>, so only the creator may read/write.
|
||||
if name.startswith(f'web-search-{user.id}-'):
|
||||
if canonical.startswith(f'web-search-{user.id}-'):
|
||||
validated.add(name)
|
||||
else:
|
||||
# May be a knowledge-base ID or a legacy/ephemeral collection.
|
||||
|
|
@ -1340,9 +1342,9 @@ async def filter_accessible_collections(
|
|||
# ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS:
|
||||
# False (default) — deny (closes the unscoped namespace)
|
||||
# True — allow (preserves legacy behaviour)
|
||||
if await Knowledges.check_access_by_user_id(name, user.id, permission=access_type):
|
||||
if await Knowledges.check_access_by_user_id(canonical, user.id, permission=access_type):
|
||||
validated.add(name)
|
||||
elif ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS and not await Knowledges.get_knowledge_by_id(name):
|
||||
elif ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS and not await Knowledges.get_knowledge_by_id(canonical):
|
||||
# Not a KB at all — legacy/ephemeral collection, allow
|
||||
validated.add(name)
|
||||
return validated
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from open_webui.config import (
|
|||
MILVUS_INDEX_TYPE,
|
||||
MILVUS_IVF_FLAT_NLIST,
|
||||
MILVUS_METRIC_TYPE,
|
||||
MILVUS_SERVER_PEM_PATH,
|
||||
MILVUS_TOKEN,
|
||||
MILVUS_URI,
|
||||
)
|
||||
|
|
@ -244,9 +245,11 @@ class MilvusClient(VectorDBBase):
|
|||
def __init__(self):
|
||||
self.collection_prefix = 'open_webui'
|
||||
if MILVUS_TOKEN is None:
|
||||
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB)
|
||||
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, server_pem_path=MILVUS_SERVER_PEM_PATH)
|
||||
else:
|
||||
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN)
|
||||
self.client = Client(
|
||||
uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN, server_pem_path=MILVUS_SERVER_PEM_PATH
|
||||
)
|
||||
if ENABLE_DB_MIGRATIONS:
|
||||
collections = {
|
||||
collection_name_full.removesuffix(BM25_STAGING_SUFFIX)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from open_webui.config import (
|
|||
MILVUS_INDEX_TYPE,
|
||||
MILVUS_IVF_FLAT_NLIST,
|
||||
MILVUS_METRIC_TYPE,
|
||||
MILVUS_SERVER_PEM_PATH,
|
||||
MILVUS_TOKEN,
|
||||
MILVUS_URI,
|
||||
)
|
||||
|
|
@ -76,7 +77,9 @@ class MilvusClient(VectorDBBase):
|
|||
def __init__(self):
|
||||
# Milvus collection names can only contain numbers, letters, and underscores.
|
||||
self.collection_prefix = MILVUS_COLLECTION_PREFIX.replace('-', '_')
|
||||
self.client = Client(uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB)
|
||||
self.client = Client(
|
||||
uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB, server_pem_path=MILVUS_SERVER_PEM_PATH
|
||||
)
|
||||
|
||||
# Main collection types for multi-tenancy
|
||||
self.MEMORY_COLLECTION = f'{self.collection_prefix}_memories'
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ def _import_glide():
|
|||
)
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
'valkey-glide-sync is required when VECTOR_DB=valkey. Install it with: pip install valkey-glide-sync==2.3.1'
|
||||
'valkey-glide-sync is required when VECTOR_DB=valkey. Install it with: pip install valkey-glide-sync==2.5.2'
|
||||
) from e
|
||||
return {
|
||||
'Batch': Batch,
|
||||
|
|
@ -193,6 +193,7 @@ class ValkeyClient(VectorDBBase):
|
|||
database_id=db if db else None,
|
||||
request_timeout=5000,
|
||||
client_name='open_webui_vector_store_client',
|
||||
client_info_tag='open-webui',
|
||||
)
|
||||
try:
|
||||
self.client = GlideClient.create(config)
|
||||
|
|
@ -206,6 +207,7 @@ class ValkeyClient(VectorDBBase):
|
|||
database_id=db if db else None,
|
||||
request_timeout=10000, # 10s — HNSW indexing can take 1-4s per vector
|
||||
client_name='open_webui_vector_store_batch_client',
|
||||
client_info_tag='open-webui',
|
||||
)
|
||||
try:
|
||||
self.batch_client = GlideClient.create(batch_config)
|
||||
|
|
|
|||
|
|
@ -1462,6 +1462,8 @@ async def post_new_message(
|
|||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
await check_channels_access(request, user)
|
||||
# Message metadata is set by the server only.
|
||||
form_data.meta = None
|
||||
|
||||
try:
|
||||
message, channel = await new_message_handler(request, id, form_data, user, db)
|
||||
|
|
@ -1769,6 +1771,7 @@ async def update_message_by_id(
|
|||
if user.role != 'admin' and message.user_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||
|
||||
form_data.meta = None
|
||||
try:
|
||||
await Messages.update_message_by_id(message_id, form_data, db=db)
|
||||
message = await Messages.get_message_by_id(message_id, db=db)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
|
|
@ -142,15 +141,11 @@ async def reindex_memory_vectors_for_user(
|
|||
if not memories:
|
||||
return 0
|
||||
|
||||
vectors = await asyncio.gather(
|
||||
*[
|
||||
request.app.state.EMBEDDING_FUNCTION(
|
||||
memory_vector_text(memory.content, memory.path),
|
||||
prefix=RAG_EMBEDDING_CONTENT_PREFIX,
|
||||
user=user,
|
||||
)
|
||||
for memory in memories
|
||||
]
|
||||
# One list call so batching and the concurrency limit apply.
|
||||
vectors = await request.app.state.EMBEDDING_FUNCTION(
|
||||
[memory_vector_text(memory.content, memory.path) for memory in memories],
|
||||
prefix=RAG_EMBEDDING_CONTENT_PREFIX,
|
||||
user=user,
|
||||
)
|
||||
|
||||
await ASYNC_VECTOR_DB_CLIENT.upsert(
|
||||
|
|
@ -488,10 +483,9 @@ async def reset_memory_from_vector_db(
|
|||
"""Reset user's memory vector embeddings.
|
||||
|
||||
CRITICAL: We intentionally do NOT use Depends(get_async_session) here.
|
||||
This endpoint generates embeddings for ALL user memories in parallel using
|
||||
asyncio.gather(). A user with 100 memories would trigger 100 embedding API
|
||||
calls simultaneously. With a session held, this could block a connection
|
||||
for MINUTES, completely exhausting the connection pool.
|
||||
This endpoint generates embeddings for ALL user memories. With a session
|
||||
held, this could block a connection for MINUTES, completely exhausting the
|
||||
connection pool.
|
||||
"""
|
||||
await check_memories_permission(user)
|
||||
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,7 @@ async def generate_completion(
|
|||
if not await Config.get('ollama.enable'):
|
||||
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
|
||||
|
||||
request.state.model_id = form_data.model
|
||||
await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL)
|
||||
await validate_ollama_backend_idx(request, form_data.model, url_idx, user)
|
||||
|
||||
|
|
@ -1164,6 +1165,8 @@ async def generate_chat_completion(
|
|||
payload.pop('metadata', None)
|
||||
|
||||
model_id = payload['model']
|
||||
if not hasattr(request.state, 'model_id'):
|
||||
request.state.model_id = model_id
|
||||
model_info = await Models.get_model_by_id(model_id)
|
||||
|
||||
if model_info is not None:
|
||||
|
|
@ -1266,6 +1269,7 @@ async def generate_openai_completion(
|
|||
payload = form_data.model_dump(exclude_none=True, exclude=['metadata'])
|
||||
|
||||
model_id = form_data.model
|
||||
request.state.model_id = model_id
|
||||
model_info = await Models.get_model_by_id(model_id)
|
||||
if model_info is not None:
|
||||
if model_info.base_model_id:
|
||||
|
|
@ -1370,6 +1374,7 @@ async def generate_openai_chat_completion(
|
|||
payload = form_data.model_dump(exclude_none=True, exclude=['metadata'])
|
||||
|
||||
model_id = form_data.model
|
||||
request.state.model_id = model_id
|
||||
model_info = await Models.get_model_by_id(model_id)
|
||||
if model_info is not None:
|
||||
if model_info.base_model_id:
|
||||
|
|
@ -1427,6 +1432,7 @@ async def generate_anthropic_messages(
|
|||
|
||||
payload = {**form_data}
|
||||
model_id = payload.get('model', '')
|
||||
request.state.model_id = model_id
|
||||
|
||||
model_info = await Models.get_model_by_id(model_id)
|
||||
if model_info:
|
||||
|
|
@ -1485,6 +1491,7 @@ async def generate_responses(
|
|||
|
||||
payload = form_data.model_dump()
|
||||
model_id = form_data.model
|
||||
request.state.model_id = model_id
|
||||
|
||||
model_info = await Models.get_model_by_id(model_id)
|
||||
if model_info:
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ from open_webui.utils.anthropic import ANTHROPIC_VERSION, get_anthropic_models,
|
|||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.headers import get_headers_and_cookies, include_user_info_headers
|
||||
from open_webui.utils.json_codec import JSONCodec
|
||||
from open_webui.utils.misc import convert_logit_bias_input_to_json
|
||||
from open_webui.utils.misc import convert_logit_bias_input_to_json, get_provider_function
|
||||
from open_webui.utils.model_ids import strip_provider_model_prefix
|
||||
from open_webui.utils.payload import (
|
||||
apply_model_params_to_body_openai,
|
||||
|
|
@ -1190,6 +1190,8 @@ def _normalize_stored_item(item: dict) -> dict:
|
|||
if allowed is None:
|
||||
# Unknown type — pass through as-is (e.g. reasoning, extension items).
|
||||
return item
|
||||
if item_type == 'function_call':
|
||||
item = {**item, **get_provider_function(item)}
|
||||
return {k: v for k, v in item.items() if k in allowed}
|
||||
|
||||
|
||||
|
|
@ -1432,6 +1434,9 @@ async def generate_chat_completion(
|
|||
metadata = payload.pop('metadata', None)
|
||||
|
||||
model_id = form_data.get('model')
|
||||
# keep the model the client asked for when reached from /api/chat/completions
|
||||
if not hasattr(request.state, 'model_id'):
|
||||
request.state.model_id = model_id
|
||||
model_info = await Models.get_model_by_id(model_id)
|
||||
|
||||
# Check model info and override the payload
|
||||
|
|
@ -1819,6 +1824,7 @@ async def responses(
|
|||
|
||||
idx = 0
|
||||
model_id = form_data.model
|
||||
request.state.model_id = model_id
|
||||
|
||||
# Enforce per-model access control
|
||||
await check_model_access(user, await Models.get_model_by_id(model_id), BYPASS_MODEL_ACCESS_CONTROL)
|
||||
|
|
@ -1942,6 +1948,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||
|
||||
idx = 0
|
||||
model_id = payload.get('model') if isinstance(payload, dict) else None
|
||||
request.state.model_id = model_id
|
||||
if model_id:
|
||||
models = request.app.state.OPENAI_MODELS
|
||||
if not models or model_id not in models:
|
||||
|
|
|
|||
|
|
@ -870,6 +870,13 @@ async def delete_user(
|
|||
detail=f'User {user_id} not found',
|
||||
)
|
||||
|
||||
# An IdP prune must not lock the instance out by deleting an admin.
|
||||
if user.role == 'admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
|
||||
)
|
||||
|
||||
success = await Auths.delete_auth_by_id(user_id, db=db)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -3837,10 +3837,12 @@ async def search_tools(
|
|||
"""
|
||||
Search the tools listed in <available_tools> and return their full definitions.
|
||||
Pass the exact tool name when you already know it.
|
||||
Invoke a result with call_tool(tool_name, params), where params is a JSON-encoded object
|
||||
matching the tool's parameters. Do not call deferred tools directly by name.
|
||||
|
||||
:param query: Keywords describing the capability you need (e.g. "jira create issue"), or an exact tool name
|
||||
:param count: Maximum number of results to return (default: 5, max: 20)
|
||||
:return: JSON with the definitions of the matching tools, which can then be called by name
|
||||
:return: JSON with the definitions of the matching tools, which can then be invoked through call_tool
|
||||
"""
|
||||
from open_webui.utils.tool_search import search_deferred_tools
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class AuditLogEntry:
|
|||
request_uri: str
|
||||
user_agent: Optional[str] = None
|
||||
source_ip: Optional[str] = None
|
||||
model_id: Optional[str] = None
|
||||
# `Request` audit level properties
|
||||
request_object: Any = None
|
||||
# `Request Response` level
|
||||
|
|
@ -254,6 +255,9 @@ class AuditLoggingMiddleware:
|
|||
if request.method not in self.audited_methods:
|
||||
return True
|
||||
|
||||
if request.url.path.startswith('/ws/socket.io'):
|
||||
return True
|
||||
|
||||
path = request.url.path.lower()
|
||||
for endpoint in self.ALWAYS_LOG_ENDPOINTS:
|
||||
if path.startswith(endpoint):
|
||||
|
|
@ -309,6 +313,7 @@ class AuditLoggingMiddleware:
|
|||
response_status_code=context.metadata.get('response_status_code', None),
|
||||
source_ip=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get('user-agent'),
|
||||
model_id=getattr(request.state, 'model_id', None),
|
||||
request_object=request_body,
|
||||
response_object=response_body,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -357,6 +357,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
|||
session_id + chat_id + message_id → async task → pipeline handles everything
|
||||
(filters, model params, knowledge/RAG, tools, DB saves, webhooks).
|
||||
"""
|
||||
chat = None
|
||||
try:
|
||||
user = await Users.get_user_by_id(automation.user_id)
|
||||
if not user:
|
||||
|
|
@ -521,6 +522,12 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
|||
request = _build_request(app, token=token)
|
||||
await app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user)
|
||||
|
||||
reply = await Chats.get_message_by_id_and_message_id(chat.id, assistant_msg_id)
|
||||
if reply and reply.get('error'):
|
||||
raise ValueError(str(reply['error']['content']))
|
||||
if not reply or not reply.get('done'):
|
||||
raise ValueError('Automation reply did not complete')
|
||||
|
||||
# Notify user
|
||||
from open_webui.socket.main import sio
|
||||
|
||||
|
|
@ -544,16 +551,18 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
|||
data={'name': automation.name, 'chat_id': chat.id},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
except (Exception, asyncio.CancelledError) as e:
|
||||
log.exception(f'Automation {automation.id} failed')
|
||||
error = str(e)[:4000]
|
||||
await _record_run(automation.id, 'error', error=error)
|
||||
error = 'Automation reply was cancelled' if isinstance(e, asyncio.CancelledError) else str(e)[:4000]
|
||||
await _record_run(automation.id, 'error', chat_id=chat.id if chat else None, error=error)
|
||||
await publish_event(
|
||||
app,
|
||||
EVENTS.AUTOMATION_RUN_FAILED,
|
||||
subject_id=automation.id,
|
||||
data={'name': automation.name, 'error': error},
|
||||
data={'name': automation.name, 'error': error, **({'chat_id': chat.id} if chat else {})},
|
||||
)
|
||||
if isinstance(e, asyncio.CancelledError):
|
||||
raise
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ def parse_custom_headers(
|
|||
'{{FILE_NAME}}': metadata.get('file_name', '') or '',
|
||||
'{{FILE_CONTENT_TYPE}}': metadata.get('file_content_type', '') or '',
|
||||
'{{TASK}}': metadata.get('task', '') or '',
|
||||
'{{MODEL_ID}}': metadata.get('model_id', '') or '',
|
||||
'{{USER_ID}}': (user.id if user else '') or '',
|
||||
'{{USER_NAME}}': (user.name.strip() if user else '') or '',
|
||||
'{{USER_EMAIL}}': (user.email.strip() if user else '') or '',
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ def file_format(record: 'Record'):
|
|||
'response_status_code': record['extra'].get('response_status_code', 0),
|
||||
'source_ip': record['extra'].get('source_ip', ''),
|
||||
'user_agent': record['extra'].get('user_agent', ''),
|
||||
'model_id': record['extra'].get('model_id', ''),
|
||||
'request_object': record['extra'].get('request_object', b''),
|
||||
'response_object': record['extra'].get('response_object', b''),
|
||||
'extra': record['extra'].get('extra', {}),
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ from open_webui.utils.task import (
|
|||
tools_function_calling_generation_template,
|
||||
)
|
||||
from open_webui.utils.tool_approval import complete_tool_call, pending_tool_calls
|
||||
from open_webui.utils.tool_search import apply_tool_search
|
||||
from open_webui.utils.tool_search import apply_tool_search, resolve_tool_call
|
||||
from open_webui.utils.tools import (
|
||||
connect_mcp_server,
|
||||
get_attached_knowledge,
|
||||
|
|
@ -441,6 +441,9 @@ def _split_tool_calls(
|
|||
return expanded
|
||||
|
||||
|
||||
CITATION_TOOL_NAMES = ('fetch_url', 'view_file', 'view_knowledge_file', 'query_knowledge_files', 'query_chat_files')
|
||||
|
||||
|
||||
def get_citation_source_from_tool_result(
|
||||
tool_name: str, tool_params: dict, tool_result: str, tool_id: str = ''
|
||||
) -> list[dict]:
|
||||
|
|
@ -3290,7 +3293,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
metadata['tools'] = tools_dict
|
||||
|
||||
if metadata.get('params', {}).get('function_calling') != 'legacy':
|
||||
deferred = await apply_tool_search(form_data, metadata, tools_dict)
|
||||
deferred = await apply_tool_search(form_data, metadata, tools_dict, inlet_filter_tools)
|
||||
|
||||
# If the function calling is native, then call the tools function calling handler
|
||||
form_data['tools'] = [
|
||||
|
|
@ -3438,6 +3441,9 @@ async def build_chat_response_context(request, form_data, user, model, metadata,
|
|||
|
||||
|
||||
async def execute_tool_call(form_data, metadata, event_caller, tool_call):
|
||||
error = resolve_tool_call(tool_call, metadata.get('tools', {}))
|
||||
if error:
|
||||
return {}, error, None, None, False
|
||||
name = tool_call.get('function', {}).get('name', '')
|
||||
tool_args = tool_call.get('function', {}).get('arguments', '{}')
|
||||
params = {}
|
||||
|
|
@ -3619,6 +3625,14 @@ async def resume_tool_calls(request, form_data, user, model, metadata, message)
|
|||
request, name, result, tool_type, direct_tool, metadata, user
|
||||
)
|
||||
await terminal_event_handler(name, params, result, event_emitter)
|
||||
citations_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get(
|
||||
'citations', True
|
||||
)
|
||||
if event_emitter and citations_enabled and name in CITATION_TOOL_NAMES and result:
|
||||
for source in get_citation_source_from_tool_result(
|
||||
tool_name=name, tool_params=params, tool_result=result, tool_id=tool.get('tool_id', '')
|
||||
):
|
||||
await event_emitter({'type': 'source', 'data': source})
|
||||
content = tool_result_content(result)
|
||||
item['arguments'] = tool_call.get('function', {}).get('arguments', '{}')
|
||||
output_parts = [{'type': 'input_text', 'text': content}]
|
||||
|
|
@ -5981,7 +5995,41 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
):
|
||||
tool_call_iterations += 1
|
||||
|
||||
response_tool_calls = tool_calls.pop(0)
|
||||
# Resolve dispatch before approvals and special handling such as ask_user.
|
||||
response_tool_calls = []
|
||||
output_by_call_id = {
|
||||
item.get('call_id'): item for item in output if item.get('type') == 'function_call'
|
||||
}
|
||||
for tool_call in tool_calls.pop(0):
|
||||
error = resolve_tool_call(tool_call, metadata.get('tools', {}))
|
||||
call_id = tool_call.get('id') or output_id('fc')
|
||||
tool_call['id'] = call_id
|
||||
function = tool_call.get('function', {})
|
||||
item = output_by_call_id.get(call_id)
|
||||
if item is None:
|
||||
item = {'type': 'function_call', 'id': call_id, 'call_id': call_id}
|
||||
output.append(item)
|
||||
output_by_call_id[call_id] = item
|
||||
item.update(
|
||||
name=function.get('name', ''),
|
||||
arguments=function.get('arguments', '{}'),
|
||||
status='failed' if error else item.get('status', 'in_progress'),
|
||||
)
|
||||
if 'provider_function' in tool_call:
|
||||
item['provider_function'] = tool_call['provider_function']
|
||||
if error:
|
||||
output.append(
|
||||
{
|
||||
'type': 'function_call_output',
|
||||
'id': output_id('fco'),
|
||||
'call_id': call_id,
|
||||
'output': [{'type': 'input_text', 'text': error}],
|
||||
'status': 'failed',
|
||||
}
|
||||
)
|
||||
else:
|
||||
response_tool_calls.append(tool_call)
|
||||
|
||||
ask_user_staged, ask_user_error = stage_ask_user_tool_calls(response_tool_calls, output, output_id)
|
||||
if ask_user_error:
|
||||
response_tool_calls = [
|
||||
|
|
@ -6006,24 +6054,6 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
if item.get('name') == 'ask_user' and item.get('status') == 'pending':
|
||||
item['status'] = 'in_progress'
|
||||
|
||||
# Append function_call items for each tool call
|
||||
# (Responses API already has them from streaming, so skip duplicates)
|
||||
existing_call_ids = {item.get('call_id') for item in output if item.get('type') == 'function_call'}
|
||||
for tc in response_tool_calls:
|
||||
call_id = tc.get('id', '')
|
||||
if call_id not in existing_call_ids:
|
||||
func = tc.get('function', {})
|
||||
output.append(
|
||||
{
|
||||
'type': 'function_call',
|
||||
'id': call_id or output_id('fc'),
|
||||
'call_id': call_id,
|
||||
'name': func.get('name', ''),
|
||||
'arguments': func.get('arguments', '{}'),
|
||||
'status': 'in_progress',
|
||||
}
|
||||
)
|
||||
|
||||
tool_approval_mode = metadata.get('params', {}).get('tool_approval_mode', 'full')
|
||||
if (
|
||||
response_tool_calls
|
||||
|
|
@ -6119,18 +6149,7 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
)
|
||||
|
||||
# Extract citation sources from tool results
|
||||
if (
|
||||
citations_enabled
|
||||
and tool_function_name
|
||||
in [
|
||||
'fetch_url',
|
||||
'view_file',
|
||||
'view_knowledge_file',
|
||||
'query_knowledge_files',
|
||||
'query_chat_files',
|
||||
]
|
||||
and tool_result
|
||||
):
|
||||
if citations_enabled and tool_function_name in CITATION_TOOL_NAMES and tool_result:
|
||||
try:
|
||||
citation_sources = get_citation_source_from_tool_result(
|
||||
tool_name=tool_function_name,
|
||||
|
|
|
|||
|
|
@ -336,6 +336,18 @@ def get_reasoning_details(payload: dict):
|
|||
return payload.get('reasoning_details') or provider_details
|
||||
|
||||
|
||||
def get_provider_function(item: dict) -> dict:
|
||||
"""Restore the model's call when OWUI displays and executes a resolved target instead."""
|
||||
function = item.get('provider_function')
|
||||
if not isinstance(function, dict):
|
||||
function = item
|
||||
arguments = function.get('arguments', '{}')
|
||||
return {
|
||||
'name': function.get('name', ''),
|
||||
'arguments': arguments if isinstance(arguments, str) else JSONCodec.dumps(arguments),
|
||||
}
|
||||
|
||||
|
||||
def convert_output_to_messages(
|
||||
output: list,
|
||||
raw: bool = False,
|
||||
|
|
@ -502,18 +514,11 @@ def convert_output_to_messages(
|
|||
continue
|
||||
|
||||
# Collect tool calls to batch into assistant message
|
||||
arguments = item.get('arguments', '{}')
|
||||
# Ensure arguments is always a JSON string
|
||||
if not isinstance(arguments, str):
|
||||
arguments = JSONCodec.dumps(arguments)
|
||||
pending_tool_calls.append(
|
||||
{
|
||||
'id': item.get('call_id', ''),
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': item.get('name', ''),
|
||||
'arguments': arguments,
|
||||
},
|
||||
'function': get_provider_function(item),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Tool search: large tool schemas are left out of the provider `tools` array and listed by name in an
|
||||
`<available_tools>` system prompt block. `search_tools` returns the full definitions of matching tools,
|
||||
which the model then calls by name. The `tools` array never changes during a chat, so prompt caches hold.
|
||||
which the model invokes through `call_tool`. Discovery does not change the provider `tools` array.
|
||||
"""
|
||||
|
||||
import fnmatch
|
||||
|
|
@ -14,6 +14,7 @@ from open_webui.utils.json_codec import JSONCodec
|
|||
from open_webui.utils.misc import add_or_update_system_message
|
||||
|
||||
SEARCH_TOOL_NAME = 'search_tools'
|
||||
CALL_TOOL_NAME = 'call_tool'
|
||||
MANIFEST_DESCRIPTION_MAX_CHARS = 100
|
||||
MAX_SEARCH_COUNT = 20
|
||||
DEFAULT_SEARCH_COUNT = 5
|
||||
|
|
@ -66,15 +67,20 @@ def build_deferred_tools_manifest(tools_dict: dict[str, dict], deferred: list[st
|
|||
'<available_tools>\n'
|
||||
'The following tools are available but their definitions are not loaded. '
|
||||
f'To use one, call `{SEARCH_TOOL_NAME}` with a short keyword query or the exact tool name to get its '
|
||||
'definition, then call the tool by name with the parameters it defines. '
|
||||
f'definition, then use `{CALL_TOOL_NAME}` with `tool_name` set to its exact name and `params` set '
|
||||
'to a JSON-encoded object containing its arguments. Do not call deferred tools directly. '
|
||||
'For example: call_tool(tool_name="get_weather", params=\'{"city":"Dubai"}\'). '
|
||||
'Never tell the user a capability is unavailable without searching first.\n'
|
||||
f'{entries}</available_tools>'
|
||||
)
|
||||
|
||||
|
||||
async def apply_tool_search(form_data: dict, metadata: dict, tools_dict: dict[str, dict]) -> set[str]:
|
||||
"""Add search_tools and the manifest when tools can be deferred; returns the names to leave out of `tools`."""
|
||||
config = SEARCH_TOOL_NAME not in tools_dict and await get_tool_search_config()
|
||||
async def apply_tool_search(
|
||||
form_data: dict, metadata: dict, tools_dict: dict[str, dict], extra_tools: list[dict] | None = None
|
||||
) -> set[str]:
|
||||
"""Add discovery and dispatch when tools can be deferred; return names to omit from provider tools."""
|
||||
names = set(tools_dict) | {(tool.get('function') or tool).get('name') for tool in extra_tools or []}
|
||||
config = not names.intersection({SEARCH_TOOL_NAME, CALL_TOOL_NAME}) and await get_tool_search_config()
|
||||
deferred = select_deferred_tools(tools_dict, config) if config else []
|
||||
if not deferred:
|
||||
return set()
|
||||
|
|
@ -100,9 +106,64 @@ async def apply_tool_search(form_data: dict, metadata: dict, tools_dict: dict[st
|
|||
'spec': get_builtin_tool_spec(search_tools),
|
||||
'type': 'builtin',
|
||||
}
|
||||
# Middleware resolves this before approvals and execution, preserving the target tool's behavior.
|
||||
tools_dict[CALL_TOOL_NAME] = {
|
||||
'tool_id': f'builtin:{CALL_TOOL_NAME}',
|
||||
'type': 'builtin',
|
||||
'spec': {
|
||||
'name': CALL_TOOL_NAME,
|
||||
'description': (
|
||||
'Invoke a tool found by search_tools. Use its exact name and encode its arguments as a JSON '
|
||||
'object string matching the returned definition. Use "{}" for a tool without arguments.'
|
||||
),
|
||||
'strict': True,
|
||||
'parameters': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'tool_name': {'type': 'string', 'description': 'Exact tool name returned by search_tools.'},
|
||||
'params': {'type': 'string', 'description': 'JSON-encoded object of arguments for the tool.'},
|
||||
},
|
||||
'required': ['tool_name', 'params'],
|
||||
'additionalProperties': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
return set(deferred)
|
||||
|
||||
|
||||
def resolve_tool_call(tool_call: dict, tools: dict[str, dict]) -> str | None:
|
||||
"""Unwrap our dispatcher in place, retaining the provider call for history replay. Never execute here."""
|
||||
function = tool_call.get('function', {})
|
||||
if (
|
||||
function.get('name') != CALL_TOOL_NAME
|
||||
or tools.get(CALL_TOOL_NAME, {}).get('tool_id') != f'builtin:{CALL_TOOL_NAME}'
|
||||
):
|
||||
return None
|
||||
|
||||
try:
|
||||
arguments = JSONCodec.loads(function.get('arguments', ''))
|
||||
if not isinstance(arguments, dict) or set(arguments) != {'tool_name', 'params'}:
|
||||
raise ValueError('call_tool requires tool_name and params.')
|
||||
name = arguments['tool_name']
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
raise ValueError('tool_name must be a non-empty string.')
|
||||
if name in {SEARCH_TOOL_NAME, CALL_TOOL_NAME}:
|
||||
raise ValueError('call_tool cannot invoke search_tools or itself.')
|
||||
if name not in tools:
|
||||
raise ValueError(f'Tool "{name}" is not available. Search for an available tool.')
|
||||
if not isinstance(arguments['params'], str):
|
||||
raise ValueError('params must be a JSON-encoded object string.')
|
||||
params = JSONCodec.loads(arguments['params'])
|
||||
if not isinstance(params, dict):
|
||||
raise ValueError('params must decode to a JSON object.')
|
||||
except (TypeError, ValueError) as error:
|
||||
return f'Error: {error}'
|
||||
|
||||
tool_call['provider_function'] = dict(function)
|
||||
tool_call['function'] = {'name': name, 'arguments': JSONCodec.dumps(params)}
|
||||
return None
|
||||
|
||||
|
||||
def strip_deferred_tools_manifest(system_prompt: str | None) -> str | None:
|
||||
"""Remove the manifest from a system prompt that is reused by a request which builds its own."""
|
||||
return _MANIFEST_RE.sub('', system_prompt) if system_prompt else system_prompt
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ pgvector==0.4.2
|
|||
PyMySQL==1.2.0
|
||||
boto3==1.42.62
|
||||
# mariadb==1.1.14 should be added if you want to support MariaDB
|
||||
# valkey-glide-sync==2.3.1 # optional: install manually if VECTOR_DB=valkey
|
||||
# valkey-glide-sync==2.5.2 # optional: install manually if VECTOR_DB=valkey
|
||||
|
||||
pymilvus==2.6.14
|
||||
qdrant-client==1.18.0
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^4.5.0",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ dependencies = [
|
|||
"pycrdt==0.14.8",
|
||||
"redis==8.0.1",
|
||||
"hiredis==3.4.2",
|
||||
# "valkey-glide-sync==2.3.1", # optional: install manually if VECTOR_DB=valkey
|
||||
# "valkey-glide-sync==2.5.2", # optional: install manually if VECTOR_DB=valkey
|
||||
|
||||
"pytz==2026.2",
|
||||
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@
|
|||
let terminalChatContextPending = false;
|
||||
let terminalChatContextHidden = false;
|
||||
let isPerChatTerminal = false;
|
||||
$: portPreviewBaseUrl =
|
||||
$: previewBaseUrl =
|
||||
selectedTerminal && isPerChatTerminal && chatId
|
||||
? `${selectedTerminal.url}/chats/${encodeURIComponent(chatId)}`
|
||||
: (selectedTerminal?.url ?? '');
|
||||
|
|
@ -1033,7 +1033,12 @@
|
|||
// Directories end with '/', downloaded as a ZIP archive
|
||||
const isDir = path.endsWith('/');
|
||||
const result = isDir
|
||||
? await archiveFromTerminal(terminal.url, terminal.key, [path.replace(/\/$/, '')])
|
||||
? await archiveFromTerminal(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
[path.replace(/\/$/, '')],
|
||||
chatId ?? undefined
|
||||
)
|
||||
: await downloadFileBlob(terminal.url, terminal.key, path, chatId ?? undefined);
|
||||
if (!result) {
|
||||
toast.error($i18n.t('Download failed'));
|
||||
|
|
@ -1218,7 +1223,13 @@
|
|||
if (!terminal) return;
|
||||
|
||||
const emptyFile = new File([''], name, { type: 'application/octet-stream' });
|
||||
const result = await uploadToTerminal(terminal.url, terminal.key, currentPath, emptyFile);
|
||||
const result = await uploadToTerminal(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
currentPath,
|
||||
emptyFile,
|
||||
chatId ?? undefined
|
||||
);
|
||||
toast[result ? 'success' : 'error'](
|
||||
result ? $i18n.t('File created') : $i18n.t('Failed to create file')
|
||||
);
|
||||
|
|
@ -1387,7 +1398,12 @@
|
|||
const paths = [...selectedEntries];
|
||||
let ok = 0;
|
||||
for (const p of paths) {
|
||||
const result = await deleteEntry(terminal.url, terminal.key, p.replace(/\/$/, ''));
|
||||
const result = await deleteEntry(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
p.replace(/\/$/, ''),
|
||||
chatId ?? undefined
|
||||
);
|
||||
if (result) ok++;
|
||||
}
|
||||
toast[ok > 0 ? 'success' : 'error'](
|
||||
|
|
@ -1415,7 +1431,12 @@
|
|||
const toastId = toast.loading($i18n.t('Preparing download...'));
|
||||
try {
|
||||
// Archive everything into a single ZIP
|
||||
const result = await archiveFromTerminal(terminal.url, terminal.key, paths);
|
||||
const result = await archiveFromTerminal(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
paths,
|
||||
chatId ?? undefined
|
||||
);
|
||||
if (!result) {
|
||||
toast.error($i18n.t('Download failed'));
|
||||
return;
|
||||
|
|
@ -1907,7 +1928,7 @@
|
|||
>
|
||||
{#if previewPort !== null}
|
||||
<PortPreview
|
||||
baseUrl={portPreviewBaseUrl}
|
||||
baseUrl={previewBaseUrl}
|
||||
port={previewPort}
|
||||
overlay={overlay || isDraggingHandle}
|
||||
onClose={() => {
|
||||
|
|
@ -1945,7 +1966,7 @@
|
|||
const DOMPurify = (await import('dompurify')).default;
|
||||
fileOfficeHtml = DOMPurify.sanitize(result.html);
|
||||
}}
|
||||
baseUrl={selectedTerminal?.url ?? ''}
|
||||
baseUrl={previewBaseUrl}
|
||||
apiKey={selectedTerminal?.key ?? ''}
|
||||
overlay={overlay || isDraggingHandle}
|
||||
onSave={async (content) => {
|
||||
|
|
@ -1954,7 +1975,13 @@
|
|||
const fileName = selectedFile.split('/').pop() ?? 'file';
|
||||
const dir = selectedFile.substring(0, selectedFile.lastIndexOf('/') + 1) || '/';
|
||||
const file = new File([content], fileName, { type: 'text/plain' });
|
||||
const result = await uploadToTerminal(terminal.url, terminal.key, dir, file);
|
||||
const result = await uploadToTerminal(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
dir,
|
||||
file,
|
||||
chatId ?? undefined
|
||||
);
|
||||
toast[result ? 'success' : 'error'](
|
||||
result ? $i18n.t('File saved') : $i18n.t('Failed to save file')
|
||||
);
|
||||
|
|
@ -2192,7 +2219,7 @@
|
|||
{#key JSON.stringify([chatId, selectedTerminal.url])}
|
||||
<PortList
|
||||
baseUrl={selectedTerminal.url}
|
||||
previewBaseUrl={portPreviewBaseUrl}
|
||||
{previewBaseUrl}
|
||||
apiKey={selectedTerminal.key}
|
||||
{chatId}
|
||||
on:previewPort={(e) => {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
let hasStartedSpeaking = false;
|
||||
let mediaRecorder;
|
||||
let audioStream = null;
|
||||
let audioContext: AudioContext | undefined;
|
||||
let audioChunks = [];
|
||||
let destroyed = false;
|
||||
let avatarView: VoiceAvatar | undefined;
|
||||
|
|
@ -358,6 +359,12 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Chrome on Android caps open audio outputs at 10.
|
||||
const closeAudioContext = () => {
|
||||
void audioContext?.close();
|
||||
audioContext = undefined;
|
||||
};
|
||||
|
||||
const stopAudioStream = async () => {
|
||||
try {
|
||||
if (mediaRecorder) {
|
||||
|
|
@ -367,6 +374,8 @@
|
|||
console.log('Error stopping audio stream:', error);
|
||||
}
|
||||
|
||||
closeAudioContext();
|
||||
|
||||
if (!audioStream) return;
|
||||
|
||||
audioStream.getAudioTracks().forEach(function (track) {
|
||||
|
|
@ -387,7 +396,8 @@
|
|||
};
|
||||
|
||||
const analyseAudio = (stream) => {
|
||||
const audioContext = new AudioContext();
|
||||
closeAudioContext();
|
||||
audioContext = new AudioContext();
|
||||
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
const analyser = audioContext.createAnalyser();
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
};
|
||||
|
||||
let stream;
|
||||
let audioContext: AudioContext | null = null;
|
||||
let speechRecognition;
|
||||
|
||||
let mediaRecorder;
|
||||
|
|
@ -116,8 +117,14 @@
|
|||
return Math.min(1.0, Math.max(0.01, scaledRMS));
|
||||
};
|
||||
|
||||
// Chrome on Android caps open audio outputs at 10.
|
||||
const closeAudioContext = () => {
|
||||
void audioContext?.close();
|
||||
audioContext = null;
|
||||
};
|
||||
|
||||
const analyseAudio = (stream) => {
|
||||
const audioContext = new AudioContext();
|
||||
audioContext = new AudioContext();
|
||||
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
const analyser = audioContext.createAnalyser();
|
||||
|
|
@ -399,6 +406,7 @@
|
|||
}
|
||||
|
||||
stream = null;
|
||||
closeAudioContext();
|
||||
};
|
||||
|
||||
const confirmRecording = async () => {
|
||||
|
|
@ -418,6 +426,7 @@
|
|||
}
|
||||
|
||||
stream = null;
|
||||
closeAudioContext();
|
||||
};
|
||||
|
||||
let resizeObserver;
|
||||
|
|
@ -463,6 +472,7 @@
|
|||
window.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
releaseWakeLock();
|
||||
closeAudioContext();
|
||||
// remove resize observer
|
||||
resizeObserver.disconnect();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
} from './diffEditorAlignment';
|
||||
import {
|
||||
copyChunk,
|
||||
keepBothChunk,
|
||||
selectedChunk,
|
||||
chunkOverview,
|
||||
changedLineCounts,
|
||||
|
|
@ -65,6 +66,7 @@
|
|||
let comparing = false;
|
||||
let merge: MergeView | null = null;
|
||||
let activeChunk = 0;
|
||||
let canKeepBoth = false;
|
||||
let overview: ReturnType<typeof chunkOverview> = [];
|
||||
let chunkCount = 0;
|
||||
let lineNumberDigits = 1;
|
||||
|
|
@ -101,6 +103,7 @@
|
|||
proposal = session.proposal;
|
||||
comparing = proposedContent !== undefined;
|
||||
activeChunk = chunkCount = 0;
|
||||
canKeepBoth = false;
|
||||
overview = [];
|
||||
lineChanges = { removed: 0, added: 0 };
|
||||
copied = null;
|
||||
|
|
@ -200,6 +203,11 @@
|
|||
if (index >= 0) activeChunk = index;
|
||||
}
|
||||
activeChunk = Math.max(0, Math.min(activeChunk, chunkCount - 1));
|
||||
const chunk = merge.chunks[activeChunk];
|
||||
canKeepBoth =
|
||||
session?.mode === 'edit' &&
|
||||
!!chunk &&
|
||||
keepBothChunk(merge.a.state.doc, merge.b.state.doc, chunk) !== null;
|
||||
};
|
||||
const reveal = (index: number) => {
|
||||
if (!merge || !chunkCount) return;
|
||||
|
|
@ -215,6 +223,7 @@
|
|||
effects: EditorView.scrollIntoView(position, { y: 'center' })
|
||||
});
|
||||
}
|
||||
refresh();
|
||||
};
|
||||
const navigateOverview = (event: MouseEvent) => {
|
||||
const bounds =
|
||||
|
|
@ -253,6 +262,21 @@
|
|||
refresh();
|
||||
};
|
||||
|
||||
const keepBoth = () => {
|
||||
if (session?.mode !== 'edit' || loading || !merge) return;
|
||||
const chunk = merge.chunks[activeChunk];
|
||||
if (!chunk) return;
|
||||
const changes = keepBothChunk(merge.a.state.doc, merge.b.state.doc, chunk);
|
||||
if (!changes) return;
|
||||
merge.b.dispatch({
|
||||
changes,
|
||||
annotations: isolateHistory.of('full'),
|
||||
userEvent: 'input.merge'
|
||||
});
|
||||
refresh();
|
||||
merge.b.focus();
|
||||
};
|
||||
|
||||
const mountMerge = (node: HTMLElement) => {
|
||||
let disposed = false;
|
||||
let view: MergeView | null = null;
|
||||
|
|
@ -403,7 +427,7 @@
|
|||
>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="diff-editor flex h-full min-h-0 flex-col text-xs" on:keydown|capture={preventSave}>
|
||||
<header class="flex shrink-0 flex-wrap items-center gap-3 px-5 pt-4 pb-2">
|
||||
<header class="flex shrink-0 flex-wrap items-center gap-2 px-5 pt-4 pb-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Tooltip content={session.path ?? ''} allowHTML={false} className="min-w-0">
|
||||
<h2 class="truncate font-medium">{session.title || $i18n.t('Compare revision')}</h2>
|
||||
|
|
@ -415,7 +439,7 @@
|
|||
>
|
||||
{/if}
|
||||
{#if comparing}
|
||||
<div class="comparison-controls flex flex-wrap items-center gap-3">
|
||||
<div class="comparison-controls flex flex-wrap items-center gap-1.5">
|
||||
<div class="mr-1 flex items-center gap-1.5">
|
||||
<span class="whitespace-nowrap text-gray-500" role="status" aria-live="polite"
|
||||
>{loading
|
||||
|
|
@ -469,6 +493,13 @@
|
|||
title={$i18n.t('Copy change to proposal')}
|
||||
on:click={() => copy('to-proposal')}>{$i18n.t('Proposed revision')} →</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
disabled={loading || !canKeepBoth}
|
||||
title={$i18n.t('Keep both versions in the proposed revision, original first.')}
|
||||
on:click={keepBoth}>{$i18n.t('Keep both')}</button
|
||||
>
|
||||
{:else}
|
||||
<button type="button" class="secondary" disabled={loading} on:click={swap}
|
||||
>{$i18n.t('Swap sides')}</button
|
||||
|
|
@ -710,13 +741,14 @@
|
|||
.secondary,
|
||||
.primary {
|
||||
display: inline-flex;
|
||||
height: 1.75rem;
|
||||
height: 1.5rem;
|
||||
min-width: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
transition:
|
||||
|
|
|
|||
|
|
@ -261,21 +261,16 @@
|
|||
<div>
|
||||
<div class=" font-normal text-lg dark:text-gray-100">
|
||||
<a
|
||||
href="#"
|
||||
href={item?.type === 'file'
|
||||
? item?.url?.startsWith('http')
|
||||
? item.url
|
||||
: `${WEBUI_API_BASE_URL}/files/${item?.id ?? item?.tempId}/content`
|
||||
: item?.type === 'collection' && item?.id
|
||||
? `/workspace/knowledge/${item.id}`
|
||||
: safeLinkUrl(item?.url)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="hover:underline line-clamp-1"
|
||||
on:click|preventDefault={() => {
|
||||
if (item.type === 'file' || safeLinkUrl(item.url)) {
|
||||
let fileId = item?.id ?? item?.tempId;
|
||||
window.open(
|
||||
item.type === 'file'
|
||||
? item?.url?.startsWith('http')
|
||||
? item.url
|
||||
: `${WEBUI_API_BASE_URL}/files/${fileId}/content`
|
||||
: item.url,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item?.name ?? 'File'}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,22 @@ export function copyChunk(a: Text, b: Text, chunk: Chunk, direction: MergeDirect
|
|||
return { from, to: Math.min(to, destination.length), insert };
|
||||
}
|
||||
|
||||
export function keepBothChunk(a: Text, b: Text, chunk: Chunk): ChangeSpec | null {
|
||||
// A line-aligned chunk can include text on both sides even for a pure EOF insertion.
|
||||
if (
|
||||
!chunk.changes.some((change) => change.fromA < change.toA) ||
|
||||
!chunk.changes.some((change) => change.fromB < change.toB)
|
||||
)
|
||||
return null;
|
||||
const original = a.sliceString(chunk.fromA, Math.min(chunk.toA, a.length));
|
||||
const proposed = b.sliceString(chunk.fromB, Math.min(chunk.toB, b.length));
|
||||
return {
|
||||
from: chunk.fromB,
|
||||
to: Math.min(chunk.toB, b.length),
|
||||
insert: original + (original.endsWith('\n') ? '' : '\n') + proposed
|
||||
};
|
||||
}
|
||||
|
||||
export function selectedChunk(chunks: readonly Chunk[], side: 'a' | 'b', position: number) {
|
||||
return chunks.findIndex((chunk) => {
|
||||
const from = side === 'a' ? chunk.fromA : chunk.fromB;
|
||||
|
|
|
|||
|
|
@ -673,7 +673,7 @@
|
|||
}}
|
||||
>
|
||||
<LinkPreview.Trigger
|
||||
class="flex min-w-0 flex-1 items-center"
|
||||
class="flex min-w-0 flex-1 -my-1.5 py-1.5 items-center"
|
||||
href="/c/{id}"
|
||||
aria-current={id === $chatId ? 'page' : undefined}
|
||||
onclick={selectChatHandler}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
<script context="module">
|
||||
let closeActiveHoverPreview = null;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
|
||||
|
||||
|
|
@ -92,6 +96,17 @@
|
|||
openPreview = false;
|
||||
}
|
||||
|
||||
const closeHoverPreview = () => {
|
||||
if (openPreview) {
|
||||
openPreview = false;
|
||||
}
|
||||
};
|
||||
|
||||
$: if (openPreview && closeActiveHoverPreview !== closeHoverPreview) {
|
||||
closeActiveHoverPreview?.();
|
||||
closeActiveHoverPreview = closeHoverPreview;
|
||||
}
|
||||
|
||||
let name = '';
|
||||
|
||||
const formatUnreadCount = (count) =>
|
||||
|
|
@ -445,6 +460,10 @@
|
|||
onDestroy(() => {
|
||||
delete folderRegistry[folderId];
|
||||
|
||||
if (closeActiveHoverPreview === closeHoverPreview) {
|
||||
closeActiveHoverPreview = null;
|
||||
}
|
||||
|
||||
if (folderElement) {
|
||||
folderElement.removeEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('drop', onDrop);
|
||||
|
|
@ -800,7 +819,9 @@
|
|||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="translate-y-[0.5px] flex min-w-0 flex-1 items-center gap-1.5 pr-6 text-start">
|
||||
<div
|
||||
class="translate-y-[0.5px] flex min-w-0 flex-1 self-stretch items-center gap-1.5 pr-6 text-start"
|
||||
>
|
||||
{#if edit}
|
||||
<input
|
||||
id="folder-{folderId}-input"
|
||||
|
|
@ -836,7 +857,7 @@
|
|||
<LinkPreview.Trigger
|
||||
href={`/folders/${folderId}`}
|
||||
draggable="false"
|
||||
class="flex min-w-0 flex-1 items-center gap-1.5"
|
||||
class="flex min-w-0 flex-1 self-stretch -my-1 items-center gap-1.5"
|
||||
ondblclick={(e) => {
|
||||
if (!isWritable || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
||||
if (clickTimer) {
|
||||
|
|
|
|||
|
|
@ -1502,6 +1502,8 @@
|
|||
"JSON Spec": "المواصفات بصيغة JSON",
|
||||
"July": "يوليو",
|
||||
"June": "يونيو",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "الاحتفاظ بعنوان URL الحالي لخطاف الويب",
|
||||
"Keep in Sidebar": "إبقاء في الشريط الجانبي",
|
||||
"Keep tools listed even while your shell is closed.": "إبقاء الأدوات مدرجة حتى أثناء إغلاق الطرفية.",
|
||||
|
|
|
|||
|
|
@ -1502,6 +1502,8 @@
|
|||
"JSON Spec": "المواصفات بصيغة JSON",
|
||||
"July": "يوليو",
|
||||
"June": "يونيو",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "الاحتفاظ بعنوان URL الحالي لخطاف الويب",
|
||||
"Keep in Sidebar": "إبقاء في الشريط الجانبي",
|
||||
"Keep tools listed even while your shell is closed.": "إبقاء الأدوات مدرجة حتى أثناء إغلاق الطرفية.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON Spesifikasiyası",
|
||||
"July": "İyul",
|
||||
"June": "İyun",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Cari Webhook URL-ini saxla",
|
||||
"Keep in Sidebar": "Yan paneldə saxla",
|
||||
"Keep tools listed even while your shell is closed.": "Əmr qabığı bağlı olduqda belə alətləri siyahıda saxlayın.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON спецификация",
|
||||
"July": "Юли",
|
||||
"June": "Юни",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Запазване на текущия URL адрес на уебкуката",
|
||||
"Keep in Sidebar": "Запазване в страничната лента",
|
||||
"Keep tools listed even while your shell is closed.": "Инструментите остават в списъка, дори когато командната обвивка е затворена.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON স্পেসিফিকেশন",
|
||||
"July": "জুলাই",
|
||||
"June": "জুন",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "বর্তমান ওয়েবহুক URL রাখুন",
|
||||
"Keep in Sidebar": "সাইডবারে রাখুন",
|
||||
"Keep tools listed even while your shell is closed.": "শেল বন্ধ থাকলেও টুলগুলো তালিকায় রাখুন।",
|
||||
|
|
|
|||
|
|
@ -1437,6 +1437,8 @@
|
|||
"JSON Spec": "JSON ཚད་གཞི།",
|
||||
"July": "ཟླ་བ་བདུན་པ།",
|
||||
"June": "ཟླ་བ་དྲུག་པ།",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "ད་ལྟའི་ webhook URL ཉར་བ།",
|
||||
"Keep in Sidebar": "གཞོགས་སྡེར་ཉར་བ།",
|
||||
"Keep tools listed even while your shell is closed.": "ཁྱེད་ཀྱི་བཀའ་བརྡའི་འགྲེལ་བཤད་ཆས་ཁ་བརྒྱབ་ཡོད་སྐབས་ཀྱང་ལག་ཆ་ཐོ་ནང་འཇོག་པ།",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "JSON specifikacija",
|
||||
"July": "Juli",
|
||||
"June": "Juni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Zadrži trenutni URL webhooka",
|
||||
"Keep in Sidebar": "Zadrži na bočnoj traci",
|
||||
"Keep tools listed even while your shell is closed.": "Alati ostaju navedeni čak i dok je vaša komandna ljuska zatvorena.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "Especificacions del JSON",
|
||||
"July": "Juliol",
|
||||
"June": "Juny",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Mantenir l'URL actual del webhook",
|
||||
"Keep in Sidebar": "Mantenir a la barra lateral",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "Espesipikasyon sa JSON",
|
||||
"July": "Hulyo",
|
||||
"June": "Hunyo",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Hupti ang kasamtangang URL sa webhook",
|
||||
"Keep in Sidebar": "Hupti sa sidebar",
|
||||
"Keep tools listed even while your shell is closed.": "Ipadayon ang paglista sa mga himan bisan kon sirado ang imong tighubad sa mga sugo.",
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,8 @@
|
|||
"JSON Spec": "Specifikace JSON",
|
||||
"July": "Červenec",
|
||||
"June": "Červen",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Zachovat aktuální URL webhooku",
|
||||
"Keep in Sidebar": "Ponechat v postranním panelu",
|
||||
"Keep tools listed even while your shell is closed.": "Ponechat nástroje v seznamu, i když je váš příkazový interpret zavřený.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON spec",
|
||||
"July": "Juli",
|
||||
"June": "Juni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Behold den aktuelle webhook-URL",
|
||||
"Keep in Sidebar": "Behold i sidebaren",
|
||||
"Keep tools listed even while your shell is closed.": "Bevar værktøjer på listen, selv når din kommandofortolker er lukket.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON-Spezifikation",
|
||||
"July": "Juli",
|
||||
"June": "Juni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Aktuelle Webhook-URL beibehalten",
|
||||
"Keep in Sidebar": "In Seitenleiste anzeigen",
|
||||
"Keep tools listed even while your shell is closed.": "Behält Werkzeuge in der Liste, auch wenn Ihre Shell geschlossen ist.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "Such JSON Spec",
|
||||
"July": "July",
|
||||
"June": "June",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Keep such current webhook URL",
|
||||
"Keep in Sidebar": "Keep in Sidebar",
|
||||
"Keep tools listed even while your shell is closed.": "Keep tools listed even while such shell is closed. So handy.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "Προδιαγραφή JSON",
|
||||
"July": "Ιούλιος",
|
||||
"June": "Ιούνιος",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Διατήρηση τρέχοντος URL webhook",
|
||||
"Keep in Sidebar": "Κρατήστε στην πλευρική μπάρα",
|
||||
"Keep tools listed even while your shell is closed.": "Τα εργαλεία παραμένουν στη λίστα ακόμη και όταν το κέλυφός σας είναι κλειστό.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "",
|
||||
"July": "",
|
||||
"June": "",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "",
|
||||
"Keep in Sidebar": "",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "",
|
||||
"July": "",
|
||||
"June": "",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "",
|
||||
"Keep in Sidebar": "",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "Especif. JSON",
|
||||
"July": "Julio",
|
||||
"June": "Junio",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Mantener la URL actual del webhook",
|
||||
"Keep in Sidebar": "Mantener en Barra Lateral",
|
||||
"Keep tools listed even while your shell is closed.": "Mantiene las herramientas en la lista incluso mientras tu intérprete de comandos está cerrado.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON spetsifikatsioon",
|
||||
"July": "Juuli",
|
||||
"June": "Juuni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Säilitage praegune webhook'i URL",
|
||||
"Keep in Sidebar": "Hoia külgribal",
|
||||
"Keep tools listed even while your shell is closed.": "Tööriistad jäävad loendisse ka siis, kui kest on suletud.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON espezifikazioa",
|
||||
"July": "Uztaila",
|
||||
"June": "Ekaina",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Mantendu uneko webhook URLa",
|
||||
"Keep in Sidebar": "Mantendu alboko barran",
|
||||
"Keep tools listed even while your shell is closed.": "Mantendu tresnak zerrendan, komando-interpretea itxita dagoenean ere.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "مشخصات JSON",
|
||||
"July": "ژوئیه",
|
||||
"June": "ژوئن",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "حفظ آدرس وب\u200cهوک فعلی",
|
||||
"Keep in Sidebar": "نگه داشتن در نوار کناری",
|
||||
"Keep tools listed even while your shell is closed.": "ابزارها حتی زمانی که شل شما بسته است در فهرست باقی می\u200cمانند.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON Spec",
|
||||
"July": "heinäkuu",
|
||||
"June": "kesäkuu",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Käytä nykyistä webhook-verkko-osoitetta",
|
||||
"Keep in Sidebar": "Pidä sivupalkissa",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1451,6 +1451,8 @@
|
|||
"JSON Spec": "JSON Spec",
|
||||
"July": "Hulyo",
|
||||
"June": "Hunyo",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Panatilihin ang kasalukuyang webhook URL",
|
||||
"Keep in Sidebar": "Panatilihin sa Sidebar",
|
||||
"Keep tools listed even while your shell is closed.": "Panatilihing nakalista ang mga tool kahit sarado ang iyong shell.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON-forskrift",
|
||||
"July": "juli",
|
||||
"June": "juni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Varðveit núverandi webhook-URL",
|
||||
"Keep in Sidebar": "Varðveit í síðustikuni",
|
||||
"Keep tools listed even while your shell is closed.": "Lat amboð vera á listanum, eisini meðan Shell hjá tær er afturlatið.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "Spécification JSON",
|
||||
"July": "Juillet",
|
||||
"June": "Juin",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Conserver l'URL actuelle du webhook",
|
||||
"Keep in Sidebar": "Epingler dans la barre latérale",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "Spécification JSON",
|
||||
"July": "Juillet",
|
||||
"June": "Juin",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Conserver l'URL actuelle du webhook",
|
||||
"Keep in Sidebar": "Epingler dans la barre latérale",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "Especificación JSON",
|
||||
"July": "Xullo",
|
||||
"June": "Xuño",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Manter a URL actual do webhook",
|
||||
"Keep in Sidebar": "Manter na barra lateral",
|
||||
"Keep tools listed even while your shell is closed.": "Manter as ferramentas listadas mesmo cando o seu intérprete de ordes estea pechado.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "מפרט JSON",
|
||||
"July": "יולי",
|
||||
"June": "יוני",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "השאר את כתובת ה-URL הנוכחית של ה-Webhook",
|
||||
"Keep in Sidebar": "הצג בסרגל הצד",
|
||||
"Keep tools listed even while your shell is closed.": "השארת הכלים ברשימה גם כשהמעטפת שלך סגורה.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON विनिर्देश",
|
||||
"July": "जुलाई",
|
||||
"June": "जून",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "वर्तमान वेबहुक URL रखें",
|
||||
"Keep in Sidebar": "साइडबार में रखें",
|
||||
"Keep tools listed even while your shell is closed.": "आपका शेल बंद होने पर भी टूल सूची में रखें.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "JSON specifikacija",
|
||||
"July": "Srpanj",
|
||||
"June": "Lipanj",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Zadrži trenutačni URL webhooka",
|
||||
"Keep in Sidebar": "Zadrži na bočnoj traci",
|
||||
"Keep tools listed even while your shell is closed.": "Zadrži alate na popisu i dok je vaša ljuska zatvorena.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON-specifikáció",
|
||||
"July": "Július",
|
||||
"June": "Június",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Jelenlegi webhook URL megtartása",
|
||||
"Keep in Sidebar": "Megtartás az oldalsávon",
|
||||
"Keep tools listed even while your shell is closed.": "Az eszközök akkor is a listában maradnak, ha a shell be van zárva.",
|
||||
|
|
|
|||
|
|
@ -1449,6 +1449,8 @@
|
|||
"JSON Spec": "Spesifikasi JSON",
|
||||
"July": "Juli",
|
||||
"June": "Juni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Pertahankan URL webhook saat ini",
|
||||
"Keep in Sidebar": "Pertahankan di Bilah Sisi",
|
||||
"Keep tools listed even while your shell is closed.": "Tetap cantumkan alat meskipun shell Anda ditutup.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "Sonraíocht JSON",
|
||||
"July": "Iúil",
|
||||
"June": "Meitheamh",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Coinnigh URL an chrúca gréasáin reatha",
|
||||
"Keep in Sidebar": "Coinnigh sa Bharra Taobh",
|
||||
"Keep tools listed even while your shell is closed.": "Coinnigh na huirlisí ar an liosta fiú nuair atá do bhlaosc dúnta.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "Specifica JSON",
|
||||
"July": "Luglio",
|
||||
"June": "Giugno",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Mantieni l'URL del webhook attuale",
|
||||
"Keep in Sidebar": "Mantieni nella barra laterale",
|
||||
"Keep tools listed even while your shell is closed.": "Mantieni gli strumenti elencati anche quando la shell è chiusa.",
|
||||
|
|
|
|||
|
|
@ -1449,6 +1449,8 @@
|
|||
"JSON Spec": "JSON仕様",
|
||||
"July": "7月",
|
||||
"June": "6月",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "現在のウェブフックURLを保持",
|
||||
"Keep in Sidebar": "サイドバーに残す",
|
||||
"Keep tools listed even while your shell is closed.": "シェルが閉じている間もツールを一覧に残します。",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON-ის სპეციფიკაცია",
|
||||
"July": "ივლისი",
|
||||
"June": "ივნისი",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "მიმდინარე ვებჰუკის URL-ის შენარჩუნება",
|
||||
"Keep in Sidebar": "დატოვება გვერდით პანელზე",
|
||||
"Keep tools listed even while your shell is closed.": "ხელსაწყოებს სიაში ტოვებს მაშინაც, როცა თქვენი გარსი დახურულია.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "Ilugan s JSON",
|
||||
"July": "Yulyu",
|
||||
"June": "Yunyu",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Eǧǧ tansa URL tamirant n webhook",
|
||||
"Keep in Sidebar": "Senṭeḍ-it deg ufeggag n yidis",
|
||||
"Keep tools listed even while your shell is closed.": "Eǧǧ ifecka deg tebdart ula ma yella shell-ik·im yemdel.",
|
||||
|
|
|
|||
|
|
@ -1438,6 +1438,8 @@
|
|||
"JSON Spec": "JSON 스펙",
|
||||
"July": "7월",
|
||||
"June": "6월",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "현재 웹훅 URL 유지",
|
||||
"Keep in Sidebar": "사이드바에 유지",
|
||||
"Keep tools listed even while your shell is closed.": "셸이 닫혀 있는 동안에도 도구를 목록에 유지합니다.",
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,8 @@
|
|||
"JSON Spec": "JSON specifikacija",
|
||||
"July": "liepa",
|
||||
"June": "birželis",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Paliekamas dabartinis webhook URL adresas",
|
||||
"Keep in Sidebar": "Laikyti šoninėje juostoje",
|
||||
"Keep tools listed even while your shell is closed.": "Rodyti įrankius sąraše net tada, kai jūsų apvalkalas uždarytas.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "JSON specifikācija",
|
||||
"July": "Jūlijs",
|
||||
"June": "Jūnijs",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Paturēt pašreizējo webhook URL",
|
||||
"Keep in Sidebar": "Saglabāt sānjoslā",
|
||||
"Keep tools listed even while your shell is closed.": "Rādīt rīkus sarakstā arī tad, kad jūsu čaula ir aizvērta.",
|
||||
|
|
|
|||
|
|
@ -1449,6 +1449,8 @@
|
|||
"JSON Spec": "Spesifikasi JSON",
|
||||
"July": "Julai",
|
||||
"June": "Jun",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Kekalkan URL webhook semasa",
|
||||
"Keep in Sidebar": "Simpan dalam Bar Sisi",
|
||||
"Keep tools listed even while your shell is closed.": "Kekalkan alat dalam senarai walaupun semasa shell anda ditutup.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON-spesifikasjon",
|
||||
"July": "juli",
|
||||
"June": "juni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Behold nåværende webhook-URL",
|
||||
"Keep in Sidebar": "Vis i sidepanelet",
|
||||
"Keep tools listed even while your shell is closed.": "Behold verktøyene i listen selv når skallet ditt er lukket.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON-specificatie",
|
||||
"July": "juli",
|
||||
"June": "juni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Huidige webhook-URL behouden",
|
||||
"Keep in Sidebar": "In zijbalk houden",
|
||||
"Keep tools listed even while your shell is closed.": "Houd tools in de lijst, ook als je shell gesloten is.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON ਨਿਰਧਾਰਨ",
|
||||
"July": "ਜੁਲਾਈ",
|
||||
"June": "ਜੂਨ",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "ਮੌਜੂਦਾ ਵੈੱਬਹੁੱਕ URL ਰੱਖੋ",
|
||||
"Keep in Sidebar": "ਸਾਈਡਬਾਰ ਵਿੱਚ ਰੱਖੋ",
|
||||
"Keep tools listed even while your shell is closed.": "ਤੁਹਾਡਾ ਸ਼ੈੱਲ ਬੰਦ ਹੋਣ 'ਤੇ ਵੀ ਟੂਲ ਸੂਚੀ ਵਿੱਚ ਰੱਖੋ।",
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,8 @@
|
|||
"JSON Spec": "Specyfikacja JSON",
|
||||
"July": "Lipiec",
|
||||
"June": "Czerwiec",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Zachowaj obecny adres URL webhooka",
|
||||
"Keep in Sidebar": "Zachowaj w pasku bocznym",
|
||||
"Keep tools listed even while your shell is closed.": "Pokazuj narzędzia na liście nawet wtedy, gdy powłoka jest zamknięta.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "Especificação JSON",
|
||||
"July": "Julho",
|
||||
"June": "Junho",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Manter a URL atual do webhook",
|
||||
"Keep in Sidebar": "Manter na barra lateral",
|
||||
"Keep tools listed even while your shell is closed.": "Mantenha as ferramentas listadas mesmo com o shell fechado.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "Especificação JSON",
|
||||
"July": "Julho",
|
||||
"June": "Junho",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Manter o URL do webhook atual",
|
||||
"Keep in Sidebar": "Manter na Barra Lateral",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "Specificație JSON",
|
||||
"July": "Iulie",
|
||||
"June": "Iunie",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Păstrează URL-ul curent al webhook-ului",
|
||||
"Keep in Sidebar": "Păstrează în bara laterală",
|
||||
"Keep tools listed even while your shell is closed.": "Păstrează instrumentele listate chiar și când shell-ul dumneavoastră este închis.",
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,8 @@
|
|||
"JSON Spec": "JSON-спецификация",
|
||||
"July": "Июль",
|
||||
"June": "Июнь",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Сохранить текущий URL вебхука",
|
||||
"Keep in Sidebar": "Оставить на боковой панели",
|
||||
"Keep tools listed even while your shell is closed.": "Оставлять инструменты в списке, даже когда командная строка закрыта.",
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,8 @@
|
|||
"JSON Spec": "Špecifikácia JSON",
|
||||
"July": "Júl",
|
||||
"June": "Jún",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Ponechať aktuálnu URL adresu webhooku",
|
||||
"Keep in Sidebar": "Ponechať v bočnom paneli",
|
||||
"Keep tools listed even while your shell is closed.": "Ponechať nástroje v zozname, aj keď je váš shell zatvorený.",
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,8 @@
|
|||
"JSON Spec": "JSON Spec",
|
||||
"July": "julij",
|
||||
"June": "junij",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Ohrani trenutni URL spletnega klica",
|
||||
"Keep in Sidebar": "Obdrži v stranski vrstici",
|
||||
"Keep tools listed even while your shell is closed.": "Orodja ohrani na seznamu tudi, ko je vaša lupina zaprta.",
|
||||
|
|
|
|||
|
|
@ -1463,6 +1463,8 @@
|
|||
"JSON Spec": "JSON спецификација",
|
||||
"July": "Јул",
|
||||
"June": "Јун",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Задржите тренутни URL вебхука",
|
||||
"Keep in Sidebar": "Задржи на бочној траци",
|
||||
"Keep tools listed even while your shell is closed.": "Задржава алате на листи чак и док је ваша љуска затворена.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON-specifikation",
|
||||
"July": "juli",
|
||||
"June": "juni",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Behåll aktuell webhook-URL",
|
||||
"Keep in Sidebar": "Behåll i sidofältet",
|
||||
"Keep tools listed even while your shell is closed.": "Behåll verktygen i listan även när ditt skal är stängt.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON விவரக்குறிப்பு",
|
||||
"July": "ஜூலை",
|
||||
"June": "ஜூன்",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "தற்போதைய வெப்ஹூக் URL ஐ வைத்திருக்கவும்",
|
||||
"Keep in Sidebar": "பக்கப்பட்டியில் வைக்கவும்",
|
||||
"Keep tools listed even while your shell is closed.": "உங்கள் ஷெல் மூடப்பட்டிருக்கும்போதும் கருவிகளைப் பட்டியலில் வைத்திருக்கவும்.",
|
||||
|
|
|
|||
|
|
@ -1437,6 +1437,8 @@
|
|||
"JSON Spec": "สเปก JSON",
|
||||
"July": "กรกฎาคม",
|
||||
"June": "มิถุนายน",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "คง URL ของ Webhook ปัจจุบันไว้",
|
||||
"Keep in Sidebar": "ปักหมุดไว้ที่แถบด้านข้าง",
|
||||
"Keep tools listed even while your shell is closed.": "แสดงเครื่องมือในรายการไว้แม้ขณะที่ Shell ของคุณปิดอยู่",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON Spesifikasiýasy",
|
||||
"July": "Iýul",
|
||||
"June": "Iýun",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Häzirki webhook URL salgysyny saklaň",
|
||||
"Keep in Sidebar": "Gapdal Panelde Sakla",
|
||||
"Keep tools listed even while your shell is closed.": "Shell-iňiz ýapyk wagty hem gurallary sanawda saklaň.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON Belirtimi",
|
||||
"July": "Temmuz",
|
||||
"June": "Haziran",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Mevcut webhook URL'sini koru",
|
||||
"Keep in Sidebar": "Kenar Çubuğunda Tut",
|
||||
"Keep tools listed even while your shell is closed.": "Kabuğunuz kapalıyken bile araçları listede tut.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON ئۆلچىمى",
|
||||
"July": "ئىيۇل",
|
||||
"June": "ئىيۇن",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "نۆۋەتتىكى Webhook URL نى ساقلاپ قېلىڭ",
|
||||
"Keep in Sidebar": "يانتاختىدا ساقلا",
|
||||
"Keep tools listed even while your shell is closed.": "shell يېپىلغان ۋاقىتتىمۇ قوراللارنى تىزىملىكتە ساقلاڭ.",
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,8 @@
|
|||
"JSON Spec": "Специфікація JSON",
|
||||
"July": "Липень",
|
||||
"June": "Червень",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Зберегти поточну URL-адресу вебхука",
|
||||
"Keep in Sidebar": "Залишити на бічній панелі",
|
||||
"Keep tools listed even while your shell is closed.": "Залишати інструменти в списку, навіть коли ваш командний рядок закрито.",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "JSON تصریح",
|
||||
"July": "جولائی",
|
||||
"June": "جون",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "موجودہ ویب ہک URL برقرار رکھیں",
|
||||
"Keep in Sidebar": "سائیڈ بار میں رکھیں",
|
||||
"Keep tools listed even while your shell is closed.": "آپ کا شیل بند ہونے پر بھی ٹولز فہرست میں رکھیں۔",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "",
|
||||
"July": "июл",
|
||||
"June": "июн",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "",
|
||||
"Keep in Sidebar": "",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@
|
|||
"JSON Spec": "",
|
||||
"July": "iyul",
|
||||
"June": "iyun",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "",
|
||||
"Keep in Sidebar": "",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1437,6 +1437,8 @@
|
|||
"JSON Spec": "Đặc tả JSON",
|
||||
"July": "Tháng 7",
|
||||
"June": "Tháng 6",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "Giữ URL webhook hiện tại",
|
||||
"Keep in Sidebar": "Giữ trên thanh bên",
|
||||
"Keep tools listed even while your shell is closed.": "Vẫn liệt kê các tool ngay cả khi shell của bạn đã đóng.",
|
||||
|
|
|
|||
|
|
@ -1447,6 +1447,8 @@
|
|||
"JSON Spec": "以 JSON 格式定义的接口规范",
|
||||
"July": "七月",
|
||||
"June": "六月",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "保留当前 webhook URL",
|
||||
"Keep in Sidebar": "保留在侧边栏",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
|
|
@ -1437,6 +1437,8 @@
|
|||
"JSON Spec": "以 JSON 格式定義的介面規範",
|
||||
"July": "7 月",
|
||||
"June": "6 月",
|
||||
"Keep both": "",
|
||||
"Keep both versions in the proposed revision, original first.": "",
|
||||
"Keep current webhook URL": "保留目前的 Webhook 網址",
|
||||
"Keep in Sidebar": "保留在側邊欄",
|
||||
"Keep tools listed even while your shell is closed.": "",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue