@@ -0,0 +1,955 @@
< template >
< div class = "p-6 h-full overflow-auto" >
<!-- 页面标题 -- >
< div class = "mb-6" >
< h1 class = "text-2xl font-bold text-gray-900" > SLS 日志分析 < / h1 >
< p class = "text-gray-600 mt-1" > 查询和分析阿里云 SLS 日志 , 支持 AI 智能分析 < / p >
< / div >
<!-- 状态提示 -- >
< div v-if = "!config.sls_configured" class="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg" >
< div class = "flex items-center text-yellow-700" >
< svg class = "w-5 h-5 mr-2" fill = "currentColor" viewBox = "0 0 20 20" >
< path fill -rule = " evenodd " d = "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip -rule = " evenodd " / >
< / svg >
< span > SLS 服务未配置 , 请在 . env 中设置 SLS _ * 配置项 < / span >
< / div >
< / div >
< div v-if = "!config.ai_configured" class="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg" >
< div class = "flex items-center text-yellow-700" >
< svg class = "w-5 h-5 mr-2" fill = "currentColor" viewBox = "0 0 20 20" >
< path fill -rule = " evenodd " d = "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip -rule = " evenodd " / >
< / svg >
< span > AI 服务未配置 , 请在下方配置 AI 提供商 < / span >
< / div >
< / div >
<!-- 查询区域 -- >
< div class = "bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6" >
< h2 class = "text-lg font-semibold text-gray-700 mb-4" > 查询条件 < / h2 >
< div class = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4" >
<!-- 开始时间 -- >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-2" > 开始时间 < / label >
< input
v-model = "queryParams.from"
type = "datetime-local"
class = "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
< / div >
<!-- 结束时间 -- >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-2" > 结束时间 < / label >
< input
v-model = "queryParams.to"
type = "datetime-local"
class = "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
< / div >
<!-- 分析模式 -- >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-2" > 分析模式 < / label >
< select
v-model = "queryParams.mode"
class = "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
< option value = "logs" > 仅日志分析 < / option >
< option value = "logs+code" > 日志 + 代码分析 < / option >
< / select >
< / div >
<!-- 快捷时间 -- >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-2" > 快捷选择 < / label >
< div class = "flex gap-2" >
< button @click ="setQuickTime('1h')" class = "px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-md" > 1 小时 < / button >
< button @click ="setQuickTime('6h')" class = "px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-md" > 6 小时 < / button >
< button @click ="setQuickTime('24h')" class = "px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-md" > 24 小时 < / button >
< / div >
< / div >
< / div >
<!-- 查询语句 -- >
< div class = "mb-4" >
< label class = "block text-sm font-medium text-gray-700 mb-2" >
SLS 查询语句
< span class = "text-xs text-gray-500 font-normal ml-2" >
( AI 分析时留空默认查询 ERROR 和 WARNING 日志 )
< / span >
< / label >
< input
v-model = "queryParams.query"
type = "text"
placeholder = '留空默认: ERROR or WARNING,或自定义如: * | where level = "ERROR"'
class = "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
< div class = "mt-1 text-xs text-gray-500" >
< details class = "cursor-pointer" >
< summary class = "text-blue-600 hover:text-blue-800" > 查询语法说明 < / summary >
< div class = "mt-2 p-3 bg-gray-50 rounded border border-gray-200 space-y-1" >
< p > < strong > 全文搜索 : < / strong > < / p >
< p class = "ml-2" > • < code class = "bg-white px-1" > * < / code > - 查询所有日志 < / p >
< p class = "ml-2" > • < code class = "bg-white px-1" > ERROR < / code > - 搜索包含 ERROR 的日志 < / p >
< p class = "ml-2" > • < code class = "bg-white px-1" > ERROR and timeout < / code > - 同时包含两个关键词 < / p >
< p class = "ml-2" > • < code class = "bg-white px-1" > ERROR or WARNING < / code > - 包含任一关键词 < / p >
< p class = "ml-2" > • < code class = "bg-white px-1" > ERROR not success < / code > - 包含 ERROR 但不包含 success < / p >
< p class = "mt-2" > < strong > SQL 分析语法 ( 需要字段索引 ) : < / strong > < / p >
< p class = "ml-2" > • < code class = "bg-white px-1" > * | where level = "ERROR" < / code > - 筛选 level 字段 < / p >
< p class = "ml-2" > • < code class = "bg-white px-1" > * | where level in ( "ERROR" , "WARNING" ) < / code > - 多个值 < / p >
< p class = "ml-2" > • < code class = "bg-white px-1" > * | where status >= 500 < / code > - 数值比较 < / p >
< / div >
< / details >
< / div >
< / div >
<!-- Logstore 选择 -- >
< div v-if = "availableLogstores.length > 0" class="mb-4" >
< label class = "block text-sm font-medium text-gray-700 mb-2" >
选择 Logstore ( { { queryParams . logstores . length } } / { { availableLogstores . length } } )
< / label >
< div class = "flex flex-wrap gap-2" >
< label
v-for = "logstore in availableLogstores"
:key = "logstore"
class = "inline-flex items-center px-3 py-2 border rounded-md cursor-pointer transition-colors"
: class = "queryParams.logstores.includes(logstore)
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'"
>
< input
type = "checkbox"
:value = "logstore"
v-model = "queryParams.logstores"
class = "mr-2"
>
< span class = "text-sm font-medium" > { { logstore } } < / span >
< / label >
< / div >
< div class = "mt-2 flex gap-2" >
< button
@click ="queryParams.logstores = [...availableLogstores]"
class = "text-sm text-blue-600 hover:text-blue-800"
>
全选
< / button >
< button
@click ="queryParams.logstores = []"
class = "text-sm text-blue-600 hover:text-blue-800"
>
清空
< / button >
< / div >
< / div >
<!-- 操作按钮 -- >
< div class = "flex gap-3" >
< button
@click ="handleQuery"
: disabled = "loading || !config.sls_configured"
class = "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
>
< svg v-if = "loading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
查询日志
< / button >
< button
@click ="handleAnalyze"
: disabled = "analyzing || !config.sls_configured || !config.ai_configured"
class = "px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
>
< svg v-if = "analyzing" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
AI 分析
< / button >
< / div >
< / div >
<!-- Tab 切换 -- >
< div class = "mb-4 border-b border-gray-200" >
< nav class = "flex space-x-8" >
< button
@click ="activeTab = 'results'"
: class = "[
'py-2 px-1 border-b-2 font-medium text-sm',
activeTab === 'results'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
查询结果
< / button >
< button
@click ="activeTab = 'analysis'"
: class = "[
'py-2 px-1 border-b-2 font-medium text-sm',
activeTab === 'analysis'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
AI 分析结果
< / button >
< button
@click ="activeTab = 'history'"
: class = "[
'py-2 px-1 border-b-2 font-medium text-sm',
activeTab === 'history'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
历史报告
< / button >
< button
@click ="activeTab = 'config'"
: class = "[
'py-2 px-1 border-b-2 font-medium text-sm',
activeTab === 'config'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
AI 配置
< / button >
< / nav >
< / div >
<!-- 错误提示 -- >
< div v-if = "error" class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700" >
{{ error }}
< / div >
< ! - - 查询结果 Tab - - >
< div v-if = "activeTab === 'results'" class="bg-white rounded-lg shadow-sm border border-gray-200" >
< div v-if = "!queryResult" class="p-8 text-center text-gray-500" >
请输入查询条件并点击 " 查询日志 "
< / div >
< div v-else >
< ! - - 统计信息 - - >
< div class = "p-4 border-b border-gray-200 bg-gray-50" >
< div class = "flex flex-col gap-3" >
< div class = "flex items-center justify-between" >
< div class = "flex gap-6" >
< span class = "text-sm text-gray-600" > 总日志数 : < strong > { { queryResult . total } } < / strong > < / span >
< span v-for = "(count, level) in queryResult.statistics?.by_level" :key="level" class="text-sm" >
< span :class = "getLevelClass(level)" > { { level } } : { { count } } < / span >
< / span >
< / div >
< / div >
<!-- 按 Logstore 分组统计 -- >
< div v-if = "queryResult.statistics?.by_logstore" class="flex flex-wrap gap-2" >
< span class = "text-sm text-gray-600 font-medium" > 按 Logstore : < / span >
< span
v-for = "(data, logstore) in queryResult.statistics.by_logstore"
:key = "logstore"
class = "text-sm px-2 py-1 rounded"
: class = "data.success ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
>
{ { logstore } } : { { data . count } }
< span v-if = "!data.success" class="text-xs" > ( 失败 ) < / span >
< / span >
< / div >
< / div >
< / div >
< ! - - 日志列表 - - >
< div class = "overflow-x-auto max-h-96" >
< table class = "min-w-full divide-y divide-gray-200" >
< thead class = "bg-gray-50 sticky top-0" >
< tr >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 时间 < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 级别 < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 应用 < / th >
< th v-if = "queryResult.statistics?.by_logstore" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > Logstore < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 消息 < / th >
< / tr >
< / thead >
< tbody class = "bg-white divide-y divide-gray-200" >
< tr v-for = "(log, index) in queryResult.logs" :key="index" class="hover:bg-gray-50" >
< td class = "px-4 py-2 whitespace-nowrap text-sm text-gray-500" > { { log . time } } < / td >
< td class = "px-4 py-2 whitespace-nowrap" >
< span :class = "getLevelBadgeClass(log.level)" class = "px-2 py-1 rounded text-xs font-medium" >
{ { log . level } }
< / span >
< / td >
< td class = "px-4 py-2 whitespace-nowrap text-sm text-gray-600" > { { log . app _name } } < / td >
< td v-if = "queryResult.statistics?.by_logstore" class="px-4 py-2 whitespace-nowrap text-sm text-gray-600" >
< span class = "px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs" > { { log . _logstore || '-' } } < / span >
< / td >
< td class = "px-4 py-2 text-sm text-gray-600 max-w-md truncate" :title = "log.message" > { { log . message } } < / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< / div >
<!-- AI 分析结果 Tab -- >
< div v-if = "activeTab === 'analysis'" class="bg-white rounded-lg shadow-sm border border-gray-200" >
< div v-if = "!analysisResult" class="p-8 text-center text-gray-500" >
请在历史报告中选择一个已完成的分析报告查看结果
< / div >
< div v-else class = "p-4" >
<!-- 状态提示 -- >
< div v-if = "analysisResult.status === 'pending'" class="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg" >
< div class = "flex items-center text-yellow-700" >
< svg class = "animate-spin h-5 w-5 mr-2" fill = "none" viewBox = "0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
< span > 分析任务正在处理中 , 请稍候 ... < / span >
< button @click ="refreshReport(analysisResult.id)" class = "ml-4 text-blue-600 hover:text-blue-800 text-sm" >
刷新状态
< / button >
< / div >
< / div >
< div v-else-if = "analysisResult.status === 'failed'" class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg" >
< div class = "text-red-700" >
< strong > 分析失败 : < / strong > { { analysisResult . error _message } }
< / div >
< / div >
<!-- 元数据 -- >
< div class = "mb-4 p-3 bg-gray-50 rounded-lg text-sm text-gray-600" >
< span > 分析时间 : { { analysisResult . metadata ? . analyzed _at || analysisResult . created _at } } < / span >
< span class = "ml-4" > 总日志 : { { analysisResult . metadata ? . total _logs ? ? analysisResult . total _logs ? ? 0 } } < / span >
< span v-if = "analysisResult.metadata?.execution_time_ms" class="ml-4" > 耗时 : {{ analysisResult.metadata.execution_time_ms }} ms < / span >
< span class = "ml-4" >
状态 :
< span :class = "getStatusClass(analysisResult.status)" class = "px-2 py-0.5 rounded text-xs font-medium" >
{ { getStatusText ( analysisResult . status ) } }
< / span >
< / span >
< / div >
<!-- 按应用展示结果 -- >
< div v-if = "analysisResult.status === 'completed' && analysisResult.results" >
< div v-for = "(result, appName) in analysisResult.results" :key="appName" class="mb-6 border border-gray-200 rounded-lg" >
< div class = "p-4 bg-gray-50 border-b border-gray-200 flex items-center justify-between" >
< h3 class = "font-semibold text-gray-800" > { { appName } } < / h3 >
< span :class = "getImpactClass(result.impact)" class = "px-3 py-1 rounded-full text-sm font-medium" >
{ { result . impact || 'N/A' } }
< / span >
< / div >
< div class = "p-4" >
< div v-if = "result.error" class="text-red-600" >
分析失败 : {{ result.error }}
< / div >
< div v-else >
< p class = "text-gray-700 mb-4" > { { result . summary } } < / p >
<!-- 异常列表 -- >
< div v-if = "result.core_anomalies?.length" class="space-y-3" >
< h4 class = "font-medium text-gray-700" > 异常列表 ( { { result . core _anomalies . length } } ) < / h4 >
< div v-for = "(anomaly, idx) in result.core_anomalies" :key="idx"
class = "p-3 bg-gray-50 rounded-lg border-l-4"
:class = "getAnomalyBorderClass(anomaly.type)" >
< div class = "flex items-center gap-2 mb-2" >
< span class = "px-2 py-0.5 bg-gray-200 rounded text-xs" > { { anomaly . type } } < / span >
< span class = "px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs" > { { anomaly . classification } } < / span >
< span class = "text-sm text-gray-500" > x { { anomaly . count } } < / span >
< / div >
< p class = "text-sm text-gray-700 mb-1" > < strong > 可能原因 : < / strong > { { anomaly . possible _cause } } < / p >
< p class = "text-sm text-gray-600" > < strong > 建议 : < / strong > { { anomaly . suggestion } } < / p >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< div v-else-if = "analysisResult.status === 'completed' && (!analysisResult.results || Object.keys(analysisResult.results).length === 0)" class="text-center text-gray-500 py-8" >
未找到匹配的日志或分析结果为空
< / div >
< / div >
< / div >
< ! - - 历史报告 Tab - - >
< div v-if = "activeTab === 'history'" class="bg-white rounded-lg shadow-sm border border-gray-200" >
< div class = "p-4 border-b border-gray-200 flex items-center justify-between" >
< h3 class = "font-semibold text-gray-700" > 历史报告 < / h3 >
< button @click ="loadHistory" :disabled = "historyLoading" class = "text-sm text-blue-600 hover:text-blue-800 flex items-center" >
< svg v-if = "historyLoading" class="animate-spin h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
刷新
< / button >
< / div >
< div v-if = "historyLoading && !historyReports.length" class="p-8 text-center text-gray-500" >
加载中...
< / div >
< div v-else-if = "!historyReports.length" class="p-8 text-center text-gray-500" >
暂无历史报告
< / div >
< div v-else class = "overflow-x-auto" >
< table class = "min-w-full divide-y divide-gray-200" >
< thead class = "bg-gray-50" >
< tr >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > ID < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 时间范围 < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 模式 < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 日志数 < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 状态 < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 创建时间 < / th >
< th class = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" > 操作 < / th >
< / tr >
< / thead >
< tbody class = "bg-white divide-y divide-gray-200" >
< tr v-for = "report in historyReports" :key="report.id" class="hover:bg-gray-50" >
< td class = "px-4 py-2 text-sm text-gray-500" > # { { report . id } } < / td >
< td class = "px-4 py-2 text-sm text-gray-600" > { { report . from _time } } ~ { { report . to _time } } < / td >
< td class = "px-4 py-2 text-sm text-gray-600" > { { report . mode } } < / td >
< td class = "px-4 py-2 text-sm text-gray-600" > { { report . total _logs } } < / td >
< td class = "px-4 py-2" >
< span :class = "getStatusClass(report.status)" class = "px-2 py-1 rounded text-xs font-medium" >
{ { getStatusText ( report . status ) } }
< / span >
< / td >
< td class = "px-4 py-2 text-sm text-gray-500" > { { report . created _at } } < / td >
< td class = "px-4 py-2" >
< button @click ="viewReport(report.id)" class = "text-blue-600 hover:text-blue-800 text-sm" >
查看详情
< / button >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
<!-- AI 配置 Tab -- >
< div v-if = "activeTab === 'config'" class="bg-white rounded-lg shadow-sm border border-gray-200 p-6" >
< h3 class = "text-lg font-semibold text-gray-700 mb-4" > AI 提供商配置 < / h3 >
<!-- 当前激活的提供商 -- >
< div v-if = "config.active_ai_provider" class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg" >
< span class = "text-green-700" > 当前使用 : { { config . active _ai _provider . name || config . active _ai _provider . key } } < / span >
< / div >
<!-- 提供商列表 -- >
< div class = "space-y-4 mb-6" >
< div v-for = "(provider, key) in config.ai_providers" :key="key"
class = "p-4 border border-gray-200 rounded-lg"
: class = "{'border-green-500 bg-green-50': config.active_ai_provider?.key === key}" >
< div class = "flex items-center justify-between mb-2" >
< div class = "flex items-center gap-2" >
< strong > { { provider . name || key } } < / strong >
< span class = "text-sm text-gray-500" > { { provider . model } } < / span >
< / div >
< div class = "flex gap-2" >
< button
v-if = "config.active_ai_provider?.key !== key"
@click ="setActiveProvider(key)"
class = "px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
>
激活
< / button >
< button
v-if = "key !== 'default'"
@click ="deleteProvider(key)"
class = "px-3 py-1 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200"
>
删除
< / button >
< / div >
< / div >
< div class = "text-sm text-gray-600" >
< span > Endpoint : { { provider . endpoint } } < / span >
< / div >
< / div >
< / div >
<!-- 添加新提供商 -- >
< div v-if = "isAdmin" class="border-t border-gray-200 pt-4" >
< h4 class = "font-medium text-gray-700 mb-3" > 添加新的 AI 提供商 < / h4 >
< div class = "grid grid-cols-1 md:grid-cols-2 gap-4 mb-4" >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-1" > 名称 < / label >
< input v-model = "newProvider.name" type="text" placeholder="例如: OpenAI" class="w-full px-3 py-2 border border-gray-300 rounded-md" >
< / div >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-1" > Key ( 唯一标识 ) < / label >
< input v-model = "newProvider.key" type="text" placeholder="例如: openai" class="w-full px-3 py-2 border border-gray-300 rounded-md" >
< / div >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-1" > Endpoint < / label >
< input v-model = "newProvider.endpoint" type="text" placeholder="https://api.openai.com/v1" class="w-full px-3 py-2 border border-gray-300 rounded-md" >
< / div >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-1" > API Key < / label >
< input v-model = "newProvider.api_key" type="password" placeholder="sk-xxx" class="w-full px-3 py-2 border border-gray-300 rounded-md" >
< / div >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-1" > Model < / label >
< input v-model = "newProvider.model" type="text" placeholder="gpt-4o" class="w-full px-3 py-2 border border-gray-300 rounded-md" >
< / div >
< div >
< label class = "block text-sm font-medium text-gray-700 mb-1" > Temperature < / label >
< input v -model .number = " newProvider.temperature " type = "number" step = "0.1" min = "0" max = "2" class = "w-full px-3 py-2 border border-gray-300 rounded-md" >
< / div >
< / div >
< button
@click ="addProvider"
: disabled = "!newProvider.key || !newProvider.endpoint || !newProvider.api_key || !newProvider.model"
class = "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
添加提供商
< / button >
< / div >
<!-- 测试连接 -- >
< div class = "border-t border-gray-200 pt-4 mt-4" >
< button @click ="testAiConnection" :disabled = "testingAi" class = "px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700" >
{ { testingAi ? '测试中...' : '测试 AI 连接' } }
< / button >
< span v-if = "aiTestResult" :class="aiTestResult.success ? 'text-green-600' : 'text-red-600'" class="ml-3" >
{{ aiTestResult.message }}
< / span >
< / div >
< ! - - 定时任务设置 - - >
< div v-if = "isAdmin" class="border-t border-gray-200 pt-4 mt-4" >
< h4 class = "font-medium text-gray-700 mb-3" > 定时任务设置 < / h4 >
< div class = "p-4 bg-gray-50 rounded-lg space-y-4" >
<!-- 每日分析任务 -- >
< div class = "flex items-center justify-between" >
< div >
< span class = "text-sm font-medium text-gray-700" > 每日自动分析 < / span >
< p class = "text-xs text-gray-500 mt-1" > 每天凌晨 2 点自动分析过去 24 小时的 ERROR 日志并推送到钉钉 < / p >
< / div >
< button
type = "button"
@click ="config.settings.daily_schedule_enabled = !config.settings.daily_schedule_enabled; updateScheduleSettings()"
: class = "[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
config.settings.daily_schedule_enabled ? 'bg-blue-600' : 'bg-gray-200'
]"
>
< span
: class = "[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
config.settings.daily_schedule_enabled ? 'translate-x-5' : 'translate-x-0'
]"
> < / span >
< / button >
< / div >
<!-- 每 4 小时分析任务 -- >
< div class = "flex items-center justify-between border-t border-gray-200 pt-4" >
< div >
< span class = "text-sm font-medium text-gray-700" > 高频分析 ( 每 4 小时 ) < / span >
< p class = "text-xs text-gray-500 mt-1" > 每 4 小时自动分析过去 6 小时的 ERROR 日志并推送到钉钉 < / p >
< / div >
< button
type = "button"
@click ="config.settings.schedule_enabled = !config.settings.schedule_enabled; updateScheduleSettings()"
: class = "[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
config.settings.schedule_enabled ? 'bg-blue-600' : 'bg-gray-200'
]"
>
< span
: class = "[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
config.settings.schedule_enabled ? 'translate-x-5' : 'translate-x-0'
]"
> < / span >
< / button >
< / div >
< div v-if = "config.settings.daily_schedule_enabled || config.settings.schedule_enabled" class="text-sm text-gray-600 border-t border-gray-200 pt-4" >
< p class = "text-xs text-gray-500" >
注意 : 需要配置 Laravel 调度器才能生效 。 请确保服务器已配置 cron 任务 : < br >
< code class = "bg-gray-100 px-1 rounded" > * * * * * cd / path / to / project && php artisan schedule : run >> / d e v / n u l l 2 > & 1 < / c o d e >
< / p >
< p class = "text-xs text-gray-500 mt-2" >
同时需要启动队列处理器来执行后台任务 : < br >
< code class = "bg-gray-100 px-1 rounded" > php artisan queue : work < / code >
< / p >
< / div >
< / div >
< / div >
< / div >
< / div >
< / template >
< script >
export default {
name : 'LogAnalysis' ,
props : {
isAdmin : {
type : Boolean ,
default : false
}
} ,
data ( ) {
return {
activeTab : 'results' ,
loading : false ,
analyzing : false ,
error : '' ,
queryParams : {
from : this . getDefaultFrom ( ) ,
to : this . getDefaultTo ( ) ,
query : '' ,
mode : 'logs' ,
logstores : [ ]
} ,
availableLogstores : [ ] ,
config : {
sls _configured : false ,
ai _configured : false ,
ai _providers : { } ,
active _ai _provider : null ,
app _env _map : { } ,
settings : {
schedule _enabled : false
}
} ,
queryResult : null ,
analysisResult : null ,
historyReports : [ ] ,
historyLoading : false ,
newProvider : {
key : '' ,
name : '' ,
endpoint : '' ,
api _key : '' ,
model : 'gpt-4o' ,
temperature : 0.3 ,
timeout : 120 ,
max _tokens : 4096 ,
enabled : true
} ,
testingAi : false ,
aiTestResult : null
} ;
} ,
async mounted ( ) {
await this . loadConfig ( ) ;
await this . loadLogstores ( ) ;
} ,
watch : {
activeTab ( newTab ) {
if ( newTab === 'history' && ! this . historyReports . length ) {
this . loadHistory ( ) ;
}
}
} ,
methods : {
// 格式化日期为本地时间字符串 (YYYY-MM-DDTHH:mm)
formatLocalDateTime ( date ) {
const year = date . getFullYear ( ) ;
const month = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
const day = String ( date . getDate ( ) ) . padStart ( 2 , '0' ) ;
const hours = String ( date . getHours ( ) ) . padStart ( 2 , '0' ) ;
const minutes = String ( date . getMinutes ( ) ) . padStart ( 2 , '0' ) ;
return ` ${ year } - ${ month } - ${ day } T ${ hours } : ${ minutes } ` ;
} ,
getDefaultFrom ( ) {
const date = new Date ( ) ;
date . setHours ( date . getHours ( ) - 1 ) ;
return this . formatLocalDateTime ( date ) ;
} ,
getDefaultTo ( ) {
return this . formatLocalDateTime ( new Date ( ) ) ;
} ,
setQuickTime ( period ) {
const now = new Date ( ) ;
const from = new Date ( ) ;
if ( period === '1h' ) from . setHours ( now . getHours ( ) - 1 ) ;
else if ( period === '6h' ) from . setHours ( now . getHours ( ) - 6 ) ;
else if ( period === '24h' ) from . setHours ( now . getHours ( ) - 24 ) ;
this . queryParams . from = this . formatLocalDateTime ( from ) ;
this . queryParams . to = this . formatLocalDateTime ( now ) ;
} ,
async loadConfig ( ) {
try {
const response = await fetch ( '/api/log-analysis/config' ) ;
const data = await response . json ( ) ;
if ( data . success ) {
this . config = data . data ;
}
} catch ( error ) {
console . error ( 'Failed to load config:' , error ) ;
}
} ,
async loadLogstores ( ) {
try {
const response = await fetch ( '/api/log-analysis/logstores' ) ;
const data = await response . json ( ) ;
if ( data . success ) {
this . availableLogstores = data . data . logstores ;
// 默认选中所有 logstore
this . queryParams . logstores = [ ... this . availableLogstores ] ;
}
} catch ( error ) {
console . error ( 'Failed to load logstores:' , error ) ;
}
} ,
async handleQuery ( ) {
this . loading = true ;
this . error = '' ;
this . queryResult = null ;
try {
const response = await fetch ( '/api/log-analysis/query' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( this . queryParams )
} ) ;
const data = await response . json ( ) ;
if ( data . success ) {
this . queryResult = data . data ;
this . activeTab = 'results' ;
} else {
this . error = data . message ;
}
} catch ( error ) {
this . error = error . message ;
} finally {
this . loading = false ;
}
} ,
async handleAnalyze ( ) {
this . analyzing = true ;
this . error = '' ;
try {
const response = await fetch ( '/api/log-analysis/analyze' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( this . queryParams )
} ) ;
const data = await response . json ( ) ;
if ( data . success ) {
// 切换到历史报告 tab 并刷新列表
this . activeTab = 'history' ;
await this . loadHistory ( ) ;
// 自动查看刚创建的报告
if ( data . data ? . report _id ) {
await this . viewReport ( data . data . report _id ) ;
}
} else {
this . error = data . message ;
}
} catch ( error ) {
this . error = error . message ;
} finally {
this . analyzing = false ;
}
} ,
async loadHistory ( ) {
this . historyLoading = true ;
try {
const response = await fetch ( '/api/log-analysis/reports' ) ;
const data = await response . json ( ) ;
if ( data . success ) {
this . historyReports = data . data . reports ;
}
} catch ( error ) {
console . error ( 'Failed to load history:' , error ) ;
} finally {
this . historyLoading = false ;
}
} ,
async viewReport ( id ) {
try {
const response = await fetch ( ` /api/log-analysis/reports/ ${ id } ` ) ;
const data = await response . json ( ) ;
if ( data . success ) {
this . analysisResult = data . data ;
this . activeTab = 'analysis' ;
}
} catch ( error ) {
this . error = error . message ;
}
} ,
async setActiveProvider ( key ) {
try {
const response = await fetch ( '/api/log-analysis/config' , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { active _ai _provider : key } )
} ) ;
const data = await response . json ( ) ;
if ( data . success ) {
await this . loadConfig ( ) ;
} else {
this . error = data . message ;
}
} catch ( error ) {
this . error = error . message ;
}
} ,
async addProvider ( ) {
const providers = { ... this . config . ai _providers } ;
providers [ this . newProvider . key ] = {
name : this . newProvider . name ,
endpoint : this . newProvider . endpoint ,
api _key : this . newProvider . api _key ,
model : this . newProvider . model ,
temperature : this . newProvider . temperature ,
timeout : this . newProvider . timeout ,
max _tokens : this . newProvider . max _tokens ,
enabled : true
} ;
try {
const response = await fetch ( '/api/log-analysis/config' , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { ai _providers : providers } )
} ) ;
const data = await response . json ( ) ;
if ( data . success ) {
await this . loadConfig ( ) ;
this . newProvider = {
key : '' , name : '' , endpoint : '' , api _key : '' ,
model : 'gpt-4o' , temperature : 0.3 , timeout : 120 , max _tokens : 4096 , enabled : true
} ;
} else {
this . error = data . message ;
}
} catch ( error ) {
this . error = error . message ;
}
} ,
async deleteProvider ( key ) {
if ( ! confirm ( ` 确定删除提供商 " ${ key } " 吗? ` ) ) return ;
const providers = { ... this . config . ai _providers } ;
delete providers [ key ] ;
try {
const response = await fetch ( '/api/log-analysis/config' , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { ai _providers : providers } )
} ) ;
const result = await response . json ( ) ;
if ( result . success ) {
await this . loadConfig ( ) ;
}
} catch ( error ) {
this . error = error . message ;
}
} ,
async testAiConnection ( ) {
this . testingAi = true ;
this . aiTestResult = null ;
try {
const response = await fetch ( '/api/log-analysis/test-ai' ) ;
this . aiTestResult = await response . json ( ) ;
} catch ( error ) {
this . aiTestResult = { success : false , message : error . message } ;
} finally {
this . testingAi = false ;
}
} ,
async updateScheduleSettings ( ) {
try {
const settings = { ... this . config . settings } ;
const response = await fetch ( '/api/log-analysis/config' , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { settings } )
} ) ;
const data = await response . json ( ) ;
if ( ! data . success ) {
this . error = data . message ;
// Revert the toggle
this . config . settings . schedule _enabled = ! this . config . settings . schedule _enabled ;
}
} catch ( error ) {
this . error = error . message ;
// Revert the toggle
this . config . settings . schedule _enabled = ! this . config . settings . schedule _enabled ;
}
} ,
getLevelClass ( level ) {
const classes = {
'ERROR' : 'text-red-600' ,
'WARN' : 'text-yellow-600' ,
'WARNING' : 'text-yellow-600' ,
'INFO' : 'text-blue-600' ,
'DEBUG' : 'text-gray-600'
} ;
return classes [ level ? . toUpperCase ( ) ] || 'text-gray-600' ;
} ,
getLevelBadgeClass ( level ) {
const classes = {
'ERROR' : 'bg-red-100 text-red-700' ,
'WARN' : 'bg-yellow-100 text-yellow-700' ,
'WARNING' : 'bg-yellow-100 text-yellow-700' ,
'INFO' : 'bg-blue-100 text-blue-700' ,
'DEBUG' : 'bg-gray-100 text-gray-700'
} ;
return classes [ level ? . toUpperCase ( ) ] || 'bg-gray-100 text-gray-700' ;
} ,
getImpactClass ( impact ) {
const classes = {
'high' : 'bg-red-100 text-red-700' ,
'medium' : 'bg-yellow-100 text-yellow-700' ,
'low' : 'bg-green-100 text-green-700'
} ;
return classes [ impact ] || 'bg-gray-100 text-gray-700' ;
} ,
getAnomalyBorderClass ( type ) {
const classes = {
'error' : 'border-red-500' ,
'warning' : 'border-yellow-500' ,
'performance' : 'border-orange-500' ,
'security' : 'border-purple-500'
} ;
return classes [ type ] || 'border-gray-500' ;
} ,
getStatusClass ( status ) {
const classes = {
'pending' : 'bg-yellow-100 text-yellow-700' ,
'completed' : 'bg-green-100 text-green-700' ,
'failed' : 'bg-red-100 text-red-700'
} ;
return classes [ status ] || 'bg-gray-100 text-gray-700' ;
} ,
getStatusText ( status ) {
const texts = {
'pending' : '处理中' ,
'completed' : '已完成' ,
'failed' : '失败'
} ;
return texts [ status ] || status ;
} ,
async refreshReport ( id ) {
try {
const response = await fetch ( ` /api/log-analysis/reports/ ${ id } ` ) ;
const data = await response . json ( ) ;
if ( data . success ) {
this . analysisResult = data . data ;
// 如果还在处理中,5秒后自动刷新
if ( data . data . status === 'pending' ) {
setTimeout ( ( ) => {
if ( this . analysisResult ? . id === id && this . analysisResult ? . status === 'pending' ) {
this . refreshReport ( id ) ;
}
} , 5000 ) ;
}
}
} catch ( error ) {
this . error = error . message ;
}
}
}
} ;
< / script >