feat(studio): 补齐剩余工作台聚合接口与真实对接
This commit is contained in:
31
frontend/src/api/__tests__/ingestion.spec.ts
Normal file
31
frontend/src/api/__tests__/ingestion.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const getMock = vi.fn();
|
||||
|
||||
vi.mock('../request', () => ({
|
||||
get: getMock,
|
||||
}));
|
||||
|
||||
describe('ingestion api', () => {
|
||||
afterEach(() => {
|
||||
getMock.mockReset();
|
||||
});
|
||||
|
||||
it('requests ingestion run aggregate with store and document params', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: { runId: 'run-20260601' },
|
||||
});
|
||||
|
||||
const { getIngestionRun } = await import('../ingestion');
|
||||
await getIngestionRun('run-20260601', '1001', '11');
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/knowledge/ingestion-runs/run-20260601', {
|
||||
params: {
|
||||
storeId: '1001',
|
||||
documentId: '11',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
26
frontend/src/api/__tests__/studioDashboard.spec.ts
Normal file
26
frontend/src/api/__tests__/studioDashboard.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const getMock = vi.fn();
|
||||
|
||||
vi.mock('../request', () => ({
|
||||
get: getMock,
|
||||
}));
|
||||
|
||||
describe('studioDashboard api', () => {
|
||||
afterEach(() => {
|
||||
getMock.mockReset();
|
||||
});
|
||||
|
||||
it('requests studio dashboard aggregate', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: { projectName: 'Common Agent Studio' },
|
||||
});
|
||||
|
||||
const { getStudioDashboard } = await import('../studioDashboard');
|
||||
await getStudioDashboard();
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/studio/dashboard');
|
||||
});
|
||||
});
|
||||
49
frontend/src/api/ingestion.ts
Normal file
49
frontend/src/api/ingestion.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { get } from './request';
|
||||
|
||||
export interface IngestionRunFile {
|
||||
documentId: string;
|
||||
attachmentId?: string | null;
|
||||
fileName?: string | null;
|
||||
parseStatus?: string | null;
|
||||
indexStatus?: string | null;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
export interface IngestionRunStep {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
status?: string | null;
|
||||
}
|
||||
|
||||
export interface IngestionRunLog {
|
||||
time?: string | null;
|
||||
level?: string | null;
|
||||
message?: string | null;
|
||||
}
|
||||
|
||||
export interface IngestionRun {
|
||||
runId: string;
|
||||
storeId: string;
|
||||
documentId: string;
|
||||
storeCode?: string | null;
|
||||
storeName?: string | null;
|
||||
files: IngestionRunFile[];
|
||||
steps: IngestionRunStep[];
|
||||
parsedTextPreview?: string | null;
|
||||
chunkPreview?: string | null;
|
||||
chunkStrategy?: number | null;
|
||||
chunkSize?: number | null;
|
||||
chunkOverlap?: number | null;
|
||||
embeddingModelId?: string | null;
|
||||
embeddingDimension?: number | null;
|
||||
logs: IngestionRunLog[];
|
||||
}
|
||||
|
||||
export function getIngestionRun(runId: string, storeId: string, documentId: string) {
|
||||
return get<IngestionRun>(`/knowledge/ingestion-runs/${runId}`, {
|
||||
params: {
|
||||
storeId,
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
}
|
||||
44
frontend/src/api/studioDashboard.ts
Normal file
44
frontend/src/api/studioDashboard.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { get } from './request';
|
||||
|
||||
export interface StudioDashboardLifecycleStep {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
status?: string | null;
|
||||
}
|
||||
|
||||
export interface StudioDashboardChecklistItem {
|
||||
label: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export interface StudioDashboardMetrics {
|
||||
todayRunCount: number;
|
||||
successRate: number;
|
||||
p50Latency?: string | null;
|
||||
estimatedCost?: string | null;
|
||||
}
|
||||
|
||||
export interface StudioDashboardRecentRun {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
type?: string | null;
|
||||
status?: string | null;
|
||||
latency?: string | null;
|
||||
cost?: string | null;
|
||||
}
|
||||
|
||||
export interface StudioDashboard {
|
||||
projectName?: string | null;
|
||||
environment?: string | null;
|
||||
publishStatus?: string | null;
|
||||
lifecycleSteps: StudioDashboardLifecycleStep[];
|
||||
readinessChecklist: StudioDashboardChecklistItem[];
|
||||
metrics: StudioDashboardMetrics;
|
||||
recentRuns: StudioDashboardRecentRun[];
|
||||
warningTitle?: string | null;
|
||||
warningMessage?: string | null;
|
||||
}
|
||||
|
||||
export function getStudioDashboard() {
|
||||
return get<StudioDashboard>('/studio/dashboard');
|
||||
}
|
||||
@@ -1,7 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { ChatDotRound, Coin, Timer } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { chatMessages, citations, traceSteps } from '@/data/studioMock';
|
||||
import type { AgentMessageRecord, AgentWorkspace } from '@/api/agent';
|
||||
import { appendAgentMessage, getAgentWorkspace } from '@/api/agent';
|
||||
|
||||
const loading = ref(false);
|
||||
const agentId = ref('1001');
|
||||
const workspace = ref<AgentWorkspace | null>(null);
|
||||
const draftMessage = ref('');
|
||||
|
||||
const citations = computed(() => {
|
||||
return (workspace.value?.messages ?? [])
|
||||
.filter((message) => message.citationJson?.includes('chunkId'))
|
||||
.map((message, index) => ({
|
||||
key: `${message.id || index}`,
|
||||
title: `引用 ${index + 1}`,
|
||||
text: message.citationJson ?? '',
|
||||
}));
|
||||
});
|
||||
|
||||
const latestAssistantMessage = computed(() => {
|
||||
const assistantMessages = (workspace.value?.messages ?? []).filter((item) => item.role === 'assistant');
|
||||
return assistantMessages.length > 0 ? assistantMessages[assistantMessages.length - 1] : null;
|
||||
});
|
||||
|
||||
function roleLabel(role: AgentMessageRecord['role']) {
|
||||
if (role === 'user') {
|
||||
return '用户';
|
||||
}
|
||||
if (role === 'assistant') {
|
||||
return 'Agent';
|
||||
}
|
||||
return '系统';
|
||||
}
|
||||
|
||||
async function loadWorkspace() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getAgentWorkspace(agentId.value);
|
||||
workspace.value = response.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (!workspace.value?.sessionId || !draftMessage.value.trim()) {
|
||||
return;
|
||||
}
|
||||
await appendAgentMessage(workspace.value.sessionId, {
|
||||
role: 'user',
|
||||
content: draftMessage.value.trim(),
|
||||
});
|
||||
ElMessage.success('调试消息已写入会话');
|
||||
draftMessage.value = '';
|
||||
await loadWorkspace();
|
||||
}
|
||||
|
||||
function formatCost(tokens?: number) {
|
||||
if (!tokens) {
|
||||
return '¥0.000';
|
||||
}
|
||||
return `¥${(tokens / 100000).toFixed(3)}`;
|
||||
}
|
||||
|
||||
onMounted(loadWorkspace);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -11,38 +76,47 @@ import { chatMessages, citations, traceSteps } from '@/data/studioMock';
|
||||
<p class="studio-kicker">AgentWorkspaceView</p>
|
||||
<h1>Agent 对话调试</h1>
|
||||
</div>
|
||||
<el-button type="primary">发布 Agent</el-button>
|
||||
<el-button type="primary">{{ workspace?.status || 'Draft' }}</el-button>
|
||||
</header>
|
||||
|
||||
<div class="agent-layout">
|
||||
<div class="agent-layout" v-loading="loading">
|
||||
<section class="studio-panel chat-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>售前问答 Agent</h2>
|
||||
<span>POST /api/agents/1001/runs</span>
|
||||
<h2>{{ workspace?.agentName || 'Agent 工作台' }}</h2>
|
||||
<span>GET /api/agent-sessions/workspace</span>
|
||||
</div>
|
||||
<el-tag>Draft</el-tag>
|
||||
<el-tag>{{ workspace?.sessionStatus || workspace?.status || 'DRAFT' }}</el-tag>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<article v-for="message in chatMessages" :key="message.content" :class="message.role">
|
||||
<strong>{{ message.role === 'user' ? '用户' : 'Agent' }}</strong>
|
||||
<article
|
||||
v-for="message in workspace?.messages ?? []"
|
||||
:key="message.id || message.content"
|
||||
:class="message.role"
|
||||
:data-test="`agent-message-${message.role}`"
|
||||
>
|
||||
<strong>{{ roleLabel(message.role) }}</strong>
|
||||
<p>{{ message.content }}</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="chat-composer">
|
||||
<span>输入调试问题,运行会写入 agent_session / agent_message 草案</span>
|
||||
<el-button type="primary"><el-icon><ChatDotRound /></el-icon> 发送</el-button>
|
||||
<span>输入调试问题,写入当前 agent_session / agent_message</span>
|
||||
<input v-model="draftMessage" data-test="agent-message-input" type="text" />
|
||||
<el-button data-test="agent-send-message" type="primary" @click="sendMessage">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
发送
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel citation-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>引用切片</h2>
|
||||
<span>3 个来源</span>
|
||||
<span>{{ workspace?.citationCount || 0 }} 个来源</span>
|
||||
</div>
|
||||
<article v-for="citation in citations" :key="citation.title" class="citation-card">
|
||||
<article v-for="citation in citations" :key="citation.key" class="citation-card" data-test="agent-citation-card">
|
||||
<strong>{{ citation.title }}</strong>
|
||||
<el-tag type="success">score {{ citation.score }}</el-tag>
|
||||
<el-tag type="success">引用</el-tag>
|
||||
<p>{{ citation.text }}</p>
|
||||
</article>
|
||||
</aside>
|
||||
@@ -50,17 +124,17 @@ import { chatMessages, citations, traceSteps } from '@/data/studioMock';
|
||||
<aside class="studio-panel run-inspector">
|
||||
<div class="panel-heading compact">
|
||||
<h2>运行追踪</h2>
|
||||
<span>modelRequestId: f4215d</span>
|
||||
<span data-test="agent-latest-request-id">requestId: {{ workspace?.latestRequestId || '-' }}</span>
|
||||
</div>
|
||||
<div class="metric-mini">
|
||||
<span><el-icon><Timer /></el-icon> 1.42s</span>
|
||||
<span><el-icon><Coin /></el-icon> ¥0.018</span>
|
||||
<span>1,248 tokens</span>
|
||||
<span><el-icon><Timer /></el-icon> {{ workspace?.sessionStatus || '-' }}</span>
|
||||
<span><el-icon><Coin /></el-icon> {{ formatCost(workspace?.totalTokens) }}</span>
|
||||
<span>{{ workspace?.totalTokens || 0 }} tokens</span>
|
||||
</div>
|
||||
<ol class="log-list">
|
||||
<li v-for="step in traceSteps" :key="step.node">
|
||||
<time>{{ step.duration }}</time>
|
||||
<span>{{ step.node }} · {{ step.output }}</span>
|
||||
<li v-if="latestAssistantMessage">
|
||||
<time>{{ workspace?.sessionCode || '-' }}</time>
|
||||
<span>{{ latestAssistantMessage.content }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</aside>
|
||||
|
||||
@@ -1,29 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { UploadFilled } from '@element-plus/icons-vue';
|
||||
|
||||
import { ingestionSteps } from '@/data/studioMock';
|
||||
import type { IngestionRun } from '@/api/ingestion';
|
||||
import { getIngestionRun } from '@/api/ingestion';
|
||||
|
||||
const loading = ref(false);
|
||||
const runId = ref('run-20260601');
|
||||
const storeId = ref('1001');
|
||||
const documentId = ref('11');
|
||||
const ingestionRun = ref<IngestionRun | null>(null);
|
||||
|
||||
const primaryFile = computed(() => ingestionRun.value?.files?.[0] ?? null);
|
||||
|
||||
function stepClass(status?: string | null) {
|
||||
if (status === 'done') {
|
||||
return 'is-done';
|
||||
}
|
||||
if (status === 'running') {
|
||||
return 'is-running';
|
||||
}
|
||||
if (status === 'blocked') {
|
||||
return 'is-blocked';
|
||||
}
|
||||
return 'is-idle';
|
||||
}
|
||||
|
||||
async function loadRun() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getIngestionRun(runId.value, storeId.value, documentId.value);
|
||||
ingestionRun.value = response.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadRun);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page ingestion-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">IngestionPipelineView</p>
|
||||
<p class="studio-kicker">IngestionRunView</p>
|
||||
<h1>文件解析管道</h1>
|
||||
</div>
|
||||
<el-button type="primary">启动索引任务</el-button>
|
||||
</header>
|
||||
|
||||
<div class="ingestion-layout">
|
||||
<div class="ingestion-layout" v-loading="loading">
|
||||
<section class="studio-panel upload-panel">
|
||||
<div class="upload-dropzone">
|
||||
<el-icon><UploadFilled /></el-icon>
|
||||
<strong>拖拽文件到这里</strong>
|
||||
<span>支持 PDF / Word / Excel / Markdown / TXT,上传后自动创建 ingestion run。</span>
|
||||
<strong>{{ primaryFile?.fileName || '拖拽文件到这里' }}</strong>
|
||||
<span>
|
||||
支持 PDF / Word / Excel / Markdown / TXT,上传后自动创建 ingestion run。
|
||||
</span>
|
||||
<el-button type="primary">选择文件</el-button>
|
||||
</div>
|
||||
<div class="pipeline-timeline">
|
||||
<article v-for="step in ingestionSteps" :key="step.name" :class="`is-${step.status}`">
|
||||
<article
|
||||
v-for="step in ingestionRun?.steps ?? []"
|
||||
:key="step.name"
|
||||
:class="stepClass(step.status)"
|
||||
:data-test="`ingestion-step-${step.name}`"
|
||||
>
|
||||
<div class="timeline-dot" />
|
||||
<div class="timeline-content">
|
||||
<strong>{{ step.name }}</strong>
|
||||
@@ -36,36 +78,40 @@ import { ingestionSteps } from '@/data/studioMock';
|
||||
<section class="studio-panel preview-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>解析与切片预览</h2>
|
||||
<span>GET /api/knowledge/ingestion-runs/run-20260531</span>
|
||||
<span>GET /api/knowledge/ingestion-runs/{{ ingestionRun?.runId || runId }}</span>
|
||||
</div>
|
||||
<div class="preview-split">
|
||||
<article>
|
||||
<h3>解析文本</h3>
|
||||
<p>私有化部署章节应覆盖基础设施、网络、安全与运维边界。平台需说明模型服务商、知识库索引策略与日志留存周期...</p>
|
||||
<p data-test="ingestion-parsed-preview">{{ ingestionRun?.parsedTextPreview || '暂无解析文本预览' }}</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>切片 #24</h3>
|
||||
<p>chunk_size=800, overlap=120, strategy=FIXED_LENGTH。该切片将进入 rag_chunk 并在向量化后写入 rag_chunk_embedding。</p>
|
||||
<h3>切片预览</h3>
|
||||
<p data-test="ingestion-chunk-preview">{{ ingestionRun?.chunkPreview || '暂无切片预览' }}</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="pipeline-controls">
|
||||
<label>切片策略 <strong>固定长度</strong></label>
|
||||
<label>Chunk Size <strong>800</strong></label>
|
||||
<label>Overlap <strong>120</strong></label>
|
||||
<label>Embedding <strong>Qwen3 1024d</strong></label>
|
||||
<label>切片策略 <strong>{{ ingestionRun?.chunkStrategy ?? '-' }}</strong></label>
|
||||
<label>Chunk Size <strong>{{ ingestionRun?.chunkSize ?? '-' }}</strong></label>
|
||||
<label>Overlap <strong>{{ ingestionRun?.chunkOverlap ?? '-' }}</strong></label>
|
||||
<label>Embedding <strong>{{ ingestionRun?.embeddingModelId || '-' }} / {{ ingestionRun?.embeddingDimension || '-' }} 维</strong></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel task-log-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>任务日志</h2>
|
||||
<span>run-20260531</span>
|
||||
<span>{{ ingestionRun?.runId || runId }}</span>
|
||||
</div>
|
||||
<ol class="log-list">
|
||||
<li><time>23:08:12</time><span>上传 4 个文件并创建 rag_document</span></li>
|
||||
<li><time>23:08:24</time><span>Tika 解析完成 3 个文件</span></li>
|
||||
<li class="warn"><time>23:08:31</time><span>服务条款更新.md 编码检测失败,等待重试</span></li>
|
||||
<li><time>23:08:40</time><span>切片任务进行中 68 / 119</span></li>
|
||||
<li
|
||||
v-for="item in ingestionRun?.logs ?? []"
|
||||
:key="`${item.time}-${item.message}`"
|
||||
:class="{ warn: item.level === 'WARN' }"
|
||||
:data-test="`ingestion-log-${item.time}`"
|
||||
>
|
||||
<time>{{ item.time }}</time><span>{{ item.message }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { DataAnalysis, Document, Setting } from '@element-plus/icons-vue';
|
||||
|
||||
import { knowledgeDocuments, knowledgeStores } from '@/data/studioMock';
|
||||
import type { KnowledgeWorkspace } from '@/api/knowledgeWorkspace';
|
||||
import { getKnowledgeWorkspace } from '@/api/knowledgeWorkspace';
|
||||
|
||||
const loading = ref(false);
|
||||
const activeStoreId = ref('1001');
|
||||
const workspace = ref<KnowledgeWorkspace | null>(null);
|
||||
|
||||
const stores = computed(() => (workspace.value ? [workspace.value] : []));
|
||||
|
||||
function statusLabel(status?: string | null) {
|
||||
if (status === 'ENABLED' || status === '可检索') {
|
||||
return '可检索';
|
||||
}
|
||||
return status || '-';
|
||||
}
|
||||
|
||||
async function loadWorkspace(storeId = activeStoreId.value) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getKnowledgeWorkspace(storeId);
|
||||
workspace.value = response.data;
|
||||
activeStoreId.value = response.data.storeId;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadWorkspace());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -14,61 +42,63 @@ import { knowledgeDocuments, knowledgeStores } from '@/data/studioMock';
|
||||
<el-button type="primary">新建知识库</el-button>
|
||||
</header>
|
||||
|
||||
<div class="three-column-layout">
|
||||
<div class="three-column-layout" v-loading="loading">
|
||||
<aside class="studio-panel collection-rail">
|
||||
<div class="panel-heading compact">
|
||||
<h2>知识集合</h2>
|
||||
<span>{{ knowledgeStores.length }} 个库</span>
|
||||
<span>{{ stores.length }} 个库</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="store in knowledgeStores"
|
||||
:key="store.id"
|
||||
v-for="store in stores"
|
||||
:key="store.storeId"
|
||||
class="collection-item"
|
||||
:class="{ active: store.id === '1001' }"
|
||||
:class="{ active: String(store.storeId) === String(activeStoreId) }"
|
||||
:data-test="`knowledge-store-${store.storeId}`"
|
||||
@click="loadWorkspace(String(store.storeId))"
|
||||
>
|
||||
<strong>{{ store.name }}</strong>
|
||||
<span>{{ store.docs }} 文档 · 健康度 {{ store.health }}%</span>
|
||||
<em>{{ store.status }}</em>
|
||||
<strong>{{ store.storeName }}</strong>
|
||||
<span>{{ store.documentCount }} 文档 · 健康度 {{ store.healthScore }}%</span>
|
||||
<em>{{ statusLabel(store.status) }}</em>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main class="studio-panel knowledge-main">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>产品制度库</h2>
|
||||
<h2>{{ workspace?.storeName || '知识工作台' }}</h2>
|
||||
<span>绑定旧数据语义:rag_store / rag_document / rag_chunk_embedding</span>
|
||||
</div>
|
||||
<el-tag type="success">可检索</el-tag>
|
||||
<el-tag type="success">{{ statusLabel(workspace?.status) }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="config-grid">
|
||||
<article>
|
||||
<el-icon><Setting /></el-icon>
|
||||
<strong>Embedding 模型</strong>
|
||||
<span>Qwen3-Embedding · 1024 维</span>
|
||||
<span>{{ workspace?.embeddingModelId || '-' }} · {{ workspace?.embeddingDimension || '-' }} 维</span>
|
||||
</article>
|
||||
<article>
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<strong>检索配置</strong>
|
||||
<span>TopK 6 · Score ≥ 0.72 · Rerank 关闭</span>
|
||||
<span>切片策略 {{ workspace?.chunkStrategy || '-' }} · Chunk {{ workspace?.chunkSize || '-' }}</span>
|
||||
</article>
|
||||
<article>
|
||||
<el-icon><Document /></el-icon>
|
||||
<strong>索引版本</strong>
|
||||
<span>index_version 14 · Draft 快照</span>
|
||||
<span>index_version {{ workspace?.indexVersion || '-' }}</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="document-table">
|
||||
<div class="table-row table-head">
|
||||
<span>文档</span><span>解析</span><span>索引</span><span>切片</span><span>更新</span>
|
||||
<span>文档</span><span>解析</span><span>索引</span><span>启用</span><span>更新</span>
|
||||
</div>
|
||||
<div v-for="doc in knowledgeDocuments" :key="doc.id" class="table-row">
|
||||
<strong>{{ doc.name }}</strong>
|
||||
<div v-for="doc in workspace?.documents ?? []" :key="doc.documentId" class="table-row" :data-test="`knowledge-document-${doc.documentId}`">
|
||||
<strong>{{ doc.documentTitle }}</strong>
|
||||
<span>{{ doc.parseStatus }}</span>
|
||||
<span>{{ doc.indexStatus }}</span>
|
||||
<span>{{ doc.chunks }}</span>
|
||||
<span>{{ doc.updatedAt }}</span>
|
||||
<span>{{ doc.enabled ? '是' : '否' }}</span>
|
||||
<span>{{ doc.updateTime || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -80,13 +110,13 @@ import { knowledgeDocuments, knowledgeStores } from '@/data/studioMock';
|
||||
</div>
|
||||
<dl class="inspector-list">
|
||||
<dt>Workspace API</dt>
|
||||
<dd>GET /api/knowledge/workspaces/1001</dd>
|
||||
<dd>GET /api/knowledge/workspaces/{{ workspace?.storeId || '-' }}</dd>
|
||||
<dt>文档健康度</dt>
|
||||
<dd>96% · 1 个解析失败</dd>
|
||||
<dd>{{ workspace?.healthScore || 0 }}% · {{ workspace?.parseFailedDocumentCount || 0 }} 个解析失败</dd>
|
||||
<dt>待处理任务</dt>
|
||||
<dd>2 个文档等待向量化</dd>
|
||||
<dd>{{ workspace?.pendingTaskCount || 0 }} 个文档等待处理</dd>
|
||||
<dt>发布影响</dt>
|
||||
<dd>更新后需要 Workflow 重新验证引用质量</dd>
|
||||
<dd>{{ workspace?.publishImpact || '-' }}</dd>
|
||||
</dl>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { modelRoutes } from '@/data/studioMock';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import type { ModelWorkspace } from '@/api/modelWorkspace';
|
||||
import { getModelWorkspace } from '@/api/modelWorkspace';
|
||||
|
||||
const loading = ref(false);
|
||||
const workspace = ref<ModelWorkspace | null>(null);
|
||||
|
||||
function routeStatus(enabled?: boolean) {
|
||||
return enabled ? '启用' : '草稿';
|
||||
}
|
||||
|
||||
async function loadWorkspace() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getModelWorkspace();
|
||||
workspace.value = response.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadWorkspace);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,23 +34,28 @@ import { modelRoutes } from '@/data/studioMock';
|
||||
<el-button type="primary">新增路由</el-button>
|
||||
</header>
|
||||
|
||||
<div class="studio-panel model-panel">
|
||||
<div class="studio-panel model-panel" v-loading="loading">
|
||||
<div class="panel-heading">
|
||||
<h2>任务路由规则</h2>
|
||||
<span>保留 model_provider / model_config / model_route_rule 语义</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<div><strong>{{ workspace?.providerCount || 0 }}</strong><span>服务商</span></div>
|
||||
<div><strong>{{ workspace?.modelCount || 0 }}</strong><span>模型</span></div>
|
||||
<div><strong>{{ workspace?.routeRuleCount || 0 }}</strong><span>路由规则</span></div>
|
||||
<div><strong>{{ workspace?.recentFailedCallCount || 0 }}</strong><span>最近失败</span></div>
|
||||
</div>
|
||||
<div class="document-table">
|
||||
<div class="table-row table-head">
|
||||
<span>任务</span><span>主模型</span><span>Fallback</span><span>最大延迟</span><span>状态</span>
|
||||
<span>任务</span><span>主模型</span><span>Fallback</span><span>状态</span>
|
||||
</div>
|
||||
<div v-for="route in modelRoutes" :key="route.task" class="table-row">
|
||||
<strong>{{ route.task }}</strong>
|
||||
<span>{{ route.primary }}</span>
|
||||
<span>{{ route.fallback }}</span>
|
||||
<span>{{ route.latency }}</span>
|
||||
<div v-for="route in workspace?.routes ?? []" :key="route.id || route.taskType" class="table-row" :data-test="`model-route-${route.taskType}`">
|
||||
<strong>{{ route.taskType }}</strong>
|
||||
<span>{{ route.primaryModelCode || route.primaryModelId }}</span>
|
||||
<span>{{ route.fallbackModelCode || route.fallbackModelId || '无' }}</span>
|
||||
<span class="status-cell">
|
||||
<span class="status-pill" :class="route.status === '启用' ? 'is-success' : 'is-warning'">
|
||||
{{ route.status }}
|
||||
<span class="status-pill" :class="routeStatus(route.enabled) === '启用' ? 'is-success' : 'is-warning'">
|
||||
{{ routeStatus(route.enabled) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { ArrowRight, Check, Warning } from '@element-plus/icons-vue';
|
||||
|
||||
import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMock';
|
||||
import type { StudioDashboard } from '@/api/studioDashboard';
|
||||
import { getStudioDashboard } from '@/api/studioDashboard';
|
||||
|
||||
const loading = ref(false);
|
||||
const dashboard = ref<StudioDashboard | null>(null);
|
||||
|
||||
const metrics = computed(() => dashboard.value?.metrics);
|
||||
|
||||
function formatPublishStatus(status?: string | null) {
|
||||
if (status === 'PUBLISHED') {
|
||||
return 'Published';
|
||||
}
|
||||
if (status === 'DRAFT') {
|
||||
return 'Draft';
|
||||
}
|
||||
return status || 'Draft';
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getStudioDashboard();
|
||||
dashboard.value = response.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadDashboard);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page dashboard-page">
|
||||
<section class="studio-page dashboard-page" v-loading="loading">
|
||||
<header class="studio-hero">
|
||||
<div>
|
||||
<p class="studio-kicker">项目 / Common Agent Studio</p>
|
||||
<p class="studio-kicker">项目 / {{ dashboard?.projectName || 'Common Agent Studio' }}</p>
|
||||
<h1>从知识接入到 Agent 发布的一体化工作台</h1>
|
||||
<p>
|
||||
使用新的聚合 ViewModel 驱动原型:知识资产、Workflow、MCP、Skill、Agent 调试与观测都围绕一次发布旅程组织。
|
||||
@@ -21,13 +50,19 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
||||
</header>
|
||||
|
||||
<div class="lifecycle-strip">
|
||||
<article v-for="(step, index) in lifecycleSteps" :key="step.name" class="lifecycle-step" :class="`is-${step.status}`">
|
||||
<article
|
||||
v-for="(step, index) in dashboard?.lifecycleSteps ?? []"
|
||||
:key="step.name"
|
||||
class="lifecycle-step"
|
||||
:class="`is-${step.status}`"
|
||||
:data-test="`dashboard-lifecycle-${step.name}`"
|
||||
>
|
||||
<div class="step-index">{{ index + 1 }}</div>
|
||||
<div>
|
||||
<strong>{{ step.name }}</strong>
|
||||
<span>{{ step.description }}</span>
|
||||
</div>
|
||||
<el-icon v-if="index < lifecycleSteps.length - 1"><ArrowRight /></el-icon>
|
||||
<el-icon v-if="dashboard?.lifecycleSteps && index < dashboard.lifecycleSteps.length - 1"><ArrowRight /></el-icon>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -38,10 +73,15 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
||||
<h2>发布就绪检查</h2>
|
||||
<span>ViewModel: StudioDashboardView</span>
|
||||
</div>
|
||||
<el-tag type="warning">Draft</el-tag>
|
||||
<el-tag type="warning">{{ formatPublishStatus(dashboard?.publishStatus) }}</el-tag>
|
||||
</div>
|
||||
<ul class="check-list">
|
||||
<li v-for="item in readinessChecklist" :key="item.label" :class="{ done: item.done }">
|
||||
<li
|
||||
v-for="item in dashboard?.readinessChecklist ?? []"
|
||||
:key="item.label"
|
||||
:class="{ done: item.done }"
|
||||
:data-test="`dashboard-check-${item.label}`"
|
||||
>
|
||||
<el-icon>
|
||||
<Check v-if="item.done" />
|
||||
<span v-else class="pending-dot" />
|
||||
@@ -54,13 +94,13 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
||||
<section class="studio-panel metrics-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>运行概览</h2>
|
||||
<span>环境: Dev</span>
|
||||
<span>环境: {{ dashboard?.environment || 'Dev' }}</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<div><strong>27</strong><span>今日运行</span></div>
|
||||
<div><strong>96.4%</strong><span>成功率</span></div>
|
||||
<div><strong>1.28s</strong><span>P50 延迟</span></div>
|
||||
<div><strong>¥4.82</strong><span>预估成本</span></div>
|
||||
<div><strong>{{ metrics?.todayRunCount ?? 0 }}</strong><span>今日运行</span></div>
|
||||
<div><strong>{{ metrics?.successRate ?? 0 }}%</strong><span>成功率</span></div>
|
||||
<div><strong>{{ metrics?.p50Latency || '-' }}</strong><span>P50 延迟</span></div>
|
||||
<div><strong>{{ metrics?.estimatedCost || '-' }}</strong><span>预估成本</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -73,7 +113,12 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
||||
<div class="run-row run-head">
|
||||
<span>名称</span><span>类型</span><span>状态</span><span>延迟</span><span>成本</span>
|
||||
</div>
|
||||
<div v-for="run in recentRuns" :key="run.id" class="run-row">
|
||||
<div
|
||||
v-for="run in dashboard?.recentRuns ?? []"
|
||||
:key="run.id"
|
||||
class="run-row"
|
||||
:data-test="`dashboard-run-${run.id}`"
|
||||
>
|
||||
<strong>{{ run.name }}</strong>
|
||||
<span>{{ run.type }}</span>
|
||||
<span class="status-cell">
|
||||
@@ -90,8 +135,8 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
||||
<section class="studio-panel warning-panel">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<div>
|
||||
<h2>生产发布前仍需确认路由兜底</h2>
|
||||
<p>AGENT_PLAN 任务当前只有草稿路由,建议补齐 fallback 模型和最大延迟阈值。</p>
|
||||
<h2>{{ dashboard?.warningTitle || '生产发布前仍需确认路由兜底' }}</h2>
|
||||
<p>{{ dashboard?.warningMessage || 'AGENT_PLAN 任务当前只有草稿路由,建议补齐 fallback 模型和最大延迟阈值。' }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { Connection, Cpu, VideoPlay } from '@element-plus/icons-vue';
|
||||
|
||||
import { traceSteps, workflowEdges, workflowNodes } from '@/data/studioMock';
|
||||
import type { WorkflowWorkspace } from '@/api/workflow';
|
||||
import { getWorkflowWorkspace } from '@/api/workflow';
|
||||
|
||||
const nodeById = Object.fromEntries(workflowNodes.map((node) => [node.id, node]));
|
||||
const canvasEdges = workflowEdges.flatMap((edge) => {
|
||||
const from = nodeById[edge.from];
|
||||
const to = nodeById[edge.to];
|
||||
const loading = ref(false);
|
||||
const projectId = ref('101');
|
||||
const workflowId = ref('201');
|
||||
const workspace = ref<WorkflowWorkspace | null>(null);
|
||||
|
||||
if (!from || !to) {
|
||||
const nodeLibrary = ['Start', 'LLM', 'Knowledge Retrieval', 'MCP Tool', 'Skill', 'Condition', 'Answer'];
|
||||
|
||||
const workflowNodes = computed(() => {
|
||||
const graphJson = workspace.value?.versions?.[0]?.graphJson;
|
||||
if (!graphJson) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(graphJson) as { nodes?: Array<Record<string, unknown>> };
|
||||
return (parsed.nodes ?? []).map((node, index) => ({
|
||||
id: String(node.id ?? `node-${index}`),
|
||||
type: String(node.type ?? 'NODE'),
|
||||
label: String(node.label ?? node.id ?? `Node ${index + 1}`),
|
||||
description: String(node.description ?? node.prompt ?? ''),
|
||||
x: Number(node.x ?? 10 + index * 18),
|
||||
y: Number(node.y ?? 42),
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${edge.from}-${edge.to}`,
|
||||
const workflowEdges = computed(() => {
|
||||
const graphJson = workspace.value?.versions?.[0]?.graphJson;
|
||||
if (!graphJson) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(graphJson) as { edges?: Array<Record<string, unknown>> };
|
||||
return (parsed.edges ?? []).map((edge, index) => ({
|
||||
id: `edge-${index}`,
|
||||
from: String(edge.from ?? edge.source ?? ''),
|
||||
to: String(edge.to ?? edge.target ?? ''),
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const canvasEdges = computed(() => {
|
||||
const nodeById = Object.fromEntries(workflowNodes.value.map((node) => [node.id, node]));
|
||||
return workflowEdges.value.flatMap((edge) => {
|
||||
const from = nodeById[edge.from];
|
||||
const to = nodeById[edge.to];
|
||||
if (!from || !to) {
|
||||
return [];
|
||||
}
|
||||
return [{
|
||||
id: edge.id,
|
||||
x1: from.x + 5,
|
||||
y1: from.y + 4,
|
||||
x2: to.x,
|
||||
y2: to.y + 4,
|
||||
},
|
||||
];
|
||||
}];
|
||||
});
|
||||
});
|
||||
|
||||
async function loadWorkspace() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getWorkflowWorkspace(projectId.value, workflowId.value);
|
||||
workspace.value = response.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadWorkspace);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,26 +93,20 @@ const canvasEdges = workflowEdges.flatMap((edge) => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workflow-layout">
|
||||
<div class="workflow-layout" v-loading="loading">
|
||||
<aside class="studio-panel node-library">
|
||||
<div class="panel-heading compact">
|
||||
<h2>节点库</h2>
|
||||
<span>JSON Graph</span>
|
||||
</div>
|
||||
<button>Start</button>
|
||||
<button>LLM</button>
|
||||
<button>Knowledge Retrieval</button>
|
||||
<button>MCP Tool</button>
|
||||
<button>Skill</button>
|
||||
<button>Condition</button>
|
||||
<button>Answer</button>
|
||||
<button v-for="node in nodeLibrary" :key="node">{{ node }}</button>
|
||||
</aside>
|
||||
|
||||
<main class="studio-panel workflow-canvas">
|
||||
<div class="canvas-toolbar">
|
||||
<span><el-icon><Connection /></el-icon> workflow-support-rag</span>
|
||||
<span>版本快照 v7</span>
|
||||
<span>环境: Dev</span>
|
||||
<span><el-icon><Connection /></el-icon> {{ workspace?.workflowCode || 'workflow' }}</span>
|
||||
<span>版本快照 v{{ workspace?.currentPublishedVersionNo || workspace?.versions?.[0]?.versionNo || '-' }}</span>
|
||||
<span>环境: {{ workspace?.environment || 'Dev' }}</span>
|
||||
</div>
|
||||
<div class="canvas-surface">
|
||||
<svg class="edge-layer" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
@@ -73,8 +123,9 @@ const canvasEdges = workflowEdges.flatMap((edge) => {
|
||||
v-for="node in workflowNodes"
|
||||
:key="node.id"
|
||||
class="workflow-node"
|
||||
:class="{ selected: node.id === 'llm' }"
|
||||
:class="{ selected: node.type === 'LLM' }"
|
||||
:style="{ left: `${node.x}%`, top: `${node.y}%` }"
|
||||
:data-test="`workflow-node-${node.id}`"
|
||||
>
|
||||
<span>{{ node.type }}</span>
|
||||
<strong>{{ node.label }}</strong>
|
||||
@@ -83,9 +134,9 @@ const canvasEdges = workflowEdges.flatMap((edge) => {
|
||||
</div>
|
||||
<div class="run-trace-drawer">
|
||||
<strong>Run Trace</strong>
|
||||
<div v-for="step in traceSteps" :key="step.node">
|
||||
<span>{{ step.node }}</span>
|
||||
<em>{{ step.status }} · {{ step.duration }} · {{ step.output }}</em>
|
||||
<div v-for="run in workspace?.recentRuns ?? []" :key="run.requestId" :data-test="`workflow-run-${run.requestId}`">
|
||||
<span>{{ run.requestId }}</span>
|
||||
<em>{{ run.status }} · {{ run.durationMs || 0 }}ms · {{ run.outputJson || '-' }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -93,17 +144,17 @@ const canvasEdges = workflowEdges.flatMap((edge) => {
|
||||
<aside class="studio-panel inspector-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>节点 Inspector</h2>
|
||||
<span>LLM</span>
|
||||
<span>{{ workflowNodes[0]?.type || 'LLM' }}</span>
|
||||
</div>
|
||||
<dl class="inspector-list">
|
||||
<dt>任务类型</dt>
|
||||
<dd>RAG_ANSWER</dd>
|
||||
<dt>输入 Schema</dt>
|
||||
<dd>question, retrieved_chunks, conversation</dd>
|
||||
<dt>输出 Schema</dt>
|
||||
<dd>answer, citations, safety_flags</dd>
|
||||
<dt>路由策略</dt>
|
||||
<dd>primary qwen-plus / fallback deepseek-v3</dd>
|
||||
<dt>工作流</dt>
|
||||
<dd>{{ workspace?.workflowName || '-' }}</dd>
|
||||
<dt>发布状态</dt>
|
||||
<dd>{{ workspace?.publishStatus || '-' }}</dd>
|
||||
<dt>当前版本</dt>
|
||||
<dd>v{{ workspace?.currentPublishedVersionNo || workspace?.versions?.[0]?.versionNo || '-' }}</dd>
|
||||
<dt>最近请求</dt>
|
||||
<dd>{{ workspace?.latestRequestId || '-' }}</dd>
|
||||
</dl>
|
||||
<button class="blue-command"><el-icon><Cpu /></el-icon> 打开模型路由</button>
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import AgentWorkspacePage from '../AgentWorkspacePage.vue';
|
||||
import { appendAgentMessage, getAgentWorkspace } from '@/api/agent';
|
||||
|
||||
vi.mock('@/api/agent', () => ({
|
||||
getAgentWorkspace: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
agentId: '1001',
|
||||
agentCode: 'sales_agent',
|
||||
agentName: '售前问答 Agent',
|
||||
storeId: '2001',
|
||||
status: 'DRAFT',
|
||||
sessionId: '3001',
|
||||
sessionCode: 'session-001',
|
||||
sessionStatus: 'ACTIVE',
|
||||
totalTokens: 1248,
|
||||
citationCount: 1,
|
||||
latestRequestId: 'req-1001',
|
||||
sessions: [],
|
||||
messages: [
|
||||
{
|
||||
id: '1',
|
||||
sessionId: '3001',
|
||||
role: 'user',
|
||||
content: '如果客户要求私有化部署,需要说明哪些内容?',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
sessionId: '3001',
|
||||
role: 'assistant',
|
||||
content: '建议说明部署拓扑、权限边界和日志留存策略。',
|
||||
citationJson: '[{"chunkId":"c-1"}]',
|
||||
tokenCount: 612,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
appendAgentMessage: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
listAgents: vi.fn(),
|
||||
queryAgents: vi.fn(),
|
||||
getAgentById: vi.fn(),
|
||||
saveAgent: vi.fn(),
|
||||
deleteAgent: vi.fn(),
|
||||
chatWithAgent: vi.fn(),
|
||||
createAgentSession: vi.fn(),
|
||||
getAgentSessionById: vi.fn(),
|
||||
listAgentMessages: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('AgentWorkspacePage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads workspace messages and citations from backend api', async () => {
|
||||
const wrapper = mount(AgentWorkspacePage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(getAgentWorkspace).toHaveBeenCalledWith('1001');
|
||||
expect(wrapper.text()).toContain('售前问答 Agent');
|
||||
expect(wrapper.text()).toContain('建议说明部署拓扑');
|
||||
expect(wrapper.findAll('[data-test="agent-citation-card"]').length).toBe(1);
|
||||
});
|
||||
|
||||
it('appends debug message through backend api', async () => {
|
||||
const wrapper = mount(AgentWorkspacePage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="agent-message-input"]').setValue('补充说明日志留存周期');
|
||||
await wrapper.get('[data-test="agent-send-message"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(appendAgentMessage).toHaveBeenCalledWith('3001', {
|
||||
role: 'user',
|
||||
content: '补充说明日志留存周期',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import IngestionPipelinePage from '../IngestionPipelinePage.vue';
|
||||
import { getIngestionRun } from '@/api/ingestion';
|
||||
|
||||
vi.mock('@/api/ingestion', () => ({
|
||||
getIngestionRun: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
runId: 'run-20260601',
|
||||
storeId: '1001',
|
||||
documentId: '11',
|
||||
storeName: '产品制度库',
|
||||
files: [
|
||||
{
|
||||
documentId: '11',
|
||||
fileName: '售前方案模板.pdf',
|
||||
parseStatus: 'PARSED',
|
||||
indexStatus: 'INDEXED',
|
||||
},
|
||||
],
|
||||
steps: [
|
||||
{ name: '上传', description: '文件已入库并创建 rag_document', status: 'done' },
|
||||
{ name: '解析', description: '解析完成,文本长度 1280,页数 12', status: 'done' },
|
||||
{ name: '切片', description: '已生成 24 个切片,chunk_size=800,overlap=120', status: 'running' },
|
||||
],
|
||||
parsedTextPreview: '私有化部署章节应覆盖基础设施、网络、安全与运维边界。',
|
||||
chunkPreview: 'chunk_size=800, overlap=120, strategy=1。预览:该切片将进入 rag_chunk 并在向量化后写入 rag_chunk_embedding。',
|
||||
chunkStrategy: 1,
|
||||
chunkSize: 800,
|
||||
chunkOverlap: 120,
|
||||
embeddingModelId: '88',
|
||||
embeddingDimension: 1024,
|
||||
logs: [
|
||||
{ time: '23:08:12', level: 'INFO', message: '上传文件并创建 rag_document 记录' },
|
||||
{ time: '23:08:24', level: 'INFO', message: '解析完成,文本长度 1280,页数 12' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
describe('IngestionPipelinePage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads ingestion aggregate from backend api', async () => {
|
||||
const wrapper = mount(IngestionPipelinePage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(getIngestionRun).toHaveBeenCalledWith('run-20260601', '1001', '11');
|
||||
expect(wrapper.text()).toContain('售前方案模板.pdf');
|
||||
expect(wrapper.text()).toContain('私有化部署章节应覆盖基础设施');
|
||||
expect(wrapper.text()).toContain('上传文件并创建 rag_document 记录');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import KnowledgeWorkspacePage from '../KnowledgeWorkspacePage.vue';
|
||||
import { getKnowledgeWorkspace } from '@/api/knowledgeWorkspace';
|
||||
|
||||
vi.mock('@/api/knowledgeWorkspace', () => ({
|
||||
getKnowledgeWorkspace: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
storeId: '1001',
|
||||
storeCode: 'PROD_DOC',
|
||||
storeName: '产品制度库',
|
||||
status: 'ENABLED',
|
||||
documentCount: 9,
|
||||
parsedDocumentCount: 6,
|
||||
parseFailedDocumentCount: 1,
|
||||
indexedDocumentCount: 5,
|
||||
pendingTaskCount: 2,
|
||||
healthScore: 96,
|
||||
embeddingModelId: '88',
|
||||
embeddingDimension: 1024,
|
||||
chunkStrategy: 1,
|
||||
chunkSize: 800,
|
||||
indexVersion: 14,
|
||||
publishImpact: '更新后需要 Workflow 重新验证引用质量',
|
||||
documents: [
|
||||
{
|
||||
documentId: '11',
|
||||
documentTitle: '售前方案模板.pdf',
|
||||
parseStatus: 'PARSED',
|
||||
indexStatus: 'INDEXED',
|
||||
enabled: true,
|
||||
updateTime: '2026-06-01 10:00:00',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
describe('KnowledgeWorkspacePage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads knowledge workspace aggregate from backend api', async () => {
|
||||
const wrapper = mount(KnowledgeWorkspacePage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(getKnowledgeWorkspace).toHaveBeenCalledWith('1001');
|
||||
expect(wrapper.text()).toContain('产品制度库');
|
||||
expect(wrapper.text()).toContain('96%');
|
||||
expect(wrapper.find('[data-test="knowledge-document-11"]').text()).toContain('售前方案模板.pdf');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import ModelWorkspacePage from '../ModelWorkspacePage.vue';
|
||||
import { getModelWorkspace } from '@/api/modelWorkspace';
|
||||
|
||||
vi.mock('@/api/modelWorkspace', () => ({
|
||||
getModelWorkspace: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
providerCount: 3,
|
||||
healthyProviderCount: 2,
|
||||
unhealthyProviderCount: 1,
|
||||
modelCount: 5,
|
||||
enabledModelCount: 4,
|
||||
routeRuleCount: 2,
|
||||
enabledRouteRuleCount: 1,
|
||||
recentFailedCallCount: 2,
|
||||
providers: [],
|
||||
models: [],
|
||||
routes: [
|
||||
{
|
||||
id: '11',
|
||||
taskType: 'RAG_ANSWER',
|
||||
primaryModelCode: 'qwen-plus',
|
||||
fallbackModelCode: 'deepseek-v3',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
failedCallSummaries: [],
|
||||
},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
describe('ModelWorkspacePage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads model workspace aggregate from backend api', async () => {
|
||||
const wrapper = mount(ModelWorkspacePage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(getModelWorkspace).toHaveBeenCalled();
|
||||
expect(wrapper.text()).toContain('3');
|
||||
expect(wrapper.find('[data-test="model-route-RAG_ANSWER"]').text()).toContain('qwen-plus');
|
||||
expect(wrapper.find('[data-test="model-route-RAG_ANSWER"]').text()).toContain('deepseek-v3');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import StudioDashboardPage from '../StudioDashboardPage.vue';
|
||||
import { getStudioDashboard } from '@/api/studioDashboard';
|
||||
|
||||
vi.mock('@/api/studioDashboard', () => ({
|
||||
getStudioDashboard: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
projectName: 'Common Agent Studio',
|
||||
environment: 'Dev',
|
||||
publishStatus: 'DRAFT',
|
||||
lifecycleSteps: [
|
||||
{ name: '知识接入', description: '上传、解析、切片、向量化', status: 'done' },
|
||||
{ name: '能力编排', description: 'Workflow 连接模型、工具与 Skill', status: 'running' },
|
||||
],
|
||||
readinessChecklist: [
|
||||
{ label: '知识库已绑定 Embedding 模型', done: true },
|
||||
{ label: 'Workflow 已存在可编辑草稿', done: false },
|
||||
],
|
||||
metrics: {
|
||||
todayRunCount: 27,
|
||||
successRate: 96.4,
|
||||
p50Latency: '1.28s',
|
||||
estimatedCost: '¥4.82',
|
||||
},
|
||||
recentRuns: [
|
||||
{ id: 'req-1001', name: '售前问答 Agent', type: 'Agent', status: '成功', latency: '1.42s', cost: '¥0.018' },
|
||||
],
|
||||
warningTitle: '生产发布前仍需确认路由兜底',
|
||||
warningMessage: 'AGENT_PLAN 任务建议补齐 fallback 模型和最大延迟阈值后再发布。',
|
||||
},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
describe('StudioDashboardPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads dashboard aggregate from backend api', async () => {
|
||||
const wrapper = mount(StudioDashboardPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(getStudioDashboard).toHaveBeenCalled();
|
||||
expect(wrapper.text()).toContain('Common Agent Studio');
|
||||
expect(wrapper.text()).toContain('96.4%');
|
||||
expect(wrapper.text()).toContain('售前问答 Agent');
|
||||
expect(wrapper.text()).toContain('AGENT_PLAN 任务建议补齐 fallback 模型');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import WorkflowBuilderPage from '../WorkflowBuilderPage.vue';
|
||||
import { getWorkflowWorkspace } from '@/api/workflow';
|
||||
|
||||
vi.mock('@/api/workflow', () => ({
|
||||
getWorkflowWorkspace: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
projectId: '101',
|
||||
projectCode: 'studio_demo',
|
||||
projectName: '演示项目',
|
||||
environment: 'DEV',
|
||||
publishStatus: 'PUBLISHED',
|
||||
workflowId: '201',
|
||||
workflowCode: 'workflow-support-rag',
|
||||
workflowName: '知识问答流程',
|
||||
workflowStatus: 'ENABLED',
|
||||
currentPublishedVersionNo: 7,
|
||||
latestRequestId: 'req-2001',
|
||||
latestDurationMs: 860,
|
||||
workflows: [],
|
||||
versions: [
|
||||
{
|
||||
id: '301',
|
||||
workflowId: '201',
|
||||
versionNo: 7,
|
||||
snapshotName: '知识问答发布版',
|
||||
graphJson:
|
||||
'{"nodes":[{"id":"start","type":"START","label":"Start","x":4,"y":42},{"id":"llm","type":"LLM","label":"LLM","x":47,"y":42}],"edges":[{"from":"start","to":"llm"}]}',
|
||||
publishStatus: 'PUBLISHED',
|
||||
},
|
||||
],
|
||||
recentRuns: [
|
||||
{
|
||||
id: '401',
|
||||
workflowId: '201',
|
||||
workflowVersionId: '301',
|
||||
requestId: 'req-2001',
|
||||
status: 'SUCCESS',
|
||||
durationMs: 860,
|
||||
outputJson: '{"answer":"done"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
listWorkflowDefinitions: vi.fn(),
|
||||
getWorkflowDefinition: vi.fn(),
|
||||
saveWorkflowDefinition: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('WorkflowBuilderPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads workflow workspace and renders graph nodes from backend api', async () => {
|
||||
const wrapper = mount(WorkflowBuilderPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(getWorkflowWorkspace).toHaveBeenCalledWith('101', '201');
|
||||
expect(wrapper.text()).toContain('workflow-support-rag');
|
||||
expect(wrapper.find('[data-test="workflow-node-start"]').text()).toContain('Start');
|
||||
expect(wrapper.find('[data-test="workflow-node-llm"]').text()).toContain('LLM');
|
||||
});
|
||||
|
||||
it('renders recent run trace from workspace aggregate', async () => {
|
||||
const wrapper = mount(WorkflowBuilderPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-test="workflow-run-req-2001"]').text()).toContain('SUCCESS');
|
||||
expect(wrapper.find('[data-test="workflow-run-req-2001"]').text()).toContain('860ms');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user