优化前缀缓存机制和UI,并引入一些新工具.preview

This commit is contained in:
minorQAQ committed 2026-08-19 15:16:17 +08:00
1 parent 863abe7c76
commit 6131c2563e
102 files changed
+9087 -4351

No files matched your search

+94 -408
View File
@@ -2,7 +2,7 @@
// 保留: 文件树渲染、文档修改追踪、外部变更处理、快捷键、拖拽引用
// 移除: Monaco 编辑器、标签页、Agent 面板
import { $, escapeHtml, showToast, dedupePendingFiles } from './utils.js';
import { $, escapeHtml, showToast, dedupePendingFiles, filterOutUnsupported, isUnsupportedFile } from './utils.js';
import { api } from './api.js';
import { state } from './state.js';
import {
@@ -14,12 +14,9 @@ import {
} from './toolcalls.js';
import { renderDocTree, setDocTreeDeps } from './doc-tree.js';
import { openFilePreview, closeFilePreview } from './file-preview.js';
import { setDocModifications } from './doc-mod-panel.js';
// ===== DOM 引用 =====
const modeEditBtn = $("modeEditBtn");
const modeChatBtn = $("modeChatBtn");
const modeCronBtn = $("modeCronBtn");
const modeCronToggleBtn = $("modeCronToggleBtn");
const chatPanel = $("chatPanel");
const sessionListWrap = $("sessionListWrap");
const docTreeWrap = $("docTreeWrap");
@@ -27,9 +24,7 @@ const cronListWrap = $("cronListWrap");
const docTree = $("docTree");
const sidebarHint = document.querySelector(".sidebar-hint");
// ===== 文档修改状态 =====
let docModifications = {};
let currentTaskModifications = [];
// ===== 外部文件变更 =====
let _externalChanges = {};
let _fsChangeTimer = null;
let _fsApplying = false;
@@ -86,52 +81,88 @@ function isBinaryFile(name) {
// ===== 模式切换 =====
let _modeSwitchCooldown = false;
/** 同步顶栏定时任务按钮的按下状态(cron 模式按下高亮,其余复位) */
function syncModeToggleBtn() {
if (!modeCronToggleBtn) return;
const isCron = state.mode === "cron";
modeCronToggleBtn.classList.toggle("is-active", isCron);
modeCronToggleBtn.setAttribute("aria-pressed", isCron ? "true" : "false");
modeCronToggleBtn.title = isCron ? "返回对话" : "定时任务";
}
export function switchToMode(mode) {
// 防连点:短时间内禁止重复切换
if (_modeSwitchCooldown) return;
_modeSwitchCooldown = true;
setTimeout(() => { _modeSwitchCooldown = false; }, 300);
// 注:edit/chat/cron 模式均只影响左侧栏(文件树/会话列表/定时任务),
// 右侧聊天区保持不变,因此 Agent 运行中也可以自由切换,无需等待
// Edit 已迁入右栏:模式切换只影响左侧栏(会话列表/定时任务)与右栏显隐,
// 聊天区保持不变,Agent 运行中也可自由切换
state.mode = mode;
syncModeToggleBtn();
if (mode === "edit") {
state.editMode = true;
modeChatBtn?.classList.remove("active");
modeEditBtn?.classList.add("active");
modeCronBtn?.classList.remove("active");
// 编辑模式只影响左侧栏:显示文件树,隐藏会话列表与定时任务列表;聊天区域保持不变
sessionListWrap?.classList.add("mode-hidden");
// 左栏始终显示会话列表;文件树在右栏
sessionListWrap?.classList.remove("mode-hidden");
cronListWrap?.classList.add("mode-hidden");
if (sidebarHint) sidebarHint.classList.add("mode-hidden");
docTreeWrap?.classList.remove("mode-hidden");
if (sidebarHint) sidebarHint.classList.remove("mode-hidden");
setRightPanel(true);
_onLeaveCron();
} else if (mode === "cron") {
state.editMode = false;
modeChatBtn?.classList.remove("active");
modeEditBtn?.classList.remove("active");
modeCronBtn?.classList.add("active");
chatPanel?.classList.remove("mode-hidden");
sessionListWrap?.classList.add("mode-hidden");
docTreeWrap?.classList.add("mode-hidden");
if (sidebarHint) sidebarHint.classList.add("mode-hidden");
cronListWrap?.classList.remove("mode-hidden");
setRightPanel(false);
closeFilePreview();
_onEnterCron();
} else {
// chat 模式
state.editMode = false;
modeChatBtn?.classList.add("active");
modeEditBtn?.classList.remove("active");
modeCronBtn?.classList.remove("active");
chatPanel?.classList.remove("mode-hidden");
sessionListWrap?.classList.remove("mode-hidden");
if (sidebarHint) sidebarHint.classList.remove("mode-hidden");
docTreeWrap?.classList.add("mode-hidden");
cronListWrap?.classList.add("mode-hidden");
setRightPanel(false);
closeFilePreview();
_onLeaveCron();
}
// 同步回合导航条显隐(cron 模式复用聊天容器,需隐藏导航条)
import('./turn-nav.js').then((m) => m.syncTurnNavVisibility()).catch(() => {});
}
// ===== git 状态(文件树着色:新增绿 / 修改橙 / 删除橙+删除线) =====
let _gitStatusTimer = null;
export function refreshGitStatus() {
if (!state._docRootPath) return;
if (_gitStatusTimer) clearTimeout(_gitStatusTimer);
_gitStatusTimer = setTimeout(async () => {
try {
const rootPath = state._docRootPath;
const data = await api(`/api/workspace/git-status?path=${encodeURIComponent(rootPath)}`);
const map = {};
for (const s of data.status || []) map[s.path] = s.state;
state.gitStatus = map;
renderDocTree();
} catch { /* git 不可用/非仓库时静默,保持无着色 */ }
}, 150);
}
// ===== 右栏(Edit 文件树)控制 =====
export function setRightPanel(open) {
const appRoot = document.getElementById("app");
if (!appRoot) return;
appRoot.classList.toggle("right-panel-open", !!open);
const rp = document.getElementById("rightPanel");
if (rp) rp.setAttribute("aria-hidden", open ? "false" : "true");
// 打开时刷新 git 状态着色
if (open) {
try {
window.dispatchEvent(new CustomEvent("git-status-changed"));
} catch { /* ignore */ }
}
}
// ===== Cron 模式进入/离开钩子(由 app.js 注入,避免循环依赖) =====
@@ -208,8 +239,6 @@ export async function openFolder(folderPath) {
}
state.docFiles = [];
state.docOpenTabs = [];
docModifications = {};
currentTaskModifications = [];
_externalChanges = {};
for (const f of uniqueFiles) {
@@ -218,6 +247,8 @@ export async function openFolder(folderPath) {
renderDocTree();
renderDocTabs && renderDocTabs();
// 打开文件夹后刷新 git 状态着色
refreshGitStatus();
if (state.docFiles.length > 0) {
state.activeDocFile = state.docFiles[0].path;
@@ -237,20 +268,11 @@ export async function openFolder(folderPath) {
}
export function showOpenButtons() {
const snapDir = _snapDir();
if (snapDir) {
try { deleteFile(snapDir, ''); } catch {}
}
state._docRootPath = null;
state._docRootName = null;
state.docFiles = [];
state.docOpenTabs = [];
docModifications = {};
currentTaskModifications = [];
_externalChanges = {};
_snapshots = [];
_turnCounter = 0;
_acceptedBaselines = {};
renderDocTree();
renderDocTabs && renderDocTabs();
}
@@ -264,7 +286,6 @@ export function renderDocTabs() {
export async function loadDocContent(filePath) {
if (!state._docRootPath) return;
const fileName = filePath.split(/[/\\]/).pop() || filePath;
const status = docModifications[filePath]?.type || "";
try {
if (isPreviewableImage(fileName)) {
@@ -278,7 +299,7 @@ export async function loadDocContent(filePath) {
const res = await readTextFile(filePath, state._docRootPath);
if (res && res.status === 'ok') {
openFilePreview(filePath, fileName, res.content || '',
{ status, onDownload: () => downloadFile(filePath, fileName) });
{ onDownload: () => downloadFile(filePath, fileName) });
}
} else if (isPreviewableTable(fileName)) {
// XLSX/XLS:后端提取为 TSV 文本,走与 CSV/TSV 一致的按列着色预览
@@ -288,10 +309,10 @@ export async function loadDocContent(filePath) {
try {
const tsv = await api('/api/table/text?path=' + encodeURIComponent(absPath));
openFilePreview(absPath, fileName, (tsv && tsv.content) || '',
{ status, onDownload: () => downloadFile(absPath, fileName) });
{ onDownload: () => downloadFile(absPath, fileName) });
} catch (e) {
openFilePreview(absPath, fileName, '(加载失败: ' + (e.message || e) + ')',
{ status, onDownload: () => downloadFile(absPath, fileName) });
{ onDownload: () => downloadFile(absPath, fileName) });
}
} else if (isBinaryFile(fileName)) {
showToast('二进制文件不支持在线查看', 'warning');
@@ -299,15 +320,15 @@ export async function loadDocContent(filePath) {
const res = await readTextFile(filePath, state._docRootPath);
if (res && res.status === 'ok') {
openFilePreview(filePath, fileName, res.content || '',
{ status, onDownload: () => downloadFile(filePath, fileName) });
{ onDownload: () => downloadFile(filePath, fileName) });
} else {
openFilePreview(filePath, fileName, '(无法读取文件)',
{ status, onDownload: () => downloadFile(filePath, fileName) });
{ onDownload: () => downloadFile(filePath, fileName) });
}
}
} catch {
openFilePreview(filePath, fileName, '(加载失败)',
{ status, onDownload: () => downloadFile(filePath, fileName) });
{ onDownload: () => downloadFile(filePath, fileName) });
}
}
@@ -319,19 +340,18 @@ export async function openFileAsText(filePath) {
showToast('二进制文件不支持在线查看', 'warning');
return;
}
const status = docModifications[filePath]?.type || "";
try {
const res = await readTextFile(filePath, state._docRootPath);
if (res && res.status === 'ok') {
openFilePreview(filePath, fileName, res.content || '',
{ status, onDownload: () => downloadFile(filePath, fileName), asText: true });
{ onDownload: () => downloadFile(filePath, fileName), asText: true });
} else {
openFilePreview(filePath, fileName, '(无法读取文件)',
{ status, onDownload: () => downloadFile(filePath, fileName) });
{ onDownload: () => downloadFile(filePath, fileName) });
}
} catch {
openFilePreview(filePath, fileName, '(加载失败)',
{ status, onDownload: () => downloadFile(filePath, fileName) });
{ onDownload: () => downloadFile(filePath, fileName) });
}
}
@@ -373,329 +393,6 @@ async function downloadFile(filePath, fileName) {
} catch { /* ignore */ }
}
// ===== 快照系统(类 Git,持久化到磁盘)=====
// 目录结构: {snapshots_base}/{workspace_key}/
// ├── _accepted/ # 接受基线
// ├── turn_1/ # 第一轮快照
// └── turn_2/ # 第二轮快照
// snapshots_base 由后端 /api/workspace 返回,默认为 workspace/.minor_snapshots
let _snapshots = []; // [{ turn: N, rootPath, timestamp, files: Map<path, {content, size, isBinary}> }]
let _turnCounter = 0;
let _acceptedBaselines = {}; // { path: { content, snapshotIdx } }
function _workspaceKey() {
if (!state._docRootPath) return null;
return state._docRootPath.replace(/[/\\]$/, '')
.replace(/^[A-Za-z]:/, '')
.replace(/[/\\:]/g, '_')
.replace(/^_+/, '');
}
function _snapDir() {
if (!state._docRootPath) return null;
const wsKey = _workspaceKey();
if (!wsKey) return null;
const base = window.__minorSnapBase
|| (state._docRootPath.replace(/[/\\]$/, '') + '/.minor_snapshots');
return base + '/' + wsKey;
}
function _turnDir(turn) { return _snapDir() + '/turn_' + turn; }
function _acceptDir() { return _snapDir() + '/_accepted'; }
/** 由外部设置快照基础目录(在 initWorkspace 中调用) */
export function setSnapBaseDir(baseDir) {
window.__minorSnapBase = baseDir.replace(/\\/g, '/').replace(/\/$/, '');
}
/** 删除快照目录中的文件 */
async function _deleteSnapFile(relPath, srcDirRel) {
const rootPath = state._docRootPath;
const srcRel = srcDirRel.replace(rootPath.replace(/[/\\]$/, ''), '').replace(/^[/\\]+/, '') + '/' + relPath.replace(/\\/g, '/');
try { await deleteFile(srcRel, rootPath); } catch {}
}
/** 复制文件(文本或二进制)到目标路径,目标路径相对于 workspace */
async function _copyFileTo(relPath, targetDirRel) {
const rootPath = state._docRootPath;
const tgtRel = targetDirRel.replace(rootPath.replace(/[/\\]$/, ''), '').replace(/^[/\\]+/, '') + '/' + relPath.replace(/\\/g, '/');
try {
// Try text first
const text = await readTextFile(relPath, rootPath);
await writeTextFile(tgtRel, text, rootPath);
return { content: text, isBinary: false };
} catch {
// Binary
try {
const bin = await readBinaryFile(relPath, rootPath);
if (bin && bin.base64) {
// writeFromBuffer takes (parentDir, fileName, base64)
const parentDir = tgtRel.substring(0, tgtRel.lastIndexOf('/'));
const fileName = relPath.replace(/\\/g, '/').split('/').pop();
await writeFromBuffer(parentDir, fileName, bin.base64);
return { content: bin, isBinary: true };
}
} catch {}
}
return null;
}
/** 恢复文件(从源目录复制到 workspace)*/
async function _restoreFile(relPath, srcDirRel) {
const rootPath = state._docRootPath;
const srcRel = srcDirRel.replace(rootPath.replace(/[/\\]$/, ''), '').replace(/^[/\\]+/, '') + '/' + relPath.replace(/\\/g, '/');
try {
// Try text
const text = await readTextFile(srcRel, rootPath);
await writeTextFile(relPath, text, rootPath);
return true;
} catch {
// Try binary
try {
const bin = await readBinaryFile(srcRel, rootPath);
if (bin && bin.base64) {
const parentDir = relPath.replace(/\\/g, '/').split('/').slice(0, -1).join('/') || '.';
const fileName = relPath.replace(/\\/g, '/').split('/').pop();
await writeFromBuffer(parentDir, fileName, bin.base64);
return true;
}
} catch {}
}
return false;
}
/** 删除某文件在所有快照中的备份(接受或撤销时清理) */
async function _purgeFileFromSnapshots(relPath) {
for (const s of _snapshots) {
s.files.delete(relPath);
await _deleteSnapFile(relPath, _turnDir(s.turn));
}
// Also delete from accepted baselines
await _deleteSnapFile(relPath, _acceptDir());
}
/** 删除某个 turn 目录 */
async function _deleteTurnDir(turn) {
const dir = _turnDir(turn);
try { await deleteFile(dir, ''); } catch {}
}
/** 保存任务前快照(含完整文件内容,持久化到磁盘) */
export async function snapshotFilesForTask() {
if (!state._docRootPath) return;
const rootPath = state._docRootPath;
const files = new Map();
const turn = ++_turnCounter;
const tDir = _turnDir(turn);
for (const f of state.docFiles) {
const result = await _copyFileTo(f.path, tDir);
if (result) {
files.set(f.path, { content: result.content, size: f.size, isBinary: result.isBinary });
} else {
files.set(f.path, { content: null, size: f.size, isBinary: false });
}
}
_snapshots.push({
turn,
rootPath,
timestamp: Date.now(),
files,
});
// 首次快照时初始化接受基线(保存到磁盘)
if (_snapshots.length === 1) {
for (const [fp, info] of files) {
await _copyFileTo(fp, _acceptDir());
_acceptedBaselines[fp] = { content: info.content, snapshotIdx: 0, isBinary: info.isBinary };
}
}
}
/** 任务结束后对比快照,将Agent产生的变更记录到docModifications */
export function detectTaskFileChanges() {
if (_snapshots.length === 0 || !state._docRootPath) return;
const preSnap = _snapshots[_snapshots.length - 1];
const preMap = preSnap.files;
const curMap = new Map(state.docFiles.map(f => [f.path, f]));
let changed = false;
for (const [fp, f] of curMap) {
const pre = preMap.get(fp);
if (!pre) {
// 新文件
docModifications[fp] = { original: "", current: f.path, type: "new", snapshotIdx: preSnap.turn };
if (!currentTaskModifications.includes(fp)) currentTaskModifications.push(fp);
changed = true;
} else if (pre.size !== f.size) {
// 内容被修改
docModifications[fp] = { original: pre.content || f.path, current: f.path, type: "modified", snapshotIdx: preSnap.turn, oldContent: pre.content };
if (!currentTaskModifications.includes(fp)) currentTaskModifications.push(fp);
changed = true;
}
}
for (const [fp] of preMap) {
if (!curMap.has(fp)) {
// 被删除
const preInfo = preMap.get(fp);
docModifications[fp] = { original: preInfo.content || fp, current: "", type: "deleted", snapshotIdx: preSnap.turn, oldContent: preInfo.content };
if (!currentTaskModifications.includes(fp)) currentTaskModifications.push(fp);
changed = true;
}
}
if (changed) {
setDocModifications(docModifications);
renderDocTree();
try { import('./doc-mod-panel.js').then(m => { m.setDocModifications(docModifications); m.renderDocModPanel(); }); } catch {}
}
}
/** 计算两个文本内容的行级 diff(返回简化差异列表) */
export function computeLineDiff(oldText, newText) {
if (oldText == null || newText == null) return null;
const oldLines = (oldText || '').split('\n');
const newLines = (newText || '').split('\n');
return _lineDiff(oldLines, newLines);
}
function _lineDiff(oldLines, newLines) {
// 回溯 LCS
const m = oldLines.length, n = newLines.length;
// 对短文本使用简单 diff
if (m === 0 && n === 0) return [];
if (m === 0) return [{ type: 'add', startLine: 1, endLine: n, lines: newLines }];
if (n === 0) return [{ type: 'remove', startLine: 1, endLine: m, lines: oldLines }];
// LCS 矩阵
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (oldLines[i - 1] === newLines[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// 回溯
const diffs = [];
let i = m, j = n;
const tempOps = [];
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
tempOps.unshift({ type: 'keep', oldLine: i, newLine: j });
i--; j--;
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
tempOps.unshift({ type: 'add', newLine: j, line: newLines[j - 1] });
j--;
} else {
tempOps.unshift({ type: 'remove', oldLine: i, line: oldLines[i - 1] });
i--;
}
}
// 合并相邻的同类型差异
for (const op of tempOps) {
if (op.type === 'keep') continue;
const last = diffs[diffs.length - 1];
if (last && last.type === op.type) {
if (op.type === 'add') {
last.endLine = op.newLine;
last.lines.push(op.line);
} else {
last.endLine = op.oldLine;
last.lines.push(op.line);
}
} else {
if (op.type === 'add') {
diffs.push({ type: 'add', startLine: op.newLine, endLine: op.newLine, lines: [op.line] });
} else {
diffs.push({ type: 'remove', startLine: op.oldLine, endLine: op.oldLine, lines: [op.line] });
}
}
}
return diffs;
}
/** 接受单个文件的修改(更新接受基线,持久化,清理快照)*/
export async function acceptFileModification(filePath) {
const rootPath = state._docRootPath;
if (!rootPath) return;
const mod = docModifications[filePath];
if (!mod) return;
if (mod.type === 'deleted') {
// 接受删除 → 从修改列表和接受基线中移除
delete docModifications[filePath];
delete _acceptedBaselines[filePath];
await _deleteSnapFile(filePath, _acceptDir());
} else {
// 接受新增/修改 → 更新磁盘上的接受基线
const result = await _copyFileTo(filePath, _acceptDir());
_acceptedBaselines[filePath] = {
content: result ? result.content : null,
snapshotIdx: _snapshots.length,
isBinary: result ? result.isBinary : false
};
delete docModifications[filePath];
}
// 清理该文件在所有快照中的备份
await _purgeFileFromSnapshots(filePath);
currentTaskModifications = currentTaskModifications.filter(f => f !== filePath);
setDocModifications(docModifications);
renderDocTree();
}
/** 撤销单个文件的修改(回到接受基线,从磁盘恢复,支持二进制)*/
export async function rejectFileModification(filePath) {
const rootPath = state._docRootPath;
if (!rootPath) return;
const mod = docModifications[filePath];
if (!mod) return;
if (mod.type === 'deleted') {
// 撤销删除 → 从接受基线恢复文件
await _restoreFile(filePath, _acceptDir());
} else if (mod.type === 'new') {
// 撤销新增 → 删除文件
try { await deleteFile(filePath, rootPath); } catch {}
} else {
// 撤销修改 → 从接受基线恢复文件
await _restoreFile(filePath, _acceptDir());
}
// 清理该文件在所有快照中的备份
await _purgeFileFromSnapshots(filePath);
delete docModifications[filePath];
currentTaskModifications = currentTaskModifications.filter(f => f !== filePath);
setDocModifications(docModifications);
renderDocTree();
// 刷新文件列表
setTimeout(() => openFolder(rootPath), 500);
}
// 向后兼容导出
export function getDocModifications() { return docModifications; }
export function getCurrentTaskModifications() { return currentTaskModifications; }
export async function acceptAllModifications() {
for (const fp of Object.keys(docModifications)) await acceptFileModification(fp);
}
export async function rejectAllModifications() {
for (const fp of Object.keys(docModifications)) await rejectFileModification(fp);
}
// ===== 外部文件变更(实时自动刷新,类似 VS Code) =====
function handleFsChange(events) {
if (_fsChangeTimer) clearTimeout(_fsChangeTimer);
@@ -743,13 +440,6 @@ async function applyFsChanges(events) {
if (changed) {
state.docFiles = currentFiles;
renderDocTree();
// 同步外部变更到文档修改面板
const extMods = {};
for (const [fp, ec] of Object.entries(_externalChanges)) {
extMods[fp] = { original: fp, current: fp, type: ec.type === 'externally_added' ? 'new' : ec.type === 'externally_modified' ? 'modified' : 'deleted' };
}
setDocModifications({ ...docModifications, ...extMods });
try { import('./doc-mod-panel.js').then(m => m.renderDocModPanel()); } catch {}
}
} finally {
_fsApplying = false;
@@ -759,8 +449,6 @@ async function applyFsChanges(events) {
export function clearExternalChange(filePath) {
delete _externalChanges[filePath];
renderDocTree();
setDocModifications(docModifications);
try { import('./doc-mod-panel.js').then(m => m.renderDocModPanel()); } catch {}
}
// ===== 拖拽引用 =====
@@ -843,7 +531,7 @@ function setupInputDropHandler() {
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
e.stopPropagation();
const items = Array.from(e.dataTransfer.items);
// 先收集所有 entry,判断是否包含文件夹
// 先收集所有 entry,判断是否包含文件夹(视频暂不支持,直接过滤)
const allEntries = [];
for (const item of items) {
if (item.kind !== 'file') continue;
@@ -851,11 +539,13 @@ function setupInputDropHandler() {
const file = item.getAsFile();
allEntries.push({ entry, file });
}
const hasDirectory = allEntries.some(({ entry }) => entry && entry.isDirectory);
const acceptedFiles = filterOutUnsupported(allEntries.map(({ file }) => file).filter(Boolean));
const filteredEntries = allEntries.filter(({ file }) => !file || acceptedFiles.includes(file));
const hasDirectory = filteredEntries.some(({ entry }) => entry && entry.isDirectory);
if (hasDirectory) {
// 有文件夹时:只处理文件夹项,跳过文件项(避免展开文件夹内容)
for (const { entry, file } of allEntries) {
for (const { entry, file } of filteredEntries) {
if (entry && entry.isDirectory) {
const folderPath = (file && file.path) ? file.path : '';
const folderName = entry.name || (file ? file.name : '文件夹');
@@ -866,7 +556,7 @@ function setupInputDropHandler() {
// 无文件夹:图片作为普通 File 上传(HumanMessage image_url),非图片作为 @file: 引用
// Electron 拖入的 File 带 path(绝对路径)且已包含二进制内容;无 path 时回退为普通上传
let needFallback = false;
for (const { file } of allEntries) {
for (const { file } of filteredEntries) {
if (!file) continue;
if (_isImageName(file.name)) {
// 图片:直接作为普通文件上传(File 已含二进制内容)
@@ -885,7 +575,7 @@ function setupInputDropHandler() {
}
}
if (allEntries.length > 0) {
if (filteredEntries.length > 0) {
import('./chat-render.js').then(m => { if (m.renderAttachmentChips) m.renderAttachmentChips(); });
}
return;
@@ -894,7 +584,7 @@ function setupInputDropHandler() {
// 4. 回退:dataTransfer.files(无 items 时)— 图片作为普通 File 上传,非图片作为 @file: 引用
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
e.stopPropagation();
const files = Array.from(e.dataTransfer.files);
const files = filterOutUnsupported(Array.from(e.dataTransfer.files));
let needFallback = false;
for (const file of files) {
if (_isImageName(file.name)) {
@@ -954,6 +644,12 @@ export async function addDocRef(filePath, lineStart, lineEnd) {
if (!filePath) return;
const fileName = filePath.split(/[/\\]/).pop() || filePath;
// 不支持的类型(视频 / 旧版 .doc / .rtf):拒绝引用
if (isUnsupportedFile(fileName)) {
showToast(`暂不支持 ${fileName}(视频 / 旧版 .doc / .rtf)`);
return;
}
// 图片且无行号引用 → 作为普通图片上传
if (_isImageName(fileName) && !lineStart) {
// 防重复:若已作为普通文件或引用存在则跳过
@@ -1041,6 +737,12 @@ export async function addExternalDocRef(absolutePath, fileName) {
if (!absolutePath) return;
const name = fileName || absolutePath.split(/[/\\]/).pop() || absolutePath;
// 不支持的类型(视频 / 旧版 .doc / .rtf):拒绝引用
if (isUnsupportedFile(name)) {
showToast(`暂不支持 ${name}(视频 / 旧版 .doc / .rtf)`);
return;
}
// 图片 → 读取二进制作为普通图片上传
if (_isImageName(name)) {
const exists = state.pendingFiles.some((f) =>
@@ -1098,18 +800,7 @@ export function clearRefChips() {
// ===== 清除状态 =====
export function clearEditState() {
// 清理该工作空间对应的快照子目录
const snapDir = _snapDir();
if (snapDir) {
try { deleteFile(snapDir, ''); } catch {}
}
docModifications = {};
currentTaskModifications = [];
_externalChanges = {};
_snapshots = [];
_turnCounter = 0;
_acceptedBaselines = {};
setDocModifications({});
closeFilePreview();
clearRefChips();
state.docFiles = [];
@@ -1142,13 +833,13 @@ export async function sendEditChat() {
// ===== 初始化 =====
export function initEditMode() {
// 模式切换按钮
modeEditBtn?.addEventListener("click", () => {
if (!state.editMode) switchToMode("edit");
});
modeChatBtn?.addEventListener("click", () => {
// 从 edit 或 cron 模式均可切回 chat
if (state.mode !== "chat") switchToMode("chat");
// git 状态刷新事件(回撤/右栏打开/文件操作后触发)
window.addEventListener("git-status-changed", () => refreshGitStatus());
// 顶栏"定时任务"按钮:cron ↔ chat 切换(Edit 模式点击同样进入 cron)
modeCronToggleBtn?.addEventListener("click", () => {
if (state.mode !== "cron") switchToMode("cron");
else switchToMode("chat");
});
// 打开文件夹 - 移除按钮,改用工作空间选择器触发
@@ -1182,9 +873,4 @@ export function initEditMode() {
openFileAsText,
externalChanges: () => _externalChanges,
});
// 初始化后同步到 doc-mod-panel
try {
import('./doc-mod-panel.js').then(m => m.bindDocModEvents());
} catch {}
}