From c216bdfdd4be074147e04b30b48590dce817a078 Mon Sep 17 00:00:00 2001 From: opsx Date: Sun, 1 Nov 2026 00:31:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E5=8A=A9=E6=89=8B=E6=B5=AE?= =?UTF-8?q?=E7=AA=97=E8=81=9A=E7=84=A6=E6=97=A5=E5=BF=97=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E9=A1=B5,=E6=90=BA=E5=B8=A6=E5=BD=93=E5=89=8D=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E7=BB=93=E6=9E=9C=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 浮球只在 /logs 渲染(产品定位:辅助分析正在查看的查询结果,其它页面不打扰) - /logs 查询成功后写入 AI 上下文:LogQL、时间范围、结果条数、error 行数、 最新 20 条样本(去 CRI 包装);对话请求自动携带 - 后端 /ai/chat 接收 context:截断 6000 字 + RedactTextOnly 统一脱敏后并入 system (ECHO 假模型实测注入完整);空态文案改为查询导向 --- backend/controllers/ai.go | 3 ++- backend/services/ai_provider.go | 9 ++++++- frontend/src/api/ai/index.ts | 2 +- frontend/src/components/AiAssistant.tsx | 5 +++- frontend/src/i18n/locales/en/aiops.ts | 2 +- frontend/src/i18n/locales/zh/aiops.ts | 2 +- frontend/src/lib/ai-assistant-context.ts | 32 ++++++++++++++++++++++++ frontend/src/pages/logs/index.tsx | 10 ++++++++ 8 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 frontend/src/lib/ai-assistant-context.ts diff --git a/backend/controllers/ai.go b/backend/controllers/ai.go index 8b8deec..74b67fa 100644 --- a/backend/controllers/ai.go +++ b/backend/controllers/ai.go @@ -161,6 +161,7 @@ func (ctrl *AIController) Chat(c *gin.Context) { History []services.ChatMessage `json:"history" binding:"required,min=1,max=40"` Page string `json:"page"` RangeMinutes int `json:"rangeMinutes"` + Context string `json:"context"` } if !utils.BindAndValidate(c, &req) { return @@ -174,7 +175,7 @@ func (ctrl *AIController) Chat(c *gin.Context) { if rangeMinutes <= 0 || rangeMinutes > 120 { rangeMinutes = 15 } - reply, err := services.RunAIChat(c.Request.Context(), cfg, src, c.GetUint("userId"), c.GetString("role") == "admin", req.History, req.Page, rangeMinutes) + reply, err := services.RunAIChat(c.Request.Context(), cfg, src, c.GetUint("userId"), c.GetString("role") == "admin", req.History, req.Page, rangeMinutes, req.Context) if err != nil { utils.Error(c, 400, err.Error()) return diff --git a/backend/services/ai_provider.go b/backend/services/ai_provider.go index 03f0fc8..9bbca36 100644 --- a/backend/services/ai_provider.go +++ b/backend/services/ai_provider.go @@ -155,7 +155,7 @@ type ChatMessage struct { // RunAIChat 全站 AI 助手浮窗:多轮对话 + 当前页面上下文注入(轻量信号摘要)。 // 与整页分析共享配置/配额/脱敏/Pro 闸门;context 里的 sourceId 可选,带了就注入日志摘要。 -func RunAIChat(ctx context.Context, cfg models.AIConfig, src *models.LogSource, userID uint, admin bool, history []ChatMessage, page string, rangeMinutes int) (string, error) { +func RunAIChat(ctx context.Context, cfg models.AIConfig, src *models.LogSource, userID uint, admin bool, history []ChatMessage, page string, rangeMinutes int, pageContext string) (string, error) { if !cfg.Enabled { return "", fmt.Errorf("AI 未启用:请先在「智能运维 → 模型配置」中开启并保存") } @@ -191,6 +191,13 @@ func RunAIChat(ctx context.Context, cfg models.AIConfig, src *models.LogSource, } } + // 页面上下文(如 /logs 刚执行的查询与样本日志):截断并统一脱敏后并入 system + if trimmed := strings.TrimSpace(pageContext); trimmed != "" { + if len([]rune(trimmed)) > 6000 { + trimmed = string([]rune(trimmed)[:6000]) + } + system += "\n\n" + RedactTextOnly(trimmed, cfg.RedactExtra) + } msgs := []aiMessage{{Role: "system", Content: system}} for _, m := range history { msgs = append(msgs, aiMessage{Role: m.Role, Content: m.Content}) diff --git a/frontend/src/api/ai/index.ts b/frontend/src/api/ai/index.ts index c65849f..0ed9487 100644 --- a/frontend/src/api/ai/index.ts +++ b/frontend/src/api/ai/index.ts @@ -92,6 +92,6 @@ export const aiApi = { ), run: (id: number) => http.get(`/ai/runs/${id}`), - chat: (data: { sourceId?: number; history: Array<{ role: 'user' | 'assistant'; content: string }>; page?: string; rangeMinutes?: number }) => + chat: (data: { sourceId?: number; history: Array<{ role: 'user' | 'assistant'; content: string }>; page?: string; rangeMinutes?: number; context?: string }) => http.post<{ reply: string }>('/ai/chat', data), } diff --git a/frontend/src/components/AiAssistant.tsx b/frontend/src/components/AiAssistant.tsx index 5326e69..86041e2 100644 --- a/frontend/src/components/AiAssistant.tsx +++ b/frontend/src/components/AiAssistant.tsx @@ -7,6 +7,7 @@ import { Textarea } from '@/components/ui/textarea' import { aiApi } from '@/api/ai' import { getUser } from '@/lib/auth-storage' import { useLicense } from '@/hooks/useLicense' +import { buildLogsContextText } from '@/lib/ai-assistant-context' interface Msg { role: 'user' | 'assistant'; content: string } @@ -30,7 +31,8 @@ export const AiAssistant = memo(function AiAssistant() { if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight }, [messages, sending]) - if (!isAdmin || !pro) return null + // 按产品定位:助手服务于日志查询 —— 只在日志查询页出现,带着当前查询结果对话 + if (!isAdmin || !pro || location.pathname !== '/logs') return null const send = async () => { const text = input.trim() @@ -44,6 +46,7 @@ export const AiAssistant = memo(function AiAssistant() { history: next.slice(-20), page: location.pathname, rangeMinutes: 15, + context: buildLogsContextText() || undefined, }) setMessages(current => [...current, { role: 'assistant', content: res.reply }]) } catch (err) { diff --git a/frontend/src/i18n/locales/en/aiops.ts b/frontend/src/i18n/locales/en/aiops.ts index 59b86b0..e9f7579 100644 --- a/frontend/src/i18n/locales/en/aiops.ts +++ b/frontend/src/i18n/locales/en/aiops.ts @@ -7,7 +7,7 @@ export default { thinking: 'Thinking…', error: 'Request failed', placeholder: 'Ask about logs, e.g. anything unusual now?', - emptyHint: 'Just ask; I can see the page you are viewing and the last 15 minutes of logs.', + emptyHint: 'I can see the logs you just queried — ask away, e.g. "common cause of these errors?"', }, title: 'AIOps', diff --git a/frontend/src/i18n/locales/zh/aiops.ts b/frontend/src/i18n/locales/zh/aiops.ts index 8129aa2..d73d7d5 100644 --- a/frontend/src/i18n/locales/zh/aiops.ts +++ b/frontend/src/i18n/locales/zh/aiops.ts @@ -7,7 +7,7 @@ export default { thinking: '思考中…', error: '请求失败', placeholder: '问问日志的事,如:当前有什么异常?', - emptyHint: '直接提问;我会参考你当前正在查看的页面与近 15 分钟日志概况。', + emptyHint: '刚查到的日志就在我手上——直接问,例如:「这些报错的共同原因是什么」「哪个应用的问题最大」', }, title: '智能运维', diff --git a/frontend/src/lib/ai-assistant-context.ts b/frontend/src/lib/ai-assistant-context.ts new file mode 100644 index 0000000..cf06c08 --- /dev/null +++ b/frontend/src/lib/ai-assistant-context.ts @@ -0,0 +1,32 @@ +// AI 助手页面上下文 — 模块级单例(避免为一个字符串加全局 Provider): +// /logs 页在查询成功后把当前查询与结果摘要写入,AiAssistant 发送时带上, +// 后端注入 system,让"刚才查到的结果有什么问题"有真实数据依据。 +export interface LogsAIContext { + query: string + rangeMinutes: number + totalEntries: number + errorLines: number + samples: string[] +} + +let logsContext: LogsAIContext | null = null + +export function setLogsAIContext(ctx: LogsAIContext | null) { + logsContext = ctx +} + +export function getLogsAIContext() { + return logsContext +} + +/** 组装给后端的上下文摘要(截断保护:样本行与总量都封顶) */ +export function buildLogsContextText(): string { + if (!logsContext) return '' + const { query, rangeMinutes, totalEntries, errorLines, samples } = logsContext + const lines = samples.slice(0, 20).map(s => s.slice(0, 300)) + return [ + `用户刚执行的 LogQL 查询: ${query}`, + `时间范围: 近 ${rangeMinutes} 分钟;加载 ${totalEntries} 条(首页),其中含 error 关键字约 ${errorLines} 条`, + lines.length ? `样本日志(最新在前,已截断):\n${lines.join('\n')}` : '', + ].filter(Boolean).join('\n') +} diff --git a/frontend/src/pages/logs/index.tsx b/frontend/src/pages/logs/index.tsx index 58c032b..0408573 100644 --- a/frontend/src/pages/logs/index.tsx +++ b/frontend/src/pages/logs/index.tsx @@ -18,6 +18,8 @@ import { SourcesManager } from './components/SourcesManager' import { LogContextDialog, type LogContext } from './components/LogContextDialog' import { LogResultsPanel } from './components/LogResultsPanel' import { literalFilters } from './query-utils' +import { setLogsAIContext } from '@/lib/ai-assistant-context' +import { stripLogEnvelope } from '@/lib/ingress-access-log' import { readQueryHistory, rememberQuery } from './query-history' import { useDebouncedValue } from '@/hooks/useDebouncedValue' @@ -173,6 +175,14 @@ export default function LogsPage() { if (version !== requestVersion.current) return if (paging && result.entries.length === 0) { setCursor(''); return } if (!paging) setQueryHistory(rememberQuery(data.query)) + // AI 助手上下文:当前查询 + 首页结果摘要(样本原文,服务端统一脱敏) + setLogsAIContext({ + query: data.query, + rangeMinutes: Math.max(1, Math.round((r.end - r.start) / 6e10)), + totalEntries: result.entries.length, + errorLines: result.entries.filter(e => e.level === 'error').length, + samples: result.entries.slice(0, 20).map(e => stripLogEnvelope(e.message)), + }) setExecutedQuery(data.query) if (!paging) setExecutedRange(r) setSearched(true)