初始化提交
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
This commit is contained in:
23
crates/openfang-migrate/Cargo.toml
Normal file
23
crates/openfang-migrate/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "openfang-migrate"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Migration engine for importing from other agent frameworks into OpenFang"
|
||||
|
||||
[dependencies]
|
||||
openfang-types = { path = "../openfang-types" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
json5 = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
77
crates/openfang-migrate/src/lib.rs
Normal file
77
crates/openfang-migrate/src/lib.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! Migration engine for importing from other agent frameworks into OpenFang.
|
||||
//!
|
||||
//! Supports importing agents, memory, sessions, skills, and channel configs
|
||||
//! from OpenClaw and other frameworks.
|
||||
|
||||
pub mod openclaw;
|
||||
pub mod report;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Source framework to migrate from.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MigrateSource {
|
||||
/// OpenClaw agent framework.
|
||||
OpenClaw,
|
||||
/// LangChain (future).
|
||||
LangChain,
|
||||
/// AutoGPT (future).
|
||||
AutoGpt,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MigrateSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::OpenClaw => write!(f, "OpenClaw"),
|
||||
Self::LangChain => write!(f, "LangChain"),
|
||||
Self::AutoGpt => write!(f, "AutoGPT"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for running a migration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MigrateOptions {
|
||||
/// Source framework.
|
||||
pub source: MigrateSource,
|
||||
/// Path to the source workspace directory.
|
||||
pub source_dir: PathBuf,
|
||||
/// Path to the OpenFang home directory.
|
||||
pub target_dir: PathBuf,
|
||||
/// If true, only report what would be done without making changes.
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
/// Run a migration with the given options.
|
||||
pub fn run_migration(options: &MigrateOptions) -> Result<report::MigrationReport, MigrateError> {
|
||||
match options.source {
|
||||
MigrateSource::OpenClaw => openclaw::migrate(options),
|
||||
MigrateSource::LangChain => Err(MigrateError::UnsupportedSource(
|
||||
"LangChain migration is not yet supported. Coming soon!".to_string(),
|
||||
)),
|
||||
MigrateSource::AutoGpt => Err(MigrateError::UnsupportedSource(
|
||||
"AutoGPT migration is not yet supported. Coming soon!".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur during migration.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MigrateError {
|
||||
#[error("Source directory not found: {0}")]
|
||||
SourceNotFound(PathBuf),
|
||||
#[error("Failed to parse config: {0}")]
|
||||
ConfigParse(String),
|
||||
#[error("Failed to parse agent: {0}")]
|
||||
AgentParse(String),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("YAML parse error: {0}")]
|
||||
Yaml(#[from] serde_yaml::Error),
|
||||
#[error("JSON5 parse error: {0}")]
|
||||
Json5Parse(String),
|
||||
#[error("TOML serialization error: {0}")]
|
||||
TomlSerialize(#[from] toml::ser::Error),
|
||||
#[error("Unsupported source: {0}")]
|
||||
UnsupportedSource(String),
|
||||
}
|
||||
4237
crates/openfang-migrate/src/openclaw.rs
Normal file
4237
crates/openfang-migrate/src/openclaw.rs
Normal file
File diff suppressed because it is too large
Load Diff
211
crates/openfang-migrate/src/report.rs
Normal file
211
crates/openfang-migrate/src/report.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
//! Migration report generation.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Summary of a migration run.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MigrationReport {
|
||||
/// Source framework name.
|
||||
pub source: String,
|
||||
/// Items that were successfully imported.
|
||||
pub imported: Vec<MigrateItem>,
|
||||
/// Items that were skipped (with reason).
|
||||
pub skipped: Vec<SkippedItem>,
|
||||
/// Warnings generated during migration.
|
||||
pub warnings: Vec<String>,
|
||||
/// Whether this was a dry run.
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
/// A successfully imported item.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MigrateItem {
|
||||
/// What type of item (agent, config, memory, session, skill, channel).
|
||||
pub kind: ItemKind,
|
||||
/// Name or identifier.
|
||||
pub name: String,
|
||||
/// Destination path.
|
||||
pub destination: String,
|
||||
}
|
||||
|
||||
/// An item that was skipped.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SkippedItem {
|
||||
/// What type of item.
|
||||
pub kind: ItemKind,
|
||||
/// Name or identifier.
|
||||
pub name: String,
|
||||
/// Why it was skipped.
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
/// The type of migrated item.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ItemKind {
|
||||
Config,
|
||||
Agent,
|
||||
Memory,
|
||||
Session,
|
||||
Skill,
|
||||
Channel,
|
||||
Secret,
|
||||
}
|
||||
|
||||
impl fmt::Display for ItemKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Config => write!(f, "Config"),
|
||||
Self::Agent => write!(f, "Agent"),
|
||||
Self::Memory => write!(f, "Memory"),
|
||||
Self::Session => write!(f, "Session"),
|
||||
Self::Skill => write!(f, "Skill"),
|
||||
Self::Channel => write!(f, "Channel"),
|
||||
Self::Secret => write!(f, "Secret"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MigrationReport {
|
||||
/// Generate a human-readable Markdown summary.
|
||||
pub fn to_markdown(&self) -> String {
|
||||
let mut out = String::new();
|
||||
let mode = if self.dry_run { " (Dry Run)" } else { "" };
|
||||
|
||||
out.push_str(&format!(
|
||||
"# Migration Report: {} -> OpenFang{}\n\n",
|
||||
self.source, mode
|
||||
));
|
||||
|
||||
// Summary
|
||||
out.push_str("## Summary\n\n");
|
||||
out.push_str(&format!("- Imported: {} items\n", self.imported.len()));
|
||||
out.push_str(&format!("- Skipped: {} items\n", self.skipped.len()));
|
||||
out.push_str(&format!("- Warnings: {}\n\n", self.warnings.len()));
|
||||
|
||||
// Imported
|
||||
if !self.imported.is_empty() {
|
||||
out.push_str("## Imported\n\n");
|
||||
out.push_str("| Type | Name | Destination |\n");
|
||||
out.push_str("|------|------|-------------|\n");
|
||||
for item in &self.imported {
|
||||
out.push_str(&format!(
|
||||
"| {} | {} | {} |\n",
|
||||
item.kind, item.name, item.destination
|
||||
));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
// Skipped
|
||||
if !self.skipped.is_empty() {
|
||||
out.push_str("## Skipped\n\n");
|
||||
out.push_str("| Type | Name | Reason |\n");
|
||||
out.push_str("|------|------|--------|\n");
|
||||
for item in &self.skipped {
|
||||
out.push_str(&format!(
|
||||
"| {} | {} | {} |\n",
|
||||
item.kind, item.name, item.reason
|
||||
));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
// Warnings
|
||||
if !self.warnings.is_empty() {
|
||||
out.push_str("## Warnings\n\n");
|
||||
for w in &self.warnings {
|
||||
out.push_str(&format!("- {w}\n"));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
// Next steps
|
||||
out.push_str("## Next Steps\n\n");
|
||||
out.push_str("1. Review imported agent manifests in `~/.openfang/agents/`\n");
|
||||
out.push_str(
|
||||
"2. Review `~/.openfang/secrets.env` — verify tokens were migrated correctly\n",
|
||||
);
|
||||
out.push_str("3. Set any remaining API keys referenced in `~/.openfang/config.toml`\n");
|
||||
out.push_str("4. Start the daemon: `openfang start`\n");
|
||||
out.push_str("5. Test your agents: `openfang agent list`\n");
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Print the report to stdout in a friendly format.
|
||||
pub fn print_summary(&self) {
|
||||
let mode = if self.dry_run { " (dry run)" } else { "" };
|
||||
println!("\n Migration complete!{mode}\n");
|
||||
println!(" Imported: {} items", self.imported.len());
|
||||
println!(" Skipped: {} items", self.skipped.len());
|
||||
println!(" Warnings: {}", self.warnings.len());
|
||||
|
||||
if !self.imported.is_empty() {
|
||||
println!("\n Imported:");
|
||||
for item in &self.imported {
|
||||
println!(" [{}] {} -> {}", item.kind, item.name, item.destination);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.skipped.is_empty() {
|
||||
println!("\n Skipped:");
|
||||
for item in &self.skipped {
|
||||
println!(" [{}] {} — {}", item.kind, item.name, item.reason);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.warnings.is_empty() {
|
||||
println!("\n Warnings:");
|
||||
for w in &self.warnings {
|
||||
println!(" - {w}");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.dry_run {
|
||||
println!("\n Next steps:");
|
||||
println!(" openfang start");
|
||||
println!(" openfang agent list");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty_report() {
|
||||
let report = MigrationReport {
|
||||
source: "OpenClaw".to_string(),
|
||||
dry_run: false,
|
||||
..Default::default()
|
||||
};
|
||||
let md = report.to_markdown();
|
||||
assert!(md.contains("Migration Report: OpenClaw"));
|
||||
assert!(md.contains("Imported: 0 items"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_with_items() {
|
||||
let report = MigrationReport {
|
||||
source: "OpenClaw".to_string(),
|
||||
imported: vec![MigrateItem {
|
||||
kind: ItemKind::Agent,
|
||||
name: "coder".to_string(),
|
||||
destination: "~/.openfang/agents/coder/agent.toml".to_string(),
|
||||
}],
|
||||
skipped: vec![SkippedItem {
|
||||
kind: ItemKind::Skill,
|
||||
name: "custom-skill".to_string(),
|
||||
reason: "Unsupported format".to_string(),
|
||||
}],
|
||||
warnings: vec!["API key not found".to_string()],
|
||||
dry_run: true,
|
||||
};
|
||||
let md = report.to_markdown();
|
||||
assert!(md.contains("(Dry Run)"));
|
||||
assert!(md.contains("coder"));
|
||||
assert!(md.contains("Unsupported format"));
|
||||
assert!(md.contains("API key not found"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user