一个插件 = 一个完整的 .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。
62 lines
2.0 KiB
Vue
62 lines
2.0 KiB
Vue
<script setup>
|
||
import { onMounted, ref } from "vue";
|
||
import { createStorage, notify } from "../svfs.js";
|
||
|
||
const storage = createStorage("demo:");
|
||
const count = ref(0);
|
||
const key = ref("greeting");
|
||
const value = ref("你好,SVFS");
|
||
const output = ref("");
|
||
|
||
onMounted(async () => {
|
||
count.value = (await storage.get("visits")) || 0;
|
||
count.value += 1;
|
||
await storage.set("visits", count.value);
|
||
});
|
||
|
||
async function reset() {
|
||
await storage.set("visits", 0);
|
||
count.value = 0;
|
||
notify("计数已清零");
|
||
}
|
||
|
||
async function run(action) {
|
||
try {
|
||
if (action === "set") {
|
||
let parsed = value.value;
|
||
try { parsed = JSON.parse(value.value); } catch { /* 字符串原样存 */ }
|
||
await storage.set(key.value, parsed);
|
||
output.value = `set(${key.value}) 成功`;
|
||
} else if (action === "get") {
|
||
output.value = `get(${key.value}) → ${JSON.stringify(await storage.get(key.value))}`;
|
||
} else if (action === "remove") {
|
||
await storage.remove(key.value);
|
||
output.value = `remove(${key.value}) 成功`;
|
||
} else {
|
||
output.value = `keys() → ${JSON.stringify(await storage.keys())}`;
|
||
}
|
||
} catch (err) {
|
||
output.value = `失败: ${err.message}`;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="panel">
|
||
<h2>storage —— 按插件隔离的键值存储(本插件已打开 {{ count }} 次)</h2>
|
||
<p>
|
||
<input v-model="key" placeholder="键" size="14" />
|
||
<input v-model="value" placeholder="值(支持 JSON)" size="22" />
|
||
<button class="act" @click="run('set')">写入</button>
|
||
<button class="act" @click="run('get')">读取</button>
|
||
<button class="act" @click="run('remove')">删除</button>
|
||
<button class="act" @click="run('keys')">全部键</button>
|
||
</p>
|
||
<pre>{{ output || "(操作结果显示在这里)" }}</pre>
|
||
<p class="muted">
|
||
<button class="act" @click="reset">打开计数清零</button>
|
||
换台机器 / 重装插件,这个存储也不会串到别的插件。
|
||
</p>
|
||
</section>
|
||
</template>
|