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:
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -322,6 +322,9 @@ export default {
|
||||
referencesDocAndWebCount: "{docCount}개 문서와 {webCount}개 웹페이지 참조",
|
||||
referenceChunkCount: "{count}개 청크",
|
||||
fallbackHint: "지식 베이스에서 관련 내용을 찾지 못했습니다. 위는 모델의 직접 응답입니다.",
|
||||
channelWeb: "웹",
|
||||
channelApi: "API",
|
||||
channelIm: "IM",
|
||||
chunkLabel: "청크 {index}:",
|
||||
navigateToDocument: "문서 상세 보기",
|
||||
referenceIconAlt: "참조 내용 아이콘",
|
||||
|
||||
@@ -1887,6 +1887,9 @@ export default {
|
||||
referencesDocAndWebCount: 'Использовано {docCount} документ(ов) и {webCount} веб-страниц(ы)',
|
||||
referenceChunkCount: '{count} фрагмент(ов)',
|
||||
fallbackHint: 'В базе знаний не найдено релевантного содержимого. Выше представлен прямой ответ модели.',
|
||||
channelWeb: 'Веб',
|
||||
channelApi: 'API',
|
||||
channelIm: 'IM',
|
||||
chunkLabel: 'Фрагмент {index}:',
|
||||
navigateToDocument: 'Просмотр документа',
|
||||
referenceIconAlt: 'Иконка ссылок на материалы',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 $$;
|
||||
Reference in New Issue
Block a user