feat(desktop): align API client and types with latest web backend endpoints

This commit is contained in:
zhayujie
2026-06-20 00:39:47 +08:00
parent 90d9db0f83
commit 3baa3252bc
2 changed files with 576 additions and 68 deletions

View File

@@ -1,38 +1,74 @@
import type { ConfigData, ChannelInfo, SkillInfo, ToolInfo, MemoryItem, SchedulerTask, Attachment } from '../types' import type {
ConfigData,
ChannelInfo,
ChannelAction,
SkillInfo,
ToolInfo,
MemoryItem,
MemoryCategory,
MemoryPage,
SchedulerTask,
Attachment,
SessionsPage,
HistoryPage,
ModelsData,
ModelsAction,
KnowledgeList,
KnowledgeGraph,
KnowledgeAction,
} from '../types'
interface ApiResult {
status: string
message?: string
}
class ApiClient { class ApiClient {
private baseUrl: string = 'http://127.0.0.1:9899' private baseUrl = 'http://127.0.0.1:9899'
setBaseUrl(url: string) { setBaseUrl(url: string) {
this.baseUrl = url this.baseUrl = url
} }
getBaseUrl() {
return this.baseUrl
}
private async request<T>(path: string, options?: RequestInit): Promise<T> { private async request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, { const res = await fetch(`${this.baseUrl}${path}`, {
...options, ...options,
// Send cookies for future web_password auth support
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers, ...options?.headers,
}, },
}) })
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
} }
return res.json() return res.json()
} }
// Chat // ---------------------------------------------------------
// Chat / messages
// ---------------------------------------------------------
async sendMessage( async sendMessage(
sessionId: string, sessionId: string,
message: string, message: string,
stream: boolean = true, opts?: { stream?: boolean; attachments?: Attachment[]; isVoice?: boolean; lang?: string }
attachments?: Attachment[] ): Promise<{ status: string; request_id: string; stream: boolean; inline_reply?: string }> {
): Promise<{ status: string; request_id: string; stream: boolean }> {
return this.request('/message', { return this.request('/message', {
method: 'POST', method: 'POST',
body: JSON.stringify({ session_id: sessionId, message, stream, attachments }), body: JSON.stringify({
session_id: sessionId,
message,
stream: opts?.stream ?? true,
attachments: opts?.attachments,
is_voice: opts?.isVoice ?? false,
lang: opts?.lang,
}),
}) })
} }
@@ -49,10 +85,38 @@ class ApiClient {
}) })
} }
async cancel(opts: { requestId?: string; sessionId?: string; lang?: string }): Promise<{ status: string; cancelled: number }> {
return this.request('/cancel', {
method: 'POST',
body: JSON.stringify({ request_id: opts.requestId, session_id: opts.sessionId, lang: opts.lang }),
})
}
createSSEStream(requestId: string): EventSource { createSSEStream(requestId: string): EventSource {
return new EventSource(`${this.baseUrl}/stream?request_id=${requestId}`) return new EventSource(`${this.baseUrl}/stream?request_id=${requestId}`)
} }
async deleteMessage(opts: {
sessionId: string
userSeq: number
deleteUser?: boolean
cascade?: boolean
}): Promise<{ status: string; deleted: number }> {
return this.request('/api/messages/delete', {
method: 'POST',
body: JSON.stringify({
session_id: opts.sessionId,
user_seq: opts.userSeq,
delete_user: opts.deleteUser ?? true,
cascade: opts.cascade ?? false,
}),
})
}
// ---------------------------------------------------------
// Upload / files
// ---------------------------------------------------------
async uploadFile(file: File, sessionId?: string): Promise<{ async uploadFile(file: File, sessionId?: string): Promise<{
status: string status: string
file_path: string file_path: string
@@ -62,49 +126,138 @@ class ApiClient {
}> { }> {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
if (sessionId) { if (sessionId) formData.append('session_id', sessionId)
formData.append('session_id', sessionId)
}
const res = await fetch(`${this.baseUrl}/upload`, { const res = await fetch(`${this.baseUrl}/upload`, {
method: 'POST', method: 'POST',
body: formData, body: formData,
credentials: 'include',
}) })
return res.json() return res.json()
} }
// Config getFileUrl(previewUrl: string): string {
async getConfig(): Promise<ConfigData> { if (/^https?:\/\//.test(previewUrl)) return previewUrl
const data = await this.request<{ status: string } & ConfigData>('/config') return `${this.baseUrl}${previewUrl}`
return data
} }
async updateConfig(updates: Partial<ConfigData>): Promise<{ status: string; applied: Record<string, unknown> }> { getServeFileUrl(absPath: string): string {
return `${this.baseUrl}/api/file?path=${encodeURIComponent(absPath)}`
}
// ---------------------------------------------------------
// Sessions
// ---------------------------------------------------------
async getSessions(page = 1, pageSize = 50): Promise<SessionsPage> {
return this.request<{ status: string } & SessionsPage>(`/api/sessions?page=${page}&page_size=${pageSize}`)
}
async deleteSession(sessionId: string): Promise<ApiResult> {
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' })
}
async renameSession(sessionId: string, title: string): Promise<ApiResult> {
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}`, {
method: 'PUT',
body: JSON.stringify({ title }),
})
}
async generateSessionTitle(sessionId: string, userMessage: string, assistantReply?: string): Promise<{ status: string; title: string }> {
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/generate_title`, {
method: 'POST',
body: JSON.stringify({ user_message: userMessage, assistant_reply: assistantReply }),
})
}
async clearContext(sessionId: string): Promise<{ status: string; context_start_seq: number }> {
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/clear_context`, { method: 'POST' })
}
async getHistory(sessionId: string, page = 1, pageSize = 20): Promise<HistoryPage> {
return this.request<{ status: string } & HistoryPage>(
`/api/history?session_id=${encodeURIComponent(sessionId)}&page=${page}&page_size=${pageSize}`
)
}
// ---------------------------------------------------------
// Config
// ---------------------------------------------------------
async getConfig(): Promise<ConfigData> {
return this.request<{ status: string } & ConfigData>('/config')
}
async updateConfig(updates: Record<string, unknown>): Promise<{ status: string; applied: Record<string, unknown> }> {
return this.request('/config', { return this.request('/config', {
method: 'POST', method: 'POST',
body: JSON.stringify({ updates }), body: JSON.stringify({ updates }),
}) })
} }
// ---------------------------------------------------------
// Models console
// ---------------------------------------------------------
async getModels(): Promise<ModelsData> {
return this.request<{ status: string } & ModelsData>('/api/models')
}
async modelsAction(action: ModelsAction): Promise<Record<string, unknown> & { status: string }> {
return this.request('/api/models', {
method: 'POST',
body: JSON.stringify(action),
})
}
// ---------------------------------------------------------
// Channels // Channels
// ---------------------------------------------------------
async getChannels(): Promise<ChannelInfo[]> { async getChannels(): Promise<ChannelInfo[]> {
const data = await this.request<{ status: string; channels: ChannelInfo[] }>('/api/channels') const data = await this.request<{ status: string; channels: ChannelInfo[] }>('/api/channels')
return data.channels return data.channels
} }
async channelAction( async channelAction(
action: 'save' | 'connect' | 'disconnect', action: ChannelAction,
channel: string, channel: string,
config?: Record<string, unknown> config?: Record<string, unknown>
): Promise<{ status: string }> { ): Promise<Record<string, unknown> & { status: string }> {
return this.request('/api/channels', { return this.request('/api/channels', {
method: 'POST', method: 'POST',
body: JSON.stringify({ action, channel, config }), body: JSON.stringify({ action, channel, config }),
}) })
} }
// Tools & Skills // Weixin QR login
async getWeixinQr(): Promise<{ status: string; qrcode_url?: string; qr_image?: string; source?: string; message?: string }> {
return this.request('/api/weixin/qrlogin')
}
async weixinQrAction(action: 'poll' | 'refresh'): Promise<Record<string, unknown> & { status: string }> {
return this.request('/api/weixin/qrlogin', {
method: 'POST',
body: JSON.stringify({ action }),
})
}
// Feishu one-click register
async getFeishuRegister(): Promise<{ status: string; qrcode_url?: string; qr_image?: string; expire_in?: number; message?: string }> {
return this.request('/api/feishu/register')
}
async feishuRegisterPoll(): Promise<Record<string, unknown> & { status: string }> {
return this.request('/api/feishu/register', {
method: 'POST',
body: JSON.stringify({ action: 'poll' }),
})
}
// ---------------------------------------------------------
// Tools & skills
// ---------------------------------------------------------
async getTools(): Promise<ToolInfo[]> { async getTools(): Promise<ToolInfo[]> {
const data = await this.request<{ status: string; tools: ToolInfo[] }>('/api/tools') const data = await this.request<{ status: string; tools: ToolInfo[] }>('/api/tools')
return data.tools return data.tools
@@ -115,60 +268,136 @@ class ApiClient {
return data.skills return data.skills
} }
async toggleSkill(name: string, action: 'open' | 'close'): Promise<{ status: string }> { async toggleSkill(name: string, action: 'open' | 'close'): Promise<ApiResult> {
return this.request('/api/skills', { return this.request('/api/skills', {
method: 'POST', method: 'POST',
body: JSON.stringify({ action, name }), body: JSON.stringify({ action, name }),
}) })
} }
// ---------------------------------------------------------
// Memory // Memory
async getMemoryList(page: number = 1, pageSize: number = 20): Promise<{ list: MemoryItem[]; total: number }> { // ---------------------------------------------------------
const data = await this.request<{ status: string; list: MemoryItem[]; total: number }>(
`/api/memory?page=${page}&page_size=${pageSize}` async getMemoryList(page = 1, pageSize = 20, category: MemoryCategory = 'memory'): Promise<MemoryPage> {
return this.request<{ status: string } & MemoryPage>(
`/api/memory?page=${page}&page_size=${pageSize}&category=${category}`
) )
return { list: data.list, total: data.total }
} }
async getMemoryContent(filename: string): Promise<string> { async getMemoryContent(filename: string, category: MemoryCategory = 'memory'): Promise<string> {
const data = await this.request<{ status: string; content: string }>( const data = await this.request<{ status: string; content: string }>(
`/api/memory/content?filename=${encodeURIComponent(filename)}` `/api/memory/content?filename=${encodeURIComponent(filename)}&category=${category}`
) )
return data.content return data.content
} }
// ---------------------------------------------------------
// Knowledge
// ---------------------------------------------------------
async getKnowledgeList(): Promise<KnowledgeList> {
return this.request<{ status: string } & KnowledgeList>('/api/knowledge/list')
}
async readKnowledge(path: string): Promise<{ status: string; content: string; path: string }> {
return this.request(`/api/knowledge/read?path=${encodeURIComponent(path)}`)
}
async getKnowledgeGraph(): Promise<KnowledgeGraph> {
return this.request<KnowledgeGraph>('/api/knowledge/graph')
}
async knowledgeAction(req: KnowledgeAction): Promise<Record<string, unknown> & { status: string }> {
return this.request('/api/knowledge/action', {
method: 'POST',
body: JSON.stringify(req),
})
}
// ---------------------------------------------------------
// Scheduler // Scheduler
// ---------------------------------------------------------
async getSchedulerTasks(): Promise<SchedulerTask[]> { async getSchedulerTasks(): Promise<SchedulerTask[]> {
const data = await this.request<{ status: string; tasks: SchedulerTask[] }>('/api/scheduler') const data = await this.request<{ status: string; tasks: SchedulerTask[] }>('/api/scheduler')
return data.tasks return data.tasks
} }
// History async toggleTask(taskId: string, enabled: boolean): Promise<{ status: string; task: SchedulerTask }> {
async getHistory( return this.request('/api/scheduler/toggle', {
sessionId: string, method: 'POST',
page: number = 1, body: JSON.stringify({ task_id: taskId, enabled }),
pageSize: number = 20 })
): Promise<{ messages: ChatMessage[]; has_more: boolean }> {
const data = await this.request<{ status: string; messages: ChatMessage[]; has_more: boolean }>(
`/api/history?session_id=${sessionId}&page=${page}&page_size=${pageSize}`
)
return { messages: data.messages, has_more: data.has_more }
} }
// Logs SSE async updateTask(taskId: string, updates: Partial<Pick<SchedulerTask, 'name' | 'enabled' | 'schedule' | 'action'>>): Promise<{ status: string; task: SchedulerTask }> {
return this.request('/api/scheduler/update', {
method: 'POST',
body: JSON.stringify({ task_id: taskId, ...updates }),
})
}
async deleteTask(taskId: string): Promise<ApiResult> {
return this.request('/api/scheduler/delete', {
method: 'POST',
body: JSON.stringify({ task_id: taskId }),
})
}
// ---------------------------------------------------------
// Voice
// ---------------------------------------------------------
async voiceAsr(audio: File | Blob): Promise<{ status: string; text?: string; audio_url?: string; message?: string }> {
const formData = new FormData()
formData.append('file', audio, 'recording.webm')
const res = await fetch(`${this.baseUrl}/api/voice/asr`, {
method: 'POST',
body: formData,
credentials: 'include',
})
return res.json()
}
async voiceTts(text: string, sessionId?: string): Promise<{ status: string; audio_url?: string; message?: string }> {
return this.request('/api/voice/tts', {
method: 'POST',
body: JSON.stringify({ text, session_id: sessionId }),
})
}
// ---------------------------------------------------------
// Logs / version
// ---------------------------------------------------------
createLogStream(): EventSource { createLogStream(): EventSource {
return new EventSource(`${this.baseUrl}/api/logs`) return new EventSource(`${this.baseUrl}/api/logs`)
} }
getFileUrl(previewUrl: string): string { async getVersion(): Promise<string> {
return `${this.baseUrl}${previewUrl}` const data = await this.request<{ version: string }>('/api/version')
return data.version
} }
}
interface ChatMessage { // ---------------------------------------------------------
role: string // Auth (web_password) — placeholder for future use
content: string // ---------------------------------------------------------
timestamp: number
async authCheck(): Promise<{ status: string; auth_required: boolean; authenticated?: boolean }> {
return this.request('/auth/check')
}
async authLogin(password: string): Promise<ApiResult> {
return this.request('/auth/login', {
method: 'POST',
body: JSON.stringify({ password }),
})
}
async authLogout(): Promise<ApiResult> {
return this.request('/auth/logout', { method: 'POST' })
}
} }
export const apiClient = new ApiClient() export const apiClient = new ApiClient()

View File

@@ -1,3 +1,7 @@
// ============================================================
// Electron bridge
// ============================================================
export interface ElectronAPI { export interface ElectronAPI {
getBackendPort: () => Promise<number | null> getBackendPort: () => Promise<number | null>
getBackendStatus: () => Promise<string> getBackendStatus: () => Promise<string>
@@ -6,6 +10,11 @@ export interface ElectronAPI {
selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise<string | null> selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise<string | null>
onBackendStatus: (callback: (data: BackendStatusEvent) => void) => void onBackendStatus: (callback: (data: BackendStatusEvent) => void) => void
onBackendLog: (callback: (line: string) => void) => void onBackendLog: (callback: (line: string) => void) => void
windowMinimize: () => Promise<void>
windowMaximize: () => Promise<boolean>
windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean>
onMaximizeChange: (callback: (maximized: boolean) => void) => void
platform: string platform: string
} }
@@ -15,32 +24,166 @@ export interface BackendStatusEvent {
error?: string error?: string
} }
// ============================================================
// Chat / messages / streaming
// ============================================================
export type Role = 'user' | 'assistant'
/** A single ordered step inside an assistant turn (matches backend history). */
export interface MessageStep {
type: 'thinking' | 'content' | 'tool'
content?: string
// tool step fields
id?: string
name?: string
arguments?: Record<string, unknown>
result?: string
is_error?: boolean
status?: string
execution_time?: number
}
/** Local UI message model (superset of backend history message). */
export interface ChatMessage { export interface ChatMessage {
id: string id: string
role: 'user' | 'assistant' role: Role
content: string content: string
/** Unix seconds. Backend history uses `created_at`; we normalize to `timestamp`. */
timestamp: number timestamp: number
attachments?: Attachment[] attachments?: Attachment[]
/** Ordered steps (thinking / content / tool). Preferred over legacy toolCalls. */
steps?: MessageStep[]
/** Legacy live-stream tool events (kept for backward compat during streaming). */
toolCalls?: ToolCall[] toolCalls?: ToolCall[]
/** Reasoning text streamed via `reasoning` SSE events. */
reasoning?: string
/** Sequence numbers from backend (for delete/regenerate). */
userSeq?: number
botSeq?: number
/** Self-evolution bubble flag. */
kind?: 'evolution'
extras?: Record<string, unknown>
isStreaming?: boolean isStreaming?: boolean
isCancelled?: boolean
error?: string
} }
export interface Attachment { export interface Attachment {
file_path: string file_path: string
file_name: string file_name: string
file_type: 'image' | 'video' | 'file' file_type: 'image' | 'video' | 'file' | 'directory'
preview_url?: string preview_url?: string
} }
/** Live tool event during SSE streaming. */
export interface ToolCall { export interface ToolCall {
type: 'tool_start' | 'tool_end' type: 'tool_start' | 'tool_end' | 'tool_progress'
tool: string tool: string
tool_call_id?: string
arguments?: Record<string, unknown> arguments?: Record<string, unknown>
result?: string result?: string
status?: string status?: string
execution_time?: number execution_time?: number
} }
/** All SSE event types emitted on /stream. */
export type StreamEventType =
| 'delta'
| 'reasoning'
| 'tool_start'
| 'tool_progress'
| 'tool_end'
| 'message_end'
| 'phase'
| 'file_to_send'
| 'image'
| 'video'
| 'file'
| 'text'
| 'done'
| 'cancelled'
| 'voice_attach'
| 'error'
export interface StreamEvent {
type: StreamEventType
content?: string
tool?: string
tool_call_id?: string
arguments?: Record<string, unknown>
status?: string
result?: string
execution_time?: number
has_tool_calls?: boolean
path?: string
file_name?: string
file_type?: string
web_url?: string
audio_url?: string
request_id?: string
timestamp?: number
user_seq?: number
bot_seq?: number
message?: string
}
// ============================================================
// Sessions / history
// ============================================================
export interface SessionItem {
session_id: string
title: string
created_at: number
last_active: number
msg_count: number
}
export interface SessionsPage {
sessions: SessionItem[]
total: number
page: number
page_size: number
has_more: boolean
}
/** Backend history message (as returned by /api/history). */
export interface HistoryMessage {
role: Role
content: string
created_at: number
steps?: MessageStep[]
tool_calls?: Array<{ id?: string; name: string; arguments?: Record<string, unknown>; result?: string }>
reasoning?: string
kind?: 'evolution'
extras?: Record<string, unknown>
/** Per-message sequence number used by delete/regenerate APIs. */
_seq?: number
}
export interface HistoryPage {
messages: HistoryMessage[]
total: number
page: number
page_size: number
has_more: boolean
context_start_seq?: number
}
// ============================================================
// Config
// ============================================================
export interface ProviderMeta {
label: string
models: string[]
api_base_key?: string
api_base_default?: string
api_key_field?: string
[k: string]: unknown
}
export interface ConfigData { export interface ConfigData {
use_agent: boolean use_agent: boolean
title: string title: string
@@ -51,54 +194,190 @@ export interface ConfigData {
agent_max_context_tokens: number agent_max_context_tokens: number
agent_max_context_turns: number agent_max_context_turns: number
agent_max_steps: number agent_max_steps: number
enable_thinking?: boolean
self_evolution_enabled?: boolean
api_bases: Record<string, string> api_bases: Record<string, string>
api_keys: Record<string, string> api_keys: Record<string, string>
providers: Record<string, unknown> providers: Record<string, ProviderMeta>
web_password_masked?: string
}
// ============================================================
// Models console (/api/models)
// ============================================================
export interface ModelProvider {
id: string
label: string
configured: boolean
is_custom: boolean
custom_id?: string
active?: boolean
api_key_masked?: string
api_base?: string
models: string[]
}
export type CapabilityKey = 'chat' | 'vision' | 'asr' | 'tts' | 'embedding' | 'image' | 'search'
export interface CapabilityState {
current_provider?: string
current_model?: string
providers?: string[]
provider_models?: Record<string, string[]>
strategy?: string
fallback_provider?: string
fallback_model?: string
[k: string]: unknown
}
export interface ModelsData {
providers: ModelProvider[]
capabilities: Record<CapabilityKey, CapabilityState>
}
export type ModelsAction =
| { action: 'set_provider'; provider_id: string; api_key?: string; api_base?: string }
| { action: 'delete_provider'; provider_id: string }
| { action: 'set_custom_provider'; name: string; id?: string; api_base: string; api_key?: string; model?: string; make_active?: boolean }
| { action: 'delete_custom_provider'; id: string }
| { action: 'set_active_custom_provider'; id: string }
| { action: 'set_capability'; capability: CapabilityKey; provider_id: string; model: string; voice?: string; strategy?: string; provider?: string }
| { action: 'set_voice_reply_mode'; mode: 'off' | 'voice_if_voice' | 'always' }
| { action: 'set_search_credential'; api_key: string }
// ============================================================
// Channels
// ============================================================
export interface ChannelField {
key: string
label: string
type: 'text' | 'secret' | 'number' | 'bool'
value?: string | number | boolean
default?: string | number | boolean
} }
export interface ChannelInfo { export interface ChannelInfo {
name: string name: string
label: Record<string, string> label: { zh: string; en: string }
icon: string icon: string
color: string color: string
active: boolean active: boolean
fields: ChannelField[] fields: ChannelField[]
login_status?: string
} }
export interface ChannelField { export type ChannelAction = 'save' | 'connect' | 'disconnect'
key: string
label: Record<string, string> // ============================================================
type: string // Tools / skills
required?: boolean // ============================================================
placeholder?: Record<string, string>
export interface ToolInfo {
name: string
description: string
} }
export interface SkillInfo { export interface SkillInfo {
name: string name: string
display_name: string
description: string description: string
source?: string
enabled: boolean enabled: boolean
category?: string
} }
export interface ToolInfo { // ============================================================
name: string // Memory
display_name: string // ============================================================
description: string
} export type MemoryCategory = 'memory' | 'dream' | 'evolution'
export interface MemoryItem { export interface MemoryItem {
filename: string filename: string
modified: number type: string // global | daily | dream | evolution
size: number size: number
updated_at: string
}
export interface MemoryPage {
list: MemoryItem[]
total: number
page: number
page_size: number
}
// ============================================================
// Knowledge
// ============================================================
export interface KnowledgeNode {
name: string
path: string
type: 'category' | 'file'
children?: KnowledgeNode[]
count?: number
}
export interface KnowledgeList {
root_files?: KnowledgeNode[]
tree: KnowledgeNode[]
stats: { pages: number; size: number }
enabled: boolean
}
export interface KnowledgeGraph {
nodes: Array<{ id: string; label: string; category?: string }>
links: Array<{ source: string; target: string }>
}
export type KnowledgeAction =
| { action: 'create_category'; payload: { path: string } }
| { action: 'rename_category'; payload: { path: string; new_path: string } }
| { action: 'delete_category'; payload: { path: string; confirm?: boolean } }
| { action: 'delete_documents'; payload: { paths: string[] } }
| { action: 'move_documents'; payload: { paths: string[]; target_category: string } }
// ============================================================
// Scheduler
// ============================================================
export interface TaskSchedule {
type: 'cron' | 'interval' | 'once'
expression?: string
seconds?: number
run_at?: string
}
export interface TaskAction {
type: 'send_message' | 'agent_task'
content?: string
task_description?: string
receiver?: string
receiver_name?: string
is_group?: boolean
channel_type?: string
} }
export interface SchedulerTask { export interface SchedulerTask {
id: string id: string
name: string name: string
cron: string
enabled: boolean enabled: boolean
last_run?: string created_at: string
next_run?: string updated_at: string
schedule: TaskSchedule
action: TaskAction
next_run_at?: string
}
// ============================================================
// Logs
// ============================================================
export interface LogEvent {
type: 'init' | 'line' | 'error'
content?: string
message?: string
} }
declare global { declare global {