Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build": "pnpm run build:web",
"build:web": "pnpm run type-check:web && pnpm --dir src/web-ui build",
"build:mobile-web": "pnpm --dir src/mobile-web build",
"prepare:mobile-web": "node scripts/mobile-web-build.cjs",
"preview": "pnpm --dir src/web-ui preview",
"desktop:dev": "node scripts/dev.cjs desktop",
"desktop:dev:raw": "cd src/apps/desktop && npx tauri dev",
Expand Down
48 changes: 7 additions & 41 deletions scripts/dev.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
printComplete,
printBlank,
} = require('./console-style.cjs');
const { buildMobileWeb } = require('./mobile-web-build.cjs');

const ROOT_DIR = path.resolve(__dirname, '..');

Expand Down Expand Up @@ -105,32 +106,6 @@ function runCommand(command, cwd = ROOT_DIR) {
});
}

/**
* Clean stale mobile-web resource copies in Tauri target directories.
*
* Tauri copies resources from src/mobile-web/dist/ into target/{profile}/mobile-web/dist/
* on each dev/build run, but never removes old files. Since Vite generates content-hashed
* filenames, previous builds leave behind orphaned assets that accumulate over time.
* This causes the relay upload to send hundreds of stale files instead of just a few.
*/
function cleanStaleMobileWebResources() {
const fs = require('fs');
const targetDir = path.join(ROOT_DIR, 'target');
if (!fs.existsSync(targetDir)) return;

let cleaned = 0;
for (const profile of fs.readdirSync(targetDir)) {
const mobileWebDir = path.join(targetDir, profile, 'mobile-web');
if (fs.existsSync(mobileWebDir) && fs.statSync(mobileWebDir).isDirectory()) {
fs.rmSync(mobileWebDir, { recursive: true, force: true });
cleaned++;
}
}
if (cleaned > 0) {
printInfo(`Cleaned stale mobile-web resources from ${cleaned} target profile(s)`);
}
}

