microclaw_engine/
messages.rs1use crate::config::UserMessageLanguage;
11
12pub fn approval_prompt(
16 lang: UserMessageLanguage,
17 tool_name: &str,
18 preview: Option<&str>,
19 advisory: Option<&str>,
20) -> String {
21 let preview_block = preview
22 .map(|p| format!("\n\n```\n{p}\n```"))
23 .unwrap_or_default();
24 let advisory_en = advisory
25 .map(|a| format!("\nRisk reviewer (advisory only): {a}"))
26 .unwrap_or_default();
27 let advisory_zh = advisory
28 .map(|a| format!("\n风险复核(仅供参考):{a}"))
29 .unwrap_or_default();
30 let en = format!(
31 "High-risk tool '{tool_name}' is waiting for your confirmation.{preview_block}{advisory_en}\n\n\
32 1. Approve once (reply \"1\" or \"approve\" / \"批准\")\n\
33 2. Always allow '{tool_name}' in this chat (reply \"2\" or \"always\")\n\
34 3. Deny (reply \"3\", \"deny\", or any other instruction)"
35 );
36 let zh = format!(
37 "高风险工具 '{tool_name}' 正在等待你的确认。{preview_block}{advisory_zh}\n\n\
38 1. 批准本次(回复 \"1\" 或 \"批准\")\n\
39 2. 本聊天始终允许 '{tool_name}'(回复 \"2\" 或 \"总是\")\n\
40 3. 拒绝(回复 \"3\"、\"拒绝\",或发送其他指令)"
41 );
42 match lang {
43 UserMessageLanguage::En => en,
44 UserMessageLanguage::Zh => zh,
45 UserMessageLanguage::Bilingual => format!("{en}\n\n{zh}"),
46 }
47}
48
49pub fn approval_button_labels(lang: UserMessageLanguage) -> [String; 3] {
52 match lang {
53 UserMessageLanguage::Zh => ["✅ 批准本次".into(), "🔁 始终允许".into(), "❌ 拒绝".into()],
54 _ => [
55 "✅ Approve once".into(),
56 "🔁 Always allow".into(),
57 "❌ Deny".into(),
58 ],
59 }
60}
61
62pub fn approval_buttons_hint(lang: UserMessageLanguage) -> &'static str {
64 match lang {
65 UserMessageLanguage::Zh => "请选择:",
66 UserMessageLanguage::Bilingual => "Choose an option / 请选择:",
67 UserMessageLanguage::En => "Choose an option:",
68 }
69}
70
71pub fn approval_choice_ack(lang: UserMessageLanguage, label: &str) -> String {
74 match lang {
75 UserMessageLanguage::Zh => format!("已选择:{label}"),
76 _ => format!("Selected: {label}"),
77 }
78}
79
80pub fn budget_refusal_body(lang: UserMessageLanguage, used: i64, budget: i64) -> String {
82 let en = format!(
83 " for this chat ({used} of {budget} tokens in the last 24h). I'll be available again \
84 once usage rolls out of the window. An operator can raise \
85 `token_budget.daily_per_chat` in the config."
86 );
87 let zh = format!(
88 "本聊天已用 {used} / {budget} tokens(近 24 小时)。用量滚出窗口后我会恢复可用;\
89 管理员可在配置中调高 `token_budget.daily_per_chat`。"
90 );
91 match lang {
92 UserMessageLanguage::En => en,
93 UserMessageLanguage::Zh => format!(" — {zh}"),
94 UserMessageLanguage::Bilingual => format!("{en}\n{zh}"),
95 }
96}
97
98pub fn interruption_notice(
101 lang: UserMessageLanguage,
102 progress: Option<&str>,
103 tool_summary: Option<&str>,
104) -> String {
105 let tool_summary = tool_summary.filter(|value| !value.trim().is_empty());
106 let progress = progress.filter(|value| !value.trim().is_empty());
107
108 let en = {
109 let mut notice = "⚠️ I restarted while a tool operation was in progress. I did not \
110 replay it because the external side effect may already have happened."
111 .to_string();
112 if let Some(summary) = tool_summary {
113 notice.push_str("\n\nOperation at the uncertain boundary: ");
114 notice.push_str(summary);
115 notice.push('.');
116 }
117 if let Some(progress) = progress {
118 notice.push_str("\nLast durable progress: ");
119 notice.push_str(progress);
120 notice.push('.');
121 }
122 notice.push_str(
123 "\n\nPlease verify the external state, then tell me to continue. This run was \
124 stopped deliberately to avoid duplicating a write, message, command, or other \
125 side effect.",
126 );
127 notice
128 };
129 let zh = {
130 let mut notice =
131 "⚠️ 我在一次工具操作进行中重启了。因为外部副作用可能已经发生,我没有重放这次操作。"
132 .to_string();
133 if let Some(summary) = tool_summary {
134 notice.push_str("\n\n不确定边界上的操作:");
135 notice.push_str(summary);
136 notice.push('。');
137 }
138 if let Some(progress) = progress {
139 notice.push_str("\n最后一次持久进度:");
140 notice.push_str(progress);
141 notice.push('。');
142 }
143 notice.push_str(
144 "\n\n请先核实外部状态,然后告诉我继续。这次运行是有意停下的,以避免重复写入、发消息、执行命令等副作用。",
145 );
146 notice
147 };
148 match lang {
149 UserMessageLanguage::En => en,
150 UserMessageLanguage::Zh => zh,
151 UserMessageLanguage::Bilingual => format!("{en}\n\n{zh}"),
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn approval_prompt_renders_options_in_each_language() {
161 let en = approval_prompt(UserMessageLanguage::En, "bash", Some("rm -rf /tmp/x"), None);
162 assert!(en.contains("1. Approve once"));
163 assert!(en.contains("rm -rf /tmp/x"));
164 assert!(en.contains("批准")); let zh = approval_prompt(UserMessageLanguage::Zh, "bash", None, Some("looks safe"));
167 assert!(zh.contains("批准本次"));
168 assert!(zh.contains("风险复核"));
169
170 let both = approval_prompt(UserMessageLanguage::Bilingual, "bash", None, None);
171 assert!(both.contains("Approve once") && both.contains("批准本次"));
172 }
173
174 #[test]
175 fn budget_refusal_en_matches_historical_wording() {
176 let body = budget_refusal_body(UserMessageLanguage::En, 10, 5);
177 assert!(body.starts_with(" for this chat (10 of 5 tokens"));
178 }
179}