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
36 changes: 18 additions & 18 deletions src/apps/relay-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "bitfun-relay-server"
version.workspace = true
authors.workspace = true
edition.workspace = true
version = "0.1.2"
authors = ["BitFun Team"]
edition = "2021"
description = "BitFun Relay Server - WebSocket relay for Remote Connect"

[lib]
Expand All @@ -15,28 +15,28 @@ path = "src/main.rs"

[dependencies]
# Web framework
axum = { workspace = true }
tower-http = { workspace = true, features = ["cors", "fs"] }
axum = { version = "0.7", features = ["json", "ws"] }
tower-http = { version = "0.6", features = ["cors", "fs"] }

# Async runtime
tokio = { workspace = true, features = ["full"] }
futures-util = { workspace = true }
tokio = { version = "1.0", features = ["full"] }
futures-util = "0.3"

# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Error handling
anyhow = { workspace = true }
anyhow = "1.0"

# Logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Utilities
uuid = { workspace = true }
chrono = { workspace = true }
dashmap = { workspace = true }
rand = { workspace = true }
base64 = { workspace = true }
sha2 = { workspace = true }
uuid = { version = "1.0", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde", "clock"] }
dashmap = "5.5"
rand = "0.8"
base64 = "0.21"
sha2 = "0.10"
27 changes: 19 additions & 8 deletions src/apps/relay-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ WebSocket relay server for BitFun Remote Connect. Bridges desktop (WebSocket) an
### Docker (Recommended)

```bash
# One-click deploy
# SSH into your target server first, then clone the repo:
git clone https://github.com/GCWing/BitFun
cd BitFun/src/apps/relay-server

# SSH into your target server first, then run:
bash deploy.sh
```

`deploy.sh` must be run on the target server itself. It only deploys to the current machine and does not SSH to a remote host.

### What URL should I fill in BitFun Desktop?

In **Remote Connect → Self-Hosted → Server URL**, use one of:

- Direct relay port: `http://<YOUR_SERVER_IP>:9700`
- Reverse proxy on domain root: `https://relay.example.com`
- Reverse proxy with `/relay` prefix: `https://relay.example.com/relay`

`/relay` is **not mandatory**. It is only needed when your reverse proxy is configured with that path prefix.

Expand Down Expand Up @@ -118,16 +122,24 @@ Only desktop clients connect via WebSocket. Mobile clients use the HTTP endpoint

## Self-Hosted Deployment

### Option A: Local Deploy (on the server itself)
### Option A: Deploy on the Server Itself

If you have the repo cloned **on the server**:

```bash
git clone https://github.com/GCWing/BitFun
cd BitFun/src/apps/relay-server/
bash deploy.sh
```

Or, if the repo is already present on the server:

```bash
cd src/apps/relay-server/
bash deploy.sh
```

This builds the Docker image locally and starts the container. It will **automatically stop any previously running relay container** before restarting.
This script must be executed in an SSH session on the target server. It builds the Docker image on that server and starts the container there. It will **automatically stop any previously running relay container** before restarting.

### Option B: Remote Deploy (from your dev machine)

Expand Down Expand Up @@ -163,8 +175,7 @@ The script will:
2. Verify health endpoint:
- `http://<server-ip>:9700/health`
3. Configure your final URL strategy:
- root domain (`https://relay.example.com`) or
- path prefix (`https://relay.example.com/relay`)
- root domain (`https://relay.example.com`)
4. Fill the same URL into BitFun Desktop "Custom Server"

### Directory Structure
Expand All @@ -177,7 +188,7 @@ relay-server/
├── Dockerfile # Docker build (standalone single-crate build)
├── docker-compose.yml # Docker Compose config
├── Caddyfile # Caddy reverse proxy config (optional)
├── deploy.sh # Local deploy (run on the server itself)
├── deploy.sh # Deploy current machine (run on the target server itself)
├── remote-deploy.sh # Remote deploy (run from dev machine via SSH)
└── README.md
```
Expand Down
11 changes: 9 additions & 2 deletions src/apps/relay-server/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# BitFun Relay Server — one-click deploy script.
# Usage: bash deploy.sh [--skip-build] [--skip-health-check]
#
# Run this script on the target server itself after SSH login.
# It deploys to the current machine only; it does not SSH to a remote host.
#
# Prerequisites: Docker, Docker Compose

set -euo pipefail
Expand All @@ -18,6 +21,10 @@ BitFun Relay Server deploy script
Usage:
bash deploy.sh [options]

Run location:
Execute this script on the target server itself after SSH login.
This script only deploys to the current machine.

Options:
--skip-build Skip docker compose build, only restart services
--skip-health-check Skip post-deploy health check
Expand Down Expand Up @@ -58,6 +65,8 @@ for arg in "$@"; do
done

echo "=== BitFun Relay Server Deploy ==="
echo "Target: current machine"
echo "Note: run this script on the target server after SSH login."
check_command docker
check_docker_compose

Expand Down Expand Up @@ -113,8 +122,6 @@ echo "Caddy proxy on ports 80/443"
echo ""
echo "Custom Server URL examples for BitFun Desktop:"
echo " - Direct relay: http://<YOUR_SERVER_IP>:9700"
echo " - Reverse proxy root: https://<YOUR_DOMAIN>"
echo " - Reverse proxy /relay:https://<YOUR_DOMAIN>/relay (if you configured path prefix)"
echo ""
echo "Check status: docker compose ps"
echo "View logs: docker compose logs -f relay-server"
Expand Down
15 changes: 2 additions & 13 deletions src/crates/core/src/service/remote_connect/bot/command_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1365,12 +1365,10 @@ async fn handle_chat_message(
/// `RemoteExecutionDispatcher` (the same path used by mobile), then
/// subscribes to the tracker's broadcast channel for real-time events.
///
/// `message_sender` is called to send intermediate messages (e.g. thinking
/// content) before the final response is returned.
pub async fn execute_forwarded_turn(
forward: ForwardRequest,
interaction_handler: Option<BotInteractionHandler>,
message_sender: Option<BotMessageSender>,
_message_sender: Option<BotMessageSender>,
) -> ForwardedTurnResult {
use crate::agentic::coordination::DialogTriggerSource;
use crate::service::remote_connect::remote_server::{
Expand Down Expand Up @@ -1401,20 +1399,11 @@ pub async fn execute_forwarded_turn(
}

let result = tokio::time::timeout(std::time::Duration::from_secs(300), async {
let mut thinking = String::new();
let mut response = String::new();
loop {
match event_rx.recv().await {
Ok(event) => match event {
TrackerEvent::ThinkingChunk(t) => thinking.push_str(&t),
TrackerEvent::ThinkingEnd => {
if !thinking.is_empty() {
if let Some(sender) = message_sender.as_ref() {
sender(thinking.clone()).await;
}
thinking.clear();
}
}
TrackerEvent::ThinkingChunk(_) | TrackerEvent::ThinkingEnd => {}
TrackerEvent::TextChunk(t) => response.push_str(&t),
TrackerEvent::ToolStarted {
tool_id,
Expand Down
57 changes: 45 additions & 12 deletions src/crates/core/src/service/remote_connect/bot/feishu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,27 +293,59 @@ impl FeishuBot {

pub async fn send_message(&self, chat_id: &str, content: &str) -> Result<()> {
let token = self.get_access_token().await?;
let card = Self::build_markdown_card(content);
let client = reqwest::Client::new();
let resp = client
.post("https://open.feishu.cn/open-apis/im/v1/messages")
.query(&[("receive_id_type", "chat_id")])
.bearer_auth(&token)
.json(&serde_json::json!({
"receive_id": chat_id,
"msg_type": "text",
"content": serde_json::to_string(&serde_json::json!({"text": content}))?,
"msg_type": "interactive",
"content": serde_json::to_string(&card)?,
}))
.send()
.await?;

if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!("feishu send_message failed: {body}"));
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
if !status.is_success() {
return Err(anyhow!("feishu send_message HTTP {status}: {body}"));
}
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(code) = parsed.get("code").and_then(|c| c.as_i64()) {
if code != 0 {
let msg = parsed.get("msg").and_then(|m| m.as_str()).unwrap_or("unknown");
warn!("Feishu send_message API error: code={code}, msg={msg}");
return Err(anyhow!("feishu send_message API error: code={code}, msg={msg}"));
}
}
}
debug!("Feishu message sent to {chat_id}");
Ok(())
}

fn build_markdown_card(content: &str) -> serde_json::Value {
serde_json::json!({
"schema": "2.0",
"config": {
"wide_screen_mode": true,
},
"body": {
"elements": [
{
"tag": "markdown",
"content": content,
"text_align": "left",
"text_size": "normal",
"margin": "0px 0px 0px 0px",
"element_id": "bitfun_remote_reply_markdown",
}
],
},
})
}

/// Download a user-sent image from a Feishu message using the message resources API.
/// The returned data-URL is compressed to at most 1 MB.
async fn download_image_as_data_url(&self, message_id: &str, file_key: &str) -> Result<String> {
Expand Down Expand Up @@ -591,11 +623,8 @@ impl FeishuBot {
fn build_action_card(chat_id: &str, content: &str, actions: &[BotAction]) -> serde_json::Value {
let body = Self::card_body_text(content);
let mut elements = vec![serde_json::json!({
"tag": "div",
"text": {
"tag": "lark_md",
"content": body,
}
"tag": "markdown",
"content": body,
})];

for chunk in actions.chunks(2) {
Expand Down Expand Up @@ -1292,11 +1321,15 @@ impl FeishuBot {
let msg_bot = msg_bot.clone();
let msg_cid = msg_cid.clone();
Box::pin(async move {
msg_bot.send_message(&msg_cid, &text).await.ok();
if let Err(err) = msg_bot.send_message(&msg_cid, &text).await {
warn!("Failed to send Feishu intermediate message to {msg_cid}: {err}");
}
})
});
let result = execute_forwarded_turn(forward, Some(handler), Some(sender)).await;
bot.send_message(&cid, &result.display_text).await.ok();
if let Err(err) = bot.send_message(&cid, &result.display_text).await {
warn!("Failed to send Feishu final message to {cid}: {err}");
}
bot.notify_files_ready(&cid, &result.full_text).await;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ export const RemoteConnectDialog: React.FC<RemoteConnectDialogProps> = ({
<label>{t('remoteConnect.serverUrl')}</label>
<input
type="url" className="bitfun-remote-connect__input"
placeholder="https://relay.example.com"
placeholder="https://relay.example.com:9700"
value={customUrl} onChange={(e) => setCustomUrl(e.target.value)}
/>
</div>
Expand Down