fix: 工作空间面板定位、中文乱码、快捷键、帮助文档

- 工作空间面板 top 改用 CSS 变量 --nav-height,JS 动态计算 header+model-bar 实际高度
- Windows 下 spawn 子进程前加 chcp 65001 切换 UTF-8 代码页,修复中文乱码
- 补全 Ctrl+K(知识库)、Ctrl+M(记忆面板)快捷键,防止 Ctrl+M 默认行为冲突
- 帮助弹框内容全面更新,修正工作空间为常驻显示,补充 Ctrl+N、布局说明等
- Token 统计图标从 emoji 🪙 改为内联 SVG,兼容所有 Windows 字体
This commit is contained in:
thzxx
2026-04-08 16:29:22 +08:00
parent 68c5571dd8
commit 8783cd9b59
5 changed files with 81 additions and 23 deletions
+11 -4
View File
@@ -298,12 +298,19 @@ export async function handleRunCommand(params: { command: string; cwd?: string;
return new Promise((resolve) => {
const isWin = process.platform === 'win32';
const shell = isWin ? 'cmd.exe' : 'bash';
const shellArgs = isWin ? ['/c', params.command] : ['-c', params.command];
const actualCmd = isWin ? `chcp 65001 >nul && ${params.command}` : params.command;
const shellArgs = isWin ? ['/c', actualCmd] : ['-c', actualCmd];
const startTime = Date.now();
_toolProc = spawn(shell, shellArgs, {
cwd,
env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }) },
env: {
...process.env,
...(isWin ? {} : { TERM: 'xterm-256color' }),
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US.UTF-8',
PYTHONIOENCODING: 'utf-8'
},
stdio: ['pipe', 'pipe', 'pipe']
});
@@ -312,13 +319,13 @@ export async function handleRunCommand(params: { command: string; cwd?: string;
// 实时推送到工作空间终端
_toolProc.stdout?.on('data', (data: Buffer) => {
const str = data.toString();
const str = data.toString('utf-8');
stdout += str;
mainWindow?.webContents.send('cmd:output', { command: params.command, type: 'stdout', data: str });
});
_toolProc.stderr?.on('data', (data: Buffer) => {
const str = data.toString();
const str = data.toString('utf-8');
stderr += str;
mainWindow?.webContents.send('cmd:output', { command: params.command, type: 'stderr', data: str });
});
+24 -8
View File
@@ -88,21 +88,30 @@ export function startProcess(
try {
const isWin = process.platform === 'win32';
const shell = isWin ? 'cmd.exe' : 'bash';
const shellArgs = isWin ? ['/c', command] : ['-c', command];
// Windows cmd 默认代码页为 GBK/CP936,中文输出会乱码
// chcp 65001 切换到 UTF-8 代码页,>nul 抑制 chcp 自身的输出
const actualCmd = isWin ? `chcp 65001 >nul && ${command}` : command;
const shellArgs = isWin ? ['/c', actualCmd] : ['-c', actualCmd];
const proc = spawn(shell, shellArgs, {
cwd: workDir,
env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }) },
env: {
...process.env,
...(isWin ? {} : { TERM: 'xterm-256color' }),
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US.UTF-8',
PYTHONIOENCODING: 'utf-8'
},
stdio: ['pipe', 'pipe', 'pipe']
});
activeProcesses.set(id, proc);
proc.stdout.on('data', (data: Buffer) => {
onOutput('stdout', data.toString());
onOutput('stdout', data.toString('utf-8'));
});
proc.stderr.on('data', (data: Buffer) => {
onOutput('stderr', data.toString());
onOutput('stderr', data.toString('utf-8'));
});
proc.on('close', (code) => {
@@ -246,17 +255,24 @@ export function execWorkspaceCommand(
try {
const isWin = process.platform === 'win32';
const shell = isWin ? 'cmd.exe' : 'bash';
const shellArgs = isWin ? ['/c', command] : ['-c', command];
const actualCmd = isWin ? `chcp 65001 >nul && ${command}` : command;
const shellArgs = isWin ? ['/c', actualCmd] : ['-c', actualCmd];
const proc = spawn(shell, shellArgs, {
cwd: workDir,
env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }) },
env: {
...process.env,
...(isWin ? {} : { TERM: 'xterm-256color' }),
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US.UTF-8',
PYTHONIOENCODING: 'utf-8'
},
stdio: ['pipe', 'pipe', 'pipe']
});
activeProcesses.set(cmdId, proc);
proc.stdout.on('data', (data: Buffer) => {
const str = data.toString();
const str = data.toString('utf-8');
if (stdoutBuf.length < MAX_OUTPUT) {
stdoutBuf += str;
if (stdoutBuf.length > MAX_OUTPUT) {
@@ -271,7 +287,7 @@ export function execWorkspaceCommand(
});
proc.stderr.on('data', (data: Buffer) => {
const str = data.toString();
const str = data.toString('utf-8');
if (stderrBuf.length < MAX_OUTPUT) {
stderrBuf += str;
if (stderrBuf.length > MAX_OUTPUT) {
+9 -10
View File
@@ -34,7 +34,7 @@
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
</button>
<span class="total-tokens" id="totalTokens" title="当前会话总 token 消耗">🪙 <span id="totalTokensValue">0</span></span>
<span class="total-tokens" id="totalTokens" title="当前会话总 token 消耗"><svg class="token-icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><text x="8" y="11.5" text-anchor="middle" fill="currentColor" font-size="9" font-weight="700" font-family="sans-serif">T</text></svg> <span id="totalTokensValue">0</span></span>
</div>
<div class="header-right">
<div class="conn-status pending" id="connStatus" title="连接状态">
@@ -387,15 +387,14 @@
</button>
</div>
<div class="modal-body">
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code></li><li>顶部下拉框选择一个模型</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong>🧠 按钮开启深度思考(需模型支持)</li><li><strong>上下文长度</strong> — 设置中调整 <code>num_ctx</code>,越大记忆越长</li><li><strong>系统提示词</strong> — 设置中开启,定义 AI 的角色和行为</li></ul></div>
<div class="help-section"><h4>📎 文件与图片</h4><ul><li><strong>📷 上传图片</strong> — 图标按钮上传,模型需支持 Vision</li><li><strong>📄 上传文件</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB</li><li>文件内容自动以代码块格式发送给模型分析</li></ul></div>
<div class="help-section"><h4>🔧 Tool CallingAI 文件操作)</h4><ul><li>AI 可以<strong>自主读写文件、搜索内容、创建目录、执行命令</strong>,像一个本地 Agent</li><li><strong>7 个工具</strong><code>read_file</code> / <code>write_file</code> / <code>list_directory</code> / <code>search_files</code> / <code>create_directory</code> / <code>delete_file</code> / <code>run_command</code></li><li>每次工具调用前弹出<strong>确认对话框</strong>,你可以批准或拒绝</li><li>危险命令(<code>rm</code><code>sudo</code> 等)会被自动拦截</li><li>工具调用过程以<strong>可视化卡片</strong>展示(pending → running → success/error</li><li>最多自动循环 <strong>10 轮</strong>工具调用,也可随时中断</li></ul></div>
<div class="help-section"><h4>📚 知识库 (RAG)</h4><ul><li>点击顶部 📚 按钮打开知识库管理</li><li>创建集合 → 选择嵌入模型(推荐 <code>nomic-embed-text</code></li><li>上传文档,自动分块并生成向量索引</li><li>开启 RAG 检索后,聊天时自动检索相关文档注入回答</li></ul></div>
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>AI 会从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则/事件),跨会话持续积累</li><li>对话满 <strong>6 条消息</strong>后自动触发记忆提取</li><li>新对话时自动检索相关记忆并注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除记忆条目</li><li>记忆持久化到 IndexedDB,清除浏览器数据会丢失</li></ul></div>
<div class="help-section"><h4>🕐 历史记录</h4><ul><li>所有会话自动保存到 IndexedDB</li><li>点击 🕐 按钮查看、搜索、恢复历史会话</li><li>支持导出 <code>.metona</code> 加密备份文件</li></ul></div>
<div class="help-section"><h4>🖥️ 工作空间</h4><ul><li>点击顶部 🖥️ 按钮打开右侧工作空间面板</li><li><strong>命令行 Tab</strong> — 终端界面,实时显示命令输出,支持长时间运行(如 <code>ollama pull</code></li><li><strong>文件 Tab</strong> — 浏览工作空间目录,点击文件预览内容</li><li>支持多终端 Tab,每个 Tab 独立进程,可随时停止</li><li>命令在工作空间目录执行,可在设置中修改路径</li><li>AI 建议执行命令时,可在工作空间中运行并实时查看进度</li></ul></div>
<div class="help-section"><h4>⚡ 快捷键</h4><table class="help-shortcuts"><tr><td><kbd>Enter</kbd></td><td>发送消息</td></tr><tr><td><kbd>Shift + Enter</kbd></td><td>换行</td></tr><tr><td><kbd>Ctrl + K</kbd></td><td>知识库管理</td></tr><tr><td><kbd>Ctrl + M</kbd></td><td>记忆面板</td></tr><tr><td><kbd>Esc</kbd></td><td>关闭弹窗</td></tr></table></div>
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools / 📚 RAG</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong>设置中开启,让模型展示深度思考过程(需模型支持,如 Qwen3</li><li><strong>多模态</strong> — 上传图片,模型需支持 Vision 能力</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,内容以代码块发送给模型</li><li><strong>上下文长度</strong> — 设置中调整 <code>num_ctx</code>,越大记忆越长,消耗显存越多</li><li><strong>系统提示词</strong> — 设置中开启,定义 AI 的角色和行为规范</li></ul></div>
<div class="help-section"><h4>🔧 Tool CallingAI 自主操作)</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>7 个工具</strong><code>read_file</code> / <code>write_file</code> / <code>list_directory</code> / <code>search_files</code> / <code>create_directory</code> / <code>delete_file</code> / <code>run_command</code></li><li>写入/删除/命令执行等高风险操作会弹出<strong>确认对话框</strong>,你可以批准或拒绝</li><li>危险命令(<code>rm -rf</code><code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code><code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error</li><li>最多自动循环 <strong>10 轮</strong>工具调用,也可随时中断</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral</li></ul></div>
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则/事件),跨会话持续积累</li><li>对话满 <strong>6 条消息</strong>后自动触发记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li>记忆存储在本地 IndexedDB,不会上传到任何服务器</li></ul></div>
<div class="help-section"><h4>📚 知识库 (RAG)</h4><ul><li>点击顶部 📚 按钮打开知识库管理</li><li>创建集合 → 选择嵌入模型(推荐 <code>nomic-embed-text</code></li><li>上传文档,自动分块并生成向量索引</li><li>开启 RAG 检索后,聊天时自动检索相关文档增强回答</li></ul></div>
<div class="help-section"><h4>🖥️ 布局说明</h4><ul><li><strong>左侧面板</strong> — 执行日志,实时显示应用运行日志(连接、模型加载、工具调用等)</li><li><strong>中间区域</strong> — 聊天消息,顶部 Header + 模型栏,底部输入框</li><li><strong>右侧面板</strong> — 工作空间(常驻显示),包含:<ul><li><strong>💻 命令行 Tab</strong> — 终端界面,实时流式输出,支持长时间运行(无超时),多终端 Tab</li><li><strong>📁 文件 Tab</strong> — 浏览工作空间目录,点击文件预览内容</li></ul></li><li>工作空间面板宽度可通过拖拽左边缘调整(300–700px)</li><li>工作空间目录可在设置中修改</li></ul></div>
<div class="help-section"><h4>🕐 历史记录</h4><ul><li>所有会话自动保存到本地 IndexedDB</li><li>点击顶部 🕐 按钮查看、搜索、恢复历史会话</li><li>支持导出 Markdown / HTML / TXT / <code>.metona</code> 加密备份</li></ul></div>
<div class="help-section"><h4>⚡ 快捷键</h4><table class="help-shortcuts"><tr><td><kbd>Enter</kbd></td><td>发送消息</td></tr><tr><td><kbd>Shift + Enter</kbd></td><td>换行</td></tr><tr><td><kbd>Ctrl + N</kbd></td><td>新建会话</td></tr><tr><td><kbd>Ctrl + K</kbd></td><td>知识库管理</td></tr><tr><td><kbd>Ctrl + M</kbd></td><td>记忆面板</td></tr><tr><td><kbd>Esc</kbd></td><td>关闭弹窗</td></tr></table></div>
</div>
</div>
</div>
+29
View File
@@ -212,6 +212,8 @@ async function init(): Promise<void> {
setupDesktopIntegration();
bindGlobalEvents();
updateNavHeight();
window.addEventListener('resize', updateNavHeight);
await checkConnection();
await loadModels();
@@ -283,10 +285,25 @@ async function init(): Promise<void> {
initSettingsModal();
initHistoryModal();
setupDesktopIntegration();
updateNavHeight();
window.addEventListener('resize', updateNavHeight);
checkConnection().then(loadModels);
}
}
/**
* 动态计算顶部导航高度(header + model-bar),
* 设为 CSS 变量 --nav-height,供 workspace-panel 引用
*/
function updateNavHeight(): void {
const header = document.querySelector<HTMLElement>('.app-header');
const modelBar = document.querySelector<HTMLElement>('.model-bar');
const headerH = header?.getBoundingClientRect().height ?? 0;
const modelBarH = modelBar?.getBoundingClientRect().height ?? 0;
const total = Math.ceil(headerH + modelBarH);
document.documentElement.style.setProperty('--nav-height', `${total}px`);
}
function bindGlobalEvents(): void {
document.querySelector('#btnNewChat')?.addEventListener('click', startNewSession);
@@ -298,6 +315,18 @@ function bindGlobalEvents(): void {
(document.querySelector('#helpModal') as HTMLElement).style.display = 'none';
(document.querySelector('#kbModal') as HTMLElement).style.display = 'none';
}
// Ctrl+K — 知识库管理
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
document.querySelector('#btnKB')?.dispatchEvent(new Event('click'));
}
// Ctrl+M — 记忆面板
if (e.ctrlKey && e.key === 'm') {
e.preventDefault();
document.querySelector('#btnMemory')?.dispatchEvent(new Event('click'));
}
});
window.addEventListener('error', (e) => {
+8 -1
View File
@@ -383,6 +383,13 @@ html, body {
user-select: none;
}
.token-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
opacity: 0.8;
}
/* ═══ 模型选择栏 ═══ */
.model-bar {
display: flex;
@@ -2667,7 +2674,7 @@ html, body {
.workspace-panel {
position: fixed;
top: 92px; /* header(44px) + border(1px) + model-bar(46px) + border(1px) */
top: var(--nav-height, 92px); /* JS 动态计算 header + model-bar 实际高度 */
right: 0;
bottom: 0;
width: 480px;