feat(home): add AI chat interface with OpenCode integration
This commit is contained in:
20
api/modules/opencode/index.ts
Normal file
20
api/modules/opencode/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Router } from 'express'
|
||||
import type { ServiceContainer } from '../../infra/container.js'
|
||||
import { createApiModule } from '../../infra/createModule.js'
|
||||
import { OPENCODE_MODULE } from '../../../shared/modules/opencode/index.js'
|
||||
import { createOpencodeRoutes } from './routes.js'
|
||||
|
||||
export * from './routes.js'
|
||||
|
||||
export const createOpencodeModule = () => {
|
||||
return createApiModule({
|
||||
...OPENCODE_MODULE,
|
||||
version: OPENCODE_MODULE.version || '1.0.0',
|
||||
}, {
|
||||
routes: (_container: ServiceContainer): Router => {
|
||||
return createOpencodeRoutes()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default createOpencodeModule
|
||||
223
api/modules/opencode/routes.ts
Normal file
223
api/modules/opencode/routes.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { Router } from 'express'
|
||||
import { createOpencodeClient } from '@opencode-ai/sdk'
|
||||
|
||||
const OPENCODE_URL = 'http://localhost:4096'
|
||||
|
||||
async function getClient() {
|
||||
console.log('[OpenCode] Creating client for URL:', OPENCODE_URL)
|
||||
return createOpencodeClient({
|
||||
baseUrl: OPENCODE_URL,
|
||||
})
|
||||
}
|
||||
|
||||
export function createOpencodeRoutes(): Router {
|
||||
const router = Router()
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const action = req.query.action as string | undefined
|
||||
|
||||
try {
|
||||
const client = await getClient()
|
||||
|
||||
if (action === 'list-sessions') {
|
||||
const response = await client.session.list()
|
||||
const sessions = (response.data || []).map((session: { id: string; title?: string; createdAt?: string }) => ({
|
||||
id: session.id,
|
||||
title: session.title || 'New Chat',
|
||||
createdAt: session.createdAt ? new Date(session.createdAt).getTime() : Date.now(),
|
||||
}))
|
||||
res.json({ sessions })
|
||||
return
|
||||
}
|
||||
|
||||
res.status(400).json({ error: 'Unknown action' })
|
||||
} catch (error: unknown) {
|
||||
console.error('API error:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
res.status(500).json({ error: errorMessage })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const body = req.body
|
||||
const { action, sessionId, title, text, attachments } = body
|
||||
console.log('[OpenCode] Received action:', action, 'sessionId:', sessionId)
|
||||
|
||||
const client = await getClient()
|
||||
console.log('[OpenCode] Client created successfully')
|
||||
|
||||
if (action === 'create-session') {
|
||||
try {
|
||||
const response = await client.session.create({
|
||||
body: { title: title || 'New Chat' },
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
res.status(500).json({ error: 'OpenCode error', details: response.error })
|
||||
return
|
||||
}
|
||||
|
||||
const sessionData = response.data as { id: string; title?: string; createdAt?: string } | undefined
|
||||
if (sessionData) {
|
||||
res.json({
|
||||
session: {
|
||||
id: sessionData.id,
|
||||
title: sessionData.title,
|
||||
createdAt: sessionData.createdAt ? new Date(sessionData.createdAt).getTime() : Date.now(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to create session - no data' })
|
||||
return
|
||||
} catch (e: unknown) {
|
||||
console.error('Create session exception:', e)
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error'
|
||||
res.status(500).json({ error: errorMessage, exception: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'delete-session') {
|
||||
const response = await client.session.delete({
|
||||
path: { id: sessionId },
|
||||
})
|
||||
res.json({ success: response.data })
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'get-session') {
|
||||
const response = await client.session.get({
|
||||
path: { id: sessionId },
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
res.json({
|
||||
title: response.data.title || 'New Chat',
|
||||
})
|
||||
return
|
||||
}
|
||||
res.status(404).json({ error: 'Session not found' })
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'get-messages') {
|
||||
const limit = body.limit || 20
|
||||
|
||||
const response = await client.session.messages({
|
||||
path: { id: sessionId },
|
||||
query: { limit }
|
||||
})
|
||||
|
||||
const messages = (response.data || []).map((item: { info?: { id?: string; role?: string; content?: string; createdAt?: string }; content?: string; parts?: unknown[]; id?: string; role?: string }) => {
|
||||
let content = item.info?.content || item.content || ''
|
||||
|
||||
const parts = item.parts as Array<{ type: string; text?: string }> | undefined
|
||||
if (parts && parts.length > 0) {
|
||||
const textParts = parts
|
||||
.filter((p) => p.type === 'text')
|
||||
.map((p) => p.text)
|
||||
.join('')
|
||||
if (textParts) {
|
||||
content = textParts
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.info?.id || item.id,
|
||||
role: item.info?.role || item.role,
|
||||
content: content,
|
||||
parts: item.parts || [],
|
||||
createdAt: item.info?.createdAt,
|
||||
}
|
||||
})
|
||||
|
||||
const hasMore = messages.length >= limit
|
||||
|
||||
res.json({ messages, hasMore })
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'prompt') {
|
||||
const parts: Array<{ type: 'text'; text: string } | { type: 'file'; url: string; name: string; mime: string }> = []
|
||||
|
||||
if (attachments && attachments.length > 0) {
|
||||
for (const att of attachments as Array<{ url: string; name: string; mediaType: string }>) {
|
||||
parts.push({
|
||||
type: 'file',
|
||||
url: att.url,
|
||||
name: att.name,
|
||||
mime: att.mediaType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
parts.push({ type: 'text', text })
|
||||
|
||||
try {
|
||||
const response = await client.session.prompt({
|
||||
path: { id: sessionId },
|
||||
body: { parts },
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
res.json({
|
||||
message: {
|
||||
id: (response.data.info as { id?: string })?.id || crypto.randomUUID(),
|
||||
role: (response.data.info as { role?: string })?.role || 'assistant',
|
||||
content: '',
|
||||
parts: response.data.parts || [],
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Failed to get response', details: response.error })
|
||||
return
|
||||
} catch (e: unknown) {
|
||||
console.error('Prompt exception:', e)
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error'
|
||||
res.status(500).json({ error: errorMessage })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'summarize-session') {
|
||||
try {
|
||||
await client.session.summarize({
|
||||
path: { id: sessionId },
|
||||
})
|
||||
|
||||
const response = await client.session.get({
|
||||
path: { id: sessionId },
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
res.json({
|
||||
title: response.data.title || 'New Chat',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Failed to get session' })
|
||||
return
|
||||
} catch (e: unknown) {
|
||||
console.error('Summarize error:', e)
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error'
|
||||
res.status(500).json({ error: errorMessage })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res.status(400).json({ error: 'Unknown action' })
|
||||
} catch (error: unknown) {
|
||||
console.error('API error:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
res.status(500).json({ error: errorMessage })
|
||||
}
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
Reference in New Issue
Block a user