mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-11-02 14:11:49 +08:00
feat(wiki): per-document entity extraction action (#673)
- Add an Extract entities action to completed documents in the knowledge base document list when entity extraction is enabled. - Add a per-document extraction endpoint with workspace, ownership and processing checks. - Rename the KB-level button to Re-extract whole KB.
This commit is contained in:
parent
9aa4619ec6
commit
2d045b252e
@ -13,10 +13,12 @@ import vip.mate.exception.MateClawException;
|
|||||||
import vip.mate.wiki.dto.WikiEntityGraphView;
|
import vip.mate.wiki.dto.WikiEntityGraphView;
|
||||||
import vip.mate.wiki.dto.WikiEntityView;
|
import vip.mate.wiki.dto.WikiEntityView;
|
||||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||||
|
import vip.mate.wiki.model.WikiRawMaterialEntity;
|
||||||
import vip.mate.wiki.service.WikiEntityExtractionService;
|
import vip.mate.wiki.service.WikiEntityExtractionService;
|
||||||
import vip.mate.wiki.service.WikiEntityGraphService;
|
import vip.mate.wiki.service.WikiEntityGraphService;
|
||||||
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
||||||
import vip.mate.wiki.service.WikiProcessingService;
|
import vip.mate.wiki.service.WikiProcessingService;
|
||||||
|
import vip.mate.wiki.service.WikiRawMaterialService;
|
||||||
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -36,6 +38,7 @@ public class WikiEntityController {
|
|||||||
private final WikiEntityGraphService graphService;
|
private final WikiEntityGraphService graphService;
|
||||||
private final WikiEntityExtractionService extractionService;
|
private final WikiEntityExtractionService extractionService;
|
||||||
private final WikiKnowledgeBaseService kbService;
|
private final WikiKnowledgeBaseService kbService;
|
||||||
|
private final WikiRawMaterialService rawMaterialService;
|
||||||
|
|
||||||
/** List entities in a KB, optionally filtered by type, ranked by salience. */
|
/** List entities in a KB, optionally filtered by type, ranked by salience. */
|
||||||
@RequireWorkspaceRole("viewer")
|
@RequireWorkspaceRole("viewer")
|
||||||
@ -92,6 +95,40 @@ public class WikiEntityController {
|
|||||||
return Map.of("status", "started", "kbId", kbId);
|
return Map.of("status", "started", "kbId", kbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually trigger entity extraction for one document. Runs on the wiki
|
||||||
|
* executor so the request returns immediately.
|
||||||
|
*
|
||||||
|
* @param force when true, re-extract chunks that already have mentions
|
||||||
|
*/
|
||||||
|
@RequireWorkspaceRole("member")
|
||||||
|
@PostMapping("/kb/{kbId}/raw/{rawId}/entities/extract")
|
||||||
|
public Map<String, Object> extractRaw(@PathVariable Long kbId,
|
||||||
|
@PathVariable Long rawId,
|
||||||
|
@RequestParam(defaultValue = "false") boolean force,
|
||||||
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||||
|
verifyKBWorkspace(kbId, workspaceId);
|
||||||
|
WikiRawMaterialEntity raw = rawMaterialService.getById(rawId);
|
||||||
|
if (raw == null || !kbId.equals(raw.getKbId())) {
|
||||||
|
throw new MateClawException(404, "Raw material not found");
|
||||||
|
}
|
||||||
|
// Extraction reads the document's chunks, which exist only after processing completes.
|
||||||
|
if (!"completed".equals(raw.getProcessingStatus())) {
|
||||||
|
throw new MateClawException(400, "Raw material has not finished processing");
|
||||||
|
}
|
||||||
|
WikiProcessingService.WIKI_EXECUTOR.submit(() -> {
|
||||||
|
try {
|
||||||
|
int count = extractionService.extractForRaw(kbId, rawId, force);
|
||||||
|
log.info("[WikiEntity] Manual document extraction completed: kbId={}, rawId={}, entities={}",
|
||||||
|
kbId, rawId, count);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WikiEntity] Manual document extraction failed for kbId={}, rawId={}: {}",
|
||||||
|
kbId, rawId, e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Map.of("status", "started", "kbId", kbId, "rawId", rawId);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Workspace Verification ====================
|
// ==================== Workspace Verification ====================
|
||||||
|
|
||||||
private void verifyKBWorkspace(Long kbId, Long headerWorkspaceId) {
|
private void verifyKBWorkspace(Long kbId, Long headerWorkspaceId) {
|
||||||
|
|||||||
@ -81,7 +81,23 @@ public class WikiEntityExtractionService {
|
|||||||
|
|
||||||
/** Extract entities from every not-yet-processed chunk of one raw material. */
|
/** Extract entities from every not-yet-processed chunk of one raw material. */
|
||||||
public int extractForRaw(Long kbId, Long rawId) {
|
public int extractForRaw(Long kbId, Long rawId) {
|
||||||
return extract(kbId, chunkService.listByRawId(rawId), false);
|
return extractForRaw(kbId, rawId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract entities from one raw material's chunks.
|
||||||
|
*
|
||||||
|
* @param force when {@code true}, re-extract chunks that already have
|
||||||
|
* mentions (manual per-document rebuild); otherwise skip them
|
||||||
|
*/
|
||||||
|
public int extractForRaw(Long kbId, Long rawId, boolean force) {
|
||||||
|
int touched = extract(kbId, chunkService.listByRawId(rawId), force);
|
||||||
|
if (force && touched > 0) {
|
||||||
|
// Re-extraction may drop entities that only this document mentioned.
|
||||||
|
// Skip pruning when nothing resolved so a model failure keeps the graph.
|
||||||
|
pruneOrphanEntities(kbId);
|
||||||
|
}
|
||||||
|
return touched;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
|||||||
import vip.mate.wiki.service.WikiEntityExtractionService;
|
import vip.mate.wiki.service.WikiEntityExtractionService;
|
||||||
import vip.mate.wiki.service.WikiEntityGraphService;
|
import vip.mate.wiki.service.WikiEntityGraphService;
|
||||||
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
||||||
|
import vip.mate.wiki.service.WikiRawMaterialService;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
@ -31,7 +32,8 @@ class WikiEntityControllerIdorTest {
|
|||||||
controller = new WikiEntityController(
|
controller = new WikiEntityController(
|
||||||
mock(WikiEntityGraphService.class),
|
mock(WikiEntityGraphService.class),
|
||||||
mock(WikiEntityExtractionService.class),
|
mock(WikiEntityExtractionService.class),
|
||||||
kbService);
|
kbService,
|
||||||
|
mock(WikiRawMaterialService.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -70,6 +72,15 @@ class WikiEntityControllerIdorTest {
|
|||||||
.isInstanceOf(MateClawException.class);
|
.isInstanceOf(MateClawException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("extractRaw (member-level write) on another workspace's KB → 403")
|
||||||
|
void extractRawCrossWorkspaceRejected() {
|
||||||
|
when(kbService.getById(10L)).thenReturn(kb(10L, 2L));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> controller.extractRaw(10L, 5L, false, 1L))
|
||||||
|
.isInstanceOf(MateClawException.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("unknown kbId → 404")
|
@DisplayName("unknown kbId → 404")
|
||||||
void unknownKbReturns404() {
|
void unknownKbReturns404() {
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
package vip.mate.wiki.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import vip.mate.exception.MateClawException;
|
||||||
|
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||||
|
import vip.mate.wiki.model.WikiRawMaterialEntity;
|
||||||
|
import vip.mate.wiki.service.WikiEntityExtractionService;
|
||||||
|
import vip.mate.wiki.service.WikiEntityGraphService;
|
||||||
|
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
||||||
|
import vip.mate.wiki.service.WikiRawMaterialService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.timeout;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/** Per-document entity extraction endpoint: ownership, readiness and dispatch. */
|
||||||
|
class WikiEntityControllerRawExtractionTest {
|
||||||
|
|
||||||
|
private static final long KB_ID = 10L;
|
||||||
|
private static final long RAW_ID = 2055137662148763649L;
|
||||||
|
|
||||||
|
private WikiKnowledgeBaseService kbService;
|
||||||
|
private WikiRawMaterialService rawService;
|
||||||
|
private WikiEntityExtractionService extractionService;
|
||||||
|
private WikiEntityController controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
kbService = mock(WikiKnowledgeBaseService.class);
|
||||||
|
rawService = mock(WikiRawMaterialService.class);
|
||||||
|
extractionService = mock(WikiEntityExtractionService.class);
|
||||||
|
controller = new WikiEntityController(mock(WikiEntityGraphService.class), extractionService, kbService, rawService);
|
||||||
|
WikiKnowledgeBaseEntity kb = new WikiKnowledgeBaseEntity();
|
||||||
|
kb.setId(KB_ID);
|
||||||
|
kb.setWorkspaceId(1L);
|
||||||
|
when(kbService.getById(KB_ID)).thenReturn(kb);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("completed document in the KB → extraction dispatched with force flag")
|
||||||
|
void completedDocumentStartsExtraction() {
|
||||||
|
when(rawService.getById(RAW_ID)).thenReturn(raw(KB_ID, "completed"));
|
||||||
|
|
||||||
|
Map<String, Object> result = controller.extractRaw(KB_ID, RAW_ID, true, 1L);
|
||||||
|
|
||||||
|
assertThat(result).containsEntry("status", "started").containsEntry("rawId", RAW_ID);
|
||||||
|
verify(extractionService, timeout(2000)).extractForRaw(KB_ID, RAW_ID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("document from another KB → 404, nothing dispatched")
|
||||||
|
void documentFromAnotherKbRejected() {
|
||||||
|
when(rawService.getById(RAW_ID)).thenReturn(raw(99L, "completed"));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> controller.extractRaw(KB_ID, RAW_ID, false, 1L))
|
||||||
|
.isInstanceOf(MateClawException.class)
|
||||||
|
.hasMessageContaining("not found");
|
||||||
|
verify(extractionService, never()).extractForRaw(anyLong(), anyLong(), anyBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("missing document → 404")
|
||||||
|
void missingDocumentRejected() {
|
||||||
|
assertThatThrownBy(() -> controller.extractRaw(KB_ID, RAW_ID, false, 1L))
|
||||||
|
.isInstanceOf(MateClawException.class)
|
||||||
|
.hasMessageContaining("not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("document still processing → rejected, nothing dispatched")
|
||||||
|
void unfinishedDocumentRejected() {
|
||||||
|
when(rawService.getById(RAW_ID)).thenReturn(raw(KB_ID, "processing"));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> controller.extractRaw(KB_ID, RAW_ID, false, 1L))
|
||||||
|
.isInstanceOf(MateClawException.class);
|
||||||
|
verify(extractionService, never()).extractForRaw(anyLong(), anyLong(), anyBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WikiRawMaterialEntity raw(long kbId, String status) {
|
||||||
|
WikiRawMaterialEntity raw = new WikiRawMaterialEntity();
|
||||||
|
raw.setId(RAW_ID);
|
||||||
|
raw.setKbId(kbId);
|
||||||
|
raw.setProcessingStatus(status);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -224,6 +224,38 @@ class WikiEntityExtractionServiceTest {
|
|||||||
verify(entityMapper, times(0)).insert(any(WikiEntityEntity.class));
|
verify(entityMapper, times(0)).insert(any(WikiEntityEntity.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("extractForRaw(force): re-extracts processed chunks and prunes orphans")
|
||||||
|
void extractForRaw_forceReextractsAndPrunes() {
|
||||||
|
when(chunkService.listByRawId(RAW_ID)).thenReturn(List.of(chunk(1L, "Alice works at Acme.")));
|
||||||
|
when(mentionMapper.selectCount(any(Wrapper.class))).thenReturn(1L);
|
||||||
|
when(mentionMapper.selectList(any())).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
int touched = service.extractForRaw(KB_ID, RAW_ID, true);
|
||||||
|
|
||||||
|
assertEquals(2, touched);
|
||||||
|
verify(mentionMapper, times(1)).delete(any());
|
||||||
|
verify(mentionMapper, times(2)).insert(any(WikiEntityMentionEntity.class));
|
||||||
|
// Orphan sweep runs after a successful forced rebuild.
|
||||||
|
verify(entityMapper, times(2)).selectList(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("extractForRaw(force): model failure keeps the existing graph")
|
||||||
|
void extractForRaw_forceWithoutResultKeepsGraph() {
|
||||||
|
when(chunkService.listByRawId(RAW_ID)).thenReturn(List.of(chunk(1L, "Alice works at Acme.")));
|
||||||
|
when(mentionMapper.selectCount(any(Wrapper.class))).thenReturn(1L);
|
||||||
|
ChatModel failing = failingModel();
|
||||||
|
when(routingService.buildChatModel(7L)).thenReturn(failing);
|
||||||
|
|
||||||
|
int touched = service.extractForRaw(KB_ID, RAW_ID, true);
|
||||||
|
|
||||||
|
assertEquals(0, touched);
|
||||||
|
verify(mentionMapper, times(0)).delete(any());
|
||||||
|
verify(relationMapper, times(0)).delete(any());
|
||||||
|
verify(entityMapper, times(0)).delete(any());
|
||||||
|
}
|
||||||
|
|
||||||
private WikiChunkEntity chunk(Long id, String content) {
|
private WikiChunkEntity chunk(Long id, String content) {
|
||||||
WikiChunkEntity c = new WikiChunkEntity();
|
WikiChunkEntity c = new WikiChunkEntity();
|
||||||
c.setId(id);
|
c.setId(id);
|
||||||
@ -233,6 +265,12 @@ class WikiEntityExtractionServiceTest {
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChatModel failingModel() {
|
||||||
|
ChatModel model = mock(ChatModel.class);
|
||||||
|
when(model.call(any(Prompt.class))).thenThrow(new IllegalStateException("model unavailable"));
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
private ChatModel cannedModel(String body) {
|
private ChatModel cannedModel(String body) {
|
||||||
ChatModel model = mock(ChatModel.class);
|
ChatModel model = mock(ChatModel.class);
|
||||||
when(model.call(any(Prompt.class))).thenReturn(new ChatResponse(List.of(
|
when(model.call(any(Prompt.class))).thenReturn(new ChatResponse(List.of(
|
||||||
|
|||||||
@ -1304,6 +1304,9 @@ export const wikiApi = {
|
|||||||
http.get(`/wiki/kb/${kbId}/entities/${entityId}/graph`, { params: { limit } }),
|
http.get(`/wiki/kb/${kbId}/entities/${entityId}/graph`, { params: { limit } }),
|
||||||
extractEntities: (kbId: number | string, force = false) =>
|
extractEntities: (kbId: number | string, force = false) =>
|
||||||
http.post(`/wiki/kb/${kbId}/entities/extract`, null, { params: { force } }),
|
http.post(`/wiki/kb/${kbId}/entities/extract`, null, { params: { force } }),
|
||||||
|
// Entity extraction for a single processed document.
|
||||||
|
extractRawEntities: (kbId: number | string, rawId: number | string, force = false) =>
|
||||||
|
http.post(`/wiki/kb/${kbId}/raw/${rawId}/entities/extract`, null, { params: { force } }),
|
||||||
|
|
||||||
// RFC-030: Jobs
|
// RFC-030: Jobs
|
||||||
getWikiJobs: (kbId: number, rawId: number) =>
|
getWikiJobs: (kbId: number, rawId: number) =>
|
||||||
|
|||||||
@ -2968,6 +2968,8 @@ export default {
|
|||||||
cancelling: 'Cancelling…',
|
cancelling: 'Cancelling…',
|
||||||
cancelledHint: 'Cancelled by user',
|
cancelledHint: 'Cancelled by user',
|
||||||
download: 'Download original file',
|
download: 'Download original file',
|
||||||
|
extractEntities: 'Extract entities',
|
||||||
|
extractEntitiesStarted: 'Started extracting entities from "{title}". Check the entity graph when it finishes.',
|
||||||
downloadFailed: 'Download failed',
|
downloadFailed: 'Download failed',
|
||||||
processAll: 'Process All Pending',
|
processAll: 'Process All Pending',
|
||||||
materialTitle: 'Title',
|
materialTitle: 'Title',
|
||||||
@ -3098,7 +3100,7 @@ export default {
|
|||||||
entityExtraction: 'Entity extraction',
|
entityExtraction: 'Entity extraction',
|
||||||
entityExtractionHint: 'Extract named entities (people, organizations, locations, ...) and their relations from raw materials to build an entity-level knowledge graph. Adds processing time and token cost; off by default.',
|
entityExtractionHint: 'Extract named entities (people, organizations, locations, ...) and their relations from raw materials to build an entity-level knowledge graph. Adds processing time and token cost; off by default.',
|
||||||
entityExtractionEnable: 'Enable entity extraction',
|
entityExtractionEnable: 'Enable entity extraction',
|
||||||
entityExtractionRun: 'Extract now',
|
entityExtractionRun: 'Re-extract whole KB',
|
||||||
entityExtractionRunning: 'Extracting…',
|
entityExtractionRunning: 'Extracting…',
|
||||||
entityTypesLabel: 'Entity types to extract',
|
entityTypesLabel: 'Entity types to extract',
|
||||||
entityTypesPlaceholder: 'Select or type an entity type, Enter to add',
|
entityTypesPlaceholder: 'Select or type an entity type, Enter to add',
|
||||||
|
|||||||
@ -2980,6 +2980,8 @@ export default {
|
|||||||
cancelling: '正在取消…',
|
cancelling: '正在取消…',
|
||||||
cancelledHint: '用户已取消处理',
|
cancelledHint: '用户已取消处理',
|
||||||
download: '下载原始文件',
|
download: '下载原始文件',
|
||||||
|
extractEntities: '实体抽取',
|
||||||
|
extractEntitiesStarted: '已开始抽取「{title}」的实体,完成后可在实体图谱中查看',
|
||||||
downloadFailed: '下载失败',
|
downloadFailed: '下载失败',
|
||||||
processAll: '处理所有待处理材料',
|
processAll: '处理所有待处理材料',
|
||||||
materialTitle: '标题',
|
materialTitle: '标题',
|
||||||
@ -3110,7 +3112,7 @@ export default {
|
|||||||
entityExtraction: '实体抽取',
|
entityExtraction: '实体抽取',
|
||||||
entityExtractionHint: '从原始材料中抽取命名实体(人物、组织、地点等)及其关系,构建实体级知识图谱。会增加处理时间与 Token 消耗,默认关闭。',
|
entityExtractionHint: '从原始材料中抽取命名实体(人物、组织、地点等)及其关系,构建实体级知识图谱。会增加处理时间与 Token 消耗,默认关闭。',
|
||||||
entityExtractionEnable: '开启实体抽取',
|
entityExtractionEnable: '开启实体抽取',
|
||||||
entityExtractionRun: '立即抽取',
|
entityExtractionRun: '全库重新抽取',
|
||||||
entityExtractionRunning: '抽取中…',
|
entityExtractionRunning: '抽取中…',
|
||||||
entityTypesLabel: '抽取的实体类型',
|
entityTypesLabel: '抽取的实体类型',
|
||||||
entityTypesPlaceholder: '选择或输入实体类型,回车添加',
|
entityTypesPlaceholder: '选择或输入实体类型,回车添加',
|
||||||
|
|||||||
@ -290,6 +290,14 @@
|
|||||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="entityExtractionEnabled && raw.processingStatus === 'completed'"
|
||||||
|
class="btn-icon" :title="t('wiki.extractEntities')"
|
||||||
|
:disabled="extractingIds.has(String(raw.id))"
|
||||||
|
@click.stop="extractRawEntities(raw)"
|
||||||
|
>
|
||||||
|
<el-icon :size="14"><Connection /></el-icon>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="raw.processingStatus !== 'uploading'"
|
v-if="raw.processingStatus !== 'uploading'"
|
||||||
class="btn-icon" :title="t('wiki.download')"
|
class="btn-icon" :title="t('wiki.download')"
|
||||||
@ -378,7 +386,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { mcToast } from '@/composables/useMcToast'
|
import { mcToast } from '@/composables/useMcToast'
|
||||||
import { mcConfirm } from '@/components/common/useConfirm'
|
import { mcConfirm } from '@/components/common/useConfirm'
|
||||||
import { useFileDrop } from '@/composables/useFileDrop'
|
import { useFileDrop } from '@/composables/useFileDrop'
|
||||||
import { Download } from '@element-plus/icons-vue'
|
import { Connection, Download } from '@element-plus/icons-vue'
|
||||||
import { useWikiStore } from '@/stores/useWikiStore'
|
import { useWikiStore } from '@/stores/useWikiStore'
|
||||||
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
import { wikiApi } from '@/api/index'
|
import { wikiApi } from '@/api/index'
|
||||||
@ -906,6 +914,39 @@ async function handleAddText() {
|
|||||||
textContent.value = ''
|
textContent.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-document entity extraction is offered only once the KB has entity
|
||||||
|
// extraction switched on in its processing config.
|
||||||
|
const entityExtractionEnabled = computed(() => {
|
||||||
|
const content = store.currentKB?.configContent
|
||||||
|
if (!content) return false
|
||||||
|
try {
|
||||||
|
return JSON.parse(content)?.entityExtractionEnabled === true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Keyed by String(id): raw ids are Snowflake values and must stay strings.
|
||||||
|
const extractingIds = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
async function extractRawEntities(raw: { id: number | string; title: string }) {
|
||||||
|
if (!store.currentKB) return
|
||||||
|
const key = String(raw.id)
|
||||||
|
if (extractingIds.value.has(key)) return
|
||||||
|
extractingIds.value = new Set(extractingIds.value).add(key)
|
||||||
|
try {
|
||||||
|
// force=true re-extracts this document only, so the latest entity-type
|
||||||
|
// config also applies to documents extracted before it changed.
|
||||||
|
await wikiApi.extractRawEntities(store.currentKB.id, raw.id, true)
|
||||||
|
mcToast.success(t('wiki.extractEntitiesStarted', { title: raw.title }))
|
||||||
|
} catch (e) {
|
||||||
|
mcToast.error(e instanceof Error ? e.message : String(e))
|
||||||
|
} finally {
|
||||||
|
const next = new Set(extractingIds.value)
|
||||||
|
next.delete(key)
|
||||||
|
extractingIds.value = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function reprocess(rawId: number) {
|
async function reprocess(rawId: number) {
|
||||||
if (!store.currentKB) return
|
if (!store.currentKB) return
|
||||||
const kbId = store.currentKB.id
|
const kbId = store.currentKB.id
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user