feat: v0.5.4 多模态增强 — 多轮图片记忆 + 多模态总开关 + DeepSeek vision 模型支持
CI / 类型检查 + Lint + 单元测试 (push) Failing after 5m47s
CI / 全量测试 (Electron ABI) (push) Failing after 5m25s
CI / 产物编译验证 (push) Successful in 10m4s

多轮图片记忆:
- 此前历史轮次的图片不回传 LLM(attachments 仅存压缩 preview,历史组装
  时被丢弃)— 跨轮对话中模型对图片内容"失忆"
- 修复:SessionSummaryService.buildHistoryMessages 从持久化的 attachments
  恢复 images(type=image 的 preview base64),历史图片随上下文回传
- Token 控制:最多注入最近 10 张(MAX_HISTORY_IMAGES,从最新消息向前
  收集)— 每张 1024px 压缩图约数百至千余 token,无上限会吃满上下文
- 摘要区间(summarizedUntilRowid 之前)的图片不恢复,符合滚动摘要语义

多模态总开关 llm.multimodalEnabled(默认关闭):
- 新配置项:CONFIG_DEFAULTS 种子 + 设置弹框 LLM 配置 Switch +
  首次引导向导 LLM 步骤 Switch(含说明文案)
- 上传入口双重判断:总开关 × 模型能力 — 未开启时即使模型支持多模态
  也不能上传图片(ChatInput 的选择/拖拽/粘贴统一拦截,Toast 区分
  "开关未开启"与"当前模型不支持"两种原因)
- 保存成功后同步 Agent Store 立即生效;App 启动时随 setProvider 加载

DeepSeek vision 模型支持:
- 新增 deepseek-v4-flash-vision-exp(OpenAI image_url content parts 格式,
  128K 上下文 / 8K 输出)
- adapter 按 isVisionModel() 判断:vision 模型将带 images 的消息转换为
  [{type:'text'},{type:'image_url'}] parts;非 vision 模型保持 images
  静默丢弃(防 API 400)

测试(236 → 243 用例):
- 多轮图片记忆 ×3(session-summary.test.ts):历史 attachments 恢复
  images / 上限 10 张从最新向前 / 摘要区间图片不恢复
- DeepSeek vision 请求格式 ×4(deepseek-vision.test.ts,契约级 mock
  fetch 断言请求体):image_url parts 转换 / 非 vision 模型丢弃 /
  max_tokens 钳制 8192 / 无图不转换
- 测试顺序修正:多轮图片用例置于 describe 末尾(插入新行消耗全局自增
  rowid,插在中间会破坏既有用例对 rowid 数值的断言)

文档: README 同步(DeepSeek 模型表 + vision 多模态列、llm.multimodalEnabled
配置项、多轮图片记忆特性行、243 用例数)

