Bitwarden For Command Palette 是一个 Windows PowerToys Command Palette 扩展,用于管理和访问 Bitwarden 密码管理器。该项目通过本地 Bitwarden CLI (bw) 与密码库进行交互,提供了一个快速、安全的密码管理界面。
BitwardenForCommandPalette/
├── BitwardenForCommandPalette/ # 主项目目录
│ ├── BitwardenForCommandPalette.cs # 扩展入口点 (IExtension 实现)
│ ├── BitwardenForCommandPaletteCommandsProvider.cs # 命令提供者
│ ├── Program.cs # 程序入口点
│ │
│ ├── Commands/ # 命令实现
│ │ └── ItemCommands.cs # 各种复制/操作命令
│ │
│ ├── Helpers/ # 辅助类
│ │ └── ResourceHelper.cs # 多语言资源管理
│ │
│ ├── Models/ # 数据模型
│ │ ├── BitwardenItem.cs # Bitwarden 项目模型
│ │ └── BitwardenStatus.cs # 状态模型
│ │
│ ├── Pages/ # UI 页面
│ │ ├── BitwardenForCommandPalettePage.cs # 主列表页面
│ │ ├── UnlockPage.cs # 解锁表单页面
│ │ └── FilterPage.cs # 筛选页面
│ │
│ ├── Services/ # 业务逻辑服务
│ │ ├── BitwardenCliService.cs # Bitwarden CLI 封装
│ │ ├── IconService.cs # 网站图标服务 (带缓存)
│ │ └── SettingsManager.cs # 设置管理
│ │
│ ├── Strings/ # 多语言资源
│ │ ├── en-US/
│ │ │ └── Resources.resw # 英文资源
│ │ └── zh-CN/
│ │ └── Resources.resw # 简体中文资源
│ │
│ └── Assets/ # 静态资源
│ └── Square44x44Logo.targetsize-24_altform-unplated.png
│
├── docs/ # 文档目录
│ └── ARCHITECTURE.md # 本文档
│
└── README.md # 项目说明
扩展遵循 PowerToys Command Palette 的标准生命周期:
// 1. 首次加载时,Command Palette 调用 Main() 并传递 -RegisterProcessAsComServer 参数
Program.Main(args)
↓
// 2. 创建 COM 服务器并注册扩展
ComServer server = new();
BitwardenForCommandPalette extensionInstance = new(extensionDisposedEvent);
server.RegisterClass<BitwardenForCommandPalette, IExtension>(() => extensionInstance);
server.Start();
↓
// 3. 扩展实例被缓存,每次请求时返回同一实例
extensionDisposedEvent.WaitOne(); // 等待扩展被释放
↓
// 4. 清理资源
server.Stop();public sealed partial class BitwardenForCommandPalette : IExtension, IDisposable
{
private readonly BitwardenForCommandPaletteCommandsProvider _provider = new();
public object? GetProvider(ProviderType providerType)
{
return providerType switch
{
ProviderType.Commands => _provider, // 仅提供命令提供者
_ => null,
};
}
}职责:
- 实现
IExtension接口 - 作为扩展的根对象
- 返回命令提供者给宿主
public partial class BitwardenForCommandPaletteCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly Settings _settings = new();
public BitwardenForCommandPaletteCommandsProvider()
{
DisplayName = ResourceHelper.AppDisplayName;
Icon = IconHelpers.FromRelativePath("Assets\\...");
InitializeSettings(); // 初始化设置页面
_commands = [
new CommandItem(new BitwardenForCommandPalettePage())
{
Title = DisplayName,
Icon = Icon
},
];
}
public override ICommandItem[] TopLevelCommands() => _commands;
}职责:
- 定义扩展的顶层命令(显示在 Command Palette 主界面)
- 管理设置页面的配置项
- 初始化设置管理器
设置项:
- BwPath: Bitwarden CLI 路径(默认 "bw")
- ClientId: Bitwarden API Client ID
- ClientSecret: Bitwarden API Client Secret
- CustomEnv: 自定义环境变量(格式:
KEY1=VALUE1;KEY2=VALUE2)
主页面继承自 DynamicListPage,实现动态列表:
internal sealed partial class BitwardenForCommandPalettePage : DynamicListPage
{
private BitwardenItem[]? _items; // 缓存的项目列表
private bool _isLoading; // 加载状态
private string? _errorMessage; // 错误信息
private BitwardenStatus? _lastStatus; // 最后一次状态
private VaultFilter _currentFilter; // 当前筛选器
public override IListItem[] GetItems()
{
// 1. 检查状态(如果未检查)
if (_lastStatus == null && !_isLoading)
return [CreateLoadingItem()];
// 2. 检查错误
if (!string.IsNullOrEmpty(_errorMessage))
return [CreateErrorItem(_errorMessage)];
// 3. 检查锁定/未登录状态
if (_lastStatus?.IsLoggedOut ?? false)
return [CreateNotLoggedInItem()];
if (_lastStatus?.IsLocked ?? true)
return [CreateUnlockItem()];
// 4. 显示项目列表
if (_items == null || _items.Length == 0)
return [CreateEmptyItem()];
var filteredItems = FilterItems(_items, SearchText, _currentFilter);
var listItems = filteredItems.Select(CreateListItem).ToList();
// 5. 添加实用命令
if (string.IsNullOrWhiteSpace(SearchText))
{
listItems.Insert(0, CreateFilterItem());
listItems.Add(CreateSyncItem());
listItems.Add(CreateLockItem());
}
return listItems.ToArray();
}
}页面状态流:
首次加载
↓
检查状态 → 未登录 → 显示"未登录"提示
↓
已锁定 → 显示"解锁"按钮 → 导航到 UnlockPage
↓
已解锁 → 加载项目列表 → 显示项目
↓
用户搜索/筛选 → 过滤显示
使用 Adaptive Cards 实现表单界面:
internal sealed partial class UnlockPage : ContentPage
{
private readonly UnlockForm _unlockForm;
public override IContent[] GetContent() => [_unlockForm];
}
internal sealed partial class UnlockForm : FormContent
{
// Adaptive Cards JSON 模板
TemplateJson = """{
"type": "AdaptiveCard",
"body": [
{ "type": "Input.Text", "id": "masterPassword", "style": "password" }
],
"actions": [{ "type": "Action.Submit", "title": "解锁" }]
}""";
public override CommandResult SubmitForm(string payload)
{
var masterPassword = ParseFormInput(payload);
var (success, message) = service.UnlockAsync(masterPassword).Result;
if (success)
{
_onUnlocked?.Invoke(); // 通知主页面刷新
return CommandResult.GoBack();
}
return CommandResult.ShowToast(message);
}
}动态列表页面,提供多种筛选选项:
internal sealed partial class FilterPage : DynamicListPage
{
public override IListItem[] GetItems()
{
return [
// 基础筛选
new ListItem(new ApplyFilterCommand(new VaultFilter(), ...))
{ Title = "所有项目" },
new ListItem(new ApplyFilterCommand(new VaultFilter { FavoritesOnly = true }, ...))
{ Title = "仅收藏" },
// 类型筛选
new ListItem(new ApplyFilterCommand(new VaultFilter { ItemType = BitwardenItemType.Login }, ...))
{ Title = "登录项目" },
...
// 文件夹筛选(动态加载)
new SectionHeaderItem("文件夹"),
..._folders.Select(f => new ListItem(new ApplyFilterCommand(...)))
];
}
}封装所有与 Bitwarden CLI 的交互,使用单例模式:
public partial class BitwardenCliService
{
private static BitwardenCliService? _instance;
public static BitwardenCliService Instance { get; }
public string? SessionKey { get; private set; } // 会话密钥,解锁后缓存
// 核心命令执行
private async Task<(string output, string error, int exitCode)> ExecuteCommandAsync(string arguments)
{
var processInfo = new ProcessStartInfo
{
FileName = SettingsManager.Instance.BwPath,
Arguments = arguments,
Environment = SettingsManager.Instance.GetEnvironmentVariables(), // 支持自定义环境变量
...
};
// 异步执行并捕获输出
var output = await process.Start();
return (output, error, exitCode);
}
// 公共 API
public static async Task<BitwardenStatus?> GetStatusAsync();
public async Task<(bool success, string message)> UnlockAsync(string masterPassword);
public async Task<bool> LockAsync();
public async Task<bool> SyncAsync();
public async Task<BitwardenItem[]?> GetItemsAsync();
public async Task<BitwardenItem[]?> SearchItemsAsync(string query);
public async Task<string?> GetTotpAsync(string itemId);
public async Task<BitwardenItem[]?> GetItemsByFolderAsync(string? folderId);
public async Task<BitwardenFolder[]?> GetFoldersAsync();
}认证流程:
1. 检查设置是否有 API Key
↓ 是
设置环境变量 BW_CLIENTID, BW_CLIENTSECRET
自动执行 bw login --apikey
2. 检查已有会话密钥
↓ 有
使用会话密钥执行命令
3. 需要解锁
↓
用户输入主密码 → bw unlock --raw → 获取会话密钥
↓
缓存会话密钥,后续命令使用 --session
使用记忆化缓存优化图标加载性能:
public static class IconService
{
// 内存缓存(最多 200 个域名)
private static readonly Dictionary<string, IconInfo> _iconCache = new();
// 已知不可用的域名(避免无效请求)
private static readonly HashSet<string> _unavailableIconDomains = new()
{
"adobe.com", "account.adobe.com"
};
public static IconInfo GetItemIcon(BitwardenItem item)
{
if (item.ItemType != BitwardenItemType.Login)
return GetDefaultIcon(item.ItemType);
var domain = ExtractDomainFromItem(item);
if (string.IsNullOrEmpty(domain))
return DefaultWebIcon;
// 检查缓存
if (_iconCache.TryGetValue(domain, out var cachedIcon))
return cachedIcon;
// 检查不可用列表
if (_unavailableIconDomains.Contains(domain))
return DefaultWebIcon;
// 生成图标 URL
var iconUrl = $"https://icons.bitwarden.net/{domain}/icon.png";
var iconInfo = new IconInfo(iconUrl);
// 管理缓存大小
if (_iconCache.Count >= MaxCacheSize)
RemoveOldCacheEntries();
_iconCache[domain] = iconInfo;
return iconInfo;
}
// 域名提取逻辑(处理各种 URL 格式)
private static string? ExtractDomainFromHostname(string hostname)
{
// 提取注册域名(accounts.google.com → google.com)
// 支持两字 TLD(co.uk, com.au 等)
}
}图标获取流程:
登录项目 → 提取域名 → 检查缓存
↓
已缓存 → 返回图标 URL
↓
未缓存 → 检查不可用列表
↓
不可用 → 返回默认图标
↓
可用 → 生成 URL 并缓存
单例模式,管理所有配置:
public sealed class SettingsManager
{
public static SettingsManager Instance { get; } = new();
public string BwPath { get; set; } = "bw";
public string CustomEnvironment { get; set; } = string.Empty;
public string BwClientId { get; set; } = string.Empty;
public string BwClientSecret { get; set; } = string.Empty;
// 解析环境变量(支持自定义格式)
public Dictionary<string, string> GetEnvironmentVariables()
{
var result = new Dictionary<string, string>();
// 添加 API Key
if (!string.IsNullOrWhiteSpace(BwClientId))
result["BW_CLIENTID"] = BwClientId;
if (!string.IsNullOrWhiteSpace(BwClientSecret))
result["BW_CLIENTSECRET"] = BwClientSecret;
// 解析自定义环境变量(KEY1=VALUE1;KEY2=VALUE2)
var pairs = CustomEnvironment.Split(';', StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs)
{
var parts = pair.Split('=', 2);
if (parts.Length == 2)
result[parts[0].Trim()] = parts[1].Trim();
}
return result;
}
}public class BitwardenItem
{
public string? Id { get; set; }
public int Type { get; set; } // 1=Login, 2=SecureNote, 3=Card, 4=Identity
public string? Name { get; set; }
public string? Notes { get; set; }
public bool Favorite { get; set; }
public string? FolderId { get; set; }
public BitwardenLogin? Login { get; set; }
public BitwardenCard? Card { get; set; }
public BitwardenIdentity? Identity { get; set; }
public BitwardenField[]? Fields { get; set; }
[JsonIgnore]
public BitwardenItemType ItemType => (BitwardenItemType)Type;
}
// Login, Card, Identity, Field 等子模型...public class BitwardenStatus
{
public string? Status { get; set; } // "unlocked", "locked", "unauthenticated"
public string? UserEmail { get; set; }
public DateTime? LastSync { get; set; }
[JsonIgnore]
public bool IsUnlocked => Status?.ToLower() == "unlocked";
[JsonIgnore]
public bool IsLocked => Status?.ToLower() == "locked";
[JsonIgnore]
public bool IsLoggedOut => Status?.ToLower() == "unauthenticated";
}所有操作都继承自 InvokableCommand:
// 基础命令模式
internal sealed partial class CopyPasswordCommand : InvokableCommand
{
private readonly BitwardenItem _item;
public CopyPasswordCommand(BitwardenItem item)
{
_item = item;
Name = ResourceHelper.CommandCopyPassword;
Icon = new IconInfo("\uE8C8"); // Fluent UI 图标
}
public override CommandResult Invoke()
{
ClipboardHelper.SetText(_item.Login.Password);
return CommandResult.Dismiss(); // 关闭并返回
}
}
// TOTP 命令(需要调用 CLI)
internal sealed partial class CopyTotpCommand : InvokableCommand
{
public override CommandResult Invoke()
{
var totp = BitwardenCliService.Instance.GetTotpAsync(_item.Id).Result;
if (!string.IsNullOrEmpty(totp))
{
ClipboardHelper.SetText(totp);
return CommandResult.ShowToast(ResourceHelper.ToastTotpCopied(totp));
}
return CommandResult.ShowToast(ResourceHelper.ToastTotpFailed);
}
}
// OFF 命令(直接在页面内执行)
internal sealed partial class InlineCommand : InvokableCommand
{
private readonly Func<CommandResult> _action;
public override CommandResult Invoke() => _action();
}命令分类:
-
登录项目命令:
- 复制密码、用户名、URL
- 打开 URL
- 复制 TOTP
-
银行卡命令:
- 复制卡号、CVV、有效期、持卡人姓名
-
身份信息命令:
- 复制姓名、邮箱、电话、地址、公司
- 复制 SSN、护照号、驾照号
-
安全笔记命令:
- 复制笔记内容
-
自定义字段命令:
- 动态生成,复制字段值
-
操作命令:
- 同步、锁定、刷新、应用筛选
用户打开 Command Palette
↓
找到 Bitwarden For Command Palette
↓
首次加载扩展
├─ 检查设置(BwPath, API Keys)
├─ 执行 bw status
├─ 状态判断:
│ ├─ 未登录 → 显示"未登录"
│ ├─ 已锁定 → 显示"解锁"按钮
│ └─ 已解锁 → 加载并显示项目
└─ 渲染 UI
点击"解锁" → 导航到 UnlockPage
↓
输入主密码 → 点击"解锁"按钮
↓
UnlockPage.SubmitForm()
↓
BitwardenCliService.UnlockAsync(masterPassword)
├─ 执行: bw unlock "password" --raw
├─ 成功 → 获取会话密钥并缓存
└─ 失败 → 显示错误 Toast
↓
成功 → Callback 通知主页面
↓
主页面调用 LoadItemsAsync()
↓
BitwardenCliService.GetItemsAsync()
├─ 执行: bw list items --session "key"
└─ 返回项目列表
↓
显示项目列表
用户在搜索框输入文字
↓
主页面 UpdateSearchText() 被调用
↓
GetItems() 触发重新渲染
↓
FilterItems() 过滤项目
├─ 名称匹配(忽略大小写)
├─ 用户名匹配
└─ URL 匹配
↓
显示过滤后的列表
点击"筛选"按钮 → 导航到 FilterPage
↓
FilterPage 加载文件夹列表
├─ 使用当前会话
└─ 执行: bw list folders --session
↓
显示筛选选项列表
├─ 所有项目
├─ 仅收藏
├─ 项目类型(登录、银行卡等)
└─ 文件夹(动态)
↓
用户点击筛选项 → ApplyFilterCommand
↓
回调主页面 → 更新 _currentFilter
↓
主页面重新渲染(GetItems())
↓
FilterItems() 应用所有筛选条件
↓
显示筛选结果
用户右键项目或选择操作
↓
执行对应 CopyXXXCommand.Invoke()
↓
从模型获取值 → ClipboardHelper.SetText()
↓
返回 CommandResult.Dismiss() 或 ShowToast()
↓
显示成功提示并关闭
点击"同步"按钮 → SyncVaultCommand
↓
BitwardenCliService.SyncAsync()
├─ 执行: bw sync --session "key"
↓
成功 → 显示 Toast
↓
刷新项目列表(LoadItemsAsync())
点击"锁定"按钮 → LockVaultCommand
↓
BitwardenCliService.LockAsync()
├─ 执行: bw lock
├─ 清除本地 SessionKey
↓
显示 Toast
↓
主页面刷新 → 显示"解锁"按钮
- 内存缓存:最多缓存 200 个域名的图标 URL
- 不可用列表:已知无图标的域名直接返回默认图标
- 域名提取:智能提取注册域名,避免重复请求
- 使用
async/await避免 UI 阻塞 - 异步加载项目和文件夹
- 流式处理 CLI 输出
- 缓存
SessionKey避免重复解锁 - 自动附加会话参数
使用 Windows ResourceLoader 实现本地化:
public static class ResourceHelper
{
private static readonly ResourceLoader _loader = ResourceLoader.GetForViewIndependentUse();
public static string AppDisplayName => GetString("AppDisplayName");
public static string ActionUnlock => GetString("ActionUnlock");
public static string ToastTotpCopied(string code) => string.Format(GetString("ToastTotpCopied"), code);
public static string GetString(string key, params object[] args)
{
var value = _loader.GetString(key);
return args.Length > 0 ? string.Format(value, args) : value;
}
}资源文件结构(.resw):
<data name="AppDisplayName" xml:space="preserve">
<value>Bitwarden For Command Palette</value>
</data>
<data name="CommandCopyPassword" xml:space="preserve">
<value>复制密码</value>
</data>- API Key:通过环境变量传递,不存储在文件中
- 主密码:仅在解锁时短暂使用,不持久化
- 会话密钥:存储在内存中,程序退出即清除
- 密码参数使用引号转义
- 不记录敏感信息到调试输出
- 创建无窗口进程 (CreateNoWindow)
- 所有操作基于本地 CLI
- 无需网络连接查看/复制已有数据
- 同步操作需要网络
var (output, error, exitCode) = await ExecuteCommandAsync("...");
if (exitCode != 0)
{
Debug.WriteLine($"bw failed: {error}");
return null;
}try
{
return JsonSerializer.Deserialize(output, ...);
}
catch (JsonException ex)
{
Debug.WriteLine($"Failed to parse: {ex.Message}");
return null;
}- 显示友好的错误消息
- 提供刷新操作
- 保持界面可交互
// 1. 在 Commands/ 创建新命令类
internal sealed partial class MyNewCommand : InvokableCommand
{
public override CommandResult Invoke() { ... }
}
// 2. 在主页面的 GetContextCommands() 中添加
private static ICommandContextItem[] GetContextCommands(BitwardenItem item)
{
var commands = new List<ICommandContextItem>();
if (/* 某些条件 */)
{
commands.Add(new CommandContextItem(new MyNewCommand(item)));
}
return commands.ToArray();
}// 在 FilterPage.GetItems() 添加选项
items.Add(new ListItem(new ApplyFilterCommand(
new VaultFilter { MyFilterProperty = value },
_onFilterApplied))
{
Title = "我的筛选",
...
});
// 在主页面 FilterItems() 应用筛选
if (filter.MyFilterProperty != null)
result = result.Where(item => /* 应用筛选逻辑 */);- 在
Strings/创建新文件夹(如ja-JP) - 复制
en-US/Resources.resw - 翻译所有
<value>内容 - 重新构建
# x64
dotnet build -p:Platform=x64
# ARM64
dotnet build -p:Platform=ARM64# Visual Studio: 右键项目 → 部署
# 或使用命令
dotnet deploy- 部署扩展
- 在 Command Palette 中执行
Reload - 设置断点
- 附加到
BitwardenForCommandPalette.exe进程 - 触发扩展操作
使用 Debug.WriteLine() 输出到调试窗口:
Debug.WriteLine($"状态: {status}");
Debug.WriteLine($"错误: {error}");| 组件 | 技术 | 说明 |
|---|---|---|
| 语言 | C# 12 | .NET 10.0 |
| 框架 | .NET 10.0 | Windows 10.0.26100.0 |
| UI | Microsoft.CommandPalette.Extensions.Toolkit | PowerToys 扩展 SDK |
| 认证 | Bitwarden CLI Session | 会话密钥机制 |
| JSON | System.Text.Json | AOT 兼容 |
| 本地化 | WinRT ResourceLoader | .resw 文件 |
| 图标 | Fluent UI 图标 | Unicode 码点 |
| 容器 | WinRT COM Server | 进程外服务 |
- 首次启动:~1-2 秒(加载 CLI + 状态检查)
- 解锁时间:~500ms(取决于密码强度)
- 项目加载:~200-500ms(取决于项目数量)
- 搜索响应:即时(内存操作)
- 图标加载:缓存后即时,首次 ~100ms
MIT License - 允许自由使用、修改和分发。
- Fork 项目
- 创建特性分支
- 提交更改
- 创建 Pull Request
文档维护者:基于项目代码分析生成 最后更新:2025-12-17