feat: v1.1.0 — 上下文引擎重构 + 技能系统升级 + SOUL.md 支持
- 版本号更新: 1.0.0 → 1.1.0,全量同步 package.json/lock/README/docs/UI - 上下文长度自动检测: 切换模型时从 model_info 读取实际 context_length - SOUL.md 支持: 每次发送自动扫描工作空间 SOUL.md,注入为首条 system 消息且不可压缩 - 系统提示词卡片: AI 回复顶部可折叠展示实际发送给模型的完整 system prompt - Token 校准: 利用 Ollama 返回的实际计数动态修正估算器(EMA) - 消息重要性评分: 纯规则打分,trim/summarize 按重要性而非时间顺序 - 结构化压缩: LLM 压缩输出 JSON 格式(topics/decisions/pendingTasks/constraints) - 技能系统 v1.1: 语义匹配(embedding) + 参数自优化 + 未使用衰减 + 技能链合并 - 修复: 100+ TypeScript strict 模式编译错误清零
This commit is contained in:
@@ -6,6 +6,7 @@ import { state, KEYS } from '../state/state.js';
|
||||
import { logError, logSession } from '../services/log-service.js';
|
||||
import { marked } from '../utils/marked-config.js';
|
||||
import { escapeHtml, formatTime } from '../utils/utils.js';
|
||||
import { estimateTokens } from '../services/context-manager.js';
|
||||
import type { ChatSession, ChatMessage, ToolCallRecord } from '../types.js';
|
||||
|
||||
function getFileIcon(filename: string): string {
|
||||
@@ -30,6 +31,31 @@ let scrollBtnEl: HTMLElement;
|
||||
let autoScroll = true;
|
||||
let currentPlaceholder: HTMLElement | null = null;
|
||||
|
||||
/** 渲染可折叠系统提示词卡片 */
|
||||
let _sysPromptIdCounter = 0;
|
||||
function renderSystemPromptCard(sysPrompt: string): string {
|
||||
const id = `sysprompt_${++_sysPromptIdCounter}`;
|
||||
const tokenEstimate = estimateTokens(sysPrompt);
|
||||
const preview = sysPrompt.length > 120 ? sysPrompt.slice(0, 120).replace(/\n/g, ' ') + '…' : sysPrompt.replace(/\n/g, ' ');
|
||||
return `<div class="system-prompt-card">
|
||||
<div class="system-prompt-header" onclick="
|
||||
var el=document.getElementById('${id}');
|
||||
var chev=this.querySelector('.sp-chevron');
|
||||
el.style.display=el.style.display==='none'?'':'none';
|
||||
chev.classList.toggle('sp-expanded');
|
||||
">
|
||||
<span class="sp-icon">📋</span>
|
||||
<span class="sp-title">系统提示词</span>
|
||||
<span class="sp-tokens">~${tokenEstimate} tokens</span>
|
||||
<span class="sp-preview">${escapeHtml(preview)}</span>
|
||||
<svg class="sp-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</div>
|
||||
<div class="system-prompt-body" id="${id}" style="display:none;">
|
||||
<pre>${escapeHtml(sysPrompt)}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export function initChatArea(): void {
|
||||
chatAreaEl = document.querySelector('#chatArea')!;
|
||||
messagesContainerEl = document.querySelector('#messagesContainer')!;
|
||||
@@ -118,6 +144,12 @@ export function appendMessageDOM(msg: ChatMessage, index: number): void {
|
||||
const safeContent = (msg.content != null) ? String(msg.content) : '';
|
||||
|
||||
if (msg.role === 'assistant') {
|
||||
// ── 系统提示词折叠卡片(每条助理消息顶部展示)──
|
||||
const sysPrompt = state.get<string>('_lastSystemPrompt', '');
|
||||
if (sysPrompt) {
|
||||
contentHtml += renderSystemPromptCard(sysPrompt);
|
||||
}
|
||||
|
||||
if (msg.think) {
|
||||
contentHtml += `
|
||||
<div class="think-block">
|
||||
@@ -273,6 +305,21 @@ export function updateLastAssistantMessage(
|
||||
const loadingText = lastMsg.querySelector('.loading-text');
|
||||
if (loadingDots) loadingDots.remove();
|
||||
if (loadingText) loadingText.remove();
|
||||
|
||||
// 流式消息首次转正:注入系统提示词折叠卡片
|
||||
const sysPrompt = state.get<string>('_lastSystemPrompt', '');
|
||||
if (sysPrompt && !lastMsg.querySelector('.system-prompt-card')) {
|
||||
const msgBody = lastMsg.querySelector('.msg-body');
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = renderSystemPromptCard(sysPrompt);
|
||||
const card = tempDiv.firstElementChild!;
|
||||
const thinkBlock = msgBody?.querySelector('.think-block');
|
||||
if (thinkBlock) {
|
||||
msgBody!.insertBefore(card, thinkBlock);
|
||||
} else {
|
||||
msgBody!.insertBefore(card, msgBody!.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (model) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder, resetCurrentPlaceholder
|
||||
} from './chat-area.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { addToolCard, updateToolCard, clearToolCardsExternal, clearTerminalExternal } from './workspace-panel.js';
|
||||
import { addToolCard, updateToolCard, clearToolCardsExternal, clearTerminalExternal, getWorkspaceDirPath } from './workspace-panel.js';
|
||||
import { ChatDB } from '../db/chat-db.js';
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
import { runAgentLoop } from '../services/agent-engine.js';
|
||||
@@ -94,9 +94,7 @@ export function initInputArea(): void {
|
||||
const bridge = getBridge();
|
||||
if (!bridge) return;
|
||||
const paths = await bridge.dialog.openFile({
|
||||
filters: [{ name: '图片', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'] }],
|
||||
properties: ['openFile'],
|
||||
multiple: false
|
||||
filters: [{ name: '图片', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'] }]
|
||||
});
|
||||
if (!paths || paths.length === 0) return;
|
||||
await handleImagePaths(paths);
|
||||
@@ -141,7 +139,7 @@ async function handleImagePaths(paths: string[]): Promise<void> {
|
||||
for (const filePath of paths) {
|
||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||
try {
|
||||
const result = await bridge.fs.readFileBase64(filePath);
|
||||
const result = await bridge!.fs.readFileBase64(filePath);
|
||||
if (!result.success) {
|
||||
appendSystemMessage(`❌ 读取 ${name} 失败: ${result.error}`);
|
||||
continue;
|
||||
@@ -186,7 +184,7 @@ async function handleTextFilePaths(paths: string[]): Promise<void> {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const result = await bridge.fs.readFile(filePath);
|
||||
const result = await bridge!.fs.readFile(filePath);
|
||||
if (!result.success) {
|
||||
appendSystemMessage(`❌ 读取 ${name} 失败: ${result.error}`);
|
||||
continue;
|
||||
@@ -308,7 +306,7 @@ async function handleRetry(): Promise<void> {
|
||||
...(retryThinkContent && { think: retryThinkContent }),
|
||||
...(toolCallRecords?.length && { toolCalls: toolCallRecords }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (s: any) => ({
|
||||
...s, messages: [...s.messages, prevMsg], updatedAt: Date.now()
|
||||
}));
|
||||
renderMessages();
|
||||
@@ -353,7 +351,7 @@ async function handleRetry(): Promise<void> {
|
||||
...(loopStats?.prompt_eval_count && { prompt_eval_count: loopStats.prompt_eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (s: any) => ({
|
||||
...s, messages: [...s.messages, lastMsg], updatedAt: Date.now()
|
||||
}));
|
||||
}
|
||||
@@ -367,7 +365,7 @@ async function handleRetry(): Promise<void> {
|
||||
...(loopStats?.prompt_eval_count && { prompt_eval_count: loopStats.prompt_eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (s: any) => ({
|
||||
...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now()
|
||||
}));
|
||||
updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null, undefined, Date.now(), toolRecords?.length ? toolRecords : undefined);
|
||||
@@ -644,7 +642,7 @@ export async function sendMessage(): Promise<void> {
|
||||
}
|
||||
|
||||
const isFirstMsg = currentSession.messages.length === 0;
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
...(isFirstMsg && {
|
||||
title: truncate(text || (pendingFiles.length > 0 ? `[文件: ${pendingFiles.map(f => f.name).join(', ')}]` : '[图片消息]'), 30),
|
||||
@@ -682,7 +680,7 @@ export async function sendMessage(): Promise<void> {
|
||||
|
||||
let assistantContent = '';
|
||||
let thinkContent = '';
|
||||
let finalStats: { eval_count?: number; prompt_eval_count?: number; total_duration?: number } | null = null;
|
||||
let finalStats: any = null;
|
||||
let modelName = '';
|
||||
|
||||
try {
|
||||
@@ -697,6 +695,20 @@ export async function sendMessage(): Promise<void> {
|
||||
};
|
||||
if (numCtx) chatParams.options = { num_ctx: numCtx, temperature };
|
||||
|
||||
// ── 扫描工作空间 SOUL.md ──
|
||||
let soulContent = '';
|
||||
const workspaceDir = getWorkspaceDirPath();
|
||||
if (workspaceDir) {
|
||||
try {
|
||||
const soulResult = await window.metonaDesktop?.workspace.readFile(
|
||||
workspaceDir.replace(/\/+$/, '') + '/SOUL.md'
|
||||
);
|
||||
if (soulResult?.success && soulResult.content) {
|
||||
soulContent = `[SOUL.md] ${soulResult.content}`;
|
||||
}
|
||||
} catch { /* SOUL.md 不存在,静默跳过 */ }
|
||||
}
|
||||
|
||||
// ── Agent 记忆注入 ──
|
||||
if (isMemoryEnabled()) {
|
||||
const userMessage = text || (msgsToAdd.find(m => m.role === 'user')?.content || '');
|
||||
@@ -704,7 +716,7 @@ export async function sendMessage(): Promise<void> {
|
||||
const relevantMemories = searchMemories(userMessage, 6);
|
||||
if (relevantMemories.length > 0) {
|
||||
const memoryContext = buildMemoryContext(relevantMemories);
|
||||
chatParams.system = memoryContext;
|
||||
chatParams.system = (soulContent ? soulContent + '\n\n' : '') + memoryContext;
|
||||
for (const m of relevantMemories) {
|
||||
await markMemoryUsed(m.id);
|
||||
}
|
||||
@@ -712,6 +724,12 @@ export async function sendMessage(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (soulContent && !chatParams.system) {
|
||||
chatParams.system = soulContent;
|
||||
}
|
||||
|
||||
// 存入 state 供 chat-area 渲染系统提示词卡片
|
||||
state.set('_lastSystemPrompt', chatParams.system || '');
|
||||
|
||||
await api.chatStream(chatParams as any, (chunk: OllamaStreamChunk) => {
|
||||
if (chunk.message) {
|
||||
@@ -752,9 +770,9 @@ export async function sendMessage(): Promise<void> {
|
||||
timestamp: Date.now(),
|
||||
...(modelName && { model: modelName }),
|
||||
...(thinkContent && { think: thinkContent }),
|
||||
...(finalStats && { eval_count: finalStats.eval_count, prompt_eval_count: finalStats.prompt_eval_count, total_duration: finalStats.total_duration })
|
||||
...((finalStats || {}) as any)
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
messages: [...session.messages, assistantMsg],
|
||||
updatedAt: Date.now()
|
||||
@@ -800,7 +818,7 @@ export async function sendMessage(): Promise<void> {
|
||||
...(thinkContent && { think: thinkContent }),
|
||||
stopped: true
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
messages: [...session.messages, partialMsg],
|
||||
updatedAt: Date.now()
|
||||
@@ -829,7 +847,7 @@ export async function sendMessage(): Promise<void> {
|
||||
const contentDiv = placeholder?.querySelector('.msg-content');
|
||||
|
||||
if (assistantContent && contentDiv) {
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
messages: [...session.messages, { role: 'assistant', content: assistantContent + '\n\n`[已中断]`', timestamp: Date.now() } as ChatMessage],
|
||||
updatedAt: Date.now()
|
||||
@@ -864,14 +882,14 @@ export async function sendMessage(): Promise<void> {
|
||||
/** 更新当前会话消息中最近一个 'running' 状态的同名工具记录 */
|
||||
function updateMessageToolRecord(toolName: string, status: 'success' | 'error', result: Record<string, unknown>): void {
|
||||
let updated = false;
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => {
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => {
|
||||
if (!session) return session;
|
||||
const messages = [...session.messages];
|
||||
// 从后往前找最近一条包含该工具 running 状态记录的消息
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (!msg.toolCalls) continue;
|
||||
const idx = msg.toolCalls.findIndex(tc => tc.name === toolName && tc.status === 'running');
|
||||
const idx = msg.toolCalls.findIndex((tc: any) => tc.name === toolName && tc.status === 'running');
|
||||
if (idx !== -1) {
|
||||
const updatedRecords = [...msg.toolCalls];
|
||||
updatedRecords[idx] = { ...updatedRecords[idx], status, result };
|
||||
@@ -919,7 +937,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
}
|
||||
|
||||
const isFirstMsg = currentSession.messages.length === 0;
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
...(isFirstMsg && {
|
||||
title: truncate(text || (pendingFiles.length > 0 ? `[文件: ${pendingFiles.map(f => f.name).join(', ')}]` : '[图片消息]'), 30),
|
||||
@@ -1009,7 +1027,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
...(iterationEvalCount > 0 && { eval_count: iterationEvalCount }),
|
||||
...(iterationDuration > 0 && { total_duration: iterationDuration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
messages: [...session.messages, prevMsg],
|
||||
updatedAt: Date.now()
|
||||
@@ -1075,7 +1093,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
...(loopStats?.prompt_eval_count && { prompt_eval_count: loopStats.prompt_eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
messages: [...session.messages, lastMsg],
|
||||
updatedAt: Date.now()
|
||||
@@ -1094,7 +1112,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
...(loopStats?.prompt_eval_count && { prompt_eval_count: loopStats.prompt_eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
messages: [...session.messages, assistantMsg],
|
||||
updatedAt: Date.now()
|
||||
@@ -1133,7 +1151,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
...(abortToolRecords?.length && { toolCalls: abortToolRecords }),
|
||||
stopped: true
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
|
||||
...session,
|
||||
messages: [...session.messages, partialMsg],
|
||||
updatedAt: Date.now()
|
||||
|
||||
@@ -122,11 +122,22 @@ async function filterEmbedModels(models: Array<{ name: string }>): Promise<void>
|
||||
const info = await api.showModel(m.name);
|
||||
const caps = info.capabilities || [];
|
||||
const isCompletion = caps.includes('completion');
|
||||
|
||||
// 从 model_info 提取上下文长度
|
||||
let contextLength = 2048;
|
||||
for (const key of Object.keys(info.model_info || {})) {
|
||||
if (key.endsWith('.context_length')) {
|
||||
contextLength = Number(info.model_info![key]) || 2048;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
modelCapabilityCache.set(m.name, {
|
||||
think: caps.includes('thinking'),
|
||||
vision: caps.includes('vision'),
|
||||
tools: caps.includes('tools'),
|
||||
completion: isCompletion,
|
||||
contextLength,
|
||||
});
|
||||
if (!isCompletion) {
|
||||
const opt = modelSelectEl.querySelector(`option[value="${CSS.escape(m.name)}"]`);
|
||||
@@ -149,7 +160,11 @@ async function filterEmbedModels(models: Array<{ name: string }>): Promise<void>
|
||||
|
||||
async function checkModelCapability(modelName: string): Promise<void> {
|
||||
if (modelCapabilityCache.has(modelName)) {
|
||||
applyCapability(modelName, modelCapabilityCache.get(modelName)!);
|
||||
const cached = modelCapabilityCache.get(modelName)!;
|
||||
state.set(KEYS.NUM_CTX, cached.contextLength);
|
||||
const displayEl = document.querySelector('#displayNumCtx');
|
||||
if (displayEl) displayEl.textContent = `${cached.contextLength.toLocaleString()} tokens`;
|
||||
applyCapability(modelName, cached);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,18 +174,40 @@ async function checkModelCapability(modelName: string): Promise<void> {
|
||||
try {
|
||||
const info = await api.showModel(modelName);
|
||||
const capabilities = info.capabilities || [];
|
||||
const caps = {
|
||||
|
||||
// 从 model_info 中提取模型实际上下文长度
|
||||
let contextLength = 2048; // 默认保守值
|
||||
const modelInfo = info.model_info || {};
|
||||
for (const key of Object.keys(modelInfo)) {
|
||||
if (key.endsWith('.context_length')) {
|
||||
contextLength = Number(modelInfo[key]) || 2048;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const caps: ModelCaps = {
|
||||
think: capabilities.includes('thinking'),
|
||||
vision: capabilities.includes('vision'),
|
||||
tools: capabilities.includes('tools'),
|
||||
completion: capabilities.includes('completion'),
|
||||
contextLength,
|
||||
};
|
||||
modelCapabilityCache.set(modelName, caps);
|
||||
logDebug(`能力检测: ${modelName}`, capabilities.join(', '));
|
||||
logDebug(`能力检测: ${modelName}`, `ctx: ${contextLength}, ` + capabilities.join(', '));
|
||||
|
||||
// 自动设置上下文长度为模型实际支持的值
|
||||
state.set(KEYS.NUM_CTX, contextLength);
|
||||
const db = state.get<any>(KEYS.DB);
|
||||
if (db) await db.saveSetting('numCtx', contextLength);
|
||||
|
||||
// 更新设置面板显示
|
||||
const displayEl = document.querySelector('#displayNumCtx');
|
||||
if (displayEl) displayEl.textContent = `${contextLength.toLocaleString()} tokens`;
|
||||
|
||||
applyCapability(modelName, caps);
|
||||
} catch (err) {
|
||||
logWarn(`能力检测失败: ${modelName}`, (err as Error).message);
|
||||
const fallback = { think: false, vision: false, tools: false, completion: true };
|
||||
const fallback: ModelCaps = { think: false, vision: false, tools: false, completion: true, contextLength: 2048 };
|
||||
modelCapabilityCache.set(modelName, fallback);
|
||||
applyCapability(modelName, fallback);
|
||||
}
|
||||
|
||||
@@ -39,16 +39,6 @@ export function initSettingsModal(): void {
|
||||
}, 500);
|
||||
document.querySelector('#inputServerUrl')!.addEventListener('input', saveServerUrl);
|
||||
|
||||
const saveNumCtx = debounce(async () => {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
const val = (document.querySelector('#inputNumCtx') as HTMLInputElement).value.trim();
|
||||
const numCtx = val ? parseInt(val) : 24576;
|
||||
state.set(KEYS.NUM_CTX, numCtx);
|
||||
if (db) await db.saveSetting('numCtx', numCtx);
|
||||
logSetting('上下文长度', `${numCtx} tokens`);
|
||||
}, 500);
|
||||
document.querySelector('#inputNumCtx')!.addEventListener('input', saveNumCtx);
|
||||
|
||||
const tempSlider = document.querySelector('#inputTemperature') as HTMLInputElement;
|
||||
const tempDisplay = document.querySelector('#tempValue')!;
|
||||
tempSlider.addEventListener('input', () => {
|
||||
@@ -250,9 +240,9 @@ export function initSettingsModal(): void {
|
||||
const result = await bridge.workspace.setDir(newDir);
|
||||
if (result.success) {
|
||||
showToast('工作空间目录已更新', 'success');
|
||||
logSetting('工作空间目录', result.dir);
|
||||
logSetting('工作空间目录', result.dir!);
|
||||
} else {
|
||||
showToast(`设置失败: ${result.error}`, 'error');
|
||||
showToast(`设置失败: ${result.error!}`, 'error');
|
||||
// 恢复原值
|
||||
const info = await bridge.workspace.getDir();
|
||||
inputWorkspaceDir.value = info.dir;
|
||||
@@ -401,7 +391,7 @@ async function importSessions(filePath: string): Promise<void> {
|
||||
|
||||
try {
|
||||
const bridge = window.metonaDesktop;
|
||||
const result = await bridge.fs.readFileBase64(filePath);
|
||||
const result = await bridge!.fs.readFileBase64(filePath);
|
||||
if (!result.success) {
|
||||
showToast(`读取文件失败: ${result.error}`, 'error');
|
||||
return;
|
||||
|
||||
@@ -301,7 +301,7 @@ function renderBarChart(
|
||||
|
||||
/** 渲染全局柱状图(按会话) */
|
||||
function renderGlobalChart(stats: AllSessionsTokenStats): void {
|
||||
const chartEl = dashboardModalEl.querySelector('#tdChart')!;
|
||||
const chartEl = dashboardModalEl.querySelector('#tdChart') as HTMLElement;
|
||||
const barsEl = chartEl.querySelector('.td-chart-bars') as HTMLElement | null;
|
||||
const savedScrollLeft = barsEl?.scrollLeft ?? 0;
|
||||
|
||||
@@ -329,7 +329,7 @@ function renderGlobalChart(stats: AllSessionsTokenStats): void {
|
||||
|
||||
/** 渲染全局明细表格 */
|
||||
function renderGlobalTable(stats: AllSessionsTokenStats): void {
|
||||
const tableTitleEl = dashboardModalEl.querySelector('#tdTableTitle');
|
||||
const tableTitleEl = dashboardModalEl.querySelector('#tdTableTitle') as HTMLElement | null;
|
||||
if (tableTitleEl) tableTitleEl.textContent = '📋 会话汇总明细';
|
||||
|
||||
// 动态更新表头
|
||||
@@ -406,7 +406,7 @@ function renderSessionView(): void {
|
||||
`;
|
||||
|
||||
// 柱状图
|
||||
const chartEl = dashboardModalEl.querySelector('#tdChart')!;
|
||||
const chartEl = dashboardModalEl.querySelector('#tdChart') as HTMLElement;
|
||||
const barsEl = chartEl.querySelector('.td-chart-bars') as HTMLElement | null;
|
||||
const savedScrollLeft = barsEl?.scrollLeft ?? 0;
|
||||
|
||||
@@ -423,7 +423,7 @@ function renderSessionView(): void {
|
||||
renderBarChart(chartEl, chartData, '按轮次', undefined, savedScrollLeft);
|
||||
|
||||
// 表格标题
|
||||
const tableTitleEl = dashboardModalEl.querySelector('#tdTableTitle');
|
||||
const tableTitleEl = dashboardModalEl.querySelector('#tdTableTitle') as HTMLElement | null;
|
||||
if (tableTitleEl) tableTitleEl.textContent = '📋 轮次明细(按时间倒序)';
|
||||
|
||||
// 动态更新表头
|
||||
|
||||
@@ -7,7 +7,7 @@ import { state, KEYS } from '../state/state.js';
|
||||
import { setToolEnabled, setRunCommandMode } from '../services/tool-registry.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { logInfo, logWarn } from '../services/log-service.js';
|
||||
import type { ChatDB } from '../types.js';
|
||||
import type { ChatDB } from '../db/chat-db.js';
|
||||
|
||||
let modalEl: HTMLElement;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user