diff --git a/backend-fastapi/ai_platform/api/agent_api.py b/backend-fastapi/ai_platform/api/agent_api.py index 939d7bc..9365617 100644 --- a/backend-fastapi/ai_platform/api/agent_api.py +++ b/backend-fastapi/ai_platform/api/agent_api.py @@ -44,6 +44,75 @@ from ai_platform.services.agent_import_export import ( 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-智能体"]) @@ -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) ) agent = result.scalar_one_or_none() + if not agent and code == "codex": + agent = await _ensure_codex_agent(db) if not agent: raise HTTPException(status_code=404, detail="智能体不存在") diff --git a/backend-fastapi/ai_platform/api/provider_api.py b/backend-fastapi/ai_platform/api/provider_api.py index 58669b2..059fd92 100644 --- a/backend-fastapi/ai_platform/api/provider_api.py +++ b/backend-fastapi/ai_platform/api/provider_api.py @@ -110,6 +110,8 @@ async def update_provider( raise HTTPException(status_code=404, detail="提供商不存在") 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(): setattr(provider, key, value) diff --git a/web/apps/web-ele/src/router/access.ts b/web/apps/web-ele/src/router/access.ts index b361a8c..2e662ce 100644 --- a/web/apps/web-ele/src/router/access.ts +++ b/web/apps/web-ele/src/router/access.ts @@ -62,6 +62,17 @@ function createLightAiMenuRoute( } 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({ component: '/ai-platform/model/index', meta: { diff --git a/web/apps/web-ele/src/router/light-menu.ts b/web/apps/web-ele/src/router/light-menu.ts index b29bda4..6832a8c 100644 --- a/web/apps/web-ele/src/router/light-menu.ts +++ b/web/apps/web-ele/src/router/light-menu.ts @@ -7,6 +7,7 @@ export const LIGHT_MENU_NAMES = new Set([ 'AIWorkflowRuns', 'AnnouncementList', 'AnnouncementManage', + 'CodexAgentChat', 'KnowledgeBase', 'PageRenderMainHome', 'SystemManagement', @@ -23,6 +24,7 @@ const LIGHT_ROUTE_PATH_PREFIXES = [ '/ai-platform/model', '/ai-platform/workflow', '/ai-platform/workflow-runs', + '/agent-chat', '/message/announcement', '/page-render/main_home', '/system/menu', diff --git a/web/apps/web-ele/src/views/ai-platform/model/model-config-form.vue b/web/apps/web-ele/src/views/ai-platform/model/model-config-form.vue index 71af536..e526d26 100644 --- a/web/apps/web-ele/src/views/ai-platform/model/model-config-form.vue +++ b/web/apps/web-ele/src/views/ai-platform/model/model-config-form.vue @@ -2,6 +2,7 @@ import type { DefaultModel, ModelListItem, + ProviderCreateInput, ProviderListItem, ProviderType, } from '#/api/ai-platform/ai-platform'; @@ -57,6 +58,7 @@ import { deleteProviderApi, fetchProviderModelsApi, getModelListApi, + getProviderDetailApi, getProviderDefaultModelsApi, getProviderListApi, getProviderTypesApi, @@ -95,6 +97,7 @@ const providerForm = ref({ name: '', provider_type: '', api_key: '', + api_key_masked: '', api_base: '', ollama_host: 'http://localhost:11434', description: '', @@ -247,6 +250,7 @@ function handleAddProvider() { name: '', provider_type: '', api_key: '', + api_key_masked: '', api_base: '', ollama_host: 'http://localhost:11434', description: '', @@ -255,28 +259,51 @@ function handleAddProvider() { showProviderDialog.value = true; } -function handleEditProvider(row: ProviderListItem) { +async function handleEditProvider(row: ProviderListItem) { providerDialogMode.value = 'edit'; - providerForm.value = { - id: row.id, - name: row.name, - provider_type: row.provider_type, - api_key: '', - api_base: '', - ollama_host: 'http://localhost:11434', - description: row.description, - is_active: row.is_active, + try { + const detail = await getProviderDetailApi(row.id); + providerForm.value = { + id: detail.id, + name: detail.name, + provider_type: detail.provider_type, + api_key: '', + api_key_masked: detail.api_key_masked || '', + 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() { try { + const payload = buildProviderPayload(); if (providerDialogMode.value === 'add') { - await createProviderApi(providerForm.value); + await createProviderApi(payload); ElMessage.success($t('ui.actionMessage.operationSuccess')); } else { - await updateProviderApi(providerForm.value.id, providerForm.value); + await updateProviderApi(providerForm.value.id, payload); ElMessage.success($t('ui.actionMessage.operationSuccess')); } showProviderDialog.value = false; @@ -889,9 +916,19 @@ defineExpose({ reset }); +
+ 当前 Key:{{ providerForm.api_key_masked }},留空不会修改。 +
([]); const edges = ref([]); const activeNodeId = ref(''); const definitionFallback = ref(false); +const activeLogNames = ref([]); const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId')); +const executionLogs = computed(() => run.value?.execution_log || []); async function loadDetail() { if (!runId.value) return; @@ -78,6 +87,7 @@ async function loadDetail() { const replayed = applyRunReplay(base.nodes, base.edges, detail); nodes.value = replayed.nodes; edges.value = replayed.edges; + activeLogNames.value = executionLogs.value.slice(0, 1).map((_, index) => String(index)); } catch (error) { console.error(error); } finally { @@ -109,6 +119,40 @@ function triggerLabel(value?: string) { 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 = { + 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 }); @@ -192,12 +236,79 @@ defineExpose({ open }); -
- +
+
+ +
+ +
+
+
+ {{ $t('ai-platform.workflowRuns.detail.timeline') }} +
+ {{ executionLogs.length }} +
+ +
+ + + + + +
+
+ {{ log.error }} +
+
+
+ {{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入 +
+
{{ previewValue(log.inputs) }}
+
+
+
+ {{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输出 +
+
{{ previewValue(log.output) }}
+
+
+ Token: {{ log.tokens_used || 0 }} + {{ log.node_type }} +
+
+
+
+
+
+
@@ -236,4 +347,20 @@ defineExpose({ open }); min-height: 320px; 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; + } +} diff --git a/web/apps/web-ele/src/views/ai-platform/workflow/index.vue b/web/apps/web-ele/src/views/ai-platform/workflow/index.vue index f5d1842..b2342d0 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow/index.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow/index.vue @@ -1,10 +1,11 @@