feat: 优化话题描述生成逻辑,增加空描述回退机制;更新话题创建命令以自动聚焦新话题

This commit is contained in:
oudecheng 2026-06-10 11:26:35 +08:00
parent dad0f57b9b
commit a4cdb31ba0
3 changed files with 39 additions and 9 deletions

View File

@ -4,15 +4,17 @@ pub async fn generate_topic_description(
provider: &dyn LLMProvider, provider: &dyn LLMProvider,
first_user_message: &str, first_user_message: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let prompt = format!( let system_prompt = "你是一个话题摘要助手。请根据用户的第一句话用简短的词语不超过15字描述这个对话的主题或意图。只输出描述内容不要任何解释、标点或前缀。";
"请根据用户的第一句话用简短的词语不超过15字描述这个对话的主题或意图。只输出描述内容不要其他解释。\n\n用户消息:{}",
first_user_message let user_prompt = format!("用户消息:{}", first_user_message);
);
let request = ChatCompletionRequest { let request = ChatCompletionRequest {
messages: vec![Message::user(prompt)], messages: vec![
temperature: Some(0.3), Message::system(system_prompt),
max_tokens: Some(50), Message::user(user_prompt),
],
temperature: Some(0.0),
max_tokens: Some(1024), // 给 reasoning 模型留足思考空间
tools: None, tools: None,
}; };
@ -20,6 +22,24 @@ pub async fn generate_topic_description(
let description = response.content.trim().to_string(); let description = response.content.trim().to_string();
if description.is_empty() { if description.is_empty() {
// 回退reasoning 模型有时把所有 token 都消耗在推理上content 为空
// 此时尝试从 reasoning_content 中提取最后一行有意义的内容作为描述
if let Some(ref reasoning) = response.reasoning_content {
let fallback: String = reasoning
.lines()
.rev()
.find(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && trimmed.len() <= 50
})
.unwrap_or("")
.trim()
.to_string();
if !fallback.is_empty() {
let truncated: String = fallback.chars().take(50).collect();
return Ok(truncated);
}
}
return Err("LLM returned empty description".into()); return Err("LLM returned empty description".into());
} }

View File

@ -202,7 +202,7 @@ function App() {
let cmd: Command let cmd: Command
switch (command) { switch (command) {
case 'new': case 'new':
cmd = { type: 'create_session', title: args.join(' ') || undefined } cmd = createTopic(args.join(' ') || undefined)
break break
case 'list': case 'list':
cmd = { type: 'list_sessions', include_archived: args[0] === 'all' } cmd = { type: 'list_sessions', include_archived: args[0] === 'all' }

View File

@ -158,6 +158,7 @@ export function useChat(): UseChatReturn {
const schedulerViewRef = useRef<SchedulerJobView | null>(null) const schedulerViewRef = useRef<SchedulerJobView | null>(null)
const topicsRef = useRef<Topic[]>([]) const topicsRef = useRef<Topic[]>([])
const selectedTopicRef = useRef<string | null>(null) const selectedTopicRef = useRef<string | null>(null)
const pendingNewTopicRef = useRef(false)
const isConnected = useMemo(() => connectionId !== null, [connectionId]) const isConnected = useMemo(() => connectionId !== null, [connectionId])
const selectedSession = useMemo( const selectedSession = useMemo(
@ -402,7 +403,15 @@ export function useChat(): UseChatReturn {
})) }))
setTopics(newTopics) setTopics(newTopics)
// 默认选中第一个 Topic如果没有选中 // 新建话题后自动聚焦到新话题(列表按 last_active_at DESC 排序,第一个即最新)
if (pendingNewTopicRef.current) {
pendingNewTopicRef.current = false
if (newTopics.length > 0) {
setSelectedTopic(newTopics[0].id)
setMessages([])
}
}
setIsLoading(false) setIsLoading(false)
break break
} }
@ -592,6 +601,7 @@ export function useChat(): UseChatReturn {
}, []) }, [])
const createTopic = useCallback((title?: string): Command => { const createTopic = useCallback((title?: string): Command => {
pendingNewTopicRef.current = true
return { return {
type: 'create_session', type: 'create_session',
title: title || `话题 ${new Date().toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`, title: title || `话题 ${new Date().toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`,