mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
feat(knowledge): add channel support to knowledge entries and related components
- Introduced a new `channel` field in the Knowledge struct and associated request types to track the source channel (e.g., "web", "api", "browser_extension"). - Updated various frontend components to display channel information and enhance user experience with channel labels. - Enhanced localization files to support channel labels in English and Chinese. - Modified backend services and database migrations to accommodate the new channel feature, ensuring consistent tracking across knowledge entries. - Refactored related functions to integrate channel handling, improving overall knowledge management and context.
This commit is contained in:
@@ -29,6 +29,7 @@ type Knowledge struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Channel string `json:"channel"`
|
||||
ParseStatus string `json:"parse_status"`
|
||||
SummaryStatus string `json:"summary_status"`
|
||||
EnableStatus string `json:"enable_status"`
|
||||
@@ -204,6 +205,8 @@ type CreateKnowledgeFromURLRequest struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
// TagID is the optional tag ID to associate with the knowledge entry
|
||||
TagID string `json:"tag_id,omitempty"`
|
||||
// Channel identifies the ingestion channel (e.g. "web", "browser_extension", "api")
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromURL creates a knowledge entry from a URL.
|
||||
@@ -435,9 +438,10 @@ func (c *Client) UpdateImageInfo(ctx context.Context,
|
||||
|
||||
// CreateManualKnowledgeRequest contains the parameters for creating a manual Markdown knowledge entry.
|
||||
type CreateManualKnowledgeRequest struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
TagID string `json:"tag_id,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateManualKnowledgeRequest contains the parameters for updating a manual Markdown knowledge entry.
|
||||
|
||||
@@ -153,7 +153,8 @@ type SearchResult struct {
|
||||
ImageInfo string `json:"image_info"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
KnowledgeFilename string `json:"knowledge_filename"`
|
||||
KnowledgeSource string `json:"knowledge_source"`
|
||||
KnowledgeSource string `json:"knowledge_source"`
|
||||
KnowledgeChannel string `json:"knowledge_channel"`
|
||||
// MatchedContent is the actual content that was matched in vector search
|
||||
// For FAQ: this is the matched question text (standard or similar question)
|
||||
MatchedContent string `json:"matched_content,omitempty"`
|
||||
|
||||
@@ -400,6 +400,23 @@ const getDisplayTitle = () => {
|
||||
return props.details.title;
|
||||
};
|
||||
|
||||
const channelLabelMap: Record<string, string> = {
|
||||
web: 'knowledgeBase.channelWeb',
|
||||
api: 'knowledgeBase.channelApi',
|
||||
browser_extension: 'knowledgeBase.channelBrowserExtension',
|
||||
wechat: 'knowledgeBase.channelWechat',
|
||||
wecom: 'knowledgeBase.channelWecom',
|
||||
feishu: 'knowledgeBase.channelFeishu',
|
||||
dingtalk: 'knowledgeBase.channelDingtalk',
|
||||
slack: 'knowledgeBase.channelSlack',
|
||||
im: 'knowledgeBase.channelIm',
|
||||
};
|
||||
|
||||
const getChannelLabel = (channel: string) => {
|
||||
const key = channelLabelMap[channel];
|
||||
return key ? t(key) : t('knowledgeBase.channelUnknown');
|
||||
};
|
||||
|
||||
// 获取类型标签
|
||||
const getTypeLabel = () => {
|
||||
switch (props.details.type) {
|
||||
@@ -714,6 +731,9 @@ const handleDetailsScroll = () => {
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="time"> {{ getTimeLabel() }}:{{ details.time }} </span>
|
||||
<t-tag v-if="details.channel && details.channel !== 'web'" size="small" variant="light" theme="warning" class="channel-tag">
|
||||
{{ getChannelLabel(details.channel) }}
|
||||
</t-tag>
|
||||
<div class="view-mode-buttons">
|
||||
<t-button
|
||||
v-if="canPreview()"
|
||||
@@ -1057,6 +1077,10 @@ const handleDetailsScroll = () => {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.channel-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chunk-count {
|
||||
color: var(--td-brand-color);
|
||||
font-size: 12px;
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function (knowledgeBaseId?: string) {
|
||||
total: 0,
|
||||
type: "",
|
||||
source: "",
|
||||
channel: "",
|
||||
file_type: "",
|
||||
chunkLoading: false,
|
||||
chunkLoadError: "",
|
||||
@@ -151,6 +152,7 @@ export default function (knowledgeBaseId?: string) {
|
||||
id: "",
|
||||
type: "",
|
||||
source: "",
|
||||
channel: "",
|
||||
file_type: "",
|
||||
chunkLoadError: "",
|
||||
});
|
||||
@@ -164,6 +166,7 @@ export default function (knowledgeBaseId?: string) {
|
||||
id: data.id,
|
||||
type: data.type || 'file',
|
||||
source: data.source || '',
|
||||
channel: data.channel || '',
|
||||
file_type: data.file_type || ''
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,6 +129,17 @@ export default {
|
||||
typeURL: 'URL',
|
||||
typeManual: 'Manual',
|
||||
typeFile: 'File',
|
||||
channelLabel: 'Source Channel',
|
||||
channelWeb: 'Web',
|
||||
channelApi: 'API',
|
||||
channelBrowserExtension: 'Browser Extension',
|
||||
channelWechat: 'WeChat',
|
||||
channelWecom: 'WeCom',
|
||||
channelFeishu: 'Feishu',
|
||||
channelDingtalk: 'DingTalk',
|
||||
channelSlack: 'Slack',
|
||||
channelIm: 'IM Channel',
|
||||
channelUnknown: 'Unknown',
|
||||
urlSource: 'Source URL',
|
||||
documentTitle: 'Document Title',
|
||||
webContent: 'Web Content',
|
||||
|
||||
@@ -128,6 +128,17 @@ export default {
|
||||
typeURL: "网页",
|
||||
typeManual: "手动创建",
|
||||
typeFile: "文件",
|
||||
channelLabel: "来源渠道",
|
||||
channelWeb: "网页端",
|
||||
channelApi: "API",
|
||||
channelBrowserExtension: "浏览器插件",
|
||||
channelWechat: "微信",
|
||||
channelWecom: "企业微信",
|
||||
channelFeishu: "飞书",
|
||||
channelDingtalk: "钉钉",
|
||||
channelSlack: "Slack",
|
||||
channelIm: "IM 渠道",
|
||||
channelUnknown: "未知",
|
||||
urlSource: "来源网址",
|
||||
documentTitle: "文档标题",
|
||||
webContent: "网页内容",
|
||||
|
||||
@@ -100,6 +100,7 @@ export interface DocumentInfoDocument {
|
||||
description?: string;
|
||||
type?: string;
|
||||
source?: string;
|
||||
channel?: string;
|
||||
file_name?: string;
|
||||
file_type?: string;
|
||||
file_size?: number;
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
<span class="field-label">{{ $t('chat.documentSourceLabel') }}</span>
|
||||
<span class="field-value">{{ formatSource(doc) }}</span>
|
||||
</div>
|
||||
<div class="info-field" v-if="doc.channel && doc.channel !== 'web'">
|
||||
<span class="field-label">{{ $t('knowledgeBase.channelLabel') }}</span>
|
||||
<span class="field-value">{{ getChannelLabel(doc.channel) }}</span>
|
||||
</div>
|
||||
<div class="info-field" v-if="doc.file_name || doc.file_type || doc.file_size">
|
||||
<span class="field-label">{{ $t('chat.documentFileLabel') }}</span>
|
||||
<span class="field-value">
|
||||
@@ -84,6 +88,23 @@ const totalChunkCount = computed(() =>
|
||||
documents.value.reduce((sum, doc) => sum + (doc.chunk_count || 0), 0),
|
||||
);
|
||||
|
||||
const channelLabelMap: Record<string, string> = {
|
||||
web: 'knowledgeBase.channelWeb',
|
||||
api: 'knowledgeBase.channelApi',
|
||||
browser_extension: 'knowledgeBase.channelBrowserExtension',
|
||||
wechat: 'knowledgeBase.channelWechat',
|
||||
wecom: 'knowledgeBase.channelWecom',
|
||||
feishu: 'knowledgeBase.channelFeishu',
|
||||
dingtalk: 'knowledgeBase.channelDingtalk',
|
||||
slack: 'knowledgeBase.channelSlack',
|
||||
im: 'knowledgeBase.channelIm',
|
||||
};
|
||||
|
||||
const getChannelLabel = (channel: string) => {
|
||||
const key = channelLabelMap[channel];
|
||||
return key ? t(key) : t('knowledgeBase.channelUnknown');
|
||||
};
|
||||
|
||||
const formatSource = (doc: DocumentInfoDocument) => {
|
||||
if (doc.type && doc.source) {
|
||||
return `${doc.type} · ${doc.source}`;
|
||||
|
||||
@@ -280,6 +280,23 @@ const formatFileSize = (bytes?: number | string) => {
|
||||
return `${(n / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
const channelLabelMap: Record<string, string> = {
|
||||
web: 'knowledgeBase.channelWeb',
|
||||
api: 'knowledgeBase.channelApi',
|
||||
browser_extension: 'knowledgeBase.channelBrowserExtension',
|
||||
wechat: 'knowledgeBase.channelWechat',
|
||||
wecom: 'knowledgeBase.channelWecom',
|
||||
feishu: 'knowledgeBase.channelFeishu',
|
||||
dingtalk: 'knowledgeBase.channelDingtalk',
|
||||
slack: 'knowledgeBase.channelSlack',
|
||||
im: 'knowledgeBase.channelIm',
|
||||
};
|
||||
|
||||
const getChannelLabel = (channel: string) => {
|
||||
const key = channelLabelMap[channel];
|
||||
return key ? t(key) : t('knowledgeBase.channelUnknown');
|
||||
};
|
||||
|
||||
// 获取知识条目的显示类型
|
||||
const getKnowledgeType = (item: any) => {
|
||||
if (item.type === 'url') {
|
||||
@@ -1923,6 +1940,7 @@ async function createNewSession(value: string): Promise<void> {
|
||||
</template>
|
||||
<div class="card-popover-meta">
|
||||
<span class="card-popover-time">{{ t('knowledgeBase.updatedAt') }}:{{ formatDocTime(hoveredCardItem.updated_at) }}</span>
|
||||
<span v-if="(hoveredCardItem as any).channel && (hoveredCardItem as any).channel !== 'web'" class="card-popover-channel">{{ getChannelLabel((hoveredCardItem as any).channel) }}</span>
|
||||
<span v-if="getTagName(hoveredCardItem.tag_id)" class="card-popover-tag">{{ getTagName(hoveredCardItem.tag_id) }}</span>
|
||||
<span class="card-popover-type">{{ getKnowledgeType(hoveredCardItem) }}</span>
|
||||
</div>
|
||||
@@ -3383,6 +3401,13 @@ async function createNewSession(value: string): Promise<void> {
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.card-popover-channel {
|
||||
padding: 1px 6px;
|
||||
background: var(--td-warning-color-light);
|
||||
color: var(--td-warning-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-popover-tag {
|
||||
padding: 1px 6px;
|
||||
background: var(--td-success-color-light);
|
||||
|
||||
@@ -601,6 +601,7 @@ func (p *PluginSearch) tryDirectChunkLoading(ctx context.Context, tenantID uint6
|
||||
res.KnowledgeTitle = k.Title
|
||||
res.KnowledgeFilename = k.FileName
|
||||
res.KnowledgeSource = k.Source
|
||||
res.KnowledgeChannel = k.Channel
|
||||
res.Metadata = k.GetMetadata()
|
||||
}
|
||||
|
||||
|
||||
@@ -221,6 +221,7 @@ func chunk2SearchResult(chunk *types.Chunk, knowledge *types.Knowledge) *types.S
|
||||
ImageInfo: chunk.ImageInfo,
|
||||
KnowledgeFilename: knowledge.FileName,
|
||||
KnowledgeSource: knowledge.Source,
|
||||
KnowledgeChannel: knowledge.Channel,
|
||||
ChunkMetadata: chunk.Metadata,
|
||||
KnowledgeBaseID: knowledge.KnowledgeBaseID,
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ func (e *EvaluationService) EvalDataset(ctx context.Context, detail *types.Evalu
|
||||
logger.Infof(ctx, "Creating knowledge from %d passages", len(passages))
|
||||
|
||||
// Create knowledge base from passages
|
||||
knowledge, err := e.knowledgeService.CreateKnowledgeFromPassage(ctx, knowledgeBaseID, passages)
|
||||
knowledge, err := e.knowledgeService.CreateKnowledgeFromPassage(ctx, knowledgeBaseID, passages, "")
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to create knowledge from passages: %v", err)
|
||||
return err
|
||||
|
||||
@@ -178,9 +178,16 @@ func checkStorageEngineConfigured(ctx context.Context, kb *types.KnowledgeBase)
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultChannel(ch string) string {
|
||||
if ch == "" {
|
||||
return types.ChannelWeb
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromFile creates a knowledge entry from an uploaded file
|
||||
func (s *knowledgeService) CreateKnowledgeFromFile(ctx context.Context,
|
||||
kbID string, file *multipart.FileHeader, metadata map[string]string, enableMultimodel *bool, customFileName string, tagID string,
|
||||
kbID string, file *multipart.FileHeader, metadata map[string]string, enableMultimodel *bool, customFileName string, tagID string, channel string,
|
||||
) (*types.Knowledge, error) {
|
||||
logger.Info(ctx, "Start creating knowledge from file")
|
||||
|
||||
@@ -322,6 +329,7 @@ func (s *knowledgeService) CreateKnowledgeFromFile(ctx context.Context,
|
||||
KnowledgeBaseID: kbID,
|
||||
TagID: tagID, // 设置分类ID,用于知识分类管理
|
||||
Type: "file",
|
||||
Channel: defaultChannel(channel),
|
||||
Title: safeFilename,
|
||||
FileName: safeFilename,
|
||||
FileType: getFileType(safeFilename),
|
||||
@@ -434,14 +442,14 @@ func isFileURL(rawURL, fileName, fileType string) bool {
|
||||
}
|
||||
|
||||
func (s *knowledgeService) CreateKnowledgeFromURL(ctx context.Context,
|
||||
kbID string, rawURL string, fileName string, fileType string, enableMultimodel *bool, title string, tagID string,
|
||||
kbID string, rawURL string, fileName string, fileType string, enableMultimodel *bool, title string, tagID string, channel string,
|
||||
) (*types.Knowledge, error) {
|
||||
logger.Info(ctx, "Start creating knowledge from URL")
|
||||
logger.Infof(ctx, "Knowledge base ID: %s, URL: %s", kbID, rawURL)
|
||||
|
||||
// Route to file_url logic when the URL points to a downloadable file
|
||||
if isFileURL(rawURL, fileName, fileType) {
|
||||
return s.createKnowledgeFromFileURL(ctx, kbID, rawURL, fileName, fileType, enableMultimodel, title, tagID)
|
||||
return s.createKnowledgeFromFileURL(ctx, kbID, rawURL, fileName, fileType, enableMultimodel, title, tagID, channel)
|
||||
}
|
||||
|
||||
url := rawURL
|
||||
@@ -510,6 +518,7 @@ func (s *knowledgeService) CreateKnowledgeFromURL(ctx context.Context,
|
||||
TenantID: tenantID,
|
||||
KnowledgeBaseID: kbID,
|
||||
Type: "url",
|
||||
Channel: defaultChannel(channel),
|
||||
Title: title,
|
||||
Source: url,
|
||||
FileHash: fileHash,
|
||||
@@ -628,6 +637,7 @@ func (s *knowledgeService) createKnowledgeFromFileURL(
|
||||
enableMultimodel *bool,
|
||||
title string,
|
||||
tagID string,
|
||||
channel string,
|
||||
) (*types.Knowledge, error) {
|
||||
logger.Info(ctx, "Start creating knowledge from file URL")
|
||||
logger.Infof(ctx, "Knowledge base ID: %s, file URL: %s", kbID, fileURL)
|
||||
@@ -720,6 +730,7 @@ func (s *knowledgeService) createKnowledgeFromFileURL(
|
||||
TenantID: tenantID,
|
||||
KnowledgeBaseID: kbID,
|
||||
Type: "file_url",
|
||||
Channel: defaultChannel(channel),
|
||||
Title: title,
|
||||
FileName: displayName,
|
||||
FileType: fileType,
|
||||
@@ -790,21 +801,21 @@ func (s *knowledgeService) createKnowledgeFromFileURL(
|
||||
|
||||
// CreateKnowledgeFromPassage creates a knowledge entry from text passages
|
||||
func (s *knowledgeService) CreateKnowledgeFromPassage(ctx context.Context,
|
||||
kbID string, passage []string,
|
||||
kbID string, passage []string, channel string,
|
||||
) (*types.Knowledge, error) {
|
||||
return s.createKnowledgeFromPassageInternal(ctx, kbID, passage, false)
|
||||
return s.createKnowledgeFromPassageInternal(ctx, kbID, passage, false, channel)
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromPassageSync creates a knowledge entry from text passages and waits for indexing to complete.
|
||||
func (s *knowledgeService) CreateKnowledgeFromPassageSync(ctx context.Context,
|
||||
kbID string, passage []string,
|
||||
kbID string, passage []string, channel string,
|
||||
) (*types.Knowledge, error) {
|
||||
return s.createKnowledgeFromPassageInternal(ctx, kbID, passage, true)
|
||||
return s.createKnowledgeFromPassageInternal(ctx, kbID, passage, true, channel)
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromManual creates or saves manual Markdown knowledge content.
|
||||
func (s *knowledgeService) CreateKnowledgeFromManual(ctx context.Context,
|
||||
kbID string, payload *types.ManualKnowledgePayload,
|
||||
kbID string, payload *types.ManualKnowledgePayload, channel string,
|
||||
) (*types.Knowledge, error) {
|
||||
logger.Info(ctx, "Start creating manual knowledge entry")
|
||||
|
||||
@@ -857,6 +868,7 @@ func (s *knowledgeService) CreateKnowledgeFromManual(ctx context.Context,
|
||||
TenantID: tenantID,
|
||||
KnowledgeBaseID: kbID,
|
||||
Type: types.KnowledgeTypeManual,
|
||||
Channel: defaultChannel(channel),
|
||||
Title: title,
|
||||
Description: "",
|
||||
Source: types.KnowledgeTypeManual,
|
||||
@@ -901,7 +913,7 @@ func (s *knowledgeService) CreateKnowledgeFromManual(ctx context.Context,
|
||||
// createKnowledgeFromPassageInternal consolidates the common logic for creating knowledge from passages.
|
||||
// When syncMode is true, chunk processing is performed synchronously; otherwise, it's processed asynchronously.
|
||||
func (s *knowledgeService) createKnowledgeFromPassageInternal(ctx context.Context,
|
||||
kbID string, passage []string, syncMode bool,
|
||||
kbID string, passage []string, syncMode bool, channel string,
|
||||
) (*types.Knowledge, error) {
|
||||
if syncMode {
|
||||
logger.Info(ctx, "Start creating knowledge from passage (sync)")
|
||||
@@ -940,6 +952,7 @@ func (s *knowledgeService) createKnowledgeFromPassageInternal(ctx context.Contex
|
||||
TenantID: ctx.Value(types.TenantIDContextKey).(uint64),
|
||||
KnowledgeBaseID: kbID,
|
||||
Type: "passage",
|
||||
Channel: defaultChannel(channel),
|
||||
ParseStatus: "pending",
|
||||
EnableStatus: "disabled",
|
||||
CreatedAt: time.Now(),
|
||||
@@ -1271,6 +1284,7 @@ func (s *knowledgeService) cloneKnowledge(
|
||||
TenantID: targetKB.TenantID,
|
||||
KnowledgeBaseID: targetKB.ID,
|
||||
Type: src.Type,
|
||||
Channel: src.Channel,
|
||||
Title: src.Title,
|
||||
Description: src.Description,
|
||||
Source: src.Source,
|
||||
@@ -6038,6 +6052,7 @@ func (s *knowledgeService) ensureFAQKnowledge(
|
||||
TenantID: tenantID,
|
||||
KnowledgeBaseID: kb.ID,
|
||||
Type: types.KnowledgeTypeFAQ,
|
||||
Channel: types.ChannelWeb,
|
||||
Title: fmt.Sprintf("%s - FAQ", kb.Name),
|
||||
Description: "FAQ 条目容器",
|
||||
Source: types.KnowledgeTypeFAQ,
|
||||
@@ -8610,6 +8625,7 @@ func (s *knowledgeService) getOrCreateFAQKnowledge(ctx context.Context, kb *type
|
||||
TenantID: kb.TenantID,
|
||||
KnowledgeBaseID: kb.ID,
|
||||
Type: types.KnowledgeTypeFAQ,
|
||||
Channel: types.ChannelWeb,
|
||||
Title: "FAQ",
|
||||
ParseStatus: "completed",
|
||||
EnableStatus: "enabled",
|
||||
@@ -8621,6 +8637,7 @@ func (s *knowledgeService) getOrCreateFAQKnowledge(ctx context.Context, kb *type
|
||||
knowledge.Title = srcKnowledge.Title
|
||||
knowledge.Description = srcKnowledge.Description
|
||||
knowledge.Source = srcKnowledge.Source
|
||||
knowledge.Channel = srcKnowledge.Channel
|
||||
knowledge.Metadata = srcKnowledge.Metadata
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ func (s *knowledgeBaseService) buildSearchResult(chunk *types.Chunk,
|
||||
ImageInfo: chunk.ImageInfo,
|
||||
KnowledgeFilename: knowledge.FileName,
|
||||
KnowledgeSource: knowledge.Source,
|
||||
KnowledgeChannel: knowledge.Channel,
|
||||
ChunkMetadata: chunk.Metadata,
|
||||
MatchedContent: matchedContent,
|
||||
KnowledgeBaseID: knowledge.KnowledgeBaseID,
|
||||
|
||||
@@ -369,7 +369,7 @@ func (s *messageService) IndexMessageToKB(ctx context.Context, userQuery string,
|
||||
passages = append(passages, passage)
|
||||
|
||||
// Use async (non-sync) passage creation so it doesn't block the response
|
||||
knowledge, err := s.knowService.CreateKnowledgeFromPassage(ctx, cfg.KnowledgeBaseID, passages)
|
||||
knowledge, err := s.knowService.CreateKnowledgeFromPassage(ctx, cfg.KnowledgeBaseID, passages, "")
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to index message to chat history KB: %v", err)
|
||||
return
|
||||
|
||||
@@ -89,7 +89,7 @@ func (s *WebSearchService) CompressWithRAG(
|
||||
if body != "" {
|
||||
contentLines = append(contentLines, body)
|
||||
}
|
||||
knowledge, err := knowSvc.CreateKnowledgeFromPassageSync(ctx, createdKB.ID, contentLines)
|
||||
knowledge, err := knowSvc.CreateKnowledgeFromPassageSync(ctx, createdKB.ID, contentLines, "")
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "failed to ingest passage into temp KB: %v", err)
|
||||
continue
|
||||
|
||||
@@ -280,8 +280,10 @@ func (h *KnowledgeHandler) CreateKnowledgeFromFile(c *gin.Context) {
|
||||
tagID = ""
|
||||
}
|
||||
|
||||
channel := c.PostForm("channel")
|
||||
|
||||
// Create knowledge entry from the file
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromFile(ctx, kbID, file, metadata, enableMultimodel, customFileName, tagID)
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromFile(ctx, kbID, file, metadata, enableMultimodel, customFileName, tagID, channel)
|
||||
// Check for duplicate knowledge error
|
||||
if err != nil {
|
||||
if h.handleDuplicateKnowledgeError(c, err, knowledge, "file") {
|
||||
@@ -348,6 +350,7 @@ func (h *KnowledgeHandler) CreateKnowledgeFromURL(c *gin.Context) {
|
||||
EnableMultimodel *bool `json:"enable_multimodel"`
|
||||
Title string `json:"title"`
|
||||
TagID string `json:"tag_id"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error(ctx, "Failed to parse URL request", err)
|
||||
@@ -375,7 +378,7 @@ func (h *KnowledgeHandler) CreateKnowledgeFromURL(c *gin.Context) {
|
||||
)
|
||||
|
||||
// Create knowledge entry from the URL
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromURL(ctx, kbID, req.URL, req.FileName, req.FileType, req.EnableMultimodel, req.Title, req.TagID)
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromURL(ctx, kbID, req.URL, req.FileName, req.FileType, req.EnableMultimodel, req.Title, req.TagID, req.Channel)
|
||||
// Check for duplicate knowledge error
|
||||
if err != nil {
|
||||
if h.handleDuplicateKnowledgeError(c, err, knowledge, "url") {
|
||||
@@ -440,7 +443,7 @@ func (h *KnowledgeHandler) CreateManualKnowledge(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromManual(ctx, kbID, &req)
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromManual(ctx, kbID, &req, req.Channel)
|
||||
if err != nil {
|
||||
if appErr, ok := errors.IsAppError(err); ok {
|
||||
c.Error(appErr)
|
||||
|
||||
@@ -1919,7 +1919,7 @@ func (s *Service) processFileToKnowledgeBase(ctx context.Context, msg *IncomingM
|
||||
fh := newInMemoryFileHeader(fileName, content)
|
||||
|
||||
// Create knowledge entry via the knowledge service
|
||||
knowledge, err := s.knowledgeService.CreateKnowledgeFromFile(kbCtx, kbID, fh, nil, nil, "", "")
|
||||
knowledge, err := s.knowledgeService.CreateKnowledgeFromFile(kbCtx, kbID, fh, nil, nil, "", "", imPlatformToChannel(channel.Platform))
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
// Check for duplicate file
|
||||
@@ -2269,6 +2269,24 @@ func fileExtension(filename string) string {
|
||||
return strings.ToLower(parts[len(parts)-1])
|
||||
}
|
||||
|
||||
// imPlatformToChannel maps an IM platform identifier to a Knowledge.Channel constant.
|
||||
func imPlatformToChannel(platform string) string {
|
||||
switch strings.ToLower(platform) {
|
||||
case "wechat":
|
||||
return types.ChannelWechat
|
||||
case "wecom", "wxwork":
|
||||
return types.ChannelWecom
|
||||
case "feishu", "lark":
|
||||
return types.ChannelFeishu
|
||||
case "dingtalk":
|
||||
return types.ChannelDingtalk
|
||||
case "slack":
|
||||
return types.ChannelSlack
|
||||
default:
|
||||
return types.ChannelIM
|
||||
}
|
||||
}
|
||||
|
||||
// newInMemoryFileHeader wraps in-memory file content as a *multipart.FileHeader
|
||||
// so it can be passed to CreateKnowledgeFromFile which expects a multipart upload.
|
||||
func newInMemoryFileHeader(filename string, data []byte) *multipart.FileHeader {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// KnowledgeService defines the interface for knowledge services.
|
||||
type KnowledgeService interface {
|
||||
// CreateKnowledgeFromFile creates knowledge from a file.
|
||||
// tagID is optional - when provided, the file will be assigned to the specified tag/category.
|
||||
// channel identifies the ingestion channel (e.g. "web", "api", "wechat"); empty defaults to "web".
|
||||
CreateKnowledgeFromFile(
|
||||
ctx context.Context,
|
||||
kbID string,
|
||||
@@ -21,11 +21,12 @@ type KnowledgeService interface {
|
||||
enableMultimodel *bool,
|
||||
customFileName string,
|
||||
tagID string,
|
||||
channel string,
|
||||
) (*types.Knowledge, error)
|
||||
// CreateKnowledgeFromURL creates knowledge from a URL.
|
||||
// When fileName or fileType is provided (or the URL path has a known file extension),
|
||||
// the URL is treated as a direct file download instead of a web page crawl.
|
||||
// tagID is optional - when provided, the knowledge will be assigned to the specified tag/category.
|
||||
// channel identifies the ingestion channel; empty defaults to "web".
|
||||
CreateKnowledgeFromURL(
|
||||
ctx context.Context,
|
||||
kbID string,
|
||||
@@ -35,16 +36,20 @@ type KnowledgeService interface {
|
||||
enableMultimodel *bool,
|
||||
title string,
|
||||
tagID string,
|
||||
channel string,
|
||||
) (*types.Knowledge, error)
|
||||
// CreateKnowledgeFromPassage creates knowledge from text passages.
|
||||
CreateKnowledgeFromPassage(ctx context.Context, kbID string, passage []string) (*types.Knowledge, error)
|
||||
// channel identifies the ingestion channel; empty defaults to "web".
|
||||
CreateKnowledgeFromPassage(ctx context.Context, kbID string, passage []string, channel string) (*types.Knowledge, error)
|
||||
// CreateKnowledgeFromPassageSync creates knowledge from text passages and waits until chunks are indexed.
|
||||
CreateKnowledgeFromPassageSync(ctx context.Context, kbID string, passage []string) (*types.Knowledge, error)
|
||||
CreateKnowledgeFromPassageSync(ctx context.Context, kbID string, passage []string, channel string) (*types.Knowledge, error)
|
||||
// CreateKnowledgeFromManual creates or saves manual Markdown knowledge content.
|
||||
// channel identifies the ingestion channel; empty defaults to "web".
|
||||
CreateKnowledgeFromManual(
|
||||
ctx context.Context,
|
||||
kbID string,
|
||||
payload *types.ManualKnowledgePayload,
|
||||
channel string,
|
||||
) (*types.Knowledge, error)
|
||||
// GetKnowledgeByID retrieves knowledge by ID (uses tenant from context).
|
||||
GetKnowledgeByID(ctx context.Context, id string) (*types.Knowledge, error)
|
||||
|
||||
@@ -16,6 +16,20 @@ const (
|
||||
KnowledgeTypeFAQ = "faq"
|
||||
)
|
||||
|
||||
// Channel constants identify through which channel a knowledge entry was ingested.
|
||||
// Aligned with Message.Channel values ("web", "api", "im") but allows finer granularity.
|
||||
const (
|
||||
ChannelWeb = "web" // Web UI (default)
|
||||
ChannelAPI = "api" // External API call
|
||||
ChannelBrowserExtension = "browser_extension" // Browser extension / plugin
|
||||
ChannelWechat = "wechat" // WeChat
|
||||
ChannelWecom = "wecom" // WeCom (企业微信)
|
||||
ChannelFeishu = "feishu" // Feishu / Lark
|
||||
ChannelDingtalk = "dingtalk" // DingTalk
|
||||
ChannelSlack = "slack" // Slack
|
||||
ChannelIM = "im" // Generic IM channel
|
||||
)
|
||||
|
||||
// Knowledge parse status constants
|
||||
const (
|
||||
// ParseStatusPending indicates the knowledge is waiting to be processed
|
||||
@@ -69,8 +83,10 @@ type Knowledge struct {
|
||||
Title string `json:"title"`
|
||||
// Description of the knowledge
|
||||
Description string `json:"description"`
|
||||
// Source of the knowledge
|
||||
// Source of the knowledge (e.g. URL address for url type, "manual" for manual type)
|
||||
Source string `json:"source"`
|
||||
// Channel indicates through which channel the knowledge was ingested (web, api, browser_extension, wechat, etc.)
|
||||
Channel string `json:"channel" gorm:"type:varchar(50);default:'web'"`
|
||||
// Parse status of the knowledge
|
||||
ParseStatus string `json:"parse_status"`
|
||||
// Summary status for async summary generation
|
||||
@@ -148,6 +164,7 @@ type ManualKnowledgePayload struct {
|
||||
Content string `json:"content"`
|
||||
Status string `json:"status"`
|
||||
TagID string `json:"tag_id"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
// KnowledgeSearchScope defines a (tenant_id, knowledge_base_id) scope for knowledge search (e.g. own KBs + shared KBs).
|
||||
@@ -271,6 +288,9 @@ func (k *Knowledge) EnsureManualDefaults() {
|
||||
if k.Source == "" {
|
||||
k.Source = KnowledgeTypeManual
|
||||
}
|
||||
if k.Channel == "" {
|
||||
k.Channel = ChannelWeb
|
||||
}
|
||||
}
|
||||
|
||||
// IsDraft returns whether the payload should be saved as draft.
|
||||
|
||||
@@ -120,6 +120,9 @@ type SearchResult struct {
|
||||
// Used to indicate the source of the knowledge, such as "url"
|
||||
KnowledgeSource string `json:"knowledge_source"`
|
||||
|
||||
// KnowledgeChannel indicates through which channel the knowledge was ingested (web, api, wechat, etc.)
|
||||
KnowledgeChannel string `json:"knowledge_channel"`
|
||||
|
||||
// ChunkMetadata stores chunk-level metadata (e.g., generated questions)
|
||||
ChunkMetadata JSON `json:"chunk_metadata,omitempty"`
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
-- Migration: 000025_message_channel (rollback)
|
||||
-- Description: Remove channel column from messages
|
||||
-- Description: Remove channel column from messages and knowledge
|
||||
ALTER TABLE messages DROP COLUMN IF EXISTS channel;
|
||||
ALTER TABLE knowledges DROP COLUMN IF EXISTS channel;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
-- Migration: 000025_message_channel
|
||||
-- Description: Add channel column to messages to track the source channel (web, api, im, etc.)
|
||||
DO $$ BEGIN RAISE NOTICE '[Migration 000025] Adding channel column to messages'; END $$;
|
||||
-- Description: Add channel column to messages and knowledge to track the source channel (web, api, im, etc.)
|
||||
DO $$ BEGIN RAISE NOTICE '[Migration 000025] Adding channel column to messages and knowledge'; END $$;
|
||||
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS channel VARCHAR(50) NOT NULL DEFAULT '';
|
||||
|
||||
COMMENT ON COLUMN messages.channel IS 'Source channel of the message: web, api, im, etc.';
|
||||
|
||||
DO $$ BEGIN RAISE NOTICE '[Migration 000025] channel column added to messages successfully'; END $$;
|
||||
ALTER TABLE knowledges ADD COLUMN IF NOT EXISTS channel VARCHAR(50) NOT NULL DEFAULT 'web';
|
||||
COMMENT ON COLUMN knowledges.channel IS 'Source channel of the knowledge: web, api, browser_extension, wechat, etc.';
|
||||
|
||||
DO $$ BEGIN RAISE NOTICE '[Migration 000025] channel column added to messages and knowledge successfully'; END $$;
|
||||
|
||||
Reference in New Issue
Block a user