feat: implement cross-account validation and improved error handling for file attachments in API tester

This commit is contained in:
CJACK
2026-04-13 03:15:12 +08:00
parent ffca8be597
commit acb110865f
9 changed files with 174 additions and 7 deletions

View File

@@ -109,6 +109,7 @@ export default function ApiTesterContainer({ config, onMessage, authFetch }) {
setMessage={setMessage}
attachedFiles={attachedFiles}
setAttachedFiles={setAttachedFiles}
setSelectedAccount={setSelectedAccount}
effectiveKey={effectiveKey}
selectedAccount={selectedAccount}
onMessage={onMessage}

View File

@@ -2,12 +2,15 @@ import { Bot, Loader2, Send, Square, User, Zap, Paperclip, X, FileIcon } from 'l
import clsx from 'clsx'
import { useRef, useState } from 'react'
import { getAttachedFileAccountIds } from './fileAccountBinding'
export default function ChatPanel({
t,
message,
setMessage,
attachedFiles = [],
setAttachedFiles,
setSelectedAccount,
effectiveKey,
selectedAccount,
onMessage,
@@ -32,6 +35,8 @@ export default function ChatPanel({
}
setUploadingFiles(true)
const initialSelectedAccount = String(selectedAccount || '').trim()
let boundAccount = initialSelectedAccount
for (const file of files) {
const formData = new FormData()
formData.append('file', file)
@@ -40,8 +45,8 @@ export default function ChatPanel({
const headers = {
'Authorization': `Bearer ${effectiveKey}`,
}
if (selectedAccount) {
headers['X-Ds2-Target-Account'] = selectedAccount
if (boundAccount) {
headers['X-Ds2-Target-Account'] = boundAccount
}
try {
@@ -57,11 +62,18 @@ export default function ChatPanel({
}
const data = await res.json()
setAttachedFiles(prev => [...prev, data])
const uploadedAccount = String(data?.account_id || '').trim()
if (!boundAccount && uploadedAccount) {
boundAccount = uploadedAccount
}
} catch (error) {
onMessage('error', error.message || 'Network error during upload')
}
}
setUploadingFiles(false)
if (!initialSelectedAccount && boundAccount && setSelectedAccount) {
setSelectedAccount(boundAccount)
}
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
@@ -70,6 +82,9 @@ export default function ChatPanel({
const removeFile = (id) => {
setAttachedFiles(prev => prev.filter(f => f.id !== id))
}
const attachmentAccountIds = getAttachedFileAccountIds(attachedFiles)
const attachmentAccountId = attachmentAccountIds.length === 1 ? attachmentAccountIds[0] : ''
return (
<div className="lg:col-span-9 flex flex-col bg-card border border-border rounded-xl shadow-sm overflow-hidden min-h-0 flex-1 relative">
<div className="flex-1 overflow-y-auto p-4 lg:p-6 space-y-8 custom-scrollbar scroll-smooth">
@@ -143,6 +158,16 @@ export default function ChatPanel({
))}
</div>
)}
{attachmentAccountIds.length > 1 && (
<div className="max-w-4xl mx-auto mb-3 text-[11px] text-amber-600">
{t('apiTester.fileAccountConflict')}
</div>
)}
{attachmentAccountId && (
<div className="max-w-4xl mx-auto mb-3 text-[11px] text-muted-foreground">
{t('apiTester.attachmentAccountHint', { account: attachmentAccountId })}
</div>
)}
<div className="max-w-4xl mx-auto relative group">
<input
type="file"

View File

@@ -0,0 +1,19 @@
export function getAttachedFileAccountIds(attachedFiles = []) {
const ids = []
const seen = new Set()
for (const file of attachedFiles || []) {
const raw = file?.account_id ?? file?.accountId ?? file?.owner_account_id ?? file?.ownerAccountId ?? ''
const id = String(raw).trim()
if (!id || seen.has(id)) continue
seen.add(id)
ids.push(id)
}
return ids
}
export function getAttachedFileAccountId(attachedFiles = []) {
const ids = getAttachedFileAccountIds(attachedFiles)
return ids.length > 0 ? ids[0] : ''
}

View File

@@ -1,5 +1,7 @@
import { useCallback } from 'react'
import { getAttachedFileAccountIds } from './fileAccountBinding'
export function useChatStreamClient({
t,
onMessage,
@@ -47,6 +49,20 @@ export function useChatStreamClient({
}
}, [t])
const resolveAttachmentAccount = useCallback(() => {
const ids = getAttachedFileAccountIds(attachedFiles)
if (ids.length > 1) {
return {
accountId: '',
error: t('apiTester.fileAccountConflict'),
}
}
return {
accountId: ids[0] || '',
error: '',
}
}, [attachedFiles, t])
const runTest = useCallback(async () => {
if (!effectiveKey) {
onMessage('error', t('apiTester.missingApiKey'))
@@ -63,12 +79,31 @@ export function useChatStreamClient({
abortControllerRef.current = new AbortController()
try {
const selectedAccountId = String(selectedAccount || '').trim()
const attachmentBinding = resolveAttachmentAccount()
if (attachmentBinding.error) {
setResponse({ success: false, error: attachmentBinding.error })
onMessage('error', attachmentBinding.error)
setLoading(false)
setIsStreaming(false)
return
}
if (attachmentBinding.accountId && selectedAccountId && selectedAccountId !== attachmentBinding.accountId) {
const errorMsg = t('apiTester.fileAccountMismatch', { account: attachmentBinding.accountId })
setResponse({ success: false, error: errorMsg })
onMessage('error', errorMsg)
setLoading(false)
setIsStreaming(false)
return
}
const requestAccount = selectedAccountId || attachmentBinding.accountId
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${effectiveKey}`,
}
if (selectedAccount) {
headers['X-Ds2-Target-Account'] = selectedAccount
if (requestAccount) {
headers['X-Ds2-Target-Account'] = requestAccount
}
const body = {
@@ -104,6 +139,8 @@ export function useChatStreamClient({
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let accumulatedThinking = ''
let accumulatedContent = ''
while (true) {
const { done, value } = await reader.read()
@@ -126,9 +163,11 @@ export function useChatStreamClient({
if (choice?.delta) {
const delta = choice.delta
if (delta.reasoning_content) {
accumulatedThinking += delta.reasoning_content
setStreamingThinking(prev => prev + delta.reasoning_content)
}
if (delta.content) {
accumulatedContent += delta.content
setStreamingContent(prev => prev + delta.content)
}
}
@@ -137,11 +176,26 @@ export function useChatStreamClient({
}
}
}
setResponse({
success: true,
status_code: res.status,
choices: [{
finish_reason: 'stop',
index: 0,
message: {
role: 'assistant',
content: accumulatedContent,
reasoning_content: accumulatedThinking,
},
}],
})
onMessage('success', t('apiTester.testSuccess', { account: requestAccount || selectedAccountId || 'Auto', time: Math.max(0, Date.now() - startedAt) }))
} else {
const data = await res.json()
setResponse({ success: true, status_code: res.status, ...data })
const elapsed = Math.max(0, Date.now() - startedAt)
onMessage('success', t('apiTester.testSuccess', { account: selectedAccount || 'Auto', time: elapsed }))
onMessage('success', t('apiTester.testSuccess', { account: requestAccount || 'Auto', time: elapsed }))
}
} catch (e) {
if (e.name === 'AbortError') {
@@ -163,6 +217,7 @@ export function useChatStreamClient({
message,
model,
onMessage,
resolveAttachmentAccount,
selectedAccount,
setIsStreaming,
setLoading,