mod transform_agents;
mod transform_channels;
mod transform_config;
mod transform_cron;
mod transform_personality;
mod transform_sessions;
mod transform_skills;
use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use super::{AreaResult, MigrationArea, SafetyVerdict, copy_dir_recursive, scan_directory_safety};
pub(crate) use transform_agents::{export_agents, import_agents};
pub(crate) use transform_channels::{export_channels, import_channels};
pub(crate) use transform_config::{export_config, import_config};
pub(crate) use transform_cron::{export_cron, import_cron};
#[allow(unused_imports)] pub(crate) use transform_personality::{
export_personality, import_personality, markdown_to_personality_toml,
personality_toml_to_markdown, titlecase,
};
pub(crate) use transform_sessions::{export_sessions, import_sessions};
pub(crate) use transform_skills::{export_skills, import_skills};
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct LegacyConfig {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub provider: Option<String>,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default)]
pub api_url: Option<String>,
#[serde(default)]
pub temperature: Option<f64>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub channels: Option<LegacyChannels>,
#[serde(default, deserialize_with = "deserialize_cron")]
pub cron: Option<Vec<LegacyCronJob>>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
fn deserialize_cron<'de, D>(deserializer: D) -> Result<Option<Vec<LegacyCronJob>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
match value {
None => Ok(None),
Some(serde_json::Value::Array(arr)) => {
let jobs: Vec<LegacyCronJob> =
serde_json::from_value(serde_json::Value::Array(arr)).unwrap_or_default();
Ok(Some(jobs))
}
Some(serde_json::Value::Object(_)) => Ok(None),
_ => Ok(None),
}
}
#[derive(Debug, Deserialize, Serialize, Default)]
pub(crate) struct LegacyChannels {
#[serde(default)]
pub telegram: Option<LegacyTelegramChannel>,
#[serde(default)]
pub whatsapp: Option<LegacyWhatsappChannel>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct LegacyTelegramChannel {
#[serde(default, alias = "botToken")]
pub token: Option<String>,
#[serde(default)]
pub enabled: Option<bool>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct LegacyWhatsappChannel {
#[serde(default)]
pub token: Option<String>,
#[serde(default, alias = "phoneNumberId")]
pub phone_id: Option<String>,
#[serde(default)]
pub enabled: Option<bool>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct LegacyCronJob {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub schedule: Option<serde_json::Value>,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub payload: Option<serde_json::Value>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct LegacyJobsFile {
#[serde(default)]
pub jobs: Vec<LegacyCronJob>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct LegacySession {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub agent_id: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub messages: Option<Vec<LegacyMessage>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct LegacyMessage {
#[serde(default)]
pub role: Option<String>,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub timestamp: Option<String>,
}
#[derive(Debug, Deserialize)]
struct LegacyJSONLLine {
#[serde(default, rename = "type")]
line_type: Option<String>,
#[allow(dead_code)]
#[serde(default)]
id: Option<String>,
#[serde(default)]
timestamp: Option<String>,
#[serde(default)]
message: Option<LegacyJSONLMessage>,
}
#[derive(Debug, Deserialize)]
struct LegacyJSONLMessage {
#[serde(default)]
role: Option<String>,
#[serde(default)]
content: Option<serde_json::Value>,
#[serde(default)]
timestamp: Option<serde_json::Value>,
}
impl LegacyJSONLMessage {
fn into_message(self, line_ts: Option<&str>) -> Option<LegacyMessage> {
let role = self.role?;
let content = match self.content? {
serde_json::Value::String(s) => s,
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|v| v.get("text").and_then(|t| t.as_str()))
.collect::<Vec<_>>()
.join("\n"),
_ => return None,
};
if content.is_empty() {
return None;
}
let ts = self
.timestamp
.and_then(|v| match v {
serde_json::Value::String(s) => Some(s),
serde_json::Value::Number(n) => {
Some(chrono::DateTime::from_timestamp_millis(n.as_i64()?)?.to_rfc3339())
}
_ => None,
})
.or_else(|| line_ts.map(String::from));
Some(LegacyMessage {
role: Some(role),
content: Some(content),
timestamp: ts,
})
}
}
pub(crate) fn qt(s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
pub(crate) fn qt_ml(s: &str) -> String {
format!("\"\"\"\n{}\n\"\"\"", s)
}
fn uuid_v4() -> String {
uuid::Uuid::new_v4().to_string()
}
fn now_iso() -> String {
chrono::Utc::now().to_rfc3339()
}
fn err(area: MigrationArea, msg: String) -> AreaResult {
AreaResult {
area,
success: false,
items_processed: 0,
warnings: vec![],
error: Some(msg),
}
}
fn store_in_keystore(key: &str, value: &str, ks_path: &Path) -> Result<(), String> {
let ks = roboticus_core::keystore::Keystore::new(ks_path.to_path_buf());
ks.unlock_machine().map_err(|e| e.to_string())?;
ks.set(key, value).map_err(|e| e.to_string())
}
fn read_from_keystore(key: &str, ks_path: &Path) -> Option<String> {
let ks = roboticus_core::keystore::Keystore::new(ks_path.to_path_buf());
if ks.unlock_machine().is_err() {
return None;
}
ks.get(key)
}
fn resolve_channel_token_for_export(
channel_table: &toml::map::Map<String, toml::Value>,
obj: &mut serde_json::Map<String, serde_json::Value>,
warnings: &mut Vec<String>,
channel_name: &str,
ks_path: &Path,
) -> bool {
if let Some(token_ref) = channel_table.get("token_ref").and_then(|v| v.as_str())
&& let Some(ks_name) = token_ref.strip_prefix("keystore:")
{
if let Some(val) = read_from_keystore(ks_name, ks_path) {
obj.insert("token".into(), serde_json::Value::String(val));
return true;
}
warnings.push(format!(
"Keystore key \"{ks_name}\" not found; {channel_name} token omitted"
));
}
if let Some(env) = channel_table.get("token_env").and_then(|v| v.as_str()) {
if let Ok(tok) = std::env::var(env) {
obj.insert("token".into(), serde_json::Value::String(tok));
return true;
}
warnings.push(format!(
"Env var {env} not set; {channel_name} token omitted"
));
}
false
}
#[cfg(test)]
mod tests;