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
Empty file added BitFun@0.1.1
Empty file.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ hostname = "0.4"

# QR code generation
qrcode = "0.14"
image = { version = "0.25", default-features = false, features = ["png"] }

# WebSocket client
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
Expand Down
Empty file added bitfun-mobile-web@0.1.1
Empty file.
71 changes: 71 additions & 0 deletions docs/remote-connect/feishu-bot-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Feishu Bot Setup Guide

[中文](./feishu-bot-setup.zh-CN.md)

Use this guide to pair BitFun through a Feishu bot.

## Setup Steps

### Step1

Open the Feishu Developer Platform and log in

<https://open.feishu.cn/app?lang=en-US>

### Step2

Create custom app

### Step3

Add Features - Bot - Add

### Step4

Permissions & Scopes -

Add permission scopes to app -

Search "im:" - Approval required "No" - Select all - Add Scopes

### Step5

Credentials & Basic Info - Copy App ID and App Secret

### Step6

Open BitFun - Remote Connect - SMS Bot - Feishu Bot - Fill in App ID and App Secret - Connect

### Step7

Back to Feishu Developer Platform

### Step8

Events & callbacks - Event configuration -

Subscription mode - persistent connection - Save

Add Events - Search "im.message" - Select all - Confirm

### Step9

Events & callbacks - Callback configuration -

Subscription mode - persistent connection - Save

Add callback - Search "card.action.trigger" - Select all - Confirm

### Step10

Publish the bot

### Step11

Open Feishu - Search "{robot name}" -

Click the robot to open the chat box - Input any message and send

### Step12

Enter the 6-digit pairing code from BitFun Desktop - Send - Connection successful
61 changes: 61 additions & 0 deletions docs/remote-connect/feishu-bot-setup.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 飞书机器人配置指南

[English](./feishu-bot-setup.md)

适用于 BitFun 通过飞书机器人完成远程连接配对。

## 配置步骤

### 第一步

打开飞书开发者平台并登录

<https://open.feishu.cn/app?lang=zh-CN>

### 第二步

创建企业自建应用

### 第三步

添加应用能力 - 机器人 - 添加

### 第四步

权限管理 - 开通权限 - 搜索"im:" - 是否需要审核选择"免审权限" - 全选 - 确认开通权限

### 第五步

凭证与基础信息 - 复制 App ID 和 App Secret

### 第六步

打开 BitFun - 远程连接 - SMS 机器人 - Feishu 机器人 - 填写 App ID 和 App Secret - 连接

### 第七步

回到飞书开发者平台机器人设置页

### 第八步

事件与回调 - 事件配置 - 订阅方式 - 使用 长连接 接收事件 - 保存

添加事件 - 搜索"im.message" - 全选 - 确认添加

### 第九步

事件与回调 - 回调配置 - 订阅方式 - 使用 长连接 接收事件 - 保存

添加回调 - 搜索"card.action.trigger" - 选中 - 确认添加

### 第十步

发布机器人

### 第十一步

打开飞书应用 - 搜索"{机器人名称}" - 点击机器人打开对话框 - 输入任意消息并发送

### 第十二步

被机器人要求输入6位验证码 - 输入 - 发送 - 连接成功
Empty file added node
Empty file.
27 changes: 27 additions & 0 deletions scripts/dev.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,32 @@ 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 @@ -173,6 +199,7 @@ async function main() {
}
process.exit(1);
}
cleanStaleMobileWebResources();
printSuccess('mobile-web build complete');
}

Expand Down
4 changes: 2 additions & 2 deletions src/apps/cli/src/agent/core_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::sync::Arc;

use super::{Agent, AgentEvent, AgentResponse};
use crate::session::{ToolCall, ToolCallStatus};
use bitfun_core::agentic::coordination::ConversationCoordinator;
use bitfun_core::agentic::coordination::{ConversationCoordinator, DialogTriggerSource};
use bitfun_core::agentic::core::SessionConfig;
use bitfun_core::agentic::events::EventQueue;
use bitfun_events::{AgenticEvent as CoreEvent, ToolEventData};
Expand Down Expand Up @@ -85,7 +85,7 @@ impl Agent for CoreAgentAdapter {
message.clone(),
None,
self.agent_type.clone(),
false,
DialogTriggerSource::Cli,
).await?;

let mut accumulated_text = String::new();
Expand Down
18 changes: 12 additions & 6 deletions src/apps/desktop/src/api/agentic_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ use std::sync::Arc;
use tauri::{AppHandle, State};

