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:
digua committed 2026-05-13 21:53:38 +08:00
1 parent 700b4266f3
commit 42272575b7
2 files changed
+147 -13

No files matched your search

+77
View File
@@ -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 () => {
+70 -13
View File
@@ -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()