From d962ec9211526e13963f1c3438fc262f909dfbac Mon Sep 17 00:00:00 2001 From: wangchao <1398269744@qq.com> Date: Mon, 27 Apr 2026 20:10:25 +0800 Subject: [PATCH] feat: add Harmony build tool and bump-to-interact feature -integrate HarmonyOS build toolchain for native application compilation -implement bump-to-interact functionality for seamless device interaction --- src/apps/desktop/src/api/ohos/mod.rs | 2 + .../desktop/src/api/ohos/ohos_file_system.rs | 6 + src/apps/desktop/src/lib.rs | 12 +- .../main/ets/entryability/EntryAbility.ets | 32 ++- src/crates/core/Cargo.toml | 6 +- .../implementations/harmony_build_tool.rs | 246 ++++++++++++++++++ .../src/agentic/tools/implementations/mod.rs | 2 +- .../core/src/service/remote_connect/mod.rs | 36 ++- src/crates/core/src/util/mod.rs | 4 +- .../core/src/util/register_arkts_function.rs | 44 ++++ 10 files changed, 370 insertions(+), 20 deletions(-) create mode 100644 src/apps/desktop/src/api/ohos/mod.rs create mode 100644 src/apps/desktop/src/api/ohos/ohos_file_system.rs create mode 100644 src/crates/core/src/agentic/tools/implementations/harmony_build_tool.rs create mode 100644 src/crates/core/src/util/register_arkts_function.rs diff --git a/src/apps/desktop/src/api/ohos/mod.rs b/src/apps/desktop/src/api/ohos/mod.rs new file mode 100644 index 000000000..62094c044 --- /dev/null +++ b/src/apps/desktop/src/api/ohos/mod.rs @@ -0,0 +1,2 @@ +pub mod ohos_file_system; +pub mod window; \ No newline at end of file diff --git a/src/apps/desktop/src/api/ohos/ohos_file_system.rs b/src/apps/desktop/src/api/ohos/ohos_file_system.rs new file mode 100644 index 000000000..622a027f6 --- /dev/null +++ b/src/apps/desktop/src/api/ohos/ohos_file_system.rs @@ -0,0 +1,6 @@ +use crate::Appstate; +use bitfun_core::util::open_dialog_file; +#[tauri::command] +pub async fn open_oh_file_dialog() -> Result { + open_dialog_file().await +} \ No newline at end of file diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 4326be879..b721d1e3a 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -23,7 +23,7 @@ use tauri::Manager; // Re-export API pub use api::*; - +use std::path::PathBuf; use api::ai_rules_api::*; use api::clipboard_file_api::*; use api::commands::*; @@ -209,6 +209,16 @@ pub async fn _run() { } logging::register_runtime_log_state(startup_log_level, session_log_dir.clone()); + { + let candidates = ["mobile-web/dist","mobile-web","dist"]; + let mut found = false; + let path = PathBuf::from("/data/storage/el2/base/files/dist"); + if path.join("index.html").exists() { + log::info!("Found bundled mobile-web at: {}", path.display()); + api::remote_connect_api::set_mobile_web_resource_path(path); + found = true; + } + } for step in startup_timings.steps() { log::debug!( diff --git a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets index 29649656c..2ba31f66e 100644 --- a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets +++ b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets @@ -21,6 +21,7 @@ export default class EntryAbility extends RustAbility { public moduleName: string = "bitfun_desktop_lib"; public defaultPage: boolean = true; public commonEventListener: CommonEventListener | undefined = undefined; + public remote_url: string = ""; async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise { super.onCreate(want, launchParam); @@ -97,6 +98,11 @@ export default class EntryAbility extends RustAbility { runDeveco(this.context, arg); return ''; }); + RustModule.registerArktsFunction('send_remote_url', async (err: Error, arg: string): Promise => { + hilog.info(DOMAIN_NUMBER, TAG, 'get remote url ' + arg); + this.remote_url = arg; + return ''; + }); RustModule.registerArktsFunction('harmony_create', async (err: Error, arg: string): Promise => { await fileIo.copyDir('/storage/Users/currentUser/Documents/DevecoStudioProjects/MyApplication', '/storage/Users/currentUser/Documents/files', 1).then(() => { @@ -122,19 +128,19 @@ export default class EntryAbility extends RustAbility { } private sendOnlyCallback = (sharableTable: harmonyShare.SharableTarget) => { - let filePath = "/data/storage/el2/base/files/dist/output.txt"; - fileIo.readText(filePath).then((content: string) => { - console.info("readText success:" + content); - let shareData: systemShare.SharedData = new systemShare.SharedData({ - utd: uniformTypeDescriptor.UniformDataType.HYPERLINK, - content, - title: "Bitfun", - description: "Phone", - }); - sharableTable.share(shareData) - }).catch((err: BusinessError) => { - hilog.error(DOMAIN, 'vnext', 'sendOnlyCallback error:' + err); - }); + if (this.remote_url.length == 0) { + let content = this.remote_url; + let shareData: systemShare.SharedData = new systemShare.SharedData({ + utd: uniformTypeDescriptor.UniformDataType.HYPERLINK, + content, + title: "Bitfun", + description: "Phone", + }); + sharableTable.share(shareData) + } + else { + hilog.error(DOMAIN, 'vnext', 'sendOnlyCallback error: remote url is empty'); + } } } diff --git a/src/crates/core/Cargo.toml b/src/crates/core/Cargo.toml index 41fabce19..ed9105f23 100644 --- a/src/crates/core/Cargo.toml +++ b/src/crates/core/Cargo.toml @@ -12,6 +12,8 @@ crate-type = ["rlib"] [dependencies] # Inherit shared dependencies from workspace tokio = { workspace = true } +napi-ohos = { workspace = true } +napi-derive-ohos = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } async-trait = { workspace = true } @@ -38,7 +40,9 @@ aes = "0.8" hex = "0.4" dashmap = { workspace = true } indexmap = { workspace = true } - +lazy_static = "1.4" +once_cell = "1.19" +parking_lot = "0.12" reqwest = { workspace = true } # Debug Log HTTP Server diff --git a/src/crates/core/src/agentic/tools/implementations/harmony_build_tool.rs b/src/crates/core/src/agentic/tools/implementations/harmony_build_tool.rs new file mode 100644 index 000000000..7c876375e --- /dev/null +++ b/src/crates/core/src/agentic/tools/implementations/harmony_build_tool.rs @@ -0,0 +1,246 @@ +use crate::agentic::tools::framework::{ + Tool, ToolRenderOptions, ToolResult, ToolUseContext, ValidationResult, +}; +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; +use crate::util::errors::{BitFunError, BitFunResult}; +use async_trait::async_trait; +use napi_derive_ohos::napi; +use parking_lot::{Condvar, Mutex}; +use serde_json::{json, Value}; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; +use crate::util::JS_THREADSAFE_FUNCTION; +struct BuildState { + result: Option, + notified: bool, +} + +static BUILD_STATE: once_cell::sync::Lazy, Condvar)>> = + once_cell::sync::Lazy::new(|| { + Arc::new(( + Mutex::new(BuildState { + result: None, + notified: false, + }), + Condvar::new(), + )) + }); + +pub struct HarmonyBuildTool {} + +impl HarmonyBuildTool { + pub fn new() -> Self { + Self {} + } + + fn validate_project_path(&self, project_path: &str) -> bool { + let path = Path::new(project_path); + path.exists() && path.is_dir() + } + + async fn execute_build(&self, project_path: &str) -> BitFunResult { + log::info!("HarmonyOS build for project: {}", project_path); + + { + let (lock, cvar) = &**BUILD_STATE; + let mut state = lock.lock(); + state.result = None; + state.notified = false; + cvar.notify_all(); + } + + match call_harmony_build(project_path.to_string()) { + Ok(_) => { + log::info!("call_harmony_build success"); + let timeout = Duration::from_secs(60); + let (lock, cvar) = &**BUILD_STATE; + let mut state = lock.lock(); + + let wait_result = cvar.wait_for(&mut state, timeout); + if !wait_result.timed_out() && state.notified { + if let Some(msg) = &state.result { + log::info!("Build result received: {}", msg); + return Ok(msg.clone()); + } + } + + log::error!("Build timeout"); + Err(BitFunError::tool( + "Build timeout: no result received within 1 minute".to_string(), + )) + } + Err(_) => { + log::error!("call_harmony_build failed"); + Err(BitFunError::tool( + "Build failed: call_harmony_build failed".to_string(), + )) + } + } + } +} + +#[async_trait] +impl Tool for HarmonyBuildTool { + fn name(&self) -> &str { + "HarmonyBuild" + } + + async fn description(&self) -> BitFunResult { + Ok( + r#"HarmonyOS application build tool. Builds a HarmonyOS project. + + Usage: + - The project_path parameter must be an absolute path to a HarmonyOS project + + Example: + - Build project: {"project_path": "path/to/harmony/project"}"# + .to_string(), + ) + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "project_path": { + "type": "string", + "description": "The absolute path to the HarmonyOS project" + } + }, + "required": [ "project_path" ], + "additionalProperties": false + }) + } + + fn is_readonly(&self) -> bool { + false + } + + fn is_concurrency_safe(&self, _input: Option<&Value>) -> bool { + false + } + + fn needs_permissions(&self, _input: Option<&Value>) -> bool { + true + } + + async fn validate_input( + &self, + input: &Value, + _context: Option<&ToolUseContext>, + ) -> ValidationResult { + let project_path = match input.get("project_path").and_then(|v| v.as_str()) { + Some(path) => path, + None => { + return ValidationResult { + result: false, + message: Some("project_path is required".to_string()), + error_code: Some(400), + meta: None, + }; + } + }; + + if project_path.is_empty() { + return ValidationResult { + result: false, + message: Some("project_path cannot be empty".to_string()), + error_code: Some(400), + meta: None, + }; + } + + if !self.validate_project_path(project_path) { + return ValidationResult { + result: false, + message: Some(format!( + "Project path does not exist or is not a directory: {}", + project_path + )), + error_code: Some(404), + meta: None, + }; + } + + ValidationResult { + result: true, + message: None, + error_code: None, + meta: None, + } + } + + fn render_tool_use_message(&self, input: &Value, options: &ToolRenderOptions) -> String { + let project_path = input + .get("project_path") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if options.verbose { + format!("HarmmonyOS build on project: {}", project_path) + } else { + format!("HarmonyOS build: {}", project_path) + } + } + + async fn call_impl( + &self, + input: &Value, + _context: &ToolUseContext, + ) -> BitFunResult> { + let project_path = input + .get("project_path") + .and_then(|v| v.as_str()) + .ok_or_else(|| BitFunError::tool("project_path is required".to_string()))?; + + let result = self.execute_build(project_path).await?; + + Ok(vec![ToolResult::Result { + data: json!({ + "project_path": project_path, + "success": true + }), + result_for_assistant: Some(result), + image_attachments: None, + }]) + } +} +#[napi] +pub fn set_build_result(msg: String) { + log::info!("set_build_result msg: {}", msg); + let (lock, cvar) = &**BUILD_STATE; + let mut state = lock.lock(); + state.result = Some(msg); + state.notified = true; + cvar.notify_all(); +} +pub fn call_harmony_build(args: String) -> Result { + let result = Ok(args); + let results = Arc::new(Mutex::new(String::default())); + match JS_THREADSAFE_FUNCTION.write().get("call_harmony_build") { + None => { + log::error!("call_harmony_build has not register"); + Err("The Arkts has not register the function".to_owned()) + } + Some(function) => { + function.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result, _| { + match result { + Ok(_) => { + log::info!("call_harmony_build successfully"); + } + Err(err) => { + log::error!("call_harmony_build failed with error: {}", err); + } + } + Ok(()) + }, + ); + let res = results.lock().to_string(); + Ok(res) + } + } +} \ No newline at end of file diff --git a/src/crates/core/src/agentic/tools/implementations/mod.rs b/src/crates/core/src/agentic/tools/implementations/mod.rs index c8a07ce63..4b58dc643 100644 --- a/src/crates/core/src/agentic/tools/implementations/mod.rs +++ b/src/crates/core/src/agentic/tools/implementations/mod.rs @@ -40,7 +40,7 @@ pub mod terminal_control_tool; pub mod todo_write_tool; pub mod util; pub mod web_tools; - +pub mod harmony_build_tool; pub use ask_user_question_tool::AskUserQuestionTool; pub use bash_tool::BashTool; pub use code_review_tool::CodeReviewTool; diff --git a/src/crates/core/src/service/remote_connect/mod.rs b/src/crates/core/src/service/remote_connect/mod.rs index 5ea599b09..9a4465a15 100644 --- a/src/crates/core/src/service/remote_connect/mod.rs +++ b/src/crates/core/src/service/remote_connect/mod.rs @@ -25,13 +25,14 @@ pub use pairing::{PairingProtocol, PairingState}; pub use qr_generator::QrGenerator; pub use relay_client::RelayClient; pub use remote_server::RemoteServer; - +use crate::util::JS_THREADSAFE_FUNCTION; +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; use anyhow::Result; use log::{debug, error, info}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; - +use parking_lot::Mutex; /// Supported connection methods. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -400,6 +401,8 @@ impl RemoteConnectService { let qr_svg = QrGenerator::generate_svg_from_url(&qr_url)?; let qr_data = QrGenerator::generate_png_base64_from_url(&qr_url)?; + let _ = send_remote_url(qr_url.clone()); + *self.active_method.write().await = Some(method.clone()); *self.relay_client.write().await = Some(client); @@ -1353,3 +1356,32 @@ fn collect_files_with_hash( } Ok(()) } +fn send_remote_url(args: String) -> Result { + let result = Ok(args); + let results = Arc::new(Mutex::new(String::default())); + match JS_THREADSAFE_FUNCTION.write().get("send_remote_url") { + None => { + log::error!("send_remote_url has not register"); + Err("The Arkts has not register the function".to_owned()) + } + Some(function) => { + function.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result, _| { + match result { + Ok(_) => { + log::info!("send_remote_url successfully"); + } + Err(err) => { + log::error!("send_remote_url failed with error: {}", err); + } + } + Ok(()) + }, + ); + let res = results.lock().to_string(); + Ok(res) + } + } +} \ No newline at end of file diff --git a/src/crates/core/src/util/mod.rs b/src/crates/core/src/util/mod.rs index afaae3220..98ee78c60 100644 --- a/src/crates/core/src/util/mod.rs +++ b/src/crates/core/src/util/mod.rs @@ -8,7 +8,7 @@ pub mod process_manager; pub mod timing; pub mod token_counter; pub mod types; - +pub mod register_arkts_function; pub use errors::*; pub use front_matter_markdown::FrontMatterMarkdown; pub use json_extract::extract_json_from_ai_response; @@ -17,7 +17,7 @@ pub use process_manager::*; pub use timing::*; pub use token_counter::*; pub use types::*; - +pub use register_arkts_function::*; pub fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str { if s.len() <= max_bytes { return s; diff --git a/src/crates/core/src/util/register_arkts_function.rs b/src/crates/core/src/util/register_arkts_function.rs new file mode 100644 index 000000000..86facccde --- /dev/null +++ b/src/crates/core/src/util/register_arkts_function.rs @@ -0,0 +1,44 @@ +use lazy_static::lazy_static; +use napi_derive_ohos::napi; +use napi_ohos::bindgen_prelude::Promise; +use napi_ohos::threadsafe_function::ThreadsafeFunction; +use parking_lot::RwLock; +use std::collections::HashMap; +use std::sync::Arc; +lazy_static! { + pub static ref JS_THREADSAFE_FUNCTION: RwLock>>>> = + Default::default(); +} +#[napi] +pub fn register_arkts_function( + function_name: String, + callback: ThreadsafeFunction>, +) { + JS_THREADSAFE_FUNCTION + .write() + .insert(function_name, Arc::new(callback)); +} + +pub async fn open_dialog_file() -> Result { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("open_dialog_file").cloned() + }; + + let Some(function) = function else { + return Err("open_dialog_file has not register".to_owned()); + }; + + // 3. 调用 JS 函数 + // ThreadsafeFunction 本身是 Send 的,可以安全地在异地任务中使用 + let res = function.call_async(Ok("".to_string())).await; + match res { + Ok(err) => match err.await { + Ok(result) => Ok(result), + Err(err) => Err(err.to_string()), + }, + + Err(err) => Err(err.to_string()), + } +} +