| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- <script setup lang="ts">
- import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
- import { useRoute, useRouter } from 'vue-router'
- import { AgentChat, useChatSession, type ChatSession } from '@agent-server/chat-ui'
- import type { SessionDetailDto, SessionDto } from '@agent-server/shared'
- import DebugPanel from '../components/DebugPanel.vue'
- import SidePane from '../components/SidePane.vue'
- import { admin, client, errorMessage } from '../lib/api'
- import { relativeTime, toast } from '../lib/ui'
- // 所有来源(各 Agent Key 与后台)的会话,只读回放
- const route = useRoute()
- const router = useRouter()
- const sessionId = computed(() => (route.params.sessionId as string | undefined) || null)
- const sessions = ref<SessionDto[]>([])
- const cursor = ref<string | null>(null)
- const keyNames = ref<Record<string, string>>({ admin: '后台' })
- const filter = ref('')
- const chat = shallowRef<ChatSession | null>(null)
- const detail = ref<SessionDetailDto | null>(null)
- const debugTab = ref<'events' | 'files' | 'session'>('events')
- const preview = ref<{ fileId: string; path: string; mime: string; size: number } | null>(null)
- const shown = computed(() => sessions.value.filter(s => !filter.value || s.source === filter.value))
- async function load(more = false) {
- try {
- const page = await client.sessions.list({ scope: 'all', limit: 100, cursor: more ? cursor.value ?? undefined : undefined })
- sessions.value = more ? [...sessions.value, ...page.items] : page.items
- cursor.value = page.nextCursor
- } catch (err) {
- toast(errorMessage(err), 'error')
- }
- }
- watch(sessionId, async id => {
- chat.value?.dispose()
- chat.value = null
- detail.value = null
- preview.value = null
- if (!id) return
- try {
- detail.value = await client.sessions.get(id)
- chat.value = useChatSession(client, id)
- } catch (err) {
- toast(errorMessage(err), 'error')
- }
- }, { immediate: true })
- onMounted(async () => {
- void load()
- const keys = await admin.apiKeys().catch(() => [])
- for (const k of keys) keyNames.value[k.id] = k.name
- })
- onBeforeUnmount(() => chat.value?.dispose())
- </script>
- <template>
- <SidePane title="会话记录" :width="300">
- <template #actions>
- <button type="button" class="icon-btn" title="刷新" @click="load()"><span class="codicon codicon-refresh" /></button>
- </template>
- <template #side>
- <div class="px-3 pb-2">
- <select v-model="filter" class="field" aria-label="来源">
- <option value="">全部来源</option>
- <option v-for="(name, id) in keyNames" :key="id" :value="id">{{ name }}</option>
- </select>
- </div>
- <div
- v-for="s in shown" :key="s.id" class="flex items-center gap-2 h-[48px] pl-5 pr-3 cursor-pointer hover:bg-hover"
- :class="{ '!bg-active': s.id === sessionId }" @click="router.push(`/sessions/${s.id}`)"
- >
- <div class="flex-1 min-w-0">
- <div class="truncate">{{ s.title || '新会话' }}</div>
- <div class="text-[11px] text-fg-muted truncate">{{ keyNames[s.source] ?? s.source }} · {{ s.model }} · {{ relativeTime(s.updatedAt) }}</div>
- </div>
- <span v-if="s.status !== 'idle'" class="w-2 h-2 rounded-full flex-none bg-accent" />
- </div>
- <button v-if="cursor" type="button" class="btn-ghost w-full" @click="load(true)">加载更多</button>
- </template>
- <div v-if="!sessionId" class="flex-1 flex items-center justify-center hint bg-bg-side">选择左侧的会话查看完整过程</div>
- <template v-else>
- <header class="h-[35px] flex-none flex items-center gap-2 px-4 border-b border-b-solid border-border bg-bg-side">
- <span class="font-500 truncate">{{ detail?.title || '新会话' }}</span>
- <span class="hint">只读回放</span>
- <span class="flex-1" />
- <button v-if="detail?.source === 'admin'" type="button" class="btn-ghost" @click="router.push(`/playground/${sessionId}`)">
- <span class="codicon codicon-comment-discussion" />在 Playground 中继续
- </button>
- </header>
- <div class="flex-1 min-h-0 flex">
- <div class="flex-1 min-w-0">
- <AgentChat v-if="chat" :key="sessionId!" :client="client" :session="chat" readonly @open-file="f => { debugTab = 'files'; preview = f }" />
- </div>
- <div v-if="chat" class="w-[380px] flex-none min-h-0">
- <DebugPanel v-model:tab="debugTab" v-model:preview="preview" :state="chat.state" :session="detail" />
- </div>
- </div>
- </template>
- </SidePane>
- </template>
|