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 state: RwLock<RhaiPluginState>,
154 plugin_context: RwLock<Option<PluginContext>>,
156 last_modified: u64,
158 cached_content: String,
160}
161
162impl RhaiPlugin {
163 pub fn last_modified(&self) -> u64 {
165 self.last_modified
166 }
167
168 pub async fn new(config: RhaiPluginConfig) -> RhaiPluginResult<Self> {
170 let content = config.source.get_content().await?;
171 let last_modified = std::time::SystemTime::now()
172 .duration_since(std::time::UNIX_EPOCH)
173 .unwrap_or_default()
174 .as_secs();
175
176 let engine = Arc::new(RhaiScriptEngine::new(config.engine_config.clone())?);
178
179 let _script_metadata: HashMap<String, String> = HashMap::new();
181
182 let mut metadata = PluginMetadata::default();
184 metadata.id = config.plugin_id.clone();
185
186 Ok(Self {
188 id: config.plugin_id.clone(),
189 config,
190 engine,
191 metadata,
192 state: RwLock::new(RhaiPluginState::Unloaded),
193 plugin_context: RwLock::new(None),
194 last_modified,
195 cached_content: content,
196 })
197 }
198
199 pub async fn from_file(plugin_id: &str, path: &PathBuf) -> RhaiPluginResult<Self> {
201 let config = RhaiPluginConfig::new_file(plugin_id, path);
202 Self::new(config).await
203 }
204
205 pub async fn from_content(plugin_id: &str, content: &str) -> RhaiPluginResult<Self> {
207 let config = RhaiPluginConfig::new_inline(plugin_id, content);
208 Self::new(config).await
209 }
210
211 pub async fn reload(&mut self) -> RhaiPluginResult<()> {
213 let new_content = self.config.source.get_content().await?;
214 self.cached_content = new_content;
215
216 self.last_modified = match &self.config.source {
218 RhaiPluginSource::File(path) => std::fs::metadata(path)?
219 .modified()?
220 .duration_since(std::time::UNIX_EPOCH)
221 .expect("时间转换失败")
222 .as_secs(),
223 _ => std::time::SystemTime::now()
224 .duration_since(std::time::UNIX_EPOCH)
225 .unwrap_or_default()
226 .as_secs(),
227 };
228
229 self.extract_metadata().await?;
231
232 Ok(())
233 }
234
235 async fn extract_metadata(&mut self) -> RhaiPluginResult<()> {
237 let script_id = format!("{}_metadata", self.id);
239 if let Err(e) = self.engine.compile_and_cache(&script_id, "metadata", &self.cached_content).await {
240 warn!("Failed to compile script for metadata extraction: {}", e);
241 return Ok(());
242 }
243
244 let context = mofa_extra::rhai::ScriptContext::new();
245
246 if let Ok(_) = self.engine.execute_compiled(&script_id, &context).await {
248 if let Ok(result) = self.engine.execute("plugin_name", &context).await {
251 if result.success {
252 if let Some(name) = result.value.as_str() {
253 self.metadata.name = name.to_string();
254 }
255 }
256 }
257
258 if let Ok(result) = self.engine.execute("plugin_version", &context).await {
260 if result.success {
261 if let Some(version) = result.value.as_str() {
262 self.metadata.version = version.to_string();
263 }
264 }
265 }
266
267 if let Ok(result) = self.engine.execute("plugin_description", &context).await {
269 if result.success {
270 if let Some(description) = result.value.as_str() {
271 self.metadata.description = description.to_string();
272 }
273 }
274 }
275 }
276
277 Ok(())
278 }
279
280 async fn call_script_function(
282 &self,
283 _function_name: &str,
284 _args: &[Dynamic],
285 ) -> RhaiPluginResult<Option<Dynamic>> {
286 Ok(None)
291 }
292}
293
294#[async_trait::async_trait]
299impl AgentPlugin for RhaiPlugin {
300 fn metadata(&self) -> &KernelPluginMetadata {
301 Box::leak(Box::new(KernelPluginMetadata::new(
304 &self.id,
305 &self.metadata.name,
306 PluginType::Tool,
307 )))
308 }
309
310 fn state(&self) -> PluginState {
311 tokio::task::block_in_place(|| {
313 let state = self.state.blocking_read();
314 state.clone().into()
315 })
316 }
317
318 async fn load(&mut self, ctx: &PluginContext) -> PluginResult<()> {
319 let mut state = self.state.write().await;
320 *state = RhaiPluginState::Loading;
321 drop(state);
322
323 *self.plugin_context.write().await = Some(ctx.clone());
325
326 self.extract_metadata().await?;
328
329 let mut state = self.state.write().await;
330 *state = RhaiPluginState::Loaded;
331 Ok(())
332 }
333
334 async fn init_plugin(&mut self) -> PluginResult<()> {
335 let mut state = self.state.write().await;
336 if *state != RhaiPluginState::Loaded {
337 return Err(anyhow::anyhow!("Plugin not loaded"));
338 }
339
340 *state = RhaiPluginState::Initializing;
341 drop(state);
342
343 match self.call_script_function("init", &[]).await {
345 Ok(_) => {
346 info!("Rhai plugin {}: init function called", self.id);
347 }
348 Err(e) => {
349 warn!("Rhai plugin {}: init function failed: {}", self.id, e);
350 }
351 }
352
353 let mut state = self.state.write().await;
354 *state = RhaiPluginState::Running;
355 Ok(())
356 }
357
358 async fn start(&mut self) -> PluginResult<()> {
359 let mut state = self.state.write().await;
360 if *state != RhaiPluginState::Running && *state != RhaiPluginState::Paused {
361 return Err(anyhow::anyhow!("Plugin not ready to start"));
362 }
363
364 match self.call_script_function("start", &[]).await {
366 Ok(_) => {
367 info!("Rhai plugin {}: start function called", self.id);
368 }
369 Err(e) => {
370 warn!("Rhai plugin {}: start function failed: {}", self.id, e);
371 }
372 }
373
374 *state = RhaiPluginState::Running;
375 Ok(())
376 }
377
378 async fn stop(&mut self) -> PluginResult<()> {
379 let mut state = self.state.write().await;
380 if *state != RhaiPluginState::Running {
381 return Err(anyhow::anyhow!("Plugin not running"));
382 }
383
384 match self.call_script_function("stop", &[]).await {
386 Ok(_) => {
387 info!("Rhai plugin {}: stop function called", self.id);
388 }
389 Err(e) => {
390 warn!("Rhai plugin {}: stop function failed: {}", self.id, e);
391 }
392 }
393
394 *state = RhaiPluginState::Paused;
395 Ok(())
396 }
397
398 async fn unload(&mut self) -> PluginResult<()> {
399 let mut state = self.state.write().await;
400 *state = RhaiPluginState::Unloaded;
401
402 match self.call_script_function("unload", &[]).await {
404 Ok(_) => {
405 info!("Rhai plugin {}: unload function called", self.id);
406 }
407 Err(e) => {
408 warn!("Rhai plugin {}: unload function failed: {}", self.id, e);
409 }
410 }
411
412 Ok(())
413 }
414
415 async fn execute(&mut self, input: String) -> PluginResult<String> {
416 let state = self.state.read().await;
417 if *state != RhaiPluginState::Running {
418 return Err(anyhow::anyhow!("Plugin not running"));
419 }
420 drop(state);
421
422 let mut context = ScriptContext::new();
424 context = context.with_variable("input", input.clone())?;
425
426 let script_id = format!("{}_exec", self.id);
428 self.engine.compile_and_cache(&script_id, "execute", &self.cached_content).await?;
429
430 match self.engine.call_function::<serde_json::Value>(
432 &script_id,
433 "execute",
434 vec![serde_json::json!(input)],
435 &context,
436 ).await {
437 Ok(result) => {
438 info!("Rhai plugin {} executed successfully via call_function", self.id);
439 Ok(serde_json::to_string_pretty(&result)?)
440 }
441 Err(e) => {
442 warn!("Failed to call execute function: {}, falling back to direct execution", e);
443
444 let result = self.engine.execute(&self.cached_content, &context).await?;
446
447 if !result.success {
448 return Err(anyhow::anyhow!("Script execution failed: {:?}", result.error));
449 }
450
451 Ok(serde_json::to_string_pretty(&result.value)?)
452 }
453 }
454 }
455
456 fn stats(&self) -> HashMap<String, serde_json::Value> {
457 HashMap::new() }
459
460 fn as_any(&self) -> &dyn Any {
461 self
462 }
463
464 fn as_any_mut(&mut self) -> &mut dyn Any {
465 self
466 }
467
468 fn into_any(self: Box<Self>) -> Box<dyn Any> {
469 self
470 }
471}
472
473#[cfg(test)]
478mod tests {
479 use super::*;
480
481 static TEST_PLUGIN_SCRIPT: &str = r#"
482 let plugin_name = "test_rhai_plugin";
483 let plugin_version = "1.0.0";
484 let plugin_description = "Test Rhai plugin";
485
486 fn init() {
487 print("Test plugin initialized");
488 }
489
490 fn execute(input) {
491 "Hello from Rhai plugin! You said: " + input
492 }
493 "#;
494
495 #[tokio::test]
496 async fn test_rhai_plugin_from_content() {
497 let plugin = RhaiPlugin::from_content("test-plugin", TEST_PLUGIN_SCRIPT)
498 .await
499 .unwrap();
500
501 assert_eq!(plugin.id, "test-plugin");
502 assert!(!plugin.cached_content.is_empty());
506 }
507
508 #[tokio::test]
509 async fn test_rhai_plugin_lifecycle() {
510 let mut plugin = RhaiPlugin::from_content("test-plugin", TEST_PLUGIN_SCRIPT)
511 .await
512 .unwrap();
513
514 let ctx = PluginContext::default();
515 plugin.load(&ctx).await.unwrap();
516 assert!(matches!(
517 *plugin.state.read().await,
518 RhaiPluginState::Loaded
519 ));
520
521 plugin.init_plugin().await.unwrap();
522 assert!(matches!(
523 *plugin.state.read().await,
524 RhaiPluginState::Running
525 ));
526
527 plugin.stop().await.unwrap();
528 assert!(matches!(
529 *plugin.state.read().await,
530 RhaiPluginState::Paused
531 ));
532
533 plugin.start().await.unwrap();
534 assert!(matches!(
535 *plugin.state.read().await,
536 RhaiPluginState::Running
537 ));
538
539 plugin.unload().await.unwrap();
540 assert!(matches!(
541 *plugin.state.read().await,
542 RhaiPluginState::Unloaded
543 ));
544 }
545
546 #[tokio::test]
547 async fn test_rhai_plugin_execute() {
548 let mut plugin = RhaiPlugin::from_content("test-plugin", TEST_PLUGIN_SCRIPT)
549 .await
550 .unwrap();
551
552 let ctx = PluginContext::default();
553 plugin.load(&ctx).await.unwrap();
554 plugin.init_plugin().await.unwrap();
555
556 let result = plugin.execute("Hello World!".to_string()).await.unwrap();
557 println!("Execute result: {}", result);
560
561 assert!(result.contains("Hello from Rhai plugin!") || result.contains("Hello World!"),
564 "Result should contain expected text, got: {}", result);
565
566 plugin.unload().await.unwrap();
567 }
568}