@pheem49/mint 1.8.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +297 -107
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/actions.rs +233 -0
- package/crates/mint-cli/src/agent.rs +382 -127
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +391 -2506
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +152 -109
- package/crates/mint-cli/src/setup.rs +143 -143
- package/crates/mint-cli/src/updater.rs +48 -6
- package/crates/mint-core/Cargo.toml +1 -0
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +431 -65
- package/crates/mint-core/src/browser.rs +630 -102
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +174 -11
- package/crates/mint-core/src/code_tools.rs +47 -31
- package/crates/mint-core/src/config.rs +302 -17
- package/crates/mint-core/src/image_gen.rs +299 -38
- package/crates/mint-core/src/lib.rs +10 -7
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +237 -96
- package/crates/mint-core/src/pictures.rs +67 -5
- package/crates/mint-core/src/semantic.rs +1 -1
- package/crates/mint-core/src/shell.rs +1 -1
- package/crates/mint-core/src/skills.rs +128 -25
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
- package/shared-modules/linux-audio/fluidsynth2.json +8 -2
- package/shared-modules/linux-audio/lilv.json +26 -19
- package/shared-modules/linux-audio/lv2.json +0 -1
- package/shared-modules/luajit/luajit.json +2 -2
- package/skills-lock.json +41 -0
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
- package/src/renderer/shared/constants/slashCommands.ts +27 -0
- package/src/renderer/shared/platform.ts +3 -0
- package/src/renderer/shared/types.ts +4 -0
- package/src/renderer/shared/utils/approval.tsx +86 -7
- package/src/renderer/shared/utils/fileIcons.ts +135 -0
- package/src/renderer/shared/utils/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +486 -69
- package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src/components/MintDashboard.tsx +195 -7
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
- package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src/components/SettingsWindow.tsx +106 -3
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- package/src/renderer/src/css/image-studio.css +106 -0
- package/src/renderer/src/css/settings.css +158 -0
- package/src/renderer/src/css/styles.css +368 -1
- package/src/renderer/src/css/veo-studio.css +626 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +144 -17
- package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
- package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src-web/css/image-studio.css +106 -0
- package/src/renderer/src-web/css/settings.css +158 -0
- package/src/renderer/src-web/css/styles.css +369 -1
- package/src/renderer/src-web/css/veo-studio.css +650 -0
- package/src/renderer/src-web/index.css +1 -0
- package/src/renderer/src-web/tauri.ts +135 -16
- package/src-tauri/src/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +155 -25
- package/src-tauri/src/webhooks.rs +45 -31
- package/src-tauri/src/workflows.rs +1 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/temp_gridgeist/AGENTS.md +8 -0
- package/temp_gridgeist/CHANGELOG.md +130 -0
- package/temp_gridgeist/LICENSE +21 -0
- package/temp_gridgeist/README.md +323 -0
- package/temp_gridgeist/README.th.md +325 -0
- package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
- package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
- package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
- package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
- package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
|
@@ -4,6 +4,7 @@ use std::{
|
|
|
4
4
|
process::{Command, Stdio},
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
|
7
8
|
use serde::Deserialize;
|
|
8
9
|
use serde_json::{Value, json};
|
|
9
10
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
@@ -11,23 +12,48 @@ use tokio::net::TcpListener;
|
|
|
11
12
|
|
|
12
13
|
use crate::{
|
|
13
14
|
AgentProgress, ApprovalOutcome, ChatRequest, ChatResponse, DEFAULT_CONVERSATION_ID,
|
|
14
|
-
ImageGenRequest, MemoryStore, MintConfig, config_path, create_folder,
|
|
15
|
-
generate_images, list_saved_pictures, load_config,
|
|
16
|
-
orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback,
|
|
17
|
-
save_config, weather,
|
|
15
|
+
ImageGenRequest, MemoryStore, MintConfig, VideoGenRequest, config_path, create_folder,
|
|
16
|
+
find_paths, generate_images, generate_video, list_saved_pictures, load_config,
|
|
17
|
+
orchestrate_agent_loop, orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback,
|
|
18
|
+
save_chat_images, save_config, weather,
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const MAX_API_REQUEST_BYTES: usize = 32 * 1024 * 1024;
|
|
21
22
|
|
|
23
|
+
pub(crate) fn log_api_req(method: &str, route: &str, status: &str, info: Option<&str>) {
|
|
24
|
+
let timestamp = chrono::Local::now().format("%H:%M:%S");
|
|
25
|
+
let status_str = if status.starts_with('2') {
|
|
26
|
+
format!("\x1b[32m{}\x1b[0m", status)
|
|
27
|
+
} else if status.starts_with('4') || status.starts_with('5') {
|
|
28
|
+
format!("\x1b[1;31m{}\x1b[0m", status)
|
|
29
|
+
} else {
|
|
30
|
+
format!("\x1b[33m{}\x1b[0m", status)
|
|
31
|
+
};
|
|
32
|
+
if let Some(detail) = info {
|
|
33
|
+
println!(
|
|
34
|
+
"\x1b[90m[{}]\x1b[0m \x1b[1;36m[API]\x1b[0m \x1b[1m{}\x1b[0m {} -> {} \x1b[90m({})\x1b[0m",
|
|
35
|
+
timestamp, method, route, status_str, detail
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
println!(
|
|
39
|
+
"\x1b[90m[{}]\x1b[0m \x1b[1;36m[API]\x1b[0m \x1b[1m{}\x1b[0m {} -> {}",
|
|
40
|
+
timestamp, method, route, status_str
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub(crate) fn log_api_err(context: &str, error: &dyn std::fmt::Display) {
|
|
46
|
+
let timestamp = chrono::Local::now().format("%H:%M:%S");
|
|
47
|
+
eprintln!(
|
|
48
|
+
"\x1b[90m[{}]\x1b[0m \x1b[1;31m[ERROR]\x1b[0m \x1b[1;33m{}\x1b[0m: {}",
|
|
49
|
+
timestamp, context, error
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
23
54
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
|
24
55
|
let listener = TcpListener::bind(addr).await?;
|
|
25
|
-
|
|
26
|
-
println!(
|
|
27
|
-
"\x1b[32m Mint Local API Server running at http://{}\x1b[0m",
|
|
28
|
-
addr
|
|
29
|
-
);
|
|
30
|
-
println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
|
|
56
|
+
// API server banner removed to prevent duplicate output
|
|
31
57
|
|
|
32
58
|
// Start background messaging bridges (Telegram, Discord, Slack)
|
|
33
59
|
crate::start_channels();
|
|
@@ -125,6 +151,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
125
151
|
|
|
126
152
|
let (route, query) = path.split_once('?').unwrap_or((path, ""));
|
|
127
153
|
|
|
154
|
+
if route.starts_with("/api/")
|
|
155
|
+
&& !route.starts_with("/api/pictures/")
|
|
156
|
+
&& !route.starts_with("/api/thumbnails/")
|
|
157
|
+
&& route != "/api/chat"
|
|
158
|
+
&& route != "/api/chat-stream"
|
|
159
|
+
&& route != "/api/image-generate"
|
|
160
|
+
&& route != "/api/video-generate"
|
|
161
|
+
&& route != "/api/action"
|
|
162
|
+
&& route != "/api/config"
|
|
163
|
+
{
|
|
164
|
+
log_api_req(method, route, "200 OK", None);
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
match (method, route) {
|
|
129
168
|
("GET", "/api/status") => {
|
|
130
169
|
let config = load_config().unwrap_or_default();
|
|
@@ -213,16 +252,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
213
252
|
new_title: String,
|
|
214
253
|
}
|
|
215
254
|
|
|
216
|
-
if let Ok(req) = serde_json::from_str::<RenameRequest>(body)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
255
|
+
if let Ok(req) = serde_json::from_str::<RenameRequest>(body)
|
|
256
|
+
&& let Ok(memory) = MemoryStore::open_default()
|
|
257
|
+
{
|
|
258
|
+
let updated = memory
|
|
259
|
+
.rename_chat_session(&req.chat_id, &req.new_title)
|
|
260
|
+
.unwrap_or(0);
|
|
261
|
+
let response = serde_json::json!({ "status": "ok", "updated": updated });
|
|
262
|
+
send_json_response(socket, "200 OK", &response.to_string()).await;
|
|
263
|
+
return;
|
|
226
264
|
}
|
|
227
265
|
send_json_response(
|
|
228
266
|
socket,
|
|
@@ -231,6 +269,48 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
231
269
|
)
|
|
232
270
|
.await;
|
|
233
271
|
}
|
|
272
|
+
("GET", "/api/learned-skills") => {
|
|
273
|
+
let mut skills = match MemoryStore::open_default() {
|
|
274
|
+
Ok(m) => m.learned_skills(100).unwrap_or_default(),
|
|
275
|
+
Err(_) => Vec::new(),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if let Some(home) = dirs::home_dir() {
|
|
279
|
+
let global_agents_path =
|
|
280
|
+
home.join(".gemini").join("config").join("AGENTS.md");
|
|
281
|
+
crate::skills::load_agent_rules_file(&global_agents_path, &mut skills);
|
|
282
|
+
|
|
283
|
+
let global_skills_path =
|
|
284
|
+
home.join(".config").join("mint").join("mint-skills");
|
|
285
|
+
crate::skills::load_skills_from_dir(&global_skills_path, &mut skills);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if let Ok(current_dir) = std::env::current_dir() {
|
|
289
|
+
let workspace_agents_path1 = current_dir.join(".agents").join("AGENTS.md");
|
|
290
|
+
crate::skills::load_agent_rules_file(&workspace_agents_path1, &mut skills);
|
|
291
|
+
let workspace_agents_path2 = current_dir.join("AGENTS.md");
|
|
292
|
+
crate::skills::load_agent_rules_file(&workspace_agents_path2, &mut skills);
|
|
293
|
+
|
|
294
|
+
let workspace_skills_path1 = current_dir.join(".agents").join("skills");
|
|
295
|
+
crate::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
|
|
296
|
+
let workspace_skills_path2 = current_dir.join("skills");
|
|
297
|
+
crate::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let mut unique_skills = std::collections::BTreeMap::new();
|
|
301
|
+
for s in skills {
|
|
302
|
+
unique_skills.insert(s.name.clone(), s);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let list: Vec<_> = unique_skills.into_values().collect();
|
|
306
|
+
send_json_response(
|
|
307
|
+
socket,
|
|
308
|
+
"200 OK",
|
|
309
|
+
&serde_json::to_string(&list).unwrap_or_default(),
|
|
310
|
+
)
|
|
311
|
+
.await;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
234
314
|
("GET", "/api/profile") => {
|
|
235
315
|
let key = query_param(query, "key").unwrap_or_default();
|
|
236
316
|
if let Ok(memory) = MemoryStore::open_default() {
|
|
@@ -252,10 +332,39 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
252
332
|
key: String,
|
|
253
333
|
value: String,
|
|
254
334
|
}
|
|
255
|
-
if let Ok(req) = serde_json::from_str::<ProfileRequest>(body)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
335
|
+
if let Ok(req) = serde_json::from_str::<ProfileRequest>(body)
|
|
336
|
+
&& let Ok(memory) = MemoryStore::open_default()
|
|
337
|
+
{
|
|
338
|
+
let _ = memory.set_profile(&req.key, &req.value);
|
|
339
|
+
send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
send_json_response(
|
|
343
|
+
socket,
|
|
344
|
+
"500 Internal Server Error",
|
|
345
|
+
"{\"status\":\"error\"}",
|
|
346
|
+
)
|
|
347
|
+
.await;
|
|
348
|
+
}
|
|
349
|
+
("POST", "/api/active-model") => {
|
|
350
|
+
#[derive(Deserialize)]
|
|
351
|
+
struct ActiveModelReq {
|
|
352
|
+
provider: String,
|
|
353
|
+
model: Option<String>,
|
|
354
|
+
}
|
|
355
|
+
if let Ok(req) = serde_json::from_str::<ActiveModelReq>(body)
|
|
356
|
+
&& let Ok(mut config) = load_config()
|
|
357
|
+
{
|
|
358
|
+
if let Ok(display_name) =
|
|
359
|
+
config.set_active_model(&req.provider, req.model.as_deref())
|
|
360
|
+
{
|
|
361
|
+
send_json_response(
|
|
362
|
+
socket,
|
|
363
|
+
"200 OK",
|
|
364
|
+
&serde_json::json!({ "status": "ok", "displayName": display_name })
|
|
365
|
+
.to_string(),
|
|
366
|
+
)
|
|
367
|
+
.await;
|
|
259
368
|
return;
|
|
260
369
|
}
|
|
261
370
|
}
|
|
@@ -287,6 +396,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
287
396
|
struct ApiSaveInteraction {
|
|
288
397
|
chat_id: String,
|
|
289
398
|
user_text: String,
|
|
399
|
+
ai_text: Option<String>,
|
|
290
400
|
provider: String,
|
|
291
401
|
model: String,
|
|
292
402
|
}
|
|
@@ -296,7 +406,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
296
406
|
match memory.add_interaction_for_chat(
|
|
297
407
|
&req.chat_id,
|
|
298
408
|
&req.user_text,
|
|
299
|
-
"",
|
|
409
|
+
req.ai_text.as_deref().unwrap_or(""),
|
|
300
410
|
&req.provider,
|
|
301
411
|
&req.model,
|
|
302
412
|
) {
|
|
@@ -341,25 +451,18 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
341
451
|
activity: Vec<AgentProgress>,
|
|
342
452
|
}
|
|
343
453
|
|
|
344
|
-
if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
.await;
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
Err(_) => {}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
454
|
+
if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body)
|
|
455
|
+
&& let Ok(memory) = MemoryStore::open_default()
|
|
456
|
+
&& let Ok(activity_json) = serde_json::to_string(&payload.activity)
|
|
457
|
+
&& memory
|
|
458
|
+
.set_interaction_agent_activity_json(
|
|
459
|
+
payload.interaction_id,
|
|
460
|
+
&activity_json,
|
|
461
|
+
)
|
|
462
|
+
.is_ok()
|
|
463
|
+
{
|
|
464
|
+
send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
|
|
465
|
+
return;
|
|
363
466
|
}
|
|
364
467
|
send_json_response(
|
|
365
468
|
socket,
|
|
@@ -397,6 +500,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
397
500
|
}
|
|
398
501
|
}
|
|
399
502
|
}
|
|
503
|
+
("GET", route) if route.starts_with("/api/thumbnails/") => {
|
|
504
|
+
let filename = percent_decode(route.trim_start_matches("/api/thumbnails/"));
|
|
505
|
+
match thumbnail_bytes(&filename) {
|
|
506
|
+
Ok((mime_type, bytes)) => {
|
|
507
|
+
send_binary_response(socket, "200 OK", &mime_type, &bytes).await
|
|
508
|
+
}
|
|
509
|
+
Err(_) => {
|
|
510
|
+
send_json_response(
|
|
511
|
+
socket,
|
|
512
|
+
"404 Not Found",
|
|
513
|
+
"{\"error\":\"thumbnail not found\"}",
|
|
514
|
+
)
|
|
515
|
+
.await
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
400
519
|
("GET", "/api/config") => {
|
|
401
520
|
let config = load_config().unwrap_or_default();
|
|
402
521
|
if let Ok(json_str) = serde_json::to_string(&config) {
|
|
@@ -406,11 +525,11 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
406
525
|
}
|
|
407
526
|
}
|
|
408
527
|
("POST", "/api/config") => {
|
|
409
|
-
if let Ok(new_config) = serde_json::from_str::<MintConfig>(body)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
528
|
+
if let Ok(new_config) = serde_json::from_str::<MintConfig>(body)
|
|
529
|
+
&& save_config(&new_config).is_ok()
|
|
530
|
+
{
|
|
531
|
+
send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
|
|
532
|
+
return;
|
|
414
533
|
}
|
|
415
534
|
send_json_response(
|
|
416
535
|
socket,
|
|
@@ -466,6 +585,71 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
466
585
|
.await;
|
|
467
586
|
}
|
|
468
587
|
}
|
|
588
|
+
("POST", "/api/uploads") => {
|
|
589
|
+
// Accept raw body bytes and optional filename query param.
|
|
590
|
+
// Body is the raw file bytes (client should POST the file as the request body).
|
|
591
|
+
let filename_param = query_param(query, "filename");
|
|
592
|
+
let header_end_idx = header_end as usize;
|
|
593
|
+
// extract raw bytes for body
|
|
594
|
+
let body_bytes = &request_bytes[header_end_idx + 4..];
|
|
595
|
+
|
|
596
|
+
if body_bytes.is_empty() {
|
|
597
|
+
send_json_response(socket, "400 Bad Request", "{\"error\":\"empty body\"}")
|
|
598
|
+
.await;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Determine mime type from filename or fallback to video/mp4
|
|
603
|
+
let (mime_type, _extension) = filename_param
|
|
604
|
+
.as_deref()
|
|
605
|
+
.and_then(|f| f.rsplit_once('.'))
|
|
606
|
+
.map(|(_, ext)| {
|
|
607
|
+
let e = ext.to_ascii_lowercase();
|
|
608
|
+
match e.as_str() {
|
|
609
|
+
"mp4" => ("video/mp4", "mp4"),
|
|
610
|
+
"webm" => ("video/webm", "webm"),
|
|
611
|
+
"mov" => ("video/quicktime", "mov"),
|
|
612
|
+
"mkv" => ("video/x-matroska", "mkv"),
|
|
613
|
+
"avi" => ("video/x-msvideo", "avi"),
|
|
614
|
+
_ => ("application/octet-stream", "bin"),
|
|
615
|
+
}
|
|
616
|
+
})
|
|
617
|
+
.unwrap_or(("video/mp4", "mp4"));
|
|
618
|
+
|
|
619
|
+
// Build data URI and reuse save_chat_images helper to persist and index file
|
|
620
|
+
let encoded = BASE64.encode(body_bytes);
|
|
621
|
+
let data_uri = format!("data:{};base64,{}", mime_type, encoded);
|
|
622
|
+
match save_chat_images(
|
|
623
|
+
vec![data_uri],
|
|
624
|
+
Some("upload".into()),
|
|
625
|
+
Some("uploaded".into()),
|
|
626
|
+
) {
|
|
627
|
+
Ok(saved) => {
|
|
628
|
+
if let Some(entry) = saved.into_iter().next() {
|
|
629
|
+
let res_json = json!({ "url": format!("/api/pictures/{}", entry.filename), "filename": entry.filename });
|
|
630
|
+
send_json_response(socket, "200 OK", &res_json.to_string()).await;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
Err(err) => {
|
|
635
|
+
let err_json = json!({ "error": err.to_string() });
|
|
636
|
+
send_json_response(
|
|
637
|
+
socket,
|
|
638
|
+
"500 Internal Server Error",
|
|
639
|
+
&err_json.to_string(),
|
|
640
|
+
)
|
|
641
|
+
.await;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
send_json_response(
|
|
646
|
+
socket,
|
|
647
|
+
"500 Internal Server Error",
|
|
648
|
+
"{\"error\":\"failed to save upload\"}",
|
|
649
|
+
)
|
|
650
|
+
.await;
|
|
651
|
+
}
|
|
652
|
+
|
|
469
653
|
("POST", "/api/chat") => {
|
|
470
654
|
#[derive(Deserialize)]
|
|
471
655
|
#[serde(rename_all = "camelCase")]
|
|
@@ -475,6 +659,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
475
659
|
chat_id: Option<String>,
|
|
476
660
|
image_data_uri: Option<String>,
|
|
477
661
|
audio_data_uri: Option<String>,
|
|
662
|
+
video_data_uri: Option<String>,
|
|
478
663
|
document_attachment: Option<crate::chat::DocumentAttachment>,
|
|
479
664
|
agent_id: Option<String>,
|
|
480
665
|
}
|
|
@@ -487,6 +672,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
487
672
|
chat_id: req.chat_id,
|
|
488
673
|
image_data_uri: req.image_data_uri,
|
|
489
674
|
audio_data_uri: req.audio_data_uri,
|
|
675
|
+
video_data_uri: req.video_data_uri,
|
|
490
676
|
document_attachment: req.document_attachment,
|
|
491
677
|
workspace_path: None,
|
|
492
678
|
agent_id: req.agent_id,
|
|
@@ -494,6 +680,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
494
680
|
let mut chat_req =
|
|
495
681
|
chat_req.with_document_context(&config).unwrap_or(chat_req);
|
|
496
682
|
let sent_image = chat_req.image_data_uri.clone();
|
|
683
|
+
let sent_video = chat_req.video_data_uri.clone();
|
|
497
684
|
let sent_message = chat_req.message.clone();
|
|
498
685
|
|
|
499
686
|
let response = if let Some(clean_message) =
|
|
@@ -513,6 +700,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
513
700
|
|
|
514
701
|
match response {
|
|
515
702
|
Ok(resp) => {
|
|
703
|
+
log_api_req(
|
|
704
|
+
"POST",
|
|
705
|
+
"/api/chat",
|
|
706
|
+
"200 OK",
|
|
707
|
+
Some(&format!("Model: {}", config.ai_provider)),
|
|
708
|
+
);
|
|
516
709
|
if let Some(image) = sent_image {
|
|
517
710
|
let _ = save_chat_images(
|
|
518
711
|
image
|
|
@@ -520,6 +713,16 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
520
713
|
.map(str::to_owned)
|
|
521
714
|
.collect::<Vec<_>>(),
|
|
522
715
|
Some("web".into()),
|
|
716
|
+
Some(sent_message.clone()),
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
if let Some(video) = sent_video {
|
|
720
|
+
let _ = save_chat_images(
|
|
721
|
+
video
|
|
722
|
+
.split_whitespace()
|
|
723
|
+
.map(str::to_owned)
|
|
724
|
+
.collect::<Vec<_>>(),
|
|
725
|
+
Some("web".into()),
|
|
523
726
|
Some(sent_message),
|
|
524
727
|
);
|
|
525
728
|
}
|
|
@@ -529,7 +732,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
529
732
|
}
|
|
530
733
|
}
|
|
531
734
|
Err(e) => {
|
|
532
|
-
|
|
735
|
+
log_api_err("API /api/chat error", &e);
|
|
533
736
|
let err_json = serde_json::json!({
|
|
534
737
|
"provider": "error",
|
|
535
738
|
"model": "error",
|
|
@@ -579,6 +782,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
579
782
|
chat_id: Option<String>,
|
|
580
783
|
image_data_uri: Option<String>,
|
|
581
784
|
audio_data_uri: Option<String>,
|
|
785
|
+
video_data_uri: Option<String>,
|
|
582
786
|
document_attachment: Option<crate::chat::DocumentAttachment>,
|
|
583
787
|
agent_id: Option<String>,
|
|
584
788
|
}
|
|
@@ -591,6 +795,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
591
795
|
chat_id: req.chat_id,
|
|
592
796
|
image_data_uri: req.image_data_uri,
|
|
593
797
|
audio_data_uri: req.audio_data_uri,
|
|
798
|
+
video_data_uri: req.video_data_uri,
|
|
594
799
|
document_attachment: req.document_attachment,
|
|
595
800
|
workspace_path: None,
|
|
596
801
|
agent_id: req.agent_id,
|
|
@@ -598,6 +803,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
598
803
|
let mut chat_req =
|
|
599
804
|
chat_req.with_document_context(&config).unwrap_or(chat_req);
|
|
600
805
|
let sent_image = chat_req.image_data_uri.clone();
|
|
806
|
+
let sent_video = chat_req.video_data_uri.clone();
|
|
601
807
|
let sent_message = chat_req.message.clone();
|
|
602
808
|
|
|
603
809
|
let is_chat = if let Some(clean_message) =
|
|
@@ -674,6 +880,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
674
880
|
|
|
675
881
|
match result {
|
|
676
882
|
Ok((response, _)) => {
|
|
883
|
+
log_api_req(
|
|
884
|
+
"POST",
|
|
885
|
+
"/api/chat-stream",
|
|
886
|
+
"200 OK",
|
|
887
|
+
Some(&format!(
|
|
888
|
+
"Provider: {}",
|
|
889
|
+
config_clone.ai_provider
|
|
890
|
+
)),
|
|
891
|
+
);
|
|
677
892
|
if let Ok(json_val) =
|
|
678
893
|
serde_json::to_string(&serde_json::json!({
|
|
679
894
|
"type": "done",
|
|
@@ -684,7 +899,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
684
899
|
}
|
|
685
900
|
}
|
|
686
901
|
Err(e) => {
|
|
687
|
-
|
|
902
|
+
log_api_err("API /api/chat-stream error", &e);
|
|
688
903
|
let err_json = serde_json::json!({
|
|
689
904
|
"type": "done",
|
|
690
905
|
"response": {
|
|
@@ -693,22 +908,27 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
693
908
|
"text": format!("Error orchestrating chat: {e}")
|
|
694
909
|
}
|
|
695
910
|
});
|
|
696
|
-
let _ = tx_done
|
|
697
|
-
.send(format!("{}\n", err_json.to_string()));
|
|
911
|
+
let _ = tx_done.send(format!("{}\n", err_json));
|
|
698
912
|
}
|
|
699
913
|
}
|
|
700
914
|
});
|
|
701
915
|
|
|
702
916
|
let abort_handle = join_handle.abort_handle();
|
|
703
917
|
if !chat_id_str.is_empty() {
|
|
704
|
-
crate::ACTIVE_AGENTS
|
|
918
|
+
crate::ACTIVE_AGENTS
|
|
919
|
+
.lock()
|
|
920
|
+
.unwrap()
|
|
921
|
+
.insert(chat_id_str.clone(), abort_handle);
|
|
705
922
|
}
|
|
706
923
|
|
|
707
924
|
let chat_id_str_cleanup = chat_id_str.clone();
|
|
708
925
|
tokio::spawn(async move {
|
|
709
926
|
let _ = join_handle.await;
|
|
710
927
|
if !chat_id_str_cleanup.is_empty() {
|
|
711
|
-
crate::ACTIVE_AGENTS
|
|
928
|
+
crate::ACTIVE_AGENTS
|
|
929
|
+
.lock()
|
|
930
|
+
.unwrap()
|
|
931
|
+
.remove(&chat_id_str_cleanup);
|
|
712
932
|
}
|
|
713
933
|
});
|
|
714
934
|
} else {
|
|
@@ -725,6 +945,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
725
945
|
let chat_id_str = chat_id.clone().unwrap_or_default();
|
|
726
946
|
let message = chat_req.message.clone();
|
|
727
947
|
let image_data_uri = chat_req.image_data_uri.clone();
|
|
948
|
+
let video_data_uri = chat_req.video_data_uri.clone();
|
|
728
949
|
let agent_id = chat_req.agent_id.clone();
|
|
729
950
|
|
|
730
951
|
let join_handle = tokio::spawn(async move {
|
|
@@ -733,6 +954,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
733
954
|
&message,
|
|
734
955
|
&root,
|
|
735
956
|
image_data_uri,
|
|
957
|
+
video_data_uri,
|
|
736
958
|
chat_id.as_deref(),
|
|
737
959
|
agent_id.as_deref(),
|
|
738
960
|
fast_mode,
|
|
@@ -768,22 +990,27 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
768
990
|
"text": format!("Error orchestrating agent: {e}")
|
|
769
991
|
}
|
|
770
992
|
});
|
|
771
|
-
let _ = tx_done
|
|
772
|
-
.send(format!("{}\n", err_json.to_string()));
|
|
993
|
+
let _ = tx_done.send(format!("{}\n", err_json));
|
|
773
994
|
}
|
|
774
995
|
}
|
|
775
996
|
});
|
|
776
997
|
|
|
777
998
|
let abort_handle = join_handle.abort_handle();
|
|
778
999
|
if !chat_id_str.is_empty() {
|
|
779
|
-
crate::ACTIVE_AGENTS
|
|
1000
|
+
crate::ACTIVE_AGENTS
|
|
1001
|
+
.lock()
|
|
1002
|
+
.unwrap()
|
|
1003
|
+
.insert(chat_id_str.clone(), abort_handle);
|
|
780
1004
|
}
|
|
781
1005
|
|
|
782
1006
|
let chat_id_str_cleanup = chat_id_str.clone();
|
|
783
1007
|
tokio::spawn(async move {
|
|
784
1008
|
let _ = join_handle.await;
|
|
785
1009
|
if !chat_id_str_cleanup.is_empty() {
|
|
786
|
-
crate::ACTIVE_AGENTS
|
|
1010
|
+
crate::ACTIVE_AGENTS
|
|
1011
|
+
.lock()
|
|
1012
|
+
.unwrap()
|
|
1013
|
+
.remove(&chat_id_str_cleanup);
|
|
787
1014
|
}
|
|
788
1015
|
});
|
|
789
1016
|
}
|
|
@@ -805,6 +1032,16 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
805
1032
|
.map(str::to_owned)
|
|
806
1033
|
.collect::<Vec<_>>(),
|
|
807
1034
|
Some("web".into()),
|
|
1035
|
+
Some(sent_message.clone()),
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
if let Some(video) = sent_video {
|
|
1039
|
+
let _ = save_chat_images(
|
|
1040
|
+
video
|
|
1041
|
+
.split_whitespace()
|
|
1042
|
+
.map(str::to_owned)
|
|
1043
|
+
.collect::<Vec<_>>(),
|
|
1044
|
+
Some("web".into()),
|
|
808
1045
|
Some(sent_message),
|
|
809
1046
|
);
|
|
810
1047
|
}
|
|
@@ -834,6 +1071,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
834
1071
|
/// Which image provider to use (overrides config.image_gen_provider).
|
|
835
1072
|
#[serde(default)]
|
|
836
1073
|
provider: Option<String>,
|
|
1074
|
+
#[serde(default)]
|
|
1075
|
+
image_data_uri: Option<String>,
|
|
1076
|
+
#[serde(default)]
|
|
1077
|
+
mask_data_uri: Option<String>,
|
|
1078
|
+
#[serde(default)]
|
|
1079
|
+
mode: Option<String>,
|
|
837
1080
|
}
|
|
838
1081
|
|
|
839
1082
|
if let Ok(req) = serde_json::from_str::<ImageGenApiRequest>(body) {
|
|
@@ -845,9 +1088,18 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
845
1088
|
num_images: req.num_images,
|
|
846
1089
|
model: req.model,
|
|
847
1090
|
provider: req.provider,
|
|
1091
|
+
image_data_uri: req.image_data_uri,
|
|
1092
|
+
mask_data_uri: req.mask_data_uri,
|
|
1093
|
+
mode: req.mode,
|
|
848
1094
|
};
|
|
849
1095
|
match generate_images(&config, &gen_request).await {
|
|
850
1096
|
Ok(result) => {
|
|
1097
|
+
log_api_req(
|
|
1098
|
+
"POST",
|
|
1099
|
+
"/api/image-generate",
|
|
1100
|
+
"200 OK",
|
|
1101
|
+
Some(&format!("Provider: {}", result.provider)),
|
|
1102
|
+
);
|
|
851
1103
|
let data_uris: Vec<String> = result
|
|
852
1104
|
.images
|
|
853
1105
|
.iter()
|
|
@@ -875,6 +1127,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
875
1127
|
send_json_response(socket, "200 OK", &response.to_string()).await;
|
|
876
1128
|
}
|
|
877
1129
|
Err(e) => {
|
|
1130
|
+
log_api_err("API /api/image-generate error", &e);
|
|
878
1131
|
let err = json!({ "error": e.to_string() });
|
|
879
1132
|
send_json_response(
|
|
880
1133
|
socket,
|
|
@@ -893,6 +1146,98 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
893
1146
|
.await;
|
|
894
1147
|
}
|
|
895
1148
|
}
|
|
1149
|
+
("POST", "/api/video-generate") => {
|
|
1150
|
+
#[derive(serde::Deserialize)]
|
|
1151
|
+
#[serde(rename_all = "camelCase")]
|
|
1152
|
+
struct VideoGenApiRequest {
|
|
1153
|
+
prompt: String,
|
|
1154
|
+
#[serde(default)]
|
|
1155
|
+
negative_prompt: Option<String>,
|
|
1156
|
+
#[serde(default)]
|
|
1157
|
+
aspect_ratio: Option<String>,
|
|
1158
|
+
#[serde(default)]
|
|
1159
|
+
duration: Option<u32>,
|
|
1160
|
+
#[serde(default)]
|
|
1161
|
+
model: Option<String>,
|
|
1162
|
+
#[serde(default)]
|
|
1163
|
+
provider: Option<String>,
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if let Ok(req) = serde_json::from_str::<VideoGenApiRequest>(body) {
|
|
1167
|
+
let config = load_config().unwrap_or_default();
|
|
1168
|
+
let gen_request = VideoGenRequest {
|
|
1169
|
+
prompt: req.prompt.clone(),
|
|
1170
|
+
negative_prompt: req.negative_prompt,
|
|
1171
|
+
aspect_ratio: req.aspect_ratio.unwrap_or_else(|| "16:9".to_string()),
|
|
1172
|
+
duration: req.duration.unwrap_or(5),
|
|
1173
|
+
model: req.model,
|
|
1174
|
+
provider: req.provider.unwrap_or_else(|| "veo".to_string()),
|
|
1175
|
+
};
|
|
1176
|
+
match generate_video(&config, &gen_request).await {
|
|
1177
|
+
Ok(result) => {
|
|
1178
|
+
let mut response =
|
|
1179
|
+
serde_json::to_value(&result).unwrap_or(json!({}));
|
|
1180
|
+
if let Some(videos) =
|
|
1181
|
+
response.get_mut("videos").and_then(|v| v.as_array_mut())
|
|
1182
|
+
{
|
|
1183
|
+
for picture in videos {
|
|
1184
|
+
let filename = picture
|
|
1185
|
+
.get("filename")
|
|
1186
|
+
.and_then(|f| f.as_str())
|
|
1187
|
+
.map(|s| s.to_string());
|
|
1188
|
+
if let Some(filename) = filename {
|
|
1189
|
+
picture.as_object_mut().unwrap().insert(
|
|
1190
|
+
"url".to_string(),
|
|
1191
|
+
json!(format!("/api/pictures/{}", filename)),
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
let id = picture
|
|
1195
|
+
.get("id")
|
|
1196
|
+
.and_then(|i| i.as_str())
|
|
1197
|
+
.map(|s| s.to_string());
|
|
1198
|
+
if let Some(id) = id {
|
|
1199
|
+
let has_thumb = picture.get("thumbnailPath").is_some()
|
|
1200
|
+
|| picture.get("thumbnailUrl").is_some();
|
|
1201
|
+
if has_thumb {
|
|
1202
|
+
picture.as_object_mut().unwrap().insert(
|
|
1203
|
+
"thumbnailUrl".to_string(),
|
|
1204
|
+
json!(format!(
|
|
1205
|
+
"/api/thumbnails/{}.thumb.png",
|
|
1206
|
+
id
|
|
1207
|
+
)),
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
log_api_req(
|
|
1214
|
+
"POST",
|
|
1215
|
+
"/api/video-generate",
|
|
1216
|
+
"200 OK",
|
|
1217
|
+
Some(&format!("Provider: {}", result.provider)),
|
|
1218
|
+
);
|
|
1219
|
+
send_json_response(socket, "200 OK", &response.to_string()).await;
|
|
1220
|
+
}
|
|
1221
|
+
Err(e) => {
|
|
1222
|
+
log_api_err("API /api/video-generate error", &e);
|
|
1223
|
+
let err = json!({ "error": e.to_string() });
|
|
1224
|
+
send_json_response(
|
|
1225
|
+
socket,
|
|
1226
|
+
"500 Internal Server Error",
|
|
1227
|
+
&err.to_string(),
|
|
1228
|
+
)
|
|
1229
|
+
.await;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
send_json_response(
|
|
1234
|
+
socket,
|
|
1235
|
+
"400 Bad Request",
|
|
1236
|
+
"{\"error\":\"invalid video generation request body\"}",
|
|
1237
|
+
)
|
|
1238
|
+
.await;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
896
1241
|
("GET", "/api/image-gen/providers") => {
|
|
897
1242
|
let config = load_config().unwrap_or_default();
|
|
898
1243
|
let mut available: Vec<String> = Vec::new();
|
|
@@ -945,6 +1290,7 @@ async fn run_web_agent_loop(
|
|
|
945
1290
|
&request.message,
|
|
946
1291
|
&root,
|
|
947
1292
|
request.image_data_uri.clone(),
|
|
1293
|
+
request.video_data_uri.clone(),
|
|
948
1294
|
request.chat_id.as_deref(),
|
|
949
1295
|
request.agent_id.as_deref(),
|
|
950
1296
|
fast_mode,
|
|
@@ -1123,6 +1469,25 @@ fn picture_bytes(filename: &str) -> Result<(String, Vec<u8>), String> {
|
|
|
1123
1469
|
Ok((picture.mime_type, bytes))
|
|
1124
1470
|
}
|
|
1125
1471
|
|
|
1472
|
+
fn thumbnail_bytes(filename: &str) -> Result<(String, Vec<u8>), String> {
|
|
1473
|
+
if filename.contains('/') || filename.contains('\\') || filename.contains("..") {
|
|
1474
|
+
return Err("invalid thumbnail path".into());
|
|
1475
|
+
}
|
|
1476
|
+
let id = filename
|
|
1477
|
+
.strip_suffix(".thumb.png")
|
|
1478
|
+
.ok_or_else(|| "invalid thumbnail name".to_string())?;
|
|
1479
|
+
let picture = list_saved_pictures()
|
|
1480
|
+
.map_err(|error| error.to_string())?
|
|
1481
|
+
.into_iter()
|
|
1482
|
+
.find(|entry| entry.id == id)
|
|
1483
|
+
.ok_or_else(|| "picture not found".to_string())?;
|
|
1484
|
+
let thumb_path = picture
|
|
1485
|
+
.thumbnail_path
|
|
1486
|
+
.ok_or_else(|| "thumbnail not found".to_string())?;
|
|
1487
|
+
let bytes = std::fs::read(&thumb_path).map_err(|error| error.to_string())?;
|
|
1488
|
+
Ok(("image/png".to_string(), bytes))
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1126
1491
|
fn query_param(query: &str, key: &str) -> Option<String> {
|
|
1127
1492
|
query.split('&').find_map(|pair| {
|
|
1128
1493
|
let (name, value) = pair.split_once('=')?;
|
|
@@ -1135,12 +1500,13 @@ fn percent_decode(raw: &str) -> String {
|
|
|
1135
1500
|
let mut output = Vec::with_capacity(bytes.len());
|
|
1136
1501
|
let mut index = 0;
|
|
1137
1502
|
while index < bytes.len() {
|
|
1138
|
-
if bytes[index] == b'%'
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1503
|
+
if bytes[index] == b'%'
|
|
1504
|
+
&& index + 2 < bytes.len()
|
|
1505
|
+
&& let Ok(hex) = u8::from_str_radix(&raw[index + 1..index + 3], 16)
|
|
1506
|
+
{
|
|
1507
|
+
output.push(hex);
|
|
1508
|
+
index += 3;
|
|
1509
|
+
continue;
|
|
1144
1510
|
}
|
|
1145
1511
|
output.push(if bytes[index] == b'+' {
|
|
1146
1512
|
b' '
|