fix(frontend): 修复合并审阅发现的构建与契约问题
恢复 Vue TypeScript 生产构建,补齐可运行页面壳子,并修复文件树与 SSE 状态问题。 按 FastAPI Wire Contract 统一 Service DTO 映射,同时补充前端开发说明和问题修复复盘。
This commit is contained in:
@@ -1,46 +1,61 @@
|
||||
import apiClient from './apiClient'
|
||||
import { SseClient } from './sseClient'
|
||||
import type { AgentRun, AgentEvent, ToolDefinition, PermissionRequest } from '@/contracts'
|
||||
import type { AgentRun, AgentEvent, ApiAgentRun, OperationResponse, PageMeta, ToolDefinition, PermissionRequest } from '@/contracts'
|
||||
|
||||
function toAgentRun(run: ApiAgentRun): AgentRun {
|
||||
return {
|
||||
run_id: run.run_id,
|
||||
status: run.status,
|
||||
current_step: run.current_step,
|
||||
max_steps: run.max_steps,
|
||||
token_usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
total_tokens: run.token_usage,
|
||||
},
|
||||
started_at: run.created_at,
|
||||
completed_at: ['completed', 'failed', 'cancelled'].includes(run.status) ? run.updated_at : undefined,
|
||||
error: run.error_message ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAgentRuns(params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<{ items: AgentRun[]; total: number }> {
|
||||
return apiClient.get('/api/agent/runs', { params })
|
||||
const response = await apiClient.get<{ items: ApiAgentRun[]; page: PageMeta }>('/api/agent/runs', { params })
|
||||
return { items: response.items.map(toAgentRun), total: response.page.total }
|
||||
}
|
||||
|
||||
export async function getAgentRun(runId: string): Promise<AgentRun> {
|
||||
return apiClient.get(`/api/agent/runs/${runId}`)
|
||||
return toAgentRun(await apiClient.get<ApiAgentRun>(`/api/agent/runs/${runId}`))
|
||||
}
|
||||
|
||||
export interface CreateAgentRunRequest {
|
||||
task: string
|
||||
provider_id?: string
|
||||
model?: string
|
||||
input: string
|
||||
provider_id: string
|
||||
model: string
|
||||
skill_id?: string
|
||||
allowed_tools?: string[]
|
||||
max_steps?: number
|
||||
tool_timeout?: number
|
||||
run_timeout?: number
|
||||
tool_timeout_seconds?: number
|
||||
run_timeout_seconds?: number
|
||||
token_budget?: number
|
||||
allow_network?: boolean
|
||||
max_concurrent_tools?: number
|
||||
}
|
||||
|
||||
export async function createAgentRun(request: CreateAgentRunRequest): Promise<AgentRun> {
|
||||
return apiClient.post('/api/agent/runs', request)
|
||||
return toAgentRun(await apiClient.post<ApiAgentRun>('/api/agent/runs', request))
|
||||
}
|
||||
|
||||
export async function cancelAgentRun(runId: string): Promise<void> {
|
||||
export async function cancelAgentRun(runId: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/agent/runs/${runId}/cancel`)
|
||||
}
|
||||
|
||||
export async function listTools(): Promise<ToolDefinition[]> {
|
||||
try {
|
||||
return await apiClient.get('/api/tools')
|
||||
} catch {
|
||||
return mockTools
|
||||
}
|
||||
const response = await apiClient.get<{ items: ToolDefinition[] }>('/api/tools')
|
||||
return response.items
|
||||
}
|
||||
|
||||
export function streamAgentEvents(
|
||||
@@ -75,12 +90,10 @@ export function streamAgentEvents(
|
||||
export async function respondToPermission(
|
||||
runId: string,
|
||||
requestId: string,
|
||||
decision: 'allow' | 'deny',
|
||||
scope?: 'once' | 'session' | 'always'
|
||||
): Promise<void> {
|
||||
decision: 'allow_once' | 'allow_session' | 'deny'
|
||||
): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/agent/runs/${runId}/permissions/${requestId}`, {
|
||||
decision,
|
||||
scope,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { ApiError, ErrorResponse } from '@/contracts'
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE || ''
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? import.meta.env.VITE_API_BASE ?? ''
|
||||
|
||||
export function resolveApiUrl(path: string): string {
|
||||
if (/^https?:\/\//i.test(path)) return path
|
||||
return `${BASE_URL.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
||||
}
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
params?: Record<string, string | number | boolean | undefined>
|
||||
@@ -22,7 +27,7 @@ export class ApiErrorClass extends Error {
|
||||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { params, token, headers, ...rest } = options
|
||||
|
||||
let url = path.startsWith('http') ? path : `${BASE_URL}${path}`
|
||||
let url = resolveApiUrl(path)
|
||||
|
||||
if (params) {
|
||||
const usp = new URLSearchParams()
|
||||
@@ -94,6 +99,13 @@ export const apiClient = {
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
},
|
||||
put<T>(path: string, body?: unknown, options?: Omit<RequestOptions, 'method' | 'body'>) {
|
||||
return request<T>(path, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
},
|
||||
delete<T>(path: string, options?: Omit<RequestOptions, 'method'>) {
|
||||
return request<T>(path, { ...options, method: 'DELETE' })
|
||||
},
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import apiClient from './apiClient'
|
||||
import { SseClient } from './sseClient'
|
||||
import type { Conversation, ChatMessage, ModelEvent } from '@/contracts'
|
||||
|
||||
export async function listConversations(): Promise<Conversation[]> {
|
||||
return apiClient.get('/api/conversations')
|
||||
}
|
||||
|
||||
export async function getConversation(conversationId: string): Promise<Conversation> {
|
||||
return apiClient.get(`/api/conversations/${conversationId}`)
|
||||
}
|
||||
|
||||
export async function getMessages(conversationId: string): Promise<ChatMessage[]> {
|
||||
return apiClient.get(`/api/conversations/${conversationId}/messages`)
|
||||
}
|
||||
|
||||
export interface ChatRequest {
|
||||
provider_id: string
|
||||
model: string
|
||||
conversation_id?: string
|
||||
message: string
|
||||
provider_id?: string
|
||||
model?: string
|
||||
system?: string
|
||||
messages: Array<{
|
||||
role: 'system' | 'user' | 'assistant' | 'tool'
|
||||
content: string
|
||||
name?: string
|
||||
tool_call_id?: string
|
||||
}>
|
||||
use_rag?: boolean
|
||||
skill_id?: string
|
||||
attachments?: string[]
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
}
|
||||
|
||||
export function streamChat(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { apiClient, ApiErrorClass } from './apiClient'
|
||||
export type { ApiError } from './apiClient'
|
||||
export type { ApiError } from '@/contracts'
|
||||
export { SseClient } from './sseClient'
|
||||
export type { SseClientOptions, SseEventHandler } from './sseClient'
|
||||
export * as noteService from './noteService'
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import apiClient from './apiClient'
|
||||
import type { IndexStatus } from '@/contracts'
|
||||
import type { ApiIndexJob, ApiIndexStatus, IndexStatus } from '@/contracts'
|
||||
|
||||
export async function getIndexStatus(): Promise<IndexStatus> {
|
||||
try {
|
||||
return await apiClient.get('/api/index/status')
|
||||
} catch {
|
||||
return mockIndexStatus
|
||||
function toIndexStatus(status: ApiIndexStatus): IndexStatus {
|
||||
return {
|
||||
status: status.status === 'idle' ? 'idle' : status.status === 'failed' ? 'error' : 'indexing',
|
||||
pending_jobs: status.pending_jobs,
|
||||
total_notes: 0,
|
||||
total_blocks: 0,
|
||||
fts_enabled: true,
|
||||
vector_enabled: true,
|
||||
last_indexed_at: status.last_completed_at ?? undefined,
|
||||
error: status.error_message ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export async function rebuildIndex(scope: 'full' | 'fts' | 'vector' = 'full'): Promise<{ job_id: string }> {
|
||||
return apiClient.post('/api/index/rebuild', { scope })
|
||||
export async function getIndexStatus(): Promise<IndexStatus> {
|
||||
return toIndexStatus(await apiClient.get<ApiIndexStatus>('/api/index/status'))
|
||||
}
|
||||
|
||||
export async function getIndexJob(jobId: string): Promise<{
|
||||
job_id: string
|
||||
status: 'queued' | 'running' | 'completed' | 'failed'
|
||||
progress: number
|
||||
total: number
|
||||
error?: string
|
||||
}> {
|
||||
export async function rebuildIndex(scope: 'full' | 'fts' | 'vector' = 'full'): Promise<ApiIndexJob> {
|
||||
const apiScope = scope === 'full' ? 'all' : scope === 'fts' ? 'notes' : 'vectors'
|
||||
return apiClient.post<ApiIndexJob>('/api/index/rebuild', { scope: apiScope })
|
||||
}
|
||||
|
||||
export async function getIndexJob(jobId: string): Promise<ApiIndexJob> {
|
||||
return apiClient.get(`/api/index/jobs/${jobId}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import apiClient from './apiClient'
|
||||
import type { Note, NoteBlock } from '@/contracts'
|
||||
import type { ApiNote, ApiNoteSummary, OperationResponse, PageMeta } from '@/contracts'
|
||||
|
||||
export async function listNotes(params?: {
|
||||
folder?: string
|
||||
tag?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<{ items: Note[]; total: number }> {
|
||||
}): Promise<{ items: ApiNoteSummary[]; page: PageMeta }> {
|
||||
return apiClient.get('/api/notes', { params })
|
||||
}
|
||||
|
||||
export async function getNote(noteId: string): Promise<{ note: Note; blocks: NoteBlock[] }> {
|
||||
export async function getNote(noteId: string): Promise<ApiNote> {
|
||||
return apiClient.get(`/api/notes/${noteId}`)
|
||||
}
|
||||
|
||||
export async function createNote(data: {
|
||||
title: string
|
||||
folder_path?: string
|
||||
content?: string
|
||||
}): Promise<Note> {
|
||||
folder?: string
|
||||
markdown?: string
|
||||
tags?: string[]
|
||||
}): Promise<ApiNote> {
|
||||
return apiClient.post('/api/notes', data)
|
||||
}
|
||||
|
||||
export async function updateNote(
|
||||
noteId: string,
|
||||
data: { title?: string; content?: string; tags?: string[] }
|
||||
): Promise<Note> {
|
||||
data: { title?: string; markdown?: string; tags?: string[] }
|
||||
): Promise<ApiNote> {
|
||||
return apiClient.patch(`/api/notes/${noteId}`, data)
|
||||
}
|
||||
|
||||
export async function deleteNote(noteId: string): Promise<void> {
|
||||
export async function deleteNote(noteId: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/notes/${noteId}`)
|
||||
}
|
||||
|
||||
export async function moveNote(noteId: string, target_folder: string): Promise<Note> {
|
||||
return apiClient.post(`/api/notes/${noteId}/move`, { target_folder })
|
||||
export async function moveNote(noteId: string, folder: string): Promise<ApiNote> {
|
||||
return apiClient.post(`/api/notes/${noteId}/move`, { folder })
|
||||
}
|
||||
|
||||
@@ -1,31 +1,59 @@
|
||||
import apiClient from './apiClient'
|
||||
import type { Plugin } from '@/contracts'
|
||||
import type { ApiPlugin, OperationResponse, Plugin, PluginContribution } from '@/contracts'
|
||||
|
||||
export async function listPlugins(): Promise<Plugin[]> {
|
||||
try {
|
||||
return await apiClient.get('/api/plugins')
|
||||
} catch {
|
||||
return mockPlugins
|
||||
function toPlugin(plugin: ApiPlugin): Plugin {
|
||||
const { manifest } = plugin
|
||||
const contributions: PluginContribution[] = []
|
||||
const append = (type: PluginContribution['type'], values: string[]) => {
|
||||
values.forEach((id) => contributions.push({ type, id, name: id }))
|
||||
}
|
||||
append('tool', manifest.contributes.tools)
|
||||
append('command', manifest.contributes.commands)
|
||||
append('importer', manifest.contributes.importers)
|
||||
append('exporter', manifest.contributes.exporters)
|
||||
append('sidebar_panel', manifest.contributes.panels)
|
||||
append('settings_section', manifest.contributes.settings_sections)
|
||||
return {
|
||||
plugin_id: manifest.plugin_id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
description: manifest.description,
|
||||
status: plugin.status,
|
||||
enabled: plugin.enabled,
|
||||
permissions: plugin.granted_permissions,
|
||||
contributions,
|
||||
backend_type: manifest.backend.type,
|
||||
transport: manifest.backend.transport,
|
||||
last_error: plugin.error_message ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPlugin(pluginId: string): Promise<Plugin> {
|
||||
return apiClient.get(`/api/plugins/${pluginId}`)
|
||||
export async function listPlugins(): Promise<Plugin[]> {
|
||||
const response = await apiClient.get<{ items: ApiPlugin[] }>('/api/plugins')
|
||||
return response.items.map(toPlugin)
|
||||
}
|
||||
|
||||
export async function installPlugin(pluginId: string): Promise<Plugin> {
|
||||
return apiClient.post('/api/plugins/install', { plugin_id: pluginId })
|
||||
export async function getPlugin(pluginId: string): Promise<Plugin> {
|
||||
return toPlugin(await apiClient.get<ApiPlugin>(`/api/plugins/${pluginId}`))
|
||||
}
|
||||
|
||||
export async function installPlugin(packagePath: string): Promise<Plugin> {
|
||||
return toPlugin(await apiClient.post<ApiPlugin>('/api/plugins/install', { package_path: packagePath }))
|
||||
}
|
||||
|
||||
export async function enablePlugin(pluginId: string): Promise<Plugin> {
|
||||
return apiClient.post(`/api/plugins/${pluginId}/enable`)
|
||||
return toPlugin(await apiClient.post<ApiPlugin>(`/api/plugins/${pluginId}/enable`))
|
||||
}
|
||||
|
||||
export async function disablePlugin(pluginId: string): Promise<Plugin> {
|
||||
return apiClient.post(`/api/plugins/${pluginId}/disable`)
|
||||
return toPlugin(await apiClient.post<ApiPlugin>(`/api/plugins/${pluginId}/disable`))
|
||||
}
|
||||
|
||||
export async function uninstallPlugin(pluginId: string): Promise<void> {
|
||||
export async function grantPluginPermissions(pluginId: string, permissions: string[]): Promise<Plugin> {
|
||||
return toPlugin(await apiClient.put<ApiPlugin>(`/api/plugins/${pluginId}/permissions`, { permissions }))
|
||||
}
|
||||
|
||||
export async function uninstallPlugin(pluginId: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/plugins/${pluginId}`)
|
||||
}
|
||||
|
||||
@@ -80,7 +108,7 @@ export const mockPlugins: Plugin[] = [
|
||||
contributions: [
|
||||
{ type: 'sidebar_panel', id: 'kanban.panel', name: '任务看板', description: '以看板方式查看和管理任务' },
|
||||
],
|
||||
backend_type: 'internal',
|
||||
backend_type: 'internal_rpc',
|
||||
},
|
||||
{
|
||||
plugin_id: 'pdf-importer',
|
||||
@@ -114,6 +142,6 @@ export const mockPlugins: Plugin[] = [
|
||||
{ type: 'sidebar_panel', id: 'calendar.widget', name: '日历小部件', description: '侧边栏日历视图' },
|
||||
],
|
||||
backend_type: 'mcp',
|
||||
transport: 'websocket',
|
||||
transport: 'http',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,36 +1,67 @@
|
||||
import apiClient from './apiClient'
|
||||
import type { ProviderConfig, ModelInfo } from '@/contracts'
|
||||
import type { ApiModelInfo, ApiProviderConfig, ModelCapability, ModelInfo, OperationResponse, ProviderConfig } from '@/contracts'
|
||||
|
||||
export async function listProviders(): Promise<ProviderConfig[]> {
|
||||
try {
|
||||
return await apiClient.get('/api/providers')
|
||||
} catch {
|
||||
return mockProviders
|
||||
function capabilityMap(capabilities: string[]): Partial<ModelCapability> {
|
||||
return Object.fromEntries(capabilities.map((capability) => [capability, true])) as Partial<ModelCapability>
|
||||
}
|
||||
|
||||
function toProvider(provider: ApiProviderConfig): ProviderConfig {
|
||||
return {
|
||||
provider_id: provider.provider_id,
|
||||
provider_type: provider.provider_type,
|
||||
name: provider.name,
|
||||
base_url: provider.base_url ?? undefined,
|
||||
default_model: provider.default_model ?? '',
|
||||
enabled: provider.enabled,
|
||||
capabilities: capabilityMap(provider.capabilities),
|
||||
credential_id: provider.credential_id ?? undefined,
|
||||
has_credential: Boolean(provider.credential_id) || provider.provider_type === 'mock',
|
||||
}
|
||||
}
|
||||
|
||||
function toModel(model: ApiModelInfo): ModelInfo {
|
||||
return { model_id: model.model, name: model.display_name, capabilities: capabilityMap(model.capabilities) }
|
||||
}
|
||||
|
||||
export async function listProviders(): Promise<ProviderConfig[]> {
|
||||
const response = await apiClient.get<{ items: ApiProviderConfig[] }>('/api/providers')
|
||||
return response.items.map(toProvider)
|
||||
}
|
||||
|
||||
export async function getProvider(providerId: string): Promise<ProviderConfig> {
|
||||
return apiClient.get(`/api/providers/${providerId}`)
|
||||
return toProvider(await apiClient.get<ApiProviderConfig>(`/api/providers/${providerId}`))
|
||||
}
|
||||
|
||||
export async function createProvider(data: Omit<ProviderConfig, 'provider_id'> & { api_key?: string }): Promise<ProviderConfig> {
|
||||
return apiClient.post('/api/providers', data)
|
||||
export async function createProvider(data: Omit<ProviderConfig, 'provider_id'>): Promise<ProviderConfig> {
|
||||
const response = await apiClient.post<ApiProviderConfig>('/api/providers', {
|
||||
provider_type: data.provider_type,
|
||||
name: data.name,
|
||||
base_url: data.base_url,
|
||||
default_model: data.default_model || null,
|
||||
credential_id: data.credential_id,
|
||||
enabled: data.enabled,
|
||||
})
|
||||
return toProvider(response)
|
||||
}
|
||||
|
||||
export async function updateProvider(providerId: string, data: Partial<ProviderConfig> & { api_key?: string }): Promise<ProviderConfig> {
|
||||
return apiClient.patch(`/api/providers/${providerId}`, data)
|
||||
export async function updateProvider(providerId: string, data: Partial<ProviderConfig>): Promise<ProviderConfig> {
|
||||
const response = await apiClient.patch<ApiProviderConfig>(`/api/providers/${providerId}`, {
|
||||
name: data.name,
|
||||
base_url: data.base_url,
|
||||
default_model: data.default_model,
|
||||
credential_id: data.credential_id,
|
||||
enabled: data.enabled,
|
||||
})
|
||||
return toProvider(response)
|
||||
}
|
||||
|
||||
export async function deleteProvider(providerId: string): Promise<void> {
|
||||
export async function deleteProvider(providerId: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/providers/${providerId}`)
|
||||
}
|
||||
|
||||
export async function listModels(providerId: string): Promise<ModelInfo[]> {
|
||||
try {
|
||||
return await apiClient.get(`/api/providers/${providerId}/models`)
|
||||
} catch {
|
||||
return mockModels[providerId] || []
|
||||
}
|
||||
const response = await apiClient.get<{ provider_id: string; items: ApiModelInfo[] }>(`/api/providers/${providerId}/models`)
|
||||
return response.items.map(toModel)
|
||||
}
|
||||
|
||||
export interface TestResult {
|
||||
@@ -42,8 +73,8 @@ export interface TestResult {
|
||||
|
||||
export async function testProvider(providerId: string): Promise<TestResult> {
|
||||
try {
|
||||
const result = await apiClient.post<{ success: boolean; latency_ms: number }>('/api/providers/test', { provider_id: providerId })
|
||||
return { success: result.success, latency_ms: result.latency_ms }
|
||||
const result = await apiClient.post<{ success: boolean; latency_ms?: number | null; message: string }>('/api/providers/test', { provider_id: providerId })
|
||||
return { success: result.success, latency_ms: result.latency_ms ?? undefined, error_message: result.success ? undefined : result.message }
|
||||
} catch (e: any) {
|
||||
return { success: false, error_code: e.code || 'TEST_FAILED', error_message: e.message }
|
||||
}
|
||||
@@ -51,7 +82,7 @@ export async function testProvider(providerId: string): Promise<TestResult> {
|
||||
|
||||
export const mockProviders: ProviderConfig[] = [
|
||||
{
|
||||
provider_id: 'mock-provider',
|
||||
provider_id: 'mock',
|
||||
provider_type: 'mock',
|
||||
name: 'Mock Provider (测试)',
|
||||
default_model: 'mock-1',
|
||||
@@ -69,7 +100,7 @@ export const mockProviders: ProviderConfig[] = [
|
||||
},
|
||||
{
|
||||
provider_id: 'openai-compat-1',
|
||||
provider_type: 'openai-compatible',
|
||||
provider_type: 'openai_compatible',
|
||||
name: 'OpenAI 兼容服务',
|
||||
base_url: 'https://api.openai.com/v1',
|
||||
default_model: 'gpt-4o-mini',
|
||||
@@ -106,7 +137,7 @@ export const mockProviders: ProviderConfig[] = [
|
||||
]
|
||||
|
||||
export const mockModels: Record<string, ModelInfo[]> = {
|
||||
'mock-provider': [
|
||||
mock: [
|
||||
{
|
||||
model_id: 'mock-1',
|
||||
name: 'Mock Model v1',
|
||||
|
||||
@@ -1,15 +1,45 @@
|
||||
import apiClient from './apiClient'
|
||||
import type { SearchRequest, SearchResult } from '@/contracts'
|
||||
import type { ApiSearchResult, PageMeta, SearchRequest, SearchResult } from '@/contracts'
|
||||
|
||||
export async function search(request: SearchRequest): Promise<{
|
||||
results: SearchResult[]
|
||||
total: number
|
||||
mode: SearchRequest['mode']
|
||||
}> {
|
||||
return apiClient.post('/api/search', request)
|
||||
const response = await apiClient.post<{
|
||||
query: string
|
||||
mode: 'fts' | 'vector' | 'hybrid'
|
||||
items: ApiSearchResult[]
|
||||
page: PageMeta
|
||||
}>('/api/search', {
|
||||
query: request.query,
|
||||
mode: request.mode ?? 'hybrid',
|
||||
folders: request.folder ? [request.folder] : [],
|
||||
note_ids: request.note_id ? [request.note_id] : [],
|
||||
tags: request.tag ? [request.tag] : [],
|
||||
limit: request.limit ?? 20,
|
||||
offset: request.offset ?? 0,
|
||||
})
|
||||
return {
|
||||
results: response.items.map((item) => ({
|
||||
block_id: item.block_id,
|
||||
note_id: item.note_id,
|
||||
note_title: item.title,
|
||||
file_path: item.file_path,
|
||||
heading_path: item.heading_path.join(' / '),
|
||||
snippet: item.snippet ?? '',
|
||||
score: item.score,
|
||||
match_type: response.mode,
|
||||
})),
|
||||
total: response.page.total,
|
||||
mode: response.mode,
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchMock(query: string, mode = 'hybrid' as const): Promise<{
|
||||
export async function searchMock(
|
||||
query: string,
|
||||
mode: 'fts' | 'vector' | 'hybrid' = 'hybrid'
|
||||
): Promise<{
|
||||
results: SearchResult[]
|
||||
total: number
|
||||
mode: 'fts' | 'vector' | 'hybrid'
|
||||
|
||||
@@ -1,31 +1,45 @@
|
||||
import apiClient from './apiClient'
|
||||
import type { Skill } from '@/contracts'
|
||||
import type { ApiSkill, OperationResponse, Skill } from '@/contracts'
|
||||
|
||||
export async function listSkills(): Promise<Skill[]> {
|
||||
try {
|
||||
return await apiClient.get('/api/skills')
|
||||
} catch {
|
||||
return mockSkills
|
||||
function toSkill(skill: ApiSkill): Skill {
|
||||
const { manifest } = skill
|
||||
return {
|
||||
skill_id: manifest.skill_id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
description: manifest.description,
|
||||
permissions: manifest.permissions,
|
||||
tools: manifest.tools,
|
||||
retrieval_config: manifest.retrieval,
|
||||
model_requirements: { capabilities: manifest.model.required_capabilities },
|
||||
status: skill.status,
|
||||
missing_dependencies: skill.missing_dependencies,
|
||||
enabled: skill.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSkill(skillId: string): Promise<Skill> {
|
||||
return apiClient.get(`/api/skills/${skillId}`)
|
||||
export async function listSkills(): Promise<Skill[]> {
|
||||
const response = await apiClient.get<{ items: ApiSkill[] }>('/api/skills')
|
||||
return response.items.map(toSkill)
|
||||
}
|
||||
|
||||
export async function installSkill(skillId: string): Promise<Skill> {
|
||||
return apiClient.post('/api/skills/install', { skill_id: skillId })
|
||||
export async function getSkill(skillId: string): Promise<Skill> {
|
||||
return toSkill(await apiClient.get<ApiSkill>(`/api/skills/${skillId}`))
|
||||
}
|
||||
|
||||
export async function installSkill(packagePath: string): Promise<Skill> {
|
||||
return toSkill(await apiClient.post<ApiSkill>('/api/skills/install', { package_path: packagePath }))
|
||||
}
|
||||
|
||||
export async function enableSkill(skillId: string): Promise<Skill> {
|
||||
return apiClient.post(`/api/skills/${skillId}/enable`)
|
||||
return toSkill(await apiClient.post<ApiSkill>(`/api/skills/${skillId}/enable`))
|
||||
}
|
||||
|
||||
export async function disableSkill(skillId: string): Promise<Skill> {
|
||||
return apiClient.post(`/api/skills/${skillId}/disable`)
|
||||
return toSkill(await apiClient.post<ApiSkill>(`/api/skills/${skillId}/disable`))
|
||||
}
|
||||
|
||||
export async function uninstallSkill(skillId: string): Promise<void> {
|
||||
export async function uninstallSkill(skillId: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/skills/${skillId}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { resolveApiUrl } from './apiClient'
|
||||
|
||||
export type SseEventHandler = (event: string, data: Record<string, unknown>) => void
|
||||
|
||||
export interface SseClientOptions {
|
||||
@@ -37,7 +39,7 @@ export class SseClient {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const resp = await fetch(url, {
|
||||
const resp = await fetch(resolveApiUrl(url), {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
@@ -53,6 +55,39 @@ export class SseClient {
|
||||
onOpen?.()
|
||||
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let eventName = 'message'
|
||||
let dataLines: string[] = []
|
||||
let doneNotified = false
|
||||
|
||||
const dispatchEvent = () => {
|
||||
if (!dataLines.length) {
|
||||
eventName = 'message'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(dataLines.join('\n')) as Record<string, unknown>
|
||||
onEvent?.(eventName, data)
|
||||
if (!doneNotified && ['Done', 'RunCompleted', 'RunFailed', 'RunCancelled'].includes(eventName)) {
|
||||
doneNotified = true
|
||||
onDone?.()
|
||||
}
|
||||
} catch (error) {
|
||||
onError?.(error instanceof Error ? error : new Error('Malformed SSE data'))
|
||||
}
|
||||
eventName = 'message'
|
||||
dataLines = []
|
||||
}
|
||||
|
||||
const consumeLine = (line: string) => {
|
||||
if (line === '') return dispatchEvent()
|
||||
if (line.startsWith(':')) return
|
||||
const separator = line.indexOf(':')
|
||||
const field = separator === -1 ? line : line.slice(0, separator)
|
||||
let fieldValue = separator === -1 ? '' : line.slice(separator + 1)
|
||||
if (fieldValue.startsWith(' ')) fieldValue = fieldValue.slice(1)
|
||||
if (field === 'event') eventName = fieldValue
|
||||
if (field === 'data') dataLines.push(fieldValue)
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await this.reader.read()
|
||||
@@ -60,39 +95,15 @@ export class SseClient {
|
||||
|
||||
this.buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
const lines = this.buffer.split('\n')
|
||||
const lines = this.buffer.split(/\r?\n/)
|
||||
this.buffer = lines.pop() || ''
|
||||
|
||||
let eventName = 'message'
|
||||
let dataStr = ''
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) {
|
||||
if (dataStr) {
|
||||
try {
|
||||
const data = JSON.parse(dataStr)
|
||||
onEvent?.(eventName, data)
|
||||
if (eventName === 'Done' || eventName === 'RunCompleted' || eventName === 'RunFailed' || eventName === 'RunCancelled') {
|
||||
onDone?.()
|
||||
}
|
||||
} catch {
|
||||
/* ignore malformed json */
|
||||
}
|
||||
eventName = 'message'
|
||||
dataStr = ''
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('event:')) {
|
||||
eventName = trimmed.slice(6).trim()
|
||||
} else if (trimmed.startsWith('data:')) {
|
||||
const d = trimmed.slice(5).trim()
|
||||
dataStr += dataStr ? '\n' + d : d
|
||||
}
|
||||
}
|
||||
lines.forEach(consumeLine)
|
||||
}
|
||||
|
||||
this.buffer += decoder.decode()
|
||||
if (this.buffer) consumeLine(this.buffer.replace(/\r$/, ''))
|
||||
dispatchEvent()
|
||||
if (!doneNotified) onDone?.()
|
||||
} catch (e) {
|
||||
if ((e as Error).name === 'AbortError') return
|
||||
onError?.(e as Error)
|
||||
|
||||
@@ -14,10 +14,10 @@ export async function getStatus(): Promise<SystemStatus> {
|
||||
return await apiClient.get<SystemStatus>('/api/status')
|
||||
} catch {
|
||||
return {
|
||||
status: 'ok',
|
||||
name: 'notes-agent',
|
||||
version: '0.1.0',
|
||||
environment: import.meta.env.DEV ? 'development' : 'production',
|
||||
ai_core_available: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,62 @@
|
||||
import apiClient from './apiClient'
|
||||
import type { TaskItem, TaskStatus, TaskPriority } from '@/contracts'
|
||||
import type { ApiTask, OperationResponse, PageMeta, TaskItem, TaskStatus, TaskPriority } from '@/contracts'
|
||||
|
||||
export async function listTasks(params?: {
|
||||
status?: TaskStatus
|
||||
priority?: TaskPriority
|
||||
source?: 'user' | 'note' | 'agent'
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<{ items: TaskItem[]; total: number }> {
|
||||
try {
|
||||
return await apiClient.get('/api/tasks', { params })
|
||||
} catch {
|
||||
return { items: mockTasks, total: mockTasks.length }
|
||||
function toTask(task: ApiTask): TaskItem {
|
||||
return {
|
||||
task_id: task.task_id,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
status: task.status,
|
||||
priority: 'medium',
|
||||
due_date: task.due_at ?? undefined,
|
||||
note_id: task.note_id ?? undefined,
|
||||
source: 'user',
|
||||
created_at: task.created_at,
|
||||
updated_at: task.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listTasks(params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<{ items: TaskItem[]; total: number }> {
|
||||
const response = await apiClient.get<{ items: ApiTask[]; page: PageMeta }>('/api/tasks', {
|
||||
params: { limit: params?.limit, offset: params?.offset },
|
||||
})
|
||||
return { items: response.items.map(toTask), total: response.page.total }
|
||||
}
|
||||
|
||||
export async function getTask(taskId: string): Promise<TaskItem> {
|
||||
return apiClient.get(`/api/tasks/${taskId}`)
|
||||
return toTask(await apiClient.get<ApiTask>(`/api/tasks/${taskId}`))
|
||||
}
|
||||
|
||||
export async function createTask(data: {
|
||||
title: string
|
||||
description?: string
|
||||
priority?: TaskPriority
|
||||
due_date?: string
|
||||
note_id?: string
|
||||
}): Promise<TaskItem> {
|
||||
return apiClient.post('/api/tasks', data)
|
||||
return toTask(await apiClient.post<ApiTask>('/api/tasks', {
|
||||
title: data.title,
|
||||
description: data.description ?? '',
|
||||
due_at: data.due_date,
|
||||
note_id: data.note_id,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function updateTask(
|
||||
taskId: string,
|
||||
data: Partial<Pick<TaskItem, 'title' | 'description' | 'status' | 'priority' | 'due_date'>>
|
||||
): Promise<TaskItem> {
|
||||
return apiClient.patch(`/api/tasks/${taskId}`, data)
|
||||
return toTask(await apiClient.patch<ApiTask>(`/api/tasks/${taskId}`, {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
status: data.status,
|
||||
due_at: data.due_date,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function deleteTask(taskId: string): Promise<void> {
|
||||
export async function deleteTask(taskId: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -209,13 +209,13 @@ export function saveFileContent(filePath: string, content: string): Promise<void
|
||||
}
|
||||
|
||||
export function createFile(folderPath: string, name: string, content = ''): Promise<FileNode> {
|
||||
const path = `${folderPath}/${name}`
|
||||
const path = `${folderPath === '/' ? '' : folderPath}/${name}`
|
||||
const id = `n-${Date.now()}`
|
||||
return Promise.resolve({ id, name, path, type: 'file' })
|
||||
}
|
||||
|
||||
export function createFolder(parentPath: string, name: string): Promise<FileNode> {
|
||||
const path = `${parentPath}/${name}`
|
||||
const path = `${parentPath === '/' ? '' : parentPath}/${name}`
|
||||
const id = `f-${Date.now()}`
|
||||
return Promise.resolve({ id, name, path, type: 'folder', is_open: true, children: [] })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user