feat: Add "My Published Tools" page in Development menu

- Add MyPublishedTools component aligned with MyPublishedApps functionality
- Add backend API endpoints:
  - GET /jingrow/my-published-tools (list published tools with search, pagination, sorting)
  - POST /jingrow/delete-published-tool (delete published tool)
- Add route configuration for my-published-tools page
- Add menu item "My Published Tools" under Development group
- Add Chinese translations for all tool-related text
- Remove uppercase transformation for tool_name display in list

The new page provides the same functionality as "My Published Apps":
- Search and filter tools
- Sort by various criteria
- Pagination support
- View tool details
- Delete published tools
- Publish new tool button
This commit is contained in:
jingrow 2025-11-21 23:08:54 +08:00
parent 964f65dc1d
commit f702b8596a
6 changed files with 869 additions and 36 deletions

View File

@ -185,6 +185,12 @@ const router = createRouter({
name: 'MyPublishedAgents',
component: () => import('../../views/dev/MyPublishedAgents.vue'),
meta: { requiresAuth: true }
},
{
path: 'my-published-tools',
name: 'MyPublishedTools',
component: () => import('../../views/dev/MyPublishedTools.vue'),
meta: { requiresAuth: true }
}
]
}

View File

@ -1048,14 +1048,20 @@
"My Published Nodes": "已发布节点",
"My Published Agents": "已发布智能体",
"My Published Tools": "已发布工具",
"Manage your published nodes in the marketplace": "管理您在市场中发布的节点",
"Manage your published agents in the marketplace": "管理您在市场中发布的智能体",
"Manage your published tools in the marketplace": "管理您在工具市场中发布的工具",
"Node name does not exist": "节点名称不存在",
"Agent name does not exist": "智能体名称不存在",
"Are you sure you want to delete node \"{0}\"? This action cannot be undone.": "确定要删除节点 \"{0}\" 吗?此操作不可恢复。",
"Are you sure you want to delete agent \"{0}\"? This action cannot be undone.": "确定要删除智能体 \"{0}\" 吗?此操作不可恢复。",
"Node deleted successfully": "节点删除成功",
"Agent deleted successfully": "智能体删除成功",
"Tool name does not exist": "工具名称不存在",
"Tool deleted successfully": "工具删除成功",
"Are you sure you want to delete tool \"{0}\"? This action cannot be undone.": "确定要删除工具 \"{0}\" 吗?此操作不可恢复。",
"Publish Your First Tool": "发布您的第一个工具",
"Tools": "工具",
"Add Tool": "添加工具",

View File

