feat: AI 对话错误详情展示优化

This commit is contained in:
digua
2026-04-28 19:48:10 +08:00
committed by digua
parent 9eb20da030
commit 57455e3120
15 changed files with 606 additions and 31 deletions
+53 -1
View File
@@ -14,6 +14,7 @@ import {
type AssistantMessage as PiAssistantMessage,
type Message as PiMessage,
type Usage as PiUsage,
streamSimple,
} from '@mariozechner/pi-ai'
import type { AgentConfig, AgentStreamChunk, AgentResult, SkillContext } from './types'
@@ -104,6 +105,18 @@ export class Agent {
let debugLastLoggedCount = 0
let debugLlmRound = 1
// 捕获最后一次 LLM 调用的请求体(用于错误诊断)
let lastRequestPayload: unknown = null
const errorCapturingStreamFn: typeof streamSimple = (model, context, options) => {
return streamSimple(model, context, {
...options,
onPayload: (payload) => {
lastRequestPayload = payload
options?.onPayload?.(payload)
},
})
}
// 初始化 PiAgentCore
const coreAgent = new PiAgentCore({
initialState: {
@@ -111,6 +124,7 @@ export class Agent {
thinkingLevel: this.piModel.reasoning ? 'medium' : 'off',
},
getApiKey: () => this.apiKey,
streamFn: errorCapturingStreamFn,
convertToLlm: (messages) => {
const filtered = messages.filter(
(msg): msg is PiMessage => msg.role === 'user' || msg.role === 'assistant' || msg.role === 'toolResult'
@@ -199,7 +213,45 @@ export class Agent {
}
if (coreAgent.state.error) {
throw new Error(coreAgent.state.error)
const agentError = new Error(coreAgent.state.error) as Error & {
agentContext?: {
provider?: string
model?: string
api?: string
url?: string
requestBody?: string
}
}
const lastMsg = [...coreAgent.state.messages].reverse().find((m) => m.role === 'assistant') as
| (PiAssistantMessage & { provider?: string; model?: string; api?: string })
| undefined
const ctx: NonNullable<typeof agentError.agentContext> = {}
if (lastMsg) {
ctx.provider = lastMsg.provider
ctx.model = lastMsg.model
ctx.api = lastMsg.api
}
// 从 model.baseUrl 和 api 类型构造完整请求 URL
const baseUrl = (this.piModel as Record<string, unknown>).baseUrl as string | undefined
if (baseUrl) {
const apiType = lastMsg?.api || (this.piModel as Record<string, unknown>).api
const pathMap: Record<string, string> = {
'openai-completions': '/chat/completions',
'openai-responses': '/responses',
'anthropic-messages': '/messages',
}
const apiPath = typeof apiType === 'string' ? pathMap[apiType] : undefined
ctx.url = apiPath ? baseUrl.replace(/\/+$/, '') + apiPath : baseUrl
}
if (lastRequestPayload) {
try {
ctx.requestBody = JSON.stringify(lastRequestPayload, null, 2)
} catch {
// ignore
}
}
agentError.agentContext = ctx
throw agentError
}
// 提取最终回复
+6 -4
View File
@@ -2,9 +2,9 @@
* Agent 模块类型定义
*/
import type { TokenUsage, AgentRuntimeStatus } from '../../../shared/types'
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../../../shared/types'
export type { TokenUsage, AgentRuntimeStatus } from '../../../shared/types'
export type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../../../shared/types'
/**
* Agent 配置
@@ -36,8 +36,8 @@ export interface AgentStreamChunk {
toolParams?: Record<string, unknown>
/** 工具执行结果(type=tool_result 时) */
toolResult?: unknown
/** 错误信息(type=error 时) */
error?: string
/** 结构化错误信息(type=error 时) */
error?: SerializedErrorInfo
/** 是否完成 */
isFinished?: boolean
/** Token 使用量(type=done 时返回累计值) */
@@ -58,6 +58,8 @@ export interface AgentResult {
toolRounds: number
/** 总 Token 使用量(累计所有 LLM 调用) */
totalUsage?: TokenUsage
/** 结构化错误信息(请求失败时) */
error?: SerializedErrorInfo
}
/**
+168
View File
@@ -0,0 +1,168 @@
/**
* 将任意错误对象序列化为 SerializedErrorInfo
* 保留尽可能多的 HTTP / provider 上下文,用于前端详情展示和日志记录。
*/
import type { SerializedErrorInfo } from '../../shared/types'
function safeString(val: unknown): string | null {
if (val === undefined || val === null) return null
if (typeof val === 'string') return val
try {
return JSON.stringify(val, null, 2)
} catch {
return String(val)
}
}
function extractFromCandidate(candidate: unknown, info: SerializedErrorInfo): void {
if (!candidate || typeof candidate !== 'object') return
const rec = candidate as Record<string, unknown>
if (typeof rec.statusCode === 'number' && info.statusCode == null) {
info.statusCode = rec.statusCode
}
if (typeof rec.status === 'number' && info.statusCode == null) {
info.statusCode = rec.status
}
if (typeof rec.url === 'string' && !info.url) {
info.url = rec.url
}
if (typeof rec.responseBody === 'string' && !info.responseBody) {
info.responseBody = rec.responseBody
}
if (rec.responseHeaders && typeof rec.responseHeaders === 'object' && !info.responseHeaders) {
try {
const headers = rec.responseHeaders as Record<string, unknown>
const plain: Record<string, string> = {}
for (const [key, val] of Object.entries(headers)) {
plain[key] = String(val)
}
info.responseHeaders = plain
} catch {
// ignore
}
}
// OpenAI SDK: headers 在 error.headers (Headers 对象)
if (rec.headers && typeof rec.headers === 'object' && !info.responseHeaders) {
try {
const h = rec.headers
if (typeof (h as any).entries === 'function') {
const plain: Record<string, string> = {}
for (const [key, val] of (h as any).entries()) {
plain[key] = String(val)
}
if (Object.keys(plain).length > 0) {
info.responseHeaders = plain
}
}
} catch {
// ignore
}
}
if (rec.requestBodyValues !== undefined && !info.requestBody) {
info.requestBody = safeString(rec.requestBodyValues)
}
if (rec.requestBody !== undefined && !info.requestBody) {
info.requestBody = safeString(rec.requestBody)
}
// OpenAI SDK: error.error 包含 response body 中的 error 对象
if (rec.error !== undefined && typeof rec.error === 'object' && !info.responseBody) {
info.responseBody = safeString(rec.error)
}
if (rec.data !== undefined && !info.responseBody) {
info.responseBody = safeString(rec.data)
}
}
/**
* 从错误消息字符串中尝试解析 HTTP 状态码。
* OpenAI SDK 的 APIError.message 通常以 "NNN " 开头,如 "401 Incorrect API key..."
*/
function parseStatusCodeFromMessage(message: string | null): number | null {
if (!message) return null
const match = message.match(/^(\d{3})\s/)
if (match) {
const code = parseInt(match[1], 10)
if (code >= 100 && code < 600) return code
}
return null
}
export function serializeError(error: unknown, provider?: string): SerializedErrorInfo {
const info: SerializedErrorInfo = {
name: null,
message: null,
stack: null,
}
if (provider) {
info.provider = provider
}
if (!error) {
info.message = 'Unknown error'
return info
}
if (typeof error === 'string') {
info.message = error
info.statusCode = parseStatusCodeFromMessage(error)
return info
}
if (!(typeof error === 'object')) {
info.message = String(error)
return info
}
const err = error as Record<string, unknown>
if (typeof err.name === 'string') info.name = err.name
if (typeof err.message === 'string') info.message = err.message
if (typeof err.stack === 'string') info.stack = err.stack
if (err.cause !== undefined) {
info.cause = safeString(err.cause)
}
// Agent 层附加的上下文(provider / model / url / requestBody
if (err.agentContext && typeof err.agentContext === 'object') {
const ctx = err.agentContext as Record<string, unknown>
if (typeof ctx.provider === 'string' && !info.provider) {
info.provider = ctx.provider
}
if (typeof ctx.url === 'string' && !info.url) {
info.url = ctx.url
}
if (typeof ctx.requestBody === 'string' && !info.requestBody) {
info.requestBody = ctx.requestBody
}
}
// 从主错误对象及其嵌套 lastError / errors / cause 中提取 HTTP 上下文
const candidates: unknown[] = [error]
if (err.lastError) candidates.push(err.lastError)
if (Array.isArray(err.errors)) candidates.push(...err.errors)
if (err.cause && typeof err.cause === 'object') candidates.push(err.cause)
for (const candidate of candidates) {
extractFromCandidate(candidate, info)
}
// 如果没有从属性中提取到 statusCode,尝试从 message 字符串解析
if (info.statusCode == null) {
info.statusCode = parseStatusCodeFromMessage(info.message)
}
return info
}
+9 -7
View File
@@ -6,6 +6,7 @@ import * as aiConversations from '../ai/conversations'
import * as llm from '../ai/llm'
import * as rag from '../ai/rag'
import { aiLogger, setDebugMode } from '../ai/logger'
import { serializeError } from '../ai/serialize-error'
import { getLogsDir } from '../paths'
import { Agent, type AgentStreamChunk, type SkillContext } from '../ai/agent'
import { getDefaultGeneralAssistantId } from '../ai/assistant/defaultGeneral'
@@ -1166,15 +1167,16 @@ export function registerAIHandlers({ win }: IpcContext): void {
})
return
}
const friendlyError = formatAIError(error, activeAIConfig.provider)
aiLogger.error('IPC', `Agent execution error: ${requestId}`, {
error: String(error),
friendlyError,
})
const serializedError = serializeError(error, activeAIConfig.provider)
serializedError.friendlyMessage = formatAIError(error, activeAIConfig.provider)
if (!serializedError.url && activeAIConfig.baseUrl) {
serializedError.url = activeAIConfig.baseUrl
}
aiLogger.error('IPC', `Agent execution error: ${requestId}`, serializedError)
// 发送错误 chunk
win.webContents.send('agent:streamChunk', {
requestId,
chunk: { type: 'error', error: friendlyError, isFinished: true },
chunk: { type: 'error', error: serializedError, isFinished: true },
})
// 发送完成事件(带错误信息),确保前端 promise 能 resolve
win.webContents.send('agent:complete', {
@@ -1184,7 +1186,7 @@ export function registerAIHandlers({ win }: IpcContext): void {
toolsUsed: [],
toolRounds: 0,
totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
error: friendlyError,
error: serializedError,
},
})
} finally {
+19 -6
View File
@@ -79,7 +79,9 @@ export interface ChatStreamChunk {
// Agent API 类型 — 从 shared/types 统一导入
export type { TokenUsage, AgentRuntimeStatus } from '../../shared/types'
import type { TokenUsage, AgentRuntimeStatus } from '../../shared/types'
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../../shared/types'
export type { SerializedErrorInfo } from '../../shared/types'
export interface AgentStreamChunk {
type: 'content' | 'think' | 'tool_start' | 'tool_result' | 'status' | 'done' | 'error'
@@ -90,7 +92,7 @@ export interface AgentStreamChunk {
toolParams?: Record<string, unknown>
toolResult?: unknown
status?: AgentRuntimeStatus
error?: string
error?: SerializedErrorInfo
isFinished?: boolean
/** Token 使用量(type=done 时返回累计值) */
usage?: TokenUsage
@@ -100,6 +102,7 @@ export interface AgentResult {
content: string
toolsUsed: string[]
toolRounds: number
error?: SerializedErrorInfo
}
/** 单条脱敏规则 */
@@ -907,7 +910,10 @@ export const agentApi = {
assistantId?: string,
skillId?: string | null,
enableAutoSkill?: boolean
): { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: string }> } => {
): {
requestId: string
promise: Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }>
} => {
// 防御性处理:确保传给 IPC 的 context 是“可结构化克隆”的纯对象
// 避免调用方误传入响应式 Proxy(例如 Pinia/Vue state)导致 invoke 失败
const sanitizedContext: ToolContext = {
@@ -949,7 +955,7 @@ export const agentApi = {
chatType ?? 'group'
)
const promise = new Promise<{ success: boolean; result?: AgentResult; error?: string }>((resolve) => {
const promise = new Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }>((resolve) => {
// 监听流式 chunks
const chunkHandler = (
_event: Electron.IpcRendererEvent,
@@ -965,7 +971,7 @@ export const agentApi = {
// 监听完成事件
const completeHandler = (
_event: Electron.IpcRendererEvent,
data: { requestId: string; result: AgentResult & { error?: string } }
data: { requestId: string; result: AgentResult & { error?: SerializedErrorInfo } }
) => {
if (data.requestId === requestId) {
console.log('[preload] Agent 完成,requestId:', requestId, 'hasError:', !!data.result?.error)
@@ -1009,7 +1015,14 @@ export const agentApi = {
console.error('[preload] Agent invoke 错误:', error)
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
ipcRenderer.removeListener('agent:complete', completeHandler)
resolve({ success: false, error: String(error) })
resolve({
success: false,
error: {
name: error instanceof Error ? error.name : null,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? (error.stack ?? null) : null,
},
})
})
})
+5 -3
View File
@@ -1,6 +1,6 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import type { AnalysisSession, MessageType, ImportProgress, ExportProgress } from '../../src/types/base'
import type { TokenUsage, AgentRuntimeStatus } from '../shared/types'
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../shared/types'
import type {
MemberActivity,
MemberNameHistory,
@@ -672,7 +672,7 @@ interface AgentStreamChunk {
toolParams?: Record<string, unknown>
toolResult?: unknown
status?: AgentRuntimeStatus
error?: string
error?: SerializedErrorInfo
isFinished?: boolean
/** Token 使用量(type=done 时返回累计值) */
usage?: TokenUsage
@@ -684,6 +684,7 @@ interface AgentResult {
toolRounds: number
/** 总 Token 使用量(累计所有 LLM 调用) */
totalUsage?: TokenUsage
error?: SerializedErrorInfo
}
/** Owner 信息(当前用户在对话中的身份) */
@@ -768,7 +769,7 @@ interface AgentApi {
assistantId?: string,
skillId?: string | null,
enableAutoSkill?: boolean
) => { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: string }> }
) => { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }> }
abort: (requestId: string) => Promise<{ success: boolean; error?: string }>
}
@@ -1213,6 +1214,7 @@ export {
AgentStreamChunk,
AgentRuntimeStatus,
AgentResult,
SerializedErrorInfo,
ToolContext,
DesensitizeRule,
PreprocessConfig,
+19
View File
@@ -5,6 +5,25 @@
* 所有使用方应从此处导入,避免重复定义导致类型漂移。
*/
/**
* 序列化后的结构化错误信息,跨进程传输 & 持久化存储。
* 所有字段可选——仅在原始错误中存在时才填充。
*/
export interface SerializedErrorInfo {
name: string | null
message: string | null
stack: string | null
statusCode?: number | null
url?: string | null
responseBody?: string | null
responseHeaders?: Record<string, string> | null
requestBody?: string | null
cause?: string | null
provider?: string | null
/** formatAIError 生成的用户友好摘要 */
friendlyMessage?: string | null
}
export interface TokenUsage {
promptTokens: number
completionTokens: number
@@ -5,6 +5,7 @@ import dayjs from 'dayjs'
import MarkdownIt from 'markdown-it'
import type { ContentBlock, ToolBlockContent } from '@/composables/useAIChat'
import CaptureButton from '@/components/common/CaptureButton.vue'
import ErrorBlock from './ErrorBlock.vue'
import { useToast } from '@/composables/useToast'
const { t, te, locale } = useI18n()
@@ -406,6 +407,9 @@ async function handleCopyMarkdown() {
</span>
</div>
</div>
<!-- 错误块 -->
<ErrorBlock v-else-if="block.type === 'error'" :error="block.error" />
</template>
<!-- 流式处理中指示器当最后一个块是已完成的工具块时显示 -->
+53
View File
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { SerializedErrorInfo } from '@electron/shared/types'
import ErrorDetailModal from './ErrorDetailModal.vue'
const { t } = useI18n()
defineProps<{
error: SerializedErrorInfo
}>()
const showDetail = ref(false)
function getDisplayMessage(error: SerializedErrorInfo): string {
return error.friendlyMessage || error.message || t('ai.chat.error.unknown')
}
</script>
<template>
<div
class="group relative my-2 w-full min-w-[320px] cursor-pointer rounded-lg border border-red-200 bg-red-50/60 px-3.5 py-3 text-[13px] transition-all duration-200 hover:border-red-300 hover:bg-red-50 dark:border-red-900/30 dark:bg-red-950/20 dark:hover:border-red-800/50 dark:hover:bg-red-950/30"
@click="showDetail = true"
>
<!-- Header -->
<div class="mb-1.5 flex items-center gap-2">
<div class="flex shrink-0 items-center justify-center text-red-500 dark:text-red-400">
<UIcon name="i-heroicons-exclamation-triangle" class="h-4 w-4" />
</div>
<div class="pr-5 text-[13px] font-semibold leading-[1.4] text-red-600 dark:text-red-400">
{{ t('ai.chat.error.title') }}
</div>
</div>
<!-- Description -->
<div class="ml-6 line-clamp-3 break-words text-xs leading-normal text-gray-600 dark:text-gray-400">
{{ getDisplayMessage(error) }}
</div>
<!-- Footer -->
<div class="mt-2.5 ml-6 flex items-center">
<div
class="ml-auto inline-flex items-center gap-0.5 text-xs text-gray-400 transition-colors duration-150 group-hover:text-red-500 dark:text-gray-500 dark:group-hover:text-red-400"
>
{{ t('ai.chat.error.viewDetail') }}
<UIcon name="i-heroicons-chevron-right" class="h-3.5 w-3.5" />
</div>
</div>
</div>
<!-- Error Detail Modal -->
<ErrorDetailModal v-model:open="showDetail" :error="error" />
</template>
@@ -0,0 +1,168 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from '@/composables/useToast'
import type { SerializedErrorInfo } from '@electron/shared/types'
const { t } = useI18n()
const toast = useToast()
const open = defineModel<boolean>('open', { required: true })
const props = defineProps<{
error: SerializedErrorInfo
}>()
interface DetailField {
label: string
value: string
isCode?: boolean
isStack?: boolean
}
const fields = computed<DetailField[]>(() => {
const e = props.error
const list: DetailField[] = []
if (e.url) {
list.push({ label: t('ai.chat.error.requestUrl'), value: e.url })
}
if (e.statusCode != null) {
list.push({ label: t('ai.chat.error.statusCode'), value: String(e.statusCode) })
}
if (e.responseBody) {
list.push({ label: t('ai.chat.error.responseBody'), value: formatJson(e.responseBody), isCode: true })
}
if (e.responseHeaders) {
list.push({
label: t('ai.chat.error.responseHeaders'),
value: JSON.stringify(e.responseHeaders, null, 2),
isCode: true,
})
}
if (e.name) {
list.push({ label: t('ai.chat.error.name'), value: e.name })
}
if (e.message) {
list.push({ label: t('ai.chat.error.message'), value: e.message })
}
if (e.stack) {
list.push({ label: t('ai.chat.error.stack'), value: e.stack, isStack: true })
}
if (e.cause) {
list.push({ label: t('ai.chat.error.cause'), value: formatJson(e.cause), isCode: true })
}
if (e.provider) {
list.push({ label: t('ai.chat.error.provider'), value: e.provider })
}
if (e.requestBody) {
list.push({ label: t('ai.chat.error.requestBody'), value: formatJson(e.requestBody), isCode: true })
}
return list
})
function formatJson(raw: string): string {
try {
return JSON.stringify(JSON.parse(raw), null, 2)
} catch {
return raw
}
}
function handleCopyAll() {
const text = fields.value.map((f) => `${f.label}: ${f.value}`).join('\n\n')
navigator.clipboard.writeText(text).then(() => {
toast.success(t('ai.chat.error.copied'))
})
}
</script>
<template>
<UModal
v-model:open="open"
:ui="{
content: 'sm:max-w-[700px] z-[100]',
overlay: 'backdrop-blur-sm z-[99]',
}"
>
<template #content>
<div class="flex max-h-[80vh] flex-col overflow-hidden">
<!-- Header -->
<div class="shrink-0 border-b border-gray-200 px-6 py-4 dark:border-gray-800">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-red-100 dark:bg-red-900/40">
<UIcon name="i-heroicons-exclamation-triangle" class="h-4 w-4 text-red-600 dark:text-red-400" />
</div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t('ai.chat.error.detail') }}
</h2>
</div>
<UButton icon="i-heroicons-x-mark" variant="ghost" color="neutral" size="sm" @click="open = false" />
</div>
</div>
<!-- Content -->
<div class="flex-1 overflow-y-auto px-6 py-4">
<div class="flex flex-col gap-4">
<div v-for="(field, idx) in fields" :key="idx" class="flex flex-col gap-1.5">
<div class="text-sm font-semibold text-gray-700 dark:text-gray-300">
{{ field.label }}
</div>
<!-- Stack trace: red mono -->
<div
v-if="field.isStack"
class="rounded-md border border-red-200 bg-red-50/50 p-3 dark:border-red-900/40 dark:bg-red-950/20"
>
<pre
class="m-0 whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-red-600 dark:text-red-400"
>{{ field.value }}</pre
>
</div>
<!-- Code block -->
<div
v-else-if="field.isCode"
class="rounded-md border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900"
>
<pre
class="m-0 max-h-[300px] overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-gray-700 dark:text-gray-300"
>{{ field.value }}</pre
>
</div>
<!-- Plain text -->
<div
v-else
class="select-text break-words rounded-md border border-gray-200 bg-gray-50 px-3 py-2 font-mono text-xs text-gray-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300"
>
{{ field.value }}
</div>
</div>
<div v-if="fields.length === 0" class="py-8 text-center text-sm text-gray-400 dark:text-gray-500">
{{ t('ai.chat.error.unknown') }}
</div>
</div>
</div>
<!-- Footer -->
<div class="shrink-0 border-t border-gray-200 px-6 py-3 dark:border-gray-800">
<div class="flex justify-end">
<UButton
icon="i-heroicons-document-duplicate"
variant="soft"
color="neutral"
size="sm"
@click="handleCopyAll"
>
{{ t('ai.chat.error.copyAll') }}
</UButton>
</div>
</div>
</div>
</template>
</UModal>
</template>
+18
View File
@@ -193,6 +193,24 @@
"noMessages": "No messages to export"
}
},
"error": {
"title": "Processing Failed",
"detail": "Error Details",
"viewDetail": "View Details",
"requestUrl": "Request URL",
"statusCode": "Status Code",
"responseBody": "Response Body",
"responseHeaders": "Response Headers",
"requestBody": "Request Body",
"name": "Error Name",
"message": "Error Message",
"stack": "Stack Trace",
"cause": "Error Cause",
"provider": "Provider",
"copyAll": "Copy All",
"copied": "Copied to clipboard",
"unknown": "Unknown error"
},
"dataSource": {
"title": "Data Source",
"keywords": "Keywords:",
+18
View File
@@ -193,6 +193,24 @@
"noMessages": "会話にメッセージがありません"
}
},
"error": {
"title": "処理に失敗しました",
"detail": "エラー詳細",
"viewDetail": "詳細を表示",
"requestUrl": "リクエストURL",
"statusCode": "ステータスコード",
"responseBody": "レスポンスボディ",
"responseHeaders": "レスポンスヘッダー",
"requestBody": "リクエストボディ",
"name": "エラー名",
"message": "エラーメッセージ",
"stack": "スタックトレース",
"cause": "エラー原因",
"provider": "プロバイダー",
"copyAll": "すべてコピー",
"copied": "クリップボードにコピーしました",
"unknown": "不明なエラー"
},
"dataSource": {
"title": "データソース",
"keywords": "キーワード:",
+18
View File
@@ -193,6 +193,24 @@
"noMessages": "对话没有消息"
}
},
"error": {
"title": "处理失败",
"detail": "错误详情",
"viewDetail": "查看详情",
"requestUrl": "请求路径",
"statusCode": "状态码",
"responseBody": "响应内容",
"responseHeaders": "响应头",
"requestBody": "请求体",
"name": "错误名称",
"message": "错误信息",
"stack": "堆栈信息",
"cause": "错误原因",
"provider": "服务商",
"copyAll": "复制全部",
"copied": "已复制到剪贴板",
"unknown": "未知错误"
},
"dataSource": {
"title": "数据源",
"keywords": "关键词:",
+18
View File
@@ -193,6 +193,24 @@
"noMessages": "對話沒有訊息"
}
},
"error": {
"title": "處理失敗",
"detail": "錯誤詳情",
"viewDetail": "查看詳情",
"requestUrl": "請求路徑",
"statusCode": "狀態碼",
"responseBody": "回應內容",
"responseHeaders": "回應標頭",
"requestBody": "請求主體",
"name": "錯誤名稱",
"message": "錯誤訊息",
"stack": "堆疊資訊",
"cause": "錯誤原因",
"provider": "服務商",
"copyAll": "複製全部",
"copied": "已複製到剪貼簿",
"unknown": "未知錯誤"
},
"dataSource": {
"title": "資料來源",
"keywords": "關鍵字:",
+30 -10
View File
@@ -12,7 +12,7 @@ import { useSessionStore } from '@/stores/session'
import { useSettingsStore } from '@/stores/settings'
import { useAssistantStore } from '@/stores/assistant'
import { useSkillStore } from '@/stores/skill'
import type { TokenUsage, AgentRuntimeStatus } from '@electron/shared/types'
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '@electron/shared/types'
// 工具调用记录
export interface ToolCallRecord {
@@ -49,6 +49,7 @@ export type ContentBlock =
tool: ToolBlockContent
}
| { type: 'skill'; skillId: string; skillName: string }
| { type: 'error'; error: SerializedErrorInfo }
// 消息类型
export interface ChatMessage {
@@ -867,8 +868,12 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
}
if (!hasStreamError) {
hasStreamError = true
appendTextToBlocks(`\n\n❌ 处理失败:${chunk.error || '未知错误'}`)
updateAIMessage({ isStreaming: false })
const blocks = targetBuffer.messages[aiMessageIndex].contentBlocks || []
blocks.push({
type: 'error',
error: chunk.error || { name: null, message: '未知错误', stack: null },
})
updateAIMessage({ contentBlocks: [...blocks], isStreaming: false })
}
setAgentPhase(state, 'error')
break
@@ -908,11 +913,19 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
await saveConversation(resolvedConversationId, userMessage, targetBuffer.messages[aiMessageIndex])
} else if (!hasStreamError) {
appendTextToBlocks(`\n\n❌ 处理失败:${result.error || '未知错误'}`)
const blocks = targetBuffer.messages[aiMessageIndex].contentBlocks || []
blocks.push({
type: 'error',
error: result.error || { name: null, message: '未知错误', stack: null },
})
targetBuffer.messages[aiMessageIndex] = {
...targetBuffer.messages[aiMessageIndex],
contentBlocks: [...blocks],
isStreaming: false,
}
await saveConversation(resolvedConversationId, userMessage, targetBuffer.messages[aiMessageIndex])
} else {
await saveConversation(resolvedConversationId, userMessage, targetBuffer.messages[aiMessageIndex])
}
return { success: true }
@@ -922,13 +935,20 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
const lastMessage = targetBuffer.messages[targetBuffer.messages.length - 1]
if (lastMessage && lastMessage.role === 'assistant') {
lastMessage.content = `❌ 处理失败:${error instanceof Error ? error.message : '未知错误'}
请检查:
- 网络连接是否正常
- API Key 是否有效
- 配置是否正确`
const errInfo: SerializedErrorInfo = {
name: error instanceof Error ? error.name : null,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? (error.stack ?? null) : null,
}
const blocks = lastMessage.contentBlocks || []
blocks.push({ type: 'error', error: errInfo })
lastMessage.contentBlocks = [...blocks]
lastMessage.isStreaming = false
const userMsg = targetBuffer.messages.find((m) => m.role === 'user')
if (userMsg) {
await saveConversation(resolvedConversationId, userMsg, lastMessage)
}
}
return { success: false, reason: 'error' }