feat: add save feedback prompts, featured agent fields, dynamic Dify API config, and chat improvements
- Add success/error feedback messages near submit buttons in all admin forms - Display success prompt for 1s before redirect after save - Show API error details on save failure - Add isFeatured/featuredOrder fields to Agent model and admin UI - Add difyApiUrl/difyApiKey fields for per-agent Dify API configuration - Show featured badge column in agent admin list - Display featured agents on homepage sorted by order - Refactor chat page streaming with AbortController and stable userId - Improve Dify API proxy to use per-agent credentials
This commit is contained in:
@@ -18,7 +18,11 @@ export default function AgentForm({
|
|||||||
features?: string
|
features?: string
|
||||||
hotQuestions?: string
|
hotQuestions?: string
|
||||||
quickQuestions?: string
|
quickQuestions?: string
|
||||||
|
difyApiUrl?: string
|
||||||
|
difyApiKey?: string
|
||||||
status?: string
|
status?: string
|
||||||
|
isFeatured?: boolean
|
||||||
|
featuredOrder?: number
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -31,10 +35,15 @@ export default function AgentForm({
|
|||||||
features: agent?.features || "",
|
features: agent?.features || "",
|
||||||
hotQuestions: agent?.hotQuestions ? JSON.parse(agent.hotQuestions).join('\n') : '',
|
hotQuestions: agent?.hotQuestions ? JSON.parse(agent.hotQuestions).join('\n') : '',
|
||||||
quickQuestions: agent?.quickQuestions ? JSON.parse(agent.quickQuestions).join('\n') : '',
|
quickQuestions: agent?.quickQuestions ? JSON.parse(agent.quickQuestions).join('\n') : '',
|
||||||
|
difyApiUrl: agent?.difyApiUrl || "",
|
||||||
|
difyApiKey: agent?.difyApiKey || "",
|
||||||
status: agent?.status || "active",
|
status: agent?.status || "active",
|
||||||
|
isFeatured: agent?.isFeatured ?? false,
|
||||||
|
featuredOrder: agent?.featuredOrder ?? 0,
|
||||||
})
|
})
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
const [success, setSuccess] = useState(false)
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -50,6 +59,8 @@ export default function AgentForm({
|
|||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
...formData,
|
...formData,
|
||||||
|
isFeatured: formData.isFeatured,
|
||||||
|
featuredOrder: formData.featuredOrder,
|
||||||
hotQuestions: JSON.stringify(formData.hotQuestions.split('\n').filter(q => q.trim())),
|
hotQuestions: JSON.stringify(formData.hotQuestions.split('\n').filter(q => q.trim())),
|
||||||
quickQuestions: JSON.stringify(formData.quickQuestions.split('\n').filter(q => q.trim())),
|
quickQuestions: JSON.stringify(formData.quickQuestions.split('\n').filter(q => q.trim())),
|
||||||
}
|
}
|
||||||
@@ -61,9 +72,12 @@ export default function AgentForm({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
setSuccess(true)
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
router.push("/admin/agents")
|
router.push("/admin/agents")
|
||||||
} else {
|
} else {
|
||||||
setError("保存失败")
|
const data = await res.json().catch(() => ({}))
|
||||||
|
setError(data.error || "保存失败")
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("保存失败,请稍后重试")
|
setError("保存失败,请稍后重试")
|
||||||
@@ -185,6 +199,34 @@ export default function AgentForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-orange-800 mb-3">热门智能体配置</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.isFeatured}
|
||||||
|
onChange={(e) => setFormData({ ...formData, isFeatured: e.target.checked })}
|
||||||
|
className="w-5 h-5 text-orange-600 border-gray-300 rounded focus:ring-orange-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-gray-700">设为热门智能体(展示在首页热门区域)</span>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
排序(数字越小越靠前)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={formData.featuredOrder}
|
||||||
|
onChange={(e) => setFormData({ ...formData, featuredOrder: parseInt(e.target.value) || 0 })}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@@ -212,10 +254,49 @@ export default function AgentForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Dify API 地址
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.difyApiUrl}
|
||||||
|
onChange={(e) => setFormData({ ...formData, difyApiUrl: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="https://api.dify.ai/v1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Dify API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={formData.difyApiKey}
|
||||||
|
onChange={(e) => setFormData({ ...formData, difyApiKey: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="app-xxxxxxxxxxxx"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg text-sm">
|
||||||
|
保存成功
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading || success}
|
||||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-blue-400"
|
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-blue-400"
|
||||||
>
|
>
|
||||||
{loading ? "保存中..." : "保存"}
|
{loading ? "保存中..." : "保存"}
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ export default async function EditAgentPage({
|
|||||||
features: agent.features,
|
features: agent.features,
|
||||||
hotQuestions: agent.hotQuestions,
|
hotQuestions: agent.hotQuestions,
|
||||||
quickQuestions: agent.quickQuestions,
|
quickQuestions: agent.quickQuestions,
|
||||||
|
difyApiUrl: agent.difyApiUrl || "",
|
||||||
|
difyApiKey: agent.difyApiKey || "",
|
||||||
status: agent.status,
|
status: agent.status,
|
||||||
|
isFeatured: agent.isFeatured,
|
||||||
|
featuredOrder: agent.featuredOrder,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export default async function AdminAgentsPage() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">名称</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">名称</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">分类</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">分类</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">热门</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">使用次数</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">使用次数</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">操作</th>
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">操作</th>
|
||||||
@@ -59,6 +60,15 @@ export default async function AdminAgentsPage() {
|
|||||||
<td className="px-6 py-4 text-sm text-gray-600">
|
<td className="px-6 py-4 text-sm text-gray-600">
|
||||||
{agent.category?.name || "-"}
|
{agent.category?.name || "-"}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
{agent.isFeatured ? (
|
||||||
|
<span className="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-orange-100 text-orange-700">
|
||||||
|
热门
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-400">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
agent.status === "active"
|
agent.status === "active"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default function CategoryForm({
|
|||||||
})
|
})
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
const [success, setSuccess] = useState(false)
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -41,9 +42,12 @@ export default function CategoryForm({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
setSuccess(true)
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
router.push("/admin/categories")
|
router.push("/admin/categories")
|
||||||
} else {
|
} else {
|
||||||
setError("保存失败")
|
const data = await res.json().catch(() => ({}))
|
||||||
|
setError(data.error || "保存失败")
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("保存失败,请稍后重试")
|
setError("保存失败,请稍后重试")
|
||||||
@@ -100,10 +104,22 @@ export default function CategoryForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg text-sm">
|
||||||
|
保存成功
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading || success}
|
||||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-blue-400"
|
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-blue-400"
|
||||||
>
|
>
|
||||||
{loading ? "保存中..." : "保存"}
|
{loading ? "保存中..." : "保存"}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default function NewsForm({
|
|||||||
})
|
})
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
const [success, setSuccess] = useState(false)
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -43,9 +44,12 @@ export default function NewsForm({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
setSuccess(true)
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
router.push("/admin/news")
|
router.push("/admin/news")
|
||||||
} else {
|
} else {
|
||||||
setError("保存失败")
|
const data = await res.json().catch(() => ({}))
|
||||||
|
setError(data.error || "保存失败")
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("保存失败,请稍后重试")
|
setError("保存失败,请稍后重试")
|
||||||
@@ -121,10 +125,22 @@ export default function NewsForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg text-sm">
|
||||||
|
保存成功
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading || success}
|
||||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-blue-400"
|
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-blue-400"
|
||||||
>
|
>
|
||||||
{loading ? "保存中..." : "保存"}
|
{loading ? "保存中..." : "保存"}
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ export default function AgentChatPage() {
|
|||||||
const [currentConversationId, setCurrentConversationId] = useState<number | null>(null)
|
const [currentConversationId, setCurrentConversationId] = useState<number | null>(null)
|
||||||
const [upstreamConversationId, setUpstreamConversationId] = useState<string>('')
|
const [upstreamConversationId, setUpstreamConversationId] = useState<string>('')
|
||||||
const [loadingConversations, setLoadingConversations] = useState(false)
|
const [loadingConversations, setLoadingConversations] = useState(false)
|
||||||
|
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
const userIdRef = useRef<string>('user-' + Math.random().toString(36).slice(2, 10))
|
||||||
|
|
||||||
const CHAT_API_URL = '/api/chat'
|
const CHAT_API_URL = '/api/chat'
|
||||||
|
|
||||||
@@ -190,8 +192,6 @@ export default function AgentChatPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const assistantMessageId = (Date.now() + 1).toString()
|
const assistantMessageId = (Date.now() + 1).toString()
|
||||||
let assistantContent = ''
|
|
||||||
let upstreamConvId = upstreamConversationId
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
id: assistantMessageId,
|
id: assistantMessageId,
|
||||||
@@ -200,70 +200,70 @@ export default function AgentChatPage() {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}])
|
}])
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
setAbortController(controller)
|
||||||
|
|
||||||
const response = await fetch(CHAT_API_URL, {
|
const response = await fetch(CHAT_API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
agentId: agent.id,
|
||||||
inputs: {},
|
inputs: {},
|
||||||
query: currentInput,
|
query: currentInput,
|
||||||
response_mode: 'streaming',
|
response_mode: 'streaming',
|
||||||
conversation_id: upstreamConversationId,
|
conversation_id: upstreamConversationId,
|
||||||
user: 'user-' + Date.now(),
|
user: userIdRef.current,
|
||||||
}),
|
}),
|
||||||
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
const reader = response.body?.getReader()
|
if (!response.ok) {
|
||||||
const decoder = new TextDecoder()
|
throw new Error('Chat request failed')
|
||||||
|
}
|
||||||
|
|
||||||
if (reader) {
|
const reader = response.body!.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
|
let accumulatedContent = ''
|
||||||
|
let finalConvId = upstreamConversationId
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
if (done) break
|
if (done) break
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
const lines = buffer.split('\n')
|
const lines = buffer.split('\n')
|
||||||
buffer = lines.pop() || ''
|
buffer = lines.pop() || ''
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(line.slice(6))
|
const data = JSON.parse(line.slice(6))
|
||||||
if (json.conversation_id) {
|
if (data.event === 'message' && data.answer) {
|
||||||
upstreamConvId = json.conversation_id
|
accumulatedContent += data.answer
|
||||||
}
|
|
||||||
const token = json.answer || json.message || ''
|
|
||||||
if (token) {
|
|
||||||
assistantContent += token
|
|
||||||
setMessages(prev => prev.map(msg =>
|
setMessages(prev => prev.map(msg =>
|
||||||
msg.id === assistantMessageId
|
msg.id === assistantMessageId
|
||||||
? { ...msg, content: assistantContent }
|
? { ...msg, content: accumulatedContent }
|
||||||
: msg
|
: msg
|
||||||
))
|
))
|
||||||
|
} else if (data.event === 'message_end') {
|
||||||
|
finalConvId = data.conversation_id || finalConvId
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore parse errors
|
// skip invalid JSON
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpstreamConversationId(upstreamConvId)
|
setUpstreamConversationId(finalConvId)
|
||||||
|
|
||||||
if (!assistantContent) {
|
|
||||||
setMessages(prev => prev.map(msg =>
|
|
||||||
msg.id === assistantMessageId
|
|
||||||
? { ...msg, content: '抱歉,我暂时无法回答这个问题。' }
|
|
||||||
: msg
|
|
||||||
))
|
|
||||||
assistantContent = '抱歉,我暂时无法回答这个问题。'
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetch(`/api/conversations/${conversationId}/messages`, {
|
await fetch(`/api/conversations/${conversationId}/messages`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: assistantContent,
|
content: accumulatedContent,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ export async function PUT(
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
icon: data.icon || null,
|
icon: data.icon || null,
|
||||||
categoryId: data.categoryId ? parseInt(data.categoryId) : null,
|
categoryId: data.categoryId ? parseInt(data.categoryId) : null,
|
||||||
|
difyApiUrl: data.difyApiUrl || null,
|
||||||
|
difyApiKey: data.difyApiKey || null,
|
||||||
features: data.features || "",
|
features: data.features || "",
|
||||||
hotQuestions: data.hotQuestions || "[]",
|
hotQuestions: data.hotQuestions || "[]",
|
||||||
quickQuestions: data.quickQuestions || "[]",
|
quickQuestions: data.quickQuestions || "[]",
|
||||||
status: data.status || "active",
|
status: data.status || "active",
|
||||||
|
isFeatured: data.isFeatured ?? false,
|
||||||
|
featuredOrder: data.featuredOrder ?? 0,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return NextResponse.json(agent)
|
return NextResponse.json(agent)
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ export async function POST(request: NextRequest) {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
icon: data.icon || null,
|
icon: data.icon || null,
|
||||||
categoryId: data.categoryId ? parseInt(data.categoryId) : null,
|
categoryId: data.categoryId ? parseInt(data.categoryId) : null,
|
||||||
|
difyApiUrl: data.difyApiUrl || null,
|
||||||
|
difyApiKey: data.difyApiKey || null,
|
||||||
features: data.features || "",
|
features: data.features || "",
|
||||||
hotQuestions: data.hotQuestions || "[]",
|
hotQuestions: data.hotQuestions || "[]",
|
||||||
quickQuestions: data.quickQuestions || "[]",
|
quickQuestions: data.quickQuestions || "[]",
|
||||||
status: data.status || "active",
|
status: data.status || "active",
|
||||||
|
isFeatured: data.isFeatured ?? false,
|
||||||
|
featuredOrder: data.featuredOrder ?? 0,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return NextResponse.json(agent)
|
return NextResponse.json(agent)
|
||||||
|
|||||||
+37
-19
@@ -1,33 +1,51 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
|
import { prisma } from "@/app/lib/prisma"
|
||||||
|
|
||||||
const API_KEY = 'app-lbe2lglt7taGtZk0dG7pAhbx'
|
const FALLBACK_API_KEY = 'app-lbe2lglt7taGtZk0dG7pAhbx'
|
||||||
const API_URL = 'http://df.clkeji.com/v1/chat-messages'
|
const FALLBACK_API_URL = 'http://df.clkeji.com/v1/chat-messages'
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
|
const { agentId, ...difyBody } = body
|
||||||
|
|
||||||
const response = await fetch(API_URL, {
|
let apiKey = FALLBACK_API_KEY
|
||||||
method: 'POST',
|
let apiUrl = FALLBACK_API_URL
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${API_KEY}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (body.response_mode === 'streaming') {
|
if (agentId) {
|
||||||
return new NextResponse(response.body, {
|
const agent = await prisma.agent.findUnique({
|
||||||
headers: {
|
where: { id: parseInt(agentId) },
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
if (agent?.difyApiUrl && agent?.difyApiKey) {
|
||||||
|
apiUrl = agent.difyApiUrl.replace(/\/+$/, '') + '/chat-messages'
|
||||||
|
apiKey = agent.difyApiKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const response = await fetch(apiUrl, {
|
||||||
return NextResponse.json(data)
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ...difyBody, response_mode: 'streaming' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text()
|
||||||
|
console.error('Dify API error:', response.status, text.slice(0, 500))
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Dify request failed' },
|
||||||
|
{ status: 502 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.set('Content-Type', 'text/event-stream')
|
||||||
|
headers.set('Cache-Control', 'no-cache')
|
||||||
|
headers.set('Connection', 'keep-alive')
|
||||||
|
|
||||||
|
return new Response(response.body, { headers })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Chat API error:', error)
|
console.error('Chat API error:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import Image from "next/image"
|
|||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const agents = await prisma.agent.findMany({
|
const agents = await prisma.agent.findMany({
|
||||||
|
where: { isFeatured: true },
|
||||||
include: { category: true },
|
include: { category: true },
|
||||||
|
orderBy: { featuredOrder: "asc" },
|
||||||
take: 6,
|
take: 6,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1"
|
||||||
},
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "tsx prisma/seed.ts"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^22.13.1",
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Agent" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"icon" TEXT,
|
||||||
|
"categoryId" INTEGER,
|
||||||
|
"features" TEXT NOT NULL DEFAULT '',
|
||||||
|
"hotQuestions" TEXT NOT NULL DEFAULT '[]',
|
||||||
|
"quickQuestions" TEXT NOT NULL DEFAULT '[]',
|
||||||
|
"difyApiUrl" TEXT,
|
||||||
|
"difyApiKey" TEXT,
|
||||||
|
"usageCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'active',
|
||||||
|
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"featuredOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Agent_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Agent" ("categoryId", "createdAt", "description", "features", "icon", "id", "name", "slug", "status", "updatedAt", "usageCount") SELECT "categoryId", "createdAt", "description", "features", "icon", "id", "name", "slug", "status", "updatedAt", "usageCount" FROM "Agent";
|
||||||
|
DROP TABLE "Agent";
|
||||||
|
ALTER TABLE "new_Agent" RENAME TO "Agent";
|
||||||
|
CREATE UNIQUE INDEX "Agent_slug_key" ON "Agent"("slug");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -39,8 +39,12 @@ model Agent {
|
|||||||
features String @default("")
|
features String @default("")
|
||||||
hotQuestions String @default("[]")
|
hotQuestions String @default("[]")
|
||||||
quickQuestions String @default("[]")
|
quickQuestions String @default("[]")
|
||||||
|
difyApiUrl String?
|
||||||
|
difyApiKey String?
|
||||||
usageCount Int @default(0)
|
usageCount Int @default(0)
|
||||||
status String @default("active")
|
status String @default("active")
|
||||||
|
isFeatured Boolean @default(false)
|
||||||
|
featuredOrder Int @default(0)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
category Category? @relation(fields: [categoryId], references: [id])
|
category Category? @relation(fields: [categoryId], references: [id])
|
||||||
|
|||||||
+26
-7
@@ -113,20 +113,37 @@ async function main() {
|
|||||||
]),
|
]),
|
||||||
usageCount: 1000,
|
usageCount: 1000,
|
||||||
status: "active",
|
status: "active",
|
||||||
|
isFeatured: true,
|
||||||
|
featuredOrder: 1,
|
||||||
|
difyApiUrl: "https://api.dify.ai/v1",
|
||||||
|
difyApiKey: "app-demo-key-123456",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.agent.upsert({
|
prisma.agent.upsert({
|
||||||
where: { slug: "writing-assistant-pro" },
|
where: { slug: "huaiqi-secretary" },
|
||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
name: "写作助手 Pro",
|
name: "淮企小秘书",
|
||||||
slug: "writing-assistant-pro",
|
slug: "huaiqi-secretary",
|
||||||
description: "营销文案、博客文章、邮件草稿,输入关键词即可生成高质量内容。",
|
description: "专为淮安企业打造的智能秘书,提供政策解读、企业办事指南、惠企政策查询等一站式服务,助力企业高效运营。",
|
||||||
icon: "✍️",
|
icon: "🏢",
|
||||||
categoryId: categories[1].id,
|
categoryId: categories[6].id,
|
||||||
features: "营销文案, 博客文章, 邮件草稿",
|
features: "政策解读, 企业办事指南, 惠企政策, 智能问答",
|
||||||
|
hotQuestions: JSON.stringify([
|
||||||
|
"到淮安这边投资,项目审批快吗?",
|
||||||
|
"目前淮安对绿色工厂有哪些支持政策?",
|
||||||
|
"请问企业实施技术改造项目,淮安市有哪些支持政策?",
|
||||||
|
"淮安有哪些产业配套服务?",
|
||||||
|
]),
|
||||||
|
quickQuestions: JSON.stringify([
|
||||||
|
"惠企政策查询",
|
||||||
|
"企业办事指南",
|
||||||
|
"政策解读",
|
||||||
|
]),
|
||||||
usageCount: 850,
|
usageCount: 850,
|
||||||
status: "active",
|
status: "active",
|
||||||
|
isFeatured: true,
|
||||||
|
featuredOrder: 2,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.agent.upsert({
|
prisma.agent.upsert({
|
||||||
@@ -141,6 +158,8 @@ async function main() {
|
|||||||
features: "数据清洗, 数据可视化, 分析报告",
|
features: "数据清洗, 数据可视化, 分析报告",
|
||||||
usageCount: 620,
|
usageCount: 620,
|
||||||
status: "active",
|
status: "active",
|
||||||
|
isFeatured: true,
|
||||||
|
featuredOrder: 3,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.agent.upsert({
|
prisma.agent.upsert({
|
||||||
|
|||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
发送对话消息接口如下,该接口能正常调用
|
|
||||||
curl -X POST 'http://df.clkeji.com/v1/chat-messages' \
|
|
||||||
--header 'Authorization: Bearer app-lbe2lglt7taGtZk0dG7pAhbx' \
|
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--data '{
|
|
||||||
"inputs": {},
|
|
||||||
"query": "What are the specs of the iPhone 13 Pro Max?",
|
|
||||||
"response_mode": "streaming",
|
|
||||||
"conversation_id": "",
|
|
||||||
"user": "abc-123"
|
|
||||||
}'
|
|
||||||
|
|
||||||
问题描述:点击对话,功能异常,请排查原因并修复
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user