修复一些已知问题

This commit is contained in:
minorQAQ committed 2026-08-11 09:16:03 +08:00
1 parent 4a11e448cf
commit b59db6879c
31 files changed
+1316 -772

No files matched your search

+74 -1
View File
@@ -59,6 +59,12 @@ function isPreviewableHtml(name) {
return (name || '').toLowerCase().endsWith('.html') || (name || '').toLowerCase().endsWith('.htm');
}
/** 二进制表格(XLSX/XLS):由后端 /api/table/text 提取为 TSV 后按列着色预览 */
function isPreviewableTable(name) {
const ext = (name || '').toLowerCase();
return ext.endsWith('.xlsx') || ext.endsWith('.xls');
}
const BINARY_EXTS = new Set([
'.exe','.dll','.so','.dylib','.bin','.dat','.zip','.rar','.7z','.tar','.gz','.bz2','.xz',
'.png','.jpg','.jpeg','.gif','.bmp','.ico','.webp','.tiff','.tif','.psd','.ai','.eps',
@@ -78,7 +84,19 @@ function isBinaryFile(name) {
}
// ===== 模式切换 =====
let _modeSwitchCooldown = false;
export function switchToMode(mode) {
// 防连点:短时间内禁止重复切换
if (_modeSwitchCooldown) return;
_modeSwitchCooldown = true;
setTimeout(() => { _modeSwitchCooldown = false; }, 300);
// Agent 运行中禁止切换到 cron/edit 模式,避免 chat 区域被替换导致工具调用和思考历史消失
if (state.sending && mode !== "chat") {
showToast("Agent 正在运行中,请等待完成后再切换模式");
return;
}
state.mode = mode;
if (mode === "edit") {
state.editMode = true;
@@ -155,6 +173,15 @@ export async function openFolder(folderPath) {
}
if (!rootPath) return;
// 先校验目录存在并列出文件;文件夹被删除/不可访问时给出明确提示,避免把失效路径写入状态
let listRes;
try {
listRes = await listDir(rootPath, 10);
} catch (e) {
showToast("打开文件夹失败: " + (e.message || e));
return;
}
state._docRootPath = rootPath;
state._docRootName = rootPath.split(/[/\\]/).pop() || rootPath;
@@ -166,7 +193,6 @@ export async function openFolder(folderPath) {
if (!state._docTreeFolders) state._docTreeFolders = {};
state._docTreeFolders[state._docRootName] = true;
const listRes = await listDir(rootPath, 10);
const files = listRes.files || [];
const supportedFiles = files.filter((f) => !f.is_dir && isSupportedFile(f.name));
// 去重:按 path 去重
@@ -257,6 +283,19 @@ export async function loadDocContent(filePath) {
openFilePreview(filePath, fileName, res.content || '',
{ status, onDownload: () => downloadFile(filePath, fileName) });
}
} else if (isPreviewableTable(fileName)) {
// XLSX/XLS:后端提取为 TSV 文本,走与 CSV/TSV 一致的按列着色预览
const absPath = (/^[A-Za-z]:[\\/]/.test(filePath) || filePath.startsWith('/') || filePath.startsWith('\\'))
? filePath
: state._docRootPath.replace(/[\\/]+$/, '') + '/' + filePath.replace(/^[\\/]+/, '');
try {
const tsv = await api('/api/table/text?path=' + encodeURIComponent(absPath));
openFilePreview(absPath, fileName, (tsv && tsv.content) || '',
{ status, onDownload: () => downloadFile(absPath, fileName) });
} catch (e) {
openFilePreview(absPath, fileName, '(加载失败: ' + (e.message || e) + ')',
{ status, onDownload: () => downloadFile(absPath, fileName) });
}
} else if (isBinaryFile(fileName)) {
showToast('二进制文件不支持在线查看', 'warning');
} else {
@@ -309,6 +348,20 @@ async function downloadFile(filePath, fileName) {
a.href = url; a.download = fileName;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
}
} else if (isBinaryFile(fileName)) {
// 二进制文件(xlsx/docx/pdf 等):按 base64 还原字节后下载
const res = await readBinaryFile(filePath, state._docRootPath);
if (res && res.status === 'ok' && res.data) {
const bin = atob(res.data);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
const blob = new Blob([bytes], { type: res.mime || 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = fileName;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
}
} else {
const res = await readTextFile(filePath, state._docRootPath);
if (res && res.status === 'ok') {
@@ -737,17 +790,37 @@ function setupInputDropHandler() {
const target = composerInput || $('textInput');
if (!target) return;
// 子元素间移动会成对触发 dragenter/dragleave,用计数器避免状态误清除
let dragDepth = 0;
const setDragState = (on) => target.classList.toggle('is-drag-attach', on);
const isAcceptedDrag = (types) => types.includes('application/doc-path') || types.includes('Files');
target.addEventListener('dragenter', (e) => {
if (isAcceptedDrag(Array.from(e.dataTransfer.types))) {
dragDepth++;
setDragState(true);
}
});
target.addEventListener('dragover', (e) => {
const types = Array.from(e.dataTransfer.types);
// 接受应用内文件/文件夹引用拖拽 或 外部文件拖拽
if (types.includes('application/doc-path') || types.includes('Files')) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
setDragState(true);
}
});
target.addEventListener('dragleave', () => {
dragDepth = Math.max(0, dragDepth - 1);
if (dragDepth === 0) setDragState(false);
});
target.addEventListener('drop', async (e) => {
e.preventDefault();
dragDepth = 0;
setDragState(false);
// 1. 内部拖拽(doc-tree 文件/文件夹引用)
// app.js 检测到 application/doc-path 也会 return,无需 stopPropagation
+101 -1
View File
@@ -6,8 +6,9 @@ import { $, escapeHtml } from './utils.js';
const IMG_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.ico']);
const HTML_EXTS = new Set(['.html', '.htm']);
const TEXT_EXTS = new Set(['.txt', '.json', '.md', '.py', '.js', '.ts', '.css', '.xml', '.yaml', '.yml', '.toml',
'.csv', '.log', '.env', '.ini', '.cfg', '.sh', '.bat', '.ps1', '.sql', '.r', '.go', '.rs', '.rb',
'.csv', '.tsv', '.log', '.env', '.ini', '.cfg', '.sh', '.bat', '.ps1', '.sql', '.r', '.go', '.rs', '.rb',
'.php', '.swift', '.kt', '.scala', '.cpp', '.c', '.h', '.java', '.m', '.svg']);
const TABLE_EXTS = new Set(['.csv', '.tsv', '.xlsx', '.xls']);
let _currentFilePath = null;
let _currentContent = null;
@@ -33,6 +34,13 @@ function isTextFile(fileName) {
return dot >= 0 && TEXT_EXTS.has(ext.substring(dot));
}
/** 判断表格文件(CSV/TSV 文本按列着色;XLSX/XLS 由后端提取为 TSV 后同样按列着色) */
function isTableFile(fileName) {
const ext = (fileName || '').toLowerCase();
const dot = ext.lastIndexOf('.');
return dot >= 0 && TABLE_EXTS.has(ext.substring(dot));
}
/**
* 打开文件预览浮窗
* @param {string} filePath - 文件路径
@@ -84,6 +92,8 @@ export function openFilePreview(filePath, fileName, content, opts = {}) {
_renderImage(body, content, fileName);
} else if (isHtml(fileName) && !opts.asText) {
_renderPlainText(body, content);
} else if (isTableFile(fileName)) {
_renderTable(body, content, fileName);
} else if (isTextFile(fileName)) {
_renderPlainText(body, content);
} else if (content && content.startsWith && content.startsWith('data:')) {
@@ -162,6 +172,96 @@ function _renderPlainText(body, content) {
codeLines.addEventListener('contextmenu', (e) => _onCodeContextMenu(e));
}
// ===== 表格预览(复用 plaintext 预览 UI,不同列不同颜色) =====
/** 按主题区分的列色板(暗色 / 亮色各一套,保证可读性) */
const TABLE_COLUMN_COLORS = {
dark: ['#e06c75', '#61afef', '#98c379', '#e5c07b', '#c678dd', '#56b6c2', '#d19a66', '#f472b6', '#7f9cf5', '#4ade80'],
light: ['#c0392b', '#2471a3', '#1e8449', '#b7950b', '#7d3c98', '#117a65', '#ca6f1e', '#a93226', '#2e86c1', '#229954'],
};
/** 判断分隔符:TSV 用 tab,CSV 用逗号,XLSX/XLS(后端已提取为 TSV)用 tab,其他按首行内容猜测 */
function _getDelimiter(fileName, content) {
const ext = (fileName || '').toLowerCase();
if (ext.endsWith('.tsv') || ext.endsWith('.xlsx') || ext.endsWith('.xls')) return '\t';
if (ext.endsWith('.csv')) return ',';
const firstLine = (content || '').split('\n')[0] || '';
return firstLine.includes('\t') ? '\t' : ',';
}
/** 解析单行分隔符文本(支持引号包裹的字段,如 "a,b") */
function _splitDelimited(line, delim) {
const cells = [];
let cur = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (inQuotes) {
if (ch === '"') {
if (line[i + 1] === '"') { cur += '"'; i++; }
else inQuotes = false;
} else cur += ch;
} else if (ch === '"') {
inQuotes = true;
} else if (ch === delim) {
cells.push(cur);
cur = '';
} else {
cur += ch;
}
}
cells.push(cur);
return cells;
}
/** 表格预览:保留行号/选中引用交互,单元格按列着色 */
function _renderTable(body, content, fileName) {
const text = content || '';
const delim = _getDelimiter(fileName, text);
const lines = text.split('\n');
const colors = document.body.classList.contains('theme-dark') ? TABLE_COLUMN_COLORS.dark : TABLE_COLUMN_COLORS.light;
const wrap = document.createElement('div');
wrap.className = 'fp-code-wrap fp-table-wrap';
// 行号列
const lineNos = document.createElement('div');
lineNos.className = 'fp-line-nos';
for (let i = 1; i <= lines.length; i++) {
const span = document.createElement('span');
span.textContent = i;
lineNos.appendChild(span);
}
// 数据列:每行一个 span,单元格按列序号着色,首行作为表头加粗
const codeLines = document.createElement('div');
codeLines.className = 'fp-code-lines';
lines.forEach((line, i) => {
const row = document.createElement('span');
row.setAttribute('data-line', i + 1);
const cells = _splitDelimited(line, delim);
if (cells.length === 1 && !line.trim()) {
row.textContent = '\u00A0';
} else {
cells.forEach((cell, ci) => {
const cellEl = document.createElement('span');
cellEl.className = 'fp-tbl-cell' + (i === 0 ? ' fp-tbl-cell--header' : '');
cellEl.textContent = cell;
cellEl.style.color = colors[ci % colors.length];
row.appendChild(cellEl);
});
}
codeLines.appendChild(row);
});
wrap.appendChild(lineNos);
wrap.appendChild(codeLines);
body.appendChild(wrap);
// 右键菜单:引用选中行
codeLines.addEventListener('contextmenu', (e) => _onCodeContextMenu(e));
}
function _onCodeContextMenu(e) {
const sel = window.getSelection();
if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
-12
View File
@@ -211,12 +211,6 @@ function buildPendingCard(pending, isSubAgent) {
inputRow.appendChild(sendBtn);
cardBody.appendChild(inputRow);
// 跳过按钮
const btnRow = document.createElement("div");
btnRow.className = "pending-overlay-btn-row";
btnRow.appendChild(makeDecisionBtn("跳过", "skip", "", () => submitSingle(pending.id, "skip", "", card)));
cardBody.appendChild(btnRow);
} else if (interactionType === "selection") {
if (pending.questions && Array.isArray(pending.questions) && pending.questions.length > 0) {
pending.questions.forEach((q) => {
@@ -289,12 +283,6 @@ function buildPendingCard(pending, isSubAgent) {
});
cardBody.appendChild(pillsRow);
}
// 跳过按钮
const btnRow = document.createElement("div");
btnRow.className = "pending-overlay-btn-row";
btnRow.appendChild(makeDecisionBtn("跳过", "skip", "", () => submitSingle(pending.id, "skip", "", card)));
cardBody.appendChild(btnRow);
}
card.appendChild(cardBody);
+45 -9
View File
@@ -139,16 +139,19 @@ function buildAgentCard(agent, idx) {
header.className = "agent-card-header";
const h4 = document.createElement("h4");
h4.textContent = `Agent: ${agent.name || "(\u672A\u547D\u540D)"}`;
const removeBtn = document.createElement("button");
removeBtn.className = "agent-card-remove";
removeBtn.textContent = "\u2715";
removeBtn.title = "\u5220\u9664\u6B64 Agent";
removeBtn.addEventListener("click", () => {
state.cachedAgentConfigs.splice(idx, 1);
renderAgentConfigs();
});
header.appendChild(h4);
header.appendChild(removeBtn);
// 主 Agent 不可删除
if (agent.role !== "main") {
const removeBtn = document.createElement("button");
removeBtn.className = "agent-card-remove";
removeBtn.textContent = "\u2715";
removeBtn.title = "\u5220\u9664\u6B64 Agent";
removeBtn.addEventListener("click", () => {
state.cachedAgentConfigs.splice(idx, 1);
renderAgentConfigs();
});
header.appendChild(removeBtn);
}
const nameField = makeField("\u540D\u79F0", "text", agent.name || "", (v) => { agent.name = v; });
const descField = makeField("\u80FD\u529B\u63CF\u8FF0 (description)", "text", agent.description || "", (v) => { agent.description = v; });
@@ -1135,6 +1138,8 @@ function renderThemePanel() {
const bgApplyBtn = $("bgImageApplyBtn");
const bgResetBtn = $("bgImageResetBtn");
const bgStatus = $("bgPreviewStatus");
const bgPickerBtn = $("bgImagePickerBtn");
const bgFileInput = $("bgImageFileInput");
if (bgInput) {
bgInput.value = getBgImage();
const applyBg = () => {
@@ -1154,6 +1159,37 @@ function renderThemePanel() {
}
// 回车也应用
bgInput.onkeydown = (e) => { if (e.key === 'Enter') applyBg(); };
// 图片选择器按钮点击事件
if (bgPickerBtn && bgFileInput) {
bgPickerBtn.onclick = () => {
bgFileInput.click();
};
// 文件选择事件
bgFileInput.onchange = (e) => {
const file = e.target.files?.[0];
if (file) {
// 使用文件路径(Electron 环境)或创建临时 URL(Web 环境)
if (window.electronAPI && window.electronAPI.getFilePath) {
// Electron 环境:获取文件路径
const filePath = window.electronAPI.getFilePath(file);
bgInput.value = filePath;
applyBg();
} else {
// Web 环境:创建临时 URL
const reader = new FileReader();
reader.onload = (event) => {
bgInput.value = event.target?.result || "";
applyBg();
};
reader.readAsDataURL(file);
}
}
// 清空 input 以便重复选择同一文件
bgFileInput.value = "";
};
}
}
// 背景模糊度
+95 -45
View File
@@ -583,12 +583,40 @@ export const THEMES = {
};
const DEFAULT_THEME = "vscode-dark";
const STORAGE_KEY = "minor_theme";
const STORAGE_BG_KEY = "minor_bg_image";
const STORAGE_FONT_KEY = "minor_font";
const STORAGE_FONT_SIZE_KEY = "minor_font_size";
const STORAGE_BLUR_KEY = "minor_bg_blur";
const STORAGE_VIGNETTE_KEY = "minor_bg_vignette";
// 主题设置统一内存态:全部落盘到服务端 theme_config.json(不使用 localStorage)
let _settings = {
theme: DEFAULT_THEME,
bg_image: "",
font_family: "",
font_size: "",
bg_blur: "",
bg_vignette: "",
};
let _loadingTheme = false; // 从服务端加载期间不写回,避免覆盖已有配置
let _persistTimer = null;
/** 防抖写回服务端 theme_config.json(仅保存用户主动修改,加载期间跳过) */
function _persistThemeConfig() {
if (_loadingTheme) return;
if (_persistTimer) clearTimeout(_persistTimer);
_persistTimer = setTimeout(() => {
_persistTimer = null;
try {
api("/api/config/theme", {
method: "POST",
body: JSON.stringify({
theme: _settings.theme,
bg_image: _settings.bg_image,
font_family: _settings.font_family,
font_size: _settings.font_size,
bg_blur: _settings.bg_blur,
bg_vignette: _settings.bg_vignette,
}),
});
} catch { /* ignore */ }
}, 300);
}
// 字体预设
export const FONT_PRESETS = {
@@ -611,14 +639,14 @@ export const FONT_SIZE_PRESETS = {
"20px": "20px",
};
const getFontKey = (k) => ({ family: STORAGE_FONT_KEY, size: STORAGE_FONT_SIZE_KEY }[k]);
export function getFontSetting(key) {
try { return localStorage.getItem(getFontKey(key)) || ""; } catch { return ""; }
return key === "family" ? _settings.font_family : key === "size" ? _settings.font_size : "";
}
export function setFontSetting(key, val) {
try { localStorage.setItem(getFontKey(key), val || ""); } catch {}
if (key === "family") _settings.font_family = val || "";
else if (key === "size") _settings.font_size = val || "";
applyFontSetting(key, val);
_persistThemeConfig();
}
export function applyFontSetting(key, val) {
const r = document.documentElement;
@@ -638,15 +666,12 @@ function localPathToUrl(localPath) {
/** 获取/设置自定义背景图 */
export function getBgImage() {
try { return localStorage.getItem(STORAGE_BG_KEY) || ""; }
catch { return ""; }
return _settings.bg_image;
}
export function setBgImage(url) {
try { localStorage.setItem(STORAGE_BG_KEY, url || ""); }
catch {}
applyBgImage(url);
// 异步同步到服务端
try { api("/api/config/theme", { method: "POST", body: JSON.stringify({ bg_image: url || "" }) }); } catch {}
_settings.bg_image = url || "";
applyBgImage(_settings.bg_image);
_persistThemeConfig();
}
export function applyBgImage(url) {
const r = document.documentElement;
@@ -666,8 +691,7 @@ export function applyBgImage(url) {
/** 获取当前保存的主题 */
export function getCurrentTheme() {
try { return localStorage.getItem(STORAGE_KEY) || DEFAULT_THEME; }
catch { return DEFAULT_THEME; }
return _settings.theme;
}
/** 应用主题到页面 */
@@ -685,8 +709,9 @@ export function applyTheme(themeId) {
Object.entries(theme.edit).forEach(([k, v]) => r.style.setProperty(k, v));
}
// 持久化
try { localStorage.setItem(STORAGE_KEY, themeId); } catch {}
// 更新内存态并落盘(加载期间跳过,避免覆盖服务端已有配置)
_settings.theme = THEMES[themeId] ? themeId : DEFAULT_THEME;
_persistThemeConfig();
// 触发 body 类名切换(用于全局暗/亮模式判断)
// 优先使用主题对象的 dark 字段;缺失时回退到 id 字符串推断
@@ -695,28 +720,27 @@ export function applyTheme(themeId) {
document.body.classList.toggle("theme-light", !isDark);
}
/** 初始化:从 localStorage 恢复主题,并从服务端同步背景图 */
/** 初始化:先用默认主题渲染,再从服务端 theme_config.json 恢复全部设置 */
export function initTheme() {
const id = getCurrentTheme();
applyTheme(id);
// 先从 localStorage 快读,再从服务端同步(异步)
applyBgImage(getBgImage());
applyFontSetting('family', getFontSetting('family'));
applyFontSetting('size', getFontSetting('size'));
applyBgBlur(getBgBlur());
applyBgVignette(getBgVignette());
// 异步从服务端同步主题配置
loadThemeConfig();
return id;
_loadingTheme = true;
applyTheme(DEFAULT_THEME);
applyBgImage("");
applyFontSetting("family", "");
applyFontSetting("size", "");
applyBgBlur("");
applyBgVignette("");
loadThemeConfig(); // 完成后在 finally 中解除 _loadingTheme
return DEFAULT_THEME;
}
// ===== 背景模糊度 =====
export function getBgBlur() {
try { return localStorage.getItem(STORAGE_BLUR_KEY) || ""; } catch { return ""; }
return _settings.bg_blur;
}
export function setBgBlur(val) {
try { localStorage.setItem(STORAGE_BLUR_KEY, val || ""); } catch {}
applyBgBlur(val);
_settings.bg_blur = val || "";
applyBgBlur(_settings.bg_blur);
_persistThemeConfig();
}
export function applyBgBlur(val) {
// val 为空字符串时使用主题默认值(由 applyTheme 设置),否则覆盖
@@ -730,11 +754,12 @@ export function applyBgBlur(val) {
// ===== 背景暗角 =====
export function getBgVignette() {
try { return localStorage.getItem(STORAGE_VIGNETTE_KEY) || ""; } catch { return ""; }
return _settings.bg_vignette;
}
export function setBgVignette(val) {
try { localStorage.setItem(STORAGE_VIGNETTE_KEY, val || ""); } catch {}
applyBgVignette(val);
_settings.bg_vignette = val || "";
applyBgVignette(_settings.bg_vignette);
_persistThemeConfig();
}
export function applyBgVignette(val) {
const r = document.documentElement;
@@ -745,14 +770,39 @@ export function applyBgVignette(val) {
}
}
/** 从服务端加载主题配置(覆盖 localStorage 缓存) */
/** 从服务端 theme_config.json 加载主题配置并应用(不使用 localStorage) */
export async function loadThemeConfig() {
try {
const res = await api("/api/config/theme");
if (res.bg_image !== undefined) {
const img = res.bg_image || "";
try { localStorage.setItem(STORAGE_BG_KEY, img); } catch {}
applyBgImage(img);
_loadingTheme = true;
if (res.theme && THEMES[res.theme]) {
applyTheme(res.theme);
}
} catch {}
if (res.bg_image !== undefined) {
_settings.bg_image = res.bg_image || "";
applyBgImage(_settings.bg_image);
}
if (res.font_family !== undefined) {
// 过滤旧版默认占位值(msjh 等非真实字体)
const fam = (res.font_family || "").trim();
_settings.font_family = (fam && fam !== "msjh" && fam !== "msjhbd") ? fam : "";
applyFontSetting("family", _settings.font_family);
}
if (res.font_size !== undefined) {
_settings.font_size = res.font_size || "";
applyFontSetting("size", _settings.font_size);
}
if (res.bg_blur !== undefined) {
_settings.bg_blur = res.bg_blur || "";
applyBgBlur(_settings.bg_blur);
}
if (res.bg_vignette !== undefined) {
_settings.bg_vignette = res.bg_vignette || "";
applyBgVignette(_settings.bg_vignette);
}
} catch {
// 服务端不可达:保持默认设置
} finally {
_loadingTheme = false;
}
}
+3 -1
View File
@@ -626,7 +626,9 @@ function renderToolCallRow(call, options = {}) {
icon.title = isRunning ? "点击折叠" : "点击展开工具返回值";
icon.addEventListener("click", (e) => {
e.stopPropagation();
row.classList.toggle("tool-call-row--expanded");
const nowExpanded = row.classList.toggle("tool-call-row--expanded");
// 展开后若详情在可视区外,滚动到可见位置(block:nearest 已可见时不滚动)
if (nowExpanded) detailEl.scrollIntoView({ block: "nearest" });
});
}
}