fix(knowledge): separate creation management and selection

This commit is contained in:
鲨鱼辣椒 committed 2026-10-13 13:57:35 +08:00
1 parent a589a864d7
commit 8e0e52f30e
11 files changed
+223 -112

No files matched your search

+27 -27
View File
@@ -1,5 +1,5 @@
import { useEffect, useId, useRef, useState } from 'react';
import { Upload } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Library, Upload } from 'lucide-react';
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -10,11 +10,10 @@ import { CloudKnowledgeDocuments } from './CloudKnowledgeDocuments';
export interface CloudOwnedKnowledgeCatalog { databases: CloudKnowledge[] | null; loading: boolean; error: string }
export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, onCatalog, refreshToken = 0, disabled = false, selectionFull = false, selectedIds = [], selectableIds }: {
export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, refreshToken = 0, disabled = false, selectableIds }: {
slug: string; onCreated: (kb: CloudKnowledge) => void; onDeleted?: (id: string) => void;
onCatalog?: (value: CloudOwnedKnowledgeCatalog) => void; refreshToken?: number; disabled?: boolean;
selectionFull?: boolean; selectedIds?: string[];
selectableIds?: string[] | null; onSelected?: (kb: CloudKnowledge) => void;
selectableIds?: string[] | null;
}) {
type CloudEmbeddingModel = CloudAgentOperations['knowledge']['output']['models'][number];
const [catalog, setCatalog] = useState<{ databases: CloudKnowledge[]; models: CloudEmbeddingModel[] } | null>(null);
@@ -22,13 +21,13 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on
const [name, setName] = useState('');
const [model, setModel] = useState('');
const [error, setError] = useState('');
const [errorTarget, setErrorTarget] = useState<'creation' | 'management'>('management');
const [catalogError, setCatalogError] = useState('');
const [catalogLoading, setCatalogLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [refresh, setRefresh] = useState(0);
const [creating, setCreating] = useState(false);
const [documentPending, setDocumentPending] = useState(false);
const targetId = useId();
const createIntent = useRef<{ operation_id: string; name: string; embedding_model: string } | null>(null);
usePendingCloudInput(busy || documentPending || Boolean(createIntent.current) || (creating && Boolean(name.trim())));
const operationInFlight = useRef(false);
@@ -51,10 +50,10 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on
.finally(() => { if (current) setCatalogLoading(false); });
return () => { current = false; };
}, [slug, refresh, refreshToken]);
const act = async (operation: () => Promise<void>) => {
const act = async (operation: () => Promise<void>, target: 'creation' | 'management' = 'management') => {
if (disabled || operationInFlight.current) return;
operationInFlight.current = true;
setBusy(true); setError('');
setBusy(true); setError(''); setErrorTarget(target);
try { await operation(); }
catch(e) { if (alive.current) setError(e instanceof Error ? e.message : '知识库操作失败'); }
finally { operationInFlight.current = false; if (alive.current) setBusy(false); }
@@ -65,7 +64,7 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on
const managementDisabled = disabled || busy || catalogLoading || Boolean(catalogError);
const bindable = (id: string) => selectableIds === undefined || Boolean(selectableIds?.includes(id));
const creationControls = <div className="space-y-3">
<Button type="button" variant="outline" disabled={disabled || busy || documentPending || selectionFull || Boolean(createIntent.current)} onClick={() => setCreating(v => !v)}>新建知识库</Button>
<Button type="button" variant="outline" disabled={disabled || busy || documentPending || Boolean(createIntent.current)} onClick={() => setCreating(v => !v)}>新建知识库</Button>
{creating && <div className="space-y-3 rounded-md bg-muted/30 p-3">
<Input aria-label="知识库名称" placeholder="知识库名称" value={name} disabled={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}
@@ -84,7 +83,7 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on
</ol>
</details>
</div>}
<Button type="button" disabled={disabled || busy || documentPending || (selectionFull && !createIntent.current) || !name.trim() || (!createIntent.current && (catalogLoading || !!catalogError || !catalog?.models.some(item => item.id === model)))} onClick={() => void act(async () => {
<Button type="button" disabled={disabled || busy || documentPending || !name.trim() || (!createIntent.current && (catalogLoading || !!catalogError || !catalog?.models.some(item => item.id === 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;
@@ -92,33 +91,35 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on
setCatalog(previous => previous ? { ...previous, databases: [kb, ...previous.databases.filter(value => value.kb_id !== kb.kb_id)] } : previous);
setKbId(kb.kb_id); setCreating(false); setName(''); setRefresh(v => v + 1); callbacks.current.onCreated(kb);
}
})}>{createIntent.current ? '重试创建' : '创建并选用'}</Button>
}, 'creation')}>{createIntent.current ? '重试创建' : '创建知识库'}</Button>
</div>}
{documentPending && selectedKnowledge && <p role="status" className="text-xs leading-5 text-amber-800">有未确认的文件操作,请先在当前知识库重试或确认状态,再切换知识库。</p>}
</div>;
return <section aria-label="上传自己的文件" className="agent-knowledge-upload space-y-4">
<div className="agent-knowledge-panel-heading"><Upload aria-hidden="true" className="size-5" /><h3>上传自己的文件</h3></div>
<div className="space-y-4 text-sm">
<p className="text-xs leading-5 text-muted-foreground">选择或新建自己的知识库,再从电脑上传资料。选用会加入右侧配置,保存并应用后生效。</p>
<label htmlFor={targetId} className="agent-knowledge-field-label">添加到我的知识库</label>
{error && <p role="alert" className="text-destructive">{error}</p>}
return <div className="agent-knowledge-owned-workspace">
<section aria-label="上传自己的文件" className="agent-knowledge-upload space-y-4">
<div className="agent-knowledge-panel-heading"><Upload aria-hidden="true" className="size-5" /><h3>上传自己的文件</h3></div>
{creationControls}
{errorTarget === 'creation' && error && <p role="alert" className="text-sm text-destructive">{error}</p>}
</section>
<section aria-label="查看我的知识库" className="agent-knowledge-management space-y-4 text-sm">
<div className="agent-knowledge-panel-heading"><Library aria-hidden="true" className="size-5" /><h3>查看我的知识库</h3></div>
{errorTarget === 'management' && error && <p role="alert" className="text-destructive">{error}</p>}
{catalogError && <p role="alert" className="text-destructive">知识库与模型目录加载失败:{catalogError}。请刷新重试。</p>}
<div className="flex flex-wrap gap-2">
<select id={targetId} aria-label="管理的知识库" className="h-10 min-w-0 flex-1 basis-48 rounded-md border bg-background px-3" value={kbId} disabled={managementDisabled || documentPending}
<select aria-label="管理的知识库" className="h-10 min-w-0 flex-1 basis-48 rounded-md border bg-background px-3" value={kbId} disabled={managementDisabled || documentPending}
onChange={e => {
const selected = manageable.find(kb => kb.kb_id === e.target.value);
if (managementDisabled || documentPending || (selected && bindable(selected.kb_id) && selectionFull && !selectedIds.includes(selected.kb_id))) return;
if (managementDisabled || documentPending) return;
setKbId(selected?.kb_id ?? ''); setError('');
if (selected) (onSelected ?? onCreated)(selected);
}}>
<option value="">{catalogLoading ? '正在读取我的知识库…' : '选择我的知识库'}</option>
{kbId && !selectedKnowledge && <option value={kbId} disabled>知识库管理权限待确认</option>}
{manageable.map(kb => <option key={kb.kb_id} value={kb.kb_id} disabled={bindable(kb.kb_id) && selectionFull && !selectedIds.includes(kb.kb_id)}>
{manageable.map(kb => <option key={kb.kb_id} value={kb.kb_id}>
{kb.name}{selectableIds === null ? '(选用状态待确认)' : !bindable(kb.kb_id) ? '(当前暂不可选用)' : ''}
</option>)}
</select>
<Button type="button" variant="ghost" disabled={disabled || busy || catalogLoading} onClick={() => setRefresh(v => v + 1)}>刷新知识库与模型</Button>
</div>
{documentPending && selectedKnowledge && <p role="status" className="text-xs leading-5 text-amber-800">有未确认的文件操作,请先在当前知识库重试或确认状态,再切换知识库。</p>}
<details className="agent-knowledge-help">
<summary>使用说明与费用</summary>
<p className="mt-2 text-xs leading-5 text-muted-foreground">上传到知识库的文档可供获准使用此智能体的人检索。解析并建立索引后才会生效;embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限。知识处理如触发对话模型调用,仍按原模型实际用量计费。</p>
@@ -134,13 +135,12 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on
</Button>
{documentPending && <p className="text-xs leading-5 text-muted-foreground">放弃只清除本地重试请求,不会删除已保存的文件。重新进入后可确认服务器状态。</p>}
</div>}
{!selectedKnowledge && <div className="space-y-4">
{!selectedKnowledge && <div>
<Button type="button" disabled title="请先选择或新建自己的知识库">从电脑上传</Button>
{creationControls}
</div>}
{kbId && <div hidden={!selectedKnowledge}><CloudKnowledgeDocuments key={slug + ':' + kbId} slug={slug} kbId={kbId} busy={managementDisabled || !selectedKnowledge}
canRead={Boolean(selectedKnowledge)} act={act} onPendingChange={setDocumentPending}
onDeleteKnowledge={() => { callbacks.current.onDeleted?.(kbId); setKbId(''); setDocumentPending(false); setRefresh(v => v + 1); }}>{selectedKnowledge && creationControls}</CloudKnowledgeDocuments></div>}
</div>
</section>;
onDeleteKnowledge={() => { callbacks.current.onDeleted?.(kbId); setKbId(''); setDocumentPending(false); setRefresh(v => v + 1); }} /></div>}
</section>
</div>;
}
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import { CheckCircle2, CircleAlert, FileText, Loader2 } from 'lucide-react';
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
import { Button } from '@/components/ui/button';
@@ -47,11 +47,11 @@ function documentState(file: CloudKnowledgeFile) {
return { label: '状态待确认', detail: '请刷新状态后再操作。', stage: 0 };
}
export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowledge, canRead = true, onPendingChange, children }: {
export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowledge, canRead = true, onPendingChange }: {
slug: string; kbId: string; busy: boolean;
act: (operation: () => Promise<void>) => Promise<void>;
onDeleteKnowledge: () => void;
canRead?: boolean; onPendingChange?: (pending: boolean) => void; children?: ReactNode;
canRead?: boolean; onPendingChange?: (pending: boolean) => void;
}) {
const [files, setFiles] = useState<CloudKnowledgeFile[]>([]);
const [pageCount, setPageCount] = useState(1);
@@ -155,7 +155,6 @@ export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowled
<Button type="button" disabled={busy || !canRead || (unresolved && !uploadIntents.current.new)} onClick={() => void upload()}>{uploadIntents.current.new ? '重新选择同一文件并重试' : '从电脑上传'}</Button>
<Button type="button" variant="ghost" disabled={loading || !canRead} onClick={refresh}>刷新状态</Button>
</div>
{children}
<p className="text-xs leading-5 text-muted-foreground">上传后自动解析并建立索引。最大 5 MB;支持 TXT、Markdown、CSV、JSON、Word、Excel、文本 PDF。</p>
{uploading && <p role="status" className="flex items-center gap-2 text-sm"><Loader2 className="size-4 animate-spin" />{uploading}</p>}
<div className="flex flex-wrap items-center justify-between gap-2 border-b pb-2 text-xs text-muted-foreground" aria-live="polite">
@@ -18,10 +18,9 @@ export function CloudKnowledgeSelector({ slug, options, selected, disabled, load
const add = (id: string) => { if (!selectionFull && !selected.includes(id)) onChange([...selected, id]); };
return <div className="agent-knowledge-workspace">
<CloudKnowledgePanel key={slug} slug={slug} disabled={disabled} onCatalog={setOwned} refreshToken={refreshToken}
selectionFull={selectionFull} selectedIds={selected} selectableIds={loading || error ? null : options.map(option => option.key)}
onSelected={kb => { if (!loading && !error && options.some(option => option.key === kb.kb_id)) { add(kb.kb_id); onRefresh(); } }}
selectableIds={loading || error ? null : options.map(option => option.key)}
onDeleted={id => { onChange(selected.filter(value => value !== id)); onRefresh(); }}
onCreated={kb => { add(kb.kb_id); onRefresh(); }} />
onCreated={onRefresh} />
<section aria-label="选择知识库" className="agent-knowledge-selection space-y-4">
<div className="agent-knowledge-panel-heading"><Library aria-hidden="true" className="size-5" /><h3>选择知识库</h3></div>
<p className="text-xs leading-5 text-muted-foreground">从 MakeLore知识和我的知识库中选择,可同时使用多个。</p>
+7 -6
View File
@@ -42,20 +42,20 @@
.agent-config-resource-manager { margin-top: 18px; }
.agent-config-resource-manager > details { border-radius: 14px; background: #ffffffa8; }
.agent-resource-card:has(.agent-knowledge-workspace) { container-type: inline-size; }
.agent-knowledge-workspace { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); align-items: start; gap: 18px; min-width: 0; }
.agent-knowledge-upload, .agent-knowledge-selection { min-width: 0; padding: 18px; border: 1px solid #dce5e0; border-radius: 17px; background: #fffdfb; overflow-wrap: anywhere; }
.agent-knowledge-workspace { display: grid; gap: 18px; min-width: 0; }
.agent-knowledge-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); align-items: start; gap: 18px; min-width: 0; }
.agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { min-width: 0; padding: 18px; border: 1px solid #dce5e0; border-radius: 17px; background: #fffdfb; overflow-wrap: anywhere; }
.agent-knowledge-upload { border-color: #dce5f3; }
.agent-knowledge-panel-heading { display: flex; align-items: center; gap: 9px; color: #33485c; }
.agent-knowledge-panel-heading h3 { font-size: 16px; font-weight: 650; line-height: 1.5; }
.agent-knowledge-panel-heading svg { flex-shrink: 0; color: #607d72; }
.agent-knowledge-upload .agent-knowledge-panel-heading svg { color: #4a6e9c; }
.agent-knowledge-field-label { display: block; font-size: 13px; font-weight: 600; color: #4c586b; }
.agent-knowledge-help { border-top: 1px solid #e8e6e1; padding-top: 12px; }
.agent-knowledge-help summary { min-height: 28px; width: fit-content; cursor: pointer; color: #606c7e; font-size: 12px; line-height: 1.7; }
.agent-knowledge-help summary:focus-visible { outline: 2px solid #9a7bb5; outline-offset: 3px; border-radius: 4px; }
.agent-knowledge-selection ul { max-height: 480px; overflow: auto; overscroll-behavior: contain; }
.agent-knowledge-selection button { flex-shrink: 0; }
@container (max-width: 620px) { .agent-knowledge-workspace { grid-template-columns: minmax(0, 1fr); } }
@container (max-width: 620px) { .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 640px) {
.agent-editor-tabs { padding: 10px 12px; }
.agent-editor-tabs [role="tab"] { gap: 6px; padding-inline: 8px; font-size: 14px; }
@@ -67,7 +67,8 @@
.agent-config-heading-title { gap: 6px; }
.agent-config-icon { width: 38px; height: 38px; border-radius: 13px; }
.agent-config-section-top > button { margin-left: 48px; }
.agent-knowledge-workspace { grid-template-columns: minmax(0, 1fr); gap: 14px; }
.agent-knowledge-upload, .agent-knowledge-selection { padding: 14px; border-radius: 14px; }
.agent-knowledge-workspace, .agent-knowledge-owned-workspace { gap: 14px; }
.agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); }
.agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { padding: 14px; border-radius: 14px; }
}
@media (prefers-reduced-motion: reduce) { .agent-resource-option { transition: none; } }