This commit is contained in:
PhoenixCPH 2026-05-25 00:07:45 +02:00 committed by GitHub
commit 6f209ecb3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 310 additions and 54 deletions

View File

@ -10,10 +10,12 @@ Zep检索工具服务
import time import time
import json import json
import re
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field from dataclasses import dataclass, field
from zep_cloud.client import Zep from zep_cloud.client import Zep
from zep_cloud.core.api_error import ApiError
from ..config import Config from ..config import Config
from ..utils.logger import get_logger from ..utils.logger import get_logger
@ -23,6 +25,55 @@ from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
logger = get_logger('mirofish.zep_tools') logger = get_logger('mirofish.zep_tools')
_MAX_RATE_LIMIT_DELAY = 60.0
def _is_rate_limit_error(error: Exception) -> bool:
status_code = getattr(error, "status_code", None)
if status_code == 429:
return True
body = getattr(error, "body", None)
if isinstance(body, str) and "rate limit" in body.lower():
return True
text = str(error).lower()
return "status_code: 429" in text or "rate limit" in text or "too many requests" in text
def _parse_retry_after(error: Exception) -> float | None:
headers = getattr(error, "headers", None)
if isinstance(headers, dict):
retry_after = headers.get("retry-after") or headers.get("Retry-After")
if retry_after:
try:
return max(float(retry_after), 0.0)
except (TypeError, ValueError):
pass
reset = headers.get("x-ratelimit-reset") or headers.get("X-RateLimit-Reset")
if reset:
try:
reset_seconds = float(reset)
if reset_seconds > 1_000_000_000:
wait_seconds = reset_seconds - time.time()
else:
wait_seconds = reset_seconds
if wait_seconds > 0:
return wait_seconds
except (TypeError, ValueError):
pass
text = str(error)
match = re.search(r"retry-after['\"]?\s*:\s*['\"]?(\d+(?:\.\d+)?)", text, re.IGNORECASE)
if match:
try:
return max(float(match.group(1)), 0.0)
except (TypeError, ValueError):
pass
return None
@dataclass @dataclass
class SearchResult: class SearchResult:
@ -448,14 +499,32 @@ class ZepToolsService:
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
return func() return func()
except Exception as e: except (Exception, ApiError) as e:
last_exception = e last_exception = e
if attempt < max_retries - 1: if attempt < max_retries - 1:
logger.warning( if _is_rate_limit_error(e):
t("console.zepRetryAttempt", operation=operation_name, attempt=attempt + 1, error=str(e)[:100], delay=f"{delay:.1f}") retry_after = _parse_retry_after(e)
) if retry_after is not None:
delay = min(max(retry_after, self.RETRY_DELAY), _MAX_RATE_LIMIT_DELAY)
else:
delay = min(max(delay, self.RETRY_DELAY), _MAX_RATE_LIMIT_DELAY)
else:
delay = min(delay, _MAX_RATE_LIMIT_DELAY)
if _is_rate_limit_error(e):
logger.warning(
f"Zep {operation_name} rate limit hit (attempt {attempt + 1}/{max_retries}); "
f"retrying in {delay:.1f}s..."
)
else:
logger.warning(
t("console.zepRetryAttempt", operation=operation_name, attempt=attempt + 1, error=str(e)[:100], delay=f"{delay:.1f}")
)
time.sleep(delay) time.sleep(delay)
delay *= 2 if _is_rate_limit_error(e):
delay = min(delay * 1.25, _MAX_RATE_LIMIT_DELAY)
else:
delay = min(delay * 2, _MAX_RATE_LIMIT_DELAY)
else: else:
logger.error(t("console.zepAllRetriesFailed", operation=operation_name, retries=max_retries, error=str(e))) logger.error(t("console.zepAllRetriesFailed", operation=operation_name, retries=max_retries, error=str(e)))

View File

