feat: 支持工作流知识库多库检索
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import {usePreferences} from '@easyflow/preferences';
|
||||
import {getOptions, sortNodes} from '@easyflow/utils';
|
||||
import {sortNodes} from '@easyflow/utils';
|
||||
import {Tinyflow} from '@tinyflow-ai/vue';
|
||||
|
||||
import {ArrowLeft, CircleCheck, Close, Promotion,} from '@element-plus/icons-vue';
|
||||
@@ -220,7 +220,14 @@ const provider = computed(() => ({
|
||||
description: item.description,
|
||||
};
|
||||
}),
|
||||
knowledge: () => getOptions('title', 'id', knowledgeList.value),
|
||||
knowledge: () => knowledgeList.value.map((item: any) => ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
description: item.description,
|
||||
embeddingModelId: item.vectorEmbedModelId,
|
||||
embeddingDimension: item.dimensionOfVectorModel,
|
||||
vectorStoreEnabled: item.vectorStoreEnabled,
|
||||
})),
|
||||
searchEngine: (): any => [
|
||||
{
|
||||
value: 'bocha-search',
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { flushSync, mount, unmount } from 'svelte';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import Select from './select.svelte';
|
||||
|
||||
describe('Select multiple interaction', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('keeps the list open across selections and restores focus on Escape', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const app = mount(Select, {
|
||||
target: host,
|
||||
props: {
|
||||
items: [
|
||||
{ label: '知识库一', value: 1 },
|
||||
{ label: '知识库二', value: 2 },
|
||||
{
|
||||
disabledReason: '向量配置不可用',
|
||||
label: '知识库三',
|
||||
selectable: false,
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
multiple: true,
|
||||
onSelect,
|
||||
value: [2, 1],
|
||||
},
|
||||
});
|
||||
flushSync();
|
||||
|
||||
const trigger = host.querySelector<HTMLButtonElement>('.tf-select-input')!;
|
||||
expect(trigger.textContent).toContain('知识库二');
|
||||
expect(trigger.textContent).toContain('+1');
|
||||
trigger.click();
|
||||
flushSync();
|
||||
|
||||
const listbox = host.querySelector<HTMLElement>('[role="listbox"]')!;
|
||||
const floating = listbox.parentElement!;
|
||||
const options = listbox.querySelectorAll<HTMLButtonElement>('[role="option"]');
|
||||
expect(floating.style.display).toBe('block');
|
||||
options[0].click();
|
||||
options[1].click();
|
||||
expect(onSelect).toHaveBeenCalledTimes(2);
|
||||
expect(floating.style.display).toBe('block');
|
||||
expect(options[2].getAttribute('aria-label')).toContain('向量配置不可用');
|
||||
options[2].click();
|
||||
expect(onSelect).toHaveBeenCalledTimes(2);
|
||||
expect(floating.style.display).toBe('block');
|
||||
|
||||
listbox.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }),
|
||||
);
|
||||
flushSync();
|
||||
expect(floating.style.display).toBe('none');
|
||||
expect(document.activeElement).toBe(trigger);
|
||||
|
||||
await unmount(app);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
isArrayDataType,
|
||||
resolveLoopOutputDataType,
|
||||
} from '../../utils/loopScope';
|
||||
import {resolveReferencedParameterDataType} from '../utils/referenceDataType';
|
||||
|
||||
onMount(() => {
|
||||
if (!param.refType) {
|
||||
@@ -122,7 +123,13 @@
|
||||
const updateRef = (item: any) => {
|
||||
const newValue = item.value;
|
||||
if (!loopOutputAggregation) {
|
||||
updateParam('ref', newValue);
|
||||
updateParams({
|
||||
ref: newValue,
|
||||
dataType: resolveReferencedParameterDataType(
|
||||
param,
|
||||
item.dataType
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
const dataType = item.dataType || 'String';
|
||||
@@ -182,6 +189,22 @@
|
||||
() => acceptedContentTypes,
|
||||
() => param.ref || ''
|
||||
);
|
||||
$effect(() => {
|
||||
const referencedDataType = selectItems.selected?.dataType;
|
||||
const nextDataType = resolveReferencedParameterDataType(
|
||||
param,
|
||||
referencedDataType
|
||||
);
|
||||
if (
|
||||
!loopOutputAggregation &&
|
||||
param.refType === 'ref' &&
|
||||
param.ref &&
|
||||
referencedDataType &&
|
||||
param.dataType !== nextDataType
|
||||
) {
|
||||
updateParam('dataType', nextDataType);
|
||||
}
|
||||
});
|
||||
let sourceDataType = $derived.by(() => {
|
||||
return selectItems.selected?.dataType || param.dataType || 'String';
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import {type NodeProps, useNodesData, useStore, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {Heading, Select} from '../base';
|
||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||
import {getOptions} from '../utils/NodeUtils';
|
||||
import {onMount} from 'svelte';
|
||||
import OutputDefList from '../core/OutputDefList.svelte';
|
||||
@@ -17,6 +16,12 @@
|
||||
syncManagedParametersForFields,
|
||||
updateFieldBindingMeta,
|
||||
} from '../../utils/workflowNodeFields';
|
||||
import {
|
||||
buildKnowledgeSelectItems,
|
||||
ensureKnowledgeOutputDefs,
|
||||
normalizeKnowledgeIds,
|
||||
toggleKnowledgeId,
|
||||
} from '../utils/knowledgeNode';
|
||||
|
||||
const { data, ...rest }: {
|
||||
data: TinyflowNodeData,
|
||||
@@ -25,7 +30,6 @@
|
||||
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
let currentNode = useNodesData(currentNodeId);
|
||||
const { addParameter } = useAddParameter();
|
||||
const { nodes, edges } = $derived(useStore());
|
||||
const editorParameters = $derived.by(() => {
|
||||
return buildEditorReferenceParameters(
|
||||
@@ -37,6 +41,7 @@
|
||||
});
|
||||
|
||||
const options = getOptions();
|
||||
const reloadKnowledgeValue = '__reload_knowledge_options__';
|
||||
const retrievalModeOptions: SelectItem[] = [
|
||||
{ value: 'HYBRID', label: '混合检索' },
|
||||
{ value: 'VECTOR', label: '向量检索' },
|
||||
@@ -44,10 +49,33 @@
|
||||
];
|
||||
|
||||
let knowledgeArray = $state<SelectItem[]>([]);
|
||||
onMount(async () => {
|
||||
const newLLMs = await options.provider?.knowledge?.();
|
||||
knowledgeArray.push(...(newLLMs || []));
|
||||
});
|
||||
let knowledgeLoading = $state(false);
|
||||
let knowledgeLoadError = $state('');
|
||||
const selectedKnowledgeIds = $derived(normalizeKnowledgeIds(data));
|
||||
const isMultiKnowledge = $derived(selectedKnowledgeIds.length > 1);
|
||||
const selectableKnowledgeItems = $derived(
|
||||
buildKnowledgeSelectItems(
|
||||
knowledgeArray,
|
||||
selectedKnowledgeIds,
|
||||
knowledgeLoadError
|
||||
? { value: reloadKnowledgeValue, label: '重新加载知识库' }
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
|
||||
async function loadKnowledges() {
|
||||
knowledgeLoading = true;
|
||||
knowledgeLoadError = '';
|
||||
try {
|
||||
knowledgeArray = [...((await options.provider?.knowledge?.()) || [])];
|
||||
} catch (error) {
|
||||
knowledgeLoadError = error instanceof Error ? error.message : '知识库加载失败';
|
||||
} finally {
|
||||
knowledgeLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(loadKnowledges);
|
||||
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
const syncFieldValue = (fieldName: 'keyword' | 'limit', nextValue: string) => {
|
||||
@@ -67,59 +95,40 @@
|
||||
});
|
||||
};
|
||||
|
||||
const toggleKnowledge = (item: SelectItem) => {
|
||||
if (item.value === reloadKnowledgeValue) {
|
||||
loadKnowledges();
|
||||
return;
|
||||
}
|
||||
const nextIds = toggleKnowledgeId(selectedKnowledgeIds, item.value);
|
||||
updateNodeData(currentNodeId, {
|
||||
knowledgeIds: nextIds,
|
||||
knowledgeId: undefined,
|
||||
retrievalMode: nextIds.length > 1
|
||||
? 'VECTOR'
|
||||
: (data.retrievalMode || 'HYBRID'),
|
||||
});
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (!data.outputDefs || data.outputDefs.length === 0) {
|
||||
addParameter(currentNodeId, 'outputDefs',
|
||||
{
|
||||
name: 'documents',
|
||||
dataType: 'Array',
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
addChildDisabled: true,
|
||||
deleteDisabled: true,
|
||||
children: [
|
||||
{
|
||||
name: 'title',
|
||||
dataType: 'String',
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
deleteDisabled: true
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
dataType: 'String',
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
deleteDisabled: true
|
||||
},
|
||||
{
|
||||
name: 'documentId',
|
||||
dataType: 'Number',
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
deleteDisabled: true
|
||||
},
|
||||
{
|
||||
name: 'knowledgeId',
|
||||
dataType: 'Number',
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
deleteDisabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
const nextOutputDefs = ensureKnowledgeOutputDefs(data.outputDefs);
|
||||
if (JSON.stringify(nextOutputDefs) !== JSON.stringify(data.outputDefs || [])) {
|
||||
updateNodeData(currentNodeId, { outputDefs: nextOutputDefs });
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!data.retrievalMode) {
|
||||
updateNodeData(currentNodeId, () => {
|
||||
return {
|
||||
retrievalMode: 'HYBRID'
|
||||
};
|
||||
if (!Array.isArray(data.knowledgeIds) && data.knowledgeId !== undefined) {
|
||||
updateNodeData(currentNodeId, {
|
||||
knowledgeIds: normalizeKnowledgeIds(data),
|
||||
knowledgeId: undefined,
|
||||
});
|
||||
}
|
||||
if (isMultiKnowledge && data.retrievalMode !== 'VECTOR') {
|
||||
updateNodeData(currentNodeId, { retrievalMode: 'VECTOR' });
|
||||
} else if (!data.retrievalMode) {
|
||||
updateNodeData(currentNodeId, { retrievalMode: 'HYBRID' });
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -136,14 +145,17 @@
|
||||
<Heading level={3} mt="10px">知识库设置</Heading>
|
||||
<div class="setting-title">知识库</div>
|
||||
<div class="setting-item">
|
||||
<Select items={knowledgeArray} style="width: 100%" placeholder="请选择知识库" onSelect={(item)=>{
|
||||
const newValue = item.value;
|
||||
updateNodeData(currentNodeId, ()=>{
|
||||
return {
|
||||
knowledgeId: newValue
|
||||
}
|
||||
})
|
||||
}} value={data.knowledgeId ? [data.knowledgeId] : []} />
|
||||
<Select
|
||||
items={selectableKnowledgeItems}
|
||||
style="width: 100%"
|
||||
placeholder={knowledgeLoading
|
||||
? '正在加载知识库…'
|
||||
: (knowledgeLoadError ? '知识库加载失败' : '请选择知识库')}
|
||||
onSelect={toggleKnowledge}
|
||||
value={selectedKnowledgeIds}
|
||||
multiple
|
||||
disabled={knowledgeLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-title">关键字</div>
|
||||
@@ -169,7 +181,8 @@
|
||||
retrievalMode: newValue
|
||||
}
|
||||
})
|
||||
}} value={data.retrievalMode ? [data.retrievalMode] : ['HYBRID']} />
|
||||
}} value={data.retrievalMode ? [data.retrievalMode] : ['HYBRID']}
|
||||
disabled={isMultiKnowledge} />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
applyEmbeddingCompatibility,
|
||||
buildKnowledgeSelectItems,
|
||||
ensureKnowledgeOutputDefs,
|
||||
normalizeKnowledgeIds,
|
||||
toggleKnowledgeId,
|
||||
} from './knowledgeNode';
|
||||
|
||||
describe('knowledge node helpers', () => {
|
||||
it('prefers canonical knowledgeIds over legacy knowledgeId', () => {
|
||||
expect(normalizeKnowledgeIds({ knowledgeId: '1', knowledgeIds: ['2', '3', '2'] }))
|
||||
.toEqual(['2', '3']);
|
||||
expect(normalizeKnowledgeIds({ knowledgeId: '1' })).toEqual(['1']);
|
||||
expect(normalizeKnowledgeIds({ knowledgeIds: [1, '1', ' 2 '] })).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('toggles one selected knowledge without changing order', () => {
|
||||
expect(toggleKnowledgeId(['1'], '2')).toEqual(['1', '2']);
|
||||
expect(toggleKnowledgeId(['1', '2'], '1')).toEqual(['2']);
|
||||
expect(toggleKnowledgeId([1], '1')).toEqual([]);
|
||||
});
|
||||
|
||||
it('disables incompatible unselected knowledge bases', () => {
|
||||
const items = applyEmbeddingCompatibility([
|
||||
{ value: '1', label: '一', embeddingModelId: 'm1', embeddingDimension: 3, vectorStoreEnabled: true },
|
||||
{ value: '2', label: '二', embeddingModelId: 'm1', embeddingDimension: 3, vectorStoreEnabled: true },
|
||||
{ value: '3', label: '三', embeddingModelId: 'm2', embeddingDimension: 3, vectorStoreEnabled: true },
|
||||
], ['1']);
|
||||
|
||||
expect(items[1]?.selectable).toBe(true);
|
||||
expect(items[2]?.selectable).toBe(false);
|
||||
expect(items[2]?.disabledReason).toContain('Embedding');
|
||||
});
|
||||
|
||||
it('keeps knowledge bases selectable when an older options API omits vector metadata', () => {
|
||||
const items = applyEmbeddingCompatibility([
|
||||
{ value: '1', label: '一' },
|
||||
{ value: '2', label: '二' },
|
||||
{ value: '3', label: '三' },
|
||||
], ['1']);
|
||||
|
||||
expect(items.every((item) => item.selectable !== false)).toBe(true);
|
||||
expect(items.every((item) => item.disabledReason === undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not let unknown metadata override an explicit unavailable state', () => {
|
||||
const unknownFirst = applyEmbeddingCompatibility([
|
||||
{ value: '1', label: '旧接口知识库' },
|
||||
{ value: '2', label: '明确不可用', vectorStoreEnabled: false },
|
||||
], ['1']);
|
||||
expect(unknownFirst[1]?.selectable).toBe(false);
|
||||
|
||||
const unavailableFirst = applyEmbeddingCompatibility([
|
||||
{ value: '1', label: '明确不可用', vectorStoreEnabled: false },
|
||||
{ value: '2', label: '旧接口知识库' },
|
||||
], ['1']);
|
||||
expect(unavailableFirst[1]?.selectable).toBe(false);
|
||||
});
|
||||
|
||||
it('requires a missing selected knowledge base to be removed before adding another', () => {
|
||||
const items = applyEmbeddingCompatibility([
|
||||
{ value: '2', label: '二' },
|
||||
], ['missing']);
|
||||
|
||||
expect(items.find((item) => item.value === '2')?.selectable).toBe(false);
|
||||
expect(items.find((item) => item.value === 'missing')?.selectable).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the reload action selectable when loading fails with an existing selection', () => {
|
||||
const items = buildKnowledgeSelectItems([], ['1'], {
|
||||
value: '__reload__',
|
||||
label: '重新加载知识库',
|
||||
});
|
||||
|
||||
expect(items.find((item) => item.value === '1')?.selectable).toBe(true);
|
||||
expect(items.find((item) => item.value === '__reload__')?.selectable).toBe(true);
|
||||
expect(items.find((item) => item.value === '__reload__')?.disabledReason).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps invalid selected knowledge bases visible and removable', () => {
|
||||
const items = applyEmbeddingCompatibility([
|
||||
{ value: 1, label: '一', embeddingModelId: 'm1', embeddingDimension: 3, vectorStoreEnabled: true },
|
||||
{ value: 2, label: '二', embeddingModelId: 'm2', embeddingDimension: 3, vectorStoreEnabled: true },
|
||||
], ['1', '2', '9']);
|
||||
|
||||
expect(items.map((item) => item.value)).toEqual(['1', '2', '9']);
|
||||
expect(items[1]?.selectable).toBe(true);
|
||||
expect(items[1]?.disabledReason).toContain('仅可移除');
|
||||
expect(items[2]?.label).toContain('已失效');
|
||||
expect(items[2]?.disabledReason).toContain('仅可移除');
|
||||
});
|
||||
|
||||
it('restores legacy output definitions without exposing retrieval diagnostics', () => {
|
||||
const outputDefs = ensureKnowledgeOutputDefs([
|
||||
{ id: 'documents-id', name: 'documents', dataType: 'Array', children: [
|
||||
{ name: 'title', dataType: 'String' },
|
||||
] },
|
||||
{ name: 'retrievalSummary', dataType: 'Object' },
|
||||
]);
|
||||
|
||||
expect(outputDefs.map((item) => item.name)).toEqual(['documents']);
|
||||
expect(outputDefs[0]?.id).toBe('documents-id');
|
||||
expect(outputDefs[0]?.children?.find((item) => item.name === 'title')?.id)
|
||||
.toBe('knowledge_documents_title');
|
||||
expect(outputDefs[0]?.children?.map((item) => item.name)).toEqual([
|
||||
'title',
|
||||
'content',
|
||||
'documentId',
|
||||
'knowledgeId',
|
||||
]);
|
||||
const ids = outputDefs.flatMap((item) => [item.id, ...(item.children || []).map((child) => child.id)]);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import type { Parameter, SelectItem } from '#types';
|
||||
|
||||
export function normalizeKnowledgeIds(data: Record<string, any>): string[] {
|
||||
if (Array.isArray(data.knowledgeIds)) {
|
||||
const seen = new Set<string>();
|
||||
return data.knowledgeIds.flatMap((id: unknown) => {
|
||||
if (id === null || id === undefined) return [];
|
||||
const value = String(id).trim();
|
||||
if (!value || seen.has(value)) return [];
|
||||
seen.add(value);
|
||||
return [value];
|
||||
});
|
||||
}
|
||||
return data.knowledgeId === null || data.knowledgeId === undefined || String(data.knowledgeId).trim() === ''
|
||||
? []
|
||||
: [String(data.knowledgeId).trim()];
|
||||
}
|
||||
|
||||
export function toggleKnowledgeId(
|
||||
selectedIds: Array<number | string>,
|
||||
value: number | string,
|
||||
): string[] {
|
||||
const key = String(value);
|
||||
return selectedIds.some((id) => String(id) === key)
|
||||
? selectedIds.filter((id) => String(id) !== key).map(String)
|
||||
: [...selectedIds.map(String), key];
|
||||
}
|
||||
|
||||
export function applyEmbeddingCompatibility(
|
||||
items: SelectItem[],
|
||||
selectedIds: Array<number | string>,
|
||||
): SelectItem[] {
|
||||
const selectedKeys = new Set(selectedIds.map(String));
|
||||
const normalizedItems = items.map((item) => ({ ...item, value: String(item.value) }));
|
||||
for (const selectedId of selectedIds.map(String)) {
|
||||
if (!normalizedItems.some((item) => String(item.value) === selectedId)) {
|
||||
normalizedItems.push({
|
||||
value: selectedId,
|
||||
label: `已失效知识库(${selectedId})`,
|
||||
selectable: true,
|
||||
disabledReason: '知识库不存在或已无权限,仅可移除',
|
||||
});
|
||||
}
|
||||
}
|
||||
const firstSelected = normalizedItems.find(
|
||||
(item) => String(item.value) === String(selectedIds[0]),
|
||||
);
|
||||
if (!firstSelected) return normalizedItems;
|
||||
|
||||
const vectorState = (item: SelectItem): 'available' | 'unavailable' | 'unknown' => {
|
||||
if (item.vectorStoreEnabled === false) return 'unavailable';
|
||||
if (
|
||||
item.vectorStoreEnabled === true
|
||||
&& item.embeddingModelId !== null
|
||||
&& item.embeddingModelId !== undefined
|
||||
&& item.embeddingDimension !== null
|
||||
&& item.embeddingDimension !== undefined
|
||||
) {
|
||||
return 'available';
|
||||
}
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
const firstState = vectorState(firstSelected);
|
||||
const isCompatible = (item: SelectItem) => firstState === 'available'
|
||||
&& vectorState(item) === 'available'
|
||||
&& firstSelected.embeddingModelId === item.embeddingModelId
|
||||
&& firstSelected.embeddingDimension === item.embeddingDimension;
|
||||
|
||||
return normalizedItems.map((item) => {
|
||||
if (selectedKeys.has(String(item.value))) {
|
||||
if (item.disabledReason) return item;
|
||||
const itemState = vectorState(item);
|
||||
if (itemState === 'unavailable') {
|
||||
return {
|
||||
...item,
|
||||
selectable: true,
|
||||
disabledReason: '未启用向量检索,仅可移除',
|
||||
};
|
||||
}
|
||||
if (
|
||||
firstState === 'available'
|
||||
&& itemState === 'available'
|
||||
&& !isCompatible(item)
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
selectable: true,
|
||||
disabledReason: '与首个知识库的 Embedding 配置不一致,仅可移除',
|
||||
};
|
||||
}
|
||||
return { ...item, selectable: true, disabledReason: undefined };
|
||||
}
|
||||
const itemState = vectorState(item);
|
||||
if (firstSelected.disabledReason || firstState === 'unavailable') {
|
||||
return {
|
||||
...item,
|
||||
selectable: false,
|
||||
disabledReason: '请先移除未启用向量检索的知识库',
|
||||
};
|
||||
}
|
||||
if (itemState === 'unavailable') {
|
||||
return {
|
||||
...item,
|
||||
selectable: false,
|
||||
disabledReason: '未启用向量检索',
|
||||
};
|
||||
}
|
||||
if (firstState === 'unknown' || itemState === 'unknown') {
|
||||
return { ...item, selectable: true, disabledReason: undefined };
|
||||
}
|
||||
return isCompatible(item)
|
||||
? { ...item, selectable: true, disabledReason: undefined }
|
||||
: {
|
||||
...item,
|
||||
selectable: false,
|
||||
disabledReason: '与首个知识库的 Embedding 模型或向量维度不一致',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function buildKnowledgeSelectItems(
|
||||
items: SelectItem[],
|
||||
selectedIds: Array<number | string>,
|
||||
reloadItem?: SelectItem,
|
||||
): SelectItem[] {
|
||||
const compatibleItems = applyEmbeddingCompatibility(items, selectedIds);
|
||||
return reloadItem
|
||||
? [
|
||||
...compatibleItems,
|
||||
{ ...reloadItem, selectable: true, disabledReason: undefined },
|
||||
]
|
||||
: compatibleItems;
|
||||
}
|
||||
|
||||
const locked = (parameter: Parameter): Parameter => ({
|
||||
...parameter,
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
deleteDisabled: true,
|
||||
});
|
||||
|
||||
const expectedOutputDefs: Parameter[] = [
|
||||
locked({
|
||||
id: 'knowledge_documents',
|
||||
name: 'documents',
|
||||
dataType: 'Array',
|
||||
addChildDisabled: true,
|
||||
children: [
|
||||
locked({ id: 'knowledge_documents_title', name: 'title', dataType: 'String' }),
|
||||
locked({ id: 'knowledge_documents_content', name: 'content', dataType: 'String' }),
|
||||
locked({ id: 'knowledge_documents_document_id', name: 'documentId', dataType: 'Number' }),
|
||||
locked({ id: 'knowledge_documents_knowledge_id', name: 'knowledgeId', dataType: 'Number' }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
function mergeParameter(current: Parameter | undefined, expected: Parameter): Parameter {
|
||||
if (!current) {
|
||||
return {
|
||||
...expected,
|
||||
children: expected.children?.map((child) => ({ ...child })),
|
||||
};
|
||||
}
|
||||
const result = { ...current, ...expected, id: current.id || expected.id };
|
||||
if (expected.children) {
|
||||
const currentChildren = current.children || [];
|
||||
result.children = expected.children.map((child) =>
|
||||
mergeParameter(
|
||||
currentChildren.find((item) => item.name === child.name),
|
||||
child,
|
||||
),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function ensureKnowledgeOutputDefs(outputDefs?: Parameter[]): Parameter[] {
|
||||
const current = outputDefs || [];
|
||||
return expectedOutputDefs.map((expected) =>
|
||||
mergeParameter(
|
||||
current.find((item) => item.name === expected.name),
|
||||
expected,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import {resolveReferencedParameterDataType} from './referenceDataType';
|
||||
|
||||
describe('resolveReferencedParameterDataType', () => {
|
||||
it('普通引用跟随来源输出类型变化', () => {
|
||||
expect(resolveReferencedParameterDataType(
|
||||
{dataType: 'String'},
|
||||
'Array<String>',
|
||||
)).toBe('Array<String>');
|
||||
});
|
||||
|
||||
it('锁定参数保持业务契约类型', () => {
|
||||
expect(resolveReferencedParameterDataType(
|
||||
{dataType: 'Array', dataTypeDisabled: true},
|
||||
'Array<String>',
|
||||
)).toBe('Array');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {Parameter} from '#types';
|
||||
|
||||
type ReferenceParameter = Pick<Parameter, 'dataType' | 'dataTypeDisabled'>;
|
||||
|
||||
export function resolveReferencedParameterDataType(
|
||||
parameter: ReferenceParameter,
|
||||
referencedDataType?: string,
|
||||
) {
|
||||
if (parameter.dataTypeDisabled === true) {
|
||||
return parameter.dataType;
|
||||
}
|
||||
return referencedDataType || parameter.dataType || 'String';
|
||||
}
|
||||
@@ -26,6 +26,9 @@ export type SelectItem = {
|
||||
isCollection?: boolean;
|
||||
tags?: string[];
|
||||
disabledReason?: string;
|
||||
embeddingModelId?: string;
|
||||
embeddingDimension?: number;
|
||||
vectorStoreEnabled?: boolean;
|
||||
children?: SelectItem[];
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
||||
conditions: ['browser']
|
||||
},
|
||||
test: {
|
||||
environment: 'happy-dom'
|
||||
environment: 'happy-dom',
|
||||
include: ['src/**/*.test.ts'],
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,10 @@ export default defineConfig({
|
||||
plugins: [Vue(), VueJsx()],
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
exclude: [...configDefaults.exclude, '**/e2e/**'],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'**/e2e/**',
|
||||
'packages/tinyflow-ui/**',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { defineWorkspace } from 'vitest/config';
|
||||
|
||||
export default defineWorkspace(['vitest.config.ts']);
|
||||
export default defineWorkspace([
|
||||
'vitest.config.ts',
|
||||
'packages/tinyflow-ui/vitest.config.ts',
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user