15 KiB
第三方前端扩展兼容现状
本文档描述 当前已经落地 的第三方前端扩展兼容实现,用于指导后续持续开发。
补充:目前已落地浏览器资源契约:
- 头像:
/thumbnail、/characters/*、/User Avatars/*,并移除缩略图 DOM monkey patch - 用户静态资源:
/backgrounds/*、/assets/*、/user/images/*、/user/files/*
1. 范围与结论
当前兼容目标是 SillyTavern 风格的 纯前端 third-party extension,即依赖:
/scripts/extensions/third-party/<ext>/<path>这一同源静态资源前缀- 浏览器原生子资源加载语义
当前方案的核心结论是:
兼容性的关键不是继续在前端 runtime 里“解释扩展代码”,而是把 third-party 资源路径重新做成 WebView 可原生加载的真实端点。
2. 当前链路
2.1 发现与激活
src/scripts/extensions.js中的startOfflineExtensionsDiscovery()在 Host Ready 后请求/api/extensions/discoversrc/tauri/main/routes/extensions-routes.js将请求转给 Rust 命令get_extensionsExtensionService -> tt-adapter-extension::FileExtensionRepository::discover_extensions()- 返回扩展列表后,前端继续读取 manifest 并缓存激活计划
- 启动期先执行
activateStartupSystemExtensions(),只激活系统扩展 - 若存在启用中的 local/global third-party 扩展,则在
APP_READY后执行activateDeferredThirdPartyExtensions()
补充约束:
- discovery 只投影扩展目录、类型与 Git/source 管理状态,不读取或建模 manifest;单个损坏的 legacy source 状态只会使对应扩展降为 unmanaged。
- 浏览器运行时 manifest 由前端从原始
manifest.json读取并消费,包括display_name/version/author与js/css/i18n等字段;缺失、损坏或非 object manifest 以及单扩展激活异常都不会阻断其他扩展。 - install/update/switch 的 candidate preflight 仍要求根
manifest.json存在、是普通文件且为 JSON object;display_name/version/author仅作为安装结果展示元数据提取,缺失或非字符串值不影响候选合法性。
扩展命名约定:
- 系统扩展:
regex、quick-reply等 - 第三方扩展:统一命名为
third-party/<folder>
当前启动时序约束:
- third-party 扩展 discovery 可以提前,但 local/global third-party 模块求值不再阻塞
APP_READY APP_READY发出后,晚加载的 third-party 扩展仍可依赖eventSource对APP_READY的 auto-fire 语义完成 ready 钩子EXTENSION_SETTINGS_LOADED在存在待激活 third-party 扩展时会延后到 deferred activation 完成后再发出
2.2 安装、版本、分支与更新
第三方扩展管理已使用 Rust 原生 gitoxide 纵切面:
- 新安装只接受匿名
http(s)Git remote;不限制 GitHub、GitLab、Gitee,也不接受 provider Web tree URL、query/fragment、URL userinfo、SSH 或 credential helper。 - 未指定 ref 时使用 remote 的 born symbolic
HEAD;显式 ref 按 branch-first、tag-second 解析。任意 revision expression/OID 不属于当前输入契约。 - 新安装直接生成标准 non-bare embedded
.git/,不再生成_tauritavern/extension-sourcesJSON。branch 使用 symbolic HEAD + upstream;tag 使用 detached peeled HEAD + exact tag refspec。 - version 只做 Git ref advertisement并比较 peeled remote OID 与 deployed HEAD,不 fetch、不写本地、不调用 provider REST API。
- branches 只通过
ls-refs refs/heads/返回排序后的唯一远端 branch 短名、7 位 OID、current 与空 label;不 unshallow、不下载 branch object、不修改本地状态。 - switch 只接受远端 branch(兼容
main与origin/main),执行 exact depth-1 fetch、candidate preflight 和受管 worktree重建,最后提交标准 symbolic HEAD/upstream;切换当前 branch 是无网络 no-op,不同 branch指向当前 deployed OID时只切换refs/config,且不暗含 update hook。 - update 对 embedded repo 只做一次 exact depth-1 fetch;OID 相同不触碰 payload,OID 不同则先验证完整 candidate tree,再重建 payload/index,最后推进 branch ref 或 detached HEAD。
- legacy JSON extension 的 version/branches 只读 Git advertisement;central source JSON优先,早期扩展根目录内的 inline V1/V2 JSON作为只读 fallback,不在启动或读取时搬迁。首次 update(即使 OID 相同)或切换到不同 branch 时,在 sibling
.tmp-*中建立标准 embedded repo,验证完成后替换活动 snapshot并删除或自然淘汰 JSON。失败保留旧 snapshot与来源状态。 - provider REST、archive ZIP 与 host allowlist 后端已删除;所有 Git 错误均 fail-fast,不存在 REST/ZIP runtime fallback。
candidate preflight 会在删除活动 payload 之前验证完整 tree:portable UTF-8 path、大小写/NFC collision、file/directory shape、ODB object header kind、根 manifest.json 的 JSON object 结构。checkout 不执行 external filter/LFS/submodule;symlink 以普通文件物化,gitlink 只生成空目录占位。
install/update/switch/delete/move 与 LAN/TT Sync 的本地写操作共享一个 application-layer fail-fast permit;busy 映射为 409。discovery/version/branches 是只读操作,不占 permit。update 前端不会再用 AbortSignal 假取消已经进入 Rust 的磁盘写操作。
2.3 前端资源加载
资源 URL 由 src/scripts/extensions/runtime/resource-paths.js 统一生成:
getExtensionResourceUrl(name, path)- 对 third-party 扩展,最终 URL 为
/scripts/extensions/third-party/<folder>/<path>
激活时:
- JS 入口由
asset-loader.js直接作为<script type="module" src="...">注入 - CSS 默认直接
<link rel="stylesheet" href="...">加载 - 只有旧 WebView 不支持 CSS
@layer时,third-party-runtime.js才会为样式 URL 附加ttCompat=layer,并由 Rust 端点返回展平后的 CSS bytes js/css字段显式接受string或单元素string[];不为多元素数组建立新的加载顺序语义
额外兼容层:
src/lib.js会把部分上游常用库挂到window;其中window._(lodash)是正式兼容 ABI,因为 JS-Slash-Runner、ST-Prompt-Template、MagVarUpdate 等生态扩展会在模块求值阶段直接访问_src/tauri/main/compat/mobile/mobile-runtime-compat.js负责旧 WebView 缺失 JS API 的 polyfills(仅 Tauri mobile)- 第三方浮层/窗口 mobile surface compat(仅 Tauri mobile):
- 分类/契约输出:
src/tauri/main/compat/mobile/mobile-overlay-surface-admission.js - 观察与有界 settle window:
src/tauri/main/compat/mobile/mobile-overlay-compat-controller.js - 同源 iframe contract bridge:
src/tauri/main/compat/mobile/mobile-iframe-viewport-contract-bridge.js
- 分类/契约输出:
src/scripts/browser-fixes.js保持与上游同步(不再承载 Tauri mobile compat)
2.4 后端资源提供
生产/打包运行时:
src-tauri/crates/tauritavern/src/lib.rs在主窗口安装on_web_resource_requestsrc-tauri/crates/tt-application/src/services/host_resource_service/user_css.rs处理/css/user.csssrc-tauri/crates/tt-application/src/services/host_resource_service/third_party.rs处理/scripts/extensions/third-party/*src-tauri/crates/tt-application/src/services/host_resource_service/thumbnail.rs处理/thumbnailsrc-tauri/crates/tt-application/src/services/host_resource_service/user_data.rs处理用户数据静态资源:/characters/*、/User Avatars/*、/backgrounds/*、/assets/*、/user/images/*、/user/files/*
请求处理步骤:
- 校验请求方法,只接受
GET/HEAD/OPTIONS - 通过
src-tauri/crates/tt-contracts/src/client_asset_paths.rs解析并校验路径,再由src-tauri/crates/tt-application/src/services/host_resource_service/route_classifier.rs分类到具体资源处理器 - 通过
src-tauri/crates/tt-adapter-media/src/host_resources.rs一次完成 local/global 选源、打开文件和 metadata/revision 构造 - 返回真实 bytes、正确
Content-Type、Cache-Control: private, no-cache、weak ETag 和 Last-Modified;条件命中时按 transport capability 返回 304 或完整 200(见docs/CurrentState/HostResourceCaching.md)- 对用户静态资源端点(如
/backgrounds/*)若请求携带Range,支持单范围并返回206 + Content-Range(见docs/CurrentState/MediaAssetContract.md)
- 对用户静态资源端点(如
- 未命中时返回真正
404,不回退到index.html
Host Resource 只校验浏览器 URL 的路径段,不禁止 data root 内部 symlink 指向外部目录或文件。这样用户可以让 SillyTavern 与 TauriTavern 共享同一套数据目录布局。
.git 是有意允许的路径组件,不是 Host Resource 或 TT-Sync 的名称级禁区。SillyTavern 迁移来的扩展可以携带标准 embedded .git/;显式请求 /scripts/extensions/third-party/<folder>/.git/config、.git/HEAD 等文件时走与其他扩展文件相同的 local-first、文件级读取与缓存流程,仍不提供目录浏览。TT-Sync 的 extensions.local / extensions.third_party 数据集也不会按 .git 名称排除这些路径。managed extension 根 .git 在仓库层是标准 Git 管理目录;这不改变路径层继续拒绝 .、..、编码分隔符、控制字符和路径逃逸。
这项兼容语义不代表 remote URL 可以保存秘密。当前第三方扩展本来就是同源可执行代码;任何 embedded .git/config 都必须只持久化脱敏 URL,私有仓库认证若未来支持,应使用设备本地 secret store。
开发态本地 Web 入口:
scripts/tauri-dev-server.mjs会在入口文档最前面注入src/dev-sw-bootstrap.js;新 WebView 会话若继承旧 Service Worker controller,会先注销旧注册并重载一次,避免跨宿主进程复用失效的 Wry 协议过滤器src/init.js会注册/tt-ext-sw.js- Service Worker 将
/css/user.css、/scripts/extensions/third-party/*、/thumbnail、/characters/*、/User Avatars/*、/backgrounds/*、/assets/*、/user/images/*、/user/files/*转发到tt-ext自定义 scheme - Rust 侧
register_uri_scheme_protocol("tt-ext", ...)在 dev 下统一分发上述资源请求 convertFileSrc('', 'tt-ext')的结果可能因平台/WebView 不同而表现为tt-ext://localhost/或http(s)://tt-ext.localhost/- Service Worker 的直接
tt-extfetch 保留原请求 cache mode;Range、If-Range、If-None-Match、If-Modified-Since在直连或 IPC 路径中保持同一 Host Resource 语义,并通过Access-Control-Expose-Headers: *保留直连响应的完整表示头 If-Range、If-None-Match、If-Modified-Since会触发 custom scheme 跨源 preflight,必须直接通过通用 header wire 进入 IPC;其余 Service Workerfetch(tt-ext)失败时切换到 window-contextfetch(tt-ext),正文以 transferableArrayBuffer返回。普通二进制资源不得经过 serde JSON IPC- 非 Android 的 production、
tt-ext与 IPC fallback 都能代理 304;Android 按 Wry 硬约束返回完整 200。IPC 不另建 Cache API,因此自动生成 validator 的存储行为仍由具体 transport 决定
因此,开发态与生产态虽然入口不同,但 third-party 路径语义保持一致。
3. 数据目录与优先级
当前目录布局是:
- local third-party 扩展:
data/default-user/extensions/<folder> - global third-party 扩展:
data/extensions/third-party/<folder> - legacy 扩展来源元数据:优先读取
data/_tauritavern/extension-sources/{local|global}/;兼容更早版本的<extension>/.tauritavern-source.json只读输入(新 embedded install 不再写入)
当前优先级规则:
- 发现时:若 local 与 global 同名,保留 local,跳过 global
- 读资源时:先查 local,再查 global
这意味着 local 扩展可以覆盖同名 global 扩展。
4. 当前已支持的兼容边界
当前目标是恢复 SillyTavern third-party 扩展依赖的“静态资源契约”。因此下列路径应按浏览器默认语义工作:
<script type="module" src="..."><link rel="stylesheet" href="...">- ESM 相对导入
fetch('/scripts/extensions/third-party/...')- CSS
url(...) - iframe 页面及其相对资源
/css/user.css作为用户 CSS 覆盖文件/thumbnail、/characters/*、/User Avatars/*作为头像相关的浏览器原生子资源端点/backgrounds/*、/assets/*、/user/images/*、/user/files/*作为用户静态资源的浏览器原生子资源端点
当前安全约束:
- 拒绝缺失扩展目录、
.、.. - 拒绝编码后的路径分隔符等非法路径
- 相对资源路径中的冗余
/仅做等价归一化,不扩大访问范围 .git、.Git等名称按普通路径段接受;不要与./..混同- 只允许 third-party 前缀内的文件级读取,不提供目录浏览
5. 当前明确不支持或不承诺的内容
- 不支持 SillyTavern 的 Node-only backend plugins
- 不提供通用“前端伪静态服务器”或任意文件读取能力
- 不支持 private auth、SSH、credential helper、Git hook/external filter/LFS/submodule执行或递归 clone
- 不支持 gitfile/linked worktree、外部 common dir、外部
core.worktree等非本地 Git 布局;管理操作会 fail-fast 并要求重新安装,不自动修复或读取 stale JSON fallback - branch UI 不提供 local-only branch、tag/OID/revision switch、commit subject label、unshallow或完整历史
- 根
.git/和 legacy source JSON 均不存在的扩展仍可被发现和加载,但update会要求重新安装 - third-party runtime 不再负责通用 JS 源码重写,不应再把它扩展回“大而全解释器”
6. 持续开发约束
后续若继续改 third-party 兼容,先问三个问题:
- 这是浏览器资源契约没有做对,还是某个平台运行时缺能力?
- 这个问题应该修在前端加载编排层,还是应该修在后端资源端点?
- 这个修复会不会重新把系统带回“前端模拟服务器”的方向?
推荐维护原则:
- 保持
/scripts/extensions/third-party/*作为唯一资源契约,不轻易改路径 - 不要把
/api/*请求拦截和 third-party 静态资源端点混回同一层 - 新兼容修复优先做成“最小能力补丁”,不要重新引入广泛源码扫描或 eager 预取
- 若调整路径规则,至少同步检查:
src/scripts/extensions/runtime/resource-paths.jssrc-tauri/crates/tt-contracts/src/client_asset_paths.rssrc-tauri/crates/tt-application/src/services/host_resource_service/route_classifier.rssrc-tauri/crates/tt-application/src/services/host_resource_service/third_party.rs- 相关测试
- 若改动开发态代理链路,也必须同步验证
src/dev-sw-bootstrap.js、src/init.js与src/tt-ext-sw.js
7. 建议的最小回归面
每次调整后,至少回归以下几类能力:
- third-party 扩展可发现、可启用
manifest.json、JS、CSS、图片/字体资源都能正确加载- 显式
.git/HEAD或无秘密 fixture 的.git/config与普通文件采用相同路径语义 - 不存在的资源返回 404,而不是 HTML fallback
- local/global 同名时仍保持 local 优先
- 旧 WebView 下的 CSS
@layer降级没有回归
如果一个问题已经超出以上边界,应先判断它是否属于“third-party 前端扩展兼容”范畴,再决定是否继续在这条链路上处理。