feat(agents): 完成云智能体工作台与交互流程

This commit is contained in:
brother7 committed 2026-09-10 19:25:12 +08:00
1 parent 46fbae9dec
commit 64a3cf4934
26 files changed
+1955 -49

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
import { useEffect, useRef, useState } from 'react';
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { usePendingCloudInput } from './CloudPending';
import type { CloudKnowledge, CloudKnowledgeFile } from '../../../shared/cloud-agents';
const statusName: Record<string, string> = {
uploaded: '待解析', parsing: '解析中', parsed: '待索引', indexing: '索引中', indexed: '可检索',
error_parsing: '解析失败', error_indexing: '索引失败',
};
export function CloudKnowledgePanel({ slug, onCreated }: { slug: string; onCreated: (kb: CloudKnowledge) => void }) {
const [catalog, setCatalog] = useState<{ databases: CloudKnowledge[]; models: { id: string; name: string }[] } | null>(null);
const [kbId, setKbId] = useState('');
const [files, setFiles] = useState<CloudKnowledgeFile[]>([]);
const [next, setNext] = useState<number | null>(null);
const [name, setName] = useState('');
const [model, setModel] = useState('');
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
const [refresh, setRefresh] = useState(0);
const [creating, setCreating] = useState(false);
usePendingCloudInput(busy || (creating && Boolean(name.trim())));
const createIntent = useRef<{ operation_id: string; name: string; embedding_model: string } | null>(null);
const uploadIntent = useRef<string | null>(null);
const processIntents = useRef<Record<string, string>>({});
const alive = useRef(true);
const paged = useRef(false);
useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
useEffect(() => {
let current = true;
cloudAgentsApi.call('knowledge', { slug }).then(v => { if (current) setCatalog(v); })
.catch(e => { if (current) setError(String(e.message || e)); });
return () => { current = false; };
}, [slug, refresh]);
useEffect(() => {
if (!kbId) { setFiles([]); return; }
paged.current = false;
let current = true;
const load = () => cloudAgentsApi.call('knowledgeFiles', { slug, kb_id: kbId })
.then(v => { if (current) { setFiles(v.files); setNext(v.next_offset); } })
.catch(e => { if (current) setError(String(e.message || e)); });
void load();
const timer = setInterval(() => { if (!paged.current) void load(); }, 5000);
return () => { current = false; clearInterval(timer); };
}, [slug, kbId, refresh]); // A refreshed first page replaces pagination only after an explicit operation.
const act = async (operation: () => Promise<void>) => {
setBusy(true); setError('');
try { await operation(); }
catch(e) { if (alive.current) setError(e instanceof Error ? e.message : '知识库操作失败'); }
finally { if (alive.current) setBusy(false); }
};
return <details className="rounded-lg border p-4">
<summary className="min-h-8 cursor-pointer text-sm font-medium">管理我的知识文档</summary>
<div className="mt-4 space-y-4 text-sm">
<p className="text-xs leading-5 text-muted-foreground">上传到知识库的文档可供获准使用此智能体的人检索。解析后建立索引才会生效,向量调用扣除你的词元点数。</p>
{error && <p role="alert" className="text-destructive">{error}</p>}
<div className="flex gap-2">
<select aria-label="管理的知识库" className="h-10 min-w-0 flex-1 rounded-md border bg-background px-3" value={kbId} disabled={busy}
onChange={e => { setKbId(e.target.value); uploadIntent.current = null; setNext(null); }}>
<option value="">选择我的知识库</option>
{catalog?.databases.map(kb => <option key={kb.kb_id} value={kb.kb_id}>{kb.name}</option>)}
</select>
<Button type="button" variant="outline" disabled={busy} onClick={() => setCreating(v => !v)}>新建知识库</Button>
</div>
{creating && <div className="space-y-3 rounded-md bg-muted/30 p-3">
<Input aria-label="知识库名称" placeholder="知识库名称" value={name} disabled={busy || !!createIntent.current} onChange={e => setName(e.target.value)} />
<select aria-label="向量模型" className="h-10 w-full rounded-md border bg-background px-3" value={model} disabled={busy || !!createIntent.current} onChange={e => setModel(e.target.value)}>
<option value="">选择向量模型</option>{catalog?.models.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
</select>
{catalog && !catalog.models.length && <p className="text-xs text-muted-foreground">服务端尚未接入可计费的向量模型。</p>}
<Button type="button" disabled={busy || !name.trim() || !model} onClick={() => void act(async () => {
createIntent.current ??= { operation_id: crypto.randomUUID(), name: name.trim(), embedding_model: model };
const kb = await cloudAgentsApi.call('createKnowledge', { slug, ...createIntent.current });
createIntent.current = null;
if (alive.current) { setKbId(kb.kb_id); setCreating(false); setName(''); setRefresh(v => v + 1); onCreated(kb); }
})}>{createIntent.current ? '重试创建' : '创建并选用'}</Button>
</div>}
{kbId && <>
<div className="flex flex-wrap gap-2">
<Button type="button" disabled={busy} onClick={() => void act(async () => {
uploadIntent.current ??= crypto.randomUUID();
const uploaded = await cloudAgentsApi.uploadKnowledge(slug, kbId, uploadIntent.current);
if (!uploaded) return;
uploadIntent.current = null;
if (alive.current) setRefresh(v => v + 1);
})}>{uploadIntent.current ? '重新选择同一文件并重试' : '上传文档'}</Button>
<Button type="button" variant="ghost" disabled={busy} onClick={() => setRefresh(v => v + 1)}>刷新状态</Button>
</div>
<p className="text-xs text-muted-foreground">最大 5 MB;支持 TXT、Markdown、CSV、JSON、Word、Excel、文本 PDF。</p>
<div className="divide-y">
{files.map(file => <div key={file.file_id} className="flex items-start justify-between gap-3 py-3">
<div className="min-w-0"><p className="break-all">{file.name}</p><p className="mt-1 text-xs text-muted-foreground">{statusName[file.status] || file.status} · {file.chunk_count || 0} 个分块</p>
{file.error && <p className="mt-1 text-xs text-destructive">{file.error}</p>}</div>
{!['parsing', 'indexing', 'indexed'].includes(file.status) && <Button type="button" size="sm" variant="outline" disabled={busy}
onClick={() => void act(async () => {
processIntents.current[file.file_id] ??= crypto.randomUUID();
await cloudAgentsApi.call('processKnowledge', { slug, kb_id: kbId, file_id: file.file_id, operation_id: processIntents.current[file.file_id] });
delete processIntents.current[file.file_id];
if (alive.current) setRefresh(v => v + 1);
})}>{file.status.startsWith('error') ? '重试处理' : '解析并索引'}</Button>}
</div>)}
{!files.length && <p className="py-4 text-xs text-muted-foreground">尚未上传文档。</p>}
</div>
{next !== null && <Button type="button" variant="ghost" disabled={busy} onClick={() => void act(async () => {
paged.current = true;
const page = await cloudAgentsApi.call('knowledgeFiles', { slug, kb_id: kbId, offset: next });
if (alive.current) { setFiles(v => [...v, ...page.files]); setNext(page.next_offset); }
})}>加载更多文档</Button>}
</>}
</div>
</details>;
}