mirror of
https://github.com/Darkatse/TauriTavern.git
synced 2026-10-19 07:35:14 +00:00
fix(clipboard): route writes through native plugin #190
This commit is contained in:
parent
ae9b24deab
commit
6c0f7359d9
14 changed files with 291 additions and 16 deletions
|
|
@ -257,7 +257,9 @@ Android AI 生成使用任务级 `dataSync` Foreground Service。Rust `ChatCompl
|
|||
|
||||
以下问题仅在 Android 旧 WebView 上高概率出现,桌面端通常不复现。
|
||||
|
||||
### 7.1 `*.at is not a function`
|
||||
### 7.1 JavaScript 运行时兼容
|
||||
|
||||
#### `*.at is not a function`
|
||||
|
||||
现象:
|
||||
|
||||
|
|
@ -273,7 +275,13 @@ Android AI 生成使用任务级 `dataSync` Foreground Service。Rust `ChatCompl
|
|||
- 在 Tauri mobile 启动期安装运行时兼容层:
|
||||
- 实现:`src/tauri/main/compat/mobile/mobile-runtime-compat.js`
|
||||
- 入口:`src/tauri/main/bootstrap.js`(仅 Android/iOS UA)
|
||||
- 行为:仅补齐缺失 API,且只执行一次;桌面端/移动端 Web 不启用。
|
||||
- 行为:基础 API 仅在缺失时补齐,且只执行一次;桌面端/移动端 Web 不启用。
|
||||
|
||||
#### Web Clipboard 写入被拒绝
|
||||
|
||||
Android WebView 可能暴露 `navigator.clipboard.writeText()`,却在调用时以 `NotAllowedError` 拒绝写入。TauriTavern 第一方复制在所有平台统一走 `writeClipboardText()`;Android 兼容层只把上游 Web Clipboard 的 `writeText` 映射到同一原生写入器,并保留 Clipboard 对象上的其他方法。
|
||||
|
||||
原生侧只授予 `clipboard-manager:allow-write-text`。写入剪贴板不需要 Android manifest 或运行时权限,也不开放读取能力;失败直接返回调用方,不静默回退。
|
||||
|
||||
### 7.2 插件面板样式大面积失效(如 `TH-custom-tailwind` 布局错乱)
|
||||
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ TauriTavern 自有的新 UI 可以作为 SillyTavern first-party extension 挂
|
|||
|
||||
- 实现位置:`src/tauri/main/compat/mobile/mobile-runtime-compat.js`。
|
||||
- 入口:`src/tauri/main/bootstrap.js` 中安装(仅 Tauri mobile)。
|
||||
- 行为:仅补齐缺失 API,且只执行一次。
|
||||
- 行为:基础 API 仅在缺失时补齐;Android 的 Web Clipboard 写入统一映射到原生写入器;整体只执行一次。
|
||||
- 当前按需补齐:
|
||||
- `Array.prototype.at`
|
||||
- `String.prototype.at`
|
||||
|
|
@ -253,8 +253,9 @@ TauriTavern 自有的新 UI 可以作为 SillyTavern first-party extension 挂
|
|||
- `Array.prototype.toSorted`
|
||||
- `Array.prototype.toReversed`
|
||||
- `Object.hasOwn`
|
||||
- `navigator.clipboard.writeText`(仅 Android;保留 Clipboard 对象上的其他方法)
|
||||
|
||||
该策略用于修复移动端第三方插件在初始化阶段出现的 `TypeError: *.at is not a function`。
|
||||
该策略用于修复移动端第三方插件在初始化阶段出现的 `TypeError: *.at is not a function`,以及 Android WebView 拒绝 Web Clipboard 写入的问题。TauriTavern 第一方代码在所有平台直接使用 `src/tauri-bridge.js` 的原生写入器;宿主只授予 `clipboard-manager:allow-write-text`,不授予剪贴板读取能力,也不需要申请操作系统运行时权限。
|
||||
|
||||
#### 7.7.2 CSS `@layer` 降级(Android 旧 WebView)
|
||||
|
||||
|
|
@ -291,6 +292,8 @@ TauriTavern 自有的新 UI 可以作为 SillyTavern first-party extension 挂
|
|||
|
||||
- 若看到 `*.at is not a function`:
|
||||
- 检查是否为 Tauri mobile 会话,并确认 `window.__TAURITAVERN_MOBILE_RUNTIME_COMPAT__ === true`。
|
||||
- 若 Android 复制失败:
|
||||
- 检查 `plugin:clipboard-manager|write_text` invoke 的拒绝原因;该链路不会静默回退到 Web Clipboard。
|
||||
- 若插件样式错乱但 CSS 已成功请求:
|
||||
- 优先检查是否命中 `@layer` 降级分支;
|
||||
- 关注 `resolveStylesheetUrl()` 是否返回带 `ttCompat=layer` 的 URL。
|
||||
|
|
@ -372,7 +375,7 @@ TauriTavern 自有的新 UI 可以作为 SillyTavern first-party extension 挂
|
|||
- 常用导出命令:
|
||||
- `window.__TAURITAVERN_PERF__.downloadReport()` 下载 JSON(便于交给 AI 分析)
|
||||
- `window.__TAURITAVERN_PERF__.exportJson({ includeResources: true })` 直接拿到 JSON 字符串
|
||||
- `await window.__TAURITAVERN_PERF__.copyReport()` 复制到剪贴板(若可用)
|
||||
- `await window.__TAURITAVERN_PERF__.copyReport()` 通过原生写入器复制到剪贴板
|
||||
- HUD 操作:拖动标题栏移动(位置持久化),点击标题栏展开/收起;桌面端可用 `Ctrl+Alt+P` 切换开关。
|
||||
|
||||
## 11. 工程守护(Guardrails + 类型检查)
|
||||
|
|
|
|||
|
|
@ -326,10 +326,13 @@ header 名也可从 `window.__TAURITAVERN__?.traceHeader` 获取(用于避免
|
|||
|
||||
- `window.__TAURITAVERN_MOBILE_RUNTIME_COMPAT__`
|
||||
- 覆盖移动端旧 WebView 的基础 polyfills(例如 `requestIdleCallback` / `cancelIdleCallback`)。
|
||||
- Android 的 `navigator.clipboard.writeText()` 映射到宿主原生写入器,same-origin iframe 同样适用;Clipboard 对象上的其他方法保持不变。
|
||||
- `window.__TAURITAVERN_MOBILE_OVERLAY_COMPAT__`
|
||||
- `window.__TAURITAVERN_MOBILE_IFRAME_VIEWPORT_CONTRACT_BRIDGE__`:same-origin iframe 的 viewport/inset contract bridge(用于 `viewport-host` boundary;主要用于 debug/幂等安装)
|
||||
- `window.__TAURITAVERN_MOBILE_WINDOW_OPEN_COMPAT__`:移动端外链 `window.open()` 通过系统浏览器打开(不创建应用内新窗口)
|
||||
|
||||
TauriTavern 第一方功能在所有平台直接使用同一个原生剪贴板写入器。该契约只授予 `clipboard-manager:allow-write-text`,不包含读取/清空/图片等权限;写入失败必须向调用方传播。
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Dialog 兼容(Public in practice)
|
||||
|
|
|
|||
147
src-tauri/Cargo.lock
generated
147
src-tauri/Cargo.lock
generated
|
|
@ -67,6 +67,27 @@ version = "1.0.104"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"image",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.60.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.2"
|
||||
|
|
@ -831,6 +852,15 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clru"
|
||||
version = "0.6.3"
|
||||
|
|
@ -1480,7 +1510,7 @@ checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
|
|||
dependencies = [
|
||||
"bit-set",
|
||||
"cssparser",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"html5ever",
|
||||
"precomputed-hash",
|
||||
"selectors",
|
||||
|
|
@ -1726,6 +1756,12 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
|
||||
|
||||
[[package]]
|
||||
name = "esaxx-rs"
|
||||
version = "0.1.10"
|
||||
|
|
@ -1801,6 +1837,12 @@ version = "2.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "fax"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
|
|
@ -1888,6 +1930,12 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
|
|
@ -3349,6 +3397,15 @@ version = "0.12.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
|
|
@ -3357,7 +3414,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
|||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
|
@ -3370,7 +3427,7 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
|||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
|
@ -3792,6 +3849,7 @@ dependencies = [
|
|||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
|
@ -4839,6 +4897,7 @@ dependencies = [
|
|||
"block2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
|
|
@ -5115,6 +5174,16 @@ version = "1.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89722d987db848624cf8fca21245d59cf6382b01b4ca79ae70261cea5747495"
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.2"
|
||||
|
|
@ -5213,6 +5282,17 @@ dependencies = [
|
|||
"indexmap 2.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap 2.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
|
|
@ -5501,7 +5581,7 @@ dependencies = [
|
|||
"log",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
"petgraph",
|
||||
"petgraph 0.7.1",
|
||||
"prettyplease",
|
||||
"prost",
|
||||
"prost-types",
|
||||
|
|
@ -7182,6 +7262,21 @@ dependencies = [
|
|||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.7.2"
|
||||
|
|
@ -7457,6 +7552,7 @@ dependencies = [
|
|||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-barcode-scanner",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-notification",
|
||||
|
|
@ -7564,6 +7660,20 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
||||
dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken-rs"
|
||||
version = "0.12.0"
|
||||
|
|
@ -7995,6 +8105,17 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree_magic_mini"
|
||||
version = "3.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"nom 8.0.0",
|
||||
"petgraph 0.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
|
|
@ -9474,6 +9595,24 @@ version = "0.57.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wl-clipboard-rs"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"os_pipe",
|
||||
"rustix",
|
||||
"thiserror 2.0.20",
|
||||
"tree_magic_mini",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-wlr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ tauri-plugin-opener = "2.5.4"
|
|||
tauri-plugin-fs = "2.5.1"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tauri-plugin-dialog = "2.7.2"
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-destroy",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"opener:default",
|
||||
"notification:allow-notify",
|
||||
"notification:allow-is-permission-granted",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub(super) fn install<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::B
|
|||
// Moving desktop/mobile plugins into setup would make capability availability
|
||||
// depend on runtime initialization order instead of Builder construction.
|
||||
let builder = builder
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { POPUP_TYPE } from '../../popup.js';
|
||||
import { translate } from '../../i18n.js';
|
||||
import { trimFrontendLogEntriesInPlace } from '../../../tauri/main/services/dev-logging/frontend-log-retention.js';
|
||||
import { writeClipboardText } from '../../../tauri-bridge.js';
|
||||
import { openFullscreenTextViewer } from './text-viewer-popup.js';
|
||||
import { runTaskOrPopup, showErrorPopup } from './setting-panel/popup-utils.js';
|
||||
import { callTauriTavernPanelPopup } from './panel-popup.js';
|
||||
|
|
@ -34,7 +35,7 @@ async function importDevLogsBundle() {
|
|||
function createActions() {
|
||||
return {
|
||||
copyText: (text) => runTaskOrPopup(async () => {
|
||||
await navigator.clipboard.writeText(String(text ?? ''));
|
||||
await writeClipboardText(String(text ?? ''));
|
||||
}),
|
||||
openTextViewer: (options) => runTaskOrPopup(async () => {
|
||||
await openFullscreenTextViewer(options);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
|
|||
import { isMobile } from '../../../RossAscends-mods.js';
|
||||
import { t, translate } from '../../../i18n.js';
|
||||
import { scanQrCodeWithBackCancellation } from '../../../../tauri/main/services/barcode-scanner/barcode-scanner-service.js';
|
||||
import { writeClipboardText } from '../../../../tauri-bridge.js';
|
||||
import {
|
||||
LAN_SYNC_DEVICES_CHANGED_EVENT,
|
||||
SYNC_AUTOMATION_CHANGED_EVENT,
|
||||
|
|
@ -508,7 +509,7 @@ async function editSyncScope(catalog, selection) {
|
|||
function createSyncActions(client) {
|
||||
return {
|
||||
copyText: async (text) => {
|
||||
await navigator.clipboard.writeText(String(text ?? ''));
|
||||
await writeClipboardText(String(text ?? ''));
|
||||
},
|
||||
scanPairUri: () => scanQrCodeWithBackCancellation(),
|
||||
reportError: (error) => showErrorPopup(error),
|
||||
|
|
|
|||
|
|
@ -245,6 +245,10 @@ export async function openDialog(options = {}) {
|
|||
return invokeWithHostNormalization('plugin:dialog|open', { options });
|
||||
}
|
||||
|
||||
export function writeClipboardText(text) {
|
||||
return invoke('plugin:clipboard-manager|write_text', { text });
|
||||
}
|
||||
|
||||
function normalizeExternalUrl(url) {
|
||||
const value = String(url instanceof URL ? url.href : url ?? '').trim();
|
||||
if (!value) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { writeClipboardText } from '../../../../tauri-bridge.js';
|
||||
import { isAndroidRuntime } from '../../../../scripts/util/mobile-runtime.js';
|
||||
|
||||
const COMPAT_KEY = '__TAURITAVERN_MOBILE_RUNTIME_COMPAT__';
|
||||
|
||||
function defineMissingMethod(target, key, implementation) {
|
||||
|
|
@ -161,6 +164,28 @@ function hasOwnPolyfill(target, property) {
|
|||
return Object.prototype.hasOwnProperty.call(Object(target), property);
|
||||
}
|
||||
|
||||
function installAndroidClipboardWriteCompat(targetWindow) {
|
||||
if (!isAndroidRuntime()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNavigator = targetWindow.navigator;
|
||||
const clipboard = targetNavigator.clipboard || {};
|
||||
// Android WebView exposes writeText but can reject it; replace only that method.
|
||||
Object.defineProperty(clipboard, 'writeText', {
|
||||
value: (text) => writeClipboardText(String(text)),
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
if (!targetNavigator.clipboard) {
|
||||
Object.defineProperty(targetNavigator, 'clipboard', {
|
||||
value: clipboard,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function installMobileRuntimeCompat(targetWindow = window) {
|
||||
if (!targetWindow) {
|
||||
return;
|
||||
|
|
@ -191,6 +216,7 @@ export function installMobileRuntimeCompat(targetWindow = window) {
|
|||
console.warn('[TauriTavern] Mobile runtime compat installed a requestIdleCallback polyfill.');
|
||||
}
|
||||
|
||||
installAndroidClipboardWriteCompat(targetWindow);
|
||||
defineMissingMethod(targetWindow.Array?.prototype, 'at', atPolyfill);
|
||||
defineMissingMethod(targetWindow.String?.prototype, 'at', atPolyfill);
|
||||
defineMissingMethod(targetWindow.Array?.prototype, 'findLast', findLastPolyfill);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { writeClipboardText } from '../../../tauri-bridge.js';
|
||||
|
||||
const GLOBAL_KEY = '__TAURITAVERN_PERF__';
|
||||
const EMBEDDED_RUNTIME_KEY = '__TAURITAVERN_EMBEDDED_RUNTIME__';
|
||||
const PANEL_RUNTIME_KEY = '__TAURITAVERN_PANEL_RUNTIME__';
|
||||
|
|
@ -1528,13 +1530,9 @@ function createPerfController(options = {}) {
|
|||
|
||||
async function copyReport(options = {}) {
|
||||
const json = exportJson(options);
|
||||
const clipboard = navigator?.clipboard;
|
||||
if (!clipboard || typeof clipboard.writeText !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await clipboard.writeText(json);
|
||||
await writeClipboardText(json);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
|
|
|||
81
tests/mobile-runtime-compat-clipboard-contract.test.mjs
Normal file
81
tests/mobile-runtime-compat-clipboard-contract.test.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
let invokedCommand;
|
||||
let invokedArgs;
|
||||
|
||||
global.window = {
|
||||
__TAURI_RUNNING__: true,
|
||||
__TAURI__: {
|
||||
core: {
|
||||
invoke: async (command, args) => {
|
||||
invokedCommand = command;
|
||||
invokedArgs = args;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const compatPath = path.join(
|
||||
REPO_ROOT,
|
||||
'src/tauri/main/compat/mobile/mobile-runtime-compat.js',
|
||||
);
|
||||
const { installMobileRuntimeCompat } = await import(pathToFileURL(compatPath).href);
|
||||
|
||||
function createTargetWindow(userAgent, clipboard) {
|
||||
return {
|
||||
navigator: { userAgent, clipboard },
|
||||
Array,
|
||||
String,
|
||||
Object,
|
||||
requestIdleCallback() {},
|
||||
cancelIdleCallback() {},
|
||||
};
|
||||
}
|
||||
|
||||
function setRuntimeUserAgent(userAgent) {
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
value: { userAgent },
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
test('Android Web Clipboard writes through the native writer without replacing other methods', async () => {
|
||||
setRuntimeUserAgent('Mozilla/5.0 (Linux; Android 15)');
|
||||
const readText = async () => 'existing';
|
||||
const clipboard = { readText, writeText: async () => assert.fail('used Web Clipboard') };
|
||||
const targetWindow = createTargetWindow('Mozilla/5.0 (Linux; Android 15)', clipboard);
|
||||
|
||||
installMobileRuntimeCompat(targetWindow);
|
||||
await targetWindow.navigator.clipboard.writeText(42);
|
||||
|
||||
assert.equal(targetWindow.navigator.clipboard.readText, readText);
|
||||
assert.equal(invokedCommand, 'plugin:clipboard-manager|write_text');
|
||||
assert.deepEqual(invokedArgs, { text: '42' });
|
||||
});
|
||||
|
||||
test('non-Android Web Clipboard is left unchanged', () => {
|
||||
setRuntimeUserAgent('Mozilla/5.0 (iPhone)');
|
||||
const writeText = async () => {};
|
||||
const targetWindow = createTargetWindow('Mozilla/5.0 (iPhone)', { writeText });
|
||||
|
||||
installMobileRuntimeCompat(targetWindow);
|
||||
|
||||
assert.equal(targetWindow.navigator.clipboard.writeText, writeText);
|
||||
});
|
||||
|
||||
test('the main window capability grants only clipboard text writes', async () => {
|
||||
const capability = JSON.parse(await readFile(
|
||||
path.join(REPO_ROOT, 'src-tauri/crates/tauritavern/capabilities/default.json'),
|
||||
'utf8',
|
||||
));
|
||||
const clipboardPermissions = capability.permissions.filter(
|
||||
permission => typeof permission === 'string' && permission.startsWith('clipboard-manager:'),
|
||||
);
|
||||
|
||||
assert.deepEqual(clipboardPermissions, ['clipboard-manager:allow-write-text']);
|
||||
});
|
||||
|
|
@ -30,8 +30,16 @@ const {
|
|||
updateTauriTavernSettings,
|
||||
openDialog,
|
||||
setDataRoot,
|
||||
writeClipboardText,
|
||||
} = await import(pathToFileURL(tauriBridgePath).href);
|
||||
|
||||
test('writeClipboardText invokes the native write-only clipboard command', async () => {
|
||||
await writeClipboardText('ttsync://pair/example');
|
||||
|
||||
assert.equal(lastInvokedCommand, 'plugin:clipboard-manager|write_text');
|
||||
assert.deepEqual(lastInvokedArgs, { text: 'ttsync://pair/example' });
|
||||
});
|
||||
|
||||
test('local endpoint authorization bridge forwards the selected provider config', async () => {
|
||||
const dto = {
|
||||
chat_completion_source: 'custom',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue