Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
588e18a
fix(registry): 修复 Shell 扩展名称错误解析,新增 MUIVerb 支持
tzzs Mar 13, 2026
ba01c71
refactor(ps): 简化 Shell 扩展名称解析,移除 DLL VersionInfo 层
tzzs Mar 13, 2026
a30976c
fix(main-page): 修复场景切换竞态条件,新增 pendingScene 队列
tzzs Mar 13, 2026
ca96ecf
fix(registry): 修复 cleanDisplayName 正则顺序,修复括号加速键残留括号问题
tzzs Mar 13, 2026
4d80cf8
feat(shellex): 新增 MUIVerb 解析层与 InprocServer32 DLL 路径展示
tzzs Mar 13, 2026
6f6e71f
fix(shellex): 修复 CLSID 键名条目名称退化问题,移除硬编码映射表,支持 ms-resource:
tzzs Mar 13, 2026
cfe62f6
feat(shellex): 新增 Resolve-ExtName Level 1.7/2.5/3 名称解析层
tzzs Mar 13, 2026
60ef193
feat(PowerShellBridge): add generic name filtering for shell extensio…
tzzs Mar 14, 2026
a36c703
refactor(registry): improve menu item resolution and caching
tzzs Mar 18, 2026
fdfb4c6
refactor(renderer): 用 HTML5 <dialog> 替代原生 prompt/confirm/alert
tzzs May 6, 2026
b968acb
docs: 将 AGENTS.md 翻译为中文
tzzs Mar 13, 2026
a3ed5c6
refactor(scripts): update worktree creation and permissions settings
tzzs Mar 13, 2026
26bf7b8
refactor(shellex): 将 Shell 扩展名称解析从 PS/C# 迁移到 TypeScript/koffi
tzzs May 6, 2026
e137a71
fix(shellex): 修复 resolveIndirect FFI 类型错误与 DLL 语言优先排序
tzzs May 6, 2026
12c4e30
fix(shellex): resolveIndirect 改用 koffi.alloc + 新增标准谓词翻译表
tzzs May 6, 2026
5230782
fix(shellex): 回退 koffi.alloc → Buffer.alloc,修复 decode('str16') segfault
tzzs May 6, 2026
92b2707
fix(shellex): CommandStore Level 1.7 间接 MUIVerb 未解析导致英文名
tzzs May 6, 2026
ab2492e
fix(shellex): 调整解析优先级 — CommandStore 优先于 plain text
tzzs May 6, 2026
d3a6a61
feat(diagnose): 添加 koffi FFI 运行时诊断通道
tzzs May 6, 2026
c1ad61c
fix(diagnose): cmdStoreSize 改为实时查询 + 新增逐条解析追踪日志
tzzs May 6, 2026
0be1d72
fix(shellex): 修复 getFileVersionInfo koffi out 参数解构崩溃
tzzs May 6, 2026
9cdb078
fix(shellex): koffi out 参数 Number 强转 + "Expected N arguments" 修复
tzzs May 6, 2026
e1b3adc
fix(shellex): Level 2 CLSID Default 英文名查翻译表 + 扩充常见英文名
tzzs May 6, 2026
26ba049
refactor(shellex): DLL FileDescription 从 koffi FFI 改为 PS 内联采集
tzzs May 6, 2026
82e2416
fix(shellex): Level 2.5 增加 ProductName 回退 + 过滤等于 fallback 的描述
tzzs May 6, 2026
e2c10c6
feat(shellex): 新增 Level 1.6 ProgID 解析链
tzzs May 6, 2026
b8502df
docs: 全面更新 context-menu-parsing-logic.md 与当前实现对齐
tzzs May 6, 2026
24d207a
fix(ps): PS 输出强制 UTF-8 编码,修复中文乱码
tzzs May 7, 2026
c3d9ed9
fix(shellex): Level 1.3 反向扫描 CommandStateHandler/DelegateExecute
tzzs May 7, 2026
a04785a
fix(shellex): 新增 CLSID\Shell 子键 MUIVerb + 反向扫描增加 ExplorerCommandHandler
tzzs May 7, 2026
4d313cd
refactor(cache): merge redundant cache layers, consolidate batch inva…
tzzs May 10, 2026
6e6bce6
style(renderer): deduplicate MainWindow type assertion
tzzs May 10, 2026
3c46394
test(shellex): add edge case coverage for name resolution levels
tzzs May 10, 2026
402fb77
docs: update CmHelper references to koffi FFI scheme
tzzs May 10, 2026
3333c7f
merge: rebase onto master
tzzs May 10, 2026
7962124
feat: 性能优化 + 菜单切换 + 三段分组 + 高危隐藏 + 多语言识别
tzzs May 11, 2026
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
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
"Write(docs/**)"
]
}
}
}
261 changes: 164 additions & 97 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,164 @@
# ContextMaster — Windows 右键菜单管理工具

