初始化提交
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:
iven
2026-03-01 16:24:24 +08:00
commit 92e5def702
492 changed files with 211343 additions and 0 deletions

View 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 }

View 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),
}

File diff suppressed because it is too large Load Diff

View 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"));
}
}