Skip to content

Commit f586c8f

Browse files
PurpleDoubleDclaude
andcommitted
fix: Tauri .exe production build — all features working
The released .exe was completely broken because features only existed in Vite middleware but had no Tauri Rust equivalents. Rust backend: - Add fetch_external command: HTTP proxy for CivitAI API, workflow JSON - Add fetch_external_bytes command: binary proxy for ZIP/image downloads - Register both in main.rs invoke_handler Frontend routing: - Add fetchExternal() and fetchExternalBytes() in backend.ts that route to Tauri invoke() in production or Vite proxy in dev mode - Fix workflows.ts: CivitAI search uses fetchExternal() instead of /civitai-api/ relative URL - Fix discover.ts: CivitAI model search uses fetchExternal() - Fix workflows.ts: fetchWorkflowFromUrl uses fetchExternalBytes() instead of /local-api/proxy-download relative URL Security: - Update CSP in tauri.conf.json: allow civitai.com, huggingface.co, ollama.com for connect-src and img-src UX: - Default view changed from Chat to Model Manager — new users need models before they can do anything - Privacy proxy: Tauri mode loads images directly (CSP allows), dev mode continues using local proxy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 621b521 commit f586c8f

8 files changed

Lines changed: 101 additions & 24 deletions

File tree