## 技术栈
- 框架:Electron 33.x + electron-forge + Vite
- 语言:TypeScript(严格模式)
- 数据库:better-sqlite3(同步 API,WAL 模式)
- 注册表访问:PowerShell 桥接(读写 HKCR)
- IPC:contextBridge + 类型化 IPC 频道

## 架构分层

```
src/
├── shared/ → 类型/枚举/IPC 频道常量(主+渲染进程共用)
├── main/
│ ├── services/ → PowerShellBridge → RegistryService → MenuManagerService
│ │ OperationHistoryService → BackupService
│ ├── data/ → Database.ts(better-sqlite3)+ Repository 层
│ └── ipc/ → 四个 handler 文件(registry/history/backup/system)
├── preload/ → contextBridge 暴露 window.api
└── renderer/ → index.html + main.ts + 四个 page 模块
```

## 关键约束
- 所有注册表写操作前必须创建回滚点
- 禁用条目通过写入 LegacyDisable 字符串值实现
- 启用条目通过删除 LegacyDisable 值实现
- 需要管理员权限(UAC manifest 已在 assets/app.manifest 配置)
- `wrapHandler` 使用 `any` 签名,IPC 边界类型由 preload 层保证

## 注册表路径
- 桌面右键:HKCR\DesktopBackground\Shell
- 文件右键:HKCR\*\shell
- 文件夹右键:HKCR\Directory\shell
- 驱动器右键:HKCR\Drive\shell
- 目录背景:HKCR\Directory\Background\shell
- 回收站:HKCR\CLSID\{645FF040-5081-101B-9F08-00AA002F954E}\shell

## 代码规范
- 使用 async/await 处理所有 IO 操作
- 错误处理使用 Result<T> 模式
- renderer page 模块通过 `(window as any)._mainPage` 等挂载供 HTML inline onclick 调用

## 运行方式
```bash
npm start # 开发模式(需管理员身份运行)
npm run make # 打包安装包
```

> 注意:better-sqlite3 为 native 模块,切换 Node/Electron 版本后需 `npx electron-rebuild`

## 设计 Token

### 颜色
```
--accent: #0067C0 /* 主按钮、选中态、激活指示器 */
--accent-hover: #005A9E
--accent-bg: #EFF6FC /* 选中行背景 */
--success: #0F7B0F /* 已启用标签 */
--success-bg: #DFF6DD
--danger: #C42B1C /* 已禁用标签、删除操作 */
--danger-bg: #FDE7E9
--warning: #9D5D00
--warning-bg: #FFF4CE
--bg: #F3F3F3 /* 应用主背景 */
--surface: #FFFFFF /* 卡片、面板 */
--surface2: #F9F9F9
--border: #E0E0E0
--text: #1A1A1A
--text2: #616161
--text3: #8A8A8A
--radius: 8px
--radius-sm: 4px
--nav-w: 220px
```

### 字体
- 主字体:Segoe UI Variable
- 代码/路径:Consolas
- 字号:正文 13px / 辅助 11px / 标题 18px / 大标题 24px

## 布局结构
- TitleBar:H=40px,含应用图标+标题+窗口控制按钮
- NavigationView(左侧):W=220px,分两组(场景 / 管理)
- Main Content:含 Toolbar(H≈52px) + ContentArea + DetailPanel(W=240px)
- StatusBar:H=26px,accent 色背景

## 页面清单
1. MainPage — 菜单管理(核心页,含场景切换)
2. HistoryPage — 操作记录
3. BackupPage — 备份管理
4. SettingsPage — 设置

## 参考文档
- UI 原型:docs/prototype/ContextMaster-UI原型.html
- 完整方案设计:docs/ContextMaster-方案设计文档.md
- 设计规范:docs/ContextMaster-Figma设计规范.md
# ContextMaster — Windows 右键菜单管理工具

