master
main
起始状态:主链路 100% 不可用,而且失败被静默吞掉——打包后库里 0 个文件、 界面仍打印"文件数量: N";解包导出 0 个文件并在磁盘上生成几百层 sub/sub/sub 垃圾目录。 缺陷修复(4 个 P0 + 12 个 P1,逐条见 BUG_REPORT.md): - 建表列名 compresse_algo 与 INSERT/read_file 用的 compress_algo 不一致 - list_directory 按 path 而非 parent_path 过滤,导致导出无限自递归 - CLI --ignore-timestamps 把 export_filesystem 赋成引用自身的 lambda - add_file 缺少 commit(直接调用 API 时静默丢数据) - LIKE 前缀误伤兄弟目录(删 doc 连带删 docs);mv_dir 二次改名 - 失败路径一律返回 exit code 0;--exclude 是子串匹配而非 glob - FolderPacker.add_file 反向调用 export_file_safe,会用库内内容覆盖源文件 存储层(schema v3,格式变更,提供 vfsmigrate 迁移): - 内容移入 blobs(hash PRIMARY KEY):相同内容只存一份,跨路径/跨快照共享 - files 的唯一约束改为 UNIQUE(snapshot_id, path):一个库存多个版本 - 完整性:每文件 sha256 + 快照级摘要 + HMAC-SHA256 签名,只读连接即可验证 - 增量打包:按 mtime+size 或 sha256 只写变化项,未变文件不重写不重压 - get_file_info 不再返回 content(元数据查询不该搬 BLOB) 新增能力: - 可选 FastAPI 静态文件插件(extras,核心仍零第三方依赖) - 内容寻址发布到对象存储 + 逐对象校验的取回客户端 - 压缩默认改回 zlib:实测比自研哈夫曼体积小 1/3~1/170、快数百倍 其它: - P2-4:时间戳显式转字符串,清掉 1240 条 DeprecationWarning(盘上格式不变) - read_file 负数 offset/size 改为报错(原先会走切片语义从末尾取) - requires-python 从 >=3.7 改为按实际 API 推导的 >=3.9 测试:0 → 343 个用例,纯标准库 unittest,有/无 FastAPI 两种环境都跑通。 证据见 docs/evidence-*.txt。
- README:新增「数据能力」四节(内容寻址去重 / 快照与时间旅行 / 完整性校验与签名 / 增量打包)与「发布到对象存储」一节,附实测对比表;重写「要不要用 --compress」 (结论从"别用"反转为"默认就该开")、「迁移旧格式」;修正项目结构与字段表 - BUG_REPORT.md:13 节,含 P0/P1 逐条分析、P2/P3 清单、五批后续追加的记录, 以及过程中被推翻的两处结论(第九节的压缩建议、P3-1 的 uv sync 判断) - docs/encryption-design.md:加密的设计说明——去重与随机 nonce 的冲突、 标准库没有密码学原语、以及现有 freq_blob 可作为每块附加数据的槽位 - docs/evidence-*.txt:各阶段的测试运行证据(修复前 / 修复后 / 双环境)
桌面应用(svfs-studio / svfs-client)要用的前置改动: * 新增 src/sqlite_vfs/cli/main.py:`svfs <子命令>` 统一入口,旧的独立命令 (vfspack / vfsunpack / …)保留为别名,两套跑同一段 execute(args);各子命令 因此从 sys.exit() 改成返回退出码,便于被复用; * FolderPacker.pack_folder(progress=…):每处理一个条目(目录也算)回调一次, 供 GUI 进度条使用;只有传了回调时才做"只 stat、不读内容"的预扫描,CLI 的 行为与开销不变;回调抛出的异常一路上抛——GUI 的中断语义就靠它; * publish.default_cache_dir():对象缓存目录(客户端设置里可改);load_manifest 对"误把服务器根地址当清单填进来"给出可用插件列表的提示,而不是只甩一个 format=None。 测试:新增 tests/test_cli_main.py、tests/test_pack_progress.py,test_publish.py 补充。
一个插件 = 一个完整的 .svfs 包:客户端把插件前端经 svfs://<插件id>/ 直接加载 进 WebView(不落盘、不起端口、不解包),每个插件一个独立 origin(浏览器按 origin 隔离 localStorage)与独立键值存储;快照即版本——升级 = 派生快照的增量 打包,回滚 = 把注册表指针切回旧快照,包文件一个字节不用动。svfs_client 复用 svfs_studio 的主题与基础部件,故两个包一并入库。 按评估结论补齐的 P0 三块: * 会话:桥在"宿主注入的 token 被 401 拒绝"时通知壳层,提示一次并给出一键 「重新登录…」;服务器状态灯把「在线但登录失效」(黄)与「离线」(红)分开, 不再把凭据失效误报成网络不可达; * 凭据:新增 credentials.py——token 不再明文进 settings.json。Windows 走 DPAPI (密文只有当前用户在本机解得开),其它平台走系统凭据库(可选依赖 keyring), 都没有才明文兜底并在「设置 → 登录凭据」如实提示;令牌与签发它的 base_url 绑定(换服务器不会把旧令牌发出去);老字段 auth_token 启动时自动迁移; * 强登录链路保持:启动动画 → 静默校验 → 登录窗 → 主窗口,不可达时离线宽限。 P1/P2:net 超时与重试(只对幂等方法,避免重发 POST 变两单)可配、代理与 自签 CA 可配(loopback 永远直连);诊断日志滚动落盘(1MB×3,可关)——崩溃后 还能回看现场;客户端自更新检查(静态 <base_url>/client/manifest.json,缺失 即静默);插件清单可选 net_hosts 白名单;notify 改走系统托盘(无托盘降级状态栏); 设置界面新增网络 / 登录凭据 / 诊断日志三块。 examples/:示例服务端(FastAPI,含登录、上传即 publish、客户端版本接口)、 Vue 3 插件模板(带浏览器版宿主模拟器)、svfs-plugins-root/ 为模板配套的演示 插件实体。测试:tests/test_client_*.py(纯层 + Qt 层 + 一次真实 WebEngine 冒烟)、 tests/test_studio_*.py、tests/test_plugin_server.py。
* 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)。
一、消息模块:服务端主动推送 + 插件定向分发 * topics.py:主题方案 <prefix>/<org>/user/<user>/message、.../user/<user>/plugin/<id>、 .../broadcast/{message,plugin/<id>}。org 是租户位(多租户上线不用改格式,broker 按 <prefix>/<org>/# 下发 ACL 即可隔离);用户名/插件 id 经**可逆**百分号编码占 一个层级,客户端与示例服务端共用同一套拼主题函数; * bus.py:统一消息总线——按**主题**分类(主机消息进消息中心、插件消息不进,仪表盘 数据那类业务载荷永远不会混进给人看的列表)、归属校验(org/user 不符即丢弃,broker ACL 配错也挡得住)、按 id 去重(QoS 1 重投与"推送/补齐重叠"都不会重复显示)、 插件消息按插件缓存 50 条,等页面打开后一次性补投; * mqtt.py + mqtt_service.py:paho 适配(可选依赖 [mqtt])+ Qt 适配(paho 的网络 线程经排队信号把消息交给主线程的总线);重连交给 paho(指数退避),重连成功 自动重新订阅;没装 paho / 没配 broker 都只是"没有实时推送",HTTP 补齐照常; * worker.py:登录后**先**补齐(GET /api/messages?since=)再连 MQTT,两条路进同一 总线;在途结果带身份快照,退出登录/换账号后丢弃; * 界面:侧栏 ✉ 未读徽标 + 消息中心(级别/标题/时间/来源/广播标记、立即同步、 全部已读、清空、通道状态灯);warn/error 顺手发系统托盘通知; * 插件侧:新权限 messages(PLUGIN_API_VERSION→5),页面里 svfs.messages.subscribe(); 宿主经 window.__svfsPush 投递,早到的先排队、订阅时补发(桥调试台已接上演示); * examples/plugin_server.py:/api/messages、/api/push(kind=message|plugin)、 /api/broadcast;配了 SVFS_MQTT_HOST 就真发布到 broker,并把 broker 地址随登录 响应下发(/api/login 与 /api/me 都带),客户端不用手填。 二、退出登录 = 关掉主窗口 + 回到登录界面 * 强制登录下"未登录的主窗口"本就不该存在:重新登录成功用新会话开回主窗口,关掉 登录窗即退出客户端;退出时断开推送、清掉该账号收到的消息与总线身份(下一个人 不该看到上一个人的消息),并收掉已打开的插件页面; * 用户中心的"退出登录"先关掉自己再进登录门,避免模态窗口叠模态。 三、插件顺序编排 * installer.arrange():编排过的在前、未编排的按名称;管理页卡片加 ↑/↓,侧栏/图标栏/ 管理页三者一致;存 settings 的 plugin_order,只影响显示(不动包与注册表); * "启动时打开"的默认规则改为"显示顺序里第一个启用的插件"(原来是按 id 排); * 新增「恢复默认顺序」(编排过才出现)。 四、修复 * 收缩侧栏后页面左上角露出一块纯白方块:页面栈对 Qt 是透明的(主题只给 QMainWindow/QDialog 设了背景),侧栏让出的那条在重绘时序里露白。给 QStackedWidget 不透明底色,并加像素级回归用例(撤掉修复即 FAIL,实测 1237 个白像素); * build_opener() 每次都要隐式枚举 Windows 证书库:全量测试跑几百次后以断点异常 崩在证书枚举里(faulthandler 定位在 _load_windows_store_certs)。改成进程内只建 一次 SSL context、启动时在主线程预建; * 在途的消息补齐结果会喂给新会话(退出登录时用户名已空 → 拼主题抛异常,被 Qt 回调 静默吞掉):带身份快照,不匹配即丢弃。 五、设置界面按类型分页签(插件 / 服务器 / 存储 / 网络 / 诊断) * 每页内容在滚动区里(窗口拉小或以后再加项是滚动条,而不是把控件压扁); * 新增「消息推送(MQTT)」一节:broker(host / host:port / mqtts://host:8883)、组织、 端口、TLS、主题前缀、broker 账号,保存即重连;状态行显示**生效**配置与来源 (手配 / 服务端下发)。broker 密码不进界面:默认用登录 token(已加密存储),需要 单独的密码时由服务端下发——与"凭据不明文落盘"保持一致。 测试与验证 * 新增 tests/test_client_messages.py(主题/封皮/总线,纯层)、tests/test_client_mqtt.py (装配层 + 自建最小 broker 的端到端,不依赖 mosquitto)、test_plugin_server.py 的 消息接口与"/api/push → broker → 客户端总线"闭环;WebEngine 冒烟里加了页面级 messages 探针; * **测试入口改为 tests/run_tests.py**:QtWebEngine 在 offscreen 下的解释器退出路径会 崩(测试全绿但退出码 139,CI 只看退出码就会判失败),runner 按 unittest 的结果 os._exit 绕开那段清理;测试真崩在中途时退出码照样非 0(用故意失败的用例验证过)。 README 与 CI 都切到这个入口; * 全量 720 条通过(6 skip),exit 0。
三处写入侧的缺口,都是被下游"模块银行"(module_bank,把模块打包进 svfs 包 再按需导入)真实逼出来的: - add_bytes(rel_path, data, ...):从内存写文件,不经过磁盘。受保护的字节码、 密文这类绝对不能落盘的内容因此可以直接入库——先写临时文件再 add_file 会把 明文留在磁盘上,而磁盘残留是这类工具最容易被忽视的泄漏点。 - make_parents=True(默认):补齐缺失的父目录行。list_directory 是按 parent_path 过滤的,而"某个目录存在"的唯一标志就是它自己那一行——少一层 目录行,export_directory 从根就递归不下去,export_filesystem() 会静默导出 一个空目录。add_file 一并受益(内部改为读盘后调 add_bytes,行为不变, 含 created_time 取 st_ctime 这个细节)。 - commit=False + 公开的 commit():一组相关文件可以写成一个事务,中途任何一步 失败整批回滚,不会在库里留下半套数据。 - rename_snapshot(id, name):create/list/delete 都有,唯独缺改名;而"把发布名 落在快照上"是版本管理的基本动作。**定格一个版本靠的正是它**:先给当前快照 改名,再另建一个工作快照接手后续写入——直接 create_snapshot(name) 的话, 名字落在会继续被写入的那一版上,下一次改动就把这个"版本"改了。 - DEFAULT_DIR_PERMISSIONS:补出来的目录行用 0o755(表的默认值 0o644 是按文件 定的,套到目录上会让导出的目录不可进入)。 - 版本 0.3.0 → 0.4.0(add_file 开始建父目录行属于可见的行为变化)。 测试:tests/test_core_add_bytes.py(22 条:往返/空内容/bytearray/时间戳与权限/ 压缩/去重/覆盖/空路径/只读拒绝/父目录行与 export 目录树/批量提交与回滚/ add_file 委托行为)+ tests/test_snapshots.py 新增 5 条改名用例(只改名字不动 内容与摘要/空名拒绝/不存在的快照报错/允许重名/改名+派生工作快照之后 已定格的版本不被后续写入改动)。完整测试套件逐个文件跑通。
No dependencies set.
The note is not visible to the blocked user.
一、消息模块:服务端主动推送 + 插件定向分发 * topics.py:主题方案 <prefix>/<org>/user/<user>/message、.../user/<user>/plugin/<id>、 .../broadcast/{message,plugin/<id>}。org 是租户位(多租户上线不用改格式,broker 按 <prefix>/<org>/# 下发 ACL 即可隔离);用户名/插件 id 经**可逆**百分号编码占 一个层级,客户端与示例服务端共用同一套拼主题函数; * bus.py:统一消息总线——按**主题**分类(主机消息进消息中心、插件消息不进,仪表盘 数据那类业务载荷永远不会混进给人看的列表)、归属校验(org/user 不符即丢弃,broker ACL 配错也挡得住)、按 id 去重(QoS 1 重投与"推送/补齐重叠"都不会重复显示)、 插件消息按插件缓存 50 条,等页面打开后一次性补投; * mqtt.py + mqtt_service.py:paho 适配(可选依赖 [mqtt])+ Qt 适配(paho 的网络 线程经排队信号把消息交给主线程的总线);重连交给 paho(指数退避),重连成功 自动重新订阅;没装 paho / 没配 broker 都只是"没有实时推送",HTTP 补齐照常; * worker.py:登录后**先**补齐(GET /api/messages?since=)再连 MQTT,两条路进同一 总线;在途结果带身份快照,退出登录/换账号后丢弃; * 界面:侧栏 ✉ 未读徽标 + 消息中心(级别/标题/时间/来源/广播标记、立即同步、 全部已读、清空、通道状态灯);warn/error 顺手发系统托盘通知; * 插件侧:新权限 messages(PLUGIN_API_VERSION→5),页面里 svfs.messages.subscribe(); 宿主经 window.__svfsPush 投递,早到的先排队、订阅时补发(桥调试台已接上演示); * examples/plugin_server.py:/api/messages、/api/push(kind=message|plugin)、 /api/broadcast;配了 SVFS_MQTT_HOST 就真发布到 broker,并把 broker 地址随登录 响应下发(/api/login 与 /api/me 都带),客户端不用手填。 二、退出登录 = 关掉主窗口 + 回到登录界面 * 强制登录下"未登录的主窗口"本就不该存在:重新登录成功用新会话开回主窗口,关掉 登录窗即退出客户端;退出时断开推送、清掉该账号收到的消息与总线身份(下一个人 不该看到上一个人的消息),并收掉已打开的插件页面; * 用户中心的"退出登录"先关掉自己再进登录门,避免模态窗口叠模态。 三、插件顺序编排 * installer.arrange():编排过的在前、未编排的按名称;管理页卡片加 ↑/↓,侧栏/图标栏/ 管理页三者一致;存 settings 的 plugin_order,只影响显示(不动包与注册表); * "启动时打开"的默认规则改为"显示顺序里第一个启用的插件"(原来是按 id 排); * 新增「恢复默认顺序」(编排过才出现)。 四、修复 * 收缩侧栏后页面左上角露出一块纯白方块:页面栈对 Qt 是透明的(主题只给 QMainWindow/QDialog 设了背景),侧栏让出的那条在重绘时序里露白。给 QStackedWidget 不透明底色,并加像素级回归用例(撤掉修复即 FAIL,实测 1237 个白像素); * build_opener() 每次都要隐式枚举 Windows 证书库:全量测试跑几百次后以断点异常 崩在证书枚举里(faulthandler 定位在 _load_windows_store_certs)。改成进程内只建 一次 SSL context、启动时在主线程预建; * 在途的消息补齐结果会喂给新会话(退出登录时用户名已空 → 拼主题抛异常,被 Qt 回调 静默吞掉):带身份快照,不匹配即丢弃。 五、设置界面按类型分页签(插件 / 服务器 / 存储 / 网络 / 诊断) * 每页内容在滚动区里(窗口拉小或以后再加项是滚动条,而不是把控件压扁); * 新增「消息推送(MQTT)」一节:broker(host / host:port / mqtts://host:8883)、组织、 端口、TLS、主题前缀、broker 账号,保存即重连;状态行显示**生效**配置与来源 (手配 / 服务端下发)。broker 密码不进界面:默认用登录 token(已加密存储),需要 单独的密码时由服务端下发——与"凭据不明文落盘"保持一致。 测试与验证 * 新增 tests/test_client_messages.py(主题/封皮/总线,纯层)、tests/test_client_mqtt.py (装配层 + 自建最小 broker 的端到端,不依赖 mosquitto)、test_plugin_server.py 的 消息接口与"/api/push → broker → 客户端总线"闭环;WebEngine 冒烟里加了页面级 messages 探针; * **测试入口改为 tests/run_tests.py**:QtWebEngine 在 offscreen 下的解释器退出路径会 崩(测试全绿但退出码 139,CI 只看退出码就会判失败),runner 按 unittest 的结果 os._exit 绕开那段清理;测试真崩在中途时退出码照样非 0(用故意失败的用例验证过)。 README 与 CI 都切到这个入口; * 全量 720 条通过(6 skip),exit 0。View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.