@ -7,10 +7,12 @@ Zep 的 node/edge 列表接口使用 UUID cursor 分页,
from __future__ import annotations from __future__ import annotations
import time import time
import re
from collections.abc import Callable from collections.abc import Callable
from typing import Any from typing import Any
from zep_cloud import InternalServerError from zep_cloud import InternalServerError
from zep_cloud.core.api_error import ApiError
from zep_cloud.client import Zep from zep_cloud.client import Zep
from .logger import get_logger from .logger import get_logger
@ -19,8 +21,57 @@ logger = get_logger('mirofish.zep_paging')
_DEFAULT_PAGE_SIZE = 100 _DEFAULT_PAGE_SIZE = 100
_MAX_NODES = 2000 _MAX_NODES = 2000
_DEFAULT_MAX_RETRIES = 3 _DEFAULT_MAX_RETRIES = 5
_DEFAULT_RETRY_DELAY = 2.0 # seconds, doubles each retry _DEFAULT_RETRY_DELAY = 2.0 # seconds, doubles each retry
_MAX_RETRY_DELAY = 60.0
def _is_rate_limit_error(error: Exception) -> bool:
status_code = getattr(error, "status_code", None)
if status_code == 429:
return True
body = getattr(error, "body", None)
if isinstance(body, str) and "rate limit" in body.lower():
return True
text = str(error).lower()
return "status_code: 429" in text or "rate limit" in text or "too many requests" in text
def _parse_retry_after(error: Exception) -> float | None:
headers = getattr(error, "headers", None)
if isinstance(headers, dict):
retry_after = headers.get("retry-after") or headers.get("Retry-After")
if retry_after:
try:
return max(float(retry_after), 0.0)
except (TypeError, ValueError):
pass
reset = headers.get("x-ratelimit-reset") or headers.get("X-RateLimit-Reset")
if reset:
try:
reset_seconds = float(reset)
if reset_seconds > 1_000_000_000:
# Some providers return a unix timestamp instead of a delta.
wait_seconds = reset_seconds - time.time()
else:
wait_seconds = reset_seconds
if wait_seconds > 0:
return wait_seconds
except (TypeError, ValueError):
pass
text = str(error)
match = re.search(r"retry-after['\"]?\s*:\s*['\"]?(\d+(?:\.\d+)?)", text, re.IGNORECASE)
if match:
try:
return max(float(match.group(1)), 0.0)
except (TypeError, ValueError):
pass
return None
def _fetch_page_with_retry( def _fetch_page_with_retry(
@ -41,14 +92,33 @@ def _fetch_page_with_retry(
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
return api_call(*args, **kwargs) return api_call(*args, **kwargs)
except (ConnectionError, TimeoutError, OSError, InternalServerError) as e: except (ConnectionError, TimeoutError, OSError, InternalServerError, ApiError) as e:
last_exception = e last_exception = e
if attempt < max_retries - 1: if attempt < max_retries - 1:
logger.warning( if _is_rate_limit_error(e):
f"Zep {page_description} attempt {attempt + 1} failed: {str(e)[:100]}, retrying in {delay:.1f}s..." retry_after = _parse_retry_after(e)
) if retry_after is not None:
delay = min(max(retry_after, retry_delay), _MAX_RETRY_DELAY)
else:
delay = min(max(delay, retry_delay), _MAX_RETRY_DELAY)
else:
delay = min(delay, _MAX_RETRY_DELAY)
if _is_rate_limit_error(e):
logger.warning(
f"Zep {page_description} rate limit hit (attempt {attempt + 1}/{max_retries}); "
f"retrying in {delay:.1f}s..."
)
else:
logger.warning(
f"Zep {page_description} attempt {attempt + 1} failed: {str(e)[:100]}, retrying in {delay:.1f}s..."
)
time.sleep(delay) time.sleep(delay)
delay *= 2 if _is_rate_limit_error(e):
# Respect server-advised retry delays; keep the same delay or back off slightly.
delay = min(delay * 1.25, _MAX_RETRY_DELAY)
else:
delay = min(delay * 2, _MAX_RETRY_DELAY)
else: else:
logger.error(f"Zep {page_description} failed after {max_retries} attempts: {str(e)}") logger.error(f"Zep {page_description} failed after {max_retries} attempts: {str(e)}")

View File

@ -8,12 +8,9 @@ import service, { requestWithRetry } from './index'
export function generateOntology(formData) { export function generateOntology(formData) {
return requestWithRetry(() => return requestWithRetry(() =>
service({ service({
url: '/api/graph/ontology/generate', url: '/graph/ontology/generate',
method: 'post', method: 'post',
data: formData, data: formData
headers: {
'Content-Type': 'multipart/form-data'
}
}) })
) )
} }
@ -26,7 +23,7 @@ export function generateOntology(formData) {
export function buildGraph(data) { export function buildGraph(data) {
return requestWithRetry(() => return requestWithRetry(() =>
service({ service({
url: '/api/graph/build', url: '/graph/build',
method: 'post', method: 'post',
data data
}) })
@ -40,7 +37,7 @@ export function buildGraph(data) {
*/ */
export function getTaskStatus(taskId) { export function getTaskStatus(taskId) {
return service({ return service({
url: `/api/graph/task/${taskId}`, url: `/graph/task/${taskId}`,
method: 'get' method: 'get'
}) })
} }
@ -52,7 +49,7 @@ export function getTaskStatus(taskId) {
*/ */
export function getGraphData(graphId) { export function getGraphData(graphId) {
return service({ return service({
url: `/api/graph/data/${graphId}`, url: `/graph/data/${graphId}`,
method: 'get' method: 'get'
}) })
} }
@ -64,7 +61,7 @@ export function getGraphData(graphId) {
*/ */
export function getProject(projectId) { export function getProject(projectId) {
return service({ return service({
url: `/api/graph/project/${projectId}`, url: `/graph/project/${projectId}`,
method: 'get' method: 'get'
}) })
} }

View File

@ -3,17 +3,19 @@ import i18n from '../i18n'
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5001', // Default to the Vite proxy so the frontend works both on localhost
timeout: 300000, // 5分钟超时本体生成可能需要较长时间 // and when accessed from another machine via the dev server host.
headers: { baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
'Content-Type': 'application/json' timeout: 300000 // 5分钟超时本体生成可能需要较长时间
}
}) })
// 请求拦截器 // 请求拦截器
service.interceptors.request.use( service.interceptors.request.use(
config => { config => {
config.headers['Accept-Language'] = i18n.global.locale.value config.headers['Accept-Language'] = i18n.global.locale.value
if (config.data instanceof FormData) {
delete config.headers['Content-Type']
}
return config return config
}, },
error => { error => {

View File

@ -5,7 +5,7 @@ import service, { requestWithRetry } from './index'
* @param {Object} data - { simulation_id, force_regenerate? } * @param {Object} data - { simulation_id, force_regenerate? }
*/ */
export const generateReport = (data) => { export const generateReport = (data) => {
return requestWithRetry(() => service.post('/api/report/generate', data), 3, 1000) return requestWithRetry(() => service.post('/report/generate', data), 3, 1000)
} }
/** /**
@ -13,7 +13,7 @@ export const generateReport = (data) => {
* @param {string} reportId * @param {string} reportId
*/ */
export const getReportStatus = (reportId) => { export const getReportStatus = (reportId) => {
return service.get(`/api/report/generate/status`, { params: { report_id: reportId } }) return service.get(`/report/generate/status`, { params: { report_id: reportId } })
} }
/** /**
@ -22,7 +22,7 @@ export const getReportStatus = (reportId) => {
* @param {number} fromLine - 从第几行开始获取 * @param {number} fromLine - 从第几行开始获取
*/ */
export const getAgentLog = (reportId, fromLine = 0) => { export const getAgentLog = (reportId, fromLine = 0) => {
return service.get(`/api/report/${reportId}/agent-log`, { params: { from_line: fromLine } }) return service.get(`/report/${reportId}/agent-log`, { params: { from_line: fromLine } })
} }
/** /**
@ -31,7 +31,7 @@ export const getAgentLog = (reportId, fromLine = 0) => {
* @param {number} fromLine - 从第几行开始获取 * @param {number} fromLine - 从第几行开始获取
*/ */
export const getConsoleLog = (reportId, fromLine = 0) => { export const getConsoleLog = (reportId, fromLine = 0) => {
return service.get(`/api/report/${reportId}/console-log`, { params: { from_line: fromLine } }) return service.get(`/report/${reportId}/console-log`, { params: { from_line: fromLine } })
} }
/** /**
@ -39,7 +39,7 @@ export const getConsoleLog = (reportId, fromLine = 0) => {
* @param {string} reportId * @param {string} reportId
*/ */
export const getReport = (reportId) => { export const getReport = (reportId) => {
return service.get(`/api/report/${reportId}`) return service.get(`/report/${reportId}`)
} }
/** /**
@ -47,5 +47,5 @@ export const getReport = (reportId) => {
* @param {Object} data - { simulation_id, message, chat_history? } * @param {Object} data - { simulation_id, message, chat_history? }
*/ */
export const chatWithReport = (data) => { export const chatWithReport = (data) => {
return requestWithRetry(() => service.post('/api/report/chat', data), 3, 1000) return requestWithRetry(() => service.post('/report/chat', data), 3, 1000)
} }

View File

@ -5,7 +5,7 @@ import service, { requestWithRetry } from './index'
* @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? } * @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? }
*/ */
export const createSimulation = (data) => { export const createSimulation = (data) => {
return requestWithRetry(() => service.post('/api/simulation/create', data), 3, 1000) return requestWithRetry(() => service.post('/simulation/create', data), 3, 1000)
} }
/** /**
@ -13,7 +13,7 @@ export const createSimulation = (data) => {
* @param {Object} data - { simulation_id, entity_types?, use_llm_for_profiles?, parallel_profile_count?, force_regenerate? } * @param {Object} data - { simulation_id, entity_types?, use_llm_for_profiles?, parallel_profile_count?, force_regenerate? }
*/ */
export const prepareSimulation = (data) => { export const prepareSimulation = (data) => {
return requestWithRetry(() => service.post('/api/simulation/prepare', data), 3, 1000) return requestWithRetry(() => service.post('/simulation/prepare', data), 3, 1000)
} }
/** /**
@ -21,7 +21,7 @@ export const prepareSimulation = (data) => {
* @param {Object} data - { task_id?, simulation_id? } * @param {Object} data - { task_id?, simulation_id? }
*/ */
export const getPrepareStatus = (data) => { export const getPrepareStatus = (data) => {
return service.post('/api/simulation/prepare/status', data) return service.post('/simulation/prepare/status', data)
} }
/** /**
@ -29,7 +29,7 @@ export const getPrepareStatus = (data) => {
* @param {string} simulationId * @param {string} simulationId
*/ */
export const getSimulation = (simulationId) => { export const getSimulation = (simulationId) => {
return service.get(`/api/simulation/${simulationId}`) return service.get(`/simulation/${simulationId}`)
} }
/** /**
@ -38,7 +38,7 @@ export const getSimulation = (simulationId) => {
* @param {string} platform - 'reddit' | 'twitter' * @param {string} platform - 'reddit' | 'twitter'
*/ */
export const getSimulationProfiles = (simulationId, platform = 'reddit') => { export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
return service.get(`/api/simulation/${simulationId}/profiles`, { params: { platform } }) return service.get(`/simulation/${simulationId}/profiles`, { params: { platform } })
} }
/** /**
@ -47,7 +47,7 @@ export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
* @param {string} platform - 'reddit' | 'twitter' * @param {string} platform - 'reddit' | 'twitter'
*/ */
export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit') => { export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit') => {
return service.get(`/api/simulation/${simulationId}/profiles/realtime`, { params: { platform } }) return service.get(`/simulation/${simulationId}/profiles/realtime`, { params: { platform } })
} }
/** /**
@ -55,7 +55,7 @@ export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit')
* @param {string} simulationId * @param {string} simulationId
*/ */
export const getSimulationConfig = (simulationId) => { export const getSimulationConfig = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/config`) return service.get(`/simulation/${simulationId}/config`)
} }
/** /**
@ -64,7 +64,7 @@ export const getSimulationConfig = (simulationId) => {
* @returns {Promise} 返回配置信息包含元数据和配置内容 * @returns {Promise} 返回配置信息包含元数据和配置内容
*/ */
export const getSimulationConfigRealtime = (simulationId) => { export const getSimulationConfigRealtime = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/config/realtime`) return service.get(`/simulation/${simulationId}/config/realtime`)
} }
/** /**
@ -73,7 +73,7 @@ export const getSimulationConfigRealtime = (simulationId) => {
*/ */
export const listSimulations = (projectId) => { export const listSimulations = (projectId) => {
const params = projectId ? { project_id: projectId } : {} const params = projectId ? { project_id: projectId } : {}
return service.get('/api/simulation/list', { params }) return service.get('/simulation/list', { params })
} }
/** /**
@ -81,7 +81,7 @@ export const listSimulations = (projectId) => {
* @param {Object} data - { simulation_id, platform?, max_rounds?, enable_graph_memory_update? } * @param {Object} data - { simulation_id, platform?, max_rounds?, enable_graph_memory_update? }
*/ */
export const startSimulation = (data) => { export const startSimulation = (data) => {
return requestWithRetry(() => service.post('/api/simulation/start', data), 3, 1000) return requestWithRetry(() => service.post('/simulation/start', data), 3, 1000)
} }
/** /**
@ -89,7 +89,7 @@ export const startSimulation = (data) => {
* @param {Object} data - { simulation_id } * @param {Object} data - { simulation_id }
*/ */
export const stopSimulation = (data) => { export const stopSimulation = (data) => {
return service.post('/api/simulation/stop', data) return service.post('/simulation/stop', data)
} }
/** /**
@ -97,7 +97,7 @@ export const stopSimulation = (data) => {
* @param {string} simulationId * @param {string} simulationId
*/ */
export const getRunStatus = (simulationId) => { export const getRunStatus = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/run-status`) return service.get(`/simulation/${simulationId}/run-status`)
} }
/** /**
@ -105,7 +105,7 @@ export const getRunStatus = (simulationId) => {
* @param {string} simulationId * @param {string} simulationId
*/ */
export const getRunStatusDetail = (simulationId) => { export const getRunStatusDetail = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/run-status/detail`) return service.get(`/simulation/${simulationId}/run-status/detail`)
} }
/** /**
@ -116,7 +116,7 @@ export const getRunStatusDetail = (simulationId) => {
* @param {number} offset - 偏移量 * @param {number} offset - 偏移量
*/ */
export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50, offset = 0) => { export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50, offset = 0) => {
return service.get(`/api/simulation/${simulationId}/posts`, { return service.get(`/simulation/${simulationId}/posts`, {
params: { platform, limit, offset } params: { platform, limit, offset }
}) })
} }
@ -132,7 +132,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
if (endRound !== null) { if (endRound !== null) {
params.end_round = endRound params.end_round = endRound
} }
return service.get(`/api/simulation/${simulationId}/timeline`, { params }) return service.get(`/simulation/${simulationId}/timeline`, { params })
} }
/** /**
@ -140,7 +140,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
* @param {string} simulationId * @param {string} simulationId
*/ */
export const getAgentStats = (simulationId) => { export const getAgentStats = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/agent-stats`) return service.get(`/simulation/${simulationId}/agent-stats`)
} }
/** /**
@ -149,7 +149,7 @@ export const getAgentStats = (simulationId) => {
* @param {Object} params - { limit, offset, platform, agent_id, round_num } * @param {Object} params - { limit, offset, platform, agent_id, round_num }
*/ */
export const getSimulationActions = (simulationId, params = {}) => { export const getSimulationActions = (simulationId, params = {}) => {
return service.get(`/api/simulation/${simulationId}/actions`, { params }) return service.get(`/simulation/${simulationId}/actions`, { params })
} }
/** /**
@ -157,7 +157,7 @@ export const getSimulationActions = (simulationId, params = {}) => {
* @param {Object} data - { simulation_id, timeout? } * @param {Object} data - { simulation_id, timeout? }
*/ */
export const closeSimulationEnv = (data) => { export const closeSimulationEnv = (data) => {
return service.post('/api/simulation/close-env', data) return service.post('/simulation/close-env', data)
} }
/** /**
@ -165,7 +165,7 @@ export const closeSimulationEnv = (data) => {
* @param {Object} data - { simulation_id } * @param {Object} data - { simulation_id }
*/ */
export const getEnvStatus = (data) => { export const getEnvStatus = (data) => {
return service.post('/api/simulation/env-status', data) return service.post('/simulation/env-status', data)
} }
/** /**
@ -173,7 +173,7 @@ export const getEnvStatus = (data) => {
* @param {Object} data - { simulation_id, interviews: [{ agent_id, prompt }] } * @param {Object} data - { simulation_id, interviews: [{ agent_id, prompt }] }
*/ */
export const interviewAgents = (data) => { export const interviewAgents = (data) => {
return requestWithRetry(() => service.post('/api/simulation/interview/batch', data), 3, 1000) return requestWithRetry(() => service.post('/simulation/interview/batch', data), 3, 1000)
} }
/** /**
@ -182,6 +182,5 @@ export const interviewAgents = (data) => {
* @param {number} limit - 返回数量限制 * @param {number} limit - 返回数量限制
*/ */
export const getSimulationHistory = (limit = 20) => { export const getSimulationHistory = (limit = 20) => {
return service.get('/api/simulation/history', { params: { limit } }) return service.get('/simulation/history', { params: { limit } })
} }

View File

@ -105,6 +105,15 @@
</div> </div>
</div> </div>
<div v-if="rateLimitNotice" class="retry-notice">
<div class="retry-notice__icon"></div>
<div class="retry-notice__body">
<div class="retry-notice__title">External API rate limit detected</div>
<div class="retry-notice__text">{{ rateLimitNotice.message }}</div>
</div>
<button class="retry-notice__dismiss" @click="dismissRetryNotice">Dismiss</button>
</div>
<div class="workflow-steps" v-if="workflowSteps.length > 0"> <div class="workflow-steps" v-if="workflowSteps.length > 0">
<div <div
v-for="(step, sidx) in workflowSteps" v-for="(step, sidx) in workflowSteps"
@ -418,6 +427,8 @@ const agentLogs = ref([])
const consoleLogs = ref([]) const consoleLogs = ref([])
const agentLogLine = ref(0) const agentLogLine = ref(0)
const consoleLogLine = ref(0) const consoleLogLine = ref(0)
const rateLimitNotice = ref(null)
let rateLimitNoticeTimer = null
const reportOutline = ref(null) const reportOutline = ref(null)
const currentSectionIndex = ref(null) const currentSectionIndex = ref(null)
const generatedSections = ref({}) const generatedSections = ref({})
@ -2017,6 +2028,52 @@ const getLogLevelClass = (log) => {
return '' return ''
} }
const clearRetryNoticeTimer = () => {
if (rateLimitNoticeTimer) {
clearTimeout(rateLimitNoticeTimer)
rateLimitNoticeTimer = null
}
}
const dismissRetryNotice = () => {
rateLimitNotice.value = null
clearRetryNoticeTimer()
}
const setRetryNotice = (message, retrySeconds = null) => {
rateLimitNotice.value = {
message,
retrySeconds
}
clearRetryNoticeTimer()
if (retrySeconds && retrySeconds > 0) {
rateLimitNoticeTimer = setTimeout(() => {
rateLimitNotice.value = null
rateLimitNoticeTimer = null
}, Math.min(retrySeconds * 1000 + 1000, 120000))
}
}
const maybeHandleRetryNotice = (logLine) => {
if (!logLine) return
const text = String(logLine)
const rateLimitMatch = text.match(/rate limit hit.*retrying in ([\d.]+)s/i)
if (rateLimitMatch) {
const retrySeconds = Number.parseFloat(rateLimitMatch[1])
const retryText = Number.isFinite(retrySeconds)
? `Zep is rate limited. The system will retry automatically in about ${retrySeconds.toFixed(1)} seconds.`
: 'Zep is rate limited. The system will retry automatically.'
setRetryNotice(retryText, Number.isFinite(retrySeconds) ? retrySeconds : null)
return
}
if (text.includes('Rate limit exceeded for FREE plan') || text.includes('429')) {
setRetryNotice('Zep is rate limited. The system will retry automatically.', null)
}
}
// Polling // Polling
let agentLogTimer = null let agentLogTimer = null
let consoleLogTimer = null let consoleLogTimer = null
@ -2055,6 +2112,7 @@ const fetchAgentLog = async () => {
if (log.action === 'report_complete') { if (log.action === 'report_complete') {
isComplete.value = true isComplete.value = true
currentSectionIndex.value = null // loading currentSectionIndex.value = null // loading
dismissRetryNotice()
emit('update-status', 'completed') emit('update-status', 'completed')
stopPolling() stopPolling()
// nextTick // nextTick
@ -2141,6 +2199,8 @@ const fetchConsoleLog = async () => {
if (newLogs.length > 0) { if (newLogs.length > 0) {
consoleLogs.value.push(...newLogs) consoleLogs.value.push(...newLogs)
consoleLogLine.value = res.data.from_line + newLogs.length consoleLogLine.value = res.data.from_line + newLogs.length
newLogs.forEach((log) => maybeHandleRetryNotice(log))
nextTick(() => { nextTick(() => {
if (logContent.value) { if (logContent.value) {
@ -2185,6 +2245,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
stopPolling() stopPolling()
clearRetryNoticeTimer()
}) })
watch(() => props.reportId, (newId) => { watch(() => props.reportId, (newId) => {
@ -2193,6 +2254,7 @@ watch(() => props.reportId, (newId) => {
consoleLogs.value = [] consoleLogs.value = []
agentLogLine.value = 0 agentLogLine.value = 0
consoleLogLine.value = 0 consoleLogLine.value = 0
dismissRetryNotice()
reportOutline.value = null reportOutline.value = null
currentSectionIndex.value = null currentSectionIndex.value = null
generatedSections.value = {} generatedSections.value = {}
@ -2289,6 +2351,64 @@ watch(() => props.reportId, (newId) => {
flex-shrink: 0; flex-shrink: 0;
} }
.retry-notice {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 12px 20px 0;
padding: 12px 14px;
border: 1px solid #F59E0B;
border-radius: 12px;
background: rgba(245, 158, 11, 0.08);
color: #92400E;
}
.retry-notice__icon {
width: 28px;
height: 28px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(245, 158, 11, 0.18);
flex-shrink: 0;
font-size: 16px;
}
.retry-notice__body {
flex: 1;
min-width: 0;
}
.retry-notice__title {
font-size: 13px;
font-weight: 700;
line-height: 1.2;
}
.retry-notice__text {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: #B45309;
}
.retry-notice__dismiss {
flex-shrink: 0;
border: 0;
border-radius: 999px;
padding: 6px 10px;
background: rgba(245, 158, 11, 0.16);
color: #92400E;
cursor: pointer;
font-size: 12px;
font-weight: 600;
}
.retry-notice__dismiss:hover {
background: rgba(245, 158, 11, 0.24);
}
/* Panel header status variants */ /* Panel header status variants */
.panel-header--active { .panel-header--active {
background: #FAFAFA; background: #FAFAFA;

View File

@ -13,7 +13,6 @@ export default defineConfig({
}, },
server: { server: {
port: 3000, port: 3000,
open: true,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:5001', target: 'http://localhost:5001',