fix: 修复三个启动相关 Bug
Bug 1: prompt()/confirm() 在 Electron contextIsolation 下不可用 - 新增 prompt-modal.ts 自定义弹窗组件 - 替换 memory-panel.ts、kb-modal.ts、settings-modal.ts、history-modal.ts 中所有 prompt()/confirm() 调用 Bug 2: 日志面板重启后无日志 - log-service.ts 新增 earlyBuffer 缓冲 initLogPanel() 之前的日志 - initLogPanel() 初始化时 flush earlyBuffer 到 DOM - main.ts 中 initLogPanel() 移至最先初始化 Bug 3: 首次启动输入框可能被禁用 - main.ts init() 中防御性重置 IS_STREAMING/IS_HISTORY_VIEW/toolCallingEnabled 等状态 - catch 块也重置关键状态并初始化日志面板
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { debounce, escapeHtml, formatTime } from '../utils/utils.js';
|
||||
import { exportAsMarkdown, exportAsHtml, exportAsTxt, renderMessages, clearMessages } from './chat-area.js';
|
||||
import { showConfirm } from './prompt-modal.js';
|
||||
import { ChatDB } from '../db/chat-db.js';
|
||||
import { logSession, logWarn } from '../services/log-service.js';
|
||||
import type { ChatSession } from '../types.js';
|
||||
@@ -56,7 +57,7 @@ export function initHistoryModal(): void {
|
||||
const db = state.get<ChatDB>(KEYS.DB);
|
||||
|
||||
if (target.classList.contains('btn-delete-session')) {
|
||||
if (confirm('确定删除此会话?')) {
|
||||
if (await showConfirm('确定删除此会话?', '删除会话')) {
|
||||
await db!.deleteSession(sessionId);
|
||||
logSession('删除', sessionId);
|
||||
const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../services/rag.js';
|
||||
import { fileToText, formatSize, escapeHtml, truncate } from '../utils/utils.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { showConfirm } from './prompt-modal.js';
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
import { logRAG, logError } from '../services/log-service.js';
|
||||
import type { VectorCollection, SearchResult } from '../types.js';
|
||||
@@ -144,7 +145,7 @@ async function refreshCollections(): Promise<void> {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const id = (btn as HTMLElement).dataset.id!;
|
||||
if (!confirm('确定删除此知识库集合?所有文档将被清除。')) return;
|
||||
if (!await showConfirm('确定删除此知识库集合?所有文档将被清除。', '删除知识库')) return;
|
||||
const vs = getVectorStore();
|
||||
await vs!.deleteCollection(id);
|
||||
if (currentColId === id) currentColId = null;
|
||||
@@ -192,7 +193,7 @@ async function refreshDocList(): Promise<void> {
|
||||
docListEl.querySelectorAll('.kb-doc-delete').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const docId = (btn as HTMLElement).dataset.docid!;
|
||||
if (!confirm('确定删除此文档?')) return;
|
||||
if (!await showConfirm('确定删除此文档?', '删除文档')) return;
|
||||
await removeDocumentFromKB(currentColId!, docId);
|
||||
showToast('文档已删除', 'success');
|
||||
await refreshCollections();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getTypeIcon, getTypeName
|
||||
} from '../services/memory-manager.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { showPrompt, showConfirm } from './prompt-modal.js';
|
||||
import { debounce, escapeHtml, formatTime } from '../utils/utils.js';
|
||||
import type { MemoryEntry, MemoryType } from '../types.js';
|
||||
|
||||
@@ -93,7 +94,7 @@ export function initMemoryPanel(): void {
|
||||
|
||||
panel.querySelector('#btnAddMemory')!.addEventListener('click', () => openAddMemoryDialog());
|
||||
panel.querySelector('#btnClearMemories')!.addEventListener('click', async () => {
|
||||
if (confirm('确定清空所有记忆?此操作不可恢复!')) {
|
||||
if (await showConfirm('确定清空所有记忆?此操作不可恢复!', '清空记忆')) {
|
||||
await clearAllMemories();
|
||||
renderMemoryList();
|
||||
updateMemoryCount();
|
||||
@@ -205,33 +206,62 @@ function renderMemoryList(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function openAddMemoryDialog(): void {
|
||||
const type = prompt('记忆类型:\n1. fact(事实)\n2. preference(偏好)\n3. rule(规则)\n4. episode(事件)\n\n请输入数字:');
|
||||
const typeMap: Record<string, MemoryType> = { '1': 'fact', '2': 'preference', '3': 'rule', '4': 'episode' };
|
||||
const memoryType = typeMap[type || ''] || 'fact';
|
||||
async function openAddMemoryDialog(): Promise<void> {
|
||||
const type = await showPrompt({
|
||||
title: '添加记忆',
|
||||
message: '选择记忆类型:',
|
||||
type: 'select',
|
||||
defaultValue: 'fact',
|
||||
options: [
|
||||
{ value: 'fact', label: '📌 事实 — 项目信息、身份背景' },
|
||||
{ value: 'preference', label: '⚙️ 偏好 — 语言风格、技术栈偏好' },
|
||||
{ value: 'rule', label: '📏 规则 — 编码规范、输出要求' },
|
||||
{ value: 'episode', label: '📝 事件 — 完成的任务、结论' }
|
||||
]
|
||||
});
|
||||
if (type === null) return;
|
||||
const memoryType = (type as MemoryType) || 'fact';
|
||||
|
||||
const content = prompt('请输入记忆内容:');
|
||||
const content = await showPrompt({
|
||||
title: '添加记忆',
|
||||
message: '请输入记忆内容:',
|
||||
type: 'textarea',
|
||||
placeholder: '例如:用户正在开发一个 Electron 桌面应用'
|
||||
});
|
||||
if (!content?.trim()) return;
|
||||
|
||||
const importanceStr = prompt('重要性(1-10,默认 5):');
|
||||
const importanceStr = await showPrompt({
|
||||
title: '添加记忆',
|
||||
message: '重要性(1-10,默认 5):',
|
||||
defaultValue: '5',
|
||||
placeholder: '5'
|
||||
});
|
||||
const importance = Math.min(10, Math.max(1, parseInt(importanceStr || '5') || 5));
|
||||
|
||||
addMemory({ type: memoryType, content: content.trim(), importance }).then(() => {
|
||||
renderMemoryList();
|
||||
updateMemoryCount();
|
||||
showToast('记忆已添加', 'success', 1500);
|
||||
});
|
||||
await addMemory({ type: memoryType, content: content.trim(), importance });
|
||||
renderMemoryList();
|
||||
updateMemoryCount();
|
||||
showToast('记忆已添加', 'success', 1500);
|
||||
}
|
||||
|
||||
function openEditMemoryDialog(entry: MemoryEntry): void {
|
||||
const newContent = prompt('编辑记忆内容:', entry.content);
|
||||
async function openEditMemoryDialog(entry: MemoryEntry): Promise<void> {
|
||||
const newContent = await showPrompt({
|
||||
title: '编辑记忆',
|
||||
message: '修改记忆内容:',
|
||||
type: 'textarea',
|
||||
defaultValue: entry.content
|
||||
});
|
||||
if (newContent === null || newContent.trim() === entry.content) return;
|
||||
|
||||
const importanceStr = prompt('重要性(1-10):', String(entry.importance));
|
||||
const importanceStr = await showPrompt({
|
||||
title: '编辑记忆',
|
||||
message: '重要性(1-10):',
|
||||
defaultValue: String(entry.importance),
|
||||
placeholder: String(entry.importance)
|
||||
});
|
||||
const importance = importanceStr ? Math.min(10, Math.max(1, parseInt(importanceStr) || entry.importance)) : entry.importance;
|
||||
|
||||
updateMemory(entry.id, { content: newContent.trim(), importance }).then(() => {
|
||||
renderMemoryList();
|
||||
showToast('记忆已更新', 'success', 1500);
|
||||
});
|
||||
await updateMemory(entry.id, { content: newContent.trim(), importance });
|
||||
renderMemoryList();
|
||||
showToast('记忆已更新', 'success', 1500);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* PromptModal - 自定义 prompt/confirm 对话框
|
||||
* 替代 window.prompt() / window.confirm()(Electron contextIsolation 下不可用)
|
||||
*/
|
||||
|
||||
let overlayEl: HTMLElement | null = null;
|
||||
let titleEl: HTMLElement | null = null;
|
||||
let bodyEl: HTMLElement | null = null;
|
||||
let inputEl: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null = null;
|
||||
let okBtn: HTMLButtonElement | null = null;
|
||||
let cancelBtn: HTMLButtonElement | null = null;
|
||||
let resolveFn: ((value: string | null) => void) | null = null;
|
||||
|
||||
function ensureModal(): void {
|
||||
if (overlayEl) return;
|
||||
|
||||
overlayEl = document.createElement('div');
|
||||
overlayEl.className = 'modal-overlay';
|
||||
overlayEl.id = 'promptModal';
|
||||
overlayEl.style.display = 'none';
|
||||
overlayEl.innerHTML = `
|
||||
<div class="modal" style="max-width:420px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="promptTitle">输入</h3>
|
||||
</div>
|
||||
<div class="modal-body" id="promptBody"></div>
|
||||
<div class="modal-actions" style="padding:12px 20px 16px;">
|
||||
<div style="flex:1;"></div>
|
||||
<button class="btn btn-outline" id="promptCancel">取消</button>
|
||||
<button class="btn btn-primary" id="promptOk">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.querySelector('#app')!.appendChild(overlayEl);
|
||||
titleEl = overlayEl.querySelector('#promptTitle')!;
|
||||
bodyEl = overlayEl.querySelector('#promptBody')!;
|
||||
okBtn = overlayEl.querySelector('#promptOk')! as HTMLButtonElement;
|
||||
cancelBtn = overlayEl.querySelector('#promptCancel')! as HTMLButtonElement;
|
||||
|
||||
overlayEl.addEventListener('click', (e) => {
|
||||
if (e.target === overlayEl) close(null);
|
||||
});
|
||||
cancelBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
close(null);
|
||||
});
|
||||
okBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (inputEl instanceof HTMLSelectElement) {
|
||||
close(inputEl.value);
|
||||
} else if (inputEl) {
|
||||
close((inputEl as HTMLInputElement | HTMLTextAreaElement).value);
|
||||
} else {
|
||||
close('');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!overlayEl || overlayEl.style.display === 'none') return;
|
||||
if (e.key === 'Escape') { e.preventDefault(); close(null); }
|
||||
if (e.key === 'Enter' && !(inputEl instanceof HTMLTextAreaElement)) {
|
||||
e.preventDefault();
|
||||
okBtn!.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function close(value: string | null): void {
|
||||
if (overlayEl) overlayEl.style.display = 'none';
|
||||
if (resolveFn) {
|
||||
const r = resolveFn;
|
||||
resolveFn = null;
|
||||
r(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示自定义 prompt,返回用户输入或 null(取消) */
|
||||
export function showPrompt(options: {
|
||||
title: string;
|
||||
message?: string;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
type?: 'text' | 'textarea' | 'select';
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
}): Promise<string | null> {
|
||||
ensureModal();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolveFn = resolve;
|
||||
titleEl!.textContent = options.title;
|
||||
|
||||
let html = '';
|
||||
if (options.message) {
|
||||
html += `<p style="margin-bottom:12px;color:var(--text-secondary);font-size:13px;white-space:pre-line;">${escapeHtml(options.message)}</p>`;
|
||||
}
|
||||
|
||||
if (options.type === 'select' && options.options) {
|
||||
html += `<select class="setting-input" id="promptInput">${options.options.map(o =>
|
||||
`<option value="${escapeHtml(o.value)}"${o.value === options.defaultValue ? ' selected' : ''}>${escapeHtml(o.label)}</option>`
|
||||
).join('')}</select>`;
|
||||
} else if (options.type === 'textarea') {
|
||||
html += `<textarea class="setting-input" id="promptInput" rows="4" placeholder="${escapeHtml(options.placeholder || '')}" style="resize:vertical;min-height:80px;">${escapeHtml(options.defaultValue || '')}</textarea>`;
|
||||
} else {
|
||||
html += `<input class="setting-input" id="promptInput" type="text" value="${escapeHtml(options.defaultValue || '')}" placeholder="${escapeHtml(options.placeholder || '')}">`;
|
||||
}
|
||||
|
||||
bodyEl!.innerHTML = html;
|
||||
inputEl = bodyEl!.querySelector('#promptInput')! as HTMLInputElement;
|
||||
|
||||
overlayEl!.style.display = '';
|
||||
|
||||
// 聚焦并选中文本
|
||||
setTimeout(() => {
|
||||
if (inputEl instanceof HTMLInputElement) {
|
||||
inputEl.focus();
|
||||
if (options.defaultValue) inputEl.select();
|
||||
} else if (inputEl instanceof HTMLTextAreaElement) {
|
||||
inputEl.focus();
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
/** 显示自定义 confirm,返回 true/false */
|
||||
export function showConfirm(message: string, title = '确认'): Promise<boolean> {
|
||||
ensureModal();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolveFn = (val) => resolve(val !== null);
|
||||
titleEl!.textContent = title;
|
||||
bodyEl!.innerHTML = `<p style="color:var(--text-secondary);font-size:13px;white-space:pre-line;">${escapeHtml(message)}</p>`;
|
||||
inputEl = null;
|
||||
overlayEl!.style.display = '';
|
||||
okBtn!.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { logSetting, logInfo, logWarn, logError, logSuccess } from '../services/
|
||||
import { updateConnectionInfo, updateRunningModels } from './header.js';
|
||||
import { loadModels } from './model-bar.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { showConfirm } from './prompt-modal.js';
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
import { ChatDB } from '../db/chat-db.js';
|
||||
import type { ChatSession } from '../types.js';
|
||||
@@ -96,7 +97,7 @@ export function initSettingsModal(): void {
|
||||
|
||||
document.querySelector('#btnClearAllHistory')!.addEventListener('click', async () => {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (confirm('确定清空所有历史记录?此操作不可恢复!')) {
|
||||
if (await showConfirm('确定清空所有历史记录?此操作不可恢复!', '清空历史')) {
|
||||
logWarn('清空所有历史记录');
|
||||
await db!.clearAll();
|
||||
(document.querySelector('#btnNewChat') as HTMLElement).click();
|
||||
|
||||
+11
-1
@@ -161,6 +161,13 @@ async function init(): Promise<void> {
|
||||
state.set(KEYS.IS_HISTORY_VIEW, false);
|
||||
state.set(KEYS.NUM_CTX, 24576);
|
||||
|
||||
// ── 防御性重置:确保首次启动不会遗留脏状态 ──
|
||||
state.set('toolCallingEnabled', false);
|
||||
state.set('runCommandEnabled', false);
|
||||
state.set('memoryEnabled', true);
|
||||
state.set('thinkEnabled', false);
|
||||
|
||||
initLogPanel(); // ← 日志面板最先初始化,确保后续 init 日志能显示
|
||||
initToast();
|
||||
initLightbox();
|
||||
initHeader();
|
||||
@@ -173,7 +180,6 @@ async function init(): Promise<void> {
|
||||
initHelpModal();
|
||||
initToolConfirmModal();
|
||||
initMemoryPanel();
|
||||
initLogPanel();
|
||||
setupDesktopIntegration();
|
||||
|
||||
bindGlobalEvents();
|
||||
@@ -234,7 +240,11 @@ async function init(): Promise<void> {
|
||||
api = new OllamaAPI();
|
||||
state.set(KEYS.API, api);
|
||||
}
|
||||
// ── 防御性重置:catch 路径也确保关键状态干净 ──
|
||||
state.set(KEYS.IS_STREAMING, false);
|
||||
state.set(KEYS.IS_HISTORY_VIEW, false);
|
||||
state.set(KEYS.CURRENT_SESSION, createNewSession());
|
||||
initLogPanel();
|
||||
bindGlobalEvents();
|
||||
initToast();
|
||||
initLightbox();
|
||||
|
||||
@@ -23,6 +23,9 @@ let autoScroll = true;
|
||||
let idCounter = 0;
|
||||
let renderQueued = false;
|
||||
const pendingEntries: LogEntry[] = [];
|
||||
let panelInitialized = false;
|
||||
/** initLogPanel 前的日志暂存区 */
|
||||
const earlyBuffer: LogEntry[] = [];
|
||||
|
||||
const LEVEL_ICONS: Record<LogLevel, string> = {
|
||||
info: 'ℹ️',
|
||||
@@ -69,6 +72,28 @@ export function initLogPanel(): void {
|
||||
autoScroll = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
||||
} catch { /* ignore */ }
|
||||
});
|
||||
|
||||
// ── 关键修复:flush initLogPanel 前缓冲的日志 ──
|
||||
panelInitialized = true;
|
||||
if (earlyBuffer.length > 0 && logBodyEl) {
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const entry of earlyBuffer) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `log-entry log-${entry.level}`;
|
||||
div.id = entry.id;
|
||||
let html = `<span class="log-time">${formatTime(entry.time)}</span>`;
|
||||
html += `<span class="log-icon">${entry.icon}</span>`;
|
||||
html += `<span class="log-msg">${escapeHtml(entry.message)}</span>`;
|
||||
if (entry.detail) {
|
||||
html += `<pre class="log-detail">${escapeHtml(entry.detail)}</pre>`;
|
||||
}
|
||||
div.innerHTML = html;
|
||||
frag.appendChild(div);
|
||||
}
|
||||
logBodyEl.appendChild(frag);
|
||||
earlyBuffer.length = 0;
|
||||
if (autoScroll) logBodyEl.scrollTop = logBodyEl.scrollHeight;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -137,6 +162,12 @@ export function addLog(level: LogLevel, message: string, detail?: string, icon?:
|
||||
logs.push(entry);
|
||||
if (logs.length > MAX_LOGS) logs.splice(0, logs.length - MAX_LOGS);
|
||||
|
||||
// initLogPanel 之前:暂存到 earlyBuffer
|
||||
if (!panelInitialized) {
|
||||
earlyBuffer.push(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
pendingEntries.push(entry);
|
||||
if (!renderQueued) {
|
||||
renderQueued = true;
|
||||
|
||||
Reference in New Issue
Block a user