manifest.rs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. use std::path::{Path, PathBuf};
  2. use serde::{Deserialize, Serialize};
  3. use crate::PluginSourceKind;
  4. pub trait Plugin {
  5. fn id(&self) -> &str;
  6. fn manifest(&self) -> &PluginManifest;
  7. fn source_kind(&self) -> PluginSourceKind;
  8. fn root(&self) -> Option<&Path>;
  9. fn resolved_hooks(&self) -> PluginHooks {
  10. self.manifest().hooks.resolve(self.root())
  11. }
  12. }
  13. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
  14. pub struct PluginHooks {
  15. #[serde(rename = "PreToolUse", alias = "preToolUse", default)]
  16. pub pre_tool_use: Vec<String>,
  17. #[serde(rename = "PostToolUse", alias = "postToolUse", default)]
  18. pub post_tool_use: Vec<String>,
  19. }
  20. impl PluginHooks {
  21. #[must_use]
  22. pub fn resolve(&self, root: Option<&Path>) -> Self {
  23. let Some(root) = root else {
  24. return self.clone();
  25. };
  26. let replacement = root.display().to_string();
  27. Self {
  28. pre_tool_use: self
  29. .pre_tool_use
  30. .iter()
  31. .map(|value| value.replace("${PLUGIN_DIR}", &replacement))
  32. .collect(),
  33. post_tool_use: self
  34. .post_tool_use
  35. .iter()
  36. .map(|value| value.replace("${PLUGIN_DIR}", &replacement))
  37. .collect(),
  38. }
  39. }
  40. }
  41. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  42. pub struct PluginManifest {
  43. pub name: String,
  44. pub description: String,
  45. #[serde(default = "default_version")]
  46. pub version: String,
  47. #[serde(default)]
  48. pub default_enabled: bool,
  49. #[serde(default)]
  50. pub hooks: PluginHooks,
  51. }
  52. impl PluginManifest {
  53. pub fn validate(&self) -> Result<(), String> {
  54. if self.name.trim().is_empty() {
  55. return Err("plugin manifest name must not be empty".to_string());
  56. }
  57. if self.description.trim().is_empty() {
  58. return Err(format!(
  59. "plugin manifest description must not be empty for {}",
  60. self.name
  61. ));
  62. }
  63. if self.version.trim().is_empty() {
  64. return Err(format!(
  65. "plugin manifest version must not be empty for {}",
  66. self.name
  67. ));
  68. }
  69. if self
  70. .hooks
  71. .pre_tool_use
  72. .iter()
  73. .chain(self.hooks.post_tool_use.iter())
  74. .any(|hook| hook.trim().is_empty())
  75. {
  76. return Err(format!(
  77. "plugin manifest hook entries must not be empty for {}",
  78. self.name
  79. ));
  80. }
  81. Ok(())
  82. }
  83. }
  84. fn default_version() -> String {
  85. "0.1.0".to_string()
  86. }
  87. #[derive(Debug, Clone, PartialEq, Eq)]
  88. pub struct LoadedPlugin {
  89. pub id: String,
  90. pub source_kind: PluginSourceKind,
  91. pub manifest: PluginManifest,
  92. pub root: Option<PathBuf>,
  93. pub origin: Option<PathBuf>,
  94. }
  95. impl LoadedPlugin {
  96. #[must_use]
  97. pub fn new(
  98. id: String,
  99. source_kind: PluginSourceKind,
  100. manifest: PluginManifest,
  101. root: Option<PathBuf>,
  102. origin: Option<PathBuf>,
  103. ) -> Self {
  104. Self {
  105. id,
  106. source_kind,
  107. manifest,
  108. root,
  109. origin,
  110. }
  111. }
  112. #[must_use]
  113. pub fn name(&self) -> &str {
  114. &self.manifest.name
  115. }
  116. }
  117. impl Plugin for LoadedPlugin {
  118. fn id(&self) -> &str {
  119. &self.id
  120. }
  121. fn manifest(&self) -> &PluginManifest {
  122. &self.manifest
  123. }
  124. fn source_kind(&self) -> PluginSourceKind {
  125. self.source_kind
  126. }
  127. fn root(&self) -> Option<&Path> {
  128. self.root.as_deref()
  129. }
  130. }
  131. #[cfg(test)]
  132. mod tests {
  133. use super::{PluginHooks, PluginManifest};
  134. use std::path::Path;
  135. #[test]
  136. fn validates_manifest_fields() {
  137. let manifest = PluginManifest {
  138. name: "demo".to_string(),
  139. description: "demo plugin".to_string(),
  140. version: "1.2.3".to_string(),
  141. default_enabled: false,
  142. hooks: PluginHooks::default(),
  143. };
  144. assert!(manifest.validate().is_ok());
  145. }
  146. #[test]
  147. fn resolves_plugin_dir_placeholders() {
  148. let hooks = PluginHooks {
  149. pre_tool_use: vec!["echo ${PLUGIN_DIR}/pre".to_string()],
  150. post_tool_use: vec!["echo ${PLUGIN_DIR}/post".to_string()],
  151. };
  152. let resolved = hooks.resolve(Some(Path::new("/tmp/plugin")));
  153. assert_eq!(resolved.pre_tool_use, vec!["echo /tmp/plugin/pre"]);
  154. assert_eq!(resolved.post_tool_use, vec!["echo /tmp/plugin/post"]);
  155. }
  156. }
备用站点 当前处于降级运行的备用站点,仅供应急访问,数据和功能可能不是最新。