matrixcode_core/tools/workflow/
content.rs1use anyhow::Result;
6use async_trait::async_trait;
7use serde_json::{Value, json};
8
9use crate::tools::{Tool, ToolDefinition};
10use crate::providers::Provider;
11use std::sync::Arc;
12
13pub struct ContentGenerationTool {
15 provider: Arc<dyn Provider>,
16}
17
18impl ContentGenerationTool {
19 pub fn new(provider: Arc<dyn Provider>) -> Self {
20 Self { provider }
21 }
22
23 fn extract_topic<'a>(&self, params: &'a Value) -> Result<&'a str> {
25 params.get("topic")
26 .and_then(|v| v.as_str())
27 .ok_or_else(|| anyhow::anyhow!("缺少 topic 参数"))
28 }
29
30 fn build_prompt(&self, params: &Value) -> String {
32 let topic = params.get("topic")
33 .and_then(|v| v.as_str())
34 .unwrap_or("");
35 let style = params.get("style")
36 .and_then(|v| v.as_str())
37 .unwrap_or("informative");
38
39 let mut prompt = format!("主题: {}\n\n风格: {}\n\n", topic, style);
40
41 if let Some(research) = params.get("research_data").and_then(|v| v.as_str()) {
42 prompt.push_str(&format!("参考资料:\n{}\n\n", research));
43 }
44
45 self.append_image_instructions(&mut prompt, params);
46 prompt.push_str("\n请生成一篇完整的图文文章,**必须包含图片**。");
47
48 prompt
49 }
50
51 fn append_image_instructions(&self, prompt: &mut String, params: &Value) {
53 if let Some(images) = params.get("image_urls")
54 && let Some(arr) = images.as_array()
55 && !arr.is_empty() {
56 prompt.push_str("\n**重要:请在文章中插入以下图片**(使用 Markdown 图片格式 ``):\n");
57 for (idx, img) in arr.iter().enumerate() {
58 let (url, desc) = self.extract_image_info(img, idx);
59 prompt.push_str(&format!("{}. \n", idx + 1, desc, url));
60 }
61 prompt.push_str("\n请将图片插入到文章的合适位置,使文章更加生动。\n");
62 }
63 }
64
65 fn extract_image_info(&self, img: &Value, idx: usize) -> (String, String) {
67 if let Some(url_str) = img.as_str() {
68 (url_str.to_string(), format!("图片{}", idx + 1))
69 } else if let Some(obj) = img.as_object() {
70 let url = obj.get("url")
71 .and_then(|u| u.as_str())
72 .unwrap_or("")
73 .to_string();
74 let desc = obj.get("description")
75 .and_then(|d| d.as_str())
76 .unwrap_or("配图")
77 .to_string();
78 (url, desc)
79 } else {
80 ("".to_string(), format!("图片{}", idx + 1))
81 }
82 }
83
84 fn build_image_gallery(&self, params: &Value) -> String {
86 if let Some(images) = params.get("image_urls")
87 && let Some(arr) = images.as_array()
88 && !arr.is_empty() {
89 let mut gallery = String::from("\n## 📷 配图\n\n");
90 for (idx, img) in arr.iter().enumerate().take(5) {
91 let (url, desc) = self.extract_image_info(img, idx);
92 if !url.is_empty() {
93 gallery.push_str(&format!("\n\n", desc, url));
94 }
95 }
96 return gallery;
97 }
98 String::new()
99 }
100
101 async fn call_ai(&self, prompt: String) -> Result<String> {
103 log::info!("ContentGenerationTool: starting for model {}", self.provider.model_name());
104
105 let request = crate::providers::ChatRequest {
106 messages: vec![crate::providers::Message {
107 role: crate::providers::Role::User,
108 content: crate::providers::MessageContent::Text(prompt),
109 }],
110 system: Some("你是一个专业的内容创作者。".to_string()),
111 tools: vec![],
112 think: false,
113 max_tokens: 4096,
114 server_tools: vec![],
115 enable_caching: false,
116 };
117
118 match self.provider.chat(request).await {
119 Ok(response) => {
120 log::info!("ContentGenerationTool: received successful response");
121 Ok(self.extract_text_content(response))
122 }
123 Err(e) => {
124 log::error!("ContentGenerationTool: AI call failed: {:?}", e);
125 Err(e)
126 }
127 }
128 }
129
130 fn extract_text_content(&self, response: crate::providers::ChatResponse) -> String {
132 response.content.iter()
133 .filter_map(|block| {
134 match block {
135 crate::providers::ContentBlock::Text { text } => Some(text.clone()),
136 _ => None,
137 }
138 })
139 .collect::<Vec<_>>()
140 .join("\n")
141 }
142
143 fn format_response(&self, content: String, topic: &str, style: &str, params: &Value) -> String {
145 let gallery = self.build_image_gallery(params);
146 let final_content = if gallery.is_empty() {
147 content
148 } else {
149 format!("{}\n\n{}", gallery, content)
150 };
151
152 json!({
153 "content": final_content,
154 "topic": topic,
155 "style": style,
156 "word_count": final_content.chars().count()
157 }).to_string()
158 }
159
160 fn format_error_response(&self, topic: &str, style: &str, error: anyhow::Error) -> String {
162 json!({
163 "content": format!("主题《{}》的内容生成失败: {}", topic, error),
164 "topic": topic,
165 "style": style,
166 "error": true
167 }).to_string()
168 }
169}
170
171#[async_trait]
172impl Tool for ContentGenerationTool {
173 fn definition(&self) -> ToolDefinition {
174 ToolDefinition {
175 name: "content_generation".to_string(),
176 description: "使用 AI 生成内容(文章等)。需要 Provider 支持。".to_string(),
177 parameters: json!({
178 "type": "object",
179 "properties": {
180 "topic": {
181 "type": "string",
182 "description": "主题"
183 },
184 "research_data": {
185 "type": "string",
186 "description": "研究资料(可选)"
187 },
188 "image_urls": {
189 "type": "array",
190 "description": "图片 URL 列表(可选)",
191 "items": {"type": "string"}
192 },
193 "style": {
194 "type": "string",
195 "description": "写作风格(informative/casual/professional)",
196 "default": "informative"
197 }
198 },
199 "required": ["topic"]
200 }),
201 ..Default::default()
202 }
203 }
204
205 async fn execute(&self, params: Value) -> Result<String> {
206 let topic = self.extract_topic(¶ms)?;
207 let style = params.get("style")
208 .and_then(|v| v.as_str())
209 .unwrap_or("informative");
210
211 let prompt = self.build_prompt(¶ms);
212
213 match self.call_ai(prompt).await {
214 Ok(content) => Ok(self.format_response(content, topic, style, ¶ms)),
215 Err(e) => Ok(self.format_error_response(topic, style, e)),
216 }
217 }
218}