fix(frontend): 修复合并审阅发现的构建与契约问题

恢复 Vue TypeScript 生产构建,补齐可运行页面壳子,并修复文件树与 SSE 状态问题。

按 FastAPI Wire Contract 统一 Service DTO 映射,同时补充前端开发说明和问题修复复盘。
This commit is contained in:
2026-08-29 12:10:33 +08:00
parent c6c28e4ebe
commit 610fc77b0f
32 changed files with 1204 additions and 611 deletions
+4 -3
View File
@@ -72,7 +72,7 @@ export const useAgentStore = defineStore('agent', () => {
event: 'RunStarted',
sequence: 1,
run_id: run.run_id,
data: { task: request.task },
data: { input: request.input },
timestamp: new Date().toISOString(),
}]
isRunning.value = true
@@ -112,9 +112,10 @@ export const useAgentStore = defineStore('agent', () => {
isRunning.value = false
}
async function respondPermission(decision: 'allow' | 'deny', scope: 'once' | 'session' | 'always' = 'once') {
async function respondPermission(decision: 'allow' | 'deny', scope: 'once' | 'session' = 'once') {
if (!activeRunId.value || !permissionRequest.value) return
await agentService.respondToPermission(activeRunId.value, permissionRequest.value.request_id, decision, scope)
const apiDecision = decision === 'deny' ? 'deny' : scope === 'session' ? 'allow_session' : 'allow_once'
await agentService.respondToPermission(activeRunId.value, permissionRequest.value.request_id, apiDecision)
permissionRequest.value = null
}
+25 -27
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { ChatMessage, Conversation, Citation } from '@/contracts'
import { mockConversations, mockMessages } from '@/services/chatService'
import type { ChatMessage, Conversation } from '@/contracts'
import { mockConversations, mockMessages, streamChat } from '@/services/chatService'
import type { SseClient } from '@/services/sseClient'
export const useChatStore = defineStore('chat', () => {
@@ -12,7 +12,7 @@ export const useChatStore = defineStore('chat', () => {
const inputText = ref('')
const useRag = ref(true)
const selectedSkillId = ref<string | null>(null)
const selectedProviderId = ref('mock-provider')
const selectedProviderId = ref('mock')
const selectedModel = ref('mock-1')
let sseClient: SseClient | null = null
@@ -35,7 +35,7 @@ export const useChatStore = defineStore('chat', () => {
if (!activeConversationId.value) {
const newConv: Conversation = {
conversation_id,
conversation_id: conversationId,
title: text.slice(0, 30),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
@@ -67,31 +67,29 @@ export const useChatStore = defineStore('chat', () => {
}
messages.value.push(aiMsg)
// Mock streaming
const fullText =
'这是一个模拟的 AI 回复。在实际环境中,这里会通过 SSE 接收后端 AI Core 的流式输出,基于 RAG 引擎和你的知识库生成回答,并附带来源引用。\n\n**要点总结:**\n1. 这是演示用的流式输出\n2. 实际会调用 ModelEvent SSE\n3. 支持 Citation、Tool Call 等事件\n\n你可以在设置中配置真实的模型 Provider 来启用完整功能。'
const citations: Citation[] = [
{
note_id: 'n-rbt',
block_id: 'b1',
file_path: '/数据结构/红黑树.md',
heading_path: '数据结构 / 红黑树 / 概述',
content: '红黑树是一种自平衡二叉搜索树...',
sseClient = streamChat({
provider_id: selectedProviderId.value,
model: selectedModel.value,
conversation_id: conversationId,
use_rag: useRag.value,
messages: messages.value
.filter((message) => message !== aiMsg)
.map((message) => ({ role: message.role, content: message.content })),
}, {
onEvent(event) {
if (event.event === 'TextDelta') aiMsg.content += String(event.data.text ?? '')
if (event.event === 'Error') aiMsg.content += `\n\n生成失败:${String(event.data.message ?? '未知错误')}`
},
]
let i = 0
const interval = setInterval(() => {
if (i >= fullText.length) {
clearInterval(interval)
onError(error) {
aiMsg.content += `\n\n连接失败:${error.message}`
isStreaming.value = false
aiMsg.citations = citations
return
}
const chunk = fullText.slice(i, i + 3)
aiMsg.content += chunk
i += 3
}, 20)
sseClient = null
},
onDone() {
isStreaming.value = false
sseClient = null
},
})
}
function stopGeneration() {
+3 -5
View File
@@ -1,12 +1,12 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { ProviderConfig, ModelInfo } from '@/contracts'
import { mockProviders, mockModels } from '@/services/providerService'
import { listModels, listProviders, mockProviders, mockModels } from '@/services/providerService'
export const useProviderStore = defineStore('provider', () => {
const providers = ref<ProviderConfig[]>(mockProviders)
const modelsByProvider = ref<Record<string, ModelInfo[]>>(mockModels)
const defaultProviderId = ref('mock-provider')
const defaultProviderId = ref('mock')
const isLoading = ref(false)
const enabledProviders = computed(() => providers.value.filter((p) => p.enabled))
@@ -17,7 +17,6 @@ export const useProviderStore = defineStore('provider', () => {
async function loadProviders() {
isLoading.value = true
try {
const { listProviders } = await import('@/services/providerService')
providers.value = await listProviders()
} finally {
isLoading.value = false
@@ -25,11 +24,10 @@ export const useProviderStore = defineStore('provider', () => {
}
async function loadModels(providerId: string) {
const { listModels } = await import('@/services/providerService')
modelsByProvider.value[providerId] = await listModels(providerId)
}
async function addProvider(data: Omit<ProviderConfig, 'provider_id'> & { api_key?: string }) {
async function addProvider(data: Omit<ProviderConfig, 'provider_id'>) {
const newProvider: ProviderConfig = {
...data,
provider_id: `prov-${Date.now()}`,
+1 -1
View File
@@ -21,7 +21,7 @@ export const useSearchStore = defineStore('search', () => {
error.value = null
try {
const resp = await searchService.searchMock(request.query, request.mode || 'hybrid')
const resp = await searchService.search(request)
results.value = resp.results
total.value = resp.total
selectedIndex.value = 0
+4
View File
@@ -88,6 +88,10 @@ export const useWorkspaceStore = defineStore('workspace', () => {
}
function addFileToTree(parentPath: string, file: FileNode) {
if (parentPath === '/' || parentPath === '') {
fileTree.value.push(file)
return
}
const parent = findNodeByPath(fileTree.value, parentPath)
if (parent?.children) {
parent.children.push(file)