fix: stabilize show_html rendering and restore conversation mode state

This commit is contained in:
JOJO 2026-04-26 01:04:23 +08:00
parent 33d270ff63
commit 383e68188c
11 changed files with 163 additions and 77 deletions

View File

@ -229,6 +229,9 @@ class WebTerminal(MainTerminal):
"conversation_id": conversation_id, "conversation_id": conversation_id,
"title": conversation_data.get("title", "未知对话"), "title": conversation_data.get("title", "未知对话"),
"messages_count": len(self.context_manager.conversation_history), "messages_count": len(self.context_manager.conversation_history),
"run_mode": self.run_mode,
"thinking_mode": self.thinking_mode,
"model_key": getattr(self, "model_key", None),
"message": f"对话已加载: {conversation_id}" "message": f"对话已加载: {conversation_id}"
} }
else: else:

View File

@ -53,9 +53,18 @@
如需在界面直接渲染 HTML 预览,使用 `<show_html ratio="比例">...</show_html>`。 如需在界面直接渲染 HTML 预览,使用 `<show_html ratio="比例">...</show_html>`。
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4` - `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
- 各比例对应的前端基准渲染尺寸(宽 x 高)如下,请按这些尺寸去设计元素大小与间距:
- `1:1` → `620x620`
- `16:9` → `620x349`
- `9:16` → `620x1102`
- `4:3` → `620x465`
- `3:4` → `620x827`
- 说明:以上为基准像素,最终仍会按窗口与容器可用空间自适应裁剪
- **禁止**输出 `width` / `height` 属性,最终像素尺寸由前端根据窗口自适应 - **禁止**输出 `width` / `height` 属性,最终像素尺寸由前端根据窗口自适应
- 实际展示给用户的渲染后内容长宽都很小,可能只有几百 px需主动使用合适的间距调整、合适的元素大小和距离如 `padding`/`gap`/`line-height`)保证内容可读、不过度留白
- 标签内容只写 **HTML + CSS**,不要写 JavaScript - 标签内容只写 **HTML + CSS**,不要写 JavaScript
- 禁止输出 `<script>`、`onclick` 等事件脚本;前端会禁用脚本执行 - 禁止输出 `<script>`、`onclick` 等事件脚本;前端会禁用脚本执行
- 当用户说“给我画一个xxx”这类可视化诉求时优先使用 `<show_html>` 标签直接实现,而不是先写一个 html 文件(除非用户明确要求落盘)
简单示例(正确格式): 简单示例(正确格式):
<show_html ratio="16:9"> <show_html ratio="16:9">

View File

@ -53,9 +53,18 @@
如需在界面直接渲染 HTML 预览,使用 `<show_html ratio="比例">...</show_html>`。 如需在界面直接渲染 HTML 预览,使用 `<show_html ratio="比例">...</show_html>`。
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4` - `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
- 各比例对应的前端基准渲染尺寸(宽 x 高)如下,请按这些尺寸去设计元素大小与间距:
- `1:1` → `620x620`
- `16:9` → `620x349`
- `9:16` → `620x1102`
- `4:3` → `620x465`
- `3:4` → `620x827`
- 说明:以上为基准像素,最终仍会按窗口与容器可用空间自适应裁剪
- **禁止**输出 `width` / `height` 属性,最终像素尺寸由前端根据窗口自适应 - **禁止**输出 `width` / `height` 属性,最终像素尺寸由前端根据窗口自适应
- 实际展示给用户的渲染后内容长宽都很小,可能只有几百 px需主动使用合适的间距调整、合适的元素大小和距离如 `padding`/`gap`/`line-height`)保证内容可读、不过度留白
- 标签内容只写 **HTML + CSS**,不要写 JavaScript - 标签内容只写 **HTML + CSS**,不要写 JavaScript
- 禁止输出 `<script>`、`onclick` 等事件脚本;前端会禁用脚本执行 - 禁止输出 `<script>`、`onclick` 等事件脚本;前端会禁用脚本执行
- 当用户说“给我画一个xxx”这类可视化诉求时优先使用 `<show_html>` 标签直接实现,而不是先写一个 html 文件(除非用户明确要求落盘)
简单示例(正确格式): 简单示例(正确格式):
<show_html ratio="16:9"> <show_html ratio="16:9">

