Initial commit

This commit is contained in:
2026-03-08 01:34:54 +08:00
commit 1f104f73c8
441 changed files with 64911 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
import type { Router } from 'express'
import type { ServiceContainer } from '../../infra/container.js'
import { createApiModule } from '../../infra/createModule.js'
import { REMOTE_MODULE } from '../../../shared/modules/remote/index.js'
import { RemoteService } from './service.js'
import { createRemoteRoutes } from './routes.js'
export * from './service.js'
export * from './routes.js'
export const createRemoteModule = () => {
return createApiModule(REMOTE_MODULE, {
routes: (container: ServiceContainer): Router => {
const remoteService = container.getSync<RemoteService>('remoteService')
return createRemoteRoutes({ remoteService })
},
lifecycle: {
onLoad: (container: ServiceContainer): void => {
container.register('remoteService', () => new RemoteService())
},
},
})
}
export default createRemoteModule

View File

@@ -0,0 +1,80 @@
import express, { type Request, type Response, type Router } from 'express'
import { asyncHandler } from '../../utils/asyncHandler.js'
import { successResponse } from '../../utils/response.js'
import { RemoteService, type DeviceData } from './service.js'
export interface RemoteRoutesDependencies {
remoteService: RemoteService
}
export const createRemoteRoutes = (deps: RemoteRoutesDependencies): Router => {
const router = express.Router()
const { remoteService } = deps
router.get(
'/config',
asyncHandler(async (req: Request, res: Response) => {
const config = await remoteService.getConfig()
successResponse(res, config)
}),
)
router.post(
'/config',
asyncHandler(async (req: Request, res: Response) => {
const config = req.body
await remoteService.saveConfig(config)
successResponse(res, null)
}),
)
router.get(
'/screenshot',
asyncHandler(async (req: Request, res: Response) => {
const deviceName = req.query.device as string | undefined
const buffer = await remoteService.getScreenshot(deviceName)
if (!buffer) {
return successResponse(res, '')
}
const base64 = `data:image/png;base64,${buffer.toString('base64')}`
successResponse(res, base64)
}),
)
router.post(
'/screenshot',
asyncHandler(async (req: Request, res: Response) => {
const { dataUrl, deviceName } = req.body
console.log('[Remote] saveScreenshot called:', { deviceName, hasDataUrl: !!dataUrl })
await remoteService.saveScreenshot(dataUrl, deviceName)
successResponse(res, null)
}),
)
router.get(
'/data',
asyncHandler(async (req: Request, res: Response) => {
const deviceName = req.query.device as string | undefined
const data = await remoteService.getData(deviceName)
successResponse(res, data)
}),
)
router.post(
'/data',
asyncHandler(async (req: Request, res: Response) => {
const { deviceName, lastConnected } = req.body
const data: DeviceData = {}
if (lastConnected !== undefined) {
data.lastConnected = lastConnected
}
await remoteService.saveData(data, deviceName)
successResponse(res, null)
}),
)
return router
}
const remoteService = new RemoteService()
export default createRemoteRoutes({ remoteService })

View File

