Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
commit
03634b1ca3
11340 files changed
+3442369
No files matched your search
@@ -0,0 +1,229 @@
|
||||
import { readDesktopFileDataUrl } from '@/lib/desktop-fs'
|
||||
import { capitalize } from '@/lib/text'
|
||||
import { $connection } from '@/store/session'
|
||||
|
||||
export type MediaKind = 'audio' | 'image' | 'video' | 'file'
|
||||
|
||||
interface MediaInfo {
|
||||
kind: MediaKind
|
||||
mime: string
|
||||
}
|
||||
|
||||
const MEDIA_BY_EXT: Record<string, MediaInfo> = {
|
||||
avi: { kind: 'video', mime: 'video/x-msvideo' },
|
||||
bmp: { kind: 'image', mime: 'image/bmp' },
|
||||
flac: { kind: 'audio', mime: 'audio/flac' },
|
||||
gif: { kind: 'image', mime: 'image/gif' },
|
||||
jpeg: { kind: 'image', mime: 'image/jpeg' },
|
||||
jpg: { kind: 'image', mime: 'image/jpeg' },
|
||||
m4a: { kind: 'audio', mime: 'audio/mp4' },
|
||||
mkv: { kind: 'video', mime: 'video/x-matroska' },
|
||||
mov: { kind: 'video', mime: 'video/quicktime' },
|
||||
mp3: { kind: 'audio', mime: 'audio/mpeg' },
|
||||
mp4: { kind: 'video', mime: 'video/mp4' },
|
||||
ogg: { kind: 'audio', mime: 'audio/ogg' },
|
||||
opus: { kind: 'audio', mime: 'audio/ogg; codecs=opus' },
|
||||
png: { kind: 'image', mime: 'image/png' },
|
||||
svg: { kind: 'image', mime: 'image/svg+xml' },
|
||||
wav: { kind: 'audio', mime: 'audio/wav' },
|
||||
webm: { kind: 'video', mime: 'video/webm' },
|
||||
webp: { kind: 'image', mime: 'image/webp' }
|
||||
}
|
||||
|
||||
function mediaInfo(path: string): MediaInfo | undefined {
|
||||
const ext = path.split(/[?#]/, 1)[0]?.split('.').pop()?.toLowerCase()
|
||||
|
||||
return ext ? MEDIA_BY_EXT[ext] : undefined
|
||||
}
|
||||
|
||||
export function mediaKind(path: string): MediaKind {
|
||||
return mediaInfo(path)?.kind ?? 'file'
|
||||
}
|
||||
|
||||
// Markdown is renderable content, not an opaque download: the preview rail
|
||||
// already knows how to render a `.md` file (rendered/source toggle), so the
|
||||
// MEDIA delivery path routes these to a preview instead of a download link.
|
||||
const MARKDOWN_EXTENSIONS = new Set(['md', 'markdown', 'mdown', 'mkd'])
|
||||
|
||||
export function isMarkdownDocumentPath(path: string): boolean {
|
||||
const ext = path.split(/[?#]/, 1)[0]?.split('.').pop()?.toLowerCase()
|
||||
|
||||
return ext ? MARKDOWN_EXTENSIONS.has(ext) : false
|
||||
}
|
||||
|
||||
export function mediaMime(path: string): string {
|
||||
return mediaInfo(path)?.mime ?? 'application/octet-stream'
|
||||
}
|
||||
|
||||
export function mediaName(path: string): string {
|
||||
try {
|
||||
const url = new URL(path)
|
||||
|
||||
return url.pathname.split('/').filter(Boolean).pop() || path
|
||||
} catch {
|
||||
return path.split(/[\\/]/).filter(Boolean).pop() || path
|
||||
}
|
||||
}
|
||||
|
||||
export function mediaMarkdownHref(path: string): string {
|
||||
return `#media:${encodeURIComponent(path)}`
|
||||
}
|
||||
|
||||
export function isInlineMediaSrc(path: string): boolean {
|
||||
return /^(?:https?|data):/i.test(path)
|
||||
}
|
||||
|
||||
export function isFileMediaPath(path: string): boolean {
|
||||
return /^(?:file:|\/|~\/|[a-z]:[\\/]|\\\\)/i.test(path)
|
||||
}
|
||||
|
||||
export async function resolveMediaDisplaySrc(path: string): Promise<string> {
|
||||
if (isInlineMediaSrc(path) || !isFileMediaPath(path)) {
|
||||
return path
|
||||
}
|
||||
|
||||
if (window.hermesDesktop && isRemoteGateway()) {
|
||||
return gatewayMediaDataUrl(path)
|
||||
}
|
||||
|
||||
if (!window.hermesDesktop?.readFileDataUrl) {
|
||||
return mediaExternalUrl(path)
|
||||
}
|
||||
|
||||
return window.hermesDesktop.readFileDataUrl(filePathFromMediaPath(path))
|
||||
}
|
||||
|
||||
// Audio/video need a seekable source instead of a whole-file data URL. Keep
|
||||
// remote URLs untouched and route filesystem paths through the Electron media
|
||||
// protocol. Its main-process handler reads local files directly or proxies a
|
||||
// remote gateway with the connection's bearer/cookie/token authentication.
|
||||
export async function resolveMediaPlaybackSrc(path: string): Promise<string> {
|
||||
if (isInlineMediaSrc(path)) {
|
||||
return path
|
||||
}
|
||||
|
||||
if (window.hermesDesktop && ['audio', 'video'].includes(mediaKind(path))) {
|
||||
return isRemoteGateway() ? mediaGatewayStreamUrl(path) : mediaStreamUrl(path)
|
||||
}
|
||||
|
||||
return resolveMediaDisplaySrc(path)
|
||||
}
|
||||
|
||||
// Resolve a media path to a URL the shell can open. Remote mode rewrites
|
||||
// gateway-local paths to an authenticated /api/files/download URL (the file
|
||||
// lives on the gateway, not this disk); local mode keeps the file:// form.
|
||||
export function mediaExternalUrl(path: string): string {
|
||||
if (/^https?:/i.test(path)) {
|
||||
return path
|
||||
}
|
||||
|
||||
if (isRemoteGateway()) {
|
||||
const conn = $connection.get()
|
||||
|
||||
if (conn?.baseUrl && conn.token) {
|
||||
const file = encodeURIComponent(filePathFromMediaPath(path))
|
||||
|
||||
return `${conn.baseUrl}/api/files/download?path=${file}&token=${encodeURIComponent(conn.token)}`
|
||||
}
|
||||
}
|
||||
|
||||
return /^file:/i.test(path) ? path : `file://${path}`
|
||||
}
|
||||
|
||||
// Remote gateway audio/video is proxied by the Electron main process. OAuth
|
||||
// connections intentionally expose no static token to the renderer, so a bare
|
||||
// HTTPS source cannot authenticate reliably. The custom protocol keeps secrets
|
||||
// out of renderer URLs while forwarding Range requests to /api/files/stream.
|
||||
export function mediaGatewayStreamUrl(path: string): string {
|
||||
const conn = $connection.get()
|
||||
|
||||
if (isRemoteGateway()) {
|
||||
const file = encodeURIComponent(filePathFromMediaPath(path))
|
||||
|
||||
const scope = [
|
||||
conn?.connectionId ? `connectionId=${encodeURIComponent(conn.connectionId)}` : '',
|
||||
conn?.profile ? `profile=${encodeURIComponent(conn.profile)}` : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('&')
|
||||
|
||||
return `hermes-media://remote/${file}${scope ? `?${scope}` : ''}`
|
||||
}
|
||||
|
||||
return mediaExternalUrl(path)
|
||||
}
|
||||
|
||||
// Custom Electron scheme (registered in electron/main.ts) that streams a local
|
||||
// file with Range support. Used for audio/video so playback bypasses the data
|
||||
// URL size cap and supports seeking. `path` may be a plain path or `file://…`.
|
||||
export function mediaStreamUrl(path: string): string {
|
||||
return `hermes-media://stream/${encodeURIComponent(filePathFromMediaPath(path))}`
|
||||
}
|
||||
|
||||
export function mediaPathFromMarkdownHref(href?: string): string | null {
|
||||
if (!href?.startsWith('#media:')) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(href.slice('#media:'.length))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function filePathFromMediaPath(path: string): string {
|
||||
if (!path.startsWith('file:')) {
|
||||
return path
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(new URL(path).pathname)
|
||||
} catch {
|
||||
return path.replace(/^file:\/\//, '')
|
||||
}
|
||||
}
|
||||
|
||||
// True when this desktop shell is wired to a remote gateway. Local media paths
|
||||
// then live on the gateway machine, not this disk, so we fetch them over the API.
|
||||
export function isRemoteGateway(): boolean {
|
||||
return $connection.get()?.mode === 'remote'
|
||||
}
|
||||
|
||||
// Fetch gateway-local media as a data URL via the authenticated desktop FS
|
||||
// bridge. Remote Desktop artifacts can live anywhere the gateway can read
|
||||
// (workspace, skills, ~/.hermes/cache, etc.); /api/media is intentionally
|
||||
// narrower and rejects non-images plus images outside its media roots.
|
||||
export async function gatewayMediaDataUrl(path: string): Promise<string> {
|
||||
return readDesktopFileDataUrl(filePathFromMediaPath(path))
|
||||
}
|
||||
|
||||
// Remote-mode replacement for opening gateway-local file paths with file://.
|
||||
// The file lives on the gateway, so ask the Electron main process to fetch the
|
||||
// bytes through the authenticated backend connection and save them locally. This
|
||||
// avoids browser/OS downloads losing OAuth cookies and avoids the data-URL cap
|
||||
// used by preview endpoints.
|
||||
export async function downloadGatewayMediaFile(
|
||||
path: string
|
||||
): Promise<{ canceled?: boolean; path?: string; saved: boolean }> {
|
||||
const file = filePathFromMediaPath(path)
|
||||
const conn = $connection.get()
|
||||
|
||||
if (!window.hermesDesktop?.saveGatewayFile) {
|
||||
throw new Error('Desktop file download bridge is unavailable')
|
||||
}
|
||||
|
||||
return window.hermesDesktop.saveGatewayFile({
|
||||
connectionId: conn?.connectionId,
|
||||
path: file,
|
||||
profile: conn?.profile,
|
||||
suggestedName: mediaName(file)
|
||||
})
|
||||
}
|
||||
|
||||
export function mediaDisplayLabel(path: string): string {
|
||||
const escaped = mediaName(path).replace(/[[\]\\]/g, '\\$&')
|
||||
const kind = mediaKind(path)
|
||||
|
||||
return `${capitalize(kind)}: ${escaped}`
|
||||
}
|
||||
Reference in new issue
Block a user