feat: add knowledge management functionality with CRUD operations and database schema

- Implemented KnowledgeHandler for managing knowledge groups and items, including listing, creating, updating, and deleting operations.
- Added database migration scripts to create necessary tables for knowledge management, including knowledge_groups, knowledge_items, knowledge_parse_tasks, and knowledge_chunks_meta.
- Introduced prompt_rule_knowledge_groups table to associate prompt rules with knowledge groups.
This commit is contained in:
2026-04-05 17:14:13 +08:00
parent 134dd063c3
commit 446f865cdf
62 changed files with 9503 additions and 2664 deletions
@@ -19,6 +19,7 @@ import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import KnowledgeGroupSelect from "@/components/KnowledgeGroupSelect.vue";
import { articlesApi, brandsApi, normalizeInputParams, templatesApi } from "@/lib/api";
import { formatError } from "@/lib/errors";
import { getTemplateMeta } from "@/lib/display";
@@ -170,6 +171,7 @@ const selectedTitle = ref("");
const customTitle = ref("");
const reviewIntroHook = ref("");
const keyPoints = ref("");
const selectedKnowledgeGroupIds = ref<number[]>([]);
const outlineSections = ref<string[]>([]);
const customOutlineSections = ref<ResolvedOutlineOption[]>([]);
const outlineOptionOrder = ref<string[]>([]);
@@ -424,6 +426,7 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
customTitle.value = "";
reviewIntroHook.value = "";
keyPoints.value = "";
selectedKnowledgeGroupIds.value = [];
customOutlineSections.value = [];
customOutlineInput.value = "";
generatedOutline.value = [];
@@ -481,6 +484,7 @@ function applyDraftArticleState(detail: ArticleDetail): void {
customTitle.value = stringFromUnknown(draftState.custom_title) || "";
reviewIntroHook.value = stringFromUnknown(draftState.review_intro_hook) || "";
keyPoints.value = stringFromUnknown(draftState.key_points) || "";
selectedKnowledgeGroupIds.value = numberListFromUnknown(draftState.knowledge_group_ids) ?? [];
customOutlineSections.value = parseCustomOutlineSections(draftState.custom_outline_sections);
outlineOptionOrder.value = stringListFromUnknown(draftState.outline_option_order) ?? outlineOptionOrder.value;
outlineSections.value = stringListFromUnknown(draftState.outline_sections) ?? outlineSections.value;
@@ -989,6 +993,8 @@ function buildPayload(): Record<string, JsonValue> {
primary_keyword: primaryKeyword.value || undefined,
keywords: keywordDrafts.value.length > 0 ? dedupeStrings(keywordDrafts.value) : undefined,
[reviewIntroHookFieldName.value]: reviewIntroHookPromptValue.value || undefined,
knowledge_group_ids:
selectedKnowledgeGroupIds.value.length > 0 ? selectedKnowledgeGroupIds.value : undefined,
competitors: competitorInputValue,
};
@@ -1031,6 +1037,7 @@ function buildDraftState(): Record<string, JsonValue> {
custom_title: customTitle.value,
title: finalTitle.value || selectedTitle.value || "",
review_intro_hook: reviewIntroHook.value,
knowledge_group_ids: selectedKnowledgeGroupIds.value,
outline_sections: outlineSections.value,
outline_option_order: outlineOptionOrder.value,
custom_outline_sections: customOutlineSections.value.map((item) => ({
@@ -1524,6 +1531,16 @@ function numberFromUnknown(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function numberListFromUnknown(value: unknown): number[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const items = value
.map((item) => (typeof item === "number" && Number.isFinite(item) ? item : NaN))
.filter((item) => Number.isFinite(item) && item > 0);
return items.length > 0 ? items : undefined;
}
function clampStep(value: number | undefined): number {
if (typeof value !== "number" || Number.isNaN(value)) {
return 0;
@@ -2415,6 +2432,14 @@ function onStructureDragEnd(): void {
/>
</div>
<div class="field-item field-item--compact">
<label>引用知识库</label>
<KnowledgeGroupSelect
v-model="selectedKnowledgeGroupIds"
placeholder="可选,用于补充文章内容和事实背景"
/>
</div>
<a-alert
type="info"
show-icon