import type { ChatMessage } from "../llm/client.js"; import { clipDictateDialogue, resolveDictateHistoryTurns, } from "./history-window.js"; import { effectiveDictateOrder, sortDictateProducts, type DictateChatTurn, type DictateProduct, } from "./types.js"; export type DictateContextPhase = "select" | "fill" | "reply"; /** 五段字数,只给用量统计,不另存正文。 */ export type DictateSegmentChars = { rules: number; history: number; products: number; injected: number; tail: number; }; export type DictateBuiltContext = { messages: ChatMessage[]; segmentChars: DictateSegmentChars; }; /** * 创作请求的块顺序:固定规则、更早的对话、全部产物、本轮注入、末尾。 * 选题不带产物正文和本轮注入。回复步只带本轮刚落盘产物与末句用户话。 * 更早对话按 historyTurns 裁剪(默认 2 轮),最后一轮用户输入放在末尾。 */ export function buildDictateContext(params: { systemPrompt: string; products: DictateProduct[]; dialogue: DictateChatTurn[]; phase?: DictateContextPhase; injected?: string; tail?: string; /** 更早对话保留轮数;默认见 resolveDictateHistoryTurns */ historyTurns?: number; }): DictateBuiltContext { const phase = params.phase ?? "fill"; if (phase === "reply") { return buildReplyContext(params); } const clipped = clipDictateDialogue( params.dialogue, resolveDictateHistoryTurns(params.historyTurns), ); const { earlier, lastUser } = splitEarlierDialogue(clipped); const history = formatEarlierDialogue(earlier); const products = phase === "fill" ? formatProducts(sortDictateProducts(params.products)) : ""; const injected = phase === "fill" ? params.injected?.trim() ?? "" : ""; const tail = joinBlocks([params.tail?.trim() ?? "", lastUser ? `最后一轮用户输入\n${lastUser}` : ""]); const fillDuty = phase === "fill" ? "## 本步收尾\n\n写完本轮节点表后,同一次请求必须调用 reply_user。只 insert 不算完成。" : ""; const sections = [ fillDuty, history, products ? `## 当前产物(已写入黑板,按相对序;对话清空后仍保留)\n\n${products}` : "", injected ? `## 本轮注入\n\n写完下列节点后,同一次请求必须调用 reply_user。\n\n本轮节点表\n\n${injected}` : "", tail ? `## 本轮\n\n${tail}` : "", ].filter(Boolean); const user = sections.join("\n\n"); return { messages: [ { role: "system", content: params.systemPrompt }, { role: "user", content: user }, ], segmentChars: { rules: params.systemPrompt.length, history: history.length, products: products.length, injected: injected.length, tail: tail.length, }, }; } /** 回复兜底:只给本轮产物 + 末句,不带全黑板与节点规格。 */ function buildReplyContext(params: { systemPrompt: string; products: DictateProduct[]; dialogue: DictateChatTurn[]; tail?: string; }): DictateBuiltContext { const { lastUser } = splitEarlierDialogue(params.dialogue); const products = formatRoundProducts(params.products); const tail = joinBlocks([ params.tail?.trim() ?? "", lastUser ? `最后一轮用户输入\n${lastUser}` : "", ]); const sections = [ products ? `## 本轮刚落盘\n\n${products}` : "## 本轮刚落盘\n\n(无正文产物;按删除与用户原话回复)", tail ? `## 本轮\n\n${tail}` : "", ].filter(Boolean); const user = sections.join("\n\n"); return { messages: [ { role: "system", content: params.systemPrompt }, { role: "user", content: user }, ], segmentChars: { rules: params.systemPrompt.length, history: 0, products: products.length, injected: 0, tail: tail.length, }, }; } function formatRoundProducts(products: DictateProduct[]): string { if (products.length === 0) return ""; return sortDictateProducts(products) .map((product) => { const score = formatSelfScore(product); return [`### ${product.tag}`, score, product.content].filter(Boolean).join("\n"); }) .join("\n\n"); } function formatSelfScore(product: DictateProduct): string { const dims = product.selfScore?.dims; if (!dims?.length) return ""; const lines = dims.map((dim) => { const gap = dim.gap?.trim() ? `;缺口:${dim.gap.trim()}` : ""; const options = dim.options?.length ? `;选项:${dim.options.map((item) => item.trim()).filter(Boolean).join(" / ")}` : ""; return `- ${dim.name}:${dim.score}${gap}${options}`; }); return `self_score:\n${lines.join("\n")}`; } export function buildDictateMessages(params: { systemPrompt: string; products: DictateProduct[]; dialogue: DictateChatTurn[]; phase?: DictateContextPhase; injected?: string; tail?: string; historyTurns?: number; }): ChatMessage[] { return buildDictateContext(params).messages; } /** 从会话消息里抽出转述式对话(用户输入 + 助手回复) */ export function extractDictateDialogue( messages: Array<{ role?: string; kind?: string; text?: string; body?: string }>, ): DictateChatTurn[] { const out: DictateChatTurn[] = []; for (const m of messages) { const text = (m.body ?? m.text ?? "").trim(); if (!text) continue; if (m.role === "user" || m.kind === "user_input") { out.push({ role: "user", text }); continue; } if (m.kind === "dictate_reply") { out.push({ role: "assistant", text }); } } return out; } function splitEarlierDialogue(dialogue: DictateChatTurn[]): { earlier: DictateChatTurn[]; lastUser: string; } { const last = dialogue.at(-1); if (last?.role === "user") { return { earlier: dialogue.slice(0, -1), lastUser: last.text.trim() }; } return { earlier: dialogue, lastUser: "" }; } function formatEarlierDialogue(dialogue: DictateChatTurn[]): string { if (dialogue.length === 0) return "## 更早的对话\n\n(尚无更早对话)"; const lines = dialogue.map((turn) => { const who = turn.role === "user" ? "用户" : "助手"; return `【${who}】\n${turn.text}`; }); return `## 更早的对话\n\n${lines.join("\n\n")}`; } function formatProducts(products: DictateProduct[]): string { if (products.length === 0) return "(尚无)"; return products .map((product) => { const ord = effectiveDictateOrder(product); return `### ${product.tag} 〔order=${ord}〕\n${product.content}`; }) .join("\n\n"); } function joinBlocks(blocks: string[]): string { return blocks.map((block) => block.trim()).filter(Boolean).join("\n\n"); }