本文档记录了在 Bitwarden For Command Palette 开发过程中遇到的问题及其解决方案,供开发者参考。
错误信息:
warning IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize<T>(T)' which has 'RequiresUnreferencedCodeAttribute'
warning IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize<T>(T)' which has 'RequiresDynamicCodeAttribute'
原因:
.NET AOT 编译要求在编译时知道所有类型信息。使用泛型 JsonSerializer.Serialize<T>() 或匿名类型会导致运行时反射,与 AOT 不兼容。
错误代码:
// ❌ 使用匿名类型
var data = new { title = "Hello", name = "World" };
var json = JsonSerializer.Serialize(data);
// ❌ 使用泛型方法
var json = JsonSerializer.Serialize<MyClass>(obj);解决方案:
// ✅ 使用 JsonObject 构建 JSON
var data = new JsonObject
{
["title"] = "Hello",
["name"] = "World"
};
var json = data.ToJsonString();
// ✅ 使用 Source Generator(需要预定义上下文)
[JsonSerializable(typeof(MyClass))]
internal partial class MyJsonContext : JsonSerializerContext { }
var json = JsonSerializer.Serialize(obj, MyJsonContext.Default.MyClass);相关文件:
Services/BitwardenCliService.cs- 定义了BitwardenJsonContextPages/CreateItemPage.cs- 使用JsonObject.ToJsonString()Pages/GeneratorPage.cs- 使用JsonObject.ToJsonString()
错误信息:
warning IL2026: Using member 'System.Text.Json.Nodes.JsonArray.Add<T>(T)' which has 'RequiresUnreferencedCodeAttribute'
原因:
JsonArray.Add<T>() 内部使用了泛型序列化,需要运行时类型信息。
错误代码:
// ❌ 直接添加 JsonObject
var choices = new JsonArray();
choices.Add(new JsonObject
{
["title"] = "Option 1",
["value"] = "1"
});解决方案:
// ✅ 使用 JsonNode.Parse() 添加
var choices = new JsonArray();
choices.Add(JsonNode.Parse("{\"title\":\"Option 1\",\"value\":\"1\"}"));
// ✅ 或者转义后构建 JSON 字符串
var title = "Option 1".Replace("\"", "\\\"");
choices.Add(JsonNode.Parse($"{{\"title\":\"{title}\",\"value\":\"1\"}}"));相关文件:
Pages/CreateItemPage.cs-GetFolderChoiceSetJson()方法
说明: 为了支持 AOT 编译,需要为所有需要 JSON 序列化/反序列化的类型定义 Source Generator 上下文。
实现:
[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSerializable(typeof(BitwardenStatus))]
[JsonSerializable(typeof(BitwardenItem[]))]
[JsonSerializable(typeof(BitwardenFolder[]))]
internal partial class BitwardenJsonContext : JsonSerializerContext
{
}使用:
// 反序列化
var status = JsonSerializer.Deserialize(json, BitwardenJsonContext.Default.BitwardenStatus);
// 序列化
var json = JsonSerializer.Serialize(item, BitwardenJsonContext.Default.BitwardenItem);问题描述:
Adaptive Cards 的 Input.ChoiceSet 组件的 choices 数组不支持通过 ${variable} 语法进行数据绑定,必须在模板中硬编码。
错误尝试:
{
"type": "Input.ChoiceSet",
"id": "folder",
"choices": "${folders}" // ❌ 不生效
}解决方案: 动态生成整个模板字符串,将 choices 直接嵌入到模板中。
private static string GetFolderChoiceSetJson(BitwardenFolder[]? folders)
{
var choices = new JsonArray();
// 动态添加选项
choices.Add(JsonNode.Parse("{\"title\":\"No Folder\",\"value\":\"\"}"));
if (folders != null)
{
foreach (var folder in folders)
{
var escapedName = folder.Name.Replace("\\", "\\\\").Replace("\"", "\\\"");
choices.Add(JsonNode.Parse($"{{\"title\":\"{escapedName}\",\"value\":\"{folder.Id}\"}}"));
}
}
var choiceSet = new JsonObject
{
["type"] = "Input.ChoiceSet",
["id"] = "folderId",
["label"] = "${folderLabel}", // 标签仍可数据绑定
["choices"] = choices
};
return choiceSet.ToJsonString();
}
// 在模板中使用字符串插值
private static string GetLoginTemplate(BitwardenFolder[]? folders)
{
var folderChoiceSet = GetFolderChoiceSetJson(folders);
return $$"""
{
"type": "AdaptiveCard",
"body": [
// ... 其他字段
{{folderChoiceSet}},
// ... 其他字段
]
}
""";
}相关文件:
Pages/CreateItemPage.csPages/EditItemPage.cs
问题描述:
Adaptive Cards 的 Input.Toggle 组件在表单提交时返回的是字符串 "true" 或 "false",而不是布尔值。
错误代码:
// ❌ 直接获取布尔值会失败
var includeUppercase = formData["uppercase"]?.GetValue<bool>() ?? true;解决方案:
// ✅ 先获取字符串,再解析为布尔值
var includeUppercase = formData["uppercase"]?.GetValue<string>() == "true";
// ✅ 或者使用更健壮的解析
var uppercaseStr = formData["uppercase"]?.GetValue<string>() ?? "true";
var includeUppercase = uppercaseStr.Equals("true", StringComparison.OrdinalIgnoreCase);相关文件:
Pages/GeneratorPage.cs-PasswordGeneratorForm.SubmitForm()
说明:
Command Palette 的 FormContent 使用 Adaptive Cards 模板系统,需要分离模板和数据。
模板(TemplateJson):
{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"text": "${title}"
},
{
"type": "Input.Text",
"id": "name",
"label": "${nameLabel}",
"value": "${name}"
}
]
}数据(DataJson):
{
"title": "Create Login",
"nameLabel": "Name",
"name": ""
}注意事项:
- 模板中使用
${variable}语法引用数据 - 表单字段的
id用于提交时获取值 - 数据绑定只适用于简单值,不适用于数组或复杂对象
问题描述:
当 ListItem 同时设置了 Command(默认命令)和 MoreCommands(更多命令)时,如果 MoreCommands 中的命令也设置了 InvokeCommand.RequestedShortcuts,可能会与默认命令冲突。
场景: 回收站中的项目,默认 Enter 键应该恢复项目,Ctrl+Enter 应该永久删除。
错误实现:
// ❌ 两个命令都响应 Enter 键
new ListItem(new RestoreCommand(item))
{
MoreCommands = [new PermanentDeleteCommand(item)]
}
// RestoreCommand 没有设置 RequestedShortcuts
// PermanentDeleteCommand 设置了 Ctrl+Enter,但也会响应 Enter正确实现:
// ✅ 明确设置默认命令的快捷键
public class RestoreCommand : InvokableCommand
{
public RestoreCommand()
{
// 只响应 Enter 键
RequestedShortcuts = [KeyboardShortcut.Enter];
}
}
public class PermanentDeleteCommand : InvokableCommand
{
public PermanentDeleteCommand()
{
// 只响应 Ctrl+Enter
RequestedShortcuts = [KeyboardShortcut.CtrlEnter];
}
}相关文件:
Pages/BitwardenForCommandPalettePage.cs- 回收站项目处理
问题描述:
DynamicListPage 的 IsLoading 属性需要手动管理,否则用户看不到加载指示器。
最佳实践:
public override IListItem[] GetItems()
{
IsLoading = true;
RaiseItemsChanged(); // 通知 UI 更新加载状态
try
{
// 执行异步操作
var items = LoadItemsAsync().Result;
return items;
}
finally
{
IsLoading = false;
// 不需要再次 RaiseItemsChanged,返回的数组会触发更新
}
}说明:
ContentPage 是一个页面容器,FormContent 是其中的内容。一个 ContentPage 可以包含多个 IContent。
结构:
internal sealed class MyPage : ContentPage
{
private readonly MyForm _form;
public MyPage()
{
_form = new MyForm();
Name = "Page Name";
Title = "Page Title";
}
public override IContent[] GetContent() => [_form];
}
internal sealed class MyForm : FormContent
{
public MyForm()
{
TemplateJson = "...";
DataJson = "...";
}
public override ICommandResult SubmitForm(string payload, string data)
{
// payload: 表单字段值(JSON)
// data: Action.Submit 的 data 属性(JSON)
return CommandResult.ShowToast("Success");
}
}问题描述: 某些情况下,Bitwarden CLI 的输出可能包含额外的文本(如警告信息),导致 JSON 解析失败。
解决方案:
private static async Task<T?> ParseCliOutputAsync<T>(string output)
{
// 尝试找到 JSON 的起始位置
var jsonStart = output.IndexOf('{');
if (jsonStart == -1)
{
jsonStart = output.IndexOf('[');
}
if (jsonStart == -1)
{
return default;
}
var jsonContent = output[jsonStart..];
return JsonSerializer.Deserialize<T>(jsonContent, options);
}问题描述:
bw create item 和 bw create folder 命令需要将 JSON 数据进行 Base64 编码后传入。
实现:
public static async Task<bool> CreateItemAsync(JsonObject newItem)
{
var itemJson = newItem.ToJsonString();
var encodedJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(itemJson));
var (output, error, exitCode) = await ExecuteCommandAsync(
$"create item \"{encodedJson}\" --session \"{SessionKey}\"");
return exitCode == 0;
}相关文件:
Services/BitwardenCliService.cs
问题描述: Bitwarden CLI 解锁后返回的 Session Key 需要在后续所有命令中使用。
管理策略:
public class BitwardenCliService
{
// 单例模式确保 Session Key 全局共享
private static BitwardenCliService? _instance;
public static BitwardenCliService Instance => _instance ??= new BitwardenCliService();
// Session Key 存储
public string? SessionKey { get; private set; }
public async Task<bool> UnlockAsync(string password)
{
var (output, _, exitCode) = await ExecuteCommandAsync($"unlock \"{password}\" --raw");
if (exitCode == 0 && !string.IsNullOrEmpty(output))
{
SessionKey = output.Trim();
return true;
}
return false;
}
// 所有后续命令都使用 SessionKey
private async Task<string> ExecuteWithSessionAsync(string command)
{
return await ExecuteCommandAsync($"{command} --session \"{SessionKey}\"");
}
}问题描述: 需要将删除等危险操作以红色显示,提醒用户注意。
解决方案:
public class DeleteCommand : InvokableCommand
{
public DeleteCommand()
{
Name = "Delete";
// 设置 IsCritical 使命令以红色显示
IsCritical = true;
}
}相关文件:
Commands/ItemCommands.cs-DeleteItemCommand,PermanentDeleteCommand
问题描述: 需要在列表项上显示状态标记(如 ✅ 表示选中,⭐ 表示收藏)。
实现:
var listItem = new ListItem(command)
{
Title = item.Name,
Tags = item.Favorite
? [new Tag { Text = "⭐" }]
: []
};
// 筛选页面中显示当前激活的筛选
var filterItem = new ListItem(command)
{
Title = "Favorites Only",
Tags = isActive
? [new Tag { Text = "✅" }]
: []
};问题描述:
Details 属性支持 Markdown 格式,可以创建富文本详情面板。
实现:
private string BuildDetailsMarkdown(BitwardenItem item)
{
var sb = new StringBuilder();
// 标题
sb.AppendLine($"## {item.Name}");
sb.AppendLine();
// 列表
sb.AppendLine("| Field | Value |");
sb.AppendLine("|-------|-------|");
sb.AppendLine($"| Username | {item.Login?.Username ?? "—"} |");
sb.AppendLine($"| Password | ••••••••• |");
// 代码块(用于显示笔记)
if (!string.IsNullOrEmpty(item.Notes))
{
sb.AppendLine();
sb.AppendLine("### Notes");
sb.AppendLine("```");
sb.AppendLine(item.Notes);
sb.AppendLine("```");
}
return sb.ToString();
}问题描述: 频繁请求网站图标会影响性能,需要缓存机制。
实现:
public static class IconService
{
private static readonly Dictionary<string, string> _iconUrlCache = new();
private const int MaxCacheSize = 200;
public static string GetIconUrl(string? url)
{
var domain = ExtractDomain(url);
if (_iconUrlCache.TryGetValue(domain, out var cachedUrl))
{
return cachedUrl;
}
// 缓存淘汰
if (_iconUrlCache.Count >= MaxCacheSize)
{
// 移除一半缓存
var keysToRemove = _iconUrlCache.Keys.Take(MaxCacheSize / 2).ToList();
foreach (var key in keysToRemove)
{
_iconUrlCache.Remove(key);
}
}
var iconUrl = $"https://icons.bitwarden.net/{domain}/icon.png";
_iconUrlCache[domain] = iconUrl;
return iconUrl;
}
}相关文件:
Services/IconService.cs
开发 Command Palette 扩展时,主要需要注意以下几点:
- AOT 兼容性:避免使用运行时反射,使用 Source Generator 和
JsonObject - Adaptive Cards 限制:了解数据绑定的边界,动态生成模板时注意字符转义
- SDK 特性:熟悉
ListItem、ContentPage、FormContent等组件的正确用法 - CLI 集成:处理 CLI 输出的边界情况,正确管理 Session Key
- 用户体验:使用
IsCritical、Tags、Details等属性增强界面
遇到问题时,建议:
- 查看编译警告,特别是 IL2026/IL3050 相关的 AOT 警告
- 使用 Debug.WriteLine 输出调试信息
- 参考官方 Adaptive Cards Designer 测试模板
- 查看 Bitwarden CLI 的帮助文档 (
bw --help)