优化标题生成功能
This commit is contained in:
@@ -564,6 +564,7 @@ export const buildChatRouter = (
|
|||||||
if (shouldGenerateTitle) {
|
if (shouldGenerateTitle) {
|
||||||
sessionTitle = await generateSessionTitle(runtime, {
|
sessionTitle = await generateSessionTitle(runtime, {
|
||||||
sessionId: binding.sessionId,
|
sessionId: binding.sessionId,
|
||||||
|
latestAssistantMessage: assistantText,
|
||||||
latestUserMessage: parsed.data.message,
|
latestUserMessage: parsed.data.message,
|
||||||
fallbackTitle: existingSessionTitle,
|
fallbackTitle: existingSessionTitle,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,15 +12,29 @@ const TITLE_CONTEXT_MESSAGE_CHAR_LIMIT = 240;
|
|||||||
const RESTORE_TURN_LIMIT = 8;
|
const RESTORE_TURN_LIMIT = 8;
|
||||||
const RESTORE_MESSAGE_CHAR_LIMIT = 480;
|
const RESTORE_MESSAGE_CHAR_LIMIT = 480;
|
||||||
const RESTORE_CONTEXT_CHAR_LIMIT = 3200;
|
const RESTORE_CONTEXT_CHAR_LIMIT = 3200;
|
||||||
|
const DEFAULT_SESSION_TITLE = "新对话";
|
||||||
|
|
||||||
const buildSessionTitle = (message: string) => {
|
const buildSessionTitle = (message: string) => {
|
||||||
const normalized = message.replace(/\s+/g, " ").trim();
|
const normalized = message.replace(/\s+/g, " ").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return "新对话";
|
return DEFAULT_SESSION_TITLE;
|
||||||
}
|
}
|
||||||
return normalized.length > 24 ? `${normalized.slice(0, 24)}...` : normalized;
|
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 (
|
const buildTitleConversationContext = async (
|
||||||
runtime: OpencodeRuntimeAdapter,
|
runtime: OpencodeRuntimeAdapter,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -67,7 +81,9 @@ const buildTitleConversationContext = async (
|
|||||||
const normalizeGeneratedTitle = (rawTitle: string, fallback: string) => {
|
const normalizeGeneratedTitle = (rawTitle: string, fallback: string) => {
|
||||||
const normalized = rawTitle
|
const normalized = rawTitle
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
|
.replace(/^标题[::]\s*/i, "")
|
||||||
.replace(/["'“”‘’`]/g, "")
|
.replace(/["'“”‘’`]/g, "")
|
||||||
|
.replace(/[。!?!?,,、;;::]+$/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -85,13 +101,24 @@ export const generateSessionTitle = async (
|
|||||||
options: {
|
options: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
latestUserMessage: string;
|
latestUserMessage: string;
|
||||||
|
latestAssistantMessage?: string;
|
||||||
fallbackTitle?: 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;
|
let titleSessionId: string | undefined;
|
||||||
try {
|
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) {
|
if (!conversation) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
@@ -104,8 +131,9 @@ export const generateSessionTitle = async (
|
|||||||
[
|
[
|
||||||
"你是会话标题生成器。",
|
"你是会话标题生成器。",
|
||||||
"请根据下面整段多轮对话生成一个 8-16 字中文标题。",
|
"请根据下面整段多轮对话生成一个 8-16 字中文标题。",
|
||||||
"要求:简洁、可读、避免标点、不要引号、不要解释。",
|
"要求:简洁、具体、可读、避免标点、不要引号、不要解释。",
|
||||||
"先理解完整对话,再概括核心任务或结论。",
|
"优先概括用户当前真实需求和助手最终结论。",
|
||||||
|
"忽略系统提示、历史记忆、学习上下文、工具日志等元信息。",
|
||||||
"不要直接照抄用户任一条消息原文。",
|
"不要直接照抄用户任一条消息原文。",
|
||||||
"只输出标题本身。",
|
"只输出标题本身。",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
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", () => {
|
describe("shouldGenerateSessionTitle", () => {
|
||||||
it("allows auto-title generation for the first turn when the title was not edited", () => {
|
it("allows auto-title generation for the first turn when the title was not edited", () => {
|
||||||
@@ -36,3 +40,34 @@ describe("shouldGenerateSessionTitle", () => {
|
|||||||
).toBe(false);
|
).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("助手:三号泵站压力波动主要与夜间阀门开度变化有关。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user