use crate::api::app_state::AppState;
use crate::api::context_upload_api::get_image_context;
use bitfun_core::agentic::coordination::ConversationCoordinator;
use bitfun_core::agentic::tools::image_context::get_image_context;
use bitfun_core::agentic::coordination::{ConversationCoordinator, DialogTriggerSource};
use bitfun_core::agentic::core::*;
use bitfun_core::agentic::image_analysis::ImageContextData;
use bitfun_core::infrastructure::get_workspace_path;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -146,6 +147,9 @@ pub async fn create_session(
coordinator: State<'_, Arc<ConversationCoordinator>>,
request: CreateSessionRequest,
) -> Result<CreateSessionResponse, String> {
let workspace_path = get_workspace_path()
.map(|p| p.to_string_lossy().to_string());

let config = request
.config
.map(|c| SessionConfig {
Expand All @@ -161,11 +165,12 @@ pub async fn create_session(
.unwrap_or_default();

let session = coordinator
.create_session_with_id(
.create_session_with_workspace(
request.session_id,
request.session_name.clone(),
request.agent_type.clone(),
config,
workspace_path,
)
.await
.map_err(|e| format!("Failed to create session: {}", e))?;
Expand Down Expand Up @@ -204,6 +209,7 @@ pub async fn start_dialog_turn(
resolved_image_contexts,
turn_id,
agent_type,
DialogTriggerSource::DesktopUi,
)
.await
.map_err(|e| format!("Failed to start dialog turn: {}", e))?;
Expand All @@ -214,7 +220,7 @@ pub async fn start_dialog_turn(
user_input,
turn_id,
agent_type,
false,
DialogTriggerSource::DesktopUi,
)
.await
.map_err(|e| format!("Failed to start dialog turn: {}", e))?;
Expand Down Expand Up @@ -254,13 +260,13 @@ fn resolve_missing_image_payloads(
image.image_path = stored
.image_path
.clone()
.filter(|s| !s.trim().is_empty());
.filter(|s: &String| !s.trim().is_empty());
}
if is_blank_text(image.data_url.as_ref()) {
image.data_url = stored
.data_url
.clone()
.filter(|s| !s.trim().is_empty());
.filter(|s: &String| !s.trim().is_empty());
}
if image.mime_type.trim().is_empty() {
image.mime_type = stored.mime_type.clone();
Expand Down
79 changes: 7 additions & 72 deletions src/apps/desktop/src/api/context_upload_api.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
//! Temporary Image Storage API

use bitfun_core::agentic::tools::image_context::{
ImageContextData as CoreImageContextData, ImageContextProvider,
create_image_context_provider as create_core_image_context_provider,
store_image_contexts,
GlobalImageContextProvider,
ImageContextData as CoreImageContextData,
};
use dashmap::DashMap;
use log::{debug, warn};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

static IMAGE_STORAGE: Lazy<DashMap<String, (ImageContextData, u64)>> = Lazy::new(DashMap::new);

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageContextData {
Expand Down Expand Up @@ -47,73 +44,11 @@ pub struct UploadImageContextRequest {

#[tauri::command]
pub async fn upload_image_contexts(request: UploadImageContextRequest) -> Result<(), String> {
let timestamp =
current_unix_timestamp().map_err(|e| format!("Failed to get current timestamp: {}", e))?;

for image in request.images {
let image_id = image.id.clone();
IMAGE_STORAGE.insert(image_id.clone(), (image, timestamp));
debug!("Stored image context: image_id={}", image_id);
}

cleanup_expired_images(300);

let images: Vec<CoreImageContextData> = request.images.into_iter().map(Into::into).collect();
store_image_contexts(images);
Ok(())
}

pub fn get_image_context(image_id: &str) -> Option<ImageContextData> {
IMAGE_STORAGE.get(image_id).map(|entry| entry.0.clone())
}

pub fn remove_image_context(image_id: &str) {
if IMAGE_STORAGE.remove(image_id).is_some() {
debug!("Removed image context: image_id={}", image_id);
}
}

fn cleanup_expired_images(max_age_secs: u64) {
let now = match current_unix_timestamp() {
Ok(timestamp) => timestamp,
Err(e) => {
warn!(
"Failed to cleanup expired images due to timestamp error: {}",
e
);
return;
}
};

let expired_keys: Vec<String> = IMAGE_STORAGE
.iter()
.filter(|entry| now.saturating_sub(entry.value().1) > max_age_secs)
.map(|entry| entry.key().clone())
.collect();

for key in expired_keys {
IMAGE_STORAGE.remove(&key);
debug!("Cleaned up expired image: image_id={}", key);
}
}

#[derive(Debug)]
pub struct GlobalImageContextProvider;

impl ImageContextProvider for GlobalImageContextProvider {
fn get_image(&self, image_id: &str) -> Option<CoreImageContextData> {
get_image_context(image_id).map(|data| data.into())
}

fn remove_image(&self, image_id: &str) {
remove_image_context(image_id);
}
}

pub fn create_image_context_provider() -> GlobalImageContextProvider {
GlobalImageContextProvider
}

fn current_unix_timestamp() -> Result<u64, std::time::SystemTimeError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
create_core_image_context_provider()
}
9 changes: 3 additions & 6 deletions src/apps/desktop/src/api/image_analysis_api.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
//! Image Analysis API

use crate::api::app_state::AppState;
use bitfun_core::agentic::coordination::ConversationCoordinator;
use bitfun_core::agentic::image_analysis::{
resolve_vision_model_from_ai_config, AnalyzeImagesRequest, ImageAnalysisResult, ImageAnalyzer,
MessageEnhancer, SendEnhancedMessageRequest,
};
use bitfun_core::agentic::coordination::{ConversationCoordinator, DialogTriggerSource};
use bitfun_core::agentic::image_analysis::*;
use log::error;
use std::sync::Arc;
use tauri::State;
Expand Down Expand Up @@ -74,7 +71,7 @@ pub async fn send_enhanced_message(
enhanced_message.clone(),
Some(request.dialog_turn_id.clone()),
request.agent_type.clone(),
false,
DialogTriggerSource::DesktopApi,
)
.await
.map_err(|e| format!("Failed to send enhanced message: {}", e))?;
Expand Down
Loading