2026-09-16 17:15:48 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""桥调试台插件生成器(纯标准库)。
|
|
|
|
|
|
|
|
|
|
|
|
设置 → 「安装调试台插件」一键安装:现场造一个"完整前端"插件包——manifest +
|
|
|
|
|
|
HTML/JS/CSS/图标,把桥的全部能力做成可点击的调试面板(host.info / storage /
|
|
|
|
|
|
net / notify / title / 原生对话框),插件开发时用来验证 window.svfs 的实际
|
|
|
|
|
|
行为。``build(out_db)`` 第一次生成 1.0.0;对同一个 out_db 再调会走与真实
|
|
|
|
|
|
升级完全相同的流程(派生快照 + 增量打包),正好演示"快照即版本"与回滚。
|
|
|
|
|
|
|
|
|
|
|
|
前端通过宿主注入的 ``window.svfsReady``(Promise)拿桥;在客户端外打开时
|
|
|
|
|
|
给出友好提示,不会白屏。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
|
|
from sqlite_vfs.core import SQLiteVFS
|
|
|
|
|
|
from sqlite_vfs.folder_packer import FolderPacker
|
|
|
|
|
|
|
|
|
|
|
|
from . import PLUGIN_API_VERSION, installer
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = ["PLUGIN_ID", "write_source", "build"]
|
|
|
|
|
|
|
|
|
|
|
|
PLUGIN_ID = "com.svfs.demo"
|
|
|
|
|
|
|
|
|
|
|
|
#: 版本 → 说明(2.0.0 有可感知的变化,用来演示升级与回滚)
|
|
|
|
|
|
CHANGELOG = {
|
|
|
|
|
|
1: "初始版本",
|
2026-09-17 01:06:02 +08:00
|
|
|
|
2: "v2:新增主题色切换、消息订阅与变更记录",
|
2026-09-16 17:15:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _manifest(version):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": PLUGIN_ID,
|
|
|
|
|
|
"name": "SVFS 桥调试台",
|
|
|
|
|
|
"version": f"{version}.0.0",
|
|
|
|
|
|
"api_version": PLUGIN_API_VERSION,
|
|
|
|
|
|
"entry": "index.html",
|
|
|
|
|
|
"icon": "icon.svg",
|
2026-09-17 01:06:02 +08:00
|
|
|
|
"permissions": ["storage", "notify", "title", "net", "dialog", "messages"],
|
2026-09-16 17:15:48 +08:00
|
|
|
|
"description": "演示/调试客户端桥的全部能力(host.info / storage / net / notify / title / 原生对话框)。",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_ICON_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
|
|
|
|
<rect x="4" y="4" width="56" height="56" rx="14" fill="#22262f"/>
|
|
|
|
|
|
<path d="M20 34 L29 43 L45 23" stroke="#5b9dff" stroke-width="6"
|
|
|
|
|
|
fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
_STYLE_CSS = """\
|
|
|
|
|
|
:root {
|
|
|
|
|
|
--bg: #16181d; --panel: #1c1f26; --border: #2b3038;
|
|
|
|
|
|
--text: #e4e7ec; --muted: #9aa3b2; --accent: #5b9dff;
|
|
|
|
|
|
}
|
|
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
|
body {
|
|
|
|
|
|
margin: 0; padding: 28px; background: var(--bg); color: var(--text);
|
|
|
|
|
|
font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
|
|
|
|
|
}
|
|
|
|
|
|
main { max-width: 640px; margin: 0 auto; }
|
|
|
|
|
|
header { display: flex; align-items: center; gap: 14px; margin-bottom: 20px; }
|
|
|
|
|
|
h1 { font-size: 20px; margin: 0; }
|
|
|
|
|
|
#version { color: var(--muted); margin: 2px 0 0; font-size: 13px; }
|
|
|
|
|
|
.card {
|
|
|
|
|
|
background: var(--panel); border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 8px; padding: 14px 16px; margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card h2 { font-size: 13px; color: var(--muted); margin: 0 0 10px; font-weight: 600; }
|
|
|
|
|
|
dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; margin: 0; }
|
|
|
|
|
|
dt { color: var(--muted); } dd { margin: 0; font-family: Consolas, monospace; font-size: 12px; }
|
|
|
|
|
|
b { color: var(--accent); font-size: 16px; }
|
|
|
|
|
|
button {
|
|
|
|
|
|
background: #22262f; color: var(--text); border: 1px solid #3a4150;
|
|
|
|
|
|
border-radius: 6px; padding: 6px 14px; cursor: pointer; margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
button:hover { border-color: var(--accent); color: var(--accent); }
|
|
|
|
|
|
.row { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; align-items: center; }
|
|
|
|
|
|
input, select, textarea {
|
|
|
|
|
|
background: #14171c; color: var(--text); border: 1px solid #3a4150;
|
|
|
|
|
|
border-radius: 6px; padding: 6px 8px; font-family: Consolas, monospace; font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
textarea { width: 100%; min-height: 54px; resize: vertical; }
|
|
|
|
|
|
pre {
|
|
|
|
|
|
background: #14171c; border: 1px solid var(--border); border-radius: 6px;
|
|
|
|
|
|
padding: 8px 10px; font-family: Consolas, monospace; font-size: 12px;
|
|
|
|
|
|
white-space: pre-wrap; word-break: break-all; margin: 8px 0 0; min-height: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.note { color: var(--muted); font-size: 12px; }
|
|
|
|
|
|
#missing { border-color: #6b552a; color: #f5b544; }
|
|
|
|
|
|
::-webkit-scrollbar {{ width: 10px; height: 10px; }}
|
|
|
|
|
|
::-webkit-scrollbar-thumb {{
|
|
|
|
|
|
background: #3a4150; border-radius: 5px; border: 2px solid var(--bg);
|
|
|
|
|
|
}}
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover {{ background: #6b7382; }}
|
|
|
|
|
|
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner {{ background: transparent; }}
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
_INDEX_TEMPLATE = """\
|
|
|
|
|
|
<!doctype html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
|
<title>SVFS 桥调试台</title>
|
|
|
|
|
|
<link rel="stylesheet" href="style.css">
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<main>
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<img src="icon.svg" width="44" height="44" alt="icon">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1>SVFS 桥调试台</h1>
|
|
|
|
|
|
<p id="version">正在连接宿主…</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="card" id="missing" hidden>
|
|
|
|
|
|
这个页面不在客户端里打开(没有检测到 svfs 桥)。
|
|
|
|
|
|
在 SVFS Client 中安装并启用本插件即可看到完整功能。
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="card">
|
|
|
|
|
|
<h2>宿主信息(host.info,无需权限)</h2>
|
|
|
|
|
|
<dl id="host-info"><dt>加载中…</dt></dl>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="card">
|
|
|
|
|
|
<h2>存储(storage 权限,按插件隔离)</h2>
|
|
|
|
|
|
<p>这个插件被打开了 <b id="count">…</b> 次</p>
|
|
|
|
|
|
<button id="reset">计数清零</button>
|
|
|
|
|
|
<button id="add10">再加 10 次</button>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<input id="kv-key" placeholder="键,如 greeting" size="14">
|
|
|
|
|
|
<input id="kv-value" placeholder="值(写入用)" size="18">
|
|
|
|
|
|
<button id="kv-get">读取</button>
|
|
|
|
|
|
<button id="kv-set">写入</button>
|
|
|
|
|
|
<button id="kv-del">删除</button>
|
|
|
|
|
|
<button id="kv-keys">列出全部键</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre id="storage-out" class="note">(操作结果显示在这里)</pre>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="card">
|
|
|
|
|
|
<h2>接口服务(net 权限,经宿主请求服务器 API,自动附带登录 token)</h2>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<select id="net-method">
|
|
|
|
|
|
<option>GET</option><option>POST</option>
|
|
|
|
|
|
<option>PUT</option><option>DELETE</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<input id="net-path" value="/api/ping" size="24">
|
|
|
|
|
|
<button id="net-send">发送请求</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<textarea id="net-body" placeholder="POST/PUT 的 body(字符串)"></textarea>
|
|
|
|
|
|
<pre id="net-out" class="note">未请求。相对 path 会拼在客户端配置的服务器地址后。</pre>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
2026-09-17 01:06:02 +08:00
|
|
|
|
<section class="card">
|
|
|
|
|
|
<h2>消息(messages 权限,服务端主动推送)</h2>
|
|
|
|
|
|
<p class="note">服务端按插件 id 定向推送的消息;这类消息不进宿主的消息中心。</p>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button id="msg-listen">开始订阅</button>
|
|
|
|
|
|
<button id="msg-clear">清空记录</button>
|
|
|
|
|
|
<span class="note">已收 <b id="msg-count">0</b> 条</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre id="msg-out" class="note">未订阅。</pre>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
2026-09-16 17:15:48 +08:00
|
|
|
|
<section class="card">
|
|
|
|
|
|
<h2>通知 / 标题(notify / title 权限)</h2>
|
|
|
|
|
|
<button id="notify">发一条原生通知</button>
|
|
|
|
|
|
<button id="paint">随机换个标题颜色</button>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="card">
|
|
|
|
|
|
<h2>原生对话框(dialog 权限,宿主弹出,Promise 返回用户选择)</h2>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button id="dlg-confirm">confirm(确定 / 取消)</button>
|
|
|
|
|
|
<button id="dlg-info">message · info</button>
|
|
|
|
|
|
<button id="dlg-warn">message · warn</button>
|
|
|
|
|
|
<button id="dlg-error">message · error</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre id="dialog-out" class="note">(用户的选择显示在这里)</pre>
|
|
|
|
|
|
<p class="note" id="changelog"></p>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
<script src="app.js"></script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
_APP_JS_TEMPLATE = """\
|
|
|
|
|
|
/* 桥调试台的前端逻辑:全部能力都走宿主注入的 window.svfs(Promise)。 */
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
|
|
var $ = function (id) { return document.getElementById(id); };
|
|
|
|
|
|
var PLUGIN_VERSION = "__PLUGIN_VERSION__";
|
|
|
|
|
|
|
|
|
|
|
|
function randomColor() {
|
|
|
|
|
|
var letters = "0123456789abcdef", out = "#";
|
|
|
|
|
|
for (var i = 0; i < 6; i++) out += letters[Math.floor(Math.random() * 16)];
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function show(id, text) {
|
|
|
|
|
|
$(id).textContent = text;
|
|
|
|
|
|
$(id).classList.remove("note");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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.authUser || "(未登录)"],
|
|
|
|
|
|
["服务器", info.baseUrl || "(未配置)"]]
|
|
|
|
|
|
.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);
|
|
|
|
|
|
};
|
|
|
|
|
|
$("kv-get").onclick = async function () {
|
|
|
|
|
|
try {
|
|
|
|
|
|
show("storage-out", "storage.get(" + JSON.stringify($("kv-key").value)
|
|
|
|
|
|
+ ") → " + JSON.stringify(await svfs.storage.get($("kv-key").value)));
|
|
|
|
|
|
} catch (e) { show("storage-out", "失败: " + e.message); }
|
|
|
|
|
|
};
|
|
|
|
|
|
$("kv-set").onclick = async function () {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var raw = $("kv-value").value;
|
|
|
|
|
|
var value;
|
|
|
|
|
|
try { value = JSON.parse(raw); } catch (e) { value = raw; }
|
|
|
|
|
|
await svfs.storage.set($("kv-key").value, value);
|
|
|
|
|
|
show("storage-out", "storage.set(" + JSON.stringify($("kv-key").value)
|
|
|
|
|
|
+ ", " + JSON.stringify(value) + ") → 成功");
|
|
|
|
|
|
} catch (e) { show("storage-out", "失败: " + e.message); }
|
|
|
|
|
|
};
|
|
|
|
|
|
$("kv-del").onclick = async function () {
|
|
|
|
|
|
await svfs.storage.remove($("kv-key").value);
|
|
|
|
|
|
show("storage-out", "storage.remove(" + JSON.stringify($("kv-key").value) + ") → 成功");
|
|
|
|
|
|
};
|
|
|
|
|
|
$("kv-keys").onclick = async function () {
|
|
|
|
|
|
show("storage-out", "storage.keys() → " + JSON.stringify(await svfs.storage.keys()));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ---- net:宿主代发 HTTP 请求(绕开 CORS,自动附带登录 token)----
|
|
|
|
|
|
$("net-send").onclick = async function () {
|
|
|
|
|
|
if (!svfs.net) {
|
|
|
|
|
|
show("net-out", "这个宿主没有 net 能力(需要客户端支持桥 API v2+)");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
show("net-out", "请求中…");
|
|
|
|
|
|
try {
|
|
|
|
|
|
var request = {
|
|
|
|
|
|
method: $("net-method").value,
|
|
|
|
|
|
path: $("net-path").value,
|
|
|
|
|
|
};
|
|
|
|
|
|
var body = $("net-body").value;
|
|
|
|
|
|
if (body && request.method !== "GET" && request.method !== "HEAD") {
|
|
|
|
|
|
request.body = body;
|
|
|
|
|
|
request.headers = { "Content-Type": "application/json" };
|
|
|
|
|
|
}
|
|
|
|
|
|
var res = await svfs.net.request(request);
|
|
|
|
|
|
show("net-out", "HTTP " + res.status + "\\n" + (res.bodyText || "(空响应)").slice(0, 600));
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
show("net-out", "请求失败: " + e.message
|
|
|
|
|
|
+ "\\n(先在「设置 → 服务器地址」里配置地址,服务端需提供对应接口)");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-17 01:06:02 +08:00
|
|
|
|
// ---- messages:订阅服务端推给本插件的消息(需要 messages 权限)----
|
|
|
|
|
|
var msgCount = 0;
|
|
|
|
|
|
var msgLines = [];
|
|
|
|
|
|
$("msg-clear").onclick = function () {
|
|
|
|
|
|
msgCount = 0; msgLines = [];
|
|
|
|
|
|
$("msg-count").textContent = "0";
|
|
|
|
|
|
show("msg-out", "(已清空记录)");
|
|
|
|
|
|
};
|
|
|
|
|
|
$("msg-listen").onclick = function () {
|
|
|
|
|
|
if (!svfs.messages) {
|
|
|
|
|
|
show("msg-out", "这个宿主没有 messages 能力(需要客户端支持桥 API v5+,"
|
|
|
|
|
|
+ "且清单里声明 messages 权限)");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
svfs.messages.subscribe(function (message) {
|
|
|
|
|
|
msgCount += 1;
|
|
|
|
|
|
$("msg-count").textContent = String(msgCount);
|
|
|
|
|
|
var body = message.json ? JSON.stringify(message.json) : message.text;
|
|
|
|
|
|
msgLines.unshift("[" + msgCount + "] " + body);
|
|
|
|
|
|
show("msg-out", msgLines.slice(0, 20).join("\\n"));
|
|
|
|
|
|
});
|
|
|
|
|
|
show("msg-out", "已订阅,等待服务端推送…(仪表盘数据这类业务载荷走的就是这条路)");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-16 17:15:48 +08:00
|
|
|
|
// ---- notify / title ----
|
|
|
|
|
|
$("notify").onclick = function () {
|
|
|
|
|
|
svfs.ui.notify("来自 " + info.pluginId + " 的通知(" + PLUGIN_VERSION + ")");
|
|
|
|
|
|
};
|
|
|
|
|
|
$("paint").onclick = function () {
|
|
|
|
|
|
svfs.ui.setTitle("SVFS Client · 调试台 " + randomColor());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 原生对话框(dialog 权限):Promise 返回用户是否点了"确定" ----
|
|
|
|
|
|
$("dlg-confirm").onclick = async function () {
|
|
|
|
|
|
var accepted = await svfs.ui.confirm({
|
|
|
|
|
|
title: "确认操作",
|
|
|
|
|
|
text: "这是一条宿主原生确认框(QMessageBox.question)。",
|
|
|
|
|
|
detail: "点「确定」返回 true,点「取消」或关闭返回 false。",
|
|
|
|
|
|
});
|
|
|
|
|
|
show("dialog-out", "ui.confirm → " + accepted);
|
|
|
|
|
|
};
|
|
|
|
|
|
$("dlg-info").onclick = async function () {
|
|
|
|
|
|
await svfs.ui.message({ title: "提示", text: "这是一条 info 级原生消息框。" });
|
|
|
|
|
|
show("dialog-out", "ui.message(info) → 已关闭");
|
|
|
|
|
|
};
|
|
|
|
|
|
$("dlg-warn").onclick = async function () {
|
|
|
|
|
|
await svfs.ui.message({ kind: "warn", title: "警告", text: "这是一条 warn 级原生消息框。" });
|
|
|
|
|
|
show("dialog-out", "ui.message(warn) → 已关闭");
|
|
|
|
|
|
};
|
|
|
|
|
|
$("dlg-error").onclick = async function () {
|
|
|
|
|
|
await svfs.ui.message({ kind: "error", title: "错误", text: "这是一条 error 级原生消息框。" });
|
|
|
|
|
|
show("dialog-out", "ui.message(error) → 已关闭");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
$("changelog").textContent = "__CHANGELOG__";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main();
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_source(dest_dir, version=1):
|
|
|
|
|
|
"""把示例插件源码树写到一个目录(升级对比/调试时也用得上)。"""
|
|
|
|
|
|
os.makedirs(dest_dir, exist_ok=True)
|
|
|
|
|
|
files = {
|
|
|
|
|
|
"plugin.json": json.dumps(_manifest(version), ensure_ascii=False, indent=2),
|
|
|
|
|
|
"index.html": _INDEX_TEMPLATE,
|
|
|
|
|
|
"style.css": _STYLE_CSS,
|
|
|
|
|
|
"icon.svg": _ICON_SVG,
|
|
|
|
|
|
"app.js": (
|
|
|
|
|
|
_APP_JS_TEMPLATE.replace("__PLUGIN_VERSION__", f"{version}.0.0")
|
|
|
|
|
|
.replace("__CHANGELOG__", CHANGELOG.get(version, ""))
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
for name, content in files.items():
|
|
|
|
|
|
with open(os.path.join(dest_dir, name), "w", encoding="utf-8", newline="\n") as f:
|
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
return dest_dir
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build(out_db, *, progress=None):
|
|
|
|
|
|
"""生成示例插件包;out_db 已存在时按真实升级流程追加快照。
|
|
|
|
|
|
|
|
|
|
|
|
版本自动决定:包不存在生成 1.0.0;已存在则升级到 2.0.0(再次生成则在
|
|
|
|
|
|
2.0.0 上追加 -2 快照……)。worker 的 ``install_sample`` 操作每次都调
|
|
|
|
|
|
这个(测试与体验用;界面入口已移除)。
|
|
|
|
|
|
|
|
|
|
|
|
返回 (out_db, 版本号字符串)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
version = 2 if os.path.exists(out_db) else 1
|
|
|
|
|
|
workdir = tempfile.mkdtemp(prefix="svfs-sample-")
|
|
|
|
|
|
# out_db 所在目录可能还不存在(首次生成时),建出来
|
|
|
|
|
|
os.makedirs(os.path.dirname(os.path.abspath(out_db)), exist_ok=True)
|
|
|
|
|
|
try:
|
|
|
|
|
|
write_source(workdir, version=version)
|
|
|
|
|
|
if version == 1:
|
|
|
|
|
|
vfs = SQLiteVFS(out_db, compress=True)
|
|
|
|
|
|
try:
|
|
|
|
|
|
vfs.create_snapshot(f"{version}.0.0")
|
|
|
|
|
|
vfs.add_vfs_metadata("svfs-plugin:com.svfs.demo")
|
|
|
|
|
|
packer = FolderPacker(vfs)
|
|
|
|
|
|
packer.pack_folder(workdir, incremental=True, rehash=True,
|
|
|
|
|
|
progress=progress)
|
|
|
|
|
|
vfs.recount()
|
|
|
|
|
|
finally:
|
|
|
|
|
|
vfs.close()
|
|
|
|
|
|
return out_db, f"{version}.0.0"
|
|
|
|
|
|
# 已有包:走与 installer._upgrade 相同的"派生快照 + 增量"流程
|
|
|
|
|
|
vfs = SQLiteVFS(out_db, compress=True)
|
|
|
|
|
|
try:
|
|
|
|
|
|
parent_id = vfs.get_stats()["snapshot_id"]
|
|
|
|
|
|
name = installer._unique_snapshot_name(vfs, f"{version}.0.0")
|
|
|
|
|
|
vfs.create_snapshot(name, parent_id=parent_id, copy_entries=True)
|
|
|
|
|
|
vfs.add_vfs_metadata("svfs-plugin:com.svfs.demo")
|
|
|
|
|
|
packer = FolderPacker(vfs)
|
|
|
|
|
|
packer.pack_folder(workdir, incremental=True, rehash=True,
|
|
|
|
|
|
progress=progress)
|
|
|
|
|
|
vfs.recount()
|
|
|
|
|
|
finally:
|
|
|
|
|
|
vfs.close()
|
|
|
|
|
|
return out_db, name
|
|
|
|
|
|
finally:
|
|
|
|
|
|
shutil.rmtree(workdir, ignore_errors=True)
|