文件与媒体库能力补全规格
定义目录选择、虚拟 scope、递归枚举、媒体元数据与外部工具检测。
定义目录与多文件选择、持久目录 scope、递归枚举、媒体元数据和外部转码工具检测的目标边界。
| 状态项 | 说明 |
|---|---|
当前 26.11.5 | 当前 26.11.5 基线只有单文件 dialog.openFile、dialog.saveFile 和固定虚拟根下的 FS 命令;没有目录选择、多选、持久目录 scope、递归枚举、媒体元数据或 FFmpeg 检测。当前事件只有 webview.navigated、webview.reloaded、window.resized。 |
目标规格(未发布) | 规格 / 待实现:新增 dialog.openFolder/openFiles、fs.walk、fs.scope.release、media.probe、media.ffmpeg.detect/validate/setPath/pickExecutable;目录授权统一映射为 app://scope/<id>/...。 |
实施阶段 | P1;依赖 P0 的扩展清单、权限、错误码和生成契约。 |
验收标准 | Windows 与 Android 都能返回虚拟目录或多文件句柄、递归枚举、读取元数据、检测转码工具并释放 scope;真实路径与 content:// URI 不进入前端。 |
本页只描述通用文件选择、虚拟 scope 和媒体探测。媒体扫描编排、缓存表、数据库记录和封面业务策略留在应用扩展层,组合 FS、SQL、Store、HTTP 与目标媒体能力完成。
| 层级 | 真实入口 | 责任 |
|---|---|---|
| 协议 | protocol/commands.json、protocol/permissions.schema.json、protocol/plugins.json | 目标命令、权限、插件归属和平台矩阵。 |
| 前端 | frontend/src/api/dialog.ts、frontend/src/api/fs.ts、frontend/src/bridge/ksm.ts | 参数守卫、虚拟路径校验、大结果 { file } 读取。 |
| Windows | windows/src/plugins/ksm_dialog_plugin.cpp、ksm_filesystem_plugin.cpp、windows/src/platform/ksm_virtual_path.cpp、ksm_file_metadata.h | 文件夹与多文件选择器、递归枚举、元数据读取、FFmpeg 可执行文件检测。 |
| Android | KSMDialogPlugin.kt、KSMFileSystemPlugin.kt、KSMVirtualPath.kt、KSMHostServices.kt | SAF 目录与多文件选择、持久授权、虚拟目录映射、元数据与媒体库探测。 |
| 依赖 | windows/vcpkg.json、android/kasumi/build.gradle.kts | FFmpeg、TagLib 或等价元数据库以及 Android documentfile;版本只固定在版本清单。 |
| 能力 | 当前 26.11.5 基线 | 目标规格 / 待实现 |
|---|---|---|
| 目录选择 | 无 dialog.openFolder。 | 返回 { scopeId, path },path 为 app://scope/<id>/...。 |
| 多文件选择 | dialog.openFile 只返回单个 picked 文件句柄。 | dialog.openFiles 返回 { files: string[] },数组元素为虚拟文件句柄。 |
| 持久目录 | picked token 只覆盖单次文件选择。 | app://scope/<id>/... 保存可撤销的目录授权;fs.scope.release 显式释放。 |
| 递归枚举 | fs.readDir 只列举单层目录。 | fs.walk 使用分页或大结果 { file },不把大列表塞入单条消息。 |
| 媒体元数据 | fs.stat 只有通用文件元数据。 | media.probe 返回标题、艺术家、专辑、时长、封面和歌词的虚拟路径;不可解码返回 MEDIA_UNSUPPORTED。 |
| 转码工具 | 没有 FFmpeg 检测或配置命令。 | media.ffmpeg.detect/validate/setPath/pickExecutable 只返回可用性、版本与虚拟可执行文件句柄。 |
| 文件上限 | 单文件上限为 16 MiB,单条消息上限为 64 KiB。 | 保留上限;目录枚举、封面和歌词大结果使用 { file }。 |
| 目标命令 | 参数与返回边界 |
|---|---|
dialog.openFolder | { filters? } → { scopeId, path } 或 { scopeId: null, path: null }。 |
dialog.openFiles | { filters?, multiple: true } → { files }。 |
fs.walk | { path, recursive?, cursor?, limit? } → { entries, nextCursor } 或 { file }。 |
fs.scope.release | { scopeId } → { released };重复释放幂等。 |
media.probe | { path } → { title?, artist?, album?, durationMs?, cover?, lyric?, format? }。 |
media.ffmpeg.* | 检测默认路径、验证可执行文件、设置虚拟路径、通过系统选择器挑选可执行文件;不返回真实路径。 |
真实入口是 kasumi-bridge 的 KSM 类、frontend/src/api/dialog.ts 和 frontend/src/api/fs.ts。protocol/kasumi.config.schema.json 与 packages/kasumi-cli/src/config.mjs 校验命令白名单;windows/src/platform/ksm_virtual_path.cpp 和 android/kasumi/src/main/java/dev/kasumi/app/bridge/KSMVirtualPath.kt 负责虚拟路径映射。
以下示例描述目标接口,尚未在 26.11.5 发布:
import { KSM, KSMWebMessageTransport } from 'kasumi-bridge';
KSM.initialize(new KSMWebMessageTransport());
const capabilities = await KSM.capabilities();
const permission = await KSM.permission.request('fs.read');
if (!permission.granted || !capabilities.commands.includes('dialog.openFolder')) {
throw new Error('The folder scope is unavailable.');
}
const folder = await KSM.invoke<{
scopeId: string;
path: string;
}, { filters?: string[] }>('dialog.openFolder', {
filters: ['audio/*']
});
const walk = await KSM.invoke<{
entries: Array<{ path: string; isDirectory: boolean }>;
nextCursor: string | null;
} | { file: string }, {
path: 'app://scope/library-01/audio';
recursive: true;
limit: 200;
}>('fs.walk', {
path: folder.path,
recursive: true,
limit: 200
});
const metadata = await KSM.invoke<{
title?: string;
durationMs?: number;
cover?: string;
}, { path: string }>('media.probe', {
path: 'app://scope/library-01/audio/track.flac'
});
await KSM.invoke('fs.scope.release', { scopeId: folder.scopeId });app://scope/<id>/... 是目录授权的虚拟根。前端不能拼接真实路径,也不能从 scope 字符串反推系统目录。大枚举结果通过 { file } 返回,再用 KSM.fs.readText() 读取。
| 平台 | 实现边界 | 行为 |
|---|---|---|
| Windows | windows/src/plugins/ksm_dialog_plugin.cpp、ksm_filesystem_plugin.cpp、windows/src/platform/ksm_virtual_path.cpp、ksm_file_metadata.h | 文件夹选择器生成 scope ID 并映射到宿主内部目录;多选返回 picked 句柄数组;递归枚举按 cursor 分页;TagLib 或等价库读取元数据;FFmpeg 可执行文件只以虚拟句柄和版本信息暴露。 |
| Android | KSMDialogPlugin.kt、KSMFileSystemPlugin.kt、KSMVirtualPath.kt、KSMHostServices.kt | ACTION_OPEN_DOCUMENT_TREE 取得目录 URI 后调用 takePersistableUriPermission,内部使用 DocumentFile 与 ContentResolver 枚举;ACTION_OPEN_DOCUMENT 支持 EXTRA_ALLOW_MULTIPLE;releasePersistableUriPermission 在 fs.scope.release 或授权失效时释放。 |
SAF 生命周期由宿主服务持有。用户撤销授权、Activity 销毁或应用数据清理时,scope 标记为失效;不向前端返回 content:// URI。FFT 与元数据读取在后台线程执行,结果回到 WebView 所属线程。
content:// URI、FFmpeg 可执行文件的真实位置或目录遍历符号链接目标;所有路径使用 app:// 虚拟根。addJavascriptInterface,不直接调用宿主私有文件接口,不在前端按平台写分支。{ file } 或以 base64 以外的方式传递。media.ffmpeg.setPath 指向未验证的二进制。fs.walk 中跟随越界链接、循环目录或 ..、~、绝对路径;拒绝危险输入后再做规范化与前缀校验。CANCELLED;选择器返回空结果,超时或窗口销毁才使用生命周期错误码。| 错误码 | 触发条件 | 期望行为 |
|---|---|---|
INVALID_ARGS | 虚拟路径、cursor、limit、过滤器或 scope ID 非法。 | 前端守卫与宿主校验同时拒绝。 |
PERMISSION_DENIED | 命令未授权、scope 越界、只读根写入或 SAF 授权撤销。 | 原生层拒绝并保留已有状态。 |
NOT_FOUND | scope、文件或可执行文件不存在。 | 返回明确错误,不创建空句柄。 |
NOT_SUPPORTED | 平台未实现文件选择、元数据或 FFmpeg 能力。 | 能力查询排除该命令。 |
IO_ERROR | 枚举、读取、元数据解析、磁盘或 ContentResolver 失败。 | 清理临时句柄并返回可操作诊断。 |
CANCELLED / TIMEOUT | 选择器超时、窗口销毁或请求取消。 | 丢弃迟到结果与临时授权。 |
BUSY / RESOURCE_LIMIT | 同一窗口已有选择器、枚举分页或文件超过上限。 | 保持当前任务或返回分页 cursor。 |
MEDIA_UNSUPPORTED | 媒体格式、标签或封面无法解析。 | 返回结构化原因,不伪造空元数据。 |
AUDIO_DEVICE_BUSY | 目标规格保留给音频设备;媒体库命令不得用其代替 IO 错误。 | 返回 IO_ERROR 或 MEDIA_UNSUPPORTED。 |
| 现象 | 优先检查 |
|---|---|
| 目录 scope 重启后失效 | SAF 持久授权标志、scope ID 持久化、Windows 授权映射和 fs.scope.release 调用时机。 |
fs.walk 结果被截断 | cursor、limit、大结果 { file }、64 KiB 消息边界和读取权限。 |
| 多文件只返回一个结果 | EXTRA_ALLOW_MULTIPLE、Windows 多选对话框标志、返回数组形状。 |
media.probe 返回空标签 | 文件格式、元数据库版本、嵌入式封面与歌词虚拟路径、文件上限。 |
| FFmpeg 检测成功但验证失败 | 可执行文件版本输出、签名或来源策略、虚拟句柄是否仍有效。 |
| Android 枚举权限突然失效 | 用户撤销授权、URI 权限是否 persistable、scope 表是否按 windowId 隔离。 |
选择器取消后前端报 CANCELLED | 空结果与生命周期取消是否被错误混用。 |
pnpm validate:protocol、pnpm codegen、pnpm typecheck、pnpm test:contract、pnpm test:packages 通过;生成契约、权限和平台矩阵无漂移。content:// URI、越界链接和超过 64 KiB 的 JSON 载荷均不进入前端;不使用 addJavascriptInterface,不引入 Rust/Node 中间 runtime,不写前端平台分支。
评论