Documentation Index: README.md
PWA service layer for API communication, device discovery, storage, and hardware access.
The services layer provides abstraction over:
- MoonHub API: Communication with MoonHub devices
- Device Discovery: LAN scanning for MoonHub devices
- Storage: IndexedDB persistence
- Voice: Speech recognition and synthesis
All services follow a singleton pattern with getXxx() accessor functions.
API client for MoonHub device communication.
File: src/services/device.ts
import { MoonHubClient, createClient, getClient } from '@/services/device'
// Create new client
const client = createClient('http://192.168.1.100:18800', 'auth-token')
// Get current connected client
const currentClient = getClient()| Method | Endpoint | Description |
|---|---|---|
ping() |
GET /api/ping |
Device online check |
getDeviceStatus() |
GET /api/system/info |
System / device info |
discoverDevices() |
GET /api/discover |
mDNS discovery on the device’s LAN (LAN-only on server; caller must reach device IP) |
getPairedDevices() |
GET /api/devices |
List paired clients stored on the device |
pair(authCode) |
POST /api/auth/pair |
Pair with auth code ({ "code": "..." }) |
verifyToken() |
POST /api/auth/verify |
Validate current token (Authorization: Bearer set by client) |
chat(request) |
POST /api/chat |
Synchronous chat |
chatStream(request) |
POST /api/chat/stream |
Streaming chat (SSE-style lines) |
generateSpace(request) |
POST /api/space/generate |
Generate Space layout |
getSpace(spaceId) |
GET /api/space/:id |
Fetch Space by id |
getGatewayStatus() |
GET /api/gateway/status |
Gateway status |
startGateway() / stopGateway() |
POST /api/gateway/start · POST /api/gateway/stop |
Control gateway |
subscribeToGatewayEvents(onEvent, onError?) |
GET /api/gateway/events (SSE via EventSource) |
Live gateway events; returns unsubscribe |
getConfig() |
GET /api/config |
Get device config |
updateConfig(config) |
PATCH /api/config |
Partial config update |
getChannels() |
GET /api/channels |
List configured channel instances |
createChannel(data) |
POST /api/channels |
Add channel instance |
updateChannel(id, data) |
PATCH /api/channels/{id} |
Update channel config |
deleteChannel(id) |
DELETE /api/channels/{id} |
Remove channel instance |
getChannelStatus(id) |
GET /api/channels/{id}/status |
Runtime status for one channel |
getChannelCatalog() |
GET /api/channels/catalog |
Channel type catalog |
listDynamicTools(source?) |
GET /api/dynamic-tools |
List AI / dynamic tools (?source=ai) |
generateDynamicTool(prompt, context?) |
POST /api/dynamic-tools/generate |
Create or dedupe tool from prompt (context: chat / space) |
executeDynamicTool(id, params, mode?) |
POST /api/dynamic-tools/{id}/execute |
Run tool; mode chat or space |
getDynamicToolSchema(id, mode?) |
GET /api/dynamic-tools/{id}/schema |
Fetch schema for rendering |
deleteDynamicTool(id) |
DELETE /api/dynamic-tools/{id} |
Remove tool |
setDynamicToolOnHome(id, onHome) |
PATCH /api/dynamic-tools/{id}/home |
Pin on Space home (on_home in body) |
getSkills() |
GET /api/skills |
List skills |
installSkill(skillUrl) |
POST /api/skills |
Install skill from URL |
getModels() |
GET /api/models |
List models |
setDefaultModel(modelName) |
POST /api/models/default |
Set default model |
updateModel / addModel / deleteModel |
/api/models/... |
Extended model CRUD (see device.ts) |
// Ping device
const result = await client.ping()
if (result.success) {
console.log('Device is online')
}
// Get device status
const status = await client.getDeviceStatus()
console.log('Device:', status.data.name)
console.log('Version:', status.data.version)
// Pair with auth code
const pairResult = await client.pair('XM8888')
if (pairResult.success) {
console.log('Token:', pairResult.data.token)
}
// Synchronous chat
const chatResult = await client.chat({ message: 'Hello' })
console.log('Response:', chatResult.data.response)
// Streaming chat
for await (const chunk of client.chatStream({ message: 'Hello' })) {
console.log('Chunk:', chunk.content)
}High-level wrapper over MoonHubClient dynamic-tool methods. Used by Space home, Space detail, and Space add flows.
File: src/services/dynamicTools.ts
import { dynamicToolsService } from '@/services/dynamicTools'
const list = await dynamicToolsService.list('ai')
const gen = await dynamicToolsService.generate('Weather for my city', 'space')
const exec = await dynamicToolsService.execute(toolId, {}, 'space')| Method | Maps to |
|---|---|
list(source?) |
listDynamicTools |
generate(prompt, context?) |
generateDynamicTool |
execute(id, params?, mode?) |
executeDynamicTool |
getSchema(id, mode?) |
getDynamicToolSchema |
delete(id) |
deleteDynamicTool |
setOnHome(id, onHome) |
setDynamicToolOnHome |
Backend reference: MoonHub web/backend/api/README.md (/api/dynamic-tools).
LAN device discovery service: parallel HTTP scan of the inferred subnet and a small set of ports (including 18800, the MoonHub web default). This avoids relying on browser mDNS. When you already have a MoonHubClient pointed at a device, you can alternatively call discoverDevices() to use the server’s mDNS implementation (GET /api/discover).
File: src/services/discovery.ts
import { DeviceDiscovery, getDiscovery } from '@/services/discovery'
const discovery = getDiscovery()| Method | Description |
|---|---|
scan(options) |
Scan for devices |
abort() |
Abort current scan |
isScanning() |
Check scan status |
| Option | Type | Default | Description |
|---|---|---|---|
timeout |
number | 5000 | Scan timeout (ms) |
onDeviceFound |
function | - | Callback when device found |
onProgress |
function | - | Progress callback |
// Basic scan
const devices = await discovery.scan({ timeout: 5000 })
for (const device of devices) {
console.log(`Found: ${device.name} at ${device.ip}`)
}
// Scan with callbacks
const devices = await discovery.scan({
timeout: 10000,
onDeviceFound: (device) => {
console.log(`Found device: ${device.name}`)
},
onProgress: (progress) => {
console.log(`Progress: ${progress.scanned}/${progress.total}`)
},
})
// Abort scan
discovery.abort()- Get local IP via WebRTC
- Calculate subnet IP range
- Scan common ports in parallel
- Call
/api/pingon each address - Retrieve device details on success
IndexedDB persistence service.
File: src/services/storage.ts
import { StorageService, getStorage } from '@/services/storage'
const storage = getStorage()| Method | Description |
|---|---|
init() |
Initialize database |
saveDevice(device) |
Save paired device |
getDevice(id) |
Get device by ID |
getAllDevices() |
Get all devices |
deleteDevice(id) |
Delete device |
saveConversation(conv) |
Save conversation |
getConversationsByDevice(id) |
Get device conversations |
deleteConversation(id) |
Delete conversation |
saveMessage(message) |
Save message |
getMessagesByConversation(id) |
Get conversation messages |
setSetting(key, value) |
Save setting |
getSetting(key) |
Get setting |
clear() |
Clear all data |
| Store | Key | Indices | Description |
|---|---|---|---|
devices |
id |
- | Paired devices |
conversations |
id |
deviceId |
Conversations |
messages |
id |
conversationId |
Messages |
spaces |
id |
- | Space data |
settings |
key |
- | App settings |
// Initialize (auto-called on import)
await storage.init()
// Device operations
await storage.saveDevice({
id: 'device-1',
name: 'Living Room',
ip: '192.168.1.100',
token: 'xxx',
})
const device = await storage.getDevice('device-1')
const allDevices = await storage.getAllDevices()
await storage.deleteDevice('device-1')
// Conversation operations
await storage.saveConversation({
id: 'conv-1',
deviceId: 'device-1',
title: 'First chat',
})
const convs = await storage.getConversationsByDevice('device-1')
// Settings
await storage.setSetting('theme', 'dark')
const theme = await storage.getSetting<string>('theme')Speech recognition and synthesis service.
File: src/services/voice.ts
import { VoiceService, getVoice } from '@/services/voice'
const voice = getVoice()| Method | Description |
|---|---|
isRecognitionSupported() / isSynthesisSupported() |
Browser capability checks |
startRecognition(options) / stopRecognition() |
Web Speech recognition with callbacks |
speak(text, options?) / stopSpeaking() |
TTS |
pauseSpeaking() / resumeSpeaking() / isSpeaking() |
Playback control |
getVoices() |
Available synthesis voices |
startAudioRecording() / recordAudio(maxDuration?) |
MediaRecorder helpers |
blobToBase64(blob) |
Encode audio for upload |
if (!voice.isRecognitionSupported()) {
console.log('Recognition not supported')
}
voice.startRecognition({
language: 'zh-CN',
interimResults: true,
onResult: (text, isFinal) => console.log(text, isFinal),
onError: (msg) => console.error(msg),
})
// … later …
voice.stopRecognition()
voice.speak('Hello', { onEnd: () => console.log('done') })Stub service for the Space “add component” flow: returns mock recommendations until a backend API exists.
File: src/services/componentRecommendation.ts
import { componentRecommendation } from '@/services/componentRecommendation'
const { components } = await componentRecommendation.getRecommendations('optional context')
await componentRecommendation.addComponent(spaceId, 'weather')| Member | Description |
|---|---|
getRecommendations(context?) |
Returns RecommendedComponent[] (mock) |
addComponent(spaceId, type) |
Placeholder success |
removeComponent(spaceId, componentId) |
Placeholder success |
updateComponentConfig(spaceId, componentId, config) |
Placeholder success |
Development / demo helpers. Import from @/services/mock when used in the app.
All API methods return a unified response format:
interface ApiResponse<T> {
success: boolean
data?: T
error?: {
code: string
message: string
details?: Record<string, unknown>
}
}const result = await client.pair('XM8888')
if (!result.success) {
console.error('Error:', result.error?.code)
console.error('Message:', result.error?.message)
return
}
// Success
console.log('Token:', result.data?.token)All services use singleton pattern:
const client = getClient()
const discovery = getDiscovery()
const storage = getStorage()
const voice = getVoice()
// componentRecommendation is a plain object export (not a getXxx singleton)