/**
* Main entry
*/
Expand Down Expand Up @@ -183,24 +158,15 @@ async function main() {
// Step 3: Build mobile-web (desktop only)
if (mode === 'desktop') {
printStep(3, 4, 'Build mobile-web');
const mobileWebDir = path.join(ROOT_DIR, 'src/mobile-web');
const mobileWebResult = runSilent('pnpm install --silent', mobileWebDir);
const mobileWebResult = buildMobileWeb({
install: true,
logInfo: printInfo,
logSuccess: printSuccess,
logError: printError,
});
if (!mobileWebResult.ok) {
printError('mobile-web pnpm install failed');
const output = tailOutput(mobileWebResult.stderr || mobileWebResult.stdout);
if (output) printError(output);
process.exit(1);
}
const buildResult = runInherit('pnpm run build', mobileWebDir);
if (!buildResult.ok) {
printError('mobile-web build failed');
if (buildResult.error && buildResult.error.message) {
printError(buildResult.error.message);
}
process.exit(1);
}
cleanStaleMobileWebResources();
printSuccess('mobile-web build complete');
}

// Final step: Start dev server
Expand Down
143 changes: 143 additions & 0 deletions scripts/mobile-web-build.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env node

/**
* Shared mobile-web build helpers for desktop dev/build flows.
*
* We must clean copied resources in Tauri target directories before rebuilding,
* otherwise old Vite hashed assets remain in target profile mobile-web folders and get bundled
* or uploaded by remote connect along with the latest files.
*/

const { execSync } = require('child_process');
const path = require('path');
const {
printInfo,
printSuccess,
printError,
} = require('./console-style.cjs');

const ROOT_DIR = path.resolve(__dirname, '..');

function decodeOutput(output) {
if (!output) return '';
if (typeof output === 'string') return output;
const buffer = Buffer.isBuffer(output) ? output : Buffer.from(output);
if (process.platform !== 'win32') return buffer.toString('utf-8');

const utf8 = buffer.toString('utf-8');
if (!utf8.includes('�')) return utf8;

try {
const { TextDecoder } = require('util');
const decoder = new TextDecoder('gbk');
const gbk = decoder.decode(buffer);
if (gbk && !gbk.includes('�')) return gbk;
return gbk || utf8;
} catch (error) {
return utf8;
}
}

function tailOutput(output, maxLines = 12) {
if (!output) return '';
const lines = output
.split(/\r?\n/)
.map((line) => line.trimEnd())
.filter((line) => line.trim() !== '');
if (lines.length <= maxLines) return lines.join('\n');
return lines.slice(-maxLines).join('\n');
}

function runSilent(command, cwd = ROOT_DIR) {
try {
const stdout = execSync(command, {
cwd,
stdio: 'pipe',
encoding: 'buffer',
});
return { ok: true, stdout: decodeOutput(stdout), stderr: '' };
} catch (error) {
const stdout = error.stdout ? decodeOutput(error.stdout) : '';
const stderr = error.stderr ? decodeOutput(error.stderr) : '';
return { ok: false, stdout, stderr, error };
}
}

function runInherit(command, cwd = ROOT_DIR) {
try {
execSync(command, { cwd, stdio: 'inherit' });
return { ok: true, error: null };
} catch (error) {
return { ok: false, error };
}
}

function cleanStaleMobileWebResources(logInfo = printInfo) {
const fs = require('fs');
const targetDir = path.join(ROOT_DIR, 'target');
if (!fs.existsSync(targetDir)) return 0;

let cleaned = 0;
for (const profile of fs.readdirSync(targetDir)) {
const mobileWebDir = path.join(targetDir, profile, 'mobile-web');
if (fs.existsSync(mobileWebDir) && fs.statSync(mobileWebDir).isDirectory()) {
fs.rmSync(mobileWebDir, { recursive: true, force: true });
cleaned++;
}
}

if (cleaned > 0) {
logInfo(`Cleaned stale mobile-web resources from ${cleaned} target profile(s)`);
}

return cleaned;
}

function buildMobileWeb(options = {}) {
const {
install = false,
logInfo = printInfo,
logSuccess = printSuccess,
logError = printError,
} = options;

const mobileWebDir = path.join(ROOT_DIR, 'src/mobile-web');

cleanStaleMobileWebResources(logInfo);

if (install) {
const installResult = runSilent('pnpm install --silent', mobileWebDir);
if (!installResult.ok) {
logError('mobile-web pnpm install failed');
const output = tailOutput(installResult.stderr || installResult.stdout);
if (output) {
logError(output);
} else if (installResult.error?.message) {
logError(installResult.error.message);
}
return { ok: false };
}
}

const buildResult = runInherit('pnpm run build', mobileWebDir);
if (!buildResult.ok) {
logError('mobile-web build failed');
if (buildResult.error?.message) {
logError(buildResult.error.message);
}
return { ok: false };
}

logSuccess('mobile-web build complete');
return { ok: true };
}

if (require.main === module) {
const result = buildMobileWeb();
process.exit(result.ok ? 0 : 1);
}

module.exports = {
buildMobileWeb,
cleanStaleMobileWebResources,
};
23 changes: 20 additions & 3 deletions src/apps/desktop/src/api/remote_connect_api.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Tauri commands for Remote Connect.

use bitfun_core::service::remote_connect::{
bot::BotConfig, lan, ConnectionMethod, ConnectionResult, PairingState, RemoteConnectConfig,
RemoteConnectService,
bot::{self, BotConfig}, lan, ConnectionMethod, ConnectionResult, PairingState,
RemoteConnectConfig, RemoteConnectService,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -201,6 +201,7 @@ pub struct RemoteConnectStatusResponse {
pub pairing_state: PairingState,
pub active_method: Option<String>,
pub peer_device_name: Option<String>,
pub peer_user_id: Option<String>,
/// Independent bot connection info — e.g. "Telegram(7096812005)".
/// Present when a bot is active, regardless of relay pairing state.
pub bot_connected: Option<String>,
Expand Down Expand Up @@ -415,7 +416,6 @@ pub async fn remote_connect_stop_bot() -> Result<(), String> {
service.stop_bots().await;
}
// Remove persistence so the bot is not auto-restored
use bitfun_core::service::remote_connect::bot;
let mut data = bot::load_bot_persistence();
data.connections.clear();
bot::save_bot_persistence(&data);
Expand All @@ -432,17 +432,34 @@ pub async fn remote_connect_status() -> Result<RemoteConnectStatusResponse, Stri
let state = service.pairing_state().await;
let method = service.active_method().await;
let peer = service.peer_device_name().await;
let peer_user_id = service.trusted_mobile_user_id().await;
let bot_connected = service.bot_connected_info().await;

Ok(RemoteConnectStatusResponse {
is_connected: state == PairingState::Connected,
pairing_state: state,
active_method: method.map(|m| format!("{m:?}")),
peer_device_name: peer,
peer_user_id,
bot_connected,
})
}

#[tauri::command]
pub async fn remote_connect_get_form_state() -> Result<bot::RemoteConnectFormState, String> {
Ok(bot::load_bot_persistence().form_state)
}

#[tauri::command]
pub async fn remote_connect_set_form_state(
request: bot::RemoteConnectFormState,
) -> Result<(), String> {
let mut data = bot::load_bot_persistence();
data.form_state = request;
bot::save_bot_persistence(&data);
Ok(())
}

#[tauri::command]
pub async fn remote_connect_configure_custom_server(url: String) -> Result<(), String> {
let holder = get_service_holder();
Expand Down
2 changes: 2 additions & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,8 @@ pub async fn run() {
api::remote_connect_api::remote_connect_stop,
api::remote_connect_api::remote_connect_stop_bot,
api::remote_connect_api::remote_connect_status,
api::remote_connect_api::remote_connect_get_form_state,
api::remote_connect_api::remote_connect_set_form_state,
api::remote_connect_api::remote_connect_configure_custom_server,
api::remote_connect_api::remote_connect_configure_bot,
// MiniApp API
Expand Down
2 changes: 1 addition & 1 deletion src/apps/desktop/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"build": {
"beforeDevCommand": "pnpm run dev:web",
"devUrl": "http://localhost:1422",
"beforeBuildCommand": "pnpm run build:web && pnpm run build:mobile-web",
"beforeBuildCommand": "pnpm run build:web && pnpm run prepare:mobile-web",
"frontendDist": "../../../dist"
},
"bundle": {
Expand Down
2 changes: 1 addition & 1 deletion src/apps/relay-server/test_incremental_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
subprocess.check_call([sys.executable, "-m", "pip", "install", "websockets", "-q"])
import websockets

RELAY_URL = sys.argv[1] if len(sys.argv) > 1 else "http://remote.openbitfun.com/relay"
RELAY_URL = sys.argv[1] if len(sys.argv) > 1 else "https://remote.openbitfun.com/relay"
WS_URL = RELAY_URL.replace("http://", "ws://").replace("https://", "wss://") + "/ws"

PASS = 0
Expand Down
1 change: 0 additions & 1 deletion src/crates/core/src/miniapp/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ fn inject_into_head(html: &str, content: &str) -> BitFunResult<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::miniapp::types::MiniAppSource;

#[test]
fn test_inject_into_head() {
Expand Down
3 changes: 2 additions & 1 deletion src/crates/core/src/service/lsp/plugin_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,12 @@ impl PluginLoader {

let command = command.replace('/', std::path::MAIN_SEPARATOR_STR);

let mut server_path = plugin_dir.join(&command);
let server_path = plugin_dir.join(&command);

if !server_path.exists() {
#[cfg(windows)]
{
let mut server_path = server_path;
let extensions = vec![".exe", ".bat", ".cmd"];
let mut found = false;

Expand Down
31 changes: 28 additions & 3 deletions src/crates/core/src/service/remote_connect/bot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,22 @@ pub struct SavedBotConnection {
pub connected_at: i64,
}

/// Persisted remote-connect form values shown in the desktop dialog.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RemoteConnectFormState {
pub custom_server_url: String,
pub telegram_bot_token: String,
pub feishu_app_id: String,
pub feishu_app_secret: String,
}

/// All persisted bot connections (one per bot type at most).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BotPersistenceData {
#[serde(default)]
pub connections: Vec<SavedBotConnection>,
#[serde(default)]
pub form_state: RemoteConnectFormState,
}

impl BotPersistenceData {
Expand Down Expand Up @@ -508,10 +520,15 @@ fn generate_download_token(chat_id: &str) -> String {
format!("{:08x}", ns ^ salt)
}

const BOT_PERSISTENCE_FILENAME: &str = "bot_connections.json";
const REMOTE_CONNECT_PERSISTENCE_FILENAME: &str = "remote_connect_persistence.json";
const LEGACY_BOT_PERSISTENCE_FILENAME: &str = "bot_connections.json";

pub fn bot_persistence_path() -> Option<std::path::PathBuf> {
dirs::home_dir().map(|home| home.join(".bitfun").join(BOT_PERSISTENCE_FILENAME))
dirs::home_dir().map(|home| home.join(".bitfun").join(REMOTE_CONNECT_PERSISTENCE_FILENAME))
}

fn legacy_bot_persistence_path() -> Option<std::path::PathBuf> {
dirs::home_dir().map(|home| home.join(".bitfun").join(LEGACY_BOT_PERSISTENCE_FILENAME))
}

pub fn load_bot_persistence() -> BotPersistenceData {
Expand All @@ -520,7 +537,15 @@ pub fn load_bot_persistence() -> BotPersistenceData {
};
match std::fs::read_to_string(&path) {
Ok(data) => serde_json::from_str(&data).unwrap_or_default(),
Err(_) => BotPersistenceData::default(),
Err(_) => {
let Some(legacy_path) = legacy_bot_persistence_path() else {
return BotPersistenceData::default();
};
match std::fs::read_to_string(&legacy_path) {
Ok(data) => serde_json::from_str(&data).unwrap_or_default(),
Err(_) => BotPersistenceData::default(),
}
}
}
}

Expand Down
Loading
Loading