增加已发布节点和已发布智能体菜单并实现功能
This commit is contained in:
parent
c0361bf5f2
commit
0ce62607db
@ -151,6 +151,18 @@ const router = createRouter({
|
||||
name: 'MyPublishedApps',
|
||||
component: () => import('../../views/dev/MyPublishedApps.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'my-published-nodes',
|
||||
name: 'MyPublishedNodes',
|
||||
component: () => import('../../views/dev/MyPublishedNodes.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'my-published-agents',
|
||||
name: 'MyPublishedAgents',
|
||||
component: () => import('../../views/dev/MyPublishedAgents.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -61,6 +61,8 @@ function getDefaultMenus(): AppMenuItem[] {
|
||||
{ id: 'app-installer', key: 'AppInstaller', label: 'App Installer', icon: 'tabler:upload', type: 'route', routeName: 'AppInstaller', parentId: 'dev-group', order: 5 },
|
||||
{ id: 'installed-apps', key: 'InstalledApps', label: 'Installed Apps', icon: 'tabler:apps', type: 'route', routeName: 'InstalledApps', parentId: 'dev-group', order: 6 },
|
||||
{ id: 'my-published-apps', key: 'MyPublishedApps', label: 'My Published Apps', icon: 'tabler:cloud-upload', type: 'route', routeName: 'MyPublishedApps', parentId: 'dev-group', order: 7 },
|
||||
{ id: 'my-published-nodes', key: 'MyPublishedNodes', label: '已发布节点', icon: 'carbon:add-child-node', type: 'route', routeName: 'MyPublishedNodes', parentId: 'dev-group', order: 7.1 },
|
||||
{ id: 'my-published-agents', key: 'MyPublishedAgents', label: '已发布智能体', icon: 'hugeicons:robotic', type: 'route', routeName: 'MyPublishedAgents', parentId: 'dev-group', order: 7.2 },
|
||||
{ id: 'app-marketplace', key: 'AppMarketplace', label: 'App Marketplace', icon: 'tabler:shopping-cart', type: 'route', routeName: 'AppMarketplace', parentId: 'dev-group', order: 7.5 },
|
||||
{ id: 'node-marketplace', key: 'NodeMarketplace', label: 'Node Marketplace', icon: 'carbon:add-child-node', type: 'route', routeName: 'NodeMarketplace', parentId: 'dev-group', order: 8 },
|
||||
{ id: 'agent-marketplace', key: 'AgentMarketplace', label: 'Agent Marketplace', icon: 'hugeicons:robotic', type: 'route', routeName: 'AgentMarketplace', parentId: 'dev-group', order: 8.5 },
|
||||
|
||||
644
apps/jingrow/frontend/src/views/dev/MyPublishedAgents.vue
Normal file
644
apps/jingrow/frontend/src/views/dev/MyPublishedAgents.vue
Normal file
@ -0,0 +1,644 @@
|
||||
<template>
|
||||
<div class="my-published-agents">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-text">
|
||||
<h1>{{ t('已发布智能体') }}</h1>
|
||||
<p>{{ t('Manage your published agents in the marketplace') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="search-container">
|
||||
<div class="search-bar">
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
:placeholder="t('Search agents...')"
|
||||
clearable
|
||||
size="large"
|
||||
@keyup.enter="loadAgents"
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Icon icon="tabler:search" /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
<n-button type="primary" size="large" @click="loadAgents" class="search-button">
|
||||
<template #icon>
|
||||
<n-icon><Icon icon="tabler:search" /></n-icon>
|
||||
</template>
|
||||
{{ t('Search') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agents-section" v-if="!loading && agents.length > 0">
|
||||
<!-- 排序控件 -->
|
||||
<div class="agents-header">
|
||||
<div class="agents-title">
|
||||
</div>
|
||||
<div class="sort-controls">
|
||||
<n-select
|
||||
v-model:value="sortBy"
|
||||
:options="sortOptions"
|
||||
:placeholder="t('Sort by')"
|
||||
style="width: 150px"
|
||||
@update:value="loadAgents"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agents-grid">
|
||||
<div v-for="agent in agents" :key="agent.name" class="agent-card">
|
||||
<!-- 智能体图标 -->
|
||||
<div class="agent-icon" @click="viewAgentDetail(agent)">
|
||||
<Icon
|
||||
v-if="agent.icon"
|
||||
:icon="agent.icon"
|
||||
:width="64"
|
||||
:height="64"
|
||||
:style="{ color: agent.color || '#6b7280' }"
|
||||
/>
|
||||
<div v-else class="agent-icon-placeholder">
|
||||
<n-icon size="64"><Icon icon="hugeicons:robotic" /></n-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能体信息 -->
|
||||
<div class="agent-content">
|
||||
<div class="agent-header">
|
||||
<div class="agent-title-section">
|
||||
<h3 @click="viewAgentDetail(agent)" class="clickable-title">{{ agent.title || agent.agent_name || agent.name }}</h3>
|
||||
<div class="agent-meta">
|
||||
<div class="agent-team" v-if="agent.team">
|
||||
<n-icon><Icon icon="tabler:users" /></n-icon>
|
||||
<span>{{ agent.team }}</span>
|
||||
</div>
|
||||
<span v-if="agent.status" class="status-badge" :class="getStatusClass(agent.status)">
|
||||
{{ t(agent.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-name" v-if="agent.agent_name">
|
||||
{{ agent.agent_name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-description" v-if="agent.description">
|
||||
{{ truncateText(agent.description, 80) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-actions">
|
||||
<n-button type="default" @click="viewAgentDetail(agent)">
|
||||
{{ t('View Details') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="deleteAgent(agent)"
|
||||
>
|
||||
{{ t('Delete') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<n-pagination
|
||||
v-model:page="page"
|
||||
:page-count="pageCount"
|
||||
size="large"
|
||||
show-size-picker
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:page-size="pageSize"
|
||||
@update:page="loadAgents"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
<n-spin size="large">
|
||||
<template #description>{{ t('Loading agents...') }}</template>
|
||||
</n-spin>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && agents.length === 0" class="empty">
|
||||
<n-empty :description="t('No agents found')">
|
||||
<template #icon>
|
||||
<n-icon><Icon icon="hugeicons:robotic" /></n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NInput, NButton, NIcon, NSpin, NEmpty, NSelect, NPagination, useMessage, useDialog } from 'naive-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import axios from 'axios'
|
||||
import { t } from '@/shared/i18n'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const router = useRouter()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const loading = ref(false)
|
||||
const agents = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '20'))
|
||||
const sortBy = ref('creation desc')
|
||||
|
||||
// 排序选项
|
||||
const sortOptions = computed(() => [
|
||||
{ label: t('Latest'), value: 'creation desc' },
|
||||
{ label: t('Oldest'), value: 'creation asc' },
|
||||
{ label: t('Name A-Z'), value: 'name asc' },
|
||||
{ label: t('Name Z-A'), value: 'name desc' },
|
||||
{ label: t('Most Popular'), value: 'modified desc' }
|
||||
])
|
||||
|
||||
// 计算总页数
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
|
||||
async function loadAgents() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.value.toString(),
|
||||
page_size: pageSize.value.toString(),
|
||||
search: searchQuery.value,
|
||||
sort_by: sortBy.value
|
||||
})
|
||||
|
||||
const response = await axios.get(`/jingrow/my-published-agents?${params}`)
|
||||
const data = response.data
|
||||
|
||||
// 如果API返回分页数据
|
||||
if (data.items) {
|
||||
agents.value = data.items
|
||||
total.value = data.total || 0
|
||||
} else {
|
||||
// 兼容旧API格式
|
||||
agents.value = data || []
|
||||
total.value = agents.value.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load agents:', error)
|
||||
message.error(t('Failed to load agents'))
|
||||
agents.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageSizeChange(newPageSize: number) {
|
||||
pageSize.value = newPageSize
|
||||
page.value = 1
|
||||
localStorage.setItem('itemsPerPage', newPageSize.toString())
|
||||
loadAgents()
|
||||
}
|
||||
|
||||
function viewAgentDetail(agent: any) {
|
||||
// 跳转到智能体详情页面,传递返回路径
|
||||
router.push({
|
||||
path: `/agent-marketplace/${agent.name}`,
|
||||
query: { returnTo: '/my-published-agents' }
|
||||
})
|
||||
}
|
||||
|
||||
function truncateText(text: string, maxLength: number): string {
|
||||
if (!text) return ''
|
||||
if (text.length <= maxLength) return text
|
||||
return text.substring(0, maxLength) + '...'
|
||||
}
|
||||
|
||||
function getStatusClass(status: string): string {
|
||||
if (!status) return ''
|
||||
// 将状态转换为小写,并将空格替换为连字符
|
||||
return status.toLowerCase().replace(/\s+/g, '-')
|
||||
}
|
||||
|
||||
async function deleteAgent(agent: any) {
|
||||
// 使用记录的name字段删除
|
||||
const recordName = agent.name
|
||||
if (!recordName) {
|
||||
message.error(t('智能体名称不存在'))
|
||||
return
|
||||
}
|
||||
|
||||
// 显示确认对话框,显示智能体标题
|
||||
const agentTitle = agent.title || agent.agent_name || recordName
|
||||
dialog.warning({
|
||||
title: t('确认删除'),
|
||||
content: t('确定要删除智能体 "{0}" 吗?此操作不可恢复。').replace('{0}', agentTitle),
|
||||
positiveText: t('确认删除'),
|
||||
negativeText: t('取消'),
|
||||
onPositiveClick: async () => {
|
||||
await performDelete(recordName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function performDelete(agentName: string) {
|
||||
try {
|
||||
// 调用本地API,由后端转发到云端
|
||||
const response = await axios.post('/jingrow/delete-published-agent', {
|
||||
name: agentName
|
||||
}, {
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
message.success(response.data.message || t('智能体删除成功'))
|
||||
// 刷新智能体列表
|
||||
loadAgents()
|
||||
} else {
|
||||
const errorMsg = response.data?.message || response.data?.error || t('删除失败')
|
||||
message.error(errorMsg)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Delete agent error:', error)
|
||||
const errorMsg = error.response?.data?.detail ||
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
t('删除失败')
|
||||
message.error(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAgents()
|
||||
})
|
||||
|
||||
// 监听搜索和排序变化
|
||||
watch([searchQuery, sortBy], () => {
|
||||
page.value = 1 // 重置到第一页
|
||||
loadAgents()
|
||||
}, { deep: true })
|
||||
|
||||
// 监听分页变化
|
||||
watch([page], () => {
|
||||
loadAgents()
|
||||
})
|
||||
|
||||
// 监听每页数量变化(从系统设置)
|
||||
watch(() => localStorage.getItem('itemsPerPage'), (newValue) => {
|
||||
if (newValue) {
|
||||
pageSize.value = parseInt(newValue)
|
||||
page.value = 1 // 重置到第一页
|
||||
loadAgents()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-published-agents {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-text h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.header-text p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.agents-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.agents-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.agents-title h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-input .n-input {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input .n-input:focus-within {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-button {
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
padding: 0 24px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.agents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.agent-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.agent-icon {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.agent-icon:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.agent-icon-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #9ca3af;
|
||||
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
|
||||
}
|
||||
|
||||
.agent-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.agent-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.agent-title-section {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.agent-title-section h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.clickable-title {
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.clickable-title:hover {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.agent-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.agent-team {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.agent-team .n-icon {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.status-badge.published {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-badge.unpublished {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-badge.draft {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-badge.pending-review {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: transparent;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 12px;
|
||||
padding: 4px 10px;
|
||||
text-align: center;
|
||||
min-width: 70px;
|
||||
letter-spacing: 0.3px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.agent-name:hover {
|
||||
border-color: #9ca3af;
|
||||
color: #374151;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.agent-description {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.agent-actions {
|
||||
padding: 0 20px 20px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-actions .n-button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agents-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.agent-icon {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
654
apps/jingrow/frontend/src/views/dev/MyPublishedNodes.vue
Normal file
654
apps/jingrow/frontend/src/views/dev/MyPublishedNodes.vue
Normal file
@ -0,0 +1,654 @@
|
||||
<template>
|
||||
<div class="my-published-nodes">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-text">
|
||||
<h1>{{ t('已发布节点') }}</h1>
|
||||
<p>{{ t('Manage your published nodes in the marketplace') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="search-container">
|
||||
<div class="search-bar">
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
:placeholder="t('Search nodes...')"
|
||||
clearable
|
||||
size="large"
|
||||
@keyup.enter="loadNodes"
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Icon icon="tabler:search" /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
<n-button type="primary" size="large" @click="loadNodes" class="search-button">
|
||||
<template #icon>
|
||||
<n-icon><Icon icon="tabler:search" /></n-icon>
|
||||
</template>
|
||||
{{ t('Search') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nodes-section" v-if="!loading && nodes.length > 0">
|
||||
<!-- 排序控件 -->
|
||||
<div class="nodes-header">
|
||||
<div class="nodes-title">
|
||||
</div>
|
||||
<div class="sort-controls">
|
||||
<n-select
|
||||
v-model:value="sortBy"
|
||||
:options="sortOptions"
|
||||
:placeholder="t('Sort by')"
|
||||
style="width: 150px"
|
||||
@update:value="loadNodes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nodes-grid">
|
||||
<div v-for="node in nodes" :key="node.name" class="node-card">
|
||||
<!-- 节点图标 -->
|
||||
<div class="node-icon" @click="viewNodeDetail(node)">
|
||||
<Icon
|
||||
v-if="node.icon"
|
||||
:icon="node.icon"
|
||||
:width="64"
|
||||
:height="64"
|
||||
:style="{ color: node.color || '#6b7280' }"
|
||||
/>
|
||||
<div v-else class="node-icon-placeholder">
|
||||
<n-icon size="64"><Icon icon="carbon:add-child-node" /></n-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点信息 -->
|
||||
<div class="node-content">
|
||||
<div class="node-header">
|
||||
<div class="node-title-section">
|
||||
<h3 @click="viewNodeDetail(node)" class="clickable-title">{{ node.title || node.name }}</h3>
|
||||
<div class="node-meta">
|
||||
<div class="node-type" v-if="node.node_type">
|
||||
{{ node.node_type }}
|
||||
</div>
|
||||
<span v-if="node.status" class="status-badge" :class="getStatusClass(node.status)">
|
||||
{{ t(node.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-name" v-if="node.name">
|
||||
{{ node.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="node-description" v-if="node.description">
|
||||
{{ truncateText(node.description, 80) }}
|
||||
</div>
|
||||
|
||||
<div class="node-meta-info" v-if="node.group">
|
||||
<n-icon><Icon icon="tabler:category" /></n-icon>
|
||||
<span>{{ node.group }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="node-actions">
|
||||
<n-button type="default" @click="viewNodeDetail(node)">
|
||||
{{ t('View Details') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="deleteNode(node)"
|
||||
>
|
||||
{{ t('Delete') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<n-pagination
|
||||
v-model:page="page"
|
||||
:page-count="pageCount"
|
||||
size="large"
|
||||
show-size-picker
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:page-size="pageSize"
|
||||
@update:page="loadNodes"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
<n-spin size="large">
|
||||
<template #description>{{ t('Loading nodes...') }}</template>
|
||||
</n-spin>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && nodes.length === 0" class="empty">
|
||||
<n-empty :description="t('No nodes found')">
|
||||
<template #icon>
|
||||
<n-icon><Icon icon="carbon:add-child-node" /></n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NInput, NButton, NIcon, NSpin, NEmpty, NSelect, NPagination, useMessage, useDialog } from 'naive-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import axios from 'axios'
|
||||
import { t } from '@/shared/i18n'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const router = useRouter()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const loading = ref(false)
|
||||
const nodes = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '20'))
|
||||
const sortBy = ref('creation desc')
|
||||
|
||||
// 排序选项
|
||||
const sortOptions = computed(() => [
|
||||
{ label: t('Latest'), value: 'creation desc' },
|
||||
{ label: t('Oldest'), value: 'creation asc' },
|
||||
{ label: t('Name A-Z'), value: 'name asc' },
|
||||
{ label: t('Name Z-A'), value: 'name desc' },
|
||||
{ label: t('Most Popular'), value: 'modified desc' }
|
||||
])
|
||||
|
||||
// 计算总页数
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
|
||||
async function loadNodes() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.value.toString(),
|
||||
page_size: pageSize.value.toString(),
|
||||
search: searchQuery.value,
|
||||
sort_by: sortBy.value
|
||||
})
|
||||
|
||||
const response = await axios.get(`/jingrow/my-published-nodes?${params}`)
|
||||
const data = response.data
|
||||
|
||||
// 如果API返回分页数据
|
||||
if (data.items) {
|
||||
nodes.value = data.items
|
||||
total.value = data.total || 0
|
||||
} else {
|
||||
// 兼容旧API格式
|
||||
nodes.value = data || []
|
||||
total.value = nodes.value.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load nodes:', error)
|
||||
message.error(t('Failed to load nodes'))
|
||||
nodes.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageSizeChange(newPageSize: number) {
|
||||
pageSize.value = newPageSize
|
||||
page.value = 1
|
||||
localStorage.setItem('itemsPerPage', newPageSize.toString())
|
||||
loadNodes()
|
||||
}
|
||||
|
||||
function viewNodeDetail(node: any) {
|
||||
// 跳转到节点详情页面,传递返回路径
|
||||
router.push({
|
||||
path: `/node-marketplace/${node.name}`,
|
||||
query: { returnTo: '/my-published-nodes' }
|
||||
})
|
||||
}
|
||||
|
||||
function truncateText(text: string, maxLength: number): string {
|
||||
if (!text) return ''
|
||||
if (text.length <= maxLength) return text
|
||||
return text.substring(0, maxLength) + '...'
|
||||
}
|
||||
|
||||
function getStatusClass(status: string): string {
|
||||
if (!status) return ''
|
||||
// 将状态转换为小写,并将空格替换为连字符
|
||||
return status.toLowerCase().replace(/\s+/g, '-')
|
||||
}
|
||||
|
||||
async function deleteNode(node: any) {
|
||||
// 使用记录的name字段删除
|
||||
const recordName = node.name
|
||||
if (!recordName) {
|
||||
message.error(t('节点名称不存在'))
|
||||
return
|
||||
}
|
||||
|
||||
// 显示确认对话框,显示节点标题
|
||||
const nodeTitle = node.title || node.node_type || recordName
|
||||
dialog.warning({
|
||||
title: t('确认删除'),
|
||||
content: t('确定要删除节点 "{0}" 吗?此操作不可恢复。').replace('{0}', nodeTitle),
|
||||
positiveText: t('确认删除'),
|
||||
negativeText: t('取消'),
|
||||
onPositiveClick: async () => {
|
||||
await performDelete(recordName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function performDelete(nodeName: string) {
|
||||
try {
|
||||
// 调用本地API,由后端转发到云端
|
||||
const response = await axios.post('/jingrow/delete-published-node', {
|
||||
name: nodeName
|
||||
}, {
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
message.success(response.data.message || t('节点删除成功'))
|
||||
// 刷新节点列表
|
||||
loadNodes()
|
||||
} else {
|
||||
const errorMsg = response.data?.message || response.data?.error || t('删除失败')
|
||||
message.error(errorMsg)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Delete node error:', error)
|
||||
const errorMsg = error.response?.data?.detail ||
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
t('删除失败')
|
||||
message.error(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadNodes()
|
||||
})
|
||||
|
||||
// 监听搜索和排序变化
|
||||
watch([searchQuery, sortBy], () => {
|
||||
page.value = 1 // 重置到第一页
|
||||
loadNodes()
|
||||
}, { deep: true })
|
||||
|
||||
// 监听分页变化
|
||||
watch([page], () => {
|
||||
loadNodes()
|
||||
})
|
||||
|
||||
// 监听每页数量变化(从系统设置)
|
||||
watch(() => localStorage.getItem('itemsPerPage'), (newValue) => {
|
||||
if (newValue) {
|
||||
pageSize.value = parseInt(newValue)
|
||||
page.value = 1 // 重置到第一页
|
||||
loadNodes()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-published-nodes {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-text h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.header-text p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.nodes-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.nodes-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.nodes-title h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-input .n-input {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input .n-input:focus-within {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-button {
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
padding: 0 24px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.nodes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.node-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.node-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.node-icon:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.node-icon-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #9ca3af;
|
||||
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
|
||||
}
|
||||
|
||||
.node-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.node-title-section {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.node-title-section h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.clickable-title {
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.clickable-title:hover {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.node-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.node-type {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.status-badge.published {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-badge.unpublished {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-badge.draft {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-badge.pending-review {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: transparent;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 12px;
|
||||
padding: 4px 10px;
|
||||
text-align: center;
|
||||
min-width: 70px;
|
||||
letter-spacing: 0.3px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.node-name:hover {
|
||||
border-color: #9ca3af;
|
||||
color: #374151;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.node-description {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.node-meta-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.node-meta-info .n-icon {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.node-actions {
|
||||
padding: 0 20px 20px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.node-actions .n-button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nodes-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.node-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1090,6 +1090,166 @@ async def delete_published_app(request: Request, payload: Dict[str, Any]):
|
||||
raise HTTPException(status_code=400, detail=result.get('message', '删除失败'))
|
||||
|
||||
|
||||
@router.get("/jingrow/my-published-nodes")
|
||||
async def get_my_published_nodes(
|
||||
request: Request,
|
||||
search: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
sort_by: Optional[str] = None
|
||||
):
|
||||
"""获取当前用户已发布的节点列表,支持搜索、分页和排序"""
|
||||
session_cookie = request.cookies.get('sid')
|
||||
if not session_cookie:
|
||||
raise HTTPException(status_code=401, detail="未提供认证信息")
|
||||
|
||||
url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.get_my_local_node_list"
|
||||
|
||||
# 构建参数
|
||||
params = {
|
||||
'order_by': sort_by or "name asc",
|
||||
'limit_start': (page - 1) * page_size,
|
||||
'limit_page_length': page_size
|
||||
}
|
||||
|
||||
if search:
|
||||
params['filters'] = json.dumps({"title": ["like", f"%{search}%"]}, ensure_ascii=False)
|
||||
|
||||
# 获取总数
|
||||
total_params = params.copy()
|
||||
total_params['limit_start'] = 0
|
||||
total_params['limit_page_length'] = 0
|
||||
|
||||
headers = get_jingrow_cloud_api_headers()
|
||||
headers['Cookie'] = f'sid={session_cookie}'
|
||||
|
||||
total_response = requests.get(url, params=total_params, headers=headers, timeout=20)
|
||||
total_response.raise_for_status()
|
||||
total_count = len(total_response.json().get('message', []))
|
||||
|
||||
# 获取分页数据
|
||||
response = requests.get(url, params=params, headers=headers, timeout=20)
|
||||
response.raise_for_status()
|
||||
nodes = response.json().get('message', [])
|
||||
|
||||
return {
|
||||
"items": nodes,
|
||||
"total": total_count,
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
|
||||
|
||||
@router.get("/jingrow/my-published-agents")
|
||||
async def get_my_published_agents(
|
||||
request: Request,
|
||||
search: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
sort_by: Optional[str] = None
|
||||
):
|
||||
"""获取当前用户已发布的智能体列表,支持搜索、分页和排序"""
|
||||
session_cookie = request.cookies.get('sid')
|
||||
if not session_cookie:
|
||||
raise HTTPException(status_code=401, detail="未提供认证信息")
|
||||
|
||||
url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.get_my_local_agent_list"
|
||||
|
||||
# 构建参数
|
||||
params = {
|
||||
'order_by': sort_by or "name asc",
|
||||
'limit_start': (page - 1) * page_size,
|
||||
'limit_page_length': page_size
|
||||
}
|
||||
|
||||
if search:
|
||||
params['filters'] = json.dumps({"title": ["like", f"%{search}%"]}, ensure_ascii=False)
|
||||
|
||||
# 获取总数
|
||||
total_params = params.copy()
|
||||
total_params['limit_start'] = 0
|
||||
total_params['limit_page_length'] = 0
|
||||
|
||||
headers = get_jingrow_cloud_api_headers()
|
||||
headers['Cookie'] = f'sid={session_cookie}'
|
||||
|
||||
total_response = requests.get(url, params=total_params, headers=headers, timeout=20)
|
||||
total_response.raise_for_status()
|
||||
total_count = len(total_response.json().get('message', []))
|
||||
|
||||
# 获取分页数据
|
||||
response = requests.get(url, params=params, headers=headers, timeout=20)
|
||||
response.raise_for_status()
|
||||
agents = response.json().get('message', [])
|
||||
|
||||
return {
|
||||
"items": agents,
|
||||
"total": total_count,
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
|
||||
|
||||
@router.post("/jingrow/delete-published-node")
|
||||
async def delete_published_node(request: Request, payload: Dict[str, Any]):
|
||||
"""删除已发布的节点,根据记录的name字段删除"""
|
||||
session_cookie = request.cookies.get('sid')
|
||||
if not session_cookie:
|
||||
raise HTTPException(status_code=401, detail="未提供认证信息")
|
||||
|
||||
# 使用记录的name字段
|
||||
record_name = payload.get('name')
|
||||
if not record_name:
|
||||
raise HTTPException(status_code=400, detail="记录名称不能为空")
|
||||
|
||||
url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.delete_local_node"
|
||||
|
||||
headers = get_jingrow_cloud_api_headers()
|
||||
headers['Cookie'] = f'sid={session_cookie}'
|
||||
|
||||
# 传递记录的name字段到云端API
|
||||
response = requests.post(url, json={'name': record_name}, headers=headers, timeout=20)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
result = data.get('message', data)
|
||||
|
||||
if result.get('success'):
|
||||
return {"success": True, "message": result.get('message', '节点删除成功')}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('message', '删除失败'))
|
||||
|
||||
|
||||
@router.post("/jingrow/delete-published-agent")
|
||||
async def delete_published_agent(request: Request, payload: Dict[str, Any]):
|
||||
"""删除已发布的智能体,根据记录的name字段删除"""
|
||||
session_cookie = request.cookies.get('sid')
|
||||
if not session_cookie:
|
||||
raise HTTPException(status_code=401, detail="未提供认证信息")
|
||||
|
||||
# 使用记录的name字段
|
||||
record_name = payload.get('name')
|
||||
if not record_name:
|
||||
raise HTTPException(status_code=400, detail="记录名称不能为空")
|
||||
|
||||
url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.delete_local_agent"
|
||||
|
||||
headers = get_jingrow_cloud_api_headers()
|
||||
headers['Cookie'] = f'sid={session_cookie}'
|
||||
|
||||
# 传递记录的name字段到云端API
|
||||
response = requests.post(url, json={'name': record_name}, headers=headers, timeout=20)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
result = data.get('message', data)
|
||||
|
||||
if result.get('success'):
|
||||
return {"success": True, "message": result.get('message', '智能体删除成功')}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('message', '删除失败'))
|
||||
|
||||
|
||||
@router.post("/jingrow/upload-image")
|
||||
async def upload_image(file: UploadFile = File(...)):
|
||||
"""上传应用图片"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user