diff --git a/design/vue/src/api/admin.ts b/design/vue/src/api/admin.ts index 0ec8823..3fb0db6 100644 --- a/design/vue/src/api/admin.ts +++ b/design/vue/src/api/admin.ts @@ -1,11 +1,12 @@ import { request } from './client' -import type { ModelInfo } from '@/types/model' +import type { ModelInfo, ModelTestResult } from '@/types/model' import type { ToolTypeInfo, ToolProviderInfo } from '@/types/tool' import type { AdminUser } from '@/types/auth' -import type { AdminSession } from '@/types/chat' +import type { AdminSession, MetricsSnapshot, TraceSummary, ChatTraceDetail } from '@/types/chat' -// ── Admin Users ── +// ── 用户管理 ── +// 分页查询用户列表 export function adminListUsers(params: { page: number pageSize: number @@ -24,6 +25,7 @@ export function adminListUsers(params: { return request<{ list: AdminUser[]; total: number; page: number; pageSize: number; pages: number }>(`/admin/users?${query.toString()}`) } +// 创建用户 export function adminCreateUser(data: { username: string email: string @@ -34,6 +36,7 @@ export function adminCreateUser(data: { return request('/admin/users', { method: 'POST', body: data }) } +// 更新用户信息 export function adminUpdateUser(id: string, data: Partial<{ username: string email: string @@ -43,29 +46,35 @@ export function adminUpdateUser(id: string, data: Partial<{ return request(`/admin/users/${id}`, { method: 'PUT', body: data }) } +// 删除用户 export function adminDeleteUser(id: string) { return request(`/admin/users/${id}`, { method: 'DELETE' }) } +// 重置用户密码 export function adminResetUserPassword(id: string, data: { password: string }) { return request(`/admin/users/${id}/reset-password`, { method: 'POST', body: data }) } -// ── Admin Models ── +// ── 模型管理 ── +// 获取模型列表 export function adminListModels() { return request<{ models: ModelInfo[] }>('/models') } +// 创建模型配置 export function adminCreateModel(data: { provider: string model_id: string base_url: string api_key: string + max_context_length: number }) { return request('/models', { method: 'POST', body: data }) } +// 更新模型配置 export function adminUpdateModel( id: string, data: Partial<{ @@ -74,15 +83,18 @@ export function adminUpdateModel( base_url: string api_key: string is_enabled: boolean + max_context_length: number }>, ) { return request(`/models/${id}`, { method: 'PUT', body: data }) } +// 删除模型 export function adminDeleteModel(id: string) { return request(`/models/${id}`, { method: 'DELETE' }) } +// 测试模型连通性 export function adminTestModel(data: { provider: string model_id: string @@ -90,21 +102,17 @@ export function adminTestModel(data: { api_key: string config?: Record }) { - return request<{ - success: boolean - message: string - error?: string - response_time_ms: number - details?: string - }>('/models/test', { method: 'POST', body: data }) + return request('/models/test', { method: 'POST', body: data }) } -// ── Admin Tools ── +// ── 工具管理 ── +// 获取工具类型列表 export function adminListToolTypes() { return request<{ tool_types: ToolTypeInfo[] }>('/admin/tool-types') } +// 创建工具类型 export function adminCreateToolType(data: { name: string tool_key: string @@ -114,6 +122,7 @@ export function adminCreateToolType(data: { return request('/admin/tool-types', { method: 'POST', body: data }) } +// 更新工具类型 export function adminUpdateToolType( id: string, data: Partial<{ @@ -127,14 +136,17 @@ export function adminUpdateToolType( return request(`/admin/tool-types/${id}`, { method: 'PUT', body: data }) } +// 删除工具类型 export function adminDeleteToolType(id: string) { return request(`/admin/tool-types/${id}`, { method: 'DELETE' }) } +// 获取指定工具类型下的供应商列表 export function adminListToolProviders(toolTypeId: string) { return request<{ providers: ToolProviderInfo[] }>(`/admin/tool-types/${toolTypeId}/providers`) } +// 创建工具供应商 export function adminCreateToolProvider( toolTypeId: string, data: { @@ -154,6 +166,7 @@ export function adminCreateToolProvider( }) } +// 更新工具供应商 export function adminUpdateToolProvider( toolTypeId: string, providerId: string, @@ -175,12 +188,14 @@ export function adminUpdateToolProvider( }) } +// 删除工具供应商 export function adminDeleteToolProvider(toolTypeId: string, providerId: string) { return request(`/admin/tool-types/${toolTypeId}/providers/${providerId}`, { method: 'DELETE', }) } +// 测试工具连通性 export function adminTestTool(data: { provider_type: string provider_config?: Record @@ -197,8 +212,9 @@ export function adminTestTool(data: { }>('/admin/tools/test', { method: 'POST', body: data }) } -// ── Admin Sessions ── +// ── 会话管理 ── +// 分页查询会话列表 export function adminListSessions(params: { page: number pageSize: number @@ -213,10 +229,41 @@ export function adminListSessions(params: { return request<{ list: AdminSession[]; total: number; page: number; pageSize: number; pages: number }>(`/admin/sessions?${query.toString()}`) } +// 删除会话 export function adminDeleteSession(id: string) { return request(`/admin/sessions/${id}`, { method: 'DELETE' }) } +// 清理过期会话 export function adminCleanupSessions() { return request<{ deleted: number }>('/admin/sessions/cleanup', { method: 'POST' }) } + +// ── 可观测性(后台) ── + +// 获取可观测性指标快照 +export function adminGetObservabilityMetrics() { + return request('/admin/observability/metrics') +} + +// 分页查询 Trace 列表 +export function adminListTraces(params: { + page?: number + pageSize?: number + session_id?: string + status?: string + rating?: number +}) { + const query = new URLSearchParams() + if (params.page !== undefined) query.set('page', String(params.page)) + if (params.pageSize !== undefined) query.set('pageSize', String(params.pageSize)) + if (params.session_id) query.set('session_id', params.session_id) + if (params.status) query.set('status', params.status) + if (params.rating !== undefined) query.set('rating', String(params.rating)) + return request<{ traces: TraceSummary[]; total: number }>(`/admin/observability/traces?${query.toString()}`) +} + +// 获取 Trace 详情 +export function adminGetTrace(traceId: string) { + return request(`/admin/observability/traces/${traceId}`) +} diff --git a/design/vue/src/api/chat.ts b/design/vue/src/api/chat.ts index 1f6bf43..2b7cf31 100644 --- a/design/vue/src/api/chat.ts +++ b/design/vue/src/api/chat.ts @@ -1,4 +1,4 @@ -import { request, streamRequest } from './client' +import { request, streamRequest, StreamReaderWithMeta } from './client' import type { ChatSession, CreateSessionRequest, @@ -6,6 +6,10 @@ import type { SendMessageRequest, ListSessionsResponse, ListMessagesResponse, + FeedbackRequest, + FeedbackInfo, + TraceSummary, + ChatTraceDetail, } from '@/types/chat' // ── Sessions ── @@ -40,6 +44,33 @@ export function getMessages(sessionId: string) { } /** Send message via SSE streaming. Returns a ReadableStream reader. */ -export function sendMessage(sessionId: string, data: SendMessageRequest, signal?: AbortSignal) { +export function sendMessage(sessionId: string, data: SendMessageRequest, signal?: AbortSignal): Promise { return streamRequest(`/chat/sessions/${sessionId}/messages`, data, signal) } + +// ── Feedback ── + +export function submitFeedback(messageId: string, data: FeedbackRequest) { + return request(`/chat/messages/${messageId}/feedback`, { method: 'POST', body: data }) +} + +export function listFeedbacks(params?: { page?: number; pageSize?: number; rating?: number }) { + const query = new URLSearchParams() + if (params?.page !== undefined) query.set('page', String(params.page)) + if (params?.pageSize !== undefined) query.set('pageSize', String(params.pageSize)) + if (params?.rating !== undefined) query.set('rating', String(params.rating)) + return request<{ feedbacks: FeedbackInfo[]; total: number }>(`/chat/feedbacks?${query.toString()}`) +} + +// ── Traces ── + +export function getTrace(traceId: string) { + return request(`/chat/traces/${traceId}`) +} + +export function listSessionTraces(sessionId: string, params?: { page?: number; pageSize?: number }) { + const query = new URLSearchParams() + if (params?.page !== undefined) query.set('page', String(params.page)) + if (params?.pageSize !== undefined) query.set('pageSize', String(params.pageSize)) + return request<{ traces: TraceSummary[]; total: number }>(`/chat/sessions/${sessionId}/traces?${query.toString()}`) +} diff --git a/design/vue/src/api/client.ts b/design/vue/src/api/client.ts index 0643bc0..190be2a 100644 --- a/design/vue/src/api/client.ts +++ b/design/vue/src/api/client.ts @@ -73,7 +73,10 @@ export async function request( throw new Error(text || `HTTP ${res.status}`) } - const data = await res.json() + const traceId = res.headers.get('X-Trace-ID') || undefined + const requestId = res.headers.get('X-Request-ID') || undefined + + const data = (await res.json()) as ApiResponse // Server-side token invalid / auth error if (data.code === 401 || data.code === 403) { removeToken() @@ -84,6 +87,9 @@ export async function request( if (data.code !== 0) { throw new Error(data.message || '请求失败') } + if (traceId || requestId) { + data._meta = { trace_id: traceId, request_id: requestId } + } return data } @@ -120,7 +126,10 @@ export async function formRequest( throw new Error(text || `HTTP ${res.status}`) } - const data = await res.json() + const traceId = res.headers.get('X-Trace-ID') || undefined + const requestId = res.headers.get('X-Request-ID') || undefined + + const data = (await res.json()) as ApiResponse if (data.code === 401 || data.code === 403) { removeToken() routerInstance?.push('/login') @@ -129,6 +138,9 @@ export async function formRequest( if (data.code !== 0) { throw new Error(data.message || '请求失败') } + if (traceId || requestId) { + data._meta = { trace_id: traceId, request_id: requestId } + } return data } @@ -151,12 +163,16 @@ export async function blobRequest(path: string): Promise { return res.blob() } +export type StreamReaderWithMeta = ReadableStreamDefaultReader & { + _meta?: { trace_id?: string; request_id?: string } +} + /** SSE stream request — returns a ReadableStream reader */ export async function streamRequest( path: string, body: unknown, signal?: AbortSignal, -): Promise> { +): Promise { const headers: Record = { 'Content-Type': 'application/json', } @@ -183,5 +199,11 @@ export async function streamRequest( throw new Error(text || `HTTP ${res.status}`) } - return res.body!.getReader() + const reader = res.body!.getReader() as StreamReaderWithMeta + const traceId = res.headers.get('X-Trace-ID') || undefined + const requestId = res.headers.get('X-Request-ID') || undefined + if (traceId || requestId) { + reader._meta = { trace_id: traceId, request_id: requestId } + } + return reader } diff --git a/design/vue/src/api/model.ts b/design/vue/src/api/model.ts index 4af482f..1e96fe5 100644 --- a/design/vue/src/api/model.ts +++ b/design/vue/src/api/model.ts @@ -8,40 +8,49 @@ import type { CreateUserModelConfigRequest, UpdateUserModelConfigRequest, ListUserModelConfigsResponse, + ModelTestResult, } from '@/types/model' -// ── System Models ── +// ── 系统模型 ── +// 获取模型列表 export function listModels() { return request('/models') } +// 获取单个模型详情 export function getModel(id: string) { return request(`/models/${id}`) } +// 创建模型 export function createModel(data: CreateModelRequest) { return request('/models', { method: 'POST', body: data }) } +// 更新模型 export function updateModel(id: string, data: UpdateModelRequest) { return request(`/models/${id}`, { method: 'PUT', body: data }) } +// 删除模型 export function deleteModel(id: string) { return request(`/models/${id}`, { method: 'DELETE' }) } -// ── User Model Configs ── +// ── 用户模型配置 ── +// 获取当前用户的模型配置列表 export function listUserModelConfigs() { return request('/user/model-configs') } +// 获取用户模型配置详情 export function getUserModelConfig(id: string) { return request(`/user/model-configs/${id}`) } +// 创建用户模型配置 export function createUserModelConfig(data: CreateUserModelConfigRequest) { return request('/user/model-configs', { method: 'POST', @@ -49,6 +58,7 @@ export function createUserModelConfig(data: CreateUserModelConfigRequest) { }) } +// 更新用户模型配置 export function updateUserModelConfig( id: string, data: UpdateUserModelConfigRequest, @@ -59,10 +69,12 @@ export function updateUserModelConfig( }) } +// 删除用户模型配置 export function deleteUserModelConfig(id: string) { return request(`/user/model-configs/${id}`, { method: 'DELETE' }) } +// 测试用户模型配置连通性 export function testUserModelConfig(data: { provider: string model_id: string @@ -70,11 +82,5 @@ export function testUserModelConfig(data: { api_key: string config?: Record }) { - return request<{ - success: boolean - message: string - error?: string - response_time_ms: number - details?: string - }>('/user/model-configs/test', { method: 'POST', body: data }) + return request('/user/model-configs/test', { method: 'POST', body: data }) } diff --git a/design/vue/src/components/StatCard.vue b/design/vue/src/components/StatCard.vue new file mode 100644 index 0000000..409b218 --- /dev/null +++ b/design/vue/src/components/StatCard.vue @@ -0,0 +1,35 @@ + + + diff --git a/design/vue/src/components/TraceRootProvider.vue b/design/vue/src/components/TraceRootProvider.vue new file mode 100644 index 0000000..a640fe9 --- /dev/null +++ b/design/vue/src/components/TraceRootProvider.vue @@ -0,0 +1,19 @@ + + + diff --git a/design/vue/src/components/TraceSpanNode.vue b/design/vue/src/components/TraceSpanNode.vue new file mode 100644 index 0000000..7e94c12 --- /dev/null +++ b/design/vue/src/components/TraceSpanNode.vue @@ -0,0 +1,154 @@ + + + diff --git a/design/vue/src/composables/useAuth.ts b/design/vue/src/composables/useAuth.ts index 9a351af..cb109a7 100644 --- a/design/vue/src/composables/useAuth.ts +++ b/design/vue/src/composables/useAuth.ts @@ -142,8 +142,10 @@ export function useAuth() { if (!hasToken()) return try { const res = await authApi.getProfile() - if (res.code === 0 && res.data) { - currentUser.value = res.data + // 后端 /user/profile 返回 { user: UserInfo, preference: {...} },只取 user 这层扁平结构 + const userInfo = res.data?.user ?? res.data + if (res.code === 0 && userInfo) { + currentUser.value = userInfo } } catch { removeToken() diff --git a/design/vue/src/composables/useChat.ts b/design/vue/src/composables/useChat.ts index cddccd7..2224ced 100644 --- a/design/vue/src/composables/useChat.ts +++ b/design/vue/src/composables/useChat.ts @@ -6,17 +6,19 @@ import * as chatApi from '@/api/chat' import * as modelApi from '@/api/model' import * as authApi from '@/api/auth' import { request } from '@/api/client' -import type { ChatSession } from '@/types/chat' +import type { ChatSession, FeedbackRequest } from '@/types/chat' import type { StreamEvent } from '@/types/chat' -// ── Local types for UI display ── +// ── UI 展示用的本地类型 ── +// 推理时间线步骤 interface TimelineStep { title: string detail?: string status: string } +// 聊天消息展示结构 interface DisplayMessage { id: string role: 'user' | 'assistant' | 'error' @@ -25,51 +27,62 @@ interface DisplayMessage { retryable?: boolean sources?: StreamEvent['sources'] timeline?: TimelineStep[] + trace_id?: string + feedback_rating?: 1 | -1 + feedback_reasons?: string[] + feedback_comment?: string } +// 模型选项 interface ModelOption { id: string name: string modelType: 'system' | 'user' } +// 知识库选项 interface KnowledgeBaseOption { id: string name: string document_count?: number } -// ── Tooltip content store (avoids HTML attribute escaping issues) ── +// ── Tooltip 内容存储(避免 HTML 属性转义问题) ── const tooltipStore = new Map() +// 获取指定 key 的 tooltip 内容 export function getTooltipContent(key: string): string { return tooltipStore.get(key) ?? '' } let _tooltipSeq = 0 +// 生成下一个 tooltip key export function nextTipKey(): string { return `tip_${_tooltipSeq++}` } +// 设置 tooltip 内容 export function setTooltip(key: string, value: string): void { tooltipStore.set(key, value) } +// 清理标题中的换行 export function cleanTitle(text: string): string { if (!text) return '' return text.replace(/[\r\n]+/g, ' ').trim() } +// 聊天组合式函数 export function useChat() { const router = useRouter() const refreshHistory = inject<() => Promise>('refreshHistory') - // ── Session ── + // ── 会话状态 ── const sessions = ref([]) const activeSessionId = ref(null) - // ── Messages ── + // ── 消息状态 ── const messages = ref([]) const collapsedTimelines = ref>(new Set()) const isLoading = ref(false) @@ -78,21 +91,21 @@ export function useChat() { const streamTimeline = ref([]) const progressText = ref('') - // ── Selectors ── + // ── 选择器 ── const modelOptions = ref([]) const knowledgeBases = ref([]) const connected = ref(false) - // ── Input ── + // ── 输入状态 ── const input = ref('') const selectedModel = ref('') const selectedKBs = ref([]) const searchMode = ref<'quick' | 'smart-reasoning'>('quick') - // ── Abort control ── + // ── 中断控制 ── let abortController: AbortController | null = null - // ── Computed ── + // ── 计算属性 ── const activeSession = computed(() => sessions.value.find((s) => s.id === activeSessionId.value), ) @@ -104,7 +117,7 @@ export function useChat() { .join(', ') }) - // ── Init ── + // ── 初始化 ── async function init() { try { const [modelsRes, userModelsRes, kbRes, profileRes] = await Promise.all([ @@ -131,9 +144,9 @@ export function useChat() { } modelOptions.value = opts - // 使用用户上次选择的模型 - if (profileRes?.code === 0 && profileRes.data?.lastModel) { - selectedModel.value = profileRes.data.lastModel + // 使用用户上次选择的模型(lastModel 嵌套在 user 对象里) + if (profileRes?.code === 0 && profileRes.data?.user?.lastModel) { + selectedModel.value = profileRes.data.user.lastModel } else if (opts.length > 0) { selectedModel.value = opts[0].id } @@ -154,7 +167,8 @@ export function useChat() { } } - // ── Sessions ── + // ── 会话操作 ── + // 加载会话列表 async function loadSessions() { try { const res = await chatApi.listSessions() @@ -162,6 +176,7 @@ export function useChat() { } catch { /* silent */ } } + // 创建新会话 async function createSession(title: string): Promise { try { const res = await chatApi.createSession({ @@ -177,6 +192,7 @@ export function useChat() { return null } + // 加载指定会话的消息列表 async function loadMessages(sessionId: string) { try { const res = await chatApi.getMessages(sessionId) @@ -219,12 +235,13 @@ export function useChat() { } } + // 选中指定会话并加载消息 function selectSession(sessionId: string) { activeSessionId.value = sessionId loadMessages(sessionId) } - // ── Send Message (SSE streaming) ── + // ── 发送消息(SSE 流式) ── async function sendMessage() { const content = input.value.trim() if (!content || isLoading.value) return @@ -248,7 +265,7 @@ export function useChat() { streamContent.value = '' streamTimeline.value = [] - // Auto-create session + // 自动创建会话 if (isNewSession) { const id = await createSession(content) if (!id) { @@ -291,15 +308,17 @@ export function useChat() { try { const reader = await chatApi.sendMessage(activeSessionId.value, { - content, - model_id: selectedModel.value, - model_type: modelOpt?.modelType ?? 'system', - search_mode: searchMode.value, - knowledge_base_ids: selectedKBs.value.length ? selectedKBs.value : knowledgeBases.value.map(k => k.id), - }, abortController.signal) + content, + model_id: selectedModel.value, + model_type: modelOpt?.modelType ?? 'system', + search_mode: searchMode.value, + knowledge_base_ids: selectedKBs.value.length ? selectedKBs.value : knowledgeBases.value.map(k => k.id), + }, abortController.signal) + + const traceId = reader._meta?.trace_id - const decoder = new TextDecoder() - let buffer = '' + const decoder = new TextDecoder() + let buffer = '' while (true) { const { done, value } = await reader.read() @@ -329,7 +348,7 @@ export function useChat() { case 'tool_call': case 'tool_result': case 'warning': - // Mark all running steps as success before adding new one + // 添加新步骤前,将所有运行中的步骤标记为成功 streamTimeline.value.forEach( (s) => s.status === 'running' && (s.status = 'success'), ) @@ -413,6 +432,7 @@ export function useChat() { streamTimeline.value.length > 0 ? [...streamTimeline.value] : undefined, + trace_id: traceId, }) if (streamTimeline.value.length > 0) { collapsedTimelines.value.add(messages.value.length - 1) @@ -424,13 +444,13 @@ export function useChat() { progressText.value = '' return } - } catch { /* skip bad JSON */ } + } catch { /* 跳过非法 JSON */ } } } } catch (e: unknown) { const isAbort = e instanceof DOMException && e.name === 'AbortError' if (isAbort) { - // User aborted — save partial content / reasoning steps / sources if any + // 用户中断——保存已有的部分内容 / 推理步骤 / 来源 if (finalContent || streamContent.value || streamTimeline.value.length || streamSources.value?.length) { messages.value.push({ id: assistantId || 'a-' + Date.now(), @@ -438,6 +458,7 @@ export function useChat() { content: streamContent.value || finalContent, sources: finalSources.length ? finalSources : (streamSources.value?.length ? [...streamSources.value] : undefined), timeline: streamTimeline.value.length ? [...streamTimeline.value] : undefined, + trace_id: traceId, }) } } else { @@ -460,25 +481,29 @@ export function useChat() { } } - // ── Helpers ── + // ── 辅助方法 ── + // 滚动到底部 function scrollToBottom(el: HTMLElement | null) { nextTick(() => { if (el) el.scrollTop = el.scrollHeight }) } + // 切换知识库选中状态 function toggleKB(id: string) { const idx = selectedKBs.value.indexOf(id) if (idx >= 0) selectedKBs.value.splice(idx, 1) else selectedKBs.value.push(id) } + // 复制文本到剪贴板 function copyText(text: string) { navigator.clipboard.writeText(text) .then(() => ElMessage.success('已复制')) .catch(() => ElMessage.error('复制失败')) } + // 重新生成最后一条回复 function regenerate() { const lastIdx = messages.value.length - 1 if (lastIdx >= 0 && messages.value[lastIdx].role === 'assistant') { @@ -498,6 +523,7 @@ export function useChat() { } } + // 重试最后一条错误消息 function retryLastMessage() { // 找到最后一条错误消息 const lastErrorIdx = [...messages.value] @@ -528,13 +554,40 @@ export function useChat() { } } + // 中止当前生成 function stopGeneration() { if (abortController) { abortController.abort() } } - // ── Content formatting ── + // ── 反馈 ── + // 提交消息反馈 + async function submitFeedback( + messageId: string, + req: FeedbackRequest, + ): Promise { + const idx = messages.value.findIndex((m) => m.id === messageId) + if (idx < 0) return + const target = messages.value[idx] + try { + await chatApi.submitFeedback(messageId, req) + const next = [...messages.value] + next[idx] = { + ...target, + feedback_rating: req.rating, + feedback_reasons: req.reasons ?? [], + feedback_comment: req.comment, + } + messages.value = next + ElMessage.success(req.rating === 1 ? '感谢您的反馈' : '感谢反馈,我们会持续优化') + } catch (e: unknown) { + ElMessage.error(e instanceof Error ? e.message : '提交反馈失败') + } + } + + // ── 内容格式化 ── + // 格式化消息内容,处理引用标签 function formatContent(content: string, _sources?: unknown[]): string { if (!content) return '' @@ -599,10 +652,11 @@ export function useChat() { return html } + // 清理 tooltip 文本中的元数据标签 function cleanTooltipText(text: string): string { if (!text) return '' - // Only remove actual metadata tags; do NOT globally strip `title=` / `doc=` - // because normal article text may legitimately contain those substrings. + // 仅移除实际的元数据标签,不要全局清除 `title=` / `doc=` + // 因为正常文章文本中可能合法包含这些子串 return text .replace(/]*)?\s*\/?>\s*/gi, '') .replace(/]*)?\s*\/?>\s*/gi, '') @@ -613,7 +667,7 @@ export function useChat() { .trim() } - // Get comma-separated chunk IDs for a source document tooltip + // 获取来源文档 tooltip 所需的逗号分隔 chunk ID function getSourceChunkIds(source: any): string { if (!source) return '' const chunks = source.chunks @@ -624,7 +678,7 @@ export function useChat() { .join(',') } - // Extract web sources from content (for displaying web search links) + // 从内容中提取网页来源(用于展示联网搜索链接) function extractWebSources(content: string): { url: string; title: string }[] { const result: { url: string; title: string }[] = [] const seen = new Set() @@ -639,6 +693,7 @@ export function useChat() { return result } + // 重置聊天状态,开始新会话 function newChat() { activeSessionId.value = null messages.value = [] @@ -682,19 +737,22 @@ export function useChat() { regenerate, retryLastMessage, stopGeneration, + submitFeedback, newChat, cleanTooltipText, } } -// ── Utilities ── +// ── 工具函数 ── +// HTML 转义 function escapeHtml(s: string): string { const d = document.createElement('div') d.textContent = s return d.innerHTML } +// HTML 属性转义 function escapeAttr(s: string): string { return s .replace(/&/g, '&') @@ -704,7 +762,8 @@ function escapeAttr(s: string): string { .replace(/\n/g, ' ') } +// 数字转圆圈数字符号 function toCircleNum(n: number): string { - const c = '①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳' + const c = '①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑯⑰⑱⑲⑳' return n >= 1 && n <= 20 ? c[n - 1] : `[${n}]` } diff --git a/design/vue/src/pages/AdminPage.vue b/design/vue/src/pages/AdminPage.vue index 4bc4910..75fc0ec 100644 --- a/design/vue/src/pages/AdminPage.vue +++ b/design/vue/src/pages/AdminPage.vue @@ -2,7 +2,7 @@

