refactor: update Recipe to use steps, add apply_config_patch command
Replace patchTemplate/impactCategory/impactSummary with steps-based recipe format. Remove preview_apply/apply_recipe commands, add apply_config_patch for inline merge patches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,71 +1,42 @@
|
||||
{
|
||||
"recipes": [
|
||||
{
|
||||
"id": "dedicated-channel-agent",
|
||||
"name": "Create dedicated Agent for Channel",
|
||||
"description": "Create an independent agent, set its identity, bind it to a Discord channel, and configure persona",
|
||||
"version": "1.0.0",
|
||||
"tags": ["discord", "agent", "persona"],
|
||||
"difficulty": "easy",
|
||||
"params": [
|
||||
{ "id": "agent_id", "label": "Agent ID", "type": "string", "required": true, "placeholder": "e.g. my-bot" },
|
||||
{ "id": "guild_id", "label": "Guild", "type": "discord_guild", "required": true },
|
||||
{ "id": "channel_id", "label": "Channel", "type": "discord_channel", "required": true },
|
||||
{ "id": "name", "label": "Display Name", "type": "string", "required": true, "placeholder": "e.g. MyBot" },
|
||||
{ "id": "emoji", "label": "Emoji", "type": "string", "required": false, "placeholder": "e.g. \ud83e\udd16" },
|
||||
{ "id": "persona", "label": "Persona", "type": "textarea", "required": true, "placeholder": "You are..." }
|
||||
],
|
||||
"steps": [
|
||||
{ "action": "create_agent", "label": "Create independent agent", "args": { "agentId": "{{agent_id}}", "independent": true } },
|
||||
{ "action": "setup_identity", "label": "Set agent identity", "args": { "agentId": "{{agent_id}}", "name": "{{name}}", "emoji": "{{emoji}}" } },
|
||||
{ "action": "bind_channel", "label": "Bind channel to agent", "args": { "channelType": "discord", "peerId": "{{channel_id}}", "agentId": "{{agent_id}}" } },
|
||||
{ "action": "config_patch", "label": "Set channel persona", "args": { "patchTemplate": "{\"channels\":{\"discord\":{\"guilds\":{\"{{guild_id}}\":{\"channels\":{\"{{channel_id}}\":{\"systemPrompt\":\"{{persona}}\"}}}}}}}" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "discord-channel-persona",
|
||||
"name": "Discord channel persona",
|
||||
"description": "Inject different system prompt for one Discord channel",
|
||||
"name": "Channel Persona",
|
||||
"description": "Set a custom persona for a Discord channel",
|
||||
"version": "1.0.0",
|
||||
"tags": ["discord", "persona", "beginner"],
|
||||
"difficulty": "easy",
|
||||
"params": [
|
||||
{
|
||||
"id": "guild_id",
|
||||
"label": "Guild",
|
||||
"type": "discord_guild",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "channel_id",
|
||||
"label": "Channel",
|
||||
"type": "discord_channel",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "persona",
|
||||
"label": "Persona",
|
||||
"type": "textarea",
|
||||
"required": true,
|
||||
"minLength": 1,
|
||||
"placeholder": "You are..."
|
||||
}
|
||||
{ "id": "guild_id", "label": "Guild", "type": "discord_guild", "required": true },
|
||||
{ "id": "channel_id", "label": "Channel", "type": "discord_channel", "required": true },
|
||||
{ "id": "persona", "label": "Persona", "type": "textarea", "required": true, "placeholder": "You are..." }
|
||||
],
|
||||
"patchTemplate": "\n{\n \"channels\": {\n \"discord\": {\n \"guilds\": {\n \"{{guild_id}}\": {\n \"channels\": {\n \"{{channel_id}}\": {\n \"systemPrompt\": \"{{persona}}\"\n }\n }\n }\n }\n }\n }\n}",
|
||||
"impactCategory": "low",
|
||||
"impactSummary": "Add/modify channel persona"
|
||||
},
|
||||
{
|
||||
"id": "setup-agent",
|
||||
"name": "Setup agent identity",
|
||||
"description": "Set the display name and emoji for an agent",
|
||||
"version": "1.0.0",
|
||||
"tags": ["agent", "identity", "beginner"],
|
||||
"difficulty": "easy",
|
||||
"action": "setup_agent",
|
||||
"params": [
|
||||
{
|
||||
"id": "agent_id",
|
||||
"label": "Agent",
|
||||
"type": "agent",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "name",
|
||||
"label": "Display Name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"placeholder": "e.g. Owlia"
|
||||
},
|
||||
{
|
||||
"id": "emoji",
|
||||
"label": "Emoji",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"placeholder": "e.g. 🦉"
|
||||
}
|
||||
],
|
||||
"patchTemplate": "",
|
||||
"impactCategory": "low",
|
||||
"impactSummary": "Set agent display name and emoji"
|
||||
"steps": [
|
||||
{ "action": "config_patch", "label": "Set channel persona", "args": { "patchTemplate": "{\"channels\":{\"discord\":{\"guilds\":{\"{{guild_id}}\":{\"channels\":{\"{{channel_id}}\":{\"systemPrompt\":\"{{persona}}\"}}}}}}}" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,12 +11,10 @@ use crate::models::resolve_paths;
|
||||
use crate::recipe::{
|
||||
load_recipes_with_fallback,
|
||||
collect_change_paths,
|
||||
build_candidate_config,
|
||||
find_recipe_with_source,
|
||||
build_candidate_config_from_template,
|
||||
format_diff,
|
||||
ApplyResult,
|
||||
PreviewResult,
|
||||
validate,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -1195,55 +1193,10 @@ pub fn list_recipes(source: Option<String>) -> Result<Vec<crate::recipe::Recipe>
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn preview_apply(
|
||||
recipe_id: String,
|
||||
pub fn apply_config_patch(
|
||||
patch_template: String,
|
||||
params: Map<String, Value>,
|
||||
source: Option<String>,
|
||||
) -> Result<PreviewResult, String> {
|
||||
let recipe =
|
||||
find_recipe_with_source(&recipe_id, source).ok_or_else(|| "unknown recipe".to_string())?;
|
||||
let errors = validate(&recipe, ¶ms);
|
||||
if !errors.is_empty() {
|
||||
return Err(format!("validation failed: {}", errors.join(",")));
|
||||
}
|
||||
let paths = resolve_paths();
|
||||
ensure_dirs(&paths)?;
|
||||
let current = read_openclaw_config(&paths)?;
|
||||
let (candidate, changes) = build_candidate_config(¤t, &recipe, ¶ms)?;
|
||||
let before_text = serde_json::to_string_pretty(¤t).unwrap_or_else(|_| "{}".into());
|
||||
let after_text = serde_json::to_string_pretty(&candidate).unwrap_or_else(|_| "{}".into());
|
||||
Ok(PreviewResult {
|
||||
recipe_id: recipe_id.clone(),
|
||||
diff: format_diff(¤t, &candidate),
|
||||
config_before: before_text,
|
||||
config_after: after_text,
|
||||
changes,
|
||||
overwrites_existing: true,
|
||||
can_rollback: true,
|
||||
impact_level: recipe.impact_category,
|
||||
warnings: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn apply_recipe(
|
||||
recipe_id: String,
|
||||
params: Map<String, Value>,
|
||||
source: Option<String>,
|
||||
) -> Result<ApplyResult, String> {
|
||||
let recipe =
|
||||
find_recipe_with_source(&recipe_id, source).ok_or_else(|| "unknown recipe".to_string())?;
|
||||
let errors = validate(&recipe, ¶ms);
|
||||
if !errors.is_empty() {
|
||||
return Ok(ApplyResult {
|
||||
ok: false,
|
||||
snapshot_id: None,
|
||||
config_path: String::new(),
|
||||
backup_path: None,
|
||||
warnings: errors,
|
||||
errors: vec!["validation failed".into()],
|
||||
});
|
||||
}
|
||||
let paths = resolve_paths();
|
||||
ensure_dirs(&paths)?;
|
||||
let current = read_openclaw_config(&paths)?;
|
||||
@@ -1251,13 +1204,13 @@ pub fn apply_recipe(
|
||||
let snapshot = add_snapshot(
|
||||
&paths.history_dir,
|
||||
&paths.metadata_path,
|
||||
Some(recipe_id.clone()),
|
||||
Some("config-patch".into()),
|
||||
"apply",
|
||||
true,
|
||||
¤t_text,
|
||||
None,
|
||||
)?;
|
||||
let (candidate, _changes) = build_candidate_config(¤t, &recipe, ¶ms)?;
|
||||
let (candidate, _changes) = build_candidate_config_from_template(¤t, &patch_template, ¶ms)?;
|
||||
write_json(&paths.config_path, &candidate)?;
|
||||
Ok(ApplyResult {
|
||||
ok: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::commands::{
|
||||
apply_recipe, fix_issues, get_system_status, get_status_light, list_history, list_recipes, preview_apply,
|
||||
apply_config_patch, fix_issues, get_system_status, get_status_light, list_history, list_recipes,
|
||||
list_model_profiles, upsert_model_profile, delete_model_profile,
|
||||
list_model_catalog, get_cached_model_catalog, refresh_model_catalog, resolve_provider_auth,
|
||||
check_openclaw_update, extract_model_profiles_from_config,
|
||||
@@ -51,8 +51,7 @@ pub fn run() {
|
||||
clear_agent_sessions,
|
||||
check_openclaw_update,
|
||||
extract_model_profiles_from_config,
|
||||
preview_apply,
|
||||
apply_recipe,
|
||||
apply_config_patch,
|
||||
list_history,
|
||||
preview_rollback,
|
||||
rollback,
|
||||
|
||||
@@ -30,6 +30,14 @@ pub struct RecipeParam {
|
||||
pub placeholder: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RecipeStep {
|
||||
pub action: String,
|
||||
pub label: String,
|
||||
pub args: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Recipe {
|
||||
@@ -40,9 +48,7 @@ pub struct Recipe {
|
||||
pub tags: Vec<String>,
|
||||
pub difficulty: String,
|
||||
pub params: Vec<RecipeParam>,
|
||||
pub patch_template: String,
|
||||
pub impact_category: String,
|
||||
pub impact_summary: String,
|
||||
pub steps: Vec<RecipeStep>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -218,22 +224,16 @@ fn render_patch_template(template: &str, params: &Map<String, Value>) -> String
|
||||
text
|
||||
}
|
||||
|
||||
pub fn build_candidate_config(
|
||||
pub fn build_candidate_config_from_template(
|
||||
current: &Value,
|
||||
recipe: &Recipe,
|
||||
template: &str,
|
||||
params: &Map<String, Value>,
|
||||
) -> Result<(Value, Vec<ChangeItem>), String> {
|
||||
let rendered = render_patch_template(&recipe.patch_template, params);
|
||||
let rendered = render_patch_template(template, params);
|
||||
let patch: Value = json5::from_str(&rendered).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut merged = current.clone();
|
||||
let mut changes = Vec::new();
|
||||
apply_merge_patch(&mut merged, &patch, "", &mut changes);
|
||||
if recipe.impact_category == "high" {
|
||||
for change in &mut changes {
|
||||
change.risk = "high".into();
|
||||
}
|
||||
}
|
||||
Ok((merged, changes))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user