@@ -0,0 +1,178 @@
import fs from 'fs/promises'
import path from 'path'
import { resolveNotebookPath } from '../../utils/pathSafety.js'
import type { RemoteConfig, RemoteDevice } from '../../../shared/modules/remote/types.js'
export interface RemoteServiceDependencies { }
const REMOTE_DIR = 'remote'
export interface DeviceData {
lastConnected?: string
}
export class RemoteService {
constructor(private deps: RemoteServiceDependencies = {}) { }
private getRemoteDir(): { relPath: string; fullPath: string } {
const { fullPath } = resolveNotebookPath(REMOTE_DIR)
return { relPath: REMOTE_DIR, fullPath }
}
private getDeviceDir(deviceName: string): { relPath: string; fullPath: string } {
const safeName = this.sanitizeFileName(deviceName)
const { fullPath } = resolveNotebookPath(path.join(REMOTE_DIR, safeName))
return { relPath: path.join(REMOTE_DIR, safeName), fullPath }
}
private sanitizeFileName(name: string): string {
return name.replace(/[<>:"/\\|?*]/g, '_').trim() || 'unnamed'
}
private getDeviceConfigPath(deviceName: string): string {
const { fullPath } = this.getDeviceDir(deviceName)
return path.join(fullPath, 'config.json')
}
private getDeviceScreenshotPath(deviceName: string): string {
const { fullPath } = this.getDeviceDir(deviceName)
return path.join(fullPath, 'screenshot.png')
}
private getDeviceDataPath(deviceName: string): string {
const { fullPath } = this.getDeviceDir(deviceName)
return path.join(fullPath, 'data.json')
}
private async ensureDir(dirPath: string): Promise<void> {
await fs.mkdir(dirPath, { recursive: true })
}
private async getDeviceNames(): Promise<string[]> {
const { fullPath } = this.getRemoteDir()
try {
const entries = await fs.readdir(fullPath, { withFileTypes: true })
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
return dirs
} catch {
return []
}
}
async getConfig(): Promise<RemoteConfig> {
const deviceNames = await this.getDeviceNames()
const devices: RemoteDevice[] = await Promise.all(
deviceNames.map(async (name) => {
try {
const configPath = this.getDeviceConfigPath(name)
const content = await fs.readFile(configPath, 'utf-8')
const deviceConfig = JSON.parse(content)
return {
id: deviceConfig.id || name,
deviceName: name,
serverHost: deviceConfig.serverHost || '',
desktopPort: deviceConfig.desktopPort || 3000,
gitPort: deviceConfig.gitPort || 3001,
}
} catch {
return {
id: name,
deviceName: name,
serverHost: '',
desktopPort: 3000,
gitPort: 3001,
}
}
})
)
return { devices }
}
async saveConfig(config: RemoteConfig): Promise<void> {
const { fullPath: remoteDirFullPath } = this.getRemoteDir()
await this.ensureDir(remoteDirFullPath)
const existingDevices = await this.getDeviceNames()
const newDeviceNames = config.devices.map(d => this.sanitizeFileName(d.deviceName))
for (const oldDevice of existingDevices) {
if (!newDeviceNames.includes(oldDevice)) {
try {
const oldDir = path.join(remoteDirFullPath, oldDevice)
await fs.rm(oldDir, { recursive: true, force: true })
} catch { }
}
}
for (const device of config.devices) {
const deviceDir = this.getDeviceDir(device.deviceName)
await this.ensureDir(deviceDir.fullPath)
const deviceConfigPath = this.getDeviceConfigPath(device.deviceName)
const deviceConfig = {
id: device.id,
serverHost: device.serverHost,
desktopPort: device.desktopPort,
gitPort: device.gitPort,
}
await fs.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), 'utf-8')
}
}
async getScreenshot(deviceName?: string): Promise<Buffer | null> {
if (!deviceName) {
return null
}
const screenshotPath = this.getDeviceScreenshotPath(deviceName)
try {
return await fs.readFile(screenshotPath)
} catch {
return null
}
}
async saveScreenshot(dataUrl: string, deviceName?: string): Promise<void> {
console.log('[RemoteService] saveScreenshot:', { deviceName, dataUrlLength: dataUrl?.length })
if (!deviceName || deviceName.trim() === '') {
console.warn('[RemoteService] saveScreenshot skipped: no deviceName')
return
}
const deviceDir = this.getDeviceDir(deviceName)
await this.ensureDir(deviceDir.fullPath)
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, '')
const buffer = Buffer.from(base64Data, 'base64')
const screenshotPath = this.getDeviceScreenshotPath(deviceName)
await fs.writeFile(screenshotPath, buffer)
}
async getData(deviceName?: string): Promise<DeviceData | null> {
if (!deviceName || deviceName.trim() === '') {
return null
}
const dataPath = this.getDeviceDataPath(deviceName)
try {
const content = await fs.readFile(dataPath, 'utf-8')
return JSON.parse(content)
} catch {
return null
}
}
async saveData(data: DeviceData, deviceName?: string): Promise<void> {
if (!deviceName || deviceName.trim() === '') {
console.warn('[RemoteService] saveData skipped: no deviceName')
return
}
const deviceDir = this.getDeviceDir(deviceName)
await this.ensureDir(deviceDir.fullPath)
const dataPath = this.getDeviceDataPath(deviceName)
await fs.writeFile(dataPath, JSON.stringify(data, null, 2), 'utf-8')
}
}
export const createRemoteService = (deps?: RemoteServiceDependencies): RemoteService => {
return new RemoteService(deps)
}