@ -65,6 +65,7 @@ function getDefaultMenus(): AppMenuItem[] {
{ 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: 'My Published Nodes', icon: 'carbon:add-child-node', type: 'route', routeName: 'MyPublishedNodes', parentId: 'dev-group', order: 7.1 },
{ id: 'my-published-agents', key: 'MyPublishedAgents', label: 'My Published Agents', icon: 'hugeicons:robotic', type: 'route', routeName: 'MyPublishedAgents', parentId: 'dev-group', order: 7.2 },
{ id: 'my-published-tools', key: 'MyPublishedTools', label: 'My Published Tools', icon: 'tabler:tool', type: 'route', routeName: 'MyPublishedTools', parentId: 'dev-group', order: 7.3 },
{ 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 },

View File

@ -0,0 +1,687 @@
<template>
<div class="my-published-tools">
<div class="page-header">
<div class="header-content">
<div class="header-text">
<h1>{{ t('My Published Tools') }}</h1>
<p>{{ t('Manage your published tools in the marketplace') }}</p>
</div>
<n-button type="primary" @click="publishTool">
<template #icon>
<n-icon><Icon icon="tabler:plus" /></n-icon>
</template>
{{ t('Publish Tool') }}
</n-button>
</div>
</div>
<div class="content">
<div class="search-container">
<div class="search-bar">
<n-input
v-model:value="searchQuery"
:placeholder="t('Search tools...')"
clearable
size="large"
@keyup.enter="loadTools"
class="search-input"
>
<template #prefix>
<n-icon><Icon icon="tabler:search" /></n-icon>
</template>
</n-input>
<n-button type="primary" size="large" @click="loadTools" class="search-button">
<template #icon>
<n-icon><Icon icon="tabler:search" /></n-icon>
</template>
{{ t('Search') }}
</n-button>
</div>
</div>
<div class="tools-section" v-if="!loading && tools.length > 0">
<!-- 排序控件 -->
<div class="tools-header">
<div class="tools-title">
</div>
<div class="sort-controls">
<n-select
v-model:value="sortBy"
:options="sortOptions"
:placeholder="t('Sort by')"
style="width: 150px"
@update:value="loadTools"
/>
</div>
</div>
<div class="tools-grid">
<div v-for="tool in tools" :key="tool.name" class="tool-card">
<!-- 工具图片 -->
<div class="tool-image" @click="viewToolDetail(tool)">
<img
v-if="tool.tool_image"
:src="getImageUrl(tool.tool_image)"
:alt="tool.title || tool.name"
@error="handleImageError"
/>
<div v-else class="tool-image-placeholder">
<n-icon size="48"><Icon icon="tabler:tool" /></n-icon>
</div>
</div>
<!-- 工具信息 -->
<div class="tool-content">
<div class="tool-header">
<div class="tool-title-section">
<h3 @click="viewToolDetail(tool)" class="clickable-title">{{ tool.title || tool.name }}</h3>
<div class="tool-meta">
<div class="tool-team" v-if="tool.team">
<n-icon><Icon icon="tabler:users" /></n-icon>
<span>{{ tool.team }}</span>
</div>
<span v-if="tool.status" class="status-badge" :class="getStatusClass(tool.status)">
{{ t(tool.status) }}
</span>
</div>
</div>
<div class="tool-name" v-if="tool.tool_name">
{{ tool.tool_name }}
</div>
</div>
<div class="tool-subtitle" v-if="tool.subtitle">
{{ truncateText(tool.subtitle, 60) }}
</div>
</div>
<div class="tool-actions">
<n-button type="default" @click="viewToolDetail(tool)">
{{ t('View Details') }}
</n-button>
<n-button
type="error"
@click="deleteTool(tool)"
>
{{ 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="loadTools"
@update:page-size="handlePageSizeChange"
/>
</div>
</div>
<div v-if="loading" class="loading">
<n-spin size="large">
<template #description>{{ t('Loading tools...') }}</template>
</n-spin>
</div>
<div v-if="!loading && tools.length === 0" class="empty">
<n-empty :description="t('No tools found')">
<template #icon>
<n-icon><Icon icon="tabler:tool" /></n-icon>
</template>
<template #extra>
<n-button type="primary" @click="publishTool">
{{ t('Publish Your First Tool') }}
</n-button>
</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 tools = 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: 'tool_name asc' },
{ label: t('Name Z-A'), value: 'tool_name desc' },
{ label: t('Most Popular'), value: 'modified desc' }
])
//
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
async function loadTools() {
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-tools?${params}`)
const data = response.data
// API
if (data.items) {
tools.value = data.items
total.value = data.total || 0
} else {
// API
tools.value = data || []
total.value = tools.value.length
}
} catch (error) {
console.error('Failed to load tools:', error)
message.error(t('Failed to load tools'))
tools.value = []
total.value = 0
} finally {
loading.value = false
}
}
function publishTool() {
router.push('/publish-tool')
}
function handlePageSizeChange(newPageSize: number) {
pageSize.value = newPageSize
page.value = 1
localStorage.setItem('itemsPerPage', newPageSize.toString())
loadTools()
}
function viewToolDetail(tool: any) {
//
router.push({
path: `/tool-marketplace/${tool.name}`,
query: { returnTo: '/my-published-tools' }
})
}
function getImageUrl(imageUrl: string): string {
if (!imageUrl) return ''
if (imageUrl.startsWith('http')) {
return imageUrl
}
// 使URL
const cloudUrl = 'https://cloud.jingrow.com'
return `${cloudUrl}${imageUrl.startsWith('/') ? '' : '/'}${imageUrl}`
}
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement
img.style.display = 'none'
img.parentElement?.querySelector('.tool-image-placeholder')?.classList.add('show')
}
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 deleteTool(tool: any) {
// 使name
const recordName = tool.name
if (!recordName) {
message.error(t('Tool name does not exist'))
return
}
//
const toolTitle = tool.title || tool.tool_name || recordName
dialog.warning({
title: t('确认删除'),
content: t('Are you sure you want to delete tool "{0}"? This action cannot be undone.').replace('{0}', toolTitle),
positiveText: t('确认删除'),
negativeText: t('取消'),
onPositiveClick: async () => {
await performDelete(recordName)
}
})
}
async function performDelete(toolName: string) {
try {
// API
const response = await axios.post('/jingrow/delete-published-tool', {
name: toolName
}, {
withCredentials: true
})
if (response.data && response.data.success) {
message.success(response.data.message || t('Tool deleted successfully'))
//
loadTools()
} else {
const errorMsg = response.data?.message || response.data?.error || t('删除失败')
message.error(errorMsg)
}
} catch (error: any) {
console.error('Delete tool error:', error)
const errorMsg = error.response?.data?.detail ||
error.response?.data?.message ||
error.message ||
t('删除失败')
message.error(errorMsg)
}
}
onMounted(() => {
loadTools()
})
//
watch([searchQuery, sortBy], () => {
page.value = 1 //
loadTools()
}, { deep: true })
//
watch([page], () => {
loadTools()
})
//
watch(() => localStorage.getItem('itemsPerPage'), (newValue) => {
if (newValue) {
pageSize.value = parseInt(newValue)
page.value = 1 //
loadTools()
}
})
</script>
<style scoped>
.my-published-tools {
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;
}
.tools-section {
margin-bottom: 32px;
}
.tools-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.tools-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);
}
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.tool-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);
}
.tool-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
border-color: #d1d5db;
}
.tool-image {
position: relative;
width: 100%;
height: 200px;
overflow: hidden;
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
cursor: pointer;
transition: opacity 0.2s ease;
}
.tool-image:hover {
opacity: 0.9;
}
.tool-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.tool-card:hover .tool-image img {
transform: scale(1.05);
}
.tool-image-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #9ca3af;
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
}
.tool-image-placeholder.show {
display: flex;
}
.tool-content {
padding: 20px;
}
.tool-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.tool-title-section {
flex: 1;
margin-right: 12px;
}
.tool-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;
}
.tool-meta {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
}
.tool-team {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #6b7280;
font-weight: 500;
}
.tool-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;
}
.tool-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;
font-size: 10px;
transition: all 0.2s ease;
}
.tool-name:hover {
border-color: #9ca3af;
color: #374151;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tool-subtitle {
color: #6b7280;
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
}
.tool-actions {
padding: 0 20px 20px;
display: flex;
gap: 12px;
}
.tool-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%;
}
.tools-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.tool-card {
border-radius: 12px;
}
.tool-image {
height: 180px;
}
}
</style>

View File

@ -2,37 +2,37 @@
<!-- 根据视图模式决定容器类名组件自包含所有样式 -->
<div :class="containerClass">
<!-- 默认操作按钮 -->
<button class="action-btn" @click.stop="context.openDetail(context.row.name)" :title="context.t('View')">
<button class="action-btn" @click.stop="handleView" :title="t('View')">
<i class="fa fa-eye"></i>
</button>
<button class="action-btn" @click.stop="context.editRecord(context.row)" :title="context.t('Edit')">
<button class="action-btn" @click.stop="handleEdit" :title="t('Edit')">
<i class="fa fa-edit"></i>
</button>
<!-- Schema 编辑按钮 -->
<button
class="action-btn schema-btn"
@click.stop="handleOpenSchemaEditor"
:title="context.t('Edit Schema')"
:title="t('Edit Schema')"
:disabled="!canEditSchema"
>
<i class="fa fa-table"></i>
</button>
<button class="action-btn delete-btn" @click.stop="context.deleteRecord(context.row.name)" :title="context.t('Delete')">
<button class="action-btn delete-btn" @click.stop="handleDelete" :title="t('Delete')">
<i class="fa fa-trash"></i>
</button>
<!-- Schema 编辑器模态框 - 使用 Teleport 渲染到 body避免事件冒泡问题 -->
<Teleport to="body">
<SchemaEditorModal
v-model:visible="showSchemaEditor"
:node-type="nodeType"
:node-name="nodeName"
:initial-schema="initialSchema"
:on-save="handleSchemaSave"
@close="showSchemaEditor = false"
/>
</Teleport>
</div>
<!-- Schema 编辑器模态框 - 使用 Teleport 渲染到 body避免事件冒泡问题 -->
<Teleport to="body">
<SchemaEditorModal
v-model:visible="showSchemaEditor"
:node-type="nodeType"
:node-name="nodeName"
:initial-schema="initialSchema"
:on-save="handleSchemaSave"
@close="showSchemaEditor = false"
/>
</Teleport>
</template>
<script setup lang="ts">
@ -41,10 +41,12 @@ import { Teleport } from 'vue'
import { useMessage } from 'naive-ui'
import axios from 'axios'
import { get_session_api_headers } from '@/shared/api/auth'
import { t } from '@/shared/i18n'
import SchemaEditorModal from '@/core/components/SchemaEditorModal.vue'
const props = defineProps<{
context: {
interface Props {
// context props
context?: {
row: any
entity: string
openDetail: (name: string) => void
@ -54,21 +56,37 @@ const props = defineProps<{
t: (key: string) => string
viewMode?: 'card' | 'list'
}
}>()
row?: any
entity?: string
viewMode?: 'card' | 'list'
}
interface Emits {
(e: 'view', row: any): void
(e: 'edit', row: any): void
(e: 'delete', row: any): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// 使 props使 context
const row = computed(() => props.row || props.context?.row)
const entity = computed(() => props.entity || props.context?.entity)
const viewMode = computed(() => props.viewMode || props.context?.viewMode || 'list')
//
const containerClass = computed(() => {
const viewMode = props.context.viewMode || 'list'
return viewMode === 'card' ? 'actions-container card-actions' : 'actions-container col-actions'
return viewMode.value === 'card' ? 'actions-container card-actions' : 'actions-container col-actions'
})
const message = useMessage()
const showSchemaEditor = ref(false)
const nodeName = computed(() => props.context.row.name || '')
const nodeType = computed(() => props.context.row.node_type || '')
const nodeName = computed(() => row.value?.name || '')
const nodeType = computed(() => row.value?.node_type || '')
const initialSchema = computed(() => {
const schema = props.context.row.node_schema
const schema = row.value?.node_schema
if (!schema) return {}
if (typeof schema === 'string') {
try {
@ -84,17 +102,42 @@ const canEditSchema = computed(() => {
return !!nodeType.value
})
//
function handleView() {
if (props.context?.openDetail) {
props.context.openDetail(row.value.name)
} else {
emit('view', row.value)
}
}
function handleEdit() {
if (props.context?.editRecord) {
props.context.editRecord(row.value)
} else {
emit('edit', row.value)
}
}
function handleDelete() {
if (props.context?.deleteRecord) {
props.context.deleteRecord(row.value.name)
} else {
emit('delete', row.value)
}
}
async function handleOpenSchemaEditor() {
if (!canEditSchema.value) {
message.warning(props.context.t('Please select node type first'))
message.warning(t('Please select node type first'))
return
}
// node_schemaAPI
if (!props.context.row.node_schema) {
if (!row.value?.node_schema) {
try {
const response = await axios.get(
`/api/data/${encodeURIComponent(props.context.entity)}/${encodeURIComponent(nodeName.value)}`,
`/api/data/${encodeURIComponent(entity.value)}/${encodeURIComponent(nodeName.value)}`,
{
headers: get_session_api_headers(),
withCredentials: true
@ -103,12 +146,12 @@ async function handleOpenSchemaEditor() {
const record = response.data?.data || {}
// row 便使
if (record.node_schema) {
props.context.row.node_schema = record.node_schema
if (record.node_schema && row.value) {
row.value.node_schema = record.node_schema
}
} catch (error) {
console.error('获取节点数据失败:', error)
message.error(props.context.t('Failed to load node data'))
message.error(t('Failed to load node data'))
return
}
}
@ -120,7 +163,7 @@ async function handleSchemaSave(schemaData: any) {
try {
// Schema
const response = await axios.put(
`/api/data/${encodeURIComponent(props.context.entity)}/${encodeURIComponent(nodeName.value)}`,
`/api/data/${encodeURIComponent(entity.value)}/${encodeURIComponent(nodeName.value)}`,
{
node_schema: schemaData
},
@ -132,15 +175,17 @@ async function handleSchemaSave(schemaData: any) {
if (response.data?.success !== false) {
// row
props.context.row.node_schema = schemaData
message.success(props.context.t('Schema saved successfully'))
if (row.value) {
row.value.node_schema = schemaData
}
message.success(t('Schema saved successfully'))
showSchemaEditor.value = false
} else {
throw new Error(response.data?.message || props.context.t('Save failed'))
throw new Error(response.data?.message || t('Save failed'))
}
} catch (error: any) {
console.error('保存 Schema 失败:', error)
message.error(error?.response?.data?.message || error?.message || props.context.t('Save failed, please check permission and server logs'))
message.error(error?.response?.data?.message || error?.message || t('Save failed, please check permission and server logs'))
throw error //
}
}

View File

@ -6,7 +6,7 @@ Jingrow Tools API
工具相关的 FastAPI 路由
"""
from fastapi import APIRouter, HTTPException, Form, UploadFile, File
from fastapi import APIRouter, HTTPException, Form, UploadFile, File, Request
from typing import Dict, Any, Optional
import json
import requests
@ -444,3 +444,91 @@ async def publish_tool_to_marketplace(
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"发布工具失败: {str(e)}")
@router.get("/jingrow/my-published-tools")
async def get_my_published_tools(
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_tool_list"
# 构建参数
params = {
'order_by': sort_by or "tool_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}'
try:
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()
tools = response.json().get('message', [])
return {
"items": tools,
"total": total_count,
"page": page,
"page_size": page_size
}
except requests.exceptions.RequestException as e:
logger.error(f"获取已发布工具列表失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取已发布工具列表失败: {str(e)}")
@router.post("/jingrow/delete-published-tool")
async def delete_published_tool(request: Request, payload: Dict[str, Any]):
"""删除已发布的工具根据记录的name字段删除"""
session_cookie = request.cookies.get('sid')
if not session_cookie:
raise HTTPException(status_code=401, detail="未提供认证信息")
# 使用记录的name字段不是tool_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_tool"
headers = get_jingrow_cloud_api_headers()
headers['Cookie'] = f'sid={session_cookie}'
try:
# 传递记录的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', '删除失败'))
except requests.exceptions.RequestException as e:
logger.error(f"删除已发布工具失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"删除已发布工具失败: {str(e)}")