Origem: D:/Documents/Github/PiAgentUI/PiAgentUi Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 4
169 lines
5.8 KiB
TypeScript
169 lines
5.8 KiB
TypeScript
import { createRoot } from 'react-dom/client'
|
||
import { createElement } from 'react'
|
||
import 'katex/dist/katex.min.css'
|
||
import './index.css'
|
||
import './i18n'
|
||
import { initOverlayScrollbars } from './lib/overlayScrollbar'
|
||
import App from './App.tsx'
|
||
import { ErrorBoundary } from './components/ErrorBoundary.tsx'
|
||
import { DirectoryProvider, SessionProvider } from './contexts'
|
||
import { themeStore } from './store/themeStore'
|
||
import { serverStore } from './store/serverStore'
|
||
import { messageStore } from './store/messageStore'
|
||
import { childSessionStore } from './store/childSessionStore'
|
||
import { todoStore } from './store/todoStore'
|
||
import { autoApproveStore } from './store/autoApproveStore'
|
||
import { serviceStore } from './store/serviceStore'
|
||
import { reconnectSSE } from './api/events'
|
||
import { resetPathModeCache } from './utils/directoryUtils'
|
||
import { isTauri, isTauriMobile } from './utils/tauri'
|
||
import { globalErrorHandler } from './utils/errorHandling'
|
||
|
||
// Polyfill: randomUUID 在非 HTTPS 环境可能缺失(如局域网 HTTP)
|
||
// 统一补齐,避免业务层 scattered fallback。
|
||
function ensureRandomUUID() {
|
||
const cryptoObj = globalThis.crypto as Crypto & { randomUUID?: () => string }
|
||
if (!cryptoObj || typeof cryptoObj.getRandomValues !== 'function') return
|
||
if (typeof cryptoObj.randomUUID === 'function') return
|
||
|
||
cryptoObj.randomUUID = () => {
|
||
const bytes = new Uint8Array(16)
|
||
cryptoObj.getRandomValues(bytes)
|
||
// RFC 4122 v4
|
||
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||
|
||
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0'))
|
||
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`
|
||
}
|
||
}
|
||
|
||
ensureRandomUUID()
|
||
|
||
// 禁用浏览器的 scroll restoration(刷新时不恢复旧 scrollTop),
|
||
// 由 ChatArea 自行控制定位
|
||
if ('scrollRestoration' in history) {
|
||
history.scrollRestoration = 'manual'
|
||
}
|
||
|
||
// 初始化主题系统(在 React 渲染前注入 CSS 变量,避免闪烁)
|
||
themeStore.init()
|
||
|
||
// 全局 overlay 滚动条 — 等 DOM 就绪后启动
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initOverlayScrollbars)
|
||
} else {
|
||
// DOM 已就绪(defer script 或者 module)
|
||
requestAnimationFrame(initOverlayScrollbars)
|
||
}
|
||
|
||
// 注册 server 切换 → 清理所有 server-specific 状态 + SSE 重连
|
||
serverStore.onServerChange(() => {
|
||
// 1. 清空内存中的 session/消息数据
|
||
messageStore.clearAll()
|
||
childSessionStore.clearAll()
|
||
todoStore.clearAll()
|
||
|
||
// 2. 重置路径模式缓存(不同服务器可能是不同操作系统)
|
||
resetPathModeCache()
|
||
|
||
// 4. 重新加载 auto-approve 开关状态(从新服务器 of storage key 读取)
|
||
autoApproveStore.reloadFromStorage()
|
||
|
||
// 5. 重连 SSE(会自动连到新服务器)
|
||
reconnectSSE()
|
||
})
|
||
|
||
const isNativeTauri = isTauri()
|
||
const isNativeTauriMobile = isNativeTauri && isTauriMobile()
|
||
|
||
type PiAgentConnection = { port: number; token: string }
|
||
type PiAgentServiceStart = {
|
||
started: boolean
|
||
connection?: PiAgentConnection | null
|
||
agent_dir?: string
|
||
session_dir?: string
|
||
}
|
||
|
||
function installPiAgentConnection(conn: PiAgentConnection) {
|
||
;(globalThis as any).window.PI_BOOT_DATA = {
|
||
baseUrl: `http://127.0.0.1:${conn.port}`,
|
||
token: conn.token,
|
||
}
|
||
}
|
||
|
||
// Tauri 原生 app 初始化
|
||
if (isNativeTauri) {
|
||
// 添加 CSS class 用于 safe-area 适配
|
||
document.documentElement.classList.add('tauri-app')
|
||
|
||
// 确保 viewport meta 包含 viewport-fit=cover(用于状态栏沉浸式)
|
||
const viewportMeta = document.querySelector('meta[name="viewport"]')
|
||
if (viewportMeta) {
|
||
const content = viewportMeta.getAttribute('content') || ''
|
||
if (!content.includes('viewport-fit=cover')) {
|
||
viewportMeta.setAttribute('content', content + ', viewport-fit=cover')
|
||
}
|
||
}
|
||
|
||
// The isolated Pi backend is discovered/started inside bootstrap() before React renders.
|
||
}
|
||
|
||
// 全局错误处理 - 防止未捕获错误导致页面刷新
|
||
window.addEventListener('error', event => {
|
||
globalErrorHandler('uncaught error', event.error)
|
||
event.preventDefault()
|
||
})
|
||
|
||
window.addEventListener('unhandledrejection', event => {
|
||
globalErrorHandler('unhandled promise rejection', event.reason)
|
||
event.preventDefault()
|
||
})
|
||
|
||
async function bootstrap() {
|
||
if (isTauri()) {
|
||
try {
|
||
const { invoke } = await import('@tauri-apps/api/core')
|
||
let conn = await invoke<PiAgentConnection | null>('get_pi_agent_connection')
|
||
const shouldAutoStartPi = serviceStore.autoStart || navigator.webdriver === true
|
||
if (!conn && !isNativeTauriMobile && shouldAutoStartPi) {
|
||
serviceStore.setStarting(true)
|
||
const result = await invoke<PiAgentServiceStart>('start_pi_service', {
|
||
url: serverStore.getActiveBaseUrl() || 'http://127.0.0.1:4096',
|
||
binaryPath: serviceStore.effectiveBinaryPath,
|
||
envVars: serviceStore.envVarsRecord,
|
||
})
|
||
serviceStore.setStartedByUs(result.started)
|
||
serviceStore.setRunning(Boolean(result.connection))
|
||
conn = result.connection ?? null
|
||
}
|
||
|
||
if (conn) {
|
||
console.log('[PiAgentUi] Discovered isolated Pi Agent connection info:', conn)
|
||
installPiAgentConnection(conn)
|
||
} else {
|
||
console.log('[PiAgentUi] No isolated Pi Agent backend found. App will run in standalone mode.')
|
||
}
|
||
} catch (err) {
|
||
console.error('[PiAgentUi] Failed to initialize Pi Agent backend:', err)
|
||
serviceStore.setRunning(false)
|
||
} finally {
|
||
serviceStore.setStarting(false)
|
||
}
|
||
}
|
||
|
||
createRoot(document.getElementById('root')!).render(
|
||
createElement(
|
||
ErrorBoundary,
|
||
null,
|
||
createElement(
|
||
DirectoryProvider,
|
||
null,
|
||
createElement(SessionProvider, null, createElement(App)),
|
||
),
|
||
),
|
||
)
|
||
}
|
||
|
||
void bootstrap()
|