存储、文件与网络

适用版本:StablEdge 桌面端 0.4.4

工作区界面没有文件系统和网络的直接访问权。持久化用宿主提供的 KV 和 Blob 存储,导入导出用系统对话框或拖放,联网由宿主代理。

存储分层

数据使用不要
少量设置、有界索引、blob_idstorage.kv存 Base64、全文、缩略图、无界数组
图片、文档、音视频、大块状态storage.blob把会话内的 src 当作持久地址
用户可见的备份和导入files.dialog获取或拼接系统路径

上限:KV 单项 64 KiB、每个插件总量 1 MiB;Blob 单个默认 64 MiB、每个插件总量默认 512 MiB。用户可以在「设置 → 插件」中调整 Blob 上限(单个 1–256 MiB,总量 64 MiB–8 GiB)。运行时请读 storage.getUsage() 返回的 blob_item_limit_bytes / blob_total_limit_bytes,不要把默认值写死在插件里。

KV

const state = await stabledgePlugin.storage.get("state");
await stabledgePlugin.storage.set("state", nextState);
await stabledgePlugin.storage.delete("state");
const usage = await stabledgePlugin.storage.getUsage();

getUsage() 返回 kv_used_bytes、kv_total_limit_bytes、kv_item_limit_bytes、blob_used_bytes、blob_total_limit_bytes、blob_item_limit_bytes。用量检查只用于提前提示,真正写入时仍要处理宿主返回的配额错误。

大型可编辑对象建议采用“Blob 存正文 + KV 存索引”:

  1. 写入新 Blob;
  2. 回读并校验;
  3. 更新 KV 索引,指向新的 blob_id;
  4. 成功后删除旧 Blob;
  5. 失败时保留旧索引,清理新产生的孤儿 Blob。

Blob

const ref = await stabledgePlugin.storage.blob.put(blob, {
  media_type: "application/json",
  filename: "asset.json"
});

const reopened = await stabledgePlugin.storage.blob.resolve(savedBlobId);
const all = await stabledgePlugin.storage.blob.list();
await stabledgePlugin.storage.blob.delete(ref.blob_id);

Blob 引用

字段类型说明
blob_idstring持久标识,只保存它和必要的元数据。
srcstring当前会话内的同源临时网址,关闭重开后会变化,不要持久化。
media_typestringMIME 类型。
size_bytesnumber字节数。
filenamestring | null原始文件名。
created_atnumber创建时间(Unix 秒)。

读取 Blob

Bridge 没有 storage.blob.get 之类的读取方法,读取一律通过 src 发起同源请求,图片和媒体可以直接设置元素的 src:

const current = await stabledgePlugin.storage.blob.resolve(blobId);
const response = await fetch(current.src);
if (!response.ok) throw new Error(`blob read failed: ${response.status}`);
const value = await response.json();

img.src = current.src;

允许存入的类型:图片和音频(按文件头识别)、application/pdf、application/zip、application/gzip,以及 application/json 和 text/plain(必须是 UTF-8,JSON 必须可解析)。不允许 SVG 和 HTML。

文件拖放

在清单工作区中声明 drops,同时申请 storage.blob。用户把文件拖入工作区(包括从应用内文件树拖拽)时,宿主整批校验扩展名、数量、大小和类型,写入插件 Blob 后发送事件:

stabledgePlugin.events.on("host.files_dropped", async ({ files }) => {
  for (const file of files) await importBlob(file);
});

插件只收到 Blob 引用,拿不到系统路径。任何一个文件不合规时整批拒绝,不会发送“部分成功”的事件。

系统文件对话框

申请 files.dialog(建议设为可选),并同时申请 storage.blob:

const opened = await stabledgePlugin.files.openFile({
  extensions: ["json", "png"],
  max_bytes: 10 * 1024 * 1024,
  title: "导入文件"
});
if (!opened.cancelled) await importBlob(opened.file);

const exported = await stabledgePlugin.files.exportBlob(ref, {
  suggested_filename: "backup.json"
});
if (!exported.cancelled) showSaved(exported.bytes_written);

openFile 的 extensions 只能取以下白名单的子集:png jpg jpeg gif webp avif bmp wav mp3 flac ogg m4a pdf zip gz gzip json txt。用户取消是正常结果。目前没有持久的目录授权,也没有在授权目录内按相对路径读写的接口。

iframe 内容安全策略

宿主为工作区 iframe 注入以下 CSP:

default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline';
img-src 'self' data: https:; font-src 'self'; connect-src 'self'; media-src 'self';
object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'

也就是说:脚本、字体、媒体只能来自包内文件和 Blob src;<img> 额外允许 data: 和任意 https: 图片;fetch 只能请求自身源(即 Blob src),外部请求一律走 net.fetch;不能嵌入子 iframe,不能提交表单。

网络请求

界面侧和 Worker 侧有各自的网络路径,权限归属不同,不能混用:

调用方API权限归属范围
工作区 / 顶部栏页面stabledgePlugin.net.fetch"worker": "host"http/https 地址
WorkercallHost("host.net.fetch", …)真实 Worker id需要该 Worker 的 net.fetch 权限,协议为 http/https;带凭据时必须 https 且命中 allowed_origins

界面侧 net.fetch

