@liujitcn/kratos-admin-rag 0.0.0-stage → 0.0.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package/declarations/index.d.ts +1 -0
- package/dist/package/declarations/src/api/rag/admin/v1/ai_knowledge.d.ts +18 -0
- package/dist/package/declarations/src/api/rag/admin/v1/ai_knowledge_chunk.d.ts +7 -0
- package/dist/package/declarations/src/api/rag/admin/v1/ai_knowledge_doc.d.ts +16 -0
- package/dist/package/declarations/src/index.d.ts +1 -0
- package/dist/package/declarations/src/locales/generated.d.ts +205 -0
- package/dist/package/declarations/src/module.d.ts +2 -0
- package/dist/package/declarations/src/rpc/google/protobuf/empty.d.ts +11 -0
- package/dist/package/declarations/src/rpc/rag/admin/v1/ai_knowledge.d.ts +114 -0
- package/dist/package/declarations/src/rpc/rag/admin/v1/ai_knowledge_chunk.d.ts +32 -0
- package/dist/package/declarations/src/rpc/rag/admin/v1/ai_knowledge_doc.d.ts +86 -0
- package/dist/package/declarations/src/views/ai-knowledge/index.vue.d.ts +3 -0
- package/dist/package/src/api/rag/admin/v1/ai_knowledge.ts +51 -0
- package/dist/package/src/api/rag/admin/v1/ai_knowledge_chunk.ts +18 -0
- package/dist/package/src/api/rag/admin/v1/ai_knowledge_doc.ts +43 -0
- package/dist/package/src/index.ts +1 -0
- package/dist/package/src/locales/en-US.json +50 -0
- package/dist/package/src/locales/generated.ts +16 -0
- package/dist/package/src/locales/ja-JP.json +50 -0
- package/dist/package/src/locales/zh-CN.json +50 -0
- package/dist/package/src/locales/zh-TW.json +50 -0
- package/dist/package/src/module.ts +14 -0
- package/dist/package/src/rpc/google/protobuf/empty.ts +19 -0
- package/dist/package/src/rpc/rag/admin/v1/ai_knowledge.ts +133 -0
- package/dist/package/src/rpc/rag/admin/v1/ai_knowledge_chunk.ts +43 -0
- package/dist/package/src/rpc/rag/admin/v1/ai_knowledge_doc.ts +102 -0
- package/dist/package/src/views/ai-knowledge/index.vue +690 -0
- package/package.json +63 -4
- package/tsconfig.json +13 -0
- package/README.md +0 -3
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="table-box">
|
|
3
|
+
<ProTable ref="table" row-key="id" :columns="columns" :header-actions="headerActions" :request-api="requestTable" />
|
|
4
|
+
<FormDialog
|
|
5
|
+
v-model="dialog.visible"
|
|
6
|
+
ref="dialogRef"
|
|
7
|
+
:title="t(dialog.editing ? 'common.action.edit' : 'common.action.create')"
|
|
8
|
+
width="min(720px, calc(100vw - 32px))"
|
|
9
|
+
label-width="10em"
|
|
10
|
+
:model="form"
|
|
11
|
+
:fields="fields"
|
|
12
|
+
:rules="rules"
|
|
13
|
+
@confirm="submit"
|
|
14
|
+
@close="resetForm"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<FormDialog
|
|
18
|
+
v-model="textDialog.visible"
|
|
19
|
+
ref="textDialogRef"
|
|
20
|
+
:title="t('rag.knowledge.doc.action.upload_text')"
|
|
21
|
+
width="min(720px, calc(100vw - 32px))"
|
|
22
|
+
label-width="10em"
|
|
23
|
+
:model="textForm"
|
|
24
|
+
:fields="textFields"
|
|
25
|
+
:rules="textRules"
|
|
26
|
+
@confirm="submitTextDoc"
|
|
27
|
+
@close="resetTextForm"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<el-drawer
|
|
31
|
+
v-model="docsDrawer.visible"
|
|
32
|
+
:title="t('rag.knowledge.doc.title')"
|
|
33
|
+
size="min(860px, calc(100vw - 32px))"
|
|
34
|
+
destroy-on-close
|
|
35
|
+
>
|
|
36
|
+
<div v-if="docsDrawer.knowledge" class="rag-knowledge-docs">
|
|
37
|
+
<div class="rag-knowledge-docs__head">
|
|
38
|
+
<span class="rag-knowledge-docs__title">{{ docsDrawer.knowledge.name }}</span>
|
|
39
|
+
<span v-if="docsDrawer.knowledge.model_name" class="rag-knowledge-docs__meta">{{
|
|
40
|
+
docsDrawer.knowledge.model_name
|
|
41
|
+
}}</span>
|
|
42
|
+
<span v-if="docsDrawer.knowledge.embedding_dimensions" class="rag-knowledge-docs__meta">{{
|
|
43
|
+
t("rag.knowledge.field.embedding_dimensions_value", { dimensions: docsDrawer.knowledge.embedding_dimensions })
|
|
44
|
+
}}</span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="rag-knowledge-docs__actions">
|
|
48
|
+
<el-button type="primary" plain :icon="Upload" :loading="uploading" @click="selectDocFile">
|
|
49
|
+
{{ t("rag.knowledge.doc.action.upload_file") }}
|
|
50
|
+
</el-button>
|
|
51
|
+
<el-button :icon="DocumentAdd" :disabled="uploading" @click="openTextDialog">
|
|
52
|
+
{{ t("rag.knowledge.doc.action.upload_text") }}
|
|
53
|
+
</el-button>
|
|
54
|
+
<el-button :icon="Refresh" :loading="loadingDocs" @click="loadDocs">
|
|
55
|
+
{{ t("common.action.refresh") }}
|
|
56
|
+
</el-button>
|
|
57
|
+
<span class="rag-knowledge-docs__hint">{{ t("rag.knowledge.doc.upload.unsupported") }}: txt/md/csv/log/json/xml/yaml/html/docx/pdf</span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div v-if="uploading" class="rag-knowledge-docs__progress">
|
|
61
|
+
<el-progress :percentage="uploadPercentage" :stroke-width="8" />
|
|
62
|
+
<span class="rag-knowledge-docs__progress-text">{{
|
|
63
|
+
t("rag.knowledge.doc.upload.progress", {
|
|
64
|
+
current: uploadProgress.current,
|
|
65
|
+
total: uploadProgress.total,
|
|
66
|
+
name: uploadProgress.name
|
|
67
|
+
})
|
|
68
|
+
}}</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<el-table v-loading="loadingDocs" :data="docList" row-key="id" size="default">
|
|
72
|
+
<el-table-column prop="name" :label="t('rag.knowledge.doc.field.name')" min-width="180" show-overflow-tooltip />
|
|
73
|
+
<el-table-column prop="chunk_count" :label="t('rag.knowledge.doc.field.chunk_count')" width="90" align="right" />
|
|
74
|
+
<el-table-column :label="t('rag.knowledge.doc.field.status')" width="110" align="center">
|
|
75
|
+
<template #default="{ row }">
|
|
76
|
+
<el-tag
|
|
77
|
+
:type="row.status === AiKnowledgeDocStatus.AI_KNOWLEDGE_DOC_STATUS_READY ? 'success' : 'danger'"
|
|
78
|
+
size="small"
|
|
79
|
+
effect="plain"
|
|
80
|
+
>
|
|
81
|
+
{{
|
|
82
|
+
row.status === AiKnowledgeDocStatus.AI_KNOWLEDGE_DOC_STATUS_READY
|
|
83
|
+
? t("rag.knowledge.doc.status.ready")
|
|
84
|
+
: t("rag.knowledge.doc.status.failed")
|
|
85
|
+
}}
|
|
86
|
+
</el-tag>
|
|
87
|
+
</template>
|
|
88
|
+
</el-table-column>
|
|
89
|
+
<el-table-column prop="error_message" :label="t('rag.knowledge.doc.field.error_message')" min-width="180" show-overflow-tooltip />
|
|
90
|
+
<el-table-column prop="created_at" :label="t('common.field.created_at')" width="170" align="center" />
|
|
91
|
+
<el-table-column :label="t('common.field.operation')" width="130" align="center">
|
|
92
|
+
<template #default="{ row }">
|
|
93
|
+
<el-button
|
|
94
|
+
v-if="row.status === AiKnowledgeDocStatus.AI_KNOWLEDGE_DOC_STATUS_FAILED"
|
|
95
|
+
link
|
|
96
|
+
type="primary"
|
|
97
|
+
:loading="reprocessingId === row.id"
|
|
98
|
+
@click="handleReprocessDoc(row as AiKnowledgeDoc)"
|
|
99
|
+
>
|
|
100
|
+
{{ t("rag.knowledge.doc.action.retry") }}
|
|
101
|
+
</el-button>
|
|
102
|
+
<el-button link type="danger" :disabled="!isDefaultTenant || !BUTTONS['base:ai-knowledge:delete']" @click="handleDeleteDoc(row as AiKnowledgeDoc)">
|
|
103
|
+
{{ t("common.action.delete") }}
|
|
104
|
+
</el-button>
|
|
105
|
+
</template>
|
|
106
|
+
</el-table-column>
|
|
107
|
+
</el-table>
|
|
108
|
+
|
|
109
|
+
<div class="rag-knowledge-search">
|
|
110
|
+
<div class="rag-knowledge-search__head">
|
|
111
|
+
<span class="rag-knowledge-search__title">{{ t("rag.knowledge.search.title") }}</span>
|
|
112
|
+
<span class="rag-knowledge-search__hint">{{ t("rag.knowledge.search.hint") }}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="rag-knowledge-search__bar">
|
|
115
|
+
<el-input
|
|
116
|
+
v-model="searchQuery"
|
|
117
|
+
:placeholder="t('rag.knowledge.search.placeholder')"
|
|
118
|
+
clearable
|
|
119
|
+
@keyup.enter="runSearch"
|
|
120
|
+
/>
|
|
121
|
+
<el-button type="primary" plain :icon="Search" :loading="searching" @click="runSearch">
|
|
122
|
+
{{ t("rag.knowledge.search.action") }}
|
|
123
|
+
</el-button>
|
|
124
|
+
</div>
|
|
125
|
+
<div v-if="searchHits.length" class="rag-knowledge-search__results">
|
|
126
|
+
<div v-for="hit in searchHits" :key="hit.chunk_id" class="rag-knowledge-search__item">
|
|
127
|
+
<div class="rag-knowledge-search__meta">
|
|
128
|
+
<span class="rag-knowledge-search__doc">{{ hit.doc_name }}</span>
|
|
129
|
+
<span class="rag-knowledge-search__score">{{ t("rag.knowledge.search.score") }}: {{ hit.score.toFixed(4) }}</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="rag-knowledge-search__content">{{ hit.content }}</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div v-else-if="searched" class="rag-knowledge-search__empty">
|
|
135
|
+
{{ t("rag.knowledge.search.empty") }}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</el-drawer>
|
|
140
|
+
|
|
141
|
+
<input
|
|
142
|
+
ref="docFileInputRef"
|
|
143
|
+
class="rag-knowledge-file-input"
|
|
144
|
+
type="file"
|
|
145
|
+
multiple
|
|
146
|
+
accept=".txt,.md,.markdown,.csv,.log,.json,.xml,.yml,.yaml,.html,.htm,.docx,.pdf"
|
|
147
|
+
@change="handleDocFileChange"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</template>
|
|
151
|
+
|
|
152
|
+
<script setup lang="ts">
|
|
153
|
+
import { computed, reactive, ref, watch } from "vue";
|
|
154
|
+
import { ElMessage, ElMessageBox } from "element-plus";
|
|
155
|
+
import type { FormRules } from "element-plus";
|
|
156
|
+
import { CirclePlus, Delete, DocumentAdd, EditPen, Refresh, Search, Upload } from "@element-plus/icons-vue";
|
|
157
|
+
import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
|
|
158
|
+
import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
|
|
159
|
+
import type { ColumnProps, HeaderActionProps, ProTableInstance } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
|
|
160
|
+
import type { ProFormField, ProFormOption } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
|
|
161
|
+
import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
|
|
162
|
+
import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
|
|
163
|
+
import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
|
|
164
|
+
import { t } from "@liujitcn/kratos-admin-core";
|
|
165
|
+
import { defAiKnowledgeService } from "@liujitcn/kratos-admin-rag/api/rag/admin/v1/ai_knowledge";
|
|
166
|
+
import { defAiKnowledgeDocService } from "@liujitcn/kratos-admin-rag/api/rag/admin/v1/ai_knowledge_doc";
|
|
167
|
+
import { defAiKnowledgeChunkService } from "@liujitcn/kratos-admin-rag/api/rag/admin/v1/ai_knowledge_chunk";
|
|
168
|
+
import type {
|
|
169
|
+
AiKnowledge,
|
|
170
|
+
AiKnowledgeForm,
|
|
171
|
+
AiKnowledgeModel,
|
|
172
|
+
PageAiKnowledgeRequest
|
|
173
|
+
} from "@liujitcn/kratos-admin-rag/rpc/rag/admin/v1/ai_knowledge";
|
|
174
|
+
import type { AiKnowledgeDoc } from "@liujitcn/kratos-admin-rag/rpc/rag/admin/v1/ai_knowledge_doc";
|
|
175
|
+
import { AiKnowledgeDocStatus } from "@liujitcn/kratos-admin-rag/rpc/rag/admin/v1/ai_knowledge_doc";
|
|
176
|
+
import type { AiKnowledgeChunkHit } from "@liujitcn/kratos-admin-rag/rpc/rag/admin/v1/ai_knowledge_chunk";
|
|
177
|
+
|
|
178
|
+
defineOptions({ name: "AiKnowledge", inheritAttrs: false });
|
|
179
|
+
|
|
180
|
+
const SUPPORTED_EXTENSIONS = [".txt", ".md", ".markdown", ".csv", ".log", ".json", ".xml", ".yml", ".yaml", ".html", ".htm", ".docx", ".pdf"];
|
|
181
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
182
|
+
|
|
183
|
+
type AiKnowledgeFormState = Omit<AiKnowledgeForm, "model_id" | "embedding_dimensions"> & {
|
|
184
|
+
model_id: number | undefined;
|
|
185
|
+
embedding_dimensions: number | undefined;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const { BUTTONS } = useAuthButtons();
|
|
189
|
+
const { isDefaultTenant } = useTenantScope();
|
|
190
|
+
const table = ref<ProTableInstance>();
|
|
191
|
+
const dialogRef = ref<InstanceType<typeof FormDialog>>();
|
|
192
|
+
const textDialogRef = ref<InstanceType<typeof FormDialog>>();
|
|
193
|
+
const docFileInputRef = ref<HTMLInputElement>();
|
|
194
|
+
const dialog = reactive({ visible: false, editing: false });
|
|
195
|
+
const textDialog = reactive({ visible: false });
|
|
196
|
+
const form = reactive<AiKnowledgeFormState>(defaultForm());
|
|
197
|
+
const textForm = reactive({ name: "", content: "" });
|
|
198
|
+
const knowledgeModels = ref<AiKnowledgeModel[]>([]);
|
|
199
|
+
// 模型级联选项:一级供应商、二级模型,叶子值取模型ID。
|
|
200
|
+
const modelOptions = computed<ProFormOption[]>(() => {
|
|
201
|
+
const groups = new Map<string, ProFormOption[]>();
|
|
202
|
+
for (const item of knowledgeModels.value) {
|
|
203
|
+
const label = item.display_name ? `${item.display_name}(${item.model_name})` : item.model_name;
|
|
204
|
+
const children = groups.get(item.provider_name) ?? [];
|
|
205
|
+
children.push({ label, value: item.id });
|
|
206
|
+
groups.set(item.provider_name, children);
|
|
207
|
+
}
|
|
208
|
+
return [...groups.entries()].map(([provider, children]) => ({ label: provider, value: provider, children }));
|
|
209
|
+
});
|
|
210
|
+
// 维度选项随所选模型联动:模型声明了维度时唯一可选,未声明时提供常用维度。
|
|
211
|
+
const dimensionOptions = computed<ProFormOption[]>(() => {
|
|
212
|
+
const selected = knowledgeModels.value.find((item: AiKnowledgeModel) => item.id === form.model_id);
|
|
213
|
+
return (selected?.dimension_options ?? []).map(value => ({ label: `${value}`, value }));
|
|
214
|
+
});
|
|
215
|
+
const docsDrawer = reactive<{ visible: boolean; knowledge: AiKnowledge | null }>({ visible: false, knowledge: null });
|
|
216
|
+
const docList = ref<AiKnowledgeDoc[]>([]);
|
|
217
|
+
const loadingDocs = ref(false);
|
|
218
|
+
const uploading = ref(false);
|
|
219
|
+
const uploadProgress = reactive({ current: 0, total: 0, name: "" });
|
|
220
|
+
const uploadPercentage = computed(() =>
|
|
221
|
+
uploadProgress.total ? Math.round((uploadProgress.current / uploadProgress.total) * 100) : 0
|
|
222
|
+
);
|
|
223
|
+
const reprocessingId = ref<number | undefined>();
|
|
224
|
+
const searchQuery = ref("");
|
|
225
|
+
const searchHits = ref<AiKnowledgeChunkHit[]>([]);
|
|
226
|
+
const searching = ref(false);
|
|
227
|
+
const searched = ref(false);
|
|
228
|
+
|
|
229
|
+
const fields = computed<ProFormField[]>(() => [
|
|
230
|
+
{ prop: "name", label: t("rag.knowledge.field.name"), component: "input", colSpan: 24, props: { maxlength: 100 } },
|
|
231
|
+
{ prop: "description", label: t("rag.knowledge.field.description"), component: "textarea", colSpan: 24, props: { maxlength: 500, rows: 3 } },
|
|
232
|
+
{ prop: "model_id", label: t("rag.knowledge.field.model"), component: "cascader", colSpan: 24, options: modelOptions.value, props: { props: { emitPath: false }, clearable: true, filterable: true, style: "width: 100%" } },
|
|
233
|
+
{
|
|
234
|
+
prop: "embedding_dimensions",
|
|
235
|
+
label: t("rag.knowledge.field.embedding_dimensions"),
|
|
236
|
+
component: "select",
|
|
237
|
+
colSpan: 24,
|
|
238
|
+
options: dimensionOptions.value,
|
|
239
|
+
// 编辑时维度是创建快照不可改;未选模型前无可选维度。
|
|
240
|
+
props: { disabled: dialog.editing || !form.model_id }
|
|
241
|
+
}
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
// 模型声明了维度或仅有一个可选维度时自动选中;切换模型后清理不再可用的维度。
|
|
245
|
+
watch(
|
|
246
|
+
() => form.model_id,
|
|
247
|
+
() => {
|
|
248
|
+
const selected = knowledgeModels.value.find((item: AiKnowledgeModel) => item.id === form.model_id);
|
|
249
|
+
const options = selected?.dimension_options ?? [];
|
|
250
|
+
if (options.length === 1) {
|
|
251
|
+
form.embedding_dimensions = options[0];
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (form.embedding_dimensions === undefined || !options.includes(form.embedding_dimensions)) {
|
|
255
|
+
form.embedding_dimensions = undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const textFields = computed<ProFormField[]>(() => [
|
|
261
|
+
{ prop: "name", label: t("rag.knowledge.doc.field.name"), component: "input", colSpan: 24, props: { maxlength: 200 } },
|
|
262
|
+
{ prop: "content", label: t("rag.knowledge.doc.field.content"), component: "textarea", colSpan: 24, props: { rows: 12, maxlength: 1000000, placeholder: t("rag.knowledge.doc.placeholder.content") } }
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
const rules = computed<FormRules>(() => ({
|
|
266
|
+
name: [{ required: true, message: t("rag.knowledge.validation.name"), trigger: "blur" }],
|
|
267
|
+
model_id: [{ required: true, message: t("rag.knowledge.validation.model"), trigger: "change" }],
|
|
268
|
+
embedding_dimensions: [{ required: true, message: t("rag.knowledge.validation.embedding_dimensions"), trigger: "change" }]
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
const textRules = computed<FormRules>(() => ({
|
|
272
|
+
name: [{ required: true, message: t("rag.knowledge.doc.validation.name"), trigger: "blur" }],
|
|
273
|
+
content: [{ required: true, message: t("rag.knowledge.doc.validation.content"), trigger: "blur" }]
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
const columns = computed<ColumnProps[]>(() => [
|
|
277
|
+
{ prop: "name", label: t("rag.knowledge.field.name"), minWidth: 160, search: { el: "input" } },
|
|
278
|
+
{ prop: "provider_name", label: t("rag.knowledge.field.provider"), minWidth: 140 },
|
|
279
|
+
{ prop: "model_name", label: t("rag.knowledge.field.model"), minWidth: 180 },
|
|
280
|
+
{ prop: "embedding_dimensions", label: t("rag.knowledge.field.embedding_dimensions"), width: 90, align: "right" },
|
|
281
|
+
{ prop: "doc_count", label: t("rag.knowledge.field.doc_count"), width: 90, align: "right" },
|
|
282
|
+
{ prop: "updated_at", label: t("common.field.updated_at"), minWidth: 170, align: "center" },
|
|
283
|
+
{
|
|
284
|
+
prop: "operation",
|
|
285
|
+
label: t("common.field.operation"),
|
|
286
|
+
cellType: "actions",
|
|
287
|
+
actions: [
|
|
288
|
+
{ label: t("common.action.edit"), link: true, icon: EditPen, hidden: () => !isDefaultTenant.value || !BUTTONS.value["base:ai-knowledge:update"], onClick: scope => openDialog((scope.row as AiKnowledge).id) },
|
|
289
|
+
{ label: t("rag.knowledge.action.docs"), link: true, icon: DocumentAdd, onClick: scope => openDocsDrawer(scope.row as AiKnowledge) },
|
|
290
|
+
{ label: t("common.action.delete"), type: "danger", link: true, icon: Delete, hidden: () => !isDefaultTenant.value || !BUTTONS.value["base:ai-knowledge:delete"], onClick: scope => handleDelete(scope.row as AiKnowledge) }
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const headerActions = computed<HeaderActionProps[]>(() => [
|
|
296
|
+
{ label: t("common.action.create"), type: "primary", icon: CirclePlus, hidden: !isDefaultTenant.value || !BUTTONS.value["base:ai-knowledge:create"], onClick: () => openDialog() },
|
|
297
|
+
{ label: t("common.action.delete"), type: "danger", icon: Delete, hidden: !isDefaultTenant.value || !BUTTONS.value["base:ai-knowledge:delete"], disabled: scope => !scope.isSelected, onClick: scope => handleDelete(scope.selectedListIds as number[]) }
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
/** 创建默认知识库表单。 */
|
|
301
|
+
function defaultForm(): AiKnowledgeFormState {
|
|
302
|
+
return { id: 0, name: "", description: "", embedding_dimensions: 0, model_id: undefined };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 请求AI知识库表格数据。 */
|
|
306
|
+
async function requestTable(params: Record<string, unknown>) {
|
|
307
|
+
const data = await defAiKnowledgeService.PageAiKnowledge(buildPageRequest<PageAiKnowledgeRequest>(params as unknown as PageAiKnowledgeRequest));
|
|
308
|
+
return { data: { list: data.knowledges ?? [], total: data.total } };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** 加载可选的已启用embedding模型。 */
|
|
312
|
+
async function loadModelOptions() {
|
|
313
|
+
try {
|
|
314
|
+
const response = await defAiKnowledgeService.ListAiKnowledgeModels({});
|
|
315
|
+
knowledgeModels.value = response.models ?? [];
|
|
316
|
+
} catch {
|
|
317
|
+
knowledgeModels.value = [];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 打开知识库编辑表单。 */
|
|
322
|
+
async function openDialog(id?: number) {
|
|
323
|
+
await dialogRef.value?.open({
|
|
324
|
+
load: () => (id ? defAiKnowledgeService.GetAiKnowledge({ id }) : undefined),
|
|
325
|
+
commit: value => {
|
|
326
|
+
Object.assign(form, defaultForm(), value ?? {});
|
|
327
|
+
dialog.editing = Boolean(id);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 提交知识库表单。 */
|
|
333
|
+
async function submit() {
|
|
334
|
+
const valid = await dialogRef.value?.validate();
|
|
335
|
+
if (!valid) return;
|
|
336
|
+
const payload = JSON.parse(JSON.stringify(form)) as AiKnowledgeForm;
|
|
337
|
+
if (dialog.editing) await defAiKnowledgeService.UpdateAiKnowledge({ ai_knowledge: payload });
|
|
338
|
+
else await defAiKnowledgeService.CreateAiKnowledge({ ai_knowledge: payload });
|
|
339
|
+
ElMessage.success(t(dialog.editing ? "rag.knowledge.message.update_success" : "rag.knowledge.message.create_success"));
|
|
340
|
+
dialog.visible = false;
|
|
341
|
+
resetForm();
|
|
342
|
+
table.value?.getTableList();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** 重置知识库表单。 */
|
|
346
|
+
function resetForm() {
|
|
347
|
+
Object.assign(form, defaultForm());
|
|
348
|
+
dialog.editing = false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** 批量删除AI知识库。 */
|
|
352
|
+
async function handleDelete(target: AiKnowledge | number | Array<number>) {
|
|
353
|
+
const row = typeof target === "object" && !Array.isArray(target) ? target : undefined;
|
|
354
|
+
const ids = (row ? [row.id] : normalizeSelectedIds(target as number | number[])).map(Number);
|
|
355
|
+
if (!ids.length) return;
|
|
356
|
+
try {
|
|
357
|
+
await ElMessageBox.confirm(
|
|
358
|
+
t("rag.knowledge.message.confirm_delete", { name: row?.name ?? ids.join(", ") }),
|
|
359
|
+
t("common.action.delete"),
|
|
360
|
+
{ type: "warning" }
|
|
361
|
+
);
|
|
362
|
+
} catch {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
for (const id of ids) await defAiKnowledgeService.DeleteAiKnowledge({ id });
|
|
366
|
+
ElMessage.success(t("rag.knowledge.message.delete_success"));
|
|
367
|
+
table.value?.getTableList();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** 打开文档管理抽屉。 */
|
|
371
|
+
async function openDocsDrawer(row: AiKnowledge) {
|
|
372
|
+
docsDrawer.knowledge = row;
|
|
373
|
+
docsDrawer.visible = true;
|
|
374
|
+
searchQuery.value = "";
|
|
375
|
+
searchHits.value = [];
|
|
376
|
+
searched.value = false;
|
|
377
|
+
await loadDocs();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** 加载当前知识库的文档列表。 */
|
|
381
|
+
async function loadDocs() {
|
|
382
|
+
const knowledge = docsDrawer.knowledge;
|
|
383
|
+
if (!knowledge) return;
|
|
384
|
+
loadingDocs.value = true;
|
|
385
|
+
try {
|
|
386
|
+
const response = await defAiKnowledgeDocService.PageAiKnowledgeDoc({ id: knowledge.id, page_num: 1, page_size: 100 });
|
|
387
|
+
docList.value = response.docs ?? [];
|
|
388
|
+
} catch {
|
|
389
|
+
docList.value = [];
|
|
390
|
+
} finally {
|
|
391
|
+
loadingDocs.value = false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** 触发文档文件选择。 */
|
|
396
|
+
function selectDocFile() {
|
|
397
|
+
docFileInputRef.value?.click();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** 读取选择的文档文件并逐个上传,展示整体进度。 */
|
|
401
|
+
async function handleDocFileChange(event: Event) {
|
|
402
|
+
const input = event.target as HTMLInputElement;
|
|
403
|
+
const files = Array.from(input.files ?? []);
|
|
404
|
+
input.value = "";
|
|
405
|
+
if (!files.length || !docsDrawer.knowledge) return;
|
|
406
|
+
const accepted: File[] = [];
|
|
407
|
+
const rejected: string[] = [];
|
|
408
|
+
for (const file of files) {
|
|
409
|
+
const extension = `.${file.name.split(".").pop()?.toLowerCase() ?? ""}`;
|
|
410
|
+
if (!SUPPORTED_EXTENSIONS.includes(extension) || file.size > MAX_FILE_SIZE) {
|
|
411
|
+
rejected.push(file.name);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
accepted.push(file);
|
|
415
|
+
}
|
|
416
|
+
if (rejected.length) {
|
|
417
|
+
ElMessage.warning(t("rag.knowledge.doc.upload.batch_invalid", { names: rejected.join("、") }));
|
|
418
|
+
}
|
|
419
|
+
if (!accepted.length) return;
|
|
420
|
+
const knowledgeId = docsDrawer.knowledge.id;
|
|
421
|
+
uploading.value = true;
|
|
422
|
+
uploadProgress.total = accepted.length;
|
|
423
|
+
uploadProgress.current = 0;
|
|
424
|
+
uploadProgress.name = accepted[0].name;
|
|
425
|
+
let successCount = 0;
|
|
426
|
+
const failedNames: string[] = [];
|
|
427
|
+
try {
|
|
428
|
+
for (const file of accepted) {
|
|
429
|
+
uploadProgress.name = file.name;
|
|
430
|
+
try {
|
|
431
|
+
const contentBase64 = await readFileAsBase64(file);
|
|
432
|
+
if (!contentBase64) {
|
|
433
|
+
failedNames.push(file.name);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
await defAiKnowledgeDocService.UploadAiKnowledgeDocFile({
|
|
437
|
+
id: knowledgeId,
|
|
438
|
+
file_name: file.name,
|
|
439
|
+
// bytes 字段线上按 base64 字符串传输,类型按生成声明适配。
|
|
440
|
+
content_base64: contentBase64 as unknown as Uint8Array,
|
|
441
|
+
doc_name: ""
|
|
442
|
+
});
|
|
443
|
+
successCount++;
|
|
444
|
+
} catch {
|
|
445
|
+
failedNames.push(file.name);
|
|
446
|
+
} finally {
|
|
447
|
+
uploadProgress.current++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} finally {
|
|
451
|
+
uploading.value = false;
|
|
452
|
+
await loadDocs();
|
|
453
|
+
table.value?.getTableList();
|
|
454
|
+
}
|
|
455
|
+
if (!failedNames.length) {
|
|
456
|
+
ElMessage.success(t("rag.knowledge.doc.upload.batch_success", { count: successCount }));
|
|
457
|
+
} else if (successCount) {
|
|
458
|
+
ElMessage.warning(t("rag.knowledge.doc.upload.batch_partial", { success: successCount, failed: failedNames.length }));
|
|
459
|
+
} else {
|
|
460
|
+
ElMessage.error(t("rag.knowledge.doc.upload.batch_partial", { success: successCount, failed: failedNames.length }));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** 打开纯文本上传弹窗。 */
|
|
465
|
+
function openTextDialog() {
|
|
466
|
+
Object.assign(textForm, { name: "", content: "" });
|
|
467
|
+
textDialog.visible = true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** 提交纯文本文档。 */
|
|
471
|
+
async function submitTextDoc() {
|
|
472
|
+
const valid = await textDialogRef.value?.validate();
|
|
473
|
+
if (!valid || !docsDrawer.knowledge) return;
|
|
474
|
+
await defAiKnowledgeDocService.UploadAiKnowledgeDocText({
|
|
475
|
+
id: docsDrawer.knowledge.id,
|
|
476
|
+
name: textForm.name,
|
|
477
|
+
content: textForm.content
|
|
478
|
+
});
|
|
479
|
+
ElMessage.success(t("rag.knowledge.doc.upload.success"));
|
|
480
|
+
textDialog.visible = false;
|
|
481
|
+
await loadDocs();
|
|
482
|
+
table.value?.getTableList();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** 重置文本上传表单。 */
|
|
486
|
+
function resetTextForm() {
|
|
487
|
+
Object.assign(textForm, { name: "", content: "" });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** 删除单个文档。 */
|
|
491
|
+
async function handleDeleteDoc(row: AiKnowledgeDoc) {
|
|
492
|
+
try {
|
|
493
|
+
await ElMessageBox.confirm(
|
|
494
|
+
t("rag.knowledge.doc.message.confirm_delete", { name: row.name }),
|
|
495
|
+
t("common.action.delete"),
|
|
496
|
+
{ type: "warning" }
|
|
497
|
+
);
|
|
498
|
+
} catch {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
await defAiKnowledgeDocService.DeleteAiKnowledgeDoc({ doc_id: row.id });
|
|
502
|
+
ElMessage.success(t("rag.knowledge.doc.message.delete_success"));
|
|
503
|
+
await loadDocs();
|
|
504
|
+
table.value?.getTableList();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** 重新处理失败的文档。 */
|
|
508
|
+
async function handleReprocessDoc(row: AiKnowledgeDoc) {
|
|
509
|
+
if (reprocessingId.value) return;
|
|
510
|
+
reprocessingId.value = row.id;
|
|
511
|
+
try {
|
|
512
|
+
await defAiKnowledgeDocService.ReprocessAiKnowledgeDoc({ doc_id: row.id });
|
|
513
|
+
ElMessage.success(t("rag.knowledge.doc.message.reprocess_success"));
|
|
514
|
+
await loadDocs();
|
|
515
|
+
table.value?.getTableList();
|
|
516
|
+
} finally {
|
|
517
|
+
reprocessingId.value = undefined;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** 执行知识库检索测试。 */
|
|
522
|
+
async function runSearch() {
|
|
523
|
+
const knowledge = docsDrawer.knowledge;
|
|
524
|
+
const query = searchQuery.value.trim();
|
|
525
|
+
if (!knowledge || !query) return;
|
|
526
|
+
searching.value = true;
|
|
527
|
+
try {
|
|
528
|
+
const response = await defAiKnowledgeChunkService.SearchAiKnowledge({ id: knowledge.id, query, top_k: 5 });
|
|
529
|
+
searchHits.value = response.hits ?? [];
|
|
530
|
+
searched.value = true;
|
|
531
|
+
} catch {
|
|
532
|
+
searchHits.value = [];
|
|
533
|
+
} finally {
|
|
534
|
+
searching.value = false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** 读取文件为 base64 字符串。 */
|
|
539
|
+
function readFileAsBase64(file: File): Promise<string | undefined> {
|
|
540
|
+
return new Promise(resolve => {
|
|
541
|
+
const reader = new FileReader();
|
|
542
|
+
reader.onload = () => {
|
|
543
|
+
const result = reader.result;
|
|
544
|
+
if (typeof result !== "string") {
|
|
545
|
+
ElMessage.error(t("rag.knowledge.doc.upload.read_failed"));
|
|
546
|
+
resolve(undefined);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
resolve(result.split(",").pop() ?? "");
|
|
550
|
+
};
|
|
551
|
+
reader.onerror = () => {
|
|
552
|
+
ElMessage.error(t("rag.knowledge.doc.upload.read_failed"));
|
|
553
|
+
resolve(undefined);
|
|
554
|
+
};
|
|
555
|
+
reader.readAsDataURL(file);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
loadModelOptions();
|
|
560
|
+
</script>
|
|
561
|
+
|
|
562
|
+
<style scoped>
|
|
563
|
+
.rag-knowledge-file-input {
|
|
564
|
+
display: none;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.rag-knowledge-docs {
|
|
568
|
+
display: flex;
|
|
569
|
+
flex-direction: column;
|
|
570
|
+
gap: 16px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.rag-knowledge-docs__head {
|
|
574
|
+
display: flex;
|
|
575
|
+
align-items: baseline;
|
|
576
|
+
gap: 12px;
|
|
577
|
+
min-width: 0;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.rag-knowledge-docs__title {
|
|
581
|
+
font-size: 16px;
|
|
582
|
+
font-weight: 600;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.rag-knowledge-docs__meta {
|
|
586
|
+
color: var(--el-text-color-secondary);
|
|
587
|
+
font-size: 12px;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.rag-knowledge-docs__actions {
|
|
591
|
+
display: flex;
|
|
592
|
+
align-items: center;
|
|
593
|
+
flex-wrap: wrap;
|
|
594
|
+
gap: 8px;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.rag-knowledge-docs__hint {
|
|
598
|
+
color: var(--el-text-color-secondary);
|
|
599
|
+
font-size: 12px;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.rag-knowledge-docs__progress {
|
|
603
|
+
display: flex;
|
|
604
|
+
align-items: center;
|
|
605
|
+
gap: 12px;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.rag-knowledge-docs__progress :deep(.el-progress) {
|
|
609
|
+
flex: 1;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.rag-knowledge-docs__progress-text {
|
|
613
|
+
max-width: 50%;
|
|
614
|
+
overflow: hidden;
|
|
615
|
+
color: var(--el-text-color-secondary);
|
|
616
|
+
font-size: 12px;
|
|
617
|
+
text-overflow: ellipsis;
|
|
618
|
+
white-space: nowrap;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.rag-knowledge-search {
|
|
622
|
+
display: flex;
|
|
623
|
+
flex-direction: column;
|
|
624
|
+
gap: 10px;
|
|
625
|
+
padding-top: 8px;
|
|
626
|
+
border-top: 1px solid var(--el-border-color-lighter);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.rag-knowledge-search__head {
|
|
630
|
+
display: flex;
|
|
631
|
+
align-items: baseline;
|
|
632
|
+
gap: 12px;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.rag-knowledge-search__title {
|
|
636
|
+
font-weight: 600;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.rag-knowledge-search__hint {
|
|
640
|
+
color: var(--el-text-color-secondary);
|
|
641
|
+
font-size: 12px;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.rag-knowledge-search__bar {
|
|
645
|
+
display: flex;
|
|
646
|
+
gap: 8px;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.rag-knowledge-search__results {
|
|
650
|
+
display: flex;
|
|
651
|
+
flex-direction: column;
|
|
652
|
+
gap: 10px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.rag-knowledge-search__item {
|
|
656
|
+
padding: 10px 12px;
|
|
657
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
658
|
+
border-radius: 6px;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.rag-knowledge-search__meta {
|
|
662
|
+
display: flex;
|
|
663
|
+
align-items: center;
|
|
664
|
+
justify-content: space-between;
|
|
665
|
+
gap: 8px;
|
|
666
|
+
margin-bottom: 6px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.rag-knowledge-search__doc {
|
|
670
|
+
font-weight: 600;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.rag-knowledge-search__score {
|
|
674
|
+
color: var(--el-color-primary);
|
|
675
|
+
font-size: 12px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.rag-knowledge-search__content {
|
|
679
|
+
color: var(--el-text-color-regular);
|
|
680
|
+
font-size: 13px;
|
|
681
|
+
line-height: 1.6;
|
|
682
|
+
white-space: pre-wrap;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.rag-knowledge-search__empty {
|
|
686
|
+
padding: 16px;
|
|
687
|
+
color: var(--el-text-color-secondary);
|
|
688
|
+
text-align: center;
|
|
689
|
+
}
|
|
690
|
+
</style>
|