feat: 灵犀 Studio Web UI 定制版
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { getActiveProfileName, getApiKey, getBaseUrlValue } from '../client'
|
||||
|
||||
/**
|
||||
* Construct a download URL with auth token as query parameter.
|
||||
* Token is passed via query param because <a> tags cannot set headers.
|
||||
*/
|
||||
export function getDownloadUrl(filePath: string, fileName?: string): string {
|
||||
const base = getBaseUrlValue()
|
||||
|
||||
// Guard: if filePath is already a full download URL, extract the real path
|
||||
// to prevent double-wrapping (/api/hermes/download?path=/api/hermes/download?path=...)
|
||||
if (filePath.startsWith('/api/hermes/download?')) {
|
||||
try {
|
||||
const parsed = new URL(filePath, 'http://localhost')
|
||||
const realPath = parsed.searchParams.get('path')
|
||||
if (realPath) filePath = realPath
|
||||
} catch {
|
||||
// fall through with original filePath
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the path first in case it's already encoded (e.g., from AI responses)
|
||||
// URLSearchParams will encode it again, so we need to start with decoded text
|
||||
const decodedPath = decodeURIComponent(filePath)
|
||||
const params = new URLSearchParams({ path: decodedPath })
|
||||
if (fileName) {
|
||||
const decodedName = decodeURIComponent(fileName)
|
||||
params.set('name', decodedName)
|
||||
}
|
||||
const profileName = getActiveProfileName()
|
||||
if (profileName) params.set('profile', profileName)
|
||||
const token = getApiKey()
|
||||
if (token) params.set('token', token)
|
||||
return `${base}/api/hermes/download?${params.toString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file. Uses fetch to detect errors, then creates a blob URL
|
||||
* for the browser download. Throws with error message on failure.
|
||||
*/
|
||||
export async function downloadFile(filePath: string, fileName?: string): Promise<void> {
|
||||
const url = getDownloadUrl(filePath, fileName)
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
||||
throw new Error(body.error || `Download failed: ${res.status}`)
|
||||
}
|
||||
const blob = await res.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = fileName || filePath.split('/').pop() || 'download'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview file content.
|
||||
* Throws with error message on failure.
|
||||
*/
|
||||
export async function fetchFileText(filePath: string, fileName?: string): Promise<string> {
|
||||
const url = getDownloadUrl(filePath, fileName)
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
||||
throw new Error(body.error || `Preview failed: ${res.status}`)
|
||||
}
|
||||
return res.text()
|
||||
}
|
||||
Reference in New Issue
Block a user