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 {
private baseUrl: string = 'http://127.0.0.1:9899'
private baseUrl = 'http://127.0.0.1:9899'
setBaseUrl(url: string) {
this.baseUrl = url
}
getBaseUrl() {
return this.baseUrl
}
private async request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
...options,
// Send cookies for future web_password auth support
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}
return res.json()
}
// Chat
// ---------------------------------------------------------
// Chat / messages
// ---------------------------------------------------------
async sendMessage(
sessionId: string,
message: string,
stream: boolean = true,
attachments?: Attachment[]
): Promise<{ status: string; request_id: string; stream: boolean }> {
opts?: { stream?: boolean; attachments?: Attachment[]; isVoice?: boolean; lang?: string }
): Promise<{ status: string; request_id: string; stream: boolean; inline_reply?: string }> {
return this.request('/message', {
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 {
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<{
status: string
file_path: string
@@ -62,49 +126,138 @@ class ApiClient {
}> {
const formData = new FormData()
formData.append('file', file)
if (sessionId) {
formData.append('session_id', sessionId)
}
if (sessionId) formData.append('session_id', sessionId)
const res = await fetch(`${this.baseUrl}/upload`, {
method: 'POST',
body: formData,
credentials: 'include',
})
return res.json()
}
// Config
async getConfig(): Promise<ConfigData> {
const data = await this.request<{ status: string } & ConfigData>('/config')
return data
getFileUrl(previewUrl: string): string {
if (/^https?:\/\//.test(previewUrl)) return previewUrl
return `${this.baseUrl}${previewUrl}`
}
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', {
method: 'POST',
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
// ---------------------------------------------------------
async getChannels(): Promise<ChannelInfo[]> {
const data = await this.request<{ status: string; channels: ChannelInfo[] }>('/api/channels')
return data.channels
}
async channelAction(
action: 'save' | 'connect' | 'disconnect',
action: ChannelAction,
channel: string,
config?: Record<string, unknown>
): Promise<{ status: string }> {
): Promise<Record<string, unknown> & { status: string }> {
return this.request('/api/channels', {
method: 'POST',
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[]> {
const data = await this.request<{ status: string; tools: ToolInfo[] }>('/api/tools')
return data.tools
@@ -115,60 +268,136 @@ class ApiClient {
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', {
method: 'POST',
body: JSON.stringify({ action, name }),
})
}
// ---------------------------------------------------------
// 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 }>(
`/api/memory/content?filename=${encodeURIComponent(filename)}`
`/api/memory/content?filename=${encodeURIComponent(filename)}&category=${category}`
)
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
// ---------------------------------------------------------
async getSchedulerTasks(): Promise<SchedulerTask[]> {
const data = await this.request<{ status: string; tasks: SchedulerTask[] }>('/api/scheduler')
return data.tasks
}
// History
async getHistory(
sessionId: string,
page: number = 1,
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 }
async toggleTask(taskId: string, enabled: boolean): Promise<{ status: string; task: SchedulerTask }> {
return this.request('/api/scheduler/toggle', {
method: 'POST',
body: JSON.stringify({ task_id: taskId, enabled }),
})
}
// 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 {
return new EventSource(`${this.baseUrl}/api/logs`)
}
getFileUrl(previewUrl: string): string {
return `${this.baseUrl}${previewUrl}`
async getVersion(): Promise<string> {
const data = await this.request<{ version: string }>('/api/version')
return data.version
}
}
interface ChatMessage {
role: string
content: string
timestamp: number
// ---------------------------------------------------------
// Auth (web_password) — placeholder for future use
// ---------------------------------------------------------
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()

View File

@@ -1,3 +1,7 @@
// ============================================================
// Electron bridge
// ============================================================
export interface ElectronAPI {
getBackendPort: () => Promise<number | null>
getBackendStatus: () => Promise<string>
@@ -6,6 +10,11 @@ export interface ElectronAPI {
selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise<string | null>
onBackendStatus: (callback: (data: BackendStatusEvent) => 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
}
@@ -15,32 +24,166 @@ export interface BackendStatusEvent {
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 {
id: string
role: 'user' | 'assistant'
role: Role
content: string
/** Unix seconds. Backend history uses `created_at`; we normalize to `timestamp`. */
timestamp: number
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[]
/** 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
isCancelled?: boolean
error?: string
}
export interface Attachment {
file_path: string
file_name: string
file_type: 'image' | 'video' | 'file'
file_type: 'image' | 'video' | 'file' | 'directory'
preview_url?: string
}
/** Live tool event during SSE streaming. */
export interface ToolCall {
type: 'tool_start' | 'tool_end'
type: 'tool_start' | 'tool_end' | 'tool_progress'
tool: string
tool_call_id?: string
arguments?: Record<string, unknown>
result?: string
status?: string
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 {
use_agent: boolean
title: string
@@ -51,54 +194,190 @@ export interface ConfigData {
agent_max_context_tokens: number
agent_max_context_turns: number
agent_max_steps: number
enable_thinking?: boolean
self_evolution_enabled?: boolean
api_bases: 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 {
name: string
label: Record<string, string>
label: { zh: string; en: string }
icon: string
color: string
active: boolean
fields: ChannelField[]
login_status?: string
}
export interface ChannelField {
key: string
label: Record<string, string>
type: string
required?: boolean
placeholder?: Record<string, string>
export type ChannelAction = 'save' | 'connect' | 'disconnect'
// ============================================================
// Tools / skills
// ============================================================
export interface ToolInfo {
name: string
description: string
}
export interface SkillInfo {
name: string
display_name: string
description: string
source?: string
enabled: boolean
category?: string
}
export interface ToolInfo {
name: string
display_name: string
description: string
}
// ============================================================
// Memory
// ============================================================
export type MemoryCategory = 'memory' | 'dream' | 'evolution'
export interface MemoryItem {
filename: string
modified: number
type: string // global | daily | dream | evolution
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 {
id: string
name: string
cron: string
enabled: boolean
last_run?: string
next_run?: string
created_at: 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 {