一个插件 = 一个完整的 .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。
80 lines
2.9 KiB
Plaintext
80 lines
2.9 KiB
Plaintext
/* 演示插件的前端逻辑:全部能力都走宿主注入的 window.svfs(Promise)。 */
|
||
"use strict";
|
||
|
||
var $ = function (id) { return document.getElementById(id); };
|
||
var PLUGIN_VERSION = "2.0.0";
|
||
|
||
function randomColor() {
|
||
var letters = "0123456789abcdef", out = "#";
|
||
for (var i = 0; i < 6; i++) out += letters[Math.floor(Math.random() * 16)];
|
||
return out;
|
||
}
|
||
|
||
async function main() {
|
||
if (!window.svfsReady) {
|
||
$("missing").hidden = false;
|
||
$("version").textContent = "插件 " + PLUGIN_VERSION + "(未连接宿主)";
|
||
return;
|
||
}
|
||
var svfs = await window.svfsReady;
|
||
|
||
var info = await svfs.host.info();
|
||
$("version").textContent = "插件 " + PLUGIN_VERSION + " · 桥 API v" + info.apiVersion;
|
||
$("host-info").innerHTML = "";
|
||
[["插件 id", info.pluginId], ["宿主", info.appName + " " + info.appVersion],
|
||
["平台", info.platform], ["来源", info.source || "-"]]
|
||
.forEach(function (row) {
|
||
var dt = document.createElement("dt"); dt.textContent = row[0];
|
||
var dd = document.createElement("dd"); dd.textContent = row[1];
|
||
$("host-info").append(dt, dd);
|
||
});
|
||
|
||
// storage:按插件隔离的键值存储,换一台机器/重装插件也不会串到别的插件
|
||
var count = 0;
|
||
try {
|
||
count = parseInt(await svfs.storage.get("visits"), 10) || 0;
|
||
count += 1;
|
||
await svfs.storage.set("visits", count);
|
||
} catch (e) { /* 没拿到 storage 权限时静默降级 */ }
|
||
$("count").textContent = String(count);
|
||
$("reset").onclick = async function () {
|
||
await svfs.storage.set("visits", 0);
|
||
$("count").textContent = "0";
|
||
svfs.ui.notify("计数已清零");
|
||
};
|
||
$("add10").onclick = async function () {
|
||
count += 10;
|
||
await svfs.storage.set("visits", count);
|
||
$("count").textContent = String(count);
|
||
};
|
||
|
||
$("notify").onclick = function () {
|
||
svfs.ui.notify("来自 " + info.pluginId + " 的通知(" + PLUGIN_VERSION + ")");
|
||
};
|
||
$("paint").onclick = function () {
|
||
svfs.ui.setTitle("SVFS Client · 演示插件 " + randomColor());
|
||
};
|
||
|
||
// net:宿主代发 HTTP 请求(不经过浏览器的 CORS)。相对 path 拼在
|
||
// 客户端配置的服务器地址(base_url)后;也可以给 {url: "https://…"}。
|
||
$("ping").onclick = async function () {
|
||
if (!svfs.net) {
|
||
$("net-out").textContent = "这个宿主没有 net 能力(需要客户端支持桥 API v2)";
|
||
return;
|
||
}
|
||
$("net-out").textContent = "请求中…";
|
||
try {
|
||
var res = await svfs.net.request({ method: "GET", path: "/api/ping" });
|
||
$("net-out").textContent = "HTTP " + res.status + "\n"
|
||
+ (res.bodyText || "(空响应)").slice(0, 300);
|
||
} catch (e) {
|
||
$("net-out").textContent = "请求失败: " + e.message
|
||
+ "\n(先在「插件管理 → 服务器」里配置地址,服务端需提供 /api/ping)";
|
||
}
|
||
};
|
||
|
||
$("changelog").textContent = "v2:新增主题色切换与变更记录";
|
||
}
|
||
|
||
main();
|