diff --git a/app/web/pages/agents/codex-button-utils.js b/app/web/pages/agents/codex-button-utils.js new file mode 100644 index 0000000..a4e181a --- /dev/null +++ b/app/web/pages/agents/codex-button-utils.js @@ -0,0 +1,25 @@ +'use strict'; + +const CODEX_NEW_THREAD_URL = 'codex://threads/new'; + +function buildCodexNewThreadUrl({ prompt, originUrl }) { + const params = new URLSearchParams(); + params.set('prompt', String(prompt || '')); + + if (originUrl) { + params.set('originUrl', String(originUrl)); + } + + return `${CODEX_NEW_THREAD_URL}?${params.toString()}`; +} + +function buildAgentDetailCodexPrompt(detail = {}, _originUrl, selectedPrompt) { + const firstPrompt = + selectedPrompt || (Array.isArray(detail.prompts) ? detail.prompts[0] : null); + return firstPrompt ? String(firstPrompt.prompt || '') : ''; +} + +module.exports = { + buildAgentDetailCodexPrompt, + buildCodexNewThreadUrl, +}; diff --git a/app/web/pages/agents/detail/AgentDetailContent.tsx b/app/web/pages/agents/detail/AgentDetailContent.tsx index 26247b1..35957ec 100644 --- a/app/web/pages/agents/detail/AgentDetailContent.tsx +++ b/app/web/pages/agents/detail/AgentDetailContent.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { + CodeOutlined, CopyOutlined, DownloadOutlined, MessageOutlined, @@ -13,6 +14,7 @@ import { Button, Card, Empty, message, Spin, Tabs, Tag, Typography } from 'antd' import { API } from '@/api'; import { copyToClipboard } from '@/utils/copyUtils'; import { safeOpenUrl } from '@/utils/safeOpenUrl'; +import { buildAgentDetailCodexPrompt, buildCodexNewThreadUrl } from '../codex-button-utils'; import type { AgentCapability, AgentDetail, AgentItem, AgentSkillRelation } from '../types'; import './style.scss'; @@ -146,6 +148,16 @@ const AgentDetailContent: React.FC = ({ name, history } ? `curl -fsSL ${currentOrigin}/agent-market/install.sh | bash -s -- ${detail.name}` : ''; + const openCodexInstall = (selectedPrompt?: { title?: string; prompt?: string }) => { + if (!detail) return; + + const originUrl = typeof window !== 'undefined' ? window.location.href : ''; + const prompt = buildAgentDetailCodexPrompt(detail, originUrl, selectedPrompt); + const codexUrl = buildCodexNewThreadUrl({ prompt, originUrl }); + + window.location.href = codexUrl; + }; + if (loading) { return (
@@ -364,6 +376,20 @@ const AgentDetailContent: React.FC = ({ name, history } {item.prompt}
+ ) diff --git a/app/web/pages/agents/detail/style.scss b/app/web/pages/agents/detail/style.scss index ba7f156..07268d5 100644 --- a/app/web/pages/agents/detail/style.scss +++ b/app/web/pages/agents/detail/style.scss @@ -343,12 +343,15 @@ } .agent-question-card-body { display: grid; - grid-template-columns: 32px minmax(0, 1fr); + grid-template-columns: 32px minmax(0, 1fr) auto; align-items: center; - gap: 12px; + gap: 16px; } .agent-question-copy { flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; .ant-typography:first-child { display: block; margin-bottom: 4px; @@ -362,6 +365,48 @@ font-size: 14px; } } + .agent-question-quick-use { + display: inline-flex; + align-items: center; + justify-content: center; + height: 36px; + padding: 0 16px 0 12px; + border-color: #D8DFEA; + border-radius: 10px; + color: #344054; + background: #FFF; + font-weight: 600; + white-space: nowrap; + box-shadow: none; + transition: + transform 0.18s ease, + border-color 0.18s ease, + color 0.18s ease, + background 0.18s ease, + box-shadow 0.18s ease; + &:hover, + &:focus { + border-color: #F6B85A; + color: #B66A00; + background: #FFF9EF; + box-shadow: 0 6px 14px rgba(245, 158, 11, 0.12); + transform: translateY(-1px); + } + &:active { + transform: translateY(0); + box-shadow: 0 3px 8px rgba(245, 158, 11, 0.1); + } + .anticon { + margin-right: 8px; + } + } + .agent-question-quick-use-icon { + display: inline-flex; + align-items: center; + justify-content: center; + color: #F59E0B; + font-size: 14px; + } .agent-skill-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -465,5 +510,12 @@ flex-direction: column; align-items: flex-start; } + .agent-question-card-body { + grid-template-columns: 32px minmax(0, 1fr); + } + .agent-question-quick-use { + grid-column: 2; + justify-self: flex-end; + } } } diff --git a/test/agent-codex-button-utils.test.js b/test/agent-codex-button-utils.test.js new file mode 100644 index 0000000..ea21c56 --- /dev/null +++ b/test/agent-codex-button-utils.test.js @@ -0,0 +1,53 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + buildAgentDetailCodexPrompt, + buildCodexNewThreadUrl, +} = require('../app/web/pages/agents/codex-button-utils'); + +test('buildCodexNewThreadUrl builds a codex new thread deep link with encoded prompt and origin', () => { + const url = buildCodexNewThreadUrl({ + prompt: '打开 Agent 市场\n使用示例', + originUrl: 'http://10.10.10.168:7001/page/agents?keyword=AI Agent', + }); + + assert.equal(url.startsWith('codex://threads/new?'), true); + assert.equal(url.includes('prompt='), true); + assert.equal(url.includes('originUrl='), true); + assert.equal(url.includes('AI+Agent'), true); + assert.equal(url.includes('\n'), false); +}); + +test('buildAgentDetailCodexPrompt returns only the opening question prompt', () => { + const prompt = buildAgentDetailCodexPrompt( + { + displayName: 'Bug 修复 Agent', + name: 'bugfix-agent', + description: '用于修复 Bug', + entrypoint: { slug: 'bugfix-workflow', name: 'Bug 修复工作流' }, + dependencies: [ + { slug: 'zentao-api', name: '禅道 API' }, + { slug: 'gitlab-mr-ci-watch', name: 'MR CI 观察' }, + ], + prompts: [{ title: '修复 Bug', prompt: '$bugfix-workflow 12345' }], + }, + 'http://10.10.10.168:7001/page/agents/bugfix-agent' + ); + + assert.equal(prompt, '$bugfix-workflow 12345'); +}); + +test('buildAgentDetailCodexPrompt can use the selected opening question', () => { + const prompt = buildAgentDetailCodexPrompt( + { + displayName: 'Bug 修复 Agent', + name: 'bugfix-agent', + prompts: [{ title: '默认问题', prompt: '$bugfix-workflow 默认' }], + }, + 'http://10.10.10.168:7001/page/agents/bugfix-agent', + { title: '自然语言', prompt: '帮我修 bug,禅道 Bug ID 是 156343' } + ); + + assert.equal(prompt, '帮我修 bug,禅道 Bug ID 是 156343'); +}); diff --git a/test/agent-detail-layout.test.js b/test/agent-detail-layout.test.js index d12e388..b176667 100644 --- a/test/agent-detail-layout.test.js +++ b/test/agent-detail-layout.test.js @@ -240,6 +240,46 @@ test('Agent 详情右侧展示可复制的自动安装命令', () => { ); }); +test('Agent 开场问题卡片提供调起 Codex 的快捷使用入口', () => { + const content = fs.readFileSync( + path.join(__dirname, '../app/web/pages/agents/detail/AgentDetailContent.tsx'), + 'utf8' + ); + const styleContent = fs.readFileSync( + path.join(__dirname, '../app/web/pages/agents/detail/style.scss'), + 'utf8' + ); + + assert.equal( + content.includes('buildAgentDetailCodexPrompt') && + content.includes('buildCodexNewThreadUrl') && + content.includes('className="agent-question-quick-use"') && + content.includes('agent-question-quick-use-icon') && + content.includes('onClick={() => openCodexInstall(item)}') && + content.includes('快捷使用'), + true, + '开场问题卡片需要提供快捷使用按钮并调起 Codex' + ); + assert.equal( + content.includes('className="agent-quick-use"'), + false, + '右侧不应再展示独立快捷使用按钮' + ); + assert.equal( + styleContent.includes('.agent-question-quick-use') && + styleContent.includes('grid-template-columns: 32px minmax(0, 1fr) auto;') && + styleContent.includes('align-items: center;') && + styleContent.includes('&:hover,') && + styleContent.includes('transform: translateY(-1px);') && + styleContent.includes('border-color: #D8DFEA;') && + styleContent.includes('.agent-question-quick-use-icon') && + !styleContent.includes('border: 1px solid #F59E0B;') && + !styleContent.includes('background: rgba(245, 158, 11, 0.12);'), + true, + '开场问题快捷使用按钮需要左右布局、垂直居中并提供克制的 hover 样式' + ); +}); + test('Agent 详情右侧提供当前原始 ZIP 下载入口', () => { const content = fs.readFileSync( path.join(__dirname, '../app/web/pages/agents/detail/AgentDetailContent.tsx'),