## 技术栈
- 框架:Electron 33.x + electron-forge + Vite
- 语言:TypeScript(严格模式)
- 包管理器:pnpm
- 数据库:better-sqlite3(同步 API,WAL 模式)
- 注册表访问:PowerShell 桥接(读写 HKCR)
- Win32 FFI:koffi(调用 `shlwapi.dll` / `kernel32.dll`)
- IPC:contextBridge + 类型化 IPC 频道

## 常用命令

```bash
pnpm install # 安装依赖
pnpm start # 开发模式(需管理员身份运行)
pnpm run make # 打包 Squirrel 安装包 → out/
pnpm run lint # ESLint 检查
pnpm run test:unit # 运行所有单元测试
pnpm run test:unit -- -t "test name" # 运行单个测试
pnpm run test:unit:watch # watch 模式
pnpm run test:unit:ui # Vitest UI
pnpm run test:coverage # 单元测试 + 覆盖率
pnpm run test:e2e # Playwright E2E 测试
pnpm run test:e2e:ui # Playwright UI 模式
```

> better-sqlite3 为 native 模块,切换 Node/Electron 版本后需 `npx electron-rebuild`

## 架构分层

```
src/
├── shared/ → 类型/枚举/IPC 频道常量(主+渲染进程共用)
├── main/
│ ├── services/ → PowerShellBridge → RegistryService → MenuManagerService
│ │ ShellExtNameResolver(名称解析)
│ │ Win32Shell(koffi FFI:SHLoadIndirectString)
│ │ SystemInfoService(OS/菜单风格检测)
│ │ OperationHistoryService → BackupService
│ ├── data/ → Database.ts(better-sqlite3)+ Repository 层
│ └── ipc/ → 四个 handler 文件(registry/history/backup/system)
├── preload/ → contextBridge 暴露 window.api
└── renderer/ → index.html + main.ts + 四个 page 模块
```

## 服务组合根

`src/main/index.ts:initServices()` 中手动 DI 构建服务链:

```
Win32Shell(koffi)
└─→ ShellExtNameResolver(名称解析,四级级联策略)
Database
├── OperationRecordRepo ──→ OperationHistoryService ──┐
└── BackupSnapshotRepo ──→ BackupService │
↓ │
PowerShellBridge → SystemInfoService │
PowerShellBridge → RegistryService → MenuManagerService ←┘
```

启动顺序:`initLogger()` → `initServices()` → `createWindow()`

## 关键约束

- 所有注册表写操作前必须创建回滚点(`RegistryService.createRollbackPoint`)
- **Classic Shell** 条目:通过写入 `LegacyDisable` 字符串值禁用,删除该值启用
- **Shell 扩展(COM)** 条目:通过重命名键名(加/去 `-` 前缀)实现启用/禁用
- Shell 扩展条目类型通过 `registryKey` 是否包含 `shellex\ContextMenuHandlers` 来判定
- PowerShell 执行有两种模式:
- `execute()` — 直接执行(管理员身份下使用)
- `executeElevated()` — 通过临时 `.ps1` 文件 + `Start-Process -Verb RunAs` 提权,结果写临时 JSON 文件
- `wrapHandler` 统一捕获异常返回 `IpcResult<T>`,IPC 边界类型由 preload 层保证
- renderer page 模块通过 `(window as any)._mainPage` 等挂载函数供 HTML inline onclick 调用

## Shell 扩展名称解析(本分支重构)

名称解析已从 `PowerShellBridge` 拆出为独立服务:

- **`Win32Shell`**:koffi FFI 封装,调用 `shlwapi.dll!SHLoadIndirectString` 解析 `@DLL,-ID` 格式字符串,并通过 `kernel32.dll!GetUserDefaultUILanguage` 检测 UI 语言
- **`ShellExtNameResolver`**:四级级联策略解析 Shell 扩展显示名:
1. `directName` 间接格式 → `resolveIndirect`
2. `LocalizedString` / `FriendlyTypeName` → `resolveIndirect`(Phase A)
3. CLSID 默认值(Phase B)
4. 处理程序键名(兜底)
- **`SystemInfoService`**:检测 OS 版本(Win10/Win11,基于 build number ≥ 22000)及当前菜单风格(`classic` / `win11-new`)
- 标准谓词(open/edit/print/runas 等)通过内置翻译表按 UI 语言转换,无需注册表查询

## 注册表路径

