diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c8b353..4933f9b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ ## [Unreleased] +## [v0.51.262] — 2026-06-04 — Release ID (stage-r12 — zh localization + Docker reveal-path + recall-prefill role fix) + +### Fixed +- **WebUI no longer sends adjacent `user` roles to strict chat templates when recall prefill is configured.** A configured recall prefill could end with `role:user`, and both in-process streaming and Gateway chat then appended the live browser user turn — producing consecutive `user` messages that strict templates (Mistral/Gemma/Jinja) reject. A new `_normalize_prefill_messages_before_user_turn()` drops a terminal `user` prefill turn at that boundary (in both `api/streaming.py` and `api/gateway_chat.py`), preserving safe assistant prefill context. (#3432, @franksong2702) +- **"Reveal in file manager" now works for Docker deployments** where the workspace is a host directory mounted into the container — container paths (e.g. `/workspace`) are translated back to the host path so the reveal resolves on the host. (#2558, @rodboev) + +### Changed +- **Completed the Chinese (Simplified) localization** — translated the remaining untranslated `zh` strings (MCP server controls, tool-list pagination/summary) to Chinese, preserving all interpolation placeholders. (@Lyr-GW) + ## [v0.51.261] — 2026-06-04 — Release IC (stage-r11 — live Todos panel via a todo_state SSE contract) ### Fixed diff --git a/api/gateway_chat.py b/api/gateway_chat.py index 89f850e2..7b5a2497 100644 --- a/api/gateway_chat.py +++ b/api/gateway_chat.py @@ -249,6 +249,7 @@ def _run_gateway_chat_streaming( from api.streaming import ( _load_webui_prefill_context, _prefill_messages_with_webui_context, + _normalize_prefill_messages_before_user_turn, _public_prefill_context_status, _webui_ephemeral_system_prompt, ) @@ -269,9 +270,11 @@ def _run_gateway_chat_streaming( }, config_data=cfg, ) + prefill_messages = _prefill_messages_with_webui_context(prefill_context, cfg) + prefill_messages = _normalize_prefill_messages_before_user_turn(prefill_messages) prefill_messages = [ {"role": "system", "content": _gateway_system_prompt}, - *_prefill_messages_with_webui_context(prefill_context, cfg), + *prefill_messages, ] put_gateway_event("context_status", { "session_id": session_id, diff --git a/api/routes.py b/api/routes.py index fc219198..43f69eb6 100644 --- a/api/routes.py +++ b/api/routes.py @@ -12262,14 +12262,28 @@ def _handle_file_reveal(handler, body): # what was missing). return bad(handler, f"File not found: {target}", 404) + target_str = str(target) + + # Optional Docker host/container path translation (mirrors _handle_file_open_vscode). + from api.config import get_config as _get_cfg # noqa: PLC0415 + vscode_cfg = _get_cfg().get("vscode", {}) + if not isinstance(vscode_cfg, dict): + vscode_cfg = {} + container_prefix = vscode_cfg.get("container_path_prefix", "") + host_prefix = vscode_cfg.get("host_path_prefix", "") + if container_prefix and host_prefix: + _norm = container_prefix.rstrip('/') + '/' + if target_str.startswith(_norm) or target_str == container_prefix.rstrip('/'): + target_str = host_prefix + target_str[len(container_prefix):] + system = platform.system() if system == "Darwin": - subprocess.Popen(["open", "-R", str(target)]) + subprocess.Popen(["open", "-R", target_str]) elif system == "Windows": - subprocess.Popen(["explorer.exe", "/select," + str(target)]) + subprocess.Popen(["explorer.exe", "/select," + target_str]) else: # Linux / other — open parent directory - subprocess.Popen(["xdg-open", str(target.parent)]) + subprocess.Popen(["xdg-open", str(Path(target_str).parent)]) return j(handler, {"ok": True, "path": body["path"]}) except (ValueError, PermissionError, OSError) as e: @@ -12342,8 +12356,10 @@ def _handle_file_open_vscode(handler, body): vscode_cfg = {} container_prefix = vscode_cfg.get("container_path_prefix", "") host_prefix = vscode_cfg.get("host_path_prefix", "") - if container_prefix and host_prefix and target_str.startswith(container_prefix): - target_str = host_prefix + target_str[len(container_prefix):] + if container_prefix and host_prefix: + _norm = container_prefix.rstrip('/') + '/' + if target_str.startswith(_norm) or target_str == container_prefix.rstrip('/'): + target_str = host_prefix + target_str[len(container_prefix):] cmd = vscode_cfg.get("command", "code") # Resolve the command to an absolute path so subprocess.Popen finds it diff --git a/api/streaming.py b/api/streaming.py index 653f4d26..4a987bca 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -657,6 +657,33 @@ def _prefill_messages_with_webui_context(prefill_context: dict, config_data: Opt return list(prefill_context.get("messages") or []) +def _normalize_prefill_messages_before_user_turn(prefill_messages: list[dict]) -> list[dict]: + """Ensure WebUI prefill does not end with user role before an appended turn. + + Some upstream prefill sources can end with `role: user` (for example, + session context or recall snippets). WebUI always appends the current user + turn after prefill in the streaming path, so a terminal user role creates an + adjacent user/user sequence that strict chat templates (Gemma, Mistral/Jinja) + reject. + + To keep behavior scoped, only consecutive terminal user messages are removed + just before that boundary; earlier roles remain untouched. + """ + sanitized = list(prefill_messages or []) + n_dropped = 0 + while sanitized: + last_message = sanitized[-1] + if not isinstance(last_message, dict): + break + if str(last_message.get("role") or "").strip().lower() != "user": + break + sanitized.pop() + n_dropped += 1 + if n_dropped: + logger.debug("Dropped %d trailing user message(s) from prefill", n_dropped) + return sanitized + + def _has_new_assistant_reply(all_messages: list, prev_count: int) -> bool: """Return True if *new* messages (beyond ``prev_count``) contain an assistant message with non-empty content. @@ -5167,6 +5194,7 @@ def _run_agent_streaming( _cfg = _get_config() _prefill_context = _load_webui_prefill_context(_cfg) _prefill_messages = _prefill_messages_with_webui_context(_prefill_context, _cfg) + _prefill_messages = _normalize_prefill_messages_before_user_turn(_prefill_messages) put('context_status', { 'session_id': session_id, 'prefill': _public_prefill_context_status(_prefill_context), diff --git a/static/i18n.js b/static/i18n.js index b840b4d2..5e7d52cc 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -7874,11 +7874,11 @@ const LOCALES = { mcp_tool_count: '{0} 个工具', mcp_enabled_yes: '已启用', mcp_enabled_no: '已禁用', - mcp_enable_server: 'Enable this MCP server', - mcp_disable_server: 'Disable this MCP server', - mcp_enabled_toast: (name) => `MCP server "${name}" enabled.`, - mcp_disabled_toast: (name) => `MCP server "${name}" disabled.`, - mcp_toggle_failed: 'Failed to update MCP server.', + mcp_enable_server: '启用此 MCP 服务器', + mcp_disable_server: '禁用此 MCP 服务器', + mcp_enabled_toast: (name) => `MCP 服务器「${name}」已启用。`, + mcp_disabled_toast: (name) => `MCP 服务器「${name}」已禁用。`, + mcp_toggle_failed: '更新 MCP 服务器失败。', mcp_tools_title: 'MCP 工具', mcp_tools_desc: '搜索活跃 MCP 服务器中的已知工具。', mcp_tools_search_placeholder: '按名称、服务器或描述搜索工具…', @@ -7887,20 +7887,20 @@ const LOCALES = { mcp_tools_load_failed: '加载 MCP 工具失败。', mcp_tools_schema_empty: '无参数。', mcp_tools_runtime_note: '工具清单仅使用已知的活跃 MCP 运行时数据;WebUI 不会启动或探测服务器。', - mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`, - mcp_tools_summary_none: 'No MCP tools to show.', - mcp_tools_summary_matching: (query) => ` matching “${query}”`, - mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`, - mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`, - mcp_tools_page_size_prefix: 'Show', - mcp_tools_page_size_suffix: 'per page', - mcp_tools_per_page_aria: 'MCP tools per page', - mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`, - mcp_tools_pagination_label: 'MCP tools pagination', - mcp_tools_previous_page: '‹ Prev', - mcp_tools_previous_page_aria: 'Previous MCP tools page', - mcp_tools_next_page: 'Next ›', - mcp_tools_next_page_aria: 'Next MCP tools page', + mcp_tools_summary_no_matches: (query,total) => `没有匹配「${query}」的 MCP 工具(共 ${total} 个)。`, + mcp_tools_summary_none: '没有 MCP 工具可显示。', + mcp_tools_summary_matching: (query) => ` 匹配「${query}」`, + mcp_tools_summary_total_note: (total) => `(共 ${total} 个)`, + mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `显示第 ${start}-${end} 个,共 ${filtered} 个 MCP 工具${searchNote}${totalNote}。第 ${page}/${pages} 页。`, + mcp_tools_page_size_prefix: '每页显示', + mcp_tools_page_size_suffix: '个', + mcp_tools_per_page_aria: '每页 MCP 工具数', + mcp_tools_inactive_configured_servers: (servers) => `已配置但在此 WebUI 运行环境中未激活:${servers}。`, + mcp_tools_pagination_label: 'MCP 工具分页', + mcp_tools_previous_page: '‹ 上一页', + mcp_tools_previous_page_aria: '上一页 MCP 工具', + mcp_tools_next_page: '下一页 ›', + mcp_tools_next_page_aria: '下一页 MCP 工具', thinking: '思考过程', expand_all: '全部展开', collapse_all: '全部折叠', @@ -7966,7 +7966,7 @@ const LOCALES = { session_toolsets_desc: '限制此会话可用工具(留空 = 使用全局配置)', session_toolsets_global: '全局(默认)', session_toolsets_custom: '自定义', - session_toolsets_placeholder: 'tool1, tool2, \u2026', // TODO: translate + session_toolsets_placeholder: 'tool1, tool2, …', // 保持英文——这是占位符示例 session_toolsets_apply: '应用', session_toolsets_clear: '清除(使用全局)', session_toolsets_applied: '工具集已更新', @@ -8138,7 +8138,7 @@ const LOCALES = { copy_file_path: '\u590d\u5236\u6587\u4ef6\u8def\u5f84', open_in_vscode: '在VS Code中打开', open_in_vscode_failed: '在VS Code中打开失败:', - download_folder: 'Download Folder', // TODO: translate + download_folder: '下载文件夹', path_copied: '\u6587\u4ef6\u8def\u5f84\u5df2\u590d\u5236\u5230\u526a\u8d34\u677f', path_copy_failed: '\u590d\u5236\u8def\u5f84\u5931\u8d25\uff1a', session_rename: '\u91cd\u547d\u540d\u5bf9\u8bdd', @@ -8999,7 +8999,7 @@ const LOCALES = { settings_label_tts_engine: 'TTS 引擎', settings_desc_tts_engine: '选择语音引擎。Edge TTS 通过服务器使用微软神经语音。', checkpoint_date: '日期', - checkpoint_diff_files_changed: (n) => `${n} file${n === 1 ? '' : 's'} changed`, // TODO: translate + checkpoint_diff_files_changed: (n) => `${n} 个文件已更改`, checkpoint_diff_no_changes: '此检查点与当前工作区之间无差异。', checkpoint_diff_title: '检查点变更', checkpoint_empty: '此工作区未找到检查点。', diff --git a/static/panels.js b/static/panels.js index cf09071e..f1999931 100644 --- a/static/panels.js +++ b/static/panels.js @@ -6282,7 +6282,10 @@ async function loadSettingsPanel(){ } } langSel.value=resolvedLanguage; - langSel.addEventListener('change',_schedulePreferencesAutosave,{once:false}); + langSel.addEventListener('change',function(){ + if(typeof setLocale==='function'){setLocale(this.value);if(typeof applyLocaleToDOM==='function')applyLocaleToDOM();} + _schedulePreferencesAutosave(); + },{once:false}); } const showUsageCb=$('settingsShowTokenUsage'); if(showUsageCb){showUsageCb.checked=!!settings.show_token_usage;showUsageCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});} diff --git a/tests/test_2558_reveal_path_translation.py b/tests/test_2558_reveal_path_translation.py new file mode 100644 index 00000000..41e3ce37 --- /dev/null +++ b/tests/test_2558_reveal_path_translation.py @@ -0,0 +1,43 @@ +"""Tests for issue #2558 -- container path translation for Reveal in File Manager. + +Pins that _handle_file_reveal applies the same container_path_prefix / +host_path_prefix substitution used by _handle_file_open_vscode. +""" +from __future__ import annotations + +import pathlib +import re + +ROOT = pathlib.Path(__file__).resolve().parent.parent +ROUTES = ROOT / "api" / "routes.py" + + +class TestRevealPathTranslation: + def test_handler_supports_path_prefix_mapping(self): + """_handle_file_reveal must contain container_path_prefix / host_path_prefix + so Docker users get the same path translation as _handle_file_open_vscode.""" + src = ROUTES.read_text(encoding="utf-8") + m = re.search( + r"def _handle_file_reveal\(handler, body\):.*?(?=\ndef )", + src, + re.DOTALL, + ) + assert m, "_handle_file_reveal not found in api/routes.py" + body = m.group(0) + assert "container_path_prefix" in body + assert "host_path_prefix" in body + + def test_handler_uses_target_str_in_subprocess(self): + """The subprocess dispatch must use the translated string, not str(target).""" + src = ROUTES.read_text(encoding="utf-8") + m = re.search( + r"def _handle_file_reveal\(handler, body\):.*?(?=\ndef )", + src, + re.DOTALL, + ) + assert m + body = m.group(0) + # After translation the variable must be named target_str (not str(target)) + assert "target_str" in body + # Ensure the translation assignment is present + assert "target_str = host_prefix + target_str[len(container_prefix):]" in body diff --git a/tests/test_issue3539_language_dropdown_all_locales.py b/tests/test_issue3539_language_dropdown_all_locales.py new file mode 100644 index 00000000..2a1561df --- /dev/null +++ b/tests/test_issue3539_language_dropdown_all_locales.py @@ -0,0 +1,45 @@ +"""Regression: the Settings language dropdown must list ALL locales in LOCALES. + +#3539 (zh localization) briefly added an `allowed=['en','zh']` filter to the +language `