一、消息模块:服务端主动推送 + 插件定向分发
* 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。
90 lines
3.6 KiB
TOML
90 lines
3.6 KiB
TOML
[project]
|
||
dependencies = []
|
||
description = "sqlite virtual file system"
|
||
name = "sqlite-vfs"
|
||
readme = "README.md"
|
||
# 版本门槛按实际用到的 API 推导,不是拍的:
|
||
# * contrib/fastapi_static.py 用了 hashlib.md5(..., usedforsecurity=False) → 3.9+
|
||
# * 测试用了 random.Random.randbytes → 3.9+
|
||
# 核心(core/compression/migration/publish/folder_*)本身只用到 3.7 就有的东西;
|
||
# 仓库里没有 3.10+ 的语法(无 match、无 PEP 604 注解、无 zip(strict=))。
|
||
# 说明:这里只在本机的 3.13 上跑过,"3.9 可用"是代码审查的结论,没有在 3.9 上实测。
|
||
requires-python = ">=3.9"
|
||
version = "0.3.0"
|
||
|
||
# 可选集成:核心功能只用标准库,第三方框架一律走 extras。
|
||
# 例如 pip install "sqlite-vfs[fastapi]" 才会装上 FastAPI/Starlette。
|
||
[project.optional-dependencies]
|
||
fastapi = [
|
||
"fastapi>=0.100",
|
||
"starlette>=0.27",
|
||
"uvicorn>=0.23",
|
||
"python-multipart>=0.0.6",
|
||
]
|
||
# 消息模块的 MQTT 推送通道(服务端主动推消息给客户端)。
|
||
# 不装也不影响消息中心:登录后的 HTTP 补齐照常工作,只是没有实时推送。
|
||
# 客户端在缺这个库时会在消息中心显示"未启用 MQTT",不会报错。
|
||
mqtt = [
|
||
"paho-mqtt>=2.0",
|
||
]
|
||
# 桌面工具用的 Qt 绑定。核心库与 CLI 依然零第三方依赖,所以放在 extra 里。
|
||
#
|
||
# 三个 extra 的分工(都装着 PySide6,区别只在要不要 WebEngine):
|
||
# studio —— svfs-studio(仅 QtCore/QtGui/QtWidgets,Essentials 就够)
|
||
# client —— svfs-client(QWebEngineView 在 PySide6-Essentials 里**没有**,
|
||
# 必须完整版)
|
||
# gui —— 两个桌面应用都装的合集,保留它是为了不破坏既有文档里的
|
||
# `uv sync --extra gui` / `sqlite-vfs[gui]` 写法
|
||
studio = [
|
||
"PySide6-Essentials>=6.6",
|
||
]
|
||
client = [
|
||
"PySide6>=6.6",
|
||
]
|
||
gui = [
|
||
"PySide6>=6.6",
|
||
]
|
||
|
||
[[tool.uv.index]]
|
||
default = true
|
||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||
|
||
# 说明:这里原先有 [tool.uv.workspace] members = ["devmo"],但仓库里没有 devmo 目录。
|
||
# 实测 uv 0.11.8 容忍这种写法(uv sync --dry-run 退出码 0),所以它不是阻塞项,
|
||
# 只是会误导人,因此删掉。
|
||
|
||
[build-system]
|
||
build-backend = "hatchling.build"
|
||
requires = ["hatchling"]
|
||
|
||
[dependency-groups]
|
||
dev = [
|
||
"twine>=4.0.2",
|
||
# starlette 1.6+ 的 TestClient(tests/test_plugin_server.py 用)需要它
|
||
"httpx2>=0.1",
|
||
# 消息模块的 MQTT 通道:测试里用真 paho 客户端对自建的最小 broker 跑端到端
|
||
# (见 tests/test_client_mqtt.py),所以开发环境需要它
|
||
"paho-mqtt>=2.0",
|
||
]
|
||
|
||
[project.scripts]
|
||
# 统一入口(推荐):svfs <子命令>
|
||
svfs = "sqlite_vfs.cli.main:main"
|
||
# 旧的独立命令保留为别名(与 svfs <同名子命令> 同一段实现)
|
||
vfspack = "sqlite_vfs.cli.packer:pack"
|
||
vfsunpack = "sqlite_vfs.cli.unpacker:unpack"
|
||
vfsverify = "sqlite_vfs.cli.verify:verify"
|
||
vfsmigrate = "sqlite_vfs.cli.migrate:main"
|
||
vfspublish = "sqlite_vfs.cli.publish:publish_objects"
|
||
vfsfetch = "sqlite_vfs.cli.fetch:fetch_objects"
|
||
svfs-studio = "svfs_studio.__main__:main"
|
||
svfs-client = "svfs_client.__main__:main"
|
||
|
||
[tool.hatch.build.targets.wheel]
|
||
# 三个包必须一起打:`svfs-studio` / `svfs-client` 两个 console script 在上面
|
||
# 声明了,svfs_client 还 import svfs_studio 的主题与基础部件(UI 套件)——
|
||
# 只打 sqlite_vfs 的话,pip 装完命令存在但模块不在(ImportError)。
|
||
# 不装 Qt([gui] extra)也能安全安装:这三个包在导入前都不会碰 PySide6
|
||
# (纯标准库层见 svfs_client/__init__.py 的分层说明)。
|
||
packages = ["src/sqlite_vfs", "src/svfs_studio", "src/svfs_client"]
|