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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
本项目采用 `0.0.x` 递增版本号。正式安装包、便携包和校验清单发布在
[GitHub Releases](https://github.com/PuppetWen/CDriveShiftAI/releases)。

## 0.0.5

### 实时搜索与后台资源

- 原生索引器在合并并落盘 Windows 文件系统增量后,向当前可见的主搜索页和独立极速搜索窗口发送轻量变更事件;新增、删除、改名和移动会自动重新执行当前查询。
- 实时刷新继续使用当前组合筛选与排序条件,删除项会从结果中消失,新匹配项按所选顺序插入;变更事件在 180 ms 内合并,避免写入密集场景反复查询。
- 托盘静默时不轮询、不向界面发送事件,也不执行动态搜索;只有可见且存在有效名称查询时才提高工作量。

### 更新与网络

- 应用启动以及从最小化或托盘重新打开主窗口时都会检查一次 GitHub Release;检查阶段显示动态状态,新版本可用时左上角状态点变红。
- 更新检查、清单获取、断点下载统一改用 Electron 的 Windows 系统网络会话,自动遵循系统代理/PAC;FlClash 等工具启用系统代理时会自动使用,没有代理时直连。
- 设置页显示本次更新链路是 Windows 系统代理还是直连,诊断日志同时记录脱敏后的网络路由。

### 启动与安装

- 新增“开机启动后最小化”设置。启用后只有 Windows 登录自动启动会直接进入托盘,手动打开仍显示主窗口。
- 覆盖安装会检查旧安装的桌面快捷方式:如果用户此前没有选择或已经删除桌面快捷方式,重新安装不会擅自创建。
- 版本更新为 `0.0.5`,继续提供可选安装目录的安装包、原路径运行的便携包和 SHA-512 更新清单。

## 0.0.4

### 修复
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ CDriveShiftAI 是一个 Windows 桌面端磁盘整理工具,用于:

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

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

## 已实现能力
Expand Down Expand Up @@ -110,11 +110,13 @@ AI 是可选的二次判断层,支持三类真实协议:
- 首次运行安装包时可自行选择安装路径;
- 使用相同 `appId` 的后续安装包会读取已安装项的 `InstallLocation`,沿用原路径覆盖升级;
- 每次打开主界面时只检查一次官方 GitHub Release,不在托盘后台循环联网;
- 更新检查、清单与下载使用 Windows 系统网络会话,自动遵循系统代理/PAC;没有代理时使用直连;
- 设置入口右侧绿点表示当前没有发现更新,红点表示存在更高版本;主题化悬浮提示会显示当前版与最新版;
- 安装版支持静默原路径升级,便携版支持原文件自替换;下载中断后按已有字节断点续传并最多自动重试三次;
- 更新包由主程序和独立更新助手分别执行 SHA-512 校验,文件大小或摘要不符时拒绝安装;
- 替换前在同一磁盘的安装目录外备份旧程序;只有新版本成功启动并回报正确版本后才删除更新包与备份,失败时自动恢复旧版本;
- 设置页使用与方块、科技、晶境主题一致的分段进度界面,显示下载量、速度、重试次数以及下载、校验、备份、替换和清理阶段;
- 覆盖安装会保留用户此前“不创建桌面快捷方式”的选择;登录时启动可单独配置为直接最小化到托盘;
- 当前安装包未使用商业 Authenticode 证书,Windows 可能显示 SmartScreen“未知发布者”提示。

### 全局快捷唤起
Expand Down
6 changes: 5 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

当前版本:`0.0.4`
当前版本:`0.0.5`

## 下载与运行

Expand All @@ -20,6 +20,7 @@ CDriveShiftAI 是面向 Windows 10/11 x64 的全盘文件搜索、目录归属
- NTFS 优先读取 MFT;不可用时自动使用多线程目录扫描。
- 支持文件类型、盘符、目录范围、扩展名、大小、日期、大小写、完整单词和正则表达式组合筛选。
- 支持搜索书签、书签文件夹、拖放整理、结果状态恢复和独立极速搜索窗口。
- 主搜索页与独立极速搜索窗口会接收原生文件系统增量事件;当前查询结果在新增、删除、改名或移动后自动按原筛选和排序刷新。
- 结果支持双击打开、主题化右键菜单、属性、重命名、复制、删除、归属分析和安全迁移。

### 指定目录内容搜索
Expand Down Expand Up @@ -60,13 +61,16 @@ CDriveShiftAI 是面向 Windows 10/11 x64 的全盘文件搜索、目录归属
- 独立极速搜索也可以配置鼠标后退键、前进键或中键及长按时间。
- 鼠标监听为被动 Raw Input,不拦截原有短按动作。
- 托盘菜单提供主界面、独立搜索、常用功能、AI 服务、主题、设置和退出入口。
- 可分别控制“登录时启动”和“开机启动后最小化”;最小化启动只作用于 Windows 登录自动启动。

## 自动更新

- 安装版静默更新到原安装路径,便携版在原文件位置自替换。
- 支持断点续传、最多三次自动重试和 SHA-512 双重校验。
- 校验失败拒绝安装;替换失败自动恢复旧版本。
- 新版本确认启动成功后才删除安装包、备份和更新临时目录。
- 启动应用、从最小化或托盘重新打开主窗口时都会检查一次版本;发现新版本后左上角状态点变红。
- 更新检查和下载遵循 Windows 系统代理/PAC,FlClash 等工具开启系统代理后无需重复填写;没有代理时使用直连。
- 更新助手会在隐藏暂存目录和可见独立运行目录间自动回退,并在 Windows 短暂拒绝执行时重试;所有启动策略都失败时保留旧版本和下载包。
- `0.0.2` 的更新助手启动失败发生在旧版程序自身,无法通过失败中的更新链路修复;此类用户需手动安装一次 `0.0.4`,之后可继续使用完整自动更新。

Expand Down
39 changes: 39 additions & 0 deletions build/installer.nsh
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
!ifndef BUILD_UNINSTALLER
Var /GLOBAL cdrivePreserveNoDesktopShortcut

!macro customInit
StrCpy $cdrivePreserveNoDesktopShortcut "false"
ReadRegStr $R7 HKEY_CURRENT_USER "${INSTALL_REGISTRY_KEY}" InstallLocation
ReadRegStr $R6 HKEY_LOCAL_MACHINE "${INSTALL_REGISTRY_KEY}" InstallLocation
${If} $R7 != ""
${OrIf} $R6 != ""
ReadRegStr $R8 HKEY_CURRENT_USER "${INSTALL_REGISTRY_KEY}" ShortcutName
${If} $R8 == ""
ReadRegStr $R8 HKEY_LOCAL_MACHINE "${INSTALL_REGISTRY_KEY}" ShortcutName
${EndIf}
${If} $R8 == ""
StrCpy $R8 "${SHORTCUT_NAME}"
${EndIf}
StrCpy $R5 "false"
SetShellVarContext current
${If} ${FileExists} "$DESKTOP\$R8.lnk"
StrCpy $R5 "true"
${EndIf}
SetShellVarContext all
${If} ${FileExists} "$DESKTOP\$R8.lnk"
StrCpy $R5 "true"
${EndIf}
SetShellVarContext current
${If} $R5 == "false"
StrCpy $cdrivePreserveNoDesktopShortcut "true"
${EndIf}
${EndIf}
!macroend

!macro customInstall
${If} $cdrivePreserveNoDesktopShortcut == "true"
Delete "$newDesktopLink"
${EndIf}
!macroend
!endif

!macro customUnInstall
${IfNot} ${isUpdated}
${IfNot} ${Silent}
Expand Down
33 changes: 33 additions & 0 deletions docs/RELEASE-0.0.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# CDriveShiftAI 0.0.5 发布说明

本版集中完善版本检测、系统代理、实时搜索结果和开机启动行为,同时保持托盘静默状态不执行轮询查询。

## 版本检测与自动更新

- 应用启动时检查一次正式 GitHub Release。
- 主窗口从最小化、托盘隐藏或无渲染窗口状态恢复时再次检查;同一恢复动作产生的重复事件会自动合并。
- 检查期间左上角版本点显示动态状态;当前版本为绿色,新版本可用时为红色,网络不可用时为琥珀色。
- Release API、更新清单和更新包下载统一使用 Electron 默认会话,遵循 Windows 系统代理与 PAC。FlClash 等代理工具开启系统代理时会自动接管;未配置代理时直连。
- 设置页显示当前检测到的网络路由;日志仅记录脱敏后的协议与端点,不记录代理凭据。

安装版仍执行静默原目录覆盖,便携版仍执行原文件替换;断点续传、三次重试、大小与 SHA-512 校验、失败保留旧版本、启动成功后清理更新包等事务保证保持不变。

## 极速搜索实时结果

- Windows 文件变更监听器完成一批增量更新后发送 `indexChanged` 事件。
- 可见的主搜索页与独立极速搜索窗口对事件做 180 ms 合并,再使用当前关键词、正则规则、范围、类型、大小、日期和排序条件重新查询。
- 文件或目录被删除后会从结果中消失;新增、改名和移动后的匹配项会按当前排序进入结果。
- 托盘静默时不向渲染层发送事件,也不运行动态查询;没有有效名称搜索时界面不会订阅变更。

## 启动与安装行为

- 新增“开机启动后最小化”。该选项只随 Windows 登录项传入 `--startup-minimized`,手动启动仍正常显示主窗口。
- 最小化自动启动不创建 Chromium 主窗口,只保留托盘、快捷键与原生索引服务,降低后台内存。
- 覆盖安装前会检查旧安装与桌面快捷方式。如果旧安装存在但快捷方式不存在,安装完成后继续保持不创建。

## 验证

- TypeScript/Electron 类型检查与 33 项既有及新增单元测试通过。
- 原生索引器与更新器共 7 项 Rust 测试通过。
- 原生索引冒烟测试验证新增文件实时出现、删除文件实时消失,以及原有名称、组合筛选、正则、可执行文件目录和内容搜索。
- NSIS 安装包与便携包已实际构建,安装脚本通过 `makensis` 编译。
1 change: 1 addition & 0 deletions electron/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export async function createDiagnosticReport(input: DiagnosticInput): Promise<st
settings: {
effectMode: input.settings.effectMode,
launchAtLogin: input.settings.launchAtLogin,
launchMinimized: input.settings.launchMinimized,
minimizeToTray: input.settings.minimizeToTray,
globalShortcutConfigured: Boolean(input.settings.globalShortcut),
quickSearchShortcutConfigured: Boolean(input.settings.quickSearchShortcut),
Expand Down
76 changes: 71 additions & 5 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ let tray: Tray | undefined;
let isQuitting = false;
let shutdownComplete = false;
let shutdownPromise: Promise<void> | undefined;
let visibleUpdateCheckTimer: NodeJS.Timeout | undefined;
let lastVisibleUpdateCheckAt = 0;
const store = new AppStore();
let migrationService: MigrationService;
const aiVerifications = new Map<string, { fingerprint: string; expiresAt: number }>();
Expand Down Expand Up @@ -380,6 +382,7 @@ function createWindow(): BrowserWindow {

window.setMenuBarVisibility(false);
trackWindowActivity(window);
window.on("restore", () => triggerVisibleUpdateCheck("main-window-restored"));
trackWindowBounds(window, "mainWindowBounds");
window.on("close", (event) => {
if (isQuitting || !store.getSettings().minimizeToTray) return;
Expand Down Expand Up @@ -434,6 +437,36 @@ function emitUpdateState(state: AppUpdateInfo): void {
}
}

function triggerVisibleUpdateCheck(reason: string, delayMs = 0): void {
if (!updateService || isQuitting) return;
const busyPhases = new Set(["checking", "downloading", "verifying", "ready", "installing"]);
if (busyPhases.has(updateService.getState().phase)) return;
if (visibleUpdateCheckTimer) {
clearTimeout(visibleUpdateCheckTimer);
visibleUpdateCheckTimer = undefined;
}
const run = () => {
visibleUpdateCheckTimer = undefined;
if (!updateService || isQuitting) return;
if (busyPhases.has(updateService.getState().phase)) return;
if (Date.now() - lastVisibleUpdateCheckAt < 1_500) return;
lastVisibleUpdateCheckAt = Date.now();
logger.info("update.visible_check", { reason });
void updateService.check(true).catch((error) => {
logger.warn("update.visible_check_failed", {
reason,
error: serializeError(error)
});
});
};
if (delayMs > 0) {
visibleUpdateCheckTimer = setTimeout(run, delayMs);
visibleUpdateCheckTimer.unref();
} else {
run();
}
}

function assertUpdateCanInstall(): Promise<void> {
const busyStages = new Set([
"preflight",
Expand Down Expand Up @@ -466,11 +499,17 @@ function showMainView(
view: "overview" | "search" | "ownership-map" | "analyze" | "migrate" | "history" | "settings",
options: { path?: string; focus?: "ai-settings" | "search-input" } = {}
): void {
const shouldCheckForUpdates =
!mainWindow ||
mainWindow.isDestroyed() ||
!mainWindow.isVisible() ||
mainWindow.isMinimized();
if (!mainWindow || mainWindow.isDestroyed()) mainWindow = createWindow();
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
sendNavigation(mainWindow, { view, ...options });
if (shouldCheckForUpdates) triggerVisibleUpdateCheck("main-window-restored");
}

function createQuickSearchWindow(): BrowserWindow {
Expand Down Expand Up @@ -1765,9 +1804,7 @@ if (!singleInstance) {
});
await store.init();
applyNativeEffect(store.getSettings().effectMode);
mainWindow = createWindow();
await completePendingUpdate();
createTray();
const currentSettings = store.getSettings();
searchService = new SearchService(
(status) => {
Expand All @@ -1784,22 +1821,41 @@ if (!singleInstance) {
}
}
},
(event) => {
for (const window of [mainWindow, quickSearchWindow]) {
if (
window &&
!window.isDestroyed() &&
window.isVisible() &&
!window.isMinimized()
) {
window.webContents.send("search:index-changed", event);
}
}
},
() => createQuickSearchWindow(),
{
button: currentSettings.mouseQuickSearchButton,
holdMs: currentSettings.mouseQuickSearchHoldMs
}
);
syncSearchBackgroundMode();
migrationService = new MigrationService(store, (record, message) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("migration:progress", { record, message });
}
});
updateService = new UpdateService(emitUpdateState, assertUpdateCanInstall);
registerIpc();
const startupMinimized =
process.argv.includes("--startup-minimized") &&
currentSettings.launchAtLogin &&
currentSettings.launchMinimized;
if (!startupMinimized) mainWindow = createWindow();
createTray();
syncSearchBackgroundMode();
triggerVisibleUpdateCheck("application-startup", 2_500);
const shortcutFailures = applyGlobalShortcuts(store.getSettings());
if (shortcutFailures.length > 0) {
if (shortcutFailures.length > 0 && mainWindow && !mainWindow.isDestroyed()) {
void dialog.showMessageBox(mainWindow, {
type: "warning",
title: "全局快捷键未完全启用",
Expand All @@ -1809,6 +1865,12 @@ if (!singleInstance) {
noLink: true
});
}
if (shortcutFailures.length > 0 && startupMinimized) {
logger.warn("shortcut.registration_incomplete", {
startupMinimized,
failures: shortcutFailures
});
}
await migrationService.recoverIncomplete();
// Let Chromium finish the first interactive frame before parsing a
// multi-million-entry persistent index. Search remains fully accurate once
Expand Down Expand Up @@ -1836,7 +1898,7 @@ if (!singleInstance) {
}

app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) mainWindow = createWindow();
if (BrowserWindow.getAllWindows().length === 0) showMainView("overview");
});
});
}
Expand All @@ -1852,6 +1914,10 @@ app.on("window-all-closed", () => {

app.on("before-quit", (event) => {
isQuitting = true;
if (visibleUpdateCheckTimer) {
clearTimeout(visibleUpdateCheckTimer);
visibleUpdateCheckTimer = undefined;
}
if (shutdownComplete) return;
event.preventDefault();
logger.info("application.shutdown_started", {
Expand Down
11 changes: 10 additions & 1 deletion electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { contextBridge, ipcRenderer } from "electron";
import type {
ContentIndexerStatus,
IndexerStatus,
MigrationRecord
MigrationRecord,
SearchIndexChangedEvent
} from "./types";
import type { AppUpdateInfo } from "./update";

Expand Down Expand Up @@ -92,6 +93,14 @@ contextBridge.exposeInMainWorld("cDriveShiftAI", {
ipcRenderer.on("indexer:status", handler);
return () => ipcRenderer.removeListener("indexer:status", handler);
},
onSearchIndexChanged: (listener: (event: SearchIndexChangedEvent) => void) => {
const handler = (
_event: Electron.IpcRendererEvent,
payload: SearchIndexChangedEvent
) => listener(payload);
ipcRenderer.on("search:index-changed", handler);
return () => ipcRenderer.removeListener("search:index-changed", handler);
},
onContentIndexerStatus: (listener: (status: ContentIndexerStatus) => void) => {
const handler = (_event: Electron.IpcRendererEvent, status: ContentIndexerStatus) =>
listener(status);
Expand Down
Loading
Loading