const response = await stabledgePlugin.net.fetch("https://api.example.com/v1/items", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ q: "hello" }),
  response_type: "json",
  timeout_ms: 15_000
});
if (response.status === 200) render(response.body);
选项说明
methodGET、HEAD、POST、PUT、PATCH、DELETE。
headers请求头。
body请求体字符串。
body_encodingutf8(默认)、base64 或 binary_base64。后两种先解码再按字节发送,用于上传图片或 multipart,解码后不超过 32 MiB。
response_typetext(默认)、json、base64、blob。blob 需要当前有活动的工作区和 storage.blob。
timeout_ms超时时间。
max_bytes单次响应上限,宿主的硬上限(32 MiB)仍然生效。response_type 为 blob 时,还受用户设置的单个 blob 上限约束。

响应包含 status、headers 和 body_type:text / json / base64 时内容在 body 中,blob_ref 时在 blob 中。宿主不跟随重定向,不共享浏览器 Cookie。

  • 界面侧会做以下检查:插件已启用、清单声明了 host 的 net.fetch、用户已授权、网址是 http/https。scope 和 connect 只用于安装页展示,不是安全边界。
  • 默认只能访问公网,访问局域网和本机服务需要单独申请权限,安装时由用户单独确认,之后可以撤销;界面侧和 Worker 侧规则相同,见局域网与本机地址。
  • 服务地址由用户自行配置时,不要把地址写死在 connect 或 scope 中,省略它们即可;改地址不需要重新打包。
  • 未声明、未授权或插件未启用时返回 PLUGIN_HOST_SERVICE_DENIED,不可重试;网址不是 http/https 时返回 PLUGIN_HOST_NET_DENIED。

Worker 侧 host.net.fetch

宿主检查以下几项:

  • 该 Worker 没有相应的 net.fetch 权限时,返回 PLUGIN_WORKER_PERMISSION_DENIED;
  • 协议不是 http 或 https 时,返回 PLUGIN_HOST_NET_DENIED;
  • 默认只能访问公网,访问局域网和本机服务需要单独申请权限,安装时由用户单独确认,之后可以撤销,见局域网与本机地址。

带 credential_refs 的请求必须用 https 并通过 allowed_origins 校验,否则返回 PLUGIN_CREDENTIAL_ORIGIN_DENIED。错误统一以 JSON-RPC -32010 返回,message 格式为 CODE:msg。详见 Worker · 网络。界面侧的 host 授权不能让 Worker 联网,反之亦然。

局域网与本机地址

界面侧的 net.fetch、Worker 侧的 host.net.fetch,以及 events.connect 建立的连接目标(SSE、WebSocket、Webhook),默认都拦截以下地址。要访问它们,需要在清单中声明 net.local,并由用户授权。

  • IPv4:0.0.0.0/8、10.0.0.0/8、127.0.0.0/8、169.254.0.0/16、172.16.0.0/12、192.168.0.0/16、100.64.0.0/10、192.0.0.0/24、组播 224.0.0.0/4、保留 240.0.0.0/4、广播地址。
  • IPv6:::、::1、fc00::/7、fe80::/10、ff00::/8。::ffff: 映射地址和 64:ff9b:: 地址按其中嵌入的 IPv4 地址判定。
  • 主机名:localhost、*.localhost、*.local。

198.18.0.0/15:网址中直接写这一段的 IP 时拒绝;域名解析到这一段时放行,以兼容代理软件的 Fake-IP 模式。

  • DNS:请求前宿主先在本地解析域名。解析结果中只要有一个地址被拒,整个请求就被拒绝;通过后,用这次解析的结果建立连接。
  • 重定向:宿主不自动跟随,3xx 响应原样返回给插件。插件按 Location 重新请求时,宿主会重新判定目标地址。
  • 云元数据地址:169.254.169.254 和 fd00:ec2::254 一律拒绝,即使已获得 net.local 也不例外。
  • 错误码:目标是局域网或本机地址、而插件没有 net.local 权限时,返回 PLUGIN_HOST_NET_PRIVATE_TARGET。Worker 侧按惯例包在 JSON-RPC -32010 中,message 为 PLUGIN_HOST_NET_PRIVATE_TARGET:…。

宿主服务

services.invoke 只能调用宿主已登记的服务和方法,例如读取聚合系统指标:

const sample = await stabledgePlugin.services.invoke("host.system_metrics", "sample", {});

对应权限为 { "permission": "system.metrics.read", "worker": "host" }。它不是通用的命令通道,也不能用来转发 Worker operation。

打开外链

申请 ui.open_external,并在真实的用户点击中调用:

button.addEventListener("click", () =>
  stabledgePlugin.ui.openExternal("https://example.com/help")
);

只接受绝对的 http(s) 网址。不要使用 window.open、顶层跳转、javascript:、file: 或带用户名密码的网址。

凭据与日志

  • 长期凭据不要放进插件包、KV、快照、网址或前端源码;需要 API 密钥或 OAuth 时,使用清单的 credentials,由宿主保管和注入。
  • 不要把 API 密钥、Cookie、Authorization、完整查询串或响应正文写进日志。
  • 本机服务不可达时要显示离线或重连状态,不要让工作区白屏。
本页目录