更新日志
v2.2.0 (2026-11-11)
新功能
键盘
- WebView 内的按键执行 foobar2000 的快捷键:页面有焦点时,用户在「首选项 > 键盘快捷键」里绑定的命令照常执行;没有绑定快捷键的按键仍交给页面。焦点在输入框里时,打字与文本编辑键留给页面;焦点在输入框之外时,空格、字母、方向键这类单键也能执行快捷键。参与的按键由组件首选项「窗口 > 快捷键」选定,默认为含 Ctrl、Alt 或 Win 的组合(包括 Ctrl+方向键)与 F1 到 F24;单键需要开启「其他按键」。按住按键不放时,快捷键只执行一次。详见 WebView 内的快捷键。
keyboard.setReservedKeys把组合留给页面:列出的组合即使在 foobar2000 里绑定了快捷键,也交给页面处理,主题自己的快捷键因此优先于用户的绑定。列表归属于页面,页面导航、重载或所在窗口关闭时清空。keyboard.setReservedKeys的all把全部按键留给页面,适用于页面在自绘的快捷键编辑器里录制按键等场景。系统级热键与开发服务器模式下的 F12 仍然生效。
右键菜单
- 右键菜单命令可以指定作用对象:
discovery.executeContextMenuCommand与discovery.executeContextMenuByPath新增target:selection:foobar2000 自己的上下文快捷键所用的选区。nowPlaying:只作用于正在播放的曲目。playlistSelection:只作用于活动播放列表的选中项。auto(缺省):作用于正在播放的曲目,未播放时作用于活动播放列表的选中项,与原来的行为相同。
面板
- Default UI 编辑模式的占位符标明面板身份:占位符显示面板名、页面来源与已开启的选项,同一布局里有多个 WebView 面板时可以据此区分。它使用 Default UI 的配色与字体,尺寸随 DPI 缩放,空间不足时省略次要的行。双击占位符打开面板配置。
- 面板配置对话框可以新建模板、打开模板文件夹:模板下拉框旁新增「新建...」与「打开文件夹」。新模板的名称沿用偏好设置页的规则:只能使用字母、数字、连字符和下划线,且不能与现有模板重名(不区分大小写)。新建的模板自动选中,点「确定」后才写入面板配置。
- 面板配置对话框重新排版:设置项分为「页面」「行为」两组,边框样式移入「行为」组。「允许此网站访问……」只在填写外部网址后显示;此前它始终显示,未启用时为灰色。
播放列表
playlist.getAutoplaylistQuery能读出内置自动播放列表的查询与排序:列表由 foobar2000 自带的 query/sort 客户端驱动时(例如在 Default UI 里创建或经playlist.createAutoplaylist创建的列表),query为列表当前的查询语句,新增的sort为排序表达式,未设置排序时为空串。两者在每次调用时重新读取。对于其他提供者的列表与本组件托管的列表,query与sort均为null,原因见note。早期版本对所有列表都返回query: null,应答里也没有sort字段。
变更
keyboard.registerHotkey拒绝会在所有程序里占用常用按键的组合:组合必须包含 Ctrl、Alt 或 Win,媒体键可以单独注册。Ctrl+A、C、V、X、Y、Z,Ctrl+Shift+Z,Ctrl+Esc,Alt+F4、Alt+Tab 与 Alt+Space 同样被拒绝。被拒绝的组合以INVALID_PARAMS失败。这些组合此前能注册成功,但热键从未触发(见「修复」)。- 组合已被占用时,错误信息指明持有者:组合已由本组件注册时,
keyboard.registerHotkey以OPERATION_FAILED失败,details给出该注册的id与windowId。组合被其他程序或 foobar2000 的全局快捷键占用时,同样以OPERATION_FAILED失败,details.win32Error为 Windows 错误码。 - 注册热键的窗口关闭时,热键随之注销:此前窗口关闭后热键仍保持注册,按键事件改发给主窗口的页面。Default UI 或 Columns UI 布局里没有主窗口页面,关闭的面板会继续占用该组合,重建的面板也无法再次注册。
keyboard.getRegisteredHotkeys的热键条目新增windowId:注册该热键、接收其按键事件的窗口。- Ctrl+P、Ctrl+O、Ctrl+N 按用户在 foobar2000 里的绑定执行:此前页面有焦点时,这三个组合固定执行打开首选项、打开文件与新建播放列表,不随用户的绑定改变。现在它们与其他按键一样,执行「首选项 > 键盘快捷键」里设置的快捷键。没有绑定时,按键交给页面。
- F5 重载有焦点的页面,页面自行处理时除外:此前焦点在页面里时,F5 是一次普通按键,只有焦点在页面之外才重载页面;开启开发者工具时由浏览器重载。现在页面先收到这次按键。页面的
keydown监听都没有调用preventDefault()时,组件在各类窗口里都会重载页面。页面正在加载或加载失败时,F5 直接重载。详见 WebView 内的快捷键。 keyboard.registerShortcut已废弃,将在 3.0 移除:它只登记组合,不会按它匹配任何按键。页面内的快捷键请监听keydown。- SMP 兼容层的
fb.GetLibraryItems更快:它改为经library.search分页读取,并且只读取句柄用到的字段。在约 10 万首曲目的媒体库上实测,耗时从约 4.2 秒降到约 0.8 秒。返回的曲目与顺序不变。
修复
键盘
keyboard.registerHotkey注册的热键能够触发:热键此前注册在 foobar2000 的主窗口上,组件不会收到该窗口的WM_HOTKEY消息,因此注册成功,keyboard:hotkey却从未派发。现在热键注册在组件自己的消息窗口上,按下时事件发给注册它的窗口。页面导航或重载时,它注册的热键随之注销,重载后的页面可以再次注册同一组合。退出 foobar2000 时,全部热键注销。
标签写入
- WAV 标签写入不再毁坏非 ASCII 标签:
metadata.write、metadata.writeBatch、metadata.removeTag、metadata.removeField、rating.set的标签回退、lyrics.save的内嵌目标与replaygain.clear遇到写入后标签含非 ASCII 文本的 WAV 文件时拒绝写入,以NOT_SUPPORTED失败,details.reason为wavInfoNonAscii,文件保持不变。foobar2000 把 WAV 标签以 ASCII 存入 RIFF INFO 块,其他字符一律写成?;它还会把 GBK 编码的 INFO 块按 Latin-1 读入,因此即使只修改其他字段,重写这类文件也会把原有标签永久替换为?。标签全为 ASCII 的 WAV 文件照常写入,其他格式不受影响。
媒体读取与封面
- 经目录映射加载的页面可以使用媒体流与封面地址:页面经
foo-ui-webview2.local这类目录映射加载时(打包的主题即是如此),宿主签发的媒体流地址与封面地址此前一律以ERR_FILE_NOT_FOUND失败。这两类地址现在使用主机fb2k-routes.invalid,该主机不经过任何目录映射。这些地址对任何页面都是跨源地址,页面要从 Canvas 读取封面像素时,须给<img>设置crossorigin。 - 卡住的云端或网络读取不再阻塞本地读取:封面与歌词的读取此前共用一条队列,读取云端占位文件或不可达的网络共享时一旦卡住,之后的本地读取都会超时,直到重启 foobar2000。网络共享与映射网络盘上的文件,以及仅在云端的文件,现在在单独的队列里读取。
- 取封面不再下载仅在云端的文件:对于本地磁盘上仅在云端的文件,
artwork.getCurrent、getByPath、getForTrack、getByPlaylistItem、getBatch、getAvailableTypes、getAvailableArtwork与getMetadata不打开文件,直接按无封面返回。此前这些方法会下载整个文件。感谢 @Sijie20 报告。
播放列表
playlist.replaceAllAndPlay播放playIndex指定的曲目:此前写入的行会去重,并按用户设置的新增项顺序重排,而playIndex按重排后的行计数,重排结果与paths的顺序不同时就会播放另一首。现在各行保持paths的顺序;每条路径只对应一首曲目时,playIndex即要播放的路径的下标。重复的路径保留,文件夹内的曲目按 foobar2000 展开文件夹的顺序加入。感谢 @LYY-LZH 报告。
面板
- Default UI 编辑模式下右键单击面板,弹出 Default UI 的标准编辑菜单:菜单提供替换、剪切、复制与粘贴,「配置...」也在其中。此前只弹出「配置...」一项。
- 编辑模式开启时新建的面板立即显示占位符:此前新面板显示满尺寸的页面,右键单击由页面接收,无法打开面板配置。
- 面板的透明区域跟随 foobar2000 的配色:页面没有绘制背景的区域显示宿主当前的背景色:Default UI 面板为 Default UI 的背景色,Columns UI 面板为 Columns UI 的全局背景色。在首选项里修改配色后,这些区域立即随之改变。此前 Default UI 面板保持面板创建时的颜色,Columns UI 面板始终为白色。
- 面板设置「透明背景」在面板创建时即生效:保存为不透明的面板,在重启 foobar2000 或重建面板后仍为不透明;此前该设置只在切换时生效。不透明背景改为宿主的背景色,不再固定为深灰色(
#1e1e1e)。 - 新建、重命名模板的起名对话框跟随 foobar2000 的深浅色模式:此前它始终为浅色。
- 新建模板的起始页文字改为中性灰:此前文字为白色,面板透明区域显示浅色背景时无法看清。已有模板不受影响。
v2.1.0 (2026-10-27)
新功能
JIT 会话与播放控制 实验性
- 文档绑定会话 (
jitSession):在页面重载后保留播放尝试与操作结果,将控制权绑定到调用文档,并支持窗口间显式交接。撤销下一条不停止当前曲目;临近曲尾、foobar2000 可能已选定下一条时,撤销操作以nextPreselected拒绝;下一条未就绪时,skip拒绝请求,不影响原生播放队列。jitSession.play的新资源仍在解析时,即使旧曲目已结束播放,新尝试仍保持等待;原生播放推进到已缓冲的下一条时,上一条未完成的尝试与操作标记为superseded,不推断为播放结束(EOF)。jitSession.release在缓冲清空后才返回应答;清空失败时以相同requestId重试会返回相同的错误。现有jitQueue方法保持原有行为。 - 客户端状态同步 (
JitSessionClient):负责管理凭据、串行发送命令、补偿遗漏的状态并等待操作结果。等待期间若遗漏终态事件会自动补查;补查期间若再次出现事件缺口,也会继续查询直到状态对齐。 - 原生播放接纳 (
prepareAdoption):支持快照记录正在播放的曲目、原生播放队列中的各队列项及其来源播放列表,且不修改它们;通过jitSession.getNativeContext分页读取该快照,jitSession.adoptNative可无缝接管正在播放的曲目;停止或释放会话时,剩余的队列项交还原生播放。jitSession.playNative与jitSession.setNativeNext可播放或预排已接管上下文中的来源行或队列项,jitSession.setNativeQueue可删除或调整待交还的队列项。来源已无可接续的内容时,最后一个队列项通过宿主保留的单曲列表__webview_restore__交还,播完即止。接管期间调用jitQueue方法照常执行,随后会话保持stopped状态,仅支持释放已接管的上下文。getCapabilities()报告adoption: true。 - 失败重测机制 (
attemptRechecked):播放尝试以unknown结束且未播满 1 秒时,宿主会重新打开该资源,报告是否再次失败及具体原因(accessDenied、notFound、unsupportedContent、network),以及 foobar2000 是否记录过其播放失败。getCapabilities().failureRecheck为true;preciseFailure保持false。 - 后台持续运行:
jitSession处于活跃状态时,最小化窗口、隐藏到托盘或锁屏不再冻结控制该会话的页面,页面持续响应jitSession:needNext,播放不会在已缓冲条目播完后中断。页面照常隐藏并降低内存配额;会话停止或释放后恢复深度挂起。getCapabilities().backgroundExecution为true。
媒体库与增量同步
- 本地副本增量同步 (
library.getChanges/library.getTracks):支持页面维护本地媒体库副本并按需拉取增量变更。library.getChanges返回跨进程重启持久有效的修订号,以及指定修订号后新增、修改或移出的曲目;进程关闭期间产生的离线变更将在启动载入后同步返回。library.getTracks支持单次按最多 5000 个句柄批量拉取行数据。使用说明见本地副本的增量同步。 - 细粒度变更通知与句柄批处理:
library:itemsAdded、library:itemsRemoved、library:itemsModified与metadb:changed新增handles与truncated字段。handles包含每首发生变更曲目的handle;单次变更超过 5000 首时truncated为true,handles为空。将handles传给titleformat.evalFieldsBatch即可只读取变更的行。编辑标签不再重建library.getRoots与library.browseTree所用的目录索引。fb-playlist-view会立即重读有变更的曲目行,无需等待页面滚动;SMP 兼容层的on_metadb_changed单次最多可接收 5000 首变更曲目(此前为 50 首)。 - 第三方插件刷新免全量重建 (
library:itemsModified):新增fromHook字段。当 foo_playcount 等组件请求 foobar2000 刷新其提供的字段(而非曲目标签实际被编辑)时,该字段为true。此类变化下,宿主仅作废已缓存的library.getAll列表,保留其余媒体库缓存;播放时写入播放次数和评分不再触发library.getRoots与library.getAlbums重建。SDK 媒体库树组件收到该事件时不再重载。
播放列表与并发控制
- 数据版本与并发冲突防护 (
contentVersion):playlist.getTracks、getTracksAt、getSelectedTracks、getSelection、getMatchingRows、getGroupRuns与queue.get返回contentVersion。无论是插入、删除、重排还是替换曲目行,即使总行数未变,版本号也会递增,避免不同版本读取的分页数据被错误拼接。playlist.insertTracks、removeTracks、moveTracks、reorder、setSelection以及queue.remove、moveToTop、setContents、insertNext、playNow支持传入expectedVersion:若目标内容在该版本后发生过变更,调用直接以STALE_TARGET失败且不作任何更改;调用成功时返回更新后的版本号。playlist:itemsAdded、itemsRemoved、itemsReordered、itemsReplaced与playback:queueChanged事件均会附带新版本号。playlist.playTrack与setFocusedTrack同样支持expectedVersion。queue.add以及传给queue.setContents、queue.insertNext的播放列表行项支持传入expectedPlaylistVersion(即读取这些行时所属播放列表的contentVersion)。SDK 对应方法已将expectedVersion作为可选参数置于末位。 - 受管自动播放列表 (
playlist.createManagedAutoplaylist):创建受管自动播放列表,其查询语句、排序规则与“保持排序”设置可通过playlist.getAutoplaylistConfig读取,并通过playlist.setAutoplaylistConfig修改。查询仍由 foobar2000 执行并随媒体库自动更新,但该播放列表要求保留安装 foo_ui_webview2。每次修改须传入所基于的revision;若依据的版本已过时,调用以details.reason: 'revisionConflict'失败;配置更新后会触发playlist:autoplaylistConfigChanged事件通知新版本号。其他自动播放列表(包括playlist.createAutoplaylist创建的普通自动播放列表)不支持此类读写。 - 原生自动播放列表编辑器唤起 (
playlist.showAutoplaylistUI):可按索引或playlistGuid打开指定自动播放列表的原生编辑器,且不切换活动播放列表、不开始播放。请求受理后立即返回;原生编辑器调用成功或失败随后通过playlist:autoplaylistUIResult事件通知调用页面(不反映用户最终保存还是取消)。playlist.getAutoplaylistInfo新增editorAvailability与editorAvailabilityReason,无需打开窗口即可判断编辑器是否可用。SDK 同步新增playlist.showAutoplaylistUI与playlist.onAutoplaylistUIResult。 - 路径展开状态追踪 (
playlist:addComplete):事件携带status字段:成功为completed;若展开出的曲目加入前播放列表被删除或锁定,则报告partial或failed,并附带reason(targetRemoved、locked);用户取消展开时为cancelled。
上下文与菜单
- 菜单状态前置校验 (
menu.runContextCommandById):支持在运行前核对菜单状态:传入节点的path作为expectPath,以及menu.getContextMenu新增返回的targetKey作为expectTarget。如果在菜单打开后,曲目、该命令 ID 对应的命令或其可用状态发生变化,调用将直接以details.reason失败,不执行任何操作,避免误触发其他命令。未传入这两个参数的调用行为保持不变。SMP 兼容层的RunContextCommandWithMetadb与ContextMenuManager.ExecuteByID已改用该核对机制。 - 原生命令执行回显:
menu.runMainMenuCommand、menu.runContextCommand与menu.runContextCommandById的应答新增completion: 'unknown'。命令已交由 foobar2000 原生执行,宿主不跟踪命令是否已完成,亦不跟踪弹出的对话框是否被取消。
窗口与桥接基础设施
- 缩放比例与换算系数反馈:
window.getZoom返回textScaleFactor;window.setDragRegions、window.setNoDragRegions和window.setClickThroughExcludeRegions返回scale(即换算坐标矩形实际使用的缩放系数)。 - 调用超时保护机制:API 调用若超过 30 秒未收到宿主应答,将以
code: 'TIMEOUT'拒绝,并在错误消息中指明方法名。注意该调用在宿主侧可能已执行完毕,详见操作结果。 - 托盘左键原生显隐 (
tray.setClickAction):设为toggleMainWindow后,左键单击托盘图标由宿主直接隐藏或显示主窗口,不经页面代码,页面挂起时也能响应;双击只切换一次。默认的event仍派发tray:click与tray:doubleClick。该设置可在tray.create之前调用,tray.destroy会将其重置为event;面板中调用以PANEL_MODE_UNSUPPORTED失败。切换前请先从原有的点击处理函数中移除窗口命令。
变更
- 拖放能力原因枚举精简:
dnd.getCapabilities与dnd:capabilitiesChanged的pathsUnavailableReason类型定义移除displaced(宿主从未返回过此值)。若 TypeScript 代码中存在与'displaced'的比较逻辑,移除该比较分支即可。 - 播放列表标准列 ID 规范:
fb-playlist-view不再将“播放次数”“首次播放”“最近播放”“添加日期”“等级”等中文汉化显示名作为列 ID。请使用标准列 ID:play_count、first_played、last_played、added与rating;或将fb.playlist.getAvailableColumns()返回的pattern配置到formats中,详见播放列表组件。 - 非本机面板 URL 权限门禁:若面板覆盖的页面 URL 位于非本机地址(非
localhost、127.x.x.x、[::1]),须在面板配置中勾选“允许此网站访问 foobar2000 和这台电脑”后方可调用 API;未勾选前页面虽可正常渲染,但所有 API 调用均以ORIGIN_DENIED拒绝。升级前已保存的网址默认保留信任。修改为其他网站或取消勾选后,确认对话框即可立即生效,无需重启 foobar2000。 - 主菜单命令按完整身份寻址 (
menu.runMainMenuCommand):按斜杠分隔的路径寻址时须匹配全部上级菜单,前缀不符时不再退而匹配同名的叶子命令。按 GUID 寻址动态叶子须同时提供guid与subGuid,动态容器不可执行。按名称或路径执行成功时,应答同样带guid。菜单树中主菜单项的executable表示其地址唯一且完整,与该项是否可用无关;unaddressableReason新增ambiguousMatch、missingDynamicIdentifier、dynamicParent与incompleteHierarchy,有歧义的项列出addressCandidates,仅供诊断。discovery.executeMainMenuCommand遇到歧义或禁用的命令时,分别以MENU_MATCH_AMBIGUOUS、MENU_ITEM_DISABLED失败。菜单状态无法读取时,menu.getMainMenu、menu.runMainMenuCommand与discovery.executeMainMenuCommand以OPERATION_FAILED失败,不再当作该项可用。 - 面板中的
window.focus:在 DUI/CUI 面板中省略windowId调用window.focus时,会把 foobar2000 主窗口切到前台,不再以PANEL_MODE_UNSUPPORTED失败。调用成功只表示已发出请求,Windows 仍可能拒绝切换前台。
修复
窗口渲染与高 DPI 适配
- 文本缩放拖动区与像素比对齐:修复 Windows 文本大小(设置 → 辅助功能 → 文本大小)大于 100% 时独立窗口的缩放与拖动判定异常。此前独立窗口会将标题栏自绘按钮误判为拖动区域(越靠右偏差越大),且
devicePixelRatio大于实际页面缩放。现已支持随文本大小正确缩放,与面板和后台窗口保持一致,app-region区域精确贴合;运行中调整文本大小亦可动态跟随。 - 自定义拖动区域缩放换算修正:修复
window.setDragRegions、window.setNoDragRegions与window.setClickThroughExcludeRegions矩形计算,改为按页面的devicePixelRatio换算坐标矩形(综合计入页面缩放与 Windows 文本大小)。此前仅按窗口 DPI 换算导致缩放不为 100% 时坐标错位;页面自乘缩放倍数的逻辑应予移除。window.setMaximizeButtonRegion亦同步计入文本大小。 - 弹出窗口 URL 参数追加修复:修复
window.createPopup打开已含查询参数(如page.html?mode=1)或哈希片段的页面时 URL 拼接错误的问题。windowId参数现能正确追加为&windowId=...,避免出现双重?导致页面无法读取自身windowId。 - 托盘图标缺失时的窗口保护:修复启用“最小化到托盘”或“关闭到托盘”但未创建托盘图标时的窗口失踪问题。未调用
tray.create时,最小化或关闭窗口回退为最小化到任务栏,避免窗口被隐藏后无法恢复。调用tray.destroy时,若主窗口当前正处于托盘隐藏状态,也会自动恢复显示。 - 系统盘媒体文件权限放宽:优化文件路径权限判定。
shell.showInExplorer、shell.openWith、clipboard.writeFiles与artwork.getFolderImages的路径检查调整为MediaRead权限级别(此前为Read)。媒体库或播放列表中位于系统盘上的音频文件现支持在资源管理器中定位、用外部程序打开及复制到剪贴板;系统盘中非受信任目录的其他路径仍保持拦截。 - Windows 10 材质与圆角回退:在 Windows 10 上请求 Mica、Mica Alt 或 Acrylic 时改用纯色背景,宿主也不再请求圆角;窗口能力中的
supportsCornerPreference与supportsMicaAlt在该系统上为false。Windows 11 行为不变。 - 弹出窗口拖边改尺寸:设置了
behavior.noActivate或进入过点击穿透模式的弹出窗口,拖动边缘改尺寸时不再进入 Windows 的模态尺寸循环,页面在拖动过程中持续渲染;其他弹出窗口在拖动中合并过密的尺寸更新,并始终应用最终尺寸。
运行时与诊断稳定性
- WebView2 创建失败原生对话框:修复独立窗口模式下 WebView2 创建失败(如未安装运行时)时的白屏/隐形问题。现在会弹出原生错误对话框说明原因并给出操作建议,支持一键复制诊断信息、打开组件首选项或控制台。此前窗口保持全透明无任何反馈。
- 运行时不可用原生兜底菜单:修复 WebView2 不可用时窗口缺少兜底交互的问题。在窗口客户区右键可弹出原生兜底菜单,提供首选项、控制台、重新加载界面、错误详情及退出选项。
- 启动生命周期日志完善:profile 目录下的
webview_lifecycle.log会在初始化开始与失败时记录组件版本、foobar2000 版本、Windows 内部版本号、WebView2 运行时版本、实际生效的数据目录、DPI 感知状态、兼容性选项、WebView2 环境变量及提权状态;创建环境失败时会记录对应 HRESULT 错误码。 - 自定义菜单崩溃防护:修复调用
ui.showCustomMenu时特定场景下可能引发崩溃的问题。感谢 @MoveToEx 报告。 - 慢速存储封面墙无响应优化:修复媒体位于低速网络存储或挂载网盘时封面墙可能导致主窗口无响应的问题。感谢 @Sijie20 报告。
- 网络共享超时主线程防卡死:修复访问离线网络共享或未连接映射盘时的界面卡顿问题。
file.read、file.exists、file.list与file.getInfo在等待网络超时期间不再阻塞主线程(此前可能导致启动时界面延迟逾 20 秒);控制台输出的慢调用告警现会附带具体路径。 - 歌词读取移出主线程:
lyrics.get与lyrics.exists改在后台线程读取标签与歌词文件,媒体位于低速存储或云盘时不再卡住窗口。 - 本地媒体读取不再探测无关网络盘:检查本地媒体路径时不再逐一打开全部映射的网络共享,未连接的映射盘不再拖慢本地文件的读取。
JIT 队列机制
- 影子列表行归属识别:修复
jitQueue会话判定机制,改为依据曲目在影子播放列表中的行项而非单纯按路径识别。以原生路径开启的会话不再意外中断;重播当前曲目不再消耗已缓冲的同文件下一首;在其他播放列表播放相同文件会正常结束当前会话。 - 带子曲目路径准确起播:修复
jitQueue.playNow与jitQueue.enqueueNext对含子曲目路径(path|subsong:N)的解析。此前本地路径会误播为子曲目 0,带查询参数的网络流地址无法起播,现均可准确起播指定子曲目。 - 解析竞态与取代处理:修复
jitQueue解析中曲目被取代时的竞态问题。尚未完成解析的条目若被后续的playNow、enqueueNext、stop、clear、preloadBatch或手动切歌/离开列表所取代,不再追加至播放队列,不触发错误事件,已被替换的下一首不会将旧资源重新写回缓冲。 - 连续调用冲突解决:修复
jitQueue.playNow连续调用时的排队冲突。前一次调用仍在解析时再次调用不再直接以OPERATION_FAILED失败,新请求会自动取代前一次解析。 - 原生播放队列完整保留:修复
jitQueue.playNow与jitQueue.skip覆盖原生播放队列的问题。播放指定曲目或跳过已缓冲曲目时,原生播放队列条目得以完整保留。JIT 曲目播放完毕后将优先顺延播放原生队列中的条目,会话随之正常结束。若无已缓冲曲目,skip回退执行 foobar2000 原生“下一首”指令。 - 迟到解析结果打断防护:修复原生播放推进时迟到的
jitQueue解析结果打断新曲目的问题;重播当前曲目时,正在异步解析的下一首条目仍会妥善保留。
媒体库与播放列表
- 启动载入状态字段校正:修复
library.getStatus的initialized字段,调整为准确反映启动时是否已完成媒体库载入(此前仅重复反映enabled)。 - 非法 UTF-8 标签容错:修复标签包含非法 UTF-8 字节时的容错处理。专辑列表、元数据读取与歌词现在仍可正常返回。异常字符以替换字符(U+FFFD)安全显示,不修改原始音频文件。
- 损坏重名专辑独立性保护:修复元数据损坏导致重名的专辑被错误合并的问题。此类专辑现仍保持独立身份;SDK 与媒体库树加载曲目时会保留其独立性,并完整保留 CUE 内嵌子曲目。这类专辑行带有
albumId,传给library.getAlbumTracks即可读取该专辑的曲目。 - 异常编码标签序列化容错:修复编码异常标签导致元数据序列化中断的问题。名称异常的标签按字段略过,不覆盖正常字段;异常标签值或编解码器信息不再阻断异步元数据批次结果的序列化。
- 展开进度取消事件补全:修复
playlist.addPathsAsync展开播放列表文件过程中取消进度对话框时不派发完成事件的问题。取消操作现会准确派发status: 'cancelled'的playlist:addComplete事件。 - 压缩包内曲目读取:路径按
MediaRead检查的方法现可接受压缩包内的曲目(unpack://…)与便携版相对路径(file-relative://…),宿主按压缩包的实际位置检查权限;此前这类调用一律以PERMISSION_DENIED失败。嵌套压缩包与名称含非 ASCII 字符的压缩包路径也能正确解析。 - 便携版 profile 位于系统目录:便携版 foobar2000 装在
C:\Program Files、C:\Program Files (x86)或C:\ProgramData下时,页面可以读写其 profile 目录;安装目录本身仍被拦截。系统盘的可读文件夹新增“图片”。 - 整轨波形按实际解码格式生成:
audio.generateFullWaveform与audio:fullWaveformReady按解码器实际输出的 PCM 格式划分波形窗口,sampleRate与channels也报告该格式,可能与文件的标称值不同。此前按标称采样率划分,两者不一致时波形与曲目对不齐。
拖放交互
- 独立窗口内部拖放重排:修复独立窗口内原生拖拽重排失效的问题。页面内部发起的拖动在
dropEffect: 'move'模式下可正常放下,拖动重排序行与标签页不再误显禁止光标或丢失drop事件。来自外部程序的拖拽仍按复制处理;面板嵌入模式行为暂未变动。 - 未处理元素拖放拦截:修复未明确监听文件拖放的文本框与
contenteditable元素误收拖放的问题。未处理文件的元素现在会与其他区域一致拒绝放下(光标显示禁止,不派发dnd:drop事件)。原生文件上传框仍支持拖入文件,文本拖拽选区填充不受影响。
v2.0.0 (2026-10-06)
自本版本起,foo_ui_webview2 遵循语义化版本,2.x 系列内不再包含破坏性变更。详见版本与兼容性。
本版的破坏性变更
- 传入未声明的参数键(
_开头的除外)、值类型错误或缺少必填项时,调用以INVALID_PARAMS失败,error指明出错的参数。此前多余的键会被忽略。 - 仅宿主信任的页面可以调用 API。打开者不信任的
http(s)弹出窗口、从受信任来源跳走的页面、file://页面,以及开发模式下不在所配置开发服务器上的页面,调用立即以ORIGIN_DENIED失败,且收不到事件。详见哪些页面能调用 API。 - SDK 的列表方法改为返回宿主的信封,失败时不再抛错,涉及
playlist.getAll、getTracks、getSelectedTracks、getAvailableColumns,output.getDevices,system与config的列表方法,以及playcount.get。请从对应字段读取列表,或使用unwrap。 - SDK 生成的
XxxResponse改为XxxSuccess | ApiFailure,须先判断success才能读取字段。部分事件载荷类型收窄,artwork.getAvailableArtwork须传path,metadata.embedArtwork须传opts,FbBaseElement._sub只接受FBEventName中的事件名。以上仅影响 TypeScript 编译。 - 无宿主环境下,SDK 调用返回
{ success: false, code: 'NOT_SUPPORTED', error, details: { method } },不再返回{ mock: true, method }。如需在假数据上运行,请将替身赋给window.fb2k。 - 曲目事件改发共享的
Track,移除id与fullPath,请改为读取handle。涉及playback:trackChanged、playback:edited、playback:itemPlayed,以及selection:changed的track与nowPlaying。 library.getAlbumTracks改为接收library.getAlbums返回行的name(传给album)与albumArtist,两者必填,不再接受artist;结果以albumArtist代替artist。- 以下调用的失败方式有变:
- 播放列表锁定时,
library.addToPlaylist、playback.playPath与playback.playPaths以LOCKED失败(此前报成功);queue.addPaths以LOCKED失败(此前为OPERATION_FAILED),playlist与isLocked移入details。 - 无活动播放列表时,
queue.add以NO_ACTIVE_ITEM失败(此前为INVALID_INDEX)。 artwork.getByPlaylistItem的播放列表序号越界时以INVALID_INDEX失败(此前为NOT_FOUND)。- 负数行号与队列位置以
INVALID_PARAMS失败(此前被丢弃,playlist.reorder与reorderPlaylists则为INVALID_INDEX)。 - 右键菜单给出的
handles全部不可用时以INVALID_PARAMS失败,不再改用正在播放的曲目、选中项或播放列表。
- 播放列表锁定时,
- 仅在页面的
dragover处理中调用了preventDefault()的位置接受文件放下;其他位置显示禁止光标,不发dnd:drop。 lyrics.save不再接受config,all只写file与embedded;子曲目不再自动读取编号或整张镜像共用的歌词文件,请通过filename指定。已有歌词不会被删除,详见迁移说明。
新功能
- 支持按 GUID 指定播放列表。
playlist.getAll、getActive、getPlaying、create、duplicate、createAutoplaylist返回guid;作用于单个播放列表的方法(含library.addToPlaylist、queue.add、queue.addPaths、artwork.getByPlaylistItem)接受playlistGuid,列表已删除时以NOT_FOUND失败。 - 以序号报告播放列表的应答同时提供
playlistGuid;playlist:*事件同样带 GUID(playlistGuid、guid、newGuid、guids)。 playlist.setActive、playlist.rename接受playlistGuid;playlist.reorderPlaylists新增newOrderGuids;queue.setContents与queue.insertNext的条目接受playlistGuid。- 新增
playlist.getMatchingRows(返回符合查询的行)与playlist.getTracksAt(按行号读取曲目)。 - SDK:接受播放列表的方法支持
PlaylistRef(序号或 GUID);<fb-playlist-view>与<fb-playlist-tabs>按 GUID 定位播放列表。 - 播放位置新增
hostTime(Unix 毫秒,与Date.now()同一时钟),见于playback.getPosition、playback.setPosition、playback:seeked、playback:timeHighRes与playback:stateChanged;SDK 新增PlaybackClock,用于推算两次更新之间的播放位置。 playback:stateChanged新增canSeek。- 本地媒体(实验性,后续版本可能变化):新增
media.getStreamUrl与media.getContainerInfo,SDK 新增MediaElementFollower与canPlay。见 Media 媒体 API 与 本地媒体。 - PCM(实验性,后续版本可能变化):新增
audio.decodePcm,把曲目或其中一段解码为 float32 PCM;新增audio.subscribeStream,订阅 foobar2000 正在播放的音频。样本经共享缓冲送达,SDK 以PcmBuffer与PcmStream读取。 audio.subscribeSpectrum新增output: 'bins',输出原始 FFT 频点;channels: 'stereo'时分left与right输出。SDK 回调接收SpectrumBinsFrame。metadata.write与metadata.writeBatch支持以字符串数组写入多值标签,空数组删除该标签。- 曲目行新增
albumArtists,library.query、library.search与playlist.getTracks的fields均可选用。 lyrics.get与lyrics.exists新增filename,可指定歌词文件。- 新增
webview.getSource,返回页面的加载来源。 - 新增
window.setMaximizeButtonRegion,使页面自绘的最大化按钮在 Windows 11 上支持贴靠布局。 window:stateChanged新增windowId。file.write、fb.file.writeBinary与fb.file.writeDataUrl新增atomic: true,不可与append同用。- 新增
tray.setMenuZones,一次替换整个托盘菜单。 dnd:enter、dnd:drop与dnd.getPathsAsync新增source,区分本页面('self')、同一 foobar2000 的其他窗口('other-window')与外部('external')的拖动。- 便携版改为使用各自 profile 目录下的 WebView2 数据,支持多个实例同时运行。首次启动时复制共用目录中的网页数据;共用目录仍被其他 foobar2000 占用时,改为以空数据启动,控制台会写明两个目录的位置。
config.set保存的设置不受影响。 - 文档站的 API 参考按命名空间分页,新增 SDK 类型参考、入门教程,以及从实时 PCM 流计算频谱的示例。
变更
library.getAlbums与library.getAlbumTracks共用同一份分组结果,再次打开同一张专辑不再扫描媒体库。library.getAlbumTracks的结果新增row,曲目按碟号、曲号、媒体库顺序排列。library.addToPlaylist、playlist.addHandles与playlist.insertTracks改为按媒体读取校验路径,有路径被拒时整次调用以PERMISSION_DENIED失败。playlist.reorder与playlist.reorderPlaylists的newOrder含重复项时以INVALID_PARAMS失败。- 曲目事件载荷新增
artists与rating,所有字段始终存在。 audio:dspPresetChanged与playlist:defaultFormatChanged改为发送空对象(此前为null)。- 解析器拒绝的查询以
INVALID_PARAMS失败并带details.param: 'query',适用于playlist.getMatchingRows、library.search与library.query。 - 超过 259 个字符的路径以
OPERATION_FAILED失败,details.value为 206;file.copyAsync、file.moveAsync与file.deleteAsync报告原因path-too-long。详见路径长度。 file.read与file.write无法打开文件时,在details.value中提供 Windows 错误码。- 歌词的
filename按 Windows 文件名规则校验:允许名称内部的句点,拒绝末尾的句点或空格。 - 保存歌词会使读取缓存失效。
- 频谱的频带输出保留以兼容,数值不变,文档将其列为旧接口。
- 宿主拒绝
fb2k.invoke请求时,Error带code属性(如METHOD_NOT_FOUND、INTERNAL_ERROR)。 - SDK 的
library.getAll失败或超时时返回OPERATION_FAILED,不再 reject;playlist.getTracksPage已弃用,getTracks返回相同的分页响应。 - 读不出序号的
|subsong:N后缀会被去掉,路径指向第一首。 - 移除
audio.generateWaveform(此前每次调用都以未实现失败),请改用audio.generateFullWaveform。
修复
- 修复弹出窗口与面板收不到
menu:select、menu:dismiss、menu:valueChanged的问题,这些事件现在发给打开菜单的页面。 - 修复弹出窗口与面板开启 JIT 会话后收不到
jitQueue:*事件、播完第一首即停止的问题。 - 修复面板页面收不到
http:response与http:downloadComplete的问题。 - 修复以 Default UI 或 Columns UI 为界面时,面板页面收不到
app:beforeQuit的问题。 - 修复同一 foobar2000 窗口中有多个面板时,各面板被识别为同一个的问题。
- 修复弹出窗口中
window.getMode的windowId恒为main的问题。 - 修复弹出窗口中的
<fb-titlebar>与<fb-window-controls>跟随主窗口最大化状态的问题。 - 修复连不上开发服务器时,弹出窗口停留在浏览器错误页的问题。
- 修复带用户名或密码的地址可冒充受信任来源的问题。
- 修复
LYRICIST标签被当作歌词读取的问题。 - 修复
playback.playPaths仍播放已报告为跳过的路径的问题。 - 修复播放列表锁定时,
playlist:addComplete的addedCount计入被拒曲目的问题。 - 修复以暂停状态开始播放时,
playback:stateChanged报告playing的问题。 - 修复事件文本含非法 UTF-8(如网络电台的流标题)时事件无法发出的问题,非法字节现在替换为 U+FFFD。
- 修复
metadb:changed对无元数据的曲目缺少播放统计评分的问题。 - 修复行或播放列表删除、移动后,
queue.get报告错误播放列表位置的问题。 - 修复选择不在活动播放列表中时,
selection.getViewingTrack报告活动列表却没有行号的问题。 - 修复
playlist.playTrack(deferred: true)在起播前有列表增删时播放错误行的问题。 - 修复 SMP 兼容层的
RemovePlaylistSelection带crop且读不出选择时清空整个列表的问题。 - 修复
discovery.executeContextMenuByPath、titleformat.eval系列方法与replaygain.get、clear、scan无法作用于多曲目文件中第一首以外曲目(|subsong:N)的问题。 - 修复
playcount.get与getBatch的失败行未回显请求路径的问题。 - 修复主窗口不在前台时
taskbar.flash报告失败的问题。 - 修复页面重载或跳转后,其打开的端口仍然保留的问题,端口现在随页面关闭并发出
port:disconnected。 - 修复
fb2k.on返回undefined的问题,现在返回移除该监听的函数。 - 修复宿主拒绝拖出时,页面内的拖动也被取消的问题。
- 修复从资源管理器长时间拖动后拖影残留、页面无响应的问题。
- 修复文件放到页面未处理的位置时,在另一个窗口中打开的问题。
v1.14.0 (2026-10-06)
本版的破坏性变更
library.getArtistAlbums、getArtistTracks与getAlbumTracks改为精确匹配:逐字节比较,区分大小写,*、?不再作为通配符。如需在getArtistAlbums中沿用原有的子串匹配,请传入match: 'substring'。library.getArtistAlbums返回行中的artist改为专辑艺术家(album artist首值,缺失时取artist首值)。library.getArtistAlbums改为先排序、再按limit截断,截断时返回的专辑可能与此前不同;limit: 0返回空列表。queue.get中不带播放列表位置的条目,其playlist与playlistItem为null,请用item.playlist == null判断。
新功能
- 支持将曲目作为真实文件拖出窗口:先用
dnd.prepareDrag换取一次性 token,再在dragstart中写入(SDK 为fb.dnd.applyDragToken)。dataTransfer.effectAllowed必须为'copy'。 - 新增
dnd:dragEnded,仅在宿主拒绝挂上文件时触发;SDK 用fb.dnd.onDragEnded订阅。 dnd.getCapabilities与dnd:capabilitiesChanged新增dragOut与dragOutUnavailableReason;拖出需要 WebView2 Runtime 144 或更新。- 新增
queue.setContents,以有序列表整体替换播放队列。 - 新增
queue.insertNext,按路径或播放列表位置插入为下一首;已在队列中的曲目会被移动,不会重复入队。 - 新增
queue.playNow,立即播放队列中的指定项。 playback:queueChanged新增count,重建类变化只广播一次。- 新增
playlist.getGroupRuns,一次返回整个播放列表的分组边界,不含曲目行。 playlist.getTracks新增fields,仅返回指定字段;未知字段名以INVALID_PARAMS失败,并列在details.unknownFields中。library.getArtistAlbums新增sort与match参数,响应新增artist、total与hasMore;返回行补全专辑字段(albumArtist、discCount、duration、genre、label、firstTrackPath、firstTrackAbsolutePath),计数只统计该艺术家参与的曲目。library.getArtists新增includeAlbums,为true时附带每位艺术家的专辑。- 支持查找按「艺术家 - 标题」命名的歌词文件,以及去掉子轨编号的共享
.lrc。 - 频谱新增
scale: 'db'、minFrequency与maxFrequency,fftSize上限提高至 65536;每帧带subscriptionId、实际使用的参数、streamTime与hostTime。 audio.getWaveform新增channels与points。- 整轨波形更换
method、signed或scale时不再重新解码,同一文件的并发请求合并为一次解码;新增audio.cancelFullWaveform,应答带maxAmplitude。 - SDK:
fb.audio.subscribeSpectrum的退订函数带ready;fb.audio.generateFullWaveform接受signal;新增playlist.getGroupRuns与playlist.getTracksPage;library.getArtistAlbums接受options。 dialog.openFolder新增defaultPath,支持%music%。- 自绘菜单中的评分、滑块与分段行支持鼠标滚轮调节。
- 偏好设置页(
Display → WebView2 UI)重做:改动点击Apply或OK后保存,Reset page恢复默认值,Cancel放弃改动。 - 新增窗口、性能、开发者三个子页,支持设置默认缩放(50–200%)、启动时预热 WebView2 与 CDP 端口(1024–65535,默认 9222)。
- 模板的新建、重命名与删除移至
Manage...,操作立即生效。
变更
- 后台模式、深度挂起、开发者工具、CDP 远程调试、开发服务器开关与 URL 从高级设置移至子页,原有设置自动沿用;HTTP 访问本地网络与无效 TLS 证书仍在
Advanced → Tools → WebView2 UI中。 - 切换组件语言改为点击
Apply后写入,之后新打开的对话框使用新语言。 - 正在使用的模板与被面板引用的模板不能重命名或删除;模板列表为空时不再自动创建
default。 - 频谱按订阅的帧率准时推送;暂停或停止时先发一帧静音再停发;参数越界以
INVALID_PARAMS失败。 audio.getWaveform的duration须在 (0, 1] 秒内,否则以INVALID_PARAMS失败。- 隐藏到托盘期间,
window.getState报告minimized: true,隐藏时多发一次window:stateChanged。 Allow insecure HTTP connections (disable HSTS)标为保留项,当前无实际效果。
修复
- 修复曲目所在目录下的外挂歌词与封面文件无法读取的问题,便携版同样适用(#15)。
- 修复文件名含
..的曲目被误判为路径穿越的问题(#17);lyrics.save、metadata.embedArtwork的filename与log.write的file按同一规则判断。 - 修复名称含双引号的艺术家无法获取专辑与曲目的问题。
- 修复
library.getArtistAlbums截断时trackCount偏小、失败时缺少albums的问题。 - 修复
<fb-library-tree>在艺术家节点下混入其他艺术家专辑的问题。 - 修复多个频谱订阅相互改变
fftSize、bands与fps的问题。 - 修复为路径过长的曲目生成整轨波形时 foobar2000 可能崩溃、不同曲目共用同一份波形的问题。
- 修复隐藏到托盘后,原窗口所在区域无法点击的问题。
- 修复以原生路径(如
E:\...)添加的曲目与播放列表中的同一文件无法对应的问题。 - 修复
playlist.addPathsSequential未按传入顺序添加的问题。 - 修复
playlist.addPathsAsync、library.addToPlaylist与audio.analyzeBPM忽略path|subsong:N的问题。 - 修复播放列表行的
rating与rating.get不一致的问题,两者现在都先读 foo_playcount。
已知问题
- 1.x 对页面来源的校验不完整:通过
window.createPopup打开的http(s)网址会被直接信任,页面可以调用全部 API(包括shell.exec);开发模式下,本机任意端口上的页面也受信任;事件会推送给当前加载的任何页面,包括已跳转到其他来源的页面。这些问题已在 v2.0.0 修复,1.x 不再修复。请勿在弹出窗口中打开不受信任的网址。 - 通过
queue.insertNext的paths插入的曲目不带播放列表位置,播放完毕后将从插入前的位置继续播放。已知曲目在播放列表中的位置时,请改用items。 - 播放队列不会在重启后保留,这是 foobar2000 自身的行为。
v1.13.0 (2026-08-26)
本版的破坏性变更
以下四项可能需要修改代码:
library.search不再把行数组重复一份items。 响应此前把同一批行以items与tracks两个键各带一份,内容逐字节相同,现在只发tracks——与library.query一直使用的键一致。读result.items的代码请改读result.tracks;新旧宿主的每一种响应形状都带tracks,因此先读tracks的代码在仍发送两份的旧宿主上同样成立。带行数据的 search 响应载荷就此减半。dialog.confirm现在 resolve{ response },而不是{ confirmed }。 原先声明的类型是错的:宿主一直返回的是所点按钮在buttons中的零基索引,从未返回过confirmed标志,读result.confirmed的 TypeScript 代码读的是从未被填充过的字段。默认按钮集为['OK', 'Cancel'],因此0表示确认、1表示取消。任务对话框创建时未带TDF_ALLOW_DIALOG_CANCELLATION,所以 Esc 与关闭按钮都关不掉它,每个结果都来自一次真实的按钮点击;-1只出现在宿主的兜底路径上——连普通消息框都显示不出来的时候。file.*的错误消息变了,且被拒绝的路径不再回显。 该命名空间内所有裸写的错误信封统一改走标准错误信封,std::filesystem异常只外传 Win32 错误号——异常文本与出错路径不再进入 payload 和宿主日志。路径安全拒绝现在形如file.read: path security denied for 'path': Access denied: protected system path:给出方法名、出错参数(数组参数带下标,如items[2].destination)与策略原因,调用方能判断是哪个实参被拒,而宿主不会把一个文件系统位置泄漏到页面可能转发出去的 payload 里。此前解析result.error取路径或匹配特定措辞的代码,需改用result.code配合result.details。- 参数形状不对现在返回
INVALID_PARAMS,不再是PERMISSION_DENIED。 路径安全装饰器此前把「路径被拒」与「实参格式错误」报在同一个 code 下。靠PERMISSION_DENIED分支去捕获类型错误的处理逻辑,将不再在那里收到它们。
异步文件操作
- 新增
file.copyAsync、file.moveAsync、file.deleteAsync与file.cancelOp。 实际工作在宿主 worker 线程上进行,复制一整张专辑不再像同步的file.copy那样冻结 UI。调用立即返回{ operationId, totalCount }回执;结果经file:opProgress陆续到达,最后由一个file:opComplete收尾。 - 新事件
file:opProgress与file:opComplete(载荷:operationId、op、done、total、results/ 三项计数与cancelled)。结果是成批发送,绝不是每条一个事件:累积到 64 条待发或距上一批已过 100 ms 时发出,因此快速运行会收敛到约ceil(total / 64)个事件,而慢速运行则以额外事件换取持续推进的进度信号。done / total可直接当进度分数用,最后一个不满批总在file:opComplete之前到达。 - 每个请求条目上报一个结果,绝不是每个文件一个:目录条目在整棵树走完后才上报一次。每个结果按请求原样回显
source(以及destination,deleteAsync没有),%变量%占位符原样保留不展开,因此可直接当查找键。status取'ok'/'skipped'/'failed',reason取自already-exists、not-found、permission、cross-volume、io-error与cancelled。 - 路径校验整批一致。 任一条目未通过宿主的读或写检查,整个调用以
PERMISSION_DENIED被拒且不产生operationId——绝不会派发半批。copyAsync按Read校验source、按FileWrite校验destination;moveAsync两端都按FileWrite校验,因为 move 会删除源;deleteAsync对每个路径按FileWrite校验。全进程最多 8 个操作同时进行。 - copy 与 move 在目标已存在时行为不同:
copyAsync把目录合并进已存在的目录(其中已有的文件被跳过且不单独上报,该条目仍报status: 'ok'),而moveAsync报skipped/already-exists。overwrite(默认false)只覆盖文件目标——Windows 无法原地替换目录,因此已存在的目录目标永不被替换。注意同步的file.move总是替换文件目标;异步形式不主动这么做。 - 同卷内的 move 是一次重命名,无论文件多大都不花代价。跨卷时宿主回退为「先复制后删源」;该条目仍报
status: 'ok',但带reason: 'cross-volume',让这份额外开销可见。 deleteAsync默认moveToTrash: true,它把每个路径交给 shell,因而需要主线程——这类删除在主线程上以 16 条一批进行并在批间让出。moveToTrash: false在 worker 线程上删除,并且会删掉非空目录——同步的file.delete在该模式下拒绝这么做。file.cancelOp在批次中途生效,而不是等到批次末尾:copy 与 move 在一个文件之内停下,中止正在传输的文件并清除其半成品;delete 在下一个条目处停下。已完成的条目保留结果,其余每个条目都上报为skipped/cancelled,且该次运行仍以带cancelled: true的file:opComplete收尾。关闭 popup 会取消该 popup 发起的操作;面板宿主没有这个钩子,因此其操作除显式取消外会跑到底。操作已结束或从未存在时返回cancelled: false——这两种情况故意不作区分。- 只要发起调用的窗口还活着,两个事件都投递给它,这也正是
results敢携带真实文件系统路径的前提。该窗口消失后宿主无法再解析它,会回退到主实例,因此迟到的事件可能出现在并未发起该操作的窗口里。有两条路径会跳过file:opComplete——运行中宿主关闭,以及宿主侧意外失败——所以监听者若持有需要清理的状态,应自带超时,而不是无限期等它。
元数据探测
- 新增
metadata.probeBatchAsync与metadata.cancelProbe。 落盘读在宿主 worker 线程上进行,几百个路径不再像metadata.readBatch那样卡住 UI。调用返回{ operationId, totalCount }回执;结果经metadata:probeProgress到达,最后由恰好一个metadata:probeComplete收尾。 - 每条结果都上报信息来源——
infoSource: 'cached' | 'direct'——这正是该端点对媒体库从未见过的文件有价值的原因。失败时按情况上报'not-found'/'unsupported-format'/'read-error'之一,而readBatch把这个区分合并成一个笼统的错误字符串。includeTags(默认true)附上扁平标签表,上游键按readBatch的口径大写;只要技术信息时传false。 - 路径可带
|subsong:N后缀、各自独立解析,并原样回显,因此可直接当查找键。与metadata.read不同,批量表面不认旧式#N子歌写法——它会把恰好以#<数字>结尾的无扩展名文件名切错。 - 路径校验整批一致:任一路径未通过宿主的媒体读检查,整个调用以
PERMISSION_DENIED被拒且不产生operationId。不提供逐路径拒绝。 metadata:probeProgress与文件事件用同一套「64 条或 100 ms」成批规则。metadata:probeComplete总会到达,取消与失败路径上也一样。取消会中断正在进行的磁盘读而不是等它结束;尚未触及的路径永不上报,被中断的那个路径既不算成功也不算失败,因此取消的运行里successCount + failureCount会小于total。
拖放
- 拖入的快捷方式现在会告诉你它指向哪里。 Windows 放进拖放文件列表的是
.lnk文件本身,而 foobar2000 播不了它,因此每个路径来源现在都配了一个平行的快捷方式目标数组:dnd:enter/dnd:drop载荷与dnd.getPathsAsync()上的resolvedPaths,以及用于同步快照的新方法dnd.getResolvedPaths()。两个数组长度恒定相等,因此paths.map((p, i) => targets[i] ?? p)即可得到可播列表。 - 拿不到目标时该项为
null:路径不是快捷方式、快捷方式指向回收站一类 shell 命名空间对象而非文件、记录的目标太长无法完整取回(Windows 以MAX_PATH截断,而被截断的路径会指向另一个文件)、COM 不可用,或为保持投放响应性而跳过了解析。永远不会是空字符串,因此真值判断就够了。 - 目标只说明快捷方式指向何处,不保证那个文件在。已失效的快捷方式上报其
.lnk记录的路径而非null——因为无论目标是否还存在,Windows 都会把那个路径交回来,而宿主在被拖放阻塞的线程上负担不起一次文件系统检查。非空项也可能指向已不存在的文件,请自行处理。只解析.lnk——.url、.library-ms与虚拟搜索结果都报null。 - 读
resolvedPaths不产生任何文件系统访问:目标在拖放到达时已解析一次,因此getPathsAsync()只读宿主内存。 - 一处已知限制 —— 页面侧快照只发布给顶层文档,因此
dnd.getPaths()与dnd.getResolvedPaths()在<iframe>内返回空数组,且该槽位在这个文档的生命周期内一直是null。被嵌入的页面若需要路径,须由主框架经postMessage转交。 - 把曲目拖出窗口仍不支持,
dnd.startDrag继续 resolve{ success: false, code: 'NOT_SUPPORTED' }。
媒体库查询
- 多值标签不再截断到第一个值。 自首个版本起,标了多位艺术家的曲目在
metadata.read之外的所有地方都只报第一位。曲目对象现在把各值按标签顺序以,拼接、不去重——与 foobar2000 本体的显示一致——作用于artist、albumArtist、genre、composer四个字段,各 API 仍只带自己本来就有的字段。 - 新增
artist的原子形态artists。 拼接串分不清一位叫"A, B"的艺术家和A、B两位艺术家,因此 library 命名空间的曲目对象——library.getAll、library.query、library.search、library.getByPath及其余library.*曲目端点——额外携带artists: string[]原样值数组,artists.join(', ') === artist逐字节成立。其他命名空间的曲目对象(playlist.getTracks、playback.getCurrentTrack、queue.get、artwork 与事件载荷)不含此字段。 - 聚合按每位参与艺术家计数。
library.getArtists给每位参与艺术家各建一个条目,trackCount因此成为参与计数:各条目相加会大于曲目总数,albumCount与totalDuration同样按每位艺术家重复计入。library.getStats的totalArtists现在与getArtists的条目数一致;library.getGenres对多值genre的每个值各建一个条目。 - 新增
library.search与library.query的字段投影。library.search接受options.fields,library.query新增第四个fields参数。返回的每一行随后只持有所请求的键、别无其他,因此TrackInfo在运行时是局部视图,而声明类型仍是完整的。可用名是 20 个曲目键——index、title、artist、artists、album、albumArtist、genre、date、trackNumber、discNumber、duration、path、absolutePath、fileSize、bitrate、sampleRate、channels、codec、subsong、rating——区分大小写匹配。省略该参数即返回全部 20 个。传入非数组、空数组、非字符串元素或未知名字都会 resolve(绝不 reject){ success: false, code: 'INVALID_PARAMS' },并在details.unknownFields回显出错的名字。 library.search与library.query现在在主线程之外做序列化,并把结果直写到通道上,不再构建中间对象图。响应管线沿途不再深拷贝,零命中的查询会短路返回。除破坏性变更里列出的items移除外,JavaScript 契约不变:两者仍从同一个 promise resolve 同样的形状。- Spider Monkey Panel 兼容层不再逐页重扫媒体库。
fb.GetQueryItems此前按 500 条一页翻页,而library.search每次请求都重扫全库,于是几千条命中的查询要为每页付一次全库扫描——大库上是 160 次。现在改为先发一次单行探测取命中总数,再一次请求拉回全部命中,并把投影收窄到FbMetadbHandle实际会读的五个键。可观测的 handle 属性没有任何变化。代价:若两次调用之间媒体库发生变化,探测到的总数就过期了,期间新增的命中会被丢弃;媒体库被并发修改时不保证返回集合稳定。 - 超大库的实务提示。 这些改动去掉的是拷贝与载荷,不是产生这些行本身的成本。在六位数规模的库上,全字段全库查询仍会占用宿主主线程相当长的时间,主导项是交给页面的响应体量。不需要全部 20 个键时请用
fields收窄投影——这是目前最有效的单一手段。32 位宿主上还请留意峰值:全字段全库结果在解析期间会同时以多种形态存在,六位数规模的库可能瞬时达到数百 MB。
标题格式化
titleformat.eval、evalBatch、evalFields与evalFieldsBatch现在上报infoAvailable。 宿主本就知道某曲目的 metadb 信息是否就绪,却把这个信号丢掉了,导致标签派生的输出可能静默出错。infoAvailable: false意味着标签派生的值不可信。批量形式按行携带该标志,失败的行不带它。- 两个需要知道的边界:在
evalFields形式下一个标志覆盖整份合并脚本,因此说不出是哪个具体字段受影响;它也从不覆盖 foo_playcount 虚拟字段。名字恰好叫infoAvailable的fields键会覆写该标志,这与path、success的既有行为一致。
错误与权限
fb2k.invoke在子框架内现在立即失败。 它以带code: 'NOT_SUPPORTED'的Errorreject,消息为fb2k.invoke is unavailable in subframes,取代此前静默挂到 30 秒超时。分离式调用(const { invoke } = fb2k)仍以 rejection 形式暴露其TypeError,而不是同步抛出。- 跨卷的目录 move 现在返回
NOT_SUPPORTED并带details.reason: 'cross-volume',而不是一个笼统的失败。跨卷的文件 move 本就成功,因为底层重命名会静默改走复制。 - 路径白名单与黑名单条目现在规范化到与判定侧一致的 canonical 真实路径形态。此前用等价但不同写法书写的条目——映射盘、junction、8.3 短名——匹配不上它本想覆盖的路径。
- 拖入快捷方式、元数据探测与异步文件操作都已纳入参考手册的权限矩阵;其中的计数由 64 个 API 上的 67 条 spec 变为 68 个 API 上的 73 条。
FileWrite 现在接受媒体库监视目录
FileWrite 链——所有 file.* 写操作背后的通道——新增了一步媒体库监视目录判定,因此监视目录内的路径现在即使在系统盘上也可写,而此前只有「非系统盘」那一步才可能放行。FileWrite 本就是暴露给主题的最宽写通道,这次进一步加宽。若你审计主题,这是第一个该看的通道,而监视目录列表现在也属于它的攻击面。
窗口与菜单
- 菜单收起不再留下看不见的点击陷阱。 WebView 渲染面积现在与菜单关闭同步收敛,不会再留下一块残余透明区域,拦截本该落到下层页面的点击。
SDK
- 异步文件表面的新导出类型:
FileOpEntry、FileOpAsyncOptions、FileDeleteAsyncOptions,以及四个新事件的载荷类型(FileOpProgressPayload、FileOpCompletePayload、MetadataProbeProgressPayload、MetadataProbeCompletePayload)与配套联合类型(FileOpKind、FileOpStatus、FileOpResultReason、MetadataProbeFailure、MetadataProbeInfoSource)。 dialog.openFile、saveFile与openFolder现在建档了各自的 resolve 形状({ canceled, filePaths }/{ canceled, filePath }/{ canceled, folderPath }),取消时路径字段为空。dnd.getPathsAsync改按共享的会话路径形状标注类型,使resolvedPaths对 TypeScript 可见。dnd.getResolvedPaths()以paths为基准补齐,因此早于该字段的宿主会返回长度正确的 null 数组,而不是一个会让按下标配对的循环静默错位的短数组。
文档
- 双语 API 参考里的每个参数表现在都带默认值列,通篇删除了只复读参数名的填充话术。
- 必填列逐条对照 C++ handler 的空值检查核验,双语共修正约 50 处标错的参数。
library.coverMaxSize的单位建档为 KB 而非字节。三张损坏的library返回字段表按宿主真实结构重建。修正了 12 处文档错误,含两处语义反转与若干失真描述。
v1.12.0 (2026-08-13)
本版的破坏性变更
以下四项可能需要修改代码:
- 拖放投放区注册表已移除。
dnd.registerDropZone/unregisterDropZone/getDropZones不复存在;宿主现在原生观测拖放,并向光标下的窗口发射dnd:enter/dnd:leave/dnd:drop,无需注册。真实路径改用fb.dnd.getPathsAsync()或dnd:drop载荷读取。 dnd.startDrag不再伪装成功。 拖出窗口需要组件未提供的原生IDropSource;调用现在 resolve{ success: false, code: 'NOT_SUPPORTED' },而不是虚报success: true。- 窗口尺寸约束改作用于调用方窗口。
window.setMinSize/getMinSize/setMaxSize/getMaxSize/setResizable/isResizable这六个端点不再回退到主窗口,解析不到目标时调用失败。依赖旧回退行为的 popup,实际约束的是主窗口。 DiscoveryContextMenuCommand不再是类型别名。 读取右键菜单命令的path/isDynamic/subGuid不再通过类型检查——这些字段从未被填充过。
拖放
- 拖放改为原生管线。 宿主通过原生
IDropTarget桥自行观测拖放手势,把 HTML5 刻意隐藏的真实文件系统路径交给页面。标准 HTML5 拖放事件照常触发,fb.dnd作为旁路通道与之并行。 - 新表面:
dnd.getPathsAsync(sessionId?)(在drop处理函数内可靠读取)、dnd.getPaths()/dnd.hasFiles()(同步快照读取,用于乐观 UI)、dnd.getCapabilities()(本窗口能否交付路径)。 - 事件
dnd:enter/dnd:leave/dnd:drop(载荷:sessionId、paths、x、y、keyState)与dnd:capabilitiesChanged,按sessionId关联同一次手势,点对点投递到光标下的窗口。 - 路径对不受信 origin 扣留,
hasFiles仍如实上报;DUI / CUI 面板(hosting: 'standard')拿不到路径——用getCapabilities()判断,而不是按窗口类型假设。 - 迁移:删除
registerDropZone/unregisterDropZone/getDropZones调用与zoneId簿记;保留(或补上)HTML5dragover/drop监听做视觉与命中判定;在drop处理函数内用await fb.dnd.getPathsAsync()读取真实路径;依赖路径的 UI 以dnd.getCapabilities()为准。
主菜单
menu.runMainMenuCommand不再透传宿主异常。 此前在汉化版 foobar2000 上,宿主异常会以宿主语言的原始Error逃逸到 JavaScript,并使名称与路径形式彻底失效。失败现统一以success: false加code返回:MENU_ITEM_DISABLED、MENU_MATCH_AMBIGUOUS(附candidates)或MENU_COMMAND_NOT_FOUND。menu.getMainMenu的叶子在汉化版宿主上可寻址了。 叶子现在按宿主渲染菜单所用的同一份文本匹配,并回填guid/subGuid;汉化版宿主实测由 158 个叶子中 0 个带 guid 提升到 167 个中 131 个。同时开始上报flags、enabled、checked、hidden——此前这些一个都没有。- 行为变更 —— 禁用命令会被拒绝执行,而不再报告成功。此前对灰显命令执行会返回
success: true,且同一条命令按名字被拒、按 GUID 却「成功」。三种请求形式现在校验一致,返回MENU_ITEM_DISABLED。不在枚举结果中的 GUID 仍会尝试执行——调用方可能持有本次枚举未覆盖的有效地址。 - 名称与路径按段精确匹配。名字有歧义时上报而不替调用方决定:汉化版宿主上可能有三条不同命令共用同一标签,取首个匹配会静默执行错误的命令。要无歧义请用
guid寻址——它是唯一跨宿主稳定的形式,因为汉化版上报的是本地化标签。 menu.runMainMenuCommand新增subGuid参数,与所属命令 GUID 搭配用于寻址动态子命令。- 无法解析出地址的叶子会明示这一点,带
executable: false与unaddressableReason,而不再伪装成一条调用方却无法执行的普通命令。扁平列举结果的available改为实读宿主状态,不再恒为true,禁用命令不会再与启用命令无从分辨。
自绘菜单
- 按调用指定菜单外观。
menu.show/menu.popup新增第三个MenuPopupOptions参数,调用方未传的键不会发送,宿主保留自身默认值:windowModel(默认'fullscreen',可选'contentSized'——把根菜单与一级子菜单绘制为按内容测量的独立紧凑窗口,每块面板各自承载真实 DWM 背景材质与系统窗口阴影;右键菜单推荐用此模型);css(至多 256 KiB)/cssReplace样式接管;backdrop(默认'acrylic',可选'mica'/'mica-alt'/'none')与backdropDarkMode(默认true);closeAnimationMs(默认0,钳制到0..1000)退出淡出时长。 - 富条目。
MenuPopupItem.type新增'nowplaying'、'rating'、'slider'、'segmented'及其配套字段(value、min/max/orientation、segments、cover/title/subtitle),任意行可用iconSvg内联单色图标。图标经运行时允许列表消毒;非法或超限的图标被丢弃,不会使该行失败。 - 新事件
menu:valueChanged以{ menuId, itemId, value }上报评分、滑杆或分段控件的变化并保持菜单开启;普通行仍经menu:select上报并关闭菜单。menu.popup只在选择或取消时 resolve,菜单含值控件时请单独订阅此事件。 - 修复 —— 失焦关闭现在可靠生效;
contentSized模型下池化子菜单窗口不再出现空白内容。
Discovery 菜单
- 行为变更 ——
discovery.searchCommands现在同时搜索右键菜单与主菜单,因此结果数增加,type出现新取值'contextmenu'。传{ scope: 'mainmenu' }可回到原有覆盖面。此前该端点只查主菜单,却把type: 'mainmenu'写进每条结果,导致右键命令搜不到。 - 行为变更 ——
discovery.searchCommands会过滤宿主不会显示的条目,与列举端点保持一致。传{ includeHidden: true }可取回完整集合。 - 行为变更 ——
discovery.getAllServices将右键菜单命令计入services.contextMenuCommands并纳入totalServices,因此totalServices数值变化。被过滤掉的数量由contextMenuHiddenFiltered给出;无选中且无播放曲目时stateKnown为false。 - 搜索结果携带与列举端点一致的状态字段(
enabled、checked、radioChecked、hidden、stateKnown、flags、source、executable、unaddressableReason),调用方无需再发一次请求即可判断能否执行。搜索包含右键菜单但无选中且无播放曲目时,响应的stateKnown为false,此时这些结果的enabled/checked不得用于过滤。 - 搜索的大小写折叠改为仅作用于 ASCII,不再可能破坏 UTF-8 序列。对中文标签的可观测匹配行为不变——中文没有可折叠的大小写。
discovery.getContextMenuTree不再静默截断。 此前子项截到 50、深度截到 10,却仍上报宿主的真实childCount,导致children.length与childCount不符且无任何说明。两个上限现为深度 16、每节点 512 子项,且任何裁剪都会上报:popup节点同时给出childCount与childrenReturned,子树被裁剪的节点带truncated,并由depthExceeded/childrenExceeded说明原因。标记向上传播,因此顶层truncated覆盖整棵树;maxDepth与maxChildrenPerNode回显生效上限。discovery.getContextMenuTree的节点现在上报状态——enabled、checked、radioChecked、hidden、stateKnown、flags——以及depth。分隔符只带类型,这也是对它唯一有意义的信息。discovery.executeContextMenuCommand在成功路径上也返回hidden与resolved,不再只在拒绝时返回,调用方因此能区分「未被拒绝」与「本版本不返回该字段」。无注册项拥有该 GUID 时resolved为false,此时没有可评估的状态。discovery.getMainMenuCommands中由动态子菜单展开的条目现在也带stateKnown、executable、unaddressableReason,与静态槽位一致。
SDK
- 破坏性类型修正 ——
DiscoveryContextMenuCommand原本是DiscoveryMainMenuCommand的类型别名,因而声明了右键侧根本不会返回的字段。现改为独立接口:右键条目是扁平注册、由宿主决定位置,因此没有菜单path,也没有动态展开相关字段。此前读取右键命令的path/isDynamic/subGuid的 TypeScript 代码,读的是从未被填充过的字段。 - 新增导出类型
MenuNodeState、MenuNodeSource、MenuUnaddressableReason,由所有菜单列举结果共用。 fb.discovery.searchCommands()接受{ scope, includeHidden };fb.discovery.getContextMenuCommands()接受{ includeHidden }。- 修复 SMP 主菜单派发。此前菜单 id 优先按
path解析,而路径匹配在汉化宿主上直接失败,导致主菜单的ExecuteByID在这类宿主上完全不工作。现改为优先guid(由宿主直接解析);右键菜单会话仍以commandId为准。 - 修复 SMP 菜单状态解码。此前
enabled/checked由原始flags位重新推导,忽略了宿主已给出的归一化布尔值。现优先使用布尔值,仅在其缺失时回退到flags。标记为stateKnown: false的条目按可用呈现而非置灰,因为「状态未知」与「可用且未勾选」在flags上无法区分,把未观测状态当作禁用会隐藏宿主本可执行的命令。
托盘与菜单
- 修复 —— 托盘菜单的「显示主窗口」不再依赖主页面。窗口一旦最小化或隐藏到托盘,页面会被深度挂起,任何
tray:menuItemClickedhandler 都跑不起来,也就无法调用window.focus把窗口找回来;左键单击图标同样无声。原生项(_sys_exit、playbackAction)全程正常。 - 行为变更 ——
showSystemItems(默认true)现在会在 bottom 分区的_sys_exit之前注入_sys_show(「显示主窗口」)。该项原生恢复并前置主窗口,保留其最大化 / 正常放置状态;与所有原生项一致,它不发tray:menuItemClicked。因此使用showSystemItems: true的菜单会多出一行;传showSystemItems: false可关闭该行为。 _sys_show与_sys_exit一同进入精确、大小写敏感的保留 id 允许列表:前端自绘「显示主窗口」行时可获得同样的原生路由并保留自己的label/icon,对应注入项自动跳过。形似 id(如_sys_show_alt、_SYS_SHOW)仍是普通用户项,且不会抑制注入。playbackAction仍拒绝'show-main-window'与'exit'——系统路由始终专属于保留 id。
窗口
- 行为变更 —— 六个尺寸约束端点(
window.setMinSize/getMinSize/setMaxSize/getMaxSize/setResizable/isResizable)现在作用于调用方窗口,或显式传入的windowId,不再回退到主窗口。此前 popup 设置自己的最小尺寸,实际约束的是主窗口。解析不到目标时调用失败,而不是返回另一个窗口的约束。DUI / CUI 面板调用方会被拒绝,并带panelMode: true。 - 文档修正 —— 这些端点的尺寸单位是物理像素,而非此前文档所写的 DIP。按设备像素比换算的调用方,在高 DPI 显示器上会把系数乘两次。经 setter 与 getter 往返的取值精确到 ±1px。
界面语言
- 插件原生界面改为跟随 foobar2000 的呈现语言,不再跟随 Windows 界面语言,因此汉化版 foobar2000 不会再显示英文的首选项页面。语言由宿主自身的字符串探测得出,探测不可行时回退到 Windows 界面语言。
- 新增界面语言首选项:自动(跟随 foobar2000)(默认)、English 或 中文。新打开的对话框立即生效;菜单标题与面板描述只向宿主注册一次,需重启 foobar2000 才会刷新。
播放列表
- 第三方组件的播放列表文件现在能正确展开。 通过 API 添加播放列表 URL 或文件时,此前用硬编码的 8 种包装扩展名(
.pls/.m3u/.m3u8/.asx/.wpl/.xspf/.fpl/.cue)判断;其他已安装组件注册的播放列表格式会被当作单条曲目。现在运行时查询宿主的 playlist_loader 注册表,宿主能加载的格式都会被展开。
性能与稳定性
- 修复 —— WebView2 浏览器进程死亡后,窗口不再一直空白。此前只有渲染进程有恢复路径,因此当外部原因(例如第三方钩子注入浏览器进程)导致其崩溃时,窗口会保持空白且无从自愈;实测有一次会话就这样运行了 10 小时。现在会重建窗口,并限制为 10 分钟内最多 3 次重建,使可复现的崩溃不会退化成「重建→崩溃→重建」死循环。窗口期过后计数重置,因此之后无关的失败仍可恢复。这与 v1.11.0 的渲染进程处理是两件事,后者负责重载页面与重建 WebView。
v1.11.0 (2026-07-27)
托盘与菜单
- 新增
TrayMenuItem.playbackAction('play-pause' | 'previous' | 'next' | 'stop'):自定义托盘项可声明由插件原生执行的播放动作。外观仍由调用方控制;声明项不发tray:menuItemClicked(同 Electronrole/ TauriPredefinedMenuItem)。仅可用于type:'normal'的叶子项;取值非法,或写在分隔符 / 子菜单 / 富控件上时,整次setContextMenu/appendMenuItems调用会以INVALID_PARAMS失败,而不是静默忽略。不接受'exit'。仅托盘菜单生效,对menu.show无效;getMenuItems会原样回读该字段。主页面深挂起(最小化 / 托盘隐藏 / 锁屏)时要后台可靠的托盘播放控制,请用本字段或内置showPlaybackControls;仅靠 click→playback.*不保证执行。自 v1.11.0 起可用;需兼容旧宿主时先用config.getVersionInfo().plugin.version探测。 - 文档澄清:
tray:menuItemClicked仅覆盖普通用户项与富值控件;内置播放 / 系统注入项与声明了playbackAction的项原生执行且不发点击事件,按钮状态请根据playback:*事件更新 menu.getMainMenu新增locale、i18n、withAvailability三个选项。locale默认'auto',保持宿主原生标签不翻译;i18n: false完全关闭displayLabel翻译;withAvailability默认true,附带各子菜单的命令可用性计数。SDK 侧签名相应变为getMainMenu(root?, opts?)。- 修复自定义菜单的 UTF-8 序列化与上下文模式选择。
DSP 与输出
dsp.*与output.*现在真正可用。 这 11 个处理器(dsp.getChain/getPresets/getAvailable/addDsp/removeDsp/moveDsp/applyPreset/setChain,output.getDevices/getEntries/getSettings)此前虽有文档,但其源文件从未被加入编译,所以任何调用都会因方法未注册而失败。自本版起已编译并注册。已发布的fb.dsp.*/fb.output.*SDK 封装本来就存在,配合本版插件后开始真正生效;插件版本低于本版时,无论 SDK 版本如何都会被拒绝。- 修复
output.getDevices崩溃。部分输出后端在回调里用「长度未知」哨兵值代替真实长度,原实现直接采用该值,导致读取远超字符串末尾并使 foobar2000 终止。 - 修复
dsp.moveDsp移动到错误位置。升序移动会少一格,因此向上移动一位等于无操作,且任何项都无法移到链尾;降序移动本来是正确的。返回的to现在是真实的最终索引。 - 行为变更 ——
dsp.setChain遇到任何不可用条目时整体拒绝,不再静默跳过。此前用三个条目构造的链可能只应用两个却仍返回success: true。每个条目级失败都返回带索引的原因:dsps[0] must be an object(元素不是对象,例如裸字符串或数字)、dsps[0]: guid is required(缺失、空串或不是字符串)、dsps[0]: Invalid GUID format: …、dsps[0]: DSP not found or no default preset: …(GUID 格式合法但该 DSP 未安装)。dsps本身缺失或不是数组时仍返回dsps array is required。任何调用被拒绝时链都保持不变。 dsp.getPresets在无选中预设时返回selectedIndex: -1。此前返回内部哨兵值18446744073709551615,该值无法用 JavaScript number 表示,到达前端时已是不可用的浮点数。dsp.getChain始终包含activePreset与activePresetIndex,无选中预设或宿主不支持预设时为null/-1。此前这两个键在上述情况下完全缺失,调用方不得不自行探测。
Discovery
discovery.getMainMenuCommands与discovery.searchCommands现在默认展开运行时构建子菜单的组件(mainmenu_commands_v2,例如 ESLyric)。这会改变既有返回结果:除父级槽位外还会含其子命令。传{ expandDynamic: false }可回到仅静态注册表的旧行为。- 命令条目新增
path、isDynamic、isDynamicParent、subGuid、flags字段;响应新增expandDynamic、dynamicCount,discovery.getAllServices新增mainMenuDynamicCommands。标记isDynamicParent的条目是容器槽位,自身不可执行。 discovery.executeMainMenuCommand新增第二个可选参数subGuid:执行由动态子菜单展开出的命令时必须一并传入,否则只会派发静态命令 GUID。
性能与稳定性
- 页面不可见期间(最小化、被遮挡、托盘隐藏或锁屏),高频可再生流在生产侧即停止:
audio:spectrum、playback:time、playback:timeHighRes不再产生,恢复可见后下一拍自然到达;window:hoverStateChanged与cursor:hiddenChanged在隐藏期间天然静默。其余事件一律可靠按序投递,不丢弃也不合并,包括http:response、library:getAllResult、audio:fullWaveformReady等异步应答与playback:itemPlayed等逐次事实。绘制频谱或播放进度的主题应把数据中断理解为「页面隐藏」而非「播放停止」。 - 最小化 / 被遮挡 / 锁屏 / 托盘隐藏时新增深度挂起(
TrySuspend),冻结渲染进程计时器与动画以便系统回收内存。新增 foobar2000 高级设置项Deep-suspend WebView when hidden (TrySuspend; frees renderer memory)(默认开启,关闭后退回原 Low 内存路径)。 - 新增高级设置项
Keep WebView active in background while CDP remote debugging is on (tray/minimize/lock)(默认开启):开启 CDP 远程调试时保持 WebView 后台活跃,以保证截图与时序类自动化工具稳定。 - 收敛 WebView2 崩溃后的僵尸态:进程失败后按重载次数上限决定重载或重建,不再停留在无响应页面。
- 改进封面获取:修正了换曲或改标签后可能返回上一首封面的缓存问题,并把解码移出 UI 线程、改为逐个排队处理,大尺寸封面不再造成界面卡顿。
artwork.*的请求与响应结构未变。
元数据
- 修复
metadata.read、metadata.readByPath、metadata.readBatch忽略容器内轨道编号的问题。路径里的|subsong:N后缀既没有被剥离也没有被采用,因此从 CUE、ISO 镜像或多轨文件里读取单条轨道时,要么直接失败,要么返回首轨的标签。这正是这类文件在 foobar2000 界面里能读、经 API 读不到的原因。metadata.readRaw原本就是正确的。 metadata.read与metadata.readByPath新增cueIndex参数,可显式指定轨道,与metadata.readRaw一致;其优先级高于路径中的|subsong:N后缀。SDK 的fb.metadata.read()/readByPath()通过第二个opts参数传入,MCP 的fb2k_metadata_read/fb2k_metadata_read_by_path已声明该参数。metadata.readBatch不接受此参数,批量读取请在各自路径上使用|subsong:N后缀。
SDK
- 新增仅 SDK 层的 additive 二进制适配 helper:
fb.file.readBinary()、fb.file.writeBinary()、fb.file.writeDataUrl()、fb.metadata.embedArtworkBytes()、fb.metadata.embedArtworkFromDataUrl(),以及公开类型FileBinaryWriteOptions、MetadataArtworkBytesOptions。 - 这些 helper 只把
ArrayBuffer/Uint8Array与严格 Base64 Data URL 适配到既有file.read、file.write、metadata.embedArtworkwire 契约;不新增 Bridge endpoint,不改变 rawinvoke或旧 facade。canonical Base64 与 Data URL 校验发生在 SDK 调用 Host 之前,Host 自身的校验和行为没有变化。 - 修正两处错误的 TypeScript 返回类型声明,使其与宿主实际 wire 契约一致:
ui.isMinimized()为{ minimized }(不存在isMinimized别名)、ui.isAlwaysOnTop()为{ enabled, isAlwaysOnTop }(两个键同值)。按旧声明编写的 TypeScript 代码需要相应调整。 fb.playcount.set()不再发送宿主从未读取的count键;window.getBackdropPolicy/window.setBackdropPolicy补齐windowId参数类型声明。fb.http.request()现在经由文档声明的http.get端点派发;此前一个失效的内部参数可能转发错配的方法名。动词 helper(fb.http.post()/put()/delete()/patch())仍各自派发到自己的端点,二进制响应也一样。- 为五个 wrapper 补上可选的末位
opts参数,对应的键宿主 handler 早已读取:fb.file.delete(path, opts?)(moveToTrash)、fb.file.copy(source, destination, opts?)(overwrite),以及fb.metadata.write(path, tags, opts?)/removeField(path, field, opts?)/removeTag(path, tags, opts?)(cueIndex)。其中metadata.write支持cueIndex补上了一处真实缺口:v1.11.0 只把cueIndex接进了元数据读取侧,因此此前无法通过 SDK 向 CUE 或镜像文件中的单曲写标签。所有新增参数均为可选,既有调用点不受影响。 fb.metadata.readByPath()的返回类型由裸JsonObject收窄为MetadataReadByPathResponse。- 修正两处与宿主契约不符的公开类型声明:
dsp.setChain的dsps是必填的{ guid }对象数组(原声明为dsps?: string[],在可选性、元素类型、结构三处均错——宿主要求dsps必须是数组,并逐项读取guid);playlist:created/playlist:renamed的 payload 保持name: string。按旧dsp.setChain声明编写的 TypeScript 代码需要相应调整。
v1.10.0 (2026-07-16)
- 新增:
TrayMenuItem.orientation(仅type:'slider','horizontal' | 'vertical',默认水平)。仅精确vertical为纵向(min 底 / max 顶;Up/Right 增、Down/Left 减、Home/End 边界);native忽略并保持分级子菜单;旧 runtime 忽略未知字段保持水平。范围规范化:max<min交换、max==min常量不发 value、初始 value clamp、IPC 越界拒绝。getMenuItemsround-trip。需要纵向时先用config.getVersionInfo().plugin.version探测运行时是否为 1.10.0 及以上 - 变更:自绘菜单两态焦点(导航 roving tabindex + 真实 focus / 富控件编辑态)与 ARIA(
menuitem/menuitemcheckbox/ 内部role=slider/ segmentedradiogroup);checked:false仍为 checkable;默认入退场在prefers-reduced-motion: reduce下禁用 transform/transition(不改 hide protocol /closeAnimationMs) - 新增:
TrayMenuConfig.layoutMode('flat' | 'zones')。默认'flat'保留#menu > .fb-item直接子 DOM;显式'zones'时为非空 top / playback / bottom 生成.fb-zone[data-zone]。native忽略;旧 runtime 忽略未知字段但不生成 wrapper;menu.show不受影响。需要 zones 的主题应先用config.getVersionInfo().plugin.version探测运行时是否为 1.10.0 及以上 - 变更:自绘菜单受保护 CSS 不再强制可见态
#menu { display:block !important };主题可直接把根菜单 / zone 设为 flex 或 grid,但无法用display:* !important重新显示已隐藏菜单 - 安全:自绘菜单 SVG 图标改为 DOMParser + 元素/属性白名单克隆,移除 raw
innerHTML注入;非法或超限单图标被丢弃,菜单继续显示;transform严格解析(拒绝前缀/函数间杂质与空参),节点必须位于 SVG namespace - 安全:
tray.setContextMenu/tray.appendMenuItems/menu.show在写入持久配置或打开 overlay 前做资源上限事务性校验(item ≤ 512、menu.show深度 ≤ 8、segmented options ≤ 64、CSS ≤ 256 KiB、SVG 总量 ≤ 256 KiB);单图 SVG > 32 KiB 仅丢弃该图标。超限返回INVALID_PARAMS且details含field/limit/actual;非法 / 超限输入属有意不兼容 - 安全加固:托盘 / 自绘菜单的内置动作改按可信内部来源路由,不再凭 public id 前缀。唯一兼容例外是 tray API 中精确、大小写敏感的
_sys_exit(保持 1.9.0 的真实退出行为);调用方提供的_pb_playPause/_pb_prev/_pb_next/_pb_stop仍是普通用户项,且不能通过同名去重抑制 runtime 自动注入的 trusted 播放项;重复 public ID 由 opaque token 区分,公共menu.show不提权。每次选择 / 值变更由不可预测的单次 token 承载并按本次菜单索引校验,未知 / 过期 token、禁用项、越界富值(rating / slider / segmented)一律拒绝。内部menu.__*IPC 另校验调用方是否为 overlay 自身窗口,且 select / dismiss / ready / submenuPanel / valueChanged 须匹配当前菜单 id;外部调用方或过期 / 伪造的 menuId 一律拒绝且不改变菜单状态 - 自绘 tray
ContentSized现在在字体就绪后离屏测量 root 与全部一级 submenu,并等待连续两帧尺寸稳定;C++ 使用 64 位安全槽位分配。固定 HWND 的 region 只覆盖当前可见 root/submenu panel,因此未展开预留区不再形成空白 acrylic/mica 面板,且调用方配置的 backdrop 不会被静默关闭 - 托盘自绘菜单
segmented分段控件的值变更明确纳入“保持菜单打开”契约:一次分段切换经tray:menuItemClicked回传{ id, value }(value= 被选中分段的从 0 起索引)且不关闭菜单,与rating/slider一致。webview运行时一直保持菜单打开,本次仅修正此前只列rating/slider的共享契约与事件文档 - 托盘菜单全隐藏(或空)的分区不再产生多余 separator:此前分区内所有项
visible:false时,隐藏项被过滤后会残留一条首/尾分隔线;现在可见性过滤先于 separator 判定,native 与 webview 菜单均不再出现该多余分隔线(默认行为修正) - 文档修正:
TrayMenuItem.icon(base64 ICO)为保留字段,当前两种后端均不渲染(native纯文本、webview 绘制iconSvg);菜单项图标请改用iconSvg
v1.9.0 (2026-06-18)
- 托盘自绘菜单(
render: 'webview')普通/子菜单项支持图标:新增TrayMenuItem.iconSvg = { viewBox, content },内联单色 SVG、currentColor跟随菜单文字色、左对齐固定 8px 间距;同层有图标时所有普通/子菜单项预留 16px 图标列以对齐文字(native菜单忽略) tray.setContextMenu新增config.autoNowPlaying:开启后nowplaying项的空字段(cover/title/subtitle)在右键弹出时由后端用当前曲目自动补全(前端传了就用前端值,前端优先);cover补全仅webview,取当前曲目封面并缩略为缩略图,title/subtitle 走%title%(自动回退文件名)/%artist%,兼容流媒体动态标题TrayMenuItem.cover现额外支持http(s)://URL(除既有data:与裸 base64 外),便于流媒体前端直传实时解析的封面- SDK 安装包同步到
1.9.0
v1.8.0 (2026-06-10)
- 新增自绘菜单能力:
menu.show/menu.close以 WebView 渲染菜单内容,支持子菜单递归渲染;菜单窗口采用内容尺寸固定窗策略,消除展开时的闪烁 tray.*新增render: 'webview'模式,托盘右键菜单可改用自绘菜单渲染,外观与主题保持统一- 新增
tray.setMenuItemState,可在不重建整个菜单的情况下更新单个菜单项状态 - 修复桌面歌词等置顶弹窗点击后,主窗口偶发被拉至前台并意外置顶的问题(撤销路径 z-order 插入误入 topmost 段 + sink 还原参照反馈环)
- 修复 SDK 发布物缺失
HTMLElementTagNameMap全局声明的问题,npm 用户恢复fb-*自定义元素的类型提示 - 修复打包脚本在新版 PowerShell 下无法生成
.fb2k-component的兼容性问题 - HttpApi 异步请求异常边界加固;修复 LibraryApi 一处 NUL 字符串处理缺陷
- SDK 安装包同步到
1.8.0;bump-version.ps1版本同步覆盖面扩展到sdk/package-lock.json与 VitePress 导航栏版本号
v1.7.0 (2026-06-06)
- 新增 Taskbar & Tray 能力:
taskbar.*可设置任务栏缩略图按钮、进度条、叠加图标和闪烁提示;tray.*可创建系统托盘图标、气泡通知和右键菜单 tray.*新增增量菜单管理:appendMenuItems/removeMenuItems/clearMenuItems/getMenuItems,可以按top/playback/bottom分区动态维护托盘菜单,不必每次重建完整菜单- 新增
taskbar:buttonClicked、tray:click、tray:doubleClick、tray:menuItemClicked、tray:beforeContextMenu事件,主题可以响应任务栏缩略图按钮和托盘交互 - 新增
webview:processFailed事件,WebView2 渲染进程异常时会广播诊断信息,并配合渲染进程自动恢复路径降低空白窗口风险 - 新增高精度播放位置事件
playback:timeHighRes,由独立 WinAPI 定时器驱动,适合歌词、进度条等需要亚秒级刷新的界面 library.getAll冷缓存全量序列化改为后台线程执行;SDK 会等待library:getAllResult并按requestId关联结果,避免大媒体库查询卡住 UI- 修复托盘隐藏后窗口恢复路径,补全
window.focus/ hidden restore 场景下的 WebView surface 恢复,降低最小化、托盘隐藏、Alt+Tab 切回后的空白风险 - 修复任务栏缩略图 pause 图标 base64 损坏和 HICON 所有权问题,避免播放按钮显示异常和 explorer.exe 崩溃
- SDK 安装包同步到
1.7.0,可直接使用新的 Taskbar & Tray 类型和事件声明 - VitePress 文档新增 Taskbar & Tray API 页面,并同步 Cursor、Playback 高频事件和相关示例
v1.6.1 (2026-05-20)
- 新增
cursor.*命名空间:cursor.setHidden(hidden)/cursor.isHidden()显式控制客户区光标可见性;解决 Visual Hosting 模式下 CSScursor: none不可靠的问题 - 新增
cursor:hiddenChanged事件,每窗口独立派发 fb.http.*新增insecureTls参数(双层门禁)— 全局开关Allow self-signed / invalid TLS certificatesON + 每请求insecureTls: true才生效,访问自签证书的内网服务(Plex / Jellyfin / Lidarr 等)不再被强制拦截fb.http.*新增responseType: 'arraybuffer' | 'binary',body 自动 base64 解码为ArrayBuffer,封面 / 字体等二进制资源不再因 UTF-8 严格校验失败- VitePress 文档同步上述变化(cursor.md / http.md / events.md)
v1.6.0 (2026-05-11)
playlist.getAll不再返回duration字段,避免为了时长把整批轨道都读一遍;playlist.getActive/playlist.getPlaying还是会返回http.get/http.post/http.head默认改成异步;如果你就是要同步调用,需要显式传async: false
v1.1.17 (2026-02-06)
- 可以真正开多个窗口了
- 新增
window.createPopup/closePopup/closeAllPopups/getAllWindows - 新增
window.sendMessage/window.broadcast,窗口之间可以互相发消息 - 支持异步关闭、无标题栏和透明背景