chore: 三包一起分发、桌面应用打包脚本与 CI,README 对齐实现
* pyproject:wheel 打三个包(sqlite_vfs + svfs_studio + svfs_client)——两个桌面 应用的 console script 都声明在同一个发行版里,只打核心包会让 pip 装完 `svfs-client` 命令存在而模块缺失(跑起来是 ImportError);extras 拆成 client(完整 PySide6,WebEngine 必需)/ studio(PySide6-Essentials 够用)/ gui(两者合集,保留旧写法)。实测重建 wheel 三个包齐全、纯标准库层在无 PySide6 的环境里可导入; * packaging/:PyInstaller spec(白名单裁剪 Qt 体积,DevTools/QML/opengl32sw 的 裁剪依据都记在注释里)与 build.py 编排;Nuitka 路线的已知问题(编译后 Shiboken 无法导入)写在脚本注释里,推荐走 PyInstaller; * .github/workflows/tests.yml:windows + ubuntu 双跑。文件头注明了两个尚未在真 runner 上验证过的点——依赖索引(pyproject 默认指清华镜像,CI 里用 UV_DEFAULT_INDEX 覆盖回 PyPI)与 Linux 上 Qt 需要的系统库; * README:按实现重新对齐——登录与认证(凭据存储、会话失效反馈、离线宽限)、 可靠性与诊断(客户端自更新、网络超时/重试/代理/CA、日志落盘)、plugin.json 的 net_hosts、数据目录(credentials.dat / logs)、桥 API、安装方式与 extras。 uv.lock 同步(dev 组与 extras)。
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
- 增量打包: 只写入变化的条目,未变的文件不重写、不重压、不重复存储
|
||||
- 可发布到对象存储: 导出成「按 sha256 命名的对象 + 一份小清单」,普通静态托管 / CDN 即可分发,
|
||||
客户端按清单取回并逐个校验(不需要任何编译扩展)
|
||||
- 插件化客户端: `svfs-client`(PySide6 + WebView)把 .svfs 当插件分发——完整前端直接
|
||||
从包内加载进 WebView,多插件互相隔离,升级即新快照、可随时回滚
|
||||
- 完整元数据: 保留文件权限、创建时间、修改时间等元数据
|
||||
- 目录结构保持: 完整的目录层级结构保持
|
||||
- 简单易用: 四个命令行工具(打包 / 解包 / 校验 / 迁移),零第三方依赖
|
||||
@@ -42,6 +44,42 @@ uv sync
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 命令行入口:`svfs <子命令>`(推荐)
|
||||
|
||||
所有功能收拢在一个入口下,git 风格的子命令;退出码统一为 0 成功 / 1 失败 / 2 用法错误:
|
||||
|
||||
```bash
|
||||
svfs pack <源文件夹> <输出.svfs> # 打包(--compress 压缩;--incremental 增量;
|
||||
# --snapshot NAME 打成新快照)
|
||||
svfs unpack <包> <目标目录> # 解包(--snapshot ID 选版本;--path 只导出一个路径)
|
||||
svfs info <包> # 概要:统计、去重/压缩收益、快照列表(--json)
|
||||
svfs verify <包> # 校验(--check-content 逐文件重算哈希;--key 验签)
|
||||
svfs sign <包> --key <密钥> # 签名(--key-id 起标识;--snapshot ID 选版本)
|
||||
svfs migrate <旧包> # v1/v2 → v3(默认先留 .bak 备份)
|
||||
svfs publish <包> <输出目录> # 发布成对象 + manifest.json
|
||||
svfs fetch <manifest> <目标目录> # 按清单取回(对象按 sha256 校验)
|
||||
|
||||
svfs --help # 总览
|
||||
svfs pack --help # 每个子命令的完整参数
|
||||
```
|
||||
|
||||
典型的发布/取回链路:
|
||||
|
||||
```bash
|
||||
svfs pack ./dist app.svfs --compress
|
||||
svfs sign app.svfs --key "$SVFS_KEY" --key-id release-1
|
||||
svfs verify app.svfs --key "$SVFS_KEY" --check-content
|
||||
svfs publish app.svfs public/
|
||||
aws s3 sync public/ s3://你的桶/ --cache-control "public, max-age=31536000, immutable"
|
||||
svfs fetch https://cdn.example.com/app/manifest.json ./restored
|
||||
```
|
||||
|
||||
> 兼容性说明:早期的六个独立命令 `vfspack` / `vfsunpack` / `vfsverify` / `vfsmigrate` /
|
||||
> `vfspublish` / `vfsfetch` 全部保留为别名——它们与 `svfs` 的同名子命令跑的是
|
||||
> **同一段实现**,输出与退出码一致,已有脚本不必迁移。区别只有一点:
|
||||
> `vfsverify --sign` 的功能在 `svfs` 里拆成了独立的 `svfs sign` 子命令。
|
||||
|
||||
### 打包文件夹
|
||||
```bash
|
||||
# 基本用法
|
||||
@@ -187,13 +225,392 @@ zlib 把体积压到 1/8.7,而**读写延迟基本不变**——解压是 C
|
||||
```
|
||||
|
||||
```bash
|
||||
vfsmigrate ./old.db # 原地迁移,先复制一份 ./old.db.bak
|
||||
vfsmigrate ./old.db --output ./new.db # 输出到新文件,原库一个字节都不动
|
||||
svfs migrate ./old.db # 原地迁移,先复制一份 ./old.db.bak
|
||||
svfs migrate ./old.db --output ./new.db # 输出到新文件,原库一个字节都不动
|
||||
```
|
||||
|
||||
迁移会读旧库、在旁边的临时文件上重建、校验通过后原子替换,所以中途失败不会留下半成品。
|
||||
迁移过程顺带按 sha256 去重:历史库里同一份内容存了多次会被合并(`vfsmigrate` 会报告省下多少字节)。
|
||||
迁移完可以用 `vfsverify --check-content` 复核每一个文件的内容。
|
||||
迁移过程顺带按 sha256 去重:历史库里同一份内容存了多次会被合并(`svfs migrate` 会报告省下多少字节)。
|
||||
迁移完可以用 `svfs verify --check-content` 复核每一个文件的内容。
|
||||
|
||||
## 桌面工具:SVFS Studio
|
||||
|
||||
`svfs-studio` 是配套的可视化工具(PySide6,暗色界面),把一个 `.svfs` 摊开来看:
|
||||
文件树与内容预览、版本快照与两版差异、内容去重与压缩的实际收益、完整性校验与签名,
|
||||
以及它作为一个 SQLite 数据库的内部结构(文件头、页、各表占用)和一个**只读** SQL 控制台。
|
||||
|
||||
它做的一切都调用本库的公开 API(`FolderPacker`、`migration.migrate`、`publish.export_objects`…),
|
||||
没有第二份实现,所以界面上得到的结果与 CLI 逐字节一致。
|
||||
|
||||
```bash
|
||||
# 安装(GUI 依赖在可选 extra 里,核心库与 CLI 依然零第三方依赖)
|
||||
uv sync --extra gui
|
||||
# 或:pip install "sqlite-vfs[gui]"
|
||||
|
||||
# 启动
|
||||
uv run svfs-studio
|
||||
# 或:python -m svfs_studio
|
||||
```
|
||||
|
||||
工具里能做的不只是"看":打包(真实逐文件进度,因为 `pack_folder` 现在支持进度回调)、
|
||||
**包内编辑**、解包、迁移旧格式、回收未引用内容、发布成对象存储布局、从清单拉取、
|
||||
与本地目录逐文件对比——都跑在后台线程上,界面不卡。
|
||||
|
||||
**包内编辑**(「文件」页的编辑那一排,或右键菜单):
|
||||
|
||||
* **添加文件 / 添加文件夹**:加进当前快照;文件夹整棵进去。同名路径会先问一句,
|
||||
选"替换"就覆盖(内容仍按 sha256 去重,重复字节不会多占空间);
|
||||
* **新建目录**:在选中的目录下建(没选中就是根目录);
|
||||
* **重命名**:改名或移动文件/目录(目录连同子项一起改);
|
||||
* **删除**:删掉文件或整个子树。库里是内容寻址 + 引用计数,所以删路径不等于删内容——
|
||||
只有一条引用都不剩时,那份内容才会被回收(「存储」页能看到可回收多少);
|
||||
* **编辑内容**:选中文本文件后点「编辑内容」(或直接改完按 Ctrl+S),就地改完写回包内。
|
||||
按 UTF-8/GBK 嗅探编码并以原编码保存;上限 1 MB——编辑器装不下的文件不给编辑,
|
||||
因为"只看到一半就保存"等于截断。二进制文件与超限文件请导出改好后用「添加文件」替换。
|
||||
|
||||
每次编辑结束都会调一次 `recount()`:刷新快照的统计与摘要,保证改完立刻能校验通过。
|
||||
代价是**之前做的签名会失效**(签名只覆盖摘要),界面上会明确提示这件事——
|
||||
这是"摘要跟着内容走"与"签名绑定摘要"这套设计的必然结果,不是 bug。
|
||||
|
||||
几个值得先试的地方:
|
||||
|
||||
* 没有现成的包?点「打个示例包看看」,会在临时目录里造一棵含两份相同内容的小目录树并打包;
|
||||
* 仓库自带的 `appcation.svfs` 是 schema v1,打开它会看到「旧格式」横幅和「去迁移」按钮,
|
||||
正好演示从旧格式升到 v3 的完整流程;
|
||||
* 「存储」页把去重与压缩分开算:`logical_size → unique_bytes → stored_bytes` 三步各省了多少一目了然;
|
||||
* 「内部」页可以直接 `SELECT`——包就是个普通 SQLite 库,这点骗不了人。
|
||||
|
||||
想要一份界面截图用于文档或评审(不需要显示器):
|
||||
|
||||
```bash
|
||||
python -m svfs_studio --shots ./shots ./某个包.svfs # 每个标签页各存一张 PNG
|
||||
```
|
||||
|
||||
界面测试(offscreen 渲染,含像素级暗色检查、预览内容与库字节比对、中断语义):
|
||||
|
||||
```bash
|
||||
python -m unittest discover -s tests -p "test_studio_ui.py" -v
|
||||
```
|
||||
|
||||
## 插件化客户端:svfs-client
|
||||
|
||||
`svfs-client` 是基于同一套 SVFS 格式的**插件化桌面客户端**(PySide6 + QWebEngineView):
|
||||
插件以 `.svfs` 单文件分发,客户端安装插件、把插件的 Web 界面直接从包内加载进
|
||||
WebView——不落盘、不起端口、不需要解包。
|
||||
|
||||
核心模型:
|
||||
|
||||
* **一个插件 = 一个完整的 .svfs 包**:完整前端(HTML/JS/CSS/图标)+ 根目录一份
|
||||
`plugin.json` 清单。插件自带全部资源,不依赖宿主或其它插件的任何文件;
|
||||
* **多插件并存、互相隔离**:每个插件一个包文件、一个独立 origin
|
||||
(`svfs://<插件id>/`——浏览器按 origin 隔离 localStorage / IndexedDB / cookie)、
|
||||
一份独立的键值存储。协议层拒绝跨插件请求,导航守卫禁止跳到别的插件或把插件页
|
||||
变成远程内容容器;
|
||||
* **快照即版本**:一个插件一个 .svfs,每次升级往里打一个新快照——没变的文件共享
|
||||
blob(升级通常只增加变化部分的字节),旧版本全部保留,**回滚 = 切换当前快照**,
|
||||
包文件一个字节都不用动;
|
||||
* **完整性与签名贯穿始终**:安装时跑 `verify()`(可选逐文件重算 sha256),
|
||||
从远程安装时每个对象按 sha256 校验。
|
||||
|
||||
### 安装与体验
|
||||
|
||||
```bash
|
||||
uv sync --extra gui # = [client] + [studio];WebEngine 需要完整版 PySide6
|
||||
uv run svfs-client # 或 python -m svfs_client
|
||||
# 想指定数据目录:uv run svfs-client --data ./my-client-data
|
||||
```
|
||||
|
||||
三个 extra 的区别只在 Qt 装多少:`client` = 完整 PySide6(WebEngine 在
|
||||
PySide6-Essentials 里**没有**)、`studio` = PySide6-Essentials(Studio 只用
|
||||
QtCore/QtGui/QtWidgets)、`gui` = 两者都装(旧写法保留,等价于 client + studio)。
|
||||
wheel 里三个包(sqlite_vfs / svfs_studio / svfs_client)一起分发——两个桌面
|
||||
应用的 console script 都声明在同一个发行版里,只打核心包会让 `svfs-client`
|
||||
命令存在而模块缺失。
|
||||
|
||||
```bash
|
||||
pip install "sqlite-vfs[client]" # 只装插件客户端(含 PySide6)
|
||||
pip install sqlite-vfs # 只要核心库 + CLI(零第三方依赖)
|
||||
```
|
||||
|
||||
启动后先经过一段启动动画和登录界面(强制登录,详见下文「登录与认证」;
|
||||
搭配本仓库的示例服务端时,默认账号是 admin / admin123)。侧栏列出已安装的
|
||||
插件;插件管理(安装/升级/回滚/卸载)在「设置 → 插件管理」里。
|
||||
|
||||
### plugin.json 规范
|
||||
|
||||
包根(当前快照根)必须有一份 `plugin.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.example.clock",
|
||||
"name": "时钟",
|
||||
"version": "1.2.0",
|
||||
"api_version": 3,
|
||||
"entry": "index.html",
|
||||
"icon": "icon.png",
|
||||
"permissions": ["storage", "notify", "title", "net"],
|
||||
"description": "一个时钟插件"
|
||||
}
|
||||
```
|
||||
|
||||
* `id`:反域名形式(小写字母/数字/连字符),全局唯一,同时是 `svfs://` 的 host;
|
||||
* `api_version`:插件**要求的最低宿主桥版本**——宿主接受 `api_version ≤ 自身
|
||||
PLUGIN_API_VERSION` 的插件(桥能力只增不破,升级客户端不会废掉旧插件;
|
||||
要求更高版本的插件会得到「请升级客户端」提示)。取值参考:v2 = net 与
|
||||
host.info 的 baseUrl;v3 = host.info 的 authUser、宿主给同源 net 请求自动
|
||||
附加登录 token;v4 = dialog 权限与 `ui.confirm`/`ui.message` 原生对话框。
|
||||
没用新能力就写 2(甚至 1)即可,兼容面更大;
|
||||
* `entry`:SPA 入口。路径 404 且末段没有扩展名时回退到它(每个插件都是完整前端);
|
||||
* `permissions`:声明这个插件要用哪些桥能力(storage / notify / title / net /
|
||||
dialog),安装时向用户展示;
|
||||
* `net_hosts`(可选,**net 目标白名单**):声明后,该插件经桥发的请求只能指向
|
||||
列表里的主机,其它一律被宿主拒绝并给出明确错误。支持精确主机名与 `*.` 后缀
|
||||
匹配(`"*.corp.example"` 含 `corp.example` 本身)。不写 = 不限制(老插件行为
|
||||
不变)。有 net 权限的插件本质上能把数据发到任何地方,给内网/企业插件的清单
|
||||
加上这一项是划算的。写了它就必须同时声明 `net` 权限(安装时校验会拦住写错的
|
||||
清单)。
|
||||
|
||||
### 服务器地址(base_url)
|
||||
|
||||
「插件管理 → 服务器」里配置一次服务端地址(`http(s)://`),它同时承担两件事:
|
||||
|
||||
1. **插件下载**:发布端把每个插件的 publish 产物同步到服务器的
|
||||
`<base_url>/<插件id>/` 子目录下;客户端安装时只填**插件 id**(或相对路径),
|
||||
自动解析成 `<base_url>/<id>/manifest.json`。填完整 URL 也可以。
|
||||
2. **接口服务**:声明了 `net` 权限的插件经桥访问这个服务器的 API——
|
||||
宿主代为发 HTTP 请求,**不经过浏览器的 CORS**,服务端不需要任何跨域配置。
|
||||
|
||||
配置存在 `<data>/settings.json`,改动即时生效(已打开的插件页面下次调用就能拿到)。
|
||||
|
||||
### 登录与认证(强制登录)
|
||||
|
||||
客户端启动时先播放一段启动动画(呼吸 logo + 状态文字,点击可跳过),随后进入
|
||||
登录界面——**必须登录成功才能使用客户端**。登录窗用同一套暗色主题,无头一次
|
||||
填写过服务器地址的话会自动预填,下次勾着「记住登录」登录后,启动动画期间就会
|
||||
静默校验 token(`GET /api/me`),有效则直接进主窗口,过期/网络不通则回到登录窗
|
||||
并提示原因。
|
||||
|
||||
* 登录请求:`POST <base_url>/api/login`(JSON `{"username","password"}`)→
|
||||
`{"ok":true,"token","username"}`。服务端会话在内存里,重启服务端即全部失效;
|
||||
* 凭据持久化:勾选「记住登录」才存 token,**且不落在 `settings.json` 里**——
|
||||
token 是能直接冒充用户的东西,改由 `credentials.py` 按可用后端加密保存:
|
||||
Windows 用 DPAPI(密文只有当前用户在本机解得开),其它平台用系统凭据库
|
||||
(可选依赖 `keyring`),两者都没有才退化成明文并**在设置界面如实提示**
|
||||
(「设置 → 登录凭据」显示当前存储方式)。老版本存在 `settings.json` 里的
|
||||
`auth_token` 会在启动时自动迁移并清空原字段。不勾「记住登录」则 token 只在
|
||||
本次运行内有效(服务器地址与用户名仍会记住);
|
||||
* 凭据绑定服务器:令牌只交还给**签发它的那个 base_url**,在设置里换了服务器
|
||||
地址后旧令牌不会被当成新服务器的凭据发出去;
|
||||
* 插件无感知:宿主桥给**发往服务器(与 base_url 同源)** 的 net 请求自动附加
|
||||
`Authorization: Bearer <token>`,不会发给第三方地址,也不覆盖插件自带的
|
||||
Authorization 头;
|
||||
* 会话失效有反馈:如果服务器不再认这个 token(服务端重启、会话过期),宿主
|
||||
注入的凭据被 401 拒绝时会**主动提示**——状态灯显示「登录已失效」(黄,而不是
|
||||
误报「离线」),横幅给出「重新登录…」一键入口,用户中心与侧栏头像同步显示
|
||||
失效状态。提示只发一次,重新登录成功后自动恢复;
|
||||
* `host.info` 新增 `authUser` 字段(当前登录用户名,未登录为空串);
|
||||
* 「插件管理 → 服务器」卡片显示登录状态,可「重新登录…」或「退出登录」。
|
||||
|
||||
### 界面:侧栏 / 设置 / 用户中心
|
||||
|
||||
* 启动后**直接显示插件内容**(可配置):优先用设置里配置的启动插件,
|
||||
未配置则按默认规则(上次打开的 > 按 id 排序第一个启用的插件),都没有
|
||||
才停在插件管理页;
|
||||
* 侧栏顶部「«」把侧栏收成**图标栏**(宽度动画过渡,不再是瞬跳也不再让插件
|
||||
不可达——图标带悬停提示,点击即打开),再点「»」展开;选择会记住,
|
||||
下次启动保持;
|
||||
* 侧栏搜索框**模糊匹配**插件名与 id(包含匹配优先,其次首字母缩写:
|
||||
「csd」能搜到 `com.svfs.demo`、中文名直接搜汉字);无结果时给出提示,
|
||||
清空即恢复全部;
|
||||
* 侧栏底部「设置」:**插件管理入口**(安装本地 .svfs、从服务器安装、
|
||||
升级/回滚/卸载)、**启动时打开**的插件、修改服务器地址(与服务器卡片
|
||||
同步生效)、**数据目录**(插件路径,写指针文件,重启生效)、**对象缓存
|
||||
路径**(留空 = 系统默认,安装时即时生效)与缓存清理、**网络**(插件 net
|
||||
请求的超时与重试次数、代理、额外 CA 证书——内网自签 HTTPS 与公司代理都
|
||||
在这里配,保存即生效,CA 路径写错会当场报错而不是等请求失败)、
|
||||
**登录凭据**存储方式、**诊断日志落盘**开关;
|
||||
* 侧栏底部的圆形头像(登录用户首字母)点击进「用户中心」:当前登录
|
||||
状态、服务器地址、本次登录时间,以及退出登录 / 重新登录。
|
||||
* 插件页面底色固定为主题底色;WebEngine 在启动阶段预热(Profile 提前
|
||||
创建 + 隐藏预热视图 + 共享 OpenGL 上下文)——首次打开插件不再闪白屏
|
||||
或整窗闪烁。
|
||||
|
||||
### 可靠性与诊断
|
||||
|
||||
* **插件更新检查**:插件管理页「检查更新」逐个比对服务器上的快照名与
|
||||
本地版本,有新版就在卡片上显示「可升级 → x.y.z」徽章与一键升级按钮
|
||||
(走远程安装链路,升级后徽章自动消失);
|
||||
* **客户端自更新检查**:启动时(以及改服务器地址后)顺带看一眼服务器上的
|
||||
`<base_url>/client/manifest.json`(静态文件,与插件清单同一套托管方式,
|
||||
内容 `{"version","url","notes"}`);比本地新就在状态栏提示并在横幅给出
|
||||
「下载新版本…」(交给系统浏览器打开 `url`)。没放这个文件 = 静态 404,
|
||||
静默跳过,不当错误;
|
||||
* **服务器连通状态**:服务器卡片上有一个状态灯(灰=未配置 / 绿=在线并
|
||||
显示延迟 / 黄=在线但登录已失效 / 红=离线),启动时探测一次并每 60 秒复查;
|
||||
* **离线宽限**:启动时若服务器不可达(网络错误,而非凭据被拒),记住的
|
||||
登录凭据仍然放行——带「离线模式」提示进入,本地插件照常用,接口类
|
||||
功能恢复连接后自然可用;
|
||||
* **网络可配置**:插件 net 请求的超时(默认 30 秒)与重试次数(默认 1,**只
|
||||
对幂等方法** GET/HEAD/OPTIONS 生效,遇 502/503/504 或连接层错误按指数退避
|
||||
重试——自动重发 POST 可能把"下单"变成两单,所以不重试);代理与额外 CA
|
||||
证书在设置里配,loopback 目标永远直连;
|
||||
* **渲染崩溃恢复**:插件页面的渲染进程崩溃时显示可一键重载的占位页,
|
||||
而不是永久白屏;
|
||||
* **诊断日志**:工作线程输出、插件页面控制台(`console.log` 等)、桥的
|
||||
net 失败(含重试与凭据被拒)、未捕获异常、服务器探测失败统一记入内存
|
||||
环形缓冲(最近 600 条),**同时滚动写入 `<data>/logs/client.log`**
|
||||
(1 MB × 3 份,可在设置里关掉)——崩溃/重启之后还能回看现场,而内存那份
|
||||
恰好会随崩溃一起消失;「设置 → 导出诊断报告…」生成含环境信息与日志的
|
||||
文本文件(报告里带日志文件路径)。
|
||||
|
||||
### 插件前端拿到的桥:`window.svfs`
|
||||
|
||||
宿主自动注入(插件作者**不需要**引用 qwebchannel.js),能力按 `permissions` 把关,
|
||||
没声明的能力调用直接失败:
|
||||
|
||||
```html
|
||||
<script>
|
||||
const info = await window.svfs.host.info();
|
||||
// → {pluginId, appName, appVersion, apiVersion, platform, baseUrl, authUser}
|
||||
|
||||
await window.svfs.storage.set('visits', 42); // 需要 "storage" 权限
|
||||
const visits = await window.svfs.storage.get('visits'); // 42(按插件隔离)
|
||||
await window.svfs.storage.remove('visits');
|
||||
const keys = await window.svfs.storage.keys();
|
||||
|
||||
// 需要 "net" 权限:宿主代发 HTTP 请求(绕开 CORS,响应上限 20 MB)
|
||||
const res = await window.svfs.net.request({ method: 'GET', path: '/api/ping' });
|
||||
// 相对 path 拼在 base_url 后;也可以 { url: 'https://api.example.com/…' }
|
||||
// → { ok, status, headers, bodyText, bodyBase64 },4xx/5xx 也是正常返回
|
||||
// 超时/重试来自客户端设置;清单里写了 net_hosts 就只有白名单主机能通
|
||||
await window.svfs.net.request({ method: 'POST', path: '/api/save',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({a: 1}) });
|
||||
|
||||
await window.svfs.ui.notify('打包完成'); // 需要 "notify" 权限
|
||||
// 宿主发系统托盘通知;没有托盘(容器/被策略禁用)时退回状态栏提示
|
||||
await window.svfs.ui.setTitle('时钟 · 12:30'); // 需要 "title" 权限
|
||||
|
||||
// 需要 "dialog" 权限(v4+):宿主**原生对话框**,Promise 返回用户是否点了"确定"
|
||||
const ok = await window.svfs.ui.confirm({ title: '删除', text: '确定删除这条记录?' });
|
||||
await window.svfs.ui.message({ kind: 'warn', title: '警告', text: '余额不足' });
|
||||
</script>
|
||||
```
|
||||
|
||||
`host.info` 不需要权限——它只包含插件自己的元数据、宿主版本号与配置的服务器
|
||||
地址,不含用户数据。
|
||||
|
||||
### 发布 → 安装的完整链路
|
||||
|
||||
发布端用现成的 `svfs publish`,客户端用 manifest 安装(对象逐个 sha256 校验):
|
||||
|
||||
```bash
|
||||
# 插件开发机:打包 + 发布到静态托管
|
||||
svfs pack ./my-plugin dist.svfs --compress
|
||||
svfs publish dist.svfs ./public
|
||||
aws s3 sync ./public s3://你的桶/plugin/com.example.clock/ \
|
||||
--cache-control "public, max-age=31536000, immutable"
|
||||
|
||||
# 客户端「从服务器安装…」里填插件 id 即可:
|
||||
# com.example.clock
|
||||
# (publish 产物已按约定同步到 <base_url>/com.example.clock/ 下;
|
||||
# 同一个插件发新版本后,客户端再装一次就完成增量升级)
|
||||
```
|
||||
|
||||
服务端如果还提供接口服务,加一个普通 HTTP API 即可——插件的 `net` 请求就是
|
||||
普通的服务端调用,无需为 `svfs://` origin 做任何跨域配置。
|
||||
|
||||
### 插件开发辅助:桥调试台与 Vue 模板
|
||||
|
||||
* **桥调试台**:客户端「设置 → 桥调试台 → 安装调试插件」一键装一个调试
|
||||
插件(`com.svfs.demo`),把 `window.svfs` 的每类能力做成可点击面板
|
||||
(host.info / storage 键值 / net 自定义请求 / notify / title / 原生对话框),
|
||||
开发自己的插件时用来对照验证真实桥行为;
|
||||
* **Vue 开发模板**:`examples/vue-plugin-template/` —— Vue 3 + Vite 起步
|
||||
模板,含 `src/svfs.js`(Promise 封装)、`src/mock-svfs.js`(浏览器 dev
|
||||
模式下的宿主模拟器,`npm run dev` 不进客户端也能迭代 UI)与每类能力
|
||||
一个的面板组件;`npm run build` 后 `svfs pack ./dist plugin.svfs` 即可
|
||||
安装(详见模板内 README)。
|
||||
|
||||
### 服务端最小示例(FastAPI)
|
||||
|
||||
`examples/plugin_server.py` 是一个单文件服务端,同时覆盖插件下载与接口服务:
|
||||
`POST /plugins/<id>` 收 .svfs 包并就地 publish(服务端用同一套清单规则校验),
|
||||
静态吐出各插件的 `manifest.json + blobs/` 供客户端下载,`GET /api/ping` 等
|
||||
API 供插件经 `net` 调用;可选 `SVFS_UPLOAD_TOKEN` 环境变量保护上传。
|
||||
|
||||
`/api/*` 需要登录(`GET /api/ping`、`POST /api/echo` 校验 Bearer token),
|
||||
登录体系三个接口:
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| `POST /api/login` | `{"username","password"}` → `{"ok",token,"username","login_time"}`,密码错 401 |
|
||||
| `GET /api/me` | 带 Bearer token → `{"ok","username","login_time"}`,用于启动时静默校验 |
|
||||
| `POST /api/logout` | 作废当前 token |
|
||||
|
||||
另外提供可选的 `/client/manifest.json`(客户端自更新检查):设置环境变量
|
||||
`SVFS_CLIENT_VERSION=9.9.9 SVFS_CLIENT_URL=https://…` 即可看到「客户端有新版本」
|
||||
提示,不设置时返回 404(客户端静默跳过)。生产上这个文件直接静态托管即可,
|
||||
内容就是 `{"version", "url", "notes"}`。
|
||||
|
||||
账号来源:默认内置演示账号 `admin / admin123`;设置环境变量
|
||||
`SVFS_USERS="user:pass,user2:pass2"` 即换成自己的账号表。插件下载与上传保持
|
||||
开放/沿用原有令牌机制,不受登录影响。
|
||||
|
||||
```bash
|
||||
uv sync --extra fastapi # fastapi/starlette + uvicorn + python-multipart
|
||||
uv run python examples/plugin_server.py # → http://127.0.0.1:8000
|
||||
# 启动时会打印登录账号提示
|
||||
|
||||
# 客户端「服务器」卡片填 http://127.0.0.1:8000,登录窗输 admin / admin123;
|
||||
# 「从服务器安装」填插件 id(或 curl 上传一个包试试,上传不需要登录 token):
|
||||
curl -F "file=@dist.svfs" http://127.0.0.1:8000/plugins/com.example.my-plugin
|
||||
|
||||
# 直接调 /api/* 的外部脚本要先拿 token:
|
||||
token=$(curl -s -X POST http://127.0.0.1:8000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin123"}' | jq -r .token)
|
||||
curl -H "Authorization: Bearer $token" http://127.0.0.1:8000/api/ping
|
||||
```
|
||||
|
||||
它的行为有测试背书(`tests/test_plugin_server.py`,含"上传 → 服务端 publish →
|
||||
真实 HTTP → 客户端 install_from_url 装回"的完整闭环)。
|
||||
|
||||
### 数据目录
|
||||
|
||||
```text
|
||||
<data>/ Windows: %LOCALAPPDATA%/svfs-client;其它: ~/.svfs-client
|
||||
├── registry.json 已安装插件注册表(enabled、当前快照、来源…)
|
||||
├── settings.json 客户端设置(服务器地址、网络、日志开关…)
|
||||
├── credentials.dat 登录令牌(DPAPI/keyring 加密;见 credentials.py)
|
||||
├── logs/client.log 诊断日志(1 MB × 3 份滚动,可关)
|
||||
├── plugins/<id>/plugin.svfs 每个插件一个包文件(快照即版本)
|
||||
├── storage/<id>.json 桥 storage 的键值存储(按插件隔离)
|
||||
└── web/ WebView 的持久化目录(localStorage 等)
|
||||
```
|
||||
|
||||
`settings.json` 里**没有**登录令牌:老版本的 `auth_token` 字段会在启动时迁移到
|
||||
`credentials.dat`(并按可用的加密后端保存),迁移后原字段留空不再使用。
|
||||
|
||||
### 实现分层(与 svfs-studio 同一套约定)
|
||||
|
||||
`manifest` / `paths` / `registry` / `installer` / `sample` / `auth` /
|
||||
`credentials` / `diagnostics` / `netutil` / `search` 是**纯标准库**
|
||||
(不 import Qt,可在无 PySide6 的环境单独测试);`scheme` / `bridge` /
|
||||
`webview` / `worker` / `login`(启动动画与登录窗)/ `shell` / `app` 属于 Qt 层。
|
||||
界面复用 svfs-studio 的主题与基础部件,视觉与 Studio 一致。
|
||||
|
||||
测试(逻辑层纯标准库;界面层 offscreen,含一次真实的 WebEngine 冒烟——加载
|
||||
示例插件并验证桥注入与 storage 往返,环境起不来 WebEngine 时自动跳过):
|
||||
|
||||
```bash
|
||||
python -m unittest discover -s tests -p "test_client_logic.py" -v
|
||||
python -m unittest discover -s tests -p "test_client_auth.py" -v # 登录认证/网络策略(纯层)
|
||||
python -m unittest discover -s tests -p "test_client_credentials.py" -v # 凭据存储(纯层)
|
||||
python -m unittest discover -s tests -p "test_client_login_ui.py" -v # 登录窗/启动动画/桥注入
|
||||
python -m unittest discover -s tests -p "test_client_ui.py" -v
|
||||
```
|
||||
|
||||
|
||||
## 数据能力
|
||||
|
||||
@@ -384,6 +801,26 @@ sqlite_vfs/
|
||||
│ ├── migrate.py # vfsmigrate
|
||||
│ ├── publish.py # vfspublish(导出对象 + 清单)
|
||||
│ └── fetch.py # vfsfetch(按清单取回并校验)
|
||||
├── src/svfs_studio/ # 桌面可视化工具(可选 extra: gui)
|
||||
│ ├── format.py # [无 Qt] 大小/时间/权限/文本嗅探/hexdump 的显示格式化
|
||||
│ ├── analysis.py # [无 Qt] 快照差异、去重与占用汇总、SQLite 内部结构解析
|
||||
│ ├── worker.py # 所有库调用都跑在一个专用线程里;进度回调、中断、日志
|
||||
│ ├── theme.py # 暗色调色板 + QSS
|
||||
│ ├── sample.py # 示例包(与 README 里的演示同一棵树)
|
||||
│ ├── views/ # 七个标签页:概览/文件/快照/存储/完整性/内部/工具箱
|
||||
│ └── widgets/ # 外壳(主窗口)、卡片、自绘图表
|
||||
├── src/svfs_client/ # 插件化桌面客户端(可选 extra: gui,需要完整版 PySide6)
|
||||
│ ├── manifest.py # [无 Qt] plugin.json 清单的读取与校验
|
||||
│ ├── paths.py # [无 Qt] 数据目录布局(注册表 / 设置 / 包 / 键值存储 / WebView)
|
||||
│ ├── settings.py # [无 Qt] 客户端设置(base_url 的读写、规范化与地址解析)
|
||||
│ ├── registry.py # [无 Qt] 已安装插件注册表(JSON 原子写)
|
||||
│ ├── installer.py # [无 Qt] 安装 / 升级 / 回滚 / 卸载(复用核心库 API)
|
||||
│ ├── sample.py # [无 Qt] 示例插件生成器(完整前端 + 桥能力演示)
|
||||
│ ├── scheme.py # svfs:// 协议:从包内直接服务文件,host=插件id 实现隔离
|
||||
│ ├── bridge.py # 受限 QWebChannel 桥(storage / net / notify / title,按权限把关)
|
||||
│ ├── webview.py # PluginView + 导航守卫 + window.svfs 注入
|
||||
│ ├── worker.py # 后台线程服务(request-id 分发、进度、独占 op)
|
||||
│ └── shell.py # 主窗口:侧栏 + 插件管理页 + 每插件一个常驻 WebView
|
||||
├── tests/ # 单元与集成测试(纯标准库 unittest)
|
||||
└── README.md
|
||||
```
|
||||
@@ -455,8 +892,12 @@ sqlite_vfs/
|
||||
# 安装开发依赖
|
||||
uv sync --dev
|
||||
|
||||
# 运行测试
|
||||
pytest
|
||||
# 运行测试(测试是纯标准库 unittest,没有引入 pytest)
|
||||
uv run python -m unittest discover -s tests
|
||||
|
||||
# 只跑界面测试(offscreen,不需要显示器;未装含 GUI 的 extra 时会整体跳过)
|
||||
uv sync --extra gui
|
||||
uv run python -m unittest discover -s tests -p "test_studio_ui.py" -v
|
||||
|
||||
# 构建包
|
||||
uv build
|
||||
|
||||
Reference in New Issue
Block a user