feat(frontend): 搭建桌面端基础界面与 Workspace

- App Shell 壳层:主导航、次导航、Title Bar、状态栏
- Vault 入口页:最近 Vault、打开/创建 Vault、AI Core 状态
- Workspace 与文件树:浏览、打开、创建笔记/文件夹
- Design Token:浅色/深色主题 CSS Variables
- Contracts / Service / Store / Router 基础架构
- 统一 ApiClient 与 SseClient,对接后端 API 契约
This commit is contained in:
2026-08-29 10:36:16 +08:00
parent 694c1b27c1
commit f9efc4f4a2
42 changed files with 8044 additions and 167 deletions
+145
View File
@@ -0,0 +1,145 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { AgentRun, AgentEvent, ToolDefinition, PermissionRequest, ToolCall } from '@/contracts'
import { mockAgentRuns, mockAgentEvents, mockTools, mockPermissionRequest } from '@/services/agentService'
import * as agentService from '@/services/agentService'
export const useAgentStore = defineStore('agent', () => {
const runs = ref<AgentRun[]>(mockAgentRuns)
const activeRunId = ref<string | null>('run-1')
const events = ref<AgentEvent[]>(mockAgentEvents.filter((e) => e.run_id === 'run-1'))
const tools = ref<ToolDefinition[]>(mockTools)
const isCreating = ref(false)
const isRunning = ref(false)
const permissionRequest = ref<PermissionRequest | null>(null)
const toolCalls = ref<ToolCall[]>([])
const activeRun = computed(() =>
runs.value.find((r) => r.run_id === activeRunId.value) || null
)
const sortedRuns = computed(() =>
[...runs.value].sort((a, b) => (b.started_at || '').localeCompare(a.started_at || ''))
)
const currentStep = computed(() => {
const tc = events.value.filter((e) => e.event === 'ToolCall').length
return tc
})
async function loadTools() {
tools.value = await agentService.listTools()
}
async function loadRuns() {
const resp = await agentService.listAgentRuns()
runs.value = resp.items
}
async function loadRun(runId: string) {
activeRunId.value = runId
events.value = mockAgentEvents.filter((e) => e.run_id === runId)
toolCalls.value = []
for (const evt of events.value) {
if (evt.event === 'ToolCall') {
const data = evt.data as any
toolCalls.value.push({
tool_call_id: data.tool_call_id,
name: data.name,
parameters: data.parameters,
status: data.status || 'completed',
started_at: evt.timestamp,
})
} else if (evt.event === 'ToolResult') {
const data = evt.data as any
const tc = toolCalls.value.find((t) => t.tool_call_id === data.tool_call_id)
if (tc) {
tc.status = data.status
tc.result = data.result
tc.completed_at = evt.timestamp
}
}
}
}
async function createRun(request: agentService.CreateAgentRunRequest) {
isCreating.value = true
try {
const run = await agentService.createAgentRun(request)
runs.value.unshift(run)
activeRunId.value = run.run_id
events.value = [{
event: 'RunStarted',
sequence: 1,
run_id: run.run_id,
data: { task: request.task },
timestamp: new Date().toISOString(),
}]
isRunning.value = true
// Mock events streaming
simulateRun(run.run_id)
return run
} finally {
isCreating.value = false
}
}
function simulateRun(runId: string) {
const runEvents: AgentEvent[] = [
{ event: 'ThinkingDelta', sequence: 2, run_id: runId, data: { text: '我需要先搜索相关笔记...' }, timestamp: new Date().toISOString() },
{ event: 'ToolCall', sequence: 3, run_id: runId, data: { tool_call_id: 'tc-mock-1', name: 'notes.search', parameters: { query: '红黑树', limit: 5 }, status: 'running' }, timestamp: new Date().toISOString() },
{ event: 'ToolResult', sequence: 4, run_id: runId, data: { tool_call_id: 'tc-mock-1', name: 'notes.search', status: 'completed', result: '找到 5 条相关结果' }, timestamp: new Date().toISOString() },
{ event: 'TextDelta', sequence: 5, run_id: runId, data: { text: '根据你的笔记,以下是...' }, timestamp: new Date().toISOString() },
{ event: 'RunCompleted', sequence: 6, run_id: runId, data: { message: 'Task completed successfully' }, timestamp: new Date().toISOString() },
]
let idx = 0
const push = () => {
if (idx >= runEvents.length) {
isRunning.value = false
return
}
events.value.push(runEvents[idx])
idx++
setTimeout(push, 800)
}
setTimeout(push, 500)
}
async function cancelRun(runId: string) {
await agentService.cancelAgentRun(runId)
const run = runs.value.find((r) => r.run_id === runId)
if (run) run.status = 'cancelled'
isRunning.value = false
}
async function respondPermission(decision: 'allow' | 'deny', scope: 'once' | 'session' | 'always' = 'once') {
if (!activeRunId.value || !permissionRequest.value) return
await agentService.respondToPermission(activeRunId.value, permissionRequest.value.request_id, decision, scope)
permissionRequest.value = null
}
function showPermissionDemo() {
permissionRequest.value = mockPermissionRequest
}
return {
runs,
activeRunId,
activeRun,
sortedRuns,
events,
tools,
isCreating,
isRunning,
permissionRequest,
toolCalls,
currentStep,
loadTools,
loadRuns,
loadRun,
createRun,
cancelRun,
respondPermission,
showPermissionDemo,
}
})