后台管理

- +
- +
@@ -115,6 +115,7 @@ 提供商 模型 ID Base URL + 最大上下文 状态 操作 @@ -124,6 +125,7 @@ {{ m.provider }} {{ m.model_id }} {{ m.base_url || '-' }} + {{ m.max_context_length ?? '-' }} {{ m.is_enabled ? '启用' : '停用' }}
@@ -214,7 +216,283 @@
- + +
+
+ +
+ + +
+
+
+

实时指标快照

+

采集时间:{{ obsMetrics?.ts ? formatDate(obsMetrics.ts) : '-' }}

+
+ 刷新 +
+
+ + + + +
+
+
+
+

Counters (累计计数)

+ {{ obsMetrics?.counters?.length || 0 }} 个 +
+ + + + + + + + + + + + +
指标名标签当前值
+
暂无 Counters
+
+
+ +
+
+

Gauges (瞬时值)

+ {{ obsMetrics?.gauges?.length || 0 }} 个 +
+ + + + + + + + + + + + +
指标名标签当前值
+
暂无 Gauges
+
+
+ +
+
+

Histograms (分布)

+ {{ obsMetrics?.histograms?.length || 0 }} 个 +
+
+ +
+
+
{{ m.name }}
+
{{ m.help }}
+
+
+
样本总数
+
{{ m.samples.reduce((a, b) => a + b.count, 0) }}
+
+
+
+
+
+
+ {{ k }}={{ v }} +
+
+ 总和: {{ s.sum }} + 样本数: {{ s.count }} +
+
+
+
+
≤ {{ b.le }}
+
{{ b.count }}
+
+
+
+
+
+
+
暂无 Histograms
+
+
+
+ + +
+
+
+ + {{ traceQuerySessionId ? '查询会话' : '拉取最新 Traces' }} +
+ + 查看 Trace +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模式状态链路ID用户ID会话ID总耗时(ms)采样率创建时间操作
+ {{ searchModeText(getSearchMode(t)) }} + + {{ traceStatusText(t.status, t.error) }} + {{ t.id }}{{ t.user_id || '-' }}{{ t.session_id || '-' }}{{ t.duration_ms }} + {{ t.sampled ? '✓ 采样' : '✗ 丢弃' }} + {{ formatSampleRate(t.sample_rate) }} + {{ formatDate(t.created_at) }}查看详情
+
暂无 Traces,请输入 Session ID 查询
+
+
+ +
+
+
+ + +
+
+
+
+
+

Trace 详情

+
+ {{ obsTraceDetail.id }} + · + Session:{{ obsTraceDetail.session_id || '-' }} + · + 耗时:{{ obsTraceDetail.duration_ms }} ms + · + {{ traceStatusText(obsTraceDetail.status, obsTraceDetail.error) }} +
+
+
+ +
+
+
+
加载中...
+
未找到该 Trace
+
+
{{ obsTraceDetail.error }}
+
+
Root Attrs
+
{{ JSON.stringify(obsTraceDetail.attrs || {}, null, 2) }}
+
+
+
Span Tree
+
+ +
+
该 Trace 未记录 span_tree(可能未采样或采样后未写入)
+
+
+
+
+
+ +
@@ -236,6 +514,11 @@
+
+ + +

范围 1024~200000,可后续通过"测试连接"自动探测

+
@@ -245,6 +528,7 @@ {{ modelTestResult.message }}
响应时间: {{ modelTestResult.response_time_ms }}ms
+
探测到的最大上下文: {{ modelTestResult.detected_max_context_length }}
错误: {{ modelTestResult.error }}
详情: {{ modelTestResult.details }}
@@ -284,7 +568,7 @@
- +
@@ -311,7 +595,7 @@