Skip to content

Commit 0ac6368

Browse files
authored
Merge pull request #4 from PuppetWen/agent/live-search-proxy-update-v0.0.5
CDriveShiftAI 0.0.5: 实时搜索与代理更新
2 parents 3746813 + b8a5bfc commit 0ac6368

29 files changed

Lines changed: 498 additions & 45 deletions

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@
33
本项目采用 `0.0.x` 递增版本号。正式安装包、便携包和校验清单发布在
44
[GitHub Releases](https://github.com/PuppetWen/CDriveShiftAI/releases)
55

6+
## 0.0.5
7+
8+
### 实时搜索与后台资源
9+
10+
- 原生索引器在合并并落盘 Windows 文件系统增量后,向当前可见的主搜索页和独立极速搜索窗口发送轻量变更事件;新增、删除、改名和移动会自动重新执行当前查询。
11+
- 实时刷新继续使用当前组合筛选与排序条件,删除项会从结果中消失,新匹配项按所选顺序插入;变更事件在 180 ms 内合并,避免写入密集场景反复查询。
12+
- 托盘静默时不轮询、不向界面发送事件,也不执行动态搜索;只有可见且存在有效名称查询时才提高工作量。
13+
14+
### 更新与网络
15+
16+
- 应用启动以及从最小化或托盘重新打开主窗口时都会检查一次 GitHub Release;检查阶段显示动态状态,新版本可用时左上角状态点变红。
17+
- 更新检查、清单获取、断点下载统一改用 Electron 的 Windows 系统网络会话,自动遵循系统代理/PAC;FlClash 等工具启用系统代理时会自动使用,没有代理时直连。
18+
- 设置页显示本次更新链路是 Windows 系统代理还是直连,诊断日志同时记录脱敏后的网络路由。
19+
20+
### 启动与安装
21+
22+
- 新增“开机启动后最小化”设置。启用后只有 Windows 登录自动启动会直接进入托盘,手动打开仍显示主窗口。
23+
- 覆盖安装会检查旧安装的桌面快捷方式:如果用户此前没有选择或已经删除桌面快捷方式,重新安装不会擅自创建。
24+
- 版本更新为 `0.0.5`,继续提供可选安装目录的安装包、原路径运行的便携包和 SHA-512 更新清单。
25+
626
## 0.0.4
727

828
### 修复

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ CDriveShiftAI 是一个 Windows 桌面端磁盘整理工具,用于:
1212

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

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

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

120122
### 全局快捷唤起

README.zh-CN.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

5-
当前版本:`0.0.4`
5+
当前版本:`0.0.5`
66

77
## 下载与运行
88

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

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

6466
## 自动更新
6567

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

build/installer.nsh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
1+
!ifndef BUILD_UNINSTALLER
2+
Var /GLOBAL cdrivePreserveNoDesktopShortcut
3+
4+
!macro customInit
5+
StrCpy $cdrivePreserveNoDesktopShortcut "false"
6+
ReadRegStr $R7 HKEY_CURRENT_USER "${INSTALL_REGISTRY_KEY}" InstallLocation
7+
ReadRegStr $R6 HKEY_LOCAL_MACHINE "${INSTALL_REGISTRY_KEY}" InstallLocation
8+
${If} $R7 != ""
9+
${OrIf} $R6 != ""
10+
ReadRegStr $R8 HKEY_CURRENT_USER "${INSTALL_REGISTRY_KEY}" ShortcutName
11+
${If} $R8 == ""
12+
ReadRegStr $R8 HKEY_LOCAL_MACHINE "${INSTALL_REGISTRY_KEY}" ShortcutName
13+
${EndIf}
14+
${If} $R8 == ""
15+
StrCpy $R8 "${SHORTCUT_NAME}"
16+
${EndIf}
17+
StrCpy $R5 "false"
18+
SetShellVarContext current
19+
${If} ${FileExists} "$DESKTOP\$R8.lnk"
20+
StrCpy $R5 "true"
21+
${EndIf}
22+
SetShellVarContext all
23+
${If} ${FileExists} "$DESKTOP\$R8.lnk"
24+
StrCpy $R5 "true"
25+
${EndIf}
26+
SetShellVarContext current
27+
${If} $R5 == "false"
28+
StrCpy $cdrivePreserveNoDesktopShortcut "true"
29+
${EndIf}
30+
${EndIf}
31+
!macroend
32+
33+
!macro customInstall
34+
${If} $cdrivePreserveNoDesktopShortcut == "true"
35+
Delete "$newDesktopLink"
36+
${EndIf}
37+
!macroend
38+
!endif
39+
140
!macro customUnInstall
241
${IfNot} ${isUpdated}
342
${IfNot} ${Silent}

docs/RELEASE-0.0.5.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# CDriveShiftAI 0.0.5 发布说明
2+
3+
本版集中完善版本检测、系统代理、实时搜索结果和开机启动行为,同时保持托盘静默状态不执行轮询查询。
4+
5+
## 版本检测与自动更新
6+
7+
- 应用启动时检查一次正式 GitHub Release。
8+
- 主窗口从最小化、托盘隐藏或无渲染窗口状态恢复时再次检查;同一恢复动作产生的重复事件会自动合并。
9+
- 检查期间左上角版本点显示动态状态;当前版本为绿色,新版本可用时为红色,网络不可用时为琥珀色。
10+
- Release API、更新清单和更新包下载统一使用 Electron 默认会话,遵循 Windows 系统代理与 PAC。FlClash 等代理工具开启系统代理时会自动接管;未配置代理时直连。
11+
- 设置页显示当前检测到的网络路由;日志仅记录脱敏后的协议与端点,不记录代理凭据。
12+
13+
安装版仍执行静默原目录覆盖,便携版仍执行原文件替换;断点续传、三次重试、大小与 SHA-512 校验、失败保留旧版本、启动成功后清理更新包等事务保证保持不变。
14+
15+
## 极速搜索实时结果
16+
17+
- Windows 文件变更监听器完成一批增量更新后发送 `indexChanged` 事件。
18+
- 可见的主搜索页与独立极速搜索窗口对事件做 180 ms 合并,再使用当前关键词、正则规则、范围、类型、大小、日期和排序条件重新查询。
19+
- 文件或目录被删除后会从结果中消失;新增、改名和移动后的匹配项会按当前排序进入结果。
20+
- 托盘静默时不向渲染层发送事件,也不运行动态查询;没有有效名称搜索时界面不会订阅变更。
21+
22+
## 启动与安装行为
23+
24+
- 新增“开机启动后最小化”。该选项只随 Windows 登录项传入 `--startup-minimized`,手动启动仍正常显示主窗口。
25+
- 最小化自动启动不创建 Chromium 主窗口,只保留托盘、快捷键与原生索引服务,降低后台内存。
26+
- 覆盖安装前会检查旧安装与桌面快捷方式。如果旧安装存在但快捷方式不存在,安装完成后继续保持不创建。
27+
28+
## 验证
29+
30+
- TypeScript/Electron 类型检查与 33 项既有及新增单元测试通过。
31+
- 原生索引器与更新器共 7 项 Rust 测试通过。
32+
- 原生索引冒烟测试验证新增文件实时出现、删除文件实时消失,以及原有名称、组合筛选、正则、可执行文件目录和内容搜索。
33+
- NSIS 安装包与便携包已实际构建,安装脚本通过 `makensis` 编译。

electron/diagnostics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export async function createDiagnosticReport(input: DiagnosticInput): Promise<st
165165
settings: {
166166
effectMode: input.settings.effectMode,
167167
launchAtLogin: input.settings.launchAtLogin,
168+
launchMinimized: input.settings.launchMinimized,
168169
minimizeToTray: input.settings.minimizeToTray,
169170
globalShortcutConfigured: Boolean(input.settings.globalShortcut),
170171
quickSearchShortcutConfigured: Boolean(input.settings.quickSearchShortcut),

electron/main.ts

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ let tray: Tray | undefined;
9999
let isQuitting = false;
100100
let shutdownComplete = false;
101101
let shutdownPromise: Promise<void> | undefined;
102+
let visibleUpdateCheckTimer: NodeJS.Timeout | undefined;
103+
let lastVisibleUpdateCheckAt = 0;
102104
const store = new AppStore();
103105
let migrationService: MigrationService;
104106
const aiVerifications = new Map<string, { fingerprint: string; expiresAt: number }>();
@@ -380,6 +382,7 @@ function createWindow(): BrowserWindow {
380382

381383
window.setMenuBarVisibility(false);
382384
trackWindowActivity(window);
385+
window.on("restore", () => triggerVisibleUpdateCheck("main-window-restored"));
383386
trackWindowBounds(window, "mainWindowBounds");
384387
window.on("close", (event) => {
385388
if (isQuitting || !store.getSettings().minimizeToTray) return;
@@ -434,6 +437,36 @@ function emitUpdateState(state: AppUpdateInfo): void {
434437
}
435438
}
436439

440+
function triggerVisibleUpdateCheck(reason: string, delayMs = 0): void {
441+
if (!updateService || isQuitting) return;
442+
const busyPhases = new Set(["checking", "downloading", "verifying", "ready", "installing"]);
443+
if (busyPhases.has(updateService.getState().phase)) return;
444+
if (visibleUpdateCheckTimer) {
445+
clearTimeout(visibleUpdateCheckTimer);
446+
visibleUpdateCheckTimer = undefined;
447+
}
448+
const run = () => {
449+
visibleUpdateCheckTimer = undefined;
450+
if (!updateService || isQuitting) return;
451+
if (busyPhases.has(updateService.getState().phase)) return;
452+
if (Date.now() - lastVisibleUpdateCheckAt < 1_500) return;
453+
lastVisibleUpdateCheckAt = Date.now();
454+
logger.info("update.visible_check", { reason });
455+
void updateService.check(true).catch((error) => {
456+
logger.warn("update.visible_check_failed", {
457+
reason,
458+
error: serializeError(error)
459+
});
460+
});
461+
};
462+
if (delayMs > 0) {
463+
visibleUpdateCheckTimer = setTimeout(run, delayMs);
464+
visibleUpdateCheckTimer.unref();
465+
} else {
466+
run();
467+
}
468+
}
469+
437470
function assertUpdateCanInstall(): Promise<void> {
438471
const busyStages = new Set([
439472
"preflight",
@@ -466,11 +499,17 @@ function showMainView(
466499
view: "overview" | "search" | "ownership-map" | "analyze" | "migrate" | "history" | "settings",
467500
options: { path?: string; focus?: "ai-settings" | "search-input" } = {}
468501
): void {
502+
const shouldCheckForUpdates =
503+
!mainWindow ||
504+
mainWindow.isDestroyed() ||
505+
!mainWindow.isVisible() ||
506+
mainWindow.isMinimized();
469507
if (!mainWindow || mainWindow.isDestroyed()) mainWindow = createWindow();
470508
if (mainWindow.isMinimized()) mainWindow.restore();
471509
mainWindow.show();
472510
mainWindow.focus();
473511
sendNavigation(mainWindow, { view, ...options });
512+
if (shouldCheckForUpdates) triggerVisibleUpdateCheck("main-window-restored");
474513
}
475514

476515
function createQuickSearchWindow(): BrowserWindow {
@@ -1765,9 +1804,7 @@ if (!singleInstance) {
17651804
});
17661805
await store.init();
17671806
applyNativeEffect(store.getSettings().effectMode);
1768-
mainWindow = createWindow();
17691807
await completePendingUpdate();
1770-
createTray();
17711808
const currentSettings = store.getSettings();
17721809
searchService = new SearchService(
17731810
(status) => {
@@ -1784,22 +1821,41 @@ if (!singleInstance) {
17841821
}
17851822
}
17861823
},
1824+
(event) => {
1825+
for (const window of [mainWindow, quickSearchWindow]) {
1826+
if (
1827+
window &&
1828+
!window.isDestroyed() &&
1829+
window.isVisible() &&
1830+
!window.isMinimized()
1831+
) {
1832+
window.webContents.send("search:index-changed", event);
1833+
}
1834+
}
1835+
},
17871836
() => createQuickSearchWindow(),
17881837
{
17891838
button: currentSettings.mouseQuickSearchButton,
17901839
holdMs: currentSettings.mouseQuickSearchHoldMs
17911840
}
17921841
);
1793-
syncSearchBackgroundMode();
17941842
migrationService = new MigrationService(store, (record, message) => {
17951843
if (mainWindow && !mainWindow.isDestroyed()) {
17961844
mainWindow.webContents.send("migration:progress", { record, message });
17971845
}
17981846
});
17991847
updateService = new UpdateService(emitUpdateState, assertUpdateCanInstall);
18001848
registerIpc();
1849+
const startupMinimized =
1850+
process.argv.includes("--startup-minimized") &&
1851+
currentSettings.launchAtLogin &&
1852+
currentSettings.launchMinimized;
1853+
if (!startupMinimized) mainWindow = createWindow();
1854+
createTray();
1855+
syncSearchBackgroundMode();
1856+
triggerVisibleUpdateCheck("application-startup", 2_500);
18011857
const shortcutFailures = applyGlobalShortcuts(store.getSettings());
1802-
if (shortcutFailures.length > 0) {
1858+
if (shortcutFailures.length > 0 && mainWindow && !mainWindow.isDestroyed()) {
18031859
void dialog.showMessageBox(mainWindow, {
18041860
type: "warning",
18051861
title: "全局快捷键未完全启用",
@@ -1809,6 +1865,12 @@ if (!singleInstance) {
18091865
noLink: true
18101866
});
18111867
}
1868+
if (shortcutFailures.length > 0 && startupMinimized) {
1869+
logger.warn("shortcut.registration_incomplete", {
1870+
startupMinimized,
1871+
failures: shortcutFailures
1872+
});
1873+
}
18121874
await migrationService.recoverIncomplete();
18131875
// Let Chromium finish the first interactive frame before parsing a
18141876
// multi-million-entry persistent index. Search remains fully accurate once
@@ -1836,7 +1898,7 @@ if (!singleInstance) {
18361898
}
18371899

18381900
app.on("activate", () => {
1839-
if (BrowserWindow.getAllWindows().length === 0) mainWindow = createWindow();
1901+
if (BrowserWindow.getAllWindows().length === 0) showMainView("overview");
18401902
});
18411903
});
18421904
}
@@ -1852,6 +1914,10 @@ app.on("window-all-closed", () => {
18521914

18531915
app.on("before-quit", (event) => {
18541916
isQuitting = true;
1917+
if (visibleUpdateCheckTimer) {
1918+
clearTimeout(visibleUpdateCheckTimer);
1919+
visibleUpdateCheckTimer = undefined;
1920+
}
18551921
if (shutdownComplete) return;
18561922
event.preventDefault();
18571923
logger.info("application.shutdown_started", {

electron/preload.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { contextBridge, ipcRenderer } from "electron";
22
import type {
33
ContentIndexerStatus,
44
IndexerStatus,
5-
MigrationRecord
5+
MigrationRecord,
6+
SearchIndexChangedEvent
67
} from "./types";
78
import type { AppUpdateInfo } from "./update";
89

@@ -92,6 +93,14 @@ contextBridge.exposeInMainWorld("cDriveShiftAI", {
9293
ipcRenderer.on("indexer:status", handler);
9394
return () => ipcRenderer.removeListener("indexer:status", handler);
9495
},
96+
onSearchIndexChanged: (listener: (event: SearchIndexChangedEvent) => void) => {
97+
const handler = (
98+
_event: Electron.IpcRendererEvent,
99+
payload: SearchIndexChangedEvent
100+
) => listener(payload);
101+
ipcRenderer.on("search:index-changed", handler);
102+
return () => ipcRenderer.removeListener("search:index-changed", handler);
103+
},
95104
onContentIndexerStatus: (listener: (status: ContentIndexerStatus) => void) => {
96105
const handler = (_event: Electron.IpcRendererEvent, status: ContentIndexerStatus) =>
97106
listener(status);

0 commit comments

Comments
 (0)