View File

@ -211,6 +211,7 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
if has_request_context(): if has_request_context():
session['run_mode'] = terminal.run_mode session['run_mode'] = terminal.run_mode
session['thinking_mode'] = terminal.thinking_mode session['thinking_mode'] = terminal.thinking_mode
session['model_key'] = getattr(terminal, "model_key", None)
if is_api_user: if is_api_user:
session['workspace_id'] = getattr(workspace, "workspace_id", None) session['workspace_id'] = getattr(workspace, "workspace_id", None)
else: else:
@ -424,6 +425,7 @@ def ensure_conversation_loaded(
if has_request_context(): if has_request_context():
session['run_mode'] = terminal.run_mode session['run_mode'] = terminal.run_mode
session['thinking_mode'] = terminal.thinking_mode session['thinking_mode'] = terminal.thinking_mode
session['model_key'] = getattr(terminal, "model_key", None)
except Exception: except Exception:
pass pass
# 应用对话级自定义 prompt / personalization仅 API # 应用对话级自定义 prompt / personalization仅 API

View File

@ -356,6 +356,9 @@ def load_conversation(conversation_id, terminal: WebTerminal, workspace: UserWor
result = terminal.load_conversation(conversation_id) result = terminal.load_conversation(conversation_id)
if result["success"]: if result["success"]:
session['run_mode'] = terminal.run_mode
session['thinking_mode'] = terminal.thinking_mode
session['model_key'] = getattr(terminal, "model_key", None)
normalized_id = _normalize_conv_id(conversation_id) normalized_id = _normalize_conv_id(conversation_id)
if _is_host_mode_request(username): if _is_host_mode_request(username):
try: try:

View File

@ -59,7 +59,9 @@ class TaskRecord:
self.updated_at = self.created_at self.updated_at = self.created_at
self.message = message self.message = message
self.conversation_id = conversation_id self.conversation_id = conversation_id
self.events: deque[Dict[str, Any]] = deque(maxlen=1000) # 刷新恢复时前端会从事件流重建进行中的输出1000 在长流式回复下会过早截断,
# 导致“只恢复最后几个字符”。这里提高缓冲上限,优先保证重建完整性。
self.events: deque[Dict[str, Any]] = deque(maxlen=20000)
self.thread: Optional[threading.Thread] = None self.thread: Optional[threading.Thread] = None
self.error: Optional[str] = None self.error: Optional[str] = None
self.model_key = model_key self.model_key = model_key

View File

@ -166,6 +166,14 @@ const SHOW_HTML_RATIO_VALUES: Record<string, number> = {
'4:3': 4 / 3, '4:3': 4 / 3,
'3:4': 3 / 4 '3:4': 3 / 4
}; };
// show_html 统一按比例对应预设基准像素渲染(前端仍会按可用空间自适应)
const SHOW_HTML_RATIO_BASE_SIZE: Record<string, { width: number; height: number }> = {
'1:1': { width: 620, height: 620 },
'16:9': { width: 620, height: 349 },
'9:16': { width: 620, height: 1102 },
'4:3': { width: 620, height: 465 },
'3:4': { width: 620, height: 827 }
};
function normalizeShowHtmlRatio(raw: string | null) { function normalizeShowHtmlRatio(raw: string | null) {
if (!raw) return '1:1'; if (!raw) return '1:1';
@ -203,6 +211,7 @@ function readShowHtmlRatio(node: Element) {
function computeAdaptiveShowHtmlSize(node: Element, ratioKey: string) { function computeAdaptiveShowHtmlSize(node: Element, ratioKey: string) {
const ratio = SHOW_HTML_RATIO_VALUES[ratioKey] || 1; const ratio = SHOW_HTML_RATIO_VALUES[ratioKey] || 1;
const base = SHOW_HTML_RATIO_BASE_SIZE[ratioKey] || SHOW_HTML_RATIO_BASE_SIZE['1:1'];
const windowW = Math.max(320, window.innerWidth || document.documentElement.clientWidth || 320); const windowW = Math.max(320, window.innerWidth || document.documentElement.clientWidth || 320);
const windowH = Math.max(360, window.innerHeight || document.documentElement.clientHeight || 360); const windowH = Math.max(360, window.innerHeight || document.documentElement.clientHeight || 360);
const parentRect = node.parentElement?.getBoundingClientRect(); const parentRect = node.parentElement?.getBoundingClientRect();
@ -216,28 +225,16 @@ function computeAdaptiveShowHtmlSize(node: Element, ratioKey: string) {
220, 220,
Math.floor(Math.min(flowRect?.width || windowW, parentRect?.width || windowW) - 8) Math.floor(Math.min(flowRect?.width || windowW, parentRect?.width || windowW) - 8)
); );
const maxWidth = Math.max(220, Math.min(availableWidth, Math.floor(windowW * 0.88), 920)); // 移除“二次约束”后:窗口/容器足够大时,严格使用 prompt 约定的基准像素;
const minWidth = Math.min(maxWidth, Math.max(220, Math.floor(windowW * 0.34))); // 仅在物理空间不足时按比例等比缩小(不再做额外的最小/最大高度钳制)。
let widthPx = Math.max(minWidth, maxWidth); // 最大卡片约束:上限按 1200x900窗口不足时仍按可用空间缩放
const limitWidth = Math.max(220, Math.min(availableWidth, Math.floor(windowW * 0.96), 1200));
const limitHeight = Math.max(180, Math.min(Math.floor(windowH * 0.9), 900));
const widthScale = limitWidth / base.width;
const heightScale = limitHeight / base.height;
const scale = Math.min(1, widthScale, heightScale);
let widthPx = Math.max(220, Math.round(base.width * scale));
let heightPx = Math.round(widthPx / ratio); let heightPx = Math.round(widthPx / ratio);
const maxHeight = Math.max(200, Math.min(900, Math.floor(windowH * 0.68)));
const minHeight = Math.max(180, Math.min(280, Math.floor(windowH * 0.32)));
if (heightPx > maxHeight) {
heightPx = maxHeight;
widthPx = Math.round(heightPx * ratio);
}
if (heightPx < minHeight) {
heightPx = minHeight;
widthPx = Math.round(heightPx * ratio);
}
if (widthPx > maxWidth) {
widthPx = maxWidth;
heightPx = Math.round(widthPx / ratio);
}
if (widthPx < minWidth) {
widthPx = minWidth;
heightPx = Math.round(widthPx / ratio);
}
return { widthPx, heightPx, ratio }; return { widthPx, heightPx, ratio };
} }
@ -260,11 +257,16 @@ function sanitizeInlineCss(css: string) {
function rewriteCssSelectorsForShadowDom(css: string) { function rewriteCssSelectorsForShadowDom(css: string) {
if (!css) return ''; if (!css) return '';
// 注意:不能直接把 html/body 连续替换成 .show-html-root
// 否则后续 /\bhtml\b/ 会再次命中 ".show-html-root" 里的 "html",产生
// 类似 ".show-.show-html-root-root" 的错误选择器,导致布局样式失效(元素偏移)。
const placeholder = '__SHOW_HTML_ROOT_SELECTOR__';
return css return css
.replace(/\bhtml\s*,\s*body\b/gi, '.show-html-root') .replace(/\bhtml\s*,\s*body\b/gi, placeholder)
.replace(/\bbody\s*,\s*html\b/gi, '.show-html-root') .replace(/\bbody\s*,\s*html\b/gi, placeholder)
.replace(/\bhtml\b/gi, '.show-html-root') .replace(/\bhtml\b/gi, placeholder)
.replace(/\bbody\b/gi, '.show-html-root'); .replace(/\bbody\b/gi, placeholder)
.replace(new RegExp(placeholder, 'g'), '.show-html-root');
} }
function sanitizeShowHtmlContent(rawHtml: string) { function sanitizeShowHtmlContent(rawHtml: string) {
@ -437,15 +439,51 @@ function renderShowImages(root: ParentNode | null = document) {
if (!node.isConnected) return; if (!node.isConnected) return;
const inStreaming = !!node.closest('.streaming-text'); const inStreaming = !!node.closest('.streaming-text');
const pathKey = buildNodePathKey(node); const pathKey = buildNodePathKey(node);
const isPartial = node.getAttribute('data-partial') === '1';
const encoded = node.getAttribute('data-encoded') || '';
const computedRatioKey = readShowHtmlRatio(node); const computedRatioKey = readShowHtmlRatio(node);
let ratioKey = computedRatioKey; let ratioKey = computedRatioKey;
let { widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey); let { widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey);
// 流式阶段:一旦 show_html 已经出现完整闭合(非 partial冻结该 path 的已完成快照;
// 后续即使继续输出其他普通文本,也复用该快照,避免已完成 show_html 反复刷新。
if (inStreaming && !isPartial && encoded) {
const frozen = showHtmlCompletedSnapshotByPath.get(pathKey);
if (!frozen || frozen.encoded !== encoded || frozen.ratioKey !== ratioKey) {
showHtmlCompletedSnapshotByPath.set(pathKey, {
encoded,
ratioKey
});
}
} else if (!inStreaming) {
showHtmlCompletedSnapshotByPath.delete(pathKey);
}
const frozenSnapshot = inStreaming ? showHtmlCompletedSnapshotByPath.get(pathKey) : null;
const effectiveEncoded = frozenSnapshot?.encoded || encoded;
if (frozenSnapshot?.ratioKey) {
ratioKey = frozenSnapshot.ratioKey;
({ widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey));
}
if (inStreaming) { if (inStreaming) {
const locked = showHtmlStreamingSizeLockByPath.get(pathKey); const locked = showHtmlStreamingSizeLockByPath.get(pathKey);
if (locked) { if (locked) {
ratioKey = locked.ratioKey; // 修复:流式初期若 ratio 解析不完整会先落到 1:1后续必须允许升级到真实比例
widthPx = locked.widthPx; const shouldUpgradeRatio =
heightPx = locked.heightPx; ratioKey !== locked.ratioKey && (locked.ratioKey === '1:1' || ratioKey !== '1:1');
if (shouldUpgradeRatio) {
ratioKey = computedRatioKey;
({ widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey));
showHtmlStreamingSizeLockByPath.set(pathKey, {
ratioKey,
widthPx,
heightPx
});
} else {
ratioKey = locked.ratioKey;
widthPx = locked.widthPx;
heightPx = locked.heightPx;
}
} else { } else {
showHtmlStreamingSizeLockByPath.set(pathKey, { showHtmlStreamingSizeLockByPath.set(pathKey, {
ratioKey, ratioKey,
@ -474,7 +512,8 @@ function renderShowImages(root: ParentNode | null = document) {
ratioAttr: node.getAttribute('ratio') || '', ratioAttr: node.getAttribute('ratio') || '',
ratioKey, ratioKey,
partial: node.getAttribute('data-partial') || '', partial: node.getAttribute('data-partial') || '',
encodedLength: (node.getAttribute('data-encoded') || '').length, encodedLength: encoded.length,
effectiveEncodedLength: effectiveEncoded.length,
innerLength: (node.innerHTML || '').length, innerLength: (node.innerHTML || '').length,
reservedWidth: `${widthPx}px`, reservedWidth: `${widthPx}px`,
reservedMinHeight: `${heightPx}px` reservedMinHeight: `${heightPx}px`
@ -511,13 +550,13 @@ function renderShowImages(root: ParentNode | null = document) {
showHtmlStreamingRenderTsByPath.delete(pathKey); showHtmlStreamingRenderTsByPath.delete(pathKey);
} }
const encoded = node.getAttribute('data-encoded') || ''; const rawHtml = effectiveEncoded ? decodeBase64Utf8(effectiveEncoded) : node.innerHTML || '';
const rawHtml = encoded ? decodeBase64Utf8(encoded) : node.innerHTML || '';
const safeHtml = sanitizeShowHtmlContent(rawHtml); const safeHtml = sanitizeShowHtmlContent(rawHtml);
debugShowHtmlLog('render:node-content', { debugShowHtmlLog('render:node-content', {
renderId, renderId,
pathKey, pathKey,
encodedLength: encoded.length, encodedLength: encoded.length,
effectiveEncodedLength: effectiveEncoded.length,
rawLength: rawHtml.length, rawLength: rawHtml.length,
safeLength: safeHtml.length, safeLength: safeHtml.length,
ratioKey, ratioKey,
@ -539,7 +578,7 @@ function renderShowImages(root: ParentNode | null = document) {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: auto;
background: transparent; background: transparent;
color: inherit; color: inherit;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
@ -548,13 +587,12 @@ function renderShowImages(root: ParentNode | null = document) {
} }
*, *::before, *::after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
max-width: 100%;
} }
.show-html-root { .show-html-root {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
overflow: hidden; overflow: auto;
background: transparent; background: transparent;
} }
`; `;
@ -578,20 +616,20 @@ function renderShowImages(root: ParentNode | null = document) {
persistent.wrapper.style.width = `${widthPx}px`; persistent.wrapper.style.width = `${widthPx}px`;
persistent.host.style.height = `${heightPx}px`; persistent.host.style.height = `${heightPx}px`;
if ( if (
persistent.encoded !== encoded || persistent.encoded !== effectiveEncoded ||
persistent.widthPx !== widthPx || persistent.widthPx !== widthPx ||
persistent.heightPx !== heightPx || persistent.heightPx !== heightPx ||
persistent.ratioKey !== ratioKey persistent.ratioKey !== ratioKey
) { ) {
persistent.root.innerHTML = safeHtml; persistent.root.innerHTML = safeHtml;
persistent.encoded = encoded; persistent.encoded = effectiveEncoded;
persistent.widthPx = widthPx; persistent.widthPx = widthPx;
persistent.heightPx = heightPx; persistent.heightPx = heightPx;
persistent.ratioKey = ratioKey; persistent.ratioKey = ratioKey;
debugShowHtmlLog('render:node-persistent-update', { debugShowHtmlLog('render:node-persistent-update', {
renderId, renderId,
pathKey, pathKey,
encodedLength: encoded.length, encodedLength: effectiveEncoded.length,
ratioKey, ratioKey,
widthPx, widthPx,
heightPx heightPx
@ -600,7 +638,7 @@ function renderShowImages(root: ParentNode | null = document) {
debugShowHtmlLog('render:node-persistent-reuse', { debugShowHtmlLog('render:node-persistent-reuse', {
renderId, renderId,
pathKey, pathKey,
encodedLength: encoded.length encodedLength: effectiveEncoded.length
}); });
} }
@ -608,12 +646,12 @@ function renderShowImages(root: ParentNode | null = document) {
node.setAttribute('data-rendered', '1'); node.setAttribute('data-rendered', '1');
const rect = persistent.wrapper.getBoundingClientRect(); const rect = persistent.wrapper.getBoundingClientRect();
debugShowHtmlLog('render:node-done', { debugShowHtmlLog('render:node-done', {
renderId, renderId,
pathKey, pathKey,
ratioKey, ratioKey,
widthPx, widthPx,
heightPx, heightPx,
rect: { rect: {
x: Math.round(rect.x), x: Math.round(rect.x),
y: Math.round(rect.y), y: Math.round(rect.y),
w: Math.round(rect.width), w: Math.round(rect.width),
@ -647,6 +685,13 @@ let showTagBindRetryTimer: number | null = null;
let showTagObservedContainer: Element | null = null; let showTagObservedContainer: Element | null = null;
const showHtmlStreamingRenderTsByPath = new Map<string, number>(); const showHtmlStreamingRenderTsByPath = new Map<string, number>();
const SHOW_HTML_STREAMING_RENDER_INTERVAL_MS = 180; const SHOW_HTML_STREAMING_RENDER_INTERVAL_MS = 180;
const showHtmlCompletedSnapshotByPath = new Map<
string,
{
encoded: string,
ratioKey: string
}
>();
const showHtmlStreamingSizeLockByPath = new Map< const showHtmlStreamingSizeLockByPath = new Map<
string, string,
{ {
@ -963,6 +1008,7 @@ export function teardownShowImageObserver() {
layoutDebugCount = 0; layoutDebugCount = 0;
layoutDebugLastTsByKey.clear(); layoutDebugLastTsByKey.clear();
showHtmlStreamingRenderTsByPath.clear(); showHtmlStreamingRenderTsByPath.clear();
showHtmlCompletedSnapshotByPath.clear();
showHtmlStreamingSizeLockByPath.clear(); showHtmlStreamingSizeLockByPath.clear();
showHtmlPersistentRenderByPath.clear(); showHtmlPersistentRenderByPath.clear();
showTagObservedContainer = null; showTagObservedContainer = null;

View File

@ -261,34 +261,7 @@ export const conversationMethods = {
} }
} }
// 应用个性化设置中的默认模型和思考模式 // 注意:加载已有对话时必须保留该对话自身的模型/模式,不能套用用户默认值。
try {
const personalizationStore = usePersonalizationStore();
if (personalizationStore.loaded) {
const defaultRunMode = personalizationStore.form.default_run_mode;
const defaultModel = personalizationStore.form.default_model;
if (defaultRunMode) {
this.runMode = defaultRunMode;
debugLog('应用默认运行模式:', defaultRunMode);
}
if (defaultModel) {
this.currentModelKey = defaultModel;
debugLog('应用默认模型:', defaultModel);
}
// 根据默认运行模式设置思考模式
if (defaultRunMode === 'thinking') {
this.thinkingMode = true;
} else if (defaultRunMode === 'fast') {
this.thinkingMode = false;
}
}
} catch (error) {
console.warn('应用个性化默认设置失败:', error);
}
// 有任务或后台子智能体运行时,提示用户确认切换 // 有任务或后台子智能体运行时,提示用户确认切换
try { try {
@ -343,6 +316,19 @@ export const conversationMethods = {
if (result.success) { if (result.success) {
debugLog('对话加载API成功:', result); debugLog('对话加载API成功:', result);
traceLog('loadConversation:api-success', { conversationId, title: result.title }); traceLog('loadConversation:api-success', { conversationId, title: result.title });
if (typeof result.run_mode === 'string') {
this.runMode = result.run_mode;
this.thinkingMode =
typeof result.thinking_mode === 'boolean'
? result.thinking_mode
: result.run_mode !== 'fast';
} else if (typeof result.thinking_mode === 'boolean') {
this.thinkingMode = result.thinking_mode;
this.runMode = result.thinking_mode ? 'thinking' : 'fast';
}
if (typeof result.model_key === 'string' && result.model_key) {
this.modelSet(result.model_key);
}
// 2. 更新当前对话信息 // 2. 更新当前对话信息
this.skipConversationHistoryReload = true; this.skipConversationHistoryReload = true;

View File

@ -1601,6 +1601,19 @@ export const uiMethods = {
const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' }); const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' });
const result = await resp.json(); const result = await resp.json();
if (result.success) { if (result.success) {
if (typeof result.run_mode === 'string') {
this.runMode = result.run_mode;
this.thinkingMode =
typeof result.thinking_mode === 'boolean'
? result.thinking_mode
: result.run_mode !== 'fast';
} else if (typeof result.thinking_mode === 'boolean') {
this.thinkingMode = result.thinking_mode;
this.runMode = result.thinking_mode ? 'thinking' : 'fast';
}
if (typeof result.model_key === 'string' && result.model_key) {
this.modelSet(result.model_key);
}
this.currentConversationId = convId; this.currentConversationId = convId;
this.currentConversationTitle = result.title || ''; this.currentConversationTitle = result.title || '';
this.titleReady = true; this.titleReady = true;
@ -1858,8 +1871,14 @@ export const uiMethods = {
}); });
} }
// 应用个性化设置中的默认模型和思考模式 // 仅在“无当前对话”的新会话入口应用默认模型/模式;
if (personalizationStore.loaded) { // 对于已存在对话(含刷新恢复/URL指定对话必须保留对话自身设置。
const statusConversationIdForDefaults = statusData?.conversation?.current_id;
const hasActiveConversation =
typeof statusConversationIdForDefaults === 'string' &&
statusConversationIdForDefaults.length > 0 &&
!statusConversationIdForDefaults.startsWith('temp_');
if (personalizationStore.loaded && !hasActiveConversation && !this.currentConversationId) {
const defaultRunMode = personalizationStore.form.default_run_mode; const defaultRunMode = personalizationStore.form.default_run_mode;
const defaultModel = personalizationStore.form.default_model; const defaultModel = personalizationStore.form.default_model;

View File

@ -281,6 +281,9 @@ export function scrollToBottom(ctx: ScrollContext, options?: ScrollOptions) {
let settleCount = 0; let settleCount = 0;
let lastHeight = -1; let lastHeight = -1;
const settleStartTs = Date.now();
const settleMaxCount = drawingActive ? 30 : 12;
const settleMaxDurationMs = drawingActive ? 3200 : 1200;
const settleScroll = () => { const settleScroll = () => {
settleCount += 1; settleCount += 1;
messagesArea.scrollTop = messagesArea.scrollTop =
@ -290,11 +293,15 @@ export function scrollToBottom(ctx: ScrollContext, options?: ScrollOptions) {
const remain = currentHeight - messagesArea.scrollTop - messagesArea.clientHeight; const remain = currentHeight - messagesArea.scrollTop - messagesArea.clientHeight;
const heightStable = Math.abs(currentHeight - lastHeight) <= 2; const heightStable = Math.abs(currentHeight - lastHeight) <= 2;
const atBottom = Math.abs(remain) <= 2; const atBottom = Math.abs(remain) <= 2;
const settleElapsed = Date.now() - settleStartTs;
const shouldContinue =
!atBottom && settleCount < settleMaxCount && settleElapsed < settleMaxDurationMs;
scrollDebugLog( scrollDebugLog(
'scrollToBottom:settle', 'scrollToBottom:settle',
{ {
traceId, traceId,
settleCount, settleCount,
settleElapsed,
currentHeight, currentHeight,
lastHeight, lastHeight,
remain, remain,
@ -307,7 +314,7 @@ export function scrollToBottom(ctx: ScrollContext, options?: ScrollOptions) {
80 80
); );
lastHeight = currentHeight; lastHeight = currentHeight;
if (settleCount < 7 && (!heightStable || !atBottom)) { if (shouldContinue) {
if ( if (
(drawingActive || options?.force) && (drawingActive || options?.force) &&
(settleCount === 1 || settleCount === 4 || settleCount === 6) (settleCount === 1 || settleCount === 4 || settleCount === 6)

View File

@ -937,7 +937,7 @@ body[data-theme='dark'] .more-icon {
margin: 14px auto; margin: 14px auto;
border: 1px solid var(--claude-border); border: 1px solid var(--claude-border);
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: auto;
background: transparent; background: transparent;
box-shadow: var(--claude-shadow); box-shadow: var(--claude-shadow);
} }