Skip to content

Commit e42f8c0

Browse files
committed
fix: centralize desktop transport contract
1 parent bf15fb9 commit e42f8c0

5 files changed

Lines changed: 71 additions & 7 deletions

File tree

scripts/prepare-resources/bridge-bootstrap-updater-contract.test.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import assert from 'node:assert/strict';
33
import { readFile } from 'node:fs/promises';
44

55
const bootstrapPath = new URL('../../src-tauri/src/bridge_bootstrap.js', import.meta.url);
6+
const chatTransportContractPath = new URL(
7+
'../../src-tauri/src/desktop_bridge_chat_transport_contract.json',
8+
import.meta.url,
9+
);
610

711
test('bridge bootstrap defines astrbotAppUpdater methods', async () => {
812
const source = await readFile(bootstrapPath, 'utf8');
@@ -13,3 +17,17 @@ test('bridge bootstrap defines astrbotAppUpdater methods', async () => {
1317
assert.match(source, /checkForAppUpdate:\s*\(\)\s*=>/);
1418
assert.match(source, /installAppUpdate:\s*\(\)\s*=>/);
1519
});
20+
21+
test('bridge bootstrap transport placeholders are backed by the shared contract', async () => {
22+
const [source, rawContract] = await Promise.all([
23+
readFile(bootstrapPath, 'utf8'),
24+
readFile(chatTransportContractPath, 'utf8'),
25+
]);
26+
const contract = JSON.parse(rawContract);
27+
28+
assert.equal(typeof contract.storageKey, 'string');
29+
assert.equal(typeof contract.websocketValue, 'string');
30+
assert.match(source, /if \(typeof window === 'undefined'\) return;/);
31+
assert.match(source, /\{CHAT_TRANSPORT_MODE_STORAGE_KEY\}/);
32+
assert.match(source, /\{CHAT_TRANSPORT_MODE_WEBSOCKET\}/);
33+
});

scripts/prepare-resources/desktop-bridge-expectations.mjs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
1+
import { readFileSync } from 'node:fs';
2+
13
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
24

3-
const CHAT_TRANSPORT_MODE_STORAGE_KEY = 'chat.transportMode';
4-
const CHAT_TRANSPORT_MODE_WEBSOCKET = 'websocket';
5+
const chatTransportContractPath = new URL(
6+
'../../src-tauri/src/desktop_bridge_chat_transport_contract.json',
7+
import.meta.url,
8+
);
9+
const chatTransportContract = JSON.parse(readFileSync(chatTransportContractPath, 'utf8'));
10+
const CHAT_TRANSPORT_MODE_STORAGE_KEY = chatTransportContract.storageKey;
11+
const CHAT_TRANSPORT_MODE_WEBSOCKET = chatTransportContract.websocketValue;
12+
13+
if (
14+
typeof CHAT_TRANSPORT_MODE_STORAGE_KEY !== 'string' ||
15+
!CHAT_TRANSPORT_MODE_STORAGE_KEY ||
16+
typeof CHAT_TRANSPORT_MODE_WEBSOCKET !== 'string' ||
17+
!CHAT_TRANSPORT_MODE_WEBSOCKET
18+
) {
19+
throw new Error(
20+
'desktop bridge chat transport contract must define non-empty string storageKey and websocketValue fields',
21+
);
22+
}
523

624
const CHAT_TRANSPORT_STORAGE_KEY_PATTERN = escapeRegex(CHAT_TRANSPORT_MODE_STORAGE_KEY);
725
const CHAT_TRANSPORT_WEBSOCKET_PATTERN = escapeRegex(CHAT_TRANSPORT_MODE_WEBSOCKET);

src-tauri/src/bridge/desktop.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
use std::sync::OnceLock;
22

3+
use serde::Deserialize;
34
use url::Url;
45

56
use crate::bridge::origin_policy;
67

78
static DESKTOP_BRIDGE_BOOTSTRAP_TEMPLATE: &str = include_str!("../bridge_bootstrap.js");
9+
static DESKTOP_BRIDGE_CHAT_TRANSPORT_CONTRACT_TEMPLATE: &str =
10+
include_str!("../desktop_bridge_chat_transport_contract.json");
811
static DESKTOP_BRIDGE_BOOTSTRAP_SCRIPT: OnceLock<String> = OnceLock::new();
12+
static DESKTOP_BRIDGE_CHAT_TRANSPORT_CONTRACT: OnceLock<DesktopBridgeChatTransportContract> =
13+
OnceLock::new();
14+
15+
#[derive(Deserialize)]
16+
#[serde(rename_all = "camelCase")]
17+
struct DesktopBridgeChatTransportContract {
18+
storage_key: String,
19+
websocket_value: String,
20+
}
21+
22+
fn desktop_bridge_chat_transport_contract() -> &'static DesktopBridgeChatTransportContract {
23+
DESKTOP_BRIDGE_CHAT_TRANSPORT_CONTRACT.get_or_init(|| {
24+
serde_json::from_str(DESKTOP_BRIDGE_CHAT_TRANSPORT_CONTRACT_TEMPLATE)
25+
.expect("desktop bridge chat transport contract must be valid JSON")
26+
})
27+
}
928

1029
fn desktop_bridge_bootstrap_script(event_name: &str) -> &'static str {
1130
DESKTOP_BRIDGE_BOOTSTRAP_SCRIPT
1231
.get_or_init(|| {
13-
DESKTOP_BRIDGE_BOOTSTRAP_TEMPLATE.replace("{TRAY_RESTART_BACKEND_EVENT}", event_name)
32+
let contract = desktop_bridge_chat_transport_contract();
33+
DESKTOP_BRIDGE_BOOTSTRAP_TEMPLATE
34+
.replace("{TRAY_RESTART_BACKEND_EVENT}", event_name)
35+
.replace("{CHAT_TRANSPORT_MODE_STORAGE_KEY}", &contract.storage_key)
36+
.replace("{CHAT_TRANSPORT_MODE_WEBSOCKET}", &contract.websocket_value)
1437
})
1538
.as_str()
1639
}

src-tauri/src/bridge_bootstrap.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
(() => {
2+
if (typeof window === 'undefined') return;
3+
24
const existingTrayRestartState = window.__astrbotDesktopTrayRestartState;
35
if (
46
window.astrbotDesktop &&
@@ -148,11 +150,10 @@
148150

149151
const TOKEN_STORAGE_KEY = 'token';
150152
const SHELL_LOCALE_STORAGE_KEY = 'astrbot-locale';
151-
// Mirror AstrBot dashboard transport persistence. Resource preparation verifies
152-
// the upstream ChatUI still recognizes this storage contract before packaging.
153+
// Values are injected from the shared desktop bridge transport contract.
153154
const CHAT_TRANSPORT = Object.freeze({
154-
STORAGE_KEY: 'chat.transportMode',
155-
WEBSOCKET: 'websocket',
155+
STORAGE_KEY: '{CHAT_TRANSPORT_MODE_STORAGE_KEY}',
156+
WEBSOCKET: '{CHAT_TRANSPORT_MODE_WEBSOCKET}',
156157
});
157158
const STORAGE_SYNC_PATCHED_FLAG = '__astrbotDesktopStorageSyncPatched';
158159
const LEGACY_TOKEN_SYNC_PATCHED_FLAG = '__astrbotDesktopTokenSyncPatched';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"storageKey": "chat.transportMode",
3+
"websocketValue": "websocket"
4+
}

0 commit comments

Comments
 (0)