mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(web): implement demo import for web mode via SSE endpoint
- Add POST /_web/demo/import backend endpoint: downloads demo data from chatlab.fun and imports using streamImport with SSE progress - Refactor DemoImportButton.vue to support both Electron (IPC) and Web (SSE fetch) modes with shared result handling
This commit is contained in:
1 parent
700b4266f3
commit
42272575b7
2 files changed
+147
-13
No files matched your search
@@ -470,6 +470,83 @@ export function registerWebRoutes(server: FastifyInstance, dbManager: DatabaseMa
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
// ==================== Demo 示例数据 ====================
|
||||
|
||||
const DEMO_BASE_URL = 'https://chatlab.fun/assets/demo'
|
||||
|
||||
server.post<{ Body: { locale?: string } }>('/_web/demo/import', async (request, reply) => {
|
||||
const locale = (request.body as any)?.locale || 'en'
|
||||
const nativeBinding = resolveNativeBinding()
|
||||
|
||||
reply.raw.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
function sendEvent(event: string, data: unknown) {
|
||||
reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
||||
}
|
||||
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chatlab-demo-'))
|
||||
const groupPath = path.join(tmpDir, 'demo-group.json')
|
||||
const privatePath = path.join(tmpDir, 'demo-private.json')
|
||||
|
||||
try {
|
||||
sendEvent('progress', { stage: 'downloading', current: 1, total: 2 })
|
||||
const groupResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-group.json`, {
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
})
|
||||
if (!groupResp.ok) throw new Error(`Download group demo failed: ${groupResp.status}`)
|
||||
fs.writeFileSync(groupPath, Buffer.from(await groupResp.arrayBuffer()))
|
||||
|
||||
sendEvent('progress', { stage: 'downloading', current: 2, total: 2 })
|
||||
const privateResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-private.json`, {
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
})
|
||||
if (!privateResp.ok) throw new Error(`Download private demo failed: ${privateResp.status}`)
|
||||
fs.writeFileSync(privatePath, Buffer.from(await privateResp.arrayBuffer()))
|
||||
|
||||
sendEvent('progress', { stage: 'importing', current: 1, total: 2 })
|
||||
const groupResult = await streamImport(dbManager, groupPath, { nativeBinding })
|
||||
|
||||
if (!groupResult.success) throw new Error(groupResult.error || 'Failed to import group demo')
|
||||
|
||||
sendEvent('progress', { stage: 'importing', current: 2, total: 2 })
|
||||
const privateResult = await streamImport(dbManager, privatePath, { nativeBinding })
|
||||
|
||||
if (!privateResult.success) throw new Error(privateResult.error || 'Failed to import private demo')
|
||||
|
||||
sendEvent('progress', { stage: 'done', current: 2, total: 2 })
|
||||
sendEvent('result', {
|
||||
success: true,
|
||||
groupSessionId: groupResult.sessionId,
|
||||
privateSessionId: privateResult.sessionId,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
sendEvent('progress', { stage: 'error', current: 0, total: 2, message })
|
||||
sendEvent('result', { success: false, error: message })
|
||||
} finally {
|
||||
try {
|
||||
fs.unlinkSync(groupPath)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
fs.unlinkSync(privatePath)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
fs.rmdirSync(tmpDir)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
reply.raw.end()
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 导入管线 ====================
|
||||
|
||||
server.get('/_web/supported-formats', async () => {
|
||||
|
||||
@@ -25,26 +25,83 @@ async function navigateToSession(sessionId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
if (!IS_ELECTRON) {
|
||||
error.value = t('home.demo.notAvailableInWeb', 'Demo 导入功能暂仅在桌面端可用')
|
||||
return
|
||||
}
|
||||
|
||||
isImporting.value = true
|
||||
error.value = null
|
||||
stage.value = 'downloading'
|
||||
|
||||
async function handleImportElectron() {
|
||||
const unsubscribe = window.chatApi.onDemoProgress((progress) => {
|
||||
if (progress.stage === 'downloading' || progress.stage === 'importing') {
|
||||
stage.value = progress.stage
|
||||
}
|
||||
})
|
||||
|
||||
const demoLocale = getChatlabSiteLocalePath(settingsStore.locale) || 'en'
|
||||
const result = await window.chatApi.importDemo(demoLocale)
|
||||
unsubscribe()
|
||||
return result
|
||||
}
|
||||
|
||||
async function handleImportWeb(): Promise<{
|
||||
success: boolean
|
||||
groupSessionId?: string
|
||||
privateSessionId?: string
|
||||
error?: string
|
||||
}> {
|
||||
const demoLocale = getChatlabSiteLocalePath(settingsStore.locale) || 'en'
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const ctrl = new AbortController()
|
||||
fetch('/_web/demo/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ locale: demoLocale }),
|
||||
signal: ctrl.signal,
|
||||
})
|
||||
.then(async (resp) => {
|
||||
if (!resp.ok || !resp.body) {
|
||||
resolve({ success: false, error: `HTTP ${resp.status}` })
|
||||
return
|
||||
}
|
||||
|
||||
const reader = resp.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
let eventType = ''
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventType = line.slice(7)
|
||||
} else if (line.startsWith('data: ')) {
|
||||
const data = JSON.parse(line.slice(6))
|
||||
if (eventType === 'progress') {
|
||||
if (data.stage === 'downloading' || data.stage === 'importing') {
|
||||
stage.value = data.stage
|
||||
}
|
||||
} else if (eventType === 'result') {
|
||||
resolve(data)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve({ success: false, error: 'Stream ended without result' })
|
||||
})
|
||||
.catch((e) => resolve({ success: false, error: String(e) }))
|
||||
})
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
isImporting.value = true
|
||||
error.value = null
|
||||
stage.value = 'downloading'
|
||||
|
||||
try {
|
||||
const demoLocale = getChatlabSiteLocalePath(settingsStore.locale) || 'en'
|
||||
const result = await window.chatApi.importDemo(demoLocale)
|
||||
unsubscribe()
|
||||
const result = IS_ELECTRON ? await handleImportElectron() : await handleImportWeb()
|
||||
|
||||
if (result.success && result.groupSessionId) {
|
||||
await sessionStore.loadSessions()
|
||||
|
||||
Reference in new issue
Block a user