feat: add global AI guide to LMS launcher
This commit is contained in:
Generated
+19
-2
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "autonomy-studio-launcher",
|
||||
"version": "1.0.14",
|
||||
"version": "1.0.27",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "autonomy-studio-launcher",
|
||||
"version": "1.0.14",
|
||||
"version": "1.0.27",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"lucide-react": "^1.14.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"tailwindcss": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -8307,6 +8308,22 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
|
||||
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty/node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.38",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"lucide-react": "^1.14.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"tailwindcss": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { get as httpGet } from 'http'
|
||||
import { get as httpsGet } from 'https'
|
||||
import { spawn, exec, execFile, ChildProcess } from 'child_process'
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import { LmsWebBridge } from './lms-web-bridge'
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'appdocs', privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true } }
|
||||
@@ -12,6 +13,7 @@ protocol.registerSchemesAsPrivileged([
|
||||
|
||||
let splash: BrowserWindow | null = null
|
||||
let pendingGoogleOAuthPayload: { loginToken?: string; error?: string } | null = null
|
||||
const lmsWebBridge = new LmsWebBridge()
|
||||
|
||||
type CommandResult = {
|
||||
ok: boolean
|
||||
@@ -954,6 +956,29 @@ function createWindow(): void {
|
||||
ipcMain.handle('get-launcher-cache-status', () => getLauncherCacheStatus())
|
||||
ipcMain.handle('clear-launcher-cache', () => clearLauncherCache())
|
||||
ipcMain.handle('start-google-login', (_, url: string) => shell.openExternal(url))
|
||||
ipcMain.handle('launch-web-app', async (_event, payload: { appName?: string; url?: string }) => {
|
||||
const rawUrl = payload.url?.trim()
|
||||
if (!rawUrl) throw new Error('웹 실행 주소가 없습니다.')
|
||||
|
||||
try {
|
||||
const url = new URL(rawUrl)
|
||||
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
||||
throw new Error('웹 실행 주소 형식이 올바르지 않습니다.')
|
||||
}
|
||||
if (payload.appName?.trim().toLowerCase() === 'lms') {
|
||||
const bridge = await lmsWebBridge.createSession(url.origin)
|
||||
const hash = new URLSearchParams(url.hash.replace(/^#/, ''))
|
||||
hash.set('bridgeUrl', bridge.url)
|
||||
hash.set('bridgeToken', bridge.token)
|
||||
url.hash = hash.toString()
|
||||
}
|
||||
await shell.openExternal(url.toString())
|
||||
mainWindow.minimize()
|
||||
return { ok: true }
|
||||
} catch (error) {
|
||||
throw new Error(error instanceof Error ? error.message : '웹 브라우저를 열지 못했습니다.')
|
||||
}
|
||||
})
|
||||
ipcMain.handle('launcher-tailscale-status', () => readTailscaleStatus())
|
||||
ipcMain.handle('launcher-tailscale-install', () => installTailscale())
|
||||
ipcMain.handle('launcher-ssh-key-ensure', () => ensureLauncherSshKey())
|
||||
@@ -1217,4 +1242,8 @@ if (!gotSingleInstanceLock) {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('will-quit', () => {
|
||||
void lmsWebBridge.stop()
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import Login from './pages/Login'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import AppWindowShell from './pages/AppWindowShell'
|
||||
import type { AuthSession } from './pages/LoginForm'
|
||||
import { refreshLauncherSession } from './auth/sessionRefresh'
|
||||
|
||||
const AUTH_SESSION_KEY = 'atnmy.auth.session'
|
||||
const LAUNCHER_API_BASE_URL = (import.meta.env.VITE_ATNMY_STDO_API_BASE_URL || 'http://localhost:4200').replace(/\/+$/, '')
|
||||
@@ -90,11 +91,7 @@ function App(): React.JSX.Element {
|
||||
|
||||
const refreshDelayMs = Math.max(30_000, ((authSession.accessTokenExpiresIn ?? 900) - 60) * 1000)
|
||||
const timer = window.setTimeout(() => {
|
||||
fetch(`${LAUNCHER_API_BASE_URL}/api/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
.then((response) => response.ok ? response.json() as Promise<AuthSession> : null)
|
||||
refreshLauncherSession()
|
||||
.then((session) => {
|
||||
if (session?.accessToken) {
|
||||
handleSessionUpdate(session)
|
||||
|
||||
@@ -437,7 +437,7 @@ body,
|
||||
padding: 9px 12px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #fca5a5;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'NanumGothic', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
@@ -446,6 +446,14 @@ body,
|
||||
}
|
||||
|
||||
.launch-action-dropdown button:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
.launch-action-dropdown .launch-action-danger {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.launch-action-dropdown .launch-action-danger:hover:not(:disabled) {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
@@ -719,6 +727,10 @@ body,
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.login-version-wrap .launcher-update-text {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.app-version-wrap {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AuthSession } from '../pages/LoginForm'
|
||||
|
||||
const launcherApiBaseUrl = (import.meta.env.VITE_ATNMY_STDO_API_BASE_URL || 'http://localhost:4200').replace(/\/+$/, '')
|
||||
|
||||
let pendingRefresh: Promise<AuthSession | null> | null = null
|
||||
|
||||
export function refreshLauncherSession(): Promise<AuthSession | null> {
|
||||
if (pendingRefresh) return pendingRefresh
|
||||
|
||||
pendingRefresh = fetch(`${launcherApiBaseUrl}/api/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
.then((response) => response.ok ? response.json() as Promise<AuthSession> : null)
|
||||
.catch(() => null)
|
||||
.finally(() => {
|
||||
pendingRefresh = null
|
||||
})
|
||||
|
||||
return pendingRefresh
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import Tooltip from '../components/Tooltip'
|
||||
import AppView from './AppView'
|
||||
import type { LauncherApp } from '../config/apps'
|
||||
import type { AuthSession } from './LoginForm'
|
||||
import { refreshLauncherSession } from '../auth/sessionRefresh'
|
||||
|
||||
const version = import.meta.env.VITE_APP_VERSION
|
||||
|
||||
@@ -17,6 +18,7 @@ interface DashboardProps {
|
||||
|
||||
const LAUNCHER_API_BASE_URL = (import.meta.env.VITE_ATNMY_STDO_API_BASE_URL || 'http://localhost:4200').replace(/\/+$/, '')
|
||||
const MANUAL_BASE_URL = (import.meta.env.VITE_MANUAL_BASE_URL || 'https://portal.autonomystudio.net/manual').replace(/\/+$/, '')
|
||||
const LMS_WEB_BASE_URL = `${(import.meta.env.VITE_LMS_WEB_BASE_URL || 'http://localhost:5177/lms-web/').replace(/\/+$/, '')}/`
|
||||
const NOTICE_READ_STORAGE_KEY = 'atnmy.notice.readSignatures'
|
||||
const LAST_LAUNCHED_APP_STORAGE_KEY = 'atnmy.launcher.lastLaunchedApp'
|
||||
const IS_DEV = import.meta.env.DEV
|
||||
@@ -186,14 +188,8 @@ function Dashboard({ authSession, onSessionUpdate, onLogout, onSessionExpired }:
|
||||
: null
|
||||
const activeAppDisplayName = appStatus?.displayName || appConfig?.displayName || activeApp
|
||||
const refreshSession = useCallback(async (): Promise<AuthSession | null> => {
|
||||
const response = await fetch(`${LAUNCHER_API_BASE_URL}/api/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) return null
|
||||
|
||||
const session = await response.json() as AuthSession
|
||||
const session = await refreshLauncherSession()
|
||||
if (!session) return null
|
||||
onSessionUpdate(session)
|
||||
return session
|
||||
}, [onSessionUpdate])
|
||||
@@ -722,13 +718,8 @@ function Dashboard({ authSession, onSessionUpdate, onLogout, onSessionExpired }:
|
||||
let response = await requestHandoff(authSession.accessToken)
|
||||
|
||||
if (response.status === 401) {
|
||||
const refreshResponse = await fetch(`${LAUNCHER_API_BASE_URL}/api/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (refreshResponse.ok) {
|
||||
const nextSession = await refreshResponse.json() as AuthSession
|
||||
const nextSession = await refreshLauncherSession()
|
||||
if (nextSession) {
|
||||
onSessionUpdate(nextSession)
|
||||
response = await requestHandoff(nextSession.accessToken)
|
||||
} else {
|
||||
@@ -786,6 +777,32 @@ function Dashboard({ authSession, onSessionUpdate, onLogout, onSessionExpired }:
|
||||
}
|
||||
}, [activeApp, appConfig, canLaunch, createHandoffToken, launchingApp])
|
||||
|
||||
const handleWebLaunch = useCallback(async () => {
|
||||
if (activeApp.trim().toLowerCase() !== 'lms' || launchingApp) return
|
||||
|
||||
setAppActionMenuOpen(false)
|
||||
setLaunchingApp(activeApp)
|
||||
try {
|
||||
const handoffToken = await createHandoffToken('LMS')
|
||||
if (!handoffToken) return
|
||||
|
||||
const launchUrl = new URL(LMS_WEB_BASE_URL)
|
||||
launchUrl.hash = new URLSearchParams({
|
||||
handoffToken,
|
||||
audienceApp: 'LMS',
|
||||
}).toString()
|
||||
await window.electron.ipcRenderer.invoke('launch-web-app', {
|
||||
appName: activeApp,
|
||||
url: launchUrl.toString(),
|
||||
})
|
||||
window.localStorage.setItem(LAST_LAUNCHED_APP_STORAGE_KEY, activeApp)
|
||||
} catch (error) {
|
||||
alert(`웹 실행에 실패했습니다. (${error instanceof Error ? error.message : '알 수 없는 오류'})`)
|
||||
} finally {
|
||||
setLaunchingApp(null)
|
||||
}
|
||||
}, [activeApp, createHandoffToken, launchingApp])
|
||||
|
||||
const handleFocusApp = useCallback(() => {
|
||||
window.electron.ipcRenderer.send('focus-app-window', activeApp)
|
||||
}, [activeApp])
|
||||
@@ -968,7 +985,17 @@ function Dashboard({ authSession, onSessionUpdate, onLogout, onSessionExpired }:
|
||||
</div>
|
||||
{appActionMenuOpen && (
|
||||
<div className="launch-action-dropdown">
|
||||
<button type="button" onClick={openDeleteConfirm} disabled={activeAppDeleting}>
|
||||
{activeApp.trim().toLowerCase() === 'lms' && (
|
||||
<button type="button" onClick={handleWebLaunch}>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<rect width="20" height="14" x="2" y="3" rx="2" />
|
||||
<path d="M8 21h8" />
|
||||
<path d="M12 17v4" />
|
||||
</svg>
|
||||
<span>웹 실행</span>
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="launch-action-danger" onClick={openDeleteConfirm} disabled={activeAppDeleting}>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M3 6h18" />
|
||||
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user