feat: memory v2, prompt styles, Dream/GAZE integration, Wyoming TTS fix
SQLite + sqlite-vec replaces JSON memory files with semantic search, follow-up injection, privacy levels, and lifecycle management. Six prompt styles (quick/standard/creative/roleplayer/game-master/storyteller) with per-style Claude model tiering (Haiku/Sonnet/Opus), temperature control, and section stripping. Characters can set default style and per-style overrides. Dream character import and GAZE character linking in the dashboard editor with auto-populated fields, cover image resolution, and preset assignment. Bridge: session isolation (conversation_id / 12h satellite buckets), model routing refactor, PUT/DELETE support, memory REST endpoints. Dashboard: mobile-responsive sidebar, retry button, style picker in chat, follow-up banner, memory lifecycle/privacy UI, cloud model options in editor. Wyoming TTS: upgraded to v1.8.0 for HA 1.7.2 compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,13 @@ const SATELLITE_MAP_PATH = '/Users/aodhan/homeai-data/satellite-map.json'
|
||||
const CONVERSATIONS_DIR = '/Users/aodhan/homeai-data/conversations'
|
||||
const MEMORIES_DIR = '/Users/aodhan/homeai-data/memories'
|
||||
const MODE_PATH = '/Users/aodhan/homeai-data/active-mode.json'
|
||||
const ACTIVE_STYLE_PATH = '/Users/aodhan/homeai-data/active-prompt-style.json'
|
||||
const PROMPT_STYLES_DIR = new URL('./homeai-agent/prompt-styles', import.meta.url).pathname
|
||||
const HA_TOKEN = process.env.HA_TOKEN || ''
|
||||
const GAZE_HOST = 'http://10.0.0.101:5782'
|
||||
const GAZE_API_KEY = process.env.GAZE_API_KEY || ''
|
||||
const DREAM_HOST = process.env.DREAM_HOST || 'http://10.0.0.101:3000'
|
||||
const DREAM_API_KEY = process.env.DREAM_API_KEY || ''
|
||||
|
||||
function characterStoragePlugin() {
|
||||
return {
|
||||
@@ -272,6 +277,7 @@ function healthCheckPlugin() {
|
||||
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
|
||||
const needsAuth = params.get('auth') === '1'; // use server-side HA_TOKEN
|
||||
if (!url) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Missing url param' }));
|
||||
@@ -298,8 +304,12 @@ function healthCheckPlugin() {
|
||||
const { default: http } = await import('http');
|
||||
const client = parsedUrl.protocol === 'https:' ? https : http;
|
||||
|
||||
const opts = { rejectUnauthorized: false, timeout: 5000 };
|
||||
if (needsAuth && HA_TOKEN) {
|
||||
opts.headers = { 'Authorization': `Bearer ${HA_TOKEN}` };
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
const reqObj = client.get(url, { rejectUnauthorized: false, timeout: 5000 }, (resp) => {
|
||||
const reqObj = client.get(url, opts, (resp) => {
|
||||
resp.resume();
|
||||
resolve();
|
||||
});
|
||||
@@ -387,15 +397,16 @@ function gazeProxyPlugin() {
|
||||
return {
|
||||
name: 'gaze-proxy',
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/api/gaze/presets', async (req, res) => {
|
||||
// Helper to proxy a JSON GET to GAZE
|
||||
const proxyGazeJson = async (apiPath, res, fallback) => {
|
||||
if (!GAZE_API_KEY) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ presets: [] }))
|
||||
res.end(JSON.stringify(fallback))
|
||||
return
|
||||
}
|
||||
try {
|
||||
const http = await import('http')
|
||||
const url = new URL(`${GAZE_HOST}/api/v1/presets`)
|
||||
const url = new URL(`${GAZE_HOST}${apiPath}`)
|
||||
const proxyRes = await new Promise((resolve, reject) => {
|
||||
const r = http.default.get(url, { headers: { 'X-API-Key': GAZE_API_KEY }, timeout: 5000 }, resolve)
|
||||
r.on('error', reject)
|
||||
@@ -407,8 +418,110 @@ function gazeProxyPlugin() {
|
||||
res.end(Buffer.concat(chunks))
|
||||
} catch {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ presets: [] }))
|
||||
res.end(JSON.stringify(fallback))
|
||||
}
|
||||
}
|
||||
|
||||
server.middlewares.use('/api/gaze/presets', async (req, res) => {
|
||||
await proxyGazeJson('/api/v1/presets', res, { presets: [] })
|
||||
})
|
||||
|
||||
server.middlewares.use('/api/gaze/characters', async (req, res) => {
|
||||
await proxyGazeJson('/api/v1/characters', res, { characters: [] })
|
||||
})
|
||||
|
||||
// Proxy cover image for a GAZE character (binary passthrough)
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
const match = req.url.match(/^\/api\/gaze\/character\/([a-zA-Z0-9_\-]+)\/cover/)
|
||||
if (!match) return next()
|
||||
const characterId = match[1]
|
||||
if (!GAZE_API_KEY) {
|
||||
res.writeHead(404)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { default: http } = await import('http')
|
||||
const url = new URL(`${GAZE_HOST}/api/v1/character/${characterId}/cover`)
|
||||
const r = http.get(url, { headers: { 'X-API-Key': GAZE_API_KEY }, timeout: 5000 }, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
'Content-Type': proxyRes.headers['content-type'] || 'image/png',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
})
|
||||
proxyRes.pipe(res)
|
||||
})
|
||||
r.on('error', () => { if (!res.headersSent) { res.writeHead(502); res.end() } })
|
||||
r.on('timeout', () => { r.destroy(); if (!res.headersSent) { res.writeHead(504); res.end() } })
|
||||
} catch {
|
||||
if (!res.headersSent) { res.writeHead(500); res.end() }
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function dreamProxyPlugin() {
|
||||
const dreamHeaders = DREAM_API_KEY ? { 'X-API-Key': DREAM_API_KEY } : {}
|
||||
return {
|
||||
name: 'dream-proxy',
|
||||
configureServer(server) {
|
||||
// Helper: proxy a JSON GET to Dream
|
||||
const proxyDreamJson = async (apiPath, res) => {
|
||||
try {
|
||||
const http = await import('http')
|
||||
const url = new URL(`${DREAM_HOST}${apiPath}`)
|
||||
const proxyRes = await new Promise((resolve, reject) => {
|
||||
const r = http.default.get(url, { headers: dreamHeaders, timeout: 5000 }, resolve)
|
||||
r.on('error', reject)
|
||||
r.on('timeout', () => { r.destroy(); reject(new Error('timeout')) })
|
||||
})
|
||||
const chunks = []
|
||||
for await (const chunk of proxyRes) chunks.push(chunk)
|
||||
res.writeHead(proxyRes.statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(Buffer.concat(chunks))
|
||||
} catch {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ characters: [], error: 'Dream unreachable' }))
|
||||
}
|
||||
}
|
||||
|
||||
// List characters (compact)
|
||||
server.middlewares.use('/api/dream/characters', async (req, res, next) => {
|
||||
// Only handle exact path (not sub-paths like /api/dream/characters/abc/image)
|
||||
if (req.url !== '/' && req.url !== '' && !req.url.startsWith('?')) return next()
|
||||
const qs = req.url === '/' || req.url === '' ? '' : req.url
|
||||
await proxyDreamJson(`/api/characters${qs}`, res)
|
||||
})
|
||||
|
||||
// Character image (binary passthrough)
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
const match = req.url.match(/^\/api\/dream\/characters\/([^/]+)\/image/)
|
||||
if (!match) return next()
|
||||
const charId = match[1]
|
||||
try {
|
||||
const { default: http } = await import('http')
|
||||
const url = new URL(`${DREAM_HOST}/api/characters/${charId}/image`)
|
||||
const r = http.get(url, { headers: dreamHeaders, timeout: 5000 }, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
'Content-Type': proxyRes.headers['content-type'] || 'image/png',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
})
|
||||
proxyRes.pipe(res)
|
||||
})
|
||||
r.on('error', () => { if (!res.headersSent) { res.writeHead(502); res.end() } })
|
||||
r.on('timeout', () => { r.destroy(); if (!res.headersSent) { res.writeHead(504); res.end() } })
|
||||
} catch {
|
||||
if (!res.headersSent) { res.writeHead(500); res.end() }
|
||||
}
|
||||
})
|
||||
|
||||
// Get single character (full details)
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
const match = req.url.match(/^\/api\/dream\/characters\/([^/]+)\/?$/)
|
||||
if (!match) return next()
|
||||
await proxyDreamJson(`/api/characters/${match[1]}`, res)
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -418,163 +531,67 @@ function memoryStoragePlugin() {
|
||||
return {
|
||||
name: 'memory-storage',
|
||||
configureServer(server) {
|
||||
const ensureDirs = async () => {
|
||||
const { mkdir } = await import('fs/promises')
|
||||
await mkdir(`${MEMORIES_DIR}/personal`, { recursive: true })
|
||||
}
|
||||
// Proxy all /api/memories/* requests to the OpenClaw bridge (port 8081)
|
||||
// The bridge handles SQLite + vector search; dashboard is just a passthrough
|
||||
const proxyMemoryRequest = async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
})
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const readJsonFile = async (path, fallback) => {
|
||||
const { readFile } = await import('fs/promises')
|
||||
try {
|
||||
return JSON.parse(await readFile(path, 'utf-8'))
|
||||
} catch {
|
||||
return fallback
|
||||
const { default: http } = await import('http')
|
||||
const chunks = []
|
||||
for await (const chunk of req) chunks.push(chunk)
|
||||
const body = Buffer.concat(chunks)
|
||||
|
||||
// Reconstruct full path: /api/memories/... (req.url has the part after /api/memories)
|
||||
const targetPath = `/api/memories${req.url}`
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const proxyReq = http.request(
|
||||
`http://localhost:8081${targetPath}`,
|
||||
{
|
||||
method: req.method,
|
||||
headers: {
|
||||
'Content-Type': req.headers['content-type'] || 'application/json',
|
||||
...(body.length > 0 ? { 'Content-Length': body.length } : {}),
|
||||
},
|
||||
timeout: 30000,
|
||||
},
|
||||
(proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
'Content-Type': proxyRes.headers['content-type'] || 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
})
|
||||
proxyRes.pipe(res)
|
||||
proxyRes.on('end', resolve)
|
||||
proxyRes.on('error', resolve)
|
||||
}
|
||||
)
|
||||
proxyReq.on('error', reject)
|
||||
proxyReq.on('timeout', () => {
|
||||
proxyReq.destroy()
|
||||
reject(new Error('timeout'))
|
||||
})
|
||||
if (body.length > 0) proxyReq.write(body)
|
||||
proxyReq.end()
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`[memory-proxy] failed:`, err?.message || err)
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(502, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: `Bridge unreachable: ${err?.message || 'unknown'}` }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const writeJsonFile = async (path, data) => {
|
||||
const { writeFile } = await import('fs/promises')
|
||||
await writeFile(path, JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
// Personal memories: /api/memories/personal/:characterId[/:memoryId]
|
||||
server.middlewares.use('/api/memories/personal', async (req, res, next) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST,DELETE', 'Access-Control-Allow-Headers': 'Content-Type' })
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
await ensureDirs()
|
||||
const url = new URL(req.url, 'http://localhost')
|
||||
const parts = url.pathname.replace(/^\/+/, '').split('/')
|
||||
const characterId = parts[0] ? parts[0].replace(/[^a-zA-Z0-9_\-\.]/g, '_') : null
|
||||
const memoryId = parts[1] || null
|
||||
|
||||
if (!characterId) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: 'Missing character ID' }))
|
||||
return
|
||||
}
|
||||
|
||||
const filePath = `${MEMORIES_DIR}/personal/${characterId}.json`
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const data = await readJsonFile(filePath, { characterId, memories: [] })
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify(data))
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
const chunks = []
|
||||
for await (const chunk of req) chunks.push(chunk)
|
||||
const memory = JSON.parse(Buffer.concat(chunks).toString())
|
||||
const data = await readJsonFile(filePath, { characterId, memories: [] })
|
||||
if (memory.id) {
|
||||
const idx = data.memories.findIndex(m => m.id === memory.id)
|
||||
if (idx >= 0) {
|
||||
data.memories[idx] = { ...data.memories[idx], ...memory }
|
||||
} else {
|
||||
data.memories.push(memory)
|
||||
}
|
||||
} else {
|
||||
memory.id = 'm_' + Date.now()
|
||||
memory.createdAt = memory.createdAt || new Date().toISOString()
|
||||
data.memories.push(memory)
|
||||
}
|
||||
await writeJsonFile(filePath, data)
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ ok: true, memory }))
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: err.message }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE' && memoryId) {
|
||||
try {
|
||||
const data = await readJsonFile(filePath, { characterId, memories: [] })
|
||||
data.memories = data.memories.filter(m => m.id !== memoryId)
|
||||
await writeJsonFile(filePath, data)
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ ok: true }))
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: err.message }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// General memories: /api/memories/general[/:memoryId]
|
||||
server.middlewares.use('/api/memories/general', async (req, res, next) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST,DELETE', 'Access-Control-Allow-Headers': 'Content-Type' })
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
await ensureDirs()
|
||||
const url = new URL(req.url, 'http://localhost')
|
||||
const memoryId = url.pathname.replace(/^\/+/, '') || null
|
||||
const filePath = `${MEMORIES_DIR}/general.json`
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const data = await readJsonFile(filePath, { memories: [] })
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify(data))
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
const chunks = []
|
||||
for await (const chunk of req) chunks.push(chunk)
|
||||
const memory = JSON.parse(Buffer.concat(chunks).toString())
|
||||
const data = await readJsonFile(filePath, { memories: [] })
|
||||
if (memory.id) {
|
||||
const idx = data.memories.findIndex(m => m.id === memory.id)
|
||||
if (idx >= 0) {
|
||||
data.memories[idx] = { ...data.memories[idx], ...memory }
|
||||
} else {
|
||||
data.memories.push(memory)
|
||||
}
|
||||
} else {
|
||||
memory.id = 'm_' + Date.now()
|
||||
memory.createdAt = memory.createdAt || new Date().toISOString()
|
||||
data.memories.push(memory)
|
||||
}
|
||||
await writeJsonFile(filePath, data)
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ ok: true, memory }))
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: err.message }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE' && memoryId) {
|
||||
try {
|
||||
const data = await readJsonFile(filePath, { memories: [] })
|
||||
data.memories = data.memories.filter(m => m.id !== memoryId)
|
||||
await writeJsonFile(filePath, data)
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ ok: true }))
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: err.message }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
server.middlewares.use('/api/memories', proxyMemoryRequest)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -698,6 +715,96 @@ function modePlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
function promptStylePlugin() {
|
||||
return {
|
||||
name: 'prompt-style-api',
|
||||
configureServer(server) {
|
||||
// GET /api/prompt-styles — list all available styles
|
||||
server.middlewares.use('/api/prompt-styles', async (req, res, next) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET', 'Access-Control-Allow-Headers': 'Content-Type' })
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
if (req.method !== 'GET') { next(); return }
|
||||
|
||||
const { readdir, readFile } = await import('fs/promises')
|
||||
try {
|
||||
const files = (await readdir(PROMPT_STYLES_DIR)).filter(f => f.endsWith('.json'))
|
||||
const styles = []
|
||||
for (const file of files) {
|
||||
try {
|
||||
const raw = await readFile(`${PROMPT_STYLES_DIR}/${file}`, 'utf-8')
|
||||
styles.push(JSON.parse(raw))
|
||||
} catch { /* skip corrupt files */ }
|
||||
}
|
||||
// Sort: cloud group first, then local
|
||||
styles.sort((a, b) => {
|
||||
if (a.group !== b.group) return a.group === 'cloud' ? -1 : 1
|
||||
return (a.id || '').localeCompare(b.id || '')
|
||||
})
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify(styles))
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: err.message }))
|
||||
}
|
||||
})
|
||||
|
||||
// GET/POST /api/prompt-style — active style
|
||||
server.middlewares.use('/api/prompt-style', async (req, res, next) => {
|
||||
// Avoid matching /api/prompt-styles (plural)
|
||||
const url = new URL(req.url, 'http://localhost')
|
||||
if (url.pathname !== '/') { next(); return }
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST', 'Access-Control-Allow-Headers': 'Content-Type' })
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const { readFile, writeFile } = await import('fs/promises')
|
||||
|
||||
if (req.method === 'GET') {
|
||||
try {
|
||||
const raw = await readFile(ACTIVE_STYLE_PATH, 'utf-8')
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(raw)
|
||||
} catch {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ style: 'standard', updated_at: '' }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
const chunks = []
|
||||
for await (const chunk of req) chunks.push(chunk)
|
||||
const data = JSON.parse(Buffer.concat(chunks).toString())
|
||||
const VALID_STYLES = ['quick', 'standard', 'creative', 'roleplayer', 'game-master', 'storyteller']
|
||||
if (!data.style || !VALID_STYLES.includes(data.style)) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: `Invalid style. Valid: ${VALID_STYLES.join(', ')}` }))
|
||||
return
|
||||
}
|
||||
const state = { style: data.style, updated_at: new Date().toISOString() }
|
||||
await writeFile(ACTIVE_STYLE_PATH, JSON.stringify(state, null, 2))
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.end(JSON.stringify({ ok: true, ...state }))
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: err.message }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function bridgeProxyPlugin() {
|
||||
return {
|
||||
name: 'bridge-proxy',
|
||||
@@ -771,10 +878,12 @@ export default defineConfig({
|
||||
satelliteMapPlugin(),
|
||||
conversationStoragePlugin(),
|
||||
memoryStoragePlugin(),
|
||||
dreamProxyPlugin(),
|
||||
gazeProxyPlugin(),
|
||||
characterLookupPlugin(),
|
||||
healthCheckPlugin(),
|
||||
modePlugin(),
|
||||
promptStylePlugin(),
|
||||
bridgeProxyPlugin(),
|
||||
tailwindcss(),
|
||||
react(),
|
||||
|
||||
Reference in New Issue
Block a user