feat: AI 会话支持按轮保存完整笔记

This commit is contained in:
lofyer
2026-08-04 20:50:31 +08:00
parent 0fd7c59e08
commit f72c26642f
9 changed files with 471 additions and 33 deletions
+5 -4
View File
@@ -14,7 +14,6 @@ const GLOBAL_ENTRY_ID = 'system:global-chat';
const LIMITS = {
sessionId: 160,
title: 200,
messageText: 20000,
question: 4000,
contextText: 12000,
contextHash: 32,
@@ -252,7 +251,9 @@ function buildMessage(raw, role, lenient) {
return {
id: newId('msg'),
role,
text: limitedString(value.text, role === 'user' ? LIMITS.question : LIMITS.messageText),
text: role === 'user'
? limitedString(value.text, LIMITS.question)
: String(value.text == null ? '' : value.text),
task: normalizeTask(value.task, lenient),
contextRef: role === 'user' ? normalizeContextRef(value.contextRef, lenient) : null,
images: normalizeImages(value.images, lenient),
@@ -807,7 +808,7 @@ function finishAssistant(sessionId, messageId, patch) {
? held.message
: doc.messages.find((item) => item.id === msgId && item.role === 'assistant');
if (!message) throw new Error('会话消息不存在');
message.text = limitedString(value.text == null ? message.text : value.text, LIMITS.messageText);
message.text = String(value.text == null ? message.text : value.text);
if (value.task !== undefined) message.task = normalizeTask(value.task, false);
if (value.images !== undefined) message.images = normalizeImages(value.images, false);
if (value.tokensEstimate !== undefined) message.tokensEstimate = count(value.tokensEstimate, 1e9);
@@ -843,7 +844,7 @@ function normalizeBudget(budget) {
return {
maxChars: clampInt(value.maxChars, 50, 4000000, LIMITS.contextText),
maxMessages: clampInt(value.maxMessages, 1, LIMITS.messagesPerSession, 20),
maxMessageChars: clampInt(value.maxMessageChars, 50, LIMITS.messageText, LIMITS.question)
maxMessageChars: clampInt(value.maxMessageChars, 50, 4000000, LIMITS.question)
};
}
+13 -17
View File
@@ -11,7 +11,7 @@ const STANDALONE_ENTRY_ID = 'system:standalone-notes';
const LIMITS = {
id: 160,
title: 500,
text: 20000,
canvasText: 20000,
quote: 10000,
context: 20000,
aiTask: 500,
@@ -24,7 +24,7 @@ const LIMITS = {
locatorJson: 50000,
richBlocks: 500,
richOps: 5000,
richJson: 12 * 1024 * 1024,
canvasRichJson: 12 * 1024 * 1024,
richImages: 12,
richImageBytes: 2 * 1024 * 1024,
richImageTotalBytes: 8 * 1024 * 1024,
@@ -316,7 +316,6 @@ function normalizeRichContent(input) {
}
if (value.ops.length > LIMITS.richOps) throw new Error('富文本笔记内容过多');
const result = { version: 2, ops: [] };
let textLength = 0;
let imageCount = 0;
let imageBytes = 0;
for (const op of value.ops) {
@@ -325,8 +324,6 @@ function normalizeRichContent(input) {
}
const attributes = normalizeRichAttributes(op.attributes);
if (typeof op.insert === 'string') {
textLength += op.insert.length;
if (textLength > LIMITS.text) throw new Error('富文本笔记文字过多');
if (op.insert) result.ops.push({
insert: op.insert,
...(attributes ? { attributes } : {})
@@ -349,7 +346,6 @@ function normalizeRichContent(input) {
}
result.ops.push({ insert: { image: image.dataUrl } });
}
if (JSON.stringify(result).length > LIMITS.richJson) throw new Error('富文本笔记过大');
return hasRichContent(result) ? result : null;
}
@@ -359,8 +355,7 @@ function richPlainText(content) {
.filter((op) => typeof op.insert === 'string')
.map((op) => op.insert)
.join('')
.replace(/\n$/, '')
.slice(0, LIMITS.text);
.replace(/\n$/, '');
}
function hasRichContent(content) {
@@ -431,7 +426,7 @@ function normalizeCanvasObject(value, imageTotals) {
throw new Error('画布对象线宽无效');
}
if (value.canvasKind === 'text'
&& (typeof value.text !== 'string' || value.text.length > LIMITS.text)) {
&& (typeof value.text !== 'string' || value.text.length > LIMITS.canvasText)) {
throw new Error('画布文字无效');
}
if ((value.canvasKind === 'pen' || value.canvasKind === 'highlight')
@@ -475,7 +470,7 @@ function normalizeCanvasFlow(value, pageIds, firstPageId) {
}
if (typeof op.insert === 'string') {
textLength += op.insert.length;
if (textLength > LIMITS.text) throw new Error('画布全局文本文字过多');
if (textLength > LIMITS.canvasText) throw new Error('画布全局文本文字过多');
const attributes = normalizeRichAttributes(op.attributes);
if (op.insert) result.ops.push({
insert: op.insert,
@@ -499,7 +494,7 @@ function normalizeCanvasFlow(value, pageIds, firstPageId) {
breakIds.add(pageId);
result.ops.push({ insert: { canvasPageBreak: pageId } });
}
if (JSON.stringify(result).length > LIMITS.richJson) throw new Error('画布全局文本过大');
if (JSON.stringify(result).length > LIMITS.canvasRichJson) throw new Error('画布全局文本过大');
const meaningful = result.ops.some((op) => (
typeof op.insert === 'string' ? op.insert.trim() : !!op.insert.canvasPageBreak
));
@@ -593,14 +588,15 @@ function canvasPlainText(content) {
return [flowText, objectText]
.filter((text) => text.trim())
.join('\n')
.slice(0, LIMITS.text);
.slice(0, LIMITS.canvasText);
}
function notePlainText(richContent, canvasContent, fallback) {
return [
richContent ? richPlainText(richContent) : String(fallback || ''),
const text = [
richContent ? richPlainText(richContent) : String(fallback == null ? '' : fallback),
canvasPlainText(canvasContent)
].filter((value) => value.trim()).join('\n').slice(0, LIMITS.text);
].filter((value) => value.trim()).join('\n');
return canvasContent ? text.slice(0, LIMITS.canvasText) : text;
}
function hasCanvasContent(content) {
@@ -1023,7 +1019,7 @@ function noteInput(note, c) {
if (noteType === 'canvas' && !hasCanvasContent(canvasContent)) {
throw new Error('画布笔记内容为空');
}
const text = notePlainText(richContent, canvasContent, limitedString(note.text, LIMITS.text));
const text = notePlainText(richContent, canvasContent, note.text);
const quote = limitedString(note.quote, LIMITS.quote);
if (!text.trim() && !quote.trim()
&& !hasRichContent(richContent) && !hasCanvasContent(canvasContent)) {
@@ -1104,7 +1100,7 @@ function updateNote(id, noteId, patch) {
else delete note.canvasContent;
}
if (Object.prototype.hasOwnProperty.call(patch, 'text')) {
fallbackText = limitedString(patch.text, LIMITS.text);
fallbackText = String(patch.text == null ? '' : patch.text);
if (note.noteType !== 'canvas'
&& !Object.prototype.hasOwnProperty.call(patch, 'richContent')) {
delete note.richContent;