| 场景 | Classic Shell | ShellEx ContextMenuHandlers |
|------|--------------|---------------------------|
| 桌面 | `HKCR\DesktopBackground\Shell` | `DesktopBackground\shellex\ContextMenuHandlers` |
| 文件 | `HKCR\*\shell` | `*\shellex\ContextMenuHandlers` |
| 文件夹 | `HKCR\Directory\shell` | `Directory\shellex\ContextMenuHandlers` |
| 驱动器 | `HKCR\Drive\shell` | `Drive\shellex\ContextMenuHandlers` |
| 目录背景 | `HKCR\Directory\Background\shell` | `Directory\Background\shellex\ContextMenuHandlers` |
| 回收站 | `HKCR\CLSID\{645FF040-5081-101B-9F08-00AA002F954E}\shell` | 同上 + `\shellex\ContextMenuHandlers` |

## IPC 模式

```
renderer → window.api.getMenuItems(scene) [preload/index.ts]
↓ ipcRenderer.invoke
main → ipcMain.handle('registry:getItems', ...) [main/ipc/registry.ts]
↓ wrapHandler
menuManager.getMenuItems(scene) [main/services/MenuManagerService.ts]
返回 IpcResult<MenuItemEntry[]>
```

## 单元测试

- 测试目录:`tests/unit/` — 镜像 `src/` 结构
- 运行器:Vitest(`globals: true`, `environment: 'node'`)
- 全局 mock:`tests/unit/setup.ts` 预 mock 了 `electron`, `electron-log`, `better-sqlite3`
- 路径别名:`@` → `src/`, `@main` → `src/main/`, `@shared` → `src/shared/` 等

## 设计 Token

```
--accent: #0067C0 /* 主按钮、选中态、激活指示器 */
--accent-hover: #005A9E
--accent-bg: #EFF6FC /* 选中行背景 */
--success: #0F7B0F /* 已启用标签 */
--success-bg: #DFF6DD
--danger: #C42B1C /* 已禁用标签、删除操作 */
--danger-bg: #FDE7E9
--warning: #9D5D00
--warning-bg: #FFF4CE
--bg: #F3F3F3 /* 应用主背景 */
--surface: #FFFFFF /* 卡片、面板 */
--surface2: #F9F9F9
--border: #E0E0E0
--text: #1A1A1A
--text2: #616161
--text3: #8A8A8A
--radius: 8px
--radius-sm: 4px
--nav-w: 220px
```

字体:Segoe UI Variable(正文 13px / 辅助 11px / 标题 18px / 大标题 24px),代码/路径:Consolas

## 布局结构

- TitleBar:H=40px,含应用图标+标题+窗口控制按钮
- NavigationView(左侧):W=220px,分两组(场景 / 管理)
- Main Content:含 Toolbar(H≈52px) + ContentArea + DetailPanel(W=240px)
- StatusBar:H=26px,accent 色背景

## 页面清单

1. MainPage — 菜单管理(核心页,含场景切换)
2. HistoryPage — 操作记录
3. BackupPage — 备份管理
4. SettingsPage — 设置

## 参考文档

- UI 原型:`docs/prototype/ContextMaster-UI原型.html`
- 完整方案设计:`docs/ContextMaster-方案设计文档.md`
- 设计规范:`docs/ContextMaster-Figma设计规范.md`
19 changes: 13 additions & 6 deletions docs/shell-extension-name-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,20 @@ Level 1.7: cmdStoreVerbs[CLSID] = "固定到任务栏" ✓

---

## CmHelper 编译与缓存
## koffi FFI 与 SHLoadIndirectString

CmHelper 是一个 C# 类,在脚本运行时动态编译(或从缓存加载)。缓存路径:`%LOCALAPPDATA%\ContextMaster\CmHelper.dll`。
`SHLoadIndirectString` 通过 TypeScript `koffi` 库直接调用 `shlwapi.dll`,
封装在 `src/main/services/Win32Shell.ts`。

**版本校验**:加载 DLL 后立即检查 `[CmHelper]::Ver == "2026.3"`,不匹配时重新编译。这确保代码变更后自动更新缓存。
DLL 版本资源通过 PowerShell `[System.Diagnostics.FileVersionInfo]::GetVersionInfo`
采集(天然支持当前 UI 语言),不依赖 C# 运行时编译。