src-tauri/src/commands/proxy.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,47 @@
1+
/// Generic HTTP proxy — fetch any external URL and return body as string.
2+
/// Used for CivitAI API calls, workflow JSON downloads, etc.
3+
#[tauri::command]
4+
pub async fn fetch_external(url: String) -> Result<String, String> {
5+
let client = reqwest::Client::builder()
6+
.user_agent("LocallyUncensored/1.5")
7+
.timeout(std::time::Duration::from_secs(60))
8+
.build()
9+
.map_err(|e| e.to_string())?;
10+
11+
let resp = client.get(&url)
12+
.send()
13+
.await
14+
.map_err(|e| format!("fetch_external: {}", e))?;
15+
16+
if !resp.status().is_success() {
17+
return Err(format!("HTTP {}: {}", resp.status().as_u16(), url));
18+
}
19+
20+
resp.text().await.map_err(|e| e.to_string())
21+
}
22+
23+
/// Binary HTTP proxy — fetch any external URL and return bytes.
24+
/// Used for downloading ZIP files, images, model files.
25+
#[tauri::command]
26+
pub async fn fetch_external_bytes(url: String) -> Result<Vec<u8>, String> {
27+
let client = reqwest::Client::builder()
28+
.user_agent("LocallyUncensored/1.5")
29+
.timeout(std::time::Duration::from_secs(300))
30+
.build()
31+
.map_err(|e| e.to_string())?;
32+
33+
let resp = client.get(&url)
34+
.send()
35+
.await
36+
.map_err(|e| format!("fetch_external_bytes: {}", e))?;
37+
38+
if !resp.status().is_success() {
39+
return Err(format!("HTTP {}: {}", resp.status().as_u16(), url));
40+
}
41+
42+
resp.bytes().await.map(|b| b.to_vec()).map_err(|e| e.to_string())
43+
}
44+
145
/// Proxy search requests to ollama.com (needed because frontend can't CORS to ollama.com)
246
#[tauri::command]
347
pub async fn ollama_search(query: String) -> Result<serde_json::Value, String> {

src-tauri/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ fn main() {
4242
commands::search::searxng_status,
4343
// Proxy
4444
commands::proxy::ollama_search,
45+
commands::proxy::fetch_external,
46+
commands::proxy::fetch_external_bytes,
4547
])
4648
.setup(|app| {
4749
let state = app.state::<AppState>();

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
}
2222
],
2323
"security": {
24-
"csp": "default-src 'self'; connect-src 'self' http://localhost:11434 http://127.0.0.1:11434 http://localhost:8188 http://127.0.0.1:8188 ws://localhost:8188 ws://127.0.0.1:8188; img-src 'self' data: blob: http://localhost:8188 http://127.0.0.1:8188; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"
24+
"csp": "default-src 'self'; connect-src 'self' http://localhost:* http://127.0.0.1:* https://civitai.com https://*.civitai.com https://huggingface.co https://*.huggingface.co https://ollama.com ws://localhost:8188 ws://127.0.0.1:8188; img-src 'self' data: blob: http://localhost:8188 http://127.0.0.1:8188 https://*.civitai.com https://*.githubusercontent.com; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"
2525
}
2626
},
2727
"bundle": {

src/api/backend.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,26 @@ export function comfyuiUrl(path: string): string {
110110
export function comfyuiWsUrl(): string {
111111
return "ws://localhost:8188/ws";
112112
}
113+
114+
/** Fetch an external URL as text — works in both Tauri and dev mode */
115+
export async function fetchExternal(url: string): Promise<string> {
116+
if (isTauri()) {
117+
const invoke = await getInvoke();
118+
return invoke('fetch_external', { url }) as Promise<string>;
119+
}
120+
const res = await fetch(`/local-api/proxy-download?url=${encodeURIComponent(url)}`);
121+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
122+
return res.text();
123+
}
124+
125+
/** Fetch an external URL as bytes — works in both Tauri and dev mode */
126+
export async function fetchExternalBytes(url: string): Promise<ArrayBuffer> {
127+
if (isTauri()) {
128+
const invoke = await getInvoke();
129+
const bytes = await invoke('fetch_external_bytes', { url }) as number[];
130+
return new Uint8Array(bytes).buffer;
131+
}
132+
const res = await fetch(`/local-api/proxy-download?url=${encodeURIComponent(url)}`);
133+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
134+
return res.arrayBuffer();
135+
}

src/api/discover.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { backendCall } from "./backend"
1+
import { backendCall, fetchExternal } from "./backend"
22

33
export interface DiscoverModel {
44
name: string
@@ -326,10 +326,8 @@ export async function searchCivitaiModels(
326326
limit: '20',
327327
sort: 'Most Downloaded',
328328
})
329-
const resp = await fetch(`/civitai-api/v1/models?${params}`)
330-
if (!resp.ok) return []
331-
332-
const data = await resp.json()
329+
const text = await fetchExternal(`https://civitai.com/api/v1/models?${params}`)
330+
const data = JSON.parse(text)
333331
const items: any[] = data.items ?? []
334332

335333
return items.map((item) => {

src/api/workflows.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import JSZip from 'jszip'
22
import type { ModelType } from './comfyui'
33
import type { GenerateParams, VideoParams } from './comfyui'
44
import { findMatchingVAE, findMatchingCLIP } from './comfyui'
5+
import { fetchExternal, fetchExternalBytes } from './backend'
56
import type {
67
WorkflowTemplate,
78
WorkflowSearchResult,
@@ -397,16 +398,19 @@ export async function fetchWorkflowFromUrl(url: string, apiKey?: string): Promis
397398
const sep = url.includes('?') ? '&' : '?'
398399
finalUrl = `${url}${sep}token=${apiKey}`
399400
}
400-
// Route external URLs through the server-side proxy to avoid CORS/redirect issues
401-
const fetchUrl = finalUrl.startsWith('/') ? finalUrl : `/local-api/proxy-download?url=${encodeURIComponent(finalUrl)}`
402-
const resp = await fetch(fetchUrl)
403-
if (!resp.ok) throw new Error(`Failed to fetch workflow: ${resp.status} ${resp.statusText}`)
401+
// Route through backend proxy (works in both Tauri and dev mode)
402+
let buffer: ArrayBuffer
403+
try {
404+
buffer = await fetchExternalBytes(finalUrl)
405+
} catch (err) {
406+
throw new Error(`Failed to fetch workflow: ${err instanceof Error ? err.message : String(err)}`)
407+
}
404408

405-
const contentType = resp.headers.get('content-type') || ''
409+
// Detect content type from URL or try parsing
410+
const isLikelyZip = url.endsWith('.zip') || finalUrl.includes('/download/')
406411

407412
// Handle ZIP archives (CivitAI downloads workflows as .zip)
408-
if (contentType.includes('zip') || contentType.includes('octet-stream') || url.endsWith('.zip')) {
409-
const buffer = await resp.arrayBuffer()
413+
if (isLikelyZip) {
410414
try {
411415
const zip = await JSZip.loadAsync(buffer)
412416
// Try all files in the ZIP that could contain workflow JSON
@@ -438,10 +442,13 @@ export async function fetchWorkflowFromUrl(url: string, apiKey?: string): Promis
438442
}
439443
}
440444

441-
// Regular JSON
442-
const json = await resp.json()
443-
const resolved = resolveWorkflowJson(json)
444-
if (resolved) return resolved
445+
// Try parsing as JSON
446+
try {
447+
const text = new TextDecoder().decode(buffer)
448+
const json = JSON.parse(text)
449+
const resolved = resolveWorkflowJson(json)
450+
if (resolved) return resolved
451+
} catch { /* not JSON */ }
445452

446453
throw new Error('Invalid workflow format. Expected ComfyUI API or web format.')
447454
}
@@ -596,13 +603,12 @@ export async function searchCivitai(query: string): Promise<WorkflowSearchResult
596603
limit: '20',
597604
sort: 'Most Downloaded',
598605
})
599-
const resp = await fetch(`/civitai-api/v1/models?${params}`)
600-
if (!resp.ok) {
601-
console.warn(`[workflows] CivitAI returned ${resp.status}`)
606+
const text = await fetchExternal(`https://civitai.com/api/v1/models?${params}`)
607+
const data = JSON.parse(text)
608+
if (!data.items) {
609+
console.warn('[workflows] CivitAI returned no items')
602610
return []
603611
}
604-
605-
const data = await resp.json()
606612
const items: CivitAIModel[] = data.items ?? []
607613

608614
return items.map((item) => {

src/lib/privacy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
* so external servers never see the user's IP or browsing behavior.
44
*/
55

6+
import { isTauri } from '../api/backend'
7+
68
/** Proxy an external image URL through the local server */
79
export function proxyImageUrl(url: string | undefined): string | undefined {
810
if (!url) return undefined
911
// Already local — no proxy needed
1012
if (url.startsWith('/') || url.startsWith('blob:') || url.startsWith('data:')) return url
11-
// Route through local proxy
13+
// In Tauri mode: CSP allows these domains directly (images loaded via img tag, no CORS)
14+
if (isTauri()) return url
15+
// Dev mode: route through local proxy
1216
return `/local-api/proxy-image?url=${encodeURIComponent(url)}`
1317
}

src/stores/uiStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface UIState {
1111
}
1212

1313
export const useUIStore = create<UIState>()((set) => ({
14-
currentView: 'chat',
14+
currentView: 'models',
1515
sidebarOpen: true,
1616

1717
setView: (view) => set({ currentView: view }),

0 commit comments

Comments
 (0)