mofa_plugins/rhai_runtime/
plugin.rs1use super::types::{PluginMetadata, RhaiPluginResult};
6use mofa_extra::rhai::{RhaiScriptEngine, ScriptContext, ScriptEngineConfig};
7use mofa_kernel::plugin::{
8 AgentPlugin, PluginContext, PluginMetadata as KernelPluginMetadata, PluginResult, PluginState,
9 PluginType,
10};
11use rhai::Dynamic;
12use std::any::Any;
13use std::collections::HashMap;
14use std::path::PathBuf;
15use std::sync::Arc;
16use tokio::sync::RwLock;
17use tracing::{error, info, warn};
18
19#[derive(Debug, Clone)]
25pub struct RhaiPluginConfig {
26 pub source: RhaiPluginSource,
28 pub engine_config: ScriptEngineConfig,
30 pub initial_context: HashMap<String, Dynamic>,
32 pub dependencies: Vec<String>,
34 pub plugin_id: String,
36}
37
38impl Default for RhaiPluginConfig {
39 fn default() -> Self {
40 Self {
41 source: RhaiPluginSource::Inline("".to_string()),
42 engine_config: ScriptEngineConfig::default(),
43 initial_context: HashMap::new(),
44 dependencies: Vec::new(),
45 plugin_id: uuid::Uuid::now_v7().to_string(),
46 }
47 }
48}
49
50impl RhaiPluginConfig {
51 pub fn new_inline(plugin_id: &str, script_content: &str) -> Self {
53 Self {
54 source: RhaiPluginSource::Inline(script_content.to_string()),
55 plugin_id: plugin_id.to_string(),
56 ..Default::default()
57 }
58 }
59
60 pub fn new_file(plugin_id: &str, file_path: &PathBuf) -> Self {
62 Self {
63 source: RhaiPluginSource::File(file_path.clone()),
64 plugin_id: plugin_id.to_string(),
65 ..Default::default()
66 }
67 }
68
69 pub fn with_engine_config(mut self, config: ScriptEngineConfig) -> Self {
71 self.engine_config = config;
72 self
73 }
74
75 pub fn with_context_var(mut self, key: &str, value: Dynamic) -> Self {
77 self.initial_context.insert(key.to_string(), value);
78 self
79 }
80}
81
82#[derive(Debug, Clone)]
84pub enum RhaiPluginSource {
85 Inline(String),
87 File(PathBuf),
89}
90
91impl RhaiPluginSource {
92 pub async fn get_content(&self) -> RhaiPluginResult<String> {
94 match self {
95 RhaiPluginSource::Inline(content) => Ok(content.clone()),
96 RhaiPluginSource::File(path) => Ok(std::fs::read_to_string(path)?),
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum RhaiPluginState {
108 Unloaded,
110 Loading,
112 Loaded,
114 Initializing,
116 Running,
118 Paused,
120 Error(String),
122}
123
124impl From<RhaiPluginState> for PluginState {
125 fn from(state: RhaiPluginState) -> Self {
126 match state {
127 RhaiPluginState::Unloaded => PluginState::Unloaded,
128 RhaiPluginState::Loading => PluginState::Loading,
129 RhaiPluginState::Loaded => PluginState::Loaded,
130 RhaiPluginState::Initializing => PluginState::Loading,
131 RhaiPluginState::Running => PluginState::Running,
132 RhaiPluginState::Paused => PluginState::Paused,
133 RhaiPluginState::Error(err) => PluginState::Error(err),
134 }
135 }
136}
137
138pub struct RhaiPlugin {
144 id: String,
146 config: RhaiPluginConfig,
148 engine: Arc<RhaiScriptEngine>,
150 metadata: PluginMetadata,
152 kernel_metadata: KernelPluginMetadata,
154 state: RwLock<RhaiPluginState>,
156 plugin_context: RwLock<Option<PluginContext>>,
158 last_modified: u64,
160 cached_content: String,
162}
163
164impl RhaiPlugin {
165 pub fn last_modified(&self) -> u64 {
167 self.last_modified
168 }
169
170 pub async fn new(config: RhaiPluginConfig) -> RhaiPluginResult<Self> {
172 let content = config.source.get_content().await?;
173 let last_modified = std::time::SystemTime::now()
174 .duration_since(std::time::UNIX_EPOCH)
175 .unwrap_or_default()
176 .as_secs();
177
178 let engine = Arc::new(RhaiScriptEngine::new(config.engine_config.clone())?);
180
181 let _script_metadata: HashMap<String, String> = HashMap::new();
183
184 let mut metadata = PluginMetadata::default();
186 metadata.id = config.plugin_id.clone();
187
188 let kernel_metadata = KernelPluginMetadata::new(
190 &config.plugin_id,
191 &metadata.name,
192 PluginType::Tool,
193 );
194
195 Ok(Self {
197 id: config.plugin_id.clone(),
198 config,
199 engine,
200 metadata,
201 kernel_metadata,
202 state: RwLock::new(RhaiPluginState::Unloaded),
203 plugin_context: RwLock::new(None),
204 last_modified,
205 cached_content: content,
206 })
207 }
208
209 pub async fn from_file(plugin_id: &str, path: &PathBuf) -> RhaiPluginResult<Self> {
211 let config = RhaiPluginConfig::new_file(plugin_id, path);
212 Self::new(config).await
213 }
214
215 pub async fn from_content(plugin_id: &str, content: &str) -> RhaiPluginResult<Self> {
217 let config = RhaiPluginConfig::new_inline(plugin_id, content);
218 Self::new(config).await
219 }
220
221 pub async fn reload(&mut self) -> RhaiPluginResult<()> {
223 let new_content = self.config.source.get_content().await?;
224 self.cached_content = new_content;
225
226 self.last_modified = match &self.config.source {
228 RhaiPluginSource::File(path) => std::fs::metadata(path)?
229 .modified()?
230 .duration_since(std::time::UNIX_EPOCH)
231 .expect("时间转换失败")
232 .as_secs(),
233 _ => std::time::SystemTime::now()
234 .duration_since(std::time::UNIX_EPOCH)
235 .unwrap_or_default()
236 .as_secs(),
237 };
238
239 self.extract_metadata().await?;
241
242 Ok(())
243 }
244
245 async fn extract_metadata(&mut self) -> RhaiPluginResult<()> {
247 let script_id = format!("{}_metadata", self.id);
249 if let Err(e) = self
250 .engine
251 .compile_and_cache(&script_id, "metadata", &self.cached_content)
252 .await
253 {
254 warn!("Failed to compile script for metadata extraction: {}", e);
255 return Ok(());
256 }
257
258 let context = mofa_extra::rhai::ScriptContext::new();
259
260 if let Ok(_) = self.engine.execute_compiled(&script_id, &context).await {
262 if let Ok(result) = self.engine.execute("plugin_name", &context).await {
265 if result.success {
266 if let Some(name) = result.value.as_str() {
267 self.metadata.name = name.to_string();
268 }
269 }
270 }
271
272 if let Ok(result) = self.engine.execute("plugin_version", &context).await {
274 if result.success {
275 if let Some(version) = result.value.as_str() {
276 self.metadata.version = version.to_string();
277 }
278 }
279 }
280
281 if let Ok(result) = self.engine.execute("plugin_description", &context).await {
283 if result.success {
284 if let Some(description) = result.value.as_str() {
285 self.metadata.description = description.to_string();
286 }
287 }
288 }
289 }
290
291 Ok(())
292 }
293
294 async fn call_script_function(
296 &self,
297 _function_name: &str,
298 _args: &[Dynamic],
299 ) -> RhaiPluginResult<Option<Dynamic>> {
300 Ok(None)
305 }
306}
307
308#[async_trait::async_trait]
313impl AgentPlugin for RhaiPlugin {
314 fn metadata(&self) -> &KernelPluginMetadata {
315 &self.kernel_metadata
316 }
317
318 fn state(&self) -> PluginState {
319 tokio::task::block_in_place(|| {
321 let state = self.state.blocking_read();
322 state.clone().into()
323 })
324 }
325
326 async fn load(&mut self, ctx: &PluginContext) -> PluginResult<()> {
327 let mut state = self.state.write().await;
328 *state = RhaiPluginState::Loading;
329 drop(state);
330
331 *self.plugin_context.write().await = Some(ctx.clone());
333
334 self.extract_metadata().await?;
336
337 let mut state = self.state.write().await;
338 *state = RhaiPluginState::Loaded;
339 Ok(())
340 }
341
342 async fn init_plugin(&mut self) -> PluginResult<()> {
343 let mut state = self.state.write().await;
344 if *state != RhaiPluginState::Loaded {
345 return Err(anyhow::anyhow!("Plugin not loaded"));
346 }
347
348 *state = RhaiPluginState::Initializing;
349 drop(state);
350
351 match self.call_script_function("init", &[]).await {
353 Ok(_) => {
354 info!("Rhai plugin {}: init function called", self.id);
355 }
356 Err(e) => {
357 warn!("Rhai plugin {}: init function failed: {}", self.id, e);
358 }
359 }
360
361 let mut state = self.state.write().await;
362 *state = RhaiPluginState::Running;
363 Ok(())
364 }
365
366 async fn start(&mut self) -> PluginResult<()> {
367 let mut state = self.state.write().await;
368 if *state != RhaiPluginState::Running && *state != RhaiPluginState::Paused {
369 return Err(anyhow::anyhow!("Plugin not ready to start"));
370 }
371
372 match self.call_script_function("start", &[]).await {
374 Ok(_) => {
375 info!("Rhai plugin {}: start function called", self.id);
376 }
377 Err(e) => {
378 warn!("Rhai plugin {}: start function failed: {}", self.id, e);
379 }
380 }
381
382 *state = RhaiPluginState::Running;
383 Ok(())
384 }
385
386 async fn stop(&mut self) -> PluginResult<()> {
387 let mut state = self.state.write().await;
388 if *state != RhaiPluginState::Running {
389 return Err(anyhow::anyhow!("Plugin not running"));
390 }
391
392 match self.call_script_function("stop", &[]).await {
394 Ok(_) => {
395 info!("Rhai plugin {}: stop function called", self.id);
396 }
397 Err(e) => {
398 warn!("Rhai plugin {}: stop function failed: {}", self.id, e);
399 }
400 }
401
402 *state = RhaiPluginState::Paused;
403 Ok(())
404 }
405
406 async fn unload(&mut self) -> PluginResult<()> {
407 let mut state = self.state.write().await;
408 *state = RhaiPluginState::Unloaded;
409
410 match self.call_script_function("unload", &[]).await {
412 Ok(_) => {
413 info!("Rhai plugin {}: unload function called", self.id);
414 }
415 Err(e) => {
416 warn!("Rhai plugin {}: unload function failed: {}", self.id, e);
417 }
418 }
419
420 Ok(())
421 }
422
423 async fn execute(&mut self, input: String) -> PluginResult<String> {
424 let state = self.state.read().await;
425 if *state != RhaiPluginState::Running {
426 return Err(anyhow::anyhow!("Plugin not running"));
427 }
428 drop(state);
429
430 let mut context = ScriptContext::new();
432 context = context.with_variable("input", input.clone())?;
433
434 let script_id = format!("{}_exec", self.id);
436 self.engine
437 .compile_and_cache(&script_id, "execute", &self.cached_content)
438 .await?;
439
440 match self
442 .engine
443 .call_function::<serde_json::Value>(
444 &script_id,
445 "execute",
446 vec![serde_json::json!(input)],
447 &context,
448 )
449 .await
450 {
451 Ok(result) => {
452 info!(
453 "Rhai plugin {} executed successfully via call_function",
454 self.id
455 );
456 Ok(serde_json::to_string_pretty(&result)?)
457 }
458 Err(e) => {
459 warn!(
460 "Failed to call execute function: {}, falling back to direct execution",
461 e
462 );
463
464 let result = self.engine.execute(&self.cached_content, &context).await?;
466
467 if !result.success {
468 return Err(anyhow::anyhow!(
469 "Script execution failed: {:?}",
470 result.error
471 ));
472 }
473
474 Ok(serde_json::to_string_pretty(&result.value)?)
475 }
476 }
477 }
478
479 fn stats(&self) -> HashMap<String, serde_json::Value> {
480 HashMap::new() }
482
483 fn as_any(&self) -> &dyn Any {
484 self
485 }
486
487 fn as_any_mut(&mut self) -> &mut dyn Any {
488 self
489 }
490
491 fn into_any(self: Box<Self>) -> Box<dyn Any> {
492 self
493 }
494}
495
496#[cfg(test)]
501mod tests {
502 use super::*;
503
504 static TEST_PLUGIN_SCRIPT: &str = r#"
505 let plugin_name = "test_rhai_plugin";
506 let plugin_version = "1.0.0";
507 let plugin_description = "Test Rhai plugin";
508
509 fn init() {
510 print("Test plugin initialized");
511 }
512
513 fn execute(input) {
514 "Hello from Rhai plugin! You said: " + input
515 }
516 "#;
517
518 #[tokio::test]
519 async fn test_rhai_plugin_from_content() {
520 let plugin = RhaiPlugin::from_content("test-plugin", TEST_PLUGIN_SCRIPT)
521 .await
522 .unwrap();
523
524 assert_eq!(plugin.id, "test-plugin");
525 assert!(!plugin.cached_content.is_empty());
529 }
530
531 #[tokio::test]
532 async fn test_rhai_plugin_lifecycle() {
533 let mut plugin = RhaiPlugin::from_content("test-plugin", TEST_PLUGIN_SCRIPT)
534 .await
535 .unwrap();
536
537 let ctx = PluginContext::default();
538 plugin.load(&ctx).await.unwrap();
539 assert!(matches!(
540 *plugin.state.read().await,
541 RhaiPluginState::Loaded
542 ));
543
544 plugin.init_plugin().await.unwrap();
545 assert!(matches!(
546 *plugin.state.read().await,
547 RhaiPluginState::Running
548 ));
549
550 plugin.stop().await.unwrap();
551 assert!(matches!(
552 *plugin.state.read().await,
553 RhaiPluginState::Paused
554 ));
555
556 plugin.start().await.unwrap();
557 assert!(matches!(
558 *plugin.state.read().await,
559 RhaiPluginState::Running
560 ));
561
562 plugin.unload().await.unwrap();
563 assert!(matches!(
564 *plugin.state.read().await,
565 RhaiPluginState::Unloaded
566 ));
567 }
568
569 #[tokio::test]
570 async fn test_rhai_plugin_execute() {
571 let mut plugin = RhaiPlugin::from_content("test-plugin", TEST_PLUGIN_SCRIPT)
572 .await
573 .unwrap();
574
575 let ctx = PluginContext::default();
576 plugin.load(&ctx).await.unwrap();
577 plugin.init_plugin().await.unwrap();
578
579 let result = plugin.execute("Hello World!".to_string()).await.unwrap();
580 println!("Execute result: {}", result);
583
584 assert!(
587 result.contains("Hello from Rhai plugin!") || result.contains("Hello World!"),
588 "Result should contain expected text, got: {}",
589 result
590 );
591
592 plugin.unload().await.unwrap();
593 }
594}