fix: wire ai platform run and codex chat
This commit is contained in:
@@ -44,6 +44,75 @@ from ai_platform.services.agent_import_export import (
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CODEX_AGENT_PROMPT = """
|
||||||
|
你是 ai-agent-admin 内置的 Codex 协作助手。你的目标是帮助用户拆解工程任务、解释代码、规划实现、生成可执行步骤,并在需要时提示用户进入智能体、流程编排、模型配置和执行历史等模块完成操作。
|
||||||
|
|
||||||
|
回答要求:
|
||||||
|
- 直接、具体、可执行。
|
||||||
|
- 优先给出最小可落地方案,再说明关键风险。
|
||||||
|
- 涉及代码或配置时,明确文件、接口、命令或字段。
|
||||||
|
- 对不确定事实要说明需要验证,不要编造。
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
async def _resolve_default_chat_model_id(db: AsyncSession) -> Optional[str]:
|
||||||
|
result = await db.execute(
|
||||||
|
select(LLMModel)
|
||||||
|
.where(
|
||||||
|
LLMModel.is_deleted == False,
|
||||||
|
LLMModel.is_active == True,
|
||||||
|
LLMModel.model_type == "chat",
|
||||||
|
)
|
||||||
|
.order_by(LLMModel.sort.desc(), LLMModel.sys_create_datetime.desc())
|
||||||
|
)
|
||||||
|
model = result.scalars().first()
|
||||||
|
return str(model.id) if model else None
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_codex_agent(db: AsyncSession) -> Agent:
|
||||||
|
model_id = await _resolve_default_chat_model_id(db)
|
||||||
|
if not model_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Codex 智能体需要先配置一个启用的 chat 模型",
|
||||||
|
)
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
name="Codex",
|
||||||
|
code="codex",
|
||||||
|
description="内置通用 AI 编程协作助手",
|
||||||
|
mode="autonomous",
|
||||||
|
status="published",
|
||||||
|
is_global=True,
|
||||||
|
is_public=True,
|
||||||
|
model_id=model_id,
|
||||||
|
temperature=0.3,
|
||||||
|
top_p=1.0,
|
||||||
|
max_tokens=4096,
|
||||||
|
max_iterations=6,
|
||||||
|
enable_memory=True,
|
||||||
|
memory_window=10,
|
||||||
|
enable_streaming=True,
|
||||||
|
persona={
|
||||||
|
"role": "AI 编程协作助手",
|
||||||
|
"skills": ["代码理解", "任务拆解", "方案设计", "问题排查"],
|
||||||
|
"constraints": ["不编造事实", "优先输出可执行步骤"],
|
||||||
|
},
|
||||||
|
system_prompt=CODEX_AGENT_PROMPT,
|
||||||
|
welcome_message="我是 Codex 协作助手,可以帮你拆解任务、解释代码、规划实现和排查问题。",
|
||||||
|
suggested_questions=[
|
||||||
|
"帮我分析当前 AI Agent Admin 的下一步实现重点",
|
||||||
|
"解释一下智能体和流程编排如何协作",
|
||||||
|
"帮我设计一个 OC-69 多智能体协作流程",
|
||||||
|
],
|
||||||
|
sort=100,
|
||||||
|
)
|
||||||
|
db.add(agent)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(agent)
|
||||||
|
return agent
|
||||||
|
|
||||||
router = APIRouter(prefix="/agent", tags=["AI-智能体"])
|
router = APIRouter(prefix="/agent", tags=["AI-智能体"])
|
||||||
|
|
||||||
|
|
||||||
@@ -171,6 +240,8 @@ async def get_agent_by_code(code: str, db: AsyncSession = Depends(get_db)):
|
|||||||
select(Agent).where(Agent.code == code, Agent.is_deleted == False)
|
select(Agent).where(Agent.code == code, Agent.is_deleted == False)
|
||||||
)
|
)
|
||||||
agent = result.scalar_one_or_none()
|
agent = result.scalar_one_or_none()
|
||||||
|
if not agent and code == "codex":
|
||||||
|
agent = await _ensure_codex_agent(db)
|
||||||
if not agent:
|
if not agent:
|
||||||
raise HTTPException(status_code=404, detail="智能体不存在")
|
raise HTTPException(status_code=404, detail="智能体不存在")
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ async def update_provider(
|
|||||||
raise HTTPException(status_code=404, detail="提供商不存在")
|
raise HTTPException(status_code=404, detail="提供商不存在")
|
||||||
|
|
||||||
update_data = data.model_dump(exclude_unset=True)
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
|
if update_data.get("api_key") == "":
|
||||||
|
update_data.pop("api_key")
|
||||||
for key, value in update_data.items():
|
for key, value in update_data.items():
|
||||||
setattr(provider, key, value)
|
setattr(provider, key, value)
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,17 @@ function createLightAiMenuRoute(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LIGHT_AI_PLATFORM_MENU_ROUTES = [
|
const LIGHT_AI_PLATFORM_MENU_ROUTES = [
|
||||||
|
createLightAiMenuRoute({
|
||||||
|
component: '/_core/agent-chat/index',
|
||||||
|
meta: {
|
||||||
|
fullPathKey: true,
|
||||||
|
icon: 'lucide:code',
|
||||||
|
order: 25,
|
||||||
|
title: 'menu-title.codex',
|
||||||
|
},
|
||||||
|
name: 'CodexAgentChat',
|
||||||
|
path: '/agent-chat/codex',
|
||||||
|
}),
|
||||||
createLightAiMenuRoute({
|
createLightAiMenuRoute({
|
||||||
component: '/ai-platform/model/index',
|
component: '/ai-platform/model/index',
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const LIGHT_MENU_NAMES = new Set([
|
|||||||
'AIWorkflowRuns',
|
'AIWorkflowRuns',
|
||||||
'AnnouncementList',
|
'AnnouncementList',
|
||||||
'AnnouncementManage',
|
'AnnouncementManage',
|
||||||
|
'CodexAgentChat',
|
||||||
'KnowledgeBase',
|
'KnowledgeBase',
|
||||||
'PageRenderMainHome',
|
'PageRenderMainHome',
|
||||||
'SystemManagement',
|
'SystemManagement',
|
||||||
@@ -23,6 +24,7 @@ const LIGHT_ROUTE_PATH_PREFIXES = [
|
|||||||
'/ai-platform/model',
|
'/ai-platform/model',
|
||||||
'/ai-platform/workflow',
|
'/ai-platform/workflow',
|
||||||
'/ai-platform/workflow-runs',
|
'/ai-platform/workflow-runs',
|
||||||
|
'/agent-chat',
|
||||||
'/message/announcement',
|
'/message/announcement',
|
||||||
'/page-render/main_home',
|
'/page-render/main_home',
|
||||||
'/system/menu',
|
'/system/menu',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type {
|
import type {
|
||||||
DefaultModel,
|
DefaultModel,
|
||||||
ModelListItem,
|
ModelListItem,
|
||||||
|
ProviderCreateInput,
|
||||||
ProviderListItem,
|
ProviderListItem,
|
||||||
ProviderType,
|
ProviderType,
|
||||||
} from '#/api/ai-platform/ai-platform';
|
} from '#/api/ai-platform/ai-platform';
|
||||||
@@ -57,6 +58,7 @@ import {
|
|||||||
deleteProviderApi,
|
deleteProviderApi,
|
||||||
fetchProviderModelsApi,
|
fetchProviderModelsApi,
|
||||||
getModelListApi,
|
getModelListApi,
|
||||||
|
getProviderDetailApi,
|
||||||
getProviderDefaultModelsApi,
|
getProviderDefaultModelsApi,
|
||||||
getProviderListApi,
|
getProviderListApi,
|
||||||
getProviderTypesApi,
|
getProviderTypesApi,
|
||||||
@@ -95,6 +97,7 @@ const providerForm = ref({
|
|||||||
name: '',
|
name: '',
|
||||||
provider_type: '',
|
provider_type: '',
|
||||||
api_key: '',
|
api_key: '',
|
||||||
|
api_key_masked: '',
|
||||||
api_base: '',
|
api_base: '',
|
||||||
ollama_host: 'http://localhost:11434',
|
ollama_host: 'http://localhost:11434',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -247,6 +250,7 @@ function handleAddProvider() {
|
|||||||
name: '',
|
name: '',
|
||||||
provider_type: '',
|
provider_type: '',
|
||||||
api_key: '',
|
api_key: '',
|
||||||
|
api_key_masked: '',
|
||||||
api_base: '',
|
api_base: '',
|
||||||
ollama_host: 'http://localhost:11434',
|
ollama_host: 'http://localhost:11434',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -255,28 +259,51 @@ function handleAddProvider() {
|
|||||||
showProviderDialog.value = true;
|
showProviderDialog.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditProvider(row: ProviderListItem) {
|
async function handleEditProvider(row: ProviderListItem) {
|
||||||
providerDialogMode.value = 'edit';
|
providerDialogMode.value = 'edit';
|
||||||
providerForm.value = {
|
try {
|
||||||
id: row.id,
|
const detail = await getProviderDetailApi(row.id);
|
||||||
name: row.name,
|
providerForm.value = {
|
||||||
provider_type: row.provider_type,
|
id: detail.id,
|
||||||
api_key: '',
|
name: detail.name,
|
||||||
api_base: '',
|
provider_type: detail.provider_type,
|
||||||
ollama_host: 'http://localhost:11434',
|
api_key: '',
|
||||||
description: row.description,
|
api_key_masked: detail.api_key_masked || '',
|
||||||
is_active: row.is_active,
|
api_base: detail.api_base || '',
|
||||||
|
ollama_host: detail.ollama_host || 'http://localhost:11434',
|
||||||
|
description: detail.description || '',
|
||||||
|
is_active: detail.is_active,
|
||||||
|
};
|
||||||
|
showProviderDialog.value = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message || $t('ui.actionMessage.operationFailed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProviderPayload() {
|
||||||
|
const payload: ProviderCreateInput = {
|
||||||
|
name: providerForm.value.name,
|
||||||
|
provider_type: providerForm.value.provider_type,
|
||||||
|
api_base: providerForm.value.api_base,
|
||||||
|
ollama_host: providerForm.value.ollama_host,
|
||||||
|
description: providerForm.value.description,
|
||||||
|
is_active: providerForm.value.is_active,
|
||||||
};
|
};
|
||||||
showProviderDialog.value = true;
|
const apiKey = providerForm.value.api_key.trim();
|
||||||
|
if (apiKey) {
|
||||||
|
payload.api_key = apiKey;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveProvider() {
|
async function handleSaveProvider() {
|
||||||
try {
|
try {
|
||||||
|
const payload = buildProviderPayload();
|
||||||
if (providerDialogMode.value === 'add') {
|
if (providerDialogMode.value === 'add') {
|
||||||
await createProviderApi(providerForm.value);
|
await createProviderApi(payload);
|
||||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||||
} else {
|
} else {
|
||||||
await updateProviderApi(providerForm.value.id, providerForm.value);
|
await updateProviderApi(providerForm.value.id, payload);
|
||||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||||
}
|
}
|
||||||
showProviderDialog.value = false;
|
showProviderDialog.value = false;
|
||||||
@@ -889,9 +916,19 @@ defineExpose({ reset });
|
|||||||
<ElInput
|
<ElInput
|
||||||
v-model="providerForm.api_key"
|
v-model="providerForm.api_key"
|
||||||
type="password"
|
type="password"
|
||||||
:placeholder="$t('ui.placeholder.input')"
|
:placeholder="
|
||||||
|
providerDialogMode === 'edit' && providerForm.api_key_masked
|
||||||
|
? '已配置,留空不修改'
|
||||||
|
: $t('ui.placeholder.input')
|
||||||
|
"
|
||||||
show-password
|
show-password
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="providerDialogMode === 'edit' && providerForm.api_key_masked"
|
||||||
|
class="text-muted-foreground mt-1 text-xs"
|
||||||
|
>
|
||||||
|
当前 Key:{{ providerForm.api_key_masked }},留空不会修改。
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
v-if="providerForm.provider_type !== 'ollama'"
|
v-if="providerForm.provider_type !== 'ollama'"
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ import { useRouter } from 'vue-router';
|
|||||||
import { ExternalLink } from '@vben/icons';
|
import { ExternalLink } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import { ElAlert, ElButton, ElTag } from 'element-plus';
|
import {
|
||||||
|
ElAlert,
|
||||||
|
ElButton,
|
||||||
|
ElCollapse,
|
||||||
|
ElCollapseItem,
|
||||||
|
ElScrollbar,
|
||||||
|
ElTag,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
import { getWorkflowDetailApi, getWorkflowRunDetailApi } from '#/api/ai-platform/ai-platform';
|
import { getWorkflowDetailApi, getWorkflowRunDetailApi } from '#/api/ai-platform/ai-platform';
|
||||||
import { ZqDesc, ZqDescItem } from '#/components/zq-desc';
|
import { ZqDesc, ZqDescItem } from '#/components/zq-desc';
|
||||||
@@ -43,8 +50,10 @@ const nodes = ref<Node[]>([]);
|
|||||||
const edges = ref<Edge[]>([]);
|
const edges = ref<Edge[]>([]);
|
||||||
const activeNodeId = ref('');
|
const activeNodeId = ref('');
|
||||||
const definitionFallback = ref(false);
|
const definitionFallback = ref(false);
|
||||||
|
const activeLogNames = ref<string[]>([]);
|
||||||
|
|
||||||
const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId'));
|
const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId'));
|
||||||
|
const executionLogs = computed(() => run.value?.execution_log || []);
|
||||||
|
|
||||||
async function loadDetail() {
|
async function loadDetail() {
|
||||||
if (!runId.value) return;
|
if (!runId.value) return;
|
||||||
@@ -78,6 +87,7 @@ async function loadDetail() {
|
|||||||
const replayed = applyRunReplay(base.nodes, base.edges, detail);
|
const replayed = applyRunReplay(base.nodes, base.edges, detail);
|
||||||
nodes.value = replayed.nodes;
|
nodes.value = replayed.nodes;
|
||||||
edges.value = replayed.edges;
|
edges.value = replayed.edges;
|
||||||
|
activeLogNames.value = executionLogs.value.slice(0, 1).map((_, index) => String(index));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -109,6 +119,40 @@ function triggerLabel(value?: string) {
|
|||||||
return getTagLabel(value, triggerOptions);
|
return getTagLabel(value, triggerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLogStatusType(status?: string) {
|
||||||
|
if (status === 'completed' || status === 'success') return 'success';
|
||||||
|
if (status === 'failed') return 'danger';
|
||||||
|
if (status === 'waiting') return 'warning';
|
||||||
|
if (status === 'running') return 'primary';
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogStatusText(status?: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
completed: $t('ai-platform.workflowRuns.status.completed'),
|
||||||
|
failed: $t('ai-platform.workflowRuns.status.failed'),
|
||||||
|
running: $t('ai-platform.workflowRuns.status.running'),
|
||||||
|
success: $t('ai-platform.workflowRuns.status.completed'),
|
||||||
|
waiting: $t('ai-platform.workflowRuns.status.waiting'),
|
||||||
|
};
|
||||||
|
return status ? map[status] || status : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewValue(value: any) {
|
||||||
|
if (value === undefined || value === null || value === '') return '-';
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLog(nodeId?: string) {
|
||||||
|
if (!nodeId) return;
|
||||||
|
activeNodeId.value = nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ open });
|
defineExpose({ open });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -192,12 +236,79 @@ defineExpose({ open });
|
|||||||
</ZqDescItem>
|
</ZqDescItem>
|
||||||
</ZqDesc>
|
</ZqDesc>
|
||||||
|
|
||||||
<div class="run-detail-canvas border-border overflow-hidden rounded-lg border">
|
<div class="run-detail-main grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_420px] gap-4">
|
||||||
<WorkflowReadonlyCanvas
|
<div class="run-detail-canvas border-border min-h-0 overflow-hidden rounded-lg border">
|
||||||
:nodes="nodes"
|
<WorkflowReadonlyCanvas
|
||||||
:edges="edges"
|
:nodes="nodes"
|
||||||
:fit-node-id="activeNodeId"
|
:edges="edges"
|
||||||
/>
|
:fit-node-id="activeNodeId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-border flex min-h-0 flex-col rounded-lg border">
|
||||||
|
<div class="border-border flex items-center justify-between border-b px-3 py-2">
|
||||||
|
<div class="text-sm font-medium">
|
||||||
|
{{ $t('ai-platform.workflowRuns.detail.timeline') }}
|
||||||
|
</div>
|
||||||
|
<ElTag size="small">{{ executionLogs.length }}</ElTag>
|
||||||
|
</div>
|
||||||
|
<ElScrollbar class="flex-1">
|
||||||
|
<div class="p-3">
|
||||||
|
<ElAlert
|
||||||
|
v-if="executionLogs.length === 0"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
:title="$t('ai-platform.workflowRuns.detail.noLogs')"
|
||||||
|
/>
|
||||||
|
<ElCollapse v-else v-model="activeLogNames">
|
||||||
|
<ElCollapseItem
|
||||||
|
v-for="(log, index) in executionLogs"
|
||||||
|
:key="`${log.node_id}-${index}`"
|
||||||
|
:name="String(index)"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div
|
||||||
|
class="flex min-w-0 flex-1 items-center gap-2"
|
||||||
|
@click="selectLog(log.node_id)"
|
||||||
|
>
|
||||||
|
<ElTag :type="getLogStatusType(log.status)" size="small">
|
||||||
|
{{ getLogStatusText(log.status) }}
|
||||||
|
</ElTag>
|
||||||
|
<span class="truncate text-sm">
|
||||||
|
{{ log.node_label || log.node_id }}
|
||||||
|
</span>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
{{ formatDuration(log.elapsed_time) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3 text-xs">
|
||||||
|
<div v-if="log.error" class="text-destructive break-words">
|
||||||
|
{{ log.error }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted-foreground mb-1 font-medium">
|
||||||
|
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入
|
||||||
|
</div>
|
||||||
|
<pre class="run-json">{{ previewValue(log.inputs) }}</pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted-foreground mb-1 font-medium">
|
||||||
|
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输出
|
||||||
|
</div>
|
||||||
|
<pre class="run-json">{{ previewValue(log.output) }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground flex gap-3">
|
||||||
|
<span>Token: {{ log.tokens_used || 0 }}</span>
|
||||||
|
<span>{{ log.node_type }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCollapseItem>
|
||||||
|
</ElCollapse>
|
||||||
|
</div>
|
||||||
|
</ElScrollbar>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ZqDialog>
|
</ZqDialog>
|
||||||
@@ -236,4 +347,20 @@ defineExpose({ open });
|
|||||||
min-height: 320px;
|
min-height: 320px;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .run-json {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 6px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.workflow-run-detail-dialog .run-detail-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {
|
import type {
|
||||||
WorkflowListItem,
|
WorkflowListItem,
|
||||||
|
WorkflowStreamEvent,
|
||||||
WorkflowType,
|
WorkflowType,
|
||||||
} from '#/api/ai-platform/ai-platform';
|
} from '#/api/ai-platform/ai-platform';
|
||||||
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui/es/page';
|
import { Page } from '@vben/common-ui/es/page';
|
||||||
@@ -24,6 +25,7 @@ import { $t } from '@vben/locales';
|
|||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElCard,
|
ElCard,
|
||||||
|
ElDialog,
|
||||||
ElDropdown,
|
ElDropdown,
|
||||||
ElDropdownItem,
|
ElDropdownItem,
|
||||||
ElDropdownMenu,
|
ElDropdownMenu,
|
||||||
@@ -35,6 +37,7 @@ import {
|
|||||||
ElMessageBox,
|
ElMessageBox,
|
||||||
ElOption,
|
ElOption,
|
||||||
ElPagination,
|
ElPagination,
|
||||||
|
ElScrollbar,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElSwitch,
|
ElSwitch,
|
||||||
ElTag,
|
ElTag,
|
||||||
@@ -49,6 +52,7 @@ import {
|
|||||||
getWorkflowDetailApi,
|
getWorkflowDetailApi,
|
||||||
getWorkflowListApi,
|
getWorkflowListApi,
|
||||||
publishWorkflowApi,
|
publishWorkflowApi,
|
||||||
|
runWorkflowStreamApi,
|
||||||
updateWorkflowApi,
|
updateWorkflowApi,
|
||||||
} from '#/api/ai-platform/ai-platform';
|
} from '#/api/ai-platform/ai-platform';
|
||||||
import { ZqDialog } from '#/components/zq-dialog';
|
import { ZqDialog } from '#/components/zq-dialog';
|
||||||
@@ -57,6 +61,10 @@ import { useAppContextStore } from '#/store/app-context';
|
|||||||
import RunDialog from '../workflow/editor/components/RunDialog.vue';
|
import RunDialog from '../workflow/editor/components/RunDialog.vue';
|
||||||
import ImportDialog from './modules/import-dialog.vue';
|
import ImportDialog from './modules/import-dialog.vue';
|
||||||
|
|
||||||
|
const DetailDialog = defineAsyncComponent(
|
||||||
|
() => import('../workflow-runs/modules/detail-dialog.vue'),
|
||||||
|
);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appContextStore = useAppContextStore();
|
const appContextStore = useAppContextStore();
|
||||||
const isMainApp = computed(() => appContextStore.isMainApp);
|
const isMainApp = computed(() => appContextStore.isMainApp);
|
||||||
@@ -96,6 +104,22 @@ const runDialogVisible = ref(false);
|
|||||||
const runWorkflowTarget = ref<any>(null);
|
const runWorkflowTarget = ref<any>(null);
|
||||||
const runWorkflowNodes = ref<any[]>([]);
|
const runWorkflowNodes = ref<any[]>([]);
|
||||||
const showImportDialog = ref(false);
|
const showImportDialog = ref(false);
|
||||||
|
const runDetailRef = ref<any>();
|
||||||
|
const runStatusDialogVisible = ref(false);
|
||||||
|
const currentRunId = ref('');
|
||||||
|
const runStatus = ref('pending');
|
||||||
|
const runError = ref('');
|
||||||
|
const runElapsedTime = ref(0);
|
||||||
|
const runEvents = ref<
|
||||||
|
Array<{
|
||||||
|
detail?: string;
|
||||||
|
id: string;
|
||||||
|
status: 'failed' | 'info' | 'running' | 'success' | 'warning';
|
||||||
|
time: string;
|
||||||
|
title: string;
|
||||||
|
}>
|
||||||
|
>([]);
|
||||||
|
let cancelWorkflowRunStream: (() => void) | null = null;
|
||||||
|
|
||||||
async function handleConfirm() {
|
async function handleConfirm() {
|
||||||
if (!formRef.value) return;
|
if (!formRef.value) return;
|
||||||
@@ -232,6 +256,157 @@ const handleQuickRun = async (row: WorkflowListItem) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runStatusType = computed(() => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
completed: 'success',
|
||||||
|
failed: 'danger',
|
||||||
|
pending: 'info',
|
||||||
|
running: 'primary',
|
||||||
|
stopped: 'info',
|
||||||
|
waiting: 'warning',
|
||||||
|
};
|
||||||
|
return map[runStatus.value] || 'info';
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRunStatusText(status: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
completed: '已完成',
|
||||||
|
failed: '失败',
|
||||||
|
pending: '等待中',
|
||||||
|
running: '运行中',
|
||||||
|
stopped: '已停止',
|
||||||
|
waiting: '等待输入',
|
||||||
|
};
|
||||||
|
return map[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRunDuration(ms?: number) {
|
||||||
|
if (!ms && ms !== 0) return '-';
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
return `${(ms / 1000).toFixed(2)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushRunEvent(
|
||||||
|
title: string,
|
||||||
|
status: 'failed' | 'info' | 'running' | 'success' | 'warning' = 'info',
|
||||||
|
detail?: string,
|
||||||
|
) {
|
||||||
|
runEvents.value.push({
|
||||||
|
detail,
|
||||||
|
id: `${Date.now()}-${runEvents.value.length}`,
|
||||||
|
status,
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetRunState() {
|
||||||
|
currentRunId.value = '';
|
||||||
|
runStatus.value = 'running';
|
||||||
|
runError.value = '';
|
||||||
|
runElapsedTime.value = 0;
|
||||||
|
runEvents.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWorkflowRunEvent(event: WorkflowStreamEvent) {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'start': {
|
||||||
|
currentRunId.value = event.run_id || '';
|
||||||
|
runStatus.value = 'running';
|
||||||
|
pushRunEvent(
|
||||||
|
'已创建运行记录',
|
||||||
|
'success',
|
||||||
|
currentRunId.value ? `Run ID: ${currentRunId.value}` : undefined,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'node_start': {
|
||||||
|
pushRunEvent(
|
||||||
|
`开始执行:${event.node_label || event.node_id || '节点'}`,
|
||||||
|
'running',
|
||||||
|
event.node_type,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'node_complete': {
|
||||||
|
const ok = event.status === 'success' || event.status === 'completed';
|
||||||
|
pushRunEvent(
|
||||||
|
`${ok ? '执行完成' : '执行失败'}:${event.node_label || event.node_id || '节点'}`,
|
||||||
|
ok ? 'success' : 'failed',
|
||||||
|
event.error_message ||
|
||||||
|
`耗时 ${formatRunDuration(event.elapsed_time)},Token ${event.tokens_used || 0}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'waiting_input': {
|
||||||
|
runStatus.value = 'waiting';
|
||||||
|
pushRunEvent(
|
||||||
|
`等待输入:${event.node_label || event.node_id || '节点'}`,
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'complete': {
|
||||||
|
runStatus.value = 'completed';
|
||||||
|
runElapsedTime.value = event.elapsed_time || 0;
|
||||||
|
pushRunEvent(
|
||||||
|
'流程运行完成',
|
||||||
|
'success',
|
||||||
|
`耗时 ${formatRunDuration(event.elapsed_time)},Token ${event.total_tokens || 0}`,
|
||||||
|
);
|
||||||
|
ElMessage.success('流程运行完成');
|
||||||
|
cancelWorkflowRunStream = null;
|
||||||
|
fetchList();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
runStatus.value = 'failed';
|
||||||
|
runError.value =
|
||||||
|
event.message || event.error_message || event.error || '流程运行失败';
|
||||||
|
pushRunEvent('流程运行失败', 'failed', runError.value);
|
||||||
|
ElMessage.error(runError.value);
|
||||||
|
cancelWorkflowRunStream = null;
|
||||||
|
fetchList();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRunWorkflow(inputs: Record<string, any>) {
|
||||||
|
if (!runWorkflowTarget.value?.id) return;
|
||||||
|
|
||||||
|
cancelWorkflowRunStream?.();
|
||||||
|
resetRunState();
|
||||||
|
runStatusDialogVisible.value = true;
|
||||||
|
pushRunEvent('已提交运行请求', 'info', runWorkflowTarget.value.name);
|
||||||
|
|
||||||
|
cancelWorkflowRunStream = runWorkflowStreamApi(
|
||||||
|
runWorkflowTarget.value.id,
|
||||||
|
inputs,
|
||||||
|
handleWorkflowRunEvent,
|
||||||
|
(error) => {
|
||||||
|
runStatus.value = 'failed';
|
||||||
|
runError.value = error.message || '流程运行失败';
|
||||||
|
pushRunEvent('流程运行失败', 'failed', runError.value);
|
||||||
|
ElMessage.error(runError.value);
|
||||||
|
cancelWorkflowRunStream = null;
|
||||||
|
fetchList();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
cancelWorkflowRunStream = null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRunHistory() {
|
||||||
|
router.push(appContextStore.getContextPath('/ai-platform/workflow-runs'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCurrentRunDetail() {
|
||||||
|
if (!currentRunId.value) return;
|
||||||
|
runDetailRef.value?.open(currentRunId.value);
|
||||||
|
}
|
||||||
|
|
||||||
const handleEdit = (row: WorkflowListItem) => {
|
const handleEdit = (row: WorkflowListItem) => {
|
||||||
router.push(
|
router.push(
|
||||||
appContextStore.getContextPath(`/ai-platform/workflow/editor/${row.id}`),
|
appContextStore.getContextPath(`/ai-platform/workflow/editor/${row.id}`),
|
||||||
@@ -371,10 +546,16 @@ const getStatusText = (status: string) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchList();
|
fetchList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cancelWorkflowRunStream?.();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ai-workflow-list-page">
|
<div class="ai-workflow-list-page">
|
||||||
|
<DetailDialog ref="runDetailRef" />
|
||||||
|
|
||||||
<Page auto-content-height v-loading="loading">
|
<Page auto-content-height v-loading="loading">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -640,12 +821,83 @@ onMounted(() => {
|
|||||||
<!-- 快速运行弹窗(数据处理/自动化工作流) -->
|
<!-- 快速运行弹窗(数据处理/自动化工作流) -->
|
||||||
<RunDialog
|
<RunDialog
|
||||||
v-model:visible="runDialogVisible"
|
v-model:visible="runDialogVisible"
|
||||||
:workflow-id="runWorkflowTarget?.id || ''"
|
|
||||||
:nodes="runWorkflowNodes"
|
:nodes="runWorkflowNodes"
|
||||||
:workflow-name="runWorkflowTarget?.name"
|
:workflow-name="runWorkflowTarget?.name"
|
||||||
:auto-run="true"
|
@submit="handleRunWorkflow"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ElDialog
|
||||||
|
v-model="runStatusDialogVisible"
|
||||||
|
title="流程运行"
|
||||||
|
width="680px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<ElTag :type="runStatusType as any">
|
||||||
|
{{ getRunStatusText(runStatus) }}
|
||||||
|
</ElTag>
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
{{ runWorkflowTarget?.name || '-' }}
|
||||||
|
</span>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
{{ currentRunId || '正在创建运行记录...' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="runError"
|
||||||
|
class="border-destructive/30 bg-destructive/10 text-destructive rounded-md border px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
{{ runError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElScrollbar height="320px">
|
||||||
|
<div class="space-y-2 pr-2">
|
||||||
|
<div
|
||||||
|
v-for="event in runEvents"
|
||||||
|
:key="event.id"
|
||||||
|
class="border-border flex gap-3 rounded-md border px-3 py-2"
|
||||||
|
>
|
||||||
|
<ElTag :type="event.status === 'failed' ? 'danger' : event.status as any" size="small">
|
||||||
|
{{ event.time }}
|
||||||
|
</ElTag>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="text-sm font-medium">{{ event.title }}</div>
|
||||||
|
<div
|
||||||
|
v-if="event.detail"
|
||||||
|
class="text-muted-foreground mt-1 break-words text-xs"
|
||||||
|
>
|
||||||
|
{{ event.detail }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElScrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<ElButton @click="openRunHistory">
|
||||||
|
打开执行历史
|
||||||
|
</ElButton>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<ElButton
|
||||||
|
:disabled="!currentRunId"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="openCurrentRunDetail"
|
||||||
|
>
|
||||||
|
查看本次详情
|
||||||
|
</ElButton>
|
||||||
|
<ElButton @click="runStatusDialogVisible = false">
|
||||||
|
关闭
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
<ImportDialog v-model="showImportDialog" @imported="handleImported" />
|
<ImportDialog v-model="showImportDialog" @imported="handleImported" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user