diff --git a/backend/api/server.py b/backend/api/server.py index 6a712cc..5c32ebe 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -1725,6 +1725,9 @@ def create_app(state: AgentState) -> FastAPI: ): continue title = session_metadata.get('title') + title_source = session_metadata.get('title_source') + if isinstance(title, str) and title_source != 'manual': + title = _clean_session_title(title) if not isinstance(title, str) or not title.strip(): title = None preview = '' @@ -3917,6 +3920,8 @@ def _read_session_title(directory: Path, session_id: str | None) -> str | None: return None title = metadata.get('title') if isinstance(title, str) and title.strip(): + if metadata.get('title_source') != 'manual': + return _clean_session_title(title) return title.strip() return None @@ -4044,8 +4049,30 @@ def _generate_session_title( def _clean_session_title(value: str) -> str | None: - title = ' '.join(value.strip().strip('"\'“”‘’`').split()) + raw = value.strip() + if not raw: + return None + raw = re.sub(r'(?is).*?', '', raw) + raw = re.sub(r'(?is).*?', '', raw) + if re.search(r'(?i)<\s*(analysis|think)\b', raw): + return None + lines = [line.strip() for line in raw.replace('\r', '\n').split('\n')] + title = next((line for line in reversed(lines) if line), '') + title = re.sub(r'^\s*(?:[-*#>\d.、\s]+)', '', title) + title = re.sub( + r'^(?:会话标题|标题|短标题|生成标题|session title)\s*[::]\s*', + '', + title, + flags=re.IGNORECASE, + ) + title = title.strip().strip('"\'“”‘’`*_ ') + title = re.sub(r'\*\*(.*?)\*\*', r'\1', title) + title = ' '.join(title.split()) title = title.rstrip('。.!!??') + if re.search(r'(?i)\b(generating|generate|thinking|reasoning)\b', title): + return None + if '<' in title or '>' in title: + return None if not title: return None return title[:24].rstrip() diff --git a/tests/test_gui_server.py b/tests/test_gui_server.py index aadb983..11f512f 100644 --- a/tests/test_gui_server.py +++ b/tests/test_gui_server.py @@ -22,6 +22,7 @@ from backend.api.server import ( AgentState, create_app, _bash_bg_manager, + _clean_session_title, _derive_initial_session_title, _ensure_session_title, _generate_session_title, @@ -860,6 +861,35 @@ class GuiServerTests(unittest.TestCase): self.assertIsNone(_read_session_title(directory, session_id)) + def test_invalid_llm_title_is_not_preserved_as_previous_title(self) -> None: + with tempfile.TemporaryDirectory() as d: + root = Path(d) + _, state = _build_client(root) + session_id = 'invalid-llm-title' + directory = state.account_paths('alice')['sessions'] + agent = state.agent_for('alice', session_id) + + _save_in_progress_session( + directory=directory, + agent=agent, + session_id=session_id, + prompt='帮我生成一批地图导航测试数据。', + ) + stored = load_agent_session(session_id, directory=directory) + save_agent_session( + replace( + stored, + session_metadata={ + **(stored.session_metadata or {}), + 'title': '**Generating short title', + 'title_source': 'llm', + }, + ), + directory=directory, + ) + + self.assertIsNone(_read_session_title(directory, session_id)) + def test_session_title_refines_first_message_title_after_run(self) -> None: with tempfile.TemporaryDirectory() as d: root = Path(d) @@ -960,6 +990,20 @@ class GuiServerTests(unittest.TestCase): self.assertEqual(config.base_url, 'http://model.mify.ai.srv/v1') client.complete.assert_called_once() + def test_clean_session_title_rejects_reasoning_leak(self) -> None: + self.assertIsNone(_clean_session_title('**Generating short title')) + self.assertIsNone( + _clean_session_title( + 'reasoning\nGenerating short Chinese title' + ) + ) + + def test_clean_session_title_strips_markdown_and_prefix(self) -> None: + self.assertEqual( + _clean_session_title('**标题:地图导航测试数据。**'), + '地图导航测试数据', + ) + def test_clear_runtime_state(self) -> None: with tempfile.TemporaryDirectory() as d: client, _ = _build_client(Path(d))