feat(channel): add channel support across various components and locales

- Introduced a new `channel` field in multiple request and message structures to track the source channel (e.g., "web", "api", "im").
- Updated frontend API calls and chat components to include the `channel` parameter, ensuring consistent channel tracking in user messages.
- Enhanced localization files to support channel labels in English, Korean, Russian, and Chinese.
- Added database migration scripts to incorporate the `channel` column in the messages table, facilitating the storage of channel information.
- Refactored related functions and components to accommodate the new channel feature, improving overall message context and tracking.
This commit is contained in:
wizardchen
2026-03-24 19:01:40 +08:00
committed by lyingbug
parent de0169d7c0
commit 5b2ebd1f44
19 changed files with 99 additions and 5 deletions
+1
View File
@@ -33,6 +33,7 @@ type AgentQARequest struct {
DisableTitle bool `json:"disable_title,omitempty"` // Whether to disable auto title generation
MCPServiceIDs []string `json:"mcp_service_ids,omitempty"` // Optional MCP service allow list (deprecated)
Images []ImageAttachment `json:"images,omitempty"` // Attached images for multimodal chat
Channel string `json:"channel,omitempty"` // Source channel: "web", "api", "im", etc.
}
// AgentResponseType defines the type of agent response
+1
View File
@@ -49,6 +49,7 @@ type Message struct {
KnowledgeReferences []*SearchResult `json:"knowledge_references"`
AgentSteps []AgentStep `json:"agent_steps,omitempty"` // Agent execution steps (only for assistant messages)
IsCompleted bool `json:"is_completed"`
Channel string `json:"channel,omitempty"` // Source channel: "web", "api", "im", etc.
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+1
View File
@@ -218,6 +218,7 @@ type KnowledgeQARequest struct {
SummaryModelID string `json:"summary_model_id"` // Optional summary model ID (overrides session default)
DisableTitle bool `json:"disable_title"` // Whether to disable auto title generation
Images []ImageAttachment `json:"images,omitempty"` // Attached images for multimodal chat
Channel string `json:"channel,omitempty"` // Source channel: "web", "api", "im", etc.
}
// LLMToolCall represents a function/tool call from the LLM
+3 -2
View File
@@ -15,7 +15,7 @@ export async function generateSessionsTitle(session_id: string, data: any) {
}
export async function knowledgeChat(data: { session_id: string; query: string; }) {
return postChat(`/api/v1/knowledge-chat/${data.session_id}`, { query: data.query });
return postChat(`/api/v1/knowledge-chat/${data.session_id}`, { query: data.query, channel: "web" });
}
// Agent chat with streaming support
@@ -28,7 +28,8 @@ export async function agentChat(data: {
return postChat(`/api/v1/agent-chat/${data.session_id}`, {
query: data.query,
knowledge_base_ids: data.knowledge_base_ids,
agent_enabled: data.agent_enabled
agent_enabled: data.agent_enabled,
channel: "web"
});
}
+1
View File
@@ -118,6 +118,7 @@ export function useStream() {
if (params.images !== undefined && params.images.length > 0) {
postBody.images = params.images;
}
postBody.channel = "web";
await fetchEventSource(url, {
method: params.method,
+3
View File
@@ -1718,6 +1718,9 @@ export default {
referencesDocAndWebCount: 'Referenced {docCount} document(s) and {webCount} web page(s)',
referenceChunkCount: '{count} chunk(s)',
fallbackHint: 'No relevant content found in knowledge base. Above is a direct response from the model.',
channelWeb: 'Web',
channelApi: 'API',
channelIm: 'IM',
chunkLabel: 'Chunk {index}:',
navigateToDocument: 'View document details',
referenceIconAlt: 'Reference materials icon',
+3
View File
@@ -322,6 +322,9 @@ export default {
referencesDocAndWebCount: "{docCount}개 문서와 {webCount}개 웹페이지 참조",
referenceChunkCount: "{count}개 청크",
fallbackHint: "지식 베이스에서 관련 내용을 찾지 못했습니다. 위는 모델의 직접 응답입니다.",
channelWeb: "웹",
channelApi: "API",
channelIm: "IM",
chunkLabel: "청크 {index}:",
navigateToDocument: "문서 상세 보기",
referenceIconAlt: "참조 내용 아이콘",
+3
View File
@@ -1887,6 +1887,9 @@ export default {
referencesDocAndWebCount: 'Использовано {docCount} документ(ов) и {webCount} веб-страниц(ы)',
referenceChunkCount: '{count} фрагмент(ов)',
fallbackHint: 'В базе знаний не найдено релевантного содержимого. Выше представлен прямой ответ модели.',
channelWeb: 'Веб',
channelApi: 'API',
channelIm: 'IM',
chunkLabel: 'Фрагмент {index}:',
navigateToDocument: 'Просмотр документа',
referenceIconAlt: 'Иконка ссылок на материалы',
+3
View File
@@ -319,6 +319,9 @@ export default {
referencesDocAndWebCount: "引用了{docCount}篇文档和{webCount}条网页",
referenceChunkCount: "{count}个片段",
fallbackHint: "未从知识库中检索到相关内容,以上为模型直接回答",
channelWeb: "网页",
channelApi: "API",
channelIm: "IM",
chunkLabel: "片段{index}:",
navigateToDocument: "查看文档详情",
referenceIconAlt: "参考内容图标",
@@ -37,6 +37,9 @@
import { defineProps, computed, ref, watch, onMounted, nextTick } from "vue";
import { hydrateProtectedFileImages } from '@/utils/security';
import picturePreview from '@/components/picture-preview.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
content: {
@@ -52,9 +55,28 @@ const props = defineProps({
type: Array,
required: false,
default: () => []
},
channel: {
type: String,
required: false,
default: ''
}
});
const channelLabelMap = {
web: () => t('chat.channelWeb'),
api: () => t('chat.channelApi'),
im: () => t('chat.channelIm'),
};
const channelLabel = computed(() => {
if (!props.channel) return '';
const label = channelLabelMap[props.channel];
return typeof label === 'function' ? label() : (label || props.channel);
});
const channelClass = computed(() => props.channel ? `channel-${props.channel}` : '');
const containerRef = ref(null);
const hasImages = computed(() => props.images && props.images.length > 0);
@@ -190,6 +212,37 @@ const closePreImg = () => {
}
}
.channel_tag {
display: inline-flex;
align-items: center;
padding: 1px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
line-height: 18px;
background: var(--td-bg-color-secondarycontainer);
color: var(--td-text-color-placeholder);
border: 1px solid var(--td-border-level-2-color, #e7e7e7);
&.channel-web {
color: var(--td-brand-color);
background: var(--td-brand-color-light);
border-color: var(--td-brand-color-2, rgba(0, 82, 217, 0.1));
}
&.channel-api {
color: var(--td-success-color);
background: var(--td-success-color-1, rgba(0, 168, 112, 0.06));
border-color: var(--td-success-color-2, rgba(0, 168, 112, 0.15));
}
&.channel-im {
color: var(--td-warning-color);
background: var(--td-warning-color-1, rgba(237, 123, 0, 0.06));
border-color: var(--td-warning-color-2, rgba(237, 123, 0, 0.15));
}
}
html[theme-mode="dark"] {
.user_msg {
background: var(--td-brand-color-3);
+1 -1
View File
@@ -355,7 +355,7 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = []
}
// 将@提及的知识库和文件信息存入用户消息
messagesList.push({ content: value, role: 'user', mentioned_items: mentionedItems, images: userImages });
messagesList.push({ content: value, role: 'user', mentioned_items: mentionedItems, images: userImages, channel: 'web' });
scrollToBottom();
// Get agent mode status from settings store
+2 -1
View File
@@ -165,7 +165,7 @@ func createAgentQueryEvent(sessionID, assistantMessageID string) interfaces.Stre
}
// createUserMessage creates a user message and returns the created message.
func (h *Handler) createUserMessage(ctx context.Context, sessionID, query, requestID string, mentionedItems types.MentionedItems, images types.MessageImages) (*types.Message, error) {
func (h *Handler) createUserMessage(ctx context.Context, sessionID, query, requestID string, mentionedItems types.MentionedItems, images types.MessageImages, channel string) (*types.Message, error) {
return h.messageService.CreateMessage(ctx, &types.Message{
SessionID: sessionID,
Role: "user",
@@ -175,6 +175,7 @@ func (h *Handler) createUserMessage(ctx context.Context, sessionID, query, reque
IsCompleted: true,
MentionedItems: mentionedItems,
Images: images,
Channel: channel,
})
}
+4 -1
View File
@@ -36,6 +36,7 @@ type qaRequestContext struct {
effectiveTenantID uint64 // when using shared agent, tenant ID for model/KB/MCP resolution; 0 = use context tenant
images []ImageAttachment // Uploaded images with analysis text
userMessageID string // Created user message ID (populated after createUserMessage)
channel string // Source channel: "web", "api", "im", etc.
}
// buildQARequest converts the qaRequestContext into a types.QARequest for service invocation.
@@ -148,6 +149,7 @@ func (h *Handler) parseQARequest(c *gin.Context, logPrefix string) (*qaRequestCo
Role: "assistant",
RequestID: c.GetString(types.RequestIDContextKey.String()),
IsCompleted: false,
Channel: request.Channel,
},
knowledgeBaseIDs: secutils.SanitizeForLogArray(kbIDs),
knowledgeIDs: secutils.SanitizeForLogArray(knowledgeIDs),
@@ -157,6 +159,7 @@ func (h *Handler) parseQARequest(c *gin.Context, logPrefix string) (*qaRequestCo
mentionedItems: convertMentionedItems(request.MentionedItems),
effectiveTenantID: effectiveTenantID,
images: request.Images,
channel: request.Channel,
}
return reqCtx, &request, nil
@@ -475,7 +478,7 @@ func (h *Handler) executeQA(reqCtx *qaRequestContext, mode qaMode, generateTitle
}
// Create user message
userMsg, err := h.createUserMessage(ctx, sessionID, reqCtx.query, reqCtx.requestID, reqCtx.mentionedItems, convertImageAttachments(reqCtx.images))
userMsg, err := h.createUserMessage(ctx, sessionID, reqCtx.query, reqCtx.requestID, reqCtx.mentionedItems, convertImageAttachments(reqCtx.images), reqCtx.channel)
if err != nil {
reqCtx.c.Error(errors.NewInternalServerError(err.Error()))
return
+1
View File
@@ -49,6 +49,7 @@ type CreateKnowledgeQARequest struct {
DisableTitle bool `json:"disable_title"` // Whether to disable auto title generation
EnableMemory bool `json:"enable_memory"` // Whether memory feature is enabled for this request
Images []ImageAttachment `json:"images"` // Attached images for multimodal chat
Channel string `json:"channel"` // Source channel: "web", "api", "im", etc.
}
// SearchKnowledgeRequest defines the request structure for searching knowledge without LLM summarization
+4
View File
@@ -1424,6 +1424,7 @@ func (s *Service) handleMessageStream(ctx context.Context, msg *IncomingMessage,
userMsg, err := s.messageService.CreateMessage(qaCtx, &types.Message{
SessionID: session.ID, Role: "user", Content: msg.Content,
RequestID: requestID, CreatedAt: time.Now(), IsCompleted: true,
Channel: "im",
})
if err != nil {
return fmt.Errorf("create user message: %w", err)
@@ -1433,6 +1434,7 @@ func (s *Service) handleMessageStream(ctx context.Context, msg *IncomingMessage,
assistantMsg, err := s.messageService.CreateMessage(qaCtx, &types.Message{
SessionID: session.ID, Role: "assistant",
RequestID: requestID, CreatedAt: time.Now(), IsCompleted: false,
Channel: "im",
})
if err != nil {
return fmt.Errorf("create assistant message: %w", err)
@@ -1611,6 +1613,7 @@ func (s *Service) runQA(ctx context.Context, session *types.Session, query strin
RequestID: requestID,
CreatedAt: time.Now(),
IsCompleted: true,
Channel: "im",
})
if err != nil {
return "", fmt.Errorf("create user message: %w", err)
@@ -1623,6 +1626,7 @@ func (s *Service) runQA(ctx context.Context, session *types.Session, query strin
RequestID: requestID,
CreatedAt: time.Now(),
IsCompleted: false,
Channel: "im",
})
if err != nil {
return "", fmt.Errorf("create assistant message: %w", err)
+2
View File
@@ -125,6 +125,8 @@ type Message struct {
IsFallback bool `json:"is_fallback,omitempty"`
// Agent total execution duration in milliseconds (from query start to answer start)
AgentDurationMs int64 `json:"agent_duration_ms,omitempty" gorm:"column:agent_duration_ms;default:0"`
// Channel indicates the source channel of this message (e.g., "web", "api", "im")
Channel string `json:"channel,omitempty" gorm:"type:varchar(50);default:''"`
// KnowledgeID links this message to a Knowledge entry in the chat history knowledge base
// Used for vector search indexing: when set, the message content has been indexed as a Knowledge passage
KnowledgeID string `json:"knowledge_id,omitempty" gorm:"type:varchar(36);index"`
+1
View File
@@ -143,6 +143,7 @@ CREATE TABLE IF NOT EXISTS messages (
mentioned_items TEXT DEFAULT '[]',
is_completed BOOLEAN NOT NULL DEFAULT 0,
is_fallback BOOLEAN NOT NULL DEFAULT 0,
channel VARCHAR(50) NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME
@@ -0,0 +1,3 @@
-- Migration: 000025_message_channel (rollback)
-- Description: Remove channel column from messages
ALTER TABLE messages DROP COLUMN IF EXISTS channel;
@@ -0,0 +1,9 @@
-- 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 $$;
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 $$;