@@ -12450,6 +12450,127 @@ async fn fetch_remote_image_data_url(url: String) -> Result<String, String> {
1245012450 Ok(format!("data:{};base64,{}", mime, STANDARD.encode(&bytes)))
1245112451}
1245212452
12453+ const FEEDBACK_ENDPOINT: &str = "https://lovstudio.ai/api/lovcode/feedback";
12454+ const FEEDBACK_RECIPIENT_EMAIL: &str = "mark@lovstudio.ai";
12455+ const MAX_FEEDBACK_MESSAGE_CHARS: usize = 5000;
12456+ const MAX_FEEDBACK_FIELD_CHARS: usize = 300;
12457+
12458+ #[derive(Debug, Deserialize)]
12459+ #[serde(rename_all = "camelCase")]
12460+ struct FeedbackSubmission {
12461+ category: String,
12462+ message: String,
12463+ contact: Option<String>,
12464+ path: Option<String>,
12465+ app_version: Option<String>,
12466+ user_agent: Option<String>,
12467+ locale: Option<String>,
12468+ timezone: Option<String>,
12469+ metadata: Option<Value>,
12470+ }
12471+
12472+ #[derive(Debug, Serialize)]
12473+ #[serde(rename_all = "camelCase")]
12474+ struct FeedbackSubmitResult {
12475+ feedback_id: String,
12476+ endpoint: String,
12477+ recipient_email: String,
12478+ }
12479+
12480+ fn normalize_feedback_field(value: Option<String>) -> Option<String> {
12481+ value
12482+ .map(|v| {
12483+ v.trim()
12484+ .chars()
12485+ .take(MAX_FEEDBACK_FIELD_CHARS)
12486+ .collect::<String>()
12487+ })
12488+ .filter(|v| !v.is_empty())
12489+ }
12490+
12491+ #[tauri::command]
12492+ async fn submit_feedback(payload: FeedbackSubmission) -> Result<FeedbackSubmitResult, String> {
12493+ use reqwest::header::{ACCEPT, CONTENT_TYPE, USER_AGENT};
12494+
12495+ let message = payload.message.trim().to_string();
12496+ let message_len = message.chars().count();
12497+ if message_len < 4 {
12498+ return Err("反馈内容太短,请至少输入 4 个字符。".to_string());
12499+ }
12500+ if message_len > MAX_FEEDBACK_MESSAGE_CHARS {
12501+ return Err(format!(
12502+ "反馈内容过长,请控制在 {} 个字符以内。",
12503+ MAX_FEEDBACK_MESSAGE_CHARS
12504+ ));
12505+ }
12506+
12507+ let category = match payload.category.trim() {
12508+ "bug" | "idea" | "contact" => payload.category.trim().to_string(),
12509+ _ => "idea".to_string(),
12510+ };
12511+ let feedback_id = uuid::Uuid::new_v4().to_string();
12512+
12513+ let body = serde_json::json!({
12514+ "id": feedback_id,
12515+ "source": "lovcode-desktop",
12516+ "category": category,
12517+ "message": message,
12518+ "contact": normalize_feedback_field(payload.contact),
12519+ "path": normalize_feedback_field(payload.path),
12520+ "appVersion": normalize_feedback_field(payload.app_version),
12521+ "userAgent": normalize_feedback_field(payload.user_agent),
12522+ "locale": normalize_feedback_field(payload.locale),
12523+ "timezone": normalize_feedback_field(payload.timezone),
12524+ "recipientEmail": FEEDBACK_RECIPIENT_EMAIL,
12525+ "metadata": payload.metadata.unwrap_or(Value::Null),
12526+ });
12527+
12528+ let client = reqwest::Client::builder()
12529+ .timeout(Duration::from_secs(15))
12530+ .redirect(reqwest::redirect::Policy::limited(3))
12531+ .build()
12532+ .map_err(|e| format!("创建反馈请求失败: {}", e))?;
12533+
12534+ let response = client
12535+ .post(FEEDBACK_ENDPOINT)
12536+ .header(USER_AGENT, "Lovcode Feedback")
12537+ .header(ACCEPT, "application/json")
12538+ .header(CONTENT_TYPE, "application/json")
12539+ .json(&body)
12540+ .send()
12541+ .await
12542+ .map_err(|e| format!("反馈提交网络失败: {}", e))?;
12543+
12544+ let status = response.status();
12545+ let response_text = response.text().await.unwrap_or_default();
12546+ if !status.is_success() {
12547+ let detail: String = response_text.chars().take(300).collect();
12548+ let suffix = if detail.is_empty() {
12549+ String::new()
12550+ } else {
12551+ format!(": {}", detail)
12552+ };
12553+ return Err(format!("反馈接口返回 HTTP {}{}", status.as_u16(), suffix));
12554+ }
12555+
12556+ let returned_id = serde_json::from_str::<Value>(&response_text)
12557+ .ok()
12558+ .and_then(|value| {
12559+ value
12560+ .get("feedbackId")
12561+ .or_else(|| value.get("id"))
12562+ .and_then(|id| id.as_str())
12563+ .map(str::to_string)
12564+ })
12565+ .unwrap_or(feedback_id);
12566+
12567+ Ok(FeedbackSubmitResult {
12568+ feedback_id: returned_id,
12569+ endpoint: FEEDBACK_ENDPOINT.to_string(),
12570+ recipient_email: FEEDBACK_RECIPIENT_EMAIL.to_string(),
12571+ })
12572+ }
12573+
1245312574/// Run a shell command in specified directory using login shell (async, non-blocking)
1245412575#[tauri::command]
1245512576async fn exec_shell_command(command: String, cwd: String) -> Result<String, String> {
@@ -14406,6 +14527,7 @@ pub fn run() {
1440614527 delete_project_logo,
1440714528 read_file_base64,
1440814529 fetch_remote_image_data_url,
14530+ submit_feedback,
1440914531 exec_shell_command,
1441014532 hook_get_monitored,
1441114533 hook_notify_complete,
0 commit comments