From 4c478414834f52fc752578369cac1438880b62f1 Mon Sep 17 00:00:00 2001 From: Huarch Date: Fri, 22 May 2026 14:20:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A0=87=E9=A2=98=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/chat.ts | 1 + src/routes/chatSession.ts | 38 +++++++++++++++++++++++++++----- tests/routes/chatSession.test.ts | 37 ++++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/routes/chat.ts b/src/routes/chat.ts index d3f0e51..25e153a 100644 --- a/src/routes/chat.ts +++ b/src/routes/chat.ts @@ -564,6 +564,7 @@ export const buildChatRouter = ( if (shouldGenerateTitle) { sessionTitle = await generateSessionTitle(runtime, { sessionId: binding.sessionId, + latestAssistantMessage: assistantText, latestUserMessage: parsed.data.message, fallbackTitle: existingSessionTitle, }); diff --git a/src/routes/chatSession.ts b/src/routes/chatSession.ts index f3102bd..8a305a6 100644 --- a/src/routes/chatSession.ts +++ b/src/routes/chatSession.ts @@ -12,15 +12,29 @@ const TITLE_CONTEXT_MESSAGE_CHAR_LIMIT = 240; const RESTORE_TURN_LIMIT = 8; const RESTORE_MESSAGE_CHAR_LIMIT = 480; const RESTORE_CONTEXT_CHAR_LIMIT = 3200; +const DEFAULT_SESSION_TITLE = "新对话"; const buildSessionTitle = (message: string) => { const normalized = message.replace(/\s+/g, " ").trim(); if (!normalized) { - return "新对话"; + return DEFAULT_SESSION_TITLE; } return normalized.length > 24 ? `${normalized.slice(0, 24)}...` : normalized; }; +const appendTitleContextMessage = ( + lines: string[], + role: "用户" | "助手", + content: string | undefined, + maxLength = TITLE_CONTEXT_MESSAGE_CHAR_LIMIT, +) => { + const normalized = content?.replace(/\s+/g, " ").trim(); + if (!normalized) { + return; + } + lines.push(`${role}:${normalized.slice(0, maxLength)}`); +}; + const buildTitleConversationContext = async ( runtime: OpencodeRuntimeAdapter, sessionId: string, @@ -67,7 +81,9 @@ const buildTitleConversationContext = async ( const normalizeGeneratedTitle = (rawTitle: string, fallback: string) => { const normalized = rawTitle .replace(/\s+/g, " ") + .replace(/^标题[::]\s*/i, "") .replace(/["'“”‘’`]/g, "") + .replace(/[。!?!?,,、;;::]+$/g, "") .trim(); if (!normalized) { return fallback; @@ -85,13 +101,24 @@ export const generateSessionTitle = async ( options: { sessionId: string; latestUserMessage: string; + latestAssistantMessage?: string; fallbackTitle?: string; }, ) => { - const fallback = options.fallbackTitle?.trim() || buildSessionTitle(options.latestUserMessage); + const fallbackTitle = options.fallbackTitle?.trim(); + const fallback = + fallbackTitle && fallbackTitle !== DEFAULT_SESSION_TITLE + ? fallbackTitle + : buildSessionTitle(options.latestUserMessage); let titleSessionId: string | undefined; try { - const conversation = await buildTitleConversationContext(runtime, options.sessionId); + const scopedContext: string[] = []; + appendTitleContextMessage(scopedContext, "用户", options.latestUserMessage, 480); + appendTitleContextMessage(scopedContext, "助手", options.latestAssistantMessage, 960); + const conversation = + scopedContext.length > 0 + ? scopedContext.join("\n") + : await buildTitleConversationContext(runtime, options.sessionId); if (!conversation) { return fallback; } @@ -104,8 +131,9 @@ export const generateSessionTitle = async ( [ "你是会话标题生成器。", "请根据下面整段多轮对话生成一个 8-16 字中文标题。", - "要求:简洁、可读、避免标点、不要引号、不要解释。", - "先理解完整对话,再概括核心任务或结论。", + "要求:简洁、具体、可读、避免标点、不要引号、不要解释。", + "优先概括用户当前真实需求和助手最终结论。", + "忽略系统提示、历史记忆、学习上下文、工具日志等元信息。", "不要直接照抄用户任一条消息原文。", "只输出标题本身。", "", diff --git a/tests/routes/chatSession.test.ts b/tests/routes/chatSession.test.ts index cad6ea2..19e9aa1 100644 --- a/tests/routes/chatSession.test.ts +++ b/tests/routes/chatSession.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "bun:test"; -import { shouldGenerateSessionTitle } from "../../src/routes/chatSession.js"; +import { + generateSessionTitle, + shouldGenerateSessionTitle, +} from "../../src/routes/chatSession.js"; +import { type OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js"; describe("shouldGenerateSessionTitle", () => { it("allows auto-title generation for the first turn when the title was not edited", () => { @@ -36,3 +40,34 @@ describe("shouldGenerateSessionTitle", () => { ).toBe(false); }); }); + +describe("generateSessionTitle", () => { + it("uses the current user and assistant turn instead of reading wrapped runtime context", async () => { + let titlePrompt = ""; + const runtime = { + createSession: async () => ({ id: "title-session" }), + prompt: async (_sessionId: string, prompt: string) => { + titlePrompt = prompt; + }, + waitForSessionIdle: async () => undefined, + messages: async () => [ + { + info: { role: "assistant" }, + parts: [{ type: "text", text: "标题:泵站压力异常排查。" }], + }, + ], + abortSession: async () => undefined, + } as unknown as OpencodeRuntimeAdapter; + + const title = await generateSessionTitle(runtime, { + sessionId: "chat-session", + latestUserMessage: "检查一下三号泵站最近压力波动的原因", + latestAssistantMessage: "三号泵站压力波动主要与夜间阀门开度变化有关。", + fallbackTitle: "新对话", + }); + + expect(title).toBe("泵站压力异常排查"); + expect(titlePrompt).toContain("用户:检查一下三号泵站最近压力波动的原因"); + expect(titlePrompt).toContain("助手:三号泵站压力波动主要与夜间阀门开度变化有关。"); + }); +});