Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 更新记录

本项目采用 `0.0.x` 递增版本号。正式安装包、便携包和校验清单发布在
[GitHub Releases](https://github.com/PuppetWen/CDriveShiftAI/releases)。

## 0.0.3

### 修复

- 修复从托盘菜单退出时,索引器管道恰好关闭导致 Electron 主进程出现 `write EPIPE` 异常的问题。
- 退出时先停止新请求、取消重启计时器、关闭标准输入、等待索引器退出,超时后再定向结束子进程。
- 修复索引器标准输入流缺少错误监听器导致管道错误升级为未捕获异常的问题。

### 性能

- 主窗口在 DOM 可显示后立即呈现,不再只等待较晚的 `ready-to-show`。
- 全盘索引延迟到首帧之后启动,并在缓存解析与全量构建期间使用较低进程优先级。
- 移除索引辅助结构构建中的数百万次临时字符串和字符数组分配,保持相同匹配算法。
- 修复 Windows 模糊删除事件被误判为目录树删除、反复全表扫描约数百万条索引的问题。
- 文件变更按路径合并、批量持久化;真实目录删除改用前缀墓碑,不再把整个缓存重新载入工作集。
- 合并磁盘发现与文件系统查询,空间总览从多个 PowerShell 进程降低为一次短时缓存查询。
- 搜索、归属地图、分析、迁移、历史和设置页面改为按需加载。
- 三套动态背景只在交互期高帧率运行,闲置时低频刷新;总览装饰改为有限交互动画,最小化后完全暂停。
- 关闭到通知区域后销毁并按需重建界面渲染窗口,后台仅保留索引、监听、快捷键和托盘能力。
- 移除与现有主题材质重复的原生 Mica 合成层,避免静止页面持续占用 GPU 进程。
- 自动更新检查延后到首屏稳定后执行。

### 文档与验证

- 增加独立的 `README.zh-CN.md` 中文使用说明。
- 增加本更新记录文档。
- 增加应用启动、前台/托盘空闲资源和干净退出回归测试。
- 安装回归会检查 CDriveShiftAI 自身进程是否完整退出;项目不依赖也不会主动调用 .NET `InstallUtil.exe`。

## 0.0.2

- 启用安装版静默更新和便携版原路径自替换。
- 增加断点续传、自动重试、SHA-512 校验、失败回滚和启动后清理。
- 增加主题化更新进度界面。
- 增加可配置鼠标按键长按唤起独立极速搜索。
- 增强全局快捷键冲突检测。

## 0.0.1

- 首个公开测试版本。
- 提供全盘名称搜索、指定目录内容索引、目录归属分析、跨盘安全迁移与迁移恢复。
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# CDriveShiftAI

[简体中文使用说明](README.zh-CN.md) · [更新记录](CHANGELOG.md)

CDriveShiftAI 是一个 Windows 桌面端磁盘整理工具,用于:

- 搜索本机所有固定磁盘中的文件名、目录名和文件夹名;
Expand All @@ -10,7 +12,7 @@ CDriveShiftAI 是一个 Windows 桌面端磁盘整理工具,用于:

当前版本完全使用自研索引管线,不调用 Everything。

当前正式版本:`0.0.2`。安装包与便携包见
当前正式版本:`0.0.3`。安装包与便携包见
[GitHub Releases](https://github.com/PuppetWen/CDriveShiftAI/releases)。

## 已实现能力
Expand Down Expand Up @@ -122,6 +124,16 @@ AI 是可选的二次判断层,支持三类真实协议:
- 鼠标快捷操作使用 Windows Raw Input 被动监听,只在达到长按阈值时触发,不拦截原程序的短按前进、后退或中键行为;
- 所有快捷设置在控件失焦或选择完成后自动保存并立即生效,并提供实际唤起测试按钮。

### 性能与进程生命周期

- 主窗口优先完成首帧显示,全盘索引在界面可交互后延迟启动,加载内容和搜索准确率不变;
- 磁盘盘符、文件系统和空间信息合并查询并短时缓存,避免空间总览为每个盘符重复拉起 PowerShell;
- 非首页功能按需加载,降低首次启动时 Chromium 解析的 JavaScript 数量和渲染进程常驻内存;
- 像素湖境、未来中枢和月白晶境在交互期间保持流畅,闲置或失焦时自动降低背景帧率,最小化后完全暂停;
- 关闭到通知区域后释放 Chromium 渲染进程和 GPU 表面;原生增量索引、文件变化监听、托盘菜单与全局快捷键继续运行;
- 原生索引在载入和全量构建期间使用较低 Windows 进程优先级,完成后恢复正常搜索优先级;
- 托盘退出会先关闭索引通信管道并等待原生进程退出,避免 `write EPIPE` 主进程异常。

## 安全边界

- 盘符根目录、Windows 目录、系统卷信息、回收站、默认/公共用户目录和关键 Microsoft 系统数据会被硬性拦截;
Expand Down
109 changes: 109 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# CDriveShiftAI 中文说明

CDriveShiftAI 是面向 Windows 10/11 x64 的全盘文件搜索、目录归属分析与跨盘安全迁移工具。名称索引、内容索引和迁移记录都由项目自身维护,不依赖 Everything。

当前版本:`0.0.3`

## 下载与运行

- 安装版:`CDriveShiftAI-x64.exe`,可以选择安装目录;检测到旧版本后沿用原目录覆盖更新。
- 便携版:`CDriveShiftAI-x64-portable.exe`,程序数据保存在便携程序所在目录或项目根目录的 `.cdriveshiftai-data`。
- 正式发布:[GitHub Releases](https://github.com/PuppetWen/CDriveShiftAI/releases)

安装包尚未使用商业 Authenticode 证书,Windows SmartScreen 可能显示“未知发布者”。请只从官方仓库 Release 下载,并对照 Release 中的 `update-manifest.json` 校验 SHA-512。

## 核心功能

### 极速名称搜索

- 搜索所有本地磁盘的文件名、目录名和完整路径。
- NTFS 优先读取 MFT;不可用时自动使用多线程目录扫描。
- 支持文件类型、盘符、目录范围、扩展名、大小、日期、大小写、完整单词和正则表达式组合筛选。
- 支持搜索书签、书签文件夹、拖放整理、结果状态恢复和独立极速搜索窗口。
- 结果支持双击打开、主题化右键菜单、属性、重命名、复制、删除、归属分析和安全迁移。

### 指定目录内容搜索

- 只索引用户明确选择的目录。
- 使用本地 SQLite FTS5 trigram 索引,支持中文、代码、配置和日志正文。
- 默认跳过依赖目录、构建产物、二进制文件、重解析点和超过限制的单文件。

### 应用归属分析

- 优先交叉匹配卸载注册表、AppX/MSIX、App Paths、安装目录和便携应用特征。
- 再根据目录结构、文件类型、包元数据和已知应用规则判断用途与来源。
- 本地证据不足时可以调用用户配置的 AI 服务;结果包含可信度、证据、迁移风险和风险说明。
- 支持 OpenAI 兼容接口、Anthropic、Gemini、Ollama、LM Studio 及常见国内外模型平台。

### 跨盘安全迁移

1. 检查保护路径、重解析点、目标空间和关联应用。
2. 复制到目标盘临时目录。
3. 校验文件数、目录数和总字节数。
4. 原子切换源目录。
5. 创建 Windows 符号链接;权限不足时使用目录联接。
6. 验证链接目标后才删除旧副本。
7. 持久化迁移记录,可恢复后再次迁移并累计次数。

目标目录直接使用源目录名称。例如:

```text
源目录:C:\Users\Example\AppData\Local\D3DSCache
目标基础目录:E:\MovedData
实际目标:E:\MovedData\D3DSCache
```

## 快捷键与托盘

- 主窗口和独立极速搜索均支持 Windows 全局快捷键。
- 保存快捷键前会实际尝试注册,检查应用内重复以及系统或其他软件占用。
- 独立极速搜索也可以配置鼠标后退键、前进键或中键及长按时间。
- 鼠标监听为被动 Raw Input,不拦截原有短按动作。
- 托盘菜单提供主界面、独立搜索、常用功能、AI 服务、主题、设置和退出入口。

## 自动更新

- 安装版静默更新到原安装路径,便携版在原文件位置自替换。
- 支持断点续传、最多三次自动重试和 SHA-512 双重校验。
- 校验失败拒绝安装;替换失败自动恢复旧版本。
- 新版本确认启动成功后才删除安装包、备份和更新临时目录。

## 性能策略

- 主界面先显示,索引加载稍后低优先级执行。
- 有效索引缓存不会在普通重启时全量重建。
- 全量更新只发生在首次运行、开机后首次运行、缓存损坏、用户手动刷新或超过 24 小时。
- 磁盘信息合并查询;非首页模块按需加载。
- 窗口闲置时降低视觉背景帧率,最小化后暂停动画和重型索引工作。
- 关闭到通知区域后释放 Chromium 渲染进程和 GPU 表面,只保留原生增量索引、文件变化监听、托盘菜单与全局快捷键。
- 托盘退出会等待索引器关闭,不留下 `cshift-indexer.exe`。

## 数据位置

应用不会把持久化索引和迁移记录放入 `%APPDATA%\CDriveShiftAI`。

```text
<项目、安装或便携目录>\.cdriveshiftai-data
```

其中包含设置、窗口状态、搜索书签、迁移记录、归属分析、名称索引、内容索引、日志和更新临时文件。

## 开发

环境要求:

- Node.js 20+
- Rust 1.75+
- Visual Studio 2022 C++ 工具
- Windows SDK

```powershell
npm install
npm run dev
npm run lint
npm test
npm run test:native
npm run dist:all
```

详细实现见 [架构说明](docs/ARCHITECTURE.md),版本变化见 [更新记录](CHANGELOG.md)。
100 changes: 84 additions & 16 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ let searchService: SearchService | undefined;
let updateService: UpdateService | undefined;
let tray: Tray | undefined;
let isQuitting = false;
let shutdownComplete = false;
let shutdownPromise: Promise<void> | undefined;
const store = new AppStore();
let migrationService: MigrationService;
const aiVerifications = new Map<string, { fingerprint: string; expiresAt: number }>();
Expand Down Expand Up @@ -283,6 +285,24 @@ function trackWindowBounds(
});
}

function showAsSoonAsRenderable(window: BrowserWindow, maximized = false): void {
let shown = false;
const show = () => {
if (shown || window.isDestroyed()) return;
shown = true;
if (maximized) window.maximize();
window.show();
};
// ready-to-show may be delayed by GPU initialization or a busy first paint.
// dom-ready is sufficient because the document has an effect-matched opaque
// background, so showing here improves perceived startup without a white flash.
window.webContents.once("dom-ready", show);
window.once("ready-to-show", show);
const fallback = setTimeout(show, 1_200);
fallback.unref();
window.once("closed", () => clearTimeout(fallback));
}

function createWindow(): BrowserWindow {
const effect = store.getSettings().effectMode;
const colors = effectColors[effect];
Expand All @@ -307,7 +327,6 @@ function createWindow(): BrowserWindow {
symbolColor: colors.symbols,
height: 48
},
...(process.platform === "win32" ? { backgroundMaterial: "mica" as const } : {}),
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
Expand All @@ -326,12 +345,24 @@ function createWindow(): BrowserWindow {
if (isQuitting || !store.getSettings().minimizeToTray) return;
event.preventDefault();
window.hide();
// Force a native background notification even if visibility events raced
// during startup. Besides pausing heavy work, this trims reclaimable pages
// across the resident process tree after the renderer is released.
searchService?.setBackgroundMode(true, true);
createTray();
// A hidden Chromium renderer and its GPU surfaces otherwise remain the
// largest part of the tray working set. Search, watchers, global
// shortcuts and the tray all live in the main/native processes, so the UI
// can be destroyed after its persisted layout/state has been saved and
// recreated on demand without reducing background accuracy.
setTimeout(() => {
if (!window.isDestroyed() && !window.isVisible()) window.destroy();
}, 150);
});
window.once("ready-to-show", () => {
if (restored?.maximized) window.maximize();
window.show();
window.on("closed", () => {
if (mainWindow === window) mainWindow = undefined;
});
showAsSoonAsRenderable(window, Boolean(restored?.maximized));
window.webContents.setWindowOpenHandler(({ url }) => {
if (/^https?:\/\//i.test(url)) void shell.openExternal(url);
return { action: "deny" };
Expand Down Expand Up @@ -653,10 +684,7 @@ function createTray(): void {
{
label: "退出",
icon: createTrayMenuIcon("exit", "#bd5d5d"),
click: () => {
isQuitting = true;
app.quit();
}
click: () => app.quit()
}
];
tray.setContextMenu(Menu.buildFromTemplate(template));
Expand Down Expand Up @@ -1625,10 +1653,10 @@ if (!singleInstance) {
});
} else {
app.on("second-instance", () => {
if (!mainWindow) return;
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
// The renderer is intentionally destroyed while resident in the tray.
// Launching the executable again must therefore recreate the main window,
// not silently return just because no BrowserWindow currently exists.
if (searchService) showMainView("overview");
});

app.whenReady().then(async () => {
Expand Down Expand Up @@ -1679,7 +1707,30 @@ if (!singleInstance) {
});
}
await migrationService.recoverIncomplete();
void searchService.start();
// Let Chromium finish the first interactive frame before parsing a
// multi-million-entry persistent index. Search remains fully accurate once
// the same cache is loaded; this only removes startup contention.
setTimeout(() => {
void searchService?.start().catch(() => {
// SearchService publishes its own unavailable/restart status.
});
}, 850);
const smokeQuitDelay = Number.parseInt(
process.env.CDRIVESHIFTAI_SMOKE_QUIT_AFTER_READY_MS ?? "",
10
);
if (app.isPackaged && Number.isFinite(smokeQuitDelay) && smokeQuitDelay >= 1_000) {
const smokeQuitTimer = setTimeout(() => app.quit(), smokeQuitDelay);
smokeQuitTimer.unref();
}
const smokeTrayDelay = Number.parseInt(
process.env.CDRIVESHIFTAI_SMOKE_CLOSE_TO_TRAY_AFTER_READY_MS ?? "",
10
);
if (app.isPackaged && Number.isFinite(smokeTrayDelay) && smokeTrayDelay >= 1_000) {
const smokeTrayTimer = setTimeout(() => mainWindow?.close(), smokeTrayDelay);
smokeTrayTimer.unref();
}

app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) mainWindow = createWindow();
Expand All @@ -1688,12 +1739,29 @@ if (!singleInstance) {
}

app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
const keepInTray =
!isQuitting &&
store.getSettings().minimizeToTray &&
tray != null &&
!tray.isDestroyed();
if (process.platform !== "darwin" && !keepInTray) app.quit();
});

app.on("before-quit", () => {
app.on("before-quit", (event) => {
isQuitting = true;
if (shutdownComplete) return;
event.preventDefault();
globalShortcut.unregisterAll();
tray?.destroy();
void searchService?.stop();
tray = undefined;
if (!shutdownPromise) {
shutdownPromise = (async () => {
try {
await searchService?.stop();
} finally {
shutdownComplete = true;
app.quit();
}
})();
}
});
Loading
Loading