**编译失败时的降级行为**:
- `ResolveIndirect` 不可用 → Level 0、Level 1(间接格式)、Level 1.3(间接格式)、Level 1.5(间接格式)失败
- `GetLocalizedVerStrings` 不可用 → Level 2.5 降级为 `FileVersionInfo`(仅当前 locale)
**优势**:
- 不依赖 .NET SDK 或运行时编译(C# `CmHelper` 已移除)
- 所有名称解析逻辑在 TypeScript 侧(`ShellExtNameResolver`),便于测试和调试
- PS 脚本只负责注册表数据采集 + DLL FileVersionInfo 读取

**koffi 不可用时的降级行为**:
- `resolveIndirect` 失败 → Level 0、Level 1/1.3/1.5(间接格式)、Level 1.7(间接格式)全部跳过
- DLL FileVersionInfo 仍通过 PS 脚本采集(Level 2.5),不受影响
- 其余 Level(plain 字符串路径)不受影响
24 changes: 12 additions & 12 deletions script/task.ps1
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
param(
[string]$Task
)

$path = ".ai/worktrees/$Task"
$branch = "task/$Task"

git worktree add $path -b $branch

Write-Host "Worktree created:"
Write-Host $path

param(
[string]$Task
)
$path = ".ai/worktrees/$Task"
$branch = "task/$Task"
git worktree add $path -b $branch
Write-Host "Worktree created:"
Write-Host $path
wt -w 0 new-tab -d $path
20 changes: 9 additions & 11 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { BackupSnapshotRepo } from './data/repositories/BackupSnapshotRepo';
import { OperationHistoryService } from './services/OperationHistoryService';
import { MenuManagerService } from './services/MenuManagerService';
import { BackupService } from './services/BackupService';
import { SystemInfoService } from './services/SystemInfoService';
import { registerRegistryHandlers } from './ipc/registry';
import { registerHistoryHandlers } from './ipc/history';
import { registerBackupHandlers } from './ipc/backup';
Expand Down Expand Up @@ -62,14 +63,15 @@ function initServices(): MenuManagerService {
const db = getDatabase();
const ps = new PowerShellBridge();
const win32Shell = new Win32Shell();
const resolver = new ShellExtNameResolver(win32Shell, win32Shell.uiLanguage);
const resolver = new ShellExtNameResolver(win32Shell, win32Shell.primaryLang);
const cmdStoreIndex = new CommandStoreIndex();
const registry = new RegistryService(ps, resolver, cmdStoreIndex);
const opRepo = new OperationRecordRepo(db);
const bkRepo = new BackupSnapshotRepo(db);
const history = new OperationHistoryService(opRepo);
const menuManager = new MenuManagerService(registry, history);
const backup = new BackupService(bkRepo, menuManager, history);
const systemInfo = new SystemInfoService(ps);

// 异步构建 CommandStore 索引(不阻塞启动)
ps.execute<Array<{ clsid: string; muiverb: string }>>(ps.buildCommandStoreScript())
Expand All @@ -82,7 +84,7 @@ function initServices(): MenuManagerService {
registerRegistryHandlers(menuManager);
registerHistoryHandlers(history, menuManager);
registerBackupHandlers(backup);
registerSystemHandlers(win32Shell, () => cmdStoreIndex.size);
registerSystemHandlers(win32Shell, () => cmdStoreIndex.size, systemInfo);
return menuManager;
}

Expand All @@ -91,15 +93,11 @@ app.whenReady().then(() => {
const menuManager = initServices();
createWindow();

// 串行预热:Desktop 优先,其余依次执行,避免饱和 PS 槽导致用户请求等待
void (async () => {
await menuManager.getMenuItems(MenuScene.Desktop).catch(e => log.warn('[Preload] Desktop failed:', e));
const rest = Object.values(MenuScene).filter(s => s !== MenuScene.Desktop) as MenuScene[];
for (const s of rest) {
await menuManager.getMenuItems(s).catch(() => null);
}
log.info('[Preload] All scenes preloaded');
})();
// 仅预热首屏 Desktop(high 优先级);其余场景按用户点击时延迟加载,
// 避免启动期饱和 PS 槽导致用户首次切换场景被阻塞排队
void menuManager.getMenuItems(MenuScene.Desktop, false, 'high')
.then(() => log.info('[Preload] Desktop preloaded'))
.catch(e => log.warn('[Preload] Desktop failed:', e));

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
Expand Down
Loading
Loading