feat: 消息模块(MQTT 推送 + 统一总线),并补退出登录/顺序编排/设置分页签
一、消息模块:服务端主动推送 + 插件定向分发
* 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。
This commit is contained in:
@@ -329,6 +329,7 @@ wheel 里三个包(sqlite_vfs / svfs_studio / svfs_client)一起分发——
|
||||
|
||||
```bash
|
||||
pip install "sqlite-vfs[client]" # 只装插件客户端(含 PySide6)
|
||||
pip install "sqlite-vfs[mqtt]" # 再加消息模块的 MQTT 推送通道(paho-mqtt)
|
||||
pip install sqlite-vfs # 只要核心库 + CLI(零第三方依赖)
|
||||
```
|
||||
|
||||
@@ -358,11 +359,13 @@ pip install sqlite-vfs # 只要核心库 + CLI(零第三方依赖
|
||||
PLUGIN_API_VERSION` 的插件(桥能力只增不破,升级客户端不会废掉旧插件;
|
||||
要求更高版本的插件会得到「请升级客户端」提示)。取值参考:v2 = net 与
|
||||
host.info 的 baseUrl;v3 = host.info 的 authUser、宿主给同源 net 请求自动
|
||||
附加登录 token;v4 = dialog 权限与 `ui.confirm`/`ui.message` 原生对话框。
|
||||
附加登录 token;v4 = dialog 权限与 `ui.confirm`/`ui.message` 原生对话框;
|
||||
v5 = messages 权限与服务端推送(`svfs.messages.subscribe`)。
|
||||
没用新能力就写 2(甚至 1)即可,兼容面更大;
|
||||
* `entry`:SPA 入口。路径 404 且末段没有扩展名时回退到它(每个插件都是完整前端);
|
||||
* `permissions`:声明这个插件要用哪些桥能力(storage / notify / title / net /
|
||||
dialog),安装时向用户展示;
|
||||
dialog / **messages**),安装时向用户展示。messages = 接收服务端推给本插件的
|
||||
消息(见下文「消息模块」);
|
||||
* `net_hosts`(可选,**net 目标白名单**):声明后,该插件经桥发的请求只能指向
|
||||
列表里的主机,其它一律被宿主拒绝并给出明确错误。支持精确主机名与 `*.` 后缀
|
||||
匹配(`"*.corp.example"` 含 `corp.example` 本身)。不写 = 不限制(老插件行为
|
||||
@@ -409,28 +412,52 @@ pip install sqlite-vfs # 只要核心库 + CLI(零第三方依赖
|
||||
误报「离线」),横幅给出「重新登录…」一键入口,用户中心与侧栏头像同步显示
|
||||
失效状态。提示只发一次,重新登录成功后自动恢复;
|
||||
* `host.info` 新增 `authUser` 字段(当前登录用户名,未登录为空串);
|
||||
* 退出登录(用户中心或服务器卡片):清掉本地凭据 → 关闭主窗口 → 回到登录
|
||||
界面(重新登录成功用新会话开回主窗口;关掉登录窗即退出客户端)——强制登录
|
||||
下不存在"能用的未登录状态"。退出时同时断开消息推送并清掉该账号收到的消息
|
||||
(下一个登录的人不该看到上一个人的消息);
|
||||
* 登录成功后接上消息模块:**先同步**(HTTP 补齐离线期间的消息),再连 MQTT
|
||||
接收实时推送;两者都进统一总线按 id 去重;
|
||||
* 「插件管理 → 服务器」卡片显示登录状态,可「重新登录…」或「退出登录」。
|
||||
|
||||
### 界面:侧栏 / 设置 / 用户中心
|
||||
|
||||
* 启动后**直接显示插件内容**(可配置):优先用设置里配置的启动插件,
|
||||
未配置则按默认规则(上次打开的 > 按 id 排序第一个启用的插件),都没有
|
||||
未配置则按默认规则(上次打开的 > 显示顺序里第一个启用的插件),都没有
|
||||
才停在插件管理页;
|
||||
* **插件顺序可编排**:插件管理页每张卡片右下角有 ↑/↓,上移/下移一位即改
|
||||
显示顺序(侧栏、图标栏、管理页三者一致);顺序存在 `settings.json` 的
|
||||
`plugin_order` 里,只影响显示——不动包、不动注册表、不请求服务器。没排到
|
||||
的插件(新装的)按名称跟在后面,"恢复默认顺序"一键清掉编排(那之后默认
|
||||
按名称排)。启动直显的默认规则也读这个顺序,把常用插件排到最前面就默认
|
||||
打开它;
|
||||
* 侧栏顶部「«」把侧栏收成**图标栏**(宽度动画过渡,不再是瞬跳也不再让插件
|
||||
不可达——图标带悬停提示,点击即打开),再点「»」展开;选择会记住,
|
||||
下次启动保持;
|
||||
* 侧栏搜索框**模糊匹配**插件名与 id(包含匹配优先,其次首字母缩写:
|
||||
「csd」能搜到 `com.svfs.demo`、中文名直接搜汉字);无结果时给出提示,
|
||||
清空即恢复全部;
|
||||
* 侧栏底部「设置」:**插件管理入口**(安装本地 .svfs、从服务器安装、
|
||||
升级/回滚/卸载)、**启动时打开**的插件、修改服务器地址(与服务器卡片
|
||||
同步生效)、**数据目录**(插件路径,写指针文件,重启生效)、**对象缓存
|
||||
路径**(留空 = 系统默认,安装时即时生效)与缓存清理、**网络**(插件 net
|
||||
请求的超时与重试次数、代理、额外 CA 证书——内网自签 HTTPS 与公司代理都
|
||||
在这里配,保存即生效,CA 路径写错会当场报错而不是等请求失败)、
|
||||
**登录凭据**存储方式、**诊断日志落盘**开关;
|
||||
* 侧栏底部「设置」——设置项按类型分成五个页签(每页内容超出时可滚动,不会被
|
||||
压扁),每页里保存按钮只作用于自己那一组:
|
||||
* **插件**:插件管理入口(安装本地 .svfs、从服务器安装、升级/回滚/卸载)、
|
||||
桥调试台(一键装调试插件)、启动时打开的插件;
|
||||
* **服务器**:服务器地址(与服务器卡片同步生效)、登录凭据的存储方式、
|
||||
消息推送(MQTT)的 broker/组织/前缀/账号(保存即重连);
|
||||
* **存储**:数据目录(插件路径,写指针文件,重启生效)、对象缓存路径
|
||||
(留空 = 系统默认,安装时即时生效)与缓存清理;
|
||||
* **网络**:插件 net 请求的超时与重试次数、代理、额外 CA 证书(内网自签
|
||||
HTTPS 与公司代理都在这里配,保存即生效,CA 路径写错会当场报错);
|
||||
* **诊断**:诊断日志落盘开关、导出诊断报告;
|
||||
* 侧栏底部的 **✉ 消息入口**(带未读数字)进「消息中心」:服务端推来的主机
|
||||
消息按时间倒序列出(级别、标题、时间、来源、广播标记),顶部是 MQTT 通道
|
||||
状态灯与「立即同步 / 全部已读 / 清空」——进页面即算已读,徽标不会一直挂着;
|
||||
* 侧栏底部的圆形头像(登录用户首字母)点击进「用户中心」:当前登录
|
||||
状态、服务器地址、本次登录时间,以及退出登录 / 重新登录。
|
||||
状态、服务器地址、本次登录时间,以及退出登录 / 重新登录;
|
||||
* **退出登录 = 关掉主窗口并回到登录界面**:客户端是强制登录的,"未登录的
|
||||
主窗口"本就不该存在,所以退出后不会留一个没有凭据的界面继续用。已打开的
|
||||
插件页面会一并收掉(插件的键值存储按插件隔离、不区分用户,页面状态同理,
|
||||
换账号重进时重新加载);重新登录成功则用新会话把主窗口开回来,用户关掉
|
||||
登录窗则退出客户端。退出登录时也会尽力通知服务器作废 token;
|
||||
* 插件页面底色固定为主题底色;WebEngine 在启动阶段预热(Profile 提前
|
||||
创建 + 隐藏预热视图 + 共享 OpenGL 上下文)——首次打开插件不再闪白屏
|
||||
或整窗闪烁。
|
||||
@@ -494,12 +521,87 @@ 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: '余额不足' });
|
||||
|
||||
// 需要 "messages" 权限(v5+):订阅服务端推给**本插件**的消息
|
||||
const off = (await window.svfsReady).messages.subscribe(function (message) {
|
||||
// message.text 是原文;能解析成 JSON 时另有 message.json(仪表盘数据这类)
|
||||
render(message.json || message.text);
|
||||
});
|
||||
// off() 取消订阅
|
||||
</script>
|
||||
```
|
||||
|
||||
`host.info` 不需要权限——它只包含插件自己的元数据、宿主版本号与配置的服务器
|
||||
地址,不含用户数据。
|
||||
|
||||
### 消息模块:服务端主动推送(MQTT)
|
||||
|
||||
服务端要能随时把消息推给某个用户(或某个插件),走的是**统一消息总线**:
|
||||
所有通道的消息先进总线,由总线按主题分类、去重、再分发。两条通道:
|
||||
|
||||
| 通道 | 时机 | 说明 |
|
||||
|---|---|---|
|
||||
| MQTT 推送 | 实时 | 服务端主动推;broker 地址由登录响应下发(也可在设置里手配) |
|
||||
| HTTP 补齐 | **登录成功后先做一次**,也可点「立即同步」 | `GET /api/messages?since=…` 把离线期间攒下的消息补回来 |
|
||||
|
||||
两条路的消息都喂进同一个总线、按 id 去重,所以"推送"与"补齐"重叠、QoS 1
|
||||
重投都不会重复显示。
|
||||
|
||||
**主题方案**(组织 `org` 是租户位,从第一版就占好——多租户上线不用改格式,
|
||||
broker 侧按 `<prefix>/<org>/#` 下发 ACL 即可隔离):
|
||||
|
||||
```text
|
||||
<prefix>/<org>/user/<user>/message 主机消息 → 消息中心
|
||||
<prefix>/<org>/user/<user>/plugin/<plugin_id> 插件消息 → 只给该插件
|
||||
<prefix>/<org>/broadcast/message 组织内广播 → 消息中心
|
||||
<prefix>/<org>/broadcast/plugin/<plugin_id> 组织内广播 → 只给该插件
|
||||
```
|
||||
|
||||
* 登录后客户端只订阅两条(最小权限,不是整个组织):`<prefix>/<org>/user/<user>/#`
|
||||
与 `<prefix>/<org>/broadcast/#`;
|
||||
* 用户名/插件 id 占一个主题层级,经**可逆**的百分号编码(`topic_segment`),
|
||||
所以 "a b" 与 "a_b" 不会串线;服务端用同一个函数(`svfs_client.topics`)
|
||||
拼主题,两端规则一致;
|
||||
* 即使 broker 的 ACL 配错、把别人的消息推过来,总线还会按当前组织/用户再挡
|
||||
一道(`forbidden` 计数 + 诊断日志)。
|
||||
|
||||
**两类消息泾渭分明**(分类只看主题,不看载荷):
|
||||
|
||||
* **主机消息**进消息中心(侧栏底部的 ✉,带未读徽标):载荷是消息封皮
|
||||
`{id, title, body, level, time, source}`,纯字符串也当正文;
|
||||
* **插件消息**不进消息中心:服务端发什么插件就收到什么(仪表盘数据、行情、
|
||||
位号刷新这类业务载荷,补历史没有意义),宿主只负责按插件 id 投递。
|
||||
|
||||
插件页面是懒加载的,所以**消息可能比页面先到**:总线按插件缓存最近 50 条
|
||||
(超了丢最旧的并计数),页面打开后一次性补投;页面侧还会再排一次队
|
||||
(宿主在 DocumentCreation 阶段就立好投递入口),插件订阅时补发——所以
|
||||
插件写 `subscribe` 时不用担心"消息是不是已经来过了"。
|
||||
|
||||
```js
|
||||
// 插件侧(清单里声明 "messages" 权限才会存在)
|
||||
const svfs = await window.svfsReady;
|
||||
svfs.messages.subscribe((message) => {
|
||||
console.log(message.text, message.json); // 原文 + 尽量解析出的 JSON
|
||||
});
|
||||
```
|
||||
|
||||
**配置与降级**:
|
||||
|
||||
* broker 地址优先用登录响应下发的 `mqtt: {host, port, tls, prefix, org}`
|
||||
(部署信息不该让每个用户手填)。**也可以在「设置 → 消息推送(MQTT)」里直接
|
||||
配**:broker(接受 `host` / `host:1883` / `mqtts://host:8883`,URL 形式里的
|
||||
scheme 与端口优先)、端口、TLS、主题前缀、组织标识、broker 账号——保存即重连,
|
||||
不用等下次登录;那一节还会显示**生效**的配置与来源(手配 / 服务端下发);
|
||||
* broker 密码不走设置界面:缺省用登录 token(已加密存储、不落明文);需要单独的
|
||||
broker 密码时由服务端在登录响应里下发(或写 `settings.json` 的
|
||||
`mqtt_password`)。落盘的键:`mqtt_host` / `mqtt_port` / `mqtt_tls` /
|
||||
`mqtt_prefix` / `mqtt_username` / `mqtt_password` / `org`;
|
||||
* MQTT 需要可选依赖:`pip install "sqlite-vfs[mqtt]"`(`paho-mqtt`)。没装、
|
||||
没配 broker、连不上,都只是"没有实时推送"——界面在消息中心显示状态
|
||||
(未启用 / 连接中 / 已连接 / 连接失败),补齐照常工作;
|
||||
* 断线重连交给 paho(指数退避 1→30 秒),重连成功后自动重新订阅;
|
||||
* 连接状态、每个通道的收发与丢弃计数都进诊断日志(含 `forbidden`/`dropped`)。
|
||||
|
||||
### 发布 → 安装的完整链路
|
||||
|
||||
发布端用现成的 `svfs publish`,客户端用 manifest 安装(对象逐个 sha256 校验):
|
||||
@@ -548,6 +650,19 @@ API 供插件经 `net` 调用;可选 `SVFS_UPLOAD_TOKEN` 环境变量保护上
|
||||
| `GET /api/me` | 带 Bearer token → `{"ok","username","login_time"}`,用于启动时静默校验 |
|
||||
| `POST /api/logout` | 作废当前 token |
|
||||
|
||||
消息相关(见上文「消息模块」):
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| `GET /api/messages?since=` | 客户端登录后/「立即同步」时补齐消息(只返回该登录用户的) |
|
||||
| `POST /api/push` | 服务端主动推:`kind=message` 进消息中心 + 待收列表;`kind=plugin` 只走插件主题(`plugin` 字段指定目标插件,`data` 原样发给插件) |
|
||||
| `POST /api/broadcast` | 组织广播(主题里带 `broadcast`) |
|
||||
|
||||
配了 broker(`SVFS_MQTT_HOST`,可选 `SVFS_MQTT_PORT` / `SVFS_MQTT_TLS` /
|
||||
`SVFS_ORG`)时,推送会真的发布到 broker,且 broker 地址随登录响应下发,
|
||||
客户端自动连上;没配就只写待收列表(响应里如实说明 `mqtt=skipped`),
|
||||
客户端仍能靠登录后的同步看到消息——这条降级路径让演示不依赖 broker。
|
||||
|
||||
另外提供可选的 `/client/manifest.json`(客户端自更新检查):设置环境变量
|
||||
`SVFS_CLIENT_VERSION=9.9.9 SVFS_CLIENT_URL=https://…` 即可看到「客户端有新版本」
|
||||
提示,不设置时返回 404(客户端静默跳过)。生产上这个文件直接静态托管即可,
|
||||
@@ -595,20 +710,32 @@ curl -H "Authorization: Bearer $token" http://127.0.0.1:8000/api/ping
|
||||
### 实现分层(与 svfs-studio 同一套约定)
|
||||
|
||||
`manifest` / `paths` / `registry` / `installer` / `sample` / `auth` /
|
||||
`credentials` / `diagnostics` / `netutil` / `search` 是**纯标准库**
|
||||
(不 import Qt,可在无 PySide6 的环境单独测试);`scheme` / `bridge` /
|
||||
`webview` / `worker` / `login`(启动动画与登录窗)/ `shell` / `app` 属于 Qt 层。
|
||||
`credentials` / `diagnostics` / `netutil` / `search` / `topics` / `messages` /
|
||||
`bus` / `mqtt` 是**纯标准库/无 Qt 依赖**(不 import Qt,可在无 PySide6 的环境
|
||||
单独测试;`mqtt` 的 paho 也是可选依赖,缺了只报 `missing` 状态);
|
||||
`scheme` / `bridge` / `webview` / `worker` / `mqtt_service` / `messages_page` /
|
||||
`login`(启动动画与登录窗)/ `shell` / `app` 属于 Qt 层。
|
||||
界面复用 svfs-studio 的主题与基础部件,视觉与 Studio 一致。
|
||||
|
||||
测试(逻辑层纯标准库;界面层 offscreen,含一次真实的 WebEngine 冒烟——加载
|
||||
示例插件并验证桥注入与 storage 往返,环境起不来 WebEngine 时自动跳过):
|
||||
示例插件并验证桥注入、storage 往返与消息订阅;环境起不来 WebEngine 时自动跳过)。
|
||||
**入口是 `tests/run_tests.py` 而不是直接 `python -m unittest`**:QtWebEngine 在
|
||||
offscreen 下的解释器退出路径会崩(测试全绿、退出码却是 139,CI 会判失败),
|
||||
runner 拿到 unittest 的结果后用 `os._exit` 退出,绕开那段清理;测试真正崩在
|
||||
中途时退出码照样非 0:
|
||||
|
||||
```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
|
||||
uv run python tests/run_tests.py # 全部(716 条)
|
||||
uv run python tests/run_tests.py -v # 逐条打印
|
||||
|
||||
# 只跑某一层(同样建议走 runner,参数透传给 unittest discover)
|
||||
uv run python tests/run_tests.py -p "test_client_logic.py" # 清单/路径/注册表/安装器/顺序编排(纯层)
|
||||
uv run python tests/run_tests.py -p "test_client_auth.py" # 登录认证/网络策略(纯层)
|
||||
uv run python tests/run_tests.py -p "test_client_credentials.py" # 凭据存储(纯层)
|
||||
uv run python tests/run_tests.py -p "test_client_messages.py" # 消息主题/封皮/总线(纯层)
|
||||
uv run python tests/run_tests.py -p "test_client_mqtt.py" # MQTT 通道(含自建 broker 端到端)
|
||||
uv run python tests/run_tests.py -p "test_client_login_ui.py" # 登录窗/启动动画/桥注入
|
||||
uv run python tests/run_tests.py -p "test_client_ui.py" # 主窗口/协议层/WebEngine 冒烟
|
||||
```
|
||||
|
||||
|
||||
@@ -892,12 +1019,13 @@ sqlite_vfs/
|
||||
# 安装开发依赖
|
||||
uv sync --dev
|
||||
|
||||
# 运行测试(测试是纯标准库 unittest,没有引入 pytest)
|
||||
uv run python -m unittest discover -s tests
|
||||
# 运行测试(测试是纯标准库 unittest,没有引入 pytest;入口见 tests/run_tests.py
|
||||
# 的说明——QtWebEngine 的解释器退出路径会崩,直接 discover 会拿到非零退出码)
|
||||
uv run python tests/run_tests.py
|
||||
|
||||
# 只跑界面测试(offscreen,不需要显示器;未装含 GUI 的 extra 时会整体跳过)
|
||||
uv sync --extra gui
|
||||
uv run python -m unittest discover -s tests -p "test_studio_ui.py" -v
|
||||
uv run python tests/run_tests.py -p "test_studio_ui.py" -v
|
||||
|
||||
# 构建包
|
||||
uv build
|
||||
|
||||
Reference in New Issue
Block a user