- Add HomeAI dashboard: service status monitor, character profile manager, character editor - Add TTS voice preview in character editor (Kokoro via OpenClaw bridge → Wyoming) - Custom preview text, loading/playing states, stop control, speed via playbackRate - Fix Wyoming API breaking changes: remove `version` from TtsVoice/TtsProgram, use SynthesizeVoice object instead of bare string in Synthesize calls - Vite dev server proxies /api/tts and /api/health to avoid CORS issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
103 lines
3.8 KiB
JavaScript
103 lines
3.8 KiB
JavaScript
import { defineConfig } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
|
|
function healthCheckPlugin() {
|
|
return {
|
|
name: 'health-check-proxy',
|
|
configureServer(server) {
|
|
server.middlewares.use('/api/health', async (req, res) => {
|
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
const url = params.get('url');
|
|
const mode = params.get('mode'); // 'tcp' for raw TCP port check
|
|
if (!url) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Missing url param' }));
|
|
return;
|
|
}
|
|
const start = Date.now();
|
|
const parsedUrl = new URL(url);
|
|
|
|
try {
|
|
if (mode === 'tcp') {
|
|
// TCP socket connect check for non-HTTP services (e.g. Wyoming)
|
|
const { default: net } = await import('net');
|
|
await new Promise((resolve, reject) => {
|
|
const socket = net.createConnection(
|
|
{ host: parsedUrl.hostname, port: parseInt(parsedUrl.port), timeout: 5000 },
|
|
() => { socket.destroy(); resolve(); }
|
|
);
|
|
socket.on('error', reject);
|
|
socket.on('timeout', () => { socket.destroy(); reject(new Error('timeout')); });
|
|
});
|
|
} else {
|
|
// HTTP/HTTPS health check
|
|
const { default: https } = await import('https');
|
|
const { default: http } = await import('http');
|
|
const client = parsedUrl.protocol === 'https:' ? https : http;
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const reqObj = client.get(url, { rejectUnauthorized: false, timeout: 5000 }, (resp) => {
|
|
resp.resume();
|
|
resolve();
|
|
});
|
|
reqObj.on('error', reject);
|
|
reqObj.on('timeout', () => { reqObj.destroy(); reject(new Error('timeout')); });
|
|
});
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ status: 'online', responseTime: Date.now() - start }));
|
|
} catch {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ status: 'offline', responseTime: null }));
|
|
}
|
|
});
|
|
// TTS preview proxy — forwards POST to OpenClaw bridge, returns audio
|
|
server.middlewares.use('/api/tts', async (req, res) => {
|
|
if (req.method !== 'POST') {
|
|
res.writeHead(405);
|
|
res.end();
|
|
return;
|
|
}
|
|
try {
|
|
const { default: http } = await import('http');
|
|
const chunks = [];
|
|
for await (const chunk of req) chunks.push(chunk);
|
|
const body = Buffer.concat(chunks);
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const proxyReq = http.request(
|
|
'http://localhost:8081/api/tts',
|
|
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': body.length }, timeout: 30000 },
|
|
(proxyRes) => {
|
|
res.writeHead(proxyRes.statusCode, {
|
|
'Content-Type': proxyRes.headers['content-type'] || 'audio/wav',
|
|
});
|
|
proxyRes.pipe(res);
|
|
proxyRes.on('end', resolve);
|
|
}
|
|
);
|
|
proxyReq.on('error', reject);
|
|
proxyReq.on('timeout', () => { proxyReq.destroy(); reject(new Error('timeout')); });
|
|
proxyReq.write(body);
|
|
proxyReq.end();
|
|
});
|
|
} catch {
|
|
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'TTS bridge unreachable' }));
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// https://vite.dev/config/
|
|
export default defineConfig({
|
|
plugins: [
|
|
healthCheckPlugin(),
|
|
tailwindcss(),
|
|
react(),
|
|
],
|
|
})
|