验证: lint 0 / typecheck 双工程 0 / test:electron 243 全过 / build 成功
This commit is contained in:
2026-08-21 23:00:20 +08:00
parent 3a30e8f5b4
commit 4ee5100661
12 changed files with 1363 additions and 353 deletions
+241 -116
View File
@@ -39,8 +39,8 @@ export interface AttachmentInfo {
name: string;
type: 'image' | 'text' | 'other';
size: number;
preview?: string; // 图片 base64 data URL
textContent?: string; // 文本文件内容
preview?: string; // 图片 base64 data URL
textContent?: string; // 文本文件内容
}
export interface ChatMessage {
@@ -133,13 +133,19 @@ interface AgentState {
provider: string;
model: string;
contextWindow: number;
// v0.5.4: 多模态总开关(llm.multimodalEnabled)— 未开启时禁止上传图片
multimodalEnabled: boolean;
// Actions
setCurrentSession: (id: string | null) => void;
setMessages: (messages: ChatMessage[]) => void;
addMessage: (message: ChatMessage) => void;
updateMessage: (id: string, updates: Partial<ChatMessage>) => void;
sendMessage: (content: string, images?: Array<{ url: string; detail?: 'low' | 'high' | 'auto' }>, attachments?: AttachmentInfo[]) => void;
sendMessage: (
content: string,
images?: Array<{ url: string; detail?: 'low' | 'high' | 'auto' }>,
attachments?: AttachmentInfo[],
) => void;
updateLastAssistantMessage: (delta: string) => void;
setAgentStatus: (status: AgentStatus) => void;
setStreaming: (streaming: boolean) => void;
@@ -159,6 +165,8 @@ interface AgentState {
*/
applyCompression: (savedTokens: number) => void;
setProvider: (provider: string, model: string) => void;
// v0.5.4: 多模态开关 setter(App 初始化与设置保存时同步)
setMultimodalEnabled: (enabled: boolean) => void;
setMaxIterations: (max: number) => void;
setCurrentIteration: (n: number) => void;
saveTraceData: () => void;
@@ -183,7 +191,13 @@ export const useAgentStore = create<AgentState>((set, get) => ({
agentStatus: 'idle',
currentIteration: 0,
maxIterations: 20,
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, lastInputTokens: 0, lastCompressedSaved: 0 },
tokenUsage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
lastInputTokens: 0,
lastCompressedSaved: 0,
},
traceSteps: [],
isStreaming: false,
configLoaded: false,
@@ -192,104 +206,158 @@ export const useAgentStore = create<AgentState>((set, get) => ({
provider: '',
model: '',
contextWindow: 0,
// v0.5.4: 多模态总开关(默认关闭,setProvider 时从配置加载)
multimodalEnabled: false,
// ===== Actions =====
setCurrentSession: (id) => {
set({ currentSessionId: id, messages: [], traceSteps: [], currentIteration: 0, tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, lastInputTokens: 0, lastCompressedSaved: 0 }, agentStatus: 'idle', isStreaming: false, currentRunId: null });
set({
currentSessionId: id,
messages: [],
traceSteps: [],
currentIteration: 0,
tokenUsage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
lastInputTokens: 0,
lastCompressedSaved: 0,
},
agentStatus: 'idle',
isStreaming: false,
currentRunId: null,
});
// 从数据库加载该会话的消息
if (id && window.metona?.sessions?.getMessages) {
window.metona.sessions.getMessages(id).then((msgs) => {
// v0.3.0 修复: 竞态保护 — 快速切换会话时,旧请求返回后不再覆盖当前会话消息
if (get().currentSessionId !== id) return;
const messages = (msgs as Array<{
id: string; role: string; content: string;
reasoningContent?: string; toolCalls?: unknown[];
toolResult?: unknown;
attachments?: Array<{ id: string; name: string; type: string; size: number; preview?: string; textContent?: string }>;
iteration?: number;
timestamp: number;
}>)
// v0.3.0 修复: 过滤掉 tool 消息 — tool 结果已包含在 assistant 消息的 toolCalls 中
// 独立的 tool 消息只用于 LLM API 上下文,不需要在前端显示为独立卡片
.filter((m) => m.role !== 'tool')
.map((m) => ({
id: m.id,
role: m.role as ChatMessage['role'],
content: m.content,
reasoningContent: m.reasoningContent,
toolCalls: m.toolCalls as ToolCallInfo[] | undefined,
attachments: m.attachments as AttachmentInfo[] | undefined,
iteration: m.iteration,
timestamp: m.timestamp,
}));
set({ messages });
}).catch((err) => { console.error('[AgentStore]', err); });
window.metona.sessions
.getMessages(id)
.then((msgs) => {
// v0.3.0 修复: 竞态保护 — 快速切换会话时,旧请求返回后不再覆盖当前会话消息
if (get().currentSessionId !== id) return;
const messages = (
msgs as Array<{
id: string;
role: string;
content: string;
reasoningContent?: string;
toolCalls?: unknown[];
toolResult?: unknown;
attachments?: Array<{
id: string;
name: string;
type: string;
size: number;
preview?: string;
textContent?: string;
}>;
iteration?: number;
timestamp: number;
}>
)
// v0.3.0 修复: 过滤掉 tool 消息 — tool 结果已包含在 assistant 消息的 toolCalls 中
// 独立的 tool 消息只用于 LLM API 上下文,不需要在前端显示为独立卡片
.filter((m) => m.role !== 'tool')
.map((m) => ({
id: m.id,
role: m.role as ChatMessage['role'],
content: m.content,
reasoningContent: m.reasoningContent,
toolCalls: m.toolCalls as ToolCallInfo[] | undefined,
attachments: m.attachments as AttachmentInfo[] | undefined,
iteration: m.iteration,
timestamp: m.timestamp,
}));
set({ messages });
})
.catch((err) => {
console.error('[AgentStore]', err);
});
}
// 从数据库加载该会话的 trace 步骤和 token 用量
if (id && window.metona?.sessions?.getTrace) {
window.metona.sessions.getTrace(id).then((data) => {
// v0.3.0 修复: 竞态保护 — 快速切换会话时,旧请求返回后不再覆盖当前会话 trace
if (get().currentSessionId !== id) return;
if (data) {
if (data.traceSteps) {
// 兼容旧数据:为缺少 id/states 的 trace 步骤补全
const steps = (data.traceSteps as TraceStep[]).map((t, i) => ({
...t,
id: t.id ?? `trace_legacy_${t.iteration}_${t.state}_${i}`,
states: t.states ?? [t.state],
}));
set({ traceSteps: steps });
window.metona.sessions
.getTrace(id)
.then((data) => {
// v0.3.0 修复: 竞态保护 — 快速切换会话时,旧请求返回后不再覆盖当前会话 trace
if (get().currentSessionId !== id) return;
if (data) {
if (data.traceSteps) {
// 兼容旧数据:为缺少 id/states 的 trace 步骤补全
const steps = (data.traceSteps as TraceStep[]).map((t, i) => ({
...t,
id: t.id ?? `trace_legacy_${t.iteration}_${t.state}_${i}`,
states: t.states ?? [t.state],
}));
set({ traceSteps: steps });
}
// v0.3.18 修复: 兼容旧数据 — 旧 tokenUsage 没有 lastInputTokens/lastCompressedSaved 字段
if (data.tokenUsage) {
const old = data.tokenUsage as Partial<TokenUsage>;
set({
tokenUsage: {
inputTokens: old.inputTokens ?? 0,
outputTokens: old.outputTokens ?? 0,
totalTokens: old.totalTokens ?? 0,
lastInputTokens: old.lastInputTokens ?? 0,
lastCompressedSaved: old.lastCompressedSaved ?? 0,
},
});
}
}
// v0.3.18 修复: 兼容旧数据 — 旧 tokenUsage 没有 lastInputTokens/lastCompressedSaved 字段
if (data.tokenUsage) {
const old = data.tokenUsage as Partial<TokenUsage>;
set({
tokenUsage: {
inputTokens: old.inputTokens ?? 0,
outputTokens: old.outputTokens ?? 0,
totalTokens: old.totalTokens ?? 0,
lastInputTokens: old.lastInputTokens ?? 0,
lastCompressedSaved: old.lastCompressedSaved ?? 0,
},
});
}
}
}).catch((err) => { console.error('[AgentStore]', err); });
})
.catch((err) => {
console.error('[AgentStore]', err);
});
}
},
setMessages: (messages) => set({ messages }),
addMessage: (message) =>
set((s) => ({ messages: [...s.messages, message] })),
addMessage: (message) => set((s) => ({ messages: [...s.messages, message] })),
updateMessage: (id, updates) =>
set((s) => ({
messages: s.messages.map((m) =>
m.id === id ? { ...m, ...updates } : m,
),
messages: s.messages.map((m) => (m.id === id ? { ...m, ...updates } : m)),
})),
sendMessage: async (content: string, images?: Array<{ url: string; detail?: 'low' | 'high' | 'auto' }>, attachments?: AttachmentInfo[]) => {
sendMessage: async (
content: string,
images?: Array<{ url: string; detail?: 'low' | 'high' | 'auto' }>,
attachments?: AttachmentInfo[],
) => {
let sessionId = get().currentSessionId;
// v0.3.18 修复: 工具未就绪时阻止发送,避免 MCP 工具不可用
if (!get().toolsReady) {
import('@metona-team/metona-toast').then((mod) => mod.default.warning('工具正在加载中,请稍候...')).catch(() => {});
import('@metona-team/metona-toast')
.then((mod) => mod.default.warning('工具正在加载中,请稍候...'))
.catch(() => {});
return;
}
// 没有当前会话时自动创建
if (!sessionId && window.metona?.sessions?.create) {
try {
const session = await window.metona.sessions.create() as { id: string; title: string; createdAt: number; updatedAt: number; messageCount: number; pinned: boolean; archived: boolean };
const session = (await window.metona.sessions.create()) as {
id: string;
title: string;
createdAt: number;
updatedAt: number;
messageCount: number;
pinned: boolean;
archived: boolean;
};
useSessionStore.getState().addSession({
id: session.id, title: session.title, createdAt: session.createdAt,
updatedAt: session.updatedAt, messageCount: session.messageCount,
pinned: session.pinned, archived: session.archived,
id: session.id,
title: session.title,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
messageCount: session.messageCount,
pinned: session.pinned,
archived: session.archived,
});
sessionId = session.id;
set({ currentSessionId: sessionId });
@@ -305,7 +373,9 @@ export const useAgentStore = create<AgentState>((set, get) => ({
timestamp: Date.now(),
});
// 额外弹 toast 作为通知,确保用户感知(system message 仅在聊天流内可见)
import('@metona-team/metona-toast').then((mod) => mod.default.error(`无法创建会话:${(err as Error).message}`)).catch(() => {});
import('@metona-team/metona-toast')
.then((mod) => mod.default.error(`无法创建会话:${(err as Error).message}`))
.catch(() => {});
return;
}
}
@@ -313,7 +383,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
const userMessage: ChatMessage = {
id: genMsgId('user'),
role: 'user',
content, // 用户可见内容(纯文本)
content, // 用户可见内容(纯文本)
timestamp: Date.now(),
attachments: attachments && attachments.length > 0 ? attachments : undefined,
};
@@ -327,19 +397,30 @@ export const useAgentStore = create<AgentState>((set, get) => ({
// 方案 A: 不清空 traceSteps,避免前一条消息的 trace 被永久覆盖
// TraceViewer 按 runId 过滤显示,只展示当前 run 的 steps
// 历史 trace 仍在 DB 中,切换会话回来可恢复
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, lastInputTokens: 0, lastCompressedSaved: 0 },
tokenUsage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
lastInputTokens: 0,
lastCompressedSaved: 0,
},
}));
// 自动更新会话标题为用户第一条消息
if (sessionId && window.metona?.sessions?.getMessages) {
window.metona.sessions.getMessages(sessionId).then((msgs) => {
if (msgs.length <= 1) {
// 这是第一条消息,更新标题
const title = content.length > 30 ? content.slice(0, 30) + '...' : content;
window.metona?.sessions?.rename(sessionId, title);
useSessionStore.getState().updateSession(sessionId, { title });
}
}).catch((err) => { console.error('[AgentStore]', err); });
window.metona.sessions
.getMessages(sessionId)
.then((msgs) => {
if (msgs.length <= 1) {
// 这是第一条消息,更新标题
const title = content.length > 30 ? content.slice(0, 30) + '...' : content;
window.metona?.sessions?.rename(sessionId, title);
useSessionStore.getState().updateSession(sessionId, { title });
}
})
.catch((err) => {
console.error('[AgentStore]', err);
});
}
if (sessionId && window.metona?.agent?.sendMessage) {
@@ -360,15 +441,16 @@ export const useAgentStore = create<AgentState>((set, get) => ({
// 非图片文件:JSON 结构化,原始文本内容(不编码)
const ext = att.name.split('.').pop() ?? 'unknown';
const rawContent = att.textContent ?? '';
parts.push(JSON.stringify({
file_name: att.name,
file_type: ext,
content: rawContent,
}));
parts.push(
JSON.stringify({
file_name: att.name,
file_type: ext,
content: rawContent,
}),
);
}
llmContent = parts.length > 0
? (content ? content + '\n\n' : '') + parts.join('\n')
: content;
llmContent =
parts.length > 0 ? (content ? content + '\n\n' : '') + parts.join('\n') : content;
}
const messageWithImages = {
@@ -386,7 +468,9 @@ export const useAgentStore = create<AgentState>((set, get) => ({
timestamp: Date.now(),
});
// 额外弹 toast 作为通知,确保用户感知(system message 仅在聊天流内可见)
import('@metona-team/metona-toast').then((mod) => mod.default.error(`消息发送失败:${(err as Error).message}`)).catch(() => {});
import('@metona-team/metona-toast')
.then((mod) => mod.default.error(`消息发送失败:${(err as Error).message}`))
.catch(() => {});
});
}
},
@@ -422,22 +506,17 @@ export const useAgentStore = create<AgentState>((set, get) => ({
setCurrentRunId: (runId) => set({ currentRunId: runId }),
addTraceStep: (step) =>
set((s) => ({ traceSteps: [...s.traceSteps, step] })),
addTraceStep: (step) => set((s) => ({ traceSteps: [...s.traceSteps, step] })),
updateTraceStep: (iteration, updates) =>
set((s) => ({
traceSteps: s.traceSteps.map((t) =>
t.iteration === iteration ? { ...t, ...updates } : t,
),
traceSteps: s.traceSteps.map((t) => (t.iteration === iteration ? { ...t, ...updates } : t)),
})),
// L-2: 按 ID 精确匹配 traceStep(避免 iteration 碰撞)
updateTraceStepById: (id, updates) =>
set((s) => ({
traceSteps: s.traceSteps.map((t) =>
t.id === id ? { ...t, ...updates } : t,
),
traceSteps: s.traceSteps.map((t) => (t.id === id ? { ...t, ...updates } : t)),
})),
updateLastTraceStep: (updates) =>
@@ -448,8 +527,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
return { traceSteps: steps };
}),
updateTokenUsage: (usage) =>
set((s) => ({ tokenUsage: { ...s.tokenUsage, ...usage } })),
updateTokenUsage: (usage) => set((s) => ({ tokenUsage: { ...s.tokenUsage, ...usage } })),
// v0.3.18 修复: 应用上下文压缩事件,记录节省的 token 数
// 压缩后下一轮 LLM 调用的 inputTokens 会大幅下降,lastInputTokens 会自动反映
@@ -459,23 +537,43 @@ export const useAgentStore = create<AgentState>((set, get) => ({
setProvider: (provider, model) => {
// L-16 修复: 使用命名常量替代魔法数字
// v0.3.1: DeepSeek/Agnes 不再固定 1M,从配置读取
const initialCtx = provider === 'ollama' ? DEFAULT_OLLAMA_CONTEXT_WINDOW : DEFAULT_CLOUD_CONTEXT_WINDOW;
const initialCtx =
provider === 'ollama' ? DEFAULT_OLLAMA_CONTEXT_WINDOW : DEFAULT_CLOUD_CONTEXT_WINDOW;
set({ provider, model, contextWindow: initialCtx });
// v0.5.4: 多模态开关随 provider/model 状态一并加载(App 初始化/设置保存共用此路径)
if (window.metona?.config?.get) {
window.metona.config
.get('llm.multimodalEnabled')
.then((v) => {
set({ multimodalEnabled: v === true });
})
.catch(() => {
/* 读取失败保持默认 false */
});
}
// v0.3.1: 异步读取实际配置(所有 Provider)
if (window.metona?.config?.get) {
// Ollama 用 ollama.numCtxDeepSeek 用 deepseek.contextWindowAgnes 用 agnes.contextWindow
const configKey = provider === 'ollama' ? 'ollama.numCtx' : `${provider}.contextWindow`;
// P2-12 修复: 竞态保护 — 快速切换 provider 时,旧 Promise resolve 不覆盖新值
const expectedProvider = provider;
window.metona.config.get(configKey).then((v) => {
if (get().provider !== expectedProvider) return; // provider 已切换,丢弃旧结果
if (v != null && typeof v === 'number' && v > 0) {
set({ contextWindow: v });
}
}).catch((err) => { console.error('[AgentStore]', err); });
window.metona.config
.get(configKey)
.then((v) => {
if (get().provider !== expectedProvider) return; // provider 已切换,丢弃旧结果
if (v != null && typeof v === 'number' && v > 0) {
set({ contextWindow: v });
}
})
.catch((err) => {
console.error('[AgentStore]', err);
});
}
},
// v0.5.4: 多模态开关 setter — 设置保存/开关切换时同步(立即生效,ChatInput 消费)
setMultimodalEnabled: (enabled) => set({ multimodalEnabled: enabled }),
setMaxIterations: (max) => set({ maxIterations: max }),
setCurrentIteration: (currentIteration) => set({ currentIteration }),
@@ -483,7 +581,11 @@ export const useAgentStore = create<AgentState>((set, get) => ({
saveTraceData: () => {
const { currentSessionId, traceSteps, tokenUsage } = get();
if (currentSessionId && window.metona?.sessions?.saveTrace) {
window.metona.sessions.saveTrace(currentSessionId, { traceSteps, tokenUsage }).catch((err) => { console.error('[AgentStore]', err); });
window.metona.sessions
.saveTrace(currentSessionId, { traceSteps, tokenUsage })
.catch((err) => {
console.error('[AgentStore]', err);
});
}
},
@@ -492,7 +594,13 @@ export const useAgentStore = create<AgentState>((set, get) => ({
messages: [],
agentStatus: 'idle',
currentIteration: 0,
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, lastInputTokens: 0, lastCompressedSaved: 0 },
tokenUsage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
lastInputTokens: 0,
lastCompressedSaved: 0,
},
traceSteps: [],
isStreaming: false,
currentRunId: null,
@@ -504,7 +612,9 @@ export const useAgentStore = create<AgentState>((set, get) => ({
// 旧 run 的 DONE 到达时会匹配 currentRunId,处理后清除;新 run 的 INIT 会覆盖
set({ agentStatus: 'idle', isStreaming: false });
if (sessionId && window.metona?.agent?.abortSession) {
window.metona.agent.abortSession(sessionId).catch((err) => { console.error('[AgentStore]', err); });
window.metona.agent.abortSession(sessionId).catch((err) => {
console.error('[AgentStore]', err);
});
}
},
@@ -512,7 +622,9 @@ export const useAgentStore = create<AgentState>((set, get) => ({
editAndResend: async (messageId, newContent) => {
const { messages, currentSessionId, isStreaming } = get();
if (isStreaming) {
import('@metona-team/metona-toast').then((mod) => mod.default.warning('Agent 正在回复中,请先中断再编辑重发')).catch(() => {});
import('@metona-team/metona-toast')
.then((mod) => mod.default.warning('Agent 正在回复中,请先中断再编辑重发'))
.catch(() => {});
return;
}
const idx = messages.findIndex((m) => m.id === messageId);
@@ -526,7 +638,9 @@ export const useAgentStore = create<AgentState>((set, get) => ({
await window.metona.sessions.truncateAfter(currentSessionId, messageId, true);
} catch (err) {
console.error('[AgentStore] truncateAfter failed:', err);
import('@metona-team/metona-toast').then((mod) => mod.default.error('消息截断失败,无法重发')).catch(() => {});
import('@metona-team/metona-toast')
.then((mod) => mod.default.error('消息截断失败,无法重发'))
.catch(() => {});
return;
}
}
@@ -556,13 +670,22 @@ export const useAgentStore = create<AgentState>((set, get) => ({
regenerate: async () => {
const { messages, currentSessionId, isStreaming } = get();
if (isStreaming) {
import('@metona-team/metona-toast').then((mod) => mod.default.warning('Agent 正在回复中,无法重新生成')).catch(() => {});
import('@metona-team/metona-toast')
.then((mod) => mod.default.warning('Agent 正在回复中,无法重新生成'))
.catch(() => {});
return;
}
// 找最后一条用户消息
const lastUserIdx = messages.length - 1 - [...messages].reverse().findIndex((m) => m.role === 'user');
if (lastUserIdx < 0 || lastUserIdx >= messages.length || messages[lastUserIdx].role !== 'user') {
import('@metona-team/metona-toast').then((mod) => mod.default.info('没有可重新生成的用户消息')).catch(() => {});
const lastUserIdx =
messages.length - 1 - [...messages].reverse().findIndex((m) => m.role === 'user');
if (
lastUserIdx < 0 ||
lastUserIdx >= messages.length ||
messages[lastUserIdx].role !== 'user'
) {
import('@metona-team/metona-toast')
.then((mod) => mod.default.info('没有可重新生成的用户消息'))
.catch(() => {});
return;
}
const lastUser = messages[lastUserIdx];
@@ -573,7 +696,9 @@ export const useAgentStore = create<AgentState>((set, get) => ({
await window.metona.sessions.truncateAfter(currentSessionId, lastUser.id, true);
} catch (err) {
console.error('[AgentStore] truncateAfter failed:', err);
import('@metona-team/metona-toast').then((mod) => mod.default.error('消息截断失败,无法重新生成')).catch(() => {});
import('@metona-team/metona-toast')
.then((mod) => mod.default.error('消息截断失败,无法重新生成'))
.catch(() => {});
return;
}
}